it-swarm-id.com

Bagaimana cara kerja ASLR dan DEP?

Bagaimana cara kerja Space Layout Randomisation (ASLR) dan Pencegahan Eksekusi Data (DEP) bekerja, dalam hal mencegah kerentanan dari dieksploitasi? Bisakah mereka dilewati?

115
Polynomial

Address Space Layout Randomisation (ASLR) adalah teknologi yang digunakan untuk membantu mencegah shellcode agar tidak berhasil. Ini dilakukan dengan secara acak mengimbangi lokasi modul dan struktur dalam memori tertentu. Pencegahan Eksekusi Data (DEP) mencegah sektor memori tertentu, mis. tumpukan, dari dieksekusi. Ketika digabungkan menjadi sangat sulit untuk mengeksploitasi kerentanan dalam aplikasi menggunakan teknik shellcode atau pemrograman berorientasi kembali (ROP).

Pertama, mari kita lihat bagaimana kerentanan normal dapat dieksploitasi. Kami akan melewatkan semua detail, tetapi katakan saja kami menggunakan kerentanan tumpukan buffer overflow. Kami telah memuat gumpalan besar 0x41414141 nilai ke dalam payload kami, dan eip telah disetel ke 0x41414141, jadi kami tahu ini bisa dieksploitasi. Kami kemudian pergi dan menggunakan alat yang sesuai (mis. Metasploit's pattern_create.rb) untuk menemukan offset nilai yang dimuat ke eip. Ini adalah offset awal dari kode exploit kami. Untuk memverifikasi, kami memuat 0x41 sebelum offset ini, 0x42424242 di offset, dan 0x43 setelah offset.

Dalam proses non-ASLR dan non-DEP, alamat tumpukan sama setiap kali kami menjalankan proses. Kami tahu persis di mana itu dalam memori. Jadi, mari kita lihat seperti apa tumpukan itu dengan data uji yang kami jelaskan di atas:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 42424242   > esp points here
 000ff6b4  | 43434343
 000ff6b8  | 43434343

Seperti yang bisa kita lihat, esp menunjuk ke 000ff6b0, yang telah diatur ke 0x42424242. Nilai sebelum ini adalah 0x41 dan nilai setelahnya adalah 0x43, seperti yang kami katakan seharusnya. Kita sekarang tahu bahwa alamat itu disimpan di 000ff6b0 akan dilompat ke. Jadi, kami mengaturnya ke alamat beberapa memori yang dapat kami kontrol:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Kami telah menetapkan nilai pada 000ff6b0 sedemikian rupa sehingga eip akan diatur ke 000ff6b4 - offset berikutnya di stack. Ini akan menyebabkan 0xcc untuk dieksekusi, yang merupakan int3 petunjuk. Sejak int3 adalah breakpoint interupsi perangkat lunak, itu akan memunculkan pengecualian dan debugger akan berhenti. Ini memungkinkan kami memverifikasi bahwa eksploitasi berhasil.

> Break instruction exception - code 80000003 (first chance)
[snip]
eip=000ff6b4

Sekarang kita dapat mengganti memori di 000ff6b4 dengan shellcode, dengan mengubah muatan kami. Ini menyimpulkan eksploitasi kami.

Untuk mencegah agar eksploitasi ini tidak berhasil, Pencegahan Eksekusi Data dikembangkan. DEP memaksa struktur tertentu, termasuk tumpukan, untuk ditandai sebagai tidak dapat dieksekusi. Ini diperkuat oleh dukungan CPU dengan bit No-Execute (NX), juga dikenal sebagai bit XD, bit EVP, atau bit XN, yang memungkinkan CPU untuk menegakkan hak eksekusi pada level perangkat keras. DEP diperkenalkan di Linux pada tahun 2004 (kernel 2.6.8), dan Microsoft memperkenalkannya pada tahun 2004 sebagai bagian dari WinXP SP2. Apple menambahkan dukungan DEP ketika mereka pindah ke arsitektur x86 pada tahun 2006. Dengan DEP diaktifkan, exploit kami sebelumnya tidak akan berfungsi:

> Access violation - code c0000005 (!!! second chance !!!)
[snip]
eip=000ff6b4

Ini gagal karena tumpukan ditandai sebagai tidak dapat dieksekusi, dan kami telah mencoba untuk menjalankannya. Untuk menyiasatinya, teknik yang disebut Return-Oriented Programming (ROP) dikembangkan. Ini melibatkan mencari potongan kecil kode, yang disebut gadget ROP, dalam modul yang sah dalam proses. Gadget ini terdiri dari satu atau beberapa instruksi, diikuti oleh pengembalian. Merantai ini bersama-sama dengan nilai-nilai yang sesuai dalam tumpukan memungkinkan kode untuk dieksekusi.

Pertama, mari kita lihat bagaimana tumpukan kita terlihat sekarang:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 000ff6b4
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Kami tahu bahwa kami tidak dapat menjalankan kode di 000ff6b4, jadi kami harus menemukan beberapa kode yang sah yang dapat kami gunakan sebagai gantinya. Bayangkan bahwa tugas pertama kita adalah memasukkan nilai ke dalam daftar eax. Kami mencari pop eax; ret kombinasi di suatu tempat di modul apa pun dalam proses. Setelah kami menemukan satu, katakanlah di 00401f60, kami memasukkan alamatnya ke stack:

stack addr | value
-----------+----------
 000ff6a0  | 41414141
 000ff6a4  | 41414141
 000ff6a8  | 41414141
 000ff6aa  | 41414141
>000ff6b0  | 00401f60
 000ff6b4  | cccccccc
 000ff6b8  | 43434343

Ketika shellcode ini dieksekusi, kami akan mendapatkan pelanggaran akses lagi:

> Access violation - code c0000005 (!!! second chance !!!)
eax=cccccccc ebx=01020304 ecx=7abcdef0 edx=00000000 esi=7777f000 edi=0000f0f1
eip=43434343 esp=000ff6ba ebp=000ff6ff

CPU sekarang telah melakukan hal berikut:

  • Melompat ke pop eax instruksi pada 00401f60.
  • Muncul cccccccc dari stack, ke eax.
  • Menjalankan ret, muncul 43434343 menjadi eip.
  • Melanggar akses karena 43434343 bukan alamat memori yang valid.

Sekarang, bayangkan itu, bukannya 43434343, nilai pada 000ff6b8 disetel ke alamat gadget ROP lain. Ini berarti bahwa pop eax dieksekusi, lalu gadget kami berikutnya. Kami dapat menghubungkan gadget bersama-sama seperti ini. Tujuan akhir kami biasanya untuk menemukan alamat API perlindungan memori, seperti VirtualProtect, dan menandai tumpukan sebagai yang dapat dieksekusi. Kami kemudian akan menyertakan gadget ROP terakhir untuk melakukan jmp esp instruksi equivilent, dan jalankan shellcode. Kami telah berhasil melewati DEP!

Untuk mengatasi trik ini, ASLR dikembangkan. ASLR melibatkan penyeimbangan struktur memori dan alamat basis modul secara acak untuk membuat perkiraan lokasi perangkat dan API ROP menjadi sangat sulit.

Pada Windows Vista dan 7, ASLR mengacak lokasi file executable dan DLL dalam memori, serta tumpukan dan tumpukan. Ketika sebuah executable dimuat ke dalam memori, Windows mendapatkan timestamp counter (TSC) prosesor, menggesernya dengan empat tempat, melakukan pembagian mod 254, kemudian menambahkan 1. Angka ini kemudian dikalikan dengan 64KB, dan gambar yang dapat dieksekusi dimuat pada offset ini . Ini berarti ada 256 lokasi yang memungkinkan untuk dieksekusi. Karena DLL dibagi dalam memori di seluruh proses, offset mereka ditentukan oleh nilai bias sistem yang dihitung saat boot. Nilai tersebut dihitung sebagai TSC CPU ketika fungsi MiInitializeRelocations pertama kali dipanggil, digeser, dan ditutup-tutupi menjadi nilai 8-bit. Nilai ini dihitung hanya sekali per boot.

Ketika DLL dimuat, mereka masuk ke wilayah memori bersama antara 0x50000000 dan 0x78000000. DLL pertama yang akan dimuat selalu ntdll.dll, yang dimuat pada 0x78000000 - bias * 0x100000, di mana bias adalah nilai bias seluruh sistem yang dihitung saat boot. Karena akan sepele untuk menghitung offset modul jika Anda tahu alamat dasar ntdll.dll, urutan di mana modul dimuat adalah acak juga.

Ketika utas dibuat, lokasi dasar tumpukan mereka diacak. Hal ini dilakukan dengan menemukan 32 lokasi yang sesuai dalam memori, kemudian memilih satu berdasarkan TSC yang sekarang di-masked menjadi nilai 5-bit. Setelah alamat basis telah dihitung, nilai 9-bit lain diturunkan dari TSC untuk menghitung alamat basis stack akhir. Ini memberikan tingkat keacakan teoretis yang tinggi.

Akhirnya, lokasi tumpukan dan alokasi tumpukan secara acak. Ini dihitung sebagai nilai turunan TSC 5-bit dikalikan dengan 64KB, memberikan kisaran heap yang mungkin 00000000 hingga 001f0000.

Ketika semua mekanisme ini dikombinasikan dengan DEP, kami dicegah dari mengeksekusi shellcode. Ini karena kita tidak dapat menjalankan stack, tetapi kita juga tidak tahu di mana instruksi ROP kita akan berada dalam memori. Trik tertentu dapat dilakukan dengan sleds nop untuk membuat exploit probabilistik, tetapi mereka tidak sepenuhnya berhasil dan tidak selalu memungkinkan untuk dibuat.

Satu-satunya cara untuk memotong DEP dan ASLR dengan andal adalah melalui kebocoran pointer. Ini adalah situasi di mana nilai pada tumpukan, di lokasi yang dapat diandalkan, dapat digunakan untuk menemukan penunjuk fungsi yang dapat digunakan atau gadget ROP. Setelah ini dilakukan, kadang-kadang dimungkinkan untuk membuat payload yang andal memotong kedua mekanisme perlindungan.

Sumber:

Bacaan lebih lanjut:

153
Polynomial

Untuk melengkapi jawaban-sendiri @ Polynomial: DEP sebenarnya dapat diberlakukan pada mesin x86 lama (yang ada sebelum NX bit), tetapi dengan harga tertentu.

Cara mudah tapi terbatas untuk melakukan DEP pada perangkat keras x86 lama adalah dengan menggunakan register segmen. Dengan sistem operasi saat ini pada sistem seperti itu, alamat adalah nilai 32-bit dalam ruang alamat 4 GB yang datar, tetapi secara internal setiap akses memori secara implisit menggunakan alamat 32-bit dan register 16-bit khusus , disebut "register segmen".

Dalam apa yang disebut mode terproteksi, register segmen menunjuk ke tabel internal ("tabel deskriptor" - sebenarnya ada dua tabel seperti itu, tetapi itu adalah teknis) dan setiap entri dalam tabel menentukan karakteristik segmen tersebut. Khususnya, jenis akses yang diizinkan, dan size segmen. Selain itu, eksekusi kode secara implisit menggunakan register segmen CS, sementara akses data sebagian besar menggunakan DS (dan menumpuk akses, mis. Dengan opcodes Push dan pop, menggunakan SS) .Ini memungkinkan sistem operasi untuk membagi ruang alamat menjadi dua bagian; alamat yang lebih rendah berada dalam jangkauan untuk CS dan DS, sedangkan alamat atas berada di luar jangkauan untuk CS. Misalnya, segmen yang dijelaskan oleh CS dibuat berukuran 512 MB. Ini berarti bahwa setiap alamat di luar 0x20000000 akan dapat diakses sebagai data (baca atau tulis untuk menggunakan DS sebagai register dasar) tetapi upaya eksekusi akan menggunakan CS, pada titik mana CPU akan memunculkan eksepsi (dimana kernel akan mengubahnya menjadi sinyal yang cocok seperti SIGILL atau SIGSEGV, biasanya menyiratkan kematian dari proses yang menyinggung).

(Perhatikan bahwa segmen diterapkan pada ruang alamat; MMU masih aktif, di lapisan bawah, jadi trik yang dijelaskan di atas adalah per-proses.)

Ini murah untuk dilakukan: perangkat keras x86 lakukan menegakkan segmen, secara sistematis (dan 80386 pertama sudah melakukannya, sebenarnya, 80286 sudah memiliki segmen tersebut dengan batas, tetapi hanya offset 16-bit ). Kita biasanya dapat melupakan mereka karena sistem operasi yang waras mengatur segmen untuk mulai dari offset nol dan menjadi 4 GB panjang, tetapi pengaturan mereka jika tidak menyiratkan overhead yang tidak kita miliki. Namun, sebagai mekanisme DEP, itu tidak fleksibel: ketika beberapa blok data diminta dari kernel, kernel harus memutuskan apakah ini untuk kode atau bukan untuk kode, karena batasnya tetap. Kami tidak dapat memutuskan untuk secara dinamis mengonversi halaman mana pun yang diberikan antara mode kode dan mode data.

Menyenangkan tapi agak lebih mahal untuk melakukan DEP menggunakan sesuatu yang disebut PaX . Untuk memahami apa fungsinya, seseorang harus masuk ke beberapa detail.

The MMU pada perangkat keras x86 menggunakan tabel dalam memori, yang menggambarkan status setiap halaman 4 kB di ruang alamat. Ruang alamat adalah 4 GB, jadi ada 1048576 halaman. Setiap halaman dijelaskan oleh entri 32-bit dalam sub-tabel; ada 1024 sub-tabel, masing-masing menampung 1024 entri, dan ada satu tabel utama, dengan 1024 entri yang mengarah ke 1024 sub-tabel. Setiap entri memberitahu di mana objek menunjuk-ke ​​(sub-tabel, atau halaman) dalam RAM, atau apakah ada sama sekali, dan apa hak aksesnya. Akar dari masalah ini adalah bahwa hak akses adalah tentang tingkat hak istimewa (kode kernel vs userland) dan hanya satu bit untuk jenis akses, sehingga memungkinkan "baca-tulis" atau "baca-saja". "Eksekusi" dianggap semacam akses baca. Oleh karena itu, MMU tidak memiliki gagasan "eksekusi" yang berbeda dari akses data. Yang dapat dibaca, dapat dieksekusi.

(Sejak Pentium Pro, kembali pada abad sebelumnya, prosesor x86 mengetahui format lain untuk tabel, yang disebut PAE . Ini menggandakan ukuran entri, yang menyisakan ruang untuk mengatasi lebih banyak RAM fisik, dan juga menambahkan bit NX - tetapi bit spesifik itu diterapkan oleh perangkat keras hanya sekitar tahun 2004.)

Namun, ada trik. RAM lambat. Untuk melakukan akses memori, prosesor pertama-tama harus membaca tabel utama untuk menemukan sub-tabel yang harus dikonsultasikan, kemudian membaca lagi ke sub-tabel itu, dan hanya pada saat itu apakah prosesor tahu apakah akses memori harus diizinkan atau tidak, dan di mana secara fisik RAM data yang diakses sebenarnya. Ini adalah akses baca dengan ketergantungan penuh (setiap akses tergantung pada nilai dibaca oleh sebelumnya) jadi ini membayar latensi penuh, yang, pada CPU modern, dapat mewakili ratusan siklus clock. Oleh karena itu, CPU menyertakan cache spesifik yang berisi tabel MMU yang paling baru diakses diakses) entri. Tembolok ini adalah Buffer Penerjemahan Lookaside .

