还原iot设备中魔改的luac
字数 1110 2025-08-20 18:18:11

还原IoT设备中魔改的Luac文件 - TP-Link Archer C7实例分析

1. 背景介绍

本文详细讲解如何还原IoT设备中经过修改的Lua字节码文件(Luac),以TP-Link Archer C7路由器为例。该设备使用的Lua版本为5.1.4,但对其字节码格式进行了自定义修改,导致标准反编译工具无法直接使用。

2. Lua字节码加载流程分析

2.1 标准Lua加载流程

标准Lua加载执行chunk的流程如下:

  1. lua_open 打开Lua环境
  2. luaL_dofile 宏实际调用 luaL_loadfilelua_pcall
  3. luaL_loadfile 最终调用 f_parser
  4. f_parser 判断文件类型并解析
#define luaL_dofile(L, fn) \
    (luaL_loadfile(L, fn) || lua_pcall(L, 0, LUA_MULTRET, 0))

int main(void) {
    lua_State *L=lua_open();
    lua_register(L, "print",print);
    if (luaL_dofile(L, NULL)!=0)
        fprintf(stderr, "%s\n",lua_tostring(L, -1));
    lua_close(L);
    return 0;
}

2.2 解析过程详解

f_parser 函数的工作流程:

static void f_parser (lua_State *L, void *ud) {
    int i;
    Proto *tf;
    Closure *cl;
    struct SParser *p = cast(struct SParser *, ud);
    int c = luaZ_lookahead(p->z);  // 预读第一个字符
    luaC_checkGC(L);
    tf = ((c == LUA_SIGNATURE[0]) ? luaU_undump : luaY_parser)(L, p->z, &p->buff, p->name);
    cl = luaF_newLclosure(L, tf->nups, hvalue(gt(L)));
    cl->l.p = tf;
    for (i=0; i<tf->nups; i++)  /* initialize eventual upvalues */
        cl->l.upvals[i] = luaF_newupval(L);
    setclvalue(L, L->top, cl);
    incr_top(L);
}

2.2.1 luaU_undump 函数

负责解析字节码文件:

Proto* luaU_undump (lua_State* L, ZIO* Z, Mbuffer* buff, const char* name) {
    LoadState S;
    if (*name=='@' || *name=='=') S.name=name+1;
    else if (*name==LUA_SIGNATURE[0]) S.name="binary string";
    else S.name=name;
    S.L=L;
    S.Z=Z;
    S.b=buff;
    LoadHeader(&S);    // 文件头
    return LoadFunction(&S,luaS_newliteral(L, "=?"));  // 函数体
}

2.2.2 文件头结构

标准Luac文件头格式:

typedef struct {
    char signature[4];    // #define LUA_SIGNATURE "\033Lua"
    uchar version;        // 0x51,0x52,0x53
    uchar format;
    uchar endian;
    uchar size_int;
    uchar size_size_t;
    uchar size_Instruction;
    uchar size_lua_Number;
    uchar lua_num_valid;
    uchar luac_tail[0x6];
} GlobalHeader;

2.2.3 LoadFunction 函数

加载函数原型:

static Proto* LoadFunction (LoadState* S, TString* p) {
    Proto* f;
    if (++S->L->nCcalls > LUAI_MAXCCALLS) error(S,"code too deep");
    f=luaF_newproto(S->L);
    setptvalue2s(S->L,S->L->top,f);
    incr_top(S->L);
    f->source=LoadString(S);
    if (f->source==NULL) f->source=p;
    // protoheader
    f->linedefined=LoadInt(S);
    f->lastlinedefined=LoadInt(S);
    f->nups=LoadByte(S);
    f->numparams=LoadByte(S);
    f->is_vararg=LoadByte(S);
    f->maxstacksize=LoadByte(S);
    LoadCode(S,f);       // code
    LoadConstants(S,f);  // constants
    LoadDebug(S,f);      // debug
    IF(!luaG_checkcode(f),"bad code");
    S->L->top--;
    S->L->nCcalls--;
    return f;
}

3. TP-Link Lua修改点分析

3.1 Opcode重排

TP-Link修改了标准Lua的Opcode顺序:

标准Opcode顺序:

typedef enum {
    OP_MOVE, OP_LOADK, OP_LOADBOOL, OP_LOADNIL,
    OP_GETUPVAL, OP_GETGLOBAL, OP_GETTABLE,
    OP_SETGLOBAL, OP_SETUPVAL, OP_SETTABLE,
    OP_NEWTABLE, OP_SELF, OP_ADD, OP_SUB,
    OP_MUL, OP_DIV, OP_MOD, OP_POW, OP_UNM,
    OP_NOT, OP_LEN, OP_CONCAT, OP_JMP, OP_EQ,
    OP_LT, OP_LE, OP_TEST, OP_TESTSET, OP_CALL,
    OP_TAILCALL, OP_RETURN, OP_FORLOOP,
    OP_FORPREP, OP_TFORLOOP, OP_SETLIST,
    OP_CLOSE, OP_CLOSURE, OP_VARARG
} OpCode;

TP-Link Opcode顺序:

typedef enum {
    OP_GETTABLE, OP_GETGLOBAL, OP_SETGLOBAL,
    OP_SETUPVAL, OP_SETTABLE, OP_NEWTABLE,
    OP_SELF, OP_LOADNIL, OP_LOADK, OP_LOADBOOL,
    OP_GETUPVAL, OP_LT, OP_LE, OP_EQ, OP_DIV,
    OP_MUL, OP_SUB, OP_ADD, OP_MOD, OP_POW,
    OP_UNM, OP_NOT, OP_LEN, OP_CONCAT, OP_JMP,
    OP_TEST, OP_TESTSET, OP_MOVE, OP_FORLOOP,
    OP_FORPREP, OP_TFORLOOP, OP_SETLIST,
    OP_CLOSE, OP_CLOSURE, OP_RETURN,
    OP_TAILCALL, OP_VARARG
} OpCode;

3.2 新增常量类型

TP-Link添加了新的常量类型 LUA_TINT (值为9),用于表示整型数据:

case 9:  // LUA_TINT
    setnvalue(o, (lua_Number)LoadInt(S));
    break;

4. 还原实现方法

4.1 使用Python脚本转换

使用Construct库定义Luac文件结构,并编写适配器处理TP-Link的特殊修改。

4.1.1 定义标准Luac结构

Luac = Struct(
    "global_head" / GlobalHead,
    "top_proto" / Proto
)

GlobalHead = Struct(
    "signature" / Const(b"\x1bLua"),
    "version" / Version,
    "format" / Format,
    "endian" / Endian,
    "size_int" / Int8ul,
    "size_size_t" / Int8ul,
    "size_instruction" / Int8ul,
    "size_lua_number" / Int8ul,
    "lua_num_valid" / Byte,
)

4.1.2 Opcode转换适配器

OpCodeMap = [
    6, 5, 7, 8, 9, 10, 11, 3, 1, 2, 4, 
    24, 25, 23, 15, 14, 13, 12, 16, 17, 18, 
    19, 20, 21, 22, 26, 27, 0, 31, 32, 33, 
    34, 35, 36, 28, 30, 29, 37
]

class InstructionAdapter(Adapter):
    def _encode(self, obj, context, path):
        obj.opcode = OpCode.parse(integer2bits(OpCodeMap.index(int(obj.opcode)), 6))
        return obj
    
    def _decode(self, obj, context, path):
        obj.opcode = OpCode.parse(integer2bits(OpCodeMap[int(obj.opcode)], 6))
        return obj

4.1.3 常量类型适配器

class ConstantAdapter(Adapter):
    def _decode(self, obj, context, path):
        if int(obj.data_type) == 9:
            obj.data_type = LuaDatatype.parse(b'\x03')
            obj.data = float(obj.data)
        return obj
    
    def _encode(self, obj, context, path):
        return obj

4.2 转换流程

  1. 解析TP-Link Luac文件
  2. 修改Opcode顺序为标准顺序
  3. 转换特殊常量类型为标准类型
  4. 生成标准Luac文件
  5. 使用标准反编译工具(如unluac)处理
