Hackergame2021 部分WriteUp

views
Word count: 3.5k (~17 mins to read) Last updated:

WriteUp by YYY


一点碎碎念

这是我的第一次Hackergame/CTF比赛,感觉很有意思。前几天心态有点崩,好在调整过来了。大家还是要把Hackergame当做game。最开心的是认识了好多大佬,抱大腿.jpg

emoji

对自己的吐槽:EasyRSA差点做出来,扩展欧几里得写错弃疗了;RAID0卡在软件没有激活码不让保存;和各种小测撞;各种体调不良,饮食睡眠不佳;晚上学校断电没法做,我又是大夜猫子……奇奇怪怪挡路的东西一直不少,归根结底还是太菜了啦。
总之非常感谢@TonyCrane大佬、GodSpeed大佬及群内各成员的鼓励支持帮助(膜不算)。
希望我能早日学会binary和math。


签到

这题二分一下page参数即可。
鼠标连点器


进制十六——参上

使用Hex Editor Neo直接新建文件抄写即可。当然OCR识别也行吧。


去吧!追寻自由的电波

下载音频后可以听出是一段人声,但是语速极快。于是使用Adobe Audition CC打开,效果->伸缩与变调放慢速度,适当调节音高就能开始听写了。可以发现这是NATO Phonetic Alphabet

A ALPHA
B BRAVO
C CHARLIE
D DELTA
E ECHO
F FOXTROT
G GOLF
H HOTEL
I INDIA
J JULIET
K KILO
L LIMA
M MIKE
N NOVEMBER
O OSCAR
P PAPA
Q QUEBEC
R ROMEO
S SIERRA
T TANGO
U UNIFORM
V VICTOR
W WHISKEY
X X-RAY
Y YANKEE
Z ZULU


猫咪问答 Pro Max

General× 杂技√

  1. 经典WebArchive 第一行就有答案:20150504

  2. https://lug.ustc.edu.cn/wiki/intro/

    此处资料显示是4次,但并非最新,我后来手动尝试才得知是5

  3. FTP服务器 Obviously,答案是Development Team of Library 后来得知网站首页news头图就有,我还找了好久

  4. 谷歌关键词SIGBOVIK2021 Newcomb-Benford直接就能找到原论文http://sigbovik.org/2021/proceedings.pdf 直接Ctrl+F找到文章,发现有14个Figures,排除第一个在Background里的Figure后得到答案为13

  5. 谷歌关键词protocol report 找到https://www.rfc-editor.org/rfc/rfc8962.html

6.Reporting Offenses
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.

还挺幽默,一开始还真以为有police在listening


卖瓜

20不是3的倍数,乍看似乎不可能用6和9加和得到。但随便试了试,发现9斤的瓜很多很多时会溢出为-9223372036854775808,据此判断为int64溢出,判断应当在此处加以利用。
我们需要让这个数字稍微溢出一点,不能溢出太多。也就是略大于floor(9223372036854775808/9),使得溢出为-9223372036854775808以外的数字,并且让该数字到20的距离能被3整除。之后直接加回20就行(简单小学(?)算数,屡有即将做出来时加过头超过20的惨剧hhh)
灵感来源:CTF 两道web整数溢出题目(猫咪银行和ltshop)


透明的文件

