Rahasia firmware

Penulis: Ph.D. Chernov A.V. ( monsieur_cher ) dan Ph.D. Troshina K.N.

Bagaimana, menggunakan asumsi paling umum berdasarkan pengetahuan arsitektur prosesor modern, dapatkah Anda mengembalikan struktur program dari gambar biner dari arsitektur yang tidak dikenal, dan kemudian mengembalikan algoritma dan banyak lagi?

Dalam artikel ini kita akan berbicara tentang satu tugas menarik yang ditetapkan sebelum kita beberapa tahun yang lalu. Pelanggan diminta untuk berurusan dengan firmware biner dari perangkat yang mengelola proses fisik tertentu. Dia membutuhkan algoritma kontrol dalam bentuk program C yang dikompilasi, serta formula dengan penjelasan tentang bagaimana mereka bekerja dan mengapa. Menurut Pelanggan, ini diperlukan untuk memastikan kompatibilitas dengan peralatan "lama" dalam sistem baru. Cara kita akhirnya berurusan dengan fisika, dalam kerangka seri artikel ini, kita hilangkan, tetapi kita akan mempertimbangkan proses memulihkan algoritma secara detail.

Penggunaan mikrokontroler yang dapat diprogram di mana-mana di perangkat massal (IOT atau SmartHome Internet of Things) hampir di mana-mana membutuhkan perhatian pada analisis biner dari kode yang disematkan, atau, dengan kata lain, analisis biner dari firmware perangkat.

Analisis biner dari firmware perangkat mungkin memiliki tujuan sebagai berikut:

  • Analisis kode untuk kerentanan yang memungkinkan untuk mendapatkan akses tidak sah ke perangkat atau ke data yang dikirimkan atau diproses oleh perangkat ini.
  • Analisis kode untuk fitur tidak berdokumen, yang mengarah, misalnya, ke kebocoran informasi.
  • Analisis kode untuk memulihkan protokol dan antarmuka interaksi dengan perangkat untuk memastikan kompatibilitas perangkat ini dengan orang lain.

Tugas analisis kode biner yang diajukan di atas dapat dianggap sebagai kasus khusus tugas analisis biner untuk memastikan kompatibilitas perangkat.

Analisis format file biner


Jika dalam dunia sistem operasi "nyata", format file yang dapat dieksekusi distandarisasi, maka dalam dunia program embedded, setiap vendor dapat menggunakan solusi eksklusifnya. Oleh karena itu, analisis file firmware biner harus dimulai dengan analisis format file biner.

Pada awal pekerjaan, situasi bagi kami adalah sebagai berikut: kami menerima file tertentu dengan firmware tanpa disertai dokumentasi. Tidak ada informasi tentang format file firmware, juga tentang arsitektur mikrokontroler.

File firmware ternyata berupa file teks. Itu berisi garis-garis bentuk berikut:

:04013000260F970CF8 :10020000004D000B043F000B34AD010C002FFE4D30 :02023000FD0BC1 :1004000018001A0000001E0008005E000200190052 

Dengan hati-hati melihat rangkaian garis-garis ini, kami menyadari bahwa ini adalah format Intel HEX yang sepenuhnya standar untuk mikrokontroler. File terdiri dari catatan, yang masing-masing menunjukkan jenisnya, lokasi memori, data, dan checksum. Dengan sendirinya, menggunakan format Intel Hex menyiratkan bahwa file tersebut kemungkinan besar tidak dienkripsi dan merupakan gambar dari program yang berada di memori.

Meskipun format Intel Hex mendukung pengalamatan memori hingga 32-bit, hanya ada alamat memori 16-bit di file kami. Oleh karena itu, mudah untuk membuat file biner dari gambar memori dari file teks di mana catatan dari file uji asli sudah akan ditempatkan di alamat yang ditentukan. Lebih mudah untuk memeriksa file biner seperti itu menggunakan utilitas analisis file biner, dan lebih mudah untuk menulis utilitas Anda sendiri untuknya daripada untuk Intel HEX. File memori gambar biner mengonfirmasi bahwa file tersebut tidak dienkripsi, karena berbagai baris bermakna ditemukan tersebar di berbagai tempat dalam kode.
Namun, ini tidak menjawab pertanyaan untuk arsitektur apa file ini.



