在IOT漏洞挖掘的过程中常常会遇到luci模式的cgi,其中多数都会对其进行编译为字节码,网络上已经有不少关于unluac的项目,例如unluac - Browse Files at SourceForge.net,但是还有一些厂商会对luac进行魔改,这就使得一般的unluac失效

之前就曾尝试过学习恢复魔改luac,可惜一直没啥头绪,前段时间和学长交流了下,学长给了篇自己写的文章奇安信攻防社区-还原iot设备中魔改的luac (butian.net),学习一下

关于lua虚拟机可以参考深入理解 Lua 虚拟机-腾讯云开发者社区-腾讯云 (tencent.com)

源码

学长的文章中只展示了部分关键代码,所以还是自己需要自己阅读源码,以下代码无特殊标注皆选自lua5.3.6

lua

main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int main (int argc, char **argv) {
int status, result;
lua_State *L = luaL_newstate(); /* create state */
if (L == NULL) {
l_message(argv[0], "cannot create state: not enough memory");
return EXIT_FAILURE;
}
lua_pushcfunction(L, &pmain); /* to call 'pmain' in protected mode */
lua_pushinteger(L, argc); /* 1st argument */
lua_pushlightuserdata(L, argv); /* 2nd argument */
status = lua_pcall(L, 2, 1, 0); /* do the call */
result = lua_toboolean(L, -1); /* get result */
report(L, status);
lua_close(L);
return (result && status == LUA_OK) ? EXIT_SUCCESS : EXIT_FAILURE;
}

可以看到只是一些初始化工作,创建lua状态机等

pmain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
static int pmain (lua_State *L) {
int argc = (int)lua_tointeger(L, 1);
char **argv = (char **)lua_touserdata(L, 2);
int script;
int args = collectargs(argv, &script);
luaL_checkversion(L); /* check that interpreter has correct version */
if (argv[0] && argv[0][0]) progname = argv[0];
if (args == has_error) { /* bad arg? */
print_usage(argv[script]); /* 'script' has index of bad arg. */
return 0;
}
if (args & has_v) /* option '-v'? */
print_version();
if (args & has_E) { /* option '-E'? */
lua_pushboolean(L, 1); /* signal for libraries to ignore env. vars. */
lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");
}
luaL_openlibs(L); /* open standard libraries */
createargtable(L, argv, argc, script); /* create table 'arg' */
if (!(args & has_E)) { /* no option '-E'? */
if (handle_luainit(L) != LUA_OK) /* run LUA_INIT */
return 0; /* error running LUA_INIT */
}
if (!runargs(L, argv, script)) /* execute arguments -e and -l */
return 0; /* something failed */
if (script < argc && /* execute main script (if there is one) */
handle_script(L, argv + script) != LUA_OK)
return 0;
if (args & has_i) /* -i option? */
doREPL(L); /* do read-eval-print loop */
else if (script == argc && !(args & (has_e | has_v))) { /* no arguments? */
if (lua_stdin_is_tty()) { /* running in interactive mode? */
print_version();
doREPL(L); /* do read-eval-print loop */
}
else dofile(L, NULL); /* executes stdin as a file */
}
lua_pushboolean(L, 1); /* signal no errors */
return 1;
}

官方的注释已经非常详细了,可以知道当给出了目标文件会进入分支

1
2
if (script < argc &&  /* execute main script (if there is one) */
handle_script(L, argv + script) != LUA_OK)

handle_script

1
2
3
4
5
6
7
8
9
10
11
12
static int handle_script (lua_State *L, char **argv) {
int status;
const char *fname = argv[0];
if (strcmp(fname, "-") == 0 && strcmp(argv[-1], "--") != 0)
fname = NULL; /* stdin */
status = luaL_loadfile(L, fname);
if (status == LUA_OK) {
int n = pushargs(L); /* push arguments to script */
status = docall(L, n, LUA_MULTRET);
}
return report(L, status);
}

真正的核心在于luaL_loadfile(L, fname);

#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL)

luaL_loadfile实际上就是luaL_loadfilex

luaL_loadfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
const char *mode) {
LoadF lf;
int status, readstatus;
int c;
int fnameindex = lua_gettop(L) + 1; /* index of filename on the stack */
if (filename == NULL) {
lua_pushliteral(L, "=stdin");
lf.f = stdin;
}
else {
lua_pushfstring(L, "@%s", filename);
lf.f = fopen(filename, "r");
if (lf.f == NULL) return errfile(L, "open", fnameindex);
}
if (skipcomment(&lf, &c)) /* read initial portion */
lf.buff[lf.n++] = '\n'; /* add line to correct line numbers */
if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */
lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */
if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
skipcomment(&lf, &c); /* re-read initial portion */
}
if (c != EOF)
lf.buff[lf.n++] = c; /* 'c' is the first character of the stream */
status = lua_load(L, getF, &lf, lua_tostring(L, -1), mode);
readstatus = ferror(lf.f);
if (filename) fclose(lf.f); /* close file (even in case of errors) */
if (readstatus) {
lua_settop(L, fnameindex); /* ignore results from 'lua_load' */
return errfile(L, "read", fnameindex);
}
lua_remove(L, fnameindex);
return status;
}

