题解作者:ldeng
出题人、验题人、文案设计等:见 Hackergame 2022 幕后工作人员。
-
题目分类:general
-
题目分值:无法 AC 的题目(150)+ 动态数据(250)
传说科大新的在线测评系统(Online Judge)正在锐意开发中。然而,新 OJ 迟迟不见踪影,旧的 OJ 和更旧的 OJ 却都已经停止了维护。某 2022 级计算机系的新生小 L 等得不耐烦了,当即表示不就是 OJ 吗,他 10 分钟就能写出来一个。
无法 AC 的题目
为了验证他写的新 OJ 的安全性,他决定在 OJ 上出一道不可能完成的题目——大整数分解,并且放出豪言:只要有人能 AC 这道题,就能得到传说中的 flag。当然,因为目前 OJ 只能运行 C 语言代码,即使请来一位少年班学院的天才恐怕也无济于事。
动态数据
为了防止数据意外泄露,小 L 还给 OJ 加入了动态数据生成功能,每次测评会随机生成一部分测试数据。这样,即使 OJ 测试数据泄露,攻击者也没办法通过所有测试样例了吧!(也许吧?)
判题脚本:下载
你可以通过 nc 202.38.93.111 10027
来连接题目,或者点击下面的 "打开/下载题目" 按钮通过网页终端与远程交互。
如果你不知道
nc
是什么,或者在使用上面的命令时遇到了困难,可以参考我们编写的 萌新入门手册:如何使用 nc/ncat?
这题考察编译期未加权限限制导致的数据泄露。
静态和动态的测试数据分别位于 ../data/static.in/out
和 ../data/dynamic{i}.in/out
(因为用户代码编译时在)。由于提交的程序是以 runner
用户的身份运行的,且数据文件的权限都是 700
,直接在提交程序里尝试读取这些文件会权限不足。但是,仔细阅读代码可以发现,OJ 程序在编译用户提交的代码时,并没有加入权限限制,这就给了我们在编译期读取数据文件的机会。(如果参加了去年的 hackergame,会记得去年的题目 amnesia 同样是编译用户提交的代码并执行的,对比两题的代码也能发现,去年的题目在编译和运行时都使用了特殊的用户。事实上,本题的代码就是在 amnesia 的基础上修改的。)
第一个 flag 只需通过一组静态样例就能获得,静态样例每次测试是不会改变的,所以我们只需要想办法得到 ./data/static.out
的内容就可以了。在编译期读取文件最简单的方法就是使用 #include
,同时,由于 OJ 程序直接将编译错误转发到了标准输出,我们只需要在数据文件的每一行构造出编译错误,就能从编译器的错误提示中拿到每一行的数据。而我们知道 static.out
中只有两行,每行是个大整数,所以这个任务非常简单,使用以下两个文件即可:
// payload1-1.c
#include "../data/static.out"
// payload1-2.c
int a =
#include "../data/static.out"
由于单独一个整数显然不是合法的 C 语言程序,直接包含整个数据文件就能使编译器在第一行出错。要使错误出现在第二行也很简单,使第一行变得合法即可(上面的例子中是将第一作为变量定义的一部分,也有很多其它方法)。分别提交两个文件,得到以下输出:
In file included from ./temp/code.c:2:
./temp/../data/static.out:1:1: error: expected identifier or ‘(’ before numeric constant
1 | 9760010330994056474520934906037798583967354072331648925679551350152225627627480095828056866209615240305792136810717998501360021210258189625550663046239919
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
静态数据测试: Compile Error
In file included from ./temp/code.c:4:
./temp/../data/static.out:2:1: error: expected ‘,’ or ‘;’ before numeric constant
2 | 10684702576155937335553595920566407462823007338655463309766538118799757703957743543601066745298528907374149501878689338178500355437330403123549617205342471
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
静态数据测试: Compile Error
可以看到,编译错误里直接包含了两行数据,提交程序直接输出两行数据即可得到第一个 flag。
// payload1-3.c
#include<stdio.h>
const char* p="9760010330994056474520934906037798583967354072331648925679551350152225627627480095828056866209615240305792136810717998501360021210258189625550663046239919";
const char* q="10684702576155937335553595920566407462823007338655463309766538118799757703957743543601066745298528907374149501878689338178500355437330403123549617205342471";
int main() {
printf("%s\n%s\n", p, q);
return 0;
}
第二个 flag 较为复杂,要求我们通过 5 组动态数据的测试。所有动态数据都是在每次提交代码编译前生成好的,所以我们仍然可以在编译时读到文件内容,但因为每次提交数据会改变,像第一个 flag 那样得到每个文件的内容再构造代码是行不通的。一个简单的想法是,用 #include
将两个数字都作为合法 C 语言代码的一部分,但是我并没有成功构造出来,搜索到的一些例子尝试了一下也没有成功,如果有人使用这种办法完成本题的,也欢迎提交自己的 writeup。
另一个想法也是本题的预期解,即将在编译期将文件作为字符串常量导入程序中,然后在运行时对比输入,选择合适的答案输出。在编译期将二进制文件作为 const char 或者 string 导入程序理论上应该是很常见的需求,但目前 C 和 C++ 都没有官方实现这样的特性,通常需要用 xxd
等命令实现,很不方便。然而,有一条汇编指令 .incbin
,却能够在汇编期完成这个任务,该汇编指令可以把文件按原样拷贝到汇编代码之中。
直接使用 .incbin
指令对不熟悉汇编的人(比如我)来说可能比较困难,还好 github 上的 incbin 项目,将该指令封装成了易用的宏。使用这个项目构造出如下代码,即可将动态数据输入输出文件文本作为字符串分别保存到 in
和 out
数组中:
#include "incbin.h"
#define PATH_PREFIX "./data/dynamic"
#define inc_in(i) INCTXT(in##i, PATH_PREFIX #i ".in")
#define inc_out(i) INCTXT(out##i, PATH_PREFIX #i ".out")
inc_in(0);
inc_in(1);
inc_in(2);
inc_in(3);
inc_in(4);
inc_out(0);
inc_out(1);
inc_out(2);
inc_out(3);
inc_out(4);
const char *in[5] = {gin0Data, gin1Data, gin2Data, gin3Data, gin4Data};
const char *out[5] = {gout0Data, gout1Data, gout2Data, gout3Data, gout4Data};
然后加入代码将标准输入和 in
数组比较,选择正确的输出打印即可。(要注意,需要手动把 incbin.h
粘贴在上述代码前,毕竟 OJ 的环境里没有这个头文件。如果嫌代码很长,也可以先用 gcc -E
预处理上述代码,把预处理后的定义部分摘出来,可以参考 payload2-2.c,是使用该方法处理后的完整 payload)。
本题 flag2 中提到的 P1040 std::embed 是 C++ 的一个提案,希望解决的就是上述编译期导入二进制文件的问题,但是很遗憾两年过去还是没有进入 C++23。(现在该提案分出了 P1967 #embed
提案,也不知道什么时候能进标准。)如果这两篇提案未来能够成功进入标准,那本题可能就只需要 std::embed("dynamic0.out")
就搞定了(想得美)。如果你既没有听过 .incbin
也没有听过 incbin 项目,那你在搜索类似 compile time import file as string
等关键字的时候,可能也会找到这篇提案,然后在提案的引用中找到 incbin 的链接,不过混在一堆 xxd
等方案里,可能确实有些难找。
本题解用到的所有 payload 都在 src/payload
目录下,可以自行查阅。