love 2d 介绍
LÖVE是一个使用 Lua 作为编程语言的 2D 游戏框架,网络上关于此引擎的修改教程还是相对较少。此次下手的目标为王国保卫战,这个手游已经有很多破解版本。这里还是记录一下我自己的摸索过程,从定位关键代码到修改。
1. 定位资源文件
一般手游的程序逻辑都是以脚本的形式存储在apk的资源文件中,这次目标apk是从 google play 下载,从 /data/app 目录中pull出apk,得到apk的解压结果如下:
base.apk
├── AndroidManifest.xml
├── META-INF
├── assets
├── classes.dex
├── error_prone
├── fabric
├── jsr305_annotations
├── lib
├── res
├── resources.arsc
└── third_party
看了一下大小,才9.5m大小,zip包中没发现资源文件:
ls -l base.apk
-rw-r--r--@ 1 max admin 9469378 base.apk
这就很奇怪了,看了一下空间占用了近 200m ,而且 /data/data 目录下也没有找到关键资源文件:
不过还是在一个名为 DownloadsDB 数据库文件中发现了一个下载链接
看到google的url,我怀疑是Play Store的特殊机制,因此在国内的应用中较为少见。网上查了一下,果不其然,这应该就是 obb 文件了,用于资源文件分发,减小apk体积。
这样应用更新的时候就不必重新下载整个apk, 同时,google 的服务器在国外的稳定性与速度都不差,也可以为开发者节省部分cdn费用。打开压缩包,果不其然,里面存放着游戏脚本,大小也有100m多,这就正好可以对上号了:
2. 定位关键代码
根据上一步资源文件的解压结果来看,这些 lua 脚本应该就是程序逻辑了。打开一个lua文件,先看看hex,为 luajit 文件:
使用 luajit-decompiler (https://gitlab.com/znixian/luajit-decompiler) 把 luajit 文件反编译成明文,使用方法如下:
python3 ./main.py --recursive ./<input directory> --dir_out ./<output directory> --catch_asserts
这样就可以得到明文 lua 脚本:
手游里,想要定位钻石有关代码,一般都是搜索 gem 关键字,搜索到一个可疑文件 platform_services_ads.lua ,代码如下:
--- 观看广告时会调用
function ads:show_video_ad(provider, reward_amount)
local function cb_show_video_ad(status, req)
local success = status == 0
if success then
log.debug("ad complete: rewarding %s gems", reward_amount)
--- 获取存档中的物品
local slot = storage:load_slot()
if slot then
--- 观看广告后所得钻石增加 reward_amount 个
slot.gems = slot.gems + reward_amount
--- 把增加后的钻石再保存到存档中
storage:save_slot(slot, nil, true)
else
log.error("error giving gems reward. slot could not be loaded")
end
end
signal.emit(SGN_PS_AD_SHOW_VIDEO_FINISHED, "ads", success, reward_amount)
end
......
上面那段代码,在每次观看广告获取钻石时被调用,这样我们的修改思路就很明确了,只要让 storage:load_slot()
函数每次返回的钻石数为我们指定的,就可以达到目的了。
3. 修改脚本
在 /all/storage.lua 中直接定位到 storage:load_slot()
的实现:
function storage:load_slot(idx, force)
... 参数检查
local input = self:load_lua(string.format(self.SLOT_FILE_FMT, idx), force)
... 参数检查
--- 读取 SLOT_ADDITIONAL_DATA 到 result,并返回
for k, v in pairs(SLOT_ADDITIONAL_DATA) do
if not input[k] then
input[k] = v
end
end
return input
end
再看一眼 SLOT_ADDITIONAL_DATA, gems 就是我们要动手的目标了:
local SLOT_ADDITIONAL_DATA = {
gems = 0,
bag = {}
}
直接 patch storage:load_slot()
函数,添加两行代码,改的时候可以多尝试一下,改错了会导致游戏蓝屏或闪退:
function storage:load_slot(idx, force)
...
for k, v in pairs(SLOT_ADDITIONAL_DATA) do
if not input[k] then
input[k] = v
end
end
input["gems"] = 88888
print("[MaxLog]", input["gems"])
return input
使用 luajit 编译修改好的文件
luajit-2.1.0-beta3 -bg ./all/storage.lua ./storage.lj
这里要注意的是,由于 love2d (https://github.com/love2d/love) 使用的是 2.1.0-beta1 版的 luajit,而 brew install luajit 直接安装的版本为 2.0.5,用老版的 luajit 编译出来的二进制文件,游戏引擎会直接解析失败崩溃,这也是我踩过的坑。我这里测试,使用 beta3版是没有问题的,用以下命令安装:
brew install --HEAD luajit
4. 测试运行
obb 文件是通过使用时 mount 到应用沙盒 /data/data 目录下,我在文件管理器中未找到真实文件,具体原理还没有研究 obb 机制。我的解决方案是 Hook open()
函数,把原脚本文件替换为我们修改过的的文件。
- 把修改过的脚本文件放入 /data/local/tmp 中:
adb push storage.lj /data/local/tmp
- Hook libc 的
open()
函数:
int new_open(const char *name, int flags, ...) {
mode_t mode = 0;
if ((flags & O_CREAT) != 0) {
va_list args;
va_start(args, flags);
mode = static_cast<mode_t >(va_arg(args, int));
va_end(args);
}
LOGD("Tag", "open(\"%s\", %d)", name, flags);
if (strstr(name, "all/storage.lua")) {
// LOGD("Tag", "open(\"%s\", %d)", name, flags);
return old_open("/data/local/tmp/storage.lj", O_RDONLY);
}
return old_open(name, flags, mode);
}
-
运行游戏,在 Lua 脚本中打的 Log ,也可以查看到:
-
游戏里的钻石也成为指定的数额:
总结
其实破解修改手游大致都可以总结以下几个套路:
- 解密脚本
常见的几个游戏引擎u3d、cocos2dx,都可以解出游戏逻辑脚本,这些脚本也是我们主要关注的目标。需要注意的是可能存在jit、aot等情况,这时候就需要进行加密解密操作。 - 定位代码
通过搜索可以关键字,分析代码逻辑,找到需要修改的地方。 - 修改测试
明文脚本可以直接修改,密文脚本注意还原,还可以使用Hook等手段直接动态修改内存。