-
-
[翻译]符号链接第二部分:获得目标名
-
2020-4-3 14:45 6608
-
原文: http://windows-internals.com/symhooks-part-two/
翻译:看雪翻译小组----OsWalker
校对:看雪翻译小组----fyb波
上一篇博客中,我们最终实现了一个能够运行的回调函数,但是没有在回调中得到要访问的路径信息。当然,我们可以在栈中得到路径,它一定位于栈中的某个位置,但我们相信一定有更优雅的办法。我们也不想在Unwind Opcodes中写一本书和如果有效地恢复栈参数。
因此,我们决定换一条路,找到一个方法强制执行我们自己的解析过程,在解析过程中获得原路径,并在其中决定是否重定向调用者的路径。脑海中有两个可选方法:
-
用ObCreateObjectTypeEx创建一个新对象类型,实现我们自己的ParseRoutine,将符号链接重定向到用我们的新类型创建的对象,这样就可以使用原始目标设备对象名为参数让我们的ParseRoutine返回STATUS_REPARSE。
-
使用IoCreateDevice创建一个新设备对象,实现我们自己的IRP_MJ_CREATE处理函数,让处理函数使用I/O管理器现成的重解析功能(称作transmogrification),我们可以给要创建的文件对象返回STATUS_REPARSE和一个新名字,这会将请求重定向到原始目标设备对象。
然而,创建新对象类型没有文档,如果进行了错误行为将被PatchGuard监控,最重要的,没有合适的API来取消所进行的操作。Yep,不存在删除对象类型的方法,我们的驱动将无法卸载。
因此,我们决定让符号链接回调函数重定向符号链接到一个我们创建的设备对象,而不是返回原始字符串。然后,当我们设备对象的IRP_MJ_CREATE处理函数被调用,I/O管理器已经创建好了一个文件对象,我们可以通过IRP得到它,获得它的名字和该文件对象及其调用者有关的其它信息。
因此,我们要先创建设备对象\Device\HarddiskVolume0。接下来,我们使用Part1中的方法获得符号链接,将其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 //打开C盘符号链接对象的句柄,hook它 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 //创建我们的设备对象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. //修改符号链接指向我们的回调而不是字符串,修改flag使联合结构被当做回调函数。修改CallbackContext为原始字符串,需要在回调函数中返回该字符串使得系统正常运行 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分区。
接下来,打开该路径时,I/O管理器将用后续路径创建一个文件对象,如\Windows\Notepad.exe,然后调用我们的驱动对象的IRP_MJ_CREATE处理函数,在处理函数中我们可以在FILE_OBJECT结构中获得文件名,并用一个新的完整路径替换它,这个完整路径包含原始设备对象路径(\Device\HarddiskVolumeC)和后续路径(\Windows\Notepad.exe)。
替换FILE_OBJECT名很有技巧,I/O管理器分配的原始路径有一个特定的pool标签,我们释放它并分配一个我们自己的路径会被各种测试工具(如Driver Verifier)看做是内存泄露,除非我们模仿原始标签。
要处理这个问题,微软实现了一个API:IoReplaceFileObjectName。它不仅使用正确的内核内部pool标签,还进行了一些优化,比如文件名字符串缓存始终以偏移56/120/248字节分配(如果名称很长,则使用准确大小)。这在很多情况下避免执行释放、重分配操作,所以新名字可以直接重写就名字。
创建新名字的最终代码如下所示:
// // Get the FILE_OBJECT from the I/O Stack Location //从I/O栈特定位置获得文件对象 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管理器的转换逻辑,这分为两步:
-
返回STATUS_REPARSE以表示需要进行重解析。触发IopParseDevice检查文件对象的新名字字符串,再次对这个新名字进行名字解析功能,释放旧对象并且结束之前的解析工作。这个代码很复杂,在ReactOS中可以找到一个类似的。
-
设置IRP的Information域为IO_REPARSE,这是进行的重解析操作的类型。这使用一个特殊的重解析标签和微软文档的匹配结构(如REPARSE_DATA_BUFFER)指示常规的真正硬链接或符号链接。但是,IO_REPARSE只是一个保留值表明一个简单的名字替换,而不是一个真正的重解析点。
// // Return a reparse operation so that the I/O manager uses the new file // object name for its lookup, and starts over //返回重解析操作,让I/O管理器解析时使用新文件对象名,重来一次 Irp->IoStatus.Information = IO_REPARSE; status = STATUS_REPARSE; Exit: // // Complete the IRP with the relevant status code //使用状态码完成IRP Irp->IoStatus.Status = status; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status;
整体功能机制如下:
你们可能会发现这个方法在没有符号链接回调时也能正常工作,只需将符号链接的LinkTarget替换为我们的设备的路径,就能获得所有的请求,实际上只需要修改路径的最后一个数字。但我们认为,这样做很容易被发现,只需检查符号链接对象就能发现被修改的路径和结构。
另一个原因是,使用回调函数我们可以动态决定只需什么动作。例如,如果我们发现在被AV驱动检查,我们可以使用回调返回原始字符串,不将请求重定向到我们的设备。如果愿意,可以不用市场改变路径,将请求重定向到一个完全不同的设备(可能有条件竞争问题)。
完成后很兴奋,我们加载新修改的驱动并查看结果:
看到这些非常开心,但是,10秒后,奔溃发送了:
Shit。我们的设备收到C卷根目录的open请求是,不能返回STATUS_REPARSE,这是规定的。
那么,我们该怎么办呢?将在第三部分揭晓(以及4,可能还有5)。
这部分介绍的对hook的改善在一个GitHub新分支中。
相关阅读
- Symbolic Hooks Part 3: The Remainder Theorem(第三部分)
- Symbolic Hooks Part 2 : Getting the Target Name
- “Move aside, signature scanning!” Better kernel data discovery through lookaside lists
- DKOM – Now with Symbolic Links!
- R.I.P ROP: CET Internals in Windows 20H1
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。