Wslink 恶意软件加载程序-混淆的虚拟机

admin 2025年2月20日23:41:43评论6 views字数 7931阅读26分26秒阅读模式

ESET 研究人员最近描述了Wslink,这是一种独特且以前未记录的恶意加载程序,它作为服务器运行,并具有基于虚拟机的混淆器。没有代码、功能或操作相似性表明这可能是来自已知威胁参与者的工具。

在我们的白皮书(链接如下)中,我们描述了 Wslink 样本中使用的虚拟机的结构,并提出了一种可能的方法来查看分析样本中使用的混淆技术。我们在受保护样本的代码块上演示了我们的方法。我们没有动力去完全去混淆代码,因为我们发现了一个非混淆样本。

https://www.welivesecurity.com/wp-content/uploads/2022/03/eset_wsliknkvm.pdf

混淆技术是一种软件保护,旨在使代码难以理解,从而隐藏其目标;混淆虚拟机技术已被广泛滥用于非法目的,例如混淆恶意软件样本,因为它们阻碍了分析和检测。分析恶意代码并随后提高我们的检测能力的能力是我们克服这些技术的动力背后的驱动力。

虚拟化 Wslink 示例不包含任何明确的工件,例如特定的部分名称,这些工件很容易将其链接到已知的虚拟化混淆器。在我们的研究中,我们成功地设计并实现了一种半自动解决方案,该解决方案能够显着促进对底层程序代码的分析。

该虚拟机引入了多种混淆技术,我们能够克服这些技术以揭示我们在这篇博文中描述的部分去混淆恶意代码。在白皮书的最后几节中,我们展示了我们为促进研究而开发的部分代码。

我们的白皮书还概述了虚拟机的内部结构,并介绍了我们详细分析 Wslink 虚拟机时使用的一些重要术语和框架。

较早的白皮书中,我们描述了自定义虚拟机的结构,以及我们对机器进行虚拟化的技术。该虚拟机包含一个有趣的反反汇编技巧,以前被FinFisher使用过——具有广泛间谍功能的间谍软件,例如通过网络摄像头和麦克风进行实时监视、键盘记录和文件泄露。我们还提出了一种去混淆的方法。

这篇博文摘自 Wslink 多层虚拟机白皮书的幕后花絮;我们鼓励所有对虚拟机和混淆技术感兴趣的人阅读原始白皮书,因为它包含有关了解 Wslink 中使用的混淆技术所需的各种步骤的详细信息。

虚拟机结构概述

在深入分析 Wslink 的虚拟机 (VM) 之前,我们先概述一下虚拟机的内部结构,描述处理此类混淆的已知方法,并介绍我们详细分析中使用的一些重要术语和框架。Wslink 虚拟机。

虚拟机的一般结构

虚拟机可以分为两大类:

  1. 系统虚拟机——支持完整操作系统的执行(例如,各种 VMWare 产品、VirtualBox)

  2. 进程虚拟机——在独立于操作系统的环境中执行单个程序(例如,Java、.NET 公共语言运行时)

在这里,我们只对第二类——进程虚拟机感兴趣——我们将简要描述它们内部结构的某些部分,这是理解本文其余部分所必需的。

进程虚拟机在其主机操作系统上作为普通应用程序运行,然后运行程序,其代码存储为独立于操作系统的字节码(图 1),代表虚拟指令集架构 ( ISA )的一系列指令(应用程序)。

Wslink 恶意软件加载程序-混淆的虚拟机

图 1. 字节码示意图,其中所有操作码和操作数都是虚拟的

人们也可以将字节码视为一种中间表示IR);由特定指令集组成的代码的抽象表示,它更像汇编而不是高级语言。它也被称为中间语言。

IR 的使用在代码可重用性方面很方便——当需要添加对新架构或 CPU 指令集的支持时,将其转换为 IR 会更容易,而不是重新编写所有需要的算法。另一个好处是它可以简化一些优化算法的应用。

通常可以将高级语言和低级语言都翻译成 IR。高级语言的翻译称为“降低”,类似地,低级语言的翻译称为“提升”。

以下示例将装配块 bb0 提升为具有伪 IR 代码irb0的块。所有的汇编指令都被翻译成一组 IR 操作,集合中的单个操作互不影响,其中ZF代表零标志,CF代表进位标志:

bb0:
  MOV R8, 0x05
  SUB AX, DX
  XCHG ECX, EDX
irb0:
  R8 = 0x05

  EAX[:0x10] = EAX[:0x10] – EDX[:0x10]
  ZF = EAX[:0x10] – EDX[:0x10] == 0x00
  CF = EAX[:0x10] < EDX[:0x10]
  ...

  ECX = EDX
  EDX = ECX

现代进程虚拟机通常提供一个编译器,该编译器可以将用高级语言(一种易于理解且使用舒适的语言)编写的代码降低到相应的字节码中。

VM 的ISA通常定义了支持的指令、数据类型和寄存器等,当然这些也必须由虚拟 ISA 实现。

指令由以下部分组成:

  • opcodes – 指定指令的操作码

  • 操作数——指令的参数

ISA 通常使用两个众所周知的虚拟寄存器:

  • 虚拟程序计数器(VPC) – 指向字节码中当前位置的指针

  • 虚拟堆栈指针——指向虚拟机内部使用的预分配虚拟堆栈空间的指针

虚拟堆栈指针不必存在于所有虚拟机中;它仅在特定类型的 VM 中很常见——基于堆栈的 VM 。

我们将虚拟 ISA 的指令及其各自部分简称为虚拟指令虚拟操作码虚拟操作数当我们显然在谈论虚拟表示时,我们有时会忽略“虚拟”的显式使用。

依赖于操作系统(图 2)的可执行文件——解释器——处理提供的字节码并顺序解释底层虚拟指令,从而执行虚拟化程序。

Wslink 恶意软件加载程序-混淆的虚拟机

图 2. 字节码和 VM 解释器之间的关系示意图

每个虚拟机都需要在解释期间将控制从一条虚拟指令转移到下一条。这个过程通常称为调度有几种记录在案的调度技术,例如:

  • Switch Dispatch – 最简单的调度机制,其中将虚拟指令定义为 case 子句,并将虚拟操作码用作测试表达式(图 3)

  • 直接调用线程——虚拟指令被定义为函数,虚拟操作码包含这些函数的地址

  • 直接线程——虚拟指令再次被定义为函数;然而,与直接调用线程相比,函数的地址存储在一个表中,虚拟操作码表示该表的偏移量。每个函数都应根据规范间接调用以下函数(图4)

解释器代码中的虚拟操作码主体通常称为虚拟处理程序,因为它定义了操作码的行为,并在虚拟程序计数器指向字节码中包含具有该操作码的虚拟指令的位置时处理它。

上下文,关于虚拟机,我们指的是一种虚拟进程上下文:在进程切换期间,每次进程从对处理器的访问中移除时,必须存储有关其当前操作状态的足够信息——它的上下文——以便当它再次出现时计划在处理器上运行,它可以从相同的位置恢复其操作。

Wslink 恶意软件加载程序-混淆的虚拟机

图 3. Switch Dispatch 示意图,其中 R0 是一个虚拟寄存器

Wslink 恶意软件加载程序-混淆的虚拟机

图 4. 直接穿线示意图

混淆技术是一种软件保护,旨在使代码难以理解,从而隐藏其目标。这种技术最初是为了保护合法软件的知识产权而开发的,例如,用来阻止逆向工程。

如上所述,用作混淆引擎的虚拟机基于进程虚拟机。主要区别在于它们不打算运行跨平台应用程序,它们通常采用为已知 ISA 编译或组装的机器代码,将其反汇编,然后将其转换为自己的虚拟 ISA。通常情况下,VM 环境和虚拟化应用程序代码都包含在一个应用程序中,而传统进程 VM 通常由一个进程组成,该进程作为独立应用程序运行,加载单独的虚拟化应用程序

这种混淆技术的优势在于虚拟机的 ISA 对任何潜在的逆向工程师都是未知的——需要对虚拟机进行彻底的分析,这可能非常耗时,需要理解虚拟指令的含义和VM 的其他结构。此外,如果性能不是问题,则可以将 VM 的 ISA 设计为任意复杂,从而减慢其虚拟化应用程序的执行速度,但会使逆向工程更加复杂。理解虚拟机对于解码字节码和使虚拟化代码易于理解是必要的。

上下文在混淆虚拟机方面有一点不同的含义:每次我们想要从本地 ISA 切换到虚拟 ISA 时,必须存储足够的关于当前操作状态的信息 - 上下文 - 以便当 lSA必须切换回来,只需要修改相关数据和寄存器即可恢复执行。

此外,混淆虚拟机通常只虚拟化某些“有趣”的功能——本机上下文映射到虚拟上下文,并预先选择代表相应功能的字节码。之后调用内置解释器(图 5)。原始函数的开头包含准备和执行解释器的代码——VM 的入口(vm_entry);图 5 中省略了他们的其余代码。

带有混淆虚拟机数据的解释器、字节码和虚拟 ISA 代码通常与部分虚拟化程序的其余部分一起存储在可执行二进制文件的专用部分中。

图 5 显示了针对普通 ISA 的原始应用程序中的函数Function 1可以为混淆 VM 的 ISA 虚拟化的方式。它需要转换为字节码,例如使用 generate_bytecode 方法。它的主体随后被调用vm_entry和零覆盖。vm_entry函数选择相应的字节码,例如根据调用函数的地址,然后进行上下文切换,然后解释字节码。最后,它返回到虚拟化函数Function 1将返回的代码。

Wslink 恶意软件加载程序-混淆的虚拟机

图 5. 虚拟化过程概述

在 x86 架构上托管的 VM 中,此类上下文切换通常由一系列 PUSH 和 POP 指令组成。例如:

PUSH EAX
PUSH EBX
PUSH ECX
...
MOV ECX, context_addr
POP DWORD PTR [ECX]
POP DWORD PTR [ECX + 4]
POP DWORD PTR [ECX + 8]
...

当字节码被完全处理后,虚拟上下文被映射回本机上下文,并在非虚拟化代码中继续执行;但是,可以立即以相同的方式执行另一个虚拟化功能。

请注意,在一个虚拟化函数中可能会发生多个上下文切换,例如,当来自原始 ISA 的本机指令无法转换为虚拟指令或需要执行来自本机 API 的未知函数时。

wslink的虚拟机入口——vm_entry

下面我们来分析一下 Wslink 的 VM。有几个函数调用进入虚拟机,所有这些调用之后都是 IDA 试图反汇编的一些乱码数据——这些数据很可能只是覆盖了虚拟化之前的函数原始代码(图 6)。

Wslink 恶意软件加载程序-混淆的虚拟机

图 6. 虚拟机的入口点

虚拟机的vm_entry

  • 通过从代码中某个位置的实际虚拟地址中减去预期的相对虚拟地址来计算实际基地址

  • 在第一次运行时解压与 VM 相关的代码和数据;它使用计算出的基地址来确定打包 VM 的位置和解压数据的目的地

  • 执行初始化函数——要描述的vm_pre_init()函数之一是基于映射到相应vm_pre_init()的调用者的相对地址

打包机

Wslink 的 VM 打包了NsPack以减少巨大的可执行文件的大小;额外的混淆可能只是一个副作用。Wslink 的解包代码和 ClamAV 的unspack()函数之间的相似之处清晰可见(图 7 和图 8)。请注意,Ghidra 已经优化了基地址的计算。

Wslink 恶意软件加载程序-混淆的虚拟机

图 7. 用 Ghidra 反编译的虚拟机 vm_entry 的一部分

Wslink 恶意软件加载程序-混淆的虚拟机

图 8. 用于在 ClamAV 中解包 NsPack 的函数

图 7 中vm_pre_init_dispatch_table是将调用者的vm_entry地址映射到要描述的相应vm_pre_init()函数的结构。

虚拟机初始化

VM 的初始化包括几个步骤,例如将本机寄存器的值保存在堆栈上,然后将它们移动到虚拟上下文,重新定位其内部结构或准备字节码。我们将在以下小节中更全面地介绍这些步骤。

vm_pre_init() 函数

vm_pre_init()函数仅用于为另一个初始化阶段准备参数(图 9)。这些函数调用带有特定参数的单个vm_init()函数(将在下一节中解释)。提供的参数是:

  • CPU 标志,在每个函数的开头使用PUSHF指令存储在堆栈中

  • 虚拟指令表的硬编码偏移量,表示要执行的第一条虚拟指令(其操作码)

  • 要解释的字节码的硬编码地址

Wslink 恶意软件加载程序-混淆的虚拟机

图 9. Miasm 对 vm_pre_init() 的符号执行显示提供给 vm_init() 的参数

vm_init() 函数

vm_init()将所有本地寄存器和提供的 CPU 标志从参数(上下文)推送到堆栈上。本机上下文稍后将移动到虚拟上下文,此外,它还包含多个内部寄存器。

其中一个内部寄存器确定 VM 的另一个实例是否已经在运行——只有一个全局虚拟上下文,并且一次只能运行一个 VM 实例。图 10 显示了部分代码忙于等待虚拟寄存器,其中RBP包含虚拟上下文的地址,而 RBX 是虚拟寄存器的偏移量——内部寄存器存储在[RBX + RBP]中。

整个函数总结在图 11 中。

Wslink 恶意软件加载程序-混淆的虚拟机

图 10. 在 vm_init() 中忙于等待解释器

参数中提供的字节码地址与硬编码的虚拟指令表的地址一起添加到虚拟上下文中。两者都有一个专用的虚拟寄存器。

VM 以与vm_entry相同的方式再次计算基地址此外,它会将地址存储在另一个内部寄存器中,以便稍后调用 API。然后基地址用于重定位指令表、它的条目和字节码的地址。

如果尚未重新定位,则计算出的基地址将简单地添加到所有函数地址。

Wslink 恶意软件加载程序-混淆的虚拟机

图 11. vm_init() 摘要

第二个虚拟机的虚拟指令

我们首先查看前几条执行的虚拟指令以观察第二个 VM 的行为,然后尝试以部分自动化的方式处理其余的指令。

图 12 中的图表以蓝色突出显示,其中第二个 VM 的虚拟指令位于 VM 的结构中。

Wslink 恶意软件加载程序-混淆的虚拟机

图 12. 虚拟机结构中的虚拟指令

第一条虚拟指令

如图 13 所示,第一条虚拟指令异常地没有被混淆。最后,我们可以看到虚拟上下文中的一些操作。

通过检查修改后的内存和计算出的指令目标地址,很明显指令做了三件事:

  • 将虚拟上下文中偏移量0xB5处的虚拟 32 位寄存器清零(在图 13 中以灰色突出显示),该寄存器存储在RBP寄存器中

  • 偏移量0x28处的虚拟 64 位寄存器增加了0x04:它是指向字节码的指针——虚拟程序计数器。因此,虚拟指令的大小为四个字节(在图 13 中以红色突出显示)。

  • 准备执行下一条虚拟指令,从虚拟程序计数器获取虚拟指令表的偏移量——虚拟操作码。虚拟指令表位于偏移量0xA4(在图 13 中以绿色突出显示)。这意味着 VM 使用 Direct Threading Dispatch 技术。

