最近我开始编写一个用于加载和映射转换PE文件的小型库(libpeconv,在我的GitHub代码库中存放了该库的早期测试版本,网址:https://github.com/hasherezade/libpeconv)。在之前的文章中,我演示了如何在该库的辅助下解决FlareOn4 CTF比赛的挑战题3(网址:https://hshrzd.wordpress.com/2017/11/24/import-all-the-things-solving-flareon-challenge-3-with-libpeconv/):我利用libPeConv库将函数从原始的crackme程序中导入,实行函数的本地可用性,而不需要重新实现或模拟。
这次,我们将更深入地研究FlareOn4比赛(网址:https://www.fireeye.com/blog/threat-research/2017/10/2017-flare-on-challenge-solutions.html)的挑战题6。这道挑战题相比而言更困难一点,因此这是个好机会来展示libPeConv库的其他一些功能——不仅是导入函数,还可以以多种方式来对导入代码进行拦截挂钩。
在FlareOn比赛期间我第一次分析这个crackme程序时,我所使用的方法非常简陋粗暴——我需要仔细分析26个对话框,记录每个值,将它们从十六进制转换为ASCII码字符,然后将它们组合得到完整的旗标——我的天呐!之后阅览相关的报告,我发现大部分人都是这样做的(更多细节请参考附录)。但是,我确信肯定有更好的解决方案——在libPeConv库的辅助下,最终我以我所期望的方式完成了这项工作:无需点击任何弹出窗口,加载器将自动组合得到旗标。
最终结果如下图所示:
存放完整发布版的加载器(代码+编译得到的二进制程序)的代码储存库网址:https://github.com/hasherezade/challs/tree/master/FlareOn2017/chall6。
最终版加载器的完整代码网址:https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_finished_sol/main.cpp
本文中,我将详细讲解构造过程并演示实验过程,以及这样做的推理过程。
所用到的工具
为了分析该crackme程序,我们需要:
·IDA + HexRays反汇编工具(可选)
·PE-bear工具(网址:https://hshrzd.wordpress.com/pe-bear/)
·hook_finder,用于转储修改后的PE文件(网址:https://github.com/hasherezade/hook_finder)
为了构建解决方案,我们需要:
·Visual Studio + CMake(网址:https://cmake.org/download/)
概述
这个名为“payload.dll”的挑战题程序是一个64字节的PE文件。当查看它的输出表时,我们可以看到它导出了一个函数,名为“EntryPoint”,如下图所示:
但是如果我们尝试以“rundll32.exe payload.dll,EntryPoint”的传统方式来运行它,结果返回“无法找到该函数”,如下图所示:
太奇怪了~让我们试一下通过序号来运行它,即“rundll32.exe payload.dll,#1”,结果如图所示:
这种方法生效了——然而离获取旗标的目标还很远。
奇怪的是为什么导出函数无法通过名称找到呢?在加载DLL文件的过程中,导出(函数)名称似乎被重写了。我们可以使用hook_finder工具(通过对比磁盘上的PE文件来检测PE文件在运行过程中是否被修改)来快速检查是否发生了重写操作。
我再次通过序号来调用函数,并且当对话框弹出时,我利用hook_finder工具来扫描rundll32.exe的运行时进程。我们可以看到,DLL文件确实被重写了,具体输出信息如下图所示:
hook_finder工具已经将修改后的镜像自动转储,因此我们可以通过传统工具来打开它。首先,我使用PE-bear工具来查看输出表,如下图所示:
好吧,现在它看起来很不同~让我们在IDA工具中观察该函数。
通过深入分析我们可以确定,这个就是用于显示之前所见信息的函数,具体代码如下图所示:
当我们不使用任何参数来调用函数时,函数将显示该信息。相反,如果以适当的参数来调用它,代码的某些后续环节将被解密执行,具体代码如下图所示:
函数名称将被用作解密密钥。问题是,所用的参数必须满足的条件是什么呢?
导出函数需要4个参数,具体如下图所示:
如下图所见,函数检查的参数是所使用的第三个参数:
函数将第三个参数与函数名称进行比较:如果它与函数名称完全一致,解密过程就开始执行;否则,函数将显示失败对话框(“Insert clever message…”)。
让我们使用适当的参数来运行函数,并观察发生了什么。
以下网址存放了一个小型包装器,用来协助我们从自己的代码中调用该函数:
https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/basic_ldr/main.cpp
具体代码如下图所示:
我们也可以通过命令行来完成同样的工作:
结果如下图所示:
太棒了,弹出了一个新的对话框。这貌似是一串关键字:0x75 -> ASCII码字符‘u’;但这个只是其中一个字符,我们必须获取完整的关键字串。
出于这个目的,我们将深入分析DLL文件,来找出用于重写输出表的代码。这个函数起始于相对虚拟地址(RVA)0x5D30处,汇编代码如下图所示:
函数的伪代码如下图所示:
它对给定索引所指向的代码块进行解密,并重定向到新的导出函数。我们想要操控索引,来获取剩余的所有关键字串片段。代码块索引是基于当前时间来计算得到的,具体代码位于函数内部RVA等于0x4710的位置,如下图所示:
我们可以看到模26的操作,这意味着26就是最大值;即,有26个可能的索引指向26个关键字串的片段。然后,计算得到的索引在解密函数中使用。解密函数很简单,就是基于异或操作,具体代码如下图所示:
首先,随机数发生器基于所用到的代码块索引加上一个常数进行初始化;然后rand()函数生成的伪随机数值,作为异或密钥使用。由于这个(弱)随机数生成器特性的存在,值并不是真正随机的——相同的种子总是生成相同的序列,因此密钥能够正常生效。
解决这个题目的最简单(也是非常繁琐)实现方法是,不停改变系统时间,运行DLL文件,并记录弹出的关键字串片段。听起来太Low了?让我们看看对此libPeConv库能够做什么~
利用LibPeConv库来导入并挂钩函数
在之前的文章(网址:https://hshrzd.wordpress.com/2017/11/24/import-all-the-things-solving-flareon-challenge-3-with-libpeconv/)中,我对PeConv库做了一些概述,因此如果你没有阅读那部分内容,请去看一看。这次,我将用到之前所介绍的功能,以及额外的一些功能。我们不仅要导入并使用原始crackme程序中的代码,而且还要将它与我们自己的代码组合,来改变一些行为。
首先,我想要从crackme程序中导入重写输出函数的函数;这个函数在RVA等于0x5D30的位置处,其原型是:
让我们编写一个加载器来将crackme程序加载到当前进程中。加载器的第一个版本如下图所示:
一切看起来都很好,理应正常工作;但当我运行加载器时,它给了我一个不太愉快的惊喜,错误对话框如下图所示:
在尝试调试代码时,我们会发现异常是从静态链接的函数srand()和rand()中抛出的。在从payload.dll导入的函数to_overwrite_memory内调用的函数dexor_chunk_index中,我们要用到它们。
这可能是由于payload.dll是手动加载而不是通过Windows系统的加载器所加载的,因而某些低层次的结构体没有填充。静态链接的srand()和rand()函数尝试访问无效地址;但这很容易修复——我们可以简单地将调用过程重定向到与驻留加载器的函数相同的副本上。
首先,我们需要获取payload.dll中的函数所在的地址:
srand函数的RVA等于0x7900(如下图所示):
而rand函数的RVA等于0x78D4(如下图所示):
下一步将它们重定向到本地副本;我添加了几行代码来完成这项工作,具体代码如下图所示:
现在整个过程成功执行而无任何崩溃发生。如果想要使用日志记录随机数值,而不是重定向到原始函数,我们可以重定向到我们自己的包装器中,比如(如下图所示):
与之前的静默执行不同,上述代码将打印每次调用rand函数所生成的随机数值(如下图所示):
现在,让我们测试一下输出函数是否被成功重写了。如果是,那么我们应该可以通过与之前的基本加载器(网址:https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/basic_ldr/main.cpp)类似的工作方式来使用它。
不同于针对以传统方式加载的模块所使用的GetProcAddress函数,我使用PeConv库中一个有着类似API原型的函数:
完整加载器的代码存放在以下网址处,其中我们使用了libPeConv库:
https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_basic_ldr/main.cpp
很好,运行它的结果完成相同,如下图所示:
如前所述,函数所使用的参数随当前年月而改变,比如对2017年12月来说,它的内容如下所示:
但是如果我们的加载器能够对其进行自动填充,那么会方便很多。
这个参数必须和输出函数名称严格一致;我们可以利用这点,使用libPeConv库列举输出函数名称的功能来实现。具体代码如下图所示:
加载器的改进版本在以下网址中:
https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_autofill_ldr/main.cpp
程序运行正常(如下图所示):
至此,测试和准备工作就完成了,接下来需要构建解决方案。
我们已经为开始操控索引做好准备了。有多种实现方法——其中之一是对导入函数GetSystemTime挂钩监控。但是利用libPeConv库我们也可以对本地函数进行挂钩,所以让我们采用更简单的方法来实现它。
在payload.dll中可以找到计算索引的函数,它的RVA等于0x4710(如下图所示):
我们将使用和之前所用到的完全相同的函数,来对静态链接的rand和srand函数进行重定向,即:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)