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 | fd = open(libPath, O_RDONLY) |
2、从ELF header获得节区头表section header table的偏移
1 | shtoff = elf->e_shoff; |
3、遍历节区头表找到类型为SHT_DYNSYM的节区头,即.dynsym节区头,根据节区偏移sh_off、节区大小sh_size将.dynsym节区拷贝内存中
1 | Elf_Shdr *sh = (Elf_Shdr *)shtAddr; |
4、遍历节区头表找到类型为SHT_STRTAB的节区头,即.dynstr节区头,根据节区偏移sh_off、节区大小sh_size将.dynstr节区拷贝内存中
1 | ctx->dynstr = calloc(1, 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 | void *my_dlsym(void *ctxStruct,char *symName){ |
2、回调函数
hook函数的参数
在shellcode中,保存上下文,将通用寄存器、CPSR、LR、SP保存到栈上,并将sp赋值给R0,然后回调beforeHook函数,参数用 struct pt_regs *
类型接收,获得和修改函数参数
1 | mov r0, sp |
hook函数返回值
很难确定函数返回时PC的值,所以通过在beforeHook回调函数中获得LR的值,对LR所在的指令进行hook,通过R0获得和修改函数返回值。
3、指令修复
采用跳板的方式跳回原指令,因为arm架构的三级流水线设计,在跳回原指令的跳板中,需要有备份的2条指令,如果这两条指令涉及PC的操作,那么就需要对指令修复
B BL BX BLX指令修复
针对如下指令情况进行修复:
1 | B imme24 |
imme24表示有符号的26位整数,因为arm32地址是4字节对齐,所以后2bit为0,因此用24bit表示26bit数据。
首先是计算跳转的真正地址,imme24是相对PC值的地址,PC是32位有符号的整数,因此要想计算真正的地址,需要将26bit数据符号扩充到32位,计算后的值要用有符号整型存储,具体操作如下:
1 | int imme32; |
BL、BLX链接跳转后会通过LR寄存器返回,所以修复方案是将跳转改成LDR PC的形式,并手动修改LR寄存器:
1 | ADD LR, PC, #4 |
LDR
针对以下指令情况修复:(这里没见过 LDR Rd,[Rm,PC] 的情况)
1 | LDR Rd,[PC,Rm] |
LDR命令第23 bit位,U标志位表示加减,0减1加。修复的主要思路是用一个临时寄存器r来保存PC的值,r不能是Rd或者Rm,将上述指令修改为:
1 | LDR Rd,[r,Rm] |
具体如下:
1 | PUSH {r} |
用 ADD PC,PC,#0
对PC的操作会清空三级流水线,那么PC就会跳过正在取码,译码的指令,从 LDR PC,[PC,#-4]
开始取码
ADD、MOV
针对以下指令类型修复:
1 | ADD Rd,PC,Rm |
修复方法和LDR类似,将PC用另外一个寄存器r替换:
1 | ADD Rd,r,Rm |
ADR
ADR是小范围的地址读取伪指令.ADR 指令将基于PC 相对偏移的地址值读取到寄存器中.在汇编编译源程序时,ADR 伪指令被编译器替换ADD 指令或SUB 指令来实现该ADR 伪指令的功能,
1 | ldr r0, _start |
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 | STMFD SP!,{R0-R8,LR} |