Dentro de MS-SHLLINK: una visita campo por campo al formato binario .lnk
· 6 min de lectura
El formato Shell Link de Windows es uno de esos raros casos en los que lo que ves en Explorer está completa y públicamente especificado. Microsoft lo documenta como [MS-SHLLINK], Shell Link (.LNK) Binary File Format, y cualquier parser conforme lo sigue byte por byte. Abajo está el recorrido práctico, en el orden en que un parser realmente lee el archivo. Marcaré los lugares en los que los parsers comúnmente se equivocan y los campos que un analista DFIR debería tener en cuenta.
1. ShellLinkHeader (76 bytes)
La cabecera de tamaño fijo con la que abre cada .lnk.
- HeaderSize, siempre
0x0000004C(76). Los cuatro bytes little-endian4C 00 00 00son la prueba olfativa Shell Link más fiable. Cualquier otra cosa no es un.lnk, sea lo que diga la extensión. - LinkCLSID,
00021401-0000-0000-C000-000000000046, el id de clase COM para objetos ShellLink. Almacenado en little-endian-mezclado según la convención COM; los bytes no están en el orden que sugiere la forma legible. - LinkFlags, 32 bits que deciden qué secciones opcionales siguen. Bits que vale la pena conocer a primera vista:
HasLinkTargetIDList,HasLinkInfo,HasName,HasRelativePath,HasWorkingDir,HasArguments,HasIconLocation,IsUnicode,ForceNoLinkInfo,HasExpString,RunInSeparateProcess,HasExpIcon. - FileAttributes, 32 bits que reflejan los atributos NTFS del destino (READONLY, HIDDEN, SYSTEM, DIRECTORY, ARCHIVE).
- CreationTime / AccessTime / WriteTime, tres valores FILETIME de 64 bits. Tics de 100 ns desde 1601-01-01 UTC. Instantáneas tomadas en la creación del enlace; no se actualizan después. Una discrepancia entre los FILETIMEs de la cabecera y los metadatos actuales del destino te dice cuándo se escribió el enlace por primera vez, que frecuentemente es la marca temporal más útil.
- FileSize, IconIndex, ShowCommand, HotKey, más bytes reservados que deben ser cero.
ShowCommand = 7 (minimizado) en un .lnk que apunta a cmd.exe es un fuerte indicador de malware. Vale la pena marcar pronto.
2. LinkTargetIDList (opcional)
Presente si HasLinkTargetIDList. Una secuencia con prefijo de longitud de registros ItemID que recorre desde una carpeta raíz de la shell (Escritorio, Mi PC, Red) a través de cada paso del namespace shell hasta el destino. Cada ItemID tiene su propio prefijo de longitud; la lista termina con un terminador de longitud cero.
La codificación exacta de cada ItemID depende del tipo de shell item, unidad, entrada de sistema de archivos, applet del panel de control, recurso compartido de red, y es parcialmente opaca a la especificación del formato. LECmd de Eric Zimmerman y liblnk de libyal implementan ambos los tipos comunes de shell item; lnkparse3 cubre menos. Cuando el recorrido IDList decodifica limpio pero LinkInfo está ausente o ForceNoLinkInfo está activado, el IDList es tu único camino al destino, y una decodificación parcial aún puede producir una cadena de ruta utilizable.
3. LinkInfo (opcional)
Presente si HasLinkInfo y ForceNoLinkInfo está borrado. Lleva la información necesaria para resolver el destino cuando el recorrido IDList falla o está ausente.
- VolumeID, tipo de unidad, número de serie, etiqueta de volumen. El serial es tu clave de correlación de medios extraíbles.
- LocalBasePath, la ruta absoluta en la máquina de origen. Normalmente contiene el nombre de usuario, que es una señal suave de atribución.
- CommonNetworkRelativeLink, ruta UNC y tipo de proveedor de red para accesos directos que vivieron en un recurso compartido de red.
- CommonPathSuffix, la cola más allá del LocalBasePath o NetName.
Variantes Unicode de cada ruta existen junto a las variantes ANSI, presentes dependiendo del flag de versión dentro de la cabecera LinkInfo. Un parser que solo lee las variantes ANSI representará mal las rutas no ASCII.
4. StringData (opcional)
Una secuencia de cadenas con prefijo de longitud, presentes en este orden si el LinkFlag correspondiente está establecido:
NAME_STRING → RELATIVE_PATH → WORKING_DIR → COMMAND_LINE_ARGUMENTS → ICON_LOCATION
Cada cadena tiene prefijo de longitud (little-endian de 16 bits, en caracteres, no bytes) y codificada como UTF-16LE si IsUnicode está establecido, de lo contrario como la página de códigos ANSI del sistema. El orden importa absolutamente. Un parser que no rastrea qué LinkFlags se establecieron leerá mal cada cadena posterior. Este es el bug más común en los parsers LNK caseros.
Para DFIR: COMMAND_LINE_ARGUMENTS es donde viven los argumentos de la carga útil de phishing. ICON_LOCATION es donde vive la ruta del icono falsificado. WORKING_DIR a menudo revela letras de unidad USB y carpetas de staging.
5. Bloques ExtraData (opcional, repetidos)
Cero o más bloques ExtraData, cada uno (size, signature, payload). La firma selecciona el tipo de bloque:
| Firma | Bloque |
|---|---|
| 0xA0000001 | EnvironmentVariableDataBlock |
| 0xA0000002 | ConsoleDataBlock |
| 0xA0000003 | TrackerDataBlock |
| 0xA0000004 | ConsoleFEDataBlock |
| 0xA0000005 | SpecialFolderDataBlock |
| 0xA0000006 | DarwinDataBlock |
| 0xA0000007 | IconEnvironmentDataBlock |
| 0xA0000008 | ShimDataBlock |
| 0xA0000009 | PropertyStoreDataBlock |
| 0xA000000B | KnownFolderDataBlock |
| 0xA000000C | VistaAndAboveIDListDataBlock |
La lista termina con un TerminalBlock de 4 bytes (size < 0x4). Itera, enruta según la firma, decodifica la carga útil en consecuencia.
El famoso en forense es TrackerDataBlock. Registra el nombre NetBIOS de la máquina de origen y un GUID droid Distributed Link Tracking derivado de la dirección MAC. El droid es un UUID v1; los últimos seis bytes son el MAC. Ahí es de donde vienen las victorias de atribución.
PropertyStoreDataBlock es un property store serializado con valores arbitrarios indexados por GUID. A veces lleva una ruta autoritativa que el LinkInfo no tiene. EnvironmentVariableDataBlock se resuelve en tiempo de ejecución, un %TEMP% literal es ruido benigno; un %TEMP%\evil.dll literal no lo es. DarwinDataBlock es el descriptor del instalador MSI; aparece en accesos directos legítimos a aplicaciones instaladas por MSI y se ve raro si nunca lo has visto antes.
Juntándolo todo
Un parser es una máquina de estados dirigida por LinkFlags. Cada flag activa o desactiva una sección descendente. El orden no es negociable. La especificación es estricta sobre los bytes reservados-deben-ser-cero, así que un solo bit de flag corrupto corrompe la longitud de cada sección posterior.
Parsers de referencia que vale la pena diff-testear tu salida contra: LECmd de Eric Zimmerman, liblnk de libyal, lnkparse3, y la Windows-LNK-Parsing-Library. Cuando dos de ellos coinciden y un tercero discrepa, gana la spec.
Para ver todo esto en un archivo real, suelta uno en el parser de la página de inicio, cada campo arriba se renderiza explícitamente.
Lecturas adicionales
- Microsoft, [MS-SHLLINK] en Microsoft Learn, la spec autoritativa.
- ¿Qué es un archivo .lnk de Windows?, introducción corta.
- Cómo abrir un archivo .lnk, caminos de inspección seguros.
- Análisis forense de archivos .lnk, lo que los investigadores extraen de cada bloque.
- El parser Jumplist, para los flujos LNK incrustados dentro de archivos
.automaticDestinations-msy.customDestinations-ms.