Linux 二進制文件格式 ELF 入門

markdown 格式地址https://github.com/ForceInjection/linux-from-beginner-to-master/blob/main/elf_101.md

Linux 二進制文件格式 ELF 101

什麼是 ELF 文件?

ELF 是 可執行和可鏈接格式(Executable and Linkable Format)的縮寫,它定義了二進制文件、庫和核心文件的結構。正式的規範允許操作系統正確解釋其底層機器指令。ELF 文件通常是編譯器或鏈接器的輸出,並且是一種二進制格式。有了合適的工具,這樣的文件就可以被分析和更好地理解。

爲什麼要學習 ELF 的細節?

在深入更技術性的細節之前,解釋一下爲什麼理解 ELF 格式是有用的。首先,它有助於學習我們操作系統的內部工作方式。當出現問題時,我們可能能更好地理解發生了什麼(或爲什麼)。然後是能夠研究 ELF 文件的價值,特別是在安全漏洞或發現可疑文件之後。最後但並非最不重要的是,在開發過程中爲了更好地理解。即使大家使用像 Golang 這樣的高級語言編程,我們仍然可能從瞭解幕後發生的事情中受益。

那麼,爲什麼要學習更多關於 ELF 的知識呢?

從源代碼到進程

無論我們運行什麼操作系統,它都需要將通用功能轉換爲 CPU 的語言,也稱爲機器代碼。一個函數可能是一些基本的事情,比如在磁盤上打開一個文件或在屏幕上顯示一些東西。我們不是直接與 CPU 對話,而是使用編程語言,使用內部函數。然後編譯器將這些函數翻譯成目標代碼。這個目標代碼隨後被鏈接器工具鏈接成一個完整的程序。結果是一個二進制文件,然後可以在特定的平臺和 CPU 類型上執行。

注意事項

不要在生產系統上運行相關命令。最好在測試機器上覆制一個現有的二進制文件並使用相關命令。此外,我們提供了一個小型的 C 程序,我們可以編譯它來進行驗證。

相關工具安裝:

sudo yum install binutils
sudo yum install pax-utils
sudo yum install prelink

ELF 文件的解剖

一個常見的誤解是 ELF 文件只是用於二進制文件或可執行文件。我們已經看到它們可以用於部分片段(目標代碼)。另一個例子是共享庫或核心轉儲(那些核心或 a.out 文件)。ELF 規範甚至在 Linux 上用於內核本身和 Linux 內核模塊。

file test
test: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs)for GNU/Linux 2.6.32, BuildID[sha1]=e0e7da04675930bdf7bd0e9fb5a5e1701a8a32b9, not stripped

文件命令顯示了這個二進制文件的一些基本信息

文件結構

由於 ELF 文件的可擴展設計,每個文件的結構都不同。一個 ELF 文件由以下部分組成:

    1. ELF 頭部
    1. 文件數據

使用 readelf 命令,我們可以查看文件的結構,它看起來像這樣:

readelf -h /usr/bin/ps
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x402f83
  Start of program headers:          64 (bytes into file)
  Start of section headers:          98256 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28

ELF 二進制文件的細節

ELF 頭部

如上述命令輸出所示,ELF 頭部以一些魔術開始。這個 ELF 頭部魔術提供了關於文件的信息。前四個十六進制部分定義了這是一個 ELF 文件(45=E,4c=L,46=F),前綴是 7f 值。

這個 ELF 頭部是強制性的。它確保在鏈接或執行期間數據被正確解釋。爲了更好地理解 ELF 文件的內部工作方式,瞭解這個頭部信息是有用的。

類別

在 ELF 類型聲明之後,定義了一個類別字段。這個值決定了文件的架構。它可以是 32 位(=01)或 64 位(=02)架構。魔術顯示了一個 02,被 readelf 命令翻譯爲 ELF64 文件。換句話說,使用 64 位架構的 ELF 文件。

數據

