抖音技术能力大揭密!钜惠大礼、深度体验,尽在火山引擎增长沙龙,就等你来! 立即报名>> 了解详情
写点什么

Android Hook 技术防范漫谈

2020 年 2 月 27 日

Android Hook技术防范漫谈

背景

当下,数据就像水、电、空气一样无处不在,说它是“21 世纪的生产资料”一点都不夸张,由此带来的是,各行业对于数据的争夺热火朝天。随着互联网和数据的思维深入人心,一些灰色产业悄然兴起,数据贩子、爬虫、外挂软件等等也接踵而来,互联网行业中各公司竞争对手之间不仅业务竞争十分激烈,黑科技的比拼也越发重要。随着移动互联网的兴起,爬虫和外挂也从单一的网页转向了 App,其中利用 Android 平台下 Dalvik 模式中的Xposed InstallerCydia 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 逻辑。


上面提到的hookMethodNativeXposedBridge.jar中的私有的本地方法,它将一个方法对象作为传入参数并修改 Dalvik 虚拟机中对于该方法的定义,把该方法的类型改变为 Native 并将其实现指向另外一个 B 方法。


换言之,当调用那个被 Hook 的 A 方法时,其实调用的是 B 方法,调用者是不知道的。在 hookMethodNative 的实现中,会调用XposedBridge.jar中的handleHookedMethod这个方法来传递参数。handleHookedMethod这个方法类似于一个统一调度的 Dispatch 例程,其对应的底层的 C++函数是xposedCallHandler。而handleHookedMethod实现里面会根据一个全局结构hookedMethodCallbacks来选择相应的 Hook 函数并调用他们的beforeafter函数,当多模块同时 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、相关的 Dex、Jar 和 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.solibsubstratedvm.so。考虑到 Java 层检测强度太低,Substrate 的检测主要在 Native 层来实现。



检测

动态加载式检测

读取/proc/self/maps,列出了 App 中所有加载的文件。



上图为Cydia Substrate在 Android 4.4 上注入后的进程 maps 表,其中libsubstrate.solibsubstrate-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 InstallerCydia Substrate的检测也仅仅是保障 App 安全的手段之一。App 安全的防御不应仅仅依赖于此,应该构建起整体的安全防御闭环,尽可能在所有已知的可能攻击点都追加检测,再配合代码加固,将防御代码隐藏。遗憾的是 App 防御代码隐藏再深也终究会被破解,仅仅依赖于客户端的防御显然是不足的。移动互联网领域的整体安全防御应该是走端云结合协作之道,共同防御,方能在攻防对抗中占据优势地位。


作者简介

  • 礼赞,美团安全工程师,2016 年 11 月加入美团。专注于二进制、移动端攻防相关工作,现负责美团 Android 移动安全组件的建设工作。

  • 毅然,美团技术专家,2016 年初加入美团。致力于美团配送 App 组的 Android App crash 解决工作、Android App 性能优化、Android App 反外挂、反爬虫。目前主导负责美团配送 Android App 移动安全相关建设。


2020 年 2 月 27 日 11:14384

评论

发布
暂无评论
发现更多内容

游戏夜读 | 改写图形API的意义

game1night

神经网络中为什么不能将权重初始值设置为一样的值

王坤祥

神经网络 学习

奈学干货分享:分布式CAP实践分析

奈学教育

分布式

ARTS_20200529

凌轩

Java ARTS 打卡计划

Server Queue 提高 QPS

风含叶

Python kafka 后端 队列

一个在游戏行业摸爬滚打了十几年的人,为何我对这本书情有独钟

图灵社区

游戏开发 游戏制作 世嘉培训教材

基于 Markdown 的中文文档排版规范

Murphy

markdown 排版规范 GitHub GFM 物联网学前班

卧槽,接到一个阎王的需求

码农神说

程序员

Nginx 入门及命令行操作

子杨

nginx 运维

我们可能都误解了什么是情商

董一凡

情绪

Weex开发:页面跳转以及Android端多应用选择窗口的处理

brave heart

android Vue 大前端 跨平台 Weex

2020智源-京东多模态对话挑战赛开战 产学研联合推动AI技术发展

DT极客

Mysql常用删除方式比较

云也退

MySQL

Cassandra可调一致性的使用及原理

老任物联网杂谈

大数据 分布式 Cassandra 可调一致性

「首度揭秘」大规模HPC生产环境 IO 特征

焱融科技

sds io 高性能 存储 焱融科技

kudmp介绍和安装

唯爱

为什么你要学习 Go?

司徒公子

go golang 编程语言 谷歌Google

探索 Go 语言数据类型的内部实现

TuringTuring

golang 内存模型 高效

从位图到布隆过滤器

王坤祥

位图 布隆过滤器

Android 通过opencv实现人脸识别,追踪

sar

android OpenCV 人脸识别

工厂模式(三)泛型工厂的概念以及示例代码

LSJ

阿里巴巴为什么让初始化集合时必须指定大小?

王磊

Java 性能

“Plus Token”传销主犯被公诉!警惕,区块链不是“取款链”!

CECBC区块链专委会

1024讲话 CECBC 区块链技术 人才发展 培训

GrowingIO 大数据多维分析自动化测试实践

GrowingIO技术专栏

大数据 自动化测试 parewise

Nginx 基础原理和命令行的真相

子杨

nginx 运维

备案问题汇总

云也退

网站 备案

10分钟了解Flink

代码诗人

你有信息焦虑症吗?

Neco.W

学习 创业 知识体系

幂等问题及解决方案

Joker

幂等 解决方案

用户故事为什么要关联开发数据?

Worktile

敏捷开发 开发数据

架构师训练营0期开营

刁架构

架构师

Study Go: From Zero to Hero

Study Go: From Zero to Hero

Android Hook技术防范漫谈-InfoQ