不可思议的微生物研究所
关于该游戏app的说明
本笔记仅为个人学习逆向工程的记录,内容较为杂乱,仅供个人参考。
该游戏app自2020年起已正式停止运营,其联网付费功能的接口也已同步关闭。因此,选择该游戏app作为练习项目,目的是通过对其功能和架构的分析与研究,提升相关技术能力。
需要特别提醒的是,由于该app已停止运营且付费接口关闭,相关数据和功能可能无法正常使用。同时,笔者郑重声明,对于因使用该游戏app或其相关资源而可能引发的任何侵权问题,均不承担任何责任。
接下来对于这款游戏破解提出了以下几点目标:
1、不花费钻石就能购买(已实现)
2、无限燃料时间(已实现)
3、扩大容纳限制(未实现)
定位食物图片:
/assets/image/food

/assets/image/foodThum

一个是喂食时,一个是购买时的图标
resources.arsc

发现常量,进入对应目录查看

又继续翻了下res目录下的资源,感觉没有有用信息
AndroidMainifest.xml:
寻找入口函数

classes.dex


即,调用了gx类中的aq方法,传入string类型的food作为参数,返回类型为gx类
跟踪这个gx:

新建了一个gx的实例,并调用初始化方法,将v0作为this指针,v1和p0作为参数传递进去,
继续研究init函数:

发现又调用了其他方法:U、kg、AI
单独跟进分析
U:判断“Given String is empty or null”
kg和AI:
resource.car

找到了一些关键词,该文件存放于assets文件夹里,并不在resources文件夹里。
接下来想要寻找相关ID
还是找不到想找的地方。不清楚那些地方是放哪些代码,哪些变量,哪些常量。
需要整理一套自己的分析流程
查找shop找到存储的常量

