首页
社区
课程
招聘
[原创]沙箱对抗某商业安全 SDK 检测
发表于: 19小时前 301

[原创]沙箱对抗某商业安全 SDK 检测

19小时前
301

分析一个竞品的app的时候发现手机打开就闪退,按照以往的经验直接用apatch开启内核root,然后app基本上就能过,这次是碰到个狠的,发现开启lite模式都不行,还是能检测到并闪退,于是研究了一番它的检测最终成功在手机上实现魔改的框架进行hook,下边是整理的文档


1. 概述

某商业安全 SDK 采用 SaaS 加固模式,在运行时执行多层环境检测。检测到异常后通过信号终止进程并上报检测结果。本文还原其客户端检测的完整技术原理。


2. 检测架构

该 SDK 的检测分为两个独立层面,采用完全不同的技术路线:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌────────────────────────────────────────────────────────┐
│  第一层: Java/JNI 检测                                  │
│  通信方式: Binder IPC                                   │
│  检测目标: 已安装应用、开发者选项、PM 代理               │
│  特点: 不走文件系统 syscall,seccomp 无法拦截            │
├────────────────────────────────────────────────────────┤
│  第二层: Native syscall 检测                            │
│  通信方式: 内联 SVC #0 直接 syscall (非 libc 封装)       │
│  检测目标: root 文件路径、/proc 虚拟文件、fd 映射        │
│  特点: PLT/GOT hook 无效,仅 seccomp-bpf 可拦截         │
├────────────────────────────────────────────────────────┤
│  自杀机制: SIGALRM 定时器                               │
│  上报机制: 后台 Service 上传检测结果                      │
└────────────────────────────────────────────────────────┘

两层检测独立运行、互不依赖,分别由不同的 native SO 执行:

SO 文件 大小 职责
libcovault-appsec.so ~1.7M 第一层:Java/JNI 检测(全量包扫描、PM 代理、开发者选项)
libtoolChecker.so ~5.4K 第二层:Native syscall 检测(路径扫描、/proc 读取、fd 遍历)
libSecureComponent.so ~1.2M 辅助安全组件

3. 第一层: Java/JNI 检测 (libcovault-appsec.so)

3.1 整体流程

Application.onCreate 后立即通过 JNI 调用 Android Java API 执行检测,整个流程约 2 秒。

1
2
3
4
5
6
7
8
Application.onCreate
  ├── SDK 初始化
  ├── PackageManager 代理检测
  ├── 全量已安装应用扫描 (核心)
  ├── DevicePolicyManager 检测
  ├── USB 调试 / 开发者选项检测
  ├── JSON 组装检测结果
  └── 启动 Service 上报

3.2 全量包扫描

核心检测手段。通过 native 层 JNI 调用 PackageManager API:

1
2
3
4
5
// 第一轮:获取所有已安装包(含元数据)
PackageManager.getInstalledPackages(GET_META_DATA)
 
// 第二轮:获取所有已安装应用信息
PackageManager.getInstalledApplications(GET_META_DATA)

对每个应用提取多维度特征:

特征 API 用途
包名 ApplicationInfo.packageName 黑名单比对
APK 路径 ApplicationInfo.sourceDir 安装位置识别
应用名称 getApplicationLabel() 辅助识别
权限列表 getPackageInfo(GET_PERMISSIONS) 敏感权限检测
Xposed 元数据 Bundle.getString("xposeddescription") Xposed 模块检测
ContentProvider ProviderInfo.authority 特定 Provider 检测

关键特点

  • 全量扫描:不是只查特定包名,而是遍历设备上全部已安装应用,扫描结果上传服务端判定
  • 通过 JNI 执行:native 层直接调用 JNI 的 CallObjectMethodV,绕过了 Java 层 Method Hook
  • Xposed 模块识别:检查每个应用的 AndroidManifest.xml 是否包含 xposeddescription 元数据

3.3 PackageManager 代理检测

1
2
3
4
5
// 原理
Object sPackageManager = ActivityThread.sPackageManager;
Proxy.getInvocationHandler(sPackageManager);
// 如果 sPackageManager 是 Proxy 实例 → PM 被 hook
// 如果抛出 IllegalArgumentException → 正常

检测通过 java.lang.reflect.Proxy 动态代理 IPackageManager 的 hook 方案。

3.4 开发者选项检测

1
2
Settings.Secure.getString("adb_enabled")
Settings.Global.getString("development_settings_enabled")

值为 "1" 则标记为异常。

3.5 为什么 seccomp 拦截不到

