上一篇帖子:飞车手游分析 & 加速,秒通关实现
大家对我上次的使用修改器修改游戏的帖子似乎比较感兴趣,本帖将带大家深入分析这个飞车小游戏并且编写快捷易用的Xposed模块
上一篇帖子我们简单使用修改器修改了这个游戏,但是碰到了切屏就失效,搜索数据浪费时间等问题,这篇帖子我们使用Xposed模块来解决这些问题,成品放在附件里面了
最终效果:速度锁400,积分赛66666分
582K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1x3U0y4Q4x3X3g2@1N6W2)9J5c8X3W2K9N6f1y4Y4z5r3p5`.
上一期我们找到了这个游戏的速度地址,但是每一局都要重新搜索,修改,有时候一局可能还需要搜索修改多次,会显得很麻烦。
本期的受害者依旧是 ↓↓↓
AmazingGame!!!

我们提到过一个想法就是Hook撞墙的代码来获取对象数据,但是这里的主要撞墙逻辑并不是传入实例后调方法的形式,所以想Hook方法参数,就只能hook玩家之间的碰撞了
在Java层我们是无法像Native层一样下访问断点追踪的,因为Java为了安全性避免了直接的内存操作,而且是运行环境也是ART虚拟机,即使下断点,访问点也都是在Code System段,所以很难通过地址定位类信息,而且搜了几天几乎没找到任何相关的追踪手段,根据我的思考和探索,有以下初步的思路
各位师傅有更好的办法浇浇我
没有进行Java层的混淆,比较适合萌新体质()
我们首先尝试搜索wall,speed,player等关键信息,这里我选择从KeyEvent入手
在Android开发中android.view.KeyEvent为玩家操作游戏提供了支持,也会为我们提供一些关键信息
我们在这里的回调函数中很轻松的找到了对我们对象状态的判断,很明显我们的游戏对局对象是inGame,其中存储着我们在这局游戏中的状态等信息

还发现了可爱的flag(?)

另一个方法也印证了这一点

我们可以进入到InGame这个类看看,发现了我们上次对内存中状态码的初步判断是正确的
发现了public static Vaisseau player = new Vaisseau();,结合后面的代码,说明玩家是一个Vaisseau类的实例

我们的目标是获取速度信息,我们在类里面搜索speed找到了对应的信息,这似乎是一个列表,这里设置了敌人的defaultParams(默认参数)我们修改之后发现敌人不能动了,不过这不是我们分析的重点

结合游戏玩法,很明显这是根据敌人选择不同的基础速率,而且float类型也符合我们对速度的判断

但是这并不是我们想要的数据,我们继续分析,来到了这里,发现这里是我们游戏角色运动的逻辑,都在doPhysics方法中
玩家加速逻辑(这里可以看出来player.param[3]是角色速度上限)

玩家之间碰撞的逻辑检测,其中是在扣除vzb这个变量数据,所以我们合理认为vzb是速度信息

所以接下来我们来到角色类(Vaisseau类)实例中,通过查找速度的交叉引用寻找到了碰撞逻辑

第二关和第五关也找到判断玩家是否在赛道上的逻辑了

这里是玩家的速度变化逻辑

这里分析了一下游戏的vxb,vyb,vzb,三者是xyz三个方向的速度

到这里其实我们关于游戏的基本信息都分析出来了,这是最直接最暴力的方法,虽然在大型,有混淆的游戏中花费的时间成本会大幅提高,但能应对各种乱七八糟的情况
其实到这里我们发现我们使用monitor并不可行,因为这里的撞墙并不是通过调用一个方法实现的,而是直接进行if判断后加减,我们如果尝试这种方法,很有可能会被误导到各种角度坐标计算方法中去,从而无法自拔,所以这只能作为一种技巧使用
实现不了的关键点在于无法获取类实例的地址,也没有找到相关的api
来来回回研究了半天,不会获取类实例的地址,寄了,半成品脚本放在这里,师傅们有建议的话可以提一下,谢谢喵
注入就崩了,可能是解析方法错了?也中道崩殂了
Java虚拟机如果创建了这些类,肯定是需要一个标识用于管理他们的,但是单纯的使用Java Heap Dump的话我们很难确定实例的头部究竟在哪里,但是GGlua脚本就为我们很好的解决了这个问题
我们使用Android Studio的Profiler这个工具来dump堆信息
官方文档
首先根据上一篇帖子,我们知道了这个值在Java Heap中的地址

首先我们要思考一个Java Heap上的对象,系统是如何判断它属于哪个类呢?其实就是根据对象头部的指针
就像身份证上的“籍贯”,告诉JVM这个对象是哪个类的实例
Java的对象头:原理与源码详解
所以我们在这里可以看到类实例头部,会有一个指针,这个指针是指向J内存的
J (JIT Memory)
所以我们可以借助这一点来使用GGlua来为我们自动确定类的头部信息

我们使用GG修改器执行如下GGlua脚本
我们在/storage/emulated/0/JSnow_FindClass.txt中可以得到类实例头部的地址
然后我们使用Android Studio自带的Profiler功能将此时的Java Heap整个Dump下来

根据官方文档,我们需要使用SDK工具将 .hprof 文件从 Android 格式转换为 Java SE .hprof 文件格式
接下来使用MAT(MemoryAnalyzer)工具分析我们输出的堆转储文件
我们根据GGlua脚本输出得到的地址搜索,很快就在找到了这个类,然后我们就可以直接快速定位关键类进行Java层分析了,相比我们在Java层分析下来这种方法的效率明显更高

所以我们可以简单使用frida写出一个作弊脚本,这里只是简单试一下效果,所以没有优化
如果-UF指定顶层会导致hook的是修改器
hook发现的确有效,说明我们寻找的是正确的,但是我们要寻找合适的Hook点,因为这种方法会导致所有赛车都加速,达不到获胜的目的
但是我们不可能每次打开游戏都启动frida,连接usb,会显得比较麻烦,而且连着个电脑打游戏总会不得劲(但是听说有一个叫算法助手的app可以便捷启动frida脚本)
我们新建项目之后,需要新建lib目录**(不是libs)**下加入我们的Xposed的jar包,为我们提供代码补全,方便我们调用api,然后在main目录下新建assets目录,新建xposed_init文件,这里用于声明Xposed的入口点,我这里是com.JSnow.cheatmodule.XposedCheatModule,所以就向其中添加com.JSnow.cheatmodule.XposedCheatModule这一串内容就可以了

为了减小app体积,我们需要在build.gradle文件中添加compileOnly,让jar包不被打包到我们的app中

然后我们就可以在我们指定的入口点开始写Hook了

分析了一圈,尝试了不少Hook点,感觉还是绘制的方法Hook起来体验最好,而且player对象是没有看到作为参数传递到某个方法中的,所以就只能利用反射获取这个实例了
现在大家都喜欢用Lsposed模块,Lsposed模块是兼容了Xposed模块的,加载后记得勾选游戏和系统框架
因为xposed的原理是替换系统的app_process文件,所以不重启是无法生效的
核心代码:
最终效果:6a0K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6T1x3U0y4Q4x3X3g2@1N6W2)9J5c8X3W2K9N6f1y4Y4z5r3p5`.
(其它地方都不能点击即看,最后还是b站收留了我的视频)
成品和源码都打包了一份,感兴趣的师傅可以拿来玩玩,游戏本体下载:
a6fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6o6g2p5k6Q4x3X3c8m8M7X3y4Z5K9i4k6W2M7#2)9J5c8U0t1H3x3U0c8Q4x3X3c8F1k6i4N6K6N6r3q4J5i4K6u0r3M7X3g2D9k6h3q4K6k6i4y4Q4x3V1k6V1L8%4N6F1L8r3!0S2k6q4)9J5c8Y4j5I4i4K6u0W2x3q4)9J5k6e0m8Q4x3V1k6F1M7K6t1@1i4K6g2X3L8h3W2K6j5#2)9J5k6i4A6A6M7l9`.`.
function find_obj(){
Java.perform(function() {
var targetAddress = ptr("0x1390013c");
console.log("目标地址: " + targetAddress);
console.log("开始搜索类实例...");
var processedClasses = new Set();
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (!className.startsWith("net.osaris") ||
!className.startsWith("com.pangbai.") ||
!className.startsWith("adrt")
) {
return;
}
try {
if (processedClasses.has(className)) return;
processedClasses.add(className);
var javaClass = Java.use(className);
Java.choose(className, {
onMatch: function(instance) {
try {
console.log("检查实例: " + instance);
console.log("addr: " + instance.$handle);
var instanceAddr = ptr(instance.$handle);
console.log("实例地址: " + instanceAddr);
var maxObjectSize = 1024;
var minAddr = instanceAddr;
var maxAddr = instanceAddr.add(maxObjectSize);
if (targetAddress.compare(minAddr) >= 0 &&
targetAddress.compare(maxAddr) <= 0) {
console.log("找到可能匹配的对象!");
console.log("类名: " + className);
console.log("实例地址: " + instanceAddr);
console.log("字段可能偏移量: " + targetAddress.sub(instanceAddr));
console.log("对象信息: " + instance.toString());
var clazz = instance.getClass();
var fields = clazz.getDeclaredFields();
console.log("类字段数量: " + fields.length);
for (var i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
console.log("字段: " + fields[i].getName() +
", 类型: " + fields[i].getType());
}
}
} catch (e) {
console.error("实例检查错误: " + e);
}
return "continue";
},
onComplete: function() {
}
});
} catch (e) {
console.error("类处理错误: " + e);
}
},
onComplete: function() {
console.log("所有类搜索完成");
}
});
console.log("搜索已启动,等待结果...");
});
}
setTimeout(find_obj, 1000);
function find_obj(){
Java.perform(function() {
var targetAddress = ptr("0x1390013c");
console.log("目标地址: " + targetAddress);
console.log("开始搜索类实例...");
var processedClasses = new Set();
Java.enumerateLoadedClasses({
onMatch: function(className) {
if (!className.startsWith("net.osaris") ||
!className.startsWith("com.pangbai.") ||
!className.startsWith("adrt")
) {
return;
}
try {
if (processedClasses.has(className)) return;
processedClasses.add(className);
var javaClass = Java.use(className);
Java.choose(className, {
onMatch: function(instance) {
try {
console.log("检查实例: " + instance);
console.log("addr: " + instance.$handle);
var instanceAddr = ptr(instance.$handle);
console.log("实例地址: " + instanceAddr);
var maxObjectSize = 1024;
var minAddr = instanceAddr;
var maxAddr = instanceAddr.add(maxObjectSize);
if (targetAddress.compare(minAddr) >= 0 &&
targetAddress.compare(maxAddr) <= 0) {
console.log("找到可能匹配的对象!");
console.log("类名: " + className);
console.log("实例地址: " + instanceAddr);
console.log("字段可能偏移量: " + targetAddress.sub(instanceAddr));
console.log("对象信息: " + instance.toString());
var clazz = instance.getClass();
var fields = clazz.getDeclaredFields();
console.log("类字段数量: " + fields.length);
for (var i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
console.log("字段: " + fields[i].getName() +
", 类型: " + fields[i].getType());
}
}
} catch (e) {
console.error("实例检查错误: " + e);
}
return "continue";
},
onComplete: function() {
}
});
} catch (e) {
console.error("类处理错误: " + e);
}
},
onComplete: function() {
console.log("所有类搜索完成");
}
});
console.log("搜索已启动,等待结果...");
});
}
setTimeout(find_obj, 1000);
function find_obj()
{
Java.perform(function() {
var fieldAddress = ptr("0x134EB318");
console.log("已知字段地址: " + fieldAddress);
var searchRange = 256;
for (var offset = 0; offset < searchRange; offset += 8) {
var potentialHeaderAddr = ptr(fieldAddress).sub(offset);
try {
var value = Memory.readPointer(potentialHeaderAddr);
console.log(potentialHeaderAddr + " -> " + value);
try {
var possibleClass = Java.cast(value, Java.use("java.lang.Class"));
console.log("可能的类引用: " + potentialHeaderAddr + " -> " + possibleClass.getName());
console.log("找到可能的对象头部,距离字段偏移: " + offset + " 字节");
} catch(e) {
try {
var possibleObj = Java.cast(value, Java.use("java.lang.Object"));
console.log("可能的对象引用: " + potentialHeaderAddr + " -> " + possibleObj.getClass().getName());
} catch(e2) {
console.log(potentialHeaderAddr + " -> [无法转换为对象或类]");
}
}
} catch(e) {
console.log(potentialHeaderAddr + " -> [无法读取]");
}
}
});
}
setTimeout(find_obj, 1000);
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2025-10-9 12:35
被JSnow编辑
,原因: