5006 字
25 分钟
实现FastCopy仅扩展右键时显示菜单
2026-11-12
无标签

AI生成的外壳扩展分析与维护记录,我是看不懂的,这里做个记录。

1. 背景与目标#

FastCopy 的外壳扩展可以把复制、删除、粘贴等操作加入资源管理器右键菜单。其中“粘贴”会根据剪贴板中的文件数据及目标位置决定是否显示。

为了保持日常右键菜单简洁,希望将这些项目改为按需显示:普通右键隐藏,按住 Shift 再右键时才进入 FastCopy 原来的菜单逻辑。同时,右键拖放文件后出现的复制、移动菜单应保留原有行为。

本文记录需求分析、二进制定位依据、修改方案和跨版本验证结果,可独立保存,供后续版本适配、故障排查和回归验证使用。文中不依赖特定补丁程序、开发目录或外部测试报告。

1.1 行为约定#

场景预期行为
普通右键不插入 FastCopy 的复制、删除、粘贴及子菜单
Shift+右键继续执行原始菜单逻辑
右键拖放继续执行原始拖放菜单逻辑
剪贴板没有可用文件数据不强制生成文件粘贴项
原设置关闭了某项功能保留该设置的效果

这里控制的是整个 FastCopy 普通右键菜单。如果只希望“粘贴”受 Shift 控制,而复制、删除仍在普通右键中出现,应改在粘贴项生成分支处增加判断,不能直接沿用本文的整体入口过滤。

1.2 已验证范围#

验证环境为 Windows 10 x64,对象是以下三个实际样本的传统外壳扩展 FastEx64.dll:

  • FastCopy 5.7.10.0。
  • FastCopy 5.11.2.0。
  • FastCopy 5.12.0。

FastExt1.dll 是另一套 32 位扩展;FastEx11.dll 涉及 Windows 11 新式菜单。二者均不属于本次修改与运行验证范围。版本名称相近或 DLL 同名,都不能替代对实际文件的检查。

2. 注册表 Extended 与动态外壳扩展的区别#

Windows 常见的右键菜单扩展方式包括静态命令和动态 COM 处理器:

静态命令:
文件类型\shell\命令名\command
动态处理器:
文件类型\shellex\ContextMenuHandlers\处理器名

对于静态命令,可以在命令对应的键下添加名为 Extended 的空字符串值,使其成为按住 Shift 才显示的扩展命令。

FastCopy 使用动态 COM 外壳扩展。资源管理器加载 DLL 后,由扩展结合选中对象、目录、剪贴板和选项决定插入哪些菜单项。

因此,直接给动态处理器注册项增加 Extended,不能自动约束 DLL 内部生成的菜单。本需求的切入点应是扩展的菜单构造逻辑。这样可以继续使用原有的文件剪贴板检测、复制与剪切识别,以及命令执行路径。

3. 使用菜单请求标志判断 Shift 条件#

传统菜单扩展通过 IContextMenu::QueryContextMenu 插入菜单项。接口参数如下:

HRESULT QueryContextMenu(
HMENU hMenu,
UINT indexMenu,
UINT idCmdFirst,
UINT idCmdLast,
UINT uFlags
);

其中 uFlags 包含调用方请求的菜单类型。与本需求有关的标志为:

CMF_EXTENDEDVERBS = 0x00000100

该标志表示调用方请求扩展命令。在资源管理器中,按住 Shift 再右键会请求这类命令。

应使用位测试:

bool wantsExtended = (uFlags & CMF_EXTENDEDVERBS) != 0;

不能写成 uFlags == 0x100,因为其他标志可能同时存在;也不能把任意非零标志视作 Shift 请求。

这与直接读取 GetKeyState(VK_SHIFT) 有区别:前者依据此次菜单请求,后者读取调用瞬间的键盘状态。对于实现 IContextMenu 的场景,优先沿用菜单调用方提供的语义。

需要注意,CMF_EXTENDEDVERBS 表示请求扩展命令,不是独立的键盘事件证据。其他文件管理器或调用方也可以自行传入该标志,其用户交互需要分别验证。

4. 保留右键拖放的场景分类#

FastCopy 的传统外壳扩展还处理右键拖放:按住鼠标右键拖动文件,到目标位置松开,再选择复制或移动。

如果所有调用都必须包含 CMF_EXTENDEDVERBS,拖放菜单也可能被隐藏。因此,新增条件必须区分普通右键与拖放。

在已检查的样本中,函数前部根据内部源对象数组和目标对象数组是否都有内容,生成拖放分类结果。维护时应识别这段实际判断,沿用原有分类规则,不能仅凭当前鼠标状态推测场景。

整体逻辑可表示为:

