0X00 前言
壳(Packers or crypters)被广泛用于保护恶意软件免于被检测和静态分析。这些辅助工具通过使用压缩和加密算法,使网络犯罪分子能够为每个活动甚至每个受害者准备独特的恶意软件样本,这增加了杀毒软件的工作难度。在某些壳的情况下,不使用动态分析就对恶意软件进行分类变成了一项挑战性任务。
为了分析恶意样本并提取其配置数据,如加密密钥和命令控制服务器地址,我们必须首先对其进行解包。我们可以通过在沙盒环境中运行恶意软件来做到这一点,例如CAPE,然后提取内存转储。然而,这种方法有一些缺点。例如,我们获取的转储往往无法进一步进行更深入的分析,而且沙盒仿真本身需要大量时间和资源。
在这篇文章中,我们研究了基于Nullsoft Scriptable Install System (NSIS)的一组封包器,并描述了创建一个工具的方法,该工具让我们能自动获得解包的样本。
0X01 NSIXloader:基于NSIS的壳
NSIS包本质上是一个自解压存档,配有支持脚本语言的安装系统。它包含压缩文件,以及用NSIS脚本语言编写的安装指令。为了在不运行安装包的情况下访问内容,我们可以使用一个解压缩工具,该工具识别NSIS格式并支持其压缩方法,例如7-Zip。
网络犯罪分子使用NSIS的优势在于,它允许他们创建在第一眼看上去与合法安装程序无异的样本。由于NSIS自行执行压缩,恶意软件开发者不需要实现压缩和解压缩算法。NSIS的脚本功能允许将一些恶意功能转移到脚本内部,使分析更加复杂。
当我们分析涉及XLoader的活动时,我们注意到同一基于NSIS的家族的封包器经常被用来保护样本。我们后来发现,这些相同的封包器被用于保护包括以下家族在内的广泛恶意软件:
- AgentTesla
- Remcos
- 404 Keylogger
- Lokibot
- Azorult
- Warzone
- Formbook
- XLoader
不幸的是,在我们分析的样本中,我们找不到任何明显指示这个封包器名称的文本字符串,除了DLL名称Loader.dll
和包含相同名称的PDB路径:
因此,我们决定称之为NSIXloader。这个恶意软件家族的壳至少在2016年就被发现了。
0X02 打包样本结构
我们分析的大多数样本在存档内部具有类似的文件结构。
在存档的根目录中,有两个带有加密数据的二进制文件。在$PLUGINSDIR目录
中,有一个DLL导出了几个函数,其中一个必须被调用以解包有效载荷。
NSIS支持一个插件系统,由默认放置在$PLUGINSDIR目录
中的DLL文件组成。恶意DLL被伪装成这些插件之一。NSIS允许使用以下语法轻松调用插件函数:
<DLL_NAME>::<function_name>
恶意安装程序使用一个非常简单的NSIS脚本,其任务是解包加密文件,将它们放入临时目录,然后调用恶意DLL内的一个函数。在下面的例子中,调用的函数是"HvDeclY":
InstallDir $TEMP
; …
Function .onGUIInit
InitPluginsDir
SetOutPath $INSTDIR
SetOverwrite off
File tiejkfis.yp
File pvynjhnv.oh
rnthgfcoj::HvDeclY
DLL
DLL的功能非常简单。DLL读取最小的加密文件(例如中的pvynjhnv.oh
),并且文件名是硬编码的。然后它使用文本键进行XOR操作来解密文件:
解密后,它将执行传递给解密后的代码:
在一些变体中,在使用 XOR 运算之前,对加密文本的每个字节进行循环移位:
壳代码
解密文件包含与位置无关的shellcode。其执行从初始化包含加密有效负载的文件的名称开始,并获取几个Windows API函数的地址。
加载程序存储的不是API函数名称,而是使用简单算法计算出的4字节哈希值。为了获取所需函数的地址,加载程序会解析 kernel32.dll的标头并找到导出表的地址。接下来,它计算每个函数名称的哈希值并将其与所需函数的哈希值进行比较。之后,加载程序读取并解密有效载荷:
每一个被分析的样本都使用独特的操作序列。尽管密码很简单,但为了实现有效载荷的自动解密器,我们需要为每个样本重现独特的命令序列。应用该算法后,加载器获得解密的有效载荷。
0X03 自动载荷解包方法
第一步,我们可以使用7-zip从NSIS包中提取和解压文件。其余的自动化操作可以用Python完成。提取文件后,我们需要从DLL中获取加密密钥。在所有分析的样本中,加密密钥都是由小写拉丁字母和数字组成的文本字符串。我们可以使用以下正则表达式进行搜索:
dll_key_re = re.compile(br"([a-z\d]{10,20})\x00")
密钥始终位于.data或.rdata部分的开头,因此可以使用malduck库按以下方式提取它:
from malduck import procmempe
def dll_extract_keys(dll_data):
p = procmempe(dll_data)
for section in filter(lambda s: b"data" in s.Name, p.pe.sections):
data = p.readp(section.PointerToRawData, section.SizeOfRawData)
for found in dll_key_re.finditer(data):
yield found.group(1)
现在我们有了密钥,我们可以轻松解密shellcode。考虑到加壳器可能会在XOR操作之前应用循环移位,我们可以检查移位的每个值,并使用正则表达式验证解密的shellcode:
def decrypt_loader(data, dll_key):
for shift in range(8):
shifted_data = [(b >> shift) | (b << (8 - shift)) & 0xFF for b in data] if shift else data
dec_data = xor(dll_key, shifted_data)
if shellcode_validation_re.search(dec_data):
return dec_data
然而,最具挑战性的任务是从shellcode重建有效载荷解密算法。我们看一下这个算法的汇编代码:
每个操作后,都会更新正在解密的缓冲区中的当前字节,并将此字节移回寄存器EAX。然后,寄存器EAX中的数据使用以下操作之一进行转换:not
、dec
、inc
、sar
、shl
、or
、add
、sub
、neg
、xor
、movzx
。
为了找到解密算法的开始和结束,我们可以使用Yara规则或正则表达式。当我们有了所需的代码部分,我们可以使用malduck库来反汇编和分析它。在每个有价值的指令中,第一个操作数是EAX或ECX,这可以作为一个过滤器。此外,我们注意到第二个操作数可以是一个寄存器、一个立即值或一个内存操作数。如果使用了内存操作数,我们可以将其转换为命名变量(它可以是b-当前字节的值,或i-当前字节的索引),使用以下映射:mem_vars_map = {0xFF: "b", 0xF8: "i"}
。
mem_vars_map = {0xFF: "b", 0xF8: "i"}
for ins in filter(
lambda _ins: _ins.op1.value in ("eax", "ecx") and _ins.mnem in supported_instructions,
procmem(data).disasmv(0, size=len(data))
):
if not ins.op2:
op2 = None
elif ins.op2.is_reg or ins.op2.is_imm:
op2 = ins.op2.value
elif ins.op2.is_mem:
op2 = mem_vars_map.get(ins.op2.value & 0xFF)
else:
continue
ops.append(get_operation(ins.mnem, ins.op1.value, op2))
上述代码示例中使用的函数get_operation
可以通过以下方式实现:
var_list = {"eax": 0, "ecx": 0, "b": 0, "i": 0}
def get_operation(name, op1, op2):
def not_op():
var_list[op1] = (~var_list[op1]) & 0xFF
def dec_op():
var_list[op1] = (var_list[op2] - 1) & 0xFF
def shl_op():
var_list[op1] = (var_list[op1] << op2) & 0xFF
def or_op():
var_list[op1] |= var_list[op2] if isinstance(op2, str) else op2
var_list[op1] &= 0xFF
# ... implementation of other operations ...
operations = {
"not": not_op, "dec": dec_op, "shl": shl_op, "or": or_op,
# ... other operations
}
return operations[name]
收集完所有操作后,我们可以模拟解密算法来解密payload:
def decrypter(enc_data):
dec_data = []
for _i, _b in enumerate(enc_data):
var_list["eax"] = _b
var_list["ecx"] = 0
var_list["b"] = _b
var_list["i"] = _i
for _op in ops:
_op()
dec_data.append(var_list["eax"])
return bytes(dec_data)
请注意,我们展示的高度简化的示例说明了实现自动解包器的一种可能方法,但它并不全面,并且可能无法适用于某些样本。
0X03 其他变体
除了这个变种之外,我们还发现了其他变种,从简单到复杂,不一而足。让我们来看看其中的一些。
嵌入shellcode的DLL
与之前讨论的变体不同,在这种情况下,shellcode也是加密的,但它没有存储在单独的文件中。相反,它直接嵌入在DLL中并加载到基于堆栈的数组中:
可以使用以下正则表达式来定位包含加密shellcode的这部分代码的边界:
shellcode_block = re.search(
b"\xC7\x85(..\xFF\xFF)(.{4})(\xC7(\x85..\xFF\xFF|\x45.)(.{4})){32,}.*\x8D..\\1",
dll_data, re.DOTALL
)
shellcode本身也可以用正则表达式来提取,如下所示:
shellcode = b"".join(re.findall(b"\xC7(?:\x85..\xFF\xFF|\x45.)(.{4})", shellcode_block, re.DOTALL))
用于解密shellcode的XOR密钥仍然存储在DLL中:
NSIS 软件包仅包含两个文件:DLL 和加密的有效载荷。NSIS 脚本有相应的更改:
Function .onGUIInit
InitPluginsDir
SetOutPath $INSTDIR
SetOverwrite off
File lbchv.zt
jlpeylfn::JKbtgdfd
EXE替代DLL
在一些示例中,DLL插件被替换为常规可执行文件。在这种情况下,NSIS没有目录$PLUGINSDIR
;所有文件都位于档案的根目录中。
NSIS 脚本略有不同:使用ExecWait命令调用可执行文件,并将存储加密shellcode的文件路径作为命令行参数传递:
Function .onGUIInit
InitPluginsDir
SetOutPath $INSTDIR
SetOverwrite off
File irgfodgeidi.lh
File hgpngqlustf.ge
File pnmess.exe
ExecWait "$\"$INSTDIR\pnmess.exe$\" $INSTDIR\hgpngqlustf.ge"
其余功能保持不变,并且可以应用前面讨论的方法进行自动解包。
资源中的Shellcode
在此变体中,加密的shellcode存储在RT_RCDATA类型的资源中:
打包程序的其余功能保持不变。
RC4加密的有效载荷
此变体具有最显著的差异,并且更难以破解。让我们检查一下使用此加壳程序变体的样本。该软件包包含以下文件:
System.dll插件与打包程序没有直接关系,它是一个嵌入式NSIS插件,提供从脚本调用Windows API函数的功能。当我们分析NSIS脚本本身时,我们确实看到了一系列API函数调用。通过这些调用,它分配内存,设置内存保护属性PAGE_EXECUTE_READWRITE(0x40)
,将文件zzeqtzxaeeuwcxjz
的内容读入其中,然后将控制权转移到那里:
Function .onInit
InitPluginsDir
SetOutPath $INSTDIR
File rdoc6dqwn7
File zeqtzxaeeuwcxjz
System::Alloc 56417
Pop $8
System::Call "kernel32::CreateFile(t'$INSTDIR\zeqtzxaeeuwcxjz', i 0x80000000, i 0, p 0, i 3, i 0, i 0)i.r10"
System::Call "kernel32::VirtualProtect(i r8, i 56417, i 0x40, p0)"
System::Call "kernel32::ReadFile(i r10, i r8, i 56417, t., i 0)"
System::Call kernel32::GetCurrentProcess()i.r5
System::Call "::$8(i r5, i r8, i0).i r5"
Nop
Exec $INSTDIR\yeller.dif
让我们检查一下加载文件中包含的代码。此文件包含加密的shellcode,并实现其加载和解密。首先,将加密的shellcode逐字节放入堆栈:
在我们确定了加密shellcode在堆栈上放置的代码边界之后,我们可以轻松地使用正则表达式来提取它:
enc_shellcode = b"".join(re.findall(b"\xC6(?:\x85..\xFF\xFF|\x45.)(.)", key_code_block, re.DOTALL))
一个简单的自定义流密码用于解密shellcode,它由一系列逻辑和算术运算组成:
为了解密shellcode,我们可以采用与之前解密payload类似的方法,但稍作修改。此外,在这个变体中,shellcode本身有很大不同。它没有使用自定义流密码,而是使用了修改后的RC4密码。RC4密钥放在堆栈字符串中:
RC4密码经过这样的修改,即应用RC4之后,我们必须对获得的数据使用RC4密钥执行XOR运算:
decrypted_data = rc4(rc4_key, enc_data)
decrypted_data = xor(rc4_key, decrypted_data)
0X04 结论
这个利用Nullsoft Scriptable Install System的恶意封包器家族相当普遍,并且多年来被网络犯罪分子用于打包大量类型的恶意载荷,如加载器、窃取器和远程访问木马(RATs)。它所提供的载荷的广泛使用和多样性表明,它很可能是在暗网上销售的商品,可供各种恶意行为者访问,而不是单一实体的专有工具。因此,开发自动化的静态解包工具是无价的。这些工具通过迅速提供恶意软件的未加密版本,便于手动和自动化分析,这对于配置检索、调试和反汇编等任务至关重要。
0X05 防御IOC
Packer.Win.NSISCrypter.*
Trojan.Win.Shellcode.F
Trojan.Win.Shellcode.G
SHA256 | Variant | Payload |
---|---|---|
12a06c74a79a595fce85c5cd05c043a6b1a830e50d84971dcfba52d100d76fc6 | DLL loader, Shellcode in a separate file | XLoader |
44e51d311fc72e8c8710e59c0e96b1523ce26cd637126b26f1280d3d35c10661 | EXE loader, Shellcode in a separate file | XLoader |
00042ff7bcfa012a19f451cb23ab9bd2952d0324c76e034e7c0da8f8fc5698f8 | Shellcode is embedded in the DLL | XLoader |
3f7771dd0f4546c6089d995726dc504186212e5245ff8bc974d884ed4f485c93 | EXE Loader, Shellcode in resources | Remcos |
160928216aafe9eb3f17336f597af0b00259a70e861c441a78708b9dd1ccba1b | Payload is RC4-encrypted | XLoader |
cd7976d9b8330c46d6117c3b398c61a9f9abd48daee97468689bbb616691429e | EXE loader, Shellcode in a separate file | Agent Tesla |
a3e129f03707f517546c56c51ad94dea4c2a0b7f2bcacf6ccc1d4453b89be9f5 | EXE loader, Shellcode in a separate file | 404 Keylogger |
bb8e87b246b8477863d6ca14ab5a5ee1f955258f4cb5c83e9e198d08354bef13 | EXE loader, Shellcode in a separate file | Formbook |
178f977beaeb0470f4f4827a98ca4822f338d0caace283ed8d2ca259543df70e | EXE loader, Shellcode in a separate file | Lokibot |
80db5ced294160666619a79f0bdcd690ad925e7f882ce229afb9a70ead46dffa | DLL loader, Shellcode in a separate file | Warzone |
090979bcb0f2aeca528771bb4a88c336aec3ca8eee1cef0dfa27a40a0a06615c | EXE loader, Shellcode in a separate file | Azorult |