首页
社区
课程
招聘
[原创]从GO到Luajit——特殊GO语言APT样本引发的思考(一)
2022-4-2 16:34 6612

[原创]从GO到Luajit——特殊GO语言APT样本引发的思考(一)

2022-4-2 16:34
6612

前言

本文的出现最早源自某疑似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漏洞挖掘与利用;代码审计。

收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 17848
活跃值: (59913)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2022-4-2 22:42
2
0
若文章不是太长,建议将完整的文章,在一帖里发完,不要分帖。
游客
登录 | 注册 方可回帖
返回