共計 5058 個字符,預計需要花費 13 分鐘才能閱讀完成。
這篇“linux pic 指的是什么”文章的知識點大部分人都不太理解,所以丸趣 TV 小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“linux pic 指的是什么”文章吧。
在 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 pic 指的是什么”這篇文章的內容,相信大家都有了一定的了解,希望丸趣 TV 小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注丸趣 TV 行業資訊頻道。