if lua_type == TPLINK_LUA:
    header = lua_tplink.GlobalHead.parse(data)
    lua_tplink.lua_type_define(header)
    h = lua_tplink.Luac.parse(data)
    
    # 32bit系统设置
    lua_ori.lua_type_set(4, 4, 8, 4)
    h.global_head = lua_ori.GlobalHead.parse(
        bytes([0x1B, 0x4C, 0x75, 0x61, 0x51, 0x00, 
               0x01, 0x04, 0x04, 0x04, 0x08, 0x00]))
    
    d = lua_ori.Luac.build(h)
    with open(outfile_path, 'wb') as fp:
        fp.write(d)

5. 总结与建议

5.1 还原Luac的通用方法

  1. 分析目标设备的Lua版本
  2. 逆向分析liblua.so中的加载逻辑
  3. 对比标准Lua与目标设备的差异:
    • 文件头魔改(magic number、格式字段)
    • 结构体字段顺序变化
    • 常量类型修改或新增
    • Opcode顺序或定义变化
  4. 编写转换工具将魔改Luac转为标准格式
  5. 使用标准反编译工具处理

5.2 针对TP-Link的特殊处理

  1. Opcode顺序重映射
  2. 处理新增的LUA_TINT常量类型
  3. 确保文件头字段正确设置(32位小端架构)

5.3 后续处理建议

  1. 使用unluac或luadec反编译标准Luac
  2. 对于复杂逻辑,可结合动态分析
  3. 考虑使用大模型辅助代码还原和理解
还原IoT设备中魔改的Luac文件 - TP-Link Archer C7实例分析 1. 背景介绍 本文详细讲解如何还原IoT设备中经过修改的Lua字节码文件(Luac),以TP-Link Archer C7路由器为例。该设备使用的Lua版本为5.1.4,但对其字节码格式进行了自定义修改,导致标准反编译工具无法直接使用。 2. Lua字节码加载流程分析 2.1 标准Lua加载流程 标准Lua加载执行chunk的流程如下: lua_open 打开Lua环境 luaL_dofile 宏实际调用 luaL_loadfile 和 lua_pcall luaL_loadfile 最终调用 f_parser f_parser 判断文件类型并解析 2.2 解析过程详解 f_parser 函数的工作流程: 2.2.1 luaU_undump 函数 负责解析字节码文件: 2.2.2 文件头结构 标准Luac文件头格式: 2.2.3 LoadFunction 函数 加载函数原型: 3. TP-Link Lua修改点分析 3.1 Opcode重排 TP-Link修改了标准Lua的Opcode顺序: 标准Opcode顺序 : TP-Link Opcode顺序 : 3.2 新增常量类型 TP-Link添加了新的常量类型 LUA_TINT (值为9),用于表示整型数据: 4. 还原实现方法 4.1 使用Python脚本转换 使用Construct库定义Luac文件结构,并编写适配器处理TP-Link的特殊修改。 4.1.1 定义标准Luac结构 4.1.2 Opcode转换适配器 4.1.3 常量类型适配器 4.2 转换流程 解析TP-Link Luac文件 修改Opcode顺序为标准顺序 转换特殊常量类型为标准类型 生成标准Luac文件 使用标准反编译工具(如unluac)处理 5. 总结与建议 5.1 还原Luac的通用方法 分析目标设备的Lua版本 逆向分析liblua.so中的加载逻辑 对比标准Lua与目标设备的差异: 文件头魔改(magic number、格式字段) 结构体字段顺序变化 常量类型修改或新增 Opcode顺序或定义变化 编写转换工具将魔改Luac转为标准格式 使用标准反编译工具处理 5.2 针对TP-Link的特殊处理 Opcode顺序重映射 处理新增的LUA_ TINT常量类型 确保文件头字段正确设置(32位小端架构) 5.3 后续处理建议 使用unluac或luadec反编译标准Luac 对于复杂逻辑,可结合动态分析 考虑使用大模型辅助代码还原和理解