Kami mendapat file dengan gambar memori beberapa mikrokontroler 16-bit atau 8-bit. Dan jenis mikrokontroler tidak jelas. Kami mengambil IDA Pro dan mencoba membongkar file dengan semua varian prosesor yang didukung. Dan tidak ada apa-apa. Tak satu pun dari prosesor IDA Pro yang didukung muncul: daftar tidak dibuat atau berisi omong kosong yang jelas. Itu mungkin program untuk salah satu prosesor IDA Pro yang didukung, tetapi kami melakukan sesuatu yang salah. Misalnya, Anda hanya perlu pemrosesan file gambar tambahan. Bagaimanapun, di sini dimungkinkan untuk menangguhkan pekerjaan dan meminta informasi tambahan tentang file biner.

Semua prosesor hampir sama.


Tetapi itu menjadi menarik bagi kami, dan apa yang dapat kita pahami dari program biner, bahkan jika prosesor yang dikompilasi tidak diketahui. Jawabannya adalah "tidak ada" - tidak menarik, bahkan jika kita bisa mengerti sedikit, itu lebih baik daripada tidak sama sekali. Jelas, string teks dapat memberikan informasi tentang program, tetapi kami bertujuan untuk lebih - untuk memahami sesuatu dari struktur program.
Berbagai arsitektur prosesor - sejumlah besar. Evolusi komputasi telah menghasilkan bahkan arsitektur yang paling tidak biasa seperti komputer ternary. Namun, mikroprosesor dan mikrokontroler yang saat ini ada, setidaknya yang massa, sangat mirip satu sama lain.

Di bawah ini kami mencantumkan prinsip-prinsip dasar umum untuk mikroprosesor modern.

Eksekusi instruksi yang konsisten. Prosesor menjalankan instruksi secara berurutan dalam memori. Ada instruksi khusus untuk lompatan bersyarat dan tak bersyarat serta menelepon dan kembali dari subrutin, yang memungkinkan Anda untuk mengganggu pemilihan instruksi berurutan dari memori dan melanjutkan ke pelaksanaan instruksi lain. Namun, sisa instruksi mengasumsikan eksekusi berurutan, dan oleh karena itu tidak mengandung alamat instruksi berikutnya.

Pengodean biner. Selain fakta bahwa prosesor memproses data dalam bentuk biner, instruksi prosesor yang membentuk program yang dapat dieksekusi dikodekan dalam format biner, yaitu bidang di mana parameter instruksi disimpan, misalnya, offset atau nomor register, menempati sejumlah bilangan bulat. Orang dapat secara teoritis membayangkan bahwa, meskipun data dan program pengkodean biner, mereka akan diproses dalam prosesor di beberapa sistem angka lainnya, tetapi kami tidak menyadari eksotis semacam itu.

Prinsip-prinsip berikut, secara umum, bukan prinsip arsitektur dasar, tetapi mereka secara praktis diterapkan secara universal, terutama untuk kode mesin yang tidak ditulis secara manual dalam bahasa assembly, tetapi dihasilkan oleh kompiler bahasa tingkat tinggi.

Pemrograman prosedural. Program ini dibagi menjadi unit struktural, yang dapat disebut berbeda: prosedur, fungsi, subprogram, dll. Subprogram dapat memanggil subprogram lain, meneruskan parameter kepada mereka dan mendapatkan kembali hasil eksekusi. Penting bahwa subprogram memiliki satu titik masuk, yaitu, semua subprogram yang memanggil yang diberikan pergi ke alamat titik entri yang sama.

Biasanya, rutin memiliki satu titik keluar yang mengembalikan kontrol ke titik panggilan, tetapi ini kurang signifikan daripada membutuhkan satu titik masuk untuk setiap rutin. Kode seperti itu biasanya diperoleh dengan menyusun program. Pengoptimal tautan-waktu dapat menghancurkan sebagian struktur ini untuk mengurangi ukuran program, dan ukuran program sangat penting untuk sistem tertanam. Selain itu, struktur ini dapat dihancurkan oleh kode obfuscator.

Bersarang panggilan subrutin dapat diatur menggunakan tumpukan, yang masih dapat digunakan untuk meneruskan argumen ke subrutin dan menyimpan variabel lokal, tetapi pada tingkat perkembangan arsitektur saat ini informasi ini terlalu dini.

