阿里 CTF 2026 逆向部分 WP

逆向三道题出了两道,另一道纯恶心人来的 发现晚上出 flag 概率比白天高 =w=

pixelflow

初步分析

找到 global-metadata.datGameAssembly.dllil2cppdumper 解包 但是在 Assembly-CSharp.dll 没看到什么东西 于是开始分析 GameAssembly.dll,参考 普通PC端IL2Cpp Unity游戏逆向方法体代码记录

反编译结果分析:

Controller___Check_b__18_0_d__MoveNext 是输入校验和提取字节,仅当输入文本 以长度为 7 的固定前缀开头,并 以长度为 1 的固定后缀结尾 才继续,截取中间部分:Substring(7, len-8),要求长度恰好 16

Controller___Check_b__18_1_d__MoveNext 新建 Texture2D(16,1, format=63),用 Color32 写入:R=byte[i], G=0, B=0, A=255SetPixels32 + Apply,然后 Graphics.CopyTextureTexF,说明 K0 输出的 16 字节正是 TexFR 通道

Controller___Check_b__18_2_d__MoveNext 新建 ComputeBuffer(16,4),命名 Buffer0,绑定到 Shader0Buffer0,绑定 TexFShader0TexF,设置 K2 常量(4 个 float),调用 Dispatch(Shader0, K0, 1,1,1),说明 K0 是加密的线程组数

Controller___Check_b__18_3_d__MoveNext 清空 Buffer0Dispatch(Shader0, K1, 1,1,1),若输入无效(bytes 为空),直接把 Buffer0[0]=1(失败标志)

Controller__Check_d__18__MoveNext 取输入并转 16 字节上传到 TexF,连续运行 3 次 runEncryptAsyn,运行 runCheckAsync,若输入无效:直接置 Buffer0[0]=1

纹理数据

AssetRipper 找纹理数据,在 sharedassets0.assets 里有个 ComputeShader 类的 Shader,Yaml 中找到 K0 K1 K2code,先存成 .bxdc 文件然后用 HLSLDecompiler 反编译成 hlsl 文件

K0.hlsl

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
// ---- Created with 3Dmigoto v1.2.45 on Sun Feb  1 01:10:35 2026
Texture2D<float4> t0 : register(t0);




// 3Dmigoto declarations
#define cmp -
Texture1D<float4> IniParams : register(t120);
Texture2D<float4> StereoParams : register(t125);


