“Sentry” 反调试和环境检测的一个开源项目
“Sentry” 反调试和环境检测的一个开源项目

文章篇幅有限,项目开源地址:
taisuii/sentry: frida检测
项目本身不是什么新东西,算是把一些检测什么的综合一下吧
这里抛出一个问题:
大家觉得APP的风控中,检测到用户在注入和调试风控系数高,还是手机越狱或者root环境系数高呢?
显然是从注入和调试风控系数高,因为有很多用户的手机可能是二手或者是搞基佬,不一定root的设备或者解了BL的就是黑灰产,反而喜欢调试和注入的用来RPC的,更容易被评定为灰产机器
这里主要是强调尽量使用syscall,以及多重验证,形成证据链去风控
-
敏感检测下沉到 Native,有利于对抗「只改 Java 层」类绕过。
-
syscall → libc 回退:在受限机型上仍能读数据,兼顾对抗强度与兼容性。
-
syscall 优先(读 /proc、套接字等):减少对 libc 常规导出符号的依赖,降低 Frida/Xposed 一刀切 Hook libc 导致整类检测失效的概率。
不把结论绑在单一 API 上,例如:
-
Maps:Native 解析 + Java Runtime.exec(“cat /proc/…/maps”),对抗只 patch 一条读路径。
-
ADB:Native(端口、/proc/net/tcp、adbd、sysfs)+ Java Settings + getprop/settings get 等 exec 兜底。
-
Bootloader:系统属性 + Key Attestation(RootOfTrust 等)组合。
-
Xposed/Hook:Java(类、堆栈、反射、ClassLoader)与 Native(路径/fd、inline/PLT·GOT、匿名 r-x、ARM64 LR 等)组合。
0x1. 关于frida/zygisk注入的检测
较为基本的检测
端口,字符串特征,内存
| 能力 | 技术要点 | 主要实现 |
|---|---|---|
| Frida 线程 | 遍历 /proc/self/task/*/comm,匹配 gmain、gdbus、frida-agent、gum-js-loop 等关键词 | thread_detector.cpp |
| Frida / 调试端口 | ① connect 探测默认端口 27042(Frida)、23946(IDA android_server);② 读 /proc/net/tcp 解析 LISTEN(0A) 行,匹配 :699A / :5D8A;③ Frida 16+ 随机端口:找 comm 含 frida-server 的进程,再读其 /proc//net/tcp 是否含监听;④ 扫描 /proc/*/comm 中的 re.frida、frida-server;⑤ 对 127.0.0.1 上 LISTEN 端口发 D-Bus AUTH,若响应含 REJECT 则高度疑似 frida-server | port_scanner.cpp |
| 内存 / maps 签名 | 在 maps 等映射信息中匹配 frida、frida-java-bridge、QuickJS、linjector 等;并对匿名可执行段做大小门限 + ARM64 LDR X16/X17,[PC]; BR X16/X17)类 trampoline 扫描(多命中才报以降低误报);advancedChecks 可收紧匿名段阈值 | memory_scanner.cpp,Java 侧 maps 二次通道在 DebugDetectionManager |
另外的一些(重要)
如果单独对这些字符串做检测,依然可能会被魔改的frida绕过,那么还有一些较强的
SO 代码段完整性(nativeGetSoIntegrityResult)
组合 libc .text 与磁盘 CRC、GOT 劫持、27042 端口预检、匿名可疑 r-x、关键函数首部 Frida 典型 inline 模式 等,native-lib.cpp 里汇总为一条「Frida/SO 异常」类结论。
不是单纯 CRC:还叠了 GOT、可疑匿名可执行段、关键函数头模式、端口预检等,能单列成“组合完整性检测”小节(so_integrity.cpp、native-lib.cpp)
ArtMethod(nativeGetArtMethodCheckResult)
看 Activity.onCreate 等方法的 entry_point 是否落在由 maps 汇总的「合法可执行区」外;典型 Frida Java Hook 常把入口指到 libart/oat 外的可执行岛。
内存脏页,为什么「脏页」和调试/注入有关
系统库(如 libc.so)的代码段一般是文件映射、多进程只读共享。
若用 inline hook 之类方式改机器码,内核会对这一页做 COW(写时复制):本进程拿到一份私有可写副本,改的是「自己的那份」。
这类页在统计上常表现为:
-
/proc/self/smaps 里该映射区段的 Private_Dirty 大于 0(有私有且被改过的脏数据)。
-
某些情况下还可配合 /proc/self/pagemap 里与「近期是否被写过」相关的 soft-dirty(位 55) 看是否像发生过写入/COW 链路。
因此:「只读共享的正版代码段」长期出现明显私有脏页,和「有人动过这块代码」在经验上高度相关;项目把它用在 Frida 全局 hook / Zygisk 类注入 等会动到 libc/libart 等场景上。
通道一:/proc/self/smaps 里的 Private_Dirty
原理:逐行读 smaps,先根据映射行判断是否落在 r-xp / r-x(可执行) 区,再在同一区段后续的统计行里找 Private_Dirty:。若 dirty_kb > 0,且映射落在可疑路径(路径里带 .so 或 libart/libc/libselinux/libandroid_runtime 等),且不在白名单(如 code_cache、libstagefright.so),就记一条告警。
/* Smaps 检测:可执行段中 Private_Dirty > 0 的可疑注入(正常代码段不应有 Private_Dirty)。先 syscall 再 libc 打开/读。 */
static int detect_private_dirty_in_smaps(char (*details)[256], int max_details) {
// ...
/* 匹配内存映射行(权限包含 r-xp 或 r-x) */
if (my_strstr(line, "r-xp") != nullptr || my_strstr(line, "r-x") != nullptr) {
in_executable = true;
my_strncpy(current_mapping, line, 383);
// ...
}
/* 在可执行段中查找 Private_Dirty */
if (in_executable && my_strstr(line, "Private_Dirty:") != nullptr) {
int dirty_kb = 0;
if (sscanf(line, "Private_Dirty: %d kB", &dirty_kb) >= 1 ||
sscanf(line, "Private_Dirty: %d KB", &dirty_kb) >= 1) {
if (dirty_kb > 0 && is_suspicious_so_mapping(current_mapping) && !is_smaps_whitelisted(current_mapping)) {
// ... 记入 details
n++;
}
}
in_executable = false;
}
注意:smaps 是内核给出的会计信息,不是应用自己瞎算;但仍需白名单压误报(JIT、多媒体库等也可能在可执行映射上出现非零 Private_Dirty)。
通道二:/proc/self/pagemap 的 bit 55(soft-dirty)
原理:对给定虚拟地址,算出所在页号,在 pagemap 里读 8 字节项,检查:
-
bit 63:页是否在物理上 present(项目里叫 PAGEMAP_BIT_PRESENT)。
-
bit 55:soft-dirty(项目里注释写明用于「COW/曾修改」类指纹)。
代码注释里强调:不要用错 bit(例如注释里说 bit 61 对文件映射的 libc 会恒为 1,误用会误报),所以实现里显式用的是 bit 55。
/* Pagemap bit 55 (soft-dirty) 检测:内核管理,用户态无法伪造
* Frida inline hook 触发 COW 后,页面的 soft-dirty 置位,即使还原字节码也不会清除
* 注意:bit 61 是 file-page/shared-anon,libc 为文件映射故 bit 61 恒为 1,误用会导致误报 */
#define PAGEMAP_BIT_SOFT_DIRTY 55
// ...
static bool check_pagemap_soft_dirty(void *vaddr, char *detail_out, size_t detail_len) {
// ... my_open("/proc/self/pagemap") ... my_lseek ... my_read 8-byte entry
bool present = (entry >> PAGEMAP_BIT_PRESENT) & 1;
bool soft_dirty = (entry >> PAGEMAP_BIT_SOFT_DIRTY) & 1;
// ...
return (present && soft_dirty);
}
「精准」用法:不是全进程扫 pagemap,而是用 dlsym 取 libc 里 fork / vfork / signal 的地址,只查这几个常被 Frida/注入链 hook 的入口所在页:
/* 检测 libc 关键函数(fork/vfork/signal)所在页是否 soft-dirty,精准打击 Frida */
static int detect_pagemap_libc_hooks(char (*details)[256], int max_details) {
void *libc = dlopen("libc.so", RTLD_NOW);
const char *names[] = { "fork", "vfork", "signal" };
// dlsym → check_pagemap_soft_dirty(addr)
和 VMap 一起算不算「脏页」?
严格说 VMap 那一段不是脏页检测:它在 /proc/self/maps 里找「匿名 + 可执行」段,再在内存里 memmem 搜 Zygisk 特征串。它和 smaps / pagemap 并列,同属 env_detect_zygisk_injection,所以里常写成 「Smaps 脏页 + VMap + Pagemap」。
int env_detect_zygisk_injection(char (*details)[256], int max_details) {
int n = 0;
n = detect_private_dirty_in_smaps(details, max_details);
if (n < max_details) {
int vmap_n = scan_maps_for_zygisk_signatures(details + n, max_details - n);
n += vmap_n;
}
if (n < max_details) {
int pagemap_n = detect_pagemap_libc_hooks(details + n, max_details - n);
n += pagemap_n;
}
return n;
}
边界:脏页类检测会受 ROM/内核版本、厂商、调试开关、合法 JIT/code_cache 等影响,所以实现里用 白名单 + 限定关键库/关键符号,并和 maps 特征一起做,而不是单一指标定罪。
还有一些其他层面上的,就不一一细说
比如:
自定义 handler + 发信号验证信号链路是否被劫持
Ptrace/附加检测:TracerPid + PTRACE_TRACEME
反射关键方法探测、ClassLoader 异常实例检测(LSPosed/InMemoryClassLoader 等)
0x2. root环境检测
偏基础、相对容易绕过
这一层适合「低成本扫一眼」,不能当最终结论。
特点:看「常见特征」:装没装、路径在不在、Java API 读到的包名。对抗方常用隐藏应用、卸载管理端、卸载列表、挂载/命名空间藏目录、或 Hook PackageManager / File / access
Java:PackageManager + 标准 meta-data
对已安装包做 getInstalledPackages(PackageManager.GET_META_DATA),看 ApplicationInfo.metaData 里是否声明 Xposed 模块常见键:
/** 检查 meta-data 是否声明为 Xposed 模块(xposedmodule / xposed_module) */
private static boolean isXposedModule(Bundle metaData) {
if (metaData == null) return false;
Object v1 = metaData.get("xposedmodule");
Object v2 = metaData.get("xposed_module");
return isTruthy(v1) || isTruthy(v2);
}
容器/虚拟化:/proc/1/cgroup
用 open_with_fallback / read_with_fallback(先 syscall 再 libc)读 cgroup,匹配 lxc / docker / kubepods 等,对应云手机、K8s Pod 一类环境,和传统 Root 检测互补。
Key Attestation / TEE RootOfTrust(现代化的一个检测方式)
这个检测,走硬件信任根,不能像改文件一样随便伪造;限制在设备/ROM/谷歌服务/证书链是否可用、以及服务端是否校验(你们侧主要是本地解析)
detectBootloader() 在 Native 读 AVB/Verified Boot 相关系统属性(如 verifiedbootstate、vbmeta.device_state、veritymode 等)之外
read_prop("ro.boot.verifiedbootstate", state, sizeof(state));
read_prop("ro.boot.flash.locked", flash_locked, sizeof(flash_locked));
read_prop("ro.boot.veritymode", verity_mode, sizeof(verity_mode));
read_prop("ro.boot.warranty_bit", warranty_bit, sizeof(warranty_bit));
read_prop("ro.boot.avb_version", avb_version, sizeof(avb_version));
read_prop("ro.boot.vbmeta.device_state", vbmeta_state, sizeof(vbmeta_state));
read_prop("sys.oem_unlock_allowed", oem_unlock, sizeof(oem_unlock));
在 Java 侧用 KeyAttestationHelper 生成本地证明链,解析扩展 OID 1.3.6.1.4.1.11129.2.1.17 里的 RootOfTrust(deviceLocked、verifiedBootState 等)。
不依赖联网/Google API,属于近年风控里常见的 TEE/StrongBox 硬件证明路线,比普通读 Build 可信得多。
0x3 沙盒逃逸与CVE漏洞利用
不讲武德的检测方式
ios:
-
TrollStore 利用 CoreTrust 签名 bug 永久安装 IPA,不需完整越狱,但会留下 bundle ID(如 com.opa334.TrollStore)或异常 entitlements
SBSLaunchApplicationWithIdentifier (不符合苹果应用商店的协议,被曝过) -
CVE-2025-24090 允许恶意 app 枚举用户已安装的 app 列表,这是权限问题,已在 iOS 18.3 和 iPadOS 18.3 中修复。
安卓:
-
CVE-2024-43093 允许用户未经授权访问或修改Android/data、Android/obb和Android/sandbox目录及其子目录,未经用户许可的情况下列出安装在用户手机上的应用包名称
-
CVE-2026-0026 (EoP - Framework) 权限提升漏洞,破坏Android沙箱信任模型,允许系统级访问敏感文件
-
CVE-2024-48336 (Magisk特化) Zimperium zLabs报告,Magisk通过GMS服务伪装提权。可以尝试利用漏洞反向验证Magisk进程伪装失败迹象,或检查KernelSU 0.5.7认证绕过痕迹。
-
CVE-2026-0047 (EoP - Framework) 可用户权限提升到系统权限,访问关键系统目录。
老生常谈的一个环境检测和反调试话题了,而且我认为未来的APP可能大多都倾向于风控,而且会越来越难,而且由于AI的加持,可能攻破一个APP的难点不再是纯算还原,而是风控
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)