飞车小游戏XPosed模块实战

AmazingGame分析

初步思路

上一期我们找到了这个游戏的速度地址,但是每一局都要重新搜索,修改,有时候一局可能还需要搜索修改多次,会显得很麻烦。

本期的受害者依旧是 ↓↓↓

AmazingGame!!!

7b9672763af7e980e1436c64e12c04d6

我们提到过一个想法就是Hook撞墙的代码来获取对象数据,但是这里的主要撞墙逻辑并不是传入实例后调方法的形式,所以想Hook方法参数,就只能hook玩家之间的碰撞了

在Java层我们是无法像Native层一样下访问断点追踪的,因为Java为了安全性避免了直接的内存操作,而且是运行环境也是ART虚拟机,即使下断点,访问点也都是在Code System段,所以很难通过地址定位类信息,而且搜了几天几乎没找到任何相关的追踪手段,根据我的思考和探索,有以下初步的思路

  • 直接分析Java层代码
    • 优点:能够应对各种奇奇怪怪的问题
    • 缺点:时间成本高,在大型游戏中难度大
  • 使用Android Studio附带SDK tools提供的monitor工具分析函数调用,来找到撞墙逻辑代码分析
    • 优点:分析速度快,能够快速定位关键方法
    • 缺点:如果撞墙不是一个方法来实现的,那就会被误导到一大堆乱七八糟的方法里面去
  • Frida通过遍历前后地址,尝试解析类,或者枚举所有实例信息后进行地址比对
    • 优点:快,便捷
    • 缺点:我不会
  • Java Heap Dump + GGlua
    • 优点:相比其他方法优点非常明显,速度快,便捷(算是frida想法手动实现)

各位师傅有更好的办法浇浇我

类的寻找

Java层逆向分析

没有进行Java层的混淆,比较适合萌新体质()

我们首先尝试搜索wallspeedplayer等关键信息,这里我选择从KeyEvent入手

在Android开发中android.view.KeyEvent为玩家操作游戏提供了支持,也会为我们提供一些关键信息

我们在这里的回调函数中很轻松的找到了对我们对象状态的判断,很明显我们的游戏对局对象是inGame,其中存储着我们在这局游戏中的状态等信息

image-20250919083927763

还发现了可爱的flag(?)

image-20250919084322270

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

image-20250919084050627

我们可以进入到InGame这个类看看,发现了我们上次对内存中状态码的初步判断是正确的

发现了public static Vaisseau player = new Vaisseau();,结合后面的代码,说明玩家是一个Vaisseau类的实例

image-20250919084632580

我们的目标是获取速度信息,我们在类里面搜索speed找到了对应的信息,这似乎是一个列表,这里设置了敌人的defaultParams(默认参数)

image-20250919085310726

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

image-20250919085502553

但是这并不是我们想要的数据,我们继续分析,来到了这里,发现这里是我们游戏角色运动的逻辑,都在doPhysics方法中

玩家加速逻辑(这里可以看出来player.param[3]是角色速度上限)

image-20250919103924765

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

image-20250919095826022

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

image-20250919102423369

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

image-20250919102648340

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

image-20250919103738150

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

image-20250919103411033

到这里其实我们关于游戏的基本信息都分析出来了,这是最直接最暴力的方法,虽然在大型,有混淆的游戏中花费的时间成本会大幅提高,但能应对各种乱七八糟的情况

monitor工具分析函数调用(不可行)

其实到这里我们发现我们使用monitor并不可行,因为这里的撞墙并不是通过调用一个方法实现的,而是直接进行if判断后加减,我们如果尝试这种方法,很有可能会被误导到各种角度坐标计算方法中去,从而无法自拔,所以这只能作为一种技巧使用

Frida搜索类实例(未实现)

获取所有类实例的地址比对

实现不了的关键点在于无法获取类实例的地址,也没有找到相关的api

