本节书摘来自异步社区《Android安全技术揭秘与防范》一书中的第8章8.节什么是Hook技术,作者周圣韬,更多章节内容可以访问云栖社区“异步社区”公众号查看。
第8章 动态注入技术
Android安全技术揭秘与防范
我们在讨论动态注入技术的时候,APIHook的技术由来已久,在操作系统未能提供所需功能的情况下,利用APIHook的手段来实现某种必需的功能也算是一种不得已的办法。在Windows平台下开发电子词典的光标取词功能,这项功能就是利用Hook API的技术把系统的字符串输出函数替换成了电子词典中的函数,从而能得到屏幕上任何位置的字符串。无论是16位的Windows95,还是32位的Windws NT,都有办法向整个系统或特定的目标进程中“注入”DLL动态库,并替换掉其中的函数。
但是在Android上进行Hook需要跨进程操作,我们知道在Linux上的跨进程操作需要Root权限。所以目前Hook技术广泛地应用在安全类软件的主动防御上,所见到的Hook类病毒并不多。
Android系统在开发中会存在两种模式,一个是Linux的Native模式,而另一个则是建立在虚拟机上的Java模式。所以,我们在讨论Hook的时候,可想而知在Android平台上的Hook分为两种。一种是Java层级的Hook,另一种则是Native层级的Hook。两种模式下,我们通常能够通过使用JNI机制来进行调用。但我们知道,在Java中我们能够使用native关键字对C/C++代码进行调用,但是在C/C++中却很难调用Java中的代码。所以,我们能够在Java层级完成的事基本也不会在Native层去完成。
8.1 什么是Hook技术
还没有接触过Hook技术读者一定会对Hook一词感觉到特别的陌生,Hook英文翻译过来就是“钩子”的意思,那我们在什么时候使用这个“钩子”呢?我们知道,在Android操作系统中系统维护着自己的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件一样,并且能够在钩上事件时,处理一些自己特定的事件。较为形象的流程如图8-1所示。
Hook的这个本领,使它能够将自身的代码“融入”被勾住(Hook)的程序的进程中,成为目标进程的一个部分。我们也知道,在Android系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使我们希望通过一个程序改变其他程序的某些行为的想法不能直接实现,但是Hook的出现给我们开拓了解决此类问题的道路。当然,根据Hook对象与Hook后处理的事件方式不同,Hook还分为不同的种类,如消息Hook、API Hook等。
8.1.1 Hook原理
Hook技术无论对安全软件还是恶意软件都是十分关键的一项技术,其本质就是劫持函数调用。但是由于处于Linux用户态,每个进程都有自己独立的进程空间,所以必须先注入到所要Hook的进程空间,修改其内存中的进程代码,替换其过程表的符号地址。在Android中一般是通过ptrace函数附加进程,然后向远程进程注入so库,从而达到监控以及远程进程关键函数挂钩。
Hook技术的难点,并不在于Hook技术,初学者借助于资料“照葫芦画瓢”能够很容易就掌握Hook的基本使用方法。如何找到函数的入口点、替换函数,这就涉及了理解函数的连接与加载机制。
从Android的开发来说,Android系统本身就提供给了我们两种开发模式,基于Android SDK的Java语言开发,基于AndroidNDK的Native C/C++语言开发。所以,我们在讨论Hook的时候就必须在两个层面上来讨论。对于Native层来说Hook的难点其实是在理解ELF文件与学习ELF文件上,特别是对ELF文件不太了解的读者来说;对于Java层来说,Hook就需要了解虚拟机的特性与Java上反射的使用。
8.1.1.1 Hook工作流程
之前我们介绍过Hook的原理就是改变目标函数的指向,原理看起来并不复杂,但是实现起来却不是那么的简单。这里我们将问题细分为两个,一个是如何注入代码,另一个是如何注入动态链接库。
注入代码我们就需要解决两个问题。
需要注入的代码我们存放在哪里?
如何注入代码?
注入动态共享库我们也需要解决两个问题:
我们不能只在自己的进程载入动态链接库,如何使进程附着上目标进程?
如何让目标进程调用我们的动态链接库函数?
这里我也不卖关子了,说一下目前对上述问题的解决方案吧。对于进程附着,Android的内核中有一个函数叫ptrace,它能够动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等,它能够满足我们的需求。而Android中的另一个内核函数dlopen,能够以指定模式打开指定的动态链接库文件。对于程序的指向流程,我们可以调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则能够将我们希望注入的动态库注入至目标进程中。
对于代码的注入(Hook API),我们可以使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都需要通过目标进程的虚拟地址空间解析与ELF文件解析来完成,具体算法如下。
通过读取 /proc//maps文件找到链接库的基地址。
读取动态库,解析ELF文件,找到符号(需要对ELF文件格式的深入理解)。
计算目标函数的绝对地址。
目标进程函数绝对地址= 函数地址 + 动态库基地址
上面说了这么多,向目标进程中注入代码总结后的步骤分为以下几步。
(1)用ptrace函数attach上目标进程。
(2)发现装载共享库so函数。
(3)装载指定的.so。
(4)让目标进程的执行流程跳转到注入的代码执行。
(5)使用ptrace函数的detach释放目标进程。
对应的工作原理流程如图8-2所示。
https://yqfile.alicdn.com/b7f12bd9b13e8202d3e5547157ba0750ef5c268d.png" >
8.1.1.2 ptrace函数
说到了Hook我们就不能不说一下ptrace函数,ptrace提供了一种使父进程得以监视和控制其他进程的方式,它还能够改变子进程中的寄存器和内核映像,因而可以实现断点调试和系统调用的跟踪。使用ptrace,你可以在用户层拦截和修改系统调用(这个和Hook所要达到的目的类似),父进程还可以使子进程继续执行,并选择是否忽略引起终止的信号。
ptrace函数定义如下所示:
int ptrace(int request, int pid, int addr, int data);
request是请求ptrace执行的操作。
pid是目标进程的ID。
addr是目标进程的地址值。
data是作用的数据。
对于ptrace来说,它的第一个参数决定ptrace会执行什么操作。常用的有跟踪指定的进程(PTRACE_ATTACH)、结束跟踪指定进程(PTRACE_DETACH)等。详细的参数与使用方式如表8-1所示。
8.1.2 Hook的种类
我们所讨论的Hook,也就是平时我们所说的函数挂钩、函数注入、函数劫持等操作。针对Android操作系统,根据API Hook对应的API不一样我们可以分为使用Android SDK开发环境的Java API Hook与使用Android NDK开发环境的Native API Hook。而对于Android中so库文件的函数Hook,根据ELF文件的特性能分为Got表Hook、Sym表Hook以及inline Hook等。当然,根据Hook方式的应用范围我们在Android这样一个特殊的环境中还能分别出全局Hook与单个应用程序Hook。本节,我们就具体地说说这些Hook的原理以及这些Hook方式给我们使用Hook带来的便利性。
TIPS
对于Hook程序的运行环境不同,还可以分为用户级API Hook与内核级API Hook。用户级API Hook主要是针对在操作系统上为用户所提供的API函数方法进行重定向修改。而内核级API Hook则是针对Android内核Linux系统提供的内核驱动模式造成的函数重定向,多数是应用在Rootkit中。
8.1.2.1 Java层API Hook
通过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的。这里我们将此类操作称为Java API Hook。因为是根据Java中的发射机制来重定向函数的,那么很多Java中反射出现的问题也会在此出现,如无法反射调用关键字为native的方法函数(JNI实现的函数),基本类型的静态常量无法反射修改等。
8.1.2.2 Native层So库Hook
主要是针对使用NDK开发出来的so库文件的函数重定向,其中也包括对Android操作系统底层的Linux函数重定向,如使用so库文件(ELF格式文件)中的全局偏移表GOT表或符号表SYM表进行修改从而达到的函数重定向,我们有可以对其称为GOT Hook和SYM Hook。针对其中的inline函数(内联函数)的Hook称为inline Hook。
8.1.2.3 全局Hook
针对Hook的不同进程来说又可以分为全局Hook与单个应用程序进程Hook,我们知道在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会创建一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使每一个应用程序进程都有一个独立的Dalvik虚拟机实例。所以如果选择对Zygote进程Hook,则能够达到针对系统上所有的应用程序进程Hook,即一个全局Hook。对比效果如图8-3所示。
而对应的app_process正是zygote进程启动一个应用程序的入口,常见的Hook框架Xposed与Cydiasubstrate也是通过替换app_process来完成全局Hook的。
8.1.3 Hook的危害
API Hook技术是一种用于改变API执行结果的技术,能够将系统的API函数执行重定向。一个应用程序调用的函数方法被第三方 Hook 重定向后,其程序执行流程与执行结果是无法确认的,更别提程序的安全性了。而Hook技术的出现并不是为病毒和恶意程序服务的,Hook技术更多的是应用在安全管理软件上面。但是无论怎么说,已经被Hook后的应用程序,就毫无安全可言了。