Android APK 加固原理(六):RASP 运行时安全防护——如何检测 Frida、Hook 与运行时攻击

236 阅读31分钟

Android APK 加固原理(六):RASP 运行时安全防护——如何检测 Frida、Hook 与运行时攻击

本文是 Android APK 加固原理系列第六篇,基于开源项目 XopProtector 当前源码进行分析。

前五篇主要解决的是“代码如何隐藏”:

  • 第一篇:Native Shell 如何隐藏和恢复 DEX
  • 第二篇:如何缩短 DEX 明文暴露窗口
  • 第三篇:PVM1 方法级代码抽取
  • 第四篇:PVM2 Native Interpreter 代码虚拟化
  • 第五篇:SO .text 段加密与运行时动态解密

而到了第六篇,问题发生了变化:

如果攻击者已经进入 App 运行时,通过 Frida、Xposed、Inline Hook、调试器、内存修改等方式攻击,加固系统又应该怎么办?

这就是 RASP —— Runtime Application Self-Protection,运行时应用自保护。


一、什么是 RASP?

RASP 的全称是:

Runtime Application Self-Protection

中文通常称为:

运行时应用自保护。

它和传统 APK 加固最大的区别,是防护时机不同。

传统加固主要关注:

APK 文件
 ↓
DEX
SO
资源

解决:

“攻击者从 APK 静态文件中能不能直接看到代码?”

而 RASP 关注的是:

App 已经启动
 ↓
代码已经运行
 ↓
攻击者进入进程
 ↓
Frida / Hook / Debugger / Dump / Patch
 ↓
应用如何发现并响应?

因此可以把整个体系理解成:

APK 加固
                     │
          ┌──────────┴──────────┐
          │                     │
      静态保护                运行时保护
          │                     │
          ▼                     ▼
       DEX/SO                RASP
          │                     │
          ▼                     ▼
    防止静态分析          防止运行时攻击

XopProtector 的 README 已经明确将 RASP、Frida/Hook 扫描、Threat Report 等作为核心能力,并把 Native Shell 定位为负责解密、恢复、解释执行以及 RASP 的运行时组件。

所以:

RASP 并不是前面五篇技术的替代品,而是整个加固体系最后的一层运行时防线。


二、为什么前五层保护还不够?

假设我们已经完成:

DEX 加密
        ↓
PVM1
        ↓
PVM2
        ↓
SO .text 加密

攻击者仍然可能选择:

不分析 APK

而是:

直接启动 App
        ↓
Attach 到进程
        ↓
Frida
        ↓
Hook
        ↓
观察函数参数
        ↓
修改返回值
        ↓
Dump 内存

例如一个授权函数:

bool checkLicense();

静态分析可能很困难。

但是攻击者如果直接:

Hook checkLicense()
        ↓
return true

那么前面的:

DEX 加密
PVM
SO 加密

都可能被绕过去。

因此:

加固不仅要隐藏代码,还必须保护代码运行时的执行环境。

这就是 RASP 存在的意义。


三、XopProtector 的 RASP 不是一个检测器,而是一套状态机

从当前源码来看,XopProtector 的 RASP 可以抽象成:

Native Shell
                       │
                       ▼
                 RASP 初始化
                       │
                       ▼
                Risk Checker
                       │
          ┌────────────┼────────────┐
          │            │            │
          ▼            ▼            ▼
       Frida         Debugger      Hook
          │            │            │
          └────────────┼────────────┘
                       │
                       ▼
                 SO Integrity
                       │
                       ▼
              Xposed / Root / Emulator
                       │
                       ▼
                Threat Report
                       │
                       ▼
                RASP Action
                       │
        ┌──────────────┼──────────────┐
        │              │              │
       Alert         Degrade        Block
        │              │              │
      记录            降级          终止

源码中的 handle_risk() 是整个响应链的核心。

它会先读取:

runtime_state().config.rasp_action

然后根据配置决定:

Alert
Degrade
Block

而不是所有风险都无条件立即退出。

这说明当前实现已经从简单的:

发现风险 → crash

发展成:

检测 → 分类 → 报告 → 决策 → 执行响应

的完整流程。


四、第一层:Frida 检测

Frida 是 Android 动态分析中最常见的运行时工具之一。

因此 XopProtector 的 risk.cpp 专门设计了:

detect_frida()

但它并没有只检查一个指标。

当前源码至少组合了:

1. Frida 默认端口
2. /proc/self/maps
3. /proc/self/fd
4. /proc/net/unix
5. Frida 临时文件
6. Frida 线程名
7. Hook Framework
8. libc Inline Hook
9. Composite Score

也就是说:

XopProtector 不是单点 Frida 检测,而是多源信号融合。

源码中 detect_frida() 对这些信号进行组合判断。


五、Frida 检测一:默认 TCP 端口

Frida Server 在一些常见部署方式下会监听:

27042
27043

XopProtector 直接对:

127.0.0.1

进行 TCP Connect。

核心逻辑:

127.0.0.1:27042
127.0.0.1:27043
        │
        ▼
     connect()
        │
        ├── 成功 → 风险
        │
        └── 失败 → 继续检测

源码中:

static const uint16_t kPorts[] = {27042, 27043, 0};

并设置了较短的 Socket 超时。

这是一种典型的:

Network Indicator

但是它本身并不能作为唯一判断依据。

原因很简单:

Frida 可以改端口

甚至可以:

不使用 frida-server

而采用:

Frida Gadget

所以:

端口检测只能作为第一层信号。


六、Frida 检测二:扫描 /proc/self/maps

这是 Android RASP 中最经典的检测方式之一。

Linux/Android 进程可以通过:

/proc/self/maps

看到当前进程的内存映射。

例如:

地址范围       权限      文件
---------------------------------------------
7xxx-7xxx      r-xp      libc.so
7xxx-7xxx      r-xp      libart.so
7xxx-7xxx      r-xp      libfrida-gadget.so

如果存在:

frida
frida-agent
frida-gadget
gum-js
linjector

等特征,就可能意味着:

Frida / Gum

已经进入当前进程。

XopProtector 的 maps_has_frida() 就是这样做的。

源码中不仅检查:

frida

还检查:

gum-js
linjector
frida-agent
frida-gadget
re.frida.server

并且这些字符串部分通过 OBSC_DECODE() 在运行时恢复,降低直接通过 strings 搜索得到完整检测特征的概率。


七、为什么不能只检查 frida

因为真正的动态分析工具生态远远不止:

frida-server

攻击者还可能使用:

Frida Gadget
Xposed
LSPosed
Substrate
SandHook
Epic
YAHFA
Whale
InlineHook

所以 XopProtector 又增加了:

maps_has_hook_framework()

源码会检查多个 Native Hook Framework 的库名,例如:

libsubstrate
libsandhook
libepic.so
libyahfa
libwhale
liband64inlinehook
libhookzz
libxhook.so

以及:

libbhook.so

等特征。

因此:

Frida 检测
        +
Hook Framework 检测

形成第一层组合。


八、Frida 检测三:扫描 /proc/self/fd

很多运行时工具不仅会留下:

.so

还可能留下:

Unix Socket
Pipe
FD

因此 XopProtector 会遍历:

/proc/self/fd

流程:

/proc/self/fd
       ↓
readdir()
       ↓
/proc/self/fd/<fd>
       ↓
readlink()
       ↓
检查目标名称

源码中的:

frida_fd_hits()

就是这一逻辑。

它最终将一些 Frida 相关的 FD 特征转换成一个风险信号。

这一层的重要性在于:

即使某个恶意模块没有直接在 /proc/self/maps 中留下明显名称,文件描述符或管道也可能暴露运行时组件。


九、Frida 检测四:扫描 /proc/net/unix

Frida/Gum 在运行过程中可能创建 Unix Domain Socket。

因此源码又增加:

frida_unix_socket_hits()

扫描:

/proc/net/unix

寻找:

frida
gum-js

等特征。

流程:

/proc/net/unix
        ↓
读取 Unix Socket 列表
        ↓
搜索 Frida 特征
        ↓
风险信号

这又增加了一个独立观察面。

所以当前 Frida 检测已经从:

只看 maps

变成:

maps
 +
fd
 +
unix socket
 +
port

十、Frida 检测五:线程名

动态分析工具通常会创建自己的工作线程。

XopProtector 会遍历:

/proc/self/task

然后读取每个线程的:

comm

也就是:

/proc/self/task/<tid>/comm

源码重点匹配:

gum-js-loop
frida
pool-frida

等特征。

这一点很重要。

因为:

/proc/self/maps

解决的是:

“有没有可疑模块?”

而:

/proc/self/task

解决的是:

“有没有可疑运行线程?”

因此攻击面又增加了一层。


十一、为什么源码没有把 gmain / gdbus 当成单独风险?

源码里有一个非常值得注意的注释。

它明确指出:

不要匹配通用 GLib 线程名

例如:

gmain
gdbus

因为这些名字在:

MIUI
OEM
正常 Native 应用

中都可能出现。

如果无脑检测:

gmain

就会产生大量误报。

所以当前实现只保留更明显的:

gum-js-loop
frida
pool-frida

等标记。

这体现了 RASP 中非常重要的一个原则:

检测能力不是越激进越好,误报控制同样重要。


十二、第二层:Inline Hook 检测

这是 XopProtector RASP 中比简单 Frida 扫描更有价值的一层。

因为:

Frida

只是 Hook 的一种实现。

攻击者完全可能使用:

Dobby
ByteHook
ShadowHook
InlineHook
手写 trampoline

甚至:

自己修改机器指令

因此:

检测 Hook 的结果,比单纯检测 Frida 更具有通用性。


十三、XopProtector 如何检测 libc Inline Hook?

源码实现了:

libc_export_hooked()

首先通过:

dlsym(RTLD_DEFAULT, name)

拿到 libc 导出函数地址。

例如:

fopen
open
connect
read

然后读取:

uint32_t w = p[0];

也就是:

直接读取函数入口处的第一条机器指令。

对于 ARM64,源码检查:

B
BL
BRK

等指令模式。

如果一个正常 libc 函数入口:

原始:
STP
MOV
...

突然变成:

B target

那么就可能意味着:

Inline Hook

例如:

libc!open
   │
   ▼
B hook_function
   │
   ▼
攻击者代码

这就是典型的:

Inline Trampoline Detection

源码通过:

libc_hook_score()

分别检查:

fopen
open
connect
read

然后累计得分。


十四、为什么要检查多个 libc 函数?

因为只检查:

open()

太容易产生:

误报

或者:

单点绕过

因此 XopProtector 采用:

fopen()
open()
connect()
read()

多个入口。

例如:

fopen   → 正常
open    → 被 Hook
connect → 被 Hook
read    → 正常

那么:

hook_score = 2

达到阈值以后才触发风险。

源码当前阈值就是:

if (hook_score >= 2)

然后触发:

libc_inline_hook

风险。

这就是:

多点一致性判断。


十五、第三层:TracerPid 调试检测

Frida、GDB、LLDB、strace 等动态分析工具可能涉及:

ptrace

因此 Android/Linux 内核会在:

/proc/self/status

提供:

TracerPid:

例如:

TracerPid: 0

通常表示:

当前没有外部 tracer

如果:

TracerPid: 12345

则说明:

PID 12345

正在跟踪当前进程。

XopProtector 的:

detect_debugger()

会读取:

/proc/self/status

并解析:

TracerPid

源码还特意排除了:

getpid()
getppid()

相关情况,避免自身运行环境造成误判。


十六、为什么 TracerPid 也不能作为唯一检测?

因为:

TracerPid

只能回答:

“当前进程是否存在 ptrace tracer。”

但是:

Frida Gadget

等注入方式并不一定表现为传统的:

TracerPid != 0

所以 RASP 必须组合:

TracerPid
+
maps
+
threads
+
socket
+
inline hook

这也是现代 RASP 的基本设计思路。


十七、第四层:进程状态检测

XopProtector 又检查:

/proc/self/stat

中的:

state

如果状态变成:

t
T

则认为进程处于:

traced / stopped

状态。

源码中的:

detect_stat_trace()

就是专门做这一层检测。

于是调试检测形成:

/proc/self/status
        ↓
TracerPid

/proc/self/stat
        ↓
Process State

Timing
        ↓
Debugger Delay

三条通道。


十八、第五层:时间异常检测

这是当前 risk.cpp 中一个比较有意思的设计。

源码执行一个非常简单的循环:

2000 iterations

然后测量:

开始时间
   ↓
执行简单计算
   ↓
结束时间

如果:

耗时 > 30ms

则认为存在:

timing anomaly

源码注释明确指出:

调试器单步执行可能导致原本非常短的循环出现明显时间异常。

因此:

正常:

2000 iterations
≈ 极短时间

调试单步:

2000 iterations
≈ 明显延迟

这可以成为 Debugger 的旁路检测信号。


十九、为什么时间检测不直接立即 Crash?

这是当前源码非常值得注意的设计。

源码注释明确说明:

timing anomaly

会倾向于:

schedule_delayed_crash()

而不是立即:

crash

原因是:

如果检测到异常之后立刻崩溃,攻击者很容易定位“哪一段代码导致崩溃”,然后针对这一位置 Patch。

而延迟处理:

检测
 ↓
记录风险
 ↓
延迟
 ↓
后续统一处理

会增加:

定位

难度。

这属于:

Delayed Response

也是现代 RASP 中常见的反分析思路。


二十、XopProtector 不仅检测 Frida,还检测 Xposed / LSPosed

当前源码中有独立的:

detect_xposed()

它首先扫描:

/proc/self/maps

寻找:

xposed
XposedBridge
lsposed
LSPosed
edxposed
EdXposed

等特征。

另外还检查一些常见路径,例如:

/system/framework/XposedBridge.jar
/system/framework/lsposed
/data/local/tmp/xposed

源码采用的是:

maps
+
filesystem

双通道。

所以整个 Hook 防护已经不再局限于:

Frida

而是:

Frida
Xposed
LSPosed
EdXposed
Native Hook Framework
Inline Hook

多个生态一起覆盖。


二十一、Root / Magisk 检测

RASP 中另一个常见问题是:

如果设备已经 Root,攻击者拥有的控制能力会显著提高。

因此 XopProtector 还包含:

detect_root()

当前源码检查:

/system/bin/su
/sbin/magisk
/data/adb/magisk
/data/adb/modules
/system/xbin/su
/system/sbin/su
/vendor/bin/su
...

等路径。

源码同时通过:

OBSC_DECODE()

对部分路径进行混淆,避免直接通过 strings 获取完整检测字符串。

需要注意:

Root Detection 本质上是环境风险检测,不等价于“已经发现攻击”。

因此商业 RASP 通常会把它作为:

risk signal

而不是唯一证据。


二十二、模拟器检测

当前源码还存在:

detect_emulator()

它会检查:

/dev/socket/qemud
/dev/qemu_pipe
/system/lib/libc_malloc_debug_qemu.so
/sys/qemu_trace
/system/bin/qemu-props

等典型模拟器标记。

源码不是:

发现一个 → 立即判定

而是:

hit count

至少达到:

2

才认为模拟器概率较高。

这种:

多指标阈值

比单一:

Build.FINGERPRINT

更加稳健。


二十三、非常关键:SO 自身完整性保护

如果 RASP 只有:

Frida 检测

仍然存在一个非常明显的问题:

攻击者可以直接修改 RASP 自己。

例如:

detect_frida()
{
    return;
}

如果攻击者直接把:

risk.cpp

编译出来的机器代码 Patch 掉,那么所有外围检测都失去意义。

因此 XopProtector 又增加:

SO Self Guard

源码位于:

native/src/main/cpp/risk/so_guard.cpp

这个模块专门保护:

libprotector.so

自身。


二十四、SO Self Guard 第一层:.bitcode CRC

Native Shell 中有一个:

.bitcode

区域。

初始化时:

g_bitcode_crc =
    crc32_update(
        0,
        g_bitcode_addr,
        g_bitcode_size
    );

也就是:

启动时:

.bitcode
   ↓
CRC32
   ↓
保存基准值

之后:

运行时:

.bitcode
   ↓
再次 CRC32
   ↓
与启动基准值比较

如果:

CRC_now != CRC_initial

则说明:

自身代码可能发生修改

然后:

handle_risk(
    "so_bitcode_crc",
    ...
)

进入风险响应。


二十五、为什么 CRC 能检测 Inline Patch?

假设:

libprotector.so

原始代码:

AA BB CC DD EE FF

攻击者 Patch:

AA BB CC 00 EE FF

那么:

CRC(original)
        !=
CRC(patched)

因此:

so_bitcode_crc

会被触发。

所以:

Frida Detection

解决:

“有没有可疑工具?”

而:

SO CRC

解决:

“我的保护代码有没有被修改?”

二者是完全不同的两个维度。


二十六、SO Self Guard 第二层:RWX Mapping

现代 Native Hook 很多时候需要:

RWX

即:

Read
Write
Execute

同时存在。

XopProtector 会扫描:

/proc/self/maps

然后寻找:

W + X

同时存在的映射。

源码:

maps_rwx_on_self()

会特别关注:

libprotector

以及:

SO 附近的匿名 RWX Mapping

如果发现可疑情况:

so_rwx

风险就会被触发。


二十七、为什么“RWX”是一个重要信号?

正常 Native 代码通常是:

RX

数据通常:

RW

而:

RWX

