· Asparux · malware · 7 min read

Protección de procesos con blockdlls

Introducción

¡Hola de nuevo hackers!

Hoy explicaremos cómo funciona ACG y qué podemos hacer para evitar que se inyecten DLL externas en nuestros procesos maliciosos.

Explicación

Algunos AV/EDR inyectan sus DLL en nuestros procesos para enganchar el área de usuario, entonces, ¿qué pasa si nuestro proceso malicioso no permite que se inyecten? Bueno, no engancharían al usuario y nuestras llamadas API maliciosas no serán detectadas.

En Windows todos los procesos y subprocesos tienen “políticas” y “atributos” , existen algunos tipos de ellos.

Échale un vistazo: Aquí

Por ejemplo, un atributo es el DEP. La Prevención de ejecución de datos es una tecnología integrada en Windows que ayuda a protegerlo contra el lanzamiento de código ejecutable desde lugares donde no debería hacerlo. DEP hace eso al marcar algunas áreas de la memoria de su PC como solo para datos, no se permitirá que ningún código ejecutable o aplicaciones se ejecuten desde esas áreas de memoria.

Otro atributo, llamado **PROCESS_CREATION_MITIGATION_POLICY_BLOCK_NON_MICROSOFT_BINARIES_ALWAYS_ON** , bloquea todas las DLL que no son firmadas por Microsoft, por lo que si intenta cargar una DLL personalizada en un proceso con este atributo, no se cargará.

Esta técnica se agregó a Cobalt Strike en 2019 con el comando “blockdlls” y luego @xpn investigó sobre eso y publicó sus fundamentos sobre cómo funcionaba, así que todos los créditos para él y su increíble trabajo.

También existe otra técnica para proteger nuestros procesos, utilizando el ACG Guard. @xpn también escribió sobre esto en la misma publicación. Es una medida de seguridad que se proporciona para evitar que el código asigne y/o modifique páginas ejecutables de memoria, a menudo necesaria para introducir código dinámico en un proceso. Entonces, si usamos esto con nuestro malware, los EDR no podrán asignar ni ejecutar páginas de memoria en nuestros procesos.

Veamos cómo podemos abordar algunas llamadas API y la lógica para hacerlo.

Código

package main

import (
  "fmt"
  "log"
  "time"
  "unsafe"

  "golang.org/x/sys/windows"
)

/*
typedef struct _PROCESS_MITIGATION_DYNAMIC_CODE_POLICY {
  union {
    DWORD Flags;
    struct {
      DWORD ProhibitDynamicCode : 1;
      DWORD AllowThreadOptOut : 1;
      DWORD AllowRemoteDowngrade : 1;
      DWORD AuditProhibitDynamicCode : 1;
      DWORD ReservedFlags : 28;
    } DUMMYSTRUCTNAME;
  } DUMMYUNIONNAME;
} PROCESS_MITIGATION_DYNAMIC_CODE_POLICY, *PPROCESS_MITIGATION_DYNAMIC_CODE_POLICY;
*/

type PROCESS_MITIGATION_DYNAMIC_CODE_POLICY struct {
  ProhibitDynamicCode   uint32
}

func main(){
  kernel32 := windows.NewLazyDLL("kernel32.dll")
  SetProcessMitigationPolicy := kernel32.NewProc("SetProcessMitigationPolicy")

  var ProcessDynamicCodePolicy int32 = 2
  var dcp PROCESS_MITIGATION_DYNAMIC_CODE_POLICY
  dcp.ProhibitDynamicCode = 1

  fmt.Println("Calling SetProcessMitigationPolicy...")
  ret, _, err := SetProcessMitigationPolicy.Call(
    uintptr(ProcessDynamicCodePolicy),
    uintptr(unsafe.Pointer(&dcp)),
    unsafe.Sizeof(dcp),
  )

  if ret != 1 {
    log.Fatal(err)
  }

  fmt.Println("ACG policy changed!")

  for {
    time.Sleep(1000 * time.Millisecond)
  }
}

Para ver información de procesos usaremos Process Hacker , en caso de que no lo tenga en su Windows, debe instalarlo ya que es realmente útil con todo este tipo de equipo rojo y desarrollo de malware.

Compilamos el código

GOARCH=amd64 GOOS=windows go build acg.go

Ahora lo transferimos y ejecutamos en una máquina con Windows.

Como puede ver, si hacemos clic derecho en nuestro proceso y luego hacemos clic en Propiedades, vemos las Políticas de mitigación. En este caso dice “Código dinámico prohibido” para que podamos confirmar que el proceso está protegido por ACG Guard.

Ahora vayamos con la parte principal, el bloque de DLL no firmado por Microsoft.

Este código se basa en otra publicación de ired.team ver aquí. A lo largo del código hay comentarios y referencias para entender mejor lo que estamos haciendo.

package main

import (
  "fmt"
  "log"
  "unsafe"
  "syscall"

  "golang.org/x/sys/windows"
)

/*
Se requieren estructuras y constantes
para interactuar con las llamadas de la API de Windows
y procesos
*/

const (
  PROC_THREAD_ATTRIBUTE_PARENT_PROCESS = 0x00020000
)

/*
typedef struct _STARTUPINFOEXA {
    STARTUPINFOA                  StartupInfo;
    LPPROC_THREAD_ATTRIBUTE_LIST  lpAttributeList;
} STARTUPINFOEXA, *LPSTARTUPINFOEXA;
*/

type StartupInfoEx struct {
  windows.StartupInfo
  AttributeList *PROC_THREAD_ATTRIBUTE_LIST
}

/*
typedef struct _PROC_THREAD_ATTRIBUTE_LIST
{
    DWORD                          dwFlags;
    ULONG                          Size;
    ULONG                          Count;
    ULONG                          Reserved;
    PULONG                         Unknown;
    PROC_THREAD_ATTRIBUTE_ENTRY    Entries[ANYSIZE_ARRAY];
} PROC_THREAD_ATTRIBUTE_LIST, *LPPROC_THREAD_ATTRIBUTE_LIST;
*/

type PROC_THREAD_ATTRIBUTE_LIST struct {
  dwFlags  uint32
  size     uint64
  count    uint64
  reserved uint64
  unknown  *uint64
  entries  []*PROC_THREAD_ATTRIBUTE_ENTRY
}

/*
typedef struct _PROC_THREAD_ATTRIBUTE_ENTRY
{
    DWORD_PTR   Attribute;  // PROC_THREAD_ATTRIBUTE_xxx
    SIZE_T      cbSize;
    PVOID       lpValue;
} PROC_THREAD_ATTRIBUTE_ENTRY, *LPPROC_THREAD_ATTRIBUTE_ENTRY;
*/

type PROC_THREAD_ATTRIBUTE_ENTRY struct {
  attribute *uint32
  cbSize    uintptr
  lpValue   uintptr
}

// funcion de https://github.com/D00MFist/Go4aRun/blob/master/pkg/sliversyscalls/syscalls/zsyscalls_windows.go
// Todos los creditos a D00MFist <3
func CreateProcess(appName *uint16, commandLine *uint16, procSecurity *windows.SecurityAttributes, threadSecurity *windows.SecurityAttributes, inheritHandles bool, creationFlags uint32, env *uint16, currentDir *uint16, startupInfo *StartupInfoEx, outProcInfo *windows.ProcessInformation) (err error) {
  var _p0 uint32
  if inheritHandles {
    _p0 = 1
  } else {
    _p0 = 0
  }

  procCreateProcessW := windows.NewLazyDLL("kernel32.dll").NewProc("CreateProcessW")

  r1, _, e1 := syscall.Syscall12(
    procCreateProcessW.Addr(),
    10,
    uintptr(unsafe.Pointer(appName)),         // lpApplicationName
    uintptr(unsafe.Pointer(commandLine)),     // lpCommandLine
    uintptr(unsafe.Pointer(procSecurity)),    // lpProcessAttributes
    uintptr(unsafe.Pointer(threadSecurity)),  // lpThreadAttributes
    uintptr(_p0),                             // bInheritHandles
    uintptr(creationFlags),                   // dwCreationFlags
    uintptr(unsafe.Pointer(env)),             // lpEnvironment
    uintptr(unsafe.Pointer(currentDir)),      // lpCurrentDirectory
    uintptr(unsafe.Pointer(startupInfo)),     // lpStartupInfo
    uintptr(unsafe.Pointer(outProcInfo)),     // lpProcessInformation
    0,
    0,
  )
  if r1 == 0 {
    if e1 != 0 {
      return e1
    }
  }

  return
}

func main(){
  fmt.Println("\n[+] Golang BlockDLLs")

  // Importamos las dll y llamadas a la API
  kernel32 := windows.NewLazyDLL("kernel32.dll")
  InitializeProcThreadAttributeList := kernel32.NewProc("InitializeProcThreadAttributeList")
  UpdateProcThreadAttribute := kernel32.NewProc("UpdateProcThreadAttribute")
  GetProcessHeap := kernel32.NewProc("GetProcessHeap")
  HeapAlloc := kernel32.NewProc("HeapAlloc")
  HeapFree := kernel32.NewProc("HeapFree")

  fmt.Println("[*] Llamando a InitializeProcThreadAttributeList...")
  lpSize := uintptr(0)
  InitializeProcThreadAttributeList.Call(
    0,                                // lpAttributeList
    2,                                // dwAttributeCount
    0,                                // dwFlags
    uintptr(unsafe.Pointer(&lpSize)), // lpSize
  )

  fmt.Println("[*] Llamando a GetProcessHeap...")
  procHeap, _, _ := GetProcessHeap.Call()

  fmt.Println("[*] Llamando a HeapAlloc...")
  attributeList, _, _ := HeapAlloc.Call(
    procHeap,
    0,
    lpSize,
  )
  defer HeapFree.Call(procHeap, 0, attributeList)

  var sInfo StartupInfoEx
  sInfo.AttributeList = (*PROC_THREAD_ATTRIBUTE_LIST)(unsafe.Pointer(attributeList))

  fmt.Println("[*] Llamando a InitializeProcThreadAttributeList...")
  InitializeProcThreadAttributeList.Call(
    uintptr(unsafe.Pointer(sInfo.AttributeList)), // lpAttributeList
    2,                                            // dwAttributeCount
    0,                                            // dwFlags
    uintptr(unsafe.Pointer(&lpSize)),             // lpSize
  )

  // PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY
  mitigate := 0x20007

  nonms := uintptr(0x100000000000|0x1000000000)

  fmt.Println("[*] Llamando a UpdateProcThreadAttribute...")
  UpdateProcThreadAttribute.Call(
    uintptr(unsafe.Pointer(sInfo.AttributeList)), // lpAttributeList
    0,                                            // dwFlags
    uintptr(mitigate),                            // Attribute
    uintptr(unsafe.Pointer(&nonms)),              // lpValue
    unsafe.Sizeof(nonms),                         // cbSize
    0,                                            // lpPreviousValue
    0,                                            // lpReturnSize
  )

  var si StartupInfoEx
  si.AttributeList = sInfo.AttributeList

  target := "C:\\Windows\\System32\\notepad.exe"
  commandLine, _ := syscall.UTF16PtrFromString(target)

  var pi windows.ProcessInformation
  si.Cb = uint32(unsafe.Sizeof(si))
  creationFlags := windows.EXTENDED_STARTUPINFO_PRESENT

  fmt.Println("[*] Llamando a CreateProcessW...")
  err := CreateProcess(
    nil,
    commandLine,
    nil,
    nil,
    true,
    uint32(creationFlags),
    nil,
    nil,
    &si,
    &pi,
  )
  if err != nil {
    log.Fatal(err)
  }

  fmt.Println("[+] Proceso creado!\n")
}

Este código combina las técnicas anteriores discutidas anteriormente para lograr mejores resultados, ¿cómo podemos verificar esto? Simplemente, usaremos Process Hacker nuevamente.

Como puede ver, notepad.exe se inició como se esperaba y las políticas de mitigación se aplicaron como se esperaba. También codifiquemos una DLL simple (también en Golang) que genera un cuadro de mensaje y luego intentaremos inyectarla en notepad.exe. Process Hacker también tiene una función incorporada para inyectar una DLL en un proceso, así que no lo hagas. Preocúpate por eso.

package main

import (
  "C"
  "log"
  "unsafe"
  "syscall"
)

//export main
func main(){
  ret, _, err := syscall.NewLazyDLL("user32.dll").NewProc("MessageBoxW").Call(
    uintptr(0),
    uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Hello! :)"))),
    uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("Example Box"))),
    uintptr(0),
  )

  if ret != 0 {
    log.Fatal(err)
  }
}

Luego, para convertir este código en una DLL, compila el archivo de esta manera:

GOARCH=amd64 GOOS=windows CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc go build -buildmode=c-shared -ldflags="-w -s -H=windowsgui" -o dll_to_inject.dll dll_to_inject.go

Manifestación

Compila el código principal si aún no lo has hecho

GOARCH=amd64 GOOS=windows go build blockdll.go

Primero que nada probamos si nuestra DLL MessageBox funciona

¡Perfecto!

Ahora ejecutemos el .exe nuevamente e inyectemos nuestra DLL (dll_a_inyectar.dll) con Process Hacker , hagamos clic derecho en el proceso notepad.exe, luego vayamos a “Varios” -> “Inyectar DLL” y seleccionemos la DLL.

No pasará nada, pero si vas a “Propiedades” -> “Módulos” verás que se cargaron las DLL oficiales de Microsoft, pero no otras DLL como la nuestra.

Entonces finalmente podemos confirmar que esta técnica funciona y que otras DLL no se cargan en los procesos con estas políticas de mitigación.

Espero que hayas aprendido muchas cosas, como ACG Guard y las políticas de mitigación pueden ayudarnos a evadir las medidas de seguridad y cómo podemos crear archivos DLL maliciosos desde Golang.

Un saludo, ASPARUX.

Back to Blog