1. 程式人生 > >AndroidLinker與SO加殼技術之上篇

AndroidLinker與SO加殼技術之上篇

1. 前言

Android 系統安全愈發重要,像傳統pc安全的可執行檔案加固一樣,應用加固是Android系統安全中非常重要的一環。目前Android 應用加固可以分為dex加固和Native加固,Native 加固的保護物件為 Native 層的 SO 檔案,使用加殼、反除錯、混淆、VM 等手段增加SO檔案的反編譯難度。目前最主流的 SO 檔案保護方案還是加殼技術, 在SO檔案加殼和脫殼的攻防技術領域,最重要的基礎的便是對於 Linker 即裝載連結機制的理解。對於非安全方向開發者,深刻理解系統的裝載與連結機制也是進階的必要條件。

本文詳細分析了 Linker 對 SO 檔案的裝載和連結過程,最後對 SO 加殼的關鍵技術進行了簡要的介紹。

對於 Linker 的學習,還應該包括 Linker 自舉、可執行檔案的載入等技術,但是限於本人的技術水平,本文的討論範圍限定在 SO 檔案的載入,也就是在呼叫dlopen("libxx.SO")之後,Linker 的處理過程。

本文基於 Android 5.0 AOSP 原始碼,僅針對 ARM 平臺,為了增強可讀性,文中列舉的原始碼均經過刪減,去除了其他 CPU 架構的相關原始碼以及錯誤處理。

另:閱讀本文的讀者需要對 ELF 檔案結構有一定的瞭解。

2. SO 的裝載與連結

2.1 整體流程說明

1. do_dlopen 呼叫 dl_open 後,中間經過 dlopen_ext, 到達第一個主要函式 do_dlopen:

  1. soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {

  2. protect_data(PROT_READ | PROT_WRITE);

  3. soinfo* si = find_library(name, flags, extinfo); // 查詢 SO

  4. if (si != NULL) {

  5. si->CallConstructors(); // 呼叫 SO 的 init 函式

  6. }

  7. protect_data(PROT_READ);

  8. return si;

  9. }

do_dlopen 呼叫了兩個重要的函式,第一個是find_library, 第二個是 soinfo 的成員函式 CallConstructors,find_library 函式是 SO 裝載連結的後續函式, 完成 SO 的裝載連結後, 通過 CallConstructors 呼叫 SO 的初始化函式。

2. find_library_internal find_library 直接呼叫了 find_library_internal,下面直接看 find_library_internal函式:

  1. static soinfo* find_library_internal(const char* name, int dlflags, const Android_dlextinfo* extinfo) {

  2. if (name == NULL) {

  3. return somain;

  4. }

  5. soinfo* si = find_loaded_library_by_name(name); // 判斷 SO 是否已經載入

  6. if (si == NULL) {

  7. TRACE("[ '%s' has not been found by name. Trying harder...]", name);

  8. si = load_library(name, dlflags, extinfo); // 繼續 SO 的載入流程

  9. }

  10. if (si != NULL && (si->flags & FLAG_LINKED) == 0) {

  11. DL_ERR("recursive link to \"%s\"", si->name);

  12. return NULL;

  13. }

  14. return si;

  15. }

find_library_internal 首先通過 find_loaded_library_by_name 函式判斷目標 SO 是否已經載入,如果已經載入則直接返回對應的soinfo指標,沒有載入的話則呼叫 load_library 繼續載入流程,下面看 load_library 函式。

3. load_library

  1. static soinfo* load_library(const char* name, int dlflags, const Android_dlextinfo* extinfo) {

  2. int fd = -1;

  3. ...

  4. // Open the file.

  5. fd = open_library(name); // 開啟 SO 檔案,獲得檔案描述符 fd

  6. ElfReader elf_reader(name, fd); // 建立 ElfReader 物件

  7. ...

  8. // Read the ELF header and load the segments.

  9. if (!elf_reader.Load(extinfo)) { // 使用 ElfReader 的 Load 方法,完成 SO 裝載

  10. return NULL;

  11. }

  12. soinfo* si = soinfo_alloc(SEARCH_NAME(name), &file_stat); // 為 SO 分配新的 soinfo 結構

  13. if (si == NULL) {

  14. return NULL;

  15. }

  16. si->base = elf_reader.load_start(); // 根據裝載結果,更新 soinfo 的成員變數

  17. si->size = elf_reader.load_size();

  18. si->load_bias = elf_reader.load_bias();

  19. si->phnum = elf_reader.phdr_count();

  20. si->phdr = elf_reader.loaded_phdr();

  21. ...

  22. if (!soinfo_link_image(si, extinfo)) { // 呼叫 soinfo_link_image 完成 SO 的連結過程

  23. soinfo_free(si);

  24. return NULL;

  25. }

  26. return si;

  27. }

load_library 函式呈現了 SO 裝載連結的整個流程,主要有3步:

  1. 裝載:建立ElfReader物件,通過 ElfReader 物件的 Load 方法將 SO 檔案裝載到記憶體
  2. 分配soinfo:呼叫 soinfo_alloc 函式為 SO 分配新的 soinfo 結構,並按照裝載結果更新相應的成員變數
  3. 連結: 呼叫 soinfo_link_image 完成 SO 的連結

通過前面的分析,可以看到, load_library 函式中包含了 SO 裝載連結的主要過程, 後文主要通過分析 ElfReader 類和 soinfo_link_image 函式, 來分別介紹 SO 的裝載和連結過程。

2.2 裝載

在 load_library 中, 首先初始化 elf_reader 物件, 第一個引數為 SO 的名字, 第二個引數為檔案描述符 fd:ElfReader elf_reader(name, fd) 之後呼叫 ElfReader 的 load 方法裝載 SO。

  1. ...

  2. // Read the ELF header and load the segments.

  3. if (!elf_reader.Load(extinfo)) {

  4. return NULL;

  5. }

  6. ...

ElfReader::Load 方法如下:

  1. bool ElfReader::Load(const Android_dlextinfo* extinfo) {

  2. return ReadElfHeader() && // 讀取 elf header

  3. VerifyElfHeader() && // 驗證 elf header

  4. ReadProgramHeader() && // 讀取 program header

  5. ReserveAddressSpace(extinfo) &&// 分配空間

  6. LoadSegments() && // 按照 program header 指示裝載 segments

  7. FindPhdr(); // 找到裝載後的 phdr 地址

  8. }

ElfReader::Load 方法首先讀取 SO 的elf header,再對elf header進行驗證,之後讀取program header,根據program header 計算 SO 需要的記憶體大小並分配相應的空間,緊接著將 SO 按照以 segment 為單位裝載到記憶體,最後在裝載到記憶體的 SO 中找到program header,方便之後的連結過程使用。 下面深入 ElfReader 的這幾個成員函式進行詳細介紹。

2.2.1 read&verify elfheader

  1. bool ElfReader::ReadElfHeader() {

  2. ssize_t rc = read(fd_, &header_, sizeof(header_));

  3. if (rc != sizeof(header_)) {

  4. return false;

  5. }

  6. return true;

  7. }

ReadElfHeader 使用 read 直接從 SO 檔案中將 elfheader 讀取 header 中,header_ 為 ElfReader 的成員變數,型別為 Elf32_Ehdr,通過 header 可以方便的訪問 elf header中各個欄位,elf header中包含有 program header table、section header table等重要資訊。 對 elf header 的驗證包括:

  • magic位元組
  • 32/64 bit 與當前平臺是否一致
  • 大小端
  • 型別:可執行檔案、SO …
  • 版本:一般為 1,表示當前版本
  • 平臺:ARM、x86、amd64 …

有任何錯誤都會導致載入失敗。

2.2.2 Read ProgramHeader

  1. bool ElfReader::ReadProgramHeader() {

  2. phdr_num_ = header_.e_phnum; // program header 數量

  3. // mmap 要求頁對齊

  4. ElfW(Addr) page_min = PAGE_START(header_.e_phoff);

  5. ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));

  6. ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);

  7. phdr_size_ = page_max - page_min;

  8. // 使用 mmap 將 program header 對映到記憶體

  9. void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);

  10. phdr_mmap_ = mmap_result;

  11. // ElfReader 的成員變數 phdr_table_ 指向program header table

  12. phdr_table_ = reinterpret_cast<ElfW(Phdr)*>(reinterpret_cast<char*>(mmap_result) + page_offset);

  13. return true;

  14. }

