论坛首页 安全工具分享区 阅读主题

[原创]RiskEngine 开源设备指纹和风险监测SDK

86 浏览 13 回复
#1 楼主 2026-06-01 21:09:13
RiskEngine 是我开源在 GitHub 上的一个 Android 端设备指纹采集 + 风险检测 SDK。Java + C++17 双层结构,纯离线,进 App 之后调一次 RiskEngine.collect() 拿一份 RiskReport。整篇按"招式"排,一招一招拆。Frida 检测占一半多的篇幅,是整个 SDK 最重的部分,按"对抗演化"的层次从入门级一路讲到内存级。代码组织上分两层:入口长这样:接的人不用关心内部细节,等回调就行。但要看安全设计,得看回调背后的逻辑。代码盘点:12 个 Detector(root、hook、模拟器、调试、mount、ADB、进程扫描、沙箱、云手机、自定义 ROM、方法完整性等),十多个 Collector(android_id、build props、telephony、wifi、bluetooth、签名、屏幕、容器信号等)。Native 那边还有 5 个原生检测器和若干原生采集器。采集层定下的第一条原则:单源采集顶多算"原始数据",做不了"指纹"。Android ID 这种东西,绝大多数人一行就完事:放在风控里这就是个一行就能 hook 掉的"假指纹"——一段 Frida 脚本:设备指纹工作直接归零。collector/java_layer/AndroidIdCollector.java 里同一个 Android ID 从 4 个独立路径各读一遍:四条路:四路读到的值丢同一个 CollectorResult,由 core/DataAggregator.java 比对一致性。DataAggregator 第 27 行起:任意两路不一致直接合成一个 multi_source_validation 的 HIGH 级检测项。这个设计的关键不在每条单路读到了什么,而在让攻击方同时维护四条路径的一致性。hook 一个静态 Java 方法,一行 Frida 就够。要让四条路全部返回"一致的伪造值",要做的事是:第四条命令行通道,要拦只能 root 之后 hook 整个 system_server 改 settings provider,或者拦 shell 调用本身,工作量级跳一档。加这一路就是冲着"hook 不到的同进程外路径"来的。讲完 Java 层多源,再看 native 层。Frida 在 Android 上的入侵姿势,一大半都是 hook libc 的几个常用函数:open openat read fopen fgets pread。原因很简单——绝大部分检测代码(不管是 Java 的 FileReader 还是 C 的 fopen)底层都会落到 libc,hook 一个就能拦一片。cpp/util/syscall_wrapper.cpp 里直接走 raw syscall:syscall(__NR_openat, ...) 不走 libc 的 openat 包装函数,直接通过 syscall 这个汇编入口(ARM64 上是 svc #0 指令)陷入内核。Frida 默认 hook 的是 libc 的 openat 符号,syscall 路径完全绕开它。如果攻击方只是 Interceptor.attach(Module.findExportByName("libc.so", "openat"), ...) 这种常规姿势,对 native 检测路径完全失效。要绕开这条得搞内核态 hook(kprobe / sys_call_table 改写),需要 root + 内核级访问;或者扫指令找到所有 svc #0 全部插桩,技术上能做,Frida 默认不干。工作量级再跳一档。syscall_wrapper.cpp 底下还封装了一个 read_file_content,把 openat + read + close 包成一个函数,几乎所有 native 检测器读 proc 文件都走它。这部分是 RiskEngine 最重的一块,单独放出来讲。这一块设计的时候有个明确的层次:从最入门的字符串扫描到最高级的内存检测,每一层都是独立的检测维度,单独看都可能被绕掉,但堆在一起就强迫攻击方在所有维度同时绕过。每层按"常规做法 + 容易被绕的姿势 + RiskEngine 怎么做"展开。讲检测前先讲对手怎么动手。Frida 在 Android 上有两种主要落地方式:frida-server 模式:电脑 PC 通过 USB / TCP 连一个跑在手机上的 frida-server,server 默认 27042 listen,跟客户端用 D-Bus over TCP 通信。需要 root,server 要 ptrace 目标进程。frida-gadget 模式:把 libfrida-gadget.so 重打包进目标 APK,进程一启动 System.loadLibrary 把 gadget 装载进自己进程,gadget 自己跑一个 listener 等远程脚本连过来。不需要 root。不管哪种,落到目标进程内一定会留下三类痕迹:下面六层检测顺着这三个维度排,越往上越靠近"物理痕迹"那一边。入门三招:扫字符串、扫线程名、扫端口。这三招实现简单、成本极低、单独看都能被绕掉。先把这三招过一遍,再讲为什么留着。扫字符串。开 /proc/self/maps,看里面有没有 frida 相关字串。HookFrameworkDetector.java 第 96 行起:maps 一行行读,全转小写,看里面有没有 frida 或 gadget。绕这层有两种姿势:把 libfrida-gadget.so 改名成 libutils_v2.so 重打包进 APK,maps 里再没 frida 字样了;或者 hook open/openat,让读 /proc/self/maps 时返回伪造内容。第二条 Java 路径走 FileReader 最终落到 libc 不抗 hook,但 native 路径走 raw syscall 不在此列。扫线程名。Frida runtime 起来后会在进程里创建带特征名的线程:gum-js-loop、gmain、pool-frida、gdbus。这些是 GLib 和 GUM 库内部线程,启动时就 setname 了。HookFrameworkDetector.java 第 116 行起:遍历 /proc/self/task/,每个子目录是一个线程的目录,里面 comm 文件存线程名。/proc/[pid]/comm 和 /proc/[pid]/task/[tid]/comm 不是同一个东西。前者是进程主线程的名字,后者是某个具体线程的名字。Frida 是开新线程,主线程名字它没改,所以必须遍历 task 子目录。绕这一层的姿势:在 Frida 启动时 hook prctl(PR_SET_NAME) 把线程名替换;或者拿 Frida 源码改名重新编译,把 gum-js-loop 改成 worker-x;又或者拦截 /proc/self/task 的目录读取(hook getdents64),过滤掉 frida 的 tid——native 走 raw getdents

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290994.htm
#2 2026-06-01 21:09:13
秒吃,我来试试
#3 2026-06-01 21:09:13
感谢分享
#4 2026-06-01 21:09:13
感谢分享
#5 2026-06-01 21:09:13
这种叫检测 离真正的风控还有距离
#6 2026-06-01 21:09:13
qqqiu

这种叫检测 离真正的风控还有距离
写着写着就主要偏向于检测了,本来还有风控的服务端我删掉了,感兴趣可以翻翻GitHub仓库的历史提交
#7 2026-06-01 21:09:13
感谢分享
#8 2026-06-01 21:09:13
tql
#9 2026-06-01 21:09:13
温泉划水鱼

秒吃,我来试试
感谢支持
#10 2026-06-01 21:09:13
看看
#11 2026-06-01 21:09:13
/proc/[pid]/net/tcp 和 /proc/[pid]/net/tcp6 这两个目录用户app没有权限,哥,
#12 2026-06-01 21:09:13
感谢分享
#13 2026-06-01 21:09:13
感谢分享
#14 2026-06-01 21:09:13
感谢分享

请登录后参与讨论

立即登录 注册账号