---
# System prepended metadata

title: Hackergame 2022 WriteUp

---

# Hackergame 2022 WriteUp

## 签到题

点提交观察 URL

```
http://202.38.93.111:12022/?result=????
```

修改 `result` 值为 `2022` 回车，获得 flag

```
flag{HappyHacking2022-**********}
```

## 猫咪问答喵

1. 谷歌搜索「中国科学技术大学 NEBULA」点进第一个结果，得到答案

   ```
   中国科学技术大学“星云战队（Nebula）”成立于2017年3月
   ```

2. bilibili 搜索「软件自由日活动」第一个由「中科大LUG」用户发布的视频，在视频 2:41:46 处右下角。没听出名字叫什么，但看 slide 中的图片有一个 `Configure Kdenlive...` 菜单项，得到答案 `Kdenlive`

3. 谷歌搜索关键字「Firefox Windows 2000」，第一个结果 [The latest versions to work on w2000 are 28.0 and 24.8.1esr. Wich to use, and why?](https://support.mozilla.org/en-US/questions/1052888) 中的 `28` 在尝试后是不正确的，答案是第一个回复说的 `12`

4. 谷歌搜索关键字「Linux argc 0」，找到文章 [Handling argc==0 in the kernel](https://lwn.net/Articles/882799/)，其中链接了一条讨论邮件 [[PATCH] fs/exec: require argv[0] presence in do_execveat_common()](https://lwn.net/ml/linux-kernel/20220126043947.10058-1-ariadne@dereferenced.org/) 包含了对 `fs/exec.c` 文件的修改补丁。在 GitHub 上的 [linux](https://github.com/torvalds/linux) 仓库转到这个文件并定位到 `retval = count(argv, MAX_ARG_STRINGS);` 行，左边点击行号，`View git blame`，可以看到它的下一行 `if (retval == 0)` 在 9 个月前有一次修改，复制出 commit hash，得到答案

   ```
   dcd46d897adb70d63e025f175a00a89797d31a43
   ```

5. 最开始在谷歌带引号全字匹配搜索 `"e4:ff:65:d7:be:5d:c8:44:1d:89:6b:50:f5:50:a0:ce"`，得到的两个结果唯一和域名有关的只有 `openssh.com`，可提交后怎么也不对。抱运气对 `***.com` 暴力后也没出结果。放下后一段时间回过头来才在 DockDockGo 上搜索到答案是 `sdf.org`

6. 谷歌搜索「中国科学技术大学 网络通」找到 [网络信息中心用户服务部FAQ](https://netfee.ustc.edu.cn/faq/)，其中包含信息

   ```
   中国科学技术大学校园网络运行及通信费用分担办法（2011年1月1日起实行）
   ```

   但这个日期提交后并不对，最后是暴力跑出来答案是 `2003-03-01`

提交答案，得到 flags

```
flag{meowexammeow_****************_**********}
flag{meowexamfullymeowed!_****************_**********}
```

## 家目录里的秘密

下载后用 vscode 打开目录，全字匹配+区分大小写搜索 `flag`，得到 flag1

```
flag{finding_everything_through_vscode_config_file_*********}
```

接着手动翻文件发现 flag2 在 `.config/rclone/rclone.conf`，明显 `pass` 的值是藏着 flag 的地方，但观察 host 也明显不是一个可连接的 FTP 地址，最开始没啥头绪就放着了，回头在谷歌搜索「rclone.conf pass」可以发现 `rclone obscure` 这个命令，描述为

> In the rclone config file, human-readable passwords are obscured. Obscuring them is done by encrypting them and writing them out in base64. This is not a secure way of encrypting these passwords as rclone can decrypt them - it is to prevent "eyedropping"

阅读不难理解 `obscure` 实际并不是一种带密钥的模糊，遂猜想可以直接还原，于是打开 `rclone` 的仓库，搜索找到 `obscure` 的实现调用了函数 `MustObscure`，在它的下一行就有还原函数 `Reveal`，虽然在 `rclone help` 中没有看到 `reveal` 命令，但抱着瞎试试（懒得手搓代码解密）的心态竟然试出来了这个隐藏命令

```
rclone reveal GXdlOU4Oji_bD41CxOmu-EfBWLC9stlki6XvEVW_5nFON-2enn94W8FBLA2NfNwAyxjFEbXuI0CUFSlpVtM
```

执行其即可还原出 flag2

```
flag{get_rclone_password_from_config!_*******}
```

## HeiLang

阅读题目可以得知要解本题必须要熟练掌握著名的何同学运算法则

```rust
fn main() {
    let input = r#"
copy these `a` assignments here
"#;

    let result = input
        .lines()
        .filter(|t| !t.trim().is_empty())
        .map(|t| {
            let mut s = t.split("=");
            let (left, right) = (s.next().unwrap().trim(), s.next().unwrap().trim());
            assert!(s.next().is_none());

            let indexes = left.trim_start_matches("a[").trim_end_matches("]");

            let assignment = indexes
                .split("|")
                .map(|i| format!("a[{}] = {}", i.trim(), right))
                .collect::<Vec<_>>()
                .join("\n");

            assignment
        })
        .collect::<Vec<_>>()
        .join("\n");

    std::fs::write(
        "./result.txt",
        result,
    )
    .unwrap();
}

```

跑完替换回去得到 flag

```
flag{****************-****************}
```

## Xcaptcha

在 Developer Tools 中 Disable Javascript 可以防止页面自动跳转，但手动计算提交发现过不了验证，可能确实有地方检查了时间差，于是写脚本提交，加循环重试，还是验证失败，有点懵。谷歌搜索了一下才反应过来，尝试使用同一个 session 再提交

```python
import requests
import re

session = '...'

s = requests.Session()
r = s.get('http://202.38.93.111:10047/xcaptcha', cookies={'session': session})

ret = re.findall('\d+\+\d+', r.text)

data = {
    'captcha1': eval(ret[0]),
    'captcha2': eval(ret[1]),
    'captcha3': eval(ret[2]),
}

r = s.post('http://202.38.93.111:10047/xcaptcha', data=data, cookies={'session': session})
print(r.text)
if r.text.find('验证失败：超过 1 秒限制') == -1:
    sys.exit(1)
```

成功得到 flag

```
flag{head1E55_br0w5er_and_ReQuEsTs_areallyour_FR1ENd_**********}
```

## 旅行照片

略了，最后航班题没找到免费查询网站，给 flightradar24 交了底裤试用了 7 天。

## 猜数字

阅读提供的 `GuessNumber.java` 代码，发现随机数是用 `SecureRandom` 生成的，所以大概率不是预测随机数，阅读判断逻辑代码，注意到有 `try` `catch`，于是不带脑子猜想触发异常也许可以过，网页端判断了输入合法性，在 Developer Tools 拿到接口手动传递 `NaN` 提交，拿到 flag

```dotnetcli
flag{gu3ss-n0t-a-numb3r-1nst3ad-****************}
```

## LaTex 机器人

flag1

```
\input{/flag1}
```

flag2 用 `\catcode` 转义一下输出即可

```
\catcode `\#=12\\n\catcode `\_=12\\n\catcode `\&=12\\n\input{/flag2}
```

但不知道为什么开头会有非预期的 `nnn` 出现，用此方法读取 `/flag1` 也可复现

**UPDATE**
经 MiaoTony 老师提醒，在 latex 中的换行是 `\\` 而不是我瞎猜的 `\n` 加转义

## 安全的在线测评

### 无法 AC 的题目

读取 `/static.out` 的内容并输出即可得到 flag

```c
#include <stdio.h>

int main() {
    char s[1024];
    FILE* stream = fopen("./data/static.out", "r");
    fread(s, 1, 1000, stream);
    puts(s);
}
```

```
flag{the_compiler_is_my_eyes_**********}
```

看到 flag 中的提示才发现这个解法可能算是个非预期，`#include` 才是出题者期待的解法。给出的判题代码中有尝试给目录设置 700 权限，但由于文件夹已存在并且没给静态文件设置权限所以可以直接读。

### 动态数据

这题没解出来，说说尝试的过程，应该也算比较有趣。

首先尝试了很久将 `#include` 进来的数据合法化成 C 代码，但最终都以失败告终。谷歌搜索了很久也没有搜到在编译时直接将文件包含成字符串的方法，虽然前不久听说过 `#embed` 这种东西，但现阶段是明显用不了的。想到了 inline asm，但是不知道有 `.incbin` 这个指令。最后是想到了也许可以读父进程的内存（也就是判题脚本的 python 进程），然后在 heap 区搜索输入，根据上下文找输出。实验了这个方法确实是能搜到输入输出的，而且 `/proc/[pid]/maps` 也能读到，但在最后被 `/proc/[pid]/mem` 要求权限宣告失败。

（中途其实还有更多一些奇思妙想，但明显都失败了）

## 线路板

下载安装 ViewMate，`Import Gerder...` 文件 `ebaz_sdr-F_Cu.gbr`，挨个双击那堆圆形遮挡物 Delete 掉即可看到 flag

```
flag{8_1ayER_rogeRS_81ind_V1a}
```

## Flag 自动机 

IDA 打开随手往上翻了一下看到字符串 `"Congratulations"`，F5 跳到窗口的 `WndProc` 消息处理函数中，跟着判断逻辑发送消息即可弹出 flag

```c
#include <Windows.h>

int main() {
    SendMessageW((HWND)0x3029E, 0x111, 3, 114514);
}
```

```
flag{Y0u_rea1ly_kn0w_Win32API_**********}
```

## 微积分计算小练习

在练习网站随便输入提交，得到成绩页面，在 Developer Tools 中阅读 JS 代码，发现姓名成绩由 URL 中 base64 编码过的 `result` 值传入，格式为 `score:name`，并且字符串拼接可能有注入。

阅读提交成绩的 `bot.py` 代码，发现 flag 会被放入 `document.cookie`

用 `<img>` 的 `onerror` 参数构造 payload

```
1:<img src="meow.jpg" onerror="document.querySelector('&#35;greeting').innerHTML = document.cookie">
```

编码 base64 后提交链接即可获得 flag

```
flag{xS5_1OI_is_N0t_SOHARD_**********}
```

## 杯窗鹅影

flag1 直接用 `ReadFile` 读 `/flag1` 文件即可

flag2 尝试得比较久，尝试直接用 `CreateProcess` 执行 `/readflag` 会失败，题目说了 `start.exe` `cmd.exe` 等文件已删除，但在递归打印 C 盘目录后发现仍然有 `C:\windows\command\start.exe` `C:\windows\system32\WindowsPowerShell\v1.0\powershell.exe` 文件存在并可用，我也就是从这里开始误入歧途的。多次交替组合用 `CreateProcess` 尝试执行类似于 `C:\\windows\\command\\start.exe /b /unix /readflag` 的操作，但很奇怪总是无法打印出孙进程（指 `readflag`，子进程是 `start.exe`）的 stdout，换其他系统进程可以打出 `/help` 内容，但在真机中却全都可以打出来，这里猜测是 wine 的 bug。在这过程中我甚至尝试了 `C:\\windows\\command\\start.exe /b \\\\?\\unix\\readflag` 也无果，最后到了晚上我才注意到直接执行 `/readflag` 的错误代码是 `ERROR_FILE_NOT_FOUND`，遂尝试直接执行 `\\\\?\\unix\\readflag`，得到 flag

```
flag{W1ne_is_NeveR_a_SaNDB0x_**********}
```
## 蒙特卡罗轮盘赌

可预测的 seed 猜随机数，拿着提供的代码改就行，`clock()` 是增量，所以 `time(0)` 不用减直接往后跑

```c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>

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

typedef struct _VALUE
{
    char v[20];
} VALUE;

typedef struct _RESULT
{
    VALUE v[5];
} RESULT;

VALUE rand_once()
{
    VALUE val;
    memset(&val, 0, sizeof(val));

    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(val.v, "%1.5f", pi);
    return val;
}

int try(unsigned int seed, const char *tried_1, const char *tried_2)
{
    srand(seed);

    VALUE r1 = rand_once();
    if (strcmp(r1.v, tried_1) == 0) {
        VALUE r2 = rand_once();
        if (strcmp(r2.v, tried_2) == 0) {
            VALUE r3 = rand_once(), r4 = rand_once(), r5 = rand_once();
            printf("next: %s, %s, %s\n", r3.v, r4.v, r5.v);
            return 1;
        }
    }
    else {
        printf("trying seed %u\n", seed);
    }
    return 0;
}

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

    const char *tried_1 = "3.13985";
    const char *tried_2 = "3.14315";

    unsigned int init_seed = (unsigned)time(0); // + clock();

    for (size_t i = 0; i < 100000000; i++) {
        if (try(init_seed + i, tried_1, tried_2) == 1) {
            break;
        }
    }
    return 0;
}
```

## 光与影

拖下 html + js 文件扫一眼，不难看出函数 `t1SDF` `t2SDF` `t3SDF` `t4SDF` 返回的是实体的顶点坐标，搜索引用发现在函数 `sceneSDF` 中有一行组合

```javascript
float tmin = min(min(min(min(t1, t2), t3), t4), t5);
```

分别移除每一层嵌套测试，可知 `t5` 是 flag 的遮罩实体，移除后可见 flag 为

```
flag{SDF-i3-FuN!}
```

## 传达不到的文件

首先是看了一遍几个 `bin` 目录，瞎试了一通发现没什么用，最后看到 `/etc/init.d/rcS` 文件，其在配置好 flag 相关文件的权限后起了一个 shell

想起之前忘了哪国发生的运维事故，大概就是在运行时 bash 脚本被修改，然后导致文件被意外删除。所以首先想到

```sh
sed -i 's/poweroff/#/g' /etc/init.d/rcS
echo "cat /flag2" >> /etc/init.d/rcS
```

但很奇怪的是，文件确实被修改了，`Ctrl+D` 后并没有打出 flag 的内容。

再想想，干脆直接改 `poweroff` 的内容来拿到 shell

```sh
# If we modify it directly, there will be an error '/bin/sh: can't create /sbin/poweroff: Text file busy'
rm /sbin/poweroff

echo "#!/bin/sh" > /sbin/poweroff
echo "cat /chall" >> /sbin/poweroff
echo "cat /flag2" >> /sbin/poweroff

chmod +x /sbin/poweroff
```

然后 `Ctrl+D`，可以在混杂着 binary 的输出中找到两个 flag

```
flag{ptr4ce_m3_4nd_1_w1ll_4lways_b3_th3r3_f0r_u}
flag{D0_n0t_O0o0pen_me__unles5_u_tr4aced_my_p4th_**********}
```

根据 flag 中的提示，很明显我们非预期解了这道题

## More

电脑关了才发现写漏了几道题，明天再补上...

---

## 惜字如金

### HS384

阅读提供的代码，在手动补全了开头的几个简单例子后，根据给定的长度猜 key。

这题没什么难度，暴力枚举就行，只要别像我最开始读错规则导致卡了好久

判断 hash 的时候可能需要注意一下，部分字母可能被删除了，但是里面有一串挺长的数字，根据规则它不会受到影响，我们可以 `find` 这个特征来判断 hash


```python
from base64 import urlsafe_b64encode
from hashlib import sha384
from hmac import digest
from sys import argv
import time
import sys

def hash_check(data):
    return sha384(data.encode()).hexdigest().find("62074271866") != -1


if __name__ == '__main__':
    key = 'ustc.edu.cn'

    guess_len = 39 - len(b'ustc.edu.cn')

    s_remain_len = guess_len
    for s in range(0, s_remain_len + 1):
        t_remain_len = s_remain_len - s
        for e1 in range(0, 2):
            t_remain_len -= e1
            for t in range(0, t_remain_len + 1):
                c_remain_len = t_remain_len - t
                for e2 in range(0, 2):
                    c_remain_len -= e2
                    for c in range(0, c_remain_len + 1):
                        d_remain_len = c_remain_len - c
                        for e3 in range(0, 2):
                            d_remain_len -= e3
                            for d in range(0, d_remain_len + 1):
                                c2_remain_len = d_remain_len - d
                                for e4 in range(0, 2):
                                    c2_remain_len -= e4
                                    for c2 in range(0, c2_remain_len + 1):
                                        n = c2_remain_len - c2
                                        for e5 in range(0, 2):
                                            n -= e5
                                            curr_key = 'us{}t{}c{}.ed{}u.c{}n{}'.format(
                                                's' * s + 'e' * e1,
                                                't' * t + 'e' * e2,
                                                'c' * c + 'e' * e3,
                                                'd' * d + 'e' * e4,
                                                'c' * c2 + 'e' * e5,
                                                'n' * n)
                                            if hash_check(curr_key):
                                                print(f'the key is {curr_key}')
                                                sys.exit(1)
```

跑出 key 为 `usssttttttce.edddddu.ccccccnnnnnnnnnnnn`，替换回题目给的代码给 3 个样本签名即可得到 flag

```
flag{y0u-kn0w-h0w-t0-sav3-7h3-l3773rs-r1gh7-****************}
```

## 片上系统

### 引导扇区

下载安装 PulseView，`Import Raw binary logic data...` 文件 `logic-1-1`，根据 `metadata` 文件内的信息，设置 `Sample rate` 为 16，添加协议解码器 *SD card (SPI mode)*，Clock 应该是持续存在的，所以选择 1 通道，其他的瞎试试出来的 MISO=3 MOSI=2 CS#=0，在缩小图像后可以明显看出长度相同的分块，根据题意定位到第一个分块结尾，右键复制出 MISO transfer 的 binary，可以看到最后有 `66 6C 61 67` 开头的 bytes，转成字符串得到 flag

```
flag{0K_you_goT_th3_b4sIc_1dE4_caRRy_0N}
```

最开始没设置 `Sample rate` 导致显示的数据似乎不正确懵了一会儿

### 操作系统

没想到也不知道能 dump 所以没做出来

## 企鹅拼盘

这个要吐槽一下，最开始在键盘上乱敲没看懂怎么玩的，后来才知道要鼠标点击 `Inputs` 才能输入

### 这么简单我闭眼都可以！

确实，搞明白玩法后第一次就试出来了（x

输入 `1000`，按 `L` 执行所有步骤，`Q` 退出看到 flag

```
flag{it_works_like_magic_**********}
```

### 大力当然出奇迹啦~ 

确实，我当然只会大力出奇迹了（

```rust
use std::{
    fmt::Display,
    fs, mem,
    ops::RangeInclusive,
    time::{Duration, SystemTime},
};

use serde::Deserialize;
use serde_json as json;

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
struct Pos {
    x: usize,
    y: usize,
}

impl Pos {
    fn check_overflow(&self, x: RangeInclusive<usize>, y: RangeInclusive<usize>) {
        debug_assert!(x.contains(&self.x));
        debug_assert!(y.contains(&self.y));
    }
}

#[derive(Debug)]
struct Branch {
    index: usize,
    zero: Vec<Move>,
    one: Vec<Move>,
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum Move {
    Left,
    Right,
    Up,
    Down,
}

impl Display for Move {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Left => f.write_str("L"),
            Self::Right => f.write_str("R"),
            Self::Up => f.write_str("U"),
            Self::Down => f.write_str("D"),
        }
    }
}

#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum Block {
    Fixed(usize),
    Blank,
}

impl Display for Block {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Fixed(id) => write!(f, "{id:02}"),
            Self::Blank => f.write_str("B"),
        }
    }
}

#[derive(Clone, PartialEq, Eq, Debug)]
struct Map {
    blocks: [[Block; 4]; 4],
    blank_pos: Pos,
}

impl Map {
    const INIT: [[Block; 4]; 4] = [
        [
            Block::Fixed(1),
            Block::Fixed(2),
            Block::Fixed(3),
            Block::Fixed(4),
        ],
        [
            Block::Fixed(5),
            Block::Fixed(6),
            Block::Fixed(7),
            Block::Fixed(8),
        ],
        [
            Block::Fixed(9),
            Block::Fixed(10),
            Block::Fixed(11),
            Block::Fixed(12),
        ],
        [
            Block::Fixed(13),
            Block::Fixed(14),
            Block::Fixed(15),
            Block::Blank,
        ],
    ];

    fn init() -> Self {
        Self {
            blocks: Self::INIT,
            blank_pos: Pos { x: 4, y: 4 },
        }
    }

    fn block(&self, pos: Pos) -> Block {
        self.blocks[pos.x - 1][pos.y - 1]
    }

    fn block_mut(&mut self, pos: Pos) -> &mut Block {
        &mut self.blocks[pos.x - 1][pos.y - 1]
    }

    fn move_blank(&mut self, m: Move) {
        debug_assert_eq!(self.block(self.blank_pos), Block::Blank);

        let mut new_pos = self.blank_pos;

        match m {
            Move::Left => new_pos.y -= 1,
            Move::Right => new_pos.y += 1,
            Move::Up => new_pos.x -= 1,
            Move::Down => new_pos.x += 1,
        }

        new_pos.check_overflow(1..=4, 1..=4);

        let (a, b) = (self.block(new_pos), self.block(self.blank_pos));
        *self.block_mut(new_pos) = b;
        *self.block_mut(self.blank_pos) = a;

        self.blank_pos = new_pos;
    }
}

impl Move {
    fn from_char(ch: char) -> Self {
        match ch {
            'L' => Self::Left,
            'R' => Self::Right,
            'U' => Self::Up,
            'D' => Self::Down,
            _ => panic!("unknown char '{ch}'"),
        }
    }
}

fn deser(data: impl AsRef<str>) -> Vec<Branch> {
    #[derive(Debug, Deserialize)]
    struct RawBranch(usize, String, String);

    let ser: Vec<RawBranch> = json::from_str(data.as_ref()).unwrap();

    ser.into_iter()
        .map(|b| Branch {
            index: b.0,
            one: b.1.chars().map(Move::from_char).collect(),
            zero: b.2.chars().map(Move::from_char).collect(),
        })
        .collect()
}

const MAX: u16 = u16::MAX;

fn test_input(branches: &Vec<Branch>, input: usize) -> bool {
    let mut map = Map::init();

    for branch in branches {
        let bit_shift = mem::size_of_val(&MAX) * 8 - 1 - branch.index;
        let bit_mask = 1 << bit_shift;
        let bit = (input & bit_mask) >> bit_shift;

        let moves = match bit {
            0 => &branch.zero,
            1 => &branch.one,
            _ => panic!("unexpected input '{bit}'"),
        };

        for &m in moves {
            map.move_blank(m)
        }
    }

    map.blocks != Map::INIT
}

fn main() {
    let data = fs::read_to_string("./BoardProgram/chals/b16_obf.json").unwrap();

    let branches = deser(data);

    let mut t = SystemTime::now();

    for input in 0..=MAX as usize {
        if test_input(&branches, input) {
            println!("the input should be '{input:016b}'");
            break;
        } else {
            let now = SystemTime::now();
            if now.duration_since(t).unwrap() > Duration::from_secs(3) {
                println!("testing '{input:016b}'");
                t = now
            }
        }
    }
}

```

## 火眼金睛的小 E

### 有手就行

下载样本后，拖到 IDA 中，根据特殊常量定特征码，在另一个样本中搜索，定位到后往上翻拿到函数头地址，提交即可得到 flag

```
flag{easy_to_use_bindiff_**********}
```

### 唯快不破

笨笨手搓壬不会自动化二进制分析喵，寄了