根据题面和文件会发现这是终端的颜色代码,网络搜索终端颜色代码格式后将所有[前补上一个\e,然后echo -e "{内容}"就行啦。记得执行前把终端先用字符填满,不然可能画不完整。
chars


旅行照片

简简单单开个盒
旅游,海滩,汉字,说明这是国内一个海边旅游景点。
蓝色KFC?这可不常见,应该有不少人打卡了吧。
百度搜索蓝色KFC,第一项就是某红书的秦皇岛蒂凡尼蓝秦皇岛网红打卡|国内唯一蒂芙尼蓝色肯德基
百度地图定位发现这家店是某海底世界分店,直接得到电话号码。
https://www.earthol.org/上通过街景发现三个汉字“海|豚|馆”
building

对照地图,视线和海岸线大约成45°,推测应当在东南方向。进而发现影子西斜,应当在下午/傍晚
通过绘制各个水平线找出灭点可以推测楼层(知乎有些文章有详细说明)。我的方法是找到对面楼“最矩形”变形最少的窗户判断为同一楼层,然后数,发现是13层左右(经尝试发现是14层)


FLAG助力大红包

既然是和ip有关,第一时间想到的就是X-Forwarded-For,果然成功了。因为每个/8ip段(也就是例如255.0.0.0-255.255.255.255)都只能算一次,我们使用BurpSuite的Intruder,将Post数据中的ip和X-Forwarded-For的ip首段打上标记,选择Battering Ram模式(让两处参数一致),构造0~255的字典,自动化访问0.114.114.1141.114.114.1142.114.114.114……255.114.114.114达成刚好256个助力获得flag。由于每次间隔2s,2s*256=512s,小于题目时长限制10min=600s所以是可行的。


Amnesia-轻度失忆

直接putchar()逐个打印绕过即可。


图之上的信息

GraphQL的利用。访问/graphql?query={}发现存在利用可能。查阅资料后得知可以利用内省注入。没有UI界面我直接地址栏输入。换行替换为%0A后,访问/graphql?query={__schema%0A{types%0A{name}}}得到:

1
{"data":{"__schema":{"types":[{"name":"Query"},{"name":"GNote"},{"name":"Int"},{"name":"String"},{"name":"GUser"},{"name":"Boolean"},{"name":"__Schema"},{"name":"__Type"},{"name":"__TypeKind"},{"name":"__Field"},{"name":"__InputValue"},{"name":"__EnumValue"},{"name":"__Directive"},{"name":"__DirectiveLocation"}]}}}

发现有个GUser类型。接下来访问/graphql?query={__type(name:"GUser"){name%0Afields{name%0Atype{name,kind}}}}爆出字段名privateEmail,然后直接/graphql?query={user(id:1){id,privateEmail}}得到flag.


赛博厨房

  • Level0:直接写
  • Level1:复制粘贴发现有长度限制 goto优化行数
    然后不会了。

阵列恢复大师-RAID5

直接丢进RAID Reconstructor 5里面跑得到镜像文件。Windows上并没法直接读,于是丢进Diskinternals Linux Reader里读文件导出。执行getflag.py却提示Did you recover my data correctly?,疑惑之下换WSL(Kali Linux)执行就成功了。
吐槽:RAID0的XFS需要注册码没法搞。WSL也mount不上,看来还是虚拟机靠谱。


助记词-第一顿大餐

代码审计后发现目的是延迟尽可能高。用BurpSuite改包在POST数据里复制出来很多行注记词提交就有了flag1。就像这样:

1
2
3
4
5
6
[
"necessary science growth addition",
"necessary science growth addition",
(重复好多好多次)
"necessary science growth addition"
]

如果不行CLEAR再试试。
clear it

小坑:token要跟上才能拿到flag,不过有时自动获取的token无效,使得flag只显示为true,因为加号没有URLEncode转义,不知道是不是bug.


马赛克

模拟题。大概思路是先扫一遍,把能确定的先确定下来。之后再dfs剩下的块(不需要全部求出,毕竟这题的二维码纠错拉满了)。
代码很丑对吧QAQ(当时不熟悉ndarray操作,全部当做list做一遍)

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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
from PIL import Image
import math
import numpy as np
# import scipy
import matplotlib.pyplot as plt

SIZE = 627
MSBLOCK = 23
QRBLOCK = 11
X, Y = 103, 137
def ImageToMatrix(filename):
# 读取图片
im = Image.open(filename)
# 显示图片
# im.show()
width,height = im.size
im = im.convert("L")
data = im.getdata()
data = np.matrix(data,dtype='float')/255.0
new_data = np.reshape(data,(width,height))
return new_data
# new_im = Image.fromarray(new_data)
# # 显示图片
# new_im.show()
def MatrixToImage(data):
data = data*255
new_im = Image.fromarray(data.astype(np.uint8))
return new_im

pre_arr = [[0 for col in range(SIZE)] for row in range(SIZE)]
res_arr = [[1 for col in range(SIZE//QRBLOCK)] for row in range(SIZE//QRBLOCK)]
kimeta = [[0 for col in range(SIZE//QRBLOCK)] for row in range(SIZE//QRBLOCK)]#钦定了
EACH = int(math.ceil(MSBLOCK/QRBLOCK)) #EACH=3
filename = 'pixelated_qrcode.bmp'
data = ImageToMatrix(filename)
np.set_printoptions(threshold=1145141919810)
for i in range(0,SIZE):
for j in range(0,SIZE):
pre_arr[i][j]=data[i].tolist()[0][j]
if i%QRBLOCK==0 and j%QRBLOCK==0:
res_arr[i//QRBLOCK][j//QRBLOCK]=int(data.tolist()[i][j])
# print(pre_arr)
def check(i,j,now):
if i<=9 or i>=51 or j<=12 or j>=54:
if not (pre_arr[i*QRBLOCK+3][j*QRBLOCK+3] == now):#随便写的offset
return False
if (kimeta[i][j] or vis[i][j]) and res_arr[i][j]!=now:
return False
return True


def getRange(i,j):
return i*QRBLOCK,j*QRBLOCK,(i+1)*QRBLOCK-1,(j+1)*QRBLOCK-1 #x1,y1,x2,y2

vis = [[0 for col in range(SIZE//QRBLOCK)] for row in range(SIZE//QRBLOCK)]
def calcDelta(Mi,Mj,fillN,comp,LUR,LUC):
fn = fillN
for xx in range(LUR,LUR+EACH) :
for yy in range(LUC,LUC+EACH):
if not check(xx,yy,fn&1):
return 114514
fn = fn>>1
avr = 0
qx1,qy1,qx2,qy2 = X+Mi*MSBLOCK,Y+Mj*MSBLOCK,X+(Mi+1)*MSBLOCK-1,Y+(Mj+1)*MSBLOCK-1
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
if (fillN&1):
x1,y1,x2,y2 = getRange(x,y)
inx1,iny1,inx2,iny2 = max(x1,qx1),max(y1,qy1),min(x2,qx2),min(y2,qy2)
avr = avr + (iny2-iny1+1)*(inx2-inx1+1)
fillN = fillN >> 1
newres = avr/(MSBLOCK*MSBLOCK)
if (newres < comp-0.1):
return 1919810
return abs(math.floor((avr/(MSBLOCK*MSBLOCK))*255)/255-comp)

def putRec(i,j):#放置识别码块
kimeta[i][j]=1
res_arr[i][j]=0

kimeta[i][j+1]=1
res_arr[i][j+1]=1
kimeta[i][j-1]=1
res_arr[i][j-1]=1
kimeta[i+1][j]=1
res_arr[i+1][j]=1
kimeta[i-1][j]=1
res_arr[i-1][j]=1
kimeta[i+1][j+1]=1
res_arr[i+1][j+1]=1
kimeta[i-1][j-1]=1
res_arr[i-1][j-1]=1
kimeta[i+1][j-1]=1
res_arr[i+1][j-1]=1
kimeta[i-1][j+1]=1
res_arr[i-1][j+1]=1

for x in range(i-2,i+3):
for y in range(j-2,j+3):
if abs(x-i)==2 or abs(y-j)==2:
kimeta[x][y]=1
res_arr[x][y]=0

putRec(28,28)
putRec(28,50)
putRec(50,50)
putRec(50,28)

for i in range(9,52):
for j in range(12,55):
if i==9 or i==51 or j==12 or j==54:
kimeta[i][j]=1
res_arr[i][j]=pre_arr[i*QRBLOCK+3][j*QRBLOCK+3]

new_arr = [[1 for col in range(SIZE)] for row in range(SIZE)]
fked = [[0 for col in range(20)] for row in range(20)]

cntt = 0
savecnt=0
def dfs(i,j):#从马赛克块的i行j列开始
global cntt,savecnt
cntt = cntt+1
LUR , LUC= (X+i*MSBLOCK)//QRBLOCK , (Y+j*MSBLOCK)//QRBLOCK
#落在哪个二维码方块上
if (cntt==200000):#保存中途结果
for i in range(0,SIZE):
for j in range(0,SIZE):
new_arr[i][j]=res_arr[i//QRBLOCK][j//QRBLOCK]
MatrixToImage(np.array(new_arr)).save('mid%s.bmp'%savecnt)
savecnt = savecnt + 1
cntt = 0
if i<0 or i>=20 or j<0 or j>=20 or fked[i][j]:
return False
fked[i][j]=1

nowMin , nowSol = 114514191981.0 , (1<<(EACH*EACH))-1
tmpp = [[0 for col in range(3)] for row in range(3)]
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
tmpp[x-LUR][y-LUC]=vis[x][y]
for filN in range(0,1<<(EACH*EACH)):#枚举每个马赛克块影响到的3*3=9个QR块
tmp = calcDelta(i, j, filN, pre_arr[X+i*MSBLOCK+2][Y+j*MSBLOCK+2],LUR,LUC)#同样是乱写的offset+2
if tmp < 0.00000001:
nowSol = filN
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
vis[x][y] = 1
res_arr[x][y]=nowSol&1
nowSol = nowSol>>1
if ((i+1<20 and (not fked[i+1][j]) and dfs(i+1,j)) or (j+1<20 and (not fked[i][j+1]) and dfs(i,j+1)) or (i-1>=0 and (not fked[i-1][j]) and dfs(i-1,j)) or (j-1>=0 and (not fked[i][j-1]) and dfs(i,j-1))):
fked[i][j]=0
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
vis[x][y] = tmpp[x-LUR][y-LUC]
return True

fked[i][j]=0
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
vis[x][y] = tmpp[x-LUR][y-LUC]
return False


#预先扫描,把能确定的、没有多解的先填上
for i in range(0,20):
for j in range(0,20):
LUR , LUC= (X+i*MSBLOCK)//QRBLOCK , (Y+j*MSBLOCK)//QRBLOCK
succnt = 0
for filN in range(0,1<<(EACH*EACH)):
tmp = calcDelta(i, j, filN, pre_arr[X+i*MSBLOCK+2][Y+j*MSBLOCK+2],LUR,LUC)
if tmp < 0.00000001:
succnt = succnt + 1
nowSol = filN
if succnt == 1:
for x in range(LUR,LUR+EACH) :
for y in range(LUC,LUC+EACH):
kimeta[x][y] = 1
res_arr[x][y]=nowSol&1
nowSol = nowSol>>1
dfs(5,5)

for i in range(0,SIZE):
for j in range(0,SIZE):
new_arr[i][j]=res_arr[i//QRBLOCK][j//QRBLOCK]
MatrixToImage(np.array(new_arr)).save('resu.bmp')

最终效果:QRCode


minecRaft

web× reverse√
打开网页查看js,找到flag.js,在jsnice.org反混淆后自己手动再替换下几个迷人眼的常量,之后进行代码审计,会发现这是个TEA加密。如何发现是TEA呢?我搜了好久,后来有人告诉我只需要搜常量就行(还需要学习一个人生经验)。
我们需要找到一个字符串s,使得s.encrypt(“{那串数字密钥}”)=== “6f……0c”
把密文切片避免转换后数字过大,在题目页面下F12,进入Console里执行:

window.btoa(LongToStr4(Base16ToLong('6fbde674'))+LongToStr4(Base16ToLong('819a59bf'))+LongToStr4(Base16ToLong('a1209256'))+LongToStr4(Base16ToLong('5b4ca2a7'))+LongToStr4(Base16ToLong('a11dc670'))+LongToStr4(Base16ToLong('c678681d'))+LongToStr4(Base16ToLong('af4afb67'))+LongToStr4(Base16ToLong('04b82f0c')))

得到密文dOa9b79ZmoFWkiChp6JMW3DGHaEdaHjGZ/tKrwwvuAQ=
谷歌TEA decryption online,进入https://www.mefancy.com/obfuscation/encryption-generator.php把上面那串数字密钥(13…)还有密文丢进去就出flag了。


p😭q

老外大奥流泪.gif 早知道,还是原道.jpg
先把gif的帧提取出来方便处理:

import os
import sys
from PIL import Image

def extractFrames(inGif, outFolder):
    frame = Image.open(inGif)
    nframes = 0
    while frame:
        frame.save('./dest/%s.png' % (nframes))
        nframes += 1
        try:
            frame.seek(nframes)
        except EOFError:
            break;
    return True

if __name__ == '__main__':
    image = os.path.abspath(sys.argv[1])
    dest = os.path.join(os.path.dirname(image), "dest")
    if not os.path.exists(dest):
        os.mkdir(dest)
    extractFrames(image, dest)

然后原题给啥库就用啥库,逆回去:

from array2gif import write_gif  # version: 1.0.4
import librosa  # version: 0.8.1
import numpy as np  # version: 1.19.5
import soundfile as sf
from PIL import Image
import matplotlib.pyplot as plt

def ImageToMatrix(filename):
    im = Image.open(filename)
    height,width = im.size
    im = im.convert('L')
    data = im.getdata()
    data = np.matrix(data,dtype='float')/255.0
    new_data = np.reshape(data,(width,height))
    return new_data
np.set_printoptions(threshold=1145141919810)
num_freqs = 32
quantize = 2
min_db = -60
max_db = 30
fft_window_size = 2048
frame_step_size = 512
window_function_type = 'hann'
red_pixel = [255, 0, 0]
white_pixel = [255, 255, 255]
sample_rate = 22050
lis =  [[0.0 for col in range(587)] for row in range(32)]
imgg = [[0.0 for col in range(130)] for row in range(92)]
for ii in range (0,587):
    filename = './dest/%s.png'%ii
    print(filename)
    data = ImageToMatrix(filename)
    for i in range(0,92):
        for j in range(0,130):
            imgg[i][j]=data.tolist()[i][j]
    for xpos in range(0,32):
        for scan in range(0,92):
            if imgg[scan][xpos*4+2]<1.0:  
                lis[xpos][ii]=float((91-scan)-60)
                break

spectrogram =  np.array(lis)
audio_signal = librosa.feature.inverse.mel_to_audio(librosa.db_to_power(spectrogram), sr=sample_rate, n_fft=fft_window_size*2, hop_length=frame_step_size, window=window_function_type)
sf.write('newnew.wav', audio_signal, sample_rate) 

最后在Adobe Audition CC里随便处理处理,勉强能听了,开始努力听写:
Theflagis f,l,a,g ......