天天看點

Linux逆向---ELF符号和重定位1.ELF符号2.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的解釋了。

elf