Mac 恶意软件的艺术:持久化

144 阅读23分钟

可以说,检测 macOS 上恶意威胁的最佳方法之一就是聚焦“持久化”。这里的“持久化”是指软件(包括恶意软件)通过某种方式在系统中安装自身,以确保其在启动、用户登录或其他确定性事件时自动重新执行。否则,一旦用户注销或系统重启,软件可能永远不会再次运行。在本章中,我将专注于枚举持久化项目。在第二部分中,我会介绍如何利用 Apple 的 Endpoint Security 实时监控持久化事件。

持久化是大多数恶意软件的共同特征,是一种强有力的检测机制,能够揭露大部分感染。在 macOS 上,恶意软件通常通过两种方式持久化:作为启动项(守护进程或代理)或登录项。本章将详细展示如何枚举这些项目,从而揭示几乎所有的 Mac 恶意软件样本。

当然,并非所有 macOS 恶意软件都会持久化。例如,加密用户文件的勒索软件或窃取敏感数据的窃取程序,通常不需要多次运行,因此很少会自我持久化。

另一方面,设计为持续运行的合法程序(如自动更新程序、安全工具,甚至简单的辅助工具)也通常会持久化。因此,某个程序被持久化安装并不意味着我们的代码应该将其标记为恶意。

持久化恶意软件示例

由于本章重点揭示以登录项或启动项形式持久化的恶意软件,我们先来看两个简短示例。研究员 Taha Karim 首次披露的 WindTail 恶意软件针对中东地区政府和关键基础设施的员工。该恶意软件常伪装成名为 Final_Presentation 的 PowerPoint 演示文稿,作为登录项持久化安装,以确保每次用户登录时自动重新执行。在其应用程序包内,有一个名为 usrnode 的主二进制文件。反编译该文件,发现其 main 函数开头的持久化逻辑如下:

int main(int argc, const char* argv[])
    r12 = [NSURL fileURLWithPath:NSBundle.mainBundle.bundlePath];

    rbx = LSSharedFileListCreate(0x0, _kLSSharedFileListSessionLoginItems, 0x0);
    LSSharedFileListInsertItemURL(rbx, _kLSSharedFileListItemLast, 0x0, 0x0, r12, 0x0, 0x0);
    ...
}

恶意软件确定自身运行路径后,调用 LSSharedFileListCreateLSSharedFileListInsertItemURL 函数,将自身安装为持久登录项。这个登录项会显示在系统偏好设置中的“登录项”面板里(见图 5-1)。显然,恶意软件作者认为这是为了持久化而做出的可接受权衡。

image.png

让我们来看另一个持久化的 macOS 恶意软件样本。名为 DazzleSpy,这款高级的国家级恶意软件利用零日漏洞远程感染 macOS 用户。虽然 DazzleSpy 的感染方式给检测带来了挑战,但它的持久化手段却相当明显,为防御者提供了直接检测的途径。

在获得初始代码执行权限并逃离浏览器沙箱后,DazzleSpy 会以伪装成 Apple 软件更新程序的启动代理(launch agent)形式进行持久化。作为启动代理的持久化项,通常会在某个 LaunchAgents 目录下创建一个属性列表(plist)文件。DazzleSpy 在当前用户的 Library/LaunchAgents 目录下创建了名为 com.apple.softwareupdate.plist 的属性列表。恶意软件的二进制文件中硬编码了启动代理目录的路径及 plist 文件名,这些字符串可以通过 strings 命令轻易看到:

% strings - DazzleSpy/softwareupdate
...
%@/Library/LaunchAgents
/com.apple.softwareupdate.plist

将恶意软件加载到反编译器中,可以发现一个名为 installDaemon 的类方法使用了这些字符串。顾名思义,这个方法会持久化安装恶意软件(虽然不是作为启动守护进程,而是作为启动代理):

+(void)installDaemon {
    rax = NSHomeDirectory();
    ...
    var_78 = [NSString stringWithFormat:@"%@/Library/LaunchAgents", rax];
    var_80 = [var_78 stringByAppendingFormat:@"/com.apple.softwareupdate.plist"];
    ...
    var_90 = [[NSMutableDictionary alloc] init];
    var_98 = [[NSMutableArray alloc] init];
    ...
    rax = @(YES);
    [var_90 setObject:rax forKey:@"RunAtLoad"];
    [var_90 setObject:@"com.apple.softwareupdate" forKey:@"Label"];
    [var_90 setObject:var_98 forKey:@"ProgramArguments"];
    ...
    [var_90 writeToFile:var_80 atomically:0x0];
    ...
}

从反编译结果看,恶意软件首先动态构建当前用户的 Library/LaunchAgents 目录路径,然后追加字符串 com.apple.softwareupdate.plist。接着,它创建一个字典,包含键如 RunAtLoadLabelProgramArguments,这些键的值描述了如何重启该持久化项、如何标识它及其路径。最后,恶意软件将该字典写入启动代理目录中的 plist 文件,实现持久化。

在一个隔离的分析环境中运行恶意软件,并使用文件监控工具观察,可以确认 DazzleSpy 的持久化行为。文件监控显示,二进制文件(softwareupdate)在当前用户的 LaunchAgents 目录中创建了属性列表文件:

# FileMonitor.app/Contents/MacOS/FileMonitor -pretty
...
{
  "event" : "ES_EVENT_TYPE_NOTIFY_CREATE",
  "file" : {
    "destination" : "/Users/User/Library/LaunchAgents/com.apple.softwareupdate.plist",
    "process" : {
      "pid" : 1469,
      "name" : "softwareupdate",
      "path" : "/Users/User/Desktop/softwareupdate"
    }
  }
}

查看新创建的 plist 文件内容,可以发现恶意软件持久化安装路径为 /Users/User/.local/softwareupdate

<?xml version="1.0" encoding="UTF-8"?>
...
<plist version="1.0">
<dict>
    <key>KeepAlive</key>
    <true/>
    <key>Label</key>
    <string>com.apple.softwareupdate</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/User/.local/softwareupdate</string>
        <string>1</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>SuccessfulExit</key>
    <true/>
</dict>
</plist>

恶意软件将 RunAtLoad 键设置为 true,这意味着 macOS 会在用户每次登录时自动重新启动指定的二进制文件。换句话说,DazzleSpy 实现了持久化。

章节开头提到,合法软件也会持久化。那么,如何判定某个持久化项是否为恶意呢?最好的办法可能是通过第三章介绍的方法检查其代码签名信息。合法软件通常由易识别的公司签名并由 Apple 进行公证。

恶意持久化项也有一些常见特征。以 DazzleSpy 为例,它从隐藏的 .local 目录运行,既未签名也未公证。它的 plist 文件名 com.apple.softwareupdate 暗示该持久化项属于 Apple,但 Apple 从不在用户的 LaunchAgents 目录安装持久组件,其所有启动项的二进制文件均仅由 Apple 官方签名。由此,DazzleSpy 并非孤例;多数恶意持久化项因类似异常而容易被判定为可疑。

后台任务管理

我们如何判断某个项是否实现了持久化?一种简单粗暴的方法是枚举所有位于启动项目录下的 .plist 文件,这些目录包括系统和用户的 LaunchDaemon 及 LaunchAgent 目录。然而,从 macOS 13 开始,Apple 鼓励开发者将启动项直接放入应用程序包内。4 这些改变实际上废弃了通过用户启动项目录实现持久化的方式,这意味着手动枚举持久化项必须扫描每个应用程序包,效率极低。此外,软件还可以作为登录项持久存在,而登录项不依赖属性列表文件或专用目录。

幸运的是,从 macOS 13 起,Apple 将最常见的持久化机制(包括启动代理、启动守护进程和登录项)的管理集中到了一个专有子系统中,称为“后台任务管理”(Background Task Management)。该子系统提供了登录项和启动项的列表,这些列表会显示在系统偏好设置应用的“登录项”面板中(见图 5-2)。

image.png

在我的电脑上,我的几个 Objective-See 工具都作为登录项安装,而 Adobe 的云同步应用和 Google Chrome 的更新程序则安装为持久化的启动项。

当然,我们希望能够通过编程方式获取这份持久化项的列表,因为任何持久化的恶意软件很可能也会出现在这里。虽然后台任务管理(Background Task Management)子系统的组件是专有且闭源的,但动态分析显示该子系统会将它跟踪的持久化项的详细元数据存储在一个单一的数据库文件中。对我们来说,这个集中式数据库的存在无疑是天赐之物。不过由于其格式是专有且未公开文档的,如果想利用它,我们还得做不少工作。

审查子系统

让我们来看一下后台任务管理子系统与该数据库的交互过程。理解这些操作有助于我们创建一个能够程序化提取其内容的工具。利用文件监控工具,我们可以看到当某项被持久化时,后台任务管理守护进程 backgroundtaskmanagementd 会更新 /private/var/db/com.apple.backgroundtaskmanagement/ 目录下的某个文件。为了保证操作的原子性,它会先创建一个临时文件,然后通过重命名操作将其移动到 com.apple.backgroundtaskmanagement 目录下:

# FileMonitor.app/Contents/MacOS/FileMonitor -pretty
{
  "event" : "ES_EVENT_TYPE_NOTIFY_CREATE",
  "file" : {
    "destination" : "/private/var/folders/zz/.../TemporaryItems/.../BackgroundItems-vx.btm",
    "process" : {
       "pid" : 612,
       "name" : "backgroundtaskmanagementd",
       ...
     }
  }
  ...
}

{
  "event" : "ES_EVENT_TYPE_NOTIFY_WRITE",
  "file" : {
    "destination" : "/private/var/folders/zz/.../TemporaryItems/.../BackgroundItems-vx.btm",
    "process" : {
      "pid" : 612,
      "name" : "backgroundtaskmanagementd",
      ...
    }
  }
  ...
}

{
  "event" : "ES_EVENT_TYPE_NOTIFY_RENAME",
  "file" : {
    "source" : "/private/var/folders/zz/.../TemporaryItems/.../BackgroundItems-vx.btm",
    "destination" : "/private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-vx.btm",
    "process" : {
      "pid" : 612,
      "name" : "backgroundtaskmanagementd",
      ...
    }
  }
  ...
}

如果我们对守护进程的二进制文件反汇编,它位于 /System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Versions/A/Resources/ 目录下,可以看到 BTMStore 类的 storeNameForDatabaseVersion: 方法中出现了一个格式字符串 BackgroundItems-v%ld.btm

