首页
社区
课程
招聘
[原创]复盘SUCTF 2019 Reverse 精解
发表于: 2019-10-18 00:06 5891

[原创]复盘SUCTF 2019 Reverse 精解

2019-10-18 00:06
5891

exeinfope 载入查壳。一个64位的ELF程序,无壳。

先来静态分析一波,载入IDA。找到 main 函数地址,F5大法...

可以看到程序先接收了输入到 v8 数组。然后经过sub_96A函数的处理。通过 gdb 动态调试可得该函数的作用即是将 HEX 转 ASCII。

继续往下看,程序调用了 __gmpz_init_set_str 函数,经过 Google 之后得知这其实是一个 GNU 高精度算法库(GNU Multiple Precision Arithmetic Library)。

官方文档入口

通过查阅官方文档,我知道了 __gmpz_init_set_str 其实就是 mpz_init_set_str

很显然这个函数的作用就是将 str 字符数组以 base 指定的进制解读成数值并写入 rop 所指向的内存。该程序通过调用这个函数来实现数据的初始化赋值。

之后调用的一个函数 __gmpz_powm 在文档中的定义是这样的:

该函数将计算 baseexp 次方,并对 mod 取模,最后将结果写入 rop 中。

这种计算与RSA中的加密过程如出一辙。

再往下就是关键比较函数 __gmpz_cmp

程序中比较之前 mpz_powm 运算的结果与程序中硬编码的值是否相等,如果相等则输出 tql。看到这里应该可以基本确定这是一道已知密文求解RSA明文的题目。

根据RSA的实现过程,首先来计算密钥。第一步是获得大整数 N ,根据程序可得

值得注意的是,这里的 N 是十进制的。

接下来对它进行大整数的因数分解,这里借助 yafu 工具。

至此我们得到了 pq

再从程序中得知 e 的值为

接下来就可以求出私钥 d,并通过私钥 d,求出明文 m,再将其转化成 ASCII 即可得到 flag

一个 64 位的没有加壳的程序

先看字符串

看到 You win ! 和 You lost sth,执行一下程序,输入 inputflagtest 猜测应该返回 You lost sth

实际上什么也没有返回,说明程序在输出 You lost sth 之前就停止了,动态调试一下找到是在哪里停下的

调试之后发现

我们首先得知道这个 v4 是干什么用的,才有可能阻止程序停止。于是往前看,找到一个循环

在这个循环之前程序将 v4 初始化为 0,也就是说我们需要通过 ++v4 这条语句让 v4 = 3 才能让程序不退出。很自然的想到去找这个循环的出口。于是找到一个

再往前看与 v9 相关的代码

v93 已经确定是 0 了。这里涉及到一个 v107 的值,它的值直接决定了 v9 会怎么变化,于是往前找到

发现 v107 的值是由函数 sub_140002B80 决定的。于是现在的问题就变成了研究这个函数的作用

这个函数的逻辑跳转比较复杂,但是经过动态调试之后,可以总结出来的是

该函数的第二个参数是我们输入的字符串指针,它会移动该指针,去取字符串中的每一位字符,取到空白符或者标点符就会返回 1 并且指针的位置也会保存下来,换言之它起到了一个分割字符串的作用,只不过分割的字符可以是任意的标点符或空白符。主要借助的是以下两个函数来实现

知道了这个函数的作用我们就能弄清楚刚刚的循环的作用了,很显然是在分割字符串,而 v3 = 3 也就意味着有三个部分

我们把输入重新改成 input_flag_test,再动态调试一遍,发现程序在此处停止了

v122 变量存储的是第一部分字符串的长度,于是我们更改输入为 input2nput_flag_test 继续调试

发现了以下循环

此时 v16 的值是 10 v15与v17 保存了当前的字符下标,我们发现 Dst 的每一位必须要与 0xAB 异或,且得到的值要与 v129 表中的值要相等。往前看到

分析一下 sub_140002690,首先从看它传入的参数 v76 被固定成了 0x31,即 '1'

动态调试后,发现它的作用很简单 sub_140002690(d,s,a) ,在 s 中去除 a 字符串并拷贝给 d

写个脚本跑一下

由于只有 5 个字符,但前面得到的信息是第一部分要有 10 个字符,这说明有 5 个字符是 '1',在 sub_140002690 中被去除了。于是第一部分就可以确定下来是 11111suctf

输入 11111suctf_flag_test ,继续调试遇到了

这里的 Size 刚好保存了第二部分的数据长度,我们这里刚刚好是 4 个

这里的循环主要限定了第二部分的字符范围为 [a-gA-G]

于是我们转换一下输入为 11111suctf_abcd_test 继续调试

