Eksploitasi JS Engine: Bahaya Image WebP
Mengungkap seni eksploitasi QuickJS Engine melalui studi kasus CVE-2023-4863 terkait bahaya gambar dengan format WebP.

Dalam dunia keamanan siber yang selalu berkembang, penting untuk memperhatikan setiap aspek yang dapat menjadi celah bagi para penyerang. Artikel ini akan membahas tentang eksploitasi QuickJS Engine, dengan fokus pada studi kasus CVE-2023-4863, dimana terdapat kerentanan pada library libwebp yang cukup menjadi perbincangan akhir-akhir ini karena library tersebut merupakan salah satu dependensi browser sejuta umat, Google Chrome.
Kita akan melakukan studi kasus pada salah satu soal dari ASIS CTF Finals 2023 bernama isWebP.js, dimana soal ini mencoba melakukan instalasi library libwebp yang rentan pada QuickJS Engine, salah satu javascript engine selain v8 dan SpiderMonkey. Kita ditantang untuk mengeksploitasi soal tersebut dengan tujuan akhir mendapatkan akses untuk Remote Code Execution.
Overview
Dalam tantangan ini, kami menerima sebuah attachment yang berisi quickjs yang dimodifikasi, libwebp yang diubah, serta skrip compile.sh untuk mengompilasinya. Anda dapat mengunduh attachment tersebut dari tautan ini. Mari kita lihat terlebih dahulu file compile.sh.
Mengamati script tersebut mengungkapkan bahwa versi libwebp yang digunakan rentan terhadap eksploitasi terbaru CVE-2023-4863. CVE ini mendetailkan sebuah isu dalam libwebp terkait dengan pembuatan Huffman Table, yang esensial untuk kompresi WebP images. WebP images menggunakan Huffman Coding untuk kompresi, dan libwebp membangun Huffman Table untuk mendekompresi mereka. Namun, karena pemeriksaan validitas yang tidak memadai, memungkinkan untuk membuat sebuah WebP image berbahaya yang menyebabkan heap overflow selama pembangunan tabel.
Dalam pencarian proof of concept (POC) secara online, kami menemukan sebuah POC yang bagus berisi sample WebP image berbahaya. Sebelum menggunakan WebP image berbahaya, mari kita periksa terlebih dahulu patch yang diimplementasikan oleh penulisnya.
Mengkaji file libwebp.patch, satu-satunya modifikasi yang dilakukan oleh penulis adalah penghapusan line of code (LOC) yang melakukan free pada Huffman table. Oleh karena itu, dalam tantangan ini, sekali Huffman table dialokasikan, ia akan tetap ada selamanya.
Selanjutnya, mari kita periksa file quickjs.patch.
Aspek kunci dari patch tersebut adalah pengenalan fungsi baru bernama isWebP. Fungsi ini menerima Uint8Array yang mewakili stream WebP image dan mencoba untuk mendekode gambar tersebut. Versi libwebp yang digunakan adalah versi yang rentan, oleh karena itu, memanggil fungsi isWebP dengan WebP image berbahaya akan memicu bug heap overflow.
Mengingat WebP image berbahaya tersebut sudah tersedia secara online, tantangan utamanya adalah membuat exploit yang memanfaatkan bug heap overflow untuk memicu Remote Code Execution (RCE). Hal ini penting karena untuk mengambil flag, kita harus menjalankan perintah /readflag.
Pengujian Heap Overflow
Pertama, mari kita siapkan helper kita.
Setelah meninjau informasi di blog POC, kita mempelajari bahwa gambar berbahaya dari POC bertujuan untuk menimpa chunk+0x3000. Mari kita coba periksa jenis overflow apa yang dapat dipicu di gdb. Pendekatan saya untuk memeriksanya adalah:
- Menyemprotkan berpasangan (
ArrayBufferdengan ukuran0x2f28danArraydengan ukuran0x1). - Membebaskan beberapa objek
ArrayBufferini. - Mencoba mengalokasikan Huffman table, dengan harapan ia menempati salah satu chunk
ArrayBufferyang telah dibebaskan karena ukurannya sama. - Memeriksa nilai pada
chunk+0x3000untuk menganalisis efek dari overflow.
Untuk mendebug lebih mudah, saya menggunakan teknik dari blog ini blog. Triknya melibatkan pengaturan breakpoint pada js_math_min_max. Selain itu, kita akan mengatur breakpoint pada DecodeImageStream+935, di mana rax menyimpan alamat huffman_tables.
Setelah menjalankan kode tersebut dan memeriksa keadaan di gdb saat berhenti pada js_math_min_max, kita melihat bahwa memang menimpa huffman_tables+0x3000 dengan nilai 0x30007.
// Before (Fetch this value during the breakpoint at DecodeImageStream+935)gef> x/30gx $rax+0x30000x555555b4f880: 0x0000000000000002 0x00000000000000310x555555b4f890: 0x0000000000000002 0x0000000000000000// After (Fetch this value during the breakpoint at js_math_min_max)gef> x/30gx 0x0000555555b4c880+0x30000x555555b4f880: 0x0000000000030007 0x00000000000000310x555555b4f890: 0x0000000000000002 0x0000000000000000
Sekarang kita tahu bahwa heap overflow memang terjadi, kita perlu mulai berpikir tentang bagaimana memanfaatkannya untuk mendapatkan RCE.
Read dan Write Primitive
Strategi di sini memanfaatkan heap overflow untuk menimpa panjang tipe data yang ada, memungkinkan kemampuan membaca/menulis Out-of-Bounds (OOB). Setelah bereksperimen di gdb untuk mencapai layout heap yang optimal, saya menemukan urutan berikut efektif.
Berikut penjelasan singkat dari pendekatan saya: Awalnya, saya spray pairs of ArrayBuffer(0x2fb0) dan Array(1.1, 2.2). Spray ini bertujuan untuk mencapai layout seperti ini:
...-----ArrayBuffer----Array-----ArrayBuffer----Array-----ArrayBuffer----Array...
Melakukan free pada beberapa objek ArrayBuffer menghasilkan heap layout seperti ini:
-----Free Chunk with size 0x2fc0----Array-----Free Chunk with size 0x2fc0----Array-----Free Chunk with size 0x2fc0----Array...
Kemudian, mengalokasikan huffman_tables melalui isWebP (yang mengalokasikan chunk berukuran 0x2f30) akan menggunakan free chunk ini. Akibatnya, huffman_tables+0x3000 akan menunjuk ke ukuran Array dan menimpanya dengan nilai besar (0x30007).
Untuk pemahaman yang lebih mendalam, mari kita atur breakpoint tepat sebelum kita menyemprotkan huffman_tables. Kita bisa saja memanggil Math.min tepat setelah membebaskan ArrayBuffer untuk memfasilitasi ini. Kemudian, kita akan memeriksa daftar largebin.
large_bins[idx=111, size=0x2a00-0x3000, @0x7ffff7dff400]: fd=0x555555e21cc0, bk=0x555555e24df0 -> Chunk(addr=0x555555e21cc0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e343e0, bk=0x7ffff7dff3f0, fd_nextsize=0x555555e21cc0, bk_nextsize=0x555555e21cc0) -> Chunk(addr=0x555555e343e0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e312b0, bk=0x555555e21cc0) -> Chunk(addr=0x555555e312b0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e2e180, bk=0x555555e343e0) -> Chunk(addr=0x555555e2e180, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e2b050, bk=0x555555e312b0) -> Chunk(addr=0x555555e2b050, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e27f20, bk=0x555555e2e180) -> Chunk(addr=0x555555e27f20, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e24df0, bk=0x555555e2b050) -> Chunk(addr=0x555555e24df0, size=0x2fc0, flags=PREV_INUSE, fd=0x7ffff7dff3f0, bk=0x555555e27f20)gef> x/30gx 0x555555e24df0+0x10+0x3000-0x500x555555e27db0: 0x0000000000002fc0 0x00000000000000500x555555e27dc0: 0x00020d0000000001 0x0000555555e24c980x555555e27dd0: 0x0000555555e2aef8 0x0000555555822e700x555555e27de0: 0x0000555555e27e10 0x00000000000000000x555555e27df0: 0x0000000000000002 0x0000555555e27e400x555555e27e00: 0x0000000000000002 0x0000000000000031
Anggaplah kita berhasil mengalokasikan chunk huffman_tables ke alamat 0x555555e24df0. Memanggil malloc(0x2f28) akan menggunakan chunk large_bins yang telah disebutkan, karena ukurannya berada dalam rentang bin dari 0x2a00 sampai 0x3000. Dalam tampilan memori yang digariskan di atas, chunk+0x3000 menunjuk ke field ukuran salah satu objek Array yang kita spray (ukurannya ada di chunk+0x40 (0x2), buffer ada di chunk+0x38 (0x0000555555e27e40)). Karena overflow, ukuran Array ini akan ditimpa.
Selanjutnya, mari kita periksa output dari file JavaScript yang kita buat.
490 undefined491 undefined492 [unsupported type]493 [unsupported type]494 [unsupported type]495 [unsupported type]496 [unsupported type]497 undefined498 undefined499 undefined
Alasan entri y[492] sampai y[496] mengeluarkan nilai selain undefined, tidak seperti yang lain, adalah karena berhasilnya penimpaan field size mereka melalui overflow.
Meskipun berhasil mencapai Out-of-Bounds (OOB) write, seperti yang Anda catat, OOB read tidak berfungsi seperti yang diharapkan, hanya menampilkan [unsupported type]. Untuk memahami lebih baik OOB write, mari kita lakukan percobaan kecil:
Jalankan kode di atas dan periksa layout memori.
// *r8 points to y[496] during breakpoint at js_math_min_maxgef> x/60gx *(uint64_t*)$r80x555555e37640: 0x00020d0000000002 0x0000555555e345180x555555e37650: 0x0000555555e3a778 0x0000555555822e700x555555e37660: 0x0000555555e37690 0x00000000000000000x555555e37670: 0x0000000000000002 0x0000555555e376c00x555555e37680: 0x0000000000030007 0x00000000000000310x555555e37690: 0x0000000000000002 0x00000000000000000x555555e376a0: 0x0000000000000000 0x00000000000000000x555555e376b0: 0x0000000000000000 0x00000000000000310x555555e376c0: 0x0000000013371337 0x00000000000000000x555555e376d0: 0x0000000013371337 0x00000000000000000x555555e376e0: 0x0000000013371337 0x00000000000000000x555555e376f0: 0x0000000013371337 0x00000000000000000x555555e37700: 0x0000000013371337 0x0000000000000000
Setelah memeriksa layout memori pasca OOB write, pengamatan berikut dibuat:
- Setiap elemen menempati 0x10 bytes.
- Hanya 4 bytes yang bisa ditulis per 0x10 bytes.
Untuk write dan read primitive yang lebih kuat, saya menjelajahi tipe data lainnya. Float64Array muncul sebagai kandidat yang kuat, memungkinkan OOB read dan write, asalkan length-nya ditimpa. Untuk memeriksa strukturnya, panggil Math.min(float_arr), mirip dengan pendekatan sebelumnya dengan Array (pastikan untuk menghapus percobaan sebelumnya dari file kita).
Berikut adalah layout memori:
gef> x/60gx *(uint64_t*)$r8-0x100x555555e2b350: 0x0000000000000000 0x00000000000000510x555555e2b360: 0x001f0d0000000002 0x0000555555e2e4980x555555e2b370: 0x0000555555e28238 0x000055555581ef300x555555e2b380: 0x0000555555e2e4e0 0x00000000000000000x555555e2b390: 0x0000555555e2b3b0 0x00005555558228900x555555e2b3a0: 0x0000000000000002gef> x/50gx 0x0000555555822890-0x100x555555822880: 0x0000000000000000 0x00000000000000210x555555822890: 0x4141414141414141 0x4141414141414141
Float64Array memiliki layout yang mirip dengan Array, dengan ukuran di chunk+0x40 (0x2) dan buffer di chunk+0x38 (0x0000555555822890). Menariknya, chunk buffer data tidak terbatas per 0x10 bytes, dan OOB read secara akurat menampilkan nilai hexadecimal sebagai floating points.
Strategi kita sekarang melibatkan menggunakan kemampuan OOB write dari Array untuk menimpa ukuran Float64Array yang baru dispray, mengubahnya menjadi primitif baru untuk membaca dan menulis. Teramati bahwa mengubah y[496][9] dan y[496][34] memengaruhi ukuran salah satu objek Float64Array yang dispray. Untuk mengidentifikasi Float64Array yang terpengaruh, kita dapat menandai elemen pertama (y[496][3] dan y[496][28]) dengan nilai unik.
Memeriksa output dari kode ini mengungkapkan bahwa z[82] dan z[83] adalah instance Float64Array yang dipengaruhi oleh OOB Write Array kita. Dengan primitif baru yang tidak terbatas ini, kita tidak lagi memerlukan OOB write yang terbatas.
...79 2261634.509803921480 2261634.509803921481 2261634.509803921482 4.576853666e-31583 4.576853666e-31584 2261634.509803921485 2261634.509803921486 2261634.509803921487 2261634.5098039214...
Mencari Informasi Penting
Selanjutnya, mari kita jelajahi nilai apa saja yang bisa bocor menggunakan z[82] di gdb. Cukup jalankan Math.min(z[82]) dan periksa nilai memori.
gef> x/100gx *(uint64_t*)$r8-0x300x555555e38790: 0x0000000000000000 0x00000000000000210x555555e387a0: 0x0000000037373737 0x00000000000000000x555555e387b0: 0x0000000000000000 0x00000000000000510x555555e387c0: 0x001f0d0000000002 0x0000555555e3e8880x555555e387d0: 0x0000555555e38878 0x000055555581ef300x555555e387e0: 0x0000555555e38810 0x00000000000000000x555555e387f0: 0x0000555555e38840 0x0000555555e387a00x555555e38800: 0x0000000037373737 0x00000000000000000x555555e38810: 0x00007ffff7dfed00 0x00007ffff7dfed00
Data dimulai di 0x0000555555e387a0. Penting untuk dicatat, data[7] mengungkapkan alamat heap, dan data[14] mengungkapkan alamat libc.
Setelah berhasil membocorkan nilai dengan OOB Read, kita sekarang berada dalam posisi untuk mencapai Remote Code Execution (RCE) dengan memanfaatkan OOB Write.
Remote Code Execution
Untuk mencapai Remote Code Execution (RCE), saya perhatikan bahwa sebuah objek QuickJS bernama JSRuntime menyimpan daftar function pointers yang dinamakan JSMallocFunctions.
Objek ini berada di heap, di mana:
JSRuntime->mfberada diheap_base+0x2a0.JSRuntime->malloc_stateberada diheap_base+0x2c0.
Memeriksa fungsi yang digunakan untuk membuat ArrayBuffer baru (js_array_buffer_constructor3), kita melihat akhirnya memanggil rt->mf.js_malloc(&rt->malloc_state, size);. Jika kita dapat mengarahkan rt->mf.js_malloc ke fungsi system dan mengatur rt->malloc_state ke /readflag, kita dapat memicu RCE.
Tujuan kita adalah melakukan OOB write di lokasi ini. Kita bisa menggunakan write primitives dari Float64Array untuk tujuan ini.
Ingat bahwa Float64Array mempertahankan pointer ke data-nya. Mengamati memory layout dari Float64Array yang rusak kita (z[82] dan z[83]), kita melihat:
gef> x/100gx *(uint64_t*)$r8-0x300x555555e38790: 0x0000000000000000 0x00000000000000210x555555e387a0: 0x0000000037373737 0x00000000000000000x555555e387b0: 0x0000000000000000 0x00000000000000510x555555e387c0: 0x001f0d0000000002 0x0000555555e3e8880x555555e387d0: 0x0000555555e38878 0x000055555581ef300x555555e387e0: 0x0000555555e38810 0x00000000000000000x555555e387f0: 0x0000555555e38840 0x0000555555e387a00x555555e38800: 0x0000000037373737 0x00000000000000000x555555e38810: 0x00007ffff7dfed00 0x00007ffff7dfed00
Dengan menimpa data[11] dengan alamat target kita, setiap penugasan ke data[0] akan mengubah konten di alamat target kita.
Menggunakan teknik ini, kita dapat memanipulasi array yang rusak kita untuk menimpa rt->malloc_state dan rt->mf.js_malloc. Alokasi ArrayBuffer baru selanjutnya kemudian akan menjalankan system("/readflag").
Berikut adalah kode exploit lengkapnya:
Setelah mengirimkan kode ini ke server, kita dapat melakukan remote code execution dan mengambil flag.
Kesimpulan
Studi kasus CVE-2023-4863 dalam QuickJS Engine ini memberikan wawasan mendalam tentang pentingnya memahami dan mengamankan aspek-aspek teknis dalam keamanan siber. Melalui analisis kerentanan di libwebp dan pemanfaatan teknik eksploitasi yang cermat, kita berhasil menunjukkan bagaimana sebuah Remote Code Execution (RCE) dapat dicapai. Eksperimen ini menggarisbawahi pentingnya validasi dan keamanan memori dalam pengembangan software, serta menyoroti kerentanan yang bisa terjadi akibat penggunaan fungsi atau library yang tidak aman.
Di Cylabus, kami berkomitmen untuk menyediakan solusi keamanan terdepan, membantu klien kami mengatasi tantangan keamanan siber yang rumit dan dinamis. Keahlian kami dalam menganalisis dan mengeksploitasi kerentanan seperti yang ditunjukkan dalam artikel ini, bersama dengan pendekatan holistik kami dalam keamanan, memastikan bahwa infrastruktur dan aplikasi Anda dilindungi dari ancaman terbaru. Jika Anda membutuhkan konsultasi atau layanan dalam bidang keamanan siber, blockchain, atau AI, tim ahli kami di Cylabus siap untuk membantu.
Dengan inovasi dan kerjasama yang terus-menerus, kami di Cylabus berdedikasi untuk memajukan keamanan siber dan mendukung penciptaan ekosistem digital yang lebih aman untuk semua.
Baca Artikel Kami Yang Lain

Hati-hati, Screenshot HP Kamu Bisa Bocorin Rahasia!
Demonstrasi Kerentanan Screenshot (CVE-2023-21036) di kehidupan sehari-hari dan simulasinya dalam CTF Digial Forensic

Eksploitasi macOS: Tantangan BabyIOKit Driver
Memahami seluk-beluk eksploitasi macOS melalui analisis mendetail terhadap IOKit driver yang rentan.

Eksploitasi Blockchain: The Pigeon Bank
Mengupas teknik eksploitasi Ethereum blockchain bernama reentrancy attack terhadap smart contract yang rentan.