getInstalledPackages 走 Binder IPC:

1
app 进程 → ioctl(binder_fd, BINDER_WRITE_READ) → system_server → 包数据库 → Binder → app 进程

数据流走 ioctl(Binder 底层),不涉及 openat/faccessat 等文件系统 syscall。seccomp-bpf 只能拦截特定 syscall,无法干预 Binder 通信内容。


4. 第二层: Native syscall 检测 (libtoolChecker.so)

4.1 整体流程

由独立的轻量级 SO(仅 5.4K)在后台线程中执行:

1
2
3
4
5
后台线程
  ├── [1] faccessat 路径扫描 (70+ 条)
  ├── [2] statx 二次验证 (关键路径)
  ├── [3] /proc 虚拟文件读取 (maps/status/mounts)
  └── [4] fd 遍历 (newfstatat + readlinkat)

4.2 文件路径扫描

直接内联 SVC 指令:检测 SO 不通过 libc 封装函数(如 access()stat()open())发起系统调用,而是在代码中直接使用内联汇编 SVC #0 指令。这意味着 PLT hook(如 xhook/bhook 等基于 GOT/PLT 表修改的方案)完全无法拦截这些调用,因为执行路径根本不经过 libc 的函数入口。

// 伪代码示意 (arm64)
MOV X8, #48          // __NR_faccessat
MOV X0, #-100        // AT_FDCWD
LDR X1, =path_str   // 文件路径
MOV X2, #0           // F_OK
SVC #0               // 直接陷入内核,不经过 libc

这是一种常见的反 hook 技术:绕过用户态所有函数级 hook(PLT hook、GOT hook、inline hook libc),只有内核级拦截(seccomp-bpf)才能干预。

通过 faccessat(AT_FDCWD, path, F_OK, 0) 检测 root 相关文件,覆盖 70+ 条路径:

su 二进制 (~24 条):
遍历所有常见 su 安装位置,包括 /system/bin/su/system/xbin/su/sbin/su/vendor/bin/su/apex/com.android.runtime/bin/su 等。

Magisk (~29 条):
覆盖 Magisk 的全部可能存在路径,包括 /sbin/magisk/data/adb/magisk/cache/magisk.log/system/addon.d/99-magisk.sh 等。

SuperSU / daemonsu (~12 条):
包括 /.subackup/data/.supersu/su/bin/daemonsu 等。

Xposed / Substrate (~6 条):
包括 /system/lib/libsubstrate.so/magisk/xposed/system/lib/libart.so 等。

4.3 statx 二次验证

在 faccessat 扫描之后,使用 statx(syscall 291)对关键路径进行二次验证。

statx 是 Linux 4.11 引入的新 syscall,功能类似 stat 但 syscall 编号不同。如果 seccomp 过滤器只覆盖了 faccessat (48) 和 newfstatat (79),没有覆盖 statx (291),检测就能绕过拦截。

同理,faccessat2(syscall 439,Linux 5.8 引入)也可能被用于绕过不完整的 syscall 过滤。

原理

1
2
3
4
5
// 同一个文件,两个不同 syscall 编号
faccessat(AT_FDCWD, "/system/bin/su", F_OK)   // syscall 48
statx(AT_FDCWD, "/system/bin/su", 0, 0, buf)  // syscall 291
 
// 如果 seccomp 只拦截了 48291 就漏过去了

4.4 /proc 虚拟文件检测

文件 检测内容 异常指标
/proc/self/maps 内存映射中的 SO 文件名 存在 hook 框架相关 SO
/proc/self/status Seccomp: 字段 值为 2 说明安装了 seccomp 过滤器
/proc/self/status TracerPid: 字段 非 0 说明正在被调试
/proc/self/mounts 挂载点列表 存在 Magisk overlay 等异常挂载

正常应用的 Seccomp: 值为 0,安装了 seccomp 过滤器的进程值为 2。检测 SDK 利用这一点反过来识别使用 seccomp 进行对抗的沙箱。

4.5 fd 遍历

遍历 /proc/self/fd/ 下的所有文件描述符:

1
2
3
4
5
对每个 fd N:
  newfstatat(AT_FDCWD, "/proc/self/fd/N", &stat, 0)
  readlinkat(AT_FDCWD, "/proc/self/fd/N", buf, size)
  → 获取 fd 指向的文件路径
  → 检查路径是否包含敏感关键词

此方法可以发现通过 dlopen 加载但已从 maps 中删除的 SO — 即使 /proc/self/maps 被过滤,已打开的 fd 仍然指向原始文件。


