2019 第三届强网杯解题报告(writeup)

Posted by HX on 2019-05-28 | 👓

0x0. MISC – 签到

打开题目,大喊“隐藏着黑暗力量的钥匙啊,在我面前显示你真正的力量!跟你定下约定的 cxk 命令你,封印解除!” flag 就会自己把自己解出来。 \[ flag\{welcome\_to\_qwb\_2019\} \]

0x1. MISC – 强网先锋-打野

比赛时没做出来(我打 CTF 像 cxk),搜了一下,用 zsteg 看一下隐写数据就得到 flag。(🐓你太美 \[ qwxf\{you\_say\_chick\_beautiful?\} \]

0x2. CRYPTO – 强网先锋-辅助

打开下载到的 Python 脚本,看到 \(p, q, e, n\) 几个字母就知道是 RSA 题。进行了两次 RSA 加密,一次是将 flag 加密,另一次是将 32 个 1 组成的字符串加密。题目给出了 \(c, e, n\),观察代码,发现第一次加密使用的素数 \(q\) 重复用到了第二次加密中,这意味着两次加密的 \(n_1=p_1 q\), \(n_2=p_2 q\) 有公因数 \(q\)。使用模不互素攻击,求 \(n_1\)\(n_2\) 的最大公因数,可直接得到 \(q\)\(p_1\)

分解了大素数乘积,RSA的难题就攻克了,接下来就是常规套路按公式解出明文。

求欧拉函数值 \(\varphi(n_1)=(p_1-1)(q-1)​\)

求私钥,即 \(e​\) 关于 \(\varphi(n_1)​\) 的模反元素,d = gmpy2.invert(e, phi)

解密:\(m = c^d \mod n_1\) \[ flag\{i\_am\_very\_sad\_233333333333\} \]

0x3. REVERSE – 强网先锋-AD

直接上 IDA。

main() 函数里接受输入,一串赋值后执行 sub_4005B7(),最后循环比较字符。先看看比较的两个字符串,v4 是传入 sub_4005B7() 的参数,而 v5 在上面的一串赋值里初始化,看下 v5 的值。

这些都是 ASCII 码,转成字符来看。

局部变量一般是按声明顺序分配栈空间,因此连续的声明就会分配到连续的内存,v5, v6, v7… 其实是同一个字符串里的连续字符,这个字符串以 == 结尾,怀疑是 base64 编码,带着这种猜测继续看代码。

sub_4005B7() 中发现了 base64 字母表,很明显这个函数就是 base64 编码函数。因此程序流程就是将输入 base64 编码,再和以上固定的字符串比较。将固定字符串 base64 解码就得到 flag。 \[ flag\{mafakuailaiqiandaob\} \]

0x3. REVERSE – JUSTRE

直接上 IDA。

这里就是 main() 函数,看字符串很容易找到。逻辑十分简单,可以猜测 sub_401CE0()scanf() 函数,接受输入的字符串并存到 v1 中,然后将 v1 作为参数先后调用 sub_401610()sub_4018A0(),均返回 1 则回显 flag。

看下 sub_401610()

一上来是个循环,又 0 又 9 又 A 的,直接猜是将输入字符串转为数字。在 v9 那一行(75 行)下断点可以看到的确如此。

输入 12ABCD3456,前八位被转成了十六进制数 0x12ABCD34 存到 v3 中。

至于 v3 外面的两个函数是什么,我们需要翻阅古老的典籍——英特尔内部函数指南(Intel® Intrinsics Guide),里面记载了 XMM 法术的各种细节。首先我们在典籍中检索 _mm_cvtsi32_si28

这真是太不可思议了,原来这个函数就是 movd 魔咒的封装,作用是把输入 a 零扩展为 128 位并存到指定 XMM 寄存器中。

这时我们有必要切换所使用的工具,因为 IDA 对 XMM 寄存器的显示乱七八糟(可能只是我不会用吧)。换用 x32dbg。

在刚刚调用 _mm_cvtsi32_si28() 函数的附近下断点,可以看到刚才两个函数其实分别对应两条汇编指令 movdpshufd,可以查内部函数指南了解它们具体的功能,但也可以直接观察寄存器内容:经过这两条指令后,XMM0 存入了零扩展后的十六进制数,而 XMM5 则将低 32 位复制到了高 96 位。

大概知道了这一块代码在做什么,回到 IDA 继续往下看。

又是一段类似的代码,但这次是对 v10v2+8 的位置进行操作,v2 就是我们输入的字符串12ABCD3456v2+8 就是 5 这个位置。再往下看还有一段对 v2+9 的类似操作,v2+9 就是 6 这个位置,因此有理由怀疑这一块代码是将输入的第 9 位和第 10 位转成十六进制数。事实上,我们在这一块代码末尾的下图处下断点就可以知道的确如此。

这里又有三行内部函数对 XMM 寄存器操作,再次切换到 x32dbg,在下图处断点。

看到 XMM0 变成了输入的第 9 位和第 10 位的重复序列 565656……,并且 movaps 指令还将 XMM0 传送到了栈上。结合上上张图 IDA 的伪代码,栈上这个位置就是局部变量 v27,也就是说 v27 里现在存着 565656……

回到 IDA 往下看,接下来有一串 XMM 操作,看着眼花,还好我们快到函数末尾了,先拉到最底看看什么情况。

回忆一下,在 main() 函数中要求这个函数返回值为 1,因此流程必须要走到最里面。这里是在比较两块内存的值是否相等,比较 96 字节如果全部相等则进入最内层。比较的两块内存首地址是 &xmmword_405018&loc_404148,前者在我们刚才跳过的部分里修改了,后者则是固定的。在最内层中,有一个反调试,可以 patch 掉,最后是 WriteProcessMemory() 修改自身内存,将 xmmword_405018 的 96 字节写到 sub_4018A0() 处。而 xmmword_405018 又必须和 loc_404148 相等,因此我们看看 loc_404148 处是什么。

竟然是一段代码!

总结一下目前的发现:

  1. 函数先把输入的前 8 个字符转为十六进制数,然后复制为 128 位存到局部变量中。

  2. 把输入的第 9 字符和第 10 字符转为十六进制数,然后复制为 128 位存到局部变量中。

  3. 对这两个局部变量进行一些运算,结果存到 &xmmword_405018 为首地址的一块内存中。

  4. 比较 xmmword_405018 与固定值 loc_404148,比较 96 字节。

  5. 若全部相等,则将 xmmword_405018 处的代码复制到 sub_4018A0() 处。

现在回头看刚刚跳过的部分。

最开始的 if 是个反调试,可以 patch 掉。然后是一串 XMM 操作,参与运算的操作数有 v27, v9, v21v9 就是上述“目前发现”的第 1 步结果,v27 是上述“目前发现”的第 2 步结果,v21 是对 v27 运算的结果,因此真正的输入只有两个:v9, v21,我们需要找出什么样的输入经过这些运算能与固定值 loc_404148 相等。具体的运算可以查英特尔内部函数指南,这里不赘述,我们重点看第 152 行到第 157 行,这几行的运算最简单,看懂了就可以反推出正确的输入。

xmmword_404360xmmword_404340 是预设的固定值,_mm_add_epi32 将它们每 32 位为一组相加(例如,0x1000000020000000+0x30000000F0000000=0x4000000010000000),结果再与 v9 每 32 位为一组相加,这个结果我们记为 axmmword_405038 也是固定值,它和 v21 每 32 位为一组相加,结果记为 b。最后 xmmword_405038 的值赋为 ab 按位异或。155 行的运算类似。

xmmword_405038 其实就是 xmmword_405018 往后 0x20 字节处,也就是 loc_404148 往后 0x20 字节处,因此输入应满足方程:

a XOR b == *(xmmword *)((BYTE *)&loc_404148 + 0x20)

155 行可以类似列出一条方程,两条方程解两个未知数 v9v21,用 Z3 求解。

运行,解出 v9v21

转为十六进制数即 0x132422080x19,所以程序输入的前十个字符应该是 1324220819

前十个字符正确后,sub_4018A0() 处的原本代码会被覆盖,也就是代码进行了自修改。看看修改后的函数。

IDA 现在无法反编译为伪代码,不过我们可以直接修改 exe,手动把 sub_4018A0() 处的字节覆盖为新函数的字节。

在这里覆盖,然后重新用 IDA 打开,找到函数就可以反编译。简单看了下,是 3DES 加密,密钥在:

注意小端序,顺序是反的。

函数对输入的后 16 个字符进行 3DES 加密,并和固定值比较。

对固定值用密钥解密,得到输入的后 16 字符。

因此,最终正确输入就是 13242208190dcc509a6f75849b\[ flag\{13242208190dcc509a6f75849b\} \]