luac 格式分析与反编译

/ lua / 没有评论 / 2357浏览

luac 格式分析与反编译

测试某游戏时,游戏加载的是luac脚本:

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在各种二进制文件格式中比较常见,通过是特定文件的前几个字节,用来表示一种特定的文件格式。

在文件头后面,紧接着的是函数体部分。一个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;
typedef struct {
    uint32 linedefined;
    uint32 lastlinedefined;
    uchar numparams;
    uchar is_vararg;
    uchar maxstacksize;
} ProtoHeader;
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

luac.exe存储luac过程分析

luac的函数体在luac.exe中分main函数和一般函数,存储过程如下:

  1. 存储main函数头、代码、lua语句字符串进行存储
  2. 存储普通函数 2-1. 存储普通函数体 2-2. 遍历普通函数链表,直至结束
  3. 存储main函数行数、参数、参数名

具体过程

分析luac.exe存储过程,很容易分析出luac的文件格式。

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);
            /* complete it... */
            if (luaD_poscall(L, firstArg)){
                /* and correct top if not multiple results */
                L->top = L->ci->top;  
            } 
        }
        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对照表:

gameorgin
00
124
2c
3d
41
52
63
74
85
97
a8
b6
ca
db
e9
fe
10f
1110
1211
1312
1413
1514
1615
1716
1817
1918
1a19
1b1a
1c1b
1d1c
1e1d
1f1e
201f
2120
2221
2322
2423
2525

编译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: 资料参考: