使用混淆主要可以减小包的大小。混淆对于安全保护来说,只是增加了阅读难度而已。混淆不会把关键代码混淆掉,比如MainActivity,Application等,可以通过分析smali和阅读jar包定位代码。
资源混淆也是换汤不换药,针对加载资源代码getString(2131230929)进行进制转换,变成16进制,从public.xml里面查找对应的资源,就能定位到资源内容。
这个是防止二次打包验证,但是,对于java代码的签名保护,可以很容易地进行修改smali代码绕过验证。
安全性也不是很高,只是一种会增加破解成本的方式。一般Native方法根据命名规则生成头文件然后写cpp代码,这种方式属于静态注册。手动动态注册是复写JNI_OnLoad方法,在该函数中手动注册方法名和对应的方法签名,方法名可以自定义,这样避免了静态注册的命名规则,让破解者难以根据规律找到要破解的方法。不过破解者可以分析JNI_OnLoad函数的汇编代码找到register函数找到注册的native方法。
IDA进行so动态调试是基于进程的注入技术,然后使用linux中ptrace机制,进行调试目标进程的附加操作。
ptrace机制有一个特点:如果一个进程被调试了,在它进程status文件中有一个字段TracerPid会记录调试者的进程id值
cat /proc/pidxx/status 可以看到TracerPid字段
方法是检测该TracerPid值,大于0就退出。但破解者会通过IDA工具给JNI_OnLoad下断点,检测轮询代码,使用nop指令跳过检测指令。
在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
机制覆写内置方法,实现功能劫持。下面我们来看一下细节。
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层的检测基本只能检测出基础的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
类中的fieldCache
、methodCache
、constructorCache
变量,读取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;
}
由上文可知,无论在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;
}
}
...
}