比赛情况

  • 赛时 ID:呜呜呜 #MUSTC科大音游小栈 群号 44834360
  • 校内排名:19
  • 总排名:91
  • 总分:2400
  • Binary:300
  • General:1000
  • Math:200
  • Web:900

杂谈

其实我对 Hackergame 这个比赛早有耳闻,似乎是在网上冲浪的时候偶然翻到的吧。
至于这个 “早有耳闻” 的 “早” 具体有多早嘛……我已经忘了,反正肯定是进校之前的事情。
别人的情况我不太清楚,反正我对这次比赛还是挺上心的。
本来打算赛前多看看我那本 CSAPP 补补课的,无奈开学之后手头事情实在不少,看书这事也就直接被我丢到爪哇国去了。

虽然赛时,我的生活习惯不允许我像某些 dalao 一样熬夜甚至通宵(听说甚至还有连续熬夜的……Orz),但在这一个星期之内,我基本也处于半脱产状态。
课是没怎么听,甚至在星期四的线代课上掏出笔记本看题,作业也基本只能维持在刚好完成的水平线上。

不过最后的排名貌似还行?大概是总榜 91,校内 19 的排名,说高也不高说低也不算低,但也基本上能代表我目前的最高水平了。
我在本次比赛中也学到了不少东西,比如对网站的架构有了更深刻的理解,以及对诸如 LUKS 这样的小玩意有了一些了解。
同时也认识到自己的很多不足。比如 Python 都不会用
赛后翻翻其他 dalao 的题解,也有比较亮眼、值得借鉴的思路和实用的工具。
娘的,在我高中的那个四线城市,我可没这个机会(和时间)研究这些东西啊。

在此感谢 Hackergame 的全体工作人员,为我们年轻人献上了一场高质量的比赛。
卷子质量很好,老师服务态度很好,明年还来(大概)。

姑且把赛后写的 WP 搬到这边来吧,顺便修修 BUG,也整理一下措辞,作为归档。

没什么参考价值而且还在吐槽的 WP 部分

签到

进入网页,观察到传递的参数是 UNIX 时间戳,随便算算就行。

进制十六——参上

网页识图把十六进制字符搞出来,扔进 010 Editor 直接看就行。

去吧!追寻自由的电波!

用 Audacity 降速会顺带改变它的音调,这正是我们想要的结果。

猫咪问答 Pro Max

T1:Wayback Machine
T3:USTC LUG 官网,活动室搬迁的文章
T2、T4、T5:Google

卖瓜

利用整型溢出,把秤上的重量调到 “与 20 的距离恰好被 3 整除的负数”,然后再往上加即可。

核心要义是通过溢出,改变 “秤上的重量” 的剩余系。

透明的文件

正好之前配置 Linux 的 PS1 的时候搞过一点点,对着 ANSI Escape code 的格式改就行。

旅行照片

蓝色的 KFC 是突破口。

百度搜索 “海边的 KFC” 就能够获得店铺信息了,从而解出 T4 和 T5。
T1、T2 是常识,T3 我没法确定,只能爆破。
T3 的官方题解是利用了水平面,呜呜呜我不就是没有常识吗呜呜呜

FLAG 助力大红包

前后端识别指的是:

  • 前端调用搜狐 API 获取本机 IP 地址,并在点击链接的时候通过传参数的方式传递给后端
  • 后端通过某些玄学方法(草我也不懂,可能跟包的传输有关)获取源 IP 地址
  • 前后端获得的 IP 地址进行比较,判断是否助力成功

我的解决方案是用 Python 发送 POST 请求,只需要:

  • 在请求头加一个 XFF 头,伪造一个 IP 地址丢进去
  • 传参的时候把 IP 地址改成和 XFF 头一样的 IP 地址

按照以上步骤,即可从任意 IP 地址完成助力。

注意到活动规则中提到:

每个用户只能够助力一次。为了建设世界一流大砍刀平台,活动要求位于同一 /8 网段的用户将会被视为同一个用户。(比如 IP 地址为 202.38.64.1 和 202.39.64.1 将被视为同一用户。)达到助力次数上限后,将无法再帮助好友助力。

那就把不同 /8 网段的 IP 地址全跑一遍好了,共计 256 个 IP 地址,跑完才能拿到 flag。
其他题解有用 Shell Script + curl 的组合的,可惜我才疏学浅不懂 Shell Script。

附上代码(Python):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import requests
import time

