DexKit Documentation

repository·master·Indexed 21 days ago

https://github.com/luckypray/dexkit

A high-performance C++ based runtime library for parsing DEX files, optimized for locating obfuscated classes, methods, and fields. DexKit 2.0 supports multi-condition searches, batch string searches, and advanced metadata APIs. It provides detailed matching capabilities via ClassMatcher, including package scope, field modifiers, method return types, constant strings, and annotations. The library includes comprehensive build instructions for Linux binaries targeting Ubuntu 16.04, 18.04, and 20.04.

Tokens
30.4K
Snippets
82
Records
108
Agent score
76%

What's inside DexKit

  1. Overview of DexKit

    master

    DexKit is a high-performance DEX runtime parsing library implemented in C++. It is designed to locate obfuscated classes, methods, or properties within DEX files.

    Key features include:

    • Multi-condition searching for classes, methods, and properties.
    • Metadata APIs for retrieving field, method, and class data.
    • Optimized batch searching for classes and methods using strings, which significantly improves search speed and prevents linear time increases when adding query groups.
  2. What is DexKit?

    master
    DexKit is a high-performance runtime parsing library for DEX files, implemented in C++. It is designed to search for obfuscated classes, methods, or properties within an application. Unlike Java-based solutions like dexlib2 or standard Java reflection, DexKit uses C++ with multi-threading and optimized algorithms to provide significantly faster search speeds, making it suitable for complex searches that would otherwise be too slow or resource-intensive for runtime use.
  3. Language requirements for DexKit

    master

    DexKit supports both Kotlin and Java:

    • Kotlin (Recommended): Provides a Domain Specific Language (DSL) for a more expressive and streamlined development experience.
    • Java: Supports chain-call patterns to ensure a smooth experience for Java developers.

    Note: All official documentation examples are written in Kotlin.

  4. Manage DexKitBridge lifecycle

    master

    Creating a DexKitBridge is a time-consuming operation. To ensure optimal performance and prevent memory leaks, follow these rules:

    1. Do not create the object repeatedly: Reuse the bridge instance for multiple operations if possible.
    2. Manage the lifecycle: If using the bridge globally, you must manually call the .close() method when it is no longer needed.
    3. Use try-with-resources or .use(): In Java, use try-with-resources. In Kotlin, use the .use { ... } extension function to ensure the bridge is automatically closed after the block executes.
    // Java: Use try-with-resources
    try (DexKitBridge bridge = DexKitBridge.create(apkPath)) {
        findPlayActivity(bridge);
    }
    // Kotlin: Use .use {}
    DexKitBridge.create(apkPath).use { bridge ->
        findPlayActivity(bridge)
    }
  5. Use Method Prototype Shorthand (ProtoShorty)

    master

    ProtoShorty is a compact string representation of a method's return and parameter types. The first character represents the return type, and the subsequent characters represent the parameter types.

    Note: In shorthand, all reference types (classes, interfaces, arrays, etc.) are represented by the single character L to maintain compactness.

    Type Character Mapping

    CharacterTypeDescription
    Vvoidno value
    ZbooleanBoolean
    BbyteByte
    SshortShort
    CcharCharacter
    IintInteger
    JlongLong
    FfloatSingle‐precision floating point
    DdoubleDouble‐precision floating point
    LObjectReference type (including object/primitive arrays)

    Usage Examples

    ShorthandCorresponding Method Signature
    VLvoid method(Object)
    ZLLboolean method(Object, Object)
    VILFDvoid method(int, Object, long, float, double)
    LLObject method(Object)
    ILIint method(Object, int)
    | V         | void | no value |
    | Z         | boolean | Boolean |
    | B         | byte | Byte |
    | S         | short | Short |
    | C         | char | Character |
    | I         | int | Integer |
    | J         | long | Long |
    | F         | float | Single‐precision floating point |
    | D         | double | Double‐precision floating point |
    | L         | Object | Reference type (including object/primitive arrays) |
  6. Understand Reference Type Signatures (Class and Array)

    master

    Reference types in DexKit are categorized into Classes and Arrays.

    Class (ClassType)

    Class signatures start with L, followed by the fully qualified name, and end with ;. Example: Ljava/lang/String; represents java.lang.String.

    Array (ArrayType)

    Array signatures start with [, followed by the type signature of the elements. Examples:

    • [I is int[]
    • [[C is char[][]
    • [Ljava/lang/String; is java.lang.String[]
  7. Understand the appTag concept

    master

    The appTag is the core identity for DexKitCacheBridge. It controls three main scopes:

    1. Bridge Reuse: DexKitCacheBridge.create(appTag, ...) reuses the current pooled wrapper if it is reachable and matches the appTag.
    2. Cache Namespace: All query cache entries are stored under a namespace derived from the appTag.
    3. Cleanup Scope: clearCache(appTag) only removes entries belonging to that specific tag.

    Best Practices:

    • Use stable identifiers like the host package name or version (e.g., "com.example:1.0.0").
    • If using version numbers in the tag, host upgrades/downgrades will automatically switch cache namespaces, preventing stale cache usage.
    • Multi-process Note: Bridge reuse is process-local. To share cache data across processes, your Cache implementation must support cross-process visibility (e.g., MMKV in multi-process mode or a ContentProvider).
  8. Manage RecyclableBridge lifecycle: close() vs destroy()

    master

    A RecyclableBridge (returned by DexKitCacheBridge.create(...)) can be ended in two ways:

    close()

    • Effect: Releases the current underlying DexKitBridge but keeps the wrapper object usable.
    • Reuse: The next access to the wrapper will recreate the underlying bridge on demand. Subsequent create(...) calls with the same appTag will likely return this same wrapper object.
    • Usage: This is the default behavior when using Kotlin's .use { ... } block.

    destroy()

    • Effect: Permanently retires the wrapper. Any further access to the object will throw an exception.
    • Reuse: Subsequent create(...) calls with the same appTag will return a brand new wrapper object.
    • Note: destroy() only affects the bridge lifecycle; it does not clear the persistent Cache. To clear data, use clearCache(appTag) or clearAllCache().
  9. Use composite matchers for complex logic

    master

    ClassMatcher, FieldMatcher, and MethodMatcher support composite conditions to create complex logical queries. Inside a matcher {} block, regular conditions are implicitly joined with AND.

    Supported composite operators:

    • allOf { ... }: All child conditions must match.
    • anyOf { ... }: At least one child condition must match.
    • noneOf { ... }: All child conditions must not match.
    • not { ... }: Negates a single condition.

    Kotlin DSL Example:

    val method = bridge.findMethod {
        matcher {
            declaredClass("org.example.PlayActivity")
            anyOf {
                match { name = "onCreate" }
                match { usingStrings("onClick") }
            }
            not { usingStrings("rollDice: ") }
        }
    }

    Java Chaining API Example: If you cannot use Kotlin DSL closures, use the chaining API:

    MethodMatcher matcher = MethodMatcher.create()
            .declaredClass("org.example.PlayActivity")
            .anyOf(
                    MethodMatcher.create().name("onCreate"),
                    MethodMatcher.create().usingStrings(List.of("onClick"), StringMatchType.Contains, false)
            )
            .not(MethodMatcher.create().usingStrings(List.of("rollDice: "), StringMatchType.Contains, false));
  10. Use DexAccessFlags for advanced DEX modifier matching

    master

    DexKit matchers and result objects use raw DEX access_flags in their modifiers fields. While most users can use standard java.lang.reflect.Modifier constants, you should use DexAccessFlags when you need to match compiler-generated flags or DEX-specific semantics that are not covered by the standard Java Modifier subset.

    Key distinctions:

    • Modifier constants are a numeric subset of DEX access flags.
    • DexAccessFlags is required for flags like BRIDGE or SYNTHETIC.
    • Synchronization matching: To match an ordinary source-level synchronized method, use DexAccessFlags.DECLARED_SYNCHRONIZED (0x20000). Note that DexAccessFlags.SYNCHRONIZED (0x20) is only valid for native methods in DEX.
    • The two models are not equivalent; DexAccessFlags provides access to the full DEX specification.
    // Kotlin usage
    modifiers = Modifier.PUBLIC or DexAccessFlags.BRIDGE or DexAccessFlags.SYNTHETIC
    // Java usage
    .modifiers(Modifier.PUBLIC | DexAccessFlags.BRIDGE | DexAccessFlags.SYNTHETIC)