將 program header 在記憶體中單獨對映一份,用於解析program header 時臨時使用,在 SO 裝載到記憶體後,便會釋放這塊記憶體,轉而使用裝載後的 SO 中的program header。

2.2.3 reserve space & 計算 load size

  1. bool ElfReader::ReserveAddressSpace(const Android_dlextinfo* extinfo) {

  2. ElfW(Addr) min_vaddr;

  3. // 計算 載入SO 需要的空間大小

  4. load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);

  5. // min_vaddr 一般情況為零,如果不是則表明 SO 指定了載入基址

  6. uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);

  7. void* start;

  8. int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;

  9. start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);

  10. load_start_ = start;

  11. load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;

  12. return true;

  13. }

首先呼叫 phdr_table_get_load_size 函式獲取 SO 在記憶體中需要的空間load_size,然後使用 mmap 匿名對映,預留出相應的空間。

關於loadbias: SO 可以指定載入基址,但是 SO 指定的載入基址可能不是頁對齊的,這種情況會導致實際對映地址和指定的載入地址有一個偏差,這個偏差便是 load_bias_,之後在針對虛擬地址進行計算時需要使用 load_bias_ 修正。普通的 SO 都不會指定載入基址,這時min_vaddr = 0,則 load_bias_ = load_start_,即load_bias_ 等於載入基址,下文會將load_bias_ 直接稱為基址。

下面深入phdr_table_get_load_size分析一下 load_size 的計算:使用成員變數 phdr_table 遍歷所有的program header, 找到所有型別為 PT_LOAD 的 segment 的 p_vaddr 的最小值,p_vaddr + p_memsz 的最大值,分別作為 min_vaddr 和 max_vaddr,在將兩個值分別對齊到頁首和頁尾,最終使用對齊後的 max_vaddr - min_vaddr 得到 load_size。

  1. size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,

  2. ElfW(Addr)* out_min_vaddr,

  3. ElfW(Addr)* out_max_vaddr) {

  4. ElfW(Addr) min_vaddr = UINTPTR_MAX;

  5. ElfW(Addr) max_vaddr = 0;

  6. bool found_pt_load = false;

  7. for (size_t i = 0; i < phdr_count; ++i) {

  8. const ElfW(Phdr)* phdr = &phdr_table[i];

  9. if (phdr->p_type != PT_LOAD) {

  10. continue;

  11. }

  12. found_pt_load = true;

  13. if (phdr->p_vaddr < min_vaddr) {

  14. min_vaddr = phdr->p_vaddr; // 記錄最小的虛擬地址

  15. }

  16. if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {

  17. max_vaddr = phdr->p_vaddr + phdr->p_memsz; // 記錄最大的虛擬地址

  18. }

  19. }

  20. if (!found_pt_load) {

  21. min_vaddr = 0;

  22. }

  23. min_vaddr = PAGE_START(min_vaddr); // 頁對齊

  24. max_vaddr = PAGE_END(max_vaddr); // 頁對齊

  25. if (out_min_vaddr != NULL) {

  26. *out_min_vaddr = min_vaddr;

  27. }

  28. if (out_max_vaddr != NULL) {

  29. *out_max_vaddr = max_vaddr;

  30. }

  31. return max_vaddr - min_vaddr; // load_size = max_vaddr - min_vaddr

  32. }

2.2.4 Load Segments