这里将刚才输入的字符串第二部分的字母从小写转到大写

这里 memcmp 将刚才转化成大写之后的字符串与原先转化之前的字符串进行了比较,换句话说,我原先输入的第二部分字符串就必须是大写否则这里就无法进入分支

于是改变输入 11111suctf_ABCD_test 继续调试

这一段的主要作用是告诉我们第二段的字符串内容,每两个字符之间后一个字符要和前一个字符相差 2

结合 [a-gA-G] 的范围得出结果 11111suctf_ACEG_test 继续调试

这里用了一种很奇妙的方法来判断第三部分的字符是否是纯数字的

我们去内存中 dump 出 0x0000000000500210 这个位置往下某一块区域的表

正好 10 个 0x84 代表着 0-9 ,经过运算只有落在这部分区域里才有可能让 v54(=4) 与 0x84 与运算,才有可能不触发 break,所以这里的作用就是要求第三部分的所有字符都是数字字符

最后关键的一部分

直接分析可能很难理解,但是通过动态调试观察可以发现是先将数字字符转化为数值型数据,然后满足一个表达式才能输出 '}',这里我们用 z3 来计算 v3 的确定值

至此,我们就得到了最终的答案 11111suctf_ACEG_31415926

这题用了OLLVM混淆(控制流平坦化),我在分析前选择了基于angr框架的符号执行来实现去除控制流平坦化。

相关资料可以看:
bird 大佬最早发布的脚本 https://github.com/SnowGirls/deflat
后来有大佬改成 python3版本的 https://github.com/cq674350529/deflat
我直接拿来用的时候发现有一些小bug,fork了之后修正了小bug 对这道题的处理上不会再出现问题了 https://github.com/Pure-T/deflat

不过这题不用去除控制流平坦化也是可以做的,问题不大。

这是我去控制流平坦化之后的结果

去除的并不是很完美,还是有很多地方没有处理好,后来对比原程序有丢失一些信息,问题主要发生在去除控制流平坦化时,识别返回块的处理上,这里不展开。

先给了一个 md5 值

反查 md5 知道是 '#' 字符

这种写法应该是在暗示我第一个字符是 '#',之后还要输入20个字符,一共21个字符

在往后他记录了一个时间差

将开始输入前的时间和开始输入之后的时间差记录成了一个变量,后来我注意到在原程序中判断了这个时间差,若大于0就退出程序,在我这里被去除控制流平坦化的脚本给删去了。当时我选择先不管这个变量,往后看看,也许不影响解题。

再往后看,有一些多余的流程,忽略就好了。

这里获取了输入字符串的长度,并记录下长度是否等于 21

初始化 v19 = 1,紧接着一个大循环

注意到循环的条件是 v19 < 21,猜测这里 v19 代表的是当前处理的字符串中字符的下标,也就是说它是从字符串的第二个字符开始处理的,这与之前猜测的以 '#' 开头呼应了

这个循环里还是有冗余的代码,我们把它处理一下去除不会执行的地方

逻辑更加清晰了,在 do While 循环的最后,判断前面经过处理之后的结果 v18 是否等于 enc 表中的值,如果不等于就继续处理变化。换句话说,我们要控制我们的输入使得经过它规定的算法变化之后,等于 enc 表中的值,即可得到 flag

一个一个函数分析,我们随便输入 #input_flag_test_6789 调试看看

main::$_0 很显然就做了一件事情返回参数 a2,怕伪代码出错,我还特意看了一眼汇编

main::$_1 与 \$_0 一样

该函数取 a1 数组的第一个字符值与 a2 进行取模运算

这个函数看上去代码那么多。。其实都是混淆,真正执行的就一句话

这里的 v11 v12 就是传入的参数 a1 a2

下一个函数也是一样...

也是一样虚胖...看上去很复杂,实际上执行的就三句话

其实就是把第二个参数 a2 返回

这个函数也比较友好...异或一下

返回第二个参数 a2

第二个参数跟第一个参数的第一个字符相乘

最后给出一个 enc 数组

刚好 20 个值

将上面的函数逻辑整理一下,大概就是下面的代码,可能语法会有点问题不影响理解

由于这里 time 预期为 0 所以

已知 c 和 i 的情况下,很显然这个算法是可逆的

最后得到答案 #flag{mY-CurR1ed_Fns}

考察的是 unicorn CPU 模拟器

func 文件里放的是机器指令,babyunic 会借助 un.so.1 模拟执行 func 里的指令

给了一张常量表 unk_202020,与 s1 比较相等就说明输入的 flag 是正确的

将输入的内容传入 sub_CBA 处理

