分析
本题逻辑是先输入 addr 后输入 data ,猜测是任意地址写
由于本题去了符号表,所以需要手动找一下 main 函数,有两种方法
- 了解_start函数的结构,当调用__libc_start_main时,rdi中的参数即为main函数
- 运行程序,通过打印的字符串交叉引用找到main函数
对于64位的ELF程序,参数传递顺序是前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上
所以__libc_start_main的函数原型:
1 | __libc_start_main(main,argc,argv&env,init,fini,rtld_fini) |
对应即:
- sub_401B6D: main
- loc_402960: fini
- sub_401EB0: __libc_start_main
漏洞点
分析main函数可以看到,逻辑就是先自加一个值,当此值为1时,允许输入一个addr,然后对addr进行变换后把数据写进去,看这个变换没看懂,但是看别人的wp上有的有函数名,叫 strtol ,百度可得是将字符串转化为整型的函数。
我们也可以通过动态调试来进行判断
rax的值为0x4d2,即十六进制表示的1234
所以这道题的漏洞点就是任意地址写,最多0x18个字节。
利用
前置知识:main函数的启动过程
__libc_start_main的参数中,除了main,还有init和fini,这俩其实就是两个函数的地址,分别是:__libc_csu_fini(sub_402960),__libc_csu_init(loc_4028D0)
csu是啥意思?What does CSU in glibc stand for,即 “C start up”
我们在IDA的view -> open subviews -> segments 中可以看到如下四个段
- .init
- .init_array
- .fini
- .fini_array
点进去即可看到.init和.fini是可执行的段,是代码,是函数。而.init_array和.fini_array是数组,里面存着函数的地址,这两个数组里的函数由谁来执行呢?
其实就是:__libc_csu_fini和__libc_csu_init
- __libc_csu_init执行.init和.init_array
- __libc_csu_fini执行.fini和.fini_array
执行顺序如下:
- .init
- .init_array[0]
- .init_array[1]
- …
- .init_array[n]
- main
- .fini_array[n]
- …
- .fini_array[1]
- .fini_array[0]
- .fini
一次写变多次写
这题中.fini_array中有两个函数,所以我们可以知道函数的执行顺序是 main -> __libc_csu_fini -> .fini_array[1] -> .fini_array[0]
所以我们可以通过覆盖.fini_array[1]来执行我们想要的代码,但是本题中并没有后门函数,所以我们需要多次写入构建ROP
我们如果把.fini_array[1]覆盖成main,把 .fini_array[0]覆盖成 __libc_csu_fini,就可以实现无限循环从而达成多次任意地址写,main函数中虽然存在一个全局变量,而且需要为1时我们才能进行写入,但是没有关系,因为这只是一个8bit的整型,所以我们改写之后在疯狂加一的情况下一会就溢出了,我们还是可以实现多次写
栈迁移
我们已经实现了任意地址多次写,并控制了rip,但是程序中没有可写可执行的代码段,无法执行shellcode,我们也并不知道栈的位置,无法实现ROP,所以我们需要布置好栈的位置,然后在某一时刻把rsp修改到那个地方,就可以实现ROP了
在__libc_csu_fini函数,也就是题目中的sub_402960函数中,调用方式是这样的
1 | .text:0000000000402960 push rbp |
可见在这个函数中rbp之前的值暂时被放到栈里了,然后将rbp当做通用寄存器去存放了一个固定的值0x4b40f0,然后就去调用了fini_array的函数,call之后的指令我们就可控了,我们可以劫持RIP到任何地方。考虑如下情况:
1 | lea rbp, off_4B40F0 ; rbp = 0x4b40f0 , rsp = 未知 |
则rsp被劫持到0x4b4100,rip和rbp分别为.fini_array[1]和.fini_array[0]的内容:
1 | low addr 0x4b40f0 +----------------+ |
则我们可以在0x4b4100的地址向上布置rop链,只要rip指向的位置的代码不会破坏高地址栈结构,然后还有个ret指令,我们就可以实现ROP了
所以我们需要完成的事情如下:
- 布置好从0x4b4100开始的栈空间(利用任意地址写)
- 保证.fini_array[1]指向的代码不破坏栈结构,还有个ret,或者直接就一句ret也行
- 通过上文类似的方法劫持rsp到0x4b4100,即可触发ROP
- 第一件事情虽然是要最先做的,但ROP是最后要执行的,所以一会在讨论。
- 第二件事情,任何一开头形如push rbp;mov rbp,rsp的正常函数都满足要求。当我们已经实现了多次任意地址写之后,这个位置是main函数,满足要求。
- 第三件事情,在main函数的结尾我们可以看到汇编
leave;retn;
leave相当于mov rsp,rbp;pop rbp
,所以我们可以把.fini_array[0]指向main函数的结尾处,即```0x401C4B``,即可劫持rsp到0x4b4100。而且当我们写入这个地址不再是__libc_csu_fini,便可中断循环。rip指向.fini_array[1],虽仍然是main函数,但因为不会疯狂加一,函数会立即返回并触发ROP。
注:retn(return near,不恢复cs) retf(return far,恢复cs)
ROP
我们最终的目的是执行execve这个系统调用从而get shell
但是需要注意的是64位和32位在传递参数和调用系统调用的时候都是有区别的:
- 首先查到execve在64位的上的系统调用号是0x3b,所以要控制rax为0x3b
- 控制rdi为”/bin/sh\x00”的地址
- 控制rsi和rdx均为0
- 64位下系统调用的指令为syscall而不是int 80
所以rop链布置如下
1 | pop_rax |
ROP链常见形式:[pop register]+[value],即参数的值在后,ret指令在前
利用ROPgadget找到相应的地址
1 | ROPgadget --binary 3x17 --only 'pop|ret' | grep "pop rax" |
exp
1 | from pwn import * |
参考: