ROP-ret2__libc_csu_init(64位ELF)
前言
随记
示例文件
64位传参(函数调用约定)
首先说一下64位文件的传参方式:
当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。
当参数为7个以上时, 前 6 个与前面一样, 但后面的依次 放入栈中,即和32位程序一样
1 | 比如参数大于7个的时候 |
利用原理
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,我们很难找到每一个寄存器对应的gadgets。
这时候,我们可以利用 x64 下的 __libc_csu_init
中的 gadgets。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。
我们先来看一下这个函数(当然,不同版本的这个函数有一定的区别),将程序扔到IDA中,其汇编代码如下:
这里我们可以利用以下几点:
从 0x000000000040061A 一直到结尾,我们可以利用栈溢出构造栈上数据来控制 rbx,rbp,r12,r13,r14,r15 寄存器的数据(因为都是向寄存器进行pop)。对应的汇编如下:
从 0x0000000000400600
到 0x0000000000400609
,我们可以将 r13 赋给 rdx
,将 r14 赋给 rsi
,将 r15 赋给 edi
需要注意的是,虽然这里赋给的是 edi,但其实此时 rdi 的高 32 位寄存器值为 0(自行调试),所以其实我们可以控制 rdi 寄存器的值,只不过只能控制低 32 位
而这三个寄存器,也是 x64 函数调用中传递的前三个寄存器(rdx、rsi、edi)。此外,如果我们可以合理地控制 r12 与 rbx,那么我们就可以调用我们想要调用的函数。比如说我们可以控制 rbx 为 0,r12 为存储我们想要调用的函数的地址。对应的汇编如下:
从 0x000000000040060D
到 0x0000000000400614
,我们可以控制 rbx 与 rbp 之间的关系为rbx+1 = rbp,这样我们就不会执行 loc_400600,进而可以继续执行下面的汇编程序。这里我们可以简单的设置rbx=0,rbp=1。对应的汇编代码如下:
分析到这里,差不多可以进入正题了。
做题
64位程序,开了NX保护,main函数如下:
进入 vulnerable_function 函数:
发现一个read函数,这是一个简单的栈溢出函数,
ssize_t read(int fd,void*buf,size_t count)
参数说明:
fd: 是文件描述符
buf:为读出数据的缓冲区;
count:为每次读取的字节数(是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移)
成功:返回读出的字节数
失败:返回-1,并设置errno,如果在调用read
之前到达文件末尾,则这次read返回0
首先在IDA里看一下read函数的栈偏移(覆盖返回地址):0x88,为了防止IDA不准确导致错误,因此手动测量一下:
利用cyclic工具生成不规则字符串(仔细看挺规则的)
pwngdb开始调试,执行如下操作:
利用rsp寄存器找偏移:
x/wx $rsp # 以16进制显示指定rsp寄存器中的数据
栈偏移为136,与IDA测的0x88相等
从IDA里可以看到,没有system函数,但有一个已知的write函数,我们可以利用这个函数并利用libc泄露出程序加载到内存后的地址(当然也可以选用__libc_start_main)。
首先寻找write函数在内存中的真实地址:
1 | from pwn import * |
解释一下这里的payload0
1 | payload0 = 'A'*136 + p64(pop_addr) + p64(0) + p64(1) + p64(write_got) + p64(8) + p64(write_got) + p64(1) + p64(mov_addr) + b'a'*(0x8+8*6) + p64(main_addr) |
首先输入136个字符使程序发生栈溢出,然后让pop_addr覆盖栈中的返回地址,使程序执行pop_addr地址处的函数,并分别将栈中的0、1、write_got函数地址、8、write_got、1分别pop到寄存器rbx、rbp、r12、r13、r14、r15中去,之后将pop函数的返回地址覆盖mov_addr的地址为,如下:
注意:payload发送的内容都在栈上或堆上,这得看你的变量在栈上还是堆上,具体情况具体分析
解释一下payload中两个write_got函数的作用:
在布置完寄存器后,由于有 call qword ptr [r12+rbx*8]
它调用了write函数,其参数为write_got函数地址(r14寄存器,动调一下就知道了),写成C语言类似于:write(write_got函数地址)==printf(write_got函数地址),再使用u64(p.recv(8))接受数据并print出来就行了
之后程序转向mov_addr函数,利用mov指令布置寄存器rdx,rsi,edi
JNZ(或JNE)(jump if not zero, or not equal),汇编语言中的条件转移指令。结果不为零(或不相等)则转移。
这里rbx和rbp都等于1,他们相等,所以继续执行payload代码(main_addr),而不是去执行loc_400600
从整体上来看,我们输入了 ‘A’136,利用payload0对寄存器布局之后又重新回到了main函数再说说’a’(0x8+8*6)的作用:它的作用就是为了平衡堆栈也就是说,当mov_addr执行完之后,按照流程仍然会执行地址400616处的函数,我们并不希望它执行到这个函数(因为他会再次pop寄存器更换我们布置好的内容),所以为了堆栈平衡,我们使用垃圾数据填充此处的代码(栈区和代码区同属于内存区域,可以被填充),如下图所示:
用垃圾数据填充地址0x16-0x22的内容,最后将main_addr覆盖ret,从而执行main_addr处的内容
这道题目我们使用系统中自带的libc.so.6文件
请注意:当程序加载的时候会寻找同目录下的libc.so.6文件,如果存在,则会自动加载,而不会去加载系统自带的libc文件
这样,我们就获得了write函数真实地址。
由此,第二部分EXP如下
1 | libc = ELF('/usr/lib/x86_64-linux-gnu/libc.so.6') |
当我们获得write函数的真实地址之后,就可以计算出libc文件的基址,从而可以计算出system函数和/bin/sh字符串在内存中的地址,从而利用它。
接下来解释一下第二个payload的意思:
1 | payload=b'a'*0x88+p64(pop_rdi_ret)+p64(binsh)+p64(system_addr) |
当程序重新执行到main函数时,我们利用栈溢出让返回地址被pop_rdi_ret覆盖,从而程序执行pop_rdi_ret。
注意,当我们send payload之后,pop_rdi_ret、binsh和system_addr被送到了栈中,利用gadgets:pop rdi;ret 将栈中的binsh地址送往rdi寄存器中(也就是说pop_rdi_ret的参数是地址binsh),然后将system函数地址覆盖到ret,程序就会执行此system函数。
当system函数执行的时候会利用到rdi里的参数,动态调试一下就知道了。
完整的exp如下:
1 | from pwn import * |
参考资料
https://cloud.tencent.com/developer/article/1345756;
https://blog.csdn.net/king_cpp_py/article/details/79483152;
https://www.yuque.com/u239977/cbzkn3/vty6x0;