来来回回研究了半天,不会获取类实例的地址,寄了,半成品脚本放在这里,师傅们有建议的话可以提一下,谢谢喵

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
function find_obj(){
Java.perform(function() {
// 我们使用修改器获得到的Java实例中速度字段的地址
var targetAddress = ptr("0x1390013c");
console.log("目标地址: " + targetAddress);

// 获取当前加载的所有类
console.log("开始搜索类实例...");

var processedClasses = new Set();

// 使用Java.enumerateLoadedClasses获取所有已加载类
Java.enumerateLoadedClasses({
onMatch: function(className) {
// 只搜索App内的类,避免系统类
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);

尝试在附近解析类

注入就崩了,可能是解析方法错了?也中道崩殂了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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);

Java Heap Dump + GGlua定位

Java虚拟机如果创建了这些类,肯定是需要一个标识用于管理他们的,但是单纯的使用Java Heap Dump的话我们很难确定实例的头部究竟在哪里,但是GGlua脚本就为我们很好的解决了这个问题

我们使用Android Studio的Profiler这个工具来dump堆信息

官方文档

首先我们知道了这个值在Java Heap中的地址

image-20250920095208805

GGlua脚本寻找信息

首先我们要思考一个Java Heap上的对象,系统是如何判断它属于哪个类呢?其实就是根据对象头部的指针

就像身份证上的“籍贯”,告诉JVM这个对象是哪个类的实例

Java的对象头:原理与源码详解

所以我们在这里可以看到类实例头部,会有一个指针,这个指针是指向J内存的

J (JIT Memory)

  • 描述: JIT(Just-In-Time)编译生成的内存区域,通常用于动态编译和执行代码。
  • 用途: 存储动态生成的可执行代码,特别是Java或Android应用的JIT编译结果。
  • 特点: 可执行代码,可能是读写混合属性。

所以我们可以借助这一点来使用GGlua来为我们自动确定类的头部信息

image-20250920101506242

我们使用GG修改器执行如下GGlua脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
function main()
local m={
'Find ClassAddr with address',
'Exit Script'
}
local g=gg.choice(m,nil,'Find ClassAddr Script')
if g==1 then F1() end
if g==2 then os.exit() end
xgck=-1
end

function F1()
local input = gg.prompt({"请输入十六进制地址(无需输入0x):"},{""},{"text"})
if input == nil then return end -- 结束脚本
local base_addr = tonumber(input[1], 16)
if base_addr == nil then
gg.toast("无效地址,请输入有效的十六进制地址")
return
end

gg.toast(string.format("Base Address: 0x%X", base_addr))

--每次减少4个字节,依次向上寻找,并检查指针是否指向Java内存区域
local found = false
-- 这里设置0x200一般够用了,可以根据需求修改这里
for i=0,0x200,4 do
--读取该地址处的值
local curr_addr = base_addr - i
local result = gg.getValues({{address = curr_addr, flags = gg.TYPE_QWORD}})
local pointer = result[1].value


--检查指针是否在Java内存区域内
local inJavaRegion = false

-- 创建一个包含 pointer 地址的表
local pointerInfo = {{address = pointer, flags = gg.TYPE_DWORD}}
-- 获取内存区域
local valueRange = gg.getValuesRange(pointerInfo)
if valueRange ~= nil then
-- 检查是否为Java区域
if valueRange[1] == "J" then
gg.toast(string.format("找到Java Class! Java Heap地址: 0x%X 指向: 0x%X", curr_addr, pointer))
gg.sleep(1000)
-- 将得到的curr_addr写入到/storage/emulated/0/JSnow_FindClass.txt文件中
local file = io.open("/storage/emulated/0/JSnow_FindClass.txt", "a")
if file then
file:write(string.format("0x%X\n", curr_addr))
file:close()
gg.toast("地址已保存到 /storage/emulated/0/JSnow_FindClass.txt")
gg.sleep(1000)
else
gg.toast("无法打开文件进行写入")
gg.sleep(1000)
end

found = true
inJavaRegion = true
else
gg.toast(string.format("指针在非Java区域 (%s)", valueRange[1]))
end
end
end

if not found then
gg.toast("未找到指向Java区域的指针")
end
end

--主循环
xgck = -1
while true do
if gg.isVisible(true) then
xgck=1
--gg.setVisible(false) -- 隐藏界面游戏会持续运行,不方便我们提取数据
end
if xgck==1 then
main()
xgck = -1
end
gg.sleep(100)
end

我们在/storage/emulated/0/JSnow_FindClass.txt中可以得到类实例头部的地址

Java Heap Dump

然后我们使用Android Studio自带的Profiler功能将此时的Java Heap整个Dump下来

image-20250920095741942

根据官方文档,我们需要使用SDK工具将 .hprof 文件从 Android 格式转换为 Java SE .hprof 文件格式

1
hprof-conv .\memory-20250920T100450.hprof dest.hprof

接下来使用MAT(MemoryAnalyzer)工具分析我们输出的堆转储文件

我们根据GGlua脚本输出得到的地址搜索,很快就在找到了这个类,然后我们就可以直接快速定位关键类进行Java层分析了

image-20250920101307293

Frida Hook测试

所以我们可以简单使用frida写出一个作弊脚本,这里只是简单试一下效果,所以没有优化

如果-UF指定顶层会导致hook的是修改器

1
frida -U -l ./hook.js -n AmazingGame 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function hook(rounds, modules)
{
var i = 0
while (i < rounds) {
i += 1
Java.perform(function () {
var classes = Java.enumerateLoadedClassesSync();
var targets = classes.filter(function (cls) {
// 过滤出我们的游戏角色实例类
return cls.startsWith("net.osaris.turbofly.models.Vaisseau")
});

console.log("匹配到的类及其实例:");
targets.forEach(function (clsName) {
try {
Java.choose(clsName, {
// 每找到一个实例就会调用这个回调函数,修改所有的实例的 vzb 字段值
onMatch: function (instance) {
console.log("找到实例: " + instance.toString());
if (modules == 1) {
instance.vzb.value = 20; // 修改 vzb 字段的值
console.log("vzb 值: " + instance.vzb.value);
}
else if (modules == 2) {
instance.vyb.value = 20;
console.log("vyb 值: " + instance.vyb.value);
}
else if (modules == 3) {
instance.vzb.value = 20;
instance.vyb.value = 20;
console.log("vzb 值: " + instance.vzb.value);
console.log("vyb 值: " + instance.vyb.value);
}
},
onComplete: function () {
console.log(clsName + " 实例枚举完成");
}
});
} catch (e) {
console.log("无法处理类: " + clsName + ",原因: " + e);
}
});
});
}
}

// 参数填写我们想hook的次数
// 因为游戏中这个实例的速度会被不断修改,而while(true)会导致hook无法退出
// 1是加速,2是飞天,3是两者都有
setTimeout(hook(10, 1), 1000);

hook发现的确有效,说明我们寻找的是正确的,但是我们要寻找合适的Hook点,因为这种方法会导致所有赛车都加速,达不到获胜的目的

但是我们不可能每次打开游戏都启动frida,连接usb,会显得比较麻烦,而且连着个电脑打游戏总会不得劲(但是听说有一个叫算法助手的app可以便捷启动frida脚本)

编写Xposed模块

我们新建项目之后,需要新建lib目录**(不是libs)**下加入我们的Xposed的jar包,为我们提供代码补全,方便我们调用api,然后在main目录下新建assets目录,新建xposed_init文件,这里用于声明Xposed的入口点,我这里是com.JSnow.cheatmodule.XposedCheatModule,所以就向其中添加com.JSnow.cheatmodule.XposedCheatModule这一串内容就可以了

image-20251006194412561

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

image-20251006194653288

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

image-20251006194834795

分析了一圈,尝试了不少Hook点,感觉还是绘制的方法Hook起来体验最好,而且player对象是没有看到作为参数传递到某个方法中的,所以就只能利用反射获取这个实例了

现在大家都喜欢用Lsposed模块,Lsposed模块是兼容了Xposed模块的,加载后记得勾选游戏和系统框架

核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.JSnow.cheatmodule;


import de.robv.android.xposed.IXposedHookLoadPackage;
import de.robv.android.xposed.XC_MethodHook;
import de.robv.android.xposed.XposedBridge;
import de.robv.android.xposed.XposedHelpers;
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class XposedCheatModule implements IXposedHookLoadPackage {
static int state;
static Object player;
@Override
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if(loadPackageParam.packageName.equals("com.pangbai.projectm")){

XposedHelpers.findAndHookMethod("net.osaris.turbofly.InGame", loadPackageParam.classLoader, "drawFrame", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 通过反射获取InGame对象的字段
player = XposedHelpers.getObjectField(param.thisObject, "player");
XposedBridge.log("player:" + player);
XposedBridge.log("player vzb:" + XposedHelpers.getObjectField(player, "vzb"));
// 修改分数和速度字段的值
// 这里本来是打算加状态判断的,但是会影响切屏后的生效,所以就不加了
XposedHelpers.setObjectField(player, "vzb", 2.0f);
XposedHelpers.setObjectField(param.thisObject, "scoreInfini", (int)666666);
// log信息
XposedBridge.log("player vzb:" + XposedHelpers.getObjectField(player, "vzb"));
}
});

XposedHelpers.findAndHookMethod("net.osaris.turbofly.InGame", loadPackageParam.classLoader, "getState", new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 监听游戏状态
if(state != (int)param.getResult() && state <= 3) {
state = (int) param.getResult();
XposedBridge.log("State:" + state);
}
}
});
}


}
}

项目代码和编译好的模块都打包了一份,感兴趣的师傅可以玩玩
AmazingCheat.apk
CheatModule.zip