ELF文件格式解析


1. ELF文件簡介

首先,你需要知道的是所謂對象文件(Object files)有三個種類:

  1. 可重定位的對象文件(Relocatable file) 
    這是由彙編器彙編生成的 .o 文件。後面的鏈接器(link editor)拿一個或一些 Relocatable object files 作爲輸入,經鏈接處理後,生成一個可執行的對象文件 (Executable file) 或者一個可被共享的對象文件(Shared object file)。我們可以使用 ar 工具將衆多的 .o Relocatable object files 歸檔(archive)成 .a 靜態庫文件。如何產生 Relocatable file,你應該很熟悉了,請參見我們相關的基本概念文章和JulWiki。另外,可以預先告訴大家的是我們的內核可加載模塊 .ko 文件也是 Relocatable object file。

  2. 可執行的對象文件(Executable file) 
    這我們見的多了。文本編輯器vi、調式用的工具gdb、播放mp3歌曲的軟件mplayer等等都是Executable object file。你應該已經知道,在我們的 Linux 系統裏面,存在兩種可執行的東西。除了這裏說的 Executable object file,另外一種就是可執行的腳本(如shell腳本)。注意這些腳本不是 Executable object file,它們只是文本文件,但是執行這些腳本所用的解釋器就是 Executable object file,比如 bash shell 程序。

  3. 可被共享的對象文件(Shared object file) 
    這些就是所謂的動態庫文件,也即 .so 文件。如果拿前面的靜態庫來生成可執行程序,那每個生成的可執行程序中都會有一份庫代碼的拷貝。如果在磁盤中存儲這些可執行程序,那就會佔用額外的磁盤空 間;另外如果拿它們放到Linux系統上一起運行,也會浪費掉寶貴的物理內存。如果將靜態庫換成動態庫,那麼這些問題都不會出現。動態庫在發揮作用的過程 中,必須經過兩個步驟:

a) 鏈接編輯器(link editor)拿它和其他Relocatable object file以及其他shared object file作爲輸入,經鏈接處理後,生存另外的 shared object file 或者 executable file。  
b)在運行時,動態鏈接器(dynamic linker)拿它和一個Executable file以及另外一些 Shared object file 來一起處理,在Linux系統裏面創建一個進程映像。

這裏我們主要是以Shared Object File(.so)爲重點分析對象,因爲我們在逆向APK中會遇到的絕大部分都是此類文件。

2. ELF文件格式

首先,ELF文件格式提供了兩種視圖,分別是鏈接視圖和執行視圖。

這裏寫圖片描述

鏈接視圖是以節(section)爲單位,執行視圖是以段(segment)爲單位。鏈接視圖就是在鏈接時用到的視圖,而執行視圖則是在執行時用到的視圖。上圖左側的視角是從鏈接來看的,右側的視角是執行來看的。總個文件可以分爲四個部分:

- ELF header: 描述整個文件的組織。
- Program Header Table: 描述文件中的各種segments,用來告訴系統如何創建進程映像的。
- sections 或者 segments:segments是從運行的角度來描述elf文件,sections是從鏈接的角度來描述elf文件,也就是說,在鏈接階段,我們可以忽略program header table來處理此文件,在運行階段可以忽略section header table來處理此程序(所以很多加固手段刪除了section header table)。從圖中我們也可以看出,segments與sections是包含的關係,一個segment包含若干個section。
- Section Header Table: 包含了文件各個segction的屬性信息,我們都將結合例子來解釋。

這裏寫圖片描述

程序頭部表(Program Header Table),如果存在的話,告訴系統如何創建進程映像。 
節區頭部表(Section Header Table)包含了描述文件節區的信息,比如大小、偏移等。

如下圖,可以通過執行命令”readelf -S android_server”來查看該可執行文件中有哪些section。 
這裏寫圖片描述

通過執行命令readelf –segments android_server,可以查看該文件的執行視圖。 
這裏寫圖片描述

這驗證了第一張圖中所述,segment是section的一個集合,sections按照一定規則映射到segment。那麼爲什麼需要區分兩種不同視圖?

當ELF文件被加載到內存中後,系統會將多個具有相同權限(flg值)section合併一個segment。操作系統往往以頁爲基本單位來管理內存分配,一般頁的大小爲4096B,即4KB的大小。同時,內存的權限管理的粒度也是以頁爲單位,頁內的內存是具有同樣的權限等屬性,並且操作系統對內存的管理往往追求高效和高利用率這樣的目標。ELF文件在被映射時,是以系統的頁長度爲單位的,那麼每個section在映射時的長度都是系統頁長度的整數倍,如果section的長度不是其整數倍,則導致多餘部分也將佔用一個頁。而我們從上面的例子中知道,一個ELF文件具有很多的section,那麼會導致內存浪費嚴重。這樣可以減少頁面內部的碎片,節省了空間,顯著提高內存利用率。

3. ELF Header

首先,我們先來看下32位ELF文件中常用的數據格式: 
這裏寫圖片描述 
然後我們來觀察一下ELF Header的結構體:

#define EI_NIDENT 16
typedef struct {
       unsigned char e_ident[EI_NIDENT];
       ELF32_Half e_type;
       ELF32_Half e_machine;
       ELF32_Word e_version;
       ELF32__Addr e_entry;
       ELF32_Off e_phoff;
       ELF32_Off e_shoff;
       ELF32_Word e_flags;
       ELF32_Half e_ehsize;
       ELF32_Half e_phentsize;
       ELF32_Half e_phnum;
       ELF32_Half e_shentsize;
       ELF32_Half e_shnum;
       ELF32_Half e_shstrndx;
}Elf32_Ehdr;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

接着運行readelf -h android_server命令,可以看到ELF Header結構的內容。

這裏寫圖片描述

或者使用010Editor的ELF模板也可以看到ELF Header結構。對比以下三類ELF文件,我們得到了以下結論: 
(1)e_type標識了文件類型 
(2)Relocatable File(.o文件)不需要執行,因此e_entry字段爲0,且沒有Program Header Table等執行視圖 
(3)不同類型的ELF文件的Section也有較大區別,比如只有Relocatable File有.strtab節。 
這裏寫圖片描述 
Shared Object File(.so文件)

這裏寫圖片描述 
Executable File(可執行文件android_server)

這裏寫圖片描述 
Relocatable File(.o文件)

在ELF Header中我們需要重點關注以下幾個字段:

  1. e_entry:程序入口地址 
    這 個sum.o的進入點是0x0(e_entry),這表面Relocatable objects不會有程序進入點。所謂程序進入點是指當程序真正執行起來的時候,其第一條要運行的指令的運行時地址。因爲Relocatable objects file只是供再鏈接而已,所以它不存在進入點。而可執行文件test和動態庫.so都存在所謂的進入點,且可執行文件的e_entry指向C庫中的_start,而動態庫.so中的進入點指向 call_gmon_start。 
    如上圖中e_entry = 0xD8B0,我們用ida打開該文件看到確實是_start()函數的地址。 
    這裏寫圖片描述

  2. e_ehsize:ELF Header結構大小

  3. e_phoff、e_phentsize、e_phnum:描述Program Header Table的偏移、大小、結構。

  4. e_shoff、e_shentsize、e_shnum:描述Section Header Table的偏移、大小、結構。

  5. e_shstrndx:這一項描述的是字符串表在Section Header Table中的索引,值25表示的是Section Header Table中第25項是字符串表(String Table)。

4. Section Header Table

一個ELF文件中到底有哪些具體的 sections,由包含在這個ELF文件中的 section head table(SHT)決定。在SHT中,針對每一個section,都設置有一個條目(entry),用來描述對應的這個section,其內容主要包括該 section 的名稱、類型、大小以及在整個ELF文件中的字節偏移位置等等。我們也可以在TISCv1.2規範中找到SHT表中條目的C結構定義: 
這裏寫圖片描述

解析android_server 可執行ELF文件,我們可以看到Section Header Table中確實有23(17h)個條目,且索引爲22(16h)確實爲section header section string table。 
這裏寫圖片描述

打開條目,我們可以看到每個entry的具體字段,與上圖的Elf32_Shdr結構一致。 
這裏寫圖片描述

需要注意的是,sh_name值實際上是.shstrtab中的索引,該string table中存儲着所有section的名字。下圖中藍色部分是.shstrtab的數據,我們可以看到,sh_name實際上是從索引1開始的”.shstrtab”字符串,因此這裏的sh_name值爲1h。 
這裏寫圖片描述

5. Section

下面我們分析一些so文件中重要的Section,包括符號表、重定位表、GOT表等。

-符號表(.dynsym)

符號表包含用來定位、重定位程序中符號定義和引用的信息,簡單的理解就是符號表記錄了該文件中的所有符號,所謂的符號就是經過修飾了的函數名或者變量名,不同的編譯器有不同的修飾規則。例如符號_ZL15global_static_a,就是由global_static_a變量名經過修飾而來。

符號表項的格式如下: 

