It’s Morphin’ Time: Self-Modifying Code Sections with WriteProcessMemory for EDR Evasion

Thiago Peixoto
12 min readApr 30, 2024

--

The Mockingjay process injection technique was designed to prevent the allocation of a buffer with RWX permission, typically used for executing malicious code, by utilizing a trusted DLL that encompasses an RWX section. However, this led to a dependency on the presence of this DLL. Now, our aim is to achieve the same outcome by enabling self-modification capability in the .text section, aided by WriteProcessMemory.

An EDR can identify malicious activity within a process through mechanisms such as kernel callbacks and userland hooks on commonly used Windows API functions exploited by malware. In the case of kernel callbacks, Windows provides EDRs with the ability to register callback functions to receive events such as process and thread creation, DLL loading, file operations, and registry accesses. Regarding hooks on Windows API functions, EDRs perform hooking of these functions in userland for detailed real-time analysis of process behavior. This enables the identification of suspicious or malicious patterns with greater precision.
One common technique used by malware is called self-injection. This technique involves the presence of encrypted/compressed malicious code, which is decrypted/unpacked in memory by the malware during its execution. Subsequently, the malware transfers execution to this code in memory. This is useful for concealing the malicious code and functionality, making detection by security software more difficult. The figure below illustrates how this technique works using Windows API functions. Initially, the malware allocates memory for the obfuscated code using functions such as VirtualAlloc. After the code deobfuscation step, it employs the VirtualProtect function to make this memory area executable. Subsequently, control of the malware’s execution is transferred to this memory area through functions like CreateThread. This process can be summarized in the figure below.

To detect patterns of self-injection code, EDRs often apply hooks to functions of the Windows API involved in this process. These hooks serve as strategically positioned interception points within the application’s code, aiming to identify and block malicious software behaviors, enabling analysis of parameters passed to Windows API functions or system calls to pinpoint suspicious activities. An article published on the Security Joes blog discusses a technique called Process Mockingjay, which aims to bypass this protection imposed by the EDR. This technique proposes the use of trusted DLLs with RWX sections, thereby avoiding calls to the aforementioned Windows API functions and consequently eluding detection of malicious behavior by the EDR.

Process Mockingjay

Process Mockingjay utilizes RWX sections present in trusted DLLs to store and execute malicious code. It’s worth noting that this idea had already been discussed within the UnKnoWnCheaTs forum community. I must confess that the idea which led to the writing of the article on Process Mockingjay in Security Joes came after reading the article ‘SysWhispers is dead, long live SysWhispers!’ This article discusses the need for SysWhispers2’s evolution, a tool that utilizes direct syscalls for EDR evasion. This prompted me to delve into various methods of Direct Syscalls and Indirect Syscalls for bypassing hooks in userland; specifically, their functioning and shortcomings. Some of these methods can be summarized in the table below.

During the analysis of these techniques, I was able to identify two approaches for creating system call stubs:

· System call stubs are placed within the executable file itself, constituting a direct system call. However, this approach has a disadvantage: EDRs, through kernel callbacks, can detect if the return address of a system call is within the memory area where NTDLL.DLL is mapped. If it isn’t, this represents a clear Indicator of Compromise (IOC).

· Memory allocations with RWX permissions are carried out using Windows API functions such as NtWriteVirtualMemory and NtProtectVirtualMemory, with system call stubs being copied into these areas. In the case of SysWhispers3, these stubs contain jumps to the syscall instructions within the memory area where NTDLL.DLL is mapped, addressing the issue of direct system calls. However, as mentioned earlier, these Windows API functions are often hooked by EDRs, which may lead to detection of system call stubs.

Understanding this, if there existed a pre-allocated RWX memory region where I could dynamically insert system call stubs, I could effectively tackle the aforementioned issues. This shift in focus led to the exploration of RWX areas within trusted binaries, ultimately giving rise to Process Mockingjay.

Windows System Calls

To maintain brevity, a more fundamental explanation of Windows system calls has been relocated to a separate concise article. If needed, please visit it before proceeding with this read.

As mentioned earlier, hooks placed by EDRs are used to intercept the execution flow of an application, enabling activities such as detecting malicious behavior by inspecting parameters in commonly used system calls employed by malware developers. Two common methods for achieving this are:

· Import Address Table Hooking (IAT Hooking): The Import Address Table (IAT) is a data structure in a program’s memory that stores addresses of functions and procedures imported from external libraries or DLLs. Through IAT hooking, an EDR can replace a function’s address in the IAT with its own, allowing it to inspect the original function parameters before allowing execution to proceed.

· Inline Hooks: When an application is launched, the EDR receives a notification about the new process creation and attaches its own dynamic library. Once executed, the EDR modifies specific functions within the in-memory copy of NTDLL.DLL by altering the byte code. By comparing the in-memory copy with the on-disk version of the NtProtectVirtualMemory function in NTDLL.DLL, we can observe that the EDR has replaced the mov eax, 50 instruction with a jmp instruction, as illustrated in the table below. This jmp instruction facilitates an unconditional jump to a memory location where inspections for the parameters of NtProtectVirtualMemory are performed. If any malicious activity is detected, the EDR promptly halts the process execution. EDRs prioritize hooking NTDLL.DLL due to its role as the intermediary layer between user applications and the Windows kernel, as shown in the previous image.

Self-Modifying Code Sections

The Process Mockingjay technique aims to utilize trusted DLLs with RWX sections for executing malicious code and creating system call stubs, but it introduces an obvious dependency: a trusted DLL with RWX sections. Although the DLL doesn’t need to be distributed alongside the executable file (it could be downloaded from the internet, for example), there’s still the need for the application to load the DLL. EDRs can utilize kernel callbacks to identify the very few trusted DLLs with these characteristics and prevent their loading.

For a while, I pondered a way to eliminate this dependency. The most obvious approach would be to create executable files with RWX sections, yet the presence of such sections may indicate binaries whose behavior is malicious or potentially dangerous. An EDR could detect the presence of these sections and classify the file as malicious during static analysis, right after the file is saved to disk.

During a check on LinkedIn, I had the opportunity to read an interesting article written by John Sherchan about the functionality of the Windows API function WriteProcessMemory. The article analyzes the code of this function in ReactOS and explains that if a memory section has RX permissions, WriteProcessMemory converts this section to RWX using NtProtectVirtualMemory, writes the buffer content using NtWriteVirtualMemory, and then restores the RX permission using NtProtectVirtualMemory again. What does this mean? It means that we can overwrite the code of the executable file itself using WriteProcessMemory or use it to write to the code section of another process, as demonstrated by Xavi Márquez Gonzalez.

The fact that we can use WriteProcessMemory to write to RX sections means that we are able to modify the code contained in the code section of our executable file. High-level languages (almost) do not provide support for us to define how final code will be generated, so we cannot blindly change any code area, as we would run a huge risk of overwriting a crucial part of a function necessary for our application’s execution, which would likely result in the application crashing. However, in lower-level languages, we can define a safe area in the application’s code so that we can write code at runtime through WriteProcessMemory. But first, let’s run a test with WriteProcessMemory to verify if we can securely write to the code section of our executable file. We’ll start with a simple C code that will calculate the factorial of any number and display its result.

#include <stdio.h>

unsigned long long factorial(int n)
{
return (n == 0 || n == 1) ? 1 : n * factorial(n - 1);
}

int main(int argc, char *argv[])
{
int num;
printf("Enter a positive integer: ");
scanf_s("%d", &num);
if (num < 0)
printf("Factorial is not defined for negative numbers.\n");
else
printf("The factorial of %d is %llu\n", num, factorial(num));
return 0;
}

When analyzing the code generated by the binary in IDA Pro, we can identify all the functions used by our code being called without any issues.

Now, let’s create an assembly code that will simply perform a jump to an address that will be overwritten later by WriteProcessMemory.

.code
PUBLIC placeholder
placeholder PROC
loc: jmp qword ptr [loc+6]
DB 8 dup (0)
placeholder ENDP
END

Now, let’s modify the C code to use WriteProcessMemory to overwrite the target address of the jump performed by the placeholder code. I’ll be hiding the same parts of the code for brevity.

#include <Windows.h>

extern "C" int placeholder();

typedef int (*printfptr)(const char*, ...);
typedef int (*scanfsptr)(const char*, ...);
typedef int (*factorialptr)(int n);

int main() {
int num;
SIZE_T numBytesWritten;

LPVOID functionptr = &printf;
LPVOID* functionptrptr = &functionptr;
::WriteProcessMemory(::GetCurrentProcess(),
(LPVOID)((PBYTE)placeholder + 6),
functionptrptr,
sizeof(LPVOID),
&numBytesWritten
);
((printfptr)placeholder)("Enter a positive integer: ");

functionptr = &scanf_s;
::WriteProcessMemory(::GetCurrentProcess(),
(LPVOID)((PBYTE)placeholder + 6),
functionptrptr,
sizeof(LPVOID),
&numBytesWritten
);
((scanfsptr)placeholder)("%d", &num);

// Do the same for all the functions in the code

As we could identify in the code above, all functions called by our code (except for WriteProcessMemory and GetCurrentProcess) will be redirected to our placeholder function. We can verify this in IDA Pro. This would be enough to confuse someone performing static analysis of our executable file. :)

As mentioned earlier, the Windows API function WriteProcessMemory internally calls NtProtectVirtualMemory and NtWriteVirtualMemory. However, these functions are often intercepted by EDRs, as can be observed in this repository by Mr.Un1k0d3r RingZer0 Team. Therefore, any attempt to write malicious code into the application’s code section could be detected by the EDR. Furthermore, the code area containing our malicious code must be large enough to accommodate it without overwriting adjacent function codes, which could cause unpredictable behavior in our application. This raises the question: how can we ensure that the code area will have the appropriate size to accommodate our malicious code?

· One approach is to create a dummy function in a high-level language that generates a large number of bytes which can be overwritten later. In this case, we would need to check the number of bytes in the function after the binary has been compiled, and if necessary, add or remove code until the code area reaches the ideal size.