借助 uc_open 函数我们可以确定 func 的指令架构和位数

unicorn/include/unicorn/unicorn.h 位置处,我们可以找到

知道了三个参数的作用,我们再去找 archmode 的值定义

因此 uc_open 中的 3 对应的是 UC_ARCH_MIPS0x40000004 其实等于 0x40000000 + 0x4 对应 UC_MODE_MIPS32 + UC_MODE_BIG_ENDIAN

知道了架构和位数我们就可以反汇编它了,这里我借助一个神器 ghidra 直接反编译

由于代码量有点大就不全部贴出来了

这里第一个参数 param_1 是我们输入的字符串, param_2 最后得到的值要等于那个常量表

这里借助 z3 一把梭,不过要注意的是 enc 那个常量是以补码形式表示的

程序有反调试,nopcall cs:IsDebuggerPresent,还开了 ASLR,用 010editor 关闭 ASLR

调试到这,发现程序很多的字符串应该都是加密了,而 key 就是 byte_140015F20

sub_140009FF0 应该是一个输入函数,但是每次调试到那程序都自动退出,不知道是反调试还是本身有 bug

只好 nop 掉它,手工修改内存

第一段算法很好懂,逆向一下

得到一段字符串

继续调试然后程序执行到了

跟进 sub_140006C10 函数看看

发现这里对大量的数据进行解密操作

密钥就是我们刚才传入的字符串,这说明 sub_140006C10 是一个解密函数,交叉引用看下有三处调用,结合前面有一段创建三个空事件的代码,猜测可能是要进行三次解密之后将这块数据 dump 出来

调试到 sub_1400093B0 获取了当前进程的路径

并且读取了 filePath:signature 交换数据流

并且经过 md5 运算然后比较

反查 FCAEEB6E34B4303E99B91206BD325F2B 得到 Overwatch

添加交换数据流信息

还要注意在内存中把截断符加上,不然 md5 值不一样

最后一样也是会执行到解密函数 sub_140006C10

之后程序就开启 sleep 了,应该还遗漏了什么,回头去检查发现

这个函数启动了一个子线程,调试一下子线程看看它做了什么

在调试的过程中,要注意这里有个 TLS 回调函数

主要用来解密出三个函数的名字,由于这题开了多线程,TLS 回调函数会被多次执行。。我们在这里下个断点,让它只解密一次。。之后断到这里都直接修改 RIP 跳过

接着继续分析我们的子线程

这里建了一个循环获取所有进程名的 md5 跟常量表的 md5 比较,相同就退出程序,猜测是反调试。F9 一运行程序果然停止了,应该是检测到了 IDA 的进程 (我用 IDA 调试的)

看到后面又运行了

这里我直接修改 RIPsub_140006C10,执行解密函数,到这里应该要开始考虑解密函数的调用顺序问题,应该是子线程先执行,接着是 Akira_aut0_ch3ss_! 密钥解密,最后是数据流密码的解密。

由于子线程还没执行完,我们继续跟踪下去

这个函数传了一个全局变量,该全局变量指向 TLS 回调函数中解密出来的三个函数地址

NtQueryInformationProcess ZwQueryInformationThread NtQueueApcThread

单步进去发现各种反调试函数...

这里通过调用 NtQueueApcThreadsub_140009850 函数加入 APC 队列 (此处涉及 windows 内核理解不是很深,不到位的地方烦请大佬补充)

接下来进入了 sub_140009850

这里等待 5 秒,需要三个事件都处于激活状态才能往下执行。

到这里我选择重新调试,先暂停主线程,将子线程运行到 WaitForMultipleObjects 处,并暂停子线程,恢复主线程,再按照前面说的顺序把主线程的解密函数执行完,然后暂停主线程,恢复子线程,此时三个事件已经都激活了

往下调发现在校验文件头

接着就可以单步进入 sub_140007D80

这个函数粗略看下做了一些拷贝的工作和释放内存的操作,我们把解密出来的 DLL 导出,然后再拖入 IDA 分析

sub_7FFFDA782800 函数往里走会发现 AES S盒代换表,因此猜测是 AES 加密,并且密钥是 Ak1i3aS3cre7K3y 按这个密钥来看应该是 128 bit 的长度

密文在子线程中


[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2019-10-18 00:07 被PureT编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (2)
雪    币: 1664
活跃值: (403)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
为啥有些 md 失效了
2019-10-18 00:07
0
雪    币: 284
活跃值: (3579)
能力值: ( LV5,RANK:75 )
在线值:
发帖
回帖
粉丝
3
2019-10-18 11:56
0
游客
登录 | 注册 方可回帖
返回
//