header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0',
'X-Forwarded-For': '',
'Origin': 'http://202.38.93.111:10888',
'DNT': '1',
'Connection': 'close',
'Referer': 'http://202.38.93.111:10888/invite/ba9ebe2b-351d-4c65-8391-db325c13d17b',
'Cookie': 'eyJ0b2tlbiI6IjEyNjc6TUVVQ0lRQy9mMnFNTUo0Q1dDOXg0N1YvYzdoazAvNDFoQVduSVJvcWNUK0JhUTNUM3dJZ0xONXU1VFRzUHdwQlVKdnBab2NFWFg0UzAySnQ4d09DYy92c3lmUWl3T2c9In0:1me8d6:itN3T7cxY5fL7Vdr6-aaFZmLjW65C_G3N1_k-G6saTU; csrftoken=kGaFCfhHsJPCfSZh2VbRRFyeuqz2b0JNSb9JBkqmZT3raIG7npoMUXVsPxtZIhf7; PHPSESSID=0dc4fb11c21c083de000de7053b20372; session=eyJ0b2tlbiI6IjEyNjc6TUVVQ0lRQy9mMnFNTUo0Q1dDOXg0N1YvYzdoazAvNDFoQVduSVJvcWNUK0JhUTNUM3dJZ0xONXU1VFRzUHdwQlVKdnBab2NFWFg0UzAySnQ4d09DYy92c3lmUWl3T2c9In0.YXPlRA.Dg66QXWVfHaUhTOAzWOYhLUXqkw'
}

urls = 'http://202.38.93.111:10888/invite/ba9ebe2b-351d-4c65-8391-db325c13d17b'

for i in range(0,256):
header['X-Forwarded-For'] = str(i) + '.222.233.233'
datas = {"ip": str(i)+'.222.233.233'}
res = requests.post(headers=header, url=urls, data=datas)
time.sleep(1)
print(res.text)

Amnesia

轻度失忆

懒得查资料了,盲猜要使得程序内不出现字符串常量。
我定义了一个字符变量,对它的值不断进行修改,并使用 putchar 输出,就过掉了。

图之上的信息

Google 一下 GraphQL Hack 就能知道如何利用 GraphQL 提供的 API 获得所有能够查询的参数。
找找看怎么查询 private email,传参查询即可。

Easy RSA

显然,这是一道数论题。
求 P 要用到 Wilson 定理的推论和乘法逆元。
求 Q 和最终的 data 要用到离散对数(或者说是欧拉函数的性质?)和乘法逆元。
我个人写这种脚本的习惯是不同功能分开写,这样便于我理清思路,所以文件数量比较多,代码又臭又长,甚至还有重复。

求 P 的代码(Python):

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
# -*- coding: UTF-8 –*- 
from Crypto.Util.number import isPrime

def EX_GCD(a,b,arr): #扩展欧几里得
if b == 0:
arr[0] = 1
arr[1] = 0
return a
g = EX_GCD(b, a % b, arr)
t = arr[0]
arr[0] = arr[1]
arr[1] = t - int(a / b) * arr[1]
return g

def ModReverse(a,n): #ax=1(mod n) 求a模n的乘法逆x
arr = [0,1,]
gcd = EX_GCD(a,n,arr)
if gcd == 1:
return (arr[0] % n + n) % n
else:
return -1

# a 大一点
a = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391760451
b = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391661439

niubi = - 1

for i in range(b + 1, a):
temp = ModReverse(i, a)
niubi = (niubi * temp) % a

if niubi < 0:
niubi += a

for i in range(niubi + 1, niubi + 100000):
if isPrime(i):
print(i)
break

求 Q 的代码(Python):

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
# -*- coding: UTF-8 –*- 
from Crypto.Util.number import isPrime

prime_9 = 80096058210213458444437404275177554701604739094679033012396452382975889905967
value_q = 5591130088089053683141520294620171646179623062803708281023766040254675625012293743465254007970358536660934858789388093688621793201658889399155357407224541324547522479617669812322262372851929223461622559971534394847970366311206823328200747893961649255426063204482192349202005330622561575868946656570678176047822163692259375233925446556338917358118222905050574458037965803154233167594946713038301249145097770337253930655681648299249481985768272321820718607757023350742647019762122572886601905212830744868048802864679734428398229280780215896045509020793530842541217790352661324630048261329493088812057300480085895399922301827190211956061083460036781018660201163819104150988531352228650991733072010425499238731811243310625701946882701082178190402011133439065106720309788819
primes = [prime_9]

