Windows 20H1中的CET内部机制(下)
2020-07-12 10:30:00 Author: www.4hou.com(查看原文) 阅读量:526 收藏

导语:最近在Windows 10的19H1(1903版)版本中发生了一件非常令人激动的事情,经过多年的讨论,Intel“控制流实施技术”(CET)的终于可以实现了。

Windows 20H1中的CET内部机制(上)

Windows 20H1中的CET内部机制(中)

异常解除和longjmp验证

如上所示,对KCONTEXT_SET和KCONTEXT_RESUME的处理涉及验证目标RIP是影子堆栈的一部分,但其他情况(KCONTEXT_UNWIND和KCONTEXT_LONGJMP)则需要通过RtlVerifyUserUnwindTarget进行扩展验证。第二个验证路径包含许多有趣的复杂性,这些复杂性要求更改PE文件格式以及对编译器的支持,以及为NITSetInformationProcess添加的新的操作级信息类,以支持JIT编译器。

由于对控制流保护(CFG)支持的增强,PE文件中的映像载荷配置目录现在包含有关分支有效目标的信息,这些信息用作setjmp/longjmp对的一部分,现代编译器应该识别这些信息,并将其传递给链接器。使用CET,可以重新使用此现有数据,但是又添加了另一个表和大小以支持异常处理程序继续。虽然Visual Studio 2017产生longjmp表,但是只有Visual Studio 2019生成了这个更新的表。

在最后一部分中,我们将研究这些表的格式,以及内核如何授权后两种类型的KCONTINUE_TYPE控制流。

PE元数据表

除了控制流保护映像中存在的标准GFIDS表之外,Windows 10还通过包含一个长跳转目标表(通常位于称为.gljmp的PE部分中)来支持对longjmp目标的验证,RVA存储在该文件中。映像加载配置目录的GuardLongJumpTargetTable字段。

每当在代码中调用setjmp时,返回地址的RVA(longjmp将跳转到的地址)都会添加到此表中。该表的存在由映像加载配置目录的GuardFlags中的IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT标志确定,并且该表包含的数量与GuardLongJumpTargetCount字段指示的数量相同。

每个条目都是一个4字节的RVA,外加n字节的元数据,其中n是从(GuardFlags & IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK) >> image_guard_function_table_size_shift的结果中获得的。对于此表,没有定义任何元数据,因此元数据字节总是期望为零。有趣的是,由于此计算与GFIDS表所用的计算相同(如果启用了导出抑制,则该表可能具有元数据),因此抑制至少一个CFG目标将导致将1字节的空元数据添加到GFIDS表的每个条目中。跳远目标表。

例如,以下是一个具有两个longjmp目标的PE文件:

22.png

请注意,GuardFlags(与IMAGE_GUARD_CF_FUNCTION_TABLE_SIZE_MASK对应)的上半字节中的值为1,这是因为该映像也使用CFG导出抑制。这告诉我们,跳远目标表中将存在一个额外的元数据字节,示例如下:

23.png

在Windows 10 20H1中,这种类型的元数据现在包括在一个额外的情况中,当异常处理程序的连续目标作为二进制控制流的一部分出现时。在映像加载配置目录的末尾添加了两个新字段GuardEHContinuationTable和GuardEHContinuationCount,并且IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT标志现在是GuardFlags的一部分。该表的布局与为Long Jump目标表所显示的布局相同,包括基于GuardFlags上半字节添加的元数据字节。

不幸的是,即使是Visual Studio 2019的当前预览版也无法生成此数据,因此我们目前无法向您展示一个示例——这个分析基于我们稍后描述的验证代码的反向工程,以及20H1 SDK中的Ntimage.h标头文件。

用户倒排函数表

既然我们知道控制流可能会发生变化,以便分支到longjmp目标或异常处理程序继续目标,那么问题就变成了,我们如何基于CONTEXT_EX中存在的RIP地址获取这两个表,作为一个对象的一部分。 NtContinueEx调用吗?由于这些操作可能在某些程序执行的上下文中频繁发生,因此内核需要一种有效的方法来解决此问题。

简而言之,反向函数表是一个数组,其中包含所有已加载的用户/内核模块的大小,以及一个指向PE异常目录的指针,该指针按虚拟地址排序,它最初是作为一种优化而创建的。

但是,以前,Ntdll.dll仅扫描其表中的用户模式异常,而Ntoskrnl.exe仅扫描其表中的内核模式异常-20H1所做的更改是内核现在也必须扫描用户表-作为一部分处理longjmp和异常延续所需的新逻辑。为此,添加了一个新的RtlpLookupUserFunctionTableInverted函数,该函数扫描KeUserInvertedFunctionTable变量,映射到Ntdll.dll中现在导出的LdrpInvertedFunctionTable符号。

这是一种令人兴奋的取证功能,因为这意味着你现在可以轻松地从内核中定位当前进程中已加载的用户模式模块,而无需解析PEB的加载器数据或枚举VAD。例如,以下是你如何在Csrss.exe中查看当前加载映像的方法:

dx @$cursession.Processes.Where(p => p.Name == "csrss.exe").First().SwitchTo()
dx -r0 @$table = *(nt!_INVERTED_FUNCTION_TABLE**)&nt!KeUserInvertedFunctionTable
dx -g @$table->TableEntry.Take(@$table->CurrentSize)

但是确实存在不包含异常目录的可能性,特别是在x86系统上,这样unwind操作码不存在,并且只有在使用/SAFESEH且至少有一个异常处理程序时才会创建.pdata。

在这种情况下,RtlpLookupUserFunctionTableInverted可能会失败,而必须改用MmGetImageBase。不出所料,这将查找映射到输入RIP对应区域的任何VAD,如果它是一个映像VAD,则返回该区域的基址和大小应与模块的基址和大小相对应。

动态异常处理程序的持续目标

最后一个障碍是处理KCONTINUE_UNWIND请求,尽管常规进程基于其代码中的__try / __ except / __ finally子句具有静态异常处理程序的连续目标,但Windows允许JIT引擎不仅动态地动态创建可执行代码,而且还允许在运行时为其注册异常处理程序并展开操作码,例如通过RtlAddFunctionTable API。虽然这些异常处理程序以前仅在用户模式堆栈移动和异常展开时才需要,但是现在,这些延续处理程序成为合法的控制流目标,内核必须将其理解为RIP的潜在有效值,这是RtlpFindDynamicEHContinuationTarget处理的最后一种可能性。

作为CET支持和NtContinueEx引入的一部分,EPROCESS结构得到了增强,增加了两个新字段,分别称为DynamicEHContinuationTargetsLock和DynamicEHContinuationTargetsTree,其中第一个是EX_PUSH_LOCK,第二个是RTL_RB_TREE,其中包含所有有效的异常处理程序地址。通过使用新的过程信息类ProcessDynamicEHContinuationTargets调用NtSetInformationProcess来管理该树,该过程类带有PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION类型的数据结构,该数据结构依次包含PROCESS_DYNAMIC_EH_CONTINUATION_TARGET的数组,该数组将在将要验证的Validate条目修改之前被DynamicTrees激活。为了更容易理解,请参见以下有关这些结构和标志的定义:

#define DYNAMIC_EH_CONTINUATION_TARGET_ADD          0x01
#define DYNAMIC_EH_CONTINUATION_TARGET_PROCESSED    0x02

typedef struct _PROCESS_DYNAMIC_EH_CONTINUATION_TARGET
{ 
    ULONG_PTR TargetAddress;
    ULONGLONG Flags;
} PROCESS_DYNAMIC_EH_CONTINUATION_TARGET, *PPROCESS_DYNAMIC_EH_CONTINUATION_TARGET;

typedef struct _PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION
{
    USHORT NumberOfTargets;
    USHORT Reserved;
    ULONG Reserved2;
    PPROCESS_DYNAMIC_EH_CONTINUATION_TARGET* Targets;
} PROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION, *PPROCESS_DYNAMIC_EH_CONTINUATION_TARGETS_INFORMATION;

调用PspProcessDynamicEHContinuationTargets函数以对此数据进行迭代,此时将对包含DYNAMIC_EH_CONTINUATION_TARGET_ADD标志集的任何条目调用RtlAddDynamicEHContinuationTarget,该条目将分配一个存储目标地址的数据结构,并将其RTL_BALANCED_PROD链接与RTL中的ETL链接。相反,如果该标志丢失,则查找目标,如果目标确实存在,则将其删除并释放其节点。在处理每个条目时,将DYNAMIC_EH_CONTINUATION_TARGET_PROCESSED标志与原始输入缓冲区进行“或”运算,以便调用者可以知道哪些条目有效,哪些无效。

显然,这可以让任何CET / CFG的函数绕过,因为每个可能的ROP小工具都可以简单地添加为“动态延续目标”。但是,由于Microsoft现在仅合法地支持针对浏览器和Flash的进程外JIT编译,因此必须注意,此API仅适用于远程进程。实际上,在当前进程上调用它总是失败,并显示STATUS_ACCESS_DENIED。

目标验证

综上所述,RtlVerifyUserUnwindTarget函数变得非常容易解释。

在CONTEXT_EX结构中查找与目标RIP关联的已加载PE模块。首先,尝试使用RtlpLookupUserFunctionTableInverted,如果失败,请改用MmGetImageBase,确保模块小于4GB。

如果找到了模块,则调用LdrImageDirectoryEntryToLoadConfig函数以获取其映像加载配置目录。然后,确保其足够大,以包含“跳远”或“动态异常处理程序连续目标表”,并且保护标志包含IMAGE_GUARD_CF_LONGJUMP_TABLE_PRESENT或IMAGE_GUARD_EH_CONTINUATION_TABLE_PRESENT。如果目录丢失、太小或根本不存在匹配表,则出于兼容性原因,返回STATUS_SUCCESS。

从映像加载配置目录中获取GuardLongJumpTargetTable或GuardEHContinuationTable,并验证GuardLongJumpTargetCount或GuardEHContinuationCount。如果条目超过40亿,则返回STATUS_INTEGER_OVERFLOW。如果条目多于0,则在将目标RIP转换为RVA之后,使用bsearch_s(将RtlpTargetCompare作为比较器传递)通过表进行二进制搜索,以找到目标RIP。如果找到,则返回STATUS_SUCCESS。

如果未找到目标RIP或者该表包含0个开始的条目,或者首先未在目标RIP上找到已加载的模块,则返回STATUS_SET_CONTEXT_DENIED以进行longjmp验证(KCONTINUE_LONGJUMP)。

否则,对于异常展开验证(KCONTINUE_UNWIND),请调用RtlpFindDynamicEHContinuationTarget来检查这是否是一个已注册的动态异常处理程序延续目标。如果是,则返回STATUS_SUCCESS,否则返回STATUS_SET_CONTEXT_DENIED。

总结

CET及其相关缓解措施的实施可以避免出现ROP攻击和其他控制流劫持。控制流完整性显然是一个复杂的主题,随着将来添加更多的缓解措施,它可能会变得更加复杂。

DKOM –现在有了符号链接!

对于内核回调,你可能会认为就是进程创建回调、对象类型回调、映像加载通知、回调对象、对象类型回调、主机扩展

在Microsoft的尝试中, Windows 10 Creators Update(RS2)添加了一种新型的回调——符号链接。

请注意OBJECT_SYMBOLIC_LINK结构最近的更改:

