一、概述
.lnk 是 Windows 快捷方式文件,取证价值却远高于它的文件体积。一个通常只有 1–2 KB 的二进制文件,往往同时记录了目标文件的三个时间戳、目标所在卷的序列号、目标绝对路径、源机器的 NetBIOS 名,甚至源机器网卡的 MAC 地址。
这意味着一个反直觉的事实:当目标文件本身已被删除、卷已被重新格式化、U盘已经拔走之后,快捷方式本身还独立存活,并且它携带的是「快捷方式被创建那一刻」的快照。它不只是导航,它是一份自包含的时间与来源记录。
Windows 还会在用户完全无感知的情况下批量生成 LNK:打开文档时在 %APPDATA%\Microsoft\Windows\Recent\ 写入一个 .lnk;任务栏右击菜单在 AutomaticDestinations\ 维护 Jump List;应用通过 ICustomDestinationList 写入 CustomDestinations\,其中甚至包含浏览器搜索词、VS Code 的最近工程等。
因此在数据泄露、钓鱼溯源、横向移动这几类案件里,LNK 与 Jump List 常常是唯一能同时回答「谁打开过哪个文件」和「这个文件是在哪台机器上生成、插过哪个 U 盘」的工件。这里讲的是 LNK 与 Jump List 两种工件。用户行为类注册表键见用户行为痕迹,浏览器数据库见浏览器取证。
它处于分析环节的「归属分析」路径上,位置比执行类工件更靠后——它回答的不是「执行了什么」,而是「谁做的、从哪来的」:
- 上游依赖 用户身份确认。LNK 与 Jump List 都在用户配置文件下,
AutomaticDestinations 属于当前用户,归属错了整套结论就错了,账户与 SID 判定见注册表取证。
- 它自己 要分清三类落点。
Recent\*.lnk(单文件快捷方式,携带卷序列号与 MachineID,保留较久)、Recent\AutomaticDestinations\(OLE 复合文档,每个应用一个)、Recent\CustomDestinations\(LNK 序列,格式自定义)。两类都要提取——Jump List 里的 LNK 会被条目上限淘汰,Recent 目录的单文件 LNK 保留更久,只查一类会漏掉另一半记录。
- 下游支撑 两处。
DriveSerialNumber 是与可疑介质匹配的钥匙,横向移动溯源靠它;MFU 排序反映用户对文件的实际使用频次,比单纯的访问时间更有指向性。
- 它与执行类工件互补:Prefetch 说「程序启动过」,LNK 说「用户打开了哪个文档」——两者缺一,行为链条就不完整。
.lnk 的结构按区段划分:
| 区段 |
关键字段 |
取证用途 |
| 固定头 |
HeaderSize、LinkFlags、FileAttributes、三个目标时间戳、文件大小、图标索引 |
时间戳与结构存在性开关 |
LinkInfo |
LinkInfoSize、LinkInfoHeaderSize、LinkInfoFlags |
见下 |
└ VolumeID |
VolumeIDSize、DriveType、DriveSerialNumber、卷标签偏移 |
核心:标识创建快捷方式时目标所在的卷 |
| └ 路径 |
LocalBasePath(置位 0)/ NetName + CommonPathSuffix(置位 1),两者可同时置位 |
目标绝对路径 |
TrackerDataBlock |
MachineID 与 droid GUID |
源机器标识,归属判定的关键 |
| 终端块 |
TerminalBlock(值小于 0x00000004,通常 0x00000000) |
界定 ExtraData 结束位置 |
Jump List 侧:
| 落点 |
格式 |
关键点 |
AutomaticDestinations |
OLE 复合文档(CFB) |
DestList 流含 MRU/MFU 排序、固定状态位、访问时间、路径、条目编号;Win7/8 与 Win10 的首字段语义与条目长度不同(后者 130 字节) |
CustomDestinations |
带分类结构的 LNK 序列 |
以 0xBABFFBAB 结束,不是复合文档 |
| 文件名 |
16 位十六进制 AppID,如 1b4dd67f29cb1962.automaticDestinations-ms |
文件名就是应用身份,可能是显式定义,也可能由可执行文件路径经 CRC-64 派生 |
DriveType 的标准值区分介质:0x02 可移动、0x03 固定磁盘、0x04 网络。0x02 或 0x04 出现在内部机的工作站 LNK 中,往往是横向移动的入口。
据此能回答的是:某个用户打开过哪些文档及其访问时间、目标文件创建时的三个时间戳、目标所在卷的序列号与介质类型、快捷方式在哪台机器上被创建(MachineID / droid GUID)、以及各应用最近访问过什么(Jump List)。
回答不了的是这些,外加两个必须先纠正的误用:
- 「用 Win7 布局硬解 Win10 文件」会得到看似合理实则错误的结果。 Win7/8 与 Win10 的
DestList 首字段语义与条目长度不同(后者 130 字节),用错布局会导致路径字段整体错位,读出乱码却以为文件损坏。
- 「AppID 未知」不是文件无效。 它只是缺少可读名称。报告里写「AppID
1b4dd67f29cb1962,未匹配到已知应用」是合格表述,写成「Chrome 记录」而不给出依据则不合格。
- 文件当前是否还存在。 LNK 记录的是创建时的快照,目标文件早已删除是常态。
- 打开的动作者。 MachineID 标识的是机器不是人,要落到具体用户仍需结合用户目录归属。
- 打开的意图。 打开一个文件与修改它完全不同,LNK 只能证明「打开过」。
- MFU 排序的精确语义。 它反映相对使用频次,不是精确计数,也不等于访问时间顺序。
读到这里你应该已经理解:NTFS 元数据与 FILETIME 时间基准,LNK 里的三个时间戳需要正确换算,见时间戳换算;OLE 复合文档(CFB)的结构,AutomaticDestinations 用的就是它,解析前需要先理解流与存储的关系;从十六进制手工校验文件头与 droid 字段,这是解析器出错时的兜底手段;注册表 hive 的用户归属判定,它决定了取哪个用户目录下的 Jump List;以及只读挂载后的镜像检索,Recent 与 AppData 下的目录需要按用户逐个遍历。
二、核心原理
所有 .lnk 以 76 字节固定头开始,其后是由标志位控制的可选结构。固定头本身就是一块高价值证据:
| 偏移 |
长度 |
字段 |
取证意义 |
| 0x00 |
4 |
HeaderSize |
恒为 0x0000004C,可作为格式校验 |
| 0x04 |
16 |
LinkCLSID |
恒为 00021401-0000-0000-C000-000000000046 |
| 0x14 |
4 |
LinkFlags |
决定后续哪些结构存在 |
| 0x18 |
4 |
FileAttributes |
目标属性,可识别目录/压缩/加密/离线等 |
| 0x1C |
8 |
CreationTime |
目标文件的创建时间快照 |
| 0x24 |
8 |
AccessTime |
目标文件的最后访问时间快照 |
| 0x2C |
8 |
WriteTime |
目标文件的最后修改时间快照 |
| 0x34 |
4 |
FileSize |
目标文件当时的大小 |
| 0x38 |
4 |
IconIndex / ShowCommand |
隐藏窗口(0x07)等异常信号 |
| 0x3C |
2 |
HotKey |
通常无价值 |
| 0x3E–0x4B |
— |
Reserved1/2/3 |
必须为 0,非零是人为构造的信号 |
三个 FILETIME 是创建快捷方式那一刻从目标文件读到的 MAC 时间,不是快捷方式自身的时间。
快捷方式自身的时间要看它的 NTFS $STANDARD_INFORMATION。
这两组时间放在同一份报告里时,差值本身就是结论:若 $SI 的修改时间远晚于头里的目标修改时间,说明文件在这期间被改过。
2.2 LinkFlags:结构的存在性开关
LinkFlags 是低 27 位的位标志,从最低位(bit A)开始:
| 位 |
值 |
名称 |
存在则紧跟固定头的结构 |
| 0 |
0x00000001 |
HasLinkTargetIDList |
LinkTargetIDList(Shell Item 列表) |
| 1 |
0x00000002 |
HasLinkInfo |
LinkInfo(卷序列号 + 路径) |
| 2 |
0x00000004 |
HasName |
NAME_STRING |
| 3 |
0x00000008 |
HasRelativePath |
RELATIVE_PATH |
| 4 |
0x00000010 |
HasWorkingDir |
WORKING_DIR |
| 5 |
0x00000020 |
HasArguments |
COMMAND_LINE_ARGUMENTS |
| 6 |
0x00000040 |
HasIconLocation |
ICON_LOCATION |
| 7 |
0x00000080 |
IsUnicode |
字符串为 UTF-16LE,否则为系统 ANSI 代码页 |
StringData 各串的顺序是强制的:NAME_STRING → RELATIVE_PATH → WORKING_DIR → COMMAND_LINE_ARGUMENTS → ICON_LOCATION,每个串前置一个 16 位「字符数」长度前缀。跳过任何一个被置位的标志,后续所有串都会错位。
从第 8 位起是行为控制位,取证上最值得注意的是 ForceNoLinkInfo(bit 8,0x0100,忽略 LinkInfo)、HasDarwinID(bit 12,MSI 安装程序快捷方式)、ForceNoLinkTrack(bit 18,忽略 Tracker 块)与 EnableTargetMetadata(bit 19,写入属性存储)。
正常由资源管理器生成的 LNK 不会置 ForceNoLinkInfo;恶意 LNK 构建工具常置位它并抹掉卷序列号来隐藏 U 盘痕迹。
2.3 LinkInfo:卷序列号与路径
LinkInfo 开头是 LinkInfoSize(整个结构长度)、LinkInfoHeaderSize、LinkInfoFlags,以及一个可选的 4 字节 VolumeIDOffset/LocalBasePathOffset(LinkInfoHeaderSize 为 0x00000024 时存在)。LinkInfoFlags 两位定义如下:
| 位 |
值 |
含义 |
| 0 |
0x00000001 |
VolumeIDAndLocalBasePath |
| 1 |
0x00000002 |
CommonNetworkRelativeLinkAndLinkInfo |
置位 0 时内含 VolumeID(VolumeIDSize、DriveType、DriveSerialNumber、卷标签偏移)与 LocalBasePath / LocalBasePathUnicode;置位 1 时改为 NetName / NetNameUnicode(UNC 前缀)加 CommonPathSuffix(UNC 尾部)。两者可同时置位。
DriveSerialNumber 是 LNK 取证的核心:它标识创建快捷方式时目标所在的卷。
同一个序列号出现在某台主机上成千上万个 LNK 里,说明这些 LNK 全部指向同一个源卷——而那个卷很可能就是已拔走的 U 盘或已送修的移动硬盘。
只要拿到可疑介质,读取它的卷序列号即可完成匹配。
DriveType 用标准值区分介质:0x02 可移动、0x03 固定磁盘、0x04 网络。0x02 或 0x04 出现在一台内部机的工作站 LNK 中,往往是横向移动的入口。
2.4 TrackerDataBlock:MachineID 与 droid GUID
ExtraData 区是一串 BlockSize + BlockSignature + 载荷 的重复结构,最后跟一个 4 字节 TerminalBlock(值小于 0x00000004,通常为 0x00000000)。常见签名:
| 签名 |
块名 |
取证用途 |
0xA0000001 |
EnvironmentVariableDataBlock |
%WINDIR%、%TEMP% 等环境变量形式的路径 |
0xA0000002 |
ConsoleDataBlock |
目标为控制台程序时的窗口设置 |
0xA0000003 |
TrackerDataBlock |
MachineID、droid GUID,归属证据的核心 |
0xA0000004 |
ConsoleFEDataBlock |
控制台代码页 |
0xA0000005 |
SpecialFolderDataBlock |
特殊文件夹 GUID |
0xA0000006 |
DarwinDataBlock |
Windows Installer 的安装描述符 |
0xA0000007 |
IconEnvironmentDataBlock |
图标路径中的环境变量 |
0xA0000008 |
ShimDataBlock |
兼容性 shim 名称 |
0xA0000009 |
PropertyStoreDataBlock |
键值化属性集合,含 SID 等 |
0xA000000B |
KnownFolderDataBlock |
已知文件夹 GUID |
TrackerDataBlock 标准块长 0x60、内部 Length 为 0x58,载荷含 16 字节 MachineID(源机器 NetBIOS 名)、Droid 与 DroidBirth 各 32 字节。droid v1 GUID 的后 6 字节可形成源机器 MAC,但虚拟机 NAT 端口、虚拟网卡、MAC 随机化都会影响归属结论,不能仅凭 MAC 相同就断言同一台物理机。
2.5 Recent 目录与 Jump List 的关系
%APPDATA%\Microsoft\Windows\Recent\ 下有两类内容:
| 位置 |
内容 |
特点 |
Recent\*.lnk |
单文件快捷方式 |
打开文档时生成,携带卷序列号与 MachineID,保留较久 |
Recent\AutomaticDestinations\ |
复合文档(OLE CFB) |
每个应用一个文件,记录该应用最近访问的文档 |
Recent\CustomDestinations\ |
LNK 序列 |
由应用自行写入,格式自定义 |
两类都要提取。
Jump List 里的 LNK 会被条目上限淘汰,Recent 目录的单文件 LNK 保留更久,只查后者会漏掉应用级访问记录,反之亦然。
2.6 Jump List 文件结构
AutomaticDestinations 是 OLE 复合文档,其中的 DestList 流含 MRU/MFU 排序、固定状态位、访问时间、路径与条目编号。CustomDestinations 不是复合文档,而是带分类结构的 LNK 序列,以 0xBABFFBAB 结束。
Windows 7/8 与 Windows 10 的 DestList 首字段语义与条目长度不同(后者为 130 字节)。用 Win7 布局硬解 Win10 文件会导致路径字段整体错位,读出乱码却以为文件损坏。
DestList 还含 8 字节 MRU / MFU 排序值——MFU(最常用)排序能反映用户对该文件的实际使用频次,比单纯的访问时间更有指向性。
2.7 AppID:文件名就是应用身份
Jump List 文件名是 16 位十六进制的 AppID,如 1b4dd67f29cb1962.automaticDestinations-ms。
它可能是应用显式定义,也可能由可执行文件路径经 CRC-64 派生。
知道哪个 AppID 属于哪个应用,是「知道哪个应用记录了哪些文件」的前提。
AppID 未知不等于文件无效,只是缺少可读名称;JLECmd 内置常见 AppID 表,也可用 --appIds 提供自定义映射。
报告里写「AppID 1b4dd67f29cb1962,未匹配到已知应用」是合格表述,写成「Chrome 记录」而不给出依据则不合格。
三、操作步骤
3.1 从镜像中定位并提取
# 1) 确认分区与系统卷
mmls /work/DigiForensics.dd
fsstat -o 2048 /work/DigiForensics.dd | head -8
# 2) 列出用户配置(fls -p 列序:mode(1) inode(2) name(3) size(4) ...)
fls -o 2048 -r -p /work/DigiForensics.dd \
| grep -Ei '/Users/[^/]+/(NTUSER\.DAT|Recent)' | head -20
# 3) 批量提取 .lnk(用 inode 命名避免重名覆盖)
mkdir -p /evidence/users/jsmith/Recent
fls -o 2048 -r -p /work/DigiForensics.dd \
| grep -Ei '/Users/jsmith/AppData/Roaming/Microsoft/Windows/Recent/.*\.lnk$' \
| grep -E '^r/r' | awk '{print $2}' \
| while read -r ino; do
icat -o 2048 /work/DigiForensics.dd "$ino" \
> "/evidence/users/jsmith/Recent/$(printf '%s.lnk' "${ino%%:*}")"
done
inode 在第 2 列(形如 294-294-1:),第 3 列才是带空格的完整路径。取错列会把文件名当 inode 传给 icat。CustomDestinations 的文件没有 .lnk 后缀,去掉结尾 \.lnk$ 过滤即可一并递归提取。
3.2 批量解析 LNK
# 单文件快速验证
LECmd.exe -f "C:\evidence\Recent\sample.lnk"
# 整目录解析(-r 只保留指向可移动介质的 LNK)
LECmd.exe -d "C:\evidence\users\jsmith\Recent" -r `
--csv "C:\evidence\out\lnk" --csvf "lnk.csv" `
--json "C:\evidence\out\lnk_json" --pretty -q
重点关注输出列:LocalPath、VolumeSerialNumber、DriveType、MachineID、MACAddress、Arguments、WorkingDirectory、RelativePath,以及 TargetCreated/TargetModified/TargetAccessed 三组目标时间戳。
只想快速看 Target ID List 与 Extra 块时用 -f 加 --nid --neb,这两个开关会抑制对应细节输出,反而更快。
3.3 解析 Jump List
# 整目录解析(--all 才会处理非 .automaticDestinations-ms 文件)
JLECmd.exe -d "C:\evidence\users\jsmith\Recent\AutomaticDestinations" `
--csv "C:\evidence\out\jl" --csvf "autodest.csv" `
--json "C:\evidence\out\jl_json" --pretty -q
# 只看 DestList 条目、不解全部 LNK 细节时
JLECmd.exe -d "C:\evidence\users\jsmith\Recent" --mp `
--dt "yyyy-MM-dd HH:mm:ss.fff" --csv "C:\evidence\out\jl_all"
# 抽出嵌入的 LNK 单独深挖
JLECmd.exe -d "C:\evidence\users\jsmith\Recent\AutomaticDestinations" `
--dumpTo "C:\evidence\out\jl_lnk" -q
LECmd.exe -d "C:\evidence\out\jl_lnk" --all `
--csv "C:\evidence\out\lnk_from_jl" --csvf "from_jl.csv"
# 对单个可疑 AppID 深挖完整 LNK 信息
JLECmd.exe -f "C:\evidence\...\1b4dd67f29cb1962.automaticDestinations-ms" --fd -q
--fd 会直接输出完整 LNK 信息,代价是慢;样本量大时先 JLECmd 定位可疑 AppID 与条目号,再对单文件深挖效率高得多。
--withDir 显示未被 DestList 条目覆盖的流,用于发现被截断或手工构造的残留。
3.4 手工校验文件头与 droid