通用Gadget
在x64下进行pwn时,由于参数的传递是通过寄存器来传递的,所以需要寻找gadget。有时用一些工具比如ROPgadget,使用下面的命令:
来寻找gadget。但有时候找不到可用的gadget。
但在x64的环境下,程序有调用libc.so的话,一般会有一个__libc_csu_init()函数。利用里面的gadget可以达到向函数进行传参的功能。这就是通用gadget。
以蒸米师傅的《一步一步学ROP之linux_x64篇》为例研究一下。其源代码level5.c如下:
用objdump查看,习惯intel语法了所以这里加了个 -Mintel
其中__libc_csu_init函数:
在《一步一步学ROP之linux_x64篇》中,利用该通用gadget的思路如下:
我们可以看到利用0x400606处的代码我们可以控制rbx,rbp,r12,r13,r14和r15的值,随后利用0x4005f0处的代码我们将r15的值赋值给rdx, r14的值赋值给rsi,r13的值赋值给edi,随后就会调用call qword ptr [r12+rbx8]。这时候我们只要再将rbx的值赋值为0,再通过精心构造栈上的数据,我们就可以控制pc去调用我们想要调用的函数了(比如说write函数)。执行完call qword ptr [r12+rbx8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址。所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。
不过具体是怎样的一个流程,可能光看这段话不太清楚。我们根据蒸米师傅的payload1来调试一下整个ROP的过程。
ROP
蒸米师傅的第一个payload,是要利用write()输出在内存中的地址,即write(1, write.got, 8)
。根据64位由寄存器传参,我们要利用rop做到以下几件事:
- 设置
rdi
为1
,这是第一个参数 - 设置
rsi
为 write的got地址即write.got,这是第二个参数 - 设置
rdx
为 8,这是第三个参数 - 成功的调用write()函数
现在利用脚本具体如下:
其中log.info(proc.pidof(p)[0])用来输出process的pid,用于gdb的attach。第一个raw_input()是在发送payload之前attach上去,这样能具体的观察到发送前后内存的bain话,最后一行raw_input()是在发送payload后挂住脚本防止level5直接退出。
如图所示,进程的PID号为4597.用gdb命令gdb attach 4597
后即可附加到进程上。在py脚本中按一下回车,这时会执行p.send(payload1)。接下来正式开始调试,以下命令均在gdb中进行。
第一阶段
接下来我们开始正式的第一阶段的ROP
payload:
此时程序执行到read(STDIN_FILENO, buf, 512),我们按n
后,程序执行完read,将我们的payload读到了栈上。我们输入了136个0
,然后是通用gadget的起始位置0x400606
。
接下去继续输入n
,当程序执行将到0x400563 <vulnerable_function+31>: ret
时,可以观察到此时栈顶为0x400606
。
继续输入n
,程序跳到通用gadget里,开始正式的rop。
根据我们的payload:p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) + p64(0x4005F0),结合上图的stack分析,可知此时的栈中数据分布如下(小端序):
起点 | +0 | +1 | +2 | +3 | +4 | +5 | +6 | +7 | 对应数据 |
---|---|---|---|---|---|---|---|---|---|
rsp | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 0x0 |
rsp+8 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 0x0 |
rsp+16 | 01 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 0x1 |
rsp+24 | 00 | 10 | 60 | 00 | 00 | 00 | 00 | 00 | 0x601000 |
rsp+32 | 01 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 0x1 |
rsp+40 | 00 | 10 | 60 | 00 | 00 | 00 | 00 | 00 | 0x601000 |
rsp+48 | 08 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 0x8 |
rsp+56 | f0 | 05 | 40 | 00 | 00 | 00 | 00 | 00 | 0x4005f0 |
此时各寄存器的值如下:
我们继续按n
,程序执行完mov rbx,QWORD PTR [rsp+0x8]
,将rsp+0x8
也即rsp+8
地址处的数据以QWORD形式即8字节赋值给rbx,执行完后,rbx将为0。所以这里有一点要注意的,我们的payload开头是这样的:p64(0) +p64(0) + p64(1) + ……,第一个p64(0),是起一个padding的作用,用来填充rsp到rsp+0x7之间的数据的。
(因为之前调用的read和write的第一个参数就是STDIN_FILENO,值为0,所以这里rbx原本就是0,所以可能感觉没啥变化,其实就是用0赋值给了一个原本就是0的rbx)
接下去按n
,执行mov rbp,QWORD PTR [rsp+0x10]
,将rsp+0x10
也即rsp+16
地址处的数据以QWORD形式即8字节赋值给rbp。
接下去一直按n
,程序依次执行mov操作,直至add rsp,0x38
,此时寄存器情况如下:
几个重要的寄存器值列举如下:
- RBX: 0x0
- RBP: 0x1
- R12: 0x601000
- R13: 0x1
- R14: 0x601000
- R15: 0x8
此时栈中数据仍然如前:
接下去我们输入n
,程序执行完add rsp,0x38
,即将栈顶加上0x38,也即加上56,这相当于是降低了栈顶(栈从低地址向高地址增长)。接下去程序将会执行ret
指令,而此时的栈,注意是此时的栈,其内容如下:
接下去输入n
,将会执行ret
指令,相当于pop rip
,也就是说执行完ret
,将会跳转到栈顶所指的地址0x4005f0
第二阶段
接下来算是进入ROP的第二阶段。
这时的各寄存器的值如下图,
在经过三个mov
操作后mov rdx,r15
,mov rsi,r14
,mov edi,r13d
,我们成功地把进行write函数调用所需的三个参数都布置好了。
寄存器数据如下:
接下去会执行call QWORD PTR [r12+rbx*8]
,而我们已经将rbx置为0,r12置为write的got地址,所以执行这句语句,其实就是在调用write()函数。用如下命令可以查看r12+rbx*8
处具体的值。
第三阶段
在call指令执行完了后,即调用完write()函数后,我们进入ROP的第三阶段,收尾阶段。毕竟调用完后不能让程序崩溃啊。这时候对应的payload是
此时寄存器中:
这是之前第一阶段中,我们设置的,RBX = 0x0,RBP = 0x1。将会进行如下三条指令:
将rbp
加1,得到0x01,然后与值为0x01的rbp进行比较,jne
指令说明如果两者比较相等则不会进行跳转,所以这里不会执行跳转,而是继续执行。
接下来“回”到了第一阶段的代码,此时的栈中的情况如下:
我们会执行六次的mov
操作,但我们此时不需要再布置参数,所以可以说不用管:)。
接下来,会将rsp加上0x38,即加上56。结合栈的情况,这次add操作,将会将main函数的地址作为栈顶。
所以这一阶段的payload的构成是56个\x00
,紧接是准备回到某个位置的内存地址。在执行完ret后,程序将回到mian函数中,以便我们进行下一次的利用。
小结
综合对三个阶段的分析,在64位程序栈溢出时若用了__libc_csu_init中的gadget,其payload组成如下
利用
现在我们利用这个通用ROP,写一个exp。大体思路是,利用DynELF借write()函数泄露出system的地址。但要注意几点,得到system的地址后,不能直接作为call_func_got_addr,而应该先写到bss段,再填入bss段的地址。所以下面的payload中,bss_addr到bss_addr+7,保存着system的地址,bss_addr+8到bss_addr+15保存着字符串“/bin/sh\x00”。
|
|