天天看點

【騰訊Bugly幹貨分享】Android Linker 與 SO 加殼技術

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

本文來自于騰訊bugly開發者社群,非經作者同意,請勿轉載,原文位址:http://dev.qq.com/topic/57e3a3bc42eb88da6d4be143

作者:王賽

1. 前言

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

對于 Linker 的學習,還應該包括 Linker 自舉、可執行檔案的加載等技術,但是限于本人的技術水準,本文的讨論範圍限定在 SO 檔案的加載,也就是在調用

dlopen("libxx.SO")

之後,Linker 的處理過程。

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

P.S. :閱讀本文的讀者需要對 ELF 檔案結構有一定的了解。

2. SO 的裝載與連結

2.1 整體流程說明

1. do_dlopen

調用 dl_open 後,中間經過 dlopen_ext, 到達第一個主要函數 do_dlopen:

soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {
   protect_data(PROT_READ | PROT_WRITE);
   soinfo* si = find_library(name, flags, extinfo); // 查找 SO
   if (si != NULL) {
     si->CallConstructors(); // 調用 SO 的 init 函數
   }
   protect_data(PROT_READ);
   return si;
 }
           

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

2. find_library_internal

find_library 直接調用了 find_library_internal,下面直接看 find_library_internal函數:

static soinfo* find_library_internal(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
   if (name == NULL) {
     return somain;
   }
   soinfo* si = find_loaded_library_by_name(name);  // 判斷 SO 是否已經加載
   if (si == NULL) {
     TRACE("[ '%s' has not been found by name.  Trying harder...]", name);
     si = load_library(name, dlflags, extinfo);     // 繼續 SO 的加載流程
   }
   if (si != NULL && (si->flags & FLAG_LINKED) == 0) {
     DL_ERR("recursive link to \"%s\"", si->name);
     return NULL;
   }
   return si;
 }
           

find_library_internal 首先通過 find_loaded_library_by_name 函數判斷目标 SO 是否已經加載,如果已經加載則直接傳回對應的soinfo指針,沒有加載的話則調用 load_library 繼續加載流程,下面看 load_library 函數。

3. load_library

static soinfo* load_library(const char* name, int dlflags, const Android_dlextinfo* extinfo) {
     int fd = -1;
     ...
     // Open the file.
     fd = open_library(name);                // 打開 SO 檔案,獲得檔案描述符 fd

     ElfReader elf_reader(name, fd);         // 建立 ElfReader 對象
     ...
     // Read the ELF header and load the segments.
     if (!elf_reader.Load(extinfo)) {        // 使用 ElfReader 的 Load 方法,完成 SO 裝載
         return NULL;
     }

     soinfo* si = soinfo_alloc(SEARCH_NAME(name), &file_stat);  // 為 SO 配置設定新的 soinfo 結構
     if (si == NULL) {
         return NULL;
     }
     si->base = elf_reader.load_start();  // 根據裝載結果,更新 soinfo 的成員變量
     si->size = elf_reader.load_size();
     si->load_bias = elf_reader.load_bias();
     si->phnum = elf_reader.phdr_count();
     si->phdr = elf_reader.loaded_phdr();
     ...
     if (!soinfo_link_image(si, extinfo)) {  // 調用 soinfo_link_image 完成 SO 的連結過程
       soinfo_free(si);
       return NULL;
     }
     return si;
 }
           

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。

...
     // Read the ELF header and load the segments.
     if (!elf_reader.Load(extinfo)) {
         return NULL;
     }
     ...
           

ElfReader::Load 方法如下:

bool ElfReader::Load(const Android_dlextinfo* extinfo) {
   return ReadElfHeader() &&             // 讀取 elf header
          VerifyElfHeader() &&           // 驗證 elf header
          ReadProgramHeader() &&         // 讀取 program header
          ReserveAddressSpace(extinfo) &&// 配置設定空間
          LoadSegments() &&              // 按照 program header 訓示裝載 segments
          FindPhdr();                    // 找到裝載後的 phdr 位址
 }
           

ElfReader::Load 方法首先讀取 SO 的elf header,再對elf header進行驗證,之後讀取program header,根據program header 計算 SO 需要的記憶體大小并配置設定相應的空間,緊接着将 SO 按照以 segment 為機關裝載到記憶體,最後在裝載到記憶體的 SO 中找到program header,友善之後的連結過程使用。

下面深入 ElfReader 的這幾個成員函數進行詳細介紹。

2.2.1 read&verify elfheader

bool ElfReader::ReadElfHeader() {
   ssize_t rc = read(fd_, &header_, sizeof(header_));

   if (rc != sizeof(header_)) {
     return false;
   }
   return true;
 }
           

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

bool ElfReader::ReadProgramHeader() {
   phdr_num_ = header_.e_phnum;      // program header 數量

   // mmap 要求頁對齊
   ElfW(Addr) page_min = PAGE_START(header_.e_phoff);
   ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));
   ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);

   phdr_size_ = page_max - page_min;
   // 使用 mmap 将 program header 映射到記憶體
   void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);

   phdr_mmap_ = mmap_result;
   // ElfReader 的成員變量 phdr_table_ 指向program header table
   phdr_table_ = reinterpret_cast<ElfW(Phdr)*>(reinterpret_cast<char*>(mmap_result) + page_offset);
   return true;
 }
           

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

2.2.3 reserve space & 計算 load size

bool ElfReader::ReserveAddressSpace(const Android_dlextinfo* extinfo) {
   ElfW(Addr) min_vaddr;
   // 計算 加載SO 需要的空間大小
   load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
   // min_vaddr 一般情況為零,如果不是則表明 SO 指定了加載基址
   uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);
   void* start;

   int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
   start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);

   load_start_ = start;
   load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;
   return true;
 }
           

首先調用 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。

size_t phdr_table_get_load_size(const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                 ElfW(Addr)* out_min_vaddr,
                                 ElfW(Addr)* out_max_vaddr) {
   ElfW(Addr) min_vaddr = UINTPTR_MAX;
   ElfW(Addr) max_vaddr = 0;
   bool found_pt_load = false;
   for (size_t i = 0; i < phdr_count; ++i) {  // 周遊 program header
     const ElfW(Phdr)* phdr = &phdr_table[i];
     if (phdr->p_type != PT_LOAD) {
       continue;
     }
     found_pt_load = true;
     if (phdr->p_vaddr < min_vaddr) {
       min_vaddr = phdr->p_vaddr;         // 記錄最小的虛拟位址
     }
     if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {
       max_vaddr = phdr->p_vaddr + phdr->p_memsz;  // 記錄最大的虛拟位址
     }
   }
   if (!found_pt_load) {
     min_vaddr = 0;
   }
   min_vaddr = PAGE_START(min_vaddr);      // 頁對齊
   max_vaddr = PAGE_END(max_vaddr);      // 頁對齊
   if (out_min_vaddr != NULL) {
     *out_min_vaddr = min_vaddr;
   }
   if (out_max_vaddr != NULL) {
     *out_max_vaddr = max_vaddr;
   }
   return max_vaddr - min_vaddr;         // load_size = max_vaddr - min_vaddr
 }
           

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。
bool ElfReader::LoadSegments() {
   for (size_t i = 0; i < phdr_num_; ++i) {
     const ElfW(Phdr)* phdr = &phdr_table_[i];

     if (phdr->p_type != PT_LOAD) {
       continue;
     }
     // Segment 在記憶體中的位址.
     ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
     ElfW(Addr) seg_end   = seg_start + phdr->p_memsz;

     ElfW(Addr) seg_page_start = PAGE_START(seg_start);
     ElfW(Addr) seg_page_end   = PAGE_END(seg_end);

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

     // 檔案偏移
     ElfW(Addr) file_start = phdr->p_offset;
     ElfW(Addr) file_end   = file_start + phdr->p_filesz;

     ElfW(Addr) file_page_start = PAGE_START(file_start);
     ElfW(Addr) file_length = file_end - file_page_start;

     if (file_length != 0) {
       // 将檔案中的 segment 映射到記憶體
       void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
                             file_length,
                             PFLAGS_TO_PROT(phdr->p_flags),
                             MAP_FIXED|MAP_PRIVATE,
                             fd_,
                             file_page_start);
     }
     // 如果 segment 可寫, 并且沒有在頁邊界結束,那麼就将 segemnt end 到頁邊界的記憶體清零。
     if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
       memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
     }

     seg_file_end = PAGE_END(seg_file_end);
     // 将 (記憶體長度 - 檔案長度) 對應的記憶體進行匿名映射
     if (seg_page_end > seg_file_end) {
       void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),
                            seg_page_end - seg_file_end,
                            PFLAGS_TO_PROT(phdr->p_flags),
                            MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,
                            -1,
                            0);
     }
   }
   return true;
 }
           

2.3 配置設定 soinfo

load_library 在調用 load_segments 完成裝載後,接着調用 soinfo_alloc 函數為目标SO配置設定soinfo,soinfo_alloc 函數實作如下:

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

   soinfo* si = g_soinfo_allocator.alloc();  //配置設定空間,可以簡單了解為 malloc
   // Initialize the new element.
   memset(si, 0, sizeof(soinfo));
   strlcpy(si->name, name, sizeof(si->name));
   si->flags = FLAG_NEW_SOINFO;

   sonext->next = si;    // 加入到存有所有 soinfo 的連結清單中
   sonext = si;
   return si;
 }
           

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):
    • 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 中的資訊。

...
     si->base = elf_reader.load_start();
     si->size = elf_reader.load_size();
     si->load_bias = elf_reader.load_bias();
     si->phnum = elf_reader.phdr_count();
     si->phdr = elf_reader.loaded_phdr();
     ...
           

2.4 連結

連結過程由 soinfo_link_image 函數完成,主要可以分為四個主要步驟:

1. 定位 dynamic section,

由函數 phdr_table_get_dynamic_section 完成,該函數會周遊 program header,找到為類型為 PT_DYNAMIC 的 header, 從中擷取的是 dynamic section 的資訊,主要就是虛拟位址和項數。

2. 解析 dynamic section

dynamic section本質上是類型為

Elf32_Dyn

的數組,Elf32_Dyn 結構如下

typedef struct {
     Elf32_Sword d_tag;      /* 類型(e.g. DT_SYMTAB),決定 d_un 表示的意義*/
     union {
         Elf32_Word  d_val;  /* 根據 d_tag的不同,有不同的意義*/
         Elf32_Addr  d_ptr;  /* 虛拟位址 */
     } d_un;
 } Elf32_Dyn;
           

Elf32_Dyn

結構的

d_tag

屬性表示該項的類型,類型決定了dun中資訊的意義,e.g.:當

d_tag = DT_SYMTAB

表示該項存儲的是符号表的資訊,d_un.d_ptr 表示符号表的虛拟位址的偏移,當

d_tag = DT_RELSZ

時,d_un.d_val 表示重定位表rel的項數。

解析的過程就是周遊數組中的每一項,根據

d_tag

的不同,擷取到不同的資訊。

dynamic section 中包含的資訊主要包括以下 3 類:

- 符号資訊
 - 重定位資訊
 - init&finit funcs
           

3. 加載 needed SO

調用 find_library 擷取所有依賴的 SO 的 soinfo 指針,如果 SO 還沒有加載,則會将 SO 加載到記憶體,配置設定一個soinfo*[]指針數組,用于存放 soinfo 指針。

4. 重定位

重定位SO 連結中最複雜同時也是最關鍵的一步。重定位做的工作主要是修複導入符号的引用,下面一節将對重定位過程進行詳細分析。

soinfo_link_image 的示意代碼:

static bool soinfo_link_image(soinfo* si, const Android_dlextinfo* extinfo) {
 ...
     // 1. 擷取 dynamic section 的資訊,si->dynamic 指向 dynamic section
     phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,
                                    &dynamic_count, &dynamic_flags);
 ...
     // 2. 解析dynamic section
     uint32_t needed_count = 0;
     for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
         switch (d->d_tag) {
          // 以下為符号資訊
          case DT_HASH:
             si->nbucket = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr)[0];
             si->nchain = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr)[1];
             si->bucket = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr + 8);
             si->chain = reinterpret_cast<uint32_t*>(base + d->d_un.d_ptr + 8 + si->nbucket * 4);
             break;
          case DT_SYMTAB:
             si->symtab = reinterpret_cast<ElfW(Sym)*>(base + d->d_un.d_ptr);
             break;
          case DT_STRTAB:
             si->strtab = reinterpret_cast<const char*>(base + d->d_un.d_ptr);
             break;
          // 以下為重定位資訊
          case DT_JMPREL:
             si->plt_rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
             break;
          case DT_PLTRELSZ:
             si->plt_rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
             break;
          case DT_REL:
             si->rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
             break;
          case DT_RELSZ:
             si->rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
             break;
          // 以下為 init&finit funcs
          case DT_INIT:
             si->init_func = reinterpret_cast<Linker_function_t>(base + d->d_un.d_ptr);
             break;
          case DT_FINI:
             ...
          case DT_INIT_ARRAY:
             si->init_array = reinterpret_cast<Linker_function_t*>(base + d->d_un.d_ptr);
             break;
          case DT_INIT_ARRAYSZ:
             ...
          case DT_FINI_ARRAY:
             ...
          case DT_FINI_ARRAYSZ:
             ...
          // SO 依賴
          case DT_NEEDED:
             ...
         ...
         }
 ...
     // 3. 加載依賴的SO
     for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
         if (d->d_tag == DT_NEEDED) {
             soinfo* lsi = find_library(library_name, 0, NULL);
             si->add_child(lsi);
             *pneeded++ = lsi;
         }
     }
     *pneeded = NULL;
 ...
     // 4. 重定位
     soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed);
     soinfo_relocate(si, si->rel, si->rel_count, needed);
 ...
     // 設定已連結标志
     si->flags |= FLAG_LINKED;
     DEBUG("[ finished linking %s ]", si->name);
 }
           

