🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
Hook的目的是为了对目标进程函数的替换和注入,Hook的危害是巨大的,Hook后的应用程序毫无安全可言。其实,自从PC时代起,Hook与反Hook一直就是一个旷日持久的战争。那么对于刚发展不久的Android操作系统安全方向而言,Hook的检测与修复无疑是给Android安全研究人员带来了巨大的挑战。本节我们就具体地看看,就Android操作系统而言,如何检测一个进程是否被Hook了,如何修复被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所示。 :-: ![](https://box.kancloud.cn/6aff1e259a84d33f76682bb85f986f8b_601x106.png) 我们这里看见浏览器应用对应的进程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所示。 :-: ![](https://box.kancloud.cn/d89e8b076edbddc801f93abbb6194af2_577x79.png) 对应地输出了Dalvik虚拟机载入的非系统应用的dex文件,如图8-30所示。 :-: ![](https://box.kancloud.cn/c87862475e86ef8b70a1fc9692d259a9_618x158.png) 在图 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所示。 :-: ![](https://box.kancloud.cn/c1bc23438e4abdef883afc5e9952d556_599x107.png) 得到其对应的进程pid为15097,我们直接查看15097进程中的虚拟地址空间加载了哪些第三方的库文件,即过滤处/data/data目录中的,具体命令如图8-32所示。 :-: ![](https://box.kancloud.cn/d428d62f07ec31ee1ab4e86a4a530497_480x81.png) 得到的输出结果如图8-33所示,发现其中多了很多的/com.saurik.substrate下面的动态库,说明该进程也已经被其注入了。所以我们判断com.example.testndklib应用程序已经被Hook,存在不安全的隐患。 :-: ![](https://box.kancloud.cn/7a4e061701335b701ab75376099d0e2c_540x426.png) 同样的方式,我们能够查看到Zygote进程的运行情况,发现也是被注入了CydiaSubstrate框架的很多so库文件,如图8-34所示。 作为应用程序对自身的检测,也只需要读取对应的进程的虚拟地址空间目录/proc/pid/maps文件,判断当前进程空间中载入的代码库文件是否存在于自己白名单中的,即可判断自身程序是否被Hook。但是,对于zygote进程来说如果没有Root权限,我们是无法访问其maps文件的,那么也就无法判断Hook与否了。 :-: ![](https://box.kancloud.cn/eed0af76efdb3637ec789b8882d9ff55_532x293.png) #### **Hook修复** 如何判断一个进程是否被其他第三方函数库Hook,我们已经知道了。为了让我们的应用程序能够在一个安全可靠的环境中运行,那么我们就必须将这些不速之客从应用程序的进程中剥离出去。 如上面我们演示的testndklib应用程序,我们在adb shell命令模式下查看其进程pid为30210,并根据进程pid查看其对应的进程虚拟地址空间。具体命令如图8-35所示。 :-: ![](https://box.kancloud.cn/77fcd2549a7c8d7e35d3c07ff49684c4_525x111.png) 从系统返回的具体结果中我们发现,已经被很多的第三方 Hook 库所加载,这里都是以/com.saurik.substrate开头的substrate框架的动态库,如图8-36所示。 :-: ![](https://box.kancloud.cn/53abe5c8d74404a3f10f4f1a63b82f59_533x187.png) 当然,我们希望除了自身包名(com.example.testndklib)下的其他动态链接全都给删除关闭,且关闭后的应用程序还能够正常地运行。因为所有的第三方库都是通过dlopen后注入的方式附加到应用程序进程中的,这里我们很容易想到我们直接使用 dlclose 将其中的第三方函数挨个卸载关闭即可。 这样一个程序思路就来了,首先扫描/proc//maps目录下的所有so库文件,并将自身的动态库文件排除,对于非自身的动态链接库我们全都卸载关闭。对于Java我们无法使用dlclose,所以这里我们还是采用了JNI的方式来完成,具体的操作函数如下所示。 ~~~ /**    * 根据包名与进程pid,删除非包名下的动态库    * @parampid 进程pid    * @parampkg 包名    * @return    */ public List<String> removeHooks(intpid, String pkg) {     List<String> hookLibFile = new ArrayList<>();     // 找到对应进程的虚拟地址空间文件     File file = newFile("/proc/" + pid + "/maps"); if(!file.exists()) { return hookLibFile;     } try{       BufferedReader bufferedReader = new BufferedReader(new InputStreamReader (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)) { int index = 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();     } return hookLibFile;   }   /**    * 卸载加载的so库    * @paramsoPath so库地址路径    */ public native void removeHookSo(String soPath); // JNI中的removeHookSo卸载一个so的加载 void Java_com_example_testndklib_MainActivity_removeHookSo(JNIEnv* env,     jobject thiz, jstring path) {    const char* chars = env->GetStringUTFChars(path, 0);    void* handle = dlopen(chars, RTLD_NOW);    int count = 4;    int i = 0;    for(i = 0; i < count; i++) {      if(NULL != handle) {        dlclose(handle);      }    } } ~~~ 在需要卸载的应用程序中调用removeHooks(Process.myPid(), getPackageName())就能够轻松地完成上述的功能。那么是否所有的动态库都被卸载移除了?我们重新查看该应用程序的虚拟地址空间,得到结果如图8-37所示。 :-: ![](https://box.kancloud.cn/5475bf93a0a428c1314faa49294765c9_495x128.png) 比较后大家都会发现,虽然卸载掉了大部分的so动态链接库,但是还是残余了少许没有被卸载干净,如我们这里剩余的libAndroidBootstrap0.so库还是依然在加载中。对于dlclose函数读者们应该都清楚,dlclose用于关闭指定句柄的动态链接库 ,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。也就是说如果我们手动卸载动态链接库之前,系统已经保持对其的应用的话,我们是无法卸载的。 Hook框架的动态库什么时候加载如何加载我们都不能够得知,所以对非本包的动态链接库卸载也需要实时监测去卸载。且就算卸载也不能够完全地保证系统就没有对相关函数的引用,达到卸载干净的目的。所以,我们得出结论,对于Hook后的应用程序修复在目前来说是一项暂无解决方案的工作。 #### **Hook检测小结** 说到如何识别一个应用程序是否被Hook、修复Hook,我们会发现因为Android操作系统上沙箱(Sandbox)机制的存在,不管我们采用何种方案手段都没有办法完全避免程序被Hook。这个时候我们就需要将我们的目光转到如何防止应用程序被Hook,预防于未然才是主要的解决方案。 #### **参考文章:** [《Android安全技术揭秘与防范》—第8章8.5节Hook检测/修复](https://yq.aliyun.com/articles/99886?spm=5176.100239.blogcont99909.22.6fd616140gXvb7)