為專案應用設定No-PIE

雜貨鋪學徒發表於2019-04-18

Steps for building your application as PIE

In Xcode, select your target in the "Targets" section, then click the "Build Settings" tab to view its settings.

For iOS apps, set iOS Deployment Target to iOS 4.3 or later. For Mac apps, set OS X Deployment Target to OS X 10.7 or later.

Verify that Generate Position-Dependent Code is set at to NO.

Verify that Don't Create Position Independent Executables is set to NO.
複製程式碼

PIE是什麼

PIE(position-independent executable)是一種生成地址無關可執行程式的技術。如果編譯器在生成可執行程式的過程中使用了PIE,那麼當可執行程式被載入到記憶體中時其載入地址存在不可預知性。

PIE還有個孿生兄弟PIC(position-independent code)。其作用和PIE相同,都是使被編譯後的程式能夠隨機的載入到某個記憶體地址。區別在於PIC是在生成動態連結庫時使用(Linux中的so),PIE是在生成可執行檔案時使用。

PIE的作用

安全性

PIE可以提高緩衝區溢位攻擊的門檻。它屬於ASLR(Address space layout randomization)的一部分。ASLR要求執行程式被載入到記憶體時,它其中的任意部分都是隨機的。包括 Stack, Heap ,Libs and mmap, Executable, Linker, VDSO。通過PIE我們能夠實現Executable 記憶體隨機化

節約記憶體使用空間

除了安全性,地址無關程式碼還有一個重要的作用是提高記憶體使用效率。

一個共享庫可以同時被多個程式裝載,如果不是地址無關程式碼(程式碼段中存在絕對地址引用),每個程式必須結合其自生的記憶體地址呼叫動態連結庫。導致不得不將共享庫整體拷貝到程式中。如果系統中有100個程式呼叫這個庫,就會有100份該庫的拷貝在記憶體中,這會照成極大的空間浪費。

相反如果被載入的共享庫是地址無關程式碼,100個程式呼叫該庫,則該庫只需要在記憶體中載入一次。這是因為PIE將共享庫中程式碼段須要變換的內容分離到資料段。使得程式碼段載入到記憶體時能做到地址無關。多個程式呼叫共享庫時只需要在自己的程式中載入共享庫的資料段,而程式碼段則可以共享。

image

PIE工作原理簡介

我們先從實際的例子出發,觀察PIE和NO-PIE在可執行程式表現形式上的區別。管中窺豹探索地址無關程式碼的實現原理。

例子一

定義如下C程式碼:

#include <stdio.h>

int global;

void main()
{
    printf("global address = %x\n", &global);
}
複製程式碼

程式中定義了一個全域性變數global並列印其地址。我們先用普通的方式編譯程式。

gcc -o sample1 sample1.c

執行程式可以觀察到global載入到記憶體的地址每次都一樣。

$./sample1
global address = 6008a8
$./sample1
global address = 6008a8
$./sample1
global address = 6008a8
複製程式碼

接著用PIE方式編譯 sample1.c

gcc -o sample1_pie sample1.c -fpie -pie

執行程式觀察global的輸出結果:

./sample1_pie
global address = 1ce72b38
./sample1_pie
global address = 4c0b38
./sample1_pie
global address = 766dcb38
複製程式碼

每次執行地址都會發生變換,說明PIE使執行程式每次載入到記憶體的地址都是隨機的。

例子二

在程式碼中宣告一個外部變數global。但這個變數的定義並未包含進編譯檔案中。

#include <stdio.h>

extern int global;

void main()
{
    printf("extern global address = %x\n", &global);
}
複製程式碼

首先使用普通方式編譯 extern_var.c。在編譯選項中故意不包含有global定義的原始檔。

gcc -o extern_var extern_var.c

發現不能編譯通過, gcc提示:

/tmp/ccJYN5Ql.o: In function `main':
extern_var.c:(.text+0xa): undefined reference to `global'
collect2: ld returned 1 exit status
複製程式碼

編譯器在連結階段有一步重要的動作叫符號解析與重定位。連結器會將所有中間檔案的資料,程式碼,符號分別合併到一起,並計算出連結後的虛擬基地址。比如 “.text”段從 0x1000開始,”.data”段從0x2000開始。接著連結器會根據基址計算各個符號(global)的相對虛擬地址。

當編譯器發現在符號表中找不到global的地址時就會報出 undefined reference to global.說明在靜態連結的過程中編譯器必須在編譯連結階段完成對所有符號的連結。

如果使用PIE方式將extern_var.c編譯成一個share library會出現什麼情況呢?

gcc -o extern_var.so extern_var.c -shared -fPIC

程式能夠順利編譯通過生成extern_var.so。但執行時會報錯,因為裝載時找不到global符號目標地址。這說明-fPIC選項生成了地址無關程式碼。將靜態連結時沒有找到的global符號的連結工作推遲到裝載階段。

那麼在編譯連結階段,連結器是如何將這個缺失的目標地址在程式碼段中進行地址引用的呢?

連結器巧妙的用一張中間表GOT(Global Offset Table)來解決被引用符號缺失目標地址的問題。如果在連結階段(jing tai)發現一個不能確定目標地址的符號。連結器會將該符號加到GOT表中,並將所有引用該符號的地方用該符號在GOT表中的地址替換。到裝載階段動態連結器會將GOT表中每個符號對應的實際目標地址填上。

當程式執行到符號對應的程式碼時,程式會先查GOT表中對應符號的位置,然後根據位置找到符號的實際的目標地址。

地址無關程式碼的生成方式 所謂地址無關程式碼要求程式被載入到記憶體中的任意地址都能夠正常執行。所以程式中對變數或函式的引用必須是相對的,不能包含絕對地址。

比如如下偽彙編程式碼:

PIE方式:程式碼可以執行在地址100或1000的地方

100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL CURRENT+10
...
111: NOP
複製程式碼

Non-PIE: 程式碼只能執行在地址100的地方

100: COMPARE REG1, REG2
101: JUMP_IF_EQUAL 111
...
111: NOP
複製程式碼

因為可執行程式的程式碼段只有讀和執行屬性沒有寫屬性,而資料段具有讀寫屬性。要實現地址無關程式碼,就要將程式碼段中須要改變的絕對值分離到資料段中。在程式載入時可以保持程式碼段不變,通過改變資料段中的內容,實現地址無關程式碼。

PIE和Non-PIE程式在記憶體中對映方式

在Non-PIE時程式每次載入到記憶體中的位置都是一樣的。

image

執行程式會在固定的地址開始載入。系統的動態連結器庫ld.so會首先載入,接著ld.so會通過.dynamic段中型別為DT_NEED的欄位查詢其他需要載入的共享庫。並依次將它們載入到記憶體中。注意:因為是Non-PIE模式,這些動態連結庫每次載入的順序和位置都一樣。

而對於通過PIE方式生成的執行程式,因為沒有絕對地址引用所以每次載入的地址也不盡相同。

image

不僅動態連結庫的載入地址不固定,就連執行程式每次載入的地址也不一樣。這就要求ld.so首先被載入後它不僅要負責重定位其他的共享庫,同時還要對可執行檔案重定位。


Reference

No-PIE 設定

APP漏洞掃描器之未使用地址空間隨機化

相關文章