吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1701|回复: 6
收起左侧

[Android 原创] frida基于bug的Stalker跟踪检测和修复

[复制链接]
ZSAIm 发表于 2025-4-2 17:49
本帖最后由 ZSAIm 于 2025-4-2 17:49 编辑

frida基于bug的Stalker跟踪检测和修复

前言

Stalker 是 Frida 内部的一个模块用于动态追踪目标程序的执行流程,也就是当我们需要知道:

  • 函数是怎么调用的?
  • 指令是怎么一步步执行的?
  • 分支是怎么跳转的?
  • 有哪些函数是频繁被调用的?

的时候,会用到Stalker.follow跟踪线程将要执行的指令,可以对指令动态插桩修改。对莫名其妙的反调试行为来说,是一个通用的兜底窥探方案。

stalker跟踪检测

关于stalker参考官方文档: https://frida.re/docs/stalker/

注: 这个检测手段仅对于在开启Stalker.follow跟踪的线程生效

  • 原理:stalker会对执行过的内存地址(block->real_start)存储的指令会进行缓存,当再次进入要执行同一个地址的指令的时候,通过比对内存和缓存中的指令来确定是否需要重编译block来确保执行到正确的原指令 - 本应该是如此,但是正如标题所说,这个检测依赖当前frida-gum项目的stalker的重编译bug,而这个bug就出现在比对和重编译上,导致了当执行同一个内存地址的block时候,实际指令都是在跟第一次编译后的缓存block的原指令快照进行比对。所以这样的后果就是显而易见的,初次执行该内存地址的指令会一直存在于stalker的"阴影中",下面我会用一个测试例子来说明这个现象。

以上的场景说明存在简化,实际的问题路径在下面对应的代码:

  1. 指令比对
  2. block重编译

通过下面一个例子来验证这个问题

  • 为了利用上面这个bug,我们可以简单的实现两个函数,一个函数(add_99_func)是对传递的第一个参数+99,另一个函数(empty_func)是不做任何操作直接返回原参数。
uint64_t add_99_func(uint64_t count) {
    return count + 99;
}

// 对应的汇编可以是:
// add x0, x0, #99
// ret
uint64 empty_func(uint64_t count){
    return count;
}

// 对应的汇编可以是:
// ret
  • 使用mmap来创建指定内存块用于复写和执行指令

static void *exec_addr = nullptr;

// insnBytes: 要写入的指令字节数组
// num: 调用func传递的count值
jstring mmap_exec(JNIEnv *env, jobject thiz, jbyteArray insnBytes, jint num) {
    size_t page_size = sysconf(_SC_PAGE_SIZE);
    if (page_size == -1) {
        std::cerr << "Failed to get page size!" << std::endl;
        return nullptr;
    }

    void *start_addr = exec_addr;
    int flags = MAP_ANON | MAP_PRIVATE;
    if (start_addr != nullptr) {
        flags |= MAP_FIXED;
    }
    void *mem = mmap(
            start_addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC,
             flags, -1, 0); 
    if (mem == MAP_FAILED) {
        std::cerr << "mmap failed!" << std::endl;
        exec_addr = nullptr;
        return nullptr;
    }
    if (exec_addr == nullptr) {
        exec_addr = mem; // 下次也使用同一内存地址来写入指令
    }
    jsize length = env->GetArrayLength((insnBytes));

    jbyte *code = env->GetByteArrayElements(insnBytes, nullptr);
    std::memcpy(mem, code, length);

    aarch64_sync_cache_range(mem, page_size); // 刷新cpu指令和内存数据缓存

    func_t func = reinterpret_cast<func_t>(mem);

    uint64_t sum = func(num); // 调用函数

    munmap(mem, page_size);

    env->ReleaseByteArrayElements(insnBytes, code, 0);

    char buff[128];
    std::sprintf(buff, "[%p] sum[%lu]", mem, sum);

    return env->NewStringUTF(buff);
}

// jni native
__attribute__((visibility("default")))
JNIEXPORT jobjectArray JNICALL
Java_com_example_frida_1stalker_1recompile_1fix_MainActivity_mmapExec(
        JNIEnv *env, jobject thiz, jbyteArray inst1, jbyteArray inst2, jint base_num) {
    jclass stringCls = env->FindClass("java/lang/String");
    const jsize test_num = 20;
    jobjectArray resultArray = env->NewObjectArray(test_num, stringCls, nullptr);
    // 每点击一次按钮,遍历交替复写执行20次
    for(jsize i = 0; i < test_num; i++) {
        jbyteArray inst = i % 2 == 0 ? inst1 : inst2;
        jstring result1 = mmap_exec(env, thiz, inst, base_num);
        env->SetObjectArrayElement(resultArray, i, result1);
    }
    return resultArray;
}
  • frida测试脚本