typedef struct _OBJECT_SYMBOLIC_LINK
{
    LARGE_INTEGER CreationTime;
+   union
+   {
        UNICODE_STRING LinkTarget;
+       struct
+       {
+            POBJECT_SYMBOLIC_LINK_CALLBACK Callback;
+            PVOID CallbackContext;
+       };
+   }
    ULONG DosDeviceDriveIndex;
    ULONG Flags;
    ULONG AccessMask;
} OBJECT_SYMBOLIC_LINK, *POBJECT_SYMBOLIC_LINK;

以前是一个包含符号链接目标的Unicode字符串,现在变成了一个union,它包含我们在查看内核回调时最喜欢的关键字。

这些回调被添加到RS2中,以支持内存分区,内存分区是一种新类型的对象,用于将物理地址范围分割到它们自己的内存管理器实例中。其关键之处在于\KernelObjects中的一些事件对象(如LowMemoryCondition)不再是全局的,而是引用当前调用者的内存分区中的特定条件。但是,为了不破坏兼容性,无法更改它们的命名和位置,例如\KernelObjects\Partition2\)。

LowMemoryCondition。因此,它们被转换为附加到动态回调的符号链接,该链接将查看EPROCESS中的当前内存分区,并为调用者的分区返回适当的KEVENT对象。

现在,只要设置了符号链接标志中的第5位(标志和OBJECT_SYMBOLIC_LINK_USE_CALLBACK),LinkTarget就不会被视为字符串,而是被视为具有以下原型的函数:

typedef
NTSTATUS
(POBJECT_SYMBOLIC_LINK_CALLBACK*) (
    _In_ POBJECT_SYMBOLIC_LINK Symlink,
    _In_ PVOID SymlinkContext,
    _Out_ PUNICODE_STRING SymlinkPath,
    _Outptr_ PVOID* Object
    );

每当重新解析符号链接时,都会调用此函数,并且必须将SymlinkPath参数设置为对象管理器要解析的目标路径,或者将对象参数设置为将用作该符号链接目标的正确对象。

该回调由ObCreateSymbolicLink函数根据包含标志和目标字符串或回调函数的输入结构来设置:

#define OB_SYMLINK_TARGET_DYNAMIC 0x01
typedef struct _OB_SYMLINK_TARGET
{
    ULONG Flags;
    union
    {
        UNICODE_STRING LinkTarget;
        struct
        {
            POBJECT_SYMBOLIC_LINK_CALLBACK Callback;
            PVOID CallbackContext;
        };
    };
} OB_SYMLINK_TARGET, *POB_SYMLINK_TARGET;

基于该参数,该函数创建一个符号链接对象,并设置其目标:

