1. Function Call Obfustacion

1.1 Cual es el problema que intenta resolver el autor?

En la primera parte, el autor habia presentado un programa en C que utilizaba varias funciones de la Windows API.

  • VirtualAlloc

  • RtlMoveMemory

  • CreateThread

  • WaitForSingleObject

Estas funciones tiene sus legitimos, pero determinadas combinaciones pueden resultar sospechosas durante un analisis de malware.

Cuando se compila un ejecutable de Windows, algunas de las funciones importadas pueden aparecer en la Import Address Table (IAT).

Esto no significa que tener VirtualAlloc en la IAT convierta automaticamente un programa en malware. Lo que importa es el contexto y el comportamiento.

1.2 Que propone el autor?

En lugar de utilizar directamente el nombre de una funcion, explica el concepto de resolucion dinamica de funciones.

Introduce dos APIs de Windows:

GetModuleHandle

Permite obtener una referencia (handle) a un modulo que ya esta cargado dentro del proceso.

GetProcAddress

Permite localizar la direccion de una funcion exportada por ese modulo.

Por ejemplo kernel32.dll es una biblioteca de Windows que proporciona numerosas funciones relacionadas con procesos, memoria y otros servicios del sistema.

La idea tecnica es que un programa puede obtener una direccion de funcion durante su ejecucion, en lugar de depender de una importacion convencional de esa funcion.

Aqui se conbina este concepto con XOR para transformar los nombres de las funciones.

1.3 Antes de continuar: que es un puntero a funcion?

Este concepto es indispensable para entender el codigo del articulo.

En C, normalmente llamamos a una funcion por su nombre:

#include <stdio.h>
 
void saludar(void) {
 printf("hi soon"\n);
}
 
int main(void) {
 saludar();
 return 0;
}

Pero una funcion tambien tiene una direccion en memoria. Podemos almacenar esa direccion en un puntero a funcion:

#include <stdio.h>
 
void saludar(void) {
 printf("Hi soon"\n);
}
 
int main(void) {
 void (*miFuncion)(void) = saludar;
 
 miFuncion();
 
 return 0;
}

Observemos esta linea:

void (*miFuncion)(void) = saludar;

La descomponemos:

ElementoSignificado
void inicialLa funcion no devuelve ningun valor.
(*miFuncion)Declara un puntero a funcion.
(void) finalLa funcion no recibe argumentos.
= saludarAsigna la direccion de la funcion saludar.
Por lo tanto, saludar() y miFuncion() terminan invocando la misma funcion.

La diferencia importante: miFuncion es una variable que almacena una direccion de codigo ejecutable, no una copia del codigo de saludar.

1.4 Relacion con el articulo

Ahora podemos entender por que el autorr presenta una declaracion como esta:

LPVOID (WINAPI *pVirtualAlloc)(
    LPVOID lpAddress,
    SIZE_T dwSize,
    DWORD flAllocationType,
    DWORD flProtect
);

Es un puntero a una funcion con una firma compatible con VirtualAlloc.

Aqui aparecen conceptos nuevos:

  • LPVOID: puntero genérico.

  • SIZE_T: tipo utilizado para tamaños.

  • DWORD: entero sin signo de 32 bits en Windows.

  • WINAPI: especificador de convención de llamada.

  • pVirtualAlloc: nombre del puntero a función.

Hasta aqui, que debemos comprender?

La diferencia fundamental es esta:

Llamada directa: el programa referencia una funcion mediante su nombre declarado.

Llamada mediante puntero: el programa dispone de una variable que contiene la direccion de la funcion y la invoca a traves de ella.

  1. Simple XOR Function Encryptor

2.1 Que intenta hacer el autor?

Recordemos que una importacion como VirtualAllocpuede apareceer en la tabla de importaciones de un ejecutable PE.

Lo que se busca es dificultar la identificacion estatica de determinadas funciones mediante dos ideas:

  • Representar el nombre de la funcion como bytes transformados mediante XOR.
  • Recuperar el nombre original durante la ejecucion para realizar una resolucion dinamica.

Importante: esto no elimina la capacidad del EDR de observar las llamadas ni garantiza evasion. Solo modifica como aparece representado el nombre en el programa.

2.2 Como funciona XOR aplicado a caracteres?

XOR es una operacion bit a bit que devuelve 1 cuando los dos bits comparados son diferentes.

ABA XOR B
000
011
101
110
En este caso se usa como ejemplo el nombre de VirtualAlloc y una clave 0x01.

Su valor ASCII hexadecimal es 0x56.

