Linux x64平台上的二进制ROP分析学习,目标文件附件中。文件开启了NX保护,使用ROP技术实现溢出绕过。
IDA 反汇编查看文件如下:
进入vuln函数后 栈开辟 rsp,40h大小,gets函数读入数据到rdi指向的空间,而此时rdi是指向栈顶,如果不遇到换行符或0 gets将一直读入数据到栈上。所以这是一个经典的栈溢出。下面开始分析学习。
使用pwn工具检查ELF文件开启的保护,代码如下:
文件开启了NX保护,因为堆栈开辟了0x40大小,所以我用40个a覆盖这个40字节大小,用8字节b覆盖rbp,后面用8字节c覆盖返回地址。函数返回时会跳转到ccc处执行,程序崩溃。
因为有NX保护所以不能再堆栈上执行,这里需要迂回利用即ROP技术得到shell。
如图所示,凡是可执行的空间不可写,凡是可写的空间不能执行。但是ELF内存中加载了libc库,这个库里面有system函数,执行system(‘/bin/sh’)同样可以获得SHELL。
问题一:ELF虽然没有开启PIE(内存地址随机化),其内存加载基地址虽然是固定的,但是ELF中并没有明确调用system函数,所以我们无法直接拿system函数用。
解答:通过观察rop文件发现其中有一个printf函数,打印来自终端输入的字符,我们控制printf函数打印system函数地址。
1.ROP链构造打印system函数地址
研究怎么控制rsi的值,现在我们能控制函数的返回值地址,那么只要跳转到一个代码片段给rsi赋值后返回,然后在跳转到printf处执行即可(这里就是获得gadget)。已有现成的工具来搜索这样的代码片段了 可网上搜索查找,这里使用IDA搜索本地pop rsi。如下:
堆栈里布置数据,先执行 0x40075A处的代码,然后跳转到 0x400740处去执行,这样就可以控制r12,edi,rsi。然后执行call的时候让call地址变成printf got表里的地址,这样就可以打印任意地址指向的内存值。
1.返回值地址处用0x40075A覆盖
2.因为call执行完后 要判断rbx rbp是否相等。所以这里让rbx为0,rbp为1就绕过了循环判断。
3.使用printf的got地址填充,这样pop r12, rbx为0 然后call调用printf函数
4.r13 并没有怎么使用所以这里填充0
5.r14 传给了 rsi 这里是我们要打印的地址,这里打印printf got表地址指向的内容,所以使用printf got地址填充
6.r15 传给了 edi,对printf函数参数来说他是一个格式化串,这里只是只用文件中的字串0x400784地址填充
7.retun指向后要返回到rsp指向的空间,这里因为要跳转到0x400740处去执行call,所以使用0x400740覆盖
8.Call执行完后,还要执行一个“add rsp,8”和6次pop,那么我们在后面再布置7个地址,然后返回地址用vuln地址,继续跳转到漏洞函数处。7个0和0x400656
注意这里有个问题:这堆栈里面 r12 和 r13并没有按照我们原本想象的那样填充。因为gets函数遇到换行符就结束了后面的内容不在读取。然而printf_got_addr = 0x600af0,这个数据中刚好有个0x0a(换行符内存中的值)
解决办法:不用直接传0x600af0,我们可以配合rbx*8 变换 然后r12变成其他的值即可。r12 = 0x600af0-rbx*8 如果要改变到0x0a 则需要rbx*8 > 0xF0 ----> rbx > 0xF0/8 == rbx > 0x1e,因为后面有个判断 rbp = rbx + 1 所以这里让rbx = 0x1f.
修改代码继续打印system地址:
后面我使用IDA 附加程序找到system函数地址和gets函数地址计算两个地址之间的偏移:
我电脑上计算的偏移为:0x28DE0
system函数在libc中的地址为 0x7fafa2911370 - 0x28DE0
64位汇编
当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样
参数个数大于 7 个的时候
H(a, b, c, d, e, f, g, h);
a->%rdi, b->%rsi, c->%rdx, d->%rcx, e->%r8, f->%r9
h->8(%esp)
g->(%esp)
call H
2.ROP链构造执行system函数
得到system函数地址后,接下来就是执行system。那么x64系统下参数是存放在寄存器上,第一个参数地址放在rdi寄存器里。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!