张麦麦

Hackergame2021 writeup(部分)

· 20min· CTF·#JavaScript#Web#逆向

去年第一次参加 hackergame 还是很有意思的,今年接触了更多相关东西,折腾这几天也学习了很多,不过可惜 binary 和 math 还是零蛋,希望以后能补齐这方面的姿势吧~

签到题

本质上是时间戳,取比赛时间的任意即可。

进制十六——参上

图片直接发到 qq 上使用文字识别复制 flag 相关部分。

Terminal window
1
20612062 7974652E 20666C61 677B5930 555F5348 30553144 5F6B6E30 775F4830
2
575F7430 5F43306E 76337274 5F484558 5F746F5F 54657854 7D20466F 72206578
Terminal window
1
a byte. flag{Y0U_SH0U1D_kn0w_H0W_t0_C0nv3rt_HEX_to_TexT} For ex

去吧!追寻自由的电波

本题考查北约音标字母 (英语:NATO phonetic alphabet) ,但是他加速过,所以你需要使用软件放慢,然放慢其加速者,损者必不可复之。所以很奇怪的是我使用 fcp 或一些软件放慢后,损失还是很大基本没法辨别。

但是使用 audacity 放慢 0.5 倍。基本上可以听出来,我怀疑是他用了某种算法吧。

听出来后是

Terminal window
1
Foxtrot Lima Alpha Golf { Papa Hotel Oscar November Echo
2
Tango India Charlie Alpha Bravo }

取下首字母,得到 flag{phoneticab}

有个小插曲,我不知道为啥认为 K 对应的是 Kapa,其实是 Kilo 。因此我用 khoneticab 提交了好几次都不行,甚至认为这里面还有一层加密……折腾了半天没有结果。

猫咪问答 Pro Max

本题就是综合性的问答吧,考查你信息检索的能力。

1.2017 年,中科大信息安全俱乐部(SEC@USTC)并入中科大 Linux 用户协会(USTCLUG)。目前,信息安全俱乐部的域名(sec.ustc.edu.cn)已经无法访问,但你能找到信息安全俱乐部的社团章程在哪一天的会员代表大会上通过的吗?

很自然的想到 Internet Archive 的网页快照,对于这种站点,基本上都是有收录的。于是我们找到: https://web.archive.org/web/20170515053637/http://sec.ustc.edu.cn/doku.php/codes

可以发现:

本章程在 2015 年 5 月 4 日,经会员代表大会审议通过。

所以答案是:20150504。

2. 中国科学技术大学 Linux 用户协会在近五年多少次被评为校五星级社团?

顺着 hackergame 的网站找到他们的官网 https://lug.ustc.edu.cn/ ,找到简介处:https://lug.ustc.edu.cn/wiki/intro/

并于 2015 年 5 月、2017 年 7 月、2018 年 9 月、2019 年 8 月及 2020 年 9 月被评为中国科学技术大学五星级学生社团

所以答案是:5。

3. 中国科学技术大学 Linux 用户协会位于西区图书馆的活动室门口的牌子上「LUG @ USTC」下方的小字是?

肯定不可能用街景地图来找,于是思路大概是搜索一下相关报道。有了上一题的思路,我们不妨在他们官网搜索一下:「图书馆」之类的关键词。结果你猜怎么着,一搜搜到了。 https://lug.ustc.edu.cn/news/2016/06/new-activity-room-in-west-library/

所以答案是:Development Team of Library(注意大小写)。

4. 在 SIGBOVIK 2021 的一篇关于二进制 Newcomb-Benford 定律的论文中,作者一共展示了多少个数据集对其理论结果进行验证?

啊,论文,omg。这题不需要你读懂论文,但是你至少得稍微理解一下意思。 我们先通过关键词找到论文: Newcomb-Benford Law SIGBOVIK 2021 google 一下第一个就是,还是完整的 pdf。如果没有的话一般也就用 google scholarresearch gate 找下咯。

The Newcomb-Benford Law, Applied to Binary Data: An Empirical and Theoretic Analysis

看一个局部,我们能知道后面这些图都是他展示的数据。 一共 14 个图,第一个图是总结性的,所以是 13 个数据集。

The tabulated data is omitted, for brevity, as well as to protect the privacy of our data sources. We converted all of the data points to their binary representations, and plotted the relative frequency of the leading digits. The figures are shown in Figs. 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, and 14 in the appendix 4 .

所以答案是 13.

5. 不严格遵循协议规范的操作着实令人生厌,好在 IETF 于 2021 年成立了 Protocol Police 以监督并惩戒所有违背 RFC 文档的行为个体。假如你发现了某位同学可能违反了协议规范,根据 Protocol Police 相关文档中规定的举报方法,你应该将你的举报信发往何处?

关键词搜一下,能找到:https://datatracker.ietf.org/doc/html/rfc8962#section-6

不难发现:

Send all your reports of possible violations and all tips about

wrongdoing to /dev/null. The Protocol Police are listening and will take care of it.

所以答案是:/dev/null。

卖瓜

话说这题真的是 web 题? 这题出要考察的是溢出。尝试进行 post 伪造之类的都不行之后,应该是溢出的面比较大了。 可以多输入几个数字试试是否能得到负值。因为我们怎么样都不可能用 6 和 9 拼出来 20 斤的瓜(整数下) 且

并且这个数值不能太,因为由于数据类型的精度限制,我们很容易损失后面的值造成误差。因此这里我们多次尝试后,两边都取 1e18 后得到的 -3446744073709551616 就是一个很好的数字。 但是哪怕这个值用 js 进行运算仍会损失精度。

C 语言,去吧!

1
#include <stdio.h>
2
3
int main() {
4
// 这个数挺大的,而且超出了 double,我们就上 long 吧,去负号加个 20。
5
long long result = 3446744073709551616+20;
6
printf("%lld\n",result);
7
long long div = result/9.0;//再除以 9,看看得到什么值
8
printf("%lld\n",div);
9
long long NINE = 9;
10
printf("%lld\n",div*NINE-result);
11
return 0;
12
}
13
14
}

我们会发现,输出为以下数值:

Terminal window
1
3446744073709551616 //原数值取正+20
2
382971563745505735 //除 9 后得到的值
3
-1 //乘 9 再加回去,得到负一。

为啥是负一? 还是精度问题吗??

其实我尝试过如果直接除 9.0,除的结果还是有问题的……得出来是-64,但是仍然可以构造出我们想要的想法

估计还是精度损失了,暂时没想到解决方法。

但是-1 够我们用了。直接用 2*6+9=21 加过去,就能得到 20 了。 也算符合我们的预期。

透明的文件

本题考查 ASCII 控制码。

ANSI 转义序列是命令行终端下用来控制光标位置、字体颜色以及其他终端选项的一项 in-bind signaling 标准。通常是在文本中嵌入确定的字节序列(符合带内信令的定义),大部分以 ESC 转义字符和 ”[” 字符开始,终端会把这些字节序列解释为相应的指令,而不是普通的字符编码。 [1]

ANSI Escape code 编码中有专门控制字符颜色的控制符,因为 \e 控制符的 16 进制码为 0x1B , 8 进制码为 033 ,所以以下表示方式等价: \e[0m , \033[0m , \u001b[0m

因此原文中的文本缺失了前面的控制符号,其次就是中间的填充字符被换为了空格。只需要一一替换即可。

1
with open('./transparent.txt','r') as f:
2
ansi = f.read().replace('[','\033[').replace(' ','█')
3
print(ansi)

输出一下还真是五彩斑斓的 flag 呢(笑

旅行照片

好耶! 是网络迷踪!

首先肯定要找到他的现实位置。 初看照片我只有几个比较强的印象:海或湖。

但是第五题提醒了我们,左上角有一个肯德基。 「海边的肯德基」,并且是蓝色的,很明显的特征,于是就可以搜到这里是:肯德基(新澳海底世界店)。

1.该照片拍摄者的面朝方向为:

面朝方向(东 | 东南 | 南 | 西南 | 西 | 西北 | 北 | 东北)

地图上找到就行,很明显拍摄者面朝东南方。

2.该照片的拍摄时间大致为:

拍摄时间(清晨 | 上午 | 中午 | 下午 | 傍晚)

拍摄时间,应该就在傍晚和下午之间,看影长傍晚的面比较大。

3.该照片的拍摄者所在楼层为:

小区内每栋楼的层高和海拔均相同,且地上部分楼层为从 1 开始的连续自然数

爆破了一下,14.

4.该照片左上角 KFC 分店的电话号码是:

请使用短横线分隔区号和本机号码,示例格式:0551-63600110

高德可以搜到 0335-7168800

5.该照片左上角 KFC 分店左侧建筑有三个水平排列的汉字,它们是:

三个「肯德基」以外的简体中文汉字

高德里有图片,是海豚馆。

FLAG 助力大红包

笑死,天下苦 pdd 久矣。

由于我使用分流 blocked 了一部分跟踪/统计的 ip ,所以反而进入他砍一刀的时候就提醒我 ip 不对。这里思路大致就可以走下去了。

首先前端部分,使用搜狗的 api 获取用户 ip ,是一个 jsonp 请求,被我的 geoip 给屏蔽了:https://pv.sohu.com/cityjson?ie=utf-8

截取一下 post 请求,发现使用 ip 字段发送,尝试伪造后,提醒前后端 ip 不一致,后端检测也没有太好的方法,一般都是 xff,直接伪造一下就行。 发现可行后,使用 python 编写程序进行随机的伪造 ip 请求即可。

1
import requests
2
import random
3
import struct
4
import socket
5
import time
6
7
def get_mid_str(s, start_str, stop_str):
8
start_pos = s.find(start_str)
9
if start_pos == -1:
10
return None
11
start_pos += len(start_str)
12
stop_pos = s.find(stop_str, start_pos)
13
if stop_pos == -1:
14
return None
15
return s[start_pos:stop_pos]
16
ip_used = []
17
def zhuli(url):
18
ip = socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
19
if(ip not in ip_used):
20
ip_used.append(ip)
21
else:
22
while(ip in ip_used):
23
ip = socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
24
ip_used.append(ip)
25
data = {
26
"ip":ip
27
}
28
r = requests.post(url,data,headers={"X-Forwarded-For":ip})
29
res = get_mid_str(r.text,'<div class="alert alert-success row" role="alert">','</div>')
30
print(res)
31
32
times = 0
33
while(True):
34
zhuli("your url")
35
times = times +1
36
37
if(times == 11):
38
time.sleep(2)
39
times = 0

请求太频繁会被提示,所以要控制好时间,这里大概取 11 次就差不多。 另外这份代码并不完善,因为随机规则+一个已用 ip 列表并不能完全符合他的要求,因为题目要求统一网段下也算相同 ip。 因此这里还可以完善一下。但是用肯定是能用的。

另外用 Burp Suite 大法也可。

图上之信息

这题考查 GraphQL 的注入漏洞,或者是权限分配问题。

搜索 GraphQL 渗透/注入 可以找到一些信息。

查询类型所有的字段[2]

1
{
2
__type (name: "Query") {
3
name
4
fields {
5
name
6
type {
7
name
8
kind
9
ofType {
10
name
11
kind
12
}
13
}
14
}
15
}
16
}

找到 privateEmail 字段查询即可。

(说真的 email 之类的我试了 n 多次,我觉得我是有试出来的可能性的)

赛博厨房

类似汇编的语法 + 找规律。

Level0 一共就四种情况,分别写出来即可。

Level1 要注意使用判断跳转,审查工具看下菜谱数量就行了, 没有啥变化。 也可以通过其他工具绕过行数限制生成若干行代码直接提交(朋友的做法)

后面的不会

mineCraft

后面的题目中唯一做出来的。 讲真的我也没想到我能做出来,所以这和 mc 到底有啥关系呢?

首先经过简单的尝试,可以看见 script 中获取键盘输入,以及 gyflagh 函数调用进行验证的逻辑,

1
function printcinput() {
2
let content = document.getElementById('spann'); // 左上角输入回显
3
if (cinput[0] === 'M') { // cinput 为数组,存储键入数值
4
if (pressplateList[64].status === false) { // 如果 64 的红石灯未亮
5
pressplateList[64].TurnOn_redstone_lamp(); // 点亮
6
pressplateList[64].status = true;
7
}
8
}
9
if (cinput.length >= 32) { // cinput 值大于 32 时
10
let tbool = gyflagh(cinput.join('')); //传入 gyflagh 判断是否是所需值
11
if (tbool) {
12
pressplateList[65].TurnOn_redstone_lamp();
13
content.innerText = 'Congratulations!!!';
14
return;
15
}
16
cinput.length = 0;
17
}
18
content.innerText = cinput.join('');
19
}

这个函数在 flag.js 里。这个函数经过了混淆,需要我们抽丝剥茧的进行还原。

混淆的大概原理是,搞一些无关的参数。 一些函数的调用要先赋给变量再调用, 又把一些核心函数和值都丢到一个里面, 最后就是全都用十六进制的命名方式。 我们把部分函数运行可以得到固定的真值,以及一些调用过来调用过去的,去掉中间变量,基本可以还原这个代码的原貌。

例如这里面的大部分函数都用到了这一函数(我当时起名叫 getList ,其实 getString 更为合适。)

1
function getList() {
2
const _0x4af9ee = ['encrypt', '33MGcQht', '6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c',
3
'14021KbbewD', 'charCodeAt', '808heYYJt', '5DlyrGX', '552oZzIQH', 'fromCharCode', '356IjESGA',
4
'784713mdLTBv', '2529060PvKScd', '805548mjjthm', '844848vFCypf', '4bIkkcJ', '1356853149054377', 'length',
5
'slice', '1720848ZSQDkr'
6
];
7
getList = function() {
8
return _0x4af9ee;
9
};
10
return getList();
11
}

这里面有一些类型自带的方法,也有一些字符,总而言之用途还挺多,很多函数都通过调用他获取某个值进一步复杂化了整个代码。

我们先来看 gyflagh ,经过还原过来后,他大概长这样。

1
function gyflagh(_0x111955) {
2
if (_0x111955.encrypt(1356853149054377) === '6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c') return true;
3
return false;
4
}

我们可以看出他将输入值加密后和某个结果对比,如果一致就直接返回真了,那么后面就是我们需要解密的东西了。 那么 encrypt 方法是啥呢?

flag.js 在最上层将 String 的原型中写入了这一方法

1
String['prototype']['encrypt'] = function (encrypt_key) { //接受参数大概是加密 key 可以这样理解 反正是两个加密成一个
2
3
const
4
array_1 = new Array(0x2),
5
array_2 = new Array(0x4);
6
let _0x1bf548 = '';
7
plaintext = escape(this); //加密字符自身,转义
8
//console.log(plaintext)
9
for (var i = 0; i < 0x4; i++)
10
array_2[i] = Str4ToLong(encrypt_key.slice(i * 0x4, (i + 0x1) * 0x4));
11
12
for (i = 0x0; i < plaintext.length; i += 0x8) {
13
array_1[0x0] = Str4ToLong(plaintext['slice'](i, i + 0x4));
14
array_1[0x1] = Str4ToLong(plaintext.slice(i + 0x4, i + 0x8));
15
code(array_1, array_2);
16
_0x1bf548 += LongToBase16(array_1[0x0]) + LongToBase16(array_1[0x1]);
17
}
18
return _0x1bf548;
19
20
})

所以这就是一个以 1356853149054377 为 key 的啥啥加密咯。

加密结果 6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c

观察算法后其实可以搜到,这是 TEA 加密,但是我试了几个在线工具都不行。 当然 encrypt 都直接给我们了, 我们完全可以倒着把解密就写出来(啊这,我不行。)

但是我还是取巧了一点,我在 GitHub 上以里面部分函数比如:Str4ToLong 搜索了一下,发现这个 TEA 加密是有原始代码的,它来自 security.js,很多项目都有引用这, 且我经过对比之后,发现他们大部分代码都是一致的,除了 mc 这边多了 String 转十六进制的相关方法,这样我们简单缝合一下就能得到结果了。

缝合后的代码.js
50 collapsed lines
1
// use (16 chars of) 'password' to encrypt 'plaintext'
2
3
function encrypt(plaintext, password) {
4
var v = new Array(2), k = new Array(4), s = "", i
5
6
plaintext = escape(plaintext) // use escape() so only have single-byte chars to encode
7
8
// build key directly from 1st 16 chars of password
9
for (var i = 0; i < 4; i++)
10
k[i] = Str4ToLong(password.slice(i * 4, (i + 1) * 4))
11
12
for (i = 0; i < plaintext.length; i += 8) { // encode plaintext into s in 64-bit (8 char) blocks
13
v[0] = Str4ToLong(plaintext.slice(i, i + 4)) // ... note this is 'electronic codebook' mode
14
v[1] = Str4ToLong(plaintext.slice(i + 4, i + 8))
15
console.log(v)
16
code(v, k)
17
console.log(v)
18
console.log(LongToBase16(v[0]), LongToBase16(v[1]))
19
// console.log(plaintext.slice(i,i+4),plaintext.slice(i+4,i+8))
20
// console.log(LongToBase16(v[0]),LongToBase16(v[1]))
21
s += LongToBase16(v[0]) + LongToBase16(v[1])
22
//s += LongToStr4(v[0]) + LongToStr4(v[1]);
23
24
}
25
26
return escCtrlCh(s)
27
}
28
29
function LongToBase16(long) {
30
let base16 = ''
31
for (let i = 0x3; i >= 0x0; i--) {
32
let v = (long >> 0x8 * i & 0xff).toString(0x10)
33
if (parseInt('0x' + v) <= 0xf) v = '0' + v
34
base16 += v
35
}
36
return base16
37
}
38
39
function Base16ToLong(base16) {
40
let long = 0x0
41
for (let i = 0; i < 8; i += 2) {
42
let v = parseInt('0x' + (base16).slice(i, i + 2))
43
long = (long << 0x8) + v
44
}
45
return long
46
}
47
48
// use (16 chars of) 'password' to decrypt 'ciphertext' with xTEA
49
50
function decrypt(ciphertext, password) {
51
var v = new Array(2), k = new Array(4), s = "", i
52
53
for (var i = 0; i < 4; i++) k[i] = Str4ToLong(password.slice(i * 4, (i + 1) * 4))
54
55
ciphertext = unescCtrlCh(ciphertext)
56
for (i = 0; i < ciphertext.length; i += 16) { // decode ciphertext into s in 64-bit (8 char) blocks
57
v[0] = Base16ToLong(ciphertext.slice(i, i + 8))
58
v[1] = Base16ToLong(ciphertext.slice(i + 8, i + 16))
59
console.log(v)
60
decode(v, k)
61
console.log(v)
62
s += LongToStr4(v[0]) + LongToStr4(v[1])
63
}
64
//LongToBase16(Base16ToLong('d7a18d1f') - 17179869184)
65
//LongToBase16(Base16ToLong('d7a18d1f') +4294967296)
66
67
// strip trailing null chars resulting from filling 4-char blocks:
68
s = s.replace(/\0+$/, '')
69
70
return unescape(s)
71
}
72
56 collapsed lines
73
function code(v, k) {
74
// Extended TEA: this is the 1997 revised version of Needham & Wheeler's algorithm
75
// params: v[2] 64-bit value block; k[4] 128-bit key
76
var y = v[0], z = v[1]
77
var delta = 0x9E3779B9, limit = delta * 32, sum = 0
78
79
while (sum != limit) {
80
y += (z << 4 ^ z >>> 5) + z ^ sum + k[sum & 3]
81
sum += delta
82
z += (y << 4 ^ y >>> 5) + y ^ sum + k[sum >>> 11 & 3]
83
// note: unsigned right-shift '>>>' is used in place of original '>>', due to lack
84
// of 'unsigned' type declaration in JavaScript (thanks to Karsten Kraus for this)
85
}
86
v[0] = y; v[1] = z
87
}
88
89
function decode(v, k) {
90
var y = v[0], z = v[1]
91
var delta = 0x9E3779B9, sum = delta * 32
92
93
while (sum != 0) {
94
z -= (y << 4 ^ y >>> 5) + y ^ sum + k[sum >>> 11 & 3]
95
sum -= delta
96
y -= (z << 4 ^ z >>> 5) + z ^ sum + k[sum & 3]
97
}
98
v[0] = y; v[1] = z
99
}
100
101
// supporting functions
102
103
function Str4ToLong(s) { // convert 4 chars of s to a numeric long
104
var v = 0
105
for (var i = 0; i < 4; i++) v |= s.charCodeAt(i) << i * 8
106
return isNaN(v) ? 0 : v
107
}
108
109
function LongToStr4(v) { // convert a numeric long to 4 char string
110
var s = String.fromCharCode(v & 0xFF, v >> 8 & 0xFF, v >> 16 & 0xFF, v >> 24 & 0xFF)
111
return s
112
}
113
114
function escCtrlCh(str) { // escape control chars which might cause problems with encrypted texts
115
return str.replace(/[\0\t\n\v\f\r\xa0'"!]/g, function (c) { return '!' + c.charCodeAt(0) + '!' })
116
}
117
118
function unescCtrlCh(str) { // unescape potentially problematic nulls and control characters
119
return str.replace(/!\d\d?\d?!/g, function (c) { return String.fromCharCode(c.slice(1, -1)) })
120
}
121
122
// let enc = encrypt('123aaaa','123aaaa')
123
// console.log(enc)
124
// console.log(decrypt(enc,'123aaaa'))
125
126
cipher = '6fbde674819a59bfa12092565b4ca2a7a11dc670c678681daf4afb6704b82f0c'
127
pwd = '1356853149054377'
128
plaintext = decrypt(cipher, pwd)
129
console.log(plaintext)

参考资料