意味着:

可写
+
可执行

这恰好是很多:

动态代码修改
Inline Hook
Trampoline
JIT

场景非常关注的权限组合。

因此:

RWX

可以作为一个重要的:

Memory Tampering Indicator

但它不能被绝对化。

因为某些正常组件:

JIT
游戏引擎
特殊 Runtime

也可能产生类似映射。

所以生产级 RASP 仍然需要:

RWX
+
模块来源
+
地址范围
+
线程
+
其他风险

综合判断。


二十八、SO Self Guard 第三层:Dump Tool 检测

源码还有:

maps_has_dump_tools()

检查:

memdump
libGameGuardian
frida-gadget
frida-agent
libdump.so

等工具特征。

也就是说:

Frida

不仅被放在:

Frida Detector

中检测,也被放在:

SO Guard

中作为另一条防线。

这种重复检测其实是有价值的。

因为:

攻击者如果只绕过一个 Detector,仍然可能被另一个 Detector 捕获。

这就是:

Defense in Depth

即:

纵深防御。

源码中的 so_guard_check() 会依次进行:

bitcode CRC
+
RWX
+
dump tool

检查。


二十九、一个非常重要的细节:MADV_DONTDUMP

so_guard_init() 在初始化 .bitcode 后还调用:

madvise(
    g_bitcode_addr,
    g_bitcode_size,
    MADV_DONTDUMP
);

也就是:

MADV_DONTDUMP

它的目的可以理解为:

告诉内核,在生成进程内存 dump 时,不要把这一段内存作为普通 dump 内容处理。

这与:

代码完整性保护

并不是同一个机制。

它解决的是:

Memory Dump Exposure

问题。

于是 SO 自保护实际上形成:

代码完整性
     +
内存映射检测
     +
Dump Tool 检测
     +
减少 Dump 暴露

四个方向。

源码明确使用 MADV_DONTDUMP 处理 .bitcode 区域。


三十、另一个非常关键的 Native 防护:PR_SET_DUMPABLE

初始化过程中源码还检查:

TracerPid

如果当前没有 tracer,则:

prctl(
    PR_SET_DUMPABLE,
    0
);

也就是说:

进程不可 dump

这是 Linux/Android Native 进程层面的一个重要保护开关。

逻辑可以理解为:

启动
 ↓
检查是否已有 tracer
 ↓
没有 tracer
 ↓
PR_SET_DUMPABLE = 0
 ↓
降低外部进程进行 dump / ptrace 类操作的能力

当然,这不是:

“设置之后就没人能攻击。”

Root、内核级能力、特权环境等仍然可能改变这个边界。

但对于普通用户态攻击:

Dump
Ptrace

它确实增加了成本。


三十一、Java 和 Native 之间还有一个“心跳机制”

这一点非常有意思。

RASP 不只监控:

攻击者

还监控:

自己的 Java Shell

源码维护:

g_last_heartbeat_ms

Java 层通过:

record_java_heartbeat()

定期刷新。

Native 层:

check_heartbeat()

检查最后一次心跳。

如果:

elapsed > 15000ms

则:

java_heartbeat

被视为异常。

源码会进入:

handle_risk()

处理。

这可以理解成:

Native → Java 双向完整性监督。


三十二、为什么需要 Java Heartbeat?

假设攻击者:

Hook Java Shell

让:

RASP Java 部分

完全停止运行。

如果 Native 完全不知道:

Java 层已经死了

那么:

RASP

就可能只剩下一半。

而 Heartbeat 机制可以变成:

Java Shell
    │
    │ heartbeat
    ▼
Native RASP
    │
    │ 15s 没收到
    ▼
风险

所以:

Heartbeat 本质上是一种跨层存活性检测。


三十三、RASP 不是启动时检测一次,而是持续检测

这一点非常重要。

很多简单的 APK 防护:

App 启动
 ↓
检测 Frida
 ↓
没发现
 ↓
继续运行

存在明显缺陷:

App 启动
 ↓
RASP 检查
 ↓
攻击者稍后注入

这样就绕过去了。

XopProtector 的源码使用:

risk_thread_main()

创建独立后台线程。

然后不断循环:

Frida
CRC
Debugger
SO Integrity
Heartbeat
Delayed Crash

源码当前周期是:

2~5 秒

并加入随机抖动:

sleep(2 + (rand() % 3));

因此不是固定:

10 秒一次

而是:

2s
3s
4s
5s
...

持续巡检。


三十四、为什么还要加入随机时间?

如果检测周期固定:

每 10 秒检查一次

攻击者可能通过:

精准时间控制

来:

检查前隐藏
检查后恢复

而:

2~5 秒随机

会增加这种时间同步攻击的难度。

所以:

周期检测
+
Jitter

形成:

非确定性检测窗口。


三十五、XopProtector 的 RASP 是“多源信号融合”

到这里可以把整个检测体系总结成:

RASP
                   │
       ┌───────────┼───────────┐
       │           │           │
     Frida       Debugger     Hook
       │           │           │
       ▼           ▼           ▼
     Port       TracerPid    libc入口
     Maps       stat         指令
     FD         timing
     Socket
     Thread
       │           │           │
       └───────────┼───────────┘
                   │
                   ▼
              SO Integrity
                   │
          ┌────────┼────────┐
          │        │        │
         CRC      RWX     Dump Tool
          │        │        │
          └────────┼────────┘
                   │
          ┌────────┼────────┐
          │        │        │
       Xposed     Root     Emulator
          │        │        │
          └────────┼────────┘
                   │
                   ▼
             Threat Report
                   │
                   ▼
             RASP Action

这已经明显不是:

if (frida)
    exit();

而是一个:

Runtime Threat Detection Engine


三十六、Composite Score:为什么不是所有弱信号都直接判定攻击?

XopProtector 的 detect_frida() 中还有一个很重要的设计:

score

例如:

Unix Socket hit      +2
/tmp/frida file      +1
Frida thread         +1
FD hit               +2
libc hook            +N

最终:

if (score >= 3)

才触发:

frida_composite

风险。

这种模型非常适合解决:

单个信号不可靠,多信号组合才可靠。

例如:

只有一个可疑线程名

可能误报。

但是:

可疑线程
+
Unix Socket
+
FD
+
libc Hook

同时出现时:

攻击概率明显提高

于是:

risk score

达到阈值。


三十七、这其实已经接近现代 RASP 的“风险评分模型”

可以抽象成:

Risk Score =
    W1 × FridaPort
  + W2 × MapsHit
  + W3 × ThreadHit
  + W4 × FDHit
  + W5 × SocketHit
  + W6 × HookScore
  + W7 × Debugger
  + W8 × Integrity

然后:

Score < T1
    ↓
正常

T1 ≤ Score < T2
    ↓
Alert

T2 ≤ Score < T3
    ↓
Degrade

Score ≥ T3
    ↓
Block

XopProtector 当前实现还没有把所有检测统一成一个单一总分数,但 detect_frida() 已经采用了局部 Composite Score,而整个系统又通过 RaspAction 对风险结果统一处理。

这其实已经具备:

风险引擎

的基本雏形。


三十八、发现风险以后,XopProtector 怎么处理?

这是当前版本非常值得关注的一点。

源码中的:

handle_risk()

会先:

report_threat()

然后根据:

rasp_action

决定行为。

当前有三种核心模式:

Alert
Degrade
Block

三十九、模式一:Alert

如果:

RaspAction::Alert

那么:

记录风险
+
打印日志

但是:

不 Crash

源码明确写了:

rasp alert: xxx (no crash)

这对于开发阶段、灰度阶段、兼容性验证非常有用。

例如:

发现某 ROM 存在误报

如果直接:

SIGILL

那么用户 App 就无法启动。

但:

Alert

可以:

记录
 ↓
上报
 ↓
分析
 ↓
调整规则

四十、模式二:Degrade

如果:

RaspAction::Degrade

则:

environment_degraded = true

也就是:

进入降级环境。

这并不意味着立即杀进程。

而是告诉整个保护体系:

当前运行环境已经不再可信

例如 PVM2 可以通过:

vmp_allowed()

检查:

environment_degraded

如果已经降级:

return false;

从而限制某些受保护能力继续运行。

所以:

RASP 和 PVM2 不是两个完全独立的模块,它们通过 Runtime State 联动。

这是整个 XopProtector 架构中非常重要的一点。


四十一、模式三:Block

如果:

RaspAction

进入阻断模式,则会根据不同场景选择:

SIGILL
SIGSEGV
SIGABRT
Hang
Exit

源码甚至刻意设计了多种 Crash Path:

crash_sigill()
crash_sigsegv()
crash_abort()
crash_hang()
crash_exit()

这样做的目的之一,是避免所有风险都依赖同一个:

exit()

路径。

因为如果攻击者只针对:

exit

做 Hook,那么其他路径仍然存在。

这也是一种:

Response Diversification

即:

响应路径多样化。

源码明确实现了这些不同的响应路径。


四十二、为什么有时候是 SIGILL,有时候是 SIGSEGV?

源码中不同检测点使用不同响应。

例如:

Frida Port
    ↓
SIGSEGV

libc Inline Hook
    ↓
SIGILL

libc CRC
    ↓
SIGILL

Xposed
    ↓
SIGSEGV

这种设计可以降低:

统一 Crash Signature

带来的可预测性。

从工程角度看,它也意味着:

检测点
+
响应方式

不是完全固定映射。


四十三、Threat Report:RASP 不只是“发现就退出”

XopProtector 还有:

report/threat_report.cpp

专门负责:

Threat Report

当前源码维护一个:

32

容量的 Ring Buffer:

static constexpr int kRingCap = 32;

每个事件包含:

reason
rasp_action
timestamp
pid
sdk

也就是:

发生了什么
采取什么策略
什么时候发生
哪个进程
哪个 Android SDK

这些信息。


四十四、Threat Report 的输出形式

源码会把 Threat Event 序列化为 JSON:

{
  "ts": 1234567890,
  "reason": "frida_maps",
  "rasp_action": 2,
  "pid": 12345,
  "sdk": 35
}

然后可以:

写入 threats.log

同时:

PLOGI()

输出日志。

另外还支持:

ThreatSink

回调。

因此:

Native Detector
        ↓
Threat Report
        ↓
本地日志
        +
Ring Buffer
        +
外部 Sink

可以形成一个完整的事件链。


四十五、为什么 Threat Report 很重要?

因为商业加固真正关心的不只是:

有没有攻击

还关心:

谁在攻击
什么攻击
什么时候攻击
什么 Android 版本
攻击频率
哪个版本最容易被攻击

例如后台可能最终统计:

Android 15
    ↓
frida_maps
    ↓
12,321 次

Android 16
    ↓
libc_inline_hook
    ↓
8,921 次

那么就可以:

更新规则

甚至:

针对特定版本

调整策略。

所以:

Threat Report 是把本地 RASP 从“检测器”升级为“安全遥测系统”的关键。


四十六、当前 RASP 的一个核心架构:Native 负责检测

为什么 XopProtector 把大量 RASP 放在:

C++

而不是:

Java/Kotlin

因为 Java 层太容易被:

Xposed
Frida Java Hook
LSPosed

直接修改。

例如:

boolean isFridaDetected() {
    return true;
}

攻击者只需要:

Hook
 ↓
return false

整个检测就失效。

而 Native:

risk.cpp
so_guard.cpp

可以直接读取:

/proc
ELF
memory
libc

因此增加了一层:

Native Detection Boundary

XopProtector README 也明确把 RASP 放在 Native Shell 中,而 Native 模块本身包含 hook、patch、PVM2、RASP 等运行时能力。


四十七、但是 Native 也不是绝对安全

