前言本文的出现最早源自某疑似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反编译 简单方案反编译可使用f56K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6p5M7V1&6W2N6$3u0A6k6g2)9J5c8X3I4#2j5h3A6A6N6q4)9J5k6r3c8W2j5$3!0E0M7r3W2D9k6i4u0Q4c8e0k6Q4z5f1c8Q4b7e0g2Q4c8e0g2Q4b7f1k6Q4b7U0W2T1P5i4c8W2j5$3!0V1k6g2!0q4z5q4!0n7c8W2)9&6b7W2!0q4z5q4!0m8x3g2)9^5b7#2!0q4y4g2)9^5c8W2)9^5c8q4!0q4y4#2!0n7b7#2)9&6y4W2!0q4z5q4!0m8c8W2)9&6x3g2!0q4x3#2)9^5x3q4)9^5x3R3`.`. 具体使用方法可以参考其手册。
复杂方案若出现了简单方案无法正确的反编译的情况,可以使用010 editor打开文件并下载luajit模板来对luajit解析:
luajit字节码解析字节码详细的说明可参考873K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8Y4N6A6K9$3W2Q4x3X3g2D9N6h3q4B7K9i4c8Q4x3X3g2G2M7X3N6Q4x3V1k6n7P5i4c8W2j5$3!0V1k6g2)9J5k6o6u0Q4x3X3f1H3i4@1g2r3i4@1u0o6i4K6S2o6i4@1f1^5i4@1u0r3i4K6V1&6i4@1f1&6i4K6R3%4i4K6S2o6i4@1f1#2i4K6S2r3i4@1q4m8i4@1f1^5i4@1q4r3i4@1t1@1i4@1f1$3i4K6V1^5i4K6S2q4i4@1f1@1i4@1t1^5i4K6R3H3i4@1f1@1i4@1u0m8i4K6W2n7i4@1f1$3i4@1q4r3i4K6V1@1i4@1f1^5i4@1u0q4i4K6R3K6i4@1f1&6i4K6R3%4i4K6S2p5i4@1f1^5i4@1p5$3i4K6R3I4i4@1f1%4i4K6W2m8i4K6R3@1i4@1f1K6i4K6R3H3i4K6R3J5 也可参考af8K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6X3k6h3W2U0L8$3&6Y4i4K6u0r3L8s2g2S2i4K6g2X3M7X3g2Q4x3V1k6T1L8r3!0T1i4K6u0r3L8h3q4K6N6r3g2J5i4K6u0r3L8s2g2S2i4K6u0r3L8s2g2S2i4K6g2X3M7X3f1@1i4K6u0W2L8h3c8Q4c8e0y4Q4z5o6m8Q4z5o6t1`. 部分指令实际构成为 前缀 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代码举例:
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!