精通恶意软件分析第二版(一)
原文:
annas-archive.org/md5/a5e642fcde320e26768a38bb6eadf732译者:飞龙
前言
新兴和发展中的技术不可避免地带来了新的恶意软件类型,创造了对能够防范这些恶意软件的 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/err…并填写表单。
版权问题:如果您在互联网上遇到任何形式的我们作品的非法复制,我们将非常感激您能提供相关位置或网站名称。请通过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)服务器进行通信、伪装自己以绕过防御、窃取凭证和其他敏感信息、提升权限等,这将指导威胁狩猎过程。掌握这些知识后,您将更好地理解如何在日志或系统的易失性和非易失性数据中高效地搜索这些技术。
恶意软件分析在创建检测中的作用
全球多家公司开发并分发网络安全系统,以保护其客户免受各种类型的威胁。检测恶意活动的方法有很多种,涵盖了攻击的不同阶段,例如,监控网络流量、检查系统日志和注册表项,或静态和执行时检查文件。在许多情况下,这需要开发某种规则或签名,用以区分恶意模式和良性模式。恶意软件分析在这方面是不可替代的,因为它帮助安全专业人员识别这些模式并创建出不产生误报的强大规则。
在下一部分,我们将讨论如何根据恶意软件的功能对其进行分类。
探索恶意软件的类型
在本节中,我们将讨论恶意软件存在的一般原因,它与其他计算机程序的不同之处,以及我们在现实世界中可能遇到的不同种类。
恶意软件发展的简史
在个人电脑崛起之前,只有非常少数的软件开发者。它们的目标是最大限度地利用当时可用的硬件,改善人们的生活,无论是会计软件、将人类送入太空的软件,还是游戏。迅速发展的网络将多台计算机连接在一起,使计算机和人们能够进行远程通信。大约在同一时期,随着计算机的进一步普及,使普通大众能够更负担得起,全球范围内的第一个黑客社区开始出现。然而,正是在学术界,出现了一个具有重大影响的最臭名昭著的恶意软件事件——莫里斯蠕虫。它能够通过网络传播到其他计算机,利用多个漏洞,主要是sendmail和fingerd软件中的漏洞。然而,蠕虫没有检查目标计算机是否已被感染,从而在每台计算机上生成多个副本,迅速消耗受害者的所有系统资源,使其无法使用。它仅仅出于纯粹的兴趣而创建,向世界展示了几行代码可能带来的后果,并导致了第一次因恶意软件开发而被定罪的案件。此后,许多其他类型的恶意软件开始出现。那时,创作者们的主要目标是展示他们在社区中的技能。
随后,焦点慢慢转向了赚钱。编程变得越来越流行,学校和大学开始教授编程,而新型高级编程语言的出现使得经验较少的人也能开始编写自己的代码,包括恶意代码。最终,职业化的网络犯罪团伙开始出现,明确分工,使得恶意软件开发成为一个非常有利可图的有组织的非法活动。这些团伙利用了所有可能的洗钱手段,包括最初的“金钱驮运者”以及后来的加密货币,以避免追踪和随后的逮捕。这些团体通常被称为“以财务为动机的行为者”。
在过去几年里,以财务为动机的团体逐渐将焦点从攻击消费者转向攻击大型组织,并通过一次攻击在一个地方赚取大笔钱。最常见的例子是使用勒索软件加密受害者的文件,然后要求赎金以恢复访问权限。在许多情况下,还使用了双重勒索方案,犯罪分子威胁要将敏感材料公开。
政府也开始寻求利用恶意软件进行网络间谍活动和破坏的可能性。正是 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 企业矩阵的网页表示
值得一提的是,框架并不是静态的,它不断演化,融入用户反馈并解决行业面临的新挑战。每个版本的框架都附带有一个 结构化威胁信息表达(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 虚拟机设置中禁用网络
- 无共享设备
许多虚拟化软件默认将连接的外部物理设备映射到虚拟机。这可能非常危险,例如在 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 表
-
扩展 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 字符串示例
除了知道如何使用比特存储数据外,还需要理解按位操作,因为它们在汇编语言中有很多应用。
按位操作
按位操作在位级别上进行,可以是单目操作,这意味着它只需要一个操作数,也可以是双目操作,这意味着它需要两个操作数并将相应的逻辑应用于每一对对齐的位。由于它们执行起来非常快速,按位操作在机器代码中找到了多种应用。让我们看看最重要的一些应用。
与(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 – 虚拟内存地址
为了节省存储和使用值地址所需的空间,开发了栈的概念。
栈
栈是一个堆叠的对象。在计算机科学中,栈是一种数据结构,它利用后进先出(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 – 条件跳转的示例
- 子程序调用:这些指令会将执行转移到另一个函数,并保存返回地址,以便在必要时恢复。
现在,是时候学习在进行逆向工程时常见的指令了。能够流利地阅读这些指令并理解它们组合的含义,是成为专业恶意软件分析师的一个重要步骤。
熟悉 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 架构
在 x86 架构中使用的寄存器(从 8 到 r15 的寄存器)仅在 x64 中可用,而在 IA-32 中不可用,且 spl、bpl、sil 和 dil 寄存器只能在 x64 中访问。
首先要提到的是,关于哪些寄存器应该称为通用寄存器(GPRs)以及哪些不应如此,可能有多种解释,因为它们中的大多数可能用于某些特定目的。
前四个寄存器(rax/eax、rbx/ebx、rcx/ecx、和 rdx/edx)是 GPRs。它们中的一些寄存器在特定指令中有特殊的用途:
-
rax/eax:这通常用于存储某些操作的结果以及函数的返回值。
-
rcx/ecx:这是在需要重复操作的指令中用作计数寄存器的。
-
rdx/edx:这是在乘法和除法中使用的,分别用来扩展结果或被除数。
在 x64 中,r8 到 r15 的寄存器被添加到可用的 GPRs 列表中。
rsi/esi 和 rdi/edi 主要用于定义在内存中复制字节组的地址。rsi/esi 寄存器始终充当源寄存器,而 rdi/edi 寄存器充当目标寄存器。这两个寄存器都是非易失性的,并且也是 GPR 寄存器。
rsp/esp 寄存器作为栈指针使用,这意味着它始终指向栈顶。当一个值被推送到栈时,它的值会减小,而当一个值被从栈中取出时,它的值会增大。
rbp/ebp 寄存器主要作为基指针使用,指示栈中的一个固定位置。它帮助访问函数的局部变量和参数,稍后在本节中我们会看到。
特殊寄存器
x86 汇编中有两个特殊的寄存器,如下所示:
-
rip/eip:这是一个指令指针,指向下一个将要执行的指令。它不能直接访问,但有一些特殊的指令可以与其一起使用。
-
rflags/eflags/flags:该寄存器包含处理器的当前状态。其标志位会受到算术和逻辑指令的影响,包括比较指令,如 cmp 和 test,并且它也用于条件跳转和其他指令。以下是其中的一些标志:
- 进位标志(CF):当算术操作超出范围时,设置该标志,如下所示:
mov al, FFh ; al = 0xFF & CF = 0
add al, 1 ; al = 0 & CF = 1
-
零标志(ZF):当算术或逻辑操作的结果为零时,设置该标志。比较指令也可以设置此标志。
-
方向标志(DF):该标志指示某些指令,如 lods,stos,scas 和 movs(稍后将看到),应当访问更高地址(当未设置时)还是更低地址(当设置时)。
-
符号标志(SF):该标志指示操作结果为负值。
-
溢出标志(OF):该标志指示操作中发生了溢出,导致符号发生变化(仅对有符号数有效),如下所示:
mov cl, 7Fh ; cl = 0x7F (127) & OF = 0
inc cl ; cl = 0x80 (-128) & OF = 1
还有其他寄存器,例如 MMX 和 FPU 寄存器(以及与之配套的指令),但它们在恶意软件中很少使用,因此它们不在本书的讨论范围内。
指令结构
许多 x86 汇编器,如 MASM 和 NASM,以及反汇编器,都使用 Intel 语法。在这种情况下,其指令的常见结构是 opcode,dest,src。
dest 和 src 通常被称为 操作数。它们的数量可以根据指令的不同从 0 到 3 不等。另一种选择是 GNU 汇编器(GAS),它使用 AT&T 语法,并交换 dest 和 src 来表示。本文中,我们将使用 Intel 语法。
现在,让我们更深入地了解每个指令部分的含义。
opcode
n``op,pushad,popad 和 movsb。
重要说明
pushad和popad在 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
指令集
在本节中,我们将介绍开始阅读汇编所需的最重要指令。
数据操作指令
一些最常见的算术指令如下所示:
重要说明
对于将操作数视为有符号整数的乘法和除法,相应的指令将是imul和idiv。
以下指令表示逻辑/位操作:
最后,以下指令表示位移和旋转操作:
要了解更多关于位运算的潜在应用,请阅读第一章,网络犯罪、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)
在被调用函数的末尾,它会返回原始的ebp和esp值,如下所示:
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.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
在这种情况下,*{cond}*代码将是一个条件码。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 架构系统的物联网恶意软件
随着架构的演变,出现了多个版本,从 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(有时称为zero)是一个常量寄存器,始终存储零,并提供只读访问。它可以用作/dev/null 的类似物来丢弃某些操作的输出,或者作为零值的快速源。
-
r31(也称为$ra)在过程调用分支/跳转和链接指令期间存储返回地址。
其他寄存器通常用于特定的目的,如下所示:
-
r1(也称为$at):汇编临时寄存器 – 在解决伪指令时使用
-
r2-r3(也称为v1):值 – 存储返回函数值。
-
r4-r7(也叫做 a3):参数寄存器 – 用于传递函数参数。
-
r8-r15(也叫做 t7/a7 和 t7):临时寄存器 – 前四个寄存器在 N32 和 N64 调用约定中也可以用来传递函数参数(另一个 O32 调用约定仅使用 r4-r7 寄存器;后续参数通过栈传递)。
-
r16-r23(也叫做 s7):保存的临时寄存器 – 跨函数调用时保持不变。
-
r24-r25(也叫做 t9):临时寄存器。
-
r26-r27(也叫做 k1):通常保留给操作系统内核使用。
-
r28(也叫做 $gp):全局指针 – 指向全局区域(数据段)。
-
r29(也叫做 $sp):栈指针。
-
r30(也叫做 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:绝对值 – 转换为ADDU、BGEZ和SUB的组合 -
BLT:小于分支——相当于SLT和BNE的组合 -
BGT/BGE/BLE:类似于BLT -
LI/LA:加载立即数/地址——相当于LUI和ORI的组合,或者用于 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 位固定长度指令来减少程序代码的大小。除了BF和BT外,所有分支指令和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]) 寄存器。
要将参数传递给函数,必须将它们放入输出寄存器中。当函数获得控制权时,它将访问这些寄存器。额外的参数可以通过栈传递。结果将放入第一个寄存器中,返回时该寄存器将变为第一个输出寄存器。SAVE和RESTORE指令在此切换中用于分别分配新的寄存器窗口并恢复先前的窗口。
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
为了理解分支和流向重定向,我们来看一下下面的图表,它展示了在伪代码中的表现形式:
](tos-cn-i-73owjymdk6/fea466dc9ba74de18b1eec9fd3e7d58b)
图 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 – 文件头解释
高亮显示的值如下:
-
Machine:此字段表示处理器类型——例如,0x14c 表示 Intel 386 或更高版本的处理器。 -
NumberOfSections:该值表示头部之后的节的数量,例如代码节、数据节或资源节(用于文件或图像)。 -
TimeDateStamp:这是该程序编译的确切日期和时间。它对于威胁情报和创建攻击时间线非常有用。 -
Characteristics:该值表示可执行文件的类型,并指定它是程序还是动态链接库(我们将在本章后面讨论)。
现在,让我们来谈谈可选头部。
可选头部
在文件头之后,可选头部带来了更多的信息,如下所示:
图 3.3 – 可选头部解释
以下是该头部中的一些最重要的值:
-
魔术值:此值标识 PE 文件支持的平台(是否是 x86 或 x64)。 -
入口点地址:这是我们分析中非常重要的字段,它指向程序执行的起始点(程序中要执行的第一个汇编指令),相对于其起始地址(基址)。这种类型的地址被称为相对虚拟地址(RVA)。 -
镜像基址:这是程序设计为加载到虚拟内存的地址。所有使用绝对地址的指令将期望该值作为程序基址。如果程序有重定位表,它可以加载到不同的基址。在这种情况下,所有这类指令将由 Windows 加载器根据该表进行更新。 -
节对齐:每个节和所有头部的大小在加载到内存时应该与此值对齐(通常此值为 0x1000)。 -
文件对齐:PE 文件中每个节的大小(以及所有头部的大小)必须与此值对齐(例如,对于一个大小为 0x1164 的节,如果文件对齐值为 0x200,则该节的大小将变更为 0x1200)。 -
主要子系统版本:表示运行该应用程序所需的最低 Windows 版本,如 Windows XP 或 Windows 7。 -
镜像大小:这是整个应用程序在内存中的大小(通常由于未初始化数据、不同的对齐方式以及其他原因,它大于硬盘上的文件大小)。 -
头部大小:这是所有头部的大小。 -
子系统:指示该程序可能是一个 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:内存保护标志(主要有EXECUTE、READ或WRITE)。
现在,让我们讨论一下 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 字节。 -
其他一些字段,如
SizeOfHeapCommit、SizeOfHeapReserve、SizeOfStackReserve和SizeOfStackCommit,现在占用 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 将一个动态链接库加载到调用程序的虚拟内存中,并返回其地址(变体包括LoadLibraryW、LoadLibraryExA和LoadLibraryExW)。 -
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 可以告诉你恶意软件是否使用互联网,例如socket、send和recv,它们可以告诉你是否连接到充当客户端的服务器,或者是否监听端口,例如connect或listen。
一些 API 甚至可以告诉你它们使用的协议,例如HTTPSendRequestA或FTPPutFile,这两个 API 都来自wininet.dll。
- 这个恶意软件还有哪些功能?
一些 API 与文件搜索相关,例如FindFirstFileA,这可能是该恶意软件是勒索软件或信息窃取者的线索。
它可能会使用诸如Process32First、Process32Next和CreateRemoteThread等 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|WRITE、READ|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 加载和进程创建的过程。我们将按顺序调查它,如下所示的步骤所示:
-
calc.exe,另一个名为explorer.exe的进程(即 Windows 资源管理器的进程)调用一个 API,CreateProcessA,该 API 向操作系统发出请求,以创建此进程并开始执行。 -
EPROCESS,为该进程设置唯一的 ID(ProcessID),并将explorer.exe文件的进程 ID 设置为新创建的calc.exe进程的父 PID。 -
EPROCESS结构。接着,它创建 PEB 结构,包含所有必要的信息,并加载 Windows 应用程序始终需要的两个主要 DLL:ntdll.dll和kernel32.dll(有些应用程序运行在其他 Windows 子系统上,例如 POSIX,并不使用kernel32.dll)。 -
加载 PE 文件:然后,Windows 开始加载 PE 文件(我们将在下一节解释),它加载所有必需的第三方库(DLL),包括这些库所需要的所有 DLL,并确保从这些库中找到所需的 API,并将其地址保存到已加载 PE 文件的导入表中,以便代码能够轻松访问并调用它们。
-
启动执行:最后但同样重要的是,Windows 在进程中创建第一个线程,执行一些初始化工作,并调用 PE 文件的入口点以启动程序执行。之前提到的 TLS 回调(如果有的话)将在入口点之前执行。
现在,让我们更深入地探讨这一过程中的 PE 加载部分。
PE 文件加载步骤
Windows PE 加载器在将可执行的 PE 文件加载到内存中(包括动态链接库)时遵循以下步骤:
-
ImageBase:将 PE 文件加载到其虚拟内存中的此地址(如果可能的话)。 -
NumberOfSections:用于加载各个节。 -
SizeOfImage:因为这是整个 PE 文件在内存中加载后的最终大小,所以这个值将用于最初的空间分配。 -
NumberOfSections字段解析 PE 文件中的所有节,确保获取所有必要的信息,包括它们在内存中的地址和大小(分别是VirtualAddress和VirtualSize),以及硬盘上节的偏移和大小,用于读取数据。 -
SectionAlignment,加载器复制所有头部信息,然后根据VirtualAddress和VirtualSize值(如果VirtualAddress或VirtualSize与SectionAlignment不对齐,加载器会先进行对齐,然后使用这些值)将每个节移动到新位置,如下图所示:
图 3.15 – 将节从磁盘映射到内存
-
处理第三方库:在这一步骤中,加载器加载所有需要的 DLL 文件,反复递归地执行这个过程,直到所有 DLL 文件都被加载完成。之后,它获取所有导入的 API 的地址,并将它们保存到已加载 PE 文件的导入表中。
-
ImageBase,加载器使用程序/库的新地址(新的ImageBase)修复代码中的所有绝对地址。 -
启动执行:最后,就像进程创建一样,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 用户界面非常简洁,易于学习。在这一部分,我们将介绍帮助你进行分析的步骤和不同的窗口:
- 选择一个样本进行调试:你可以直接通过 文件 | 打开 来打开样本文件,选择一个 PE 文件进行打开(它也可以是一个 DLL 文件,但请确保它是 32 位样本)。另外,你还可以将其附加到一个正在运行的进程,如下所示:
图 3.20 – OllyDbg 附加对话框
- CPU 窗口:这是你的主窗口。在调试过程中,你大部分时间都会待在这个窗口。此窗口包含左上角的汇编代码,并提供通过双击地址设置断点或修改程序汇编代码的选项。
在右上角,你也可以看到寄存器。只要执行被暂停,就可以随时修改它们。在底部,你可以看到堆栈和十六进制格式的数据,这些也可以修改。
你可以在以下两种视图中轻松修改内存中的任何数据:
图 3.21 – OllyDbg 默认窗口布局解释
- 可执行模块窗口:OllyDbg 中有多个窗口可以帮助你进行分析,例如可执行模块窗口(你可以通过进入视图 | 可执行模块来访问它),如下图所示:
图 3.22 – OllyDbg 可执行模块对话框
这个窗口将帮助你看到该进程虚拟内存中加载的所有 PE 文件,包括恶意样本和与之一起加载的所有库或 DLL。
- 内存映射窗口:在这里,你可以看到进程虚拟内存中的所有分配的内存。分配的内存是代表物理(RAM)内存或者页面文件的内存,用于在内存不足时存储 RAM 中的内容。你可以看到它们代表的内容及其内存保护状态(读取、写入和/或执行),如下图所示:
图 3.23 – OllyDbg 内存映射对话框
- 调试示例:在调试菜单中,你有多种选项来运行程序的汇编代码,例如通过运行完全执行示例,直到遇到断点,或者直接使用F9。
另一个选项是直接单步跳过。单步跳过执行一行代码。然而,如果这行代码是对另一个函数的调用,它会完全执行该函数,并在函数返回后停止。这使得它与单步进入不同,后者会进入函数内部并在函数开始时停止,如下图所示:
图 3.24 – OllyDbg 调试菜单
它包括设置硬件断点并查看它们的选项,我们将在本章后面介绍。
- 更多功能: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-DR3、DR6和DR7。
这些寄存器允许你设置最多四个断点,每个断点都有特定的地址,可以从给定地址开始读取、写入或执行 1、2 或 4 个字节。它们非常有用,因为它们不会像 INT3 断点那样修改指令字节,而且通常更难被检测到。然而,它们仍然可能会被恶意软件检测并移除,我们将在第六章中讨论,绕过反向工程技术。
你可以通过进入Debug菜单并选择Hardware breakpoints来查看它们,如下图所示:
图 3.27 – OllyDbg 硬件断点对话框
如你所见,每种类型的断点都有其特定的用途,并且有各自的优缺点,因此了解它们并根据任务需求使用它们非常重要。
修改程序的执行
为了绕过反调试技巧,强制恶意软件与 C&C 通信,甚至测试恶意软件执行的不同分支,你需要能够改变恶意软件的执行流程。让我们来看一下可以用来改变执行流程和任何线程行为的不同技巧。
修改程序的汇编指令
你可以通过更改汇编指令来修改代码执行路径。例如,你可以将条件跳转指令更改为相反条件,如下图所示,强制执行本不该执行的特定分支:
图 3.28 – 在 OllyDbg 中使用汇编
除了代码之外,还可以更改寄存器的内容。
更改 EFlags
与其修改条件跳转指令的代码,你也可以通过更改 EFlags 寄存器来修改其比较结果。
在右上角的寄存器之后,你可以看到多个可以更改的标志。每个标志表示任何比较的特定结果(其他指令也会改变这些标志)。例如,ZF 表示两个值是否相等,或某个寄存器是否变为零。通过更改 ZF 标志,你可以强制条件跳转(如jnz和jz)跳到相反的分支,从而强制改变执行路径。
修改指令指针值
你可以通过简单地修改指令指针 (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:指向相应可执行文件的文件路径,可包含可选参数。 -
Type:REG_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>注册表键,并用与当前值进行按位或运算的结果替换其数据,运算结果为0x00000100DWORD(SERVICE_INTERACTIVE_PROCESS标志)。例如,0x00000010将变为0x00000110。 -
此外,当使用
sc工具并带有type= interact type= own或type= 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 服务及其调试方法。
在这一点上,你应该已经掌握了进行基本恶意软件分析的基础,包括静态分析和动态分析。你也应该了解在每个步骤中需要回答的问题以及你需要遵循的流程,以便全面理解这个恶意软件的功能。
在第四章《解包、解密与去混淆》中,我们将把讨论拓展到恶意软件的解包、解密和去混淆。我们将探索恶意软件作者为绕过检测和欺骗经验不足的逆向工程师而采用的不同技巧。我们还将学习如何绕过这些技巧并应对它们。