关于Android x86平台 函数hook知识的一点记录

原创 2014年02月10日 16:49:49

对x86平台更熟悉一些,所以就写一下x86的吧,至于arm平台,只是寄存器有些不同,其他的思路应该还是差不多的。

其实主要是对了解的知识做一下记录,方便以后翻出来看看

 

Linux函数hook一般是使用ptrace函数,对指定进程使用ptrace,可以暂停进程,并可以读/写目标进程指定内存/寄存器,整个hook的流程其实很简单,

假设我们要替换运行进程P中的某个函数调用M_org,大致的思路如下:

1,生成一个动态库libInject.so,里面包含了用来替换M_org的函数M_replace;另外还有一个hook_work函数用来做替换操作;

2,获得目标进程P的进程号tPid;

3,分别获得加载到目标进程P中的dlopen,dlsym,dlclose,dlerror函数地址;

4,通过上一步获得的目标进程dlopen函数地址,在目标进程里加载libInject.so,并取得返回的so句柄;

5,调用dlsym获得目标进程中hook_work函数(刚才已经加载libInject.so了,这个函数位于这个模块里)地址,并调用它;

hook_work主要做以下工作:

w1,通过解析包含M_org函数的动态库,假设叫libM.so(如果这个函数直接位于目标程序里,则解析目标binary文件),找到libM.so在被加载模块中对应的GOT表相对于libM.so被加载位置偏移量;

w2,获得libM.so在目标进程P中加载位置,再加上一步获得的GOT偏移量,可获得libM.so的GOT表在目标进程中的地址;

w3,获得M_org的地址,并遍历第w2步获得的GOT表项,如果有一项对应的地址和M_org地址相同,则将这个表项里的地址替换成M_replace函数地址

 

整个步骤完成之后,以后每次调用M_org函数时,在GOT表项里找到的实际是M_replace函数地址,也就是说实际执行的是M_replace函数!

注意android的链接加载器和linux的链接加载器稍微有点不同,而正是这点不同之处可以使得w1~w3能正常工作:

标准Linux链接器是ld.so,支持Lazy绑定,引用模块A在真正开始调用被应用模块B中符号时,才将该符号地址写到A中于B对应的GOT表项中,也就是说,模块A在编译期间生成的调用模块B的原始代码,流程是从调用代码到PLT表到链接器。运行期第一次调模块乙时,首先进入链接器,链接器根据调用信息加载模块B搜寻其符号并将找到的函数地址填入GOT表,之后的后续调用流程就直接走PLT/GOT表了。这种机制能减少加载时的开销。

Android虽然内核基于Linux,但其动态链接机制却不是ld.so而是自带的linker,不支持Lazy绑定。也就是说,上述模块AB如果在Android平台上,则是模块A加载时,linker就会根据模块A中的.rel.plt表和字符串表中的内容加载模块B并搜索其所需函数地址并预先填入GOT表。之后调用流程每次都直接走PLT/GOT表,不再进linker,PLT表中也省去了跳至linker的代码。

 

2-5是运行在我们自己的进程中,那它是怎样实现获取目标进程地址信息以及对目标进程里函数的调用呢?这里就是用到了ptrace,也就是先attach一个进程,使其暂停执行,然后利用ptrace函数对目标进程做操作;如果attach我们自己进程的子进程,则不需要root权限,否则,需要root权限,android上的hook涉及到的attach操作是需要root权限的;

我们具体看看2~5部是怎么实现的:

detail-2: 获取目标进程pid很容易,因为我们一般都知道目标进程的进程名,只需要轮询所有/proc/pids/下的cmdline就可以了,找到匹配的进程名,就可以的对应的进程id

detail-3:获得目标进程里的指定函数地址:

a,先调用ptrace(PTRACE_ATTACH, pid, NULL, 0)暂停目标进程,注意,最好在这之后调用waitpid(pid, &status , WUNTRACED) 确保目标进程真正暂停了;

b,使用ptrace(PTRACE_GETREGS, pid, NULL, regs)获得目标进程寄存器内容,最好将原始寄存器内容保存下来以便恢复目标进程状态;

c,获得当前进程以及目标进程中libc.so的加载地址,假设分别为local_addr_module, remote_addr_module,一般被引用so在引用进程中的加载首地址可以通过搜索/proc/pid/maps文件来获取;

d,因为模块里的函数地址相对模块的加载地址偏移量是固定的,那么目标进程中指定函数地址为local_addr_function+remote_addr_module-loca_addr_module;

detail-4:在当前进程里实现目标进程dlopen调用(也适用其他任意函数调用)

a, 某些函数调用可能需要用到一块内存,比如说dlopen函数原型

void * dlopen( const char * pathname, int mode);

第一个参数是要加载的库路径

因此必须想办法在目标进程里申请一块内存,并将libInject.so完整路径字符串写到这块内存上。

在目标进程里申请一块内存,可以通过mmap函数来申请,目标进程里mmap函数首地址可以通过detail-3描述的方法获取;

void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);

mmap可以用来做文件映射(即申请一块内存,将文件映射到此内存,操作内存相当于操作文件)或匿名映射,匿名映射即将flags置为MAP_ANONYMOUS,此时忽略文件,只是申请一块内存,start为指定映射内存起始位置,设为0的话,则系统帮忙设定,返回的是系统分配的内存起始位置;

b,写入参数/调整寄存器状态/让目标进程继续运行,这里有几个细节需要注意的:

函数调用栈标准c规范是:从右到左将参数压倒当前栈上,由于栈的增长方向是由高到低,因此,第一个参数的地址是小于第二个参数地址的,以此类推;

如果使用push指令将参数推到栈上,则不用手动调整esp(指向当前栈顶)大小,但如果使用mov指令或直接使用内存拷贝将参数拷贝到对应栈位置,则需要先调整esp大小,

在这里我们使用ptrace(PTRACE_POKETEXT, pid, dest, d.val) 将参数拷贝到目标进程当前栈上,那么我们需要手动调整esp大小,

注意:因为参数数组里设置参数值是按从左到右来的,而且内存拷贝是从低地址到高地址,所以dest的起始位置应该是reg->esp-sizeof(参数),这样才能起到和push参数一样的效果,并且不会覆盖之前的栈的数据。

使用ptrace(PTRACE_POKETEXT...)拷贝数据时,最好四字节四字节写,而且要考虑到如果最后剩下的没有四字节的话,写进的数据不能覆盖之前栈的数据,下面是一个writedata的代码片段(取自网络上的一段代码)

int ptrace_writedata(pid_t pid, uint8_t *dest, uint8_t *data, size_t size)
{
    uint32_t i, j, remain;
    uint8_t *laddr;

    union u {
        long val;
        char chars[sizeof(long)];
    } d;

    j = size / 4;
    remain = size % 4;

    laddr = data;

    for (i = 0; i < j; i ++) {
        memcpy(d.chars, laddr, 4);
        ptrace(PTRACE_POKETEXT, pid, dest, d.val);

        dest  += 4;
        laddr += 4;
    }

    if (remain > 0) {
        d.val = ptrace(PTRACE_PEEKTEXT, pid, dest, 0); //先把原有数据取出来,保证不覆盖原有数据
        for (i = 0; i < remain; i ++) {
            d.chars[i] = *laddr ++;
        }

        ptrace(PTRACE_POKETEXT, pid, dest, d.val);
    }

    return 0;
}

还需要调整eip内容到需要执行函数的地址,也就是说我们需要调整pt_regs->esp和pt_regs->eip值

然后调用ptrace(PTRACE_SETREGS, pid, NULL, regs) 将寄存器状态修改应用到目标进程中;

接下来要做的就是让目标进程继续运行

int stat = 0;
    waitpid(pid, &stat, WUNTRACED);
    while (WIFSTOPPED(stat)==0) {
        if (ptrace_continue(pid) == -1) {
            printf("error\n");
            return -1;
        }
        waitpid(pid, &stat, WUNTRACED);
    }

waitpid会因三种原因返回,正常结束,暂停,信号中断,WIFSTOPPED判断是不是因为暂停而返回

到此就完成了在目标进程中执行指定函数的任务!

补充一点,我们看一下dlopen参数到底怎么传:

我们之前已经在目标进程中申请了一段内存,假设是map_addr, 接下来要做的将libInjector.so完整路径写到这块内存,这个很简单,直接使用上面给出的ptrace_writedata函数就可以了,之后我们定义一个long类型的parameters数组,赋值如下:

  parameters[0] = map_base;
  parameters[1] = RTLD_NOW| RTLD_GLOBAL;

然后就可以根据上面介绍的知识点去目标进程里调用dlopen方法了(调整寄存器值,写参数,让目标进程继续运行)

c,最后是要获取dlopen返回的so句柄:

让进程继续运行并且再一次暂停之后,我们通过ptrace(PTRACE_GETREGS, pid, NULL, regs)获取目标寄存器当前状态,在x86平台上,regs->eax里保存即是当前函数调用的返回值,在这里也就是so句柄。

detail-5: 让目标进程调用dlsys获取hook_work函数地址并让目标进程执行这个函数,这个过程涉及到的就是函数调用了,上面已经描述过了,过程一模一样,就不详细描述了。

 

还有一点需要介绍的是:怎样替换被hook函数的函数指针:

进程每一个依赖的模块在进程内存空间里都对应有一个GOT表,用来定位该模块导出符号(全局变量或函数)的地址

当一个进程依赖某个模块(动态库),而且需要调用这个模块中的某个函数时,进程会找到这个模块对应的GOT表,然后从GOT表里取到该函数的地址表项,然后call method_addr 即可,也就是说,我们只需要把被hook函数在GOT表项里的内容替换成我们自己的函数地址,那么,以后进程每次调用这个函数时,实际上找到是我们自己函数地址,也就是调用了hook函数。

那么怎么定位被hook函数的GOT表项呢,根据android linux链接器特征,这些表项内容实际在动态库加载完毕之后,就已经被填充好了(标准linux使用的是Lazy加载,也就是说动态库加载之后,这些GOT表项内容为空),因此只需要定位到GOT表的位置即可(定位到GOT表位置后,我们可以遍历GOT表里的所有表项内容,与被hook函数的函数地址比较,如果相同,则对应GOT表项即对应了被hook函数,然后把这个GOT表项里的内容替换成hook函数地址)。

如何定位到对应动态库的GOT表位置,这个信息其实是由两部组成,GOT_position=module_load_address + GOT_offset_in_module:

模块的加载地址很容易获得(/proc/pid/maps),GOT表相对于模块的偏移信息实际是保存在模块的binary文件中,即只需解析这个动态库的Elf文件即可:

在模块的ELF里搜索.plt.got或got section, section里的sh_addr即为偏移信息,sh_size为GOT表的大小。

相关知识点还有点模糊,待查更详尽的资料之后再描述GOT表位置这个概念

 

最后,利用这一套hook机制我们可以hook任何敏感函数的调用,通过和用户的交互,达到保护用户隐私信息的目的,LBE的实现原理应该和这个差不多;

另外有一个开源android 函数hook工具,Xposed,他是通过在app_process里添加逻辑,加载他自己的XposeBrige来实现动态hook逻辑,他需要在机子启动的时候用自己的app_process替换原有的app_process程序;

在Xposed中,当你需要hook一个方法时,你只需要在java层创建一个hook类,调用finaAndHookMethod来注册你需要hook的method,它提供了beforeHookMethod和afterHookMethod回调函数让用户做一些具体的hook逻辑。

