Shut Up and Hack

I like reading more than writing, and, in fact, I don't write too much.

Sandbox Detection: Pafish Overview

After an internal Pafish source code analysis my company asked me to write a blog post about it. You can find the original article published here: https://labs.portcullis.co.uk/blog/sandbox-detection-pafish-overview/

Bellow is a just a local mirror for future reference.

Sandbox detection: Pafish overview

Here at Portcullis, we are frequently involved in “red team” exercises, which means we subject an organisation’s information security systems to rigorous testing and analysis. The opposite of a red team is a “blue team”. A blue team attempts to identify and stop the red team from compromising systems. One of the techniques used when red teaming is to write malicious code to test the security systems of our clients. One of the issues we face resides in the fact that we need to bypass sandbox systems that analyse our files in real-time to identify if the potentially malicious file should be blocked and Indicators Of Compromise (IOCs) generated or if the files are benign and safe. At the same time, blue teams that catch our files will try to reverse engineer them in order to understand how we may be compromising systems. Even though the last point is not really relevant for us (ultimately we’re not the bad guys), the first point is.

In order to be able to mitigate this issue, our code needs to be able to detect if it is being run inside a debugger, a Virtual Machine (VM) or a sandbox. There are some well known open source projects that are able to achieve this that are often used by malware writers. One of the most well known is called Pafish, Paranoid Fish, the code for which can be found in the Pafish GitHub repo. So I decided to take a look at the code and go through all the tricks Pafish has in order to assess if they should be incorporated in to our exercises. If our code is running inside a debugger, VM or sandbox it should deviate from it’s original path and do something legitimate or terminate immediately. If it isn’t running in any of these environments it should run it’s malicious code and infect the system.

Pafish is written in C and can be built with MinGW (gcc + make) as it says on its official GitHub web site.

To build pafish you will basically need to install mingw-w64 and make. After unziping the Pafish source code, we can see the project source has different source code files, each one used for different detection purposes.

    • Detect a debugger: debuggers.c
    • Detect a sandbox: gensandbox.c
    • Detect hooked functions: hooks.c
    • Detect VirtualBox: vbox.c
    • Detect VMWare: vmware.c
    • Detect Qemu: qemu.c
    • Detect Bochs: bochs.c
    • Detect Cuckoo: cuckoo.c
    • Detect Sandboxie: sandboxie.c
    • Detect Wine: wine.c

In the next sections I’ll take a quick look at each one of these files. As you can see some sandboxes are missing, like FireEye, AMP Threat Grid from Cisco, Maltracker from AnubisNetworks, among others. By looking at these techniques we might find insights on how to bypass them if we find one in use at our clients during our engagements.

debuggers.c

By opening debuggers.c, we can see the first method implemented by Pafish. The IsDebuggerPresent() function is a Win32 API function that can be used to determine whether the calling process is being debugged by a debugger.

Still on debuggers.c we can see another function called debug_outputdebugstring(). This uses another function from the Win32 API, OutputDebugString(). According to MSDN, this function “sends a string to the debugger for display”. This is exactly what this code does. If the application is not running under a debugger the string will be sent to system’s debugger to be displayed with the DbgPrint() function from the Windows Driver Kit (WDK). If the application is not running inside a debugger and there is no system debugger then the OutputDebugString() does nothing. If the function doesn’t return an error the process is not being debugged. Otherwise it concludes it is running inside a debugger.

gensandbox.c

Another interesting file for us is gensandbox.c. This file contains 12 functions that it uses to detect a sandbox. The first one, gensandbox_mouse_act() uses GetCursorPos() to determine the position of the mouse cursor and whether it is actively being used. According to MSDN this function “retrieves the position of the mouse cursor, in screen coordinates”. Now if you look at the function code that does the actual detection, you can see that the function first calls the GetCursorPos() function in order to receive cursor co-ordinates and saves them into the position1 variable. After that it sleeps for 2000 milliseconds (i.e. 2 seconds), and then calls the same function again, this time saving the coordinates into the position2 variable. Afterwards, the two samples of the x and y coordinates of the mouse cursor are compared to one other. This determines whether the mouse cursor has changed between the two GetCursorPos() function calls. If the position of the mouse cursor has not changed then there was no mouse activity during the sleep function. Under such circumstances, the code will conclude that it is being run within a sandbox.

Another interesting function in this file is gensandbox_username(). It uses the GetUserName function that retrieves the name of the user associated with the current thread. After that all the lower case letters are converted to upper case letters and the name is compared with the following strings:

    • SANDBOX
    • VIRUS
    • MALWARE

The strstr() function is used to detect any occurrence of the presented strings in the username. If one of the strings above is found, it means that the program is being run inside a sandbox. Otherwise it returns FALSE. This method is highly questionable but gives us some insights on how one might either bypass it (if one was part of the “red team”) or create a better sandbox (if playing for the “blue team”).

The next function, called gensandbox_path(), is using GetModuleFileName(). According to MSDN this function “retrieves the fully qualified path for the file that contains the specified module. The module must have been loaded by the current process”.

Here, the strstr() function is used to check whether the retrieved path contains any of the strings:

    • \\SAMPLE
    • \\VIRUS
    • SANDBOX

If that’s the case it concludes that it is running within a sandbox. Again, we can see multiple ways to improve this code and also get some ideas on how to apply this thinking to detect other environments even though, as you can see, it is a pretty basic technique.

gensandbox_common_names(), takes a similar approach but looks for the following strings instead:

    • sample.exe
    • malware.exe

Another interesting function is gensandbox_drive_size(). This checks if the first physical drive is larger than 60GB by using the CreateFile() and DeviceIoControl() functions. As we can read on MSDN, the CreateFile() function “creates or opens a file or I/O device. The most commonly used I/O devices are as follows: file, file stream, directory, physical disk, volume, console buffer, tape drive, communications resource, mailslot, and pipe. The function returns a handle that can be used to access the file or device for various types of I/O depending on the file or device and the flags and attributes specified”. Using a handle retrieved using the CreateFile() function call, the DeviceIoControl() function is used to send a control code directly to a specified device driver, causing the corresponding device to perform the IOCTL_DISK_GET_LENGTH_INFO operation.

Once again, this is tricky but it is almost always true these days. People don’t like to allocate that much disk space for their testing VMs.

Pafish also has a second function that plays with the size of the C drive, gensandbox_drive_size2(). It basically checks for the amount of free space on drive C. Again, this can be tricky, I’ve seen production servers (mostly databases) running out of space due to a lack of good sysadmin practices and bad planning. And yes… this happens a lot.

Another interesting function is gensandbox_uptime(). It uses GetTickCount() function to retrieve the number of milliseconds that have elapsed since the system was started (up to 49.7 days).

Once again, the assumption done here might be wrong if we think about laptops. Remember, the whole purpose of this analysis is to apply this code to our “red team” exercies, where desktop users are usually the target.

There are some more small functions inside gensandbox.c that I’d recommend you to have a look. These can also give some good insights to detect other sandboxes.

    • gensandbox_sleep_patched()
    • gensandbox_one_cpu()
    • gensandbox_one_cpu_GetSystemInfo()
    • gensandbox_less_than_onegb()
    • gensandbox_IsNativeVhdBoot()

hooks.c

The hooks.c source code only contains four small functions. Their aim is detect if any of the following functions have been hooked:

    • DeleteFileW
    • ShellExecuteExW
    • CreateProcessA

Here’s the whole code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int check_hook_m1(DWORD * dwAddress) {
  BYTE *b = (BYTE *)dwAddress;
  return (*b == 0x8b) && (*(b+1) == 0xff) ? FALSE : TRUE;
}

/* Causes FP in Win 8 */
int check_hook_DeleteFileW_m1() {
  return check_hook_m1((DWORD *)DeleteFileW);
}

int check_hook_ShellExecuteExW_m1() {
  return check_hook_m1((DWORD *)ShellExecuteExW);
}

int check_hook_CreateProcessA_m1() {
  return check_hook_m1((DWORD *)CreateProcessA);
}

Basically each one of the functions store the the address of the funtion (either DeleteFileW, ShellExecuteExW or CreateProcessA) into the dwAddress variable of the check_hook_m1() function.

Then it checks whether the first two bytes of the function are 0xff8b, which represent the assembly instruction for jump back instruction. Usually the functions create a new stack frame upon being called, but the jmp instruction at the beginning of a function clearly indicates the function has been hooked.

vbox.c/vmware.c/qemu.c

The code to detect VirtualBox is quite extensive and there are multiple function that look for Windows Registry keys. Here are those functions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/**
* SCSI registry key check
**/
int vbox_reg_key1() {
  return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\DEVICEMAP\\Scsi\\Scsi Port 0\\Scsi Bus 0\\Target Id 0\\Logical Unit Id 0", "Identifier", "VBOX");
}

/**
* SystemBiosVersion registry key check
**/
int vbox_reg_key2() {
  return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\Description\\System", "SystemBiosVersion", "VBOX");
}

/**
* VirtualBox Guest Additions key check
**/
int vbox_reg_key3() {
  return pafish_exists_regkey(HKEY_LOCAL_MACHINE, "SOFTWARE\\Oracle\\VirtualBox Guest Additions");
}

/**
* VideoBiosVersion key check
**/
int vbox_reg_key4() {
  return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\Description\\System", "VideoBiosVersion", "VIRTUALBOX");
}

/**
* ACPI Regkey detection
**/
int vbox_reg_key5() {
  return pafish_exists_regkey(HKEY_LOCAL_MACHINE, "HARDWARE\\ACPI\\DSDT\\VBOX__");
}

/**
* FADT ACPI Regkey detection
**/
int vbox_reg_key7() {
  return pafish_exists_regkey(HKEY_LOCAL_MACHINE, "HARDWARE\\ACPI\\FADT\\VBOX__");
}

/**
* RSDT ACPI Regkey detection
**/
int vbox_reg_key8() {
  return pafish_exists_regkey(HKEY_LOCAL_MACHINE, "HARDWARE\\ACPI\\RSDT\\VBOX__");
}

/**
* VirtualBox Services Regkey detection
**/
int vbox_reg_key9(int writelogs) {
  int res = FALSE, i;
  const int count = 5;
  char message[200];

  string strs[count];
  strs[0] = "SYSTEM\\ControlSet001\\Services\\VBoxGuest";
  strs[1] = "SYSTEM\\ControlSet001\\Services\\VBoxMouse";
  strs[2] = "SYSTEM\\ControlSet001\\Services\\VBoxService";
  strs[3] = "SYSTEM\\ControlSet001\\Services\\VBoxSF";
  strs[4] = "SYSTEM\\ControlSet001\\Services\\VBoxVideo";
  for (i=0; i < count; i++) {
      if (pafish_exists_regkey(HKEY_LOCAL_MACHINE, strs[i])) {
          snprintf(message, sizeof(message)-sizeof(message[0]), "VirtualBox traced using Reg key HKLM\\%s", strs[i]);
          if (writelogs) write_log(message);
          res = TRUE;
      }
  }
  return res;
}

/**
* HARDWARE\\DESCRIPTION\\System SystemBiosDate == 06/23/99
**/
int vbox_reg_key10() {
  return pafish_exists_regkey_value_str(HKEY_LOCAL_MACHINE, "HARDWARE\\DESCRIPTION\\System", "SystemBiosDate", "06/23/99");
}

The names and the code is prety self explanatory. The code is mostly based on the functions pafish_exists_regkey_value_str() and pafish_exists_regkey() and it can be foud on utils.c. Basically, it uses RegOpenKeyEx. If the function finds the specified registry entry it will return ERROR_SUCCESS. Otherwise a value of nonzero will be returned. As stated on MSDN, the RegOpenKeyEx() function “opens the specified registry key. Note that key names are not case sensitive”.

In the VirtualBox code there’s also some other interesting tricks. Like the function vbox_sysfile1(). This function basically looks for the presence of VirtualBox drivers installed on the system. See the code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int vbox_sysfile1(int writelogs) {
    const int count = 4;
    string strs1ount];
    int res = FALSE, i = 0;
    char message[200];

    strs[0] = "C:\\WINDOWS\\system32\\drivers\\VBoxMouse.sys";
    strs[1] = "C:\\WINDOWS\\system32\\drivers\\VBoxGuest.sys";
    strs[2] = "C:\\WINDOWS\\system32\\drivers\\VBoxSF.sys";
    strs[3] = "C:\\WINDOWS\\system32\\drivers\\VBoxVideo.sys";
    for (i=0; i < count; i++) {
        if (pafish_exists_file(strs[i])) {
            snprintf(message, sizeof(message)-sizeof(message[0]), "VirtualBox traced using driver file %s", strs[i]);
            if (writelogs) write_log(message);
            res = TRUE;
        }
    }
    return res;
}

On the same line of thinking you can find the function vbox_sysfile2(). Which basically looks for the presence of specific VirtualBox DLLs. Here is the actual code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int vbox_sysfile2(int writelogs) {
    const int count = 14;
    string strs1ount];
    int res = FALSE, i = 0;
    char message[200];

    strs[0] = "C:\\WINDOWS\\system32\\vboxdisp.dll";
    strs[1] = "C:\\WINDOWS\\system32\\vboxhook.dll";
    strs[2] = "C:\\WINDOWS\\system32\\vboxmrxnp.dll";
    strs[3] = "C:\\WINDOWS\\system32\\vboxogl.dll";
    strs[4] = "C:\\WINDOWS\\system32\\vboxoglarrayspu.dll";
    strs[5] = "C:\\WINDOWS\\system32\\vboxoglcrutil.dll";
    strs[6] = "C:\\WINDOWS\\system32\\vboxoglerrorspu.dll";
    strs[7] = "C:\\WINDOWS\\system32\\vboxoglfeedbackspu.dll";
    strs[8] = "C:\\WINDOWS\\system32\\vboxoglpackspu.dll";
    strs[9] = "C:\\WINDOWS\\system32\\vboxoglpassthroughspu.dll";
    strs[10] = "C:\\WINDOWS\\system32\\vboxservice.exe";
    strs[11] = "C:\\WINDOWS\\system32\\vboxtray.exe";
    strs[12] = "C:\\WINDOWS\\system32\\VBoxControl.exe";
    strs[13] = "C:\\program files\\oracle\\virtualbox guest additions\\";
    for (i = 0; i < count; i++) {
        if (pafish_exists_file(strs[i])) {
            snprintf(message, sizeof(message)-sizeof(message[0]), "VirtualBox traced using system file %s", strs[i]);
            if (writelogs) write_log(message);
            res = TRUE;
        }
    }
    return res;
}

One of the tricks mostly common used is… yes, you guessed it. Check the MAC address identifier. Here’s the code that check’s for the OUI vendor:

1
2
3
4
int vbox_mac() {
    /* VirtualBox mac starts with 08:00:27 */
    return pafish_check_mac_vendor("\x08\x00\x27");
}

The code for pafish_check_mac_vendor() can be found on the utils.c source file. Here’s the actual code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int pafish_check_mac_vendor(char * mac_vendor) {
    unsigned long alist_size = 0, ret;

    ret = GetAdaptersAddresses(AF_UNSPEC,0,0,0,&alist_size);
    if(ret==ERROR_BUFFER_OVERFLOW) {
        IP_ADAPTER_ADDRESSES* palist = (IP_ADAPTER_ADDRESSES*)LocalAlloc(LMEM_ZEROINIT,alist_size);
        if(palist) {
            GetAdaptersAddresses(AF_UNSPEC,0,0,palist,&alist_size);
            char mac[6]={0};
            while (palist){
                if (palist->PhysicalAddressLength==0x6){
                    memcpy(mac,palist->PhysicalAddress,0x6);
                    if (!memcmp(mac_vendor, mac, 3)) { /* First 3 bytes are the same */
                        LocalFree(palist);
                        return TRUE;
                    }
                }
                palist = palist->Next;
            }
            LocalFree(palist);
        }
    }
    return FALSE;
}

There are a few more tricks on the vbox.c file that shouldn’t be ignored, but I’ll ignore them for now.

As you can imagine most of the code used to fingerprint VirtualBox is used in almost identical fashion to fingerprint VMware and Qemu.Basically, the code looks for specific Windows Registry keys, specific file paths, drivers and MAC vendor.

bochs.c

One neat idea we spotted in terms of how Pafish fingerprints Bochs was to play with CPU specific featurs that are present in Bochs but not real CPU.

