lua作为嵌入式脚本语言,在项目开发中的积极作用毋庸置疑。虽然便捷,但随lua使用者的数量增多,lua暴露出来的项目核心代码的问题的越来越明显。随着官方提供的luac的解释器,能够给lua明文代码做简单的加密。文章主要正对字节码的序列化和反序列化进行分析。
luac.c文件就是luac.exe的实现部分。
ldump.c和lundump.c 分别是lua的保存和读取字节码的实现部分。
luaU_dump作为dump的函数入口,声明如下:
int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip)
Proto类型是lua中一段chunk的原型,在lobject.h中声明,是不对外公开的内部类型,lua把任何一段可执行的脚本都看成一个Proto,Proto包含了函数原型的字节码,函数引用的常量表,debug信息(包括行号),本地变量,upvalue。由于Proto保存了相当完整的debug信息,所以如果在compile的时候把这些debug信息输出,是非常有利于反编译的。
实际上Proto就是一个闭包实现,而在lua中你可以把一个lua文件看成一个闭包。所以就很容易理解,lua语言结构其实就是递归包含闭包的关系。
从DumpFunction函数的实现可以就能看出这层关系,子闭包作为一个常量在DumpConstants函数中dump了。
static void DumpFunction(const Proto* f, const TString* p, DumpState* D)
{
DumpString((f->source==p || D->strip) ? NULL : f->source,D);
DumpInt(f->linedefined,D);
DumpInt(f->lastlinedefined,D);
DumpChar(f->nups,D);
DumpChar(f->numparams,D);
DumpChar(f->is_vararg,D);
DumpChar(f->maxstacksize,D);
DumpCode(f,D);
DumpConstants(f,D);
DumpDebug(f,D);
}
static void DumpConstants(const Proto* f, DumpState* D)
{
int i,n=f->sizek;
DumpInt(n,D);
for (i=0; iconst TValue* o=&f->k[i];
DumpChar(ttype(o),D);
switch (ttype(o))
{
case LUA_TNIL:
break;
case LUA_TBOOLEAN:
DumpChar(bvalue(o),D);
break;
case LUA_TNUMBER:
DumpNumber(nvalue(o),D);
break;
case LUA_TSTRING:
DumpString(rawtsvalue(o),D);
break;
default:
lua_assert(0); /* cannot happen */
break;
}
}
n=f->sizep;
DumpInt(n,D);
for (i=0; ip[i],f->source,D);
}
那么真正输入输出的地方是LoadBlock和DumpBlock函数。那么如果我们需要给我们的字节码进行加密,修改这两个函数即可。可以对数组b做一些简单的运算即可。
static void LoadBlock(LoadState* S, void* b, size_t size)
{
size_t r=luaZ_read(S->Z,b,size);
IF (r!=0, "unexpected end");
}
static void DumpBlock(const void* b, size_t size, DumpState* D)
{
if (D->status==0)
{
lua_unlock(D->L);
D->status=(*D->writer)(D->L,b,size,D->data);
lua_lock(D->L);
}
}
如果你觉得这么做太容易被破解了,你可以重写DumpFunction的实现,打乱闭包中各成员的dump顺序。如果你不直接使用lua的动态链接库也可以增加静态分析的破解成本。