原文是英文,烏雲上的是中文翻譯,英文原文地址 Android Anti-Hooking Techniques in Javajava
原文連接:Android Java層的anti-hooking技巧android
一個最近關於檢測native hook框架的方法讓我開始思考一個Android應用如何在Java層檢測Cydia Substrate或者Xposed框架。git
聲明:下文全部的anti-hooking技巧很容易就能夠被有經驗的逆向人員繞過,這裏只是展現幾個檢測的方法。在最近DexGuard和GuardIT等工具中尚未這類anti-hooking檢測功能,不過我相信不久就會增長這個功能。github
一個最直接的想法就是檢測設備上有沒有安裝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."); } }
另外一個想到的方法是檢查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."); } } }
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()); } } } } }
/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
上面討論了幾個anti-hooking的方法,不過相信也會有人提出繞過的方法,這裏對應每一個檢測方法以下:
hook PackageManager的getInstalledApplications,把Xposed或者Substrate的包名去掉
hook Exception的getStackTrace,把本身的方法去掉
hook getModifiers,把flag改爲看起來不是native
hook 打開的文件的操做,返回/dev/null或者修改的map文件
第三個方法中經過分析java方法的屬性是否爲 native 來判斷是否被hook, 這中方式能夠用來保護一些自定義的接口,或者SDK API 接口。
後來同事經過分析Xposed源碼,發現經過ClassLoader能夠識別出具體的Xposed module hook了某個具體的函數,下次說下分析思路。