Hackergame 2021 writeups

第一次打CTF比赛

Summary

作为新手,第一次参加 CTF ,题目比 MSClub 的招新 puzzle 难得多,但是也好玩得多,前两天有幸短暂地在总榜停留过(或者只是第一天?忘了),最终组内排名第四(因为校内的大佬不在)( GZTime tql ,凌晨肝神冲到总榜第四orz,此处附上 wp 传送门)。比赛的题目出得都非常得有趣 (除了透明的文件),对新手来说体验确实不错,也确实是名副其实的 pythongame(零基础强行入门),做到灯那题的时候我面对线性非齐次方程组的无穷多解毫无头绪,这彻底燃起了我学线代的激情(嗯,有意思),最后我交的第一个错误的 flag 上了花絮也有小小的惊喜(没错我确实是有备而来 hiahiahia)。总体体验良好,明年再来。下面是我做出的几题的 wp,内容较水,可酌情移步至神(mcfx)的 wp


签到

为了能让大家顺利签到,命题组把每一秒的 flag 都记录下来制成了日记本的一页。你只需要打开日记,翻到 Hackergame 2021 比赛进行期间的任何一页就能得到 flag!

很简单的 web 题,注意到网址栏的后面的?page=<当面的数字>,直接修改,疯狂加 0,然后二分法试探,最终在 ?page=1635000000 的时候到达当前时间,出 flag (后来才知道这是时间戳)。

进制十六——参上

为严防 flag 泄漏以及其他存在于未来所有可能的意外灾难,神通广大的 Z 同学不仅强制要求每一道题目都加上权限和资源的限制,还给所有参与 Hackergame 2021 命题的计算机施加了一层法术结界。任何试图从结界逃逸的 flag 都会被无情抹除。

而一位明面上是计算机学院的新生,实则为物理学院暗部核心成员的 X 同学,在 Hackergame 2021 命题组已经潜伏多时。妄想趁比赛开始的午时,借阳火正旺之势,冲破 Z 同学的结界,以图片而非明文的形式,将 flag 悄悄传递出来。

好在 Z 同学法力之深厚,不可管窥蠡测。在 flag 被传出去的前两天,就已预知此事并将图片中的 flag 无声消泯了。

只是,这位 X 同学,虽然不会退出 Vim,但是似乎对打开十六进制编辑器颇有造诣……

Hex

同样是很简单的题目,随便找个十六进制编辑器照着抄下然后得出右边的 flag (手打真的累,还打错了一个字母)。

去吧!追寻自由的电波

(前情提要) 为了打破 Z 同学布下的结界,X 同学偷偷搬出社团的业余无线电台试图向外界通讯。

当然,如果只是这样还远远不够。遵依史称“老爹”的上古先贤的至理名言,必须要“用魔法打败魔法”。X 同学向上级申请到了科大西区同步辐射实验室设备的使用权限,以此打通次元空间,借助到另一个平行宇宙中 Z 同学的法力进行数据对冲,方才于乱中搏得一丝机会,将 flag 用无线电的形式发射了出去。

考虑到信息的鲁棒性,X 同学使用了无线电中惯用的方法来区分字符串中读音相近的字母。即使如此,打破次元的强大能量扭曲了时空,使得最终接受到的录音的速度有所改变。

为了保障同步辐射设备的持续运转,组织牺牲了大量的能源,甚至以东北部分地区无计划限电为代价,把这份沉甸甸的录音文件送到了你的手上。而刚刚起床没多久,试图抢签到题一血还失败了的你,可以不辜负同学们对你的殷切期望吗?

注:flag 花括号内只包含小写字母。

题目说了速度变了,显然是变快了,直接用经典音频隐写软件 Audacity 打开, Ctrl + A 全选,效果-改变速率,然后随手一拖把改变百分比拖到 70 就有非常标准清晰的单词,不看题目都能盲猜是首字母,后来查电报区分字符串中读音相近的字母的方法也确实对应,但是我还是不想自己听,打开翻译软件,嗯,语音识别不错,除了两个花括号我一开始没反应过来把也当成字母了QAQ。

猫咪问答 Pro Max

我猛然一看,就猛然看到这个猫咪问答,我直呼我直呼,上次看到这么这么的发言还是上次,这问答属于是典型的典型了,我之前还没发现,当我发现的时候我已经发现了,这问答就像一个问答,问答的内容充满了内容,我不禁感慨了一句感慨:希望下次看到这么这么的猫咪问答是下次。

题目的废话文学着实是让我涨了见识,这简直简直了,我以前从未想过有如此废话的废话文学,等我看见的时候已经看见了……第一问也是最难的一问,把百度必应谷歌翻烂了也没有,后来终于在一个专门存快照的国外网站上找到了那个已经消失的俱乐部的网页存档,第三问直接在中科大 Linux 用户协会官网找到,第五问也不难搜(虽然我没想到举报信可以发到这么奇怪的…呃不知怎么形容,目录??去),第四问我看到论文直接望而生畏,搜索 data set 无果后果断爆破,合理猜测在 100 以下,但发现居然不行???万分怀疑的我试探性地把第二问改成 3 (或许是我对近五年的理解有问题?),也不行,改成 5 ,行了 (这协会真不要 face ),脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests as r
import re
from tqdm import tqdm

def main():
url = 'http://202.38.93.111:10001/'
payload = {'q1': 20150504, 'q2': 5,
'q3': 'Development Team of Library', 'q4': 0, 'q5': '/dev/null'}
headers = {'Cookie': 'session=eyJ0b2tlbiI6IjQ2Ok1FWUNJUUNyY2xUYU1XcG9sUzZqZzM0Z2IyZnMyeEJBNGtMa01DYnpKV1dERGNKWUJnSWhBT3RQTzBXSnhLMnRmcytWcVRReWd5eHdKMFY2dEZ6bGlycXVNSURHbU9EaSJ9.YXYWYA.2MXfMT2si0FkY-Z9jOc7V4jenTU'}
for i in tqdm(range(100)):
payload['q4'] = i
_ = r.post(url=url, data=payload, headers=headers).text
try:
rst = re.findall(
r'<div class="alert alert-secondary" role="alert">((?:.|\n)*?)</div>', _)[0]
except IndexError:
s=re.findall(r'(flag{.*?})', _)[0]
print ('\n'+s)
print('q4答案是'+str(i))
break

if __name__ == '__main__':
main()

卖瓜

有一个人前来买瓜。

HQ:哥们,这瓜多少钱一斤啊?

你:两块钱一斤。

HQ:What’s up!这瓜皮子是金子做的还是瓜粒子是金子做的?

你:你瞧瞧现在哪有瓜啊?这都是大棚的瓜,只有 6 斤一个和 9 斤一个的,你嫌贵我还嫌贵呢。

(HQ 心里默默一算)

HQ:给我来 20 斤的瓜。

你:行!

HQ:行?这瓜能称出 20 斤吗?

你:我开水果摊的,还不会称重?

HQ:我问你这瓜能称出 20 斤吗?

你:你是故意找茬,是不是?你要不要吧!

HQ:你这瓜要是刚好 20 斤吗我肯定要啊。那它要是没有怎么办啊?

你:要是不是 20 斤,我自己吃了它,满意了吧?

(你开始选瓜称重)

在看过上一年的题目后可以直接看出是溢出,但是输入的数字过大后出现了浮点数,于是我上网搜了数据类型,随便复制了一个数( 2 的多少次方来着??忘了)过来输入到 9 斤那,出现了一个大负数,然后我掏出大宝贝—— Windows 计算器除以 6 ,结果去掉小数后再输入到 6 斤那,此时称上有了 -2 斤,嗯,可行,再来一次, -4 了,直接再放 4 个 6 斤,逃过一劫,得到 flag 。

FLAG 助力大红包

“听说没?【大砍刀】平台又双叒做活动啦!参与活动就送 0.5 个 flag 呢,攒满 1 个 flag 即可免费提取!”

“还有这么好的事情?我也要参加!”

“快点吧!我已经拿到 flag 了呢!再不参加 flag 就要发完了呢。”

“那怎么才能参加呢?”

“这还不简单!点击下面的链接就行”

这应该是让我学到最多东西的一题,看到题目的说明我第一反应就是通过 http 请求头伪造 IP 地址,搜到可以通过 X-Forwarded-For 实现,但中途我试图通过审查元素修改表单数据实现,结果提示前后端地址不匹配(后来才发现我没读题),于是找了个伪造 IP 地址的插件,这次是后端改了前端还是原来的,两个结合,发现可以成功砍到,手改几次后发现速度来不及,被迫找到 Python 的实现,一开始没加时间限制发现有令牌桶防着,就加了个停顿,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
import requests
import time
from tqdm import tqdm
url = 'http://202.38.93.111:10888/invite/77632ca2-c165-46ee-a8d2-ba89094b0401'
ua = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:93.0) Gecko/20100101 Firefox/93.0'
for i in tqdm(range(256)):
s = (str(i)+'.104.67.89')
params = {"ip": s,}
request = requests.post(url, data=params, headers={'User-agent': ua, 'X-Forwarded-For':s})
time.sleep(0.9)
print(request.text)

Amnesia第一问

你的程序只需要输出字符串 Hello, world!(结尾有无换行均可)并正常结束。

编译指令:gcc -O file.c -m32

运行指令:./a.out

编译器版本:Docker 镜像 ustclug/debian:10 中 apt update && apt -y upgrade && apt install -y gcc=4:8.3.0-1 gcc-multilib=4:8.3.0-1 的版本

轻度失忆

编译后 ELF 文件的 .data 和 .rodata 段会被清零。

连接题目:nc 202.38.93.111 10051 或网页终端

判题脚本:下载

第一问不难,发现直接 printf("Hello,world!") 失败后改成用 printf("%c",<每个字符的ASCII码>),但提交后错误,仔细检查发现多了几个奇怪的不可显示字符,应该是直接从网页复制的问题,删掉后提交得到flag,比赛结束后才知道还可以 putchar 呃呃

图之上的信息

小 T 听说 GraphQL 是一种特别的 API 设计模式,也是 RESTful API 的有力竞争者,所以他写了个小网站来实验这项技术。

你能通过这个全新的接口,获取到没有公开出来的管理员的邮箱地址吗?

第一反应是 SQL 注入,但零基础的我忙活了半天才知道 /graphql 是端点,查看 http 请求看到负载语句 {query: "{ notes(userId: 2) { id↵contents }}"} 猜测 admin 的 id 是 1 ,于是疯狂查询 1 的 contents ,结果回显都是我没有权限,后来搜到了 Graphql 的 IDE ,找到了存储邮箱的地方,查询得到 flag 。

Easy RSA

自从 Hackergame 2018 公然揭露了大整数可以被神童口算分解的事实,RSA 在 hackergame 中已经只能处于低分值的地位了。如果不在其名称前面加上 Easy 这个单词,似乎就会显得完全对不起其他题目。

更何况,在本题的附件中,你还获得了构造 p 和 q 的方式。数理基础扎实的你应该可以轻松解决这些问题吧。

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
import math
import sympy
from Crypto.Util.number import *

e = 65537


def get_p():
x = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391760451
y = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391661439
value_p = sympy.nextprime((math.factorial(y)) % x) # Hint:这里直接计算会溢出,请你仔细观察 x 和 y 的特征
return value_p


def get_q():
value = [getPrime(256)]
for i in range(1, 10):
value.append(sympy.nextprime(value[i - 1]))
print("value[-1] = ", value[-1])
# value[-1] = 80096058210213458444437404275177554701604739094679033012396452382975889905967
n = 1
for i in range(10):
n = n * value[i]
q = getPrime(512)
value_q = pow(q, e, n)
print("value_q = ", value_q)
# value_q = 5591130088089053683141520294620171646179623062803708281023766040254675625012293743465254007970358536660934858789388093688621793201658889399155357407224541324547522479617669812322262372851929223461622559971534394847970366311206823328200747893961649255426063204482192349202005330622561575868946656570678176047822163692259375233925446556338917358118222905050574458037965803154233167594946713038301249145097770337253930655681648299249481985768272321820718607757023350742647019762122572886601905212830744868048802864679734428398229280780215896045509020793530842541217790352661324630048261329493088812057300480085895399922301827190211956061083460036781018660201163819104150988531352228650991733072010425499238731811243310625701946882701082178190402011133439065106720309788819
return sympy.nextprime(q)

# this destroyes the rsa cryptosystem
p = get_p()
q = get_q()

m = int.from_bytes(open("flag.txt", "rb").read(), "big")
c = pow(m, e, p * q)
print("c = ", c)
# c = 110644875422336073350488613774418819991169603750711465190260581119043921549811353108399064284589038384540018965816137286856268590507418636799746759551009749004176545414118128330198437101472882906564195341277423007542422286760940374859966152871273887950174522820162832774361714668826122465471705166574184367478

数理杀我的一题, acm 的题中有过类似的,威尔逊定理求出 $p$ , $q$ 的话不难求,很好,现在是经典 RSA 了,什么,你问我到底怎么求的 p?哦?我还等你讲给我听呢(装傻,阿巴阿巴)。
首先科普一下模逆元的概念,比如 $x\equiv 3^{-1}\mod 5$ ,这个并非是 $x=\frac{1}{3}$ 的意思,而是 $x$ 满足 $3x\equiv 1 \mod 5$ ,这时口算得出 $x=2$ ,因为 $6\equiv 1 \mod5$ 嘛。
然后是威尔逊定理,即 $(p-1)!\equiv-1 \pmod p$ 是 $p$ 为素数的充要条件。
题目给出大数 $x$ 和比 $x$ 小一点的 $y$ ,要我们求 $y! \mod x$ ,检验发现 $x$ 是一个素数,那么就有 $(x-1)!\equiv-1 \pmod x$ 。
继而由 $(x-1)!=y(y+1)(y+2)\cdots(x-1)$ ,不难推知
$$
y!\equiv -1(x-1)^{-1}(x-2)^{-1}\cdots (y+1)^{-1} \mod\ x
$$
python 计算逆元有两种方法,一种是 pow(a, -1, b),另一种是用 gmpy2 库,即 gmpy2.invert(a,b)

完整 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
import sympy
from Crypto.Util.number import *
from gmpy2 import invert

e = 65537


def get_p():
x = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391760451
y = 11124440021748127159092076861405454814981575144744508857178576572929321435002942998531420985771090167262256877805902135304112271641074498386662361391661439
facymodx = -1
for i in range(y+1,x):
facymodx = facymodx*invert(i,x)%x
value_p = sympy.nextprime(facymodx)
return value_p


def get_q():
value_inverse = [
80096058210213458444437404275177554701604739094679033012396452382975889905967]
for i in range(1, 10):
value_inverse.append(sympy.prevprime(value_inverse[i-1]))

n = 1
phi = 1

for i in range(10):
n = n * value_inverse[i]
phi = phi * (value_inverse[i]-1)

value_q = 5591130088089053683141520294620171646179623062803708281023766040254675625012293743465254007970358536660934858789388093688621793201658889399155357407224541324547522479617669812322262372851929223461622559971534394847970366311206823328200747893961649255426063204482192349202005330622561575868946656570678176047822163692259375233925446556338917358118222905050574458037965803154233167594946713038301249145097770337253930655681648299249481985768272321820718607757023350742647019762122572886601905212830744868048802864679734428398229280780215896045509020793530842541217790352661324630048261329493088812057300480085895399922301827190211956061083460036781018660201163819104150988531352228650991733072010425499238731811243310625701946882701082178190402011133439065106720309788819
d = invert(e, phi)
q = pow(value_q, d, n)
return sympy.nextprime(q)


c = 110644875422336073350488613774418819991169603750711465190260581119043921549811353108399064284589038384540018965816137286856268590507418636799746759551009749004176545414118128330198437101472882906564195341277423007542422286760940374859966152871273887950174522820162832774361714668826122465471705166574184367478

if __name__ == '__main__':
p = get_p()
q = get_q()
# print(len(str(q)))
print(p)
print(q)
n = p * q
phi = (p-1) * (q-1)

d = invert(e, phi)

m = pow(c, d, n)

print(long_to_bytes(m))
作者

未央

发布于

2021-11-05

更新于

2023-11-21

许可协议

评论