移动安全防护策略之——Xposed 反调试
追溯移动安全的历史,在移动互联兴起的时候,利用 Android 平台下的Xposed Installer和Cydia Substrate框架对 APP 的函数进行Hook这一招,堪称老牌经典,也是经久不衰,虽然随着 Android 版本的迭代升级, Xposed 的身影越来越少,但是作为攻击者而言它依然是一个经典利器。
Xposed 是一款可以在不修改APK的情况下影响程序运行(修改系统)的框架服务,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。借助旧版本的手机或平台,可以实现各种花式操作,同时也是我们端侧安全技术防范的重点之一。
在 Android 的系统启动流程中,存在几个关键的节点,梳理如下:
(1)按下电源键 > 引导程序 Bootloader 加载到 RAM 并开始执行
(2)内核启动,加载驱动、设置缓存,完成系统设置等 >启动 init 进程
(3)init 进程初始化 >启动 Zygote 进程
(4)创建 Java VM 、注册 JNI 、创建服务端 Socket 、启动 SystemServer
(5)启动 Binder 线程池和 SystemServiceManager ,并且启动各种系统服务
(6)SystemServer 进程 >启动 ActivityManagerService >启动Launcher,Launcher 将已安装应用的快捷图标显示到界面上
(7)完成系统启动流程
其中init是内核启动的第一个用户级进程,zygote是由init进程通过解析 init.zygote.rc 文件而创建的,zygote 所对应的具体可执行程序是 appprocess,所对应的源文件是Appmain.cpp,进程名称为 zygote, zygote 进程是所有应用的父进程,结合下面的流程图,可以看下 zygote 究竟做了什么事情:
(1)创建 AppRuntime 对象并调用它的 start 方法,启动 zygote 进程,此后的操作活动由 AppRuntime 来控制
(2)调用 startVm 创建 Java 虚拟机,调用 startReg 来注册 JNI 函数
(3)通过 JNI 调用 com.android.internal.os.ZygoteInit下的 main 函数,初始化整个 Java 层
(4)通过 registerZygoteSocket 函数创建服务端 Socket,并通过 runSelectLoop 函数等待 ActivityManagerService 的请求来创建新的应用程序进程
(5)启动 SystemServer 进程
Xposed 构成:
名称 | 介绍 |
Xposed | Xposed框架Native部分 |
XposedBridge | Xposed向开发者提供的API与相应工具类库 |
XposedInstaller | Xposed框架Android端本地管理,环境框架,以及第三方module资源下载的工具 |
zygote进程在启动的过程中会将Java Rumtime加载到进程中并注册一些Android核心类的JNI(Java Native Interface,Java本地接口)方法。一个App进程被zygote进程 fork 出来的时候,不仅会获得 zygote 进程中的虚拟机实例拷贝,还会与 zygote 进程一起共享Java Rumtime,也就是可以将XposedBridge.jar这个Jar包加载到每一个Android App进程中去。安装Xposed Installer之后,系统app_process将被替换,然后利用Java的Reflection机制覆写内置方法,实现功能劫持。xposed的主要接口在XposedBrigde.jar中,核心功能在替换的虚拟机中实现,appprocess是Android App的启动程序(具体形式是zygote fork() 调用appprocess作为Android app的载体)。
Xposed 主要工作流程如下:
Xposed Installer中真正起作用的是对方法的 Hook 与 Replace。在系统启动时,zygote 进程加载XposedBridge.jar,将需要替换的方法通过JNI中的hookMethodNative指向Native方法xposedCallHandler,xposedCallHandler再调用handleHookedMethod来调用被劫持的方法,进而转入Hook逻辑。
hookMethodNative是XposedBridge.jar中的私有的本地方法,将一个方法对象作为传入参数并修改虚拟机中对于该方法的定义,换言之,当调用那个被Hook的A方法时,其实调用的是B方法,调用者是无法感知。
基于上述设计,我们可以发现在针对Xposed的安全防范策略中,Xposed Installer检测是必不可少的一环,因此围绕该核心点,对 Xposed 框架的整体防御检测可其实现分为两块内容:
(1) Java 层检测
(2) Native 层检测
Java 层检测
01 遍历App安装列表检测法
检测原理:当 App 获取到系统权限的时候,可获取系统的所有运行中 App 的列表,进而可以发现是否存在有Xposed相关的App(通常都是Xposed Installer相关的Apk,如de.robv.android.xposed.installer)保持运行状态,一旦存在,就表明用户很有可能存在Hook行为。
示例代码:
对抗逻辑:修改Installer包名
检测原理:在正常的Android系统启动过程中,init 进程会去解析 init.rc 文件启动一系列的服务,其中就有 appprocess 进程,在 appprocess 执行过程中,会设置自身进程名为 Zygote,启动 com.android.internal.os.ZygoteInit.Main 方法。而 Xposed 修改了 app_process 进程,会先启动de.robv.android.xposed.XposedBridge.Main 方法,再由它去启动 com.android.internal.os.ZygoteInit.Main 方法,因此堆栈信息中会多出一些内容。简单说就是 Xposed 先于了 Zygote 进程,因此在系统堆栈信息中会多出 Xposed 相关的内容。
示例代码:
对抗逻辑:通过Hook堆栈类StackTraceElement,当发现Xposed和Zygote有错误输出时,修改输出信息,例如将输出置空来绕过错误信息检测。
检测原理:检测 ClassLoader的 loadClass 中是否有 Xposed 相关参数
示例代码:
对抗逻辑:Hook loadClass加载类来修改加载的类名,例如修改de.robv.android.xposed成另一个普通的包名
检测原理:通过反射遍历XposedHelper类中的fieldCache、methodCache、constructorCache变量,读取HashMap缓存字段,如字段项的key中包含App中唯一或敏感方法等,即可认为有Xposed注入,如下图:
示例代码:
对抗逻辑:修改类名,让检测方找不到相关类,可以参考第三种方案。
检测原理:当一个Android App中的Java方法被莫名其妙地变成了Native JNI方法,则非常有可能被Xposed Hook了。由此可得,检查关键方法是不是变成Native JNI方法,也可以检测是否被Hook。通过反射调用Modifier.isNative(method.getModifiers())方法可以校验方法是不是Native JNI方法。
对抗逻辑:Xposed同样可以篡改isNative这个方法的返回值
Native 层检测
由上文可知,无论在Java层做何种检测,Xposed都可以通过Hook相关的API并返回指定的结果来绕过检测,只要有方法就可以被Hook。如果仅在Java层检测就显得很徒劳,为了有效提搞检测准确率,就须做到Java和Native层同时检测。
检测原理:通过读取/proc/self/maps文件,在linux内核中,这个文件存储了进程映射了的内存区域和访问权限,因此遍历自身加载的库,就可以拿到当前上下文的so和jar列表,通过查找Xposed相关文件来做检测。
示例代码:
对抗逻辑:因为读取的时候会调用BufferedReader进行读取命令的内容,
Hook BufferedReader过滤掉XposedBridge.jar等相关内容就可以完成绕过。