1. 程式人生 > >luac 格式分析與反編譯

luac 格式分析與反編譯

前言

測試某遊戲時,遊戲載入的是luac指令碼:

  • 檔案格式 - 010editor官方bt只能識別luac52版本
  • opcode對照表 - 這個遊戲luac的opcode對照表也被重新排序,unluac需要找到lua vm的opcode對照表,才能反編譯。

文章目錄

luac51格式分析

Luac檔案格式

一個Luac檔案包含兩部分:檔案頭與函式體。

檔案頭格式

typedef struct {
    char signature[4];   //".lua"
    uchar version;
    uchar format;
    uchar endian;
    uchar size_int;
    uchar size_size_t;
    uchar size_Instruction;
    uchar size_lua_Number;
uchar lua_num_valid; uchar luac_tail[0x6]; } GlobalHeader;

第一個欄位**signature**在lua.h標頭檔案中有定義,它是LUA_SIGNATURE,取值為“\033Lua",其中,\033表示按鍵。LUA_SIGNATURE作為Luac檔案開頭的4位元組,它是Luac的Magic Number,用來標識它為Luac位元組碼檔案。Magic Number在各種二進位制檔案格式中比較常見,通過是特定檔案的前幾個位元組,用來表示一種特定的檔案格式。

version 欄位表示Luac檔案的格式版本,它的值對應於Lua編譯的版本,對於5.2版本的Lua生成的Luac檔案,它的值為0x52。

**format**欄位是檔案的格式標識,取值0代表official,表示它是官方定義的檔案格式。這個欄位的值不為0,表示這是一份經過修改的Luac檔案格式,可能無法被官方的Lua虛擬機器正常載入。

**endian**表示Luac使用的位元組序。現在主流的計算機的位元組序主要有小端序LittleEndian與大端序BigEndian。這個欄位的取值為1的話表示為LittleEndian,為0則表示使用BigEndian。

**size_int**欄位表示int型別所佔的位元組大小。size_size_t欄位表示size_t型別所佔的位元組大小。這兩個欄位的存在,是為了相容各種PC機與移動裝置的處理器,以及它們的32位與64位版本,因為在特定的處理器上,這兩個資料型別所佔的位元組大小是不同的。

**size_Instruction**欄位表示Luac位元組碼的程式碼塊中,一條指令的大小。目前,指令Instruction所佔用的大小為固定的4位元組,也就表示Luac使用等長的指令格式,這顯然為儲存與反編譯Luac指令帶來了便利。

**size_lua_Number**欄位標識lua_Number型別的資料大小。lua_Number表示Lua中的Number型別,它可以存放整型與浮點型。在Lua程式碼中,它使用LUA_NUMBER表示,它的大小取值大小取決於Lua中使用的浮點資料型別與大小,對於單精度浮點來說,LUA_NUMBER被定義為float,即32位大小,對於雙精度浮點來說,它被定義為double,表示64位長度。目前,在macOS系統上編譯的Lua,它的大小為64位長度。

**lua_num_valid**欄位通常為0,用來確定lua_Number型別能否正常的工作。

**luac_tail**欄位用來捕捉轉換錯誤的資料。在Lua中它使用LUAC_TAIL表示,這是一段固定的字串內容:"\x19\x93\r\n\x1a\n"。

在檔案頭後面,緊接著的是函式體部分。一個Luac檔案中,位於最上面的是一個頂層的函式體,函式體中可以包含多個子函式,子函式可以是巢狀函式、也可以是閉包,它們由常量、程式碼指令、Upvalue、行號、區域性變數等資訊組成。

函式體

typedef struct {
    //header
    ProtoHeader header;

    //code
    Code code;

    // constants
    Constants constants;

    // functions
    Protos protos;

    // upvalues
  //Upvaldescs upvaldescs;

    // string
    //SourceName src_name;

    // lines
    Lines lines;
    
    // locals
    LocVars loc_vars;
    
    // upvalue names
    UpValueNames names;
} Proto;

**ProtoHeader**是Proto的頭部分。它的定義如下:

typedef struct {
    uint32 linedefined;
    uint32 lastlinedefined;
    uchar numparams;
    uchar is_vararg;
    uchar maxstacksize;
} ProtoHeader;

**code**lua vm操作碼
constants lua語句其實只是將標誌符合關鍵字給去掉了

printf("Hello %s from %s on %s\n",os.getenv"USER" or "there",_VERSION,os.date())

這是一條lua語句,那麼在luac檔案中可見,可以推測出大概的lua語句

���printf����Hello %s from %s on %s
����os����getenv����USER����there�  ���_VERSION����date

**protos**下一個函式體,函式體是鏈式儲存的
**lines**該函式存在多少行lua語句
**loc_vars**函式內部區域性變數個數
**names**函式內部區域性變數名

luac.exe儲存luac過程分析

luac的函式體在luac.exe中分main函式和一般函式,儲存過程如下:
1、儲存main函式頭、程式碼、lua語句字串進行儲存
2、儲存普通函式
2.1、儲存普通函式體
2.2、遍歷普通函式連結串列,直至結束
3、儲存main函式行數、引數、引數名

#### 具體過程
分析luac.exe儲存過程,很容易分析出luac的檔案格式。
```cpp
int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip)
{
 DumpState D;
 D.L=L;
 D.writer=w;
 D.data=data;
 D.strip=strip;
 D.status=0;
 DumpHeader(&D);
 DumpFunction(f,NULL,&D);
 return D.status;
}

lua原始碼luac.c中,DumpHeader 儲存了luac頭格式,而在**DumpFunction**則對函式體進行了儲存。

static void DumpHeader(DumpState* D)
{
 char h[LUAC_HEADERSIZE];
 luaU_header(h);
 DumpBlock(h,LUAC_HEADERSIZE,D);
}

**DumpHeader**儲存了LUAC_HEADERSIZE(12)byte的頭格式。

static void DumpConstants(const Proto* f, DumpState* D)
{
 int i,n=f->sizek;
 DumpInt(n,D);
 for (i=0; i<n; i++)
 {
  const TValue* o=&f->k[i];
  DumpChar(ttype(o),D);
  switch (ttype(o))
  {
   case LUA_TNIL:
  break;
   case LUA_TBOOLEAN:
  DumpChar(bvalue(o),D);
  break;
   case LUA_TNUMBER:
  DumpNumber(nvalue(o),D);
  break;
   case LUA_TSTRING:
  DumpString(rawtsvalue(o),D);
  break;
   default:
  lua_assert(0);      /* cannot happen */
  break;
  }
 }
 n=f->sizep;
 DumpInt(n,D);
 for (i=0; i<n; i++) DumpFunction(f->p[i],f->source,D);
}

static void DumpDebug(const Proto* f, DumpState* D)
{
 int i,n;
 n= (D->strip) ? 0 : f->sizelineinfo;
 DumpVector(f->lineinfo,n,sizeof(int),D);
 n= (D->strip) ? 0 : f->sizelocvars;
 DumpInt(n,D);
 for (i=0; i<n; i++)
 {
  DumpString(f->locvars[i].varname,D);
  DumpInt(f->locvars[i].startpc,D);
  DumpInt(f->locvars[i].endpc,D);
 }
 n= (D->strip) ? 0 : f->sizeupvalues;
 DumpInt(n,D);
 for (i=0; i<n; i++) DumpString(f->upvalues[i],D);
}

static void DumpFunction(const Proto* f, const TString* p, DumpState* D)
{
 DumpString((f->source==p || D->strip) ? NULL : f->source,D);
 DumpInt(f->linedefined,D);
 DumpInt(f->lastlinedefined,D);
 DumpChar(f->nups,D);
 DumpChar(f->numparams,D);
 DumpChar(f->is_vararg,D);
 DumpChar(f->maxstacksize,D);
 DumpCode(f,D);
 DumpConstants(f,D);
 DumpDebug(f,D);
}

遞迴遍歷儲存函式體過程。


luac反編譯

獲取lua51 vm

  $ git clone https://github.com/mkottman/AndroLua
  $ ndk-build

lua_execute函式

在lua vm中luac解釋執行luac opcode的函式是luaV_execute,但是有些so會將lua_execute函式名隱藏,以下是通過lua_resume找到execute函式。

  LUA_API int lua_resume (lua_State *L, int nargs) {
    int status;
    lua_lock(L);
    if (L->status != LUA_YIELD && (L->status != 0 || L->ci != L->base_ci))
        return resume_error(L, "cannot resume non-suspended coroutine");
    if (L->nCcalls >= LUAI_MAXCCALLS)
      return resume_error(L, "C stack overflow");
    luai_userstateresume(L, nargs);
    lua_assert(L->errfunc == 0);
    L->baseCcalls = ++L->nCcalls;
    status = luaD_rawrunprotected(L, resume, L->top - nargs);
    if (status != 0) {  /* error? */
      L->status = cast_byte(status);  /* mark thread as `dead' */
      luaD_seterrorobj(L, status, L->top);
      L->ci->top = L->top;
    }
    else {
      lua_assert(L->nCcalls == L->baseCcalls);
      status = L->status;
    }
    --L->nCcalls;
    lua_unlock(L);
    return status;
  }

lua_resume中呼叫luaD_rawrunprotected函式,將resum函式指標作為引數傳入

  static void resume (lua_State *L, void *ud) {
    StkId firstArg = cast(StkId, ud);
    CallInfo *ci = L->ci;
    if (L->status == 0) {  /* start coroutine? */
      lua_assert(ci == L->base_ci && firstArg > L->base);
      if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA)
        return;
    }
    else {  /* resuming from previous yield */
      lua_assert(L->status == LUA_YIELD);
      L->status = 0;
      if (!f_isLua(ci)) {  /* `common' yield? */
        /* finish interrupted execution of `OP_CALL' */
        lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL ||
                   GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL);
        if (luaD_poscall(L, firstArg))  /* complete it... */
          L->top = L->ci->top;  /* and correct top if not multiple results */
      }
      else  /* yielded inside a hook: just continue its execution */
        L->base = L->ci->base;
    }
    luaV_execute(L, cast_int(L->ci - L->base_ci));
  }

resum函式中呼叫luaV_execute

void luaV_execute (lua_State *L, int nexeccalls) {

    ...

    switch (GET_OPCODE(i)) {
      case OP_MOVE: {
        setobjs2s(L, ra, RB(i));
        continue;
      }
      case OP_LOADK: {
        setobj2s(L, ra, KBx(i));
        continue;
      }
      case OP_LOADBOOL: {
        setbvalue(ra, GETARG_B(i));
        if (GETARG_C(i)) pc++;  /* skip next instruction (if C) */
        continue;
      }

    ...

    }
  }
}

luaV_execute中存在38個switch case,然後再該遊戲的lua vm中找到execute函式同樣也是38個switch case,分析得到opcode對照表:

game orgin
  0 0
  1 24
  2 c
  3 d
  4 1
  5 2 
  6 3 
  7 4 
  8 5
  9 7 
  a 8
  b 6
  c a
  d b
  e 9
  f e
  10 f
  11 10
  12 11
  13 12 
  14 13
  15 14
  16 15
  17 16
  18 17
  19 18
  1a 19
  1b 1a
  1c 1b
  1d 1c
  1e 1d
  1f 1e
  20 1f
  21 20
  22 21
  23 22
  24 23
  25 25

編譯unluac

將分析出來的對照表覆蓋到unluac的OpcodeMap.java中進行編譯,生成unluac.jar

    } else if(version == 0x51) {
    map = new Op[38];
    map[0] = Op.MOVE;
    map[36] = Op.LOADK;
    map[12] = Op.LOADBOOL;
    map[13] = Op.LOADNIL;
    map[1] = Op.GETUPVAL;
    map[2] = Op.GETGLOBAL;
    map[3] = Op.GETTABLE;
    map[4] = Op.SETGLOBAL;
    map[7] = Op.SETUPVAL;
    map[5] = Op.SETTABLE;
    map[8] = Op.NEWTABLE;
    map[9] = Op.SELF;
    map[10] = Op.ADD;
    map[11] = Op.SUB;
    map[6] = Op.MUL;
    map[14] = Op.DIV;
    map[15] = Op.MOD;
    map[16] = Op.POW;
    map[17] = Op.UNM;
    map[18] = Op.NOT;
    map[19] = Op.LEN;
    map[20] = Op.CONCAT;
    map[21] = Op.JMP;
    map[22] = Op.EQ;
    map[23] = Op.LT;
    map[24] = Op.LE;
    map[25] = Op.TEST;
    map[26] = Op.TESTSET;
    map[27] = Op.CALL;
    map[28] = Op.TAILCALL;
    map[29] = Op.RETURN;
    map[30] = Op.FORLOOP;
    map[31] = Op.FORPREP;
    map[32] = Op.TFORLOOP;
    map[33] = Op.SETLIST;
    map[34] = Op.CLOSE;
    map[35] = Op.CLOSURE;
    map[37] = Op.VARARG; 
  } else if(version == 0x52) {

最後嘗試反編譯luac檔案,成功!!!~~~

java -jar unluac.jar test.lua

010 editor模版:https://github.com/saidyou/unluc.git

luac

Note: 資料參考: