· Asparux · malware · 9 min read
Hash de API de Windows para la evasión de malware
En el panorama en constante evolución de la ciberseguridad, los desarrolladores de malware idean constantemente técnicas sofisticadas para ocultar sus creaciones maliciosas ante la detección y el análisis. Una de esas técnicas que ha ganado importancia se conoce como hash API. Este enfoque agrega una capa adicional de complejidad al ya intrincado mundo del análisis de malware al ocultar llamadas sospechosas a la API de Windows dentro de la tabla de direcciones de importación (IAT) de archivos ejecutables portátiles (PE). En este artículo, exploraremos el concepto de hash de API, su funcionamiento y por qué ha demostrado ser una estrategia muy eficaz para ocultar las verdaderas intenciones del malware.
Introducción
El desafío que enfrentan los desarrolladores de malware
Para los analistas de seguridad, un archivo PE con un IAT intacto sirve como una ventana a las capacidades del binario. Las API importadas de bibliotecas como “Ws2_32.dll” indican funcionalidades de red, mientras que funciones como “RegCreateKeyEx” revelan la capacidad de manipular el Registro de Windows. Esta transparencia ayuda enormemente a los analistas a identificar amenazas potenciales. Sin embargo, esta previsibilidad es un obstáculo para los desarrolladores de malware, obligándolos a buscar métodos que confundan sus intenciones.
API Hashing: la solución para desarrolladores de malware
Para contrarrestar la transparencia que proporciona la IAT, los autores de malware han ideado el hash API. Esta técnica tiene como objetivo complicar el análisis inicial de archivos PE, lo que dificulta que los analistas identifiquen llamadas API sospechosas mediante un examen estático. Al emplear API hash, los creadores de malware pueden garantizar que API específicas de Windows permanezcan ocultas a la observación casual. Esta capa de ocultamiento obliga a los analistas a profundizar más para descubrir la verdadera naturaleza del malware.
Descomprimiendo la mecánica del hash API
Imagine un escenario en el que un desarrollador de malware pretende emplear la función “CreateThread” dentro de su código. Normalmente, el IAT del expediente PE resultante revelaría abiertamente esta intención. Sin embargo, mediante el hash API, los desarrolladores pueden determinar dinámicamente la dirección de la función en tiempo de ejecución. Este proceso implica generar un valor hash para el nombre de la función, como “CreateThread”, y utilizar este hash para localizar la dirección virtual de la función dentro de la memoria del proceso. Esta dirección se puede utilizar para ejecutar la función dejando rastros mínimos en el IAT.
Un recorrido práctico
Para comprender mejor las complejidades del hash de API, veamos un ejemplo práctico. Crearemos un script de PowerShell para calcular valores hash para nombres de funciones y un programa Golang para resolver dinámicamente direcciones de funciones basadas en estos hashes. Este enfoque dinámico permite a los desarrolladores de malware ocultar llamadas a funciones críticas, haciendo que la verdadera intención del ejecutable sea menos visible para los analistas.
Código
powershell.ps1
$APIsToHash = @("CreateThread")
$APIsToHash | % {
$api = $_
$hash = 0x35
[int]$i = 0
$api.ToCharArray() | % {
$l = $_
$c = [int64]$l
$c = '0x{0:x}' -f $c
$hash += $hash * 0xab10f29f + $c -band 0xffffff
$hashHex = '0x{0:x}' -f $hash
$i++
write-host "Iteration $i : $l : $c : $hashHex"
}
write-host "$api`t $('0x00{0:x}' -f $hash)"
}
Este fragmento de código es un script de PowerShell que demuestra el proceso de hash de un nombre de función mediante un algoritmo de hash personalizado. Analicemos el código paso a paso:
$APIsToHash = @("CreateThread")
Aquí, se crea una matriz denominada que contiene un único elemento de cadena “CreateThread”. Esta matriz almacenará los nombres de las funciones que deben ser codificadas.
$APIsToHash | % {
$api = $_
$hash = 0x35
[int]$i = 0
El código utiliza una canalización ( |) para iterar sobre cada elemento de la $APIsToHash matriz. Para cada nombre de función (en este caso, “CreateThread”), el script configura variables iniciales:
- $api: Almacena el nombre de la función actual que se está procesando (por ejemplo, “CreateThread”).
- $hash: Inicializa el valor hash con el valor hexadecimal 0x35.
- $i: Inicializa una variable de contador para realizar un seguimiento de las iteraciones.
$api.ToCharArray() | % {
$l = $_
$c = [int64]$l
$c = '0x{0:x}' -f $c
$hash += $hash * 0xab10f29f + $c -band 0xffffff
$hashHex = '0x{0:x}' -f $hash
$i++
write-host "Iteration $i : $l : $c : $hashHex"
}
El script convierte el nombre de la función actual (por ejemplo, “CreateThread”) en una matriz de caracteres utilizando el .ToCharArray()método. Luego itera sobre cada carácter de la matriz. Para cada personaje:
- $l: Almacena el carácter actual que se está procesando.
- $c: Convierte el valor ASCII del carácter a int64 y lo formatea como una cadena hexadecimal.
- El hash se calcula utilizando el algoritmo personalizado: $hash += $hash * 0xab10f29f + $c -band 0xffffff. Esto actualiza el hash realizando operaciones aritméticas en el valor hash actual, un valor constante y el valor del carácter.
- $hashHex: formatea el valor hash actualizado como una cadena hexadecimal.
- El contador de bucle $i se incrementa y se imprime un mensaje que muestra los detalles de la iteración usando write-host.
write-host "$api`t $('0x00{0:x}' -f $hash)"
Después de recorrer todos los caracteres del nombre de la función, el script imprime el valor hash final en formato hexadecimal, junto con el nombre de la función original. El formato $(‘0x00{0:x}’ -f $hash) garantiza que el hash siempre esté representado por al menos cuatro dígitos en formato hexadecimal.
Básicamente, este script demuestra un algoritmo hash personalizado básico que calcula un valor hash para el nombre de la función de entrada iterando sobre sus caracteres y realizando operaciones aritméticas específicas. Tenga en cuenta que este es un ejemplo simplificado con fines ilustrativos y no un algoritmo hash seguro o resistente a colisiones.
apiHashing.go
package main
import (
"fmt"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
type IMAGE_DOS_HEADER struct {
e_magic uint16
e_cblp uint16
e_cp uint16
e_crlc uint16
e_cparhdr uint16
e_minalloc uint16
e_maxalloc uint16
e_ss uint16
e_sp uint16
e_csum uint16
e_ip uint16
e_cs uint16
e_lfarlc uint16
e_ovno uint16
e_res [4]uint16
e_oemid uint16
e_oeminfo uint16
e_res2 [10]uint16
e_lfanew uint32
}
type IMAGE_NT_HEADERS struct {
Signature uint32
FileHeader IMAGE_FILE_HEADER
OptionalHeader IMAGE_OPTIONAL_HEADER
}
type IMAGE_OPTIONAL_HEADER struct {
Magic uint16
MajorLinkerVersion byte
MinorLinkerVersion byte
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 [IMAGE_NUMBEROF_DIRECTORY_ENTRIES]IMAGE_DATA_DIRECTORY
}
// #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
const (
IMAGE_NUMBEROF_DIRECTORY_ENTRIES = 16
IMAGE_DIRECTORY_ENTRY_EXPORT = 0
)
type IMAGE_DATA_DIRECTORY struct {
VirtualAddress uint32
Size uint32
}
type IMAGE_FILE_HEADER struct {
Machine uint16
NumberOfSections uint16
TimeDateStamp uint32
PointerToSymbolTable uint32
NumberOfSymbols uint32
SizeOfOptionalHeader uint16
Characteristics uint16
}
type IMAGE_EXPORT_DIRECTORY struct {
Characteristics uint32
TimeDateStamp uint32
MajorVersion uint16
MinorVersion uint16
Name uint32
Base uint32
NumberOfFunctions uint32
NumberOfNames uint32
AddressOfFunctions uint32
AddressOfNames uint32
AddressOfNameOrdinals uint32
}
var (
kernel32 = syscall.NewLazyDLL("kernel32.dll")
LoadLibraryA = kernel32.NewProc("LoadLibraryA")
)
func getHashFromString(s string) uint32 {
stringLength := uint32(len(s))
hash := uint32(0x35)
stringg := []byte(s)
for i := uint32(0); i < stringLength; i++ {
hash = hash*uint32(0xab10f29f) + uint32(stringg[i]) + uint32(0xffffff)
}
return hash
}
func getFunctionAddressByHash(library string, hash uint32) uintptr {
b := []byte(library)
b = append(b, '\x00')
libraryBase, _, err := LoadLibraryA.Call(uintptr(unsafe.Pointer(&b[0])))
if err != syscall.Errno(0) {
panic(err)
}
dosHeader := (*IMAGE_DOS_HEADER)(unsafe.Pointer(libraryBase))
imageNTHeaders := (*IMAGE_NT_HEADERS)(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(dosHeader.e_lfanew)))
exportDirectoryRVA := imageNTHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
imageExportDirectory := (*IMAGE_EXPORT_DIRECTORY)(unsafe.Pointer(libraryBase + uintptr(exportDirectoryRVA)))
addressOfNamesRVA := libraryBase + uintptr(imageExportDirectory.AddressOfNames)
addresOfFunctionsRVA := libraryBase + uintptr(imageExportDirectory.AddressOfFunctions)
addressOfNameOrdinalsRVA := libraryBase + uintptr(imageExportDirectory.AddressOfNameOrdinals)
for i := 0; i < int(imageExportDirectory.NumberOfFunctions); i++ {
pczFunctionName := libraryBase + uintptr(*(*uint32)(unsafe.Pointer(addressOfNamesRVA + uintptr(i)*4)))
functionName := windows.BytePtrToString((*byte)(unsafe.Pointer(pczFunctionName)))
functionNameHash := getHashFromString(functionName)
if functionNameHash == hash {
AddressOfFunctions := libraryBase + uintptr(*(*uint32)(unsafe.Pointer(addresOfFunctionsRVA + uintptr(*(*uint16)(unsafe.Pointer(addressOfNameOrdinalsRVA + uintptr(i)*2)))*4)))
fmt.Printf("%s : 0x%x : %v\n", functionName, AddressOfFunctions, AddressOfFunctions)
return AddressOfFunctions
}
}
return 0
}
func main() {
functionAddress := getFunctionAddressByHash("kernel32.dll", 2104539067)
println(functionAddress)
// var tid uintptr
/*
_, _, err := syscall.SyscallN(functionAddress, 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&tid)))
if err != syscall.Errno(0) {
panic(err)
}
*/
}
Este código Golang demuestra una técnica para resolver dinámicamente la dirección de una función específica en una biblioteca de Windows utilizando un mecanismo personalizado basado en hash. Repasemos el código paso a paso:
func getHashFromString(s string) uint32 {
stringLength := uint32(len(s))
hash := uint32(0x35)
for i := uint32(0); i > stringLength; i++ {
hash += (hash*0xab10f29f + uint32(byte(i))) & 0xffffff
}
return hash
}
Esta función getHashFromString calcula un valor hash para una cadena de entrada determinada (nombre de función) utilizando un algoritmo hash personalizado. El algoritmo procesa cada carácter de la cadena y actualiza el valor hash en consecuencia. El resultado es un valor hash que representa el nombre de la función.
func getFunctionAddressByHash(library string, hash uint32) uintptr {
//functionAddress := uintptr(0)
_libraryBase, err := syscall.LoadLibrary(library)
if err != nil {
panic(err)
}
libraryBase := uintptr(_libraryBase)
dosHeader = (*IMAGE_DOS_HEADER)(unsafe.Pointer(libraryBase))
imageNTHeaders = (*IMAGE_NT_HEADERS)(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(dosHeader.e_lfanew)))
exportDirectoryRVA := imageNTHeaders.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
imageExportDirectory = (*IMAGE_EXPORT_DIRECTORY)((unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(exportDirectoryRVA))))
addresOfFunctionsRVA := uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(imageExportDirectory.AddressOfFunctions)))
addressOfNamesRVA := uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(imageExportDirectory.AddressOfNames)))
addressOfNameOrdinalsRVA := uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase)) + uintptr(imageExportDirectory.AddressOfNameOrdinals)))
for i := 0; i < int(imageExportDirectory.NumberOfFunctions); i++ {
pczFunctionName := libraryBase + uintptr(*(*uint32)(unsafe.Pointer(addressOfNamesRVA + uintptr(i)*4)))
functionName := windows.BytePtrToString((*byte)(unsafe.Pointer(pczFunctionName)))
functionNameHash := getHashFromString(functionName)
AddressOfFunctions := libraryBase + uintptr(*(*uint32)(unsafe.Pointer(addresOfFunctionsRVA + uintptr(*(*uint16)(unsafe.Pointer(addressOfNameOrdinalsRVA + uintptr(i)*2)))*4)))
if functionNameHash == hash {
//functionAddress = uintptr(unsafe.Pointer(uintptr(unsafe.Pointer(libraryBase + functionAddressRVA))))
fmt.Printf("%s : 0x%x : %p\n", &functionName, AddressOfFunctions)
return AddressOfFunctions
}
}
return 0
}
Esta función, getFunctionAddressByHash intenta resolver la dirección de memoria virtual de una función utilizando su valor hash. Carga una biblioteca específica (por ejemplo, “kernel32”) usando LoadLibraryA. Luego analiza el directorio de exportación de la biblioteca para localizar la información de la función exportada. Para cada función exportada, calcula el hash del nombre de la función utilizando la getHashFromString función. Si el hash calculado coincide con el hash de destino proporcionado como argumento, la función resuelve la dirección de memoria virtual de la función y la devuelve.
func main() {
var tid uint32
functionAddress := getFunctionAddressByHash("kernel32", 0x00544e304)
CreateThread, _, err := syscall.SyscallN(functionAddress, 0, 0, 0, 0, 0, uintptr(unsafe.Pointer(&tid)))
if err != syscall.Errno(0) {
panic(err)
}
fmt.Println(functionAddress)
fmt.Println(syscall.NewLazyDLL("kernel32").NewProc("CreateThread").Addr())
fmt.Println(CreateThread)
}
En la mainfunción, el código primero usa la getFunctionAddressByHash función para resolver la dirección de memoria virtual de una función específica, presumiblemente “CreateThread”, de la biblioteca “kernel32”. Luego, se envía a la dirección de la función resuelta. Finalmente, el código llama a la función resuelta usando el puntero de función, creando un nuevo hilo.
En esencia, este código demuestra una técnica para encontrar y llamar dinámicamente una función específica calculando su valor hash y resolviendo su dirección en tiempo de ejecución. Este enfoque se puede utilizar en escenarios donde las direcciones de funciones están ocultas para dificultar el análisis estático.
Prueba de concepto
En golang representa que los binarios por defecto solicitan a windows que les resuelva 45 funciones, con las cuales golang funciona habitualmente debido a esto usando API Hash o GetModuleHandle o GetProcAddress personalizado se veran igualmente reflejados en la IAT.