El resultado 0x57 corresponde a la letra W en ASCII.

Tenemos en cuenta que la propiedad fundamental de XOR permite invertir la operacion:

Es decir, aplicar XOR dos veces con la misma clave recupera el valor original.

2.3 Por que esta tecnica tiene limitaciones?

Una cadena transformada mediante XOR puede dejar de aparecer como texto legiblee en una busqueda sencilla de strings, pero:

  • La transformacion es reversible.
  • Una clave fija y pequenia se puede analizar con facilidad.
  • El nombre original vuelve a existir en memoria cuando el programa lo recupera.
  • Las operaciones posteriores siguen siendo observables por herramientas de seguridad.

2.4 Relacion con GetProcAddress

GetProcAddress recibe, entro otros argumentos, el nombre de la funcion exportada que se desea localizar.

El detalle importante es que necesita conocer el nombre real de esa funcion, no una representacion transformada mediante XOR.

Por lo tanto, conceptualmente existen tres momentos:

**

Y esto permite entender una distincion:

XOR transforma datos. GetProcAdress resuelve direcciones de funciones. Son operaciones completamente diferentes, aunque el autor un poco las combina en su explicacion.

  1. Windows API, MSDN y resolucion dinamica de funciones

Ya debemos comprender dos conceptos:

  • Los punteros a funciones permite invocar una funcion a traves de su direccion de memoria.
  • XOR permite transformar una cadena de caracteres y recuperar su contenido original.

3.1 Primero: que es una DLL en Windows?

Una DLL (Dynamic LInk Library) es un archivo que contiene codigo y otros recursos que pueden utilizar diferentes programas.

Por ejemplo, Windows dispone de bibliotecas como:

DLLFuncionalidad
kernel32.dllProcesos, memoria, hilos y otras funciones del sistema.
user32.dllVentanas, interaccion con el usuario y mensajes.
advapi32.dllRegistro de Windows y determinadas funciones de seguridad.
ntdll.dllInterfaces de bajo nivel de Windows, incluyendo funciones nativas.
Imagina que desarrollas un programa en C y necesitas consultar identificador de tu proceso.

No necesitas programar desde cero como es que Windows obtiene esa informacion. PUedes utilizar la funcion GetCurrentProcessId, exportada por kernel32.dll.

3.2 Que significa que una DLL este cargada en memoria?

Cuando Windows carga una DLL dentro de un proceso, sus secciones se mapean en el espacio de direcciones virtuales de ese proceso.

Esquema simplicado: el orden y las direcciones reales varian por proceso y por ASLR.

Una DLL no se carga necesariamente en todos los procesos. Cada proceso tiene su propio espacio de direcciones virtuales.

3.3 Que hace GetModuleHandle?

La funcion GetModuleHandle obtiene un identificador de un modulo que ya esta cargado en el proceso actual.

Su prototipo simplificado es:

HMODULE GetModuleHandleA(
    LPCSTR lpModuleName
);

Desglosemos:

  • HMODULE: identificador de un modulo cargado.
  • LPCSTR: puntero a una cadena de caracteres constante.
  • lpModuleName: nombre del modulo que queremos localizar.
    Por ejemplo, en un programa legitimo podemos consultar si kernel32.dll esta cargada:
#include <windows.h>
#include <stdio.h>
 
int main(void) {
 HMODULE modulo = GetModuleHandleA("kernel.dll");
 
 if (modulo != NULL) {
  printf("Modulo encontrado: %p\n", (void *)modulo)
 }
 return 0;
}

Aqui lo quue se hace es imprimir una direccion de memoria: la direccion base donde esta cargada kernel32.dll en tu proceso.

Siempre hay que convertir HMODULE a void * para imprimirlo? Si usas %p, si, porque %p exige un void * La regla vale para cualquier puntero que imprimas con %p (int *, char *, HANDLE, …):

int x = 5;
int *p = &x;
printf("%p\n", (void *)p); // mismo caso
 
HMODULE m = GetModuleHandleA("kernel32.dll");
printf("%p\n", (void *)m); // correcto

Aqui no se esta cargando una DLL nueva: simplemente se consulta una que ya existe en el proceso.

3.4 Que hace GetProcAddress?

Supongamos que ya tenemos el modulo kernel32.dll.

Ahora queremos encontrar donde esta implementada una funcion determianda.

Para ellos existe GetProcAddress.

FARPROC GetProcAddress(
    HMODULE hModule,
    LPCSTR lpProcName
);

Sus argumentos son:

ArgumentoFuncion
hModuleIdentifica el modulo donde buscar.
lpProcNameIndica el nombre de la funcion exportada.
Retorno FARPROCDireccion de la funcion o NULL si no se encuentra.
Ejemplo practico y seguro.

Vamos a resolver dinamicamente GetCurrentProcessId, una funcion que unicamente devuelve el PID del proceso actual.

#include <windows.h>
#include <stdio.h>
 
typedef DWORD (WINAPI *FnGetPID)(void);
 
int main(void) {
 HMODULE modulo = GetModuleHandleA("kernel32.dll");
 
 if (modulo == NULL) {
  return 1;
 }
 
 FnGetPID obtenerPID = (FnGetPID) GetProcAddress(modulo, "GetCurrentProcessId");
 
 if (obtenerPID == NULL) {
  return 1; 
 }
 
 printf("PID actual: %lu\n", obtenerPID());
 
 return 0;
}

El typedef :

ParteSignificado
typedef”Voy a crear un nombre nuevo para un tipo.”
DWORDLa funcion devuelve un DWORD (entero sin signo de 32 bits).
WINAPIConvencion de llamada (__stdcall), la que usan las funciones de la API de Windows.
*FnGetPIDFnGetPID es un puntero (el * es lo que lo hace puntero).
(void)La funcion no recibe parametros.
Se lee asi: “FnGetPID es un tipo: puntero a una funcion WINAPI que no recibe nada y devuelve DWORD”.

Observemos especialmente esta linea:

FnGetPID obtenerPID = (FnGetPID) GetProcAddress(modulo, "GetCurrentProcessId");

Estamos obteniendo una direccion y almacenandola en un puntero a funcion cuyo tipo ya definimos mediante typedef.

Despues:

obtenerPID();

Invoca la funcion a traves de ese puntero.

3.5 Como se relaciona con el articulo?

Se uriliza el mismo concepto, pero aplicado a VirtualAlloc.

Se muestra esta linea:

pVirtualAlloc = GetProcAddress(
    GetModuleHandle("kernel32.dll"),
    x
);

En la explicacion, x representa el nombre de la funcion despues de recuperar su valor original mendiante XOR.

La logica conceptual:

3.6 Y que relacion tiene esto con la IAT?

Importacion convencional: el ejecutable declara dependencias que el cargador de Windows ayuda a resolver durante la carga del programa.

Resolucion dinamica: el propio programa solicita una direccion de funcion durante la ejecucion.

Esto puede cambiar que funciones aparecen explicitamente en la tabla de importaciones, pero no significa que el comportamineto resulta invisible. Un EDR puede utilizar otras fuentes de telemetria para detectar operaciones sospechosas.

Si GetProcAddress devuelve una direccion de memoria. que necesitamos para llamar correctamente a esa funcion desde C?

Necesitamos utilizar un puntero a funcion con una firma compatible.
La dirección se utiliza mediante un puntero cuyo tipo debe coincidir con el valor de retorno, los parámetros y la convención de llamada de la función.

  1. Sandbox Evasion

4.1 Que es una sandbox?

Una sandbox es un entorno aislado donde podemos ejecutar un programa sospechoso y observar su comportamiento sin exponer directamente el sistema principal.

Por ejemplo, imagina que un analista recibe un archivo llamado suspicious.exe.

En lugar de ejecutarlo en su computadora personal, puede utilizar una maquina virtual preparada para analisis.

Herramientas como ANY.RUN y CAPE Sandbox permiten observar parte de ese comportamiento.

4.2 Que es Sandbox Evasion?

Consiste en que un programa intente determinar si se esta ejecutando dentro de un entorno de analisis y modifique su comportamiento dependiendo del resultado.

Imagina este escenario:

  • Computadora convencional: el programa continua con su comportamiento habitual.
  • Entorno de analisis: el programa detecta ciertas caracteristicas y termina anticipadamente.

De esta forma, el analista podria observar menos actividad de la que se produciria y termina anticipadamente.

Sin embargo, estar dentro de una maquina virtual no implica necesariamente estar dentro de una sandbox, y detectar una VM no equivale a demostrar que existe un analista observando el programa.

4.3 Primera tecnica del articulo: comprobacion del registro de Windows.

El autor presenta un programa que utiliza dos funciones:

  • RegOpenKeyEx
  • RegQueryValueEx

Antes de entender su codigo, debemos conocer el registro de Windows.

Que es el Windows Registry?

Es una base de datos jerarquica donde Windows y numerosas aplicaciones almacenan configuraciones.

Por ejemplo:

HKEY_LOCAL_MACHINE
  └── SOFTWARE
       └── Microsoft
            └── Windows
                 └── CurrentVersion

Las rutas dentro del registro se llaman claves y pueden contener valores, cada uno con un nombre, tipo y contenido.

Que hace RegOpenKeyEx?

Abre una clave existente del registro y proporciona un identificador para consultarla.

Por ejemplo:

RegOpenKeyEx(
    HKEY_LOCAL_MACHINE,
    _T("SOFTWARE\\Microsoft\\Windows\\CurrentVersion"),
    0,
    KEY_READ,
    &hKey
);

Desglose de los argumentos:

ArgumentoSignificado
HKEY_LOCAL_MACHINERaiz del registro que contiene configuraciones del equipo.
Ruta de la claveUbicacion que se desea consultar.
0Parametro reservado
KEY_READSolicita permisos de lectura.
&hKeyLugar donde se almacenara el identificador obtenido.
La funcion devuelve ERROR_SUCCESS si la operacion se realiza correctamente.

Que hace RegQueryValueEx?

Permite consultar un valor almacenado dentro de una clave abierta.

En el articulo aparece una consulta a ProductId:

RegQueryValueEx(
    hKey,
    _T("ProductId"),
    NULL,
    &dwType,
    (LPBYTE)&dwData,
    &dwDataSize
);

Esta funcion intenta leer el contenido del valor especificado.

Aqui encontramos un detalle importante del articulo: el codigo no comprueba directamente si Windows esta virtualizado.

Lo que realmente hace es intentar abrir una clave y consultar su valor. Si alguna de esas operaciones falla, termina el programa.

4.4 Segunda tecnica: inspeccion del escritorio

Se plantea otra idea: algunos entornos automatizados tiene perfiles de usuario muy vacios.

Su hipotesis es que una computadora real suele contener archivos en Desktop o Downloads, mientras que determinadas sandboxes pueden carecer de ellos.

El ejemplo utiliza estas funciones de Windows:

FuncionProposito
SHGetFolderPathObtener la ruta de una carpeta espeecial del usuario.
FindFirsttFileIniciar una busqueda de archivos.
FindNextFileContinuar enumerando resultados.
FindCloseLiberar el identificador de busqueda.
Cual es la logica?

  1. Anti-Debugging

5.1 Que es un debugger?

Un debugger o depurador es una herramienta que permite inspeccionar y controlar la ejecucion de un programa.

Por ejemplo, con x64dbg puedes:

  • Ejecutar un programa instruccion por instruccion.
  • Examinar registros de la CPU, como RAX, RBX, RCX y RIP.
  • Inspeccionar regiones de memoria.
  • Establecer breakpoints para detener temporalmente la ejecucion.
  • Observar los argumentos y resultados de llamadas a funciones.

Imagina un ejecutable que realiza tres operaciones:

int main(void) {
    int a = 10;
    int b = 20;
    int resultado = a + b;
 
    return 0;
}

Normalmente, el programa se ejecuta de principio a fin.

Con un debugger, puedes detenerlo antes de la suma y observar los valores de a, b y resultado.

Eso resulta particularmente util para analizar programa cuando no dispones de su codigo fuente.

5.2 Que es Anti-Debuggin?

En el articulo se define como un conjunto de tecnicas utilizadas para dificultar la depuracion y el analisis de un programa.

Puede utilizarse con propositos diferentes:

  • Software legitimo: dificultar la ingenieria inversa o proteger determinada logica.
  • Malware: intentar detectar el analisis y modificar su comportamiento para dificultar la investigacion.
    La idea central es que el programa consulte ciertas condiciones relacionadas con el entorno de depuracion.

5.3 Que tecnica utiliza el articulo?

Se presenta una comprobacion sencilla mediante la API de Windows:

IsDebuggerPresent()

Esta funcion permite comprobar si el proceso que la invoca esta siendo depurado por un depurador de modo usuario.

El fragmento del articulo es:

// check if a debugger is attached
if (IsDebuggerPresent()) {
 printf("Debugger is attached, exiting the process....\n");
 ExitProcess(0);
}
else {
 printf("Debugger is not attached, continuing....\n");
}

5.4 Como funciona internamente IsDebuggerPresent?

Windows mantiene una estructtura denominada Process Environment Block (PEB), asociada a cada proceso.

Esta estructura almacena informacion sobre el entorno del proceso y contiene un campo llamado BeingDebugged.

5.5 Ejemplo practico en C

#include <windows.h>
#include <stdio.h>
 
int main(void) {
 BOOL debuggerDetectado = IsDebuggerPresent();
 
 if (debuggerDetectado) {
  printf("Debugger detectado\n");
 }
 else {
  printf("No se detecto debugger\n");
 }
 printf("Programa finalizado normalmente\n");
 
 return 0;
}

Si ejecutas un programa y IsDebuggerPresent() devuelve FALSE, ¿qué puedes concluir?

  • La comprobación no ha detectado un depurador en el proceso actual.
  • IsDebuggerPresent() no permite determinar si hay un EDR, una sandbox u otras técnicas de instrumentación observando el proceso.
  1. UUID Shellcode Obfuscation

6.1 Que es un UUID?

UUID significa Universally Unique Identifier, es decir, Identificador Unico Universal.

Es un identificador de 128 bits (16 bytes) que normalmente se representa como una cadena hexadecimal de 36 caracteres, contando los guiones.

Por ejemplo:

Cada caracter hexadecimal representa 4 bits:

Como un byte contiene 8 bits:

Por eso un UUID puede representar exactamente 16 bytes.

Este es el fundamento matematico de la tecnica que explica el autor.

6.2 Que relacion tiene un UUID con el shellcode?

Recordemos que un shellcode es una secuencia de instrucciones de maquina representadas mediante bytes.

Por ejemplo, una secuencia arbitraria de 16 bytes:

31 C0 50 68 2F 2F 73 68
68 2F 62 69 6E 89 E3 50

El punto importante es que los datos binarios tambien pueden expresarse mediante otra representacion textual.

Debes distinguir dos operaciones:

  • Codificacion: transforma la representacion de informacion sin necesitar una clave secreta.
  • Cifrado: utiliza una transformacion criptografica para proteger el contenido.

Convertir bytes a formato UUID es una codificacion, no un cifrado.

6.3 Que propone exactamente el autor?

En el articulo,, proponer utilizar dos operaciones:

  1. Aplicar XOR a los bytes originales.
  2. Convertir el resultado al formato UUID.

El ejemplo utiliza python y comienza importando la biblioteca uuid.

import uuid
 
key = 0x23
 
encrypted_shellcode = bytes([
    b ^ key for b in shellcode
])
 
uuid_obj = uuid.UUID(bytes=encrypted_shellcode)
 
print(str(uuid_obj))

Ejemplo mas especifico:

import uuid
import sys
 
# Define the shellcode to encrypt
#shellcode = b'\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80'
shellcode = b"ABCDEFGHIJKLMNOP"
 
# Define the encryption key
key = 0x23
 
# Encrypt the shellcode using XOR
encrypted_shellcode = bytes([b ^ key for b in shellcode])
 
# Generate a UUID from the encrypted shellcode
# La funcion uuid.UUID() exige exactamente 16 bytes, pero el shellcode tiene 25 bytes
# asi que obtendremos: ValueError: bytes is not a 16-char string
uuid_obj = uuid.UUID(bytes=encrypted_shellcode)
# Si queremos represenntar todos los bytes cifrados mediante UUID, debes dividirlos
# en bloques de 16 bytes y definir como tratar el ultimo bloque incompleto.
 
with open("salida_uuid.txt", "w", encoding="utf-8") as f:
 sys.stdout = f
 # Print the UUID in string format
 print(str(uuid_obj)) 
 sys.stdout = sys.__stdout__

Salida:

62616067-6665-646b-6a69-686f6e6d6c73

Si tenemos 48 bytes de información y queremos representarlos completamente mediante UUID, ¿cuántos necesitamos?

  • 3 UUID
  • Cada UUID representa 16 bytes. Por tanto, 48 ÷ 16 = 3 UUID.
  1. Process Injection in C

Se presenta un Process Injection, utilizando Microsoft Edge (msedge.exe) como proceso de destino.

7.1 Que es un proceso en Windows?

Un proceso es una instancia de un programa en ejecucion.

Por ejemplo, cuando abre Microsoft Edge, WIndows puede crear uno o varios procesos asociados al navegador.

Cada proceso tiene recursos propios, como:

  • Un identificador denominado PID (Process ID).
  • Un espacio de direcciones virtuales.
  • Uno o varios hilos de ejecucion (threads).
  • Handles a objetos del sistema.
  • Informacion de seguridad y permisos.

Un aspecto importante es que un programa y un proceso no son los mimo.

msedge.exe es un archivo ejecutable. Cuando Windows ejecuta ese archivo, crea un proceso con su propio estado.

Incluso puedes tener varios procesos originados a partir del mismo archivo ejecutable.