65.9K
CodeProject 正在变化。 阅读更多。
Home

64 位环境中的代码混淆

starIconstarIconstarIconstarIconstarIcon

5.00/5 (12投票s)

2016年12月2日

CPOL

11分钟阅读

viewsIcon

24036

downloadIcon

495

64 位环境中的代码混淆

(注意:如果您无法从此链接下载源代码,您也可以在 SmidgeonSoft 网站上找到它 - 前往“新闻”页面,查找日期为 2016 年 2 月 14 日的条目。 源代码的更新版本(日期为 2016 年 12 月 11 日)已上传到我的网站并提供了链接 - HDSpoof 教程中发现的一种模式已被改编并整合到代码中,给调试器带来压力,使其更难逆向工程。)

引言

在我作为逆向工程师的职业生涯中,我曾多次被指派找出我的雇主的产品在存在第三方应用程序时神秘失败的原因。在无法访问原始源代码的情况下,我不可避免地会启动静态分析器和/或调试器,并开始单步执行二进制代码,试图理解它的作用以及它如何干扰我们产品的运行。在我的网站 www.smidgeonsoft.com 上,您可以找到关于其中两个事件的更详细文档(以及在项目创建过程中使用的两个实用程序:PEBrowse64 ProfessionalPEBrowseDbg64 Interactive)。

  • “逆向 HDSpoof - 教程”
  • “揭示 Yoda's Protector 中的资源泄漏”

另一个项目没有记录,因为它需要逆向安装程序的复制保护,但仍然为本文后面将要学习的内容提供了灵感。因此,由于这些冒险经历和其他经验,我坚信以下内容...

如果可以调试,就可以逆向!

我希望在本次讨论中通过检查一个示例程序来分享这个座右铭/信念,该程序采用了我在逆向工程冒险中发现的一些技巧和陷阱。上述项目都是 32 位挑战。我发现很少有关于如何在 64 位平台上实现其中一些技术的示例,因此,我提供了 SimplestWithSpeedBumps。您需要Visual Studio 12 或更高版本才能构建二进制文件。但是,源代码中的任何错误、失误或 bug 都由我本人负责 - 源代码按原样提供,可以在附件文件中找到,即SimplestWithFooSourceCode.zip

Simplest

本文中的主程序 Simplest 最初是尝试创建一个不包含任何导入(例如 Kernel32User32 等)的简单 Windows 程序。受另一来源的启发

我能够生成一个具有您在此图像中看到的结构的程序(使用PEBrowse64 Professional 显示,这是一个您可以从我网站的软件页面下载的静态分析器)。

任何对可移植可执行文件(Portable Executable files)有所了解并有调查未知可执行文件经验的人都可以看到,程序的结构看起来有些奇怪 - 没有导入目录或导入列表 - 甚至没有 NTDLL - 并且缺少一些节,例如 .reloc。运行原始程序会显示一个简单的消息框,显示“Ok”。但这需要一些技巧才能达到这个结果。加载所需的支持 DLL 并访问其 API 意味着在没有通常的 LoadLibraryGetProcAddress 机制的情况下开发代码。为了增加我给自己设定的挑战,我决定在代码库中添加一些我在以前的项目中看到过的内容,即,故意混淆代码,这将挫败任何静态分析的尝试。此外,这些混淆程序引入了旨在阻止调试器和任何可能用于“了解情况”的其他工具的技巧。这导致引入了结构化异常处理,这意味着我现在必须放弃在 Simplest 中不进行导入引用的目标。

除了来自Kernel32 的异常处理支持例程(我不太想自己创建)以及对CreateProcessW 的引用外,请注意神秘的二进制资源“BIN”以及更多目录的添加。顺便说一句,我在此阶段拒绝将程序重命名为 - Simple.EXE

Foo.exe

在深入研究 Simplest 的内部之前,让我们先简要了解嵌入在二进制资源“BIN”中的可执行文件 Foo.exe(以及在单独的 Foo 项目和源文件 Foo.cpp 中找到的)。Foo 最初由Visual Studio 向导生成,用于生成一个包含 MessageBox 调用程序,该程序会根据一个简单的反调试技巧显示“Okay”或“Fail” - 程序是独立启动的(可能是通过在之前的逆向会话后提取或保存二进制资源)还是由父程序 Simplest 启动的?两个连续调用 Toolhelp32 函数来确定父进程并返回一个 BOOL 值。虽然不是很复杂,但此代码和整个程序 Foo 是更复杂概念的替代品,需要混淆,例如保护复制保护、隐藏专有算法或屏蔽秘密代码/程序防御。在 Foo 中生成“Okay”将表示保护代码被击败,而“Fail”则表示代码未能成功逆向。

SimplestWithSpeedBumps 组织

Simplest(或 SimplestWithSpeedBumps)的设计目标有点“与众不同”。也就是说,在调试器外部启动 Simplest 将导致创建 Foo 进程并显示带有“Fail”字样的消息框。但是,在调试会话中启动 Simplest 将导致程序崩溃或(在成功导航了程序的技巧和陷阱之后)启动 Foo 程序并显示“Okay”字样。(WithSpeedBumps 后缀仅仅是一个异想天开的想法,承认我的目标是减缓逆向大师的速度。)

Simplest 的程序结构相当直接,尽管比普通 C++ 程序更复杂。有支持代码包含在三个单独的源文件中:SimplestPESupportSimplestCRTSimplestAntiDebug。第一个,SimplestPESupport,包含例程 my_GetModuleHandlemy_GetProcAddressmy_GetProcAddressEx 的实现 - 它们都是 Kernel32 API 中功能的替代品。这些函数部分依赖于定位程序环境块 (PEB) 的地址及其对加载器表地址的引用(其结构定义和描述可以在互联网上的许多地方找到)。SimplestCRT 的作用是响应对 string 比较和长度的请求。SimplestAntiDebug 包含三个反调试技巧,所有这些技巧都旨在检测进程中调试器的存在。

有一个“桥接”层,有助于将 C++ 代码与用 IA64 汇编语言编写的一些混淆代码连接起来。我在编写代码时发现的一件事是,Visual Studio 在某个时候放弃了对内联汇编指令的支持,即保留关键字 _asm,因此迫使像这样的代码包含在一个单独的源文件中(您可能需要更改项目中的目录指针以匹配您系统上的汇编器位置)。汇编器源中还有一系列回调,用于将控制权转移回执行“繁重工作”的 C++ 例程。这些回调特意编写得重复且乏味(除一个例外!),目的是增加混淆。

最后,C++ 层如前所述,执行繁重的工作,例如从资源节提取 Foo 程序,创建并写入临时独立文件,调用创建进程 API,删除临时文件,最后通过调用 CloseHandle 进行清理。在查看这里的源代码时,您可能会被一些技术所诱惑/启发,以增加更多障碍。尽管代码看起来效率不高,但请记住,目的是迷惑调试代码的人,并且大部分代码只会执行一次。

SimplestWithSpeedBumps 混淆

现在,我们可以进入本文的核心 - 代码混淆及其背后的思想。主入口点“让我们初步了解”并“摆好架势”。在启动时,代码会调用一个名为 get_peb 的例程 - 注意没有 WinMainCRTStartup 或其他类似代码。使用调试器单步进入该例程或仔细分析指令将向您展示执行的是以下内容。

00007FF7`544B3D0B  CALL 0x00007FF7544B57D0 - (Simplest.exe!get_peb)
00007FF7`544B57D0  PUSH RAX
00007FF7`544B57D1  NEG RCX
00007FF7`544B57D4  CALL 0x00007FF7544B57D9 - (Simplest.exe!get_peb + 0x9)
00007FF7`544B57D9  POP RAX
00007FF7`544B57DA  IMUL RAX,RAX,0x3
00007FF7`544B57DE  CALL 0x00007FF7544B57E7 - (Simplest.exe!get_peb + 0x17)
00007FF7`544B57E7  POP RAX
00007FF7`544B57E8  NOP 
00007FF7`544B57E9  JMP 0x00007FF7544B5808
00007FF7`544B5808  CALL 0x00007FF7544B5802 - (Simplest.exe!get_peb + 0x32)
00007FF7`544B5802  POP RAX
00007FF7`544B5803  CALL 0x00007FF7544B57FC - (Simplest.exe!get_peb + 0x2C)
00007FF7`544B57FC  POP RAX
00007FF7`544B57FD  CALL 0x00007FF7544B57F9 - (Simplest.exe!get_peb + 0x29)
00007FF7`544B57F9  POP RAX
00007FF7`544B57FA  JMP 0x00007FF7544B580D
00007FF7`544B580D  POP RAX                 (see 00007FF7`544B57D0)
00007FF7`544B580E  NEG RCX
00007FF7`544B5811  MOV RAX,RSI
00007FF7`544B5814  PUSH RDX
00007FF7`544B5815  MOV RDX,RAX
00007FF7`544B5818  NEG RAX
00007FF7`544B581B  IMUL RAX,RAX,0x3
00007FF7`544B581F  POP RAX                 (see 00007FF7`544B5814)
00007FF7`544B5820  MOV RAX,R8   <<<<<<<<<<<<<<<<<<<<<<<<<<<<<
00007FF7`544B5823  RET 

大多数指令都是无用的,除了对堆栈进行仔细的压栈和弹栈平衡外,唯一有意义的指令是返回前的那条指令,即返回 R8 的内容。获取 PEB 的地址允许后续调用 my_GetModuleHandle 等进行操作。R8 的选择实际上是后续所有内容中唯一脆弱的假设 - PEB 值的分配以及程序启动时的查找位置已经从早期版本的 Windows 发生了变化。

除了对 CreateProcess 的错误引用外,接下来的项目是访问冲突,以及所有剩余的代码 - 都包含在异常处理程序中!在生成初始访问冲突(任何严重错误都可以)并“处理它”之后,任何进一步未处理的异常都将导致程序崩溃。这会产生“紧张”,这是创建混淆代码的目标设计中的几个元素之一。以下是其他一些元素:

  • 紧张
  • 误导/欺骗
  • 疲劳/无捷径
  • 无聊
  • 消除字符串和其他有意义的数据片段,以及
  • 将有意义的值隐藏在显眼之处。

这些元素的出现可以在 Simplest 的源代码中找到。初始异常引入的紧张感通过代码中对诸如调试器和调试断点存在性等内容的许多检查而加剧,这些检查如果被错过或未被识别,将生成另一个访问冲突 - 这最初是对 callback0 的调用,后来被内联以避免通过 NOP(在调试会话期间修改代码)来移除该例程。任何一个都会最终导致程序崩溃。误导或欺骗的例子可以在初始错误的 CreateProcess 调用中找到,也可以在对 LdrUnloadDllNtCreateUserProcess 和其他进程创建 API 的断点检查中找到(主要是为了阻止那些知道第二个进程被创建并试图通过猜测进程创建位置来绕过逆向过程的人)。在单步执行看似无意义的代码和 DoCreateProcess 中发现的一些长循环时,会产生疲劳。或者,这可能是由于错过调试时需要的某个寄存器值更改,看到程序崩溃,然后不得不重新开始。callbackX 例程的单调乏味和无聊的外观不仅预示着即将从 C++ 代码转换,而且还保留了大量的堆栈区域,可能足以在堆栈垃圾的残骸中移除容易看到的线索。只有一个分配了不同的堆栈区域,但这只是在修复了因堆栈对齐引起的 NTDLL 中的实际崩溃后才添加的 - 有关更多信息,请参阅代码注释。已尝试避免使用 string 直到最后一刻,方法是逐个字符(有时按随机顺序)分配 Simplest 使用的 API 服务例程的名称。而且,当 my_GetProcAddressEx 返回地址时,为了避免早期检测,会对其值进行取反,以将其隐藏在“显眼之处”。

调试/逆向代码

在开发 Simplest 的过程中,我几乎完全使用了我自己的调试器PEBrowseDbg64 Interactive(可从我网站的软件页面获取)。事实上,我认为我的调试器在某些方面优于其他调试器,例如,尽可能显示上下文,智能反汇编代码等,这些都为我提供了进一步混淆代码的灵感。实际上,大多数重型调试器都可以做到这一点,即它们应该支持寄存器/内存内容操作以及重置/更改程序的执行路径。我还没有尝试用Visual Studio 调试程序,所以我无法说这个练习是否会成功 - 您的经验可能会有所不同。

结束语或如何改进混淆代码

除了源代码中的建议和想法外,几乎有无限的方法可以减缓他人单步执行代码并发现其秘密。最重要也是最容易的方法是增加代码中的噪声量,例如添加更多虚假和富有想象力的指令和序列。其中一些需要对汇编语言有深入的了解 - 例如,添加几个字节如何导致代码序列完全混乱 - HDSpoof 文章包含一些美观示例的参考。这种技术会干扰所有但最坚定的静态分析,几乎迫使使用调试器。代码自修改,例如简单地取反大段代码,然后在执行前反转该过程,是另一种可能性。混淆整个进程的启动,虽然难以调试,但会是一个吸引人的想法。例如,Simplest 可以启动自己的另一个副本,并测试代码是第一个还是第二个实例,并根据需要遵循一个代码路径或另一个。 通过测试调试器调试不同 PE 类型的能力,可以创建 Simplest 的 32 位和 64 位版本,其中 64 位版本启动 32 位版本并检查位性以确定代码中的关键路径。 而且,还有其他方法可以检查代码是否正在被调试,这些方法可以作为更多的反调试技巧添加。最终,您必须权衡额外的努力和回报,以及您的计划会被任何有决心的逆向工程师解码的事实。

通过本文,我认为我已经揭开了混淆代码的一些神秘和危险之处。即使不能激发您自己寻找新想法、技巧和陷阱,我也希望我已经说服了您我的开场白:

如果可以调试,就可以逆向!

© . All rights reserved.