安卓代码加固
安卓代码加固(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 / 11 | false |
| 4 | 不排队 |
| 5 | 立刻断开的空 Connection |
| 12 / 15 | "SDK 未初始化" |
| 13 | 仍给当前 Activity 加 FLAG_SECURE |
| 16 | 常量 12 |
| 其余 | null |
对照指令表
实现类方法名(XOR 解开后)和 op 的对应关系:
| op | 实现方法 | 参数 | 作用 |
|---|---|---|---|
| 1 | configure | String? | 指定接听后落地页 |
| 2 | registerPhoneAccount | Context | 登记自管理账号 |
| 3 | startIncomingCall | Context | 立刻发起 |
| 4 | startIncomingCallLater | Context, Long | 延迟发起 |
| 5 | createIncomingConnection | Context | ConnectionService 占位 |
| 6 | hangUp | — | 结束会话 |
| 7 | performAnswer | — | 接听 |
| 8 | performReject | — | 拒绝 |
| 9 | onLandingOpened | — | 落地页已打开 |
| 10 | onIncomingUiShown | — | 全屏路由页已展示 |
| 11 | isIncomingRinging | — | 是否仍在振铃 |
| 12 | statusText | Context | 调试状态 |
| 13 | applyScreenProtection | Activity, Boolean | 防录屏 |
| 14 | getLastError | — | 最近错误 |
| 15 | getLastStatus | — | 最近状态 |
| 16 | getAbiVersion | — | ABI |
Kotlin 属性 abiVersion、lastError、lastStatus 在 JNI 里对应 getter。改属性名等于改 so 里的 XOR 字节。
旧方案先在 Java 里
loadClass,再 as HiddenApi。接口方法名会出现在主 DEX。现在由 so 直接构造 HiddenLandingPage,主 DEX 不再包含 HiddenApi。APK 自检还会确认 Lcom/example/minidex/sdk/HiddenApi; 不存在。把可读名留在宿主
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 不会一直挂起一通无效来电。