typedef struct {  
     Elf32_Word st_name;      //符號表項名稱。如果該值非0,則表示符號名的字
                                             //符串表索引(offset),否則符號表項沒有名稱。
     Elf32_Addr st_value;       //符號的取值。依賴於具體的上下文,可能是一個絕對值、一個地址等等。
     Elf32_Word st_size;         //符號的尺寸大小。例如一個數據對象的大小是對象中包含的字節數。
     unsigned char st_info;    //符號的類型和綁定屬性。
     unsigned char st_other;  //未定義。
     Elf32_Half st_shndx;        //每個符號表項都以和其他節區的關係的方式給出定義。
             //此成員給出相關的節區頭部表索引。
} Elf32_sym; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

下面是通過010Editor解析出的符號表.dynsym的section header表項: 
這裏寫圖片描述

再來看一下符號表的具體內容: 
這裏寫圖片描述

-字符串表(.dynstr)

上面我們提到,符號表的st_name是符號名的字符串表中的索引,那麼字符串表中肯定存放着所有符號的名稱字符串。下面,我們先來看一看字符串表的section header表項: 
這裏寫圖片描述

再看一下下圖中字符串表的具體內容,我們可以看出,.dynstr和.shstrtab結構完全相同,不過一個存儲的是符號名稱的字符串,而另一個是Section名稱的字符串。 
這裏寫圖片描述

-重定位表

重定位表在ELF文件中扮演很重要的角色,首先我們得理解重定位的概念,程序從代碼到可執行文件這個過程中,要經歷編譯器,彙編器和鏈接器對代碼的處理。然而編譯器和彙編器通常爲每個文件創建程序地址從0開始的目標代碼,但是幾乎沒有計算機會允許從地址0加載你的程序。如果一個程序是由多個子程序組成的,那麼所有的子程序必需要加載到互不重疊的地址上。重定位就是爲程序不同部分分配加載地址,調整程序中的數據和代碼以反映所分配地址的過程。簡單的言之,則是將程序中的各個部分映射到合理的地址上來。 
換句話來說,重定位是將符號引用與符號定義進行連接的過程。例如,當程序調用了一個函數時,相關的調用指令必須把控制傳輸到適當的目標執行地址。 
具體來說,就是把符號的value進行重新定位。

可重定位文件必須包含如何修改其節區內容的信息,從而允許可執行文件和共享目標文件保存進程的程序映象的正確信息。這就是重定位表項做的工作。重定位表項的格式如下:

typedef struct {  
    Elf32_Addr r_offset;     //重定位動作所適用的位置(受影響的存儲單位的第一個字節的偏移或者虛擬地址)
    Elf32_Word r_info;       //要進行重定位的符號表索引,以及將實施的重定位類型(哪些位需要修改,以及如何計算它們的取值)
                                         //其中 .rel.dyn 重定位類型一般爲R_386_GLOB_DAT和R_386_COPY;.rel.plt爲R_386_JUMP_SLOT
} Elf32_Rel; 
 ```
 ```
typedef struct {  
    Elf32_Addr r_offset;  
    Elf32_Word r_info;  
    Elf32_Word r_addend;
 } Elf32_Rela; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

對 r_info 成員使用 ELF32_R_TYPE 宏運算可得到重定位類型,使用 ELF32_R_SYM 宏運算可得到符號在符號表裏的索引值。 三種宏的具體定義如下:

#define ELF32_R_SYM(i) ((i)>>8) 
#define ELF32_R_TYPE(i) ((unsigned char)(i)) 
#define ELF32_R_INFO(s, t) (((s)
  • 1
  • 2
  • 3

再看一下重定位表中的內容。

這裏寫圖片描述

以下是.rel.plt表的具體內容: 
這裏寫圖片描述

我們可以看到,每8個字節(s_entsize)一個表項。第一個表項中的r_offset值爲0xc7660,r_info爲0xa16。其中r_offset指向下圖中GOT表中第一項__imp_clock_gettime外部函數地址。那麼我們如何利用r_offset值來找到其對應的符號呢?如上所述,進行 ELF32_R_SYM宏運算實際上就是將r_info右移8位,0xa16右移8位得到0xa,因此這就是其在符號表中的索引。

這裏寫圖片描述

從下圖中可以看見符號表的s_entsize值爲10h,即16個字節每條目。因此我們可以找到其索引爲0xa的條目的st_name值爲0x9ea。那麼怎麼證明我們確實找到的是clock_gettime函數的符號呢?我們再來看一下st_name值是不是正確的。

這裏寫圖片描述

