diff --git a/404.html b/404.html new file mode 100644 index 00000000..e80b38fc --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + +
+ + + + + +下面是一个简单的 Demo Activity。这个 PlayActivity 内部属性以及方法都是被混淆的,且每个版本都会发生变化。
四大组件默认不会混淆,我们假设这个 Activity 被混淆了。
package org.luckypray.dexkit.demo;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.h;
+import java.util.Random;
+import org.luckypray.dexkit.demo.annotations.Router;
+
+@Router(path = "/play")
+public class PlayActivity extends AppCompatActivity {
+ private static final String TAG = "PlayActivity";
+ private TextView a;
+ private Handler b;
+
+ public void d(View view) {
+ Handler handler;
+ int i;
+ Log.d("PlayActivity", "onClick: rollButton");
+ float nextFloat = new Random().nextFloat();
+ if (nextFloat < 0.01d) {
+ handler = this.b;
+ i = -1;
+ } else if (nextFloat < 0.987f) {
+ handler = this.b;
+ i = 0;
+ } else {
+ handler = this.b;
+ i = 114514;
+ }
+ handler.sendEmptyMessage(i);
+ }
+
+ public void e(boolean z) {
+ int i;
+ if (!z) {
+ i = RandomUtil.a();
+ } else {
+ i = 6;
+ }
+ String a = h.a("You rolled a ", i);
+ this.a.setText(a);
+ Log.d("PlayActivity", "rollDice: " + a);
+ }
+
+ protected void onCreate(Bundle bundle) {
+ super/*androidx.fragment.app.FragmentActivity*/.onCreate(bundle);
+ setContentView(0x7f0b001d);
+ Log.d("PlayActivity", "onCreate");
+ HandlerThread handlerThread = new HandlerThread("PlayActivity");
+ handlerThread.start();
+ this.b = new PlayActivity$1(this, handlerThread.getLooper());
+ this.a = (TextView) findViewById(0x7f080134);
+ ((Button) findViewById(0x7f08013a)).setOnClickListener(new a(this));
+ }
+}
+
此时我们想得到这个 PlayActivity 可以使用如下代码:
这仅仅是个样例,实际使用中并不需要这么复杂且全面的条件,按需使用即可。
private fun findPlayActivity(bridge: DexKitBridge) {
+ val classData = bridge.findClass {
+ // 指定搜索的包名范围
+ searchPackages("org.luckypray.dexkit.demo")
+ // 排除指定的包名范围
+ excludePackages("org.luckypray.dexkit.demo.annotations")
+ // ClassMatcher 针对类的匹配器
+ matcher {
+ // FieldsMatcher 针对类中包含字段的匹配器
+ fields {
+ // 添加对于字段的匹配器
+ add {
+ // 指定字段的修饰符
+ modifiers = Modifier.PRIVATE or Modifier.STATIC or Modifier.FINAL
+ // 指定字段的类型
+ type = "java.lang.String"
+ // 指定字段的名称
+ name = "TAG"
+ }
+ // 添加指定字段的类型的字段匹配器
+ addForType("android.widget.TextView")
+ addForType("android.os.Handler")
+ // 指定类中字段的数量
+ count = 3
+ }
+ // MethodsMatcher 针对类中包含方法的匹配器
+ methods {
+ // 添加对于方法的匹配器
+ add {
+ // 指定方法的修饰符
+ modifiers = Modifier.PROTECTED
+ // 指定方法的名称
+ name = "onCreate"
+ // 指定方法的返回值类型
+ returnType = "void"
+ // 指定方法的参数类型,如果参数类型不确定,使用 null,使用此方法会隐式声明参数个数
+ paramTypes("android.os.Bundle")
+ // 指定方法中使用的字符串
+ usingStrings("onCreate")
+ }
+ add {
+ paramTypes("android.view.View")
+ // 指定方法中使用的数字,类型为 Byte, Short, Int, Long, Float, Double 之一
+ usingNumbers(0.01, -1, 0.987, 0, 114514)
+ }
+ add {
+ paramTypes("boolean")
+ // 指定方法中调用的方法列表
+ invokeMethods {
+ add {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "int"
+ // 指定方法中调用的方法中使用的字符串,所有字符串均使用 Equals 匹配
+ usingStrings(listOf("getRandomDice: "), StringMatchType.Equals)
+ }
+ // 只需要包含上述方法的调用即可
+ matchType = MatchType.Contains
+ }
+ }
+ // 指定类中方法的数量,最少不少于1个,最多不超过10个
+ count(1..10)
+ }
+ // AnnotationsMatcher 针对类中包含注解的匹配器
+ annotations {
+ // 添加对于注解的匹配器
+ add {
+ // 指定注解的类型
+ type = "org.luckypray.dexkit.demo.annotations.Router"
+ // 该注解需要包含指定的 element
+ addElement {
+ // 指定 element 的名称
+ name = "path"
+ // 指定 element 的值
+ stringValue("/play")
+ }
+ }
+ }
+ // 类中所有方法使用的字符串
+ usingStrings("PlayActivity", "onClick", "onCreate")
+ }
+ }.single()
+ println(classData.name)
+}
+
获得的结果如下:
org.luckypray.dexkit.demo.PlayActivity
+
如果存在这么一个类,它唯一的特征是祖辈没被混淆,中间的父类都被混淆了,我们也可以使用 DexKit
来找到它。
private fun findActivity(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher { // ClassMatcher
+ // androidx.appcompat.app.AppCompatActivity
+ superClass { // ClassMatcher
+ // androidx.fragment.app.FragmentActivity
+ superClass { // ClassMatcher
+ // androidx.activity.ComponentActivity
+ superClass { // ClassMatcher
+ // androidx.core.app.ComponentActivity
+ superClass { // ClassMatcher
+ superClass = "android.app.Activity"
+ }
+ }
+ }
+ }
+ }
+ }.forEach {
+ // org.luckypray.dexkit.demo.MainActivity
+ // org.luckypray.dexkit.demo.PlayActivity
+ println(it.name)
+ }
+}
+
获得的结果如下:
org.luckypray.dexkit.demo.MainActivity
+org.luckypray.dexkit.demo.PlayActivity
+
小提示
在 DexKit
中,一切符合逻辑的关系都可以作为查询条件
如果我们需要寻找的方法中存在一个被混淆的参数,我们可以使用 null
来替代,这样它就能匹配任意类型的参数。
private fun findMethodWithFuzzyParam(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ // 指定方法的参数类型,如果参数类型不确定,使用 null
+ paramTypes("android.view.View", null)
+ // paramCount = 2 // paramTypes 长度为 2 已经隐式确定了参数个数
+ usingStrings("onClick")
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
使用 DexKit 查询到的结果如何序列化保存下来,以便下次使用呢?
DexKit 中对 Class、Method、Field 提供了相应的包装类,分别是 DexClass
、DexMethod
、DexField
。 包装类继承了 Serializable
接口,因此可以直接使用 Java 的序列化方式来保存。对于查询返回的对象,可以直接使用 toDexClass()
、toDexMethod()
、toDexField()
方法来转换为包装类。当然,您也可以使用 Data 对象的 descriptor
属性来保存,它是一个 Dailvik 描述
标识了唯一的对象。
private fun saveData(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ paramTypes("android.view.View", null)
+ usingStrings("onClick")
+ }
+ }.single().let {
+ val descriptor = it.toDexMethod().descriptor
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ sp.edit().putString("onClickMethod", descriptor).apply()
+ }
+}
+
+private fun readData(): Method {
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ val descriptor = sp.getString("onClickMethod", null)
+ if (descriptor != null) {
+ val dexMethod = DexMethod(descriptor)
+ val method = dexMethod.getMethodInstance(hostClassLoader)
+ return method
+ }
+ error("No saved")
+}
+
Here is a simple Demo Activity, PlayActivity, where the internal properties and methods are obfuscated, and they change in each version.
The four major components are not obfuscated by default. We assume this Activity is obfuscated.
package org.luckypray.dexkit.demo;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.h;
+import java.util.Random;
+import org.luckypray.dexkit.demo.annotations.Router;
+
+@Router(path = "/play")
+public class PlayActivity extends AppCompatActivity {
+ private static final String TAG = "PlayActivity";
+ private TextView a;
+ private Handler b;
+
+ public void d(View view) {
+ Handler handler;
+ int i;
+ Log.d("PlayActivity", "onClick: rollButton");
+ float nextFloat = new Random().nextFloat();
+ if (nextFloat < 0.01d) {
+ handler = this.b;
+ i = -1;
+ } else if (nextFloat < 0.987f) {
+ handler = this.b;
+ i = 0;
+ } else {
+ handler = this.b;
+ i = 114514;
+ }
+ handler.sendEmptyMessage(i);
+ }
+
+ public void e(boolean z) {
+ int i;
+ if (!z) {
+ i = RandomUtil.a();
+ } else {
+ i = 6;
+ }
+ String a = h.a("You rolled a ", i);
+ this.a.setText(a);
+ Log.d("PlayActivity", "rollDice: " + a);
+ }
+
+ protected void onCreate(Bundle bundle) {
+ super/*androidx.fragment.app.FragmentActivity*/.onCreate(bundle);
+ setContentView(0x7f0b001d);
+ Log.d("PlayActivity", "onCreate");
+ HandlerThread handlerThread = new HandlerThread("PlayActivity");
+ handlerThread.start();
+ this.b = new PlayActivity$1(this, handlerThread.getLooper());
+ this.a = (TextView) findViewById(0x7f080134);
+ ((Button) findViewById(0x7f08013a)).setOnClickListener(new a(this));
+ }
+}
+
In this scenario, we want to find the PlayActivity using the following code:
This is just an example. In actual use, you don't need conditions as complex and comprehensive as this. Use as needed.
private fun findPlayActivity(bridge: DexKitBridge) {
+ val classData = bridge.findClass {
+ // Search within the specified package name range
+ searchPackages("org.luckypray.dexkit.demo")
+ // Exclude the specified package name range
+ excludePackages("org.luckypray.dexkit.demo.annotations")
+ // ClassMatcher Matcher for classes
+ matcher {
+ // FieldsMatcher Matcher for fields in a class
+ fields {
+ // Add a matcher for the field
+ add {
+ // Specify the modifiers of the field
+ modifiers = Modifier.PRIVATE or Modifier.STATIC or Modifier.FINAL
+ // Specify the type of the field
+ type = "java.lang.String"
+ // Specify the name of the field
+ name = "TAG"
+ }
+ // Add a matcher for the field of the specified type
+ addForType("android.widget.TextView")
+ addForType("android.os.Handler")
+ // Specify the number of fields in the class
+ count = 3
+ }
+ // MethodsMatcher Matcher for methods in a class
+ methods {
+ // Add a matcher for the method
+ add {
+ // Specify the modifiers of the method
+ modifiers = Modifier.PROTECTED
+ // Specify the name of the method
+ name = "onCreate"
+ // Specify the return type of the method
+ returnType = "void"
+ // Specify the parameter types of the method, if the parameter types are uncertain,
+ // use null, and this method will implicitly declare the number of parameters
+ paramTypes("android.os.Bundle")
+ // Specify the strings used in the method
+ usingStrings("onCreate")
+ }
+ add {
+ paramTypes("android.view.View")
+ // Specify the numbers used in the method, the type is Byte, Short, Int, Long, Float, Double
+ usingNumbers(0.01, -1, 0.987, 0, 114514)
+ }
+ add {
+ paramTypes("boolean")
+ // Specify the methods called in the method list
+ invokeMethods {
+ add {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "int"
+ // Specify the strings used in the method called in the method,
+ usingStrings(listOf("getRandomDice: "), StringMatchType.Equals)
+ }
+ // Only need to contain the call to the above method
+ matchType = MatchType.Contains
+ }
+ }
+ count(1..10)
+ }
+ // AnnotationsMatcher Matcher for annotations in a class
+ annotations {
+ // Add a matcher for the annotation
+ add {
+ // Specify the type of the annotation
+ type = "org.luckypray.dexkit.demo.annotations.Router"
+ // The annotation needs to contain the specified element
+ addElement {
+ // Specify the name of the element
+ name = "path"
+ // Specify the value of the element
+ stringValue("/play")
+ }
+ }
+ }
+ // Strings used by all methods in the class
+ usingStrings("PlayActivity", "onClick", "onCreate")
+ }
+ }.single()
+ println(classData.name)
+}
+
The result is as follows:
org.luckypray.dexkit.demo.PlayActivity
+
if there is such a class, its only feature is that the ancestors are not obfuscated, and the middle parents are all obfuscated, we can also use DexKit
to find it.
private fun findActivity(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher { // ClassMatcher
+ // androidx.appcompat.app.AppCompatActivity
+ superClass { // ClassMatcher
+ // androidx.fragment.app.FragmentActivity
+ superClass { // ClassMatcher
+ // androidx.activity.ComponentActivity
+ superClass { // ClassMatcher
+ // androidx.core.app.ComponentActivity
+ superClass { // ClassMatcher
+ superClass = "android.app.Activity"
+ }
+ }
+ }
+ }
+ }
+ }.forEach {
+ // org.luckypray.dexkit.demo.MainActivity
+ // org.luckypray.dexkit.demo.PlayActivity
+ println(it.name)
+ }
+}
+
The result is as follows:
org.luckypray.dexkit.demo.MainActivity
+org.luckypray.dexkit.demo.PlayActivity
+
Tips
In DexKit
, any logical relationship can be used as a query condition
If we need to find a method with an obfuscated parameter, we can use null
to replace it, so that it can match any type of parameter.
private fun findMethodWithFuzzyParam(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ // Specify the parameters of the method, if the parameters are uncertain, use null
+ paramTypes("android.view.View", null)
+ // paramCount = 2 // paramTypes length is 2, which has implicitly determined the number of parameters
+ usingStrings("onClick")
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
How can you serialize and save the results obtained from DexKit queries for later use?
DexKit provides corresponding packaging classes for Class, Method, and Field, namely DexClass
, DexMethod
, and DexField
. The wrapper class inherits the Serializable
interface, so it can be saved directly using Java's serialization method. For the objects returned by the query, you can directly use toDexClass()
, toDexMethod()
, toDexField()
methods to convert to a wrapper class. Of course, you can also use the Data object's descriptor
attribute, which is a Dailvik description
that identifies a unique object.
private fun saveData(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ paramTypes("android.view.View", null)
+ usingStrings("onClick")
+ }
+ }.single().let {
+ val descriptor = it.toDexMethod().descriptor
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ sp.edit().putString("onClickMethod", descriptor).apply()
+ }
+}
+
+private fun readData(): Method {
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ val descriptor = sp.getString("onClickMethod", null)
+ if (descriptor != null) {
+ val dexMethod = DexMethod(descriptor)
+ val method = dexMethod.getMethodInstance(hostClassLoader)
+ return method
+ }
+ error("No saved")
+}
+
DexKit
is a high-performance runtime parsing library for dex implemented in C++, used to search for obfuscated classes, methods, or properties.
In the development of Xposed modules, we often need to hook specific methods. However, due to code obfuscation, module developers often have to maintain multiple versions of hook points to ensure compatibility of the module across different versions. This adaptation approach is cumbersome and error-prone.
Is there a better solution? Some developers may consider traversing all classes in the ClassLoader and traversing the characteristics of the classes through reflection, such as method names, parameter types, return value types, and annotations, and then adapting based on these features. However, this approach also has obvious drawbacks. First, due to the time-consuming nature of Java's reflection mechanism itself, the search speed is affected by device performance. Secondly, in complex conditions, the search may take a long time, and in extreme conditions, it may even exceed 30 seconds. In addition, forcibly loading some classes may cause unpredictable problems in the host APP.
Typically, developers decompile the host APP to obtain smali or decompiled Java code and search the code based on known features. The results are then written into the adaptation file. To simplify this process, we need an automated way. Currently, most solutions for parsing Dex files rely on dexlib2
, but due to its development in Java, there are performance bottlenecks. Especially when the host application has a large number of dex files, the parsing time is long, affecting the user experience. Therefore, DexKit
came into being. It is implemented in C++, providing superior performance, internal optimizations using multi-threading and various algorithms, enabling complex searches to be completed in a very short time.
It is recommended to use Kotlin for development because it provides DSL support, allowing us to have a better experience when using DexKit
. If you are not familiar with Kotlin, you don't need to worry either; the API also provides corresponding chain call support, which allows Java developers to have a good experience.
DexKit
是一个使用 C++ 实现的 dex 高性能运行时解析库,用于查找被混淆的类、方法或者属性。
在 Xposed 模块的开发中,我们通常需要对特定的方法进行 hook。然而,由于代码混淆,模块开发者往往不得不维护多个版本的 hook 点,以确保模块在不同版本下的兼容性。这种适配方式繁琐且容易出错。
有没有更好的解决方案呢?有些开发者可能会考虑遍历 ClassLoader 中的所有类,并通过反射遍历类的特征,如方法名、参数类型、 返回值类型和注解等,然后根据这些特征进行适配。然而,这种方式也有明显的缺点。首先,由于 Java 的反射机制本身耗时较长, 查找速度受设备性能影响。其次,在复杂条件下,查找可能需要较长时间,极端条件下甚至可能超过 30 秒。另外, 强行加载部分类可能会导致宿主 APP 出现不可预知的问题。
通常,开发者会对宿主 APP 进行反编译,获取 smali 或反编译后的 Java 代码,并通过已知的特征对代码进行搜索。 然后将结果写入适配文件。为了简化这个过程,我们需要一种自动化的方式。目前针对 Dex 文件的解析方案大多依赖于 dexlib2
,但由于其是用 Java 开发,性能存在瓶颈。特别是当宿主应用存在大量 dex 文件时,解析时间会很长, 影响用户体验。为此,DexKit
应运而生。它采用 C++ 实现,性能优越的同时且内部使用多线程和多种算法进行优化, 能够在极短时间内完成复杂的搜索。
推荐使用 Kotlin 进行开发,因为其提供了 DSL 支持,可以让我们在使用 DexKit
时获得更好的体验。如果您不熟悉 Kotlin,也无需担心,API 也提供了相应的链式调用支持,同样能让 Java 开发者获得良好的体验。
Assume this is obfuscated code from a host app. We need to dynamically adapt the hook for this method. Due to obfuscation, method names and class names may change with each version.
public class abc {
+
+ public boolean cvc() {
+ boolean b = false;
+ // ...
+ Log.d("VipCheckUtil", "userInfo: xxxx");
+ // ...
+ return b;
+ }
+}
+
DexKit can easily solve this problem!
By creating an instance of
DexKitBridge
, we can perform specific searches on the app's dex file.
warning
Only one instance of DexKitBridge
needs to be created. If you do not wish to use try-with-resources or Kotlin's .use for automatic closing of the DexKitBridge
instance, you need to manage its lifecycle manually. Ensure to call .close()
when it's no longer needed to prevent memory leaks.
假设这是一个宿主 APP 的被混淆后的代码,我们需要对这个方法的 hook 进行动态适配,由于混淆的存在,可能每个版本方法名以及类名都会发生变化。
public class abc {
+
+ public boolean cvc() {
+ boolean b = false;
+ // ...
+ Log.d("VipCheckUtil", "userInfo: xxxx");
+ // ...
+ return b;
+ }
+}
+
DexKit 可以轻松解决这个问题!
通过创建
DexKitBridge
实例,我们可以对 APP 的 dex 进行特定的查找
注意
对于 DexKitBridge
只需要创建一个实例。 如果您不希望使用 try-with-resources 或者 kotlin .use 自动关闭 DexKitBridge
实例,需要自行维护其生命周期。请确保在不需要时调用 .close()
方法以防止内存泄漏。
Here we provide some basic knowledge to help you better understand the usage of
DexKit
. Experienced developers can skip this section.
When using DexKit
, there are some fundamental concepts you need to understand, including but not limited to:
Notice
The content in the basic knowledge is not necessarily completely accurate. Please read it according to your own understanding. If you find any inaccuracies, feel free to point them out and help improve.
Type Signature | Primitive Type | Size (Bytes) |
---|---|---|
V | void | - |
Z | boolean | 1 |
B | byte | 1 |
C | char | 2 |
S | short | 2 |
I | int | 4 |
J | long | 8 |
F | float | 4 |
D | double | 8 |
Reference types are divided into classes and arrays.
The type signature of a class starts with L
, followed by the fully qualified name (FullClassName) of the class, and ends with ;
. For example, Ljava/lang/String;
represents the java.lang.String
class.
For example:
Type Signature | Java Type Definition |
---|---|
Ljava/lang/String; | java.lang.String |
Ljava/util/List; | java.util.List |
The type signature of an array starts with [
, followed by the type signature of the array elements. For example, [[I
represents a two-dimensional array where the element type is int
.
For example:
Type Signature | Java Type Definition |
---|---|
[I | int[] |
[[C | char[][] |
[Ljava/lang/String; | java.lang.String[] |
Tips
The term 'class' and 'type' are not entirely equivalent: 'type' is Type, while 'class' is Class. 'Class' is a subset of 'type'. For example:
java.lang.Integer
is a 'class' and also a 'type'java.lang.Integer[]
is an 'array type', but not a 'class'int
is a 'primitive type', but not a 'class'For method parameters, return types, and field types, we use the term 'type,' specifically 'Type.'
A method signature consists of the signature of the return type and the signature of the parameter types. For example, ()V
represents a parameterless void
method.
For example:
For ease of description, all methods in the table are named as
function
.
Method Signature | Java Method Definition |
---|---|
()V | void function() |
(I)V | void function(int) |
(II)V | void function(int, int) |
(ILjava/lang/String;J)V | void function(int, java.lang.String, long) |
(I[II)V | void function(int, int[], int) |
([[Ljava/lang/String;)V | void function(java.lang.String[][]) |
()[Ljava/lang/String; | java.lang.String[] function() |
In a Dex file, we can represent specific classes, methods, or fields using the 'Dalvik Descriptor.' In DexKit
API, the term 'descriptor' is commonly used.
The format of a class descriptor is [class signature]
, such as Ljava/lang/String;
.
The format of a method descriptor is [class signature]->[method name][method signature]
, such as Ljava/lang/String;->length()I
.
Tips
In 'Dalvik Descriptor,' the method name for constructors is <init>
, and for static initialization methods, it's <clinit>
. Therefore, in DexKit
, to find constructors, you need to use <init>
as the method name.
The format of a field descriptor is [class signature]->[field name]:[type signature]
, such as Ljava/lang/String;->count:I
.
Tips
In DexKit, the className/Type query parameter only supports the Java primitive syntax. For example:
void
, int
, boolean
.java.lang.String
or java/lang/String
.int[]
, java.lang.String[][]
or java/lang/String[][]
.这里提供了一些基础知识,帮助您更好的理解
DexKit
的使用,已经掌握的开发者可以跳过这一章节。
在使用 DexKit
时,有一些基础知识是必要的,其中包括但不限于以下内容:
注意
基础知识中的内容不一定完全准确,请根据自己的见解酌情阅读,若发现内容有误,欢迎指正并帮助改进。
类型签名 | 原始类型 | 大小(字节) |
---|---|---|
V | void | - |
Z | boolean | 1 |
B | byte | 1 |
C | char | 2 |
S | short | 2 |
I | int | 4 |
J | long | 8 |
F | float | 4 |
D | double | 8 |
引用类型分为类与数组。
类的类型签名都是以 L
开头,以 ;
结尾,中间是类的全限定名(FullClassName),如 Ljava/lang/String;
。
例如:
类型签名 | Java 中类型定义 |
---|---|
Ljava/lang/String; | java.lang.String |
Ljava/util/List; | java.util.List |
数组类型的类型签名以 [
开头,后面跟着数组元素的类型签名,如 [[I
表示一个二维数组,数组中的元素类型是 int
。
例如:
类型签名 | Java 中类型定义 |
---|---|
[I | int[] |
[[C | char[][] |
[Ljava/lang/String; | java.lang.String[] |
小提示
类
与 类型
并不完全等价:类型为 Type,而类为 Class。 类
是 类型
的子集。 例如:
java.lang.Integer
是 类
,也是 类型
java.lang.Integer[]
是 数组类型
,但不是 类
int
是 原始类型
,但不是 类
对于方法参数,返回值类型以及字段的类型,我们统一称为 类型
即 Type
方法签名由方法的返回值类型签名和参数类型签名组成,如 ()V
表示一个无参的 void
方法。
例如:
为了方便表述,表格中所有的方法都命名为
function
方法签名 | Java 中方法定义 |
---|---|
()V | void function() |
(I)V | void function(int) |
(II)V | void function(int, int) |
(ILjava/lang/String;J)V | void function(int, java.lang.String, long) |
(I[II)V | void function(int, int[], int) |
([[Ljava/lang/String;)V | void function(java.lang.String[][]) |
()[Ljava/lang/String; | java.lang.String[] function() |
在 Dex 文件中,我们可以通过 Dalvik 描述
的方式来表示特定的类、方法或字段。在 DexKit
API中,通常使用 descriptor
来命名。
类描述的格式为 [类签名]
,如 Ljava/lang/String;
。
方法描述的格式为 [类签名]->[方法名][方法签名]
,如 Ljava/lang/String;->length()I
。
小提示
在 Dalvik 描述
中,构造函数的方法名为 <init>
,静态初始化函数的方法名为 <clinit>
。 所以在 DexKit
中如果想要查找构造函数,需要使用 <init>
作为方法名。
字段描述的格式为 [类签名]->[字段名]:[类型签名]
,如 Ljava/lang/String;->count:I
。
小提示
DexKit 中 className/Type 查询参数只支持 Java 原始写法,例如:
void
,int
,boolean
形式的 Java PrimitiveTypejava.lang.String
或者 java/lang/String
形式的 FullClassNameint[]
,java.lang.String[][]
或者 java/lang/String[][]
形式的 ArrayTypeNameIn DexKit, various queries may achieve the same functionality, but the difference in performance can be significant, varying by several tens of times. This section will introduce some techniques for performance optimization.
At the native layer, DexKit maintains lists of classes, methods, and fields in the Dex file. How does DexKit scan these lists in several APIs? The traversal order of findClass
, findMethod
, and findField
is based on the respective lists' sequential order. Then, each condition is matched one by one.
Some users may use the declaredClass
condition to write queries like the following when using:
private fun badCode(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ declaredClass {
+ usingStrings("getUid", "", "_event")
+ }
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
This search takes 4310ms
.
At first glance, this query seems fine, but in reality, its performance is very poor. Why? As mentioned earlier, the findMethod
API traverses all methods and then matches each condition one by one. However, there is a many-to-one relationship between methods and classes, meaning a class may contain multiple methods, but a method can only belong to one class. Therefore, during the process of traversing all methods, each method will be matched once with the declaredClass
condition, leading to performance waste.
So, let's change our approach. By first searching for declaredClass
and then using chain calls, we can search for methods within the classes that meet the criteria. Won't this help avoid the issue?
private fun goodCode(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher {
+ usingStrings("getUid", "", "_event")
+ }
+ }.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
This search takes 77ms
, showing a performance improvement by several tens of times.
When using findMethod
or findField
, the declaredClass
condition should be avoided as much as possible.
在 DexKit 中,多种查询或许能实现同样的功能,但是性能差距却可能相差几十倍。本节将介绍一些性能优化的技巧。
在 native 层,DexKit 会维护 Dex 中的类、方法以及字段的列表,那么在几个 API 中,DexKit 是如何扫描这些列表的呢?findClass
、findMethod
、findField
的遍历顺序均是按照各自列表的先后顺序进行遍历,然后再逐一对各个条件进行匹配。
可能有些用户在使用 findMethod
或 findField
时,会使用 declaredClass
条件写出如下的查询:
private fun badCode(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ declaredClass {
+ usingStrings("getUid", "", "_event")
+ }
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
这个搜索耗时 4310ms
。 乍一看这个查询似乎没有什么问题,但是实际上这个查询的性能是非常差。为什么?前面提到过,findMethod
API 会遍历一遍所有的方法,然后再逐一对各个条件进行匹配。而 method 与 class 之间却是一个多对一的关系,即一个 class 中可能包含多个 method,但是一个 method 只能属于一个 class。因此,遍历所有方法的过程中,每个 method 都会被匹配一次 declaredClass
条件,这就导致了性能的浪费。
那么,我们换一个思路,先搜索 declaredClass,配合链式调用就能在符合条件的类中再搜索 method,这样不就可以避免了吗?
private fun goodCode(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher {
+ usingStrings("getUid", "", "_event")
+ }
+ }.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
这个搜索耗时 77ms
, 性能提升了数十倍之多。
在使用 findMethod
或 findField
时,尽量避免使用 declaredClass
附带过于复杂的逻辑。
集成
DexKit
到您的项目中。
确保您的开发环境满足以下要求:
注意
如果您的项目的 minSdkVersion 小于 23,在 Xposed 模块内使用 System.loadLibrary("dexkit")
时可能会抛出 java.lang.UnsatisfiedLinkError: xxx couldn't find "libdexkit.so"
的异常。这是因为打包时默认会压缩 lib/
目录下的 so 文件,导致无法通过 System.loadLibrary
加载 so 文件。解决方案是在 app/build.gradle
中添加以下配置:
android {
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
+ }
+}
+
或者手动解压 apk 内的 lib/ 目录下的 libdexkit.so 文件至任意可读写目录,然后通过 System.load("/path/to/libdexkit.so") 加载。
`,8),b={href:"https://central.sonatype.com/search?q=dexkit&namespace=org.luckypray",target:"_blank",rel:"noopener noreferrer"},v=e("img",{src:"https://img.shields.io/maven-central/v/org.luckypray/dexkit.svg?label=Maven Central",alt:"Maven Central"},null,-1),y=e("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[e("pre",{class:"shiki github-dark-dimmed",style:{"background-color":"#22272e"},tabindex:"0"},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#ADBAC7"}},"dependencies {")]),s(` +`),e("span",{class:"line"},[e("span",{style:{color:"#ADBAC7"}}," "),e("span",{style:{color:"#768390"}},"// 将在您的项目中的
app/build.gradle
或者app/build.gradle.kts
添加dexkit
的依赖。
小提示
从 DexKit 2.0 开始,新的 ArtifactId 已从 DexKit
更改为 dexkit
。
现在您已经成功集成了 DexKit
到您的项目中,接下来我们将会介绍如何使用 DexKit
来完成一些常见的需求。
Integrate
DexKit
into your project.
Make sure your development environment meets the following requirements:
Notice
If your project's minSdkVersion is less than 23, using System.loadLibrary("dexkit")
within an Xposed module may throw a java.lang.UnsatisfiedLinkError: xxx couldn't find "libdexkit.so"
exception. This is because the so files under the lib/
directory are compressed by default during packaging, making it impossible to load the so file via System.loadLibrary
. The solution is to add the following configuration in app/build.gradle
:
android {
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
+ }
+}
+
or manually extract the libdexkit.so file from the lib/ directory within the apk to any readable and writable directory, and then load it using System.load("/path/to/libdexkit.so")
.
`,8),h={href:"https://central.sonatype.com/search?q=dexkit&namespace=org.luckypray",target:"_blank",rel:"noopener noreferrer"},v=e("img",{src:"https://img.shields.io/maven-central/v/org.luckypray/dexkit.svg?label=Maven Central",alt:"Maven Central"},null,-1),y=e("div",{class:"language-groovy line-numbers-mode","data-ext":"groovy"},[e("pre",{class:"shiki github-dark-dimmed",style:{"background-color":"#22272e"},tabindex:"0"},[e("code",null,[e("span",{class:"line"},[e("span",{style:{color:"#ADBAC7"}},"dependencies {")]),n(` +`),e("span",{class:"line"},[e("span",{style:{color:"#ADBAC7"}}," "),e("span",{style:{color:"#768390"}},"// replaceAdd the dependency for dexkit in your project's
app/build.gradle
orapp/build.gradle.kts
.
Tips
Starting from DexKit 2.0, the new ArtifactId has been changed from DexKit
to dexkit
.
Now that you have successfully integrated DexKit
into your project, next we will introduce how to use DexKit
to achieve some common requirements.
Starting from version
1.1.0
, DexKit supports running on desktop platforms without the need for packaging as an APK for testing on Android.
The basic runtime environment requires gcc/clang, cmake, and ninja/make.
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
+
After installation, we need to add the mingw64/bin
directory to the environment variables for future use.
warning
DexKit will use ninja
as the default build system by default. If you need to use make
in mingw for building, you need to execute pacman -S mingw-w64-x86_64-make
, after installation, you need to rename msys64\\mingw64\\bin\\mingw32-make.exe
to make.exe
or add it as a shortcut, otherwise the build will fail due to gradle-cmake-plugin
not finding the make command. Additionally, delete generator.set(generators.ninja)
in :dexkit/build.gradle
, or modify it to generator.set(generators.unixMakefiles)
.
On Linux, you normally only need to install ninja
to use it.
brew install cmake ninja
+
git clone https://github.com/LuckyPray/DexKit.git
+
Execute the submodule :main
to perform testing.
gradle :main:run
+
从
1.1.0
开始,DexKit
支持桌面平台运行,无需打包成 apk 在 Android 进行测试工作。
需要 gcc/clang、cmake 以及 ninja/make 作为基础运行环境。
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
+
安装完成后,我们需要将 mingw64/bin
目录添加进环境变量中便于后续使用。
warning
DexKit 默认将使用 ninja
作为默认构建系统,如果您需要在 mingw 中使用 make
进行构建, 需要执行 pacman -S mingw-w64-x86_64-make
,安装完成后需要将 msys64\\mingw64\\bin\\mingw32-make.exe
重命名为 make.exe
或者添加为快捷方式,否则会由于 gradle-cmake-plugin
找不到 make
命令而构建失败。 同时删除 :dexkit/build.gradle
中的 generator.set(generators.ninja)
,或者修改为 generator.set(generators.unixMakefiles)
对于Linux用户,正常情况只需要安装 ninja
即可。
brew install cmake ninja
+
git clone https://github.com/LuckyPray/DexKit.git
+
执行子模块 :main
即可进行测试。
gradle :main:run
+
This section introduces the structure components of
DexKit
queries. Matcher objects can be recursively nested, and all conditions are optional.
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchIn | Collection<ClassData> | Search for classes in the specified list of classes |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | ClassMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for fields in the specified list of classes |
searchInFields | Collection<FieldData> | Search for fields in the specified list of fields |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | FieldMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for methods in the specified list of classes |
searchInMethods | Collection<MethodData> | Search for methods in the specified list of methods |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | MethodMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchIn | Collection<ClassData> | Search for classes in the specified list of classes |
matchers | Collection<StringMatchersGroup> | List of query groups using strings |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for methods in the specified list of classes |
searchInMethods | Collection<MethodData> | Search for methods in the specified list of methods |
matchers | Collection<StringMatchersGroup> | List of query groups using strings |
Field Name | Type | Description |
---|---|---|
modifiers | Int | Bit masks for matching modifiers |
matchType | MatchType | Matching mode |
Field Name | Type | Description |
---|---|---|
byteValue | Byte | Matched byte |
shortValue | Short | Matched short integer |
charValue | Char | Matched character |
intValue | Int | Matched integer |
longValue | Long | Matched long integer |
floatValue | Float | Matched float |
doubleValue | Double | Matched double precision float |
stringValue | StringMatcher | Matched string |
methodValue | MethodMatcher | Matched method |
enumValue | FieldMatcher | Matched enumeration |
arrayValue | AnnotationEncodeArrayMatcher | Matched array |
annotationValue | AnnotationMatcher | Matched annotation |
nullValue | None | Match null |
boolValue | Boolean | Matched boolean |
Field Name | Type | Description |
---|---|---|
min | Int | Minimum value, default is 0 |
max | Int | Maximum value, default is Int.MAX_VALUE |
Field Name | Type | Description |
---|---|---|
byteValue | Byte | Matched byte |
shortValue | Short | Matched short integer |
charValue | Char | Matched character |
intValue | Int | Matched integer |
longValue | Long | Matched long integer |
floatValue | Float | Matched float |
doubleValue | Double | Matched double precision float |
Field Name | Type | Description |
---|---|---|
opCodes | Collection<Int> | Matched Int values of opcodes |
opNames | Collection<String> | Matched names of opcodes (as per smali syntax) |
matchType | OpCodeMatchType | Matching mode |
size | IntRange | Total length range of the opcode |
Field Name | Type | Description |
---|---|---|
value | String | Matched string |
matchType | StringMatchType | Matching mode |
ignoreCase | Boolean | Ignore case sensitivity |
Field Name | Type | Description |
---|---|---|
types | Collection<TargetElementType> | List of matched annotation element types |
matchType | MatchType | Matching mode |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Matched name |
value | AnnotationEncodeValueMatcher | Matched value |
Field Name | Type | Description |
---|---|---|
elements | Collection<AnnotationElementMatcher> | List of matched annotation elements |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched elements |
Field Name | Type | Description |
---|---|---|
values | Collection<AnnotationEncodeValueMatcher> | List of matched annotation elements |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched elements |
Field Name | Type | Description |
---|---|---|
type | ClassMatcher | Matched annotation type |
targetElementTypes | TargetElementTypesMatcher | List of matched annotation element types |
policy | RetentionPolicyType | Matched retention policy |
elements | AnnotationElementsMatcher | List of matched annotation elements |
usingStrings | Collection<StringMatcher> | List of strings used in the annotation |
Field Name | Type | Description |
---|---|---|
annotations | Collection<AnnotationMatcher> | List of matched annotations |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched annotations |
Field Name | Type | Description |
---|---|---|
source | StringMatcher | Source file name of the class, i.e., .source field in smali |
className | StringMatcher | Name of the class |
modifiers | AccessFlagsMatcher | Modifiers of the class |
superClass | ClassMatcher | Superclass of the class |
interfaces | InterfacesMatcher | List of interfaces implemented by the class |
annotations | AnnotationsMatcher | List of annotations for the class |
fields | FieldsMatcher | List of fields in the class |
methods | MethodsMatcher | List of methods in the class |
usingStrings | Collection<StringMatcher> | List of strings used in the class |
Field Name | Type | Description |
---|---|---|
interfaces | Collection<ClassMatcher> | List of matched interfaces |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched interfaces |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Name of the field |
modifiers | AccessFlagsMatcher | Modifiers of the field |
declaredClass | ClassMatcher | Declaring class of the field |
type | ClassMatcher | Type of the field |
annotations | AnnotationsMatcher | List of annotations for the field |
getMethods | MethodsMatcher | List of methods to get the field |
putMethods | MethodsMatcher | List of methods to set the field |
Field Name | Type | Description |
---|---|---|
fields | Collection<FieldMatcher> | List of matched fields |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched fields |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Name of the method |
modifiers | AccessFlagsMatcher | Modifiers of the method |
declaredClass | ClassMatcher | Declaring class of the method |
returnType | ClassMatcher | Return type of the method |
params | ParametersMatcher | List of parameters |
annotations | AnnotationsMatcher | List of annotations for the method |
opCodes | OpcodesMatcher | List of opcodes for the method |
usingStrings | Collection<StringMatcher> | List of strings used in the method |
usingFields | Collection<UsingFieldMatcher> | List of fields used in the method |
usingNumbers | Collection<Number> | List of numbers used in the method |
invokeMethods | MethodsMatcher | List of methods invoked by the method |
callMethods | MethodsMatcher | List of methods that call the method |
Field Name | Type | Description |
---|---|---|
methods | Collection<MethodMatcher> | List of matched methods |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched methods |
Field Name | Type | Description |
---|---|---|
type | ClassMatcher | Type of the parameter |
annotations | AnnotationsMatcher | List of annotations for the parameter |
Field Name | Type | Description |
---|---|---|
params | Collection<ParameterMatcher> | List of matched parameters |
count | IntRange | Number of matched parameters |
Field Name | Type | Description |
---|---|---|
groupName | String | Group name for the query |
matchers | Collection<StringMatcher> | List of strings used in the query |
Field Name | Type | Description |
---|---|---|
field | FieldMatcher | Matched field |
usingType | UsingType | Type of usage |
Field Name | Type | Description |
---|---|---|
value | Byte | byte value |
Field Name | Type | Description |
---|---|---|
value | Short | short integer value |
Field Name | Type | Description |
---|---|---|
value | Char | character value |
Field Name | Type | Description |
---|---|---|
value | Int | integer value |
Field Name | Type | Description |
---|---|---|
value | Long | long integer value |
Field Name | Type | Description |
---|---|---|
value | Float | float value |
Field Name | Type | Description |
---|---|---|
value | Double | double value |
Field Name | Type | Description |
---|---|---|
value | String | string value |
This object has no fields.
Field Name | Type | Description |
---|---|---|
value | Boolean | boolean value |
Field Name | Type | Description |
---|---|---|
ByteValue | Enum | Byte type |
ShortValue | Enum | Short integer type |
CharValue | Enum | Character type |
IntValue | Enum | Integer type |
LongValue | Enum | Long integer type |
FloatValue | Enum | Float type |
DoubleValue | Enum | Double precision float type |
StringValue | Enum | String type |
TypeValue | Enum | Type type |
MethodValue | Enum | Method type |
EnumValue | Enum | Enum type |
ArrayValue | Enum | Array type |
AnnotationValue | Enum | Annotation type |
NullValue | Enum | Null type |
BoolValue | Enum | Boolean type |
Field Name | Type | Description |
---|---|---|
Build | Enum | Visible at build time |
Runtime | Enum | Visible at runtime |
System | Enum | Visible to the system |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
Equals | Enum | Equals |
Field Name | Type | Description |
---|---|---|
ByteValue | Enum | Byte type |
ShortValue | Enum | Short integer type |
CharValue | Enum | Character type |
IntValue | Enum | Integer type |
LongValue | Enum | Long integer type |
FloatValue | Enum | Float type |
DoubleValue | Enum | Double precision float type |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
StartsWith | Enum | Starts with |
EndsWith | Enum | Ends with |
Equals | Enum | Equals |
This Enum corresponds to java.lang.annotation.RetentionPolicy
.
Field Name | Type | Description |
---|---|---|
Source | Enum | Source level |
Class | Enum | Class level |
Runtime | Enum | Runtime level |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
StartsWith | Enum | Starts with |
EndsWith | Enum | Ends with |
SimilarRegex | Enum | Regex-like pattern, supporting only ^ and $ |
Equals | Enum | Equals |
This Enum corresponds to java.lang.annotation.ElementType
.
Field Name | Type | Description |
---|---|---|
Type | Enum | Type |
Field | Enum | Field |
Method | Enum | Method |
Parameter | Enum | Parameter |
Constructor | Enum | Constructor |
LocalVariable | Enum | Local variable |
AnnotationType | Enum | Annotation type |
Package | Enum | Package |
TypeParameter | Enum | Type parameter |
TypeUse | Enum | Type use |
Field Name | Type | Description |
---|---|---|
Any | Enum | Any usage |
Get | Enum | Get usage |
Set | Enum | Set usage |
这里介绍了
DexKit
查询的结构组成,匹配器对象可以进行递归嵌套,一切条件都是可以选的。
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchIn | Collection<ClassData> | 在指定的类列表中搜索类 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | ClassMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索字段 |
searchInFields | Collection<FieldData> | 在指定的字段列表中搜索字段 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | FieldMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索方法 |
searchInMethods | Collection<MethodData> | 在指定的方法列表中搜索方法 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | MethodMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchIn | Collection<ClassData> | 在指定的类列表中搜索类 |
matchers | Collection<StringMatchersGroup> | 查询分组列表 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索方法 |
searchInMethods | Collection<MethodData> | 在指定的方法列表中搜索方法 |
matchers | Collection<StringMatchersGroup> | 查询分组列表 |
字段名 | 类型 | 说明 |
---|---|---|
modifiers | Int | 匹配的修饰符的 bit masks |
matchType | MatchType | 匹配模式 |
字段名 | 类型 | 说明 |
---|---|---|
byteValue | Byte | 匹配的字节 |
shortValue | Short | 匹配的短整型 |
charValue | Char | 匹配的字符 |
intValue | Int | 匹配的整型 |
longValue | Long | 匹配的长整型 |
floatValue | Float | 匹配的浮点型 |
doubleValue | Double | 匹配的双精度浮点 |
stringValue | StringMatcher | 匹配的字符串 |
methodValue | MethodMatcher | 匹配的方法 |
enumValue | FieldMatcher | 匹配的枚举 |
arrayValue | AnnotationEncodeArrayMatcher | 匹配的数组 |
annotationValue | AnnotationMatcher | 匹配的注解 |
nullValue | 无 | 匹配 null |
boolValue | Boolean | 匹配的布尔值 |
字段名 | 类型 | 说明 |
---|---|---|
min | Int | 最小值,默认为 0 |
max | Int | 最大值,默认为 Int.MAX_VALUE |
字段名 | 类型 | 说明 |
---|---|---|
byteValue | Byte | 匹配的字节 |
shortValue | Short | 匹配的短整型 |
charValue | Char | 匹配的字符 |
intValue | Int | 匹配的整型 |
longValue | Long | 匹配的长整型 |
floatValue | Float | 匹配的浮点型 |
doubleValue | Double | 匹配的双精度浮点 |
字段名 | 类型 | 说明 |
---|---|---|
opCodes | Collection<Int> | 匹配的操作码对应的 Int 值 |
opNames | Collection<String> | 匹配的操作码对应的名称,即 smali 语法中的名称 |
matchType | OpCodeMatchType | 匹配模式 |
size | IntRange | 该操作码总长度范围 |
字段名 | 类型 | 说明 |
---|---|---|
value | String | 匹配的字符串 |
matchType | StringMatchType | 匹配模式 |
ignoreCase | Boolean | 是否忽略大小写 |
字段名 | 类型 | 说明 |
---|---|---|
types | Collection<TargetElementType> | 匹配的注解声明的元素类型列表 |
matchType | MatchType | 匹配模式 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 匹配的名称 |
value | AnnotationEncodeValueMatcher | 匹配的值 |
字段名 | 类型 | 说明 |
---|---|---|
elements | Collection<AnnotationElementMatcher> | 匹配的注解声明的元素列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解声明的元素数量 |
字段名 | 类型 | 说明 |
---|---|---|
values | Collection<AnnotationEncodeValueMatcher> | 匹配的注解声明的元素列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解声明的元素数量 |
字段名 | 类型 | 说明 |
---|---|---|
type | ClassMatcher | 匹配的注解类型 |
targetElementTypes | TargetElementTypesMatcher | 匹配的注解声明的元素类型列表 |
policy | RetentionPolicyType | 匹配的注解声明的保留策略 |
elements | AnnotationElementsMatcher | 匹配的注解声明的元素列表 |
usingStrings | Collection<StringMatcher> | 注解中使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
annotations | Collection<AnnotationMatcher> | 匹配的注解列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解数量 |
字段名 | 类型 | 说明 |
---|---|---|
source | StringMatcher | 类的源码文件名,即 smali 中的 .source 字段 |
className | StringMatcher | 类的名称 |
modifiers | AccessFlagsMatcher | 类的修饰符 |
superClass | ClassMatcher | 类的父类 |
interfaces | InterfacesMatcher | 类的接口列表 |
annotations | AnnotationsMatcher | 类的注解列表 |
fields | FieldsMatcher | 类的字段列表 |
methods | MethodsMatcher | 类的方法列表 |
usingStrings | Collection<StringMatcher> | 类中使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
interfaces | Collection<ClassMatcher> | 匹配的接口列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的接口数量 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 字段的名称 |
modifiers | AccessFlagsMatcher | 字段的修饰符 |
declaredClass | ClassMatcher | 字段的声明类 |
type | ClassMatcher | 字段的类型 |
annotations | AnnotationsMatcher | 字段的注解 |
getMethods | MethodsMatcher | 读取该字段的方法列表 |
putMethods | MethodsMatcher | 设置该字段的方法列表 |
字段名 | 类型 | 说明 |
---|---|---|
fields | Collection<FieldMatcher> | 匹配的字段列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的字段数量 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 方法的名称 |
modifiers | AccessFlagsMatcher | 方法的修饰符 |
declaredClass | ClassMatcher | 方法的声明类 |
returnType | ClassMatcher | 方法的返回值类型 |
params | ParametersMatcher | 方法的参数列表 |
annotations | AnnotationsMatcher | 方法的注解 |
opCodes | OpcodesMatcher | 方法的操作码列表 |
usingStrings | Collection<StringMatcher> | 方法中使用的字符串列表 |
usingFields | Collection<UsingFieldMatcher> | 方法中使用的字段列表 |
usingNumbers | Collection<Number> | 方法中使用的数字列表 |
invokeMethods | MethodsMatcher | 方法中调用的方法列表 |
callMethods | MethodsMatcher | 调用了该方法的方法列表 |
字段名 | 类型 | 说明 |
---|---|---|
methods | Collection<MethodMatcher> | 匹配的方法列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的方法数量 |
字段名 | 类型 | 说明 |
---|---|---|
type | ClassMatcher | 参数的类型 |
annotations | AnnotationsMatcher | 参数的注解 |
字段名 | 类型 | 说明 |
---|---|---|
params | Collection<ParameterMatcher> | 匹配的参数列表 |
count | IntRange | 匹配的参数数量 |
字段名 | 类型 | 说明 |
---|---|---|
groupName | String | 分组名称 |
matchers | Collection<StringMatcher> | 该查询分组匹配对象所使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
field | FieldMatcher | 匹配的字段 |
usingType | UsingType | 使用类型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Byte | 字节 |
字段名 | 类型 | 说明 |
---|---|---|
value | Short | 短整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Char | 字符 |
字段名 | 类型 | 说明 |
---|---|---|
value | Int | 整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Long | 长整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Float | 浮点型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Double | 双精度浮点 |
字段名 | 类型 | 说明 |
---|---|---|
value | String | 字符串 |
该对象无字段。
字段名 | 类型 | 说明 |
---|---|---|
value | Boolean | 布尔值 |
字段名 | 类型 | 说明 |
---|---|---|
ByteValue | Enum | 字节类型 |
ShortValue | Enum | 短整型类型 |
CharValue | Enum | 字符类型 |
IntValue | Enum | 整型类型 |
LongValue | Enum | 长整型类型 |
FloatValue | Enum | 浮点类型 |
DoubleValue | Enum | 双精度浮点类型 |
StringValue | Enum | 字符串类型 |
TypeValue | Enum | 类型类型 |
MethodValue | Enum | 方法类型 |
EnumValue | Enum | 枚举类型 |
ArrayValue | Enum | 数组类型 |
AnnotationValue | Enum | 注解类型 |
NullValue | Enum | 空类型 |
BoolValue | Enum | 布尔类型 |
字段名 | 类型 | 说明 |
---|---|---|
Build | Enum | 构建时可见 |
Runtime | Enum | 运行时可见 |
System | Enum | 系统可见 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
Equals | Enum | 等于 |
字段名 | 类型 | 说明 |
---|---|---|
ByteValue | Enum | 字节类型 |
ShortValue | Enum | 短整型类型 |
CharValue | Enum | 字符类型 |
IntValue | Enum | 整型类型 |
LongValue | Enum | 长整型类型 |
FloatValue | Enum | 浮点类型 |
DoubleValue | Enum | 双精度浮点类型 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
StartsWith | Enum | 以...开头 |
EndsWith | Enum | 以...结尾 |
Equals | Enum | 等于 |
该 Enum 与 java.lang.annotation.RetentionPolicy
保持对应。
字段名 | 类型 | 说明 |
---|---|---|
Source | Enum | 源码级别 |
Class | Enum | 类级别 |
Runtime | Enum | 运行时级别 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
StartsWith | Enum | 以...开头 |
EndsWith | Enum | 以...结尾 |
SimilarRegex | Enum | 类正则匹配,只支持 ^ 与 $ |
Equals | Enum | 等于 |
该 Enum 与 java.lang.annotation.ElementType
保持对应。
字段名 | 类型 | 说明 |
---|---|---|
Type | Enum | 类型 |
Field | Enum | 字段 |
Method | Enum | 方法 |
Parameter | Enum | 参数 |
Constructor | Enum | 构造方法 |
LocalVariable | Enum | 局部变量 |
AnnotationType | Enum | 注解类型 |
Package | Enum | 包 |
TypeParameter | Enum | 类型参数 |
TypeUse | Enum | 类型使用 |
字段名 | 类型 | 说明 |
---|---|---|
Any | Enum | 任意 |
Get | Enum | 获取 |
Set | Enum | 设置 |
If you have any questions, feel free to reach out to us through the following means: Click to join our Telegram group
During the reading of this section, you may need to refer to the Structural Quick Reference Table for a better understanding.
You can get the source code and some test cases for the demo below.
Here is a simple Demo Activity, PlayActivity, where the internal properties and methods are obfuscated, and they change in each version.
The four major components are not obfuscated by default. We assume this Activity is obfuscated.
package org.luckypray.dexkit.demo;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.h;
+import java.util.Random;
+import org.luckypray.dexkit.demo.annotations.Router;
+
+@Router(path = "/play")
+public class PlayActivity extends AppCompatActivity {
+ private static final String TAG = "PlayActivity";
+ private TextView a;
+ private Handler b;
+
+ public void d(View view) {
+ Handler handler;
+ int i;
+ Log.d("PlayActivity", "onClick: rollButton");
+ float nextFloat = new Random().nextFloat();
+ if (nextFloat < 0.01d) {
+ handler = this.b;
+ i = -1;
+ } else if (nextFloat < 0.987f) {
+ handler = this.b;
+ i = 0;
+ } else {
+ handler = this.b;
+ i = 114514;
+ }
+ handler.sendEmptyMessage(i);
+ }
+
+ public void e(boolean z) {
+ int i;
+ if (!z) {
+ i = RandomUtil.a();
+ } else {
+ i = 6;
+ }
+ String a = h.a("You rolled a ", i);
+ this.a.setText(a);
+ Log.d("PlayActivity", "rollDice: " + a);
+ }
+
+ protected void onCreate(Bundle bundle) {
+ super/*androidx.fragment.app.FragmentActivity*/.onCreate(bundle);
+ setContentView(0x7f0b001d);
+ Log.d("PlayActivity", "onCreate");
+ HandlerThread handlerThread = new HandlerThread("PlayActivity");
+ handlerThread.start();
+ this.b = new PlayActivity$1(this, handlerThread.getLooper());
+ this.a = (TextView) findViewById(0x7f080134);
+ ((Button) findViewById(0x7f08013a)).setOnClickListener(new a(this));
+ }
+}
+
In this scenario, we want to find the PlayActivity using the following code:
This is just an example. In actual use, you don't need conditions as complex and comprehensive as this. Use as needed.
private fun findPlayActivity(bridge: DexKitBridge) {
+ val classData = bridge.findClass {
+ // Search within the specified package name range
+ searchPackages("org.luckypray.dexkit.demo")
+ // Exclude the specified package name range
+ excludePackages("org.luckypray.dexkit.demo.annotations")
+ // ClassMatcher Matcher for classes
+ matcher {
+ // FieldsMatcher Matcher for fields in a class
+ fields {
+ // Add a matcher for the field
+ add {
+ // Specify the modifiers of the field
+ modifiers = Modifier.PRIVATE or Modifier.STATIC or Modifier.FINAL
+ // Specify the type of the field
+ type = "java.lang.String"
+ // Specify the name of the field
+ name = "TAG"
+ }
+ // Add a matcher for the field of the specified type
+ addForType("android.widget.TextView")
+ addForType("android.os.Handler")
+ // Specify the number of fields in the class
+ count = 3
+ }
+ // MethodsMatcher Matcher for methods in a class
+ methods {
+ // Add a matcher for the method
+ add {
+ // Specify the modifiers of the method
+ modifiers = Modifier.PROTECTED
+ // Specify the name of the method
+ name = "onCreate"
+ // Specify the return type of the method
+ returnType = "void"
+ // Specify the parameter types of the method, if the parameter types are uncertain,
+ // use null, and this method will implicitly declare the number of parameters
+ paramTypes("android.os.Bundle")
+ // Specify the strings used in the method
+ usingStrings("onCreate")
+ }
+ add {
+ paramTypes("android.view.View")
+ // Specify the numbers used in the method, the type is Byte, Short, Int, Long, Float, Double
+ usingNumbers(0.01, -1, 0.987, 0, 114514)
+ }
+ add {
+ paramTypes("boolean")
+ // Specify the methods called in the method list
+ invokeMethods {
+ add {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "int"
+ // Specify the strings used in the method called in the method,
+ usingStrings(listOf("getRandomDice: "), StringMatchType.Equals)
+ }
+ // Only need to contain the call to the above method
+ matchType = MatchType.Contains
+ }
+ }
+ count(1..10)
+ }
+ // AnnotationsMatcher Matcher for annotations in a class
+ annotations {
+ // Add a matcher for the annotation
+ add {
+ // Specify the type of the annotation
+ type = "org.luckypray.dexkit.demo.annotations.Router"
+ // The annotation needs to contain the specified element
+ addElement {
+ // Specify the name of the element
+ name = "path"
+ // Specify the value of the element
+ stringValue("/play")
+ }
+ }
+ }
+ // Strings used by all methods in the class
+ usingStrings("PlayActivity", "onClick", "onCreate")
+ }
+ }.single()
+ println(classData.name)
+}
+
The result is as follows:
org.luckypray.dexkit.demo.PlayActivity
+
if there is such a class, its only feature is that the ancestors are not obfuscated, and the middle parents are all obfuscated, we can also use DexKit
to find it.
private fun findActivity(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher { // ClassMatcher
+ // androidx.appcompat.app.AppCompatActivity
+ superClass { // ClassMatcher
+ // androidx.fragment.app.FragmentActivity
+ superClass { // ClassMatcher
+ // androidx.activity.ComponentActivity
+ superClass { // ClassMatcher
+ // androidx.core.app.ComponentActivity
+ superClass { // ClassMatcher
+ superClass = "android.app.Activity"
+ }
+ }
+ }
+ }
+ }
+ }.forEach {
+ // org.luckypray.dexkit.demo.MainActivity
+ // org.luckypray.dexkit.demo.PlayActivity
+ println(it.name)
+ }
+}
+
The result is as follows:
org.luckypray.dexkit.demo.MainActivity
+org.luckypray.dexkit.demo.PlayActivity
+
Tips
In DexKit
, any logical relationship can be used as a query condition
If we need to find a method with an obfuscated parameter, we can use null
to replace it, so that it can match any type of parameter.
private fun findMethodWithFuzzyParam(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ // Specify the parameters of the method, if the parameters are uncertain, use null
+ paramTypes("android.view.View", null)
+ // paramCount = 2 // paramTypes length is 2, which has implicitly determined the number of parameters
+ usingStrings("onClick")
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
How can you serialize and save the results obtained from DexKit queries for later use?
DexKit provides corresponding packaging classes for Class, Method, and Field, namely DexClass
, DexMethod
, and DexField
. The wrapper class inherits the Serializable
interface, so it can be saved directly using Java's serialization method. For the objects returned by the query, you can directly use toDexClass()
, toDexMethod()
, toDexField()
methods to convert to a wrapper class. Of course, you can also use the Data object's descriptor
attribute, which is a Dailvik description
that identifies a unique object.
private fun saveData(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ paramTypes("android.view.View", null)
+ usingStrings("onClick")
+ }
+ }.single().let {
+ val descriptor = it.toDexMethod().descriptor
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ sp.edit().putString("onClickMethod", descriptor).apply()
+ }
+}
+
+private fun readData(): Method {
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ val descriptor = sp.getString("onClickMethod", null)
+ if (descriptor != null) {
+ val dexMethod = DexMethod(descriptor)
+ val method = dexMethod.getMethodInstance(hostClassLoader)
+ return method
+ }
+ error("No saved")
+}
+
DexKit
is a high-performance runtime parsing library for dex implemented in C++, used to search for obfuscated classes, methods, or properties.
In the development of Xposed modules, we often need to hook specific methods. However, due to code obfuscation, module developers often have to maintain multiple versions of hook points to ensure compatibility of the module across different versions. This adaptation approach is cumbersome and error-prone.
Is there a better solution? Some developers may consider traversing all classes in the ClassLoader and traversing the characteristics of the classes through reflection, such as method names, parameter types, return value types, and annotations, and then adapting based on these features. However, this approach also has obvious drawbacks. First, due to the time-consuming nature of Java's reflection mechanism itself, the search speed is affected by device performance. Secondly, in complex conditions, the search may take a long time, and in extreme conditions, it may even exceed 30 seconds. In addition, forcibly loading some classes may cause unpredictable problems in the host APP.
Typically, developers decompile the host APP to obtain smali or decompiled Java code and search the code based on known features. The results are then written into the adaptation file. To simplify this process, we need an automated way. Currently, most solutions for parsing Dex files rely on dexlib2
, but due to its development in Java, there are performance bottlenecks. Especially when the host application has a large number of dex files, the parsing time is long, affecting the user experience. Therefore, DexKit
came into being. It is implemented in C++, providing superior performance, internal optimizations using multi-threading and various algorithms, enabling complex searches to be completed in a very short time.
It is recommended to use Kotlin for development because it provides DSL support, allowing us to have a better experience when using DexKit
. If you are not familiar with Kotlin, you don't need to worry either; the API also provides corresponding chain call support, which allows Java developers to have a good experience.
All example code in the documentation will be written in Kotlin. You can easily understand the corresponding Java usage through the examples here.
Here we provide some basic knowledge to help you better understand the usage of
DexKit
. Experienced developers can skip this section.
When using DexKit
, there are some fundamental concepts you need to understand, including but not limited to:
Notice
The content in the basic knowledge is not necessarily completely accurate. Please read it according to your own understanding. If you find any inaccuracies, feel free to point them out and help improve.
Usually, you can use jadx to meet most of your needs. It can restore readable Java code in most cases.
Type Signature | Primitive Type | Size (Bytes) |
---|---|---|
V | void | - |
Z | boolean | 1 |
B | byte | 1 |
C | char | 2 |
S | short | 2 |
I | int | 4 |
J | long | 8 |
F | float | 4 |
D | double | 8 |
Reference types are divided into classes and arrays.
The type signature of a class starts with L
, followed by the fully qualified name (FullClassName) of the class, and ends with ;
. For example, Ljava/lang/String;
represents the java.lang.String
class.
For example:
Type Signature | Java Type Definition |
---|---|
Ljava/lang/String; | java.lang.String |
Ljava/util/List; | java.util.List |
The type signature of an array starts with [
, followed by the type signature of the array elements. For example, [[I
represents a two-dimensional array where the element type is int
.
For example:
Type Signature | Java Type Definition |
---|---|
[I | int[] |
[[C | char[][] |
[Ljava/lang/String; | java.lang.String[] |
Tips
The term 'class' and 'type' are not entirely equivalent: 'type' is Type, while 'class' is Class. 'Class' is a subset of 'type'. For example:
java.lang.Integer
is a 'class' and also a 'type'java.lang.Integer[]
is an 'array type', but not a 'class'int
is a 'primitive type', but not a 'class'For method parameters, return types, and field types, we use the term 'type,' specifically 'Type.'
A method signature consists of the signature of the return type and the signature of the parameter types. For example, ()V
represents a parameterless void
method.
For example:
For ease of description, all methods in the table are named as
function
.
Method Signature | Java Method Definition |
---|---|
()V | void function() |
(I)V | void function(int) |
(II)V | void function(int, int) |
(ILjava/lang/String;J)V | void function(int, java.lang.String, long) |
(I[II)V | void function(int, int[], int) |
([[Ljava/lang/String;)V | void function(java.lang.String[][]) |
()[Ljava/lang/String; | java.lang.String[] function() |
In a Dex file, we can represent specific classes, methods, or fields using the 'Dalvik Descriptor.' In DexKit
API, the term 'descriptor' is commonly used.
The format of a class descriptor is [class signature]
, such as Ljava/lang/String;
.
The format of a method descriptor is [class signature]->[method name][method signature]
, such as Ljava/lang/String;->length()I
.
Tips
In 'Dalvik Descriptor,' the method name for constructors is <init>
, and for static initialization methods, it's <clinit>
. Therefore, in DexKit
, to find constructors, you need to use <init>
as the method name.
The format of a field descriptor is [class signature]->[field name]:[type signature]
, such as Ljava/lang/String;->count:I
.
Tips
In DexKit, the className/Type query parameter only supports the Java primitive syntax. For example:
void
, int
, boolean
.java.lang.String
or java/lang/String
.int[]
, java.lang.String[][]
or java/lang/String[][]
.In DexKit, various queries may achieve the same functionality, but the difference in performance can be significant, varying by several tens of times. This section will introduce some techniques for performance optimization.
At the native layer, DexKit maintains lists of classes, methods, and fields in the Dex file. How does DexKit scan these lists in several APIs? The traversal order of findClass
, findMethod
, and findField
is based on the respective lists' sequential order. Then, each condition is matched one by one.
Some users may use the declaredClass
condition to write queries like the following when using:
private fun badCode(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ declaredClass {
+ usingStrings("getUid", "", "_event")
+ }
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
This search takes 4310ms
.
At first glance, this query seems fine, but in reality, its performance is very poor. Why? As mentioned earlier, the findMethod
API traverses all methods and then matches each condition one by one. However, there is a many-to-one relationship between methods and classes, meaning a class may contain multiple methods, but a method can only belong to one class. Therefore, during the process of traversing all methods, each method will be matched once with the declaredClass
condition, leading to performance waste.
So, let's change our approach. By first searching for declaredClass
and then using chain calls, we can search for methods within the classes that meet the criteria. Won't this help avoid the issue?
private fun goodCode(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher {
+ usingStrings("getUid", "", "_event")
+ }
+ }.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
This search takes 77ms
, showing a performance improvement by several tens of times.
When using findMethod
or findField
, the declaredClass
condition should be avoided as much as possible.
Integrate
DexKit
into your project.
Make sure your development environment meets the following requirements:
Notice
If your project's minSdkVersion is less than 23, using System.loadLibrary("dexkit")
within an Xposed module may throw a java.lang.UnsatisfiedLinkError: xxx couldn't find "libdexkit.so"
exception. This is because the so files under the lib/
directory are compressed by default during packaging, making it impossible to load the so file via System.loadLibrary
. The solution is to add the following configuration in app/build.gradle
:
android {
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
+ }
+}
+
or manually extract the libdexkit.so file from the lib/ directory within the apk to any readable and writable directory, and then load it using System.load("/path/to/libdexkit.so")
.
Add the dependency for dexkit in your project's
app/build.gradle
orapp/build.gradle.kts
.
dependencies {
+ // replace <version> with your desired version, e.g. '2.0.0-rc1'
+ implementation 'org.luckypray:dexkit:<version>'
+}
+
dependencies {
+ // replace <version> with your desired version, e.g. '2.0.0-rc1'
+ implementation("org.luckypray:dexkit:<version>")
+}
+
Tips
Starting from DexKit 2.0, the new ArtifactId has been changed from DexKit
to dexkit
.
Now that you have successfully integrated DexKit
into your project, next we will introduce how to use DexKit
to achieve some common requirements.
Starting from version
1.1.0
, DexKit supports running on desktop platforms without the need for packaging as an APK for testing on Android.
The basic runtime environment requires gcc/clang, cmake, and ninja/make.
Windows
users can use MSYS2 to set up the runtime environment. Since all Windows systems are currently 64-bit, we use mingw64.exe
for dependency installation:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
+
After installation, we need to add the mingw64/bin
directory to the environment variables for future use.
warning
DexKit will use ninja
as the default build system by default. If you need to use make
in mingw for building, you need to execute pacman -S mingw-w64-x86_64-make
, after installation, you need to rename msys64\mingw64\bin\mingw32-make.exe
to make.exe
or add it as a shortcut, otherwise the build will fail due to gradle-cmake-plugin
not finding the make command. Additionally, delete generator.set(generators.ninja)
in :dexkit/build.gradle
, or modify it to generator.set(generators.unixMakefiles)
.
On Linux, you normally only need to install ninja
to use it.
It's recommended to use HomeBrew for dependency management.
brew install cmake ninja
+
git clone https://github.com/LuckyPray/DexKit.git
+
Execute the submodule :main
to perform testing.
gradle :main:run
+
This section introduces the structure components of
DexKit
queries. Matcher objects can be recursively nested, and all conditions are optional.
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchIn | Collection<ClassData> | Search for classes in the specified list of classes |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | ClassMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for fields in the specified list of classes |
searchInFields | Collection<FieldData> | Search for fields in the specified list of fields |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | FieldMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for methods in the specified list of classes |
searchInMethods | Collection<MethodData> | Search for methods in the specified list of methods |
findFirst | Boolean | Return the first result found immediately; results are not guaranteed to be unique due to multi-threading |
matcher | MethodMatcher | Matching conditions |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchIn | Collection<ClassData> | Search for classes in the specified list of classes |
matchers | Collection<StringMatchersGroup> | List of query groups using strings |
Field Name | Type | Description |
---|---|---|
searchPackages | Collection<String> | List of package names to search |
excludePackages | Collection<String> | List of excluded package names |
ignorePackagesCase | Boolean | Whether to ignore package name case |
searchInClasses | Collection<ClassData> | Search for methods in the specified list of classes |
searchInMethods | Collection<MethodData> | Search for methods in the specified list of methods |
matchers | Collection<StringMatchersGroup> | List of query groups using strings |
Field Name | Type | Description |
---|---|---|
modifiers | Int | Bit masks for matching modifiers |
matchType | MatchType | Matching mode |
Field Name | Type | Description |
---|---|---|
byteValue | Byte | Matched byte |
shortValue | Short | Matched short integer |
charValue | Char | Matched character |
intValue | Int | Matched integer |
longValue | Long | Matched long integer |
floatValue | Float | Matched float |
doubleValue | Double | Matched double precision float |
stringValue | StringMatcher | Matched string |
methodValue | MethodMatcher | Matched method |
enumValue | FieldMatcher | Matched enumeration |
arrayValue | AnnotationEncodeArrayMatcher | Matched array |
annotationValue | AnnotationMatcher | Matched annotation |
nullValue | None | Match null |
boolValue | Boolean | Matched boolean |
Field Name | Type | Description |
---|---|---|
min | Int | Minimum value, default is 0 |
max | Int | Maximum value, default is Int.MAX_VALUE |
Field Name | Type | Description |
---|---|---|
byteValue | Byte | Matched byte |
shortValue | Short | Matched short integer |
charValue | Char | Matched character |
intValue | Int | Matched integer |
longValue | Long | Matched long integer |
floatValue | Float | Matched float |
doubleValue | Double | Matched double precision float |
Field Name | Type | Description |
---|---|---|
opCodes | Collection<Int> | Matched Int values of opcodes |
opNames | Collection<String> | Matched names of opcodes (as per smali syntax) |
matchType | OpCodeMatchType | Matching mode |
size | IntRange | Total length range of the opcode |
Field Name | Type | Description |
---|---|---|
value | String | Matched string |
matchType | StringMatchType | Matching mode |
ignoreCase | Boolean | Ignore case sensitivity |
Field Name | Type | Description |
---|---|---|
types | Collection<TargetElementType> | List of matched annotation element types |
matchType | MatchType | Matching mode |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Matched name |
value | AnnotationEncodeValueMatcher | Matched value |
Field Name | Type | Description |
---|---|---|
elements | Collection<AnnotationElementMatcher> | List of matched annotation elements |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched elements |
Field Name | Type | Description |
---|---|---|
values | Collection<AnnotationEncodeValueMatcher> | List of matched annotation elements |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched elements |
Field Name | Type | Description |
---|---|---|
type | ClassMatcher | Matched annotation type |
targetElementTypes | TargetElementTypesMatcher | List of matched annotation element types |
policy | RetentionPolicyType | Matched retention policy |
elements | AnnotationElementsMatcher | List of matched annotation elements |
usingStrings | Collection<StringMatcher> | List of strings used in the annotation |
Field Name | Type | Description |
---|---|---|
annotations | Collection<AnnotationMatcher> | List of matched annotations |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched annotations |
Field Name | Type | Description |
---|---|---|
source | StringMatcher | Source file name of the class, i.e., .source field in smali |
className | StringMatcher | Name of the class |
modifiers | AccessFlagsMatcher | Modifiers of the class |
superClass | ClassMatcher | Superclass of the class |
interfaces | InterfacesMatcher | List of interfaces implemented by the class |
annotations | AnnotationsMatcher | List of annotations for the class |
fields | FieldsMatcher | List of fields in the class |
methods | MethodsMatcher | List of methods in the class |
usingStrings | Collection<StringMatcher> | List of strings used in the class |
Field Name | Type | Description |
---|---|---|
interfaces | Collection<ClassMatcher> | List of matched interfaces |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched interfaces |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Name of the field |
modifiers | AccessFlagsMatcher | Modifiers of the field |
declaredClass | ClassMatcher | Declaring class of the field |
type | ClassMatcher | Type of the field |
annotations | AnnotationsMatcher | List of annotations for the field |
getMethods | MethodsMatcher | List of methods to get the field |
putMethods | MethodsMatcher | List of methods to set the field |
Field Name | Type | Description |
---|---|---|
fields | Collection<FieldMatcher> | List of matched fields |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched fields |
Field Name | Type | Description |
---|---|---|
name | StringMatcher | Name of the method |
modifiers | AccessFlagsMatcher | Modifiers of the method |
declaredClass | ClassMatcher | Declaring class of the method |
returnType | ClassMatcher | Return type of the method |
params | ParametersMatcher | List of parameters |
annotations | AnnotationsMatcher | List of annotations for the method |
opCodes | OpcodesMatcher | List of opcodes for the method |
usingStrings | Collection<StringMatcher> | List of strings used in the method |
usingFields | Collection<UsingFieldMatcher> | List of fields used in the method |
usingNumbers | Collection<Number> | List of numbers used in the method |
invokeMethods | MethodsMatcher | List of methods invoked by the method |
callMethods | MethodsMatcher | List of methods that call the method |
Field Name | Type | Description |
---|---|---|
methods | Collection<MethodMatcher> | List of matched methods |
matchType | MatchType | Matching mode |
count | IntRange | Number of matched methods |
Field Name | Type | Description |
---|---|---|
type | ClassMatcher | Type of the parameter |
annotations | AnnotationsMatcher | List of annotations for the parameter |
Field Name | Type | Description |
---|---|---|
params | Collection<ParameterMatcher> | List of matched parameters |
count | IntRange | Number of matched parameters |
Field Name | Type | Description |
---|---|---|
groupName | String | Group name for the query |
matchers | Collection<StringMatcher> | List of strings used in the query |
Field Name | Type | Description |
---|---|---|
field | FieldMatcher | Matched field |
usingType | UsingType | Type of usage |
Field Name | Type | Description |
---|---|---|
value | Byte | byte value |
Field Name | Type | Description |
---|---|---|
value | Short | short integer value |
Field Name | Type | Description |
---|---|---|
value | Char | character value |
Field Name | Type | Description |
---|---|---|
value | Int | integer value |
Field Name | Type | Description |
---|---|---|
value | Long | long integer value |
Field Name | Type | Description |
---|---|---|
value | Float | float value |
Field Name | Type | Description |
---|---|---|
value | Double | double value |
Field Name | Type | Description |
---|---|---|
value | String | string value |
This object has no fields.
Field Name | Type | Description |
---|---|---|
value | Boolean | boolean value |
Field Name | Type | Description |
---|---|---|
ByteValue | Enum | Byte type |
ShortValue | Enum | Short integer type |
CharValue | Enum | Character type |
IntValue | Enum | Integer type |
LongValue | Enum | Long integer type |
FloatValue | Enum | Float type |
DoubleValue | Enum | Double precision float type |
StringValue | Enum | String type |
TypeValue | Enum | Type type |
MethodValue | Enum | Method type |
EnumValue | Enum | Enum type |
ArrayValue | Enum | Array type |
AnnotationValue | Enum | Annotation type |
NullValue | Enum | Null type |
BoolValue | Enum | Boolean type |
Refer to Visibility values
Field Name | Type | Description |
---|---|---|
Build | Enum | Visible at build time |
Runtime | Enum | Visible at runtime |
System | Enum | Visible to the system |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
Equals | Enum | Equals |
Field Name | Type | Description |
---|---|---|
ByteValue | Enum | Byte type |
ShortValue | Enum | Short integer type |
CharValue | Enum | Character type |
IntValue | Enum | Integer type |
LongValue | Enum | Long integer type |
FloatValue | Enum | Float type |
DoubleValue | Enum | Double precision float type |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
StartsWith | Enum | Starts with |
EndsWith | Enum | Ends with |
Equals | Enum | Equals |
This Enum corresponds to java.lang.annotation.RetentionPolicy
.
Field Name | Type | Description |
---|---|---|
Source | Enum | Source level |
Class | Enum | Class level |
Runtime | Enum | Runtime level |
Field Name | Type | Description |
---|---|---|
Contains | Enum | Contains |
StartsWith | Enum | Starts with |
EndsWith | Enum | Ends with |
SimilarRegex | Enum | Regex-like pattern, supporting only ^ and $ |
Equals | Enum | Equals |
This Enum corresponds to java.lang.annotation.ElementType
.
Field Name | Type | Description |
---|---|---|
Type | Enum | Type |
Field | Enum | Field |
Method | Enum | Method |
Parameter | Enum | Parameter |
Constructor | Enum | Constructor |
LocalVariable | Enum | Local variable |
AnnotationType | Enum | Annotation type |
Package | Enum | Package |
TypeParameter | Enum | Type parameter |
TypeUse | Enum | Type use |
Field Name | Type | Description |
---|---|---|
Any | Enum | Any usage |
Get | Enum | Get usage |
Set | Enum | Set usage |
如果您有任何问题,可以通过以下方式联系我们: 点击加入 Telegram 群组
在阅读本部分内容的过程中可能需要搭配结构速查表以获得更好的理解。
您可以在下方获取 Demo 的源码以及部分测试用例
下面是一个简单的 Demo Activity。这个 PlayActivity 内部属性以及方法都是被混淆的,且每个版本都会发生变化。
四大组件默认不会混淆,我们假设这个 Activity 被混淆了。
package org.luckypray.dexkit.demo;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.h;
+import java.util.Random;
+import org.luckypray.dexkit.demo.annotations.Router;
+
+@Router(path = "/play")
+public class PlayActivity extends AppCompatActivity {
+ private static final String TAG = "PlayActivity";
+ private TextView a;
+ private Handler b;
+
+ public void d(View view) {
+ Handler handler;
+ int i;
+ Log.d("PlayActivity", "onClick: rollButton");
+ float nextFloat = new Random().nextFloat();
+ if (nextFloat < 0.01d) {
+ handler = this.b;
+ i = -1;
+ } else if (nextFloat < 0.987f) {
+ handler = this.b;
+ i = 0;
+ } else {
+ handler = this.b;
+ i = 114514;
+ }
+ handler.sendEmptyMessage(i);
+ }
+
+ public void e(boolean z) {
+ int i;
+ if (!z) {
+ i = RandomUtil.a();
+ } else {
+ i = 6;
+ }
+ String a = h.a("You rolled a ", i);
+ this.a.setText(a);
+ Log.d("PlayActivity", "rollDice: " + a);
+ }
+
+ protected void onCreate(Bundle bundle) {
+ super/*androidx.fragment.app.FragmentActivity*/.onCreate(bundle);
+ setContentView(0x7f0b001d);
+ Log.d("PlayActivity", "onCreate");
+ HandlerThread handlerThread = new HandlerThread("PlayActivity");
+ handlerThread.start();
+ this.b = new PlayActivity$1(this, handlerThread.getLooper());
+ this.a = (TextView) findViewById(0x7f080134);
+ ((Button) findViewById(0x7f08013a)).setOnClickListener(new a(this));
+ }
+}
+
此时我们想得到这个 PlayActivity 可以使用如下代码:
这仅仅是个样例,实际使用中并不需要这么复杂且全面的条件,按需使用即可。
private fun findPlayActivity(bridge: DexKitBridge) {
+ val classData = bridge.findClass {
+ // 指定搜索的包名范围
+ searchPackages("org.luckypray.dexkit.demo")
+ // 排除指定的包名范围
+ excludePackages("org.luckypray.dexkit.demo.annotations")
+ // ClassMatcher 针对类的匹配器
+ matcher {
+ // FieldsMatcher 针对类中包含字段的匹配器
+ fields {
+ // 添加对于字段的匹配器
+ add {
+ // 指定字段的修饰符
+ modifiers = Modifier.PRIVATE or Modifier.STATIC or Modifier.FINAL
+ // 指定字段的类型
+ type = "java.lang.String"
+ // 指定字段的名称
+ name = "TAG"
+ }
+ // 添加指定字段的类型的字段匹配器
+ addForType("android.widget.TextView")
+ addForType("android.os.Handler")
+ // 指定类中字段的数量
+ count = 3
+ }
+ // MethodsMatcher 针对类中包含方法的匹配器
+ methods {
+ // 添加对于方法的匹配器
+ add {
+ // 指定方法的修饰符
+ modifiers = Modifier.PROTECTED
+ // 指定方法的名称
+ name = "onCreate"
+ // 指定方法的返回值类型
+ returnType = "void"
+ // 指定方法的参数类型,如果参数类型不确定,使用 null,使用此方法会隐式声明参数个数
+ paramTypes("android.os.Bundle")
+ // 指定方法中使用的字符串
+ usingStrings("onCreate")
+ }
+ add {
+ paramTypes("android.view.View")
+ // 指定方法中使用的数字,类型为 Byte, Short, Int, Long, Float, Double 之一
+ usingNumbers(0.01, -1, 0.987, 0, 114514)
+ }
+ add {
+ paramTypes("boolean")
+ // 指定方法中调用的方法列表
+ invokeMethods {
+ add {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "int"
+ // 指定方法中调用的方法中使用的字符串,所有字符串均使用 Equals 匹配
+ usingStrings(listOf("getRandomDice: "), StringMatchType.Equals)
+ }
+ // 只需要包含上述方法的调用即可
+ matchType = MatchType.Contains
+ }
+ }
+ // 指定类中方法的数量,最少不少于1个,最多不超过10个
+ count(1..10)
+ }
+ // AnnotationsMatcher 针对类中包含注解的匹配器
+ annotations {
+ // 添加对于注解的匹配器
+ add {
+ // 指定注解的类型
+ type = "org.luckypray.dexkit.demo.annotations.Router"
+ // 该注解需要包含指定的 element
+ addElement {
+ // 指定 element 的名称
+ name = "path"
+ // 指定 element 的值
+ stringValue("/play")
+ }
+ }
+ }
+ // 类中所有方法使用的字符串
+ usingStrings("PlayActivity", "onClick", "onCreate")
+ }
+ }.single()
+ println(classData.name)
+}
+
获得的结果如下:
org.luckypray.dexkit.demo.PlayActivity
+
如果存在这么一个类,它唯一的特征是祖辈没被混淆,中间的父类都被混淆了,我们也可以使用 DexKit
来找到它。
private fun findActivity(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher { // ClassMatcher
+ // androidx.appcompat.app.AppCompatActivity
+ superClass { // ClassMatcher
+ // androidx.fragment.app.FragmentActivity
+ superClass { // ClassMatcher
+ // androidx.activity.ComponentActivity
+ superClass { // ClassMatcher
+ // androidx.core.app.ComponentActivity
+ superClass { // ClassMatcher
+ superClass = "android.app.Activity"
+ }
+ }
+ }
+ }
+ }
+ }.forEach {
+ // org.luckypray.dexkit.demo.MainActivity
+ // org.luckypray.dexkit.demo.PlayActivity
+ println(it.name)
+ }
+}
+
获得的结果如下:
org.luckypray.dexkit.demo.MainActivity
+org.luckypray.dexkit.demo.PlayActivity
+
小提示
在 DexKit
中,一切符合逻辑的关系都可以作为查询条件
如果我们需要寻找的方法中存在一个被混淆的参数,我们可以使用 null
来替代,这样它就能匹配任意类型的参数。
private fun findMethodWithFuzzyParam(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ // 指定方法的参数类型,如果参数类型不确定,使用 null
+ paramTypes("android.view.View", null)
+ // paramCount = 2 // paramTypes 长度为 2 已经隐式确定了参数个数
+ usingStrings("onClick")
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
使用 DexKit 查询到的结果如何序列化保存下来,以便下次使用呢?
DexKit 中对 Class、Method、Field 提供了相应的包装类,分别是 DexClass
、DexMethod
、DexField
。 包装类继承了 Serializable
接口,因此可以直接使用 Java 的序列化方式来保存。对于查询返回的对象,可以直接使用 toDexClass()
、toDexMethod()
、toDexField()
方法来转换为包装类。当然,您也可以使用 Data 对象的 descriptor
属性来保存,它是一个 Dailvik 描述
标识了唯一的对象。
private fun saveData(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "void"
+ paramTypes("android.view.View", null)
+ usingStrings("onClick")
+ }
+ }.single().let {
+ val descriptor = it.toDexMethod().descriptor
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ sp.edit().putString("onClickMethod", descriptor).apply()
+ }
+}
+
+private fun readData(): Method {
+ val sp = getSharedPreferences("dexkit", Context.MODE_PRIVATE)
+ val descriptor = sp.getString("onClickMethod", null)
+ if (descriptor != null) {
+ val dexMethod = DexMethod(descriptor)
+ val method = dexMethod.getMethodInstance(hostClassLoader)
+ return method
+ }
+ error("No saved")
+}
+
DexKit
是一个使用 C++ 实现的 dex 高性能运行时解析库,用于查找被混淆的类、方法或者属性。
在 Xposed 模块的开发中,我们通常需要对特定的方法进行 hook。然而,由于代码混淆,模块开发者往往不得不维护多个版本的 hook 点,以确保模块在不同版本下的兼容性。这种适配方式繁琐且容易出错。
有没有更好的解决方案呢?有些开发者可能会考虑遍历 ClassLoader 中的所有类,并通过反射遍历类的特征,如方法名、参数类型、 返回值类型和注解等,然后根据这些特征进行适配。然而,这种方式也有明显的缺点。首先,由于 Java 的反射机制本身耗时较长, 查找速度受设备性能影响。其次,在复杂条件下,查找可能需要较长时间,极端条件下甚至可能超过 30 秒。另外, 强行加载部分类可能会导致宿主 APP 出现不可预知的问题。
通常,开发者会对宿主 APP 进行反编译,获取 smali 或反编译后的 Java 代码,并通过已知的特征对代码进行搜索。 然后将结果写入适配文件。为了简化这个过程,我们需要一种自动化的方式。目前针对 Dex 文件的解析方案大多依赖于 dexlib2
,但由于其是用 Java 开发,性能存在瓶颈。特别是当宿主应用存在大量 dex 文件时,解析时间会很长, 影响用户体验。为此,DexKit
应运而生。它采用 C++ 实现,性能优越的同时且内部使用多线程和多种算法进行优化, 能够在极短时间内完成复杂的搜索。
推荐使用 Kotlin 进行开发,因为其提供了 DSL 支持,可以让我们在使用 DexKit
时获得更好的体验。如果您不熟悉 Kotlin,也无需担心,API 也提供了相应的链式调用支持,同样能让 Java 开发者获得良好的体验。
文档中的所有示例代码都将使用 Kotlin 编写。您可以通过 这里 的示例,轻松了解相应的 Java 写法。
这里提供了一些基础知识,帮助您更好的理解
DexKit
的使用,已经掌握的开发者可以跳过这一章节。
在使用 DexKit
时,有一些基础知识是必要的,其中包括但不限于以下内容:
注意
基础知识中的内容不一定完全准确,请根据自己的见解酌情阅读,若发现内容有误,欢迎指正并帮助改进。
通常,您可以使用 jadx 来满足大部分需求, 它在大多数情况下能还原出可读的 Java 代码。
类型签名 | 原始类型 | 大小(字节) |
---|---|---|
V | void | - |
Z | boolean | 1 |
B | byte | 1 |
C | char | 2 |
S | short | 2 |
I | int | 4 |
J | long | 8 |
F | float | 4 |
D | double | 8 |
引用类型分为类与数组。
类的类型签名都是以 L
开头,以 ;
结尾,中间是类的全限定名(FullClassName),如 Ljava/lang/String;
。
例如:
类型签名 | Java 中类型定义 |
---|---|
Ljava/lang/String; | java.lang.String |
Ljava/util/List; | java.util.List |
数组类型的类型签名以 [
开头,后面跟着数组元素的类型签名,如 [[I
表示一个二维数组,数组中的元素类型是 int
。
例如:
类型签名 | Java 中类型定义 |
---|---|
[I | int[] |
[[C | char[][] |
[Ljava/lang/String; | java.lang.String[] |
小提示
类
与 类型
并不完全等价:类型为 Type,而类为 Class。 类
是 类型
的子集。 例如:
java.lang.Integer
是 类
,也是 类型
java.lang.Integer[]
是 数组类型
,但不是 类
int
是 原始类型
,但不是 类
对于方法参数,返回值类型以及字段的类型,我们统一称为 类型
即 Type
方法签名由方法的返回值类型签名和参数类型签名组成,如 ()V
表示一个无参的 void
方法。
例如:
为了方便表述,表格中所有的方法都命名为
function
方法签名 | Java 中方法定义 |
---|---|
()V | void function() |
(I)V | void function(int) |
(II)V | void function(int, int) |
(ILjava/lang/String;J)V | void function(int, java.lang.String, long) |
(I[II)V | void function(int, int[], int) |
([[Ljava/lang/String;)V | void function(java.lang.String[][]) |
()[Ljava/lang/String; | java.lang.String[] function() |
在 Dex 文件中,我们可以通过 Dalvik 描述
的方式来表示特定的类、方法或字段。在 DexKit
API中,通常使用 descriptor
来命名。
类描述的格式为 [类签名]
,如 Ljava/lang/String;
。
方法描述的格式为 [类签名]->[方法名][方法签名]
,如 Ljava/lang/String;->length()I
。
小提示
在 Dalvik 描述
中,构造函数的方法名为 <init>
,静态初始化函数的方法名为 <clinit>
。 所以在 DexKit
中如果想要查找构造函数,需要使用 <init>
作为方法名。
字段描述的格式为 [类签名]->[字段名]:[类型签名]
,如 Ljava/lang/String;->count:I
。
小提示
DexKit 中 className/Type 查询参数只支持 Java 原始写法,例如:
void
,int
,boolean
形式的 Java PrimitiveTypejava.lang.String
或者 java/lang/String
形式的 FullClassNameint[]
,java.lang.String[][]
或者 java/lang/String[][]
形式的 ArrayTypeName在 DexKit 中,多种查询或许能实现同样的功能,但是性能差距却可能相差几十倍。本节将介绍一些性能优化的技巧。
在 native 层,DexKit 会维护 Dex 中的类、方法以及字段的列表,那么在几个 API 中,DexKit 是如何扫描这些列表的呢?findClass
、findMethod
、findField
的遍历顺序均是按照各自列表的先后顺序进行遍历,然后再逐一对各个条件进行匹配。
可能有些用户在使用 findMethod
或 findField
时,会使用 declaredClass
条件写出如下的查询:
private fun badCode(bridge: DexKitBridge) {
+ bridge.findMethod {
+ matcher {
+ declaredClass {
+ usingStrings("getUid", "", "_event")
+ }
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
这个搜索耗时 4310ms
。 乍一看这个查询似乎没有什么问题,但是实际上这个查询的性能是非常差。为什么?前面提到过,findMethod
API 会遍历一遍所有的方法,然后再逐一对各个条件进行匹配。而 method 与 class 之间却是一个多对一的关系,即一个 class 中可能包含多个 method,但是一个 method 只能属于一个 class。因此,遍历所有方法的过程中,每个 method 都会被匹配一次 declaredClass
条件,这就导致了性能的浪费。
那么,我们换一个思路,先搜索 declaredClass,配合链式调用就能在符合条件的类中再搜索 method,这样不就可以避免了吗?
private fun goodCode(bridge: DexKitBridge) {
+ bridge.findClass {
+ matcher {
+ usingStrings("getUid", "", "_event")
+ }
+ }.findMethod {
+ matcher {
+ modifiers = Modifier.PUBLIC or Modifier.STATIC
+ returnType = "long"
+ addInvoke {
+ name = "parseLong"
+ }
+ addInvoke {
+ name = "toString"
+ }
+ }
+ }.single().let {
+ println(it)
+ }
+}
+
这个搜索耗时 77ms
, 性能提升了数十倍之多。
在使用 findMethod
或 findField
时,尽量避免使用 declaredClass
附带过于复杂的逻辑。
集成
DexKit
到您的项目中。
确保您的开发环境满足以下要求:
注意
如果您的项目的 minSdkVersion 小于 23,在 Xposed 模块内使用 System.loadLibrary("dexkit")
时可能会抛出 java.lang.UnsatisfiedLinkError: xxx couldn't find "libdexkit.so"
的异常。这是因为打包时默认会压缩 lib/
目录下的 so 文件,导致无法通过 System.loadLibrary
加载 so 文件。解决方案是在 app/build.gradle
中添加以下配置:
android {
+ packagingOptions {
+ jniLibs {
+ useLegacyPackaging true
+ }
+ }
+}
+
或者手动解压 apk 内的 lib/ 目录下的 libdexkit.so 文件至任意可读写目录,然后通过 System.load("/path/to/libdexkit.so") 加载。
在您的项目中的
app/build.gradle
或者app/build.gradle.kts
添加dexkit
的依赖。
dependencies {
+ // 将 <version> 替换为您需要的版本,例如 '2.0.0-rc1'
+ implementation 'org.luckypray:dexkit:<version>'
+}
+
dependencies {
+ // 将 <version> 替换为您需要的版本,例如 '2.0.0-rc1'
+ implementation("org.luckypray:dexkit:<version>")
+}
+
小提示
从 DexKit 2.0 开始,新的 ArtifactId 已从 DexKit
更改为 dexkit
。
现在您已经成功集成了 DexKit
到您的项目中,接下来我们将会介绍如何使用 DexKit
来完成一些常见的需求。
从
1.1.0
开始,DexKit
支持桌面平台运行,无需打包成 apk 在 Android 进行测试工作。
需要 gcc/clang、cmake 以及 ninja/make 作为基础运行环境。
Windows
用户可以使用 MSYS2 搭建运行环境。由于目前Windows系统均为64位, 所以我们使用 mingw64.exe
进行依赖安装:
pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
+
安装完成后,我们需要将 mingw64/bin
目录添加进环境变量中便于后续使用。
warning
DexKit 默认将使用 ninja
作为默认构建系统,如果您需要在 mingw 中使用 make
进行构建, 需要执行 pacman -S mingw-w64-x86_64-make
,安装完成后需要将 msys64\mingw64\bin\mingw32-make.exe
重命名为 make.exe
或者添加为快捷方式,否则会由于 gradle-cmake-plugin
找不到 make
命令而构建失败。 同时删除 :dexkit/build.gradle
中的 generator.set(generators.ninja)
,或者修改为 generator.set(generators.unixMakefiles)
对于Linux用户,正常情况只需要安装 ninja
即可。
推荐使用 HomeBrew 进行依赖管理,
brew install cmake ninja
+
git clone https://github.com/LuckyPray/DexKit.git
+
执行子模块 :main
即可进行测试。
gradle :main:run
+
这里介绍了
DexKit
查询的结构组成,匹配器对象可以进行递归嵌套,一切条件都是可以选的。
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchIn | Collection<ClassData> | 在指定的类列表中搜索类 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | ClassMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索字段 |
searchInFields | Collection<FieldData> | 在指定的字段列表中搜索字段 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | FieldMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索方法 |
searchInMethods | Collection<MethodData> | 在指定的方法列表中搜索方法 |
findFirst | Boolean | 查询到第一个结果后立即返回结果,由于多线程执行,不保证结果唯一 |
matcher | MethodMatcher | 匹配条件 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchIn | Collection<ClassData> | 在指定的类列表中搜索类 |
matchers | Collection<StringMatchersGroup> | 查询分组列表 |
字段名 | 类型 | 说明 |
---|---|---|
searchPackages | Collection<String> | 搜索的包名列表 |
excludePackages | Collection<String> | 排除的包名列表 |
ignorePackagesCase | Boolean | 是否忽略包名大小写 |
searchInClasses | Collection<ClassData> | 在指定的类列表中搜索方法 |
searchInMethods | Collection<MethodData> | 在指定的方法列表中搜索方法 |
matchers | Collection<StringMatchersGroup> | 查询分组列表 |
字段名 | 类型 | 说明 |
---|---|---|
modifiers | Int | 匹配的修饰符的 bit masks |
matchType | MatchType | 匹配模式 |
字段名 | 类型 | 说明 |
---|---|---|
byteValue | Byte | 匹配的字节 |
shortValue | Short | 匹配的短整型 |
charValue | Char | 匹配的字符 |
intValue | Int | 匹配的整型 |
longValue | Long | 匹配的长整型 |
floatValue | Float | 匹配的浮点型 |
doubleValue | Double | 匹配的双精度浮点 |
stringValue | StringMatcher | 匹配的字符串 |
methodValue | MethodMatcher | 匹配的方法 |
enumValue | FieldMatcher | 匹配的枚举 |
arrayValue | AnnotationEncodeArrayMatcher | 匹配的数组 |
annotationValue | AnnotationMatcher | 匹配的注解 |
nullValue | 无 | 匹配 null |
boolValue | Boolean | 匹配的布尔值 |
字段名 | 类型 | 说明 |
---|---|---|
min | Int | 最小值,默认为 0 |
max | Int | 最大值,默认为 Int.MAX_VALUE |
字段名 | 类型 | 说明 |
---|---|---|
byteValue | Byte | 匹配的字节 |
shortValue | Short | 匹配的短整型 |
charValue | Char | 匹配的字符 |
intValue | Int | 匹配的整型 |
longValue | Long | 匹配的长整型 |
floatValue | Float | 匹配的浮点型 |
doubleValue | Double | 匹配的双精度浮点 |
字段名 | 类型 | 说明 |
---|---|---|
opCodes | Collection<Int> | 匹配的操作码对应的 Int 值 |
opNames | Collection<String> | 匹配的操作码对应的名称,即 smali 语法中的名称 |
matchType | OpCodeMatchType | 匹配模式 |
size | IntRange | 该操作码总长度范围 |
字段名 | 类型 | 说明 |
---|---|---|
value | String | 匹配的字符串 |
matchType | StringMatchType | 匹配模式 |
ignoreCase | Boolean | 是否忽略大小写 |
字段名 | 类型 | 说明 |
---|---|---|
types | Collection<TargetElementType> | 匹配的注解声明的元素类型列表 |
matchType | MatchType | 匹配模式 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 匹配的名称 |
value | AnnotationEncodeValueMatcher | 匹配的值 |
字段名 | 类型 | 说明 |
---|---|---|
elements | Collection<AnnotationElementMatcher> | 匹配的注解声明的元素列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解声明的元素数量 |
字段名 | 类型 | 说明 |
---|---|---|
values | Collection<AnnotationEncodeValueMatcher> | 匹配的注解声明的元素列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解声明的元素数量 |
字段名 | 类型 | 说明 |
---|---|---|
type | ClassMatcher | 匹配的注解类型 |
targetElementTypes | TargetElementTypesMatcher | 匹配的注解声明的元素类型列表 |
policy | RetentionPolicyType | 匹配的注解声明的保留策略 |
elements | AnnotationElementsMatcher | 匹配的注解声明的元素列表 |
usingStrings | Collection<StringMatcher> | 注解中使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
annotations | Collection<AnnotationMatcher> | 匹配的注解列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的注解数量 |
字段名 | 类型 | 说明 |
---|---|---|
source | StringMatcher | 类的源码文件名,即 smali 中的 .source 字段 |
className | StringMatcher | 类的名称 |
modifiers | AccessFlagsMatcher | 类的修饰符 |
superClass | ClassMatcher | 类的父类 |
interfaces | InterfacesMatcher | 类的接口列表 |
annotations | AnnotationsMatcher | 类的注解列表 |
fields | FieldsMatcher | 类的字段列表 |
methods | MethodsMatcher | 类的方法列表 |
usingStrings | Collection<StringMatcher> | 类中使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
interfaces | Collection<ClassMatcher> | 匹配的接口列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的接口数量 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 字段的名称 |
modifiers | AccessFlagsMatcher | 字段的修饰符 |
declaredClass | ClassMatcher | 字段的声明类 |
type | ClassMatcher | 字段的类型 |
annotations | AnnotationsMatcher | 字段的注解 |
getMethods | MethodsMatcher | 读取该字段的方法列表 |
putMethods | MethodsMatcher | 设置该字段的方法列表 |
字段名 | 类型 | 说明 |
---|---|---|
fields | Collection<FieldMatcher> | 匹配的字段列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的字段数量 |
字段名 | 类型 | 说明 |
---|---|---|
name | StringMatcher | 方法的名称 |
modifiers | AccessFlagsMatcher | 方法的修饰符 |
declaredClass | ClassMatcher | 方法的声明类 |
returnType | ClassMatcher | 方法的返回值类型 |
params | ParametersMatcher | 方法的参数列表 |
annotations | AnnotationsMatcher | 方法的注解 |
opCodes | OpcodesMatcher | 方法的操作码列表 |
usingStrings | Collection<StringMatcher> | 方法中使用的字符串列表 |
usingFields | Collection<UsingFieldMatcher> | 方法中使用的字段列表 |
usingNumbers | Collection<Number> | 方法中使用的数字列表 |
invokeMethods | MethodsMatcher | 方法中调用的方法列表 |
callMethods | MethodsMatcher | 调用了该方法的方法列表 |
字段名 | 类型 | 说明 |
---|---|---|
methods | Collection<MethodMatcher> | 匹配的方法列表 |
matchType | MatchType | 匹配模式 |
count | IntRange | 匹配的方法数量 |
字段名 | 类型 | 说明 |
---|---|---|
type | ClassMatcher | 参数的类型 |
annotations | AnnotationsMatcher | 参数的注解 |
字段名 | 类型 | 说明 |
---|---|---|
params | Collection<ParameterMatcher> | 匹配的参数列表 |
count | IntRange | 匹配的参数数量 |
字段名 | 类型 | 说明 |
---|---|---|
groupName | String | 分组名称 |
matchers | Collection<StringMatcher> | 该查询分组匹配对象所使用的字符串列表 |
字段名 | 类型 | 说明 |
---|---|---|
field | FieldMatcher | 匹配的字段 |
usingType | UsingType | 使用类型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Byte | 字节 |
字段名 | 类型 | 说明 |
---|---|---|
value | Short | 短整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Char | 字符 |
字段名 | 类型 | 说明 |
---|---|---|
value | Int | 整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Long | 长整型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Float | 浮点型 |
字段名 | 类型 | 说明 |
---|---|---|
value | Double | 双精度浮点 |
字段名 | 类型 | 说明 |
---|---|---|
value | String | 字符串 |
该对象无字段。
字段名 | 类型 | 说明 |
---|---|---|
value | Boolean | 布尔值 |
字段名 | 类型 | 说明 |
---|---|---|
ByteValue | Enum | 字节类型 |
ShortValue | Enum | 短整型类型 |
CharValue | Enum | 字符类型 |
IntValue | Enum | 整型类型 |
LongValue | Enum | 长整型类型 |
FloatValue | Enum | 浮点类型 |
DoubleValue | Enum | 双精度浮点类型 |
StringValue | Enum | 字符串类型 |
TypeValue | Enum | 类型类型 |
MethodValue | Enum | 方法类型 |
EnumValue | Enum | 枚举类型 |
ArrayValue | Enum | 数组类型 |
AnnotationValue | Enum | 注解类型 |
NullValue | Enum | 空类型 |
BoolValue | Enum | 布尔类型 |
详情参照 Visibility values
字段名 | 类型 | 说明 |
---|---|---|
Build | Enum | 构建时可见 |
Runtime | Enum | 运行时可见 |
System | Enum | 系统可见 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
Equals | Enum | 等于 |
字段名 | 类型 | 说明 |
---|---|---|
ByteValue | Enum | 字节类型 |
ShortValue | Enum | 短整型类型 |
CharValue | Enum | 字符类型 |
IntValue | Enum | 整型类型 |
LongValue | Enum | 长整型类型 |
FloatValue | Enum | 浮点类型 |
DoubleValue | Enum | 双精度浮点类型 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
StartsWith | Enum | 以...开头 |
EndsWith | Enum | 以...结尾 |
Equals | Enum | 等于 |
该 Enum 与 java.lang.annotation.RetentionPolicy
保持对应。
字段名 | 类型 | 说明 |
---|---|---|
Source | Enum | 源码级别 |
Class | Enum | 类级别 |
Runtime | Enum | 运行时级别 |
字段名 | 类型 | 说明 |
---|---|---|
Contains | Enum | 包含 |
StartsWith | Enum | 以...开头 |
EndsWith | Enum | 以...结尾 |
SimilarRegex | Enum | 类正则匹配,只支持 ^ 与 $ |
Equals | Enum | 等于 |
该 Enum 与 java.lang.annotation.ElementType
保持对应。
字段名 | 类型 | 说明 |
---|---|---|
Type | Enum | 类型 |
Field | Enum | 字段 |
Method | Enum | 方法 |
Parameter | Enum | 参数 |
Constructor | Enum | 构造方法 |
LocalVariable | Enum | 局部变量 |
AnnotationType | Enum | 注解类型 |
Package | Enum | 包 |
TypeParameter | Enum | 类型参数 |
TypeUse | Enum | 类型使用 |
字段名 | 类型 | 说明 |
---|---|---|
Any | Enum | 任意 |
Get | Enum | 获取 |
Set | Enum | 设置 |