吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 606|回复: 0
收起左侧

[Android 原创] 基于QDBI的真机trace方案

[复制链接]
lyldalek 发表于 2026-7-12 12:20
好长时间没发贴了,贴一下最近的文章。

最近一直在研究 unidbg 来还原一些so的逻辑,但是遇到了一些问题:

  1. Unidbg太慢了,有一些so会在app运行5-10分钟后才会触发一些逻辑,Unidbg模拟起来会进入空转状态。
  2. 有时候还会与网络等进行交互,弄起来很糟心,一些特征也需要伪造。
  3. 生命周期问题,有些so的一些方法是有调用顺序的,还会涉及到消息队列等,这些模拟起来就得先摸清楚整套so的java与native交互,不然会在奇葩的地方卡住,或者走不全逻辑。

于是就往真机 trace 方向研究了一下。真机的trace,之前是尝试过 Frida 的 Stalker,但是不稳定,挂上去就crash,后来尝试了 GumTrace,说是提升了稳定性,有一个 so 确实可以 trace 了,但是依然无法 trace 另一个 so。
这篇文章主要是介绍一下 qbdi,先看一个基于 qbdi 的项目。
我写自己写了一个demo,下面也会介绍。

https://bbs.kanxue.com/thread-290574.htm

直接看代码,发现有 jnitrace 功能,直接看看是如何实现的。

首先需要了解一个前提,就是 qbdi 调用 call 的时候,需要传递参数,如果我们需要trace真实环境的话,那么就需要等到这个方法主动触发才行。

所以,如何才能知道这个方法被调用了还能获取方法参数呢?使用 hook 框架。

比如:

https://github.com/jmpews/Dobby

https://github.com/bytedance/android-inline-hook

有了 hook 框架,我们在我们想要监控的函数上下 hook,然后在 hook 逻辑里面去调用 vm call 方法,就可以 trace 方法了。

看一下具体的代码:

addJNITrace((void*)pJFunc->NewStringUTF,"NewStringUTF",trace_NewStringUTF);

这个方法只是将需要 trace 的函数地址给储存了起来。

shadowhook_hook_func_addr((void*)(_g_trace_data->start + _g_trace_data->target),
                                                            (void*)(hook_and_trace_arg8),
                                                            (void**)&ori_arg8);

_g_trace_data->start + _g_trace_data->target是需要 trace 的函数的地址,将函数入口地址替换为 hook_and_trace_arg8。

在这个方法里面先做了 unhook,再开启 call:

shadowhook_unhook(_g_trace_data->hooktask);

size_t function_address = _g_trace_data->start + _g_trace_data->target;
auto qvm = vm_->init(_g_trace_data->start,_g_trace_data->end);
bool qbdi_success = qvm.call(&qbdi_retval, (uint64_t)function_address);

shadowhook_hook_func_addr((void*)(_g_trace_data->start + _g_trace_data->target),
                                                            (void*)(hook_and_trace_arg8),
                                                            (void**)&ori_arg8);

可以看到 call 的时候传递的是函数地址,所以必须先 unhook。如果不先 unhook,function_address 入口还是被 shadowhook patch 过的。那么 QBDI 一调用目标函数,又会跳回 hook_and_trace_arg8,造成递归/死循环。

trace 完成后,需要重新再次 hook 上,如果,你只想trace一次的话,那就没必要。

看看 vm_ 的 init 方法,里面肯定有设置 callback 的:

    //指令前hook
    cid = qvm.addCodeCB(QBDI::PREINST, showPreInstruction, this);

    //读写trace
    cid = qvm.addMemAccessCB(MEMORY_READ_WRITE, showMemoryAccess, this);

    // 该模块需要插桩
    bool ret = qvm.addInstrumentedModuleFromAddr(reinterpret_cast<QBDI::rword>(start));

函数执行的过程中,会触发 showPreInstruction 和 showMemoryAccess 回调,由于我们设置了整个so范围插桩,所以,这个函数调用了其他so内的函数,也可以被记录下来。但是,调用了 libc.so 中的方法就无法记录了。

在 showPreInstruction 里面,它判断了指令:

if(instAnalysis->isCall && !strcmp(instAnalysis->mnemonic,"BLR"))
if(instAnalysis->isBranch && !strcmp(instAnalysis->mnemonic,"BR") && hasLibctrace())

是这两个指令的话,再去详细查询这个地址在不在 map 里面,前面我们设置 jni trace 的时候,是保存了需要trace的函数地址的,所以这里用来做比较。如果相等,就调用设置的回调函数。

libc 函数的 trace 基本是同样的逻辑。

EXEC_TRANSFER_CALL,这里处理的过于麻烦了。

实战演示

