Android脱壳1-so文件格式解析

youncyb 发布于 20 天前 204 次阅读 reverse


这篇 Markdown 文档探讨了如何解析 Android 平台上的 .so 文件格式,以用于脱壳和逆向分析。内容覆盖了 ELF(Executable and Linkable Format)文件的基本结构、各个节的功能,以及如何利用这些信息来定位关键代码和数据。

1. 简介

Android加固中有一步是对so进行加固,加固的方式可以是:删除部分ELF Header、删除Section Header、加密Section等等,导致逆向软件无法正确分析。为了识别加固措施,必须要对so文件有一个深入的理解。

so文件被称为共享目标文件,也称为动态链接文件,其与可执行文件、静态链接文件(.a)都采用ELF文件格式,但与可执行文件不同在于可执行文件在链接的过程中可以合并多个节区(Section),然后变成段(Segment),如图1所示[1]

image-20220303162124608

ELF文件主要包含了4个部分,如图2所示。

  • ELF Header

  • Program Header Table (程序头表)

  • Section (节区)

  • Section Header Table (节区头表)

image-20220228172909396

ELF Header是文件头,包含固定长度的信息:魔术数字、版本信息、操作系统架构、文件结构偏移量等等;

Section Header Table包含了程序的Section信息:Section名称、Section类型、Section地址等等;

Program Header Table告诉操作系统如何在内存中构建程序的映象。

名称 大小 对齐 作用
Elf32_Addr 4 4 无符号地址
Elf32_Half 2 2 无符号short int
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword 4 4 有符号int
Elf32_Word 4 4 无符号int
unsigned char 1 1 无符号char

名称 大小 对齐 作用
Elf64_Addr 8 8 无符号地址
Elf64_Half 2 2 无符号short int
Elf32_Off 8 8 无符号文件偏移
Elf32_Sword 4 4 有符号int
Elf32_Word 4 4 无符号int
Elf64_Xword 8 8 无符号long long
Elf64_Sxword 8 8 有符号long long
unsigned char 1 1 无符号char

2. ELF Header

Android so ELF Header由如下结构体构成(64位和32位一样):

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;
  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;

typedef struct elf64_hdr {
  unsigned char e_ident[EI_NIDENT];
  Elf64_Half e_type;
  Elf64_Half e_machine;
  Elf64_Word e_version;
  Elf64_Addr e_entry;
  Elf64_Off e_phoff;
  Elf64_Off e_shoff;
  Elf64_Word e_flags;
  Elf64_Half e_ehsize;
  Elf64_Half e_phentsize;
  Elf64_Half e_phnum;
  Elf64_Half e_shentsize;
  Elf64_Half e_shnum;
  Elf64_Half e_shstrndx;
} Elf64_Ehdr;

# readelf -h输出
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           AArch64
  Version:                           0x1
  Entry point address:               0x15760
  Start of program headers:          64 (bytes into file)
  Start of section headers:          1244320 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         4
  Section header string table index: 1

2.1 e_ident

e_ident由16字节组成的数组,给出了ELF的部分标识信息,分别包含了以下部分:

名称 位置(字节) 备注
EI_MAG0-EI_MAG3 0-3 固定魔术头:7f 45 4c 46<br />0x7f, ‘E’, ‘L’, ‘F’ <br />也常见8进制编码方式:\177ELF
EI_CLASS 4 有三个值:<br />0 - ELFCLASSNONE: 非法类别<br />1 - ELFCLASS32: ELF 32位<br />2 - ELFCLASS64: ELF 64位
EI_DATA 5 数据编码方式:<br />0 - ELFDATANONE:非法数据编码<br />1 - ELFDATA2LSB:低位在前,即小端编码<br />2 - ELFDATA2MSB:高位在前,即大端编码<br />ARM默认是小端模式,所以一般都是1
EI_VERSION 6 ELF头部版本号:必须为EV_CURRENT = 1
EI_OSABI 7 标识操作系统类型: <br /> ELFOSABI_NONE = 0, // UNIX System V ABI
EI_ABIVERSION 8 ABI版本
EI_PAD 9 填充
EI_NIDENT 16 e_ident的size

2.2 e_type

e_type表示文件的类型,其值可为如下值:

  ET_NONE   = 0,      // No file type
  ET_REL    = 1,      // 可重定位文件
  ET_EXEC   = 2,      // 可执行文件
  ET_DYN    = 3,      // 共享目标文件
  ET_CORE   = 4,      // Core file
  ET_LOPROC = 0xff00, // Beginning of processor-specific codes
  ET_HIPROC = 0xffff  // Processor-specific

ELF文件一般是可重定位文件(.o、.a)、可执行文件和共享文件(.so)。

2.3 e_machine

e_machine表示CPU的架构类型:183表示AARCH64架构。

2.4 剩余参数

名称 备注
e_version 必须为EV_CURRENT=1
e_entry 程序入口虚拟地址。可重定位文件(.o)一般没有程序入口,可以为 0
e_phoff 程序头部表格(Program Header Table)的偏移量(按字节计算)。如果文件没有程序头部表格,可以为 0
e_shoff 节区头部表格(Section Header Table)的偏移量(按字节计算)。如果文件没有节区头部表格,可以为 0。
e_flags 保存与文件相关的,特定于处理器的标志。标志名称采用 EF_machine_flag的格式。
e_ehsize ELF 头部 的大小(以字节计算)。
e_phentsize 程序 头部 的表项大小(按字节计算)。
e_phnum 程序 头部 的表项数目。可以为 0。
e_shentsize 节区 头部 的表项大小(按字节计算,32位固定为0x28,64位固定为0x40)。
e_shnum 节区 头部表 的表项数目。可以为 0。
e_shstrndx 节区头部表 中与节区名称字符串表相关的表项的索引,是节区中最后一个表。如果文件没有节区名称字符串表,此参数可以为 SHN_UNDEF。

如果SO文件处理了e_shnum和e_shstrndx,并且没有处理e_shoff,也可以通过以下公式得到(注:elf32(64)_shdr==e_shentsize):

$e_shnum = (fil_size - e_shoff) \div sizeof(elf32_shdr)$

$e_shstrndx = e_shnum - 1$

所以当ida对so的segment分析出现问题时,首先需要判断e_shnum是否正确,然后判断e_shstrndx是否正确。如果e_shoff与e_shnum都被修改过,则需要更深入的分析。

3. Program Header Table

程序头表说明了如何装载该so,其结构体如下:

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;

typedef struct elf64_phdr {
  Elf64_Word p_type;
  Elf64_Word p_flags;
  Elf64_Off p_offset;
  Elf64_Addr p_vaddr;
  Elf64_Addr p_paddr;
  Elf64_Xword p_filesz;
  Elf64_Xword p_memsz;
  Elf64_Xword p_align;
} Elf64_Phdr;
字段 含义
p_type 描述了段类型
p_offset 描述了从文件到该段(每个程序头表的data区域)的文件偏移
p_vaddr 描述了段在内存中的偏移
p_paddr 描述了物理地址相关,在应用层无作用。
p_filesz p_offset描述了段在文件中的偏移。那么此成员就描述了在文件中所占的大小,可以为0。
p_memsz 同上,描述了内存中映像所占的字节数。 可以为0
p_flags mmap映射的prot参数。int prot = PFLAGS_TO_PROT(phdr->p_flags);
p_align 描述了对齐。

p_type包含以下类型:

enum {
  PT_NULL    = 0, // Unused segment.
  PT_LOAD    = 1, // Loadable segment.
  PT_DYNAMIC = 2, // Dynamic linking information.
  PT_INTERP  = 3, // Interpreter pathname.
  PT_NOTE    = 4, // Auxiliary information.
  PT_SHLIB   = 5, // Reserved.
  PT_PHDR    = 6, // The program header table itself.
  PT_TLS     = 7, // The thread-local storage template.
  PT_LOOS    = 0x60000000, // Lowest operating system-specific pt entry type.
  PT_HIOS    = 0x6fffffff, // Highest operating system-specific pt entry type.
  PT_LOPROC  = 0x70000000, // Lowest processor-specific program hdr entry type.
  PT_HIPROC  = 0x7fffffff, // Highest processor-specific program hdr entry type.
  // x86-64 program header types.
  // These all contain stack unwind tables.
  PT_GNU_EH_FRAME  = 0x6474e550,
  PT_SUNW_EH_FRAME = 0x6474e550,
  PT_SUNW_UNWIND   = 0x6464e550,
  PT_GNU_STACK  = 0x6474e551, // Indicates stack executability.
  PT_GNU_RELRO  = 0x6474e552, // Read-only after relocation.
  // ARM program header types.
  PT_ARM_ARCHEXT = 0x70000000, // Platform architecture compatibility info
  // These all contain stack unwind tables.
  PT_ARM_EXIDX   = 0x70000001,
  PT_ARM_UNWIND  = 0x70000001,
  // MIPS program header types.
  PT_MIPS_REGINFO  = 0x70000000,  // Register usage information.
  PT_MIPS_RTPROC   = 0x70000001,  // Runtime procedure table.
  PT_MIPS_OPTIONS  = 0x70000002,  // Options segment.
  PT_MIPS_ABIFLAGS = 0x70000003   // Abiflags segment.
};

3.1 PT_PHDR

33PT_PHDR是程序头表的第一部分,表示了程序头表相对于so起始地址的偏移量和程序头表的大小,如图3所示。

$phdr_addr = load_bias + p_vaddr$

此处p_vaddr=0x34h(和e_phoff相等),load_bias指代so在内存中的地址。

image-20220302171135066

图3

还有一种so是没有PT_PHDR条目,该so一般以PT_LOAD开头,如图4所示。

$phdr_addr = load_bias + p_vaddr + e_phoff$

由于p_vaddr=0,所以仍然是so文件在内存的起始地址+程序头的偏移。[2]

image-20220303103559388

图4

3.2 PT_INTERP

PT_INTERP是可选的,一般为/system/bin/linker,如图5所示。

image-20220303104501548

图5

3.3 PT_LOAD

PT_LOAD是程序头最重要的部分,通过mmap将p_type=PT_LOAD的映射到内存。映射以page为单位,不足以0填充。

3.4 PT_DYNAMIC

PT_DYNAMIC指向.dynamic节区,.dynamic节区包含了动态链接所需的一切信息,如图6所示。详情见5.4。

image-20220303150728737

图6

4. Section Header Table

节区头表详细的描述了so文件的对应节的各种属性,如果节区头表被删除了,那么像IDA这样的逆向工具也无法正确的显示文件的信息。比如识别不了代码段、数据段等等。

节区头表结构如下:32位占40字节,64位占64字节。

typedef struct elf32_shdr {
  Elf32_Word sh_name;
  Elf32_Word sh_type;
  Elf32_Word sh_flags;
  Elf32_Addr sh_addr;
  Elf32_Off sh_offset;
  Elf32_Word sh_size;
  Elf32_Word sh_link;
  Elf32_Word sh_info;
  Elf32_Word sh_addralign;
  Elf32_Word sh_entsize;
} Elf32_Shdr;

typedef struct elf64_shdr {
  Elf64_Word sh_name;
  Elf64_Word sh_type;
  Elf64_Xword sh_flags;
  Elf64_Addr sh_addr;
  Elf64_Off sh_offset;
  Elf64_Xword sh_size;
  Elf64_Word sh_link;
  Elf64_Word sh_info;
  Elf64_Xword sh_addralign;
  Elf64_Xword sh_entsize;
} Elf64_Shdr;
名称 含义
sh_name 节名是一个字符串,保存在一个名为.shstrtab的字符串表(可通过Section Header索引到)。sh_name的值实际上是其节名字符串在.shstrtab中的偏移值
sh_type 节类型,见4.1。
sh_flags 节标志位,见4.2。
sh_addr 节地址:节的虚拟地址。如果该节可以被加载,则sh_addr为该节被加载后在进程地址空间中的虚拟地址;否则sh_addr为0
sh_offset 节偏移。如果该节存在于文件中,则表示该节在文件中的偏移;否则无意义,如sh_offset对于BSS节来说是没有意义的,因为BSS节是运行时申请。
sh_size 节大小
sh_link、sh_info 节链接信息
sh_addralign 节地址对齐方式。0和1表示无需对齐,2的正整数幂用来表示对齐。
sh_entsize 节项大小。有些节包含了一些固定大小的项,如符号表,其包含的每个符号所在的大小都一样的,对于这种节,sh_entsize表示每个项的大小。如果为0,则表示该节不包含固定大小的项。

4.1 sh_type

名称 含义
SHT_NULL 0 无意义的节
SHT_PROGBITS 1 由程序定义的节。代码节、数据节都是这种类型。
SHT_SYMTAB<br />SHT_DYNSYM 2<br />11 SHT_SYMTAB和SHT_DNYSYM都标识了符号表,区别在于SHT_DNYSYM是SHT_SYMTAB的子集,SHT_SYMTAB包含了完整的符号表。见5.1
SHT_STRTAB 3 字符串表,代表符号和节区的名字。见5.2
SHT_RELA 4 重定位表,该节包含了重定位信息。
SHT_HASH 5 符号的哈希表,一个动态链接对象必须包含符号哈希表。见5.3
SHT_DYNAMIC 6 动态链接信息
SHT_NOTE 7 提示性信息
SHT_NOBITS 8 表示该节在文件中没有内容。如.bss节
SHT_REL 9 该节包含了重定位信息
SHT_SHLIB 10 保留
SHT_DYNSYM 11 动态链接的符号表
SHT_INIT_ARRAY 14 so加载初始化表
SHT_FINI_ARRAY 15 so销毁表

4.2 sh_flag

常量 含义
SHF_WRITE 1 表示该节在进程空间中可写
SHF_ALLOC 2 表示该节在进程空间中需要分配空间。有些包含指示或控制信息的节不需要在进程空间中分配空间,就不会有这个标志。
SHF_EXECINSTR 4 表示该节在进程空间中可以被执行

5. Section

5.1 符号表(Symbol Table)

5.1.1 符号

符号表的内容是符号,而符号就是函数名、变量名。比如:

int Sym[3]={1, 2, 3};
int main(){}

Sym是全局变量,即全局符号,main函数也是符号,即全局符号。

符号分为3类:

  • 全局符号

    由A模块定义,并且能在A模块和B模块使用,例如:non-static 全局变量和non-static 函数

    • 全局符号里又存在弱符号,弱符号指只定义而没有赋值。具体作用可见[3]
  • 外部符号

    由B模块定义,在A模块声明和使用。即是一种参照物体系。

  • 本地符号

    A模块定义的static函数或者static变量。

定义在函数内部的变量不是符号,而是由运行时操作系统分配到栈上。

例如图7:[3:1]

image-20220304161857242

5.1.2 .dynsym和.symtab

两个节区都指向了符号表区域,不同的是.symtab是完整的符号表,包含了许多动态链接中不需要的符号。Linux上可通过strip命令去掉.symtab,去掉后,使用file命令查看文件,就可以发现文件是stripped状态。

Android so一般没有.symtab区域,但一定会有.dynsym区域,用于参与动态链接。

5.1.3 符号表结构

符号表有多个符号表条目构成,每一个符号表条目大小为16字节,通过符号表条目就可以找到符号所在。

struct Elf32_Sym {
  Elf32_Word    st_name;  // Symbol name (index into string table)
  Elf32_Addr    st_value; // Value or address associated with the symbol
  Elf32_Word    st_size;  // Size of the symbol
  unsigned char st_info;  // Symbol's type and binding attributes
  unsigned char st_other; // Must be zero; reserved
  Elf32_Half    st_shndx; // Which section (header table index) it's defined in
};
名称 含义
st_name 符号名,本质上是一个字符串表的偏移量。
st_value 可执行文件和共享文件中,st_value表示符号的虚拟地址。
st_size 符号的大小。
st_info 符号的类型和绑定的属性:<br />1. 绑定的属性<br />STB_LOCAL:本地符号=0<br />STB_GLOBAL:全局符号=1<br />STB_WEAK:弱符号=2<br /><br />2.类型<br />STT_NOTYPE:没有指定的类型=0<br />STT_OBJECT:此符号与数据对象相关联(.data),例如变量、数组等=1<br />STT_FUNC:此符号与函数或可执行代码关联=2<br />STT_COMMON:此符号表示未初始化的公共块(.bss)=5
st_other 符号的可见性描述,一般为STV_DEFAULT=0,表示由绑定的属性决定。
st_shndx 表示该符号所在的节区,值和sh_type有关。

image-20220304175839414

图8

5.2 字符串表(String Table)

字符串表保存了符号的名字和节区的名字,一个共享文件可以有多个字符串表,比如:.shstrtab(节区名字符串表)和.dynstr(动态链接所需信息,如5.1符号的名字)。

如图9所示,字符串表以\x00\x00结尾,中间的每一个字符串都是以\x00结尾。

例如获取符号名字,只需要通过符号条目的st_name获取偏移,然后到.dynstr节区寻找一个完整的字符串即可。

image-20220305164034450

图9

5.3 哈希表(Hash Table)

动态链接过程中用到的符号存在于动态符号表节区(.dynsym),只需通过程序头表找到.dynsym节区,就可以在节区中通过对比st_name,获取对应的函数或者变量,但是由于目标文件存在大量的符号,如果按照顺序找到,那么效率是非常低的。

所以,自然而然的就相到了通过哈希表来进行索引,哈希表存放了符号名和符号地址偏移量的对应关系,如此即可通过符号名,快速的获取符号结构。

so的哈希表结构如图10所示,由n个桶和n个链组成,计算方式如下:

image-20220307101511155

图10

首先通过hash函数将符号名转换为hash value:

uint32_t calculate_elf_hash(const char* name) {
  const uint8_t* name_bytes = reinterpret_cast<const uint8_t*>(name);
  uint32_t h = 0, g;

  while (*name_bytes) {
    h = (h << 4) + *name_bytes++;
    g = h & 0xf0000000;
    h ^= g;
    h ^= g >> 24;
  }

  return h;
}

获取hash value (假设为x) 之后,通过$y=bucket[x\ mod\ bucket.size]$,获取chain[y]的索引,然后通过对比这条链上的每一个元素,直到找到元素。

bool soinfo::elf_lookup(SymbolName& symbol_name,
                        const version_info* vi,
                        uint32_t* symbol_index) const {
  uint32_t hash = symbol_name.elf_hash();

	...

  for (uint32_t n = bucket_[hash % nbucket_]; n != 0; n = chain_[n]) {
    ElfW(Sym)* s = symtab_ + n;
    const ElfW(Versym)* verdef = get_versym(n);

    // skip hidden versions when verneed == 0
    if (verneed == kVersymNotNeeded && is_versym_hidden(verdef)) {
        continue;
    }

    if (check_symbol_version(verneed, verdef) &&
        strcmp(get_string(s->st_name), symbol_name.get_name()) == 0 &&
        is_symbol_global_and_defined(this, s)) {
      TRACE_TYPE(LOOKUP, "FOUND %s in %s (%p) %zd",
                 symbol_name.get_name(), get_realpath(),
                 reinterpret_cast<void*>(s->st_value),
                 static_cast<size_t>(s->st_size));
      *symbol_index = n;
      return true;
    }
  }

	...

  *symbol_index = 0;
  return true;
}

5.4 动态节区(.dynamic)

如果一个目标文件要参与动态链接,那么必定会存在.dynamic节区。目标文件在链接时,会根据程序头表的Dynamic Segment找到动态节区的位置,再从动态节区中找到其他所需节区的信息,如:SHT_STRTAB、SHT_HASH等等。(Android 10 在装载so时,通过 sh头和ph头 找到动态节区做校验,在链接时通过ph头找到动态节区)

动态节区的结构体如下,一共8个字节:

typedef struct dynamic {
  Elf32_Sword d_tag;
  union {
    Elf32_Sword d_val;
    Elf32_Addr d_ptr;
  } d_un;
} Elf32_Dyn;

d_tag 控制着结构体的类型;

d_val 不同类型的结构体有着不同的意思;

d_ptr 结构体的偏移地址。

d_tag有如下类型:

名称 含义
DT_NULL 0 代表动态节区的结束
DT_NEEDED 1 需要被导入的库,计算:$lib_addr=strtab_addr+d_ptr$ (相对于字符串表的偏移)
DT_PLTGOT 3 PLT和GOT表
DT_HASH 4 哈希表
DT_STRTAB 5 字符串表
DT_SYMTAB 6 符号表
DT_INIT 12 初始化节区
DT_FINIT 13 终止节区
DT_INIT_ARRAY 25 初始化数组节区
DT_FINI_ARRAY 26 终止数组节区

比如查看字符串表,图11:

image-20220307161325449

image-20220307161350720

图11

比如查看DT_NEEDED,可以得知相对于字符串表的偏移为0x4c0,所以最终地址:

$0x54c+0x4c0=0xa0c$

图12:

image-20220307161654938

image-20220307162130613

图12

5.5 自定义Section

在声明一个函数或变量时,可以加上attribute((section(“自定义section名”)))前缀的方式,将其添加到自定义段。

Reference

  1. 计算机那些事(4)——ELF文件结构

  2. elf.h

  3. [原创]一 Android ELF系列:ELF文件格式简析到linker的链接so文件原理分析


  1. Object File Format ↩︎

  2. Android Linker学习笔记 ↩︎

  3. 程序的链接(三):符号和符号表 ↩︎ ↩︎