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:.bitcodeCRC、RWX 检测、Dump Tool 检测、MADV_DONTDUMP、PR_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/mmap64Hook、execveHook 等运行时 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 技术架构图。