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

源码分析

hijack.c

这个文件实现了一个注入工具,可以向 -p 参数指定的进程注入一个so。

要实现这个效果,首先,需要得到目标进程若干函数如dlopen函数的地址,其次,需要能影响目标进程的正常执行流,让其中间某个时候执行dlopen加载指定的库,最后,还要能用动态加载的so里的函数覆盖原有内存里的函数。

下面开始研究,如何得到目标进程指定函数的地址,首先要得到的是dlopen函数的地址,adbi是这么做的:

    void *ldl = dlopen("libdl.so", RTLD_LAZY);
    if (ldl) {
        dlopenaddr = (unsigned long)dlsym(ldl, "dlopen");//dlopenaddr 存放本进程的dlopen函数地址
        dlclose(ldl);
    }
    unsigned long int lkaddr;
    unsigned long int lkaddr2;
    find_linker(getpid(), &lkaddr);
    find_linker(pid, &lkaddr2);
    dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr); // dlopenaddr 存放目标进程的dlopen函数的地址

上述代码是为了得到目标进程的dlopen函数地址。

首先,dlopen加载libdl.so,由于进程启动后libdl.so肯定会先加载好,所以这里返回已经加载好的libdl.so映射在本进程的起始地址空间,然后调用dlsym返回本进程的dlopen函数地址。

接着,find_linker函数利用 /proc/pid/maps 文件可以得到进程pid的地址空间进而得到libdl.so映射到内存的起始地址,其中,注入进程的libdl.so映射的初始地址是 lkaddr, 目标进程是lkaddr2

最后,再利用dlopen函数在libdl.so动态库的代码的偏移是固定的(注入进程和被注入进程使用的是同一个libdl.so),dlopenaddr - lkaddr 先算出这个偏移值,lkaddr2 再上上述偏移值即得到目标进程的 dlopen 函数的地址

maps文件在linux和android上的地址块命名有些区别,一般linux上libdl.so映射的地址是这样的

 7f6a96672000-7f6a96695000 r-xp 00000000 08:01 397502                  /lib/x86_64-linux-gnu/ld-2.19.so

android 里的命名叫 linker

find_linker 函数调用了 load_memmap函数和 find_linker_mem函数,

static int find_linker(pid_t pid, unsigned long *addr)
{
    struct mm mm[1000];
    unsigned long libcaddr;
    int nmm;
    char libc[256];
    symtab_t s;

    if (0 > load_memmap(pid, mm, &nmm)) {
        printf("cannot read memory map\n");
        return -1;
    }
    if (0 > find_linker_mem(libc, sizeof(libc), &libcaddr, mm, nmm)) {
        printf("cannot find libc\n");
        return -1;
    }

    *addr = libcaddr;

    return 1;
}

load_memmap 函数基本流程:打开maps文件,按照maps文件的格式解析成一个数组,每一项存放一个动态库的名称以及其映射到内存里的起始和结束地址

static int
load_memmap(pid_t pid, struct mm *mm, int *nmmp)
{
    char raw[80000]; // this depends on the number of libraries an executable uses
    char name[MAX_NAME_LEN];
    char *p;
    unsigned long start, end;
    struct mm *m;
    int nmm = 0;
    int fd, rv;
    int i;

    sprintf(raw, "/proc/%d/maps", pid);
    fd = open(raw, O_RDONLY);
    if (0 > fd) {
        printf("Can‘t open %s for reading\n", raw);
        return -1;
    }

    /* Zero to ensure data is null terminated */
    memset(raw, 0, sizeof(raw));

    p = raw;
    while (1) {
        rv = read(fd, p, sizeof(raw)-(p-raw));
        if (0 > rv) {
            //perror("read");
            return -1;
        }
        if (0 == rv)
            break;
        p += rv;
        if (p-raw >= sizeof(raw)) {
            printf("Too many memory mapping\n");
            return -1;
        }
    }
    close(fd);

    p = strtok(raw, "\n");
    m = mm;
    while (p) {
        /* parse current map line */
        rv = sscanf(p, "%08lx-%08lx %*s %*s %*s %*s %s\n",
                &start, &end, name);

        p = strtok(NULL, "\n");

        if (rv == 2) {
            m = &mm[nmm++];
            m->start = start;
            m->end = end;
            strcpy(m->name, MEMORY_ONLY);
            continue;
        }

        if (strstr(name, "stack") != 0) {
            stack_start = start;
            stack_end = end;
        }

        /* search backward for other mapping with same name */
        for (i = nmm-1; i >= 0; i--) {
            m = &mm[i];
            if (!strcmp(m->name, name))
                break;
        }

        if (i >= 0) {
            if (start < m->start)
                m->start = start;
            if (end > m->end)
                m->end = end;
        } else {
            /* new entry */
            m = &mm[nmm++];
            m->start = start;
            m->end = end;
            strcpy(m->name, name);
        }
    }

    *nmmp = nmm;
    return 0;
}

