ELF文件格式

ELF是UNIX系统实验室(USL)作为应用程序二进制接口(Application Binary Interface,ABI)而开发和发布的,也是Linux的主要可执行文件格式.全称是Executable and Linking Format,这个名字相当关键,包含了ELF所需要支持的两个功能——执行和链接.

准备ELF文件

1
2
3
4
int main(int argc, char* argv[]){
    printf("Hello ELF!\n");
    return 0;
}
1
2
3
4
5
6
7
8
AS_PATH  := /data/local/tmp
ELF_PATH := E:/SoftWareWork/AndroidStudioWork/LuoELF/app/build/intermediates/cmake/debug/obj/armeabi-v7a
ELF_NAME := luoelf

all:
 adb push $(ELF_PATH)/$(ELF_NAME) $(AS_PATH)
 adb shell chmod a+x $(AS_PATH)/$(ELF_NAME)
 adb shell $(AS_PATH)/$(ELF_NAME)

文件格式

概述

文件格式概述

程序头表指向段,等价于PE中的节,用来描述内存映射. 节头表指向节,类似PE的数据目录,指向各种表. 在Obj文件中段是可选的,在可执行文件中节是可选的,但是NDK编译出的ELF文件,段和节都是有的.

总体来说就是一个ELF文件包含3部分,ELF文件头、节、段.

数据类型:

数据类型

文件头

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#define EI_NIDENT 16 
typedef struct {
 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;

e_ident:

这16字节是ELF标识,前4字节是文件标志,不可修改.

e_ident

EI_CLASS:

EI_CLASS

这个字节指明了文件类型. Android系统不检查这个字节,它是通过判断指令集v7a,v8a来确定是32位还是64位. IDA检查了这个字节,如果修改了这个字节,IDA就无法反汇编.

EI_DATA:

EI_DATA

这个字节决定了接下来ELF文件结构体的解析是按大尾方式还是小尾方式. Android系统不检查这个字节,默认为小尾方式. IDA检查了这个字节,如果修改了这个字节,IDA就无法反汇编.

EI_VERSION:

ELF文件头的版本.

e_type:

e_type

这2字节是文件类型. Android5.0之后,可执行文件全部为so文件. Android高版本,这2字节只能是03,不能修改.

e_machine:

e_machine1

e_machine2

这2字节是指令集,可在elf-em.h这个头文件中,查看扩展指令集. 这2字节不可修改.

e_version:

e_version

这4字节指明目标文件的版本. Android系统不检查这4字节. IDA检查了这4字节,不过影响不大,如果修改了,IDA还是可以反汇编.

e_entry:

这4字节是程序入口点(OEP),是个RVA. 但如果上面e_type的值是02,这个值就是VA. 如果是共享库文件,这4字节是0. 如果是可执行文件,这4字节不是0. 这是共享库文件和可执行文件很重要的差别.

e_phoff:

这4字节是程序头表偏移,如果没有程序头表,该值应为0.

e_shoff:

这4字节是节头表偏移,如果没有节头表,该值应为0. 在Android对抗中,删节表是很常见的.

e_flags:

这4字节是个标志,没什么用.

e_ehsize:

这2字节是ELF文件头大小,以字节为单位. Android系统,不检查这2字节,默认ELF文件头大小为52字节. IDA检查了这2字节,如果我们修改了这2字节内容.IDA只会产生警告,影响不大,仍然可以反汇编.

e_phentsize:

这2字节表明程序头表每一个表项的大小,以字节为单位.

e_phnum:

这2字节表明程序头表总共有多少表项,如果没有程序头表,该值应设为0.

e_shentsize:

这2字节表明节头表每一个表项的大小,以字节为单位.

e_shnum:

这2字节表明节头表总共有多少表项,如果没有节头表,该值应设为0.

e_shstrndx:

这2字节是节头表中与节名字表相对应表项的索引.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
typedef struct {
 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;

sh_name:

4字节,是个偏移值,暂记为P1. 首先通过ELF文件头最后一项e_shstrndx拿到节表中节名称表对应表项的索引, 然后在节表中找到该项,找到sh_offset文件偏移值,暂记为P2 最后将P1 + P2,即可得到该节名字的字符串文件偏移值.

sh_type:

4字节,节类型.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
typedef enum <Elf32_Word> {
 SHT_NULL = 0x0, /* Inactive section header */
 SHT_PROGBITS = 0x1, /* Information defined by the program */
 SHT_SYMTAB = 0x2, /* Symbol table - not DLL */
 SHT_STRTAB = 0x3, /* String table */
 SHT_RELA = 0x4, /* Explicit addend relocations, Elf64_Rela */
 SHT_HASH = 0x5, /* Symbol hash table */
 SHT_DYNAMIC = 0x6, /* Information for dynamic linking */
 SHT_NOTE = 0x7, /* A Note section */
 SHT_NOBITS = 0x8, /* Like SHT_PROGBITS with no data */
 SHT_REL = 0x9, /* Implicit addend relocations, Elf64_Rel */
 SHT_SHLIB = 0xA, /* Currently unspecified semantics */
 SHT_DYNSYM = 0xD, /* Symbol table for a DLL */
 SHT_INIT_ARRAY = 0xE, /* Array of constructors */
 SHT_FINI_ARRAY = 0xF, /* Array of deconstructors */
 SHT_PREINIT_ARRAY = 0x10, /* Array of pre-constructors */
 SHT_GROUP = 0x11, /* Section group */
 SHT_SYMTAB_SHNDX = 0x12, /* Extended section indeces */
 SHT_NUM = 0x13, /* Number of defined types */

 SHT_LOOS = 0x60000000, /* Lowest OS-specific section type */
 SHT_GNU_ATTRIBUTES = 0x6ffffff5, /* Object attribuytes */
 SHT_GNU_HASH = 0x6ffffff6, /* GNU-style hash table */
 SHT_GNU_LIBLIST = 0x6ffffff7, /* Prelink library list */
 SHT_CHECKSUM = 0x6ffffff8, /* Checksum for DSO content */
 SHT_LOSUNW = 0x6ffffffa, /* Sun-specific low bound */
 SHT_SUNW_move = 0x6ffffffa, // Same thing
 SHT_SUNW_COMDAT = 0x6ffffffb,
 SHT_SUNW_syminfo = 0x6ffffffc,
 SHT_GNU_verdef = 0x6ffffffd, /* Version definition section */
 SHT_GNU_verdneed = 0x6ffffffe, /* Version needs section */
 SHT_GNY_versym = 0x6fffffff, /* Version symbol table */
 SHT_HISUNW = 0x6fffffff, /* Sun-specific high bound */
 SHT_HIOS = 0x6fffffff, /* Highest OS-specific section type */
 SHT_LOPROC = 0x70000000, /* Start of processor-specific section type */
 SHT_HIPROC = 0x7fffffff, /* End of processor-specific section type */
 SHT_LOUSER = 0x80000000, /* Start of application-specific */
 SHT_HIUSER = 0x8fffffff /* Ennd of application-specific */
} s_type32_e;

sh_flags:

4字节,由一系列标志比特位组成.

sh_flags

sh_addr:

4字节,内存地址.

sh_offset:

4字节,文件偏移.

sh_size:

4字节,节大小.

sh_link:

4字节,一个索引值,指向节头表中本节所对应的位置.

sh_info:

4字节,节的附加信息,根据节的类型不同,本成员的意义也有所不同.

sh_info

sh_addralign:

4字节, 对齐值.

sh_entsize:

4字节,有一些节的内容是一张表,其中每一个表项的大小是固定的,比如符号表,对于这种表来说,本成员指定其每一个表项的大小.

特殊节

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*节名字不重要,重要的是节的类型.
当节名字未被修改过,是具有参考价值的.
*/
.shstrtab 节名称表
.text     代码段
.bss      数据段(未初始化)
.data     数据段(初始化)
.rodata   常量区
    
//ndk的版本信息,仅供参考
.note.android.ident
.note.gnu.build - id
.note.gnu.gold - version

//编译器版本信息
.comment
.gnu.version
.gnu.version_r
.ARM.attributes

//调试信息,等价于PDB文件
.debug_str
.debug_abbrev
.debug_info
.debug_macinfo
.debug_frame
.debug_line
.debug_loc
.debug_ranges
.debug_aranges

//初始化表
.init_array //初始化函数表
.fini_array //反初始化函数表

//异常表, 不是ELF的标准,是由操作系统和编译器自定义的
.ARM.exidx  //索引表
.ARM.extab  //异常表

符号表

1
2
3
4
5
6
7
8
//符号表
//当打包为apk,里面的动态库只有.dynsym
.dynsym
.symtab

//符号表字符串
.dynstr
.strtab

符号表项结构:

1
2
3
4
5
6
7
8
typedef struct {
 Elf32_Word st_name;     //符号的名字,指向字符串表的索引
 Elf32_Addr st_value;    //符号的值, 地址, 是个RVA
 Elf32_Word st_size;     //符号的大小
 unsigned char st_info;  //符号的类型和属性
 unsigned char st_other; //暂未使用
 Elf32_Half st_shndx;    //一个索引值,指向相关联的节在节头表中的索引
} Elf32_Sym;

符号表描述了导入和导出,st_value的值为非0,表示是导出为0,则表示是导入.

st_name:

指向字符串表的索引,节头表中的link值说明了是哪个字符串表.

st_name

st_info:

1
2
3
#define ELF32_ST_BIND(i) ((i)>>4) 
#define ELF32_ST_TYPE(i) ((i)&0xf) 
#define ELF32_ST_INFO(b,t) (((b)<<4)+((t)&0xf))

高4字节是符号绑定.

符号绑定

低4字节是符号类型.

符号类型

st_shndx:

这里我们导出一个全局变量,来理解下这个字段的意思.

源代码:

1
2
3
4
5
6
int g_n = 100;
int main(int argc, char* argv[]) {

 printf("Hello ELF! %d\n", g_n);
 return 0;
}

分析:

st_shndx1

从上图中可以看到,导出的全局变量g_n的RVA为0x2000,大小为4字节,st_shndx字段为22. 那么这个全局变量的值在文件偏移的哪个地方记录呢? 我们去节头表中的第22项看下.

st_shndx2

节头表的第22项,所在的RVA是0x2000,FOA是0x1000, 而全局变量g_n的RVA为0x2000,大小为4字节, 也就是说文件偏移0x1000的地方开始是4字节内容就是全局变量的值,100.

哈希表

1
2
3
//哈希表
.hash      //旧版,导入导出均可查
.gnu.hash //新版,效率高,只可查导出

旧版(.hash):

哈希表结构:

哈希表结构

Bucket数组中含有nbucket个项,chain数组中含有nchain个项,序号都从0开始. Bucket和chain中包含的都是符号表中的索引.hash有可能冲突,chain里面放的就是所有冲突的. 给定一个符号名字,返回一个哈希值x,然后由bucket[x%nbucket]得到一个符号表索引y, 如果索引y对应的符号表项不是想要的符号(通过对比名字),则由chain[y]得到下一个符号表索引z,如果仍不是想要的符号,继续chain[z]…, 如果chain[z]的值为0,说明该符号不存在.

哈希算法:

原始算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
unsigned long elf_hash(const unsigned char* name)
{
 unsigned long h = 0, g;
 while (*name)
 {
  h = (h << 4) + *name++;
  if (g = h & 0xf0000000)
   h ^= g >> 24;
  h &= ~g;
 }
 return h;
}

Android中的hash算法:

Google并没有用原始的hash算法.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//http://androidxref.com/4.0.3_r1/xref/bionic/linker/linker.c#elfhash
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;
}

示例:

环境:解压AndroidStudio编译出来的Release版apk,查看其中的so文件.

目标:查找main这个符号.

  1. 查看.hash数据.

哈希表示例1

可以得到: nbucket = 0x11 nchain = 0x14

  1. 计算hash%nbucket.

哈希表示例2

得到的结果y = 10.

  1. 查找main.

哈希表示例3

y = 10,bucket[y] = 7,查看动态符号表的第7项不是main, chain[7] = 3,查看动态符号表的第3项不是main, chain[3] = 0x11,查看动态符号表的第17项是main.

程序链接表

1
.plt //过程链接,Procedure Link Table

外部调用的跳板,是个代理函数.

程序链接表

IDA是通过节表拿到plt节的地址,从而解析plt函数的. 从上图中可以看到每两个plt函数之间的大小为12字节, 假如我们将节表中plt节的偏移加12字节的倍数,IDA解析api就会错位,实际上也确是如此. 这样修改之后不会影响程序的运行,因为程序的运行是不需要节表的.

全局偏移表

1
.got //等价于PE中的IAT表,Global Offset Table

记录外部调用的入口地址.

plt和got的关系:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//每调一个api,编译器就会生成一个plt函数
int (*g_printf)(const char* __fmt, ...) = plt;

/*
 * plt:
 *   jmp [got]
 *
 * got:
 *   load1
 *
 * load1:
 *   dlopen
 *   dlsym
 *   got = addr
 */

重定位表

1
2
.rel.plt //修api地址
.rel.dyn //修全局变量

项结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct {
 Elf32_Addr r_offset; //Rva,修完之后放在哪里
 Elf32_Word r_info;   //既给出了重定位所作用的符号表索引,也给出了重定位的类型
} Elf32_Rel;

typedef struct {
 Elf32_Addr r_offset;
 Elf32_Word r_info;
 Elf32_Sword r_addend;
} Elf32_Rela;

r_info 的宏定义:

1
2
3
#define ELF32_R_SYM(i) ((i)>>8)               //高24位是符号表索引,修的位置
#define ELF32_R_TYPE(i) ((unsigned char)(i))  //低8位是重定位的类型,如何修
#define ELF32_R_INFO(s,t) (((s)<<8)+(unsigned char)(t))

示例:

重定位表1

Elf32_Xword s_entsize = 8, 表示每项为8字节 第一项为 F0 1F 00 00 16 02 00 00 r_offset: 0x1FF0 符号表索引:0x02 重定位类型:0x16

IDA查看偏移:

重定位表2

符号表第2项:

重定位表3

动态节

1
2
3
4
.dynamic
/*这里存放了软件运行时所需要的表,操作系统是从这里拿表的.
但是这样说又有点矛盾,因为我们前面讲软件的运行是不需要节表的.
那么操作系统不通过节表,如何拿到动态节的地址? 段中有描述.*/

项结构:

1
2
3
4
5
6
7
typedef struct {
 Elf32_Sword d_tag;     //什么表
 union {                 //在哪里
  Elf32_Word d_val;
  Elf32_Addr d_ptr;
 } d_un;
} Elf32_Dyn;

d_tag:

d_tag

DT_NEEDED:

DT_NEEDED

这里面存放的是软件运行所需要的动态库. 前面我们讲符号表和重定位表,都没有提到对应的api在哪个库里面. 假设软件运行需要两个库,libc.so、 libm.so 现在有一个api,操作系统会看这个api在libc.so里面没, 如果不在,就去看libm.so里面有没有这个导出函数. 操作系统就这样遍历需要加载的库,就知道对应的api在哪个库里面了.

程序头表

用来保存程序加载到内存中所需要的信息,用段(segment)来表示,与节头表类似,同样是数组结构. 数组的位置在偏移e_phoff处,每个元素(segment header)的大小为e_phentsize,共有e_phnum个元素.

程序头结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct {
 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;

p_type:

类型 PT_NULL:表示该段未使用. PT_LOAD:表示要将文件中的segment内容映射到进程内存中对应的地址上. PT_DYNAMIC:动态节. PT_INTERP:包含interpreter的路径. PT_HDR:表示程序头表本身.

p_offset:

该段的文件偏移.

p_vaddr:

段数据应该加载到进程的虚拟地址.

p_paddr:

段数据应该加载到进程的物理地址.

p_filesz:

该段的文件大小.

p_memsz:

该段的内存大小.

p_flags:

段的内存属性.

p_flags

p_align:

该段数据的对齐值.

段示例:

段示例

攻防对抗

动态运行-修改API地址

要实现的效果是,代码中动态调用libc.so库中的exit函数,通过修改libc.so符号表中的exit函数的地址为任意函数地址,从而使在IDA中静态分析,看到的是调用exit函数,实际上调用的是其他函数.函数前加static关键字,可使该函数不导出.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#include <jni.h>
#include <string>
#include <stdio.h>

#include <elf.h>
#include <dlfcn.h>
#include <sys/mman.h>

#define SOINFO_NAME_LEN 128
struct soinfo
{
    const char name[SOINFO_NAME_LEN];
    Elf32_Phdr *phdr;
    int phnum;
    unsigned entry;
    unsigned base;
    unsigned size;

    int unused;  // DO NOT USE, maintained for compatibility.

    unsigned *dynamic;

    unsigned wrprotect_start;
    unsigned wrprotect_end;

    soinfo *next;
    unsigned flags;

    const char *strtab;
    Elf32_Sym *symtab;

    unsigned nbucket;
    unsigned nchain;
    unsigned *bucket;
    unsigned *chain;

    unsigned *plt_got;

    Elf32_Rel *plt_rel;
    unsigned plt_rel_count;

    Elf32_Rel *rel;
    unsigned rel_count;

    Elf32_Rela *plt_rela;
    unsigned plt_rela_count;

    Elf32_Rela *rela;
    unsigned rela_count;

    unsigned *preinit_array;
    unsigned preinit_array_count;

    unsigned *init_array;
    unsigned init_array_count;
    unsigned *fini_array;
    unsigned fini_array_count;

    void (*init_func)(void);
    void (*fini_func)(void);

    unsigned *ARM_exidx;
    unsigned ARM_exidx_count;
    
    unsigned refcount;
};

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

//获取模块基址
void *get_module_base(const char *moduleName) {
    char buf[260] = {};
    void *addr = NULL;

    FILE *fp = fopen("/proc/self/maps", "r");
    if (fp == NULL) {
        perror("fopen");
        return NULL;
    }

    while (!feof(fp)) {
        memset(buf, 0, sizeof buf);
        fgets(buf, sizeof buf, fp);
        if (strstr(buf, moduleName) != NULL) {
            sscanf(buf, "%08x", &addr);
            break;
        }
    }
    fclose(fp);
    return addr;
}

void FillSoInfoStruct(soinfo* si, const char* szLibName){
	//获取libc.so的模块基址
    char* libc_base = (char*)get_module_base(szLibName);
    si->base = (unsigned)libc_base;
	
    //拿动态节地址
    elf32_hdr* header = (elf32_hdr*)si->base;
    elf32_phdr* phdr = (elf32_phdr*)(si->base + header->e_phoff);
    Elf32_Dyn *dyn = NULL;
    for (int i = 0; i < header->e_phnum; ++i) {
        if (phdr[i].p_type == PT_DYNAMIC){
            dyn = (Elf32_Dyn*)(si->base + phdr[i].p_vaddr);
            si->dynamic = (unsigned *)dyn;
            break;
        }
    }

    printf("dyn = %p\n", dyn);

    //遍历表
    //遍历表的操作, Android源码中有写,这里查看的是Android 4.0.3_r1版本的源码
    //在xref: /bionic/linker/linker.c中
    //搜索DT_HASH
    unsigned *d;
    for(d = si->dynamic; *d; d++){
        switch(*d++){
            case DT_HASH:
                si->nbucket = ((unsigned *) (si->base + *d))[0];
                si->nchain = ((unsigned *) (si->base + *d))[1];
                si->bucket = (unsigned *) (si->base + *d + 8);
                si->chain = (unsigned *) (si->base + *d + 8 + si->nbucket * 4);
                break;
            case DT_STRTAB:
                si->strtab = (const char *) (si->base + *d);
                break;
            case DT_SYMTAB:
                si->symtab = (Elf32_Sym *) (si->base + *d);
                break;
        }
    }
}

//获取函数名对应的符号表的下标
int GetSymtabIndex(soinfo* si, char* szFuncName){

    //查询hash表
    int nIndex = elfhash(szFuncName) % si->nbucket;
    nIndex = si->bucket[nIndex];
    if (nIndex == 0){
        return 0;
    }

    do {
        if (strcmp(si->strtab + si->symtab[nIndex].st_name, szFuncName) == 0){
            break;
        }
        nIndex = si->chain[nIndex];
    } while (nIndex != 0);

    return nIndex;
}

//替换后的函数
//加static,可使该函数不导出
static void fun2(int n){
    puts("fun2");
}

__attribute__((constructor)) void fun1(){
    puts("fun1");

    //soinfo结构体是操作系统内部的结构体
    soinfo si = {0};

    //填充soinfo结构体
    FillSoInfoStruct(&si, "libc.so");
	
    //获取libc.so库中的exit函数在符号表中的下标
    int nIndex = GetSymtabIndex(&si, "exit");
    if (nIndex == 0){
        return;
    }

    //修改libc.so库中的exit函数地址为fun2的地址
    mprotect((void*)((int)&si.symtab[nIndex] & ~0xfff), 0x1000, PROT_READ | PROT_WRITE);
    si.symtab[nIndex].st_value = (char*)fun2 - (char*)si.base;
}

typedef void (*pfnEXIT)(int);
int main(int argc, char* argv[]){

    //直接调exit(0), 走的是plt函数
    void* handle = dlopen("libc.so", 0);
    pfnEXIT pfnExit = (pfnEXIT)dlsym(handle, "exit");
    pfnExit(0);

    return 0;
}

动态调用修改api地址.7z

Got表Hook

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#include <jni.h>
#include <string>
#include <stdio.h>

#include <elf.h>
#include <dlfcn.h>
#include <sys/mman.h>

#include <unistd.h>
#include <limits.h>

#define SOINFO_NAME_LEN 128
struct soinfo
{
    const char name[SOINFO_NAME_LEN];
    Elf32_Phdr *phdr;
    int phnum;
    unsigned entry;
    unsigned base;
    unsigned size;

    int unused;  // DO NOT USE, maintained for compatibility.

    unsigned *dynamic;

    unsigned wrprotect_start;
    unsigned wrprotect_end;

    soinfo *next;
    unsigned flags;

    const char *strtab;
    Elf32_Sym *symtab;

    unsigned nbucket;
    unsigned nchain;
    unsigned *bucket;
    unsigned *chain;

    unsigned *plt_got;

    Elf32_Rel *plt_rel;
    unsigned plt_rel_count;

    Elf32_Rel *rel;
    unsigned rel_count;

    Elf32_Rela *plt_rela;
    unsigned plt_rela_count;

    Elf32_Rela *rela;
    unsigned rela_count;

    unsigned *preinit_array;
    unsigned preinit_array_count;

    unsigned *init_array;
    unsigned init_array_count;
    unsigned *fini_array;
    unsigned fini_array_count;

    void (*init_func)(void);
    void (*fini_func)(void);

    unsigned *ARM_exidx;
    unsigned ARM_exidx_count;

    unsigned refcount;
};

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

//获取模块基址
void *get_module_base(const char *moduleName) {
    char buf[260] = {};
    void *addr = NULL;

    FILE *fp = fopen("/proc/self/maps", "r");
    if (fp == NULL) {
        perror("fopen");
        return NULL;
    }

    while (!feof(fp)) {
        memset(buf, 0, sizeof buf);
        fgets(buf, sizeof buf, fp);
        if (strstr(buf, moduleName) != NULL) {
            sscanf(buf, "%08x", &addr);
            break;
        }
    }
    fclose(fp);
    return addr;
}

void FillSoInfoStruct(soinfo* si, const char* szLibName){
    //获取模块基址
    char* libc_base = (char*)get_module_base(szLibName);
    si->base = (unsigned)libc_base;

    //拿动态节地址
    elf32_hdr* header = (elf32_hdr*)si->base;
    elf32_phdr* phdr = (elf32_phdr*)(si->base + header->e_phoff);
    Elf32_Dyn *dyn = NULL;
    for (int i = 0; i < header->e_phnum; ++i) {
        if (phdr[i].p_type == PT_DYNAMIC){
            dyn = (Elf32_Dyn*)(si->base + phdr[i].p_vaddr);
            si->dynamic = (unsigned *)dyn;
            break;
        }
    }

    //遍历表
    //遍历表的操作, Android源码中有写,这里查看的是Android 4.0.3_r1版本的源码
    //在xref: /bionic/linker/linker.c中
    //搜索DT_HASH
    unsigned *d;
    for(d = si->dynamic; *d; d++){
        switch(*d++){
            case DT_HASH:
                si->nbucket = ((unsigned *) (si->base + *d))[0];
                si->nchain = ((unsigned *) (si->base + *d))[1];
                si->bucket = (unsigned *) (si->base + *d + 8);
                si->chain = (unsigned *) (si->base + *d + 8 + si->nbucket * 4);
                break;
            case DT_STRTAB:
                si->strtab = (const char *) (si->base + *d);
                break;
            case DT_SYMTAB:
                si->symtab = (Elf32_Sym *) (si->base + *d);
                break;
            case DT_JMPREL:
                //第一个重定位表
                si->plt_rel = (Elf32_Rel*) (si->base + *d);
                break;
            case DT_PLTRELSZ:
                si->plt_rel_count = *d / 8;
                break;
            case DT_REL:
                //第二个重定位表
                si->rel = (Elf32_Rel*) (si->base + *d);
                break;
            case DT_RELSZ:
                si->rel_count = *d / 8;
        }
    }
}

//获取函数名对应的符号表的下标
int GetSymtabIndex(soinfo* si, const char* szFuncName){
    //查询hash表
    int nIndex = elfhash(szFuncName) % si->nbucket;
    nIndex = si->bucket[nIndex];
    if (nIndex == 0){
        return 0;
    }

    do {
        if (strcmp(si->strtab + si->symtab[nIndex].st_name, szFuncName) == 0){
            break;
        }
        nIndex = si->chain[nIndex];
    } while (nIndex != 0);

    return nIndex;
}


__attribute__((constructor)) void fun1(){
    fflush(stdout);
    puts("fun1");
}


size_t FakeStrlen(const char* const){
    return 100;
}

typedef size_t (*PFN_STRLEN)(const char* const);
void* g_oldpfn = nullptr;

//全局变量
PFN_STRLEN g_pfnstrlen = strlen;

bool LuoHook(const char* lib, const char* name, void* addr, void** OldAddr){

    /*
     * 1.找到strlen符号索引(哈希表,符号表,字符串表)
     * 2.遍历重定位表,修改got地址(重定位表)
     **/

    //soinfo结构体是操作系统内部的结构体
    soinfo si = {0};
    //填充soinfo结构体
    FillSoInfoStruct(&si, lib);

    int nIndex = GetSymtabIndex(&si, name);

    if (nIndex == 0){
        return false;
    }

    //修改.rel.plt
    for (int i = 0; i < si.plt_rel_count; ++i) {
        //判断符号
        if (ELF32_R_SYM(si.plt_rel[i].r_info) == nIndex){
            //找到Got地址
            void** got = (void**)(si.base + si.plt_rel[i].r_offset);

            //修改内存保护属性
            size_t pageSize = sysconf(_SC_PAGE_SIZE);
            if (mprotect((void*)((int)got & ~(pageSize - 1)),
                    pageSize,
                    PROT_READ | PROT_WRITE) < 0){

                perror("mprotect");
                return false;
            }

            //保存旧的地址
            *OldAddr = *got;
            //修改got地址
            *got = addr;
        }
    }

    //.rel.dyn
    for (int i = 0; i < si.rel_count; ++i) {
        //判断符号
        if (ELF32_R_SYM(si.rel[i].r_info) == nIndex){
            //找到Got地址
            void** got = (void**)(si.base + si.rel[i].r_offset);

            //修改内存保护属性
            size_t pageSize = sysconf(_SC_PAGE_SIZE);
            if (mprotect((void*)((int)got & ~(pageSize - 1)),
                         pageSize,
                         PROT_READ | PROT_WRITE) < 0){

                perror("mprotect");
                return false;
            }

            //保存旧的地址
            *OldAddr = *got;
            //修改got地址
            *got = addr;
        }
    }

    return true;
}

int main(int argc, char* argv[]){

    //Got表Hook
    //遍历重定位表,修改got地址
    //有两个重定位表都需要来修
    //.rel.plt
    // .rel.dyn
    LuoHook("/data/local/tmp/luoelf", "strlen",(void*)FakeStrlen, (void**)&g_oldpfn);

    //全局变量
    printf("%d\n", g_pfnstrlen("1"));

    //局部变量
    PFN_STRLEN pfnStrlen = strlen;
    printf("%d\n", pfnStrlen("12"));

    //直接调
    printf("%d\n", strlen("123"));

    return 0;
}

Got表Hook.7z

阅读系统加载动态节数据源码

系统加载动态节数据源码

从以上代码,我们能够发现两个问题, ①系统读取动态节数据,并没有拿动态节的大小字段,而是通过*d,判断是否是0,若是0,就结束. ②动态节每项8字节,描述了什么表,在哪里.如果动态节中有重复的表,会以最后的表为准(重复的,最后一个会将前面的覆盖掉),这样我们可以在原始动态节的后面伪造数据,只要动态节的结尾是0即可.我们可以将原始动态节的其中一个表多伪造几组假数据,只要动态节的最后,这个表记录的地址是原始的即可.这样修改后,IDA静态分析不会出错,但是IDA的动态调试就会出现问题.

抹除文件格式

我们知道程序的运行是不需要节的,需要的是段,因为段描述了内存映射. 但是当程序运行起来的时候,内存已经映射完成,此时我们可以将ELF文件的头以及程序头表抹除, 这样不会影响程序的运行,可以防止别人内存Dump. 如何破这一招? 内存可以抹除文件格式,但是文件不可以,我们可以先内存Dump, 然后从文件中将ELF文件的头以及程序头表复制到Dump出来的文件中.

重建节表

IDA在解析ELF文件时,会去拿节表中的值,来提高反汇编的效果.而软件的运行,是不需要节表的.所以我们可以修改节表中的内容,来欺骗IDA.如修改节表代码段中的文件偏移或者大小,或者数据段已初始化节中的文件偏移或大小等.

我们如何预防别人这样的操作呢? 我们将节表删了(即将ELF文件头中的e_shoff设置为0)用IDA分析,来和未删节表前来进行对比.

没有节表,IDA的反汇编效果很差,但是有节表,又担心被骗,我们可以根据段来重建节表.

节和段的关系:

重建节表

plt:

结构:

重建节表1

重建节表2

FOA:

DT_JMPREL表的FOA + SIZE,就是plt表的FOA.

SIZE:

遍历段中的DT_JMPREL,拿重定位的api项数,记为x,plt的头为20字节,每个plt项12字节,则plt节的大小为 12 * x + 20.

text:

FOA:

plt表的FOA + SIZE,就是text表的FOA.

SIZE:

PT_SHT_ARM_EXIDX表的FOA - (plt表的FOA + plt表的SIZE).

数据段:

1
2
.data //初始化数据段,占空间
.bss  //未初始化数据段,不占空间

根据程序头表中的内存映射,来区分是.data还是.bss,映射内存的是.data,否则是.bss.

参考链接

ELF文件格式解析


相关内容

0%