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

参考资料:

原文地址:https://www.cnblogs.com/gm-201705/p/9864048.html

时间: 2024-11-07 19:03:35

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

使用cydia substrate 来进行android native hook

? cydia不仅可以hook java代码,同样可以hook native代码,下面举一个例子来进行android native hook 我是在网上找到的supermathhook这个项目,在他基础上修改的,本来是为了仓促应对阿里的ctf 这个项目位置: 这个项目是用来hook jni 代码的,而我是用来hook dvmDexFileOpenPartial这个函数的,所以必须使用 Mshookfunction这个函数,这个函数在libsubstrate.so中,自己去官网下载就可以了. 接下

Android Art Hook 技术方案

Android Art Hook 技术方案 by 低端码农 at 2015.4.13 www.im-boy.net 0x1 开始 Anddroid上的ART从5.0之后变成默认的选择,可见ART的重要性,目前关于Dalvik Hook方面研究的文章很多,但我在网上却找不到关于ART Hook相关的文章,甚至连鼎鼎大名的XPosed和Cydia Substrate到目前为止也不支持ART的Hook.当然我相信,技术方案他们肯定是的,估计卡在机型适配上的了. 既然网上找不到相关的资料,于是我决定自己

android 5 HOOK 技术研究之 ADBI 项目

简介 adbi 是一个android平台的二进制注入框架,源码开放在github上 :  ADBI 项目 ,从hook技术的分类来说,其属于so注入+inline hook, 这种方式的套路是:基于linux系统的ptrace机制,attach一个目标进程,注入一个动态链接库进入目标进程的地址空间,然后用so里边的函数地址替换目标进程地址空间里原有的函数地址(老的函数地址一般也需要保存起来). 源码目录 hijack:  可执行程序,用于注入一个so到目标进程 libbase:  注入库,提供h

android 5 HOOK 技术研究之 ADBI 项目 02

源码分析 hijack.c 这个文件实现了一个注入工具,可以向 -p 参数指定的进程注入一个so. 要实现这个效果,首先,需要得到目标进程若干函数如dlopen函数的地址,其次,需要能影响目标进程的正常执行流,让其中间某个时候执行dlopen加载指定的库,最后,还要能用动态加载的so里的函数覆盖原有内存里的函数. 下面开始研究,如何得到目标进程指定函数的地址,首先要得到的是dlopen函数的地址,adbi是这么做的: void *ldl = dlopen("libdl.so", RTL

Android逆向分析之Xposed的hook技术

Android逆向工程里常用到的工具除了的dex2jar,jd-gui,  Apktool之外还有一个Xposed. 这个工具是一个在不修改APK的情况下,影响其运行过程的服务框架.可以根据自己的需求编写模块,让模块控制目标应用的运行. 因为本人也是新手,对于Xposed用法还有很多的不熟悉,所以只对其hook技术进行简单的介绍,并让hook技术应用到以后的逆向分析工程中. 至于什么是hook,不了解的话就先去百度一下,这里基于菜鸟有限的经验,我只能说是一种函数拦截技术~ 首先,下载Xposed

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

前文介绍了导入表hook,现在来说下导出表的hook.导出表的hook的流程如下.1.获取动态库基值 1 void* get_module_base(pid_t pid, const char* module_name){ 2 FILE* fp; 3 long addr = 0; 4 char* pch; 5 char filename[32]; 6 char line[1024]; 7 8 // 格式化字符串得到 "/proc/pid/maps" 9 if(pid < 0){

九月份参加OPPO和腾讯Android面试:技术一面+二面+三面+HR四面,我的面经总结!

之前很多时候我是拒绝说我的面试经验的,因为我们简历经历不一样问的问题也会不一样,且大厂面试光靠背几个面试题就想过还是比较难的.因此在这里提醒一下大家不要临时抱佛脚,你花几天能背下的东西,别人花几天一定能超过你的,但我们花几年沉淀的东西,人家花几个月就未必能赶上,希望大家不飞则已,一飞冲天. OPPO 面试过程 boss 直聘 HR 推了简历给有关部门,简历通过后电话约的面试机会.当时约的是 10:30 的面试时间,所以只请了半天假. 第一轮是两个面试官轮流问问题,第一面大约面了一个小时,大约等了

Android代码混淆技术

Android混淆是Android开发者经常使用的一种用于防止被反编译的常见手法.Android开发基于java语言的,很容易被别人反编译出来,一下就相当于裸奔了,特别是用于商业用途的时候,防止反编译是必要的措施.而Android混淆的确可以保证Android源代码的一定安全. Android混淆技术 Java类名.方法名混淆 Dalvik字节码包含了大量的调试信息,如类名.方法名.字段名.参数名.变量名等,使用反编译工具可以还原这些信息.由于类名.方法名等通常都会遵循一定的命名规范,破解者很容

Android Native jni 编程入门

在某些情况下,java编程已经不能满足我们的需要,比如一个复杂的算法处理,这时候就需要用到jni(java native interface)技术: jni 其实就是java和c/cpp之间进行通信的一个接口规范,java可以调用c/cpp里面的函数,同样,c/cpp也可以调用java类的方法: jni开发工具ndk的安装:在最新的ndk版本中,安装ndk很简单,只需要装ndk的路径配置到系统环境变量中即可:在编译的时候,进入工程根目录:执行命令  ndk-build  即可完成编译: 下面就通