Resources:

Sleeping With Control Flow Guard

In this post we demonstrate how to disable Control Flow Guard (CFG) using direct syscalls. CFG is an exploit mitigation security control built into Windows.

By Brian Chamberlain | R&D Lead & Red Team Operator


TL:DR 

Disabling CFG allows an attacker to use sleep obfuscation techniques to evade detection. Additionally, we walk through the basics of reverse engineering an undocumented struct. This is a skill that will continue to be relevant for the security community as detections and detection avoidance rely increasingly on low-level operating system concepts. 

Background 

There has been a lot of discussion in the malware dev world about “sleep obfuscation” techniques. The goal of sleep obfuscation is to protect malware from memory scanning by hiding in Read/Write memory space while sleeping. This is effective because memory scanning is resource-intensive and therefore typically targets only executable regions. 

Recently, a brilliant young security researcher, @C5pider, released a proof-of-concept tool on GitHub called Ekko. This tool implements one of the public sleep obfuscation methods, first introduced by @peterwintrsmith, in an easy-to-understand manner which we will use as a reference throughout this blog post.   

This implementation of sleep obfuscation sets up asynchronous timers using CreateTimerQueueTimer.  Each executes one after the other and does the following tasks: 

  • Set the malware executable memory regions to Read/Write.
  • Encrypt memory region.
  • Wait a specific time period.
  • Decrypt memory region.
  • Set memory back to Read/Execute.

This all works because CreateTimerQueueTimer uses a callback routine to execute a given function on the completion of each timer. The callback routine occurs outside the malware’s memory space and so can execute even when memory is set to Read/Write only.

Enter Control Flow Guard

There is a problem that you will run into if you use this (or similar) type of execution method: Microsoft’s Control Flow Guard (CFG).   

CFG is an anti-exploitation technology included in Windows (starting in Windows 8.1) that prevents arbitrary code from being indirectly executed in a program. This hinders exploits that attempt to disable Data Execution Prevention (DEP) or set read/write-only buffers containing malicious code to executable.

If you compile and run Ekko it will work fine; however, if you compile it with CFG enabled (/guard:cf) and try to run it you will trigger CFG, and the process will be given a quick and painless death. 

Figure 1 – Control Flow Guard error

This is because the callback routine that CreateTimerQueueTimer points to is NtContinue, which is on a special list of functions that cannot be called indirectly with CFG enabled. An indirect function call is a call that is initiated from another function outside the program’s call stack; in this case the call triggered by our timer. Here’s the full list of disallowed function calls and an explanation. 

Since malware rarely runs in its own “Evil.exe” process these days, it will almost always be injected or loaded into a “known good” process, and most of those are compiled with CFG enabled. As malware developers (good, ethical, upstanding malware developers), we need to figure out how to bypass or disable CFG for NtContinue. Thankfully, Microsoft loves malware developers as much as Sam loves Frodo and documented this for us.   

From MSDN: “[SetProcessValidCallTargets] provides CFG with a list of valid indirect call targets and specifies whether they should be marked valid or not.”  

CFG is not actually built to stop the types of attacks we are talking about here. It exists to hinder exploitation by making Use-After-Free and ROP chaining more difficult. As a result, it is easy to take a given memory location and set it to be a valid target if you are in a “normal” code execution situation.  Add this function into the Ekko project, call the function on NtContinue before starting Ekko and….

Figures 2 and 3 – Ekko running with CFG enabled

Great! Problem solved. Except that it is 2022 and we must worry about pesky userland hooks. Now let’s look at what is going on under the hood of SetProcessValidCallTargets to see if it calls into an NT API. 

Testing With Syscalls 

Figure 4 – NtSetInformationVirtualMemory from SetProcessValidCallTargets

Under the hood SetProcessValidCallTargets is calling NtSetInformationVirtualMemory. Unfortunately, this syscall has even less documentation than usual, so we can’t immediately jump into it. Looking around online, we find an older blog post that digs into the required inputs. It is for sure worth a read; however, when attempting to replicate their research, “invalid parameter” errors are returned from the call to NtSetInformationVirtualMemory.   

The original Fortinet post is targeting an x86 process, which requires a different struct than for x64, which is creating the invalid parameter error. 

Reviewing What We Know Works 

We have the previous working function, so let’s walk through it in a debugger and see if we can map out how it successfully passes arguments to NtSetInformationVirtualMemory for an x64 process. 

SetProcessValidCallTargets takes five arguments. X64 calling convention will pass the first four arguments to the registers RCX, RDX, R8, and R9. The final argument will be pushed to the stack. 

