首页
课程
问答
CTF
社区
招聘
峰会
发现
排行榜
知识库
工具下载
看雪20年
看雪商城
证书查询
登录
注册
首页
社区
课程
招聘
发现
问答
CTF
排行榜
知识库
工具下载
峰会
看雪商城
证书查询
社区
Android安全
发新帖
4
4
[原创]一款corona lua 手游逆向
发表于: 2026-3-26 21:20
3100
[原创]一款corona lua 手游逆向
方北七
2026-3-26 21:20
3100
# 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游戏存档校验分析一例](https://bbs.kanxue.com/thread-155193.htm) ## 导出lu ### 文件结构分析 liblua.so作为引擎库,需要载入.lua的脚本文件,在assets/resource.car中找到了相关的结构:  ```c //------------------------------------------------ //--- 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; } } ``` ### 导出对应文件: ```python 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') ```  到后面才发现有项目已经实现过了:<a href="elink@6f2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1j5H3b7Y4g2d9L8X3g2J5i4K6u0r3j5$3!0J5L8$3&6S2i4K6u0V1j5i4u0U0K9r3W2$3k6i4t1`.">GitHub - 0BuRner/corona-archiver: Python script to help pack and unpack Corona/Solar2D archive .car file · GitHub</a> 接下来反汇编lua文件:<a href="elink@bc9K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6K6P5Y4c8#2M7s2W2Q4x3V1k6D9N6h3q4V1k6h3x3#2x3b7`.`.">GitHub - sztupy/luadec51: Lua Decompiler for Lua version 5.1 · GitHub</a>  实验后确定可行,反汇编成功 批量反编译 ```python 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类型。 转换: ```python 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的模板解析下  ```c //------------------------------------------------ //--- 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或其相关资源而可能引发的任何侵权问题,均不承担任何责任。
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!
#逆向分析
收藏
・
4
点赞
・
4
打赏
分享
分享到微信
分享到QQ
分享到微博
赞赏记录
参与人
雪币
留言
时间
北门观雪
+3
谢谢你的细致分析,受益匪浅!
2026-3-29 16:35
我的小拇指啊
你的分享对大家帮助很大,非常感谢!
2026-3-26 22:23
andy张刘
感谢你的积极参与,期待更多精彩内容!
2026-3-26 22:18
git_47499test-look
你的分享对大家帮助很大,非常感谢!
2026-3-26 21:41
查看更多
赞赏
×
1 雪花
5 雪花
10 雪花
20 雪花
50 雪花
80 雪花
100 雪花
150 雪花
200 雪花
支付方式:
微信支付
赞赏留言:
快捷留言
感谢分享~
精品文章~
原创内容~
精彩转帖~
助人为乐~
感谢分享~
最新回复
(
3
)
iBa0
雪 币:
1560
活跃值:
(5833)
能力值:
( LV4,RANK:40 )
在线值:
发帖
6
回帖
482
粉丝
37
关注
私信
iBa0
2
楼
看看看看
2026-3-27 08:37
0
Imxz
雪 币:
112
活跃值:
(9405)
能力值:
( LV2,RANK:10 )
在线值:
发帖
6
回帖
750
粉丝
9
关注
私信
Imxz
3
楼
rbq
2026-3-27 09:06
0
huangjw
雪 币:
6679
活跃值:
(11862)
能力值:
( LV2,RANK:10 )
在线值:
发帖
0
回帖
510
粉丝
2
关注
私信
huangjw
4
楼
火钳刘明
2026-3-27 09:30
0
游客
登录
|
注册
方可回帖
回帖
表情
雪币赚取及消费
高级回复
返回
方北七
3
发帖
4
回帖
10
RANK
关注
私信
他的文章
[原创]一款corona lua 手游逆向
3100
使用frida进行硬件断点
19557
[总结]app启动流程和so加载流程
23849
关于我们
联系我们
企业服务
看雪公众号
专注于PC、移动、智能设备安全研究及逆向工程的开发者社区
看原图
赞赏
×
雪币:
+
留言:
快捷留言
为你点赞!
返回
顶部