遍歷 program header table,找到型別為 PT_LOAD 的 segment:

  1. 計算 segment 在記憶體空間中的起始地址 segstart 和結束地址 seg_end,seg_start 等於虛擬偏移加上基址load_bias,同時由於 mmap 的要求,都要對齊到頁邊界得到 seg_page_start 和 seg_page_end。
  2. 計算 segment 在檔案中的頁對齊後的起始地址 file_page_start 和長度 file_length。
  3. 使用 mmap 將 segment 對映到記憶體,指定對映地址為 seg_page_start,長度為 file_length,檔案偏移為 file_page_start。
  1. bool ElfReader::LoadSegments() {

  2. for (size_t i = 0; i < phdr_num_; ++i) {

  3. const ElfW(Phdr)* phdr = &phdr_table_[i];

  4. if (phdr->p_type != PT_LOAD) {

  5. continue;

  6. }

  7. // Segment 在記憶體中的地址.

  8. ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;

  9. ElfW(Addr) seg_end = seg_start + phdr->p_memsz;

  10. ElfW(Addr) seg_page_start = PAGE_START(seg_start);

  11. ElfW(Addr) seg_page_end = PAGE_END(seg_end);

  12. ElfW(Addr) seg_file_end = seg_start + phdr->p_filesz;

  13. // 檔案偏移

  14. ElfW(Addr) file_start = phdr->p_offset;

  15. ElfW(Addr) file_end = file_start + phdr->p_filesz;

  16. ElfW(Addr) file_page_start = PAGE_START(file_start);

  17. ElfW(Addr) file_length = file_end - file_page_start;

  18. if (file_length != 0) {

  19. // 將檔案中的 segment 對映到記憶體

  20. void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),

  21. file_length,

  22. PFLAGS_TO_PROT(phdr->p_flags),

  23. MAP_FIXED|MAP_PRIVATE,

  24. fd_,

  25. file_page_start);

  26. }

  27. // 如果 segment 可寫, 並且沒有在頁邊界結束,那麼就將 segemnt end 到頁邊界的記憶體清零。

  28. if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {

  29. memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));

  30. }

  31. seg_file_end = PAGE_END(seg_file_end);

  32. // 將 (記憶體長度 - 檔案長度) 對應的記憶體進行匿名對映

  33. if (seg_page_end > seg_file_end) {

  34. void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),

  35. seg_page_end - seg_file_end,

  36. PFLAGS_TO_PROT(phdr->p_flags),

  37. MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,

  38. -1,

  39. 0);

  40. }

  41. }

  42. return true;

  43. }

2.3 分配 soinfo

load_library 在呼叫 load_segments 完成裝載後,接著呼叫 soinfo_alloc 函式為目標SO分配soinfo,soinfo_alloc 函式實現如下:

  1. static soinfo* soinfo_alloc(const char* name, struct stat* file_stat) {

  2. soinfo* si = g_soinfo_allocator.alloc(); //分配空間,可以簡單理解為 malloc

  3. // Initialize the new element.

  4. memset(si, 0, sizeof(soinfo));

  5. strlcpy(si->name, name, sizeof(si->name));

  6. si->flags = FLAG_NEW_SOINFO;

  7. sonext->next = si; // 加入到存有所有 soinfo 的連結串列中

  8. sonext = si;

  9. return si;

  10. }

Linker 為 每個 SO 維護了一個soinfo結構,呼叫 dlopen時,返回的控制代碼其實就是一個指向該 SO 的 soinfo 指標。soinfo 儲存了 SO 載入連結以及執行期間所需的各類資訊,簡單列舉一下:

裝載連結期間主要使用的成員:

  • 裝載資訊
    • const ElfW(Phdr)* phdr;
    • size_t phnum;
    • ElfW(Addr) base;
    • size_t size;
  • 符號資訊
    • const char* strtab;
    • ElfW(Sym)* symtab;
  • 重定位資訊
    • ElfW(Rel)* plt_rel;
    • size_t plt_rel_count;
    • ElfW(Rel)* rel;
    • size_t rel_count;
  • init 函式和 finit 函式
    • Linker_function_t* init_array;
    • size_t init_array_count;
    • Linker_function_t* fini_array;
    • size_t fini_array_count;
    • Linker_function_t init_func;
    • Linker_function_t fini_func;

執行期間主要使用的成員:

  • 匯出符號查詢(dlsym):
    • const char* strtab;
    • ElfW(Sym)* symtab;
    • size_t nbucket;
    • size_t nchain;
    • unsigned* bucket;
    • unsigned* chain;
    • ElfW(Addr) load_bias;
  • 異常處理:
    • unsigned* ARM_exidx;
    • size_t ARM_exidx_count;

load_library 在為 SO 分配 soinfo 後,會將裝載結果更新到 soinfo 中,後面的連結過程就可以直接使用soinfo的相關欄位去訪問 SO 中的資訊。

  1. ...

  2. si->base = elf_reader.load_start();

  3. si->size = elf_reader.load_size();

  4. si->load_bias = elf_reader.load_bias();

  5. si->phnum = elf_reader.phdr_count();

  6. si->phdr = elf_reader.loaded_phdr();

  7. ...

--------------------- 本文來自 Omni-Space 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/omnispace/article/details/54610553?utm_source=copy