# 3. Programmatically detect ntdll hooks

A very well known and documented way of bypassing userland hooks is to make use of direct and indirect system calls (blog posts will follow for these techniques). In order to perform either technique we need to identify the SSN(System Service Numbers)  at runtime or hardcode them in our program.

Hardcoding the SSNs has its limitations since they can some times change between Windows updates. So in order to hardcode the SSNs we will need to know exact target system in advance.&#x20;

A better approach is to use [Hell's Gate](https://github.com/am0nsec/HellsGate/blob/master/hells-gate.pdf) (I highly recommend to read this document first). Using this method we will identify the SSNs at runtime without any prior knowledge of the target system.

### Flowchart of the approach:

{% @mermaid/diagram content="flowchart TD
id1(Get NTDLL.dll base address )-->id2(Identify location of IMAGE\_EXPORT\_DIRECTORY)
\--> id3(Extract from the struct:

* Numbers of functions
* Address of functions
* Address of Names)
  \-->
  id4(Loop Through the Exports and Extract:
* function RVA
* function Name RVA
* name
* location of the syscall instruction -tramboline-
* any installed hooks
* SSNs - syscall numbers)" %}

With the above in mind, let's dive into the code

### Get NTDLL.dll base address

Walking / Exploring the [PEB (process environment block)](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb) is a common method used by shellcoders to dynamically find the location of exported functions. A similar approach will be used to identify the base address of ntdll and subsequently the export functions.&#x20;

To get a pointer to the PEB the following undocumented function was used.

#### [RtlGetCurrentPeb (ntdll)](http://pinvoke.net/default.aspx/ntdll/RtlGetCurrentPeb.html)

```c
[DllImport("ntdll.dll", SetLastError : true)]
def RtlGetCurrentPeb() as IntPtr:
     pass
```

What it does it essentially returns a pointer to the beginning of the PEB. Thankfully for us this function is implemented in the windows package.

```go
peb := windows.RtlGetCurrentPeb()
```

PEB Structure&#x20;

{% code lineNumbers="true" %}

```go
type PEB struct {
	reserved1              [2]byte
	BeingDebugged          byte
	BitField               byte
	reserved3              uintptr
	ImageBaseAddress       uintptr
	Ldr                    *PEB_LDR_DATA
	ProcessParameters      *RTL_USER_PROCESS_PARAMETERS
	reserved4              [3]uintptr
	AtlThunkSListPtr       uintptr
	reserved5              uintptr
	reserved6              uint32
	reserved7              uintptr
	reserved8              uint32
	AtlThunkSListPtr32     uint32
	reserved9              [45]uintptr
	reserved10             [96]byte
	PostProcessInitRoutine uintptr
	reserved11             [128]byte
	reserved12             [1]uintptr
	SessionId              uint32
}
```

{% endcode %}

To view the PEB Structure in windbg we can use the following command:

```c
dt nt!_PEB @$Peb
```

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FJjGNS5LGkXBMBqwhhjZS%2Fimage.png?alt=media&#x26;token=121f4131-8a8e-4dfe-a9e6-4f2ffa4fd71a" alt=""><figcaption><p>PEB Struct in windbg</p></figcaption></figure>

We are particularly interested in the [PEB\_LDR\_DATA](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data)

```go
type PEB_LDR_DATA struct {
	reserved1               [8]byte
	reserved2               [3]uintptr
	InMemoryOrderModuleList LIST_ENTRY
}
```

To view this struct we simply click on Ldr in windbg that generates the following command:

```c
x -r1 ((ntdll!_PEB_LDR_DATA *)0x7ffb0e1143c0)
```

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FZUiaPgXEmSicBiNwtleB%2Fimage.png?alt=media&#x26;token=9a253842-8a2a-46b0-89d5-c884cf0013cd" alt=""><figcaption><p>PEB_LDR_DATA in windbg</p></figcaption></figure>

InMemoryOrderModuleList: The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR\_DATA\_TABLE\_ENTRY structure.&#x20;

Once again to view InMemoryOrderMOduleList we just click on it in windbg. The command is generated for us:

```c
 dx -r1 (*((ntdll!_LIST_ENTRY *)0x7ffb0e1143e0))
```

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FOYzQwEuhv1NaX6BtdDAm%2Fimage.png?alt=media&#x26;token=358cff8d-b68a-48db-bc7d-61c20d260647" alt=""><figcaption><p>InMemoryOrderModuleList in windbg</p></figcaption></figure>

The [LDR\_DATA\_TABLE\_ENTRY](https://learn.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data#remarks) structure is defined as follows:

```c
typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;
    PVOID EntryPoint;
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;
```

Finally to get tge LDR\_DATA\_TABLE\_ENTRY that holds the name and dllbase we run the following command in windbg

```
dt _LDR_DATA_TABLE_ENTRY (0x1ddbe804af0 -0x10)
```

where 0x1ddbe804af0 is the Flink address from the previous step. \_LDR\_DATA\_TABLE\_ENTRY is at an offset of -0x10

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FR1zjB3f8BwuREmzcExOR%2Fimage.png?alt=media&#x26;token=8caee2fd-ea25-4aef-86b1-6f9ed537780a" alt=""><figcaption><p>LDR_DATA_TABLE_ENTRY in windbg</p></figcaption></figure>

On the screenshot we can see both the module name and base address. To now print the dll address and dll name we run the following commands based on the offsets shown in windbg

```
0:019> dq 0x1ddbe804af0 -0x10 +0x30 L1
000001dd`be804b10  00007ff6`851d0000

0:019> db poi(0x1ddbe804af0 -0x10 +0x58 + 0x8)
000001dd`be8046de  4e 00 6f 00 74 00 65 00-70 00 61 00 64 00 2e 00  N.o.t.e.p.a.d...
000001dd`be8046ee  65 00 78 00 65 00 00 00-22 00 43 00 3a 00 5c 00  e.x.e...".C.:.\.

```

Now we can find the dll base address and dll name we can loop through using the forward links until we reach an empty dll name.

Here is the code in go:

```go
// adds all loaded modules and their base addresses in a slice
func ListDllFromPEB() []dllstruct {

	peb := windows.RtlGetCurrentPeb()
	moduleList := peb.Ldr.InMemoryOrderModuleList
	a := moduleList.Flink
	loadedModules := []dllstruct{}
	for {

		listentry := uintptr(unsafe.Pointer(a))
		// -0x10 beginning of the _LDR_DATA_TABLE_ENTRY_ structure
		// +0x30 Dllbase address
		// +0x58 +0x8 address holding the address pointing to base dllname
		// offsets different for 32-bit processes
		DllBase := uintptr(listentry) - 0x10 + 0x30
		BaseDllName := uintptr(listentry) - 0x10 + 0x58 + 0x8

		v := *((*uintptr)(unsafe.Pointer(BaseDllName)))
		//fmt.Printf("%p\n", (unsafe.Pointer(v))) // prints the address that holds the dll name

		s := ((*uint16)(unsafe.Pointer(v))) // turn uintptr to *uint16
		dllNameStr := windows.UTF16PtrToString(s)
		if dllNameStr == "" {
			break
		}

		dllbaseaddr := *((*uintptr)(unsafe.Pointer(DllBase)))
		//fmt.Printf("%p\n", (unsafe.Pointer(dllbaseaddr))) // prints the dll base addr
		loadedModules = append(loadedModules, dllstruct{
			name:                   dllNameStr,
			address:                dllbaseaddr,
			exportDirectoryAddress: 0,
			exportDirectory:        IMAGE_EXPORT_DIRECTORY{Characteristics: 0, TimeDateStamp: 0, MajorVersion: 0, MinorVersion: 0, Name: 0, Base: 0, NumberOfFunctions: 0, NumberOfNames: 0, AddressOfFunctions: 0, AddressOfNames: 0, AddressOfNameOrdinals: 0},
			exportedNtFunctions:    []Exportfunc{},
			exportedZwFunctions:    []Exportfunc{},
		})
		a = a.Flink
	}

	return loadedModules
}
```

We essentially save the name and address of each module into a dllstruct structure and return them all in a slice called loadedModules. This function was created with future applications in mind. In this case since we only care about ntdll.dll we could return only that dllstruct.&#x20;

The following function was created to return the dllstruct needed. It receives the dll name as an argument and returns the struct.&#x20;

```go
func GetStructOfLoadedDll(name string) (dllstruct, error) {
	modules := ListDllFromPEB()
	for _, module := range modules {
		if module.name == name {
			return module, nil
		}

	}
	return dllstruct{}, fmt.Errorf("dll not Found")
}
```

For debugging purposes we can print the table to console just to ensure that the code generates the expected output.&#x20;

```go
func PrintModules() {
	t := table.NewWriter()
	fmt.Printf("---------------------------------------------\nLoaded modules in current process\n")
	t.AppendHeader(table.Row{"#", "DLL Name", "Address"})

	for i, module := range ListDllFromPEB() {
		t.AppendRow(table.Row{i, module.name, fmt.Sprintf("0x%x", module.address)})
	}
	fmt.Println(t.Render())
}
```

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2Fc0M4kGUNUMvQKNQDNjJ9%2Fimage.png?alt=media&#x26;token=72b19db8-995e-47cd-86eb-c5114e69d6d0" alt=""><figcaption><p>Loaded Module names and addresses</p></figcaption></figure>

We can now cross check using windbg using the "`lm"` command or using ProcessHacker

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FFr88ECEJCR3QyiPn1AYO%2Fimage.png?alt=media&#x26;token=73ecad48-ab5e-40cf-bf4e-2e01c8bce481" alt=""><figcaption><p>Loaded Modules in Process Hacker</p></figcaption></figure>

We can see that the addresses and names match.&#x20;

{% hint style="info" %}
All the above could be replaced by the following code ! :smile: What if the LoadDLL function is hooked though ?

```go
	t, err := windows.LoadDLL(`C:\Windows\System32\ntdll.dll`)
	if err != nil {
		return err
	}
	h := t.Handle
	dllBase := uintptr(h)
```

{% endhint %}

### Identify location of IMAGE\_EXPORT\_DIRECTORY

Once the base address of the ntdll.dll is identified the next step is to find the location of the image\_export\_directory. Let's have a look at the contents of the struct.&#x20;

```go
type IMAGE_EXPORT_DIRECTORY struct { //offsets
	Characteristics       uint32 // 0x0
	TimeDateStamp         uint32 // 0x4
	MajorVersion          uint16 // 0x8
	MinorVersion          uint16 // 0xa
	Name                  uint32 // 0xc
	Base                  uint32 // 0x10
	NumberOfFunctions     uint32 // 0x14
	NumberOfNames         uint32 // 0x18
	AddressOfFunctions    uint32 // 0x1c
	AddressOfNames        uint32 // 0x20
	AddressOfNameOrdinals uint32 // 0x24
}
```

We are particularly interested in the AddressOfFunctions and AddressOfNames (or AddressOfNameOrdinals).&#x20;

Similarly to the techniques used in [Process Hollowing](https://www.scriptchildie.com/code-injection-techniques/shellcode-injection/2.-process-hollowing) we can get the export directory from the Optional Header.&#x20;

* [MS-DOS Stub](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#ms-dos-stub-image-only): At location 0x3c, the stub has the file offset to the PE signature.
* From the offset found at 0x3c we then calculate the size of the different components of PE.

  * [Signature (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#signature-image-only): 4-bytes (PE00)&#x20;
  * [COFF File Header (Object and Image)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#coff-file-header-object-and-image):  (2+2+4+4+4+2+2) 20-bytes (0x14)
  * [Optional Header Standard Fields (Image Only)](https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#optional-header-standard-fields-image-only): Offset to Export Directory 0x70

The above translated to the code below:

```go
func (dll *dllstruct) getExportTableAddress() uintptr {
	e_lfanew := *((*uint32)(unsafe.Pointer(dll.address + 0x3c)))
	ntHeader := dll.address + uintptr(e_lfanew)
	fileHeader := ntHeader + 0x4
	// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
	optionalHeader := fileHeader + 0x14 // 0x14 is the size of the image_file_header struct
	exportDir := optionalHeader + 0x70  // offset to export table
	exportDirOffset := *((*uint32)(unsafe.Pointer(exportDir)))
	dll.exportDirectoryAddress = dll.address + uintptr(exportDirOffset)
	return dll.exportDirectoryAddress
}
```

### Extract the values from memory into the struct

&#x20;func (dll \*dllstruct) GetImageExportDirectory() {

```go

	dll.exportDirectory.Characteristics = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress)))
	dll.exportDirectory.TimeDateStamp = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x4)))
	dll.exportDirectory.MajorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0x8)))
	dll.exportDirectory.MinorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0xa)))
	dll.exportDirectory.Name = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0xc)))
	dll.exportDirectory.Base = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x10)))
	dll.exportDirectory.NumberOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x14)))
	dll.exportDirectory.NumberOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x18)))
	dll.exportDirectory.AddressOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x1c)))
	dll.exportDirectory.AddressOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x20)))
	dll.exportDirectory.AddressOfNameOrdinals = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x24)))

}
```

What this code does is to take the pointers to the memory locations and cast them to Golang types before placing them into the struct

### Extract the exported function names, addresses, SSNs, syscall function address and whether there are hooks installed

The next and final step is to get the export function information. A struct was defined with all the required information&#x20;

```go
type Exportfunc struct {
	funcRVA         uint32  // relative address to the base address of the dll
	functionAddress uintptr // absolute address
	name            string  // name of the exported function
	syscallno       uint16  // SSN
	tramboline      uintptr // syscall ;ret; address location
	isHooked        bool    // Is the function hooked?
}
```

A for loop was used loop through the exported functions

#### function RVA and Name

```go
		funcRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfFunctions) + uintptr((i+1)*0x4)))))
		nameRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfNames) + uintptr(i*0x4)))))
		nameAddr := dll.address + uintptr(nameRVA)
		nameRVAbyte := (*[4]byte)(unsafe.Pointer(nameAddr))[:]
		name := windows.BytePtrToString(&nameRVAbyte[0])
```

The above code is used to extract the function relative address and then read then function name  bytes from memory and turn it to string

#### Syscall Tramboline

This value will be useful when we write the code for the indirect system calls.&#x20;

Let's have another look in windbg to identify the byte sequence of a syscall;ret;&#x20;

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FuTgsfxrryazCHEFUL70J%2Fimage.png?alt=media&#x26;token=a81add40-ca53-43fa-8e90-7e6af5b0a62c" alt=""><figcaption><p>syscall;ret</p></figcaption></figure>

From the screenshot above we can see that the byte sequence for a syscall;ret is `\x0f\x05\xc3`. So when we identify the function address we start a loop to identify the location of the syscall return. I am pretty sure that any syscall from the ntdll could be used without raising any flags by the EDR when we implement indirect syscalls but I decided to find the address of the corresponding syscall instruction for the exported function since it was simple enough.

Here is the code:

```go
		absAddress = dll.address + uintptr(funcRVA)
		for j := 0; j < 100; j++ {
			if *(*byte)(unsafe.Pointer(absAddress)) == 0x0f {
				if *(*byte)(unsafe.Pointer(absAddress + 1)) == 0x05 {
					if *(*byte)(unsafe.Pointer(absAddress + 2)) == 0xc3 {
						break
					}
				}
			}
			absAddress += 1
		}
