前不久,我的项目需要对一个 iOS 设备的系统级进程(Daemon)进行动态分析,这个进程由许多XPC活动(XPC Activities)组成。我们可以在该进程的.plist配置文件(位于 /System/Library/LaunchDaemons)内的LaunchEvents字段下找到这些 XPC 活动:

# audioanalyticsd 为示例Daemon
$ plutil -p "/System/Library/LaunchDaemons/com.apple.audioanalyticsd.plist"

# ...
  "Label" => "com.apple.audioanalyticsd"
  "LaunchEvents" => {
    # 该Daemon的XPC活动
    "com.apple.xpc.activity" => {
      "com.apple.audioanalyticsd.assets.refresh" => {
        "AllowBattery" => 0
        "GracePeriod" => 1800
        "Interval" => 43200  # 运行间隔 12h = 43200s/3600s
        "Priority" => "Maintenance"
        "Repeating" => 1
      }
    }
  }
  "ProgramArguments" => [
    0 => "/usr/libexec/audioanalyticsd"
  ]
# ...

但这些 XPC 活动常常有定义好的运行周期,示例中为12小时,有些更长达24小时;有的 XPC 活动还需要等待系统条件满足才能运行。这导致对它们动态分析实在很困难。所以我想寻找能手动运行这些 XPC 活动的方法。

Bryce 的这篇文章给了我很多启发。他通过观察 Console 应用中的日志,发现有个叫做dasd(DuetActivitySchedulerDaemon)的进程负责决定什么时候运行什么 XPC 活动。他尝试使用调试工具 lldb 调试dasd进程:先获得_DASDaemon类实例,再调用它的方法‑[_DASDaemon forceRunActivities:]。这成功触发了对应的XPC活动!

(lldb) e (void)[[_DASDaemon sharedInstance] 
	forceRunActivities:@[@"com.apple.CacheDelete.daily"]]

接下来他构建了一个命令行工具,这个工具将改写后的forceRunActivities:completion:方法放在新协议中,再使用 Theos hook了XPC通信接口类NSXPCInterface,并且在_DASDaemonClient(与_DASDaemon选择器相同)加载后,经过XPC通信用新协议新方法替换掉dasd中的原协议的原方法,以调用指定XPC活动并提供反馈。

但这个方法还是过于复杂了。我们可以用 Frida 加载脚本来hook dasd进程。

测试:Frida 实时调用XPC活动

我们的思路很清晰:先访问_DASDaemon类的实例,再通过该实例调用‑[_DASDaemon forceRunActivities:]方法。

首先使用 Frida 来动态调试 dasd

frida -U dasd

尝试获取_DASDaemon类的实例,并将实例作为一个 Objective-C 对象。

// 获取_DASDaemon类的实例
-> sharedInstance = ObjC.classes._DASDaemon.sharedInstance();
// 将实例转化为ObjC对象
-> sharedInstance = new ObjC.Object(sharedInstance);

获得实例后,尝试调用forceRunActivities:方法。根据Bryce在lldb中的测试,我们了解到该方法的参数是一个数组,数组中是表示「XPC活动」名称的字符串。因此,我们使用[NSString stringWithString:]方法将想启动的「XPC活动」的名称存入一个 NSString 类型的字符串,再将字符串放入一个NSArray类型的数组,作为参数传入‑[_DASDaemon forceRunActivities:]方法。注意,需要提前声明NSStringNSArray为 Objective-C 的类,否则会报错。

// 声明ObjC的类
-> const { NSString, NSArray } = ObjC.classes;
// 初始化ObjC字符串并赋值
-> const activity_string = NSString['stringWithString:']('com.apple.audioanalyticsd.assets.refresh');
// 初始化ObjC数组并赋值
-> const activity_array = NSArray.alloc().initWithObject_(activity_string);
// 调用实例的方法并传参
-> sharedInstance.forceRunActivities_(activity_array);

在 Console 应用中,我们观察到特定的 XPC Activities 启动成功!接下来,我们把它写成一个JS脚本,以便直接加载。

完成:Frida 脚本调用XPC活动

根据上一步的方法,我们构建了以下的JS脚本,并将调用‑[_DASDaemon forceRunActivities:]方法的行为封装进一个函数run_activity

// dasd_schedule_activity.js

console.log("reloaded script");

var sharedInstance = ObjC.classes._DASDaemon.sharedInstance();
sharedInstance = new ObjC.Object(sharedInstance);
console.log("Got DASDaemon instance: " + sharedInstance);

function run_activity(activity_name) {
    const {NSString, NSArray} = ObjC.classes;
    const activity_string = NSString['stringWithString:'](activity_name);
    const activity_array = NSArray.alloc().initWithObject_(activity_string);

    sharedInstance.forceRunActivities_(activity_array);
    console.log("ran activity");
}
console.log("run_activity('xpc_activity_name')");

使用 Frida 加载 JS 脚本,再运行run_activity函数来调用任意XPC活动:

frida -U dasd --load dasd_schedule_activity.js
-> run_activity('your_xpc_activity_name')

完成!现在你可以按需启动 XPC 活动了。

(特别鸣谢 Jiska