Android Java层的anti-hooking技巧

原文是英文,乌云上的是中文翻译,英文原文地址 Android Anti-Hooking Techniques in Javajava

原文连接:Android Java层的anti-hooking技巧android

0x00 前言

一个最近关于检测native hook框架的方法让我开始思考一个Android应用如何在Java层检测Cydia Substrate或者Xposed框架。git

声明:下文全部的anti-hooking技巧很容易就能够被有经验的逆向人员绕过,这里只是展现几个检测的方法。在最近DexGuard和GuardIT等工具中尚未这类anti-hooking检测功能,不过我相信不久就会增长这个功能。github

0x01 检测安装的应用

一个最直接的想法就是检测设备上有没有安装Substrate或者Xposed框架,能够直接调用PackageManager显示全部安装的应用,而后看是否安装了Substrate或者Xposed。bash

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")) {
        Log.wtf("HookDetection", "Xposed found on the system.");
    }
    if(applicationInfo.packageName.equals("com.saurik.substrate")) {
        Log.wtf("HookDetection", "Substrate found on the system.");
    }
}

0x02 检查调用栈里的可疑方法

另外一个想到的方法是检查Java调用栈里的可疑方法,主动抛出一个异常,而后打印方法的调用栈。代码以下:app

public class DoStuff {
    public static String getSecret() {
        try {
            throw new Exception("blah");
        }
        catch(Exception e) {
            for(StackTraceElement stackTraceElement : e.getStackTrace()) {
                Log.wtf("HookDetection", stackTraceElement.getClassName() + "->" + stackTraceElement.getMethodName());
            }
        }
        return "ChangeMePls!!!";
    }
}

当应用没有被hook的时候,正常的调用栈是这样的:框架

com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main
dalvik.system.NativeStart->main

可是假若有Xposed框架hook了com.example.hookdetection.DoStuff.getSecret方法,那么调用栈会有2个变化:函数

  • 在dalvik.system.NativeStart.main方法后出现de.robv.android.xposed.XposedBridge.main调用工具

  • 若是Xposed hook了调用栈里的一个方法,还会有de.robv.android.xposed.XposedBridge.handleHookedMethod和de.robv.android.xposed.XposedBridge.invokeOriginalMethodNative调用oop

因此若是hook了getSecret方法,调用栈就会以下:

com.example.hookdetection.DoStuff->getSecret

de.robv.android.xposed.XposedBridge->invokeOriginalMethodNative
de.robv.android.xposed.XposedBridge->handleHookedMethod

com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main

de.robv.android.xposed.XposedBridge->main

dalvik.system.NativeStart->main

下面看下Substrate hook com.example.hookdetection.DoStuff.getSecret方法后,调用栈会有什么变化:

  • dalvik.system.NativeStart.main调用后会出现2次com.android.internal.os.ZygoteInit.main,而不是一次

  • 若是Substrate hook了调用栈里的一个方法,还会出现com.saurik.substrate.MS$2.invoked,com.saurik.substrate.MS$MethodPointer.invoke还有跟Substrate扩展相关的方法(这里是com.cigital.freak.Freak$1$1.invoked)

因此若是hook了getSecret方法,调用栈就会以下:

com.example.hookdetection.DoStuff->getSecret

com.saurik.substrate._MS$MethodPointer->invoke
com.saurik.substrate.MS$MethodPointer->invoke
com.cigital.freak.Freak$1$1->invoked
com.saurik.substrate.MS$2->invoked

com.example.hookdetection.DoStuff->getSecret
com.example.hookdetection.MainActivity->onCreate
android.app.Activity->performCreate
android.app.Instrumentation->callActivityOnCreate
android.app.ActivityThread->performLaunchActivity
android.app.ActivityThread->handleLaunchActivity
android.app.ActivityThread->access$800
android.app.ActivityThread$H->handleMessage
android.os.Handler->dispatchMessage
android.os.Looper->loop
android.app.ActivityThread->main
java.lang.reflect.Method->invokeNative
java.lang.reflect.Method->invoke
com.android.internal.os.ZygoteInit$MethodAndArgsCaller->run
com.android.internal.os.ZygoteInit->main

com.android.internal.os.ZygoteInit->main

dalvik.system.NativeStart->main

在知道了调用栈的变化以后,就能够在Java层写代码进行检测:

try {
    throw new Exception("blah");
}
catch(Exception e) {
    int zygoteInitCallCount = 0;
    for(StackTraceElement stackTraceElement : e.getStackTrace()) {
        if(stackTraceElement.getClassName().equals("com.android.internal.os.ZygoteInit")) {
            zygoteInitCallCount++;
            if(zygoteInitCallCount == 2) {
                Log.wtf("HookDetection", "Substrate is active on the device.");
            }
        }
        if(stackTraceElement.getClassName().equals("com.saurik.substrate.MS$2") && 
                stackTraceElement.getMethodName().equals("invoked")) {
            Log.wtf("HookDetection", "A method on the stack trace has been hooked using Substrate.");
        }
        if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && 
                stackTraceElement.getMethodName().equals("main")) {
            Log.wtf("HookDetection", "Xposed is active on the device.");
        }
        if(stackTraceElement.getClassName().equals("de.robv.android.xposed.XposedBridge") && 
                stackTraceElement.getMethodName().equals("handleHookedMethod")) {
            Log.wtf("HookDetection", "A method on the stack trace has been hooked using Xposed.");
        }

    }
}

0x03 检测并不该该native的native方法

Xposed框架会把hook的Java方法类型改成"native",而后把原来的方法替换成本身的代码(调用hookedMethodCallback)。能够查看 XposedBridge_hookMethodNative的实现,是修改后app_process里的方法。