Bagaimana prinsip-prinsip ini dapat diterapkan pada analisis awal kode biner?

Kami membuat asumsi dasar bahwa ada instruksi RET (kembali dari subrutin) dalam sistem perintah prosesor. Instruksi ini memiliki beberapa jenis representasi biner tetap, yang akan kita cari. Jika RET bukan satu-satunya, seperti pada x86, di mana RET memiliki argumen - ukuran area parameter subrutin, atau jika RET adalah efek samping dari operasi yang lebih rumit, seperti pada ARMv7, di mana nilai PC dapat diambil dari tumpukan secara bersamaan dengan nilai register lainnya (ldmfd sp! , {fp, pc}), maka, kemungkinan besar, pencarian heuristik kami tidak akan menghasilkan hasil.

Kita juga perlu segera membuat asumsi yang masuk akal tentang prinsip pengkodean instruksi prosesor yang sedang dipelajari. Prosesor yang ada menggunakan beberapa prinsip untuk instruksi pengkodean:

  • Aliran byte dari mana instruksi dihasilkan, dan instruksi berbeda dikodekan dengan jumlah byte yang berbeda. Dalam kategori ini, perwakilan paling terkenal adalah keluarga x86, dari prosesor 8080 pertama hingga prosesor 64-bit paling modern. Satu instruksi prosesor x86_64 dapat dikodekan dalam urutan 1 hingga 16 byte. Rangkaian prosesor yang sama dengan panjang instruksi variabel mencakup 8051, yang digunakan dalam mikrokontroler.
  • Aliran nilai 16-bit. Selain itu, setiap instruksi memiliki ukuran tetap - 16 bit.
  • Aliran nilai 16-bit, sedangkan instruksi berukuran bervariasi. Salah satu perwakilan dari keluarga ini adalah arsitektur PDP-11, di mana perintah itu sendiri menempati 16 bit pertama, dan dapat diikuti oleh nilai langsung atau alamat memori untuk pengalamatan langsung. Ini termasuk pengkodean THUMB dalam arsitektur ARM.
  • Aliran nilai 32-bit, setiap instruksi memiliki ukuran tetap 32 bit. Ini adalah mayoritas dari prosesor RISC 32- dan 64-bit: ARMv7, ARMv8, MIPS.

Memilih antara aliran byte panjang variabel dan aliran kata 16-bit akan membantu melihat gambar memori "dengan mata". Tidak peduli bagaimana instruksi prosesor dikodekan, dalam suatu program dengan panjang yang cukup mereka akan diulangi. Misalnya, pada instruksi x86

 add %ebx,%eax 

yang menambahkan nilai register eax dan ebx dan menempatkan hasilnya dalam eax, dikodekan dalam dua byte:

 01 d8. 

Pada instruksi ARMv7

 add r0, r0, r1 

yang menambahkan nilai register r0 dan r1 dan menempatkan hasilnya dalam r0, dikodekan oleh nilai 32-bit e0800001.

Dalam program yang cukup besar, instruksi seperti itu akan diulang lebih dari satu kali. Jika urutan byte yang menarik bagi kami (misalnya, 01 d8) terjadi pada alamat yang tidak selaras sewenang-wenang, kami dapat mengasumsikan bahwa instruksi prosesor dikodekan oleh aliran byte ukuran variabel. Jika nilainya, misalnya, e0800001 hanya ditemukan di alamat yang merupakan kelipatan dari 4, kita dapat membuat asumsi tentang ukuran tetap dari instruksi prosesor. Tentu saja, ada kesalahan di sini bahwa kami mengambil byte data untuk instruksi, atau itu terjadi secara kebetulan bahwa beberapa instruksi selalu ternyata selaras. Namun, dampak dari "kebisingan" tersebut pada program dengan ukuran yang cukup akan kecil.

Ketika kami melihat firmware yang dianalisis dari sudut ini, menjadi jelas bahwa kemungkinan besar instruksi untuk prosesor tersebut dikodekan dengan nilai 16-bit.

Jadi, berdasarkan asumsi bahwa pengkodean instruksi RET adalah beberapa nilai tetap 16-bit, mari kita coba menemukannya. Kami menemukan dalam gambar program nilai 16-bit paling umum. Dalam kasus kami, yang berikut ini terjadi:

  (hex)   0b01 854 5.1% 0800 473 2.8% 8c0d 432 2.6% 2b00 401 2.4% 4e1c 365 2.2% 0801 277 1.6% 

Kami akan mencari instruksi RET di antara nilai-nilai 16-bit ini yang paling sering ditemui dalam kode. Kami akan segera mencari instruksi PANGGILAN - dipasangkan dengan instruksi RET. Instruksi PANGGILAN memiliki setidaknya satu parameter - alamat lompatan, sehingga nilai tetap sangat diperlukan.

Kami membuat asumsi bahwa dalam banyak kasus, segera setelah akhir satu subprogram, yaitu, setelah instruksi RET, subprogram lain dimulai, dan subprogram ini dipanggil oleh instruksi PANGGILAN dari titik lain dalam program. Sejumlah besar hop ke alamat segera setelah RET akan menjadi salah satu keunggulan dari instruksi CALL. Tentu saja, aturan ini tidak universal, karena pada beberapa platform, khususnya, ARMv7, segera setelah selesai dari subrutin, biasanya terdapat kumpulan yang konstan. Dalam hal ini, kami dapat mempertimbangkan beberapa kisaran alamat yang wajar segera setelah RET sebagai titik transisi dari instruksi RET.

Dalam hal instruksi PANGGILAN, ada cukup banyak opsi untuk menyandikannya ke subrutin. Pertama, prosesor dapat menggunakan urutan byte yang berbeda dalam kata: little-endian, seperti pada kebanyakan prosesor modern, ketika integer multibyte ditulis ke memori, dimulai dengan byte rendah, dan big-endian, ketika integer multibit ditulis ke memori, mulai dari byte tinggi. Hampir semua prosesor modern beroperasi dalam mode little-endian, tetapi Anda tidak boleh membuang perintah byte lain yang mungkin dalam satu kata.
Kedua, instruksi PANGGILAN dapat menggunakan pengalamatan absolut dari titik lompatan atau pengalamatan relatif terhadap alamat saat ini. Dalam hal pengalamatan absolut, instruksi yang dikodekan berisi alamat yang ingin Anda tuju dalam beberapa bit dari instruksi yang dikodekan. Untuk memastikan bahwa subrutin dipanggil dari titik mana saja di ruang alamat 16-bit ke titik lain di alamat absolut kata 16-bit, instruksi yang disandikan tidak cukup, karena selain alamat transisi, bit-bit dari kode operasi perlu disimpan di tempat lain. Oleh karena itu, masuk akal untuk mempertimbangkan dua kata 16-bit berturut-turut dan mencoba berbagai opsi untuk memisahkan alamat transisi antara kata-kata ini.

Alternatif pengodean absolut dari alamat rutin adalah pengodean relatif. Dalam instruksi yang disandikan, kami mencatat perbedaan antara alamat subprogram dan titik saat ini. Pengodean relatif biasanya lebih baik daripada absolut, karena, pertama, program dengan transisi relatif secara independen, yaitu, dapat ditempatkan di memori dari alamat apa pun tanpa ada perubahan dalam kode biner. Kedua, untuk pengkodean offset, bit lebih sedikit dapat dicadangkan daripada dimensi ruang alamat, berdasarkan pada kenyataan bahwa dalam banyak kasus rutin yang dipanggil tidak jauh dari yang dipanggil. Namun, jika offset untuk panggilan berada di luar kisaran nilai yang dapat diwakili, Anda harus memasukkan instruksi khusus - "melompat".

Pengkodean relatif alamat subprogram dapat dilakukan dengan beberapa variasi: pertama, alamat titik saat ini dari program dapat diambil baik sebagai alamat instruksi saat ini, atau sebagai alamat instruksi berikutnya, seperti pada prosesor x86, atau alamat beberapa instruksi lain di dekat titik saat ini. Misalnya, dalam prosesor ARM, titik referensi adalah alamat instruksi saat ini +8 (yaitu, bukan alamat instruksi yang mengikuti CALL, tetapi alamat instruksi yang mengikuti yang berikutnya). Selain itu, karena dalam kasus kami program ditulis sebagai aliran kata-kata 16-bit, adalah logis untuk mengharapkan bahwa offset akan diekspresikan dalam kata-kata. Artinya, untuk mendapatkan alamat rutin yang disebut, offset harus dikalikan dengan 2.

