Aldair Maihuiri

Análisis de Malware - LockBit Ransomware

Sábado 30 de Mayo del 2026

Autor: Gino Aldair Maihuiri Romero
Fecha del análisis: 30 de mayo de 2026
Tipo de muestra: LockBit ransomware (PE32 / 32 bits)
Herramientas principales: Detect It Easy (DIE), Ghidra, Python 3
Etiquetas: malware-analysis reverse-engineering lockbit ghidra obfuscation stack-strings dynamic-api-resolution


© 2026 Gino Aldair Maihuiri Romero. Todos los derechos reservados.
Este documento constituye una publicación técnica original del autor. Se permite enlazar, citar fragmentos breves y compartir la publicación con la debida atribución. La reproducción total o de partes sustanciales del contenido sin autorización previa del autor está prohibida.

Nota de alcance: Este writeup se publica en el estado exacto en que quedó el proceso de investigación. Documenta el análisis estático y la ingeniería inversa desarrollados hasta el inicio del estudio de mw_resolve_api; no pretende constituir un análisis completo de todas las capacidades de LockBit.


Introducción

Este documento presenta el análisis estático del ransomware LockBit, una de las amenazas más lucrativas del cibercrimen global debido al enorme impacto financiero de sus campañas. Actualmente, este grupo destaca por implementar la táctica de triple extorsión, un comportamiento ampliamente documentado y respaldado por agencias internacionales de inteligencia y ciberseguridad, entre las que se incluyen la CISA y el FBI de Estados Unidos, el ACSC de Australia y el NCSC del Reino Unido. El propósito fundamental de esta investigación es desglosar la mecánica del ataque y evaluar el nivel de sofisticación técnica de sus desarrolladores. Comprender sus capacidades y metodologías operativas permite dimensionar con precisión la magnitud de la amenaza, facilitando así la identificación de sus vectores de ejecución y el fortalecimiento de las capacidades de respuesta adaptativa. El presente análisis se desarrollará bajo una estructura y metodología de autoría propia, adoptando un enfoque independiente que prescinde de modelos convencionales o plantillas preestablecidas por otros investigadores del sector.

Análisis con DIEC El triaje preliminar mediante el análisis estático se ejecutó utilizando Detect It Easy (diec). Aunque esta verificación estructural es perfectamente factible dentro del propio entorno de Ghidra, la inspección previa con un identificador de firmas especializado aporta una perspectiva analítica externa crucial antes de proceder con el desensamblado. Los primeros vectores de información revelados por la herramienta arrojan resultados altamente favorables para el avance de la investigación, descartando la presencia de un empaquetador comercial o de capas externas complejas sobre la estructura global del binario.

Los metadatos de telemetría extraídos confirman el uso de herramientas nativas de Windows para entornos de desarrollo de 2017:

Arquitectura: PE32 (Ejecutable portable de 32 bits)

Linker: Microsoft Linker (14.16.27048)

Compiler: Microsoft Visual C/C++ (2017, 15.5-6, by EP)

Tool: Microsoft Visual Studio (2017, 15.9)

Debug data: Records[pogo]

La preservación de estos artefactos originales verifica de manera anticipada que la muestra prescinde de protecciones comerciales genéricas como UPX o Themida. Este factor optimiza significativamente el flujo de trabajo, garantizando que la posterior reconstrucción de la Tabla de Importaciones (IAT) en Ghidra sea directa y transparente para la identificación de las APIs del sistema operativo.

El hallazgo más crítico dentro de este bloque de metadatos reside en el registro de depuración Records[pogo]. Este indicador expone el empleo de la técnica Profile-Guided Optimization (Optimización Guiada por Perfil), un factor que evidencia la sofisticación y el alto nivel técnico de los desarrolladores de LockBit. La adopción de metodologías de optimización industrial denota que el código fue entrenado y reestructurado previamente en entornos de prueba para maximizar la velocidad de ejecución en el procesador. El objetivo subyacente es claro: dotar al ransomware de una tasa de transferencia de cifrado masivo excepcionalmente veloz, minimizando el margen de respuesta de los sistemas de detección locales desde el instante de su ejecución.

Al profundizar en la distribución de la muestra con el cálculo de entropía por secciones, se observa una asimetría técnica de gran interés. De entrada, el archivo figura globalmente bajo la etiqueta de “not packed” (no empaquetado). Sin embargo, esto constituye una falsa sensación de seguridad para firmas automatizadas: mientras la estructura externa mantiene valores normales, la lógica real y el núcleo ejecutable del malware sí se encuentran empaquetados de manera interna e intencional. El desglose pormenorizado revela esta distribución matemática:

Total: 6.45735 (not packed)

0 Header: 0 1024 2.60819 (not packed)
1 Section (1) [“.text”]: 1024 181760 6.50314 (packed)
2 Section (2) [“.rdata”]: 182784 25600 4.84726 (not packed)
3 Section (3) [“.data”]: 208384 23040 5.23358 (not packed)
4 Section (4) [“.reloc”]: 231424 5120 6.29066 (not packed)

El punto crítico radica en la sección .text (bloque físico donde reside el código ejecutable primario), la cual registra una entropía de 6.50314, catalogándola explícitamente como protegida u ofuscada (packed). Esta configuración demuestra que el malware camufla su firma global manteniendo las secciones auxiliares (.rdata, .data y .reloc) y las cabeceras intactas para evadir alarmas genéricas, pero blinda el código ejecutable real.

Esta segmentación delimita con claridad el área de interés analítico, permitiendo concentrar los recursos de ingeniería inversa en una región de memoria específica. No obstante, bajo la rigurosidad de una metodología basada en el principio de Zero Trust (Confianza Cero), el análisis no se limitará de forma exclusiva a la sección de código. Se mantendrá un escrutinio exhaustivo sobre los encabezados y las secciones de datos complementarias, reconociendo que los desarrolladores de amenazas avanzadas suelen instrumentalizar anomalías estructurales sutiles en zonas aparentemente legítimas para evadir las heurísticas de seguridad convencionales.

Análisis de strings

Análisis de Metadatos y Artefactos de Compilación (Líneas 0-80) La inspección del primer bloque analítico expone un conjunto de cadenas características del entorno de desarrollo, destacando directivas de llamada y estructuras como __cdecl, vftable, vbtable, Class Hierarchy Descriptor y artefactos de RTTI (Run-Time Type Information). La preservación de estos elementos confirma de manera inequívoca que el espécimen fue desarrollado bajo el paradigma de programación orientada a objetos en lenguaje C++. El hecho de que los metadatos de depuración y la información de tipos en tiempo de ejecución no hayan sido eliminados (unstripped) representa una ventaja táctica significativa para la fase de ingeniería inversa en Ghidra. Este factor facilitará la reconstrucción precisa de clases, la definición de estructuras de funciones complejas y el mapeo directo de los bloques internos para el manejo de excepciones.

Interacción con el Subsistema y Vectores Potenciales de la API (Líneas 91-110) El segmento comprendido entre las líneas 91 y 110 evidencia la preparación del binario para interactuar con los componentes nativos del sistema operativo a través de API Sets (api-ms-win-core… y ext-ms-win). A nivel de librerías dinámicas estándar, destaca la presencia de kernel32.dll, subsistema crítico que proporciona las capacidades necesarias para la gestión de memoria, manipulación del sistema de archivos y control de procesos. En un escenario de ejecución maliciosa, este componente permitiría al ransomware realizar técnicas de inyección de código en procesos legítimos mediante el uso de APIs como VirtualAllocEx y WriteProcessMemory, o implementar mecanismos de antianálisis mediante IsDebuggerPresent.

