Berkenalan Dengan Bug Null Pointer Dereference [Versi Lawas]
Wunderbar Emporium
Wunderbar_emporium dirilis oleh Brad Spander sekitar tanggal 14 Agustus 2009, eksploit tersebut sebagai respon dari advisories yang dirilis oleh Tavis Ormandy dan Julien Tinnes (Google Security Team) kepada public.

Bug tersebut merupakan NULL Pointer Dereference yang disebabkan oleh “incorrect proto_ops initializations” pada kernel linux (CVE-2009–2692).
Ringkasan CVE-2009-2692:
The Linux kernel 2.6.0 through 2.6.30.4, and 2.4.4 through 2.4.37.4, does not initialize all function pointers for socket operations in proto_ops structures, which allows local users to trigger a NULL pointer dereference and gain privileges by using mmap to map page zero, placing arbitrary kode on this page, and then invoking an unavailable operation, as demonstrated by the sendpage operation (sock_sendpage function) on a PF_PPPOX socket.
Pada advisories mereka yang dipublish oleh beberapa mailing-list security[12], diberikan juga bagaimana metode untuk men-trigger bug tersebut.
Pembahasan lengkap mengenai bug serta eksploit ini telah dilakukan oleh
xorl[13], sehingga pembahasan yang saya berikan pada artikel ini sifatnya hanya summary, dan yang pasti dalam bahasa Indonesia ;)
Memicu Bug Pada Kernel
Pertama-tama, kita akan melihat bug kernel linux yang dapat mentrigger Null Pointer Dereference tersebut.

Pada Linux setiap socket memiliki asosiasi dengan suatu struktur yang disebut proto_ops. Struktur tersebut memiliki variable pointer yang menunjuk suatu fungsi operasi tertentu. Misalnya kita membuka koneksi socket internet, maka telah tersedia beberapa fungsi untuk operasi pada socket internet (seperti connect()
, accept()
, bind()
, dll) tersebut. Nah fungsi-fungsi inilah yang ditunjuk oleh variable pointer struktur sock_ops diatas.
Seluruh socket akan diberikan struktur diatas, sehingga muncul pertanyaan bagaimana seandainya ada socket yang tidak mengimplementasikan satu atau beberapa operasi dari struktur proto_ops?! Misalnya, saya men-design cyberheb_socket()
untuk diintegrasikan dalam Linux namun tidak ingin mengimplementasikan fungsi accept()
. Jika hal tersebut terjadi maka cyberheb_socket()
diharapkan untuk melakukan inisialisasi terhadap variable pointer *accept ke suatu fungsi, misalnya sock_no_accept()
.
Sebetulnya hal ini tidak akan menjadi masalah karena biasanya implementasi suatu socket telah melakukan pengecekan tersendiri. Taviso dan Julien mengambil contoh sock_splice_read()
,

Kita bisa lihat bahwa sesaat setelah socket di-inisialisasikan (baris 746),
maka pada baris berikut nya (baris 748) dilakukan pengecekan terhadap NULL Pointer Dereference. Jika sock adalah NULL maka fungsi sock_splice_read()
diatas akan langsung mengembalikan nilai error. Ini adalah salah satu contoh implementasi yang baik dan sesuai aturan.
Namun ternyata Taviso dan Jullien menemukan fakta bahwa ada implementasi socket yang tidak melakukan pengecekan terhadap NULL Pointer Dereference sebelum melakukan dereference, yaitu pada implementasi sock_sendpage()
.

Bisa kita lihat dengan jelas bahwa sock di-inisialisasikan dengan nilai dari
private_data (baris 733), dan kemudian tanpa proses pengecekan apakah sock bernilai NULL fungsi tersebut langsung melakukan dereference di bagian akhir (baris 739).
Berdasarkan advisories tersebut beberapa contoh implementasi lain juga memiliki masalah yang sama diantaranya pada protocol PF_BLUETOOTH, PF_IUCV, PF_PPPOX, dll. Advisories mereka juga dilengkapi dengan metode untuk men-trigger bug tersebut:

Advisories yang lengkap walaupun tidak menyertakan eksploit. Namun bagi para exploit writer hal tersebut sudah cukup. Dan hal yang mungkin paling menyenangkan adalah statement berikut ini:
“This issue is easily exploitable for local privilege escalation. In order to exploit this, an attacker would create a mapping at address zero containing code to be executed with privileges of the kernel, and then trigger a vulnerable operation”
Bug tersebut terjadi pada process kernel, sehingga privilege nya merupakan ring 0. Jika kita bisa mengarahkan eksekusi processor dengan privilege ring 0 dan mengontrolnya maka tidak ada yang tidak bisa kita lakukan.
Berikut ini step-by-step gambaran umum eksploit wunderbar_emporium:
- Melakukan pengecakan apakah sistem target merupakan 32-bit ataupun 64-bit. Eksploit ini portable sehingga bisa dilakukan pada target 32-bit ataupun 64-bit (intel).
- Melakukan pengecekan terhadap
mmap_min_addr()
restriction. Cara paling mudah yang bisa dilakukan oleh user process biasa adalah melihat isi file/proc/sys/vm/mmap_min_addr
. Jikammap_min_addr()
bernilai lebih besar dari nol, maka proses eksploitasi akan dilanjutkan dengan trik bypassmmap_min_addr()
, namun jika bernilai nol atau file tersebut tidak ada (versi kernel 2.6 yang lama) maka akan dilakukan proses eksploitasi. Pada wunderbar_emporium2 juga ditambahkan pengecekan apakah terdapat SELinux (/selinux/enforce
), jika terdapat SELinux maka akan dimanfaatkan untuk bypassmmap_min_addr()
karena ternyata secara default policy SELinux malah justru dapat mem-bypassmmap_min_addr()
melalui trik unconfined_t seperti yang digambarkan oleh Dan Walsh. - Proses bypass
mmap_min_addr()
(tanpa SELinux) menggunakan trik pulseaudio. File exploit.c akan di-compile sebagai shared-object (.so) yang dapat di-load sebagai library oleh pulseaudio (dengan options “-L”). Proses ini terlebih dahulu melakukan set personality menjadi SVr4, sehingga ketika dijalankan maka pulseaudio akan secara otomatis melakukan mapping page 0. - Proses eksploitasi akan dilakukan oleh exploit.c, page 0 akan di-set dengan suatu fungsi yang menjalankan beberapa procedure. Diantara procedure-procedure tersebut adalah men-disable fitur-fitur LSM (AppArmor, Audit, SELinux). Proses ini menggunakan trik patching dari simbol kernel yang telah didapatkan sebelumnya. Kenapa bisa di-disable?! tentu saja karena pada tahap ini kita telah mendapatkan ring 0 sehingga seakan-akan procedure ini dilakukan dengan privilege kernel.
- Trigger NULL Pointer Dereference pada kernel sehingga eksekusi akan dibawa ke zero page.
- Ubah status uid menjadi root.
- Bangkitkan shell dengan uid root.
- r00tshell
Eksploit Wunderbar Emporium
Wunder emporium terdiri dari 3 files: wunder_emporium.sh
, pwnkernel.c
,exploit.c
, kita cukup menjalankan wunder_emporium.sh dan sisanya akan dijalankan oleh script tersebut. Berikut ini isi shell script tersebut:

Script ini melakukan otomasi proses eksploitasi, dan tidak ada yang istimewa karena sudah jelas dari script-nya. Jika tidak ada perlindungan mmap_min_addr()
maka akan segera dilakukan eksploitasi menggunakan hasil compile file exploit.c
, namun jika terdapat restriction mmap_min_addr()
akan dilihat kembali apakah SELinux diaktifkan dan Enforcing, jika tidak maka dijalankan pwnkernel.c
(trik personality + pulseaudio), namun jika terdapat SELinux dan Enforcing maka sesuai blog Dan Walsh akan digunakan trik unconfined_t user yang memanfaatkan runcon untuk mengubah context process (bruteforce) ke initrc_t, wine_t, vbetool_t, unconfined_mono_t atau samba_unconfined_net_t. Trik ini bisa dilihat mulai dari baris 26 sampai baris 44.
Berikutnya kita akan melihat isi file kernel.c
yang mengimplementasikan trik personality+pulseaudio.

Cukup simple dan tidak perlu panjang lebar, pwnkernel akan melakukan setting personality menjadi SVr4 (PER_SVR4) dan kemudian menjalankan pulseaudio dengan terlebih dahulu melakukan pemeriksaan apakah pulseaudio merupakan binary dengan suid root. Sisanya tinggal mengeksekusi pulseaudio dengan mendefinisikan exploit.so sebagai library untuk di-load. Pada tahap ini page 0 akan di map read-only secara otomatis dan eksekusi selanjutnya akan ditangani oleh kode didalam eksploit.so.
Tentu saja bagian yang paling menarik adalah exploit.c, disinilah terdapat
kode-kode untuk melakukan patching LSM, mendapatkan akses root, dsb. Seperti yang telah disampaikan sebelumnya, saya akan membuang bagian video untuk keren-kerenan nya. Dan berhubung exploit.c
adalah inti eksploitnya maka akan dibahas per fungsi.

Sebagaimana biasanya, eksploit akan mulai dari fungsi main()
. Fungsi main()
hanya terdapat 2 baris, yang pertama adalah melakukan set variable called_from_main, jika bernilai 1 maka video tzameti.avi akan dimainkan sesaat sebelum mendapatkan shell root, jika bernilai 0 maka video tidak akan dimainkan.
Selanjutnya akan dijalankan fungsi pa__init(NULL)
. Mungkin ada yang bertanya mengapa memilih nama pa__init()
ataupun terdapat fungsi pa__done()
pada exploit.c
?! jangan lupa, pada salah satu trik bypass mmap_min_addr()
, exploit.c
akan di-compile sebagai shared object dan diload sebagai library oleh pulseaudio. Dalam hal ini program utama pulseaudio akan mencari fungsi pa__init()
dan pa__end()
saat menjalankan library tersebut. Itu sebabnya fungsi utama dalam exploit.c akan dimasukan kedalam pa__init()
.
Sekarang mari kita lihat apa isi dari fungsi pa_init()
.

Tampak tidak asing?
uid proses saat ini akan dimasukan kedalam variable our_uid, dan selanjutnya dilakukan pengecekan terhadap personality. Jika personality process saat ini bukan SVr4 (zero page belum di-mapped), maka akan dilakukan mapping secara manual. Kita bisa sampai pada bagian ini biasanya karena kernel user tidak ada restriction mmap_min_addr()
, mmap_min_addr()
di-set
bernilai 0, atau restriction pada mmap_min_addr()
telah berhasil di-bypass.
Kernel lama yang menggunakan SELinux menggunakan pembatasan terhadap page 0 dengan tidak mengijinkan mapping RWX (Read+Write+Execute), dalam hal ini bypass-nya cukup dengan mengganti protocol untuk mapping di page 0 menjadi (Read+Write).
Jika personality sudah di-set SVr4 yang berarti page 0 telah di-mapped (Read), maka tinggal mengganti hak aksesnya menggunakan syscall mprotect()
agar menjadi (Read+Write+Execute).
Proses selanjutnya adalah mendapatkan lokasi modul-modul LSM yang telah diaktifkan didalam sistem target,

Ketika kernel mengaktifkan suatu modul security (LSM), maka modul tersebut akan di-load ke suatu lokasi memory dan lokasi tersebut akan disimpan pada suatu file (/proc/kallsyms
atau /proc/ksyms
). Proses berikutnya dari exploit.c adalah mendapatkan lokasi LSM tersebut pada memory, hal ini dilakukan oleh fungsi get_kernel_sym()
yang isinya sebagai berikut:

Fungsi ini akan membaca isi dari file /proc/kallsyms
atau /proc/ksyms
dan
mencari lokasinya berdasarkan input nama modul LSM yang diinginkan. Saya rasa sudah sangat jelas inti dari kode diatas.

Bagian selanjutnya adalah menulis page 0 yang telah di-mapped sebelumnya agar berisi kode-kode yang kita inginkan. Pada lokasi NULL (Zero Page) dimasukan 0xff, pada lokasi NULL+1 dimasukan 0x25, dan kemudian jika mesin terget adalah 32-bit maka NULL+2 akan dimasukan 0 namun jika target adalah komputer 64-bit maka NULL+2 akan dimasukan 6. Sisanya NULL+6 akan dimasukan lokasi / alamat dari fungsi own_the_kernel()
.
Untuk yang penasaran dengan proses pengecekan diatas (antara 32-bit dan 64-bit), trik ini sangat sederhana. Trik ini membandingkan size tipe data “unsigned long” dengan tipe data “unsigned int”, pada komputer 32-bit kedua tipe data ini tidak memiliki size yang sama namun pada komputer 64 bit kedua tipe data ini memiliki size yang sama.
Selanjutnya kita akan melihat bagaimana isi dari fungsi own_the_kernel()
, karena pada saat NULL Pointer Dereference terjadi maka eksekusi akan dibawa pada lokasi fungsi ini berada.

Pada saat NULL pointer dereference terjadi pada kernel, maka eksekusi akan
dibawa menuju fungsi ini dan tentu saja level privilege-nya adalah kernel. Jika eksekusi berhasil mencapai tahap ini maka kita sudah bisa menyatakan bahwa “ring 0” telah berhasil didapatkan. Bagian awal dari own_the_kernel()
segera melakukan hal ini dengan men-set variable got_ring0 dengan angka 1.
Proses selanjutnya adalah men-disable LSM. Dengan privilege kernel maka kita dapat melakukan hal ini dengan mudah. Seperti yang telah dibahas sebelumnya bahwa get_kernel_sysm()
akan memberikan lokasi modul-modul security yang diaktifkan, sehingga dengan operasi pointer kita dapat segera melakukan ‘patching’ lokasi tersebut agar bernilai ‘nol’.
Ini salah satunya:
...
if (audit_enabled)
*audit_enabled = 0;
...
Pada bagian selanjutnya yang cukup menarik adalah proses patching SELinux:

kode diatas digunakan untuk melakukan patching pada sel_read_enforce()
. Patching ini seperti layaknya proses patching sebelumnya ataupun proses patching ketika kita hendak meng-crack suatu program dimana patch dilakukan secara langsung pada lokasi memory.
Pada komputer 32-bit akan dicari posisi kode “push [selinux_enforcing]”
dan menggantinya dengan “push 1”. Posisi kode tersebut dicari mulai dari lokasi yang didefinisikan oleh *sel_read_enforce
(dari symbol kernel) hingga (+0x20) pada memory. Hal yang sama dilakukan untuk komputer 64-bit. Penjelasan pada kode tersebut sangat jelas. Sekali lagi, patching apapun mungkin jika sudah mendapatkan “ring 0”.
Bagian selanjutnya adalah gimme r00t. Fungsi ini sangat umum digunakan pada local eksploit Linux, dalam eksploit spender kali ini disebut sebagai fungsi give_it_to_me_any_way_you_can()
.

Ini adalah tehnik variasi dari “gimme r00t”. Pada kernel linux yang dirilis
akhir-akhir ini terdapat fungsi untuk melakukan set credential secara langsung, berikut ini definisinya:

