Goal
In this lab we’ll write a simple Windows application written in Go that will serve as a loader for our dll.
It uses standard Windows API functions, accessed via Go’s windows and syscall packages,
to dynamically load the calc_dll.dll (created in Lab 1.1) and execute its exported LaunchCalc function.
This helps us come to grips with the most vanilla method of interacting with DLLs before we move on to more advanced techniques.
Code
Note that I also provide the code with a hefty helping of explanatory comments at the bottom. Also note the build tags at the top are only required if you are developing in Darwin/Linux, if you’re working directly inside of Windows these can be omitted.
//go:build windows
// +build windows
package main
import (
"fmt"
"log"
"syscall"
_ "unsafe"
"golang.org/x/sys/windows"
)
func main() {
fmt.Println("[+] Starting basic Go DLL loader...")
dllPath := "calc_dll.dll"
fmt.Printf("[+] Attempting to load DLL: %s\n", dllPath)
dllHandle, err := windows.LoadLibrary(dllPath)
if err != nil {
log.Fatalf("[-] Failed to load DLL '%s': %v\n", dllPath, err)
}
defer func() {
fmt.Println("[+] Attempting to free DLL handle...")
err := windows.FreeLibrary(dllHandle)
if err != nil {
log.Printf("[!] Warning: Failed to free DLL handle: %v\n", err)
} else {
fmt.Println("[+] DLL handle freed successfully.")
}
}()
fmt.Printf("[+] DLL loaded successfully. Handle: 0x%X\n", dllHandle)
funcName := "LaunchCalc"
fmt.Printf("[+] Attempting to get address of function: %s\n", funcName)
funcAddr, err := windows.GetProcAddress(dllHandle, funcName)
if err != nil {
log.Fatalf("[-] Failed to find function '%s' in DLL: %v\n", funcName, err)
}
fmt.Printf("[+] Function '%s' found at address: 0x%X\n", funcName, funcAddr)
fmt.Printf("[+] Calling function '%s'...\n", funcName)
ret, _, callErr := syscall.SyscallN(funcAddr, 0, 0, 0, 0)
if callErr != 0 {
log.Fatalf("[-] Error occurred during syscall to '%s': %v\n", funcName, callErr)
}
if ret != 0 {
fmt.Printf("[+] Function '%s' executed successfully (returned TRUE).\n", funcName)
} else {
fmt.Printf("[-] Function '%s' execution reported failure (returned FALSE).\n", funcName)
}
fmt.Println("[+] Loader finished.")
}
Code Breakdown
Imports:
fmt,log: Standard packages for printing output and handling errors.syscall: Used specifically for theSyscallNfunction, which allows calling arbitrary function pointers (like the one returned byGetProcAddress).golang.org/x/sys/windows: The simplest package for interacting with the Windows API. It provides Go-style wrappers likeLoadLibraryandGetProcAddress.
dllPath := "calc_dll.dll":
- Defines the name of the DLL file.
- Since no full path is given,
LoadLibrarywill search for it in standard locations, including the directory whereloader.exeis launched from.
windows.LoadLibrary(dllPath):
- Calls the
LoadLibraryWWindows API function to load the specified DLL into the current process’s memory. - It returns a handle (
HMODULE) to the loaded DLL or an error if it fails.
defer windows.FreeLibrary(dllHandle):
- Idiomatic Go way of cleaning up.
- The defer statement ensures that
FreeLibraryis called after themainfunction finishes (either normally or due to a panic). FreeLibrarydecrements the DLL’s reference count; the OS unloads the DLL from memory when its reference count drops to zero.
windows.GetProcAddress(dllHandle, funcName):
- Calls the
GetProcAddressWindows API function. - It takes the handle of the loaded DLL and the name of the exported function ("
LaunchCalc") and returns the memory address where that function resides, or an error if the function isn’t found in the DLL’s export table.
syscall.SyscallN(funcAddr, 0, 0, 0, 0):
- This is the core execution step.
funcAddr: The memory address ofLaunchCalcobtained fromGetProcAddress.0: The number of arguments ourLaunchCalcfunction takes, in this case, none.- The subsequent
0s are placeholders for the arguments themselves, since we have0arguments, these are just padding. - It returns
ret(the function’s return value, cast touintptr), a reserved value (usually ignored), andcallErr(an error object representing thesyscall’s success/failure status).
Error/Return Value Checks:
- The code checks both
callErr(did thesyscallitself fail?) andret - What did
LaunchCalcreturn?TRUE/1for success,FALSE/0for failure.
Instructions
Compile source code into an amd64 *.exe binary for Windows
GOOS=windows GOARCH=amd64 go build
Then simply run executable on target machine, in same directory as *.dll produced in Lab 1.1.
Expected Outcome
Upon executing the loader calc.exe should launch along with the following output printed to terminal:

Code with Comments
//go:build windows
// +build windows
package main
import (
"fmt"
"log"
"syscall"
_ "unsafe" // Required only if directly manipulating pointers in complex ways, but good practice to know syscall uses them.
// Use the preferred windows package for API calls
"golang.org/x/sys/windows"
)
func main() {
fmt.Println("[+] Starting basic Go DLL loader...")
// Define the path to the DLL.
// LoadLibrary will search in standard locations, including the current directory.
dllPath := "calc_dll.dll"
fmt.Printf("[+] Attempting to load DLL: %s\n", dllPath)
// Load the DLL using LoadLibraryW (the Unicode version)
// windows.LoadLibrary is a wrapper around the LoadLibraryW Windows API call.
dllHandle, err := windows.LoadLibrary(dllPath)
if err != nil {
// If LoadLibrary fails, err will be non-nil.
log.Fatalf("[-] Failed to load DLL '%s': %v\n", dllPath, err)
}
// If LoadLibrary succeeds, ensure FreeLibrary is called when main exits.
// This decrements the DLL's reference count.
defer func() {
fmt.Println("[+] Attempting to free DLL handle...")
err := windows.FreeLibrary(dllHandle)
if err != nil {
log.Printf("[!] Warning: Failed to free DLL handle: %v\n", err)
} else {
fmt.Println("[+] DLL handle freed successfully.")
}
}()
fmt.Printf("[+] DLL loaded successfully. Handle: 0x%X\n", dllHandle)
// Define the name of the function we want to call
funcName := "LaunchCalc"
fmt.Printf("[+] Attempting to get address of function: %s\n", funcName)
// Get the address of the exported function using GetProcAddress
// windows.GetProcAddress wraps the GetProcAddress Windows API call.
// It requires the DLL handle and the function name (as a null-terminated string).
funcAddr, err := windows.GetProcAddress(dllHandle, funcName)
if err != nil {
// If GetProcAddress fails (e.g., function not found), err will be non-nil.
log.Fatalf("[-] Failed to find function '%s' in DLL: %v\n", funcName, err)
}
fmt.Printf("[+] Function '%s' found at address: 0x%X\n", funcName, funcAddr)
// Call the function using syscall.SyscallN
// SyscallN is used to call a function pointer when the number of arguments is known at compile time.
// For LaunchCalc(), which takes no arguments (BOOL LaunchCalc()), we call it like this:
// SyscallN(functionAddress, argCount, arg1, arg2, ...)
// Here, argCount is 0. We pass uintptr(0) for unused arguments.
fmt.Printf("[+] Calling function '%s'...\n", funcName)
// The first return value 'ret' holds the function's return value (BOOL as uintptr: 1 for TRUE, 0 for FALSE).
// The second return value is reserved (usually 0 on success).
// The third return value 'callErr' holds any error from the syscall itself (e.g., access violation).
ret, _, callErr := syscall.SyscallN(funcAddr, 0, 0, 0, 0)
// Check the error returned by the syscall mechanism itself.
// callErr != 0 indicates a problem during the call setup or execution (like invalid address).
// Note: '0' corresponds to ERROR_SUCCESS in Windows syscalls.
if callErr != 0 {
log.Fatalf("[-] Error occurred during syscall to '%s': %v\n", funcName, callErr)
}
// Check the actual return value of the LaunchCalc function.
// Our DLL function returns TRUE (1) on success, FALSE (0) on failure.
if ret != 0 { // Corresponds to TRUE
fmt.Printf("[+] Function '%s' executed successfully (returned TRUE).\n", funcName)
} else { // Corresponds to FALSE
// This might happen if VirtualAlloc failed inside the DLL, for example.
fmt.Printf("[-] Function '%s' execution reported failure (returned FALSE).\n", funcName)
}
fmt.Println("[+] Loader finished.")
}