2019-上海市大学生网络安全大赛-writeup-REVERSE-Satan
Satan
这次比赛只看了这道逆向题,下午快调出来的时候被 rjj 拉过去唱歌,晚上回来已经结束了,调了几个脚本的bug出了这道题。
题目给了一个 64 位 ELF,丢进 IDA 中打开,主要逻辑都在 main 函数内,用 C++/STL 编写了很多字符串操作。
先看前几行的操作,第 183 行初始化了一个字符串。第 184 行"operator >>" 就是C++的输入流,而"unk_6072A0"可以通过交叉引用发现是 cin,于是这一行把用户输入读到字符串中。第 185 行从用户输入的字符串新建了另一个字符串,之后把新建的字符串送入 sub_4020DF 做检验,如果检验失败则输出"error"。
从 sub_4020DF 函数我们看到输入的格式,首先由flag{}包裹,里面共有五段,由'-'分隔。可以构造一个样例flag以方便动态调试。
flag
之后的一个大 switch 根据flag第一段第一个字符的大小,决定shuffle的方法,对5段输入做一个shuffle。例如下图中,第一段第一个字符为'0'的情况,打乱的顺序是01433。这个变换只进行一次,没有什么难度不需要仔细看。
之后来到了这道题最关键的地方,加密。这段代码对 shuffle 后的 32 位 flag 分为两段,分别加密。sub_401AA3是加密函数。
加密的密钥是由函数自身的text段生成的,sub_4025A2根据从start函数位置开始,到start+0x3D22为止的所有内容,生成一个整数 v96,再用这个整数构造256bit的密钥 v177。显然text段的内容是不会改变的,密钥也是固定的。(如果这样想就会被坑到,,,)
搞定了密钥,只剩下加密算法了。查了一下加密算法用到的加密盒,并没有查到什么内容。整个加密过程类似AES的SP结构。每一轮需要经过轮密钥加、非线性盒变换和位移变换。找不到已知算法,只能拿头去逆了。回想AES的解密流程,轮密钥加只需要调换顺序异或即可,S盒变换和位移变换则需要写出逆向算法。又因为密钥是固定的,可以直接从 gdb 中扣出所有轮密钥,看起来需要逆向的工作也没有太多。
-
轮函数
算法把明文分为四块,每一块存在一个32位整数中。共33轮,前31轮,每一轮对明文进行轮密钥加,非线性盒变换和线性混合。最后的32轮和33轮稍微不一样,不过用到的函数都一样。
-
非线性盒变换
这里我们先要弄懂如何进行这个变换的。经过一番头铁逆向,这段代码把从明文的四个块中各取一位,组合成一个4bit整数,然后在盒中做代换,代换后的值拼接起来,最后取代原来的四个明文。
逆向的话,整个流程相同,不过要用逆盒去做代换。出题人比较善良,逆盒存储在非线性盒的后面,直接用IDA扣出来就好了。最后用python编写逆变换。
-
线性混合
这里做了循环移位和异或。刚开始我想偷懒用z3,也没解出来,还是直接逆出来了。
我们可以看到,13和14行,第1块和第3块异或生成了第0块和第2块,之后第1、3块没有做修改,第0,2块只做了循环移位。也就是逆向的时候,我们可以直接还原13和14行,异或上去的第0、2块,那么也就能通过异或还原出初始的第0、2块。同理第1、3块也能够还原。所以这份代码可以直接写出逆向算法。
这样几个问题都解决了,只需要动态调试抠出轮密钥,然后做验证就行了。到这里遇到了一个大坑。需要了解一下gdb调试的原理。
gdb 内部使用 ptrace 实现的,在下断点的时候,会替换目标指令为 trap,当执行到目标指令时,实际执行 trap 指令,把控制权交给了控制器,也就是 gdb。也就是说 gdb 下断点会修改程序text段的值,那么加密函数的密钥生成就被影响了。用gdb调这道题会发现轮密钥经常变化,和猜想不符的情况。
搞清楚了这个就简单了,写一个脚本直接计算函数 sub_4025A2 的返回值,再用gdb调试就能拿到真正的轮密钥。
最后做解密,还原一下shuflle就拿到了flag。
flag