逆向三道题出了两道,另一道纯恶心人来的 发现晚上出 flag 概率比白天高
=w=
pixelflow
初步分析
找到 global-metadata.dat 和
GameAssembly.dll 用 il2cppdumper 解包 但是在
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=255,SetPixels32 + Apply,然后
Graphics.CopyTexture 到 TexF,说明 K0 输出的
16 字节正是 TexF 的 R 通道
Controller___Check_b__18_2_d__MoveNext
新建 ComputeBuffer(16,4),命名 Buffer0,绑定到
Shader0 的 Buffer0,绑定 TexF 到
Shader0 的 TexF,设置 K2 常量(4
个 float),调用 Dispatch(Shader0, K0, 1,1,1),说明 K0
是加密的线程组数
Controller___Check_b__18_3_d__MoveNext
清空
Buffer0,Dispatch(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
K2 的 code,先存成 .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.
若不相等则失败
因此有:
t 0[x ] = (u 0[x ] + x ) m o d 256
从而目标字节为:
t a r g e t [x ] = (t 0[x ] − x ) m o d 256
K0 的 VM 指令序列(来自 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 npdef 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 校验的 u0 是
u0 = K0(K0(K0(input))) 因此需要对 target
反向执行 K0 的逆变换三次,得到原始输入字节
x = (out[i] - i) mod 256
x = x * 7^{-1} mod 256,其中 7−1 ≡ 183 m o d 256
x = ROR8(x, i & 7)
x = x ^ a
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 Imagedef inv_round (out_bytes ): a = 42 inv7 = 183 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 x = x ^ a inp[i] = x a = (a + out_bytes[i]) & 0xFF return inpdef main (): ci = Image.open ("ciTex.png" ).convert("RGBA" ) row = [ci.getpixel((x, 0 ))[0 ] for x in range (16 )] target = [(row[i] - i) & 0xFF for i in range (16 )] 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 魔数切块逐一解压来还原
.java 与 Image1Part*.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 annotationsfrom pathlib import Pathfrom 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, bodydef 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 vdef 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 annotationsfrom pathlib import Pathimport structimport reimport 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, offsetsdef 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 blocksdef 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 annotationsfrom pathlib import Pathimport structimport reimport 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, offsetsdef 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_rhsdef 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 soldef 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()
解得 seed 为 a91b1bb4e8978bda,然后合成真实
payload 并解压得到 .java 与
Image1Part*.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 annotationsfrom pathlib import Pathimport reimport structimport 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, offsetsdef 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 blocksdef 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 annotationsfrom pathlib import Pathimport base64import redef 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_pathdef 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 Pathimport numpy as npimport cv2import 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}