Overview
We’ll now modify our code from Lab 4.1 to resolve the imported functions required by the calc_dll.dll
.
This involves:
- Parsing the DLL’s Import Directory,
- Loading the necessary dependency DLLs (like
kernel32.dll
), - Finding the addresses of the required functions within those dependencies using
GetProcAddress
, and - Patching the Import Address Table (IAT) within our manually mapped DLL image with these resolved addresses.
Notes
We’ll remove our forced relocations from the previous lab, this was only to ensure we tested the relocation logic, but since we now know it works, no need to do so any longer.
Also note that there is no need to do the same for IAT lookup (meaning forcing it as with base relocations). Unlike base relocations which only need to happen if the delta
is non-zero (and which we could force by making delta
non-zero), IAT resolution is always necessary if the DLL imports any functions, which is always the case if you use other win32 API functions like we do.
In any case, our output will confirm whether were able to resolve DLL imports, so we’ll know whether we’ve succeeded or not.
Code
//go:build windows
// +build windows
package main
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"log"
"os"
"runtime"
"unsafe"
"golang.org/x/sys/windows"
)
// --- Existing PE Structures ---
type IMAGE_DOS_HEADER struct {
Magic uint16
_ [58]byte
Lfanew int32
} //nolint:revive
type IMAGE_FILE_HEADER struct {
Machine uint16
NumberOfSections uint16
TimeDateStamp uint32
PointerToSymbolTable uint32
NumberOfSymbols uint32
SizeOfOptionalHeader uint16
Characteristics uint16
} //nolint:revive
type IMAGE_DATA_DIRECTORY struct{ VirtualAddress, Size uint32 } //nolint:revive
type IMAGE_OPTIONAL_HEADER64 struct {
Magic uint16
MajorLinkerVersion uint8
MinorLinkerVersion uint8
SizeOfCode uint32
SizeOfInitializedData uint32
SizeOfUninitializedData uint32
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
MajorOperatingSystemVersion uint16
MinorOperatingSystemVersion uint16
MajorImageVersion uint16
MinorImageVersion uint16
MajorSubsystemVersion uint16
MinorSubsystemVersion uint16
Win32VersionValue uint32
SizeOfImage uint32
SizeOfHeaders uint32
CheckSum uint32
Subsystem uint16
DllCharacteristics uint16
SizeOfStackReserve uint64
SizeOfStackCommit uint64
SizeOfHeapReserve uint64
SizeOfHeapCommit uint64
LoaderFlags uint32
NumberOfRvaAndSizes uint32
DataDirectory [16]IMAGE_DATA_DIRECTORY
} //nolint:revive
type IMAGE_SECTION_HEADER struct {
Name [8]byte
VirtualSize, VirtualAddress, SizeOfRawData, PointerToRawData, PointerToRelocations, PointerToLinenumbers uint32
NumberOfRelocations, NumberOfLinenumbers uint16
Characteristics uint32
} //nolint:revive
type IMAGE_BASE_RELOCATION struct{ VirtualAddress, SizeOfBlock uint32 } //nolint:revive
type IMAGE_IMPORT_DESCRIPTOR struct{ OriginalFirstThunk, TimeDateStamp, ForwarderChain, Name, FirstThunk uint32 } //nolint:revive
// --- Constants ---
const (
IMAGE_DOS_SIGNATURE = 0x5A4D
IMAGE_NT_SIGNATURE = 0x00004550
IMAGE_DIRECTORY_ENTRY_BASERELOC = 5
IMAGE_DIRECTORY_ENTRY_IMPORT = 1
IMAGE_REL_BASED_DIR64 = 10
IMAGE_REL_BASED_ABSOLUTE = 0
IMAGE_ORDINAL_FLAG64 = uintptr(1) << 63
MEM_COMMIT = 0x00001000
MEM_RESERVE = 0x00002000
MEM_RELEASE = 0x8000
PAGE_READWRITE = 0x04
PAGE_EXECUTE_READWRITE = 0x40
)
// --- Global Proc Address Loader ---
var (
kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
procGetProcAddress = kernel32DLL.NewProc("GetProcAddress")
)
// --- Helper Functions ---
func sectionNameToString(nameBytes [8]byte) string {
n := bytes.IndexByte(nameBytes[:], 0)
if n == -1 {
n = 8
}
return string(nameBytes[:n])
}
func machineTypeToString(machine uint16) string {
switch machine {
case 0x0:
return "Unknown"
case 0x14c:
return "x86 (I386)"
case 0x8664:
return "x64 (AMD64)"
case 0xaa64:
return "ARM64"
case 0x1c0:
return "ARM"
default:
return "Other"
}
}
func magicTypeToString(magic uint16) string {
switch magic {
case 0x10b:
return "PE32 (32-bit)"
case 0x20b:
return "PE32+ (64-bit)"
default:
return "Unknown/Invalid"
}
}
// --- Main Function ---
func main() {
// Ensure running on Windows
if runtime.GOOS != "windows" {
log.Fatal("[-] This program must be run on Windows.")
}
fmt.Println("[+] Starting Manual DLL Mapper (with IAT Resolution)...")
// --- Step 1: Read DLL and Parse Headers ---
if len(os.Args) < 2 {
log.Fatalf("[-] Usage: %s <path_to_dll>\n", os.Args[0])
}
dllPath := os.Args[1]
fmt.Printf("[+] Reading file: %s\n", dllPath)
dllBytes, err := os.ReadFile(dllPath)
if err != nil {
log.Fatalf("[-] Failed to read file '%s': %v\n", dllPath, err)
}
reader := bytes.NewReader(dllBytes)
var dosHeader IMAGE_DOS_HEADER
if err := binary.Read(reader, binary.LittleEndian, &dosHeader); err != nil {
log.Fatalf("[-] Failed to read DOS header: %v\n", err)
}
if dosHeader.Magic != IMAGE_DOS_SIGNATURE {
log.Fatalf("[-] Invalid DOS signature")
}
if _, err := reader.Seek(int64(dosHeader.Lfanew), 0); err != nil {
log.Fatalf("[-] Failed to seek to NT Headers: %v\n", err)
}
var peSignature uint32
if err := binary.Read(reader, binary.LittleEndian, &peSignature); err != nil {
log.Fatalf("[-] Failed to read PE signature: %v\n", err)
}
if peSignature != IMAGE_NT_SIGNATURE {
log.Fatalf("[-] Invalid PE signature")
}
var fileHeader IMAGE_FILE_HEADER
if err := binary.Read(reader, binary.LittleEndian, &fileHeader); err != nil {
log.Fatalf("[-] Failed to read File Header: %v\n", err)
}
var optionalHeader IMAGE_OPTIONAL_HEADER64
if err := binary.Read(reader, binary.LittleEndian, &optionalHeader); err != nil {
log.Fatalf("[-] Failed to read Optional Header: %v\n", err)
}
if optionalHeader.Magic != 0x20b {
log.Printf("[!] Warning: Optional Header Magic is not PE32+ (0x20b).")
}
fmt.Println("[+] Parsed PE Headers successfully.")
fmt.Printf("[+] Target ImageBase: 0x%X\n", optionalHeader.ImageBase)
fmt.Printf("[+] Target SizeOfImage: 0x%X (%d bytes)\n", optionalHeader.SizeOfImage, optionalHeader.SizeOfImage)
// --- Step 2: Allocate Memory for DLL ---
fmt.Printf("[+] Allocating 0x%X bytes of memory for DLL...\n", optionalHeader.SizeOfImage)
allocSize := uintptr(optionalHeader.SizeOfImage)
preferredBase := uintptr(optionalHeader.ImageBase)
allocBase, err := windows.VirtualAlloc(preferredBase, allocSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_EXECUTE_READWRITE)
if err != nil {
fmt.Printf("[*] Failed to allocate at preferred base 0x%X: %v. Trying arbitrary address...\n", preferredBase, err)
allocBase, err = windows.VirtualAlloc(0, allocSize, windows.MEM_RESERVE|windows.MEM_COMMIT, windows.PAGE_EXECUTE_READWRITE)
if err != nil {
log.Fatalf("[-] Failed to allocate memory at arbitrary address: %v\n", err)
}
}
fmt.Printf("[+] DLL memory allocated successfully at actual base address: 0x%X\n", allocBase)
defer func() {
fmt.Printf("[+] Attempting to free main DLL allocation at 0x%X...\n", allocBase)
err := windows.VirtualFree(allocBase, 0, windows.MEM_RELEASE)
if err != nil {
log.Printf("[!] Warning: Failed to free main DLL memory: %v\n", err)
} else {
fmt.Println("[+] Main DLL memory freed successfully.")
}
}()
// --- Step 3: Copy Headers into Allocated Memory ---
fmt.Printf("[+] Copying PE headers (%d bytes) to allocated memory...\n", optionalHeader.SizeOfHeaders)
headerSize := uintptr(optionalHeader.SizeOfHeaders)
dllBytesPtr := uintptr(unsafe.Pointer(&dllBytes[0]))
var bytesWritten uintptr
err = windows.WriteProcessMemory(windows.CurrentProcess(), allocBase, (*byte)(unsafe.Pointer(dllBytesPtr)), headerSize, &bytesWritten)
if err != nil || bytesWritten != headerSize {
log.Fatalf("[-] Failed to copy PE headers to allocated memory: %v (Bytes written: %d)", err, bytesWritten)
}
fmt.Printf("[+] Copied %d bytes of headers successfully.\n", bytesWritten)
// --- Step 4: Copy Sections into Allocated Memory ---
fmt.Println("[+] Copying sections...")
firstSectionHeaderAddr := allocBase + uintptr(dosHeader.Lfanew) + 4 + unsafe.Sizeof(fileHeader) + uintptr(fileHeader.SizeOfOptionalHeader) // Address of first section header IN allocBase
sectionHeaderSize := unsafe.Sizeof(IMAGE_SECTION_HEADER{})
numSections := fileHeader.NumberOfSections
for i := uint16(0); i < numSections; i++ {
currentSectionHeaderAddr := firstSectionHeaderAddr + uintptr(i)*sectionHeaderSize
sectionHeader := (*IMAGE_SECTION_HEADER)(unsafe.Pointer(currentSectionHeaderAddr))
// sectionName := sectionNameToString(sectionHeader.Name) // Less verbose logging
if sectionHeader.SizeOfRawData == 0 {
continue
}
if uintptr(sectionHeader.PointerToRawData)+uintptr(sectionHeader.SizeOfRawData) > uintptr(len(dllBytes)) {
log.Printf("[!] Warning: Section %d ('%s') raw data exceeds file size. Skipping copy.", i, sectionNameToString(sectionHeader.Name))
continue
}
sourceAddr := dllBytesPtr + uintptr(sectionHeader.PointerToRawData)
if uintptr(sectionHeader.VirtualAddress)+uintptr(sectionHeader.SizeOfRawData) > allocSize {
log.Printf("[!] Warning: Section %d ('%s') virtual address/size exceeds allocated size. Skipping copy.", i, sectionNameToString(sectionHeader.Name))
continue
}
destAddr := allocBase + uintptr(sectionHeader.VirtualAddress)
sizeToCopy := uintptr(sectionHeader.SizeOfRawData)
err = windows.WriteProcessMemory(windows.CurrentProcess(), destAddr, (*byte)(unsafe.Pointer(sourceAddr)), sizeToCopy, &bytesWritten)
if err != nil || bytesWritten != sizeToCopy {
log.Fatalf(" [-] Failed to copy section '%s': %v (Bytes written: %d)", sectionNameToString(sectionHeader.Name), err, bytesWritten)
}
}
fmt.Println("[+] All sections copied.")
// --- Step 5: Process Base Relocations ---
fmt.Println("[+] Checking if base relocations are needed...")
delta := int64(allocBase) - int64(optionalHeader.ImageBase)
if delta == 0 {
fmt.Println("[+] Image loaded at preferred base. No relocations needed.")
} else {
fmt.Printf("[+] Image loaded at non-preferred base (Delta: 0x%X). Processing relocations...\n", delta)
relocDirEntry := optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]
relocDirRVA := relocDirEntry.VirtualAddress
relocDirSize := relocDirEntry.Size
if relocDirRVA == 0 || relocDirSize == 0 {
fmt.Println("[!] Warning: Image rebased, but no relocation directory found or empty.")
} else {
fmt.Printf("[+] Relocation Directory found at RVA 0x%X, Size 0x%X\n", relocDirRVA, relocDirSize)
relocTableBase := allocBase + uintptr(relocDirRVA)
relocTableEnd := relocTableBase + uintptr(relocDirSize)
currentBlockAddr := relocTableBase
totalFixups := 0
for currentBlockAddr < relocTableEnd {
if currentBlockAddr < allocBase || currentBlockAddr+unsafe.Sizeof(IMAGE_BASE_RELOCATION{}) > allocBase+allocSize {
log.Printf("[!] Error: Relocation block address 0x%X is outside allocated range. Stopping relocations.", currentBlockAddr)
break
}
blockHeader := (*IMAGE_BASE_RELOCATION)(unsafe.Pointer(currentBlockAddr))
if blockHeader.VirtualAddress == 0 || blockHeader.SizeOfBlock <= uint32(unsafe.Sizeof(IMAGE_BASE_RELOCATION{})) {
break
}
if currentBlockAddr+uintptr(blockHeader.SizeOfBlock) > relocTableEnd {
log.Printf("[!] Error: Relocation block size (%d) at 0x%X exceeds directory bounds. Stopping relocations.", blockHeader.SizeOfBlock, currentBlockAddr)
break
}
numEntries := (blockHeader.SizeOfBlock - uint32(unsafe.Sizeof(IMAGE_BASE_RELOCATION{}))) / 2
entryPtr := currentBlockAddr + unsafe.Sizeof(IMAGE_BASE_RELOCATION{})
for i := uint32(0); i < numEntries; i++ {
entryAddr := entryPtr + uintptr(i*2)
if entryAddr < allocBase || entryAddr+2 > allocBase+allocSize {
log.Printf(" [!] Error: Relocation entry address 0x%X is outside allocated range. Skipping entry.", entryAddr)
continue
}
entry := *(*uint16)(unsafe.Pointer(entryAddr))
relocType := entry >> 12
offset := entry & 0xFFF
if relocType == IMAGE_REL_BASED_DIR64 {
patchAddr := allocBase + uintptr(blockHeader.VirtualAddress) + uintptr(offset)
if patchAddr < allocBase || patchAddr+8 > allocBase+allocSize {
log.Printf(" [!] Error: Relocation patch address 0x%X is outside allocated range. Skipping fixup.", patchAddr)
continue
}
originalValuePtr := (*uint64)(unsafe.Pointer(patchAddr))
*originalValuePtr = uint64(int64(*originalValuePtr) + delta)
totalFixups++
} else if relocType != IMAGE_REL_BASED_ABSOLUTE {
fmt.Printf(" [!] Warning: Skipping unhandled relocation type %d at offset 0x%X\n", relocType, offset)
}
}
currentBlockAddr += uintptr(blockHeader.SizeOfBlock)
}
fmt.Printf("[+] Relocation processing complete. Total fixups applied: %d\n", totalFixups)
}
}
// --- End Step 5 ---
// --- Step 6: Process Import Address Table (IAT) ---
fmt.Println("[+] Processing Import Address Table (IAT)...")
importDirEntry := optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
importDirRVA := importDirEntry.VirtualAddress
if importDirRVA == 0 {
fmt.Println("[*] No Import Directory found. Skipping IAT processing.")
} else {
fmt.Printf("[+] Import Directory found at RVA 0x%X\n", importDirRVA)
importDescSize := unsafe.Sizeof(IMAGE_IMPORT_DESCRIPTOR{})
importDescBase := allocBase + uintptr(importDirRVA)
importCount := 0
// fmt.Printf(" DEBUG: Import Directory VA: 0x%X\n", importDescBase)
// // DEBUG: Read and print the first descriptor BEFORE the loop
// if importDescBase < allocBase || importDescBase+importDescSize > allocBase+allocSize {
// log.Printf(" [-] Error: Calculated Import Directory VA 0x%X is outside allocated range [0x%X - 0x%X]. Cannot read first descriptor.",
// importDescBase, allocBase, allocBase+allocSize-1)
// } else {
// firstDesc := (*IMAGE_IMPORT_DESCRIPTOR)(unsafe.Pointer(importDescBase))
// fmt.Printf(" DEBUG: First Descriptor Raw Values: OFT=0x%X, TS=0x%X, FC=0x%X, NameRVA=0x%X, FT=0x%X\n",
// firstDesc.OriginalFirstThunk, firstDesc.TimeDateStamp, firstDesc.ForwarderChain, firstDesc.Name, firstDesc.FirstThunk)
// }
// Iterate through IMAGE_IMPORT_DESCRIPTOR array (null terminated)
for i := 0; ; i++ {
currentDescAddr := importDescBase + uintptr(i)*importDescSize
if currentDescAddr < allocBase || currentDescAddr+importDescSize > allocBase+allocSize {
log.Printf(" [!] Error: Calculated descriptor address 0x%X is outside allocated range. Stopping IAT processing.", currentDescAddr)
break
}
// fmt.Printf("\n DEBUG: Reading descriptor %d at address 0x%X\n", i, currentDescAddr) // Keep DEBUG optional
importDesc := (*IMAGE_IMPORT_DESCRIPTOR)(unsafe.Pointer(currentDescAddr))
// fmt.Printf(" DEBUG: Desc %d: OFT=0x%X, TS=0x%X, FC=0x%X, NameRVA=0x%X, FT=0x%X\n", i, importDesc.OriginalFirstThunk, importDesc.TimeDateStamp, importDesc.ForwarderChain, importDesc.Name, importDesc.FirstThunk) // Keep DEBUG optional
if importDesc.OriginalFirstThunk == 0 && importDesc.FirstThunk == 0 { /* fmt.Printf(" DEBUG: Null descriptor found at index %d. Stopping.\n", i); */
break
}
importCount++
dllNameRVA := importDesc.Name
if dllNameRVA == 0 {
log.Printf(" [!] Warning: Descriptor %d has null Name RVA. Skipping.", i)
continue
}
dllNamePtrAddr := allocBase + uintptr(dllNameRVA)
// fmt.Printf(" DEBUG: DLL Name String RVA=0x%X, VA=0x%X\n", dllNameRVA, dllNamePtrAddr)
if dllNamePtrAddr < allocBase || dllNamePtrAddr >= allocBase+allocSize {
log.Printf(" [!] Error: Calculated DLL Name VA 0x%X is outside allocated range. Skipping descriptor %d.", dllNamePtrAddr, i)
continue
}
dllNamePtr := (*byte)(unsafe.Pointer(dllNamePtrAddr))
dllName := windows.BytePtrToString(dllNamePtr)
fmt.Printf(" [->] Processing imports for: %s\n", dllName)
hModule, err := windows.LoadLibrary(dllName)
if err != nil {
log.Fatalf(" [-] FATAL: Failed to load dependency library '%s': %v\n", dllName, err)
}
// fmt.Printf(" [+] Loaded '%s' successfully. Handle: 0x%X\n", dllName, hModule) // Less verbose
iltRVA := importDesc.OriginalFirstThunk
if iltRVA == 0 {
iltRVA = importDesc.FirstThunk
}
iatRVA := importDesc.FirstThunk
if iltRVA == 0 || iatRVA == 0 {
log.Printf(" [!] Warning: Descriptor %d for '%s' has null ILT/IAT RVA. Skipping.", i, dllName)
continue
}
iltBase := allocBase + uintptr(iltRVA)
iatBase := allocBase + uintptr(iatRVA)
entrySize := unsafe.Sizeof(uintptr(0))
// fmt.Printf(" DEBUG: ILT VA=0x%X, IAT VA=0x%X\n", iltBase, iatBase)
for j := uintptr(0); ; j++ {
iltEntryAddr := iltBase + (j * entrySize)
iatEntryAddr := iatBase + (j * entrySize)
if iltEntryAddr < allocBase || iltEntryAddr >= allocBase+allocSize {
log.Printf(" [!] Error: Calculated ILT Entry VA 0x%X is outside allocated range. Stopping imports for %s.", iltEntryAddr, dllName)
break
}
iltEntry := *(*uintptr)(unsafe.Pointer(iltEntryAddr))
// fmt.Printf(" DEBUG: Reading ILT Entry %d at 0x%X, Value=0x%X\n", j, iltEntryAddr, iltEntry)
if iltEntry == 0 {
break
}
var funcAddr uintptr
var procErr error
importNameStr := ""
if iltEntry&IMAGE_ORDINAL_FLAG64 != 0 {
ordinal := uint16(iltEntry & 0xFFFF)
importNameStr = fmt.Sprintf("Ordinal %d", ordinal)
// fmt.Printf(" DEBUG: Importing by %s\n", importNameStr)
ret, _, callErr := procGetProcAddress.Call(uintptr(hModule), uintptr(ordinal))
if ret == 0 {
errMsg := fmt.Sprintf("GetProcAddress by ordinal %d returned NULL", ordinal)
if callErr != nil && callErr != windows.ERROR_SUCCESS {
procErr = fmt.Errorf("%s - syscall error: %w", errMsg, callErr)
} else {
procErr = errors.New(errMsg)
}
} else if callErr != nil && callErr != windows.ERROR_SUCCESS {
procErr = fmt.Errorf("GetProcAddress by ordinal %d syscall failed: %w", ordinal, callErr)
}
funcAddr = ret
} else {
hintNameRVA := uint32(iltEntry)
hintNameAddr := allocBase + uintptr(hintNameRVA)
// fmt.Printf(" DEBUG: Importing by Name. Hint/Name RVA=0x%X, VA=0x%X\n", hintNameRVA, hintNameAddr)
if hintNameAddr < allocBase || hintNameAddr+2 >= allocBase+allocSize {
log.Printf(" [!] Error: Calculated Hint/Name VA 0x%X is outside allocated range. Skipping import.", hintNameAddr)
continue
}
funcNamePtr := unsafe.Pointer(hintNameAddr + 2)
funcName := windows.BytePtrToString((*byte)(funcNamePtr))
importNameStr = fmt.Sprintf("Function '%s'", funcName)
// fmt.Printf(" DEBUG: Importing %s\n", importNameStr)
funcAddr, procErr = windows.GetProcAddress(hModule, funcName)
if procErr != nil && funcAddr == 0 {
procErr = fmt.Errorf("GetProcAddress failed for %s: %w", funcName, procErr)
} else if procErr == nil && funcAddr == 0 {
procErr = fmt.Errorf("GetProcAddress returned NULL for %s", funcName)
}
}
if procErr != nil || funcAddr == 0 {
log.Fatalf(" [-] FATAL: Failed to resolve import %s from %s: %v (Addr: 0x%X)\n", importNameStr, dllName, procErr, funcAddr)
}
if iatEntryAddr < allocBase || iatEntryAddr >= allocBase+allocSize {
log.Printf(" [!] Error: Calculated IAT Entry VA 0x%X is outside allocated range. Skipping patch for %s.", iatEntryAddr, importNameStr)
continue
}
iatEntryPtr := (*uintptr)(unsafe.Pointer(iatEntryAddr))
*iatEntryPtr = funcAddr
// fmt.Printf(" [+] Resolved %s -> 0x%X. Patched IAT at 0x%X.\n", importNameStr, funcAddr, iatEntryAddr) // Less verbose
} // End inner loop
fmt.Printf(" [+] Finished imports for '%s'.\n", dllName)
} // End outer loop
fmt.Printf("[+] Import processing complete (%d DLLs).\n", importCount)
}
// --- *** End Step 6 *** ---
// --- Step 7: Self-Check ---
fmt.Println("[+] Manual mapping process complete (Headers, Sections copied, Relocations potentially applied, IAT resolved).")
fmt.Println("[+] Self-Check Suggestion: Use a debugger...")
fmt.Printf(" to inspect the memory at the allocated base address (0x%X).\n", allocBase)
fmt.Println(" Verify that the 'MZ' and 'PE' signatures are present at the start")
fmt.Println(" and that data corresponding to sections appears at the correct RVAs.")
fmt.Println(" If relocations occurred, check known absolute addresses (if any) were patched.")
fmt.Println(" Inspect the IAT section: pointers should now point to actual function addresses in loaded modules.")
fmt.Println("\n[+] Press Enter to free memory and exit.")
fmt.Scanln()
fmt.Println("[+] Mapper finished.")
}
Code Breakdown
Truncated Headers
Note that I’ve now started truncating some of the headers, only including values we actually require.
For example before our IMAGE_DOS_HEADER
explicitly defined each field
type IMAGE_DOS_HEADER struct {
Magic uint16 // Magic number (MZ)
Cblp uint16 // Bytes on last page of file
Cp uint16 // Pages in file
Crlc uint16 // Relocations
Cparhdr uint16 // Size of header in paragraphs
MinAlloc uint16 // Minimum extra paragraphs needed
MaxAlloc uint16 // Maximum extra paragraphs needed
Ss uint16 // Initial (relative) SS value
Sp uint16 // Initial SP value
Csum uint16 // Checksum
Ip uint16 // Initial IP value
Cs uint16 // Initial (relative) CS value
Lfarlc uint16 // File address of relocation table
Ovno uint16 // Overlay number
Res [4]uint16 // Reserved words
Oemid uint16 // OEM identifier (for e_oeminfo)
Oeminfo uint16 // OEM information; e_oemid specific
Res2 [10]uint16 // Reserved words
Lfanew int32 // File address of new exe header (PE header offset)
}
But this is complete overkill since, as we learned before, we’ll only ever use Magic
and Lfanew
. And so hence forth we’ll black box unused values (see below). It remains important however that we define the right amount of bytes between the fields so our program can correctly parse the values we do need, in this case 58 bytes. This is because each uint16
is 2 bytes, with Res
being 4 x 2 bytes, and Res2
being 10 x 2 bytes.
// --- Existing PE Structures ---
type IMAGE_DOS_HEADER struct {
Magic uint16
_ [58]byte
Lfanew int32
} //nolint:revive
New Structs/Constants/Variables
Structs
IMAGE_IMPORT_DESCRIPTOR
struct: Added to define the layout of entries in the Import Directory table.
Constants
IMAGE_DIRECTORY_ENTRY_IMPORT
(1): Index for the Import Directory.
IMAGE_ORDINAL_FLAG64
: Bit flag for identifying ordinal imports on 64-bit.
Structs
kernel32DLL = windows.NewLazySystemDLL("kernel32.dll")
: Loads kernel32.dll
lazily.
procGetProcAddress = kernel32DLL.NewProc("GetProcAddress")
: Gets a procedure object for GetProcAddress
itself. This is needed to correctly call GetProcAddress
by ordinal.
Process Import Address Table (Step 6)
This is where we now apply our IAT processing logic we learned about in Theory 4.2.
- Locate Import Directory: Gets the
importDirEntry
fromoptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]
. Skips if no import directory exists. - Calculate Descriptor Address: Calculates the starting VA (
importDescBase
) of theIMAGE_IMPORT_DESCRIPTOR
array within the mapped DLL’s memory. - Iterate Descriptors (Outer Loop): Loops through descriptors until a null one is found.
- Get Dependency DLL Name: Reads the DLL name string from the mapped memory using the
Name
RVA. - Load Dependency DLL: Calls
windows.LoadLibrary
to load the required DLL (e.g.,kernel32.dll
) into the current process. Stores the handle (hModule
). - Locate ILT/IAT: Calculates the VAs (
iltBase
,iatBase
) of the Import Lookup Table and Import Address Table within the mapped DLL’s memory. - Iterate Imports (Inner Loop): Loops through the ILT/IAT entries until a null ILT entry.
- Reads the ILT entry (
iltEntry
). - Check Import Type: Determines if import is by ordinal (
iltEntry & IMAGE_ORDINAL_FLAG64 != 0
) or by name. - Resolve Address:
- By Ordinal: Extracts the ordinal. Crucially, it now calls
procGetProcAddress.Call(uintptr(hModule), uintptr(ordinal))
using the dynamically loaded procedure object, which correctly handles ordinal lookups. It checks the return value (ret
) and the call error (callErr
) appropriately. - By Name: Calculates the name string address (skipping the hint). Calls the standard
windows.GetProcAddress(hModule, funcName)
wrapper, as this works correctly for name lookups. Error checking remains the same. - Stores the resolved address in
funcAddr
. Handles errors (fatal). - Patch IAT: Calculates the IAT entry address (
iatEntryAddr
) and usesunsafe.Pointer
to write the resolvedfuncAddr
into the IAT within the mapped DLL’s memory.
- Reads the ILT entry (
Interesting Observation
Perhaps you’ve noticed something peculiar about our code, right around line 364
:
hModule, err := windows.LoadLibrary(dllName)
if err != nil {
log.Fatalf(" [-] FATAL: Failed to load dependency library '%s': %v\n", dllName, err)
}
fmt.Printf(" [+] Loaded '%s' successfully. Handle: 0x%X\n", dllName, hModule)
Spotted the irony? We’re building a reflective loader with the primary goal of avoiding the use of LoadLibrary
, but in order to do so we have to process IAT, which requires us to… That’s right, use LoadLibrary
. But the devil is in the details, we’re primarily interested in avoiding its use for our main payload, and here we are using it to resolve the payload’s dependencies, i.e. legitimate Windows DLLs. So the key distinction is what’s being loaded. Loading kernel32.dll
is benign whereas loading MyEvilImplant.dll
via LoadLibrary
might trigger alerts.
Note however that sophisticated loaders might try to resolve imports without calling LoadLibrary
/GetProcAddress
at all, perhaps by manually parsing the export tables of dependency modules already loaded in the process (found via the PEB), but this adds significant complexity and fragility compared to just using the standard APIs for resolving dependencies.
Instructions
- Compile the IAT processor.
GOOS=windows GOARCH=amd64 go build
- Then copy it over to target system and invoke from command-line, providing as argument the dll you’d like to analyze, for example:
".\iat_process.exe .\calc_dll.dll"
Results
[+] Starting Manual DLL Mapper (with IAT Resolution)...
[+] Reading file: .\calc_dll.dll
[+] Parsed PE Headers successfully.
[+] Target ImageBase: 0x26A5B0000
[+] Target SizeOfImage: 0x22000 (139264 bytes)
[+] Allocating 0x22000 bytes of memory for DLL...
[+] DLL memory allocated successfully at actual base address: 0x26A5B0000
[+] Copying PE headers (1536 bytes) to allocated memory...
[+] Copied 1536 bytes of headers successfully.
[+] Copying sections...
[+] All sections copied.
[+] Checking if base relocations are needed...
[+] Image loaded at preferred base. No relocations needed.
[+] Processing Import Address Table (IAT)...
[+] Import Directory found at RVA 0x9000
[->] Processing imports for: KERNEL32.dll
[+] Finished imports for 'KERNEL32.dll'.
[->] Processing imports for: api-ms-win-crt-environment-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-environment-l1-1-0.dll'.
[->] Processing imports for: api-ms-win-crt-heap-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-heap-l1-1-0.dll'.
[->] Processing imports for: api-ms-win-crt-runtime-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-runtime-l1-1-0.dll'.
[->] Processing imports for: api-ms-win-crt-stdio-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-stdio-l1-1-0.dll'.
[->] Processing imports for: api-ms-win-crt-string-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-string-l1-1-0.dll'.
[->] Processing imports for: api-ms-win-crt-time-l1-1-0.dll
[+] Finished imports for 'api-ms-win-crt-time-l1-1-0.dll'.
[+] Import processing complete (7 DLLs).
[+] Manual mapping process complete (Headers, Sections copied, Relocations potentially applied, IAT resolved).
[+] Self-Check Suggestion: Use a debugger...
to inspect the memory at the allocated base address (0x26A5B0000).
Verify that the 'MZ' and 'PE' signatures are present at the start
and that data corresponding to sections appears at the correct RVAs.
If relocations occurred, check known absolute addresses (if any) were patched.
Inspect the IAT section: pointers should now point to actual function addresses in loaded modules.
[+] Press Enter to free memory and exit.
[+] Mapper finished.
[+] Attempting to free main DLL allocation at 0x26A5B0000...
[+] Main DLL memory freed successfully.
Discussion
DLL memory allocated successfully at actual base address: 0x26A5B0000
- Confirms the memory for the DLL was allocated successfully, and in this instance, it occurred at the preferredImageBase
(0x26A5B0000).Import Directory found at RVA 0x9000
- The program successfully located the start of the import information within the mapped PE headers using the Data Directory.Import processing complete (7 DLLs).
- Confirmation that the IAT processing logic successfully iterated through all 7 dependency DLLs listed in the import directory before encountering the null terminator, indicating the main loop worked correctly. (Implicitly, the inner loops resolved the functions, otherwise a fatal error would have occurred).Manual mapping process complete (Headers, Sections copied, Relocations potentially applied, IAT resolved).
- This final status message confirms all implemented stages of the reflective loading process up to this point completed successfully.
Conclusion
Let’s take stock: We’ve parsed the important PE information, we’ve loaded our DLL into memory, base internal addresses will be relocated if need be, and external dependencies are resolved (IAT patched).
The only remaining step is to actually execute code within it.