Figure 5 – Our SetProcessValidCallTargets Arguments
Figure 6 – SetProcessValidCallTargets Registers

Here we see RCX is 0XFFFFFFFFFFFFFFFF, which in this case is a representation of a handle to the current process. RDX is pointing to 0x00007FF96372D000, the base address of ntdll.dll, which is the DLL that we are modifying. R8 is 0x0000000000080000, the size of the executable region in ntdll. R9 is 0x0000000000000001, the number of memory locations we are modifying.   

Figure 7 – Setting up stack arguments

We now need to figure out how the CFG_CALL_TARGET_INFO struct is set up and stored.   

The relevant areas are highlighted in the screenshot above.   

First, RCX, with value 0x790, is moved to RAX and then RAX is pushed to the stack at RSP + 0x48. This is the first value in our CFG_CALL_TARGET_INFO struct, the offset to NtContinue.   

Next, 0x1 is pushed to the stack at RSP +0x50.  0x1 is the value of the CFG_CALL_TARGET_VALID flag that we passed in as our second value.   

Finally, a pointer to the beginning of the struct, RSP + 0x48 is moved to RAX and then pushed to the stack as RSP + 0x20. This is the standard position of the fifth argument passed to a function in x64. The pointer is to the memory location 0x00000071110FF678, which we need to remember as it will come up later. 

Figure 8 – NTSetInformationVirtualMemory

Here are where the required arguments for NtSetInformationVirtualMemory taken from. 

Since we are getting an invalid parameter error, let’s just step through the entire call to see what the registers and stack look like on syscall.  

Figure 9 – Registers at syscall

RCX is still the representation of local process handle. RDX is 0x2, which matches the VIRTUAL_MEMORY_INFORMATION_CLASS enum of VmCfgCallTargetInformation. R8 is 0x1, the number of memory entries we are modifying and R9 is 0x00000071110FF570, which is a memory pointer to the MEMORY_RANGE_ENTRY struct with two entries that should look familiar from our previous arguments.   

First is the base address of ntdll at 0x00007FF96372D000.  

The second is the size of the executable region that we previously passed as our third argument to SetProcessValidCallTargets. 

Figure 10 – MEMORY_RANGE_ENTRY struct in memory

Defining an Undocumented Struct 

So far, everything looks good. Let’s look at the stack.   

At RSP+0x20, the beginning of our stack-based arguments for NtSetInformationVirtualMemory, we see two entries, which match the final two arguments in the existing NtSetInformationVirtualMemory documentation.   

The first stack argument is a pointer to a memory region, 0x00007FF961489E80, which MSDN says is a variable buffer that depends on the information class that is queried; the second is the size of the buffer.   

Figure 11 – Stack at syscall

Immediately we see a discrepancy from the previous blog. 0x28 (40 bytes) is larger than the 0x10 of the previous research.  

Let’s go to the memory location pointed to by the fifth argument, 0x00000071110FF580, and see what it looks like.   

Figure 12 – 40 Bytes at Memory Pointer

This is the location in memory of the new struct we need to figure out.   

First, we have 0x1, which it is safe to assume refers to the number of offsets passed.  

The second value, 0x00000071110FF5E8, appears to be a pointer to another memory location, which is currently set to zero.   

The third value, 0x00000071110FF678, is also a pointer. This one should look familiar as it is a pointer to the memory previously set aside for our CFG_CALL_TARGET_INFO struct that we passed to SetProcessValidCallTargets. This means that this will be the third value in our new struct.   

Finally, we have 16 bytes of zeroes.   

Let’s step over the syscall and see if any of these values change. 

Figure 13 – Value set from 0 to 1 after syscall

The only value that changes is the value of the second argument at 0x00000071110FF5E8, which has changed from zero to one. This suggests it is a pointer to a numerical value; we’ll say ULONG since it is a common type for NT calls.   

Here is our new struct: 

Figure 14 – New struct

And here is our new function to mark a memory location as valid for CFG: 

Figure 15 – New function

Let’s see if it works. 

Figure 16 – Ekko execution with CFG using NtSetInformationVirtualMemory

In Summary 

We’ve covered how to disable CFG for a given memory location using direct syscalls.   

CFG isn’t generally a hurdle once post-exploitation is reached, but it is something to keep in mind, especially when doing memory gymnastics to hide in memory and evade EDR.   

Additionally, we’ve worked through documenting an undocumented struct to get our code to work correctly.  Interacting with these low-level APIs and structures is becoming a major part of malware development and detection engineering. The better we can document and understand these the more successful we can be.  

The full PoC code can be found on Icebreaker’s GitHub.

Give the Icebreaker team a shout to learn more.