|
|
本帖最后由 最强AI选手 于 2026-5-16 23:09 编辑
上篇VIP教程发完发现一个问题:
绝大多数教程都是"搜这个字符串→改成1→完成", 看的人抄一遍
新版本类名一变又得重新求人
这次先讲思路 后给成品
想抄的拉到第七章
想学的从头看
============================================
一、99%的人都走过的弯路
============================================
我自己就走了
刚开始做K歌评分破解, 思路是:
1. 反编译看到 MultiScore 类有5个分数字段
2. hook它, 把字段都设成100
3. 录歌看效果 → 没生效
4. 再hook MultiScoreDetail / RecordingCalorieScoreData / kw2.a ...
5. 全部hook完依然不生效
为什么?
这些都是"下游"数据类, 真正的分数在更深的地方算出来后才"流"到这些类
hook下游 = 在水管末端拦水, 但水龙头一直在出新水, 拦不过来
============================================
二、真正要思考的问题
============================================
不是"在哪个类里把分数改成100"
而是"分数最初是在哪里产生的"
K歌评分的数据流向(任何评分类APP都类似):
麦克风采集音频 (PCM)
↓
JNI传给底层so做声学分析
↓
so算出原始分数 → 存到一个Java对象里 (★这里是源头)
↓
SDK暴露 getter 给业务层调
↓
业务层包装成各种业务数据类
↓
UI层从业务数据类读出来显示
99%的人在最后两层做hook, 我也是
正确做法在第3层 — SDK暴露getter的地方做hook
============================================
三、怎么找到"源头"
============================================
方法1: 看包名结构(最快)
K歌包名里有这俩:
com.tencent.karaoke.audiobasesdk.scorer.* ← 音频SDK评分包
com.tencent.karaoke.module.songedit.* ← 业务层
看到 audiobasesdk 这种命名, 一眼就知道是底层SDK, 优先扒它
方法2: 看so列表
解包APK看 lib/arm64-v8a/ 下哪些so, 关注:
libxxxScore.so / libxxxScorer.so
libxxxsing.so / libxxxKtv.so
这些native库往上一层就是Java SDK封装层
方法3: 看日志关键字
我抓到一条日志:
DefaultScoreImpl$setScoreResultCallback ... updateScoreResult
DefaultScoreImpl 这名字本身就告诉你"我是默认评分实现"
直接锁定这个类作为入口
============================================
四、找到核心类后看什么
============================================
com.tencent.karaoke.audiobasesdk.scorer.DefaultScoreImpl 字段方法:
字段:
result: ScoreResult ← ★最终结果对象
validScoreArray: [I ← ★有效句子分数数组
方法:
getScoreResult() ← getter (业务层从这取)
getValidScoreArray() ← getter
setScoreResultCallback(...) ← 注册回调
processWithBuffer(...) ← 实时处理音频
重点是 result 字段 → 它的类型 ScoreResult 才是真宝藏:
ScoreResult 暴露的所有 getter:
acfTotalScore() / pYinTotalScore() ← 总分(2套算法)
acfSentenceScores() / pYinSentenceScores() ← 每句分数数组
rhythmScore() / stableScore() / longtoneScore() / skillScore() ← 五维分
rhythmScoreArray() / stableScoreArray() / longtoneScoreArray() ← 每句五维
graceNum() / vibNum() / porNum() ← 装饰音/颤音
rap() ← 说唱判断
一切上层显示的分数都从这取
============================================
五、Hook策略
============================================
不要hook业务层 不要hook UI层
就hook ScoreResult 这个数据类的所有getter
策略:
- 所有 ()I 方法:
- 名字含 Total → 返回 句子数 × 100
- 其他单值分数 → 直接返回 100
- 所有 ()[I 方法:
- 拿到原数组, 遍历改成全100
- 不要新建数组, 不要改长度
============================================
六、关键坑
============================================
坑1: 别 hook 句子数
getValidSentenceNum() 这种别动
它是真实歌词句数, 改了UI显示会乱
坑2: byte[] 数据别动
getNewScores()[B 这种绕开
二进制结构, 字段位置hardcode偏移量, 改一个字节闪退
坑3: 别贪心改 Total
总分 = 句子数 × 单句分
返回 9999 但句子数是15, 后端比对会算异常
正确: 返回 句子数 × 100
坑4: 数组改值 vs 改引用
错误写法:
int[] arr = (int[]) param.getResult();
arr = new int[]{100, 100, ...}; ← 错!只改了局部变量
正确写法:
int[] arr = (int[]) param.getResult();
for (int i = 0; i < arr.length; i++) arr = 100; ← 对
============================================
七、成品代码 (想直接抄的看这里)
============================================
新建 ScoreHook.java 放到模块工程:
package com.kmod.karaoke;
import java.lang.reflect.Method;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XC_MethodReplacement;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;
public class ScoreHook {
public static final int SENTENCE_SCORE = 100;
public static void install(XC_LoadPackage.LoadPackageParam lpparam) {
ClassLoader cl = lpparam.classLoader;
try { hookScoreResult(cl); } catch (Throwable t) { XposedBridge.log("[ScoreHook] " + t); }
try { hookDefaultScoreImpl(cl); } catch (Throwable t) { XposedBridge.log("[ScoreHook] " + t); }
}
private static void hookScoreResult(ClassLoader cl) {
Class<?> clazz = XposedHelpers.findClass(
"com.tencent.karaoke.audiobasesdk.scorer.ScoreResult", cl);
for (Method m : clazz.getDeclaredMethods()) {
if (m.isSynthetic()) continue;
if (m.getParameterTypes().length != 0) continue;
Class<?> ret = m.getReturnType();
final String name = m.getName();
if (ret == int.class) {
XposedBridge.hookMethod(m, new XC_MethodReplacement() {
@Override
protected Object replaceHookedMethod(MethodHookParam param) {
if (name.contains("Total")) {
int sentences = getValidSentenceCount(param.thisObject);
return sentences * SENTENCE_SCORE;
}
return SENTENCE_SCORE;
}
});
} else if (ret == int[].class) {
XposedBridge.hookMethod(m, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
int[] arr = (int[]) param.getResult();
if (arr != null) {
for (int i = 0; i < arr.length; i++) arr = SENTENCE_SCORE;
}
}
});
}
}
}
private static int getValidSentenceCount(Object scoreResult) {
if (scoreResult == null) return 1;
try {
Method m = scoreResult.getClass().getDeclaredMethod("acfSentenceScores");
m.setAccessible(true);
Object arr = m.invoke(scoreResult);
if (arr instanceof int[]) {
int len = ((int[]) arr).length;
if (len > 0) return len;
}
} catch (Throwable ignore) {}
return 1;
}
private static void hookDefaultScoreImpl(ClassLoader cl) {
Class<?> clazz = XposedHelpers.findClass(
"com.tencent.karaoke.audiobasesdk.scorer.DefaultScoreImpl", cl);
for (Method m : clazz.getDeclaredMethods()) {
if (m.isSynthetic()) continue;
if (m.getParameterTypes().length != 0) continue;
if (m.getReturnType() != int[].class) continue;
XposedBridge.hookMethod(m, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) {
int[] arr = (int[]) param.getResult();
if (arr != null) {
for (int i = 0; i < arr.length; i++) arr = SENTENCE_SCORE;
}
}
});
}
}
}
在 MainHook.handleLoadPackage 里加一行调用:
try {
ScoreHook.install(lpparam);
} catch (Throwable t) {
XposedBridge.log("ScoreHook failed: " + t);
}
============================================
八、效果
============================================
旧版hook(业务层): C级 24分 / 170分
新版hook(SDK层): SSS级 4000+分 (40句歌词)
五维满分, 最大连击=句数
============================================
九、举一反三
============================================
这套思路适用于所有有"评分"概念的APP:
- 音乐识别评分
- 健身APP的卡路里/动作评分
- 游戏的得分
通用步骤:
1. 看包名 找带 sdk / engine / core 的包
2. 看类名 找 Default*Impl / *Result / *Bean
3. 看字段 找 result / data 这种"成品"字段
4. 看方法签名 优先 ()I / ()[I / ()F 这种纯getter
不要被那些"搜某个字符串改1"的教程牵着走
先理解APP数据从哪流到哪 再决定在哪打断
============================================
结尾
============================================
代码免费抄
但真心建议照思路自己重写一遍
下次类名一变, 抄的人傻眼, 学的人五分钟搞定
完
|
|