还原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的流程如下:
lua_open打开Lua环境luaL_dofile宏实际调用luaL_loadfile和lua_pcallluaL_loadfile最终调用f_parserf_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 转换流程
- 解析TP-Link Luac文件
- 修改Opcode顺序为标准顺序
- 转换特殊常量类型为标准类型
- 生成标准Luac文件
- 使用标准反编译工具(如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的通用方法
- 分析目标设备的Lua版本
- 逆向分析liblua.so中的加载逻辑
- 对比标准Lua与目标设备的差异:
- 文件头魔改(magic number、格式字段)
- 结构体字段顺序变化
- 常量类型修改或新增
- Opcode顺序或定义变化
- 编写转换工具将魔改Luac转为标准格式
- 使用标准反编译工具处理
5.2 针对TP-Link的特殊处理
- Opcode顺序重映射
- 处理新增的LUA_TINT常量类型
- 确保文件头字段正确设置(32位小端架构)
5.3 后续处理建议
- 使用unluac或luadec反编译标准Luac
- 对于复杂逻辑,可结合动态分析
- 考虑使用大模型辅助代码还原和理解