从这个函数开始就到了真正解析luac文件的部分

lf 是一个 LoadF 结构,包含了文件相关的部分信息

1
2
3
4
5
typedef struct LoadF {
int n; /* number of pre-read characters */
FILE *f; /* file being read */
char buff[BUFSIZ]; /* area for reading file */
} LoadF;

关注luac相关处理部分

1
2
3
4
5
if (c == LUA_SIGNATURE[0] && filename) {  /* binary file? */
lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */
if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
skipcomment(&lf, &c); /* re-read initial portion */
}

#define LUA_SIGNATURE "\033Lua"

所以当文件的第一个字节是1b的时候会被认为是luac编译后的字节码文件,并重新以二进制模式("rb")打开文件,逐个读取字节直到EOF或者1B,

跳过可能存在的 Unix 执行标识行

lua_load

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LUA_API int lua_load (lua_State *L, lua_Reader reader, void *data,
const char *chunkname, const char *mode) {
ZIO z;
int status;
lua_lock(L);
if (!chunkname) chunkname = "?";
luaZ_init(L, &z, reader, data);
status = luaD_protectedparser(L, &z, chunkname, mode);
if (status == LUA_OK) { /* no errors? */
LClosure *f = clLvalue(L->top - 1); /* get newly created function */
if (f->nupvalues >= 1) { /* does it have an upvalue? */
/* get global table from registry */
Table *reg = hvalue(&G(L)->l_registry);
const TValue *gt = luaH_getint(reg, LUA_RIDX_GLOBALS);
/* set global table as 1st upvalue of 'f' (may be LUA_ENV) */
setobj(L, f->upvals[0]->v, gt);
luaC_upvalbarrier(L, f->upvals[0]);
}
}
lua_unlock(L);
return status;
}

直接进入我们关注的部分luaD_protectedparser

luaD_protectedparser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int luaD_protectedparser (lua_State *L, ZIO *z, const char *name,
const char *mode) {
struct SParser p;
int status;
L->nny++; /* cannot yield during parsing */
p.z = z; p.name = name; p.mode = mode;
p.dyd.actvar.arr = NULL; p.dyd.actvar.size = 0;
p.dyd.gt.arr = NULL; p.dyd.gt.size = 0;
p.dyd.label.arr = NULL; p.dyd.label.size = 0;
luaZ_initbuffer(L, &p.buff);
status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc);
luaZ_freebuffer(L, &p.buff);
luaM_freearray(L, p.dyd.actvar.arr, p.dyd.actvar.size);
luaM_freearray(L, p.dyd.gt.arr, p.dyd.gt.size);
luaM_freearray(L, p.dyd.label.arr, p.dyd.label.size);
L->nny--;
return status;
}

以上两个函数其实都没有针对字节码文件的专门处理,无需过多关注

f_parser

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static 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);
}
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);
}

再次判断第一个字节是否为1B,是则处理函数设置为luaU_undump

luaU_undump

终于正式开始加载字节码文件

在此之前5.1和5.3差异不大,不过在这个函数就开始出现较大差异了

5.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Proto* luaU_undump (lua_State* L, ZIO* Z, Mbuffer* buff, const char* name)
{
LoadState S;
if (*name=='@' || *name=='=')
S.name=name+1;
else if (*name==LUA_SIGNATURE[0])
S.name="binary string";
else
S.name=name;
S.L=L;
S.Z=Z;
S.b=buff;
LoadHeader(&S);
return LoadFunction(&S,luaS_newliteral(L,"=?"));
}
LoadHeader
1
2
3
4
5
6
7
8
static void LoadHeader(LoadState* S)
{
char h[LUAC_HEADERSIZE];
char s[LUAC_HEADERSIZE];
luaU_header(h);
LoadBlock(S,s,LUAC_HEADERSIZE);
IF (memcmp(h,s,LUAC_HEADERSIZE)!=0, "bad header");
}

luaU_header获取标准header

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void luaU_header (char* h)
{
int x=1;
memcpy(h,LUA_SIGNATURE,sizeof(LUA_SIGNATURE)-1);
h+=sizeof(LUA_SIGNATURE)-1;
*h++=(char)LUAC_VERSION;
*h++=(char)LUAC_FORMAT;
*h++=(char)*(char*)&x; /* endianness */
*h++=(char)sizeof(int);
*h++=(char)sizeof(size_t);
*h++=(char)sizeof(Instruction);
*h++=(char)sizeof(lua_Number);
*h++=(char)(((lua_Number)0.5)==0); /* is lua_Number integral? */
}

LoadBlock则获取目标文件header

1
2
3
4
5
static void LoadBlock(LoadState* S, void* b, size_t size)
{
size_t r=luaZ_read(S->Z,b,size);
IF (r!=0, "unexpected end");
}

从这里我们可以分析出字节码文件的header结构应该如下()

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
char signature[4]; // #define LUA_SIGNATURE "\033Lua"
uchar version;
uchar format;
uchar endian;
uchar size_int;
uchar size_size_t;
uchar size_Instruction;
uchar size_lua_Number;
uchar lua_num_valid;
} GlobalHeader;
LoadFunction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static Proto* LoadFunction(LoadState* S, TString* p)
{
Proto* f;
if (++S->L->nCcalls > LUAI_MAXCCALLS) error(S,"code too deep");
f=luaF_newproto(S->L);
setptvalue2s(S->L,S->L->top,f); incr_top(S->L);
f->source=LoadString(S);
if (f->source==NULL) f->source=p;
f->linedefined=LoadInt(S);
f->lastlinedefined=LoadInt(S);
f->nups=LoadByte(S);
f->numparams=LoadByte(S);
f->is_vararg=LoadByte(S);
f->maxstacksize=LoadByte(S);
LoadCode(S,f);
LoadConstants(S,f);
LoadDebug(S,f);
IF (!luaG_checkcode(f), "bad code");
S->L->top--;
S->L->nCcalls--;
return f;
}

创建一个函数原型proto之后,加载proto的一些信息

  • f->source:尝试从字节码文件中加载函数的源代码位置信息,通常是文件名或函数名。

  • f->linedefined:函数在源文件中定义的起始行号,通过 LoadInt(S) 读取。

  • f->lastlinedefined:函数在源文件中的结束行号,表示函数的定义区间。

  • f->nups:表示函数所需的 upvalues(闭包捕获的外部变量)的数量。

  • f->numparams:表示函数的固定参数数量。
  • f->is_vararg:表示函数是否是可变参数函数。该值是一个布尔标记,若为 1 则表示函数是可变参数的。
  • f->maxstacksize:函数执行时需要的最大栈大小,表示该函数最多会占用多少 Lua 虚拟机的栈空间。
LoadCode
1
2
3
4
5
6
7
static void LoadCode(LoadState* S, Proto* f)
{
int n=LoadInt(S);
f->code=luaM_newvector(S->L,n,Instruction);
f->sizecode=n;
LoadVector(S,f->code,n,sizeof(Instruction));
}

调用 LoadCode(S, f)从字节码文件中读取函数的实际指令(字节码)

使用 f->code 存储这些指令,f->sizecode存储指令数量

Lua一个指令占4个字节,下图是格式,但其实这个图理解起来可能会出现一些误解,因为通常人类认为的小端序,常常是默认左侧为低右侧为高(四个字节0123),但在字节内部确是左侧为高右侧为低(8bits—>76543210)

虽然对于计算机来说不会出任何问题,但人在理解时有时就会出现差异,例如按照小端序取最低6位,本应该是0-5,但按照之前的理解实际上会取到2-7,并且高低位还会搞反,其实还是人自己理解的角度,写这么一大串就是因为我自己开始理解错了

但如果将Lua字节码字节串按字节反转,那么就刚好4字节32位完全按照从高到低排列,也就是下图

此外mi_lua在parse指令时就是先将其按字节反转,再套入顺序颠倒的Bitstruct,这样就得到了正确的操作码和操作数

LoadConstants
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static void LoadConstants(LoadState* S, Proto* f)
{
int i,n;
n=LoadInt(S);
f->k=luaM_newvector(S->L,n,TValue);
f->sizek=n;
for (i=0; i<n; i++) setnilvalue(&f->k[i]);
for (i=0; i<n; i++)
{
TValue* o=&f->k[i];
int t=LoadChar(S);
switch (t)
{
case LUA_TNIL:
setnilvalue(o);
break;
case LUA_TBOOLEAN:
setbvalue(o,LoadChar(S)!=0);
break;
case LUA_TNUMBER:
setnvalue(o,LoadNumber(S));
break;
case LUA_TSTRING:
setsvalue2n(S->L,o,LoadString(S));
break;
default:
error(S,"bad constant");
break;
}
}
n=LoadInt(S);
f->p=luaM_newvector(S->L,n,Proto*);
f->sizep=n;
for (i=0; i<n; i++) f->p[i]=NULL;
for (i=0; i<n; i++) f->p[i]=LoadFunction(S,f->source);
}

f->k存储常量

f->sizek存储常量数量

  • LUA_TNIL: nil
  • LUA_TBOOLEAN:布尔值
  • LUA_TNUMBER:数字,使用浮点表示
  • LUA_TSTRING:字符串。

在处理完所有的常量之后,便开始了递归的处理该函数的所有子函数,并在f->p中存储子函数proto,f->sizep中存储子函数数量

到这里就可以分析知道luac文件除去header字段后,剩余部分就是一个最大的proto嵌套更多小的proto,每个小的proto又继续往下嵌套

虽然只处理了四个常量,但实际上lua共有9个常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
** basic types
*/
#define LUA_TNONE (-1)

#define LUA_TNIL 0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8
LoadDebug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void LoadDebug(LoadState* S, Proto* f)
{
int i,n;
n=LoadInt(S);
f->lineinfo=luaM_newvector(S->L,n,int);
f->sizelineinfo=n;
LoadVector(S,f->lineinfo,n,sizeof(int));
n=LoadInt(S);
f->locvars=luaM_newvector(S->L,n,LocVar);
f->sizelocvars=n;
for (i=0; i<n; i++) f->locvars[i].varname=NULL;
for (i=0; i<n; i++)
{
f->locvars[i].varname=LoadString(S);
f->locvars[i].startpc=LoadInt(S);
f->locvars[i].endpc=LoadInt(S);
}
n=LoadInt(S);
f->upvalues=luaM_newvector(S->L,n,TString*);
f->sizeupvalues=n;
for (i=0; i<n; i++) f->upvalues[i]=NULL;
for (i=0; i<n; i++) f->upvalues[i]=LoadString(S);
}

