【Pwn#0x0D】HackIM CTF 2023 spygame writeup
我们CTF萌新小分队已经达到了4人之多(`ヮ´),这次排名41/433,感觉很好~
这道题有十个队做出来,很高兴我也弄出来了,还顺便大致学会了docker配本地环境,挺感动的~~
漏洞:随机数未设种子、数组下标溢出
程序逻辑
程序给了 N 个代码源文件以及DockerFile。主要逻辑是用 C code 写的,但是封装成了一个 Python 可以调用的模块,名为 spy
,相关信息参考python文档 Extending Python with C or C++ — Python 3.11.2 documentation。
game.py
会让玩家选择游戏模式(easy or hard),然后调用 spy 模块的接口,如果返回通过就把 flag 打印出来。spy 模块的主要逻辑大致如下:
- 首先进行八轮循环,每轮循环中:
- 生成一个固定大小的数组,元素类型
uint8_t
- 随机取两个数交换
- 打印交换后的数组
- 玩家输入两个 index(这一步将会计时,并分别将前后的时间保存到局部变量
start_ns
和end_ns
中) - 程序交换两个 index 的值
- 程序检查交换后数组,若正确则
total_ok++
- 程序将
end_ns - start_ns
加到total_ns
中
- 生成一个固定大小的数组,元素类型
- 循环完毕后,检查
total_ok == 5
和total_ns
是否足够小,并返回结果。
在 easy 模式下,total_ns 的限制换算后为 60 秒;但在 hard 模式下,total_ns 的限制为 1000ns,这通过正常的途径是不可能做到的(远程环境下最快每轮循环也需要 6000ns+)。
漏洞分析
第一个漏洞:程序生成随机数没有设置随机的种子,所以我们可以直接知道每一轮的答案是什么,从而达成五轮胜利来满足 total_ok == 5
的条件。
第二个漏洞:程序读取将要交换的 index 时,并没有做边界检查,所以我们可以干扰栈上的局部变量。
函数中的局部变量声明如下:
1 | char user_input[256]; |
其中最为重要的显然是 total_ns
和 total_ok
变量。但由于我们无法获取实际运行的 binary 文件,所以也没办法知道这些变量是存在栈上还是寄存器中,也没办法知道栈上相对 numbers 数组的偏移。
一种容易想到的方法是先答对五轮,然后尝试用 0 与 total_ns
交换来减少所花的时间。但这种方法需要我们知道 total_ns
的地址(如果它真的在栈上)。
在本地环境,经过幸苦的调试,可以发现 total_ns
确实在栈上,并利用这种方法攻击成功。但在远程环境,不论如何调试,都没办法找到 total_ns
的位置,我估计这个变量存寄存器上了。(这里省略了部分细节)
3.10/20:30 UPDATE:赛后看了别的师傅的writeup,发现这个方法是完全可以的,现在再跑之前的脚本就跑出来了,不知道昨天晚上为什么一直跑不出,感觉是运气实在太差了……
没办法直接修改 total_ns
,那就通过程序内的代码来修改 total_ns
。total_ns += end_ns - start_ns;
如果我们能够交换 end_ns
和 start_ns
,那就可以让 total_ns
减小。
为了找到这两个变量的位置,同样需要慢慢试。由于程序每轮会告知玩家所花的时间,因此这两个变量的位置可以很方便地试出来(所花时间非常大就说明打到了)。由于我们只能交换两个 uint8_t
,因此需要考虑更换哪两个位。
根据远程返回的信息可以发现,如果我们让程序以最快的速度运行(程序用 fgets 读取玩家输入,我们直接发送一个大字符串,其中用换行符区分答案),那么每轮的时间大约在 6000ns-10000ns 左右,换算为十六进制为 0x1770-0x2710。
这可以说明大部分情况下,开始和结束的时间,除了最后两个字节外,其余的字节都是相同的。所以我们只要交换两个时间的倒数第二个字节,就可以让它们的真值也大致交换。
此外,由于时间会波动,因此若最后三次交换成功,就会有一定几率让最后的 total_ns
小于 1000ns。接下来就是编写脚本。
漏洞利用
经过测试发现,328-335 的偏移是 start_ns,320-327 的偏移是 end_ns。我交换的位偏移为 321 和 329。
利用脚本如下:
1 | def pwn(): |
最后附上爆破出flag的截图~ 今天早上挂上脚本后去干别的事了,回来突然看到打出来了很激动哈哈哈。