Asimismo, la inclusión de advapi32.dll faculta al vector para interactuar con el Registro de Windows y el Administrador de Control de Servicios (SCM). Esto le permite consolidar su persistencia modificando las claves de ejecución automática (Run o RunOnce) mediante RegSetValueEx, instanciar servicios persistentes con CreateService bajo privilegios elevados de SYSTEM, o alterar tokens de seguridad con AdjustTokenPrivileges para evadir el Control de Cuentas de Usuario (UAC). Por último, la vinculación de user32.dll sugiere capacidades para interactuar con la interfaz gráfica o la enumeración de componentes de ventana, una aptitud frecuentemente instrumentalizada mediante funciones como FindWindow para detectar y evadir entornos de depuración o herramientas de análisis activas.

Restricciones de Entorno y Configuración Regional (Líneas 87-90) Entre las líneas 87 y 90 se identifican de manera explícita cadenas asociadas a configuraciones regionales específicas (locales), tales como ja-JP (Japón), zh-CN (China), ko-KR (Corea del Sur) y zh-TW (Taiwán). Estos identificadores apuntan a la implementación de rutinas de verificación de entorno basadas en la distribución del teclado o en la configuración idiomática del sistema operativo de la víctima. Dependiendo de las directrices lógicas programadas por los operadores de LockBit, el hallazgo de estas firmas determina el flujo del ransomware; esto puede derivar en una rutina de autodestrucción inmediata para prevenir la infección en regiones geopolíticas restringidas, o en la carga de módulos de configuración y notas de rescate adaptadas sintácticamente a la arquitectura del entorno asiático afectado.

Arquitectura de Concurrencia, Fibras y Sincronización (Líneas 14-18) Las líneas 14 a 18 revelan la importación de primitivas como FlsAlloc, FlsFree, FlsGetValue y InitializeCriticalSectionEx, las cuales confirman el despliegue de una arquitectura multihilo altamente optimizada basada en el uso de fibras (fibers) y secciones críticas. En el contexto de un ransomware avanzado, el subsistema de fibras otorga un control granular sobre la planificación de tareas sin depender exclusivamente del planificador del kernel de Windows, optimizando el rendimiento de la CPU al máximo.

Por su parte, las secciones críticas actúan como mecanismos de exclusión mutua para proteger recursos compartidos en la memoria RAM. Dado que el malware opera con hilos concurrentes para lograr un cifrado masivo a gran velocidad, estas primitivas previenen condiciones de carrera (race conditions) al bloquear temporalmente estructuras críticas, tales como las colas de archivos pendientes, contadores de progreso o claves criptográficas. Esta combinación dota a LockBit de un entorno de ejecución estable, coordinado y de alto rendimiento, asegurando un proceso de destrucción masivo sin riesgo de colapso estructural del binario.

Estructuras Cronológicas e Identificadores Locales (Líneas 124-300) Durante el análisis del rango de líneas 124 a 165, identifiqué una enumeración exhaustiva de los días de la semana y meses del año codificados en formatos ASCII y UTF-16LE, acompañados de máscaras de formateo temporal. Estos artefactos están asociados a las rutinas encargadas de calcular los tiempos límite de pago expuestos en las notas de rescate, interactuando directamente con el reloj del sistema para generar marcas de tiempo precisas en los archivos de configuración local. Inmediatamente después, El rango 166–300 corresponde a la tabla de locales embebida por el runtime de MSVC como dependencia de las rutinas de formateo temporal identificadas previamente — overhead del compilador, no lógica del actor. El hallazgo de interés analítico real son las 4 cadenas aisladas en las líneas 87–90 (ja-JP, zh-CN, ko-KR, zh-TW), cuya posición y aislamiento sugieren una lista de verificación intencional para exclusión o configuración regional diferenciada, pendiente de confirmación en análisis dinámico.

Análisis Criptográfico y Funciones Matemáticas (Líneas 511-529) En el segmento de las líneas 511 a 529 de la sección .rdata, detecté funciones matemáticas estándar junto a las constantes mágicas expand 32-byte k y expand 16-byte k. Estas cadenas de texto constituyen la firma e inicializadores de matriz obligatorios para los cifradores de flujo Salsa20 o ChaCha20, confirmando que la lógica criptográfica está embebida de forma estática en el código fuente. Mediante esta implementación, el binario evade el uso de las APIs nativas de Windows como CryptoAPI o BCrypt, una maniobra táctica diseñada para eludir el monitoreo de soluciones de seguridad EDR y optimizar la velocidad de destrucción directamente en los registros de la CPU. Las funciones matemáticas complementarias halladas corresponden a dependencias agregadas automáticamente por la biblioteca en tiempo de ejecución (CRT) durante la compilación de este tipo de lógica.

Importación de APIs Críticas y Capacidades de Red (Líneas 572-640) El análisis de las importaciones entre las líneas 572 y 640 confirmó la integración de la librería WS2_32.dll, lo que dota al binario de funciones de red necesarias para establecer comunicación con infraestructuras de comando y control (C2), exfiltrar información sensible o explorar recursos compartidos mediante protocolos de red local como SMB. A nivel de manipulación de archivos, documenté el uso de las APIs FindFirstFileExA y FindNextFileA de KERNEL32.dll para el recorrido iterativo de directorios (directory traversal), complementadas por CreateFileW, WriteFile y FlushFileBuffers para ejecutar el secuestro directo de la información. Asimismo, la presencia de llamadas de almacenamiento local de hilos como TlsAlloc corrobora que el binario utiliza una arquitectura multihilo para paralelizar las tareas operativas de cifrado.

Mutación a la Sección .data y Estructura del Compilador (Líneas 641-879) A partir de la línea 641, registré la transición formal desde los datos de solo lectura hacia la sección .data, caracterizada por un incremento severo en la entropía y la aparición de cadenas UTF-8 inconexas. Esta firma estructural evidencia la existencia de variables globales protegidas u ocultas mediante tablas de hashes con el fin de ocultar indicadores de compromiso (IoCs) críticos, tales como extensiones objetivo, procesos a finalizar o direcciones de red. Finalmente, entre las líneas 873 y 879, la aleatoriedad cesa ante la aparición de artefactos RTTI de Microsoft Visual C++, reflejados en firmas de manejo de excepciones como bad_alloc y logic_error. Este hallazgo confirma que el binario fue estructurado bajo C++ empleando contenedores avanzados de la biblioteca estándar (STL), lo que facilita enormemente el mapeo del flujo de control y la ubicación exacta de las funciones encargadas del procesamiento de buffers.

Análisis de Cabeceras PE y Rich Header Durante la revisión de las cabeceras del ejecutable, se constató que tanto el encabezado principal (PE Header) como el Rich Header presentan una estructura completamente estándar. A pesar de contar previamente con la telemetría de DIEC, la inspección minuciosa de estos componentes se realizó con el objetivo de descartar anomalías de compilación o la inyección manual de código oculto. Este hallazgo evidencia la estrategia de los desarrolladores del binario, quienes estructuraron una arquitectura legítima y convencional para mitigar las alertas tempranas y evadir los motores de detección estática de las soluciones antivirus.

