首页
社区
课程
招聘
[原创]一款corona lua 手游逆向
发表于: 3小时前 108

[原创]一款corona lua 手游逆向

3小时前
108

Lua分析

liblua.so文件应该是关键的lua文件,其中也可以找到luaL_loadbuffer函数的定义

确定版本号:Lua: Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio

同时还存在相关的libcorona.so,Corona SDK (现 Solar2D) 早期版本(特别是基于 Android 的旧版本)默认捆绑的就是 Lua 5.1.5

网上资料都是对cocos2dlua的讲解,对corona lua的分享较少,就找到了一篇2013年的资料:Corona SDK的iphone游戏存档校验分析一例

导出lu

文件结构分析

liblua.so作为引擎库,需要载入.lua的脚本文件,在assets/resource.car中找到了相关的结构:

//------------------------------------------------
//--- 010 Editor v13.0.1 Binary Template
//
//      File: 
//   Authors: zydt10
//   Version: 
//   Purpose: get lua from resource.car
//  Category: 
// File Mask: 
//  ID Bytes: 
//   History: 
//------------------------------------------------
typedef struct{
    DWORD magic_number;
    BYTE unknow[8];
    DWORD lua_file_count;   
} FileEntry;

typedef struct{
    DWORD type; // type == 01
    DWORD offset;
    DWORD filenamelen;   // 文件长度
    CHAR name[filenamelen]; // 变长字符数组
    CHAR padding[4-filenamelen%4]; // 依据filenamelen和下一次Lua_file分析得出
} Lua_Info;

typedef struct{
    DWORD type; // type == 02
    DWORD unknow; // 比rawlen大一些
    DWORD rawlen; // 
    BYTE raw[rawlen];
    if (rawlen%4 != 0){
        CHAR padding[4-rawlen%4];
    }
} Lua_Data;

LittleEndian(); // 设置小端序


FileEntry entry;
while (!FEof()) {
    if (ReadUInt() == 0x01) {
        Lua_Info info;
    } else
    if (ReadUInt() == 0x02) {
        Lua_Data data;
    } else
    {
        return;
    }
}

导出对应文件:

import struct
import os

def read_lua_files(file_path, output_dir):
    os.makedirs(output_dir, exist_ok=True)
    
    with open(file_path, 'rb') as f:
        # 读取FileEntry
        magic = struct.unpack('<I', f.read(4))[0]
        unknow = f.read(8)  # unknow
        lua_file_count = struct.unpack('<I', f.read(4))[0]
        
        files_info = []
        
        # 读取所有Lua_File1(Info)
        while True:
            type_val_bytes = f.read(4)
            if not type_val_bytes:
                break
            type_val = struct.unpack('<I', type_val_bytes)[0]
            
            if type_val == 0x01:
                offset = struct.unpack('<I', f.read(4))[0]
                filenamelen = struct.unpack('<I', f.read(4))[0]
                name = f.read(filenamelen).decode('utf-8', errors='ignore').rstrip('\x00')
                
                # 跳过padding (文件名的padding总是填充到下一个4的倍数)
                padding_len = 4 - (filenamelen % 4)
                f.read(padding_len)
                
                files_info.append({'offset': offset, 'name': name})
            elif type_val == 0x02:
                break
            else:
                break
        
        # 读取所有Lua_File2(Data)
        for info in files_info:
            f.seek(info['offset'])
            type_val = struct.unpack('<I', f.read(4))[0]
            if type_val != 0x02:
                print(f"Warning: data block for {info['name']} does not start with 0x02 (got {type_val})")
                continue
            
            unknow2 = f.read(4)  # unknow
            rawlen = struct.unpack('<I', f.read(4))[0]
            raw_data = f.read(rawlen)
            
            # 写入文件
            safe_name = info['name'].lstrip('/\\')
            output_path = os.path.join(output_dir, safe_name)
            os.makedirs(os.path.dirname(output_path), exist_ok=True)
            with open(output_path, 'wb') as out:
                out.write(raw_data)
            print(f"导出: {safe_name}")

if __name__ == '__main__':
    read_lua_files(r'nz.co.qmax.ponpon2\assets\resource.car', 'output')

547

到后面才发现有项目已经实现过了:GitHub - 0BuRner/corona-archiver: Python script to help pack and unpack Corona/Solar2D archive .car file · GitHub

接下来反汇编lua文件:GitHub - sztupy/luadec51: Lua Decompiler for Lua version 5.1 · GitHub

实验后确定可行,反汇编成功批量反编译

import os
import subprocess
from pathlib import Path

LUADEC_EXE = r"C:\Users\lenovo\Desktop\正在进行的逆向项目\环境配置\luadec51_2.0.2_win32\Luadec51.exe"
INPUT_DIR = r"C:\Users\lenovo\Desktop\正在进行的逆向项目\不可思议的微生物研究所\output"
OUTPUT_DIR = r"C:\Users\lenovo\Desktop\正在进行的逆向项目\不可思议的微生物研究所\transform"