下一部分是數據字段。它知道兩個選項:01 用於 LSB 最低有效位,也稱爲小端。然後是值 02,用於 MSB(最高有效位,大端)。這個特定值有助於正確解釋文件內剩餘的對象。這很重要,因爲不同類型的處理器以不同的方式處理傳入的指令和數據結構。在這種情況下,使用了 LSB,這對於 AMD64 類型的處理器來說是常見的。

LSB 的效果在使用 hexdump 對二進制文件進行操作時變得明顯。讓我們展示一下 /usr/bin/ps 的 ELF 頭部細節。

hexdump -n 16 /usr/bin/ps
0000000 457f 464c 0102 0001 0000 0000 0000 0000
0000010

我們可以看到值對是不同的,這是由於字節順序的正確解釋造成的。

版本

接下來是魔術中的另一個 “01”,這是版本號。目前,只有一個版本類型:當前版本,其值是 “01”。

OS/ABI

每個操作系統在公共功能上有很大一部分重疊。此外,它們都有特定的功能,或者至少在它們之間有細微的差別。正確的設置定義是通過 應用程序二進制接口 (ABI)。這樣,操作系統和應用程序都知道該期待什麼,函數被正確轉發。這兩個字段描述了使用了什麼 ABI 以及相關的版本。在這種情況下,值是 00,這意味着沒有使用特定的擴展。輸出顯示爲 System V。

ABI 版本

如果需要,可以指定 ABI 的版本。

機器

我們還可以在頭部找到預期的機器類型。

類型

類型 字段告訴我們文件的目的是什麼。有一些常見的文件類型。

查看完整標頭詳細信息

雖然一些字段已經可以通過 readelf 輸出的魔術值顯示,但還有更多。例如,文件是針對什麼特定的處理器類型。使用 hexdump 我們可以看到完整的 ELF 頭部及其值。

hexdump -C -n 64 /usr/bin/ps
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  83 2f 40 00 00 00 00 00  |..>....../@.....|
00000020  40 00 00 00 00 00 00 00  d0 7f 01 00 00 00 00 00  |@...............|
00000030  00 00 00 00 40 00 38 00  09 00 40 00 1d 00 1c 00  |....@.8...@.....|
00000040

上面的高亮字段是定義機器類型的地方。值 3e 是十進制的 62,等於 AMD64。要了解所有機器類型,可以查看這個 ELF 頭部文件。

雖然我們可以用十六進制轉儲做很多事情,但讓工具爲大家工作是有意義的。dumpelf 工具在這方面可以提供幫助。它顯示了一個格式化的輸出,非常類似於 ELF 頭部文件。非常適合學習哪些字段被使用以及它們的典型值。

澄清了所有這些字段之後,是時候看看真正的魔法發生在哪裏,進入下一個頭部了!

文件數據

除了 ELF 頭部,ELF 文件由三個部分組成。

在我們深入這些頭部之前,最好知道 ELF 有兩個互補的 “視圖”。一個用於鏈接器允許執行(段)。另一個用於對指令和數據進行分類(節)。因此,根據目標,使用相關的頭部類型。讓我們從程序頭部開始,我們在 ELF 二進制文件中找到它們。

程序頭

一個 ELF 文件由零個或多個Segments)組成,並描述瞭如何創建運行時執行的進程 / 內存映像。當內核看到這些段時,它使用它們將它們映射到虛擬地址空間,使用 mmap(2) 系統調用。換句話說,它將預定義的指令轉換爲內存映像。如果 ELF 文件是一個普通的二進制文件,它需要這些程序頭部。否則,它根本不能運行。它使用這些頭部和底層的數據結構來形成一個進程。對於共享庫,這個過程是類似的。

readelf -l /usr/bin/ps

Elf file type is EXEC (Executable file)
Entry point 0x402f83
There are 9 program headers, starting at offset 64