Analisis de la seccion .text El análisis inicial mediante Detect It Easy (DiE) revela una anomalía estructural crítica en el binario: únicamente la sección .text se encuentra empaquetada, mientras que el resto de las secciones PE permanecen intactas. Esta configuración altera significativamente la percepción que tienen los antivirus que se basan en análisis estáticos. Para complementar el empaquetado del código, el artefacto implementa una estrategia de evasión basada en Stack Strings (Cadenas en Pila). Al fragmentar las cadenas esenciales y asignarlas byte por byte a variables locales consecutivas en la pila, el compilador genera instrucciones de movimiento de datos individuales. Este mecanismo destruye la continuidad lineal de las cadenas en el disco, anulando por completo la efectividad de los escáneres de firmas estáticas tradicionales y de las reglas YARA estándar, debido a que los indicadores de compromiso no se encuentran expuestos de forma contigua en ninguna sección del archivo ejecutable. Una vez que los bytes son posicionados en la pila, el binario ejecuta bucles de transformación aritmética personalizados para revertir la ofuscación directamente en la memoria volátil del proceso. Este diseño elude la reconstrucción automática de la Tabla de Importaciones (IAT). Las interfaces de programación de aplicaciones (APIs) críticas o recurrentes en campañas de ransomware solo existen de forma legible en un espacio efímero de memoria y justo antes de su invocación, forzando al analista a interceptar el flujo de ejecución para identificar las capacidades reales de la muestra. Dentro de esta arquitectura de ocultamiento, la función identificada como FUN_004052f0(posteriormente renombrada a mw_stack_strings) opera exclusivamente como un resolvedor dinámico de APIs personalizado, actuando de manera homóloga a la función nativa GetProcAddress. Aunque esta rutina no contiene ni ejecuta el algoritmo de descifrado de la sección .text, es la encargada de procesar las cadenas previamente reconstruidas en el stack para obtener las direcciones legítimas de las funciones del sistema operativo en memoria. Por consiguiente, el seguimiento de las APIs resueltas por esta función permite mapear de forma indirecta las llamadas de manipulación de memoria y control que el stub utilizará para desplegar la carga útil final.

Dentro de la función mw_stack_strings

A continuación apreciamos la función que ha sido renombrada como mw_stack_strings, aprovechando que Ghidra nos da la posibilidad de renombrar funciones, variables, etc. Todo ello nos servirá para guiarnos de forma mucho más ordenada dentro de la lógica del malware. Esta función descifra y carga un total de 11 DLLs necesarias para el malware: 8 de ellas mediante un algoritmo de transformación afín con clave y multiplicador distintos por cada string, y las 3 restantes delegando el descifrado a subfunciones auxiliares, cuyo algoritmo interno aún está pendiente de confirmar. A continuación desglosamos cada bloque en el orden en que aparece.

void __fastcall mw_stack_strings(int param_1)