+[BTMStore storeNameForDatabaseVersion:]
    pacibsp
    sub    sp, sp, #0x20
    stp    fp, lr, [sp, #0x10]
    add    fp, sp, #0x10
    nop
    ldr    x0, =_OBJC_CLASS_$_NSString
    str    x2, [sp, #0x10 + var_10]
    adr    x2, #0x100031f10            ; @"BackgroundItems-v%ld.btm"
    ...

进一步的逆向分析表明,数据库文件名中包含一个版本号,随着 macOS 新版本的发布而递增。此处用 x 代替该版本号,但在你的系统中很可能是 8 或更高。通过 file 命令可以看出 BackgroundItems-vx.btm 文件的内容是二进制属性列表格式。要查看细节,请确保使用系统中正确的版本号:

% file /private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-vx.btm
/private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-vx.btm: Apple binary property list

我们可以使用 plutil 将二进制属性列表转换为 XML。不幸的是,转换得到的 XML 不仅拼写有误,还有序列化的对象不易直接阅读:

% plutil -p /private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-vx.btm
{
  "$archiver" => "NSKeyedArchiver"
  "$objects" => [
    0 => "$null"
    1 => {
      "$class" =>
      <CFKeyedArchiverUID 0x600002854240 [0x1e3bcf9a0]>{value = 265}

      "itemsByUserIdentifier" =>
      <CFKeyedArchiverUID 0x600002854260 [0x1e3bcf9a0]>{value = 2}

      "mdmPaloadsByIdentifier" =>
      <CFKeyedArchiverUID 0x600002854280 [0x1e3bcf9a0]>{value = 263}

      "userSettingsByUserIdentifier" =>
      <CFKeyedArchiverUID 0x6000028542a0 [0x1e3bcf9a0]>{value = 257}
    }
    ...

    265 => {
      "$classes" => [
         0 => "Storage"
         1 => "NSObject"
      ]
      "$classname" => "Storage"
    }
    ...
}

序列化是将已初始化的内存对象转换成可保存格式(如文件)的过程。虽然序列化使程序与对象交互更高效,但序列化后的对象一般不可读。此外,如果这些对象属于未公开文档的类,我们必须先理解类的内部细节,才能编写代码正确解析它们。

作为后台任务管理(Background Task Management)子系统的一部分,Apple 提供了一个名为 sfltool 的命令行工具,它可以与 BackgroundItems-vx.btm 文件进行交互。如果使用 dumpbtm 参数执行该工具,它会反序列化并打印出文件内容:

# sfltool dumpbtm

#1:
                 UUID: 8C271A5F-928F-456C-B177-8D9162293BA7
                 Name: softwareupdate
       Developer Name: (null)
                 Type: legacy daemon (0x10010)
          Disposition: [enabled, allowed, visible, notified] (11)
           Identifier: com.apple.softwareupdate
                  URL: file:///Library/LaunchDaemons/com.apple.softwareupdate.plist
      Executable Path: /Users/User/.local/softwareupdate
           Generation: 1
    Parent Identifier: Unknown Developer

#2:
        UUID: 9B6C3670-2946-4F0F-B58C-5D163BE627C0
                 Name: ChmodBPF
       Developer Name: Wireshark
      Team Identifier: 7Z6EMTD2C6
                 Type: curated legacy daemon (0x90010)
          Disposition: [enabled, allowed, visible, notified] (11)
           Identifier: org.wireshark.ChmodBPF
                  URL: file:///Library/LaunchDaemons/org.wireshark.ChmodBPF.plist
      Executable Path: /Library/Application Support/Wireshark/ChmodBPF/ChmodBPF
           Generation: 1
    Assoc. Bundle IDs: [org.wireshark.Wireshark]
    Parent Identifier: Wireshark

在这个例子中,反序列化后的对象包括 DazzleSpy(softwareupdate)和 Wireshark 的 ChmodBPF 守护进程。由于 sfltool 能够从专有数据库生成反序列化输出,逆向分析它有助于我们理解其反序列化和解析逻辑。这反过来应该使我们能够编写自己的解析器,来程序化枚举后台任务管理子系统管理的所有持久化项目,包括任何恶意软件。

分析 sfltool

虽然本书重点不是逆向工程,但我会简要讲述如何分析 sfltool,以帮助你理解它与后台任务管理其他组件及极其重要的 .btm 文件的交互过程。在终端中,我们可以在运行带有 dumpbtm 参数的 sfltool 的同时,实时查看系统日志输出:

% log stream
...
backgroundtaskmanagementd: -[BTMService listener:shouldAcceptNewConnection:]:
connection=<NSXPCConnection: 0x152307aa0> connection from pid 52886 on mach service named com.apple.backgroundtaskmanagement

backgroundtaskmanagementd dumpDatabaseWithAuthorization: error=Error Domain=NSOSStatusErrorDomain Code=0 "noErr: Call succeeded with no error"

从日志输出(为简洁略作修改)可见,后台任务管理守护进程收到了来自进程 ID 为 52886(对应正在运行的 sfltool 实例)的消息。可以看到该工具已与守护进程建立了一个 XPC 连接。如果连接成功,sfltool 就能调用守护进程中的远程方法。例如,从日志消息可以看到它调用了守护进程的 dumpDatabaseWithAuthorization: 方法以获取后台任务管理数据库的内容。

在清单 5-1 中,我们尝试实现同样的方法,利用私有的 BackgroundTaskManagement 框架,该框架实现了必需的类如 BTMManager 和客户端方法 dumpDatabaseWithAuthorization:error:

#import <dlfcn.h>
#import <Foundation/Foundation.h>
#import <SecurityFoundation/SFAuthorization.h>

#define BTM_DAEMON "/System/Library/PrivateFrameworks/BackgroundTaskManagement.framework/Resources/backgroundtaskmanagementd"

@interface BTMManager : NSObject
    +(id)shared;
    -(id)dumpDatabaseWithAuthorization:(SFAuthorization*)arg1 error:(id*)arg2;
@end

int main(int argc, const char* argv[]) {
    void* btmd = dlopen(BTM_DAEMON, RTLD_LAZY);

    Class BTMManager = NSClassFromString(@"BTMManager");
    id sharedInstance = [BTMManager shared];

    SFAuthorization* authorization = [SFAuthorization authorization];
    [authorization obtainWithRight:"system.privilege.admin"
    flags:kAuthorizationFlagExtendRights error:NULL];

    id dbContents = [sharedInstance dumpDatabaseWithAuthorization:authorization error:NULL];
    ...
}

不幸的是,这种方法失败了。如下日志消息所示,失败的原因似乎是我们的程序(此处进程 ID 为 20987)缺少连接后台任务管理守护进程所需的 Apple 私有权限:

% log stream
...
backgroundtaskmanagementd: -[BTMService listener:shouldAcceptNewConnection:]:
process with pid=20987 lacks entitlement 'com.apple.private.backgroundtaskmanagement.manage'
or deprecated entitlement 'com.apple.private.coreservices.canmanagebackgroundtasks'

我们可以通过逆向守护进程中处理新客户端 XPC 连接的代码来确认这一点:

/* @class BTMService */
-(BOOL)listener:(NSXPCListener*)listener
shouldAcceptNewConnection:(NSXPCConnection*)newConnection {
    ...
    x24 = [x0 valueForEntitlement:@"com.apple.private.coreservices.canmanagebackgroundtasks"];
    ...
    if(objc_opt_isKindOfClass(x24, objc_opt_class(@class(NSNumber))) == 0x0 ||
    [x24 boolValue] == 0x0) {
        // 拒绝尝试连接的客户端
    }
}

这段反汇编代码中,我们看到对私有权限 com.apple.private.coreservices.canmanagebackgroundtasks 的检查,正是我们在日志中看到的权限名称。如果客户端没有该权限(或新的 com.apple.private.backgroundtaskmanagement.manage 权限),系统会拒绝连接。

使用 codesign 工具可以看到,sfltool 确实包含了所需的权限:

% codesign -d --entitlements - /usr/bin/sfltool
Executable=/usr/bin/sfltool
[Dict]
    [Key] com.apple.private.coreservices.canmanagebackgroundtasks
    [Value]
        [Bool] true
    [Key] com.apple.private.sharedfilelist.export
    [Value]
        [Bool] true

由于我们无法为自己的程序获取连接后台任务管理守护进程所需的 Apple 私有权限,我们只能直接访问并解析磁盘上的数据库文件。

获得完整磁盘访问权限后,访问该数据库的内容变得很容易。然而,解析其内容则需要更多工作,因为里面包含了未公开文档的序列化对象。幸运的是,进一步的逆向工程发现,当后台任务管理守护进程读取数据库内容时,其反序列化逻辑会从名为 _decodeRootData:error: 的方法开始:

-(void*)_decodeRootData:(NSData*)data error:(void**)arg3 {
    ...
    x0 = [NSKeyedUnarchiver alloc];
    x21 = [x0 initForReadingFromData:data error:&error];
    ...
    x0 = [x21 decodeObjectOfClass:objc_opt_class(@class(Storage)) forKey:@"store"];

当后台任务管理守护进程读取数据库内容时,它通过以下标准步骤执行反序列化:

  1. 将数据库内容读取到内存中,作为 NSData 对象。
  2. 使用该 NSData 数据初始化一个 NSKeyedUnarchiver 对象。
  3. 通过调用 NSKeyedUnarchiver 的 decodeObjectOfClass:forKey: 方法来反序列化对象。

请注意序列化类名 Storage 及其在归档器中的键 store,这些将在后续处理中发挥作用。同时需要注意,当调用 decodeObjectOfClass:forKey: 方法时,任何嵌入对象的 initWithCoder: 方法也会自动被调用,使得对象可以执行自身的反序列化逻辑。

编写后台任务管理数据库解析器

现在,我们准备编写自己的解析器。让我们利用逆向工程中学到的知识,写一个能够反序列化后台任务管理数据库中所有持久化项目元数据的工具。我将在这里逐步讲解相关代码片段,你也可以在 Objective-See 的 GitHub 仓库(github.com/objective-s…)找到该解析器的完整代码,名为 DumpBTM。最后,我会演示如何在自己的代码中使用这个库,来程序化获取任何 macOS 系统上持久化项目的列表。

查找数据库路径

首先,写段代码动态查找数据库路径。尽管数据库存放在 /private/var/db/com.apple.backgroundtaskmanagement/ 目录下,但 Apple 会随着 macOS 版本升级而提升文件名中的版本号。即便如此,我们可以通过唯一的扩展名 .btm 来轻松定位数据库文件。示例代码(清单5-2)用一个谓词查找该目录下所有 .btm 文件,通常只有一个,但为了保险,代码选择版本号最高的那个。

#define BTM_DIRECTORY @"/private/var/db/com.apple.backgroundtaskmanagement/"

NSURL* getPath(void) {
  NSArray* files = [NSFileManager.defaultManager contentsOfDirectoryAtURL:
    [NSURL fileURLWithPath:BTM_DIRECTORY] includingPropertiesForKeys:nil options:0 error:nil];

  NSArray* btmFiles = [files filteredArrayUsingPredicate:[NSPredicate
    predicateWithFormat:@"self.absoluteString ENDSWITH '.btm'"]];

  return btmFiles.lastObject;
}

代码先列出目录内所有文件(❶),用谓词筛选出 .btm 文件(❷),并返回版本最高的一个(❸)。

反序列化后台任务管理文件

后台任务管理文件中的序列化对象是该子系统专用的未公开类实例。要反序列化它们,至少需要提供类声明。我们在守护进程中发现了这些类,其中最顶层的序列化对象属于一个名为 Storage 的未公开类。它包含多个实例变量,其中有一个字典 itemsByUserIdentifier。为了反序列化 Storage,需要声明如下:

@interface Storage : NSObject <NSSecureCoding>
    @property(nonatomic, retain)NSDictionary* itemsByUserIdentifier;
@end

更进一步逆向显示,itemsByUserIdentifier 字典的值是另一个未公开的后台任务管理类 ItemRecordItemRecord 包含每个持久化项目的元数据,比如路径、代码签名信息、状态(启用或禁用)等。

同样,ItemRecord 也需在代码中声明,示例如下:

@interface ItemRecord : NSObject <NSSecureCoding>
    @property NSInteger type;
    @property NSInteger generation;
    @property NSInteger disposition;
    @property(nonatomic, retain)NSURL* url;
    @property(nonatomic, retain)NSString* identifier;
    @property(nonatomic, retain)NSString* developerName;
    @property(nonatomic, retain)NSString* executablePath;
    @property(nonatomic, retain)NSString* teamIdentifier;
    @property(nonatomic, retain)NSString* bundleIdentifier;
@end

实现 initWithCoder: 方法

反序列化过程会调用每个对象的 initWithCoder: 方法,且这些对象都遵循 NSSecureCoding 协议。我们需要为这些未公开类实现该方法以保证反序列化成功。通过反汇编,我们找到 ItemRecordinitWithCoder: 示例:

-(void*)initWithCoder:(NSCoder*)decoder {
  x0 = objc_opt_class(@class(NSUUID));
  x0 = [decoder decodeObjectOfClass:x0 forKey:@"uuid"];
  self.uuid = x0;

  x0 = objc_opt_class(@class(NSString));
  x0 = [decoder decodeObjectOfClass:x0 forKey:@"executablePath"];
  self.executablePath = x0;

  x0 = objc_opt_class(@class(NSString));
  x0 = [decoder decodeObjectOfClass:x0 forKey:@"teamIdentifier"];
  self.teamIdentifier = x0;
  ...
}

我们可以在代码中模仿它:

-(id)initWithCoder:(NSCoder *)decoder {
    self = [super init];
    if(nil != self) {
        self.uuid = [decoder decodeObjectOfClass:[NSUUID class] forKey:@"uuid"];
        self.executablePath = [decoder decodeObjectOfClass:[NSString class] forKey:@"executablePath"];
        self.teamIdentifier = [decoder decodeObjectOfClass:[NSString class] forKey:@"teamIdentifier"];
        ...
    }
    return self;
}

利用守护进程的实现简化反序列化

守护进程中已经实现了 StorageItemRecord 类的 initWithCoder: 方法。只要将守护进程二进制文件加载到我们的进程地址空间,就能直接调用这些方法,无需重写。

所有可执行文件现在都是位置无关的,所以我们可以将守护进程链接进来。示例代码(清单5-6)演示了如何加载守护进程并用其对象来完成反序列化:

#define BTM_DAEMON "/System/Library/PrivateFrameworks/\
BackgroundTaskManagement.framework/Resources/backgroundtaskmanagementd"

void* btmd = dlopen(BTM_DAEMON, RTLD_LAZY);

NSURL* path = getPath();
NSData* data = [NSData dataWithContentsOfURL:path options:0 error:NULL];

NSKeyedUnarchiver* keyedUnarchiver =
[[NSKeyedUnarchiver alloc] initForReadingFromData:data error:NULL];

Storage* storage = [keyedUnarchiver decodeObjectOfClass:
[NSClassFromString(@"Storage") class] forKey:@"store"];

代码先通过 dlopen 加载守护进程(❶),调用之前写好的函数获取数据库路径(❷),读入数据库内容到内存(❸),然后用 keyed unarchiver 初始化反序列化对象(❹),最后反序列化出顶层 Storage 对象(❺)。

以上是基于逆向和反序列化技术实现的后台任务管理数据库解析器的核心步骤讲解。你可以基于此开发工具,实现程序化枚举和分析 macOS 系统中所有持久化项(包括恶意软件)。

现在代码已准备好通过 keyed archiver 的 decodeObjectOfClass:forKey: 方法触发数据库中对象的反序列化。之前提到,数据库顶层对象的类名为 Storage。由于该类未公开,我们通过 NSClassFromString(@"Storage") 动态解析它。因为已经将实现该类的守护进程加载到进程空间,所以解析成功。为开始反序列化所需的键,我们模拟守护进程,指定字符串 "store"(❺)。

幕后,这段代码会触发调用 Storage 类的 initWithCoder: 方法,反序列化数据库中的顶层 Storage 对象。该对象包含一个字典,字典中存储了描述每个持久化项的 ItemRecord 对象。随后会自动调用 ItemRecord 类的 initWithCoder: 方法,反序列化这些嵌套对象。

访问元数据

完成反序列化后,我们可以访问由后台任务管理(Background Task Management)系统管理的每个持久化项的元数据(见清单5-7)。

int itemNumber = 0;

for(NSString* key in storage.itemsByUserIdentifier) {  // ❶
    NSArray* items = storage.itemsByUserIdentifier[key];  // ❷
    for(ItemRecord* item in items) {
        printf(" #%d\n", ++itemNumber);
        printf(" %s\n", [[item performSelector:NSSelectorFromString(@"dumpVerboseDescription")] UTF8String]);  // ❸
    }
}

访问元数据很简单,只需遍历 Storage 对象中 itemsByUserIdentifier 字典(❶),该字典按用户 UUID 分类存储持久化项(❷)。对于所有 ItemRecord 对象,我们调用其 dumpVerboseDescription 方法(❸)以格式化输出每个对象。由于该方法未在类接口声明,我们使用 Objective-C 的 performSelector: 动态按名称调用它。

编译并运行代码后,输出信息与 Apple 封闭源代码工具 sfltool 的输出相同:

% ./dumpBTM
Opened /private/var/db/com.apple.backgroundtaskmanagement/BackgroundItems-vx.btm
...
#1
    UUID: 8C271A5F-928F-456C-B177-8D9162293BA7
    Name: softwareupdate
    Developer Name: (null)
    Type: legacy daemon (0x10010)
    Disposition: [enabled, allowed, visible, notified] (11)
    Identifier: com.apple.softwareupdate
    URL: file:///Library/LaunchDaemons/com.apple.softwareupdate.plist
    Executable Path: /Users/User/.local/softwareupdate
    Generation: 1
    Parent Identifier: Unknown Developer

#2
    UUID: 9B6C3670-2946-4F0F-B58C-5D163BE627C0
    Name: ChmodBPF
    Developer Name: Wireshark
    Team Identifier: 7Z6EMTD2C6
    Type: curated legacy daemon (0x90010)
    Disposition: [enabled, allowed, visible, notified] (11)
    Identifier: org.wireshark.ChmodBPF
    URL: file:///Library/LaunchDaemons/org.wireshark.ChmodBPF.plist
    Executable Path: /Library/Application Support/Wireshark/ChmodBPF/ChmodBPF
    Generation: 1
    Assoc. Bundle IDs: [org.wireshark.Wireshark]
    Parent Identifier: Wireshark

由于大多数 macOS 恶意软件都会保持持久化状态,因此程序化枚举持久安装项的能力至关重要。不过,如上示例所示,枚举结果中也会包含诸如 Wireshark 的 ChmodBPF 守护进程这样的合法项目。

识别恶意项

当然,在尝试程序化检测恶意软件时,仅仅打印出持久化项的信息帮助不大。正如你刚才看到的,后台任务管理数据库中包含了许多无害持久化项的元数据,因此代码必须对每个项进行仔细检查。例如,工具输出中的第一个项看起来就很可疑;它的名字暗示它是苹果的核心组件,但它却运行于一个隐藏目录且未签名。(剧透:它就是 DazzleSpy。)而第二个项的代码签名信息,包括开发者名和团队 ID,显示它是网络监控与分析工具 Wireshark 的合法组件。

为了程序化地提取每个项的信息,你可以直接访问 ItemRecord 对象的相关属性。例如,清单 5-8 更新了之前清单 5-7 中的代码,访问每个项的属性列表路径、名称和可执行文件路径。

for(NSString* key in storage.itemsByUserIdentifier) {
    NSArray* items = storage.itemsByUserIdentifier[key];

    for(ItemRecord* item in items) {
        NSURL* url = item.url;
        NSString* name = item.name;
        NSString* path = item.executablePath;
        ...
    }
}

清单 5-8:访问 ItemRecord 属性

这里展示的代码摘自 DumpBTM 项目——一个完整的后台任务管理解析器。它被编译成库,方便链接到其他项目中。DumpBTM 还能将每个持久化项的元数据提取成字典,简洁地封装了未公开的后台任务管理对象的内部细节(见清单 5-9)。其他代码即可使用这个字典来检查每个项的异常情况,或应用启发式方法判断其是良性还是潜在恶意。

#define KEY_BTM_ITEM_URL @"url"
#define KEY_BTM_ITEM_UUID @"uuid"
#define KEY_BTM_ITEM_NAME @"name"
#define KEY_BTM_ITEM_EXE_PATH @"executablePath"

NSDictionary* toDictionary(ItemRecord* item) {
    NSMutableDictionary* dictionary = [NSMutableDictionary dictionary];

    dictionary[KEY_BTM_ITEM_UUID] = item.uuid;
    dictionary[KEY_BTM_ITEM_URL] = item.url;
    dictionary[KEY_BTM_ITEM_NAME] = item.name;
    dictionary[KEY_BTM_ITEM_EXE_PATH] = item.executablePath;
    ...
    return dictionary;
}

清单 5-9:将属性提取到字典中

提取 ItemRecord 对象的属性时,我们只需创建一个字典,并将每个属性以自定义键添加进去。

在 DumpBTM 库中,有一个导出的函数 parseBTM 会调用这里展示的 toDictionary 函数。本章结尾我会展示你的代码如何调用 parseBTM,从而程序化获取存储在后台任务管理数据库中所有持久化项的元数据字典。

在你自己的代码中使用 DumpBTM

当你编译 DumpBTM 后,会在其 library/lib 目录下找到两个文件:库的头文件(dumpBTM.h)和编译好的库文件 libDumpBTM.a。将这两个文件都添加到你的项目中。在源代码中通过 #include#import 指令包含头文件,因为这个文件包含了库导出的函数定义和常量。如果你在编译时链接了这个编译好的库,代码就可以调用库中导出的函数(见清单 5-10)。

#import "dumpBTM.h"
...

❷ NSDictionary* contents = parseBTM(nil);

❸ for(NSString* uuid in contents[KEY_BTM_ITEMS_BY_USER_ID]) {
    for(NSDictionary* item in contents[KEY_BTM_ITEMS_BY_USER_ID][uuid]) {
        // 在这里添加代码处理每个持久化项
    }
}

清单 5-10:枚举持久化项

导入库的头文件后 ❶,我们调用它导出的 parseBTM 函数 ❷。该函数返回一个字典,包含了后台任务管理子系统管理并存储在其数据库中的所有持久化项,字典以唯一用户标识符为键。代码遍历每个用户标识符,然后遍历对应的每个持久化项 ❸。

结论

识别持久安装项的能力对于检测恶意软件至关重要。在本章中,你学习了如何通过编程方式与 macOS 的后台任务管理(Background Task Management)数据库交互,该数据库包含了所有持久化启动项和登录项的元数据。虽然这个过程涉及了后台任务管理子系统的内部机制,但我们成功构建了一个完整的解析器,能够完全反序列化数据库中的所有对象,从而获取持久安装项的清单。

不过,需要注意的是,有些恶意软件采用了更为隐蔽的持久化机制,这些机制并不在后台任务管理子系统的追踪范围内,因此这些恶意软件不会出现在该子系统的数据库中。别担心;在第10章,我们将深入介绍 KnockKnock 这个工具,它利用了超出后台任务管理的技术手段,能全面发现系统中任何位置的持久恶意软件。

本章结束了第一部分关于数据收集的讨论。你现在已经准备好探索实时监控的世界,这将为主动检测方法奠定坚实基础。

备注

  1. Thomas Brewster,“Hackers Are Exposing an Apple Mac Weakness in Middle East Espionage”,Forbes,2018年8月30日,www.forbes.com/sites/thoma…
  2. Patrick Wardle,“Cyber Espionage in the Middle East: Unravelling OSX.WindTail”,VirusBulletin,2019年10月3日,www.virusbulletin.com/uploads/pdf…
  3. Marc-Etienne M. Léveillé 和 Anton Cherepanov,“Watering Hole Deploys New macOS Malware, DazzleSpy, in Asia”,We Live Security,2022年1月25日,www.welivesecurity.com/2022/01/25/…
  4. “Updating Helper Executables from Earlier Versions of macOS”,Apple Developer Documentation,developer.apple.com/documentati…
  5. 如果你对后台任务管理子系统的内部机制感兴趣,包括如何逆向工程以理解其组成部分,可以参考我2023年DEF CON的演讲“Demystifying (& Bypassing) macOS’s Background Task Management”,speakerdeck.com/patrickward…