导语:最近在Windows 10的19H1(1903版)版本中发生了一件非常令人激动的事情,经过多年的讨论,Intel“控制流实施技术”(CET)的终于可以实现了。
前一段时间,我们做了一些研究。该特定项目可能会在将来的其他时间发布,我们在这里不会对此进行过多介绍。但是,作为该项目的一部分,我们希望访问某些驱动程序使用的内部数据结构。可悲的是,没有导出驱动程序指向该数据结构的全局指针,并且我们找不到从驱动程序本身外部访问它的方法。它存储在池中,因此我们甚至无法扫描驱动程序地址空间以查看此结构的迹象。
然后,总是可以选择基于引用全局函数的函数签名对驱动程序进行二进制解析,或对全局变量使用已知偏移量的数组,并添加驱动程序库以找到它。但是,这些方法需要为每个版本的驱动程序以及所有潜在的功能签名找到并使用正确的RVA。由于此驱动程序没有导出功能,因此此类签名会很脆弱,并且在发行版之间可能会发生变化。
因此,我们对数据结构本身进行了逆向工程,并提出了一个有趣的想法,可以使我们轻松访问此数据结构以及其他许多结构。我们感兴趣的数据结构非常大,除此之外,还包含一些嵌入其中的后备列表。后备列表是包含固定大小的池分配的单个链接列表。驱动程序将它们用于缓存内存分配,而不是始终从内存管理器中请求它们。
系统后备列表
这是GENERAL_LOOKASIDE_LAYOUT的wdm.h定义(GENERAL_LOOKASIDE只是GENERAL_LOOKASIDE_LAYOUT的对齐版本):
// // The goal here is to end up with two structure types that are identical except // for the fact that one (GENERAL_LOOKASIDE) is cache aligned, and the other // (GENERAL_LOOKASIDE_POOL) is merely naturally aligned. // // An anonymous structure element would do the trick except that C++ can't handle // such complex syntax, so we're stuck with this macro technique. // #define GENERAL_LOOKASIDE_LAYOUT \ union { \ SLIST_HEADER ListHead; \ SINGLE_LIST_ENTRY SingleListHead; \ } DUMMYUNIONNAME; \ USHORT Depth; \ USHORT MaximumDepth; \ ULONG TotalAllocates; \ union { \ ULONG AllocateMisses; \ ULONG AllocateHits; \ } DUMMYUNIONNAME2; \ \ ULONG TotalFrees; \ union { \ ULONG FreeMisses; \ ULONG FreeHits; \ } DUMMYUNIONNAME3; \ \ POOL_TYPE Type; \ ULONG Tag; \ ULONG Size; \ union { \ PALLOCATE_FUNCTION_EX AllocateEx; \ PALLOCATE_FUNCTION Allocate; \ } DUMMYUNIONNAME4; \ \ union { \ PFREE_FUNCTION_EX FreeEx; \ PFREE_FUNCTION Free; \ } DUMMYUNIONNAME5; \ \ LIST_ENTRY ListEntry; \ ULONG LastTotalAllocates; \ union { \ ULONG LastAllocateMisses; \ ULONG LastAllocateHits; \ } DUMMYUNIONNAME6; \ ULONG Future[2];
需要注意的一个有用事实是,此结构包含一个链接列表(GENERAL_LOOKASIDE.ListEntry),意味着所有后备列表都将这样做。根据后备列表是使用ExInitializeNPagedLookasideList还是ExInitializePagedLookasideList创建的(或者,如果使用ExInitializeLookasideListEx,则传入的PoolType)将数据结构输入两个列表头之一。这样,如果我们遵循任何后备列表的ListEntry,我们最终将以ExPagedLookasideListHead或ExNPagedLookasideListHead结尾。由于我们通过这些API创建了自己的后备列表,因此,如果我们选择与目标结构相同的池类型,那么我们就可以遍历所有其他后备面,最终到达目标结构中包含的后备列表。在此特定用例中,使用我们自己的结构定义,有用的CONTAINING_RECORD宏,以及知道结构的第一个成员是始终包含相同值的“魔术” ULONG,我们使用此机制搜索了所有后备列表直到我们达到结构。
但是可能性不止于此–这种方法使我们可以访问任何包含后备列表的内核结构(无论是否导出)。那还有什么呢?
基于池的后备列表
借助一些WinDbg魔术,我们还可以找到有关数据的有价值的信息-无论是在驱动程序内部(以及哪个驱动程序!)还是在内核池中,它属于谁,分配大小等。为探索可能性,我们编写了一个简单的WinDbg脚本,该脚本遍历所有后备列表,并使用非常有用的!pool扩展来转储有关它们的信息。尽管我们可以在自定义C驱动程序中构建类似的功能,但是没有Windows Kernel API可以为我们提供有关池分配和解析池页面以进行检索的类似信息,这是很多工作,因此我们决定避免实现相同的功能在C中由于懒惰。实际上,当我们尝试实现自己的基于C的池解析器时,我们最终意识到没有人描述Windows 10 RS5及更高版本的池管理器中的无数变化,因此我们忙于编写有关该主题的书。
使用我们的脚本,我们发现了包含后备列表的结构,这些后备列表属于FltMgr.sys,Win32k.sys,Windows Defender驱动程序,各种显示驱动程序等等。
dx -r0 @$GeneralLookaside = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPagedLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry") dx -r0 @$lookasideAddr = @$GeneralLookaside.Select(l => ((__int64)&l).ToDisplayString("x")) dx -r0 @$extractBetween = ((x,y,z) => x.Substring(x.IndexOf(y) + y.Length, x.IndexOf(z) - x.IndexOf(y) - y.Length)) dx -r0 @$extractWithSize = ((x,y,z) => x.Substring(x.IndexOf(y) + y.Length, z)) dx -r2 @$poolData = @$lookasideAddr.Select(l => Debugger.Utility.Control.ExecuteCommand("!pool "+l+" 2")).Where(l => l[1].Length != 0x55 && l[1].Length != 0).Select(l => new {address = "0x" + @$extractBetween(l[1], "*", "size:"), tag = @$extractWithSize(l[1], "(Allocated) *", 4), tagDesc = l[2].Contains(",") ? @$extractBetween(l[2], ": ", ",") : l[2].Substring(l[2].IndexOf(":")+2), binary = l[2].Contains("Binary") ? l[2].Substring(l[2].IndexOf("Binary :")+9) : "unknown", size = "0x" + @$extractBetween(l[1], "size:", "previous size:").Replace(" ", "")}) [0x4a] address : 0xffff988679939400 tag : Vi10 tagDesc : Video memory manager process heap binary : dxgmms2.sys size : 0x70 [0x4b] address : 0xffff98867b647650 tag : DxgK tagDesc : Vista display driver support binary : dxgkrnl.sys size : 0x640 [0x4c] address : 0xffff98867b647650 tag : DxgK tagDesc : Vista display driver support binary : dxgkrnl.sys size : 0x640 [0x4d] address : 0xffff9886790f5430 tag : Vi17 tagDesc : Video memory manager pool binary : dxgmms2.sys size : 0x150 [0x4e] address : 0xffff98867966e230 tag : Usla tagDesc : USERTAG_LOOKASIDE binary : win32k!InitLockRecordLookaside size : 0xa0 [0x4f] address : 0xffff98867966ea50 tag : Usla tagDesc : USERTAG_LOOKASIDE binary : win32k!InitLockRecordLookaside size : 0xa0 [0x50] address : 0xffff98867966e690 tag : Gla1 tagDesc : GDITAG_HMGR_LOOKASIDE_DC_TYPE binary : win32k.sys size : 0xa0 [0x51] address : 0xffff98867966e550 tag : Gla4 tagDesc : GDITAG_HMGR_LOOKASIDE_RGN_TYPE binary : win32k.sys size : 0xa0 [0x52] address : 0xffff98867966ecd0 tag : Gla5 tagDesc : GDITAG_HMGR_LOOKASIDE_SURF_TYPE binary : win32k.sys size : 0xa0
在某些结果中,pool标签是未知的,因此很难跟踪它们所属的驱动程序。一种有趣的解决方法是使用驱动程序验证程序的池跟踪功能。我们可以修改脚本,并用!verifier < address > 2替换!pool < address > 2命令,并接收有关分配驱动程序的信息以及分配的完整堆栈跟踪。但是,在如此多的地址上运行此命令非常慢,并且会转储很多难以整理的信息。因此,另一个选择是采用一种更加手动的方法-启用驱动程序验证程序,但按原样执行以前的脚本,并且仅查询验证程序似乎很有趣的特定地址。
基于图像的后备列表
最初,我们仅在池中搜索数据,因为这是分配我们感兴趣的结构的位置。但是通过此技巧,我们还可以访问驱动程序内部的后备列表,并且我们可以使用很酷的新RtlPcToFileName函数来找出这些结构所在的驱动程序。在这种情况下,我们确实选择了用C代码实现此功能,因为它的功能更多。简单,快速地执行:
_Use_decl_annotations_ NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NTSTATUS status; LOOKASIDE_LIST_EX lookaside; PLIST_ENTRY lookasideList; PLIST_ENTRY lookasideListHead; PGENERAL_LOOKASIDE generalLookaside; UNICODE_STRING pcName = RTL_CONSTANT_STRING(L"RtlPcToFileName"); DECLARE_UNICODE_STRING_SIZE(driverName, 32); UNREFERENCED_PARAMETER(RegistryPath); DriverObject->DriverUnload = DriverUnload; auto RtlPcToFileNamePtr = (decltype(RtlPcToFileName)*)(MmGetSystemRoutineAddress(&pcName)); NT_ASSERT(RtlPcToFileNamePtr != nullptr); // // Create our own lookaside list to use for finding other lookaside lists in the kernel. // status = ExInitializeLookasideListEx(&lookaside, nullptr, nullptr, PagedPool, 0, 8, 'Fake', 0); if (!NT_SUCCESS(status)) { goto Exit; } // // Iterate over our lookaside list to find all the other lookaside lists // and print information about them // generalLookaside = nullptr; lookasideListHead = &lookaside.L.ListEntry; lookasideList = lookasideListHead->Flink; do { generalLookaside = CONTAINING_RECORD(lookasideList, GENERAL_LOOKASIDE, ListEntry); // // Use RtlPcToFileName to find whether the lookaside list is // inside a driver and if so, which one // status = RtlPcToFileNamePtr(generalLookaside, &driverName); if (NT_SUCCESS(status)) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Lookaside list is in driver %wZ\n", driverName); } else { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, “Lookaside list is not inside a driver\n”); } lookasideList = lookasideList->Flink; } while (lookasideList != lookasideListHead); status = STATUS_SUCCESS; Exit: ExDeleteLookasideListEx(&lookaside); return status; }
通过此代码,我们在Ntoskrnl.exe,Ci.dll,Ntfs.sys等中找到了后备列表。当然,由于它们是嵌入在驱动程序内存中的,因此了解它们是独立的后备列表还是它们是较大结构的一部分的唯一方法是转储地址并对驱动程序进行反向工程。但是我们都是喜欢逆向工程的书呆子,或者我们不会写/读这个博客。
如果愿意,我们还可以使用ln命令在WinDbg中实现相同的查询,该命令搜索与地址最接近的符号:
dx -r0 @$GeneralLookaside = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPagedLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry") dx -r0 @$lookasideAddr = @$GeneralLookaside.Select(l => ((__int64)&l).ToDisplayString("x")) dx -r2 @$symData = @$lookasideAddr.Select(l => new {addr = l, sym = Debugger.Utility.Control.ExecuteCommand("ln "+l)}).Where(l => l.sym.Count() > 3).Select(l => new {addr = l.addr, sym = @$extractBetween(l.sym[3], " ", "|")}) [0x9] addr : 0xfffff8000e4eb300 sym : nt!AlpcpLookasides+0x100 [0xa] addr : 0xfffff8000e4db180 sym : nt!IopSymlinkInfoLookasideList [0xb] addr : 0xfffff8000e4ef040 sym : nt!WmipDSChunkInfoLookaside [0xc] addr : 0xfffff8000e4eefc0 sym : nt!WmipGEChunkInfoLookaside [0xd] addr : 0xfffff8000e4ef140 sym : nt!WmipISChunkInfoLookaside [0xe] addr : 0xfffff8000e4ef0c0 sym : nt!WmipMRChunkInfoLookaside [0xf] addr : 0xfffff8001172a880 sym : FLTMGR!FltGlobals+0x340 [0x10] addr : 0xfffff8001172ad00 sym : FLTMGR!FltGlobals+0x7c0 [0x11] addr : 0xfffff8001172af00 sym : FLTMGR!FltGlobals+0x9c0 [0x12] addr : 0xfffff8001172b080 sym : FLTMGR!FltGlobals+0xb40
这是一个很酷的技巧,导致了各种各样的有趣发现。而且,我们仅搜索分页的后备列表。整个世界都没有页面调度的后备列表,我们甚至都没有看过。我们运行与以前相同的WinDbg脚本,只是将我们的起点从nt!ExPagedLookasideListHead更改为nt!ExNPagedLookasideListHead以获取非分页的后备列表,并得到了一些有趣的结果。我们在池中寻找未分页的后备列表:
[0x55] address : 0xffff97884ba5c990 tag : Vkin tagDesc : Hyper-V VMBus KMCL driver (incoming packets) binary : vmbkmcl.sys size : 0x2d0 [0x56] address : 0xffff97884bad1590 tag : NDnd tagDesc : NDIS_TAG_POOL_NDIS binary : ndis.sys size : 0x800 [0x57] address : 0xffff97884bad3000 tag : NDrt tagDesc : NDIS_TAG_RST_NBL binary : ndis.sys size : 0x800 [0x58] address : 0xffff97884ba19130 tag : Nnbf tagDesc : NetIO NetBufferLists binary : netio.sys size : 0x800
在驱动程序内部:
[0x14] addr : 0xfffff8000e4db100 sym : nt!IopOplockFoExtLookasideList [0x15] addr : 0xfffff8000e4ee880 sym : nt!WmipRegLookaside [0x16] addr : 0xfffff80010e40bc0 sym : ACPI!BuildRequestLookAsideList [0x17] addr : 0xfffff80010e40dc0 sym : ACPI!RequestLookAsideList [0x18] addr : 0xfffff80010e40c40 sym : ACPI!DeviceExtensionLookAsideList [0x19] addr : 0xfffff80010e40d40 sym : ACPI!RequestDependencyLookAsideList [0x1a] addr : 0xfffff80010e40cc0 sym : ACPI!ObjectDataLookAsideList [0x17] addr : 0xfffff80010e40f40 sym : ACPI!XswContextLookAsideList
处理器后备列表
实际上,还有一个我们尚未讨论的后备列表的链接列表:ExPoolLookasideListHead。从Windows NT的第一个版本开始,直到Windows 10 RS5重写池管理器以使用后端堆,它都利用了32个后备列表的每个处理器阵列,一个对于池块大小的每个索引倍数。在x86上,这基本上意味着8到256字节之间的任何8字节对齐分配,而在x64上,这是16到512字节之间的任何16字节对齐分配。
由于既有页面缓冲池又有非页面缓冲池,因此每个KPRCB都有两个这样的数组-PPNPagedLookasideList和PPPagedLookasideList。使用Windows 8并引入了不可执行的非页面缓冲池,创建了第三个数组:PPNxPagedLookasideList。因此,所有这些后备列表都插入到相同的链接列表头中,并且在我们的系统上,您可以轻松地看到存在多少个处理器(16):
lkd> dx -r0 @$poolasides = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPoolLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry") @$poolasides = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExPoolLookasideListHead, "nt!_GENERAL_LOOKASIDE", "ListEntry") lkd> dx @$poolasides.Count(), d @$poolasides.Count(), d : 1536 lkd> dx 1536 / 32 / 3 1536 / 32 / 3 : 16 lkd> dx *(int*)&nt!KeNumberProcessors *(int*)&nt!KeNumberProcessors : 16 [Type: int]
最初,这看起来很令人兴奋,因为这意味着不仅可以轻松地找到包含后备列表的结构,而且实际上可以找到是池块大小倍数的任何池结构的能力。不幸的是,如果我们查看一下现代Windows 10系统上的这些列表,就会发现它们完全没有使用:
lkd> dx @$poolasides.Sum(p => p.TotalAllocates + p.TotalFrees) @$poolasides.Sum(p => p.TotalAllocates + p.TotalFrees) : 0x0
确实,通过查看ExAllocatePoolWithTag和朋友中的代码,此逻辑已作为与堆相关的更改的一部分而被完全删除,我们将在以后的研究论文中进行介绍。
更酷的是,后备列表并不是链接到相同类型的所有其他结构的唯一内核结构!另一个示例是ERESOURCE,该结构用于实现驱动程序的读/写锁定。执行资源也包含在许多内核结构中,如果我们知道如何找到它们,它们可以使我们访问更多内部内核信息。我们更改了WinDbg脚本,以遍历ERESOURCE.SystemResourcesList中从nt!ExpSystemResourcesList开始的链接列表。
我们首先在池中搜索ERESOURCE对象:
[0xb8] address : 0xffff97884bf9fb90 tag : Ntfx tagDesc : Unrecognized NTFS tag (update base\published\pooltag.w) binary : ntfs.sys size : 0x170 [0xb9] address : 0xffff97884bf50e80 tag : SeTl tagDesc : Security Token Lock binary : nt!se size : 0x80 [0x4c] address : 0xffff97884bf9ed30 tag : Ntfx tagDesc : Unrecognized NTFS tag (update base\published\pooltag.w) binary : ntfs.sys size : 0x170
然后对于驱动程序内部的ERESOURCE对象:
[0x3c] addr : 0xfffff8001268e8e0 sym : Ntfs!NtfsDynamicRegistrySettingsResource [0x3d] addr : 0xfffff80011211ef0 sym : NDIS!SharedMemoryResource [0x3e] addr : 0xfffff80012967630 sym : ksecpkg!g_rgCachedPagedSslProvs+0x410 [0x3f] addr : 0xfffff80011a032f8 sym : tcpip!FlIsolationState+0x18 [0x40] addr : 0xfffff80011d482e0 sym : mup!MupProviderTable+0x20 [0x41] addr : 0xfffff80011d48100 sym : mup!MupiSurrogateList+0x20 [0x42] addr : 0xfffff8000f4ac370 sym : CI!g_IgnoreLifetimeSigningEKU+0x70 [0x43] addr : 0xfffff8000f4acb80 sym : CI!g_GRLContextLock [0x44] addr : 0xfffff80012f081c0 sym : netbios!g_erGlobalLock
我们发现了一些非常有趣的结果,可能值得进一步研究,例如与NTFS卷对象相关的池结构,Ci.dll内部的结构等等。在我们的机器上,我们发现了超过40万个执行资源:
lkd> dx -r0 @$eresource = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExpSystemResourcesList, "nt!_ERESOURCE", "SystemResourcesList") @$eresource = Debugger.Utility.Collections.FromListEntry(*(nt!_LIST_ENTRY*)&nt!ExpSystemResourcesList, "nt!_ERESOURCE", "SystemResourcesList") lkd> dx @$eresource.Count(),d @$eresource.Count(),d : 400960
由于数量庞大,因此无法使用LINQ进行分析,因此,我们希望使用C代码获取其中一些ERESOURCE结构的库信息,并开始对其进行分析。不幸的是,与后备列表不同,ERESOURCE结构没有将其pool标签作为结构的一部分,因此我们必须编写一个池解析器以获取每个ERESOURCE的池信息。正如我们之前提到的,事实证明,在RS5和更高版本中,这根本不是一件容易的事,正如您将在对新的后端堆支持内核池的最新研究中看到的那样。
获取目标名称
现在,我们以有效的回调作为结束,但是没有有关正在打开的路径的信息。当然,我们可以从堆栈中获取它,因为它应该保存在某个地方。
因此,我们决定提出一种方法来强制执行我们自己的解析例程,在该例程中,我们可以获得原始路径,并决定是否重定向调用方。我想到了两个选择:
1. 我们可以使用ObCreateObjectTypeEx创建一个新的对象类型,实现我们自己的ParseRoutine,并使符号链接重定向到该类型的对象,以便我们可以使例程返回STATUS_REPARSE,并使用原始目标设备对象的名称。
2. 我们可以使用IoCreateDevice创建一个新的设备对象,实现我们自己的IRP_MJ_CREATE处理程序,并使其使用I / O管理器的现有解析逻辑(此过程称为transmogrification),以便我们可以返回STATUS_REPARSE和它创建的任何文件对象的新名称,它将重定向到原始目标设备对象。
最终,创建新的对象类型是没有记录的,如果我们采取任何错误措施,则由Patch Guard进行监视,最重要的是,没有匹配的API来撤消/销毁该操作。是的,无法删除对象类型,因此我们的驱动程序将永远无法卸载。
因此,我们决定让我们的symlink回调将符号链接重定向到我们将创建的设备对象,而不是返回原始字符串。然后,当我们调用设备对象的IRP_MJ_CREATE处理程序时,I / O管理器已经创建了一个文件对象,我们可以从IRP中获取它,检索它的名称,以及有关它和创建者/调用者的任何其他信息。
因此,我们首先创建设备-\ Device \ HarddiskVolume0。接下来,以与第1部分中相同的方式获取到C:volume的符号链接,并对其进行修改以指向回调对象LinkTarget。然后,我们只需要做一个更改:我们传递新设备的路径,而不是将原始链接目标字符串作为SymlinkContext中的参数传递:
_Use_decl_annotations_ NTSTATUS DriverEntry ( _In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath ) { NTSTATUS status; HANDLE symLinkHandle; DECLARE_CONST_UNICODE_STRING(symlinkName, L"\\GLOBAL??\\c:"); OBJECT_ATTRIBUTES objAttr = RTL_CONSTANT_OBJECT_ATTRIBUTES(&symlinkName, OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE); UNREFERENCED_PARAMETER(RegistryPath); // // Make sure our alignment trick worked out // if (((ULONG_PTR)SymLinkCallback & 0xFFFF) != 0) { status = STATUS_CONFLICTING_ADDRESSES; DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Callback function not aligned correctly!\n"); goto Exit; } // // Set an unload routine so we can update during testing // DriverObject->DriverUnload = DriverUnload; // // Open a handle to the symbolic link object for C: directory, // so we can hook it // status = ZwOpenSymbolicLinkObject(&symLinkHandle, SYMBOLIC_LINK_ALL_ACCESS, &objAttr); if (!NT_SUCCESS(status)) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed opening symbolic link with error: %lx\n", status); goto Exit; } // // Get the symbolic link object and close the handle since we // no longer need it // status = ObReferenceObjectByHandle(symLinkHandle, SYMBOLIC_LINK_ALL_ACCESS, NULL, KernelMode, (PVOID*)&g_SymLinkObject, NULL); ObCloseHandle(symLinkHandle, KernelMode); if (!NT_SUCCESS(status)) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed referencing symbolic link with error: %lx\n", status); goto Exit; } // // Create our device object hook // RtlAppendUnicodeToString(&g_DeviceName, L"\\Device\\HarddiskVolume0"); status = IoCreateDevice(DriverObject, 0, &g_DeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &g_DeviceObject); if (!NT_SUCCESS(status)) { // // Fail, and drop the symlink object reference // ObDereferenceObject(g_SymLinkObject); DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed create devobj with error: %lx\n", status); goto Exit; } // // Attach our create handler // DriverObject->MajorFunction[IRP_MJ_CREATE] = SymHookCreate; // // Save the original string that the symlink points to // so we can change the object back when we unload // g_LinkPath = g_SymLinkObject->LinkTarget; // // Modify the symlink to point to our callback instead of the string // and change the flags so the union will be treated as a callback. // Set CallbackContext to the original string so we can // return it from the callback and allow the system to run normally. // g_SymLinkObject->Callback = SymLinkCallback; RtlAppendUnicodeStringToString(&g_DeviceName, &g_TailName); g_SymLinkObject->CallbackContext = &g_DeviceName; MemoryBarrier(); g_SymLinkObject->Flags |= OBJECT_SYMBOLIC_LINK_USE_CALLBACK; Exit: // // Return the result back to the system // return status; }
此代码意味着,当有人尝试访问符号链接时,他们将到达我们的回调并接收到设备对象路径(\ Device \ HarddiskVolume0)的路径,而不是\ Device \ HarddiskVolume < N >,其中N是真实的C:volume。
然后,当打开此路径时,I / O管理器将为其余路径创建一个文件对象,例如\ Windows \ Notepad.exe,然后将调用驱动程序对象的IRP_MJ_CREATE处理程序,从中获取该名称。 FILE_OBJECT结构,并将其替换为新的完全限定的路径,包括原始设备对象路径和其余路径。
替换FILE_OBJECT的名称比听起来要棘手-由I / O管理器分配的原始路径具有特定的池标记,而我们释放它并分配自己的名称似乎对各种测试工具(如Driver Verifier)泄漏,除非我们模仿原始标签。
若要解决此问题,Microsoft实现了特殊的API:IoReplaceFileObjectName。它不仅使用正确的内部内核池标记,而且还实现了某些优化,以使文件名字符串缓冲区的长度将始终“对齐”为56、120或248个字节(除非名称更大,这种情况下使用的是精确尺寸)。这样可以避免在许多情况下必须释放/重新分配缓冲区,因为新名称可以简单地覆盖旧名称。
创建新名称的方式如下所示:
// // Get the FILE_OBJECT from the I/O Stack Location // ioStack = IoGetCurrentIrpStackLocation(Irp); fileObject = ioStack->FileObject; // // Allocate space for the original device name, plus the size of the // file name, and adding space for the terminating NUL. // bufferLength = fileObject->FileName.Length + g_LinkPath.Length + sizeof(UNICODE_NULL); buffer = (PWCHAR)ExAllocatePoolWithTag(PagedPool, bufferLength, 'maNF'); if (buffer == NULL) { status = STATUS_INSUFFICIENT_RESOURCES; goto Exit; } // // Append the original device name first // buffer[0] = UNICODE_NULL; NT_VERIFY(NT_SUCCESS(RtlStringCbCatNW(buffer, bufferLength, g_LinkPath.Buffer, g_LinkPath.Length))); // // Then add the name of the file name // NT_VERIFY(NT_SUCCESS(RtlStringCbCatNW(buffer, bufferLength, fileObject->FileName.Buffer, fileObject->FileName.Length))); // // Ask the I/O manager to free the original file name and use ours instead // status = IoReplaceFileObjectName(fileObject, buffer, bufferLength - sizeof(UNICODE_NULL)); if (!NT_SUCCESS(status)) { DbgPrintEx(DPFLTR_IHVDRIVER_ID, DPFLTR_ERROR_LEVEL, "Failed to swap file object name: %lx\n", status); ExFreePool(buffer); goto Exit; }
替换文件对象的名称后,此代码仍然存在问题,我们无法返回STATUS_SUCCESS,因为这将使我们成为该新文件的所有者设备对象,而实际上并未指向该对象的原始目标设备对象。划分。将来所有的I / O都将作为IRP流经我们的驱动程序,并且现在我们必须为每个操作实现转发器。
我们可以为\ Device \ HarddiskVolume < N >获取正确的设备对象,并手动将所有IRP转发给它,但是所有请求仍将附加到我们的设备上。这不仅使我们更加可见,而且从本质上来说,它变成了文件系统过滤器驱动程序。我们只想获取创建请求,然后将其传递给正确的设备,而不必再次处理。
为了使其正常工作,我们必须使用I / O管理器的迁移逻辑,该过程分为两个步骤:
1. 返回STATUS_REPARSE,以指示需要重新解析操作。这将导致IopParseDevice查看文件对象中的新名称字符串,并基于该新名称重新开始名称查找逻辑,从而释放旧对象并完成以前的工作。该代码非常复杂,但是你可以在此处的ReactOS源代码中看到它的简单版本。
2. 将IRP的“信息”字段设置为IO_REPARSE,这表明我们正在尝试的重新解析操作的类型。通常,这是通过使用特殊的重新分析标记和Microsoft文档记录的匹配结构(例如REPARSE_DATA_BUFFER)来指示真正的硬链接或符号链接的地方。但是,IO_REPARSE是一个魔术值/保留值,它仅指示名称的普通替换,而不是真正的重解析点。
考虑到这些要点,我们的IRP_MJ_CREATE处理程序以以下逻辑完成:
// // Return a reparse operation so that the I/O manager uses the new file // object name for its lookup, and starts over // Irp->IoStatus.Information = IO_REPARSE; status = STATUS_REPARSE; Exit: // // Complete the IRP with the relevant status code // Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status;
所以现在我们有了一种看起来像这样的机制:
有些人可能会指出,此方法完全不需要使用symlink回调也可以正常工作。比如我们可以将符号链接的LinkTarget替换为设备的路径,并且无论如何都会获得所有请求。实际上,我们只需要更改路径的最后一位。但是,我们认为这样做会使我们更加可见,因为任何检查symlink对象的人都可以轻松地看到此路径以及结构的变化。
另一个原因是,通过回调,我们可以动态地决定要做什么的问题。例如,如果我们知道我们正在接受杀毒驱动程序的检查,则可以使用回调函数返回原始字符串,而不必将请求重定向到我们的设备。如果愿意,我们甚至可以重定向到一个完全不同的设备,而不必不断更改路径。
加载了经过改进的新驱动程序并查看结果:
大约10秒后,我们崩溃了。
本文翻译自:https://windows-internals.com/lookaside-list-forensics/ https://windows-internals.com/symhooks-part-two/如若转载,请注明原文地址: