lua语言特性及用途

Lua是一个小巧的脚本语言,其设计目的是为了通过灵活嵌入应用程序中从而为应用程序提供灵活的扩展和定制功能。Lua由标准C编写而成,几乎在所有操作系统和平台上都可以编译,运行。Lua并没有提供强大的库,这是由它的定位决定的。

Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,ini等文件格式,并且更容易理解和维护。

运行可以通过 Lua 的交互模式,也可以用记事本编辑代码保存为 .lua 的格式,通过 Lua 编译器运行。也可以通过第三方工具,将 Lua 打包独立运行。

特性

轻量级: Lua语言的官方版本只包括一个精简的核心和最基本的库。这使得Lua体积小、启动速度快。

源码行数对比表

语言 行数
python所有c源码 54万行
python核心c源码 约17万行
lua所有c源码 约2.4万行

可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。

用途

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。大多数游戏和一些应用都是用lua嵌入其他语言中使用。

  1. 游戏开发
  2. 独立应用脚本
  3. Web 应用脚本
  4. 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  5. 安全系统,如入侵检测系统

lua基本语法

安装

curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
tar zxf lua-5.3.0.tar.gz
cd lua-5.3.0
make linux test
make install

基本语法

交互式编程模式: lua -i 或 lua

➜  docker lua -i
Lua 5.1.5  Copyright (C) 1994-2012 Lua.org, PUC-Rio
> 

脚本式编程: 类似于python脚本。lua test.lua或 加#!/usr/local/bin/lua直接运行test.lua

注释: 单行-- 、多行--[[ text --]]

标识符和关键词:
最好不要使用下划线+大写字母、不允许使用@ $ %定义标识符,区分大小写。

and break do else
elseif end false for
function if in local
nil not or repeat
return then true until
while goto

lua数据类型:

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

局部变量: local b = 5,全局不需要

函数:
格式:function … end,可多返回值,变参...

function foo()
    c = 5
    return c
end

select(‘#’, …) 返回可变参数的长度。
select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表

循环:

while(condition)
do
   statements
end

for var=exp1,exp2,exp3 do  
    <执行体>  
end  
-- var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 "执行体"。exp3 是可选的,如果不指定,默认为1。

--泛型for循环
--打印数组a的所有值  
a = {"one", "two", "three"}
for i, v in ipairs(a) do
    print(i, v)
end 
-- i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。

repeat
   statements
until( condition )

各个循环可嵌套。
pairs 和 ipairs异同
同:都是能遍历集合(表、数组)

异:ipairs 仅仅遍历值,按照索引升序遍历,索引中断停止遍历。即不能返回 nil,只能返回数字 0,如果遇到 nil 则退出。它只能遍历到集合中出现的第一个不是整数的 key。

pairs 能遍历集合的所有元素。即 pairs 可以遍历集合中所有的 key,并且除了迭代器本身以及遍历表本身还可以返回 nil。

流程控制:
Lua认为false和nil为假,true和非nil为真。
要注意的是Lua中 0 为 true

--[ 0 为 true ]
if(0)
then
    print("0 为 true")
end

运算符:
算术运算符:

操作符 描述 实例
+ 加法 A + B 输出结果 30
- 减法 A - B 输出结果 -10
* 乘法 A * B 输出结果 200
/ 除法 B / A 输出结果 2
% 取余 B % A 输出结果 0
^ 乘幂 A^2 输出结果 100
- 负号 -A 输出结果 -10

逻辑运算符:

操作符 描述 实例
== 等于,检测两个值是否相等,相等返回 true,否则返回 false (A == B) 为 false。
~= 不等于,检测两个值是否相等,不相等返回 true,否则返回 false (A ~= B) 为 true。
> 大于,如果左边的值大于右边的值,返回 true,否则返回 false (A > B) 为 false。
< 小于,如果左边的值大于右边的值,返回 false,否则返回 true (A < B) 为 true。
>= 大于等于,如果左边的值大于等于右边的值,返回 true,否则返回 false (A >= B) 返回 false。
<= 小于等于, 如果左边的值小于等于右边的值,返回 true,否则返回 false (A <= B) 返回 true。

逻辑运算符:

操作符 描述 实例
and 逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。 (A and B) 为 false。
or 逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。 (A or B) 为 true。
not 逻辑非操作符。与逻辑运算结果相反,如果条件为 true,逻辑非为 false。 not(A and B) 为 true。

lua5.1没有异或等位算数运算符。后面到5.3支持位运算,可以在opcodes中看到BXOR等操作指令。

其他运算符:

操作符 描述 实例
连接两个字符串 a…b ,其中 a 为 "Hello " , b 为 “World”, 输出结果为 “Hello World”。
# 一元运算符,返回字符串或表的长度。 #“Hello” 返回 5

运算符优先级:
由高到低:

^
not    - (unary)
*      /
+      -
..
<      >      <=     >=     ~=     ==
and
or

模块与包:
模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

以下为创建自定义模块 module.lua,文件代码格式如下:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
 
-- 定义一个常量
module.constant = "这是一个常量"
 
-- 定义一个函数
function module.func1()
    io.write("这是一个公有函数!\n")
end
 
local function func2()
    print("这是一个私有函数!")
end
 
function module.func3()
    func2()
end
 
return module

使用require加载模块:

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
-- 别名变量 m
-- local m = require("module")
print(module.constant)
module.func3()
--[[
    result:
        这是一个常量
        这是一个私有函数!
--[[

lua文件io:

简单模式(simple model):拥有一个当前输入文件和一个当前输出文件,并且提供针对这些文件相关的操作。
完全模式(complete model): 使用外部的文件句柄来实现。它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法
打开文件: file = io.open(filename,mode)
mode有:

模式 描述
r 以只读方式打开文件,该文件必须存在。
w 打开只写文件,若文件存在则文件长度清为0,即该文件内容会消失。若文件不存在则建立该文件。
a 以附加的方式打开只写文件。若文件不存在,则会建立该文件,如果文件存在,写入的数据会被加到文件尾,即文件原先的内容会被保留。(EOF符保留)
r+ 以可读写方式打开文件,该文件必须存在。
w+ 打开可读写文件,若文件存在则文件长度清为零,即该文件内容会消失。若文件不存在则建立该文件。
a+ 与a类似,但此文件可读可写
b 二进制模式,如果文件是二进制文件,可以加上b
+ 号表示对文件既可以读也可以写

简单模式:

-- 以只读方式打开文件
file = io.open("test.lua", "r")

-- 设置默认输入文件为 test.lua
io.input(file)

-- 输出文件第一行
print(io.read())

-- 关闭打开的文件
io.close(file)

-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")

-- 设置默认输出文件为 test.lua
io.output(file)

-- 在文件最后一行添加 Lua 注释
io.write("--  test.lua 文件末尾注释")

-- 关闭打开的文件
io.close(file)

io.read参数:

模式 描述
*n 读取一个数字并返回它。例:file.read(“*n”)
*a 从当前位置读取整个文件。例:file.read(“*a”)
*l (默认) 读取下一行,在文件尾 (EOF) 处返回 nil。例:file.read(“*l”)
number 返回一个指定字符个数的字符串,或在 EOF 时返回 nil。例:file.read(5)
io.write函数:
函数原型为io.write(…)。该函数将所有参数顺序的写入到当前输出文件中。如:
    io.write("hello","world") --写出的内容为helloworld

完全模式:

-- 以只读方式打开文件
file = io.open("test.lua", "r")

-- 输出文件第一行
print(file:read())

-- 关闭打开的文件
file:close()

-- 以附加的方式打开只写文件
file = io.open("test.lua", "a")

-- 在文件最后一行添加 Lua 注释
file:write("--test")

-- 关闭打开的文件
file:close()

lua、c互调例子,lua调用so库例子

lua栈:
lua中的栈是一个很奇特的数据结构,普通的栈只有一排索引,但是在lua中有两排索引,正数1索引的位置在栈底,负数-1索引的位置在栈顶。如下图所示。

             _______________
           5 |____data5____| -1
           4 |____data4____| -2
           3 |____data3____| -3 
           2 |____data2____| -4
           1 |____data1____| -5
  • 当索引是1的时候对应的是栈底
  • 当索引是-1的时候对于的是栈顶。
             _______________
           5 |____.....____| -1
           4 |____"str"____| -2
           3 |____"343"____| -3 
           2 |___"table"___| -4
           1 |____"func"___| -5
  • lua_pushcclosure(L, func, 0) // 创建并压入一个闭包
  • lua_createtable(L, 0, 0) // 新建并压入一个表
  • lua_pushnumber(L, 343) // 压入一个数字
  • lua_pushstring(L, “mystr”) // 压入一个字符串

存入栈的数据类型包括数值, 字符串, 指针, talbe, 闭包等。
压入的值在C看来是不同类型的,在lua看来都是TValue结构。

typedef struct lua_TValue {
  Value value; 
  int tt
} TValue;

/*
** Union of all Lua values
*/
typedef union {
  GCObject *gc;
  void *p;
  lua_Number n;
  int b;
} Value;

/*
** Union of all collectable objects
*/
union GCObject {
  GCheader gch;
  union TString ts;
  union Udata u;
  union Closure cl;
  struct Table h;
  struct Proto p;
  struct UpVal uv;
  struct lua_State th;  /* thread */
};

TValue结构对应于lua中的所有数据类型, 是一个{值, 类型} 结构, 这就lua中动态类型的实现, 它把值和类型绑在一起, 用tt记录value的类型, value是一个联合结构, 由Value定义, 可以看到这个联合有四个域, 先简单的说明:

  • p --> 可以存一个指针, 实际上是lua中的light userdata结构
  • n --> 所有的数值存在这里, 不光是int , 还是float
  • b --> Boolean值存在这里, 注意, lua_pushinteger不是存在这里, 而是存在n中, b只存布尔
  • gc --> 其他诸如table, thread, closure, string需要内存管理垃圾回收的类型都存在这里

gc是一个指针, 它可以指向的类型由联合体GCObject定义, 从图中可以看出, 有string, userdata, closure, table, proto, upvalue, thread

  1. lua中, number, boolean, nil, light userdata四种类型的值是直接存在栈上元素里的, 和垃圾回收无关.
  2. lua中, string, table, closure, userdata, thread存在栈上元素里的只是指针, 他们都会在生命周期结束后被垃圾回收.

详见

lua常用api

lua_State* L=luaL_newstate(); luaL_newstate()函数返回一个指向堆栈的指针
lua_createtable(L,0,0);新建并压入一张表
lua_pushstring(L,0,0);压入一个字符串
lua_pushnumber(L,0,0);压入一个数字
lua_tostring(L,1);取出一个字符串   return const char *
lua_tointeger(L,1);取出数字  return int
double b=lua_tonumber();取出一个double类型的数字
lua_load()函数 当这个函数返回0时表示加载
luaL_loadfile(filename) 这个函数也是只允许加载lua程序文件,不执行lua文件。它是在内部去用lua_load()去加载指定名为filename的lua程序文件。当返回0表示没有错误。
luaL_dofile 这个函数不仅仅加载了lua程序文件,还执行lua文件。返回0表示没有错误。
lua_push*(L,data)压栈,
lua_to*(L,index)取值,
lua_pop(L,count)出栈。
lua_close(L);释放lua资源
lua_getglobal(L, "val");//获取全局变量的val的值,并将其放入栈顶

lua调用c库

lua为程序主题调用c库函数,lua用require "myLualib"来请求c库,之后可以调用c函数,例子:
c库代码,需要添加lua.h和lauxlib.h头文件:

#include <lua.h>
#include <lauxlib.h>
#include <stdio.h>
/*  库 open 函数的前置声明   */
int luaopen_myLualib(lua_State *L);
static int ltest1(lua_State *L) {
    int num = luaL_checkinteger(L, 1); /*检查参数是否有误*/
    printf("--- ltest1, num:%d\n", num);
    return 0;                          /*如果有返回结果,则lua_pushnumber(L,op1 + op2);等回传结果,并return [返回的个数]*/
}
 
static int ltest2(lua_State *L) {
    size_t len = 0;
    const char * msg = luaL_checklstring(L, 1, &len); /*checklstring计算string长度给第三个参数*/
    printf("--- ltest2, msg:%s, len:%d\n", msg, len);
    return 0;
}
 
static int ltest3(lua_State *L) {
    size_t len = 0;
    int num = luaL_checkinteger(L, 1);
    const char * msg = luaL_checklstring(L, 2, &len);
    printf("--- ltest3, num:%d, msg:%s, len:%d\n", num, msg, len);
    return 0;
}
/*  将定义的函数名集成到一个结构数组中去,建立 lua 中使用的方法名与 C 的函数名的对应关系   */
static const luaL_reg myLualib_lib[] = {
	{ "test1", ltest1 },
	{ "test2", ltest2 },
	{ "test3", ltest3 },
	{ NULL, NULL },
};
 
/*  库打开时的执行函数(相当于这个库的 main 函数),执行完这个函数后, lua 中就可以加载这个 so 库了   */
int luaopen_myLualib(lua_State *L)
{
   /*  把那个结构体数组注册到 mt (名字可自己取)库中去 */
   luaL_register(L, "myLualib", myLualib_lib);
   return 1;
}
/*5.4.x
int luaopen_myLualib(lua_State *L) {
 
    luaL_Reg l[] = {
        { "test1", ltest1 },
        { "test2", ltest2 },
        { "test3", ltest3 },
        { NULL, NULL },
    };
    luaL_newlib(L, l);
 
    return 1;
}

luaL_register现在已经弃用,取而代之的是luaL_newlib
lua主程序:

function test3( ... )
    print("----- test myCustomLib")
    package.cpath = "./?.so" --so搜寻路径
    local f = require "myLualib" -- 对应luaopen_myLualib中的myLualib
 
    f.test1(123)
    f.test2("hello world")
    f.test3(456, "yangx")
end
 
test3()
--[[
    ----- test myCustomLib
--- ltest1, num:123
--- ltest2, msg:hello world, len:11
--- ltest3, num:456, msg:yangx, len:5

--]]

lua调用c还可以是先在c中注册好函数供lua调用,详见pwnsky调用流程。

c调用lua

C++ C调lua遵守的原则

  1. 所有lua中的值由lua自己管理 C++/C并不知道 并且他们其中的值Lua也不知道 如果C++/C要lua中的东西 由lua产生放到栈上 C++/C通过API接口获取这个值
  2. 凡是lua的变量 lua负责这些变量的生命周期和垃圾回收

main.lua文件:

name = "bob"
age= 20
mystr="hello lua"
mytable={name="tom",id=123456}

function add(x,y)
    return 2*x+y
end

test.c:

#include <stdio.h>
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
void gettable(lua_State *L){
    printf("读取lua table中对应的值\n");
    //将全局变量mytable压入栈
    lua_getglobal(L, "mytable"); //获取table对象
    /*//压入表中的key
    lua_pushstring(L, "name");
    
    //lua_gettable会在栈顶取出一个元素并且返回把查找到的值压入栈顶
    lua_gettable(L, 1);
    */
    lua_getfield(L,-1,"name"); //lua_getfield(L,-1,"name")的作用等价于 lua_pushstring(L,"name") + lua_gettable(L,1)
    const char *name = lua_tostring(L, -1); //在栈顶取出数据
    printf("name:%s\n", name);
    
    lua_pushstring(L,"id");//压入id
    lua_gettable(L, 1);//在lua mytable表中取值返回到栈顶
    int id = lua_tonumber(L, -1); //在栈顶取出数据
    printf("id:%d\n", id);
}
void add(lua_State *L){
    //调用函数,依次压入参数
    lua_getglobal(L, "add");
    lua_pushnumber(L, 10);
    lua_pushnumber(L, 20);
    //查看压入栈的元素
    for (int i=1;i<3;i++)
    {
        printf("number:%f\n",lua_tonumber(L, -i));
    }
    //lua_pcall(L,2,1,0):传入两个参数 期望得到一个返回值,0表示错误处理函数在栈中的索引值,压入结果前会弹出函数和参数
    int pcallRet = lua_pcall(L, 2, 1, 0); //lua_pcall将计算好的值压入栈顶,并返回状态值
    if (pcallRet != 0)
    {
        printf("error %s\n", lua_tostring(L, -1));
        return -1;
    }

    printf("pcallRet:%d\n", pcallRet);
    int val = lua_tonumber(L, -1); //在栈顶取出数据
    printf("val:%d\n", val);
    lua_pop(L, -1); //弹出栈顶
    //再次查看栈内元素,发现什么都没有,因为lua在返回函数计算值后会清空栈,只保留返回值 
    for (int i=1;i<3;i++)
    {
        printf("number:%f\n",lua_tonumber(L, -i));
    }
}
int main()
{
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    int retLoad = luaL_loadfile(L, "main.lua");
    if (retLoad == 0)
    {
        printf("load file success retLoad:%d\n", retLoad);
    }
    if (retLoad || lua_pcall(L, 0, 0, 0))
    {
        printf("error %s\n", lua_tostring(L, -1));
        return -1;
    }

    lua_getglobal(L, "name"); //lua获取全局变量name的值并且返回到栈顶
    lua_getglobal(L, "age");  //lua获取全局变量age的值并且返回到栈顶,这个时候length对应的值将代替width的值成为新栈顶
    //注意读取顺序
    int age = lua_tointeger(L, -1); //栈顶
    const char *name = lua_tostring(L, -2);//次栈顶
    printf("name = %s\n", name); 
    printf("age = %d\n", age);   
    add(L);
    gettable(L);
    lua_close(L);
    return 0;
}

注意:这个时候我们修改一下lua中的add函数:把2改为4

function add(x,y)
    return 4*x+y
end

这时不进行编译,直接再运行一下./main,可以看到这个结果改变了从40变成了60,这是在我们没有进行重复编译的情况下直接产生的变化。
漂亮的证明了lua在c语言里的嵌入特性,lua中的函数就像是文本一样被读取,但是又确实是作为程序被执行。当我们的项目很大的时候,每次编译都需要几十分钟,这个时候如果合理的利用lua特性,仅仅是需要修改lua文件就可以避免这十几分钟的空白时间。

例子见津门杯easyre题目。

2021津门杯easyRe的出题思路和lua脚本

这个题属于c调用lua脚本,出题思路是:

easyRe

题目给出的有:加密的lua脚本、base64加密的output数组、可执行程序

思路

程序对用户输入的32位flag进行了操作,取了第6,第15,第29位作为产生随机数的种子,经过线性同余算法生成33组随机数,之后把input的每一个字符分别和33个随机数异或,随后将每个数和上一轮对应位置+1(即array[i+j])的数相加,随后每个数随机数通过lua脚本进行了异或处理,该异或处理是由c调用lua脚本实现的(本地脚本时经过加密的,可从内存dump出来),之后和固定数组(长度为64)作比较,输出是否成功,重点是了解c如何调用lua.

这里对于线性同余算法的seed,由于&0xfff,可以在range(0xfff)范围爆破,之后使用z3约束求解.

步骤

程序对elf文件header的entry point进行了修改,使其不能运行,程序没有开PIE,所以一般程序基址为0x40000,由ida对比文件偏移可知entry point被修改,number of header进行了修改,使ida分析text段出错,一般Number of section headers比Section header string table index大1,可以对elf文件头进行修复使其能运行,然后可动态调试算法。

  1. 产生32组随机数
  2. 用z3模拟程序流程对随机数就行异或处理
  3. 约束求解

2021安洵杯pwnsky出题思路,分析lua、c互调流程

详见

反编译工具及演示

反编译工具主要有两个:

  1. luadec。c编写
  2. unluac。java编写
java -jar unluac_2021_09_23.jar lua.bin > lua.lua
luadec lua.bin

luadec安装:

git clone https://github.com/viruscamp/luadec
cd luadec
git submodule update --init lua-5.1
cd lua-5.1
make linux
cd ../luadec
make LUAVER=5.1

版本支持5.1,实验版本支持5.2、5.3

两个工具都可以反编译字节码,区别unluac支持较新的版本支持lua5.4,luadec对lua5.1支持较好,5.2、5.3为实验性版本。关键在于luadec可以在lua源码基础上修改,然后直接编译,unluac由于用java编写,不是很好定位修改位置。
存放了最新的unluac源代码。

测试得到luadec对lua5.3反编译不是很好,只能反汇编。

lua文件格式,及源码修改 ,lua5.1字节码修改方法,以及几个有趣的自定义解释器的例子,以及相应反编译工具的修改重打包。

luac文件格式

lua头文件有12字节:
1b 4c 75 61 51 00 01 04 08 04 08 00
源码定义:

/*
* make header  src/lundump.c
*/
void luaU_header (char* h)
{
 int x=1;
 memcpy(h,LUA_SIGNATURE,sizeof(LUA_SIGNATURE)-1);    //4bytes
 h+=sizeof(LUA_SIGNATURE)-1; //移动指针
 *h++=(char)LUAC_VERSION; //1
 *h++=(char)LUAC_FORMAT; //1
 *h++=(char)*(char*)&x;				/* endianness */ 1
 *h++=(char)sizeof(int);  //1
 *h++=(char)sizeof(size_t); //1
 *h++=(char)sizeof(Instruction); //1
 *h++=(char)sizeof(lua_Number); //1
 *h++=(char)(((lua_Number)0.5)==0);		/* is lua_Number integral? */ 1
}

其中第1-4字节为:“\033Lua”;
第5字节标识lua的版本号,lua5.1为 0x51;
第6字节为官方中保留,lua5.1中为 0x0;
第7字节标识字节序,little-endian为0x01,big-endian为0x00;
第8字节为sizeof(int);
第9字节为sizeof(size_t);
第10字节为sizeof(Instruction),Instruction为lua内的指令类型,在32位以上的机器上为unsigned int;
第11字节为sizeof(lua_Number),lua_Number即为double;
第12字节是判断lua_Number类型起否有效,一般为 0x00;

不同版本的字节码头文件有些许差别。
文件头后面就是函数体部分。函数都被解释成Proto结构体进行解析,函数体涉及结构体太多,先掠过。

字节码修改

如今灰产横行,而lua对嵌入式支持很好,有很强的可配置性被广泛应用到游戏等,一些嵌入式设备也会用到,但是由于lua这一特性,很容易被人修改,所以各大厂商会对lua进行自己的修改,来防止使用反编译工具直接得到源码。修改方法有以下几种:

  1. 普通的对称加密,在加载脚本之前解密
  2. 修改lua虚拟机中opcode的顺序
  3. 交叉使用

这里简单说下字节码修改过程:
一共涉及到两个文件的修改:lopcodes.hlopcodes.c
lopcodes.h:

/*
** grep "ORDER OP" if you change these enums 老外留的提示
*/

typedef enum {
/*----------------------------------------------------------------------
name		args	description
------------------------------------------------------------------------*/
OP_MOVE,/*	A B	R(A) := R(B)					*/
OP_LOADK,/*	A Bx	R(A) := Kst(Bx)					*/
OP_LOADBOOL,/*	A B C	R(A) := (Bool)B; if (C) pc++			*/
OP_LOADNIL,/*	A B	R(A) := ... := R(B) := nil			*/
OP_GETUPVAL,/*	A B	R(A) := UpValue[B]				*/
*
*
*
OP_CLOSE,/*	A 	close all variables in the stack up to (>=) R(A)*/
OP_CLOSURE,/*	A Bx	R(A) := closure(KPROTO[Bx], R(A), ... ,R(A+n))	*/

OP_VARARG/*	A B	R(A), R(A+1), ..., R(A+B-1) = vararg		*/
} OpCode;

作者提示了如果修改enum要更改ORDER OP
lopcodes.c:

/* ORDER OP */

const char *const luaP_opnames[NUM_OPCODES+1] = {
  "MOVE",
  "LOADK",
  "LOADBOOL",
  "LOADNIL",
  "GETUPVAL",
*
*
*
  "CLOSE",
  "CLOSURE",
  "VARARG",
  NULL
};

#define opmode(t,a,b,c,m) (((t)<<7) | ((a)<<6) | ((b)<<4) | ((c)<<2) | (m))

const lu_byte luaP_opmodes[NUM_OPCODES] = {
/*       T  A    B       C     mode		   opcode	*/
  opmode(0, 1, OpArgR, OpArgN, iABC) 		/* OP_MOVE */
 ,opmode(0, 1, OpArgK, OpArgN, iABx)		/* OP_LOADK */
 ,opmode(0, 1, OpArgU, OpArgU, iABC)		/* OP_LOADBOOL */
 ,opmode(0, 1, OpArgR, OpArgN, iABC)		/* OP_LOADNIL */
*
*
*
 ,opmode(0, 0, OpArgU, OpArgU, iABC)		/* OP_SETLIST */
 ,opmode(0, 0, OpArgN, OpArgN, iABC)		/* OP_CLOSE */
 ,opmode(0, 1, OpArgU, OpArgN, iABx)		/* OP_CLOSURE */
 ,opmode(0, 1, OpArgU, OpArgN, iABC)		/* OP_VARARG */
};

这三处顺序可以随意修改。

Luac指令完整由:OpCode、OpMode操作模式,以及不同模式下使用的不同的操作数组成。

官方5.1版本的Lua使用的指令有3种格式,使用OpMode表示,它的定义如下:

enum OpMode {iABC, iABx, iAsBx};  /* basic instruction format */

其中,i表示6位的OpCode;A表示一个8位的数据;B表示一个9位的数据,C表示一个9位的无符号数据;后面跟的x表示数据组合,如Bx表示B与C组合成18位的无符号数据。sBx前的s表示是有符号数,即sBx是一个18位的有符号数。OpMode的表格luaP_opmodes指出了每个opcode属于哪种opmode。
定义如下:

/*===========================================================================
  We assume that instructions are unsigned numbers.
  All instructions have an opcode in the first 6 bits.
  Instructions can have the following fields:
	`A' : 8 bits
	`B' : 9 bits
	`C' : 9 bits
	`Bx' : 18 bits (`B' and `C' together)
	`sBx' : signed Bx

  A signed argument is represented in excess K; that is, the number
  value is the unsigned value minus K. K is exactly the maximum value
  for that argument (so that -max is represented by 0, and +max is
  represented by 2*max), which is half the maximum for the corresponding
  unsigned argument.
===========================================================================*/
/*
** size and position of opcode arguments.
*/
#define SIZE_C		9
#define SIZE_B		9
#define SIZE_Bx		(SIZE_C + SIZE_B)
#define SIZE_A		8

#define SIZE_OP		6

#define POS_OP		0
#define POS_A		(POS_OP + SIZE_OP)
#define POS_C		(POS_A + SIZE_A)
#define POS_B		(POS_C + SIZE_C)
#define POS_Bx		POS_C

以小端序为例,完整的指令格式定义如下表所示:

OpMode B C A OpCode
iABC B(23~31) C(14~22) A(6~13) opcode(0~5)
iABx Bx (14~31) A(6~13) opcode(0~5)
iAsBx sBx (14~31) A(6~13) opcode(0~5)

虚拟机修改-运算

lparser.c:

static BinOpr getbinopr (int op) {
  switch (op) {
    case '+': return OPR_ADD;
    case '-': return OPR_SUB;
    case '*': return OPR_MUL;
    case '/': return OPR_DIV;
    case '%': return OPR_MOD;
    case '^': return OPR_POW;
    case TK_CONCAT: return OPR_CONCAT;
    case TK_NE: return OPR_NE;
    case TK_EQ: return OPR_EQ;
    case '<': return OPR_LT;
    case TK_LE: return OPR_LE;
    case '>': return OPR_GT;
    case TK_GE: return OPR_GE;
    case TK_AND: return OPR_AND;
    case TK_OR: return OPR_OR;
    default: return OPR_NOBINOPR;
  }
}
`+`、`-`操作交换。可以达到另一种混淆效果。

虚拟机修改-函数

修改了 function 关键字,llex.h、llex.c:
llex.c:

/* ORDER RESERVED */
const char *const luaX_tokens [] = {
    "and", "break", "do", "else", "elseif",
    "end", "false", "for", "function", "if",
    "in", "local", "nil", "not", "or", "repeat",
    "return", "then", "true", "until", "while",
    "..", "...", "==", ">=", "<=", "~=",
    "<number>", "<name>", "<string>", "<eof>",
    NULL
};

llex.h:

#define FIRST_RESERVED	257

/* maximum length of a reserved word */
#define TOKEN_LEN	(sizeof("function")/sizeof(char))

可以将上述两个funtion字段改成其他你想改的字段,如funtion -> cyber,代码可写成:

cyber foo(a,b)
    return a+b
end

print('a+b=',foo(3,5))

修改建议本地install,以免覆盖原始文件。
make linux test && make local
lua各个版本之间的区别还是很大的,最新的版本5.4已经有80多条指令。

反编译工具修改

对应的修改反编译工具就行,对于opcode修改只变换反编译工具opcode顺序:
unluac:

public OpcodeMap(Version.OpcodeMapType type) {
    switch (type) {
        case LUA50:
            map = new Op[35];
            map[0] = Op.MOVE;
            map[1] = Op.LOADK;
            map[2] = Op.LOADBOOL;
            map[3] = Op.LOADNIL;
            map[4] = Op.GETUPVAL;
            map[5] = Op.GETGLOBAL;
            map[6] = Op.GETTABLE;
            .
            .

重新编译jar包。
源码
release

luadec:
将修改好的lua官方源码替换掉原始源码,重新编译,注意确认好源码是官方来源。

修改lua解释器,文件加密落地,防止通用反编译工具

dump前加密:
wb+可读写方式打开,位置/src/luac.c:

  FILE* D= (output==NULL) ? stdout : fopen(output,"wb+"); <-------
//   printf("%s\n",output);
  if (D==NULL) cannot("open");
  lua_lock(L);
  luaU_dump(L,f,writer,D,stripping);
  lua_unlock(L);
  D = GetFileSizeAndDump(D);   <------
  if (ferror(D)) cannot("write");
  if (fclose(D)) cannot("close");
 }

load后解密:
rb+可读写方式打开,位置/src/lauxlib.c:

  if (c == LUA_SIGNATURE[0] && filename) {  /* binary file? */
    lf.f = freopen(filename, "rb+", lf.f);  /* reopen in binary mode */
    lf = GetFileSizeAndLoad(c,lf);         <-----------
    if (lf.f == NULL) return errfile(L, "reopen", fnameindex);
    /* skip eventual `#!...' */
   while ((c = getc(lf.f)) != EOF && c != LUA_SIGNATURE[0]) ;
    lf.extraline = 0;
  }
  ungetc(c, lf.f);
  status = lua_load(L, getF, &lf, lua_tostring(L, -1));
  lf = GetFileSizeAndLoad(c,lf); <---------
  readstatus = ferror(lf.f);
  if (filename) fclose(lf.f);  /* close file (even in case of errors) */

修改遇到的问题:
加载流程和定位合适的处理点搞了好久:
文件dump是通过DumpBlock函数调用writer函数来dump,而且是根据文件格式分段dump,dump的size跟前面的还有依赖,所以在writer里直接加密不合适。往上找,找到所有的dump操作完成后,fclose之前讲文件流改写。

load是根据文件头第一个字符来判断文件类型(binary or src),修改要注意lua加载的两种文件类型都用到了 status = lua_load(L, getF, &lf, lua_tostring(L, -1)); getF(),为了不影响源码文件执行,必修在此之前修改文件流,还有一个点是在解密完文件流后,fclose时会将解密的内容重新写回文件,所以在fclose时,要重新加密文件流,注意此时要判断一下文件类型。

最终实现效果:lua文件头不变(也可以加密),luac生成字节码是加密的,lua执行时解密执行。

加密方式可以自行决定,这里仅仅是亦或了一下。

修改后的opcode顺序简单还原

准备

  1. 被修改opcode顺序后的lua虚拟机生成的luac字节码
  2. 同版本原始lua虚拟机生成的luac字节码
  3. 一个尽可能多的覆盖所有操作指令的lua脚本

思路

  1. 来自于同一个lua脚本修改前后的luac字节码进行对比,不同的就是opcode部分。
  2. 用正长的字节码去校队修改后的字节码

通常经过luac编译的字节码*.luac 文件其中的数据是由 luaU_dump 函数产生,在 luac.c 文件中被调用,而 luaU_dump 的另一个入口在 lapi.c 的 lua_dump,被绑定到 Lua 的 string.dump 函数。
通过在 Lua 脚本中对需要 dump 的函数用 string.dump,可以得到对应的字节码。
对于只修改了opcode顺序luac文件,对比正常文件之后也是只有6bit操作码不同,数据是相同的,所以只要对比出二者不同就是opcode的位置,即可还原出修改后的顺序。
当然这个只针对仅仅修改了opcode顺序的情况,如果还修改了其他的地方就需要甄别转换了。
源码lvm.c中的luaV_execute函数进行了opcode识别,可知是由GET_OPCODE(i)来获取opcode的,看源码知道他是在lopcodes.h中的宏定义:

#define cast(t, exp)	((t)(exp))
#define SIZE_OP		6

#define POS_OP		0
/* creates a mask with `n' 1 bits at position `p' */
#define MASK1(n,p)	((~((~(Instruction)0)<<n))<<p)
#define GET_OPCODE(i)	(cast(OpCode, ((i)>>POS_OP) & MASK1(SIZE_OP,0)))

可知i就是OpCode枚举类的元素,MASK1(SIZE_OP,0)经过计算是0x3f,经过&0x3f后得到opcode。
尝试写lua脚本,实现对opcode还原:

local bit = require('bit') -- lua 位操作脚本
local test = require("test") -- 测试的lua脚本,尽可能多的覆盖所有指令

-- 加载用正常 lua 的 dump 文件
local fp = io.open("test.luac","rb")
local ori_data = fp:read("*all")
fp:close()

-- print('data len '.. #data)
print('ori_data len ' .. #ori_data)

local ori_op_name = {
    "MOVE",
    "LOADK",
    "LOADBOOL",
    "LOADNIL",
    "GETUPVAL",
    "GETGLOBAL",
    "GETTABLE",
    "SETGLOBAL",
    "SETUPVAL",
    "SETTABLE",
    "NEWTABLE",
    "SELF",
    "ADD",
    "SUB",
    "MUL",
    "DIV",
    "MOD",
    "POW",
    "UNM",
    "NOT",
    "LEN",
    "CONCAT",
    "JMP",
    "EQ",
    "LT",
    "LE",
    "TEST",
    "TESTSET",
    "CALL",
    "TAILCALL",
    "RETURN",
    "FORLOOP",
    "FORPREP",
    "TFORLOOP",
    "SETLIST",
    "CLOSE",
    "CLOSURE",
    "VARARG",
}
local data = string.dump(test)	-- dump
print('modify_data len ' .. #data)

local new_op = {}
-- 用目标 lua 和正常 lua 的 dump 数据对比
for i = 1, #data do
    local by_ori = string.byte(ori_data,i)
    local by_new = string.byte(data,i)
    if by_ori ~= by_new then
        local op_name = ori_op_name[bit:_and(0x3F,by_ori) + 1] -- enum第0元素为nil
        local op_idx = bit:_and(0x3F,by_new)
        new_op[op_name] = op_idx
    end
end
-- print(ori_op_name[1])
print("old \t new \t name")
for idx, op_name in pairs(ori_op_name) do
    local tmp = ''
    if new_op[op_name] ~= nil then
        tmp = new_op[op_name]
    end
    print((idx - 1) .. "\t" .. tmp .. "\t" .. op_name )
end

bindiff对操作指令敏感,对指令的操作数是识别不了的。对源码进行修改可以根据bindiff对比异同,快速定位,比如+、-换位,opcode换位等,可以通过分析加载流程确定。

2020华为云安全逆向 weird-lua

题目给了check_license_out.lua和对应的lua文件,32位elf,运行lua可知是lua5.3.3,hexdump -C check_license_out.lua发现文件头不对,可知文件被做了处理。
根据固定文件头lua version应为0x53,但是这里是0xac:

00000000  1c 4c 75 61 ac ff 19 93  0d 0a 1a 0a fb fb fb f7  |.Lua............|
00000010  f7 78 56 00 00 00 00 00  00 00 00 00 00 00 28 77  |.xV...........(w|
00000020  40 fe ff 00 00 00 00 00  00 00 00 ff fd ca 59 01  |@.............Y.|
00000030  00 00 2d 00 00 00 20 00  00 80 0b 00 80 0a 41 40  |..-... .......A@|

紧接着应该为0,而这里为0xff,可知这里肯定是做了异或,异或了0xff,经过验证确实如此。那么源码推测是在dump的时候异或落地且不是全部亦或,对源码了解一点的都知道ldump.c是做dump的,可以推测dumpByte函数做了亦或,反之load函数肯定也要异或,不然运行不了,可知ldump.c的loadByte函数也要异或。

还可以看到头文件第一个标识也不对,应该由0x1b改成了0x1c,之后发现编译出来的lua还运行不了,可能是opcodes被改了,用上面提到的还原方法,在把异或处理完成后,对opcodes进行dump。

ori_data len 1547
modify_data len 1547
old      new     name
0               MOVE
1               LOADK
2               LOADKX
3               LOADBOOL
4               LOADNIL
5               GETUPVAL
6       29      GETTABUP
7       8       GETTABLE
8       32      SETTABUP
9       44      SETUPVAL
10      43      SETTABLE
11              NEWTABLE
12      10      SELF
13              ADD
14              SUB
15              MUL
16              MOD
17              POW
18              DIV
19              IDIV
20              BAND
21              BOR
22              BXOR
23              SHL
24              SHR
25              UNM
26              BNOT
27              NOT
28              LEN
29      39      CONCAT
30              JMP
31      46      EQ
32      47      LT
33      12      LE
34      31      TEST
35      9       TESTSET
36              CALL
37      40      TAILCALL
38      7       RETURN
39      37      FORLOOP
40      34      FORPREP
41      33      TFORCALL
42      41      TFORLOOP
43      38      SETLIST
44      45      CLOSURE
45      42      VARARG
46              EXTRAARG

得到opcode顺序后,修改源码再次编译luadec去反编译,最后得到asm汇编码,对汇编进行分析可得到,lua的逻辑仅仅是个异或和表替换。
源码编译修改makefile的gcc选项加上-m32(需要安装32位库 sudo apt-get install libreadline-dev:i386 && sudo apt-get install linux-libc-dev:i386)
部分反汇编代码:

; Function:        0
; Defined at line: 0
; #Upvalues:       1
; #Parameters:     0
; Is_vararg:       2
; Max Stack Size:  53

    0 -[0000002d 2d]: CLOSURE   R0 0         ; R0 := closure(Function #0_0)
    1 -[80000020 20]: SETTABUP  U0 K0 R0     ; U0["to_v"] := R0
    2 -[0a80000b 0b]: NEWTABLE  R0 21 0      ; R0 := {} (size = 21,0)
    3 -[00004041 01]: LOADK     R1 K1        ; R1 := 172
    4 -[00008081 01]: LOADK     R2 K2        ; R2 := 25
    5 -[0000c0c1 01]: LOADK     R3 K3        ; R3 := 60
    6 -[00010101 01]: LOADK     R4 K4        ; R4 := 95
    7 -[00014141 01]: LOADK     R5 K5        ; R5 := 5
    8 -[00018181 01]: LOADK     R6 K6        ; R6 := 27
    9 -[0001c1c1 01]: LOADK     R7 K7        ; R7 := 49
   10 -[00020201 01]: LOADK     R8 K8        ; R8 := 58
   11 -[00024241 01]: LOADK     R9 K9        ; R9 := 171
   12 -[00014281 01]: LOADK     R10 K5       ; R10 := 5
   .
   .
   .

参考

  1. lua_re
  2. lua字节码的解析
  3. 深入理解Lua的闭包