找回密码
 立即注册
搜索
查看: 292|回复: 3

[翻译] 深度解析跳转列表(Jump lists):吃透底层格式,精准掌握工具的实际能力边界

[复制链接]

413

主题

2

回帖

3908

积分

管理员

积分
3908
发表于 2026-9-11 18:35:50 | 显示全部楼层 |阅读模式 来自 湖北武汉
    前几天我收到了Harry Parsonage的邮件,他试用了LECmd工具并且给出了好评。他针对CSV输出的日期格式提出了优化建议,我会在LECmd的下一个版本(同时也会同步更新到PECmd中)落地这些调整,优化后的格式能大幅提升数据排序等操作的便利性。

    最后,Harry还提议我深入研究跳转列表(Jump lists),我便着手开展了相关分析。

背景与开篇思考
    关于跳转列表的部分基础背景资料可以在相关链接中查阅,但正如我们后续会看到的,跳转列表的底层格式已经发生了不少变化——这些变化直接导致不少现有跳转列表解析工具出现了兼容性故障。

    归根结底,每一位取证分析人员都应当充分理解自己所查看的数据的底层逻辑,我也希望这篇文章和我的其他同类内容,能帮助大家理清数据的来源,而不是仅仅被动消费工具输出的结果;与此同时,工具开发者也肩负着相应的责任。

    我知道我已经反复强调过很多次:当工具出现解析失败的情况时,必须第一时间向终端用户发出明确告警,这样用户才有机会主动排查解析失败的根本原因。

    正如我去年在SANS技术峰会上分享的那样,我始终坚信以下几条核心原则:
  • 工具开发者无权自行判定哪些数据相关、该保留,哪些数据无关、该剔除
  • 让解析过程彻底失败并明确告知终端用户,远比悄无声息地丢弃数据要好得多
  • 如果你无法获取完整的全量数据,又怎么能察觉到有信息被遗漏了呢?
  • 我当时也指出,有人可能会说只要你拿到了包含目标取证工件的原始文件,就等于掌握了全部信息,但如果要靠十六进制编辑器手动完成全量校验,这个过程的工作量会极其繁重。

    以开发者的视角来看,根据我的实操经验,排查和修复这类问题最高效的方案是依托单元测试:只要你持续向测试套件中补充新操作系统、新系统版本对应的样本数据,单元测试就能自动帮你发现潜在问题。当你搭建起一套完善的单元测试体系、用它来强制校验基础解析规则后,只要底层格式发生任何变动,测试用例都会第一时间给你反馈。

    只要你从事任何形式的开发工作,无论你用的是哪门编程语言,都应该去学习对应生态下的单元测试框架——这既是对自己负责,也是对使用你工具的技术社区负责。虽然前期编写测试用例会增加额外工作量,但从长期来看它能帮你节省大量时间,更重要的是,后续你做代码重构时完全不用担心自己的改动会破坏原有功能。

    关于单元测试的相关内容我们后面还会进一步展开!😉

现在让我们回到跳转列表的主题上来
    如果你在Windows 7及更高版本系统中右键点击过任务栏上的图标,大概率都见过大家常说的跳转列表,它的界面形态大致是这样的:
jumplist.jpg
    但当你点击跳转列表里的某一项时,Windows是怎么知道要打开什么内容的呢?其实Windows打开跳转列表条目的底层机制,和你双击桌面上的快捷方式启动程序的逻辑是完全一致的。

    简单来说,跳转列表(至少我们接下来要讨论的这两类)本质上就是把一堆lnk快捷方式文件打包封装在单个文件里。当然实际的底层逻辑比这个要复杂一些,我们后面会详细展开。由于lnk文件在跳转列表的实现中无处不在,提前搞懂lnk文件里存储了哪些信息会非常有帮助。

    刚好我开发的lnk解析工具LECmd,就可以完整展示lnk文件的全部内容。

    本文不会深入讲解跳转列表的创建和更新机制,而是重点解析两类跳转列表的数据格式:自动生成的目标列表和自定义目标列表。我们先从相对简单的自定义目标列表开始介绍。

    和很多Windows文件格式一样,Joachim Metz已经公开了一份跳转列表布局的可用规范文档。如果你对这部分内容感兴趣(做这行的技术人谁会不感兴趣呢),一定要去查阅这份资料。

自定义目标列表
    自定义目标文件(*.customDestinations-ms)的一种生成场景,是用户将某个项目固定到跳转列表中时自动创建的。Harlan早在2011年就已经介绍过这种生成机制

    这类文件的存储路径如下:C:\Users\<用户配置文件夹>\AppData\Roaming\Microsoft\Windows\Recent\CustomDestinations

    自定义目标跳转列表文件的内部通用结构大致如下:
  • 文件头
  • 一系列首尾拼接的lnk快捷方式文件
  • (文件中可能还包含其他额外的数据结构)
  • 文件尾(特征签名为0xbabffbab)

    这里给出一类自定义目标跳转列表的十六进制编辑器示例,图中已高亮标注出各核心部分:
    顶部的紫色区块是文件头,紧接着的粉色(或者说三文鱼色?)区块是第一个lnk文件,最后的绿色区块则是下一个lnk文件的起始位置。
customHex.jpg
    文件的最底部是文件尾:
customfooter.jpg

    如果我们把粉色区域的字节数据提取出来并保存为一个独立文件,就可以用LECmd工具来处理它,操作示意如下:
LECmdcus.jpg

    现在,解析器只需要掌握如何从 *.customDestinations-ms 文件中提取出 lnk 文件,你就可以使用任意选择的工具来提取这些 lnk 文件中包含的所有详细信息。

    听起来不错,但我们该如何确定每个 lnk 文件的起始和结束位置呢?如果每个 lnk 文件前面都能附带一个字节长度值,让我们先读取这个大小,再读取相应数量的字节,那该多方便啊。可惜,这仅仅是一种美好的设想。

    我处理自定义目标文件的方法是,在海量的字节数据中搜索 lnk 文件特有的几个标识特征:
  • ‌头部长度‌:0x4C
  • ‌Lnk 类标识符 GUID‌:00021401-0000-0000-c000-000000000046

    你可能会注意到,构成该 GUID 的字节在 lnk 文件中的排列顺序与标准格式并不一致。因此,我们需要在字节流中查找如下特定的字节模式:4C 00 00 00 01 14 02 00 00 00 00 00 C0 00 00 00 00 00 00 46

    其中,前 4 个字节代表头部长度,接下来的 16 个字节构成了我们的 GUID。

    如果我们能定位到这些特征出现的偏移量,也就确定了每个 lnk 文件的起始位置。既然知道了每个文件的起始点,我们就可以从第一个文件开始,利用第二个文件的起始偏移量来计算第一个 lnk 文件所占用的字节数。这种方法一直适用,直到处理最后一个 lnk 文件为止。对于最后一个文件,我们需要找到文件尾(footer)的偏移量,并结合最后一个 lnk 文件的起始偏移量,从而计算出最后一个 lnk 文件的字节大小。

    这就是自定义目标跳转列表在磁盘上的存储方式。

    最后,我手动从我们一直分析的自定义目标文件中提取出每个 lnk 文件,并使用 LECmd 工具对其进行了处理。在每个 lnk 文件中,都有一个包含“Title”(标题)属性的“属性存储数据块”(Property store data block)。以下是这些 lnk 文件中提取出的标题(按照它们在自定义目标文件中出现的顺序排列):
  • Start Capture(开始捕获)
  • Toggle Capture Window(切换捕获窗口)
  • Create New Image(创建新图像)
  • Convert Images(转换图像)

    现在,让我们来看看当我右键点击任务栏上的 Snag-It Editor 图标时弹出的菜单。
sem.jpg

    正如你所见,列表顶部的条目已被“固定”到跳转列表中,这正是它们出现在这个特定的自定义目标跳转列表中的原因。太棒了!

自动目标列表
    搞定了两种跳转列表格式中相对简单的一类,现在我们可以来研究自动目标列表,也就是*.automaticDestinations-ms格式的文件。这类文件存放在以下目录中:
C:\Users\<用户配置文件夹>\AppData\Roaming\Microsoft\Windows\Recent\AutomaticDestinations


    自动目标跳转列表采用OLE复合文件(CF),也就是OLE CF格式存储。要深入讨论自动目标跳转列表,我们需要先对这种文件格式建立基础认知。关于该格式的所有底层细节都可以在相关官方文档中查阅,不过我们先对它做基础梳理,再进一步讲解它和理解自动目标跳转列表的关联逻辑。

    请注意,本文内容属于概述性质,部分细节做了适当简化处理。

OLE 复合文件(OLE CF)解析
    注:我用 C# 编写了自己的 OLE CF 解析器,如果你希望查看所有技术细节,可以在这里获取。

‌    为什么要自行开发解析器?‌
    原因与我开发其他解析器的初衷一致:为了验证现有的文档规范、文件格式说明以及具体实现细节的正确性(此外,我个人也更偏爱使用 C# 作为开发语言)。

    OLE 复合文件主要包含以下核心结构:
  • ‌文件头(Header)‌
  • ‌扇区分配表(Sector Allocation tables)‌
  • ‌目录结构(Directory)‌

    其中,文件头长度为 512 字节,包含了解析文件其余部分所需的大量关键信息。以下是一个典型的文件头结构示例:
header.jpg

    部分较为重要的属性如下:
  • 偏移量30处:扇区大小
  • 偏移量32处:短扇区大小
  • 偏移量44处:扇区分配表(SAT)的总扇区数
  • 偏移量48处:目录所用第一个扇区的扇区ID
  • 偏移量56处:标准流的最小大小(单位:字节)
  • 偏移量60处:短扇区分配表(SSAT)所用第一个扇区的扇区ID
  • 偏移量64处:SSAT占用的总扇区数
  • 偏移量76处:主扇区分配表(MSAT)的起始位置

    要得到一个扇区和短扇区实际占用的字节数,只需将对应大小值做2的幂次运算即可。举个例子:如果扇区大小字段的值是9,那么对应的扇区字节数就是2的9次方,也就是512字节;如果短扇区大小字段的值是6,那么短扇区的字节数就是64。

    在你确定扇区大小后(暂时忽略短扇区),就可以把OLE复合文件按「扇区字节数」的长度切分成多个数据块,每一个数据块就对应一个扇区。

    有一点需要特别注意:扇区编号是相对值,和文件内的绝对偏移不是一回事。要计算扇区数据对应的文件绝对偏移,需要用扇区编号乘以扇区大小,再加上512字节的头部长度,这个计算规则后续会频繁用到。

    这套逻辑在短扇区存储中也大致适用,但短扇区存储有个区别:当你拿到构成短存储空间的全部字节后,短扇区编号是相对于短存储空间起始位置的绝对偏移。你当然也可以用和上面类似的公式计算(头部大小 + 短存储空间的绝对偏移 + 短扇区大小 × 短扇区编号),但把短存储空间想象成一块独立的“数据孤岛”会更容易理解(至少对我来说是这样)。

    接下来要理解的是标准流的最小大小参数,绝大多数场景下这个值是4096字节。这个阈值决定了OLE复合文件里的内容会存放在主存储区还是短扇区存储区,设置这个规则的核心目的是提升存储效率:如果你要存储很多小体积数据,明明只需要87字节,却按512字节的粒度分配空间,会造成大量存储空间浪费。

SAT与SSAT
    接下来我们来介绍扇区分配表(SAT)和短扇区分配表(SSAT)。二者的工作机制本质上完全一致,核心作用是追踪哪些扇区/短扇区属于某一段数据链、哪些扇区处于空闲状态等信息。

    你可以把这两种结构理解为一个数组,数组的每个槽位中存储一个数值。这个数值可以标识多种状态,通常槽位内的数值会指向数据链的下一个槽位、标记数据链结束(值为-2),或是标记该记录处于空闲状态(值为-1)。

    要构建SAT和SSAT,我们首先要读取主扇区分配表(MSAT)。如前文所述,MSAT的前109个条目存储在文件头部从偏移量76开始的位置。从该偏移量起,连续排列着109个32位(4字节)有符号整数,这些整数指向我们构建SAT和SSAT所需读取的各个扇区。

    举一个简单示例,假设从偏移量76开始的原始数据如下:
  • 00 00 00 00 → 十进制值0
  • 80 00 00 00 → 十进制值128
  • 00 01 00 00 → 十进制值256
  • 80 01 00 00
  • 00 02 00 00
  • 80 02 00 00
  • 00 03 00 00
  • 80 03 00 00
  • 00 04 00 00
  • 80 04 00 00 → 十进制值1152
  • 00 05 00 00 → 十进制值1536
  • 80 05 00 00
  • 00 06 00 00
  • 80 06 00 00
  • 00 07 00 00 → 十进制值1792
  • FF FF FF FF  → 十进制值-1

    这些数值对应SAT数据所在的各个相对扇区编号。接下来我们只需要按照MSAT中指定的每个扇区位置依次读取数据(直到遇到值为-1的条目,该值标识对应槽位空闲),将读取到的所有数据按顺序拼接起来,最终就能得到完整的扇区分配表。

示例
    我们通过几个实际示例来进一步理清相关逻辑,让内容更清晰易懂。

    要计算对应的文件绝对偏移,我们直接套用之前介绍过的公式即可。

    第一个示例非常简单:
    (0 × 512) + 512 = 512,也就是十六进制的0x200。

    如果我们在十六进制编辑器中跳转到偏移量0x200的位置,就能看到如下内容:
sat0.jpg

    再看另一个示例,如果我们查看MSAT中的第二个条目,可以按如下方式进行计算:

    (128 × 512) + 512 = 66048(十进制),即十六进制的 0x10200。

    如果我们在十六进制编辑器中跳转到该偏移量位置,就能看到如下内容:
sat1.jpg

    当我们对所有扇区执行完上述操作后,就能定位到那些由SAT管理的数据(请记住,这对应的是所有大小超过4096字节的内容)。

    如果我们查看前文示例中SAT的第一块数据,会看到如下内容(已按4字节为一组进行分组):
  • FD FF FF FF → 十进制值 -3
  • 08 00 00 00 → 十进制值 8
  • 15 00 00 00 → 十进制值 21
  • 04 00 00 00 → 十进制值 4
  • 05 00 00 00 → 十进制值 5
  • 06 00 00 00 → 十进制值 6
  • 07 00 00 00 → 十进制值 7
  • 09 00 00 00 → 十进制值 9
  • 12 00 00 00 → 十进制值 18
  • 0A 00 00 00 → 十进制值 10

    以此类推。

    如果我们换一种方式来可视化呈现这一逻辑,就像这样:

sat.jpg
    我们就能把相关逻辑理解得更透彻了。

    构成SAT的每个扇区的第一个条目都有一个特殊标识FD FF FF FF,对应的十进制值是-3。如果你构建的SAT由多个扇区组成,就会在SAT中多次看到这个标识。由于每个扇区大小为512字节,每个扇区ID占32位,单个扇区最多可以存放128个扇区ID。基于这个特性,你会在SAT中每间隔128个条目就看到一次值为-3的标识。

    除此之外,既然我们已知这个标识永远是SAT扇区的第一个条目,就可以在跳转到对应绝对偏移后校验这个值:如果读到-3,说明跳转位置正确;如果没有读到,就说明之前的偏移计算存在错误。

    回忆一下头部的关键字段:目录起始位置的扇区ID(后续我们会详细说明目录的具体作用)。在上面示例的头部中,这个字段位于偏移量0x30处,本例中该字段的值为1。基于这个值,我们就可以开始构建存放目录的扇区数据链。

    和之前构建SAT的流程一样,我们需要顺着数据链依次访问每个扇区,读取数据后按顺序拼接。完成这一步后,就可以处理所有的目录条目了(相关内容后续展开说明)。

    回到前面示例的SAT中,我们查看1号槽位,得到值8;接着跳转到8号槽位,得到值18;之后再跳转到18号槽位查看对应数值。我们持续执行这个跳转流程,直到遇到值为-2的槽位——-2是数据链结束的标识。

    因此,对于组成目录的全部字节,我们重复上述流程即可:
从1号扇区开始,套用公式计算绝对偏移:(1 × 512) + 512 = 1024,也就是文件起始偏移后的0x400字节位置。跳转到该位置后,我们就能看到如下内容:
dir0.jpg

    1号槽位指向8号槽位,因此我们再次套用公式计算:
    (8 × 512) + 512 = 4608,即十六进制的0x1200。
    跳转到该偏移位置后,我们就能看到如下内容:
dir1.jpg

    组成目录的整条数据链中的所有扇区,都按照上述流程依次处理即可。

    你可能已经能从中总结出一套固定的处理模式,如果还没理清也完全不用担心,我们很快就会详细讲解目录的相关内容。这里最核心要掌握的要点是:如何定位目标数据链,进而读取到你所关注对象的完整字节内容。


休息片刻
    呼!刚才的内容信息量不小,先去倒杯咖啡放松一下吧,别着急,我会在这里等你。

言归正传
    现在我们已经完全掌握了SAT的相关逻辑,也理解了如何读取特定结构的完整字节内容,接下来就可以讲解SSAT了。SSAT的工作机制和SAT完全一致,它的作用是为短扇区存储区中存放的数据提供数据链查询能力。并非所有OLE复合文件都自带SSAT,你需要校验SSAT第一个扇区的扇区ID是否大于0:如果文件中不存在SSAT,该字段的值就会是-2。

    要获取SSAT的完整字节内容,我们顺着对应的数据链遍历即可,操作流程和之前读取目录的方式完全相同。完成读取后,你会得到和SAT结构类似的条目列表,同样包含槽位0、槽位1、槽位2等依次排列的条目。

    在我们上面的示例中,SSAT的第一个扇区对应的头部字段位于偏移量0x3C处。在之前示例的文件头部中,该字段的十进制值为640,也就是十六进制的0x280。再次套用偏移计算公式可得:(640 × 512) + 512 = 328192,即十六进制的0x50200。

    跳转到该偏移位置后,我们就能看到如下内容:
ssat0.jpg

    这部分的工作逻辑和我们之前构建SAT时完全一致。SAT和SSAT的区别仅在于我们最终获取目标数据的位置。不过在讲解这部分差异之前,我们需要先介绍目录结构。

目录
    目录由若干个目录条目组成,每个目录条目固定占用128字节。单个目录条目内存储的信息包括对象名称、对象类型、创建时间与修改时间、该目录条目对应数据的首个扇区ID,以及该目录条目的数据总字节大小。

    目录本质上是OLE复合文件中所有存储对象的总目录。回忆之前的内容,我们正是通过SAT遍历数据链,才拼接得到了目录的完整字节内容。目录字节流的开头部分呈现形式如下:
dir0 (1).jpg

    由于每个目录条目都固定占用128字节,接下来我们就对其中一个目录条目进行完整拆解分析。
dirEn.jpg

    因此,该目录条目的具体属性如下:
  • ‌名称‌:Root Entry
  • ‌名称长度‌:22(包含终止符)
  • ‌类型‌:05(5代表根存储对象;其他常见类型包括1表示存储对象、2表示流对象等)
  • ‌创建时间‌:未存储创建日期
  • ‌修改时间‌:2016年2月22日 18:09:43
  • ‌起始扇区ID‌:3
  • ‌大小‌:611,136 字节

    上述解析流程会对构成目录对象的每128字节数据重复执行。

Root Entry 的特殊性
    在上述示例中,我们可以看到名称为“Root Entry”,大小为611,136字节。Root Entry是一个特殊的目录条目,其存储的字节数据正是我们在读取通过SSAT(短扇区分配表)存储的对象时所需的基础数据空间。换句话说,所有小于4096字节的对象最终都会存放在Root Entry所指向的数据区域内。在Jump List(跳转列表)文件的分析场景中,这意味着几乎所有的.lnk文件数据都隐藏在Root Entry的字节内容中。

    你可以这样理解:SAT对应的是整个Jump List文件的扇区映射,而SSAT仅对应Root Entry数据区域内部的映射关系。在本例中,由于Root Entry的数据大小超过了4096字节,我们需要先利用SAT来读取Root Entry的完整数据。获取到这些数据后,将其切割成若干个64字节的块,并从0开始对这些块进行索引编号。

    在此基础上,当需要读取存储在SSAT中的对象数据时,我们通过SSAT构建数据链,并定位到由Root Entry引用的那些字节(即前文提到的611,136字节数据)。按照数据链的顺序找到每个64字节的块并将它们依次拼接。处理完成后,我们就得到了存储对象的完整字节内容(可能包含部分未使用的填充空间)。为了确定构成逻辑“文件”的有效字节范围,我们依据目录条目中记录的大小值进行截取;超出该大小直到拼接末尾的部分,均被视为空闲空间(Slack Space)。

    至此,我们已经掌握了SAT、SSAT和目录的核心工作机制,这也意味着我们具备了从OLE复合文件中定位数据块(以及获取其类型、时间戳等元数据)的能力。

    太棒了!
    接下来我们要介绍另一个特殊的目录条目:DestList。

DestList
    如果我们按照名为DestList的目录条目所指向的位置提取出对应的字节数据,就会得到如下形式的内容:
destEntryB.jpg

    DestList目录条目由一个32字节的头部,以及后续多个长度可变的DestList条目共同组成。

    该头部包含的核心字段有:
  • 版本号
  • DestList条目总数
  • 已固定的DestList条目数量
  • 最后一个已使用的条目编号

    而每个独立的DestList条目包含的核心字段有:
  • 卷Droid ID
  • 文件Droid ID
  • 原始卷Droid ID
  • 原始文件Droid ID
  • 主机名
  • 条目编号
  • 最后修改时间戳
  • 固定状态
  • 路径长度
  • 文件路径

    Windows 10之前版本的跳转列表使用的版本号为1,但在2015年7月15日正式发布RTM版本的Windows 10中,该版本号升级为3。这一变更的原因是DestList条目的内部结构发生了改动,几乎导致所有旧版跳转列表解析工具全部失效:工具可能直接崩溃,也可能仅能显示DestList中的部分内容,悄悄丢弃其余数据。

    我自从 Windows 10 发布以来就一直在使用它,因此也开始利用 Windows 10 的跳转列表(Jump Lists)进行测试。当我进行到 DestList 解析阶段时,在应用了之前链接中提到的(Metz 相关的)有效规范后,我的代码出现了故障。

    随后我通过搜索发现了一篇关于 Windows 10 跳转列表格式变更的参考资料:

    在那篇文章中,ssenyl 记录了他所观察到的变化。在编写完我的解析器后,基于单元测试中可用的数据,我可以确认他的结论是正确的。

    这两种格式之间的差异虽然不算巨大,但足以造成影响。在版本 1 中,文件路径位于偏移量 114 处;而在版本 3 中,文件路径则位于偏移量 130 处。

那么 .lnk 文件究竟在哪里呢?

    很高兴你问到这一点!请回想一下,目录(Directory)就像是跳转列表(Jump List)中所有存储内容的总目录。DestList 条目所引用的每一个 .lnk 文件,都作为独立的项目存储在目录中,并且拥有我们之前讨论目录时提到的所有相同信息(如名称、起始扇区等)。

    要获取构成 .lnk文件的实际字节数据,我们只需检查该 .lnk 文件的大小,然后根据情况使用 SAT(扇区分配表)或 SSAT(短扇区分配表)来构建数据链,从而收集所需的字节。

整合全流程
    抛开底层复杂的细节不谈,在处理自动目标(Automatic Destinations)跳转列表时,核心流程可以归纳为以下几步:
  • ‌处理所有目录条目‌:解析整个目录结构。
  • ‌定位 DestList‌:找到名为 DestList的特殊目录条目。
  • ‌处理 DestList 条目‌:解析 DestList 中的数据项。
  • ‌关联匹配‌:对于每个 DestList 条目,在目录中找到对应的 .lnk 文件条目。匹配条件通常为:DestListEntry.EntryNumber(条目编号)等于 DirectoryEntry.Name(目录条目名称)。
  • ‌提取 .lnk 数据‌:一旦找到了对应 .lnk 文件的目录条目,就可以根据其中记录的大小和起始扇区,去提取构成该 .lnk 文件的完整字节数据。
  • ‌展示结果‌:显示 DestList 中的元数据信息,并导出 .lnk 文件的相关详细信息。

    第 5 步是整个过程的关键,让我们更仔细地看一下这一步。

    下面是一个条目编号为 1112(即十六进制 0x458)的 DestList 条目示例。
entryNum.jpg

    接下来,我们需要查找目录名称(Directory Name)为 458 的目录条目。找到后,我们会看到它呈现如下形式:
DirEntry.jpg

    既然我们已经获取了对应的目录条目并定位到了 .lnk 文件,现在我们知道该目录条目的大小为 864 字节,并且由于该大小小于 4096 字节,我们需要使用 SSAT(短扇区分配表)来获取数据。

    因此,如果我们利用 SSAT 构建数据链、收集数据,并将其写入一个名为 458.lnk 的文件中,结果将如下所示:
458.jpg

    如果我们查看该文件的属性,就能看到其文件大小,这与之前从目录条目中获取的预期大小完全一致:

size.jpg

    最后,如果我们使用 LECmd 工具处理该文件,就能看到所有的详细信息:
leout.jpg

    现在,我们只需在几秒内重复这个过程数百次,就能完整获取跳转列表中的所有内容!

工具测试说明
    注意:如果有人知道其他支持(或不支持)Windows 10跳转列表解析的工具,欢迎告知我,我会更新这篇文章。据我所知,在X-Ways Forensics 18.8 preview 3版本中,已经可以正确解析Windows 10自动目标(auto dest)跳转列表。下面是我们一直在讨论的那个跳转列表在X-Ways中的部分输出结果:
xwf.jpg

    上面的「Total」属性值,就是DestList头部中「最后使用的条目编号」字段的值。

    下面这个示例展示了我们一直在讨论的这个自动目标跳转列表中可用的数据量。

    以下是我的OLE复合文件项目在解析自动目标跳转列表时所构建的所有对象。

    可以注意到,X-Ways中显示的DestList条目数量与我计算出的结果一致(673条),这也与DestList头部记录的数值相吻合。
example.jpg

    你可以看到 DestList 包含 673 个条目,而 DirectoryItems(目录项集合)则有 675 个。在这个示例中,每个 DestList 条目都对应一个独立的目录条目。多出来的那两个目录条目分别是 Root Entry(根入口)和 DestList 本身。

    上面的截图来自我在 Visual Studio 中调试 OLE 复合文件解析器时的界面。如果我们展开 DestList 条目集合,就会看到如下内容:
destListExpand.jpg

    这里我展开了673个DestList条目中的2个,你可以看到其中的详细信息。

    注意,第一个条目(编号0)引用的路径包含“DarkCometInformation.txt”,第二个条目(编号1)引用的是“Internet.pdf”,这两个条目在之前展示的X-Ways部分输出结果中也有对应记录。

    在后续的讨论中,请记住这个特定的自动目标跳转列表文件中一共包含673个DestList条目。

    下面是某款在推特圈广受推荐的热门工具,在解析同一个跳转列表文件时输出的结果:
toolout.jpg

    仅显示了第一个DestList条目。


    这是另一款热门工具的输出结果:

example2.jpg

    通过这个示例,你就能清晰地发现存在异常问题。

    这里还有另一款工具的运行结果,它也向我们暴露出了同类解析缺陷:

tool 3.jpg

    还有另一个(工具的运行结果):
linux.jpg

    还有另一个工具也暴露出了同类解析缺陷:
tool 4.jpg
    还有另一个(工具的运行结果):
tool 5.jpg


谁会在意这些细节呢?
    要知道,Windows 10 发布至今已经过去很长时间了,随着微软持续推进系统部署、冲击十亿装机量,取证分析人员会接触到越来越多的 Windows 10 设备。

    这正是单元测试这类工作发挥价值的地方(我们现在算是兜兜转转回到了原点)!如果在 Windows 10 正式发布后,就把 Windows 10 自动目标跳转列表的测试用例加入单元测试套件,这类格式兼容问题会立刻暴露出来,完全可以更早得到修复。

    我在自己的测试工作中就会采用这类做法:

unit.jpg

    最后一行代码的逻辑是:如果 DestList 条目的实际数量与头部报告中记录的数量不一致,测试应当判定为失败。你可以看到绿色的状态点表示测试通过,而且我的测试用例中还包含了来自 Windows 10 的、拥有超过 700 个 DestList 条目的复杂样本。

总结
    你还在看吗?太棒了!我真的很佩服你的毅力,毕竟连我自己写这篇长文时都觉得有点吃力。希望这篇文章能帮你更清晰地理解跳转列表的工作机制,以及通过单元测试进行自动化验证的重要性。

    我接下来即将发布的工具是 JLECmd,这是一款专门用于解析跳转列表(包括自定义目标和自动目标)的工具,支持从 Windows 7 到 Windows 10 的所有版本。该工具将在首次发布后开源,并托管在 GitHub 上。

    其输出结果将类似于 LECmd 提供的嵌入式 .lnk 文件详细信息,并在此基础上额外增加了 DestList 条目的相关元数据。

    下面是一个预览示例:
JLECmdOut.jpg

    它最终将支持导出在SAT和SSAT中发现的所有空闲空间,把全部lnk文件转储到指定目录(这就是我之前在Twitter上发布的相关内容,点此查看),以及更多功能。

    后续我还会推出其他惊喜功能,大幅降低跳转列表相关新研究的门槛,希望社区能借此获得更多收益。

    先告一段落啦!

原文链接

0

主题

279

回帖

600

积分

高级会员

积分
600
发表于 2026-9-11 23:21:50 | 显示全部楼层 来自
正如我们后续会看到的

0

主题

279

回帖

600

积分

高级会员

积分
600
发表于 2026-9-13 23:42:06 | 显示全部楼层 来自
先告一段落啦!

0

主题

279

回帖

600

积分

高级会员

积分
600
发表于 2026-9-13 23:42:38 | 显示全部楼层 来自
希望社区能借此获得更多收益
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表