背景
当下, 数据就像水电空气一样无处不在, 说它是 21 世纪的生产资料一点都不夸张, 由此带来的是, 各行业对于数据的争夺热火朝天随着互联网和数据的思维深入人心, 一些灰色产业悄然兴起, 数据贩子爬虫外挂软件等等也接踵而来, 互联网行业中各公司竞争对手之间不仅业务竞争十分激烈, 黑科技的比拼也越发重要随着移动互联网的兴起, 爬虫和外挂也从单一的网页转向了 App, 其中利用 Android 平台下 Dalvik 模式中的 Xposed Installer 和 Cydia Substrate 框架对 App 的函数进行 Hook 这一招, 堪称老牌经典
接下来, 本文将分别介绍针对这两种框架的防护技术
Xposed Installer
原理
Zygote
在 Android 系统中 App 进程都是由 Zygote 进程孵化出来的 Zygote 进程在启动时会创建一个虚拟机实例, 每当它孵化一个新的应用程序进程时, 都会将这个 Dalvik 虚拟机实例复制到新的 App 进程里面去, 从而使每个 App 进程都有一个独立的 Dalvik 虚拟机实例
Zygote 进程在启动的过程中, 除了会创建一个虚拟机实例之外还会将 Java Rumtime 加载到进程中并注册一些 Android 核心类的 JNI(Java Native Interface,Java 本地接口)方法一个 App 进程被 Zygote 进程孵化出来的时候, 不仅会获得 Zygote 进程中的虚拟机实例拷贝, 还会与 Zygote 进程一起共享 Java Rumtime, 也就是可以将 XposedBridge.jar 这个 Jar 包加载到每一个 Android App 进程中去安装 Xposed Installer 之后, 系统 app_process 将被替换, 然后利用 Java 的 Reflection 机制覆写内置方法, 实现功能劫持下面我们来看一下细节
Hook 和 Replace
Xposed Installer 框架中真正起作用的是对方法的 Hook 和 Replace 在 Android 系统启动的时候, Zygote 进程加载 XposedBridge.jar, 将所有需要替换的 Method 通过 JNI 方法 hookMethodNative 指向 Native 方法 xposedCallHandler, 这个方法再通过调用 handleHookedMethod 这个 Java 方法来调用被劫持的方法转入 Hook 逻辑
上面提到的 hookMethodNative 是 XposedBridge.jar 中的私有的本地方法, 它将一个方法对象作为传入参数并修改 Dalvik 虚拟机中对于该方法的定义, 把该方法的类型改变为 Native 并将其实现指向另外一个 B 方法
换言之, 当调用那个被 Hook 的 A 方法时, 其实调用的是 B 方法, 调用者是不知道的在 hookMethodNative 的实现中, 会调用 XposedBridge.jar 中的 handleHookedMethod 这个方法来传递参数 handleHookedMethod 这个方法类似于一个统一调度的 Dispatch 例程, 其对应的底层的 C++ 函数是 xposedCallHandler 而 handleHookedMethod 实现里面会根据一个全局结构 hookedMethodCallbacks 来选择相应的 Hook 函数并调用他们的 before 和 after 函数, 当多模块同时 Hook 一个方法的时候 Xposed 会自动根据 Module 的优先级来排序
调用顺序如下: A.before -> B.before -> original method -> B.after -> A.after
检测
在做 Android App 的安全防御中检测点众多, Xposed Installer 检测是必不可少的一环对于 Xposed 框架的防御总体上分为两层: Java 层和 Native 层
Java 层检测
需要说明的是, Java 层的检测基本只能检测出基础的 Xposed Installer 框架, 而不能防护其对 App 内方法的 Hook, 如果框架中带有反检测则 Java 层检测大多不起作用
下面列出 Java 层的检测点, 仅供参考
通过 PackageManager 查看安装列表
最简单的检测, 我们调用 Android 提供的 PackageManager 的 API 来遍历系统中 App 的安装情况来辨别是否有安装 Xposed Installer 相关的软件包
- PackageManager packageManager = context.getPackageManager();
- List applicationInfoList = packageManager.getInstalledApplications(PackageManager.GET_META_DATA);
- for (ApplicationInfo applicationInfo: applicationInfoList) {
- if (applicationInfo.packageName.equals("de.robv.android.xposed.installer")) {
- // is Xposed TODO... }
- }
通常情况下使用 Xposed Installer 框架都会屏蔽对其的检测, 即 Hook 掉 PackageManager 的 getInstalledApplications 方法的返回值, 以便过滤掉 de.robv.android.xposed.installer 来躲避这种检测
自造异常读取栈
Xposed Installer 框架对每个由 Zygote 孵化的 App 进程都会介入, 因此在程序方法异常栈中就会出现 Xposed 相关的身影, 我们可以通过自造异常 Catch 来读取异常堆栈的形式, 用以检查其中是否存在 Xposed 的调用方法
- try {
- throw new Exception("blah");
- } catch(Exception e) {
- for (StackTraceElement stackTraceElement: e.getStackTrace()) {
- // stackTraceElement.getClassName() stackTraceElement.getMethodName() 是否存 在 Xposed
- }
- }
- E/GEnvironment: no such table: preference (code 1): while compiling: SELECT keyguard_show_livewallpaper FROM preference
- ...
- at com.meituan.test.extpackage.ExtPackageManager.checkUpdate(ExtPackageManager.java:127)
- at com.meituan.test.MiFGService$1.run(MiFGService.java:41)
- at android.os.Looper.loop(Looper.java:136)
- at android.app.ActivityThread.main(ActivityThread.java:5072)
- at java.lang.reflect.Method.invokeNative(Native Method)
- at java.lang.reflect.Method.invoke(Method.java:515)
- ...
- at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
- at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:609)
- at de.robv.android.xposed.XposedBridge.main(XposedBridge.java:132) // 发现 Xposed 模块
- at dalvik.system.NativeStart.main(Native Method)
检查关键 Java 方法被变为 Native JNI 方法
当一个 Android App 中的 Java 方法被莫名其妙地变成了 Native JNI 方法, 则非常有可能被 Xposed Hook 了由此可得, 检查关键方法是不是变成 Native JNI 方法, 也可以检测是否被 Hook
通过反射调用 Modifier.isNative(method.getModifiers())方法可以校验方法是不是 Native JNI 方法, Xposed 同样可以篡改 isNative 这个方法的返回值
反射读取 XposedHelper 类字段
通过反射遍历 XposedHelper 类中的 fieldCachemethodCacheconstructorCache 变量, 读取 HashMap 缓存字段, 如字段项的 key 中包含 App 中唯一或敏感方法等, 即可认为有 Xposed 注入
- boolean methodCache = CheckHook(clsXposedHelper, "methodCache", keyWord);
- private static boolean CheckHook(Object cls, String filedName, String str) {
- boolean result = false;
- String interName;
- Set keySet;
- try {
- Field filed = cls.getClass().getDeclaredField(filedName);
- filed.setAccessible(true);
- keySet = filed.get(cls)).keySet();
- if (!keySet.isEmpty()) {
- for (Object aKeySet: keySet) {
- interName = aKeySet.toString().toLowerCase();
- if (interName.contains("meituan") || interName.contains("dianping") ) {
- result = true;
- break;
- }
- }
- }
- ...
- return result;
- }
Native 层检测
由上文可知, 无论在 Java 层做何种检测, Xposed 都可以通过 Hook 相关的 API 并返回指定的结果来绕过检测, 只要有方法就可以被 Hook 如果仅在 Java 层检测就显得很徒劳, 为了有效提搞检测准确率, 就须做到 Java 和 Native 层同时检测每个 App 在系统中都有对应的加载库列表, 这些加载库列表在 / proc / 下对应的 pid/maps 文件中描述, 在 Native 层读取 / proc/self/maps 文件不失为检测 Xposed Installer 的有效办法之一由于 Xposed Installer 通常只能 Hook Java 层, 因此在 Native 层使用 C 来解析 / proc/self/maps 文件, 搜检 App 自身加载的库中是否存在 XposedBridge.jar 相关的 DexJar 和 So 库等文件
- bool is_xposed()
- {
- bool rel = false;
- FILE *fp = NULL;
- char* filepath = "/proc/self/maps";
- ...
- string xp_name = "XposedBridge.jar";
- fp = fopen(filepath,"r"))
- while (!feof(fp))
- {
- fgets(strLine,BUFFER_SIZE,fp);
- origin_str = strLine;
- str = trim(origin_str);
- if (contain(str,xp_name))
- {
- rel = true; // 检测到 Xposed 模块
- break;
- }
- }
- ...
- }
- Cydia Substrate
原理
Cydia Substrate 注入 Hook 的一个典型流程如下图所示, 在 Java 层配置注入的关键 So 库 libsubstrate.so 和 libsubstratedvm.so 考虑到 Java 层检测强度太低, Substrate 的检测主要在 Native 层来实现
检测
动态加载式检测
读取 / proc/self/maps, 列出了 App 中所有加载的文件
上图为 Cydia Substrate 在 Android 4.4 上注入后的进程 maps 表, 其中 libsubstrate.so 和 libsubstrate-dvm.so 两个文件为 Substrate 必载入文件通过 IDA Pro 分析对其分析
先来看 libsubstrate-dvm.so 的导出表, 共有 9 个函数导出
当进程 maps 表中出现 libsubstrate-dvm.so, 可以尝试去 load 该 so 文件并调用 MSJavaHookMethod 方法, 它会返回该方法的地址即判定为恶意模块(第三方程序)
- void* lookup_symbol(char* libraryname,char* symbolname)
- {
- void *imagehandle = dlopen(libraryname, RTLD_GLOBAL | RTLD_NOW);
- if (imagehandle != NULL){
- void * sym = dlsym(imagehandle, symbolname);
- if (sym != NULL){
- return sym; // 发现 Cydia Substrate 相关模块
- }
- ...
- }
该方式基于载入库文件的文件名或文件路径和导出函数来判断是否为恶意模块, 如果完全依赖此方式来判断可能会误判, 但也不失为检测方式的一个点
基于方法特征码检测
特征码即用来判断某段数据属于哪个计算机字段在非 Root 环境下一般一个正常 App 在启动时候, 系统会调度相关大小的内存空间给 App 使用, 此时 App 的运行环境内产生的数据内存存储等是独立于其它 App 的 (即独立运行在沙箱中) 因为处于运行沙箱环境中的进程对沙箱的内存有最高读写权限, 当我们的 App 进程被恶意模块附加或注入时, 就可以通过对当前进程的 PID 所对应的 maps 中加载的模块进行合法校验这里的模块校验我们可以采取对单个模块内容取样来判断是否为恶意模块, 这种方式被定义为基于方法的特征码检测
下面对一段程序段中 OpcodeSample 方法来提取特征码
方法原型:
- #define LOGD(fmt, args...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, fmt, ##args) void OpcodeSample(int a, int b) {
- int c,
- d,
- e;
- c = a + b;
- d = a * b;
- e = a / b;
- LOGD("Hello It's c !%s\n", c);
- LOGD("Hello It's d !%s\n", d);
- LOGD("Hello It's e !%s\n", e);
- return;
- }
通过 IDA Pro 对其分析
左侧红色方框代表为 OpcodeSample 方法的操作码, 右边为操作码对应 ARM 平台的指令集我们要在左侧的操作码中取出一段作为 OpcodeSample 的定位特征码, 选用__android_log_print 方法调用指令集上下文, 来确定特征码
第一次取样:"03 20 31 46 42 46 FF F7 ?? EA"
通过第一次取样, 查找结果有三处相似, 再进一步分析这次我们加入一个常量取样:
第二次取样:"7E 44 ?? ?? F8 44 03 20 31 46 42 46 FF F7 ?? EA"
继而得出唯一特征码, 到此, 我们对特征码方法取样有了初步的了解下面来把它转为实用的技能动态加载式检测 + 特征码结合
我们对 libsubstrate-dvm.so 中导出函数 MSJavaHookMethod 来精准定位
IDA PRO 导出函数表如图:
第三次取样:"55 57 56 53 E8 CC 14 ?? ?? 81 C3 DB ?? ?? ?? 8D 64 ?? ?? 8B 83 F4 ?? ?? ??"
以上即为对 Cydia Substrate 的注入检测识别, 通过检测 / proc/self/maps 下的加载 so 库列表得到各个库文件绝度路径, 通过 fopen 函数将 so 库的内容以 16 进制读进来放在内存里面进行规则比对, 采用字符串模糊查找来检测是否命中黑名单中的方法特征码
总结
在安全对抗领域, 相比攻击方, 防守方历来处于弱势的一方上文所提到的 Xposed Installer 和 Cydia Substrate 的检测也仅仅是保障 App 安全的手段之一 App 安全的防御不应仅仅依赖于此, 应该构建起整体的安全防御闭环, 尽可能在所有已知的可能攻击点都追加检测, 再配合代码加固, 将防御代码隐藏遗憾的是 App 防御代码隐藏再深也终究会被破解, 仅仅依赖于客户端的防御显然是不足的移动互联网领域的整体安全防御应该是走端云结合协作之道, 共同防御, 方能在攻防对抗中占据优势地位
来源: https://juejin.im/entry/5a8ea2916fb9a0635c0488c7