前言

测试某游戏时,游戏加载的是luac脚本:

  • 文件格式 - 010editor官方bt只能识别luac52版本
  • opcode对照表 - 这个游戏luac的opcode对照表也被重新排序,unluac需要找到lua vm的opcode对照表,才能反编译。

文章目录

  • 前言
  • luac51格式分析
    • Luac文件格式
      • 文件头格式
      • 函数体
    • luac.exe存储luac过程分析
  • luac反编译
    • 获取lua51 vm
    • lua_execute函数
    • 编译unluac
  • luac

luac51格式分析

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、行号、局部变量等信息组成。

函数体

typedef struct {//headerProtoHeader header;//codeCode code;// constantsConstants constants;// functionsProtos protos;// upvalues//Upvaldescs upvaldescs;// string//SourceName src_name;// linesLines lines;// localsLocVars loc_vars;// upvalue namesUpValueNames names;
} Proto;

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

typedef struct {uint32 linedefined;uint32 lastlinedefined;uchar numparams;uchar is_vararg;uchar maxstacksize;
} ProtoHeader;

**code**lua vm操作码
constants lua语句其实只是将标志符合关键字给去掉了

printf("Hello %s from %s on %s\n",os.getenv"USER" or "there",_VERSION,os.date())

这是一条lua语句,那么在luac文件中可见,可以推测出大概的lua语句

���printf����Hello %s from %s on %s
����os����getenv����USER����there�  ���_VERSION����date

**protos**下一个函数体,函数体是链式存储的
**lines**该函数存在多少行lua语句
**loc_vars**函数内部局部变量个数
**names**函数内部局部变量名

luac.exe存储luac过程分析

luac的函数体在luac.exe中分main函数和一般函数,存储过程如下:
1、存储main函数头、代码、lua语句字符串进行存储
2、存储普通函数
2.1、存储普通函数体
2.2、遍历普通函数链表,直至结束
3、存储main函数行数、参数、参数名

#### 具体过程
分析luac.exe存储过程,很容易分析出luac的文件格式。
```cpp
int luaU_dump (lua_State* L, const Proto* f, lua_Writer w, void* data, int strip)
{DumpState D;D.L=L;D.writer=w;D.data=data;D.strip=strip;D.status=0;DumpHeader(&D);DumpFunction(f,NULL,&D);return D.status;
}

lua源码luac.c中,DumpHeader 存储了luac头格式,而在**DumpFunction**则对函数体进行了存储。

static void DumpHeader(DumpState* D)
{char h[LUAC_HEADERSIZE];luaU_header(h);DumpBlock(h,LUAC_HEADERSIZE,D);
}

**DumpHeader**存储了LUAC_HEADERSIZE(12)byte的头格式。

static void DumpConstants(const Proto* f, DumpState* D)
{int i,n=f->sizek;DumpInt(n,D);for (i=0; i<n; i++){const TValue* o=&f->k[i];DumpChar(ttype(o),D);switch (ttype(o)){case LUA_TNIL:break;case LUA_TBOOLEAN:DumpChar(bvalue(o),D);break;case LUA_TNUMBER:DumpNumber(nvalue(o),D);break;case LUA_TSTRING:DumpString(rawtsvalue(o),D);break;default:lua_assert(0);      /* cannot happen */break;}}n=f->sizep;DumpInt(n,D);for (i=0; i<n; i++) DumpFunction(f->p[i],f->source,D);
}static void DumpDebug(const Proto* f, DumpState* D)
{int i,n;n= (D->strip) ? 0 : f->sizelineinfo;DumpVector(f->lineinfo,n,sizeof(int),D);n= (D->strip) ? 0 : f->sizelocvars;DumpInt(n,D);for (i=0; i<n; i++){DumpString(f->locvars[i].varname,D);DumpInt(f->locvars[i].startpc,D);DumpInt(f->locvars[i].endpc,D);}n= (D->strip) ? 0 : f->sizeupvalues;DumpInt(n,D);for (i=0; i<n; i++) DumpString(f->upvalues[i],D);
}static void DumpFunction(const Proto* f, const TString* p, DumpState* D)
{DumpString((f->source==p || D->strip) ? NULL : f->source,D);DumpInt(f->linedefined,D);DumpInt(f->lastlinedefined,D);DumpChar(f->nups,D);DumpChar(f->numparams,D);DumpChar(f->is_vararg,D);DumpChar(f->maxstacksize,D);DumpCode(f,D);DumpConstants(f,D);DumpDebug(f,D);
}

递归遍历存储函数体过程。


luac反编译

获取lua51 vm

  $ git clone https://github.com/mkottman/AndroLua$ ndk-build

lua_execute函数

在lua vm中luac解释执行luac opcode的函数是luaV_execute,但是有些so会将lua_execute函数名隐藏,以下是通过lua_resume找到execute函数。

  LUA_API int lua_resume (lua_State *L, int nargs) {int status;lua_lock(L);if (L->status != LUA_YIELD && (L->status != 0 || L->ci != L->base_ci))return resume_error(L, "cannot resume non-suspended coroutine");if (L->nCcalls >= LUAI_MAXCCALLS)return resume_error(L, "C stack overflow");luai_userstateresume(L, nargs);lua_assert(L->errfunc == 0);L->baseCcalls = ++L->nCcalls;status = luaD_rawrunprotected(L, resume, L->top - nargs);if (status != 0) {  /* error? */L->status = cast_byte(status);  /* mark thread as `dead' */luaD_seterrorobj(L, status, L->top);L->ci->top = L->top;}else {lua_assert(L->nCcalls == L->baseCcalls);status = L->status;}--L->nCcalls;lua_unlock(L);return status;}

lua_resume中调用luaD_rawrunprotected函数,将resum函数指针作为参数传入

  static void resume (lua_State *L, void *ud) {StkId firstArg = cast(StkId, ud);CallInfo *ci = L->ci;if (L->status == 0) {  /* start coroutine? */lua_assert(ci == L->base_ci && firstArg > L->base);if (luaD_precall(L, firstArg - 1, LUA_MULTRET) != PCRLUA)return;}else {  /* resuming from previous yield */lua_assert(L->status == LUA_YIELD);L->status = 0;if (!f_isLua(ci)) {  /* `common' yield? *//* finish interrupted execution of `OP_CALL' */lua_assert(GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_CALL ||GET_OPCODE(*((ci-1)->savedpc - 1)) == OP_TAILCALL);if (luaD_poscall(L, firstArg))  /* complete it... */L->top = L->ci->top;  /* and correct top if not multiple results */}else  /* yielded inside a hook: just continue its execution */L->base = L->ci->base;}luaV_execute(L, cast_int(L->ci - L->base_ci));}

resum函数中调用luaV_execute

void luaV_execute (lua_State *L, int nexeccalls) {...switch (GET_OPCODE(i)) {case OP_MOVE: {setobjs2s(L, ra, RB(i));continue;}case OP_LOADK: {setobj2s(L, ra, KBx(i));continue;}case OP_LOADBOOL: {setbvalue(ra, GETARG_B(i));if (GETARG_C(i)) pc++;  /* skip next instruction (if C) */continue;}...}}
}

luaV_execute中存在38个switch case,然后再该游戏的lua vm中找到execute函数同样也是38个switch case,分析得到opcode对照表:

game orgin0 01 242 c3 d4 15 2 6 3 7 4 8 59 7 a 8b 6c ad be 9f e10 f11 1012 1113 12 14 1315 1416 1517 1618 1719 181a 191b 1a1c 1b1d 1c1e 1d1f 1e20 1f21 2022 2123 2224 2325 25

编译unluac

将分析出来的对照表覆盖到unluac的OpcodeMap.java中进行编译,生成unluac.jar

    } else if(version == 0x51) {map = new Op[38];map[0] = Op.MOVE;map[36] = Op.LOADK;map[12] = Op.LOADBOOL;map[13] = Op.LOADNIL;map[1] = Op.GETUPVAL;map[2] = Op.GETGLOBAL;map[3] = Op.GETTABLE;map[4] = Op.SETGLOBAL;map[7] = Op.SETUPVAL;map[5] = Op.SETTABLE;map[8] = Op.NEWTABLE;map[9] = Op.SELF;map[10] = Op.ADD;map[11] = Op.SUB;map[6] = Op.MUL;map[14] = Op.DIV;map[15] = Op.MOD;map[16] = Op.POW;map[17] = Op.UNM;map[18] = Op.NOT;map[19] = Op.LEN;map[20] = Op.CONCAT;map[21] = Op.JMP;map[22] = Op.EQ;map[23] = Op.LT;map[24] = Op.LE;map[25] = Op.TEST;map[26] = Op.TESTSET;map[27] = Op.CALL;map[28] = Op.TAILCALL;map[29] = Op.RETURN;map[30] = Op.FORLOOP;map[31] = Op.FORPREP;map[32] = Op.TFORLOOP;map[33] = Op.SETLIST;map[34] = Op.CLOSE;map[35] = Op.CLOSURE;map[37] = Op.VARARG; } else if(version == 0x52) {

最后尝试反编译luac文件,成功!!!~~~

java -jar unluac.jar test.lua

010 editor模版:https://github.com/saidyou/unluc.git

luac

Note: 资料参考:

  • **luac反编译工具 **:https://github.com/viruscamp/unluac
  • lua52反编译分析博客 : https://github.com/feicong/lua_re
  • lua51源码 : https://github.com/lua/lua/tree/v5_1

luac 格式分析与反编译相关推荐

  1. Android逆向分析(一) - 反编译看看手Q口令红包的实现原理

    原文:http://blog.zhaiyifan.cn/2016/02/09/android-reverse-1/ 本系列文章是<Android软件安全与逆向分析>的实践笔记(一些工具的版 ...

  2. Android逆向分析(1) 反编译看看手Q口令红包的实现原理

    前言 本篇文章是作者MarkZhai的逆向分析系列的第一篇,已授权发布,并计划之后该系列的更新会第一时间发布在本公号上,敬请关注! 原文 本系列文章是<Android软件安全与逆向分析>的 ...

  3. Objective-C基本分析法 反编译

    以Hello World程序为例: 代码: #import <foundation/foundation.h>@interface SaySomething :NSObject- (voi ...

  4. Android逆向笔记-通过ApkTool源码分析未能反编译APK的原因

    如下错误: 这里可以看到一个IO错误:Expected: 0x001c0001, got: 0x00000001 这里看下源码: 看下还有哪个个地方调用了他 下面来看下这个CHUNK)STRINGPO ...

  5. 【笔记】reko 0.10.2 反编译工具安装和使用记录|(2) 翻译 user‘s guide

    Reko user's guide Reko是一个二进制可执行文件的反编译器.它接受输入的一个或多个二进制可执行文件,然后反编译成高级语言.它可以在GUI shell中被交互地使用,作为一个命令行项目 ...

  6. Android反编译工具与实践

    版权声明 本文原创作者:谷哥的小弟 作者博客地址:http://blog.csdn.net/lfdfhl What is Android Android is a mobile operating s ...

  7. 浅析android手游lua脚本的加密与解密(番外篇之反编译的对抗)

    前言   去年在看雪论坛写了一篇<浅析android手游lua脚本的加密与解密>的精华文章,今年写一篇番外篇,将一些lua反编译对抗的内容整合一起,并以3个实例作为说明(包括2018腾讯游 ...

  8. 『贝壳找房APP』反编译到底有多简单--反编译和调试实践

    最近,在朋友圈看到有人发贝壳找房在Appstore上的下载排行榜,已进入前三.于是,我在应用宝上下载了贝壳release apk. 点击此处下载贝壳找房Apk 点击此处下载反编译debug包 Apk存 ...

  9. .net反编译工具Reflector使用详解

    早就听说Reflector这个强大的类库分析与反编译工具,不过一直没有很好的利用起来.最近使用Reflector解决了一个实际开发问题,现将其总结出来. 需求导入:项目是一个GIS项目,在项目中使用到 ...

  10. python反编译luac_LuaJIT反编译总结

    本文所指luajit,皆指luajit2.1.0-beta2版本. 一.背景 逆向apk时,得到luajit字节码文件,将反编译luajit的过程记录如下. 本文主要分析ljd反编译工具源码(http ...

最新文章

  1. 【ABAP】在线预览文档对象的开发实现
  2. MySQL锁机制和PHP锁机制
  3. 发布 项目_第十八期科创基金项目发布会圆满结束
  4. Oracle数据库的增删改操作介绍
  5. VS Code远程连接矩池云GPU主机
  6. 中国家禽细菌学诊断行业市场供需与战略研究报告
  7. Docker镜像源更改
  8. 实现子元素在父元素中水平垂直都居中笔记
  9. 如何在数据库中创建表
  10. Python pdf 转jpg/png工具
  11. 百度文库免费下载(附:分享一些有趣的网站,最后一个可以免飞下载百度文库)
  12. Oracle的学习视频
  13. OSChina 周二乱弹 —— 她根本就配不上我这么聪明的男人
  14. 源支付聚合免签支付系统
  15. “铜三铁四“来一套程序员内卷超车赛道-音视频开发
  16. 网页数据抓取-网页实时数据抓取软件
  17. 甜椒刷机助手(安卓一键刷机助手) v3.5.1.1 电脑版
  18. 3d渲染服务器系统,3d渲染云服务器
  19. 查看RocketMQ的broker启动部分源码分析总结
  20. 你是不是也,睡不着?

热门文章

  1. LinuxQt打包发布
  2. Ubuntu18.04配置ORB SLAM3
  3. 慕课网EMOS在线办公系统源码笔记1-6章
  4. 配电室智能监控系统设计及实现分析
  5. 【小程序】小游戏开发工具详解(上)
  6. 老男孩 linux 2014 360下载,360安全卫士2014旧版
  7. U8如何设置和调用凭证模板
  8. arping的使用和原理简介
  9. 【超图+CESIUM】【基础API使用示例】47、超图|CESIUM - 平行光源设置
  10. 从Bus Hound开始学USB之U盘