dan berikut ini implementasinya:

commit_creds()
merupakan fungsi yang di-export sebagai symbol kedalam kernel linux, sehingga kita dapat menggunakan pemanggilan langsung (fastcall) dari suatu program dengan bantuan gcc.
Dalam exploit.c terdapat bagian berikut ini:

dengan fungsi diatas commit_creds()
dan prepare_kernel_cred()
merupakan variable yang memegang lokasi symbol fungsi commit_creds()
serta prepare_kernel_cred()
dalam memory (export dari kernel). Dengan cara inilah maka fungsi commit_creds()
dapat dipanggil dari dalam exploit.c, namun tentu saja tidak semua kernel memiliki symbol ini, itu sebabnya pada bagian awal fungsi get_kernel_sym()
termasuk
mencari apakah pada kernel terdapat symbol “commit_creds” dan
“prepare_kernel_cred”.
Tujuan dari fungsi commit_creds()
sudah jelas, fungsi tersebut akan memberikan credential yang baru pada suatu proses. Dan credential yang baru tersebut diminta melalui fungsi prepare_kernel_cred(0) dimana credential baru untuk uid=0 (root) akan disiapkan. Jika berhasil, maka variable got_root= 1 akan di-set.
Jika kernel yang digunakan target adalah kernel jenis lama ataupun tidak
memiliki fungsi commit_creds()
, maka tehnik untuk mendapatkan root model lama akan dijalankan.

Linux memiliki support untuk dua macam kernel stack, 4K dan 8K kernel page dalam single-page untuk arsitektur x86. Secara default, x86 mendukung 8K kernel stacks, dan ini digunakan juga oleh sistem operasi lain seperti Microsoft
Windows. Namun sejak kernel 2.6.6 (saya sendiri tidak yakin kapan patch untuk 4K stack dimasukan dalam kernel Linux) linux memasukan fitur ini kedalam kernel versi stabil. Pengurangan besar kernel stack sebesar 50% ini dipercaya dapat meningkatkan performa linux, dan telah di-implementasikan oleh distro seperti Fedora atau RedHat.
Mungkin kita akan bertanya-tanya mengapa untuk 4K/8K stack dibuat fungsi yang berbeda? atau mengapa antara x86 (32-bit) dan x64 (64-bit) dibuat juga fungsi yang berbeda? Jawabannya adalah alokasi memory. Alokasi sistem memory yang dilakukan oleh kernel berbeda-beda, antara 2.4 dengan 2.6 saja terdapat perbedaan style alokasi memory.
Wunderbar emporium merupakan contoh eksploit yang sudah dipoles untuk sebisa mungkin stabil dijalankan pada arsitektur 32/64 bit ataupun penggunaan kernel 2.4/2.6, sehingga eksploit ini melakukan beragam pengecekan diatas.
Jika pada metode pertama (commit_cred()) kita menggunakan fungsi built-in kernel untuk mendapatkan credential root (uid=0), maka pada metode kedua yang disebut old-style ini kita akan melakukan ‘patching’ secara manual dengan mencari suatu lokasi memory dan kemudian mengganti isinya sehingga proses yang kita jalankan (eksploit) mendapatkan credential root.
Pertanyaan selanjutnya adalah “apa yang kita cari?!”. Jawaban singkat,
“task_struct”. Dalam Linux, setiap proses memiliki struktur yang disebut sebagai “task_struct”. Ketika suatu aplikasi / program dijalankan, dan proses / thread dibuat oleh kernel maka proses tersebut akan memiliki beberapa identitas seperti informasi stacks, register, parent process, dll. Termasuk diantaranya adalah uid dari proses tersebut. Informasi ini disimpan dalam memory secara dinamis ketika proses dibuat.
Proses dari eksploit yang kita jalankan tentu saja akan memiliki uid yang
menjalankan proses tersebut (mis: apache, nobody, local user, dll), dan tugas
dari “gimme r00t” adalah mencari lokasi task_struct yang berisi uid untuk
kemudian diganti dengan uid milik root. Mudah bukan?!
Kok bisa semudah itu?! Jangan lupa, bug yang kita eksploitasi ini merupakan
“ring0”, apapun perintahnya akan dijalankan :).
Langkah pertama tentu saja mencari lokasi dari proses saat ini, berikut ini
fungsi untuk x86 (4K/8K) dan x64 (8K),

Ambil contoh diatas untuk x86/4K, kode awal merupakan kode inline assembly yang digunakan untuk mendapatkan isi register [ESP]. Ketika suatu proses dibuat, maka ESP (Extended Stack Pointer) akan menyimpan lokasi memory awal dari stack. Setiap proses akan memiliki stack tersendiri untuk menyimpan nilai yang sifatnya dinamis, dalam hal ini kita fokuskan pada nilai uid dari proses tersebut yang diberikan oleh kernel. Nilai ESP tersebut akan dimasukan pada variable current. Variable tersebut kemudian akan dioperasikan proses logika “and” dengan lokasi memory 0xfffff000. Akan dilakukan pengecekan apakah nilai current lebih besar dari 0xc0000000 dan lebih kecil dari 0xfffff000 karena lokasi tersebut merupakan standar untuk x86/4K. Cukup jelas bukan?!
Setelah lokasi awal dari stack didapatkan, maka selanjutnya adalah mencari
posisi informasi uid disimpan,

Proses pencarian dilakukan dari posisi “current” hingga “(current + 0x1000 -
17)”, apabila telah didapatkan posisi dalam memory yang berisi informasi uid
maka selanjutnya adalah mengganti nilainya dengan “0”. Untuk itu digunakan fungsi memset()
.
Inilah salah satu metode lama untuk mendapatkan uid root dalam Linux. Jika
exploit.c berhasil mencapai titik ini berarti tahap persiapan telah dilakukan
dengan baik.
Semua langkah diatas merupakan umpan yang akan dijalankan oleh kernel target sesaat setelah mengalami bug NULL pointer dereference. Sehingga langkah selanjutnya sangat sederhana: trigger bug pada kernel.

Ada pendapat yang mengatakan bahwa “menemukan bug lebih sulit dibandingkan sekedar menulis eksploit”, namun ada juga yang mengatakan bahwa “menulis eksploit (yang reliable) lebih rumit karena harus melewati beragam proteksi dari sistem dibandingkan menemukan bug yang hanya bermodalkan fuzzer”. Well, saya tidak tahu mana yang benar namun tentu saja masing-masing ada tantangan tersendiri. Namun untuk bug kali ini para penulis eksploit tidak perlu bersusah payah karena dalam advisories-nya julien dan taviso sudah memberikan metode untuk men-trigger bug tersebut. Trigger diatas adalah implementasi dari advisories mereka. Dan domain yang akan diserang telah didefinisikan sebelumnya (bisa ditambahkan sendiri),

Tidak ada yang perlu dijelaskan lebih jauh disini.
Sisa dari proses eksploit adalah membiarkan kernel masuk dalam perangkap NULL ptr dereference dan menjalankan beragam hal yang telah dipersiapkan diatas.
Hasilnya:
- Beragam modul LSM akan di-disable (diset 0).
- Proses dari eksploit akan memiliki uid = 0 (root).
Bagian akhir adalah tujuan dari semua ini,
...
execl("/bin/sh", "/bin/sh", "-i", NULL);
...
Dengan uid=0 kita akan membangkitkan program shell yang kemudian memberikan kita si cantik “r00tsh3ll”. Berikut contohnya pada target SuSe Linux Enterprise 9 (default instalasi tanpa hardening):

Inilah akhir dari proses local kernel eksploitation yang memanfaatkan NULL
Pointer Deference.