Analisis kode lama ketika kode sumber hilang: harus dilakukan atau tidak?

Analisis kode biner, yaitu kode yang dieksekusi langsung oleh mesin, adalah tugas yang tidak sepele. Dalam kebanyakan kasus, jika Anda perlu menganalisis kode biner, kode tersebut dipulihkan terlebih dahulu dengan membongkar, dan kemudian mendekompilasi menjadi beberapa representasi tingkat tinggi, dan kemudian mereka menganalisis apa yang terjadi.

Di sini harus dikatakan bahwa kode yang dipulihkan, sesuai dengan representasi teks, memiliki sedikit kesamaan dengan kode yang awalnya ditulis oleh programmer dan dikompilasi menjadi file yang dapat dieksekusi. Tidak mungkin untuk memulihkan file biner yang diterima dari bahasa pemrograman yang dikompilasi seperti C / C ++, Fortran, karena ini adalah tugas yang tidak diformalkan secara algoritmik. Dalam proses konversi kode sumber yang ditulis programmer ke dalam program yang dijalankan mesin, kompiler melakukan konversi yang tidak dapat diubah.

Pada tahun 90-an abad terakhir, secara luas diyakini bahwa penyusun, seperti penggiling daging, menggiling program asli, dan tugas memulihkannya mirip dengan tugas memulihkan seekor domba jantan dari sosis.


Namun, tidak terlalu buruk. Dalam proses mendapatkan sosis, domba kehilangan fungsinya, sedangkan program biner menyimpannya. Jika sosis yang dihasilkan bisa berlari dan melompat, maka tugasnya akan sama.

Jadi, setelah program biner mempertahankan fungsinya, kita dapat mengatakan bahwa adalah mungkin untuk mengembalikan kode yang dapat dieksekusi ke representasi tingkat tinggi sehingga fungsionalitas dari program biner, yang representasi aslinya tidak ada, dan program yang representasi teksnya kami terima, adalah setara.

Menurut definisi, dua program secara fungsional setara jika, pada input data yang sama, keduanya selesai atau gagal untuk menyelesaikan eksekusi dan, jika eksekusi selesai, menghasilkan hasil yang sama.

Tugas pembongkaran biasanya diselesaikan dalam mode semi-otomatis, yaitu spesialis melakukan pemulihan manual menggunakan alat-alat interaktif, misalnya, pembongkar interaktif IdaPro , radare atau alat lain. Selanjutnya, juga dalam mode semi-otomatis, dekompilasi dilakukan. HexRays , SmartDecompiler atau decompiler lain, yang cocok untuk menyelesaikan tugas dekompilasi ini, digunakan sebagai alat dekompilasi untuk membantu spesialis.

Pemulihan representasi tekstual asli dari program dari byte-code dapat dibuat cukup akurat. Untuk bahasa yang ditafsirkan seperti Java atau bahasa keluarga .NET, terjemahan yang dilakukan dalam byte-code, tugas dekompilasi diselesaikan secara berbeda. Kami tidak mempertimbangkan masalah ini dalam artikel ini.

Jadi, Anda dapat menganalisis program biner melalui dekompilasi. Biasanya, analisis semacam itu dilakukan untuk memahami perilaku suatu program untuk mengganti atau memodifikasinya.

Dari praktik bekerja dengan program warisan


Beberapa perangkat lunak, yang ditulis 40 tahun lalu di keluarga bahasa tingkat rendah C dan Fortran, mengendalikan peralatan produksi minyak. Kegagalan peralatan ini bisa sangat penting untuk produksi, sehingga mengubah perangkat lunak sangat tidak diinginkan. Namun, selama beberapa tahun terakhir, kode sumber hilang.

Karyawan baru dari departemen keamanan informasi, yang tanggung jawabnya untuk memahami cara kerjanya, menemukan bahwa program pemantauan sensor menulis sesuatu ke disk dengan keteraturan, dan bahwa ia menulis dan bagaimana informasi ini dapat digunakan tidak jelas. Dia juga memiliki gagasan bahwa pemantauan pengoperasian peralatan dapat ditampilkan pada satu layar besar. Untuk melakukan ini, perlu dipahami bagaimana program bekerja, apa dan dalam format apa ia menulis ke disk, bagaimana informasi ini dapat ditafsirkan.

Untuk mengatasi masalah tersebut, teknologi dekompilasi diterapkan, diikuti dengan analisis kode yang dipulihkan. Kami pertama-tama membongkar komponen perangkat lunak satu per satu, kemudian melokalkan kode yang bertanggung jawab atas input / output informasi, dan secara bertahap mulai pulih dari kode ini, mengingat ketergantungannya. Kemudian, logika program dipulihkan, yang memungkinkan untuk menjawab semua pertanyaan dari layanan keamanan mengenai perangkat lunak yang dianalisis.

Jika Anda perlu menganalisis program biner untuk mengembalikan logika operasinya, sebagian atau sepenuhnya mengembalikan logika konversi data input ke data output, dll., Akan lebih mudah untuk melakukan ini menggunakan decompiler.

Selain tugas-tugas tersebut, dalam praktiknya ada masalah menganalisis program biner untuk persyaratan keamanan informasi. Selain itu, pelanggan tidak selalu memiliki pemahaman bahwa analisis ini sangat memakan waktu. Tampaknya, lakukan dekompilasi dan jalankan kode yang dihasilkan dengan analisa statis. Tetapi sebagai hasil dari analisis kualitatif, hampir tidak pernah berhasil.

