首页
社区
课程
招聘
[原创]AndProxy 升级版:基于 Seccomp 的无 Hook Binder 服务代理
发表于: 1天前 609

[原创]AndProxy 升级版:基于 Seccomp 的无 Hook Binder 服务代理

1天前
609

本文是 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 的必要信息,并不是冗余字段。

因此,让开发者在注解中同时声明 serviceClassserviceName,是更务实的工程选择。

这样既避免维护大型映射表,也让代理关系更加显式。

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天前 被孤木落编辑 ,原因:
收藏
免费 26
支持
分享
最新回复 (17)
雪    币: 808
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
谢谢分享
1天前
0
雪    币: 1076
活跃值: (69)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
3
基于这个思路,实现了一个类似于CorePatch的内核模块,通过内核转发和注册系统服务,直接干掉签名校验
1天前
0
雪    币: 5
活跃值: (1266)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
谢谢分享
1天前
0
雪    币: 76
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
看看
1天前
0
雪    币: 26
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
1天前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
向大佬学习
1天前
0
雪    币: 2186
活跃值: (3183)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
厉害了
20小时前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
谢谢分享
18小时前
0
雪    币: 95
活跃值: (2862)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
666666666666
7小时前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
tql
6小时前
0
雪    币: 3576
活跃值: (4664)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
学习一下
5小时前
0
雪    币: 1115
活跃值: (1726)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
13
666
3小时前
0
雪    币: 3920
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
666
3小时前
0
雪    币: 5
活跃值: (2400)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
看看老哥
3小时前
0
雪    币: 361
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
学习学习
3小时前
0
雪    币: 361
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
17
孤木落 基于这个思路,实现了一个类似于CorePatch的内核模块,通过内核转发和注册系统服务,直接干掉签名校验
可以在应用层实现吗
3小时前
0
雪    币: 919
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
18
谢谢分享
1小时前
0
游客
登录 | 注册 方可回帖
返回