Linux逆向---ELF符號和重定位
1.ELF符號
符號是對某些型別的資料或者程式碼(全域性變數、函式等)的符號引用,例如printf函式會在動態符號表.dynsym中存有一個指向該函式的符號條目。
.dynsym儲存了引用來自外部檔案符號的全域性符號,.symtab中還儲存了可執行檔案的本地符號,如全域性變數等,.dynsym儲存的符號是.symtab儲存的符號的子集。
.dynsym在程式執行時會被分配並裝載進記憶體,主要用於動態連結可執行檔案的執行。而.symtab則不會,它主要用來進行除錯和連結的。
ELF檔案符號項的結構體如下:
typedef struct {
uint32_t st_name; //儲存了指向符號表中字串表的偏移地址(.dynstr或.strtab)
unsigned char st_info; //制定符號型別及繫結屬性
unsigned char st_other; //定義符號的可見性
uint16_t st_shndx; //每個符號表的條目的定義都與某些節對應,該變數儲存了相關節頭表的索引
Elf64_Addr st_value; //存放符號的值(地址或者位置偏移量)
uint64_t st_size; //存放符號的大小
} Elf64_Sym;
這裡我們可以觀察一個例項:
1.1.symtab符號表分析
首先用readelf -S檢視節資訊:
[29] .symtab SYMTAB 0000000000000000 00001070
0000000000000648 0000000000000018 30 47 8
[30] .strtab STRTAB 0000000000000000 000016b8
0000000000000216 0000000000000000 0 0 1
然後再用hexedit來檢視程式程式碼:
.symtab節:(這裡沒有從0x1070頂頭擷取,0x1310是第28項的起始)
00001310 01 00 00 00 04 00 F1 FF 00 00 00 00 00 00 00 00 ................
00001320 00 00 00 00 00 00 00 00 0C 00 00 00 01 00 15 00 ................
00001330 20 0E 60 00 00 00 00 00 00 00 00 00 00 00 00 00 .`.............
00001340 19 00 00 00 02 00 0E 00 60 04 40 00 00 00 00 00 ........` [email protected]
00001350 00 00 00 00 00 00 00 00 1B 00 00 00 02 00 0E 00 ................
00001360 A0 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00 [email protected]
00001370 2E 00 00 00 02 00 0E 00 E0 04 40 00 00 00 00 00 [email protected]
.strtab節:
00 63 72 74 73 74 75 66 .........crtstuf
000016C0 66 2E 63 00 5F 5F 4A 43 52 5F 4C 49 53 54 5F 5F f.c.__JCR_LIST__
000016D0 00 64 65 72 65 67 69 73 74 65 72 5F 74 6D 5F 63 .deregister_tm_c
000016E0 6C 6F 6E 65 73 00 5F 5F 64 6F 5F 67 6C 6F 62 61 lones.__do_globa
000016F0 6C 5F 64 74 6F 72 73 5F 61 75 78 00 63 6F 6D 70 l_dtors_aux.comp
00001700 6C 65 74 65 64 2E 37 35 39 34 00 5F 5F 64 6F 5F leted.7594.__do_
00001710 67 6C 6F 62 61 6C 5F 64 74 6F 72 73 5F 61 75 78 global_dtors_aux
我們可以計算一下:
每一個符號項的大小為:4+1+1+2+8+8=24
則這一段程式碼中包含的符號項數量為:0x648/24=67項,然後我們用如下命令檢視一波符號表:
readelf -s hello.out
輸出為:
Symbol table '.symtab' contains 67 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000400238 0 SECTION LOCAL DEFAULT 1
2: 0000000000400254 0 SECTION LOCAL DEFAULT 2
證明我們的計算是正確的。
拿.dynsym中的第三個符號項為例子分析一下:
19 00 00 00 02 00 0E 00 60 04 40 00 00 00 00 00 00 00 00 00 00 00 00 00
首先知道偏移量為0x19,這樣的話它的字串在.strtab中的位置就是:0x19+0x16B8=0x16D1,結束位置為下一個0x00所在位置,即0x16E5
即:“deregister_tm_clones”,它的st_info欄位的值為0x02,st_other欄位的值為0x00,st_shndex的值為0x0E,st_value的值為0x400460,st_size的值為0x00。接下來我們再列印一波符號表然後看看第31項:
30: 0000000000400460 0 FUNC LOCAL DEFAULT 14 deregister_tm_clones
看來與我們的計算也是相同的。
1.2.dyntab符號表分析
剛才分析的表的地址已經超出了資料段和程式碼段的地址了,這也說明程式執行時並不會把他們載入進記憶體,所以如果我們要分析與程式執行相關的符號表就要看.dyntab表,首先檢視節資訊:
[ 5] .dynsym DYNSYM 00000000004002b8 000002b8
0000000000000060 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 0000000000400318 00000318
000000000000003f 0000000000000000 A 0 0 1
然後找出對應的內容:
.dyntab:
00 00 00 00 00 00 00 00 ................
000002C0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000002D0 0B 00 00 00 12 00 00 00 00 00 00 00 00 00 00 00 ................
000002E0 00 00 00 00 00 00 00 00 12 00 00 00 12 00 00 00 ................
000002F0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000300 24 00 00 00 20 00 00 00 00 00 00 00 00 00 00 00 $... ...........
00000310 00 00 00 00 00 00 00 00
.dynstr:
00 6C 69 62 63 2E 73 6F .........libc.so
00000320 2E 36 00 70 72 69 6E 74 66 00 5F 5F 6C 69 62 63 .6.printf.__libc
00000330 5F 73 74 61 72 74 5F 6D 61 69 6E 00 5F 5F 67 6D _start_main.__gm
00000340 6F 6E 5F 73 74 61 72 74 5F 5F 00 47 4C 49 42 43 on_start__.GLIBC
00000350 5F 32 2E 32 2E 35 00 00 00 00 02 00 02 00 00 00 _2.2.5..........
這裡首先也簡單計算一下吧,一共長度為0x60位元組,每項24位元組大小,共有0x60/24=4項,可以列印一下符號表來驗證一下:
Symbol table '.dynsym' contains 4 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [email protected]_2.2.5 (2)
2: 0000000000000000 0 FUNC GLOBAL DEFAULT UND [email protected]_2.2.5 (2)
3: 0000000000000000 0 NOTYPE WEAK DEFAULT UND __gmon_start__
說明計算是正確的。
這裡還是用第三項來分析,第三項的程式碼為:
12 00 00 00 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
偏移量為0x12,即strtab中的0x32a,一直到0x33B,也就是如下字元:"__libc_start_main",它的 st_info欄位為0x12,其餘的欄位都是0。
但是現在可以發現一個有趣的現象,那就是符號表列印的並不是"__libc_start_main",而是是"[email protected]_2.2.5 (2)",
我想這也與st_info欄位的取值有關,這個欄位決定了列印結果中的兩列的取值—type與bind。
符號型別type有如下幾種:
-
STT_NOTYPE 符號未定義
-
STT_FUNC 表示該符號與函式或者其他可執行程式碼關聯
-
STT_OBJECT 表明該符號與資料目標檔案關聯
符號繫結bind有如下幾種:
-
STB_LOCAL 本地符號,在目標檔案之外都是不可見的,如一個宣告為static的函式
-
STB_GLOBAL 全域性符號,對於所有要合併的目標檔案來說都是可見的
-
STB_WEAK 與全域性繫結類似,不過比STB_GLOBAL優先順序低,甚至可能會被同名的未標記為STB_WEAK的符號覆蓋
所以我覺得應該是因為這兩個符號是全域性符號的原因,所以需要在後綴新增上這些資訊。
2.ELF重定位
重定位就是將符號定義和符號引用進行連線的過程,包括描述如何修改節內容的相關資訊,從而使得可執行檔案和共享目標檔案能夠儲存程序的程式映象所需的正確資訊。重定位條目就是我們上面說的相關資訊。
重定位記錄儲存瞭如何對給定的符號的對應程式碼進行補充的相關資訊,重定位實際上是一種給二進位制檔案打補丁的機制。
簡單點來說,就是兩個目標檔案輸出可執行檔案之前,是無法確定各自符號和程式碼在記憶體中的位置的,而重定位之後,目標檔案中的程式碼會被重定位到可執行檔案段中的一個給定的地址。
看一下64位的重定位條目:
typedef struct {
Elf64_Addr r_offset;
uint64_t r_info;
} Elf64
有的條目還需要append欄位:
typedef struct {
Elf64_Addr r_offset; //指向需要進行重定位操作的位置
uint64_t r_info; //指定必須對其進行重定位符號表索引以及要應用的重定位型別
int64_t r_addend; //制定常量加數,用於計算儲存在可重定位欄位中的值
} Elf64_Rela;
這裡我突然想起來之前分析節的時候看見的.rela.dyn、.rela.plt節:
[ 9] .rela.dyn RELA 0000000000400380 00000380
0000000000000018 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000400398 00000398
0000000000000030 0000000000000018 AI 5 24 8
我想這些部分與重定位密切相關。
到這裡我參考的書提到了隱式加數的概念,不過鑑於64位往往採用顯式儲存,所以這裡就不去探究了。
2.1.重定位項檢視
重定位項是可以直接檢視的,用如下命令:
readelf -r hello.out
得到如下的輸出:
重定位節 '.rela.dyn' 位於偏移量 0x380 含有 1 個條目:
偏移量 資訊 型別 符號值 符號名稱 + 加數
000000600ff8 000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0
重定位節 '.rela.plt' 位於偏移量 0x398 含有 2 個條目:
偏移量 資訊 型別 符號值 符號名稱 + 加數
000000601018 000100000007 R_X86_64_JUMP_SLO 0000000000000000 [email protected]_2.2.5 + 0
000000601020 000200000007 R_X86_64_JUMP_SLO 0000000000000000 [email protected]_2.2.5 + 0
這裡也可以簡單的進行一個計算:
每個重定位條目大小為3*8=24=0x18,所以一個條目是0x18,兩個條目就是0x30,我們再去分析一下對應地址的二進位制部分:
.dela.dyn對應的:
00000380 F8 0F 60 00 00 00 00 00 06 00 00 00 03 00 00 00 ..`.............
00000390 00 00 00 00 00 00 00 00
從這裡,我們可以看出0~8位元組為偏移量r_offset=0x600FF8,8 ~16位元組為資訊r_info=0x300000006,加數r_append=0x0,也正好與我們的條目相對應。
2.2.偏移計算
為了說明這個計算,首先我們需要生成一個目標檔案,原始碼依然用最簡單的hello world:
gcc -c hello.c
然後我們目錄下會出現這個hello.o,然後我們檢視一下它的指令部分:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: bf 00 00 00 00 mov $0x0,%edi
9: b8 00 00 00 00 mov $0x0,%eax
e: e8 00 00 00 00 callq 13 <main+0x13>
13: b8 00 00 00 00 mov $0x0,%eax
18: 5d pop %rbp
19: c3 retq
只有這麼一小部分,但是可以看到它的函式呼叫部分是callq 0x13,按我參考書上的說法,這是因為此時目標檔案並沒有printf函式的地址,而當生成可執行檔案時,連結器會對該位置進行修改,在printf函式被包含進可執行檔案時,連結器會通過偏移補齊4個位元組,這樣也就相當於儲存了foo的實際偏移地址,這裡再列印一下重定位表:
重定位節 '.rela.text' 位於偏移量 0x1f8 含有 2 個條目:
偏移量 資訊 型別 符號值 符號名稱 + 加數
000000000005 00050000000a R_X86_64_32 0000000000000000 .rodata + 0
00000000000f 000a00000002 R_X86_64_PC32 0000000000000000 printf - 4
重定位節 '.rela.eh_frame' 位於偏移量 0x228 含有 1 個條目:
偏移量 資訊 型別 符號值 符號名稱 + 加數
000000000020 000200000002 R_X86_64_PC32 0000000000000000 .text + 0
這裡可以通過簡單計算:0x13-4=0xF,也就是說第二個偏移條目printf的函式的地址會被新增到這個位置,也就是callq指令的部分,而這個等待被補全的地址剛好是四個位元組,也可以解釋加數4了。
接下來便是生成可執行檔案,然後看一下它重定位後的結果:
gcc hello.o -o hello.out
objdump -d hello.out
可以看到如下輸出:
0000000000400400 <[email protected]>:
400400: ff 25 12 0c 20 00 jmpq *0x200c12(%rip) # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
400406: 68 00 00 00 00 pushq $0x0
40040b: e9 e0 ff ff ff jmpq 4003f0 <_init+0x28>
........
0000000000400526 <main>:
400526: 55 push %rbp
400527: 48 89 e5 mov %rsp,%rbp
40052a: bf c4 05 40 00 mov $0x4005c4,%edi
40052f: b8 00 00 00 00 mov $0x0,%eax
400534: e8 c7 fe ff ff callq 400400 <[email protected]>
400539: b8 00 00 00 00 mov $0x0,%eax
40053e: 5d pop %rbp
40053f: c3 retq
可以看到,callq這裡的地址的確被替換掉了。
每種型別都有各自的計算方式,比如這個R_X86_64_PC32,被替換的值符合S+A-P的方式。
這裡S是呼叫printf函式地址指令所在的地址,即0x400535,A為.o檔案時列印重定位項的加數-4,P則是要進行重定位的儲存單元的地址,在這裡是printf函式所在的地址,即0x400400,所以偏移量就是 0x400535-4+0x400400=-0x139=0xFFFFFEC7。
所以這也是我之前做注入的小實驗時那個講究的4的解釋了。