本文是 AndProxy——Syscall 及 Binder 运行时代理库 的升级版方案介绍。
原方案通过 GotHook 拦截 Binder ioctl 实现服务代理;升级版完全抛弃 Hook,仅使用 seccomp-notify 机制,直接修改 Binder 事务中的目标 handle,利用 Binder 驱动自身的路由机制实现透明转发。
关于 Binder 通信的基础概念,例如:
可参考前置文章。
在 Android 系统上,对系统服务调用进行拦截和代理,是虚拟机、应用分身、隐私保护等场景中的基础能力。
传统代理方案大致分为两类。
通过反射替换 ServiceManager 中的 IBinder 缓存。
这种方式的问题是:
通过 Hook libbinder.so 中的 ioctl 函数实现 Binder 调用拦截。
原版 AndProxy 采用的就是这种方案,但它存在几个固有问题:
既然所有 Binder 调用最终都会经过:
那么问题就变成:
能不能直接在系统调用层面拦截并修改 Binder 数据,而不修改任何代码?
答案是:
升级版 AndProxy 的核心思路是:
整个过程不修改:
只修改 Binder 事务中的一个字段:
在展开方案之前,需要先理清几个关键概念。
这是最常见的误区,也是方案设计的关键前提。
Android 系统服务的注册名和类名并不相同:
客户端通常通过服务名调用:
获取对应的 Binder handle。
只知道服务类名,无法直接推导出服务名。
因此,在代理系统中必须同时区分:
在 Binder 通信中,handle 是 Binder 驱动用来路由 IPC 数据的整数标识符。
客户端发起 BC_TRANSACTION 时,会在:
中指定目标服务的 handle。
Binder 驱动根据这个值,将数据投递到对应的服务端进程。
因此,只要能修改这个字段,就可以改变 Binder 事务的路由目标。
也就是说:
修改 target.handle,就等价于修改 Binder 请求的目的地。
一次完整的 Binder 调用会被封装在:
中。
其中两个字段是本方案最关注的核心:
方案的核心操作就是:
在 BC_TRANSACTION 被提交到内核之前,将 target.handle 替换为代理服务的 handle。
升级版 AndProxy 由三个子系统构成,均在用户态运行。
整个方案通过注入 APK 的方式进入目标进程,在目标进程的用户态空间中运行。
核心机制包括:
整个过程不需要:
升级版方案使用单个 @ProxyMethod 注解,直接声明代理一个目标方法所需的全部信息。
示例:
三个参数分别承担不同功能,缺一不可。
Android 系统服务数量很多,而且服务类名与注册名之间没有统一规律。
例如:
手动维护一套完整映射表的成本很高,并且容易出错。
更重要的是:
serviceName 本身就是运行时获取 handle 的必要信息,并不是冗余字段。
因此,让开发者在注解中同时声明 serviceClass 和 serviceName,是更务实的工程选择。
这样既避免维护大型映射表,也让代理关系更加显式。
KSP 处理器在编译期完成以下工作。
遍历所有被 @ProxyMethod 标记的函数,提取三元组:
例如:
同一个 serviceName 下的代理方法会被归入同一个代理服务类。
例如:
每个代理服务对应一个 Binder 服务实例,运行时只注册一次。
为每个被代理的方法生成一条路由记录。
每条记录包含:
最终生成的结构可以理解为:
例如:
这是整个方案的核心。
它利用 Linux seccomp-notify 机制,在系统调用层面截获 Binder ioctl,并修改事务数据。
seccomp-notify 对应的返回值是:
它允许用户态 supervisor 接管被过滤器命中的系统调用。
大致流程是:
相比 Hook 方案,seccomp-notify 的优势是:
SECCOMP_RET_USER_NOTIF 需要内核支持,典型环境要求:
Seccomp-BPF 过滤器在内核态做快速判断。
过滤逻辑如下:
BPF 层只负责粗过滤。
更精确的匹配逻辑,例如:
都交给用户态 supervisor 处理。
当 BPF 过滤器返回 SECCOMP_RET_USER_NOTIF 后,supervisor 接管处理。
完整流程如下:
Binder 事务的数据缓冲区中通常包含接口描述符,例如:
这个字符串正好对应注解中的:
由于注解中同时声明了:
KSP 可以在编译期生成查找链:
Supervisor 解析出 serviceClass 后,就可以直接定位到对应路由表。
然后根据 binder_transaction_data.code 判断是否命中某个代理方法。
整个替换过程完全发生在用户态内存中。
ioctl 的第三个参数是一个指向用户空间结构体的指针:
而 binder_transaction_data 又嵌套在 BWR 的写缓冲区中。
当 seccomp-notify 暂停系统调用后:
此时 Binder 驱动继续处理 ioctl 时,从用户态缓冲区读到的已经是修改后的数据:
因此驱动会自然地将事务路由到代理服务。
这个过程只修改一个字段:
却把 Binder 路由交还给了 Binder 驱动本身完成。
这也是该方案最简洁的地方:
不重新实现 Binder 协议栈,只改变 Binder 驱动的路由目标。
辅助系统维护各类映射关系,并为 supervisor 提供基础能力。
辅助系统通过注解中声明的 serviceClass,在运行时反射对应 AIDL Stub 类中的 TRANSACTION_* 常量。
例如:
注解中的:
会与:
建立对应关系。
最终生成:
例如:
辅助系统在代理服务启动时完成以下工作:
整体映射可以理解为:
例如:
辅助系统提供 Binder 协议解析函数,用于 supervisor 中精确解析被拦截的 ioctl 数据。
主要包括:
这些函数是 supervisor 判断是否需要代理的基础。
辅助系统还负责代理服务的启动和生命周期管理。
具体包括:
代理服务本身不需要知道请求是如何被路由过来的。
从它的视角看,它只是一个普通 Binder 服务。
下面以目标应用调用:
为例,展示一次完整代理过程。
开发者编写代理方法:
KSP 处理器生成代理路由表:
并生成对应代理服务类。
初始化阶段完成:
通过反射 IPackageManager.Stub 获取:
将 code = 3 填入路由表。
通过:
获取原始服务 handle。
创建代理服务实例。
注册代理服务并获取代理服务 handle。
安装 seccomp 过滤器。
启动 supervisor。
目标应用调用:
此时流程如下:
该方案不修改:
也不需要:
所有操作都发生在系统调用通知和用户态内存数据修改层面。
方案只修改:
之后的路由过程完全交给 Binder 驱动完成。
因此不需要重新实现:
本质上,这是在利用 Binder 已有能力做转发。
通过 KSP 注解处理器,代理声明在编译期被收集和整理。
运行时只需要填充:
代理路由结构在编译期就已经确定,运行时逻辑更简单。
Seccomp-BPF 过滤器在内核态执行粗过滤。
只有命中的:
才会进入 supervisor。
Supervisor 只需要做:
整体路径清晰,额外逻辑集中在 Binder 事务发起前。
相比传统 Hook 方案,该方案不会产生典型的 Hook 修改痕迹,例如:
它更接近一种系统调用层面的代理调度机制。
升级版 AndProxy 最大的扩展价值,不只是“无 Hook”,而是它天然站在 系统调用边界 上。
传统 Binder 代理方案通常深植于:
这些方案很难和内核侧能力形成稳定协同。
而基于 seccomp-notify 的方案不同。
它的拦截点位于:
这个位置天然适合与内核技术组合。
可以在内核侧通过 eBPF 观察或统计 Binder 相关行为,例如:
用户态 AndProxy 负责:
内核侧 eBPF 负责:
两者可以形成清晰分工。
如果系统环境允许,还可以在内核侧通过 tracepoint、kprobe 或受控 inline patch 观察 Binder 驱动路径。
例如关注:
这样可以构建一套从用户态到内核态的完整 Binder 代理观测链路:
由于方案的核心动作发生在系统调用边界,因此它更容易扩展成系统级透明代理能力。
典型组合方式是:
这样,代理系统不再局限于某一个 Java API 或某一个 Hook 点,而是可以围绕 Binder 调用链构建更完整的策略层。
这种架构的最大好处是分层明确:
因此,它不只是一个 Binder 代理技巧,而是一个可以和内核观测、内核策略、系统级监控结合的代理框架基础。
seccomp-notify 需要较新的内核支持。
典型目标环境为:
在更老的 Android 版本上,内核可能不支持 SECCOMP_RET_USER_NOTIF。
Android 存在多个 Binder 域:
不同 Binder 域服务不同的系统组件。
Supervisor 需要区分不同 Binder fd,避免错误代理。
Binder 调用高度并发。
目标进程可能有多个线程同时发起:
因此 supervisor 需要处理:
代理调用通常会变成:
相比直接访问系统服务,多了一层 Binder 转发。
因此需要关注:
Supervisor 需要从 Binder transaction 的数据缓冲区中解析接口描述符。
不同 Android 版本、不同 AIDL 生成模式、不同调用路径下,Parcel 数据布局可能存在差异。
因此解析逻辑需要做充分兼容。
AndProxy 升级版的核心思想可以概括为一句话:
不 Hook ioctl,而是在 ioctl 真正进入 Binder 驱动前,修改它即将提交给驱动的 Binder transaction 数据。
它真正修改的只有一个字段:
但这个字段正是 Binder 驱动进行路由的关键。
因此,该方案不需要重新实现 Binder 协议,也不需要侵入 libbinder.so,而是把 Binder 原生路由机制变成了代理能力的一部分。
从工程架构上看,它由三部分组成:
这种设计的价值不只是减少 Hook 痕迹,更重要的是把 Binder 代理能力放到了系统调用边界上。
这使它天然可以与 eBPF、内核 trace、内核策略模块等技术组合,形成从用户态代理到内核态观测的完整系统。
对于虚拟化、应用分身、隐私保护、行为审计等场景,这是一种比传统 Java 代理或 Native Hook 更干净、更分层、更容易扩展的 Binder 代理架构。
| 服务类名 |
ServiceManager 注册名 |
com.android.server.am.ActivityManagerService |
"activity" |
com.android.server.pm.PackageManagerService |
"package" |
com.android.server.wm.WindowManagerService |
"window" |
| 字段 |
作用 |
target.handle |
目标服务 handle,决定 Binder 请求被路由到哪里 |
code |
方法编号,对应 AIDL 接口中的某个方法 |
| 参数 |
用途 |
为什么必须手动指定 |
serviceClass |
反射获取 Stub.TRANSACTION_*,得到方法 code |
code 值定义在 AIDL 生成的 Stub 类中,必须知道完整接口名 |
serviceName |
调用 ServiceManager.getService(name) 获取目标服务 handle |
服务名是向 ServiceManager 注册的字符串,无法从类名稳定推导 |
methodName |
反射获取对应 TRANSACTION_* 常量 |
同一个接口有多个方法,必须指定代理哪一个 |
| 服务类名 |
服务名 |
ActivityManagerService |
"activity" |
PackageManagerService |
"package" |
WindowManagerService |
"window" |
| 字段 |
含义 |
serviceClass |
Binder 接口描述符,用于从事务数据中匹配接口 |
serviceName |
系统服务注册名,用于获取原始服务和代理服务 handle |
code |
AIDL 方法编号,运行时通过反射填充 |
handler |
对应的代理函数引用 |
| 层级 |
职责 |
| KSP 编译期 |
生成代理声明和路由表 |
| 用户态 seccomp 层 |
拦截 Binder ioctl,修改 handle |
| Binder 驱动 |
按 handle 完成原生路由 |
| 代理服务 |
执行业务逻辑 |
| 内核观测层 |
统计、审计、追踪和辅助策略 |
ioctl(fd, BINDER_WRITE_READ, &bwr)
seccomp-notify
binder_transaction_data.target.handle
ServiceManager.getService("package")
binder_transaction_data.target.handle
binder_transaction_data
┌──────────────────────────────────────┐
│ 编译期:KSP 注解处理 │
│ │
│ @ProxyMethod( │
│ serviceClass, │
│ serviceName, │
│ methodName │
│ ) │
│ │
│ 自动生成代理路由表和代理服务类 │
└──────────────┬───────────────────────┘
│
│ 生成代理路由表
▼
┌──────────────────────────────────────┐
│ 运行时:Seccomp 拦截层 │
│ │
│ Seccomp-BPF 过滤 ioctl │
│ 解析 txn │
│ 匹配路由表 │
│ 替换 target.handle │
└──────────────┬───────────────────────┘
│
│ 修改后的 Binder txn
▼
┌──────────────────────────────────────┐
│ 辅助系统:初始化层 │
│ │
│ 服务名 → handle 映射 │
│ code → 方法名映射 │
│ BWR / Txn 解析函数 │
│ 代理服务启动与注册 │
└──────────────────────────────────────┘
@ProxyMethod(
serviceClass = "android.content.pm.IPackageManager",
serviceName = "package",
methodName = "getInstalledPackages"
)
fun proxyGetInstalledPackages(flags: Int, userId: Int): Any? {
}
(serviceClass, serviceName, methodName)
android.content.pm.IPackageManager
package
getInstalledPackages
serviceName = "package"
├── getInstalledPackages
├── getPackageInfo
└── queryIntentActivities
serviceName = "activity"
├── startActivity
└── getRunningAppProcesses
serviceClass
→ serviceName
→ code
→ handler
android.content.pm.IPackageManager
→ package
→ 3
→ proxyGetInstalledPackages
SECCOMP_RET_USER_NOTIF
Linux kernel >= 5.10
Android 12+
1. 检查系统调用号
- 如果不是 __NR_ioctl,直接放行
2. 检查 ioctl cmd 参数
- 如果不是 BINDER_WRITE_READ,直接放行
3. 命中 ioctl + BINDER_WRITE_READ
- 返回 SECCOMP_RET_USER_NOTIF
- 通知 supervisor
4. 其他情况
- 返回 SECCOMP_RET_ALLOW
1. 通过 SECCOMP_IOCTL_NOTIF_RECV 获取通知
2. 从通知中提取系统调用参数:
- fd
- cmd
- arg
3. 通过 process_vm_readv() 读取目标进程中的 binder_write_read
4. 遍历 BWR 写缓冲区,解析 Binder 命令
5. 遇到 BC_TRANSACTION 时:
- 解析 binder_transaction_data
- 读取 target.handle
- 读取 code
- 从数据缓冲区中提取接口描述符 serviceClass
- 根据 serviceClass 找到 serviceName
- 根据 serviceName 和 code 匹配代理路由表
6. 如果命中代理规则:
- 将 target.handle 替换为代理服务 handle
7. 通过 process_vm_writev() 写回修改后的数据
8. 通过 SECCOMP_IOCTL_NOTIF_SEND 通知内核继续执行
9. 内核恢复原始 ioctl
10. Binder 驱动读取到已经被修改的 handle,
将事务路由到代理服务
[培训]《冰与火的战歌:Windows内核攻防实战》!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于 1天前
被孤木落编辑
,原因: