Hiding in Plain Sight: A Survey of EDR Evasion Techniques
Adam Hassan / May 2024 (1415 Words, 8 Minutes)
Transcript:
Hiding in Plain Sight A Survey of EDR Evasion Techniques
What is this talk? I will walk you through the steps that I took to evade almost every EDR on the market
I will explain every step and every technique so you can use them in your own malware
ToC EDR vs AV Bypassing AMSI Memory Patching … Bypassing ETW Payload Encryption API Hooking Imports & Import Obfuscation API Hashing Direct Syscalls Indirect Syscalls Stack Spoofing Heuristics Sleep Obfuscation API Hammering Compiler choice
What is an EDR? EDR: Endpoint Detection & Response Tool that does real-time monitoring on a host (endpoint) to find threats Often does so by hooking potentially suspicious API functions May be in user-space or kernel-space
EDR vs AV AV: Antivirus Typically is entirely signature-based This means it is best at finding known threats Not new ones
How can we bypass modern EDR solutions? Try to chain as many steps as possible If there is something we can’t bypass, try to add an extra separate step that will allow us to bypass
If each step is independent from the other, this can be used to make modular malware that is dependent on the EDR solution we are attempting to bypass
First: Bypassing AV through AMSI AMSI: Antimalware Scan Interface API that allows any tool to do signature-based static analysis on scripts. Used by PowerShell, cmd.exe, Windows Defender, and sometimes external AntiVirus Loaded from amsi.dll
Key API Functions: AmsiInitialize AmsiOpenSession AmsiScanBuffer AmsiScanString
Using AMSI Example
Bypassing AMSI - Memory Patching
AmsiScanBuffer Analysis What’s happening here? Function Parameters are being checked If they are invalid, set eax to 0x80070057 (E_INVALIDARG) If the parameters are invalid, don’t scan the buffer
What does this mean? If we can make AmsiScanBuffer think the params are always invalid, we can make it so it never scans the buffer
Patching AmsiScanBuffer pt.1
Patching AmsiScanBuffer pt.2
Patching AmsiScanBuffer pt.3 (Obfuscation)
Without AMSI Bypass
With AMSI Bypass Loading Bypass
Other AMSI Bypass Methods: Fly the Fail Flag PowerShell has an attribute for AMSI integration called amsiInitFailed If this is set to true, AMSI stops Fake amsi.dll Force PowerShell to load a patched version of amsi.dll No longer viable PowerShell will crash if proper interfaces aren’t available in amsi.dll … https://github.com/S3cur3Th1sSh1t/Amsi-Bypass-Powershell
One problem Sadly, AMSI patch will get detected by any decent EDR Done through tamper-protection Sophos:
Alternative Languages: Golang This is a snippet of code in Go that uses golang.org/x/sys/windows and syscall Works exactly like the PowerShell version Sophos doesn’t detect it
Another Alternative: Python From BC-Security/IronSharpPack
- PowerShell AMSI Bypass run and detected by Sophos
- Running Golang AMSI Bypass
- Loading Invoke-Mimikatz into memory doesn’t get detected
Important: AV isn’t that good… AV does a great job at detecting publicly available malware Really bad at detecting custom malware.␋ If your goal is to get a simple reverse shell or C2, you can write both and you probably won’t be detected https://github.com/adamkadaban/C2 (C2 written in Go) https://github.com/BlWasp/rs-shell (Revshell written in Rust)␋ If you want to run somebody else’s tools (eg. Mimikatz), then you use an AMSI bypass
Bypassing ETW
Bypassing ETW via memory patching ETW: Event Tracing for Windows Kernel-level logging for Windows events Is used by many advanced EDRs We can bypass similar to how we bypassed AMSI
Get a handle to our own process Identify the addresses of functions linked to ETW (EtwEventWrite, NtTraceEvent, …) Patch each function with a ret
Now… Finally on to EDR Bypasses
Payload Encryption If we are trying to give our Malware as many steps as possible, we can start by having a dropper/loader that will take encrypted shellcode and execute it in-memory If encrypted properly, it won’t be picked up by signature-based detection Running in memory will prevent analysis that happens on disk. ␋ Important to note that some encryption can look suspicious: XOR is known to be used for malware and can often be brute-forced AES is similar. You can use your own AES library HTTPS can be good, as it is common
Imports
Imports are a big deal IAT: Import Address Table Simple AVs can look at imports to determine what a program might be doing␋ Many EDRs will also hook API functions (either in user-space or kernel-space) to detect when a function is called
How do EDRs work?
NTDLL.dll without API Hooks
NTDLL.dll without API Hooks
NTDLL.dll with API Hooks Hooked Not everything is hooked
All EDRs work differently They will look at one or more of: Imports Strings Functions called In user-space In kernel-space In what order they are called … Parameters in functions called …
IAT Obfuscation through API Hashing We can dynamically load functions like so:
IAT Obfuscation through API Hashing Okay, so we got rid of some of the imports, but we still have strings in the binary
IAT Obfuscation through API Hashing
We can fix that with function hashing: Key Terms: PEB: Process Environment Block Structure containing information about the process (including modules loaded) Export Table Table containing all exported function in a module␋ Obtain the modules loaded in the PEB Loop through the modules and check every exported function If hash(func_you_need) == hash(func_exported_in_module) That means you have the right function and you can use it!
What if an EDR is hooking API calls?
User-mode hooking https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
Direct Syscalls Instead of calling windows API functions as they are defined in a module, we can just write it ourselves with custom assembly.
If we do this, we skip the API function entirely and go straight to the kernel (via the syscall)
User-mode hooking (with direct syscalls) https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
Direct Syscalls: Tools Several tools will implement direct syscalls for you: Syswhispers2 Syswhispers3 Hell’s Gate Halo’s Gate
What if the EDR is hooking syscalls?
The problem with direct syscalls Many modern EDRs have recently been switching to kernel-mode hooking The expectation is that when syscalls for Windows API functions get to the kernel, they come from ntdll.dll If they come from MaliciousProgram.exe instead, we know something is wrong
If we can make syscalls from the memory of ntdll.dll, then we can mitigate detection
Indirect Syscalls: One minor change Direct Syscalls Indirect Syscalls
Indirect Syscalls: One minor change Direct Syscalls Indirect Syscalls Notice the missing ret!
Indirect Syscalls We use the ret that’s in ntdll.dll Think ROP
This technique can be automated with Hell’s Hall
Stack Spoofing Even if we change where something was called and where something returns to, the call stack is still available to an EDR (if it uses ETW) This call stack looks more legitimate, but still allows an EDR to see the program calling the function https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls
Stack Spoofing We control the call stack of the program, because we are running the program We can simply modify the stack to make it look like our indirect syscalls are coming from ntdll.dll instead of from our program Many ntdll.dll exports call each other, so this is completely normal Key Terms: RUNTIME_FUNCTION Structure that contains an entry on the function table (includes stack frame size) Locate the RUNTIME_FUNCTION of the stack frame we want to make Create a fake stack frame: Decrement RSP by stack size, set [RSP] to return address Call jmp [RBX] to jump to the function we want to call
SilentMoonWalk https://github.com/klezVirus/SilentMoonwalk
Tricking Heuristics
Sleep Obfuscation Beacons often sleep (either directly or indirectly) with NtDelayExecution or similar to prevent detection The problem is that these functions are suspicious when they have an unusual stack trace We can bypass detection here by making a “timer queue object”, which will hold timers that execute callbacks. This is a timer that expires at a specific time and then will call a function after that amount of time. Essentially sleep, but it never makes the offending syscall We can use the NtContinue callback to continue executing the thread with a specific context
Sleep Obfuscation To prevent detection while sleeping: Make memory writable Encrypt memory Sleep (CreateTimerQueueTimer) Decrypt memory Make memory unwritable␋ Example implementation: Ekko
API Hammering Certain API functions are rarely suspicious and must take up time File I/O functions are particularly expensive Thus, we can make code “sleep” by doing a lot of useless I/O functions
CreateFileW WriteFile ReadFile
Picking your compiler Mingw always seems to get hits on VirusTotal This includes the Go compiler Even for “Hello, World”
MSVC compiler doesn’t seem to impact detection
Zig as a drop-in compiler has given me very good results at getting past static analysis
Customizing Your Payload For signature-based detection (Windows Defender, …) All you need is AMSI bypass User-mode hooking Direct Syscalls Kernel-mode hooking Indirect Syscalls For EDRs that use ETW (Microsoft Defender ATP (MDATP), Elastic, FortiEDR) ETW Bypass OR Stack Spoofing + Indirect Syscalls
The Final Product AMSI Bypass ->
The Final Product AMSI Bypass -> Loader that download payload ->
The Final Product AMSI Bypass -> Loader that download payload -> API Hammering on loader ->
The Final Product AMSI Bypass -> Loader that download payload -> API Hammering on loader -> Shellcode payload sent over HTTPS ->
The Final Product AMSI Bypass -> Loader that download payload -> API Hammering on loader -> Shellcode payload sent over HTTPS -> Process injection w/ indirect syscalls+stack obfuscation ->
The Final Product AMSI Bypass -> Loader that download payload -> API Hammering on loader -> Shellcode payload sent over HTTPS -> Process injection w/ indirect syscalls+stack obfuscation -> Sleep Obfuscation for injected code
Demo!