Using low-level Windows Native API functions dump LSASS process memory
Prerequisites: Enabling SeDebugPrivilege
Since lsass.exe
is a protected system process, we must first enable the SeDebugPrivilege
to gain the necessary permissions. This privilege allows our process to open handles to protected processes and read their memory.
Step 1: Enumerating Running Processes
We use the NtQuerySystemInformation
API with the SystemProcessInformation
(value: 5) information class to retrieve an array of SYSTEM_PROCESS_INFORMATION
structures containing details about all running processes. Replace using CreateToolhelp32Snapshot
create snapshot.
|
|
The SYSTEM_PROCESS_INFORMATION structure contains various process details, including the process name stored in Unicode format:
|
|
I iterate through the returned structures in a while loop, searching for the process with the image name matching “lsass.exe”.
To make the process names human-readable, we convert the Unicode strings to standard Rust strings. This conversion will later be replaced with DJB2 hash-based obfuscation for evasion purposes.
|
|
Step 2: Querying LSASS Process Information
Once we obtain a handle to the lsass.exe
process, we need to locate the lsasrv.dll
module, which contains the credential storage mechanisms. We use NtQueryInformationProcess
with the ProcessBasicInformation
class (value: 0) to retrieve the Process Environment Block (PEB) address.
Use NtQueryInformationProcess
query lsass.exe process information
|
|
Step 3: Traversing the PEB to Find lsasrv.dll
Since we’re querying another process’s memory space, we cannot directly access its PEB. Instead, we must read the target process’s virtual memory using NtReadVirtualMemory
. By traversing the PEB structure and reading the loader data structures, we can enumerate loaded modules and locate lsasrv.dll
.
|
|
Step 4: Querying Virtual Memory Information
After locating the lsasrv.dll
base address, we use NtQueryVirtualMemory
to retrieve detailed information about the memory regions.
|
|
Understanding Windows Virtual Memory Structure
Windows virtual memory is organized into pages, with the standard page size being 4KB (0x1000 bytes). A process’s virtual address space is divided into numerous pages, each with its own state, protection attributes, and base address.
MEMORY_BASIC_INFORMATION Structure
When calling VirtualQueryEx
(or NtQueryVirtualMemory
), the system returns a MEMORY_BASIC_INFORMATION
structure containing critical fields:
-
BaseAddress: The starting virtual address of the memory region
-
RegionSize: The size of this region (typically multiple pages combined)
-
State: The allocation state of the memory:
MEM_COMMIT
: Memory is allocated and backed by physical pages (accessible)MEM_RESERVE
: Address space is reserved but not backed by physical memoryMEM_FREE
: Memory is not allocated
-
Protect: Access permissions for the pages:
PAGE_NOACCESS
: No access allowedPAGE_READONLY
: Read-only accessPAGE_READWRITE
: Read and write accessPAGE_EXECUTE_READWRITE
: Execute, read, and write accessPAGE_GUARD
: Guard page for debugging or exception triggering
|
|
Step 5: Filtering Valid Memory Regions
We establish the boundaries for memory scanning:
|
|
We need to skip unreadable or unallocated memory blocks. We only process memory regions that are committed (MEM_COMMIT) and have at least some access permissions (not PAGE_NOACCESS).
|
|
Handling Memory Region Boundaries
NtQueryVirtualMemory
returns a region that may contain one or multiple pages. Sometimes a region is only one page size (0x1000 bytes), which typically indicates:
- Special protection boundaries
- Guard pages
- Specific allocation patterns
When performing memory dumps or module scanning, we need to merge adjacent regions to correctly capture complete modules.
Region Merging Logic
We check if region_size
equals 0x1000. If so, this typically indicates the beginning of a new module. We then accumulate subsequent regions. Finally, we use NtReadVirtualMemory
to convert the memory region into raw bytes and write them to our dump buffer.
Step 6: Creating a Minidump File
The final step involves constructing a binary file similar to a Windows minidump (MDMP format) with the following structure:
- Header (fixed 32 bytes, including signature “MDMP”)
- Stream Directory (each stream entry = 12 bytes: streamType (u32), size (u32), offset (u32))
- SystemInfo Stream (fixed 56 bytes implementation, varies by OS version)
- ModuleList Stream (contains module entries + module path blocks)
- Memory64List Stream (contains memory-region list + offset pointing to actual memory bytes)
- regions_memdump (actual memory bytes read, placed at the end of the file)
Testing and Results
We can verify the dump file using pypykatz:
|
|
Examining the Import Address Table (IAT)
Using PeStudio to inspect the IAT reveals the imported functions:
The detection is likely due to not implementing DJB2 or other hash-based obfuscation methods for API calls.
Antivirus Evasion Results
Initial VirusTotal scan detected 1/72:
However, rule-based detection systems are relatively easy to bypass. After minor modifications and re-uploading, the detection rate dropped to 0/72: