AICon全球人工智能与机器学习技术大会8折特惠,购票立减¥960! 了解详情
写点什么

Lua 程序逆向之 Luac 文件格式分析(上)

2019 年 11 月 29 日

Lua程序逆向之Luac文件格式分析(上)

简介

Lua 语言对于游戏开发与相关逆向分析的人来说并不陌生。Lua 语言凭借其高效、简洁与跨平台等多种特性,一直稳立于游戏、移动 APP 等特定的开发领域中。


目前 Lua 主要有 5.1、5.2、5.3 共三个版本。5.1 版本的 Lua 之所以目前仍然被广泛使用的原因之一,是由于另一个流行的项目 LuaJit 采用了该版本 Lua 的内核。单纯使用 Lua 来实现的项目中,5.2 与 5.3 版本的 Lua 则更加流行。这里主要以 Lua 版本 5.2 为例,通过分析它生成的 Luac 字节码文件,完成 Lua 程序的初步分析,为以后更深入的反汇编、字节码置换与重组等技能打下基础。


Lua 与 Luac

Lua 与 Python 一样,可以被定义为脚本型的语言,与 Python 生成 pyc 字节码一样,Lua 程序也有自己的字节码格式 luac。Lua 程序在加载到内存中后,Lua 虚拟机环境会将其编译为 Luac(下面文中 Luac 与 luac 含义相同)字节码,因此,加载本地的 Luac 字节码与 Lua 源程序一样,在内存中都是编译好的二进制结构。


为了探究 Luac 的内幕,我们需要找到合适的资料与工具来辅助分析 Luac 文件。最好的资料莫过于 Lua 的源码,它包含了 Lua 相关知识的方方面面,阅读并理解 Luac 的构造与 Lua 虚拟机加载字节码的过程,便可以通透的了解 Luac 的格式。但这里并不打算这么做,而采取阅读第三方 Lua 反编译工具的代码。主要原因是:这类工具的代码往往更具有针对性,代码量也会少很多,分析与还原理解 Luac 字节码文件格式可以省掉不少的时间与精力。


luadec 与 unlua 是最流行的 Luac 反汇编与反编译工具,前者使用 C++语言开发,后者使用 Java 语言,这两个工具都能很好的还原与解释 Luac 文件,但考虑到 Lua 本身采用 C 语言开发,并且接下来打算编写 010 Editor 编辑器的 Luac.bt 文件格式模板,010 Editor 的模板语法类似于 C 语言,为了在编码时更加顺利,这里分析时主要针对 luadec。


Luac 文件格式

一个 Luac 文件包含两部分:文件头与函数体。文件头格式定义如下:


typedef struct {    char signature[4];   //".lua"    uchar version;    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;
复制代码


第一个字段 signature 在 lua.h 头文件中有定义,它是 LUA_SIGNATURE,取值为“\033Lua",其中,\033 表示按键。LUA_SIGNATURE 作为 Luac 文件开头的 4 字节,它是 Luac 的 Magic Number,用来标识它为 Luac 字节码文件。Magic Number 在各种二进制文件格式中比较常见,通过是特定文件的前几个字节,用来表示一种特定的文件格式。


version 字段表示 Luac 文件的格式版本,它的值对应于 Lua 编译的版本,对于 5.2 版本的 Lua 生成的 Luac 文件,它的值为 0x52。


format 字段是文件的格式标识,取值 0 代表 official,表示它是官方定义的文件格式。这个字段的值不为 0,表示这是一份经过修改的 Luac 文件格式,可能无法被官方的 Lua 虚拟机正常加载。


endian 表示 Luac 使用的字节序。现在主流的计算机的字节序主要有小端序 LittleEndian 与大端序 BigEndian。这个字段的取值为 1 的话表示为 LittleEndian,为 0 则表示使用 BigEndian。


size_int 字段表示 int 类型所占的字节大小。size_size_t 字段表示 size_t 类型所占的字节大小。这两个字段的存在,是为了兼容各种 PC 机与移动设备的处理器,以及它们的 32 位与 64 位版本,因为在特定的处理器上,这两个数据类型所占的字节大小是不同的。


size_Instruction 字段表示 Luac 字节码的代码块中,一条指令的大小。目前,指令 Instruction 所占用的大小为固定的 4 字节,也就表示 Luac 使用等长的指令格式,这显然为存储与反编译 Luac 指令带来了便利。


size_lua_Number 字段标识 lua_Number 类型的数据大小。lua_Number 表示 Lua 中的 Number 类型,它可以存放整型与浮点型。在 Lua 代码中,它使用 LUA_NUMBER 表示,它的大小取值大小取决于 Lua 中使用的浮点数据类型与大小,对于单精度浮点来说,LUA_NUMBER 被定义为 float,即 32 位大小,对于双精度浮点来说,它被定义为 double,表示 64 位长度。目前,在 macOS 系统上编译的 Lua,它的大小为 64 位长度。


lua_num_valid 字段通常为 0,用来确定 lua_Number 类型能否正常的工作。


luac_tail 字段用来捕捉转换错误的数据。在 Lua 中它使用 LUAC_TAIL 表示,这是一段固定的字符串内容:"\x19\x93\r\n\x1a\n"。


在文件头后面,紧接着的是函数体部分。一个 Luac 文件中,位于最上面的是一个顶层的函数体,函数体中可以包含多个子函数,子函数可以是嵌套函数、也可以是闭包,它们由常量、代码指令、Upvalue、行号、局部变量等信息组成。


在 Lua 中,函数体使用 Proto 结构体表示,它的声明如下:


typedef struct {    //header    ProtoHeader header;    //code    Code code;    // constants    Constants constants;    // functions    Protos protos;    // upvalues    Upvaldescs upvaldescs;    // string    SourceName src_name;    // lines    Lines lines;         // locals    LocVars loc_vars;         // upvalue names    UpValueNames names;} Proto;
复制代码


ProtoHeader 是 Proto 的头部分。它的定义如下:


typedef struct {    uint32 linedefined;    uint32 lastlinedefined;    uchar numparams;    uchar is_vararg;    uchar maxstacksize;} ProtoHeader;
复制代码


ProtoHeader 在 Lua 中使用 lua_Debug 表示,lua_Debug 的作用是调试时提供函数的行号,函数与变量名等信息,只是它部分字段的信息在生成 Luac 字节码时,最终没有写入 Luac 文件中。linedefined 与 lastlinedefined 是定义的两个行信息。numparams 表示函数有几个参数。is_vararg 表示参数是否为可变参数列表,例如这个函数声明:


function f1(a1, a2, ...)    ......end
复制代码


这点与 C 语言类似,三个点“…”表示这是一个可变参数的函数。f1()在这里的 numparams 为 2,并且 is_vararg 的值为 1。


maxstacksize 字段指明当前函数的 Lua 栈大小。值为 2 的幂。


在 ProtoHeader 下面是函数的代码部分,这里使用 Code 表示。Code 存放了一条条的 Luac 机器指令,每条指令是一个 32 位的整型大小。Code 定义如下:


struct Code {    uint32 sizecode;    uint32 inst[];} code;
复制代码


sizecode 字段标识了接下来的指令条数。inst 则存放了当前函数所有的指令,在 Lua 中,指令采用 Instruction 表示,它的定义如下:


#define LUAI_UINT32unsigned inttypedef LUAI_UINT32 lu_int32;typedef lu_int32 Instruction;
复制代码


当 LUAI_BITSINT 定义的长度大于等于 32 时,LUAI_UINT32 被定义为 unsigned int,否则定义为 unsigned long,本质上,也就是要求 lu_int32 的长度为 32 位。


接下来是 Constants,它存放了函数中所有的常量信息。定义如下:


typedef struct {    uint32 sizek;    Constant constant[];} Constants;
复制代码


sizek 字段标识了接下来 Constant 的个数。constant 则是 Constant 常量列表,存放了一个个的常量信息。的定义如下:


typedef struct {    LUA_DATATYPE const_type;    TValue val;} Constant;
复制代码


LUA_DATATYPE 是 Lua 支持的各种数据类型结构。如 LUA_TBOOLEAN 表示 bool 类型,使用 lua_Val 表示;LUA_TNUMBER 表示数值型,它可以是整型,使用 lua_Integer 表示,也可以是浮点型,使用 lua_Number 表示;LUA_TSTRING 表示字符串。这些所有的类型信息使用 const_type 字段表示,大小为 1 字节。


TValue 用于存放具体的数据内容。它的定义如下:


typedef struct {    union Value {        //GCObject *gc;     /* collectable objects */        //void *p;          /* light userdata */        lua_Val val;        /* booleans */        //lua_CFunction f;  /* light C functions */        lua_Integer i;      /* integer numbers */        lua_Number n;       /* float numbers */    } value_;} TValue;
复制代码


对于 LUA_TBOOLEAN,它存放的值可以通过 Lua 中提供的宏 bvalue 来计算它的值。


对于 LUA_TNUMBER,它存放的可能是整型,也可能是浮点型,可以直接通过 nvalue 宏自动进行类型判断,然后获取它格式化后的字符串值。对于 Lua 的 5.3 版本,对 nvalue 宏进行了改进,可以使用 ivalue 宏获取它的整型值,使用 fltvalue 宏来获取它的浮点值。


对于 LUA_TSTRING,它存放的是字符串信息。可以使用 rawtsvalue 宏获取它的字符串信息。而写入 Luac 之后,这里的信息实则是 64 位的值存放了字符串的大小,并且紧跟着后面是字符串的内容。


接下来是 Protos,它表示当前函数包含的子函数信息。定义如下:


typedef struct(string level) {    uint32 sizep;    Proto proto[];} Protos
复制代码


sizep 字段表示当前函数包含的子函数的数目。所谓子函数,指的是一个函数中包含的嵌套函数与闭包。如下面的代码:


function Create(n)     local function foo1()         print(n)     end    local function foo2()         n = n + 10     end    return foo1,foo2end
复制代码


Create()函数包含了 foo1()与 foo2()两个子函数,因此,这里 sizep 的值为 2。proto 表示子函数信息,它与父函数使用一样的结构体信息。因此,可见 Lua 的函数部分使用了一种树式的数据结构进行数据存储。


Upvaldescs 与 UpValueNames 共同描述了 Lua 中的 UpValue 信息。当函数中包含子函数或团包,并且访问了函数的参数或局部变量时,就会产生 UpValue。如上面的 Create()函数,foo1()与 foo2()两个子函数都访问了参数 n,因此,这里会产生一个 UpValue,它的名称为“n”。


Upvaldesc 的定义如下:


typedef struct {    uchar instack;    uchar idx;} Upvaldesc;
复制代码


instack 字段表示 UpValue 是否在栈上创建的,是的话取值为 1,反之为 0。idx 字段表示 UpValue 在 UpValue 数据列表中的索引,取值从 0 开始。


UpValueNames 存放了当前函数中所有 UpValue 的名称信息,它的定义如下:


typedef struct {    uint32 size_upvalue_names;    UpValueName upvalue_name[];} UpValueNames;
复制代码


size_upvalue_names 字段表示 UpValueName 条目的数目,每一条 UpValueName 存放了一个 UpValue 的名称,它的定义如下:


typedef struct {    uint64 name_size;    char var_str[];} UpValueName;
复制代码


name_size 字段是符号串的长度,var_str 为具体的字符串内容。


SourceName 存放了当前 Luac 编译前存放的完整文件名路径。它的定义如下:


typedef struct {    uint64 src_string_size;    char str[];} SourceName
复制代码


SourceName 的定义与 UpValueName 一样,两个字段分别存放了字符串的长度与内容。


Lines 存放了所有的行号信息。它的定义如下:


typedef struct {    uint32 sizelineinfo;    uint32 line[];} Lines;
复制代码


sizelineinfo 字段表示当前函数所有的行总数目。line 字段存放了具体的行号。


LocVars 存放了当前函数所有的局部变量信息,它的定义如下:


typedef struct {    uint32 sizelocvars;    LocVar local_var[];} LocVars;
复制代码


sizelocvars 字段表示局部变量的个数。local_var 字段是一个个的局部变量,它的类型 LocVar 定义如下:


typedef struct {    uint64 varname_size;    char varname[];    uint32 startpc;    uint32 endpc;} LocVar;
复制代码


varname_size 字段是变量的名称长度大小。varname 字段存放了变量的名称字符串内容。startpc 与 endpc 是两个指针指,存储了局部变量的作用域信息,即它的起始与结束的地方。


到此,一个 Luac 的文件格式就讲完了。


010 Editor 模板语法

为了方便分析与修改 Luac 二进制文件,有时候使用 010 Editor 编辑器配合它的文件模板,可以达到很直观的查看与修改效果,但 010 Editor 官方并没有提供 Luac 的格式模板,因此,决定自己动手编写一个模板文件。


010 Editor 支持模板与脚本功能,两者使用的语法与 C 语言几乎一样,只是有着细微的差别与限制,我们看看如何编写 010 Editor 模板文件。


2019 年 11 月 29 日 15:03833

评论

发布
暂无评论
发现更多内容

脱不花:怎样成为高效学习的人 学习笔记

魔曦

Spring中@Import的作用

张健

实践为主,理论够用!腾讯高工手码MySQL高阶宝典震撼开源

程序员小毕

Java MySQL 架构 性能优化 性能调优

求职阿里Java 技术岗位的经历,三轮技术面+HR面,面试也不过如此

Java架构之路

Java 程序员 架构 面试 编程语言

sync.singleflight 到底怎么用才对?

cyningsun

golang 并发 Concurrency singleflight Cache Miss

量化自动交易系统开发,量化炒币

薇電13242772558

数字货币 策略模式

华为云张昆:支持全场景全业务,GaussDB加速企业数字化转型

华为云开发者社区

数据库

波场链智能合约软件开发|波场链智能合约APP系统开发

开發I852946OIIO

系统开发

WireMock 使用

hungxy

测试 WireMock

Alluxio Day 2021 线上直播

小小的一朵云

大数据

testing

对于我们程序员来说,基本面是什么呢?

Java架构师迁哥

SpringCloud 从入门到精通 09--- 支付服务集群

Felix

毕业三年,从小公司到大厂,先后四面阿里、小米、美团等,终于收到offer!

Java架构之路

Java 程序员 架构 面试 编程语言

Mobileye的创新科技与方案将助力自动驾驶汽车畅行世界、惠及大众

新闻科技资讯

SpringCloud 从入门到精通 08--- Eureka集群

Felix

从根上理解高性能、高并发(四):深入操作系统,彻底理解同步与异步

JackJiang

网络编程 高并发 高性能 即时通讯

阿里开发7年大牛:闭关60天学懂NDK+Flutter,大厂面试题汇总

欢喜学安卓

android 程序员 面试 移动开发

安卓开发详解!Flutter全方位深入探索,吊打面试官系列!

欢喜学安卓

android 程序员 面试 移动开发

LeetCode题解:236. 二叉树的最近公共祖先,存储父节点,JavaScript,详细注释

Lee Chen

算法 LeetCode 前端进阶训练营

连续三年蝉联第一,Flink 荣膺全球最活跃的 Apache 开源项目

Apache Flink

Apache flink

Dubbo 版 Swagger 来啦!Dubbo-Api-Docs 发布

阿里巴巴云原生

Java 云原生 前端 dubbo 中间件

真是太刺激了!美团CTO五轮面试,Java岗高级工程师一二三四五面面经(已拿到offer)

Java架构之路

Java 程序员 架构 面试 编程语言

week8-homework

J

【设计模式】断路器模式

soolaugust

设计模式 28天写作

即构推出低延迟直播产品L3,可将直播延迟降到1s

ZEGO即构

第九周作业

dll

程序员的五年:双非学历,两年进入苏宁,五年跳槽到阿里,建议收藏!

996小迁

Java 架构 面试 JVM Spring全家桶

面向对象之魔术方法· 第1篇《__init__方法,__new__方法》

清菡

测试

区块链即时通讯系统开发方案,IM聊天社交软件开发

v16629866266

用技术的方式,在UI设计稿中设置随机码,保证高清

行者AI

Python

Lua程序逆向之Luac文件格式分析(上)-InfoQ