07-ELF文件格式分析

1.目标文件格式

链接视图和执行视图

链接视图:elf未加载到内存中时完整的文件结构

执行视图:elf加载到内存中时的文件结构

 

 

其对应关系如下:

 

 

在ELF文件中section是最小的不可分割的元素。

当so加载到文件中,多个section被组合成segment,segment此时是最小的不可分割的元素。

相关源码

在/android/4.2.2/bionic/linker/linker.c:

static soinfo* load_library(const char* name)

{

    //1.打开so文件,获取文件描述符

    scoped_fd fd;

    fd.fd = open_library(name);

    if (fd.fd == -1) {

        DL_ERR("library \"%s\" not found", name);

        return NULL;

    }

    // 2.读取ELF header.

    Elf32_Ehdr header[1];

    int ret = TEMP_FAILURE_RETRY(read(fd.fd, (void*)header, sizeof(header)));

    ......

    // 3.验证ELF header.

    if (verify_elf_header(header) < 0) {

        DL_ERR("not a valid ELF executable: %s", name);

        return NULL;

    }

    //4.从so文件读取并分配内存,然后加载 program header table到内存.

    const Elf32_Phdr* phdr_table;

    phdr_ptr phdr_holder;

    ret = phdr_table_load(fd.fd, header->e_phoff, header->e_phnum,

                          &phdr_holder.phdr_mmap, &phdr_holder.phdr_size, &phdr_table);

    if (ret < 0) {

        DL_ERR("can't load program header table: %s: %s", name, strerror(errno));

        return NULL;

    }

    size_t phdr_count = header->e_phnum;

    // 5.遍历program header table,根据LOAD属性的segment,分配足够大的内存空间,

   //用于保存所有LOAD属性的segment对应的空间

    Elf32_Addr ext_sz = phdr_table_get_load_size(phdr_table, phdr_count);

    ……

    // 6.分配足够大的内存空间用于加载所有LOAD属性的segment.

    void* load_start = NULL;

    Elf32_Addr load_size = 0;

    Elf32_Addr load_bias = 0;

    ret = phdr_table_reserve_memory(phdr_table,

                                    phdr_count,

                                    &load_start,

                                    &load_size,

                                    &load_bias);

    ……

    

    //7.加载所有LOAD属性的segment到进程空间中(上一步分配好的空间)

    ret = phdr_table_load_segments(phdr_table,

                                   phdr_count,

                                   load_bias,

                                   fd.fd);

    ……

}

 

加固提示

elf文件中section header table未加载到内存中,只是在静态分析时用到。即使完全删除也不影响so文件的加载和正常使用。在加固时候,可以篡改section header table让反编译工具解析失败或者完全删除section header table。

2.ELF Header部分

例子

这里,我们使用ndk写一个简单demo,作为后面几个部分文件结构分析使用。

 

 

demo1.cpp

#include "com_demo_MainActivity.h"

#include <stdio.h>

 

void init_func() __attribute__((constructor));

void fini_func() __attribute__((destructor));

 

 

void init_func(){

puts("---------------- init_func() ---------------------");

}

 

void fini_func(){

puts("---------------- fini_func() ---------------------");

}

 

int global_init_var = 99;

int global_unint_var;

 

 

JNIEXPORT void JNICALL Java_com_demo_MainActivity_test

  (JNIEnv * env, jobject obj){

   global_unint_var = 1;

 

   puts("1st call puts!");

   puts("2st call puts!");

}

然后使用ndk-build编译成demo1.so文件即可。

基本概念

位置:elf.h

typedef struct elf32_hdr{

  unsigned char e_ident[EI_NIDENT];

  Elf32_Half e_type;

  Elf32_Half e_machine;

  Elf32_Word e_version;

  Elf32_Addr e_entry;  /* Entry point */

  Elf32_Off e_phoff;

  Elf32_Off e_shoff;

  Elf32_Word e_flags;

  Elf32_Half e_ehsize;

  Elf32_Half e_phentsize;

  Elf32_Half e_phnum;

  Elf32_Half e_shentsize;

  Elf32_Half e_shnum;

  Elf32_Half e_shstrndx;

} Elf32_Ehdr;

 

 

几个关键的字段

字段名

注释

e_phoff

Program Header Table偏移量

e_shoff

Section Header Table偏移量

e_ehsize

ELF Header大小

e_phentsize

Program Header Table的表项大小

e_phnum

Program Header Table的表项数量

e_shentsize

Section Header Table的表项大小

e_shnum

Section Header Table的表项数量

e_shstrndx

Section Header Table中节区名称字符串表索引

借助readelf工具或010 Editor分析上面libdemo1.so中的ELF Header结构,如下:

 

 

相关源码

位置: 4.2.2/bionic/linker/linker.c

static int

verify_elf_header(const Elf32_Ehdr* hdr)

{

    if (hdr->e_ident[EI_MAG0] != ELFMAG0) return -1;

    if (hdr->e_ident[EI_MAG1] != ELFMAG1) return -1;

    if (hdr->e_ident[EI_MAG2] != ELFMAG2) return -1;

    if (hdr->e_ident[EI_MAG3] != ELFMAG3) return -1;

    if (hdr->e_type != ET_DYN) return -1;

    /* TODO: Should we verify anything else in the header? */

#ifdef ANDROID_ARM_LINKER

    if (hdr->e_machine != EM_ARM) return -1;

#elif defined(ANDROID_X86_LINKER)

    if (hdr->e_machine != EM_386) return -1;

#elif defined(ANDROID_MIPS_LINKER)

    if (hdr->e_machine != EM_MIPS) return -1;

#endif

    return 0;

}

 

位置:android7.1.1_r6/bionic/linker/linker_phdr.cpp

bool ElfReader::VerifyElfHeader() {

  if (memcmp(header_.e_ident, ELFMAG, SELFMAG) != 0) {

    DL_ERR("\"%s\" has bad ELF magic", name_.c_str());

    return false;

  }

 

  // Try to give a clear diagnostic for ELF class mismatches, since they're

  // an easy mistake to make during the 32-bit/64-bit transition period.

  int elf_class = header_.e_ident[EI_CLASS];

#if defined(__LP64__)

  if (elf_class != ELFCLASS64) {

    if (elf_class == ELFCLASS32) {

      DL_ERR("\"%s\" is 32-bit instead of 64-bit", name_.c_str());

    } else {

      DL_ERR("\"%s\" has unknown ELF class: %d", name_.c_str(), elf_class);

    }

    return false;

  }

#else

  if (elf_class != ELFCLASS32) {

    if (elf_class == ELFCLASS64) {

      DL_ERR("\"%s\" is 64-bit instead of 32-bit", name_.c_str());

    } else {

      DL_ERR("\"%s\" has unknown ELF class: %d", name_.c_str(), elf_class);

    }

    return false;

  }

#endif

 

  if (header_.e_ident[EI_DATA] != ELFDATA2LSB) {

    DL_ERR("\"%s\" not little-endian: %d", name_.c_str(), header_.e_ident[EI_DATA]);

    return false;

  }

 

  if (header_.e_type != ET_DYN) {

    DL_ERR("\"%s\" has unexpected e_type: %d", name_.c_str(), header_.e_type);

    return false;

  }

 

  if (header_.e_version != EV_CURRENT) {

    DL_ERR("\"%s\" has unexpected e_version: %d", name_.c_str(), header_.e_version);

    return false;

  }

 

  if (header_.e_machine != GetTargetElfMachine()) {

    DL_ERR("\"%s\" has unexpected e_machine: %d", name_.c_str(), header_.e_machine);

    return false;

  }

 

  return true;

}

Android4.2.2中verify_elf_object检测的elf header字段较少。这里我们用Android7.1.1中VerifyElfHeader检测的比较多得为准。VerifyElfHeader用于检测加载到内存中的elf header部分字段是否合法。

提示:由上面的源码可知,e_entry、e_flag、e_ident中EI_VERSION和EI_PAD未检查。这4个空闲的字段可在加固的时候用于其他用途,例如用于保存某section或方法的位移地址、大小之类的。

3.Section Header Table部分

基本概念

typedef struct elf32_shdr {

  Elf32_Word sh_name;      //指向.shstrtab字符串表的索引位置

  Elf32_Word sh_type;

  Elf32_Word sh_flags;       //定义节区的属性:可写、执行、是否占用内存。

  Elf32_Addr sh_addr;       //对应section在内存中的虚拟地址,一般跟sh_offset一样

  Elf32_Off sh_offset;      //对应section在so文件中的偏移量

  Elf32_Word sh_size;        //对应section的大小

  Elf32_Word sh_link;

  Elf32_Word sh_info;

  Elf32_Word sh_addralign;

  Elf32_Word sh_entsize;

} Elf32_Shdr;

section对应在内存的实际地址为:base + sh_addr,其中base为so文件加载到内存的基地址。例如libdemo1.so在内存的基地址:

 

 

则base=0x4c142000,即section对应内存的时机地址为:0x4c142000+sh_addr。

借助readelf工具或010 Editor分析上面libdemo1.so中的Section Header Table结构,如下:

 

 

几个比较重要的section:

节区名

注释

.interp

指定装在动态链接器的路径,如:/system/bin/linker

.dynsym

包含dynamic symbol table

.dynstr

包含动态符号名称的字符串表,与dynsym对应

.hash

哈希表,用于定位函数符号

.rel.dyn

重定位表,这里是对导入数据进行重定位

.rel.plt

重定位表,这里是对导入函数进行重定位

.plt

包含过程连接表(procedure linkage table),用于延时绑定符号

.text

可执行代码保存在这个section

.rodata

保存只读数据

.fini_array

进程终止代码的一部分。当程序正常退出时,系统将安排执行这里的代码。相当于C++的析构函数。

.init_array

进程初始化代码的一部分。当程序开始执行时,系统要在开始调用主程序入口之前执行这些代码。相当于C++的构造函数。

.dynamic

dynamic link table,包含动态链接信息

.got

保存全局变量和导入的函数引用的地址

.data

保存初始化的数据

.bss

未初始化的数据,在elf文件中不分配空间,加载到内存的时候分配空间

.shstrtab

节区名称字符串表

加固提示

由链接视图和执行视图,我们知道so加载到内存中时section header table是没有用的,它主要是有助于静态分析elf文件结构。在对so做加固的时候,可以篡改或删除section header table的数据,增加静态分析的难度。

4..Program Header Table部分

基本概念

位置:elf.h

typedef struct elf32_phdr{

  Elf32_Word p_type;

  Elf32_Off p_offset;

  Elf32_Addr p_vaddr;

  Elf32_Addr p_paddr;

  Elf32_Word p_filesz;

  Elf32_Word p_memsz;

  Elf32_Word p_flags;

  Elf32_Word p_align;

} Elf32_Phdr;

借助readelf工具或010 Editor分析上面libdemo1.so中的Program Header结构,如下:

 

 

结合上面的执行视图和linker源码,我们知道,LOAD类型表示会加载到内存中。

这里我们主要关注p_type=PT_LOAD这个类型。

PHDR[2]表项,在so中范围为offset~offset+FileSiz,即:0x0~1ee0。同理。

PHDR[3]表项,在so中范围为:0x2ea4~0x300c。其对应的so文件范围如下:

 

 

相关源码

在/android/4.2.2/bionic/linker/linker.cpp:

int phdr_table_load_segments(const Elf32_Phdr* phdr_table,

                         int               phdr_count,

                         Elf32_Addr        load_bias,

                         int               fd)

{

    int nn;

    for (nn = 0; nn < phdr_count; nn++) {

        const Elf32_Phdr* phdr = &phdr_table[nn];

        void* seg_addr;

        if (phdr->p_type != PT_LOAD)

            continue;

        /* Segment addresses in memory */

        Elf32_Addr seg_start = phdr->p_vaddr + load_bias;

        Elf32_Addr seg_end   = seg_start + phdr->p_memsz;

        Elf32_Addr seg_page_start = PAGE_START(seg_start);

        Elf32_Addr seg_page_end   = PAGE_END(seg_end);

        Elf32_Addr seg_file_end   = seg_start + phdr->p_filesz;

        /* File offsets */

        Elf32_Addr file_start = phdr->p_offset;

        Elf32_Addr file_end   = file_start + phdr->p_filesz;

        Elf32_Addr file_page_start = PAGE_START(file_start);

        Elf32_Addr file_page_end   = PAGE_END(file_end);

        seg_addr = mmap((void*)seg_page_start,

                        file_end - file_page_start,

                        PFLAGS_TO_PROT(phdr->p_flags),

                        MAP_FIXED|MAP_PRIVATE,

                        fd,

                        file_page_start);

        if (seg_addr == MAP_FAILED) {

            return -1;

        }

        /* if the segment is writable, and does not end on a page boundary,

         * zero-fill it until the page limit. */

        if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {

            memset((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((void*)seg_file_end,

                                    seg_page_end - seg_file_end,

                                    PFLAGS_TO_PROT(phdr->p_flags),

                                    MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,

                                    -1,

                                    0);

            if (zeromap == MAP_FAILED) {

                return -1;

            }

        }

    }

    return 0;

}

有上面的源码可知,linker通过解析Program Header Table来分配内存空间并加载LOAD类型的表项内容。

我们通过下述两个命令来查看libdemo1.so加载到内存的位置。

 

 

 

TODO

有两个LOAD加载到内存,为什么实际分成3段?

我们看section header table中flag字段属性。如下:

其实第一个LOAD段是可以分成两部分的。可以看到”.interp”到”.rel.plt”节是只读属性,然后”.plt”和”.text”是可读可执行属性,”.ARM.extab”、”.ARM.exidx”和”.rodata”是可读属性。然后第二个LOAD段则全是可读可写属性。

所以实际加载到内存的时候,是分成了2段来加载。这也为什么实际内存分布有3段。主要是因为根据section的属性来分配的。

 对应源码的过程如何???

 

 

 

5.interp节区解析

“.interp”主要是指定linker的加载路径。通过解析Program Header Table,我们知道LOAD属性的segment是包含”.interp”节区的,也就是会加载到内存中让系统读取。

 通过010 Editor查看”.interp”节区:

 

 

 

也就是说该so文件指定了android系统中”/system/bin/linker”作为动态链接器。我们可以通过下述命令查看上面demo中so文件的内存状态:

 

 

可以看到该so文件使用的动态链接器正是”.interp”节区指定的。

6.dynsym节区解析

基本概念

在这个节区里面存储了导入和导出的函数和变量。

typedef struct elf32_sym{

  Elf32_Word st_name;   //动态符号名称字符串表的索引,指向dynstr

  Elf32_Addr st_value;   //可能是地址、值等等,根据具体上下文来看

  Elf32_Word st_size;

  unsigned char st_info;   //说明符号是数据还是函数类型,以及符号的属性

  unsigned char st_other;

  Elf32_Half st_shndx;   //指定符号所在section

} Elf32_Sym;

实例分析

通过readelf –s libdemo1.so查看dynsym节区:

 

 

例如上面的symtab[11] Java_com_demo_MainActivity。st_shndx=8,如下,就是”.text”存放代码的section。

而st_shndx为UND表示该符号不是在该so模块中定义,是导入的符号。

通过ELF32_ST_TYPE(st_info)知道该符号类型是函数。同样的。

symtab[10] global_uint_var是在17,即”.bss”存放未初始化变量的section。

symtab[12] global_init_var是在16,即”.data”存放初始化的变量的section。

跟demo1.cpp源码保持一致:

 

 

7.dynstr节区解析

保存dynamic symbol table中符号名称字符串的表,上一节中Elf32_Sym结构体中的st_name正是指向这个符号名称字符串表。

我们借助010 editor工具查看libdemo1.so的”.dynstr”节区。

 

 

8.shstrtab节区解析

保存section名称的字符串表,Elf32_Shdr结构体的sh_name索引正是指向这个字符串表。

我们借助010 editor工具查看libdemo1.so的”.shstrtab”节区。

 

 

9.data和bss解析

基本概念

l 初始化的变量保存在”.data”处。

“.data”节区保存的是初始化变量的值。

l 未初始化的变量保存在”.bss”处,默认初始化为0,在so文件中”.bss”不分配空间

实例分析

(1)“.data”节区分析:

 

 

 

我们在静态分析的时候使用s_addr,即变量在的虚拟地址。看看下面ida的分析:

 

 

其映射关系如下:

物理地址s_offset

虚拟地址s_addr

0x3000

.data 00004000

0

0x3004

.data 00004004

0x63

当libdemo1.so加载到内存中时,”.data”节区中变量在内存中的地址为:base + s_addr,其中base是libdemo1.so在内存中的初始地址。如下,我们查看libdemo1.so的内存情况,则其base=0x4c142000。

 

 

(2)“.bss”节区分析:

 

 

有上面知道Elf32_Shdr结构的s_name属性为SHT_NOBITS,所以此时s_size即使为4非0,但实际是不占用文件空间。

 

 

10.rel.dyn和.rel.plt解析

基本概念

(1) “.rel.dyn”和”.rel.plt”两个节区,存储了那些在”.dynsym”和”.plt”中链接时需要重定位的符号。他们的作用是在加载so文件初始化时,告诉linker那些符号需要重定位。具体过程见相关源码部分分析。

(2).rel.dyn记录了加载时需要重定位的变量

(3).rel.plt记录的了需要重定位的函数

(4)“.dynsym”、”.plt”、”.got”、”.rel.dyn”、”.rel.plt”之间关系如下图所示:

 

 

.rel.dyn和.rel.plt重定位表项结构如下(”.rel”开头的都是重定位表):

Typedef struct{

    Elf32_Addr r_offset;   

    Elf32_Word r_info;    

} Elf32_Rel

r_offset表示在虚拟内存中的地址

#define ELF32_R_SYM(i) ((i)>>8),符号索引,指向dynamic symbol table,即”.dynsym”节区

#define ELF32_R_TYPE(i) ((unsigned char)(i)),重定位类型

在ARM结构体中,一共有5种类型:

重定位类型

计算方式

R_ARM_JUMP_SLOT

*((unsigned*)reloc) = sym_addr;

R_ARM_GLOB_DAT

*((unsigned*)reloc) = sym_addr;

R_ARM_ABS32

*((unsigned*)reloc) += sym_addr;

R_ARM_REL32

*((unsigned*)reloc) += sym_addr - rel->r_offset;

R_ARM_RELATIVE

*((unsigned*)reloc) += si->base;

实例分析

.rel.dyn包含需要重定位的导入变量,使用”readelf –r libdemo1.so”查看:

 

 

通过Elf32_Rel的r_info获取对应的符号索引表如下:

需要重定位的变量索引表

对应动态符号名称

0x17>>8=0

0xc15>>8=0xc=12

__gnu_Unwind_Find_exidx

0x2215>>8=0x22=34

__cxa_call_unexpected

.rel.plt包含需要重定位的导入函数,使用”readelf –r libdemo1.so”查看:

 

 

通过Elf32_Rel的r_info获取对应的符号索引表如下:

需要重定位的变量索引表

对应动态符号名称

0x216>>8=0x2=2

__cxa_atexit

0x116>>8=0x1=1

__cxa_finalize

0x416>>8=0x7=4

puts

0xc16>>8=0xc=12

__gnu_Unwind_Find_exid

0x1216>>8=0x12=18

abort

0x1416>>8=0x14=20

memcpy

0x1f16>>8=0x1f=31

__cxa_begin_cleanup

0x2016>>8=0x20=32

__cxa_type_match

结合上述两个重定位表,参考”.dynsym”节区,如下:

 

 

 

再看看GOT表,我们发现”.rel.plt”和”.rel.dyn”中r_offset其实是跳转到GOT表位置。GOT表中第二列其实就是那些需要重定位的符号地址,在linker加载so重定位时,其实就是修改这里的地址指向符号的内存地址。

具体过程见下面源码。

 

 

 

相关源码

在/android/4.2.2/bionic/linker/linker.c:

static int soinfo_link_image(soinfo* si)

{

….

//符号重定位

  if(si->plt_rel) {

        if(soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed))

            goto fail;

    }

    //符号重定位

    if(si->rel) {

        if(soinfo_relocate(si, si->rel, si->rel_count, needed))

            goto fail;

    }

 

….

}

static int soinfo_relocate(soinfo *si, Elf32_Rel *rel, unsigned count,

                           soinfo *needed[])

{

    Elf32_Sym *symtab = si->symtab;

    const char *strtab = si->strtab;

    Elf32_Sym *s;

    Elf32_Addr offset;

    Elf32_Rel *start = rel;

    for (size_t idx = 0; idx < count; ++idx, ++rel) {

        unsigned type = ELF32_R_TYPE(rel->r_info); //重定位类型

        unsigned sym = ELF32_R_SYM(rel->r_info);   //重定位的符号表索引

        unsigned reloc = (unsigned)(rel->r_offset + si->load_bias);  //需要重定位的地址

        unsigned sym_addr = 0;

        char *sym_name = NULL;

        DEBUG("%5d Processing '%s' relocation at index %d\n", pid,

              si->name, idx);

        if (type == 0) { // R_*_NONE

            continue;

        }

        //1.获取sym_addr符号地址

        if(sym != 0) {

            sym_name = (char *)(strtab + symtab[sym].st_name);

            bool ignore_local = false;

            ignore_local = (type == R_ARM_COPY);

            //查找sym_name定义在哪个so

            s = soinfo_do_lookup(si, sym_name, &offset, needed, ignore_local);

            if(s == NULL) {

                s = &symtab[sym];

                if (ELF32_ST_BIND(s->st_info) != STB_WEAK) {

                    DL_ERR("cannot locate symbol \"%s\" referenced by \"%s\"...", sym_name, si->name);

                    return -1;

                }

                ......

  

            } else {

                /* We got a definition.  */

                sym_addr = (unsigned)(s->st_value + offset);

            }

            count_relocation(kRelocSymbol);

        } else {

            s = NULL;

        }

        //2.根据重定位类型进行地址修复

        switch(type){

        case R_ARM_JUMP_SLOT:

            count_relocation(kRelocAbsolute);

            MARK(rel->r_offset);

            *((unsigned*)reloc) = sym_addr;

            break;

        case R_ARM_GLOB_DAT:

            count_relocation(kRelocAbsolute);

            MARK(rel->r_offset);

            *((unsigned*)reloc) = sym_addr;

            break;

        case R_ARM_ABS32:

            count_relocation(kRelocAbsolute);

            MARK(rel->r_offset);

            *((unsigned*)reloc) += sym_addr;

            break;

        case R_ARM_REL32:

            count_relocation(kRelocRelative);

            MARK(rel->r_offset);

            *((unsigned*)reloc) += sym_addr - rel->r_offset;

            break;

        case R_ARM_RELATIVE:

            count_relocation(kRelocRelative);

            MARK(rel->r_offset);

            if (sym) {

                DL_ERR("odd RELATIVE form...", pid);

                return -1;

            }

            *((unsigned*)reloc) += si->base;

            break;

        default:

            DL_ERR("unknown reloc type %d @ %p (%d)",

                   type, rel, (int) (rel - start));

            return -1;

        }

    }

    return 0;

}

由上面的源码分析知,linker在初始化so文件时,通过”.rel.plt”和”.rel.dyn”跳转到GOT表,并修改GOT表对应符号在内存地址,完成重定位过程。过程如下:

 

 

11.plt和.got解析

基本概念

.plt和.got的关系如下:

当一个外部符号被调用时,PLT去引用GOT中的其付哈哦对应的绝对地址,然后转入并执行。

“.got”:全局偏移表用于记录在elf文件中所导入的符号的绝对地址。

实例分析

“.plt”节区分析

“.plt”节区的内容如下:

 

 

我们用ndk自带的objdump反编译libdemo1.so文件:

arm-linux-androideabi-objdump.exe -D libdemo1.so > out.txt

如下,PLT表一共有8个表项,查看”rel.plt”节区确实有8个符号需要进行重定位。

 

 

Disassembly of section .plt:

 

00000c24 <__cxa_atexit@plt-0x14>:

 c24: e52de004 push {lr} ; (str lr, [sp, #-4]!)

 c28: e59fe004 ldr lr, [pc, #4] ; c34 <__cxa_atexit@plt-0x4>

 c2c: e08fe00e add lr, pc, lr

 c30: e5bef008 ldr pc, [lr, #8]!

 c34: 000033a0 andeq r3, r0, r0, lsr #7

 

 

 

 

 

“.plt”节区开头是一个4x5=20bytes的固定结构,见左边黄色范围。

 

plt表的每个表项大小为:3x4=12bytes

 

模型如下:

4 bytes

4 bytes

4 bytes

4 bytes

4 bytes

12*n bytes

 

00000c38 <__cxa_atexit@plt>:

 c38: e28fc600 add ip, pc, #0, 12

 c3c: e28cca03 add ip, ip, #12288 ; 0x3000

 c40: e5bcf3a0 ldr pc, [ip, #928]! ; 0x3a0

 

(1) ip = pc + 0 = 0xc38 + 0x8 = 0xc40

(2) ip = ip + 0x3000 = 0x3c40

(3) pc <= [ip + 0x3a0] = [0x3fe0]

(4) 跳转到地址0x3fe0地址,即GOT表,见下图

 

00000c44 <__cxa_finalize@plt>:

 c44: e28fc600 add ip, pc, #0, 12

 c48: e28cca03 add ip, ip, #12288 ; 0x3000

 c4c: e5bcf398 ldr pc, [ip, #920]! ; 0x398

 

(1) ip = pc + 0 = 0xc44 + 0x8 = 0xc4c

(2) ip = ip + 0x3000 = 0x3c4c

(3) pc <= [ip + 0x398] = [3fe4]

(4) 跳转到地址0x3fe4地址,即GOT表,见下图

 

00000c50 <puts@plt>:

 c50: e28fc600 add ip, pc, #0, 12

 c54: e28cca03 add ip, ip, #12288 ; 0x3000

 c58: e5bcf390 ldr pc, [ip, #912]! ; 0x390

 

(1) ip = pc + 0 = 0xc50 + 0x8 = 0xc58

(2) ip = ip + 0x3000 = 0x3c58

(3) pc <= [ip + 0x390] = [3fe8]

(4) 跳转到地址0x3fe8地址,即GOT表,见下图

 

00000c5c <__gnu_Unwind_Find_exidx@plt>:

 c5c: e28fc600 add ip, pc, #0, 12

 c60: e28cca03 add ip, ip, #12288 ; 0x3000

 c64: e5bcf388 ldr pc, [ip, #904]! ; 0x388

 

(1) ip = pc + 0 = 0xc5c + 0x8 = 0xc64

(2) ip = ip + 0x3000 = 0x3c64

(3) pc <= [ip + 0x388] = [3fec]

(4) 跳转到地址0x3fec地址,即GOT表,见下图

 

00000c68 <abort@plt>:

 c68: e28fc600 add ip, pc, #0, 12

 c6c: e28cca03 add ip, ip, #12288 ; 0x3000

 c70: e5bcf380 ldr pc, [ip, #896]! ; 0x380

 

(1) ip = pc + 0 = 0xc68 + 0x8 = 0xc70

(2) ip = ip + 0x3000 = 0x3c70

(3) pc <= [ip + 0x380] = [3ff0]

(4) 跳转到地址0x3ff0地址,即GOT表,见下图

 

以此类推...

备注:由于ARM采用三级

可见,上表PLT表最终跳转到GOT表对应位置

 

 

 

“.got”节区分析

“.got”节区内容如下:

 

 

通过objdump反编译”.got”节区内容如下:

 

 

 

动态调试

在第3章,我们观察libdemo1.so在内存的加载情况,其base地址为: 0x4c142000,则”.got”在内存中的地址为:0x4c145fb4。

下面我们通过ida动态调试libdemo1.so,观察其在内存中状况:

内存中GOT表正好和”.rel.plt”的对应。

 

 

 

 

 

 

linker在动态加载so的过程中,会重定位so中导入的符号地址,其实就是修改”.got”节区中的GOT表,将导入的符号地址指向该符号在内存中的地址,下面我们以libdemo1.so中调用的外部函数puts为例分析下:

puts函数是在libc.so库中,该库在Android系统启动的时候一起加载。

我们先用ida确定puts函数在libc.so文件中的位置:

 

 

然后我们查看libc.so在内存中的基地址:

 

 

ibc.so在内存的基地址:base = 0x40113000,所以prinf在内存中的地址为:ADDRESS(puts) = 0x40113000+ 0x1e8a8 = 0x40131a8。

我们也可以通过ida动态调试,查看到puts的内存情况,结果是一样的。

 

 

通过readelf –r libdemo1.so查看重定位表:

 

 

查看上面objdump反汇编后”.got”的内容我们知道:

libdemo1.so未加载到内存的时候,该”.got”GOT表中该puts函数的引用地址为:0xc24。这个地址是临时地址,在libdemo1.so加载到内存后,linker会根据printf在内存中的实际地址来修改GOT表中该符号的地址,并指向正确的地址,这时候printf函数才能被正确调用。

接下来我们通过动态调试跳转到PLT表,PLT表内存地址计算如下:

base = 0x4c142000,addr(PLT) = base + s_addr = 0x4c142000 + 0xc24 = 0x4c142c24。

 

 

我们在两个调用puts函数的地方下断点,观察内存状况变化:

 

 

 

这里在 0x4C142D54是第一个调用puts的低值,其指令:BL  unk_4C142C50正好是PLT[3]表项,我们跳转进去看到确实是PLT[3]表项位置。

 

 

 

然后再继续单步调试。

LDR PC, [R12, #(off_4C145FE8 – 0x4C145C58)]!

上面的[R12, #(off_4C145FE8 – 0x4C145C58)] = [0x4C145FE8] = [0x4C142000 + 0x3FE8] = [base + 0x3FE8],正好是GOT表中的地址。也就是说,上面的指令是讲GOT表项中存储的重定位后的符号的地址赋值给PC指针。

 

  

 

继续单步调试,这时候我们就到了puts函数的内存地址。

 

 

 

上述过程可以用下面的流程图表示。

 

 

 

TODO

这里为什么相差1?

分析:thumb模式特征:指令都是2bytes。我们查看用ida查看libc.so的printf的汇编指令:

每个指令相差2,很明显的thumb模式特征。

 

 

而thunb模式和arm模式的切换可以通过如下:

 

 

 

所以printf在GOT表和实际的内存地址相差1,就是因为thumb模式。

 

12.hash解析

基本概念

哈希表结构

nbucket

nchain

bucket[0]

bucket[nbucket-1]

chain[0]

chain[nchain-1]

chain[i]与symbolTable[i]对应

函数索引过程:

1.获取函数名的哈希值

funHash=elfhash(function_name)

2.获取函数索引

funIndex = bucket[funHash % nbucket]

3.如果funIndex对应的符号不是所需的,则chain[funIndex]给出具有相同哈希值的下一个符号表项,沿着chain链一直搜索,知道找到符合的符号位置

算法如下:

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;

}

实例分析

 

 

 

nbucket=0x25=37

nchain=0x3c=60

….

13.init_array和.fini_array解析

基本概念

带有” __attribute__((constructor))”的函数会放到.init_array初始化,如下:

void init_func() __attribute__((constructor));

同样,带有” __attribute__((destructor))”的函数会放到.fini_array初始化,如下:

void fini_func() __attribute__((destructor));

实例分析

以上面的libdemo1.so文件为例,”.init_array”节区内容如下:

 

 

 

0x0CDC正好指向init_func函数地址。

 

 

以上面的libdemo1.so文件为例,”.fini_array”节区内容如下:

 

 

 

0x0C98和0x0CFC正好分别指向:sub_C98和fini_func两个函数:

 

 

 

 

 

参考

[1] ELF文件格式分析. 滕启明. http://staff.ustc.edu.cn/~sycheng/ssat/exp_crack/ELF.pdf

[2] Android 4.2.2、7.1.1中linker和elf.h源码.

[3] Android漫游记(1)---内存映射镜像(memory maps). http://blog.csdn.net/lifeshow/article/details/29174457

[4] http://flint.cs.yale.edu/cs422/doc/ELF_Format.pdf

[5] 通过GDB调试理解GOT/PLT. http://rickgray.me/2015/08/07/use-gdb-to-study-got-and-plt.html

[6] Android ELF文件PLT和GOT http://laokaddk.blog.51cto.com/368606/1160386

[7] Redirecting functions in shared ELF libraries. https://www.codeproject.com/Articles/70302/Redirecting-functions-in-shared-ELF-libraries

[8]《链接器和加载器》3.7 UNIX的ELF格式

posted @ 2023-01-30 18:49  Domefy  阅读(263)  评论(0编辑  收藏  举报