2.4.1 重定位 relocate

Android ARM 下需要處理兩個重定位表,plt_rel 和 rel,plt 指的是延遲綁定,但是 Android 目前并不對延遲綁定做特殊處理,直接與普通的重定位同時處理。兩個重定位的表都由 soinfo_relocate 函數處理。

soinfo_relocate 函數需要周遊重定位表,處理每個重定位項,每個重定位項的處理過程可以分為 3 步:

1. 解析重定位項和導入符号的資訊

重定位項的結構如下
typedef struct {
      Elf32_Addr  r_offset;   /* 需要重定位的位置的偏移 */
      Elf32_Word  r_info;     /* 高24位為符号在符号表中的index,低8位為重定位類型 */
 } Elf32_Rel;
           

首先從重定位項擷取的資訊如下:

  • 重定位的類型 type
  • 符号在符号表中的索引号 sym,sym 為0表示為本SO内部的重定位,如果不為0,意味着該符号為導入符号
  • 重定位的目标位址 reloc,使用r_offset + si_load_bias,相當于 偏移位址+基位址
    符号表表項的結構為elf32_sym:
    typedef struct elf32_sym {
      Elf32_Word  st_name;    /* 名稱 - index into string table */
      Elf32_Addr  st_value;   /* 偏移位址 */
      Elf32_Word  st_size;    /* 符号長度( e.g. 函數的長度) */
      unsigned char   st_info;    /* 類型和綁定類型 */
      unsigned char   st_other;   /* 未定義 */
      Elf32_Half  st_shndx;   /* section header的索引号,表示位于哪個 section 中 */
    } Elf32_Sym;
               

    2. 如果 sym 不為0,則查找導入符号的資訊

    如果 sym 不為0,則繼續使用 sym 在符号表中擷取符号資訊,從符号資訊中進一步擷取符号的名稱。随後調用 soinfo_do_lookup 函數在所有依賴的 SO 中根據符号名稱查找符号資訊,傳回值類型為 elf32_sym,同時還會傳回含有該符号的 SO 的 soinfo( lsi ),如果查找成功則該導入符号的位址為:

    sym_addr = s->st_value + lsi->load_bias;

    3. 修正需要重定位的位址

    根據重定位類型的不同,修正重定位位址,具體的重定位類型定義和計算方法可以參考 aaelf 文檔的 4.6.1.2 節。

    對于導入符号,則使用根據第二步得到 sym_addr 去修正,對于 SO 内部的相對偏移修正,則直接将reloc的位址加上 SO 的基址。

    static int soinfo_relocate(soinfo* si, ElfW(Rel)* rel, unsigned count, soinfo* needed[]) {
      ElfW(Sym)* s;
      soinfo* lsi;
    
      // 周遊重定位表
      for (size_t idx = 0; idx < count; ++idx, ++rel) {
          //
          // 1. 解析重定位項和導入符号的資訊
          //
          // 重定位類型
          unsigned type = ELFW(R_TYPE)(rel->r_info);
          // 導入符号在符号表中的 index,可以為0,(修正 SO 内部的相對偏移)
          unsigned sym = ELFW(R_SYM)(rel->r_info);
          // 需要重定位的位址
          ElfW(Addr) reloc = static_cast<ElfW(Addr)>(rel->r_offset + si->load_bias);
          ElfW(Addr) sym_addr = 0;
          const char* sym_name = NULL;
    
          if (type == 0) { // R_*_NONE
              continue;
          }
          if (sym != 0) {
              //
              // 2. 如果 sym 有效,則查找導入符号
              //
              // 從符号表中獲得符号資訊,在根據符号資訊從字元串表中擷取字元串名
              sym_name = reinterpret_cast<const char*>(si->strtab + si->symtab[sym].st_name);
              // 在依賴的 SO 中查找符号,傳回值為 Elf32_Sym 類型
              s = soinfo_do_lookup(si, sym_name, &lsi, needed);
              if (s == NULL) {}
                  // 查找失敗,不關心
              } else {
                  // 查找成功,最終的符号位址 = s->st_value + lsi->load_bias
                  // s->st_value 是符号在依賴 SO 中的偏移,lsi->load_bias 為依賴 SO 的基址
                  sym_addr = static_cast<ElfW(Addr)>(s->st_value + lsi->load_bias);
              }
          } else {
              s = NULL;
          }
          //
          // 3. 根據重定位類型,修正需要重定位的位址
          //
          switch (type) {
          // 判斷重定位類型,将需要重定位的位址 reloc 修正為目标符号位址
          // 修正導入符号
          case R_ARM_JUMP_SLOT:
              *reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr;
              break;
          case R_ARM_GLOB_DAT:
              *reinterpret_cast<ElfW(Addr)*>(reloc) = sym_addr;
              break;
          case R_ARM_ABS32:
              *reinterpret_cast<ElfW(Addr)*>(reloc) += sym_addr;
              break;
          case R_ARM_REL32:
              *reinterpret_cast<ElfW(Addr)*>(reloc) += sym_addr - rel->r_offset;
              break;
          // 不支援
          case R_ARM_COPY:
              /*
               * ET_EXEC is not supported SO this should not happen.
               */
              DL_ERR("%s R_ARM_COPY relocations are not supported", si->name);
              return -1;
          // SO 内部的偏移修正
          case R_ARM_RELATIVE:
              if (sym) {
                  DL_ERR("odd RELATIVE form...");
                  return -1;
              }
              *reinterpret_cast<ElfW(Addr)*>(reloc) += si->base;
              break;
    
          default:
              DL_ERR("unknown reloc type %d @ %p (%zu)", type, rel, idx);
              return -1;
          }
      }
      return 0;
    }
               

2.5 CallConstructors

在編譯 SO 時,可以通過連結選項

-init

或是給函數添加屬性

__attribute__((constructor))

來指定 SO 的初始化函數,這些初始化函數在 SO 裝載連結後便會被調用,再之後才會将 SO 的 soinfo 指針傳回給 dl_open 的調用者。SO 層面的保護手段,有兩個介入點, 一個是 jni_onload, 另一個就是初始化函數,比如反調試、脫殼等,逆向分析時經常需要動态調試分析這些初始化函數。

完成 SO 的裝載連結後,傳回到 do_dlopen 函數, do_open 獲得 find_library 傳回的剛剛加載的 SO 的 soinfo,在将 soinfo 傳回給其他子產品使用之前,最後還需要調用 soinfo 的成員函數 CallConstructors。

soinfo* do_dlopen(const char* name, int flags, const Android_dlextinfo* extinfo) {
 ...
   soinfo* si = find_library(name, flags, extinfo);
   if (si != NULL) {
     si->CallConstructors();
   }
   return si;
 ...
 }
           

CallConstructors 函數會調用 SO 的首先調用所有依賴的 SO 的 soinfo 的 CallConstructors 函數,接着調用自己的 soinfo 成員變量 init 和 看 init_array 指定的函數,這兩個變量在在解析 dynamic section 時指派。

void soinfo::CallConstructors() {
   //如果已經調用過,則直接傳回
   if (constructors_called) {
     return;
   }
   // 調用依賴 SO 的 Constructors 函數
   get_children().for_each([] (soinfo* si) {
     si->CallConstructors();
   });
   // 調用 init_func
   CallFunction("DT_INIT", init_func);
   // 調用 init_array 中的函數
   CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
 }
           

有了以上分析基礎後,在需要動态跟蹤初始化函數時,我們就知道可以将斷點設在 do_dlopen 或是 CallConstructors。

3. 加殼技術

在病毒和版權保護領域,“殼”一直扮演着極為重要的角色。通過加殼可以對代碼進行壓縮和加密,同時再輔以虛拟化、代碼混淆和反調試等手段,達到防止靜态和動态分析。

在 Android 環境中,Native 層的加殼主要是針對動态連結庫 SO,SO 加殼的示意圖如下:

加殼工具、loader、被保護SO。

  • SO: 即被保護的目标 SO。
  • loader: 自身也是一個 SO,系統加載時首先加載 loader,loader 首先還原出經過加密、壓縮、變換的 SO,再将 SO 加載到記憶體,并完成連結過程,使 SO 可以正常被其他子產品使用。
  • 加殼工具: 将被保護的 SO 加密、壓縮、變換,并将結果作為資料與 loader 整合為 packed SO。

下面對 SO 加殼的關鍵技術進行簡單介紹。

3.1 loader 執行時機

Linker 加載完 loader 後,loader 需要将被保護的 SO 加載起來,這就要求 loader 的代碼需要被執行,而且要在 被保護 SO 被使用之前,前文介紹了 SO 的初始化函數便可以滿足這個要求,同時在 Android 系統下還可以使用 JNI_ONLOAD 函數,是以 loader 的執行時機有兩個選擇:

  • SO 的 init 或 initarray
  • jni_onload

3.2 loader 完成 SO 的加載連結

loader 開始執行後,首先需要在記憶體中還原出 SO,SO 可以是經過加密、壓縮、變換等手段,也可已單純的以完全明文的資料存儲,這與 SO 加殼的技術沒有必要的關系,在此不進行讨論。

在記憶體中還原出 SO 後,loader 還需要執行裝載和連結,這兩個過程可以完全模仿 Linker 來實作,下面主要介紹一下相對 Linker,loader 執行這兩個過程有哪些變化。

3.2.1 裝載

還原後的 SO 在記憶體中,是以裝載時的主要變化就是從檔案裝載到從記憶體裝載。

Linker 在裝載 PT_LAOD segment時,使用 SO 檔案的描述符 fd:

void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
                             file_length,
                             PFLAGS_TO_PROT(phdr->p_flags),
                             MAP_FIXED|MAP_PRIVATE,
                             fd_,
                             file_page_start);
           

按照 Linker 裝載,PT_LAOD segment時,需要分為兩步:

// 1、改用匿名映射
       void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
                             file_length,
                             PFLAGS_TO_PROT(phdr->p_flags),
                             MAP_FIXED|MAP_PRIVATE,
                             -1,
                             0);
      // 2、将記憶體中的 segment 複制到映射的記憶體中
      memcpy(seg_addr+seg_page_offset, elf_data_buf + phdr->p_offset, phdr->p_filesz);
           

注意第2步複制 segment 時,目标位址需要加上 seg_page_offset,seg_page_offset 是 segment 相對與頁面起始位址的偏移。

其他的步驟基本按照 Linker 的實作即可,隻需要将一些從檔案讀取修改為從記憶體讀取,比如讀 elfheader和program header時。

3.2.2 配置設定 soinfo

soinfo 儲存了 SO 裝載連結和運作時需要的所有資訊,為了維護相關的資訊,loader 可以照搬 Linker 的 soinfo 結構,用于存儲中間資訊,裝載連結結束後,還需要将 soinfo 的資訊修複到 Linker 維護的soinfo,3.3節進行詳細說明。

3.2.3 連結

連結過程完全是操作記憶體,不論是從檔案裝載還是記憶體裝載,連結過程都是一樣,完全模仿 Linker 即可。

另外連結後記得順便調用 SO 初始化函數( init 和 init_array )。

3.3 soinfo 修複

SO 加殼的最關鍵技術點在于 soinfo 的修複,由于 Linker 加載的是 loader,而實際對外使用的是被保護的 SO,是以 Linker 維護的 soinfo 可以說是錯誤,loader 需要将自己維護的 soinfo 中的部分資訊導出給 Linker 的soinfo。

修複過程如下:

  1. 擷取 Linker 維護的 soinfo,可以通過 dlopen 打開自己來獲得:self_soinfo = dlopen(self)。
  2. 将 loader soinfo 中的資訊導出到 self_soinfo,最簡單粗暴的方式就是直接指派,比如:

    self_soinfo.base = soinfo.base

    。需要導出的主要有以下幾項:
    • SO位址範圍:base、size、load_bias
    • 符号資訊:sym_tab、str_tab、
    • 符号查找資訊:nbucket、nchain、bucket、chain
    • 異常處理:ARM_exidx、ARM_exidx_count

參考

  • <<Linkers and loaders>>

  • <<ELF for the ARM Architecture>>

更多精彩内容歡迎關注bugly的微信公衆賬号:

騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!