首页
社区
课程
招聘
[翻译]利用PE文件映射库libpeconv来解决FlareOn4 CTF比赛的挑战题6
2017-12-27 10:54 4024

[翻译]利用PE文件映射库libpeconv来解决FlareOn4 CTF比赛的挑战题6

2017-12-27 10:54
4024

        最近我开始编写一个用于加载和映射转换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

        具体代码如下图所示:


        我们也可以通过命令行来完成同样的工作:

rundll32.exe payload.dll [func_name] [checked_str]

        结果如下图所示:


        太棒了,弹出了一个新的对话框。这貌似是一串关键字: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的位置处,其原型是:

__int64 __fastcall to_overwrite_mem(__int64 a1);

        让我们编写一个加载器来将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原型的函数:

peconv::get_exported_func(loaded_pe, MAKEINTRESOURCE(1));

        完整加载器的代码存放在以下网址处,其中我们使用了libPeConv库:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_basic_ldr/main.cpp

        很好,运行它的结果完成相同,如下图所示:


        如前所述,函数所使用的参数随当前年月而改变,比如对2017年12月来说,它的内容如下所示:

leggykickedflutters

        但是如果我们的加载器能够对其进行自动填充,那么会方便很多。

        这个参数必须和输出函数名称严格一致;我们可以利用这点,使用libPeConv库列举输出函数名称的功能来实现。具体代码如下图所示:


        加载器的改进版本在以下网址中:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_autofill_ldr/main.cpp

        程序运行正常(如下图所示):


        至此,测试和准备工作就完成了,接下来需要构建解决方案。


        操控块索引

        我们已经为开始操控索引做好准备了。有多种实现方法——其中之一是对导入函数GetSystemTime挂钩监控。但是利用libPeConv库我们也可以对本地函数进行挂钩,所以让我们采用更简单的方法来实现它。

        在payload.dll中可以找到计算索引的函数,它的RVA等于0x4710(如下图所示):


        我们将使用和之前所用到的完全相同的函数,来对静态链接的rand和srand函数进行重定向,即:

peconv::redirect_to_local64

        需要做的所有准备工作就是我们自己用于返回索引的函数。比如,我们可以强制返回一些硬编码的索引,具体代码如下图所示:


        程序成功运行(如下图所示)!


        但是每次想要改变索引我们都需要重新编译加载器,这并不是一个好主意。因此,我编写了一个加载器的改进版本,允许用户以所提交的参数为索引,查看相应位置的关键字块。加载器的代码存放在以下网址:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_basic_sol/main.cpp

        通过这种方式,我们可以逐一获取所有的关键字串片段;然而,我们仍然需要处理这些烦人的对话框。让我们替代它们,并把将要显示的信息重定向到我们自己的函数中。接下来我们将对一个动态链接的函数进行挂钩监控——因此,挂钩的安装过程会有一点不同。


        利用libPeConv库挂钩输入地址表(IAT)

        当libPeConv库加载可执行文件时,它在一个特殊专用类(即peconv::t_function_resolver)的帮助下对输入函数进行解析。除了默认选项,该库还允许使用非标准的解析器——唯一的要求是它们必须继承这个基类。

        完整软件包中包含多个解析器,其中之一是hooking_func_resolver。该类允许对IAT进行挂钩,基本流程是:在加载输入函数时,它可以将一些输入函数替换为我们自己的函数(唯一要求是它们必须有相同的API原型)。目前,该解析器支持对名称定义的函数进行替换。因此比如说,如果我们想要使用我们自己的my_MessageBoxA函数来替换MessageBoxA函数,具体代码如下:


        比如,我们可以使用如下函数来替换它:


        现在,不同于在对话框中显示十六进制的字符(如下图所示)……


        加载器将以ASCII字符形式显示相同的字符(如下图所示):


        完整加载器的代码在以下网址中:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_hooked_msgbox_sol/main.cpp

        我们可以利用一个小型的批处理脚本来使用它,脚本代码存放于以下网址:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_hooked_msgbox_sol/run.bat

        批处理脚本的具体代码如下:

@echo off
set loopcount=0
:loop
peconv_hooked_msgbox_sol.exe payload.dll %loopcount%
set /a loopcount=loopcount+1
if %loopcount%==26 goto exitloop
goto loop
:exitloop
echo.
pause

        最终,我们打印获取了完整的旗标,并且没有任何烦人的弹出窗口(如下图所示):


        应用程序的最终版本,将自动组合得到关键字串,代码网址如下:

        https://github.com/hasherezade/challs/blob/master/FlareOn2017/chall6/peconv_finished_sol/main.cpp


总结

        LibPeConv库是我的一个新工程,当前仍处于早期的开发阶段,因此其中很多内容可能会发生改变——但实践证明它对于解决一些挑战题是有用的。该库不仅能够为你提供将其他可执行文件中的代码导入自己工程的能力,而且还允许你对其进行挂钩和修改。我希望在该库的开发期间,大家能够试用并从中找到尽可能多的乐趣。我希望大家能够给予反馈!

        在本例中所用到的所有二进制程序的网址如下:

        https://drive.google.com/open?id=1Jh6fmi1fdaSRo7lSiSCY3X2Sd9UrQSS1

        压缩包的密码是“crackme”。


附录

        本题的其他一些解决方法,请参考如下网址:

        ·https://www.fireeye.com/content/dam/fireeye-www/global/en/blog/threat-research/Flare-On%202017/Challenge6.pdf

        ·http://blog.attify.com/2017/10/13/flare4-writeup-p2/

        ·https://vsec.com.vn/en/blogen/write-chal6-flare-on4.html





原文地址:https://hshrzd.wordpress.com/2017/12/01/hook-the-planet-solving-flareon4-challenge6-with-libpeconv/
本文由  看雪翻译小组 木无聊偶  编译
转载请注明出处

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

收藏
点赞1
打赏
分享
最新回复 (1)
雪    币: 1243
活跃值: (1815)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
库尔 2017-12-27 15:01
2
0
mark
游客
登录 | 注册 方可回帖
返回