find_linker_mem函数的流程:遍历上述数组,根据动态库名称匹配,即可获取libdl.so对应的数组元素,从而得到libdl.so在进程内的起始和终止地址,代码这里就不贴了。

以上,是获取目标进程某个动态库内的函数在目标进程的真实地址的方法。那么目标进程,非动态库函数的地址怎么获取呢?

=== ==

接下去研究第二个问题,如何影响目标进程的执行流,这里必须介绍ptrace函数了。

ptrace

SYNOPSIS
       #include <sys/ptrace.h>

       long ptrace(enum __ptrace_request request, pid_t pid,
                   void *addr, void *data);

DESCRIPTION
       The  ptrace()  system  call  provides  a  means  by which one
       process (the "tracer") may observe and control the  execution
       of another process (the "tracee"), and examine and change the
       tracee‘s memory and  registers.   It  is  primarily  used  to
       implement breakpoint debugging and system call tracing.

动态库注入技术一般都依赖于ptrace机制,ptrace是linux kernel 为了支持应用层debug功能而实现的系统调用,这个系统调用提供了“让A进程关联到B进程,并动态修改B进程的内存和寄存器”的机制,由于A进程可以动态修改B进程的内存空间和寄存器的值,所有可以影响B进程的执行序列,比如在正常的执行序列里插入某段代码,让B进程加载一个动态库。

下面研究adbi是怎么使用ptrace达到目的的:

首先,attach到目标进程

  if (0 > ptrace(PTRACE_ATTACH, pid, 0, 0)) {
        printf("cannot attach to %d, error!\n", pid);
        exit(1);
    }
    waitpid(pid, NULL, 0);

其次,获取目标进程当前寄存器

ptrace(PTRACE_GETREGS, pid, 0, &regs);

接着,构造新的寄存器值,这一步是关键。

数组sc存放初始化的指令

unsigned int sc[] = {
0xe59f0040, //        ldr     r0, [pc, #64]   ; 48 <.text+0x48>
0xe3a01000, //        mov     r1, #0  ; 0x0
0xe1a0e00f, //        mov     lr, pc
0xe59ff038, //        ldr     pc, [pc, #56]   ; 4c <.text+0x4c>
0xe59fd02c, //        ldr     sp, [pc, #44]   ; 44 <.text+0x44>
0xe59f0010, //        ldr     r0, [pc, #20]   ; 30 <.text+0x30>
0xe59f1010, //        ldr     r1, [pc, #20]   ; 34 <.text+0x34>
0xe59f2010, //        ldr     r2, [pc, #20]   ; 38 <.text+0x38>
0xe59f3010, //        ldr     r3, [pc, #20]   ; 3c <.text+0x3c>
0xe59fe010, //        ldr     lr, [pc, #20]   ; 40 <.text+0x40>
0xe59ff010, //        ldr     pc, [pc, #20]   ; 44 <.text+0x44>
0xe1a00000, //        nop                     r0
0xe1a00000, //        nop                     r1
0xe1a00000, //        nop                     r2
0xe1a00000, //        nop                     r3
0xe1a00000, //        nop                     lr
0xe1a00000, //        nop                     pc
0xe1a00000, //        nop                     sp
0xe1a00000, //        nop                     addr of libname
0xe1a00000, //        nop                     dlopenaddr
};

下面使用ptrace获取的寄存器值填充到sc数组的11到17,

    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;

然后用前面获取到的目标进程的dlopen函数的地址填充到第19位置,18位置存放动态库的名字字符串的地址,然后调用wirte_mem函数将动态库的名字字符串写到libaddr地址指定的内存区。

  sc[19] = dlopenaddr;
        // push library name to stack
    libaddr = regs.ARM_sp - n*4 - sizeof(sc);
    sc[18] = libaddr;    

   // write library name to stack
    if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) {
        printf("cannot write library name (%s) to stack, error!\n", arg);
        exit(1);
    }

其中,n是这么算的,动态库(如 /data/local/tmp/libexample.so)的字节数+1,然后除以4,如果有余数,结果加1. 其实,得到的n就是以‘4字节’为单位的数值,这么算主要是 write_mem函数的实现,下面会看到。结合上面的代码,这个字符串会被写入 libaddr 对应的内存,这个内存地址是这么算的:

regs.ARM_sp - n*4 - sizeof(sc); 即原来的栈顶指针往低地址移动 “sc 数组大小+动态库字符串字节长度” 
            case ‘l‘:
                                n = strlen(optarg)+1;
                                n = n/4 + (n%4 ? 1 : 0);
                                arg = malloc(n*sizeof(unsigned long));
                                memcpy(arg, optarg, n*4);
                                break;

下面看write_mem是怎么将一块数据写入目标进程的内存地址的:

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;
}

pid是目标进程标识,buf是要写入目标进程内存的数据块,nlong是‘4字节’为单位的长度,pos是要写入的地址。 由于数据buf是 long 型数组,所以循环一次即写入4字节的数据。最终是调用 ptrace 函数,另第一个参数为  PTRACE_POKETEXT 实现写入的。

接下去,写入新的指令数据(即sc数组)到目标进程:

// write code to stack
    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);
    }// calc stack pointer

可以看到,方法跟上述写动态库名字字符串类似,要写入的目标地址 = 栈顶指针 - Sc 数组长度,然后调用write_mem函数将数组Sc写入

接下去,移动栈顶指针为新的栈顶(往低地址移动”sc数组长度+动态库名字字符串长度“),接下去,根据是否有 mprotect 调用,会有两种执行流:如果没有mprotect,则将PC寄存器的值变成sc数组开始的位置,即接下去直接执行Sc数组的指令。否则,pc寄存器的值设置为 mprotect 函数,然后将lr寄存器设置为sc数组。并将r0,r1,r2参数设置为 mprotect调用的参数,这样,首先执行 mprotect 函数将 r0,r1指定的内存范围设置为 r2指定的权限,在这个例子是,是将目标内存设置为 rwx, 执行完mprotect后再执行 sc 数组的指令。

    regs.ARM_sp = regs.ARM_sp - n*4 - sizeof(sc);

    // call mprotect() to make stack executable
    regs.ARM_r0 = stack_start; // want to make stack executable
    //printf("r0 %x\n", regs.ARM_r0);
    regs.ARM_r1 = stack_end - stack_start; // stack size
    //printf("mprotect(%x, %d, ALL)\n", regs.ARM_r0, regs.ARM_r1);
    regs.ARM_r2 = PROT_READ|PROT_WRITE|PROT_EXEC; // protections

    // normal mode, first call mprotect
    if (nomprotect == 0) {
        if (debug)
            printf("calling mprotect\n");
        regs.ARM_lr = codeaddr; // points to loading and fixing code
        regs.ARM_pc = mprotectaddr; // execute mprotect()
    }
    // no need to execute mprotect on old Android versions
    else {
        regs.ARM_pc = codeaddr; // just execute the ‘shellcode‘
    }

经过上述设置后,新的寄存器值如下图:

结合上面的图,新的指令流执行如下(下面转自”参考1“)

