请选择 进入手机版 | 继续访问电脑版
您好,欢迎访问! 登录

QQ登录

只需一步,快速开始

立即注册 切换到窄版
查看: 2906|回复: 4

[转载] 就算Lua也hook给你看-Corona SDK 游戏魔女防御战的作弊插件

[复制链接]

  离线 

25

主题

51

帖子

159

积分

版主

Rank: 7Rank: 7Rank: 7

积分
159
发表于 2014-6-9 13:14:04 | 显示全部楼层 |阅读模式
标 题: 【原创】就算Lua也hook给你看-Corona SDK 游戏魔女防御战的作弊插件
作 者: 曾半仙
时 间: 2013-03-01,17:25:09
链 接: http://bbs.pediy.com/showthread.php?t=163435

去年发了一个帖子是分析魔女防御战的存档校验. 当时貌似提到了这个程序是Corona SDK, 逻辑核心是lua完成的.
废话不说, 今天我们就来实现针对游戏中lua逻辑的修改.
下载了1.8.0版的Defense Witches, 照例用decar解压(decar -x resource.car .\lu), 得到一堆lu文件.
这些.lu文件+0C的地方开始就是lua字节码, +8是大小.
今天下手的目标决定是初始化的300点, 这里还有个小插曲, 因为一开始以为那个叫做水晶点, 所以费了一番工夫, 各种修改都无效果.
一开始将main.lu反汇编成为main_luadec.asm, 在里面搜索300, 搜索到一行这个:
代码:
  1. 117 [-]: LOADK     R0 K64       ; R0 := 300
  2.   118 [-]: SETGLOBAL R0 K63       ; GetCrystal := R0
复制代码

K63/K64都是常量, 存储在这段代码块后面的常量表里面. 在main.lu搜索GetCrystal得到如下数据:

  1. <font color="#13253c">0A37h: 04 0B 00 00 00 47 65 74 43 72 79 73 74 61 6C 00  .....GetCrystal. </font>
  2. <font color="#13253c">0A47h: 03 </font><font color="#ff8c00">00 00 00 00 00 C0 72 40</font><font color="#13253c">                       ......Àr@</font>
复制代码

注意橙色标注的部分. 常量表的结构是开头是常量的总数的DWORD, 然后后面跟每条记录. 记录有一个字节的类型和后面的数据构成, 类型这里出现了2种, 03 = Number, 04 = String.

lua的字节码里面, Number是用标准的IEE 754编码的double, 占8个字节. 这个长度从lua头部也可以看出.
顺便八一下字节码文件的头部 前五个字节分别是, 0x1B,Lua, 0x51这里的51指定了是5.1, 后面还有是否是正式版本和Endian/各数据类型长度, 简单的说这个arm游戏的字节码跟x86是一样的.

想要折腾这个SDK的推荐一本pdf叫做ANoFrillsIntroToLua51VMInstructions.pdf, 网上也有各种不同的翻译版本, 里面描述了字节码文件和函数块常量数据, 指令等二进制编码方式.

在010Editor中把光标定位在橙色部分的首字节, 在Data Inspector里面, 在Double一栏将数据从300修改为2300, 保存.
尝试将resource.car做同样修改, 覆盖回程序目录, 试验运行, 果断闪退. 原因吗, 是因为_CodeSignature\CodeResources里面有每个资源文件的签名.

不过这个难不倒我们, 我们可以hook呀. 说干就干, 先用dumpdecrypted.dylib来dump出未加密的主程序, 打开IDA, 在字串列表里查找main.lu