// 行为示意;isDragDrop 应来自已确认的原始分类规则。
if (!isDragDrop && !(uFlags & CMF_EXTENDEDVERBS)) {
return MAKE_HRESULT(SEVERITY_SUCCESS, 0, 0);
}
// 继续执行原始 QueryContextMenu。

返回成功且命令编号增量为零,表示没有增加命令。此分支不调用粘贴或复制命令。

进入菜单函数
│
├─ 请求扩展命令 ──────────→ 进入原函数
│
└─ 未请求扩展命令
│
├─ 原始规则判定为拖放 ─→ 进入原函数
│
└─ 普通右键 ─────────→ 返回成功,不插入菜单

放行只意味着进入原函数。原来的菜单选项、剪贴板检查、重复菜单过滤及 CMF_DEFAULTONLY 等标志处理仍由原函数决定,不能将放行等同于必定显示。

5. 从固定偏移修改转向结构定位#

5.1 固定偏移适合单个已知文件#

最初针对 5.7.10 的修改,在菜单函数内部增加跳转,并使用已确认的函数间填充区存放判断。对于完整哈希、原始字节和函数布局均已确认的文件,这种方式可以验证需求。

但固定偏移与特定构建绑定。函数位置、寄存器分配、条件分支和填充区都可能随编译变化。后续检查 5.12.0 时,函数地址及内部菜单分支确实发生变化,因此没有直接复用旧地址。

跨版本维护应先重新确认结构,再决定如何改动。

5.2 沿 RTTI 和虚表定位菜单函数#

已验证样本保留了 MSVC 的运行时类型信息(RTTI),其中可以找到:

.?AVShellExt@@

可沿下面的结构关系定位:

ShellExt 类型描述符
→ 主完整对象定位器(Complete Object Locator)
→ 对应虚表
→ IContextMenu::QueryContextMenu

在本次确认的类布局中,主接口对应 IContextMenu。虚表顺序为:

索引(从零开始)方法
0QueryInterface
1AddRef
2Release
3QueryContextMenu
4InvokeCommand
5GetCommandString

不能只搜索类名后直接取附近地址。还应检查定位器与类型描述符的引用关系、主对象偏移、虚表指针所在节及函数指针的可执行映射。多继承、接口布局变化或调整桩都可能影响判断。

定位结果必须唯一。如果类型信息消失、主接口布局变化或出现多个候选,应停止沿用该定位方式,转为重新分析接口创建与调用关系。

5.3 确认函数边界与拖放分类#

对于本次 x64 样本,可使用 PE 异常目录中的运行时函数表确认菜单函数边界,再检查入口和前部的场景分类指令。

分类分析应覆盖:

  • 两次数量比较实际使用的对象基址与成员偏移。
  • 条件跳转是否指向预期的同一失败路径。
  • 成功与失败路径是否生成明确的布尔结果。
  • 中间指令是否修改了对象基址、栈指针或相关寄存器。

三个样本恢复出的数量字段偏移均为 0x30 与 0x20。这是样本分析结果,应在每次适配中从实际指令确认,不能当成 FastCopy 的稳定公开 ABI。

5.12.0 的布尔赋值形式与其他两个版本不同。可复用的依据是已核实的比较、跳转和赋值关系,而非所有指令字节完全相同。

6. 在入口增加前置判断#

6.1 修改位置与跳转结构#

后续采用的方案是在 PE 中新增只读、可执行节 .fcshift,把前置判断放在其中;原函数入口替换为一个 5 字节相对跳转。

原入口:JMP 新增判断
│
├─ 隐藏 ─→ 返回成功
│
└─ 放行 ─→ 重放入口指令 ─→ JMP 原函数后续代码

该方案减少了对内部菜单分支的依赖,也不需要复用函数间填充区。

已验证样本的入口第一条指令为:

44 89 4C 24 20 mov dword ptr [rsp + 20h], r9d

它恰好占 5 字节,且不包含需要随地址变化重定位的 RIP 相对操作数,可以在新位置重放后跳回原入口加 5 的位置。

后续版本如果入口发生变化,必须重新确认完整指令长度、可迁移性、被覆盖范围是否存在其他入口,以及是否涉及展开元数据。不能为了凑足 5 字节而截断指令,也不能直接搬运依赖原位置的指令。

6.2 x64 参数位置#

该 COM 方法有隐含的 this 参数。Windows x64 调用约定下,尚未建立栈帧的入口参数位置如下:

参数入口位置
thisRCX
hMenuRDX
indexMenuR8D
idCmdFirstR9D
idCmdLast[rsp + 0x28]
uFlags[rsp + 0x30]

因此,入口可以用下面的指令检查扩展命令标志:

test dword ptr [rsp + 30h], 100h

这个栈偏移只适用于相应调用约定下、尚未调整栈指针的位置。若把判断移到原函数中段,必须重新计算参数位置。

