变形引擎






4.82/5 (7投票s)
应用程序如何进行变质以适应并“在恶劣”环境中生存。
在此处下载源代码
引言
变质技术旨在使应用程序能够在不偏离既定目标的情况下物理地更改其代码。这意味着应用程序的实际代码和结构是动态的并且会发生变化,但应用程序想要做的事情保持不变。例如,如果您的程序目标是执行某些数学运算或管理 I/O 流,那么它将继续执行这些操作并产生相同的结果……但它实现该目标的方式会改变。
为什么应用程序要进行这种自我转换?好吧,这项技术是在几年前(实际上是很久以前)开发的,主要是为了欺骗杀毒软件。这实际上是使病毒应用程序的危害程度提高到更高水平的技术之一,因为它基本上使杀毒软件签名数据库失效,而这些数据库是用于定位并随后隔离或删除您 PC 上病毒感染的最重要信息。与多态引擎(请参阅我在此处的介绍)结合使用,它们是允许您的应用程序在操作系统上未被检测到地运行的实际方法。
背景
本文中发现的所有概念都面向高级用户;使用这项技术而对 CPU 指令集缺乏深入了解,很可能会导致您的应用程序完全失败。C 编程技能是必须的,因为这是开发引擎所选的语言(实际上是我最喜欢的语言,也是我编程的首选)。熟悉常见的构建工具(如 GCC 和链接器)、汇编器(本文中的 NASM)以及 Linux 命令行提供的其他实用程序(objdump 或 readelf)是强制性的。
您还必须了解应用程序是如何编译和打包以便在操作系统中加载和运行的。这意味着理解 PE 和 ELF 文件格式,分别用于 Windows 和 Linux 平台。由于此处开发的技术适用于 x86 处理器系列,因此基本上所有使用此平台的操作系统都可能受到以下软件的影响。
一如既往,我将尽量使应用程序内部的描述尽可能简单,以便更广泛的读者能够理解以下概念。
警告:需要谨慎。
这些技术实际上可以嵌入到任何应用程序中,以对代码片段、整个应用程序以及引擎本身进行变质处理。正如我所说,这些技术主要涉及恶意软件开发,但也可以更广泛地用于好坏目的。由您决定是要朝一个方向发展还是另一个方向。您可以设计能够“藏身于寻常之中”的应用程序,也可以设计能够逃避恶意软件检测和销毁的杀毒软件实用程序(一些病毒会禁用杀毒软件,或使其无效)。
基本上,接下来几章介绍的内容与现实世界中的武器类似,尤其是在当今的“虚拟战争”时代。
变质是如何工作的?
变质引擎的主要目标是更改应用程序某些区域例程代码的部分,使其“形状”发生变化,但工作内容不变。由于这个概念通常不是一目了然的,让我们用一个比喻来介绍它:自然语言中的变质。在英语句子上应用变质意味着找出一些同义词,用它们来改变句子的形状,同时保持其意义不变。
例如,如果我们看下面的句子
The device will operate under water
并且我们用规则指示我们的变质引擎
device <--> apparatus
句子转换后,我们最终得到的结果将是
The apparatus will operate under water
现在请注意,即使句子的单词发生了变化,意义仍然保持不变。我不是一个母语为英语的作家/说话者,所以请尝试理解上面示例背后的概念;我很难玩弄比喻或英语语法。
如果我们将前面的句子转译为 CPU 指令,即每个单词都是处理器单元可以理解和执行的操作码,那么最终得到的结果将是以下格式
Thedevicewilloperateunderwater
这是因为 CPU 不需要任何空格,因为它们的词汇量比人类少得多。每个单词都精确地完成一项工作,最终结果是对寄存器执行操作。这是 CPU 指令集实际工作方式的一个粗略图景,我已经大大降低了其复杂性。这使我能够引入 CPU 指令和变质相关的以下重要问题:原始代码的对齐丢失。
丢失与原始代码的对齐意味着从句子的错误起点开始考虑第一个可执行操作码。例如,如果从句子中删除了初始的“T”字母,我们将找到
hedevicewilloperateunderwater
然后将其转译为以下句子
hed evicew illo perateu nderw ater
正如您的大脑,经过数百万年进化,会意识到某些地方不对劲,所有单词都“偏移”了一位,而 CPU 不会。它实际上会尝试运行“hed”操作,在最坏的情况下会导致异常。此时,操作系统通常会通过终止应用程序并向用户报告错误来处理此类异常,用户将意识到其计算机中正在发生某些不正常的事情。
正确理解指令集的能力,以及找到要修改的良好起始点程序,是这项技术必不可少的先决条件。通常,变质引擎是直接用汇编语言“特别制作”的,这样它们就可以改变自己的行为或交换部分代码来执行变质。]
我不喜欢这样的限制,因此我实现了一个通用变质引擎。
ISA
指令集分析器 (ISA) 是一个紧凑的 x86 扫描器,它能够导航 CPU 指令而不丢失与它们的对齐。这意味着,一旦找到可靠的起点,您就可以计算下一个 x86 操作码的长度。如果您知道这一点,您就可以找到 CPU 字之间的正确“空格”,从而以正确的方式遵循执行路径。
ISA **不是** 反汇编器 **也不是** CPU 仿真工具(但可以扩展以覆盖这些角色);它的唯一工作是定位提供的操作码的长度,以便在扫描要转换为其他指令的模式时不会丢失代码对齐。ISA 已被指示遵循 Intel 手册 325462 中提供的文档来评估 32 位和 64 位体系结构的操作码大小和格式(请在 Intel 网站上搜索以获取更多信息或下载副本)。
您可以在项目根目录下的“isa”文件夹中找到 ISA 源代码。源代码基本上包含一些表格,总结了 Intel 指令集的逻辑。通过使用这些表格,分析器可以检测操作码是否为前缀,是否具有 ModR/M 或 SIB 字节,之后是否有位移字节,以及指令是否需要立即值。
例如,前缀表组织如下
#define TT (X86_ARCH_32) #define SF (X86_ARCH_64) #define AL (TT | SF) static char x86_pre[256] = { /* 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f */ /* 00 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, AL, /* 10 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 20 */ 0, 0, 0, 0, 0, 0, AL, 0, 0, 0, 0, 0, 0, 0, AL, 0, /* 30 */ 0, 0, 0, 0, 0, 0, AL, 0, 0, 0, 0, 0, 0, 0, AL, 0, /* 40 */ 0, 0, 0, 0, 0, 0, 0, 0, SF, SF, SF, SF, SF, SF, SF, SF, /* 50 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 60 */ 0, 0, 0, 0, AL, AL, AL, AL, 0, 0, 0, 0, 0, 0, 0, 0, /* 70 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 80 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* 90 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* a0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* b0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* c0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* d0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* e0 */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, /* f0 */ AL, 0, AL, AL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
并标识 0x26(第 2 行,第 6 列)是 32 位和 64 位体系结构的有效前缀。在同一代码表中可以找到类似的表格(用于 ModR/M 字节和一对、二对和三字节操作码的立即数),以及帮助理解整体逻辑组织方式的注释。
x86.h 头文件中唯一的公共过程是以下内容
int x86_decode(unsigned char * buf, x86arch arch, x86op * op);
它允许您解码由 *buf* 指针指向的操作码,通过指定要考虑的体系结构和一个将填充宝贵信息的*操作码*结构指针。结果值在出错时为负值,否则为已评估操作码的长度。
ISA 已调试,以确保在评估操作码时不会丢失对齐,但由于这是我在业余时间制作的项目,我不能 100% 确定它能无错误地覆盖所有可能的操作码。通过查看 *test* 目录,您可以找到在 ISA 上进行的测试,这些测试由专为 32 位和 64 位体系结构设计的汇编代码组成。在这些文件中,列出了 CPU 可以执行的所有可能的有效操作(每个操作码条目一个),它们都对齐到 16 字节并填充了 Int3。
*Iseta* 是一个调试实用程序,您可以使用它来检查 make 创建的 RAW 二进制文件,它将使用 ISA 来导航指令并测试分析器内部机制是否存在问题。您可以调用此应用程序而不带任何参数来查看快速帮助,或者使用类似的命令行来调试已编译的二进制文件之一
piku@HAL:/M1/tests$ ./iseta x86/iset32.bin 0 4128 32
此执行的预期结果如下
Istruction Set Analizer debugging utility. Operating in 32 bits mode. Analyzing file chunk at 0: 00 d8 cc cc cc cc cc cc cc cc cc cc cc cc cc cc OPCODE RESUME: OP code:00 OP size:2 N.of prefixes: 0 1-byte operation! ModR/M detected: d8 Press ENTER for next opcode... Analyzing file chunk at 16: 01 d9 cc cc cc cc cc cc cc cc cc cc cc cc cc cc OPCODE RESUME: OP code:01 OP size:2 N.of prefixes: 0 1-byte operation! ModR/M detected: d9 Press ENTER for next opcode...
依此类推,直到二进制文件终止。如果一切正常,您应该始终看到从 0x00 到 0xFF 的每个 ENTER 键盘输入只有一个操作码。表格中有一些空白,但它们是根据 Intel 手册存在的,因为有些操作码是不允许的,可能保留给将来使用。
M1 简单变质引擎
在介绍了理解变质引擎问题和行为的所有必要机制后,让我们看看它们是如何工作的。M1 是一个简单、通用的变质引擎,它使用 ISA 来扫描操作码并根据硬编码规则更改操作。由于我想保持简单,注入 M1 的唯一规则是算术运算的加法和减法互换。这意味着将一个值 *n* 的加法更改为减去 *-n*,将一个值 *n* 的减法转换为加上 *-n*。与寄存器互换一样,这是变质引擎可以执行的最基本操作之一。
为了进一步限制情况,我将重点关注对 1 字节立即数据以及仅在某些寄存器上进行的加法/减法。这意味着变质引擎将扫描 0x83 操作码系列,并且只会影响发生在选定寄存器上的这类操作。
如果在项目根目录调用 make,将生成两个应用程序:一个是 M1,另一个是我们著名的 dummy。Dummy 将是测试变质的初始目标。此应用程序不执行任何智能操作,但只请求一个将由五个不同例程(“something”系列例程,请参阅源代码)递增并打印到标准输出的值。
如果您在 make 之后立即尝试运行它,您将看到以下输出
piku@HAL:/M1$ ./dummy Give me a value!!! piku@HAL:/M1$ ./dummy 1 Value is now 1 Value is now 2 Value is now 3 Value is now 4 Value is now 5
还有一个文本文档与应用程序一起生成,称为 *dummy.pre.txt*。此文件将包含我们 dummy 应用程序的已反汇编代码段,这对于理解 M1 在其上执行后实际发生的情况至关重要。如果您集中关注偏移量 40066e、第 197 行的 *something* 过程,您将详细了解 C 代码是如何被翻译、编译,然后从 CPU 操作码反汇编回来的。
000000000040066e <something>: 40066e: 55 push %rbp 40066f: 48 89 e5 mov %rsp,%rbp 400672: 48 83 ec 10 sub $0x10,%rsp 400676: 89 7d fc mov %edi,-0x4(%rbp) 400679: 8b 45 fc mov -0x4(%rbp),%eax 40067c: 89 c6 mov %eax,%esi 40067e: bf 74 07 40 00 mov $0x400774,%edi 400683: b8 00 00 00 00 mov $0x0,%eax 400688: e8 03 fe ff ff callq 400490 <printf@plt> 40068d: 8b 45 fc mov -0x4(%rbp),%eax 400690: 83 c0 01 add $0x1,%eax 400693: 89 c7 mov %eax,%edi 400695: e8 a6 ff ff ff callq 400640 <something_1> 40069a: c9 leaveq 40069b: c3 retq
这里的 400672 和 400690 将根据 M1 的需要进行更改。
注意:对于此及后续操作码报告,我将假设您正在 64 位体系结构上操作。32 位的情况略有不同,本文档未涵盖。
现在是时候运行 M1 并指向不幸的 dummy 实用程序了。如果您使用以下参数运行变质引擎
piku@HAL:/M1$ ./m1 /M1/dummy 1646 45 2
您将从 1646 字节(十六进制 0x66e)开始评估 dummy,持续 45 字节(十六进制 0x66e + 0x2d = 0x69b)。这正是我们“something”过程所在的位置!在运行 M1 后调用 *make dump* 将会转储另一个包含已反汇编目标应用程序的文本文件。通过将新创建的文件滑向“something”应用程序(始终位于 40066e),您现在会发现我们指定的两个操作已有所不同(此处高亮显示)
000000000040066e <something>: 40066e: 55 push %rbp 40066f: 48 89 e5 mov %rsp,%rbp 400672: 48 83 c4 f0 add $0xfffffffffffffff0,%rsp 400676: 89 7d fc mov %edi,-0x4(%rbp) 400679: 8b 45 fc mov -0x4(%rbp),%eax 40067c: 89 c6 mov %eax,%esi 40067e: bf 74 07 40 00 mov $0x400774,%edi 400683: b8 00 00 00 00 mov $0x0,%eax 400688: e8 03 fe ff ff callq 400490 <printf@plt> 40068d: 8b 45 fc mov -0x4(%rbp),%eax 400690: 83 e8 ff sub $0xffffffff,%eax 400693: 89 c7 mov %eax,%edi 400695: e8 a6 ff ff ff callq 400640 <something_1> 40069a: c9 leaveq 40069b: c3 retq
这表明 M1 已完成工作并正确地交换了操作。但是受影响的应用程序仍然可以工作吗?嗯,为什么不再次调用 dummy 上的前一个命令自己看看呢?
piku@HAL:/M1$ ./dummy 1 Value is now 1 Value is now 2 Value is now 3 Value is now 4 Value is now 5
产生相同的、确切的输出,但应用程序现在已经物理改变了!恭喜您,您刚刚完成了第一次变质操作。:-)
如果您想变异多个过程怎么办?好吧,基本上没有什么能阻止您选择更大的区域,但您必须始终确保您是从有效操作码进行评估,并且中间没有任何东西破坏您的对齐。M1 被故意遗留为 dummy,因为我只想演示变质引擎的工作原理,而不是真正构建一个强大的引擎。有时我修改了代码的某些部分,在中间,存储了我认为是一些随机数据的东西(嗯,无效的操作码);这从那个点开始使 ISA 的计算错位,使随后的修改过程无效(这会导致应用程序崩溃,并报告段错误)。
如果您再次调用 make 来重建 dummy,并使用以下命令行运行 M1
piku@HAL:/M1$ ./m1 /M1/dummy 1469 407 2
您将从 *something4* 过程开始进行变质,并包含 *main* 过程。如果您再次转储 dummy 应用程序,您会注意到所有兼容的操作都已替换为 M1 中指示的操作。同样,如果您尝试调用 dummy 实用程序在其变质之后,您将再次获得有效的输出,例如
piku@HAL:/M1$ ./dummy 1 Value is now 1 Value is now 2 Value is now 3 Value is now 4 Value is now 5
继续前进
既然我们有了一个能够对其他应用程序进行变质而不破坏它们的应用程序,下一步是什么?我们还能做什么来继续并验证更多的 M1?
嗯,显然是修改一个旧应用程序。:-)
在我进行的测试中,我成功地尝试修改了 Explorer.exe 应用程序,但尽管其文本部分中的代码是合法的,Windows(版本 10)还是检测到了更改并阻止了它运行(InPageError,0xc0000428)。在查看 Windows 文件夹时,我复制并尝试运行其他应用程序,以检测谁还能在经典的“C:\Windows”域之外运行,我检测到了 write.exe。
Write.exe 是简单的 Wordpad,一个介于记事本和 Microsoft Office Word 之间(好吧,我更倾向于记事本那边)的应用程序。要执行此测试,您必须将其复制到另一个文件夹,以免原始文件不受更改的影响(因此从 C:\Windows 复制到您想要的任何位置)。
作为第一项操作,我扫描了 write.exe 的头信息,并通过调用以下命令保存了(与 dummy 一样)备份的 objdump 跟踪(请注意,我将 write.exe 的副本保存在了项目根目录)
piku@HAL:/M1$ objdump -x write.exe piku@HAL:/M1$ objdump -S write.exe > write.pre.dump
第一个操作对于获取信息至关重要,您将使用这些信息找到要修改过程的正确位置,第二个操作用于拥有原始内部副本,以便在变质后进行评估。头信息扫描的输出提供了以下反馈
piku@HAL:/M1$ objdump -x write.exe | more write.exe: file format pei-x86-64 write.exe architecture: i386:x86-64, flags 0x0000012f: HAS_RELOC, EXEC_P, HAS_LINENO, HAS_DEBUG, HAS_LOCALS, D_PAGED start address 0x0000000140001420 Characteristics 0x22 executable large address aware Time/Date Sat Jul 16 04:28:49 2016 Magic 020b (PE32+) MajorLinkerVersion 14 MinorLinkerVersion 0 SizeOfCode 00000a00 SizeOfInitializedData 00002200 SizeOfUninitializedData 00000000 AddressOfEntryPoint 0000000000001420 BaseOfCode 0000000000001000 ImageBase 0000000140000000 SectionAlignment 0000000000001000 FileAlignment 0000000000000200 MajorOSystemVersion 10 MinorOSystemVersion 0 MajorImageVersion 10 MinorImageVersion 0 MajorSubsystemVersion 10 MinorSubsystemVersion 0 Win32Version 00000000 SizeOfImage 00007000 SizeOfHeaders 00000400 CheckSum 00011d73 Subsystem 00000002 (Windows GUI) DllCharacteristics 0000c160 SizeOfStackReserve 0000000000080000 SizeOfStackCommit 0000000000002000 SizeOfHeapReserve 0000000000100000 SizeOfHeapCommit 0000000000001000 LoaderFlags 00000000 NumberOfRvaAndSizes 00000010
特别是这里需要的是 AddressOfEntryPoint 和 BaseOfCode 值。通过查找 .text 头,您会发现它位于文件开头偏移量 0x400 处,这是经典的做法。有了这些信息,您终于可以检查并找到一个区域来测试变质引擎。由于您永远不知道修改的部分是否受应用程序正常打开的影响(操作系统加载程序将其加载到内存并从入口点开始执行),我们将目标修改靠近入口点(在代码路径的上下文中)的一个部分,这样如果出现问题,应用程序将立即失败,您就会知道您的失败。
首先,我们需要读取 0x1420 位置的实际内容。您可以通过查看 write.pre.dump 文件并搜索位置 140001420 来快速完成;再次,对于最懒的人,我将在此处报告输出
14000141c: cc int3 14000141d: cc int3 14000141e: cc int3 14000141f: cc int3 140001420: 48 83 ec 28 sub $0x28,%rsp 140001424: e8 5b 02 00 00 callq 0x140001684 140001429: 48 83 c4 28 add $0x28,%rsp 14000142d: e9 7e fd ff ff jmpq 0x1400011b0 140001432: cc int3 140001433: cc int3 140001434: cc int3 140001435: cc int3
正如您所见,它什么也没做,而是立即将执行转移到另一个位置。所以,让我们移到 140001684 并看看我们能找到什么
140001682: cc int3 140001683: cc int3 140001684: 48 89 5c 24 20 mov %rbx,0x20(%rsp) 140001689: 55 push %rbp 14000168a: 48 8b ec mov %rsp,%rbp 14000168d: 48 83 ec 20 sub $0x20,%rsp 140001691: 48 83 65 18 00 andq $0x0,0x18(%rbp) 140001696: 48 bb 32 a2 df 2d 99 movabs $0x2b992ddfa232,%rbx 14000169d: 2b 00 00 1400016a0: 48 8b 05 61 19 00 00 mov 0x1961(%rip),%rax # 0x140003008 1400016a7: 48 3b c3 cmp %rbx,%rax 1400016aa: 0f 85 8f 00 00 00 jne 0x14000173f 1400016b0: 48 8d 4d 18 lea 0x18(%rbp),%rcx 1400016b4: ff 15 76 0a 00 00 callq *0xa76(%rip) # 0x140002130 1400016ba: 48 8b 45 18 mov 0x18(%rbp),%rax 1400016be: 48 89 45 10 mov %rax,0x10(%rbp) 1400016c2: ff 15 78 0a 00 00 callq *0xa78(%rip) # 0x140002140 1400016c8: 8b c0 mov %eax,%eax 1400016ca: 48 31 45 10 xor %rax,0x10(%rbp) 1400016ce: ff 15 64 0a 00 00 callq *0xa64(%rip) # 0x140002138 1400016d4: 8b c0 mov %eax,%eax 1400016d6: 48 31 45 10 xor %rax,0x10(%rbp) 1400016da: ff 15 48 0a 00 00 callq *0xa48(%rip) # 0x140002128 1400016e0: 8b c0 mov %eax,%eax 1400016e2: 48 c1 e0 18 shl $0x18,%rax 1400016e6: 48 31 45 10 xor %rax,0x10(%rbp) 1400016ea: ff 15 38 0a 00 00 callq *0xa38(%rip) # 0x140002128 1400016f0: 8b c0 mov %eax,%eax 1400016f2: 48 8d 4d 10 lea 0x10(%rbp),%rcx 1400016f6: 48 33 45 10 xor 0x10(%rbp),%rax 1400016fa: 48 33 c1 xor %rcx,%rax 1400016fd: 48 8d 4d 20 lea 0x20(%rbp),%rcx 140001701: 48 89 45 10 mov %rax,0x10(%rbp) 140001705: ff 15 3d 0a 00 00 callq *0xa3d(%rip) # 0x140002148 14000170b: 8b 45 20 mov 0x20(%rbp),%eax 14000170e: 48 b9 ff ff ff ff ff movabs $0xffffffffffff,%rcx 140001715: ff 00 00 140001718: 48 c1 e0 20 shl $0x20,%rax 14000171c: 48 33 45 20 xor 0x20(%rbp),%rax 140001720: 48 33 45 10 xor 0x10(%rbp),%rax 140001724: 48 23 c1 and %rcx,%rax 140001727: 48 b9 33 a2 df 2d 99 movabs $0x2b992ddfa233,%rcx 14000172e: 2b 00 00 140001731: 48 3b c3 cmp %rbx,%rax 140001734: 48 0f 44 c1 cmove %rcx,%rax 140001738: 48 89 05 c9 18 00 00 mov %rax,0x18c9(%rip) # 0x140003008 14000173f: 48 8b 5c 24 48 mov 0x48(%rsp),%rbx 140001744: 48 f7 d0 not %rax 140001747: 48 89 05 c2 18 00 00 mov %rax,0x18c2(%rip) # 0x140003010 14000174e: 48 83 c4 20 add $0x20,%rsp 140001752: 5d pop %rbp 140001753: c3 retq 140001754: cc int3 140001755: cc int3
好的,现在这里变得有趣了,因为这是一个真正的过程。一些基本计算将为我们提供硬盘上源文件中的位置以及过程的大小(0x1684 - 0x1000 + 0x400 = 0xa84,十进制为 2692,而这个过程的大小为 206 字节)。您可以通过用 vim 打开可执行文件并切换到十六进制视图来验证这是否正确,如下所示
piku@HAL:/M1$ vim ./write.exe :%!xxd -c16
剩下的就是调用 M1 并使用正确的参数来修改过程,如下所示(这次我也报告了 M1 的输出)
piku@HAL:/M1$ ./m1 /M1/write.exe 2692 206 2 Read 206 bytes from location a84 Found one at 0x9 To add: 8b --> c0 Found one at 0xd Found one at 0xca Written 206 bytes to location a84 File mutated!
正如您所见,只做了一个更改,另外两个可能的候选被删除,因为它们不符合我们对变质引擎施加的限制。
现在是时候重复 objdump 操作并检查更改的内容了
piku@HAL:/M1$ objdump -S write.exe > write.dump
现在,如果您将 *write.pre.dump* 文件与 *write.dump* 文件在 0x140001684 位置对齐,您将得到以下过程(我在位置 0x14000168d 处高亮显示了变质操作)
140001682: cc int3 140001683: cc int3 140001684: 48 89 5c 24 20 mov %rbx,0x20(%rsp) 140001689: 55 push %rbp 14000168a: 48 8b ec mov %rsp,%rbp 14000168d: 48 83 c4 e0 add $0xffffffffffffffe0,%rsp 140001691: 48 83 65 18 00 andq $0x0,0x18(%rbp) 140001696: 48 bb 32 a2 df 2d 99 movabs $0x2b992ddfa232,%rbx 14000169d: 2b 00 00 1400016a0: 48 8b 05 61 19 00 00 mov 0x1961(%rip),%rax # 0x140003008 1400016a7: 48 3b c3 cmp %rbx,%rax 1400016aa: 0f 85 8f 00 00 00 jne 0x14000173f 1400016b0: 48 8d 4d 18 lea 0x18(%rbp),%rcx 1400016b4: ff 15 76 0a 00 00 callq *0xa76(%rip) # 0x140002130 1400016ba: 48 8b 45 18 mov 0x18(%rbp),%rax 1400016be: 48 89 45 10 mov %rax,0x10(%rbp) 1400016c2: ff 15 78 0a 00 00 callq *0xa78(%rip) # 0x140002140 1400016c8: 8b c0 mov %eax,%eax 1400016ca: 48 31 45 10 xor %rax,0x10(%rbp) 1400016ce: ff 15 64 0a 00 00 callq *0xa64(%rip) # 0x140002138 1400016d4: 8b c0 mov %eax,%eax 1400016d6: 48 31 45 10 xor %rax,0x10(%rbp) 1400016da: ff 15 48 0a 00 00 callq *0xa48(%rip) # 0x140002128 1400016e0: 8b c0 mov %eax,%eax 1400016e2: 48 c1 e0 18 shl $0x18,%rax 1400016e6: 48 31 45 10 xor %rax,0x10(%rbp) 1400016ea: ff 15 38 0a 00 00 callq *0xa38(%rip) # 0x140002128 1400016f0: 8b c0 mov %eax,%eax 1400016f2: 48 8d 4d 10 lea 0x10(%rbp),%rcx 1400016f6: 48 33 45 10 xor 0x10(%rbp),%rax 1400016fa: 48 33 c1 xor %rcx,%rax 1400016fd: 48 8d 4d 20 lea 0x20(%rbp),%rcx 140001701: 48 89 45 10 mov %rax,0x10(%rbp) 140001705: ff 15 3d 0a 00 00 callq *0xa3d(%rip) # 0x140002148 14000170b: 8b 45 20 mov 0x20(%rbp),%eax 14000170e: 48 b9 ff ff ff ff ff movabs $0xffffffffffff,%rcx 140001715: ff 00 00 140001718: 48 c1 e0 20 shl $0x20,%rax 14000171c: 48 33 45 20 xor 0x20(%rbp),%rax 140001720: 48 33 45 10 xor 0x10(%rbp),%rax 140001724: 48 23 c1 and %rcx,%rax 140001727: 48 b9 33 a2 df 2d 99 movabs $0x2b992ddfa233,%rcx 14000172e: 2b 00 00 140001731: 48 3b c3 cmp %rbx,%rax 140001734: 48 0f 44 c1 cmove %rcx,%rax 140001738: 48 89 05 c9 18 00 00 mov %rax,0x18c9(%rip) # 0x140003008 14000173f: 48 8b 5c 24 48 mov 0x48(%rsp),%rbx 140001744: 48 f7 d0 not %rax 140001747: 48 89 05 c2 18 00 00 mov %rax,0x18c2(%rip) # 0x140003010 14000174e: 48 83 c4 20 add $0x20,%rsp 140001752: 5d pop %rbp 140001753: c3 retq 140001754: cc int3 140001755: cc int3
正如您所见,M1 再次将减法运算变质为加法运算,该运算将对 CPU 寄存器产生相同的影响。write.exe 现在还能工作吗?为什么不运行它自己看看呢?
恢复
二进制变质是一种有趣的技术,它允许指令引擎对期望的目标指令集进行变质处理。这项技术从最简单的更改开始,就像前几章所示的示例一样,到更复杂的更改,这些更改需要扩展 .text 部分、重新计算重定位以及其他类型的调整,以避免破坏应用程序。
这项技术允许创建者“藏身于寻常之中”,因为任何试图对其进行哈希处理的努力都将被内部指令的变质所破坏。这当然不会阻止操作系统和杀毒软件执行完整性检查,正如我在 explorer.exe 应用程序的测试中所见。如您所见,这远非易事,因为它需要精确的指令集分析软件、正确的变质规则和对齐检测例程,以避免更改应用程序的错误部分。
就这样,希望您喜欢这个简单的变质入门介绍。:-)