这一点必须实事求是。

Native RASP 只能:

增加攻击成本。

不能:

让攻击者永远无法绕过。

因为如果攻击者拥有足够高的能力:

Root
+
Kernel
+
隐藏式 Hook
+
内存修改
+
系统级注入

仍然可能:

隐藏 maps
伪造 /proc
修改检测结果
Patch Native

所以现代 RASP 永远是一场:

攻防对抗

而不是:

一次性解决

公开的 Android RASP 项目也普遍采用类似思路:Debugger、Root、Hook、Frida、环境完整性等需要持续迭代,而且没有单一检测可以保证绝对准确。


四十八、因此真正高级的 RASP 不应该只依赖 /proc/self/maps

很多早期 Android Anti-Frida 方案基本都是:

open("/proc/self/maps")
        ↓
strstr("frida")
        ↓
发现

问题是:

单点

太容易被针对。

例如:

maps 被隐藏

之后:

Frida

仍然可能存在。

所以现代方案逐渐演化成:

Maps
+
TracerPid
+
Thread
+
Socket
+
FD
+
Inline Hook
+
Memory Integrity
+
Timing
+
Environment

多层组合。

XopProtector 当前 RASP 正是在往这个方向走。


四十九、XopProtector 的 RASP 和第五篇 SO 加密实际上是联动的

这一点非常关键。

第五篇中我们讲:

SO .text
 ↓
RC4
 ↓
Runtime Decrypt

但是:

运行时明文

一定存在。

于是攻击者可以:

等待解密
 ↓
Attach
 ↓
Dump

RASP 就在这里发挥作用:

SO 解密
   ↓
Native 明文代码
   ↓
RASP 持续检测
   ↓
发现 Hook / Dump / Patch
   ↓
阻断

因此:

第五篇:
保护“代码”

第六篇:
保护“代码运行时的环境”

两者结合以后才形成:

Native Runtime Protection


五十、RASP 与 PVM2 也存在直接联动

第四篇讲过:

PVM2

通过:

Native Interpreter

执行虚拟化指令。

如果攻击者:

Hook Interpreter

或者:

Patch VM Dispatch

那么 PVM2 本身也会受到影响。

XopProtector 当前的:

vmp_allowed()

会定期进行轻量 SO 完整性检查,并检查:

environment_degraded

如果环境已经进入:

degraded

状态:

return false;

也就是说:

RASP
 ↓
environment_degraded
 ↓
PVM2
 ↓
vmp_allowed()
 ↓
拒绝继续执行

这就是:

RASP Gate

即:

运行时安全状态作为虚拟机执行的前置条件。


五十一、整个 XopProtector 到这里已经形成一个闭环

前面的五篇如果单独看:

DEX 加密
SO 加密
PVM

似乎是互相独立的技术。

但把第六篇加入以后,就形成:

XopProtector
                         │
          ┌──────────────┼──────────────┐
          │              │              │
         DEX             VM            SO
          │              │              │
          ▼              ▼              ▼
       加密/恢复       PVM1/PVM2      .text 加密
          │              │              │
          └──────────────┼──────────────┘
                         │
                         ▼
                       RASP
                         │
        ┌────────────────┼────────────────┐
        │                │                │
      Frida            Hook           Debugger
        │                │                │
        ├────────────────┼────────────────┤
        │                │                │
      Xposed           Root           Emulator
        │                │                │
        └────────────────┼────────────────┘
                         │
                         ▼
                  SO Integrity
                         │
                         ▼
                  Threat Report
                         │
                         ▼
                   RaspAction
                         │
              ┌──────────┼──────────┐
              │          │          │
            Alert      Degrade     Block
              │          │          │
              │          ▼          ▼
              │       PVM Gate    Crash
              │
              ▼
           Continue

这已经不是单一的 APK 加固。

而是一套:

Runtime Protection Architecture


五十二、从攻击链角度重新理解整个系统

假设攻击者想分析一个经过 XopProtector 保护的 APK。

第一阶段:

解压 APK

发现:

DEX

已经受到保护。

于是:

继续分析 Shell

第二阶段:

PVM1 / PVM2

增加代码理解成本。

第三阶段:

Native SO

发现:

.text

被加密。

第四阶段:

运行 App

准备:

Frida

然后:

Frida
 ↓
maps
 ↓
thread
 ↓
fd
 ↓
socket
 ↓
port

可能被 RASP 发现。

即使进一步:

绕过 Frida 字符串

又可能遇到:

Inline Hook Detection

即使:

绕过 Hook Detection

又可能遇到:

libc CRC

即使:

修改 RASP

又可能遇到:

SO Self Guard

即使:

停止 Java RASP

还有:

Java Heartbeat

最终形成:

攻击成本
   ↑
   │
   │                         ┌──── RASP
   │                    ┌────┤
   │               ┌────┤    ├──── SO Guard
   │          ┌────┤    │    ├──── Heartbeat
   │     ┌────┤    │    │    ├──── Threat Report
   │ ┌───┤    │    │    │
   └─┴───┴────┴────┴────┴────────────→ 攻击阶段
     APK  DEX  VM   SO   Runtime

所以:

现代商业加固真正追求的是提高攻击链每一个阶段的成本。


五十三、RASP 最重要的不是“检测多少”,而是“检测纵深”

一个成熟 RASP 的思路不是:

检测 100 个字符串

而是:

不同攻击
    ↓
不同证据
    ↓
不同检测层
    ↓
不同响应方式

例如:

Frida
 ↓
Port
Maps
Thread
FD
Socket

Inline Hook:

函数入口
 ↓
机器码
 ↓
CRC

Debugger:

TracerPid
+
Process State
+
Timing

Native Patch:

.bitcode CRC

Memory Attack:

RWX
+
Dump Tool
+
DONTDUMP

Java 层失效:

Heartbeat

最终:

Threat Report

统一收敛。

这才是:

Defense in Depth

真正的含义。


五十四、XopProtector 当前 RASP 设计中最值得研究的几个点

从源码角度看,我认为当前版本最有价值的并不是某一个检测函数,而是下面几个架构设计。

1. 多源 Frida 检测

不是:

maps only

而是:

port
maps
fd
socket
thread
hook

组合判断。


2. libc Inline Hook 指令级检测

直接检查:

fopen
open
connect
read

的函数入口机器码。

这已经从:

工具检测

升级到:

行为痕迹检测

3. Native Shell 自保护

通过:

.bitcode CRC
RWX Detection
Dump Tool Detection
MADV_DONTDUMP
PR_SET_DUMPABLE

保护 RASP 自身。


4. RASP 与 PVM2 联动

通过:

environment_degraded

影响:

vmp_allowed()

使 RASP 不再只是旁路报警器,而是能够影响保护代码的执行状态。


5. Threat Report

将:

检测

变成:

安全事件

并提供:

Ring Buffer
+
日志
+
JSON
+
Sink

等输出能力。


五十五、它的局限性是什么?

源码分析必须同时看到它的边界。

第一:

字符串检测

仍然存在。

所以对于经过充分隐藏的:

Frida
Gadget
Hook Framework

识别能力会下降。

第二:

/proc/self/maps

本身是用户态攻击者经常关注的目标。

第三:

libc inline hook

属于启发式检测。

因为合法的:

系统差异
厂商修改
特殊运行时

理论上也可能产生异常指令模式。

第四:

CRC

如果基准值本身被攻击者控制,也存在完整性问题。

第五:

Root / Emulator Detection

本质上都存在误报和漏报。

因此:

RASP 不应该被理解为一个“绝对正确的安全判断器”,而应该被理解为一个不断积累证据的运行时风险引擎。

公开 RASP 项目也普遍强调:Root、Emulator、Hook、Debugger 等检测都存在攻防演进,不能把某一种检查理解成绝对可靠的结论。


五十六、商业级 RASP 为什么越来越强调“多信号”

假设:

Frida Port = false
Maps Frida = false
Thread = false
TracerPid = 0

单看:

风险 = 0

但如果:

libc open() 被修改
libc read() 被修改
libc connect() 被修改

那么:

风险

显然已经很高。

因此现代 RASP 更合理的模型是:

Evidence
                   │
       ┌───────────┼───────────┐
       │           │           │
     Weak         Medium       Strong
       │           │           │
     port         thread      CRC mismatch
     path         socket      inline patch
     file         maps        tracer
       │           │           │
       └───────────┼───────────┘
                   ▼
              Risk Engine
                   │
                   ▼
             Policy Decision

也就是说:

RASP 的本质不是“检测某一个东西”,而是“判断当前执行环境是否可信”。


五十七、这也是 RASP 与传统 Anti-Debug 的根本区别

传统 Anti-Debug:

检测 Debugger
 ↓
发现
 ↓
退出

现代 RASP:

Debugger
Hook
Frida
Root
Xposed
Tamper
Memory
Environment
Integrity
       ↓
Evidence
       ↓
Risk
       ↓
Policy
       ↓
Alert / Degrade / Block

所以:

Anti-Debug 只是 RASP 的一个子集。

真正完整的 RASP 应该同时解决:

环境安全
代码完整性
运行时完整性
动态注入
Hook
调试
内存攻击
策略响应
安全遥测

五十八、把第六篇和前五篇串起来

到这里,整个系列已经可以完整串起来了。

第一层:DEX 保护

DEX
 ↓
加密
 ↓
隐藏
 ↓
Runtime Restore

解决:

APK 静态分析。


第二层:DEX 明文窗口控制

加密 DEX
 ↓
按需恢复
 ↓
ART
 ↓
尽快回收明文

解决:

DEX 明文暴露时间。


第三层:PVM1

方法
 ↓
抽取
 ↓
独立保护
 ↓
运行时恢复

解决:

方法级静态分析。


第四层:PVM2

Native Code
 ↓
Virtual Instruction
 ↓
Native Interpreter

解决:

原始机器码语义直接暴露。


第五层:SO .text

Native SO
 ↓
.text RC4
 ↓
APK 中密文
 ↓
Runtime Decrypt

解决:

Native 静态反汇编。


第六层:RASP

Runtime
 ↓
Frida
Hook
Debugger
Xposed
Root
Patch
Dump
Tamper
 ↓
Detection
 ↓
Threat Report
 ↓
Policy

解决:

运行时攻击。


五十九、最终形成“静态 + 动态 + 运行时”的三维保护体系

如果把整个 XopProtector 用一句架构语言概括:

Android APK
                     │
          ┌──────────┼──────────┐
          │          │          │
        DEX          VM         SO
          │          │          │
          ▼          ▼          ▼
       Encryption   PVM1/PVM2   .text Crypto
          │          │          │
          └──────────┼──────────┘
                     │
                     ▼
                Native Shell
                     │
                     ▼
                    RASP
                     │
       ┌─────────────┼─────────────┐
       │             │             │
     Detect        Protect       Report
       │             │             │
       ▼             ▼             ▼
     Frida        SO Guard      Threat Log
     Hook         CRC           JSON
     Debug        RWX           Sink
     Xposed       DONTDUMP
     Root         Dump Guard
       │
       ▼
   Risk Decision
       │
 ┌─────┼─────┐
 ▼     ▼     ▼
Alert Degrade Block

因此:

XopProtector 的核心并不是某一个“加密算法”,而是一套从 APK 静态文件一直延伸到进程运行时的纵深防御体系。


六十、第五篇与第六篇之间最重要的关系

第五篇我们讲的是:

如何把 Native 代码藏起来。

第六篇讲的是:

当 Native 代码已经在内存中恢复以后,如何保护它所处的运行环境。

所以:

第五篇:

磁盘
 ↓
加密
 ↓
隐藏代码

而:

第六篇:

内存
 ↓
检测攻击
 ↓
保护代码

两个技术结合:

Native Code
               │
       ┌───────┴───────┐
       │               │
      磁盘             内存
       │               │
       ▼               ▼
    .text 加密        RASP
       │               │
       ▼               ▼
   防静态分析       防运行时攻击

这才是真正完整的:

Native Runtime Protection。


六十一、总结:RASP 真正保护的是什么?

很多人第一次接触 RASP,会认为:

RASP 就是检测 Frida。

实际上完全不是。

从 XopProtector 当前源码来看,RASP 至少包含:

① Frida Detection
② Hook Framework Detection
③ Inline Hook Detection
④ Debugger Detection
⑤ TracerPid Detection
⑥ Timing Anomaly Detection
⑦ Xposed / LSPosed Detection
⑧ Root / Magisk Detection
⑨ Emulator Detection
⑩ Native SO Integrity
⑪ .bitcode CRC
⑫ RWX Mapping Detection
⑬ Dump Tool Detection
⑭ MADV_DONTDUMP
⑮ PR_SET_DUMPABLE
⑯ Java Heartbeat
⑰ Background Periodic Scan
⑱ Threat Report
⑲ Alert / Degrade / Block
⑳ PVM2 Security Gate

因此,如果一定要用一句话总结:

XopProtector 的 RASP 本质上是一个运行在 Native Shell 中的持续性运行时风险检测与响应系统,它通过 Frida/Hook/Debugger/环境/完整性等多维信号判断当前进程是否处于可信状态,再通过 Alert、Degrade、Block 等策略决定应用应该继续运行、降低保护能力还是终止执行。


六十二、真正理解 XopProtector,需要理解“攻击成本”而不是“不可破解”

到了第六篇,其实整个系列的核心思想已经非常清楚。

没有任何:

DEX Encryption
SO Encryption
PVM
RASP

可以做到:

绝对不可破解。

因为:

程序最终必须运行

只要运行:

CPU

最终就一定会看到:

指令
数据
Key
状态

所以真正的目标是:

普通 APK:

解压
 ↓
反编译
 ↓
看到代码


XopProtector:

解压
 ↓
DEX 密文
 ↓
分析 Shell
 ↓
PVM
 ↓
SO 密文
 ↓
启动 App
 ↓
RASP
 ↓
检测 Hook
 ↓
检测 Debug
 ↓
检测 Patch
 ↓
检测 Dump
 ↓
Threat Report
 ↓
Risk Policy

攻击者需要从:

一次静态分析

变成:

静态分析
+
Native 分析
+
动态调试
+
运行时跟踪
+
环境分析
+
RASP 对抗

最终实现的并不是:

“让攻击者永远无法破解。”

而是:

让破解从一个简单的文件分析问题,变成一个需要同时理解 DEX、ART、ELF、Native Runtime、VM Interpreter、Hook Framework、进程内存和安全检测机制的综合工程。

这才是商业 Android APK 加固真正的价值。


六十三、系列总结预告

到这里,前六篇已经分别完成:

第一篇
Native Shell
DEX 隐藏与恢复

第二篇
DEX
明文窗口控制

第三篇
PVM1
方法级代码抽取

第四篇
PVM2
Native Interpreter 虚拟化

第五篇
SO
.text 加密与运行时解密

第六篇
RASP
运行时攻击检测与响应

实际上已经可以把 XopProtector 总结成:

XopProtector
                       │
       ┌───────────────┼───────────────┐
       │               │               │
      静态             代码            运行时
       │               │               │
       ▼               ▼               ▼
   DEX/SO Crypto     PVM1/PVM2       RASP
       │               │               │
       └───────────────┼───────────────┘
                       │
                       ▼
                 Native Shell
                       │
                       ▼
                 Runtime Security

因此第七篇就非常适合作为整个系列的最终总结:

《从 APK 加密到代码虚拟化:XopProtector 多层 Android 应用保护体系解析》

第七篇不再单独讲某一个源码模块,而是把前六篇重新组合成一个完整的:

APK
 ↓
DEX Protection
 ↓
Plaintext Window Control
 ↓
PVM1
 ↓
PVM2
 ↓
SO .text Protection
 ↓
Runtime Materialization
 ↓
RASP
 ↓
Threat Detection
 ↓
Risk Response

最终从架构层面解释:

为什么现代 Android APK 加固必须从“文件保护”走向“代码保护 + 执行保护 + 运行时环境保护”的多层纵深防御。


源码依据

本文主要依据 XopProtector 当前仓库的 Native RASP 实现进行分析:

  • native/src/main/cpp/risk/risk.cpp:Frida、Hook、Debugger、Xposed、Root、Emulator、Timing、CRC、Heartbeat、后台风险线程等核心逻辑。
  • native/src/main/cpp/risk/so_guard.cpp.bitcode CRC、RWX 检测、Dump Tool 检测、MADV_DONTDUMPPR_SET_DUMPABLE 等 Native Shell 自保护机制。
  • native/src/main/cpp/report/threat_report.cpp:Threat Event、Ring Buffer、JSON、日志以及 Threat Sink。
  • native/src/main/cpp/hook/hooks.cpp:ByteHook、ART Hook、mmap/mmap64 Hook、execve Hook 等运行时 Hook 基础设施。
  • XopProtector README:项目整体架构以及 RASP、Frida/Hook 扫描、Threat Report、PVM2 等能力说明。

至此,Android APK 加固原理系列的六个核心技术模块已经全部串起来了。

下一篇就是最终总结篇:

《从 APK 加密到代码虚拟化:XopProtector 多层 Android 应用保护体系解析》

这一篇最适合做整个系列的​总纲文章​​:不再按源码文件逐个分析,而是把 DEX 加密 → ART 加载 → PVM1 → PVM2 → SO .text 加密 → Runtime Loader → RASP → Threat Response 统一起来,形成一张完整的 XopProtector 技术架构图。