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。虚表顺序为:
| 索引(从零开始) | 方法 |
|---|---|
| 0 | QueryInterface |
| 1 | AddRef |
| 2 | Release |
| 3 | QueryContextMenu |
| 4 | InvokeCommand |
| 5 | GetCommandString |
不能只搜索类名后直接取附近地址。还应检查定位器与类型描述符的引用关系、主对象偏移、虚表指针所在节及函数指针的可执行映射。多继承、接口布局变化或调整桩都可能影响判断。
定位结果必须唯一。如果类型信息消失、主接口布局变化或出现多个候选,应停止沿用该定位方式,转为重新分析接口创建与调用关系。
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 调用约定下,尚未建立栈帧的入口参数位置如下:
| 参数 | 入口位置 |
|---|---|
| this | RCX |
| hMenu | RDX |
| indexMenu | R8D |
| idCmdFirst | R9D |
| 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.0 | 0x11A10 | 0x10E10 | 立即数赋值 |
| 5.11.2.0 | 0x11A30 | 0x10E30 | 立即数赋值 |
| 5.12.0 | 0x11ED0 | 0x112D0 | MOV 赋值 |
RVA 是相对于映像基址的地址,不能与文件偏移混用。常规节内映射关系为:
文件偏移 = 节的 PointerToRawData + (RVA - 节的 VirtualAddress)使用前还需要确认 RVA 落在该节的文件支持范围内。上表仅用于识别已验证样本,不应作为其他文件的修改地址。
7.2 原始文件身份
以下 SHA-256 均对应未修改的 FastEx64.dll,用于区分同名或同版本的不同文件:
| 版本 | 原始 SHA-256 |
|---|---|
| 5.7.10.0 | 9860eaabb0aaeb2ed30bbd21b9d1637def3e2a012ba464899d2ac78aab5ba60c |
| 5.11.2.0 | 4c23b8aa24e42669d13e5fa34f9280763b89834b35ddbddd29a7f22bf00a9608 |
| 5.12.0 | d44323d1e062de3856a8736a6e23edfa5477278d2e5ce9861bf284705e8164a5 |
三个样本均通过结构分析与运行对照。5.11.2 使用与此前两个版本相同的定位和修改规则验证,没有增加该版本的专用地址或识别特例。
这说明同一思路适用于这三个实际样本。其他版本仍需重新检查结构并验证行为。
8. 菜单行为的验证方法
8.1 原版与修改版对照
在独立测试进程中加载 DLL,直接调用 DllGetClassObject 获取 COM 类工厂,再创建扩展对象,调用 IShellExtInit::Initialize 和 IContextMenu::QueryContextMenu。
每次查询使用新扩展对象和新菜单,减少内部重复菜单过滤对结果的干扰。记录返回值、命令项目及其显示内容,分别对比原版与修改版。
每个版本包含四组测试:
| 剪贴板语义 | 菜单设置 |
|---|---|
| 复制文件 | 当前真实设置 |
| 剪切文件 | 当前真实设置 |
| 复制文件 | 测试进程内受控设置 |
| 剪切文件 | 测试进程内受控设置 |
每组覆盖原版与修改版、选中文件夹/文件夹背景/右键拖放三种场景,以及以下十五组 uFlags:
0x0000 0x0004 0x0010 0x0014 0x01000x0104 0x0114 0x0200 0x0204 0x03000x8000 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. 后续版本的维护流程
新版本适配时,建议依次完成以下工作,并把结果补充到本文的样本记录中。
- 保留原始样本。 记录 FastCopy 版本、DLL 版本、架构、文件大小、SHA-256 和原始签名状态,确认文件未被其他方式修改。
- 确认实际加载对象。 区分传统扩展、32 位扩展和 Windows 11 新式扩展,核对 COM 注册指向的完整 DLL 路径。
- 重新定位接口。 验证 RTTI、完整对象定位器、虚表与函数边界。定位不唯一时停止沿用旧结论。
- 重新确认分类语义。 检查源、目标数量比较和布尔结果,恢复实际成员偏移。若分类规则变化,应更新前置判断。
- 核对调用约定和入口。 确认栈参数位置、完整指令边界、重定位要求及展开信息,避免直接套用旧入口字节。
- 检查 PE 可修改条件。 核对节表空间、节与文件边界、跳转范围、证书和附加数据,再生成独立修改副本。
- 完成对照回归。 覆盖复制与剪切、普通右键与 Shift、目录背景与选中对象、右键拖放,并加入明确的菜单正向检查。
- 在目标桌面验证。 确认注册路径、DLL 加载、显示行为和实际命令运行,记录操作系统与文件管理器环境。
可使用下面的记录格式,避免只有“支持/不支持”而缺少复现依据:
版本与样本: FastCopy 版本 / DLL 版本 / 架构 / 文件大小 原始 SHA-256 / 签名状态
定位依据: 接口及虚表 / 菜单函数 RVA / 文件偏移 函数边界 / 入口指令 / 场景分类规则及字段偏移
修改记录: 修改范围 / 新代码位置 / 跳转目标 PE 元数据变化 / 修改后 SHA-256 / 校验和
验证记录: 系统与文件管理器 / 菜单设置 / 剪贴板场景 普通菜单 / Shift 菜单 / 拖放 / 正向检查 文件完整性 / 桌面实际操作 / 未覆盖项目如果未来版本移除 RTTI、改变主接口、调整入口指令或使用不同菜单机制,应将其视为需要重新分析的对象,不能仅因为版本号接近就继续应用原方案。
10. 部署与恢复的环境因素
修改副本需要在扩展已卸载占用后替换。可先在对应的 FastCopy 中关闭外壳扩展并应用,让资源管理器及其他文件管理器卸载 DLL,保留原文件,再替换并重新启用。
需要核对注册路径:修改某个便携目录中的 DLL,并不会自动改变系统已注册的另一套 FastCopy 扩展路径。注册指向哪个文件,资源管理器就应加载哪个文件。
安装后的基本检查顺序是:复制测试文件,确认普通右键隐藏;按住 Shift 确认粘贴项出现;剪切后重复检查;在原设置启用拖放菜单时,再验证拖放选项。
恢复时按相同的卸载占用流程放回原文件并重新启用扩展。软件升级或修复安装可能覆盖修改,之后需要重新检查。修改副本不具备厂商原签名,签名强制策略也可能影响加载。
参考资料
- Microsoft:IContextMenu::QueryContextMenu,菜单标志及返回值语义。
- Microsoft:Creating Context Menu Handlers,静态命令、动态处理器及 Extended Verbs。
- Microsoft:x64 calling convention,寄存器参数、影子空间及栈参数。
- Microsoft:PE Format,节表、地址映射和数据目录。
- FastCopy 作者公开的旧版 shellext.cpp,传统扩展实现的辅助分析线索。
旧版公开源码不能代替对后续二进制的检查。本文的样本地址、字段偏移及兼容性结果来自实际 DLL 分析与菜单调用对照;后续维护应继续区分已观察事实、设计推断和未验证范围。