利用Xposed改变hook方法的这个特性(Substrate也使用相似的原理),就能够用来检测是否被hook了。注意这不能用来检测ART运行时的Xposed,由于不必把方法的类型改成native。

假设有下面这个方法:

public class DoStuff {
    public static String getSecret() {
        return "ChangeMePls!!!";
    }
}

若是getSecret方法被hook了,在运行的时候就会像下面的定义:

public class DoStuff {
        // calls hookedMethodCallback if hooked using Xposed
    public native static String getSecret(); 
}

基于上面的原理,检测的步骤以下:

  • 定位到应用的DEX文件

  • 枚举全部的class

  • 经过反射机制判断运行时不该该是native的方法

下面的Java展现了这个技巧。这里假设了应用自己没有经过JNI调用本地代码,大多数应用都不须要调用本地方法。不过若是有JNI调用的话,只须要把这些native方法添加到一个白名单中便可。理论上这个方法也能够用于检测Java库或者第三方库,不过须要把第三方库的native方法添加到一个白名单。检测代码以下:

for (ApplicationInfo applicationInfo : applicationInfoList) {
    if (applicationInfo.processName.equals("com.example.hookdetection")) {      
        Set classes = new HashSet();
        DexFile dex;
        try {
            dex = new DexFile(applicationInfo.sourceDir);
            Enumeration entries = dex.entries();
            while(entries.hasMoreElements()) {
                String entry = entries.nextElement();
                classes.add(entry);
            }
            dex.close();
        } 
        catch (IOException e) {
            Log.e("HookDetection", e.toString());
        }
        for(String className : classes) {
            if(className.startsWith("com.example.hookdetection")) {
                try {
                    Class clazz = HookDetection.class.forName(className);
                    for(Method method : clazz.getDeclaredMethods()) {
                        if(Modifier.isNative(method.getModifiers())){
                            Log.wtf("HookDetection", "Native function found (could be hooked by Substrate or Xposed): " + clazz.getCanonicalName() + "->" + method.getName());
                        }
                    }
                }
                catch(ClassNotFoundException e) {
                    Log.wtf("HookDetection", e.toString());
                }
            }
        }
    }
}

0x04 经过/proc/[pid]/maps检测可疑的共享对象或者JAR

/proc/[pid]/maps记录了内存映射的区域和访问权限,首先查看Android应用的映像,第一列是起始地址和结束地址,第六列是映射文件的路径。

#cat /proc/5584/maps

40027000-4002c000 r-xp 00000000 103:06 2114      /system/bin/app_process
4002c000-4002d000 r--p 00004000 103:06 2114      /system/bin/app_process
4002d000-4002e000 rw-p 00005000 103:06 2114      /system/bin/app_process
4002e000-4003d000 r-xp 00000000 103:06 246       /system/bin/linker
4003d000-4003e000 r--p 0000e000 103:06 246       /system/bin/linker
4003e000-4003f000 rw-p 0000f000 103:06 246       /system/bin/linker
4003f000-40042000 rw-p 00000000 00:00 0 
40042000-40043000 r--p 00000000 00:00 0 
40043000-40044000 rw-p 00000000 00:00 0 
40044000-40047000 r-xp 00000000 103:06 1176      /system/lib/libNimsWrap.so
40047000-40048000 r--p 00002000 103:06 1176      /system/lib/libNimsWrap.so
40048000-40049000 rw-p 00003000 103:06 1176      /system/lib/libNimsWrap.so
40049000-40091000 r-xp 00000000 103:06 1237      /system/lib/libc.so
... Lots of other memory regions here ...

所以能够写代码检测加载到当前内存区域中的可疑文件:

try {
    Set libraries = new HashSet();
    String mapsFilename = "/proc/" + android.os.Process.myPid() + "/maps";
    BufferedReader reader = new BufferedReader(new FileReader(mapsFilename));
    String line;
    while((line = reader.readLine()) != null) {
        if (line.endsWith(".so") || line.endsWith(".jar")) {
            int n = line.lastIndexOf(" ");
            libraries.add(line.substring(n + 1));
        }
    }
    for (String library : libraries) {
        if(library.contains("com.saurik.substrate")) {
            Log.wtf("HookDetection", "Substrate shared object found: " + library);
        }
        if(library.contains("XposedBridge.jar")) {
            Log.wtf("HookDetection", "Xposed JAR found: " + library);
        }
    }
    reader.close();
}
catch (Exception e) {
    Log.wtf("HookDetection", e.toString());
}

Substrate会用到几个so:

Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidBootstrap0.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidCydia.cy.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libDalvikLoader.cy.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libsubstrate-dvm.so
Substrate shared object found: /data/app-lib/com.saurik.substrate-1/libAndroidLoader.so

Xposed会用到一个Jar:

Xposed JAR found: /data/data/de.robv.android.xposed.installer/bin/XposedBridge.jar

0x05 绕过检测的方法

上面讨论了几个anti-hooking的方法,不过相信也会有人提出绕过的方法,这里对应每一个检测方法以下:

  • hook PackageManager的getInstalledApplications,把Xposed或者Substrate的包名去掉

  • hook Exception的getStackTrace,把本身的方法去掉

  • hook getModifiers,把flag改为看起来不是native

  • hook 打开的文件的操做,返回/dev/null或者修改的map文件

0x06 总结

第三个方法中经过分析java方法的属性是否为 native 来判断是否被hook, 这中方式能够用来保护一些自定义的接口,或者SDK API 接口。

后来同事经过分析Xposed源码,发现经过ClassLoader能够识别出具体的Xposed module hook了某个具体的函数,下次说下分析思路。

相关文章
相关标签/搜索