def EX_GCD(a,b,arr): #扩展欧几里得
if b == 0:
arr[0] = 1
arr[1] = 0
return a
g = EX_GCD(b, a % b, arr)
t = arr[0]
arr[0] = arr[1]
arr[1] = t - int(a / b) * arr[1]
return g

def ModReverse(a,n): #ax=1(mod n) 求a模n的乘法逆x
arr = [0,1,]
gcd = EX_GCD(a,n,arr)
if gcd == 1:
return (arr[0] % n + n) % n
else:
return -1

num = 1
for i in range(prime_9 - 1, prime_9 - 100000, -1):
if isPrime(i):
primes.append(i)
num += 1

if num == 10:
break

n = 1
nn = 1
primes = primes[::-1]
for i in primes:
n *= i
nn *= (i - 1)

expp = ModReverse(65537, nn)
q = pow(value_q, expp, n)

print(q)

for i in range(q + 1, q + 100000):
if isPrime(i):
print(i)
break

求 flag 的代码(Python):

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
# -*- coding: UTF-8 –*- 

# 扩展欧几里得求逆元
def exgcd(a, b):
if b == 0:
return 1, 0, a
else:
x, y, q = exgcd(b, a % b)
x, y = y, (x - (a // b) * y)
return x, y, q

# 扩展欧几里得求逆元
def ModReverse(a,p):
x, y, q = exgcd(a,p)
if q != 1:
raise Exception("No solution.")
else:
return (x + p) % p #防止负数


p = 10569944080090591401315432556965818857327680380269154543273468441025963038065648915158194147019839932524599260058098616377893091051396090650574162446875263
q = 10477925992460766451892208516181598312750484426056814542870756188277177949099084361476539803367804757559880919838828678145609717295215924948786830953571811
c = 110644875422336073350488613774418819991169603750711465190260581119043921549811353108399064284589038384540018965816137286856268590507418636799746759551009749004176545414118128330198437101472882906564195341277423007542422286760940374859966152871273887950174522820162832774361714668826122465471705166574184367478
test = 3338241147601905675961845375760830741082637481762357299890556168228051452057836433529928514826

wori = (p - 1) * (q - 1)
expp = ModReverse(65537, wori)
print((expp * 65537) % wori)

m = pow(c, expp, p * q)
print(m)
gg = m.to_bytes(length=200, byteorder="big")
print(gg)

加密的 U 盘

搜索可知,LUKS 的加密密钥是独立于 Password 的,并且所有的加密信息全都在 LUKS 头里。这就意味着,在能够获得修改密码前的 LUKS 头的情况下,小 T 修改密码的操作并没有什么吊用。

直接把 day1.img 的 LUKS 头换给 day2.img 就可以用原来的密码打开文件了。

什么接头霸王

反而是如何正常使用 day1.img 费了我绝大多数的时间,毕竟我对 Linux 不太熟悉。

我 Google 了很长时间才找到了解决方案:https://unix.stackexchange.com/questions/504230/mount-encrypted-partition-of-an-image-file

minecRaft

这题跟 Minecraft 有什么关系吗(

F12 翻 JS 代码可以发现,对于输入的字符串:

  • 第一个字母如果是大写的 M,可以点亮前两盏灯。
  • 如果其长度大于等于 32,并且通过了 gyflash 函数的测试,就可以点亮第三盏灯。

滚去翻 gyflash 函数,这令人不忍直视的函数名和变量名,显然是经过了混淆。
虽然很明显,这些名字都是十六进制数,但是直接翻成 ASCII 貌似不太行。

这里我就直接连蒙带猜外加分析了,最终猜测 gyflash 的大致流程为:

  1. 输入字符串
  2. 8 个字符切片,分成两组 4 个字符,转成 Long
  3. 对两组 4 个字符同时进行 code 函数的处理
  4. 将 code 结果转成两组 8 位 base16 加入进去
    上述步骤完全可逆。(code 函数得好好看看,我当时不知道这种加密方式叫做 TEA)

为了确保不出岔子,我在原 JS 代码的基础上修改了一下,在本地跑出了明文。
因为在同样的 JS 代码上修改了多次,我无法给出解密的代码了。

超 OI 的 WriteUp 模拟器

果然还是逆向比较简单(真的逆向了)

年轻人的第一次逆向
年轻人无法理解官方题解在做什么

下载文件,用 IDA 打开,直接看 main 函数,汇编不太熟,所以 F5 看伪代码。

总之就是输入一个字符串,并且如果:

  • 长度等于 17(实际上不计算末尾的话,输入的字符串长度应该是 16)
  • 通过 sub_1160 函数的检验
    则这个字符串就是正确的 code。

滚去翻 sub_1160 函数,WDNMD,这熟悉的味道,显然这玩意也被混淆了。

大胆猜测这个函数的执行顺序与输入无关,拖到 C 语言的 IDE 里面,跑一遍单步调试,结果喜人,函数可以简化如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 sub_1160(__int64 a1, __int64 a2, unsigned __int64 a3, unsigned __int64 a4)
{
unsigned __int64 v4; // rbp

for (int i = 1; i <= 12; i++){
v4 = a3 ^ ((0xBD7C314368E2199FLL * (a4 | 0xEB0B677FB0D42156LL)) | 1);
a3 = a4;
a4 = v4;
printf("%lld %lld\n", a3, a4);
}

a4 = ((0xCAAD5AB28237193DLL * a3) ^ 0x9E67E274EFE555F9LL | a3 & 0xCAAD5AB28237193DLL ^ 0x408910200231012DLL | (0xA00AB088D60E4BA1LL * a4) ^ 0xFC339CFD2A635DA7LL | a4 & 0xA00AB088D60E4BA1LL ^ 0xA900846004001LL) != 0;
printf("%lld %lld\n", a3, a4);
return a4;
}

现在,只需要让返回的 a4 等于 0 就可以了。

通过分析 main 函数中,调用这个函数的语句,我们可以发现,这个函数的形参 a3 和 a4 分别代表了:输入的字符串的二进制表示,从中间劈开均分成两半,转成 64 位整型数字的结果。

因此,为了获得正确的 code,我们必须解出这样的 a3 和 a4,使得函数的返回值等于 0。
为此,我们需要倒过来查看这个 sub_1160 函数。

第一步,我们显然要解出这样一个方程组:

  • a3 * 0xCAAD5AB28237193DLL == 0x9E67E274EFE555F9LL
  • a3 & 0xCAAD5AB28237193DLL == 0x408910200231012DLL
  • a4 * 0xA00AB088D60E4BA1LL == 0xFC339CFD2A635DA7LL
  • a4 & 0xA00AB088D60E4BA1LL == 0xA900846004001LL

以上运算均在 64 位整型域内完成。
很显然,我们可以通过两次爆破解出 a3 和 a4。

第一次爆破的代码(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
#include <stdio.h>
#include <stdint.h>
char wokao[105] = "1010000000001010101100001000100011010110000011100100101110100001";
unsigned __int64 origin = 0xA00AB088D60E4BA1LL;
// andresult:0x000A900846004001
char wori[105] = "0000000000001010100100000000100001000110000000000100000000000001";
// multiplyresult
unsigned __int64 fuckyou = 0xFC339CFD2A635DA7LL;
int sign[105] = {0};

void calculate(int layer, unsigned __int64 nima){
if (layer == 64){
if (nima * origin == fuckyou){
printf("%llu\n", nima);
}
return;
}
unsigned __int64 temp = 1LL;
temp <<= layer;

if (wokao[63 - layer] == '0' && wori[63 - layer] == '0'){
calculate(layer + 1, nima + temp);
calculate(layer + 1, nima);
}

else if (wokao[63 - layer] == '1' && wori[63 - layer] == '1'){
calculate(layer + 1, nima + temp);
}

else if (wokao[63 - layer] == '1' && wori[63 - layer] == '0'){
calculate(layer + 1, nima);
}
}

int main(void){
int cnt = 0;

calculate(0, 0LL);
return 0;
}

第二次爆破的代码和第一次基本一致,这里就不放了。
于是,我们就求出了在 sub_1160 函数中,被那段循环语句处理之后的 a3 和 a4。

第二步,对上述得到的 a3 和 a4 再跑一遍那段循环语句的逆过程,代码如下(C 语言):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>

int main(void){
unsigned __int64 a3, a4, v4;
a3 = 6979190900151903085;
a4 = 5935342296973304903;

for (int i = 1; i <= 12; i++){
// v4 = a3
v4 = a4 ^ ((0xBD7C314368E2199FLL * (a3 | 0xEB0B677FB0D42156LL)) | 1);
a4 = a3;
a3 = v4;

printf("%llu %llu\n", a4, a3);
}

return 0;
}

第三步,把 a3 和 a4 转换成对应的输入的字符串,这个字符串就是正确的 code。

2022/4/25:不需要 C 语言爆破,直接用 z3 解上面的方程组即可。

To Do 部分

灯,等灯等灯(Level 0)

其实题目已经写完了,但是还在咕咕咕……

赛博厨房(Level 2)

马赛克

p😭q