{
  byte bVar1;
  code *pcVar2;
  int iVar3;
  int iVar4;
  byte *pbVar5;
  int iVar6;
  int iVar7;
  int iVar8;
  int iVar9;
  int iVar10;
  int iVar11;
  int iVar12;
  int iVar13;
  int iVar14;
  undefined4 extraout_ECX;
  undefined4 extraout_ECX_00;
  undefined4 extraout_ECX_01;
  uint uVar15;
  byte local_a8 [16];
  byte local_98 [16];
  byte local_88 [16];
  undefined1 local_78;
  byte local_77 [15];
  undefined1 local_68;
  byte local_67 [15];
  undefined1 local_58;
  byte local_57 [15];
  undefined1 local_48;
  byte local_47 [15];
  undefined1 local_38;
  byte local_37 [15];
  undefined1 local_28;
  byte local_27 [23];
  undefined1 local_10;
  byte local_f [11];
  
  local_48 = 0;
  local_47[0] = 0x14;
  local_47[1] = 9;
  local_47[2] = 0x36;
  local_47[3] = 0x59;
  local_47[4] = 9;
  local_47[5] = 0x2b;
  local_47[6] = 2;
  local_47[7] = 0x6a;
  local_47[8] = 0xe;
  local_47[9] = 0x71;
  local_47[10] = 0x2b;
  local_47[0xb] = 0x2b;
  local_47[0xc] = 99;
  uVar15 = 0;
  do {
    bVar1 = local_47[uVar15];
    local_47[uVar15] = (byte)(((int)((99 - (uint)bVar1) * 0xb) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xd);
  pcVar2 = (code *)mw_resolve_api(99 - (uint)bVar1,0xf,0x439c7e33,4);
  iVar3 = (*pcVar2)(local_47);
  local_28 = 0;
  local_27[0] = 0x7b;
  local_27[1] = 0x66;
  local_27[2] = 0x6e;
  local_27[3] = 0x7c;
  local_27[4] = 0x14;
  local_27[5] = 0x6e;
  local_27[6] = 0x59;
  local_27[7] = 0x37;
  local_27[8] = 0x61;
  local_27[9] = 0x61;
  local_27[10] = 0x26;
  uVar15 = 0;
  do {
    bVar1 = local_27[uVar15];
    local_27[uVar15] = (byte)(((int)((0x26 - (uint)bVar1) * 0x18) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xb);
  pcVar2 = (code *)mw_resolve_api(0x26 - (uint)bVar1,0xf,0x439c7e33,4);
  iVar4 = (*pcVar2)(local_27);
  local_98[0] = 0;
  local_98[1] = 0x2c;
  local_98[2] = 10;
  local_98[3] = 0x69;
  local_98[4] = 100;
  local_98[5] = 0x1f;
  local_98[6] = 0x72;
  local_98[7] = 0x53;
  local_98[8] = 0x71;
  local_98[9] = 0x6a;
  local_98[10] = 10;
  local_98[0xb] = 0x18;
  local_98[0xc] = 0x18;
  local_98[0xd] = 0x59;
  pbVar5 = FUN_00401c40(local_98);
  pcVar2 = (code *)mw_resolve_api(extraout_ECX,0xf,0x439c7e33,4);
  iVar6 = (*pcVar2)(pbVar5);
  local_10 = 0;
  local_f[0] = 0x15;
  local_f[1] = 6;
  local_f[2] = 0x2e;
  local_f[3] = 0x1a;
  local_f[4] = 0x1a;
  local_f[5] = 0x36;
  local_f[6] = 0x2e;
  local_f[7] = 0x1a;
  local_f[8] = 0x1a;
  local_f[9] = 0x2a;
  uVar15 = 0;
  do {
    local_f[uVar15] = (byte)(((int)((local_f[uVar15] - 0x2a) * 0x19) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 10);
  pcVar2 = (code *)mw_resolve_api(uVar15,0xf,0x439c7e33,4);
  iVar7 = (*pcVar2)(local_f);
  local_a8[0] = 0;
  local_a8[1] = 0x52;
  local_a8[2] = 0x4e;
  local_a8[3] = 0x18;
  local_a8[4] = 5;
  local_a8[5] = 0x18;
  local_a8[6] = 0x15;
  local_a8[7] = 0x5b;
  local_a8[8] = 5;
  local_a8[9] = 0x79;
  local_a8[10] = 0x7e;
  local_a8[0xb] = 0x4b;
  local_a8[0xc] = 0x4b;
  local_a8[0xd] = 0x41;
  pbVar5 = FUN_00401be0(local_a8);
  pcVar2 = (code *)mw_resolve_api(extraout_ECX_00,0xf,0x439c7e33,4);
  iVar8 = (*pcVar2)(pbVar5);
  local_27[0xb] = 0;
  local_27[0xc] = 0x77;
  local_27[0xd] = 0x44;
  local_27[0xe] = 0x13;
  local_27[0xf] = 0x32;
  local_27[0x10] = 0x2b;
  local_27[0x11] = 0xf;
  local_27[0x12] = 0xc;
  local_27[0x13] = 0x44;
  local_27[0x14] = 0x44;
  local_27[0x15] = 0x4a;
  uVar15 = 0;
  do {
    bVar1 = local_27[uVar15 + 0xc];
    local_27[uVar15 + 0xc] = (byte)(((int)((0x4a - (uint)bVar1) * 0x12) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 10);
  pcVar2 = (code *)mw_resolve_api(0x4a - (uint)bVar1,0xf,0x439c7e33,4);
  iVar9 = (*pcVar2)(local_27 + 0xc);
  local_58 = 0;
  local_57[0] = 0x78;
  local_57[1] = 0x3b;
  local_57[2] = 100;
  local_57[3] = 0x4b;
  local_57[4] = 0x61;
  local_57[5] = 0x79;
  local_57[6] = 0x1e;
  local_57[7] = 0x36;
  local_57[8] = 0x17;
  local_57[9] = 0x7c;
  local_57[10] = 0x3b;
  local_57[0xb] = 0x3b;
  local_57[0xc] = 0x6f;
  uVar15 = 0;
  do {
    local_57[uVar15] = (byte)(((int)((local_57[uVar15] - 0x6f) * 0x25) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xd);
  pcVar2 = (code *)mw_resolve_api(uVar15,0xf,0x439c7e33,4);
  iVar10 = (*pcVar2)(local_57);
  local_68 = 0;
  local_67[0] = 0x10;
  local_67[1] = 0x4b;
  local_67[2] = 0x77;
  local_67[3] = 4;
  local_67[4] = 0x30;
  local_67[5] = 0x13;
  local_67[6] = 0xd;
  local_67[7] = 0x1b;
  local_67[8] = 0x53;
  local_67[9] = 0x59;
  local_67[10] = 0x68;
  local_67[0xb] = 0x68;
  local_67[0xc] = 0x5c;
  uVar15 = 0;
  do {
    local_67[uVar15] = (byte)(((int)((local_67[uVar15] - 0x5c) * 9) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xd);
  pcVar2 = (code *)mw_resolve_api(uVar15,0xf,0x439c7e33,4);
  iVar11 = (*pcVar2)(local_67);
  local_78 = 0;
  local_77[0] = 4;
  local_77[1] = 0x4f;
  local_77[2] = 5;
  local_77[3] = 0x2a;
  local_77[4] = 0x4f;
  local_77[5] = 99;
  local_77[6] = 0x4f;
  local_77[7] = 0x2e;
  local_77[8] = 0x28;
  local_77[9] = 0x5f;
  local_77[10] = 0x2a;
  local_77[0xb] = 0x2a;
  local_77[0xc] = 0x3b;
  uVar15 = 0;
  do {
    bVar1 = local_77[uVar15];
    local_77[uVar15] = (byte)(((int)((bVar1 - 0x3b) * 0x1f) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xd);
  pcVar2 = (code *)mw_resolve_api(bVar1 - 0x3b,0xf,0x439c7e33,4);
  iVar12 = (*pcVar2)(local_77);
  local_88[0] = 0;
  local_88[1] = 0x75;
  local_88[2] = 9;
  local_88[3] = 0x3d;
  local_88[4] = 0x4d;
  local_88[5] = 0x2d;
  local_88[6] = 0x71;
  local_88[7] = 0x16;
  local_88[8] = 0x11;
  local_88[9] = 0x54;
  local_88[10] = 0x3d;
  local_88[0xb] = 0x3d;
  local_88[0xc] = 0x36;
  pbVar5 = FUN_00401b80(local_88);
  pcVar2 = (code *)mw_resolve_api(extraout_ECX_01,0xf,0x439c7e33,4);
  iVar13 = (*pcVar2)(pbVar5);
  local_38 = 0;
  local_37[0] = 0x7b;
  local_37[1] = 0x62;
  local_37[2] = 0x1d;
  local_37[3] = 0x3f;
  local_37[4] = 0x3f;
  local_37[5] = 0x16;
  local_37[6] = 0x7e;
  local_37[7] = 0x22;
  local_37[8] = 6;
  local_37[9] = 0x3f;
  local_37[10] = 0x3f;
  local_37[0xb] = 0x77;
  uVar15 = 0;
  do {
    bVar1 = local_37[uVar15];
    local_37[uVar15] = (byte)(((int)((0x77 - (uint)bVar1) * 0xb) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xc);
  pcVar2 = (code *)mw_resolve_api(0x77 - (uint)bVar1,0xf,0x439c7e33,4);
  iVar14 = (*pcVar2)(local_37);
  if (iVar3 != 0) {
    FUN_00401730(iVar3);
  }
  if (iVar4 != 0) {
    FUN_00401730(iVar4);
  }
  if (iVar6 != 0) {
    FUN_00401730(iVar6);
  }
  if (iVar8 != 0) {
    DAT_0043b398 = 1;
    FUN_00401730(iVar8);
  }
  if (iVar9 != 0) {
    FUN_00401730(iVar9);
  }
  if (iVar10 != 0) {
    FUN_00401730(iVar9);
  }
  if (iVar11 != 0) {
    FUN_00401730(iVar11);
  }
  if (iVar12 != 0) {
    FUN_00401730(iVar12);
  }
  if (iVar13 != 0) {
    FUN_00401730(iVar13);
  }
  if (iVar14 != 0) {
    FUN_00401730(iVar14);
  }
  if (iVar7 != 0) {
    FUN_00401730(iVar7);
  }
  return;
}

Al comenzar el análisis de la función mw_stack_strings, se observa en el prólogo el uso de la convención de llamada __fastcall. Esto nos indica una optimización en el paso de argumentos, ya que el compilador prioriza el uso de registros del procesador (como ECX para el primer parámetro) en lugar de empujarlos a la pila mediante instrucciones PUSH (__stdcall o __cdecl), logrando así una ejecución más veloz al reducir los accesos a memoria.

Para proceder con el análisis vamos a observar las variables primero; de aquí podremos intuir ciertas cosas, no solo de las técnicas que ha usado el autor del malware, sino también de la forma en que Ghidra decompila, maneja la información y cómo interactúa con C, C++ y Ensamblador. Comenzamos encontrando code pcVar2. Este es un tipo code, que Ghidra utiliza para marcar que una dirección se va a tratar como instrucciones ejecutables (una función), aunque no tenga información concreta sobre esa función. Es importante aclarar que esto no significa que Ghidra haya fallado en decompilar una zona del binario — todo el contenido de .text se decompila sin excepción. Lo que ocurre aquí es distinto: pcVar2 es un puntero cuyo destino no puede determinarse de forma estática, porque la dirección real solo se calcula en tiempo de ejecución. Ghidra sabe que ese valor se va a invocar como función, pero no sabe cuál función es ni qué firma tiene, y por eso usa code como tipo genérico. Esta es precisamente la lógica que LockBit utiliza para evadir las firmas: aunque los nombres de las DLLs sí existen dentro del binario, no aparecen como referencias de importación explícitas en la IAT, se resuelven en tiempo de ejecución, logrando un sigilo estratégico. Entonces, code *pcVar2 significa “puntero a código ejecutable”, es decir, un puntero a una función genérica. La secuencia ocurre en dos pasos. Primero, se llama a mw_resolve_api pasándole los argumentos correspondientes; esta función devuelve un valor (una dirección de memoria), y ese valor se castea con (code *) para indicarle a Ghidra que debe tratarse como la dirección de una función ejecutable. Ese resultado ya casteado se guarda en pcVar2, que pasa a representar la dirección de otra función, no la dirección de mw_resolve_api en sí misma, sino el valor que esta devuelve. Segundo, (pcVar2)(local_47) desreferencia el puntero y lo invoca: usa la función a la que apunta pcVar2, pasándole local_47 como argumento. Esto logra la ofuscación porque el malware no llama a una API de forma directa; si esto sucediera, en el desensamblado encontraríamos CALL kernel32.LoadLibraryA, con el nombre visible en la IAT (Import Address Table). Aquí no hay un CALL a un nombre conocido, lo que encontramos es un CALL a un registro, cuyo valor se calculó en tiempo de ejecución. En caso de que la víctima tenga un antivirus, y este escanee la IAT buscando si el ejecutable importa funciones sospechosas (como las de manipulación de archivos o cifrado de datos), no encontrará nada fuera de lo común. En las siguientes variables vamos a encontrar información muy relevante, y que considero fundamental para todo aquel que quiere analizar una gran cantidad de ejecutables. Ghidra tiene una forma de interpretar las variables al decompilarlas, y de ahí podemos deducir muchas cosas con el simple hecho de observar los prefijos que llevan. Como ya explicamos con code *, hay ciertos casos donde podemos intuir comportamiento específico antes incluso de ver la lógica. Ghidra usa i para representar enteros (iVar3), b para bytes (bVar1), u para variables sin signo, es decir, sin bit de signo, por lo que interpretan todos sus bits como magnitud y solo representan valores positivos (uVar15) , p para punteros (pcVar2, pbVar5), y combinaciones como pb, que indica puntero a byte. El caso de code * ya fue explicado en detalle. Ahora, para int iVar3 hasta iVar14: en un binario de 32 bits, una dirección de memoria y una variable de tipo entero ocupan exactamente los mismos 4 bytes, lo que significa que en este punto no podemos afirmar con certeza qué representan estas variables, el margen es demasiado amplio todavía. Lo iremos precisando conforme avancemos y relacionemos cada variable con la lógica de la función. Lo que sí podemos observar de forma objetiva es que son exactamente 11 variables de este tipo, lo que sugiere que la función realiza 11 operaciones de naturaleza similar. Observando el bloque final de la función mw_stack_strings donde todas estas variables convergen:

if (iVar3  != 0) { FUN_00401730(iVar3);  }
if (iVar4  != 0) { FUN_00401730(iVar4);  }
if (iVar6  != 0) { FUN_00401730(iVar6);  }
if (iVar8  != 0) { DAT_0043b398 = 1;
                   FUN_00401730(iVar8);  }
if (iVar9  != 0) { FUN_00401730(iVar9);  }
if (iVar10 != 0) { FUN_00401730(iVar9);  }
if (iVar11 != 0) { FUN_00401730(iVar11); }
if (iVar12 != 0) { FUN_00401730(iVar12); }
if (iVar13 != 0) { FUN_00401730(iVar13); }
if (iVar14 != 0) { FUN_00401730(iVar14); }
if (iVar7  != 0) { FUN_00401730(iVar7);  }

Contando las variables que aparecen aquí, iVar3, iVar4, iVar6, iVar7, iVar8, iVar9, iVar10, iVar11, iVar12, iVar13, iVar14, obtenemos exactamente 11 variables distintas, cada una con un valor asignado en algún punto del cuerpo de la función. Esto confirma que hubo 11 operaciones que produjeron un resultado, sin necesidad de entrar a ninguna subfunción ni contar loops manualmente. Rastreando hacia arriba el origen de cada variable dentro del cuerpo de mw_stack_strings, encontramos dos patrones de asignación claramente distintos que nos permiten clasificar los 11 bloques antes de analizarlos en detalle: 8 bloques con patrón directo: el buffer local pasa por un loop do/while visible, y el resultado de invocar mw_resolve_api se asigna directamente a la variable (iVar3, iVar4, iVar7, iVar9, iVar10, iVar11, iVar12, iVar14). 3 bloques con patrón delegado: el buffer local se pasa a una subfunción auxiliar (FUN_00401c40, FUN_00401be0, FUN_00401b80), el primer argumento de mw_resolve_api proviene de extraout_ECX en vez de un cálculo local, y el argumento de la invocación final es pbVar5 en vez del buffer directamente (iVar6, iVar8, iVar13). No coincidentemente, son exactamente los mismos 3 extraout_ECX que ya identificamos en la sección de declaraciones,lo que entonces era solo una observación estructural, ahora se confirma como evidencia de un patrón de implementación distinto. byte *pbVar5 es un puntero a byte, por lo que probablemente apunta a un buffer o a alguna secuencia de bytes, aunque, al igual que las anteriores, su propósito exacto quedará claro más adelante. En el caso de undefined4 extraout_ECX, extraout_ECX_00 y extraout_ECX_01: este prefijo indica que, en algún punto del cuerpo de la función, el registro ECX contiene un valor que el código usa, pero Ghidra no logra enlazarlo a ninguna asignación explícita dentro del flujo de datos normal, no porque no sepa que el valor existe, sino porque no puede rastrear de dónde proviene siguiendo el flujo convencional. El undefined indica que Ghidra conoce el tamaño (4 bytes, de ahí el 4), pero no puede determinar qué tipo de dato representa. Vale la pena notar que son exactamente tres, un dato que retomaremos cuando avancemos en la lógica. uint uVar15 es una variable sin signo, que por su tipo podría funcionar como contador, índice de iteración, o dirección de memoria. Sin más contexto por ahora, lo dejamos como observación abierta. Los buffers de bytes son los que más información nos proporcionan en esta etapa. byte local_a8 [16], byte local_98 [16] y byte local_88 [16] están declarados como arrays planos de 16 bytes. En cambio, undefined1 local_78; byte local_77 [15] — y el mismo patrón para local_68/local_67, local_58/local_57, local_48/local_47 y local_38/local_37 ,presentan un byte separado antes de cada array de 15 bytes, sumando igualmente 16 bytes en total, pero partidos en dos “variables” por Ghidra. Esta diferencia en la interpretación probablemente refleja un patrón de acceso distinto en el código: los tres primeros buffers son accedidos de una manera, mientras que los otros cinco lo son de otra. Cuál es exactamente esa diferencia, y si tiene implicaciones para entender el comportamiento del malware, lo confirmaremos cuando veamos las asignaciones. Primer bloque de la función mw_stack_strings

  local_48 = 0;
  local_47[0] = 0x14;
  local_47[1] = 9;
  local_47[2] = 0x36;
  local_47[3] = 0x59;
  local_47[4] = 9;
  local_47[5] = 0x2b;
  local_47[6] = 2;
  local_47[7] = 0x6a;
  local_47[8] = 0xe;
  local_47[9] = 0x71;
  local_47[10] = 0x2b;
  local_47[0xb] = 0x2b;
  local_47[0xc] = 99;
  uVar15 = 0;
  do {
    bVar1 = local_47[uVar15];
    local_47[uVar15] = (byte)(((int)((99 - (uint)bVar1) * 0xb) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
  } while (uVar15 < 0xd);
  pcVar2 = (code *)mw_resolve_api(99 - (uint)bVar1,0xf,0x439c7e33,4);
  iVar3 = (*pcVar2)(local_47);

Inicialización del buffer Lo primero que ocurre es local_48 = 0: se escribe un byte nulo en la posición de memoria inmediatamente anterior al buffer local_47. Como vimos en el análisis de declaraciones, ambas variables son físicamente contiguas en el stack, por lo que este cero actúa como terminador nulo de la string que quedará descifrada en local_47, una preparación de estructura. A continuación se asignan los bytes cifrados uno por uno al buffer local_47, desde la posición [0] hasta la [0xc] (posición 12, que en decimal son 13 bytes en total). Estos valores hexadecimales fueron calculados por el desarrollador del malware al momento de escribir el código fuente, de forma que al aplicarles la transformación inversa produzcan el string objetivo. El compilador los tradujo como constantes sin saber que eran strings cifradas para el compilador eran simplemente números. El último valor asignado es 99 (decimal), en la posición [0xc]. Este número es la clave del cifrado de este bloque específico. Por diseño del algoritmo, al aplicarle la fórmula del loop a este byte, siempre produce 0 ,el byte nulo que terminará la string descifrada. Identificar este patrón (último byte del buffer = clave del loop) es una de las primeras cosas que debemos buscar al encontrar este tipo de construcción, porque permite confirmar rápidamente los límites de cada string cifrada antes de calcular nada. El loop do/while una transformación afín

uVar15 = 0;
do {
    bVar1 = local_47[uVar15];
    local_47[uVar15] = (byte)(((int)((99 - (uint)bVar1) * 0xb) % 0x7f + 0x7f) % 0x7f);
    uVar15 = uVar15 + 1;
} while (uVar15 < 0xd);

uVar15 actúa como contador del loop ,arranca en 0 y se incrementa en 1 en cada iteración. La condición uVar15 < 0xd (donde 0xd = 13 en decimal) hace que el loop recorra exactamente las 13 posiciones del buffer, de [0] a [12]. Al terminar el loop, uVar15 valdrá 13 , ese valor quedará guardado en la variable para uso posterior fuera del loop, algo que veremos más adelante. bVar1 actúa como variable temporal de un solo byte: en cada iteración captura el valor del byte actual antes de que sea sobreescrito. Esto es necesario porque la fórmula necesita el valor original del byte para calcular el nuevo. Al terminar el loop, bVar1 contendrá el valor del último byte procesado, el byte en posición [12], que es 99 (la clave). Este valor también se usará fuera del loop. La fórmula que se le aplica a cada byte es:

byte_nuevo = ((99 - byte_original) × 11) mod 127

Esta es una transformación afín una operación matemática de la forma f(x) = a·x + b, que en criptografía clásica es la generalización del cifrado César. La diferencia con un César simple es que además del desplazamiento (b), hay una multiplicación (a) que mezcla más los valores y hace la transformación más difícil de revertir a simple vista. Aquí los parámetros son: Clave (k): 99 — la constante que se resta al byte original antes de multiplicar, Multiplicador: 0xb (11 en decimal) — el factor por el que se multiplica la diferencia, Módulo: 0x7f (127 en decimal) — el espacio de valores sobre el que opera el cifrado. En este caso se realiza la operación % 0x7f + 0x7f) % 0x7f porque garantiza un resultado positivo para resolver el problema de La operación 99 - byte_original produce una resta. Cuando byte_original es mayor que 99, el resultado es negativo. Por ejemplo:

byte_original = 0x71 (113 en decimal) 99 - 113 = -14 -14 × 11 = -154

En matemáticas puras, -154 mod 127 debería dar 100 (porque -154 + 2×127 = 100). Pero en C, el comportamiento del operador % con números negativos no está garantizado de ser positivo ,depende del compilador y la arquitectura. En la mayoría de implementaciones modernas, -154 % 127 da -27, no 100. Ese -27 como valor de un byte es un problema serio: los bytes son sin signo por definición (rango 0-255), y un valor negativo produciría comportamiento indefinido al intentar almacenarlo en un byte. La solución % n + n) % n: La expresión completa es:

(valor % 0x7f + 0x7f) % 0x7f
#la vemos paso a paso con el ejemplo de -154:
Paso 1: -154 % 127 = -27      (resultado potencialmente negativo)
Paso 2: -27 + 127 = 100       (sumamos 127 para forzar positivo)
Paso 3: 100 % 127 = 100       (módulo final — si ya era positivo, no cambia nada)

La clave está en el paso 2: sumar 0x7f (127) garantiza que cualquier valor en el rango [-126, 0] se convierta en un valor en el rango [1, 127]. El paso 3 es necesario para el caso opuesto — cuando el valor del paso 1 ya era positivo, sumarle 127 podría sacarlo del rango correcto, y el módulo final lo devuelve al rango válido. El desarrollador del malware necesita esto específicamente por dos razones entrelazadas: La primera es de corrección matemática: el cifrado afín está definido matemáticamente sobre aritmética modular, que por definición solo produce resultados en el rango [0, n-1]. Si el código produce valores negativos, el cifrado se rompe, algunos bytes se descifrarán incorrectamente y el string resultante será basura, lo que significaría que el malware no puede cargar la DLL que necesita y fallaría en ejecución. El truco es, simplemente, implementar correctamente la aritmética modular en C. La segunda es de compatibilidad con ASCII: todos los caracteres que forman nombres de DLLs de Windows (k, e, r, n, e, l, 3, 2, ., d, l, l) tienen valores ASCII en el rango [46, 122] — completamente dentro del rango [0, 127]. El desarrollador diseñó el cifrado para que opere sobre ese espacio (módulo 127), lo que garantiza que los bytes descifrados siempre sean ASCII imprimible válido. Un resultado negativo o mayor a 127 rompería esa garantía. Resolución e invocación:

pcVar2 = (code *)mw_resolve_api(99 - (uint)bVar1, 0xf, 0x439c7e33, 4);
iVar3 = (*pcVar2)(local_47);

Al salir del loop, el buffer local_47 ya contiene el string descifrado en claro, directamente en la pila del proceso, no en una región separada de memoria, no en el heap, sino en la pila de llamadas activa de mw_stack_strings. pcVar2 recibe la dirección calculada por mw_resolve_api. Sus cuatro argumentos son: 99 - (uint)bVar1: bVar1 contiene el último byte procesado por el loop, que es 99 (la clave). 99 - 99 = 0. Este primer argumento siempre vale 0 al terminar el loop en este bloque — por el mismo diseño que hace que el último byte descifre como \0. 0xf: constante idéntica en todos los bloques, su propósito exacto queda pendiente de confirmar cuando analicemos mw_resolve_api en detalle. 0x439c7e33: constante idéntica en todos los bloques, candidato fuerte a ser un hash o identificador de módulo interno del resolutor. 4: constante idéntica en todos los bloques, posiblemente un flag de modo de operación. Finalmente, (*pcVar2)(local_47) invoca la función a la que apunta pcVar2, pasándole el buffer ya descifrado (“kernel32.dll\0”) como argumento. El valor que esa función devuelve queda en iVar3. Sobre la naturaleza de iVar3: en este punto del análisis podemos plantear una hipótesis de trabajo fundamentada, pero no una afirmación definitiva. Solo sabemos que se le pasó el string “kernel32.dll” a una función resuelta dinámicamente. Descifrado verificable: el script Python Para no depender únicamente de la lectura manual de la fórmula, que es propensa a errores aritméticos, replicamos el algoritmo exactamente tal como aparece en el pseudocódigo de Ghidra:

def descifrar_bloque(bytes_cifrados, clave, multiplicador):
    """
    Replica la transformacion afin del loop do/while:
    byte_nuevo = ((clave - byte_original) * multiplicador % 0x7f + 0x7f) % 0x7f
    """
    resultado = bytearray()
    for b in bytes_cifrados:
        byte_nuevo = ((clave - b) * multiplicador % 0x7f + 0x7f) % 0x7f
        resultado.append(byte_nuevo)
    return resultado

# Bloque 1: local_47
# Bytes cifrados visibles en Ghidra (posiciones [0] a [0xc])
bytes_cifrados = [0x14, 9, 0x36, 0x59, 9, 0x2b, 2, 0x6a, 0xe, 0x71, 0x2b, 0x2b, 99]
clave = 99       # último byte del buffer = clave del loop
multiplicador = 0xb  # constante visible en el loop

resultado = descifrar_bloque(bytes_cifrados, clave, multiplicador)
print(bytes(resultado))

Output: b’kernel32.dll\x00’

El byte nulo final (\x00) confirma la hipótesis del terminador: el último byte cifrado (99) aplicado a la fórmula produce exactamente 0, cerrando el string de forma válida para cualquier función de la API de Windows que espere una cadena terminada en null. Hemos observado 11 bloques de lógica muy parecida, 8 que tienen la misma mecánica con diferente clave y 3 de lógica distinta , dando un vistazo a toda la función mw stack strings observamos if (iVar3 != 0).

La condición if (iVar3 != 0), que se repite para cada una de las 11 variables, es el primer dato concreto que tenemos sobre la naturaleza de estos valores. En C, comparar un valor con 0 (o NULL, que en un sistema de 32 bits es simplemente 0) es el patrón estándar para verificar si una operación tuvo éxito o falló. Las funciones de la Win32 API que devuelven handles o punteros usan sistemáticamente NULL como señal de fallo — si la operación no pudo completarse, devuelven 0; si tuvo éxito, devuelven cualquier valor distinto de 0. Esto nos da la primera restricción concreta sobre la naturaleza de iVar3…iVar14: son valores que pueden ser cero (indicando fallo) o distintos de cero (indicando éxito), y el código solo procede con FUN_00401730 cuando la operación fue exitosa. Este patrón es incompatible con valores puramente aritméticos — un resultado de un cálculo matemático no se “verifica de éxito” de esta manera. Lo que sí se verifica así son handles y punteros, donde NULL tiene el significado semántico de “no existe” o “no se pudo obtener”. Combinando esta observación con lo que ya sabíamos, que cada variable recibió el resultado de invocar una función con un nombre de DLL como argumento, el universo de candidatas se reduce de forma significativa. Las funciones de la Win32 API que reciben un nombre de DLL como string y devuelven un valor que puede ser NULL en caso de fallo son esencialmente tres: LoadLibraryA, LoadLibraryExA y GetModuleHandleA. De estas, LoadLibraryExA recibe tres argumentos y aquí solo se pasa uno, así que queda descartada. Las candidatas reales son LoadLibraryA y GetModuleHandleA. Tres anomalías que no deben pasar desapercibidas: La primera es iVar8, es la única variable que, además de invocar FUN_00401730, activa un flag global:

if (iVar8 != 0) { DAT_0043b398 = 1; FUN_00401730(iVar8); }

DAT_0043b398 es una variable global que se establece en 1 únicamente cuando la carga asociada a iVar8 tiene éxito, y no ocurre para ninguna otra de las 11 variables. Esto sugiere que la DLL correspondiente a iVar8 activa una funcionalidad condicional en otra parte del binario: algo que el malware solo ejecutará si esa carga específica fue exitosa. Cuál es esa funcionalidad quedará pendiente hasta que encontremos dónde se lee DAT_0043b398 más adelante en el binario. La segunda anomalía es iVar10:

if (iVar10 != 0) { FUN_00401730(iVar9); }

La condición verifica iVar10, pero la llamada usa iVar9, no iVar10. Esto puede ser un error real del malware, un artefacto de la decompilación de Ghidra confundiendo registros, o una lógica intencional que aún no comprendemos. Lo anotamos como pendiente de verificación en la fase dinámica, donde veremos el valor real de ambas variables en ese instante. La tercera observación es el orden de procesamiento: iVar7 es la última variable en procesarse, a pesar de haber sido asignada en el bloque 4, no al final. Esto indica que el orden de invocación de FUN_00401730 no refleja el orden de asignación de las variables, sino posiblemente una dependencia lógica: quizás la DLL asociada a iVar7 debe procesarse después de todas las demás por alguna razón de inicialización. al haber hechado un vistazo en todo mw stack strings, nos damos cuenta que a lo largo de toda la funcion dependemos de mw resolve api por lo que pasaremos a su analisis, en lugar de haber ido a por toda la funcion, simplemente los demás strings de dlls dentro de mw stack strings están ofuscados de forma parecida, y algunos que tienen otra forma lo cual ya hemos mencionado anteriormente, asi que vamos ahora por el analisis de mw resolve api la cual llamé así por que vi a simple vista que quiza, podría encontrar alli la resolucion de apis, pero ahora es por primera vez que entraremos allí por lo cual si su función es otra pasarémos a renombrarla de forma distinta entrando tenemos:

/* WARNING: Removing unreachable block (ram,0x0040553f) */
/* WARNING: Removing unreachable block (ram,0x00405535) */
/* WARNING: Removing unreachable block (ram,0x00405546) */
/* WARNING: Removing unreachable block (ram,0x004055ad) */
/* WARNING: Removing unreachable block (ram,0x004055bc) */
/* WARNING: Removing unreachable block (ram,0x004055d5) */
/* WARNING: Removing unreachable block (ram,0x004055e5) */
/* WARNING: Removing unreachable block (ram,0x004055e2) */
/* WARNING: Removing unreachable block (ram,0x004055e6) */
/* WARNING: Removing unreachable block (ram,0x00405548) */
/* WARNING: Removing unreachable block (ram,0x00405560) */
/* WARNING: Removing unreachable block (ram,0x00405567) */
/* WARNING: Removing unreachable block (ram,0x0040556d) */
/* WARNING: Removing unreachable block (ram,0x00405572) */
/* WARNING: Removing unreachable block (ram,0x00405582) */
/* WARNING: Removing unreachable block (ram,0x0040557f) */
/* WARNING: Removing unreachable block (ram,0x00405583) */
/* WARNING: Removing unreachable block (ram,0x00405590) */
/* WARNING: Removing unreachable block (ram,0x00405597) */
/* WARNING: Removing unreachable block (ram,0x00405599) */
/* WARNING: Removing unreachable block (ram,0x00405333) */
/* WARNING: Removing unreachable block (ram,0x00405344) */
/* WARNING: Removing unreachable block (ram,0x0040534b) */
/* WARNING: Removing unreachable block (ram,0x00405351) */
/* WARNING: Removing unreachable block (ram,0x00405362) */
/* WARNING: Removing unreachable block (ram,0x00405369) */
/* WARNING: Removing unreachable block (ram,0x0040536b) */
/* WARNING: Removing unreachable block (ram,0x00405377) */
/* WARNING: Removing unreachable block (ram,0x00405419) */
/* WARNING: Removing unreachable block (ram,0x00405428) */
/* WARNING: Removing unreachable block (ram,0x00405446) */
/* WARNING: Removing unreachable block (ram,0x00405457) */
/* WARNING: Removing unreachable block (ram,0x00405454) */
/* WARNING: Removing unreachable block (ram,0x00405458) */
/* WARNING: Removing unreachable block (ram,0x004053ae) */
/* WARNING: Removing unreachable block (ram,0x004053b8) */
/* WARNING: Removing unreachable block (ram,0x004053bf) */
/* WARNING: Removing unreachable block (ram,0x004053c1) */
/* WARNING: Removing unreachable block (ram,0x004053d9) */
/* WARNING: Removing unreachable block (ram,0x004053e0) */
/* WARNING: Removing unreachable block (ram,0x004053e6) */
/* WARNING: Removing unreachable block (ram,0x004053f0) */
/* WARNING: Removing unreachable block (ram,0x00405400) */
/* WARNING: Removing unreachable block (ram,0x004053fd) */
/* WARNING: Removing unreachable block (ram,0x00405401) */
/* WARNING: Removing unreachable block (ram,0x0040540e) */
/* WARNING: Removing unreachable block (ram,0x00405415) */
/* WARNING: Removing unreachable block (ram,0x00405417) */
/* WARNING: Removing unreachable block (ram,0x004054eb) */
/* WARNING: Removing unreachable block (ram,0x00405497) */
/* WARNING: Removing unreachable block (ram,0x004054a9) */
/* WARNING: Removing unreachable block (ram,0x004054b0) */
/* WARNING: Removing unreachable block (ram,0x004054b6) */
/* WARNING: Removing unreachable block (ram,0x004054c7) */
/* WARNING: Removing unreachable block (ram,0x004054ce) */
/* WARNING: Removing unreachable block (ram,0x004054d0) */

int __fastcall mw_resolve_api(undefined4 param_1,undefined4 param_2,uint param_3,uint param_4)

{
  int iVar1;
  int iVar2;
  int iVar3;
  
  iVar1 = param_4 * 4;
  iVar3 = *(int *)(iVar1 + DAT_0043b3a0);
  for (iVar2 = 0x440807 - iVar3; iVar2 % 3 == 0; iVar2 = iVar2 + 1) {
  }
  if (iVar3 == 0) {
    iVar3 = FUN_00404530(0,param_2,param_3);
    param_4 = 0x440807 - iVar3;
    if ((int)param_4 % 3 == 0) {
      do {
        param_4 = param_4 + 1;
      } while ((int)param_4 % 3 == 0);
      *(int *)(iVar1 + DAT_0043b3a0) = iVar3;
      return iVar3;
    }
    *(int *)(iVar1 + DAT_0043b3a0) = iVar3;
  }
  return iVar3;
}

Lo primero que nos encontramos son 54 warnings de este tipo:

/* WARNING: Removing unreachable block (ram,0x0040553f) */

Para entender qué significa esto, hay que entender cómo trabaja Ghidra internamente. Cuando Ghidra decompila una función, primero construye un grafo de flujo de control, una representación de todos los caminos posibles que puede tomar la ejecución dentro de esa función. Cada nodo del grafo es un “bloque básico”: una secuencia de instrucciones que siempre se ejecutan juntas, sin saltos en el medio. Cuando Ghidra analiza ese grafo, identifica qué bloques son alcanzables desde el punto de entrada de la función y cuáles no lo son, es decir, qué bloques nunca pueden ejecutarse sin importar qué camino tome el flujo. Los bloques que detecta como inalcanzables los elimina del pseudocódigo y te avisa con este warning. Que haya 54 bloques inalcanzables en una función tan corta es llamativo y no es accidental. La causa directa ya la identificamos con el análisis de DIEC al inicio del documento: el binario usa Profile-Guided Optimization (PGO), evidenciado por los Records[pogo] en los metadatos de compilación. El PGO funciona así: el desarrollador compila el programa, lo ejecuta en condiciones de prueba, el compilador registra cuáles son los caminos de ejecución más frecuentes, y en una segunda compilación reorganiza el código para que esos caminos frecuentes sean más eficientes, moviendo bloques de memoria, eliminando saltos, fusionando condiciones. Este proceso de reorganización agresiva produce inevitablemente bloques de código que quedaron físicamente en el binario pero que ya ninguna instrucción de salto apunta a ellos. No son código muerto en el sentido de que el programador los escribió por error, son el residuo estructural de una optimización industrial. Ghidra los detecta, los descarta del pseudocódigo, y nos avisa. Esto tiene valor concreto: 54 bloques inalcanzables en esta sola función son evidencia adicional y directa del PGO, consistente con lo que ya observamos en el triaje inicial. No es algo que se menciona de pasada, es una confirmación independiente del mismo hallazgo desde un ángulo distinto. Tambien debemos tener en cuenta que ghidra para optimizar la decompilacion obvia aquellas partes de codigo que finalmente no hacen nada, simplificandonos el analisis ante el gran trabajo que realizó el ofuscador de este malware diificultandonos la lógica. La función

int __fastcall mw_resolve_api(undefined4 param_1, undefined4 param_2, uint param_3, uint param_4)

Ya conocemos __fastcall del análisis de mw_stack_strings: los primeros dos argumentos viajan en registros del procesador (ECX para param_1, EDX para param_2) en vez de en la pila, optimizando el paso de parámetros. La función devuelve un int de 4 bytes, que en un binario PE32 (32 bits) tiene exactamente el mismo tamaño que una dirección de memoria, lo cual ya anticipamos que es relevante. Los cuatro parámetros tienen tipos undefined4 o uint, Ghidra usa undefined4 cuando conoce el tamaño (4 bytes) pero no puede determinar el tipo exacto del dato.


Derechos de autor y atribución

© 2026 Gino Aldair Maihuiri Romero. Todos los derechos reservados.

Este análisis, su redacción, estructura, explicaciones técnicas, interpretaciones, scripts y material original asociado están protegidos por derechos de autor. Se permite citar fragmentos breves con fines académicos, educativos o de investigación siempre que se atribuya claramente la autoría y se incluya un enlace a la publicación original. La reproducción total, redistribución sustancial, republicación o adaptación del contenido sin autorización previa del autor no está permitida.

Autor: Gino Aldair Maihuiri Romero
Sitio oficial: https://aldairmaihuiri-svchost.github.io/
Publicación: 2026