void main()
{
// Needs manual fix for instruction:
// unknown dcl_: dcl_uav_typed_texture2d (float,float,float,float) u0
float4 r0,r1,r2,r3,r4;
uint4 bitmask, uiDest;
float4 fDest;

float4 x0[16];
float4 x1[32];
// Needs manual fix for instruction:
// unknown dcl_: dcl_thread_group 1, 1, 1
x1[0].x = 0;
x1[1].x = 0;
x1[2].x = 0;
x1[3].x = 0;
x1[4].x = 0;
x1[5].x = 0;
x1[6].x = 0;
x1[7].x = 0;
x1[8].x = 0;
x1[9].x = 0;
x1[10].x = 0;
x1[11].x = 0;
x1[12].x = 0;
x1[13].x = 0;
x1[14].x = 0;
x1[15].x = 0;
x1[16].x = 0;
x1[17].x = 0;
x1[18].x = 0;
x1[19].x = 0;
x1[20].x = 0;
x1[21].x = 0;
x1[22].x = 0;
x1[23].x = 0;
x1[24].x = 0;
x1[25].x = 0;
x1[26].x = 0;
x1[27].x = 0;
x1[28].x = 0;
x1[29].x = 0;
x1[30].x = 0;
x1[31].x = 0;
x0[0].x = 0;
x0[1].x = 0;
x0[2].x = 0;
x0[3].x = 0;
x0[4].x = 0;
x0[5].x = 0;
x0[6].x = 0;
x0[7].x = 0;
x0[8].x = 0;
x0[9].x = 0;
x0[10].x = 0;
x0[11].x = 0;
x0[12].x = 0;
x0[13].x = 0;
x0[14].x = 0;
x0[15].x = 0;
t0.GetDimensions(0, uiDest.x, uiDest.y, uiDest.z);
r0.x = uiDest.x;
r1.yzw = float3(0,0,0);
r2.yz = float2(0,-nan);
r3.xz = float2(0,0);
r0.yz = float2(0,0);
while (true) {
r0.w = cmp((uint)r0.z >= 1024);
if (r0.w != 0) break;
r0.w = cmp((uint)r3.x >= (uint)r0.x);
r0.w = (int)r0.w | (int)r3.z;
if (r0.w != 0) {
break;
}
r2.x = r3.x;
r4.xyzw = t0.Load(r2.xyy).xyzw;
r4.xyzw = float4(255,255,255,255) * r4.xyzw;
r4.xyzw = (uint4)r4.xyzw;
switch (r4.x) {
case 0 : x1[r4.y+0].x = r4.w;
r3.x = (int)r3.x + 1;
break;
case 1 : r0.w = x1[r4.z+0].x;
r1.x = (int)r0.w & 15;
// No code for instruction (needs manual fix):
ld_uav_typed_indexable(texture2d)(float,float,float,float) r0.w, r1.xyzw, u0.yzwx
r0.w = 255 * r0.w;
r0.w = round(r0.w);
r0.w = (uint)r0.w;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 2 : r0.w = x1[r4.y+0].x;
r1.x = x1[r4.z+0].x;
r0.w = (int)r0.w ^ (int)r1.x;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 3 : r0.w = x1[r4.y+0].x;
r1.x = x1[r4.z+0].x;
r1.x = (int)r1.x & 7;
r2.w = (int)r0.w & 255;
bitmask.w = ((~(-1 << 8)) << r1.x) & 0xffffffff; r0.w = (((uint)r0.w << r1.x) & bitmask.w) | ((uint)0 & ~bitmask.w);
r1.x = (int)-r1.x + 8;
r1.x = (uint)r2.w >> (uint)r1.x;
r0.w = (int)r0.w | (int)r1.x;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 4 : r0.w = x1[r4.z+0].x;
r1.x = (int)r4.w & 255;
r0.w = (int)r0.w * (int)r1.x;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 5 : r0.w = x1[r4.z+0].x;
r1.x = (int)r4.w & 255;
r0.w = (int)r0.w + (int)r1.x;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 6 : r0.w = x1[r4.y+0].x;
r1.x = x1[r4.z+0].x;
r0.w = (int)r0.w + (int)r1.x;
r0.w = (int)r0.w & 255;
x1[r4.y+0].x = r0.w;
r3.x = (int)r3.x + 1;
break;
case 7 : r0.w = x1[r4.y+0].x;
r0.w = (int)r0.w & 15;
r1.x = x1[r4.z+0].x;
r1.x = (int)r1.x & 255;
x0[r0.w+0].x = r1.x;
r3.x = (int)r3.x + 1;
break;
case 8 : r0.w = x1[r4.y+0].x;
r1.x = (int)r4.w & 255;
r0.y = cmp((uint)r0.w < (uint)r1.x);
r3.x = (int)r3.x + 1;
break;
case 9 : r0.w = (int)r4.w & 128;
r1.x = (int)r4.w + -256;
r0.w = r0.w ? r1.x : r4.w;
r3.y = (int)r0.w + (int)r3.x;
r0.w = cmp((int)r3.y < 0);
r1.x = cmp((int)r3.y >= (int)r0.x);
r0.w = (int)r0.w | (int)r1.x;
r2.xw = r0.ww ? r2.xz : r3.yz;
r3.x = (int)r3.x + 1;
r3.xz = r0.yy ? r2.xw : r3.xz;
break;
default :
r3.z = -1;
break;
}
r0.z = (int)r0.z + 1;
}
r0.x = x0[0].x;
r0.y = x0[1].x;
r0.z = x0[2].x;
r0.w = x0[3].x;
r1.x = x0[4].x;
r1.y = x0[5].x;
r1.z = x0[6].x;
r1.w = x0[7].x;
r2.x = x0[8].x;
r2.y = x0[9].x;
r2.z = x0[10].x;
r2.w = x0[11].x;
r3.x = x0[12].x;
r3.y = x0[13].x;
r3.z = x0[14].x;
r3.w = x0[15].x;
r0.x = (uint)r0.x;
r4.x = 0.00392156886 * r0.x;
r4.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(0,0,0,0), r4.xyzw
r0.x = (uint)r0.y;
r4.x = 0.00392156886 * r0.x;
r4.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(1,0,0,0), r4.xyzw
r0.x = (uint)r0.z;
r4.x = 0.00392156886 * r0.x;
r4.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(2,0,0,0), r4.xyzw
r0.x = (uint)r0.w;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(3,0,0,0), r0.xyzw
r0.x = (uint)r1.x;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(4,0,0,0), r0.xyzw
r0.x = (uint)r1.y;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(5,0,0,0), r0.xyzw
r0.x = (uint)r1.z;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(6,0,0,0), r0.xyzw
r0.x = (uint)r1.w;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(7,0,0,0), r0.xyzw
r0.x = (uint)r2.x;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(8,0,0,0), r0.xyzw
r0.x = (uint)r2.y;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(9,0,0,0), r0.xyzw
r0.x = (uint)r2.z;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(10,0,0,0), r0.xyzw
r0.x = (uint)r2.w;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(11,0,0,0), r0.xyzw
r0.x = (uint)r3.x;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(12,0,0,0), r0.xyzw
r0.x = (uint)r3.y;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(13,0,0,0), r0.xyzw
r0.x = (uint)r3.z;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(14,0,0,0), r0.xyzw
r0.x = (uint)r3.w;
r0.x = 0.00392156886 * r0.x;
r0.yzw = float3(0,0,1);
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, l(15,0,0,0), r0.xyzw
return;
}

K1.hlsl

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
// ---- Created with 3Dmigoto v1.2.45 on Sun Feb  1 01:10:41 2026
Texture2D<float4> t0 : register(t0);




// 3Dmigoto declarations
#define cmp -
Texture1D<float4> IniParams : register(t120);
Texture2D<float4> StereoParams : register(t125);


void main()
{
// Needs manual fix for instruction:
// unknown dcl_: dcl_uav_typed_texture2d (float,float,float,float) u0
// Needs manual fix for instruction:
// unknown dcl_: dcl_uav_structured u1, 4
float4 r0,r1;
uint4 bitmask, uiDest;
float4 fDest;

// Needs manual fix for instruction:
// unknown dcl_: dcl_thread_group 16, 1, 1
r0.x = vThreadID.x;
r0.yzw = float3(0,0,0);
// No code for instruction (needs manual fix):
ld_uav_typed_indexable(texture2d)(float,float,float,float) r1.x, r0.xwww, u0.xyzw
r1.x = 255 * r1.x;
r1.x = round(r1.x);
r1.x = (uint)r1.x;
r1.x = (int)r1.x & 255;
r1.x = (int)r1.x + (int)vThreadID.x;
r1.x = (int)r1.x & 255;
r0.x = t0.Load(r0.xyz).x;
r0.x = 255 * r0.x;
r0.x = round(r0.x);
r0.x = (uint)r0.x;
r0.x = (int)r0.x & 255;
r0.x = cmp((int)r0.x != (int)r1.x);
if (r0.x != 0) {
t0[0].0 = u1.x;
}
return;
}

K2.hlsl

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
// ---- Created with 3Dmigoto v1.2.45 on Sun Feb  1 01:10:44 2026
Texture2D<float4> t1 : register(t1);

Texture2D<float4> t0 : register(t0);

SamplerState s0_s : register(s0);

cbuffer cb0 : register(b0)
{
float4 cb0[2];
}




// 3Dmigoto declarations
#define cmp -
Texture1D<float4> IniParams : register(t120);
Texture2D<float4> StereoParams : register(t125);


void main()
{
// Needs manual fix for instruction:
// unknown dcl_: dcl_uav_typed_texture2d (float,float,float,float) u0
// Needs manual fix for instruction:
// unknown dcl_: dcl_uav_structured u1, 4
float4 r0,r1,r2,r3;
uint4 bitmask, uiDest;
float4 fDest;

// Needs manual fix for instruction:
// unknown dcl_: dcl_thread_group 8, 8, 1
u0.GetDimensions(0, uiDest.x, uiDest.y, uiDest.z);
r0.xy = uiDest.xy;
r0.zw = cmp((uint2)vThreadID.xy >= (uint2)r0.xy);
r0.z = (int)r0.w | (int)r0.z;
if (r0.z != 0) {
return;
}
r0.zw = (uint2)vThreadID.yx;
r0.xy = (uint2)r0.yx;
r0.xy = r0.zw / r0.xy;
r0.xy = r0.xy * cb0[0].wz + float2(-0.5,-0.5);
t0.GetDimensions(0, uiDest.x, uiDest.y, uiDest.z);
r0.zw = uiDest.xy;
r0.zw = (uint2)r0.wz;
r0.z = r0.z / r0.w;
sincos(cb0[1].x, r1.x, r2.x);
r1.xy = r1.xx * r0.xy;
r3.x = r0.y * r2.x + -r1.x;
r0.y = r0.x * r2.x + r1.y;
r3.y = r0.y / r0.z;
r0.yz = float2(0.5,0.5) + r3.xy;
// Missing reflection info for shader. No names possible.
// Known bad code for instruction (needs manual fix):
ld_structured_indexable(structured_buffer, stride=4)(mixed,mixed,mixed,mixed) r0.w, l(0), l(0), u1.xxxx
r0.w = no_StructuredBufferName[no_srcAddressRegister].no_srcByteOffsetName.swiz;
r1.x = cmp((int)r0.w == 1);
if (r0.w == 0) {
r1.yz = cb0[0].xy * cb0[1].yy + r0.yz;
r2.xyzw = t0.SampleLevel(s0_s, r1.yz, 0).xyzw;
} else {
r2.xyzw = float4(0,0,0,1);
}
if (r1.x != 0) {
r0.yz = -cb0[0].xy * cb0[1].yy + r0.yz;
r2.xyzw = t1.SampleLevel(s0_s, r0.yz, 0).xyzw;
}
r1.xyzw = float4(0,0,0,1) + -r2.xyzw;
r0.xyzw = r0.xxxx * r1.xyzw + r2.xyzw;
// No code for instruction (needs manual fix):
store_uav_typed u0.xyzw, vThreadID.xyyy, r0.xyzw
return;
}

主要逻辑

K1 的核心逻辑: 1. 读取 u0[x](来自 K0 输出纹理) 2. 计算 u0[x] + x(按 8-bit 截断) 3. 读取 t0[x](来自 ciTex 纹理) 4. 若不相等则失败

因此有:

t0[x] = (u0[x] + x) mod 256

从而目标字节为:

target[x] = (t0[x] − x) mod 256

K0VM 指令序列(来自 coTex.png)等价于对 16 字节循环一次的变换,关键点:初始化寄存器:x1[1]=42, x1[2]=0; 对每个索引 i(0..15): 1. x = u0[i] 2. x = x ^ a(a 为累加器,初始 42) 3. x = ROL8(x, i & 7) 4. x = (x * 7) mod 256 5. x = (x + i) mod 256 6. out[i] = x 7. a = (a + x) mod 256

正向模拟 K0 VM

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
import numpy as np

def solve_vm():
ci_hex = "e98e8a8ab7e7c9e0b897b74b3b21d37cbc8acfd5a8812ea8a3d2946bba809d"
u0_initial = [int(ci_hex[i:i+2], 16) for i in range(0, 32, 2)]

co_hex = "0001002a00020000010002000200010003000200040000070600020007020000060100000502020108020010090000f70a00000000020012020001000400000607020000060202050900009001010109050100030801014a05010003"
bytecode = []
for i in range(0, len(co_hex), 8):
pix = co_hex[i:i+8]
if len(pix) == 8:
bytecode.append([int(pix[j:j+2], 16) for j in range(0, 8, 2)])

x1 = [0] * 32
x0 = [0] * 16
pc = 0
cond = 0

for _ in range(1024):
if pc >= len(bytecode):
break

instr = bytecode[pc]
op = instr[0]
y = instr[1]
z = instr[2]
w = instr[3]

next_pc = pc + 1

if op == 0:
x1[y] = w
elif op == 1:
idx = x1[z] & 15
x1[y] = u0_initial[idx]
elif op == 2:
x1[y] = (x1[y] ^ x1[z]) & 0xFF
elif op == 3:
shift = x1[z] & 7
val = x1[y] & 0xFF
x1[y] = ((val << shift) | (val >> (8 - shift))) & 0xFF
elif op == 4:
x1[y] = (x1[z] * w) & 0xFF
elif op == 5:
x1[y] = (x1[z] + w) & 0xFF
elif op == 6:
x1[y] = (x1[y] + x1[z]) & 0xFF
elif op == 7:
x0[x1[y] & 15] = x1[z] & 0xFF
elif op == 8:
cond = 1 if x1[y] < w else 0
elif op == 9:
signed_w = w if w < 128 else w - 256
if cond:
next_pc = pc + signed_w + 1
else:
break

pc = next_pc

return x0

key = solve_vm()
print(f"key: {key}")

主流程里 runEncryptAsync 连续执行 3 次,所以最终供 K1 校验的 u0u0 = K0(K0(K0(input))) 因此需要对 target 反向执行 K0 的逆变换三次,得到原始输入字节

  1. x = (out[i] - i) mod 256
  2. x = x * 7^{-1} mod 256,其中 7−1 ≡ 183 mod 256
  3. x = ROR8(x, i & 7)
  4. x = x ^ a
  5. a = (a + out[i]) mod 256

将这个逆过程对 target 重复 3 次

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
from PIL import Image

def inv_round(out_bytes):
a = 42
inv7 = 183 # 7 * 183 % 256 == 1
inp = [0] * 16
for i in range(16):
x = out_bytes[i]
x = (x - i) & 0xFF
x = (x * inv7) & 0xFF
s = i & 7
x = ((x >> s) | ((x << (8 - s)) & 0xFF)) & 0xFF # ROR8
x = x ^ a
inp[i] = x
a = (a + out_bytes[i]) & 0xFF
return inp

def main():
ci = Image.open("ciTex.png").convert("RGBA")
row = [ci.getpixel((x, 0))[0] for x in range(16)]

# target = (t0[x] - x) mod 256
target = [(row[i] - i) & 0xFF for i in range(16)]

# invert 3 rounds
v = target
for _ in range(3):
v = inv_round(v)

print("target", target)
print("input", v)
print("hex", "".join(f"{b:02x}" for b in v))
print("ascii", "".join(chr(b) if 32 <= b <= 126 else "." for b in v))

if __name__ == "__main__":
main()

flag 为 alictf{5haderVM_Rep3at!}

Thief

流量包分析

分析流量包,里面有三个数据流,从 Wireshark 导出那 3 个 TCP 流的原始数据

用 010 看会发现每条消息的开头都是 0x89 "ali",结构为 magic(4) + partIdx(4) + rsaLen(4) + rsaBlob(rsaLen) + fileCount(4) + [nameLen + nameXor + offset]* + payload,其中 nameXor 每字节异或 233 可以得到真实文件名,offset 指向 payload 内各 LZRR 块起始位置

三条流内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
part1:
AbstractPatternMachine.java
flagImage/Image1Part3.java
flagImage/Image1Part1.java

part2:
ParticlePhysicsSimulator.java
flagImage/Image1Part4.java
flagImage/Image1Part2.java

part3:
GeneticAlgorithmEngine.java
flagImage/Image1Part5.java
flagImage/Image1Part6.java

webp 文件分析

Thief-hdpi 这个目录下有很多 .webp 文件,并且其中有的名字还带有混淆,应该属于是比较关键的内容 发现 .webp 开头是 CA FE BA BE,实际上是 Java class,对其进行反编译:

i.l.L 负责扫描 user.dir 上级目录并批量打包,每 3 个文件一组 i.l.l1I 内嵌 base64 数据段,解码得到两份算法表(记为 algo2/algo3) i.l.l1I 负责打包:文件内容先经 i.l.Il1 压缩为 LZRR 格式;文件名 UTF‑8 后与 233 异或;offset 为每个文件压缩块的拼接偏移 i.l.Il1 产生的头部是大端 "LZRR" + ver(0x0201) + flag + in_len + crc - flag=13:直接 LZ 比特流;flag=15:先 RLE(0xFF 作为转义与游程标记),再进入 LZ 比特流。 - LZ 采用 1bit 标志区分字面量/匹配,offset 8/16 位可变,length 为 3+编码值

i.l.l1I 内含 RSA 公钥模数(2048 位)与 exponent 65537;对 8 字节随机 seed 做 modPow 得到 rsaBlob

l1I 的 base64 常量解出两段二进制表,其中一段用于 Runner.encrypt 的 algo2,另一段用于 algo3,通过 TryDecrypt.java 调用 Runner.encrypt 可验证哪一个表对应哪种行为

TryDecrypt.java

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
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.List;

public class TryDecrypt {
private static final byte[] MAGIC = new byte[] {(byte)0x89, 0x61, 0x6c, 0x69};
private static final byte[] LZRR = new byte[] {0x4c, 0x5a, 0x52, 0x52};

private static int u32le(byte[] b, int off) {
return ByteBuffer.wrap(b, off, 4)
.order(ByteOrder.LITTLE_ENDIAN)
.getInt();
}

private static int indexOf(byte[] data, byte[] needle) {
for (int i = 0; i <= data.length - needle.length; i++) {
boolean ok = true;
for (int j = 0; j < needle.length; j++) {
if (data[i + j] != needle[j]) {
ok = false;
break;
}
}
if (ok) return i;
}
return -1;
}

private static class Msg {
int partIdx;
int rsaLen;
byte[] rsa;
int fileCount;
String[] files;
int[] offsets;
byte[] payload;
}

private static Msg parseMessage(byte[] data) throws Exception {
int start = indexOf(data, MAGIC);
if (start < 0) throw new IllegalArgumentException("magic not found");

int partIdx = u32le(data, start + 4);
int rsaLen = u32le(data, start + 8);
int pos = start + 12;

if (pos + rsaLen + 4 > data.length)
throw new IllegalArgumentException("bad rsa len");

byte[] rsa = Arrays.copyOfRange(data, pos, pos + rsaLen);
pos += rsaLen;

int fileCount = u32le(data, pos);
pos += 4;

String[] files = new String[fileCount];
int[] offsets = new int[fileCount];

for (int i = 0; i < fileCount; i++) {
int nameLen = u32le(data, pos);
pos += 4;

byte[] nameX = Arrays.copyOfRange(data, pos, pos + nameLen);
pos += nameLen;

for (int j = 0; j < nameX.length; j++) {
nameX[j] ^= (byte) 233;
}
files[i] = new String(nameX, "UTF-8");

offsets[i] = u32le(data, pos);
pos += 4;
}

byte[] payload = Arrays.copyOfRange(data, pos, data.length);

Msg m = new Msg();
m.partIdx = partIdx;
m.rsaLen = rsaLen;
m.rsa = rsa;
m.fileCount = fileCount;
m.files = files;
m.offsets = offsets;
m.payload = payload;
return m;
}

private static void trySeed(byte[] algo, byte[] payload, byte[] seed, String tag) {
try {
byte[] out = i.l.Runner.encrypt(algo, payload, seed);
int pos = indexOf(out, LZRR);
System.out.println(" " + tag + " -> outlen=" + out.length + " LZRR@" + pos);

if (pos >= 0) {
Path outPath = Path.of("dec_" + tag + ".bin");
Files.write(outPath, out);
System.out.println(" wrote " + outPath.toAbsolutePath());
}
} catch (Throwable t) {
System.out.println(" " + tag + " -> failed: "
+ t.getClass().getSimpleName() + ": " + t.getMessage());
}
}

private static void probeSeedSensitivity(byte[] algo, byte[] payload) {
byte[] baseSeed = new byte[8];
byte[] baseOut = i.l.Runner.encrypt(algo, payload, baseSeed);
byte[] basePrefix = Arrays.copyOf(baseOut, 8);

System.out.println("seed sensitivity: basePrefix=" + bytesToHex(basePrefix));

for (int idx = 0; idx < 8; idx++) {
byte[] seed = new byte[8];
seed[idx] = 1;
byte[] out = i.l.Runner.encrypt(algo, payload, seed);
byte[] pref = Arrays.copyOf(out, 8);
boolean same = Arrays.equals(basePrefix, pref);
System.out.println(" seed[" + idx + "] samePrefix=" + same
+ " prefix=" + bytesToHex(pref));
}
}

private static String bytesToHex(byte[] b) {
StringBuilder sb = new StringBuilder();
for (byte v : b) sb.append(String.format("%02x", v));
return sb.toString();
}

public static void main(String[] args) throws Exception {
System.out.println("Working directory = " +
Path.of(".").toAbsolutePath().normalize());
byte[] algo2 = Files.readAllBytes(Path.of("b64_0.bin"));
byte[] algo3 = Files.readAllBytes(Path.of("b64_1.bin"));

byte[] a10 = new byte[10];
byte[] b20 = new byte[20];
System.out.println("len test: out1="
+ i.l.Runner.encrypt(algo2, a10, b20).length
+ " out2="
+ i.l.Runner.encrypt(algo2, b20, a10).length);

List<String> streams = List.of(
"streams_py/127.0.0.1_61111-127.0.0.1_8889.bin",
"streams_py/127.0.0.1_57692-127.0.0.1_8889.bin",
"streams_py/127.0.0.1_52024-127.0.0.1_8889.bin"
);

for (String p : streams) {
byte[] data = Files.readAllBytes(Path.of(p));
Msg m = parseMessage(data);

System.out.println("stream=" + p
+ " part=" + m.partIdx
+ " files=" + m.fileCount
+ " payload=" + m.payload.length);

byte[] rsa = m.rsa;
byte[] rsaFirst8 = Arrays.copyOf(rsa, Math.min(8, rsa.length));
byte[] rsaLast8 = Arrays.copyOfRange(rsa,
Math.max(0, rsa.length - 8), rsa.length);
byte[] zeros8 = new byte[8];

for (int a = 0; a < 2; a++) {
byte[] algo = (a == 0) ? algo2 : algo3;
String algoTag = (a == 0) ? "algo2" : "algo3";

trySeed(algo, m.payload, rsa,
"part" + m.partIdx + "_" + algoTag + "_rsa");
trySeed(algo, m.payload, rsaFirst8,
"part" + m.partIdx + "_" + algoTag + "_rsaFirst8");
trySeed(algo, m.payload, rsaLast8,
"part" + m.partIdx + "_" + algoTag + "_rsaLast8");
trySeed(algo, m.payload, zeros8,
"part" + m.partIdx + "_" + algoTag + "_zeros8");
}

if (m.partIdx == 2) {
Files.write(
Path.of("dec_part2_algo2_zeros.bin"),
i.l.Runner.encrypt(algo2, m.payload, zeros8)
);
Files.write(
Path.of("dec_part2_algo3_zeros.bin"),
i.l.Runner.encrypt(algo3, m.payload, zeros8)
);

byte[] seedFirst8 = Arrays.copyOf(rsa, 8);
byte[] seedLast8 = Arrays.copyOfRange(rsa, rsa.length - 8, rsa.length);
byte[] seedXor = new byte[8];
for (int i = 0; i < 8; i++)
seedXor[i] = (byte)(seedFirst8[i] ^ seedLast8[i]);

trySeed(algo2, m.payload, seedFirst8, "part2_algo2_rsaFirst8");
trySeed(algo2, m.payload, seedLast8, "part2_algo2_rsaLast8");
trySeed(algo2, m.payload, seedXor, "part2_algo2_rsaXor");
trySeed(algo2, m.payload,
MessageDigest.getInstance("MD5").digest(rsa),
"part2_algo2_md5");
trySeed(algo2, m.payload,
MessageDigest.getInstance("SHA-1").digest(rsa),
"part2_algo2_sha1");
trySeed(algo2, m.payload,
MessageDigest.getInstance("SHA-256").digest(rsa),
"part2_algo2_sha256");

for (int si = 0; si < 8; si++) {
for (int bit = 0; bit < 8; bit++) {
byte[] seed = new byte[8];
seed[si] = (byte)(1 << bit);
Files.write(
Path.of("dec_part2_algo2_seedb_" + si + "_" + bit + ".bin"),
i.l.Runner.encrypt(algo2, m.payload, seed)
);
}
}

probeSeedSensitivity(algo2, m.payload);
}
}
}
}

试验发现 algo3 与 seed 无关,任意 seed 输出一致,因此可视为对 payload 的固定变换;algo2 对 seed 的影响是线性 XOR,seed 64bit 每一位对应一个可叠加的差分输出 对于 part1 和 part3,用 algo3 解密对应 payload 即 Runner.encrypt(algo3, payload, seed) 直接得到 LZRR 压缩流, 依据 offsets 或按 LZRR 魔数切块逐一解压来还原 .javaImage1Part*.java

LZRR + RLE 解压

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
from __future__ import annotations

from pathlib import Path
from typing import Tuple
import struct


MAGIC = b"LZRR"
VERSION = 0x0201


def parse_header(data: bytes) -> Tuple[int, int, int, int, bytes]:
if len(data) < 16:
raise ValueError("data too short")
if data[:4] != MAGIC:
raise ValueError("bad magic")
ver = struct.unpack(">H", data[4:6])[0]
if ver != VERSION:
raise ValueError(f"bad version: {ver}")
flag = struct.unpack(">H", data[6:8])[0]
in_len = struct.unpack(">I", data[8:12])[0]
crc = struct.unpack(">I", data[12:16])[0]
body = data[16:]
return flag, in_len, crc, ver, body


def rle_decode(data: bytes) -> bytes:
out = bytearray()
i = 0
n = len(data)
while i < n:
b = data[i]
i += 1
if b != 0xFF:
out.append(b)
continue
if i >= n:
raise ValueError("truncated RLE")
cnt = data[i]
i += 1
if cnt == 0xFF:
out.append(0xFF)
continue
if i >= n:
raise ValueError("truncated RLE value")
val = data[i]
i += 1
out.extend([val] * (cnt + 4))
return bytes(out)


class BitReader:
def __init__(self, data: bytes) -> None:
self.data = data
self.pos = 0
self.bit = 0

def read_bit(self) -> int:
if self.pos >= len(self.data):
raise EOFError("bitstream exhausted")
b = self.data[self.pos]
v = (b >> (7 - self.bit)) & 1
self.bit += 1
if self.bit == 8:
self.bit = 0
self.pos += 1
return v

def read_bits(self, n: int) -> int:
v = 0
for _ in range(n):
v = (v << 1) | self.read_bit()
return v


def lzrr_decompress(data: bytes) -> bytes:
flag, in_len, _crc, _ver, body = parse_header(data)
if flag == 15:
body = rle_decode(body)
elif flag != 13:
raise ValueError(f"unsupported flag: {flag}")

br = BitReader(body)
out = bytearray()
while len(out) < in_len:
tag = br.read_bit()
if tag == 0:
out.append(br.read_bits(8))
continue
if br.read_bit() == 0:
offset = br.read_bits(8)
else:
offset = br.read_bits(16)
if br.read_bit() == 0:
l = br.read_bits(3)
else:
if br.read_bit() == 0:
l = br.read_bits(6) + 8
else:
l = br.read_bits(8)
length = l + 3
if offset <= 0:
raise ValueError("invalid offset")
start = len(out) - offset
if start < 0:
raise ValueError("offset out of range")
for i in range(length):
out.append(out[start + i])
return bytes(out)


def main() -> None:
inputs = [
Path("dec_part1_algo3_rsa.bin"),
Path("dec_part3_algo3_rsa.bin"),
]
for p in inputs:
data = p.read_bytes()
out = lzrr_decompress(data)
out_path = p.with_suffix(p.suffix + ".out")
out_path.write_bytes(out)
print(f"{p.name} -> {out_path.name} ({len(out)} bytes)")


if __name__ == "__main__":
main()

恢复 part1/part3 的 .java 与图片段:

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
from __future__ import annotations

from pathlib import Path
import struct
import re
import lzrr_decompress as lz

MAGIC = b"\x89ali"


def parse_message(data: bytes):
start = data.find(MAGIC)
if start < 0:
raise ValueError("magic not found")
part_idx = struct.unpack_from("<I", data, start + 4)[0]
rsa_len = struct.unpack_from("<I", data, start + 8)[0]
pos = start + 12 + rsa_len
file_count = struct.unpack_from("<I", data, pos)[0]
pos += 4
files = []
offsets = []
for _ in range(file_count):
name_len = struct.unpack_from("<I", data, pos)[0]
pos += 4
name_x = data[pos:pos + name_len]
pos += name_len
name = bytes(b ^ 233 for b in name_x).decode("utf-8", errors="replace")
off = struct.unpack_from("<I", data, pos)[0]
pos += 4
files.append(name)
offsets.append(off)
return part_idx, files, offsets


def split_lzrr_blocks(payload: bytes) -> list[bytes]:
magic = b"LZRR"
positions = [i for i in range(len(payload) - 3) if payload[i:i + 4] == magic]
blocks = []
for i, start in enumerate(positions):
end = positions[i + 1] if i + 1 < len(positions) else len(payload)
blocks.append(payload[start:end])
return blocks


def recover(part_stream: Path, dec_payload: Path, out_dir: Path) -> None:
data = part_stream.read_bytes()
part_idx, files, _offsets = parse_message(data)
payload = dec_payload.read_bytes()

blocks = split_lzrr_blocks(payload)
if len(blocks) != len(files):
print(f"part{part_idx}: block count {len(blocks)} != file count {len(files)}")

out_dir.mkdir(parents=True, exist_ok=True)

for idx, block in enumerate(blocks):
out = lz.lzrr_decompress(block)
name = files[idx] if idx < len(files) else f"file_{idx}.bin"
dest = out_dir / name
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(out)
print(f"part{part_idx}: wrote {dest} ({len(out)} bytes)")


def search_flag(root: Path) -> None:
for p in root.rglob("*"):
if p.is_file():
b = p.read_bytes()
m = re.search(rb"alictf\{[^}]{0,200}\}", b)
if m:
print("flag", m.group(0).decode("ascii", errors="ignore"), "in", p)


def main() -> None:
base = Path(".")

recover(
base / "streams_py" / "127.0.0.1_61111-127.0.0.1_8889.bin",
base / "dec_part1_algo3_rsa.bin",
base / "recovered" / "part1",
)
recover(
base / "streams_py" / "127.0.0.1_52024-127.0.0.1_8889.bin",
base / "dec_part3_algo3_rsa.bin",
base / "recovered" / "part3",
)
search_flag(base / "recovered")


if __name__ == "__main__":
main()

恢复 part2 的 .java 与图片段

对于 part2,先用 seed=0 得到 dec_part2_algo2_zeros.bin,再对 seed 64 位逐位置 1,生成 64 个基向量输出 dec_part2_algo2_seedb_{si}_{bit}.bin,利用 LZRR 头 8 字节已知模式 LZRR 0201 flag 建立 GF(2) 线性方程组

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
from __future__ import annotations

from pathlib import Path
import struct
import re
import lzrr_decompress as lz

MAGIC = b"\x89ali"


def parse_message(path: Path):
data = path.read_bytes()
start = data.find(MAGIC)
if start < 0:
raise ValueError("magic not found")
rsa_len = struct.unpack_from("<I", data, start + 8)[0]
pos = start + 12 + rsa_len
file_count = struct.unpack_from("<I", data, pos)[0]
pos += 4
files = []
offsets = []
for _ in range(file_count):
name_len = struct.unpack_from("<I", data, pos)[0]
pos += 4
name_x = data[pos:pos + name_len]
pos += name_len
name = bytes(b ^ 233 for b in name_x).decode("utf-8", errors="replace")
off = struct.unpack_from("<I", data, pos)[0]
pos += 4
files.append(name)
offsets.append(off)
return files, offsets


def build_equations(base: bytes, deltas: list[bytes], offsets: list[int], flags: list[int]):
eq_masks = []
eq_rhs = []
for off, flag in zip(offsets, flags):
header = b"LZRR" + b"\x02\x01" + struct.pack(">H", flag)
for i in range(8):
target = header[i] ^ base[off + i]
for bit in range(8):
rhs = (target >> bit) & 1
mask = 0
for j in range(64):
if ((deltas[j][off + i] >> bit) & 1) != 0:
mask |= (1 << j)
eq_masks.append(mask)
eq_rhs.append(rhs)
return eq_masks, eq_rhs


def solve_linear(eq_masks: list[int], eq_rhs: list[int]) -> int | None:
rows = list(zip(eq_masks, eq_rhs))
pivots = {}

for mask, rhs in rows:
m = mask
r = rhs
while m != 0:
p = (m & -m)
bit = (p.bit_length() - 1)
if bit in pivots:
m ^= pivots[bit][0]
r ^= pivots[bit][1]
else:
pivots[bit] = (m, r)
break
if m == 0 and r == 1:
return None

sol = 0
for bit in sorted(pivots.keys(), reverse=True):
m, r = pivots[bit]
known = sol & m
parity = bin(known).count("1") & 1
val = r ^ parity
if val:
sol |= (1 << bit)
return sol


def apply_seed(base: bytes, deltas: list[bytes], seed_bits: int) -> bytes:
out = bytearray(base)
for j in range(64):
if (seed_bits >> j) & 1:
dj = deltas[j]
out = bytearray(b ^ d for b, d in zip(out, dj))
return bytes(out)


def main() -> None:
root = Path(__file__).resolve().parent
stream = root / "streams_py" / "127.0.0.1_57692-127.0.0.1_8889.bin"
base_path = root / "dec_part2_algo2_zeros.bin"

files, offsets = parse_message(stream)
base = base_path.read_bytes()

deltas: list[bytes] = []
for si in range(8):
for bit in range(8):
p = root / f"dec_part2_algo2_seedb_{si}_{bit}.bin"
data = p.read_bytes()
delta = bytes(b ^ s for b, s in zip(base, data))
deltas.append(delta)

flags_list = [13, 15]
found = None
for f0 in flags_list:
for f1 in flags_list:
for f2 in flags_list:
flags = [f0, f1, f2]
eq_masks, eq_rhs = build_equations(base, deltas, offsets, flags)
seed_bits = solve_linear(eq_masks, eq_rhs)
if seed_bits is None:
continue
out = apply_seed(base, deltas, seed_bits)
ok = True
for off, flag in zip(offsets, flags):
header = out[off:off + 8]
if header != (b"LZRR" + b"\x02\x01" + struct.pack(">H", flag)):
ok = False
break
if ok:
found = (seed_bits, flags, out)
break
if found:
break
if found:
break

if not found:
print("no seed found")
return

seed_bits, flags, out = found
seed_bytes = seed_bits.to_bytes(8, "little")
print("seed", seed_bytes.hex(), "flags", flags)

out_path = root / "dec_part2_algo2_seed.bin"
out_path.write_bytes(out)

out_dir = root / "recovered" / "part2"
out_dir.mkdir(parents=True, exist_ok=True)
for idx, off in enumerate(offsets):
end = offsets[idx + 1] if idx + 1 < len(offsets) else len(out)
block = out[off:end]
data = lz.lzrr_decompress(block)
dest = out_dir / files[idx]
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(data)
print("wrote", dest)

for p in out_dir.rglob("*.java"):
m = re.search(rb"alictf\{[^}]{0,200}\}", p.read_bytes())
if m:
print("flag", m.group(0).decode("ascii", errors="ignore"), "in", p)


if __name__ == "__main__":
main()

解得 seeda91b1bb4e8978bda,然后合成真实 payload 并解压得到 .javaImage1Part*.java

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
from __future__ import annotations

from pathlib import Path
import re
import struct
import lzrr_decompress as lz

MAGIC = b"\x89ali"

def parse_message(data: bytes):
start = data.find(MAGIC)
if start < 0:
raise ValueError("magic not found")
part_idx = struct.unpack_from("<I", data, start + 4)[0]
rsa_len = struct.unpack_from("<I", data, start + 8)[0]
pos = start + 12 + rsa_len
file_count = struct.unpack_from("<I", data, pos)[0]
pos += 4
files = []
offsets = []
for _ in range(file_count):
name_len = struct.unpack_from("<I", data, pos)[0]
pos += 4
name_x = data[pos:pos + name_len]
pos += name_len
name = bytes(b ^ 233 for b in name_x).decode("utf-8", errors="replace")
off = struct.unpack_from("<I", data, pos)[0]
pos += 4
files.append(name)
offsets.append(off)
return part_idx, files, offsets

def split_lzrr_blocks(payload: bytes) -> list[bytes]:
magic = b"LZRR"
positions = [i for i in range(len(payload) - 3) if payload[i:i + 4] == magic]
blocks = []
for i, start in enumerate(positions):
end = positions[i + 1] if i + 1 < len(positions) else len(payload)
blocks.append(payload[start:end])
return blocks

def recover(part_stream: Path, dec_payload: Path, out_dir: Path) -> None:
data = part_stream.read_bytes()
part_idx, files, _offsets = parse_message(data)
payload = dec_payload.read_bytes()
blocks = split_lzrr_blocks(payload)

out_dir.mkdir(parents=True, exist_ok=True)
for idx, block in enumerate(blocks):
out = lz.lzrr_decompress(block)
name = files[idx] if idx < len(files) else f"file_{idx}.bin"
dest = out_dir / name
dest.parent.mkdir(parents=True, exist_ok=True)
dest.write_bytes(out)
if dest.suffix == ".java":
print(f"part{part_idx}: {dest.relative_to(out_dir).as_posix()}")

def main() -> None:
root = Path(__file__).resolve().parent
recover(
root / "streams_py" / "127.0.0.1_61111-127.0.0.1_8889.bin",
root / "dec_part1_algo3_rsa.bin",
root / "recovered" / "part1",
)
recover(
root / "streams_py" / "127.0.0.1_57692-127.0.0.1_8889.bin",
root / "dec_part2_algo2_seed.bin",
root / "recovered" / "part2",
)
recover(
root / "streams_py" / "127.0.0.1_52024-127.0.0.1_8889.bin",
root / "dec_part3_algo3_rsa.bin",
root / "recovered" / "part3",
)

if __name__ == "__main__":
main()

图片合成

最后从 Image1Part*.java 抽取 base64 ,生成 part1..6.jpg,再横向拼接成 combined.jpg

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
from __future__ import annotations

from pathlib import Path
import base64
import re

def extract_images(recovered_root: Path, out_dir: Path) -> list[Path]:
out_dir.mkdir(parents=True, exist_ok=True)
out_paths: list[Path] = []
for java_path in recovered_root.rglob("Image1Part*.java"):
text = java_path.read_text(encoding="utf-8", errors="ignore")
m = re.search(r"Image1Part(\d+)", java_path.name)
m2 = re.search(r'IMAGE_DATA\s*=\s*"([A-Za-z0-9+/=]+)"', text)
if not (m and m2):
continue
part_no = int(m.group(1))
data = base64.b64decode(m2.group(1))
out_path = out_dir / f"part{part_no}.jpg"
out_path.write_bytes(data)
out_paths.append(out_path)
return sorted(out_paths, key=lambda p: int(re.search(r"part(\d+)", p.name).group(1)))

def combine_images(paths: list[Path], out_path: Path) -> Path | None:
try:
from PIL import Image
except Exception as exc:
print(f"PIL not available: {exc}")
return None
imgs = [Image.open(p) for p in paths]
widths, heights = zip(*(im.size for im in imgs))
total_width = sum(widths)
max_height = max(heights)
combined = Image.new("RGB", (total_width, max_height))
x = 0
for im in imgs:
combined.paste(im, (x, 0))
x += im.size[0]
combined.save(out_path)
return out_path

def main() -> None:
root = Path(__file__).resolve().parent
recovered_root = root / "recovered"
images_dir = recovered_root / "images"
parts = extract_images(recovered_root, images_dir)
if not parts:
print("no image parts extracted")
return
out = combine_images(parts, images_dir / "combined.jpg")
if out:
print(f"combined image -> {out}")

if __name__ == "__main__":
main()

OCR 识别一下

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
from pathlib import Path
import numpy as np
import cv2
import easyocr

BASE_DIR = Path(__file__).resolve().parent
img_path = BASE_DIR / 'recovered' / 'images' / 'combined.jpg'

raw = img_path.read_bytes()
img = cv2.imdecode(np.frombuffer(raw, dtype=np.uint8), cv2.IMREAD_COLOR)

gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

def get_boxes(bin_img):
cnts, _ = cv2.findContours(bin_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
boxes = []
h, w = bin_img.shape
for c in cnts:
x, y, wc, hc = cv2.boundingRect(c)
area = wc * hc
if area < 1000:
continue
if wc > 0.95 * w and hc > 0.95 * h:
continue
boxes.append((x, y, wc, hc, area))
boxes.sort(key=lambda t: t[4], reverse=True)
return boxes

th = cv2.adaptiveThreshold(
gray,
255,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY_INV,
35,
10
)

boxes = get_boxes(th)

reader = easyocr.Reader(['en'], gpu=False)
allow = 'abcdefABCDEF0123456789-{}'

for i, (x, y, wc, hc, area) in enumerate(boxes[:5]):
crop = img[y:y+hc, x:x+wc]
crop2 = cv2.resize(crop, None, fx=2, fy=2, interpolation=cv2.INTER_CUBIC)
res = reader.readtext(crop2, allowlist=allow)
texts = [(t, c) for _, t, c in res]
print(i, (x, y, wc, hc), texts)

flag 为 alictf{5a8e0fb1-d3f5-4b13-8424-164faab9bbd2}


阿里 CTF 2026 逆向部分 WP
http://example.com/2026/02/02/alictf2026/
作者
Eleven
发布于
2026年2月2日
许可协议