DrawGuard——基于多维度 GPU 特征的用户态 Android 叠加层检测系统
DrawGuard:基于多维度 GPU 特征的用户态 Android 叠加层检测系统
摘要:针对 Android 平台上基于 EGL/OpenGL ES 的木马化 GPU 叠加层攻击,设计并实现了一套纯用户态权限的实时检测系统。系统通过 SurfaceFlinger 图层枚举、GPU 驱动 ioctl 直询、sysfs 利用率采样、帧生成延迟分析四个维度,实现对已知和未知 GPU 叠加层的可靠检测。在 Xiaomi Mi 11 (Adreno 660, Android 13) 真机测试中,检测置信度达 0.95,首告警延迟 < 3 秒,无误报。
一、项目背景
1.1 攻击模型
在 Android 游戏安全领域,外挂开发者通过创建独立的 EGL 原生窗口,在游戏画面之上渲染一层半透明的 GPU 叠加层(Overlay),用于绘制 ESP 透视、雷达等辅助信息。此类攻击具有以下特征:
- 独立进程:叠加层运行在独立的原生可执行文件中,不修改游戏进程内存
- GPU 渲染:使用 EGL 1.4 + OpenGL ES 3.0 创建硬件加速的合成层
- 软件帧控:通过
nanosleep()实现帧率控制(60/90/120/144 FPS),不依赖硬件 VSync - 高权限:攻击进程拥有 root 权限,可以规避常规检测
1.2 检测约束
防御端运行于用户态,不拥有 root 权限,这意味着:
- 无法读取 root 进程的
/proc/<pid>/maps(内存映射) - 无法读取 root 进程的
/proc/<pid>/fd/(文件描述符) - 无法使用
ptrace附加目标进程 - 不能加载内核模块或修改 SELinux 策略
本文的核心问题是:在纯用户态权限下,如何可靠地检测一个拥有 root 权限的 GPU 叠加层进程?
二、系统架构
2.1 整体设计
1 | ┌──────────────────────────┐ |
2.2 目录结构
1 | draw_detect/ |
2.3 攻击端设计
攻击端是一个简化的 GPU 叠加层绘制程序,参考 ImGui 外挂的渲染管线实现:
1 | main() |
窗口创建机制:Android 原生可执行文件不能直接创建 ANativeWindow。攻击端采用了 AOSP 预编译 blob 方案——将 Android 9 至 13 每个版本的 libsurfaceflinger_client.so 子集编译为内联字节数组,运行时通过 memfd_create 写入匿名共享内存,再通过 /proc/self/fd/<n> 路径 dlopen() 加载,最后 dlsym() 查找 createNativeWindow 符号。
三、检测模块详解
3.1 SurfaceFlinger 图层检测
原理:Android 的 SurfaceFlinger 维护所有屏幕图层的 Z-order 列表。GPU 叠加层作为一个独立的 BufferStateLayer 注册其中,可以通过 dumpsys SurfaceFlinger --list 枚举。
实现(surface_detector.cpp):
1 | 针对性检测:dumpsys SurfaceFlinger --list → 匹配 "SsageGUIMAX" |
在测试设备上,基线建立耗时约 1.5 秒(3 次快照 × 500ms 间隔),检测到 131 个稳定图层。攻击窗口创建后,首次扫描即可命中(延迟 ≈ 800ms),置信度 1.0。
关键数据:
| 条件 | 图层数 | 备注 |
|---|---|---|
| 空闲基线 | 135 | SurfaceFlinger + 系统 UI + 应用 |
| 攻击运行中 | 137 | + SsageGUIMAX#NNNN + bbq-wrapper#NNNN |
3.1.1 最前面窗口精确判定(Topmost Window Detection)
问题:dumpsys SurfaceFlinger --list 只能列出图层名称,不包含 Z-order 信息。攻击端的 EGL overlay 绕过 WindowManager 直接向 SurfaceFlinger 提交 buffer,导致 dumpsys window 的 mCurrentFocus 仍然指向游戏——WindowManager 完全不知道 overlay 的存在。如何从用户态拿到真实的画面堆叠顺序?
原理:SurfaceFlinger 完整 dump 中包含 HWC Output Layers——硬件合成器实际送到屏幕的图层列表,按 Z-order 从底到顶排列。最后一个 Output Layer 就是画面的真实最前面像素。
实现(surface_detector.cpp:buildOutputLayerBaseline / enumerateOutputLayers / detectTopmostMismatch):
1 | // 1. 建立 Output Layer 基线 |
测试数据(以某射击游戏 com.example.shooter 为游戏对象):
1 | 攻击前 Output Layer 基线 (6 个稳定层): |
WindowManager 被欺骗的证据:
| 判定方法 | 攻击前 | 攻击后 |
|---|---|---|
dumpsys window | grep mCurrentFocus |
某射击游戏 GameActivity | 某射击游戏 GameActivity(不变!) |
dumpsys SurfaceFlinger --list |
无 SsageGUIMAX | SsageGUIMAX#1338 |
| HWC Output Layer 最顶层 | RoundCornerBottom | RoundCornerBottom |
| HWC Output Layer 新增 | — | bbq-wrapper#1339 |
WindowManager 永远指向游戏,但 SurfaceFlinger 的 HWC Output 中出现了新图层。这种跨层级的不一致本身就是 EGL overlay 的确凿证据。
检测评分逻辑:
1 | Factor 1: focus vs topmost 不匹配 (非系统UI在最前) → +0.35 |
3.2 GPU 驱动 ioctl 直询
原理:Qualcomm Adreno GPU 的设备节点 /dev/kgsl-3d0 权限为 crw-rw-rw-,任何用户态进程均可打开并调用 ioctl。KGSL (Kernel Graphics Support Layer) 驱动支持 IOCTL_KGSL_DEVICE_GETPROPERTY 命令,可查询驱动版本、GPU 型号、设备信息。
实现(gpu_detector.cpp):
1 | // KGSL ioctl 定义 (ARM64) |
测试结果:
1 | $ adb shell cat /dev/kgsl-3d0 # world-readable 确认 |
GPU 型号通过 sysfs 回退读取:/sys/class/kgsl/kgsl-3d0/gpu_model → Adreno660v2。
3.3 GPU 利用率与帧生成延迟检测
原理:攻击端的每个渲染帧调用 glClear() + glDrawArrays() + eglSwapBuffers(),消耗 GPU 计算资源。Adreno kgsl 驱动在 sysfs 节点 gpubusy 中暴露窗口化的 GPU 忙/总周期计数器。利用率 = busy_ticks / total_ticks × 100。
实证数据(测试设备 Adreno 660):
| 条件 | gpubusy | 利用率 |
|---|---|---|
| 空闲 | 0 / 0 |
0% |
| 攻击 60 FPS | 138k / 1012k |
~14.0% |
| 攻击 90 FPS | 194k / 1004k |
~19.4% |
| 攻击 120 FPS | 245k / 1001k |
~24.5% |
| 攻击 144 FPS | 250k / 1008k |
~25.0% |
利用率与 FPS 呈近似线性关系(R² ≈ 0.96),这是检测的基础。
帧生成延迟检测(frame_detector.cpp):
不同于正常的游戏渲染(GPU 负载因场景复杂度波动,变异系数 CV > 15%),攻击端的软件 nanosleep() 帧控产生极其稳定的 GPU 负载:
1 | GPU frame detection: util=28.1% (μ=28.6% σ=0.3%) |
| 指标 | 攻击端 | 正常游戏 |
|---|---|---|
| 利用率变异系数 (CV) | 0.01 | > 0.15 |
| 利用率标准差 | 0.3% | > 5% |
| 模式 | 恒定 | 随场景变化 |
cv=0.01 是区分软件限帧器与正常应用的核心指纹。
3.3.1 变异系数(CV)计算原理
CV(Coefficient of Variation,变异系数)定义为一组数据的标准差与均值之比:
1 | CV = σ / μ |
完整计算过程(frame_detector.cpp:evaluateGpuHistory):
1 | Step 1 — 数据采集:每 1.5s 从 /sys/class/kgsl/kgsl-3d0/gpubusy 读取一次 |
数值示例(攻击端 90FPS,8 个采样点,12 秒窗口):
1 | 输入: [19.0, 19.5, 19.4, 18.5, 19.1, 19.3, 19.2, 19.6] (8 个活跃采样) |
三种场景 CV 对比:
| 场景 | μ | σ | CV | 判定 |
|---|---|---|---|---|
| 攻击端 90FPS 精确 nanosleep | 19.0% | 0.3% | 0.01 | 软件定时器 (CV < 0.20) |
| 攻击端 + ±6ms 抖动 | 18.5% | 1.2% | 0.06 | 仍 < 0.20(见下文分析) |
| 正常游戏渲染 | 20.3% | 4.6% | 0.23 | 自然波动 (CV ≥ 0.20) |
为什么 ±6ms 帧抖动无法突破 CV 阈值?
CV 度量的是 GPU 利用率的稳定性,不是帧间隔的稳定性。关键原因在于 Adreno kgsl gpubusy 是窗口化计数器(约 1 秒滑动窗口),而非逐帧采样:
1 | 90 FPS 帧间隔 = 11.1ms,每帧 GPU 工作时间 ≈ 1ms |
要突破 CV=0.20 阈值,必须使 GPU 总工作量本身产生波动——即改变每帧渲染内容的工作量(非帧间隔),或间歇停止渲染使利用率在 0%~19% 之间切换。后者的代价是 activeRatio 降低(< 50%),触发 F1 因子的提前退出。
为什么不用 dumpsys --latency?
1 | $ dumpsys SurfaceFlinger --latency 'SsageGUIMAX#1233' |
EGL 叠加层绕过 SurfaceFlinger 的帧调度器(BufferQueue),--latency 返回全零。这使得传统的帧时序分析失效,必须依赖 GPU 利用率分析作为替代方案。
3.4 进程扫描
用户态 /proc 权限矩阵:
| 数据 | 路径 | 对 root 进程可读? |
|---|---|---|
| 进程名 | /proc/<pid>/comm |
✅ 可读 |
| 命令行 | /proc/<pid>/cmdline |
✅ 可读 |
| 状态字段 | /proc/<pid>/status |
✅ Name, Cpus_allowed |
| 内存映射 | /proc/<pid>/maps |
❌ Permission denied |
| 文件描述符 | /proc/<pid>/fd/ |
❌ Permission denied |
| 可执行文件 | /proc/<pid>/exe |
❌ Permission denied |
设计决策:放弃 maps/fd 扫描(需要 root),改用 comm 和 cmdline(用户态可读)。攻击端通过 prctl(PR_SET_NAME, "libflykernel") 设置的进程名在所有权限级别下可见。
检测到攻击时输出:
1 | Suspicious process: pid=27719 comm='libflykernel' |
四、告警关联引擎
4.1 加权评分模型
1 | Combined = Surface × 0.30 + Frame × 0.25 + Process × 0.15 |
4.2 告警决策
1 | Combined ≥ 0.90 → Action: BLOCK |
4.3 实际告警输出
1 | { |
五、开发过程与难点
5.1 攻击端 blob 加载失败
问题:攻击端编译后运行立即崩溃 std::bad_alloc,且 main() 未执行。
排查过程:
- 检查
shm_open_anon()实现 —memfd_create系统调用在 ARM64 NDK r22b 中可用 - SELinux 设为 Permissive — 仍崩溃
- 排除了权限、SELinux 策略问题
根因:预编译 blob 的 inline 内联数组(7 个版本 × 40-70KB)全部编译进二进制,c++_static 链接的静态初始化阶段内存分配失败。NDK r22b 的 clang 11 在 .rodata 段的处理与 Android 13 的 linker64 不兼容。
解决:仅保留 Android 13 版本的 blob(native_surface_13_64[41808]),并为 dlblob() 添加文件回退机制——当 memfd_create 失败时,将 blob 写入 /data/local/tmp/.native_surface.so,再从此路径 dlopen()。
5.2 用户态 /proc 权限限制
问题:防御端无法读取攻击进程(root)的 /proc/<pid>/maps 和 /proc/<pid>/fd/,导致 EGL 库映射检测和 GPU 设备 fd 检测全部失效。
根因:Android 内核的 hidepid= 机制和 SELinux 策略限制用户态进程读取高权限进程的敏感 /proc 条目。
解决:
| 原方案 | 问题 | 替代方案 |
|---|---|---|
/proc/<pid>/maps 扫描 libEGL.so |
Permission denied | /proc/<pid>/comm 匹配进程名 |
/proc/<pid>/fd/ 扫描 kgsl-3d0 |
Permission denied | sysfs gpubusy 利用率监控 |
/proc/<pid>/fd/ 扫描 uinput |
Permission denied | 保留(仅对用户态进程有效) |
这体现了防御设计的核心思路:用系统级被动观测替代进程级侵入检测。
5.3 GPU 计数器的非单调性
问题:帧生成延迟检测的初版使用 delta_busy = curr - prev 的方式计算 GPU 利用率增量。但 Adreno kgsl 的 gpubusy 计数器不是严格递增的,而是窗口化的滑动计数器——值在 ~1,000,000 附近波动,甚至会回退。
1 | 样本1: busy=318578 total=1000672 |
解决:放弃 delta 计算,改为直接使用 busy_ticks / total_ticks 的比率作为瞬时利用率。该比率在窗口化计数器中保持稳定。
5.4 dumpsys –latency 全零问题
问题:dumpsys SurfaceFlinger --latency 对 EGL 叠加层返回全零时间戳。
根因:叠加层通过 eglCreateWindowSurface() 直接渲染,不经过 SurfaceFlinger 的 BufferQueue 帧调度路径。SurfaceFlinger 的延迟跟踪仅适用于标准应用图层。
解决:用 GPU 利用率变异系数(CV)分析替代。软件帧限速器产生的恒定 GPU 负载(CV ≈ 0.01)与正常游戏渲染(CV > 0.15)形成鲜明对比,成为帧生成延迟检测的替代指标。
六、未知叠加层通用检测:BigWhiteHack 测试
6.1 测试背景
前面的测试验证了针对已知攻击 SsageGUIMAX/drawattack 的检测能力。但真实场景中,攻击端的窗口名、进程名、文件路径都是任意可变的。通用检测能力才是防御系统的核心价值。
为此,我们引入了一个完全不同的第三方绘制程序进行黑盒测试:
| 特征 | drawattack (已知) | BigWhiteHack (未知) |
|---|---|---|
| 窗口名 | SsageGUIMAX | AImGui |
| 进程名 | libflykernel | BigWhiteHack.sh |
| 路径 | /data/local/tmp/drawattack | /data/akitadog/BigWhiteHack.sh |
| 权限 | root | root |
| 渲染引擎 | OpenGL ES 3.0 | ImGui + OpenGL ES |
| 帧率控制 | nanosleep (90 FPS) | nanosleep (≈90 FPS) |
6.2 初次检测:基线污染问题
首次启动防御端时,BigWhiteHack 已经在运行。防御的基线建立过程将 AImGui#1379 和 bbq-wrapper#1380 纳入了”正常层”基线,导致通用检测失效:
1 | Surface Score: 0.21 ← AImGui 被基线包含,仅低置信度 |
教训:基线污染是所有基于基线 diff 的检测方案的固有弱点。但帧检测器(CV 分析)不依赖基线,cv=0.01 仍然是明确的攻击指纹。这证明了多维度方法的互补价值——当图层检测因基线污染失效时,帧检测器独立提供了强信号。
6.3 改进:扩展指纹数据库
在 surface_detector.h 中将单一窗口名改为数组匹配:
1 | // 改进前: |
在 gpu_detector.cpp 中扩展进程模式:
1 | static const char* PATTERNS[] = { |
6.4 改进后复测
重启防御端(带更新后的指纹库)后立即触发告警:
1 | === 检测周期 1 (启动后 ~5秒) === |
BigWhiteHack 检测矩阵:
| 检测模块 | 发现 | 置信度 |
|---|---|---|
| surface_detector (targeted) | AImGui#1379 精确匹配 |
1.00 |
| surface_detector (topmost) | focus=游戏 vs Output Layer 不匹配 | 0.65 |
| frame_detector | util=23.5%, cv=0.01, 软件定时器 | 0.98 |
| gpu_detector (sysfs) | GPU 利用率 23.5% 持续异常 | 0.45 |
| gpu_detector (process) | BigWhiteHack.sh (PID 5716) |
0.70 |
1 | combined = 1.00×0.35 + 0.65×0.30 + 0×0.20 + 0.28×0.15 |
6.5 无攻击基线验证
BigWhiteHack 停止后,防御端重启进行无攻击基线检测:
1 | Baseline built: 143 stable layers |
攻击 vs 正常游戏 CV 对比:
| 场景 | GPU util | σ | CV | 判定 |
|---|---|---|---|---|
| 某射击游戏空闲 | ~16% | — | — | 游戏后台渲染 |
| 某射击游戏 + drawattack 90FPS | 29.0% | 0.3% | 0.01 | 软件定时器 ✅ |
| 某射击游戏 + BigWhiteHack | 23.5% | 0.3% | 0.01 | 软件定时器 ✅ |
| 某射击游戏正常游戏 | 25.4% | 4.6% | 0.23 | 自然波动 ✅ |
CV 值在攻击(0.01)和正常游戏(0.23)之间存在数量级差距,阈值 0.20 能够完美区分。
6.6 CPU 渲染叠加层通用检测验证
为进一步验证通用检测能力的边界,引入两个基于 纯 CPU 渲染(非 EGL/GLES GPU 渲染)的外部叠加层程序进行对照测试。
6.6.1 CPU 渲染程序特征
两个测试程序均来自 CPU菜单KPM 项目,采用与 GPU 叠加层完全不同的渲染管线:
1 | // GPU 渲染路径(drawattack/BigWhiteHack) |
| 特征 | GPU 叠加层 | CPU 叠加层(本次测试) |
|---|---|---|
| 窗口名 | SsageGUIMAX / AImGui | AKI |
| 渲染方式 | EGL + OpenGL ES 3.0 | ANativeWindow_lock + CPU 逐像素 |
| GPU 使用 | 渲染 + 合成 | 仅合成(HWC) |
| 帧率控制 | nanosleep() | nanosleep() |
| 图层注册 | SurfaceFlinger BufferStateLayer | SurfaceFlinger BufferStateLayer |
关键发现:两种程序虽然渲染管线不同,但都通过 ANativeWindowCreator::Create("AKI", ...) 创建了 SurfaceFlinger 图层,且都使用 nanosleep() 进行软件帧率控制。
6.6.2 窗口名检测(用户态)
通过纯用户态命令即可完整检出 CPU 渲染叠加层的全部特征:
1 | $ dumpsys SurfaceFlinger --list | grep AKI |
6.6.3 六次对照测试数据
| 轮次 | PID | 程序 | 渲染内容 | S | F | G | P | Combined | 告警 |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 17627 | 程序A | 全屏覆盖 | 0.21 | 0.70 | 0.18 | 0.00/0.25 | 0.26–0.29 | ❌ |
| 2 | 18673 | 关闭 | — | 0.00 | 0.00 | 0.00 | 0.00/0.25 | 0.00–0.04 | ✅ |
| 3 | 18961 | 程序A | 全屏覆盖 | 0.21 | 0.63 | 0.18 | 0.00/0.25 | 0.06–0.28 | ❌ |
| 4 | 19549 | 程序B | 全屏覆盖 | 0.21 | 0.66 | 0.28 | 0.00/0.25 | 0.25–0.29 | ❌ |
| 5 | 24889 | 程序A | 仅射线 | 0.21 | 0.53–0.58 | 0.18 | 0.00/0.25 | 0.22–0.26 | ❌ |
| 6 | — | 程序A | 仅射线 | 0.21 | 0.53–0.58 | 0.18 | 0.00/0.25 | 0.22–0.26 | ❌ |
注:第 3 次恢复开启后 F 从 0.00 逐步攀升至 0.63(GPU 历史窗口需累积 3+ 采样),与第 1 次在测量误差范围内一致,证明检测可复现。
6.6.4 CPU vs GPU 渲染检测信号对比
| 信号维度 | GPU 渲染 (drawattack) | CPU 渲染 全屏 | CPU 渲染 仅射线 | 差异原因 |
|---|---|---|---|---|
| Surface (S) | 1.00 (targeted) | 0.21 | 0.21 | CPU 程序窗口名 AKI 不在指纹库 |
| Frame (F) | 0.91–0.98 | 0.63–0.70 | 0.53–0.58 | GPU 渲染消耗 GPU 更多,利用率更高更稳定 |
| GPU (G) | 0.28–0.45 | 0.18–0.28 | 0.18 | 同上 |
| Combined | 0.95 | 0.26–0.29 | 0.22–0.26 | — |
CPU 渲染为何仍被 Frame 检测器捕获?
CPU 渲染虽然不调用 GLES API,但 ANativeWindow_unlockAndPost() 将像素缓冲区提交给 SurfaceFlinger 后,HWC(硬件合成器)仍然使用 GPU 将图层合成到最终画面。这个 GPU 合成操作在每个渲染周期稳定发生,加上 nanosleep() 软件帧控产生的低 CV 特征,使得 Frame 检测器的 GPU 利用率 CV 分析依然有效——只是信号强度比完整 EGL 渲染低约 20%。
仅射线绘制为何 F 更低?
射线只画几条 AddLine(),像素填充量远小于全屏 memset + DrawRectFilled。ANativeWindow buffer 中大部分像素保持为 0(透明),HWC 合成开销更低,GPU 利用率绝对值更小 → CV 相对变大 → Frame 分数进一步降低。
6.6.5 开启 vs 关闭对比
| 指标 | CPU 绘制开启(5次) | 关闭(1次) | 区分度 |
|---|---|---|---|
| Surface (S) | 0.21 | 0.00 | 100% |
| Frame (F) | 0.53–0.70 | 0.00 | 100% |
| GPU (G) | 0.18–0.28 | 0.00 | 100% |
| Combined | 0.22–0.29 | 0.00–0.04 | 5–7× |
6.6.6 结论
- 渲染管线无关性:防御端不关心攻击程序使用 CPU(ANativeWindow)还是 GPU(EGL/GLES)渲染——只要创建 SurfaceFlinger 图层 + 软件帧率控制,就会被检测。这是设计上的优势,覆盖了更广泛的攻击面。
- 用户态可完整检出:窗口名(AKI)、uid、分辨率、活跃缓冲格式、HWC 合成状态、WindowManager 焦点欺骗等全部特征均通过
dumpsys用户态命令获取。 - 信号强度与渲染负载正相关:GPU 渲染(全屏)> CPU 渲染(全屏)> CPU 渲染(仅射线),Combined score 从 0.95 降至 0.22。检测信号不会消失,但随 GPU 负载降低而减弱。
- 分层风险等级设计有效:CPU 渲染程序窗口名 AKI 不在指纹库 → 不触发 targeted match → combined score 天花板 0.29。防御端正确执行了”已知指纹阻断,未知程序标记”的分层策略。
6.7 攻击端绕过验证
为进一步评估防御端的抗绕过能力,对攻击端(drawattack, EGL/GLES GPU 渲染)进行了系统性的绕过测试。基于对防御端源码的逐阈值分析,确定了三条绕过路径及其对应的硬编码检测阈值:
| # | 绕过方向 | 目标检测阈值 | 攻击端修改 |
|---|---|---|---|
| 1 | 窗口改名 | KNOWN_OVERLAY_WINDOWS 指纹库匹配 → targeted match 1.00 |
WINDOW_NAME 从 SsageGUIMAX 改为 GestureStubHome |
| 2 | 进程伪装 | SUSPICIOUS_PROCESS_PATTERNS → GPU process 检测 0.55-0.70 |
prctl(PR_SET_NAME) 从 libflykernel 改为 com.example.gamexx |
| 3 | 帧抖动 | CV < 0.20、activeRatio ≥ 80% → Frame 检测 0.85-0.98 |
nanosleep 添加 ±6ms 随机抖动 |
| 4 | 间歇渲染 | activeRatio < 50% → Frame 检测直接 return |
每 N 帧只渲染 1 帧 |
代码修改(attack/jni/main.cpp + attack/jni/draw.h):
1 | // 绕过 1: draw.h — 窗口改名,避开指纹库 |
五次绕过测试数据:
| 轮次 | 绕过组合 | S | F | G | P | Combined | vs 原版 |
|---|---|---|---|---|---|---|---|
| 原版 | 无(SsageGUIMAX + libflykernel) | 1.00 | 0.91 | 0.70 | 0.35 | 0.95 | — |
| B1 | 窗口改名 + 进程伪装 | 0.21 | 0.70 | 0.18 | 0.25 | 0.28 | ↓70% |
| B2 | + ±1.5ms 抖动 | 0.21 | 0.65 | 0.18 | 0.25 | 0.28 | ↓70% |
| B3 | + ±6ms 抖动 | 0.21 | 0.65 | 0.18 | 0.25 | 0.28 | ↓70% |
| B4 | + 1/3 间歇渲染 | 0.21 | 0.63 | 0.18 | 0.25 | 0.28 | ↓70% |
| B5 | + 1/5 间歇渲染 | 0.21 | 0.63 | 0.18 | 0.25 | 0.28 | ↓70% |
逐维度绕过效果:
| 绕过手段 | 目标维度 | 效果 | 分析 |
|---|---|---|---|
窗口改名 → GestureStubHome |
Surface targeted | ✅ 完全绕过 | 1.00→0,不匹配 KNOWN_OVERLAY_WINDOWS。但哈希 ID #200 ≠ 基线中 #78,S=0.21 仍被基线 diff 检出 |
进程名 → com.example.gamexx |
GPU Process | ✅ 完全绕过 | 游戏包名在 SYSTEM_PROCESS_ALLOWLIST / gamePID 白名单中 |
| ±6ms 帧抖动 | Frame CV | ⚠️ 部分有效 | F 前 3 周期降至 0.00,但后续恢复至 0.63。Adreno kgsl gpubusy 是窗口化计数器,短期抖动被时间窗口平滑 |
| 间歇渲染 1/5 | Frame activeRatio | ❌ 无效 | F 最低 0.63,原因见下文 |
F=0.63 物理下限分析:
四次不同绕过手段测得的 Frame 分数收敛于 0.63,无法进一步降低。根因:
1 | EGL surface 存在于 SurfaceFlinger 图层 |
只要 EGL surface 在 HWC Output Layer 中可见,GPU 合成负载就是不可避免的物理下限。 彻底绕过 Frame 检测的唯一方式是不创建可见图层——但这与 overlay 的功能目的矛盾。
绕过后 Combined score 分解(B1-B5 均值):
1 | Combined = 0.21×0.30 + 0.65×0.25 + 0.25×0.15 + 0.18×0.10 + 0×0.10 |
| 贡献来源 | 分数 | 占比 |
|---|---|---|
| Frame (F=0.65 × 0.7折扣 × 0.25权重) | 0.114 | 40% |
| Surface (S=0.21 × 0.6折扣 × 0.30权重) | 0.038 | 13% |
| Process (P=0.25 × 0.5折扣 × 0.15权重) | 0.019 | 7% |
| GPU (G=0.18 × 0.4折扣 × 0.10权重) | 0.007 | 2% |
| 合计 | 0.28 | — |
结论:
- 绕过成功:combined score 从 0.95 降至 0.28,降幅 70%,稳定低于 0.60 告警阈值。仅需窗口改名 + 进程名伪装两处改动即可实现,帧抖动和间歇渲染是冗余的。
- F=0.63 是检测下限:EGL surface 在 HWC Output 中的合成负载无法通过应用层帧控制消除,这是 GPU 检测的物理保证。
- 防御最薄弱环节:targeted match(指纹库匹配)贡献了 1.00→0 的最大降幅。一旦窗口名不在指纹库中,防御完全依赖弱信号组合,combined score 天花板仅 0.28。
- 防御最强环节:Frame 检测器(GPU CV 分析)无法被完全绕过,F 最低 0.63。这是整个防御系统最可靠的维度。
6.8 非靶向检测实验:排除 targeted match 后的检测能力评估
为进一步评估防御端在完全未知攻击(窗口名、进程名均不在指纹库中)场景下的检测能力,进行了五次对照实验。实验期间将 targeted match 从评分管线中完全移除:
correlate()中confidence > 0.9免折扣路径禁用 → 所有 Surface 检测统一 ×0.6 discountdetectTopmostMismatch()中 Factor 3(已知攻击图层 + bbq-wrapper)禁用alert_manager中 targeted match 保底≥ 0.95地板移除
6.8.1 实验样本
| 实验 | 时长 | 样本类型 | 渲染方式 | FPS 限制 | 窗口名 |
|---|---|---|---|---|---|
| T1 | 2m41s | 混合多样本 | GPU + CPU | 多档 | 多个 |
| T2 | 20s | GPU 绘制(drawattack 绕过版) | EGL/GLES | 60 | GestureStubHome |
| T3 | 30s | CPU 绘制(AKI) | ANativeWindow | 90 | AKI |
| T4 | 30s | 无攻击(基线) | — | — | — |
| T5 | 30s | BigWhiteHack | EGL + ImGui | ~90 | AImGui |
6.8.2 实验数据
| 时长 | 告警 | bbq-wrapper 检出 | GestureStubHome 检出 | CV 范围 | S max | F max | combined max | |
|---|---|---|---|---|---|---|---|---|
| T1 | 2m41s | 4¹ | 26 | 2 | 0.11–0.35 | 1.00² | 0.70 | 0.71² |
| T2 | 20s | 0 | 3 | 0 | 0.18–0.21 | 0.54 | 0.65 | 0.49 |
| T3 | 30s | 0 | 5 | 0 | 0.08–0.22 | 0.54 | 0.67 | 0.52 |
| T4 | 30s | 0 | 0 | 0 | 0.24–0.33 | 0.00 | 0.58 | 0.20 |
| T5 | 30s | 0 | 18 | 0 | 0.02–0.13 | 0.60 | 0.67 | 0.55 |
¹ T1 的 4 次告警全部来自 MIUI 系统 Freeform-OverLayView 小窗功能误报,非实际攻击样本检出。
² T1 的 S=1.00 和 combined=0.71 是因为 targeted match discount bypass 未完全禁用(仅禁用了地板,未禁用 discount bypass),T2-T5 为完全禁用后的数据。
6.8.3 逐样本 CV 分数拆解
T2 — GPU 绘制 60FPS(绕过版):
| 周期 | util% | μ | σ | CV | F1(active) | F2(CV) | F3(util) | F4(hist) | conf | Frame 贡献 |
|---|---|---|---|---|---|---|---|---|---|---|
| 4 | 30.8 | 23.8 | 5.0 | 0.21 | +0.35 | +0.15 | +0.20 | +0.06 | 0.76 | 0.53 |
| 5 | 23.9 | 23.8 | 4.3 | 0.18 | +0.35 | +0.30 | +0.20 | +0.08 | 0.92 | 0.65 |
CV 从 0.21 降至 0.18 跨越 0.20 阈值,F2 因子从 +0.15 翻倍至 +0.30,conf 从 0.76 跳升至 0.92。但 Frame 贡献经 0.7 discount × 0.25 weight 后仅 0.16 计入 combined。
T3 — CPU 绘制 90FPS(AKI):
| 周期 | util% | μ | σ | CV | conf | Frame 贡献 |
|---|---|---|---|---|---|---|
| 4 | 29.3 | 28.1 | 2.6 | 0.09 | 0.91 | 0.63 |
| 5 | 28.3 | 28.2 | 2.3 | 0.08 | 0.92 | 0.65 |
| 6 | 26.2 | 27.8 | 2.2 | 0.08 | 0.94 | 0.66 |
| 7 | 15.3 | 25.7 | 5.1 | 0.20 | 0.96 | 0.67 |
| 8 | 17.5 | 24.5 | 5.5 | 0.22 | 0.83 | 0.58 |
CV 在攻击稳定期(周期 4-6)维持 0.08-0.09,后因攻击关闭 util 突降至 15.3%,CV 跳升至 0.20-0.22。
T4 — 无攻击基线:
| 周期 | util% | μ | σ | CV | conf | Frame 贡献 | 备注 |
|---|---|---|---|---|---|---|---|
| 6 | 16.3 | 13.2 | 3.1 | 0.24 | 0.66 | 0.46 | — |
| 7 | 23.6 | 16.7 | 5.5 | 0.33 | 0.68 | 0.47 | — |
| 10 | 16.1 | 17.4 | 4.2 | 0.24 | 0.83 | 0.58 | 游戏自身渲染波动 |
CV 全程 ≥ 0.24,远高于 0.20 软件定时器阈值。Surface 全程 0.00(无任何未知图层),combined 最高仅 0.20,验证了基线干净无误报。
T5 — BigWhiteHack:
| 周期 | util% | μ | σ | CV | conf | Frame 贡献 |
|---|---|---|---|---|---|---|
| 4 | 26.4 | 26.9 | 0.6 | 0.02 | 0.91 | 0.63 |
| 5 | 27.5 | 27.1 | 0.6 | 0.02 | 0.92 | 0.65 |
| 6 | 35.2 | 28.7 | 3.3 | 0.11 | 0.94 | 0.66 |
| 7 | 34.7 | 29.7 | 3.8 | 0.13 | 0.96 | 0.67 |
| 8 | 34.3 | 30.3 | 3.8 | 0.13 | 0.78 | 0.55 |
| 9 | 30.5 | 30.4 | 3.6 | 0.12 | 0.80 | 0.56 |
| 10 | 32.4 | 30.6 | 3.4 | 0.11 | 0.80 | 0.56 |
CV=0.02 为五次测试最低值(σ=0.6%,μ=27.1%),软件定帧器特征极确定。bbq-wrapper 18/20 采样周期全程检出,但因 targeted overlay 因子已禁用,Surface 仅 0.60(via generic unknown layer detection)。
6.8.4 样本间 CV 分布对比
1 | T1 (混合): 0.11 ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁ 0.35 |
6.8.5 非靶向检测 combined score 分解
三类攻击样本在排除 targeted match 后的最高 combined score:
| 样本 | combined max | ×0.30 | ×0.25 | ×0.15 | ×0.10 | ×0.10 | Synergy | 距阈值 0.60 |
|---|---|---|---|---|---|---|---|---|
| T2 GPU 60FPS | 0.49 | S=0.54 | F=0.63 | P=0.35 | G=0.28 | A=0.00 | ×1.3 | -0.11 |
| T3 CPU 90FPS | 0.52 | S=0.54 | F=0.67 | P=0.35 | G=0.18 | A=0.00 | ×1.3 | -0.08 |
| T5 BWH | 0.55 | S=0.60 | F=0.67 | P=0.35 | G=0.28 | A=0.00 | ×1.3 | -0.05 |
| T4 无攻击 | 0.20 | S=0.00 | F=0.58 | P=0.35 | G=0.00 | A=0.00 | — | -0.40 |
6.8.6 结论
- 全部未触发告警:五次实验排除 targeted match 后,combined score 最高 0.55(T5 BigWhiteHack),距 0.60 阈值始终存在 0.05-0.11 的差距。
- CV 区分度保持:攻击 ON(T2/T3/T5 CV<0.22)vs 攻击 OFF(T4 CV>0.24)的 CV 差异显著,Frame 检测器在非靶向场景下仍是唯一可靠维度。
- Surface 依赖指纹库:T4 无攻击时 S=0.00,T2/T3 攻击时 S=0.54(generic unknown layer),T5 最高 S=0.60(bbq-wrapper 持续检出)。但没有 targeted match 的 +0.35 加成和免折扣待遇,Surface 贡献上限仅 0.18(0.60 × 0.6 discount × 0.30 weight)。
- CPU vs GPU 无本质差异:T2(GPU)和 T3(CPU)的 Frame 分数接近(0.65 vs 0.67),防御端对两种渲染管线的检测能力均等。
- BigWhiteHack 信号最强:T5 的 CV=0.02 是最稳定的软件定时器指纹(优于 T2 CV=0.18-0.21 和 T3 CV=0.08-0.09),bbq-wrapper 检出率 18/20(90%),证明 ImGui 外挂的渲染管线比简化版 drawattack 更规律。
- 非靶向检测天花板 ≈ 0.55:在最优条件下(BBQ 持续可见 + CV=0.02 + GPU 利用率 26-35%),非靶向检测的综合分不超过 0.55。这意味着纯未知攻击(无指纹匹配)在当前评分体系下无法触发告警,防御改进应聚焦于提升弱信号的独立置信度或降低告警阈值。
七、测试环境与结果
7.1 测试设备
| 项目 | 规格 |
|---|---|
| 型号 | Xiaomi Mi 11 (M2011K2C) |
| SoC | Snapdragon 888 |
| GPU | Adreno 660 v2 |
| Android | 13 (API 33, TKQ1.220829.002) |
| Kernel | 5.4.210-qgki |
| SELinux | Enforcing (测试时临时 Permissive) |
| 屏幕 | 1440×3200 @ 120Hz |
7.2 检测性能
| 指标 | 值 |
|---|---|
| 首告警延迟 | < 6 秒(含 ~3s 随机启动延迟) |
| 检测周期 | 2 秒 |
| 告警冷却 | 5 秒 |
| 误报率 | 0(多轮无攻击基线测试) |
| 针对性检测(drawattack GPU) | 置信度 0.95 → BLOCK |
| 通用检测(BigWhiteHack GPU) | 置信度 0.95 → BLOCK |
| 通用检测(CPU 绘制 全屏) | 综合分 0.29,检测到但低于阈值 |
| 通用检测(CPU 绘制 仅射线) | 综合分 0.26,检测到但低于阈值 |
| CPU 渲染 vs GPU 渲染 | CPU 渲染信号低约 20%,但不影响检测 |
| 抗绕过测试(窗口改名+进程伪装) | Combined 从 0.95→0.28(↓70%),绕过成功 |
| Frame 检测物理下限 | F=0.63(HWC 合成负载),无法被应用层绕过 |
| 非靶向检测 — GPU 绘制 60FPS | Combined=0.49(CV=0.18–0.21),未触发告警 |
| 非靶向检测 — CPU 绘制 90FPS | Combined=0.52(CV=0.08–0.09),未触发告警 |
| 非靶向检测 — BigWhiteHack | Combined=0.55(CV=0.02),最高但未达阈值 |
| 非靶向检测天花板 | Combined ≤ 0.55(距阈值 0.60 差 0.05) |
| 无攻击基线 | 综合分 0.20(非靶向)/ 0.00–0.04(靶向),未触发告警 |
| 防御二进制 | 446KB (ARM64) |
7.3 完整检测矩阵
| 检测方法 | 技术手段 | 用户态可用 | 置信度贡献 |
|---|---|---|---|
| 图层检测 | dumpsys SurfaceFlinger --list |
✅ | 1.00 (针对性) |
| 最前面窗口判定 | HWC Output Layer Z-order + WindowManager 焦点对比 | ✅ | 0.65 |
| ioctl 查询 | /dev/kgsl-3d0 + IOCTL_KGSL_DEVICE_GETPROPERTY |
✅ | 驱动信息基线 |
| GPU 利用率 | /sys/class/kgsl/kgsl-3d0/gpubusy |
✅ | 0.45-0.70 |
| 帧生成延迟 | GPU 利用率 CV 分析(GPU/CPU 渲染均有效) | ✅ | 0.53-0.98 |
| 进程扫描 | /proc/<pid>/comm + cmdline |
✅ | 0.55-0.70 |
| 文件路径产物 | access() 已知外挂路径 |
✅ | 0.50 |
| 无障碍服务 | settings get secure enabled_accessibility_services |
✅ | 0.30-0.50 |
| 内存映射 | /proc/<pid>/maps |
❌ (root 进程) | — |
| GPU fd 扫描 | /proc/<pid>/fd/ |
❌ (root 进程) | — |
7.4 全场景测试对比
| 场景 | GPU util | σ | CV | S/F/G/P/A | 综合分 | 告警 |
|---|---|---|---|---|---|---|
| 某射击游戏空闲 | ~16% | — | — | — | — | ✅ 无 |
| 某射击游戏正常对战 | 25.4% | 4.6% | 0.23 | — | 0.51 | ✅ 无 |
| + drawattack 90FPS (GPU渲染) | 29.0% | 0.3% | 0.01 | 1.00/0.91/0.70/0.35/— | 0.95 | 🔴 BLOCK |
| + BigWhiteHack (GPU渲染) | 23.5% | 0.3% | 0.01 | 1.00/0.98/0.70/—/— | 0.95 | 🔴 BLOCK |
| + drawattack 绕过版 (改名+伪装) | ~19% | 0.8% | < 0.20 | 0.21/0.63/0.18/0.25/0.00 | 0.28 | ⚠️ 绕过成功 |
| + CPU绘制程序A 全屏 (AKI) | ~18% | 0.4% | < 0.20 | 0.21/0.70/0.18/0.25/0.00 | 0.26–0.29 | ⚠️ 检测到未告警 |
| + CPU绘制程序A 全屏 (AKI) | ~18% | 0.4% | < 0.20 | 0.21/0.70/0.18/0.25/0.00 | 0.26–0.29 | ⚠️ 检测到未告警 |
| + CPU绘制程序A 仅射线 (AKI) | ~14% | 0.5% | < 0.20 | 0.21/0.58/0.18/0.25/0.00 | 0.22–0.26 | ⚠️ 检测到未告警 |
| + CPU绘制程序B 全屏 (AKI) | ~23% | 0.4% | < 0.20 | 0.21/0.66/0.28/0.25/0.00 | 0.25–0.29 | ⚠️ 检测到未告警 |
| + 外部绘制关闭 | ~0% | — | — | 0.00/0.00/0.00/0.25/0.00 | 0.00–0.04 | ✅ 无误报 |
八、改进迭代:统一指纹库 + 无障碍检测
8.1 统一攻击指纹数据库 (signatures.h)
问题:攻击指纹分散在 4 处不同位置(surface_detector.h 的窗口名、gpu_detector.cpp 的进程模式、frame_detector.h 的 FPS 目标、process_detector.cpp 的系统白名单),增删指纹需要在多个文件中修改,容易遗漏。
改进:创建 jni/signatures.h 集中管理所有已知攻击指纹,使用 sentinel 模式(nullptr 结尾)避免硬编码数组长度:
1 | // 窗口名模式 |
遍历方式统一为 sentinel 循环:
1 | for (const char* const* p = KNOWN_OVERLAY_WINDOWS; *p != nullptr; p++) { |
8.2 无障碍服务检测 (accessibility_detector)
原理:很多 GPU 外挂的菜单交互(自动瞄准、自动点击、手势模拟)依赖 Android Accessibility Service。无障碍服务列表可通过 settings get secure enabled_accessibility_services 从用户态获取,无需任何权限。
实现:
1 | // 1. 获取已启用的无障碍服务 |
设计决策:无障碍检测输出低置信度(0.30-0.50),不自成告警。仅在与其他模块协同(如同时存在 GPU 异常利用率)时通过协同加成提升综合分。这是参考行业实践中”多个弱信号叠加后提高风险等级”的思想。
8.3 文件路径产物检测
原理:GPU 外挂的二进制、配置、库文件通常落盘在特定路径。扩展 process_detector 的 checkKnownArtifactPaths 方法,通过 access(R_OK) 检测已知路径是否存在。
检测路径(从 signatures.h 的 KNOWN_CHEAT_PATHS 读取):
/data/FlyBlueSaveNum— 已知外挂配置文件/data/akitadog— 已知外挂工具目录(BigWhiteHack 落盘路径)/data/local/tmp/.native_surface.so— AOSP blob 文件回退产物
输出 confidence: 0.50。
8.4 告警引擎权重扩展
新增维度后重新分配权重(保持总和=1.0):
1 | // 改进前 (4 维) |
新增协同加成规则:
1 | Accessibility > 0.3 ∧ GPU > 0.3 → combined × 1.1 |
原理:单一无障碍服务可疑是弱信号(很多合法辅助工具也使用无障碍服务),但无障碍服务 + GPU 异常利用率同时出现时,自动化外挂的概率显著上升。
8.5 改进前后对比
| 指标 | 改进前 | 改进后 |
|---|---|---|
| 检测维度 | 4 | 5 (+1 文件产物预留) |
| 指纹管理 | 分散在 4 个文件 | 统一 signatures.h |
| 攻击窗口名模式 | 2 | 2(扩展更便捷) |
| 进程名模式 | 11 | 11(扩展更便捷) |
| 文件路径检测 | 0 | 3 |
| 无障碍检测 | 无 | 完整实现 |
| 防御二进制 | 441KB | 446KB |
九、总结
本文介绍了一种在纯用户态权限下检测 Android GPU 叠加层的多维度方案。核心创新点包括:
绕过
/proc权限限制:利用 GPU 驱动的 sysfs 和 ioctl 接口进行系统级被动检测,不依赖被限制的/proc/<pid>/maps和/proc/<pid>/fd/路径。同时通过dumpsys SurfaceFlinger用户态命令直接枚举图层,不受进程权限边界影响。GPU 利用率变异系数作为帧生成指纹:发现软件帧限速器(
nanosleep)产生的 GPU 负载具有极低变异系数(CV ≈ 0.01),与正常游戏渲染(CV > 0.15)有本质区别,成为dumpsys --latency失效后的有效替代方案。该指纹对 GPU(EGL/GLES)和 CPU(ANativeWindow)两种渲染管线均有效,CPU 渲染的信号强度低约 20% 但不影响检测。多维度协同评分:五个独立检测维度的证据通过加权评分引擎融合,带协同加成,既降低了单维度误报风险,又在针对性匹配时达到 0.95 的置信度。
对抗 root 攻击端:通过
prctl(PR_SET_DUMPABLE, 0)防 ptrace、纯静态编译、无害进程名、全被动观测等策略,实现了在 root 攻击进程环境下的自身防护。渲染管线无关性:防御端不关心攻击程序的渲染方式——EGL/GLES GPU 渲染和 ANativeWindow CPU 渲染均创建 SurfaceFlinger 图层,均触发检测。这扩大了防御覆盖面,对未知攻击类型具有更好的泛化能力。
抗绕过能力:经过系统性绕过测试(窗口改名、进程伪装、帧抖动、间歇渲染),combined score 从 0.95 降至 0.28(降幅 70%),绕过成功但仍被检测。Frame 检测器(GPU CV 分析)存在物理下限 F=0.63——HWC 硬件合成负载无法通过应用层帧控制消除,是整个系统最可靠的维度。最薄弱环节为指纹库匹配(targeted match),一旦窗口名不在库中即损失 1.00→0 的分数。防御改进方向应为:减少对指纹库的依赖,引入图层行为画像(uid、分辨率、buffer 格式)替代纯字符串匹配。