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做到的直接對二進位制檔案進行編輯。編輯之後再檢視差不多是這個樣子:
然後執行一下編輯之後的程式:
可以來檢視一下編輯後的hello1.out的大小,也發生了變化:
可見程式仍可執行,而我們用指令去查詢這個新程式的節頭表時,會出現以下的現象:
可見,這個程式的確找不到節頭表了,儘管沒有節頭可執行程式仍然可以執行,但是這樣的程式會讓gdb、objdump這樣的工具沒法排上用場,也會對逆向造成極大的障礙。