A common method of unhooking user-land API hooks is to load a fresh copy of NTDLL from KnownDlls
, a special object directory that’s used to essentially cache commonly used system DLLs. We can use WinObj to view the Object Manager namespace, where we can see the KnownDlls
directory, and the mapped sections it contains for each system DLL.
解除用户土地 API 挂钩的常用方法是从 加载 KnownDlls
NTDLL 的新副本,这是一个特殊的对象目录,用于缓存常用的系统 DLL。我们可以使用 WinObj 查看对象管理器命名空间,在其中我们可以看到 KnownDlls
目录以及它包含的每个系统 DLL 的映射部分。
Whilst working through the excellent Maldev Academy course material, it was pointed out that you can’t seem to use OpenFileMapping
to retrieve a handle to the KnownDlls
directory, despite it’s purpose being to open named file mapping objects. Attempting to use the function to open \KnownDlls\ntdll.dll
, or any other DLL in that directory, will result in error 161 – ERROR_BAD_PATHNAME
. Instead, most malware uses the native NtOpenSection
instead.
在学习优秀的马尔德夫学院课程材料时,有人指出,您似乎不能用于 OpenFileMapping
检索 KnownDlls
目录的句柄,尽管其目的是打开命名文件映射对象。尝试使用该函数打开 \KnownDlls\ntdll.dll
该目录中的任何其他 DLL 将导致错误 161 – ERROR_BAD_PATHNAME
。相反,大多数恶意软件使用本机 NtOpenSection
。
I wanted to investigate why the function was failing in this manner, and this post is just a short walkthrough what I found.
我想调查为什么函数以这种方式失败,这篇文章只是我发现的简短演练。
OpenFileMapping
and NtOpenSection
OpenFileMapping
和 NtOpenSection
As already mentioned, the OpenFileMapping
function “Opens a named file mapping object”. It’s definition is as follows:
如前所述, OpenFileMapping
函数“打开命名文件映射对象”。它的定义如下:
HANDLE OpenFileMappingA(
[in] DWORD dwDesiredAccess,
[in] BOOL bInheritHandle,
[in] LPCSTR lpName
);
These parameters are all pretty self-explanatory; dwDesiredAccess
specifies the access level for the file mapping object and is checked against the security descriptor on the target object. bInheritHandle
specifies whether the handle can be inherited by another process or not. The lpname
obviously specifies the name of the file mapping object to be opened, and as noted in the documentation: “The name can have a “Global” or “Local” prefix to explicitly open an object in the global or session namespace.”.
这些参数都是不言自明的; dwDesiredAccess
指定文件映射对象的访问级别,并根据目标对象上的安全描述符进行检查。 bInheritHandle
指定句柄是否可以由另一个进程继承。显然 lpname
指定了要打开的文件映射对象的名称,并且如文档中所述:“名称可以具有”全局“或”本地“前缀,以显式打开全局或会话命名空间中的对象。
OpenFileMapping
eventually calls the native function NtOpenSection
, which is used to open a handle for an existing section object:
OpenFileMapping
最终调用本机函数,该函数 NtOpenSection
用于打开现有节对象的句柄:
NTSYSAPI NTSTATUS ZwOpenSection(
[out] PHANDLE SectionHandle,
[in] ACCESS_MASK DesiredAccess,
[in] POBJECT_ATTRIBUTES ObjectAttributes
);
The most relevant parameter here is the pointer to the OBJECT_ATTRIBUTES
structure, which is what really holds the meat of what object it is we want to open a handle to:
这里最相关的参数是指向结构的 OBJECT_ATTRIBUTES
指针,它真正包含我们想要打开句柄的对象:
typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
HANDLE RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES;
So what’s causing the ERROR_BAD_PATHNAME
when we call the function with \KnownDlls\ntdll.dll
?
那么当我们调用函数时是什么原因造成的 \KnownDlls\ntdll.dll
呢 ERROR_BAD_PATHNAME
?
A safe assumption is that the issue can be found in the OBJECT_ATTRIBUTES
struct that OpenFileMapping
is constructing and passing to NtOpenSection
. We’ll write a simple program that calls the function, and then set a debugger breakpoint on NtOpenSection
to see what is passed in the ObjectAttributes
parameter.
一个安全的假设是,可以在 OpenFileMapping
正在构造并传递给 的结构 NtOpenSection
中找到 OBJECT_ATTRIBUTES
问题。我们将编写一个简单的程序来调用该函数,然后设置调试器断点 NtOpenSection
以查看 ObjectAttributes
参数中传递的内容。
We know that the NtOpenSection
function takes three parameters, and with WinAPI using fastcall, that means the ObjectAttributes
pointer argument will be in the R8
register when we hit our breakpoint. Following the pointer in R8
in a memory dump section will lead us to the OBJECT_ATTRIBUTES
object being passed:
我们知道该 NtOpenSection
函数采用三个参数,并且使用 fastcall 的 WinAPI,这意味着当我们命中断点时, ObjectAttributes
指针参数将位于 R8
寄存器中。跟随内存转储部分中的 R8
指针将引导我们到达正在传递 OBJECT_ATTRIBUTES
的对象:
Length
– red |RootDirectory
– green
Length
– 红色 |RootDirectory
-绿ObjectName
– blue |Attributes
– orange
ObjectName
– 蓝色 |Attributes
-橙SecurityDescriptor
– pink |SecurityQualityOfService
– purple
SecurityDescriptor
– 粉红 |SecurityQualityOfService
-紫色
Both of the final parameters are NULL
, which is expected – the first one being NULL
means the object will receive default security settings, and the second is optional and used to ‘indicate the security impersonal level and context tracking mode,’ which isn’t likely to be causing our issue here. We can check the ObjectName
field first and just make sure that the path we are passing to OpenFileMapping
is actually what is being passed to NtOpenSection
, and isn’t mangled somewhere along the way.
最后两个参数都是 NULL
,这是预期的 – 第一个是表示对象将接收默认安全设置,第二个是 NULL
可选的,用于“指示安全非个人级别和上下文跟踪模式”,这不太可能导致我们的问题在这里。我们可以先检查字段, ObjectName
并确保我们传递到的路径 OpenFileMapping
实际上是要传递到的 NtOpenSection
路径,并且在此过程中没有被破坏。
Following the pointer will lead us to a UNICODE_STRING
structure which is defined as such:
跟随指针将引导我们进入一个 UNICODE_STRING
结构,该结构定义如下:
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
We can see from debugger comment which has resolved the address of the string that the path is being passed as we expect, and there isn’t anything unusual about the Length
or MaximumLength
values. Returning to the OBJECT_ATTRIBUTES
structure, we are left with two other offending values – the RootDirectory
and the Attributes
. We can quickly check that the argument passed for the Attributes
is 0x80
which is the value for OBJ_OPENIF
. This attribute has a kinda confusing explanation in Microsoft’s documentation, but seems to mean that if the object exists a handle to it should be opened, unless the routine is trying to create a new object with that name, in which case it will return an NTSTATUS
of STATUS_OBJECT_NAME_COLLISION
. If we actually step through the syscall
with our debugger to see what is returned from NtOpenSection
, we receive a STATUS_OBJECT_PATH_SYNTAX_BAD
status, meaning this attribute is unlikely to be what is erroring.
我们可以从解决了字符串地址的调试器注释中看到,路径正在按预期传递, Length
并且 or MaximumLength
值没有任何异常。回到结构, OBJECT_ATTRIBUTES
我们只剩下另外两个有问题的值 – 和 RootDirectory
Attributes
.我们可以快速检查为 是 传递的参数是 Attributes
0x80
的值 OBJ_OPENIF
。此属性在 Microsoft 的文档中有一个有点令人困惑的解释,但似乎意味着如果对象存在,则应打开它的句柄,除非例程尝试创建具有该名称的新对象,在这种情况下,它将返回 NTSTATUS
of STATUS_OBJECT_NAME_COLLISION
.如果我们实际使用调试器单步执行以查看 syscall
从 返回 NtOpenSection
的内容,我们会收到一个 STATUS_OBJECT_PATH_SYNTAX_BAD
状态,这意味着此属性不太可能是错误所在。
That leaves us with the RootDirectory
. This is an optional field, which if set to NULL
means that the ObjectName
field has to point to the fully qualified path to an object. If RootDirectory
isn’t NULL
, ObjectName
will point to an object relative to the RootDirectory
. So this quite obviously is what is causing us issues. We are passing in a fully qualified path to an object, \KnownDlls\ntdll.dll
, which we are expecting to access at the root of the object manager namespace – but NtOpenSection
is trying to open this path from presumably a different root. So what location is actually being passed as the RootDirectory
? We can have a closer look at what OpenFileMapping
is doing to find out:
这给我们留下了. RootDirectory
这是一个可选字段,如果设置为 , NULL
则表示该 ObjectName
字段必须指向对象的完全限定路径。如果不是 NULL
, ObjectName
则将 RootDirectory
指向相对于 . RootDirectory
所以这很显然是给我们带来问题的原因。我们正在将一个完全限定的路径传递给一个对象, \KnownDlls\ntdll.dll
我们希望在对象管理器命名空间的根目录下访问该路径 – 但 NtOpenSection
正在尝试从可能不同的根打开此路径。那么实际上传递的位置是什么 RootDirectory
?我们可以仔细看看 OpenFileMapping
正在做什么来找出:
The BaseFormatObjectAttributes
jumps out immediately. This function is what constructs our initial OBJECT_ATTRIBUTES
structure. If we follow through the execution, we find that it later calls BaseGetNamedObjectDirectory
, and this is the value that is set in the RootDirectory
field. Some quick searching for this function returns some community documentation from undoc.airesoft.co.uk. The provided overview of the function is that it returns a handle to a named object directory for the current session, in the remarks stating that ’the returned handle may refer to the BaseNamedObject
directory if the current user can gain full access to it, or the BaseNamedObjects\Restricted
directory if not.’
立即 BaseFormatObjectAttributes
跳了出来。这个函数是构造我们初始 OBJECT_ATTRIBUTES
结构的函数。如果我们执行完,我们会发现它稍后调用 BaseGetNamedObjectDirectory
,这是 RootDirectory
在字段中设置的值。对此函数进行一些快速搜索会从 undoc.airesoft.co.uk 返回一些社区文档。提供的函数概述是,它返回当前会话的命名对象目录的句柄,在备注中指出“如果当前用户可以完全访问该目录,则返回的句柄可能引用该 BaseNamedObject
目录,如果不能,则引用 BaseNamedObjects\Restricted
该目录。
Returning to WinObj will give us a better visual image of the issue this causes:
返回到 WinObj 将为我们提供更好的视觉图像,了解由此导致的问题:
The RootDirectory
we are passing is being set to \Sessions\1\BaseNamedObjects\
, and it doesn’t seem possible to traverse back past the root directory and to \KnownDlls
. This can be confirmed by using OpenFileMapping
to successfully open a handle to a section included in this directory:
我们正在传递的 RootDirectory
被设置为 \Sessions\1\BaseNamedObjects\
,并且似乎无法遍历根目录和 \KnownDlls
。这可以通过使用 成功 OpenFileMapping
打开此目录中包含的部分的句柄来确认:
Conclusion + Workaround 结论 + 解决方法
So that’s it – that’s why you can’t use OpenFileMapping
to open the KnownDlls
mapped section. Is there a way around it? Yep but it’s a stupid amount of work in order to call OpenFileMapping
when you could just call NtOpenSection
, and also requires us importing functions from the hokoed version of ntdll.dll
– which is exactly what we are trying to bypass. But we’ll do it anyway because who doesn’t love wasting time overengineering solutions to problems that they’ve made up 🙂
就是这样 – 这就是为什么你不能用来 OpenFileMapping
打开映射的部分 KnownDlls
。有没有办法解决它?是的,但是为了在您可以调用时调用 OpenFileMapping
NtOpenSection
,这是一个愚蠢的工作量,并且还需要我们从hokoed版本导入函数 ntdll.dll
– 这正是我们试图绕过的。但无论如何我们都会这样做,因为谁不喜欢浪费时间过度设计解决方案来解决他们编造的问题:)
The over-the-top workaround is symlinks, as inspired by James Forshaw in https://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html.
过度的解决方法是符号链接,正如 https://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html 年 James Forshaw 的启发。
We can create a symlink to to \GLOBAL??
and then use it in the path to the OpenFileMapping
call:
我们可以创建一个指向 to 的 \GLOBAL??
符号链接,然后在 OpenFileMapping
调用的路径中使用它:
#include <windows.h>
#include <stdio.h>
#include <winternl.h>
#define NT_SUCCESS(status) (((NTSTATUS)(status)) >= 0)
#define SYMBOLIC_LINK_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | 0x1)
typedef VOID(NTAPI *_RtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSTATUS (WINAPI * _BaseGetNamedObjectDirectory)(HANDLE* phDir);
typedef NTSTATUS(NTAPI* _NtCreateSymbolicLinkObject)(PHANDLE LinkHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PUNICODE_STRING TargetName);
HANDLE CreateSymlink(LPCWSTR linkname, LPCWSTR targetname) {
HANDLE rootDir = NULL;
HANDLE hNtdll = GetModuleHandleW(L"NTDLL");
NTSTATUS status = NULL;
_RtlInitUnicodeString fRtlInitUnicodeString = (_RtlInitUnicodeString) GetProcAddress(hNtdll, "RtlInitUnicodeString");
_NtCreateSymbolicLinkObject fNtCreateSymbolicLinkObject = (_NtCreateSymbolicLinkObject) GetProcAddress(hNtdll, "NtCreateSymbolicLinkObject");
_BaseGetNamedObjectDirectory fBaseGetNamedObjectDirectory = (_BaseGetNamedObjectDirectory) GetProcAddress(GetModuleHandleW(L"kernel32"), "BaseGetNamedObjectDirectory");
if (!fRtlInitUnicodeString || !fNtCreateSymbolicLinkObject || !fBaseGetNamedObjectDirectory) {
printf("[!] Error resolving functions:\n");
printf("\tfRtlInitUnicodeString: %x\n", fRtlInitUnicodeString);
printf("\tfNtCreateSymbolicLinkObject: %x\n", fNtCreateSymbolicLinkObject);
printf("\tfBaseGetNamedObjectDirectory: %x\n",fBaseGetNamedObjectDirectory);
return NULL;
}
OBJECT_ATTRIBUTES objAttr;
UNICODE_STRING name;
UNICODE_STRING target;
HANDLE hLink = NULL;
fRtlInitUnicodeString(&name, linkname);
fRtlInitUnicodeString(&target, targetname);
status = fBaseGetNamedObjectDirectory(&rootDir);
if (!NT_SUCCESS(status)) {
printf("[!] Error calling BaseGetNamedObjectDirectory: %0.8X\n", status);
return NULL;
}
InitializeObjectAttributes(&objAttr, &name, OBJ_CASE_INSENSITIVE, rootDir, NULL);
status = fNtCreateSymbolicLinkObject(&hLink, SYMBOLIC_LINK_ALL_ACCESS, &objAttr, &target);
if (NT_SUCCESS(status)) {
printf("[i] Created link %ls -> %ls: %p\n", linkname, targetname, hLink);
} else {
printf("[!] Error creating link: %ls -> %ls\n", linkname, targetname);
}
CloseHandle(hNtdll);
return hLink;
}
INT main(VOID) {
HANDLE hNtdll = NULL;
HANDLE symlinkRedirector = NULL;
puts("Starting execution. Press enter to continue...");
getchar();
if (!(symlinkRedirector = CreateSymlink(L"inbits", L"\\GLOBAL??"))) {
printf("[!] CreateSymlink failed\n");
return 1;
}
hNtdll = OpenFileMappingW(FILE_MAP_READ, FALSE, L"inbits\\GLOBALROOT\\KnownDlls\\ntdll.dll");
if (!hNtdll || hNtdll == INVALID_HANDLE_VALUE) {
printf("[!] OpenFileMappingW failed with error: %d\n", GetLastError());
return 1;
}
printf("[i] Opened a handle to ntdll.dll: %x\n", hNtdll);
getchar();
/*
Actually overwrite the hooked ntdll.dll with the clean one
*/
CloseHandle(symlinkRedirector);
CloseHandle(hNtdll);
return 0;
}