Xposed的主要思想是改变dalvik中对被hook方法的定义,他将该方法的类型改变为native,并且将这个方法的实现链接到它本地的通用方法中,在这个通用方法中,它会根据传入的方法名找到对应的hook实例,在native层实现java层的代码调用(包括java层定义的beforeHookMethod,afterHookMethod以及被hook方法的原本实现)。

另外一个开源的基于android的hook框架Dynamic Dalvik Instrumentation和Xposed实现机制比较像。

 

Hook android系统调用研究(一)

一、Android内核源码的编译环境 系统环境:Ubuntu 14.04 Android系统版本:Android 4.4.4 r1 Android内核版本:android-msm-hammerhead...
  • QQ1084283172
  • QQ1084283172
  • 2017年02月20日 18:43
  • 5538

Xposed框架之函数Hook学习

作者:Fly2015 Xposed是Android下Java层的开源Hook框架类似的有cydiasubstrate框架并且据说cydiasubstrate框架能实现Android的Java层和Nat...
  • QQ1084283172
  • QQ1084283172
  • 2015年07月06日 16:54
  • 8533

Android x86源码架构

Android 4.4 |——abi                             应用程序二进制接口 |——art                             ...
  • mcskyding
  • mcskyding
  • 2016年04月17日 10:59
  • 1155

Android Hook框架adbi的分析(3)---编译和inline Hook实践

本文博客地址:http://blog.csdn.net/qq1084283172/article/details/75200800一、序言在前面的博客中,已经分析过了Android Hook框架adb...
  • QQ1084283172
  • QQ1084283172
  • 2017年07月16日 23:30
  • 1821

[强烈推荐]X86平台操作系统概览

UNIX家族及类UNIX系统1969年,在AT&T的Bell Labs,Ken Thompson和Dennis Ritchie(他们曾是大型操作系统Multics的两名开发者,Multics太庞大了最...
  • taker2001
  • taker2001
  • 2004年12月20日 02:47
  • 1418

hook模板x86/x64通用版(2)--中转函数的shellcode编写

这个模板的思路是这样的: 1.破坏原地址的指令(至少5字节,此处如果含有跳转会报失败),写一个跳转,被破坏的指令迁移到别的地方; 2.跳转到中转函数,中转函数中会调用用户定义的功能函数; 3.执行原地...
  • yes2
  • yes2
  • 2016年01月27日 22:00
  • 659

linux arm和x86 inline hook技术

Suterusu Rootkit: Inline Kernel Function Hooking on x86 and ARM Posted on January 7, 2013 Tabl...
  • fanlilei
  • fanlilei
  • 2015年11月18日 16:43
  • 1251

so注入(inject)和挂钩(hook) 以及同进程动态库so文件的函数hook方法介绍

so注入(inject)和挂钩(hook) - For both x86 and arm对于Android for arm上的so注入(inject)和挂钩(hook),网上已有牛人给出了代码-lib...
  • jinxinliu1
  • jinxinliu1
  • 2015年05月28日 11:12
  • 1406

一点关于Android事件处理的知识

Android中事件处理需要三个很重要的元素: 事件源(Event Source):事件发生的场所,通常就是组件等; 事件(Event):具体特定的事情,一次用户的操作,例如点击,滑动等; 事件...
  • carterjin
  • carterjin
  • 2012年05月07日 15:38
  • 665

我参与的一个x86平台项目的经历

今年是第一次完整参与一个项目的方方面面——当然,是站在开发人员的角度的,至于市场需求收集,采购元件生产,新品发布,销售拿钱,就不可能有我的份了。以前参与项目,都是只负责一点点东西,而且中后期完全处于边...
  • subfate
  • subfate
  • 2015年02月24日 13:27
  • 1072
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:关于Android x86平台 函数hook知识的一点记录
举报原因:
原因补充:

(最多只允许输入30个字)