```

#### Storing the values in the Exportfunc slice

[With a few exceptions](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/using-nt-and-zw-versions-of-the-native-system-services-routines), each native system services routine has two slightly different versions that have similar names but different prefixes. For example, calls to NtCreateFile and ZwCreateFile perform similar operations and are, in fact, serviced by the same kernel-mode system routine. For system calls from user mode, the Nt and Zw versions of a routine behave identically. For calls from a kernel-mode driver, the Nt and Zw versions of a routine differ in how they handle the parameter values that the caller passes to the routine.

A kernel-mode driver calls the Zw version of a native system services routine to inform the routine that the parameters come from a trusted, kernel-mode source. In this case, the routine assumes that it can safely use the parameters without first validating them. However, if the parameters might be from either a user-mode source or a kernel-mode source, the driver instead calls the Nt version of the routine, which determines, based on the history of the calling thread, whether the parameters originated in user mode or kernel mode. For more information about how the routine distinguishes user-mode parameters from kernel-mode parameters.

We will split these functions into two slices. It will later allow us to sort these functions and unhook them using the [halo's gate](https://blog.sektor7.net/#!res/2021/halosgate.md).

```go
		if strings.HasPrefix(name, "Nt") && !slices.Contains(exclusions, name) {
			funcExp := Exportfunc{
				funcRVA:         funcRVA,
				functionAddress: dll.address + uintptr(funcRVA),
				name:            name,
				tramboline:      absAddress,
			}
			funcExp.GetSyscallNumbers(dll.address)
			dll.exportedNtFunctions = append(dll.exportedNtFunctions, funcExp)
		}

		if strings.HasPrefix(name, "Zw") {
			funcExp := Exportfunc{
				funcRVA:         funcRVA,
				functionAddress: dll.address + uintptr(funcRVA),
				name:            name,
				tramboline:      absAddress,
			}
			funcExp.GetSyscallNumbers(dll.address)
			dll.exportedZwFunctions = append(dll.exportedZwFunctions, funcExp)
		}
```

We then sort them by the function address&#x20;

```go
	sort.SliceStable(dll.exportedNtFunctions, func(i, j int) bool {
		return (dll.exportedNtFunctions)[i].funcRVA < (dll.exportedNtFunctions)[j].funcRVA
	})
	sort.SliceStable(dll.exportedZwFunctions, func(i, j int) bool {
		return (dll.exportedZwFunctions)[i].funcRVA < (dll.exportedZwFunctions)[j].funcRVA
	})
```

We the write a function to print the exports

```go
func (dll *dllstruct) PrintExports() {
	noPrint := []string{"NtQuerySystemTime", "ZwQuerySystemTime"}

	tNt := table.NewWriter()
	tNt.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Tramboline", "Hooked?"})
	for i, fun := range dll.exportedNtFunctions {
		if slices.Contains(noPrint, fun.name) {
			continue
		}
		tNt.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.tramboline), fun.isHooked})
	}
	tZw := table.NewWriter()
	tZw.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Tramboline", "Hooked?"})
	for i, fun := range dll.exportedZwFunctions {
		if slices.Contains(noPrint, fun.name) {
			continue
		}
		tZw.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.tramboline), fun.isHooked})
	}
	fmt.Println(tNt.Render())
	fmt.Println(tZw.Render())
}
```

### Results & Testing against the EDR

We will first run the code against windows defender that doesn't use API hooks to check if the addresses, names, SSNs and trampoline addresses match the functions&#x20;

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FGLtVYxW3jS5v6uaRYlQl%2Fimage.png?alt=media&#x26;token=04ad114e-6385-4835-a5bc-1da267e086d6" alt=""><figcaption><p>Script Output</p></figcaption></figure>

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2FEmhQYn7EoidXB1cEinil%2Fimage.png?alt=media&#x26;token=326f9aa0-8204-42b8-9386-552347074010" alt=""><figcaption><p>Windbg cross check</p></figcaption></figure>

The values from the script and the script and windbg are a complete match. Great.

Let's run the script against OpenEDR:

To get only the hooked functions we use the following command

```
 .\unhk.exe | findstr true
  - or - 
 go run . | findstr true
