最初发布于 PT SWARM 或 andrd3v.github.io
Finder 用来“修复”iCloud 文件权限的守护进程,结果却愿意修复任何东西的权限——包括系统目录。在本文中,我们将深入探讨 DesktopServicesHelper 的内部工作原理,以及为什么一个 XPC 请求就足以获取 root 权限。
我当时在寻找带有 com.apple.private.tcc.allow 且值为 kTCCServiceSystemPolicyAllFiles 的目标,DesktopServicesHelper 引起了我的注意。DesktopServicesHelper 位于 /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper,Finder 使用它来执行需要提升权限的文件操作:移动文件、设置权限、处理废纸篓。

将二进制文件加载到 IDA 中并转到 start()。除了常规的定时器和队列设置之外,它还创建了一个 XPC 监听器——这是该守护进程的入口点。该监听器在特定名称下注册了一个 mach 服务,并开始监听传入的连接。任何知道这个名称的进程都可以连接并发送请求。以下是关键部分:
// get the service name
v16 = (const char *)sub_1000709F4(a1: 0);
// register the listener
mach_service = xpc_connection_create_mach_service(name: v16, targetq: nullptr, flags: 1u);
// set the incoming event handler
xpc_connection_set_event_handler(connection: mach_service, handler: &stru_1000B4B48);
xpc_connection_resume(connection: mach_service);
// start the event loop
CFRunLoopRun();
函数 sub_1000709F4 返回 mach 服务名称。在 start() 中,它以参数 0 被调用,这意味着该守护进程以名称 com.apple.DesktopServicesHelper 进行注册。
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 是一个处理程序块(回调),守护进程会为每个传入事件调用它。XPC 是基于消息的:客户端构造一个包含键和值的字典,将其发送给守护进程,守护进程解析内容并决定如何处理。所有传入事件最终都会进入这个处理程序。让我们深入看看。

该块的调用函数是 sub_100032044。在新连接上,守护进程首先获取客户端的 euid 和 egid——即有效用户 ID 和组 ID——也就是发起连接的进程所运行的用户和组的标识符:
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
然后它为连接上的传入消息分配一个处理程序:
v15[2] = sub_10003AB2C; // block's invoke function
xpc_connection_set_event_handler(connection: v14, handler: v15);
xpc_connection_resume(connection: v14);
sub_10003AB2C - 这是来自客户端的实际 XPC 请求到达的地方。现在我们需要理解守护进程如何决定执行哪些请求、拒绝哪些请求。
sub_10003AB2C 是主分发器。守护进程在这里决定如何处理请求。该函数很大,所以我们来逐步拆解它。
首先,它从消息中取出 "request" 字符串——即客户端想要执行的命令名称:
reply = xpc_dictionary_create_reply(original: v12);
string = xpc_dictionary_get_string(xdict: v12, key: "request");
// ...
// logging: "Got Request '%{public}@'"
然后,守护进程会获取连接进程的审计令牌,并检查它是否运行在 App Sandbox 中。App Sandbox 是 macOS 的一种应用程序隔离机制,用于限制对文件系统、网络及其他资源的访问。这里就是分支所在:
xpc_dictionary_get_audit_token(a1: v12, a2: buf);
qmemcpy(__p, buf, sizeof(__p));
if ( (unsigned int)sandbox_check_by_audit_token(a1: __p, a2: 0, a3: 0) != 0 )
{
// client is sandboxed - check against allowlist
if ( (sub_10003B25C(a1: &v32, a2: "Handshake") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "exit") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "SetDefaultPermissionOnHomeSubdirectories") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "MoveAsideICloudToArchiveInHome") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "RepairPermissionsForCloudItems") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "CopyClientActive") & 1) == 0
&& (sub_10003B25C(a1: &v32, a2: "DeleteItem") & 1) == 0 )
{
sub_100034250(a1: v12, a2: &v32); // not in the list - kill the daemon
}
}
else
{
// client is not sandboxed - check entitlement
*(_QWORD *)buf = sub_1000343C0(a1: v12, a2: CFSTR("com.apple.private.tcc.allow"));
v19 = (const __CFArray *)sub_10003B1E0();
v20 = v19;
if ( v19 == nullptr
|| (v35.length = CFArrayGetCount(theArray: v19),
v35.location = 0,
CFArrayContainsValue(theArray: v20, range: v35, value: CFSTR("kTCCServiceSystemPolicyAllFiles")) == 0) )
{
sub_100034250(a1: v12, a2: &v32); // no entitlement - kill the daemon
}
}
如果客户端处于沙盒中——请求名称会与一个包含七个字符串的允许列表进行比对。如果匹配——检查通过,请求放行。如果不匹配——sub_100034250 会终止守护进程。
如果客户端未处于沙盒中——守护进程会检查是否存在值为 kTCCServiceSystemPolicyAllFiles 的 com.apple.private.tcc.allow 权限。没有该权限同样意味着终止。
通过检查后,请求会依次经过两个处理程序表。首先是 sub_10002F7C8,然后是 sub_10002A194:
// first table (8 commands, two arguments)
v21 = sub_10002F7C8(a1: buf);
// ...
if ( v21 != 0 )
v22(a1: v12, a2: v11); // found - invoke
// second table (21 commands, three arguments)
v27 = sub_10002A194(a1: buf);
// ...
if ( v27 != nullptr )
v27(a1: v12, a2: reply, a3: v11); // found - invoke
现在让我们看看处理程序表。第一个表(sub_10002F7C8):
__int64 __fastcall sub_10002F7C8(__int64 a1)
{
__int64 result; // x0
void **v3; // x21
void **v4; // x8
_BYTE v5[32]; // [xsp+8h] [xbp-128h] BYREF
__int64 v6; // [xsp+28h] [xbp-108h] BYREF
__int64 v7; // [xsp+48h] [xbp-E8h] BYREF
__int64 v8; // [xsp+68h] [xbp-C8h] BYREF
__int64 v9; // [xsp+88h] [xbp-A8h] BYREF
__int64 v10; // [xsp+A8h] [xbp-88h] BYREF
__int64 v11; // [xsp+C8h] [xbp-68h] BYREF
__int64 v12; // [xsp+E8h] [xbp-48h] BYREF
__int64 v13; // [xsp+108h] [xbp-28h] BYREF
if ( (atomic_load_explicit((atomic_uchar *volatile)&qword_1000BD8C8, memory_order_acquire) & 1) == 0
&& __cxa_guard_acquire(a1: &qword_1000BD8C8) != 0 )
{
sub_10002AA78(a1: (int)v5, __s: "OperationSizing");
sub_10002AA78(a1: (int)&v6, __s: "SetChildPermissions");
sub_10002AA78(a1: (int)&v7, __s: "RunTrashOperation");
sub_10002AA78(a1: (int)&v8, __s: "ChildCreateLock");
sub_10002AA78(a1: (int)&v9, __s: "RunSetRootMetadata");
sub_10002AA78(a1: (int)&v10, __s: "RunCopyMoveOperation");
sub_10002AA78(a1: (int)&v11, __s: "DeleteBackup");
sub_10002AA78(a1: (int)&v12, __s: "DeleteItem");
sub_10003BDDC(a1: &unk_1000BD8A0, a2: v5, a3: 8);
v3 = (void **)&v13;
do
{
v4 = v3;
v3 -= 4;
if ( *((char *)v4 - 9) < 0 )
operator delete(__p: *v3);
}
while ( v3 != (void **)v5 );
__cxa_guard_release(a1: &qword_1000BD8C8);
}
result = sub_10003BCF0(a1: &unk_1000BD8A0, a2: a1);
if ( result != 0 )
return *(_QWORD *)(result + 40);
return result;
}
这里没有什么值得关注的内容。八个处理程序(OperationSizing、SetChildPermissions 等)全部受权限检查保护。
但还有第二张表——v27 = sub_10002A194(a1: buf):
__int64 __fastcall sub_10002A194(__int64 a1)
{
__int64 result; // x0
void **v3; // x21
void **v4; // x8
_BYTE v5[32]; // [xsp+8h] [xbp-2C8h] BYREF
__int64 v6; // [xsp+28h] [xbp-2A8h] BYREF
__int64 v7; // [xsp+48h] [xbp-288h] BYREF
__int64 v8; // [xsp+68h] [xbp-268h] BYREF
__int64 v9; // [xsp+88h] [xbp-248h] BYREF
__int64 v10; // [xsp+A8h] [xbp-228h] BYREF
__int64 v11; // [xsp+C8h] [xbp-208h] BYREF
__int64 v12; // [xsp+E8h] [xbp-1E8h] BYREF
__int64 v13; // [xsp+108h] [xbp-1C8h] BYREF
__int64 v14; // [xsp+128h] [xbp-1A8h] BYREF
__int64 v15; // [xsp+148h] [xbp-188h] BYREF
__int64 v16; // [xsp+168h] [xbp-168h] BYREF
__int64 v17; // [xsp+188h] [xbp-148h] BYREF
__int64 v18; // [xsp+1A8h] [xbp-128h] BYREF
__int64 v19; // [xsp+1C8h] [xbp-108h] BYREF
__int64 v20; // [xsp+1E8h] [xbp-E8h] BYREF
__int64 v21; // [xsp+208h] [xbp-C8h] BYREF
__int64 v22; // [xsp+228h] [xbp-A8h] BYREF
__int64 v23; // [xsp+248h] [xbp-88h] BYREF
__int64 v24; // [xsp+268h] [xbp-68h] BYREF
__int64 v25; // [xsp+288h] [xbp-48h] BYREF
__int64 v26; // [xsp+2A8h] [xbp-28h] BYREF