{
	"id": "3707d1df-b4a0-492f-9f71-23a9381200f5",
	"created_at": "2026-04-06T00:11:40.935758Z",
	"updated_at": "2026-04-10T13:12:14.146999Z",
	"deleted_at": null,
	"sha1_hash": "827cf3b5a8d1360d8cd744d19bff71ad9b2bffcf",
	"title": "A blueprint for evading industry leading endpoint protection in 2022",
	"llm_title": "",
	"authors": "",
	"file_creation_date": "0001-01-01T00:00:00Z",
	"file_modification_date": "0001-01-01T00:00:00Z",
	"file_size": 3532278,
	"plain_text": "A blueprint for evading industry leading endpoint protection in\r\n2022\r\nBy vivami\r\nPublished: 2022-04-18 · Archived: 2026-04-05 14:40:36 UTC\r\nHome\r\nBlog\r\nProjects\r\nMonday. April 18, 2022 - 19 mins\r\nAbout two years ago I quit being a full-time red team operator. However, it still is a field of expertise that stays\r\nvery close to my heart. A few weeks ago, I was looking for a new side project and decided to pick up an old red\r\nteaming hobby of mine: bypassing/evading endpoint protection solutions.\r\nIn this post, I’d like to lay out a collection of techniques that together can be used to bypassed industry leading\r\nenterprise endpoint protection solutions. This is purely for educational purposes for (ethical) red teamers and\r\nalike, so I’ve decided not to publicly release the source code. The aim for this post is to be accessible to a wide\r\naudience in the security industry, but not to drill down to the nitty gritty details of every technique. Instead, I will\r\nrefer to writeups of others that deep dive better than I can.\r\nIn adversary simulations, a key challenge in the “initial access” phase is bypassing the detection and response\r\ncapabilities (EDR) on enterprise endpoints. Commercial command and control frameworks provide unmodifiable\r\nshellcode and binaries to the red team operator that are heavily signatured by the endpoint protection industry and\r\nin order to execute that implant, the signatures (both static and behavioural) of that shellcode need to be\r\nobfuscated.\r\nIn this post, I will cover the following techniques, with the ultimate goal of executing malicious shellcode, also\r\nknown as a (shellcode) loader:\r\n1. Shellcode encryption\r\n2. Reducing entropy\r\n3. Escaping the (local) AV sandbox\r\n4. Import table obfuscation\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 1 of 10\n\n5. Disabling Event Tracing for Windows (ETW)\r\n6. Evading common malicious API call patterns\r\n7. Direct system calls and evading “mark of the syscall”\r\n8. Removing hooks in ntdll.dll\r\n9. Spoofing the thread call stack\r\n10. In-memory encryption of beacon\r\n11. A custom reflective loader\r\n12. OpSec configurations in your Malleable profile\r\n1. Shellcode encryption\r\nLet’s start with a basic but important topic, static shellcode obfuscation. In my loader, I leverage a XOR or RC4\r\nencryption algorithm, because it is easy to implement and doesn’t leave a lot of external indicators of encryption\r\nactivities performed by the loader. AES encryption to obfuscate static signatures of the shellcode leaves traces in\r\nthe import address table of the binary, which increase suspicion. I’ve had Windows Defender specifically trigger\r\non AES decryption functions (e.g. CryptDecrypt , CryptHashData , CryptDeriveKey etc.) in earlier versions of\r\nthis loader.\r\nOutput of dumpbin /imports, an easy giveaway of only AES decryption functions being used in the binary.\r\n2. Reducing entropy\r\nMany AV/EDR solutions consider binary entropy in their assessment of an unknown binary. Since we’re\r\nencrypting the shellcode, the entropy of our binary is rather high, which is a clear indicator of obfuscated parts of\r\ncode in the binary.\r\nThere are several ways of reducing the entropy of our binary, two simple ones that work are:\r\n1. Adding low entropy resources to the binary, such as (low entropy) images.\r\n2. Adding strings, such as the English dictionary or some of \"strings C:\\Program\r\nFiles\\Google\\Chrome\\Application\\100.0.4896.88\\chrome.dll\" output.\r\nA more elegant solution would be to design and implement an algorithm that would obfuscate (encode/encrypt)\r\nthe shellcode into English words (low entropy). That would kill two birds with one stone.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 2 of 10\n\n3. Escaping the (local) AV sandbox\r\nMany EDR solutions will run the binary in a local sandbox for a few seconds to inspect its behaviour. To avoid\r\ncompromising on the end user experience, they cannot afford to inspect the binary for longer than a few seconds\r\n(I’ve seen Avast taking up to 30 seconds in the past, but that was an exception). We can abuse this limitation by\r\ndelaying the execution of our shellcode. Simply calculating a large prime number is my personal favourite. You\r\ncan go a bit further and deterministically calculate a prime number and use that number as (a part of) the key to\r\nyour encrypted shellcode.\r\n4. Import table obfuscation\r\nYou want to avoid suspicious Windows API (WINAPI) from ending up in our IAT (import address table). This\r\ntable consists of an overview of all the Windows APIs that your binary imports from other system libraries. A list\r\nof suspicious (oftentimes therefore inspected by EDR solutions) APIs can be found here. Typically, these are\r\nVirtualAlloc , VirtualProtect , WriteProcessMemory , CreateRemoteThread , SetThreadContext etc.\r\nRunning dumpbin /exports \u003cbinary.exe\u003e will list all the imports. For the most part, we’ll use Direct System\r\ncalls to bypass both EDR hooks (refer to section 7) of suspicious WINAPI calls, but for less suspicious API calls\r\nthis method works just fine.\r\nWe add the function signature of the WINAPI call, get the address of the WINAPI in ntdll.dll and then create\r\na function pointer to that address:\r\ntypedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD flNewProtect, PDWORD lpflOldPro\r\npVirtualProtect fnVirtualProtect;\r\nunsigned char sVirtualProtect[] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };\r\nunsigned char sKernel32[] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };\r\nfnVirtualProtect = (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect\r\n// call VirtualProtect\r\nfnVirtualProtect(address, dwSize, PAGE_READWRITE, \u0026oldProt);\r\nObfuscating strings using a character array cuts the string up in smaller pieces making them more difficult to\r\nextract from a binary.\r\nThe call will still be to an ntdll.dll WINAPI, and will not bypass any hooks in WINAPIs in ntdll.dll , but\r\nis purely to remove suspicious functions from the IAT.\r\n5. Disabling Event Tracing for Windows (ETW)\r\nMany EDR solutions leverage Event Tracing for Windows (ETW) extensively, in particular Microsoft Defender\r\nfor Endpoint (formerly known as Microsoft ATP). ETW allows for extensive instrumentation and tracing of a\r\nprocess’ functionality and WINAPI calls. ETW has components in the kernel, mainly to register callbacks for\r\nsystem calls and other kernel operations, but also consists of a userland component that is part of ntdll.dll\r\n(ETW deep dive and attack vectors). Since ntdll.dll is a DLL loaded into the process of our binary, we have\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 3 of 10\n\nfull control over this DLL and therefore the ETW functionality. There are quite a few different bypasses for ETW\r\nin userspace, but the most common one is patching the function EtwEventWrite which is called to write/log\r\nETW events. We fetch its address in ntdll.dll , and replace its first instructions with instructions to return 0\r\n( SUCCESS ).\r\nvoid disableETW(void) {\r\n// return 0\r\nunsigned char patch[] = { 0x48, 0x33, 0xc0, 0xc3}; // xor rax, rax; ret\r\nULONG oldprotect = 0;\r\nsize_t size = sizeof(patch);\r\nHANDLE hCurrentProc = GetCurrentProcess();\r\nunsigned char sEtwEventWrite[] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };\r\nvoid *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);\r\nNtProtectVirtualMemory(hCurrentProc, \u0026pEventWrite, (PSIZE_T) \u0026size, PAGE_READWRITE, \u0026oldprotect);\r\nmemcpy(pEventWrite, patch, size / sizeof(patch[0]));\r\nNtProtectVirtualMemory(hCurrentProc, \u0026pEventWrite, (PSIZE_T) \u0026size, oldprotect, \u0026oldprotect);\r\nFlushInstructionCache(hCurrentProc, pEventWrite, size);\r\n}\r\nI’ve found the above method to still work on the two tested EDRs, but this is a noisy ETW patch.\r\n6. Evading common malicious API call patterns\r\nMost behavioural detection is ultimately based on detecting malicious patterns. One of these patters is the order of\r\nspecific WINAPI calls in a short timeframe. The suspicious WINAPI calls briefly mentioned in section 4 are\r\ntypically used to execute shellcode and therefore heavily monitored. However, these calls are also used for benign\r\nactivity (the VirtualAlloc , WriteProcess , CreateThread pattern in combination with a memory allocation\r\nand write of ~250KB of shellcode) and so the challenge for EDR solutions is to distinguish benign from malicious\r\ncalls. Filip Olszak wrote a great blog post leveraging delays and smaller chunks of allocating and writing memory\r\nto blend in with benign WINAPI call behaviour. In short, his method adjusts the following behaviour of a typical\r\nshellcode loader:\r\n1. Instead of allocating one large chuck of memory and directly write the ~250KB implant shellcode into that\r\nmemory, allocate small contiguous chunks of e.g. \u003c64KB memory and mark them as NO_ACCESS . Then\r\nwrite the shellcode in a similar chunk size to the allocated memory pages.\r\n2. Introduce delays between every of the above mentioned operations. This will increase the time required to\r\nexecute the shellcode, but will also make the consecutive execution pattern stand out much less.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 4 of 10\n\nOne catch with this technique is to make sure you find a memory location that can fit your entire shellcode in\r\nconsecutive memory pages. Filip’s DripLoader implements this concept.\r\nThe loader I’ve built does not inject the shellcode into another process but instead starts the shellcode in a thread\r\nin its own process space using NtCreateThread . An unknown process (our binary will de facto have low\r\nprevalence) into other processes (typically a Windows native ones) is suspicious activity that stands out\r\n(recommended read “Fork\u0026Run – you’re history”). It is much easier to blend into the noise of benign thread\r\nexecutions and memory operations within a process when we run the shellcode within a thread in the loader’s\r\nprocess space. The downside however is that any crashing post-exploitation modules will also crash the process of\r\nthe loader and therefore the implant. Persistence techniques as well as running stable and reliable BOFs can help\r\nto overcome this downside.\r\n7. Direct system calls and evading “mark of the syscall”\r\nThe loader leverages direct system calls for bypassing any hooks put in ntdll.dll by the EDRs. I want to avoid\r\ngoing into too much detail on how direct syscalls work, since it’s not the purpose of this post and a lot of great\r\nposts have been written about it (e.g. Outflank).\r\nIn short, a direct syscall is a WINAPI call directly to the kernel system call equivalent. Instead of calling the\r\nntdll.dll VirtualAlloc we call its kernel equivalent NtAlocateVirtualMemory defined in the Windows\r\nkernel. This is great because we’re bypassing any EDR hooks used to monitor calls to (in this example)\r\nVirtualAlloc defined in ntdll.dll .\r\nIn order to call a system call directly, we fetch the syscall ID of the system call we want to call from ntdll.dll ,\r\nuse the function signature to push the correct order and types of function arguments to the stack, and call the\r\nsyscall \u003cid\u003e instruction. There are several tools that arrange all this for us, SysWhispers2 and SysWhisper3 are\r\ntwo great examples. From an evasion perspective, there are two issues with calling direct system calls:\r\n1. Your binary ends up with having the syscall instruction, which is easy to statically detect (a.k.a “mark of\r\nthe syscall”, more in “SysWhispers is dead, long live SysWhispers!”).\r\n2. Unlike benign use of a system call that is called through its ntdll.dll equivalent, the return address of\r\nthe system call does not point to ntdll.dll . Instead, it points to our code from where we called the\r\nsyscall, which resides in memory regions outside of ntdll.dll . This is an indicator of a system call that\r\nis not called through ntdll.dll , which is suspicious.\r\nTo overcome these issues we can do the following:\r\n1. Implement an egg hunter mechanism. Replace the syscall instruction with the egg (some random\r\nunique identifiable pattern) and at runtime, search for this egg in memory and replace it with the\r\nsyscall instruction using the ReadProcessMemory and WriteProcessMemory WINAPI calls. Thereafter,\r\nwe can use direct system calls normally. This technique has been implemented by klezVirus.\r\n2. Instead of calling the syscall instruction from our own code, we search for the syscall instruction in\r\nntdll.dll and jump to that memory address once we’ve prepared the stack to call the system call. This\r\nwill result in an return address in RIP that points to ntdll.dll memory regions.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 5 of 10\n\nBoth techniques are part of SysWhisper3.\r\n8. Removing hooks in ntdll.dll\r\nAnother nice technique to evade EDR hooks in ntdll.dll is to overwrite the loaded ntdll.dll that is loaded\r\nby default (and hooked by the EDR) with a fresh copy from ntdll.dll . ntdll.dll is the first DLL that gets\r\nloaded by any Windows process. EDR solutions make sure their DLL is loaded shortly after, which puts all the\r\nhooks in place in the loaded ntdll.dll before our own code will execute. If our code loads a fresh copy of\r\nntdll.dll in memory afterwards, those EDR hooks will be overwritten. RefleXXion is a C++ library that\r\nimplements the research done for this technique by MDSec. RelfeXXion uses direct system calls NtOpenSection\r\nand NtMapViewOfSection to get a handle to a clean ntdll.dll in \\KnownDlls\\ntdll.dll (registry path with\r\npreviously loaded DLLs). It then overwrites the .TEXT section of the loaded ntdll.dll , which flushes out the\r\nEDR hooks.\r\nI recommend to use adjust the RefleXXion library to use the same trick as described above in section 7.\r\n9. Spoofing the thread call stack\r\nThe next two sections cover two techniques that provide evasions against detecting our shellcode in memory. Due\r\nto the beaconing behaviour of an implant, for a majority of the time the implant is sleeping, waiting for incoming\r\ntasks from its operator. During this time the implant is vulnerable for memory scanning techniques from the EDR.\r\nThe first of the two evasions described in this post is spoofing the thread call stack.\r\nWhen the implant is sleeping, its thread return address is pointing to our shellcode residing in memory. By\r\nexamining the return addresses of threads in a suspicious process, our implant shellcode can be easily identified.\r\nIn order to avoid this, want to break this connection between the return address and shellcode. We can do so by\r\nhooking the Sleep() function. When that hook is called (by the implant/beacon shellcode), we overwrite the\r\nreturn address with 0x0 and call the original Sleep() function. When Sleep() returns, we put the original\r\nreturn address back in place so the thread returns to the correct address to continue execution. Mariusz Banach has\r\nimplemented this technique in his ThreadStackSpoofer project. This repo provides much more detail on the\r\ntechnique and also outlines some caveats.\r\nWe can observe the result of spoofing the thread call stack in the two screenshots below, where the non-spoofed\r\ncall stack points to non-backed memory locations and a spoofed thread call stack points to our hooked Sleep\r\n( MySleep ) function and “cuts off” the rest of the call stack.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 6 of 10\n\nDefault beacon thread call stack.\r\nSpoofed beacon thread call stack.\r\n10. In-memory encryption of beacon\r\nThe other evasion for in-memory detection is to encrypt the implant’s executable memory regions while sleeping.\r\nUsing the same sleep hook as described in the section above, we can obtain the shellcode memory segment by\r\nexamining the caller address (the beacon code that calls Sleep() and therefore our MySleep() hook). If the\r\ncaller memory region is MEM_PRIVATE and EXECUTABLE and roughly the size of our shellcode, then the memory\r\nsegment is encrypted with a XOR function and Sleep() is called. Then Sleep() returns, it decrypts the\r\nmemory segment and returns to it.\r\nAnother technique is to register a Vectored Exception Handler (VEH) that handles NO_ACCESS violation\r\nexceptions, decrypts the memory segments and changes the permissions to RX . Then just before sleeping, mark\r\nthe memory segments as NO_ACCESS , so that when Sleep() returns, it throws a memory access violation\r\nexception. Because we registered a VEH, the exception is handled within that thread context and can be resumed\r\nat the exact same location the exception was thrown. The VEH can simply decrypt and change the permissions\r\nback to RX and the implant can continue execution. This technique prevents a detectible Sleep() hook being in\r\nplace when the implant is sleeping.\r\nMariusz Banach has also implemented this technique in ShellcodeFluctuation.\r\n11. A custom reflective loader\r\nThe beacon shellcode that we execute in this loader ultimately is a DLL that needs to be executed in memory.\r\nMany C2 frameworks leverage Stephen Fewer’s ReflectiveLoader. There are many well written explanations of\r\nhow exactly a relfective DLL loader works, and Stephen Fewer’s code is also well documented, but in short a\r\nReflective Loader does the following:\r\n1. Resolve addresses to necessary kernel32.dll WINAPIs required for loading the DLL (e.g.\r\nVirtualAlloc , LoadLibraryA etc.)\r\n2. Write the DLL and its sections to memory\r\n3. Build up the DLL import table, so the DLL can call ntdll.dll and kernel32.dll WINAPIs\r\n4. Load any additional library’s and resolve their respective imported function addresses\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 7 of 10\n\n5. Call the DLL entrypoint\r\nCobalt Strike added support for a custom way for reflectively loading a DLL in memory that allows a red team\r\noperator to customize the way a beacon DLL gets loaded and add evasion techniques. Bobby Cooke and Santiago\r\nP built a stealthy loader (BokuLoader) using Cobalt Strike’s UDRL which I’ve used in my loader. BokuLoader\r\nimplements several evasion techniques:\r\nLimit calls to GetProcAddress() (commonly EDR hooked WINAPI call to resolve a function address, as\r\nwe do in section 4)\r\nAMSI \u0026 ETW bypasses\r\nUse only direct system calls\r\nUse only RW or RX , and no RWX ( EXECUTE_READWRITE ) permissions\r\nRemoves beacon DLL headers from memory\r\nMake sure to uncomment the two defines to leverage direct system calls via HellsGate \u0026 HalosGate and bypass\r\nETW and AMSI (not really necessary, as we’ve already disabled ETW and are not injecting the loader into another\r\nprocess).\r\n12. OpSec configurations in your Malleable profile\r\nIn your Malleable C2 profile, make sure the following options are configured, which limit the use of RWX marked\r\nmemory (suspicious and easily detected) and clean up the shellcode after beacon has started.\r\n set startrwx \"false\";\r\n set userwx \"false\";\r\n set cleanup \"true\";\r\n set stomppe \"true\";\r\n set obfuscate \"true\";\r\n set sleep_mask \"true\";\r\n set smartinject \"true\";\r\nConclusions\r\nCombining these techniques allow you to bypass (among others) Microsoft Defender for Endpoint and\r\nCrowdStrike Falcon with 0 detections (tested mid April 2022), which together with SentinelOne lead the endpoint\r\nprotection industry.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 8 of 10\n\nCrowdStrike Falcon with 0 alerts.\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 9 of 10\n\nWindows Defender (and also Microsoft Defender for Endpoint, not screenshotted) with 0 alerts.\r\nOf course this is just one and the first step in fully compromising an endpoint, and this doesn’t mean “game over”\r\nfor the EDR solution. Depending on what post-exploitation activity/modules the red team operator choses next, it\r\ncan still be “game over” for the implant. In general, either run BOFs, or tunnel post-ex tools through the implant’s\r\nSOCKS proxy feature. Also consider putting the EDR hooks patches back in place in our Sleep() hook to avoid\r\ndetection of unhooking, as well as removing the ETW/AMSI patches.\r\nIt’s a cat and mouse game, and the cat is undoubtedly getting better.\r\nSource: https://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nhttps://vanmieghem.io/blueprint-for-evading-edr-in-2022/\r\nPage 10 of 10",
	"extraction_quality": 1,
	"language": "EN",
	"sources": [
		"Malpedia"
	],
	"origins": [
		"web"
	],
	"references": [
		"https://vanmieghem.io/blueprint-for-evading-edr-in-2022/"
	],
	"report_names": [
		"blueprint-for-evading-edr-in-2022"
	],
	"threat_actors": [],
	"ts_created_at": 1775434300,
	"ts_updated_at": 1775826734,
	"ts_creation_date": 0,
	"ts_modification_date": 0,
	"files": {
		"pdf": "https://archive.orkl.eu/827cf3b5a8d1360d8cd744d19bff71ad9b2bffcf.pdf",
		"text": "https://archive.orkl.eu/827cf3b5a8d1360d8cd744d19bff71ad9b2bffcf.txt",
		"img": "https://archive.orkl.eu/827cf3b5a8d1360d8cd744d19bff71ad9b2bffcf.jpg"
	}
}