Lua程序逆向之Luac位元組碼與反彙編

作者:非蟲

預估稿費:800RMB

投稿方式:發送郵件至linwei#360.cn,或登陸網頁版在線投稿

傳送門


【技術分享】Lua程序逆向之Luac文件格式分析

簡介


在了解完了Luac位元組碼文件的整體結構後,讓我們把目光聚焦,放到更具體的指令格式上。Luac位元組碼指令是整個Luac最精華、也是最具有學習意義的一部分,了解它的格式與OpCode相關的知識後,對於逆向分析Luac,會有事半功倍的效果,同時,也為自己開發一款虛擬機執行模板與引擎打下良好的理論基礎。

指令格式分析


Luac指令在Lua中使用Instruction來表示,是一個32位大小的數值。在Luac.bt中,我們將其定義了為Inst結構體,回顧一下它的定義與讀取函數:

typedef struct(int pc) {nlocal int pc_ = pc;nlocal uchar inst_sz = get_inst_sz();nif (inst_sz == 4) {nuint32 inst;n} else {nWarning("Error size_Instruction.");n}n} Inst <optimize=false>;n

定義的每一條指令為uint32,這與ARM處理器等長的32位指令一樣,但不同的是,Lua 5.2使用的指令只有40條,也就是說,要為其Luac編寫反彙編引擎,比起ARM指令集,在工作量上要少出很多。

Luac指令完整由:OpCode、OpMode操作模式,以及不同模式下使用的不同的操作數組成。

官方5.2版本的Lua使用的指令有四種格式,使用OpMode表示,它的定義如下:

enum OpMode {iABC, iABx, iAsBx, iAx};n

其中,i表示6位的OpCode;A表示一個8位的數據;B表示一個9位的數據,C表示一個9位的無符號數據;後面跟的x表示數據組合,如Bx表示B與C組合成18位的無符號數據,Ax表示A與B和C共同組成26位的無符號數據。sBx前的s表示是有符號數,即sBx是一個18位的有符號數。

ABC這些位元組大小與起始位置的定義如下:

#define SIZE_C9n#define SIZE_B9n#define SIZE_Bx(SIZE_C + SIZE_B)n#define SIZE_A8n#define SIZE_Ax(SIZE_C + SIZE_B + SIZE_A)n#define SIZE_OP6n#define POS_OP0n#define POS_A(POS_OP + SIZE_OP)n#define POS_C(POS_A + SIZE_A)n#define POS_B(POS_C + SIZE_C)n#define POS_BxPOS_Cn#define POS_AxPOS_An

從定義中可以看來,從位0開始,ABC的排列為A->C->B。

以小端序為例,完整的指令格式定義如下表所示:

先來看最低6位的OpCode,在Lua中,它使用枚舉表示,5.2版本的Lua支持40條指令,它們的定義如下所示:

typedef enum {n/*----------------------------------------------------------------------nnameargsdescriptionn------------------------------------------------------------------------*/nOP_MOVE,/*A BR(A) := R(B)*/nOP_LOADK,/*A BxR(A) := Kst(Bx)*/nOP_LOADBOOL,/*A B CR(A) := (Bool)B; if (C) pc++*/nOP_LOADNIL,/*A BR(A) := ... := R(B) := nil*/nOP_GETUPVAL,/*A BR(A) := UpValue[B]*/nOP_GETGLOBAL,/*A BxR(A) := Gbl[Kst(Bx)]*/nOP_GETTABLE,/*A B CR(A) := R(B)[RK(C)]*/nOP_SETGLOBAL,/*A BxGbl[Kst(Bx)] := R(A)*/nOP_SETUPVAL,/*A BUpValue[B] := R(A)*/nOP_SETTABLE,/*A B CR(A)[RK(B)] := RK(C)*/n......nOP_CLOSE,/*A close all variables in the stack up to (>=) R(A)*/nOP_CLOSURE,/*A BxR(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))*/nOP_VARARG/*A BR(A), R(A+1), ..., R(A+B-1) = vararg*/n} OpCode;n

OpCode定義的注釋中,詳細說明了每一條指令的格式、使用的參數,以及它的含義。以第一條OP_MOVE指令為例,它接受兩個參數R(A)與R(B),的作用是完成一個賦值操作「R(A) := R(B)」

從指令的格式可以看出,儘管OpCode定義的注釋中描述了每條指令使用的哪種OpMode,但32位的指令格式中,並沒有指出到底每個OpCode對應哪一種OpMode,Lua的解決方法是單獨做了一張OpMode的表格luaP_opmodes,它的定義如下:

LUAI_DDEF const lu_byte luaP_opmodes[NUM_OPCODES] = {n/* T A B C mode opcode*/nopmode(0, 1, OpArgR, OpArgN, iABC)/* OP_MOVE */n,opmode(0, 1, OpArgK, OpArgN, iABx)/* OP_LOADK */n,opmode(0, 1, OpArgN, OpArgN, iABx)/* OP_LOADKX */n,opmode(0, 1, OpArgU, OpArgU, iABC)/* OP_LOADBOOL */n,opmode(0, 1, OpArgU, OpArgN, iABC)/* OP_LOADNIL */n,opmode(0, 1, OpArgU, OpArgN, iABC)/* OP_GETUPVAL */n,opmode(0, 1, OpArgU, OpArgK, iABC)/* OP_GETTABUP */n,opmode(0, 1, OpArgR, OpArgK, iABC)/* OP_GETTABLE */n,opmode(0, 0, OpArgK, OpArgK, iABC)/* OP_SETTABUP */n,opmode(0, 0, OpArgU, OpArgN, iABC)/* OP_SETUPVAL */n,opmode(0, 0, OpArgK, OpArgK, iABC)/* OP_SETTABLE */n,opmode(0, 1, OpArgU, OpArgU, iABC)/* OP_NEWTABLE */n,opmode(0, 1, OpArgR, OpArgK, iABC)/* OP_SELF */n,opmode(0, 1, OpArgK, OpArgK, iABC)/* OP_ADD */n,opmode(0, 1, OpArgK, OpArgK, iABC)/* OP_SUB */n......n,opmode(0, 1, OpArgU, OpArgN, iABx)/* OP_CLOSURE */n,opmode(0, 1, OpArgU, OpArgN, iABC)/* OP_VARARG */n,opmode(0, 0, OpArgU, OpArgU, iAx)/* OP_EXTRAARG */n};n

構成完整的OpMode列表使用了opmode宏,它的定義如下:

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))n

它將OpMode相關的數據採用一位元組表示,並將其組成劃分為以下幾個部分:

m位,占最低2位,即前面OpMode中定義的四種模式,通過它,可以確定OpCode的參數部分。

c位,佔2~3位,使用OpArgMask表示,說明C參數的類型。定義如下:

enum OpArgMask {nOpArgN, /* 參數未被使用 */nOpArgU, /* 已使用參數 */nOpArgR, /* 參數是寄存器或跳轉偏移 */nOpArgK /* 參數是常量或寄存器常量 */n};n

b位,佔4~5位。使用OpArgMask表示,說明B參數的類型。

a位,佔位6。表示是否是寄存器操作。

t位,佔位7。表示是否是測試操作。跳轉和測試指令該位為1。

luaP_opmodes的值使用如下代碼列印出來:

printf("opcode ver 5.2:n");nfor (int i=0; i<sizeof(luaP_opmodes); i++) {nprintf("0x%x, ", luaP_opmodes[i]);n}nprintf("n");n

輸出如下:

opcode ver 5.2:n0x60, 0x71, 0x41, 0x54, 0x50, 0x50, 0x5c, 0x6c, 0x3c, 0x10, 0x3c, 0x54, 0x6c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x7c, 0x60, 0x60, 0x60, 0x68, 0x22, 0xbc, 0xbc, 0xbc, 0x84, 0xe4, 0x54, 0x54, 0x10, 0x62, 0x62, 0x4, 0x62, 0x14, 0x51, 0x50, 0x17,n

可以看到,有很多指令的OpMode是相同的,比如有多條指令對應的值都是0x7c,如果OpMode的順序經過修改,要想通過OpMode直接還原所有的指令,是無法做到的,需要配合其他方式來還原,比如Lua虛擬機對指令的處理部分。

反彙編引擎實現


編寫反彙編引擎需要做到以下幾點:

正確的識別指令的OpCode。識別該條指令對應的OpCode,了解當前指令的作用。

處理指令的參數列表。解析不同指令使用到的參數信息,與OpCode在一起可以完成指令反彙編與指令的語義轉換。

指令解析。反彙編引擎應該能夠支持所有的指令。

指令語義轉換。完成反彙編後,加入語義轉換,更加方便了解指令的意圖。

處理指令依賴關係。處理語義轉換時,需要處理好指令之前的關係信息。

下面,我們一條條看如何實現。

OpCode獲取

首先是通過指令獲取對應的OpCode,即傳入一個32位的指令值,返回一個OpCode的名稱。Lua中有一個GET_OPCODE宏可以通過指令返回對應的OpCode,定義如下:

#define GET_OPCODE(i)n(cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))n

這個宏在010 Editor模板語法中並不支持,因此,實現上,需要編寫展開後的代碼,並將其定義為函數。功能上就是取32位指令的最低6位,代碼如下所示:

uchar GET_OPCODE(uint32 inst) {nreturn ((inst)>>POS_OP) & ((~((~(Instruction)0)<<(SIZE_OP)))<<(0));n}n

參數獲取

取指令的參數,包括取指令的A、B、C、Bx、Ax、sBx等信息。前面已經介紹了它們在指令中的位偏移,因此,獲取這些參數信息與獲取OpCode一樣,Lua中提供了GETARG_A、GETARG_B、GETARG_C、GETARG_Bx、GETARG_Ax、GETARG_sBx等宏來完成這些功能,定義如下:

#define GETARG_A(i)getarg(i, POS_A, SIZE_A)n#define GETARG_B(i)getarg(i, POS_B, SIZE_B)n#define GETARG_C(i)getarg(i, POS_C, SIZE_C)n#define GETARG_Bx(i)getarg(i, POS_Bx, SIZE_Bx)n#define GETARG_Ax(i)getarg(i, POS_Ax, SIZE_Ax)n#define GETARG_sBx(i)(GETARG_Bx(i)-MAXARG_sBx)n

同樣的,010 Editor模板語法不支持直接定義這些宏,需要編寫展開後的代碼,實現如下:

int GETARG_A(uint32 inst) {nreturn ((inst)>>POS_A) & ((~((~(Instruction)0)<<(SIZE_A)))<<(0));n}nint GETARG_B(uint32 inst) {nreturn ((inst)>>POS_B) & ((~((~(Instruction)0)<<(SIZE_B)))<<(0));n}nint GETARG_C(uint32 inst) {nreturn ((inst)>>POS_C) & ((~((~(Instruction)0)<<(SIZE_C)))<<(0));n}nint GETARG_Bx(uint32 inst) {nreturn ((inst)>>POS_Bx) & ((~((~(Instruction)0)<<(SIZE_Bx)))<<(0));n}nint GETARG_Ax(uint32 inst) {nreturn ((inst)>>POS_Ax) & ((~((~(Instruction)0)<<(SIZE_Ax)))<<(0));n}nint GETARG_sBx(uint32 inst) {nreturn GETARG_Bx(inst)-MAXARG_sBx;n}n

指令解析

在指令解析的編寫工作上,參考了luadec的反彙編引擎。它的實現主要位於luadec_disassemble()函數。這裡要做的工作就是將它的所有代碼與語法都進行一次010 Editor模板語法化。代碼片斷如下:

// luadec_disassemble() from luadec disassemble.cnstring InstructionRead(Inst &inst) {nlocal int i = inst.inst;nOpCode o = (OpCode)GET_OPCODE(i);n/*nPrintf("inst: 0x%xn", o);n*/nlocal int a = GETARG_A(i);nlocal int b = GETARG_B(i);nlocal int c = GETARG_C(i);nlocal int bc = GETARG_Bx(i);nlocal int sbc = GETARG_sBx(i);nlocal int dest;nlocal string line;nlocal string lend;nlocal string tmpconstant1;nlocal string tmpconstant2;nlocal string tmp;nlocal string tmp2;nlocal uchar lua_version_num = get_lua_version();nlocal int pc = inst.pc_;nn//Printf("Inst: %sn", EnumToString(o));nswitch (o) {ncase OP_MOVE:n/* A B R(A) := R(B) */nSPrintf(line,"R%d R%d",a,b);nSPrintf(lend,"R%d := R%d",a,b);nbreak;ncase OP_LOADK: //FIXME OP_LOADK DecompileConstantn/* A Bx R(A) := Kst(Bx) */nSPrintf(line,"R%d K%d",a,bc);n//Printf("OP_LOADK bc:%dn", bc);ntmpconstant1 = DecompileConstant(parentof(parentof(inst)),bc);nSPrintf(lend,"R%d := %s",a,tmpconstant1);nbreak;n......ncase OP_CLOSURE:n{n/* A Bx R(A) := closure(KPROTO[Bx]) */nSPrintf(line,"R%d %d",a,bc);nSPrintf(lend, "R%d := closure(Function #%d)", a, bc);nbreak;n}ndefault:nbreak;nn}nnlocal string ss;nSPrintf(ss, "[%d] %-9s %-13s; %sn", pc, get_opcode_str(o),line,lend);nnreturn ss;n}n

上面的代碼中,通過GET_OPCODE獲取OpCode後,分別對它進行判斷與處理,參數信息在函數的最開始獲取,方便指令中使用。pc表示當前執行的指令所在位置,方便代碼中做語義轉換與依賴處理。代碼中這一行需要注意:

DecompileConstant(parentof(parentof(inst))n

因為處理指令時,需要讀取指令所在Proto的常量信息,但010 Editor尷尬的模板語法不支持傳遞指針,也不支持引用類型作為函數的返回值,這導致無法直接讀到到Proto的Constants信息。幸好新版本的010 Editor的模板語法加入了self與parentof關鍵字,用於獲取當前結構體與父結構體的欄位信息,因此,這裡需要對Proto結構體進行修改,讓Code結構體成為它的內聯的子結構體,如下所示:

typedef struct(string level) {nlocal string level_ = level;n//Printf("level:%sn", level_);n//headernProtoHeader header;n//coden//Code code;nstruct Code {nuint32 sizecode <format=hex>;nlocal uchar inst_sz = get_inst_sz();nlocal int pc = 1;nif (inst_sz == 4) {nlocal uint32 sz = sizecode;nwhile (sz-- > 0) {nInst inst(pc);npc++;n}n} else {nWarning("Error size_Instruction.");n}n ntypedef struct(int pc) {nlocal int pc_ = pc;nlocal uchar inst_sz = get_inst_sz();nif (inst_sz == 4) {nuint32 inst;n} else {nWarning("Error size_Instruction.");n}n} Inst <read=InstructionRead, optimize=false>;n n} code <optimize=false>;n......n// upvalue namesnUpValueNames names;n} Proto <read=ProtoRead>;n

然後在代碼中,通過parentof(parentof(inst)就能夠返回一個Proto的引用類型,然後就可以愉快的讀Proto中所有的欄位數據了。

指令語義轉換

所謂語義轉換,就是將直接的指令格式表示成可以讀懂的指令反彙編語句。如指令0x0000C1,反彙編後,它的指令表示為「LOADK R3 K0」,LOADK為OpCode的助記符,這裡取助記符時,直接通過010 Editor模板函數EnumToString(),傳入OpCode名,然後去掉前面的OP_就可以獲得。使用get_opcode_str()實現該功能,代碼如下:

string get_opcode_str(OpCode o) {nstring str = EnumToString(o);nstr = SubStr(str, 3);n nreturn str;n}n

R3表示寄存器,K0表示常量1,即當前函數的Constants中索引為0的Constant。這一條指令經過語義轉換後就變成了「R3 := xxx」,這個xxx是常量的值,需要通過DecompileConstant()獲取它具體的值。

在進行語義轉換時,將處理後的指令信息保存到line字元串中,將語義字元串轉換到lend字元串中,處理完後輸出時加在一起,中間放一個分號。如下所示是指令處理後的輸出效果:

struct Inst inst[1]n[2] LOADK R3 K0 ; R3 := 1n

指令依賴處理

指令依賴是什麼意思呢?即一條指令想要完整的了解它的語義,需要依賴它前面或後面的指令,就解析該指令時,需要用到指令前面或後面的數據。

拿OP_LE指令來說,它的注釋部分如下:

/*nA B Cnif ((RK(B) <= RK(C)) ~= A) then pc++ n*/n條件滿足時,跳轉去執行,否則pc向下,在編寫反彙編引擎時,使用的代碼片斷如下:n

case OP_LE:n{n/*A B Cif ((RK(B) <= RK(C)) ~= A) then pc++ */ndest = GETARG_sBx(parentof(inst).inst[pc+1].inst) + pc + 2;nSPrintf(line,"%d %c%d %c%d",a,CC(b),CV(b),CC(c),CV(c));ntmpconstant1 = RK(parentof(parentof(inst)), b);ntmpconstant2 = RK(parentof(parentof(inst)), c);nSPrintf(lend,"if %s %s %s then goto [%d] else goto [%d]",tmpconstant1,(a?invopstr(o):opstr(o)),tmpconstant2,pc+2,dest);nbreak;n}n

dest是要跳轉的目標地址,GETARG_sBx()返回的是一個有符號的跳轉偏移,因為指令是可以向前與向後進行跳轉的,RK宏判斷參數是寄存器還是常量,然後返回它的值,這裡的實現如下:

string RegOrConst(Proto &f, int r) {nif (ISK(r)) {nreturn DecompileConstant(f, INDEXK(r));n} else {nstring tmp;nSPrintf(tmp, "R%d", r);nreturn tmp;n}n}n//#define RK(r) (RegOrConst(f, r))nstring RK(Proto &f, int r) {nreturn (RegOrConst(f, r));n}n

最終,OP_LE指令處理後輸出如下:

struct Inst inst[35] [36] LE 0 R5 R6 ; if R5 <= R6 then goto [38] else goto [40]n

其他所有的指令的處理可以參看luadec_disassemble()的代碼,這裡不再展開。

最後,所有的代碼編寫完成後,效果如圖所示:

luac.bt的完整實現可以在這裡找到:github.com/feicong/lua_


推薦閱讀:

【遊戲安全】看我如何通過hook攻擊LuaJIT
Lua 語言有哪些不足?
公式計算機的另一種實現思路

TAG:编程 | Lua | 网络安全 |