首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
1
3
[原创]从死亡现象反推检测点:一次检测链路定位方法论
发表于: 2026-9-3 11:39
362
[原创]从死亡现象反推检测点:一次检测链路定位方法论
permanent011
2026-9-3 11:39
362
### 前言 > 本文主要记录实习期间在 Frida 反检测分析过程中的一些思路与实践经验。文章不会涉及具体游戏、厂商或敏感实现细节,更多是对分析流程、定位方法以及个人理解的阶段性总结。 在分析加固 Native 检测逻辑时,最麻烦的往往不是某一个 API Hook,而是: - 控制流被 OLLVM 打散; - 关键字符串被加密; - 部分逻辑运行在 VM 内; - 检测线程数量多; - 崩溃、退出、杀进程路径混在一起; - 很难判断某个函数到底是“检测点”还是“处置点”。 因此,与其一开始就试图完整还原所有混淆函数,不如先抓住一个更稳定的目标: ```c 进程最后是怎么死的? ``` 也就是先找“死亡出口”。 只要能稳定记录到死亡出口的调用方、线程、调用栈和附近上下文,就可以从结果往前倒推: ```c 谁触发了死亡 → 谁设置了风险标志 → 风险标志来自哪个检测项 → 检测项采集了什么输入 → 使用了什么判断方式 ``` --- ### 1. 什么是“死亡出口” 这里的“死亡出口”不是检测逻辑本身,而是检测命中之后最终执行的处置路径。 常见出口包括: ```c kill / tgkill exit / _exit abort raise / pthread_kill syscall(SYS_kill / SYS_tgkill / SYS_exit) SIGSEGV / SIGBUS / SIGABRT 主动制造非法访问 ART / odex 内跳转到崩溃路径 ``` 这些位置的特点是: 无论前面的检测逻辑如何混淆,最终大概率要通过这些出口让进程退出、崩溃或卡死。 所以先 Hook 它们,可以快速得到“检测链路的尾部”。 --- ### 2.为什么不直接从检测点开始找 在 OLLVM 或 VM 场景下,直接找检测点会遇到几个问题。 #### 2.1 控制流噪声太大 例如一个简单逻辑: ```c if (check_frida()) { kill(getpid(), SIGKILL); } ``` 经过混淆后可能变成: ```c dispatcher case 0x123: case 0x456: case 0x789: opaque predicate indirect branch state variable ``` 如果没有动态锚点,很难判断哪一段才是真正的业务判断。 #### 2.2 API 调用不等于检测点 例如看到 `open("/proc/self/maps")`,只能说明它读了 maps。 但它可能是: ```c 正常调试信息 崩溃收集 模块枚举 反注入检测 VM 环境初始化 ``` 所以单个 API 命中不能直接等于检测点。 #### 2.3 死亡出口更稳定 不管检测方式是: ```c 端口探测 maps 扫描 线程名扫描 时间检测 文件检测 ptrace 检测 ``` 最终通常还是会流向: ```c kill / exit / abort / crash ``` 所以先抓出口,能更快拿到回溯入口。 --- ### 3.Hook 死亡出口时要记录什么 Hook 死亡出口时,不只是简单打印一句“调用了 kill”。 真正有价值的是记录完整上下文。 建议记录字段如下: ```c 时间戳 线程 ID 线程名 API 名称 API 参数 returnAddress 调用方模块 调用方 RVA backtrace 当前已加载模块 死亡前最近的敏感 API 记录 ``` 例如 `tgkill`: ```c API: tgkill pid: xxx tid: xxx sig: SIGKILL caller: libxxx.so + 0x123456 thread: detect_worker backtrace: libxxx.so + 0x123456 libxxx.so + 0x234567 libxxx.so + 0x345678 libc.so + 0x... ``` 这时最重要的是: ```c caller = module + RVA ``` 因为它可以直接回到 IDA 里定位。 --- ### 4.returnAddress 的作用 `returnAddress` 是动态反推静态位置的核心。 比如 Hook 了 libc 的 `connect`,命中时拿到: ```c returnAddress = 0x7a12345678 ``` 通过模块基址归一化: ```c RVA = returnAddress - moduleBase ``` 得到: ```c libxxx.so + 0x345678 ``` 然后在 IDA 中跳转到该 RVA,看附近的逻辑: ```c BL connect CMP 返回值 CBZ / CBNZ 写风险字段 调用上报函数 进入 kill / abort 分支 ``` 这样就完成了从动态 API 命中到静态代码位置的映射。 --- ### 5.backtrace 的作用 `returnAddress` 只能看到直接调用者,但检测逻辑通常有多层封装: ```c detect_thread_entry → detect_dispatcher → check_frida_port → socket_connect_wrapper → connect ``` 如果只看 `returnAddress`,可能只能看到 wrapper。 而 backtrace 可以帮助区分: ```c 底层 API 包装函数 检测子项函数 检测调度函数 检测线程入口 ``` 例如: ```c connect hit: libxxx.so + 0x111111 // connect wrapper libxxx.so + 0x222222 // check local port libxxx.so + 0x333333 // detect dispatcher libxxx.so + 0x444444 // thread entry ``` 这样还原时就不会误把 wrapper 当成检测主逻辑。 --- ### 6. 需要 Hook 哪些观测边界 #### 6.1 加载边界 ```c dlopen android_dlopen_ext ``` 用途: ```c 确认检测模块何时加载 确认加载顺序 确认哪个 so 加载后开始死亡 确认检测逻辑是否在 init/init_array 中触发 ``` #### 6.2 线程边界 ```c pthread_create clone ``` 用途: ```c 记录 start_routine 定位检测线程入口 区分加载期线程和运行期线程 判断检测是否周期执行 ``` 重点记录: ```c start_routine = module + RVA caller = module + RVA arg = xxx ``` #### 6.3 文件读取边界 ```c open openat read pread64 fopen ``` 重点路径: ```c /proc/self/maps /proc/self/status /proc/self/cmdline /proc/self/fd /proc/self/task/*/status ``` 用途: ```c 判断是否读取进程映射 判断是否扫描线程名 判断是否枚举 fd 判断是否读取调试状态 ``` #### 6.4 字符串搜索边界 ```c strstr strcasestr strcmp strncmp memcmp memmem ``` 用途: ```c 捕获 needle 判断搜索目标 定位字符串检测函数 ``` 但要注意: ```c strstr 没命中,不代表没有字符串检测。 ``` 它可能使用: ```c 自写搜索 hash 比较 VM 内比较 加密字符串解密后比较 ``` #### 6.5 网络边界 ```c socket connect send recv ``` 用途: ```c 识别本地端口探测 识别检测上报 识别心跳类通信 ``` 典型情况: ```c connect 127.0.0.1:27042 connect 127.0.0.1:27043 ``` 可归类为本地调试端口探测候选。 #### 6.6 时间边界 ```c clock_gettime gettimeofday nanosleep usleep sleep ``` 用途: ```c 判断检测线程轮询周期 判断是否存在反调试时间差检测 判断是否存在卡顿检测 ``` --- ### 7. 从死亡出口反推检测点的流程 整体流程可以概括为: ```c 1. Hook 死亡出口 ↓ 2. 记录死亡 API 的 caller 和 backtrace ↓ 3. 得到 module + RVA ↓ 4. 回到 IDA 定位处置函数 ↓ 5. 找处置函数的调用者 ↓ 6. 找风险标志或状态字段 ↓ 7. 找风险字段的写入点 ↓ 8. 回溯输入来源 ↓ 9. 结合动态 Hook 证据验证 ``` 最终目标不是只知道“进程被 kill 了”,而是还原成: ```c 检测输入:读取 /proc/self/maps 检测方式:搜索 frida/gum 等特征 判断条件:命中后设置 risk_flag 处置方式:risk_flag 非零后进入 tgkill 静态位置:libxxx.so + RVA 动态证据:open/read/strstr/tgkill 调用栈 ``` --- ### 8. 一个典型反推例子 假设运行时记录到: ```c tgkill(SIGKILL) caller = libxxx.so + 0xA100 backtrace: libxxx.so + 0xA100 libxxx.so + 0x9F20 libxxx.so + 0x8B40 ``` 回到 IDA: ```c 0xA100 附近是 tgkill 调用 0x9F20 是风险处置函数 0x8B40 是检测调度函数 ``` 继续找 `0x9F20` 的输入: ```c if (risk_flag != 0) { tgkill(pid, tid, SIGKILL); } ``` 再找 `risk_flag` 写入: ```c risk_flag |= check_port(); risk_flag |= check_maps(); risk_flag |= check_thread_name(); ``` 再结合动态日志: ```c connect 127.0.0.1:27042 caller = libxxx.so + 0x7010 ``` 静态看 `0x7010`: ```c sock = socket(AF_INET, SOCK_STREAM, 0); ret = connect(sock, "127.0.0.1:27042"); if (ret == 0) { risk_flag |= PORT_DETECTED; } ``` 最后可以写: ```c 检测点:libxxx.so + 0x7010 检测方式:本地端口探测 检测目标:127.0.0.1:27042 处置点:libxxx.so + 0xA100 证据链:connect 命中 → risk_flag 写入 → tgkill ``` 这就是完整闭环。 --- ### 总结 这类检测还原的关键不是“Hook 到一个 API 就下结论”,而是建立证据链: ```c 输入来源 → 检测判断 → 风险标志 → 上报/处置 → 死亡出口 ``` “死亡出口”提供的是尾部锚点; `returnAddress` 提供动态到静态的映射; `backtrace` 提供调用链层级; 单变量实验提供验证。 只有当动态观测和静态数据流能够互相印证时,才适合把某个点写成明确检测点。否则应标为候选或部分验证。 这篇文章是在我实习过程中针对某个保护壳进行绕过时的思路和方法,真正准备秋招中,回复大家的评论会有点慢。希望对大家有帮助。 声明:文档由AI辅助生成,所有的方法与思路皆为一次实战后的总结
冰与火的战歌:Windows内核攻防实战高级班!从零到实战,融合AI与Windows内核攻防全技术栈,打造具备自动化能力的内核开发高手。
最后于
2026-9-3 11:44 被permanent011编辑 ,原因:
#基础理论
#逆向分析
#混淆加固
#脱壳反混淆
#HOOK注入
收藏
・
1
点赞
・
3
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
bluegatar
为你点赞!
2026-9-6 18:19
focaccia
谢谢你的细致分析,受益匪浅!
2026-9-4 10:41
mb_asiwnxyv
感谢你分享这么好的资源!
2026-9-3 14:56
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
1
)
mb_ldbucrik
雪 币:
6
能力值:
( LV1,RANK:0 )
在线值:
发帖
0
回帖
693
粉丝
7
关注
私信
mb_ldbucrik
2
楼
感谢分享
2026-9-3 12:17
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
permanent011
1
发帖
15
回帖
0
RANK
关注
私信
他的文章
[原创]从死亡现象反推检测点:一次检测链路定位方法论
361
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部