0x00 前言
這篇文章其實是我之前學習elf文件關於符號表的學習筆記,網上也有很多關於符號表的文章,怎么說呢,感覺像是在翻譯elf文件格式的文檔一樣,千篇一律,因此把自己的學習筆記分享出來。dlsym()的源碼是分析的android4.4的源碼,android自己實現的bonic C庫。
0x01 基本流程
android中關於elf文件,關於so文件信息的結構體:

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); void CallFunction(const char* function_name, linker_function_t function); };
然后就是我們關心的dlsym()函數,
void*dlsym(void*handle,constchar*symbol)
dlsym()的實現對於handle是分三種情況
(1) handle = RTLD_DEFAULT;
(2) handle = RTLD_NEXT;
(3) 其他,也就是我們平常調用dlopen()的返回值。
0x02 RTLD_DEFAFULT
soinfo* found = NULL; Elf32_Sym* sym = NULL; if (handle == RTLD_DEFAULT) { sym = dlsym_linear_lookup(symbol, &found, NULL); } ///bonic/linker/Linker.cpp Elf32_Sym* dlsym_linear_lookup(const char* name, soinfo** found, soinfo* start) { unsigned elf_hash = elfhash(name); //計算符號名稱的hash值 if (start == NULL) { //static soinfo* solist = &libdl_info; libdl_info是soinfo類型的全局變量包含libdl.so的信息 start = solist; }
接下來就開始循環調用soinfo_elf_lookup() 遍歷soinfo的鏈表
Elf32_Sym* s = NULL; for (soinfo* si = start; (s == NULL) && (si != NULL); si = si->next) { s = soinfo_elf_lookup(si, elf_hash, name); if (s != NULL) { *found = si; break; } }
soinfo_elf_lookup()函數的源碼:
// /bonic/linker/Linker.cpp //soinfo_elf_lookup()在指定模塊查找指定的符號項 static Elf32_Sym* soinfo_elf_lookup(soinfo* si, unsigned hash, const char* name) { Elf32_Sym* symtab = si->symtab; //這里實際上是.dynsym 不要被名字誤導 const char* strtab = si->strtab; //這里實際上是.dynstr TRACE_TYPE(LOOKUP, "SEARCH %s in %s@0x%08x %08x %d", name, si->name, si->base, hash, hash % si->nbucket); for (unsigned n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n]) { Elf32_Sym* s = symtab + n; if (strcmp(strtab + s->st_name, name)) continue; /* only concern ourselves with global and weak symbol definitions */ switch(ELF32_ST_BIND(s->st_info)){ case STB_GLOBAL: case STB_WEAK: if (s->st_shndx == SHN_UNDEF) { continue; } TRACE_TYPE(LOOKUP, "FOUND %s in %s (%08x) %d", name, si->name, s->st_value, s->st_size); return s; } } return NULL; } if (s != NULL) { TRACE_TYPE(LOOKUP, "%s s->st_value = 0x%08x, found->base = 0x%08x", name, s->st_value, (*found)->base); } return s; }
需要說明的兩點:
1. symtab 和strtab
Elf32_Sym* symtab = si->symtab;
const char* strtab = si->strtab;
這兩句代碼,可以跟入dlopen()的源碼看到soinfo_link_image()函數中對si->symtab和si->strtab的賦值,這里只截取部分代碼:
for (Elf32_Dyn* d = si->dynamic; d->d_tag != DT_NULL; ++d) { switch(d->d_tag){ 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: //.dynsym si->strtab = (const char *) (base + d->d_un.d_ptr); break; case DT_SYMTAB: //.dynstr si->symtab = (Elf32_Sym *) (base + d->d_un.d_ptr); break; } }
可以看到strtab和symtab實際是.dynsym 和.dynstr。
可以看到循環條件中的si->dynamic,.dynamic section 保存了動態鏈接需要的基本信息,比如依賴哪些共享對象,動態鏈接符號表(.dynsym)的位置,動態鏈接重定位表(.rel.dyn)的位置,共享對象初始化代碼的地址等等。.dynamic section可以看成動態鏈接下的類似的ELF“文件頭”。
typedef struct { Elf32_Word d_tag; /* entry tag value */ union { Elf32_Addr d_ptr; Elf32_Word d_val; } d_un; } Elf32_Dyn; typedef struct { Elf64_Xword d_tag; /* entry tag value */ union { Elf64_Addr d_ptr; Elf64_Xword d_val; } d_un; } Elf64_Dyn;
d_un聯合體的取值很據d_tag來定,這里列舉常用的一些:
d_tag |
d_un |
作用 |
DT_SYMTAB |
d_ptr .dynsym section addr |
確定.dynsym section |
DT_STRTAB |
d_ptr .dynstr section addr |
確定 .dynstr |
DT_STRSZ |
d_val .dynstr section size (byte) |
|
DT_REL |
d_ptr .rel.dyn addr |
確定.rel.dyn |
DT_RELSZ |
d_val .rel.dyn size (byte) |
|
DT_JMPREL |
d_ptr .rel.plt addr |
確定.rel.plt |
DT_PLTRELSZ |
d_val .rel.plt size (byte) |
|
DT_INIT_ARRAY |
d_ptr .init_array addr |
確定 .init_array |
DT_INIT_ARRAYSZ |
d_val .init_arrary size (byte) |
|
DT_FINT_ARRAY |
d_ptr .fint_array addr |
確定.finit_array |
DT_FINT_ARRAYSZ |
d_val .fint_array size (byte) |
2. 迭代的條件
for (unsigned n = si->bucket[hash % si->nbucket]; n != 0; n = si->chain[n])
迭代的條件根據符號表(這里就是.dynsym)中Elf32_Sym的存儲結構來的
舉個很簡單的例子來理解這個結構:
一個由函數名hash獲得的值為X,那么bucket[X%nbucket]將給出一個索引Y,即是符號表項的索引也是chain表的索引,如果根據Y得到符號表項不滿足條件,chain[Y]將給出下一個符號表項(同樣的哈希變量),可以一直沿着“chain鏈”直到選擇到期望名字的符號表項。
android 源碼下的elfhash函數/bonic/linker/Linker.cpp
static unsigned elfhash(const char* _name) { const unsigned char* name = (const unsigned char*) _name; unsigned h = 0, g; while(*name) { h = (h << 4) + *name++; g = h & 0xf0000000; h ^= g; h ^= g >> 24; } return h; }
0x03 RTLD_NEXT
else if (handle == RTLD_NEXT) { void* ret_addr = __builtin_return_address(0); soinfo* si = find_containing_library(ret_addr); sym = NULL; if (si && si->next) { sym = dlsym_linear_lookup(symbol, &found, si->next); } }
__builtin_return_address(0)是什么?
它其實是一個gcc的內置函數,用於幫助獲取給定函數的調用地址,此處即是要獲取dlsym()函數的調用地址。
__builtin_return_address()接收一個稱為level的參數。這個參數定義希望獲取返回地址的調用堆棧級別。例如,如果指定level為0,那么就是請求當前函數的返回地址。如果指定level為1,那么就是請求調用了當前函數的函數的返回地址,以此類推。
接下來的find_containing_library()
soinfo* find_containing_library(const void* p) { Elf32_Addr address = reinterpret_cast<Elf32_Addr>(p); //類型轉換 for (soinfo* si = solist; si != NULL; si = si->next) { if (address >= si->base && address - si->base < si->size) { return si; } } return NULL; }
//static soinfo* solist = &libdl_info;
這里的solist是不是很熟悉,就是之前在dym_linear_lookup()也使用soinfo結構的鏈表。
很明顯find_containing_library()就是獲得調用dlsym()函數的模塊信息soinfo。
接下來也同樣是調用dlsym_linear_lookup()函數,與RTLD_DEAFULT不同的是start參數的不同,從指定的soinfo開始查詢,也就是dlsym()的調用模塊。
0x04 dlopen()
else { found = reinterpret_cast<soinfo*>(handle); //類型轉換 sym = dlsym_handle_lookup(found, symbol); }
很明顯dlopen()返回值其實就是指定模塊對應的信息soinfo結構體的地址。
Elf32_Sym* dlsym_handle_lookup(soinfo* si, const char* name) { return soinfo_elf_lookup(si, elfhash(name), name); }
在dlsym_handle_lookup()也知道簡單的封裝,只是調用了一次soinfo_elf_lookup()來查找指定的符號項。而soinfo_elf_lookup()的具體實現之前已經討論了。
0x05 剩下的部分
if (sym != NULL) { unsigned bind = ELF32_ST_BIND(sym->st_info); //是全局符號,並且不是section索引不是SHN_UNDEF(如果是SHN_UNDEF)說明這個 //符號的定義並不在本文件中) if (bind == STB_GLOBAL && sym->st_shndx != 0) { unsigned ret = sym->st_value + found->load_bias; //load_bias :.so文件加載的虛擬基地址 return (void*) ret; } __bionic_format_dlerror("symbol found but not global", symbol); return NULL; } else { __bionic_format_dlerror("undefined symbol", symbol); return NULL; } }
#define SHN_UNDEF 0 /* Undefined section */ #define ELF_ST_BIND(info) ((uint32_t)(info) >> 4) #define ELF32_ST_BIND(info) ELF_ST_BIND(info)
剩下的代碼需要了解Elf32_Sym結構體各個字段的含義
typedef struct { Elf32_Word st_name; /* Symbol name (.strtab index) */ Elf32_Word st_value; /* value of symbol */ Elf32_Word st_size; /* size of symbol */ Elf_Byte st_info; /* type / binding attrs */ Elf_Byte st_other; /* unused */ Elf32_Half st_shndx; /* section index of symbol */ } Elf32_Sym;
st_name:
符號的名字,但它並不是一個字符串,而是字符串表(.strtab或者.dynstr)中的一個索引值,在字符串表中該索引值的位置上存放的字符串就是該符號名字的實際文本。如果此值不為0,則它就代表符號名字在字符串表中的索引值;如果此值為0,則表示此符號沒有名字。
st_value:
符號的值;這個字段的值沒有固定的類型,它可能代表一個數值,也可能是一個地址,具體要依據上下文來確定;
- 在可重定位文件中,st_value 包含節索引為 SHN_COMMON 的符號的對齊約束。
- 在可重定位文件中,st_value 包含所定義符號的節偏移。st_value 表示從 st_shndx 所標識的節的起始位置的偏移。
- 在可執行文件和共享目標文件中,st_value 包含虛擬地址。為使這些文件的符號更適用於運行時鏈接程序,節偏移(文件解釋)會替換為與節編號無關的虛擬地址(內存解釋)。(根據源碼,顯然在android的bonic中這里的虛擬地址是指相對於加載基地址的偏移)。
例如:一個可執行文件中含有一個函數的引用,而這個函數被定義在一個共享目標文件中,那么在可執行文件中,針對那個共享目標文件的符號表中就應該含有這個函數的符號;符號表的st_shndx字段的值為SHN_UNDEF,這就告訴動態鏈接器,這個函數的符號定義並不在可執行文件中;如果已經在可執行文件中給這個符號申請了一個函數連接表項,而且符號表項的st_value字段的值不是0,那么st_value字段的值就將是函數連接表項中第一條指令的地址;否則,st_value字段的值就是0;這個函數連接表項的地址被動態鏈接器用來解析函數地址;
st_size:
符號的大小;各種符號的大小各不相同,比如一個對象的大小就是它實際占用的字節數;如果一個符號的大小為0,或者大小未知,則這個值為0;
0x06 小結
android 下打包進apk的so文件不存在.symtab和.strtab section ,dlsym()函數所做的就是解析so文件的.dynamic 通過符號表來獲得函數的地址。而Windows下的應用層用GetProcAddress()獲得動態鏈接庫(dll)獲得導出函數地址,內核層用MmGetSystemRoutinAddr()獲的ntoskrnl.exe的導出表中的地址。Windows下的與elf不同,Windwos是通過PE文件的導出表獲得函數地址。這里給出《0day》里面關於自己實現GetProcAddress()功能的匯編代碼。

#include <IOSTREAM> #include <WINDOWS.H> using namespace std; //GetProcAddress() 的匯編層實現 //MessageBoxA GetHash--->0x1e380a6a //ExitProcess GetHash--->0x4fd18963 //LoadLibraryA GetHash--->0x0c917432 DWORD GetHash(char *pFuncName) { DWORD digest = 0; while(*pFuncName) { digest = ((digest<<25)|digest>>7); digest += *pFuncName; pFuncName++; } return digest; } //在將hash壓入棧中之前,注意先將增量標志位DF清零, //當shellcode 是利用異常處理機制植入的時候, //往往產生標志位的變化,使shellcode中的字符處理方向發生變化而發生錯誤 void Func() { _asm{ // ;find base addr of kernel32.dll mov ebx , fs:[edx+0x30] //ebx = addr of PEB mov ecx , [ebx+0x0c] //Ldr // typedef struct _PEB_LDR_DATA32 // { // ULONG Length; +0x00 // BOOLEAN Initialized; +0x04 // HANDLE SsHandle; +0x08 // LIST_ENTRY InLoadOrderModuleList; +0x0c //按模塊的加載順序 // LIST_ENTRY InMemoryOrderModuleList; +0x14 //按模塊在內存中的地址順序 // LIST_ENTRY InInitializationOrderModuleList; +0x1c //按初始化順序 // PVOID EntryInProgress; +0x24 // } PEB_LDR_DATA32, *PPEB_LDR_DATA32; mov ecx , [ecx+0x1c] //ecx -->xxx.exe mov ebp , [ecx+0x08] //ebp = addr of kernel32 } } int main() { _asm{ CLD push 0x1e380a6a //MessageBoxA push 0x4fd18963 //ExitProcess push 0x0c917432 //LoadLibraryA mov esi ,esp lea edi ,[esi-0x0c] //抬高棧頂 xor ebx ,ebx sub esp ,0x04 //????? //push a pointer to "user32" onto stack mov bx , 0x3233 //reset of ebx is null push ebx push 0x72657375 push esp xor edx , edx //find base addr of kernel32.dll mov ebx , fs:[edx+0x30] //ebx = address of PEB mov ecx , [ebx+0x0c] //ecx = pointer to loader data mov ecx , [ecx+0x1c] //ecx = first entry in initialization order list mov ecx , [ecx] //ecx = second entry in list (kernel32.dll) mov ebp , [ecx+0x08] //ebp = base address of kernel32.dll find_lib_function: lodsd //load next hash into al and increment esi cmp eax , 0x1e380a6a //hash of MessageBoxA - trigger //LoadLibrary("user32") jne find_functions schg eax , ebp //save current hash call [edi-0x08] //LoadLibraryA xchg eax , ebp //restore current hash, and update ebp //with base address of user32.dll find_function: pushed //preserve registers mov eax , [ebp+0x3c] //eax = start of PE header mov ecx , [ebp+eax+0x78] //ecx = relative offset of export table add ecx , ebp //ecx = absolute addr of export table mov ebx , [ecx+0x20] //ebx = relative offset of name table add ebx , ebp //ebx = absolute addr of name table xor edi , edi //edi will count throght the function next_function_loop: inc edi //increment function counter mov esi , [ebx+edi*4] add esi , ebp cdq hash_loop: movsx eax , byte ptr[esi] cmp al , ah jz compare_hsah ror edx , 7 add edx , eax inc esi jmp hash_loop compare_hash: cmp edx , [esp+0x1c] } return 0; }
dlsym()是通過解析符號來獲得函數地址的,可以考慮一下,在寫native代碼時,函數聲明加 extern"C" 和不加extern "C"通過dlsym()獲得地址有什么不同?dlsym()什么時候會失敗?
Windows下開發dll程序時,加extern"C" 和__delspec(dllexport) 有什么用?如果不加為什么GetProcAddress()會崩潰?