Bagaimana cara mengubah kode, misalnya panggilan fungsi?
Jawaban:
PIE mendukung pengacakan tata letak ruang alamat (ASLR) dalam file yang dapat dieksekusi.
Sebelum mode PIE dibuat, program yang dapat dieksekusi tidak dapat ditempatkan di alamat acak dalam memori, hanya pustaka dinamis kode independen posisi (PIC) yang dapat dipindahkan ke offset acak. Ia bekerja sangat mirip dengan apa yang dilakukan PIC untuk perpustakaan dinamis, perbedaannya adalah bahwa Tabel Tautan Prosedur (PLT) tidak dibuat, melainkan relokasi relatif-PC digunakan.
Setelah mengaktifkan dukungan PIE di gcc / linker, badan program dikompilasi dan ditautkan sebagai kode yang tidak bergantung pada posisi. Linker dinamis melakukan pemrosesan relokasi penuh pada modul program, seperti pustaka dinamis. Setiap penggunaan data global diubah menjadi akses melalui Tabel Offset Global (GOT) dan relokasi GOT ditambahkan.
PIE dijelaskan dengan baik dalam presentasi OpenBSD PIE ini .
Perubahan fungsi ditampilkan dalam slide ini (PIE vs PIC).
gambar x86 vs pai
Variabel global dan fungsi lokal dioptimalkan secara bersamaan
Variabel dan fungsi global eksternal sama seperti pic
dan di slide ini (PIE vs penautan gaya lama)
x86 pie vs no-flags (tetap)
Variabel dan fungsi global lokal mirip dengan tetap
Variabel dan fungsi global eksternal sama seperti pic
Perhatikan, bahwa PIE mungkin tidak kompatibel dengan -static
Contoh minimal runnable: GDB dapat dieksekusi dua kali
Bagi mereka yang ingin melihat beberapa tindakan, mari kita lihat ASLR bekerja pada PIE yang dapat dieksekusi dan mengubah alamat di seluruh proses:
main.c
#include <stdio.h>
int main(void) {
puts("hello");
}
main.sh
#!/usr/bin/env bash
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
for pie in no-pie pie; do
exe="${pie}.out"
gcc -O0 -std=c99 "-${pie}" "-f${pie}" -ggdb3 -o "$exe" main.c
gdb -batch -nh \
-ex 'set disable-randomization off' \
-ex 'break main' \
-ex 'run' \
-ex 'printf "pc = 0x%llx\n", (long long unsigned)$pc' \
-ex 'run' \
-ex 'printf "pc = 0x%llx\n", (long long unsigned)$pc' \
"./$exe" \
;
echo
echo
done
Untuk yang bersama -no-pie
, semuanya membosankan:
Breakpoint 1 at 0x401126: file main.c, line 4.
Breakpoint 1, main () at main.c:4
4 puts("hello");
pc = 0x401126
Breakpoint 1, main () at main.c:4
4 puts("hello");
pc = 0x401126
Sebelum memulai eksekusi, break main
setel breakpoint pada 0x401126
.
Kemudian, selama kedua eksekusi, run
berhenti di alamat 0x401126
.
Namun yang satu -pie
jauh lebih menarik:
Breakpoint 1 at 0x1139: file main.c, line 4.
Breakpoint 1, main () at main.c:4
4 puts("hello");
pc = 0x5630df2d6139
Breakpoint 1, main () at main.c:4
4 puts("hello");
pc = 0x55763ab2e139
Sebelum memulai eksekusi, GDB hanya membutuhkan "boneka" alamat yang hadir dalam eksekusi: 0x1139
.
Namun, setelah dimulai, GDB dengan cerdas memperhatikan bahwa pemuat dinamis menempatkan program di lokasi yang berbeda, dan pemutusan pertama berhenti di 0x5630df2d6139
.
Kemudian, proses kedua juga dengan cerdas memperhatikan bahwa eksekusi bergerak lagi, dan akhirnya berhenti 0x55763ab2e139
.
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
memastikan bahwa ASLR aktif (default di Ubuntu 17.10): Bagaimana saya dapat menonaktifkan sementara ASLR (Address space layout randomization)? | Tanya Ubuntu .
set disable-randomization off
diperlukan jika tidak GDB, seperti namanya, menonaktifkan ASLR untuk proses secara default untuk memberikan alamat tetap di seluruh proses untuk meningkatkan pengalaman debugging: Perbedaan antara alamat gdb dan alamat "asli"? | Stack Overflow .
readelf
analisis
Selain itu, kami juga dapat mengamati bahwa:
readelf -s ./no-pie.out | grep main
memberikan alamat pemuatan runtime aktual (pc menunjuk ke instruksi berikut 4 byte setelah):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
sementara:
readelf -s ./pie.out | grep main
hanya memberikan offset:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
Dengan mematikan ASLR (dengan salah satu randomize_va_space
atau set disable-randomization off
), GDB selalu memberikan main
alamat 0x5555555547a9
:, jadi kami menyimpulkan bahwa -pie
alamat tersebut terdiri dari:
0x555555554000 + random offset + symbol offset (79a)
TODO di mana 0x555555554000 hard code di kernel Linux / glibc loader / di mana saja? Bagaimana alamat bagian teks dari PIE yang dapat dieksekusi ditentukan di Linux?
Contoh perakitan minimal
Hal keren lainnya yang dapat kita lakukan adalah bermain-main dengan beberapa kode assembly untuk memahami lebih konkret apa arti PIE.
Kita dapat melakukannya dengan rakitan berdiri bebas Linux x86_64 hello world:
main.S
.text
.global _start
_start:
asm_main_after_prologue:
/* write */
mov $1, %rax /* syscall number */
mov $1, %rdi /* stdout */
mov $msg, %rsi /* buffer */
mov $len, %rdx /* len */
syscall
/* exit */
mov $60, %rax /* syscall number */
mov $0, %rdi /* exit status */
syscall
msg:
.ascii "hello\n"
len = . - msg
dan itu berkumpul dan bekerja dengan baik dengan:
as -o main.o main.S
ld -o main.out main.o
./main.out
Namun, jika kami mencoba menautkannya sebagai PIE dengan ( --no-dynamic-linker
diperlukan seperti yang dijelaskan di: Cara membuat ELF independen yang dapat dieksekusi secara independen di Linux? ):
ld --no-dynamic-linker -pie -o main.out main.o
maka tautan akan gagal dengan:
ld: main.o: relocation R_X86_64_32S against `.text' can not be used when making a PIE object; recompile with -fPIC
ld: final link failed: nonrepresentable section on output
Karena garis:
mov $msg, %rsi /* buffer */
hardcode alamat pesan di mov
operan, dan karena itu tidak independen posisi.
Jika kita malah menulisnya dalam posisi independen:
lea msg(%rip), %rsi
lalu tautan PIE berfungsi dengan baik, dan GDB menunjukkan kepada kita bahwa file yang dapat dieksekusi dimuat di lokasi berbeda dalam memori setiap saat.
Perbedaannya di sini adalah bahwa lea
menyandikan alamat msg
relatif ke alamat PC saat ini karena rip
sintaksnya, lihat juga: Bagaimana cara menggunakan RIP Relative Addressing dalam program perakitan 64-bit?
Kami juga dapat mengetahuinya dengan membongkar kedua versi dengan:
objdump -S main.o
yang memberi masing-masing:
e: 48 c7 c6 00 00 00 00 mov $0x0,%rsi
e: 48 8d 35 19 00 00 00 lea 0x19(%rip),%rsi # 2e <msg>
000000000000002e <msg>:
2e: 68 65 6c 6c 6f pushq $0x6f6c6c65
Jadi kami melihat dengan jelas bahwa lea
sudah memiliki alamat lengkap yang benar msg
dikodekan sebagai alamat saat ini + 0x19.
The mov
Versi namun telah menetapkan alamat ke 00 00 00 00
, yang berarti bahwa relokasi akan dilakukan di sana: Apa yang linker lakukan? Cryptic R_X86_64_32S
dalam ld
pesan kesalahan adalah jenis relokasi sebenarnya yang diperlukan dan yang tidak dapat terjadi di executable PIE.
Hal menyenangkan lainnya yang dapat kita lakukan adalah meletakkan msg
di bagian data alih-alih .text
dengan:
.data
msg:
.ascii "hello\n"
len = . - msg
Sekarang .o
berkumpul untuk:
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
jadi offset RIP sekarang 0
, dan kami menduga relokasi telah diminta oleh assembler. Kami mengonfirmasi hal itu dengan:
readelf -r main.o
pemberian yang mana:
Relocation section '.rela.text' at offset 0x160 contains 1 entry:
Offset Info Type Sym. Value Sym. Name + Addend
000000000011 000200000002 R_X86_64_PC32 0000000000000000 .data - 4
jadi jelas R_X86_64_PC32
adalah relokasi relatif PC yang ld
dapat menangani executable PIE.
Eksperimen ini mengajarkan kita bahwa linker itu sendiri memeriksa program tersebut dapat menjadi PIE dan menandainya seperti itu.
Kemudian saat mengompilasi dengan GCC, -pie
memberi tahu GCC untuk menghasilkan perakitan independen posisi.
Tetapi jika kita menulis perakitan sendiri, kita harus secara manual memastikan bahwa kita telah mencapai independensi posisi.
Di ARMv8 aarch64, posisi hello world yang independen dapat dicapai dengan instruksi ADR .
Bagaimana cara menentukan apakah ELF adalah posisi independen?
Selain hanya menjalankannya melalui GDB, beberapa metode statis disebutkan di:
Diuji di Ubuntu 18.10.