setImmediate(() => {
    Interceptor.attach(
        Module.getExportByName('libdl.so', 'android_dlopen_ext'), {
            onEnter(args) {
                this.filename = args[0].readCString()
                console.error(`[android_dlopen_ext] ${this.filename}`)
            },
            onLeave(retval) {
                const filename: string = this.filename
                if(filename.includes('libtest_frida.so')) {
                    attachMmapExec(Process.findModuleByName(filename)!)
                }
            },
        }
    )

    function attachMmapExec(mod: Module) {
        console.error(`[attachMmapExec] ${JSON.stringify(mod)}`)
        const target = mod.getExportByName("Java_com_example_frida_1stalker_1recompile_1fix_MainActivity_mmapExec")
        Interceptor.attach(target, {
            onEnter(args) {
                console.log(`[mmap_exec] follow => tid[${Process.getCurrentThreadId()}]`)
                Stalker.follow(Process.getCurrentThreadId(), {
                    transform(iterator: StalkerArm64Iterator) {
                        let inst
                        while((inst = iterator.next()) !== null) {
                            // console.log(`[${Process.getCurrentThreadId()}] ${inst}`)
                            iterator.keep()
                        }
                    }
                })
            },
            onLeave(retval) {
                Stalker.unfollow()
                console.error(`[mmap_exec] unfollow => tid[${Process.getCurrentThreadId()}]`)
            },
        })
    }
})
  • 通过对于同一个内存块进行来回按照empty_funcadd_99_func的顺序来写入对应的的汇编来执行(假设传递的count=1):<sup>代码(2)</sup>
    1. 第一次执行empty_func(1)的时候返回值是1(首次编译,缓存根据empty_func生成的插桩指令)
    2. 通过比对指令,第二次进行了重编译,执行add_99_func(1)返回100 (第一次重编译,缓存根据add_99_func生成的插桩指令,但是指令快照却是empty_func)
    3. 第三次执行,预期是执行empty_func(1)返回1,但实际却因为快照是第一次编译的指令(empty_func),所以误认为指令没变动,所以执行了第一次重编译的插桩代码add_99_func,导致了此时会返回100。
    4. 第四次执行,预期是执行add_99_func(1)返回100,实际上也是执行了add_99_func(1),但是错误的又重编译一次
    5. ...接下来的都会是执行add_99_func(1)的结果

以下展示了三种测试结果用于说明:

1. 正常无frida-stalker跟踪现象

003-frida-stalker修复.jpg

2. 使用原版frida-stalker跟踪现象

002-frida-stalker原版.jpg

3. 使用修复的frida-stalker后的跟踪现象

001-正常执行.jpg

编译修复frida-server

拉取源项目和合并修改

# 从frida中递归子模块拉取主分支(修改基于16.5.9进行,所以我编译这个版本,其他版本如果没冲突可以试试)
git clone --branch 16.5.9 --single-branch --recurse-submodules https://github.com/frida/frida.git

# 进入子模块下的frida-gum
cd subprojects/frida-gum

# 添加修复的远程代码仓库地址
git remote add zsa233 https://github.com/zsa233/frida-gum.git

# 拉取zsa233仓库,并且将对应的修改分支合并到本地仓库
git fetch zsa233
git merge zsa233/fix/stalker-wrong-recompile

# 后面接着就是编译

编译

参考官方文档: https://frida.re/docs/building/#cross

# 指定ANDROID_NDK_ROOT,我这里下载r25c版本(16.5.9要求
# wget https://dl.google.com/android/repository/android-ndk-r25c-linux.zip
# 其中还需要node.js >= 18之类的编译依赖,根据错误提示解决即可,这里不再赘述
# 
export ANDROID_NDK_VERSION=r25c
# !!!/mypath/改成自己的ndk路径
export ANDROID_NDK_ROOT=/mypath/android-ndk-$ANDROID_NDK_VERSION/

# 回到frida项目下,配置编译android-arm64平台
./configure --host=android-arm64

# 编译
make

# 将编译成功的frida-server上传
adb push build/subprojects/frida-core/server/frida-server /data/local/tmp/frida-server
# 加上x权限
adb shell chmod u+x /data/local/tmp/frida-server

测试

修复的测试结果如上图

资源链接

  1. 代码仓库
  2. 测试app代码
  3. so库实现
  4. frida注入脚本
  5. 编译的arm64测试apk

免费评分

参与人数 5吾爱币 +6 热心值 +5 收起 理由
longforus + 1 + 1 谢谢@Thanks!
timeni + 1 + 1 用心讨论,共获提升!
芽衣 + 2 + 1 用心讨论,共获提升!
helian147 + 1 + 1 热心回复!
Bob5230 + 1 + 1 我很赞同!

查看全部评分

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

oddant 发表于 2025-4-3 11:18
学习学习
stopforme 发表于 2025-4-3 21:38
linlin01 发表于 2025-4-4 23:34
mustime 发表于 2025-4-7 10:20
学习了,相当硬核。
另外可以直接拉取楼主的 pull request: https://github.com/frida/frida-gum/pull/995
wangjichuan 发表于 2025-4-7 11:53
学习了,谢谢
vip888pj 发表于 2025-4-13 05:11
大谢楼主。
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2025-4-30 11:22

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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