【日常随笔】Android 跳转行为“管控” -- AMS HOOK

46 阅读4分钟

HOOK AMS 技术白皮书

Android AMS Hook 跳转治理能力说明

版本:V1.0
适用平台:Android 8.0 及以上
适用对象:SDK 接入方 / 技术评审 / 安全合规 / 架构评估
源码:见底部


一、摘要(Executive Summary)

随着 Android 生态中广告 SDK 与第三方 SDK 的复杂化,应用在跳转治理、行为可解释性与合规控制方面面临显著挑战。 提供一套基于 Android AMS(Activity Manager Service)Hook 的跳转治理能力,在不修改系统、不依赖系统权限的前提下,实现对应用内跳转行为的统一监听、分析与控制。


二、背景与问题定义

2.1 行业痛点

  • 跳转行为由多方 SDK 触发,来源不可追溯
  • 广告点击是否引发跳离难以精确判断
  • 隐式 Intent / PendingIntent 绕过业务层监控
  • 合规审查缺乏技术证据链

2.2 Android 系统特性

在 Android 系统中,所有跨组件、跨进程启动行为最终均需通过 AMS 完成调度。

包括:

  • startActivity
  • startService
  • sendBroadcast
  • startIntentSender

三、技术方案概述

3.1 技术定位

App 在 应用进程内 对 AMS Binder 进行代理,实现对跳转行为的统一治理。

特性包括:

  • SDK 级能力(无需系统权限)
  • 不修改 system_server
  • 覆盖所有跳转来源
  • 可审计、可降级、可关闭

3.2 架构总览

业务代码 / 第三方 SDK

AMS Hook SDK(代理层)

原始 AMS Binder

system_server


四、核心技术原理

4.1 AMS 在应用进程中的形态

Android 在应用进程中通过 Singleton 缓存 AMS Binder 实例:

Android 版本接口
8–10IActivityManager
10+IActivityTaskManager

4.2 Hook 实现机制

SDK 使用以下 Java 标准能力:

  • Reflection(反射)
  • Dynamic Proxy(动态代理)

实现方式:

  1. 获取 AMS Singleton
  2. 取得原始 Binder 实例
  3. 创建代理对象
  4. 替换 Singleton.mInstance

SDK 在代理中完成监控与策略判断后,原样调用系统能力


五、功能能力说明

5.1 覆盖的系统调用

类型支持
Activity 启动
Service 启动
Broadcast 发送
PendingIntent

5.2 跳转分析能力

SDK 可解析以下信息:

  • 目标包名 / 组件名
  • 显式 / 隐式 Intent
  • 调用线程
  • 调用堆栈

并区分:

  • 本应用内部跳转
  • 第三方应用跳转
  • 高风险跳转行为

六、典型应用场景

6.1 广告与商业化

  • 判断广告点击是否导致 App 跳离
  • 构建点击 → 跳转证据链

6.2 SDK 行为治理

  • 审计第三方 SDK 跳转行为
  • 防止私自拉起第三方应用

6.3 合规与安全

  • 提供可验证的技术说明
  • 跳转行为可回溯、可解释

七、合规与安全声明

7.1 SDK 不涉及的能力

  • 不采集用户隐私数据
  • 不绕过系统权限模型
  • 不修改系统服务

7.2 合规原则

  • 仅作用于当前应用进程
  • 所有系统调用原样转发
  • 支持随时关闭与降级

八、版本适配与稳定性

Android 版本支持情况
8–11稳定支持
12自动降级
13+建议与 Instrumentation 联合

九、接入建议

  • 在 Application.attachBaseContext 初始化
  • 支持灰度启用
  • 提供统一开关配置

十、结语

AMS Hook 跳转治理能力是 Android SDK 体系中,实现跳转可控、行为可解释与责任可追溯的重要技术基础。

源码

@SuppressLint({"PrivateApi", "BlockedPrivateApi"})
public final class AMSHookHelper {

    private static boolean sHooked = false;

    public static void hook() {
        if (sHooked) return;

        try {
            int sdk = Build.VERSION.SDK_INT;

            Object singleton;
            if (sdk >= 29) {
                singleton = getStaticField(
                        "android.app.ActivityTaskManager",
                        "IActivityTaskManagerSingleton"
                );
            } else if (sdk >= 26) {
                singleton = getStaticField(
                        "android.app.ActivityManager",
                        "IActivityManagerSingleton"
                );
            } else {
                singleton = getStaticField(
                        "android.app.ActivityManagerNative",
                        "gDefault"
                );
            }

            Object raw = getField(
                    "android.util.Singleton",
                    singleton,
                    "mInstance"
            );

            if (raw == null) {
                if (sdk >= 29) {
                    callStatic("android.app.ActivityTaskManager", "getService");
                } else if (sdk >= 26) {
                    callStatic("android.app.ActivityManager", "getService");
                } else {
                    callStatic("android.app.ActivityManagerNative", "getDefault");
                }

                raw = getField(
                        "android.util.Singleton",
                        singleton,
                        "mInstance"
                );
            }

            if (raw == null) return;

            Class<?> iFace = sdk >= 29
                    ? Class.forName("android.app.IActivityTaskManager")
                    : Class.forName("android.app.IActivityManager");

            Object proxy = Proxy.newProxyInstance(
                    iFace.getClassLoader(),
                    new Class[]{iFace},
                    new AMSInvocationHandler(raw)
            );

            setField(
                    "android.util.Singleton",
                    singleton,
                    "mInstance",
                    proxy
            );

            sHooked = true;

        } catch (Throwable ignored) {
        }
    }

    // ====== 反射工具 ======

    private static Object getStaticField(String cls, String field)
            throws Exception {
        Class<?> c = Class.forName(cls);
        Field f = c.getDeclaredField(field);
        f.setAccessible(true);
        return f.get(null);
    }

    private static Object getField(String cls, Object obj, String field)
            throws Exception {
        Field f = Class.forName(cls).getDeclaredField(field);
        f.setAccessible(true);
        return f.get(obj);
    }

    private static void setField(String cls, Object obj, String field, Object val)
            throws Exception {
        Field f = Class.forName(cls).getDeclaredField(field);
        f.setAccessible(true);
        f.set(obj, val);
    }

    private static void callStatic(String cls, String method)
            throws Exception {
        Method m = Class.forName(cls).getDeclaredMethod(method);
        m.setAccessible(true);
        m.invoke(null);
    }
}

//拦截点
public class AMSInvocationHandler implements InvocationHandler {

    private final Object mBase;

    public AMSInvocationHandler(Object base) {
        this.mBase = base;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {

        String name = method.getName();

        if ("startActivity".equals(name)
                || "startActivityAsUser".equals(name)
                || "startActivityIntentSender".equals(name)) {

            Intent intent = findIntent(args);
            if (intent != null) {
                // === 这里放你原来 Jump / Block / Record 逻辑 ===
            }
        }

        return method.invoke(mBase, args);
    }

    private Intent findIntent(Object[] args) {
        if (args == null) return null;
        for (Object arg : args) {
            if (arg instanceof Intent) {
                return (Intent) arg;
            }
        }
        return null;
    }
}