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()
函数的附近下断点,可以看到刚才两个函数其实分别对应两条汇编指令 movd
和 pshufd
,可以查内部函数指南了解它们具体的功能,但也可以直接观察寄存器内容:经过这两条指令后,XMM0 存入了零扩展后的十六进制数,而 XMM5 则将低 32 位复制到了高 96 位。
大概知道了这一块代码在做什么,回到 IDA 继续往下看。
又是一段类似的代码,但这次是对 v10
即 v2+8
的位置进行操作,v2
就是我们输入的字符串12ABCD3456
,v2+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
处是什么。
竟然是一段代码!
总结一下目前的发现:
函数先把输入的前 8 个字符转为十六进制数,然后复制为 128 位存到局部变量中。
把输入的第 9 字符和第 10 字符转为十六进制数,然后复制为 128 位存到局部变量中。
对这两个局部变量进行一些运算,结果存到
&xmmword_405018
为首地址的一块内存中。比较
xmmword_405018
与固定值loc_404148
,比较 96 字节。若全部相等,则将
xmmword_405018
处的代码复制到sub_4018A0()
处。
现在回头看刚刚跳过的部分。
最开始的 if
是个反调试,可以 patch 掉。然后是一串 XMM 操作,参与运算的操作数有 v27
, v9
, v21
,v9
就是上述“目前发现”的第 1 步结果,v27
是上述“目前发现”的第 2 步结果,v21
是对 v27
运算的结果,因此真正的输入只有两个:v9
, v21
,我们需要找出什么样的输入经过这些运算能与固定值 loc_404148
相等。具体的运算可以查英特尔内部函数指南,这里不赘述,我们重点看第 152 行到第 157 行,这几行的运算最简单,看懂了就可以反推出正确的输入。
xmmword_404360
和 xmmword_404340
是预设的固定值,_mm_add_epi32
将它们每 32 位为一组相加(例如,0x1000000020000000+0x30000000F0000000=0x4000000010000000),结果再与 v9
每 32 位为一组相加,这个结果我们记为 a
。xmmword_405038
也是固定值,它和 v21
每 32 位为一组相加,结果记为 b
。最后 xmmword_405038
的值赋为 a
和 b
按位异或。155 行的运算类似。
xmmword_405038
其实就是 xmmword_405018
往后 0x20 字节处,也就是 loc_404148
往后 0x20 字节处,因此输入应满足方程:
a XOR b == *(xmmword *)((BYTE *)&loc_404148 + 0x20)
155 行可以类似列出一条方程,两条方程解两个未知数 v9
和 v21
,用 Z3 求解。
运行,解出 v9
和 v21
。
转为十六进制数即 0x13242208
和 0x19
,所以程序输入的前十个字符应该是 1324220819
。
前十个字符正确后,sub_4018A0()
处的原本代码会被覆盖,也就是代码进行了自修改。看看修改后的函数。
IDA 现在无法反编译为伪代码,不过我们可以直接修改 exe,手动把 sub_4018A0()
处的字节覆盖为新函数的字节。
在这里覆盖,然后重新用 IDA 打开,找到函数就可以反编译。简单看了下,是 3DES 加密,密钥在:
注意小端序,顺序是反的。
函数对输入的后 16 个字符进行 3DES 加密,并和固定值比较。
对固定值用密钥解密,得到输入的后 16 字符。
因此,最终正确输入就是 13242208190dcc509a6f75849b
。 \[
flag\{13242208190dcc509a6f75849b\}
\]