Elf(可執行和可連結檔案)是一個永遠也繞不開的話題,只要我們還在使用安卓手機/linux伺服器,我們就需要了解elf的一些方方面面,現在就讓我們從一個常量值提取的小需求出發,逐步解析elf檔案結構吧!
一、寫作目的:
網路上關於elf檔案結構描述的文章不在少數,但能具體到二進位制分析的卻屈指可數,總給人一種八股文的感覺,而最近恰好又遇到了一個需要透過符號表獲取其表示的常量值的需求,在完成之後,我將實現的過程進行總結提煉寫下這麼一篇elf結構入門的文章供後續學習回顧。
二、需求:
在C++中存在許多常量賦值和使用的操作,現在我們獲取到了一個由C++編譯成的動態連結庫.so檔案,我們想要反推一下其中可能的符號及其所表示的值。
三、基礎知識
①.Elf檔案型別:
Elf檔案型別分為三種,.o\.so\.exe,普通的.exe可執行檔案相信大家並不陌生,這裡主要介紹一下.o和.so檔案。.o為可重定位的目標檔案,.so為共享目標檔案,兩者的區別就是.o是靜態的,.so是動態的,靜態就是指它將被連結器在編譯時合併到可執行檔案中,而動態則是在可執行檔案要使用它時才進行載入。除了用途不同,其檔案結構和檔案結構中各種資料型別都是相同的,elf檔案中資料型別大致為圖中幾類有符號無符號1/2/4大小的地址偏移整數等(我們解析時只需記住char是1位元組half是2位元組其餘4位元組即可)。
②.Elf檔案連結檢視和執行檢視
elf的檔案結構就稍微複雜一些,其分為連結檢視和執行檢視兩種視角,之所以叫視角,是因為如同人看待一個物體的不同角度,雖然看上去不一樣,但本質都是同一個物體。這裡的連結檢視即指以連結器的角度來看elf檔案,它關注的是elf檔案的節區,即用頭部節取表去定位各個節區然後進行連結,而執行檢視則是以程式執行的角度來看elf檔案,它關注的是如何使用程式頭部表去定位各段然後載入到記憶體中去。其兩種檢視的對應關係也如上圖所示,.text節對應程式碼段,.rodata/.data/.bbs等包含資料的節對應資料段,其餘還有一些專門連結用的如動態符號表等,則不會被載入到記憶體中。
四、實現
在瞭解了上述的一些基礎知識後,我們也知道要獲取符號及其對應值,我們不能從執行檢視出發,因為符號表可能都不會被程式頭部表識別到,所以我們從連結檢視出發,根據頭部節區表定位資料節區和符號表節區,根據其索引關係完成匹配,具體實現過程如下所示。
接著,讓我們一步一步梳理
①解析elf檔案頭部:
首先貼出elf檔案頭的結構定義
想必精通C/C++的各位大佬一定是一眼秒懂的,這裡就不過多解釋構造了,其ELF32的資料型別具體表示含義在上面已有展示,這裡也不多說。這裡關鍵資料有以下幾點:
e_ident:十六位元組陣列
首先就是魔數了,看檔案先看魔數,這裡的16位位元的e_ident的前4位資料只能是0x7F454C46,轉換成ascii碼即0x7F ELF。然後依次表示進位制(1為32位/2為64位)、大小端(1為LSB/2為MSB)、版本資訊(1為當前版本)、執行所在系統(0為UNIX/3為Linux...)、作業系統ABI、7位填充資料。
e_type:兩位元組目標檔案型別
1表示可重定位檔案、2表示可執行檔案、3表示共享物件檔案、4表示核心轉儲檔案
依照這個規律解讀上圖所示例子,即這是一個32位/LSB/當前版本/執行在UNIX上的.so檔案。
在確定了檔案型別之後,我們便可以依照上述的流程接著往下解析...
節區頭部表格偏移、表項大小、表項數
根據elf頭部結構我們可以輕鬆知道上面我們要的資訊
e_shoff(32-35)\e_shentsize(46-47)\e_shnum(48-49)
該測試檔案頭部節區表偏移為2896(LSB)、表項大小為40、表項數為22
節區頭部表名稱字串表索引
節區頭部表中每一個表項所需使用的名稱字串,對應的字串表,這個在解析節區頭部表項時會使用到,此處為21(0X15)
驗證
我們知道從連結檢視來看,elf檔案頭部節區表結束後檔案也就讀取完了,故我們2896+40*22應該就是檔案大小3776了(果真如此,看來上述分析工作全對)
②獲取頭部節區表:
在elf頭部中,我們已經獲取了頭部節區表偏移、表項大小、表項數,現在我們就可以根據頭部表項依次讀取節區了,節區表表項結構如下圖所示
其中我們需要關注的有以下幾點:
表項序號(index)、節區名(sh_name)、節區型別(sh_type)、節區偏移(sh_offset)、節區長度(sh_size)、附加資訊(sh_info)
表項序號:
載入時透過計數獲得
節區偏移:
表項內第17-20個位元組,表示檔案內節區資料偏移
節區長度:
表項內第21-24個位元組,表示檔案內節區資料偏移
節區名:
節點區名為在對應(檔案頭部的字串表索引)字串表中的索引,再以\0結尾取得一個字串。
節區型別:
位於單個表項的第5-8位位元,表示節區用途,常見的有:
SHT_PROGBITS(0x1):包含程式定義的資料,如程式碼、只讀資料、可讀寫資料等。
SHT_SYMTAB(0x2):包含符號表資訊,用於連結或除錯。
SHT_STRTAB(0x3):包含字串表,通常用於表示符號表或節區表中的名字。
...
SHT_DYNSYM(11):包含動態連結符號表,用於執行時的符號解析。
...
③獲取節區字串表
而上文檔案頭部中我們已經得到節區使用的字串表項的索引為15,而節區表偏移為2896、表項大小為40,所以該字串表表項的偏移為2896 + 21*40 = 3736
從字串表的節區表項中我們可以得到其實際字串表的偏移為2691(0xA83),長度為202(CA)
而一個表項的節區名即表項內第1-4個位元組,為對應字串表的內部索引,字串表的節區名索引為178(0XB2),再根據\0結尾斷句,即頭部節區表對應字串表名字為.shstrtab
(透過節區頭部表項對應的字串表.shstrtab我們也能夠大致知道該elf檔案中的成分資訊了--如是否包含某些特定節區)
上述字串表第5-8位為0x03000000,即表示它包含字串表(其他節區也可能包含字串表,但用法就不盡相同了)...
④獲取符號表
(從上述節區名字串表中我們可以得知存在動態符號表.dynsym,不存在靜態符號表.symtab,所以在遍歷節區表項的時候,我們不僅可以透過名稱字串”.dynsym”也可以透過節區型別11/0x0B來定位動態符號表項)
根據符號表節區表項的資訊我們可以知道符號表存放的具體位置及單個專案大小
sh_addr(節區在記憶體中位置):第13-16個位元組,值為524(0x0C020000)
sh_offset(節區資料檔案中偏移):第17-20個位元組,值為524(0x0C020000)
sh_size(節區長度):第21-24個位元組,值為304(0x30010000)
sh_link(節區頭部表索引):第25-28個位元組,值為7
sh_entsize(節區中單個專案大小):第37-40個位元組,值為16(0x10000000)
在上述符號表中,實際儲存19個符號結構體(304 / 16)
單個符號項如上圖所示,其中有這麼幾個值
符號名稱:
st_name,第1-4個位元組,為符號表中sh_link指向的字串表中的索引,同樣透過索引+\0結尾的方式獲取該符號名稱字串。
符號值:
st_value,第5-8個位元組,根據具體情況取得含義,例如符號表示函式時,該值為函式在記憶體中的起始地址,若該符號表示全域性或靜態變數時,表示記憶體在變數中的位置。
符號值值大小:
st_size,第9-12個位元組,變數長度或者函式程式碼所佔位元組數
符號型別:
st_info,第13個位元組,根據1個位元組的八位位元作為flag標註符號的特徵,高4位表示繫結屬性(Binding),低4位表示符號型別(Type)
Type:
STT_OBJECT(1):資料物件,通常是變數
STT_FUNC(2):函式或其他可執行程式碼
...
Binding:
STB_LOCAL(0):區域性符號,只在當前模組中可見
STB_GLOBAL(1):全域性符號,在所有模組中可見
...
節區頭部索引:
st_shndx,第15-16個位元組,根據具體情況取得含義
⑤獲取符號名稱字串表
首先我們要獲取符號名,符號名即變數名/函式名...
根據節區頭部符號表項中的sh_link值7,我們可以計算出對應字串表的起始地址
2896+40*7
(根據節區表項中的節區型別為3,我們也可以篤定該節區就是我們要找的字串節區)
讀取節區資訊:
名稱:93(0x5D),加上名稱符號表偏移2691,得到該名稱字串.dynstr
偏移:1180(0x9C04)
大小:341(0x0155)
以下即符號名字字串表
⑥遍歷符號
獲取符號型別
遍歷符號表(與4中圖重複)每一個符號(4中已簡述每個符號結構),獲取其符號名和符號型別,st_info的低四位為1,則符號為OBJECT變數,若4size則可能為字串指標
如這六個符號,其符號值大小為4,st_info(0x11)為00010001即全域性的資料物件
獲取符號值節區
在根據st_shndx值18(0x12),定位到符號值儲存節區頭部表項偏移2896 + 18*40 = 3616
名稱值為77+2691即.ARM.attributes
記憶體中地址為14620(0x1C39)
檔案偏移為2332(0x1C09)
節區長度為24(0x18)
獲取符號值和對應常量
再結合上述六個符號(符號名對應字串表已在上文給出),我們可以得到以下資訊
(ad_value為透過計算st_value與上述記憶體偏移14620獲得的符號變數值-地址)(注意:此處地址值仍需LSB轉換)
(value為透過地址值偏移獲取的變數對應的常量值)
st_name-1: 1426 = 1180 + 246(0xF6) -> “global_var2”
st_value-1: 14628(0x2439)
ad_value-1: 1831(0x2707)
Value: ”測”
st_name-2: 1385 = 1180 + 205(0xCD) -> “a”
st_value-2: 14636(0x2C39)
ad_value-2: 0xFFFFFF7F
Value-2: 0x7FFFFFFF(INT_MAX)
st_name-3: 1438 = 1180 + 258(0x0201) -> “global_var3”
st_value-3: 14632(0x2839)
ad_value-3: 1743(0xCF06)
Value: ”abc”
st_name-4: 1401 = 1180 + 221(0xDD) -> “b”
st_value-4: 14640(0x3039)
ad_value-4: 0xFFFFFF7F
Value-4: 0x7FFFFFFF(INT_MAX)
st_name-5: 1403 = 1180 + 223(0xDF) -> “global_var”
st_value-5: 14620(0x1C39)
ad_value-5: 1747(0xD306)
Value: ”doGlobalVarTest測試”
st_name-6: 1414 = 1180 + 234(0xEA) -> “global_var1”
st_value-6: 14624(0x2039)
ad_value-6: 1652(0x7406)
Value: “測aaa”
四、總結
透過上述步驟,我們依託定位符號常量的需求,逐步分析了elf的檔案架構。並根據以下測試程式我們實驗了有哪些資料會在編譯成.so檔案後保留符號(全域性非靜態變數),以及如何獲取其變數值