Dexposed框架可以用来实现热补丁功能和AOP编程功能,在AOP编程功能的基础上我们可以实现事件自动埋点、程序性能监控和日志记录等等功能。整个Dexposed框架中最为重要的功能点是实现Java方法的hook,可以说Java方法的hook是Dexposed框架实现热补丁功能和AOP编程框架的基石。而Dexposed框架通过修改虚拟机内部描述Java方法的数据结构成员的值来实现Java方法的Hook,为此整个框架必须针对Android系统不同虚拟机和同一种虚拟机的不同本版进行适配。
AOP框架的基本思路是在特定的Java方法被调用的时候提供预处理方法和后处理方法。在预处理方法内可以进行事件拦截、日志记录操作,而在后处理方法内可以进行性能统计这类操作。在Dexposed框架中,预处理方法和后处理方法的调用是通过Hook指定的Java方法,将程序对被Hook方法的调用转派到框架指定的一个native函数,该函数实现了预处理方法和后处理方法的调用。
热补丁框架一般来说由补丁程序和宿主程序两部分组成,为此热补丁框架需要定义补丁程序和宿主程序的通讯协议。通讯协议必须能够明确补丁程序想要替换宿主程序内哪些类和哪些方法并且规定了补丁程序编写时应该遵循的规范,而对于宿主程序来说必须要实现指定类或则方法的替换。在Dexposed框架内宿主程序和补丁程序通讯协议的实现是定义在patchloader.jar内,宿主程序实现特定方法的替换则是实现在DexposedBridge.jar内。
由于整个框架的的核心是Java方法的hook,所以我们先来了解一下dexposed框架内如何hook一个Java方法。如果读者学习使用过Xposed框架的话,那么对于Dexposed框架hook Java方法的API应该十分熟悉。
下面是一段Dexposed框架hook Java方法的实例代码片段:
DexposedBridge.findAndHookMethod(cls, "showDialog", new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
builder.setTitle("Dexposed sample")
.setMessage("The dialog is shown from patch apk!")
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).create().show();
return null;
}
});
DexposedBridge类提供了静态方法findAndHookMethod()方法来hook指定的Java方法。该方法的第一个参数是被hook方法所在类的类信息,第二个参数是需要hook的Java方法的名字,接下来是一个参数列表用来传递该Java方法的参数类型列表,而findAndHookMethod()方法最后一个参数必须是Dexposed框架提供的一个回调接口。通过实例化不同回调对象实现Java方法的替换功能或则添加预处理代码和后处理代码功能。
在使用Dexposed框架实现Java方法hook的时候,接触比较多的是XC_MethodHook和XC_MethodReplacement这两个回调抽象类。下面将对这个回调类的基本实现进行简单的介绍:
XC_MethodHook回调类一般用于实现AOP编程,该类提供了beforeHookedMethod()
和afterHookedMethod()
两个方法作为指定方法的预处理方法和后处理方法。在beforeHookedMethod()
方法内可以实现事件拦截、事件埋点这类操作,而在afterHookedMethod()
方法内可以实现一些诸如资源释放、数据统计之类的操作。
XC_MethodReplacement回调类一般用于实现热补丁功能,该类继承至XC_MethodHook。XC_MethodReplacement提供了replaceHookedMethod()方法用于实现被hook的Java方法的功能的替换。同时该类默认实现了beforeHookedMethod()
和afterHookedMethod()
方法。beforeHookedMethod()
通过调用replaceHookedMethod()
获取用户设置的返回结果后调用参数MethodHookParam的setResult()
阻止框架调用被hook的Java方法。
通过以上对Java方法hook的介绍,我们知道可以在补丁程序中通过DexposedBridge类的findAndHookMethod()方法指定我们需要替换宿主程序中的特定类的方法。下面是补丁程序实现的基本代码片段:
public class DialogPatch implements IPatch {
@Override
public void handlePatch(final PatchParam arg0) throws Throwable {
Class> cls = null;
try {
cls = arg0.context.getClassLoader()
.loadClass("com.taobao.dexposed.MainActivity");
} catch (ClassNotFoundException e) {
e.printStackTrace();
return;
}
DexposedBridge.findAndHookMethod(cls, "showDialog",
new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
Activity mainActivity = (Activity) param.thisObject;
AlertDialog.Builder builder = new AlertDialog.Builder(mainActivity);
builder.setTitle("Dexposed sample")
.setMessage("The dialog is shown from patch apk!")
.setPositiveButton("ok", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).create().show();
return null;
}
});
}
}
实现补丁程序的基本步骤是定义一系列的补丁类,在这些补丁类内指定我们要替换的宿主程序内的方法。现在将实现补丁程序的基本步骤总结如下:
在实现补丁程序的时候有两个基本的难点,如何获取宿主程序中需要被hook的Java类的类信息和如何取得将被hook的Java方法的实参。这两个基本问题都可以通过通讯协议框架提供的数据得以实现。宿主程序在加载补丁程序的时候会通过IPatch接口的handlePatch()方法传递宿主程序的Context,补丁程序可以从handlePatch()方法的参数PatchParam获取到宿主程序的Context进而获取到宿主程序的ClassLoader和类信息。宿主程序在实现Java方法的hook的时候会通过回调类XC_MethodReplacement的方法replaceHookedMethod()
参数MethodHookParam传递被hook的Java方法运行时的实参。
MethodHookParam参数的成员如下:
public static class MethodHookParam extends Param {
public Member method; // 被hook方法的描述
public Object thisObject; // 被hook的类的当前对象,静态方法该成员值为空
public Object[] args; // 被hook的方法调用是的实参列表
private Object result = null; // 被hook的方法的返回值
private Throwable throwable = null;
boolean returnEarly = false;
.......
}
从以上MethodHookParam的定义可以看出,我们在实现补丁逻辑的时候可以通过该参数获取到当前的实例、实参列表这些基本的信息。在实现补丁程序的大多数时候都需要使用到被hook的Java方法的实参进行逻辑上的修改,所以MethodHookParam携带的参数完美的解决了这部分需求。
注意: 补丁程序需要引用到patchloader.jar和dexosedbridge.jar中的类和接口,但是这两个库是打包在宿主程序中而非补丁程序中,所以在打包补丁程序的时候不能将这两个库打包进入补丁程序
补丁框架源码集中在patchloader.jar的实现上,这部分代码的实现比较简洁省略了部分安全方面的校验,在实际项目中需要根据具体情况添加相应的安全校验功能。补丁框架要解决的基本问题可以归纳为以下几个:
1). 定义宿主程序和补丁程序通讯的接口
2). 宿主程序如何加载补丁程序和识别出补丁类
3). 宿主程序如何识别补丁程序的身份和校验数据的完整性
宿主程序和补丁程序通讯的接口应该包括宿主程序传递给补丁程序的数据的描述、补丁程序返回给宿主程序的响应信息的描述和宿主程序调用补丁程序中补丁类的约定方法。宿主程序传递给补丁程序的数据包括宿主程序的上下文、被hook的Java方法运行时需要访问的宿主程序类或则实例。补丁程序对宿主程序响应的信息包括补丁操作的结果、出现的错误和异常信息。由于补丁程序中的补丁类实现有多种,所以必须定义这些补丁类实现统一的接口。实现按接口编程可以方便宿主程序调用补丁程序中补丁类的操作。
在Dexposed框架中这三个数据的描述如下:
public class PatchParam {
protected final Object[] callbacks;
public PatchParam(ReadWriteSet callbacks) {
this.callbacks = callbacks.getSnapshot();
}
/**
* The context can be get by patch class. The most important is offer classloader.
*/
public Context context;
/**
* This map contains objects that may be used by patch class.
*/
public HashMap contentMap;
}
public class PatchResult {
private boolean result;
private int erroCode;
private String ErrorInfo;
private Throwable throwable;
/**
* Success
*/
public static int NO_ERROR = 0;
/**
* This device is not support.
*/
public static int DEVICE_UNSUPPORT = 1;
/**
* Exception happened during System.loadLibrary loading so.
*/
public static int LOAD_SO_EXCEPTION = 2;
/**
* The dvm crashed during loading so at last time, so it tell this crash if try to load again.
*/
public static int LOAD_SO_CRASHED = 3;
/**
* Please check the hotpatch file path if correct.
*/
public static int FILE_NOT_FOUND = 4;
/**
* Exception happened during loading patch classes.
*/
public static int FOUND_PATCH_CLASS_EXCEPTION = 5;
/**
* The hotpatch apk doesn't include some classes to patch.
*/
public static int NO_PATCH_CLASS_HANDLE = 6;
/**
* All patched classes run failed. Please check them if correct.
*/
public static int ALL_PATCH_FAILED = 7;
public PatchResult(boolean isSuccess, int code, String info) {
this.result = isSuccess;
this.erroCode = code;
this.ErrorInfo = info;
}
public PatchResult(boolean isSuccess, int code, String info, Throwable t) {
this.result = isSuccess;
this.erroCode = code;
this.ErrorInfo = info;
this.throwable = t;
}
public boolean isSuccess() {
return this.result;
}
public int getErrocode() {
return this.erroCode;
}
public String getErrorInfo() {
return this.ErrorInfo;
}
public Throwable getThrowbale() {
return this.throwable;
}
}
package com.taobao.patch;
/**
* The interface implemented by hotpatch classes.
*/
public interface IPatch {
void handlePatch(PatchParam lpparam) throws Throwable;
}
在大多数情况下补丁程序以apk或则包含dex文件的jar包形式出现,所以宿主程序加载补丁程序的问题归根到底是在Android中如何加载一个Dex文件的问题。在Android中加载一个未安装的Dex文件的技术手段基本上是通过实例化一个DexFile对象来完成,一旦有了补丁程序对应的DexFile实例,我们就可以获取到补丁文件内的类的名字,通过该名字可以使用DexClassLoader就可以加载补丁程序中指定的类信息了。在加载完补丁程序中的类信息就可以判断类是否是补丁类,如果该类确认是补丁类那么可以直接实例化一个该类的对象并调用该补丁类的hook逻辑。
宿主程序通过调用框架中的PatchMain类的load()方法实现对补丁文件类的加载。基本代码片段如下:
/**
* Load a runnable patch apk.
*
* @param context the application or activity context.
* @param apkPath the path of patch apk file.
* @param contentMap the object maps that will be used by patch classes.
* @return PatchResult include if success or error detail.
*/
public static PatchResult load(Context context, String apkPath, HashMap contentMap) {
if (!new File(apkPath).exists()) {
return new PatchResult(false, PatchResult.FILE_NOT_FOUND, "FILE not found on " + apkPath);
}
PatchResult result = loadAllCallbacks(context, apkPath, context.getClassLoader());
if (!result.isSuccess()) {
return result;
}
if (loadedPatchCallbacks.getSize() == 0) {
return new PatchResult(false, PatchResult.NO_PATCH_CLASS_HANDLE, "No patch class to be handle");
}
PatchParam lpparam = new PatchParam(loadedPatchCallbacks);
lpparam.context = context;
lpparam.contentMap = contentMap;
return PatchCallback.callAll(lpparam);
}
private static PatchResult loadAllCallbacks(Context context, String apkPath, ClassLoader cl) {
try {
// String dexPath = new File(context.getFilesDir(), apkPath.).getAbsolutePath();
File dexoptFile = new File(apkPath + "odex");
if (dexoptFile.exists()) {
dexoptFile.delete();
}
ClassLoader mcl = null;
try {
mcl = new DexClassLoader(apkPath, context.getFilesDir().getAbsolutePath(), null, cl);
} catch (Throwable e) {
return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
}
DexFile dexFile = DexFile.loadDex(apkPath, context.getFilesDir().getAbsolutePath() + File.separator + "patch.odex", 0);
Enumeration entrys = dexFile.entries();
// clean old callback
synchronized (loadedPatchCallbacks) {
loadedPatchCallbacks.clear();
}
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
Class> entryClass = null;
try {
entryClass = mcl.loadClass(entry);
} catch (ClassNotFoundException e) {
e.printStackTrace();
break;
}
if (isImplementInterface(entryClass, IPatch.class)) {
Object moduleInstance = entryClass.newInstance();
hookLoadPatch(new PatchCallback((IPatch) moduleInstance));
}
}
} catch (Exception e) {
return new PatchResult(false, PatchResult.FOUND_PATCH_CLASS_EXCEPTION, "Find patch class exception ", e);
}
return new PatchResult(true, PatchResult.NO_ERROR, "");
}
在Dexposed框架中没有提供补丁程序身份识别和数据完整性的校验,但是在实际项目使用时必须考虑这个问题。补丁身份的识别和数据完整性可以通过签名来实现,确保补丁程序是应用厂商发布的并且没有被第三方篡改。对于这部分的实现可以使用JCA Provider提供的签名类进行签名,在更高要求的情况下可以考虑使用对称加密算法进行加密和解密。
整个hook框架的基本原理是通过修改指定Java方法在虚拟机内部描述的数据结构字段的值来实现的,通过修改值将虚拟机对指定Java方法的调用改为对指定的本地函数的调用。在指定的分派函数中可以实现对原来Java方法逻辑的修改,方法返回结果的替换这些操作。Hook框架由dexposedbridge.jar和一系列动态库构成,其中dexposedbridge.jar定义了框架hook API规范并根据需要与动态库进行交互,而动态库则是实现数据结构值修改和本地派发函数的核心。
Hook框架的Java层代码定义了hook API,规定了应用程序如何进行特定类的指定方法的hook。整个Java层代码除了包含hook API的定义,还包括了框架运行环境的检查、被hook类和被hook方法的解析以及与native层的交互逻辑。
检查运行环境模块的代码实现在DeviceCheck.java中,包括检查系统运行的虚拟机类型、当前的系统版本、系统使用的处理器类型这些内容。其中检查虚拟机类型是通过获取“android.os.SystemProperties”系统属性来判断,检查系统使用的处理器类型是通过“getprop ro.product.cpu.abi”属性来判断而当前运行的系统版本则是通过系统API来判断。
private static String getCurrentRuntimeValue() {
try {
Class> systemProperties = Class
.forName("android.os.SystemProperties");
try {
Method get = systemProperties.getMethod("get", String.class,
String.class);
if (get == null) {
return "WTF?!";
}
try {
final String value = (String) get.invoke(systemProperties,
SELECT_RUNTIME_PROPERTY,
/* Assuming default is */"Dalvik");
if (LIB_DALVIK.equals(value)) {
return "Dalvik";
} else if (LIB_ART.equals(value)) {
return "ART";
} else if (LIB_ART_D.equals(value)) {
return "ART debug build";
}
return value;
} catch (IllegalAccessException e) {
return "IllegalAccessException";
} catch (IllegalArgumentException e) {
return "IllegalArgumentException";
} catch (InvocationTargetException e) {
return "InvocationTargetException";
}
} catch (NoSuchMethodException e) {
return "SystemProperties.get(String key, String def) method is not found";
}
} catch (ClassNotFoundException e) {
return "SystemProperties class is not found";
}
}
private static boolean isYunOS() {
String s1 = null;
String s2 = null;
try {
Method m = Class.forName("android.os.SystemProperties")
.getMethod("get", String.class);
s1 = (String) m.invoke(null, "ro.yunos.version");
s2 = (String) m.invoke(null, "java.vm.name");
} catch (NoSuchMethodException a) {
} catch (ClassNotFoundException b) {
} catch (IllegalAccessException c) {
} catch (InvocationTargetException d) {
}
if ((s2 != null && s2.toLowerCase().contains("lemur")) || (s1 != null && s1.trim().length() > 0)) {
return true;
} else {
return false;
}
}
private static boolean isX86CPU() {
Process process = null;
String abi = null;
InputStreamReader ir = null;
BufferedReader input = null;
try {
process = Runtime.getRuntime().exec("getprop ro.product.cpu.abi");
ir = new InputStreamReader(process.getInputStream());
input = new BufferedReader(ir);
abi = input.readLine();
if (abi.contains("x86")) {
return true;
}
} catch (Exception e) {
} finally {
if (input != null) {
try {
input.close();
} catch (Exception e) {
}
}
if (ir != null) {
try {
ir.close();
} catch (Exception e) {
}
}
if (process != null) {
try {
process.destroy();
} catch (Exception e) {
}
}
}
return false;
}
private static boolean isSupportSDKVersion() {
if (android.os.Build.VERSION.SDK_INT >= 14 && android.os.Build.VERSION.SDK_INT < 20) {
return true;
} else if (android.os.Build.VERSION.SDK_INT == 10 || android.os.Build.VERSION.SDK_INT == 9) {
return true;
}
return false;
}
解析类和方法模块使用到了反射机制,基本的原理就是利用反射的获取Java的Method对象。不过在该模块中实现了用户友好的方式获取指定的Method实例,可以通过用户习惯的表达方式获取到指定的类信息和方法信息。
该模块定义了用户hook Java方法回调的接口,用户通过给hook API传递该类型的参数实现业务逻辑的定制。整个回调体系由两个基本的基类派生出来,这两个基类分别定义了hook Java方法的回调接口和unhook Java方法的回调接口。用户经常使用到的回调接口一般是继承至这两个基本类,使用比较多的子类是XC_MethodHook和XC_MethodReplacement。
整个回调体系中的基本基类是IXUnhook和XCallback。IXUnhook定义了unhook Java方法的回调,一般在该回调中调用DexposedBridge的unhook逻辑进行处理。XCallback是所有hook Java方法回调接口的基类,不过在XCallback类中仅仅定义了接口的优先级和接口回调方法的参数,参数主要用来保存原先Java方法运行实参。
package com.taobao.android.dexposed.callbacks;
public interface IXUnhook {
void unhook();
}
public abstract class XCallback implements Comparable<XCallback> {
public final int priority;
......
......
public static class Param {
public final Object[] callbacks;
private Bundle extra;
......
}
public static final int PRIORITY_DEFAULT = 50;
/**
* Call this handler last
*/
public static final int PRIORITY_LOWEST = -10000;
/**
* Call this handler first
*/
public static final int PRIORITY_HIGHEST = 10000;
}
XCallback的直接子类是XC_MethodHook,该类定义了两个特殊的方法预处理方法和后处理方法。而XC_MethodHook的直接子类是XC_MethodReplacement,该类对继承至XC_MethodHook的beforeHookedMethod()进行重写,通过在该方法内调用replaceHookedMethod()方法替换被Hook Java方法的调用。对于这两个回调接口的介绍已经足够充分,所以在这里不再进行赘述。
Dexposed框架提供了友好的API方便开发者进行Java方法的hook。该模块的实现代码集中在DexposedBridge这个类中,在该类中定义了一系列API供开发者使用,同时该类也定义了一系列native方法实现底层数据结构的修改。动态库中的本地分派函数也会调用到该类的handleHookedMethod()方法实现用户回调接口的回调和被hook Java方法的调用。
Hook API列表包括hook特定Java方法的API、hook特定类构造器的API、unhook指定方法的API和在特定Java方法被hook的情况下调用被hook Java方法的逻辑的API。
hook特定Java方法的API
这一类API大致有三个,他们分别是hookMethod() 、 hookAllMethods()和findAndHookMethod()。其中hookMethod()是Java层最基本的hook函数,其他所有实现hook功能的API最终都要调用到该方法。
1). hookMethod()方法基本逻辑很简单,实现了将参数传递进来的回调接口保存到一个HashMap中列表内,同时根据参数Method获取方法的参数类型列表和返回值类型。在获得这些类型信息后将调用native方法实现虚拟机内部数据结构字段值的修改。
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor>)) {
throw new IllegalArgumentException("only methods and constructors can be hooked");
}
boolean newMethod = false;
CopyOnWriteSortedSet callbacks;
synchronized (hookedMethodCallbacks) {
callbacks = hookedMethodCallbacks.get(hookMethod);
if (callbacks == null) {
callbacks = new CopyOnWriteSortedSet();
hookedMethodCallbacks.put(hookMethod, callbacks);
newMethod = true;
}
}
callbacks.add(callback);
if (newMethod) {
Class> declaringClass = hookMethod.getDeclaringClass();
if (runtime == RUNTIME_UNKNOW) runtime = getRuntime();
int slot = (runtime == RUNTIME_DALVIK) ? (int) getIntField(hookMethod, "slot") : 0;
Class>[] parameterTypes;
Class> returnType;
if (hookMethod instanceof Method) {
parameterTypes = ((Method) hookMethod).getParameterTypes();
returnType = ((Method) hookMethod).getReturnType();
} else {
parameterTypes = ((Constructor>) hookMethod).getParameterTypes();
returnType = null;
}
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(
callbacks, parameterTypes, returnType);
hookMethodNative(hookMethod, declaringClass,
slot, additionalInfo);
}
return callback.new Unhook(hookMethod);
}
2). hookAllMethods()方法
该方法通过遍历参数指定类的所有方法,筛选出名字同参数指定名字一样的方法。符合特定名称的方法将都会通过调用hookMethod()方法修改其在虚拟机内部的数据结构,在调用hookMethod()方法后都将返回一个Unhook接口,该接口也将会被缓存到一个列表中。
3). findAndHookMethod()方法
该方法在前面已经介绍过了,针对的是指定类的特定方法进行hook。在分离出参数传递进来的回调接口、参数列表后将直接通过hookMethod()方法修改虚拟机内部的数据结构,最后也会将hookMethod()方法返回的Unhook接口缓存到列表内。
public static XC_MethodHook.Unhook findAndHookMethod(Class> clazz,
String methodName, Object... parameterTypesAndCallback) {
if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length - 1]
instanceof XC_MethodHook))
throw new IllegalArgumentException("no callback defined");
XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length - 1];
Method m = XposedHelpers.findMethodExact(clazz, methodName, parameterTypesAndCallback);
XC_MethodHook.Unhook unhook = hookMethod(m, callback);
if (!(callback instanceof XC_MethodKeepHook
|| callback instanceof XC_MethodKeepReplacement)) {
synchronized (allUnhookCallbacks) {
allUnhookCallbacks.add(unhook);
}
}
return unhook;
}
hook特定类构造器的API
hook特定类构造器的API其实就是hookAllConstructors()方法,该方法通过遍历参数指定的类的构造器并一个个进行hook.
public static Set hookAllConstructors(
Class> hookClass, XC_MethodHook callback) {
Set unhooks = new HashSet<
XC_MethodHook.Unhook>();
for (Member constructor : hookClass.getDeclaredConstructors())
unhooks.add(hookMethod(constructor, callback));
return unhooks;
}
unhook指定API实际上就是将以上hook回调接口从缓存列表中移除,防止在被hook的Java方法运行时调用到这些回调的内容。
invokeOriginalMethod()比较简单,通过调用native方法invokeOriginalMethodNative()实现被hook Java方法原先逻辑的调用。
public static Object invokeOriginalMethod(Member method, Object thisObject, Object[] args)
throws NullPointerException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
if (args == null) {
args = EMPTY_ARRAY;
}
Class>[] parameterTypes;
Class> returnType;
if (method instanceof Method) {
parameterTypes = ((Method) method).getParameterTypes();
returnType = ((Method) method).getReturnType();
} else if (method instanceof Constructor) {
parameterTypes = ((Constructor>) method).getParameterTypes();
returnType = null;
} else {
throw new IllegalArgumentException("method must be of type Method or Constructor");
}
return invokeOriginalMethodNative(method, 0, parameterTypes, returnType, thisObject, args);
}
hookMethodNative() 修改被Hook Java方法在虚拟机内部描述的数据结构的值
invokeOriginalMethodNative() 实现被hook Java方法原先逻辑的调用
invokeSuperNative() 调用被hook Java方法父类的方法
handleHookedMethod()是native层分派函数回调到Java层的方法。该方法主要作用是实现hook回调接口的调用,根据是否还有回调接口注册决定是否调用被hook Java代码原先的逻辑还是直接调用回调接口的beforeHookedMethod()方法。在调用了beforeHookedMethod()后根据该方法返回的结果决定是否调用被hook Java方法原先的逻辑还是afterHookedMethod()方法。
private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,
Object thisObject, Object[] args) throws Throwable {
AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;
Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();
final int callbacksLength = callbacksSnapshot.length;
if (callbacksLength == 0) {
try {
return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,
additionalInfo.returnType, thisObject, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}
MethodHookParam param = new MethodHookParam();
param.method = method;
param.thisObject = thisObject;
param.args = args;
// call "before method" callbacks
int beforeIdx = 0;
do {
try {
((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);
} catch (Throwable t) {
log(t);
// reset result (ignoring what the unexpectedly exiting callback did)
param.setResult(null);
param.returnEarly = false;
continue;
}
if (param.returnEarly) {
// skip remaining "before" callbacks and corresponding "after" callbacks
beforeIdx++;
break;
}
} while (++beforeIdx < callbacksLength);
// call original method if not requested otherwise
if (!param.returnEarly) {
try {
param.setResult(invokeOriginalMethodNative(method, originalMethodId,
additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));
} catch (InvocationTargetException e) {
param.setThrowable(e.getCause());
}
}
// call "after method" callbacks
int afterIdx = beforeIdx - 1;
do {
Object lastResult = param.getResult();
Throwable lastThrowable = param.getThrowable();
try {
((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);
} catch (Throwable t) {
DexposedBridge.log(t);
// reset to last result (ignoring what the unexpectedly exiting callback did)
if (lastThrowable == null)
param.setResult(lastResult);
else
param.setThrowable(lastThrowable);
}
} while (--afterIdx >= 0);
// return
if (param.hasThrowable())
throw param.getThrowable();
else
return param.getResult();
}
框架的native层代码是实现整个hook逻辑的核心,在动态库中实现指定Java方法类型的修改、Java方法本地函数的指派,分派函数方法指派这些逻辑。总的来说,整个动态库的执行流程可以划分成三个部分。动态库加载进虚拟机时,虚拟机工具函数的初始化,Java本地方法的动态注册。在调用Java hook API时,修改虚拟机内部数据结构,改变函数执行流程的过程。最后在业务逻辑调用到被hook Java方法是,方法执行流程的改变。
下面是动态库的三个模块的基本介绍(Dalvik虚拟机为例):
动态库在加载进虚拟机的时候会调用JNI_OnLoad()函数,在该函数中可以进行一些初始化工作。在Dexposed的动态库中进行了trace函数指针的初始化、检查系统信息参数、检查系统运行的虚拟机类型、修改虚拟机检查函数的返回值、动态注册DexposedBridge的native方法、获取DexposedBridge类在native的引用和DexposedBridge类方法在native的引用。
void initTypePointers()
{
char sdk[PROPERTY_VALUE_MAX];
const char *error;
property_get("ro.build.version.sdk", sdk, "0");
RUNNING_PLATFORM_SDK_VERSION = atoi(sdk);
dlerror();
if (RUNNING_PLATFORM_SDK_VERSION >= 18) {
*(void **) (&PTR_atrace_set_tracing_enabled) = dlsym(RTLD_DEFAULT, "atrace_set_tracing_enabled");
if ((error = dlerror()) != NULL) {
ALOGE("Could not find address for function atrace_set_tracing_enabled: %s", error);
}
}
}
检查系统参数通过调用native framework提供的系统属性获取函数获取sdk、release、厂商这些信息并通过日志打印出来。
void dexposedInfo() {
char release[PROPERTY_VALUE_MAX];
char sdk[PROPERTY_VALUE_MAX];
char manufacturer[PROPERTY_VALUE_MAX];
char model[PROPERTY_VALUE_MAX];
char rom[PROPERTY_VALUE_MAX];
char fingerprint[PROPERTY_VALUE_MAX];
property_get("ro.build.version.release", release, "n/a");
property_get("ro.build.version.sdk", sdk, "n/a");
property_get("ro.product.manufacturer", manufacturer, "n/a");
property_get("ro.product.model", model, "n/a");
property_get("ro.build.display.id", rom, "n/a");
property_get("ro.build.fingerprint", fingerprint, "n/a");
LOGI("Starting Dexposed binary version %s, compiled for SDK %d\n", DEXPOSED_VERSION, PLATFORM_SDK_VERSION);
ALOGD("Phone: %s (%s), Android version %s (SDK %s)\n", model, manufacturer, release, sdk);
ALOGD("ROM: %s\n", rom);
ALOGD("Build fingerprint: %s\n", fingerprint);
}
检查虚拟机类型主要是通过获取”persist.sys.dalvik.vm.lib”参数判断是否是实现Dalvik虚拟机功能的动态库。
bool isRunningDalvik() {
if (RUNNING_PLATFORM_SDK_VERSION < 19)
return true;
char runtime[PROPERTY_VALUE_MAX];
property_get("persist.sys.dalvik.vm.lib", runtime, "");
if (strcmp(runtime, "libdvm.so") != 0) {
ALOGE("Unsupported runtime library %s, setting to libdvm.so", runtime);
return false;
} else {
return true;
}
}
框架在访问虚拟机内部的数据结构时虚拟机会调用一系列检查方法,只有在检查通过后才能访问这些基本的数据结构。在这里框架主要是修改了类信息数据结构、方法数据结构和成员变量数据结构的访问检查方法的返回值,动态库通过定位到这些方法在代码区的位置后修改代码区块的访问属性为可读写进而在指定位置写入数据达到修改这些函数的返回值的目的。同时在该函数中动态注册了DexposedBridge的native方法hookMethod()和获取DexposedBridge类在native层的引用。
bool dexposedOnVmCreated(JNIEnv* env, const char* className) {
keepLoadingDexposed = keepLoadingDexposed && dexposedInitMemberOffsets(env);
if (!keepLoadingDexposed)
return false;
// disable some access checks
patchReturnTrue((uintptr_t) &dvmCheckClassAccess);
patchReturnTrue((uintptr_t) &dvmCheckFieldAccess);
patchReturnTrue((uintptr_t) &dvmInSamePackage);
patchReturnTrue((uintptr_t) &dvmCheckMethodAccess);
env->ExceptionClear();
dexposedClass = env->FindClass(DEXPOSED_CLASS);
dexposedClass = reinterpret_cast(env->NewGlobalRef(dexposedClass));
if (dexposedClass == NULL) {
ALOGE("Error while loading Dexposed class '%s':\n", DEXPOSED_CLASS);
dvmLogExceptionStackTrace();
env->ExceptionClear();
return false;
}
ALOGI("Found Dexposed class '%s', now initializing\n", DEXPOSED_CLASS);
if (register_com_taobao_android_dexposed_DexposedBridge(env) != JNI_OK) {
ALOGE("Could not register natives for '%s'\n", DEXPOSED_CLASS);
return false;
}
return true;
}
该函数通过JNIEnv获取DexposedBridge方法在native的引用,同时为DexposedBridge的部分native方法建立映射。
static void com_taobao_android_dexposed_DexposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,
jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {
// Usage errors?
if (declaredClassIndirect == NULL || reflectedMethodIndirect == NULL) {
dvmThrowIllegalArgumentException("method and declaredClass must not be null");
return;
}
// Find the internal representation of the method
ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);
Method* method = dvmSlotToMethod(declaredClass, slot);
if (method == NULL) {
dvmThrowNoSuchMethodError("could not get internal representation for method");
return;
}
if (dexposedIsHooked(method)) {
// already hooked
return;
}
// Save a copy of the original method and other hook info
DexposedHookInfo* hookInfo = (DexposedHookInfo*) calloc(1, sizeof(DexposedHookInfo));
memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));
hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));
hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));
// Replace method with our own code
SET_METHOD_FLAG(method, ACC_NATIVE);
method->nativeFunc = &dexposedCallHandler;
method->insns = (const u2*) hookInfo;
method->registersSize = method->insSize;
method->outsSize = 0;
if (PTR_gDvmJit != NULL) {
// reset JIT cache
MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;
}
}
修改被hook Java方法在虚拟机内部数据结构的值是整个框架最为核心的部分,该函数是DexposedBridge类的hookMethod()方法在native映射的本地函数。DexposedBridge的hookMethod()方法和本地函数com_taobao_android_dexposed_DexposedBridge_hookMethodNative()的映射关系的建立是在刚才描述的初始化模块中进行的。
该本地函数的核心功能是获取被Hook Java方法在虚拟机内部描述数据结构Method,修改该结构体中access Flag为ACC_NATIVE(本地方法),设置Method结构体的分派函数指针、设置分派函数的实际参数数据。
{
// Save a copy of the original method and other hook info
DexposedHookInfo* hookInfo = (DexposedHookInfo*) calloc(1, sizeof(DexposedHookInfo));
memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));
hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(),
env->NewGlobalRef(reflectedMethodIndirect));
hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(),
env->NewGlobalRef(additionalInfoIndirect));
// Replace method with our own code
SET_METHOD_FLAG(method, ACC_NATIVE);
method->nativeFunc = &dexposedCallHandler;
method->insns = (const u2*) hookInfo;
method->registersSize = method->insSize;
method->outsSize = 0;
}
由于在调用到hook API时,虚拟机内部描述方法的数据结构的值已经被修改过。被hook的Java方法在虚拟机内部被改成native方法,同时也指定了被hook的Java方法替代的分派函数。所以当应用程序执行到被hook的Java方法时实际将会调用到分派函数。而该分派函数则定义了整个hook过程函数调用流程。
该分派函数也就是dexposedCallHandler()函数,该函数根据被hook Java方法对应的Method成员的值,构造出一个ArrayObject对象保存实际参数。再以这些参数调用DexposedBridge的handleHookedMethod()方法,在该Java方法中实现hook回调接口的回调和被hook Java方法原先逻辑的调用。
static void dexposedCallHandler(const u4* args, JValue* pResult, const Method* method, ::Thread* self) {
if (!dexposedIsHooked(method)) {
dvmThrowNoSuchMethodError("could not find Dexposed original method - how did you even get here?");
return;
}
DexposedHookInfo* hookInfo = (DexposedHookInfo*) method->insns;
Method* original = (Method*) hookInfo;
Object* originalReflected = hookInfo->reflectedMethod;
Object* additionalInfo = hookInfo->additionalInfo;
// convert/box arguments
const char* desc = &method->shorty[1]; // [0] is the return type.
Object* thisObject = NULL;
size_t srcIndex = 0;
size_t dstIndex = 0;
// for non-static methods determine the "this" pointer
if (!dvmIsStaticMethod(original)) {
thisObject = (Object*) args[0];
srcIndex++;
}
ArrayObject* argsArray = dvmAllocArrayByClass(objectArrayClass, strlen(method->shorty) - 1, ALLOC_DEFAULT);
if (argsArray == NULL) {
return;
}
while (*desc != '\0') {
char descChar = *(desc++);
JValue value;
Object* obj;
switch (descChar) {
case 'Z':
case 'C':
case 'F':
case 'B':
case 'S':
case 'I':
value.i = args[srcIndex++];
obj = (Object*) dvmBoxPrimitive(value, dvmFindPrimitiveClass(descChar));
dvmReleaseTrackedAlloc(obj, self);
break;
case 'D':
case 'J':
value.j = dvmGetArgLong(args, srcIndex);
srcIndex += 2;
obj = (Object*) dvmBoxPrimitive(value, dvmFindPrimitiveClass(descChar));
dvmReleaseTrackedAlloc(obj, self);
break;
case '[':
case 'L':
obj = (Object*) args[srcIndex++];
break;
default:
ALOGE("Unknown method signature description character: %c\n", descChar);
obj = NULL;
srcIndex++;
}
dexposedSetObjectArrayElement(argsArray, dstIndex++, obj);
}
// call the Java handler function
JValue result;
dvmCallMethod(self, dexposedHandleHookedMethod, NULL, &result,
originalReflected, (int) original, additionalInfo, thisObject, argsArray);
dvmReleaseTrackedAlloc((Object *)argsArray, self);
// exceptions are thrown to the caller
if (dvmCheckException(self)) {
return;
}
// return result with proper type
ClassObject* returnType = dvmGetBoxedReturnType(method);
if (returnType->primitiveType == PRIM_VOID) {
// ignored
} else if (result.l == NULL) {
if (dvmIsPrimitiveClass(returnType)) {
dvmThrowNullPointerException("null result when primitive expected");
}
pResult->l = NULL;
} else {
if (!dvmUnboxPrimitive((Object *)result.l, returnType, pResult)) {
dvmThrowClassCastException(((Object *)result.l)->clazz, returnType);
}
}
}