Pertama, kerentanan yang ditemukan harus tidak hanya dapat ditemukan, tetapi juga untuk dijelaskan. Jika kerentanan ditemukan dalam suatu program dalam bahasa tingkat tinggi, analis atau alat analisis kode menunjukkan di dalamnya fragmen kode mana yang mengandung kelemahan tertentu, yang keberadaannya menyebabkan kerentanan. Bagaimana jika tidak ada kode sumber? Bagaimana cara menunjukkan kode mana yang menyebabkan kerentanan?

Decompiler memulihkan kode yang "berserakan" dengan artefak pemulihan, dan tidak ada gunanya memetakan kerentanan yang diungkapkan untuk kode tersebut, tetapi tidak ada yang jelas. Selain itu, kode yang dipulihkan tidak terstruktur dengan baik dan oleh karena itu kurang cocok untuk alat analisis kode. Menjelaskan kerentanan dalam hal program biner juga sulit, karena orang yang penjelasannya harus berpengalaman dalam representasi biner program.

Kedua, analisis biner berdasarkan persyaratan keamanan informasi harus dilakukan dengan pemahaman tentang apa yang harus dilakukan dengan hasil yang dihasilkan, karena sangat sulit untuk memperbaiki kerentanan dalam kode biner, tetapi tidak ada kode sumber.

Terlepas dari semua fitur dan kesulitan melakukan analisis statis program biner sesuai dengan persyaratan keamanan informasi, ada banyak situasi ketika analisis seperti itu diperlukan. Jika karena alasan tertentu tidak ada kode sumber, dan program biner menjalankan fungsionalitas penting untuk persyaratan keamanan informasi, itu harus diperiksa. Jika kerentanan ditemukan, aplikasi seperti itu harus dikirim untuk revisi, jika mungkin, atau "shell" tambahan harus dibuat untuk itu, yang akan memungkinkan mengendalikan pergerakan informasi sensitif.

Ketika kerentanan disembunyikan dalam file biner


Jika kode yang dijalankan oleh program memiliki tingkat kekritisan yang tinggi, bahkan jika kode sumber program tersebut dalam bahasa tingkat tinggi, akan berguna untuk mengaudit file biner. Ini akan membantu menghilangkan fitur yang dapat dikompilasi oleh kompiler dengan melakukan transformasi optimal. Jadi, pada bulan September 2017, transformasi optimasi yang dilakukan oleh kompiler Dentang dibahas secara luas. Hasilnya adalah panggilan ke fungsi yang seharusnya tidak pernah dipanggil.

#include <stdlib.h> typedef int (*Function)(); static Function Do; static int EraseAll() { return system("rm -rf /"); } void NeverCalled() { Do = EraseAll; } int main() { return Do(); } 

Sebagai hasil dari transformasi optimisasi, kompiler akan mendapatkan kode assembler tersebut. Contoh ini dikompilasi di Linux X86 dengan flag -O2.

  .text .globl NeverCalled .align 16, 0x90 .type NeverCalled,@function NeverCalled: # @NeverCalled retl .Lfunc_end0: .size NeverCalled, .Lfunc_end0-NeverCalled .globl main .align 16, 0x90 .type main,@function main: # @main subl $12, %esp movl $.L.str, (%esp) calll system addl $12, %esp retl .Lfunc_end1: .size main, .Lfunc_end1-main .type .L.str,@object # @.str .section .rodata.str1.1,"aMS",@progbits,1 .L.str: .asciz "rm -rf /" .size .L.str, 9 

Ada perilaku yang tidak ditentukan dalam kode sumber. Fungsi NeverCalled () dipanggil karena transformasi optimasi yang dilakukan oleh kompiler. Selama proses pengoptimalan, ia kemungkinan besar melakukan analisis terhadap alliases , dan sebagai hasilnya, fungsi Do () menerima alamat dari fungsi NeverCalled (). Dan karena metode main () memanggil fungsi Do (), yang tidak didefinisikan, yang merupakan perilaku tidak terdefinisi (perilaku tidak terdefinisi), hasilnya adalah sebagai berikut: fungsi EraseAll () dipanggil, yang menjalankan rm -rf / perintah.

Contoh berikut: sebagai hasil dari transformasi optimisasi dari kompiler, kami kehilangan tanda centang untuk NULL sebelum mendereferensinya.

 #include <cstdlib> void Checker(int *P) { int deadVar = *P; if (P == 0) return; *P = 8; } 

Karena baris 3 dereferences pointer, kompilator mengasumsikan pointer tidak nol. Selanjutnya, baris 4 dihapus sebagai hasil dari optimasi "penghapusan kode yang tidak dapat dijangkau" , karena perbandingan dianggap berlebihan, dan setelah itu, baris 3 dihapus oleh kompiler sebagai hasil dari optimasi " penghapusan kode mati". Hanya tersisa baris 5. Kode assembler yang dihasilkan dari kompilasi gcc 7.3 di Linux x86 dengan flag -O2 ditunjukkan di bawah ini.

  .text .p2align 4,,15 .globl _Z7CheckerPi .type _Z7CheckerPi, @function _Z7CheckerPi: movl 4(%esp), %eax movl $8, (%eax) ret 

Contoh-contoh pekerjaan optimisasi kompiler di atas adalah hasil dari perilaku UB yang tidak ditentukan dalam kode. Namun, ini adalah kode yang cukup normal yang oleh sebagian besar programmer dianggap aman. Hari ini, programmer menghabiskan waktu menghilangkan perilaku tidak terdefinisi dalam program, sementara 10 tahun yang lalu mereka tidak memperhatikannya. Akibatnya, kode lawas mungkin mengandung kerentanan UB.

Sebagian besar penganalisa kode sumber statis modern tidak mendeteksi kesalahan yang berhubungan dengan UB. Oleh karena itu, jika kode melakukan fungsional fungsional untuk persyaratan keamanan informasi, perlu untuk memeriksa kode sumbernya dan kode yang akan dieksekusi.

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


All Articles