Wslink 恶意软件加载程序-混淆的虚拟机

图 13. 第二个 VM 的初始虚拟指令

请注意,下一条指令的操作码大小只有两个字节,其余字未使用。当我们查看虚拟操作数时,我们可以看到它只是一个零(图 14)。其他指令的大小不同——不仅仅是填充为所有指令保留相同的大小。

Wslink 恶意软件加载程序-混淆的虚拟机

图 14. 虚拟指令的字节码

第二条虚拟指令

第二条虚拟指令没有做任何特别的事情;它只是将几个虚拟寄存器清零并跳转到下一条指令(图 15)。

Wslink 恶意软件加载程序-混淆的虚拟机

图 15. 第二条虚拟指令修改的目标地址和内存

第三条虚拟指令

第三条虚拟指令将堆栈指针的地址存储在虚拟寄存器中(图 16);寄存器的偏移量由操作数之一决定,在我们的例子中它的偏移量是0x0141

Wslink 恶意软件加载程序-混淆的虚拟机

图 16. 第三条虚拟指令修改的目标地址和内存

第四条虚指令

与之前的指令相比,第四条指令包含两个立即可见的异常——堆栈指针的增量在函数末尾较低,并且包含一个条件分支(图 17)。

Wslink 恶意软件加载程序-混淆的虚拟机

图 17. 第四条虚拟指令的堆栈指针的条件分支和增量

第一个块的符号执行表明一个值从堆栈弹出到一个虚拟寄存器(图 18),这是有道理的,因为本机寄存器的值在被vm2_init()保存在堆栈中后仍保留在堆栈上。它们现在被移至虚拟上下文——上下文切换部分由许多虚拟指令执行,每条虚拟指令都会将一个值从堆栈中弹出到不同的寄存器中。

Wslink 恶意软件加载程序-混淆的虚拟机

图 18. 第四条虚拟指令修改的目标地址和内存

本地寄存器的值将被保存到的虚拟寄存器由一个操作数和另外两个位于偏移量 0x0B 和 0x70 的虚拟寄存器确定。但是,它们的初始值是已知的:它们被第二条虚拟指令设置为零(图 15),这意味着我们可以计算寄存器的偏移量并简化表达式——它们仅用于混淆代码。

滚动解密

对其他虚拟指令的分析证实,偏移量 0x0B 和 0x70 处的虚拟寄存器仅用于编码操作数。这种技术称为滚动解密,并且已知被VMProtect混淆器使用。但是,它是与该混淆器的唯一重叠,我们非常有信心该 VM 是不同的。

混淆技术无疑是产生大量虚拟指令的原因之一——使用该技术需要复制单个指令,因为每个指令都使用不同的密钥来解码操作数。

简化

当我们应用虚拟寄存器的已知值时,表达式可以简化为:

IRDst = (-@16[@64[RBP_init + 0x28] + 0x4] ^ 0x3038 == @16[@64[RBP_init + 0x28] + 0x6])?(0x7FEC91ABD1C,0x7FEC91ABCF6)

@64[RBP_init + {-@16[@64[RBP_init + 0x28] + 0x4] ^ 0x3038, 0, 16, 0x0, 16, 64}] = @64[RSP_init]

现在让我们看一下条件块中的表达式:

@64[RBP_init + {@16[@64[RBP_init + 0x28] + 0x6], 0, 16, 0x0, 16, 64}] = @64[RBP_init + {@16[@64[RBP_init + 0x28] + 0x6 ], 0, 16, 0x0, 16, 64}] + 0x8

我们现在可以看到虚拟指令肯定是 POP——它将一个值从堆栈顶部移到一个虚拟寄存器,其偏移量仍然被简单的 XOR 混淆;当目标寄存器不是堆栈指针时,它还会增加堆栈指针。

