Lua脚本加密(一)
需求背景:
基于 Nginx 开发的服务器,业务逻辑使用了 Lua 脚本进行编程。打包部署到现场的时候,业务代码完全暴露,安装包流转过程中,也容易泄露,暴露 Lua 脚本的业务逻辑。
安全起见,对 Lua 脚本进行加密,同时让 Nginx 同时支持加载加密和非加密的 Lua 脚本。
Nginx 对 Lua 的支持
nginx 为了支持 lua,需要要有 lua 模块,
https://github.com/openresty/lua-nginx-module/archive/refs/tags/v0.10.22.tar.gz
有了 lua 模块我们还需要安装 luajit 库。
源码分析
Lua 脚本的加载是在 LuaJIT 中实现,lua 模块只是会调用 C 的一些接口。所以要实现加密,则需要更改 LuaJIT 中的接口。
LuaJIT 加载 lua 脚本并不是一次性加载文件,而是会调用 getc,ungetc,以及 fread 函数。代码如下:
ngx_int_t
ngx_http_lua_clfactory_loadfile(lua_State *L, const char *filename)
{
int c, status, readstatus;
ngx_flag_t sharp;
ngx_http_lua_clfactory_file_ctx_t lf;
/* index of filename on the stack */
int fname_index;
sharp = 0;
fname_index = lua_gettop(L) + 1;
lf.extraline = 0;
lf.file_type = NGX_LUA_TEXT_FILE;
#ifndef OPENRESTY_LUAJIT
lf.begin_code.ptr = CLFACTORY_BEGIN_CODE;
lf.begin_code_len = CLFACTORY_BEGIN_SIZE;
lf.end_code.ptr = CLFACTORY_END_CODE;
lf.end_code_len = CLFACTORY_END_SIZE;
#endif
lua_pushfstring(L, "@%s", filename);
lf.f = fopen(filename, "r");
if (lf.f == NULL) {
return ngx_http_lua_clfactory_errfile(L, "open", fname_index);
}
c = getc(lf.f);
if (c == '#') { /* Unix exec. file? */
lf.extraline = 1;
while ((c = getc(lf.f)) != EOF && c != '\n') {
/* skip first line */
}
if (c == '\n') {
c = getc(lf.f);
}
sharp = 1;
}
if (c == LUA_SIGNATURE[0] && filename) { /* binary file? */
lf.f = freopen(filename, "rb", lf.f); /* reopen in binary mode */
if (lf.f == NULL) {
return ngx_http_lua_clfactory_errfile(L, "reopen", fname_index);
}
/* check whether lib jit exists */
luaL_findtable(L, LUA_REGISTRYINDEX, "_LOADED", 1);
lua_getfield(L, -1, "jit"); /* get _LOADED["jit"] */
if (lua_istable(L, -1)) {
lf.file_type = NGX_LUA_BT_LJ;
} else {
lf.file_type = NGX_LUA_BT_LUA;
}
lua_pop(L, 2);
/*
* Loading bytecode with an extra header is disabled for security
* reasons. This may circumvent the usual check for bytecode vs.
* Lua code by looking at the first char. Since this is a potential
* security violation no attempt is made to echo the chunkname either.
*/
if (lf.file_type == NGX_LUA_BT_LJ && sharp) {
if (filename) {
fclose(lf.f); /* close file (even in case of errors) */
}
filename = lua_tostring(L, fname_index) + 1;
lua_pushfstring(L, "bad byte-code header in %s", filename);
lua_remove(L, fname_index);
return LUA_ERRFILE;
}
while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) {
/* skip eventual `#!...' */
}
#ifndef OPENRESTY_LUAJIT
status = ngx_http_lua_clfactory_bytecode_prepare(L, &lf, fname_index);
if (status != 0) {
return status;
}
#endif
lf.extraline = 0;
}
#ifndef OPENRESTY_LUAJIT
if (lf.file_type == NGX_LUA_TEXT_FILE) {
ungetc(c, lf.f);
}
lf.sent_begin = lf.sent_end = 0;
#else
ungetc(c, lf.f);
#endif
status = lua_load(L, ngx_http_lua_clfactory_getF, &lf,
lua_tostring(L, -1));
readstatus = ferror(lf.f);
if (filename) {
fclose(lf.f); /* close file (even in case of errors) */
}
if (readstatus) {
lua_settop(L, fname_index); /* ignore results from `lua_load' */
return ngx_http_lua_clfactory_errfile(L, "read", fname_index);
}
lua_remove(L, fname_index);
return status;
}
c = getc(lf.f);
ungetc(c, lf.f);
后面再调用 lua_load,文件读取的回调函数为ngx_http_lua_clfactory_getF。
status = lua_load(L, ngx_http_lua_clfactory_getF, &lf,
lua_tostring(L, -1));
ngx_http_lua_clfactory_getF
static const char *
ngx_http_lua_clfactory_getF(lua_State *L, void *ud, size_t *size)
{
#ifndef OPENRESTY_LUAJIT
char *buf;
#endif
size_t num;
ngx_http_lua_clfactory_file_ctx_t *lf;
lf = (ngx_http_lua_clfactory_file_ctx_t *) ud;
if (lf->extraline) {
lf->extraline = 0;
*size = 1;
return "\n";
}
#ifndef OPENRESTY_LUAJIT
if (lf->sent_begin == 0) {
lf->sent_begin = 1;
*size = lf->begin_code_len;
if (lf->file_type == NGX_LUA_TEXT_FILE) {
buf = lf->begin_code.ptr;
} else {
buf = lf->begin_code.str;
}
return buf;
}
#endif /* OPENRESTY_LUAJIT */
num = fread(lf->buff, 1, sizeof(lf->buff), lf->f);
dd("fread returned %d", (int) num);
if (num == 0) {
#ifndef OPENRESTY_LUAJIT
if (lf->sent_end == 0) {
lf->sent_end = 1;
*size = lf->end_code_len;
if (lf->file_type == NGX_LUA_BT_LUA) {
buf = lf->end_code.str;
} else {
buf = lf->end_code.ptr;
}
return buf;
}
#endif /* OPENRESTY_LUAJIT */
*size = 0;
return NULL;
}
#ifndef OPENRESTY_LUAJIT
if (lf->file_type == NGX_LUA_BT_LJ) {
/* skip the footer(\x00) in luajit */
lf->rest_len -= num;
if (lf->rest_len == 0) {
if (--num == 0 && lf->sent_end == 0) {
lf->sent_end = 1;
buf = lf->end_code.ptr;
*size = lf->end_code_len;
return buf;
}
}
}
#endif /* OPENRESTY_LUAJIT */
*size = num;
return lf->buff;
}
ngx_http_lua_clfactory_getF 中使用到了 fread。
初步结论
所以总结起来,我们要 hook 掉 getc,ungetc以及 fread 函数,这里的 hook 就是换掉 getc, ungetc 以及 fread 函数,功能不变,只是操作的是加密文件,返回的是明文。
加密算法的选择
读取加密文件每次的字节数都是不固定的,所以只能选择流加密算法。
这里选择 CBC 加密算法,CBC 加密,当前加密的结果和前一个块有关系,这样就规避了用统计的方法猜出明文。
CBC 算法
CBC(Cipher Block Chaining)算法介绍
**CBC(Cipher Block Chaining)**是一种常用的分组密码加密模式,它通过将每个明文块与前一个加密后的密文块进行“链式”操作,从而增加加密的复杂性和安全性。CBC模式是对称加密算法中的一种常见工作模式,通常与分组密码(如 AES、DES)一起使用。
工作原理
在 CBC 模式中,明文数据被划分为固定大小的块(例如 128 位)。每个块会经过以下处理:
- 初始化向量(IV):首先,CBC 模式需要一个随机生成的初始化向量(IV)。IV 的大小与分组密码的块大小一致(例如,AES 的块大小为 128 位)。IV 在加密过程中与明文的第一个块进行异或(XOR)操作。
- 链式异或加密:加密过程中的每个块都依赖于前一个密文块。对于第一个明文块,首先将其与 IV 进行异或操作,然后对结果进行加密,得到第一个密文块。对于后续的每个明文块,都会将当前的明文块与前一个密文块进行异或操作,然后再加密,得到对应的密文块。具体来说:
- 第一个密文块:
C1 = E(IV ⊕ P1)(其中P1是第一个明文块,E()表示加密操作,IV是初始化向量) - 第二个密文块:
C2 = E(C1 ⊕ P2) - 第三个密文块:
C3 = E(C2 ⊕ P3) - 以此类推…
- 解密过程:解密时,必须使用相同的密钥和 IV。解密的过程反向进行,即先解密密文块,然后将解密后的结果与前一个密文块异或,恢复出原始的明文数据。
- 第一个明文块:
P1 = D(C1) ⊕ IV - 第二个明文块:
P2 = D(C2) ⊕ C1 - 第三个明文块:
P3 = D(C3) ⊕ C2 - 以此类推…
特点和优缺点
优点:
- 安全性:由于 CBC 模式涉及到将每个明文块与前一个密文块进行异或操作,这使得相同的明文在不同的加密过程中会生成不同的密文(即使使用相同的密钥)。因此,CBC 能够防止“相同明文产生相同密文”的攻击(即“频率分析”攻击)。
- 抗重放攻击:由于密文块的加密依赖于前一个块的密文,因此即使有重放攻击,攻击者也无法伪造有效的密文块。
- 分组加密:可以将明文分割成固定大小的块进行加密,这对于一些数据格式(如文件加密)非常方便。
缺点:
- 初始化向量(IV)问题:IV 的随机性和保密性至关重要。如果 IV 重复或被泄露,可能会导致加密过程中的安全性问题。因此,IV 通常需要随机生成,并且必须在传输过程中与密文一同发送。
- 并行化难度:CBC 模式在加密和解密过程中都依赖于前一个密文块的输出,这使得 CBC 模式不容易并行处理(这在硬件或多核处理器上是一个问题)。每个块的加密必须等待前一个块的加密结果,因此不能高效地并行化。
- 填充问题:由于明文的长度通常不是块的整数倍,所以需要进行填充操作,可能导致额外的存储和处理开销。
应用场景:
- 加密文件:CBC 模式通常用于文件加密,因为它能处理长数据流,并且避免相同数据产生相同密文。
- SSL/TLS:在一些网络安全协议(如 SSL/TLS)中,也使用 CBC 模式来加密传输的数据。
- 数据库加密:在加密数据库内容时,CBC 模式常常用于保护存储的敏感数据。
安全性
虽然 CBC 是一种有效的加密模式,但它也有一些潜在的攻击方式,尤其是IV管理不当。攻击者如果知道 IV 或者两个密文块的 IV 区分不清楚,可能会进行特定的攻击(例如,修改密文、重放攻击等)。因此,在使用 CBC 时,必须小心处理 IV,确保每次加密使用不同的随机 IV,并且 IV 和密文一起发送。
另外,使用 CBC 时还要注意填充问题。常见的填充方法包括 PKCS#7,目的是确保明文的长度是块大小的整数倍。