Android Native Hook技术(一)
原理分析
ADBI是一个著名的安卓平台hook框架,基于 动态库注入 与 inline hook 技术实现。该框架主要由2个模块构成:1)hijack负责将so注入到目标进程空间,2)libbase是注入的so本身,提供了inline hook能力。
源码目录中的example则是一个使用ADBI进行hook epoll_wait的示例。
hijack
hijack实现动态库注入功能,通过在目标进程插入dlopen()调用序列,加载指定so文件。要实现这个功能,主要做两件事情:
- 获得目标进程中dlopen()地址
- 在目标进程的栈空间上构造一处dlopen()调用
下面分别解决这两个问题
1. 获得目标进程中dlopen()地址
在ADBI中,通过下面代码来获得目标进程中dlopen()函数地址:
void *ldl = dlopen("libdl.so", RTLD_LAZY);
if (ldl) {
dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");
dlclose(ldl);
}
unsigned long int lkaddr;
unsigned long int lkaddr2;
find_linker(getpid(), &lkaddr);
find_linker(pid, &lkaddr2);
dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);
其中find_linker()函数功能是获取指定进程中linker的地址。
linker是Android提供的动态链接器,每个进程都会映射一份到自己的进程空间,而dlopen()函数就是在linker里面定义,其相对于linker头部偏移是固定的。
因此要计算某进程中dlopen()函数地址,只需分别取当前进程linker地址lkaddr和dlopen()地址dlopenaddr,并通过 /proc/pid_xxx/maps 读取被注入进程linker地址lkaddr2。
dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr) 即为目标进程中dlopen()地址。
2. 在目标进程的栈空间上构造dlopen()调用
要修改目标进程寄存器等信息,需使用到ptrace()函数,gdb等程序拥有查看、修改调试进程寄存器等的能力就是因为使用了ptrace()。
先将hijack attach到目标进程上去:
if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
printf("cannot attach to %d, error!\n", pid);
exit(1);
}
waitpid(pid, NULL, 0);
这时目标进程暂停,就可以通过ptrace对其进行修改了,以下代码获取寄存器值保存在regs中:
ptrace(PTRACE_GETREGS, pid, 0, ®s);
接下来要做的就是修改寄存器的值,在目标进程的栈空间上构造一处dlopen()调用,关键在sc数组:
unsigned int sc[] = {
0xe59f0040, //0 ldr r0, [pc, #64]
0xe3a01000, //1 mov r1, #0
0xe1a0e00f, //2 mov lr, pc
0xe59ff038, //3 ldr pc, [pc, #56]
0xe59fd02c, //4 ldr sp, [pc, #44]
0xe59f0010, //5 ldr r0, [pc, #16]
0xe59f1010, //6 ldr r1, [pc, #16]
0xe59f2010, //7 ldr r2, [pc, #16]
0xe59f3010, //8 ldr r3, [pc, #16]
0xe59fe010, //9 ldr lr, [pc, #16]
0xe59ff010, //10 ldr pc, [pc, #16]
0xe1a00000, //11 nop r0
0xe1a00000, //12 nop r1
0xe1a00000, //13 nop r2
0xe1a00000, //14 nop r3
0xe1a00000, //15 nop lr
0xe1a00000, //16 nop pc
0xe1a00000, //17 nop sp
0xe1a00000, //18 nop addr of libname
0xe1a00000, //19 nop dlopenaddr
};
接下来使用上文取到的寄存器值对sc数组进行初始化:
sc[11] = regs.ARM_r0;
sc[12] = regs.ARM_r1;
sc[13] = regs.ARM_r2;
sc[14] = regs.ARM_r3;
sc[15] = regs.ARM_lr;
sc[16] = regs.ARM_pc;
sc[17] = regs.ARM_sp;
sc[19] = dlopenaddr;
libaddr = regs.ARM_sp - n*4 - sizeof(sc);
sc[18] = libaddr;
上面代码数组内容就是我们要写入到目标进程当前栈空间的指令,即一份shellcode,接下来看一下这段shellcode实现了什么样的功能:
ldr r0,[pc,#64]
将so路径字符串地址存入r0
ARM指令集中PC寄存器总 指向当前指令的下两条指令
地址处,这是为了加快指令执行速度,如下图第一条指令执行时,第三条指令已经在读取:
指令一 > 读取 解析 执行
指令二 > 读取 解析 执行
指令三 > 读取 解析 执行
因此PC+64实际指向sc[18]的位置,取其内容即为so路径字符串的地址
mov r1,#0
将0赋值给r1寄存器。
ldr pc,[pc,#56]
调用dlopen()函数,第一个参数r0为so路径符串地址,第二个参数r1为0。
ldr sp, [pc, #44] ldr r0, [pc, #16] ldr r1, [pc, #16] ldr r2, [pc, #16] ldr r3, [pc, #16] ldr lr, [pc, #16] ldr pc, [pc, #16]
函数执行完后,依次恢复保存的 sp/r0/r1/r2/r3/lr/pc
寄存器,并继续执行。
接下来我们通过ptrace调用,将上面构造的shellcode以及so路径字符串写入到目标进程栈上:
// so name写入栈
if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) {
printf("cannot write library name (%s) to stack, error!\n", arg);
exit(1);
}
// shellcode 写入栈
codeaddr = regs.ARM_sp - sizeof(sc);
if (0 > write_mem(pid, (unsigned long*)&sc, sizeof(sc)/sizeof(long), codeaddr)) {
printf("cannot write code, error!\n");
exit(1);
}
/* Write NLONG 4 byte words from BUF into PID starting
at address POS. Calling process must be attached to PID. */
static int
write_mem(pid_t pid, unsigned long *buf, int nlong, unsigned long pos)
{
unsigned long *p;
int i;
for (p = buf, i = 0; i < nlong; p++, i++)
if (0 > ptrace(PTRACE_POKETEXT, pid, (void *)(pos+(i*4)), (void *)*p))
return -1;
return 0;
}
写入栈以后,shellcode并不能执行,因为当前Android都开启了栈执行保护,需要先通过mprotect(),来修改栈的可执行权限:
// 计算栈顶指针
regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc);
// 调用mprotect()设置栈可执行
regs.ARM_r0 = stack_start; // 栈起始位置
regs.ARM_r1 = stack_end - stack_start; // 栈大小
regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // 权限
if (nomprotect == 0) {
if (debug)
printf("calling mprotect\n");
regs.ARM_lr = codeaddr; // lr指向shellcode,mprotect()后执行
regs.ARM_pc = mprotectaddr;
}
// 旧版本Android没有栈保护,Android 2.3引入
else {
regs.ARM_pc = codeaddr;
}
这段代码首先计算栈顶位置,接着将栈 起始地址/栈大小/权限位
3个参数压栈,然后调用mprotect()函数设置栈的可执行权限,最后将lr寄存器设置为栈上代码的起始地址,这样当mprotect()函数返回后就可以正常执行栈上代码了。
最后,恢复目标进程的寄存器值,并恢复被ptrace()暂停的进程:
ptrace(PTRACE_SETREGS, pid, 0, ®s);
ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
if (debug)
printf("library injection completed!\n");
到目前为止,我们已经能够在指定进程加载任意so库了!
libbase
其实so注入到目标进程中后,hook功能完全可以在init_array中实现,但ADBI为了方便我们使用,编写了一个通用的hook框架libbase.so
libbase依然要解决2个问题:
- 定位被 hook 函数位置
- 进行 inline hook
关于获取hook函数地址的方法这里不再赘述。直接看inline hook部分,这部分功能在hook.c的hook()函数中实现,先看hook_t结构体:
struct hook_t {
unsigned int jump[3]; // 跳转指令(ARM)
unsigned int store[3]; // 原指令(ARM)
unsigned char jumpt[20]; // 跳转指令(Thumb)
unsigned char storet[20]; // 原指令(Thumb)
unsigned int orig; // 被hook函数地址
unsigned int patch; // 补丁地址
unsigned char thumb; // 补丁代码指令集,1为Thumb,2为ARM
unsigned char name[128]; // 被hook函数名
void *data;
};
hook_t是一个标准inline hook结构体,保存了 跳转指令/跳转地址/指令集/被hook函数名
等信息。因为ARM使用了ARM和Thumb两种指令集,所以代码中需进行区分:
if (addr % 4 == 0) {
/* ARM指令集 */
} else {
/* Thumb指令集 */
}
这样进行判断的依据是,Thumb指令的地址最后一位固定为 1。
接下来看一下ARM指令集分支的处理流程,这是该问题解决的核心部分:
if (addr % 4 == 0) {
log("ARM using 0x%lx\n", (unsigned long)hook_arm)
h->thumb = 0;
h->patch = (unsigned int)hook_arm;
h->orig = addr;
h->jump[0] = 0xe59ff000; // LDR pc, [pc, #0]
h->jump[1] = h->patch;
h->jump[2] = h->patch;
for (i = 0; i < 3; i++)
h->store[i] = ((int*)h->orig)[i];
for (i = 0; i < 3; i++)
((int*)h->orig)[i] = h->jump[i];
}
首先填充hook_t结构体,第一个for循环保存了原地址处3条指令,共12字节。第二个for循环用新的跳转指令进行覆写,关键的三条指令分别保存在jump[0]-[2]中:
jump[0]赋值0xe59ff000,翻译成ARM汇编为 ldr pc,[pc,#0]
,由于pc寄存器读出的值是当前指令地址加8,因此这条指令实际是将jump[2]的值加载到pc寄存器。
jump[2]保存的是hook函数地址。jump[1]仅用来4字节占位。Thumb分支原理与ARM分支一致,不再分析。
接下来我们注意到,函数最后调用了一处hook_cacheflush()函数:
hook_cacheflush((unsigned int)h->orig, (unsigned int)h->orig+sizeof(h->jumpt));
我们知道,现代处理器都有指令缓存,用来提高执行效率。前面我们修改的是内存中的指令,为防止缓存的存在,使我们修改的指令执行不到,需进行缓存的刷新:
void inline hook_cacheflush(unsigned int begin, unsigned int end)
{
const int syscall = 0xf0002;
__asm __volatile (
"mov r0, %0\n"
"mov r1, %1\n"
"mov r7, %2\n"
"mov r2, #0x0\n"
"svc 0x00000000\n"
:
: "r" (begin), "r" (end), "r" (syscall)
: "r0", "r1", "r7"
);
}
参考资料:
- ADBI github源码下载
- [ARM Cache Flush on mmap’d Buffers with __clear_cache()]bb
Android Native Hook技术(一)的更多相关文章
- Android Native Hook技术(二)
Hook技术应用 已经介绍了安卓 Native hook 原理,这里介绍 hook 技术的应用,及 Cyida Substrate 框架. 分析某APP,发现其POST请求数据经过加密,我们希望还原其 ...
- 使用cydia substrate 来进行android native hook
cydia不仅可以hook java代码,同样可以hook native代码,下面举一个例子来进行android native hook 我是在网上找到的supermathhook这个项目,在他基 ...
- 使用Cydia Substrate 从Native Hook Android Native世界
同系列文章: 使用Cydia Substrate 从Native Hook Android Java世界 使用Cydia Substrate Hook Android Java世界 一.建立工程 手机 ...
- Android so注入(inject)和Hook技术学习(三)——Got表hook之导出表hook
前文介绍了导入表hook,现在来说下导出表的hook.导出表的hook的流程如下.1.获取动态库基值 void* get_module_base(pid_t pid, const char* modu ...
- Hook技术
hook钩子: 使用技术手段在运行时动态的将额外代码依附现进程,从而实现替换现有处理逻辑或插入额外功能的目的. 它的技术实现要点有两个: 1)如何注入代码(如何将额外代码依附于现有代码中). 2)如何 ...
- Android热修复技术原理详解(最新最全版本)
本文框架 什么是热修复? 热修复框架分类 技术原理及特点 Tinker框架解析 各框架对比图 总结 通过阅读本文,你会对热修复技术有更深的认知,本文会列出各类框架的优缺点以及技术原理,文章末尾简单 ...
- Android Native 内存泄漏系统化解决方案
导读:C++内存泄漏问题的分析.定位一直是Android平台上困扰开发人员的难题.因为地图渲染.导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代码.解决这个问题对于产品质量尤为重要和关 ...
- Android热修复技术原理详解
阿里Dexposed -- native解决方案 原理: 直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能 他的思想完全来源于Xposed框架,完美诠释 ...
- 深入探索Android热修复技术原理读书笔记 —— 热修复技术介绍
1.1 什么是热修复 对于广大的移动开发者而言,发版更新是最为寻常不过的事了.然而,如果你 发现刚发出去的包有紧急的BUG需要修复,那你就必须需要经过下面这样的流程: 这就是传统的更新流程,步骤十分繁 ...
随机推荐
- Git:合并分支冲突问题
首先创建分支feature1,并且修改readme.txt内容,再在该分支上提交. 切换回master分支, 并且修改readme.txt内容,再在master分支上提交. 此时的分支路线如下,合并时 ...
- [CodeForces - 197E] E - Paint Tree
E - Paint Tree You are given a tree with n vertexes and n points on a plane, no three points lie on ...
- python and pycharm and django 环境配置
python 安装 https://www.python.org/ 我的是win7 32位,下载exe文件傻瓜式安装…… cmd 输入命令 python 则安装成功 如果不能进入,则有可能是环境 ...
- CentOS安装教程(VMware)
1.下载镜像文件 下载链接:https://wiki.centos.org/Download LinveCD--可装在CD光盘上启动的版本. LiveDVD--可装在DVD光盘上启动的版本. DVD1 ...
- Mysql设置自增字段的方法
#int : 字段类型 alter table 表名 modify 字段名 int auto_increment primary key
- laravel Eloquent 查询数据库判断获取的内容是否为空
原文地址:https://www.cnblogs.com/love-snow/articles/7205338.html 在使用 Laravel Eloquent 模型时,我们要判断取出的结果集是否为 ...
- Android 音视频深入 一 AudioRecord录音生成pcm转换为wav(附源码下载)
本篇项目地址,名字是AudioRecord录音(能暂停,将pch转换为wav),求starhttps://github.com/979451341/Audio-and-video-learning-m ...
- unity中把一个图片切割成两个UI图片
1.在unity3D的Project视图下选中需要更改的图片,将图片的Texture Type更改为Sprite (2D and UI),点击Apply即可.操作如图所示: 2.完成步骤一,点击App ...
- 执行这些代码, Edit1只能输入数字,小数点和负号,负号和小数点只能输入一个,负号必须在最前,粘贴的数字必须完全正确.
执行这些代码, Edit1只能输入数字,小数点和负号,负号和小数点只能输入一个,负号必须在最前,粘贴的数字必须完全正确. type TForm1 = class(TForm) Edit1: TEdit ...
- RabbitMQ Dead Lettering(死信)
死信,顾名思义,就是死掉的消息,死掉的消息是会被一般的队列丢弃的.如果这些消息很重要,而我们又需要,怎么办?凡事都有一个退路,现在就有一种方法可将这些死信消息存下来,那就是DLX(Dead Lette ...