我写了一个demo:https://github.com/aprz512/qbdi-android,里面演示了如何使用真机来做trace

目标APP

设计比较简单,就是几个简单的 case,为了方便展示和学习,方法符号都保留了。

第一个,演示 init 函数的trace:

__attribute__((constructor)) void native_constructor() {
    integrity_capture_baseline();
    demo_init_stage();
}

第二个,演示 JNI 函数的 trace(只 trace 了 call 调用,参数,返回值等):

extern "C" std::string demo_jni_case(JNIEnv *env, jobject thiz) {

第三个,演示 libc 函数的 trace(只 trace 了 call 调用,参数,返回值等):

extern "C" std::string demo_libc_case() {

第四个,演示检测对抗,做了一个简单的 maps 扫描,和 .text 完整性检测,使用 QBDI 来修改寄存器/指令的方式来绕过检测:

extern "C" std::string demo_integrity_case() {

第5个,演示trace算法进行还原:

extern "C" uint64_t demo_algorithm_case(const uint8_t *data, size_t size) {

trace框架的设计与使用

整个框架的的入口在:

qbdi_tracer_install_module

这个方法由 frida 的 js 调用,参数也从 js 里面传递进去,installModuleObserver会监控 so 的加载,检测到目标 so 加载时,会调用上面的 c++ 方法。

Frida 的 moduleObserver.onAdded 本身就会在 so 的 init 阶段前触发,所以我们可以 trace so 的 init 内的方法。

参数配置:

const config = {
  packageName: 'com.aprz.qbdiandroid',
  remoteDir: '/data/local/tmp/qbdi-android',
  tracer: 'libqbdi_tracer.so',
  targetSo: 'libdemo_target.so',
  scenes: {
    init: { offset: '0x6AC90' },
    jni: { offset: '0x6DCA8' },
    libc: { offset: '0x6E204' },
    algorithm: { offset: '0x6DB38' }
    integrity: { offset: '0x6E584' }
  }
};

scenes 里面就是配置了一些 trace 点,一共支持 256 个,每个 scene 都会调用 trace_proxy_for方法,这个方法会先 unhook,再调用 vm.call,再hook,具体可以看代码。

核心逻辑就是 run_with_qbdi,里面设置了各种回调:

    vm.recordMemoryAccess(QBDI::MEMORY_READ_WRITE);
    vm.addCodeCB(QBDI::PREINST, on_pre_instruction, &state);
    vm.addCodeCB(QBDI::POSTINST, on_post_instruction, &state);
    vm.addMemAccessCB(QBDI::MEMORY_READ_WRITE, on_memory, &state);
    vm.addVMEventCB(QBDI::EXEC_TRANSFER_CALL | QBDI::EXEC_TRANSFER_RETURN, on_exec_transfer,
                    &state);

所以,trace会有比较详细的信息。

监控  libc 与 jni 都是使用的EXEC_TRANSFER_CALL,简单又方便,当然现在还很简陋,没有打印足够的信息,好在后续可以扩展。

另外,还设计了一个指令hook方案,需要自行在register_user_code_rules这个方法里面添加逻辑,下面是一个 demo ,可以做到类似  ida patch 指令的效果,后面会演示如何修改指令执行来过检测:

 * class ForceEqualsRule final : public OffsetCodeRule {
 * public:
 *     explicit ForceEqualsRule(uintptr_t offset) : OffsetCodeRule(offset) {}
 *
 *     QBDI::VMAction on_pre_instruction(CodeRuleContext &context) override {
 *         context.set_zero_flag(true);
 *         context.trace_rule("force_equals", "z=1");
 *         return QBDI::CONTINUE;
 *     }
 * };

trace抓取

先安装目标 app。

运行frida 脚本:

frida -U -f com.aprz.qbdiandroid -l scripts/spawn_trace.js

app 启动时会生成一个 init 阶段的 trace,点击 app 里面的按钮会生成对应的 trace,点击最后一个 按钮刚开始会crash。

init 函数 trace 效果如下:

TRACE_BEGIN scene=init target=libdemo_target.so+0x6ac90 base=0x758a0c0000 address=0x758a12ac90 pid=16850 tid=16850
1 libdemo_target.so+0x6ac90     stp x29, x30, [sp, #-0x10]! | R:FP=0xb400007589fbb000 LR=0x2a SP=0xb400007589fbaf80 
MEM libdemo_target.so+0x6ac94 type=w addr=0xb400007589fbaf70 size=8 value=0xb400007589fbb000
MEM libdemo_target.so+0x6ac94 type=w addr=0xb400007589fbaf78 size=8 value=0x2a
2 libdemo_target.so+0x6ac94     mov x29, sp | R:SP=0xb400007589fbaf70 
3 libdemo_target.so+0x6ac98     bl  #0x3ab4 | R:SP=0xb400007589fbaf70 
4 libdemo_target.so+0x6e74c     sub sp, sp, #0x20 | R:SP=0xb400007589fbaf70 

对比 ida 中的汇编:

.text:000000000006AC90                 STP             X29, X30, [SP,#-0x10+var_s0]!
.text:000000000006AC94                 MOV             X29, SP
.text:000000000006AC98                 BL              integrity_capture_baseline
.text:000000000006AC9C                 BL              demo_init_stage
.text:000000000006ACA0                 LDP             X29, X30, [SP+var_s0],#0x10
.text:000000000006ACA4                 RET

这里,由于我没有传 end,所以默认 trace 整个 so,会将 init 函数调用的子函数也一起 trace 进去。

算法还原

将抓到的trace文件丢给AI,以deepseek v4 pro 为例:

 分析一下 1783670470970_16203_16203_algorithm_0x6db38.trace.txt, 还原一下算法逻辑     

AI3分钟后回复:

  ———

  ### 核心算法

  这是一个 TEA 结构 + MurmurHash3 终结器 的混合哈希函数,逐字节处理:

  state = len ^ 0x9E3779B185EBCA87    ← 初始化(黄金比例常数异或长度)

  for each byte in data:
      tmp = byte + 0x9E3779B97F4A7C15 + (state << 6) + (state >> 2)
      state ^= tmp                         ← TEA 风格混叠
      state = rotl(state, 13)              ← 左旋 13 位
      state *= 0xFF51AFD7ED558CCD          ← MurmurHash3 fmix64 常数

  state ^= state >> 33
  state *= 0xC4CEB9FE1A85EC53              ← MurmurHash3 fmix64 常数
  state ^= state >> 29
  return state                             ← 最终哈希值

  ———

可以对比一下原代码,看看效果。

libc/JNI 监控

目前做的比较简单,需要详细信息可以自己扩展:

CALL jni.RegisterNatives target=0x7615e45960
CALL return.RegisterNatives jni.RegisterNatives target=0x7615e45960 ret=0x78d1294065

CALL jni.GetObjectClass target=0x7615e466a0
CALL return.GetObjectClass jni.GetObjectClass target=0x7615e466a0 ret=0xb40000774a27af40

CALL libc.fopen target=0x78c54fe330 x0=0x758a102b22 preview="/proc/self/maps"
CALL return.fopen libc.fopen target=0x78c54fe330 ret=0xb40000780a239888

CALL libc.__system_property_get target=0x78c54acb10 x0=0x758a101782 preview="ro.build.version.sdk"
CALL return.__system_property_get libc.__system_property_get target=0x78c54acb10 ret=0x2

检测绕过

查看 ida ,发现绕过检测逻辑很简单:

.text:000000000006E5C0                 TBNZ            W0, #0, loc_6E5D0
.text:000000000006E5C4                 B               loc_6E5C8
.text:000000000006E5C8 ; ---------------------------------------------------------------------------
.text:000000000006E5C8
.text:000000000006E5C8 loc_6E5C8                               ; CODE XREF: demo_integrity_case+40↑j
.text:000000000006E5C8                 BL              integrity_crash

只需要修改 6E5C0 处的寄存器值为1,即可绕过:

// 6E5C0: TBNZ W0, #0, loc_6E5D0
// Set bit #0 of W0 so TBNZ takes the branch to loc_6E5D0
class ForceTbnzW0Rule final : public OffsetCodeRule {
public:
    explicit ForceTbnzW0Rule(uintptr_t offset) : OffsetCodeRule(offset) {}

    QBDI::VMAction on_pre_instruction(CodeRuleContext &context) override {
        uint64_t w0 = context.reg(0);
        context.set_reg(0, w0 | 1ULL);
        context.trace_rule("force_tbnz", "W0 bit#0 set, jump to loc_6E5D0");
        return QBDI::CONTINUE;
    }
};

另一处类似,就不贴代码了。

使用 frida 注入 so, 再次点击 trace integrity case 按钮,就不会闪退了,但是注意,这里trace的范围很多,生成一个 400多M的trace文件,因为so里面将 string 的一些函数打进去了,所以会卡一下,需要优化一下文件读写。



我是基于 pixel 6 - android 14做的,整体使用还是比较流畅的,trace性能确实是一个问题,400M左右就卡了接近8s,需要重点优化,当然现在只是一个demo。

免费评分

参与人数 3吾爱币 +5 热心值 +3 收起 理由
smile1110 + 3 + 1 非常强
canty胖胖 + 1 + 1 用心讨论,共获提升!
laos + 1 + 1 谢谢@Thanks!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-13 18:49

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表