1. 创建Xposed工程
在Android Studio中新建一个app工程,修改其中的 AndroidManifest.xml 文件,在<application></application>标签中增加如下代码
<meta-dataandroid:name="xposedmodule"android:value="true" />
<meta-dataandroid:name="xposeddescription"android:value="hello xposed" />
<meta-dataandroid:name="xposedminversion"android:value="82" />
在 app/libs 目录下添加Xposed的依赖库,api-82.jar, api-82-sources.jar(网上自行搜索)
修改 app 目录下的 build.gradle 文件,将以上的两个依赖库添加到 dependencies{} 中,如下
dependencies {compileOnly 'de.robv.android.xposed:api:82'compileOnly 'de.robv.android.xposed:api:82:sources'// other content......
}
在 mian 目录下创建一个assets文件夹,并在assets目录下创建一个 xposed_init 文件,文件中写入实现了 IXposedHookLoadPackage 接口的类名及包名;例如我当前新建了一个类 TestReverse 实现了 IXposedHookLoadPackage 接口,且当前包名为 com.test.xposed,则 xposed_init 中的内容应为
com.test.xposed.TestReverse
2. 相关API介绍
Xposed 模块API用法:XposedHelpers | Xposed Framework API
a.IXposedHookLoadPackage 接口,实现hook逻辑的类必须实现该接口,APP被加载的时候会调用该接口中的 handleLoadPackage(final XC_LoadPackage.LoadPackageParam lpparam) 方法,所以我们需要重写该函数,并在其中实现我们的hook逻辑
b.XC_LoadPackage.LoadPackageParam,该类下包含与正在加载的应用程序的有关信息。入下,其中packageName可以用于判断当前hook住的APP名称,processName表示当前APP所在的进程,classLoader表示当前APP使用的ClassLoader。
c.XposedHelpers类
1.findClass(String className, ClassLoader classLoader),使用特定的ClassLoader查找指定类,返回值为指定类的class;使用如下
final Class<?> callContextClass = XposedHelpers.findClass("com.alibaba.ariver.engine.api.bridge.model.NativeCallContext",lpparam.classLoader);
2.XposedHelpers.findAndHookMethod(String className, ClassLoader classLoader, String methodName, Object... parameterTypesAndCallback),hook指定的方法。
其中 Object... parameterTypesAndCallback是java中的范式,可以传递多个参数;当使用XposedHelpers.findAndHookMethod()方法时,需要传递被hook的函数所需要的全部参数的字节码,这些字节码可以通过.class获取到,或者findClass反射获取;java自带的类型比如String,就可以用String.class获取到。
回调对象XC_MethodHook()需重写两个方法 beforeHookedMethod(MethodHookParam param):这个方法中的代码逻辑会在hook住的方法调用前执行 ;afterHookedMethod(MethodHookParam param) 方法中的代码逻辑则会在hook住的方法调用完成后执行。
使用如下,以下代码逻辑表示,在com.baidu.swan.apps.jsbridge.SwanAppGlobalJsBridge 类下的 dispatchOnUiThread() 函数已经被hook住,在这个函数执行前后,beforeHookedMethod() 和 afterHookedMethod() 中的逻辑会分别执行。
XposedHelpers.findAndHookMethod("com.baidu.swan.apps.jsbridge.SwanAppGlobalJsBridge", lpparam.classLoader, "dispatchOnUiThread", String.class, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);XposedBridge.log("Baidu: dispatchOnUiThread 的线程:" + Thread.currentThread().getName());XposedBridge.log("Baidu:dispatchOnUiThread 入参:" + java.net.URLDecoder.decode(param.args[0].toString()) + "返回值:" + param.getResult());}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);Field mCallbackHandler = jsbridge_a.getDeclaredField("mCallbackHandler");Object mmCallbackHandler = (Object) mCallbackHandler.get(param.thisObject);//com.baidu.swan.apps.core.slave.SwanAppWebViewWidgetXposedBridge.log("Baidu: mCallbackHandler在webview里调用API时的实现类:" + mmCallbackHandler.getClass().getName());});
3. XposedHelpers.findAndHookConstructor(String className, ClassLoader classLoader, Object... parameterTypesAndCallback),参数含义同上findAndHookMethod()。
使用如下,以下代码逻辑表示,当 com.baidu.swan.apps.jsbridge.SwanAppNativeSwanJsBridge 类的构造函数被调用时,在构造函数执行前后,beforeHookedMethod() 和 afterHookedMethod() 中的逻辑会分别执行。
XposedHelpers.findAndHookConstructor("com.baidu.swan.apps.jsbridge.SwanAppNativeSwanJsBridge", lpparam.classLoader, container_a, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);Log.d("Baidu:", "SwanAppNativeSwanJsBridge()构造函数参数: " + param.args[0].getClass().getName());}@Overrideprotected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);}});
d. XposedBridge类
-
XposedBridge.log(String text),将消息写入Xposed错误日志。用于打印想要了解的信息
-
XposedBridge.hookMethod(Member hookMethod, XC_MethodHook callback),用特定的回调方法hook任意的方法或构造函数;用这种方法不需要像XposedHelpers.findAndHookMethod() 构造那么多参数,使用如下,以下代码逻辑表示先从一个类中获取到对应的method实例,然后将该method对象传递给XposedBridge.hookMethod()方法,即可直接hook住该方法
for(final Method method: TestClass.getDeclaredMethods()){XposedBridge.hookMethod(method, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);XposedBridge.log("Baidu:share中的" + method.getName() + "被调用了");printStack();}});}
3. 打印函数调用栈
在逆向APP的框架逻辑时,当hook住一个被成功触发的函数,往往需要查看其调用栈,也就是该函数的上层调用函数,这些信息会对我们逆向APP框架有很大的帮助。
private void printStack() {// 获取线程的StackTraceElement[]Throwable ex = new Throwable();StackTraceElement[] stackElements = ex.getStackTrace();if (stackElements != null) {for (int i = 0; i < stackElements.length; i++) {XposedBridge.log("Baidu: Dump Stack" + i + ": " + stackElements[i].getClassName()+ "----" + stackElements[i].getFileName()+ "----" + stackElements[i].getLineNumber()+ "----" + stackElements[i].getMethodName());}}XposedBridge.log("Baidu: Dump Stack: ---------------over----------------");}
4. 修改hook住函数的参数
1. 修改基础数据类型和String类型的参数
直接赋值即可,如下
param.args[0] = 5; // int类型
param.args[1] = "hello"; //String类型
2. 修改基础数据类型和String类型的参数
先获取引用,再修改;例如
Map<String, String> target = (Map<String, String>) param.args[0]; // Map
target.put("name", "tnoy"); //向Map中添加数据
3. 修改类的实例中的成员变量
先用Field获取到相应的成员变量,再用Field.set(类的实例,修改后的值)进行修改;例如
final Class<?> model_d = XposedHelpers.findClass("com.alipay.android.phone.globalsearch.model.d", lpparam.classLoader);
Object dVar = param.args[0];
Field b = model_d.getDeclaredField("b"); // b是model类中的public的String类型的变量
b.set(dVar, "search_auto");
4. 修改Object[]类型的数组
针对Object数组中的某一个元素,先使用.getClass().getName()获取元素的类型,然后去类里面看要修改的对应的Field,然后进行上述第三点的操作,最后封装成一个新的Object[],再把这个Object[]赋值给param.args[i]。例如
Object[] obj = (Object[]) param.args[2];
for(Object o: obj){
if(o != null){// 判断当前Object的类是不是我们需要的那个类if(o.getClass().getName().equals("com.alipay.mobile.aompfavorite.base.rpc.request.MiniAppHistoryRequestPB")){// 在jadx中源码为:List<MiniAppHistoryReqItemPB> miniAppItems;Field miniAppItems = MiniAppHistoryRequestPB.getDeclaredField("miniAppItems");// 先用List<Object>反射拿到List<MiniAppHistoryReqItemPB>列表List<Object> item = (List<Object>) miniAppItems.get(o);// 获取List列表的第一个值Object item_1 = item.get(0);Log.d("Mini 修改前", o.toString());Log.d("Mini List[0]", item_1.toString());// MiniAppHistoryReqItemPB类中的成员变量appIdField appId = MiniAppHistoryReqItemPB.getDeclaredField("appId");String AppId = (String) appId.get(item_1);Log.d("Mini appid", AppId);appId.set(item_1, "2018112262208014");Log.d("Mini 修改后", o.toString());}Log.d("Mini handler_1.1", o.getClass().getName() + " " + o.toString());}
}
5. hook动态加载进来的类
在APP框架中有些代码是动态加载进来的,比如插件化开发。使用以下代码,可以hook住动态加载进来的函数
XposedBridge.hookAllMethods(ClassLoader.class, "loadClass", new XC_MethodHook() {@Overrideprotected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {if (param.hasThrowable()) return;if (param.args.length != 1) return;Class<?> cls = (Class<?>) param.getResult();String name = cls.getName();// 判断是否是我要hook的动态加载的类if ("com.bytedance.webview.chromium.ContentSettingsAdapter".equals(name)) {// 指定要hook的methodXposedBridge.hookAllMethods(cls,"setJavaScriptEnabled",new XC_MethodHook() {protected void beforeHookedMethod(MethodHookParam param) throws Throwable {try {XposedBridge.log("commonRe: setJavaScriptEnabled被调用了: " + param.args[0]);printStack();} catch (Exception e) {XposedBridge.log("commonRe: hook error!");}}});}}});
6. 打印动态加载进来的类的存储位置(获取插件apk)
final Class<?> DexPathList = XposedHelpers.findClass("dalvik.system.DexPathList", lpparam.classLoader);for (final Method method: DexPathList.getDeclaredMethods()){XposedBridge.hookMethod(method, new XC_MethodHook() {@Overrideprotected void beforeHookedMethod(MethodHookParam param) throws Throwable {super.beforeHookedMethod(param);// splitPaths()方法传入的是动态加载进来的类在手机中的路径if(method.getName().equals("splitPaths")){XposedBridge.log("Baidu: DexPathList中的" + method.getName() + "被调用了,传参为" + param.args[0]+" " + param.args[1]);}}}) ;}
7. 自动化hook方法脚本
在某些情况下,我们拥有一系列需要hook的方法的signature,而且需要hook住这些方法获取某些信息时,我们可以通过自动化脚来实现将这些方法一一hook住,就不需要手工挨个写hook每一个方法的脚本。
代码举例实现如下
StaticInfo.java:用于从json文件中读取要hook方法的signature,并通过反射获取到对应的method对象
package com.example.ttest;import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;public class StaticInfo {public static HashMap<PoolHook.normalHandlerHook, Method> HookMethods = new HashMap<>();public XC_LoadPackage.LoadPackageParam lpparam;public StaticInfo(XC_LoadPackage.LoadPackageParam lpparam) throws NoSuchFieldException {this.lpparam = lpparam;// read from filetry{InputStream is = new FileInputStream(new File("/sdcard/tmp/static_info.json"));init(is);is.close();}catch (Exception e){XposedBridge.log("AMZ: read json error! " + e.toString());}}public void init(InputStream in) throws IOException, NoSuchFieldException {BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));StringBuilder sb = new StringBuilder();String line;String ls = System.getProperty("line.separator");while ((line = bufferedReader.readLine()) != null){sb.append(line);sb.append(ls);}String text = sb.toString();XposedBridge.log("AMZ: 读取json文件成功!");JSONArray jsonArray = JSON.parseArray(text);for (int i=0; i<jsonArray.size(); i++){JSONObject jsonObject = jsonArray.getJSONObject(i);String signature = jsonObject.getString("signature");String filedName = jsonObject.getString("field");String type = jsonObject.getString("type");String declaringClass = jsonObject.getString("declaringClass");reflectMethod(signature, filedName, type, declaringClass);}}public void init(String text) throws NoSuchFieldException {JSONArray jsonArray = JSON.parseArray(text);for (int i=0; i<jsonArray.size(); i++){JSONObject jsonObject = jsonArray.getJSONObject(i);String signature = jsonObject.getString("signature");String filedName = jsonObject.getString("field");String type = jsonObject.getString("type");String declaringClass = jsonObject.getString("declaringClass");reflectMethod(signature, filedName, type, declaringClass);}}// init HashMap for Method and Field due to json filepublic void reflectMethod(String signature, String fieldName, String type, String declaringClass) throws NoSuchFieldException {XposedBridge.log("AMZ: 正在反射获取Method:"+ signature + " " + fieldName + " " + type);Field field = XposedHelpers.findClass(declaringClass, lpparam.classLoader).getDeclaredField(fieldName);PoolHook.normalHandlerHook hook = new PoolHook.normalHandlerHook(fieldName,type, signature, field);String className = signature.split(" ")[0].substring(1,signature.split(" ")[0].length()-1);XposedBridge.log("AMZ: parse classname: " + className);try {Class<?> clazz = XposedHelpers.findClass(className, this.lpparam.classLoader);String methodName = signature.split(" ")[2].split("\\(")[0];ArrayList<String> paramTypes = new ArrayList(Arrays.asList(signature.split(" ")[2].split("\\(")[1].substring(0,signature.split(" ")[2].split("\\(")[1].length()-2).split(",")));XposedBridge.log("AMZ: 要找的方法的参数为 " + paramTypes.toString());for (Method method: clazz.getDeclaredMethods()){XposedBridge.log("AMZ: 正在匹配方法:" + method.getName() + " ==> " + methodName);XposedBridge.log("AMZ: 当前方法参数个数:" + method.getParameterTypes().length + " 目标方法的参数个数:" + paramTypes.size());if (method.getName().equals(methodName)){if (method.getParameterTypes().length == paramTypes.size()){XposedBridge.log("AMZ: 方法名和参数个数匹配上了");ArrayList<String> paramTypesCopy = (ArrayList<String>) paramTypes.clone();for (Class paramType: method.getParameterTypes()){XposedBridge.log("AMZ: 当前方法的参数类型:" + paramType.getName());if (paramTypes.contains(paramType.getName())){paramTypesCopy.remove(paramType.getName());}}if (paramTypesCopy.size() == 0 && !HookMethods.containsKey(hook)){XposedBridge.log("AMZ: find target method: " + method.toString() + " target_field: " + field);HookMethods.put(hook, method);}}else if (method.getParameterTypes().length == 0 && paramTypes.size() ==1 && paramTypes.get(0).length() == 0){XposedBridge.log("AMZ: 目标方法没有参数,匹配成功");if (!HookMethods.containsKey(hook)){XposedBridge.log("AMZ: find target method: " + method.toString() + " target_field: " + field);HookMethods.put(hook, method);}}}}}catch (Exception e){XposedBridge.log("AMZ: " + e.toString());}}
}
PoolHook.java: 继承 IXposedHookLoadPackage 接口,编写hook逻辑
package com.example.ttest;import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;public class PoolHook implements IXposedHookLoadPackage{public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable{new StaticInfo(lpparam);if (lpparam.packageName.equals("com.eg.android.AlipayGphone")){XposedBridge.log("AMZ: hook start!!");hookPackage(lpparam);}}private void hookPackage(XC_LoadPackage.LoadPackageParam lpparam){if (StaticInfo.HookMethods.isEmpty()){XposedBridge.log("AMZ: HashMap is empty");}else {XposedBridge.log("AMZ: HashMap is not empty");}for (Map.Entry<normalHandlerHook, Method> entry: StaticInfo.HookMethods.entrySet()){XposedBridge.hookMethod(entry.getValue(), entry.getKey());}}static class normalHandlerHook extends XC_MethodHook{private String fieldName;private String type;private Field field;private String signature;public normalHandlerHook(String fieldName, String type, String signature, Field field){this.fieldName = fieldName;this.type = type;this.signature = signature;this.field = field;}protected void beforeHookedMethod(MethodHookParam param) throws Throwable{System.out.println("AMZ: current method: " + param.method);XposedBridge.log("AMZ: hook field right now:" + field.toString());XposedBridge.log("AMZ: pool type:" + this.type);this.field.setAccessible(true);if (this.type.equals("HashMap")){XposedBridge.log("AMZ: this is HashMap");HashMap<Object, Object> mmap = (HashMap<Object, Object>) this.field.get(param.thisObject);for (Map.Entry<Object, Object> entry: mmap.entrySet()){Object key = entry.getKey();Object value = entry.getValue();XposedBridge.log("AMZ: signature=" + this.signature +" key=" + key + " value=" + value);}}else if(this.type.equals("Map") || this.type.equals("ConcurrentHashMap") || this.type.equals("LinkedHashMap")){XposedBridge.log("AMZ: this is Map or ConcurrentHashMap");Map<Object, Object> mmap_1 = (Map<Object, Object>) field.get(param.thisObject);for (Map.Entry<Object, Object> entry: mmap_1.entrySet()){Object key = entry.getKey();Object value = entry.getValue();XposedBridge.log("AMZ: signature=" + this.signature +" key=" + key + " value=" + value);}}}}
}
8. Xposed hook 原理
Java虚拟机JVM在加载了dex后,会将整个dex文件的内容mmap(一种内存映射文件的方法)到内存中。JVM在load一个class的时候,根据类的描述符,在内存的dex区域,查询到对应的数据,构建出ClassObject对象,以及这个ClassObject关联的Method。
Method分为directMethod和nativeMethod,分别是Java里实现的方法和C/C++里面实现的方法。Method里面有两个重要的指针:
const u2* insns;
DalvikBridgeFunc nativeFunc;
对于directMethod,insns存放了该方法在内存中的字节码指针;对于nativeMethod,会根据方法描述符,通过特定的映射关系得到一个native层的函数名(JNI method),然后去查找对应的函数,得到了函数指针后,再将这个指针赋值给insns。在nativeFunc这个桥接函数中,将insns解析为函数指针,然后进行调用。
Xposed在对java方法进行hook的时候,会先将JVM里面的这个方法的Method属性改为nativeMethod(也就是修改一个表示字段),然后将该方法的nativeFunc指向自己实现的一个native方法。于是当被hook的方法被调用到时,就会实际去调用自己实现的这个native方法。
在自己实现的native方法中,xposed直接调用了一个java方法,这个java方法里面对原方法进行了调用,并在调用前后插入了钩子,于是就hook住了这个方法。
Android所有的APP进程都是由Zygote进程启动的,所以Zygote进程中加载的代码,在后续所有fork出来的子进程中都有。Xposed替换了Zygote进程对应的可执行文件/system/bin/app_process,并用于加载xposed相关代码。
总的来说,Xposed将要hook的JAVA方法变成了native方法,在beforeMethodHook()和afterMethodHook()方法中的逻辑实现在native方法中,并将原被hook函数插在这两个函数之间正常调用,示意图如下。