```

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2F5bCWSg0Mpz44yDjooOJq%2Fimage.png?alt=media&#x26;token=068f52e1-8360-4f73-9703-6a88edf3b496" alt=""><figcaption><p>OpenEDR hooks</p></figcaption></figure>

A few APIs were found to be hooked. Let's check in the Debugger if that's the case

<figure><img src="https://1525675160-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F1BtmYYGIbNqDCOEq0YOj%2Fuploads%2Fy6dPw1n3qT4rSRWoXbGD%2Fimage.png?alt=media&#x26;token=27974cdc-0d07-4d3f-846c-f37e5a06e8ad" alt=""><figcaption><p>Hooked Functions</p></figcaption></figure>

### Complete Code

```go
package main

import (
	"fmt"
	"log"
	"slices"
	"sort"
	"strings"
	"unsafe"

	"github.com/jedib0t/go-pretty/v6/table"
	"golang.org/x/sys/windows"
)

type IMAGE_EXPORT_DIRECTORY struct { //offsets
	Characteristics       uint32 // 0x0
	TimeDateStamp         uint32 // 0x4
	MajorVersion          uint16 // 0x8
	MinorVersion          uint16 // 0xa
	Name                  uint32 // 0xc
	Base                  uint32 // 0x10
	NumberOfFunctions     uint32 // 0x14
	NumberOfNames         uint32 // 0x18
	AddressOfFunctions    uint32 // 0x1c
	AddressOfNames        uint32 // 0x20
	AddressOfNameOrdinals uint32 // 0x24
}
type Exportfunc struct {
	funcRVA         uint32  // relative address to the base address of the dll
	functionAddress uintptr // absolute address
	name            string  // name of the exported function
	syscallno       uint16  // SSN
	trampoline      uintptr // syscall ;ret; address location
	isHooked        bool    // Is the function hooked?
}

type dllstruct struct {
	name                   string
	address                uintptr
	exportDirectoryAddress uintptr
	exportDirectory        IMAGE_EXPORT_DIRECTORY
	exportedNtFunctions    []Exportfunc
	exportedZwFunctions    []Exportfunc
}

func main() {
	PrintModules()
	dll, err := GetStructOfLoadedDll("ntdll.dll")
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Printf("\n[+] Base Address of dll %s is 0x%x\n\n", dll.name, dll.address)

	fmt.Printf("[+] Export Table Address 0x%x\n\n", dll.getExportTableAddress())
	dll.GetImageExportDirectory()
	dll.getExportTableAddress()
	dll.GetModuleExports()
	dll.PrintExports()
}

func (dll *dllstruct) PrintExports() {
	noPrint := []string{"NtQuerySystemTime", "ZwQuerySystemTime"}

	tNt := table.NewWriter()
	tNt.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"})
	for i, fun := range dll.exportedNtFunctions {
		if slices.Contains(noPrint, fun.name) {
			continue
		}
		tNt.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked})
	}
	tZw := table.NewWriter()
	tZw.AppendHeader(table.Row{"#", "Function Address", "Function Name", "SysCallNo (SSN)", "Trampoline", "Hooked?"})
	for i, fun := range dll.exportedZwFunctions {
		if slices.Contains(noPrint, fun.name) {
			continue
		}
		tZw.AppendRow(table.Row{i, fmt.Sprintf("0x%x", fun.functionAddress), fun.name, fmt.Sprintf("0x%x", fun.syscallno), fmt.Sprintf("0x%x", fun.trampoline), fun.isHooked})
	}
	fmt.Println(tNt.Render())
	fmt.Println(tZw.Render())
}

func (fun *Exportfunc) GetSyscallNumbers(address uintptr) {

	funcbytes := (*[5]byte)(unsafe.Pointer(fun.functionAddress))[:]

	if funcbytes[0] == 0x4c && funcbytes[1] == 0x8b && funcbytes[2] == 0xd1 && funcbytes[3] == 0xb8 { // Check if the function is hooked.
		fun.syscallno = *(*uint16)(unsafe.Pointer(&funcbytes[4])) // Get Syscall Number
		fun.isHooked = false
	} else {
		fun.syscallno = 0xffff // when hooked set the syscall number 0xff
		fun.isHooked = true
	}

	//fmt.Printf("Func RVA: %x , nameRVA: %x , name: %s, syscallno : %x\n", exFunc.funcRVA, exFunc.nameRVA, exFunc.name, exFunc.syscallno)

}

func (dll *dllstruct) GetModuleExports() {

	exclusions := []string{"NtdllDefWindowProc_A", "NtdllDefWindowProc_W", "NtdllDialogWndProc_A", "NtdllDialogWndProc_W", "NtGetTickCount"}

	var absAddress uintptr

	for i := 0; i < int(dll.exportDirectory.NumberOfNames); i++ {
		funcRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfFunctions) + uintptr((i+1)*0x4)))))
		nameRVA := *((*uint32)(unsafe.Pointer(dll.address + (uintptr(dll.exportDirectory.AddressOfNames) + uintptr(i*0x4)))))
		nameAddr := dll.address + uintptr(nameRVA)
		nameRVAbyte := (*[4]byte)(unsafe.Pointer(nameAddr))[:]
		name := windows.BytePtrToString(&nameRVAbyte[0])

		absAddress = dll.address + uintptr(funcRVA)
		for j := 0; j < 100; j++ {
			if *(*byte)(unsafe.Pointer(absAddress)) == 0x0f {
				if *(*byte)(unsafe.Pointer(absAddress + 1)) == 0x05 {
					if *(*byte)(unsafe.Pointer(absAddress + 2)) == 0xc3 {
						break
					}
				}
			}
			absAddress += 1
		}

		if strings.HasPrefix(name, "Nt") && !slices.Contains(exclusions, name) {
			funcExp := Exportfunc{
				funcRVA:         funcRVA,
				functionAddress: dll.address + uintptr(funcRVA),
				name:            name,
				trampoline:      absAddress,
			}
			funcExp.GetSyscallNumbers(dll.address)
			dll.exportedNtFunctions = append(dll.exportedNtFunctions, funcExp)
		}

		if strings.HasPrefix(name, "Zw") {
			funcExp := Exportfunc{
				funcRVA:         funcRVA,
				functionAddress: dll.address + uintptr(funcRVA),
				name:            name,
				trampoline:      absAddress,
			}
			funcExp.GetSyscallNumbers(dll.address)
			dll.exportedZwFunctions = append(dll.exportedZwFunctions, funcExp)
		}

	}
	sort.SliceStable(dll.exportedNtFunctions, func(i, j int) bool {
		return (dll.exportedNtFunctions)[i].funcRVA < (dll.exportedNtFunctions)[j].funcRVA
	})
	sort.SliceStable(dll.exportedZwFunctions, func(i, j int) bool {
		return (dll.exportedZwFunctions)[i].funcRVA < (dll.exportedZwFunctions)[j].funcRVA
	})
}

// Get Image Export directory. We are interested in
// - AddressofFunctions
// - AddressOfNames
// - AddressOFNameOrdinals (maybe in the future)
// - Number of functions
func (dll *dllstruct) GetImageExportDirectory() {

	dll.exportDirectory.Characteristics = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress)))
	dll.exportDirectory.TimeDateStamp = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x4)))
	dll.exportDirectory.MajorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0x8)))
	dll.exportDirectory.MinorVersion = *((*uint16)(unsafe.Pointer(dll.exportDirectoryAddress + 0xa)))
	dll.exportDirectory.Name = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0xc)))
	dll.exportDirectory.Base = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x10)))
	dll.exportDirectory.NumberOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x14)))
	dll.exportDirectory.NumberOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x18)))
	dll.exportDirectory.AddressOfFunctions = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x1c)))
	dll.exportDirectory.AddressOfNames = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x20)))
	dll.exportDirectory.AddressOfNameOrdinals = *((*uint32)(unsafe.Pointer(dll.exportDirectoryAddress + 0x24)))

}

func (dll *dllstruct) getExportTableAddress() uintptr {
	e_lfanew := *((*uint32)(unsafe.Pointer(dll.address + 0x3c)))
	ntHeader := dll.address + uintptr(e_lfanew)
	fileHeader := ntHeader + 0x4
	// https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_file_header
	optionalHeader := fileHeader + 0x14 // 0x14 is the size of the image_file_header struct
	exportDir := optionalHeader + 0x70  // offset to export table
	exportDirOffset := *((*uint32)(unsafe.Pointer(exportDir)))
	dll.exportDirectoryAddress = dll.address + uintptr(exportDirOffset)
	return dll.exportDirectoryAddress
}

func GetStructOfLoadedDll(name string) (dllstruct, error) {
	modules := ListDllFromPEB()
	for _, module := range modules {
		if module.name == name {
			return module, nil
		}

	}
	return dllstruct{}, fmt.Errorf("dll not Found")
}

func PrintModules() {
	t := table.NewWriter()
	fmt.Printf("---------------------------------------------\nLoaded modules in current process\n")
	t.AppendHeader(table.Row{"#", "DLL Name", "Address"})

	for i, module := range ListDllFromPEB() {
		t.AppendRow(table.Row{i, module.name, fmt.Sprintf("0x%x", module.address)})
	}
	fmt.Println(t.Render())
}

// adds all loaded modules and their base addresses in a slice
func ListDllFromPEB() []dllstruct {

	peb := windows.RtlGetCurrentPeb()
	moduleList := peb.Ldr.InMemoryOrderModuleList
	a := moduleList.Flink
	loadedModules := []dllstruct{}
	for {

		listentry := uintptr(unsafe.Pointer(a))
		// -0x10 beginning of the _LDR_DATA_TABLE_ENTRY_ structure
		// +0x30 Dllbase address
		// +0x58 +0x8 address holding the address pointing to base dllname
		// offsets different for 32-bit processes
		DllBase := uintptr(listentry) - 0x10 + 0x30
		BaseDllName := uintptr(listentry) - 0x10 + 0x58 + 0x8

		v := *((*uintptr)(unsafe.Pointer(BaseDllName)))
		//fmt.Printf("%p\n", (unsafe.Pointer(v))) // prints the address that holds the dll name

		s := ((*uint16)(unsafe.Pointer(v))) // turn uintptr to *uint16
		dllNameStr := windows.UTF16PtrToString(s)
		if dllNameStr == "" {
			break
		}

		dllbaseaddr := *((*uintptr)(unsafe.Pointer(DllBase)))
		//fmt.Printf("%p\n", (unsafe.Pointer(dllbaseaddr))) // prints the dll base addr
		loadedModules = append(loadedModules, dllstruct{
			name:                   dllNameStr,
			address:                dllbaseaddr,
			exportDirectoryAddress: 0,
			exportDirectory:        IMAGE_EXPORT_DIRECTORY{Characteristics: 0, TimeDateStamp: 0, MajorVersion: 0, MinorVersion: 0, Name: 0, Base: 0, NumberOfFunctions: 0, NumberOfNames: 0, AddressOfFunctions: 0, AddressOfNames: 0, AddressOfNameOrdinals: 0},
			exportedNtFunctions:    []Exportfunc{},
			exportedZwFunctions:    []Exportfunc{},
		})
		a = a.Flink
	}

	return loadedModules
}

```