NTSTATUS
ObCreateSymbolicLink (
    _Out_ PHANDLE LinkHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ POB_SYMLINK_TARGET TargetInfo,
    _In_ KPROCESSOR_MODE AccessMode
    )
{
    NTSTATUS status;
    PWCHAR linkString;
    POBJECT_SYMBOLIC_LINK symlinkObject;
    HANDLE linkHandle;

    //
    // Create the symlink object
    //
    symlinkObject = NULL;
    status = ObCreateObjectEx(AccessMode,
                              ObpSymbolicLinkObjectType,
                              ObjectAttributes,
                              AccessMode,
                              0,
                              sizeof(*symlinkObject),
                              0,
                              0,
                              &symlinkObject,
                              NULL);
    if (NT_SUCCESS(status))
    {
        KeQuerySystemTime(&symlinkObject->CreationTime);
        symlinkObject->DosDeviceDriveIndex = 0;
        symlinkObject->Flags = 0;

        //
        // If the symlink has a dynamic target, set the flags accordingly
        // and populate the callback field
        //
        if (TargetInfo->Flags & OB_SYMLINK_TARGET_DYNAMIC)
        {
            symlinkObject->Flags = OBJECT_SYMBOLIC_LINK_USE_CALLBACK;
            symlinkObject->Callback = TargetInfo->Callback;
        }
        else
        {
            //
            // If the symlink doesn't have a dynamic target, set the LinkTarget to the string
            //
            symlinkObject->LinkTarget.MaximumLength = TargetInfo->LinkTarget.MaximumLength;
            symlinkObject->LinkTarget.Length = TargetInfo->LinkTarget.Length;
            linkString = (PWCHAR)ExAllocatePoolWithTag(PagedPool,
                                                       TargetInfo->LinkTarget.MaximumLength,
                                                       'tmyS');
            symlinkObject->LinkTarget.Buffer = linkString;
            if (linkString == NULL)
            {
                status = STATUS_NO_MEMORY;
                goto Exit;
            }

            RtlCopyMemory(linkString,
                          TargetInfo->LinkTarget.Buffer,
                          TargetInfo->LinkTarget.MaximumLength);
        }

        if (RtlIsSandboxedToken(NULL, AccessMode) != FALSE)
        {
            symlinkObject->Flags |= OBJECT_SYMBOLIC_IS_SANDBOXED;
        }

        status = ObInsertObjectEx(symlinkObject,
                                  NULL,
                                  DesiredAccess,
                                  0,
                                  0,
                                  NULL,
                                  &linkHandle);
        symlinkObject = NULL;

        if (NT_SUCCESS(status))
        {
            *LinkHandle = linkHandle;
            status = STATUS_SUCCESS;
        }
    }

Exit:
    if (symlinkObject != NULL)
    {
        ObDereferenceObject(symlinkObject);
    }

    return status;
}

不幸的是,ObCreateSymbolicLink没有被导出,因此我们不能自己调用它,也不能使用回调函数创建符号链接。该函数有2个调用程序——NtCreateSymbolicLinkObject和MiCreateMemoryEvent。后一个函数处理我们前面描述的内存分区功能。它将各种内存事件创建为没有目标字符串的符号链接,并将它们的回调设置为MiResolveMemoryEvent:

25.png

你可以在WinObjEx中看到这些符号链接,可以在没有目标字符串的情况下来识别它们:

26.png

但如果MiCreateMemoryEvent是一个内部函数,则对我们来说不是很有用。因此,我们来看一下NtCreateSymbolicLinkObject,它几乎无法使用:

27.png

它始终将OB_SYMLINK_TARGET结构的标志设置为0,这意味着目标始终是字符串,而不是函数指针。这是不幸的,因为这意味着我们不能创建包含用户模式回调的符号链接对象。现在,我们决定尝试和修改一个现有的符号链接,我们可以使用这个功能来钩住一些经常使用的符号链接,并注册我们自己的函数来调用它。

我们选择C:volume的符号链接作为目标,为了实现目标,我们首先需要打开符号链接并获取它的对象,这样我们才能修改它:

NTSTATUS status;
HANDLE symLinkHandle = NULL;
POBJECT_SYMBOLIC_LINK symlinkObject;
UNICODE_STRING symlinkName = RTL_CONSTANT_STRING(L"\\GLOBAL??\\c:");
OBJECT_ATTRIBUTES objectAttributes =
RTL_CONSTANT_OBJECT_ATTRIBUTES(&symlinkName,
                               OBJ_KERNEL_HANDLE | OBJ_CASE_INSENSITIVE);
//
// Open a handle to the symbolic link object for C: directory,
// so we can hook it
//
status = ZwOpenSymbolicLinkObject(&symLinkHandle,
                                  SYMBOLIC_LINK_ALL_ACCESS,
                                  &objectAttributes);
if (!NT_SUCCESS(status))
{
    goto Cleanup;
}

//
// Get the symbolic link object
//
status = ObReferenceObjectByHandle(symLinkHandle,
                                   SYMBOLIC_LINK_ALL_ACCESS,
                                   NULL,
                                   KernelMode,
                                   (PVOID*)&symlinkObject,
                                   NULL);
