Android免Root环境下Hook框架Legend原理分析

title: Android免Root环境下Hook框架Legend原理分析date: 2017-02-12 13:37:22tags: Android软件安全post_asset_folder: true

top: 100

0x1 应用场景

现如今,免Root环境下的逆向分析已经成为一种潮流!

在2015年之前的iOS软件逆向工程领域,要想对iOS平台上的软件进行逆向工程分析,越狱iOS设备与安装Cydia是必须的!几乎绝大多数的逆向相关的动态调试工具、Hook注入框架都依赖于获取IOS设备的最高访问权限,就技术本身上而言,对当前程序进行Hook与动态调试,只需要拥有与当前程序相同的权限即可,理论上无需对设备进行Root越狱,实际上,在2015年就出现了在非越狱设备上进行插件开发的实用案例,2016年的iOS软件逆向工程界,更是一发不可收拾,各种名越狱环境下的逆向工具与逆向技巧被安全人员所发掘,在没有越狱的iOS设备上进行软件的动态调试与逆向工程已经是主流的趋势胃。这样的情况下,最直接的影响是安全研究人员不再对iOS设备越狱有着强烈的追求了,越狱需求的下降可能会直接影响到iOS设备越狱工具的发布与技术的更新迭代。

同样的,在Android设备的免Root环境下,进行软件动态调试与逆向工程分析的需求更加强烈。免Root环境下动态调试与逆向工程就技术本质而言是可行的,安全研究人员的智慧更是有力的证明了这一点,LBE发布免Root环境下APK双开工具平行空间就是最好的例子,它是打破逆向工程技术的原始格局的第一个大锤!随后的,各种APK多开框架、免Root环境下的Hook、免Root环境下的动态调试等技术都被研究人员公开,这是Android软件逆向工程界的福音,逆向工程人员在以后的逆向分析过程中,可能再也不需要为自己的手机能否越狱而感到苦恼,手上在吃灰淘汰的Android小米机可能就是你的逆向必备工具之一。

好了,说了这么多,无非是告诉大家,开发技术在更新迭代,软件的逆向工程技术也在不停的更新,各位研究软件安全的朋友们,你们跟上了时代的脚步吗?!

0x2 Legend框架简介

Legend是Lody开源的一个Android免Root环境下的一个APK Hook框架,代码放在github上:asLody/legend。 该框架代码设计简洁,通用性高,适合逆向工程时一些Hook场景。

先来看看如何使用它。框架提供了两种使用方法:基于Annotation注解与代码直接调用。基于Annotation注解的Hook技术不是第一次被发现了,在Java开发的世界里,这种技术被广泛使用,大名鼎鼎的基于AOP开发的Aspectj就大量使用这种技术。使用Annotation方式编写的Java代码有着很强的灵活与扩展性。Legend中Annotation方式的Hook这样使用:

@Hook(“android.app.Activity::[email protected]”) public static void Activity_startActivity(Activity thiz, Intent intent) { if (!ALLOW_LAUNCH_ACTIVITY) { Toast.makeText(thiz, “I am sorry to turn your Activity down :)”, Toast.LENGTH_SHORT).show(); } else { HookManager.getDefault().callSuper(thiz, intent); } }

@Hook(“xxx”)部分指明需要Hook的类与方法以及方法的签名,此处的Activity_startActivity()是自己实现的替换android.app.Activity::startActivity()的方法,HookManager.getDefault().callSuper(thiz, intent);调用是调用原方法。

这种方式Hook的方法,需要执行一次Hook应用操作来激活所有注解Hook,方法是执行下面的方法,传入的YourClass.class是包含了注解的类:

HookManager.getDefault().applyHooks(YourClass.class);

另一种代码方式进行Hook使用起来更简单,Hook操作只需要一行代码:

HookManager.getDefault().hookMethod(originMethod, hookMethod);

这是Legend提供的demo展示的一个完整实例:

package com.legend.demo; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; import android.telephony.TelephonyManager; import android.widget.Toast; import com.lody.legend.Hook; import com.lody.legend.HookManager; /** * @author Lody * @version 1.0 */ public class App extends Application { public static boolean ENABLE_TOAST = true; public static boolean ALLOW_LAUNCH_ACTIVITY = true; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); HookManager.getDefault().applyHooks(App.class); } @Hook(“android.app.Application::onCreate”) public static void Application_onCreate(Application app) { Toast.makeText(app, “Application => onCreate()”, Toast.LENGTH_SHORT).show(); HookManager.getDefault().callSuper(app); } @Hook(“android.telephony.TelephonyManager::getSimSerialNumber”) public static String TelephonyManager_getSimSerialNumber(TelephonyManager thiz) { return “110”; } @Hook(“android.widget.Toast::show”) public static void Toast_show(Toast toast) { if (ENABLE_TOAST) { HookManager.getDefault().callSuper(toast); } } @Hook(“android.app.Activity::[email protected]”) public static void Activity_startActivity(Activity activity, Intent intent) { if (!ALLOW_LAUNCH_ACTIVITY) { Toast.makeText(activity, “I am sorry to turn your Activity down :)”, Toast.LENGTH_SHORT).show(); }else { HookManager.getDefault().callSuper(activity, intent); } } }

0x3 原理分析

先来看看Hook注解的实现:

// Legend/legendCore/src/main/java/com/lody/legend/Hook.java package com.lody.legend; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author Lody * @version 1.0 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Hook { String value() default “”; }

`@Target(ElementType.METHOD)`指明Hook注解用于修饰类中的Method,与之类似的还有@Target(ElementType.FIELD)用来修饰类中的Field。如果想让注解同时修饰类的Field与Method,可以这么写:

@Target({ElementType.FIELD, ElementType.METHOD}) public @interface Hook{} ……

@Retention(RetentionPolicy.RUNTIME)指明Hook注解以何种形式进行保留。RetentionPolicy是一个enum类型,声明如下:

public enum RetentionPolicy { SOURCE, CLASS, RUNTIME }

`SOURCE`表明该注解类型的信息只保留在程序源码里,源码经过编译之后,注解的数据就会消失;CLASS表明注解类型的信息除了保留在程序源码里,同时也保留在编译好的class文件里面,但在执行的时候,并不会把这些信息加载到内存中去;RUNTIME是最大范围的保留,表示同时在源码与编译好的class文件中保留信息,并且在执行的时候会把这些信息加载到内存中去。

定义好了Hook注解,看它是如何使用的,这就是HookManager.getDefault().applyHooks()方法要做的工作,它的代码如下:

// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java public void applyHooks(Class holdClass) { for (Method hookMethod : holdClass.getDeclaredMethods()) { Hook hook = hookMethod.getAnnotation(Hook.class); if (hook != null) { String statement = hook.value(); String[] splitValues = statement.split(“::”); if (splitValues.length == 2) { String className = splitValues[0]; String[] methodNameWithSignature = splitValues[1].split(“@”); if (methodNameWithSignature.length <= 2) { String methodName = methodNameWithSignature[0]; String signature = methodNameWithSignature.length == 2 ? methodNameWithSignature[1] : “”; String[] paramList = signature.split(“”); if (paramList[0].equals(“”)) { paramList = new String[0]; } try { Class clazz = Class.forName(className); boolean isResolve = false; for (Method method : clazz.getDeclaredMethods()) { if (method.getName().equals(methodName)) { Class[] types = method.getParameterTypes(); if (paramList.length == types.length) { boolean isMatch = true; for (int N = 0; N < types.length; N++) { if (!types[N].getName().equals(paramList[N])) { isMatch = false; break; } } if (isMatch) { hookMethod(method, hookMethod); isResolve = true; Logger.d(“[+++] %s have hooked.”, method.getName()); } } } if (isResolve) { break; } } if (!isResolve) { Logger.e(“[—] Cannot resolve Method : %s.”, Arrays.toString(methodNameWithSignature)); } } catch (Throwable e) { Logger.e(“[—] Error to Load Hook Method From : %s.” , hookMethod.getName()); e.printStackTrace(); } }else { Logger.e(“[—] Cant split method and signature : %s.”, Arrays.toString(methodNameWithSignature)); } }else { Logger.e(“[—] Cant understand your statement : [%s].”, statement); } } } }

该方法遍历类的所有方法,查找匹配注解信息中指定的方法,方法是:对于需要Hook的Class类holdClass,调用它的getDeclaredMethods()获取所有声明的方法,依次调用每个类的方法的getAnnotation()获取注解信息,取到的注解信息保存在String类型的statement变量中,类与完整的方法签名以::进行分隔,方法签名中的方法名与参数签名使用@进行分隔,参数签名中每个参数之间使用进行分隔,取完一个方法所有的信息后,与类中的方法进行比较,如果完全匹配说明找到了需要Hook的方法,这个时候,调用hookMethod()方法进行Hook操作,注意这里的hookMethod()方法,即Legend框架支持的第二种Hook方式。

hookMethod()调用Runtime.isArt()判断当前代码执行在Art还是Dalvik模式,如果是Art模式,执行hookMethodArt()来完成Hook操作,如果是Dalvik模式,执行hookMethodDalvik()完成Hook。

Runtime.isArt()的代码只有一行,即判断虚拟机版本字符串是否以字符2开头,如下:

public static boolean isArt() { return getVmVersion().startsWith(“2”); } public static String getVmVersion() { return System.getProperty(“java.vm.version”); }

执行完Hook后会返回一个backupMethod,这是一个原始方法的备份,最后将backupMethod放入以methodName命令的backupList,在methodNameToBackupMethodsMap备份就完事了。

接下来看看hookMethodArt()都干了啥:

private static Method hookMethodArt(Method origin, Method hook) { ArtMethod artOrigin = ArtMethod.of(origin); ArtMethod artHook = ArtMethod.of(hook); Method backup = artOrigin.backup().getMethod(); backup.setAccessible(true); long originPointFromQuickCompiledCode = artOrigin.getEntryPointFromQuickCompiledCode(); long originEntryPointFromJni = artOrigin.getEntryPointFromJni(); long originEntryPointFromInterpreter = artOrigin.getEntryPointFromInterpreter(); long originDeclaringClass = artOrigin.getDeclaringClass(); long originAccessFlags = artOrigin.getAccessFlags(); long originDexCacheResolvedMethods = artOrigin.getDexCacheResolvedMethods(); long originDexCacheResolvedTypes = artOrigin.getDexCacheResolvedTypes(); long originDexCodeItemOffset = artOrigin.getDexCodeItemOffset(); long originDexMethodIndex = artOrigin.getDexMethodIndex(); long hookPointFromQuickCompiledCode = artHook.getEntryPointFromQuickCompiledCode(); long hookEntryPointFromJni = artHook.getEntryPointFromJni(); long hookEntryPointFromInterpreter = artHook.getEntryPointFromInterpreter(); long hookDeclaringClass = artHook.getDeclaringClass(); long hookAccessFlags = artHook.getAccessFlags(); long hookDexCacheResolvedMethods = artHook.getDexCacheResolvedMethods(); long hookDexCacheResolvedTypes = artHook.getDexCacheResolvedTypes(); long hookDexCodeItemOffset = artHook.getDexCodeItemOffset(); long hookDexMethodIndex = artHook.getDexMethodIndex(); ByteBuffer hookInfo = ByteBuffer.allocate(ART_HOOK_INFO_SIZE); hookInfo.putLong(originPointFromQuickCompiledCode); hookInfo.putLong(originEntryPointFromJni); hookInfo.putLong(originEntryPointFromInterpreter); hookInfo.putLong(originDeclaringClass); hookInfo.putLong(originAccessFlags); hookInfo.putLong(originDexCacheResolvedMethods); hookInfo.putLong(originDexCacheResolvedTypes); hookInfo.putLong(originDexCodeItemOffset); hookInfo.putLong(originDexMethodIndex); hookInfo.putLong(hookPointFromQuickCompiledCode); hookInfo.putLong(hookEntryPointFromJni); hookInfo.putLong(hookEntryPointFromInterpreter); hookInfo.putLong(hookDeclaringClass); hookInfo.putLong(hookAccessFlags); hookInfo.putLong(hookDexCacheResolvedMethods); hookInfo.putLong(hookDexCacheResolvedTypes); hookInfo.putLong(hookDexCodeItemOffset); hookInfo.putLong(hookDexMethodIndex); artOrigin.setEntryPointFromQuickCompiledCode(hookPointFromQuickCompiledCode); artOrigin.setEntryPointFromInterpreter(hookEntryPointFromInterpreter); artOrigin.setDeclaringClass(hookDeclaringClass); artOrigin.setDexCacheResolvedMethods(hookDexCacheResolvedMethods); artOrigin.setDexCacheResolvedTypes(hookDexCacheResolvedTypes); artOrigin.setDexCodeItemOffset((int) hookDexCodeItemOffset); artOrigin.setDexMethodIndex((int) hookDexMethodIndex); int accessFlags = origin.getModifiers(); if (Modifier.isNative(accessFlags)) { accessFlags &= ~ Modifier.NATIVE; artOrigin.setAccessFlags(accessFlags); } long memoryAddress = Memory.alloc(ART_HOOK_INFO_SIZE); Memory.write(memoryAddress,hookInfo.array()); artOrigin.setEntryPointFromJni(memoryAddress); return backup; }

