前言本文的出现最早源自某疑似apt样本,HASH:cbef6bd78137deab082d39983cdb198f370330da410c5d29f65af2386b8c1b2d,该样本使用混淆了函数名的GOLANG,在cgo中包含了一个luajit解析器,利用luajit执行CS马。 在研究此样本时不可避免的涉及到了luajit相关内容,在此记录,内容比较散,语言比较随意,有部分内容是来源于官方的说法的谷歌翻译,如有错误敬请指出。本文不会涉及到过多的样本内容,万一出了二就来讲讲样本分析吧(咕咕咕) 最后本来要做免杀测试的,忘了,就交给各位大佬试试免杀情况吧。
安装luajitluajit的安装要先在github上下载源码,然后打开事先准备好的VS或者MinGW这种编译器。除VS外的编译器直接使用make来编译。 若使用VS则打开VS,然后选择下图中的选项来打开VS环境中的CMD: 然后cd到你解压luajit的文件夹中的src目录,执行msvcbuild.bat脚本等待其安装完成即可。
特别说明:若需要创建static库则在安装时输入msvcbuild.bat static即可。
生成的文件中luajit.exe可进行编译、运行等功能,还可分析字节码。若要嵌入到其他程序中则主要是lua51.lib负责提供API。
编译luajit编译所使用的参数为luajit -b src dst这样的格式,但特别要注意:dst的后缀决定了以什么样的形式输出。 可输出的格式有bytecode、.c、.h。默认是以bytecode输出的。
使用编译后的bytecode文件编译后的bytecode可使用3种方式进行调用/加载:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
int
(luaL_loadbuffer) (lua_State
*
L, const char
*
buff, size_t sz,const char
*
name);
说明:从内存仅加载不调用(会检查内容的规范性,若编译器报错则会失败),调用此函数后解析的buff内容将会自动填到状态机调用栈中,随后调用lua_pcall()即可运行此buff内容。在luavm中调用的是lua_load函数。
ret:
0
为正常,其余为出错
L:lua状态机对象
buff:char
*
类型的bytecode或lua源代码(该函数自动识别类型)
sz:buff长度
name:这个chunk叫啥名(不重要)
int
(luaL_loadfile) (lua_State
*
L, const char
*
filename);
说明:从文件仅加载不调用(会检查内容的规范性,若编译器报错则会失败),调用此函数后解析的文件内容将会自动填到状态机调用栈中,随后调用lua_pcall()即可运行此文件内容。在luavm中先会读文件,然后调用的是lua_load函数。
ret:
0
为正常,其余为出错
L:lua状态机对象
filename:文件名或路径
int
(luaL_loadstring) (lua_State
*
L, const char
*
s);
说明:从字符串加载不调用(会检查内容的规范性,若编译器报错则会失败),调用此函数后解析的字符串内容将会自动填到状态机调用栈中,随后调用lua_pcall()即可运行此字符串内容。特别要注意,该函数不可用于bytecode,因为其计算字符串长度使用的是strlen(),会因为bytecode带有
0x00
而被截断。最终调用的函数为luaL_loadbuffer。
ret:
0
为正常,其余为出错
L:lua状态机对象
s:lua源代码
int
(lua_pcall) (lua_State
*
L,
int
nargs,
int
nresults,
int
errfunc);
说明:执行状态机的调用栈中的当前函数。调用完成后自动清理调用栈与数据栈。
ret:
0
为正常,其余为出错
L:lua状态机对象
nargs:参数数量
nresults:返回值数量
errfunc:错误处理函数(可以没有)
int
(lua_cpcall) (lua_State
*
L, lua_CFunction func, void
*
ud);
说明:让lua去调用c代码,该函数调用的func不需要入调用栈!
ret:
0
为正常,其余为出错
L:lua状态机对象
func:要调用的C函数指针
ud:自定义结构体(未证实)
特别说明: 根据编译调试发现,当调用lua_load时,其不是调用的minilua.c文件的lua_load,而是调用的lj_load.c代码中的lua_load。当要比对源码时要注意该项!
嵌入lua bytecode到C代码现假设你已经编译了一个bytecode文件,当你需要从自身代码直接调用bytecode时,可以使用以下方法。 1.使用winhex打开编译后的文件 2.编辑->全部复制->c源码 3.将该部分代码粘贴到你的C中(最好是做成全局的变量) 4.使用如下的代码调用之
luajit与cgo 混淆器的缺点根据对样本的分析,当go使用Cgo时,部分混淆器无法对C部分的代码进行完全的混淆名称,利用这个可以来确定luajit相关函数。
代码优化在利用cgo对代码进行编译时,会出现代码优化的情况。原本根据源码对lual_loadbuff的调用链为lual_loadbuff->lua_load->lua_loadx->lj_vm_cpcall...。但由于代码优化,在拿到的样本cgo中调用链被简化为了lual_loadbuff->lua_loadx->lj_vm_cpcall,而在VS编译的代码中被简化为了lual_loadbuff->lj_vm_cpcall。在分析代码比对源码的时候尤为要注意。 代码优化合并现象:
luajit特征一般而言,若使用luajit来加载lua代码最终调用的函数都是lua_loadx,不管是由文件调用还是bytecode调用都走这个函数。根据源码,该函数会去判断其chunkname是否带有“?”字符: 且在其下方不远处有lual_loadfilex/lual_loadfile,这两个函数带有明显的读文件特征:
luajit bytecode反编译 简单方案反编译可使用https://github.com/DrNewbie/luajit-decompiler来对bytecode进行反编译。 具体使用方法可以参考其手册。
复杂方案若出现了简单方案无法正确的反编译的情况,可以使用010 editor打开文件并下载luajit模板来对luajit解析:
luajit字节码解析字节码详细的说明可参考http://wiki.luajit.org/Bytecode-2.0,这里只说明一些比较重要的。 也可参考https://github.com/feicong/lua_re/blob/master/lua/lua_re4.md。 部分指令实际构成为 前缀 op后缀,详细说明如下:
前缀指令的前缀一般表示参数的配置,具体如下: T table 表。 F function 函数。 U UpValue 上值。 K constant 常量。 G global 全局。 例:指令KSTR就是取字符串常量的功能。 T一般指的表为Constants表,该表内包含有所有常量字符串等数据。
后缀指令的后缀一般表示指令操作的类型,具体如下: V variable slot 变量槽。 S string constant 字符串常量。 N number constant 数值常量。 P primitive type 原始类型。 B unsigned byte literal 无符号字节字面量。 M multiple arguments/results 多参数与返回值。
表达式格式操作码一般格式基本如下: op dst/arg1 (arg2) (arg3) dst为操作完成后存放结果的位置,可能存在的arg2与可能存在的arg3为参与操作的对象。 当arg1与arg2都不存在则dst为参与操作的对象arg1。
具体操作请参考官方文档。
call所有操作需要特殊说明的就是call家族的几个指令。 这里以最常见的call来说明其参数的特殊性。 call家族一般有三个参数(部分只有两个),定义如下: call base lit1 lit2 base指的是要call的东西,一般由get家族操作来设置。 lit1为返回值数量+1 lit2为参数数量+1
假设有以下slot(临时/全局变量槽): 现有指令call 2 2 5,代表这个call实际代码应该是 ccc(ddd,eee,fff,ggg)返回值有1个,放在2号槽位置,返回值存放的位置为2~(2+2-2),公式base~(base+lit1-2)所以仅2号槽被存放了一个返回值。
lua payload相关技巧lua代码来编写二进制字符串的方式经测试可用三种,主要用于CS等生成的payload
table类型使用大括号引起的为table类型,该方式未经后续调用测试,仅通过了编译语法测试。
string.char函数使用string.char函数来初始化string:
字符串(基本类型)在lua中字符串是接受二进制数据的。使用[[ ]]来包括二进制数据即会生成string类型的数组。但有个小问题就是当数据中包括了]]这种数据会导致字符串提前结束。这个时候就可以使用特殊写法的[[]]来处理。这种写法可以直接贴二进制数据。 特殊写法:[===(任意数量=)[ ]===(跟前面一样数量=)] 例如:
payload调用调用payload代码举例:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!