XYCTF_Reverse出题小记

本文最后更新于 2025年4月8日 下午

XYCTF_Reverse出题小记

写在前面

被好朋友墨水师傅邀请到XYCTF2025的出题群里,十分开心也颇具纪念意义,我们就是因为XYCTF2024而结缘,从选手到出题人身份的转变也让我倍感荣幸。虽然赛中出现了各种非预期和更换了一次附件的问题,但我希望各位师傅从这次比赛能有所收获,感谢大家对我题目的认可,我也会继续努力,成为想要成为的自己。

“要有最朴素的生活和最遥远的梦想,即使明天天寒地冻,山高水远,路远马亡。”

Moon

附件里给了一个 main.py和moon.pyd 。

那么可能有人会问,什么是.pyd文件呢?

.pyd 文件是 Python 动态模块(Python Dynamic Module) 的一种,它本质上是一个 Windows 下的动态链接库(DLL)文件,但扩展名是 .pyd,以便 Python 能识别并导入。

.pyd 文件允许你用 C、C++ 或其他编译型语言编写 Python 模块,然后作为一个模块供 Python 导入和调用,就像普通的 .py 文件一样。

你可以这样使用:

1
2
import mymodule  
mymodule.func()

很多人赛中来询问我关于python版本的问题,显示模块导入不成功,目前见过的pyd文件大多都可以在字符串表里找到关于python版本号的问题

image-20250407115907479

比如这里,我们可以看到python版本为3.11

类比去年ciscn初赛的rand0m,也是同样的,能看到python版本为3.12

image-20250407120012150

打开main.py我们发现是导入了moon模块然后调用了moon.check_flag来进行验证(顺便说一下这题推荐了毛姆的《月亮与六便士》哈哈哈)

我们打开moon.pyd搜索字符串,能定位到sub_180002550函数

里面大概呈现了所有的字符串

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
v9[1] = "426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7";
si128 = _mm_load_si128((const __m128i *)&xmmword_180009050);
v0 = (__int64 *)((char *)off_18000B618 + 56);
v13 = (char *)off_18000B618 + 64;
v14 = "SEED";
v19 = (char *)off_18000B618 + 72;
v20 = "TARGET_HEX";
v24 = (char *)off_18000B618 + 80;
v25 = "UnicodeEncodeError";
v30 = (char *)off_18000B618 + 88;
v31 = "*";
v35 = (char *)off_18000B618 + 96;
v36 = "?";
v41 = (char *)off_18000B618 + 104;
v42 = "asyncio.coroutines";
v46 = (char *)off_18000B618 + 112;
v47 = "b";
v52 = (char *)off_18000B618 + 120;
v53 = "check_flag";
v57 = (char *)off_18000B618 + 128;
v32 = _mm_load_si128((const __m128i *)&xmmword_180008FB0);
v58 = "cline_in_traceback";
v63 = (char *)off_18000B618 + 136;
v64 = "data_bytes";
v68 = (char *)off_18000B618 + 144;
v69 = "encode";
v74 = (char *)off_18000B618 + 152;
v75 = "encoding_error";
v79 = (char *)off_18000B618 + 160;
v80 = "encrypted";
v85 = (char *)off_18000B618 + 168;
v86 = &unk_180008DC0;
v90 = (char *)off_18000B618 + 176;
v91 = "__import__";
v96 = (char *)off_18000B618 + 184;
v97 = "_initializing";
v101 = (char *)off_18000B618 + 192;
v102 = "input_str";
v107 = (char *)off_18000B618 + 200;
v108 = "_is_coroutine";
v112 = (char *)off_18000B618 + 208;
v76 = _mm_load_si128((const __m128i *)&xmmword_180009030);
v113 = "__main__";
v87 = _mm_load_si128((const __m128i *)&xmmword_180008FC0);
v118 = (char *)off_18000B618 + 216;
v123 = (char *)off_18000B618 + 224;
v124 = "moon.py";
v129 = (char *)off_18000B618 + 232;
v130 = "__name__";
v134 = (char *)off_18000B618 + 240;
v135 = "randint";
v140 = (char *)off_18000B618 + 248;
v141 = "random";
v145 = (char *)off_18000B618 + 256;
v146 = "seed";
v151 = (char *)off_18000B618 + 264;
v152 = "seed_value";
v156 = (char *)off_18000B618 + 272;
v157 = "__spec__";
v162 = (char *)off_18000B618 + 280;
v163 = "__test__";
v167 = (char *)off_18000B618 + 288;
v168 = "unknown_error";
v120 = _mm_load_si128((const __m128i *)&xmmword_180008FD0);
v173 = (char *)off_18000B618 + 296;
v142 = _mm_load_si128((const __m128i *)&xmmword_180008FF0);
v174 = "utf-8";
v175 = _mm_load_si128((const __m128i *)&xmmword_180008FE0);
v176 = 256;
v178 = (char *)off_18000B618 + 304;
v179 = "xor_crypt";

我们能看到有xor_crypt,seed,random,甚至密文的十六进制值在这里也进行了体现

那么我们下一步就是要找到xor的位置,其实就是寻找函数PyNumber_Xor,搜索引用发现在函数sub_180001370处有逻辑

image-20250407121249488

先xor然后写进一个list里面

往后找比较,找PyObject_RichCompare找到sub_180001B70

image-20250407121801208

其实看到这里就不难猜到是random.seed去生成随机数而且进行xor了,而事实也的确如此

现在我们需要找到随机数种子(当然也可以直接根据flag头flag{爆破)

观察到xor的v8来自off_18000B618,一交叉引用发现很多,然后找到了函数sub_180003010

image-20250407122626224

随机数种子是0x114514,当然也可以有快速定位的方法,关注函数PyLong_FromLong,这是对数值进行处理的函数,然后分析到这里我们就可以写解密脚本了

最终解密脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import random

def decrypt(ch, seed):
c1 = bytes.fromhex(ch)

random.seed(seed)
key = bytes([random.randint(0, 255) for _ in range(len(c1))])

p = bytes([c ^ k for c, k in zip(c1, key)])

try:
return p.decode('utf-8')
except UnicodeDecodeError:
return f" {p} {p.hex()}"

ch = "426b87abd0ceaa3c58761bbb0172606dd8ab064491a2a76af9a93e1ae56fa84206a2f7"
seed = 0x114514

result = decrypt(ch, seed)
print(result)

这里后面有个非预期的解法(也可能是我太菜了),import moon模块,然后help(moon)

可以得到以下信息:

image-20250406111457273

这里就能猜出来大概逻辑了,ida找密文就行了

最终flag:

1
flag{but_y0u_l00k3d_up_@t_th3_mOOn}

” 希望大家都能在满地都是六便士的街上,抬起头看到自己的月光“

Lake

出了个old school的编程语言pascal,最早的国内OI比赛的时候用的这个,也是人生学的第一门编程语言,出题也有纪念意义了。

对于pascal的编译环境配置这里提一下:现在的fpc编译器已经没有直接支持x64版本的了,如果想在win下配置fpc,那么需要先下载32位的编译器,然后再在同目录下安装交叉编译器,这样就能得到可进行64位编译的fpc pascal编译器了。

这里先把题目源码放上:

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
program WaldenObf;

uses
SysUtils, Crt;

const
OP_ADD = $01;
OP_SUB = $02;
OP_MUL = $03;
OP_DIV = $04;
OP_MOD = $05;
OP_AND = $06;
OP_OR = $07;
OP_XOR = $08;

var
a: array[0..94] of Integer = (
$6A, $5C, $46, $13, $5E, $46, $40, $47, $13, $5F, $5A, $45, $56, $13, $5A, $5D,
$13, $47, $5B, $56, $13, $43, $41, $56, $40, $56, $5D, $47, $1F, $13, $5F, $52,
$46, $5D, $50, $5B, $13, $4A, $5C, $46, $41, $40, $56, $5F, $55, $13, $5C, $5D,
$13, $56, $45, $56, $41, $4A, $13, $44, $52, $45, $56, $1F, $13, $55, $5A, $5D,
$57, $13, $4A, $5C, $46, $41, $13, $56, $47, $56, $41, $5D, $5A, $47, $4A, $13,
$5A, $5D, $13, $56, $52, $50, $5B, $13, $5E, $5C, $5E, $56, $5D, $47, $1D
);

b: array[0..73] of Integer = (
$6B, $94, $94, $91, $98, $45, $98, $99, $86, $93, $89, $45, $94, $93, $45, $99,
$8D, $8A, $8E, $97, $45, $8E, $98, $91, $86, $93, $89, $45, $94, $8B, $45, $94,
$95, $95, $94, $97, $99, $9A, $93, $8E, $99, $8E, $8A, $98, $45, $86, $93, $89,
$45, $91, $94, $94, $90, $45, $99, $94, $9C, $86, $97, $89, $45, $86, $93, $94,
$99, $8D, $8A, $97, $45, $91, $86, $93, $89, $53
);

c: array[0..55] of Integer = (
$23, $1F, $12, $5, $12, $57, $1E, $4, $57, $19, $18, $57, $18, $3, $1F, $12,
$5, $57, $1B, $16, $19, $13, $4C, $57, $3, $1F, $12, $5, $12, $57, $1E, $4,
$57, $19, $18, $57, $18, $3, $1F, $12, $5, $57, $1B, $1E, $11, $12, $57, $15,
$2, $3, $57, $3, $1F, $1E, $4, $59
);

d: array[0..49] of Integer = (
$46, $67, $6F, $18, $68, $64, $5D, $59, $6B, $5D, $18, $61, $66, $68, $6D, $6C,
$18, $71, $67, $6D, $6A, $18, $6D, $66, $5C, $5D, $6A, $6B, $6C, $59, $66, $5C,
$61, $66, $5F, $18, $67, $5E, $18, $6C, $60, $5D, $18, $4F, $59, $64, $5C, $5D,
$66, $32
);

con: array[0..43] of Integer = (
$6B, $47, $46, $4F, $5A, $49, $5C, $5D, $44, $49, $5C, $41, $47, $46, $5B, $6,
$6D, $46, $42, $47, $51, $8, $51, $47, $5D, $5A, $8, $5C, $41, $45, $4D, $8,
$4A, $51, $8, $5C, $40, $4D, $8, $44, $49, $43, $4D, $6
);

err: array[0..35] of Integer = (
$65, $49, $51, $4A, $4D, $8, $51, $47, $5D, $8, $5B, $40, $47, $5D, $44, $4C,
$8, $4C, $47, $8, $45, $47, $5A, $4D, $8, $5C, $47, $8, $4E, $41, $46, $4C,
$8, $41, $5C, $6
);

t: array[0..39] of Byte;
p: array[0..39] of Byte = (
$4A, $AB, $9B, $1B, $61, $B1, $F3, $32, $D1, $8B,
$73, $EB, $E9, $73, $6B, $22, $81, $83, $23, $31,
$CB, $1B, $22, $FB, $25, $C2, $81, $81, $73, $22,
$FA, $03, $9C, $4B, $5B, $49, $97, $87, $DB, $51
);

opcode: array[0..122] of Integer = (
OP_SUB, 2, $0C,
OP_ADD, 26, $55,
OP_ADD, 35, $0C,
OP_SUB, 14, $09,
OP_ADD, 27, $06,
OP_XOR, 6, $05,
OP_XOR, 1, $05,
OP_SUB, 27, $0E,
OP_SUB, 25, $03,
OP_SUB, 26, $04,
OP_XOR, 4, $08,
OP_ADD, 3, $0C,
OP_SUB, 12, $0A,
OP_ADD, 37, $02,
OP_ADD, 32, $02,
OP_ADD, 9, $0C,
OP_XOR, 26, $05,
OP_SUB, 4, $0D,
OP_XOR, 8, $0F,
OP_SUB, 10, $0E,
OP_ADD, 16, $07,
OP_ADD, 12, $07,
OP_XOR, 34, $08,
OP_XOR, 21, $0A,
OP_ADD, 39, $7E,
OP_SUB, 7, $02,
OP_XOR, 15, $03,
OP_XOR, 10, $0A,
OP_ADD, 34, $0B,
OP_SUB, 18, $08,
OP_SUB, 25, $09,
OP_XOR, 14, $06,
OP_XOR, 0, $05,
OP_ADD, 10, $08,
OP_XOR, 27, $07,
OP_XOR, 13, $06,
OP_XOR, 13, $04,
OP_XOR, 23, $0C,
OP_XOR, 34, $0E,
OP_SUB, 18, $34,
OP_ADD, 38, $77
);

pos: Integer = 0;

procedure add(index: Integer; operand: Byte);
begin
t[index] := t[index] + operand;
end;

procedure sub(index: Integer; operand: Byte);
begin
t[index] := t[index] - operand;
end;

procedure mul(index: Integer; operand: Byte);
begin
t[index] := t[index] * operand;
end;

procedure divi(index: Integer; operand: Byte);
begin
if operand <> 0 then
t[index] := t[index] div operand;
end;

procedure mod_(index: Integer; operand: Byte);
begin
t[index] := t[index] mod operand;
end;

procedure andt(index: Integer; operand: Byte);
begin
t[index] := t[index] and operand;
end;

procedure ort(index: Integer; operand: Byte);
begin
t[index] := t[index] or operand;
end;

procedure xort(index: Integer; operand: Byte);
begin
t[index] := t[index] xor operand;
end;

procedure obf(var state: array of Byte);
var
temp: array[0..39] of Byte;
i: Integer;
begin
for i := 0 to 39 div 4 do
begin
if i*4+2 <= 39 then
temp[i*4+0] := (state[i*4+2] shr 5) or (state[i*4+1] shl 3);
if i*4+3 <= 39 then
temp[i*4+1] := (state[i*4+3] shr 5) or (state[i*4+2] shl 3);
if i*4+0 <= 39 then
temp[i*4+2] := (state[i*4+0] shr 5) or (state[i*4+3] shl 3);
if i*4+1 <= 39 then
temp[i*4+3] := (state[i*4+1] shr 5) or (state[i*4+0] shl 3);
end;
for i := 0 to 39 do
state[i] := temp[i];
end;

var
i, j: Integer;
cmd, index, operand: Integer;
inputStr: string;
begin

for i := 0 to 94 do
begin
a[i] := a[i] xor $33;
Write(Chr(a[i]));
Flush(Output);
Sleep(100);
end;
WriteLn;

for i := 0 to 73 do
begin
b[i] := b[i] - $25;
Write(Chr(b[i]));
Flush(Output);
Sleep(100);
end;
WriteLn;

for i := 0 to 55 do
begin
c[i] := c[i] xor $77;
Write(Chr(c[i]));
Flush(Output);
Sleep(100);
end;
WriteLn;

Sleep(500);

for i := 0 to 49 do
begin
d[i] := d[i] + $08;
Write(Chr(d[i]));
Flush(Output);
Sleep(200);
end;
WriteLn;

ReadLn(inputStr);
FillChar(t, SizeOf(t), 0);
for i := 1 to Length(inputStr) do
begin
if (i-1) <= High(t) then
t[i-1] := Ord(inputStr[i]);
end;

i := 0;
while i < Length(opcode) do
begin
cmd := opcode[i];
index := opcode[i+1];
operand := opcode[i+2];
Inc(i, 3);

case cmd of
OP_ADD: add(index, operand);
OP_SUB: sub(index, operand);
OP_MUL: mul(index, operand);
OP_DIV: divi(index, operand);
OP_MOD: mod_(index, operand);
OP_AND: andt(index, operand);
OP_OR: ort(index, operand);
OP_XOR: xort(index, operand);
end;
end;

obf(t);

pos := 0;
for j := 0 to High(t) do
if t[j] <> p[j] then
pos := 1;

if pos = 0 then
begin
for i := 0 to 43 do
begin
con[i] := con[i] xor $28;
Write(Chr(con[i]));
Flush(Output);
Sleep(100);
end;
end
else
begin
for i := 0 to 35 do
begin
err[i] := err[i] xor $28;
Write(Chr(err[i]));
Flush(Output);
Sleep(100);
end;
end;
WriteLn;
end.

然后我们拿到这个程序运行一下发现是类似于打字机的效果,然后推测肯定有sleep函数

通过sleep函数跟到主函数

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
void __noreturn sub_100001B70()
{
sub_100008570();
word_100020040 = -1;
do
{
aJF[(unsigned __int16)++word_100020040] ^= 0x33u;
v0 = sub_10000C190();
sub_10000C760(0, v0, LOBYTE(aJF[(unsigned __int16)word_100020040]));
sub_100008510();
sub_10000C2F0(v0);
sub_100008510();
if ( qword_100021B50 )
v1 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v1 = &unk_100020588;
sub_10000C0C0(v1);
sub_100008510();
sub_1000130D0(100);
}
while ( word_100020040 < 94 );
v2 = sub_10000C190();
sub_10000C310(v2);
sub_100008510();
word_100020040 = -1;
do
{
*(_WORD *)&byte_100015210[2 * (unsigned __int16)++word_100020040] -= 37;
v3 = sub_10000C190();
sub_10000C760(0, v3, byte_100015210[2 * (unsigned __int16)word_100020040]);
sub_100008510();
sub_10000C2F0(v3);
sub_100008510();
if ( qword_100021B50 )
v4 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v4 = &unk_100020588;
sub_10000C0C0(v4);
sub_100008510();
sub_1000130D0(100);
}
while ( word_100020040 < 73 );
v5 = sub_10000C190();
sub_10000C310(v5);
sub_100008510();
word_100020040 = -1;
do
{
*(_WORD *)&byte_1000152B0[2 * (unsigned __int16)++word_100020040] ^= 0x77u;
v6 = sub_10000C190();
sub_10000C760(0, v6, byte_1000152B0[2 * (unsigned __int16)word_100020040]);
sub_100008510();
sub_10000C2F0(v6);
sub_100008510();
if ( qword_100021B50 )
v7 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v7 = &unk_100020588;
sub_10000C0C0(v7);
sub_100008510();
sub_1000130D0(100);
}
while ( word_100020040 < 55 );
v8 = sub_10000C190();
sub_10000C310(v8);
sub_100008510();
sub_1000130D0(500);
word_100020040 = -1;
do
{
aFgo[(unsigned __int16)++word_100020040] += 8;
v9 = sub_10000C190();
sub_10000C760(0, v9, LOBYTE(aFgo[(unsigned __int16)word_100020040]));
sub_100008510();
sub_10000C2F0(v9);
sub_100008510();
if ( qword_100021B50 )
v10 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v10 = &unk_100020588;
sub_10000C0C0(v10);
sub_100008510();
sub_1000130D0(200);
}
while ( word_100020040 < 49 );
v11 = sub_10000C190();
sub_10000C310(v11);
sub_100008510();
v12 = sub_10000C160();
sub_10000CB90(v12, byte_100020090, 255);
sub_100008510();
sub_10000C940(v12);
sub_100008510();
sub_1000026E0(byte_100020010, 40, 0);
v13 = (unsigned __int8)byte_100020090[0];
if ( byte_100020090[0] )
{
word_100020040 = 0;
do
{
if ( word_100020040++ <= 39LL )
byte_100020010[word_100020040 - 1] = byte_100020090[(unsigned __int8)word_100020040];
}
while ( v13 > word_100020040 );
}
word_100020040 = 0;
while ( word_100020040 < 123LL )
{
word_100020060 = word_100015470[(unsigned __int16)word_100020040];
word_100020070 = word_100015470[word_100020040 + 1];
word_100020080 = word_100015470[word_100020040 + 2];
word_100020040 += 3;
if ( word_100020060 >= 1 )
{
switch ( word_100020060 )
{
case 1:
sub_1000017A0((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 2:
sub_1000017E0((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 3:
sub_100001820((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 4:
sub_100001860((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 5:
sub_1000018B0((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 6:
sub_1000018F0((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 7:
sub_100001930((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
case 8:
sub_100001970((unsigned int)word_100020070, (unsigned __int8)word_100020080);
break;
}
}
}
sub_1000019B0(byte_100020010, 39);
word_100015570 = 0;
word_100020050 = -1;
while ( 1 )
{
++word_100020050;
if ( byte_100020010[(unsigned __int16)word_100020050] != byte_100015440[(unsigned __int16)word_100020050] )
word_100015570 = 1;
if ( word_100020050 >= 39 )
{
if ( word_100015570 )
{
word_100020040 = -1;
do
{
aEiqjmQgGDlLgEg[(unsigned __int16)++word_100020040] ^= 0x28u;
v16 = sub_10000C190();
sub_10000C760(0, v16, LOBYTE(aEiqjmQgGDlLgEg[(unsigned __int16)word_100020040]));
sub_100008510();
sub_10000C2F0(v16);
sub_100008510();
if ( qword_100021B50 )
v17 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v17 = &unk_100020588;
sub_10000C0C0(v17);
sub_100008510();
sub_1000130D0(100);
}
while ( word_100020040 < 35 );
}
else
{
word_100020040 = -1;
do
{
aKgfoziDiAgf[(unsigned __int16)++word_100020040] ^= 0x28u;
v14 = sub_10000C190();
sub_10000C760(0, v14, LOBYTE(aKgfoziDiAgf[(unsigned __int16)word_100020040]));
sub_100008510();
sub_10000C2F0(v14);
sub_100008510();
if ( qword_100021B50 )
v15 = (void *)qword_100021B50((unsigned int)dword_100020580);
else
v15 = &unk_100020588;
sub_10000C0C0(v15);
sub_100008510();
sub_1000130D0(100);
}
while ( word_100020040 < 43 );
}
v18 = sub_10000C190();
sub_10000C310(v18);
sub_100008510();
sub_100008980();
}
}
}

观察到实现了一个类vm,然后后面的sub_1000019B0是个字节位移的加密,灵感来源2024熵密杯某题(

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
char __fastcall sub_1000019B0(__int64 a1, __int64 a2)
{
v7 = a1;
v6 = a2;
v3 = -1;
do
{
if ( 4LL * ++v3 + 2 <= 39 )
v5[4 * v3] = (8 * *(_BYTE *)(v7 + 4LL * v3 + 1)) | (*(_BYTE *)(v7 + 4LL * v3 + 2) >> 5);
if ( 4LL * v3 + 3 <= 39 )
v5[4 * v3 + 1] = (8 * *(_BYTE *)(v7 + 4LL * v3 + 2)) | (*(_BYTE *)(v7 + 4LL * v3 + 3) >> 5);
if ( 4LL * v3 <= 39 )
v5[4 * v3 + 2] = (*(_BYTE *)(v7 + 4LL * v3) >> 5) | (8 * *(_BYTE *)(v7 + 4LL * v3 + 3));
if ( 4LL * v3 + 1 <= 39 )
v5[4 * v3 + 3] = (8 * *(_BYTE *)(v7 + 4LL * v3)) | (*(_BYTE *)(v7 + 4LL * v3 + 1) >> 5);
}
while ( v3 < 9 );
v4 = -1;
do
{
result = v5[(unsigned __int16)++v4];
*(_BYTE *)(v7 + v4) = result;
}
while ( v4 < 39 );
return result;
}

然后可以发现密文和vm的opcode(这里一开始产生了多解,给各位解题的师傅带来了不好的体验,在这里说声抱歉)

opcode的格式是

1
2
操作名 操作下标,操作值
OP_SUB, 2, 0x0C

各操作与代码对应关系为:

1
2
3
4
5
6
7
8
#define OP_ADD 0x01
#define OP_SUB 0x02
#define OP_MUL 0x03
#define OP_DIV 0x04
#define OP_MOD 0x05
#define OP_AND 0x06
#define OP_OR 0x07
#define OP_XOR 0x08

那么我们先还原位移加密然后再爆破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
table = [0x0002, 0x0002, 0x000C, 0x0001, 0x001A, 0x0055, 0x0001, 0x0023, 0x000C, 0x0002, 0x000E, 0x0009,0x0001, 0x001B, 0x0006, 0x0008, 0x0006, 0x0005, 0x0008, 0x0001, 0x0005, 0x0002, 0x001B, 0x000E, 0x0002, 0x0019, 0x0003,0x0002, 0x001A, 0x0004, 0x0008, 0x0004, 0x0008, 0x0001, 0x0003, 0x000C, 0x0002, 0x000C, 0x000A, 0x0001, 0x0025,0x0002, 0x0001, 0x0020, 0x0002, 0x0001, 0x0009, 0x000C, 0x0008, 0x001A, 0x0005, 0x0002, 0x0004, 0x000D, 0x0008, 0x0008,0x000F, 0x0002, 0x000A, 0x000E, 0x0001, 0x0010, 0x0007, 0x0001, 0x000C, 0x0007, 0x0008, 0x0022, 0x0008, 0x0008,0x0015, 0x000A, 0x0001, 0x0027, 0x007E, 0x0002, 0x0007, 0x0002, 0x0008, 0x000F, 0x0003, 0x0008, 0x000A, 0x000A,0x0001, 0x0022, 0x000B, 0x0002, 0x0012, 0x0008, 0x0002, 0x0019, 0x0009, 0x0008, 0x000E, 0x0006, 0x0008, 0x0000,0x0005, 0x0001, 0x000A, 0x0008, 0x0008, 0x001B, 0x0007, 0x0008, 0x000D, 0x0006, 0x0008, 0x000D, 0x0004, 0x0008,0x0017, 0x000C, 0x0008, 0x0022, 0x000E, 0x0002, 0x0012, 0x0034, 0x0001, 0x0026, 0x0077][:123]
ss = [0x4a,0xab,0x9b,0x1b,0x61,0xb1,0xf3,0x32,0xd1,0x8b,0x73,0xeb,0xe9,0x73,0x6b,0x22,0x81,0x83,0x23,0x31,0xcb,0x1b,0x22,0xfb,0x25,0xc2,0x81,0x81,0x73,0x22,0xfa,0x3,0x9c,0x4b,0x5b,0x49,0x97,0x87,0xdb,0x51][:40]
# ss = [0x63,0x69,0x55,0x73,0x66,0x4c,0x36,0x3e,0x7d,0x7a,0x31,0x6e,0x64,0x5d,0x2e,0x6d,0x66,0x30,0x30,0x64,0x5f,0x79,0x63,0x64,0x30,0x24,0xb8,0x50,0x40,0x6e,0x64,0x5f,0x69,0x33,0x89,0x6b,0x6a,0x32,0xf0,0xfb][:40]
#print(ss, len(ss))
s = []
for i in range(0, 40, 4):
s.append(((ss[i+3]>>3)|(ss[i+2]<<5))&0xff)
s.append(((ss[i]>>3)|(ss[i+3]<<5))&0xff)
s.append(((ss[i+1]>>3)|(ss[i]<<5))&0xff)
s.append(((ss[i+2]>>3)|(ss[i+1]<<5))&0xff)
#print(s)
flag = [0] * 40
flag1 = [0] * 40
for k in range(40):
for j in range(32, 127):
flag[k] = j
for i in range(0, len(table), 3):
if table[i] == 1:
flag[table[i+1]] += table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 2:
flag[table[i+1]] -= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 3:
flag[table[i+1]] *= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 4:
flag[table[i+1]] //= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 5:
flag[table[i+1]] %= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 6:
flag[table[i+1]] &= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 7:
flag[table[i+1]] |= table[i+2]
flag[table[i+1]]&=0xff
elif table[i] == 8:
flag[table[i+1]] ^= table[i+2]
flag[table[i+1]]&=0xff

if flag[k] == s[k]:
flag1[k] = j
#print(k, j)
break
# print("".join(map(chr, flag)))
for i in range(len(flag1)):
print(chr(flag1[i]), end="")

最终得到flag:

flag{L3@rn1ng_1n_0ld_sch00l_@nd_g3t_j0y}

梭罗的《瓦尔登湖》非常值得一读(又双叒叕植入书籍推荐哈哈)

“最大的收获和价值远不能受到人们的赞赏。我们很容易怀疑它们是否存在。我们很快把它们忘记了。它们是最高的现实。也许最惊人、最真实的事实从来没有在人与人之间交流过。我每天生命的最真实的收获,就像朝霞暮霭那样难以捉摸,无法言传。那是我抓到的一点儿星尘,一片彩虹。”

Summer

出这题的设想和灵感来自于我的CTF领路人,然后一个有趣的Haskell题目就应运而生了。

函数式语言的最大特点就是惰性计算和使用惰性列表来进行存储,惰性计算就是在调用一个值之前不会对这个值进行计算,惰性列表存储类似于数据结构中的链表,存在头指针,然后跟着的是存储的数据,数据存储是不连续的,也就是为什么这个题的密文比较难寻找的原因了。

照例放一下题目的源码:

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
{-# LANGUAGE ScopedTypeVariables #-}
import Control.Concurrent (threadDelay)
import Data.Word (Word8)
import Data.Bits (xor)
import Data.Char (ord)
import Data.Array.IO
( getElems,
newListArray,
readArray,
writeArray,
MArray(newArray),
IOUArray )
import Data.Array.MArray (readArray, writeArray)
import System.IO (hFlush, stdout)

a :: [Word8]
a =
[ 0xec, 0xcf, 0xac, 0xf7, 0xe8, 0x48, 0x15, 0x64
, 0x93, 0x40, 0xcf, 0x6a, 0x86, 0x52, 0xfc, 0xcc
, 0xf6, 0x86, 0x7a, 0x0d, 0x0d, 0x99, 0xda, 0xbc
, 0x36, 0xbb, 0xbf, 0x32, 0x8d, 0x27, 0x5d, 0xe8
, 0xbd, 0x93, 0x35, 0x31, 0x96, 0xc2, 0x9b, 0x76
, 0x4e, 0x6f, 0x26, 0x37, 0xfe, 0xe3, 0xea, 0x85
, 0xe6, 0xd0
]

keyStr :: String
keyStr = "Inkleqmp%q]Ncqv]Qwoogp"

initSBox :: IOUArray Int Word8 -> [Word8] -> IO ()
initSBox s key = do
let keyLen = length key
k = [ key !! (i `mod` keyLen) | i <- [0..255] ]
mapM_ (\i -> writeArray s i (fromIntegral i)) [0..255]
let loop :: Int -> Int -> IO ()
loop j i | i > 255 = return ()
| otherwise = do
si <- readArray s i
let j' = (j + fromIntegral si + fromIntegral (k !! i)) `mod` 256
sj <- readArray s j'
writeArray s i sj
writeArray s j' si
loop j' (i + 1)
loop 0 0

crypt :: IOUArray Int Word8 -> IOUArray Int Word8 -> Int -> IO ()
crypt s d len = do
let loop :: Int -> Int -> Int -> IO ()
loop i j k | k >= len = return ()
| otherwise = do
let i' = (i + 1) `mod` 256
si <- readArray s i'
let j' = (j + fromIntegral si) `mod` 256
s_i <- readArray s i'
s_j <- readArray s j'
writeArray s i' s_j
writeArray s j' s_i
s_i' <- readArray s i'
s_j' <- readArray s j'
let t = (fromIntegral s_i' + fromIntegral s_j') `mod` 256
st <- readArray s t
d_k <- readArray d k
let newVal = d_k `xor` (st `xor` 0x23)
writeArray d k newVal
loop i' j' (k + 1)
loop 0 0 0

compareBytes :: [Word8] -> [Word8] -> Int -> Bool
compareBytes data1 data2 len = (take len data1) == (take len data2)

main :: IO ()
main = do

putStrLn "The waters of the world will meet again,"
hFlush stdout
threadDelay 1000000
putStrLn "and the Arctic Ocean and the Nile will merge in wet clouds.\n"
putStrLn "Even if we roam, every road will bring us home.\n"
threadDelay 2000000
putStrLn "Now,let's find the truth behind this summer:"

pDataStr <- getLine
let inputStr = take 511 pDataStr
len = length inputStr
pDataArr <- newListArray (0, len - 1) (map (fromIntegral . ord) inputStr) :: IO (IOUArray Int Word8)

let keyBytesOriginal = map (fromIntegral . ord) keyStr :: [Word8]
keyModified = [ if i < 22 then b `xor` 0x2 else b | (i, b) <- zip [0..] keyBytesOriginal ]
s <- newArray (0, 255) 0 :: IO (IOUArray Int Word8)
initSBox s keyModified
crypt s pDataArr len
pDataFinal <- getElems pDataArr
if compareBytes pDataFinal a len && len == length a
then putStrLn "I believe that you know what I want you to find."
else putStrLn "Maybe you should try again"

拿到这个题目,没有什么好入手的办法,动调或者摁撕汇编了(预期其实就是这两种方法)

很多人应该找到了那个反编译Haskell的库(https://github.com/gereeter/hsdecomp)

,但是实在是太老了,反编译不了是预期的。所以我们只能硬着头皮进行调试

以下部分分析思路来自5m10v3师傅,我觉得写得非常好,就放到这里跟大家一块分享

进入 main 函数,可以看到 hs_main 以 ZCMain_main_closure 作为参数,指向 haskell 程序的真正入口点。

img

ZCMain_main_closure里面可以看到有Main_main_info,这个函数也调用了很多底层的函数

ghczminternal_GHCziInternalziBase这个函数的第一个参数指向了sub_40F318,而sub_40F318又调用sub_40F2D0,这个函数其实就是打印字符串

ghczminternal_GHCziInternalziList_length_info这个函数是检测长度的,我们可以断在这里看密文和密钥的长度

这里的这个rbx最后实际存储了长度,第一次断在这里获得我们输入的长度,第二次断下获得密钥长度0x16,第三次断下获得密文长度0x32

我们继续向下调试,在字符串列表能看到一串Inkleqmp%q]Ncqv]Qwoogp,推测这就是被加密的密钥,那么我们调试到loc_43E370的时候发现这里经过了一些处理,会有字符出现,再次断到这里发现这里就是对密钥的处理

(这里是需要连续跑到三次才能跑到下一个字符,第一次会返回字符实际的值,第二次返回一个1,第三次会返回对应的长度,比如1,2)

和被加密的密钥对比,发现密钥异或了0x2,那么真正的密钥就是Klingsor’s_Last_Summer(克林索尔的最后夏天)

我们继续往下调试,发现这里其实不是单纯解密了密钥,因为密钥是循环被写入一个位置的,会调试出大于0x16的序列,最后一次的返回值是0xFF

循环写入密钥,并且最后返回的长度是0xFF,我们可以联想到RC4的KSA过程,这里基本就可以确定是rc4的加密算法了,那么接下来是寻找密文的过程,通过搜索Haskell的惰性存储方式,了解到非字符串的数据是以惰性链表的形式存储的它的内存存储方式和 C 语言中的数组非常不同,不是连续的内存块,而是由链式结构组成。

每个节点保存两个指针:一个指向当前的数据值,一个指向下一个列表元素(即尾部)

那么我们已知密钥出现在data段,那么去data段寻找密文

很快就发现了在密钥附近有链式的存储结构

然后就可以提取出密文,密文为

1
2
3
4
5
6
7
  0xec, 0xcf, 0xac, 0xf7, 0xe8, 0x48, 0x15, 0x64
, 0x93, 0x40, 0xcf, 0x6a, 0x86, 0x52, 0xfc, 0xcc
, 0xf6, 0x86, 0x7a, 0x0d, 0x0d, 0x99, 0xda, 0xbc
, 0x36, 0xbb, 0xbf, 0x32, 0x8d, 0x27, 0x5d, 0xe8
, 0xbd, 0x93, 0x35, 0x31, 0x96, 0xc2, 0x9b, 0x76
, 0x4e, 0x6f, 0x26, 0x37, 0xfe, 0xe3, 0xea, 0x85
, 0xe6, 0xd0

尝试用密文解密rc4发现仍然不对,但是稍微根据flag头思考一下就会发现解得的值跟密文差了xor 0x23,那么就能判断rc4之后还xor了0x23,当然直接动调单步跟也是可以的。

Haskell这类的函数式语言程序最大的问题在于下内存读写断点是无法跟到比较逻辑的,这或许就是惰性计算的魅力所在吧。

最终解题脚本如下:

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
def initialize_key_schedule(key):
key_length = len(key)
sbox = list(range(256))
j = 0
for i in range(256):
j = (j + sbox[i] + key[i % key_length]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
return sbox

def generate_keystream(sbox):
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + sbox[i]) % 256
sbox[i], sbox[j] = sbox[j], sbox[i]
keystream_value = sbox[(sbox[i] + sbox[j]) % 256]
yield keystream_value

key = [ord(c) for c in 'Klingsor\'s_Last_Summer']
ciphertext = bytes.fromhex('eccfacf7e84815649340cf6a8652fcccf6867a0d0d99dabc36bbbf328d275de8bd93353196c29b764e6f2637fee3ea85e6d0')
sbox = initialize_key_schedule(key)
keystream_generator = generate_keystream(sbox)
plaintext = ""
for byte in ciphertext:
plaintext += chr(byte ^ 0x23^next(keystream_generator))
print(plaintext)

得到flag:

flag{Us3_H@sk3ll_t0_f1nd_th3_truth_1n_th1s_Summ3R}

希望师傅们通过这道题可以初窥Haskell逆向的冰山一角,本人也是在出题的过程中慢慢学习的Haskell相关知识,另外推荐《克林索尔的最后夏天》,黑塞用诗意的笔调勾画了克林索尔的形象。

“于是我愿重走这条路,带着不同的感触,聆听小溪,凝视夜空,一次又一次。”

写在最后

笔行至此,忽然想到某老贼书里酒德麻衣悠悠唱起的和歌:”或许是不知梦的缘故,流离之人追逐幻影“,就以一句对其的应和作为结尾吧:

”人总要抱紧什么才知道自己真的存在,哪怕那只是幻影“


XYCTF_Reverse出题小记
http://example.com/2025/04/08/XYCTF_Reverse出题小记/
作者
peace dawn
发布于
2025年4月8日
许可协议