Windows 跳转列表(Jumplist)是任务栏右键菜单里展示的最近访问文件与常用任务集合。系统将这些数据持久化到用户目录下,文件扩展名为 .automaticDestinations-ms 和 .customDestinations-ms。用 C# 分析这些文件能够还原用户最近打开的程序、文档和访问时间,适合用于电子取证、员工行为审计或本机使用习惯分析。不过这类文件不是普通文本,必须理解其复合文档结构并解析内部流,才能拿到有意义的信息。

Jumplist 文件存储位置与复合文档结构
自动目标列表文件存放于 C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations 目录。自定义目标列表位于 C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations。前者由系统根据应用用户模型 ID 自动生成,后者保存用户手动固定到跳转列表中的内容。
自动目标列表的文件名通常由应用用户模型 ID 经过哈希计算得到,形如 1b4dd67f29a37c5c.automaticDestinations-ms。每个文件对应一个应用或系统组件。这些文件并不是 XML 或 JSON,而是微软 OLE 复合文档格式(Compound File Binary,CFB)。文件头固定签名为十六进制 D0 CF 11 E0 A1 B1 1A E1,读取前 8 个字节即可确认文件类型。
复合文档内部由扇区、目录流、FAT 和 Mini FAT 组成,逻辑上可以看作一个微型文件系统。Jumplist 文件中的核心流包括 DestList 和多个以 16 字节标识符命名的流。DestList 保存最近使用列表的顺序、时间戳和目标 ID,其他流则保存对应目标的 Shell Link 数据。只有同时解析这两类流,才能把访问记录与具体文件路径对应起来。
用 C# 枚举并读取关键流
要读取复合文档,可以手动实现 CFB 解析器,但工作量较大。更实际的做法是引入 NuGet 包 OpenMCDF,它提供了轻量级的复合文档访问接口。使用 CompoundFile 类打开 .automaticDestinations-ms 文件,可以枚举所有流名称并读取字节。
先用文件头判断是否为有效复合文档,避免对无关文件进行解析。下面的代码展示如何验证签名并使用 OpenMCDF 枚举流:
using OpenMcdf;
byte[] signature = new byte[8] { 0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1 };
byte[] buffer = File.ReadAllBytes(filePath);
if (buffer.Length < 8 || !buffer.Take(8).SequenceEqual(signature))
{
Console.WriteLine("不是有效的复合文档");
return;
}
CompoundFile cf = new CompoundFile(filePath);
var streams = cf.RootStorage.GetStreams();
foreach (var stream in streams)
{
Console.WriteLine(stream.Name);
}
枚举结果中通常能看到 DestList 以及多个 32 位十六进制字符串命名的流。DestList 流的前部没有分隔符,按固定长度顺序排列。每个条目通常包含 16 字节目标 ID、8 字节 FILETIME 时间戳以及若干标志位。解析时将流从头部开始循环读取,直到结束。
下面的代码读取 DestList 流并输出目标 ID 与最后访问时间:
CFStream destStream = cf.RootStorage.GetStream("DestList");
byte[] destData = destStream.GetData();
int offset = 0;
while (offset + 24 <= destData.Length)
{
string targetId = BitConverter.ToString(destData, offset, 16).Replace("-", "");
long fileTime = BitConverter.ToInt64(destData, offset + 16);
DateTime lastAccess = DateTime.FromFileTimeUtc(fileTime).ToLocalTime();
Console.WriteLine(targetId + " => " + lastAccess);
offset += 24;
}
需要注意,DestList 条目大小在不同 Windows 版本中可能略有差异,实际开发时应根据文件大小和剩余字节做容错处理。读取时间戳后先转换为 UTC 再转本地时间,可以避免时区偏移导致的分析误差。
解析 LNK 流还原目标路径
DestList 中的目标 ID 对应复合文档中同名的流,这些流保存的是 Shell Link(LNK)二进制数据。LNK 文件有标准结构,头部为 76 字节的 ShellLinkHeader,其中偏移 20 处的 LinkFlags 字段决定后续包含哪些可选结构。
要提取目标路径,可以先判断 LinkFlags 中的 HasLinkTargetIDList 和 HasName 位是否置位。目标路径可能以 ANSI 或 Unicode 字符串形式出现在不同位置。对于大多数自动目标列表文件,路径信息位于 LinkInfo 结构或字符串数据区域中。下面的简化函数演示了如何从 LNK 数据中提取目标字符串:
private static string ReadLinkTarget(byte[] data)
{
if (data.Length < 76) return string.Empty;
uint linkFlags = BitConverter.ToUInt32(data, 20);
bool isUnicode = (linkFlags & 0x00000080) != 0;
int offset = 76;
if ((linkFlags & 0x00000001) != 0)
{
ushort idListSize = BitConverter.ToUInt16(data, offset);
offset += idListSize + 2;
}
if ((linkFlags & 0x00000002) != 0)
{
int localBasePathOffset = BitConverter.ToInt32(data, offset + 20);
int basePathOffset = offset + localBasePathOffset;
if (basePathOffset < data.Length)
{
if (isUnicode)
return System.Text.Encoding.Unicode.GetString(data, basePathOffset, data.Length - basePathOffset).TrimEnd('\0');
return System.Text.Encoding.Default.GetString(data, basePathOffset, data.Length - basePathOffset).TrimEnd('\0');
}
}
return string.Empty;
}
提取出的路径可能是绝对路径,也可能是含有环境变量的相对路径。对于某些 UWP 应用,路径可能指向 shell:AppsFolder 下的应用程序标识,需要进一步调用系统 API 才能获得可读名称。解析 LNK 时还要注意 Volume ID 结构,它包含驱动器序列号和卷标,有时路径并不是直接可用的本地路径,需要结合卷信息还原。
将每个 DestList 条目的 ID 传入上述函数并对应输出,就可以得到类似 C:\Users\你的用户名\Documents\报告.docx 或 C:\Program Files\Notepad++\notepad++.exe 的结果。这样就把 16 字节的抽象 ID 与用户实际打开的对象关联起来。
统计访问频率与合规注意事项
拿到所有条目的时间戳和目标路径后,可以按路径分组统计访问次数,并按最后访问时间排序,输出用户常用文件和程序列表。例如遍历 AutomaticDestinations 目录下的每个复合文档,聚合 DestList 中的记录,再用目标 ID 关联 LNK 流,形成完整的数据集。
string automaticPath = @"C:\Users\你的用户名\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations";
var accessCount = new Dictionary<string, int>();
foreach (string file in Directory.GetFiles(automaticPath, "*.automaticDestinations-ms"))
{
try
{
CompoundFile cf = new CompoundFile(file);
// 此处省略解析 DestList 和 LNK 流的具体过程
foreach (string path in parsedPaths)
{
if (accessCount.ContainsKey(path))
accessCount[path]++;
else
accessCount[path] = 1;
}
}
catch (Exception ex)
{
Console.WriteLine("跳过文件 " + file + ",原因:" + ex.Message);
}
}
这段代码使用 Dictionary<string, int> 做频率统计,显示时可按值倒序排列。需要注意,复合文档可能被正在运行的应用锁定,打开时应使用 FileShare.ReadWrite 参数。代码示例中为集中展示核心逻辑省略了流解析部分,实际项目应将解析函数封装成独立类,方便复用。
Windows Jumplist 包含大量用户行为信息,属于个人隐私数据。在电子取证、内部审计或家长监控等场景中使用前,必须获得明确授权或具备合法依据。切勿将分析工具用于未授权的监控或数据窃取。技术上还应考虑不同 Windows 版本之间的结构差异,比如部分版本的 DestList 条目长度可能扩展,解析时要基于可用的流长度动态判断。
C# JumplistWindows跳转列表用户行为分析修改时间:2026-09-23 01:48:42