อ็อพชัน -fPIE สำหรับไฟล์ปฏิบัติการที่ไม่ขึ้นกับตำแหน่งใน gcc และ ld คืออะไร?


คำตอบ:


100

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


3
นอกจากนี้ใน wikipedia: en.wikipedia.org/wiki/…
osgx

5
เหตุใด -pie และ -static จึงเข้ากันได้บน ARM และเข้ากันไม่ได้ที่ x86 คำถาม SO ของฉัน: stackoverflow.com/questions/27082959/…
4ntoine

56

ตัวอย่างที่รันได้น้อยที่สุด: 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 main0x401126

จากนั้นในช่วงทั้งประหารชีวิตหยุดที่อยู่run0x401126

สิ่งที่-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

GitHub อัปสตรีม

และประกอบและทำงานได้ดีด้วย:

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


1
สวัสดี Ciro! คุณสามารถสร้างคำถามแยกต่างหากสำหรับที่อยู่เริ่มต้น ASLR-off pie-on และเชื่อมโยงที่นี่ได้หรือไม่
osgx

1
@osgx เสร็จแล้ว. คุณรู้แล้วหรือกำลังจะขุดมันขึ้นมาทันที? :-) ในขณะที่คุณอยู่มันจะเป็นการดีที่จะอธิบายว่าตัวโหลดเคอร์เนล / dyn ของลินุกซ์กำหนดว่าบางสิ่งมี PIE หรือไม่: unix.stackexchange.com/questions/89211/…
Ciro Santilli 郝海东冠状病六四事件法轮功

ฉันไม่รู้อยู่แล้ว แต่ฉันรู้ว่าควรขุดมาจาก rtld ของ glibc - glibc / elf github.com/lattera/glibc/tree/master/elf (หากล่ามยังคงเป็น ld-linux.so) สามปีที่แล้ว Basile ไม่แน่ใจเกี่ยวกับ 0x55555555 ด้วยstackoverflow.com/questions/29856044แต่คำถามนั้นเกี่ยวกับที่อยู่เริ่มต้นของ ld.so เองดังนั้นให้ขุดลงในfs / binfmt_elf.cของเคอร์เนลหรือ readelf / objdump และ linker scripts
osgx
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.