前不久,我的项目需要对一个 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:]
方法。注意,需要提前声明NSString
和NSArray
为 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)