linux pic是什麼

2022-07-15 22:00:27

在linux中,pic的中文意思為「位置無關程式碼」,是指程式碼無論被載入到哪個地址上都可以正常執行。PIC用於生成位置無關的共用庫,所謂位置無關,指的是共用庫的程式碼斷是唯讀的,存放在程式碼段,多個程序可同時公用這份程式碼段而不需要拷貝副本。

本教學操作環境:linux7.3系統、Dell G3電腦。

在linux中,pic全稱「Position Independent Code」,中文意思為「位置無關程式碼」。

一、程式虛擬地址空間及位置有關程式碼概述

Linux程序從磁碟載入到記憶體中執行的過程中,核心會為程序分配虛擬地址空間,虛擬地址空間被劃分為一塊塊的區域(Segment),其中最重要的幾個區域如下:

圖1 - 應用程式虛擬地址空間說明

核心地址空間,對所有應用來說都是相同的,這部分地址空間應用無法直接存取。核心地址空間不是本文關注的重點,我們重點關注應用程式的重要的一些SEGMENT。

表1 - 應用程式重要segment描述

如果系統沒有開啟地址隨機化(ASLR - Address Space Layout Randomization,地址隨機化,後文會介紹),則Linux會將上面表格中的各個segment的地址空間放到一個固定的地址上面。

我們寫一個實際的程式來看看在一個Linux X86_64的機器上各個segment的地址是如何排布的,程式如下,覆蓋了我們關心的segment。

圖2 - 虛擬地址空間演示程式

編譯

gcc -o addr_test addr_test.c -static

(此處使用靜態連結,以便演示位置相關程式碼的特徵)

我們執行這個程式3次,會發現所有的地址都是一個固定值。這是因為在沒有開ASLR特性時,系統不會隨機化分配程式的虛擬地址空間,程式所有的地址都是按照固定的規則來生成。

圖3 - 固定segment地址分佈

通過objdump命令反組合後可以看到,對於全域性變數和函數呼叫的存取,組合指令跟的地址都是固定的,這樣的程式碼我們就稱它為位置相關的。

圖4 - 位置相關程式碼組合語句範例

這種程式碼,由於地址是寫死的,只能載入到指定地址上執行,一旦載入地址有變化,由於程式碼裡存取的變數、函數地址是固定的,載入地址變化後程式無法正常執行。

固定地址的方式雖然簡單,但是無法實現一些高階特性比如動態庫支援。動態庫的程式碼會通過mmap()系統呼叫來對映到程序的虛擬地址空間,不同的程序中,同一個動態庫對映的虛擬地址是不確定的。如果動態庫的實現上使用位置相關的程式碼,則無法達到其任意地址執行的目的,這種情況下我們就需要引入位置無關程式碼PIC的概念了。

另外,我們可以看到,在沒有開啟地址隨機化特性的系統上,由於程式各個segment的地址是固定的,駭客在攻擊時會更加簡單(感興趣的同學可以搜尋一下Ret2shellcode或Ret2libc攻擊),此時需要引入PIE的概念搭配ASLR一起來防護。

二、位置無關程式碼PIC和動態庫的實現

PIC位置無關程式碼是指程式碼無論被載入到哪個地址上都可以正常執行。gcc選項中新增-fPIC會產生相關程式碼。

PIC用於生成位置無關的共用庫,所謂位置無關,指的是共用庫的程式碼斷是唯讀的,存放在程式碼段,多個程序可同時公用這份程式碼段而不需要拷貝副本。庫中的變數(全域性變數和靜態變數)通過GOT表存取,而庫中的函數,通過PLT->GOT->函數位置進行存取。Linux下編譯共用庫時,必須加上-fPIC引數,否則在連結時會有錯誤提示(有資料說AMD64的機器才會出現這種錯誤,但我在Inter的機器上也出現了)。

關鍵點#1 - 程式碼段和資料段的偏移

程式碼段和資料段之間的偏移,在連結的時候由連結器給出,對於PIC來說非常重要。當連結器將各個目標檔案的所有p組合到一起的時候,連結器完全知道每個p的大小和它們之間的相對位置。

圖5 - 程式碼段和資料段偏移範例

