TL;DR

In this blog post, we see how to retrieve a loaded module handle by parsing the PEB. Then, we use this handle to resolve function address by parsing the EAT.

We do this to avoid using GetModuleHandle and GetProcAddress functions which are often hooked by EDRs. This also prevents having functions considered as malicious to be present in our Import Table. We then take a look at the “API hashing” technique and its advantages. Finally, we fork mkwinsyscall to include our custom resolving Go module and create a drop-in replacement.

Resolve module handle

On Windows, as Microsoft documentation states, a module is an executable file or DLL and each process consists of one or more modules. We can use Process Hacker 2 to see the list of modules loaded by a process.

Modules loaded by Notepad.exe, viewed in Process Hacker 2

Modules loaded by Notepad.exe, viewed in Process Hacker 2

The “Base address” column indicates the address where the module has been loaded in memory by the system. This address points to the beginning of the module. It is the handle to the module. It is possible to get the module handle by using the Windows API and more specifically the GetModuleHandle function.

HMODULE GetModuleHandleA(
  [in, optional] LPCSTR lpModuleName
);

This function takes a string as a parameter and returns the handle to the specified module. lpModuleName parameter is marked as optional, which means it can be NULL (equivalent to 0), in this case the function returns a handle to the calling process module (the executable used to create the process).

ℹ Like for many Windows API functions, there is an ANSI and a WIDE (UTF-16) version differentiated by the letter A or W which suffixes the name.

We can avoid using this function by manually parsing the Process Environment Block (PEB) structure. This structure contains process information and a pointer to it is present in the Thread Environment Block (TEB) structure. According to the documentation, it describes the state of a thread. On Windows x64, the segment GS stores a pointer to this structure.

typedef struct _TEB {
    PVOID Reserved1[12];
    PPEB  ProcessEnvironmentBlock;
    PVOID Reserved2[399];
    BYTE  Reserved3[1952];
    PVOID TlsSlots[64];
    BYTE  Reserved4[8];
    PVOID Reserved5[26];
    PVOID ReservedForOle;
    PVOID Reserved6[4];
    PVOID TlsExpansionSlots;
} TEB, *PTEB;

It is possible to retrieve a pointer to the PEB at GS with an offset of 0x60 using Go assembly.

ℹ️ The 0x60 offset is calculated by multiplying the size of a pointer (0x8 on x64) by the number of elements (12) in the Reserved1 field.

TEXT ·GetPEBptr(SB), $0-8
    MOVQ 	0x60(GS), AX
    MOVQ	AX, ret+0(FP)
    RET

Most fields in the PEB are marked as Reserved, but it has been reversed so we know what they actually mean. You can find the whole structure in the awesome Vergilius Project.

Some fields are interesting from a malware development perspective :

  • BeingDebugged : byte indicating if the process is being debugged
  • ProcessParameters : pointer to a structure that contains among other fields the command line
  • Ldr : pointer to the structure PEB_LDR_DATA describing the loaded modules

According to the official documentation, the only “exposed” field of the Ldr structure is InMemoryOrderModuleList, a doubly-linked list where each item is a pointer to a LDR_DATA_TABLE_ENTRY structure. However, there is another unofficially documented field, InLoadOrderModuleList, which is similar to InMemoryOrderModuleList but without an offset.

Illustration of PEB structure walking

Illustration of PEB structure walking

By looping over the InLoadOrderModuleList doubly-linked list, we can retrieve each entry containing the DllBase of the module, in other words its handle. Below is the implementation of the previous explanation, looping over each entry and comparing the BaseDllName with the module we are looking for.

func NewDLL(module string) *windows.DLL {

    dll := new(windows.DLL)

    peb := GetPEB()
    head := peb.Ldr.InLoadOrderModuleList

    // current module = first entry
    if module == "" {
        entry := (*LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(head.Flink))
        dll.Handle = windows.Handle(entry.DllBase)
        dll.Name = strings.ToLower(entry.BaseDllName.String())
        return dll
    }

    // search for module
    module = strings.ToLower(module)
    for next := head.Flink; *next != head; next = next.Flink {
        entry := (*LDR_DATA_TABLE_ENTRY)(unsafe.Pointer(next))
        name := strings.ToLower(entry.BaseDllName.String())
        if module == name {
            dll.Handle = windows.Handle(entry.DllBase)
            dll.Name = name
            return dll
        }
    }

    return nil

}

Resolve proc address

Now that we have a handle to the desired module, we can resolve its exported function (or procedure). Usually, we would use the GetProcAddress. This function takes the module handle as the first parameter and the function name or ordinal as the second parameter. It returns the address of the desired function. But we can resolve it manually by parsing the PE structure. This has the benefit of not including these functions in our PE import table.

FARPROC GetProcAddress(
  [in] HMODULE hModule,
  [in] LPCSTR  lpProcName
);

As a reminder, the image below is, in my opinion, the best overview of the PE format.

