前言
本文的出现最早源自某疑似apt样本,HASH:cbef6bd78137deab082d39983cdb198f370330da410c5d29f65af2386b8c1b2d,该样本使用混淆了函数名的GOLANG,在cgo中包含了一个luajit解析器,利用luajit执行CS马。
在研究此样本时不可避免的涉及到了luajit相关内容,在此记录,内容比较散,语言比较随意,有部分内容是来源于官方的说法的谷歌翻译,如有错误敬请指出。本文不会涉及到过多的样本内容,万一出了二就来讲讲样本分析吧(咕咕咕)
最后本来要做免杀测试的,忘了,就交给各位大佬试试免杀情况吧。
安装luajit
luajit的安装要先在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代码举例:
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。