基于Android的ELF PLT/GOT符号重定向过程
Posted SingleShu888
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于Android的ELF PLT/GOT符号重定向过程相关的知识,希望对你有一定的参考价值。
引言
写这篇技术文的原因,主要有两个:
- 其一是发现网上大部分描述PLT/GOT符号重定向过程的文章都是针对x86的,比如《Redirecting functions in shared ELF libraries》就写得非常不错。虽然其过程跟ARM非常类似,但由于CPU体系不同,指令实现差异非常大;
- 其二是网上大部分关于ELF文件格式的介绍,都是基于链接视图(Linking View),链接视图是基于节(Section)对ELF进行解析的。然而动态链接库在加载的过程中,linker只关注ELF中的段(Segment)信息。因此ELF中的节信息被完全篡改或者甚至删除掉,并不会影响linker的加载过程,这样做可以防止静态分析工具(比如IDA,readelf等)对其进行分析,一般加过壳的ELF文件都会有这方面的处理。对于这种ELF文件,如果要实现hook功能,则必须要基于执行视图(Execution View)进行符号解析;
准备
在往下阅读之前,请先确保对ELF文件格式和ARM汇编有个大概了解,参考指引:
准备工具:
- readelf(NDK包含)
- objdump(NDK包含)
- IDA Pro 6.4或以上
- android真机或者模拟器
符号重定向
在ARM上,常见的重定向类型,主要有三种,分别是R_ARM_JUMP_SLOT、R_ARM_ABS32和R_ARM_GLOB_DAT,而我们要hook elf函数,则需要同时处理好这三种重定向类型。
例子
先看示例代码
typedef int (*strlen_fun)(const char *);
strlen_fun global_strlen1 = (strlen_fun)strlen;
strlen_fun global_strlen2 = (strlen_fun)strlen;
#define SHOW(x) LOGI("%s is %d", #x, x)
extern "C" jint Java_com_example_allhookinone_HookUtils_elfhook(JNIEnv *env, jobject thiz)
const char *str = "helloworld";
strlen_fun local_strlen1 = (strlen_fun)strlen;
strlen_fun local_strlen2 = (strlen_fun)strlen;
int len0 = global_strlen1(str);
int len1 = global_strlen2(str);
int len2 = local_strlen1(str);
int len3 = local_strlen2(str);
int len4 = strlen(str);
int len5 = strlen(str);
SHOW(len0);
SHOW(len1);
SHOW(len2);
SHOW(len3);
SHOW(len4);
SHOW(len5);
return 0;
这段代码分别以三种不同的方式调用strlen,分别是全局函数指针、局部函数指针以及直接调用,下而我们针对这个例子,分别对三种调用分析进行分析。
先通过readelf,我们查看一下重定向表,如下所示:
Relocation section '.rel.dyn' at offset 0x2a48 contains 17 entries:
Offset Info Type Sym.Value Sym. Name
0000ade0 00000017 R_ARM_RELATIVE
0000af00 00000017 R_ARM_RELATIVE
0000af0c 00000017 R_ARM_RELATIVE
0000af10 00000017 R_ARM_RELATIVE
0000af18 00000017 R_ARM_RELATIVE
0000af1c 00000017 R_ARM_RELATIVE
0000af20 00000017 R_ARM_RELATIVE
0000af24 00000017 R_ARM_RELATIVE
0000af28 00000017 R_ARM_RELATIVE
0000af30 00000017 R_ARM_RELATIVE
0000aefc 00003215 R_ARM_GLOB_DAT 00000000 __stack_chk_guard
0000af04 00003715 R_ARM_GLOB_DAT 00000000 __page_size
0000af08 00004e15 R_ARM_GLOB_DAT 00000000 strlen
0000b004 00004e02 R_ARM_ABS32 00000000 strlen
0000b008 00004e02 R_ARM_ABS32 00000000 strlen
0000af14 00006615 R_ARM_GLOB_DAT 00000000 __gnu_Unwind_Find_exid
0000af2c 00007415 R_ARM_GLOB_DAT 00000000 __cxa_call_unexpected
...
...
Relocation section '.rel.plt' at offset 0x2ad0 contains 48 entries:
Offset Info Type Sym.Value Sym. Name
0000af40 00000216 R_ARM_JUMP_SLOT 00000000 __cxa_atexit
0000af44 00000116 R_ARM_JUMP_SLOT 00000000 __cxa_finalize
0000af48 00001716 R_ARM_JUMP_SLOT 00000000 memcpy
...
0000afd4 00004c16 R_ARM_JUMP_SLOT 00000000 fgets
0000afd8 00004d16 R_ARM_JUMP_SLOT 00000000 fclose
0000afdc 00004e16 R_ARM_JUMP_SLOT 00000000 strlen
0000afe0 00004f16 R_ARM_JUMP_SLOT 00000000 strncmp
...
...
在.rel.plt和.rel.dyn两个section中,我们发现一共出现了4个strlen,我们先把它们的关键信息记录下来,后面分析会非常有用。它们分别是
.rel.dyn 0000AF08 R_ARM_GLOB_DAT
.rel.dyn 0000B004 R_ARM_ABS32
.rel.dyn 0000B008 R_ARM_ABS32
.rel.plt 0000AFDC R_ARM_JUMP_SLOT
在代码中,我们一共调用了6次strlen,但为什么只出现了4次呢?另外,它们之间又是如何对应的呢,带着这些问题去分析汇编代码。把编译出来的so拖到IDA,我们看到示例代码的指令:
.text:000050BC EXPORT Java_com_example_allhookinone_HookUtils_elfhook
.text:000050BC Java_com_example_allhookinone_HookUtils_elfhook
.text:000050BC
.text:000050BC var_40 = -0x40
.text:000050BC var_38 = -0x38
.text:000050BC var_34 = -0x34
.text:000050BC s = -0x2C
.text:000050BC var_28 = -0x28
.text:000050BC var_24 = -0x24
.text:000050BC var_20 = -0x20
.text:000050BC var_1C = -0x1C
.text:000050BC var_18 = -0x18
.text:000050BC var_14 = -0x14
.text:000050BC var_10 = -0x10
.text:000050BC var_C = -0xC
.text:000050BC
.text:000050BC PUSH R4,LR
.text:000050BE SUB SP, SP, #0x38
.text:000050C0 STR R0, [SP,#0x40+var_34]
.text:000050C2 STR R1, [SP,#0x40+var_38]
.text:000050C4 LDR R4, =(_GLOBAL_OFFSET_TABLE_ - 0x50CA)
.text:000050C6 ADD R4, PC ; _GLOBAL_OFFSET_TABLE_
.text:000050C8 LDR R3, =(aHelloworld - 0x50CE)
.text:000050CA ADD R3, PC ; "helloworld"
.text:000050CC STR R3, [SP,#0x40+s]
.text:000050CE LDR R3, =(strlen_ptr - 0xAF34)
.text:000050D0 LDR R3, [R4,R3] ; __imp_strlen
.text:000050D2 STR R3, [SP,#0x40+var_28]
.text:000050D4 LDR R3, =(strlen_ptr - 0xAF34)
.text:000050D6 LDR R3, [R4,R3] ; __imp_strlen
.text:000050D8 STR R3, [SP,#0x40+var_24]
.text:000050DA LDR R3, =(global_strlen1_ptr - 0xAF34)
.text:000050DC LDR R3, [R4,R3] ; global_strlen1
.text:000050DE LDR R3, [R3]
.text:000050E0 LDR R2, [SP,#0x40+s]
.text:000050E2 MOVS R0, R2
.text:000050E4 BLX R3
.text:000050E6 MOVS R3, R0
.text:000050E8 STR R3, [SP,#0x40+var_20]
.text:000050EA LDR R3, =(global_strlen2_ptr - 0xAF34)
.text:000050EC LDR R3, [R4,R3] ; global_strlen2
.text:000050EE LDR R3, [R3]
.text:000050F0 LDR R2, [SP,#0x40+s]
.text:000050F2 MOVS R0, R2
.text:000050F4 BLX R3
.text:000050F6 MOVS R3, R0
.text:000050F8 STR R3, [SP,#0x40+var_1C]
.text:000050FA LDR R2, [SP,#0x40+s]
.text:000050FC LDR R3, [SP,#0x40+var_28]
.text:000050FE MOVS R0, R2
.text:00005100 BLX R3
.text:00005102 MOVS R3, R0
.text:00005104 STR R3, [SP,#0x40+var_18]
.text:00005106 LDR R2, [SP,#0x40+s]
.text:00005108 LDR R3, [SP,#0x40+var_24]
.text:0000510A MOVS R0, R2
.text:0000510C BLX R3
.text:0000510E MOVS R3, R0
.text:00005110 STR R3, [SP,#0x40+var_14]
.text:00005112 LDR R3, [SP,#0x40+s]
.text:00005114 MOVS R0, R3 ; s
.text:00005116 BLX strlen
.text:0000511A MOVS R3, R0
.text:0000511C STR R3, [SP,#0x40+var_10]
.text:0000511E LDR R3, [SP,#0x40+s]
.text:00005120 MOVS R0, R3 ; s
.text:00005122 BLX strlen
.text:00005126 MOVS R3, R0
...
...
.text:000051CA ADD SP, SP, #0x38
.text:000051CC POP R4,PC
.text:000051CC ; End of function Java_com_example_allhookinone_HookUtils_elfhook
先把几个重要的地址找出来,它们分别是
- GLOBAL_OFFSET_TABLE: 0x0000AF34
- strlen_ptr: 0x0000AF08
- __imp_strlen: 0x0000B0C8
- global_strlen1_ptr: 0x0000AF0C
- global_strlen1: 0x0000B004
- global_strlen2_ptr: 0x0000AF10
- global_strlen2: 0x0000B008
##全局函数指针调用外部函数
global_strlen1和global_strlen2的调用,对应0x000050E4和0x000050F4两处的BLX指令,通过计算最终R3的值分别是*global_strlen1和*global_strlen2,而global_strlen1和global_strlen2的值正好对应位于.rel.dyn的两个R_ARM_ABS32的重定位项,因此我们得出结论:通过全局函数指针的方式调用外部函数,它的重定位类型是R_ARM_ABS32,并且位于.rel.dyn节区。
我们只分析global_strlen1的调用过程,首先定位到global_strlen1_ptr(0x0000AF0C),该地址位于.got节区,_GLOBAL_OFFSET_TABLE_的上方。然后再通过global_strlen1_ptr定位到0x0000B004(位于.data节区),最后再通过0x0000B004定位到最终的函数地址,因此R_ARM_ABS32重定位项的Offset指向最终调用函数地址的地址(也就是函数指针的指针),整个重定位过程是先位到.got,再从.got定位到.date。下面是.got段区的16进制表示片段:
...
0000AF0C 04 B0 00 00 08 B0 00 00 DC B0 00 00 B4 87 00 00
0000AF1C F4 84 00 00 60 5B 00 00 58 5B 00 00 50 5B 00 00
0000AF2C EC B0 00 00 FC 8C 00 00 00 00 00 00 00 00 00 00
...
0000B004 C8 B0 00 00 C8 B0 00 00 ?? ?? ?? ?? ?? ?? ?? ??
0000B014 ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ?? ??
0000B024 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
0000B0C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000B0D8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
最后发现0x0000B0C8地址片的指令全为0,当动态链接时,linker会覆盖0x0000B004地址的值,指向strlen的真正地址(而不是现在的0x0000B0C8,有点绕)。
局部函数指针调用外部函数
local_strlen1和local_strlen2的调用,对应0x00005100和0x0000510C两处的BLX指令,通过计算最终R3的值都是*strlen_prt,即0x0000AF08,正好对应位于.rel.dyn中的R_ARM_GLOB_DAT重定位项,因此我们得出结论:通过局部函数指针方式调用外部函数,它的重定位类型是R_ARM_GLOB_DAT,并且位于.re.dyn节区。
我们只分析local_strlen1的调用过程,首先是定位到strlen_prt(0x0000AF08),该地址位于.got节区,_GLOBAL_OFFSET_TABLE_的上方,然后再通过strlen_prt,定位到0x0000B0C8,跟上面分析的结果居然一样,因此R_ARM_GLOB_DAT的重定项Offset指向最终调用函数地址的地址(也就是函数指针的指针),下面是.got段区的16进制表示片段:
0000AF08 C8 B0 00 00 04 B0 00 00 08 B0 00 00 DC B0 00 00
0000AF18 B4 87 00 00 F4 84 00 00 60 5B 00 00 58 5B 00 00
0000AF28 50 5B 00 00 EC B0 00 00 FC 8C 00 00 00 00 00 00
...
0000B0C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0000B0D8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
...
需要注意的是,0x000050D8的指令“STR R3, [SP,#0x40+var_24]”,这里已经把函数的真实地址保存到堆栈了,因此哪怕我们修改了GOT表也不会影响堆栈的值,因此这两种重定位类型无法通过修改地址进行hook。(这个地方我觉得有问题,这里只是把.got中存储的地址保存到栈中了,如果我在so加载后马上修改GOT表,那么函数执行的时候依然会调用hook后的函数吧?) 而且经过我实际的试验,应该R_ARM_GLOB_DAT对应全局函数指针,R_ARM_ABS32对应局部函数指针才对。
##直接调用外部函数
最后看看strlen的直接调用,对应0x0000511A和0x00005122两处的BLX指令,最后它们都指向.plt节区指令,如下所示:
.plt:00002E38 ADR R12, 0x2E40
.plt:00002E3C ADD R12, R12, #0x8000
.plt:00002E40 LDR PC, [R12,#(strlen_ptr_0 - 0xAE40)]! ; __imp_strlen
...
0000AFDC C8 B0 00 00 CC B0 00 00 D0 B0 00 00 D4 B0 00 00
0000AFEC D8 B0 00 00 DC B0 00 00 E0 B0 00 00 E4 B0 00 00
0000AFFC E8 B0 00 00 00 00 00 00 C8 B0 00 00 C8 B0 00 00
...
最后,PC指向*strlen_ptr_0,即strlen_ptr_0的地址0x0000AFDC,该地址位于.got节区,而0x0000AFDC地址值的正好是0x0000B0C8,多么熟悉的身影。因此得到结论,直接调用外部函数,它的重定位类型是R_ARM_JUMP_SLOT,并且位于.re.plt节区,其Offset指向最终调用函数地址的地址(也就是函数指针的指针)。整个过程是先到.plt,再到.got,最后才定位到真正的函数地址。
以上是关于基于Android的ELF PLT/GOT符号重定向过程的主要内容,如果未能解决你的问题,请参考以下文章
elf文件中的.plt .rel.dyn .rel.plt .got .got.plt的关系
Android 逆向ELF 文件格式 ( 程序头数据 | 节区头数据 | 动态符号表 )