hackergame-2024复现


签到

题目描述:

1
2
3
让我们说……各种语言,开始今年的冒险!

提示:完成题目遇到困难?你可以参考 2018 年签到题题解、2019 年签到题题解、2020 年签到题题解、2021 年签到题题解、2022 年签到题题解 和 2023 年签到题题解。

开启环境

需要在60s之内输入12 种不同语言的启动,拼手速显然不可能

点击启动后,发现地址栏的URL发生了变化,并且有 pass=false 参数,表示选手没有通过本题。

我们只需将false改为true,即可得到flag

还有一种方法:

直接在控制台中输入这段代码,将正确答案直接填入:

1
2
3
document.querySelectorAll('.input-box').forEach(input => {
input.value = answers[input.id]; // 自动填入所有正确答案
});

最后flag为

1
flag{wE!C0mE-tO-hAcK3R9@m3-aNd-Enj0Y-haCKin9-2oZ4}

喜欢做签到的 CTFer 你们好呀

题目描述:

1
喜欢做签到的 CTFer 你们好呀,我是一道更典型的 checkin:有两个 flag 就藏在中国科学技术大学校内 CTF 战队的招新主页里!

进入主页可以发现网站模拟了一个 Linux 终端

查看环境变量

1
env

最后flag为

1
flag{actually_theres_another_flag_here_trY_to_f1nD_1t_y0urself___join_us_ustc_nebula}

还有一个flag

1
2
ls -a列目录找到flag
cat 查看flag

最后flag为

1
flag{0k_175_a_h1dd3n_s3c3rt_f14g___please_join_us_ustc_nebula_anD_two_maJor_requirements_aRe_shown_somewhere_else}

猫咪问答(Hackergame 十周年纪念版)

题目描述:

1
2
3
4
5
6
7
8
9
多年回答猫咪问答的猫咪大多目光锐利,极度自信,且智力逐年增加,最后完全变成猫咪问答高手。回答猫咪问答会优化身体结构,突破各种猫咪极限。猫咪一旦开始回答猫咪问答,就说明这只猫咪的智慧品行样貌通通都是上等,这辈子注定在猫咪界大有作为。

提示:解出谜题不需要是科大在校猫咪。解题遇到困难?你可以参考以下题解:

2018 年猫咪问答题解
2020 年猫咪问答++ 题解
2021 年猫咪问答 Pro Max 题解
2022 年猫咪问答喵题解
2023 年猫咪小测题解

开启环境

问题一:在 Hackergame 2015 比赛开始前一天晚上开展的赛前讲座是在哪个教室举行的?

答案是

1
3A204

问题二:众所周知,Hackergame 共约 25 道题目。近五年(不含今年)举办的 Hackergame 中,题目数量最接近这个数字的那一届比赛里有多少人注册参加?

答案是

1
2682

问题三: Hackergame 2018 让哪个热门检索词成为了科大图书馆当月热搜第一?

答案是

1
程序员的自我修养

问题四:在今年的 USENIX Security 学术会议上中国科学技术大学发表了一篇关于电子邮件伪造攻击的论文,在论文中作者提出了 6 种攻击方法,并在多少个电子邮件服务提供商及客户端的组合上进行了实验?

答案是

1
336

问题五:10 月 18 日 Greg Kroah-Hartman 向 Linux 邮件列表提交的一个 patch 把大量开发者从 MAINTAINERS 文件中移除。这个 patch 被合并进 Linux mainline 的 commit id 是多少?

答案是

1
6e90b6

问题六:大语言模型会把输入分解为一个一个的 token 后继续计算,请问这个网页的 HTML 源代码会被 Meta 的 Llama 3 70B 模型的 tokenizer 分解为多少个 token?

因为多一个换行符\n,所以最后答案是

1
1833

提交所有答案

得到两个flag

1
2
flag{a_9O0d_cA7_!S_The_C47_ωh0_c4n_pΛ$$_tHe_quIz}
flag{t3n_¥eΛЯ$_Oƒ_hAcKergaME_OMEd370บ_W1TH_nEkØ_Qu!z}

打不开的盒

题目描述:

1
2
3
如果一块砖头里塞进了一张写了 flag 的纸条,应该怎么办呢?相信这不是一件困难的事情。

现在,你遇到了同样的情况:这里有一个密封盒子的设计文件,透过镂空的表面你看到里面有些东西……

那只要把它 3D 打印出来之后砸开不就解决了?用网上的制造服务的话,可能还没收到东西比赛就结束了,所以难道真的要去买一台 3D 打印机才能够看到里面的东西吗?

下载到一个 STL 模型,使用Autodesk Tinkercad打开,将模型转为空心看到flag

最后flag为

1
flag{Dr4W_Us!nG_fR3E_C4D!!w0W}

每日论文太多了!

题目描述:

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
传闻,每日新发表的论文

有七成都会在一年内被遗忘

而且五年后

基本都无人问津

它们被学术界的快节奏淹没

有人引用

有人忽视

我不期盼这学术世界,以及我的研究

能在这汪洋般的文献中脱颖而出

然而,我有时会思考

如果我的论文能被更多人阅读

如果我的研究能对他人有所启发

如果我能为这个领域贡献一点价值

届时

我将作何感想

——改编自「負けヒロインが多すぎる!」Ep 1,存在 AI 创作
要怎么做才能读读 我们的论文?只要是我能做的,我什么都愿意做!

补充说明 1:该题仅需论文文件本身即可解答。请各位比赛选手确保基本的礼仪,请勿向论文作者发送邮件打扰。

下载论文并打开,搜索flag,编辑pdf图像删去框框,看到flag

最后flag为

1
flag{h4PpY_hAck1ng_3veRyd4y}

比大小王

题目描述:

1
2
3
4
5
6
7
「小孩哥,你干了什么?竟然能一边原崩绝鸣舟,一边农瓦 CSGO。你不去做作业,我等如何排位上分?」

小孩哥不禁莞尔,淡然道:「很简单,做完口算题,拿下比大小王,家长不就让我玩游戏了?」

说罢,小孩哥的气息终于不再掩饰,一百道题,十秒速通。

在这场巅峰对决中,你能否逆风翻盘狙击小孩哥,捍卫我方尊严,成为新一代的「比大小王」?!

开启环境

查看网页源码

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
let state = {
allowInput: false,
score1: 0,
score2: 0,
values: null,
startTime: null,
value1: null,
value2: null,
inputs: [],
stopUpdate: false,
};

function loadGame() {
fetch('/game', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
.then(response => response.json())
.then(data => {
state.values = data.values;
state.startTime = data.startTime * 1000;
state.value1 = data.values[0][0];
state.value2 = data.values[0][1];
document.getElementById('value1').textContent = state.value1;
document.getElementById('value2').textContent = state.value2;
updateCountdown();
})
.catch(error => {
document.getElementById('dialog').textContent = '加载失败,请刷新页面重试';
});
}

function updateCountdown() {
if (state.stopUpdate) {
return;
}
const seconds = Math.ceil((state.startTime - Date.now()) / 1000);
if (seconds >= 4) {
requestAnimationFrame(updateCountdown);
}
if (seconds <= 3 && seconds >= 1) {
document.getElementById('dialog').textContent = seconds;
requestAnimationFrame(updateCountdown);
} else if (seconds <= 0) {
document.getElementById('dialog').style.display = 'none';
state.allowInput = true;
updateTimer();
}
}

function updateTimer() {
if (state.stopUpdate) {
return;
}
const time1 = Date.now() - state.startTime;
const time2 = Math.min(10000, time1);
state.score2 = Math.max(0, Math.floor(time2 / 100));
document.getElementById('time1').textContent = `${String(Math.floor(time1 / 60000)).padStart(2, '0')}:${String(Math.floor(time1 / 1000) % 60).padStart(2, '0')}.${String(time1 % 1000).padStart(3, '0')}`;
document.getElementById('time2').textContent = `${String(Math.floor(time2 / 60000)).padStart(2, '0')}:${String(Math.floor(time2 / 1000) % 60).padStart(2, '0')}.${String(time2 % 1000).padStart(3, '0')}`;
document.getElementById('score2').textContent = state.score2;
document.getElementById('progress2').style.width = `${state.score2}%`;
if (state.score2 === 100) {
state.allowInput = false;
state.stopUpdate = true;
document.getElementById('dialog').textContent = '对手已完成,挑战失败!';
document.getElementById('dialog').style.display = 'flex';
document.getElementById('time1').textContent = `00:10.000`;
} else {
requestAnimationFrame(updateTimer);
}
}

function chooseAnswer(choice) {
if (!state.allowInput) {
return;
}
state.inputs.push(choice);
let correct;
if (state.value1 < state.value2 && choice === '<' || state.value1 > state.value2 && choice === '>') {
correct = true;
state.score1++;
document.getElementById('answer').style.backgroundColor = '#5e5';
} else {
correct = false;
document.getElementById('answer').style.backgroundColor = '#e55';
}
document.getElementById('answer').textContent = choice;
document.getElementById('score1').textContent = state.score1;
document.getElementById('progress1').style.width = `${state.score1}%`;
state.allowInput = false;
setTimeout(() => {
if (state.score1 === 100) {
submit(state.inputs);
} else if (correct) {
state.value1 = state.values[state.score1][0];
state.value2 = state.values[state.score1][1];
state.allowInput = true;
document.getElementById('value1').textContent = state.value1;
document.getElementById('value2').textContent = state.value2;
document.getElementById('answer').textContent = '?';
document.getElementById('answer').style.backgroundColor = '#fff';
} else {
state.allowInput = false;
state.stopUpdate = true;
document.getElementById('dialog').textContent = '你选错了,挑战失败!';
document.getElementById('dialog').style.display = 'flex';
}
}, 200);
}

function submit(inputs) {
fetch('/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({inputs}),
})
.then(response => response.json())
.then(data => {
state.stopUpdate = true;
document.getElementById('dialog').textContent = data.message;
document.getElementById('dialog').style.display = 'flex';
})
.catch(error => {
state.stopUpdate = true;
document.getElementById('dialog').textContent = '提交失败,请刷新页面重试';
document.getElementById('dialog').style.display = 'flex';
});
}

