콘텐츠로 건너뛰기

[1day] CVE-2026-20610: Setup Assistant(mbsystemadministration): root 소유자 파일을 손상시킬 수 있는 심볼릭 추종 취약점

개요

해당 취약점은 다음 버전에서 패치되었다.

  • macOS Tahoe 26.3

무엇이 바꼈나?

여러 파일들을 봐왔지만, 문자열 경로만 바꼈으며, 코드 로직은 거의 변함이 없었다.

이중에서 일반 사용자가 생성할 수 있을만한 /private/tmp/.AppleMiniSetupDidRun 파일을 주목해서 한번 봤다.

트리거 방법?

System Settings 앱에서 새로운 부계정을 생성한 다음에

fs_usage로 파일 사용을 추적해보면,
최초 부계정 로그인 할시에 mbsystemadministration 프로세스에서 /private/tmp/.AppleMiniSetupDidRun 파일을 생성하는 것을 볼 수 있다.

seo@seos-Virtual-Machine ~ % sudo fs_usage -w -f filesys | grep -iE "DidRunFLO|/tmp|var/run"
...
16:18:10.774192  stat64                 [  2]           private/tmp/.AppleMiniSetupDidRun                                                                                                                                     0.000035   mbsystemadministration.48270
16:18:10.774455  open              F=3        (_WC_T__________)  private/tmp/.AppleMiniSetupDidRun                                                                                                                            0.000160   mbsystemadministration.48270
16:18:10.774983    WrData[A]       D=0x008799c7  B=0x1000   /dev/disk3s5  private/tmp/.AppleMiniSetupDidRun                                                                                                                   0.000486 W mbsystemadminist.48270

mbsystemadministration에서 /tmp/.AppleMiniSetupDidRun 문자열을 xref해봤을때, -[MBSAServer writeMiniBuddyCookieForUser:completion:] 에서 해당 파일을 생성한다.

/tmp/.AppleMiniSetupDidRun 해당 파일이 없더라도 상관없이 생성 가능하며,

