Kodu nasıl değiştirecek, örneğin işlev çağrıları?
Yanıtlar:
PIE, yürütülebilir dosyalarda adres alanı düzenini (ASLR) desteklemektir .
PIE modu yaratılmadan önce, programın yürütülebilir dosyası belleğe rastgele bir adrese yerleştirilemezdi, yalnızca konumdan bağımsız kod (PIC) dinamik kitaplıkları rastgele bir ofsete yeniden yerleştirilebilirdi. Dinamik kitaplıklar için PIC'in yaptığı gibi çalışır, fark, Prosedür Bağlantı Tablosunun (PLT) yaratılmaması, bunun yerine PC'ye göre yeniden konumlandırmanın kullanılmasıdır.
Gcc / linkers'da PIE desteğini etkinleştirdikten sonra, programın gövdesi derlenir ve konumdan bağımsız kod olarak bağlanır. Dinamik bir bağlayıcı, tıpkı dinamik kitaplıklar gibi program modülünde tam yer değiştirme işlemi yapar. Global verilerin herhangi bir kullanımı, Global Offsets Table (GOT) aracılığıyla erişime dönüştürülür ve GOT yer değiştirmeleri eklenir.
PIE, bu OpenBSD PIE sunumunda iyi tanımlanmıştır .
İşlevlerdeki değişiklikler bu slaytta gösterilmektedir (PIE - PIC).
x86 pic vs turta
Yerel global değişkenler ve işlevler pastada optimize edilmiştir
Harici global değişkenler ve fonksiyonlar pic ile aynıdır
ve bu slaytta (PIE ve eski stil bağlantı)
x86 pastası vs işaretsiz (düzeltildi)
Yerel global değişkenler ve işlevler, sabit
Harici global değişkenler ve fonksiyonlar pic ile aynıdır
PIE'nin aşağıdakilerle uyumsuz olabileceğini unutmayın: -static
Minimum çalıştırılabilir örnek: GDB çalıştırılabilir iki kez
Biraz eylem görmek isteyenler için, ASLR'nin PIE çalıştırılabilir üzerinde çalıştığını ve çalıştırmalar arasında adresleri değiştirdiğini görelim:
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
Olan biri için -no-pie
her şey sıkıcı:
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
Yürütmeye başlamadan önce, break main
bir kesme noktası belirler 0x401126
.
Daha sonra her iki yürütme sırasında run
adreste durur 0x401126
.
İle bir -pie
yandan çok daha ilginç:
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
Yürütme başlamadan önce, GDB sadece çalıştırılabilir bulunan bir "kukla" adresi alır: 0x1139
.
Ancak başladıktan sonra GDB akıllıca dinamik yükleyicinin programı farklı bir konuma yerleştirdiğini ve ilk molanın burada durduğunu fark eder 0x5630df2d6139
.
Ardından, ikinci çalıştırma da çalıştırılabilir dosyanın tekrar hareket ettiğini akıllıca fark etti ve bozulma noktasına geldi 0x55763ab2e139
.
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
ASLR'nin açık olmasını sağlar (Ubuntu 17.10'da varsayılan): ASLR'yi geçici olarak nasıl devre dışı bırakabilirim (Adres alanı düzeni randomizasyonu)? | Ubuntu'ya sorun .
set disable-randomization off
aksi takdirde GDB, adından da anlaşılacağı gibi, hata ayıklama deneyimini iyileştirmek için çalıştırmalar arasında sabit adresler vermek üzere varsayılan olarak işlem için ASLR'yi kapatır: gdb adresleri ve "gerçek" adresler arasındaki fark? | Yığın Taşması .
readelf
analiz
Ayrıca şunu da gözlemleyebiliriz:
readelf -s ./no-pie.out | grep main
gerçek çalışma zamanı yükleme adresini verir (pc aşağıdaki talimatı 4 bayt sonra gösterir):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
süre:
readelf -s ./pie.out | grep main
sadece bir ofset verir:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
ASLR'yi kapatarak ( randomize_va_space
veya ile set disable-randomization off
), GDB her zaman main
adresi verir :, 0x5555555547a9
bu nedenle -pie
adresin şunlardan oluştuğunu anlıyoruz :
0x555555554000 + random offset + symbol offset (79a)
TODO Linux çekirdeğinde / glibc yükleyicide / nerede olursa olsun 0x555555554000 sabit kodludur? Linux'ta bir PIE yürütülebilir dosyasının metin bölümünün adresi nasıl belirlenir?
Minimal montaj örneği
Yapabileceğimiz bir başka harika şey de, PIE'nin ne anlama geldiğini daha somut bir şekilde anlamak için bazı montaj kodlarıyla oynamaktır.
Bunu bir Linux x86_64 bağımsız montaj merhaba dünya ile yapabiliriz:
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
ve şunlarla birleştirilir ve iyi çalışır:
as -o main.o main.S
ld -o main.out main.o
./main.out
Bununla birlikte, onu PIE olarak bağlamaya çalışırsak ( --no-dynamic-linker
şu sayfada açıklandığı gibi gereklidir: Linux'ta statik olarak bağlantılı bir konum bağımsız çalıştırılabilir ELF nasıl oluşturulur? )
ld --no-dynamic-linker -pie -o main.out main.o
bağlantı şununla başarısız olur:
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
Çünkü satır:
mov $msg, %rsi /* buffer */
mov
işlenen içindeki mesaj adresini sabit kodlar ve bu nedenle konumdan bağımsız değildir.
Bunun yerine bağımsız bir şekilde yazarsak:
lea msg(%rip), %rsi
PIE bağlantısı düzgün çalışıyor ve GDB bize yürütülebilir dosyanın her seferinde bellekte farklı bir konuma yüklendiğini gösteriyor.
Buradaki fark , sözdizimi nedeniyle geçerli PC adresine göre lea
adresin kodlanmasıdır, ayrıca bkz: 64 bitlik bir derleme programında RIP Göreli Adresleme nasıl kullanılır?msg
rip
Bunu, her iki sürümü de aşağıdakilerle sökerek de anlayabiliriz:
objdump -S main.o
sırasıyla verir:
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
Dolayısıyla , şu anki adres + 0x19 olarak kodlanmış lea
tam doğru adresin zaten olduğunu açıkça görüyoruz msg
.
Ancak mov
sürüm adresi olarak ayarlamıştır 00 00 00 00
, bu da orada bir yeniden konumlandırma yapılacağı anlamına gelir: Bağlayıcılar ne yapar? Şifreli R_X86_64_32S
içinde ld
hata mesajı gerekti ve hangi PIE yürütülebilir içinde olamaz tehcir gerçek türüdür.
Yapabileceğimiz bir başka eğlenceli şey de, aşağıdakiler msg
yerine veri bölümüne koymaktır .text
:
.data
msg:
.ascii "hello\n"
len = . - msg
Şimdi .o
montajlar:
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
bu yüzden RIP ofseti şimdi 0
ve montajcı tarafından bir yeniden konumlandırma talep edildiğini tahmin ediyoruz. Bunu şu şekilde onaylıyoruz:
readelf -r main.o
hangi verir:
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
Bu nedenle R_X86_64_PC32
, ld
PIE çalıştırılabilirlerini idare edebilen bir PC'ye göre yeniden konumlandırmadır .
Bu deney bize, bağlayıcının programın PIE olup olmadığını kontrol ettiğini ve bu şekilde işaretlediğini öğretti.
Daha sonra GCC ile derlerken, GCC'ye -pie
konumdan bağımsız derleme oluşturmasını söyler.
Ancak montajı kendimiz yazarsak, pozisyon bağımsızlığına ulaştığımızdan manuel olarak emin olmalıyız.
ARMv8 aarch64'te, pozisyondan bağımsız merhaba dünya ADR talimatı ile elde edilebilir .
Bir ELF'nin konumdan bağımsız olup olmadığı nasıl belirlenir?
Sadece GDB üzerinden çalıştırmanın yanı sıra, bazı statik yöntemlerden de bahsedilmektedir:
Ubuntu 18.10'da test edilmiştir.