5. 自杀与上报机制

5.1 SIGALRM 自杀

1
2
3
4
启动时: alarm(N) 设置定时器
检测完成:
  ├── 全部通过 → 取消定时器,正常运行
  └── 发现异常 → 定时器到期 → SIGALRM → 进程死亡

选择 SIGALRM 而非 SIGKILL 的原因:

  • 日志中显示为 "Alarm clock" 而非 "Killed",更隐蔽
  • Java 层的 UncaughtExceptionHandler 无法捕获信号级进程终止
  • 应用看起来像正常超时退出

5.2 多进程架构

1
2
3
主进程      ← seccomp 已安装
隔离进程 1  ← 后台服务,上报检测结果
隔离进程 2  ← 看门狗,监控主进程

看门狗进程运行在独立进程空间,不受主进程 seccomp 影响。它可以从外部读取主进程的 /proc/PID/status/proc/PID/maps,理论上可以检测主进程是否安装了 seccomp 或被注入了 SO。


6. Inline Hook 检测

SDK 会校验关键函数(如 pthread_create)的入口指令:

1
2
3
4
// 原理
void *func = dlsym(libc, "pthread_create");
uint32_t first_insn = *(uint32_t *)func;
// 如果为跳转指令 (B/BL/BR/LDR PC 等) → 被 inline hook 篡改

Dobby 等 inline hook 框架会修改函数前几个字节为跳转指令。此检测会导致使用 inline hook 修改 libc 函数的方案失效。


7. 检测使用的 syscall 汇总

syscall 编号 (arm64) 用途
faccessat 48 文件存在性检查 (主要)
statx 291 文件状态扩展查询 (二次验证)
faccessat2 439 文件访问检查 v2
openat 56 打开文件
read 63 读取 /proc 虚拟文件
newfstatat 79 fd 状态查询
readlinkat 78 读取 fd 链接目标
fstat 80 fd 状态查询
statfs 43 文件系统状态

8. 数据收集行为

SDK 在检测过程中收集并上报以下数据:

数据类型 收集方式
设备全部已安装应用列表(包名、路径、权限、Provider) PackageManager API
每个应用的完整权限列表 getPackageInfo(GET_PERMISSIONS)
Xposed 模块标识 Bundle.getString("xposeddescription")
USB 调试状态 Settings.Secure
进程内存映射 /proc/self/maps
进程挂载信息 /proc/self/mounts
进程文件描述符列表 /proc/self/fd 遍历

全量应用扫描(遍历设备所有已安装应用并提取详细信息)超出了安全检测的合理最小范围。Android 11 引入包可见性限制正是为了限制此类行为。


9. 检测局限性

架构层面

  • 权限不对等:检测 SDK 运行在用户态,kernel root 运行在内核态,检测者权限低于被检测者
  • syscall 可被拦截:所有文件系统检测通过标准 syscall 执行,seccomp-bpf 是内核级拦截机制
  • Binder 返回值可被修改:PM 查询走 Binder,返回值经过 Java 层可被过滤
  • 单次检测:启动时一次性执行,无持续监控

实现层面

  • 内联 SVC 仍可被拦截:虽然使用 SVC #0 绕过了 PLT/GOT hook,但 seccomp-bpf 工作在内核层面,在 syscall 进入内核时拦截,与用户态调用方式无关。无论是 libc 封装还是内联 SVC,最终都要通过 SVC #0 陷入内核,seccomp 都能拦截
  • 路径硬编码:su/magisk 路径列表固定在 SO 中
  • 未校验拦截行为:只看结果(文件存在/不存在),不验证 syscall 是否被拦截
  • SIGALRM 依赖检测结果:全部通过就不触发自杀
  • 看门狗进程未充分利用:理论上可从外部检测主进程异常,实际未观察到充分利用

与更强方案的对比

方案 检测层面 绕过难度
本 SDK (文件扫描) 用户态 syscall 低 (seccomp)
本 SDK (包扫描) 用户态 Binder 低 (Java hook)
Play Integrity API 硬件 TEE + 服务端 极高
服务端行为分析 服务端 视实现而定
内核级检测 内核 极高

纯客户端用户态检测方案的根本局限在于:检测代码和被检测环境运行在同一权限层级(甚至更低),无法建立可信的信任根。真正有效的检测需要将信任根转移到硬件(TEE)或服务端。


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

上传的附件:
收藏
免费 7
支持
分享
最新回复 (1)
雪    币: 1
活跃值: (1180)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
6
4小时前
0
游客
登录 | 注册 方可回帖
返回