One of the popular methods for dumping LSASS is using the procdump.exe program from the Sysinternals Suite. Something like:
procdump64.exe -accepteula -ma <lsass pid> -o dumpfile.dmp
However, Microsoft is well aware of this method, and it is being tracked along with several other common methods and tools.
Now procdump is legitimate software with many use cases and it is signed by Microsoft. From the Microsoft article that discusses preventing LSASS credential dumping, we can see that it’s alerting on procdump with the -ma command line flag (which writes a full dump file) on the LSASS.exe process. So, what if we start procdump with some ordinary, non-suspicious command line arguments, and then swap them out behind the scenes with our LSASS dumping magic. At the time of writing (December 2023) we can successfully dump LSASS undetected on a fully updated Windows 10 machine. On Windows 11, this technique will work, but the resulting dump file will be detected. However, the good news is we will be able to safely secure the contents of the file before Defender can get its paws on it.
At a high level, we will:
We will be using Rust for this. It is a great language for offensive development due to its speed and difficulty to reverse. It’s also my favorite programming language.
Start by creating a new project with:
cargo new proc_noprocdump
We will start off by calling CreateProcessA to start procdump64.exe in a suspended state.
A couple things to note here. First, the non-suspicious arguments we are calling here must be longer than what we are replacing. Our LSASS dumping arguments will be:
-accepteula -ma <lsass pid> -o test.dmp
So as long as the initial arguments are longer than that, we are good. Next, in addition to CREATE_SUSPENDED, we are also passing the CREATE_NEW_CONSOLE flag. This is to allow our program to continue executing while the dump file is being created. This will be important later.
Next, we will use dinvoke_rs to call NtQueryInformationProcess. This library allows us to dynamically call the function, bypassing any API hooks. It also will not create an entry in the Import Address Table.
The function signature for NtQueryInformationProcessis the following:
We will have to create a function pointer with the Rust data type equivalents. HANDLE, and NTSTATUS we can get from the Windows crate. The rest we will use a comparable Rust data type. We can see the out parameter ProcessInformation has a type of PVOID. This will get filled out to be a PROCESS_BASIC_INFORMATION struct. So, we will pass a mutable pointer to that type (courtesy of the Windows crate) in our function signature.
The resulting function pointer will look like the following:
Then we can call the “dynamic_invoke!” macro, giving it the library base address, function name, function pointer, return variable, and our actual NtQueryInformationProcess parameters.
This call will fill the PROCESS_BASIC_INFORMATION struct which contains the base address to the PEB (Process Environment Block). The PEB has a field “ProcessParameters” which is what we’re after.
Before we dive into reading the PEB data, I want to talk a bit about types, type casting, references, and pointers, and how that works in Rust.
In so many WinAPI calls, you pass a pointer to the variable that receives the data. For example, let’s look at the function signature for ReadProcessMemory in MSDN and the Windows crate documentation.
We are going to be calling ReadProcessMemory to read the PebBaseAddress field from the PROCESS_BASIC_INFORMATION struct into a local PEB variable.
In C you could do something like:
PEB peb = NULL;
ReadProcessMemory(…, …, &peb, …, …);
Defining a variable of type PEB and assigning it to NULL. Then passing the pointer to the ReadProcessMemory function to fill out the PEB struct.
However, in Rust, the compiler is very strict on types and there is no “NULL” that we can assign. We also can’t just declare the variable and initialize it later. The compiler will yell at us.
Fortunately, the solution is very simple. Basically, all types will have a default method which can be called to set a default value for the type.
let peb: PEB = PEB::default();
However, if we look back at the lpbuffer variable in ReadProcessMemory, it is expecting a type of *mut c_void. This is very common and most WinAPI calls in Rust will be expecting this type when dealing with buffers and memory addresses.
We can’t just pass a reference &mut peb to the function when it is expecting a pointer of a different type. The compiler will yell at us.
You may be thinking can we just cast &mut peb to *mut c_void? Short answer, no. Long answer, yes.
This is where transmute comes in. This function allows us to perform this cast in a “Rust approved” fashion.
We give it the type that we have, and the type that we want, and pass it the data it will operate on.
let peb: PEB = PEB::default();
let peb_c_void: *mut c_void = mem::transmute::<&PEB, *mut c_void>(&peb);
There are a couple extra steps we need to transform the data, but that’s one of the
headaches joys of Rust 😊.
Getting back on track, now we will make two calls to ReadProcessMemory. The first will be to fill out our peb variable. The second will be to read the ProcessParameters field in the PEB.
Now if we run it, we can see that the memory address where our arguments are. If we attach a debugger to procdump64.exe and go to that address, we can confirm that’s the start of our arguments.
Now we need to create our argument string and write it to memory. If we look at the definition of RTL_USER_PROCESS_PARAMETERS, we see the CommandLine parameter is of type UNICODE_STRING.
The Rust type definition for UNICODE_STRING is as follows:
Since we are dealing with UNICODE and PWSTR, these are all going to be wide char strings. In Rust we will use u16. We will get the PID of LSASS with the sysinfo crate, create our string, encode it to be UTF-16. I mentioned in the beginning that the original arguments need to be longer than the LSASS dump arguments. After we create our new argument string, we will check the length, and if it’s shorter than the original, we will add 0’s to the end so they are the same length. Then we will call WriteProcessMemory to replace the original arguments with our new ones.
Here is our get_pid() function to get the LSASS PID.
Excellent, so now if we inspect the arguments, we can see they have been replaced with our LSASS dumping arguments.
The only caveat with this is that inspecting the process with something like procexp64.exe will show the new arguments.
Let’s fix that.
Looking back at the CommandLine field, we know that the buffer is a UNICODE_STRING. This type has three fields: Length, MaximumLength, and Buffer. We need to find the offset to the Length field, and update that value to be the length of just our call to C:\SysinternalsSuite\procdump64.exe.
We already have a pointer to our ProcessParameters variable where we wrote our arguments. So, we can use that to access the CommandLine field and cast that to a UNICODE_STRING pointer. Then we will get the offset by subtracting our ProcessParameter pointer from our UNICODE_STRING pointer. Lastly, will add this offset to our peb.ProcessParameters variable and this should get us the address of the Length field.
Inspecting the address with a debugger shows 92 in hex, which matches our length of 146 in decimal.
We get the length of our call to C:\Sysinternals\procdump64.exe and multiply it by size of u16 type (since we are dealing with Unicode) and call WriteProcessMemory to update the value.
Looking at the address again, we see the length field is now 70.
If we look at the procdum64.exe now with procexp64 we can see that the LSASS dumping arguments are no longer there.
At this point we can resume the thread and call it a day. We have a capable payload that will dump LSASS on a fully updated Windows 10 machine without Defender batting an eye.
To have this succeed on Windows 11, we have a little more work to do. I should clarify - running this will succeed on Windows 11 and the dump file will get created, however it will get detected by Defender once it’s finished and Defender will delete it.
To overcome this, my thought was that we can read the file as it’s getting written to by procdump and write the contents into a buffer that we can use later on.
This is where kicking off procdump in the new console window is helpful, because now our program can continue. While procdump is doing its thing, we will wait for the file to exist by checking if we get an error or not by trying to open a handle to the dump file. We will run this in a while loop so that it continues to check until the dump file is first created by procdump.
Next, we will use the Tokio Asynchronous Runtime library to open a handle to the file. We will use a tokio Interval to read the file every 500 milliseconds and write the contents to a byte vector. We will do this with the read_to_end method which returns the amount of bytes read and saves the file content in the byte vector. We will keep track of this number and use the seek function to jump to the new portion of the file in each iteration.
While I was testing this, it doesn’t appear that procdump is consistently writing to the file. It writes the data in two big chunks. So, we can’t just wait till the buffer stops growing. Instead, we’ll just give it a minute or so to write all the contents. There is probably a better way to do it, but meh, it’s fine.
Here is our code from resuming the thread to reading the dump file: Notice I set the file path to not write the file to my noscan folder which is immune from Defender. I am also running our proc_noprocdump.exe file on my Desktop as well. So now we are under the microscope with Defender.
Also, a friendly PSA to remind you to turn Automatic sample submission off to keep Microsoft’s grubby paws off your tooling.
When we run it and procdump is creating the dump file, we can see the data being written to the file in two chunks as our count increases.
Funny enough, procdump finishes while our loop is still running, and Defender flags the dump file. However, because we have an open handle to the test.dmp file, Defender is not able to delete it.
Now that we have the LSASS dump file contents safely in memory, we can do with it what we wish. For example, POST it to a web server to extract the contents offline, or encrypt it and write it back to the machine so that you can take it offline and decrypt later.
We will go with the latter.
For our example we will just encrypt the data with RC4 and write it to a file.
When we run it, it will save our encrypted LSASS dump file. Now we don’t have to worry about Defender detecting it! We can just take it offline later to decrypt and feed into mimikatz.
This is a rough PoC that has lots of room for improvement. Some cool optimizations would be encrypting our command line arguments string with a library like litcrypt, downloading procdump from a remote webserver or including it in the binary, and not having to hard code all the file paths.
Ideally, we would not be triggering Defender at all, but at least we are able to achieve the same result, which is getting the LSASS dump file.
Full source code is available here:
Thanks for reading!