简介
自 Elastic Security Labs 成立以来,我们一直专注于开发恶意软件分析工具,这不仅有助于我们的研究和分析,还可以向公众发布。我们希望回馈社会,从中获得多少,我们就回馈多少。为了使这些工具更加强大并减少代码重复,我们创建了 Python 库nightMARE。该库汇集了逆向工程和恶意软件分析所需的各种有用功能。我们主要用它来为不同的广泛存在的恶意软件系列创建配置提取器,但 nightMARE 是一个可应用于多种情况的库。
随着 0.16 版的发布,我们希望正式介绍该库,并在本文中详细介绍该模块提供的一些有趣功能,以及一个简短的教程,说明如何使用它来实现自己的配置提取器,使其与最新版本的 LUMMA 兼容(截至发稿日期)。
nightMARE 特色巡演
由 Rizin 提供
为了重现常用反汇编程序的功能,nightMARE 最初使用一组 Python 模块来执行静态分析所需的各种任务。例如,我们使用LIEF进行可执行文件解析(PE、ELF),使用Capstone对二进制文件进行反汇编,使用SMDA进行交叉引用(xref)分析。
这些众多的依赖关系使得图书馆的维护工作变得更加复杂。因此,为了尽可能减少第三方模块的使用,我们决定使用目前最全面的逆向工程框架。我们的选择自然倾向于日进。
Rizin是一款开源逆向工程软件,从 Radare2 项目分叉而来。它的速度、模块化设计以及基于类似 Vim 命令的几乎无限的功能,使其成为一个优秀的后台选择。我们使用rz-pipe模块将其集成到项目中,通过该模块,可以非常容易地从 Python 创建 Rizin 实例并对其进行调试。
项目结构
该项目围绕三个轴心展开:
- "analysis" 模块包含对静态分析有用的子模块。
- "核心" 模块包含常用的子模块:位运算、整数转换和用于配置提取的循环重码。
- "恶意软件" 模块包含所有算法实现(加密、解包、配置提取等),按恶意软件系列分组,适用时按版本分组。
分析模块
对于静态二进制分析,该模块提供了两种互补的工作技术:通过反转模块使用 Rizin 进行反汇编和指令分析,以及通过仿真模块进行指令仿真。
例如,当常量被手动移动到堆栈上时,与其逐条分析指令以获取直接数据,不如模拟整段代码,并在处理完成后读取堆栈上的数据。
我们在本文后面还将看到另一个例子,就加密函数而言,如果它很复杂,直接在二进制文件中使用仿真调用它往往比尝试手动实现它更简单。
逆转模块
该模块包含 Rizin 类,它是 Rizin 功能的抽象,可通过rz-pipe 直接向 Rizin 发送命令,并免费为用户提供惊人的分析能力。因为它是一个抽象概念,所以无需事先了解框架,就能在脚本中轻松使用该类所公开的函数。
虽然该类暴露了许多不同的功能,但我们并不打算详尽无遗。我们的目标是减少所有工具中重复功能的重复代码。不过,如果用户发现缺少某项功能,他们可以直接与rz-pipe 对象交互,向日进发出指令,实现自己的目标。
以下是我们使用最多的功能的简短列表:
# Disassembling
def disassemble(self, offset: int, size: int) -> list[dict[str, typing.Any]]
def disassemble_previous_instruction(self, offset: int) -> dict[str, typing.Any]
def disassemble_next_instruction(self, offset: int) -> dict[str, typing.Any]
# Pattern matching
def find_pattern(
self,
pattern: str,
pattern_type: Rizin.PatternType) -> list[dict[str, typing.Any]]
def find_first_pattern(
self,
patterns: list[str],
pattern_type: Rizin.PatternType) -> int
# Reading bytes
def get_data(self, offset: int, size: int | None = None) -> bytes
def get_string(self, offset: int) -> bytes
# Reading words
def get_u8(self, offset: int) -> int
...
def get_u64(self, offset: int) -> int
# All strings, functions
def get_strings(self) -> list[dict[str, typing.Any]]
def get_functions(self) -> list[dict[str, typing.Any]]
# Xrefs
def get_xrefs_from(self, offset: int) -> list
def get_xrefs_to(self, offset: int) -> list[int]
仿真模块
在 0.16 版中,我们重新设计了仿真模块,以充分利用 Rizin 的功能来执行各种数据相关任务。在引擎盖下,它使用Unicorn 引擎进行仿真。
目前,该模块仅提供"light" PE 仿真类 WindowsEmulator。无需重新定位,无需 DLL,无需操作系统模拟。其目标不是完全模拟 Windows 可执行文件,如Qiling或Sogen,而是提供一种简单的方法来执行代码片段或函数的简短序列,同时了解其局限性。
WindowsEmulator 类提供了几个有用的抽象概念。
# Load PE and its stack
def load_pe(self, pe: bytes, stack_size: int) -> None
# Manipulate stack
def push(self, x: int) -> None
def pop(self) -> int
# Simple memory management mechanisms
def allocate_memory(self, size: int) -> int
def free_memory(self, address: int, size: int) -> None
# Direct ip and sp manipulation
@property
def ip(self) -> int
@property
def sp(self) -> int
# Emulate call and ret
def do_call(self, address: int, return_address: int) -> None
def do_return(self, cleaning_size: int = 0) -> None
# Direct unicorn access
@property
def unicorn(self) -> unicorn.Uc
该类允许注册两种挂钩:普通独角兽挂钩和 IAT 挂钩。
# Set unicorn hooks, however the WindowsEmulator instance get passed to the callback instead of unicorn
def set_hook(self, hook_type: int, hook: typing.Callable) -> int:
# Set hook on import call
def enable_iat_hooking(self) -> None:
def set_iat_hook(
self,
function_name: bytes,
hook: typing.Callable[[WindowsEmulator, tuple, dict[str, typing.Any]], None],
) -> None:
作为使用示例,我们使用 Windows 二进制文件 DismHost.exe.
二进制文件使用地址为0x140006404 的睡眠导入:
因此,我们将创建一个脚本,为睡眠导入注册一个 IAT 钩子,在地址0x140006404 开始仿真执行,并在地址0x140006412 结束。
# coding: utf-8
import pathlib
from nightMARE.analysis import emulation
def sleep_hook(emu: emulation.WindowsEmulator, *args) -> None:
print(
"Sleep({} ms)".format(
emu.unicorn.reg_read(emulation.unicorn.x86_const.UC_X86_REG_RCX)
),
)
emu.do_return()
def main() -> None:
path = pathlib.Path(r"C:\Windows\System32\Dism\DismHost.exe")
emu = emulation.WindowsEmulator(False)
emu.load_pe(path.read_bytes(), 0x10000)
emu.enable_iat_hooking()
emu.set_iat_hook("KERNEL32.dll!Sleep", sleep_hook)
emu.unicorn.emu_start(0x140006404, 0x140006412)
if __name__ == "__main__":
main()
值得注意的是,钩子函数必须与do_return 函数一起返回,这样我们才能到达调用后的地址。
当模拟器启动时,我们的钩子就会正确执行。
恶意软件模块
恶意软件模块包含我们涵盖的每个恶意软件系列的所有算法实现。这些算法可以包括配置提取、加密功能或样本解包,具体取决于恶意软件的类型。所有这些算法都使用分析模块的功能来完成它们的工作,并为如何使用库提供了很好的范例。
随着 v0.16 版本的发布,以下是我们涵盖的不同恶意软件系列。
blister
deprecated
ghostpulse
latrodectus
lobshot
lumma
netwire
redlinestealer
remcos
smokeloader
stealc
strelastealer
xorddos
我们在下一章教程中介绍的 LUMMA 算法的完整实现可以在 LUMMA 子模块中找到。
请注意,恶意软件的快速发展给维护这些模块带来了困难,但我们欢迎对项目的任何帮助、直接贡献或开放问题。
示例:LUMMA 配置提取LUMMA 配置提取
LUMMA STEALER 又名 LUMMAC2,是一种信息窃取恶意软件,尽管最近在 2025 年 5 月被清除,但仍广泛用于感染活动。这种恶意软件采用了控制流混淆和数据加密技术,使静态和动态分析更具挑战性。
在本节中,我们将使用以下未加密样本作为参考:26803ff0e079e43c413e10d9a62d344504a134d20ad37af9fd3eaf5c54848122
我们将简要分析它是如何一步步解密域名的,然后演示如何使用 nightMARE 构建配置提取器。
步骤 1:初始化 ChaCha20 上下文
在此版本中,LUMMA 在加载WinHTTP.dll 后,会使用解密密钥和 nonce 执行加密上下文初始化;每次调用ChaCha20 解密函数时,都会重复使用该上下文,而无需重新初始化。这里的细微差别在于,每次使用都会更新上下文中的一个内部计数器,因此稍后我们需要在第一个域解密之前考虑该计数器的值,然后按照正确的顺序对它们进行解密。
要在脚本中重现这一步骤,我们需要收集密钥和非密钥。问题是,我们事先不知道它们的位置,但我们知道它们在哪里使用。我们对这部分代码进行模式匹配,然后从指令中提取地址g_key_0 (key) 和g_key_1 (nonce) 。
CRYPTO_SETUP_PATTERN = "b838?24400b???????00b???0???0096f3a5"
def get_decryption_key_and_nonce(binary: bytes) -> tuple[bytes, bytes]:
# Load the binary in Rizin
rz = reversing.Rizin.load(binary)
# Find the virtual address of the pattern
if not (
x := rz.find_pattern(
CRYPTO_SETUP_PATTERN, reversing.Rizin.PatternType.HEX_PATTERN
)
):
raise RuntimeError("Failed to find crypto setup pattern virtual address")
# Extract the key and nonce address from the instruction second operand
crypto_setup_va = x[0]["address"]
key_and_nonce_address = rz.disassemble(crypto_setup_va, 1)[0]["opex"]["operands"][
1
]["value"]
# Return the key and nonce data
return rz.get_data(key_and_nonce_address, CHACHA20_KEY_SIZE), rz.get_data(
key_and_nonce_address + CHACHA20_KEY_SIZE, CHACHA20_NONCE_SIZE
)
def build_crypto_context(key: bytes, nonce: bytes, initial_counter: int) -> bytes:
crypto_context = bytearray(0x40)
crypto_context[0x10:0x30] = key
crypto_context[0x30] = initial_counter
crypto_context[0x38:0x40] = nonce
return bytes(crypto_context)
步骤 2:找到解密功能
在该版本中,LUMMA 的解密功能在加载 WinHTTP 导入后立即使用,因此可以轻松跨样本定位。
我们从函数的第一个字节推导出十六进制模式,以便在脚本中找到它:
DECRYPTION_FUNCTION_PATTERN = "5553575681ec1?0100008b??243?01000085??0f84??080000"
def get_decryption_function_address(binary) -> int:
# A cache system exist so the binary is only loaded once, then we get the same instance of Rizin :)
if x := reversing.Rizin.load(binary: bytes).find_pattern(
DECRYPTION_FUNCTION_PATTERN, reversing.Rizin.PatternType.HEX_PATTERN
):
return x[0]["address"]
raise RuntimeError("Failed to find decryption function address")
步骤 3:找到加密域的基本地址
通过使用解密函数的 xrefs(该函数不像其他 LUMMA 函数那样以混淆的间接方式调用),我们可以很容易地找到调用该函数解密域的位置。
与第一步一样,我们将使用指令来发现二进制文件中加密域的基地址:
C2_LIST_MAX_LENGTH = 0xFF
C2_SIZE = 0x80
C2_DECRYPTION_BRANCH_PATTERN = "8d8?e0?244008d7424??ff3?565?68????4500e8????ffff"
def get_encrypted_c2_list(binary: bytes) -> list[bytes]:
rz = reversing.Rizin.load(binary)
address = get_encrypted_c2_list_address(binary)
encrypted_c2 = []
for ea in range(address, address + (C2_LIST_MAX_LENGTH * C2_SIZE), C2_SIZE):
encrypted_c2.append(rz.get_data(ea, C2_SIZE))
return encrypted_c2
def get_encrypted_c2_list_address(binary: bytes) -> int:
rz = reversing.Rizin.load(binary)
if not len(
x := rz.find_pattern(
C2_DECRYPTION_BRANCH_PATTERN, reversing.Rizin.PatternType.HEX_PATTERN
)
):
raise RuntimeError("Failed to find c2 decryption pattern")
c2_decryption_va = x[0]["address"]
return rz.disassemble(c2_decryption_va, 1)[0]["opex"]["operands"][1]["disp"]
步骤 4:使用仿真技术解密域
对解密功能的快速分析表明,该版本的 LUMMA 使用了略有定制的ChaCha20 版本。我们在二进制文件中发现了相同的小型解密功能。在这里,它们被用来解密ChaCha20 " 扩展 32 字节 k" 常量的部分内容,然后在存储到上下文结构之前进行 XOR-ROL 推导。
虽然我们可以在脚本中实现解密函数,但我们拥有所有必要的地址,可以演示如何使用 nightMARE 的仿真模块,直接调用二进制文件中已有的函数来解密我们的域。
# We need the right initial value, before decrypting the domain
# the function is already called once so 0 -> 2
CHACHA20_INITIAL_COUNTER = 2
def decrypt_c2_list(
binary: bytes, encrypted_c2_list: list[bytes], key: bytes, nonce: bytes
) -> list[bytes]:
# Get the decryption function address (step 2)
decryption_function_address = get_decryption_function_address(binary)
# Load the emulator, True = 32bits
emu = emulation.WindowsEmulator(True)
# Load the PE in the emulator with a stack of 0x10000 bytes
emu.load_pe(binary, 0x10000)
# Allocate the chacha context
chacha_ctx_address = emu.allocate_memory(CHACHA20_CTX_SIZE)
# Write at the chacha context address the crypto context
emu.unicorn.mem_write(
chacha_ctx_address,
build_crypto_context(
key,
nonce,
CHACHA20_INITIAL_COUNTER,
),
)
decrypted_c2_list = []
for encrypted_c2 in encrypted_c2_list:
# Allocate buffers
encrypted_buffer_address = emu.allocate_memory(C2_SIZE)
decrypted_buffer_address = emu.allocate_memory(C2_SIZE)
# Write encrypted c2 to buffer
emu.unicorn.mem_write(encrypted_buffer_address, encrypted_c2)
# Push arguments
emu.push(C2_SIZE)
emu.push(decrypted_buffer_address)
emu.push(encrypted_buffer_address)
emu.push(chacha_ctx_address)
# Emulate a call
emu.do_call(decryption_function_address, emu.image_base)
# Fire!
emu.unicorn.emu_start(decryption_function_address, emu.image_base)
# Read result from decrypted buffer
decrypted_c2 = bytes(
emu.unicorn.mem_read(decrypted_buffer_address, C2_SIZE)
).split(b"\x00")[0]
# If result isn't printable we stop, no more domain
if not bytes_re.PRINTABLE_STRING_REGEX.match(decrypted_c2):
break
# Add result to the list
decrypted_c2_list.append(b"https://" + decrypted_c2)
# Clean up the args
emu.pop()
emu.pop()
emu.pop()
emu.pop()
# Free buffers
emu.free_memory(encrypted_buffer_address, C2_SIZE)
emu.free_memory(decrypted_buffer_address, C2_SIZE)
# Repeat for the next one ...
return decrypted_c2_list
结果
最后,我们可以通过pytest 运行我们的模块,并查看 LUMMA C2 列表 (decrypted_c2_list):
https://mocadia[.]com/iuew
https://mastwin[.]in/qsaz
https://ordinarniyvrach[.]ru/xiur
https://yamakrug[.]ru/lzka
https://vishneviyjazz[.]ru/neco
https://yrokistorii[.]ru/uqya
https://stolevnica[.]ru/xjuf
https://visokiykaf[.]ru/mntn
https://kletkamozga[.]ru/iwqq
本例重点介绍了 nightMARE 库如何用于二进制分析,特别是从 LUMMA 窃取程序中提取配置。
下载 nightMARE
本文所介绍代码的完整实现可在此处获取。
结论
nightMARE 是一个多功能 Python 模块,基于开源社区提供的最佳工具。我们希望通过 0.16 版的发布和这篇短文,展示它的能力和潜力。
在内部,该项目是各种更加雄心勃勃的项目的核心,我们将继续尽最大努力维护 NightMARE。
