linux pic是什么
在Linux中,pic的中文意思為“位置無關(guān)代碼”,是指代碼無論被加載到哪個地址上都可以正常執(zhí)行。PIC用于生成位置無關(guān)的共享庫,所謂位置無關(guān),指的是共享庫的代碼斷是只讀的,存放在代碼段,多個進程可同時公用這份代碼段而不需要拷貝副本。
程序員必備接口測試調(diào)試工具:立即使用
Apipost = Postman + Swagger + Mock + Jmeter
Api設(shè)計、調(diào)試、文檔、自動化測試工具
后端、前端、測試,同時在線協(xié)作,內(nèi)容實時同步
本教程操作環(huán)境:linux7.3系統(tǒng)、Dell G3電腦。
在linux中,pic全稱“Position Independent Code”,中文意思為“位置無關(guān)代碼”。
一、程序虛擬地址空間及位置有關(guān)代碼概述
Linux進程從磁盤加載到內(nèi)存中運行的過程中,內(nèi)核會為進程分配虛擬地址空間,虛擬地址空間被劃分為一塊塊的區(qū)域(Segment),其中最重要的幾個區(qū)域如下:
圖1 - 應(yīng)用程序虛擬地址空間說明
內(nèi)核地址空間,對所有應(yīng)用來說都是相同的,這部分地址空間應(yīng)用無法直接訪問。內(nèi)核地址空間不是本文關(guān)注的重點,我們重點關(guān)注應(yīng)用程序的重要的一些SEGMENT。
表1 - 應(yīng)用程序重要segment描述
如果系統(tǒng)沒有開啟地址隨機化(ASLR - Address Space Layout Randomization,地址隨機化,后文會介紹),則Linux會將上面表格中的各個segment的地址空間放到一個固定的地址上面。
我們寫一個實際的程序來看看在一個Linux X86_64的機器上各個segment的地址是如何排布的,程序如下,覆蓋了我們關(guān)心的segment。
圖2 - 虛擬地址空間演示程序
編譯
gcc -o addr_test addr_test.c -static
登錄后復(fù)制
(此處使用靜態(tài)鏈接,以便演示位置相關(guān)代碼的特征)
我們運行這個程序3次,會發(fā)現(xiàn)所有的地址都是一個固定值。這是因為在沒有開ASLR特性時,系統(tǒng)不會隨機化分配程序的虛擬地址空間,程序所有的地址都是按照固定的規(guī)則來生成。
圖3 - 固定segment地址分布
通過objdump命令反匯編后可以看到,對于全局變量和函數(shù)調(diào)用的訪問,匯編指令跟的地址都是固定的,這樣的代碼我們就稱它為位置相關(guān)的。
圖4 - 位置相關(guān)代碼匯編語句實例
這種代碼,由于地址是寫死的,只能加載到指定地址上運行,一旦加載地址有變化,由于代碼里訪問的變量、函數(shù)地址是固定的,加載地址變化后程序無法正常執(zhí)行。
固定地址的方式雖然簡單,但是無法實現(xiàn)一些高級特性比如動態(tài)庫支持。動態(tài)庫的代碼會通過mmap()系統(tǒng)調(diào)用來映射到進程的虛擬地址空間,不同的進程中,同一個動態(tài)庫映射的虛擬地址是不確定的。如果動態(tài)庫的實現(xiàn)上使用位置相關(guān)的代碼,則無法達到其任意地址運行的目的,這種情況下我們就需要引入位置無關(guān)代碼PIC的概念了。
另外,我們可以看到,在沒有開啟地址隨機化特性的系統(tǒng)上,由于程序各個segment的地址是固定的,黑客在攻擊時會更加簡單(感興趣的同學(xué)可以搜索一下Ret2shellcode或Ret2libc攻擊),此時需要引入PIE的概念搭配ASLR一起來防護。
二、位置無關(guān)代碼PIC和動態(tài)庫的實現(xiàn)
PIC位置無關(guān)代碼是指代碼無論被加載到哪個地址上都可以正常執(zhí)行。gcc選項中添加-fPIC會產(chǎn)生相關(guān)代碼。
PIC用于生成位置無關(guān)的共享庫,所謂位置無關(guān),指的是共享庫的代碼斷是只讀的,存放在代碼段,多個進程可同時公用這份代碼段而不需要拷貝副本。庫中的變量(全局變量和靜態(tài)變量)通過GOT表訪問,而庫中的函數(shù),通過PLT->GOT->函數(shù)位置進行訪問。Linux下編譯共享庫時,必須加上-fPIC參數(shù),否則在鏈接時會有錯誤提示(有資料說AMD64的機器才會出現(xiàn)這種錯誤,但我在Inter的機器上也出現(xiàn)了)。
關(guān)鍵點#1 - 代碼段和數(shù)據(jù)段的偏移
代碼段和數(shù)據(jù)段之間的偏移,在鏈接的時候由鏈接器給出,對于PIC來說非常重要。當(dāng)鏈接器將各個目標文件的所有p組合到一起的時候,鏈接器完全知道每個p的大小和它們之間的相對位置。
圖5 - 代碼段和數(shù)據(jù)段偏移示例
如上圖所示,示例中這里TEXT和DATA時緊緊挨著的,其實無論DATA和TEXT是否是相鄰的,鏈接器都能知道這兩個段的偏移。根據(jù)這個偏移,可以計算出在TEXT段內(nèi)任意一條指令相對于DATA段起始地址的相對偏移量。如上圖,無論TEXT段被放到了哪個虛擬地址上,假設(shè)一條mov指令在TEXT內(nèi)部的0xe0偏移處,那么我們可以知道,DATA段的相對偏移位置就是:TEXT段的大小 - mov指令在TEXT內(nèi)部的偏移 = 0xXXXXE000 - 0xXXXX00E0 = 0xDF20
關(guān)鍵點#2 - X86上指令相對偏移的計算
如果使用相對位置進行處理,可以看到代碼能夠做到位置無關(guān)。但在X86平臺上mov指令對于數(shù)據(jù)的引用需要一個絕對地址,那應(yīng)該怎么辦呢?
從“關(guān)鍵點1”里的描述來看,我們?nèi)绻懒水?dāng)前指令的地址,那么就可以計算出數(shù)據(jù)段的地址。X86平臺上沒有獲取當(dāng)前指令指針寄存器IP的值的指令(X64上可以直接訪問RIP),但可以通過一個小技巧來獲取。來看一段偽代碼:
圖6 - X86平臺獲取指令地址匯編
這段代碼在實際運行時,會有以下的事情發(fā)生:
當(dāng)cpu執(zhí)行 call STUB的時候,會將下一條指令的地址保存到stack上,然后跳到標簽STUB處執(zhí)行。
STUB處的指令是pop ebx,這樣就將 "pop ebx"這條指令所在的地址從stack彈出放到了ebx寄存器中,這樣就得到了IP寄存器的值。
1.全局偏移表GOT
在理解了前面的幾點后,來看看在X86上是如何實現(xiàn)位置無關(guān)的數(shù)據(jù)引用的,此特性是通過全局偏移表global offset table(GOT)來實現(xiàn)的。
GOT是一張在data p中保存的一張表,里面記錄了很多地址字段 (entry)。假設(shè)一條指令想要引用一個變量,并不是直接去用絕對地址,而是去引用GOT里的一個entry。GOT表在data p中的地址是明確的,GOT的entry包含了變量的絕對地址。
圖7 - 代碼地址和GOT表entry關(guān)系
如上圖,根據(jù)"關(guān)鍵點1"和“關(guān)鍵點2”,我們可以先獲取到當(dāng)前IP的值,然后計算得到GOT表的絕對地址,由于變量的地址entry在GOT表中的偏移也是已知的,因此可以實現(xiàn)位置無關(guān)的數(shù)據(jù)訪問。
以一條絕對地址的mov指令的偽代碼為例(X86平臺):
圖8 - 位置相關(guān)mov指令示例
如果要變成位置無關(guān)的代碼,則要多幾個步驟
圖9 - 結(jié)合GOT實現(xiàn)位置無關(guān)的mov指令示例
通過上面的步驟,就可以實現(xiàn)代碼訪問變量的地址無關(guān)化。但是還有一個問題,這個GOT表里存儲的VAR_ADDR值又是怎么變成實際的絕對地址的呢?
假設(shè)有一個libtest.so,有一個全局變量g_var,我們通過readelf -r libtest.so后,會看到如下的輸出
圖10 - rel.dyn段全局變量重定向描述字段
動態(tài)加載器會解析rel.dyn段,當(dāng)它看到重定向類型為R_386_GLOB_DAT的時候,會做如下操作:將符號g_var實際的地址值替換到偏移0x1fe4處(也就是將Sym.Value的值替換為實際地址值)
2.函數(shù)調(diào)用的位置無關(guān)化實現(xiàn)
從理論上講,函數(shù)的PIC實現(xiàn)也可以通過和數(shù)據(jù)引用GOT表相同的方式實現(xiàn)位置無關(guān)。不直接使用函數(shù)的地址,而是通過查GOT來找到實際的函數(shù)絕對地址。但實際上函數(shù)的PIC特性并不是這么做的,實際情況會復(fù)雜一些。為什么不按照和數(shù)據(jù)引用一樣的方式,先來看一個概念:延遲綁定。
對于動態(tài)庫的函數(shù)來說,在沒有加載到程序的地址空間前,函數(shù)的實際地址都是未知的,動態(tài)加載器會處理這些問題,解析出實際地址的過程,這個過程稱之為綁定。綁定的動作會消耗一些時間,因為加載器要通過特殊的查表、替換操作。
如果動態(tài)庫有成百上千個函數(shù)接口,而實際的進程只用到了其中的幾十個接口,如果全部都在加載的時候進行綁定操作,沒有意義并且非常耗時。因此提出了延遲綁定的概念,程序只有在使用到對應(yīng)接口時才實時地綁定接口地址。
因為有了延遲綁定的需求,所以函數(shù)的PIC實現(xiàn)和數(shù)據(jù)訪問的PIC有所區(qū)別。為了實現(xiàn)延遲綁定,就額外增加了一個間接表PLT(過程鏈接表)。
PLT搭配GOT實現(xiàn)延遲綁定的過程如下:
第一次調(diào)用函數(shù)
圖11 - 首次調(diào)用PIC函數(shù)時PLT,GOT關(guān)系
首先跳到PLT表對應(yīng)函數(shù)地址PLT[n],然后取出GOT中對應(yīng)的entry。GOT[n]里保存了實際要跳轉(zhuǎn)的函數(shù)的地址,首次執(zhí)行時此值為PLT[n]的prepare resolver的地址,這里準備了要解析的函數(shù)的相關(guān)參數(shù),然后到PLT[0]處調(diào)用resolver進行解析。
resolver函數(shù)會做幾件事情:
(1)解析出代碼想要調(diào)用的func函數(shù)的實際地址A
(2)用實際地址A覆蓋GOT[n]保存的plt_resolve_addr的值
(3)調(diào)用func函數(shù)
首次調(diào)用后,上圖的鏈接關(guān)系會變成下圖所示:
圖12 - 首次調(diào)用PIC函數(shù)后PLT,GOT關(guān)系
隨后的調(diào)用函數(shù)過程,就不需要再走resolver過程了
三、位置無關(guān)可執(zhí)行程序PIE
PIE,全稱Position Independent Executable。2000年早期及以前,PIC用于動態(tài)庫。對于可執(zhí)行程序來講,仍然是使用絕對地址鏈接,它可以使用動態(tài)庫,但程序本身的各個segment地址仍然是固定的。隨著ASLR的出現(xiàn),可執(zhí)行程序運行時各個segment的虛擬地址能夠隨機分布,這樣就讓攻擊者難以預(yù)測程序運行地址,讓緩存溢出攻擊變得更困難。OS在使能ASLR的時候,會檢查可執(zhí)行程序是否是PIE的可執(zhí)行程序。gcc選項中添加-fPIE會產(chǎn)生相關(guān)代碼。
四、Linux ASLR機制和PIE的關(guān)系
ASLR的全稱為 Address Space Layout Randomization。在Linux 2.6.12 中被引入到 Linux 系統(tǒng),它將進程的某些虛擬地址進行隨機化,增大了入侵者預(yù)測目的地址的難度,降低應(yīng)用程序被攻擊成功的風(fēng)險。
在Linux系統(tǒng)上,ASLR有三個級別
表2 - ASLR級別描述
ASLR的級別通過兩種方式配置:
echo level > /proc/sys/kernel/randomize_va_space
登錄后復(fù)制