emm找到相关函数后看不懂逻辑。于是还是打算学习frida hook 来拿到相关传参才行。
经过了一下午的尝试,还是没有找到相关的方法,shop、food、time、pt、money等关键字都搜索过了,没有突破口。
于是考虑在网上寻找corona的逆向博客,然后也没发现多少资料,这个时候貌似卡住了。没有办法定位主要逻辑的so文件,也就没有办法找到相关的方法和内存。
这时想到了可以尝试像CE一样通过diff内存来找到游戏数值变化的地方,经过一番尝试后发现变量太大了,难以精确定位。
于是尝试通过内存搜索定位,也就是搜索精确数值。在搜索的过程中发现地址总是不一样,是因为so加载的基址是会变化的,于是改回attach附加。
function findpt(searchPattern) { // 修改参数名避免冲突
const ranges = Process.enumerateRangesSync('rw-');
let results = [];
ranges.forEach(range => {
try {
const matches = Memory.scanSync(range.base, range.size, searchPattern); // 修改变量名
if (matches.length > 0) {
results = results.concat(matches);
}
} catch (e) {
// console.log(e);
}
});
return results;
}
function outputRes(results) {
let str=new Array();
results.forEach((match, index) => {
try {
str.push(match.address)
// console.log(`[${index}] 地址: ${match.address}`);
// console.log(hexdump(match.address, {
// offset: 0,
// length: 16,
// header: false,
// ansi: false
// }));
} catch (e) {
// console.log(`[${index}] 地址: ${match.address} (无法访问)`);
}
});
console.log(str)
return str;
}
function patchmoney(pt_addr) {
console.log("start to patch")
Memory.writeUtf8String(pt_addr, "1234");
}
function readmoney(pt_addr) {
try {
const bytes = Memory.readByteArray(pt_addr, 16*15);
console.log(bytes);
} catch (e) {
console.log("读取内存失败:", e);
}
}
function patchmoney(pt_addr) {
const newValue = [0x31, 0x32, 0x33, 0x34]; // "1234"的ASCII字节
const targetAddr = pt_addr.add(0x93);
Memory.writeByteArray(targetAddr, newValue);
try {
Memory.protect(targetAddr, newValue.length, 'rwx');
if (Process.flush) {
Process.flush();
}
} catch (e) {
console.log("内存保护设置失败:", e);
}
}
function readandpatch(search_addr){
for (let i = 0; i < search_addr.length; i++) {
const pt_addr = ptr(search_addr[i]);
readmoney(pt_addr)
console.log(" ")
patchmoney(pt_addr)
readmoney(pt_addr)
console.log("--------------------------");
}
}
function main() {
const pattern = "7b 22 73 74 6f 72 79 22 3a";
// const pattern = "31 32 33 34"; // 原本是匹配"1234"的字节模式,在此前还尝试过匹配hex(1234),经过尝试发现是以string形式存储
let results = findpt(pattern)
let search_addr=outputRes(results)
readandpatch(search_addr)
}
setImmediate(function () {
setTimeout(main, 1000);
});
定位到相关地址后上移找到存储特征{"story":,于是以特征开头作为搜索匹配,避免匹配到其他地方。

本来以为定位到了资源后修改就能完成hook了,在patch后发现,即便patch了但是游戏显示的资源数值没有任何变化。
可能是游戏是通过文件存储的数值,也可能游戏将真正源数值加密存储了,或者二者兼有。而我们只修改这些投射的数字是无法修改到真正的源数值的。
于是尝试通过找到的地址来定位so文件
function Match_so(search_addr) {
console.log("开始匹配地址所属的SO文件...");
// 获取所有已加载的模块
const modules = Process.enumerateModules();
for (let i = 0; i < search_addr.length; i++) {
const address = ptr(search_addr[i]);
let found = false;
// 遍历所有模块,检查地址是否在模块范围内
for (let j = 0; j < modules.length; j++) {
const module = modules[j];
const moduleBase = module.base;
const moduleEnd = moduleBase.add(module.size);
if (address.compare(moduleBase) >= 0 && address.compare(moduleEnd) < 0) {
// 计算偏移量
const offset = address.sub(moduleBase);
console.log(`[${i}] 地址: ${address} 属于模块: ${module.name}`);
console.log(` 路径: ${module.path}`);
console.log(` 基址: ${moduleBase}`);
console.log(` 偏移: 0x${offset.toString(16)}`);
found = true;
break;
}
}
if (!found) {
console.log(`[${i}] 地址: ${address} 不属于任何已知模块,可能是堆或栈内存`);
// 尝试获取内存区域信息
try {
const memoryRange = Process.findRangeByAddress(address);
if (memoryRange) {
console.log(` 内存区域: ${memoryRange.base} - ${memoryRange.base.add(memoryRange.size)}`);
console.log(` 权限: ${memoryRange.protection}`);
console.log(` 类型: ${memoryRange.file ? "文件映射" : "匿名映射"}`);
}
} catch (e) {
console.log(` 无法获取内存区域信息: ${e}`);
}
}
console.log("--------------------------");
}
}

发现这些数据可能是在运行时动态分配的,而不是直接存储在某个so文件中。
思考了一下,接下来该如何定位so文件。第一种方法是去资源文件里寻找story、特征字符串,第二种方法是利用frida的插桩,在数据的映射存储地方下attach,第三种办法也是插桩,不过是插桩malloc、calloc等堆申请函数。接下来由易到难开始尝试。
尝试寻找资源文件的内容,不过我觉得加密存储的可能性大些,明文可能找不到。
lenovo@DESKTOP-660LLLG MINGW64 ~/Desktop/nz.co.qmax.ponpon2
$ grep -r -n story ./
Binary file ./assets/resource.car matches
Binary file ./classes.dex matches
Binary file ./lib/armeabi-v7a/libcorona.so matches
找到了相关字符串,但是没找到存储数值,如:

都是这种情况,找不到存储的值。
考虑第二种方法:插桩映射地址。
发现实现起来好复杂。由于frida的memory的内存断点只能断下一次。
于是找到APP私有数据目录:/data/data/

在0x7C0处找到数据。

尝试替换

成功破解pt

接下来的诱饵同理,修改“foodList”里的"6"
研究等级修改“Lv”
破解时间也一样

但是不能插入字节,一旦插入,app貌似不能根据存储的偏移读取到
adb push ./data.db /data/data/nz.co.qmax.ponpon2/app_data/data.db
adb pull /data/data/nz.co.qmax.ponpon2/app_data/data.db ./data.db

在分析后发现规律,如果要存储的位数加一,则地址和指针都会发生对应变化。
在反复观察下,发现end处的指针貌似指向图鉴。
hook fopen
使用FCAnd.traceFopen(),过滤无用重复信息后得到以下路径
____
/ _ | Frida 15.2.2 - A world-class dynamic instrumentation toolkit
| (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit
. . . .
. . . . More info at https://frida.re/docs/home/
. . . .
. . . . Connected to 2201123C (id=127.0.0.1:7555)
Spawning `nz.co.qmax.ponpon2`...
[INFO][05/09/2025, 02:58:39 AM][PID:6183][6213][JAVA]: available
Spawned `nz.co.qmax.ponpon2`. Resuming main thread!
[2201123C::nz.co.qmax.ponpon2 ]-> [DEBUG][05/09/2025, 02:58:39 AM][PID:6183][Binder:6183_1][6209][MAIN]: HELLO FridaContainer, please add code on the index.ts
[INFO][05/09/2025, 02:58:39 AM][PID:6183][Binder:6183_1][6209][traceFopen]: fopen_ptr: 0xef2708e0
/system/fonts/Roboto-Regular.ttf
/data/resource-cache/com.android.systemui-neutral-OMTx.frro
/data/resource-cache/com.android.systemui-accent-2uEW.frro
/data/system/etc/mumu-configs/renderer.config
/etc/openal/alsoft.conf
/sys/devices/system/cpu/online
/sys/devices/system/cpu/online
/sys/devices/system/cpu/online
/sys/devices/system/cpu/online
(null)/network.lua
(null)/network.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libnetwork.so
./network.so
(null)/network.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libnetwork.so
./network.so
(null)/network.so
(null)/plugin/fuse.lua
(null)/plugin/fuse.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin/fuse.so
./plugin/fuse.so
(null)/plugin/fuse.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.so
./plugin.so
(null)/plugin.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.fuse.so
./plugin.fuse.so
(null)/plugin.fuse.so
/data/user/0/nz.co.qmax.ponpon2/cache/.system/.com.coronalabs.corona.analyticsData
/data/user/0/nz.co.qmax.ponpon2/cache/.system/.com.coronalabs.corona.analyticsData
/data/user/0/nz.co.qmax.ponpon2/cache/.system/.com.coronalabs.corona.analyticsData
(null)/licensing.lua
(null)/licensing.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/liblicensing.so
(null)/CoronaProvider/licensing/google.lua
(null)/CoronaProvider/licensing/google.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libCoronaProvider/licensing/google.so
./CoronaProvider/licensing/google.so
(null)/CoronaProvider/licensing/google.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libCoronaProvider.so
./CoronaProvider.so
(null)/CoronaProvider.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libCoronaProvider.licensing.google.so
./CoronaProvider.licensing.google.so
(null)/CoronaProvider.licensing.google.so
(null)/plugin/google/iap/v3.lua
(null)/plugin/google/iap/v3.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin/google/iap/v3.so
./plugin/google/iap/v3.so
(null)/plugin/google/iap/v3.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.so
./plugin.so
(null)/plugin.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.google.iap.v3.so
./plugin.google.iap.v3.so
(null)/plugin.google.iap.v3.so
/data/user/0/nz.co.qmax.ponpon2/app_data/seVol
/system/fonts/NotoSansSC-Regular.otf
(null)/plugin/adrally.lua
(null)/plugin/adrally.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin/adrally.so
./plugin/adrally.so
(null)/plugin/adrally.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.so
./plugin.so
(null)/plugin.so
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libplugin.adrally.so
./plugin.adrally.so
(null)/plugin.adrally.so
/data/user/0/nz.co.qmax.ponpon2/app_webview/pref_store
/proc/6183/stat
/proc/6183/stat
/proc/6183/stat
/system/etc/hosts
/data/user/0/nz.co.qmax.ponpon2/app_webview/Default/Preferences
/data/user/0/nz.co.qmax.ponpon2/app_data/tama3
/sys/devices/system/cpu/online
/data/user/0/nz.co.qmax.ponpon2/files/coronaResources/sound/button.wav
(null)/ads.lua
(null)/ads.lua
/data/app/~~KERY7P6YlpLR7HY3u8QmfQ==/nz.co.qmax.ponpon2-MxRd6zr_iLyK48FbpJam-g==/lib/arm/libads.so
/data/user/0/nz.co.qmax.ponpon2/app_data/uuid
/data/user/0/nz.co.qmax.ponpon2/app_data/userid
/data/user/0/nz.co.qmax.ponpon2/app_data/userid
/data/user/0/nz.co.qmax.ponpon2/app_data/userpass
/data/user/0/nz.co.qmax.ponpon2/app_data/userpass
/data/user/0/nz.co.qmax.ponpon2/app_data/TimeMaster
/data/user/0/nz.co.qmax.ponpon2/app_data/TimeMaster
/data/user/0/nz.co.qmax.ponpon2/app_data/lastSaveTime
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara
/data/user/0/nz.co.qmax.ponpon2/app_data/aid
/data/user/0/nz.co.qmax.ponpon2/app_data/storyCheck9
/data/user/0/nz.co.qmax.ponpon2/app_data/lastSaveTime
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara_tmp
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara_tmp
/data/user/0/nz.co.qmax.ponpon2/app_data/lastSaveTime
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara_tmp
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara_tmp
/data/user/0/nz.co.qmax.ponpon2/app_data/lastSaveTime
/data/user/0/nz.co.qmax.ponpon2/app_data/save_chara_tmp
可以观察到可能分为了加载部分和写入部分,加载部分直到"aid",到后面应该都是运行时app时写入部分。
查看上述加载的文件内容,发现都不是我们想找的文件,示例save_chara:

我们想要查找的文件加载有且只有data.db。
怀疑是在java层加载的data.db,但是搜索java层的字符串又找不到。
既然fopen没有突破口,接下来尝试hook写入函数,程序退出时一定会保存当前的数据。

浙公网安备 33010602011771号