不可思议的微生物研究所

关于该游戏app的说明

本笔记仅为个人学习逆向工程的记录,内容较为杂乱,仅供个人参考。

该游戏app自2020年起已正式停止运营,其联网付费功能的接口也已同步关闭。因此,选择该游戏app作为练习项目,目的是通过对其功能和架构的分析与研究,提升相关技术能力。

需要特别提醒的是,由于该app已停止运营且付费接口关闭,相关数据和功能可能无法正常使用。同时,笔者郑重声明,对于因使用该游戏app或其相关资源而可能引发的任何侵权问题,均不承担任何责任。


接下来对于这款游戏破解提出了以下几点目标:
1、不花费钻石就能购买(已实现)
2、无限燃料时间(已实现)
3、扩大容纳限制(未实现)

定位食物图片:

/assets/image/food
assets/不可思议的微生物研究所/file-20250320162303256.png
/assets/image/foodThum
assets/不可思议的微生物研究所/file-20250320162315101.png
一个是喂食时,一个是购买时的图标

resources.arsc

assets/不可思议的微生物研究所/file-20250320162626667.png
发现常量,进入对应目录查看
assets/不可思议的微生物研究所/file-20250320162706423.png
又继续翻了下res目录下的资源,感觉没有有用信息

AndroidMainifest.xml:

寻找入口函数
assets/不可思议的微生物研究所/file-20250320163317300.png

classes.dex

assets/不可思议的微生物研究所/file-20250320163740992.png
assets/不可思议的微生物研究所/file-20250320163900586.png
即,调用了gx类中的aq方法,传入string类型的food作为参数,返回类型为gx类
跟踪这个gx:
assets/不可思议的微生物研究所/file-20250320163955118.png
新建了一个gx的实例,并调用初始化方法,将v0作为this指针,v1和p0作为参数传递进去,
继续研究init函数:
assets/不可思议的微生物研究所/file-20250320164224222.png
发现又调用了其他方法:U、kg、AI
单独跟进分析
U:判断“Given String is empty or null”
kg和AI:assets/不可思议的微生物研究所/file-20250320164527370.png

resource.car

assets/不可思议的微生物研究所/file-20250321094659842.png
找到了一些关键词,该文件存放于assets文件夹里,并不在resources文件夹里。
接下来想要寻找相关ID


还是找不到想找的地方。不清楚那些地方是放哪些代码,哪些变量,哪些常量。
需要整理一套自己的分析流程

查找shop找到存储的常量
assets/不可思议的微生物研究所/file-20250405110118839.png

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":,于是以特征开头作为搜索匹配,避免匹配到其他地方。
assets/不可思议的微生物研究所/file-20250412201320099.png

本来以为定位到了资源后修改就能完成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("--------------------------");
    }
}

assets/不可思议的微生物研究所/file-20250412204322210.png
发现这些数据可能是在运行时动态分配的,而不是直接存储在某个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

找到了相关字符串,但是没找到存储数值,如:
assets/不可思议的微生物研究所/file-20250412220225645.png
都是这种情况,找不到存储的值。

考虑第二种方法:插桩映射地址。
发现实现起来好复杂。由于frida的memory的内存断点只能断下一次。

于是找到APP私有数据目录:/data/data/
assets/不可思议的微生物研究所/file-20250413163024515.png
在0x7C0处找到数据。
assets/不可思议的微生物研究所/file-20250413162243428.png
尝试替换
assets/不可思议的微生物研究所/file-20250413162757188.png
成功破解pt
assets/不可思议的微生物研究所/file-20250413162819085.png
接下来的诱饵同理,修改“foodList”里的"6"
研究等级修改“Lv”
破解时间也一样
assets/不可思议的微生物研究所/file-20250413170518287.png
但是不能插入字节,一旦插入,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

assets/不可思议的微生物研究所/file-20250414002555600.png
在分析后发现规律,如果要存储的位数加一,则地址和指针都会发生对应变化。

在反复观察下,发现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:
assets/不可思议的微生物研究所/file-20250509154656943.png
我们想要查找的文件加载有且只有data.db

怀疑是在java层加载的data.db,但是搜索java层的字符串又找不到。

既然fopen没有突破口,接下来尝试hook写入函数,程序退出时一定会保存当前的数据。

posted @ 2025-05-10 14:25  方北七  阅读(51)  评论(0)    收藏  举报