1. 程式人生 > >Android SO庫檔案頭分析

Android SO庫檔案頭分析

轉自:

因為專案的需要,我對Android系統載入.so檔案有一些些研究,把最近看過的一些大牛的分析和現狀結合一下,
寫篇東西做一下筆記。

.so 檔案是什麼

.so 檔案是 Shared Object 檔案的字尾,
直白的說就是Linux系統中的“動態連結庫” ,
就和Windows系統下的 .dll 檔案(Dynamic Link Library)類似。

.so 檔案實則是 ELF 格式的檔案,和Linux的可執行檔案的格式一致。

ELF 檔案結構

ELF 格式的檔案中的“資料”實際上是以“段”(節,英文:Section)的形式儲存的。
其順序大致符合下面的排序:

  • ELF Header:ELF頭部
  • .dynsym
    :儲存動態連結相關符號,記錄其偏移值
  • .dynstr.dynsym 的輔助段
  • .hash:快速查詢符號的雜湊表,類似 .dynstr
  • .rel.got:資料引用修正,修正到 .got
  • .rel.plt:函式引用修正,修正到 .got.plt
  • .text:程式碼段
  • 其他自定義的程式碼段
  • .rodata:字串常量段
  • .fini_array:終止函式段
  • .init_array:初始化函式段
  • .dynamic:動態連結庫特有,儲存動態連結用到的表資訊
  • .got:函式的絕對地址
  • .data:存放已經初始化的全域性變數,靜態記憶體分配相關
  • .bss:存放未初始化的全域性變數,靜態記憶體分配相關

ELF 頭部:ELF Header

其中,一切的起點都在ELF頭部,其偏移量(offset)為 0。
ELF頭部的結構體為 elf32_hdr

 或 elf64_hdr
在Android系統原始碼的 /bionic/libc/kernel/uapi/linux/elf.h 可以找到。

以32位程式的ELF頭部為例:

06#define EI_NIDENT 16typedef struct elf32_hdr { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; Elf32_Word e_flags; Elf32_Half e_ehsize; Elf32_Half e_phentsize; Elf32_Half e_phnum; Elf32_Half e_shentsize; Elf32_Half e_shnum; Elf32_Half e_shstrndx;} Elf32_Ehdr;

e_ident

ELF格式檔案的識別區域,固定為 16Bytes 的字串。
下面是這串字串各個位元組的含義。

EI_MAG

  • Offset: 0 - 3 (EI_MAG0)
  • Length: 4 (SELFMAG)
  • Type: String

ELF Identification 的前四個位元組為魔數(Magic Number),
內容為 {0x7F, 'E', 'L', 'F'}

如果魔數不一致,在Android系統會報錯“has bad ELF magic”,
終止 load_library並APP閃退。

EI_CLASS

  • Offset: 4 (EI_CLASS)
  • Length: 1
  • Type: Number

判斷 ELF 檔案是 32位的 還是 64位的。

常量定義:

  • ELFCLASSNONE = 0:無定義【非法】
  • ELFCLASS32 = 1:32位
  • ELFCLASS64 = 2:64位
  • ELFCLASSNUM = 3:未知【非法】

在Android <5 的系統上,由於當年的系統不支援64位的指令集,
因此只要不是32位,就輸出錯誤 “not 32-bit”,並APP閃退。

在Android >=5 系統上,已經出現了64位的指令集
,如 arm64_v8ax86_64
若32位的指令集遇到64位的SO庫,
會輸出錯誤 “is 64-bit instead of 32-bit”,
並APP閃退;
若32位的指令集遇到64位的SO庫,
會輸出錯誤 “is 32-bit instead of 64-bit”,
並APP閃退;
若出現非法的 ELFCLASS
會輸出錯誤 “has unknown ELF class: ?”,
並APP閃退。

EI_DATA

  • Offset: 5 (EI_DATA)
  • Length: 1
  • Type: Number (unsigned char)

這個是判斷 ELF檔案是 LSB(Little-endian,低位元組序)
還是 MSB(Big-endian,高位元組序)。

常量定義:

  • ELFDATANONE = 0:無定義【非法】
  • ELFDATA2LSB = 1:LSB
  • ELFDATA2MSB = 2:MSB【非法】

安卓系統只允許 LSB,因此只要不是 1
就輸出錯誤 “not little-endian”,
並APP閃退。

EI_VERSION

  • Offset: 6
  • Length: 1
  • Type: Number (unsigned char)

顧名思義,是 ELF 檔案格式的版本號,預設是 EV_CURRENT(= 1)。

Android系統不從這裡檢測 Version,而在 e_version 上檢測,因此修改無影響。

EI_OSABI

  • Offset: 7
  • Length: 1
  • Type: Number (unsigned char)

指出來該 ELF 檔案可以在什麼作業系統執行,參考:

#define ELFOSABI_NONE 0 /* UNIX System V ABI */#define ELFOSABI_SYSV 0 /* Alias. */#define ELFOSABI_HPUX 1 /* HP-UX */#define ELFOSABI_NETBSD 2 /* NetBSD. */#define ELFOSABI_LINUX 3 /* Linux. */#define ELFOSABI_SOLARIS 6 /* Sun Solaris. */#define ELFOSABI_AIX 7 /* IBM AIX. */#define ELFOSABI_IRIX 8 /* SGI Irix. */#define ELFOSABI_FREEBSD 9 /* FreeBSD. */#define ELFOSABI_TRU64 10 /* Compaq TRU64 UNIX. */#define ELFOSABI_MODESTO 11 /* Novell Modesto. */#define ELFOSABI_OPENBSD 12 /* OpenBSD. */#define ELFOSABI_ARM_AEABI 64 /* ARM EABI */#define ELFOSABI_ARM 97 /* ARM */#define ELFOSABI_STANDALONE 255 /* Standalone (embedded) application */

Android系統下的 SO檔案 此處的值預設為 0
而且載入時不檢測,修改無影響。

EI_ABIVERSION

  • Offset: 8
  • Length: 1
  • Type: Number (unsigned char)

指出該 ELF 檔案可以在哪個API版本下執行,Android下的預設值是 0

此處載入時不檢測,修改無影響。

EL_PAD

  • Offset: 9
  • Length: 7

填充位,無檢測,修改無影響。

技巧

Android >= 6 的系統版本只針對 0x00 - 0x05 之間的位元組進行檢測,
0x06 - 0x0F 之間的位元組(10 Bytes)無檢測,
因此可以任意修改,存放想要存放的資料。

e_type

  • Offset: 0x10
  • Length: 2
  • Type: unsigned short

ELF檔案型別,如:

#define ET_NONE 0 /* 無定義【非法】 */#define ET_REL 1 /* 已編譯未連結 */#define ET_EXEC 2 /* 已編譯已連結的可執行程式 */#define ET_DYN 3 /* 已編譯已連結的動態連結庫 */

ET_REL 指的是 Relocatable file,
缺少 Program Header,
不可以載入到記憶體中,
gcc 編譯時候生成的 .o 檔案就是 REL檔案。

ET_EXEC 指的是可執行程式,
存在程式入口,
有 Program Header,
可以載入到記憶體中執行,
在 Linux 下的可執行程式都是這樣的。

ET_DYN 特指動態連結庫。

由於Android的SO庫本質就是動態連結庫,
因此SO庫編譯後 e_type = ET_DYN

Android系統會檢測 e_type
若不為 ET_DYN,則丟擲錯誤 has unexpected e_type
並APP閃退。

e_machine

  • Offset: 0x12
  • Length: 2
  • Type: unsigned short

ELF 檔案的CPU平臺屬性(指令集)。

參考:

#if defined(__arm__) return EM_ARM;#elif defined(__aarch64__) return EM_AARCH64;#elif defined(__i386__) return EM_386;#elif defined(__mips__) return EM_MIPS;#elif defined(__x86_64__) return EM_X86_64;#endif

Android系統會使用 GetTargetElfMachine 函式
檢測SO庫的 e_machine 和現在的系統是否一致。
若不一致,則丟擲錯誤 has unexpected e_machine
並APP閃退。

e_version

  • Offset: 0x14
  • Length: 4
  • Type: unsigned int

顧名思義,是 ELF 檔案格式的版本號,預設是 EV_CURRENT(= 1)。

Android系統會檢測 e_version
若不為 EV_CURRENT,則丟擲錯誤 has unexpected e_version
並APP閃退。

e_entry

  • Offset: 0x18
  • Length: 4 (32bits)
  • Type: unsigned int

ELF程式的入口虛擬地址。僅用於可執行程式載入完成後,從此處開始執行程序指令。

動態連結庫不存在入口地址,所以Android系統不檢測。

e_phoff

  • Offset: 0x1C
  • Length: 4 (32bits)
  • Type: unsigned int

程式頭表的偏移,涉及“連線檢視”和“執行檢視”。

與實際SO庫中程式碼的指令執行相關,不允許修改。

e_shoff

  • Offset: 0x20
  • Length: 4 (32bits)
  • Type: unsigned int

段表在檔案中的偏移,涉及讀取段表。

Android <7 時,讀取段表依靠檢視,使用的是 e_phoff,而非 e_shoff
因此可以隨意修改。

涉及 linker_phdr.cpp 的 phdr_table_get_dynamic_section 函式

Android >=7 時,讀取段表依靠 e_shoff
修改為其他值會導致無法定位 .dynamic 段,
丟擲錯誤 “.dynamic section header was not found”。

涉及 linker_phdr.cpp 的 ElfReader::ReadDynamicSection 函式

為什麼要強調這個?因為看雪裡面的這個文章是不相容新的OS的,內容過時了。

e_flags

  • Offset: 0x24
  • Length: 4 (32bits)
  • Type: unsigned int

ELF標誌位,用來標誌一些ELF檔案平臺相關的屬性。

Android 系統不使用也不檢測此引數。

e_ehsize

  • Offset: 0x28
  • Length: 2
  • Type: unsigned short

ELF頭部的長度。

Android 系統不使用也不檢測此引數。

e_phentsize

  • Offset: 0x2A
  • Length: 2
  • Type: unsigned short

程式頭的大小。

Android 系統不使用也不檢測此引數。

e_phnum

  • Offset: 0x2C
  • Length: 2
  • Type: unsigned short

在執行檢視中,Segments的數量。

Android 系統對其進行檢測,並且嚴格到實際的數目。
若不一致,則丟擲錯誤 “has invalid e_phnum”、“has invalid phdr offset/size”
或者 “phdr mmap failed”等。

涉及函式 ElfReader::ReadProgramHeaders 和 ElfReader::ReadProgramHeader

e_shentsize

  • Offset: 0x2E
  • Length: 2
  • Type: unsigned short

段表描述符的大小,= sizeof(ElfW(Shdr))

Android <= 6 系統不使用也不檢測此引數。

Android >= 7 系統檢測此引數。
若與 sizeof(ElfW(Shdr)) 不相等,
則丟擲錯誤 “has unsupported e_shentsize”。

e_shnum

  • Offset: 0x30
  • Length: 2
  • Type: unsigned short

段表描述符的數量。這個值等於ELF檔案中擁有的段(section)的數量。

Android <= 6 系統不使用也不檢測此引數。

Android >= 7 系統檢測並使用此引數。
若為0,則丟擲錯誤 “has no section headers”。
若超出檔案大小範圍,則丟擲錯誤 “has invalid shdr offset/size”。

參考函式 ElfReader::ReadSectionHeaders

e_shstrndx

  • Offset: 0x32
  • Length: 2
  • Type: unsigned short

段表字符串表所在的段在段表中的下標,一般是 = e_shnum - 1

Android <= 6 系統不使用也不檢測此引數。

Android >= 7 系統檢測此引數。
若為0,則丟擲錯誤 “has invalid e_shstrndx”。

參考函式 ElfReader::VerifyElfHeader

偷就完事了

在ELF頭部,有這些地方可以瘋狂偷空間:

  1. 0x06 - 0x0F 合計 10Bytes
  2. 0x18 - 0x1B 合計 4Bytes
  3. 0x24 - 0x2B 合計 8Bytes

無限制儲存空間合計為 22Bytes。

以下地方,只有Android <=6 才可以偷空間存東西:

  1. 0x20 - 0x23 合計 4Bytes
  2. 0x2E - 0x33 合計 6Bytes

存在版本限制儲存空間合計為 10Bytes。

為什麼要偷空間出來存資料?
主要還是為了方便SO庫的打包工具做一些附加資料的儲存,
偷出來的地方是隨SO庫載入而載入,可以在記憶體直接讀取的。

值得學習

Android 系統原始碼提供了很多很好玩的設計思路,下面展示一些:

Code A

根據不同的指令集,使用不同的結構體:

// 如果是 64bits 的系統#if defined(__LP64__)#define ElfW(type) Elf64_ ## type#else#define ElfW(type) Elf32_ ## type#endif// ElfW(Ehdr) === Elf32_Ehdr

Code B

一些關於指令集的巨集定義:

// __arm__ -> armeabi, armeabi-v7a [32bits]// __aarch64__ -> arm64-v8a [64bits]// __i386__ -> x86 [32bits]// __mips__ -> mips, mips64 [32 / 64bits]// __x86_64__ -> x86_64 [64bits]#if defined(__arm__) printf("This is ARM Architecture!");#endif