For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int bochs_cpu_amd1() {
    char cpu_brand[49];
    cpu_write_brand(cpu_brand);
    /* It checks the lowercase P in 'processor', an actual AMD returns Processor */
    return !memcmp(cpu_brand, "AMD Athlon(tm) processor", 24) ? TRUE : FALSE;
}

int bochs_cpu_amd2() {
    int eax;
    __asm__ volatile(".intel_syntax noprefix;"
            "xor eax, eax;"
            "cpuid;"
            "cmp ecx, 0x444d4163;" /* AMD CPU? */
            "jne b2not_detected;"
            "mov eax, 0x8fffffff;" /* query easter egg */
            "cpuid;"
            "jecxz b2detected;" /* ECX value not filled */
            "b2not_detected: xor eax, eax; jmp b2exit;"
            "b2detected: mov eax, 0x1;"
            "b2exit: nop;"
            ".att_syntax;"
            : "=a"(eax));
    return eax ? TRUE : FALSE;
}

int bochs_cpu_intel1() {
    char cpu_brand[49];
    cpu_write_brand(cpu_brand);
    /* This processor name is not known to be valid in an actual CPU */
    return !memcmp(cpu_brand, "              Intel(R) Pentium(R) 4 CPU        ", 47) ? TRUE : FALSE;
}

The first and third functions bochs_cpu_amd1() and bochs_cpu_intel1() will check for typos in the processor CPU string, whilst the second, bochs_cpu_amd2() triggers an assembly level easter egg that is present in the Bochs x86 CPU emulation.

cuckoo.c

Cuckoo is an open source project and the hooks it implements are known. The code you can find on cuckoo.c is quite small and basically plays with Cuckoo TLS_HOOK_INFO. As a side note don’t forget that most Cuckoo set-ups use VirtualBox.

sandboxie.c

The trick to detect Sandboxie is quite simple.

1
2
3
4
5
6
7
8
int sboxie_detect_sbiedll() {
  if (GetModuleHandle("sbiedll.dll") != NULL) {
      return TRUE;
  }
  else {
      return FALSE;
  }
}

As you can see the function aboves tries to load the Sandboxie specific DLL called sbiedll.dll. If it succeeds Sandboxie is installed in the systems. Otherwise it is not. Pretty small test but quite effective.

wine.c

The code to detect the Wine environment is also quite small. The first function is wine_detect_get_unix_file_name().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int wine_detect_get_unix_file_name() {
  HMODULE k32;
  k32 = GetModuleHandle("kernel32.dll");
  if (k32 != NULL) {
      if (GetProcAddress(k32, "wine_get_unix_file_name") != NULL) {
          return TRUE;
      }
      else {
          return FALSE;
      }
  }
  else {
      return FALSE;
  }
}

It starts by getting an handle to the kernel32.dll and then calling the GetProcAddress to retrieve the address of the function/variable wine_get_unix_file_name exported and available in the kernel32.dll. If the function succeeds it will return the address of the exported function, otherwise it will return NULL. Meaning that if doesn’t return NULL Wine has been fingerprinted and the wine_detect_get_unix_file_name returns TRUE.

The other function that Pafish implements is shown bellow.

1
2
3
int wine_reg_key1() {
  return pafish_exists_regkey(HKEY_CURRENT_USER, "SOFTWARE\\Wine");
}

Nothing new here, wine_reg_key1 simply looks for the presence of a Windows Registry key.

Conclusion

This short introduction to Pafish code was meant to evaluate how the code can be applied to our Red Team Exercises. Since the code/checks are not too advanced and in some cases can be completely fooled, the code should be tweaked if we want to use it in our engagements. It also doesn’t make sense to include all the checks blindly, which means a good information gathering phase must be assured before any “red team” exercise.

Anyway these different methods of checking whether the program is running under a debugger, Virtual Machine or a sandbox might be quite useful if we want to develop code for a specific environment. Looking at Pafish code certainly improves our knowledge about how to bypass some sandboxes.

Based on the code I’ve read, the most common ways to identify that we are running in a virtualized environment (running inside a debugger is not that useful to us) are:

    • Registry Checks
    • Memory Checks
    • Communication Checks (with the host)
    • Processes and Files Checks
    • Hardware