前置判断不调用其他函数、不分配栈空间,也不修改非易失寄存器。放行时重放被覆盖指令,继续进入原函数。原函数已有的展开信息保留;如果后续实现引入压栈、寄存器保存或函数调用,需要重新评估新增代码的展开要求。

6.3 PE 结构与签名#

新增节前应检查文件头可用空间、节对齐、文件对齐、节区边界、数据目录和附加数据。存在重叠、未知覆盖数据或节表空间不足时,应停止修改。

完成后应更新节数量、节头、相关大小字段、SizeOfImage 和 PE 校验和,并确认相对跳转位移正确。

修改代码后,原 Authenticode 签名不再有效。本次修改副本移除了失效的证书目录及相应证书数据。PE 校验和正确不代表数字签名有效,也不能通过重算校验和恢复厂商签名。

证书位置与长度需要先确认。若其与附加数据的关系不符合预期,不应直接截断文件。

7. 已验证样本与定位结果#

7.1 地址记录#

FastCopy 版本QueryContextMenu RVA对应文件偏移布尔赋值形式
5.7.10.00x11A100x10E10立即数赋值
5.11.2.00x11A300x10E30立即数赋值
5.12.00x11ED00x112D0MOV 赋值

RVA 是相对于映像基址的地址,不能与文件偏移混用。常规节内映射关系为:

文件偏移 = 节的 PointerToRawData + (RVA - 节的 VirtualAddress)

使用前还需要确认 RVA 落在该节的文件支持范围内。上表仅用于识别已验证样本,不应作为其他文件的修改地址。

7.2 原始文件身份#

以下 SHA-256 均对应未修改的 FastEx64.dll,用于区分同名或同版本的不同文件:

版本原始 SHA-256
5.7.10.09860eaabb0aaeb2ed30bbd21b9d1637def3e2a012ba464899d2ac78aab5ba60c
5.11.2.04c23b8aa24e42669d13e5fa34f9280763b89834b35ddbddd29a7f22bf00a9608
5.12.0d44323d1e062de3856a8736a6e23edfa5477278d2e5ce9861bf284705e8164a5

三个样本均通过结构分析与运行对照。5.11.2 使用与此前两个版本相同的定位和修改规则验证,没有增加该版本的专用地址或识别特例。

这说明同一思路适用于这三个实际样本。其他版本仍需重新检查结构并验证行为。

8. 菜单行为的验证方法#

8.1 原版与修改版对照#

在独立测试进程中加载 DLL,直接调用 DllGetClassObject 获取 COM 类工厂,再创建扩展对象,调用 IShellExtInit::Initialize 和 IContextMenu::QueryContextMenu。

每次查询使用新扩展对象和新菜单,减少内部重复菜单过滤对结果的干扰。记录返回值、命令项目及其显示内容,分别对比原版与修改版。

每个版本包含四组测试:

剪贴板语义菜单设置
复制文件当前真实设置
剪切文件当前真实设置
复制文件测试进程内受控设置
剪切文件测试进程内受控设置

每组覆盖原版与修改版、选中文件夹/文件夹背景/右键拖放三种场景,以及以下十五组 uFlags:

0x0000 0x0004 0x0010 0x0014 0x0100
0x0104 0x0114 0x0200 0x0204 0x0300
0x8000 0x0101 0x0102 0x0105 0x0106

这些组合覆盖含扩展命令标志、不含该标志以及默认命令等其他标志混合的情况,检查是否存在错误放行或改变原处理逻辑的问题。

调用数量为:

每组:2 份 DLL × 3 种场景 × 15 组标志 = 90 次
每版本:4 组 × 90 次 = 360 次
三个版本合计:1080 次

这表示实际调用数量,不表示 1080 个相互独立的功能场景。

8.2 明确的通过条件#

普通右键且不属于拖放时,修改版应返回成功、不增加菜单。包含扩展命令标志或属于拖放时,应保持与原版一致的返回结果和菜单内容。

同时必须有正向检查:

  • Shift 场景下,文件剪贴板确实能产生粘贴项。
  • 受控设置中,右键拖放确实能产生原有复制、移动菜单。

仅仅比较“原版与修改版都为空”,不能证明功能被正确保留。当前设置如果只启用了粘贴,没有文件剪贴板时就容易出现这种假通过。

测试中的受控设置通过在测试进程内临时调整 GetMenuFlags 的返回值实现,仅用于覆盖相关分支,没有写注册表或保存到修改后的 DLL。

8.3 文件剪贴板与隔离#

测试构造 CF_HDROP 文件列表,并通过 Preferred DropEffect 表示复制或剪切意图,分别验证两类粘贴场景。