由于字节码中的值也是已知的,我们可以应用它们并将指令进一步简化为以下最终无条件表达式:

IRDst = @64[@64[RBP_init + 0xA4] + 0x5A8]
@64[RBP_init + 0x28] = @64[RBP_init + 0x28] + 0x8
@64[RBP_init + 0x141] = @64[RBP_init + 0x141] + 0x8
@ 64[RBP_init + 0x12A] = @64[RSP_init]

自动分析虚拟指令

由于对超过 1000 条指令执行此操作非常耗时,因此我们使用 Miasm 编写了一个 Python 脚本,为我们收集这些信息,以便我们更好地了解正在发生的事情。我们对修改后的内存和目标地址特别感兴趣。

就像在第四条虚拟指令中一样,我们将某些虚拟寄存器作为具体值来检索清晰的表达式。这些寄存器专用于滚动解密并执行与字节码指针相关的内存访问,例如[<obf_reg_1>] = [<bytecode_ptr> + 0x05] ^ 0xABCD

随后,我们也将指向虚拟指令表的指针具体化,并在虚拟指令结束时:计算下一条的地址,清除符号状态,并从以下虚拟指令开始。

我们还保留了与 VM 内部寄存器无关的内存分配,并逐步构建基于虚拟程序计数器的图形(图 19)。

Wslink 恶意软件加载程序-混淆的虚拟机

图 19. 从内存分配和 VPC 生成的调用图

当我们不能明确地确定下一个要执行的虚拟指令时,我们就会停止;可以通过这种方式自动处理大部分虚拟指令。

请注意,由于符号执行的路径爆炸问题,无法确定地处理具有复杂循环的指令,并且需要单独解决,例如在论文需求驱动的组合符号执行中描述了这一点:“系统地执行符号化所有可行的程序路径不能扩展到大型程序。事实上,可行路径的数量可以是程序大小的指数,甚至在存在无限次迭代的循环时是无限的。”

有关虚拟指令和虚拟机初始化的其他操作,请参阅 ESET Research 白皮书 Under the hood of Wslink 的多层虚拟机。

结论

我们已经描述了 Wslink 中的高级多层虚拟机的内部结构,并成功设计并实现了一个半自动解决方案,该解决方案能够显着促进程序代码的分析。

该虚拟机引入了其他几种混淆技术,例如垃圾代码、虚拟操作数编码、虚拟操作码重复、不透明谓词、虚拟指令合并和嵌套虚拟机,以进一步阻碍对其保护的代码的逆向工程,但我们成功地克服了它们。

为了处理混淆,我们修改了一种已知技术,该技术使用简化规则的符号执行来提取虚拟操作码的语义。此外,我们将用于混淆的内部虚拟寄存器以及与虚拟程序计数器相关的内存访问具体化,以自动应用已知值和去混淆虚拟指令的语义——这也打破了各个虚拟指令之间的界限。

边界对于防止符号执行的路径爆炸是必要的;如果没有它们,我们将失去对虚拟程序计数器的跟踪——我们在解释代码中的位置。

我们通过符号化虚拟指令表的地址来定义新的边界,因为它需要获取下一条指令,并且只有在我们需要移动到下面的虚拟指令时才将其具体化。我们随后基于虚拟程序计数器从其中一个字节码块中以中间表示形式构建了原始代码的控制流图,并提取了各个虚拟指令的去混淆语义。我们最终通过将嵌套虚拟机完全具体化,扩展了同时处理两个虚拟机的方法。再次重申:有关详细信息,请参阅我们的白皮书。

Wslink 恶意软件加载程序-混淆的虚拟机

Wslink 恶意软件加载程序-混淆的虚拟机

原文始发于微信公众号(安全狗的自我修养):Wslink 恶意软件加载程序-混淆的虚拟机

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月20日23:41:43
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Wslink 恶意软件加载程序-混淆的虚拟机https://cn-sec.com/archives/1021966.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息