如上圖所示,範例中這裡TEXT和DATA時緊緊挨著的,其實無論DATA和TEXT是否是相鄰的,連結器都能知道這兩個段的偏移。根據這個偏移,可以計算出在TEXT段內任意一條指令相對於DATA段起始地址的相對偏移量。如上圖,無論TEXT段被放到了哪個虛擬地址上,假設一條mov指令在TEXT內部的0xe0偏移處,那麼我們可以知道,DATA段的相對偏移位置就是:TEXT段的大小 - mov指令在TEXT內部的偏移 = 0xXXXXE000 - 0xXXXX00E0 = 0xDF20

關鍵點#2 - X86上指令相對偏移的計算

如果使用相對位置進行處理,可以看到程式碼能夠做到位置無關。但在X86平臺上mov指令對於資料的參照需要一個絕對地址,那應該怎麼辦呢?

從「關鍵點1」裡的描述來看,我們如果知道了當前指令的地址,那麼就可以計算出資料段的地址。X86平臺上沒有獲取當前指令指標暫存器IP的值的指令(X64上可以直接存取RIP),但可以通過一個小技巧來獲取。來看一段虛擬碼:

圖6 - X86平臺獲取指令地址組合

這段程式碼在實際執行時,會有以下的事情發生:

  • 當cpu執行 call STUB的時候,會將下一條指令的地址儲存到stack上,然後跳到標籤STUB處執行。

  • STUB處的指令是pop ebx,這樣就將 "pop ebx"這條指令所在的地址從stack彈出放到了ebx暫存器中,這樣就得到了IP暫存器的值。

1.全域性偏移表GOT

在理解了前面的幾點後,來看看在X86上是如何實現位置無關的資料參照的,此特性是通過全域性偏移表global offset table(GOT)來實現的。

GOT是一張在data p中儲存的一張表,裡面記錄了很多位址列位 (entry)。假設一條指令想要參照一個變數,並不是直接去用絕對地址,而是去參照GOT裡的一個entry。GOT表在data p中的地址是明確的,GOT的entry包含了變數的絕對地址。

圖7 - 程式碼地址和GOT表entry關係

如上圖,根據"關鍵點1"和「關鍵點2」,我們可以先獲取到當前IP的值,然後計算得到GOT表的絕對地址,由於變數的地址entry在GOT表中的偏移也是已知的,因此可以實現位置無關的資料存取。

以一條絕對地址的mov指令的虛擬碼為例(X86平臺):

圖8 - 位置相關mov指令範例

如果要變成位置無關的程式碼,則要多幾個步驟

圖9 - 結合GOT實現位置無關的mov指令範例

通過上面的步驟,就可以實現程式碼存取變數的地址無關化。但是還有一個問題,這個GOT表裡儲存的VAR_ADDR值又是怎麼變成實際的絕對地址的呢?

假設有一個libtest.so,有一個全域性變數g_var,我們通過readelf -r libtest.so後,會看到如下的輸出

圖10 - rel.dyn段全域性變數重定向描述欄位

動態載入器會解析rel.dyn段,當它看到重定向型別為R_386_GLOB_DAT的時候,會做如下操作:將符號g_var實際的地址值替換到偏移0x1fe4處(也就是將Sym.Value的值替換為實際地址值)

2.函數呼叫的位置無關化實現

從理論上講,函數的PIC實現也可以通過和資料參照GOT表相同的方式實現位置無關。不直接使用函數的地址,而是通過查GOT來找到實際的函數絕對地址。但實際上函數的PIC特性並不是這麼做的,實際情況會複雜一些。為什麼不按照和資料參照一樣的方式,先來看一個概念:延遲繫結。

對於動態庫的函數來說,在沒有載入到程式的地址空間前,函數的實際地址都是未知的,動態載入器會處理這些問題,解析出實際地址的過程,這個過程稱之為繫結。繫結的動作會消耗一些時間,因為載入器要通過特殊的查表、替換操作。

如果動態庫有成百上千個函數介面,而實際的程序只用到了其中的幾十個介面,如果全部都在載入的時候進行繫結操作,沒有意義並且非常耗時。因此提出了延遲繫結的概念,程式只有在使用到對應介面時才實時地繫結介面地址。

因為有了延遲繫結的需求,所以函數的PIC實現和資料存取的PIC有所區別。為了實現延遲繫結,就額外增加了一個間接表PLT(過程連結表)。

PLT搭配GOT實現延遲繫結的過程如下:

第一次呼叫函數

圖11 - 首次呼叫PIC函數時PLT,GOT關係

首先跳到PLT表對應函數地址PLT[n],然後取出GOT中對應的entry。GOT[n]裡儲存了實際要跳轉的函數的地址,首次執行時此值為PLT[n]的prepare resolver的地址,這裡準備了要解析的函數的相關引數,然後到PLT[0]處呼叫resolver進行解析。

resolver函數會做幾件事情:

(1)解析出程式碼想要呼叫的func函數的實際地址A

(2)用實際地址A覆蓋GOT[n]儲存的plt_resolve_addr的值

(3)呼叫func函數

首次呼叫後,上圖的連結關係會變成下圖所示:

圖12 - 首次呼叫PIC函數後PLT,GOT關係

隨後的呼叫函數過程,就不需要再走resolver過程了

三、位置無關可執行程式PIE

PIE,全稱Position Independent Executable。2000年早期及以前,PIC用於動態庫。對於可執行程式來講,仍然是使用絕對地址連結,它可以使用動態庫,但程式本身的各個segment地址仍然是固定的。隨著ASLR的出現,可執行程式執行時各個segment的虛擬地址能夠隨機分佈,這樣就讓攻擊者難以預測程式執行地址,讓快取溢位攻擊變得更困難。OS在使能ASLR的時候,會檢查可執行程式是否是PIE的可執行程式。gcc選項中新增-fPIE會產生相關程式碼。

四、Linux ASLR機制和PIE的關係

ASLR的全稱為 Address Space Layout Randomization。在Linux 2.6.12 中被引入到 Linux 系統,它將程序的某些虛擬地址進行隨機化,增大了入侵者預測目的地址的難度,降低應用程式被攻擊成功的風險。

在Linux系統上,ASLR有三個級別

表2 - ASLR級別描述

ASLR的級別通過兩種方式設定:

echo level > /proc/sys/kernel/randomize_va_space

sysctl -w kernel.randomize_va_space=level

例子:

echo 0 > /proc/sys/kernel/randomize_va_space 關閉地址隨機化

sysctl -w kernel.randomize_va_space=2 最大級別的地址隨機化

我們還是以文章開頭的那個程式來說明ASLR在不同級別下時如何表現的,首先在ASLR關閉的情況下,相關地址不變,輸出如下:

圖13 - ASLR=0時虛擬地址空間分配情況

我們把ASLR級別設定為1,執行兩次,看看結果:

圖14 - ASLR=1時虛擬地址空間分配情況

可以看到STACK和MMAP的地址發生了變化。堆、資料段、程式碼段仍然是固定地址。

接下來我們把ASLR級別設定為2,執行兩次,看看結果:

圖15 - ASLR=2,PIE不啟用時虛擬地址空間分配情況

可以看到此時堆的地址也發生了變化,但是我們發現BSS,DATA,TEXT段的地址仍然是固定的,不是說ASLR=2的時候,是完全隨機化嗎?

這裡就引出了PIE和ASLR的關係了。從上面的實驗可以看出,如果不對可執行檔案做一些特殊處理,ASLR即使在設定為完全隨機化的時候,也僅能對STACK,HEAP,MMAP等執行時才分配的地址空間進行隨機化,而可執行檔案本身的BSS,DATA,TEXT等沒有辦法隨機化。結合文章前面講到的PIE相關知識,我們也很容易理解這一點,因為編譯和連結過程中,如果沒有PIE的選項,生成的可執行檔案裡都是位置相關的程式碼。如果OS不管這一點,ASLR=2時也將BSS,DATA,TEXT等隨意排布,可想而知程式根本不能正常執行起來。

明白了原因,我們在編譯時加入PIE選項,然後在ASLR=2時重新執行一下看看結果如何

圖16 - ASLR=2,PIE啟用時虛擬地址空間分配情況

可以看到在PIE開啟的情況下,搭配ASLR=2,可以實現各個段的虛擬地址完全隨機化分佈。

相關推薦:《Linux視訊教學

以上就是linux pic是什麼的詳細內容,更多請關注TW511.COM其它相關文章!