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