จะเปลี่ยนรหัสอย่างไรเช่นการเรียกฟังก์ชัน
จะเปลี่ยนรหัสอย่างไรเช่นการเรียกฟังก์ชัน
คำตอบ:
PIE รองรับการสุ่มเค้าโครงพื้นที่ที่อยู่ (ASLR)ในไฟล์ปฏิบัติการ
ก่อนที่จะสร้างโหมด PIE โปรแกรมปฏิบัติการไม่สามารถวางไว้ที่แอดเดรสแบบสุ่มในหน่วยความจำได้เฉพาะไลบรารีไดนามิกของรหัสอิสระ (PIC) เท่านั้นที่สามารถย้ายไปยังออฟเซ็ตแบบสุ่มได้ มันทำงานเหมือนกับสิ่งที่ PIC ทำกับไลบรารีไดนามิกความแตกต่างคือไม่ได้สร้างตารางการเชื่อมโยงโพรซีเดอร์ (PLT) แต่จะใช้การย้ายที่สัมพันธ์กับพีซีแทน
หลังจากเปิดใช้งานการสนับสนุน PIE ใน gcc / linkers เนื้อหาของโปรแกรมจะถูกคอมไพล์และเชื่อมโยงเป็นรหัสที่ไม่ขึ้นกับตำแหน่ง ตัวเชื่อมโยงแบบไดนามิกทำการประมวลผลการย้ายตำแหน่งทั้งหมดบนโมดูลโปรแกรมเช่นเดียวกับไลบรารีไดนามิก การใช้ข้อมูลส่วนกลางใด ๆ จะถูกแปลงเป็นการเข้าถึงผ่าน Global Offsets Table (GOT) และการย้าย GOT
PIE ได้รับการอธิบายอย่างดีในงานนำเสนอ OpenBSD PIEนี้
การเปลี่ยนแปลงฟังก์ชันจะแสดงในสไลด์นี้ (PIE vs PIC)
x86 pic เทียบกับพาย
ตัวแปรและฟังก์ชันส่วนกลางในพื้นที่ได้รับการปรับให้เหมาะสมในวงกลม
ตัวแปรส่วนกลางภายนอกและฟังก์ชันเหมือนกับรูป
และในสไลด์นี้ (PIE เทียบกับการเชื่อมโยงแบบเก่า)
x86 พายเทียบกับไม่มีแฟล็ก (คงที่)
ตัวแปรและฟังก์ชันโกลบอลโลคัลจะคล้ายกับค่าคงที่
ตัวแปรส่วนกลางภายนอกและฟังก์ชันเหมือนกับรูป
โปรดทราบว่า PIE อาจเข้ากันไม่ได้กับไฟล์ -static
ตัวอย่างที่รันได้น้อยที่สุด: GDB ที่เรียกใช้งานได้สองครั้ง
สำหรับผู้ที่ต้องการดูการดำเนินการบางอย่างมาดูกันว่า ASLR ทำงานบน PIE ที่เรียกใช้งานได้และเปลี่ยนที่อยู่ระหว่างการรัน:
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
สำหรับคนที่มี-no-pie
ทุกอย่างน่าเบื่อ:
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
ก่อนที่จะเริ่มดำเนินการกำหนดจุดพักที่break main
0x401126
จากนั้นในช่วงทั้งประหารชีวิตหยุดที่อยู่run
0x401126
สิ่งที่-pie
น่าสนใจกว่ามาก:
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
ก่อนที่จะเริ่มดำเนินการ GDB ใช้เวลาเพียง "จำลอง" 0x1139
ที่อยู่ที่มีอยู่ในปฏิบัติการ:
หลังจากที่มันจะเริ่มต้นอย่างไร GDB 0x5630df2d6139
ชาญฉลาดสังเกตเห็นว่ารถตักดินแบบไดนามิกที่วางโปรแกรมในสถานที่ที่แตกต่างกันและเป็นครั้งแรกที่หยุด
0x55763ab2e139
จากนั้นระยะที่สองยังชาญฉลาดสังเกตเห็นว่าปฏิบัติการย้ายอีกครั้งและจบลงที่หมด
echo 2 | sudo tee /proc/sys/kernel/randomize_va_space
ตรวจสอบให้แน่ใจว่า ASLR เปิดอยู่ (ค่าเริ่มต้นใน Ubuntu 17.10): ฉันจะปิดใช้งาน ASLR ชั่วคราว (การสุ่มเค้าโครงพื้นที่ที่อยู่) ได้อย่างไร | ถามอูบุนตู
set disable-randomization off
จำเป็นต้องใช้ GDB ตามชื่อที่แนะนำจะปิด ASLR สำหรับกระบวนการโดยค่าเริ่มต้นเพื่อให้ที่อยู่คงที่ในการรันเพื่อปรับปรุงประสบการณ์การดีบัก: ความแตกต่างระหว่างที่อยู่ gdb และที่อยู่ "จริง"? | กองมากเกิน
readelf
การวิเคราะห์
นอกจากนี้เรายังสามารถสังเกตได้ว่า:
readelf -s ./no-pie.out | grep main
ให้ที่อยู่โหลดรันไทม์จริง (พีซีชี้ไปที่คำสั่งต่อไปนี้ 4 ไบต์หลัง):
64: 0000000000401122 21 FUNC GLOBAL DEFAULT 13 main
ในขณะที่:
readelf -s ./pie.out | grep main
ให้เพียงแค่การชดเชย:
65: 0000000000001135 23 FUNC GLOBAL DEFAULT 14 main
เมื่อปิด ASLR (ด้วยrandomize_va_space
หรือset disable-randomization off
) GDB จะให้main
ที่อยู่เสมอ: 0x5555555547a9
ดังนั้นเราจึงอนุมานได้ว่าที่-pie
อยู่ประกอบด้วย:
0x555555554000 + random offset + symbol offset (79a)
สิ่งที่ต้องทำ 0x555555554000 ฮาร์ดโค้ดใน Linux kernel / glibc loader อยู่ที่ไหน / ที่ไหน ที่อยู่ของส่วนข้อความของ PIE ปฏิบัติการถูกกำหนดใน Linux ได้อย่างไร?
ตัวอย่างการประกอบขั้นต่ำ
สิ่งที่ยอดเยี่ยมอีกอย่างที่เราทำได้คือการเล่นกับรหัสแอสเซมบลีเพื่อทำความเข้าใจอย่างเป็นรูปธรรมมากขึ้นว่า PIE หมายถึงอะไร
เราสามารถทำได้ด้วยชุดประกอบอิสระของ Linux x86_64 สวัสดีชาวโลก:
หลัก
.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
และประกอบและทำงานได้ดีด้วย:
as -o main.o main.S
ld -o main.out main.o
./main.out
อย่างไรก็ตามหากเราพยายามเชื่อมโยงเป็น PIE ด้วย ( --no-dynamic-linker
จำเป็นต้องมีตามที่อธิบายไว้ที่: จะสร้าง ELF ที่ปฏิบัติการได้อิสระในตำแหน่งที่เชื่อมโยงแบบคงที่ใน Linux ได้อย่างไร ):
ld --no-dynamic-linker -pie -o main.out main.o
จากนั้นลิงก์จะล้มเหลวด้วย:
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
เพราะบรรทัด:
mov $msg, %rsi /* buffer */
ฮาร์ดโค้ดที่อยู่ข้อความในmov
ตัวถูกดำเนินการดังนั้นจึงไม่อยู่ในตำแหน่งที่เป็นอิสระ
ถ้าเราเขียนในตำแหน่งที่เป็นอิสระแทน:
lea msg(%rip), %rsi
จากนั้นลิงก์ PIE ก็ทำงานได้ดีและ GDB แสดงให้เราเห็นว่าไฟล์ปฏิบัติการถูกโหลดที่ตำแหน่งอื่นในหน่วยความจำทุกครั้ง
ความแตกต่างที่นี่คือการlea
เข้ารหัสที่อยู่ที่msg
สัมพันธ์กับที่อยู่พีซีปัจจุบันเนื่องจากrip
ไวยากรณ์ดูเพิ่มเติม: วิธีใช้ RIP Relative Addressing ในโปรแกรมประกอบ 64 บิต
นอกจากนี้เรายังสามารถค้นหาได้โดยการแยกชิ้นส่วนทั้งสองเวอร์ชันด้วย:
objdump -S main.o
ซึ่งให้ตามลำดับ:
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
ดังนั้นเราจึงเห็นได้อย่างชัดเจนว่าlea
มีที่อยู่ที่ถูกต้องเต็มของการmsg
เข้ารหัสเป็นที่อยู่ปัจจุบัน + 0x19
mov
รุ่น แต่ได้มีการกำหนดที่อยู่ไป00 00 00 00
ซึ่งหมายความว่าการย้ายจะดำเนินการมี: Linkers จะทำอย่างไร? ความลับR_X86_64_32S
ในld
ข้อความแสดงข้อผิดพลาดคือประเภทของการย้ายตำแหน่งจริงที่จำเป็นและไม่สามารถเกิดขึ้นได้ในไฟล์ปฏิบัติการ PIE
สิ่งที่น่าสนุกอีกอย่างที่เราทำได้คือใส่msg
ส่วนข้อมูลแทน.text
ด้วย:
.data
msg:
.ascii "hello\n"
len = . - msg
ตอนนี้.o
ประกอบไปที่:
e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 15 <_start+0x15>
ดังนั้นการชดเชย RIP จึงเป็นตอนนี้0
และเราเดาว่าแอสเซมเบลอร์ได้ร้องขอการย้ายตำแหน่งแล้ว เรายืนยันว่า:
readelf -r main.o
ซึ่งจะช่วยให้:
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_X86_64_PC32
เป็นการย้ายตำแหน่งแบบสัมพัทธ์ของพีซีที่ld
สามารถจัดการกับไฟล์ปฏิบัติการ PIE ได้
การทดลองนี้สอนเราว่าตัวเชื่อมโยงเองตรวจสอบโปรแกรมสามารถเป็น PIE และทำเครื่องหมายเป็นเช่นนั้น
จากนั้นเมื่อคอมไพล์กับ GCC -pie
บอกให้ GCC สร้างตำแหน่งประกอบอิสระ
แต่ถ้าเราเขียนแอสเซมบลีเองเราต้องมั่นใจว่าเราได้รับตำแหน่งที่เป็นอิสระ
ใน ARMv8 aarch64 โลกตำแหน่งอิสระสวัสดีสามารถทำได้ด้วยการเรียนการสอน ADR
จะตรวจสอบได้อย่างไรว่า ELF มีตำแหน่งเป็นอิสระหรือไม่?
นอกเหนือจากการเรียกใช้งานผ่าน GDB แล้วเมธอดคงที่ยังกล่าวถึงที่:
ทดสอบใน Ubuntu 18.10