原方法与替换的方法分别为artOrigin与artHook,执行artOrigin的backup()完成方法的备份操作,backup()内部通过反射获取AbstractMethod类的artMethod字段,然后使用当前类的method进行填充,实际的操作就是复制一份当前类的method,此处不展开它的代码。

接下来的代码是获取artOrigin与artHook的重要字段,然后构造ByteBuffer类型的hookInfo,最后调用以下三行代码来完成Hook:

long memoryAddress = Memory.alloc(ART_HOOK_INFO_SIZE); Memory.write(memoryAddress,hookInfo.array()); artOrigin.setEntryPointFromJni(memoryAddress);

ArtMethod在底层的内存结构定义仅次于Android源码的art/runtime/art_method.h文件,不同系统版本的Android这个结构体都可能会发现变化,为了保持兼容性,Legend在Java层手动定义保存了它们的字段偏移信息,与Legend/legendCore/src/main/java/com/lody/legend/art/ArtMethod.java文件保存在同一目录,在调用ArtMethod::of()方法构造ArtMethod时,会根据不同的系统版本来构造不同的对象。

Memory.write()方法底层调用的LegendNative.memput(),它是一个native方法,对应的实现是android_memput(),代码如下:

// Legend/Native/jni/legend_native.cpp void android_memput(JNIEnv * env, jclass clazz, jlong dest, jbyteArray src) { jbyte *srcPnt = env->GetByteArrayElements(src, 0); jsize length = env->GetArrayLength(src); unsigned char * destPnt = (unsigned char *)dest; for(int i = 0; i < length; ++i) { destPnt[i] = srcPnt[i]; } env->ReleaseByteArrayElements(src, srcPnt, 0); }

可以看出,馐的内存写操作是直接使用底层指定长度的字节流覆盖的,简单与暴力,而能够这样操作的原因,是当前操作的内存是自己的内存,想怎么干就怎么干!

setEntryPointFromJni()直接将原方法起始地址的指针内容,通过构造的memoryAddress覆盖写入!如此这般,Art模式下的Hook就完成了,当然,这其中很多小细节没有讲到,读者可以看行阅读它的代码。

接下来看看Dalvik下的Hook方法hookMethodDalvik()都干了啥:

// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java private static Method hookMethodDalvik(Method origin, Method hook) { DalvikMethodStruct dvmOriginMethod = DalvikMethodStruct.of(origin); DalvikMethodStruct dvmHookMethod = DalvikMethodStruct.of(hook); byte[] originClassData = dvmOriginMethod.clazz.read(); byte[] originInsnsData = dvmOriginMethod.insns.read(); byte[] originInsSizeData = dvmOriginMethod.insSize.read(); byte[] originRegisterSizeData = dvmOriginMethod.registersSize.read(); byte[] originAccessFlags = dvmOriginMethod.accessFlags.read(); byte[] originNativeFunc = dvmOriginMethod.nativeFunc.read(); byte[] hookClassData = dvmHookMethod.clazz.read(); byte[] hookInsnsData = dvmHookMethod.insns.read(); byte[] hookInsSizeData = dvmHookMethod.insSize.read(); byte[] hookRegisterSizeData = dvmHookMethod.registersSize.read(); byte[] hookAccessFlags = dvmHookMethod.accessFlags.read(); byte[] hookNativeFunc = dvmHookMethod.nativeFunc.read(); dvmOriginMethod.clazz.write(hookClassData); dvmOriginMethod.insns.write(hookInsnsData); dvmOriginMethod.insSize.write(hookInsSizeData); dvmOriginMethod.registersSize.write(hookRegisterSizeData); dvmOriginMethod.accessFlags.write(hookAccessFlags); ByteBuffer byteBuffer = ByteBuffer.allocate(DVM_HOOK_INFO_SIZE); byteBuffer.put(originClassData); byteBuffer.put(originInsnsData); byteBuffer.put(originInsSizeData); byteBuffer.put(originRegisterSizeData); byteBuffer.put(originAccessFlags); byteBuffer.put(originNativeFunc); //May leak long memoryAddress = Memory.alloc(DVM_HOOK_INFO_SIZE); Memory.write(memoryAddress, byteBuffer.array()); dvmOriginMethod.nativeFunc.write(memoryAddress); return origin; }

分析完Art模式,Dalvik下的就不难看懂的,DalvikMethodStruct.of()会返回DalvikMethodStruct类型结构体,它是Dalvik虚拟机内部DalvikMethod结构体的内线性布局表示。

dvmOriginMethod与dvmHookMethod分别代表原方法与Hook替换的方法,同样的,使用底层内存的写操作,对所有需要替换的字段进行替换。

最后就是Hook后的方法调用原方法了,它的代码如下:

// Legend/legendCore/src/main/java/com/lody/legend/HookManager.java public T callSuper(Object who, Object… args) { StackTraceElement[] traceElements = Thread.currentThread().getStackTrace(); StackTraceElement currentInvoking = traceElements[3]; String invokingClassName = currentInvoking.getClassName(); String invokingMethodName = currentInvoking.getMethodName(); Map

这段代码是在之前保存的methodNameToBackupMethodsMap中查找备份的方法,找到后对Art与Dalvik模式分别调用callSuperArt()与callSuperDalvik(),前者比较简单,只是调用方法的invoke()就完事,而Dalvik模式由于没有像Art那样做备份,所以多出了一个字段回替换的操作,完事也是调用的invoke()来执行原方法。

0x4 一些感想

分析完上面的代码,可以出来Legend尽管实现了Art与Dalvik双模式下的Hook,但在实际逆向Hook中,还是有一些不足:

不能Hook字段。在很多应用场景中可能会用到,这里有一个迂回的替代的方案是:在字段较敏感的方法中对方法做Hook,然后在Hook代码中反射操作字段。Hook自定义的类加载器加载的类方法。由于反射查找的类的方法列表依赖于类的查找,对于部分自定义ClassLoader的情况,获取Class本身就存在着难度,更别说Hook它的方法了。兼容性。只支持4.2到6.0,当然,根据技术原理,从2.3到7.1应该都是可以做到的。稳定性。与该框架技术原理类似的还有很多,比较alibaba的AndFix,在系统自定义修改较多的情况下,框加要的稳定性存疑,当然,逆向工程时使用的稳定性远没有做产品要求的高,一些全新思路的Hook修改方案如Tinker可能也是一个不错的选择,留待以后测试了!

最后,讲完了它的原理,并没有讲如何在逆向工程中使用,这个交给聪明的安全研究人员作为思维发散。

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!插入图片请使用ssl
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容