Dengan mempertimbangkan semua hal di atas, kami memperoleh ruang enumerasi berikut untuk mencari pasangan CALL / RET dalam kode biner.

Pertama, kami mengambil kata 16-bit dari daftar nilai paling umum dalam kode sebagai kandidat untuk instruksi RET. Selanjutnya, kami mencari melalui instruksi PANGGILAN:

  • Urutan byte kata big-endian dan little-endian
  • Pengkodean absolut dan relatif dari alamat rutin dalam instruksi.

Untuk pengkodean absolut, kami mempertimbangkan dua nilai 16-bit berturut-turut, yaitu, kami menyortir berbagai opsi untuk menempatkan bidang bit yang menyimpan alamat absolut dalam kata 32-bit, dan untuk pengkodean relatif kami mempertimbangkan nilai 16-bit dan dua kata 16-bit berturut-turut . Selanjutnya, kami memilah berbagai opsi untuk menempatkan bidang bit yang menyimpan offset. Kami memeriksa apakah offset dinyatakan dalam byte atau kata-kata 16-bit, yaitu, apakah perlu untuk mengalikan offset dengan 2, kami memeriksa opsi yang berbeda untuk titik referensi: alamat instruksi saat ini, alamat instruksi berikutnya.

Untuk setiap opsi di ruang pencarian yang dijelaskan di atas, kami menghitung statistik:

  • Berapa banyak alamat yang diasumsikan dari awal subprogram tidak jelas benar, yaitu mereka berlokasi di mana tidak ada, atau di mana data (string eksplisit, atau tabel nilai eksplisit) berada. Bahkan untuk nilai yang sesuai dengan pengkodean yang benar dari instruksi CALL, sangat mungkin bahwa sejumlah kecil alamat yang salah dari awal subprogram dimungkinkan jika nilai yang sesuai dengan instruksi CALL secara tidak sengaja terjadi dalam data.
  • Berapa banyak alamat mulai putatif rutin segera setelah instruksi RET putatif.
  • Berapa banyak alamat awal hipotetis dari rutinitas digunakan lebih dari sekali.

Jika asumsi kami tentang sepasang instruksi CALL / RET benar, maka itu harus di ruang enumerasi yang dijelaskan. Tetapi mungkin juga ada positif palsu. Baiklah, kami memulai pencarian.

Dan kami hanya menemukan satu opsi yang memungkinkan!

 Trying 8c0d as RET After-ret-addr-set-size: 430 Matching call opcodes for 1, ff00ff00, 1: 000b003d: total: 1275, hits: 843 (66%), misses: 432 (33%), coverage: 76% 

Jadi, kata 8c0d 16-bit cocok sebagai kandidat untuk instruksi RET. Secara total, firmware berisi 430 posisi alamat program segera setelah instruksi ini. Kami mempertimbangkan nilai 32-bit (dua kata 16-bit berturut-turut), dengan nilai mask alamat ff 00 ff 00, sebuah instruksi dengan kode 00 0b 00 3d ditemukan. Ada 1.275 instruksi semacam itu, di mana 843 (mis. 66%) mentransfer kendali ke titik segera mengikuti kandidat untuk RET. Dengan demikian, dua instruksi telah diidentifikasi:

  • RET: 8c0d (16-bit Little-Endian)
  • CALL HHLL: 0bHH 3dLL (2 Little-Endian 16-bit)

Instruksi PANGGILAN menggunakan pengalamatan absolut, dan saat menulis alamat lompat, byte tinggi ditulis terlebih dahulu, kemudian byte rendah ditulis. Ada kemungkinan bahwa pada kenyataannya ini adalah dua instruksi prosesor, yang masing-masing memuat setengah alamat transisi, tetapi dari sudut pandang analisis program, ini tidak penting. Mengetahui instruksi PANGGILAN dan RET, kami dapat lebih akurat menandai area kode dan data program, yang akan penting untuk analisis lebih lanjut.

Dilanjutkan ...

Selanjutnya kita akan mengatakan bagaimana transisi kondisional dan tanpa syarat dan beberapa operasi aritmatika dan logis dipulihkan.

Source: https://habr.com/ru/post/id442262/


All Articles