USTC Hackergame 2021 Writeup

Author Avatar
Sora 10月 30, 2021

今年是本菜鸡第一次参加CTF,本人学的不是信息安全专业,因此对大多数CTF没什么兴趣。
参加本次Hackergame是因为经常水USTCLUG的群,挺喜欢USTCLUG的氛围,所以来随便卷一卷(不是

我记得我最高的时候是84名,深深地体会到了卷王的厉害
对于我这种信息安全外行来说,这个成绩可能还算不错的样子,就是有一道题差点做出来没错就差最后一步了,如果我做出来的话,完全可以上榜单的……没事,反正第一次不熟练是正常的,反正分数和第100名是一样的,不用后悔,明年再战
不用后悔对吧
不用后悔的
……
呜啊哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇哇

签到

签到题没什么好说的,Unix时间戳贴到URL里rua一把就完事了。

进制十六——参上

遮了明文却没遮住16进制部分,用WinHex(Windows)或者UltraEdit(Windows/macOS)甚至Python/JavaScript输入相应的16进制部分即得到结果。

去吧!追寻自由的电波

无线电是吧,Google“无线电 区分字符串中读音相近的字母”找到单词表。

当时这道题难了我好久,用macOS IINA降速失真严重,用Firefox降速总有听不清的单词,最后忍着安装用不到的软件的悲痛上了上次用还是高一时的Adobe Audition,降速+降调
这次算是清晰了,不过第5个单词和最后一个单词听了半天,怀疑这个词真的在单词表里面吗?
干脆掏出Google翻译语音识别这个单词——还真TM不在单词表里面@(huaji_han)

对不起,是我英语太烂了

猫咪问答 Pro Max

紧跟时事是吧
不知道你们这个猫咪问答炼丹性能能不能和M1 Max有得一拼

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

提示:输入格式为 YYYYMMDD,如 20211023。请不要回答 “能” 或者 “不能”。

sec.ustc.edu.cn已经挂了,搜索site:lug.ustc.edu.cn可以找到LUG官网的存档,但是所有的链接已经被替换为archive.org的链接。
点击首页,点击信息安全俱乐部社团章程,找到日期。

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

提示:是一个非负整数。

点击LUG官网——了解更多,找到可能是答案的内容:

为了表彰其出色表现,协会于 2011 年 5 月被评为中国科学技术大学优秀学生社团,于 2012 年 5 月、2013 年 5 月及 2014 年 5 月分别被评为中国科学技术大学四星级学生社团,并于 2015 年 5 月、2017 年 7 月、2018 年 9 月、2019 年 8 月及 2020 年 9 月被评为中国科学技术大学五星级学生社团。

今年是2021年,填了4,答案错误。我当时怀疑是后面的答案出了问题,试了好几次,最后随便试了一下5,过了;后来我听说USTCLUG在2021年也被评选为五星社团了。

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

提示:正确答案的长度为 27,注意大小写。

考不上科大的菜逼当然是找图了。Google一下USTCLUG找到图:

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

提示:是一个非负整数。

Google搜索Newcomb-Benford SIGBOVIK 2021找到论文合集,搜索Newcomb-Benford找到相应的论文。

可见共有14个图表,第一个图表为作者引用之前的结果,并非用于对其理论结果进行验证,故为13。

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

提示:正确答案的长度为 9。

Google一下Protocol Police找到RFC 8962 Establishing the Protocol Police,阅读后找到答案:

  1. 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.

卖瓜

有一个人前来买瓜。你拥有以下物品:

一个大棚,里面有许多 6 斤一个的瓜和许多 9 斤一个的瓜。

一个电子秤(最开始是空的);

你的任务是在电子秤上称出刚好 20 斤的瓜。

正常肯定是不行的,尝试输个n个9成功使其溢出:

先给它加到比较小的数:

然后随便排列组合一下,给它加到20:

透明的文件

一个透明的文件,用于在终端中展示一个五颜六色的 flag。 可能是在 cmd.exe

等劣质终端中被长期使用的原因,这个文件失去了一些重要成分,变成了一堆乱码,也不会再显示出 flag 了。

注意:flag内部的字符全部为小写字母。

这个转义总感觉很眼熟,Google搜索[39m 转义知道此乃ANSI转义序列,文件里的一堆空格是它输出的字符。说了cmd不能识别这种转义,反正我用的macOS(不是Mac),不慌。
[替换为\033[,因为空格看不清所以替换为方块,输出即可。
记得最后暂停一下,否则会被清除。

file = './transparent-2.txt'
with open(file, 'r') as f:
    data = f.read()
data2 = data.replace('[', '\033[')
data2 = data2.replace(' ', '█')
print(data2)
input()

旅行照片

和猫咪问答一样,又是社工题。
看到KFC,Google一下KFC 海边搜图,找到秦皇岛新澳海底世界KFC。
海底世界也不搞大点,我问了一个秦皇岛的同学,他说没去过
听说有人打电话给这家KFC了,甚至还有人打给EXIF信息上的电话,结果接电话的是科大保卫处
知道了具体的KFC就很容易找到电话号码了,至于旁边的几个字稍微翻一翻也可以找到,4、5题搞定。
至于前三道选择题,我用我毕生所学的初中地理知识无解,反正是静态资源+base64文件名,暴力解决。

import requests
import base64
tpl = '1={0}&2={1}&3={2}&4=0335-7168800&5=%E6%B5%B7%E8%B1%9A%E9%A6%86'
url = 'http://202.38.93.111:10055/%s.txt'
for c1 in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']:
    for c2 in ['a', 'b', 'c', 'd', 'e']:
        for n in range(8, 18):
            data = tpl.format(c1, c2, n)
            req = requests.get(url % base64.b64encode(data.encode()).decode())
            print('%s %s %s %s' % (c1, c2, n, req.status_code))
            if req.status_code != 404:
                print(req.text)

FLAG 助力大红包

题目要求所有的/8段IP都轮一遍,这按正常方法显然是不行的,就算你在科大校内可以拿到10.0.0.0/8,那无效地址0.0.0.0/8,环回地址127.0.0.0/8,广播地址255.0.0.0/8怎么办对吧
于是上X-Forward-For协议头伪造IP地址,成功过关,感谢一年前某网站试图屏蔽我的爬虫让我知道了这个HTTP头

import requests
for i in range(0, 255):
    time.sleep(1)
    data = requests.post(
        url='http://202.38.93.111:10888/invite/3730fa86-8117-4a49-ae4d-7d52fca5088c',
        headers={'Content-Type': 'application/x-www-form-urlencoded', 'X-Forwarded-For': f'{i}.8.8.8'},
        data=f'ip={i}.8.8.8'
    ).text
    print(data)

Amnesia 轻度失忆

重度失忆不会,没学过汇编
编译后 ELF 文件的 .data 和 .rodata 段会被清零,那我们不从这里拿字符串不就行了

#include <stdio.h>
int main(){
    putchar('H');
    putchar('e');
    putchar('l');
    putchar('l');
    putchar('o');
    putchar(',');
    putchar(' ');
    putchar('W');
    putchar('o');
    putchar('r');
    putchar('l');
    putchar('d');
    putchar('!');
}

图之上的信息

GraphQL?我以前还真没有听说过这个技术栈,据说是把接口当数据库用,同时相对于SQL更好进行权限处理。
搜索GraphQL 渗透,找到渗透测试之玩转graphQL,得知GraphQL有内省查询问题。
对页面进行抓包,可以看到笔记部分使用graphql作为接口获取数据

构造内省查询包,取得用户表结构:


可知邮箱位于User表下的privateEmail字段,查询获得flag:

加密的U盘

LUKS的原理是使用masterkey加密内容,之后再使用passphrase加密masterkey,而修改passphrase并不会修改masterkey。将day2.img的header替换为day1.img的header,就可以用day1.img的passphrase解密day2.img。操作环境为Ubuntu 20.04.5 LTS。

sudo apt install -y cryptsetup-bin
sudo losetup -f # 获取空闲loop环回虚拟设备,假设为/dev/loop1
sudo losetup -P loop1 ./day1.img # 挂载day1.img到loop1
sudo losetup -f # 获取空闲loop环回虚拟设备,假设为/dev/loop2
sudo losetup -P loop2 ./day2.img # 挂载day2.img到loop2
sudo cryptsetup luksHeaderBackup /dev/loop1p1 --header-backup-file ./header1.bin # 备份day1.img的header
sudo cryptsetup luksHeaderRestore /dev/loop2p1 --header-backup-file ./header1.bin # 将header还原到day2.img
sudo cryptsetup open /dev/loop2p1 day2 # 解密挂载day2.img到day2 mapper,需要输入密码
# 因为已经恢复为day1的header,输入day1的密码即可
mkdir day2 # 建个目录供挂载
mount /dev/mapper/day2 ./day2 # 挂载至day2文件夹
cat ./day2/flag.txt # 获取flag

Micro World

** 注意:这里的解法不是完整的解法。**

没错,我开头说的差点做出来的题就是这一题!明明已经把源码逆向出来了,然而终究还是被出题人预判了……经验不足,明年再……**呜哇啊啊啊啊啊啊啊啊啊啊啊啊啊我的300分啊啊啊有了这300分不就进前80了啊啊啊**
本人exe逆向零基础,试图用IDA Pro逆向失败,索性摆烂,掏出超清一亿像素60fps丝滑录像照亮你的美小米10Pro准备录下开头美好的flag。
flag没录到开头那特么是个什么玩意,但是当我仔细翻看录像时,发现了新大陆:

pygame是吧?那八成用的pyinstaller打包,上pyinstxtractor,得到2.pyc,然后网上随便找个pyc反编译,得到python源码。
然后欣喜地运行,结果初始画面和手机录到的并没有什么区别,改了配色还是没看出什么玄机

试了半天找不出flag来……
根据官方解法,将vx和vy置相反数可以使粒子反向运动获得flag。~~~我为什么没想到啊啊啊我还是太嫩了~~~

马赛克

下载生成马赛克的源码,看到马赛克是通过将块内的所有像素取均值得到的。通过最外层露出来的不完整像素可解得马赛克块的部分像素;并且马赛克块内像素的均值已知,接下来不断地去碰撞即可。我的代码没有对可能存在的多种情况进行处理,而是直接加入队尾后跳过;理论上可以通过递归的方式获得完整的二维码,但是反正二维码一般有容错机制,商业二维码扫描库也支持自动纠错,扫得出来就懒得折腾了。最终剩余154个马赛克块未解开,微信扫码获得flag。

import numpy as np
from PIL import Image
import queue

X, Y = 103, 137 # 纵向x,横向y
N = 20 # 马赛克块数,NxN
BOX_SIZE = 23 # 马赛克块边长
PIXEL_SIZE = 11 # 二维码像素边长

# 打开图片
img = Image.open('./pixelated_qrcode.bmp')
arr = np.asarray(img)

def get_color(x, y):
    '''
    根据已有颜色判断该方格原有颜色
    若为黑色0返回1,白色255返回0,未知返回-1
    '''
    pixel_x = int(x / PIXEL_SIZE) * PIXEL_SIZE
    pixel_y = int(y / PIXEL_SIZE) * PIXEL_SIZE
    pixels = arr[pixel_x:pixel_x+PIXEL_SIZE, pixel_y:pixel_y+PIXEL_SIZE]
    for _y in pixels:
        for color in _y:
            if color == 0:
                return 1
            elif color == 255:
                return 0
    return -1

def divide_by_pixels(x, y) -> tuple[int, int, int, int]:
    '''
    根据二维码像素切割马赛克块
    '''
    if x < X or y < Y:
        return False
    block_x = int((x - X) / BOX_SIZE) * BOX_SIZE + X # 马赛克块位置
    block_y = int((y - Y) / BOX_SIZE) * BOX_SIZE + Y
    pixel0_x = int(block_x / PIXEL_SIZE) * PIXEL_SIZE # 二维码像素起始位置
    pixel0_y = int(block_y / PIXEL_SIZE) * PIXEL_SIZE
    pixel1_x = pixel0_x # 二维码像素x向最大位置
    while True:
        pixel1_x += PIXEL_SIZE
        if pixel1_x >= block_x + BOX_SIZE:
            break
    pixel1_y = pixel0_y # 二维码像素y向最大位置
    while True:
        pixel1_y+= PIXEL_SIZE
        if pixel1_y >= block_y + BOX_SIZE:
            break
    result = [
        ((max(x1, block_x), max(y1, block_y)),
        (
            min(x1 + PIXEL_SIZE, block_x + BOX_SIZE),
            min(y1 + PIXEL_SIZE, block_y + BOX_SIZE))
        )
        for x1 in map(
            lambda x: pixel0_x + PIXEL_SIZE * x,
            range(0, int((pixel1_x - pixel0_x) / PIXEL_SIZE))
        )
        for y1 in map(
            lambda y: pixel0_y + PIXEL_SIZE * y,
            range(0, int((pixel1_y - pixel0_y) / PIXEL_SIZE))
        )
    ]
    return result

def gen_possibility(length):
    '''
    返回所有可能性
    '''
    return [[int(i) for i in list('{:b}'.format(n).rjust(length, '0'))] for n in range(0, 2 ** length)]

def avgs(parts):
    '''
    计算在所有可能性下的均值
    '''
    n = len(parts)
    poss = gen_possibility(n)
    result = []
    for s in poss:
        all_sum = sum(
            [(x2 - x1) * (y2 - y1) * (0 if s[i] else 255)
                for i,((x1,y1),(x2,y2)) in enumerate(parts)]
        )
        avg = all_sum / (BOX_SIZE ** 2)
        result.append((s, avg))
    return result

def possibilities(parts, known, target):
    '''
    获取所有符合条件的可能性
    '''
    print(known)
    all_poss = [poss for poss, avg in avgs(parts) if int(avg) == target]
    result = []
    for poss in all_poss:
        available = True
        for n, i in enumerate(known):
            if i == -1:
                continue
            if i != poss[n]:
                available = False
                break
        if available:
            result.append(poss)
    return result

def main():
    '''
    主程序
    '''
    q = queue.Queue()
    for item in [(nx, ny) for nx in range(0, N) for ny in range(0, N)]:
        q.put(item)
    while True:
        if q.qsize() == 0:
            break
        nx, ny = q.get()
        print(f'current: {nx}, {ny}, {q.qsize()}')
        if q.qsize() == 154:
            Image.fromarray(arr).save('./result.png')
            exit(0)
        block_x = X + BOX_SIZE * nx
        block_y = Y + BOX_SIZE * ny
        parts = divide_by_pixels(block_x, block_y)
        colors = [get_color(x1, y1) for (x1, y1), _ in parts]
        poss = possibilities(parts, colors, arr[block_x, block_y])
        if len(poss) != 1:
            q.put((nx, ny))
            continue
        poss = poss[0]
        for n, i in enumerate(poss):
            (x1, y1), (x2, y2) = parts[n]
            if i == 1:
                arr[x1:x2, y1:y2] = 0
            elif i == 0:
                arr[x1:x2, y1:y2] = 255
            else:
                raise Exception
                Image.fromarray(arr).show()
                exit(0)
    Image.fromarray(arr).show()

if __name__ == '__main__':
    main()

总结

这次Hackergame题目印刷清晰,手感舒适;厨房阿姨态度和蔼,瓜非常好吃;flag比pdd好拼,一点也不难;总体来说感觉很不错,明年再来。