WMCTF 2025 re wp

catfriend

image-20250920155316459

校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/usr/bin/env python3
import struct

# (value, destination offset in the 43-byte buffer)
chunks = [
(0x61357B4654434D57, 0),
(0x312D623266386533, 8),
(0x663661342D643763, 16),
(0x64302D653938622D, 24),
(0x6234613166326333, 32),
(0x7D63356234613166, 35),
]

buf = bytearray(43)
for value, offset in chunks:
buf[offset:offset + 8] = struct.pack('<Q', value)

flag = buf.decode()
print(flag)

WMCTF{5a3e8f2b-1c7d-4a6f-b89e-0d3c2f1a4b5c}

appfriend

jadx打开

逆向native层

image-20250920160150873

很明显的sm4

image-20250920161439543

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
#!/usr/bin/env python3
from dataclasses import dataclass

SBOX = [
0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05,
0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62,
0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6,
0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8,
0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35,
0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87,
0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e,
0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1,
0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3,
0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f,
0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51,
0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8,
0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0,
0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84,
0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48,
]

CK = [
0x00070e15, 0x1c232a31, 0x383f464d, 0x545b6269, 0x70777e85, 0x8c939aa1, 0xa8afb6bd, 0xc4cbd2d9,
0xe0e7eef5, 0xfc030a11, 0x181f262d, 0x343b4249, 0x50575e65, 0x6c737a81, 0x888f969d, 0xa4abb2b9,
0xc0c7ced5, 0xdce3eaf1, 0xf8ff060d, 0x141b2229, 0x30373e45, 0x4c535a61, 0x686f767d, 0x848b9299,
0xa0a7aeb5, 0xbcc3cad1, 0xd8dfe6ed, 0xf4fb0209, 0x10171e25, 0x2c333a41, 0x484f565d, 0x646b7279,
]

FK = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc]
KEY = bytes.fromhex("0123456789abcdeffedcba9876543210")
EXPECTED = bytes.fromhex(
"dbe98e0ad47ed658741cd38ef8595985"
"8177d9f3a8f90f24cfe14fd11a313b72"
"002a8a4efa863ccad024ac0300bb40d2"
)


def rotl32(x: int, n: int) -> int:
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))


def tau(word: int) -> int:
return (
(SBOX[(word >> 24) & 0xFF] << 24)
| (SBOX[(word >> 16) & 0xFF] << 16)
| (SBOX[(word >> 8) & 0xFF] << 8)
| SBOX[word & 0xFF]
)


def T_key(x: int) -> int:
t = tau(x)
return t ^ rotl32(t, 13) ^ rotl32(t, 23)


def T_round(x: int) -> int:
t = tau(x)
return t ^ rotl32(t, 2) ^ rotl32(t, 10) ^ rotl32(t, 18) ^ rotl32(t, 24)


def expand_rk(key: bytes) -> list[int]:
MK = [int.from_bytes(key[i:i + 4], "big") for i in range(0, 16, 4)]
K = [MK[i] ^ FK[i] for i in range(4)]
for i in range(32):
x = K[i + 1] ^ K[i + 2] ^ K[i + 3] ^ CK[i]
K.append(K[i] ^ T_key(x))
return K[4:]


def sm4_ecb_decrypt(cipher: bytes, rk: list[int]) -> bytes:
rk_dec = rk[::-1]
out = bytearray()
for block_offset in range(0, len(cipher), 16):
block = cipher[block_offset:block_offset + 16]
X = [int.from_bytes(block[i:i + 4], "big") for i in range(0, 16, 4)]
for r in range(32):
x = X[r + 1] ^ X[r + 2] ^ X[r + 3] ^ rk_dec[r]
X.append(X[r] ^ T_round(x))
words = X[-4:][::-1]
for w in words:
out.extend(w.to_bytes(4, "big"))
pad = out[-1]
if not 1 <= pad <= 16 or out[-pad:] != bytes([pad]) * pad:
raise ValueError("Invalid PKCS#7 padding")
return bytes(out[:-pad])


if __name__ == "__main__":
round_keys = expand_rk(KEY)
flag = sm4_ecb_decrypt(EXPECTED, round_keys)
print(flag.decode())

Want2BecomeMagicalGirl[复现]

WMCTF2025 Want2BecomeMagicalGirl - Britney

我原以为逻辑是这样的(通过blutter看到的):

image-20250925191431009

image-20250925191519986

check 里:

  1. 初始化 AesCrypt,从 native_add 获取一些 native 值getSymgetKey),组装 Key/IV
  2. 根据文本长度补齐到 16 字节块(自定义 padding),UTF-8 编码;
  3. 设定 AES 的 keys & mode,执行 aesEncrypt
  4. 把密文 Uint8List 转十六进制字符串(uint8ListToHex),再 send()
  5. 拿到 send() 的返回字符串后(通过 .then(...)),再 setState:如果返回值等于一段硬编码字符串("8sAFX45zT7u..."),显示成功文案“You have become a magical girl !!”,否则显示“This spell has no magic power”。
1
8sAFX45zT7uc0vSUyFNNly1h/d5zTt89tV3kcVr5P5n7lRKPyYtxg31zYNB2lPV0c5nf/x2/IK94XV9Ufs9XfaDG5IXxMlZy+Z2nE+ZZRFBSpMoKzQXfUq2TSjJJfQxV

From Base64, AES Decrypt - CyberChef

发现解出来是乱码

出题人给了提示init_array,猜到是做了修改,但是安卓还是不太熟

前端有部分xxtea

密文前端,还有加密逻辑都有了,可以得知是一个aes–>xxtea(中间可能还有rc4生成key)–>base64返回与那一串密文校验

剩下的就是其他操作了,看了wp

WMCTF2025 Want2BecomeMagicalGirl - Britney

复现下

1
__int64 __fastcall magical_girl_aes_crypt_null_safe__Aes::_mixColumns_28d8b0(__int64 a1, __int64 a2)

发现一大串疑似sbox的东西

image-20250925194329359

image-20250925194538093

image-20250925194606638

其中这些函数

1
__int64 magical_girl_EditView_MyEditTextState::check_28bea8()

最重要

image-20250925195223779

包括setkey和mode

image-20250925195246855

加密的数据通过 aesEncrypt_28c6a8 和其他相关函数进行处理,这表明该函数处理的是加密或解密任务。

显然这里肯定会传一个类似实例的东西,借助blutter自带的frida脚本

给了一个blutter_frida.js hook脚本

image-20250925200045853

hook试试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function onLibappLoaded() {
const fn_addr = 0x28c6a8;
Interceptor.attach(libapp.add(fn_addr), {
onEnter: function () {
init(this.context);
let objPtr = getArg(this.context, 1);
const [tptr, cls, values] = getTaggedObjectValue(objPtr);
console.log(
`${cls.name}@${tptr.toString().slice(2)} =`,
JSON.stringify(values, null, 2)
);
},
});
}

或者hook这个

image-20251019195744279

getArg(this.context, 0) 更改第二个参数控制传参打印位置

1
2
3
4
5
6
7
8
9
10
11
function onLibappLoaded() {
const fn_addr = 0x2915a4;
Interceptor.attach(libapp.add(fn_addr), {
onEnter: function () {
init(this.context);
let objPtr = getArg(this.context, 0);
const [tptr, cls, values] = getTaggedObjectValue(objPtr);
console.log(`${cls.name}@${tptr.toString().slice(2)} =`, JSON.stringify(values, null, 2));
}
});
}

image-20251019200651511

hook得到

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
AesCrypt@6f00854529 = {
"off_8!_Aes@6f00854539": {
"off_8!Smi": 4,
"off_c!Smi": 10,
"off_10!_Uint32List@6f00854f49": [
2049295652,
3325250608,
4083330988,
16843781,
6783327,
3327446383,
892756675,
876046022,
3267769349,
76740970,
832871337,
93584751,
2526363713,
2449979691,
2745377410,
2788220909,
3825754614,
1980750045,
3584883295,
1939613106,
2595761419,
3971478998,
958074761,
1250372155,
576656933,
3471369203,
4159958138,
3178546753,
3672808602,
335708009,
3824259859,
1585796434,
1333110389,
1534581020,
3095808527,
3858964317,
4090255858,
2830860526,
272485089,
4131313084,
1712018262,
3467695032,
3733789017,
682872037
],
"off_14!List@6f00854569": [
{
"key": [
0,
0,
0,
0
]
},
{
"key": [
0,
0,
0,
0
]
},
{
"key": [
0,
0,
0,
0
]
},
{
"key": [
0,
0,
0,
0
]
}
],
"off_18!AesMode@6f0045a1b1": {
"parent!_Enum": {
"off_8": "0",
"off_10!String@6f000f7a61": "ecb"
}
},
"off_1c!_Uint8List@6f00854ee9": [
122,
37,
197,
36,
198,
51,
76,
48,
243,
98,
175,
172,
1,
1,
4,
5
],
"off_20!_Uint8List@6f00854f19": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16
]
},
"off_c!Map@6f00854719": "Unhandle class id: 85, Map"
}

aesEncrypt_28c6a8 都第一个参数是输入的flag

image-20251019200938343

16字节的那个有点像key

1
frida -U -f work.pangbai.magic.magical_girl -l blutter_frida.js

反正现在S盒子S盒逆key ecb都有了可以写代码解密了

sbox 我们将这些值均除以2

flutter的整数貌似都是*2存储的

发现还改了一下addroundkey和mixClou

image-20251008214555519

Flutter 函数调用约定: 参数从右往左入栈,最后入栈对象的this指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
typedef struct
{
uint32_t eK[44], dK[44]; // encKey, decKey
int Nr; // 10 rounds
} AesKey;

#define BLOCKSIZE 16 // AES-128分组长度为16字节

// uint8_t y[4] -> uint32_t x
#define LOAD32H(x, y) \
do \
{ \
(x) = ((uint32_t)((y)[0] & 0xff) << 24) | ((uint32_t)((y)[1] & 0xff) << 16) | \
((uint32_t)((y)[2] & 0xff) << 8) | ((uint32_t)((y)[3] & 0xff)); \
} while (0)

// uint32_t x -> uint8_t y[4]
#define STORE32H(x, y) \
do \
{ \
(y)[0] = (uint8_t)(((x) >> 24) & 0xff); \
(y)[1] = (uint8_t)(((x) >> 16) & 0xff); \
(y)[2] = (uint8_t)(((x) >> 8) & 0xff); \
(y)[3] = (uint8_t)((x) & 0xff); \
} while (0)

// 从uint32_t x中提取从低位开始的第n个字节
#define BYTE(x, n) (((x) >> (8 * (n))) & 0xff)

// 密钥扩展中的SubWord(RotWord(temp),字节替换然后循环左移1位
#define MIX(x) (((S[BYTE(x, 2)] << 24) & 0xff000000) ^ ((S[BYTE(x, 1)] << 16) & 0xff0000) ^ \
((S[BYTE(x, 0)] << 8) & 0xff00) ^ (S[BYTE(x, 3)] & 0xff))

// uint32_t x循环左移n位
#define ROF32(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// uint32_t x循环右移n位
#define ROR32(x, n) (((x) >> (n)) | ((x) << (32 - (n))))

// AES-128轮常量,无符号长整型
static const uint32_t rcon[10] = {
0x01000000UL, 0x02000000UL, 0x04000000UL, 0x08000000UL, 0x10000000UL,
0x20000000UL, 0x40000000UL, 0x80000000UL, 0x1B000000UL, 0x36000000UL};
// S盒
unsigned char S[256] = {32, 123, 24, 167, 66, 68, 215, 74, 205, 50, 209, 236, 243, 129, 165, 137, 14, 145, 75, 240, 233, 93, 141, 245, 70, 252, 49, 54, 182, 172, 155, 185, 38, 9, 230, 64, 212, 176, 81, 79, 156, 62, 231, 121, 48, 136, 177, 60, 122, 92, 211, 20, 90, 171, 86, 192, 4, 41, 208, 59, 31, 249, 163, 87, 0, 138, 132, 22, 244, 26, 234, 100, 166, 214, 46, 190, 47, 23, 196, 224, 30, 2, 58, 34, 143, 159, 203, 168, 44, 103, 52, 37, 213, 255, 239, 246, 226, 170, 217, 114, 254, 206, 161, 120, 133, 150, 42, 119, 202, 193, 55, 116, 162, 94, 108, 253, 184, 77, 125, 112, 179, 221, 207, 113, 115, 97, 248, 25, 72, 227, 99, 51, 61, 21, 174, 152, 229, 128, 189, 188, 130, 198, 148, 1, 228, 222, 6, 80, 149, 223, 71, 247, 144, 139, 69, 154, 110, 7, 173, 28, 53, 131, 104, 3, 111, 91, 183, 251, 29, 197, 16, 124, 216, 106, 204, 105, 142, 36, 76, 57, 180, 160, 11, 82, 232, 169, 178, 140, 10, 191, 40, 134, 109, 175, 218, 65, 250, 117, 181, 67, 195, 96, 98, 43, 85, 242, 158, 45, 18, 35, 13, 219, 107, 199, 56, 127, 95, 151, 8, 237, 225, 187, 238, 157, 210, 146, 73, 63, 220, 88, 135, 194, 186, 153, 201, 78, 241, 33, 235, 19, 101, 89, 118, 12, 200, 5, 164, 84, 147, 27, 102, 17, 39, 83, 126, 15};

// 逆S盒
unsigned char inv_S[256] = {64, 143, 81, 163, 56, 245, 146, 157, 218, 33, 188, 182, 243, 210, 16, 255, 170, 251, 208, 239, 51, 133, 67, 77, 2, 127, 69, 249, 159, 168, 80, 60, 0, 237, 83, 209, 177, 91, 32, 252, 190, 57, 106, 203, 88, 207, 74, 76, 44, 26, 9, 131, 90, 160, 27, 110, 214, 179, 82, 59, 47, 132, 41, 227, 35, 195, 4, 199, 5, 154, 24, 150, 128, 226, 7, 18, 178, 117, 235, 39, 147, 38, 183, 253, 247, 204, 54, 63, 229, 241, 52, 165, 49, 21, 113, 216, 201, 125, 202, 130, 71, 240, 250, 89, 162, 175, 173, 212, 114, 192, 156, 164, 119, 123, 99, 124, 111, 197, 242, 107, 103, 43, 48, 1, 171, 118, 254, 215, 137, 13, 140, 161, 66, 104, 191, 230, 45, 15, 65, 153, 187, 22, 176, 84, 152, 17, 225, 248, 142, 148, 105, 217, 135, 233, 155, 30, 40, 223, 206, 85, 181, 102, 112, 62, 246, 14, 72, 3, 87, 185, 97, 53, 29, 158, 134, 193, 37, 46, 186, 120, 180, 198, 28, 166, 116, 31, 232, 221, 139, 138, 75, 189, 55, 109, 231, 200, 78, 169, 141, 213, 244, 234, 108, 86, 174, 8, 101, 122, 58, 10, 224, 50, 36, 92, 73, 6, 172, 98, 194, 211, 228, 121, 145, 149, 79, 220, 96, 129, 144, 136, 34, 42, 184, 20, 70, 238, 11, 219, 222, 94, 19, 236, 205, 12, 68, 23, 95, 151, 126, 61, 196, 167, 25, 115, 100, 93};

/* copy in[16] to state[4][4] */
int loadStateArray(uint8_t (*state)[4], const uint8_t *in)
{
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[j][i] = *in++;
}
}
return 0;
}

/* copy state[4][4] to out[16] */
int storeStateArray(uint8_t (*state)[4], uint8_t *out)
{
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
*out++ = state[j][i];
}
}
return 0;
}

// 密钥扩展,只接受16字初始密钥
int keyExpansion(const uint8_t *key, uint32_t keyLen, AesKey *aesKey)
{

if (NULL == key || NULL == aesKey)
{
printf("keyExpansion param is NULL\n");
return -1;
}

if (keyLen != 16)
{
printf("keyExpansion keyLen = %d, Not support.\n", keyLen);
return -1;
}

uint32_t *w = aesKey->eK; // 加密密钥
uint32_t *v = aesKey->dK; // 解密密钥

// 扩展密钥长度44=4*(10+1)个字,原始密钥128位,4个32位字,Nb*(Nr+1)

/* W[0-3],前4个字为原始密钥 */
for (int i = 0; i < 4; ++i)
{
LOAD32H(w[i], key + 4 * i);
}

/* W[4-43] */
// temp=w[i-1];tmp=SubWord(RotWord(temp))xor Rcon[i/4] xor w[i-Nk]
for (int i = 0; i < 10; ++i)
{
w[4] = w[0] ^ MIX(w[3]) ^ rcon[i];
w[5] = w[1] ^ w[4];
w[6] = w[2] ^ w[5];
w[7] = w[3] ^ w[6];
w += 4;
}

w = aesKey->eK + 44 - 4;
// 解密密钥矩阵为加密密钥矩阵的倒序,方便使用,把ek的11个矩阵倒序排列分配给dk作为解密密钥
// 即dk[0-3]=ek[41-44], dk[4-7]=ek[37-40]... dk[41-44]=ek[0-3]
for (int j = 0; j < 11; ++j)
{
for (int i = 0; i < 4; ++i)
{
v[i] = w[i];
}
w -= 4;
v += 4;
}

return 0;
}

// 轮密钥加
int addRoundKey(uint8_t (*state)[4], const uint32_t *key)
{
uint8_t k[4][4];

/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
k[i][j] = (uint8_t)BYTE(key[j], 3 - i); /* 把 uint32 key[4] 先转换为矩阵 uint8 k[4][4] */
state[i][j] ^= k[i][j];
}
}

return 0;
}

// 字节替换
int subBytes(uint8_t (*state)[4])
{
/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = S[state[i][j]]; // 直接使用原始字节作为S盒数据下标
}
}

return 0;
}

// 逆字节替换
int invSubBytes(uint8_t (*state)[4])
{
/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = inv_S[state[i][j]];
}
}
return 0;
}

// 行移位
int shiftRows(uint8_t (*state)[4])
{
uint32_t block[4] = {0};

/* i: row */
for (int i = 0; i < 4; ++i)
{
// 便于行循环移位,先把一行4字节拼成uint_32结构,移位后再转成独立的4个字节uint8_t
LOAD32H(block[i], state[i]);
block[i] = ROF32(block[i], 8 * i); // block[i]循环左移8*i位,如第0行左移0位
STORE32H(block[i], state[i]);
}
return 0;
}

// 逆行移位
int invShiftRows(uint8_t (*state)[4])
{
uint32_t block[4] = {0};

/* i: row */
for (int i = 0; i < 4; ++i)
{
LOAD32H(block[i], state[i]);
block[i] = ROR32(block[i], 8 * i);
STORE32H(block[i], state[i]);
}

return 0;
}

/* Galois Field (256) Multiplication of two Bytes */
// 两字节的伽罗华域乘法运算
uint8_t GMul(uint8_t u, uint8_t v)
{
uint8_t p = 0;

for (int i = 0; i < 8; ++i)
{
if (u & 0x01)
{
p ^= v;
}

int flag = (v & 0x80);
v <<= 1;
if (flag)
{
v ^= 0x1B;
}

u >>= 1;
}

return p;
}

