Keep learning, keep living...

0%

Lua代码加密

我们的程序很多业务逻辑由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;
}

/* check file header */
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) {
/* decrypt file */
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的文件。因而我们采用临时文件的方法来实现。