linker是Android系統動態庫so的載入器和連結器,也是Android脫殼一重要脫殼點,這裡介紹一下此部分的Android原始碼,並介紹幾個脫殼點,及分析過程中產生的反除錯手段,學習Linker的載入和啟動原理,又需要介紹so的載入和啟動。
系統 :Android4.4-r1 linker原始碼的位置 : Android/bionic/linker
0x00 載入與啟動so
1、Java層中宣告載入某個so檔案以共享,則可在Java層宣告程式碼:
static{
System.loadLibrary("libhello.so")
}
複製程式碼
其對應的執行流程如下: 1、 定位到檔案Dalvik/vm/native/java_lang_Runtime.cpp 2、 呼叫Dalvik_java_lang_Runtime_nativeLoad -> Dalvik/vm/Native.cpp:dvmLoadNativeCode 具體程式碼如下,我略過一些錯誤判斷程式碼,這段程式碼以/*** 說明 ***/的形式省略
bool dvmLoadNativeCode(const char* pathName, Object* classLoader,
char** detail)
{
SharedLib* pEntry;
void* handle;
bool verbose;
/*********根據path查詢so檔案**********/
pEntry = findSharedLibEntry(pathName);
/*********載入指定so檔案,並延遲載入*******/
handle = dlopen(pathName, RTLD_LAZY);
dvmChangeStatus(self, oldStatus);
/* create a new entry */
SharedLib* pNewEntry;
/*.......*/
/* try to add it to the list */
/***當執行addShareLibEntry()方法的時候,如果還有執行緒B同時在載入該so,***/
/***並且B執行緒先執行到了這裡,那麼就說明該so的資訊已經新增過了,我們就不需要再執行新增pNewEntry的操作***/
SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
if (pNewEntry != pActualEntry) {
ALOGI("WOW: we lost a race to add a shared lib (%s CL=%p)",
pathName, classLoader);
freeSharedLibEntry(pNewEntry);
return checkOnLoadResult(pActualEntry);
} else {
if (verbose)
ALOGD("Added shared lib %s %p", pathName, classLoader);
bool result = false;
void* vonLoad;
int version;
/***定位JNI_OnLoad()方法***/
vonLoad = dlsym(handle, "JNI_OnLoad");//
/***如果找不到則延遲載入,說明是用javah風格的程式碼**/
if (vonLoad == NULL) {
ALOGD("No JNI_OnLoad found in %s %p, skipping init", pathName, classLoader);
result = true;
} else {
/*
* Call JNI_OnLoad. We have to override the current class
* loader, which will always be "null" since the stuff at the
* top of the stack is around Runtime.loadLibrary(). (See
* the comments in the JNI FindClass function.)
*/
/***這裡省略了重寫相應類載入器的程式碼,如上面雞腸文所示功能,不重要**/
/***gDvm是一個全域性變數,功能後面再補***/
if (gDvm.verboseJni) {
ALOGI("[Calling JNI_OnLoad for \"%s\"]", pathName);
}
/***執行JNI_OnLoad***/
version = (*func)(gDvmJni.jniVm, NULL);
/***省略一大段程式碼。。。***/
dvmUnlockMutex(&pNewEntry->onLoadLock);
return result;
}
}
複製程式碼
從上面的程式碼我們可看出 1、Android系統載入so檔案時使用了dlopen函式; 2、定位JNI_OnLoad()方法,則dlsym(handle, "JNI_OnLoad"); 3、執行JNI_OnLoad()方法:(*func)(gDvmJni.jniVm, NULL) 我們查詢載入so檔案的函式dlopen在bionic/linker/dlfcn.c中,而此函式有主要呼叫了do_dlopen函式,這裡對dlopen函式不詳細贅述,主要解析一下do_dlopen函式,而此關鍵函式正是在Linker中。接下來詳細分析Linker原始碼。
0x01 Linker原始碼總覽
0x02 do_dlopen
先貼上程式碼:
soinfo* si = find_library(name);//**完成so的載入到記憶體的工作
if (si != NULL) {
si->CallConstructors();//**完成so及本身的建構函式的呼叫。完成so檔案的載入
}
複製程式碼
嗯,註釋簡單明瞭,接下來解析find_library(name)與si->CallConstructors()。
0x03 續0 find_library
作用:完成so的載入到記憶體的工作,成為是否載入過該so的重要依據
static soinfo* find_library(const char* name) {//成為是否載入過該so的重要依據
soinfo* si = find_library_internal(name);//尋找相應的so資訊
if (si != NULL) {
si->ref_count++;
}
return si;
複製程式碼
嗯,還是很清晰,裡面的呼叫的方法先放下先,我們現分析如下方法。
0x04 si->CallConstructors()
完成so及本身的建構函式的呼叫。完成so檔案的載入
/***省略一大堆程式碼,下面是主要函式****/
if (dynamic != NULL) {
for (Elf32_Dyn* d = dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {//調動依賴庫的建構函式
const char* library_name = strtab + d->d_un.d_val;
TRACE("\"%s\": calling constructors in DT_NEEDED \"%s\"", name, library_name);
find_loaded_library(library_name)->CallConstructors();
}
// DT_INIT should be called before DT_INIT_ARRAY if both are present.
TRACE("\"%s\": calling constructors", name);
CallFunction("DT_INIT", init_func);//呼叫自己的一系列建構函式後返回,這裡是so檔案加殼的脫殼點
CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);//dex檔案的脫殼點
複製程式碼
這主要是完成so檔案的載入,然後遍歷所有動態節,再根據標籤d_tag ==DT_NEEDED呼叫依賴庫的建構函式,再呼叫自己的一系列建構函式,以及init_arry函式,其中後面兩個函式分別是so檔案和dex檔案的脫殼點,這樣就結束了so檔案的載入,但分析遠遠沒有結束,我們需要回過頭來解析find_library方法。
0x05 find_library 之find_library_internal()
操作:尋找相應的so資訊
static soinfo* find_library_internal(const char* name) {
if (name == NULL) {
return somain;
}
soinfo* si = find_loaded_library(name);
if (si != NULL) {
if (si->flags & FLAG_LINKED) {
return si;
}
DL_ERR("OOPS: recursive link to \"%s\"", si->name);
return NULL;
}
TRACE("[ '%s' has not been loaded yet. Locating...]", name);
si = load_library(name);//真正載入so檔案的函式
if (si == NULL) {
return NULL;
}
// At this point we know that whatever is loaded @ base is a valid ELF
// shared library whose segments are properly mapped in.
TRACE("[ init_library base=0x%08x sz=0x%08x name='%s' ]",
si->base, si->size, si->name);
if (!soinfo_link_image(si)) {//進行重定位
munmap(reinterpret_cast<void*>(si->base), si->size);
soinfo_free(si);
return NULL;
}
複製程式碼
註釋很明白,整理一下基本流程: 1、find_loaded_library():尋找相應的so資訊 2、load_library():真正載入so檔案的函式 3、soinfo_link_image():處理動態節dynamic section,初始化動態節dynamic section的屬性
0x06 find_library_internal() 之 find_loaded_library()
//尋找相應的so資訊
static soinfo *find_loaded_library(const char *name)
{
soinfo *si;
const char *bname;
// TODO: don't use basename only for determining libraries
// http://code.google.com/p/android/issues/detail?id=6670
bname = strrchr(name, '/');//查詢一個字元"/"在另一個字串name中末次出現的位置並返回這個位置的地址
bname = bname ? bname + 1 : name;
for (si = solist; si != NULL; si = si->next) {//判斷是否有載入這個so
if (!strcmp(bname, si->name)) {
return si;
}
}
return NULL;
}
複製程式碼
0x07 find_library_internal() 之load_library()
操作:真正載入so檔案的函式
static soinfo* load_library(const char* name) {
// Open the file.
int fd = open_library(name);
if (fd == -1) {
DL_ERR("library \"%s\" not found", name);
return NULL;
}
// Read the ELF header and load the segments.
ElfReader elf_reader(name, fd);
if (!elf_reader.Load()) {//讀取elf的操作,原始碼中看出只讀取了Program 段
return NULL;
}
const char* bname = strrchr(name, '/');
//在so檔案載入完以後,接著就會呼叫soinfo_alloc函式為so分配soinfo
soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
if (si == NULL) {
return NULL;
}//利用裝載結果初始化soinfo物件
si->base = elf_reader.load_start();
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->flags = 0;
si->entry = 0;
si->dynamic = NULL;
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
return si;
}
複製程式碼
這裡著重強調一下elf_reader.Load()方法:
bool ElfReader::Load() {
return ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeader() &&
ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr();
}
複製程式碼
可以看出讀取elf的操作,原始碼中看出只讀取了Program 段,這也是很多加固進行抹頭操作的原因:IDA只通過載入section Header段,來讀取so檔案,而實際上原始碼只讀取了Program段,如果只是將section Header段抹頭了,IDA便無法正常解析so檔案,而Android系統卻可以正常解析so檔案,這也是一種反除錯手段。 再後來就進行一系列對so檔案的初始化操作:
si->base = elf_reader.load_start();
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->flags = 0;
si->entry = 0;
si->dynamic = NULL;
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
return si;
複製程式碼
其中si結構體如下(可略):
struct soinfo {
public:
char name[SOINFO_NAME_LEN];
const Elf32_Phdr* phdr;
size_t phnum;
Elf32_Addr entry;
Elf32_Addr base;
unsigned size;
uint32_t unused1; // DO NOT USE, maintained for compatibility.
Elf32_Dyn* dynamic;
uint32_t unused2; // DO NOT USE, maintained for compatibility
uint32_t unused3; // DO NOT USE, maintained for compatibility
soinfo* next;
unsigned flags;
const char* strtab;
Elf32_Sym* symtab;
size_t nbucket;
size_t nchain;
unsigned* bucket;
unsigned* chain;
unsigned* plt_got;
Elf32_Rel* plt_rel;
size_t plt_rel_count;
Elf32_Rel* rel;
size_t rel_count;
linker_function_t* preinit_array;
size_t preinit_array_count;
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;
#if defined(ANDROID_ARM_LINKER)
// ARM EABI section used for stack unwinding.
unsigned* ARM_exidx;
size_t ARM_exidx_count;
#elif defined(ANDROID_MIPS_LINKER)
unsigned mips_symtabno;
unsigned mips_local_gotno;
unsigned mips_gotsym;
#endif
size_t ref_count;
link_map_t link_map;
bool constructors_called;
// When you read a virtual address from the ELF file, add this
// value to get the corresponding address in the process' address space.
Elf32_Addr load_bias;
bool has_text_relocations;
bool has_DT_SYMBOLIC;
void CallConstructors();
void CallDestructors();
void CallPreInitConstructors();
private:
void CallArray(const char* array_name, linker_function_t* functions, size_t count, bool reverse);//dex檔案脫殼點
void CallFunction(const char* function_name, linker_function_t function);//so檔案脫殼點
};
複製程式碼
0x08 find_library_internal() 之soinfo_link_image(si)
在si = load_library(name)獲得了so檔案的info之後,就開始進行一系列操作: 1、定位動態節; 2、解析動態節; 3、載入動態節 4、重定位
1、定位動態節:
phdr_table_get_dynamic_section(const Elf32_Phdr* phdr_table,
int phdr_count,
Elf32_Addr load_bias,
Elf32_Dyn** dynamic,
size_t* dynamic_count,
Elf32_Word* dynamic_flags)
{
const Elf32_Phdr* phdr = phdr_table;
const Elf32_Phdr* phdr_limit = phdr + phdr_count;
for (phdr = phdr_table; phdr < phdr_limit; phdr++) {
if (phdr->p_type != PT_DYNAMIC) {//遍歷phdr尋找DYNAMIC段,存放了字串,方法等偏移地址
continue;
}
*dynamic = reinterpret_cast<Elf32_Dyn*>(load_bias + phdr->p_vaddr);
if (dynamic_count) {
*dynamic_count = (unsigned)(phdr->p_memsz / 8);
}
if (dynamic_flags) {
*dynamic_flags = phdr->p_flags;
}
return;
}
*dynamic = NULL;
if (dynamic_count) {
*dynamic_count = 0;
}
}
複製程式碼
值得注意的是,這裡原始碼載入動態節的時候只載入了第一個動態節,後面的動態節都沒有載入,因此我們可以自己自定義多個programm段中的動態節區,然後在section header中也改變相應的資料,這樣IDA解析的資訊是有所偏差的,因為Android系統實際上只讀取了第一個動態節。 2、解析動態節。 解析程式碼如下:
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
DEBUG("d = %p, d[0](tag) = 0x%08x d[1](val) = 0x%08x", d, d->d_tag, d->d_un.d_val);
switch(d->d_tag){/初始化動態節dynamic section的屬性
case DT_HASH:
si->nbucket = ((unsigned *) (base + d->d_un.d_ptr))[0];
si->nchain = ((unsigned *) (base + d->d_un.d_ptr))[1];
si->bucket = (unsigned *) (base + d->d_un.d_ptr + 8);
si->chain = (unsigned *) (base + d->d_un.d_ptr + 8 + si->nbucket * 4);
break;
case DT_STRTAB:
si->strtab = (const char *) (base + d->d_un.d_ptr);
break;
case DT_SYMTAB:
si->symtab = (Elf32_Sym *) (base + d->d_un.d_ptr);
break;
case DT_PLTREL:
if (d->d_un.d_val != DT_REL) {
DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
return false;
}
break;
case DT_JMPREL:
si->plt_rel = (Elf32_Rel*) (base + d->d_un.d_ptr);
break;
case DT_PLTRELSZ:
si->plt_rel_count = d->d_un.d_val / sizeof(Elf32_Rel);
break;
case DT_REL:
si->rel = (Elf32_Rel*) (base + d->d_un.d_ptr);
break;
case DT_RELSZ:
si->rel_count = d->d_un.d_val / sizeof(Elf32_Rel);
break;
case DT_PLTGOT:
/* Save this in case we decide to do lazy binding. We don't yet. */
si->plt_got = (unsigned *)(base + d->d_un.d_ptr);
break;
case DT_DEBUG:
// Set the DT_DEBUG entry to the address of _r_debug for GDB
// if the dynamic table is writable
if ((dynamic_flags & PF_W) != 0) {
d->d_un.d_val = (int) &_r_debug;
}
break;
case DT_RELA:
DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
return false;
case DT_INIT:
si->init_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_INIT) found at %p", si->name, si->init_func);
break;
case DT_FINI:
si->fini_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func);
break;
case DT_INIT_ARRAY:
si->init_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_INIT_ARRAY) found at %p", si->name, si->init_array);
break;
case DT_INIT_ARRAYSZ:
si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_FINI_ARRAY:
si->fini_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s destructors (DT_FINI_ARRAY) found at %p", si->name, si->fini_array);
break;
case DT_FINI_ARRAYSZ:
si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_PREINIT_ARRAY:
si->preinit_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
DEBUG("%s constructors (DT_PREINIT_ARRAY) found at %p", si->name, si->preinit_array);
break;
case DT_PREINIT_ARRAYSZ:
si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(Elf32_Addr);
break;
case DT_TEXTREL:
si->has_text_relocations = true;
break;
case DT_SYMBOLIC:
si->has_DT_SYMBOLIC = true;
break;
case DT_NEEDED:
++needed_count;
break;
#if defined DT_FLAGS
// TODO: why is DT_FLAGS not defined?
case DT_FLAGS:
if (d->d_un.d_val & DF_TEXTREL) {
si->has_text_relocations = true;
}
if (d->d_un.d_val & DF_SYMBOLIC) {
si->has_DT_SYMBOLIC = true;
}
break;
#endif
#if defined(ANDROID_MIPS_LINKER)
case DT_STRSZ:
case DT_SYMENT:
case DT_RELENT:
break;
case DT_MIPS_RLD_MAP:
// Set the DT_MIPS_RLD_MAP entry to the address of _r_debug for GDB.
{
r_debug** dp = (r_debug**) d->d_un.d_ptr;
*dp = &_r_debug;
}
break;
case DT_MIPS_RLD_VERSION:
case DT_MIPS_FLAGS:
case DT_MIPS_BASE_ADDRESS:
case DT_MIPS_UNREFEXTNO:
break;
case DT_MIPS_SYMTABNO:
si->mips_symtabno = d->d_un.d_val;
break;
case DT_MIPS_LOCAL_GOTNO:
si->mips_local_gotno = d->d_un.d_val;
break;
case DT_MIPS_GOTSYM:
si->mips_gotsym = d->d_un.d_val;
break;
default:
DEBUG("Unused DT entry: type 0x%08x arg 0x%08x", d->d_tag, d->d_un.d_val);
break;
#endif
}
}
複製程式碼
3、載入依賴庫
//********載入依賴庫 (NEEDED) Shared library: [liblog.so]
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
if (d->d_tag == DT_NEEDED) {
const char* library_name = si->strtab + d->d_un.d_val;
DEBUG("%s needs %s", si->name, library_name);
soinfo* lsi = find_library(library_name);
if (lsi == NULL) {
strlcpy(tmp_err_buf, linker_get_error_buffer(), sizeof(tmp_err_buf));
DL_ERR("could not load library \"%s\" needed by \"%s\"; caused by %s",
library_name, si->name, tmp_err_buf);
return false;
}
*pneeded++ = lsi;
}
}
複製程式碼
4、重定位操作
if (si->has_text_relocations) {//重定位操作
/* Unprotect the segments, i.e. make them writable, to allow
* text relocations to work properly. We will later call
* phdr_table_protect_segments() after all of them are applied
* and all constructors are run.
*/
DL_WARN("%s has text relocations. This is wasting memory and is "
"a security risk. Please fix.", si->name);
if (phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias) < 0) {
DL_ERR("can't unprotect loadable segments for \"%s\": %s",
si->name, strerror(errno));
return false;
}
}
if (si->plt_rel != NULL) {//修改資料達到重定位的目的
DEBUG("[ relocating %s plt ]", si->name );
if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) {
return false;//修改資料達到重定位的目的
}
}
if (si->rel != NULL) {
DEBUG("[ relocating %s ]", si->name );
if (soinfo_relocate(si, si->rel, si->rel_count, needed)) {
return false;
}
}
複製程式碼
其中具體的意義參見以下部落格 blog.csdn.net/feibabeibei…
0xFF 收尾
1、dlopen執行完畢後 2、呼叫 vonLoad = dlsym(handle, "JNI_OnLoad")定位JNI_Onload 3、再執行version = (*func)(gDvmJni.jniVm, NULL);執行JNI_Onload 期間可以有三個斷點: .init->.init_array->JNI_Onload->java_com_XX. 反除錯思路: 1、section Header的抹頭操作 2、自定義多個動態節
這裡留個坑,就是未進行對全域性變數gDvm的解析,師傅們多多指教哇~ 參考連結:
blog.csdn.net/feibabeibei… xianzhi.aliyun.com/forum/read/…blog.csdn.net/maspchen/ar…