void __cdecl -[MBSAServer writeMiniBuddyCookieForUser:completion:](MBSAServer *self, SEL a2, unsigned int a3, id a4)
{
  __int64 v4; // x19
  void (__fastcall **v5)(id, __int64); // x20
  __int64 _18; // [xsp+18h] [xbp+8h]

  v4 = *(_QWORD *)&a3;
  v5 = (void (__fastcall **)(id, __int64))objc_retain(a4);
  sub_100019048(v4);
  v5[2](v5, 1);
  if ( ((_18 ^ (2 * _18)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  objc_release(v5);
}

void __fastcall sub_100019048(__int64 a1)
{
  NSNumber *v1; // x20
  NSArray *v2; // x21
  void *v3; // x19
  id v4; // x20
  NSDictionary *v5; // x21
  const __CFString *v6; // [xsp+0h] [xbp-40h] BYREF
  void *v7; // [xsp+8h] [xbp-38h] BYREF
  NSNumber *v8; // [xsp+10h] [xbp-30h] BYREF

  v1 = objc_retainAutoreleasedReturnValue(+[NSNumber numberWithUnsignedInt:](&OBJC_CLASS___NSNumber, "numberWithUnsignedInt:", a1));
  v8 = v1;
  v2 = objc_retainAutoreleasedReturnValue(+[NSArray arrayWithObjects:count:](&OBJC_CLASS___NSArray, "arrayWithObjects:count:", &v8, 1));
  v3 = -[NSArray mutableCopy](v2, "mutableCopy");
  objc_release(v2);
  objc_release(v1);
  v4 = objc_retainAutoreleasedReturnValue(sub_100019170());
  if ( v4 )
    objc_msgSend(v3, "addObjectsFromArray:", v4);
  v6 = CFSTR("uids");
  v7 = v3;
  v5 = objc_retainAutoreleasedReturnValue(
         +[NSDictionary dictionaryWithObjects:forKeys:count:](
           &OBJC_CLASS___NSDictionary,
           "dictionaryWithObjects:forKeys:count:",
           &v7,
           &v6,
           1));
  -[NSDictionary writeToFile:atomically:](v5, "writeToFile:atomically:", CFSTR("/tmp/.AppleMiniSetupDidRun"), 0);
  objc_release(v5);
  objc_release(v4);
  objc_release(v3);
}

id sub_100019170()
{
  NSFileManager *v0; // x19
  unsigned int v1; // w20
  NSDictionary *v2; // x19
  id v3; // x20
  __int64 v4; // x0
  id v5; // x21
  __int64 _28; // [xsp+28h] [xbp+8h]

  v0 = objc_retainAutoreleasedReturnValue(+[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager"));
  v1 = -[NSFileManager fileExistsAtPath:](v0, "fileExistsAtPath:", CFSTR("/tmp/.AppleMiniSetupDidRun"));
  objc_release(v0);
  if ( v1 )
  {
    v2 = objc_retainAutoreleasedReturnValue(
           +[NSDictionary dictionaryWithContentsOfFile:](
             &OBJC_CLASS___NSDictionary,
             "dictionaryWithContentsOfFile:",
             CFSTR("/tmp/.AppleMiniSetupDidRun")));
    v3 = objc_retainAutoreleasedReturnValue(-[NSDictionary objectForKey:](v2, "objectForKey:", CFSTR("uids")));
    v4 = objc_opt_class(&OBJC_CLASS___NSArray);
    if ( (objc_opt_isKindOfClass(v3, v4) & 1) != 0 )
      v5 = objc_retain(v3);
    else
      v5 = nullptr;
    objc_release(v3);
    objc_release(v2);
  }
  else
  {
    v5 = nullptr;
  }
  if ( ((_28 ^ (2 * _28)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v5);
}

-[NSDictionary writeToFile:atomically:] 메소드를 호출하여 아래와 같이 UID 키값이 있는 데이터를 쓸 것이다.

<?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>uids</key>
        <array>
            <integer>501</integer>
        </array>
    </dict>
</plist>

하지만 mbsystemadministration 에서 수락받을 수 있는 XPC 클라이언트는 com.apple.private.mbsystemadministration 자격을 필요로 한다.

bool __cdecl -[MBSAServer listener:shouldAcceptNewConnection:](MBSAServer *self, SEL a2, id a3, id a4)
{
  id v6; // x19
  id v7; // x20
  char v8; // w21
  id v9; // x23
  NSString *v10; // x24
  id v11; // x23
  id v12; // x23
  OS_dispatch_queue *v13; // x24
  id v14; // x23
  void *v15; // x0
  NSString *v16; // x22
  _QWORD v18[5]; // [xsp+8h] [xbp-A8h] BYREF
  id v19; // [xsp+30h] [xbp-80h] BYREF
  id location; // [xsp+38h] [xbp-78h] BYREF
  _QWORD block[5]; // [xsp+40h] [xbp-70h] BYREF
  id v22; // [xsp+68h] [xbp-48h]

  v6 = objc_retain(a3);
  v7 = objc_retain(a4);
  v8 = sub_100003FAC(CFSTR("com.apple.private.mbsystemadministration"), v7);
  v9 = objc_retainAutoreleasedReturnValue(+[MBLogManager sharedManager](&OBJC_CLASS___MBLogManager, "sharedManager"));
  if ( (v8 & 1) != 0 )
  {
    ...
  }
  else
  {
    v16 = objc_retainAutoreleasedReturnValue(
            +[NSString stringWithFormat:](
              &OBJC_CLASS___NSString,
              "stringWithFormat:",
              CFSTR("<Security> Rejecting connection due to lack of entitlement")));
    objc_msgSend(
      v9,
      "logType:inFunction:atLine:withString:",
      1,
      "-[MBSAServer listener:shouldAcceptNewConnection:]",
      238,
      v16);
    objc_release(v16);
    v15 = v9;
  }
  objc_release(v15);
  objc_release(v7);
  objc_release(v6);
  return v8;
}

아래 링크를 통해 자격을 확인해봤을때 com.apple.private.mbsystemadministration 자격을 가진 프로세스는 딱 4개가 존재한다.

https://codecolor.ist/entdb/os/find?key=com.apple.private.mbsystemadministration&os=mac%2F26.2_25C56

mbsystemadministration 프로세스를 디버깅하여 -[MBSAServer writeMiniBuddyCookieForUser:completion:] 에 브레이크포인트를 잡으면, XPC통신으로 타 프로세스에 와 통신하는 것을 볼 수 있고

seo@seos-Virtual-Machine ~ % sudo lldb
(lldb) process attach --name mbsystemadministration --waitfor
Process 1126 stopped
* thread #1, stop reason = signal SIGSTOP
    frame #0: 0x000000019ae7932c dyld`metadata_visitor::Visitor::resolveRebase(metadata_visitor::ResolvedValue const&) const + 4
dyld`metadata_visitor::Visitor::resolveRebase:
->  0x19ae7932c <+4>:  ldr    x9, [x9]
    0x19ae79330 <+8>:  cbz    x9, 0x19ae7935c ; <+52>
    0x19ae79334 <+12>: ldrb   w10, [x0, #0x18]
    0x19ae79338 <+16>: tbz    w10, #0x0, 0x19ae79368 ; <+64>
Target 0: (mbsystemadministration) stopped.
Executable binary set to "/System/Library/CoreServices/Setup Assistant.app/Contents/Resources/mbsystemadministration".
Architecture set to: arm64e-apple-macosx-.

(lldb) c
Process 1126 resuming
Process 1126 stopped
* thread #2, queue = 'com.apple.NSXPCConnection.user.com.apple.mbsystemadministration.1113', stop reason = breakpoint 1.1
    frame #0: 0x0000000100e60ed8 mbsystemadministration`___lldb_unnamed_symbol_10000ced8
mbsystemadministration`___lldb_unnamed_symbol_10000ced8:
->  0x100e60ed8 <+0>:  pacibsp
    0x100e60edc <+4>:  stp    x20, x19, [sp, #-0x20]!
    0x100e60ee0 <+8>:  stp    x29, x30, [sp, #0x10]
    0x100e60ee4 <+12>: add    x29, sp, #0x10
Target 0: (mbsystemadministration) stopped.
(lldb) bt
* thread #2, queue = 'com.apple.NSXPCConnection.user.com.apple.mbsystemadministration.1113', stop reason = breakpoint 1.1
  * frame #0: 0x0000000100e60ed8 mbsystemadministration`___lldb_unnamed_symbol_10000ced8
    frame #1: 0x000000019b26bdc4 CoreFoundation`__invoking___ + 148
    frame #2: 0x000000019b26bc4c CoreFoundation`-[NSInvocation invoke] + 424
    frame #3: 0x000000019cabd4d0 Foundation`__NSXPCCONNECTION_IS_CALLING_OUT_TO_EXPORTED_OBJECT__ + 16
    frame #4: 0x000000019d5ee460 Foundation`-[NSXPCConnection _decodeAndInvokeMessageWithEvent:reply:flags:] + 1208
    frame #5: 0x000000019d5efde0 Foundation`message_handler_message + 88
    frame #6: 0x000000019ca833ac Foundation`message_handler + 152
    frame #7: 0x000000019aec26f0 libxpc.dylib`_xpc_connection_call_event_handler + 112
    frame #8: 0x000000019aec0f80 libxpc.dylib`_xpc_connection_mach_event + 1488
    frame #9: 0x000000019b03eb1c libdispatch.dylib`_dispatch_client_callout4 + 16
    frame #10: 0x000000019b04139c libdispatch.dylib`_dispatch_mach_msg_invoke + 464
    frame #11: 0x000000019b02d350 libdispatch.dylib`_dispatch_lane_serial_drain + 332
    frame #12: 0x000000019b042104 libdispatch.dylib`_dispatch_mach_invoke + 472
    frame #13: 0x000000019b02d350 libdispatch.dylib`_dispatch_lane_serial_drain + 332
    frame #14: 0x000000019b02dff8 libdispatch.dylib`_dispatch_lane_invoke + 440
    frame #15: 0x000000019b038474 libdispatch.dylib`_dispatch_root_queue_drain_deferred_wlh + 292
    frame #16: 0x000000019b037d6c libdispatch.dylib`_dispatch_workloop_worker_thread + 692
    frame #17: 0x000000019b1dde4c libsystem_pthread.dylib`_pthread_wqthread + 292
(lldb)

/System/Library/CoreServices/Setup Assistant.app/Contents/MacOS/Setup Assistant 에서 XPC 연결을 시도한다.

(lldb) po [NSXPCConnection currentConnection]
<NSXPCConnection: 0xc44c4c280> connection from pid 1113 on mach service named com.apple.mbsystemadministration

(lldb) shell ps -A | grep 1113
 1113 ??         0:01.67 /System/Library/CoreServices/Setup Assistant.app/Contents/MacOS/Setup Assistant -MiniBuddyYes
 1380 ttys001    0:00.01 /bin/sh -c  ps -A | grep 1113
 1382 ttys001    0:00.00 grep 111

정리하자면, XPC 연결을 하는데에 앞서 클라이언트는 /System/Library/CoreServices/Setup Assistant.app/Contents/MacOS/Setup Assistant, 서버는 /System/Library/CoreServices/Setup Assistant.app/Contents/Resources/mbsystemadministration이 되겠다.

해당 XPC 서버는 com.apple.private.mbsystemadministration 자격을 갖춘 클라이언트만 수용하고 있고, 보통은 listener:shouldAcceptNewConnection: 메소드에서 자격이 필요한지 안한지 확인할 수 있다.

이제 클라이언트 프로세스인 Setup Assistant 를 확인해보자.

일반 사용자가 터미널에서 Setup Assistant 를 실행만하면, applicationDidFinishLaunching:writeMiniBuddyCookieForUser:completion: 메소드가 순서대로 호출되어 mbsystemadministration XPC 서버에게 /tmp/.AppleMiniSetupDidRun 파일 생성을 요청한다.

void __cdecl -[MacBuddyAppDelegate applicationDidFinishLaunching:](MacBuddyAppDelegate *self, SEL a2, id a3)
{
  //...
  {
    v48 = objc_retainAutoreleasedReturnValue(+[MBSAConnection sharedConnection](&OBJC_CLASS___MBSAConnection, "sharedConnection"));
    objc_msgSend(
      v48,
      "writeMiniBuddyCookieForUser:completion:",
      -[MacBuddyAppDelegate miniBuddyTargetUID](self, "miniBuddyTargetUID"),
      &stru_100283028);
    objc_release(v48);
  }
  objc_release(v8);
  objc_release(v7);
  if ( ((_58 ^ (2 * _58)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  objc_release(v6);
}

실제로 실행해서 확인해보면, /private/tmp 경로에 .AppleMiniSetupDidRun 파일이 생성되있다.

seo@seos-Virtual-Machine ~ % open /System/Library/CoreServices/Setup\ Assistant.app

seo@seos-Virtual-Machine ~ % ls -la /private/tmp
total 8
drwxrwxrwt  10 root          wheel  320  7월 12 20:44 .
drwxr-xr-x   6 root          wheel  192  7월 12 18:47 ..
-rw-r--r--   1 root          wheel  374  7월 12 20:44 .AppleMiniSetupDidRun
...

악용 가능성

root 권한 소유의 파일을 /private/tmp/.AppleMiniSetupDidRun 에 심볼릭 링크로 걸어둔다면, 해당 파일을 아래 내용과 같은 형식으로 변조할 수 있다. uid 값을 제외하고는 완전히 파일 내용을 제어할 수는 없기 때문에 파급력이 크진 않다.

<?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>uids</key>
        <array>
            <integer>(UID)</integer>
        </array>
    </dict>
</plist>

Demo

태그: