样本:d.protected.lua,自定义 Lua VM 字节码保护(文件头自称「神秘人加密」)。五层结构层层套娃,最终静态还原出一个 MQTT 消息队列推送任务,21 个加密页面、100 条指令、38 个常量全部解开。
本文用到的所有解密还原工具已在文章末尾打包提供。
一、保护结构解剖
自顶向下五层:
1. 位运算兼容层:文件头先手写一整套 32 位整数/位运算(mCnMLgHBhb/qTl6QngWEF等混淆名)——目标运行环境没有 bit 库,VM 自带全套 bit32。
2. XOR 字符串:字符串常量是(function(e,k) ... end)(bytes, key)立即调用形态,单字节 XOR。
3. VM 数据表:顶层 Lua 表存 VM 全部参数——常量池、页面映射、密钥材料、模块元数据。
4. 页面加密(核心):字节码切成若干「页面」,每页先Base64 变种(自定义索引表偏移)再PRNG 流密码:
密钥推导: key = (w - tok - fk - pid*97 - bid) % 2147483647 -- 每页密钥不同 PRNG: state = (state * 1103515245 + 12345) & 0x7fffffff -- 经典 glibc LCG 流字节: (state >> 16) & 0xff 与密文 XOR页面还带 Tag 完整性校验,解完再过 codec 还原成标准字节码。
5. 自定义字节码格式:头部魔数HV\x01\x02+ 指令计数;每条指令32 字节 = 8 个 uint32 字段,字段含义经 F 表映射(编码/pc/next/A/B/C/BX/SBX);操作码本身也加密:
oid = (encoded - (fk + pc*131 + pid)) % 2147483647 -- 依赖 pc 和页面 id 再查 OM 表 → 真实 opcode二、还原思路
纯静态,全程不跑样本。两个 Python 脚本打穿五层:
parse_and_decrypt.py (20KB) Lua 表解析 → XOR 字符串 → 逐页推导密钥 → LCG 流解密 → Tag 校验 → codec 解码 → 指令字段按 F 表拆解 → opcode 反推 + OM 表映射 → 反汇编 full_disasm.py 合并所有页面、按 pc 排序输出完整指令流 ↓ d.protected_deobfuscated.lua(人工归纳)解页面的顺序很关键:密钥依赖pid(页号)和bid(模块 id),而页号藏在已解密部分里——所以从外层模块参数表入手,先拿fk/bid/tok0/stride/bias一组常量,再逐页递推。
两套模块参数全部提取:外层(页面 757350772)+ 子模块 535390(21 页 / 100 条指令)。
三、还原结果
剥到最后是个物联网 MQTT 推送任务:
sys.taskInit(function()whiletruedoifnext(msg_queue)==nilthensys.waitUntil('NEW_MQTT_MSG',10000)-- 空队列等消息elseifnotutil_mqtt.isReady()thensys.waitUntil('MQTT_READY',5000)-- 断线等重连elselocalmsg=msg_queue[1]table.remove(msg_queue,1)ifmsg.retry>config.MSG_RETRY_MAXthen-- 超次丢弃+清缓存fskv.del(msg.id)elseifutil_mqtt.publish(...)then-- 推送成功清缓存fskv.del(msg.id)else-- 失败重入队+退避msg.retry=msg.retry+1table.insert(msg_queue,msg)sys.wait(config.MSG_RETRY_INTERVAL)endendsys.wait(100)endend)100 条 VM 指令对应的全部语义,零残留。
四、关键点
- 每页密钥独立(依赖页号/模块号),不能一把梭——必须从外层参数表推出常量再逐页解。
- opcode 依赖 pc:
encoded - (fk + pc*131 + pid),指令换个位置编码就不同,直接静态映射表会错,必须模拟解码流程。 - Tag 校验是最好的调试器:解密对不对,校验值立刻告诉你,不用猜。
五、小结
这是五个样本里结构最工整的一个:加密链路长(Base64→LCG→codec→字段映射→opcode 反推),但每一环都是确定性算法,只要参数表拿到,整条链多米诺式解开。难在找到第一块骨牌——VM 数据表里的那组密钥常量。
工具附件
解密工具包括样本.zip