攻击 macOS XPC 助手:协议逆向工程 ylc3000 2025-11-13 0 浏览 0 点赞 长文 # Attacking macOS XPC Helpers: Protocol Reverse Engineering / 攻击 macOS XPC 助手:协议逆向工程 --- - **Date / 日期:** November 1, 2025 / 2025年11月1日 - **Read Time / 阅读时间:** 14 min read / 14 分钟阅读 - **Tags / 标签:** `macos`, `xpc`, `objective c`, `red teaming` --- ### Context / 背景 It’s been a while since I started poking around [Mickey Jin’s research post about Sandbox escapes](https://jhftss.github.io/A-New-Era-of-macOS-Sandbox-Escapes/). It was the first type of vulnerability I experimented with. I’d like to share the mistakes I made and how I overcame them. 距离我开始研究 [Mickey Jin 关于沙箱逃逸的研究文章](https://jhftss.github.io/A-New-Era-of-macOS-Sandbox-Escapes/)已经有一段时间了。这是我试验的第一种漏洞类型。我想分享我犯过的错误以及我是如何克服它们的。 In this post, I'll show you how to: 在这篇文章中,我将向你展示如何: * Filter existing XPC helpers * Check whether a service accepts connections * Script an XPC client in Objective-C * 筛选现有的 XPC 助手 * 检查服务是否接受连接 * 用 Objective-C 编写 XPC 客户端脚本 In short, this post contains what I wished I had found in Mickey Jin’s original article when I started. 简而言之,这篇文章包含了我刚开始时希望能在 Mickey Jin 的原文中找到的内容。 ### XPC helpers of type application / 应用类型的 XPC 助手 This post focuses on connecting to XPC helpers of type Application. These helpers accept requests from other processes, which makes them interesting to attack. For a deeper explanation, see Mickey Jin’s original post. 本文重点介绍如何连接到应用类型的 XPC 助手。这些助手接受来自其他进程的请求,这使得它们成为有趣的攻击目标。更深入的解释,请参阅 Mickey Jin 的原文。 First, let’s find where these services live. Below I share a Python script that searches common locations for `.xpc` bundles and reports those whose processes are running. 首先,让我们找到这些服务的存放位置。下面我分享一个 Python 脚本,它会搜索 `.xpc` 包的常见位置,并报告那些正在运行的进程。 ```python import os import subprocess import plistlib from pathlib import Path def main(): print("Lookup XPC Services Application Type (Living Services Only)") output_file = "xpc_services_check.txt" # Clear the output file with open(output_file, 'w') as f: f.write("") # Find all .xpc directories xpc_path = Path('/System/Library/PrivateFrameworks') xpc_services = list(xpc_path.rglob('*.xpc')) living_services_count = 0 for service in xpc_services: plist_path = service / 'Contents' / 'Info.plist' # First check if the service is alive if not check_process_alive(service): continue # If we get here, the service is alive, now check if it's an Application type if plist_path.exists() and check_service_type(plist_path): living_services_count += 1 print(f"Found living service: {service}") with open(output_file, 'a') as f: # Write service info service_info = f"Service found: {service}\n" print(service_info, end='') f.write(service_info) # Get and write bundle ID bundle_id = get_bundle_id(plist_path) bundle_info = f" Bundle ID: {bundle_id}\n" print(bundle_info, end='') f.write(bundle_info) # Process status (we know it's alive) process_status = " ✅ Process is running\n" print(process_status, end='') f.write(process_status) # Get and write entitlements entitlements = get_entitlements(service) if entitlements == ['None']: entitlements_info = " Entitlements: None specified\n" else: entitlements_info = f" Entitlements: {', '.join(entitlements)}\n" print(entitlements_info, end='') f.write(entitlements_info) # Add blank line print() f.write("\n") print(f"\nAnalysis complete. Found {living_services_count} living XPC services.") print(f"Output written to {output_file}") def run_command(cmd): """Run a shell command and return its output.""" try: result = subprocess.run(cmd, shell=True, capture_output=True, text=True) return result.stdout.strip() except subprocess.CalledProcessError: return "" def check_service_type(plist_path): """Check if the Info.plist contains Application ServiceType.""" try: with open(plist_path, 'rb') as f: plist_data = plistlib.load(f) service_type = plist_data.get('XPCService', {}).get('ServiceType') return service_type == 'Application' except Exception: return False def get_bundle_id(plist_path): """Get the bundle ID from the Info.plist file.""" try: with open(plist_path, 'rb') as f: plist_data = plistlib.load(f) return plist_data.get('CFBundleIdentifier', 'Not found') except Exception: return 'Not found' def get_entitlements(service_path): """Get entitlements for the service.""" cmd = f'codesign -d --entitlements :- "{service_path}"' output = run_command(cmd) entitlements = [line for line in output.split('\n') if 'com.apple.private' in line or 'com.apple.security' in line] return entitlements if entitlements else ['None'] def check_process_alive(service_path): """Check if the process is running.""" cmd = f'ps aux | grep "{service_path}/Contents/MacOS" | grep -v grep' return bool(run_command(cmd)) if __name__ == "__main__": main() ``` You can tweak the `xpc_path` variable to search for `/System/Library/Frameworks` instead of private frameworks. 你可以调整 `xpc_path` 变量来搜索 `/System/Library/Frameworks` 而不是私有框架。 The script should print entries that look like this: 该脚本应打印出如下所示的条目: ```console Service found: /System/Library/PrivateFrameworks/AppPredictionFoundation.framework/Versions/A/XPCServices/AppPredictionIntentsHelperService.xpc Bundle ID: com.apple.proactive.AppPredictionIntentsHelperService ✅ Process is running Entitlements: <?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict><key>com.apple.intents.extension.discovery</key><true/><key>com.apple.private.coreservices.canmaplsdatabase</key><true/><key>com.apple.security.exception.files.absolute-path.read-only</key><array><string>/private/var/containers/Bundle/Application/</string><string>/Applications/</string></array><key>com.apple.security.exception.files.home-relative-path.read-only</key><array><string>/Library/Caches/GeoServices/</string></array><key>com.apple.security.exception.mach-lookup.global-name</key><array><string>com.apple.remindd</string><string>com.apple.calaccessd</string></array><key>com.apple.security.exception.shared-preference.read-only</key><array><string>com.apple.GEO</string><string>com.apple.AppSupport</string><string>com.apple.coremedia</string></array><key>com.apple.security.ts.geoservices</key><true/><key>platform-application</key><true/><key>seatbelt-profiles</key><array><string>temporary-sandbox</string></array></dict></plist> ``` Now lets reverse the content of this binary. 现在让我们来逆向这个二进制文件的内容。 ### Should this service accept a new connection? / 该服务是否应该接受新连接? This is the whole question! 这才是问题的关键! Before sending any request to an XPC helper, we must first establish a connection. 在向 XPC 助手发送任何请求之前,我们必须首先建立一个连接。 On the helper’s side, it decides whether to accept the connection or not. To do this, the helper implements the `NSXPCListenerDelegate` interface, which defines the `shouldAcceptNewConnection` method. This method returns `YES` or `NO` (`true` or `false`) to accept or reject incoming connections. 在助手端,它会决定是否接受连接。为此,助手实现了 `NSXPCListenerDelegate` 接口,该接口定义了 `shouldAcceptNewConnection` 方法。此方法返回 `YES` 或 `NO` (`true` 或 `false`) 来接受或拒绝传入的连接。 From a researcher’s point of view, we’re interested in two cases: 从研究者的角度来看,我们对两种情况感兴趣: * The method always returns `YES` and accepts any connection blindly. * The method contains logic that can be bypassed. * 方法总是返回 `YES`,盲目地接受任何连接。 * 方法包含可以被绕过的逻辑。 Using `AppPredictionIntentsHelperService` as an example, let’s inspect the implementation of this method in the disassembler. 以 `AppPredictionIntentsHelperService` 为例,让我们在反汇编器中检查此方法的实现。 ```console 100000d34 bool -[ServiceDelegate listener:shouldAcceptNewConnection:](struct ServiceDelegate* self, 100000d34 SEL sel, id listener, id shouldAcceptNewConnection) 100000d34 { 100000d34 id obj = [shouldAcceptNewConnection retain]; 100000d78 id obj_1 = [[NSXPCInterface interfaceWithProtocol: 100000d78 &protocol_AppPredictionIntentsHelperServiceProtocol] retain]; 1000036f4 [obj setExportedInterface:obj_1]; 100000d90 [obj_1 release]; 100000d9c id obj_2 = [AppPredictionIntentsHelperService new]; 100003714 [obj setExportedObject:obj_2]; 100003534 [obj _setQueue:self->_queue]; 100003674 [obj resume]; 100000dc8 [obj release]; 100000dd0 [obj_2 release]; 100000de4 return 1; 100000d34 } ``` > **Note:** I used the pseudo Objective-C representation of Binary Ninja to have that result. > **注意:** 我使用了 Binary Ninja 的伪 Objective-C 表示来得到这个结果。 What do we have here? A basic XPC helper setup. The service defines its protocol and exported object. This object handles the incoming RPC requests. 我们这里有什么?一个基本的 XPC 助手设置。该服务定义了其协议和导出的对象。这个对象处理传入的 RPC 请求。 But the interesting part is located at `100000de4`: the function simply returns `1` (or `true`) so it accepts every incoming connection. 但有趣的部分位于 `100000de4`:该函数简单地返回 `1` (或 `true`),因此它接受每一个传入的连接。 In the real world use case it won't be enough. The next step is to check the interface of `AppPredictionIntentsHelperService` to see if there is something exploitable. 在实际用例中,这还不够。下一步是检查 `AppPredictionIntentsHelperService` 的接口,看看是否有可利用之处。 > **Note:** Keep in mind we won’t exploit anything in this post. We only show how to establish a connection and call XPC methods. > **注意:** 请记住,我们在这篇文章中不会利用任何漏洞。我们只展示如何建立连接和调用 XPC 方法。 ### Reverse XPC interface / 逆向 XPC 接口 For the sake of this practical post, I just picked a method and will try to call it. 为了这篇实践性文章的目的,我只选择了一个方法并尝试调用它。 ```console 100002bb0 void -[AppPredictionIntentsHelperService createEventIntentWithStartDate:endDate:withReply:]( 100002bb0 struct AppPredictionIntentsHelperService* self, SEL sel, id createEventIntentWithStartDate, 100002bb0 id endDate, id reply, void* arg) ``` The first step is to deduce types passed to this function. 第一步是推断传递给此函数的类型。 ```objc 100002bec id start_date = [createEventIntentWithStartDate retain]; 100002bf8 id end_date = [endDate retain]; ... 100002c38 id event = [[EKEvent eventWithEventStore:event_store] retain]; ... 100003734 [event setStartDate:start_date]; 1000036d4 [event setEndDate:end_date]; ``` > **Note:** I renamed the two first variables myself for readability > **注意:** 为了可读性,我自己重命名了前两个变量。 From the disassembly we can deduce that the method accepts two date arguments: `startDate` and `endDate`. The code passes these values to an `EKEvent` instance via `setStartDate:` and `setEndDate:`, so they are `NSDate *` objects. 从反汇编中我们可以推断出该方法接受两个日期参数:`startDate` 和 `endDate`。代码通过 `setStartDate:` 和 `setEndDate:` 将这些值传递给一个 `EKEvent` 实例,所以它们是 `NSDate *` 对象。 > For instance, [in the EKEvent documentation](https://developer.apple.com/documentation/eventkit/ekevent/startdate?language=objc) we can read that we have a `NSDate *`. > 例如,在 [EKEvent 文档](https://developer.apple.com/documentation/eventkit/ekevent/startdate?language=objc)中我们可以读到它是一个 `NSDate *`。 We still need to know the interface of the third parameter which is a callback (`reply`). 我们还需要知道第三个参数的接口,它是一个回调函数(`reply`)。 ```console 100002c9c id obj = [_INIntentWithTypedIntent(event_intent) retain]; ... 100002cdc reply_cb(reply_cb, obj, 0); ``` The disassembly shows the service building an INIntent like object and then invoking the reply callback with that object and an integer. In other words, the callback receives an INIntent (or similar) and a numeric result. 反汇编显示,该服务构建了一个类似 INIntent 的对象,然后用该对象和一个整数调用了 reply 回调。换句话说,回调接收一个 INIntent(或类似对象)和一个数值结果。 Putting that together, the interface can be described as: 综上所述,该接口可以描述为: ```objc @protocol AppPredictionIntentsHelperServiceProtocol - (void)createEventIntentWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate withReply:(void (^)(id, id))block; @end ``` Now let's try to build a script for connecting to this XPC helper. 现在让我们尝试构建一个连接到这个 XPC 助手的脚本。 ### The script / 脚本 To interact with the XPC helper, we’ll follow these steps: 要与 XPC 助手交互,我们将遵循以下步骤: * Declare a protocol representing the helper’s interface. * Load the corresponding framework. * Establish an XPC connection to the target service. * Invoke the desired method. * 声明一个代表助手接口的协议。 * 加载相应的框架。 * 建立到目标服务的 XPC 连接。 * 调用所需的方法。 My advice is to take it step by step: compile the code and test your binary after each change. 我的建议是循序渐进:每次更改后都编译代码并测试你的二进制文件。 You can compile your script using `clang -framework Foundation -o xpc_ping xpc_ping.m` 你可以使用 `clang -framework Foundation -o xpc_ping xpc_ping.m` 来编译你的脚本。 You’ll notice that I didn’t use a sandboxed app, even though that’s the main focus of this research. This choice is simply for convenience, as I work primarily in Neovim and prefer to stay out of Xcode whenever possible. Later in this post, I’ll show that this snippet works perfectly fine inside a sandboxed application. 你会注意到我没有使用沙盒应用,尽管这是这项研究的主要焦点。这个选择纯粹是为了方便,因为我主要在 Neovim 中工作,并尽可能避免使用 Xcode。在本文后面,我将展示这个代码片段在沙盒应用中也能完美运行。 #### The AppPredictionIntentsHelperServiceProtocol protocol / AppPredictionIntentsHelperServiceProtocol 协议 The first thing you should know is that you don’t have to reverse each method of the protocol. 你应该知道的第一件事是,你不必逆向协议的每个方法。 You can focus on the methods you want to use for your exploit. Here, I picked two methods that we’ll use for this post. 你可以专注于你想用于漏洞利用的方法。在这里,我挑选了两个我们将在本文中使用的方法。 ```objc @protocol AppPredictionIntentsHelperServiceProtocol - (void)localizedStringForLinkString:(id)str withReply:(void (^)(id, id))block; - (void)createEventIntentWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate withReply:(void (^)(id, id))block; @end ``` #### Load the AppPredictionFoundation Framework / 加载 AppPredictionFoundation 框架 As Mickey said in his blog post, the first step is to load the framework: 正如 Mickey 在他的博客文章中所说,第一步是加载框架: ```objc bool ok = [[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/" @"AppPredictionFoundation.framework/"] load]; NSLog(@"AppPredictionFoundation loaded=%d", ok); ``` #### Establish the XPC connection / 建立 XPC 连接 There’s a bit of boilerplate required to establish the XPC connection: 建立 XPC 连接需要一些样板代码: ```objc // Initialize the XPC connection (bundle ID you found) NSXPCConnection *conn = [[NSXPCConnection alloc] initWithServiceName: @"com.apple.proactive.AppPredictionIntentsHelperService"]; // Set the remote interface using the protocol conn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol( AppPredictionIntentsHelperServiceProtocol)]; // Interruption & invalidation handlers (console logging) [conn setInterruptionHandler:^{ NSLog(@"Connection was interrupted. The service may have terminated or " @"rejected the connection."); }]; [conn setInvalidationHandler:^{ NSLog(@"Connection was invalidated. Check Console.app for service logs."); }]; // Resume connection [conn resume]; ``` We basically: 我们基本上做了以下几步: * Initialize the `NSXPCConnection` object * Declare the remote object interface using our Protocol * Set invalidation and interruption handlers * Resume the connection * 初始化 `NSXPCConnection` 对象 * 使用我们的协议声明远程对象接口 * 设置失效和中断处理程序 * 恢复连接 #### Call the method / 调用方法 Now we need a proxy object to call the XPC method: 现在我们需要一个代理对象来调用 XPC 方法: ```objc id proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err) { NSLog(@"[proxy error] %@", err); }]; ``` Once the proxy is created, we can invoke the desired method on it... 一旦代理被创建,我们就可以在其上调用所需的方法... ```objc NSDate *start = [[NSDate alloc] init]; NSDate *end = [NSDate dateWithTimeIntervalSinceNow:3 * 60 * 60]; [proxy createEventIntentWithStartDate:start endDate:end withReply:^(id obj, id result) { NSLog(@"[reply] obj=%@ result=%@", obj, result); }]; ``` ... and run the script: ... 然后运行脚本: ```console ./xpc_ping 2025-10-31 19:37:01.432 apppredict_ping[89398:64224964] AppPredictionFoundation loaded=1 2025-10-31 19:37:01.504 apppredict_ping[89398:64224976] [proxy error] Error Domain=NSCocoaErrorDomain Code=4101 "connection to service with pid 89399 named com.apple.proactive.AppPredictionIntentsHelperService" UserInfo={NSDebugDescription=connection to service with pid 89399 named com.apple.proactive.AppPredictionIntentsHelperService} ``` Now it’s time to open the Console application to see why we got this error. 现在是时候打开“控制台”应用程序,看看我们为什么会收到这个错误。 ### Special object type / 特殊对象类型 Open the Console application and enter `AppPredictionIntentsHelperService` in the search bar. 打开“控制台”应用程序,并在搜索栏中输入 `AppPredictionIntentsHelperService`。  By picking the error message (the one with the little red circle), you should see something like: 通过选择错误消息(带有小红圈的那个),你应该会看到类似下面的内容: ```console [...] Exception: value for key '<no key>' was of unexpected class 'INIntent' (0x1f90db5e0) [/System/Library/Frameworks/Intents.framework]. Allowed classes are: {( "'NSDate' (0x1f8fa5688) [/System/Library/Frameworks/CoreFoundation.framework]", "'NSArray' (0x1f8fa5598) [/System/Library/Frameworks/CoreFoundation.framework]", "'NSString' (0x1f8fac3b8) [/System/Library/Frameworks/Foundation.framework]", "'NSNumber' (0x1f8fabeb8) [/System/Library/Frameworks/Foundation.framework]", "'NSDictionary' (0x1f8fa56d8) [/System/Library/Frameworks/CoreFoundation.framework]", "'NSData' (0x1f8fa5660) [/System/Library/Frameworks/CoreFoundation.framework]" )} [...] ``` Basically, it says the interface contains the `INIntent` class, which is not allowed. 基本上,它说接口包含了 `INIntent` 类,而这是不允许的。 We have to add this class to the list of allowed classes: 我们必须将这个类添加到允许的类列表中: ```objc NSSet *allowedClasses = [NSSet setWithObjects:[NSDate class], [NSString class], [NSNumber class], [NSDictionary class], [NSArray class], [NSData class], NSClassFromString(@"INIntent"), nil]; [conn.remoteObjectInterface setClasses:allowedClasses forSelector:@selector(createEventIntentWithStartDate: endDate:withReply:) argumentIndex:0 // reply block argument 0 (first parameter) ofReply:YES]; ``` I simply created an `NSSet` with the allowed classed mentioned in the error message and added the `INIntent`. 我只是用错误消息中提到的允许的类创建了一个 `NSSet`,并添加了 `INIntent`。 Now if I run it again: 现在如果我再次运行它: ```console 2025-10-31 21:19:20.194 apppredict_ping[97075:64332158] AppPredictionFoundation loaded=1 2025-10-31 21:19:20.196 apppredict_ping[97075:64332158] done 2025-10-31 21:19:20.245 apppredict_ping[97075:64332172] [reply] obj=<INIntent: 0x6000004b8000> { allDay = 0; endDate = <INObject: 0x6000023997c0> { pronunciationHint = <null>; displayString = Sat 1 Nov at 00:19; subtitleString = <null>; identifier = 783645560.000000#Europe/Paris; alternativeSpeakableMatches = ( ); }; startDate = <INObject: 0x600002399740> { pronunciationHint = <null>; displayString = Fri 31 Oct at 21:19; subtitleString = <null>; identifier = 783634760.000000#Europe/Paris; alternativeSpeakableMatches = ( ); }; locationName = <null>; title = ; locationAddress = <null>; geolocation = <null>; } result=(null) ``` Bingo! I successfully contacted an XPC service of type Application from a sandboxed application. 成功了!我成功地从一个沙盒应用程序中联系了一个应用类型的 XPC 服务。 > **Note:** For the sake of this tutorial I used an standalone script. But it will works the same inside a macOS sandboxed application. > **注意:** 为了本教程,我使用了一个独立的脚本。但它在 macOS 沙盒应用程序中同样有效。  Now, I would like to test the other method from the protocol. 现在,我想测试协议中的另一个方法。 ### Interface with other private interfaces / 与其他私有接口交互 Let's comment how first call and add a new one: 让我们注释掉第一个调用,并添加一个新的: ```objc [proxy localizedStringForLinkString:@"lol" withReply:^(id obj, id result) { NSLog(@"[reply] obj=%@ result=%@", obj, result); }]; ``` You’ll notice that I use random parameters here; this is mostly because I ignore the domain for this exercise. 你会注意到我在这里使用了随机参数;这主要是因为我在此练习中忽略了领域知识。 Let's compile and run the script again: 让我们再次编译并运行脚本: ```console 2025-10-31 22:38:05.951 apppredict_ping[2028:64381904] AppPredictionFoundation loaded=1 2025-10-31 22:38:05.965 apppredict_ping[2028:64381905] Connection was interrupted. The service may have terminated or rejected the connection. ``` Now our `setInterruptionHandler` is triggered! Let’s inspect the logs again: 现在我们的 `setInterruptionHandler` 被触发了!让我们再次检查日志: ```console Exception: value for key '<no key>' was of unexpected class 'NSString' (0x1f8fac3b8) [/System/Library/Frameworks/Foundation.framework]. Allowed classes are: {( "'LNStaticDeferredLocalizedString' (0x28c7ec9f8) [/System/Library/PrivateFrameworks/LinkMetadata.framework]" )} ``` In the example above I used an `NSString` literal, but an `LNStaticDeferredLocalizedString` is required here. 在上面的例子中,我使用了一个 `NSString` 字面量,但这里需要一个 `LNStaticDeferredLocalizedString`。 But before we can pass it to the function, we should reverse the framework that contains it to understand how to create an instance. 但在我们能将它传递给函数之前,我们应该逆向包含它的框架,以了解如何创建一个实例。 Fortunately, the logs above include a helpful hint: `System/Library/PrivateFrameworks/LinkMetadata.framework.` 幸运的是,上面的日志包含了一个有用的提示:`System/Library/PrivateFrameworks/LinkMetadata.framework`。 ### Reverse LinkMetadata framework / 逆向 LinkMetadata 框架 Instead of starting an old fashioned reverse approach (like for the previous case), I would like to share with you another approach that has worked for me with private frameworks. 与之前的情况不同,我不想采用老式的逆向方法,而是想与你分享另一种对我处理私有框架有效的方法。 You can check [this website, which lists a ton of private frameworks interfaces](https://developer.limneos.net/index.php). 你可以查看[这个网站,它列出了大量的私有框架接口](https://developer.limneos.net/index.php)。 You can search for `LinkMetadata`, for instance: 例如,你可以搜索 `LinkMetadata`:  But for our use case, I'd rather target the class name: 但对于我们的用例,我更倾向于直接搜索类名:  If you scroll a bit, you’ll find a bunch of initializers; I’ll take one of them: 如果你向下滚动一点,你会发现一堆初始化方法;我将选择其中一个: ```objc -(id)initWithKey:(id)arg1 table:(id)arg2 bundleURL:(id)arg3 ; ``` The issue here is that every parameter is typed as `id`, which isn’t very informative. We can still look at the class properties to see which ones match the same names: 这里的问题是每个参数都被类型化为 `id`,这提供不了太多信息。我们仍然可以查看类的属性,看看哪些属性与这些名称匹配: ```objc @property (nonatomic,copy,readonly) NSString* key; @property (nonatomic,copy,readonly) NSString* defaultValue; @property (nonatomic,copy,readonly) NSString* table; @property (nonatomic,copy,readonly) NSURL* bundleURL; ``` Which leads me to think the real initializer signature could be: 这让我认为真正的初始化方法签名可能是: ```objc -(id)initWithKey:(NSString *)arg1 table:(NSString *)arg2 bundleURL:(NSURL *)arg3 ;``` This is less cumbersome than the technique we used for the first XPC call. Now it’s time to try this initializer. 这比我们第一次调用 XPC 时使用的技术要简单。现在是时候尝试这个初始化方法了。 #### Call LNStaticDeferredLocalizedString constructor / 调用 LNStaticDeferredLocalizedString 构造函数 Let’s continue with the lazy approach and ask Claude how to call this interface. 让我们继续用懒人方法,问问 Claude 如何调用这个接口。 ```objc id instance = [NSClassFromString(@"LNStaticDeferredLocalizedString") alloc]; NSInvocation *inv = [NSInvocation invocationWithMethodSignature: [instance methodSignatureForSelector: NSSelectorFromString(@"initWithKey:table:bundleURL:")]]; [inv setSelector:NSSelectorFromString(@"initWithKey:table:bundleURL:")]; [inv setTarget:instance]; NSString *k = @"KEY", *t = @"Localizable"; NSURL *u = [NSBundle mainBundle].bundleURL; [inv setArgument:&k atIndex:2]; [inv setArgument:&t atIndex:3]; [inv setArgument:&u atIndex:4]; [inv invoke]; id result; [inv getReturnValue:&result]; [proxy localizedStringForLinkString:result withReply:^(id obj, id result) { NSLog(@"[reply] obj=%@ result=%@", obj, result); }]; ``` Surprisingly, this is a method I’d never used; I generally use `Class`, `SEL`, and `objc_msgSend`. 令人惊讶的是,这是一个我从未使用过的方法;我通常使用 `Class`、`SEL` 和 `objc_msgSend`。 Let’s add it to our script, recompile, and run it! 让我们把它添加到我们的脚本中,重新编译并运行它! ```objc 2025-11-01 18:20:04.140 apppredict_ping[54416:65126968] AppPredictionFoundation loaded=1 2025-11-01 18:20:04.142 apppredict_ping[54416:65126968] done 2025-11-01 18:20:04.152 apppredict_ping[54416:65126981] [reply] obj=KEY result=(null) ``` It worked! 成功了! ### Wrap up / 总结 In this post, I tried to show the methodology I use to reverse-engineer the interface of a private XPC helper. Keep in mind that the big challenge is finding an actual exploitable logic bug. 在这篇文章中,我试图展示我用来逆向工程一个私有 XPC 助手接口的方法。请记住,真正的挑战在于找到一个实际可利用的逻辑漏洞。 This is the full script, enjoy! 这是完整的脚本,尽情享用吧! ```objc #import <Foundation/Foundation.h> #import <objc/message.h> @protocol AppPredictionIntentsHelperServiceProtocol - (void)localizedStringForLinkString:(id)str withReply:(void (^)(id, id))block; - (void)createEventIntentWithStartDate:(NSDate *)startDate endDate:(NSDate *)endDate withReply:(void (^)(id, id))block; @end int main(int argc, const char *argv[]) { @autoreleasepool { pid_t pid = getpid(); NSLog(@"PID: %d", pid); // load associated framework (as in your template) bool ok = [[NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/" @"AppPredictionFoundation.framework/"] load]; NSLog(@"AppPredictionFoundation loaded=%d", ok); // Initialize the XPC connection (bundle ID you found) NSXPCConnection *conn = [[NSXPCConnection alloc] initWithServiceName: @"com.apple.proactive.AppPredictionIntentsHelperService"]; // Set the remote interface using the protocol conn.remoteObjectInterface = [NSXPCInterface interfaceWithProtocol:@protocol( AppPredictionIntentsHelperServiceProtocol)]; // Interruption & invalidation handlers (console logging) [conn setInterruptionHandler:^{ NSLog(@"Connection was interrupted. The service may have terminated or " @"rejected the connection."); }]; [conn setInvalidationHandler:^{ NSLog(@"Connection was invalidated. Check Console.app for service logs."); }]; // Resume connection [conn resume]; // Call the simple selector (matches your disassembly: object + block) id proxy = [conn remoteObjectProxyWithErrorHandler:^(NSError *err) { NSLog(@"[proxy error] %@", err); }]; // First call NSSet *allowedClasses = [NSSet setWithObjects:[NSDate class], [NSString class], [NSNumber class], [NSDictionary class], [NSArray class], [NSData class], NSClassFromString(@"INIntent"), nil]; [conn.remoteObjectInterface setClasses:allowedClasses forSelector:@selector(createEventIntentWithStartDate: endDate:withReply:) argumentIndex:0 // reply block argument 0 (first parameter) ofReply:YES]; NSDate *start = [[NSDate alloc] init]; NSDate *end = [NSDate dateWithTimeIntervalSinceNow:3 * 60 * 60]; [proxy createEventIntentWithStartDate:start endDate:end withReply:^(id obj, id result) { NSLog(@"[reply] obj=%@ result=%@", obj, result); }]; // Second call id instance = [NSClassFromString(@"LNStaticDeferredLocalizedString") alloc]; NSInvocation *inv = [NSInvocation invocationWithMethodSignature: [instance methodSignatureForSelector: NSSelectorFromString(@"initWithKey:table:bundleURL:")]]; [inv setSelector:NSSelectorFromString(@"initWithKey:table:bundleURL:")]; [inv setTarget:instance]; NSString *k = @"KEY", *t = @"Localizable"; NSURL *u = [NSBundle mainBundle].bundleURL; [inv setArgument:&k atIndex:2]; [inv setArgument:&t atIndex:3]; [inv setArgument:&u atIndex:4]; [inv invoke]; id result; [inv getReturnValue:&result]; [proxy localizedStringForLinkString:result withReply:^(id obj, id result) { NSLog(@"[reply] obj=%@ result=%@", obj, result); }]; NSLog(@"done"); // Keep run loop alive to wait for reply [[NSRunLoop currentRunLoop] run]; } return 0; } ``` 网闻录 攻击 macOS XPC 助手:协议逆向工程