3. Programmatically detect ntdll hooks
#EDREvasion #UserlandHooks #unhook
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.
A better approach is to use Hell's Gate (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:
With the above in mind, let's dive into the code
Get NTDLL.dll base address
Walking / Exploring the PEB (process environment block) 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.
To get a pointer to the PEB the following undocumented function was used.
[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.
peb := windows.RtlGetCurrentPeb()
PEB Structure
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
}
To view the PEB Structure in windbg we can use the following command:
dt nt!_PEB @$Peb

We are particularly interested in the PEB_LDR_DATA
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:
x -r1 ((ntdll!_PEB_LDR_DATA *)0x7ffb0e1143c0)

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.
Once again to view InMemoryOrderMOduleList we just click on it in windbg. The command is generated for us:
dx -r1 (*((ntdll!_LIST_ENTRY *)0x7ffb0e1143e0))

The LDR_DATA_TABLE_ENTRY structure is defined as follows:
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

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:
// 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.
The following function was created to return the dllstruct needed. It receives the dll name as an argument and returns the struct.
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.
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())
}

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

We can see that the addresses and names match.
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.
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).
Similarly to the techniques used in Process Hollowing we can get the export directory from the Optional Header.
MS-DOS Stub: 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): 4-bytes (PE00)
COFF File Header (Object and Image): (2+2+4+4+4+2+2) 20-bytes (0x14)
Optional Header Standard Fields (Image Only): Offset to Export Directory 0x70
The above translated to the code below:
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
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)))
}
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
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
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.
Let's have another look in windbg to identify the byte sequence of a syscall;ret;

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


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

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

Complete Code
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
}
Last updated
Was this helpful?