1. 程式人生 > >Linux逆向---ELF格式分析之檔案頭和程式頭

Linux逆向---ELF格式分析之檔案頭和程式頭

在Linux下,可以利用vim編輯器來對編譯生成後的可執行程式進行編輯,比如說把75jne指令改成74je指令,這樣可以在不重新編譯的情況下去修改程式的控制流,這樣玩感覺還是很有意思的,不過也僅限於此,所以我借了一本書想要學學逆向。。結果發現這本書真的難啃。。如果只是讀它的內容的話很快就讀過去了,但是會發現讀完之後自己還是什麼都不知道,於是我決定慢慢讀,並且用例子去對照著看,感覺這樣或許會有些效果。

這裡我的系統是64位Ubuntu,32位和64位的可執行程式的十六進位制表示還是有一些區別的,所以這裡有必要說明一下,很顯著的一個特徵就是32位中用4個位元組表示的東西,這裡需要用8個位元組來進行表示。

1.原始碼:

這裡為了簡單期間,我實現了一個helloworld。。然後用它編譯後的程式來進行之後的分析。

hello.c:

#include <stdio.h>
int main()
{
    printf("hello world");
    return 0;
}

編譯生成hello.out

gcc hello.c -o hello.out

2.檔案頭

1.檢視檔案頭資訊:

readelf -h hello.out

輸出:

ELF 頭:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  類別:                              ELF64
  資料:                              2 補碼,小端序 (
little endian) 版本: 1 (current) OS/ABI: UNIX - System V ABI 版本: 0 型別: EXEC (可執行檔案) 系統架構: Advanced Micro Devices X86-64 版本: 0x1 入口點地址: 0x400430 程式頭起點: 64 (
bytes into file) Start of section headers: 6616 (bytes into file) 標誌: 0x0 本頭的大小: 64 (位元組) 程式頭大小: 56 (位元組) Number of program headers: 9 節頭大小: 64 (位元組) 節頭數量: 31 字串表索引節頭: 28

2.檢視十六進位制格式:

hexedit hello.out

輸出

00000000   7F 45 4C 46  02 01 01 00  00 00 00 00  00 00 00 00  .ELF............
00000010   02 00 3E 00  01 00 00 00  30 04 40 00  00 00 00 00  ..>[email protected]
00000020   40 00 00 00  00 00 00 00  D8 19 00 00  00 00 00 00  @...............
00000030   00 00 00 00  40 00 38 00  09 00 40 00  1F 00 1C 00  [email protected]@.....

通過man 5 ELF對ELF手冊的檢視,可以知道頭部可以使用一個結構體表示:

#define EI_NIDENT 16
typedef struct {
	unsigned char e_ident[EI_NIDENT];
	uint16_t      e_type;
	uint16_t      e_machine;
    uint32_t      e_version;
    ElfN_Addr     e_entry;
    ElfN_Off      e_phoff;
    ElfN_Off      e_shoff;
    uint32_t      e_flags;
    uint16_t      e_ehsize;
    uint16_t      e_phentsize;
    uint16_t      e_phnum;
    uint16_t      e_shentsize;
    uint16_t      e_shnum;
    uint16_t      e_shstrndx;
} ElfN_Ehdr;

3.分析

e_ident:

第一行為ELF頭,共16個位元組 ,表示了e_ident

  • 0~4:MAGIC

  • 5: 02表示64位檔案,若為1則為32位,否則都不是

  • 6: 01表明是小端編碼,為2則為大端編碼

  • 7: 01檔案版本,1表明是當前版本

  • 8~16: 暫時未用到,用於以後擴充套件

e_type

02 00 實際上應該為00 02,後面的部分看的時候也需要轉換一下,即數值2,表明為可執行檔案,其他的數值及型別對應關係在man手冊中也都能查到。

e_machine

003E 為體系架構,

e_version

0001 為當前版本,也就是數值1

e_entry

30 04 40 00 00 00 00 00 ->00 00 00 00 00 40 04 30 即0x400430,為程式入口地址

e_phoff

0000 0000 0000 0040程式頭起點 0x40=64byte

e_shoff

0000 0000 0000 19D8節頭起點 0x19d8=6616

e_flags

0000 0000 標誌:0x0

e_ehsize

0040 ELF頭長度 0x40=64

e_phentsize

0038 程式頭長度 0x38=56

e_phnum

0009 程式頭表的專案數量 9

e_shentsize

0040 節頭表的專案大小 0x40=64位元組

e_shnum

001F 節頭數量0x1f=31

e_shstrndx

001C 字串索引節頭 0x1c=28

3.程式頭例項:

程式頭對段的描述,是程式裝載必需的一部分。

1.檢視程式頭表:

使用如下命令:

readelf -l hello.out

輸出:

Elf 檔案型別為 EXEC (可執行檔案)
入口點 0x400430
共有 9 個程式頭,開始於偏移量 64

程式頭:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000006fc 0x00000000000006fc  R E    200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000005d0 0x00000000004005d0 0x00000000004005d0
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  段節...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got

這個表中的每一項都是對應著結構體Elf64_Phdr的:

           typedef struct {
               uint32_t   p_type;
               uint32_t   p_flags;
               Elf64_Off  p_offset;
               Elf64_Addr p_vaddr;
               Elf64_Addr p_paddr;
               uint64_t   p_filesz;
               uint64_t   p_memsz;
               uint64_t   p_align;
           } Elf64_Phdr;

2.對各段的觀察

PHDR:

在例項中的地址為0x40~0x238,可以用hexedit去觀察它的內容,不過基本都是不可見字元,所以寫出來沒什麼意義。

PHDR段儲存了程式頭表本身的位置和大小,程式頭表則儲存了所有的程式頭對檔案中段的描述資訊。

這裡我們可以簡單的進行一個計算,這個結構體所佔空間為4+4+8+8+8+8+8+8=56,一共9個專案,所以

hex(56)+0x40=0x238,這也足以說明這一段的含義。

INTERP:

對程式直譯器位置的描述 0x238-0x254,這一段可以用hexedit觀察到內容:

從可見字元可知,這個程式的程式直譯器為:/lib64/ld-linux-x86-64.so.2

									2F 6C 69 62  36 34 2F 6C  ......../lib64/l
00000240  64 2D 6C 69  6E 75 78 2D  78 38 36 2D  36 34 2E 73  d-linux-x86-64.s
00000250  6F 2E 32 00  										  o.2.				   

LOAD1:

程式程式碼段: 0x000-0x6fc,這裡內容太多就不貼出來了,主要是一些機器指令。

LOAD2:

資料段:0xe10-0x1038,這裡大多數是二進位制資料,全部貼出來也沒有什麼意義

DYNAMIC

動態段:0xe28~0xff8

動態段包含了動態連結器所必需的一些資訊,在動態段中包含了一些標記和指標。包括執行時所需要的共享庫列表、全域性偏移表的地址,以及重定位條目的相關資訊等。

NOTE:

0x254-0x298,儲存了與特定供應商或者系統相關的附加資訊,實際的可執行檔案執行時並不需要這個段,可以檢視一下這一段的內容。

						04 00 00 00  10 00 00 00  01 00 00 00  o.2.............
00000260   47 4E 55 00  00 00 00 00  02 00 00 00  06 00 00 00  GNU.............
00000270   20 00 00 00  04 00 00 00  14 00 00 00  03 00 00 00   ...............
00000280   47 4E 55 00  3D F4 DD 79  8B A7 5D 1A  69 C7 CD C9  GNU.=..y..].i...
00000290   1E E5 C1 CD  69 A2 C8 F4

GNU_*:

這部分似乎並不怎麼被關注,並且man手冊中提的也很少,所以先忽略掉。

3.分析

通過對以上的地址觀察,我們可以得到一些結論:

  • 執行所需的程式部分總體上可以看做是程式碼段和資料段組成的。
  • PHDR、INTERP、NOTE段被包含在了程式碼段中。
  • DYNAMIC段被包含在了資料段中。

4.程式的剩餘部分

剩餘地址的部分為對ELF節頭的描述,如果去掉,程式仍然可以正常執行,但是無法利用節頭來引用節,預設是有節頭的,這裡我也是試驗了一下,因為我分析的檔案就是一個可執行檔案,資料段停止於0x1038,於是我將0x1038之後的所有位元組全部清除掉,這裡我做了一個副本hello1.out,然後用vim+:%!xxd做到的直接對二進位制檔案進行編輯。編輯之後再檢視差不多是這個樣子:

pic2

然後執行一下編輯之後的程式:

pic3

可以來檢視一下編輯後的hello1.out的大小,也發生了變化:

pic4

可見程式仍可執行,而我們用指令去查詢這個新程式的節頭表時,會出現以下的現象:

pic5

可見,這個程式的確找不到節頭表了,儘管沒有節頭可執行程式仍然可以執行,但是這樣的程式會讓gdb、objdump這樣的工具沒法排上用場,也會對逆向造成極大的障礙。