一点需要说明一下,对于ARM处理器来说,pc寄存器的值,指向的不是当前正在执行指令的地址,而是往下第二条指令的地址。

好,我们正式开始分析代码的含义,指令将从codeaddr指示的位置从低到高依次执行。

第一条指令将pc寄存器的值加上64,读出那个地方的内容(4个字节),然后放到寄存器r0中。刚才说过了,pc寄存器值指向的是当前指令位置加8个字节,也就是说这条指令实际读出的是当前指令位置向下72个字节。由于sc数组是int型的,就是数组当前元素位置向下18个元素处。数一数,刚好是libaddr的位置。所以这条指令是为了让r0寄存器指向.so共享库路径名字符串。

第二条指令很简单,是将0赋值给寄存器r1。

第三条指令用来将pc寄存器值保存到lr寄存器中,这样做的目的是为了调用dlopen()函数返回后,跳转到指令“ldr sp, [pc, #56]”处。

第四条指令是将pc加上56处的数值加载到pc中,pc+56处是哪?当前指令位置往下64字节,16个元素,刚好是dlopen()函数的调用地址。所以,这条指令其实就是调用dlopen()函数,传入的参数一个是r0寄存器指向的共享库路径名,另一个是r1寄存器中的0。

调用dlopen()返回后将继续执行下面的所有指令,我就不一一分析了,作用就是恢复目标进程原来寄存器的值。先是sp,然后是r0、r1、r2、r3和lr,最后恢复原来pc的值,继续执行被暂停之前的指令,就像什么都没发生过一样。

=====

最后,使用ptrace设置新的寄存器值进入目标内存,hook开始生效

      ptrace(PTRACE_SETREGS, pid, 0, &regs);
        ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);

最后,如果参数有 -s ,还会执行下述流程:

    if (appname) {
        if (ptrace(PTRACE_SETOPTIONS, pid, (void*)1, (void*)(PTRACE_O_TRACEFORK))) {
            printf("FATAL ERROR: ptrace(PTRACE_SETOPTIONS, ...)");
            return -1;
        }
        ptrace(PTRACE_CONT, pid, (void*)1, 0);

        int t;
        int stat;
        int child_pid = 0;
        for (;;) {
            t = waitpid(-1, &stat, __WALL|WUNTRACED);

            if (t != 0 && t == child_pid) {char fname[256];
                sprintf(fname, "/proc/%d/cmdline", child_pid);
                int fp = open(fname, O_RDONLY);
                if (fp < 0) {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
                read(fp, fname, sizeof(fname));
                close(fp);

                if (strcmp(fname, appname) == 0) {
                   // detach from zygote
                    ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);

                    // now perform on new process
                    pid = child_pid;
                    break;
                }
                else {
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                    continue;
                }
            }

            if (WIFSTOPPED(stat) && (WSTOPSIG(stat) == SIGTRAP)) {
                if ((stat >> 16) & PTRACE_EVENT_FORK) {
                    if (debug > 1)
                        printf("fork\n");
                    int b = t; // save parent pid
                    ptrace(PTRACE_GETEVENTMSG, t, 0, &child_pid);
                    t = child_pid;
                    ptrace(PTRACE_CONT, b, (void*)1, 0);
                    ptrace(PTRACE_SYSCALL, child_pid, 0, 0);
                }
            }
        }
    }

    if (zygote) {
        int i = 0;
        for (i = 0; i < zygote; i++) {
            // -- zygote fix ---
            // we have to wait until the syscall is completed, IMPORTANT!
            ptrace(PTRACE_SYSCALL, pid, 0, 0);
            if (debug > 1)
                printf("/");
            waitpid(pid, NULL, 0);

            ptrace(PTRACE_GETREGS, pid, 0, &regs);
            if (regs.ARM_ip != 0) {
                if (debug > 1)
                    printf("not a syscall entry, wait for entry\n");
                ptrace(PTRACE_SYSCALL, pid, 0, 0);
                waitpid(pid, NULL, 0);
            }

            ptrace(PTRACE_SYSCALL, pid, 0, 0);
            if (debug > 1)
                printf("\\");
            waitpid(pid, NULL, 0);

        }
    }

参考

http://blog.csdn.net/roland_sun/article/details/34109569

时间: 2024-10-28 14:22:58

android 5 HOOK 技术研究之 ADBI 项目 02的相关文章

android 5 HOOK 技术研究之 ADBI 项目

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

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 Native Hook技术(一)

原理分析 ADBI是一个著名的安卓平台hook框架,基于 动态库注入 与 inline hook 技术实现.该框架主要由2个模块构成:1)hijack负责将so注入到目标进程空间,2)libbase是注入的so本身,提供了inline hook能力. 源码目录中的example则是一个使用ADBI进行hook epoll_wait的示例. hijack hijack实现动态库注入功能,通过在目标进程插入dlopen()调用序列,加载指定so文件.要实现这个功能,主要做两件事情: 获得目标进程中d

Android推送技术研究

前言 近期研究Android推送的实现, 研究了两天一夜, 有了一点收获, 写下来既为了分享, 也为了吐槽. 须要说明的是有些东西偏底层硬件和通信行业, 我对这些一窍不通, 仅仅能说说自己的理解. 为什么要研究Android推送技术? 主要还是毕业设计要做一个即时通信app, 我是不喜欢做什么社交app的, 也就象牙塔里的人想得出来, 说实话有这功夫还不如钻研一个小技术点, 把一个点研究透彻, 比搞个大而全, 还没用的东西好得多, 只是谁叫咱们是普通人, 没得选呢. Android推送服务的几种

android瓦片地图技术研究

最近根据公司项目需求,需要制作场馆的室内图并且实现根据rfid信号的自动定位功能,研究了好久找到了一个目前为止还算好用的瓦片地图工具——TileView. github连接:https://github.com/moagrius/TileView Gradle: compile 'com.qozix:tileview:2.0.1' 这个控件的功能非常强大,你可以根据需求制作任意尺寸大小的地图,可以添加marker标记,绘制路线,定位等等,如果你需要展示超大图片,或制作自定义地图应用这是你的不二选

基于复杂网络隐藏度量空间的互联网自适应可扩展路由理论与关键 技术研究

随着信息技术的迅速发展与互联网创新应用的不断涌现,互联网体系结构所对应的基本能力与当今人们对互联网规模.功能.性能.服务和安全等方面的需求所对应的能力要求产生了明显矛盾.构建规模更大,性能更高和可扩展的下一代互联网体系结构是解决这一矛盾的主要途径.在此背景下,需重新反思互联网信息传递.转发.路由.拥塞控 制等基本问题.本项目重点研究互联网路由系统规模可扩展性问题,其主要内容包括:互联网拓扑特征及其演化模型研究,基于拓扑特征的可扩展路由模型及其优化方法与关键技术研究. 本项目已取得了如下一些结果:

Android开发技术前线 (android-tech-frontier) --优质技术文章的聚合项目

Android开发技术前线 ( android-tech-frontier ) Android开发技术前线一个定期翻译.发布国内外Android优质的技术.开源库.软件架构设计.测试等文章的开源项目,让我们的技术跟上国际步伐. 项目首页请猛击这里. 我们翻译的文章在能够联系到作者的情况下都会在获得作者授权后进行翻译,并且公开发布.发布的文章中都会保留原文链接.作者名,如有相关的版权协议我们也会一并附上.目前已经联系到的作者列表请参考授权文档; 已完成列表 2015.4.12 ( 第五期 ) 文章

Android官方技术文档翻译——迁移 Gradle 项目到1.0.0 版本

本文译自Android官方技术文档<Migrating Gradle Projects to version 1.0.0>,原文地址:http://tools.android.com/tech-docs/new-build-system/migrating-to-1-0-0. 本篇文档介绍的是低版本的Gradle项目怎么升级到1.0.0版本. 翻译不易,转载请注明CSDN博客上的出处: http://blog.csdn.net/maosidiaoxian/article/details/427

Android逆向分析之Xposed的hook技术

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