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 在文档中的定义是这样的:
该函数将计算 base 的 exp 次方,并对 mod 取模,最后将结果写入 rop 中。
这种计算与RSA中的加密过程如出一辙。
再往下就是关键比较函数 __gmpz_cmp
程序中比较之前 mpz_powm 运算的结果与程序中硬编码的值是否相等,如果相等则输出 tql。看到这里应该可以基本确定这是一道已知密文求解RSA明文的题目。
根据RSA的实现过程,首先来计算密钥。第一步是获得大整数 N ,根据程序可得
值得注意的是,这里的 N 是十进制的。
接下来对它进行大整数的因数分解,这里借助 yafu 工具。
至此我们得到了 p 和 q
再从程序中得知 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
位置处,我们可以找到
知道了三个参数的作用,我们再去找 arch
和 mode
的值定义
因此 uc_open 中的 3 对应的是 UC_ARCH_MIPS
,0x40000004
其实等于 0x40000000 + 0x4
对应 UC_MODE_MIPS32 + UC_MODE_BIG_ENDIAN
知道了架构和位数我们就可以反汇编它了,这里我借助一个神器 ghidra
直接反编译
由于代码量有点大就不全部贴出来了
这里第一个参数 param_1
是我们输入的字符串, param_2
最后得到的值要等于那个常量表
这里借助 z3 一把梭,不过要注意的是 enc 那个常量是以补码形式表示的
程序有反调试,nop
掉 call 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 调试的)
看到后面又运行了
这里我直接修改 RIP
到 sub_140006C10
,执行解密函数,到这里应该要开始考虑解密函数的调用顺序问题,应该是子线程先执行,接着是 Akira_aut0_ch3ss_!
密钥解密,最后是数据流密码的解密。
由于子线程还没执行完,我们继续跟踪下去
这个函数传了一个全局变量,该全局变量指向 TLS
回调函数中解密出来的三个函数地址
NtQueryInformationProcess
ZwQueryInformationThread
NtQueueApcThread
单步进去发现各种反调试函数...
这里通过调用 NtQueueApcThread
将 sub_140009850
函数加入 APC
队列 (此处涉及 windows 内核理解不是很深,不到位的地方烦请大佬补充)
接下来进入了 sub_140009850
这里等待 5 秒,需要三个事件都处于激活状态才能往下执行。
到这里我选择重新调试,先暂停主线程,将子线程运行到 WaitForMultipleObjects
处,并暂停子线程,恢复主线程,再按照前面说的顺序把主线程的解密函数执行完,然后暂停主线程,恢复子线程,此时三个事件已经都激活了
往下调发现在校验文件头
接着就可以单步进入 sub_140007D80
了
这个函数粗略看下做了一些拷贝的工作和释放内存的操作,我们把解密出来的 DLL
导出,然后再拖入 IDA
分析
sub_7FFFDA782800
函数往里走会发现 AES
S盒代换表,因此猜测是 AES
加密,并且密钥是 Ak1i3aS3cre7K3y
按这个密钥来看应该是 128 bit 的长度
密文在子线程中
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2019-10-18 00:07
被PureT编辑
,原因: