Nó sẽ thay đổi mã như thế nào, ví dụ như các lệnh gọi hàm?
Câu trả lời:
PIE hỗ trợ ngẫu nhiên hóa bố cục không gian địa chỉ (ASLR) trong các tệp thực thi.
Trước khi chế độ PIE được tạo, tệp thực thi của chương trình không thể được đặt tại một địa chỉ ngẫu nhiên trong bộ nhớ, chỉ các thư viện động mã độc lập về vị trí (PIC) mới có thể được di chuyển đến một điểm bù ngẫu nhiên. Nó hoạt động rất giống với những gì PIC làm cho các thư viện động, sự khác biệt là một Bảng liên kết thủ tục (PLT) không được tạo, thay vào đó việc di chuyển tương đối PC được sử dụng.
Sau khi bật hỗ trợ PIE trong gcc / linkers, phần nội dung của chương trình được biên dịch và liên kết dưới dạng mã độc lập với vị trí. Trình liên kết động thực hiện xử lý tái định cư đầy đủ trên mô-đun chương trình, giống như các thư viện động. Mọi việc sử dụng dữ liệu toàn cầu đều được chuyển đổi thành quyền truy cập thông qua Bảng giá trị toàn cầu (GOT) và các vị trí GOT được thêm vào.
PIE được mô tả rõ ràng trong bài thuyết trình OpenBSD PIE này .
Các thay đổi đối với các chức năng được hiển thị trong slide này (PIE so với PIC).
x86 pic vs pie
Các biến và hàm toàn cục cục bộ được tối ưu hóa trong bánh
Các biến và hàm toàn cục bên ngoài giống như pic
và trong trang trình bày này (PIE so với liên kết kiểu cũ)
x86 pie vs no-flags (cố định)
Các biến và hàm toàn cục cục bộ tương tự như các biến cố định
Các biến và hàm toàn cục bên ngoài giống như pic
Lưu ý rằng PIE có thể không tương thích với -static
Ví dụ có thể chạy tối thiểu: GDB tệp thực thi hai lần
Đối với những người muốn xem một số hành động, hãy xem ASLR hoạt động trên tệp thực thi PIE và thay đổi địa chỉ qua các lần chạy:
C chính
#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
Đối với người có -no-pie
, mọi thứ đều nhàm chán:
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
Trước khi bắt đầu thực hiện, hãy break main
đặt điểm ngắt tại 0x401126
.
Sau đó, trong cả hai lần thực thi, run
dừng lại ở địa chỉ 0x401126
.
-pie
Tuy nhiên, một trong những thú vị hơn nhiều:
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
Trước khi bắt đầu thực hiện, GDB chỉ mất một địa chỉ "giả" đó là hiện diện trong thực thi: 0x1139
.
Tuy nhiên, sau khi bắt đầu, GDB thông báo một cách thông minh rằng trình tải động đã đặt chương trình ở một vị trí khác và lần ngắt đầu tiên dừng lại ở 0x5630df2d6139
.
Sau đó, lần chạy thứ hai cũng nhận thấy một cách thông minh rằng tệp thực thi lại di chuyển và cuối cùng bị phá vỡ 0x55763ab2e139
.
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
đảm bảo rằng ASLR đang bật (mặc định trong Ubuntu 17.10): Làm cách nào để tôi có thể tạm thời vô hiệu hóa ASLR (ngẫu nhiên hóa bố cục không gian địa chỉ)? | Hỏi Ubuntu .
set disable-randomization off
Nếu không, GDB là cần thiết, như tên cho thấy, theo mặc định, tắt ASLR cho quá trình để cung cấp các địa chỉ cố định trong các lần chạy để cải thiện trải nghiệm gỡ lỗi: Sự khác biệt giữa địa chỉ gdb và địa chỉ "thực"? | Tràn ngăn xếp .
readelf
phân tích
Hơn nữa, chúng ta cũng có thể quan sát thấy rằng:
readelf -s ./no-pie.out | grep main
cung cấp địa chỉ tải thời gian chạy thực tế (pc trỏ đến lệnh sau 4 byte sau):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
trong khi:
readelf -s ./pie.out | grep main
chỉ cung cấp một phần bù:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
Bằng cách tắt ASLR (với một trong hai randomize_va_space
hoặc set disable-randomization off
), GDB luôn cung cấp main
địa chỉ 0x5555555547a9
:, vì vậy chúng tôi suy ra rằng -pie
địa chỉ được tạo từ:
0x555555554000 + random offset + symbol offset (79a)
CẦN LÀM ở đâu được mã hóa cứng 0x555555554000 trong nhân Linux / bộ tải glibc / ở đâu? Địa chỉ của phần văn bản của tệp thực thi PIE được xác định như thế nào trong Linux?
Ví dụ lắp ráp tối thiểu
Một điều thú vị khác mà chúng ta có thể làm là thử với một số mã lắp ráp để hiểu cụ thể hơn ý nghĩa của PIE.
Chúng tôi có thể làm điều đó với một hợp ngữ rời Linux x86_64 hello world:
chính.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
và nó lắp ráp và chạy tốt với:
as -o main.o main.S
ld -o main.out main.o
./main.out
Tuy nhiên, nếu chúng tôi cố gắng liên kết nó dưới dạng PIE với ( --no-dynamic-linker
được yêu cầu như được giải thích tại: Cách tạo ELF thực thi độc lập vị trí liên kết tĩnh trong Linux? ):
ld --no-dynamic-linker -pie -o main.out main.o
thì liên kết sẽ không thành công với:
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
Vì dòng:
mov $msg, %rsi /* buffer */
mã hóa cứng địa chỉ tin nhắn trong mov
toán hạng và do đó không độc lập về vị trí.
Nếu thay vào đó chúng ta viết nó theo một cách độc lập về vị trí:
lea msg(%rip), %rsi
thì liên kết PIE hoạt động tốt và GDB cho chúng ta thấy rằng tệp thực thi luôn được tải ở một vị trí khác trong bộ nhớ.
Sự khác biệt ở đây là được lea
mã hóa địa chỉ msg
tương đối với địa chỉ PC hiện tại do rip
cú pháp, xem thêm: Làm thế nào để sử dụng Địa chỉ tương đối RIP trong chương trình hợp ngữ 64 bit?
Chúng tôi cũng có thể tìm ra điều đó bằng cách tháo rời cả hai phiên bản với:
objdump -S main.o
cung cấp cho tương ứng:
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
Vì vậy, chúng tôi thấy rõ ràng rằng lea
đã có địa chỉ chính xác đầy đủ msg
được mã hóa là địa chỉ hiện tại + 0x19.
Các mov
tuy nhiên phiên bản đã thiết lập các địa chỉ để 00 00 00 00
, có nghĩa là một di dời sẽ được thực hiện có: làm linkers làm gì? Điều khó hiểu R_X86_64_32S
trong thông ld
báo lỗi là kiểu di dời thực tế được yêu cầu và không thể xảy ra trong tệp thực thi PIE.
Một điều thú vị khác mà chúng ta có thể làm là đưa msg
phần dữ liệu vào phần dữ liệu thay vì .text
bằng:
.data
msg:
.ascii "hello\n"
len = . - msg
Bây giờ các .o
tập hợp để:
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
vì vậy phần bù RIP bây giờ là 0
, và chúng tôi đoán rằng trình lắp ráp đã yêu cầu chuyển vị trí. Chúng tôi xác nhận điều đó với:
readelf -r main.o
mang lại:
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
rõ ràng R_X86_64_PC32
là một sự di dời tương đối của PC ld
có thể xử lý cho các tệp thực thi PIE.
Thử nghiệm này đã dạy chúng tôi rằng trình liên kết tự kiểm tra chương trình có thể là PIE và đánh dấu nó như vậy.
Sau đó, khi biên dịch với GCC, -pie
yêu cầu GCC tạo lắp ráp vị trí độc lập.
Nhưng nếu chúng ta tự viết assembly, chúng ta phải tự đảm bảo rằng chúng ta đã đạt được sự độc lập về vị trí.
Trong ARMv8 aarch64, có thể đạt được vị trí hello world độc lập với lệnh ADR .
Làm cách nào để xác định một ELF có độc lập về vị trí hay không?
Bên cạnh việc chỉ chạy nó qua GDB, một số phương thức tĩnh được đề cập tại:
Đã thử nghiệm trong Ubuntu 18.10.