题目作者: Matt Williams (@0xmwilliams)
翻译前言: 文章对代码自修改的分析很细致, 使用Unicorn框架来模拟执行代码和Capstone进行反汇编.
ps: 程序和脚本可以从附件下载, 程序可能会报毒但是安全的, 建议在虚拟机下操作, 解压密码: www.pediy.com
greek_to_me.exe
是一个Windows x86可执行文件, 如下图所示, 程序中的字符串表露了00401101
处要达成的情况, 如下所示.
然而, 在地址00401101
前面的汇编代码却包含如下所示的奇怪汇编指令
不过也许你在此时能准确地猜测到, 程序为了能达到地址0x401101
, 会修改这些奇怪的指令, 因为这些奇怪的指令运行下去, 我们的程序会极有可能崩溃. 另一种迹象能暗合我们认为这是代码自修改的推测, 那就是在查看程序的文件头时, 我们发现程序入口点所在的.text
区段是可写的. 到这里, 我们正常的套路就可以往上查看分析, 看是什么能让程序选择0x401063
的正确分支.
当然还有另外一种方法就是确定程序的套接字是在哪里生成的, 话不多说, 我们这就来尝试.
greek_to_me.exe
包含有0x401151
处的一个简单socket函数调用, 如下图所示
在sub_401121
里我们可以观察到, 程序用了一系列Windows API函数: socket,bind,listen和accept
创建了一个监听本地TCP端口2222(0x8AE)的套接字
程序一直等待着监听端口的连接, 直到从建立连接的客户端那接收到最多4个字节. 接收到的字节会存储在缓冲区中并以参数的形式传递给sub_401121
. 一旦有接收到字节, 该函数就能在 不停止现有连接的情况下返回一个socket句柄. 要记住, 当执行到0x401071
或0x401101
时, 程序就会使用到它.
如果sub_401121
返回了一个合法的socket句柄, 程序会继续执行, 否则程序退出. 如下代码块为寄存器赋初值, 这几个寄存器将在解码循环中发挥用处
我们看这段代码, 首先, 一个位于.text
区段的可执行的代码地址赋值给ECX
寄存器, 并且加上了常量值79h
, 这也表明了随后将介绍的解码循环里的终止地址. 地址0x40107C
赋给EAX寄存器, 代表解码循环的起始地址. 在0x401036
, recv
缓冲区的第1个字节被赋给了EDX
寄存器的低8位
继续向下看代码块, 其中包含一个进行如下操作的循环
在EAX
中存储的地址则自增1并且跟ECX
中存储的最大地址进行比较, 只有当EAX
的内容跟最大地址0x4010F5
相等时循环才会结束.
继续向下看, 程序随后便将刚刚修改了的代码块首地址(0040107C
)和块大小0x79
作参数传递给sub_4011E6
.
我们可以看到程序返回值的低16位(AX
)赋给了EAX
寄存器再将EAX
跟硬编码值0xFB5E
进行比较, 而比较的结果则决定了程序是跳向0x40107C
还是执行到显示失败信息的分支
获得这些信息, 我们可以做出正确假设: sub_4011E6
是用来计算验证值或者说是之前解码循环所修改的字节的校验值. 并且可以确定从socket接受到的字节值是用作异或修改0x40107C
和0x4010F4
之间代码块的key值. 而程序自修改的代码则通过一个硬编码的校验值进行验证. 因为使用的key只有单字节, 因此我们可以进行简单的暴力穷举来获得期望的key.
如果修改后的代码正常执行并且通过socket返回了Congratulations
字符串, 那么就可以确定暴力穷举成功了. 基于这个假设, 我们可以编写一个如下的脚本代码帮助输出正确值:
但如果我们并不想基于解码的字节都正确执行这样一个假设来操作, 而是自己验证解码后的校验值是否匹配, 要怎么办呢? 相比花大量时间逆向校验算法, 这次我们来尝试体验一个有趣的恶意代码分析技术: 代码模拟执行
首先, 我们提取校验函数sub_4011E6
的操作码, 我们只关心在0x401265
执行完后存储在AX
中的返回值, 如下图所示. 并且不需要提取函数的平衡栈的结尾部分.
我们同样也需要从0x40107C
处提取0x79
长度的待解码字节. 我们提取的字节集合都在如下的用于模拟执行的python简易脚本中可见
如下的代码定义了一个函数, 给定函数一个0x00
到0xFF
之间的值, 就能执行原来程序的解码操作.
接下来, 我们定义一个函数, 函数在给定待解码字节后会利用Unicorn
框架来模拟执行校验值函数
如上的代码中初始化了一个32位的x86模拟器, 随后创建了一个用于存储校验函数代码, 函数内部栈以及待解码字节的2MB内存. 校验代码和待解码字节可以写入内存范围的任意地址里.
校验值函数从栈上取两个参数: 待解码字节的起始地址(0x40107C
)以及长度(0x79
), 下图显示了校验值函数调用后的状态.
为了能让校验值函数能在模拟时正确执行, 我们还需要对栈进行设置来匹配上图的栈空间布局, 并适当填充ESP寄存器. 如下所示, 在模拟执行结束后, 我们可以从emulate_checksum
返回计算后的校验值
现在到轻松的部分了. 我们暴力穷举异或的key, 解码字节并模拟校验操作, 然后确定哪一个key能获得正确的校验值. 如下所示
运行脚本最后打印出正确的单字节值: 0xA2
. 然而我们仍然不明白解码后0x40107C
处的指令干了什么. 我们来尝试使用Capstone
反汇编器来反汇编这些指令, 如下所示
运行我们的脚本并提供指令, 结果如下所示
我们可以看出两点, 首先栈上正在填充成一个字符串, 其次, 填充到栈上的常量十六进制值在可显字符范围内(0x20~0x7E
). 依据它们在栈上的顺序依次提取出这些可显字符, 或者你可以用调试器观察栈上的内容, 得到题目解答: et_tu_brute_force@flare-on.com
.
以下附上python脚本
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)