本篇进入 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 truevar 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 8var 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
为节省大家研究时间。