ArturoYi
安卓代码加固
解密后的 dex 只存在于 ByteBuffer 中,用 InMemoryDexClassLoader 加载,不写入 filesDir。

常见动态加载会把 dex 写到 getDir("dex", …),再用 DexClassLoader 加载。这份工程不这样做。明文只存在 native 堆和一块 Java byte[] 里,随即 ByteBuffer.wrap,交给 dalvik.system.InMemoryDexClassLoader。

InMemoryDexClassLoader 需要 API 26。这也是 minSdk 26 的原因之一。

把明文交给 ClassLoader

nativeInit 确认文件头是 dex 后,把 C 缓冲拷进 jbyteArray,并立即清除 C 侧明文:

hidden-sdk/src/main/cpp/hidden_bridge.cpp
jbyteArray arr = env->NewByteArray(static_cast<jsize>(out_len));
env->SetByteArrayRegion(arr, 0, static_cast<jsize>(out_len),
                        reinterpret_cast<jbyte *>(plain));
hidden_secure_zero(plain, pt_cap);
free(plain);

jclass buf_cls = env->FindClass("java/nio/ByteBuffer");
jmethodID wrap = env->GetStaticMethodID(buf_cls, "wrap", "([B)Ljava/nio/ByteBuffer;");
jobject buffer = env->CallStaticObjectMethod(buf_cls, wrap, arr);

父 ClassLoader 来自 context.getClassLoader()。实现类以 compileOnly 依赖的 HiddenIncomingKeepAlive 位于主 DEX,必须能从父 loader 解析。

hidden-sdk/src/main/cpp/hidden_bridge.cpp
jclass loader_cls = env->FindClass("dalvik/system/InMemoryDexClassLoader");
jmethodID loader_ctor = env->GetMethodID(
        loader_cls, "<init>",
        "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V");
jobject loader = env->NewObject(loader_cls, loader_ctor, buffer, parent);
Java 侧注释仍写「把结果交给 InMemoryDexClassLoader」,但 HiddenSdk 已经不再调用 HiddenDecrypt,也不再自行 loadClass。加载、查找类、构造实例都在 so 里完成。反编译主 DEX 看不到实现类名。

构造实现对象

类名同样以 XOR 存放。解码后调用 ClassLoader.loadClass,再做无参构造,并保留三个 GlobalRef:

hidden-sdk/src/main/cpp/hidden_bridge.cpp
jobject impl_obj = env->CallObjectMethod(loader, load_class, jname);
jmethodID ctor = env->GetMethodID(impl, "<init>", "()V");
jobject instance = env->NewObject(impl, ctor);

g_loader = env->NewGlobalRef(loader);
g_impl_class = static_cast<jclass>(env->NewGlobalRef(impl));
g_instance = env->NewGlobalRef(instance);
全局引用作用
g_loader保住 ClassLoader,避免实现类被回收
g_impl_classnativeDispatch 用来 GetMethodID
g_instance单例实现对象

nativeReady 只看 g_instance != nullptr。Java 的 ready() / isInitialized() 都走这条 JNI,不在 Java 堆上再存一份「已加载」标志。

对照实现类约束

加密 dex 里的入口类必须使用无参构造,方法名也必须保持稳定,因为 so 用 GetMethodID 按名称查找:

hidden-landing-page/src/main/java/com/example/minidex/hidden/HiddenLandingPage.kt
class HiddenLandingPage {
    val abiVersion: Int = 12

    fun configure(landingActivityClassName: String?) { ... }
    fun registerPhoneAccount(context: Context): Boolean = ...
    fun startIncomingCall(context: Context): Boolean = ...
}

KDoc 写明了约束:不要加构造参数,不要改方法名,不要对 :hidden-sdk 使用 implementation。

系统能按清单绑定的组件(ConnectionService、来电路由页)必须留在主 DEX。InMemoryDexClassLoader 加载的类不能写进 Manifest 当导出组件。

处理失败回滚

加载过程中任一步失败,release_locked 会 DeleteGlobalRef 这三个引用。进程内可以再次 init。一旦成功,再次 init 会直接返回 ALREADY_INITIALIZED,不会用新密钥重新解密。

hidden-sdk/src/main/java/com/example/minidex/sdk/HiddenSdk.kt
if (nativeReady()) {
    val already = HiddenInitResult.alreadyInitialized()
    initResult = already
    return already
}

要更换密钥,只能结束进程。这样可以避免运行中途用错误密钥替换已加载的实现。

下一步

Copyright © 2026