· Alternatively, we can create a dummy function in Assembly and manually fill in the function’s bytes or use the MASM Assembler directive DB <N> DUP(V) to reserve a specified number of bytes (N) with the value (V) within our function body, which would simplify the process.

To address the two aforementioned issues, we’ll explore a method to self-modify our code without using functions commonly hooked by EDR and generate a secure code area that can be overwritten without affecting adjacent code.

We’ll first generate a system call stub for NtProtectVirtualMemory to alter permissions of our secure code area for writing the new code. Since system call numbers change with each Windows version, we’ll implement a function to dynamically retrieve these numbers. Additionally, our stub will jump to a code region within NTDLL.DLL to evade any detection by EDR of potentially malicious behavior. This process can be summarized in the figure below:

Initially, we retrieve the address of NtProtectVirtualMemory and verify if the bytes in the system call stub correspond to the instruction mov eax,??. If affirmative, we return the system call number from the instruction. If negative, we recursively search for system call numbers in adjacent system call stubs. Knowing that the numbers are generated sequentially, it’s simply a matter of addition or subtraction, depending on the stub being analyzed. Additionally, we obtain the address of the first instruction after the jmp instruction placed by the EDR and replace it along with the system call number in our own system call stub. By doing so, we set the system call number in the RAX register and jump to an address within NTDLL.DLL, as planned.

Now that we’ve learned how to create our stub for NtProtectVirtualMemory, we can focus on building a secure code area in the .text section that will be replaced by our malicious code. Let’s craft an Assembly code with two functions: syscall_placeholder and function_placeholder. The former will contain 64 bytes of code to store our system call stub for NtProtectVirtualMemory, written only once with WriteProcessMemory. The latter function will hold our malicious code, with a size specified by the attacker. In our case, we’ve reserved 4096 bytes. Below is the code for reference.

.CODE
PUBLIC function_placeholder
PUBLIC syscall_placeholder

function_placeholder PROC
DB 4096 DUP(0)
function_placeholder ENDP

syscall_placeholder PROC
DB 64 DUP(0)
syscall_placeholder ENDP

END

The attack will follow this sequence: first, we will use the WriteProcessMemory function to insert our system call stub for NtProtectVirtualMemory into the code area of the syscall_placeholder placeholder. Next, we will employ this system call stub to control the permissions of the syscall_placeholder code area. Temporarily, we will change this area to RWX, insert our malicious code, and then restore the permissions to RX. As mentioned earlier, due to the use of our own system call stub, the EDR will not be able to intercept calls to NtProtectVirtualMemory and hooks in userland. This will enable the generation of malicious code in the .text section at runtime, resulting in a self-modifying executable file. It is important to note that the code inserted in this section must be position-independent, otherwise it may generate invalid references to application code and data. Additionally, if necessary, a step to adjust references for program code and data can be added in the malicious code before its execution. The entire process can be viewed in the diagram below.

The figure below demonstrates the .text section before and after the modification and execution of the malicious code. It also offers a glimpse into the dynamic process of the self-injection technique, illustrating how the binary’s code evolves during runtime to evade static analysis commonly employed by traditional security solutions.

Conclusion

In conclusion, while the self-injection technique presented in this article represents a novel approach to evade Endpoint Detection and Response (EDR) systems, it’s important to note that modifying the .text section of binaries is not a new technique. Historically, malware authors have utilized various methods to manipulate executable code to evade detection by security solutions.

However, the significance of this technique lies in its ability to automate the modification of the .text section, thereby complicating static analysis and requiring EDR solutions to continuously monitor for changes in code behavior. This automation introduces a new level of sophistication to evasion tactics, posing a significant challenge to EDR vendors and highlighting the need for continuous innovation in threat detection and response capabilities.

Moreover, it’s important to acknowledge that this method does not bypass kernel callbacks or other application behavior collection methods. This means that any malicious actions detectable through these methods will still be caught.

As the cybersecurity landscape continues to evolve, both attackers and defenders must adapt to emerging threats and countermeasures. EDR vendors need to enhance their detection capabilities to effectively identify and mitigate evasion techniques, while organizations must bolster their security posture by implementing proactive measures to defend against advanced attacks.

In summary, while modifying the .text section is not a new concept, the automated self-injection technique exemplifies the ongoing evolution of evasion tactics in cybersecurity. By acknowledging the capabilities and limitations of such techniques, organizations can better prepare themselves to defend against sophisticated threats and mitigate the impact of cyber-attacks.

A Quick Sidenote

One could argue that only NtProtectVirtualMemory would be necessary for the initial creation of the system call stub, which is, in turn, invoked by WriteProcessMemory. That argument holds true. As the purpose of the blog post was to elucidate the steps leading to the conception of this method, I opted to use WriteProcessMemory for stub creation itself. However, for the Proof of Concept (PoC), only NtProtectVirtualMemory will be employed.

--

--

Thiago Peixoto
Thiago Peixoto

Written by Thiago Peixoto

Reverse Engineer | Malware Analyst | Offensive Security Engineer | Information Security Analyst | Speaker

No responses yet