安卓代码加固(3):内存加载
常见动态加载会把 dex 写到 getDir("dex", …),再用 DexClassLoader 加载。这份工程不这样做。明文只存在 native 堆和一块 Java byte[] 里,随即 ByteBuffer.wrap,交给 dalvik.system.InMemoryDexClassLoader。
InMemoryDexClassLoader 需要 API 26。这也是 minSdk 26 的原因之一。把明文交给 ClassLoader
nativeInit 确认文件头是 dex 后,把 C 缓冲拷进 jbyteArray,并立即清除 C 侧明文:
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 解析。
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);
HiddenSdk 已经不再调用 HiddenDecrypt,也不再自行 loadClass。加载、查找类、构造实例都在 so 里完成。反编译主 DEX 看不到实现类名。构造实现对象
类名同样以 XOR 存放。解码后调用 ClassLoader.loadClass,再做无参构造,并保留三个 GlobalRef:
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_class | nativeDispatch 用来 GetMethodID |
g_instance | 单例实现对象 |
nativeReady 只看 g_instance != nullptr。Java 的 ready() / isInitialized() 都走这条 JNI,不在 Java 堆上再存一份「已加载」标志。
对照实现类约束
加密 dex 里的入口类必须使用无参构造,方法名也必须保持稳定,因为 so 用 GetMethodID 按名称查找:
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,不会用新密钥重新解密。
if (nativeReady()) {
val already = HiddenInitResult.alreadyInitialized()
initResult = already
return already
}
要更换密钥,只能结束进程。这样可以避免运行中途用错误密钥替换已加载的实现。