随着越来越多的游戏,软件采用Lua来实现业务逻辑,


想搞黑产的同学,时常便会遇见lua脚本,可惜大部分都是编译过的lua脚本,而且还是自定义的。


便难倒了很多菜鸟,lua 的实现机制,那可是虚拟机技术,非常难于调试。


本教程,便来普及lua 的虚拟机指令及其反编译lua脚本,成为文本形式的脚本



1.Lua的虚拟机指令,5.2 的有40条



Lua的指令使用一个32bit的unsigned integer表示。所有指令的定义都在lopcodes.h文件中(可以从Lua 官方网站下载),使用一个enum OpCode代表指令类型。在lua5.2中,总共有40种指令(id从0到39)。根据指令参数的不同,可以将所有指令分为4类:


 


typedef enum { 

/*---------------------------------------------------------------------- 

name    args  description 


------------------------------------------------------------------------*/ 

OP_MOVE,/*  A B  R(A) := R(B)          */ 

OP_LOADK,/*  A Bx  R(A) := Kst(Bx)          */ 

OP_LOADKX,/*  A   R(A) := Kst(extra arg)        */ 

OP_LOADBOOL,/*  A B C  R(A) := (Bool)B; if (C) pc++      */ 

OP_LOADNIL,/*  A B  R(A), R(A+1), ..., R(A+B) := nil    */ 

OP_GETUPVAL,/*  A B  R(A) := UpValue[B]        */ 


OP_GETTABUP,/*  A B C  R(A) := UpValue[B][RK(C)]      */ 

OP_GETTABLE,/*  A B C  R(A) := R(B)[RK(C)]        */ 


OP_SETTABUP,/*  A B C  UpValue[A][RK(B)] := RK(C)      */ 

OP_SETUPVAL,/*  A B  UpValue[B] := R(A)        */ 

OP_SETTABLE,/*  A B C  R(A)[RK(B)] := RK(C)        */ 


OP_NEWTABLE,/*  A B C  R(A) := {} (size = B,C)        */ 


OP_SELF,/*  A B C  R(A+1) := R(B); R(A) := R(B)[RK(C)]    */ 


OP_ADD,/*  A B C  R(A) := RK(B) + RK(C)        */ 

OP_SUB,/*  A B C  R(A) := RK(B) - RK(C)        */ 

OP_MUL,/*  A B C  R(A) := RK(B) * RK(C)        */ 

OP_DIV,/*  A B C  R(A) := RK(B) / RK(C)        */ 

OP_MOD,/*  A B C  R(A) := RK(B) % RK(C)        */ 

OP_POW,/*  A B C  R(A) := RK(B) ^ RK(C)        */ 

OP_UNM,/*  A B  R(A) := -R(B)          */ 

OP_NOT,/*  A B  R(A) := not R(B)        */ 

OP_LEN,/*  A B  R(A) := length of R(B)        */ 


OP_CONCAT,/*  A B C  R(A) := R(B).. ... ..R(C)      */ 


OP_JMP,/*  A sBx  pc+=sBx; if (A) close all upvalues >= R(A) + 1  */ 

OP_EQ,/*  A B C  if ((RK(B) == RK(C)) ~= A) then pc++    */ 

OP_LT,/*  A B C  if ((RK(B) <  RK(C)) ~= A) then pc++    */ 

OP_LE,/*  A B C  if ((RK(B) <= RK(C)) ~= A) then pc++    */ 


OP_TEST,/*  A C  if not (R(A) <=> C) then pc++      */ 

OP_TESTSET,/*  A B C  if (R(B) <=> C) then R(A) := R(B) else pc++  */ 


OP_CALL,/*  A B C  R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */ 

OP_TAILCALL,/*  A B C  return R(A)(R(A+1), ... ,R(A+B-1))    */ 

OP_RETURN,/*  A B  return R(A), ... ,R(A+B-2)  (see note)  */ 


OP_FORLOOP,/*  A sBx  R(A)+=R(A+2); 

      if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/ 

OP_FORPREP,/*  A sBx  R(A)-=R(A+2); pc+=sBx        */ 


OP_TFORCALL,/*  A C  R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));  */ 

OP_TFORLOOP,/*  A sBx  if R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }*/ 


OP_SETLIST,/*  A B C  R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B  */ 


OP_CLOSURE,/*  A Bx  R(A) := closure(KPROTO[Bx])      */ 


OP_VARARG,/*  A B  R(A), R(A+1), ..., R(A+B-2) = vararg    */ 


OP_EXTRAARG/*  Ax  extra (larger) argument for previous opcode  */ 

} OpCode; 


********************************************************** 

                                
 虚拟机指令(2) MOVE & LOAD 


OP_MOVE  A  B  

OP_MOVE用来将寄存器B中的值拷贝到寄存器A中,由于Lua是基于寄存器虚拟机,大部分的指令都是直接对寄存器进行操作,而不需要对数据进行压栈和弹栈。OP_MOVE 指令的作用 是将一个Local变量复制给另一个local变量. 

例子: 

local a = 10;   

local b = a;   

编译出来的结果 

1   [1]  
LOAD        0 1 
;1代表的是常量表的项,这里代表的是10   

2   [2]  
MOVE        1 0  

所代表的二进制为 

                   
   B                 A        OP_Code  

Load    0  1  =  100000000 000000000 00000000   000001   =  0x80000001 ,也就是说, 0x80000001 的二进制所代表的指令为  Load  0  1,这里B中的最高位为1,表示的B为常量表的序号,而不是寄存器 


MOVE   1  0 =   000000000 000000000 00000001  000000   =  0x40 


*****************华丽分割线*********************************************** 

1.lua 的二进制格式,官方的luac.exe 编译出来的格式 



原始的lua 脚本为 

local a = 10 

local b = a 

print(b) 



下面介绍格式文件,介绍每个字段的意思.当然啦,这种格式是官方的,各个游戏公司可能会做一些改动,但是万变不离其宗。个个字段已经用颜色标明了 

在lua 的源文件中,前面四个字节  1b 4c 75 61  也就是 \033Lua , 标识的是lua文件的特有的标示符数据格式,代表是lua  

#define LUA_SIGNATURE  "\033Lua"  033时八进制  = 0x1b ,很多那些反编译工具判断这四个字节的值,来判断是否能反编译,很多公司都会偷偷的去掉或者用其他的值来替换,以迷惑菜鸟。呵呵 


52  第五个字节,表示的是,当前lua 的目标版本,这里指的是5.2 版本。 

感觉编辑的好痛苦,我还是直接贴我的比较图算了,看起来比较舒服 


 



函数的头描述 

linedefined   =    00 00 00 00   ;函数定义开始处的行号 

linedefined   =    00 00 00 00     ; 函数定义结束处的行号  
;顶级函数开始和结束行号都是为00 

numparams  =    00          ;固定参数的数目 number of fixed parameters  

is_vararg      =    01            ;可变参数标识符 

                                            • 1=VARARG_HASARG 

                                            • 2=VARARG_ISVARARG 

                                             • 4=VARARG_NEEDSARG 

maxstacksize  =  03         ;调用函数所需要的堆栈空间指令段 

sizecode         =   06 00 00 00  ; 函数中 指令的数目,缓存区的大小 = sizecode * sizeof(Instruction),每四个字节为一条指令 

code               =  02 00 00 00 41 00 00 00 87 40 40 00 c1 00 80 00 a0 40 00 01 1e 00 80 00 

                     

常量列表 保存着函数中引用的常量的列表 (常量池)  

Constant.sizek    =  02 00 00 00    ;常量列表的大小 ,缓存区的大小  = Constant.sizek * sizeof(TValue) = 2 * 8 = 16,每项为8个字节, 

TValue *               =                                                                             03 00 00 . 

                                           00 00 00 00 24 40 04 06 00 00 00 70 72 69 6e 74  ....$@.....print 

Constant list 数据结构   保存着函数中引用的常量的列表 (常量池)  

Integer 常量列表的大小 (sizek) 

[ 

    1 byte 常量类型 (value in parentheses):  • 0=LUA_TNIL, 1=LUA_TBOOLEAN,• 3=LUA_TNUMBER, 4=LUA_TSTRING 

     Const 常量本身: 如果常量类型是0这个域不存在;如果类型是1,这个是0或1;如果类型是3这个域是 Number;如果类型是4 这个域是String。 

] 

这里的String 是包含"0"为结束的字符串 

 


为什么上传图片以后,图片都变小了,而且不清晰呢? 


 ***********************给大家发一点福利,矫正虚拟机指令的函数**************************************
 //矫正虚拟机指令
 DWORD Rectify(DWORD Source);
 {
     DWORD Instruction = Source;
     BYTE  Source_OpCode =  Instruction & 0x3F;
     switch(Source_OpCode)
     {
   case OP_MOVE:
     Source_OpCode  = Target_OpCode; 
        break;
          ...
     }
    Instruction = ((Instruction & 0xFFFFFFC0) | Source_OpCode);
    return Instruction
 }