Apa opsi -fPIE untuk executable independen posisi di gcc dan ld?

Jawaban:

100

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

osgx
sumber
3
Juga di wikipedia: en.wikipedia.org/wiki/…
osgx
5
Mengapa -pie dan -static kompatibel di ARM dan TIDAK kompatibel di x86? Pertanyaan SO saya: stackoverflow.com/questions/27082959/…
4ntoine
56

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 mainsetel breakpoint pada 0x401126.

Kemudian, selama kedua eksekusi, runberhenti di alamat 0x401126.

Namun yang satu -piejauh 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_spacememastikan bahwa ASLR aktif (default di Ubuntu 17.10): Bagaimana saya dapat menonaktifkan sementara ASLR (Address space layout randomization)? | Tanya Ubuntu .

set disable-randomization offdiperlukan 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_spaceatau set disable-randomization off), GDB selalu memberikan mainalamat 0x5555555547a9:, jadi kami menyimpulkan bahwa -piealamat 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

GitHub upstream

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-linkerdiperlukan 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 movoperan, 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 leamenyandikan alamat msgrelatif ke alamat PC saat ini karena ripsintaksnya, 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 leasudah memiliki alamat lengkap yang benar msgdikodekan sebagai alamat saat ini + 0x19.

The movVersi 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_32Sdalam ldpesan kesalahan adalah jenis relokasi sebenarnya yang diperlukan dan yang tidak dapat terjadi di executable PIE.

Hal menyenangkan lainnya yang dapat kita lakukan adalah meletakkan msgdi bagian data alih-alih .textdengan:

.data
msg:
    .ascii "hello\n"
len = . - msg

Sekarang .oberkumpul 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_PC32adalah relokasi relatif PC yang lddapat 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, -piememberi 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.

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
sumber
1
Hai Ciro! Dapatkah Anda membuat pertanyaan terpisah untuk alamat awal ASLR-off pie-on dan menautkannya di sini?
osgx
1
@osgx Selesai. Apakah Anda sudah tahu atau akan menggalinya dengan cepat? :-) Saat Anda melakukannya, akan sangat bagus untuk menjelaskan bagaimana kernel / dyn loader Linux menentukan apakah sesuatu memiliki PIE atau tidak: unix.stackexchange.com/questions/89211/…
Ciro Santilli 郝海东 冠状 病 六四 事件法轮功
Saya belum tahu, tetapi saya tahu bahwa ini harus digali dari rtld dari glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (jika interpreter masih ld-linux.so). Tiga tahun yang lalu Basile tidak yakin tentang 0x55555555 juga stackoverflow.com/questions/29856044 , tetapi pertanyaan itu adalah tentang memulai alamat ld.so itu sendiri, jadi gali ke fs / binfmt_elf.c kernel atau skrip readelf / objdump dan linker.
osgx