代码:
  1. __text:0008677A 61 6B                                   LDR             R1, [R4,#0x34]
  2. __text:0008677C 01 23                                   MOVS            R3, #1  ; flag
  3. __text:0008677E 20 6C                                   LDR             R0, [R4,#0x40] ; obj
  4. __text:00086780 09 68                                   LDR             R1, [R1] ; a2
  5. __text:00086782 4A F6 F2 42 C0 F2 09 02                 MOV             R2, (aMain_lu - 0x8678E) ; "main.lu"
  6. __text:0008678A 7A 44                                   ADD             R2, PC  ; "main.lu"
  7. __text:0008678C E3 F7 46 FC                             BL              func_executebytecodefile
  8. __text:00086790 80 46                                   MOV             R8, R0
复制代码



这个被调用的函数定义是这样的
int func_executebytecodefile(ResourceObjRef resource, lua_State *state, const char *filename, int flag)
其中ResourceObjRef是我的定义的类型指针, +0C地方是resource.car完整内容的内存映射指针.
写出替换函数如下:

代码:
  1. int my_func_exebytecodefile(ResourceObjRef resource, lua_State *state, const char *filename, int flag) {
  2.     if (strcmp(filename, "main.lu") == 0) {
  3.         logstderr("matched main.lu\n");
  4.         // patch carbuf
  5.         // 04 0B 00 00 00 47 65 74  43 72 79 73 74 61 6C 00
  6.         // 03 00 00 00 00 00 C0 72  40
  7.         uint32_t start = (uint32_t)resource->carbuffer;
  8.         for (const unsigned char* ptr = (unsigned char*)start; ptr < (unsigned char*)start + 0x100000; ptr++) {
  9.             // B0 B5 02 AF 1C 46 0D 46
  10.             if (*(unsigned*)ptr == 0x00000B04 && *(unsigned*)(ptr+4) == 0x74654700 && *(unsigned*)(ptr+8) == 0x73797243 && *(unsigned*)(ptr+12) == 0x006C6174
  11.                 && *(unsigned*)(ptr+16) == 0x00000003 && *(unsigned*)(ptr+20) == 0x72C00000 && *(unsigned char*)(ptr+24) == 0x40) {
  12.                 // patch + 17
  13.                 logstderr("found GetCrystal at 0x%08X\n", ptr);
  14.                 hexdump(ptr, 25);
  15.                 *(double*)(ptr+17) = 800;
  16.                 hexdump(ptr, 25);
  17.             }
  18.         }
  19.     }
  20.     return ms_func_exebytecodefile(resource, state, filename, flag);
  21. }
复制代码

因为对resource.car映射的内容写将引发bus error, 虽然有办法强写, 不过还是用了更简单的办法, 先复制一份再修改调用. 同时找到更内层的函数进行截取, 但是结果很遗憾, 和一开始说的一样, 这个GetCrystal是水晶也就是内购的货币, 我改错对象了.
然后再次把每个lu文件里面都搜索300.0过程中, 发现game.lu里面的_G["MP"]可能是我们想要的东西.

代码:
  1. 36 [-]: GETGLOBAL R1 K3        ; R1 := _G
  2.    37 [-]: SETTABLE  R1 K18 K15   ; R1["MP"] := 0
  3.    38 [-]: GETGLOBAL R1 K3        ; R1 := _G
  4.    39 [-]: SETTABLE  R1 K19 K20   ; R1["HP"] := 20
复制代码

这段就是_G.MP = 0; _G.HP = 20; 对应的是画面可以用来购买和升级己方单位的点数和剩余塔的攻击剩余次数.

当时想法是, 初始化是0, 在什么地方被添加成了300吧, 那么我们把初始化的地方改为20, 不就变为320了么?

为什么非要用20, 那是因为SETTABLE opcode里面没有立即数寻址, 只有寄存器和常量寻址.

这个时候要了解一下lua的opcode编码方式了.

SETTABLE R1 K18 K15 是iABC格式的编码, 我们今天所用到的所有修改都是这个格式.
其中SETTABLE(助记符编号)是i, R1(目标) K18(源1) K15(源2)是A B C. 其中B/C可以使用常数和寄存器编号, 使用常数表编号时候, 最高bit置1, 也就是+256.

i的值查表可以得到, 每个opcode对应一个索引, SETTABLE是9, ADD是12.

手动换算的话, 就是9, 1, 256+18, 256+15
将这四个数字化为2进制, 然后按照
B:9 C:9 A:8 i:6的方式组合起来
代码:
  1. 100010010 100001111 00000001 001001
  2. 8943C049
复制代码

将C的K15替换为K20后如下
代码:
  1. 100010010 100010100 00000001 001001
  2. 89450049
复制代码

修改后运行, mp依然是300.
然后逐个寻找操作mp的, 最后在status.lu中发现了如下代码
代码:
  1. 0 [-]: LOADK     R1 K0        ; R1 := "SELECT ratio, firstmp FROM information WHERE id=1 AND flag=0"
  2.     1 [-]: LOADNIL   R2 R2        ; R2 := nil
  3.     2 [-]: SELF      R3 R0 K1     ; R4 := R0; R3 := R0["nrows"]
  4.     3 [-]: MOVE      R5 R1        ; R5 := R1
  5.     4 [-]: CALL      R3 3 4       ; R3,R4,R5 := R3(R4,R5)
复制代码

原来还有这一招, 每个地图初始化mp都是sqlite数据库中指定的, 存放在data\map\01\001.sqlite这样的路径里.
这里就有两种选择了, 要么hook读写地图数据地方, 提供虚假的数据库内容.
不过接着往下看

代码:
  1. 34 [-]: DIV       R4 R4 K10    ; R4 := R4 / 100
  2.    35 [-]: SETTABLE  R3 K8 R4     ; R3["EnemyScaleBase"] := R4
  3.    36 [-]: GETGLOBAL R3 K2        ; R3 := _G
  4.    37 [-]: GETTABLE  R4 R2 K12    ; R4 := R2["firstmp"]
  5. // TODO: insert ADD R4 R4 R4 or MUL R4 R4 K10 or MUL R4 R4 R4
  6.    38 [-]: SETTABLE  R3 K11 R4    ; R3["MP"] := R4
复制代码

这里将数据库的查询结果, 存储到_G["MP"]中. 这时候我考虑补丁代码会造成后面的东西整体偏移, 不如自己做一个字节码文件, 额外执行一次, 专门修改_G["MP"]的值.

撰写lua文件如下, 并用luac编译.
代码:
  1. module(..., package.seeall)
  2. function Init()
  3.   _G.MP = _G.MP * 2
  4. end
复制代码

编译后, 在执行status.lu后执行一次我们的bytecode. 结果是… 返回后没事, 执行其他lu时候崩掉了.

代码:
  1. execute c02_becky08.lu
  2. 2013-03-01 05:34:35.332 D.Witches[10534:707] Lua Runtime Error: lua_pcall failed with status: 2, error message is: ?:0: attempt to index field '?' (a nil value)
复制代码

没关系, 我们就费点事, 补丁掉37行的地方吧.
lua的每个函数块开头都有固定格式的头部.
  1. 00 00 00 00 92 00 00 00 A2 00 00 00 05 00 00 05 2A 00 00 00
复制代码
其中00000000是存储函数名的, 这里为空. 接着两个DWORD分别是代码开始行和结束行. 这个行指的是源码的行, 我们模拟在源码插入一行_G.MP = _G.MP + _G.MP, 顺便将结束行也加一为妙.

要补丁的有2A000000这个是属于bytecode代码块列表的数量字段, 描述的是虚拟机的字节码.
每条指令都是4字节, 这个数量必须改.
为了方便算字节码, 写了个小程序luaasm, 可以接收数字或者汇编输入
代码:
  1. $ luaasm iABC 12 4 4 4
  2. opcode: 0x0201010C

  3. $ luaasm iABC ADD R4 R4 R4
  4. 12 4 4 4 -> opcode: 0x0201010C
复制代码

其中12是ADD的编号, R4 = R4 + R4就会把300 MP给倍增.
原理类似于x86里面, 从某内存地址取值存在EDX中再放进目标地址, 此时我们倍增EDX的效果相同.

最后测试了插入2条MP*2代码也无误, 就没有继续测试MP*200了(第二条改为*100和第一条叠加)
最终代码如下:
  1. int my_func_executelu(lua_State *state, const char *buffer, int size, const char *subfilename)
  2. {
  3.     if (strcmp(subfilename, "status.lu") == 0 && g_statuslu == NULL) {
  4.         logstderr("matched status.lu\n");
  5.         // patch SETTABLE
  6.         uint32_t start = (uint32_t)buffer;
  7.         for (const unsigned char* ptr = (unsigned char*)start; ptr < (unsigned char*)start + size; ptr++) {
  8.             //92 00 00 00 A2 00 00 00 05 00 00 05 2A 00 00 00
  9.             if (*(unsigned*)ptr == 0x00000092 && *(unsigned*)(ptr+4) == 0x000000A2 && *(unsigned*)(ptr+8) == 0x05000005 && *(unsigned*)(ptr+12) == 0x0000002A) {
  10.                 logstderr("found Init chunk at 0x%08X\n", ptr);
  11.                 // make backup
  12.                 g_statuslu = (char*)malloc(size + 8); // expand for MP*2
  13.                 memcpy(g_statuslu, buffer, size);
  14.                 memcpy(g_statuslu + (ptr + 0xA8 + 8 - (unsigned char*)start), ptr + 0xA8, size - (ptr + 0xA8 - (unsigned char*)start));
  15.                 atexit(freestatuslu);
  16.                
  17.                 *(unsigned*)(g_statuslu + (ptr + 0xA8 - (unsigned char*)start)) = 0x0201010C; // 300 * 2
  18.                 *(unsigned*)(g_statuslu + (ptr + 0xA8 + 4 - (unsigned char*)start)) = 0x0201010C; // 600 * 2
  19.                
  20.                 *(unsigned*)(g_statuslu + (ptr + 0x04 - (unsigned char*)start)) = 0x000000A4;
  21.                 *(unsigned*)(g_statuslu + (ptr + 0x0C - (unsigned char*)start)) = 0x0000002C;
  22.                
  23.                 return ms_func_executelu(state, g_statuslu, size + 8, subfilename); // with new buffer
  24.             }
  25.         }

  26.     }
  27.     return ms_func_executelu(state, buffer, size, subfilename);
  28. }
复制代码

这段代码并未做到通用, 理想的方式是自带lua字节码反汇编功能, 能选择插入的位置同时修改所有位于修改点后的代码块对应的源码行数(其实不改也正常.)

hook的方式是用mobilesubstrate, 搜索函数头指纹并hook为替代函数. 代码丑可能还有拼写错误...

运行截图如下


可见初始化的300 MP经过两次翻倍, 变成了1200, 写好过滤条件打包为deb收工不提~~

附送luaasm.exe工具和源码. 没写完, 只认 ADD R4 R4 K10这种格式的iABC指令, 也可以输入4个数字.
$ luaasm iABC ADD R4 R4 R4
12 4 4 4 -> opcode: 0x0201010C

$ luaasm iABC MUL R4 R4 K10
14 4 4 266 -> opcode: 0x0242810E
com.shanghaiichi.DWitchesBooster_1.0-1_iphoneos-arm.rar (7.53 KB, 下载次数: 1)
我不会告诉你,在游源签到是一种执着!
回复

使用道具 举报

  离线 

1

主题

30

帖子

42

积分

游源小侠

Rank: 2Rank: 2

积分
42
发表于 2014-8-7 13:18:47 | 显示全部楼层
勉勉强强看得懂一点!没有学过编程真痛苦啊1!
该会员没有填写今日想说内容.
回复 支持 反对

使用道具 举报

  离线 

0

主题

2

帖子

18

积分

游源小侠

Rank: 2Rank: 2

积分
18
发表于 2016-8-28 15:10:53 | 显示全部楼层
蚂蚁花呗取现客服QQ 3014579034-认准老字号玩转花呗-[email=www.mayihuabeii.com]www.mayihuabeii.com[/email]
回复 支持 反对

使用道具 举报

  离线 

0

主题

16

帖子

32

积分

游源小侠

Rank: 2Rank: 2

积分
32
发表于 2016-11-4 04:16:51 | 显示全部楼层
支持一下吧,确实是不错的贴子。












回复 支持 反对

使用道具 举报

  离线 

0

主题

2

帖子

18

积分

游源小侠

Rank: 2Rank: 2

积分
18
发表于 2016-12-15 15:46:37 | 显示全部楼层
水平很高啊,值得学习












回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

站点统计|小黑屋|手机版|Archiver|游源网 ( 冀ICP备14006073号-1

Copyright 2013 最新最精彩-社区论坛 版权所有 discuz 模板All Rights Reserved.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表