为了避免影响用户当前剪贴板,测试进程创建带唯一名称的私有 window station 和桌面,在其独立剪贴板中放入测试数据。

使用唯一名称有助于避免并行测试相互干扰。隔离测试失败时,应先检查桌面、窗口站和剪贴板访问条件,再判断是否为扩展行为异常。

8.4 文件完整性与拒绝场景#

文件层面验证了:修改后的 PE 校验和正确,原有各节除了菜单入口 5 字节外保持一致,原始输入未改变。

已有十四项结构、拒绝及完整性检查覆盖了错误架构、类型信息缺失、未知入口、节表空间不足、异常附加数据、重复修改和已有输出内容保护等情况;5.11.2 还补充检查了自身文件完整性与重复修改拒绝。

这些检查用于避免把不符合前提的文件当作兼容样本,不能替代运行对照。

8.5 尚未覆盖的验证#

已完成的是菜单构造函数的直接调用验证,没有通过资源管理器 GUI 完成安装后的端到端操作,也没有执行实际复制、移动命令。

因此,已验证结论限于修改的菜单显示逻辑及其对原菜单路径的保留。扩展注册、资源管理器加载、签名策略和实际命令执行仍需在部署环境中确认。

9. 后续版本的维护流程#

新版本适配时,建议依次完成以下工作,并把结果补充到本文的样本记录中。

  1. 保留原始样本。 记录 FastCopy 版本、DLL 版本、架构、文件大小、SHA-256 和原始签名状态,确认文件未被其他方式修改。
  2. 确认实际加载对象。 区分传统扩展、32 位扩展和 Windows 11 新式扩展,核对 COM 注册指向的完整 DLL 路径。
  3. 重新定位接口。 验证 RTTI、完整对象定位器、虚表与函数边界。定位不唯一时停止沿用旧结论。
  4. 重新确认分类语义。 检查源、目标数量比较和布尔结果,恢复实际成员偏移。若分类规则变化,应更新前置判断。
  5. 核对调用约定和入口。 确认栈参数位置、完整指令边界、重定位要求及展开信息,避免直接套用旧入口字节。
  6. 检查 PE 可修改条件。 核对节表空间、节与文件边界、跳转范围、证书和附加数据,再生成独立修改副本。
  7. 完成对照回归。 覆盖复制与剪切、普通右键与 Shift、目录背景与选中对象、右键拖放,并加入明确的菜单正向检查。
  8. 在目标桌面验证。 确认注册路径、DLL 加载、显示行为和实际命令运行,记录操作系统与文件管理器环境。

可使用下面的记录格式,避免只有“支持/不支持”而缺少复现依据:

版本与样本:
FastCopy 版本 / DLL 版本 / 架构 / 文件大小
原始 SHA-256 / 签名状态
定位依据:
接口及虚表 / 菜单函数 RVA / 文件偏移
函数边界 / 入口指令 / 场景分类规则及字段偏移
修改记录:
修改范围 / 新代码位置 / 跳转目标
PE 元数据变化 / 修改后 SHA-256 / 校验和
验证记录:
系统与文件管理器 / 菜单设置 / 剪贴板场景
普通菜单 / Shift 菜单 / 拖放 / 正向检查
文件完整性 / 桌面实际操作 / 未覆盖项目

如果未来版本移除 RTTI、改变主接口、调整入口指令或使用不同菜单机制,应将其视为需要重新分析的对象,不能仅因为版本号接近就继续应用原方案。

10. 部署与恢复的环境因素#

修改副本需要在扩展已卸载占用后替换。可先在对应的 FastCopy 中关闭外壳扩展并应用,让资源管理器及其他文件管理器卸载 DLL,保留原文件,再替换并重新启用。

需要核对注册路径:修改某个便携目录中的 DLL,并不会自动改变系统已注册的另一套 FastCopy 扩展路径。注册指向哪个文件,资源管理器就应加载哪个文件。

安装后的基本检查顺序是:复制测试文件,确认普通右键隐藏;按住 Shift 确认粘贴项出现;剪切后重复检查;在原设置启用拖放菜单时,再验证拖放选项。

恢复时按相同的卸载占用流程放回原文件并重新启用扩展。软件升级或修复安装可能覆盖修改,之后需要重新检查。修改副本不具备厂商原签名,签名强制策略也可能影响加载。

参考资料#

旧版公开源码不能代替对后续二进制的检查。本文的样本地址、字段偏移及兼容性结果来自实际 DLL 分析与菜单调用对照;后续维护应继续区分已观察事实、设计推断和未验证范围。

实现FastCopy仅扩展右键时显示菜单
https://blog.firpe.cn/posts/实现fastcopy仅扩展右键时显示菜单/
作者
杉
发布于
2026-11-12
许可协议
CC BY-NC-SA 4.0