Android so 文件進階<二> 從dlsym()源碼看android 動態鏈接過程


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);
};
soinfo

然后就是我們關心的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;
}
asm GetProcAddr()

 

dlsym()是通過解析符號來獲得函數地址的,可以考慮一下,在寫native代碼時,函數聲明加 extern"C" 和不加extern "C"通過dlsym()獲得地址有什么不同?dlsym()什么時候會失敗?

Windows下開發dll程序時,加extern"C" 和__delspec(dllexport) 有什么用?如果不加為什么GetProcAddress()會崩潰?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM