• Android Hook框架adbi源码浅析(一)


    adbi(The Android Dynamic Binary Instrumentation Toolkit)是一个Android平台通用hook框架,基于动态库注入与inline hook技术实现。
    该框架由两个主要模块构成,1.hijack负责将动态库注入到目标进程;2.libbase提供动态库本身,它实现了通用的hook功能。

    而example则是一个使用adbi进行epoll_wait hook的demo。

    zangzy@android-PC:~/Android/adbi-master$ tree
    .
    ├── build.sh
    ├── clean.sh
    ├── hijack
    │   ├── hijack.c
    │   └── jni
    │       └── Android.mk
    ├── instruments
    │   ├── base
    │   │   ├── base.c
    │   │   ├── base.h
    │   │   ├── hook.c
    │   │   ├── hook.h
    │   │   ├── jni
    │   │   │   ├── Android.mk
    │   │   │   └── Application.mk
    │   │   ├── util.c
    │   │   └── util.h
    │   └── example
    │       ├── epoll_arm.c
    │       ├── epoll.c
    │       └── jni
    │           └── Android.mk
    └── README.md
    
    7 directories, 16 files
    zangzy@android-PC:~/Android/adbi-master$

    一、hijack

    hijack实现进程注入功能,通过在目标进程插入dlopen()调用序列,加载指定SO动态库文件。要实现这个功能,主要做两件事情:1.获得目标进程中dlopen()地址;2.在目标进程的栈空间上构造一处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);
    //printf("own linker: 0x%x
    ", lkaddr);
    //printf("offset %x
    ", dlopenaddr - lkaddr);
    find_linker(pid, &lkaddr2);
    //printf("tgt linker: %x
    ", lkaddr2);
    //printf("tgt dlopen : %x
    ", lkaddr2 + (dlopenaddr - lkaddr));
    dlopenaddr = lkaddr2 + (dlopenaddr - lkaddr);

    首先调用 void *ldl = dlopen(“libdl.so”, RTLD_LAZY); 返回动态库libdl.so地址,我们的目标函数dlopen()就在这个库中实现。但是libdl.so是动态加载的,在每个进程中地址并不固定。看一下adbi如何解决这个问题:

    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
    ");
            return -1;
        }
        if (0 > find_linker_mem(libc, sizeof(libc), &libcaddr, mm, nmm)) {
            printf("cannot find libc
    ");
            return -1;
        }
     
        *addr = libcaddr;
     
        return 1;
    }

    主要调用了load_memmap和find_linker_mem两个函数。
    首先分析load_memmap函数,这个函数分3个步骤:

    static int load_memmap(pid_t pid, struct mm *mm, int *nmmp)
    {
        char raw[80000]; // increase this if needed for larger "maps"
        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
    ", raw);
            return -1;
    }

    (1)首先通过/proc//maps读取目标进程的内存映射信息,其格式大致如下:

    2a002000-2a003000 r–p 00001000 1f:00 933 /system/bin/app_process

    2a003000-2a1df000 rw-p 2a003000 00:00 0 [heap]

    40000000-4000f000 r-xp 00000000 1f:00 984 /system/bin/linker

    接下来一行行读取文件内容并解析:

    /* (2)读文件内容 */
        /* 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
    ");
                return -1;
            }
        }
        close(fd);
    /* (3)解析之 */
    p = strtok(raw, "
    ");
        m = mm;
        while (p) {
            /* parse current map line */
            rv = sscanf(p, "%08lx-%08lx %*s %*s %*s %*s %s
    ",
                    &start, &end, name); /* 分割每行内容 */
     
            p = strtok(NULL, "
    ");
     
            if (rv == 2) {
                m = &mm[nmm++];
                m->start = start;
                m->end = end;
                strcpy(m->name, MEMORY_ONLY); /* 40012000-40014000 r–p 40012000 00:00 0为空的情况 */
                continue;
            }
     
            /* 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()功能:

    static int
    find_linker_mem(char *name, int len, unsigned long *start,
          struct mm *mm, int nmm)
    {
        int i;
        struct mm *m;
        char *p;
        for (i = 0, m = mm; i < nmm; i++, m++) {         
            //printf("name = %s
    ", m->name);
            //printf("start = %x
    ", m->start);
            if (!strcmp(m->name, MEMORY_ONLY))
                continue;
            p = strrchr(m->name, '/');
            if (!p)
                continue;
            p++;
            if (strncmp("linker", p, 6))
                continue;
            break; //  'libc.so' or 'libc-[0-9]' */
            if (!strncmp(".so", p, 3) || (p[0] == '-' && isdigit(p[1])))
                break;
        } /* 获取/system/bin/linker加载地址 */
        if (i >= nmm)
            /* not found */
            return -1;
     
        *start = m->start;
        strncpy(name, m->name, len);
        if (strlen(m->name) >= len)
            name[len-1] = '';
        return 0;
    }

    这段代码的作用是获取/system/bin/linker在目标进程的加载地址。

    linker是android提供的动态链接器,被各进程间共用。dlopen()函数就是在linker里面定义,所以其内部的dlopen()函数相对于linker头的偏移量是固定的,这样计算其它进程内dlopen()函数的地址就非常简单了,先在本进程内计算出dlopen()相对于linker头的偏移量,再加上目标进程中linker的加载地址。

    而linker的加载地址,就是上面通过/proc/<?>/maps读到的40000000-4000f000 r-xp 00000000 1f:00 984 /system/bin/linker开始地址。

    2.在目标进程的栈空间上构造一处dlopen()调用

    要修改目标进程寄存器等信息,需使用到ptrace()函数,gdb等程序拥有查看、修改调试进程寄存器等的能力就是因为使用了ptrace()。

    首先将hijack attach到目标进程上去:

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

    这时目标进程暂停,就可以通过ptrace对其进行修改了,如获取寄存器值:

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

    接下来要做的就是修改寄存器的值,在目标进程的栈空间上构造一处dlopen()调用,关键在于一个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, #16]   ; 30 <.text+0x30>
    0xe59f1010, //        ldr     r1, [pc, #16]   ; 34 <.text+0x34>
    0xe59f2010, //        ldr     r2, [pc, #16]   ; 38 <.text+0x38>
    0xe59f3010, //        ldr     r3, [pc, #16]   ; 3c <.text+0x3c>
    0xe59fe010, //        ldr     lr, [pc, #16]   ; 40 <.text+0x40>
    0xe59ff010, //        ldr     pc, [pc, #16]   ; 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
    };

    可以发现,这里使用了上文获取到的寄存器值,初始化了部分数组元素:

    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实现了什么样的功能。

    1.首先指令从2处开始执行,ldr r0,[pc,#64] 将pc+64指向地址的内容存入r0寄存器,即图中libaddr(.so地址)项,对其取值则r0指向.SO库路径名字符串。(说明:对ARM指令集而言,PC总是指向当前指令的下两条指令的地址,即PC的值为当前指令的地址值加8个字节。所以[pc,#64]指向第(64+8)/4=18个元素处)

    2.mov r1,#0 将0赋值给r1寄存器。

    3.ldr pc,[pc,#56] 调用dlopen()函数,第一个入参为r0:so库路径名字符串,第二个参数为r1:0。

    4.函数执行完后,通过设置PC回到1处继续执行,依次恢复pc/sp/r0/r1/r2/r3寄存器。

    下面就可以将我们精心构造好的shellcode写入到目标进程栈空间上:

    // write library name to stack
    if (0 > write_mem(pid, (unsigned long*)arg, n, libaddr)) {
        printf("cannot write library name (%s) to stack, error!
    ", arg);
        exit(1);
    }
     
    // write code to stack
    codeaddr = regs.ARM_sp - sizeof(sc);
    if (0 > write_mem(pid, (unsigned long*)&amp;amp;amp;sc, sizeof(sc)/sizeof(long), codeaddr)) {
        printf("cannot write code, error!
    ");
        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并不能执行,因为当前linux都开启了栈执行保护的功能。可以查看栈属性进行印证,没有x位:
    beeaf000-beec4000 rw-p befeb000 00:00 0 [stack]

    但我们可以通过mprotect()函数,来修改栈内存的可执行权限:

    // calc stack pointer
    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
    ", regs.ARM_r0);
    regs.ARM_r1 = stack_end - stack_start; // stack size
    //printf("mprotect(%x, %d, ALL)
    ", 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
    ");
        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'
    }

    这段代码首先计算栈顶位置,接着将 栈起始地址/栈大小/权限位 3个参数压栈,然后调用mprotect()设置代码所在栈区的可执行权限,最后将lr寄存器设置为栈上代码的起始地址,这样当调用mprotect()函数返回后就可以正常执行栈上代码了。

    最后,恢复目标进程的寄存器值,并恢复被ptrace()暂停的进程:

    // detach and continue
    ptrace(PTRACE_SETREGS, pid, 0, &amp;amp;amp;regs);
    ptrace(PTRACE_DETACH, pid, 0, (void *)SIGCONT);
     
    if (debug)
        printf("library injection completed!
    ");

    到目前为止,我们已经能够在指定进程加载任意SO库了!

  • 相关阅读:
    猴面包树果 baobab tree
    关于 韩国 申明 豆浆 和 端午 是其国家创造或历史的 看法
    初中英语课本里隐藏着的惊人秘密(转载)
    如果不出意外,我每周都会去工大打球
    新开始做wpf,随便写点经验
    当你老了 叶芝
    继承Form中的DevExpress控件不能打开编辑器Designer
    骑 自行车 从公司 到家
    LJP Little John PalmOS 1.0 Release 最新版 (RC9后的正式版)
    我的语文备忘
  • 原文地址:https://www.cnblogs.com/gm-201705/p/9901538.html
Copyright © 2020-2023  润新知