def main():
    input_path = Path(INPUT_DIR)
    output_path = Path(OUTPUT_DIR)
    
    # 确保输出目录存在
    output_path.mkdir(parents=True, exist_ok=True)
    
    # 查找所有 .lu 文件
    lu_files = list(input_path.rglob("*.lu"))
    print(f"找到 {len(lu_files)} 个 .lu 文件需要处理。")
    
    success = 0
    for lu_file in lu_files:
        # 计算相对路径,以保持在 transform 目录中的原始目录结构
        rel_path = lu_file.relative_to(input_path)
        out_file = output_path / rel_path.with_suffix(".luac")
        
        # 确保特定的输出子目录存在
        out_file.parent.mkdir(parents=True, exist_ok=True)
        
        print(f"正在反编译: {rel_path} -> {out_file.name}")
        try:
            # 执行反编译并将标准输出重定向到文件
            with open(out_file, "wb") as f_out:
                subprocess.run(
                    [LUADEC_EXE, "-dis", str(lu_file)],
                    stdout=f_out,
                    stderr=subprocess.PIPE,
                    check=True
                )
            success += 1
        except subprocess.CalledProcessError as e:
            print(f"反编译失败 {rel_path}: {e.stderr.decode(errors='ignore')}")
        except Exception as e:
            print(f"处理 {rel_path} 时发生未知错误: {e}")
            
    print(f"\n完成!成功反编译 {success}/{len(lu_files)} 个文件。")

if __name__ == "__main__":
    main()

分析逻辑

需要破解的诱饵为虹色液,由于游戏停止维护,充值渠道关闭,该物品无法购买。定位:

而对应的price为-1,在游戏设计中通常意为着不可购买或者是存在特殊的判断逻辑,例如当price为-1时检测特殊的游戏货币(钻石)。这里要将-1改为1。但是不能直接改,lua有一套自己的常数逻辑,与字符串的常量逻辑不同。

将K常量值改为1之前,得搞清楚K的数值常量逻辑,

从《ANoFrillsIntroToLua51VMInstructions.pdf》中得知:552

657Number是IEEE 754 64位double类型。

转换:

import struct

def float_to_hex(double_val: float) -> str:
    packed_bytes = struct.pack('>d', double_val)
    hex_string = packed_bytes.hex()
    return hex_string.upper()

test_values = [
    10000,
    -1,
    1
]

for val in test_values:
    hex_result = float_to_hex(val)
    print(f"十进制数值: {val:>10}")
    print(f"IEEE 754 64位十六进制: 0x{hex_result}\n")

405验证后符合K42、K3等常量的值

接下来寻找常量表的存储位置,依旧是手搓个010的模板解析下

//------------------------------------------------
//--- 010 Editor v13.0.1 Binary Template
//
//      File: 
//   Authors: zydt10
//   Version: 
//   Purpose: 
//  Category: 
// File Mask: 
//  ID Bytes: 
//   History: 
//------------------------------------------------
typedef struct{
    if (ReadByte(FTell())==0x04){
        BYTE magic;
        BYTE len;
        BYTE padding1[3];
        BYTE value[len-1];
        BYTE padding2;
    }
    else if (ReadByte(FTell())==0x03){
        BYTE unknow[9];
    }
}constK;

LittleEndian();

BYTE used[0x1FD];// 上面的先不分析

while (!FEof()) {
    constK K;
    if ((ReadByte(FTell())!=0x03) && ((ReadByte(FTell())!=0x04)))
        return;
}

另一种思路是无法修改数值就修改调用变量,将price对应的常量(K18、K25、K28、K33、K38、K42)的位置替换为数值较小的(K3)。这里不做研究。

接下来就是对应回原来的resource.car处的值patch过去。比较定位:覆盖:434

签名后启动apk,游戏闪退。接下来怀疑过是不是修改的内容出问题了,检查了好几遍resource.car,确定里面不存在对里面小部分的单独的哈希校验。那就只可能是外部的java层或者native层存在对rescource.car文件的完整性校验了

接下来寻找哪里存在自定义的完整性校验:466如果grep找不到的话也可能是resource.car文件名进行了编码加解密。不过好在这里没有。

进入so文件,定位到相关逻辑:

进入sub 1109A8函数:

借助ai恢复符号后,整体的校验逻辑也比较清楚,确定了就是某种校验:555下面的值就是自定义存储好的初始哈希值或者常数表:676

有兴趣的可以深入分析这个校验。回过头来看,如果找不到字符串的话,尝试寻找这种哈希校验要用到的常数表或许也是一种不错的办法。

接下来直接回到之前if条件处patch。

0x10EAC8 12 00 00 1A -> 12 00 00 EA

打包回去签名。成功修改结果如下图:362

关于该游戏app的说明

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

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

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


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 3
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回