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:
-
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步:
- 裝載:建立ElfReader物件,通過 ElfReader 物件的 Load 方法將 SO 檔案裝載到記憶體
- 分配soinfo:呼叫 soinfo_alloc 函式為 SO 分配新的 soinfo 結構,並按照裝載結果更新相應的成員變數
- 連結: 呼叫 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) {
-
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:
- 計算 segment 在記憶體空間中的起始地址 segstart 和結束地址 seg_end,seg_start 等於虛擬偏移加上基址load_bias,同時由於 mmap 的要求,都要對齊到頁邊界得到 seg_page_start 和 seg_page_end。
- 計算 segment 在檔案中的頁對齊後的起始地址 file_page_start 和長度 file_length。
- 使用 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):
- 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 中的資訊。
-
...
-
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();
-
...
--------------------- 本文來自 Omni-Space 的CSDN 部落格 ,全文地址請點選:https://blog.csdn.net/omnispace/article/details/54610553?utm_source=copy