Lua脚本加密(一)

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 位)。每个块会经过以下处理:

  1. 初始化向量(IV):首先,CBC 模式需要一个随机生成的初始化向量(IV)。IV 的大小与分组密码的块大小一致(例如,AES 的块大小为 128 位)。IV 在加密过程中与明文的第一个块进行异或(XOR)操作。
  2. 链式异或加密:加密过程中的每个块都依赖于前一个密文块。对于第一个明文块,首先将其与 IV 进行异或操作,然后对结果进行加密,得到第一个密文块。对于后续的每个明文块,都会将当前的明文块与前一个密文块进行异或操作,然后再加密,得到对应的密文块。具体来说:
  • 第一个密文块:C1 = E(IV ⊕ P1) (其中 P1 是第一个明文块,E() 表示加密操作,IV 是初始化向量)
  • 第二个密文块:C2 = E(C1 ⊕ P2)
  • 第三个密文块:C3 = E(C2 ⊕ P3)
  • 以此类推…
  1. 解密过程:解密时,必须使用相同的密钥和 IV。解密的过程反向进行,即先解密密文块,然后将解密后的结果与前一个密文块异或,恢复出原始的明文数据。
  • 第一个明文块:P1 = D(C1) ⊕ IV
  • 第二个明文块:P2 = D(C2) ⊕ C1
  • 第三个明文块:P3 = D(C3) ⊕ C2
  • 以此类推…

特点和优缺点

优点:

  1. 安全性:由于 CBC 模式涉及到将每个明文块与前一个密文块进行异或操作,这使得相同的明文在不同的加密过程中会生成不同的密文(即使使用相同的密钥)。因此,CBC 能够防止“相同明文产生相同密文”的攻击(即“频率分析”攻击)。
  2. 抗重放攻击:由于密文块的加密依赖于前一个块的密文,因此即使有重放攻击,攻击者也无法伪造有效的密文块。
  3. 分组加密:可以将明文分割成固定大小的块进行加密,这对于一些数据格式(如文件加密)非常方便。

缺点:

  1. 初始化向量(IV)问题:IV 的随机性和保密性至关重要。如果 IV 重复或被泄露,可能会导致加密过程中的安全性问题。因此,IV 通常需要随机生成,并且必须在传输过程中与密文一同发送。
  2. 并行化难度:CBC 模式在加密和解密过程中都依赖于前一个密文块的输出,这使得 CBC 模式不容易并行处理(这在硬件或多核处理器上是一个问题)。每个块的加密必须等待前一个块的加密结果,因此不能高效地并行化。
  3. 填充问题:由于明文的长度通常不是块的整数倍,所以需要进行填充操作,可能导致额外的存储和处理开销。

应用场景:

  • 加密文件:CBC 模式通常用于文件加密,因为它能处理长数据流,并且避免相同数据产生相同密文。
  • SSL/TLS:在一些网络安全协议(如 SSL/TLS)中,也使用 CBC 模式来加密传输的数据。
  • 数据库加密:在加密数据库内容时,CBC 模式常常用于保护存储的敏感数据。

安全性

虽然 CBC 是一种有效的加密模式,但它也有一些潜在的攻击方式,尤其是IV管理不当。攻击者如果知道 IV 或者两个密文块的 IV 区分不清楚,可能会进行特定的攻击(例如,修改密文、重放攻击等)。因此,在使用 CBC 时,必须小心处理 IV,确保每次加密使用不同的随机 IV,并且 IV 和密文一起发送。

另外,使用 CBC 时还要注意填充问题。常见的填充方法包括 PKCS#7,目的是确保明文的长度是块大小的整数倍。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注