Hackergame 2021 Writeup

被浏览

之前我们战队的副队长在新生群里发了这个比赛,就决定拿它来初步感受一下 CTF 的比赛氛围。

题目和真正的 CTF 感觉还是有不少差别的,感觉很多题可以归类到 Misc 中去。

不过这个比赛让我深刻地体会到了 CTF 知识的宽度有多广,不过我啥也不会全靠搜。

听说中科大的 CTF 战队入队标准是进排行榜前 41,还是有点恐怖。排行榜越往上分数断层也越大,也说明了我们与顶尖 CTF 选手之间的水平差距巨大。榜首的 mcfx 之前打 OI 的时候也听说过,是个巨佬。

Linux 有关的知识至关重要,所以等我新电脑到了就把老电脑换成 Arch,正好可以修订一下我的Arch 安装教程。就我个人经历而言,不推荐双系统,因为坑爹的 Win10 更新指不定哪天就把你的引导给覆盖了。

得益于丰富的库,Python 在某些操作上十分方便,看来得学习一下基础语法了。比赛里好多题有想法,却因为不会写 Python 而放弃了,有点可惜。

签到

真签到题,就如题面所述,“你只需要打开日记,翻到 Hackergame 2021 比赛进行期间的任何一页就能得到 flag!”

所以你只需要 ?page=任意比赛时间进行期间的时间戳 就能拿到 flag。

随便试的时候记得有时间是比赛暂停的(

进制十六——参上

签到题加一,把右边涂掉部分对应的左侧 Hex 编码输入到随便一个 Hex 编辑器即可。

去吧!追寻自由的电波

挺有意思的一道题,下载下来的音频是经过变速的,得手动放慢时间。

但是现在的倍速软件过于智能,变速不改变音调,没啥办法,用舍友的 AU 调了一段不是很清晰的出来。

勉强听出 Golf Hotel November Tango India,怀疑是哪里的单词表,直接 Google。

发现果然是单词表里的词,但是不按顺序。我还在 Reddit 上看到了这样一个帖子,顿时会心一笑(主要还是这个帖子内容有趣)。

原来是用单词的首字母加密的,于是在魔鬼调音下听了半小时,对着单词表勉强对应,得到录音内容:

1
Foxtrot Lima Alpha Golf Leftbracket Papa Hotel Oscar November Echo Tango India Charlie Bravo Alpha Rightbracket

注意题面告诉我们都是小写字母,翻译一下就是:flag{phoneticab}

正好也是单词表的名字:PhoneticAlphabet

猫咪问答 Pro Max

看介绍似乎是 Hackergame 的传统了?考验信息搜集能力(

第一问

我一开始没想起来 web.archive.org 这个网站,一直在找搜索引擎的网页快照,完全失败。

后来我就想,既然 SEC@USTC 和 USTCLUG 合并了,能不能在 USTCLUG 的网站上找到蛛丝马迹呢?

终于,我找到了这个,历年比赛的归档里有一个是在 web.archive.org 上的!

于是顺藤摸瓜,找到了章程

答案是 20150504

第二问

官方介绍中,是只有四次的,所以我就一直写的 4,但是一直不对。

然后我就想,这么有影响力的社团今年怎么可能缺席呢,估计是网站没更新,就对了。

答案是 5

垃圾网站不更新,浪费我时间

第三问

我找了个中科大的朋友直接要了一份图片。人脉也算信息搜集的一环,不是吗

答案是 Development Team of Library

第四问

就按着题面去找 SIGBOVIK 2021,这篇论文在这个pdf的 216 页。

这个无厘头的论文证明了任何非零十进制数的二进制开头是 1,真是白花了我那么久时间看英文论文,按照文章逻辑,后面附的数据集都是。

答案是 13

P.S. SIGBOVIK 好像每年的论文都很无厘头,比如这篇论文的上一篇就是一个有味道的文章(

第五问

直接按题面关键字搜索,发现这份文件

大致看了下,好像是吐槽别人不按规范操作的搞笑协议,和上一问一起让我体会到了外国人的幽默感。

答案在 section-6,是 /dev/null

P.S. 按照 Wiki 的说法,/dev/null(或称空设备)在类Unix系统中是一个特殊的设备文件,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF。再结合文章内容,也让人会心一笑。

卖瓜

一开始看这题分类是 web,以为是改包这类的,后来发现检测是在后端,那就没啥办法了。

只能随便试试,发现称 6 斤的瓜溢出后是 0,但是称 9 斤的瓜溢出后是 -2^63,这个数对 3 取模是 2,而我们的目标 20 对 3 取模也是 2,所以我们只需要称两次溢出的 9 斤瓜就能让我们需要的瓜数量被 3 整除。

但我一开始直接溢出两次发现虽然我是 20/20,但是就是不给我 flag。

想了很久才意识到直接溢出两次显示的是 -1.844674407371E+19,而题目的补充说明告诉我们,当称的数字变为浮点数而不是整数时,HQ 不会认可最终的称重结果。

所以溢出一次后加回到正值再溢出就行了。

最后的输入(前面是选择瓜的种类,后面是数量):

1
2
3
4
5
9 10000000000000000000
6 1537228672808120301
6 1537228672808120301
9 10000000000000000000
6 2018004

透明的文件

根据题面的描述“劣质终端”以及附件中的内容加上我仅有的一些 Linux 常识,我判断这些是 ANSI 转义字符。

所以在所有的 [ 前加上 \e,然后放到 WSL 中去输出,修改后的文件内容很长,我就不放了。

结果得到了这样的输出:

完全看不清啊(#`O′)

一天后我向一位高中时的学长吐槽时发了截图,What’s up,缩小后的图案居然清晰了!

根据题目描述,勉强辨认出 flag{abxnniohkalmcowsayfiglet}

旅行照片

一开始觉得这道题非常离谱,毫无头绪。

后来发现了那个独特的蓝色 KFC,好家伙,网红店啊,再根据地理位置推测出方位。

于是我们得到面朝方向为东南,电话号码是 0335-7168800,隔壁是海豚馆。

再根据照片倾斜角度推测是向下拍摄的,剩下两个直接枚举就行。

拍摄时间大致为傍晚,楼层是 14。

FLAG 助力大红包

发现有个助力链接,能加提取量。总感觉在内涵某些 APP 呢

活动要求位于同一 /8 网段的用户将会被视为同一个用户,达到助力次数上限后,将无法再帮助好友助力。并且使用前后端方式检查用户的 IP。

先看一下网页源代码,发现前端检测是借助第三方 js 实现的。然后点一下助力抓个包看看,发现将通过第三方得到的 ip 传给了后端,并且知道后端是 PHP 实现。

通过搜索可知,可以在 Header 中设置 X-Forwarded-For 来伪造地址。

于是我们可以直接写脚本了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sendRequest(x) {
var ip = x + ".0.0.0";
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("POST", "http://202.38.93.111:10888/invite/8cf96f4d-7a5e-47c5-b20c-683310d8a91b");
xhr.setRequestHeader("X-Forwarded-For", ip);
xhr.send("ip=" + ip);
}

const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))

for (i = 0; i <= 255; i++) {
await sleep(1750);
sendRequest(i);
}

十分钟的时间还是有点紧的。

Amnesia - 轻度失忆

通过搜索可得,ELF 文件中存在不存放在 .data.rodata 中的数据,那就是 .bss,通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。临时变量就放在 .bss 中。

所以有代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main() {
char c;
c = 'H'; putchar(c);
c = 'e'; putchar(c);
c = 'l'; putchar(c);
c = 'l'; putchar(c);
c = 'o'; putchar(c);
c = ','; putchar(c);
c = ' '; putchar(c);
c = 'w'; putchar(c);
c = 'o'; putchar(c);
c = 'r'; putchar(c);
c = 'l'; putchar(c);
c = 'd'; putchar(c);
c = '!'; putchar(c);
}

图之上的信息

用给的 guest 账户登录后,提示 flag 是 admin 的邮箱。

进行抓包,账号密码挺正常的,但发现有个请求内容很奇怪:

1
2
url: http://202.38.93.111:15001/graphql
body: {"query":"{ notes(userId: 2) { id\ncontents }}"}

试了一下 userId: 1,发现权限不够,猜测 admin 的用户 id 就是 1。

是没见过的类型,想起来题目里提到了 GraphQL,赶紧搜一下(

搜到了这样一篇文章,讲了一些基础的东西。

利用里面的方法,我们用 payload:

?query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

来得到内部的数据结构,和 SQL 中的 information_schema 有异曲同工之妙。

以下是省略了无关内容的表。

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
{
"data": {
"__schema": {
"types": [
{
"name": "Query",
"fields": [
{
"name": "note",
"args": [
{
"name": "id",
"description": null,
"type": {
"name": "Int",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "notes",
"args": [
{
"name": "userId",
"description": null,
"type": {
"name": "Int",
"kind": "SCALAR",
"ofType": null
}
}
]
},
{
"name": "user",
"args": [
{
"name": "id",
"description": null,
"type": {
"name": "Int",
"kind": "SCALAR",
"ofType": null
}
}
]
}
]
},
{
"name": "GNote",
"fields": [
{
"name": "id",
"args": []
},
{
"name": "contents",
"args": []
}
]
},
......
{
"name": "GUser",
"fields": [
{
"name": "id",
"args": []
},
{
"name": "username",
"args": []
},
{
"name": "privateEmail",
"args": []
}
]
},
......
]
}
}
}

借由一开始的例子,我们很容易构造出 payload:

?query={ user(id: 1) { id, username, privateEmail }}

P.S. 在我没登录的时候也能通过最开始的语句查询到 guest 的信息,完全有理由怀疑那就是个幌子,来引诱人向登录 admin 账户这一方向思考。

minecRaft

看上去很难,所以直接查看源码,发现一个 <script src="jsm/miscs/flag.js"></script>,先别急,看看是怎么调用这个 js 里的函数的。

找到:

1
2
3
4
5
6
7
8
9
if(cinput.length>=32){
let tbool=gyflagh(cinput.join(''));
if(tbool) {
pressplateList[65].TurnOn_redstone_lamp();
content.innerText='Congratulations!!!';
return;
}
cinput.length=0;
}

说明 flag 长度为 32,且通过 gyflagh 来判断是否正确。

然后我们把目光转向 jsm/miscs/flag.js,发现里面乱七八糟一团糟,全是十六进制,直接裂开。

但仔细研究后发现有相当一部分是为了混淆原来代码的,比如第一个函数是为了检查最下面的字符串数组数据完整性,剩下有相当一部分是利用那个数组把字符串的一些操作给混淆成十六进制数。

经过整理和重命名变量后,得到一份人能看懂的代码:

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
(String['prototype']['encrypt'] = function (str) {
const s = new Array(2), key = new Array(4);
let res = ''; plaintext = escape(this);
for (var i = 0; i < 4; i++)
key[i] = Str4ToLong(str.slice(i * 4, (i + 1) * 4)); // 将密码表8位一组放入 key 中
for (i = 0; i < plaintext.length; i += 8) {
s[0] = Str4ToLong(plaintext.slice(i, i + 4));
s[1] = Str4ToLong(plaintext.slice(i + 4, i + 8));
// 8位一组中的前4位和后4位分别存入s[0]和s[1]中
code(s, key);
res += LongToBase16(s[0]) + LongToBase16(s[1]);
}
return res;
});
function code(s, key) {
const delta = 2654435769;
let v0 = s[0], v1 = s[1];
let sum = 0;
for (let i = 0; i < 32; i++) {
v0 += (((v1 << 4) ^ (v1 >>> 5)) + v1) ^ (sum + key[now & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >>> 5)) + v0) ^ (sum + key[now >>> 11 & 3]);
}
s[0] = v0, s[1] = v1;
}
function Str4ToLong(s) {
let res = 0;
for (let i = 0; i < 4; i++)
res |= s.charCodeAt(i) << (i * 8);
return isNaN(res) ? 0 : res;
}
function LongToBase16(x) {
let res = '';
for (let i = 3; i >= 0; i--) {
let tmp = (x >> 8 * i & 255).toString(16);
if (parseInt('0x' + tmp) <= 15) tmp = '0' + tmp;
res += tmp;
}
return res;
}
function gyflagh(_0x111955) {
let _0x3b790d = _0x111955['encrypt']('1356853149054377');
if (_0x3b790d === '6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c') return 1;
return 0;
}

可以发现是将操作序列用密钥加密后和原有的比较。

关键点是那个 code 函数,直接反向操作即可,正好这份代码中还提供了 LongToStr4 和 Base16ToLong,直接调用即可。

反向操作时要求数据恒正,我不会用 Javascript 进行简便的操作,就写了一份 C++ 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <bits/stdc++.h>
using namespace std;
unsigned key[] = {909456177, 825439544, 892352820, 926364468};
void decode(unsigned v0, unsigned v1) {
unsigned delta = 2654435769, sum = delta * 32;
for (int i = 0; i < 32; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum >> 11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
cout << v0 << ' ' << v1 << endl << endl;
}
int main() {
int x, y;
while (cin >> x >> y) decode(x, y);
}

再通过提供的 LongToStr4 即可得到原文:

拼接起来即为 flag:flag{McWebRE_inMlnCrA1t_3a5y_1cIuop9i}

P.S. 可以通过搜索得到这个算法