Program Headers:
  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
                 0x0000000000016844 0x0000000000016844  R E    200000
  LOAD           0x0000000000016de0 0x0000000000616de0 0x0000000000616de0
                 0x0000000000000640 0x00000000000217a8  RW     200000
  DYNAMIC        0x0000000000016df8 0x0000000000616df8 0x0000000000616df8
                 0x0000000000000200 0x0000000000000200  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000141fc 0x00000000004141fc 0x00000000004141fc
                 0x0000000000000784 0x0000000000000784  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000016de0 0x0000000000616de0 0x0000000000616de0
                 0x0000000000000220 0x0000000000000220  R      1

 Section to Segment mapping:
  Segment Sections...
   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 .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

ELF 二進制文件中的程序頭部概覽

我們在示例中看到有 9 個程序頭部。

PT_LOAD

這是最常見的段類型,用於定義需要加載到內存的部分。通常分爲兩個區域:一個只讀區域(通常包含代碼)和一個可讀寫區域(通常包含數據)。

PT_DYNAMIC

包含與動態鏈接有關的信息,例如共享庫的路徑、符號表等。動態鏈接器會使用這個段來加載動態庫。

PT_INTERP

如果程序需要由解釋器(例如動態鏈接器)執行,這個段會指定解釋器的路徑。

GNU_EH_FRAME

這是一個由 GNU C 編譯器(gcc)使用的排序隊列。它存儲異常處理程序。所以當出現問題時,它可以利用這個區域正確處理。

GNU_STACK

這個頭部用於存儲堆棧信息。堆棧是一個緩衝區,或臨時存放的地方,存儲像局部變量這樣的項目。這將以 LIFO(後進先出)的方式發生,類似於將箱子堆疊在一起。當一個進程函數開始時,會保留一個塊。當函數結束時,它將被標記爲再次空閒。現在有趣的部分是堆棧不應該是可執行的,因爲這可能會引入安全漏洞。通過操縱內存,人們可以引用這個可執行堆棧並運行預期的指令。

如果 GNU_STACK 段不可用,那麼通常使用可執行堆棧。scanelf 和 execstack 是兩個展示堆棧詳細信息的工具。

scanelf -e /usr/bin/ps
 TYPE   STK/REL/PTL FILE
ET_EXEC RW- R-- RW- /usr/bin/ps
execstack -q /usr/bin/ps
- /usr/bin/ps

查看程序頭部的命令

ELF 節(Section)

節頭部

節頭部定義了文件中的所有節。正如所說,這個 “視圖” 用於鏈接和重定位。

節可以在 ELF 二進制文件中找到,在 GNU C 編譯器將 C 代碼轉換爲彙編語言之後,然後是 GNU 彙編器,它創建了它的對象。

如例子所示,一個段可以有 0 個或多個節。對於可執行文件,有四個主要節:.text.data.rodata 和 .bss。每個這些節都以不同的訪問權限加載,這可以通過 readelf -S 看到。

readelf -S /usr/bin/ps
There are 29 section headers, starting at offset 0x17fd0:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000050  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002e8  000002e8
       0000000000000a20  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400d08  00000d08
       000000000000042d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000401136  00001136
       00000000000000d8  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000401210  00001210
       00000000000000a0  0000000000000000   A       6     3     8
  [ 9] .rela.dyn         RELA             00000000004012b0  000012b0
       00000000000000a8  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000401358  00001358
       0000000000000900  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         0000000000401c58  00001c58
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000401c80  00001c80
       0000000000000610  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000402290  00002290
       000000000000a4fa  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         000000000040c78c  0000c78c
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         000000000040c7a0  0000c7a0
       0000000000007a5a  0000000000000000   A       0     0     32
  [16] .eh_frame_hdr     PROGBITS         00000000004141fc  000141fc
       0000000000000784  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000414980  00014980
       0000000000001ec4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000616de0  00016de0
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000616de8  00016de8
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000616df0  00016df0
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000616df8  00016df8
       0000000000000200  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000616ff8  00016ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000617000  00017000
       0000000000000318  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000617318  00017318
       0000000000000108  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000617420  00017420
       0000000000021168  0000000000000000  WA       0     0     32
  [26] .gnu_debuglink    PROGBITS         0000000000000000  00017420
       0000000000000010  0000000000000000           0     0     4
  [27] .gnu_debugdata    PROGBITS         0000000000000000  00017430
       0000000000000a90  0000000000000000           0     0     1
  [28] .shstrtab         STRTAB           0000000000000000  00017ec0
       000000000000010d  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

.text

包含可執行代碼。它將被打包到一個具有讀寫權限的段中。它只加載一次,因爲內容不會改變。這可以通過 objdump 實用程序看到。

objdump -d /usr/bin/ps -j .text | head

/usr/bin/ps:     file format elf64-x86-64


Disassembly of section .text:

0000000000402290 <.text>:
  402290: 41 55                 push   %r13
  402292: be c0 c4 40 00        mov    $0x40c4c0,%esi
  402297: 41 54                 push   %r12

.data

初始化數據,具有讀寫訪問權限

.rodata

初始化數據,僅具有讀取訪問權限(=A)。

.bss

未初始化數據,具有讀寫訪問權限(=WA)

  [24] .data             PROGBITS         0000000000617318  00017318
       0000000000000108  0000000000000000  WA       0     0     8
  [25] .bss              NOBITS           0000000000617420  00017420
       0000000000021168  0000000000000000  WA       0     0     32

查看節和頭部的命令

節組

一些節可以被分組,因爲它們形成一個整體,或者換句話說是一個依賴關係。較新的鏈接器支持這個功能。儘管如此,這並不常見於發現這種情況:

readelf -g /usr/bin/ps

There are no section groups in this file.

雖然這看起來可能不太有趣,但它清楚地表明瞭研究可用的 ELF 工具包的好處,用於分析。因此,在本文的末尾包含了工具及其主要目標的概述。

靜態 vs. 動態二進制文件

在處理 ELF 二進制文件時,瞭解有兩種類型以及它們的鏈接方式是很好的。類型是靜態的還是動態的,指的是所使用的庫。出於優化目的,我們經常看到二進制文件是 “動態的”,這意味着它需要外部組件才能正確運行。通常這些外部組件是正常的庫,包含像打開文件或創建網絡套接字這樣的公共函數。另一方面,靜態二進制文件包含了所有庫。這使它們變得更大,但更具可移植性(例如,在另一個系統上使用它們)。

如果我們想檢查一個文件是靜態編譯還是動態編譯,使用 file 命令。如果它顯示類似的東西:

file /usr/bin/ps
/usr/bin/ps: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs)for GNU/Linux 2.6.32, BuildID[sha1]=36fb21d67d92790800dc4212bd9f610602e3dc08, stripped

要確定使用了哪些外部庫,只需對同一二進制文件使用 ldd:

ldd /usr/bin/ps
 linux-vdso.so.1 (0x00007fff72f72000)
 libprocps.so.4 => /lib64/libprocps.so.4 (0x00007f4e1e354000)
 libsystemd.so.0 => /lib64/libsystemd.so.0 (0x00007f4e1e123000)
 libdl.so.2 => /lib64/libdl.so.2 (0x00007f4e1df1f000)
 libc.so.6 => /lib64/libc.so.6 (0x00007f4e1db65000)
 libcap.so.2 => /lib64/libcap.so.2 (0x00007f4e1d960000)
 libm.so.6 => /lib64/libm.so.6 (0x00007f4e1d5e2000)
 librt.so.1 => /lib64/librt.so.1 (0x00007f4e1d3da000)
...

提示: 要查看底層依賴關係,使用 lddtree 實用程序可能更好。

段與節的區別

假設一個簡單的 ELF 文件有以下節和段:

在運行時,操作系統會將這些組織成

二進制分析工具

