吾愛破解 - LCG - LSG |安卓破解|病毒分析|破解軟件|www.kvamco.live

 找回密碼
 注冊[Register]

QQ登錄

只需一步,快速開始

搜索
查看: 4094|回復: 37
上一主題 下一主題

[Android 轉帖] 解析android手游lua腳本的加密與解密(番外篇之反編譯的對抗)

[復制鏈接]
跳轉到指定樓層
樓主
薛-藍狐 發表于 2019-10-24 22:46 回帖獎勵
本帖最后由 薛-藍狐 于 2019-10-24 23:13 編輯

前言
去年在看雪論壇寫了一篇《淺析android手游lua腳本的加密與解密》的精華文章,今年寫一篇番外篇,將一些lua反編譯對抗的內容整合一起,并以3個實例作為說明(包括2018騰訊游戲競賽和夢幻西游手游相關的補充),文章開頭還增加了相關工作,方便大家學習lua逆向時使用。本文由3篇文章整合成1篇,所以內容上面有點多,有興趣的朋友需要點耐心,當然也可以跳著看。最后,請大佬們不吝賜教。最最后,大家有問題也歡迎留言,一起交流學習。

相關工作
為了能讓一些同學更好的學習lua的逆向,我把收集的一些資料組合成一篇lua加解密的相關工作給大家參考。看這節內容之前還是需要一些lua的基礎知識,這里推薦云風大佬的《Lua源碼欣賞》[19],建議結合搜索引擎學習之。

文章分2部分介紹,第1部分介紹lua加解密的相關文章介紹,第2部分介紹lua的相關工具。

文章介紹
這一節介紹了互聯網上對lua的各種相關文章,包括lua的加解密如文件格式的解析、基于lua的游戲和比賽的介紹、lua的hook技術等。

1. lua加解密入門:

非蟲大佬[1-4] 寫了4篇關于luac和luajit文件格式和字節碼的相關文章,并開源了010Editor的解析luac和luajit的模板代碼。Ganlv 同學[7] 在吾愛破解寫了7篇關于lua加解密的系列教程。騰訊gslab[9] 寫了一篇關于lua游戲逆向的入門介紹,這是一篇比較早的lua游戲解密的文章。INightElf 同學[10] 寫了一篇關于lua腳本反編譯入門的文章。

2. 基于lua的手游:

lua不僅能用于端游戲,也能用于手游,而且由于手游的火熱,帶動了lua逆向相關分析文章的分享。wmsuper 同學[11] 在android平臺下解密了騰訊游戲開心消消樂的lua腳本,后續可以通過修改lua腳本達到作弊的目的。Unity 同學[8] 通過hook的方法解密和修改lua手游《放置江湖》的流程,達到修改游戲獎勵的目的。littleNA 同學[12] 通過3種方式解密了3個手游的lua腳本,并且修復了夢幻手游lua opcode的順序。

3. 基于lua的比賽:

隨著國內CTF的發展,lua技術也運用到了比賽中。看雪ctf2016第2題[13]、2017第15題[14]和騰訊游戲安全2018決賽第2題[15]都使用了lua引擎作為載體的CrackMe比賽,其中看雪2016將算法驗證用lua代碼實現并編譯成luac,最后還修改了luac的文件頭,使得反編譯工具報錯;看雪2017的題使用殼和大量的混淆,最后一步是luajit的簡單異或運算;騰訊2018使用的lua技術更加深入,進階版更是修改了lua的opcode順序,并使用lua編寫了一個虛擬機。以上3題的writeup網上都可以搜索到,有興趣的朋友可以練練手,加深印象。

4. lua hooking:

Hook是修改軟件流程的常用手段,lua中也存在hook技術。曾半仙 同學[9] 在看雪發布了一種通過hook lua字節碼達到修改游戲邏輯的方法,并發布了一個lua匯編引擎。Nikc Cano[5] 的blog寫了一篇關于Hooking luajit的文章,興趣使然的小胃 同學[6] 對該篇文章進行了翻譯。

工具介紹
逆向解密lua和luajit游戲都有相關的工具,這一節將對一些主流的工具進行介紹。

1. lua相關:

luadec [16]:這是一個用c語言結合lua引擎源碼寫的開源lua反編譯器,解析整個lua字節碼文件并盡可能的還原為源碼。當然,由于還原的是高級語言,所以兼容性一般,當反編譯大量文件時肯定會遇到bug,這時就需要自己手動修復bug;并且很容易被針對造成反編譯失敗。目前支持的版本有lua5.1,5.2和5.3。

chunkspy:一款非常有用的lua分析工具,本身就是lua語言所寫。它解析了整個lua字節文件,由于其輸出的是lua的匯編形式,所以兼容性非常高,也造成了一定的閱讀障礙。chunkspy 不僅可以解析luac文件,它還包括了一個交互式的命令,可以將輸入的lua腳本轉換成lua字節碼匯編的形式,這對學習lua字節碼非常有幫助。luadec工具中集成了這個腳本,目前支持的版本也是有lua5.1,5.2和5.3。

unluac:這也是一個開源的lua反編譯器,java語言所寫,相比luadec 工具兼容性更低,。一般很少使用,只支持lua5.1,當上面工具都失效時可以嘗試。

2. luajit相關:

luajit-decomp[17]:github開源的一款luajit反編譯工具,使用au3語言編寫。先通過luajit原生的exe文件將luajit字節碼文件轉換成匯編,然后該工具再將luajit匯編轉換成lua語言。由于反匯編后的luajit字節碼缺少很多信息,如變量名、函數名等,造成反編譯后的結果讀起來比較隱晦,類似于IDA的F5。但是兼容性超好,只要能夠反匯編就能夠反編譯,所以使用時需要替換對應版本的luajit引擎(滿足反匯編的需求)。目前是支持所有的luajit版本。

ljd[18]:也是github開源的一款luajit反編譯工具,使用python編寫,與luajit-decomp 反編譯luajit匯編的方式不同,其從頭解析了整個luajit文件,能夠獲取更多的信息,還原的程度更高,但是由于精度更高,所以兼容性也會弱一點。查看該項目的fork可以獲取更多的其他兼容版本,目前支持的版本有luajit2.0、luajit2.1等。

反編譯對抗
眾所周知,反匯編/反編譯 工具在逆向人員工作中第一步被使用,其地位非常之高,而對于軟件保護者來說,如何對抗 反匯編/反編譯 就顯得尤為重要。例如,動態調試中對OD的的檢測、內核調試對windbg的破壞、加殼加花對IDA靜態分析的阻礙、apktool的bug導致對修改后的apk反編譯失敗、修改PE頭導致OD無法識別、修改 .Net dll中的區段導致ILspy工具失效等等例子,都說明對抗反編譯工具是很常用的一種軟件保護手段。當然,lua的反編譯工具也面臨這個問題。處理這樣的問題無非就幾種思路:

用調試器調試反編譯工具為何解析錯誤,排查原因。

用調試器調試原引擎是如何解析文件的。

用文件格式解析工具解析文件,看哪個點解析出錯。

下面將以3個例子來實戰lua反編譯是如何對抗與修復。

例子1:一個簡單的問題
這是在看雪論壇看到的一個問題,問題是由于游戲(可能是征途手游)將lua字符串的長度int32修改為int64,導致反編譯失敗的一個例子,內容較為簡單,修復方法請看帖子中本人的回答,地址:https://bbs.pediy.com/thread-217033.htm

例子2:2018騰訊游戲安全競賽
這一節以2018騰訊游戲安全競賽決賽第二題進階版第1關的題目為例子,主要是講一下如何修復當lua的opcode被修改的情況,以及如何修復該題對抗lua反編譯的問題。

opcode問題及其修復
修復opcode的目的是 當輸入題目的luac文件,反匯編工具Chunkspy和反編譯工具luadec能夠輸出正確的結果。

首先,我們在ida中分析lua引擎tmgs.dll文件,然后定位到luaV_execute函數(搜索字符串“ 'for' limit must be a number ”),發現switch下的case的參數(lua的opcode)是亂序的,到這里我們就能夠確認,該題的lua虛擬機opcode被修改了。

接著,我們進行修復操作。一種很耗時的辦法就是一個一個opcode還原,分析每一個case下面的代碼然后找出對應opcode的順序。但是這一題我們不用這么麻煩,通過對比分析我們發現普通版的題目并沒有修改opcode:

普通版lua引擎的luaV_execute函數        進階版lua引擎的luaV_execute函數
       
觀察發現,進階版的題目只是修改了每個case的數值或者多個值映射到同一個opcode,但是沒有打亂case里的代碼(也就是說,虛擬機解析opcode代碼的順序沒有變,只是修改了對應的數值,這跟夢幻手游的打亂opcode的方法不同)。由于lua5.3只使用到0x2D的opcode,而一個opcode長度為6位(0x3F),該題就將剩余的沒有使用的字節映射到同一個opcode下,修復時只需要反過來操作就可以了。分析到這里,我們的修復方案就出來了:

通過ida分別導出2個版本的 luaV_execute 的文本

通過python腳本提取opcode的修復表

在工具(Chunkspy和luadec)初始化lua文件后,用修復表將opcode替換

測試運行,修復其他bug

第一步直接IDA手動導出: File --> Produce file --> Create LST File ;第二步使用python分析,代碼如下:

# -*- coding: utf-8 -*-# 通過掃碼IDA導出的文本文件,獲取lua字節碼的opcode順序def get_opcode(filepath):    f = open(filepath)
    lines = f.readlines()
    opcodes = []

    # 循環掃碼文件的每一行    for i in range(len(lines)):
        line = lines
        if line.find('case') != -1:
            line = line.replace('case', '')
            line = line.replace(' ', '')
            line = line.replace('\n','')
            line = line.replace('u:', '')

            # 如果上一行也是case,那么這2個case對應同一個opcode            if lines[i-1].find('case') != -1:
                opcode = opcodes[-1]
                opcode.append(line)
            else:
                opcode = []
                opcode.append(line)
                opcodes.append(opcode)
    f.close()
    return opcodes

o1 = get_opcode(u'基礎版opcode.txt')
o2 = get_opcode(u'進階版opcode.txt')# 還原for i in range(len(o1)):
    print '基礎版:',o1,'\t進階版:',o2# 映射opcode獲取修復表op_tbl = [-1 for i in range(64)]for i in range(len(o1)):
    o1opcode = o1[0]
    o1opcode = o1opcode.replace('0x','')

    for o2opcode in o2:
        o2opcode = o2opcode.replace('0x','')
        op_tbl[int(o2opcode,16)] = int(o1opcode,16)print '修復表:',op_tbl
運行結果:

基礎版: ['0']        進階版: ['6', '7', '0x16', '0x1B']
基礎版: ['1']        進階版: ['0x22', '0x28', '0x29', '0x3C']
基礎版: ['2']        進階版: ['0x3E']
基礎版: ['3']        進階版: ['0x3B']
基礎版: ['4']        進階版: ['0x12']
基礎版: ['5']        進階版: ['8', '0x11', '0x17', '0x36']
基礎版: ['6']        進階版: ['2']
基礎版: ['7']        進階版: ['0xD']
基礎版: ['8']        進階版: ['0x1A']
基礎版: ['9']        進階版: ['1']
基礎版: ['0xA']      進階版: ['0x1D']
基礎版: ['0xB']      進階版: ['0x1F']
基礎版: ['0xC']      進階版: ['0xE']
基礎版: ['0xD']      進階版: ['0x31']
基礎版: ['0xE']      進階版: ['0x2F']
基礎版: ['0xF']      進階版: ['0x1E']
基礎版: ['0x12']     進階版: ['0x13']
基礎版: ['0x14']     進階版: ['0x2B']
基礎版: ['0x15']     進階版: ['0x1C']
基礎版: ['0x16']     進階版: ['0x2D']
基礎版: ['0x17']     進階版: ['0x19']
基礎版: ['0x18']     進階版: ['0x3F']
基礎版: ['0x10']     進階版: ['0x15']
基礎版: ['0x13']     進階版: ['0x24']
基礎版: ['0x11']     進階版: ['0x3A']
基礎版: ['0x19']     進階版: ['0x18']
基礎版: ['0x1A']     進階版: ['0x33']
基礎版: ['0x1B']     進階版: ['0xF']
基礎版: ['0x1C']     進階版: ['0x34']
基礎版: ['0x1D']     進階版: ['0x20']
基礎版: ['0x1E']     進階版: ['5', '9', '0xA', '0x25']
基礎版: ['0x1F']     進階版: ['0x30']
基礎版: ['0x20']     進階版: ['0x26']
基礎版: ['0x21']     進階版: ['0x35']
基礎版: ['0x22']     進階版: ['0x38']
基礎版: ['0x23']     進階版: ['0x2A']
基礎版: ['0x24']     進階版: ['0x23', '0x37', '0x39', '0x3D']
基礎版: ['0x25']     進階版: ['0x27']
基礎版: ['0x27']     進階版: ['0x2C']
基礎版: ['0x28']     進階版: ['0x32']
基礎版: ['0x29']     進階版: ['0x21']
基礎版: ['0x2A']     進階版: ['3']
基礎版: ['0x2B']     進階版: ['0xC']
基礎版: ['0x2C']     進階版: ['0x2E']
基礎版: ['0x2D']     進階版: ['0x14']
基礎版: ['0x26']     進階版: ['4']
修復表: [-1, 9, 6, 42, 38, 30, 0, 0, 5, 30, 30, -1, 43, 7, 12, 27, -1, 5, 4, 18, 45, 16, 0, 5, 25, 23, 8, 0, 21, 10, 15, 11, 29, 41, 1, 36, 19, 30, 32, 37, 1, 1, 35, 20, 39, 22, 44, 14, 31, 13, 40, 26, 28, 33, 5, 36, 34, 36, 17, 3, 1, 36, 2, 24]
注意了,這里有幾個opcode是沒有對應關系的(默認是-1),跟蹤代碼發現,其實這些opcode的功能相當于nop操作,而原本lua是不存在nop的,我們只需在修復的過程中跳過這個字節碼即可。

最后將獲取的修復表替換到工具中,Chunspy修復點在DecodeInst函數中,修改結果如下:

function DecodeInst(code, iValues)  local iSeq, iMask = config.iABC, config.mABC
  local cValue, cBits, cPos = 0, 0, 1  -- decode an instruction  for i = 1, #iSeq do    -- if need more bits, suck in a byte at a time    while cBits < iSeq do      cValue = string.byte(code, cPos) * (1 << cBits) + cValue
      cPos = cPos + 1; cBits = cBits + 8    end    -- extract and set an instruction field    iValues[config.nABC[ i ]] = cValue % iMask
    cValue = cValue // iMask
    cBits = cBits - iSeq
  end  -- add by littleNA  local optbl = { -1, 9, 6, 42, 38, 30, 0, 0, 5, 30, 30, -1, 43, 7, 12, 27, -1, 5, 4, 18, 45, 16, 0, 5, 25, 23, 8, 0, 21, 10, 15, 11, 29, 41, 1, 36, 19, 30, 32, 37, 1, 1, 35, 20, 39, 22, 44, 14, 31, 13, 40, 26, 28, 33, 5, 36, 34, 36, 17, 3, 1, 36, 2, 24 }   
  iValues.OP = optbl[iValues.OP+1]  -- 注意,lua的下標是從1開始的數起的  -- add by littleNA end  iValues.opname = config.opnames[iValues.OP]   -- get mnemonic  iValues.opmode = config.opmode[iValues.OP]-- add by littleNA  if iValues.OP == -1 then    iValues.opname = "Nop"    iValues.opmode = iABx
  end  -- add by littleNA endif iValues.opmode == iABx then                 -- set Bx or sBx    iValues.Bx = iValues.B * iMask[3] + iValues.C
  elseif iValues.opmode == iAsBx then    iValues.sBx = iValues.B * iMask[3] + iValues.C - config.MAXARG_sBx
  elseif iValues.opmode == iAx then    iValues.Ax = iValues.B * iMask[3] * iMask[2] + iValues.C * iMask[2] + iValues.A
  end  return iValuesend
測試發現出錯了,出錯結果:



從出錯的結果可以看出是luac文件的版本號有錯誤,這里無法識別lua 11的版本其實是題目故意設計讓工具識別錯誤,我們將文件的第4個字節(lua版本號)11修改成53就可以了。正確結果:



luadec修復點在ldo.c文件的f_parser函數,并且增加一個RepairOpcode函數,修復如下:

// add by littleNAvoid RepairOpcode(Proto* f){
    // opcode 替換表    char optbl[] = { -1, 9, 6, 42, 38, 30, 0, 0, 5, 30, 30, -1, 43, 7, 12, 27, -1, 5, 4, 18, 45, 16, 0, 5, 25, 23, 8, 0, 21, 10, 15, 11, 29, 41, 1, 36, 19, 30, 32, 37, 1, 1, 35, 20, 39, 22, 44, 14, 31, 13, 40, 26, 28, 33, 5, 36, 34, 36, 17, 3, 1, 36, 2, 24 };
    for (int i = 0; i < f->sizecode; i++)
    {
        Instruction code = f->code;
        OpCode o = GET_OPCODE(code);
        SET_OPCODE(code, optbl[o]);

        f->code = code;
    }

    for (int i = 0; i < f->sizep; i++)
    {// 處理子函數        RepairOpcode(f->p);
    }
}// add by littleNA endstatic void f_parser (lua_State *L, void *ud) {
  LClosure *cl;
  struct SParser *p = cast(struct SParser *, ud);
  int c = zgetc(p->z);  /* read first character */  if (c == LUA_SIGNATURE[0]) {
    checkmode(L, p->mode, "binary");
    cl = luaU_undump(L, p->z, p->name);

    // add by littleNA    Proto *f = cl->p;
    RepairOpcode(f);
    // add by littleNA end  }
  else {
    checkmode(L, p->mode, "text");
    cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
  }
  lua_assert(cl->nupvalues == cl->p->sizeupvalues);
  luaF_initupvals(L, cl);
}
運行一下,發現出錯了,并且停留在StringBuffer_add函數中,其中str指向錯誤的地方,導致字符串讀取出錯:



到這里我們修復了opcode,并且Chunkspy順利反匯編,但是luadec的反編譯還是有問題,我們在下一節分析。

反編譯問題及其修復
看了幾個大佬的writeup,發現他們都沒有修復這個問題,解題過程中都是直接分析的是lua匯編代碼。我們看看出錯的原因,查看vs的調用堆棧:



發現上一層函數是listUpvalues函數,也就是說luadec在解析upvalues時出錯了,深入分析發現其實是由于文件中的upvalue變量名被抹掉了,導致解析出錯,我們只需要在ProcessCode函數(decompile.c文件)調用listUpvalues函數前,增加臨時的upvalue命名就可以了,修改代碼如下:

char* ProcessCode(Proto* f, int indent, int func_checking, char* funcnumstr)
{
...
       // make function comment       StringBuffer_printf(str, "-- function num : %s", funcnumstr);
       if (NUPS(f) > 0) {
              // add by littleNA              for (i = 0; i<f->sizeupvalues; i++) {
                     char tmp[10];
                     sprintf(tmp, "up_%d", i);
                     f->upvalues.name = luaS_new(f->L, tmp);
              }
              // add by littleNA end              StringBuffer_add(str, " , upvalues : ");
              listUpvalues(f, str);
       }
...
}
最后完美運行luadec,反編譯成功。

例子3:夢幻西游手游
這一節是去年學習破解夢幻西游手游lua代碼時記錄的一些問題,今天將其整理并共享出來,所以不一定適合現在版本的夢幻手游,大家還是以參考為目的唄。

當時反編譯夢幻西游手游時遇到的問題大約有12個,修改完基本上可以完美復現lua源碼,這里用的luadec5.1版本。

修復一
問題1: 由于夢幻手游lua的opcode是被修改過的,之前的解決方案是找到夢幻的opcode,替換掉反編譯工具的原opcode,并且修改opmode,再進行反編譯。問題是部分測試的結果是可以的,但是當對整個手游的luac字節碼反編譯時,會出現各種錯誤,原因是luadec5.1 在很多地方都默認了opcode的順序,并進行了特殊處理,所以需要找到這些特殊處理的地方一一修改。不過這樣很麻煩,從而想到另外一種方式,不修改原來的opcode和opmode,而是在luadec解析到字節碼的時候,將opcode還原成原來的opcode。

解決1: 定位到解析code的位置在  lundump.c --> LoadFunction --> LoadCode (位置不唯一,可以看上一節騰訊比賽的修復),當執行完LoadCode函數的時候,f變量則指向了code的結構,在這之后執行自己寫的函數ConvertCode函數,如下:

// add by littleNAvoid ConvertCode(Proto *f){
        int pnOpTbl[] = { 3,13,18,36,27,10,20,25,34,2,32,15,30,16,31,9,26,24,29,1,6,28,4,17,33,0,7,11,5,14,8,19,35,12,21,22,23,37 };
        for (int pc = 0; pc < f->sizecode; pc++)
        {
               Instruction i = f->code[pc];
               OpCode o = GET_OPCODE(i);
               SET_OPCODE(i, pnOpTbl[o]);
               f->code[pc] = i;
        }
}
修復二
問題2: 在文件頭部 反編譯出現錯誤  -- DECOMPILER ERROR: Overwrote pending register.

解決2: 分析發現,原來是解析OP_VARARG錯誤導致的。OP_VARARG主要的作用是復制B-1個參數到A寄存器中,而反編譯工具復制了B個參數,多了一個。修改后的代碼如下:

...
         case OP_VARARG: // Lua5.1 specific.         {
            int i;
            /*
             * Read ... into register.
             */            if (b==0) {
                TRY(Assign(F, REGISTER(a), "...", a, 0, 1));
            } else {
                 // add by littleNA                 // for(i = 0;i<b;i++) {                 for(i = 0; i < b-1; i++) {
                      TRY(Assign(F, REGISTER(a+i), "...", a+i, 0, 1));
                 }
           }
           break;
         }
...
修復三
問題3: 在解析table出現反編譯錯誤  -- DECOMPILER ERROR: Confused about usage of 。registers!

解決3: 分析發現,這里的OP_NEWTABLE 的c參數表示hash table中key的大小,而反編譯代碼中將c參數進行了錯誤轉換,導致解析錯誤,修改代碼如下:

// add by littleNA//#define fb2int(x)    (((x) & 7) << ((x) >> 3))#define fb2int(x)      ((((x) & 7)^8) >> (((x) >> 3)-1))
修復四
問題4: 反編譯工具出錯并且退出。

解決4: 跟蹤發現是在AddToTable函數中,當keyed為0時會調用PrintTable,而PrintTable釋放了table,下次再調用table時內存訪問失敗,修改代碼如下:

void AddToTable(Function* F, DecTable * tbl, char *value, char *key){
   DecTableItem *item;
   List *type;
   int index;
   if (key == NULL) {
      type = &(tbl->numeric);
      index = tbl->topNumeric;
      tbl->topNumeric++;
   } else {
      type = &(tbl->keyed);
      tbl->used++;
      index = 0;
   }
   item = NewTableItem(value, index, key);
   AddToList(type, (ListItem *) item);
   // FIXME: should work with arrays, too   // add by littleNA   // if(tbl->keyedSize == tbl->used && tbl->arraySize == 0){   if (tbl->keyedSize != 0 && tbl->keyedSize == tbl->used && tbl->arraySize == 0) {
      PrintTable(F, tbl->reg, 0);
      if (error)
         return;
   }
}
修復五
問題5: 當函數是多值返回結果并且賦值于多個變量時反編譯錯誤,情況如下(lua反匯編):

21 [-]: GETGLOBAL R0 K9        ; R0 := memoryStatMap 22 [-]: GETGLOBAL R1 K9        ; R1 := memoryStatMap 23 [-]: GETGLOBAL R2 K2        ; R2 := preload 24 [-]: GETTABLE  R2 R2 K3     ; R2 := R2["utils"] 25 [-]: GETTABLE  R2 R2 K16    ; R2 := R2["getCocosStat"] 26 [-]: CALL      R2 1 3       ; R2,R3 := R2() 27 [-]: SETTABLE  R1 K15 R3    ; R1["cocosTextureBytes"] := R3 28 [-]: SETTABLE  R0 K14 R2    ; R0["cocosTextureCnt"] := R2
當上面的代碼解析到27行時,從寄存器去取R3時報錯,原因是前面的call返回多值時,只是在F->Rcall中進行了標記,沒有在寄存器中標記,編譯的結果應該為:

memoryStatMap.cocosTextureCnt, memoryStatMap.cocosTextureBytes = preload.utils.getCocosStat()
解決5: 當reg為空時并且Rcall不為空,增加一個return more的標記,修改2個函數:

char *RegisterOrConstant(Function * F, int r)
{
   if (IS_CONSTANT(r)) {
      return DecompileConstant(F->f, r - 256); // TODO: Lua5.1 specific. Should change to MSR!!!   } else {
      char *copy;
      char *reg = GetR(F, r);
      if (error)
         return NULL;

        // add by littleNA        // if(){}        if (reg == NULL && F->Rcall[r] != 0)
        {
            reg = "return more";
        }

      copy = malloc(strlen(reg) + 1);
      strcpy(copy, reg);
      return copy;
   }
}
void OutputAssignments(Function * F){
   int i, srcs, size;
   StringBuffer *vars;
   StringBuffer *exps;
   if (!SET_IS_EMPTY(F->tpend))
      return;
   vars = StringBuffer_new(NULL);
   exps = StringBuffer_new(NULL);
   size = SET_CTR(F->vpend);
   srcs = 0;
   for (i = 0; i < size; i++) {
      int r = F->vpend->regs;
      if (!(r == -1 || PENDING(r))) {
         SET_ERROR(F,"Attempted to generate an assignment, but got confused about usage of registers");
         return;
      }

      if (i > 0)
         StringBuffer_prepend(vars, ", ");
      StringBuffer_prepend(vars, F->vpend->dests);

      if (F->vpend->srcs && (srcs > 0 || (srcs == 0 && strcmp(F->vpend->srcs, "nil") != 0) || i == size-1)) {
                 // add by littleNA                 // if()                 if (strcmp(F->vpend->srcs, "return more") != 0)
                 {
                         if (srcs > 0)
                                StringBuffer_prepend(exps, ", ");
                         StringBuffer_prepend(exps, F->vpend->srcs);
                         srcs++;
                }
      }

   }
...
}
修復六
問題6: 當函數只有一個renturn的時候會反編譯錯誤。

解決6:

case OP_RETURN:
{
    ...
    // add by littleNA    // 新增的if    if (pc != 0)
    {
        for (i = a; i < limit; i++) {
                char* istr;
                if (i > a)
                        StringBuffer_add(str, ", ");
                istr = GetR(F, i);
                TRY(StringBuffer_add(str, istr));
        }
        TRY(AddStatement(F, str));
    }
    break;      
}
修復七
問題7: 部分table初始化會出錯。

解決7:

char *GetR(Function * F, int r)
{
   if (IS_TABLE(r)) {
     // add by littleNA        return "{ }";
    //  PrintTable(F, r, 0);    //  if (error) return NULL;   }
...
}
修復八
問題8: 可變參數部分解析出錯,但是工具反編譯時是不報錯誤的。

解決8: is_vararg為7時,F->freeLocal多加了一次:

   if (f->is_vararg==7) {
      TRY(DeclareVariable(F, "arg", F->freeLocal));
      F->freeLocal++;
   }
   // add by littleNA   // 修改if為else if   else if ((f->is_vararg&2) && (functionnum!=0)) {
      F->freeLocal++;
   }
修復九
問題9: 反編譯工具輸出的中文為url類型的字符(類似 “\230\176\148\231\150\151\230\156\175”),不是中文。

解決9: 在proto.c文件中的DecompileString函數中,注釋掉default 轉換字符串的函數:

char *DecompileString(const Proto * f, int n)
{
...
        default:
              //add by littleNA//            if (*s < 32 || *s > 127) {//               char* pos = &(ret[p]);//               sprintf(pos, "\\%d", *s);//               p += strlen(pos);//            } else {               ret[p++] = *s;//            }            break;
...
}
然后再下面3處增加判斷的約束條件,因為中文字符的話,char字節是負數,這樣isalpha和isalnum函數就會出錯,所以增加約束條件,小于等于127:

void MakeIndex(Function * F, StringBuffer * str, char* rstr, int self){
...
   int dot = 0;
   /*
    * see if index can be expressed without quotes
    */   if (rstr[0] == '\"') {

      // add by littleNA      // (unsigned char)(rstr[1]) <= 127 &&      if ((unsigned char)(rstr[1]) <= 127 && isalpha(rstr[1]) || rstr[1] == '_') {
         char *at = rstr + 1;
         dot = 1;
         while (*at != '"') {

            // add by littleNA            // *(unsigned char*)at <= 127 &&            if (*(unsigned char*)at <= 127 && !isalnum(*at) && *at != '_') {
               dot = 0;
               break;
            }
            at++;
         }
      }
   }
....
}

...
    case OP_TAILCALL:
    {
               // add by littleNA               // (unsigned char)(*at) <= 127 &&               while (at > astr && ((unsigned char)(*at) <= 127 && isalpha(*at) || *at == '_')) {
                  at--;
               }
    }
...
修復十
問題10: 反匯編失敗。因為一些文件中含有很長的字符串,導致sprintf函數調用失敗。

解決10: 增加緩存的大小:

void luaU_disassemble(const Proto* fwork, int dflag, int functions, char* name) {
...
                       // add by littleNA                       // char lend[MAXCONSTSIZE+128];                       char lend[MAXCONSTSIZE+2048];
...
}
修復十一
問題11: op_setlist操作碼當b==0時,反編譯失敗。

解決11: 當遇到類似下面的lua語句時,反編譯工具會失敗,出現的情況在@lib_ui.lua文件中:

local a={func()}
匯編后的代碼:

               a   b   c
[1] newtable   0   0   0    ; array=0, hash=0[2] getglobal  1   0        ; func
[3] call       1   1   0[4] setlist    0   0   1    ; index 1 to top
[5] return     0   1
出現的問題有2處,第一個是newtable,當b == 0 && c == 0時,反編譯工具認為table是空的table,直接輸出了table并且釋放了table的內存,導致后面setlist初始化table時找不到內存而報錯。

第二個是setlist有問題,當b==0時,其實是指寄存器a+1到棧頂(top)的值全部賦值于table,而反編譯器沒有對b==0的判斷,加上就可以了。所以修改如下:

void StartTable(Function * F, int r, int b, int c){
   DecTable *tbl = NewTable(r, F, b, c);
   AddToList(&(F->tables), (ListItem *) tbl);
   F->Rtabl[r] = 1;
   F->Rtabl[r] = 1;
   if (b == 0 && c == 0) {

           // add by littleNA           // for(){}           for (int npc = F->pc + 1; npc < F->f->sizecode; npc++)
           {
                  Instruction i = F->f->code[npc];
                  OpCode o = GET_OPCODE(i);
                  if ((o != OP_SETLIST && o != OP_SETTABLE) && r == GETARG_A(i))
                  {
                          PrintTable(F, r, 1);
                          return;
                  }
                  else if ((o == OP_SETLIST || o == OP_SETTABLE) && r == GETARG_A(i))
                  {
                          return;
                  }
           }
      PrintTable(F, r, 1);
      if (error)
         return;
   }
}void SetList(Function * F, int a, int b, int c){
...
   // add by littleNA   // if(){}   if (b == 0)
   {
           Instruction i = F->f->code[F->pc-1];
           OpCode o = GET_OPCODE(i);
           if (o == OP_CALL)
           {
                  int aa = GETARG_A(i);
                  for (i = a + 1; i < aa + 1; i++)
                  {
                          char* rstr = GetR(F, i);
                          if (error)
                                 return;
                          AddToTable(F, tbl, rstr, NULL);
                          if (error)
                                 return;
                  }
           }
           else           {
                  for (i = 1;;i++) {
                          char* rstr = GetR(F, a + i);
                          if (rstr == NULL)
                                 return;
                          AddToTable(F, tbl, rstr, NULL);
                          if (error)
                                 return;
                  }
           }
   }
...
}
StartTable 增加的for循環表示,如果執行了newtable(r 0 0),后面非初始化table的操作覆蓋了r寄存器(把table覆蓋了),那就表明new出來的table是空的,后面沒有對table的賦值;如果后面有對r寄存器初始化,證明此時new出了的table不是空的,是可變參數的table。

SetList 增加的if表示,如果指令是call指令,那么將a+1到call指令寄存器aa的棧元素加入到table中(這里為何不是到棧頂的元素而是到aa的元素呢?因為call指令對應的是函數調用,反編譯工具已經把函數調用的字符串解析到aa中了,這里跟實際運行可能有點不一樣;else后面就是將a+1到棧頂的元素初始化到table中,直到GetR函數為空表示到棧頂了。

修復十二
問題12: 當一個函數開頭只是局部變量聲明,如:

function func()     local a,b,c
     c = f(a,b)
     return cend
第一行 local a,b,c 會反編譯失敗,導致后面的代碼出現各種錯誤。

解決12:

void DeclareLocals(Function * F){
...
   for (i = startparams; i < F->f->sizelocvars; i++) {
      if (F->f->locvars.startpc == F->pc) {
             ...
             if (PENDING(r)) {...}
             // add by littleNA             // else if(){}             else if (locals == 0 && F->pc == 0)
             {
                     StringBuffer_add(str, LOCAL(i));
                     char *szR = GetR(F, r);
                     StringBuffer_add(rhs, szR==NULL?"nil":szR);
             }
             ...   
       }
   }
...
}
當變量的startpc 等于 當前pc,變量的個數為0并且當前pc為0,表示第一行聲明了變量,添加的else if就是解析這種情況的(原來是直接報錯不解析)。

總結
上文首先總結了近年來公開的lua逆向技術相關文章和相關工具,接著講解了lua反匯編和反編譯的對抗,并以3個實例作為說明。第1個例子舉例了征途手游的修復,第2個例子修復了lua虛擬機的opcode并成功反編譯lua腳本,第3個例子完美修復了夢幻手游的lua腳本反編譯出現的大量錯誤。

lua加解密的技術還是會一直發展下去,但是這篇文章到此就結束了。接下來可能會寫一篇2018騰訊游戲安全競賽的詳細分析報告(詳細到每一個字節喔),內容包括但不限于STL逆向、AES算法分析、Blueprint腳本分析等等,敬請期待。

點評

轉貼都能轉歪來 還是轉的本站帖子  發表于 2019-10-25 14:41

免費評分

參與人數 8吾愛幣 +6 熱心值 +6 收起 理由
onihot + 1 + 1 [email protected]
0xxx + 1 我很贊同!
RZUI + 1 + 1 用心討論,共獲提升!
xmhwws + 1 + 1 原貼地址, 請補充一下
二娃 -1 http://www.kvamco.live/thread-763394-1-1.html
tanqiquan360 + 1 + 1 用心討論,共獲提升!
椎名牧 + 1 + 1 [email protected]
鶴舞九月天 + 1 + 1 [email protected]

查看全部評分

本帖被以下淘專輯推薦:

發帖前要善用論壇搜索功能,那里可能會有你要找的答案或者已經有人發布過相同內容了,請勿重復發帖。

推薦
code871 發表于 2019-10-26 17:23
本帖最后由 code871 于 2019-10-26 17:29 編輯

某航線的一個檢測lua在反編譯后不修改內容重新編譯不會有任何改動,但是如果修改了這個lua的內容,重新編譯后內容會發生改變,游戲結算也會失敗
這是原來的內容
function AddCfgBaseInfo(slot0)
        for slot4, slot5 in ipairs(slot0.all) do
                if rawget(slot0[slot5], "base") ~= nil then
                        rawset(slot6, "base", nil)
                        setmetatable(slot6, {
                                __index = slot0[slot7]
                        })
                end
        end
end

function GetSpeNum(slot0, slot1)
        for slot5, slot6 in pairs(slot0) do
                if type(slot6) == "number" then
                        slot1 = slot1 + slot6
                elseif slot7 == "table" then
                        slot1 = GetSpeNum(slot6, slot1)
                elseif slot7 == "string" then
                        slot1 = slot1 + (tonumber(slot6) or 0)
                end
        end

        return slot1
end

function GetDataValue(slot0)
        for slot5, slot6 in ipairs(slot0.all) do
                slot1 = GetSpeNum(slot0[slot6], 0)
        end

        return slot1
end

function CheckTables(slot0, slot1)
        for slot5, slot6 in pairs(slot0) do
                if type(slot6) == "number" or slot7 == "string" then
                        if slot6 ~= slot1[slot5] then
                                return false
                        end
                elseif slot7 == "table" and not CheckTables(slot6, slot1[slot5]) then
                        return false
                end
        end

        return true
end

AddCfgBaseInfo(pg.equip_data_statistics)
AddCfgBaseInfo(pg.weapon_property)
AddCfgBaseInfo(pg.equip_data_template)
AddCfgBaseInfo(pg.aircraft_template)
AddCfgBaseInfo(pg.enemy_data_statistics)

ys.EquipDataStatisticVertify = GetDataValue(pg.equip_data_statistics)
ys.WeaponPropertyVertify = GetDataValue(pg.weapon_property)
ys.ShipStatisticsVertify = GetDataValue(pg.ship_data_statistics)
ys.EnemyStatisticsVertify = GetDataValue(pg.enemy_data_statistics)
ys.ExpeditionDataVertify = GetDataValue(pg.expedition_data_template)
ys.BattleVertifyTable = {
        {
                hash = ys.EquipDataStatisticVertify,
                hashCheck = ys.EquipDataStatisticVertify,
                data = pg.equip_data_statistics
        },
        {
                hash = ys.WeaponPropertyVertify,
                hashCheck = ys.WeaponPropertyVertify,
                data = pg.weapon_property
        },
        {
                hash = ys.ShipStatisticsVertify,
                hashCheck = ys.ShipStatisticsVertify,
                data = pg.ship_data_statistics
        },
        {
                hash = ys.EnemyStatisticsVertify,
                hashCheck = ys.EnemyStatisticsVertify,
                data = pg.enemy_data_statistics
        },
        {
                hash = ys.ExpeditionDataVertify,
                hashCheck = ys.ExpeditionDataVertify,
                data = pg.expedition_data_template
        }
}

function GetBattleCheck()
        return math.floor(ys.EquipDataStatisticVertify + ys.WeaponPropertyVertify + ys.ShipStatisticsVertify + ys.EnemyStatisticsVertify + ys.ExpeditionDataVertify + GetSpeNum(pg.skillCfg, 0) + GetSpeNum(pg.buffCfg, 0))
end

ys.BattleConfigVertify = GetSpeNum(ys.Battle.BattleConfig, 0)
ys.BattleConstVertify = GetSpeNum(pg.bfConsts, 0)
ys.BattleShipLevelVertify = {}

如果修改內容之后,重新編譯,就會發生改變,再反編譯出來看看就會發現
function GetDataValue下的slot值都會改變
但是如果什么都不做,重新編譯,什么也不會變
想問下這是什么原理呢
如果需要文件本體的話在這pan.baidu.com/s/158u05JsYFIzh_8NhFshzTA 提取edwv
推薦
boyfree 發表于 2019-11-26 23:57
樓主,怎么破解有卡密或者激活碼的驗證,求個思路
沙發
tmsq 發表于 2019-10-25 02:07
3#
hgfty1 發表于 2019-10-25 06:37
看的頭很暈~~~
4#
夜步城 發表于 2019-10-25 08:27
好長好復雜,不過學習一下,謝謝樓主
5#
MaKa_Maka 發表于 2019-10-25 09:24
表示沒怎么解除過!
6#
Windows10 發表于 2019-10-25 09:33
很實用雖然看不懂還是感謝下
7#
煙塵沐雨丶 發表于 2019-10-25 10:31
斜體看著頭大。
8#
wuyaqing981206 發表于 2019-10-25 10:47
懵逼的進來,懵逼的出去
9#
一片楓葉落地無 發表于 2019-10-25 11:16
一臉懵逼的進來 又一臉懵逼的初期- -
10#
Light紫星 發表于 2019-10-25 11:29
好熟悉的文章
您需要登錄后才可以回帖 登錄 | 注冊[Register]

本版積分規則 警告:禁止回復與主題無關內容,違者重罰!

快速回復 收藏帖子 返回列表 搜索

RSS訂閱|小黑屋|聯系我們|吾愛破解 - LCG - LSG ( 京ICP備16042023號 | 京公網安備 11010502030087號 )

GMT+8, 2019-12-12 01:14

Powered by Discuz!

© 2001-2017 Comsenz Inc.

快速回復 返回頂部 返回列表
3d开机号今天