获取行号信息,局部变量,和upvalue。

当函数A中包含子函数B,并且函数B访问了函数A的参数或局部变量时,就会产生upvalue

5.3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
LoadState S;
LClosure *cl;
if (*name == '@' || *name == '=')
S.name = name + 1;
else if (*name == LUA_SIGNATURE[0])
S.name = "binary string";
else
S.name = name;
S.L = L;
S.Z = Z;
checkHeader(&S);
cl = luaF_newLclosure(L, LoadByte(&S));
setclLvalue(L, L->top, cl);
luaD_inctop(L);
cl->p = luaF_newproto(L);
luaC_objbarrier(L, cl, cl->p);
LoadFunction(&S, cl->p, NULL);
lua_assert(cl->nupvalues == cl->p->sizeupvalues);
luai_verifycode(L, buff, cl->p);
return cl;
}
checkHeader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void checkHeader (LoadState *S) {
checkliteral(S, LUA_SIGNATURE + 1, "not a"); /* 1st char already checked */
if (LoadByte(S) != LUAC_VERSION)
error(S, "version mismatch in");
if (LoadByte(S) != LUAC_FORMAT)
error(S, "format mismatch in");
checkliteral(S, LUAC_DATA, "corrupted");
checksize(S, int);
checksize(S, size_t);
checksize(S, Instruction);
checksize(S, lua_Integer);
checksize(S, lua_Number);
if (LoadInteger(S) != LUAC_INT)
error(S, "endianness mismatch in");
if (LoadNumber(S) != LUAC_NUM)
error(S, "float format mismatch in");
}

5.3中checkHeader取代了LoadHeader,并且Header格式也有一点变化

变成了如下

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct {
char signature[4]; // #define LUA_SIGNATURE "\033Lua"
uchar version;
uchar format;
char luac_data[6]; //#define LUAC_DATA "\x19\x93\r\n\x1a\n"
uchar size_int;
uchar size_size_t;
uchar size_Instruction;
uchar lua_Integer;
uchar size_lua_Number;
long long endian;//#define LUAC_INT 0x5678
double lua_num_valid;//#define LUAC_NUM cast_num(370.5)
} GlobalHeader;
LoadFunction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void LoadFunction (LoadState *S, Proto *f, TString *psource) {
f->source = LoadString(S, f);
if (f->source == NULL) /* no source in dump? */
f->source = psource; /* reuse parent's source */
f->linedefined = LoadInt(S);
f->lastlinedefined = LoadInt(S);
f->numparams = LoadByte(S);
f->is_vararg = LoadByte(S);
f->maxstacksize = LoadByte(S);
LoadCode(S, f);
LoadConstants(S, f);
LoadUpvalues(S, f);
LoadProtos(S, f);
LoadDebug(S, f);
}

同样是加载一些proto信息

LoadCode
1
2
3
4
5
6
static void LoadCode (LoadState *S, Proto *f) {
int n = LoadInt(S);
f->code = luaM_newvector(S->L, n, Instruction);
f->sizecode = n;
LoadVector(S, f->code, n);
}

并无变化

LoadConstants
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void LoadConstants (LoadState *S, Proto *f) {
int i;
int n = LoadInt(S);
f->k = luaM_newvector(S->L, n, TValue);
f->sizek = n;
for (i = 0; i < n; i++)
setnilvalue(&f->k[i]);
for (i = 0; i < n; i++) {
TValue *o = &f->k[i];
int t = LoadByte(S);
switch (t) {
case LUA_TNIL:
setnilvalue(o);
break;
case LUA_TBOOLEAN:
setbvalue(o, LoadByte(S));
break;
case LUA_TNUMFLT:
setfltvalue(o, LoadNumber(S));
break;
case LUA_TNUMINT:
setivalue(o, LoadInteger(S));
break;
case LUA_TSHRSTR:
case LUA_TLNGSTR:
setsvalue2n(S->L, o, LoadString(S, f));
break;
default:
lua_assert(0);
}
}
}

递归处理不在出现在该函数

数字变量将整数于浮点数区分开,短字符串于长字符串分离

  • LUA_TNUMFLT:浮点数
  • LUA_TNUMINT:整数
  • LUA_TSHRSTR:短字符串
  • LUA_TLNGSTR:长字符串

但实际上应该只是针对某种特殊处理,因为基本类型还是这9种

1
2
3
4
5
6
7
8
9
10
11
#define LUA_TNIL		0
#define LUA_TBOOLEAN 1
#define LUA_TLIGHTUSERDATA 2
#define LUA_TNUMBER 3
#define LUA_TSTRING 4
#define LUA_TTABLE 5
#define LUA_TFUNCTION 6
#define LUA_TUSERDATA 7
#define LUA_TTHREAD 8

#define LUA_NUMTAGS 9
LoadUpvalues
1
2
3
4
5
6
7
8
9
10
11
12
static void LoadUpvalues (LoadState *S, Proto *f) {
int i, n;
n = LoadInt(S);
f->upvalues = luaM_newvector(S->L, n, Upvaldesc);
f->sizeupvalues = n;
for (i = 0; i < n; i++)
f->upvalues[i].name = NULL;
for (i = 0; i < n; i++) {
f->upvalues[i].instack = LoadByte(S);
f->upvalues[i].idx = LoadByte(S);
}
}

提前处理upvalue

LoadProtos
1
2
3
4
5
6
7
8
9
10
11
12
13
static void LoadProtos (LoadState *S, Proto *f) {
int i;
int n = LoadInt(S);
f->p = luaM_newvector(S->L, n, Proto *);
f->sizep = n;
for (i = 0; i < n; i++)
f->p[i] = NULL;
for (i = 0; i < n; i++) {
f->p[i] = luaF_newproto(S->L);
luaC_objbarrier(S->L, f, f->p[i]);
LoadFunction(S, f->p[i], f->source);
}
}

将递归处理proto单独使用一个函数

LoadDebug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void LoadDebug (LoadState *S, Proto *f) {
int i, n;
n = LoadInt(S);
f->lineinfo = luaM_newvector(S->L, n, int);
f->sizelineinfo = n;
LoadVector(S, f->lineinfo, n);
n = LoadInt(S);
f->locvars = luaM_newvector(S->L, n, LocVar);
f->sizelocvars = n;
for (i = 0; i < n; i++)
f->locvars[i].varname = NULL;
for (i = 0; i < n; i++) {
f->locvars[i].varname = LoadString(S, f);
f->locvars[i].startpc = LoadInt(S);
f->locvars[i].endpc = LoadInt(S);
}
n = LoadInt(S);
for (i = 0; i < n; i++)
f->upvalues[i].name = LoadString(S, f);
}

处理debug信息

docall

在分析完加载的过程后,我们再次回到handle_script向下执行,来到docall

1
2
3
4
5
6
7
8
9
10
11
12
static int docall (lua_State *L, int narg, int nres) {
int status;
int base = lua_gettop(L) - narg; /* function index */
lua_pushcfunction(L, msghandler); /* push message handler */
lua_insert(L, base); /* put it under function and args */
globalL = L; /* to be available to 'laction' */
signal(SIGINT, laction); /* set C-signal handler */
status = lua_pcall(L, narg, nres, base);
signal(SIGINT, SIG_DFL); /* reset C-signal handler */
lua_remove(L, base); /* remove message handler from the stack */
return status;
}

接着如下调用lua_pcall(lua_pcallk)->luaD_pcall->f_call->luaD_callnoyield->luaD_call->luaV_execute

luaV_execute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void luaV_execute (lua_State *L) {
CallInfo *ci = L->ci;
LClosure *cl;
TValue *k;
StkId base;
ci->callstatus |= CIST_FRESH; /* fresh invocation of 'luaV_execute" */
newframe: /* reentry point when frame changes (call/return) */
lua_assert(ci == L->ci);
cl = clLvalue(ci->func); /* local reference to function's closure */
k = cl->p->k; /* local reference to function's constant table */
base = ci->u.l.base; /* local copy of function's base */
/* main loop of interpreter */
for (;;) {
Instruction i;
StkId ra;
vmfetch();
vmdispatch (GET_OPCODE(i)) {
vmcase(OP_MOVE) {
setobjs2s(L, ra, RB(i));
vmbreak;
}
vmcase(OP_LOADK) {
TValue *rb = k + GETARG_Bx(i);
setobj2s(L, ra, rb);
vmbreak;
}

读取OPCODE并进行对应操作,5.15.3OPCODE有差异

5.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
typedef enum {
/*----------------------------------------------------------------------
name args description
------------------------------------------------------------------------*/
OP_MOVE,/* A B R(A) := R(B) */
OP_LOADK,/* A Bx R(A) := Kst(Bx) */
OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */
OP_LOADNIL,/* A B R(A) := ... := R(B) := nil */
OP_GETUPVAL,/* A B R(A) := UpValue[B] */

OP_GETGLOBAL,/* A Bx R(A) := Gbl[Kst(Bx)] */
OP_GETTABLE,/* A B C R(A) := R(B)[RK(C)] */

OP_SETGLOBAL,/* A Bx Gbl[Kst(Bx)] := R(A) */
OP_SETUPVAL,/* A B UpValue[B] := R(A) */
OP_SETTABLE,/* A B C R(A)[RK(B)] := RK(C) */

OP_NEWTABLE,/* A B C R(A) := {} (size = B,C) */

OP_SELF,/* A B C R(A+1) := R(B); R(A) := R(B)[RK(C)] */

OP_ADD,/* A B C R(A) := RK(B) + RK(C) */
OP_SUB,/* A B C R(A) := RK(B) - RK(C) */
OP_MUL,/* A B C R(A) := RK(B) * RK(C) */
OP_DIV,/* A B C R(A) := RK(B) / RK(C) */
OP_MOD,/* A B C R(A) := RK(B) % RK(C) */
OP_POW,/* A B C R(A) := RK(B) ^ RK(C) */
OP_UNM,/* A B R(A) := -R(B) */
OP_NOT,/* A B R(A) := not R(B) */
OP_LEN,/* A B R(A) := length of R(B) */

OP_CONCAT,/* A B C R(A) := R(B).. ... ..R(C) */

OP_JMP,/* sBx pc+=sBx */

OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */
OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */
OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */

OP_TEST,/* A C if not (R(A) <=> C) then pc++ */
OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */

OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */
OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */

OP_FORLOOP,/* A sBx R(A)+=R(A+2);
if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/* A sBx R(A)-=R(A+2); pc+=sBx */

OP_TFORLOOP,/* A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2));
if R(A+3) ~= nil then R(A+2)=R(A+3) else pc++ */
OP_SETLIST,/* A B C R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B */

OP_CLOSE,/* A close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n)) */

OP_VARARG/* A B R(A), R(A+1), ..., R(A+B-1) = vararg */
} OpCode;
5.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
typedef enum {
/*----------------------------------------------------------------------
name args description
------------------------------------------------------------------------*/
OP_MOVE,/* A B R(A) := R(B) */
OP_LOADK,/* A Bx R(A) := Kst(Bx) */
OP_LOADKX,/* A R(A) := Kst(extra arg) */
OP_LOADBOOL,/* A B C R(A) := (Bool)B; if (C) pc++ */
OP_LOADNIL,/* A B R(A), R(A+1), ..., R(A+B) := nil */
OP_GETUPVAL,/* A B R(A) := UpValue[B] */

OP_GETTABUP,/* A B C R(A) := UpValue[B][RK(C)] */
OP_GETTABLE,/* A B C R(A) := R(B)[RK(C)] */

OP_SETTABUP,/* A B C UpValue[A][RK(B)] := RK(C) */
OP_SETUPVAL,/* A B UpValue[B] := R(A) */
OP_SETTABLE,/* A B C R(A)[RK(B)] := RK(C) */

OP_NEWTABLE,/* A B C R(A) := {} (size = B,C) */

OP_SELF,/* A B C R(A+1) := R(B); R(A) := R(B)[RK(C)] */

OP_ADD,/* A B C R(A) := RK(B) + RK(C) */
OP_SUB,/* A B C R(A) := RK(B) - RK(C) */
OP_MUL,/* A B C R(A) := RK(B) * RK(C) */
OP_MOD,/* A B C R(A) := RK(B) % RK(C) */
OP_POW,/* A B C R(A) := RK(B) ^ RK(C) */
OP_DIV,/* A B C R(A) := RK(B) / RK(C) */
OP_IDIV,/* A B C R(A) := RK(B) // RK(C) */
OP_BAND,/* A B C R(A) := RK(B) & RK(C) */
OP_BOR,/* A B C R(A) := RK(B) | RK(C) */
OP_BXOR,/* A B C R(A) := RK(B) ~ RK(C) */
OP_SHL,/* A B C R(A) := RK(B) << RK(C) */
OP_SHR,/* A B C R(A) := RK(B) >> RK(C) */
OP_UNM,/* A B R(A) := -R(B) */
OP_BNOT,/* A B R(A) := ~R(B) */
OP_NOT,/* A B R(A) := not R(B) */
OP_LEN,/* A B R(A) := length of R(B) */

OP_CONCAT,/* A B C R(A) := R(B).. ... ..R(C) */

OP_JMP,/* A sBx pc+=sBx; if (A) close all upvalues >= R(A - 1) */
OP_EQ,/* A B C if ((RK(B) == RK(C)) ~= A) then pc++ */
OP_LT,/* A B C if ((RK(B) < RK(C)) ~= A) then pc++ */
OP_LE,/* A B C if ((RK(B) <= RK(C)) ~= A) then pc++ */

OP_TEST,/* A C if not (R(A) <=> C) then pc++ */
OP_TESTSET,/* A B C if (R(B) <=> C) then R(A) := R(B) else pc++ */

OP_CALL,/* A B C R(A), ... ,R(A+C-2) := R(A)(R(A+1), ... ,R(A+B-1)) */
OP_TAILCALL,/* A B C return R(A)(R(A+1), ... ,R(A+B-1)) */
OP_RETURN,/* A B return R(A), ... ,R(A+B-2) (see note) */

OP_FORLOOP,/* A sBx R(A)+=R(A+2);
if R(A) <?= R(A+1) then { pc+=sBx; R(A+3)=R(A) }*/
OP_FORPREP,/* A sBx R(A)-=R(A+2); pc+=sBx */

OP_TFORCALL,/* A C R(A+3), ... ,R(A+2+C) := R(A)(R(A+1), R(A+2)); */
OP_TFORLOOP,/* A sBx if R(A+1) ~= nil then { R(A)=R(A+1); pc += sBx }*/

OP_SETLIST,/* A B C R(A)[(C-1)*FPF+i] := R(A+i), 1 <= i <= B */

OP_CLOSURE,/* A Bx R(A) := closure(KPROTO[Bx]) */

OP_VARARG,/* A B R(A), R(A+1), ..., R(A+B-2) = vararg */

OP_EXTRAARG/* Ax extra (larger) argument for previous opcode */
} OpCode;

mi_lua

在github中找到了不少unluac的项目,但大多数都是java编写而成,不过学长提到的zh-explorer/mi_lua: xiaomi lua anti (github.com)

利用python的constrcut包构建与luac文件等价的结构体

将魔改后的luac文件先parse为中间结构体(既不是完全与mogailuac等价的luac,也不是标准的luac),然后再通过该结构体build为标准的luac

最后通过调用外部unluac对其进行还原可读格式

xiaomi

尝试恢复小米的luac文件,小米的lua版本是lua5.1

ida静态加载lua,按照前面的分析发现luaL_loadfile是个导入符号,grep搜索,可以知道这个符号来自liblua.so.5.1.5

在顺着路线向下分析到达关键的luaU_undump

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int __fastcall sub_11350(int a1, int a2, int a3, const char *a4)
{
int v5; // r0
bool v6; // zf
int v7; // r3
int v8; // r0
int v10[5]; // [sp+0h] [bp-50h] BYREF
char v11[10]; // [sp+14h] [bp-3Ch] BYREF
unsigned __int8 v12; // [sp+1Eh] [bp-32h]
char v13[10]; // [sp+24h] [bp-2Ch] BYREF
unsigned __int8 v14; // [sp+2Eh] [bp-22h]
int v15; // [sp+34h] [bp-1Ch]

v15 = *(_DWORD *)off_2FFE8;
v5 = *(unsigned __int8 *)a4;
v6 = v5 == 61;
if ( v5 != 61 )
v6 = v5 == 64;
if ( v6 )
{
++a4;
}
else if ( v5 == 27 )
{
a4 = "binary string";
}
v10[3] = (int)a4;
v10[0] = a1;
v10[1] = a2;
v10[2] = a3;
sub_112B8(v11);
sub_10A90(v10, v13, 16);
v7 = v14;
v14 = v12;
v10[4] = v7 != v12;
if ( memcmp(v11, v13, 16) )
sub_10A5C(v10, "bad header");
v8 = luaS_newlstr(a1, "=?", 2);
return sub_10D7C(v10, v8);
}

handle_header

通过下面这个函数我们可以知道header信息,可以大致判断小米修改了标识头,并且size_size_t变为了4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int __fastcall sub_112B8(char *a1)
{
BOOL v1; // r6
int result; // r0

if ( a1 < "\x1BFate/Z\x1B" && a1 + 8 > "\x1BFate/Z\x1B" )
__und(0);
v1 = a1 < "";
if ( a1 <= "\x1BFate/Z\x1B" )
v1 = 0;
if ( v1 )
__und(0);
result = ((int (*)(void))memcpy)();
a1[9] = 0;
a1[8] = 0x51;
a1[10] = 1;
a1[14] = 8;
a1[11] = 4;
a1[12] = 4;
a1[13] = 4;
a1[15] = 4;
return result;
}

在脚本中的处理是,更改结构体定义

1
2
3
4
5
6
7
8
9
10
11
GlobalHead = Struct(
"signature" / Const(b"\x1bFate/Z\x1b"),
"version" / Version,
"format" / Format,
"endian" / Endian,
"size_int" / Int8ul,
"size_size_t" / Int8ul,
"size_instruction" / Int8ul,
"size_lua_number" / Int8ul,
"lua_num_valid" / Byte
)

同时能够通过如下函数延迟绑定动态决定相关数据大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def lua_type_define(head):
global LuaInstruction, LuaInt, LuaNumber, LuaSize_t
if head.size_int == 4:
LuaInt = Int32sl
elif head.size_int == 8:
LuaInt = Int64sl
else:
LuaDecodeException("Unsupported size int")

if head.size_size_t == 4:
LuaSize_t = Int32ul
elif head.size_size_t == 8:
LuaSize_t = Int64ul
else:
LuaDecodeException("Unsupported size int")

if head.size_lua_number == 8:
LuaNumber = Double
elif head.size_lua_number == 4:
LuaNumber = Single
else:
LuaDecodeException("Unsupported size int")

if head.size_instruction == 4:
LuaInstruction = Int32ul
else:
LuaDecodeException("Unsupported size int")

handle_constans

接着进入Functioin的加载部分,主要关注switch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    switch ( sub_10BEC(a1) )
{
case 3:
v42 = 0;
goto LABEL_43;
case 4:
v43 = sub_10BEC(a1);
v42 = 1;
*(_DWORD *)(v40 + 16 * i) = v43 != 0;
goto LABEL_43;
case 6:
sub_10AC0(a1, &v53, 1, 8, v52);
*(_QWORD *)v41 = v53;
v42 = 3;
goto LABEL_43;
case 7:
*(_DWORD *)(v40 + 16 * i) = sub_10CB8(a1);
*(_DWORD *)(v41 + 8) = 4;
continue;
case 12:
sub_10AC0(a1, &v53, 1, 4, v52);
*(_DWORD *)(v40 + 16 * i) = v53;
v42 = 9;
LABEL_43:
*(_DWORD *)(v41 + 8) = v42;
break;
default:
sub_10A5C(a1, "bad constant");
break;
}

通过比较操作的代码,可以知道小米只是将常量加了偏移3,但可以发现xiaomi多了一种常量处理,虽然不知道其是什么类型,但可以知道其是4字节大小

所以需要额外添加一个常量(这并不一定能够成功),然后因为不好分析其具体类型,就只能当作number来处理,并且数据经过尝试应该要是为float

偏移则通过解码时减去3处理,添加一种数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
LuaDatatype = Enum(Byte,
LUA_TNIL=0,
LUA_TBOOLEAN=1,
LUA_TLIGHTUSERDATA=2,
LUA_TNUMBER=3,
LUA_TSTRING=4,
LUA_TTABLE=5,
LUA_TFUNCTION=6,
LUA_TUSERDATA=7,
LUA_TTHREAD=8,
LUA_MIDATA=9)


class LuaDatatypeAdapter(Adapter):
def _decode(self, obj, context, path):
if obj == 12:
logging.warning("translate may not success")
return LuaDatatype.parse(bytes([obj - 3]))

def _encode(self, obj, context, path):
return bytes([int(obj) + 3])

然后数据类型解析增加对应情况

1
2
3
4
5
Constant = ConstantAdapter(Struct(
"data_type" / LuaDatatypeAdapter(Byte),
"data" / Switch(this.data_type,
{"LUA_TNIL": Pass, "LUA_TBOOLEAN": Flag,
"LUA_TNUMBER": LazyBound(lambda: LuaNumber), "LUA_TSTRING": String, "LUA_MIDATA": Int32ul})

最后添加的数据类型还是按照处理数字类型的处理,但是数据转换为float

1
2
3
4
5
6
7
8
9
class ConstantAdapter(Adapter):
def _decode(self, obj, context, path):
if int(obj.data_type) == 9:
obj.data_type = LuaDatatype.parse(b'\x03')
obj.data = float(obj.data)
return obj

def _encode(self, obj, context, path):
return obj

handle_string

继续通过观察xiaomi的十六进制数据,可以发现其中几乎没有明文字符串,但正常luac是能够找到一些明文的,所以猜测xiaomi还对字符串进行了加密

进入字符串获取的处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __fastcall sub_10CB8(_DWORD *a1, int a2)
{
int v4; // r5
_BYTE *i; // r3
unsigned int v6; // [sp+0h] [bp-18h] BYREF
int v7; // [sp+4h] [bp-14h]

v6 = (unsigned int)a1;
v7 = a2;
v7 = *(_DWORD *)off_2FFE8;
((void (__fastcall *)(_DWORD *, unsigned int *, int, int))sub_10AC0)(a1, &v6, 1, 4);
if ( !v6 )
return 0;
v4 = sub_137C8(*a1, a1[2]);
sub_10A90((int)a1);
for ( i = (_BYTE *)v4; v6 > (unsigned int)&i[-v4]; ++i )
*i ^= 13 * v6 + 55;
return luaS_newlstr(*a1, v4, v6 - 1);
}

解密部分是这一块

1
2
for ( i = (_BYTE *)v4; v6 > (unsigned int)&i[-v4]; ++i )
*i ^= 13 * v6 + 55;

v6通过上下文可以知道就是字符串的长度,那么对应的处理也就知晓

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class StrAdapter(Adapter):
def __init__(self, key, subcon):
assert key < 0xff
self.key = key
super().__init__(subcon)

def _decode(self, obj, context, path):
l = []
key = evaluate(self.key, context)
for i in obj:
l.append(i ^ key)
return bytes(l)

def _encode(self, obj, context, path):
l = []
key = evaluate(self.key, context)
for i in obj:
l.append(i ^ key)
return bytes(l)


String = Struct(
"size" / LazyBound(lambda: LuaSize_t),
"str" / StrAdapter((this.size * 13 + 55) & 0xff, Bytes(this.size))
)

原版的 String 结构非常直接,没有任何自定义的处理或操作,就是读取字符串长度 size,读取对应长度的字节流 str

处理xiaomi的顺序,则是读取字符串长度size,然后通过size计算出密钥,然后读取size长度的字节流,最后对字节流按密钥处理

handle_Opcode

最后xiaomi还对opcode顺序进行了打乱

此外xiaomi还打乱了Opcode的值与操作对应关系,一般就是通过与自己编译出来的作比较,判断出映射关系,建议以官方版本为对照,逐个去改版中找对应官方顺序的操作

找出对应的映射

1
OpCodeMap = [20, 36, 0, 24, 19, 24, 1, 34, 30, 26, 33, 32, 13, 29, 15, 11, 28, 9, 4, 23, 23, 21, 25, 25, 2, 16, 31, 6, 10, 35, 37, 22, 18, 17, 14, 27, 0, 12, 5, 8, 7, 3]

然后根据OpCodeMap使用魔改后的opcode找到其实现的标准功能,并将其opcode转化为标准形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class InstructionsAdapter(Adapter):
def _encode(self, obj, context, path):
obj.opcode = OpCode.parse(integer2bits(OpCodeMap.index(int(obj.opcode)), 6))
return obj

def _decode(self, obj, context, path):
if int(obj.opcode) == 2:
logging.warning("find mi opcode")
if obj.C == 0:
op = 29
elif obj.C == 1:
op = 0
elif obj.C == 2:
op = 32
elif obj.C == 3:
op = 4
else:
LuaDecodeException("opcode error")
obj.opcode = OpCode.parse(integer2bits(OpCodeMap[op], 6))
else:
obj.opcode = OpCode.parse(integer2bits(OpCodeMap[int(obj.opcode)], 6))
return obj