AndLua逆向实战:从混淆字节码到源码还原的完整解析

张开发
2026/4/18 19:57:10 15 分钟阅读

分享文章

AndLua逆向实战:从混淆字节码到源码还原的完整解析
1. AndLua逆向工程入门从加密原理到实战准备第一次接触AndLua逆向时我被那些看似乱码的加密字符串搞得一头雾水。后来才发现这就像玩解谜游戏只要掌握关键线索就能层层突破。AndLua作为Android平台上的Lua实现其加密方式通常包含Base64编码、异或运算和zlib压缩的三重组合拳。这里我分享下自己踩坑后总结的实战经验。要理解加密原理得先看典型的文件结构。加密后的Lua脚本往往以特殊字符开头比如0x1d或0x1c这就像信封上的火漆印章暗示着解密方式。举个例子当文件首字节是0x1d时意味着后续内容经过Base64变形处理——标准Base64解码表被修改过第一个字符需要替换为0x1d才能正确解码。这就像把钥匙藏在门把手下面你得先知道这个约定才能进门。准备工具链是成功的第一步。我的装备清单包括Python环境用于编写解密脚本推荐3.7版本zlib库处理压缩数据Python自带JDK运行Java反编译工具unluac.jar经典Lua反编译器010 Editor十六进制查看器分析文件头神器特别提醒不同版本的AndLua可能采用不同加密方案。有次我遇到一个样本前三个字节是0xEF 0xBB 0xBF这其实是UTF-8 BOM标记直接解密会失败。后来用hex编辑器去掉这些元数据才成功——这种细节往往决定成败。2. 三重加密破解实战Base64/异或/zlib全解析2.1 Base64变形解码技巧标准的Base64解码在Python里用base64.b64decode()就能搞定但AndLua用的往往是魔改版。我曾遇到过一个案例解码表里的和/被替换成其他字符。这时候需要先进行字符替换import base64 modified_b64 buffer.replace(bH, b) # 替换魔改字符 decoded base64.b64decode(modified_b64)更复杂的情况是动态变种——解码表会在运行时生成。有次逆向某商业APP时发现它在so库里动态修改了Base64的索引表。我的解决方案是先用Frida挂钩相关函数dump出内存中的解码表再复现解码逻辑。这就像侦探破案得先找到被藏起来的密码本。2.2 异或运算的破解之道异或加密就像数字世界的变色龙相同的密钥能加密也能解密。AndLua常用两种模式固定密钥整个文件与某个固定值如0xAA异或滚动密钥密钥随位置变化比如前文提到的size-1算法对付第一种很简单key 0xAA decrypted bytes([b ^ key for b in encrypted_data])但遇到滚动密钥就得动点脑筋了。有次我逆向的样本使用如下算法init 0 result [] for byte in encrypted_data: init init ^ byte result.append(init)这种自反式加密需要精确跟踪init值的变化错一位就全乱。建议先用小段数据单步调试验证算法。2.3 zlib压缩数据还原zlib压缩的数据通常以0x78开头但AndLua可能会改头换面。我遇到过最刁钻的情况是先用0x1C标记zlib数据解压前需要替换为0x78解压后再恢复原始标记Python处理示例import zlib fixed_data b\x78 compressed_data[1:] # 修复头 decompressed zlib.decompress(fixed_data)有个坑要注意Windows和Linux下的zlib实现可能有差异。有次在Win10解压成功的文件放到Android设备上就报incorrect header check。后来发现是压缩级别参数不兼容加上wbits15才解决zlib.decompress(fixed_data, wbits15)3. 反编译改造让unluac识别魔改字节码3.1 文件头修复实战正常Lua字节码以0x1B开头但加密文件往往被改得面目全非。我总结的修复流程是用hex编辑器查看前16字节对比标准Lua签名不同版本不同Lua5.1: \x1BLuaLua5.3: \x1BLua\x53手动修复关键字段Java反编译命令也要对应调整# 错误的尝试 java -jar unluac.jar broken.lua # 正确的姿势 java -jar unluac.jar --rawstring fixed.lua output.lua遇到过最棘手的案例是某金融APP的Lua脚本不仅改文件头还修改了字节码的常量表加载方式。最终解决方案是逆向分析其自定义的LoadString函数然后修改unluac的StringType.java// 原版代码 String value new String(bytes, charset); // 修改为解密逻辑 byte[] decrypted xorDecrypt(bytes, key); String value new String(decrypted, charset);3.2 字符串混淆处理技巧解出来的代码常看到这种天书_ENV[u\229\136\006\133\020\186](o\024\148;\177G)这其实是字符串加密元表劫持的组合拳。我的破解步骤在Lua虚拟机初始化时挂钩字符串加载定位到LoadString的魔改逻辑用IDAPRO分析so库找到解密算法编写对应的Python解密函数比如某次发现的算法def decrypt_str(enc): key len(enc) - 1 result [] for i, c in enumerate(enc): result.append(chr(c ^ (key % 255))) key (key (ord(enc[0]) ^ key)) return .join(result)对于顽固的元表保护可以用debug.getmetatable暴力破解local mt debug.getmetatable(_ENV) for k,v in pairs(mt) do print(k,v) end4. 高级技巧动态分析与自动化工具4.1 Frida动态挂钩实战静态分析有时会遇到死胡同这时候Frida动态注入就是神器。比如挂钩Lua的lua_load函数Interceptor.attach(Module.findExportByName(liblua.so, lua_load), { onEnter: function(args) { console.log(Loading chunk with mode:, args[1].readCString()); }, onLeave: function(retval) { if (!retval.isNull()) { console.log(Thread.backtrace(this.context, Backtracer.ACCURATE)); } } });我曾用这个方法发现某游戏在加载脚本前会先校验签名挂钩后直接绕过验证逻辑省去大量逆向时间。4.2 自动化解密脚本开发手动操作容易出错我后来都写成自动化工具。分享个自用的解密框架class AndLuaDecryptor: def __init__(self, filepath): with open(filepath, rb) as f: self.buffer f.read() def detect_encryption(self): if self.buffer[0] 0x1D: return base64 elif self.buffer[0] 0x1C: return xorzlib else: raise ValueError(Unknown encryption) def decrypt(self): method self.detect_encryption() if method base64: self._handle_base64() elif method xorzlib: self._handle_xor_zlib() return self.buffer def _handle_base64(self): # 实现Base64解密逻辑 pass def _handle_xor_zlib(self): # 实现异或zlib解密 pass进阶技巧是集成反编译步骤自动修复文件头后调用unluacdef decompile(self): self.decrypt() with tempfile.NamedTemporaryFile() as tmp: tmp.write(self.buffer) tmp.flush() result subprocess.run( [java, -jar, unluac.jar, tmp.name], capture_outputTrue ) return result.stdout.decode()遇到最难搞的样本是个多层嵌套的外层是Base64中间层是AES密钥藏在assets里内层是zlib最后还有自定义字节码混淆解决方案是用Frida dump内存中的解密函数再结合Capstone引擎分析ARM指令最终还原出完整的解密流程。整个过程就像剥洋葱得一层层耐心处理。

更多文章