这篇文章上次修改于 292 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

前言

jnitrace想必大家都不陌生,但是使用起来肯定有些不顺手,在此对其进行优化并增强信息打印

优化jnitrace

你可能遇到过这样的情况

  • 使用jnitrace的时候APP总是卡在启动页、手机一直处于黑屏状态

这个问题我也遇到过了,不过根据经验,确实有些手机没有这样的情况,具体这是怎么回事我也没搞清楚

不过在分析了一波jnitrace的源代码之后,我发现这个地方可能存在问题

const dlopen = new NativeFunction(dlopenRef, "pointer", ["pointer", "int"]);
Interceptor.replace(dlopen, new NativeCallback((filename: NativePointer, mode: number): NativeReturnValue => {
    const path = filename.readCString();
    const retval = dlopen(filename, mode);

    if (path !== null) {
        if (checkLibrary(path)) {
            // eslint-disable-next-line @typescript-eslint/no-base-to-string
            trackedLibs.set(retval.toString(), true);
        } else {
            // eslint-disable-next-line @typescript-eslint/no-base-to-string
            libBlacklist.set(retval.toString(), true);
        }
    }

    return retval;
}, "pointer", ["pointer", "int"]));

Interceptor.replace的目标函数是dlopen,但是在其回调函数内又调用了dlopen,这是否会出现死循环呢?

我尝试在NativeCallback中加入打印日志,发现并没有出现死循环,然后我尝试将其注释掉,发现这下APP能进到启动页了

后来检查logcat,发现在APP卡退时,日志中出现了Waiting for a blocking GC ProfileSaver

搜了下这个异常好像和内存有关,然后再次测试,发现jnitrace启动后APP在卡退之前,CPU居然占到了100%

所以是为什么呢,把上面那部分代码注释掉就正常,但是根据日志结果又没有无限循环

最后我的推测是:ROM某些编译设定引发的bug

如果有人知道为什么请告诉我

为了不影响原有功能,用Interceptor.attach实现上面的逻辑,这样不会引起卡死

Interceptor.attach(dlopenRef, {
    onEnter:function(args){
        this.path = args[0].readCString();
    },onLeave:function(retval){
        if (this.path !== null) {
            if (checkLibrary(this.path)) {
                trackedLibs.set(retval.toString(), true);
            }
            else {
                libBlacklist.set(retval.toString(), true);
            }
        }
    }
});

增强信息打印

jnitrace会贴心地把jstring内容打印出来,无论是传入参数还是返回结果

但还有一种情况下迫切需要知道内容是什么,即参数是Ljava/lang/String;类型

在分析了jnitrace源码后,以CallObjectMethodV为例,对于返回结果是Ljava/lang/String;的,可以在这两处添加代码,实现内容打印

if (name == "CallObjectMethodV") {
    // args => JNIEnv*, jobject, jmethodID, va_list
    const MID_ARG_INDEX = 2;
    const mid_key = data.args[MID_ARG_INDEX].toString();
    const sig = this.jmethodIDs.get(mid_key);
    if (sig && sig.endsWith("Ljava/lang/String;")) {
        let utf = "getStringUtfChars error";
        try {
            utf = Java.vm.getEnv().getStringUtfChars(data.ret, null).readUtf8String();
        } catch (error) {
            console.log(`errorerror => ${error}`);
        }
        outputRet.push(
            new DataJSONContainer(
                data.ret,
                utf
            )
        );
        return null;
    }
}
if (typeof data === "string"){
    this.metadata = data as string;
}

修改后的效果

另外有一个需要注意的点,win用户运行的时候记得设置PYTHONIOENCODING,不然有时候会出现终端输出会出现编码异常

chcp 65001
set PYTHONIOENCODING=utf-8

一旦出现了中文,那么保存为json文件的时候也要注意编码,这里需要修改两处

argparse.FileType("w", encoding='utf-8')
json.dump(formatter.get_output(), args.output, ensure_ascii=False, indent=4)

同理对于其他jni调用,如果入参是Ljava/lang/String;,则可以在addJNIEnvArgs中操作

jnitrace原本有关字符串获取是通过从DataTransportjstrings缓存取值实现的,但是这样只能打印部分字符串

上面通过Java.vm.getEnv().getStringUtfChars(data.ret, null).readUtf8String();获取的字符串,日志中会多出ReleaseStringUTFChars对应字符串的信息,不过这倒无伤大雅了

奇怪的技巧

在测试过程中,我修改替换了dlopen部分的代码,但是还是有hook不上的情况

但是我发现,如果我点击启动界面的跳过按钮,则hook顺利...

(当然,如果不修改dlopen部分的代码,根本见不到启动界面

演示视频如下

  • 演示视频
  • 第一次直接执行命令,很快就崩掉了
  • 第二次点击了屏幕任意位置,相对第一次打印的信息多一些
  • 第三次点击了跳过按钮,顺利hook且不会崩溃