· 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.

Share:
Back to Blog