1. 程式人生 > >Linux逆向---ELF符號和重定位

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的解釋了。