ArturoYi
安卓代码加固

安卓代码加固(4):指令分发

公开入口只有 invoke(Int, Any?, Any?)。指令含义不进入 AAR,native 按整数查找方法。

如果主 DEX 出现 startIncomingCall、performAnswer 这类名字,静态分析就能还原调用图。当前公开类型只有 HiddenSdk 和 HiddenInitResult。业务全部通过 invoke 调用。

hidden-sdk/src/main/java/com/example/minidex/sdk/HiddenSdk.kt
/**
 * 唯一公开入口:校验密钥、加载 payload、按整数指令分发。
 * 主 DEX 不暴露业务方法名。指令含义见仓库 `接入与使用.md`。
 */
@JvmStatic
@JvmOverloads
fun invoke(op: Int, a: Any? = null, b: Any? = null): Any? {
    if (!nativeReady()) {
        return fallback(op, a, b)
    }
    return nativeDispatch(op, a, b)
}
对照表只写在仓库文档和宿主本地包装里,不要写进 SDK 源码或 AAR。JADX 只能看到 invoke(Int, Any?, Any?)。

native 如何分发

nativeDispatch 是一个 switch (cmd)。每个 case 用 XOR 过的方法名做 GetMethodID,再按签名拆箱参数。

hidden-sdk/src/main/cpp/hidden_bridge.cpp
case 3: {
    static const uint8_t n[] = {
            0x29, 0x2e, 0x3b, 0x28, 0x2e, 0x13, 0x34, 0x39, 0x35, 0x37,
            0x33, 0x34, 0x3d, 0x19, 0x3b, 0x36, 0x36
    };
    jmethodID mid = mid_by_xor(
            env, impl, n, sizeof(n),
            "(Landroid/content/Context;)Z");
    if (mid != nullptr) {
        result = box_bool(env, env->CallBooleanMethod(instance, mid, arg1));
    }
    break;
}

用 0x5A 解开后,上面这段是 startIncomingCall(Context): Boolean。Java 侧看不到这个名字。

未初始化时 不会 进入 native。fallback 按指令返回空结果,不抛异常:

op未初始化
2 / 3 / 11false
4不排队
5立刻断开的空 Connection
12 / 15"SDK 未初始化"
13仍给当前 Activity 加 FLAG_SECURE
16常量 12
其余null

对照指令表

实现类方法名(XOR 解开后)和 op 的对应关系:

op实现方法参数作用
1configureString?指定接听后落地页
2registerPhoneAccountContext登记自管理账号
3startIncomingCallContext立刻发起
4startIncomingCallLaterContext, Long延迟发起
5createIncomingConnectionContextConnectionService 占位
6hangUp—结束会话
7performAnswer—接听
8performReject—拒绝
9onLandingOpened—落地页已打开
10onIncomingUiShown—全屏路由页已展示
11isIncomingRinging—是否仍在振铃
12statusTextContext调试状态
13applyScreenProtectionActivity, Boolean防录屏
14getLastError—最近错误
15getLastStatus—最近状态
16getAbiVersion—ABI

Kotlin 属性 abiVersion、lastError、lastStatus 在 JNI 里对应 getter。改属性名等于改 so 里的 XOR 字节。

把可读名留在宿主

Demo 用 HostBridge 把整数封装成方法。这个对象是 internal,只存在于 :app,不进入 SDK AAR。

app/src/main/java/com/example/minidex/HostBridge.kt
internal object HostBridge {
    fun start(context: Context): Boolean =
        HiddenSdk.invoke(3, context) as? Boolean ?: false

    fun answer() {
        HiddenSdk.invoke(7)
    }

    fun protect(activity: Activity, enable: Boolean = true) {
        HiddenSdk.invoke(13, activity, enable)
    }
}

工具包对外再包一层 YourLandingPageKit,也是同一做法:最终 App 调用接入方自己的方法名。分析 SDK AAR 时,仍然只能看到 invoke。

处理占位组件

ConnectionService 必须留在主 DEX,系统按清单类名绑定。Demo 占位组件走本地 HostBridge,内部仍是 invoke(5):

app/src/main/java/com/example/minidex/DemoConnectionService.kt
override fun onCreateIncomingConnection(...): Connection {
    return runCatching {
        HostBridge.createConnection(this)
    }.getOrElse { error ->
        object : Connection() {
            init {
                setDisconnected(DisconnectCause(DisconnectCause.ERROR, error.message))
                destroy()
            }
        }
    }
}

未初始化时,fallback 返回已断开的空 Connection,reason 为「SDK 未初始化」。Telecom 不会一直挂起一通无效来电。

下一步

Copyright © 2026