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