首页
社区
课程
招聘
5
frida基于bug的Stalker跟踪检测和修复
发表于: 2025-4-2 17:50 2045

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

2025-4-2 17:50
2045

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)是不做任何操作直接返回原参数。
1
2
3
4
5
6
7
uint64_t add_99_func(uint64_t count) {
    return count + 99;
}
 
// 对应的汇编可以是:
// add x0, x0, #99
// ret
1
2
3
4
5
6
uint64 empty_func(uint64_t count){
    return count;
}
 
// 对应的汇编可以是:
// ret
  • 使用mmap来创建指定内存块用于复写和执行指令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
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):
  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跟踪现象

无注入执行

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

frida-stalker原版跟踪

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

frida-stalker修复版跟踪

编译修复frida-server

拉取源项目和合并修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 从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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 指定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
支持
分享
赞赏记录
参与人
雪币
留言
时间
马先越
期待更多优质内容的分享,论坛有你更精彩!
2025-4-15 19:24
sinker_
谢谢你的细致分析,受益匪浅!
2025-4-3 19:37
陈可牛
非常支持你的观点!
2025-4-3 14:36
pexillove
为你点赞!
2025-4-2 20:35
ngiokweng
感谢你的贡献,论坛因你而更加精彩!
2025-4-2 18:58
最新回复 (0)
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册