pollux's Dairy

inlineHookS安卓内联hook框架

字数统计: 2.3k阅读时长: 14 min
2020/02/19 Share

PROLOGUE

1、获得目标函数符号地址,将目标NDK库映射到内存,并解析动态符号表和动态字符串表,获得目标函数的符号地址(为了突破android7后系统对私有NDK库的访问限制)

2、hook过程有4个跳板代码组成,因为arm架构的三级流水线设计,在hook点首先要备份8字节的代码,将其修改为长跳转指令,跳转到跳板0-beforeshellcoode,在beforeshellcode中首先将处理机上下文压入栈,并将sp赋值给r0,将lr赋值给了r1,接着回调beforehook函数,由于arm32的函数调用约定,进入被调函数体后,r0~r4为前4个参数(之后的参数在栈上),所以beforehook函数就可以得到目标函数的参数,并可修改栈上的参数,beforeshellcode最后会将栈上的内容弹回原寄存器,完成了参数的输出与修改。

3、因为很难判断函数体的结尾,所以选择函数返回后的地址作为afterhook函数的hook点,通过beforehook函数获得lr寄存器的值,并在beforehook函数尾实现lr指向地址的指令的hook,将其跳转到跳板2-afterhookshellcode,在afterhook中输出或修改r0的值,即可完成函数返回值的输出和修改。

4、完成上述的hook,一共hook了两处代码,因为备份的代码可能存在涉及PC指令或相对跳转的操作,而执行完beforeshellcoode和afterhookshellcode后的PC值已经改变或者相对地址不再指向正确的地址。因此需要指令修复,修复指令分别在beforeshellcode和aftershellcode后的跳板1和跳板2中,实现跳转回到hook点

  • 对于涉及PC指令的修复,是选一个暂时用不到的寄存器Rr保存原PC的值,修改指令,将PC寄存器替换为Rr,即可完成修复。
  • 对于相对地址的跳转,则是先计算出绝对地址,使用寄存器远跳完成指令修复。

5、(存在问题2.27更新)想实现任意进程的hook,就需要将hook库注入到目标进程,Xposed通过替换app_process,将代码注入到zygote进程,这样每个app进程都存在xposed的代码。libc.so每个app也都会调用,尝试通过patch libc.so的.init_array节区,将其指向一段shellcode,shellcode加载hook库,并在返回值调用源.init_array中的函数。这样每个进程确实被注入了hook库,但是app在启动时并没有触发hook库中的JNI_Onload函数。

问题:zygote在被复制前已经加载了libc.so,而所有app都是复制的zygote,所以libc.so不是被所有app进程加载,而是被zygote加载。所以hook库没有在app启动时调用JNI_Onload中的函数。

下面具体说下实现细节:

1、获得函数符号地址

android7后,系统将阻止应用动态链接非公开NDK库,所以采用自映射目标NDK库的方法,解析ELF文件,获得.dynsym、.dynstr节区的信息和程序定义节区偏移,将其保存在ctx结构体变量中

1
void *ctx = my_dlopen(libpath);

my_dlopen内容:

1、将目标NDK库映射到内存中

1
2
fd = open(libPath, O_RDONLY)
elf = (Elf32_Ehdr *) mmap(0, size, PROT_READ, MAP_SHARED, fd, 0)

2、从ELF header获得节区头表section header table的偏移

1
shtoff = elf->e_shoff;

3、遍历节区头表找到类型为SHT_DYNSYM的节区头,即.dynsym节区头,根据节区偏移sh_off、节区大小sh_size将.dynsym节区拷贝内存中

1
2
3
Elf_Shdr *sh = (Elf_Shdr *)shtAddr;
ctx->dynsym = calloc(1, sh->sh_size);
memcpy(ctx->dynsym, ((void *) elf + sh->sh_offset), sh->sh_size);

4、遍历节区头表找到类型为SHT_STRTAB的节区头,即.dynstr节区头,根据节区偏移sh_off、节区大小sh_size将.dynstr节区拷贝内存中

1
2
ctx->dynstr = calloc(1, sh->sh_size);
memcpy(ctx->dynstr, (void *) elf + sh->sh_offset, sh->sh_size);

5、遍历节区头表找到类型为SHT_PROGBITS的节区头,即程序定义节区,计算得到偏移,动态链接库文件中 sh->sh_addr = sh->sh_offset

ctx->off = sh->sh_addr - sh->sh_offset;

通过my_dlsym获得符号地址

.dynsym节区中的符号名为.dymstr节区的下标,通过符号名找到符号的偏移,计算出符号地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void *my_dlsym(void *ctxStruct,char *symName){
if(ctxStruct==NULL){
return NULL;
}
CTXINFO *ctx = (CTXINFO*)ctxStruct;
Elf_Sym *sym =(Elf_Sym*) ctx->dynsym;
char *dynstrAddr = ctx->dynstr;
int i;
for(i=0;i<ctx->symsNum;i++,sym++){
char *name = dynstrAddr+sym->st_name;
if(strcmp(name,symName)==0){
void *res = ctx->execAddr+sym->st_value-ctx->off;
return res;
}
}
return NULL;
}

2、回调函数

hook函数的参数

在shellcode中,保存上下文,将通用寄存器、CPSR、LR、SP保存到栈上,并将sp赋值给R0,然后回调beforeHook函数,参数用 struct pt_regs *类型接收,获得和修改函数参数

1
2
3
4
mov     r0, sp
ldr r1,[sp,#0x38]
ldr r3, _new_function_addr_s
blx r3

hook函数返回值

很难确定函数返回时PC的值,所以通过在beforeHook回调函数中获得LR的值,对LR所在的指令进行hook,通过R0获得和修改函数返回值。

3、指令修复

采用跳板的方式跳回原指令,因为arm架构的三级流水线设计,在跳回原指令的跳板中,需要有备份的2条指令,如果这两条指令涉及PC的操作,那么就需要对指令修复

B BL BX BLX指令修复

针对如下指令情况进行修复:

1
2
3
4
B imme24
BL imme24
BX PC
BLX imme24

imme24表示有符号的26位整数,因为arm32地址是4字节对齐,所以后2bit为0,因此用24bit表示26bit数据。

首先是计算跳转的真正地址,imme24是相对PC值的地址,PC是32位有符号的整数,因此要想计算真正的地址,需要将26bit数据符号扩充到32位,计算后的值要用有符号整型存储,具体操作如下:

1
2
3
4
5
6
7
int imme32;
int value;
x = (instruction & 0xFFFFFF); //取指令中的24bit立即数
topBit = x >> 23;
imme32 = topBit ? ((x << 2) | 0xFC000000) : x<<2;
value = imme32 + pc;
对于BX PC的情况,value = pc

BL、BLX链接跳转后会通过LR寄存器返回,所以修复方案是将跳转改成LDR PC的形式,并手动修改LR寄存器:

1
2
3
4
5
ADD LR, PC, #4
LDR PC, [PC, #-4]
value
LDR PC,[PC,#-4]
backAddr

LDR

针对以下指令情况修复:(这里没见过 LDR Rd,[Rm,PC] 的情况)

1
2
LDR Rd,[PC,Rm]
LDR Rd,[PC,#imme]

LDR命令第23 bit位,U标志位表示加减,0减1加。修复的主要思路是用一个临时寄存器r来保存PC的值,r不能是Rd或者Rm,将上述指令修改为:

1
2
LDR Rd,[r,Rm]
LDR Rd,[r,#imme]

具体如下:

1
2
3
4
5
6
7
8
PUSH {r}
LDR r,[PC,#8] ;r为PC的值
LDR Rd,[r,Rm] ;将LDR Rd,[PC,Rm]中的PC替换
POP {r}
ADD PC,PC,#0
pc
LDR PC,[PC,#-4]
backAddr

ADD PC,PC,#0 对PC的操作会清空三级流水线,那么PC就会跳过正在取码,译码的指令,从 LDR PC,[PC,#-4] 开始取码

ADD、MOV

针对以下指令类型修复:

1
2
3
ADD Rd,PC,Rm
ADD Rd,PC
MOV Rd,PC

修复方法和LDR类似,将PC用另外一个寄存器r替换:

1
2
3
ADD Rd,r,Rm
ADD Rd,r
MOV Rd,r

ADR

ADR是小范围的地址读取伪指令.ADR 指令将基于PC 相对偏移的地址值读取到寄存器中.在汇编编译源程序时,ADR 伪指令被编译器替换ADD 指令或SUB 指令来实现该ADR 伪指令的功能,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ldr r0, _start
adr r0, _start
ldr r0, =_start
_start:
b _start

0x00000000: e59f0004 ldr r0, [pc, #4] ; 0xc
0x00000004: e28f0000 add r0, pc, #0 ; 0x0
0x00000008: e59f0000 ldr r0, [pc, #0] ; 0x10
0x0000000c: eafffffe b 0xc

r0 = 0xeafffffe
r0 = 0x0000000c
r0 = 0x0000000c

4、patch libc.so

为了实现hook任意动态链接库中的函数,需要把生成的so注入到目标进程。

__attribute__((constructor)) 修饰的函数会出现在动态链接库的 .init_array节区,.init_array节区中的函数指针在动态链接库被加载时就会运行。那么可以patch libc.so的 .init_array节区,使加载libc.so时,自动加载hookso。

将这段shellcode指令放在.rodata段,修改 .init_array节区的第一个函数指针,使之指向shellcode指令,并在shellcode指令最后,调用原函数指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
STMFD	SP!,{R0-R8,LR}
LDR R0,[PC,#24] ;R0 = 0xC
MOV R1,#0
ADD R0,PC,R0
;dlopen("soName",0)
; dlopenAddr = (dlopenOff - PC)>>2 = (dlopenOff - (shellcodeOff+0x18)) >>2
BL dlopenAddr
LDMFD SP!,{R0-R8,LR}
LDR R0,[PC,#8]
ADD R0,PC,R0
BX R0
soNameOff - shellcodeOff - 5*4
oldInitAddr - shellcodeOff - 9*4
0x0
CATALOG
  1. 1. PROLOGUE
  2. 2. 1、获得函数符号地址
  3. 3. 2、回调函数
  4. 4. 3、指令修复
    1. 4.1. B BL BX BLX指令修复
    2. 4.2. LDR
    3. 4.3. ADD、MOV
    4. 4.4. ADR
  5. 5. 4、patch libc.so