原理分析

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, &regs);

接下来要做的就是修改寄存器的值,在目标进程的栈空间上构造一处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, &regs);
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"
);
}

参考资料:

Android Native Hook技术(一)的更多相关文章

  1. Android Native Hook技术(二)

    Hook技术应用 已经介绍了安卓 Native hook 原理,这里介绍 hook 技术的应用,及 Cyida Substrate 框架. 分析某APP,发现其POST请求数据经过加密,我们希望还原其 ...

  2. 使用cydia substrate 来进行android native hook

      cydia不仅可以hook java代码,同样可以hook native代码,下面举一个例子来进行android native hook 我是在网上找到的supermathhook这个项目,在他基 ...

  3. 使用Cydia Substrate 从Native Hook Android Native世界

    同系列文章: 使用Cydia Substrate 从Native Hook Android Java世界 使用Cydia Substrate Hook Android Java世界 一.建立工程 手机 ...

  4. Android so注入(inject)和Hook技术学习(三)——Got表hook之导出表hook

    前文介绍了导入表hook,现在来说下导出表的hook.导出表的hook的流程如下.1.获取动态库基值 void* get_module_base(pid_t pid, const char* modu ...

  5. Hook技术

    hook钩子: 使用技术手段在运行时动态的将额外代码依附现进程,从而实现替换现有处理逻辑或插入额外功能的目的. 它的技术实现要点有两个: 1)如何注入代码(如何将额外代码依附于现有代码中). 2)如何 ...

  6. Android热修复技术原理详解(最新最全版本)

    本文框架 什么是热修复? 热修复框架分类 技术原理及特点 Tinker框架解析 各框架对比图 总结   通过阅读本文,你会对热修复技术有更深的认知,本文会列出各类框架的优缺点以及技术原理,文章末尾简单 ...

  7. Android Native 内存泄漏系统化解决方案

    导读:C++内存泄漏问题的分析.定位一直是Android平台上困扰开发人员的难题.因为地图渲染.导航等核心功能对性能要求很高,高德地图APP中存在大量的C++代码.解决这个问题对于产品质量尤为重要和关 ...

  8. Android热修复技术原理详解

    阿里Dexposed -- native解决方案 原理: 直接在native层进行方法的结构体信息对换,从而实现完美的方法新旧替换,从而实现热修复功能   他的思想完全来源于Xposed框架,完美诠释 ...

  9. 深入探索Android热修复技术原理读书笔记 —— 热修复技术介绍

    1.1 什么是热修复 对于广大的移动开发者而言,发版更新是最为寻常不过的事了.然而,如果你 发现刚发出去的包有紧急的BUG需要修复,那你就必须需要经过下面这样的流程: 这就是传统的更新流程,步骤十分繁 ...

随机推荐

  1. MSSQL2012中SQL调优(SQL TUNING)时CBO支持和常用的hints

    虽然当前各关系库CBO都已经非常先进和智能,但因为关系库理论和实现上的限制,CBO在特殊场景下也会给出次优甚至存在严重性能问题的执行计划,而这些场景中,有一部分只能或适合通过关系库提供的hints来进 ...

  2. Qt_自定义菜单

    一.右键菜单 右键菜单实现:通过重写contextMenuEvent(QContextMenuEvent *event)事件,QMenu+QAction即可完美实现! 重写voidcontextMen ...

  3. httpd.conf文件格式解析

    apache http server,俗称apache,程序名httpd,默认配置文件/etc/httpd/conf/httpd.conf:该文件通过其中的Include conf.d/*.conf指 ...

  4. python QMainWindow QWidget

    from PyQt5 import QtWidgetsfrom untitled import Ui_MainWindowfrom PyQt5.QtWidgets import QFileDialog ...

  5. python 利用turtle库绘制五角星

    # -*- coding: utf-8 –*-import turtleimport math def draw_polygon(aTurtle, size=50, n=3): for i in ra ...

  6. PAT-GPLT训练集 L2-001 紧急救援(最短路)

    PAT-GPLT训练集 L2-001 紧急救援 题目大意:求最短路的条数,最短路中的权重和的最大值和这条最短路的路线 分析:使用dijkstra算法求出最短路,并且对dijkstra算法进行变化,设起 ...

  7. Asp.Net 中 HTTP 和 HTTPS 切换

    Asp.Net 中 HTTP 和 HTTPS 切换   目的 HTTP,超文本传输协议,明文传输,无状态,服务器默认端口80 HTTPS,具有SSL加密的HTTP,加密传输,需要申请ca证书,服务器默 ...

  8. @ResponseBody中文乱码解决方案

    java web项目,使用了springmvc4.0,用@ResponseBody返回中文字符串,乱码$$ 本以为很简单的问题,不过也找了一个小时. 网上有说这样配置的: <mvc:annota ...

  9. 经典DFS问题实践

    八皇后问题: //八皇后问题 经典的DFS问题实践 #include<iostream> #include<cmath> #include<algorithm> # ...

  10. C++ Templates 关于程序库的概念和通用工具

    using namespace std所谓的命名空间,就是一种将程序库名称封装起来的方法,它就像在程序库中竖立了一道围墙 标准程序库中有一部分,比如string classes,支持具体的错误处理,它 ...