我们的程序很多业务逻辑由Lua实现,为了防止业务逻辑被曝光,需要对Lua代码进行加密。
我们有两种思路:
- 自定义字节码: Lua库可以直接调用编译后生成的Lua字节码,因而我们可以将源码编译成字节码对外提供。但是因为Lua是开源的,可以通过工具将字节码反编译回源码。我们可以自定义字节码,加大反编译的难度。
- 将Lua源码文件加密,在Lua编译字节码前,对源码文件进行解密
本文主要介绍第二种思路的实现。
我们的程序使用LuaJIT来执行Lua代码,因而以LuaJIT来说明。
我们的C程序使用luaL_loadfile()函数来加载Lua源码。
1 2 3 4
| LUALIB_API int luaL_loadfile(lua_State *L, const char *filename) { return luaL_loadfilex(L, filename, NULL); }
|
可以看到luaL_loadfile是luaL_loadfilex()的简单封装,再来看luaL_loadfilex实现:
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
| LUALIB_API int luaL_loadfilex(lua_State *L, const char *filename, const char *mode) { FileReaderCtx ctx; int status; const char *chunkname; if (filename) { ctx.fp = fopen(filename, "rb"); if (ctx.fp == NULL) { lua_pushfstring(L, "cannot open %s: %s", filename, strerror(errno)); return LUA_ERRFILE; } chunkname = lua_pushfstring(L, "@%s", filename); } else { ctx.fp = stdin; chunkname = "=stdin"; } status = lua_loadx(L, reader_file, &ctx, chunkname, mode); if (ferror(ctx.fp)) { L->top -= filename ? 2 : 1; lua_pushfstring(L, "cannot read %s: %s", chunkname+1, strerror(errno)); if (filename) fclose(ctx.fp); return LUA_ERRFILE; } if (filename) { L->top--; copyTV(L, L->top-1, L->top); fclose(ctx.fp); } return status; }
|
当从文件加载Lua代码时,luaL_loadfilex()调用fopen()打开文件,将文件流指针存储在ctx.fp中,再调用lua_loadx()编译Lua源码。
我们可以在这里将源码文件进行解密,再打开解密后的文件,用解密后的文件流指针替换密文文件流指针,再调用lua_loadx()完成编译。我们使用一个特定文件头来标识密文文件。为了兼容未加密的文件,我们首先判断是否为密文文件。若不是,则直接将文件指针恢复到文件头。否则,调用decrypt_file()解密文件并打开解密后的文件,将文件指针存储于ctx.fp中。
修改后的源码为:
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
| LUALIB_API int luaL_loadfilex(lua_State *L, const char *filename, const char *mode) { FileReaderCtx ctx; int status; const char *chunkname; char file_header[FILE_HEADER_LEN]; size_t sz;
if (filename) { ctx.fp = fopen(filename, "rb"); if (ctx.fp == NULL) { lua_pushfstring(L, "cannot open %s: %s", filename, strerror(errno)); return LUA_ERRFILE; }
sz = fread(file_header, 1, FILE_HEADER_LEN, ctx.fp); if (sz == FILE_HEADER_LEN) { if (memcmp(file_header, FILE_HEADER, FILE_HEADER_LEN - 1) == 0) { ctx.fp = decrypt_file(ctx.fp); } } fseek(ctx.fp, 0L, SEEK_SET);
chunkname = lua_pushfstring(L, "@%s", filename); } else { ctx.fp = stdin; chunkname = "=stdin"; } status = lua_loadx(L, reader_file, &ctx, chunkname, mode); if (ferror(ctx.fp)) { L->top -= filename ? 2 : 1; lua_pushfstring(L, "cannot read %s: %s", chunkname+1, strerror(errno)); if (filename) fclose(ctx.fp); return LUA_ERRFILE; } if (filename) { L->top--; copyTV(L, L->top-1, L->top); fclose(ctx.fp); } return status; }
|
接着来看decrypt_file()实现:
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 69 70 71 72 73 74 75 76 77
| static FILE *decrypt_file(FILE *ofp) { int fd, len; size_t sz; FILE *fp; unsigned char *buf, *obuf; char file_temp[] = "/tmp/luajit-XXXXXX";
fp = NULL; buf = NULL; obuf = NULL; fd = -1;
fseek(ofp, 0L, SEEK_END); sz = ftell(ofp);
obuf = malloc(sz); if (obuf == NULL) { goto failed; }
fseek(ofp, 0L, SEEK_SET); if (fread(obuf, 1, sz, ofp) < sz) { goto failed; }
fclose(ofp); ofp = NULL;
buf = blowfish_decrypt(obuf + FILE_HEADER_LEN, sz - FILE_HEADER_LEN, g_key, g_iv, &len); if (buf == NULL) { goto failed; }
free(obuf); obuf = NULL;
fd = mkstemp(file_temp); if (fd < 0) { goto failed; } unlink(file_temp);
fp = fdopen(fd, "wb+"); if (fp == NULL) { goto failed; } fwrite(buf, 1, len, fp); free(buf); buf = NULL;
return fp;
failed:
if (fp) { fclose(fp); }
if (ofp) { fclose(ofp); }
if (obuf) { free(obuf); }
if (buf) { free(buf); }
return NULL; }
|
首先,调用解密函数将源码文件解密到内存BUFFER中。然后,调用mkstemp()创建一个临时文件,并调用unlink()将其删除,避免解密后的文件被其他用户或程序获取到,也避免留下没用的临时文件。之后,接着将解密出的内容写入临时文件,最后返回临时文件的文件指针,完成文件指针的替换。
除了临时文件,也可以使用PIPE()来实现。将解密后的内容写入PIPE写端,调用fdopen()将PIPE读端构造成文件流指针返回。由于PIPE的大小有限制,一般为64K,不能处理大于64K的文件。因而我们采用临时文件的方法来实现。