當我們想要分析 ELF 文件時,首先尋找可用的工具肯定是有用的。一些軟件包提供了一個工具包,用於逆向工程二進制文件或可執行代碼。如果讀者是分析 ELF 惡意軟件或固件的新手,請考慮首先學習 靜態分析。這意味着我們在不實際執行文件的情況下檢查文件。當大家更好地瞭解它們的工作原理時,然後轉向 動態分析。現在我們將運行文件樣本,並在低級代碼作爲實際處理器指令執行時查看它們的實際行爲。無論我們進行什麼類型的分析,請確保在專用系統上進行,最好有嚴格的網絡規則。這特別是當我們處理未知樣本或與惡意軟件相關時。

流行工具

Radare2

Radare2 工具包由 Sergi Alvarez 創建。版本中的 “2” 指的是與第一版本相比的完整重寫。它現在被許多逆向工程師用來了解二進制文件的工作原理。它可以用來解剖固件、惡意軟件和任何其他看起來是可執行格式的東西。

軟件包

大多數 Linux 系統已經安裝了 binutils 包。其他包可能有助於顯示更多細節。擁有正確的工具包可能會簡化大家的工作,特別是當我們進行分析或更多地瞭解 ELF 文件時。因此,我們收集了一個包列表和相關實用程序。

elfutils

洞見:elfutils 包是一個很好的開始,因爲它包含了大多數用於執行分析的實用程序。

elfkickers

洞見:ELFKickers 包的作者專注於 ELF 文件的操作,這可能對於我們在發現畸形 ELF 二進制文件時瞭解更多非常有幫助。

pax-utils

注意:此包中的一些實用程序可以遞歸掃描整個目錄。適合目錄的大規模分析。工具的重點是收集 PaX 詳細信息。除了 ELF 支持外,還可以提取有關 Mach-O 二進制文件的一些詳細信息。

示例輸出:

scanelf -a /usr/bin/ps
 TYPE    PAX   PERM ENDIAN STK/REL/PTL TEXTREL RPATH BIND FILE
ET_EXEC PeMRxS 0755 LE RW- R-- RW-    -      -   LAZY /usr/bin/ps

示例二進制文件

如果我們想自己創建一個二進制文件,只需創建一個小的 C 程序並編譯它。這裏有一個示例,它打開 /tmp/test.txt,將內容讀入緩衝區並顯示它。確保創建相關的 /tmp/test.txt 文件。

#include <stdio.h>

int main(int argc, char **argv)
{
   FILE *fp;
   char buff[255];

   fp = fopen("/tmp/test.txt""r");
   fgets(buff, 255, fp);
   printf("%s\n", buff);
   fclose(fp);

   return 0;
}

這個程序可以用:gcc -o test test.c 來編譯

常見問題解答

什麼是 ABI?

ABI 是 Application Binary Interface 的縮寫,它指定了操作系統和一段可執行代碼之間的低級接口。

什麼是 ELF?

ELF 是 Executable and Linkable Format 的縮寫。它是一個正式的規範,定義了指令在可執行代碼中的存儲方式。

我怎樣才能看到一個未知文件的文件類型?

使用 file 命令進行第一輪分析。該命令可能能夠根據頭部信息或魔術數據顯示詳細信息。

結論

ELF 文件用於執行或鏈接。根據主要目標,它包含所需的段或節。段由內核查看並映射到內存中(使用 mmap)。節由鏈接器查看,以創建可執行代碼或共享對象。

ELF 文件類型非常靈活,爲多種 CPU 類型、機器架構和操作系統提供支持。它也非常可擴展:每個文件的構建方式不同,取決於所需的部分。

頭部是文件的一個重要部分,準確描述了 ELF 文件的內容。通過使用正確的工具,我們可以對文件的目的有一個基本的瞭解。從那裏開始,我們可以進一步檢查二進制文件。這可以通過確定它使用的函數或存儲在文件中的字符串來完成。對於那些從事惡意軟件研究或想要更好地瞭解進程行爲(或不行爲!)的人來說,這是一個很好的開始。

參考文章

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8L04s20E-9Qyz0DKhlOrKQ