
Artikel ini akan fokus pada fitur keamanan kecil yang ditambahkan di GNU ld ke rilis 2.30 pada Desember 2018. Di Rusia, peningkatan ini disebutkan di opennet dengan anotasi berikut:
Mode "-z kode terpisah", yang meningkatkan keamanan file yang dapat dieksekusi dengan biaya peningkatan kecil dalam ukuran dan konsumsi memori
Mari kita cari tahu. Untuk menjelaskan masalah keamanan seperti apa yang kita bicarakan dan solusinya, mari kita mulai dengan fitur umum dari eksploitasi kerentanan biner.
Mengeksploitasi masalah aliran kontrol
Seorang penyerang dapat mentransfer data ke program dan memanipulasi dengan cara ini dengan bantuan berbagai kerentanan: menulis dengan indeks di luar batas array, menyalin string yang tidak aman, menggunakan objek setelah rilis. Kesalahan seperti itu khas untuk kode program C dan C ++ dan dapat menyebabkan kerusakan memori dengan data input tertentu untuk program tersebut.
Kerentanan korupsi memoriCWE-20: Validasi Input Tidak Benar
CWE-118: Akses Sumber Daya Terindeks yang Salah ('Rentang Kesalahan')
CWE-119: Pembatasan Operasi yang Tidak Tepat dalam Batas-Batas Penyangga Memori
CWE-120: Buffer Copy tanpa Memeriksa Ukuran Input ('Classic Buffer Overflow')
CWE-121: Overflow Buffer Berbasis Stack
CWE-122: Buffer Overflow berbasis Heap
CWE-123: Kondisi Tulis-apa-di mana
CWE-124: Buffer Underwrite ('Buffer Underflow')
CWE-125: Baca di luar batas
CWE-126: Buffer Over-read
CWE-127: Buffer Under-read
CWE-128: Kesalahan Kelalaian
CWE-129: Validasi Indeks Array yang Tidak Tepat
CWE-130: Penanganan yang Tidak Benar Panjang Parameter Parameter
CWE-131: Perhitungan Ukuran Buffer yang Salah
CWE-134: Penggunaan String Format yang Dikontrol Secara Eksternal
CWE-135: Perhitungan Panjang String Multi-Byte yang Salah
CWE-170: Pemutusan Null Tidak Tepat
CWE-190: Integer Overflow atau Wraparound
CWE-415: Gratis Ganda
CWE-416: Gunakan Setelah Gratis
CWE-476: NULL Pointer Dereference
CWE-787: Out-of-bounds Tulis
CWE-824: Akses Pointer Tidak diinisialisasi
...
Elemen exploit klasik dari kerentanan seperti korupsi memori adalah menimpa pointer di memori. Pointer kemudian akan digunakan oleh program untuk mentransfer kontrol ke kode lain: untuk memanggil metode kelas atau fungsi dari modul lain, untuk kembali dari suatu fungsi. Dan karena pointer ditimpa, kontrol akan dicegat oleh penyerang - yaitu, kode yang disiapkan olehnya akan dieksekusi. Jika Anda tertarik dengan variasi dan detail teknik ini, kami sarankan membaca dokumen .
Momen umum dari operasi eksploitasi semacam ini diketahui, dan di sini untuk penyerang, penghalang telah lama ditempatkan:
- Memeriksa integritas pointer sebelum melewati kontrol: stack cookies, control flow guard, otentikasi pointer
- Pengacakan alamat segmen dengan kode dan data: pengacakan tata letak ruang alamat
- Mencegah kode dari mengeksekusi segmen kode luar: perlindungan ruang yang dapat dieksekusi
Selanjutnya, kita akan fokus melindungi tipe yang terakhir.
perlindungan ruang yang dapat dieksekusi
Memori program heterogen dan dibagi menjadi beberapa segmen dengan hak berbeda: membaca, menulis, dan mengeksekusi. Ini dipastikan oleh kemampuan prosesor untuk menandai halaman memori dengan bendera akses di tabel halaman. Gagasan perlindungan didasarkan pada pemisahan kode dan data yang ketat: data yang diterima dari penyerang dalam proses pemrosesan harus ditempatkan di segmen yang tidak dapat dieksekusi (stack, heap), dan kode program itu sendiri - dalam segmen yang tidak dapat diubah yang terpisah. Dengan demikian, ini harus menghilangkan kemampuan penyerang untuk menempatkan dan mengeksekusi kode asing dalam memori.
Untuk menghindari larangan eksekusi kode di segmen data, teknik penggunaan kembali kode digunakan. Yaitu, penyerang mentransfer kontrol ke fragmen kode (selanjutnya disebut sebagai gadget) yang terletak di halaman yang dapat dieksekusi. Teknik-teknik semacam ini memiliki berbagai kesulitan, dengan urutan yang meningkat:
- mentransfer kontrol ke fungsi yang melakukan apa yang cukup untuk penyerang: ke fungsi system () dengan argumen yang terkontrol untuk menjalankan perintah shell arbitrary (ret2libc)
- mentransfer kontrol ke fungsi atau rangkaian gadget yang akan menonaktifkan perlindungan atau membuat bagian dari memori dapat dieksekusi (misalnya, memanggil
mprotect()
), diikuti dengan pelaksanaan kode arbitrer - pelaksanaan semua tindakan yang diinginkan menggunakan rantai panjang gadget
Dengan demikian, penyerang dihadapkan dengan tugas menggunakan kembali kode yang ada dalam satu volume atau yang lain. Jika ini adalah sesuatu yang lebih rumit daripada kembali ke fungsi tunggal, maka rantai gadget akan diperlukan. Untuk mencari gadget berdasarkan segmen yang dapat dieksekusi, ada alat: ropper , ropgadget .
Lubang READ_IMPLIES_EXEC
Namun, kadang-kadang area memori dengan data dapat dieksekusi, dan prinsip-prinsip kode dan pemisahan data yang dijelaskan di atas jelas dilanggar. Dalam kasus seperti itu, penyerang terhindar dari kesulitan menemukan gadget atau fungsi untuk menggunakan kembali kode. Temuan yang menarik dari jenis ini adalah stack yang dapat dieksekusi dan semua segmen data pada "firewall industri" yang sama.
Listing /proc/$pid/maps
:
00008000-00009000 r-xp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00010000-00011000 rwxp 00000000 08:01 10 /var/flash/dmt/nx_test/a.out 00011000-00032000 rwxp 00000000 00:00 0 [heap] 40000000-4001f000 r-xp 00000000 1f:02 429 /lib/ld-linux.so.2 4001f000-40022000 rwxp 00000000 00:00 0 40027000-40028000 r-xp 0001f000 1f:02 429 /lib/ld-linux.so.2 40028000-40029000 rwxp 00020000 1f:02 429 /lib/ld-linux.so.2 4002c000-40172000 r-xp 00000000 1f:02 430 /lib/libc.so.6 40172000-40179000 ---p 00146000 1f:02 430 /lib/libc.so.6 40179000-4017b000 r-xp 00145000 1f:02 430 /lib/libc.so.6 4017b000-4017c000 rwxp 00147000 1f:02 430 /lib/libc.so.6 4017c000-40b80000 rwxp 00000000 00:00 0 be8c2000-be8d7000 rwxp 00000000 00:00 0 [stack]
Di sini Anda melihat kartu memori dari proses utilitas tes. Peta terdiri dari area memori - baris tabel. Pertama, perhatikan kolom kanan - ini menjelaskan isi area (segmen kode, data perpustakaan fungsi atau program itu sendiri) atau tipenya (heap, stack). Di sebelah kiri, secara berurutan, adalah rentang alamat yang ditempati setiap area memori dan, lebih jauh, bendera hak akses: r (baca), w (tulis), x (eksekusi). Bendera ini menentukan perilaku sistem ketika mencoba membaca, menulis, dan mengeksekusi memori di alamat ini. Jika mode akses yang ditunjuk dilanggar, pengecualian dilemparkan.
Perhatikan bahwa hampir semua memori di dalam proses dapat dieksekusi: stack, heap, dan semua segmen data. Ini masalah. Jelas, kehadiran halaman rwx memori akan membuat hidup lebih mudah bagi penyerang, karena ia akan dapat dengan bebas mengeksekusi kodenya dalam proses semacam itu di setiap tempat di mana kodenya dapatkan ketika mentransfer data (paket, file) ke program seperti itu untuk diproses.
Mengapa situasi seperti itu muncul pada perangkat modern yang mendukung larangan eksekusi kode pada halaman data dengan perangkat keras, apakah keamanan jaringan perusahaan dan industri bergantung pada perangkat, dan masalah yang terdengar serta solusinya telah dikenal sejak lama?
Gambar ini ditentukan oleh perilaku kernel selama inisialisasi proses (mengalokasikan stack, heap, memuat ELF utama, dll.) Dan selama pelaksanaan panggilan proses nuklir. Atribut kunci yang memengaruhi ini adalah bendera kepribadian READ_IMPLIES_EXEC
. Efek dari flag ini adalah semua memori yang dapat dibaca juga dapat dieksekusi. Bendera dapat diatur ke proses Anda karena beberapa alasan:
- Legacy dapat secara eksplisit diminta oleh flag perangkat lunak di header ELF untuk menerapkan mekanisme yang sangat menarik: loncatan pada tumpukan ( 1 , 2 , 3 )!
- Itu bisa diwarisi oleh proses anak dari orang tua.
- Itu dapat diinstal oleh kernel secara independen untuk semua proses! Pertama, jika arsitektur tidak mendukung memori yang tidak dapat dieksekusi. Kedua, untuk berjaga-jaga, untuk mendukung beberapa kruk kuno lainnya. Kode ini ada di kernel 2.6.32 (ARM), yang memiliki umur yang sangat panjang. Ini hanya kasus kami.
Ruang untuk menemukan gadget dalam gambar ELF
Fungsi pustaka dan program yang dapat dieksekusi dalam format ELF. Kompiler gcc menerjemahkan konstruksi bahasa ke dalam kode mesin dan meletakkannya di satu bagian, dan data yang kode ini beroperasi di bagian lain. Ada banyak bagian dan mereka dikelompokkan oleh ld linker ke dalam segmen-segmen. Dengan demikian, ELF berisi gambar program yang memiliki dua representasi: tabel bagian dan tabel segmen.
$ readelf -l /bin/ls Elf file type is EXEC (Executable file) Entry point 0x804bee9 There are 9 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 RE 0x4 INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x08048000 0x08048000 0x1e40c 0x1e40c RE 0x1000 LOAD 0x01ef00 0x08067f00 0x08067f00 0x00444 0x01078 RW 0x1000 DYNAMIC 0x01ef0c 0x08067f0c 0x08067f0c 0x000f0 0x000f0 RW 0x4 NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x018b74 0x08060b74 0x08060b74 0x00814 0x00814 R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x01ef00 0x08067f00 0x08067f00 0x00100 0x00100 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 04 .dynamic 05 .note.ABI-tag .note.gnu.build-id 06 .eh_frame_hdr 07 08 .init_array .fini_array .jcr .dynamic .got
Di sini Anda melihat pemetaan bagian ke segmen dalam gambar ELF.
Tabel bagian digunakan oleh utilitas untuk menganalisis program dan perpustakaan, tetapi tidak digunakan oleh loader untuk memproyeksikan ELF ke dalam memori proses. Tabel bagian menjelaskan struktur ELF lebih detail daripada tabel segmen. Beberapa bagian dapat berada dalam satu segmen.
Gambar ELF dalam memori dibuat oleh loader ELF berdasarkan pada isi tabel segmen . Tabel partisi tidak lagi digunakan untuk memuat ELF ke dalam memori.
Tetapi ada pengecualian untuk aturan ini.Misalnya, di alam, ada tambalan pengembang Debian untuk loader ELF ld.so untuk arsitektur ARM, yang mencari bagian ".ARM.attributes" khusus seperti SHT_ARM_ATTRIBUTES dan biner dengan tabel bagian yang digergaji dalam sistem tersebut tidak dimuat ...
Segmen ELF memiliki bendera yang menentukan izin apa yang dimiliki segmen tersebut dalam memori. Secara tradisional, sebagian besar perangkat lunak untuk GNU / Linux diatur sedemikian rupa sehingga dua segmen PT_LOAD
(loadable dalam memori) dinyatakan dalam tabel segmen - seperti dalam daftar di atas:
Segmen dengan bendera RE
1.1. Kode eksekusi ELF: bagian .init
, .text
, .fini
1.2. Data yang tidak dapat diubah dalam ELF: .symtab
, .rodata
Segmen Bendera RW
2.1. Data variabel dalam ELF: bagian .plt
, .got
, .bss
, .bss
Jika Anda memperhatikan komposisi segmen pertama dan flag aksesnya, menjadi jelas bahwa tata letak seperti itu memperluas ruang untuk mencari gadget untuk teknik penggunaan kembali kode. Dalam ELF besar seperti libcrypto, tabel layanan dan data abadi lainnya dapat menempati hingga 40% dari segmen yang dapat dieksekusi . Kehadiran sesuatu yang mirip dengan potongan kode dalam data ini dikonfirmasi oleh upaya untuk membongkar file biner tersebut dengan sejumlah besar data dalam segmen yang dapat dieksekusi tanpa tabel bagian dan simbol. Setiap urutan byte dalam segmen yang dapat dieksekusi tunggal ini dapat dianggap berguna untuk fragmen serang kode mesin dan loncatan - menjadi urutan byte ini dengan setidaknya sepotong garis pesan debugging dari program, bagian dari nama fungsi dalam tabel simbol atau jumlah konstan algoritma kriptografi ...
Header Eksekusi PEHeader dan tabel yang dapat dieksekusi pada awal segmen pertama dari gambar ELF menyerupai situasi dengan Windows sekitar 15 tahun yang lalu. Ada sejumlah virus yang menginfeksi file, menulis kode mereka di header PE mereka, yang juga dapat dieksekusi di sana. Saya berhasil menggali sampel seperti itu di arsip:

Seperti yang Anda lihat, tubuh virus diperas tepat setelah tabel bagian di area header PE. Dalam proyeksi file ke memori virtual, biasanya ada sekitar 3KB ruang kosong di sini. Setelah tubuh virus ada ruang kosong dan kemudian bagian pertama dimulai dengan kode program.
Namun, untuk Linux ada banyak karya yang lebih menarik dari adegan VX: Pembalasan .
Solusi
- Masalah yang dijelaskan di atas sudah lama diketahui.
- Tetap 12 Januari 2018 : kunci `ld -z kode-terpisah:" Buat kode terpisah "header segmen PT_LOAD dalam objek ditambahkan. Ini menentukan segmen memori yang harus berisi hanya instruksi dan harus sepenuhnya memisahkan halaman dari data lain. Jangan membuat segmen kode "PT_LOAD" terpisah jika kode noseparate digunakan. "). Fitur ini dirilis pada rilis 2.30 .
- Selanjutnya, fitur ini dimasukkan secara default di rilis berikutnya 2.31 .
- Hadir dalam paket
binutils
baru, misalnya, di repositori Ubuntu 18.10. Banyak paket telah dikumpulkan dengan fitur baru ini, yang ditemui dan didokumentasikan oleh peneliti ElfMaster
Sebagai hasil dari perubahan pada algoritma tata letak, gambar ELF baru diperoleh:
$ readelf -l ls Elf file type is DYN (Shared object file) Entry point 0x41aa There are 11 program headers, starting at offset 52 Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align PHDR 0x000034 0x00000034 0x00000034 0x00160 0x00160 R 0x4 INTERP 0x000194 0x00000194 0x00000194 0x00013 0x00013 R 0x1 [Requesting program interpreter: /lib/ld-linux.so.2] LOAD 0x000000 0x00000000 0x00000000 0x01e6c 0x01e6c R 0x1000 LOAD 0x002000 0x00002000 0x00002000 0x14bd8 0x14bd8 RE 0x1000 LOAD 0x017000 0x00017000 0x00017000 0x0bf80 0x0bf80 R 0x1000 LOAD 0x0237f8 0x000247f8 0x000247f8 0x0096c 0x01afc RW 0x1000 DYNAMIC 0x023cec 0x00024cec 0x00024cec 0x00100 0x00100 RW 0x4 NOTE 0x0001a8 0x000001a8 0x000001a8 0x00044 0x00044 R 0x4 GNU_EH_FRAME 0x01c3f8 0x0001c3f8 0x0001c3f8 0x0092c 0x0092c R 0x4 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 GNU_RELRO 0x0237f8 0x000247f8 0x000247f8 0x00808 0x00808 R 0x1 Section to Segment mapping: Segment Sections... 00 01 .interp 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt 03 .init .plt .plt.got .text .fini 04 .rodata .eh_frame_hdr .eh_frame 05 .init_array .fini_array .data.rel.ro .dynamic .got .data .bss 06 .dynamic 07 .note.ABI-tag .note.gnu.build-id 08 .eh_frame_hdr 09 10 .init_array .fini_array .data.rel.ro .dynamic .got
Batas antara kode dan data sekarang lebih akurat. Segmen yang hanya dapat dieksekusi benar-benar hanya berisi bagian kode: .init, .plt, .plt.got, .text, .fini.
Apa sebenarnya yang telah diubah di dalam ld?Seperti yang Anda ketahui, struktur file output ELF dijelaskan oleh skrip linker . Anda dapat melihat skrip default seperti ini:
$ ld --verbose GNU ld (GNU Binutils for Ubuntu) 2.26.1 * * * using internal linker script: ================================================== /* Script for -z combreloc: combine and sort reloc sections */ /* Copyright (C) 2014-2015 Free Software Foundation, Inc. * * *
Banyak skrip lain untuk berbagai platform dan kombinasi opsi terdapat di direktori ldscripts
. Script baru telah dibuat untuk opsi separate-code
.
$ diff elf_x86_64.x elf_x86_64.xe 1c1 < /* Default linker script, for normal executables */ --- > /* Script for -z separate-code: generate normal executables with separate code segment */ 46a47 > . = ALIGN(CONSTANT (MAXPAGESIZE)); 70a72,75 > . = ALIGN(CONSTANT (MAXPAGESIZE)); > /* Adjust the address for the rodata segment. We want to adjust up to > the same address within the page on the next page up. */ > . = SEGMENT_START("rodata-segment", ALIGN(CONSTANT (MAXPAGESIZE)) + (. & (CONSTANT (MAXPAGESIZE) - 1)));
Di sini Anda dapat melihat bahwa direktif telah ditambahkan untuk mendeklarasikan segmen baru dengan bagian read-only mengikuti segmen kode.
Namun, selain skrip, perubahan dilakukan pada sumber tautan. Yaitu, dalam fungsi _bfd_elf_map_sections_to_segments
- lihat komit . Sekarang, ketika memilih segmen untuk bagian, segmen baru akan ditambahkan ketika bagian berbeda dengan bendera SEC_CODE
dari bagian sebelumnya.
Kesimpulan
Seperti sebelumnya , kami menyarankan agar pengembang tidak lupa menggunakan bendera keamanan yang dibangun di kompiler dan tautan saat mengembangkan perangkat lunak. Perubahan kecil seperti itu bisa sangat menyulitkan kehidupan penyerang, dan membuat hidup Anda jauh lebih tenang.