Hackergame 2022 Writeup

好久不玩 CTF,偶然看到了中科大的校赛,就又去看了一下。 完全不知道还好,但只要看到了题,就会上瘾,忍不住去做,太花时间了。

虽然我还是一样菜,但题目还是跟几年前一样保持着趣味性,挺有意思的

Xcaptcha

直接写脚本交互,注意 cookie 的设置

检索和社工题

检索题都好找,社工题对我不太友好,我太菜了,试了好久放弃了。版本号那里不知道具体含义只看到0231,就卡很久, 最后航班我没找到可以看历史轨迹的,找最近机场看航线猜可能的结果组合也太多,各种乱试也没弄对。

HeiLang

不觉得这很酷吗?

猜数字

Java 从字符串 Parse 一个 Double 判断是否与目标相等,判断的方法 !isLess && !isMore 有问题, 构造特殊输入 “NaN”

LaTeX 机器人

第一问:直接用 \input命令插入文件 $$\input{/flag1}$$

第二问:文件中有特殊字符,从这里 了解到一个\newread命令, 从一个 stackoverflow 回答了解到\catcode命令, 最终可以读取文件并替换特殊字符,最后构造的输入:

$$\newread\file \catcode`\#=11 \catcode`\_=13 \openin\file=/flag2 \read\file to\line \line \closein\file$$

Flag 的痕迹

从这个开源项目的 GitHub issue 的相关讨论了解到除了 revision 还有一个 diff 可以用,项目的鉴权逻辑有问题,直接利用

安全的在线评测

第一问可以直接读文件,第二问不会,打算先缓存当前次数后读文件,但 RE

线路板

隐写题,根据文件后缀名找软件打开文件,去掉遮挡

Flag 自动机

动态调试,有个反调试函数TlsCallback_0,跳转到正常的函数之前,通过 MessageBoxSetWindowPos 系统 API 的调用找到关键函数,用x64dbg 修改汇编指令去掉没用的跳转和按钮的移动。不太会动态调试,按理说看题目逻辑应该会写入到文件,我是中间有一步从寄存器读出来,没能写入到文件,我的修改:

>flag_machine.exe
0000151A:0F->E9
0000151B:84->EB
0000151C:FC->02
0000151D:03->00
0000151F:00->90
0000180A:81->C7
0000180B:7D->45
00001811:74->EB
000019FF:FF->90
00001A00:D0->90
0000226C:83->FF
0000226D:F8->D0
0000226E:02->90

虽然接触得不多,但我觉得x64dbg这个项目是个好东西

微积分计算小练习

XSS,但后端有屏蔽关键字,有些提交直接没响应,具体屏蔽规则不知道,用各种绕过方式瞎试,最后用的是 斜杠+unicode编码

<img/src/onerror="&#97;&#108;&#101;&#114;&#116;&#40;&#100;&#111;&#99;&#117;&#109;&#101;&#110;&#116;&#46;&#99;&#111;&#111;&#107;&#105;&#101;&#41;">

杯窗鹅影

这个有意思,用到的知识之前还真没了解过。Wine 不提供沙箱,所以函数可以直接用 linux 的系统调用访问 wine 之外的各种东西。 第一问直接用 read 系统调用,第二问感觉要写汇编,没做出来

#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

int main(int argc, char **argv) {

  // flag1
  int fd = open("/flag1", O_RDONLY);
  if (fd == -1) {
    printf("file not found.\n");
  }
  char buf[1024];
  memset(buf, '\0', 1024);
  read(fd, &buf, 1024);
  write(1, &buf, 1024);
  return 0;
}

蒙特卡罗轮盘赌

随机数种子设置是time(0)+clock(),时间戳可以一连上就获取本地时间,早一两秒也不要紧,因为 clock() 的返回大概在几百之间,前两次故意输错结果,根据返回爆破种子。

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

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

void compute(unsigned int seed, char *guess) {
  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(guess, "%1.5f", pi);
  guess[7] = '\0';
}

int main(int argc, const char **argv) {
  // disable buffering
  setvbuf(stdin, NULL, _IONBF, 0);
  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);
  if (argc != 4) {
    printf("Invalid input");
    return 0;
  }
  const unsigned t = atoi(argv[1]);
  const char *target1 = argv[2];
  const char *target2 = argv[3];

  for (unsigned int s = 0; s < 2000; s++) {
    srand(t + s);
    char guess1[2000];
    char guess2[2000];
    compute(t + s, (char *)&guess1);
    compute(t + s, (char *)&guess2);
    if (strcmp(target1, guess1) == 0 && strcmp(target2, guess2) == 0) {
      char ans[2000];
      for (int i = 0; i < 3; i++) {
        compute(t + s, (char *)&ans);
        printf("%s\n", ans);
      }
      return 0;
    }
  }
  return 1;
}

置换魔群

这个也有意思,学习了一下代数,复习了一下密码学。我数学不好,所以这里写的很不专业,表达个大体意思。

第一问:给出n, e, 和x**e,求x, 可以发现置换有周期性,周期也就是模数是组成置换的每个轮换的最小公倍数,像 RSA 那样用模逆求出d,乘回去即可

def solve_rsa(enc: permutation_element, n: int, e: int):
    import math
    std = enc.to_standard(enc.permutation_list)
    lens = list(map(lambda x: len(x), std))
    lcm = math.lcm(*lens)
    d = pow(e, -1, lcm)
    ans = enc**d
    return ans

第二问:给出n, g, yy=g^x, 求出 x。还是先用轮换表示, 找出原始和目标的每个轮换之间差几步,存为 vs,每个轮换的长度存为ms。 题目就是求同余方程组的解,不过不保证ms之间是互质的,抄来了一个扩展的中国剩余定理,解出结果

def solve_dh(n: int, g: permutation_element, y: permutation_element):
    g_std = g.to_standard(g.permutation_list)
    y_std = y.to_standard(y.permutation_list)
    y_idx = {}
    for y_group in y_std:
        for y_ele in y_group:
            y_idx[y_ele] = y_group

    vs = []
    ms = []
    for g_group in g_std:
        if len(g_group) == 1:
            continue
        y_group = set()
        for v in g_group:
            y_group.add(y_idx[v])
        y_group = sorted(list(y_group), key=lambda grp: grp[0])

        new_y = [i for i in range(1, n + 1)]
        for sg in y_group:
            sg = list(sg)
            sg.append(sg[0])
            for v, vn in zip(sg, sg[1:]):
                new_y[v - 1] = vn

        new_g = [i for i in range(1, n + 1)]
        sg = list(g_group)
        sg.append(sg[0])
        for v, vn in zip(sg, sg[1:]):
            new_g[v - 1] = vn

        new_g = permutation_element(len(new_g), new_g)
        new_y = permutation_element(len(new_y), new_y)

        temp = new_g
        o = 0
        while True:
            if temp == new_y:
                break
            temp = temp * new_g
            o += 1
        if o == len(g_group):
            vs.append(len(g_group))
            ms.append(len(g_group))
        else:
            vs.append(o % len(g_group))
            ms.append(len(g_group))
    print(ms)
    print(vs)
    if len(vs) == 1:
        return vs[0]
    ans, mod = CRT(zip(vs, ms))
    return ans + 1

第三问:给出n,自己输入两个g,得到对应的y=g^x,求x。不会了

光与影

完全不懂图形学,下载到本地,有一段 GLSL 代码,里面有个 SDF5 很可疑,就手动把值改到很大再打开网页,蒙出来了结果。

链上记忆大师

第一问送分,学一下 solidity 的智能合约上的数据存储

后面不会了,不熟悉,懒得学

片上系统

第一问隐写,拿文件提示的软件打开,选 SD 相关的 decoder,选好时钟信号和看起来有数据的信号,就能看到 hex 的 flag

第二问感觉太麻烦,没看

量子藏宝图

第一问学一下 BB84 的密钥交换,用最简单的全相同的位可以水过,但还是不懂实际原理

第二问给了个电路图,用提示里的库 qiskit 手动构建电路图求解,但还是不懂实际原理,懒得学+学不会

企鹅拼盘

只会前两问,提取出逻辑,直接爆破。这里花了挺久,因为想复用原来的异步逻辑, 结果题目用的 TUI 库高速迭代,早期版本根本没有文档了,跟新版本差别巨大,根本学不会了。 还好逻辑比较简单,于是只取了一部分代码加手写一部分爆破。

第三问看起来就很麻烦很数学,不会。

补充

官方题解

总结

总之题还是很友好,不过我太菜了555。本来想着随便玩玩就好,后来看着进排行榜了而且有些题真的挺有意思就有点认真了, 最后花了挺多时间还是排得巨低,但真的很好玩。