1. 项目概述
作为一名从事移动安全研究多年的工程师,我最近对一款流行的连连看单机游戏进行了逆向分析。这款游戏采用了Unity引擎开发,内置了金币系统、道具购买和广告展示等常见商业化设计。本文将详细记录如何通过逆向工程手段实现游戏内无限金币、无限道具和免广告功能的全过程。
2. 环境准备与工具链
2.1 硬件与系统环境
我的分析环境采用MacBook Pro (M1 Pro芯片)搭配macOS Monterey 12.7.6系统。选择macOS主要考虑到其Unix-like环境对开发者友好,且与Android开发工具链兼容性好。实际测试发现,虽然目标游戏是ARM架构的Android应用,但在Intel芯片的Mac上通过Rosetta 2转译运行各类逆向工具也没有性能问题。
2.2 核心工具选型
逆向分析需要一套完整的工具链支持,以下是经过实战验证的工具组合:
-
静态分析工具:
- IDA Pro 9.2:反汇编核心so文件,进行控制流分析
- JADX 1.5.3:反编译APK中的Java代码
- ILSpy 7.2:查看Unity的C#脚本逻辑
-
动态分析工具:
- Frida 16.7.19:运行时hook和内存修改
- GameGuardian:内存搜索与修改(辅助验证用)
-
Unity专项工具:
- Il2CppDumper 6.7.46:提取Unity游戏的符号信息
- AssetRipper 0.0.0a0:解包Unity资源文件
工具选择经验:对于Unity游戏,Il2CppDumper+IDA+Frida的组合已经能解决90%的逆向需求。如果游戏使用Mono编译,则需要更多依赖.NET反编译工具。
3. 游戏逆向实战
3.1 无限金币实现
3.1.1 金币存储机制分析
首先解压APK文件,通过查看lib目录确认这是基于IL2CPP编译的Unity游戏。使用Il2CppDumper导出符号信息后,在IDA中定位到金币相关的关键函数:
c复制__int64 __fastcall MyApp_ProfileManager__set_Money(__int64 a1, int a2)
{
if ( a2 >= 999999 )
a2 = 999999;
v3 = MyClasses_MyEncryptedNumber__EncryptInt(a2);
UnityEngine_PlayerPrefs__SetInt("Money", v3);
UnityEngine_PlayerPrefs__Save();
}
这段代码揭示了三个重要信息:
- 金币上限被硬编码为999999
- 数值经过MyEncryptedNumber类加密后才存储
- 最终通过PlayerPrefs保存到本地
3.1.2 加密算法逆向
继续分析加密函数:
c复制__int64 __fastcall MyClasses_MyEncryptedNumber__EncryptInt(int a1)
{
return *(_DWORD *)(MyClasses_MyEncryptedNumber_TypeInfo + 0xB8) ^ ~a1;
}
加密逻辑是:先对原始值取反,再与一个固定seed异或。通过Frida hook可以获取到这个seed值:
javascript复制function dumpEncryptSeed() {
let typeInfo = Module.getExportByName("libil2cpp.so",
"MyClasses_MyEncryptedNumber_TypeInfo");
let seed = Memory.readS32(ptr(typeInfo).add(0xB8));
console.log("Encryption Seed:", seed);
}
// 输出:Encryption Seed: 616
3.1.3 Frida Hook实现
基于上述分析,编写Frida脚本hook金币设置函数:
javascript复制Interceptor.attach(Module.getExportByName("libil2cpp.so",
"MyApp_ProfileManager__set_Money"), {
onEnter: function(args) {
console.log("Original value:", args[1].toInt32());
args[1] = ptr(999999); // 强制设置为最大值
}
});
关键点说明:
- 需要在游戏触发金币变化时才会调用此函数
- 修改后立即生效,无需重启游戏
- 存档中的加密值也会相应更新
3.2 无限道具实现
3.2.1 道具系统分析
游戏中有三种道具:火箭(清除一行)、灯泡(提示配对)、加时(延长时间)。通过IDA搜索字符串"Rocket",找到道具数量相关的函数:
c复制int __fastcall MyApp_Rocket__get_Count(__int64 this)
{
return *(int *)(this + 0x18);
}
3.2.2 双重Hook策略
单纯修改数量获取函数还不够,因为游戏还会校验道具实际消耗。需要同时hook两个关键点:
javascript复制// Hook数量获取
Interceptor.replace(Module.getExportByName("libil2cpp.so",
"MyApp_Rocket__get_Count"),
new NativeCallback(function() {
return 99; // 总是返回99
}, 'int', []));
// Hook消耗逻辑
Interceptor.attach(Module.getExportByName("libil2cpp.so",
"MyApp_ItemButton__Use"), {
onEnter: function(args) {
let countPtr = args[0].add(0x44);
countPtr.writeS32(99); // 阻止数量减少
}
});
3.2.3 使用限制破解
测试发现即使道具显示99个,每局游戏仍有使用次数限制。通过分析发现游戏在BoardView类中维护了局内使用计数,需要额外hook:
javascript复制let boardView = Memory.readPointer(
Module.findBaseAddress("libil2cpp.so").add(0x123456));
Memory.writeS32(boardView.add(0x60), 0); // 重置使用计数
3.3 免广告实现
3.3.1 广告逻辑定位
在IDA中搜索字符串"noads",找到关键检测函数:
c复制bool MyApp_ProfileManager__get_IsNoAds()
{
return PlayerPrefs.GetInt("NoAds", 0) == 1;
}
3.3.2 永久去广告方案
通过Frida修改函数返回值:
javascript复制Interceptor.replace(Module.getExportByName("libil2cpp.so",
"MyApp_ProfileManager__get_IsNoAds"),
new NativeCallback(function() {
return 1; // 总是返回true
}, 'bool', []));
更彻底的方案是直接修改PlayerPrefs的持久化值:
javascript复制let noAdsKey = Memory.allocUtf8String("NoAds");
UnityEngine_PlayerPrefs__SetInt(noadsKey, 1);
UnityEngine_PlayerPrefs__Save();
4. 游戏核心机制解析
4.1 棋盘数据结构
通过hook游戏逻辑,可以dump出棋盘的内存结构:
code复制Row 0: [4,0] [7,0] [7,0] [6,0]...
Row 1: [6,0] [8,0] [8,0] [2,0]...
...
数据结构特点:
- 6行×9列的二维数组
- 每个元素包含图案ID和锁定状态
- 行号从下往上递增,列号从左往右递增
- 0表示未锁定,1表示锁定状态
4.2 配对算法分析
灯泡道具的实现揭示了游戏的核心算法:
c复制bool MyApp_BoardView__IsHasValidPair(BoardView *this)
{
// 遍历所有可能的图案组合
// 检查是否存在可达路径
}
路径检查算法主要考虑三个因素:
- 直线连接(0转角)
- 单拐角连接(1转角)
- 双拐角连接(2转角)
4.3 自动化脚本设计
基于上述分析,可以设计自动解题算法:
python复制def find_pairs(board):
for i in range(len(board)):
for j in range(len(board[0])):
if board[i][j] == 0: continue
# 搜索匹配项
# 检查路径可达性
return (i1,j1), (i2,j2)
5. 高级技巧与异常处理
5.1 反调试对抗
部分游戏会检测调试器,常见对抗手段包括:
javascript复制// 绕过ptrace检测
Interceptor.replace(Module.getExportByName("libc.so", "ptrace"),
new NativeCallback(function() {
return 0;
}, 'int', ['int', 'int', 'int', 'int']));
// 屏蔽调试端口检测
Interceptor.replace(Module.getExportByName("libc.so", "fopen"),
new NativeCallback(function(path, mode) {
if (path.readUtf8String().includes("/proc/self/status"))
return NULL;
return original_fopen(path, mode);
}, 'pointer', ['pointer', 'pointer']));
5.2 异常处理策略
游戏可能会崩溃的情况处理:
-
版本兼容性:
- 通过Module.enumerateImports()检查函数偏移
- 准备多版本hook方案
-
错误恢复:
javascript复制Process.setExceptionHandler(function(exception) { console.warn("Crash intercepted:", exception); return true; // 阻止崩溃 }); -
延迟注入:
javascript复制setTimeout(function() { if(!isHooked) retryHook(); }, 3000);
6. 游戏机制扩展思路
现代连连看游戏已经发展出多种创新玩法:
-
动态难度调整:
csharp复制void UpdateDifficulty() { float speed = 1.0f + score * 0.01f; Time.timeScale = Mathf.Clamp(speed, 1, 3); } -
社交功能集成:
- 微信分享得分
- 好友排行榜
- 道具赠送系统
-
跨平台数据同步:
java复制public class CloudSave { void sync() { // 使用Firebase同步存档 } }
7. 法律与道德考量
需要特别注意的是:
-
合法使用原则:
- 仅用于学习研究
- 不破坏在线游戏平衡
- 不进行商业获利
-
技术防御建议:
- 关键逻辑放在Native层
- 使用自定义加密算法
- 增加完整性校验
-
道德边界:
- 尊重开发者劳动成果
- 不传播破解成果
- 支持正版游戏
通过这次逆向工程实践,我不仅深入理解了Unity游戏的安全机制,也对移动游戏的商业化设计有了更深刻的认识。技术研究应该以促进产业健康发展为目的,希望开发者能通过这些案例提升游戏的安全性,同时也为玩家提供更友好的游戏体验。