Desde 2026

RelevantSec

2026-07-16

auto
EN ES
investigación

Investigación original de RelevantSec

AnyDesk: análisis de la primera etapa de un goodware

Resumen

Hace poco analizamos AnyDesk 9.7.10 y descubrimos que su ejecutable de primera etapa emplea varias técnicas que solemos encontrar durante el análisis de malware:

  • Recorrido de la PEB.
  • Un directorio de importaciones vacío.
  • Nombres de API cifrados.
  • Resolución de exportaciones en tiempo de ejecución.
  • Una imagen PE grande, cifrada y comprimida.
  • Páginas de memoria convertidas en escribibles y ejecutables.
  • Un cargador interno que aplica relocalizaciones y repara su propia IAT.

La lista podría describir un implante empaquetado, pero en este caso pertenece a un producto de acceso remoto correctamente firmado. Por eso, el archivo resulta un ejercicio útil de análisis de malware.

Seguiremos la pequeña primera etapa hasta que recupere la aplicación de 30 MB oculta en su interior. Introduciremos cada concepto cuando el cargador lo necesite.

Visión general

Cuando se descarga desde la página oficial de AnyDesk para Windows, el ejecutable firmado solo ocupa unos 8 MB. Su función es reconstruir un PE interno mucho mayor. A grandes rasgos, la ruta de inicio tiene dos etapas:

text
AnyDesk.exe firmado (8.343.992 bytes)
  |
  +-- Etapa 1: recorrido de la PEB + arranque mediante la EAT
  |      |
  |      +-- LCG-XOR descifra nombres y payload
  |      +-- decodificación LZMA + reversión del BCJ x86
  |      +-- copia cabeceras y secciones en la zona .itext reservada
  |
  +-- PE interno sin mapear (30.489.600 bytes)
         |
         +-- Etapa 2: relocalizaciones + importaciones + protecciones
         +-- entrada en la aplicación AnyDesk

Las dos etapas se reparten el trabajo habitual del cargador de imágenes de Windows:

Responsabilidad Etapa externa Etapa interna
Encontrar un módulo inicial sin importaciones No
Resolver un pequeño conjunto de API nativas Usa callbacks proporcionados
Descifrar y descomprimir el PE incrustado No
Copiar cabeceras y secciones del PE según su RVA No
Aplicar relocalizaciones de base No
Cargar 23 dependencias No
Resolver 767 importaciones y reparar la IAT No
Establecer las protecciones finales de cada sección No
Entrar en la aplicación real No

No se trata de una inyección reflectiva de DLL clásica, porque no interviene ningún proceso remoto. Es más preciso describirlo como un programa que reconstruye manualmente una imagen dentro de su propio espacio de direcciones.

Primera inspección

Antes de abrir un descompilador, comprobamos que todos estuviéramos observando la misma muestra:

Propiedad Valor observado
Versión del producto AnyDesk 9.7.10
Arquitectura PE32 / x86
Tamaño del archivo 8.343.992 bytes
SHA-256 46872febd9684df716d392b457aef6611ae7b8716d2ece6bca30fb97271bce1d
Estado de Authenticode Válido
Firmante AnyDesk Software GmbH
Validez del certificado Del 11 de febrero de 2026 al 13 de febrero de 2027
Directorio de importaciones externo Vacío

Los nombres de funciones que aparecen a continuación son etiquetas que asignamos durante el análisis; el binario no contiene símbolos. También hemos eliminado de los fragmentos de ensamblador las direcciones específicas de la muestra, pero hemos conservado las instrucciones relevantes.

Con DiE ya podemos observar propiedades habituales en un empaquetador: un directorio de importaciones vacío, una sección .data de alta entropía y una sección .itext que ocupa memoria, pero no contiene bytes en el archivo.

Salida de DiE para AnyDesk.exe
Salida de DiE para AnyDesk.exe

Una entropía alta no permite determinar por sí sola si los datos están comprimidos, cifrados o si simplemente son inusuales. Sí indica dónde conviene mirar. En esta muestra, casi todos los bytes de alta entropía del archivo están en .data.

Entropía de cada sección de AnyDesk.exe
Entropía de cada sección de AnyDesk.exe

Dadas las funciones de AnyDesk y la poca cantidad de código visible, nuestra hipótesis de trabajo es que .data contiene la aplicación real transformada de algún modo.

La tabla de secciones respalda esa hipótesis:

Sección Tamaño virtual Tamaño en archivo Características
.text 0x296D 0x2A00 0x60000020
.itext 0x1D33C00 0 0xC0000080
.rdata 0x434 0x600 0x40000040
.data 0x7E8344 0x7E8000 0xC0000040
.rsrc 0x4878 0x4A00 0x40000040
.reloc 0x84 0x200 0x42000040

Esto nos da tres observaciones útiles:

  1. .text solo contiene unos 10 KB de código del cargador.
  2. .data contiene casi todos los bytes que ocupan espacio en disco.
  3. .itext no ocupa bytes físicos en el archivo, pero solicita 30.620.672 bytes de espacio virtual inicializado a cero al mapear la imagen.

El punto de entrada está dentro de la pequeña sección .text. A partir de ahí, podemos seguir el cargador en el mismo orden en que resuelve sus problemas.

Recorrido de la PEB

El primer problema aparece de inmediato: el ejecutable no tiene importaciones normales. Para entender por qué es importante, debemos desviarnos brevemente para ver cómo conecta Windows un programa con las funciones de una DLL.

Por qué importa la IAT

Un PE normal contiene un directorio de importaciones. Este indica a Windows qué DLL necesita el programa y qué funciones quiere utilizar de cada una. Para cada función importada, el cargador resuelve su dirección real y la escribe en la tabla de direcciones de importación (Import Address Table, IAT).

En el código fuente, un programa podría contener:

c
VirtualProtect(address, size, PAGE_EXECUTE_READWRITE, &old_protection);

En Windows de 32 bits, la llamada compilada suele tener conceptualmente este aspecto:

x86asm
call dword ptr [__imp__VirtualProtect@16]

__imp__VirtualProtect@16 es una entrada de la IAT. Antes de que se inicie el programa, Windows sustituye esa entrada por la dirección real de VirtualProtect dentro de kernel32.dll.

text
Descriptor de importación del PE
  -> "kernel32.dll"
  -> "VirtualProtect"
  -> Windows resuelve la exportación
  -> Windows escribe su dirección en FirstThunk / la IAT
  -> el programa llama a través de esa entrada

En este archivo de AnyDesk, el directorio de importaciones está vacío. No existe ninguna entrada rellenada por el cargador para VirtualProtect, GetProcAddress ni siquiera GetModuleHandleW. Esta primera etapa no tiene funciones importadas normales que Windows pueda enlazar.

Esto crea un problema de arranque:

text
Sin importaciones -> no hay GetProcAddress
Sin GetProcAddress -> no hay una forma sencilla de resolver otra API

La solución consiste en utilizar información que Windows ya ha colocado en el proceso.

Siguiendo la PEB

Cada hilo tiene un bloque de entorno del hilo (Thread Environment Block, TEB). Cada proceso también tiene un bloque de entorno del proceso (Process Environment Block, PEB). Entre otras cosas, la PEB apunta a datos del cargador que contienen una lista enlazada de los módulos que ya están cargados en el proceso.

En Windows de 32 bits, el segmento FS permite que el código acceda a la TEB actual. La función auxiliar de AnyDesk solo contiene dos instrucciones:

x86asm
mov eax, dword ptr fs:[0x18]  ; TEB actual
ret

La función que la invoca sigue el resto de la cadena:

x86asm
call get_teb                 ; función auxiliar anterior
mov  eax, [eax+0x30]         ; TEB->ProcessEnvironmentBlock
mov  esi, [eax+0x0C]         ; PEB->Ldr
add  esi, 0x0C               ; &Ldr->InLoadOrderModuleList
mov  edi, [esi]              ; primera LIST_ENTRY (Flink)

Eso es el recorrido de la PEB en su forma más sencilla: seguir punteros a través de estructuras que Windows ya ha inicializado.

La misma lógica se puede escribir como pseudocódigo de estilo C. Se ha preparado deliberadamente para 32 bits y reproduce los desplazamientos que usa esta muestra:

c
static void *find_ntdll_base(void) {
    uint32_t teb  = __readfsdword(0x18);
    uint32_t peb  = *(uint32_t *)(teb + 0x30);
    uint32_t ldr  = *(uint32_t *)(peb + 0x0c);
    uint32_t head = ldr + 0x0c;

    // AnyDesk descifra este valor durante la ejecución.
    wchar_t *wanted = decrypt_wide_name(
        encrypted_ntdll, 10, 0x3114b604);  // -> L"ntdll"

    for (uint32_t entry = *(uint32_t *)head;
         entry != head;
         entry = *(uint32_t *)entry) {

        wchar_t *base_name = *(wchar_t **)(entry + 0x30);

        // Cinco caracteres UTF-16 = 10 bytes.
        if (local_memcmp(base_name, wanted, 10) == 0)
            return (void *)*(uint32_t *)(entry + 0x18);
    }

    return NULL;
}

AnyDesk tampoco importa memcmp para realizar esta comparación. La primera etapa incluye su propia rutina pequeña de comparación de bytes.

La parte relevante del bucle real tiene este aspecto:

x86asm
module_loop:
push 0x0A                 ; descifra 10 bytes
push encrypted_ntdll
push ntdll_seed
call decrypt_wide_name    ; produce L"ntdll"
push 0x0A                 ; longitud de comparación
push eax                  ; nombre descifrado
push dword ptr [edi+0x30] ; BaseDllName.Buffer
call local_memcmp
test eax, eax
je   module_found
mov  edi, [edi]           ; entry = entry->Flink
cmp  edi, esi             ; ¿hemos vuelto a la cabecera?
jne  module_loop

module_found:
mov edi, [edi+0x18]       ; entrada LDR -> DllBase

Por tanto, el objetivo es muy concreto: obtener la dirección base de ntdll.dll sin llamar a una API importada y sin depender de una IAT.

Si quieres explorar las mismas estructuras de forma interactiva en WinDbg, el recorrido de la PEB de ired.team es un recurso complementario excelente. Muestra la PEB, los datos del cargador y las listas de módulos desde el punto de vista del depurador.

Los desplazamientos anteriores no forman parte de una API portable de Windows. Microsoft considera estas estructuras internas, y sus valores difieren entre procesos de 32 y 64 bits. Son correctos para esta muestra x86 y explican con precisión lo que hacen sus instrucciones.

Por qué es relevante

Observar fs:[0x18] no basta por sí solo para clasificar algo como malware. El patrón importante es lo que ocurre a continuación:

text
leer la TEB
  -> llegar a los datos del cargador en la PEB
  -> recorrer los módulos cargados
  -> recuperar la base de ntdll
  -> tratar esa base como un PE
  -> analizar sus exportaciones

Ahora conocemos la dirección de una DLL, pero aún no la de una función dentro de ella. Esto nos lleva de forma natural al siguiente paso.

Resolución de API

La contrapartida de una importación es una exportación. La tabla de direcciones de exportación (Export Address Table, EAT) de una DLL describe las funciones que esta pone a disposición de otro código.

Como AnyDesk todavía no puede llamar a GetProcAddress, la primera etapa realiza la búsqueda por sí misma.

Recorrido de la EAT

Cuando edi contiene la base de ntdll, la primera etapa la analiza como un PE:

x86asm
mov eax, [edi+0x3C]       ; DOS.e_lfanew
mov eax, [eax+edi+0x78]   ; RVA del directorio de exportaciones
add eax, edi              ; IMAGE_EXPORT_DIRECTORY *
mov ecx, [eax+0x1C]       ; AddressOfFunctions
mov edx, [eax+0x20]       ; AddressOfNames
mov ecx, [eax+0x24]       ; AddressOfNameOrdinals

A continuación, recorre los nombres exportados hasta encontrar LdrGetProcedureAddress. El nombre buscado se descifra justo antes de compararlo:

x86asm
mov esi, [edx+ebp*4]      ; RVA del nombre
add esi, edi              ; nombre exportado
call decrypt_ascii_name   ; -> "LdrGetProcedureAddress"
call local_strcmp
...
movzx eax, word ptr [ordinals+ebp*2]
mov   eax, [functions+eax*4]
add   eax, edi            ; dirección final de la función

La misma búsqueda resulta más fácil de leer en C simplificado:

c
static void *resolve_export(uint8_t *module, const char *wanted) {
    IMAGE_DOS_HEADER *dos = (IMAGE_DOS_HEADER *)module;
    IMAGE_NT_HEADERS32 *nt =
        (IMAGE_NT_HEADERS32 *)(module + dos->e_lfanew);

    DWORD export_rva =
        nt->OptionalHeader
          .DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT]
          .VirtualAddress;

    IMAGE_EXPORT_DIRECTORY *exports =
        (IMAGE_EXPORT_DIRECTORY *)(module + export_rva);

    DWORD *names = (DWORD *)(module + exports->AddressOfNames);
    WORD *ordinals =
        (WORD *)(module + exports->AddressOfNameOrdinals);
    DWORD *functions =
        (DWORD *)(module + exports->AddressOfFunctions);

    for (DWORD i = 0; i < exports->NumberOfNames; i++) {
        char *name = (char *)(module + names[i]);
        if (local_strcmp(name, wanted) == 0) {
            WORD ordinal = ordinals[i];
            return module + functions[ordinal];
        }
    }

    return NULL;
}

Una vez disponible LdrGetProcedureAddress, el arranque se vuelve mucho más sencillo. AnyDesk la utiliza para resolver el resto de su pequeña tabla de ntdll, que incluye LdrLoadDll, NtProtectVirtualMemory y NtMapViewOfSection. Después obtiene kernel32.dll y resuelve funciones como HeapAlloc, GetProcessHeap, VirtualProtect y GetModuleHandleW.

text
recorrido de la PEB
  -> base de ntdll
  -> búsqueda manual en la EAT
  -> LdrGetProcedureAddress
  -> resto de funciones de ntdll
  -> LdrLoadDll / kernel32
  -> API de heap, línea de comandos, módulos y protecciones

Nombres cifrados

Es tentador llamar «API hashing» a cualquier resolver de este tipo, pero aquí sería incorrecto.

El API hashing guarda un entero fijo para cada función deseada. El resolver calcula el hash de cada nombre exportado y compara enteros. AnyDesk, en cambio, almacena bytes cifrados de forma reversible, los descifra y realiza comparaciones normales de cadenas.

Entre otros, recuperamos estos nombres:

text
ntdll
kernel32
LdrGetProcedureAddress
LdrLoadDll
NtProtectVirtualMemory
HeapAlloc
VirtualProtect
GetModuleHandleW

La diferencia es importante durante el análisis. El hashing nos obliga a reproducir un algoritmo de hash. Esta muestra nos obliga a reproducir un flujo de claves.

Descifrado de datos

Las funciones auxiliares que descifran nombres y el payload grande usan la misma construcción básica: un generador congruencial lineal, o LCG, produce un byte de clave cada vez y ese byte se combina con el texto cifrado mediante XOR.

text
state = state * 0x0019660D + 0x3C6EF35F
key   = (state >> 12) & 0xFF
plain = cipher XOR key

La actualización del estado es determinista y cada semilla está presente en el código de la primera etapa.

El bucle del payload

Este es el bucle completo de descifrado del payload de la muestra, con las direcciones sustituidas por etiquetas:

x86asm
push esi
mov  edx, data_section      ; inicio de .data en memoria
mov  ecx, 0x0000727F        ; semilla
mov  esi, encrypted_size    ; número de bytes cifrados

decrypt_loop:
imul ecx, ecx, 0x0019660D
add  ecx, 0x3C6EF35F
mov  eax, ecx
shr  eax, 0x0C
xor  byte ptr [edx], al
inc  edx
sub  esi, 1
jne  decrypt_loop
pop  esi
ret

A un nivel más alto, podría expresarse así:

c
static void decrypt_payload(uint8_t *data) {
    uint32_t state = 0x727f;

    for (uint32_t i = 0; i < 0x7e7f32; i++) {
        state = state * 0x0019660d + 0x3c6ef35f;
        data[i] ^= (uint8_t)(state >> 12);
    }
}

Esto es ofuscación, no criptografía segura. El algoritmo, la semilla y el texto cifrado están presentes en el archivo, y no existe ninguna etiqueta de autenticación. Aun así, consigue varios efectos útiles para un empaquetador: las cadenas desaparecen, el payload deja de tener una cabecera reconocible y los análisis básicos de importaciones o firmas revelan poco sobre la aplicación real.

Descompresión de datos

Descifrar el bloque solo expone la siguiente capa: un flujo comprimido.

AnyDesk utiliza LZMA (algoritmo de cadena de Markov Lempel-Ziv), el algoritmo de compresión conocido sobre todo por 7-Zip. LZMA combina coincidencias en un diccionario con codificación por rangos. Es más lento y consume más memoria que formatos más simples, pero suele obtener una relación de compresión alta. Esto resulta útil cuando un proveedor quiere distribuir una aplicación grande dentro de un envoltorio mucho menor.

Se trata de un flujo LZMA1 sin procesar, no de un contenedor .7z o .xz. Un contenedor normalmente incluiría metadatos descriptivos. Un decodificador de un flujo sin procesar necesita, en cambio, las propiedades del modelo LZMA, el tamaño del diccionario, los datos comprimidos y el tamaño de salida esperado. En esta muestra, el flujo no tiene marcador de fin, por lo que el tamaño de salida conocido también indica al decodificador cuándo debe detenerse.

Cómo conocemos las propiedades

Después del bucle XOR, el inicio de .data descifrado es:

text
5D 00 00 00 04

Esos cinco bytes siguen el formato esperado por el SDK de LZMA:

  1. El primer byte empaqueta lc, lp y pb:

    text
    property = (pb * 5 + lp) * 9 + lc
    0x5D     = (2  * 5 + 0)  * 9 + 3

    lc controla cuántos bits altos del literal anterior aportan contexto, lp añade bits bajos de la posición al contexto del literal y pb controla el modelo de posición empleado para las coincidencias.

  2. Los cuatro bytes siguientes representan el tamaño del diccionario en little-endian. En este caso, 00 00 00 04 equivale a 64 MiB.

Campo Valor
lc, lp, pb 3, 0, 2
Tamaño del diccionario 64 MiB
Bytes comprimidos después de las propiedades 0x7E7F2D
Tamaño de salida esperado 0x1D13C00 (30.489.600 bytes)

Estos valores no son una suposición basada únicamente en los bytes. El código de la primera etapa descifra el payload, trata sus primeros cinco bytes como un bloque de propiedades, pasa el resto a su decodificador LZMA y reserva el tamaño esperado para los datos decodificados. El formato estándar de las propiedades, los argumentos del código que llama al decodificador y la decodificación correcta de 30.489.600 bytes coinciden.

La orquestación de la primera etapa muestra el orden directamente:

x86asm
call decrypt_payload
mov  ebx, decoded_size
call heap_alloc
...
call lzma_decode
...
push esi                 ; búfer decodificado
call x86_bcj_convert
                         ; el PE está listo cuando la función retorna

El paso BCJ

La salida de LZMA todavía no es definitiva. AnyDesk también utiliza el conversor estándar de ramas x86 BCJ.

Las instrucciones x86 cercanas CALL y JMP, identificadas normalmente por los opcodes E8 y E9, almacenan un desplazamiento con signo de 32 bits relativo al final de la instrucción:

text
target = address_after_instruction + relative_displacement
relative_displacement = target - address_after_instruction

Esta representación es cómoda durante la ejecución, pero dificulta la compresión. Dos llamadas que llegan a la misma función suelen contener bytes de desplazamiento diferentes porque las instrucciones están en posiciones distintas. Por tanto, LZMA encuentra menos secuencias de bytes repetidas de las que sugeriría la estructura del código fuente.

Antes de comprimir, el codificador BCJ busca instrucciones E8 y E9 aptas y convierte sus operandos relativos en valores normalizados según la posición. Conceptualmente:

text
empaquetado:    operando_normalizado = desplazamiento_relativo + posicion_final
desempaquetado: desplazamiento_relativo = operando_normalizado - posicion_final

El conversor real también comprueba los rangos de los operandos y sus bytes altos para no modificar a ciegas cada byte E8 o E9 que encuentra. Los valores normalizados hacen que las ramas hacia destinos relacionados se parezcan más entre sí, lo que permite a LZMA encontrar mejores coincidencias. BCJ no comprime nada por sí solo ni proporciona confidencialidad; es un filtro reversible de preprocesamiento.

Tras decodificar LZMA, todavía hay que restaurar esos operandos normalizados o el flujo de control del programa sería incorrecto. La rutina de esta muestra coincide con el diseño x86_Convert del SDK de LZMA y se invoca con el indicador de codificación a cero, lo que significa decodificar.

Obtención del PE real

Hay dos formas prácticas de recuperar el ejecutable interno.

La ruta estática sigue las transformaciones que acabamos de identificar:

text
bytes cifrados del payload en .data
  -> LCG-XOR con la semilla 0x727F
  -> lectura del bloque de propiedades LZMA de cinco bytes
  -> decodificación LZMA sin procesar a 0x1D13C00 bytes
  -> reversión de BCJ x86
  -> archivo PE32 sin mapear

La ruta dinámica es más corta. Un depurador puede detenerse justo después de que retorne la rutina BCJ y volcar de la memoria el búfer de salida completo. Así se recupera el mismo archivo. Mencionamos esta posibilidad como método alternativo de verificación, sin convertir el artículo en una guía paso a paso para realizar el volcado.

El archivo recuperado se verifica así:

Propiedad Valor
Tamaño 30.489.600 bytes
MD5 c280dca9bf3927c8238854cf9dbc62e3
SHA-1 5840c5218c0c31d82b1b4308a0979f384b76b75c
SHA-256 4117961150ab001e524a39be50250d221662f02b302c98e94248fc6ec450f282

Mapeo del PE

Recuperar un archivo PE y conseguir que pueda ejecutarse son tareas distintas. Un archivo almacena las secciones de acuerdo con PointerToRawData; una imagen mapeada las coloca en su VirtualAddress.

El ejecutable externo ya había reservado una región .itext grande, inicializada a cero. La primera etapa cambia esa región a PAGE_EXECUTE_READWRITE, valida las cabeceras recuperadas, copia las cabeceras del PE y después copia cada sección en su posición virtual.

El código real de mapeo permite ver la transición:

x86asm
push 0x40                  ; PAGE_EXECUTE_READWRITE
push dword ptr [ebp+0x1C]  ; tamaño de la región reservada
...
push edi                   ; destino .itext
call eax                   ; VirtualProtect resuelto

mov  eax, 0x5A4D
cmp  word ptr [edx], ax       ; "MZ"
mov  ecx, [edx+0x3C]          ; e_lfanew
add  ecx, edx                 ; cabeceras NT
cmp  dword ptr [ecx], 0x4550  ; "PE\0\0"

push eax                   ; SizeOfHeaders
push edx                   ; PE sin mapear
push edi                   ; destino mapeado
call local_memcpy

push dword ptr [edi-0x04]  ; SizeOfRawData
mov  eax, [esi+0x0C]       ; base del PE sin mapear
add  eax, [edi]            ; + PointerToRawData
push eax                   ; origen
mov  eax, [edi-0x08]       ; VirtualAddress
add  eax, [esi+0x20]       ; + base mapeada
push eax                   ; destino
call local_memcpy

Al final de esta función, .itext se parece a un PE mapeado, pero todavía no está listo para ejecutarse. La primera etapa aún no ha:

  • aplicado las relocalizaciones de base;
  • cargado las dependencias de la imagen interna;
  • resuelto sus importaciones;
  • rellenado su IAT; ni
  • aplicado las protecciones finales de cada sección.

Ese trabajo pertenece al cargador incluido dentro del PE recuperado.

La transición

La imagen interna exporta tres nombres útiles:

Exportación Función
loader_main_thunk Puente hacia el inicio de la aplicación
ldr_thunk_data Contexto compartido del cargador
loader_entry_thread_thunk Entrada al cargador interno

La primera etapa analiza la EAT interna para encontrar ldr_thunk_data. Rellena esa estructura con la base mapeada, la línea de comandos, campos de estado, datos de protección de páginas y los punteros a funciones nativas que resolvió antes. Después resuelve y llama directamente a loader_entry_thread_thunk.

Segunda etapa

El cargador interno termina ahora las tareas que normalmente realiza el cargador de PE de Windows.

Relocalizaciones

La imagen recuperada registra una base preferida en sus cabeceras PE, pero la primera etapa la colocó dentro de la región .itext de la imagen externa. La segunda etapa calcula la diferencia y recorre el directorio de relocalizaciones de base, aplicando correcciones x86 IMAGE_REL_BASED_HIGHLOW.

Sin este paso, las direcciones absolutas compiladas para la base preferida apuntarían a posiciones de memoria incorrectas.

Reparación de importaciones

A diferencia del envoltorio externo, el PE interno tiene un directorio de importaciones normal: 23 DLL y 767 símbolos importados.

Para cada descriptor de importación, la segunda etapa:

  1. Carga o localiza la DLL solicitada mediante el callback LdrLoadDll proporcionado.
  2. Lee cada nombre u ordinal solicitado desde el thunk de búsqueda.
  3. Lo resuelve con LdrGetProcedureAddress.
  4. Escribe la dirección resultante en FirstThunk, es decir, en la IAT.

Es el mismo enlace que Windows suele realizar antes de iniciar un ejecutable. La diferencia es que el cargador interno de AnyDesk lo hace manualmente.

La primera etapa lee las EAT para arrancar. La segunda escribe la IAT de la imagen interna para que sus puntos de llamada normales funcionen.

Protecciones finales

La región .itext completamente RWX resulta útil mientras se construye la imagen. Finalmente, la segunda etapa convierte los indicadores de las secciones PE en protecciones de página normales: código ejecutable y legible, datos de solo lectura, y datos legibles y escribibles.

¿Por qué hacerlo así?

El análisis estático puede mostrar qué consigue el diseño, pero no qué requisito consideraron más importante sus desarrolladores.

Los resultados visibles son claros:

  • La imagen interna de 30,49 MB se distribuye dentro de un ejecutable externo de 8,34 MB.
  • AnyDesk sigue siendo un único archivo portable.
  • El proveedor controla la transición entre el contenedor y la aplicación.
  • Un análisis estático superficial ve un cargador diminuto en lugar del programa completo.

Son ventajas normales de ingeniería de producto con efectos secundarios que se parecen a la evasión de malware. Ambas afirmaciones pueden ser ciertas al mismo tiempo.

Conclusión

El ejecutable externo de AnyDesk es un contenedor y un cargador de arranque, no la aplicación completa.

Comienza sin importaciones normales, por lo que recorre la PEB para encontrar ntdll. Analiza la EAT para resolver su primera API del cargador, construye el resto de su tabla de API, descifra mediante LCG-XOR el flujo incrustado en .data, lo expande con LZMA, revierte el filtro BCJ x86 y mapea el PE resultante en .itext.

Después, el cargador interno aplica las relocalizaciones, resuelve 767 importaciones de 23 DLL, establece las protecciones finales y entra en la aplicación real.

La secuencia resulta familiar porque el malware utiliza los mismos componentes. La habilidad útil no consiste en memorizar qué técnicas son «malas», sino en poder explicar qué consigue cada instrucción, cómo una etapa crea las condiciones para la siguiente y qué dice sobre el archivo el conjunto de las evidencias.

Referencias

  1. ired.team, Exploring Process Environment Block.
  2. Microsoft, Formato PE.
  3. Microsoft, estructura PEB y estructura PEB_LDR_DATA.
  4. Microsoft, Información sobre la firma de archivos ejecutables.
  5. 7-Zip, SDK de LZMA y conversor BCJ x86.
  6. AnyDesk, página de descarga para Windows y registro de cambios para Windows.
  7. MITRE ATT&CK, Software Packing (T1027.002), Dynamic API Resolution (T1027.007), Embedded Payloads (T1027.009) y Encrypted/Encoded File (T1027.013).