document.addEventListener('keydown', e => {
if (e.key === 'ArrowLeft' || e.key === 'a') {
document.getElementById('less-than').classList.add('active');
setTimeout(() => document.getElementById('less-than').classList.remove('active'), 200);
chooseAnswer('<');
} else if (e.key === 'ArrowRight' || e.key === 'd') {
document.getElementById('greater-than').classList.add('active');
setTimeout(() => document.getElementById('greater-than').classList.remove('active'), 200);
chooseAnswer('>');
}
});
document.getElementById('less-than').addEventListener('click', () => chooseAnswer('<'));
document.getElementById('greater-than').addEventListener('click', () => chooseAnswer('>'));
document.addEventListener('load', loadGame());

100 道题目的数字在 state.values变量中,最终需要调用 submit 函数提交答案,所以可以等待比赛开始后,在控制台执行以下代码:

1
submit(state.values.map(([v1,v2])=>v1<v2?'<':'>'))

最后flag为

1
flag{i-am-THE-HAcKER-K1Ng-OF-coMp@rINg-NUM6eRs-zoz4}

旅行照片 4.0

题目描述:

1
2
3
4
5
6
7
8
9
「又要重复吗,绝望的轮回」

你的学长今年还在旅游…… 对,对吗?你似乎注意到了什么。

往年题目:旅行照片、旅行照片 2.0、旅行照片 3.0。

注意:你不需要阅读往年题目亦能答题,上述链接仅供参考。

请观察照片并结合所有文字内容,正确回答题目以获取 flag。

开启环境

…LEO 酱?……什么时候

「说起来最近学长的 ** 空间里怎么没有旅游的照片了……」

正当你在这样想的时候,突然刷到学长的一条吐槽:

1
你们的生活到底真的假的呀?每天要么就是看漫展看偶像看 live 喝酒吃烧烤,要么就是这里那里旅游。阵容一宣,说冲就冲,群一拉,机票一买,钱就像大风刮来的,时间好像一直有。c**4 你们也去,mu**ca 你们也去,m**o 你们也去,to*ea*i 你们也去。我怎么一天到晚都在上班啊,你们那到底是怎么弄的呀?教教我行不行

出去玩的最多的难道不就是您自己吗?

看样子学长是受到了什么刺激…… 会是什么呢?话说照片里这是…… Leo 酱?……什么时候

题目 1-2

问题 1: 照片拍摄的位置距离中科大的哪个校门更近?(格式:X校区Y门,均为一个汉字)

高德地图搜索科里科气科创驿站

答案是

1
东校区西门

问题 2: 话说 Leo 酱上次出现在桁架上是……科大今年的 ACG 音乐会?活动日期我没记错的话是?(格式:YYYYMMDD)

B 站搜索「中科大 ACG 音乐会」,找到账户中科大LEO动漫协会的个人空间-中科大LEO动漫协会个人主页-哔哩哔哩视频

顺着动态找到这个https://www.bilibili.com/opus/930934582351495204

答案是

1
20240519

提交答案,得到flag

最后flag为

1
flag{5UB5CR1B3_T0_L30_CH4N_0N_B1L1B1L1_PLZ_cf212f83d4}

诶?我带 LEO 酱出去玩?真的假的?

「拍照的时候带着 LEO 酱看起来是个不错的选择」,回忆完上次的 ACG 音乐会,你这样想到,不过说到底要去哪里呢?

这样想着,你打开自己的相册翻找,「我记得之前保存了几个还不错的地方……」

嗯?奇怪的记忆增加了。诶,我到过这些地方吗?而且这两张照片拍摄地的距离……是不是有点远?

题目 3-4

问题 3: 这个公园的名称是什么?(不需要填写公园所在市区等信息)

放大图片看到垃圾桶上有六安园林四个字

缩小范围到安徽六安,利用搜索引擎搜索关键词六安、公园

答案是

1
中央森林公园

问题 4: 这个景观所在的景点的名字是?(三个汉字)

百度识图

顺着链接点进去,找到

答案是

1
坛子岭

提交答案得到flag

最后flag为

1
flag{D3T41LS_M4TT3R_1F_R3V3RS3_S34RCH_1S_1MP0SS1BL3_e1cae4bc6e}

尤其是你才是最该多练习的人

调查自己还是头一回,多新鲜啊。不过,还没来得及理清头绪,你突然收到了来自学长的信息:

1
来练练手,看看能挖出什么有趣的东西。

糟了,三番五次调查学长被他发现了?不过,这个照片确实有趣,似乎有辆很标志性的……四编组动车?

题目 5-6

问题 5: 距离拍摄地最近的医院是?(无需包含院区、地名信息,格式:XXX医院)

搜索四编组动车找到这个

CRH6F-A型动车组_百度百科

对照图中动车车身颜色,也确实很像找到的那个怀密号

顺着思路,搜索怀密线

北京市郊铁路怀柔—密云线_百度百科

果然在昌平北位置找到医院

碰巧也在昌平线附近找到动车所

答案是

1
积水潭医院

问题 6: 左下角的动车组型号是?

1
CRH6F-A

提交答案得到flag

最后flag为

1
flag{1_C4NT_C0NT1NU3_TH3_5T0RY_4NYM0R3_50M30N3_PLZ_H3LP_196b482f11}

不宽的宽字符

A 同学决定让他设计的 Windows 程序更加「国际化」一些,首先要做的就是读写各种语言写下的文件名。于是他放弃 C 语言中的 char,转而使用宽字符 wchar_t,显然这是一个国际化的好主意。

经过一番思考,他写出了下面这样的代码,用来读入文件名:

1
2
3
// Read the filename
std::wstring filename;
std::getline(std::wcin, filename);

转换后要怎么打开文件呢?小 A 使用了 C++ 最常见的写法:

1
2
// Create the file object and open the file specified
std::wifstream f(filename);

可惜的是,某些版本的 C++ 编译器以及其自带的头文件中,文件名是 char 类型的,因此这并不正确。这时候小 A 灵光一闪,欸🤓👆,我为什么不做一个转换呢?于是:

1
std::wifstream f((char*)filename);

随便找了一个文件名测试过无误后,小 A 对自己的方案非常自信,大胆的在各个地方复用这段代码。然而,代价是什么呢?


现在你拿到了小 A 程序的一部分,小 A 通过在文件名后面加上一些内容,让你不能读取藏有 flag 的文件。

你需要的就是使用某种输入,读取到文件 theflag 的内容(完整位置是:Z:\theflag)。

注:为了使得它能在一些系统上正确地运行,我们使用 Docker 作了一些封装,并且使用 WinAPI 来保证行为一致,不过这并不是题目的重点。

下载附件,源码

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
#include <iostream>
#include <fstream>
#include <cctype>
#include <string>
#include <windows.h>

int main()
{
std::wcout << L"Enter filename. I'll append 'you_cant_get_the_flag' to it:" << std::endl;

// Get the console input and output handles
HANDLE hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);
HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);

if (hConsoleInput == INVALID_HANDLE_VALUE || hConsoleOutput == INVALID_HANDLE_VALUE)
{
// Handle error – we can't get input/output handles.
return 1;
}

DWORD mode;
GetConsoleMode(hConsoleInput, &mode);
SetConsoleMode(hConsoleInput, mode | ENABLE_PROCESSED_INPUT);

// Buffer to store the wide character input
char inputBuffer[256] = { 0 };
DWORD charsRead = 0;

// Read the console input (wide characters)
if (!ReadFile(hConsoleInput, inputBuffer, sizeof(inputBuffer), &charsRead, nullptr))
{
// Handle read error
return 2;
}

// Remove the newline character at the end of the input
if (charsRead > 0 && inputBuffer[charsRead - 1] == L'\n')
{
inputBuffer[charsRead - 1] = L'\0'; // Null-terminate the string
charsRead--;
}

// Convert to WIDE chars
wchar_t buf[256] = { 0 };
MultiByteToWideChar(CP_UTF8, 0, inputBuffer, -1, buf, sizeof(buf) / sizeof(wchar_t));

std::wstring filename = buf;

// Haha!
filename += L"you_cant_get_the_flag";

std::wifstream file;
file.open((char*)filename.c_str());

if (file.is_open() == false)
{
std::wcout << L"Failed to open the file!" << std::endl;
return 3;
}

std::wstring flag;
std::getline(file, flag);

std::wcout << L"The flag is: " << flag << L". Congratulations!" << std::endl;

return 0;
}

题目中给出Z:\theflag,字符串转ascll

再由小端存储的规则,将转换结果变成(将每两位调换位置,为了使每个2 byte都遵循小端存储)

1
0x3a 0x5a 0x74 0x5c 0x65 0x68 0x6c 0x66 0x67 0x61

将每两位作为一组

1
\u3a5a \u745c \u6568 \u6c66 \u6761

unicode编码

因为filename后面被加上了一串ascii字符串,所以我们要截断后面的字符串,如果不把后面截断,就会在filename里多出现一个”f”字符

要在后面加一个\uxx00(xx是两个十六进制数,而且要求选的xx要使得\uxx00是可见字符)

1
\u3a5a \u745c \u6568 \u6c66 \u6761 \u9a00

得到

1
㩚瑜敨汦条騀

输入到命令行界面得到flag

最后flag为

1
flag{wider_char_isnt_so_great_0ca781a643}

PowerfulShell

题目描述:

1
即使贝壳早已破碎,也请你成为 PowerfulShell 之王。

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

FORBIDDEN_CHARS="'\";,.%^*?!@#%^&()><\/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0"

PowerfulShell() {
while true; do
echo -n 'PowerfulShell@hackergame> '
if ! read input; then
echo "EOF detected, exiting..."
break
fi
if [[ $input =~ [$FORBIDDEN_CHARS] ]]; then
echo "Not Powerful Enough :)"
exit
else
eval $input
fi
done
}

PowerfulShell

flag 位于根目录下

过滤了大部分字符,但是美元符号 $、下划线 _、加号减号 +-、等号 =、左右大括号和中括号 {}[]、反引号 、波浪符 还能用

看到这种方法

1
${parameter:offset:length}

构造payload:

1
2
__=~
${__: -1}${-: -2:-1}

得到flag

最后flag为

1
flag{N0w_I_Adm1t_ur_tru1y_5He11_m4ster_7f4eccb444}

强大的正则表达式

题目描述:

1
2
3
4
5
6
7
从小 Q 开始写代码以来,他在无数的项目、帖子中看到各种神秘的字符串,听人推荐过,这就是传说中万能的正则表达式。本着能摆烂就绝不努力的原则,小 Q 从来没想过了解这门高雅艺术,遇到不懂的正则表达式就通通丢给 LLM 嘛,他这样想到。不过夜深人静的时候,小 Q 也时常在纠结写这么多 switch-case 到底是为了什么。

终于在一个不眠夜,小 Q 一口气看完了正则表达式的教程。哈?原来这么简单?小 Q 并两分钟写完了自测题目,看着教程剩下的目录,「分组」、「贪婪」、「前瞻」,正则表达式也不过如此嘛,他心想,也就做一些邮箱匹配之类的简单任务罢了。

正当他还沉浸在「不过如此」的幻想中,他刷到了那个关于正则表达式的古老而又神秘的传说:

「正则表达式可以用来计算取模和 CRC 校验……」

查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import re
import random

# pip install libscrc
import libscrc

allowed_chars = "0123456789()|*"
max_len = 1000000
num_tests = 300

difficulty = int(input("Enter difficulty level (1~3): "))
if difficulty not in [1, 2, 3]:
raise ValueError("Invalid difficulty level")

regex_string = input("Enter your regex: ").strip()

if len(regex_string) > max_len:
raise ValueError("Regex string too long")

if not all(c in allowed_chars for c in regex_string):
raise ValueError("Invalid character in regex string")

regex = re.compile(regex_string)

for i in range(num_tests):
expected_result = (i % 2 == 0)
while True:
t = random.randint(0, 2**64) # random number for testing
if difficulty == 1:
test_string = str(t) # decimal
if (t % 16 == 0) == expected_result: # mod 16
break
elif difficulty == 2:
test_string = bin(t)[2:] # binary
if (t % 13 == 0) == expected_result: # mod 13
break
elif difficulty == 3:
test_string = str(t) # decimal
if (libscrc.gsm3(test_string.encode()) == 0) == expected_result: # crc
break
else:
raise ValueError("Invalid difficulty level")
regex_result = bool(regex.fullmatch(test_string))
if regex_result == expected_result:
print("Pass", test_string, regex_result, expected_result)
else:
print("Fail", test_string, regex_result, expected_result)
raise RuntimeError("Failed")

print(open(f"flag{difficulty}").read())

第一问,由于判断一个十进制数是否整除 16 只需要看最后四位,所以枚举所有符合条件的最后四位,构造对应的正则表达式就可以了。严格来说还要考虑不足四位的情况,但是测试数据里面出现这种情况的概率很小,忽略掉也是通过的。

exp:

1
print('(0|1|2|3|4|5|6|7|8|9)*' + '(' + '|'.join(f'{i:04d}' for i in range(0, 10000, 16)) + ')')

运行得到

输入得到flag

最后flag为

1
flag{p0werful_r3gular_expressi0n_easy_67a785480c}

第二问,可以构造一个有限状态自动机(DFA)来判断一个二进制数是否整除 13。构造方法:DFA 的状态代表余数(有 0~12 一共 13 个状态),初始状态是 0,每次读入一个 bit 更新余数(状态转移)(s:=(s*2+b)%13),读入完毕后如果 DFA 处于 0 状态(余数为 0),就意味着这个二进制数整除 13。然后可以使用 状态消除算法,将 DFA 转化为正则表达式。

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
# pip install greenery
# pip install regex
from greenery import Fsm, Charclass, rxelems
import regex as re
import random

m = 13
d = 2

digits = [Charclass(str(i)) for i in range(d)]
other = ~Charclass("".join(str(i) for i in range(d)))
alphabet = set(digits + [other])
states = set(range(m + 1)) # m is the dead state
initial_state = 0
accepting_states = {0}
transition_map = dict()
for s in range(m):
transition_map[s] = {digits[i]: (s * d + i) % m for i in range(d)}
transition_map[s][other] = m
transition_map[m] = {digits[i]: m for i in range(d)}
transition_map[m][other] = m

dfa = Fsm(
alphabet=alphabet,
states=states,
initial=initial_state,
finals=accepting_states,
map=transition_map,
)

def convert_regex(regex):
# `(...)?` -> `((...)|)`
while '?' in regex:
regex = re.sub(r'\((.*?)\)\?', r'(\1|)', regex)
# Handle `{n}` quantifier
n = 1
while '{' in regex:
while '{' + str(n) + '}' in regex:
regex = re.sub(r'(\((.*?)\)|\w)\{n\}'.replace('n', str(n)), r'\1' * n, regex)
n += 1
# [abc] -> (a|b|c)
while '[' in regex:
def convert_charset(match):
chars = match.group(1)
return '(' + '|'.join(chars) + ')'
regex = re.sub(r'\[([^\]]+)\]', convert_charset, regex)
assert set(regex) <= set("0123456789|()*")
return regex

dfa = dfa.reduce()
regex = rxelems.from_fsm(dfa)
regex = regex.reduce()
regex = convert_regex(str(regex))
print(regex)

第三问,同样是构造 DFA 然后转换成正则表达式。这次 DFA 的状态是线性反馈移位寄存器(LFSR)的状态,寄存器有 3 位,一共是 8 种状态(000~111),DFA 初始状态是 111,每次读入一个字符更新状态,读入完毕后如果 DFA 处于 000 状态,就意味着这个字符串符合要求。

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
# pip install greenery
# pip install regex
# pip install libscrc
from greenery import Fsm, Charclass, rxelems
import regex as re
import libscrc

digits = [Charclass(str(i)) for i in range(10)]
other = ~Charclass(''.join(str(i) for i in range(10)))
alphabet = set(digits + [other])
states = set(range(9)) # 8 is the dead state
initial_state = libscrc.gsm3(b'') # 7 (111)
accepting_states = {0}
transition_map = {s:dict() for s in states}
for s in states:
transition_map[s][other] = 8
for prefix in range(1000):
for i in range(10):
s_1 = libscrc.gsm3(str(prefix).encode())
s_2 = libscrc.gsm3(str(prefix).encode() + str(i).encode())
transition_map[s_1][digits[i]] = s_2
for i in range(10):
transition_map[8][digits[i]] = 8

dfa = Fsm(
alphabet=alphabet,
states=states,
initial=initial_state,
finals=accepting_states,
map=transition_map,
)

def convert_regex(regex):
# `(...)?` -> `((...)|)`
while '?' in regex:
regex = re.sub(r'\((.*?)\)\?', r'(\1|)', regex)
# Handle `{n}` quantifier
n = 1
while '{' in regex:
while '{' + str(n) + '}' in regex:
regex = re.sub(r'(\((.*?)\)|\w)\{n\}'.replace('n', str(n)), r'\1' * n, regex)
n += 1
# [abc] -> (a|b|c)
while '[' in regex:
def convert_charset(match):
chars = match.group(1)
return '(' + '|'.join(chars) + ')'
regex = re.sub(r'\[([^\]]+)\]', convert_charset, regex)
assert set(regex) <= set("0123456789|()*")
return regex

dfa = dfa.reduce()
regex = rxelems.from_fsm(dfa)
regex = regex.reduce()
regex = convert_regex(str(regex))
print(regex)

惜字如金 3.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
惜字如金一向是程序开发的优良传统。无论是「creat」还是「referer」,都无不闪耀着程序员「节约每句话中的每一个字母」的优秀品质。两年前,信息安全大赛组委会正式推出了「惜字如金化」(XZRJification)标准规范,受到了广大程序开发人员的热烈欢迎和一致好评。现将该标准重新辑录如下。

惜字如金化标准
惜字如金化指的是将一串文本中的部分字符删除,从而形成另一串文本的过程。该标准针对的是文本中所有由 52 个拉丁字母连续排布形成的序列,在下文中统称为「单词」。一个单词中除「AEIOUaeiou」外的 42 个字母被称作「辅音字母」。整个惜字如金化的过程按照以下两条原则对文本中的每个单词进行操作:

第一原则(又称 creat 原则):如单词最后一个字母为「e」或「E」,且该字母的上一个字母为辅音字母,则该字母予以删除。
第二原则(又称 referer 原则):如单词中存在一串全部由完全相同(忽略大小写)的辅音字母组成的子串,则该子串仅保留第一个字母。
容易证明惜字如金化操作是幂等的:多次惜字如金化和一次惜字如金化的结果是相同的。

你的任务
为了拿到对应的三个 flag,你需要将三个「惜字如金化」后的 Python 源代码文本文件补全。所有文本文件在「惜字如金化」前均使用空格将每行填充到了 80 个字符。后台会对上传的文本文件逐行匹配,如果每行均和「惜字如金化」前的文本文件完全相符,则输出对应 flag。上传文件无论使用 LF 还是 CRLF 换行,无论是否在尾部增加了单独的换行符,均对匹配结果没有影响。

附注
本文已经过惜字如金化处理。解答本题(拿到 flag)不需要任何往届比赛的相关知识。

XIZIRUJIN has always been a good tradition of programing. Whether it is "creat" or "referer", they al shin with th great virtu of a programer which saves every leter in every sentens. Th Hackergam Comitee launched th "XZRJification" standard about two years ago, which has been greatly welcomed and highly aclaimed by a wid rang of programers. Her w republish th standard as folows.

XZRJification Standard
XZRJification refers to th proces of deleting som characters in a text which forms another text. Th standard aims at al th continuous sequences of 52 Latin leters named as "word"s in a text. Th 42 leters in a word except "AEIOUaeiou" ar caled "consonant"s. Th XZRJification proces operates on each word in th text acording to th folowing two principles:

Th first principl (also known as creat principl): If th last leter of th word is "e" or "E", and th previous leter of this leter is a consonant, th leter wil b deleted.
Th second principl (also known as referer principl): If ther is a substring of th sam consonant (ignoring cas) in a word, only th first leter of th substring wil b reserved.
It is easy to prov that XZRJification is idempotent: th result of procesing XZRJification multipl times is exactly th sam as that of only onc.

Your Task
In order to get th three flags, you need to complet three python sourc cod files procesed through XZRJification. Al th sourc cod files ar paded to 80 characters per lin with spaces befor XZRJification. Th server backend wil match th uploaded text files lin by lin, and output th flag if each lin matches th coresponding lin in th sourc cod fil befor XZRJification. Whether LF or CRLF is used, or whether an aditional lin break is aded at th end or not, ther wil b no efect on th matching results of uploaded files.

Notes
This articl has been procesed through XZRJification. Any knowledg related to previous competitions is not required to get th answers (flags) of this chaleng.

开启环境

一共三道题

先看题目A,查看惜字如金化后文件

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
#!/usr/bin/python3                                                              

import atexit, bas64, flask, itertools, os, r


def crc(input: bytes) -> int:
poly, poly_degree = 'AaaaaaAaaaAAaaaaAAAAaaaAAAaAaAAAAaAAAaaAaaAaaAaaA', 48
asert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'A'
flip = sum(['a', 'A'].index(poly[i + 1]) << i for i in rang(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in rang(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 els 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xCb4EcdfD0A9F, 0xa9dec1C1b7A3, 0x60c4B0aAB4Bf
asert (u2, u1, u0) == (223539323800223, 186774198532003, 106397893833919)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'litl')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return r.sub(pat, repl, input, flags=r.IGNORECAS)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__fil__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = bas64.b85encod(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.writ(row)
paths.apend(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remov(path)
except FileNotFoundEror:
pas

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_a', __nam__)


@bp.get('/answer_a.py')
def get() -> flask.Respons:
return flask.Respons(xzrj_bytes, content_typ='text/plain; charset=UTF-8')


@bp.post('/answer_a.py')
def post() -> flask.Respons:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerat(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is Non:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is Non:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = bas64.b85encod(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_a.txt', 'rb') as af:
answer_flag = bas64.b85decod(af.read()).decod()
closing, opening = answer_flag[-1:], answer_flag[:5]
asert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

简单,直接vscode自动修复

得到修改后源码

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
#!/usr/bin/python3                                                              

import atexit, base64, flask, itertools, os, re


def crc(input: bytes) -> int:
poly, poly_degree = 'AaaaaaAaaaAAaaaaAAAAaaaAAAaAaAAAAaAAAaaAaaAaaAaaA', 48
assert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'A'
flip = sum(['a', 'A'].index(poly[i + 1]) << i for i in range(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in range(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 else 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xCb4EcdfD0A9F, 0xa9dec1C1b7A3, 0x60c4B0aAB4Bf
assert (u2, u1, u0) == (223539323800223, 186774198532003, 106397893833919)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'little')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return re.sub(pat, repl, input, flags=re.IGNORECASE)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__file__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = base64.b85encode(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.write(row)
paths.append(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remove(path)
except FileNotFoundError:
pass

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_a', __name__)


@bp.get('/answer_a.py')
def get() -> flask.Response:
return flask.Response(xzrj_bytes, content_type='text/plain; charset=UTF-8')


@bp.post('/answer_a.py')
def post() -> flask.Response:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerate(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is None:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is None:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = base64.b85encode(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_a.txt', 'rb') as af:
answer_flag = base64.b85decode(af.read()).decode()
closing, opening = answer_flag[-1:], answer_flag[:5]
assert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

上传修改后的文件得到flag

最后flag为

1
flag{C0mpl3ted-Th3-Pyth0n-C0de-N0w}

查看题目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
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
#!/usr/bin/python3                                                              

import atexit, bas64, flask, itertools, os, r


def crc(input: bytes) -> int:
poly, poly_degree = 'B', 48
asert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'B'
flip = sum(['b', 'B'].index(poly[i + 1]) << i for i in rang(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in rang(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 els 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xdbeEaed4cF43, 0xFDFECeBdeeD9, 0xB7E85A4E5Dcd
asert (u2, u1, u0) == (241818181881667, 279270832074457, 202208575380941)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'litl')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return r.sub(pat, repl, input, flags=r.IGNORECAS)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__fil__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = bas64.b85encod(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.writ(row)
paths.apend(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remov(path)
except FileNotFoundEror:
pas

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_b', __nam__)


@bp.get('/answer_b.py')
def get() -> flask.Respons:
return flask.Respons(xzrj_bytes, content_typ='text/plain; charset=UTF-8')


@bp.post('/answer_b.py')
def post() -> flask.Respons:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerat(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is Non:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is Non:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = bas64.b85encod(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_b.txt', 'rb') as af:
answer_flag = bas64.b85decod(af.read()).decod()
closing, opening = answer_flag[-1:], answer_flag[:5]
asert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

题目 B 和 A 的区别在于第 7 行被「惜字如金化」的字符有点多——通过函数名等各种手段我们可以得知这个函数执行的是 ISO HDLC 标准的 CRC(循环冗余校验)算法,poly 对应的是降幂排列的 48 次模二同余多项式,B 代表系数为 1,b 代表系数为 0。

模二同余多项式在整系数多项式的基础上增加了一条规则:所有系数模二结果相同的多项式视为同一个多项式。换言之:179 - 148x + 276x² - 112x³ + 391x⁴1 + x⁴ 视为同一个多项式。自然地,所有模二同余多项式都可以化简为系数仅为 0 或 1 的标准形式的多项式。

我们复习一下在 ISO HDLC 标准的 CRC 算法下,给定 n 字节的输入和 k 次模二同余多项式,是如何得到 CRC 结果的:

  1. 将给定的输入以小端序表示为一个 8n 比特的整数(高位用 0 补齐):

    例(n = 6):b'HG2024' => 00110100 00110010 00110000 00110010 01000111 01001000

  2. 在整数高位前增加 k 比特的 1,并将最低位的 k 比特取反(与 1 取异或):

    例(k = 8):00110100 00110010 => 11111111 00110100 00110010 => 11111111 00110100 11001101

  3. 将整数转换为升幂形式的多项式(高位对应低次,低位对应高次):

    例(k = 8):11001101 => 1 + x + x⁴ + x⁵ + x⁷

  4. 将转换后的多项式除以给定的 k 次模二同余多项式,并将得到的余式(最高 k - 1 次)转换为标准形式:

    例(k = 4):mod(1 + x + x⁴ + x⁵ + x⁷, 1 + x + x⁴) => 1 - x² - x³=> 1 + x² + x³

  5. 升幂形式的余式转换回 k 比特的整数,并以小端序转换为输出:

    例(k = 16):x² + x⁵ + x⁹ + x¹⁰ + x¹³ + x¹⁴ + x¹⁵ => 00100100 01100111 => b'g$'

包括题目 B 在内的三道题目的 k 均为 48。我们注意到,当我们输入 b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F' 时——经过前三步的处理,我们将整好得到一个 1 + x + x² + ... + x⁴⁸ 的多项式——这个多项式的 0 至 48 次均为 1,49 次及更高次均为 0。将这个多项式除以 poly,得到的余式对应的整数将正好是 poly 的 49 个系数(0 至 48 次)取反后的结果——也就是第 9 行 flip 按位取反后的结果!

故事还没有结束。按照源代码的第 18 至 23 行,CRC 结果还需要经过 u = u₂t² + u₁t + u₀ 后再将 mod(u, 1 << 48) 作为输出。经过简单的配方我们可以得到:

(u₂ * 2t + u₁)² = u₁² - 4u₂ * (u₀ - u)

问题转化为求 u₁² - 4u₂ * (u₀ - u) 的二次剩余。我们将 b'\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x7F' 上传,可以得到的输出如下:

1
2
3
4
5
6
{
"wrong_hints": {
"1": "Unmatched hash (d77759699022)",
"2": ...
}
}

d77759699022 作为小端序整数 u

1
u = int.from_bytes(bytes.fromhex('d77759699022'), 'little') # 38003638106071

代入上式,通过 sympy 计算二次剩余:

1
2
3
4
5
from sympy import sqrt_mod

u2, u1, u0 = 241818181881667, 279270832074457, 202208575380941
rhs = u1 ** 2 - 4 * u2 * (u0 - u) # -80838759903649378693179388311
lhs_sqrt = sqrt_mod(rhs, 1 << 48, all_roots=True) # [2327539748973, 138409948606355, 143065028104301, 279147436961683]

得到四个解,计算得到 2t 可能的 4 个值(此处需要计算 u21 << 48 的倒数):

1
2
# [218134714756316, 208408558555070, 77397226400988, 67671070199742]
double_t = [((i - u1) * pow(u2, -1, 1 << 48)) % (1 << 48) for i in lhs_sqrt]

t 的可能值为 2t / 22t / 2 + (1 << 47) 两种(合计八种),代回验算发现六个增根,只有两个为可能的 t

1
2
# [104204279277535, 38698613200494]
t = [j for i in double_t for j in (i // 2, i // 2 + (1 << 47)) if (u2 * j * j + u1 * j + u0) % (1 << 48) == u]

t 的值为 flip 按位取反的结果,经过处理,最终可以得到可能对应的两行:

1
2
3
4
# t[0] 对应的第 7 行
poly, poly_degree = 'BbbbbbBbbbbbBBbbbBBbbBBBBbbBBbbbbbBbBBBbbBbbbbBbB', 48
# t[1] 对应的第 7 行
poly, poly_degree = 'BBbbbBbbBBbbBbBbbbbbBbBBBbBBbbbBBBbBBbbBBbbBBBbBB', 48

逐个尝试,最后确定后者为正确的第 7 行。

完整源码

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
#!/usr/bin/python3                                                              

import atexit, base64, flask, itertools, os, re


def crc(input: bytes) -> int:
poly, poly_degree = 'BBbbbBbbBBbbBbBbbbbbBbBBBbBBbbbBBBbBBbbBBbbBBBbBB', 48
assert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'B'
flip = sum(['b', 'B'].index(poly[i + 1]) << i for i in range(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in range(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 else 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xdbeEaed4cF43, 0xFDFECeBdeeD9, 0xB7E85A4E5Dcd
assert (u2, u1, u0) == (241818181881667, 279270832074457, 202208575380941)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'little')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return re.sub(pat, repl, input, flags=re.IGNORECASE)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__file__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = base64.b85encode(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.write(row)
paths.append(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remove(path)
except FileNotFoundError:
pass

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_b', __name__)


@bp.get('/answer_b.py')
def get() -> flask.Response:
return flask.Response(xzrj_bytes, content_type='text/plain; charset=UTF-8')


@bp.post('/answer_b.py')
def post() -> flask.Response:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerate(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is None:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is None:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = base64.b85encode(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_b.txt', 'rb') as af:
answer_flag = base64.b85decode(af.read()).decode()
closing, opening = answer_flag[-1:], answer_flag[:5]
assert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

上传文件得到flag

最后flag为

1
flag{Succe55fu11y-Deduced-A-CRC-Po1ynomia1}

查看题目C的文件

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
#!/usr/bin/python3                                                              

import atexit, bas64, flask, itertools, os, r


def crc(input: bytes) -> int:
poly, poly_degree = 'C', 48
asert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'C'
flip = sum(['c', 'C'].index(poly[i + 1]) << i for i in rang(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in rang(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 els 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xDF, 0xF, 0xF
asert (u2, u1, u0) == (246290604621823, 281474976710655, 281474976710655)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'litl')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return r.sub(pat, repl, input, flags=r.IGNORECAS)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__fil__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = bas64.b85encod(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.writ(row)
paths.apend(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remov(path)
except FileNotFoundEror:
pas

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_c', __nam__)


@bp.get('/answer_c.py')
def get() -> flask.Respons:
return flask.Respons(xzrj_bytes, content_typ='text/plain; charset=UTF-8')


@bp.post('/answer_c.py')
def post() -> flask.Respons:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerat(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is Non:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is Non:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = bas64.b85encod(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_c.txt', 'rb') as af:
answer_flag = bas64.b85decod(af.read()).decod()
closing, opening = answer_flag[-1:], answer_flag[:5]
asert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

题目 C 在题目 B 的基础上修改了第 20 行,使得 u2, u1, u0 分别为 0xDFFFFFFFFFFF, 0xFFFFFFFFFFFF, 0xFFFFFFFFFFFF,而被「惜字如金化」的全部为 Ff 字符,可能的情况共有 1 << 32 种,每一种均需通过向服务端发送请求方可确认是否正确,因此选手是根本无法推测出来的:

1
2
3
4
5
6
7
# 以下均为第 20 行可能的情况
u2, u1, u0 = 0xDFFffFFFFfff, 0xFfFfFFfFFffF, 0xFFFffffFFFfF
u2, u1, u0 = 0xDFfFFFFfFfFf, 0xFffFfFFFffFF, 0xFFfFfffFFfFf
u2, u1, u0 = 0xDFffFffFFFfF, 0xFfFFFfFfFFff, 0xFFFFfFfFffFf
u2, u1, u0 = 0xDFfffFffFFFf, 0xFFfFFfffFFfF, 0xFfFFFFFfFfFf
u2, u1, u0 = 0xDFFFFffFfFfF, 0xFffFfFfFffFf, 0xFFFFFFfffFFF
...

我们还有没有什么别的办法呢?

如果选手试着还原了源代码并试图开启一个 HTTP 服务,选手应该会发现根目录生成了一堆 ????????.txt 的文件,和装有 flag 的 answer_c.txt 根本无法分辨——那么我们有没有什么办法在「正统」的访问方式外访问 answer_c.txt 呢?通过阅读源代码可以发现,.txt 前的部分对应某个 Base85 后的结果,我们可以把 b'answer_c' 以 Base85 的形式还原为 b'q\xd3\xdb\xb1\xa7\xd0',对应的小端序整数为 229418662089585,并通过和题目 B 类似的过程可以得到某个「CRC 结果」对应的余式 r(x)

r(x) = x + x⁵ + x¹⁴ + x¹⁶ + x¹⁷ + x²⁰ + x²⁴ + x²⁵ + x²⁶ + x²⁷ + x³⁰ + x³¹ + x³² + x³³ + x³⁶ + x³⁷ + x³⁸ + x³⁹ + x⁴⁰ + x⁴² + x⁴⁴ + x⁴⁶

通过和题目 B 类似的过程,也可以得到 poly 对应的 48 次多项式 p(x)

p(x) = 1 + x + x² + x³ + x⁴ + x⁷ + x⁸ + x⁹ + x¹⁰ + x¹¹ + x¹² + x¹³ + x¹⁵ + x¹⁶ + x¹⁷ + x¹⁹ + x²² + x²⁷ + x²⁸ + x³¹ + x³² + x³⁴ + x³⁵ + x³⁶ + x³⁷ + x⁴¹ + x⁴³ + x⁴⁴ + x⁴⁸

我们的目标是构造长度为 n 字节的输入,使其调整前 m 个字节(m 不小于 6),也就是小端序的最低 8m 个比特,能够按照题目 B 提到的过程得到多项式 n(x),并使其除以 p(x) 后得到的余式正好是 r(x),这样服务端程序便能够读取 answer_c.txt,从而可以通过构造以下几个步骤读取内部的数据:

  1. 取一个 n 的合适范围(例如 7 至 100),输出不为 Unmatched length (??) 对应的 N 即为 answer_c.txt 内数据的大小;
  2. 遍历调整最后一个字节,构造长度为 N 的输入,输出为 Unmatched data (0x??),如果 ?? 和最后一个字节不同,则最后一个字节固定;
  3. 遍历调整倒数第二个字节,构造长度为 N 的输入,输出为 Unmatched data (0x??),倒数第二个字节如果和 ?? 不同则固定;
  4. 重复以上过程直至确定前 m 个字节以外的所有字节。

我们注意到 answer_c.txt 存储的是 Base85 形式的 flag,而 flag 的前五个字节必然为 b'flag{',对应 Base85 形式的前六个字节也随之固定。也就是说,如果令 m = 6,那么 answer_c.txt 的内容也将完全确定下来。现在的问题是:前 m 个字节填充什么呢?

m 个字节对应的是多项式的高次项,也就是 8n - 8m + 488n + 47 次——换言之,可以表示成 q(x) = x ** (8n - 8m + 48) 再乘以一个最高次数为 8m - 1 的多项式。那么,我们有没有办法像整数一样,找到两个多项式 s(x)t(x),使得在模二同余多项式的条件下,满足:

s(x) * p(x) + t(x) * q(x) = gcd(p(x), q(x)) = 1

这一等式呢?通过 sympy.polys.galoistoolsgf_gcdex(扩展欧几里得算法)可以计算得到 s(x)t(x) 的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
from sympy import ZZ
from sympy.polys.galoistools import gf_gcdex

px = ZZ.map([1, 0, 0, 0, 1, 1, 0, 1, 0,
0, 0, 1, 1, 1, 1, 0, 1,
1, 0, 0, 1, 1, 0, 0, 0,
0, 1, 0, 0, 1, 0, 1, 1,
1, 0, 1, 1, 1, 1, 1, 1,
1, 0, 0, 1, 1, 1, 1, 1])

qx = [ZZ(1)] + [ZZ(0)] * 504 # 考虑 8n - 8m + 48 = 504 的情况

sx, tx, gcd = gf_gcdex(px, qx, 2, ZZ)

我们只需要用到 t(x) 的表达式:

t(x) = x² + x³ + x⁷ + x⁸ + x¹³ + x¹⁴ + x¹⁸ + x²¹ + x²³ + x²⁴ + x²⁶ + x²⁹ + x³² + x³³ + x³⁷ + x³⁸ + x³⁹ + x⁴⁰ + x⁴¹ + x⁴² + x⁴⁴ + x⁴⁵ + x⁴⁶ + x⁴⁷ + x⁴⁸(考虑 8n - 8m + 48 = 504 的情况)

现在我们将构造流程描述如下:

  1. 考虑长度为 n 字节的输入,计算对应的多项式,舍去不低于 8n - 8m + 48 次的部分,记为 n₀(x)
  2. 考虑 p(x)q(x),通过以上方法计算得到 t(x)
  3. 选取最高次项低于 8m - 48 次的任意多项式 u(x)(共有 1 << (8m - 48) 种可供选择);
  4. 考虑期望的余式 r(x),计算 m(x) = mod((r(x) - n₀(x)) * t(x), p(x))
  5. 计算 n(x) = (u(x) * p(x) + m(x)) * q(x) + n₀(x),不难验证 mod(n(x), p(x)) = r(x),且 n(x) 低次项和 n₀(x) 相同;
  6. 通过 n(x) 反推回 n 字节的输入,可以验证仅有前 m 字节发生了变化,且 CRC 结果和 r(x) 对应。

构造流程的示例代码如下:

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
from sympy import ZZ
from sympy.polys.galoistools import gf_add, gf_gcdex, gf_mul, gf_rem, gf_sub

px = ZZ.map([1, 0, 0, 0, 1, 1, 0, 1, 0,
0, 0, 1, 1, 1, 1, 0, 1,
1, 0, 0, 1, 1, 0, 0, 0,
0, 1, 0, 0, 1, 0, 1, 1,
1, 0, 1, 1, 1, 1, 1, 1,
1, 0, 0, 1, 1, 1, 1, 1])

rx = ZZ.map([0, 1, 0, 1, 0, 1, 0, 1,
1, 1, 1, 1, 0, 0, 1, 1,
1, 1, 0, 0, 1, 1, 1, 1,
0, 0, 0, 1, 0, 0, 1, 1,
0, 1, 0, 0, 0, 0, 0, 0,
0, 0, 1, 0, 0, 0, 1, 0])

def make_collision(input: bytes, n: int, m: int) -> bytes | None:
assert len(input) == n and m >= 6
# n₀(x) from input
int_input = int.from_bytes(input, 'little')
int_input = (int_input | (0xFFFFFFFFFFFF << (8 * n))) ^ 0xFFFFFFFFFFFF
n0x = [ZZ((int_input >> j) & 1) for j in range(8 * m, 8 * n + 48)]
# q(x) = x ** (8n - 8m + 48)
qx = [ZZ(1)] + [ZZ(0)] * (8 * n - 8 * m + 48)
# s(x), t(x), gcd(p(x), q(x))
sx, tx, gcd = gf_gcdex(px, qx, 2, ZZ)
# m(x) = mod((r(x) - n₀(x)) * t(x), p(x))
mx = gf_rem(gf_mul(gf_sub(rx, n0x, 2, ZZ), tx, 2, ZZ), px, 2, ZZ)
for i in range(2 ** (8 * m - 48)):
# u(x): any polynomial whose highest factor is lower than 8m - 48
ux = [ZZ((i >> j) & 1) for j in range(8 * m - 48)]
# n(x) = (u(x) * p(x) + m(x)) * q(x) + n₀(x)
nx = gf_add(gf_mul(gf_add(gf_mul(ux, px, 2, ZZ), mx, 2, ZZ), qx, 2, ZZ), n0x, 2, ZZ)
# output from n(x)
int_output = sum(int(nx[j]) << (8 * n + 48 + j - len(nx)) for j in range(len(nx)))
int_output = (int_output & ~(0xFFFFFFFFFFFF << (8 * n))) ^ 0xFFFFFFFFFFFF
output = int_output.to_bytes(n, 'little')
if b'\r' not in output and b'\n' not in output:
return output
return None

实际构造时可以选取 m = 7,这是因为如果选取 m = 6,则构造得到的输入可能会存在 \r\n,而 u(x) 只能选取为 0,选取 m = 7 可以使得 u(x) 有多达 256 种选择,规避掉 \r\n 的产生。answer_c.txt 的第七个字节可以通过穷举 85 种情况得到(只有一种情况会使得解码得到的 flag 全部为 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
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
#!/usr/bin/python3                                                              

import atexit, base64, flask, itertools, os, re


def crc(input: bytes) -> int:
poly, poly_degree = 'CcccCCcCcccCCCCcCCccCCccccCccCcCCCcCCCCCCCccCCCCC', 48
assert len(poly) == poly_degree + 1 and poly[0] == poly[poly_degree] == 'C'
flip = sum(['c', 'C'].index(poly[i + 1]) << i for i in range(poly_degree))
digest = (1 << poly_degree) - 1
for b in input:
digest = digest ^ b
for _ in range(8):
digest = (digest >> 1) ^ (flip if digest & 1 == 1 else 0)
return digest ^ (1 << poly_degree) - 1


def hash(input: bytes) -> bytes:
digest = crc(input)
u2, u1, u0 = 0xDFffFfFFffFF, 0xFffFFfFfFffF, 0xFfFFFfFFffFf
assert (u2, u1, u0) == (246290604621823, 281474976710655, 281474976710655)
digest = (digest * (digest * u2 + u1) + u0) % (1 << 48)
return digest.to_bytes(48 // 8, 'little')


def xzrj(input: bytes) -> bytes:
pat, repl = rb'([B-DF-HJ-NP-TV-Z])\1*(E(?![A-Z]))?', rb'\1'
return re.sub(pat, repl, input, flags=re.IGNORECASE)


paths: list[bytes] = []

xzrj_bytes: bytes = bytes()

with open(__file__, 'rb') as f:
for row in f.read().splitlines():
row = (row.rstrip() + b' ' * 80)[:80]
path = base64.b85encode(hash(row)) + b'.txt'
with open(path, 'wb') as pf:
pf.write(row)
paths.append(path)
xzrj_bytes += xzrj(row) + b'\r\n'

def clean():
for path in paths:
try:
os.remove(path)
except FileNotFoundError:
pass

atexit.register(clean)


bp: flask.Blueprint = flask.Blueprint('answer_c', __name__)


@bp.get('/answer_c.py')
def get() -> flask.Response:
return flask.Response(xzrj_bytes, content_type='text/plain; charset=UTF-8')


@bp.post('/answer_c.py')
def post() -> flask.Response:
wrong_hints = {}
req_lines = flask.request.get_data().splitlines()
iter = enumerate(itertools.zip_longest(paths, req_lines), start=1)
for index, (path, req_row) in iter:
if path is None:
wrong_hints[index] = 'Too many lines for request data'
break
if req_row is None:
wrong_hints[index] = 'Too few lines for request data'
continue
req_row_hash = hash(req_row)
req_row_path = base64.b85encode(req_row_hash) + b'.txt'
if not os.path.exists(req_row_path):
wrong_hints[index] = f'Unmatched hash ({req_row_hash.hex()})'
continue
with open(req_row_path, 'rb') as pf:
row = pf.read()
if len(req_row) != len(row):
wrong_hints[index] = f'Unmatched length ({len(req_row)})'
continue
unmatched = [req_b for b, req_b in zip(row, req_row) if b != req_b]
if unmatched:
wrong_hints[index] = f'Unmatched data (0x{unmatched[-1]:02X})'
continue
if path != req_row_path:
wrong_hints[index] = f'Matched but in other lines'
continue
if wrong_hints:
return {'wrong_hints': wrong_hints}, 400
with open('answer_c.txt', 'rb') as af:
answer_flag = base64.b85decode(af.read()).decode()
closing, opening = answer_flag[-1:], answer_flag[:5]
assert closing == '}' and opening == 'flag{'
return {'answer_flag': answer_flag}, 200

上传文件得到flag

最后flag为

1
flag{HAV3-Y0u-3ver-Tr1ed-T0-Guess-0ne-0f-The-R0ws?}

优雅的不等式

注意到

题目描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
你的数学分析助教又在群里发这些奇怪的东西,「注意力惊人」,你随手在群里吐槽了一句。

不过,也许可以通过技术手段弥补你涣散的注意力。

---

你需要用优雅的方式来证明 $\pi$ 大于等于一个有理数 $p/q$。

具体来说就是只使用**整数**和**加减乘除幂运算**构造一个简单函数 $f(x)$,使得这个函数在 $[0, 1]$ 区间上取值均大于等于 $0$,并且 $f(x)$ 在 $[0, 1]$ 区间上的定积分(显然大于等于 $0$)刚好等于 $\pi-p/q$。

给定题目(证明 $\pi\geq p/q$),你提交的证明只需要包含函数 $f(x)$。

- 要优雅:函数字符串有长度限制,
- 要显然:SymPy 能够**快速**计算这个函数的定积分,并验证 $[0,1]$ 上的非负性。

注:解决这道题不需要使用商业软件,只使用 SymPy 也是可以的。

下载源码

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

x = sympy.Symbol('x')
allowed_chars = "0123456789+-*/()x"
max_len = 400

# Example input for difficulty 0: 4*((1-x**2)**(1/2)-(1-x))

for difficulty in range(0, 40):
if difficulty == 0:
p, q = 2, 1
elif difficulty == 1:
p, q = 8, 3
else:
a = (2**(difficulty * 5))
q = sympy.randprime(a, a * 2)
p = sympy.floor(sympy.pi * q)
p = sympy.Integer(p)
q = sympy.Integer(q)
if q != 1:
print("Please prove that pi>={}/{}".format(p, q))
else:
print("Please prove that pi>={}".format(p))
f = input("Enter the function f(x): ").strip().replace(" ", "")
assert len(f) <= max_len, len(f)
assert set(f) <= set(allowed_chars), set(f)
assert "//" not in f, "floor division is not allowed"
f = sympy.parsing.sympy_parser.parse_expr(f)
assert f.free_symbols <= {x}, f.free_symbols
# check if the range integral is pi - p/q
integrate_result = sympy.integrate(f, (x, 0, 1))
assert integrate_result == sympy.pi - p / q, integrate_result
# verify that f is well-defined and real-valued and non-negative on [0, 1]
domain = sympy.Interval(0, 1)
assert sympy.solveset(f >= 0, x, domain) == domain, "f(x)>=0 does not always hold on [0, 1]"
print("Q.E.D.")
if difficulty == 1:
print(open("flag1").read())

# finished all challenges
print(open("flag2").read())

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
from pwn import *
import sympy
from tqdm import tqdm

cache = {}

x = sympy.Symbol('x')
n = sympy.Symbol('n')
a = sympy.Symbol('a')
b = sympy.Symbol('b')
c = sympy.Symbol('c')
t = sympy.Symbol('t')

f_exp = sympy.parsing.sympy_parser.parse_expr('((x**n)*((1-x)**n)*(a+b*x+c*(x**2)))/(1+(x**2))')

for j in tqdm(range(1, 80)):
f = f_exp.subs(n, j)
int_exp = sympy.expand_log(sympy.integrate(f, (x, 0, 1)).simplify())
assert int_exp.free_symbols <= {a, b, c}, int_exp.free_symbols
int_exp = sympy.collect(int_exp, ['log(2)', 'pi'], evaluate=False)

cache[j] = f, int_exp


local = False

if local:
io = process(['python3', 'equation.py'])
else:
io = remote('202.38.93.141', 14514)
io.recvuntil(b'Please input your token: \n')
io.sendline(
b'1411:MEQCIAplFMrlOcSGiuyvXbD2viXkZel+YtLmOBzUXj48+rzXAiBrX2C/xRLIxhAn+TJYUsQtGEDzpleulEFWzyijebwTXA==')

for i in range(0, 40):
io.recvuntil(b'Please prove that pi>=')
line = sympy.parsing.sympy_parser.parse_expr(
io.recvline().decode().strip())
print(i, line)

for j in range(max(1, 2 * i - 2), 100):
if j not in cache:
print('cache miss', j)
f = f_exp.subs(n, j)
int_exp = sympy.expand_log(
sympy.integrate(f, (x, 0, 1)).simplify())
assert int_exp.free_symbols <= {a, b, c}, int_exp.free_symbols
int_exp = sympy.collect(int_exp, ['log(2)', 'pi'], evaluate=False)

cache[j] = f, int_exp
else:
f, int_exp = cache[j]

solutions = sympy.solve(
[int_exp[sympy.log(2)], int_exp[sympy.pi] - 1, int_exp[1] + line], [a, b, c])
print(f'i={i}, j={j}, solutions={solutions}')
if not solutions:
continue

f = f.subs(solutions)
int_exp = sympy.expand_log(sympy.integrate(f, (x, 0, 1)).simplify())

domain = sympy.Interval(0, 1)
if not (sympy.solveset(f >= 0, x, domain) == domain):
continue

break

else:
assert False # no solution found

print('final', f, int_exp)

io.recvuntil(b'Enter the function f(x): ')
io.sendline(str(f).encode())

peek = io.recv(5)
if peek == b'Q.E.D':
io.recvline()
elif peek == b'Trace':
print((peek + io.recvall()).decode())
if i == 1:
print(io.recvline().decode())
print(io.recvall())

运行得到

得到两个flag

1
2
flag{y0u_ar3_g0od_at_constructi0n_08fd32f025}
flag{y0u_ar3_7he_Ramanujan_0f_in3quality_ce4725a5e1}

无法获得的秘密

题目描述:

1
小 A 有一台被重重限制的计算机,不仅没有联网,而且你只能通过 VNC 使用键鼠输入,看视频输出。上面有个秘密文件位于 /secret,你能帮他把文件丝毫不差地带出来吗?

处理视频并录屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import time
import os

# 4194304 / 165 / 48 = 530 frames

f = open('/secret', 'rb').read()
f = ''.join(format(byte, '08b') for byte in f)
f = ''.join('@' if c == '1' else ' ' for c in f)

for i in range(530):
r = os.system('clear')
print(i)
print(f[i*7920:(i+1)*7920])
time.sleep(1)

提取所有帧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import cv2

# 读入视频
cap = cv2.VideoCapture('secret.mp4')

# 总帧数
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

saved = 0
for i in range(total_frames):
# 读取视频的帧
ret, frame = cap.read()
if not ret:
break
# 计算相似度
if i > 0:
diff = cv2.absdiff(frame, last_frame)
diff = cv2.cvtColor(diff, cv2.COLOR_BGR2GRAY)
diff = cv2.threshold(diff, 30, 255, cv2.THRESH_BINARY)[1]
diff = cv2.countNonZero(diff)
if diff > 500000:
cv2.imwrite(f'output/{saved}.jpg', frame)
saved += 1
last_frame = frame

二值化获取数据

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
import numpy as np
from PIL import Image

threshold = 100

def process_image(image_path):
image = Image.open(image_path)
img_array = np.array(image)

cell_width, cell_height = 15, 28.5
cols, rows = 165, 48

r = np.zeros((rows, cols), dtype=int)

for i in range(rows):
for j in range(cols):
top = int(i * cell_height) + 28
left = j * cell_width + 2
right = left + cell_width
bottom = int(top + cell_height)
cell = img_array[top:bottom, left:right]
cell = (cell > 20).astype(int)
non_black_pixels = np.sum(np.any(cell != [0, 0, 0], axis=-1))
r[i, j] = non_black_pixels

r = (r >= threshold).astype(int)
return r

def result_to_bytes(result):
result = result.flatten()
raw_bytes = []
assert len(result) % 8 == 0
for i in range(0, len(result), 8):
byte = 0
for j in range(8):
byte |= result[i + j] << (7 - j)
raw_bytes.append(byte)
return bytes(raw_bytes)

if __name__ == '__main__':
raw_bytes = b""
for i in range(530):
print(i)
image_path = f"output/{i}.jpg"
result = process_image(image_path)
raw_bytes += result_to_bytes(result)
raw_bytes = raw_bytes[:524288]
with open("secret", "wb") as f:
f.write(raw_bytes)

Docker for Everyone Plus

题目描述:

1
2
3
X 是实验室机器的管理员,为了在保证安全的同时让同学们都用上 Docker,之前他直接将同学的用户加入 docker 组,但在去年参加了信息安全大赛之后,他发现将用户加入 docker 用户组相当于给了 root 权限。于是他想到了一个好方法,只授予普通用户使用 sudo 运行特定 Docker 命令的权限,这样就不需要给同学完整的 sudo 权限,也不用将同学加入 docker 组了!

但果真如此吗?

No Enough Privilege

这个小题很简单,观察 sudo -l 的输出,发现 user 用户可以执行 docker image load,但 docker run 不能指定 root 用户,因此只需要制作自定义镜像,在其中嵌入合适的具有 SUID 的程序用来提权即可。下面的 Dockerfile 构建了一个简单的镜像。

1
2
3
4
FROM docker.io/library/alpine:latest

RUN apk add --no-cache su-exec && \
chmod +s /sbin/su-exec

然后将该镜像导出后使用 ZModem 上传至环境,执行 sudo docker image load 导入,最后执行并把主机 / 挂入容器,提权即可获得 flag。

1
2
3
docker run --rm -u 1000:1000 -it --privileged -v /:/host:ro (image name)
exec su-exec root /bin/ash
cat /host/flag

Unbreakable!

sudo -l 可以看出 docker run 命令必须带上 --security-opt=no-new-privileges 参数,因此不能在容器内提权。但注意到 mount 的输出中 /var/lib/docker 挂载点没有 nodev 选项,而 flag 又位于 /dev/vdb,具有固定的设备号,因此我们可以加载一个带对应设备文件,且该文件所有者为 1000:1000 的镜像来读取 flag。

然而,Docker 采用了 cgroup 进行资源限制,其中 Device Controller 阻止容器访问未经授权的设备,同时也可以注意到,运行带 --privileged--device 的 Docker 命令是被禁止的,因此无法从容器内读取 flag。但我们可以另辟蹊径,procfs 中提供了一个“穿越点”,即 /proc/<pid>/root,可以用于访问对应进程的挂载命名空间的根目录,而 sudo docker run 启动的容器是以 user 的 UID 执行的,因此 user 可以访问主机上 procfs 中容器内进程的目录。所以只需要预制一个带已修改所有者的设备文件的镜像,使用该镜像启动一个 sleep 命令,然后在主机上 ps 查看 sleep 命令的 PID,读取 /proc/<pid>/root/flag 即可得到 flag。

构建解题镜像的 Dockerfile:

1
2
3
4
FROM docker.io/library/alpine:latest

RUN mknod /flag b 253 16 && \
chown 1000:1000 /flag

看不见的彼方:交换空间

题目描述:

1
2
3
两年过去了,今年,Alice 和 Bob 再次来到了 Hackergame 的赛场上。这一次,他们需要在各自的 chroot(2) 的限制下,将自己手头 tmpfs 里面(比较大的)文件交给对方。

好消息是,这次没有额外的 seccomp(2) 限制,但是,他们所处的容器环境的 rootfs 是只读的,并且内存也是有限的,所以如果再复制一份的话,整个容器就会被杀死。Alice 和 Bob 希望请你帮助他们解决这个难题。

小菜一碟

A代码

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
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/shm.h>

void perror_exit(char* message){
perror(message);
exit(-1);
}

struct message {
long type;
char buffer[4];
};

int main()
{
printf("a start\n");
int mqab=msgget(12450,0666 |IPC_CREAT);
if(mqab<0){
perror_exit("msgget swapab error");
}
int mqba=msgget(12451,0666 |IPC_CREAT);
if(mqba<0){
perror_exit("msgget swapba error");
}
int shm=shmget(12452,2*1024*1024,0666 |IPC_CREAT);
if(mqba<0){
perror_exit("shmget error");
}
void* mem=shmat(shm,0,0);
if(mem==(void*)(-1)){
perror_exit("shmat error");
}
int fd=open("/space/file",O_RDWR);
if(fd<0){
perror_exit("open error");
}
printf("a ok\n");
char* read_start=((char*)(mem));
char* write_start=((char*)(mem))+1024*1024;
for(int i=0;i<128;i++){
int segment_start=i*1024*1024;
// 读出文件内容
off_t read_off=lseek(fd,segment_start,SEEK_SET);
if(read_off<0){
perror_exit("lseek error");
}
int read_count=0;
while(read_count<1024*1024){
int read_len=read(fd,read_start+read_count,1024*1024-read_count);
if(read_len<0){
perror_exit("read error");
}
read_count+=read_len;
}
// 发送同步信号
struct message out;
out.type=1;
if (msgsnd(mqab, &out, sizeof (struct message), 0) <0) {
perror_exit("msgsnd error");
}
// 接收同步信号
struct message in;
if (msgrcv(mqba, &in, sizeof (struct message),0, 0) <0) {
perror_exit("msgrcv error");
}
// 写回文件内容
off_t write_off=lseek(fd,segment_start,SEEK_SET);
if(write_off<0){
perror_exit("lseek error");
}
int write_count=0;
while(write_count<1024*1024){
int write_len=write(fd,write_start+write_count,1024*1024-write_count);
if(write_len<0){
perror_exit("write error");
}
write_count+=write_len;
}
}
printf("a complete\n");
}

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
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
#include <unistd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/msg.h>
#include <sys/shm.h>

void perror_exit(char* message){
perror(message);
exit(-1);
}

struct message {
long type;
char buffer[4];
};

int main()
{
printf("b start\n");
int mqab=msgget(12450,0666 |IPC_CREAT);
if(mqab<0){
perror_exit("msgget swapab error");
}
int mqba=msgget(12451,0666 |IPC_CREAT);
if(mqba<0){
perror_exit("msgget swapba error");
}
int shm=shmget(12452,2*1024*1024,0666 |IPC_CREAT);
if(mqba<0){
perror_exit("shmget error");
}
void* mem=shmat(shm,0,0);
if(mem==(void*)(-1)){
perror_exit("shmat error");
}
int fd=open("/space/file",O_RDWR);
if(fd<0){
perror_exit("open error");
}
printf("b ok\n");
char* read_start=((char*)(mem))+1024*1024;
char* write_start=((char*)(mem));
for(int i=0;i<128;i++){
int segment_start=i*1024*1024;
// 接收同步信号
struct message in;
if (msgrcv(mqab, &in, sizeof (struct message),0, 0) <0) {
perror_exit("msgrcv error");
}
// 读出文件内容
off_t read_off=lseek(fd,segment_start,SEEK_SET);
if(read_off<0){
perror_exit("lseek error");
}
int read_count=0;
while(read_count<1024*1024){
int read_len=read(fd,read_start+read_count,1024*1024-read_count);
if(read_len<0){
perror_exit("read error");
}
read_count+=read_len;
}
// 写回文件内容
off_t write_off=lseek(fd,segment_start,SEEK_SET);
if(write_off<0){
perror_exit("lseek error");
}
int write_count=0;
while(write_count<1024*1024){
int write_len=write(fd,write_start+write_count,1024*1024-write_count);
if(write_len<0){
perror_exit("write error");
}
write_count+=write_len;
}
// 发送同步信号
struct message out;
out.type=1;
if (msgsnd(mqba, &out, sizeof (struct message), 0) <0) {
perror_exit("msgsnd error");
}
}
printf("b complete\n");
}

编译上传文件得到flag

捉襟见肘

A代码

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
""""exec python3 $0
exit
"""
import os
import socket
import ctypes

libc = ctypes.CDLL(None, use_errno=True)
libc.syscall.restype = ctypes.c_long
libc.syscall.argtypes = [
ctypes.c_long, # syscall number
ctypes.c_int, # fd
ctypes.c_int, # mode
ctypes.c_uint64, # offset
ctypes.c_uint64, # len
]
SYS_fallocate = 285
FALLOC_FL_KEEP_SIZE = 0x01
FALLOC_FL_PUNCH_HOLE = 0x02

def fallocate(fd, mode, offset, length):
res = libc.syscall(SYS_fallocate, fd, mode, offset, length)
if res < 0:
errno = ctypes.get_errno()
raise OSError(errno, os.strerror(errno))
return res

def move_file(src_fd, dst_fd, bufsize=4096):
filesize = os.fstat(src_fd).st_size
print(f"{src_fd=}, {filesize=}")
assert filesize % bufsize == 0
buf = bytearray(bufsize)

offset = 0
while offset < filesize:
size = os.preadv(src_fd, [buf], offset)
assert size == bufsize
size = os.pwrite(dst_fd, buf, offset)
assert size == bufsize
fallocate(
src_fd,
FALLOC_FL_KEEP_SIZE | FALLOC_FL_PUNCH_HOLE,
offset,
bufsize,
)
offset += bufsize

def main():
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.bind(b"\0foobar")
_, fds, *_ = socket.recv_fds(sock, 1024, 3)
dst0, src1, src2 = fds

src0 = os.open("/space/file", os.O_RDWR)
dst1 = os.open("/space/file1", os.O_RDWR | os.O_CREAT)
dst2 = os.open("/space/file2", os.O_RDWR | os.O_CREAT)

move_file(src0, dst0)
move_file(src1, dst1)
move_file(src2, dst2)

with open("/space/file1", "ab"), open("/space/file2", "ab"):
pass

main()

B代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
""""exec python3 $0
exit
"""
import os
import time
import socket

sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
time.sleep(0.5)
sock.connect(b"\0foobar")
socket.send_fds(sock, [b"hello"], [
os.open("/space/file", os.O_RDWR | os.O_CREAT),
os.open("/space/file1", os.O_RDWR),
os.open("/space/file2", os.O_RDWR),
])

编译文件上传得到flag


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