Dari 80486 dan seterusnya, x86 CPU tidak memiliki satu TLB, tetapi dua. Caching berfungsi pada heuristik, dan heuristik bergantung pada pola akses, dan pola akses untuk kode cenderung berbeda dari pola akses untuk data. Jadi orang-orang pintar di Intel/AMD/lainnya merasa berharga memiliki TLB yang didedikasikan untuk akses kode (eksekusi), dan lainnya untuk akses data. Selain itu, 80486 memiliki opcode (invlpg) yang dapat menghapus entri tertentu dari TLB.

Jadi idenya adalah sebagai berikut: membuat kedua TLB memiliki tampilan berbeda dari entri yang sama. Semua halaman ditandai dalam tabel (dalam RAM) sebagai "tidak ada", sehingga memicu pengecualian pada akses. Kernel menjebak pengecualian, dan pengecualian menyertakan beberapa data tentang jenis akses, khususnya apakah itu untuk eksekusi kode, atau tidak. Kernel kemudian membatalkan entri TLB yang baru dibaca (yang bertuliskan "absen"), kemudian mengisi entri dalam RAM dengan beberapa hak yang memungkinkan akses, lalu memaksa satu akses dari jenis yang diperlukan ( baik membaca data atau mengeksekusi kode), yang mengumpankan entri ke TLB yang sesuai, dan satunya yang itu. Kernel kemudian segera mengatur entri dalam RAM kembali ke absen, dan akhirnya kembali ke proses (kembali ke mencoba lagi opcode yang memicu pengecualian).

Efek bersihnya adalah, ketika eksekusi kembali ke kode proses, TLB untuk kode atau TLB untuk data berisi entri yang sesuai, tetapi TLB lainnya tidak, dan tidak akan karena tabel di RAM masih mengatakan "absen". Pada saat itu, kernel berada dalam posisi untuk memutuskan apakah akan mengizinkan eksekusi atau tidak, terlepas dari apakah itu memungkinkan akses data atau tidak. Dengan demikian dapat menegakkan semantik seperti NX.

Iblis menyembunyikan detailnya; dalam hal ini, ada ruang untuk seluruh legiun setan. Tarian seperti itu dengan perangkat keras tidak mudah diimplementasikan dengan benar. Terutama pada sistem multi-core.

Overhead adalah sebagai berikut: ketika akses dilakukan dan TLB tidak mengandung entri yang relevan, tabel di RAM harus diakses, dan itu saja menyiratkan kehilangan beberapa ratus siklus. Untuk itu biaya, PaX menambahkan overhead pengecualian, dan kode manajemen yang mengisi TLB yang tepat, sehingga mengubah "beberapa ratus siklus" menjadi "beberapa ribu siklus". Untungnya, kehilangan TLB benar. Orang-orang PaX mengklaim memiliki terukur perlambatan hanya 2,7% pada pekerjaan kompilasi besar (ini tergantung pada jenis CPU, meskipun).

Bit NX membuat semua ini menjadi usang. Perhatikan bahwa patchset PaX juga berisi beberapa fitur terkait keamanan lainnya, seperti ASLR, yang berlebihan dengan beberapa fungsionalitas kernel resmi yang lebih baru.

40
Thomas Pornin