if (!NT_SUCCESS(status))
{
    goto Cleanup;
}
//
// Save the original string that the symlink points to
// so we can change the object back when we unload
//
origStr = symlinkObj->LinkTarget;

获得请求的符号链接对象后,我们需要保存目标字符串或它所指向的设备,以便从回调函数中返回它。获取设备很麻烦,可能会有一些问题,而目标字符串就在对象本身中。我们将它存储在一个全局变量中,然后就有了修改符号链接所需的一切。我们只需要创建我们的回调函数:

NTSTATUS
SymLinkCallback (
    _In_ POBJECT_SYMBOLIC_LINK Symlink,
    _In_ PVOID SymlinkContext,
    _Out_ PUNICODE_STRING SymlinkPath,
    _Outptr_ PVOID* Object
    )
{
    UNREFERENCED_PARAMETER(Symlink);

    //
    // We need to either return the right object for this symlink
    // or the correct target string.
    // It's a lot easier to get the string, so we can set Object to Null.
    //
    *Object = NULL;
    *SymlinkPath = *(PUNICODE_STRING)(SymlinkContext); // OrigStr

    return STATUS_SUCCESS;
}

symlinkCallback函数接收符号链接对象、2个输出参数(其中只有一个必须由函数设置)和一个SymlinkContext参数(由注册函数的人控制)。我们选择使用这个上下文来存储原始的LinkTarget字符串,这样我们就可以将输出的SymlinkPath参数设置为它,并将这个符号链接发送到它正确的目的地。

定义了回调函数后,我们可以回到主函数并编辑symlink对象:

//
// 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.
//
symlinkObj->Callback = SymLinkCallback;
symlinkObj->CallbackContext = &symlinkObj->LinkTarget;
_MemoryBarrier();
symlinkObj->Flags |= OBJECT_SYMBOLIC_LINK_USE_CALLBACK;

理论上,我们已经完成了。我们可以加载我们的驱动程序,并且对C: volume的每次访问都应该到达我们的回调。但你可能会注意到,这里有一个竞态条件。由于回调函数和上下文是union(也可以是Unicode字符串)的一部分,所以放置在那里的数据可能被解释为错误的类型,导致类型混淆和不可避免的崩溃。

union
{
    UNICODE_STRING LinkTarget;
    struct
    {
        PVOID Callback;
        PVOID CallbackContext;
    };
};

此数据的类型由OBJECT_SYMBOLIC_LINK确定,但是回调字段离OBJECT_SYMBOLIC_LINK结构中的Flags字段太远了,这意味着,如果我们先更改标志,可能会有人试图在我们更改LinkTarget字符串之前访问这个符号链接,内核将尝试调用Unicode字符串,就好像它是可执行内存一样,这会导致崩溃。或者,如果我们先改变字符串,而有人试图使用符号链接,内核会将回调地址的较低2字节解释为字符串长度,并尝试将这么多字节作为字符串读取。这可能会产生一个巨大的数字,再次崩溃。

我们找到了解决此问题的方法,并且我们认为这是一个非常聪明的主意。确实需要花费大约45分钟的时间才能解决链接器设置,但这只是你有时必须付出的代价。我们还意识到,“简单”的解决方案也是可行的:使用正确的设置创建我们自己的OBJECT_SYMBOLIC_LINK,然后修改OBJECT_DIRECTORY_ENTRY以将原始对象的指针与我们的对象交换。因为它是一个简单的指针,所以我们可以使用InterlockedCompareExchangePointer。此外,OBJECT_DIRECTORY具有一个锁(EX_PUSH_LOCK),我们可以使用它来使操作完全安全。但是我们更喜欢我们的聪明方法(并想炫耀)。

如前所述,如果我们将字符串指针更改为函数指针,并且有人在更改标志之前尝试使用符号链接,则他们会将回调地址的前2个字节视为字符串长度,并尝试解析“字符串”。因此,我们决定仅确保回调地址的前2个字节为0000,因此将长度视为0,并且不尝试任何字符串解析。这意味着我们需要将回调函数调整为64KB。经过测试最终有效的是:

1. 创建一个名为.call $ 0的节,并在其中放置一个大小为0xB000的缓冲区。

2. 用零填充此缓冲区,这样编译器就不会对其进行优化。我们选择0xB000是因为我们注意到.text段位于0x5000,这使我们的节位于0xB000 + 0x5000(0x10000–64KB)字节。

3. 创建一个名为.call $ 1的可执行节,并将回调函数放在此处。

进行以下更改后,我们的驱动程序将如下所示:

EXTERN_C_START

__declspec(code_seg(".call$1"))
NTSTATUS
SymLinkCallback (
    _In_ POBJECT_SYMBOLIC_LINK Symlink,
    _In_ PVOID SymlinkContext,
    _Out_ PUNICODE_STRING SymlinkPath,
    _Outptr_ PVOID* Object
);

EXTERN_C_END

#pragma section(".call$0", write)
__declspec(allocate(".call$0")) UCHAR buffer[0xB000] = { 0 };

#pragma code_seg(".text")
_Use_decl_annotations_
NTSTATUS
DriverEntry (
    PDRIVER_OBJECT DriverObject,
    PUNICODE_STRING RegistryPath
    )
{
    ...
}

#pragma section(".call$1", execute)
__declspec(code_seg(".call$1"))

NTSTATUS
SymLinkCallback (
    _In_ POBJECT_SYMBOLIC_LINK Symlink,
    _In_ PVOID SymlinkContext,
    _Out_ PUNICODE_STRING SymlinkPath,
    _Outptr_ PVOID* Object
    )
{
    ...
}

我们编译了驱动程序,并在IDA中将其打开,以查看回调函数的地址:

233.png

让我们加载驱动程序,看看如果我们尝试将符号链接目标视为字符串会发生什么……

234.png

我们转储修改过的符号链接,我们可以看到,当尝试将回调地址视为Unicode字符串时,我们会得到一个Length == 0的字符串,这可以解决我们的竞争条件。

当然,如果我们希望卸载驱动程序,我们还需要实现一个卸载例程,该例程会将符号链接更改回其原始目标。我们将对象保存在全局symlinkObj变量中,并将原始LinkTarget保存在全局origStr变量中,以便在卸载时可以将所有内容改回来:

_Use_decl_annotations_
VOID
DriverUnload (
    _In_ PDRIVER_OBJECT DriverObject
    )
{
    UNREFERENCED_PARAMETER(DriverObject);

    symlinkObj->Flags &= ~OBJECT_SYMBOLIC_LINK_SANDBOXED;
    _MemoryBarrier();
    symlinkObj->LinkTarget = origStr;

    ObDereferenceObject(symlinkObj);
}

请务必先更改Flags,然后再更改LinkTarget。此时你可能会注意到第二条有趣的代码行,这意味着无法保证我们编写这两行C的方式最终会在汇编代码中匹配。

为了解决这个问题,Visual C / C ++包含内联函数,例如_ReadWriteBarrier()。但是,现代处理器本身也可以选择在硬件级别对存储操作进行重新排序,这意味着这两次写操作也可能以不同的顺序发生。要解决硬件重新排序问题,我们需要使用fence指令,这是_MemoryBarrier()的作用。

现在,我们有了一个有效的回调函数,只要有人尝试访问C:volume,就会调用该函数。除非特别搜索,否则此方法不可见,也不会被检测到。 WinObjEx会将此符号链接显示为没有目标,这只会对正在寻找该技术的人表示怀疑。

236.png

但是,每次访问符号链接时,都将获得调用方请求的完整路径。例如,这将使我们根据调用者是谁以及监视访问来返回不同的文件或目录。

本文翻译自:https://windows-internals.com/dkom-now-with-symbolic-links/如若转载,请注明原文地址:


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