// 列混合
int mixColumns(uint8_t (*state)[4])
{
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x02, 0x03, 0x01, 0x01},
{0x01, 0x02, 0x03, 0x01},
{0x01, 0x01, 0x02, 0x03},
{0x03, 0x01, 0x01, 0x02}};

/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
tmp[i][j] = state[i][j];
}
}

for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{ // 伽罗华域加法和乘法
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j]) ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}

return 0;
}

// 逆列混合
int invMixColumns(uint8_t (*state)[4])
{
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x0E, 0x0B, 0x0D, 0x09},
{0x09, 0x0E, 0x0B, 0x0D},
{0x0D, 0x09, 0x0E, 0x0B},
{0x0B, 0x0D, 0x09, 0x0E}}; // 使用列混合矩阵的逆矩阵

/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
tmp[i][j] = state[i][j];
}
}

for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j]) ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}

return 0;
}

// AES-128加密接口,输入key应为16字节长度,输入长度应该是16字节整倍数,
// 这样输出长度与输入长度相同,函数调用外部为输出数据分配内存
int aesEncrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *pt, uint8_t *ct, uint32_t len)
{
AesKey aesKey;
uint8_t *pos = ct;
const uint32_t *rk = aesKey.eK; // 加密密钥指针
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};

if (NULL == key || NULL == pt || NULL == ct)
{
printf("param err.\n");
return -1;
}

if (keyLen > 16)
{
printf("keyLen must be 16.\n");
return -1;
}

if (len % BLOCKSIZE)
{
printf("inLen is invalid.\n");
return -1;
}

memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); // 密钥扩展

// 使用ECB模式循环加密多个分组长度的数据
for (int i = 0; i < len; i += BLOCKSIZE)
{
// 把16字节的明文转换为4x4状态矩阵来进行处理
loadStateArray(state, pt);
// 轮密钥加
addRoundKey(state, rk);

for (int j = 1; j < 10; ++j)
{
rk += 4;
subBytes(state); // 字节替换
shiftRows(state); // 行移位
addRoundKey(state, rk); // 轮密钥加
mixColumns(state); // 列混合
}

subBytes(state); // 字节替换
shiftRows(state); // 行移位
// 此处不进行列混合
addRoundKey(state, rk + 4); // 轮密钥加

// 把4x4状态矩阵转换为uint8_t一维数组输出保存
storeStateArray(state, pos);

pos += BLOCKSIZE; // 加密数据内存指针移动到下一个分组
pt += BLOCKSIZE; // 明文数据指针移动到下一个分组
rk = aesKey.eK; // 恢复rk指针到秘钥初始位置
}
return 0;
}

// AES128解密, 参数要求同加密
int aesDecrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *ct, uint8_t *pt, uint32_t len)
{
AesKey aesKey;
uint8_t *pos = pt;
const uint32_t *rk = aesKey.dK; // 解密密钥指针
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};

if (NULL == key || NULL == ct || NULL == pt)
{
printf("param err.\n");
return -1;
}

if (keyLen > 16)
{
printf("keyLen must be 16.\n");
return -1;
}

if (len % BLOCKSIZE)
{
printf("inLen is invalid.\n");
return -1;
}

memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); // 密钥扩展,同加密

for (int i = 0; i < len; i += BLOCKSIZE)
{
// 把16字节的密文转换为4x4状态矩阵来进行处理
loadStateArray(state, ct);
// 轮密钥加,同加密
addRoundKey(state, rk);

for (int j = 1; j < 10; ++j)
{
rk += 4;
invShiftRows(state); // 逆行移位
invSubBytes(state); // 逆字节替换,这两步顺序可以颠倒
invMixColumns(state); // 逆列混合
addRoundKey(state, rk); // 轮密钥加,同加密
}

invShiftRows(state); // 逆行移位
invSubBytes(state); // 逆字节替换
// 此处没有逆列混合
addRoundKey(state, rk + 4); // 轮密钥加,同加密

storeStateArray(state, pos); // 保存明文数据
pos += BLOCKSIZE; // 输出数据内存指针移位分组长度
ct += BLOCKSIZE; // 输入数据内存指针移位分组长度
rk = aesKey.dK; // 恢复rk指针到秘钥初始位置
}
return 0;
}

下一步就是java层的xxtea

标准的xxtea,不过出不来T.T,出题人提示init_proc,猜测可能是被改掉了 ,而且字符串均被加密了

在libnative_add.so里

image-20251008214918515

init_proc()libnative_add.so 里动态解密/打补丁(把“shellcode”写到 mmap 出来的内存,再改写目标函数的前两条指令为 trampoline 跳转)。因此字符串/代码/数据的解密过程发生在 native 层内存复制之前或期间。抓取被解密后的字符串,最可靠的方式是 在解密后 memcpy 把解密内容写入可执行内存时把那块内容 dump 出来,或者在 sub_19F78(看起来像解密)返回时直接 dump 它返回的内存。

中间参考了下出题人的解释:

(●´3`●)やれやれだぜ

“原生库”是指用 C 或 C++ 编写并编译为 .so 文件(Shared Object,共享库) 的代码。

它不是 Java 层代码,而是直接运行在 Linux 层 的二进制代码。

在 Android 应用中,如果你用 JNI(Java Native Interface) 调用本地函数,例如:

1
System.loadLibrary("mylib");

系统库是 Android 平台本身提供的 .so 文件,位于系统分区,比如:

1
2
3
4
5
/system/lib/ 或 /system/lib64/
libc.so
libm.so
liblog.so
libandroid_runtime.so

所有的应用库都是原生库,但不是所有的原生库都是应用库。

名称 含义 由谁编写 存放位置 运行层级
原生库 指用 C/C++ 编写、编译成 .so(共享库)的底层二进制库。包括系统的和应用自己的。 Android 系统或应用开发者 /system/lib//vendor/lib//data/app/.../lib/ Linux 层(Native 层)
应用库 应用自己带的原生库(一个子集),随 APK 打包,用于 JNI 调用。 应用开发者 /data/app/包名/lib/ 应用进程的 Native 层

Android 7.0 的变化:引入“命名空间隔离”

在 Android 7.0(Nougat)之前,应用加载原生库时,它能看到系统 /system/lib/ 目录下几乎所有 .so 文件。
这带来了严重问题:

  • 有些应用会“偷偷”调用 Android 内部未公开的系统库函数(非 SDK 接口);
  • 这些接口未被官方支持,系统更新后容易崩溃;
  • 同名库(系统库 vs 应用自带库)容易“串库”或冲突。

Android 7.0 起,每个进程(尤其是 app 进程)都有独立的 linker namespace

这意味着:

  • 系统库和应用库在“命名空间”上是隔离的;
  • 应用默认只能访问经过允许的系统库(例如 libc.so, libm.so, liblog.so 等);
  • 其他系统内部库(如 libandroid_runtime.so, libbinder.so)对应用是不可见的
  • 如果应用想加载这些“私有系统库”,会报错

原生库的命名空间 | Android Open Source Project

Android 7.0 为原生库引入了命名空间,以限制内部 API 的可见性,听起来很高大上,具体解释就是安卓上的动态链接库使用被约束了,普通开发者不管是编译时链接还是使用 dlsym 都不能获取到被限制的 lib 的符号地址, native 开发能调用的 api 被严重限制,同理 Java 也有类似的约束 hiddenapi 用来限制反射的功能。

ssrlive/fake-dlfcn: dlopen/dlclose/dlsym in Android

本题目使用 fake-dlfcn 来查找调用 libart.so 的符号,同时修改了一些代码来适配高版本安卓。这个项目原理比较简单,扫描 proc/self/maps 得到原生库的基址,并通过解析原生库的 elf 符号表得到偏移,再进行计算得到符号地址。

libart.so是什么?

libart.so 的全称就是 Android Runtime Library

它是 Android 系统中实现 ART(Android Runtime)虚拟机 的核心共享库。

通俗地说:libart.so 就是 Android 上运行 Java/Kotlin 应用的底层“虚拟机引擎”。

fake-dlfcn 是一个“自实现的 dlopen/dlsym”小工具/库,它绕过 Android(尤其是 Nougat 以后的)对 dlopen/dlsym 访问的限制,允许在受限环境下加载系统 .so(比如 libart.so)或直接解析 ELF 符号,从而找到/取到那些符号在运行时的地址。

Android 7+ 有命名空间限制,应用不能任意 dlopen("/system/lib/libart.so"),系统会把很多内部库对应用隐藏;因此普通 dlsym/dlopen 不一定生效。

fake-dlfcn 提供了替代实现(或直接解析 ELF)来读取/解析系统库的符号表并返回函数地址,或者通过把库映射到进程地址空间再解析符号,从而找到调用 libart.so 的函数名/地址,便于你在运行时定位与 hook。

接下来可以看 native 了, libnative_add.so 在 Flutter 输入框回车的时候被加载。

genKey 函数是一个 rc4 用来生成静态的 Key ,通过 blutter 生成的代码可以知道这个函数被 flutter 的 getKey() 调用

getSym 函数被 flutter 的 getSym 函数调用。其伪代码如下

1
2
3
4
5
6
7
8
9
__int64 __fastcall getSym(char *a1)
{
__int64 result; // x0

result = sub_19B50("libart.so", 0LL);
if ( result )
return sub_19F78(result, a1);
return result;
}

后面卡住了hook不到….

需要较深的安卓知识,只好先放一放了

解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#define DELTA 1640531527
#define MX (((z >> 5 ^ y >> 2) + (y >> 3 ^ z >> 4)) ^ ((sum ^ y) + (key[(p & 3) ^ e] ^ z)))

void btea(uint32_t *v, int n, uint32_t const key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if (n > 1) /* Coding Part */
{
rounds = 6 + 52 * n;
sum = 0;
z = v[n - 1];
do
{
sum -= DELTA;
e = (sum >> 2) & 3;
for (p = 0; p < n - 1; p++)
{
y = v[p + 1];
z = v[p] += MX;
}
y = v[0];
z = v[n - 1] += MX;
} while (--rounds);
}
else if (n < -1) /* Decoding Part */
{
n = -n;
rounds = 6 + 52 * n;
sum = rounds * (~DELTA + 1);
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p = n - 1; p > 0; p--)
{
z = v[p - 1];
y = v[p] -= MX;
}
z = v[n - 1];
y = v[0] -= MX;
sum += DELTA;
} while (--rounds);
}
}

typedef struct
{
uint32_t eK[44], dK[44]; // encKey, decKey
int Nr; // 10 rounds
} AesKey;

#define BLOCKSIZE 16 // AES-128分组长度为16字节

// uint8_t y[4] -> uint32_t x
#define LOAD32H(x, y) \
do \
{ \
(x) = ((uint32_t)((y)[0] & 0xff) << 24) | ((uint32_t)((y)[1] & 0xff) << 16) | \
((uint32_t)((y)[2] & 0xff) << 8) | ((uint32_t)((y)[3] & 0xff)); \
} while (0)

// uint32_t x -> uint8_t y[4]
#define STORE32H(x, y) \
do \
{ \
(y)[0] = (uint8_t)(((x) >> 24) & 0xff); \
(y)[1] = (uint8_t)(((x) >> 16) & 0xff); \
(y)[2] = (uint8_t)(((x) >> 8) & 0xff); \
(y)[3] = (uint8_t)((x) & 0xff); \
} while (0)

// 从uint32_t x中提取从低位开始的第n个字节
#define BYTE(x, n) (((x) >> (8 * (n))) & 0xff)

// 密钥扩展中的SubWord(RotWord(temp),字节替换然后循环左移1位
#define MIX(x) (((S[BYTE(x, 2)] << 24) & 0xff000000) ^ ((S[BYTE(x, 1)] << 16) & 0xff0000) ^ \
((S[BYTE(x, 0)] << 8) & 0xff00) ^ (S[BYTE(x, 3)] & 0xff))

// uint32_t x循环左移n位
#define ROF32(x, n) (((x) << (n)) | ((x) >> (32 - (n))))
// uint32_t x循环右移n位
#define ROR32(x, n) (((x) >> (n)) | ((x) << (32 - (n))))

// AES-128轮常量,无符号长整型
static const uint32_t rcon[10] = {
0x01000000UL, 0x02000000UL, 0x04000000UL, 0x08000000UL, 0x10000000UL,
0x20000000UL, 0x40000000UL, 0x80000000UL, 0x1B000000UL, 0x36000000UL};
// S盒
unsigned char S[256] = {32, 123, 24, 167, 66, 68, 215, 74, 205, 50, 209, 236, 243, 129, 165, 137, 14, 145, 75, 240, 233, 93, 141, 245, 70, 252, 49, 54, 182, 172, 155, 185, 38, 9, 230, 64, 212, 176, 81, 79, 156, 62, 231, 121, 48, 136, 177, 60, 122, 92, 211, 20, 90, 171, 86, 192, 4, 41, 208, 59, 31, 249, 163, 87, 0, 138, 132, 22, 244, 26, 234, 100, 166, 214, 46, 190, 47, 23, 196, 224, 30, 2, 58, 34, 143, 159, 203, 168, 44, 103, 52, 37, 213, 255, 239, 246, 226, 170, 217, 114, 254, 206, 161, 120, 133, 150, 42, 119, 202, 193, 55, 116, 162, 94, 108, 253, 184, 77, 125, 112, 179, 221, 207, 113, 115, 97, 248, 25, 72, 227, 99, 51, 61, 21, 174, 152, 229, 128, 189, 188, 130, 198, 148, 1, 228, 222, 6, 80, 149, 223, 71, 247, 144, 139, 69, 154, 110, 7, 173, 28, 53, 131, 104, 3, 111, 91, 183, 251, 29, 197, 16, 124, 216, 106, 204, 105, 142, 36, 76, 57, 180, 160, 11, 82, 232, 169, 178, 140, 10, 191, 40, 134, 109, 175, 218, 65, 250, 117, 181, 67, 195, 96, 98, 43, 85, 242, 158, 45, 18, 35, 13, 219, 107, 199, 56, 127, 95, 151, 8, 237, 225, 187, 238, 157, 210, 146, 73, 63, 220, 88, 135, 194, 186, 153, 201, 78, 241, 33, 235, 19, 101, 89, 118, 12, 200, 5, 164, 84, 147, 27, 102, 17, 39, 83, 126, 15};

// 逆S盒
unsigned char inv_S[256] = {64, 143, 81, 163, 56, 245, 146, 157, 218, 33, 188, 182, 243, 210, 16, 255, 170, 251, 208, 239, 51, 133, 67, 77, 2, 127, 69, 249, 159, 168, 80, 60, 0, 237, 83, 209, 177, 91, 32, 252, 190, 57, 106, 203, 88, 207, 74, 76, 44, 26, 9, 131, 90, 160, 27, 110, 214, 179, 82, 59, 47, 132, 41, 227, 35, 195, 4, 199, 5, 154, 24, 150, 128, 226, 7, 18, 178, 117, 235, 39, 147, 38, 183, 253, 247, 204, 54, 63, 229, 241, 52, 165, 49, 21, 113, 216, 201, 125, 202, 130, 71, 240, 250, 89, 162, 175, 173, 212, 114, 192, 156, 164, 119, 123, 99, 124, 111, 197, 242, 107, 103, 43, 48, 1, 171, 118, 254, 215, 137, 13, 140, 161, 66, 104, 191, 230, 45, 15, 65, 153, 187, 22, 176, 84, 152, 17, 225, 248, 142, 148, 105, 217, 135, 233, 155, 30, 40, 223, 206, 85, 181, 102, 112, 62, 246, 14, 72, 3, 87, 185, 97, 53, 29, 158, 134, 193, 37, 46, 186, 120, 180, 198, 28, 166, 116, 31, 232, 221, 139, 138, 75, 189, 55, 109, 231, 200, 78, 169, 141, 213, 244, 234, 108, 86, 174, 8, 101, 122, 58, 10, 224, 50, 36, 92, 73, 6, 172, 98, 194, 211, 228, 121, 145, 149, 79, 220, 96, 129, 144, 136, 34, 42, 184, 20, 70, 238, 11, 219, 222, 94, 19, 236, 205, 12, 68, 23, 95, 151, 126, 61, 196, 167, 25, 115, 100, 93};

/* copy in[16] to state[4][4] */
int loadStateArray(uint8_t (*state)[4], const uint8_t *in)
{
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[j][i] = *in++;
}
}
return 0;
}

/* copy state[4][4] to out[16] */
int storeStateArray(uint8_t (*state)[4], uint8_t *out)
{
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
*out++ = state[j][i];
}
}
return 0;
}

// 密钥扩展,只接受16字初始密钥
int keyExpansion(const uint8_t *key, uint32_t keyLen, AesKey *aesKey)
{

if (NULL == key || NULL == aesKey)
{
printf("keyExpansion param is NULL\n");
return -1;
}

if (keyLen != 16)
{
printf("keyExpansion keyLen = %d, Not support.\n", keyLen);
return -1;
}

uint32_t *w = aesKey->eK; // 加密密钥
uint32_t *v = aesKey->dK; // 解密密钥

// 扩展密钥长度44=4*(10+1)个字,原始密钥128位,4个32位字,Nb*(Nr+1)

/* W[0-3],前4个字为原始密钥 */
for (int i = 0; i < 4; ++i)
{
LOAD32H(w[i], key + 4 * i);
}

/* W[4-43] */
// temp=w[i-1];tmp=SubWord(RotWord(temp))xor Rcon[i/4] xor w[i-Nk]
for (int i = 0; i < 10; ++i)
{
w[4] = w[0] ^ MIX(w[3]) ^ rcon[i];
w[5] = w[1] ^ w[4];
w[6] = w[2] ^ w[5];
w[7] = w[3] ^ w[6];
w += 4;
}

w = aesKey->eK + 44 - 4;
// 解密密钥矩阵为加密密钥矩阵的倒序,方便使用,把ek的11个矩阵倒序排列分配给dk作为解密密钥
// 即dk[0-3]=ek[41-44], dk[4-7]=ek[37-40]... dk[41-44]=ek[0-3]
for (int j = 0; j < 11; ++j)
{
for (int i = 0; i < 4; ++i)
{
v[i] = w[i];
}
w -= 4;
v += 4;
}

return 0;
}

// 轮密钥加
int addRoundKey(uint8_t (*state)[4], const uint32_t *key)
{
uint8_t k[4][4];

/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
k[i][j] = (uint8_t)BYTE(key[j], 3 - i); /* 把 uint32 key[4] 先转换为矩阵 uint8 k[4][4] */
state[i][j] ^= k[i][j];
}
}

return 0;
}

// 字节替换
int subBytes(uint8_t (*state)[4])
{
/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = S[state[i][j]]; // 直接使用原始字节作为S盒数据下标
}
}

return 0;
}

// 逆字节替换
int invSubBytes(uint8_t (*state)[4])
{
/* i: row, j: col */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = inv_S[state[i][j]];
}
}
return 0;
}

// 行移位
int shiftRows(uint8_t (*state)[4])
{
uint32_t block[4] = {0};

/* i: row */
for (int i = 0; i < 4; ++i)
{
// 便于行循环移位,先把一行4字节拼成uint_32结构,移位后再转成独立的4个字节uint8_t
LOAD32H(block[i], state[i]);
block[i] = ROF32(block[i], 8 * i); // block[i]循环左移8*i位,如第0行左移0位
STORE32H(block[i], state[i]);
}
return 0;
}

// 逆行移位
int invShiftRows(uint8_t (*state)[4])
{
uint32_t block[4] = {0};

/* i: row */
for (int i = 0; i < 4; ++i)
{
LOAD32H(block[i], state[i]);
block[i] = ROR32(block[i], 8 * i);
STORE32H(block[i], state[i]);
}

return 0;
}

/* Galois Field (256) Multiplication of two Bytes */
// 两字节的伽罗华域乘法运算
uint8_t GMul(uint8_t u, uint8_t v)
{
uint8_t p = 0;

for (int i = 0; i < 8; ++i)
{
if (u & 0x01)
{
p ^= v;
}

int flag = (v & 0x80);
v <<= 1;
if (flag)
{
v ^= 0x1B;
}

u >>= 1;
}

return p;
}

// 列混合
int mixColumns(uint8_t (*state)[4])
{
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x02, 0x03, 0x01, 0x01},
{0x01, 0x02, 0x03, 0x01},
{0x01, 0x01, 0x02, 0x03},
{0x03, 0x01, 0x01, 0x02}};

/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
tmp[i][j] = state[i][j];
}
}

for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{ // 伽罗华域加法和乘法
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j]) ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}

return 0;
}

// 逆列混合
int invMixColumns(uint8_t (*state)[4])
{
uint8_t tmp[4][4];
uint8_t M[4][4] = {{0x0E, 0x0B, 0x0D, 0x09},
{0x09, 0x0E, 0x0B, 0x0D},
{0x0D, 0x09, 0x0E, 0x0B},
{0x0B, 0x0D, 0x09, 0x0E}}; // 使用列混合矩阵的逆矩阵

/* copy state[4][4] to tmp[4][4] */
for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
tmp[i][j] = state[i][j];
}
}

for (int i = 0; i < 4; ++i)
{
for (int j = 0; j < 4; ++j)
{
state[i][j] = GMul(M[i][0], tmp[0][j]) ^ GMul(M[i][1], tmp[1][j]) ^ GMul(M[i][2], tmp[2][j]) ^ GMul(M[i][3], tmp[3][j]);
}
}

return 0;
}

// AES-128加密接口,输入key应为16字节长度,输入长度应该是16字节整倍数,
// 这样输出长度与输入长度相同,函数调用外部为输出数据分配内存
int aesEncrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *pt, uint8_t *ct, uint32_t len)
{
AesKey aesKey;
uint8_t *pos = ct;
const uint32_t *rk = aesKey.eK; // 加密密钥指针
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};

if (NULL == key || NULL == pt || NULL == ct)
{
printf("param err.\n");
return -1;
}

if (keyLen > 16)
{
printf("keyLen must be 16.\n");
return -1;
}

if (len % BLOCKSIZE)
{
printf("inLen is invalid.\n");
return -1;
}

memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); // 密钥扩展

// 使用ECB模式循环加密多个分组长度的数据
for (int i = 0; i < len; i += BLOCKSIZE)
{
// 把16字节的明文转换为4x4状态矩阵来进行处理
loadStateArray(state, pt);
// 轮密钥加
addRoundKey(state, rk);

for (int j = 1; j < 10; ++j)
{
rk += 4;
subBytes(state); // 字节替换
shiftRows(state); // 行移位
addRoundKey(state, rk); // 轮密钥加
mixColumns(state); // 列混合
}

subBytes(state); // 字节替换
shiftRows(state); // 行移位
// 此处不进行列混合
addRoundKey(state, rk + 4); // 轮密钥加

// 把4x4状态矩阵转换为uint8_t一维数组输出保存
storeStateArray(state, pos);

pos += BLOCKSIZE; // 加密数据内存指针移动到下一个分组
pt += BLOCKSIZE; // 明文数据指针移动到下一个分组
rk = aesKey.eK; // 恢复rk指针到秘钥初始位置
}
return 0;
}

// AES128解密, 参数要求同加密
int aesDecrypt(const uint8_t *key, uint32_t keyLen, const uint8_t *ct, uint8_t *pt, uint32_t len)
{
AesKey aesKey;
uint8_t *pos = pt;
const uint32_t *rk = aesKey.dK; // 解密密钥指针
uint8_t out[BLOCKSIZE] = {0};
uint8_t actualKey[16] = {0};
uint8_t state[4][4] = {0};

if (NULL == key || NULL == ct || NULL == pt)
{
printf("param err.\n");
return -1;
}

if (keyLen > 16)
{
printf("keyLen must be 16.\n");
return -1;
}

if (len % BLOCKSIZE)
{
printf("inLen is invalid.\n");
return -1;
}

memcpy(actualKey, key, keyLen);
keyExpansion(actualKey, 16, &aesKey); // 密钥扩展,同加密

for (int i = 0; i < len; i += BLOCKSIZE)
{
// 把16字节的密文转换为4x4状态矩阵来进行处理
loadStateArray(state, ct);
// 轮密钥加,同加密
addRoundKey(state, rk);

for (int j = 1; j < 10; ++j)
{
rk += 4;
invShiftRows(state); // 逆行移位
invSubBytes(state); // 逆字节替换,这两步顺序可以颠倒
invMixColumns(state); // 逆列混合
addRoundKey(state, rk); // 轮密钥加,同加密
}

invShiftRows(state); // 逆行移位
invSubBytes(state); // 逆字节替换
// 此处没有逆列混合
addRoundKey(state, rk + 4); // 轮密钥加,同加密

storeStateArray(state, pos); // 保存明文数据
pos += BLOCKSIZE; // 输出数据内存指针移位分组长度
ct += BLOCKSIZE; // 输入数据内存指针移位分组长度
rk = aesKey.dK; // 恢复rk指针到秘钥初始位置
}
return 0;
}

int main()
{

unsigned char enc[] = {0xf2, 0xc0, 0x05, 0x5f, 0x8e, 0x73, 0x4f, 0xbb, 0x9c, 0xd2, 0xf4, 0x94, 0xc8, 0x53, 0x4d, 0x97, 0x2d, 0x61, 0xfd, 0xde, 0x73, 0x4e, 0xdf, 0x3d, 0xb5, 0x5d, 0xe4, 0x71, 0x5a, 0xf9, 0x3f, 0x99, 0xfb, 0x95, 0x12, 0x8f, 0xc9, 0x8b, 0x71, 0x83, 0x7d, 0x73, 0x60, 0xd0, 0x76, 0x94, 0xf5, 0x74, 0x73, 0x99, 0xdf, 0xff, 0x1d, 0xbf, 0x20, 0xaf, 0x78, 0x5d, 0x5f, 0x54, 0x7e, 0xcf, 0x57, 0x7d, 0xa0, 0xc6, 0xe4, 0x85, 0xf1, 0x32, 0x56, 0x72, 0xf9, 0x9d, 0xa7, 0x13, 0xe6, 0x59, 0x44, 0x50, 0x52, 0xa4, 0xca, 0x0a, 0xcd, 0x05, 0xdf, 0x52, 0xad, 0x93, 0x4a, 0x32, 0x49, 0x7d, 0x0c, 0x55, 0x0};
uint32_t v[] = {1650877538, 875968049, 842479969, 1714696806, 862139446, 1667380838, 912679474, 1631019828};
uint32_t k[4] = {842610225, 57, 0, 0};
int n = 24;
n = 24;
btea((uint32_t *)enc, -n, k);
printf("xxtea -> %s\n", enc);
unsigned char encrypted[] = {0xb0, 0xca, 0x9f, 0xa0, 0xa3, 0xee, 0x7e, 0x58, 0x1c, 0x46, 0xc8, 0xca, 0x69, 0x2f, 0xc9, 0xb1, 0x2a, 0xd4, 0x33, 0xf8, 0xa3, 0x23, 0x2c, 0xb6, 0xd7, 0x3b, 0x54, 0xcf, 0x82, 0xe8, 0x0c, 0x71, 0x53, 0xba, 0x9b, 0xb1, 0xb2, 0xda, 0x3e, 0x43, 0xbe, 0x66, 0x7d, 0x65, 0xdc, 0x10, 0xe5, 0xd5, 0x0};
unsigned char key[] = {0x7a, 0x25, 0xc5, 0x24, 0xc6, 0x33, 0x4c, 0x30, 0xf3, 0x62, 0xaf, 0xac, 0x3f, 0x23, 0x03, 0xd5};
unsigned char decrypted[16] = {0};
aesDecrypt(key, 16, encrypted, decrypted, 48);
printf("aes ecb -> %s", decrypted);
return 0;
}
1
WMCTF{I_R4@11y_w@n7_70 _84c0m4_@_m@gic@1_Gir1}

Videoplayer [复现]

WMCTF2025 VideoPlayer 逆向出题笔记 | Liv’s blog

有壳VMP

image-20250924225205009

首先我们需要VMProtect。VMProtect是一款虚拟机保护软件,是目前最为流行的保护壳之一。VMProtect将保护后的代码放到虚拟机中运行,这将使分析反编译后的代码和破解变得极为困难。除了代码保护,VMProtect还可以生成和验证序列号,设置过期时间,限制免费更新等等。这样即使攻击者用反汇编工具看到指令,也只是虚拟机的字节码,而不是真实的 x86/AMD64 指令,大大增加了逆向难度。虚拟机保护通常会和大量反调试 / 反分析 / 完整性检查一起使用,以进一步提升逆向难度。

脱壳需要寻找oep,程序入口点

VMProtect + 其他反调试 会在用户态用很多 API/机制检测调试器(如 IsDebuggerPresent、异常行为、时间差、调试寄存器、调试句柄等),这些检测很难单靠用户态插件完全隐蔽。

用了sharpod和Scyllahide均无法过

出题人给了两种解法

法1:TitanHide过反调试

这边反调试使用TitanHide驱动,Github可以搜索到相关项目,运行TitanHide.sys需要配置环境。

mrexodia/TitanHide: Hiding kernel-driver for x86/x64.

微信公众平台

1
bcdedit /set testsigning on

image-20250925081824950

如果无法设置需要进入bios去关闭相应的安全模式,电脑不同方法不同

命令成功后需要重启,左/右下交出现test成功了

关闭就是

1
bcdedit /set testsigning off

使用VKD工具的target64中的vminstall在虚拟机中运行安装,会多出来一个引导启动,重启电脑选择新的引导启动就可以,他会进入内核调试模式,禁止驱动强制签名以及关闭PG,也就可以让我们加载titanhide驱动。

运行titanhide.sys,将titanhide的dbg相关插件文件放入dbg的plugins文件夹中,运行dbg即可调试VMP程序。

法2:CheatEngine Veh debugger

软件运行后,使用CE附加,选用VEH Debugger即可进行断点调试。

image-20250925084449156

image-20251009083541751

这能调出来我觉得也是神人了T.T

还是得脱壳,调试可以用CE

VMP脱壳

TitanHide环境配置

先搞个win10 64虚拟机,开启测试模式后

把TitanHide.sys放在(root)/system32/drive/下

注意使用管理员权限

1
2
3
4
bcdedit /set testsigning on
sc.exe create TitanHide binPath= "C:\Windows\System32\drivers\TitanHide.sys" type= kernel
sc.exe start TitanHide
sc.exe query TitanHide

然后把GUI打开

image-20251009133625595

更详细步骤可以看:https://www.ctfiot.com/272572.html

image-20251009133803723

下断点

image-20251009135850764

还是卡住了,看了下wp忽略了一条消息

使用VKD工具的target64中的vminstall在虚拟机中运行安装,会多出来一个引导启动,重启电脑选择新的引导启动就可以,他会进入内核调试模式,禁止驱动强制签名以及关闭PG,也就可以让我们加载titanhide驱动。

VKDVirtualKD(一个用来加速/简化在虚拟机上做 Windows 内核调试的工具链);

target64 是 VirtualKD 发布包里针对 64-bit 客机的那一组文件夹(包含 vminstall.exe、驱动和调试传输 dll 等);

vminstall.exe 是放在 target64 里、在虚拟机内运行的安装/引导助手 —— 它会在客机上安装需要的组件并添加一个新的启动项(boot entry)以便用内核调试模式启动虚拟机,从而能更方便地进行 KD(kernel debugging)。这些信息都可以在 VirtualKD 的官方文档/教程里找到。

注意使用TitanHide需要删掉其它用户态的反调试插件如ScyllaHide等,多亏了Liv师傅,不然得搞半天

image-20251019221257149

可以先转到然后搜索GetSystemTimeAsFileTime

image-20251019221352976

image-20251019221405866

下断点

运行到EntryPoint后

第二次运行到断点后

image-20251019222500328

下硬件断点

image-20251019222618735

Scylla导出dump

image-20251019222645655

分析

image-20251019232252247

题目说存在后门账户,猜测会通过strcmp判断用户名或密码文本(这边通过断点用户名或密码的内存进行后续分析也可以)

随便输入用户名和密码后,断点strcmp,点击Login按钮断下,发现会判断一次用户名是否为WMAdmin_#6&JZZ%B,证实确实存在后门用户名判断,但是目前还是登入失败。

做到这目前是我的极限了……