首页
社区
课程
招聘
[原创]lua 逆向学习 & RCTF picstore 还原代码块
发表于: 2022-12-23 14:59 21834

[原创]lua 逆向学习 & RCTF picstore 还原代码块

2022-12-23 14:59
21834

打ctf的时候遇到了lua逆向,浅记一下遇到的知识点和逆向思路

luac命令可以把.lua代码预编译为字节码文件
因为lua程序执行的核心是在JIT虚拟机上运行字节码,所以luac的作用就是把所有字节码打包成一个二进制文件

=>
图片描述

https://sourceforge.net/projects/unluac/
处理lua5.0 - lua5.4

https://github.com/viruscamp/luadec
主要针对lua5.1,对lua5.2和lua5.3是实验性的 , 依赖lua源码

参数介绍

API文档
https://www.runoob.com/manual/lua53doc/manual.html
https://pgl.yoyo.org/luai/i/lua_pushcclosure
https://www.bookstack.cn/read/lua-5.3/spilt.23.spilt.1.5.md

API积累

(这里以读入text类型的代码块为例
图片描述
其中lua_load 函数通过 luaD_protectedparser 保护方式来进行文件读取和语法树解析

luaD_protectedparser 函数内部调用 luaD_pcall 函数
而 luaD_pcall 函数 回调了 f_parser 函数

f_parser 函数中真正做语法树解析的是luaY_parser 函数(如果是text数据的话)

该函数最后执行mainfunc方法,用于执行语法树的解析工作。

mainfunc函数中,有两个函数比较关键。luaX_next:主要用于语法TOKEN的分割,是语法分割器;statlist:主要根据luaX_next分割器分割出来的TOKEN,组装成语法块语句statement,最后将语句逐个组装成语法树
而luaX_next函数内部真正执行Token分割的函数是llex...
...

参考:
https://zhuanlan.zhihu.com/p/429597744
https://blog.csdn.net/initphp/article/details/104428729

正常情况下,luaU_undump 函数调用 checkHeader 函数进行一系列的检查(不只是检查头部魔数)
checkHeader 函数源码如下:

我们从上往下逐个分析

checkliteral(S, LUA_SIGNATURE + 1, "not a");
LUA_SIGNATURE 的宏定义为:

刚好对应luac文件的开头四字节

if (LoadByte(S) != LUAC_VERSION)
LUAC_VERSION 的宏定义如下:

MYINT(s) 宏定义如下

LUA_VERSION_MAJOR 代表了主版本号,LUA_VERSION_MINOR 代表了子版本号
比如,我们的lua版本号为5.3
那么 LUAC_VERSION 就等于5*16+3=83 <=> 0x53
那么luac文件开头的第4个字节应该为 0x53
图片描述

if (LoadByte(S) != LUAC_FORMAT)
lua5.3.3源码给出的LUAC_FORMAT的宏定义为

这个值应该一般都为0
检查地址在开头的第5字节
图片描述

checkliteral(S, LUAC_DATA, "corrupted");
lua5.3.3源码给出的LUAC_DATA的宏定义为

这个值,不同版本应该也不怎么会变
检查地址在开头的第6 ~ 11 字节,检查长度为6字节
图片描述

checksize(S, int);
checksize(S, size_t);
checksize(S, Instruction);
checksize(S, lua_Integer);
checksize(S, lua_Number);
程序用一个巧妙的宏定义完成了检测,#define后面加上一个#号表示将参数字符串化,将其转换成一个字符串常量

图片描述

if (LoadInteger(S) != LUAC_INT)
if (LoadNumber(S) != LUAC_NUM)

LUAC_INT :
图片描述
LUAC_NUM:
图片描述

题目中给了一个lua5.33打包的elf文件和一个被魔改的 luac文件
分析elf文件,其读入的代码块类型为binary,我们直接沿着Lua的文件读取过程寻找哪里被魔改了
luaU_undump
图片描述
查看 luaU_undump 源码
图片描述
图片描述

对比程序的checksize的函数和源码:
图片描述
图片描述

可以发现,程序魔改了LoadByte函数,添加了这样一句判断

经过一番研究,发现程序还魔改了 LoadInterger、LoadNumber、LoadInt等函数,既然有load,肯定有dump
对照源码,不难发现程序同样魔改了DumpByte、DumpInt、DumpNumber、DumpInteger函数

下面,我们有一种通用的思路来处理这道题目--魔改lua源码
如下图:
图片描述
之后重新编译luadec,再进行反编译即可

这里我想介绍的是第二种方法--修复 picstore.bin 代码块
唯一的难题就是: 怎样知道哪些字节被修改了
已知:
(1)、程序load代码块的核心函数有且只有 一个函数 -- LoadBlock
(2)、程序dump代码块的核心函数有且只有 一个函数 --DumpBlock
(3)、这两个函数就像堆栈一样,Load代表 push ,Dump代表 pop
我们只需要记录 LoadBlock 和 DumpBlock 函数的执行次数和函数参数,就可以知道到哪些字节被改变了

我使用gdb_python 脚本进行了记录

图片描述
修复 picstore.bin

反编译

剩下的问题就比较常规了,z3求解即可
exp:

flag即为
flag{U_90t_th3_p1c5t0re_fl49!}

新人第一次发帖,若有不足,欢迎提出

-o : 把所有字节码打包成一个文件,-o 后跟打包成的文件名
-l : 把所有字节码打包成一个文件,默认名为luac.out,并打印出所有字节码文件的信息,如下图
-o : 把所有字节码打包成一个文件,-o 后跟打包成的文件名
-l : 把所有字节码打包成一个文件,默认名为luac.out,并打印出所有字节码文件的信息,如下图
java -jar unluac.jar luac.out > 3.lua
java -jar unluac.jar --rawstring luac.out > 3.lua
java -jar unluac.jar luac.out > 3.lua
java -jar unluac.jar --rawstring luac.out > 3.lua
git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.3   # lua5.2 就替换成 lua-5.2 下面也是一样
cd lua-5.3
make linux
cd ../luadec
make LUAVER=5.3
git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.3   # lua5.2 就替换成 lua-5.2 下面也是一样
cd lua-5.3
make linux
cd ../luadec
make LUAVER=5.3
-pn  : 打印函数嵌套结构
-dis : 反汇编luac.out或lua源码  # 这里的反汇编指的是生成字节码
luadec abc.lua 或 luadec  luac.out :  反编译lua源码或luac二进制文件  #这里的反编译指的是生成lua源码
-pn  : 打印函数嵌套结构
-dis : 反汇编luac.out或lua源码  # 这里的反汇编指的是生成字节码
luadec abc.lua 或 luadec  luac.out :  反编译lua源码或luac二进制文件  #这里的反编译指的是生成lua源码
 
1、lua_pushcclosure
lua_pushcclosure()函数是Lua C API提供注册C函数最基础的。其他注册方式都是在该函数上面拓展的
void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
fn为要注册的函数指针
 
2、luaL_dofile
加载并运行指定的文件。 它是用下列宏定义出来
 (luaL_loadfile(L, filename) || lua_pcall(L, 0, LUA_MULTRET, 0))
 
3、luaL_loadfilex
int luaL_loadfilex (lua_State *L, const char *filename, const char *mode);
把一个文件加载为 Lua 代码块,代码块的名字被命名为 filename , 如果 filename 为 NULL,它从标准输入加载
 
4、lua_load
int lua_load (lua_State *L,lua_Reader reader,void *data,const char *chunkname,const char *mode);
加载一段 Lua 代码块,但不运行它。 如果没有错误, lua_load 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息
参数 reader , 用来读取数据,比如 luaL_loadfilex 内部调用 lua_load 函数,reader 就是getF函数,其通过fread函数读取文件
参数 data , 是指向可选数据结构的指针,可以传递给reader函数。
参数 chunkname是一个字符串,标识了正在加载的块了名字
参数 mode是一个字符串,指定如何编译数据块。可能取值为:"b"(二进制):该块是预编译的二进制块,加载速度比源块快。"t" (text): chunk是一个文本块,在执行前被编译成字节码。"bt" (both):数据块可以是二进制数据块也可以是文本数据块,函数首先尝试将其作为二进制数据块加载,如果加载失败,则尝试将其作为文本数据块加载。
 
5、lua_pushfstring
const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
把一个格式化过的字符串压栈,然后返回这个字符串的指针。 它和 C 函数 sprintf 比较像
 
6、lua_pushstring
const char *lua_pushstring (lua_State *L, const char *s);
将指针 s 指向的零结尾的字符串压栈。
 
7、lua_tolstring
const char *lua_tolstring (lua_State *L, int index, size_t *len);
把给定索引处的 Lua 值转换为一个 C 字符串。 如果 len 不为 NULL , 它还把字符串长度设到 *len 中。 这个 Lua 值必须是一个字符串或是一个数字; 否则返回返回 NULL 。 如果值是一个数字, lua_tolstring 还会 把堆栈中的那个值的实际类型转换为一个字符串。
1、lua_pushcclosure
lua_pushcclosure()函数是Lua C API提供注册C函数最基础的。其他注册方式都是在该函数上面拓展的
void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);
fn为要注册的函数指针
 
2、luaL_dofile
加载并运行指定的文件。 它是用下列宏定义出来
 (luaL_loadfile(L, filename) || lua_pcall(L, 0, LUA_MULTRET, 0))
 
3、luaL_loadfilex
int luaL_loadfilex (lua_State *L, const char *filename, const char *mode);
把一个文件加载为 Lua 代码块,代码块的名字被命名为 filename , 如果 filename 为 NULL,它从标准输入加载
 
4、lua_load
int lua_load (lua_State *L,lua_Reader reader,void *data,const char *chunkname,const char *mode);
加载一段 Lua 代码块,但不运行它。 如果没有错误, lua_load 把一个编译好的代码块作为一个 Lua 函数压到栈顶。 否则,压入错误消息
参数 reader , 用来读取数据,比如 luaL_loadfilex 内部调用 lua_load 函数,reader 就是getF函数,其通过fread函数读取文件
参数 data , 是指向可选数据结构的指针,可以传递给reader函数。
参数 chunkname是一个字符串,标识了正在加载的块了名字
参数 mode是一个字符串,指定如何编译数据块。可能取值为:"b"(二进制):该块是预编译的二进制块,加载速度比源块快。"t" (text): chunk是一个文本块,在执行前被编译成字节码。"bt" (both):数据块可以是二进制数据块也可以是文本数据块,函数首先尝试将其作为二进制数据块加载,如果加载失败,则尝试将其作为文本数据块加载。
 
5、lua_pushfstring
const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
把一个格式化过的字符串压栈,然后返回这个字符串的指针。 它和 C 函数 sprintf 比较像
 
6、lua_pushstring
const char *lua_pushstring (lua_State *L, const char *s);
将指针 s 指向的零结尾的字符串压栈。
 
7、lua_tolstring
const char *lua_tolstring (lua_State *L, int index, size_t *len);
把给定索引处的 Lua 值转换为一个 C 字符串。 如果 len 不为 NULL , 它还把字符串长度设到 *len 中。 这个 Lua 值必须是一个字符串或是一个数字; 否则返回返回 NULL 。 如果值是一个数字, lua_tolstring 还会 把堆栈中的那个值的实际类型转换为一个字符串。
/**
 * 文件解析函数(保护方式调用)
 * 调用:luaD_pcall方法
 */
int luaD_protectedparser (lua_State *L, ZIO *z, const char *name, const char *mode) {
  status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc);
}
/**
 * 文件解析函数(保护方式调用)
 * 调用:luaD_pcall方法
 */
int luaD_protectedparser (lua_State *L, ZIO *z, const char *name, const char *mode) {
  status = luaD_pcall(L, f_parser, &p, savestack(L, L->top), L->errfunc);
}
static void f_parser (lua_State *L, void *ud) {
//通过数据头的signature来判断读取的数据是binary还是text的,如果是binary的数据,
//则调用luaU_undump来读取预编译好的lua chunks,如果是text数据,则调用luaY_parser来parse lua代码     
 
//也就是说,读取text源码文件要多了一步paser工作
    if (c == LUA_SIGNATURE[0]) {
      checkmode(L, p->mode, "binary");
      cl = luaU_undump(L, p->z, &p->buff, p->name);
    }
    else {
      checkmode(L, p->mode, "text");
      cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
    }
}
static void f_parser (lua_State *L, void *ud) {
//通过数据头的signature来判断读取的数据是binary还是text的,如果是binary的数据,
//则调用luaU_undump来读取预编译好的lua chunks,如果是text数据,则调用luaY_parser来parse lua代码     
 
//也就是说,读取text源码文件要多了一步paser工作
    if (c == LUA_SIGNATURE[0]) {
      checkmode(L, p->mode, "binary");
      cl = luaU_undump(L, p->z, &p->buff, p->name);
    }
    else {
      checkmode(L, p->mode, "text");
      cl = luaY_parser(L, p->z, &p->buff, &p->dyd, p->name, c);
    }
}
LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, Dyndata *dyd, const char *name, int firstchar) {
  LexState lexstate;
  FuncState funcstate;
 
  mainfunc(&lexstate, &funcstate);
}
LClosure *luaY_parser (lua_State *L, ZIO *z, Mbuffer *buff, Dyndata *dyd, const char *name, int firstchar) {
  LexState lexstate;
  FuncState funcstate;
 
  mainfunc(&lexstate, &funcstate);
}
static void mainfunc (LexState *ls, FuncState *fs) {
  luaX_next(ls);  /* 读取第一个token read first token  */
  statlist(ls);  /* 语法树遍历解析 parse main body */
}
static void mainfunc (LexState *ls, FuncState *fs) {
  luaX_next(ls);  /* 读取第一个token read first token  */
  statlist(ls);  /* 语法树遍历解析 parse main body */
}
 
LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
  LoadState S;
  LClosure *cl;
  if (*name == '@' || *name == '=')    // 加载的 chunkname
    S.name = name + 1;
  else if (*name == LUA_SIGNATURE[0])
    S.name = "binary string";
  else
    S.name = name;
  S.L = L;
  S.Z = Z;
  checkHeader(&S);
  cl = luaF_newLclosure(L, LoadByte(&S));
  setclLvalue(L, L->top, cl);
  luaD_inctop(L);
  cl->p = luaF_newproto(L);
  LoadFunction(&S, cl->p, NULL);
  lua_assert(cl->nupvalues == cl->p->sizeupvalues);
  luai_verifycode(L, buff, cl->p);
  return cl;
}
LClosure *luaU_undump(lua_State *L, ZIO *Z, const char *name) {
  LoadState S;
  LClosure *cl;
  if (*name == '@' || *name == '=')    // 加载的 chunkname
    S.name = name + 1;
  else if (*name == LUA_SIGNATURE[0])
    S.name = "binary string";
  else
    S.name = name;
  S.L = L;
  S.Z = Z;
  checkHeader(&S);
  cl = luaF_newLclosure(L, LoadByte(&S));
  setclLvalue(L, L->top, cl);
  luaD_inctop(L);
  cl->p = luaF_newproto(L);
  LoadFunction(&S, cl->p, NULL);
  lua_assert(cl->nupvalues == cl->p->sizeupvalues);
  luai_verifycode(L, buff, cl->p);
  return cl;
}
static void checkHeader (LoadState *S) {
  checkliteral(S, LUA_SIGNATURE + 1, "not a");  /* 1st char already checked */
  if (LoadByte(S) != LUAC_VERSION)
    error(S, "version mismatch in");
  if (LoadByte(S) != LUAC_FORMAT)
    error(S, "format mismatch in");
  checkliteral(S, LUAC_DATA, "corrupted");
  checksize(S, int);
  checksize(S, size_t);
  checksize(S, Instruction);
  checksize(S, lua_Integer);
  checksize(S, lua_Number);
  if (LoadInteger(S) != LUAC_INT)
    error(S, "endianness mismatch in");
  if (LoadNumber(S) != LUAC_NUM)
    error(S, "float format mismatch in");
}
static void checkHeader (LoadState *S) {
  checkliteral(S, LUA_SIGNATURE + 1, "not a");  /* 1st char already checked */
  if (LoadByte(S) != LUAC_VERSION)
    error(S, "version mismatch in");
  if (LoadByte(S) != LUAC_FORMAT)
    error(S, "format mismatch in");
  checkliteral(S, LUAC_DATA, "corrupted");
  checksize(S, int);
  checksize(S, size_t);
  checksize(S, Instruction);
  checksize(S, lua_Integer);
  checksize(S, lua_Number);
  if (LoadInteger(S) != LUAC_INT)
    error(S, "endianness mismatch in");
  if (LoadNumber(S) != LUAC_NUM)
    error(S, "float format mismatch in");
}
#define LUA_SIGNATURE    "\x1bLua"
#define LUA_SIGNATURE    "\x1bLua"
#define LUAC_VERSION    (MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR))
#define LUAC_VERSION    (MYINT(LUA_VERSION_MAJOR)*16+MYINT(LUA_VERSION_MINOR))
#define MYINT(s)    (s[0]-'0')
#define MYINT(s)    (s[0]-'0')
#define LUAC_FORMAT    0    /* this is the official format */
#define LUAC_FORMAT    0    /* this is the official format */
#define LUAC_DATA    "\x19\x93\r\n\x1a\n"
#define LUAC_DATA    "\x19\x93\r\n\x1a\n"
#define checksize(S,t)    fchecksize(S,sizeof(t),#t)
 
static void fchecksize (LoadState *S, size_t size, const char *tname) {
  if (LoadByte(S) != size)
    error(S, luaO_pushfstring(S->L, "%s size mismatch in", tname));
}
#define checksize(S,t)    fchecksize(S,sizeof(t),#t)
 
static void fchecksize (LoadState *S, size_t size, const char *tname) {
  if (LoadByte(S) != size)
    error(S, luaO_pushfstring(S->L, "%s size mismatch in", tname));
}
#define LUAC_INT    0x5678
#define LUAC_NUM    cast_num(370.5)
#define cast_num(i)    cast(lua_Number, (i))
#define cast(t, exp)    ((t)(exp))
#define LUAC_INT    0x5678
#define LUAC_NUM    cast_num(370.5)
#define cast_num(i)    cast(lua_Number, (i))
#define cast(t, exp)    ((t)(exp))
 
 

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2022-12-23 15:03 被tlsn编辑 ,原因:
上传的附件:
收藏
免费 8
支持
分享
最新回复 (7)
雪    币: 55
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
大佬可否留个Q联系
2022-12-24 20:56
0
雪    币: 221
活跃值: (1085)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
mb_aicyioze 大佬可否留个Q联系
q: 2723682041
2022-12-24 23:01
0
雪    币: 6011
活跃值: (5425)
能力值: ( LV5,RANK:65 )
在线值:
发帖
回帖
粉丝
4
感谢分享,很6
2022-12-25 09:34
0
雪    币: 16
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
这种luaUdump以及更上层面的修改lua虚拟机意义不大,可以不需要分析虚拟机就能解的……
2023-1-7 13:58
0
雪    币: 1781
活跃值: (3955)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
mark
2023-1-8 15:54
0
雪    币: 638
活跃值: (6477)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
mark
2023-1-8 19:42
0
雪    币: 230
活跃值: (162)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
8
mark 
2024-4-2 10:15
0
游客
登录 | 注册 方可回帖
返回
//