Files
reverse-engineering-anything/tests/fixtures/binaryDiagnostics/windowsShortcut.ts
T
RRFZ b4c3a028fe fix(windows): leave a shortcut icon comparison unknown across code pages (#9691)
* fix(windows): leave a shortcut icon comparison unknown across code pages

Windows stores the LinkInfo LocalBasePath only as ANSI when the system
code page can represent it, while StringData stays UTF-16. The parser
projects the ANSI bytes as Latin1, so on a code page 936 host a path such
as F:\百度网盘\BaiduNetdisk\BaiduNetdisk.exe never matched the same
Unicode IconLocation, and icon_location_differs_from_target reported true
for a shortcut whose icon is its own target.

Record whether each target path is an ANSI projection. Compare it with
the icon only when both use the same encoding or the ANSI text is ASCII.
A full target that cannot be compared makes the result null, like a
%VAR% target.

* fix(windows): keep each LinkInfo part's encoding when joining paths

A joined LinkInfo target was marked ANSI when any part was ANSI. An ASCII
ANSI share with a UTF-16 suffix such as \\server\share + 工具\app.exe
then could not be compared with a Unicode icon, although no byte needs a
code page. Record which encoding each part's non-ASCII text came from,
and compare two paths when their non-ASCII text shares one encoding.
2026-11-10 15:39:04 -05:00

598 lines
20 KiB
TypeScript

/** Synthesize Shell Link bytes for parser, CLI, and MCP tests. */
export const SHELL_LINK_CLSID = Buffer.from([
0x01, 0x14, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x46,
]);
export const MY_COMPUTER_CLSID = Buffer.from([
0xe0, 0x4f, 0xd0, 0x20, 0xea, 0x3a, 0x69, 0x10, 0xa2, 0xd8, 0x08, 0x00, 0x2b,
0x30, 0x30, 0x9d,
]);
/** 2024-01-15T12:00:00.000Z as FILETIME 100-nanosecond intervals. */
export const FIXTURE_FILETIME = 133497936000000000n;
export const LINK_FLAG = {
HasLinkTargetIDList: 0x00000001,
HasLinkInfo: 0x00000002,
HasName: 0x00000004,
HasRelativePath: 0x00000008,
HasWorkingDir: 0x00000010,
HasArguments: 0x00000020,
HasIconLocation: 0x00000040,
IsUnicode: 0x00000080,
HasExpString: 0x00000200,
HasExpIcon: 0x00004000,
} as const;
export const EXTRA_SIGNATURE = {
EnvironmentVariable: 0xa0000001,
Tracker: 0xa0000003,
ConsoleFE: 0xa0000004,
SpecialFolder: 0xa0000005,
IconEnvironment: 0xa0000007,
PropertyStore: 0xa0000009,
KnownFolder: 0xa000000b,
} as const;
export const TRACKER_MAC = Buffer.from([0x00, 0x11, 0x22, 0x33, 0x44, 0x55]);
/** UUID timestamp 100ns since 1582-10-15 matching 2024-01-15T12:00:00.000Z. */
export const TRACKER_UUID_TIMESTAMP = 139289664000000000n;
export type FixtureIdListItem =
| {
readonly kind: "root";
readonly sortIndex?: number;
readonly folderId?: Buffer;
}
| { readonly kind: "volume"; readonly name: string }
| {
readonly kind: "file";
readonly classType?: number;
readonly fileSize?: number;
readonly shortName: string;
}
| { readonly kind: "raw"; readonly data: Buffer };
export type FixtureLinkInfo = {
readonly driveType?: number;
readonly serial?: number;
readonly label?: string;
/** Bytes between the 16-byte header and an ANSI label. */
readonly volumeLabelGap?: Buffer;
readonly localBasePath?: string;
readonly commonPathSuffix?: string;
readonly netName?: string;
readonly deviceName?: string;
readonly networkProviderType?: number;
readonly networkFlags?: number;
/**
* Write LinkInfo's Unicode fields: header 0x24, Unicode volume label and
* network names. Defaults to the link's IsUnicode.
*/
readonly unicode?: boolean;
/** Write Unicode network names; defaults to `unicode`. */
readonly networkUnicode?: boolean;
};
export type FixtureExtraBlock =
| {
readonly kind: "environment";
readonly ansi?: string;
readonly unicode?: string;
}
| {
readonly kind: "icon_environment";
readonly ansi?: string;
readonly unicode?: string;
}
| {
readonly kind: "tracker";
readonly length?: number;
readonly version?: number;
readonly machineId?: string;
readonly droid?: readonly [Buffer, Buffer];
readonly birthDroid?: readonly [Buffer, Buffer];
}
| {
readonly kind: "known_folder";
readonly folderId?: Buffer;
readonly itemOffset?: number;
}
| {
readonly kind: "special_folder";
readonly folderId?: number;
readonly itemOffset?: number;
}
| { readonly kind: "console_fe"; readonly codePage?: number }
| { readonly kind: "property_store"; readonly payload?: Buffer }
| {
readonly kind: "unknown";
readonly signature: number;
readonly payload?: Buffer;
};
export type WindowsShortcutFixtureOptions = {
readonly headerSize?: number;
readonly clsid?: Buffer;
readonly flags?: number;
readonly fileAttributes?: number;
readonly creationTime?: bigint;
readonly accessTime?: bigint;
readonly writeTime?: bigint;
readonly fileSize?: number;
readonly iconIndex?: number;
readonly showCommand?: number;
readonly hotKey?: number;
readonly unicode?: boolean;
readonly idList?: readonly FixtureIdListItem[] | null;
readonly linkInfo?: FixtureLinkInfo | null;
readonly name?: string;
readonly relativePath?: string;
readonly workingDir?: string;
readonly arguments?: string;
readonly iconLocation?: string;
readonly extraData?: readonly FixtureExtraBlock[];
readonly terminalBlock?: boolean;
readonly truncateAt?: number;
readonly omitStringBytes?: number;
};
const ansiZ = (value: string): Buffer =>
Buffer.concat([Buffer.from(value, "latin1"), Buffer.from([0])]);
const utf16z = (value: string): Buffer =>
Buffer.concat([Buffer.from(value, "utf16le"), Buffer.alloc(2)]);
const padField = (
value: string,
bytes: number,
encoding: "latin1" | "utf16le",
) => {
const field = Buffer.alloc(bytes);
Buffer.from(value, encoding).copy(field);
return field;
};
const writeU32 = (target: Buffer, offset: number, value: number): void => {
target.writeUInt32LE(value >>> 0, offset);
};
const writeU64 = (target: Buffer, offset: number, value: bigint): void => {
target.writeBigUInt64LE(value, offset);
};
/** Windows GUID bytes for a version-1 UUID with the supplied node and timestamp. */
export const uuidV1Bytes = (
timestamp100ns: bigint,
clockSeq: number,
node: Buffer,
): Buffer => {
const bytes = Buffer.alloc(16);
bytes.writeUInt32LE(Number(timestamp100ns & 0xffffffffn), 0);
bytes.writeUInt16LE(Number((timestamp100ns >> 32n) & 0xffffn), 4);
bytes.writeUInt16LE(Number((timestamp100ns >> 48n) & 0x0fffn) | 0x1000, 6);
bytes[8] = ((clockSeq >> 8) & 0x3f) | 0x80;
bytes[9] = clockSeq & 0xff;
node.copy(bytes, 10, 0, 6);
return bytes;
};
const defaultTrackerGuid = (clockSeq: number): Buffer =>
uuidV1Bytes(TRACKER_UUID_TIMESTAMP, clockSeq, TRACKER_MAC);
const itemBytes = (item: FixtureIdListItem): Buffer => {
if (item.kind === "raw") {
const size = Buffer.alloc(2);
size.writeUInt16LE(item.data.length + 2, 0);
return Buffer.concat([size, item.data]);
}
if (item.kind === "root") {
const data = Buffer.alloc(18);
data[0] = 0x1f;
data[1] = item.sortIndex ?? 0x50;
(item.folderId ?? MY_COMPUTER_CLSID).copy(data, 2);
const size = Buffer.alloc(2);
size.writeUInt16LE(20, 0);
return Buffer.concat([size, data]);
}
if (item.kind === "volume") {
const name = Buffer.from(`${item.name}\0`, "latin1");
const data = Buffer.concat([Buffer.from([0x2f]), name]);
const size = Buffer.alloc(2);
size.writeUInt16LE(data.length + 2, 0);
return Buffer.concat([size, data]);
}
const shortName = Buffer.from(`${item.shortName}\0`, "latin1");
const padded =
shortName.length % 2 === 0
? shortName
: Buffer.concat([shortName, Buffer.from([0])]);
const data = Buffer.alloc(12 + padded.length);
data[0] = item.classType ?? 0x32;
data.writeUInt32LE(item.fileSize ?? 0, 2);
padded.copy(data, 12);
const size = Buffer.alloc(2);
size.writeUInt16LE(data.length + 2, 0);
return Buffer.concat([size, data]);
};
const idListBytes = (items: readonly FixtureIdListItem[]): Buffer => {
const body = Buffer.concat([...items.map(itemBytes), Buffer.alloc(2)]);
const size = Buffer.alloc(2);
size.writeUInt16LE(body.length, 0);
return Buffer.concat([size, body]);
};
const volumeIdBytes = (info: FixtureLinkInfo, unicode: boolean): Buffer => {
const label = unicode
? utf16z(info.label ?? "VOL")
: ansiZ(info.label ?? "VOL");
const gap = info.volumeLabelGap ?? Buffer.alloc(0);
const header = unicode ? 20 : 16 + gap.length;
const bytes = Buffer.alloc(header + label.length);
writeU32(bytes, 0, bytes.length);
writeU32(bytes, 4, info.driveType ?? 3);
writeU32(bytes, 8, info.serial ?? 0x1234abcd);
writeU32(bytes, 12, unicode ? 0x14 : header);
if (unicode) writeU32(bytes, 16, header);
else gap.copy(bytes, 16);
label.copy(bytes, header);
return bytes;
};
const networkLinkBytes = (info: FixtureLinkInfo, unicode: boolean): Buffer => {
const netAnsi = ansiZ(info.netName ?? "\\\\server\\share");
const deviceAnsi = ansiZ(info.deviceName ?? "Z:");
const netWide = unicode
? utf16z(info.netName ?? "\\\\server\\share")
: Buffer.alloc(0);
const deviceWide = unicode
? utf16z(info.deviceName ?? "Z:")
: Buffer.alloc(0);
const header = unicode ? 0x1c : 0x14;
const deviceAnsiOffset = header + netAnsi.length;
const netWideOffset = deviceAnsiOffset + deviceAnsi.length;
const deviceWideOffset = netWideOffset + netWide.length;
const bytes = Buffer.alloc(deviceWideOffset + deviceWide.length);
writeU32(bytes, 0, bytes.length);
writeU32(bytes, 4, info.networkFlags ?? 0x3);
writeU32(bytes, 8, header);
writeU32(bytes, 12, deviceAnsiOffset);
writeU32(bytes, 16, info.networkProviderType ?? 0x00020000);
if (unicode) {
writeU32(bytes, 20, netWideOffset);
writeU32(bytes, 24, deviceWideOffset);
}
netAnsi.copy(bytes, header);
deviceAnsi.copy(bytes, deviceAnsiOffset);
netWide.copy(bytes, netWideOffset);
deviceWide.copy(bytes, deviceWideOffset);
return bytes;
};
const linkInfoBytes = (info: FixtureLinkInfo, unicode: boolean): Buffer => {
const hasVolume =
info.localBasePath !== undefined || info.label !== undefined;
const hasNetwork =
info.netName !== undefined || info.deviceName !== undefined;
const flags = (hasVolume ? 0x1 : 0) | (hasNetwork ? 0x2 : 0) || 0x1;
const headerSize = unicode ? 0x24 : 0x1c;
const volume = flags & 0x1 ? volumeIdBytes(info, unicode) : Buffer.alloc(0);
const localAnsi =
flags & 0x1
? ansiZ(info.localBasePath ?? "C:\\target.exe")
: Buffer.alloc(0);
const network =
flags & 0x2
? networkLinkBytes(info, info.networkUnicode ?? unicode)
: Buffer.alloc(0);
const suffixAnsi = ansiZ(info.commonPathSuffix ?? "");
const localWide =
unicode && flags & 0x1
? utf16z(info.localBasePath ?? "C:\\target.exe")
: Buffer.alloc(0);
const suffixWide = unicode
? utf16z(info.commonPathSuffix ?? "")
: Buffer.alloc(0);
let cursor = headerSize;
const volumeOffset = flags & 0x1 ? cursor : 0;
if (flags & 0x1) cursor += volume.length;
const localOffset = flags & 0x1 ? cursor : 0;
if (flags & 0x1) cursor += localAnsi.length;
const networkOffset = flags & 0x2 ? cursor : 0;
if (flags & 0x2) cursor += network.length;
const suffixOffset = cursor;
cursor += suffixAnsi.length;
const localWideOffset = unicode && flags & 0x1 ? cursor : 0;
if (unicode && flags & 0x1) cursor += localWide.length;
const suffixWideOffset = unicode ? cursor : 0;
if (unicode) cursor += suffixWide.length;
const bytes = Buffer.alloc(cursor);
writeU32(bytes, 0, cursor);
writeU32(bytes, 4, headerSize);
writeU32(bytes, 8, flags);
writeU32(bytes, 12, volumeOffset);
writeU32(bytes, 16, localOffset);
writeU32(bytes, 20, networkOffset);
writeU32(bytes, 24, suffixOffset);
if (unicode) {
writeU32(bytes, 28, localWideOffset);
writeU32(bytes, 32, suffixWideOffset);
}
if (volumeOffset > 0) volume.copy(bytes, volumeOffset);
if (localOffset > 0) localAnsi.copy(bytes, localOffset);
if (networkOffset > 0) network.copy(bytes, networkOffset);
suffixAnsi.copy(bytes, suffixOffset);
if (localWideOffset > 0) localWide.copy(bytes, localWideOffset);
if (suffixWideOffset > 0) suffixWide.copy(bytes, suffixWideOffset);
return bytes;
};
/** StringData is counted, not NUL-terminated (MS-SHLLINK 2.4). */
const stringDataField = (value: string, unicode: boolean): Buffer => {
const text = Buffer.from(value, unicode ? "utf16le" : "latin1");
const count = Buffer.alloc(2);
count.writeUInt16LE(value.length, 0);
return Buffer.concat([count, text]);
};
const environmentBlock = (
signature: number,
ansi: string,
unicode: string,
): Buffer => {
const bytes = Buffer.alloc(0x314);
writeU32(bytes, 0, 0x314);
writeU32(bytes, 4, signature);
padField(ansi, 260, "latin1").copy(bytes, 8);
padField(unicode, 520, "utf16le").copy(bytes, 268);
return bytes;
};
const extraBlockBytes = (block: FixtureExtraBlock): Buffer => {
if (block.kind === "environment")
return environmentBlock(
EXTRA_SIGNATURE.EnvironmentVariable,
block.ansi ??
"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
block.unicode ??
"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
);
if (block.kind === "icon_environment")
return environmentBlock(
EXTRA_SIGNATURE.IconEnvironment,
block.ansi ?? "%SystemRoot%\\System32\\shell32.dll",
block.unicode ?? "%SystemRoot%\\System32\\shell32.dll",
);
if (block.kind === "tracker") {
const bytes = Buffer.alloc(0x60);
writeU32(bytes, 0, 0x60);
writeU32(bytes, 4, EXTRA_SIGNATURE.Tracker);
writeU32(bytes, 8, block.length ?? 0x58);
writeU32(bytes, 12, block.version ?? 0);
padField(block.machineId ?? "WORKSTATION", 16, "latin1").copy(bytes, 16);
const droid0 = block.droid?.[0] ?? defaultTrackerGuid(1);
const droid1 = block.droid?.[1] ?? defaultTrackerGuid(2);
const birth0 = block.birthDroid?.[0] ?? defaultTrackerGuid(3);
const birth1 = block.birthDroid?.[1] ?? defaultTrackerGuid(4);
droid0.copy(bytes, 32);
droid1.copy(bytes, 48);
birth0.copy(bytes, 64);
birth1.copy(bytes, 80);
return bytes;
}
if (block.kind === "known_folder") {
const bytes = Buffer.alloc(0x1c);
writeU32(bytes, 0, 0x1c);
writeU32(bytes, 4, EXTRA_SIGNATURE.KnownFolder);
(block.folderId ?? MY_COMPUTER_CLSID).copy(bytes, 8);
writeU32(bytes, 24, block.itemOffset ?? 0);
return bytes;
}
if (block.kind === "special_folder") {
const bytes = Buffer.alloc(0x10);
writeU32(bytes, 0, 0x10);
writeU32(bytes, 4, EXTRA_SIGNATURE.SpecialFolder);
writeU32(bytes, 8, block.folderId ?? 0x24);
writeU32(bytes, 12, block.itemOffset ?? 0);
return bytes;
}
if (block.kind === "console_fe") {
const bytes = Buffer.alloc(0x0c);
writeU32(bytes, 0, 0x0c);
writeU32(bytes, 4, EXTRA_SIGNATURE.ConsoleFE);
writeU32(bytes, 8, block.codePage ?? 437);
return bytes;
}
if (block.kind === "property_store") {
const payload = block.payload ?? Buffer.alloc(4);
const bytes = Buffer.alloc(8 + payload.length);
writeU32(bytes, 0, bytes.length);
writeU32(bytes, 4, EXTRA_SIGNATURE.PropertyStore);
payload.copy(bytes, 8);
return bytes;
}
const payload = block.payload ?? Buffer.from([0xde, 0xad]);
const bytes = Buffer.alloc(8 + payload.length);
writeU32(bytes, 0, bytes.length);
writeU32(bytes, 4, block.signature);
payload.copy(bytes, 8);
return bytes;
};
const defaultIdList = (): readonly FixtureIdListItem[] => [
{ kind: "root" },
{ kind: "volume", name: "C:\\" },
{
kind: "file",
shortName: "POWERS~1.EXE",
fileSize: 450560,
classType: 0x32,
},
];
const derivedFlags = (
options: WindowsShortcutFixtureOptions,
unicode: boolean,
): number => {
if (options.flags !== undefined) return options.flags;
let flags = 0;
if (options.idList !== null) flags |= LINK_FLAG.HasLinkTargetIDList;
if (options.linkInfo !== null) flags |= LINK_FLAG.HasLinkInfo;
if (options.name !== undefined) flags |= LINK_FLAG.HasName;
if (options.relativePath !== undefined) flags |= LINK_FLAG.HasRelativePath;
if (options.workingDir !== undefined) flags |= LINK_FLAG.HasWorkingDir;
if (options.arguments !== undefined) flags |= LINK_FLAG.HasArguments;
if (options.iconLocation !== undefined) flags |= LINK_FLAG.HasIconLocation;
if (unicode) flags |= LINK_FLAG.IsUnicode;
const extra = options.extraData ?? [];
if (extra.some((block) => block.kind === "environment"))
flags |= LINK_FLAG.HasExpString;
if (extra.some((block) => block.kind === "icon_environment"))
flags |= LINK_FLAG.HasExpIcon;
return flags;
};
/** Build one Shell Link. Omitted options produce a unicode PowerShell shortcut. */
export const windowsShortcutBytes = (
options: WindowsShortcutFixtureOptions = {},
): Buffer => {
const unicode = options.unicode ?? true;
const name = options.name ?? "Windows PowerShell";
const relativePath =
options.relativePath ?? ".\\WindowsPowerShell\\v1.0\\powershell.exe";
const workingDir = options.workingDir ?? "C:\\Windows\\System32";
const argumentText =
options.arguments ??
'-WindowStyle Hidden -NoProfile -Command "Get-Process"';
const iconLocation =
options.iconLocation ?? "C:\\Windows\\System32\\shell32.dll";
const idList =
options.idList === undefined ? defaultIdList() : options.idList;
const linkInfo =
options.linkInfo === undefined
? {
driveType: 3,
serial: 0x9a8b7c6d,
label: "OS",
localBasePath:
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
commonPathSuffix: "",
}
: options.linkInfo;
const extraData =
options.extraData === undefined
? ([
{ kind: "environment" },
{ kind: "icon_environment" },
{ kind: "tracker" },
{ kind: "console_fe", codePage: 437 },
{ kind: "special_folder", folderId: 0x24 },
{ kind: "known_folder" },
{ kind: "property_store" },
] satisfies FixtureExtraBlock[])
: options.extraData;
const flags = derivedFlags(
{
...options,
name,
relativePath,
workingDir,
arguments: argumentText,
iconLocation,
},
unicode,
);
const header = Buffer.alloc(76);
writeU32(header, 0, options.headerSize ?? 0x4c);
(options.clsid ?? SHELL_LINK_CLSID).copy(header, 4);
writeU32(header, 20, flags);
writeU32(header, 24, options.fileAttributes ?? 0x20);
writeU64(header, 28, options.creationTime ?? FIXTURE_FILETIME);
writeU64(header, 36, options.accessTime ?? FIXTURE_FILETIME);
writeU64(header, 44, options.writeTime ?? FIXTURE_FILETIME);
writeU32(header, 52, options.fileSize ?? 450560);
header.writeInt32LE(options.iconIndex ?? 0, 56);
writeU32(header, 60, options.showCommand ?? 7);
header.writeUInt16LE(options.hotKey ?? 0, 64);
const parts: Buffer[] = [header];
if (flags & LINK_FLAG.HasLinkTargetIDList && idList !== null)
parts.push(idListBytes(idList));
if (flags & LINK_FLAG.HasLinkInfo && linkInfo !== null)
parts.push(linkInfoBytes(linkInfo, linkInfo.unicode ?? unicode));
const strings: Buffer[] = [];
if (flags & LINK_FLAG.HasName) strings.push(stringDataField(name, unicode));
if (flags & LINK_FLAG.HasRelativePath)
strings.push(stringDataField(relativePath, unicode));
if (flags & LINK_FLAG.HasWorkingDir)
strings.push(stringDataField(workingDir, unicode));
if (flags & LINK_FLAG.HasArguments)
strings.push(stringDataField(argumentText, unicode));
if (flags & LINK_FLAG.HasIconLocation)
strings.push(stringDataField(iconLocation, unicode));
let stringBytes = Buffer.concat(strings);
if (options.omitStringBytes !== undefined)
stringBytes = stringBytes.subarray(
0,
Math.max(0, stringBytes.length - options.omitStringBytes),
);
parts.push(stringBytes);
for (const block of extraData) parts.push(extraBlockBytes(block));
if (options.terminalBlock !== false) parts.push(Buffer.alloc(4));
const bytes = Buffer.concat(parts);
return options.truncateAt === undefined
? bytes
: bytes.subarray(0, options.truncateAt);
};
export const powershellHiddenIconSpoofShortcut = (): Buffer =>
windowsShortcutBytes();
export const networkShareShortcut = (): Buffer =>
windowsShortcutBytes({
showCommand: 1,
flags:
LINK_FLAG.HasLinkTargetIDList |
LINK_FLAG.HasLinkInfo |
LINK_FLAG.HasName |
LINK_FLAG.HasRelativePath |
LINK_FLAG.IsUnicode,
name: "Share",
relativePath: ".\\report.docx",
linkInfo: {
netName: "\\\\fileserver\\docs",
deviceName: "Z:",
networkProviderType: 0x00020000,
commonPathSuffix: "report.docx",
},
extraData: [],
});
export const ansiShortcut = (): Buffer =>
windowsShortcutBytes({
unicode: false,
extraData: [{ kind: "environment", ansi: "C:\\Windows\\notepad.exe" }],
});
export const truncatedStringDataShortcut = (): Buffer =>
windowsShortcutBytes({
extraData: [],
terminalBlock: false,
omitStringBytes: 8,
});
export const unknownExtraDataShortcut = (): Buffer =>
windowsShortcutBytes({
extraData: [
{ kind: "unknown", signature: 0xa0ffffff, payload: Buffer.from("xx") },
],
});
export const wrongClsidShortcut = (): Buffer =>
windowsShortcutBytes({
clsid: Buffer.alloc(16),
extraData: [],
});