首頁 >運維 >linux運維 >linux pic是什麼

linux pic是什麼

青灯夜游
青灯夜游原創
2022-07-15 19:14:302945瀏覽

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

linux 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是什麼的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn