
إثبات مفهوم وشرح تقني لـ CVE-2026-43783، وهو تصعيد صلاحيات محلي في macOS عبر DesktopServicesHelper XPC لتنفيذ chown عشوائي للحصول على صلاحيات root.
نُشر في الأصل على PT SWARM أو andrd3v.github.io
تبيّن أن الخدمة الخلفية (daemon) التي يستخدمها Finder "لإصلاح" أذونات ملفات iCloud كانت على استعداد لإصلاح الأذونات على أي شيء - بما في ذلك أدلة النظام. في هذه المقالة سنلقي نظرة على كيفية عمل DesktopServicesHelper من الداخل، ولماذا كان طلب XPC واحد كافيًا للحصول على صلاحيات الجذر.
كنت أبحث عن أهداف تحمل 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، مما يعني أن الخدمة الخفية (daemon) تُسجّل تحت الاسم com.apple.DesktopServicesHelper.
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48 هو كتلة معالج (callback) يستدعيها البرنامج الخفي لكل حدث وارد. يعتمد XPC على الرسائل: يُنشئ العميل قاموسًا بمفاتيح وقيم، ويرسله إلى البرنامج الخفي، الذي يحلّل المحتويات ويقرّر ما يجب فعله. تنتهي جميع الأحداث الواردة في هذا المعالج. لنلقِ نظرة على ما بداخله.

دالة الاستدعاء الخاصة بالكتلة هي sub_100032044. عند اتصال جديد، يسترجع البرنامج الخفي أولًا euid وegid الخاصين بالعميل - معرّف المستخدم الفعّال ومعرّف المجموعة - وهما المعرّفان الخاصان بالمستخدم والمجموعة اللذين تعمل تحتهما العملية المتصلة:
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
}
}
إذا كان العميل معزولاً (sandboxed) - يُفحص اسم الطلب مقابل قائمة سماح من سبع سلاسل نصية. إذا تطابق - تنجح عملية الفحص ويمر الطلب. وإذا لم يتطابق - يقتل sub_100034250 الخادم (daemon).
إذا لم يكن العميل معزولاً - يتحقق الخادم من استحقاق com.apple.private.tcc.allow بالقيمة kTCCServiceSystemPolicyAllFiles. وغياب الاستحقاق يعني الموت أيضاً.
بعد اجتياز الفحص، يمر الطلب عبر جدولي معالجة. أولاً 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;
}