st_name值表示的是符號名字符串中的第一個字符在字符串表中的偏移量,因此我們用0x9ea加上符號表的起始位置(0x7548)就能得到該字符串在‭0x7F32位置。如下圖所示。 
這裏寫圖片描述 
‬ 
-常見的重定位表類型:

  • .rel.text:重定位的地方在.text段內,以offset指定具體要定位位置。在鏈接時候由鏈接器完成。.rel.text屬於普通重定位輔助段 ,他由編譯器編譯產生,存在於obj文件內。連接器連接時,他用於最終可執行文件或者動態庫的重定位。通過它修改原obj文件的.text段後,合併到最終可執行文件或者動態文件的.text段。其類型一般爲R_386_32和R_386_PC32。

  • .rel.dyn:重定位的地方在.got段內。主要是針對外部數據變量符號。例如全局數據。重定位在程序運行時定位,一般是在.init段內。定位過程:獲得符號對應value後,根據rel.dyn表中對應的offset,修改.got表對應位置的value。另外,.rel.dyn 含義是指和dyn有關,一般是指在程序運行時候,動態加載。區別於rel.plt,rel.plt是指和plt相關,具體是指在某個函數被調用時候加載。我個人理解這個Section的作用是,在重定位過程中,動態鏈接器根據r_offset找到.got對應表項,來完成對.got表項值的修改。

.rel.dyn和.rel.plt是動態定位輔助段。由連接器產生,存在於可執行文件或者動態庫文件內。藉助這兩個輔助段可以動態修改對應.got和.got.plt段,從而實現運行時重定位。

  • .rel.plt:重定位的地方在.got.plt段內(注意也是.got內,具體區分而已)。 主要是針對外部函數符號。一般是函數首次被調用時候重定位。首次調用時會重定位函數地址,把最終函數地址放到.got內,以後讀取該.got就直接得到最終函數地址。我個人理解這個Section的作用是,在重定位過程中,動態鏈接器根據r_offset找到.got對應表項,來完成對.got表項值的修改。

  • .plt段(過程鏈接表):所有外部函數調用都是經過一個對應樁函數,這些樁函數都在.plt段內。具體調用外部函數過程是: 
    調用對應樁函數—>樁函數取出.got表表內地址—>然後跳轉到這個地址.如果是第一次,這個跳轉地址默認是樁函數本身跳轉處地址的下一個指令地址(目的是通過樁函數統一集中取地址和加載地址),後續接着把對應函數的真實地址加載進來放到.got表對應處,同時跳轉執行該地址指令.以後樁函數從.got取得地址都是真實函數地址了。 
    下圖是.plt某表項,它包含了取.got表地址和跳轉執行兩條指令。 
    這裏寫圖片描述

  • .got(全局偏移表)

6. Program Header Table

程序頭部(Program Header)描述與程序執行直接相關的目標文件結構信息。用來在文件中定位各個段的映像。同時包含其他一些用來爲程序創建映像所必須的信息。 
可執行文件或者共享目標文件的程序頭部是一個結構數組,每個結構描述了一個段或者系統準備程序執行所必須的其他信息。目標文件的“段”包含一個或者多個“節區”,也就是“段內容(Segment Contents)”。程序頭部僅對可執行文件和共享目標文件有意義。

程序頭部的數據結構如下:

typedef struct {  
    Elf32_Word p_type;           //此數組元素描述的段的類型,或者如何解釋此數組元素的信息。 
    Elf32_Off  p_offset;           //此成員給出從文件頭到該段第一個字節的偏移
    Elf32_Addr p_vaddr;         //此成員給出段的第一個字節將被放到內存中的虛擬地址
    Elf32_Addr p_paddr;        //此成員僅用於與物理地址相關的系統中。System V忽略所有應用程序的物理地址信息。
    Elf32_Word p_filesz;         //此成員給出段在文件映像中所佔的字節數。可以爲0。
    Elf32_Word p_memsz;     //此成員給出段在內存映像中佔用的字節數。可以爲0。
    Elf32_Word p_flags;         //此成員給出與段相關的標誌。
    Elf32_Word p_align;        //此成員給出段在文件中和內存中如何對齊。
} Elf32_phdr;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

我們看到,以下兩個工具確實是照此格式解析的。

這裏寫圖片描述

這裏寫圖片描述


7. 參考鏈接

1.http://www.choudan.net/2013/11/16/Linux%E8%BF%9B%E7%A8%8B%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4%E5%86%8D%E5%AD%A6%E4%B9%A0.html

2.http://www.choudan.net/2013/10/25/Linux%E8%BF%9B%E7%A8%8B%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4%E5%AD%A6%E4%B9%A0%28%E4%BA%8C%29.html Linux進程地址空間學習

3.http://blog.chinaunix.net/uid-52437-id-3029374.html linux下elf重定位理解


轉載自:http://www.tqcto.com/article/mobile/144165.html

發佈了30 篇原創文章 · 獲贊 9 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章