PE format overview (source: https://onlyf8.com/pe-format)

PE format overview (source: https://onlyf8.com/pe-format)

Data directories are an array of pointers to structures containing information about the PE. Among theses directories, some interesting we can mention are :

  • Export directory (IMAGE_DIRECTORY_ENTRY_EXPORT)
  • Import directory ( IMAGE_DIRECTORY_ENTRY_IMPORT)
  • Exception directory (IMAGE_DIRECTORY_ENTRY_EXCEPTION)

We will be focusing on the directory entry containing exported functions. This entry points to an IMAGE_EXPORT_DIRECTORY structure that can be viewed with PE-bear. The AddressOfFunctions, AddressOfNames and AddressOfNameOrdinals fields are relative virtual addresses (RVA) to tables (or arrays) corresponding respectively to the Export Address Table, the Export Names Table and the Export Ordinal Table.

Illustration of IMAGE_EXPORT_DIRECTORY structure with different tables

Illustration of IMAGE_EXPORT_DIRECTORY structure with different tables

Export Directory, viewed in PE-bear

Export Directory, viewed in PE-bear

We can get their virtual address (VA) by adding the RVA to the module base.

addrOfFunctions := unsafe.Add(module, exportDir.AddressOfFunctions)
addrOfNames := unsafe.Add(module, exportDir.AddressOfNames)
addrOfNameOrdinals := unsafe.Add(module, exportDir.AddressOfNameOrdinals)

These tables contain the RVA that must be added to the module base address to obtain the final value (function address, function name or function ordinal).

We can resolve function address by looping over those tables and comparing the current entry name with the desired function name.

for i := uintptr(0); i < uintptr(exportDir.NumberOfNames); i++ {
    currentName := windows.BytePtrToString((*byte)(unsafe.Add(module, *(*uint32)(unsafe.Add(addrOfNames, i*sizeOfUint32)))))
    if currentName == procName {
        index := *(*uint16)(unsafe.Add(addrOfNameOrdinals, i*sizeOfUint16))
        procAddr = uintptr(module) + uintptr(*(*uint32)(unsafe.Add(addrOfFunctions, index*uint16(sizeOfUint32))))
        goto Found
    }
}

ℹ️ AddressOfNames is a DWORD array, or uint32 in Go. To access an element in this array (as in C AddressOfNames[i]), you need to add i x the size of a uint32 to the address. The same goes for AddressOfFunctions. However, AddressOfNameOrdinals is a WORD array, or uint16 in Go.

If the Export Names Table is alphabetically ordered, we might consider a more efficient way of searching like the binary search.

left := uintptr(0)
right := uintptr(exportDir.NumberOfNames - 1)

for left != right {
    middle := left + ((right - left) >> 1)
    currentName := windows.BytePtrToString((*byte)(unsafe.Add(module, *(*uint32)(unsafe.Add(addrOfNames, middle*sizeOfUint32)))))
    if currentName == procName {
        index = *(*uint16)(unsafe.Add(addrOfNameOrdinals, middle*sizeOfUint16))
        procAddr = uintptr(module) + uintptr(*(*uint32)(unsafe.Add(addrOfFunctions, index*uint16(sizeOfUint32))))
        goto Found
    } else if currentName < procName {
        left = middle + 1
    } else {
        right = middle - 1
    }
}

Another option is to resolve it by ordinal (identifier for an exported function). This approach doesn’t require to loop, we can directly get the right index in the Export Address Table (EAT) by substracting the Base (first ordinal) to the ordinal of the function we are looking for.

if procOrdinal != 0 {
    procOrdinal = procOrdinal - uint16(exportDir.Base)
    rva := *(*uint32)(unsafe.Add(addrOfFunctions, procOrdinal*uint16(sizeOfUint32)))
    procAddr = uintptr(module) + uintptr(rva)
    goto Found
}

API hashing

Resolving handles and function addresses manually to avoid including some functions in the PE import table is a great starting point. However, we still “leak” function names that we are using in our malware. We can use a the strings.exe utility to see them.

Resolving function addresses using strings

Resolving function addresses using strings

To get rid of theses strings that could potentially reveal intentions of our malware, we can apply a technique called “API hashing”. Instead of resolving module handle and function address by name, we can hash the strings before compiling the malware. We can use a simple hashing algorithm that produce short 32-bit hashes like sdbm. The hashing algorithm doesn’t need to be cryptographically secure, we just need to ensure that there won’t be collisions.

func hash(str string) uint32 {
    var hash uint32 = 0
    for i := 0; i < len(str); i++ {
        hash = uint32(str[i]) + (hash << 6) + (hash << 16) - hash
    }
    return hash
}

Then, while parsing structures, we can hash module names and function names at runtime and compare them with the pre-calculated hashes.

if currentName == moduleName || **hash(currentName) == moduleHash** {
    dll.Handle = windows.Handle(entry.DllBase)
    dll.Name = currentName
    return dll
}
if currentName == procName || **hash(currentName) == procHash** {
    index := *(*uint16)(unsafe.Add(addrOfNameOrdinals, i*sizeOfUint16))
    procAddr = uintptr(module) + uintptr(*(*uint32)(unsafe.Add(addrOfFunctions, index*uint16(sizeOfUint32))))
    goto Found
}

This way strings won’t be present in the final binary and we hide our malicious intent.

Resolving function addresses using hashes

Resolving function addresses using hashes

ℹ️ In the second screenshot, we can see that WriteProcessMemory is still present in the final binary. This is due to the fact that we import the golang.org/x/sys/windows package which already resolves this function. This import is required because our custom resolving functions return *windows.DLL and *windows.Proc types.

As mentioned, this technique can be used to hide our intents by making used function not appear in the import table. However, even in a program that doesn’t do anything, Go still imports some functions by default that can’t be removed.

Default imports of a Go binary, viewed in PE-bear

Default imports of a Go binary, viewed in PE-bear

Go generate

The standard Windows package provides a code generator called mkwinsyscall. It generate functions based on their prototypes. To do so, we can specify a Windows function prototype in Go’s format, prefixed with a //sys marker.

package winfunctions

//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output winfuncs.go definitions.go

//sys RtlCopyMemory(dest uintptr, src uintptr, dwSize uint32) = ntdll.RtlCopyMemory
//sys HeapAlloc(hHeap windows.Handle, dwFlags uint32, dwBytes uintptr) (lpMem uintptr, err error) = kernel32.HeapAlloc

This example will parse a file named definitions.go and will produce winfuncs.go. This is pretty convenient as we won’t need to handle type conversions and errors.

// Code generated by 'go generate'; DO NOT EDIT.

package winfunctions

[...]

var (
    modntdll    = windows.NewLazySystemDLL("ntdll.dll")
    modkernel32 = windows.NewLazySystemDLL("kernel32.dll")

    procRtlCopyMemory = modntdll.NewProc("RtlCopyMemory")
    procHeapAlloc     = modkernel32.NewProc("HeapAlloc")
)

func RtlCopyMemory(dest uintptr, src uintptr, dwSize uint32) {
    syscall.Syscall(procRtlCopyMemory.Addr(), 3, uintptr(dest), uintptr(src), uintptr(dwSize))
    return
}

func HeapAlloc(hHeap windows.Handle, dwFlags uint32, dwBytes uintptr) (lpMem uintptr, err error) {
    r0, _, e1 := syscall.Syscall(procHeapAlloc.Addr(), 3, uintptr(hHeap), uintptr(dwFlags), uintptr(dwBytes))
    lpMem = uintptr(r0)
    if lpMem == 0 {
        err = errnoErr(e1)
    }
    return
}

However, using the functions from the standard Windows package “leaks” names of the modules and the functions we are resolving. Under the hood, NewLazySystemDLL (or NewLazyDLL) will call LoadLibrary to get a handle on the module and NewProc will call GetProcAddress to find the function address. To address this issue, we fork the mkwinsyscall generator and edit it to use our custom Go module. With a few modifications, we make a drop-in replacement with manual resolution and API hashing.

Few lines edited in mkwinsyscall

Few lines edited in mkwinsyscall

We can replace the go generate command in the previous example and run it again.

//go:generate go run gtihub.com/atsika/mkwinsyscall -output winfuncs.go definitions.go
// Code generated by 'go generate'; EDIT AT YOUR OWN RISK.

package winfunctions

[...]

var (
    modntdll    = pelib.NewDLL(uint32(0xd22e2014)) // ntdll.dll
    modkernel32 = pelib.NewDLL(uint32(0x8f7ee672)) // kernel32.dll
    
    procRtlCopyMemory = pelib.NewProc(modntdll, uint32(0xbe91a4e0))    // RtlCopyMemory
    procHeapAlloc     = pelib.NewProc(modkernel32, uint32(0xa4695109)) // HeapAlloc
)

func RtlCopyMemory(dest uintptr, src uintptr, dwSize uint32) {
    syscall.SyscallN(procRtlCopyMemory.Addr(), uintptr(dest), uintptr(src), uintptr(dwSize))
    return
}

func HeapAlloc(hHeap windows.Handle, dwFlags uint32, dwBytes uintptr) (lpMem uintptr, err error) {
    r0, _, e1 := syscall.SyscallN(procHeapAlloc.Addr(), uintptr(hHeap), uintptr(dwFlags), uintptr(dwBytes))
    lpMem = uintptr(r0)
    if lpMem == 0 {
        err = errnoErr(e1)
    }
    return
}

ℹ️ In case the DLL isn’t already loaded by the process, the manual module handle resolution using API hashing won’t work. You should check if the module is different from nil before using it.

Both PElib, the custom Go module for manual module and function address resolution, and the mkwinsyscall fork are available in my GitHub repo.

References