首页
社区
课程
招聘
[原创] trace 检测
发表于: 10小时前 271

[原创] trace 检测

10小时前
271

项目地址:dbi_detect

上周让 AI 分析了几个大厂 VMP 样本,只给 trace 日志,两天秒了三个 VMP ,而且两天是因为,iOS端 APP 主程序太大了,最大的一个有 900MB ,两天里面有 1.9 天浪费在 IDA 反编译上。基本上 AI 40分钟左右搞定一个,给我吓哭了。这还原算法的成本还是太低了

这个问题产生的原因主要有两点吧:

当然我的日常工作主要聚焦在 iOS 端,所以本文后续的 检测内容都是基于 Android 平台的ovo

官方的说法:DBI全称Dynamic Binary Instrumentation,动态二进制插桩。它可以在程序运行期间分析、重写原始机器码,并在其中插入回调、计数、访存记录等逻辑

我们以QBDI这类用户态DBI为例,它大概会经历下面几个步骤:

QBDI官方文档对这个过程描述得很直接:DBI核心依赖JIT重编译,插桩后的basic block会写入可执行内存,并保存在code cache中避免重复翻译

那么这时会同时就会存在两套PC概念:

原生执行时,两者基本一致,都位于目标SO的.text中。进入DBI后,Guest PC仍然可以被维护成原始地址,但Physical PC通常已经落在code cache或者DBI调度路径里了

当然,你可能会想那我直接写一句ADR读取PC不就行了吗

实际测下来确实没啥卵用,原始指令adr x8, #4执行后,DBI给出的x8仍然是下一条Guest指令的地址,结果依旧落在目标 so 代码段内

因为ADR本身也会被DBI翻译,它可以在重定位时维持Guest PC语义,所以我们需要找一个不完全受DBI控制的观察者

Linux内核正好可以充当这个观察者

从信号上下文偷看真实PC

Linux向用户态线程投递信号时,会保存线程被打断瞬间的寄存器上下文。使用SA_SIGINFO安装handler后,可以从第三个参数拿到ucontext_t,ARM64下的PC位于:

如果目标函数原生执行,内核保存的PC应该落在我们预先确定的探针窗口内。如果目标函数已经被翻译到code cache执行,而DBI又没有额外虚拟化信号上下文,那么这里就会泄露Physical PC

围绕这个思路,当前检测流程如下:

图里虽然有一个watchdog线程,但它不是后台轮询检测线程。它平时阻塞在条件变量上,只在受保护函数主动执行检测宏时被唤醒,本质上仍然是同步检测

我们不能在整个业务函数执行期间要求PC始终位于目标SO,因为业务函数可能正常调用libc、系统库或者其他SO

所以宏里放了一段纯汇编探针窗口。窗口内部不调用外部函数,也不执行系统调用,只维护几个状态并等待采样完成:

async_beginasync_end都是由ADR在运行时生成的实际地址,因此ASLR不会影响判断

检测第一次运行时会创建一个共享watchdog线程,此后它一直休眠,只有探针开始前才被唤醒

目标线程进入汇编窗口并设置active后,watchdog通过pthread_kill()向目标线程发送SIGUSR2。handler从ucontext_t保存PC,watchdog等待本次样本写入后再发送下一次,一共采集8个样本

这里不能简单地连续发送8次普通信号,因为非实时信号会合并,最后可能只收到一次

最终判断很朴素:

只要有一个有效样本落在窗口外,就记录一项强证据

如果信号被延迟或者没有拿到样本,则不能单凭这一点判断存在DBI。调度抖动、信号屏蔽和设备负载都有可能造成相同现象,所以当前实现只把明确的窗口外PC当成命中

异步采样依赖另一个线程和信号调度,优点是触发时机不容易预测,缺点是流程稍微复杂一些。第二种方案就简单粗暴一点,直接主动制造一次同步异常

探针在编译时记录BRK标签地址,然后执行:

原生执行时,BRK触发SIGTRAP,内核交给handler的PC应当等于brk_expected。handler记录PC、SP和LR,再把上下文中的PC增加4字节,跳过当前ARM64指令继续执行:

结束时检查:

如果BRK在code cache中执行,而DBI直接把异常交给内核,ucontext_t.pc就可能指向code cache。如果DBI不执行BRK,而是自己模拟后续流程,那trap_seen就可能一直是0

这两个结果都和原生语义不一致

这两种方案本质上都在检查PC,但观察时机不同:

单独使用BRK,DBI可以识别这条固定指令并伪造异常现场。单独依赖异步采样,又会受到调度和信号投递的影响。两种方案一起上,就需要同时处理异步中断和同步异常两条路径

最终结果使用进程级sticky状态:

一旦任意一次检测获得强证据,后续调用都会继续返回已检测状态,不会因为下一次刚好没有采到异常PC又恢复正常

具体建议直接去拉源码看一下。实现上没有搞得太复杂,对外只留了一个DBI_TRACE_DETECTED()宏,直接插在想保护的函数里就行

宏内部大概就是跑一轮探针,然后返回进程级检测状态


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

上传的附件:
收藏
免费 0
打赏
分享
最新回复 (12)
雪    币: 213
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
最简单的检测办法就是看执行时间,一旦trace耗时几何级增长
10小时前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
向大佬学习
9小时前
0
雪    币: 149
活跃值: (1639)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
6
9小时前
0
雪    币: 9085
活跃值: (5833)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
看看
8小时前
0
雪    币: 1904
活跃值: (3657)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
6
mb_cfjwplfo 最简单的检测办法就是看执行时间,一旦trace耗时几何级增长

这个是在运行时就检测到,然后走不同的分支。你检测时间的话,对方已经trace完了

最后于 8小时前 被Yangser编辑 ,原因:
8小时前
0
雪    币: 3023
活跃值: (5881)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
666
7小时前
0
雪    币: 411
活跃值: (541)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
666
7小时前
0
雪    币: 767
活跃值: (975)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
9
严肃学习
5小时前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
10
666
5小时前
0
雪    币: 88
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
理论上能用但真空球形鸡
4小时前
0
雪    币: 248
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
mark
4小时前
0
雪    币: 99
活跃值: (1712)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
mark
17分钟前
0
游客
登录 | 注册 方可回帖
返回