Andrew Davis最近发布了他的新Windows模拟框架,名为Speakeasy。本文将强调该框架的另一个强大用途:自动大规模分析恶意程序。我将通过代码示例演示如何通过编程方式使用Speakeasy:
- 绕过不受支持的Windows API以继续模拟和分析;
- 使用API挂钩保存动态分配的代码的虚拟地址;
- 使用代码挂钩以外科方式直接执行代码的关键区域;
- 从模拟器内存中转储分析的PE并修复其节头;
- 通过查询Speakeasy以获得符号信息来帮助重建导入表;
初始设置
与Speakeasy交互的一种方法是创建Speakeasy类的子类。图1显示了Python代码段,它设置了一个类,这个类将在后面的示例中扩展。
创建一个Speakeasy子类
图1中的代码接受一个Speakeasy配置字典,该字典可用于覆盖默认配置。 Speakeasy附带了几个配置文件。 Speakeasy类是基础模拟器类的压缩器类。根据二进制文件的PE标头加载二进制文件或将其指定为shellcode时,会自动选择模拟器类。子类化Speakeasy使访问,扩展或修改接口变得容易,它还有助于在模拟期间读取和写入状态数据。
模拟二进制
图2显示了如何将二进制文件加载到Speakeasy模拟器中。
将二进制文件加载到模拟器中
load_module函数为磁盘上提供的二进制文件返回PeFile对象。它是在speakeasy/windows/common.py中定义的PeFile类的实例,该类是pefile的PE类的子类,或者,你可以使用data参数而不是指定文件名来提供二进制字节。图3显示了如何模拟已加载的二进制文件。
启动模拟
API挂钩
Speakeasy框架提供了对数百个Windows API的支持,并经常添加更多的API。这是通过定义在speakeasy/winenv/ API目录的适当文件中的Python API处理程序来实现的。当在模拟过程中调用特定的API时,可以安装API挂钩以执行自己的代码。可以为任何API安装它们,而不管是否存在处理程序。API挂钩可用于覆盖现有处理程序,并且可以选择从你的挂钩中调用该处理程序。 Speakeasy中的API挂钩机制提供了灵活性和对模拟的控制。让我们研究一下在模拟分析代码以检索分析的有效载荷的上下文中API挂钩的几种用法。
绕过不受支持的API
当Speakeasy遇到不受支持的Windows API调用时,它将停止模拟并提供不受支持的API函数的名称。如果所讨论的API函数对于分析二进制文件不是至关重要的,则可以添加一个API挂钩,该挂钩仅返回允许执行继续的值。例如,最近的样本的分析代码包含对分析过程没有影响的API调用。这样的API调用之一就是GetSysColor,为了绕过此调用并允许执行继续,可以添加一个API挂钩,如图4所示。
添加的API挂钩
根据MSDN,此函数采用1参数,并返回表示为DWORD的RGB颜色值。如果要挂接的API函数的调用约定不是stdcall,则可以在可选的call_conv参数中指定调用约定。调用约定常量在speakeasy/common/arch.py文件中定义。因为GetSysColor返回值不会影响分析过程,所以我们可以简单地返回0。图5显示了图4中指定的getsyscolor_hook函数的定义。
GetSysColor挂钩返回0
如果API函数需要更精细的处理,则可以实现更具体,更有意义的挂钩,以满足你的需要。如果你的挂钩实现足够稳定,则可以考虑将其作为API处理程序添加到Speakeasy项目!
添加API处理程序
在speakeasy / winenv / api目录中,你将找到usermode和kernelmode子目录,这些子目录包含对应二进制模块的Python文件。这些文件包含每个模块的API处理程序。在usermode / kernel32.py中,我们看到为SetEnvironmentVariable定义的处理程序,如图6所示。
SetEnvironmentVariable的API处理程序
处理程序以函数修饰符(第1行)开头,该修饰符定义API的名称及其接受的参数数量。在处理程序开始时,最好的做法是将MSDN记录在案的原型作为注释(第3-8行)。
处理程序的代码首先将argv参数的元素存储在以相应API参数(第9行)命名的变量中,处理程序的ctx参数是一个字典,其中包含有关API调用的上下文信息。对于以“A”或“W”结尾的API函数(例如,CreateFileA),可以通过将ctx参数传递给get_char_width函数(第10行)来检索字符宽度。然后可以将此宽度值传递给read_mem_string之类的调用(第12和13行),该调用在给定地址读取模拟器的内存并返回一个字符串。
最好的作法是用相应的字符串值(第14和15行)覆盖argv参数中的字符串指针值。这使Speakeasy能够在其API日志中显示字符串值而不是指针值。为了说明更新argv值的影响,请检查图7中所示的Speakeasy输出。在VirtualAlloc条目中,符号常量字符串PAGE_EXECUTE_READWRITE替换值0x40。在GetModuleFileNameA和CreateFileA条目中,指针值被替换为文件路径。
Speakeasy API日志
保存分析的代码地址
压缩样本通常使用VirtualAlloc之类的函数来分配用于存储分析样本的内存。捕获分析后的代码的位置和大小的有效方法是首先挂接分析存根使用的内存分配函数。图8显示了挂钩VirtualAlloc以捕获虚拟地址和API调用分配的内存量的示例。
VirtualAlloc挂钩可以保存内存转储信息
图8中的挂钩在第12行调用Speakeasy的VirtualAlloc的API处理程序,以分配内存。 API处理程序返回的虚拟地址将保存到名为rv的变量。由于VirtualAlloc可用于分配与分析过程无关的内存,因此在第13行使用附加检查以确认截获的VirtualAlloc调用是分析代码中使用的调用。根据先前的分析,我们正在寻找一个VirtualAlloc调用,该调用将接收lpAddress值0和flProtect值PAGE_EXECUTE_READWRITE(0x40)。如果存在这些自变量,则虚拟地址和指定的大小将存储在第15行和第16行,因此在分析代码完成后,可以将它们用于从内存中提取分析的有效载荷。最后,在第17行,该挂钩返回VirtualAlloc处理程序的返回值。
使用API和代码挂钩的外科式代码模拟
Speakeasy是一个强大的模拟框架,但是,你可能会遇到包含大量有问题代码的二进制文件。例如,一个示例可能会调用许多不受支持的API,或者只是花费太长时间而无法进行模拟。在以下情形中描述了克服这两个挑战的示例。
取消隐藏在MFC项目中的存根
一种用于掩盖恶意有效载荷的流行技术涉及将它们隐藏在大型的开源MFC项目中。 MFC是Microsoft Foundation Class的缩写,它是用于构建Windows桌面应用程序的流行库。这些MFC项目通常是从流行的网站(例如Code Project)中任意选择的,尽管MFC库使创建桌面应用程序变得容易,但是MFC应用程序由于其大小和复杂性而难以进行反向工程。由于它们调用许多不同的Windows API的大型初始化例程,因此特别难以模拟。以下是对我使用Speakeasy编写Python脚本以自动分析自定义压缩程序的经验的描述,该自定义压缩程序将其分析存根隐藏在MFC项目中。
对压缩程序进行反向工程后发现,在CWinApp对象的初始化过程中最终会调用分析存根,该过程在C运行时和MFC初始化之后发生。尝试绕过不受支持的API之后,我意识到,即使成功,模拟也将花费太长时间而无法实现。我考虑过完全跳过初始化代码,然后直接跳转到分析存根。不幸的是,为了成功模拟分析存根,需要执行C运行时初始化代码。
我的解决方案是在代码中确定在C运行时初始化后落在MFC初始化例程中的早期位置,检查完图9所示的Speakeasy API日志后,很容易发现该位置。与图形相关的API函数GetDeviceCaps在MFC初始化例程的早期被调用。这是基于以下事实推论得出的:1.MFC是与图形相关的框架,2.在C运行时初始化期间不太可能调用GetDeviceCaps。
在Speakeasy API日志中标识MFC代码的开头
为了在此阶段拦截执行,我为GetDeviceCaps创建了一个API挂钩,如图10所示。该挂钩确认该函数在第2行中首次被调用。
GetDeviceCaps的API挂钩集
第4行显示了使用Speakeasy类的add_code_hook函数创建代码挂钩,代码挂钩允许你指定在模拟每个指令之前调用的回调函数。 Speakeasy还允许你通过指定begin和end参数来指定代码挂钩对其有效的地址范围。
在第4行上添加代码挂钩之后,GetDeviceCaps挂钩完成,并且在执行示例的下一条指令之前,将调用start_unpack_func_hook函数。此函数如图11所示。
更改指令指针的代码挂钩
代码挂钩接收模拟器对象,当前指令的地址和大小以及上下文字典(第1行)。在第2行,代码挂钩将自身禁用。因为代码挂钩是与每个指令一起执行的,所以这会大大降低模拟速度。因此,应谨慎使用并尽快将其禁用。在第3行,该挂钩计算了分析函数的虚拟地址。使用正则表达式定位用于执行此计算的偏移量。为了简洁起见,省略了该示例。
self.module属性先前是在图2所示的示例代码中设置的。它是从pefile的PE类中继承而来的,它使我们可以在第3行访问有用的函数,例如get_rva_from_offset()。该行还包括一个使用self的示例。 .module.get_base()以检索模块的基本虚拟地址。
最后,在第4行,使用set_pc函数更改了指令指针,并在分析代码处继续进行模拟。图10和图11中的代码段使我们能够在C运行时初始化完成后将执行重定向到分析代码,并避免使用MFC初始化代码。
发布和修复未压缩的PE
一旦模拟达到了分析样本的原始入口点,就该删除PE并对其进行修复了。通常,一个挂钩会将分析的PE的基址保存在该类的属性中,如图8的第15行所示。如果分析的PE在其PE标头中未包含正确的入口点,则真实入口点也可能需要在模拟过程中捕获。图12显示了如何将模拟器内存转储到文件的示例。
转储未压缩的PE
如果要转储已加载到内存中的PE,则由于节对齐方式的不同,其布局将与磁盘上的布局不同。因此,可能需要修改转储的PE头。一种方法是修改每个部分的PointerToRawData值以匹配其VirtualAddress字段。为了符合PE可选标头中指定的FileAlignment值,可能需要填充每个部分的SizeOfRawData值。请记住,生成的PE不太可能成功执行。但是,这些努力将使大多数静态分析工具能够正确运行。
修复转储的PE的最后一步是修复其导入表,这是一个很复杂的任务,在此不详细讨论。但是,第一步涉及在模拟器内存中收集库函数名称及其地址的列表。如果知道分析程序存根使用GetProcAddress API来解析分析的PE的导入,则可以调用get_dyn_imports函数,如图13所示。
查看动态导入
否则,你可以通过调用get_symbols函数来查询模拟器类以检索其符号信息,如图14所示。
从模拟器类检索符号信息
此数据可用于发现未压缩PE的IAT并修复或重建其导入相关表。
总结
编写Speakeasy脚本来分析恶意程序样本可以分为以下步骤:
(1) 对分析存根进行反向工程,以识别:
- 分析的代码将驻留在哪里或分配其内存的位置;
- 执行转移到分析的代码的位置;
- 任何可能引入问题的有问题代码,例如不受支持的API,缓慢模拟或反分析检查。
(2) 如有必要,请设置挂钩以绕过有问题的代码;
(3) 设置一个挂钩以标识虚拟地址,以及可选的分析二进制文件的大小;
(4) 设置一个挂钩,以在分析代码的原始入口点执行时或之后停止模拟;
(5) 收集Windows API的虚拟地址并重建PE的导入表;
(6) 修复PE的标头,并将字节写入文件中以进行进一步分析;
(7) 有关分析UPX样本的脚本的示例,请在Speakeasy存储库中查看UPX分析脚本。
Speakeasy框架提供了一个易于使用、灵活且函数强大的编程界面,使分析人员能够解决诸如分析恶意程序之类的复杂问题。使用Speakeasy自动化这些解决方案可以使它们大规模执行。
本文翻译自:
https://www.fireeye.com/blog/threat-research/2020/12/using-speakeasy-emulation-framework-programmatically-to-unpack-malware.html