本篇进入 Android frida 实战,旨在分析学习全民K歌这个 app 演唱页面的判断逻辑。
版本:8.22.38.278


此 app 为腾讯推出的面向国内的社交娱乐类应用软件,主要功能是提供用户唱歌、录制和分享自己演唱的歌曲。当非 vip 用户演唱某 vip 歌曲等功能时便会触发弹窗,阻止用户使用其功能。

我们进入演唱页面,点击切换音质,触发 vip 弹窗。

借助算法助手,找到点击事件的回调类:e51.a

查看其 smali 代码,一路跟踪 onlick 方法,可以看到 onclick 调用了 e51.e 的 V 方法,最终来到了 n0 这个方法处。


省略复杂且漫长的追堆栈过程,最终弹窗在 com.tencent.tme.record.module.vip.RecordPrivilegeAccountModule 这个类的 M0 方法中被触发,M0 的 smali 太啰嗦,我们直接反编译看逻辑:

@UiThread
public final void M0(ye2.a aVar, boolean z, boolean z2, String str, f51.a aVar2) {
    byte[] bArr = SwordSwitches.switches1;
    if (bArr != null && ((bArr[888] >> 1) & 1) > 0) {
        if (SwordProxy.proxyMoreArgs(new Object[] { aVar, Boolean.valueOf(z), Boolean.valueOf(z2), str, aVar2 }, this,
                7106).isSupported) {
            return;
        }
    }
    TaskUtilsKt.s(new RecordPrivilegeAccountModule$showChargeVIPDialog$1(this, z2, z, aVar2, str, aVar));
}

经研究得知,上部分的 SwordProxy 是通用逻辑负责合法校验,不起业务判断作用,校验通过后,下部分无条件直接 new 一个 showChargeVIPDialog。也就是说 M0 只要被调用,就无条件触发弹窗。因此我们需要查看 M0 的上游,通过追堆栈找到上游为同类中的 m0 方法,再次反编译出来如下:

public final boolean m0(int i, boolean z, boolean z2, String str, f51.a aVar) {
    int i2 = i;
    boolean z3 = z;
    f51.a aVar2 = aVar;
    byte[] bArr = SwordSwitches.switches1;
    if (bArr != null && ((bArr[882] >> 3) & 1) > 0) {
        SwordProxyResult proxyMoreArgs = SwordProxy.proxyMoreArgs(
                new Object[] { Integer.valueOf(i), Boolean.valueOf(z), Boolean.valueOf(z2), str, aVar2 }, this, 7060);
        if (proxyMoreArgs.isSupported) {
            return ((Boolean) proxyMoreArgs.result).booleanValue();
        }
    }
    int u = (int) yu1.e.f().b().u();
    boolean E = yu1.e.f().b().E();
    String str2 = this.F;
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.append("handleSuperSoundVIP level = ");
    stringBuilder.append(i);
    stringBuilder.append(" isVip = ");
    stringBuilder.append(z);
    stringBuilder.append(",userVipLevel:");
    stringBuilder.append(u);
    stringBuilder.append(",userIsSubscription:");
    stringBuilder.append(E);
    LogUtil.i(str2, stringBuilder.toString());
    str2 = "record_module#sound_quality_panel#nul";
    if (z3) {
        if (u >= i2) {
            return true;
        }
        if (E && aVar2.l) {
            this.M = true;
            return true;
        } else if (aVar2.l) {
            g5.a.e(this.e, i, aVar2.k, str2);
        } else {
            H(i, u, z2, null, aVar);
        }
    } else if (aVar2.l) {
        g5.a.e(this.e, i, aVar2.k, str2);
    } else if (u >= i2) {
        M0(null, true, z2, str, aVar);
    } else {
        H(i, u, z2, null, aVar);
    }
    return false;
}

Finally,看到了熟悉的日志字样,传入参数 z 是一个布尔类型,代表 isVip 字段。 int u = (int) yu1.e.f().b().u() 链式调用的方式得到 u,代表 userVipLevel

代码执行时,将 z 的值赋给 z3,判断 if (z3),若用户是 vip 则走 if 下面的逻辑,若用户不是 vip 则走 else if 的逻辑,其中有一条 else if 调用到 M0(null, true, z2, str, aVar),触发弹窗。

当 isVip 是 true 的时候,接着判断 if (u >= i2) ,也就是用户的 vip 等级是否大于想要切换的音质的等级,若是则直接返回 true,否则继续判断走下面。


现在已经清晰了,若我们只想 hook 切换音质这一个功能的话,直接让这个 m0 方法返回 true 即可,但我们想找到传入 m0 的代表用户是否是 vip 的参数 z 是哪来的,以及用户的 vip 等级的链式调用最终走到了哪里返回。
或许所有 vip 功能的判断最终在底层走的都是同一个函数调用,这样我们就不需要一个一个去 hook 单功能点了。

篇幅关系,再次省略复杂且漫长的追堆栈过程:

1. isVip 判断逻辑:

isVip 是 true 还是 false 在 yu1.d 类中的 F() 方法中返回,F 方法如下:

    public boolean F() {
        byte[] bArr = SwordSwitches.switches9;
        if (bArr != null && ((bArr[102] >> 7) & 1) > 0) {
            SwordProxyResult proxyOneArg = SwordProxy.proxyOneArg(null, this, 192824);
            if (proxyOneArg.isSupported) {
                return ((Boolean) proxyOneArg.result).booleanValue();
            }
        }
        return o01.d.d(v());
    }

先调用 v() 方法,将返回值传入 o01.d 类中的 d 方法中,最终返回 true/false。(真啰嗦)

d 方法如下,当传入的参数为 2、3、5 时,返回 true:

public static boolean d(int i) {
    if (!(3 == i || 2 == i)) {
        if (5 != i) {
            return false;
        }
    }
    return true;
}

v 方法如下,再次调用 ah.e 中的 o 方法得到要传入 d 方法中的参数:

public int v() {
    int i = 1;
    try {
        i = ah.e.o(this.b, this.c);
    } catch (Exception e) {
        dn.e.b(e, "运行时类初始化异常");
    }
    return i;
}
// o 方法:
public static int o(long j, long j2) {
    if (2 == j2) {
        return 2;
    }
    if (1 == j) {
        return 3;
    }
    if (5 != j) {
        if (5 != j2) {
            if (3 != j) {
                if (4 != j2) {
                    return 1;
                }
            }
            return 4;
        }
    }
    return 5;
}

终于,终于,往下终于没有了,这个 o 方法就是最后一层了,它接收两个 long 参数,并返回 int 值,如果返回的是 2、3、5 则是 vip ,否则为非 vip。

梳理一下:yu1.d.v() => ah.e.o() 得到 int i ,i 传入 o01.d.d(i) 得到 isVip 为 true/false ,最终一层层返回到上层的业务逻辑。

2. userVipLevel 判断逻辑:

int u = (int) yu1.e.f().b().u(),相比之下,这个链式调用就朴素很多,没有那么多花花肠子。一层层最终来到了 yu1.d 类中的 u 方法:

public long u() {
    return this.a;
}

返回此类中的变量 a ,类型为 long,代表 vip 等级。


总结:

明明是要学习 frida 的实战,却发现难点根本不在 hook 代码的编写,而是逆向过程,如何找到 hook 点,以及一层一层寻找调用堆栈。所以请保持敏锐并不断累积经验,这才是提升技术的要点。

附上部分 frida 代码:

Java.perform(function() {

    // 1.hook isVip true
    var hookIsVip = Java.use('ah.e');
    hookIsVip.o.implementation = function(j,j2) {
        console.log('[*] Hook userIsVip success ; class:ah.e ; method:o');
        return 5
    }
    // 2.hook vipLevel 8
    var hookVipLevel = Java.use('yu1.d');
    hookVipLevel.u.implementation = function() {
        console.log('[*] Hook vipLevel=8 success ; class:yu1.d ; method:u');
        return 8
    }
})

顺便提醒:

用户信息:com.tencent.karaoke.karaoke_db_base.cachedata.user.UserInfoCacheData
演唱分数:com.tencent.karaoke.audiobasesdk.scorer.ScoreResult

为节省大家研究时间。