-
-
[原创]一款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')

到后面才发现有项目已经实现过了: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》中得知:
Number是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")
验证后符合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过去。比较定位:
覆盖:
签名后启动apk,游戏闪退。接下来怀疑过是不是修改的内容出问题了,检查了好几遍resource.car,确定里面不存在对里面小部分的单独的哈希校验。那就只可能是外部的java层或者native层存在对rescource.car文件的完整性校验了。
接下来寻找哪里存在自定义的完整性校验:
如果grep找不到的话也可能是resource.car文件名进行了编码加解密。不过好在这里没有。
进入so文件,定位到相关逻辑:

进入sub 1109A8函数:
借助ai恢复符号后,整体的校验逻辑也比较清楚,确定了就是某种校验:
下面的值就是自定义存储好的初始哈希值或者常数表:
有兴趣的可以深入分析这个校验。回过头来看,如果找不到字符串的话,尝试寻找这种哈希校验要用到的常数表或许也是一种不错的办法。
接下来直接回到之前if条件处patch。
0x10EAC8 12 00 00 1A -> 12 00 00 EA
打包回去签名。成功修改结果如下图:

关于该游戏app的说明
本笔记仅为个人学习逆向工程的记录,内容较为杂乱,仅供个人参考。
该游戏app自2020年起已正式停止运营,其联网付费功能的接口也已同步关闭。因此,选择该游戏app作为练习项目,目的是通过对其功能和架构的分析与研究,提升相关技术能力。
需要特别提醒的是,由于该app已停止运营且付费接口关闭,相关数据和功能可能无法正常使用。同时,笔者郑重声明,对于因使用该游戏app或其相关资源而可能引发的任何侵权问题,均不承担任何责任。
赞赏
- [原创]一款corona lua 手游逆向 109
- 使用frida进行硬件断点 18195
- [总结]app启动流程和so加载流程 23107