【AI】TOTALCMD64.EXE 11.58 GUI 分析报告
TOTALCMD64.EXE GUI 分析报告
- TOTALCMD64.EXE GUI 分析报告
- 1. 概述
- 2. 汇总统计
- 3. 窗体目录
- 4. 主窗口分析: TTOTAL_CMD
- 5. 关键事件处理器 IDA 反编译分析
- 5.1 TTOTAL_CMD_FormCreate (0x7BE270, 5656 字节)
- 5.2 TTOTAL_CMD_FormClose (0x7E95E0, 968 字节)
- 5.3 TTOTAL_CMD_idbCopyClick (0x7DC8F0, 641 字节)
- 5.4 TTOTAL_CMD_idbDeleteClick (0x7DD0F0, 152 字节)
- 5.5 TTOTAL_CMD_idbMkdirClick (0x7DD190, 315 字节)
- 5.6 TTOTAL_CMD_idbRenmovClick (0x7DCB80, 603 字节)
- 5.7 TTOTAL_CMD_DoubleClick (0x7D2380, 462 字节)
- 5.8 TTOTAL_CMD_FormShow (0x7C4EB0, 6264 字节)
- 5.9 TLister_FormKeyDown (0x65A320, 5466 字节)
- 5.10 TFindFile_FormCreate (0x68AA20, 3827 字节)
- 6. 关键对话框窗体
- 7. 事件处理器模式
- 8. IDA 分析脚本
- 9. VMT 地址快速参考
- 10. VMT 解析功能详解
生成日期: 2026-07-06 | 二进制文件: TOTALCMD64.EXE (Lazarus/FPC 64 位, LCL 框架)
IDA 数据库: TOTALCMD64.EXE.i64
1. 概述
TOTALCMD64.EXE 使用 Free Pascal Compiler (FPC) 编译, 基于 Lazarus Component Library (LCL) 框架 (与 Delphi 的 VCL 兼容)。GUI 资源以 TPF0/TPF1 LFM (Lazarus Form Module) 二进制资源形式嵌入可执行文件中。
FPC 64 位 VMT 结构
每个 LCL 类都拥有一个 虚拟方法表 (Virtual Method Table, VMT), 其布局如下 (13 个 qword 字段, 共 104 字节):
| 偏移 | 字段 | 说明 |
|---|---|---|
| +0 | vInstanceSize | 实例大小 (uint64) |
| +8 | vInstanceSize2 | -vInstanceSize (校验值) |
| +16 | vParentRef | 父类 VMT 指针 |
| +24 | vClassName | Pascal ShortString 类名指针 |
| +32 | vDynamicTable | 动态方法表指针 |
| +40 | vMethodTable | 已发布方法表指针 |
| +48 | vFieldTable | 已发布字段表指针 |
| +56 | vTypeInfo | RTTI 类型信息指针 |
| +64 | vInitTable | 初始化表指针 |
| +72 | vAutoTable | 自动化表指针 |
| +80 | vIntfTable | 接口表指针 |
| +88 | vMsgStrPtr | 消息字符串指针 |
| +96 | vDestroy | 析构函数代码地址 |
方法表格式 (位于 vMethodTable 所指地址):
+0: count (uint32, 方法数量)+4: entries[count], 每项 16 字节:{name_ptr:uint64, code_ptr:uint64}
IDA 结构体类型定义与应用
以下结构体类型已通过 MCP 在 IDB 中声明, 并 应用到全部 72 个 VMT 及方法表:
struct FPC_VMT {
unsigned __int64 vInstanceSize;
unsigned __int64 vInstanceSize2;
unsigned __int64 vParentRef;
unsigned __int64 vClassName;
unsigned __int64 vDynamicTable;
unsigned __int64 vMethodTable;
unsigned __int64 vFieldTable;
unsigned __int64 vTypeInfo;
unsigned __int64 vInitTable;
unsigned __int64 vAutoTable;
unsigned __int64 vIntfTable;
unsigned __int64 vMsgStrPtr;
unsigned __int64 vDestroy;
};
struct FPC_MethodTableEntry {
unsigned __int64 name_ptr; // -> Pascal ShortString (方法名)
unsigned __int64 code_ptr; // -> 函数代码地址
};
struct FPC_MethodTable {
unsigned int count;
FPC_MethodTableEntry entries[1]; // 变长数组
};
状态: 已通过 MCP py_eval 完成 — 全部 72 个 VMT 已:
- 命名为
{ClassName}_VMT(例如0xC855F8处的TTOTAL_CMD_VMT) - 应用
FPC_VMT结构体类型 (104 字节) - 方法表命名为
{ClassName}_MethodTable并应用FPC_MethodTable结构体类型 - 类名字符串命名为
str_{ClassName} - 为关键 VMT (TTOTAL_CMD, TDLGCONFIGALL, TLister, TFindFile, TMultiRename, TCmpForm, TFileCompForm) 添加注释
IDB 已保存到磁盘。setup_vmt_structs.py 脚本也可手动执行以重新应用类型。
2. 汇总统计
| 指标 | 数量 |
|---|---|
| LFM 资源 (TPF0/TPF1) | 79 |
| 已解析窗体 | 78 |
| 唯一窗体类名 | 75 |
| 窗体类 VMT (有 LFM 对应) | 72 |
| 全部 VMT (直接扫描法) | 561 |
| 无 VMT 的类 | 3 |
| 组件总数 | 1534 |
| 事件处理器总数 (LFM) | 1291 |
| VMT 方法总数 | 1534 |
| 已重命名函数 | 1534 (全部来自先前运行的 ALREADY_SET) |
| 已命名并应用类型的 VMT | 72 (通过 MCP py_eval) |
| 已命名并应用类型的方法表 | 72 (通过 MCP py_eval) |
| 已命名的类名字符串 | 72 (通过 MCP py_eval) |
| 已添加的注释 | 7 个关键 VMT + 7 个方法表 |
注: "窗体类 VMT" 指有 LFM 资源对应的窗体类 (72 个), 已通过 MCP 命名并应用结构体类型。"全部 VMT" 指通过
scan_all_vmts()直接扫描法在数据段中找到的所有 FPC VMT (561 个), 包括 TList、TStream、TStringList 等非窗体基础类。第 9 节列出的是 72 个窗体类 VMT。
未找到 VMT 的类
| 类名 | LFM 地址 |
|---|---|
| TCalendarPopupForm | 0xE51BC0 |
| TFindDialogForm | 0xE53660 |
| TReplaceDialogForm | 0xE54C4C |
这些窗体很可能使用运行时创建而无静态 VMT 引用, 或其 VMT 是动态构造的。
3. 窗体目录
全部 78 个已解析窗体, 含 LFM 地址、VMT 地址及方法数量:
| 序号 | 类名 | LFM 地址 | VMT 地址 | 方法数 | 顶层事件 | 子组件 |
|---|---|---|---|---|---|---|
| 1 | TNASTYNAGSCREEN | 0xBE4800 | 0xBE3698 | 11 | 6 | 1 |
| 2 | TATTRIBDLG | 0xBE8558 | 0xBE7908 | 24 | 6 | 13 |
| 3 | TUSERMENUCHANGE | 0xBEA9F8 | 0xBE9F20 | 36 | 6 | 5 |
| 4 | TDRIVEDLG | 0xBED138 | 0xBEC7B8 | 10 | 4 | 7 |
| 5 | TASSOCIATEDLG | 0xBEE618 | 0xBEDA10 | 15 | 5 | 14 |
| 6 | TDupeSelForm | 0xBEFD00 | 0xBEF370 | 22 | 6 | 4 |
| 7 | TVOLLABELDLG | 0xBF25F8 | 0xBF1C48 | 7 | 4 | 6 |
| 8 | TDLGUNZIPALL | 0xBF39C0 | 0xBF2C48 | 12 | 4 | 15 |
| 9 | TDLGZIP | 0xBF5670 | 0xBF4B60 | 29 | 5 | 9 |
| 10 | TDLGPACKERINFO | 0xBF7D38 | 0xBF7178 | 8 | 4 | 5 |
| 11 | TQUICKSEARCH | 0xBF9CB0 | 0xBF91B0 | 8 | 3 | 1 |
| 12 | TSTDTREEDLG | 0xBFB4B0 | 0xBFA928 | 15 | 5 | 13 |
| 13 | TCHANGETREEDLG | 0xBFCC18 | 0xBFC190 | 28 | 7 | 6 |
| 14 | TINFODLG | 0xBFEE40 | 0xBFDC18 | 10 | 7 | 2 |
| 15 | TVERSIONINFODLG | 0xC01BD0 | 0xC01258 | 4 | 4 | 6 |
| 16 | TBUTTONCHANGEDLG | 0xC035D0 | 0xC02AA0 | 43 | 5 | 18 |
| 17 | TONEBUTTONCHANGEDLG | 0xC064A8 | 0xC05990 | 28 | 4 | 19 |
| 18 | TDLGUNZIP | 0xC08560 | 0xC07C00 | 9 | 5 | 12 |
| 19 | TProgressDialog | 0xC09C88 | 0xC092C0 | 19 | 6 | 9 |
| 20 | TDLG2FILEACTIONMIN | 0xC0D730 | 0xC0CB10 | 10 | 6 | 11 |
| 21 | TExtMsgForm | 0xC0EBB8 | 0xC0E1F8 | 9 | 6 | 14 |
| 22 | TCOMBOINPUT | 0xC118B8 | 0xC10B08 | 13 | 5 | 12 |
| 23 | TDLGCONFIGALL | 0xC16A68 | 0xC141F0 | 214 | 6 | 16 |
| 24 | TLister | 0xC3C638 | 0xC3A180 | 62 | 15 | 3 |
| 25 | TFindFile | 0xC41238 | 0xC3F9F8 | 81 | 8 | 4 |
| 26 | TDriveSelBox | 0xC46EB0 | 0xC46520 | 9 | 5 | 7 |
| 27 | TLISTCONF | 0xC48670 | 0xC47978 | 22 | 5 | 5 |
| 28 | TOverWriteForm | 0xC4BFC0 | 0xC4B580 | 21 | 8 | 11 |
| 29 | TSEARCHTEXT | 0xC4DE60 | 0xC4D4F0 | 7 | 3 | 12 |
| 30 | TFireWallDlg | 0xC510D0 | 0xC50768 | 6 | 3 | 7 |
| 31 | TCheckEditBox | 0xC52C70 | 0xC52240 | 15 | 6 | 15 |
| 32 | TNewConnDlg | 0xC54440 | 0xC53A30 | 11 | 3 | 11 |
| 33 | TDETAILS | 0xC559D8 | 0xC54EF0 | 23 | 5 | 6 |
| 34 | TChmodForm | 0xC59568 | 0xC58BF0 | 7 | 4 | 10 |
| 35 | TEditTypeForm | 0xC5B4F0 | 0xC5A910 | 24 | 6 | 22 |
| 36 | TEdActionForm | 0xC5D1A8 | 0xC5C7A8 | 9 | 4 | 14 |
| 37 | TChooseIcon | 0xC5ED08 | 0xC5E308 | 12 | 4 | 9 |
| 38 | TFileCompForm | 0xC60A70 | 0xC5F9D0 | 63 | 8 | 1 |
| 39 | TPrintConfDlg | 0xC65F70 | 0xC655F8 | 9 | 4 | 8 |
| 40 | TDlgCustomColors | 0xC687E8 | 0xC67CB0 | 17 | 5 | 5 |
| 41 | Thostdefinedlg | 0xC6A3B0 | 0xC695E8 | 10 | 4 | 11 |
| 42 | TEncodeEditBox | 0xC6BEE0 | 0xC6B588 | 11 | 4 | 14 |
| 43 | TMultiRename | 0xC6E410 | 0xC6D0F0 | 69 | 8 | 9 |
| 44 | TCONNECT | 0xC72410 | 0xC718D8 | 24 | 5 | 5 |
| 45 | TInpComboDlg | 0xC73E00 | 0xC733E0 | 26 | 6 | 16 |
| 46 | TSplitEditBox | 0xC776F0 | 0xC76C98 | 8 | 4 | 10 |
| 47 | TPrintDlg | 0xC78BC0 | 0xC780E8 | 23 | 6 | 2 |
| 48 | TMarginsDlg | 0xC7AB58 | 0xC7A1E0 | 5 | 4 | 6 |
| 49 | TCmpForm | 0xC7D318 | 0xC7BF18 | 68 | 10 | 2 |
| 50 | TCmtEditForm | 0xC82C18 | 0xC82268 | 9 | 3 | 10 |
| 51 | TFsPluginConfigForm | 0xC84030 | 0xC83640 | 13 | 6 | 10 |
| 52 | TTOTAL_CMD | 0xC87350 | 0xC855F8 | 99 | 10 | 3 |
| 53 | TCustomColumnForm | 0xC8F720 | 0xC8ECB0 | 17 | 4 | 5 |
| 54 | TCmdSelForm | 0xC91860 | 0xC90E98 | 22 | 7 | 5 |
| 55 | TDateTimePickForm | 0xC93380 | 0xC92A20 | 4 | 4 | 4 |
| 56 | TChooseEncodingForm | 0xC94550 | 0xC93BF8 | 5 | 4 | 7 |
| 57 | TPathSelForm | 0xC95E88 | 0xC95158 | 19 | 7 | 6 |
| 58 | TPassForm | 0xC97470 | 0xC969E0 | 8 | 3 | 5 |
| 59 | TShellExtForm | 0xC99520 | 0xC98BC0 | 13 | 4 | 3 |
| 60 | TCrcForm | 0xC9DF88 | 0xC9D518 | 7 | 5 | 11 |
| 61 | TPluginSelForm | 0xC9FB60 | 0xC9F090 | 22 | 7 | 5 |
| 62 | TCheckMsgBox | 0xCBDFB0 | 0xCBD598 | 7 | 6 | 7 |
| 63 | TDriveTip | 0xD42358 | 0xD41570 | 3 | 3 | 0 |
| 64 | TPrintForm | 0xD4C7D0 | 0xD4BE70 | 3 | 3 | 4 |
| 65 | TICONLIBCHANGEDLG | 0xD54C48 | 0xD54200 | 9 | 4 | 11 |
| 66 | TCheckComboBox | 0xD5DD30 | 0xD5D3B0 | 10 | 6 | 14 |
| 67 | TTabIconDialog | 0xD60890 | 0xD5FF38 | 3 | 3 | 5 |
| 68 | TCryptOptions | 0xD61B90 | 0xD61238 | 1 | 1 | 6 |
| 69 | TButtonEditBox | 0xD62C80 | 0xD62328 | 4 | 3 | 10 |
| 70 | THistoryForm | 0xD650B0 | 0xD643A0 | 15 | 6 | 5 |
| 71 | TSyncStartForm | 0xD685F0 | 0xD67C98 | 5 | 3 | 21 |
| 72 | TSYNCEXTINPUT | 0xD69C88 | 0xD69310 | 10 | 4 | 13 |
| 73 | TCalendarPopupForm | 0xE51BC0 | (无) | - | 3 | 1 |
| 74 | TDupeSelForm (第二个) | 0xE51D98 | 0xBEF370 | 22 | 6 | 4 |
| 75 | TFindDialogForm | 0xE53660 | (无) | - | 1 | 9 |
| 76 | TReplaceDialogForm | 0xE54C4C | (无) | - | - | - |
方法数最多的窗体: TDLGCONFIGALL (214), TTOTAL_CMD (99), TFindFile (81), TMultiRename (69), TCmpForm (68), TFileCompForm (63), TLister (62)。
4. 主窗口分析: TTOTAL_CMD
4.1 LFM 属性
Address: 0xC87350
VMT: 0xC855F8 (99 methods)
Caption: 'Total Commander'
Dimensions: Left=614, Top=176, Width=622, Height=421
ClientSize: 622 x 401
Font: MS Sans Serif, Height=-11, Bold
KeyPreview: True
Menu: MainMenu
ShowInTaskBar: stAlways
LCLVersion: 0.9.31
4.2 组件层次结构
TTOTAL_CMD: TOTAL_CMD
├── TMyPanel: RightPanel (Tag=0)
│ ├── TMySpeedButton: RightRoot (Tag=160, Caption='\')
│ ├── TMySpeedButton: RightParent (Tag=161, Caption='..')
│ ├── TMyPanel: RightInfo (Tag=240)
│ ├── TPathPanel: RightPath (Tag=250)
│ └── THeaderClick: RightHeader
├── TMyPanel: LeftPanel
│ ├── TMySpeedButton: LeftRoot (Tag=160)
│ ├── TMySpeedButton: LeftParent (Tag=161)
│ ├── TMyPanel: LeftInfo (Tag=240)
│ ├── TPathPanel: LeftPath (Tag=250)
│ └── THeaderClick: LeftHeader
├── TBitBtn: RenameOKBtn
└── TMainMenu: MainMenu
└── Files (TMenuItem)
├── FilesChangeattributes
├── FilesPack
├── FilesUnpackspecificfiles
├── Testarchives1
├── Comparebycontent1
├── FilesAssociatewith
├── Changeinternalassociations1
├── Filesversioninfo
├── MenuItemID503 (Mark menu)
│ ├── Selectallwithsameextension1
│ ├── Saveselection1
│ ├── MarkRestoreSelection
│ ├── Saveselectiontofile1
│ ├── Loadselectionfromfile1
│ ├── Copy1
│ ├── Copytoclipboardwithfullpath1
│ ├── Copytoclipboardwithalldetails1
│ ├── Copytoclipboardwithpathdetails1
│ ├── MarkComparedirectories
│ └── MarkMarknewerhidesamefiles
├── Commands
│ ├── CommandsCDtree
│ ├── CommandsSearch
│ ├── CommandsVolumelabel
│ ├── CommandsSystemInformation
│ ├── Synchronizedirs1
│ └── ShowRereadsource
├── Configuration
│ ├── ConfigurationConfiguration
│ ├── ConfigurationButtonbar
│ ├── ConfigurationButtonbar2
│ ├── ConfigurationIniChange
│ ├── DarkNormalMenu
│ ├── ConfigurationSaveposition
│ └── ConfigurationSaveSettings
└── Help
├── HelpIndex
├── HelpKeyboard
├── HelpRegistrationInfo
├── VisittheWebsite1
├── CheckForUpdates1
└── HelpAbout
4.3 VMT 方法 (共 99 个, 按类别分组)
窗体生命周期事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7BE270 | FormCreate | TTOTAL_CMD_FormCreate | 窗口初始化 (5656 字节) |
| 0x7C4EB0 | FormShow | TTOTAL_CMD_FormShow | 窗口显示处理器 (6264 字节) |
| 0x7E95E0 | FormClose | TTOTAL_CMD_FormClose | 关闭处理器, 保存配置 |
| 0x7FAB20 | FormCloseQuery | TTOTAL_CMD_FormCloseQuery | 关闭确认 (3586 字节) |
| 0x7B9CB0 | FormDeactivate | TTOTAL_CMD_FormDeactivate | 失活处理器 |
| 0x7CC880 | FormPaint | TTOTAL_CMD_FormPaint | 绘制处理器 |
| 0x7B8940 | FormResize | TTOTAL_CMD_FormResize | 大小改变处理器 (4928 字节) |
| 0x7B9C80 | FormWindowStateChange | TTOTAL_CMD_FormWindowStateChange | 状态改变 (最小化/最大化/还原) |
键盘事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7D2760 | FormKeyDown | TTOTAL_CMD_FormKeyDown | 主键盘分发器 (23672 字节) |
| 0x7DDD40 | FormKeyUp | TTOTAL_CMD_FormKeyUp | 按键释放处理器 |
| 0x7D83E0 | FormUTF8KeyPress | TTOTAL_CMD_FormUTF8KeyPress | UTF8 按键 (快速搜索) |
文件操作按钮
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7DC8F0 | idbCopyClick | TTOTAL_CMD_idbCopyClick | 复制 (F5)。检查 Shift/Alt/Ctrl |
| 0x7DCB80 | idbRenmovClick | TTOTAL_CMD_idbRenmovClick | 重命名/移动 (F6)。行内重命名或移动对话框 |
| 0x7DD0F0 | idbDeleteClick | TTOTAL_CMD_idbDeleteClick | 删除 (F8/Del) |
| 0x7DD190 | idbMkdirClick | TTOTAL_CMD_idbMkdirClick | 新建目录 (F7)。Shift=先切换面板 |
| 0x7DD2D0 | idbExitClick | TTOTAL_CMD_idbExitClick | 退出 (Alt+F4) |
| 0x7DE260 | idbEditClick | TTOTAL_CMD_idbEditClick | 编辑 (F4) |
| 0x7FAA50 | idbListClick | TTOTAL_CMD_idbListClick | 查看 (F3) |
| 0x7FAB20 | StartHandlerClick | TTOTAL_CMD_StartHandlerClick | 启动外部处理器 |
导航
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7DAA00 | RootClick | TTOTAL_CMD_RootClick | 跳转到根目录 \ |
| 0x7DBB60 | ParentClick | TTOTAL_CMD_ParentClick | 跳转到上级目录 .. |
| 0x7FE6A0 | LeftPathClick | TTOTAL_CMD_LeftPathClick | 左侧路径点击 |
| 0x7FFE00 | RightPathClick | TTOTAL_CMD_RightPathClick | 右侧路径点击 |
| 0x805BF0 | PathDblClick | TTOTAL_CMD_PathDblClick | 路径双击 |
| 0x8049E0 | PathMouseUp | TTOTAL_CMD_PathMouseUp | 路径鼠标释放 |
| 0x804D20 | PathMouseDown | TTOTAL_CMD_PathMouseDown | 路径鼠标按下 |
| 0x7FF290 | W_PathPartialPathClick | TTOTAL_CMD_W_PathPartialPathClick | 部分路径点击 |
| 0x7FFB80 | W_PathPartialPathDragDrop | TTOTAL_CMD_W_PathPartialPathDragDrop | 部分路径拖放 |
| 0x7FE910 | W_PathShowDropPanel | TTOTAL_CMD_W_PathShowDropPanel | 显示拖放面板 |
| 0x7FF210 | LeftPathMouseInDropPanel | TTOTAL_CMD_LeftPathMouseInDropPanel | 鼠标进入拖放面板 |
| 0x80E4C0 | W_PathExit | TTOTAL_CMD_W_PathExit | 路径退出 |
| 0x80E510 | BreadCrumbBtnClick | TTOTAL_CMD_BreadCrumbBtnClick | 面包屑按钮 |
文件列表事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7DDDA0 | ListChange | TTOTAL_CMD_ListChange | 列表内容改变 |
| 0x7E90F0 | ListSelectionChange | TTOTAL_CMD_ListSelectionChange | 选择改变 |
| 0x7E9010 | ListClick | TTOTAL_CMD_ListClick | 列表点击 |
| 0x7E9260 | ListExit | TTOTAL_CMD_ListExit | 列表退出 |
| 0x7E99B0 | ListLButtonUp | TTOTAL_CMD_ListLButtonUp | 左键释放 |
| 0x7E9F70 | ListLButtonDown | TTOTAL_CMD_ListLButtonDown | 左键按下 |
| 0x7EB600 | ListMouseMove | TTOTAL_CMD_ListMouseMove | 鼠标移动 |
| 0x7EBC70 | ListRButtonDown | TTOTAL_CMD_ListRButtonDown | 右键按下 |
| 0x7EC090 | ListRButtonUp | TTOTAL_CMD_ListRButtonUp | 右键释放 |
| 0x804770 | ListKeyUp | TTOTAL_CMD_ListKeyUp | 列表按键释放 |
| 0x809E80 | ListVScroll | TTOTAL_CMD_ListVScroll | 垂直滚动 |
| 0x809D70 | ListHScroll | TTOTAL_CMD_ListHScroll | 水平滚动 |
| 0x7CDA80 | ListDrawItem | TTOTAL_CMD_ListDrawItem | 绘制列表项 |
| 0x7D06A0 | ListMeasureItem | TTOTAL_CMD_ListMeasureItem | 测量列表项 |
| 0x80CA00 | ListGetText | TTOTAL_CMD_ListGetText | 获取项文本 (ANSI) |
| 0x80CA60 | ListGetTextW | TTOTAL_CMD_ListGetTextW | 获取项文本 (Unicode) |
| 0x7D9110 | LeftListEnter | TTOTAL_CMD_LeftListEnter | 左侧列表进入 |
| 0x7D91C0 | RightListEnter | TTOTAL_CMD_RightListEnter | 右侧列表进入 |
| 0x7D90C0 | TreeListEnter | TTOTAL_CMD_TreeListEnter | 树形列表进入 |
面板事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7D2380 | DoubleClick | TTOTAL_CMD_DoubleClick | 面板双击 |
| 0x80C7E0 | PanelDblClick | TTOTAL_CMD_PanelDblClick | 面板双击 |
| 0x80D9B0 | PanelMouseUp | TTOTAL_CMD_PanelMouseUp | 面板鼠标释放 |
| 0x807D40 | PanelPaint | TTOTAL_CMD_PanelPaint | 面板绘制 |
| 0x807AB0 | BottomPanelPaint | TTOTAL_CMD_BottomPanelPaint | 底部面板绘制 |
| 0x7BA030 | BottomPanelResize | TTOTAL_CMD_BottomPanelResize | 底部面板大小改变 |
| 0x7BAF00 | LeftPanelResize | TTOTAL_CMD_LeftPanelResize | 左面板大小改变 |
| 0x7BA1D0 | RightPanelResize | TTOTAL_CMD_RightPanelResize | 右面板大小改变 |
| 0x7D8DA0 | LeftPanelClick | TTOTAL_CMD_LeftPanelClick | 左面板点击 |
驱动器栏事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7D9AE0 | DrivesChange | TTOTAL_CMD_DrivesChange | 驱动器选择改变 |
| 0x7DD3A0 | DrivesKeyDown | TTOTAL_CMD_DrivesKeyDown | 驱动器栏按键按下 (1517 字节) |
| 0x7DD9F0 | DrivesDropdown | TTOTAL_CMD_DrivesDropdown | 驱动器下拉 |
| 0x800190 | DrivesCloseup | TTOTAL_CMD_DrivesCloseup | 驱动器关闭 |
| 0x80DB90 | DrivesDrawItem | TTOTAL_CMD_DrivesDrawItem | 驱动器绘制项 (2343 字节) |
| 0x8003B0 | DrivesMouseUp | TTOTAL_CMD_DrivesMouseUp | 驱动器鼠标释放 |
| 0x7DD990 | DrivesEnterKeyDown | TTOTAL_CMD_DrivesEnterKeyDown | 驱动器回车键 |
| 0x8002B0 | LeftDrivesExit | TTOTAL_CMD_LeftDrivesExit | 左侧驱动器退出 |
命令行事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x7B9E60 | CmdLineEnter | TTOTAL_CMD_CmdLineEnter | 命令行回车 |
| 0x7B9FC0 | CmdLineKeyUp | TTOTAL_CMD_CmdLineKeyUp | 命令行按键释放 |
| 0x7FB9B0 | CmdLineKeyDown | TTOTAL_CMD_CmdLineKeyDown | 命令行按键按下 (1023 字节) |
| 0x7FBDB0 | CmdLineKeyPress | TTOTAL_CMD_CmdLineKeyPress | 命令行按键 |
| 0x7DDC20 | CmdLineDblClick | TTOTAL_CMD_CmdLineDblClick | 命令行双击 |
| 0x802170 | CmdLineChange | TTOTAL_CMD_CmdLineChange | 命令行内容改变 |
| 0x8022C0 | CmdLineDropDown | TTOTAL_CMD_CmdLineDropDown | 命令行下拉 |
表头 / 列事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x80A010 | HeaderDblClick | TTOTAL_CMD_HeaderDblClick | 表头双击 (3941 字节) |
| 0x80AF80 | HeaderMouseUp | TTOTAL_CMD_HeaderMouseUp | 表头鼠标释放 |
| 0x7DA030 | HeaderSectionClick | TTOTAL_CMD_HeaderSectionClick | 表头分节点击 |
| 0x7D1B60 | HeaderSectionResize | TTOTAL_CMD_HeaderSectionResize | 表头分节大小改变 (2077 字节) |
| 0x8054C0 | HeaderSectionTrack | TTOTAL_CMD_HeaderSectionTrack | 表头分节拖动 |
| 0x7C6730 | HeaderSized | TTOTAL_CMD_HeaderSized | 表头大小已改变 |
| 0x804D10 | HeaderSizing | TTOTAL_CMD_HeaderSizing | 表头大小改变中 |
分隔条事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x800C90 | SplitPanelMouseDown | TTOTAL_CMD_SplitPanelMouseDown | 分隔条鼠标按下 |
| 0x800DD0 | SplitPanelMouseUp | TTOTAL_CMD_SplitPanelMouseUp | 分隔条鼠标释放 |
| 0x8015D0 | SplitPanelMouseMove | TTOTAL_CMD_SplitPanelMouseMove | 分隔条鼠标移动 |
| 0x8015C0 | SplitPanelDblClick | TTOTAL_CMD_SplitPanelDblClick | 分隔条双击 |
FTP / 连接事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x805C40 | FtpTransferModeBoxChange | TTOTAL_CMD_FtpTransferModeBoxChange | FTP 模式改变 |
| 0x806030 | FtpTransferModeBoxKeyDown | TTOTAL_CMD_FtpTransferModeBoxKeyDown | FTP 模式按键按下 |
| 0x806560 | DisconnectButtonClick | TTOTAL_CMD_DisconnectButtonClick | 断开连接按钮 (1697 字节) |
| 0x806090 | FtpInfoButtonClick | TTOTAL_CMD_FtpInfoButtonClick | FTP 信息按钮 |
就地编辑事件
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x808160 | W_InPlaceEditKeyDown | TTOTAL_CMD_W_InPlaceEditKeyDown | 就地编辑按键按下 |
| 0x80E500 | W_InPlaceEditKeyPress | TTOTAL_CMD_W_InPlaceEditKeyPress | 就地编辑按键 |
| 0x7E8860 | W_InPlaceEditGoingOff | TTOTAL_CMD_W_InPlaceEditGoingOff | 就地编辑结束 |
重命名 / 菜单
| 地址 | 方法名 | IDA 符号 | 说明 |
|---|---|---|---|
| 0x80E550 | RenameOKBtnClick | TTOTAL_CMD_RenameOKBtnClick | 重命名确定按钮 |
| 0x80E590 | RenameOKBtnMouseDown | TTOTAL_CMD_RenameOKBtnMouseDown | 重命名确定鼠标按下 |
| 0x80E5C0 | RenameOKBtnMouseLeave | TTOTAL_CMD_RenameOKBtnMouseLeave | 重命名确定鼠标离开 |
| 0x7FA9F0 | MenuDispatch | TTOTAL_CMD_MenuDispatch | 集中式菜单分发器 |
| 0x8079B0 | idbDeleteMouseDown | TTOTAL_CMD_idbDeleteMouseDown | 删除按钮鼠标按下 |
| 0x8056C0 | SizePaint | TTOTAL_CMD_SizePaint | 尺寸绘制 |
4.4 LFM 事件处理器绑定
窗体级事件 (来自 LFM)
| 事件 | 处理器 |
|---|---|
| OnClose | FormClose |
| OnCloseQuery | FormCloseQuery |
| OnCreate | FormCreate |
| OnDeactivate | FormDeactivate |
| OnKeyDown | FormKeyDown |
| OnKeyUp | FormKeyUp |
| OnPaint | FormPaint |
| OnResize | FormResize |
| OnShow | FormShow |
| OnWindowStateChange | FormWindowStateChange |
菜单项 — 全部使用 MenuDispatch
MainMenu 中所有 TMenuItem 的 OnClick 事件都路由到单一的 MenuDispatch 处理器。这是一种集中式命令分发模式 — 每个 TMenuItem 的 Tag 属性标识命令。MenuDispatch 函数 (位于 0x7FA9F0) 读取 Tag 并分发到对应的命令处理器。
关键菜单路径:
Files.FilesChangeattributes→ 修改属性Files.FilesPack→ 打包文件Files.FilesUnpackspecificfiles→ 解包文件Files.Testarchives1→ 测试压缩包Files.Comparebycontent1→ 按内容比较Files.FilesAssociatewith→ 关联Files.FilesVersioninfo→ 版本信息Files.MenuItemID503.*→ 标记/选择命令 (11 个子项)Files.Commands.*→ 命令 (CD 树、搜索、卷标等)Files.Configuration.*→ 配置 (7 个子项)Files.Help.*→ 帮助 (6 个子项)
5. 关键事件处理器 IDA 反编译分析
5.1 TTOTAL_CMD_FormCreate (0x7BE270, 5656 字节)
用途: 主窗口初始化。
分析: 这是最大的初始化函数。它:
- 初始化 FPC try/except 异常帧 (
sub_410100/sub_412E70模式) - 设置左右面板、驱动器栏、文件列表控件
- 初始化命令行、按钮栏和状态栏
- 加载配置 (窗口位置、面板大小、列宽)
- 创建子控件并设置事件处理器
- 读取 DPI 设置并相应缩放界面
关键模式: sub_410100(1, ...) 创建异常帧; sub_412E70() 检查是否发生异常; sub_410500() 离开异常帧。
5.2 TTOTAL_CMD_FormClose (0x7E95E0, 968 字节)
用途: 处理主窗口关闭。
分析:
// Pseudocode summary:
TTOTAL_CMD_FormClose(form, action, close_action) {
// Hide window first
if (!exception_check1()) {
hwnd = get_window_handle(form);
ShowWindow(hwnd, SW_HIDE); // 0 = SW_HIDE
}
// Save configuration
if (exception_check1()) {
// Error path - save what we can
save_config();
}
// Cleanup resources
if (!exception_check2()) {
if (flag_D7C470) sub_62B000(); // Save layout
sub_B44EF0(); // Save window position
// Free child objects
if (form[0xC1] != form[0xEB]) {
sub_4408F0(form); // Cleanup panels
}
if (form[0x13E]) sub_7E9550(form); // Cleanup file lists
if (form[0x2D8]) { free(form[0x2D8]); form[0x2D8] = 0; }
if (form[0x2D9]) { free(form[0x2D9]); form[0x2D9] = 0; }
// Free global objects
if (qword_BD5DA0) { free(qword_BD5DA0); qword_BD5DA0 = 0; }
if (qword_BD5D50) { free(qword_BD5D50); qword_BD5D50 = 0; }
}
*close_action = 2; // caFree - free the form
}
关键发现: *close_action = 2 设置 TCloseAction = caFree, 表示窗体在关闭后被释放。
5.3 TTOTAL_CMD_idbCopyClick (0x7DC8F0, 641 字节)
用途: 复制按钮处理器 (F5 键)。
分析:
// Pseudocode summary:
TTOTAL_CMD_idbCopyClick(form, button) {
if (form[0x14F7] || byte_C0D490) return; // Re-entrancy guard
form[0x14F7] = 1; // Set busy flag
sub_AF95E0(); // Reset UI state
if (!exception_check()) {
Focus = GetFocus(); // Remember focused panel
panel_id = (Focus == qword_D7B8B8) ? 1 :
(Focus == qword_D7B8B0) ? 2 : 0;
if (!byte_D7C300 && !byte_D7C310)
sub_447A20(qword_BBAC30); // Close popup menus
shift = (GetKeyState(VK_SHIFT) < 0); // 0x10 = VK_SHIFT
if (shift && sub_490BF0()) {
// Shift+Copy: special mode
sub_6C4860(get_hwnd(form));
} else if (GetKeyState(VK_MENU) >= 0 || sub_490BF0()) {
// Normal copy or Ctrl+Copy
if (byte_D7C470 && GetKeyState(VK_CONTROL) < 0)
sub_7DBBD0(form, 4, 0, 0); // Ctrl+Copy: mode 4
else
sub_7DBBD0(form, 1, shift, 0); // Normal: mode 1
} else {
// Alt+Copy variant
sub_58B8E0(ctrl_pressed, shift, 0);
}
// Restore focus
if (panel_id == 1) SetFocus(qword_D7B8B8);
else if (panel_id == 2) SetFocus(qword_D7B8B0);
else sub_47EC30();
}
form[0x14F7] = 0; // Clear busy flag
}
关键发现: sub_7DBBD0(form, mode, shift, 0) 是核心复制/移动操作分发器:
- mode=1: 普通复制
- mode=3: 移动 (由 idbRenmovClick 使用)
- mode=4: Ctrl+复制 (特殊模式)
5.4 TTOTAL_CMD_idbDeleteClick (0x7DD0F0, 152 字节)
用途: 删除按钮处理器 (F8/Delete 键)。
分析: 简单包装 — 调用 sub_7DCDE0(form, 0) 执行实际删除操作。重入保护模式 (form[0x14F7]) 与 idbCopyClick 相同。
5.5 TTOTAL_CMD_idbMkdirClick (0x7DD190, 315 字节)
用途: 新建目录按钮处理器 (F7 键)。
分析:
// If Shift pressed or called from inactive panel:
if (shift_pressed || from_inactive) {
if (sub_7BA010(form)) return; // Check if action cancelled
SetFocus(other_panel); // Switch to other panel
sub_7D8F10(form, other_panel); // Update panel state
byte_BD58C0 = !byte_BD58C0; // Toggle active panel flag
}
// Create directory dialog
sub_5C7890(get_hwnd(form), active_panel, 0);
关键发现: byte_BD58C0 是活动面板标志 (0=左, 1=右)。Shift+F7 在 另一个 面板中创建目录。
5.6 TTOTAL_CMD_idbRenmovClick (0x7DCB80, 603 字节)
用途: 重命名/移动按钮处理器 (F6 键)。
分析:
// Determine if single-file inline rename is possible
file_count = sub_4802B0(active_panel);
can_inline_rename = (shift || sub_490BF0())
&& file_count <= 1
&& (selection_allows_rename);
if (can_inline_rename && file_count == 1) {
// Verify single file rename is valid
if (sub_481650(active_panel, 1, &index) == 1
&& sub_480290(active_panel) == index) {
can_inline_rename = 1;
}
}
if (can_inline_rename) {
// Inline rename (edit box in file list)
sub_7E8E20(form, panel, from_button, 0);
} else {
// Move dialog
shift = (GetKeyState(VK_SHIFT) < 0);
if (sub_490BF0()) {
sub_6C4540(get_hwnd(form)); // Special mode
} else {
sub_7DBBD0(form, 3, shift, 0); // Move: mode 3
}
}
关键发现: 在单个选中文件上按 F6 触发行内重命名 (sub_7E8E20); 在多个文件上按 F6 打开移动对话框 (sub_7DBBD0(form, 3, ...))。
5.7 TTOTAL_CMD_DoubleClick (0x7D2380, 462 字节)
用途: 面板/文件列表双击处理器。
分析:
// Kill any pending timer
if (form[0x16F7]) {
KillTimer(get_hwnd(form), 2);
form[0x16F7] = 0;
}
sub_7E9550(form); // Cancel any in-place editing
// Determine which panel was double-clicked
panel = (form[0x9E8] == clicked_object); // 0=right, 1=left
// Check for rapid double-click (suppress triple-click)
if (form[0x1580]) { // Last click time
elapsed = abs(GetTickCount() - form[0x1580]);
threshold = GetDoubleClickTime() + 50; // 50ms extra tolerance
if (elapsed < threshold) return;
}
// If drag-drop mode or special mode, check cursor position
if (byte_D7C2D0 || byte_D7CAF0[panel]) {
GetCursorPos(&point);
ScreenToClient(panel_window[panel], &point);
item_index = sub_482060(panel, point);
if (item_index < 0) return;
if (item_index >= file_count[panel]) return;
if (sub_480610(panel) != item_index) return;
}
// Normal double-click: open file
if (GetKeyState(VK_MENU) >= 0 || GetKeyState(VK_CONTROL) < 0) {
// Alt NOT pressed, or Ctrl pressed: normal open
cmd_id = sub_41C860(panel) + 0x12D687; // Calculate command ID
PostMessageA(get_hwnd(form), WM_USER + 0x57, cmd_id, 0);
} else {
// Alt+double-click: alternate action (Alt+Enter style)
sub_59A4D0(get_hwnd(form));
}
关键发现: 双击向窗体发送 WM_USER + 0x57 (0x457 = 1111) 消息。wParam 编码命令。Alt+双击调用 sub_59A4D0 (可能显示文件属性或用关联程序打开)。
5.8 TTOTAL_CMD_FormShow (0x7C4EB0, 6264 字节)
用途: 主窗口变为可见时调用。
分析: 非常大的函数, 处理:
- 从保存的配置恢复窗口位置
- 使用
GetDialogBaseUnits()进行 DPI 感知缩放 - 面板宽度/高度调整
- 驱动器栏初始化
- 文件列表列宽恢复
- 按钮栏状态加载
- 状态栏文本设置
- 初始目录读取
5.9 TLister_FormKeyDown (0x65A320, 5466 字节)
用途: Lister (文件查看器) 键盘快捷键处理器。
分析: 处理 Lister 中的所有键盘快捷键:
- 方向键: 滚动
- PgUp/PgDn: 翻页滚动
- Home/End: 跳转到开头/结尾
- F2: 切换自动换行
- F3: 查找下一个
- F4: 编辑文件
- F7: 查找
- F8: 切换十六进制/ASCII 模式
- Ctrl+键: 各种命令 (复制、全选等)
- Alt+键: 代码页切换、缩放
该函数是按键代码上的大型 switch/case, 包含众多分支以处理不同查看器模式 (文本、二进制、图像、多媒体)。
5.10 TFindFile_FormCreate (0x68AA20, 3827 字节)
用途: 查找文件对话框初始化。
分析: 设置:
- 带历史记录的搜索文本组合框
- 驱动器选择复选框
- 日期范围过滤控件 (
id_DateFromBtn,id_DateToBtn) - 搜索选项复选框 (ANSI、DOS、Hex 等)
- 插件字段配置
- 重复文件查找模式初始化
- Everything 搜索集成 (
id_Everything) - 窗口位置恢复
6. 关键对话框窗体
6.1 TDLGCONFIGALL — 配置对话框
- VMT: 0xC141F0
- 方法数: 214 (最大的窗体)
- LFM: 0xC16A68, 6 个顶层事件, 16 个子组件
这是主配置对话框, 拥有 214 个已发布方法, 涵盖 Total Commander 全部设置项: 显示、字体、颜色、制表位、操作行为、ZIP/FTP 设置等。
6.2 TFindFile — 查找文件对话框
- VMT: 0xC3F9F8
- 方法数: 81
- LFM: 0xC41238, 8 个顶层事件, 4 个子组件
关键处理器:
| 地址 | 处理器 | 说明 |
|---|---|---|
| 0x68AA20 | FormCreate (3827 字节) | 初始化搜索控件 |
| 0x68B920 | FormShow (2827 字节) | 显示对话框, 加载上次搜索 |
| 0x69FDB0 | FormCloseQuery (1867 字节) | 关闭确认 |
| 0x6911C0 | CancelClick (811 字节) | 取消按钮 |
| 0x6A33A0 | id_DeleteClick (736 字节) | 从结果中删除 |
| 0x69C290 | id_gotopathClick (920 字节) | 在文件管理器中跳转到路径 |
| 0x6ADA20 | id_EverythingClick (1231 字节) | Everything SDK 集成 |
| 0x6A81A0 | id_DupeFinderClick (593 字节) | 重复查找模式 |
| 0x65A320 | FormKeyDown (1040 字节) | 键盘快捷键 |
6.3 TMultiRename — 多重重命名工具
- VMT: 0xC6D0F0
- 方法数: 69
- LFM: 0xC6E410, 8 个顶层事件, 9 个子组件
关键处理器:
| 地址 | 处理器 | 说明 |
|---|---|---|
| 0x73EE50 | FormCreate (1280 字节) | 初始化重命名控件 |
| 0x732000 | FormShow (4213 字节) | 显示对话框, 填充文件列表 |
| 0x743240 | FormKeyDown (679 字节) | 键盘快捷键 |
| 0x72F690 | AddCounterClick (28 字节) | 插入 [C] 计数器占位符 |
| 0x72F6B0 | AddCounterEClick (28 字节) | 插入 [CE] 扩展计数器 |
| 0x72F670 | AddExtClick (28 字节) | 插入 [E] 扩展名占位符 |
| 0x72F650 | AddNameClick (28 字节) | 插入 [N] 名称占位符 |
| 0x731670 | AddDateClick (28 字节) | 插入 [D] 日期占位符 |
| 0x731690 | AddTimeClick (28 字节) | 插入 [T] 时间占位符 |
| 0x744530 | Editnamesindefaulteditor1Click (2237 字节) | 在外部编辑器中编辑名称 |
| 0x73EAD0 | HeaderClick1SectionClick (655 字节) | 列标题点击 |
6.4 TLister — 文件查看器
- VMT: 0xC3A180
- 方法数: 62
- LFM: 0xC3C638, 15 个顶层事件, 3 个子组件
关键处理器:
| 地址 | 处理器 | 说明 |
|---|---|---|
| 0x63E110 | FormCreate (5850 字节) | 初始化查看器 |
| 0x63F840 | FormShow (2502 字节) | 显示查看器, 加载文件 |
| 0x65A320 | FormKeyDown (5466 字节) | 全部键盘快捷键 |
| 0x652E90 | FormPaint (8807 字节) | 渲染文件内容 |
| 0x661E30 | FormCloseQuery (2583 字节) | 关闭确认 |
| 0x679FF0 | CodePageClick (3267 字节) | 代码页切换 |
| 0x65F440 | EditFindClick (1664 字节) | 查找对话框 |
| 0x65FAC0 | EditFindnextClick (548 字节) | 查找下一个 |
| 0x65EDF0 | CopyasHexClick (1603 字节) | 复制为十六进制 |
| 0x65DEA0 | EditCopyastextClick (3907 字节) | 复制为文本 |
6.5 TCmpForm — 文件比较
- VMT: 0xC7BF18
- 方法数: 68
- LFM: 0xC7D318, 10 个顶层事件, 2 个子组件
6.6 TFileCompForm — 按内容比较文件
- VMT: 0xC5F9D0
- 方法数: 63
- LFM: 0xC60A70, 8 个顶层事件, 1 个子组件
7. 事件处理器模式
7.1 FPC 异常处理模式
大多数事件处理器使用 FPC 的 try/except 模式, 在反编译中表现为:
sub_410100(1, &buf1, &buf2); // __enter_exception_frame
result = sub_412E70(buf); // __check_exception (returns 0 = no exception)
if (!result) {
// ... normal code ...
}
sub_410500(); // __leave_exception_frame
if (result) {
// ... exception handler code ...
sub_4106F0(); // __end_exception_handler
}
7.2 重入保护模式
按钮点击处理器使用 form + 0x14F7 处的忙标志:
if (form[0x14F7] || byte_C0D490) return; // Already busy
form[0x14F7] = 1; // Set busy
// ... do work ...
form[0x14F7] = 0; // Clear busy
7.3 菜单分发模式
所有 TMenuItem OnClick 事件使用单一的 MenuDispatch 处理器 (0x7FA9F0)。每个 TMenuItem 的 Tag 属性标识命令。这是一种命令模式实现:
procedure TTOTAL_CMD.MenuDispatch(Sender: TObject);
begin
case TComponent(Sender).Tag of
cm_Copy: { copy command };
cm_Move: { move command };
// ... etc
end;
end;
7.4 通用窗体生命周期方法
大多数窗体实现以下标准方法:
| 方法 | 用途 |
|---|---|
| FormCreate | 初始化窗体, 加载设置 |
| FormShow | 显示前的最终设置 |
| FormClose | 清理, 设置 CloseAction |
| FormCloseQuery | 确认关闭 (返回 CanClose) |
| FormDestroy | 释放已分配资源 |
| FormKeyDown | 键盘快捷键处理器 |
| FormResize | 处理窗口大小改变 |
8. IDA 分析脚本
"""LFM (Lazarus Form Module) 与 VMT (虚拟方法表) 解析器 for TOTALCMD64.EXE - v5.
功能:
1. 扫描并解析所有 TPF0/TPF1 LFM 资源 (窗体、组件、事件处理器)
2. 解析指定地址或当前光标位置的 FPC 64-bit VMT 结构
3. 解析指定地址或当前光标位置的 LFM 窗体资源
4. 自动检测指定地址处是 LFM 还是 VMT, 并调用相应解析器
5. 扫描二进制文件中所有 VMT 并输出完整报告
运行方式:
- 交互模式 (IDA Pro 中 Alt+F7 / File > Script File):
弹出三按钮对话框: VMT全扫描 / LFM扫描 / 自动解析
选择"自动解析"后弹出地址输入框 (默认当前光标位置),
自动检测该地址处是 LFM (TPF0/TPF1 标记) 还是 VMT (校验字段) 并解析
- 批处理模式 (idat64 -S"parse_lfm.py --parse 0x地址"):
--parse [地址] 自动检测并解析 LFM/VMT (地址缺省时用当前光标)
--lfm [地址] 解析指定地址 LFM
--vmt [地址] 解析指定地址 VMT
--vmt-scan 扫描所有 VMT
- Python 控制台直接调用:
auto_parse_at(0xC855F8) # 自动检测并解析
auto_parse_at() # 使用当前光标位置
parse_vmt_main(0xC855F8) # 解析指定地址 VMT
parse_lfm_main(0xC855F8) # 解析指定地址 LFM
scan_all_vmts() # 扫描所有 VMT
scan_lfms() # 扫描所有 LFM
v4 遗留特性 (均已保留):
- 分块读取 LFM 数据, 消除逐字节 IDA API 调用导致的卡顿
- 合理的迭代上限和操作计数器, 防止解析器失控
- 嵌入式字符串恢复 (6 字节零值校验)
- 0xF1/0xF2 Lazarus 扩展标记处理
"""
import os
import sys
import idc
import ida_bytes
import ida_segment
import idautils
import ida_kernwin
# Value type identifiers (from FPC classes.pp TValueType enum)
# vaNull=0, vaList=1, vaInt8=2, vaInt16=3, vaInt32=4, vaExtended=5,
# vaString=6, vaIdent=7, vaFalse=8, vaTrue=9, vaBinary=10, vaSet=11,
# vaLString=12, vaNil=13, vaCollection=14, vaSingle=15, vaCurrency=16,
# vaDate=17, vaWString=18, vaInt64=19, vaUTF8String=20, vaUString=21
VA_NIL = 0x0D
VA_LIST = 0x01
VA_INT8 = 0x02
VA_INT16 = 0x03
VA_INT32 = 0x04
VA_EXTENDED = 0x05
VA_STRING = 0x06
VA_IDENT = 0x07
VA_FALSE = 0x08
VA_TRUE = 0x09
VA_BINARY = 0x0A
VA_SET = 0x0B
VA_LSTRING = 0x0C
VA_COLLECTION = 0x0E
VA_SINGLE = 0x0F
VA_CURRENCY = 0x10
VA_DATE = 0x11
VA_WSTRING = 0x12
VA_INT64 = 0x13
VA_UTF8STRING = 0x14
VA_USTRING = 0x15
MAX_DEPTH = 15
MAX_PROPS = 512
MAX_CHILDREN = 512
MAX_LIST_ITEMS = 4096
MAX_LFM_SIZE = 0x100000 # 1MB safety cap per LFM resource
MAX_OPS = 500000 # total operation counter cap per LFM
CHUNK_SIZE = 0x100000 # 1MB chunks for scanning
# Global operation counter (reset per LFM). Using list for mutable closure access.
_ops_counter = [0]
# Output file: defaults to the IDB directory (same folder as the .i64 file).
_idb_dir = os.path.dirname(idc.get_idb_path()) if idc.get_idb_path() else '.'
OUTPUT_PATH = os.path.join(_idb_dir, 'lfm_forms.txt')
# =============================================================================
# VMT (虚拟方法表) 常量
# =============================================================================
# FPC 64-bit VMT 布局 (13 个 qword 字段, 共 104 字节):
# +0 vInstanceSize 实例大小 (uint64)
# +8 vInstanceSize2 -vInstanceSize (校验字段, 用于验证 VMT 有效性)
# +16 vParentRef 父类 VMT 指针
# +24 vClassName 类名 Pascal ShortString 指针
# +32 vDynamicTable 动态方法表指针
# +40 vMethodTable 已发布方法表指针
# +48 vFieldTable 已发布字段表指针
# +56 vTypeInfo RTTI 类型信息指针
# +64 vInitTable 初始化表指针
# +72 vAutoTable 自动化表指针
# +80 vIntfTable 接口表指针
# +88 vMsgStrPtr 消息字符串指针
# +96 vDestroy 析构函数代码地址
#
# 方法表格式 (位于 vMethodTable 指向的地址):
# +0 count (uint32) 方法数量
# +4 entries[count] 每项 16 字节:
# +0 name_ptr (uint64) -> 方法名 Pascal ShortString
# +8 code_ptr (uint64) -> 方法代码地址
VMT_SIZE = 13 * 8 # 104 字节
MAX_VMT_CHAIN_DEPTH = 32 # 父类链最大深度, 防止循环引用
MAX_METHODS_PER_VMT = 1000 # 单个 VMT 方法数上限
VMT_SEARCH_BACK_RANGE = 0x1000 # 向前查找 VMT 起始的最大范围 (4KB)
# VMT 字段定义: (偏移, 字段名, 说明)
VMT_FIELDS = [
(0, 'vInstanceSize', '实例大小'),
(8, 'vInstanceSize2', '校验值 (-vInstanceSize)'),
(16, 'vParentRef', '父类 VMT 指针'),
(24, 'vClassName', '类名字符串指针'),
(32, 'vDynamicTable', '动态方法表指针'),
(40, 'vMethodTable', '已发布方法表指针'),
(48, 'vFieldTable', '已发布字段表指针'),
(56, 'vTypeInfo', 'RTTI 类型信息指针'),
(64, 'vInitTable', '初始化表指针'),
(72, 'vAutoTable', '自动化表指针'),
(80, 'vIntfTable', '接口表指针'),
(88, 'vMsgStrPtr', '消息字符串指针'),
(96, 'vDestroy', '析构函数地址'),
]
VMT_REPORT_PATH = os.path.join(_idb_dir, 'vmt_parse_report.txt')
VMT_SCAN_REPORT_PATH = os.path.join(_idb_dir, 'vmt_scan_report.txt')
def _check_ops():
"""Increment operation counter and bail out if limit exceeded."""
_ops_counter[0] += 1
if _ops_counter[0] > MAX_OPS:
raise RuntimeError(f'Operation limit exceeded ({MAX_OPS})')
def try_recover_embedded_string(data, placeholder, pos, end_pos):
"""Try to recover a string from Lazarus embedded data format.
When a Pascal string contains 0x00 bytes, it may be a placeholder
followed by 16 bytes of embedded metadata and the actual string suffix.
Empirically determined format (from TOTALCMD64.EXE LFM resources):
- N bytes placeholder (may contain partial string + 0x00 padding)
- 8 bytes 0xFF marker (sentinel)
- 1 byte = N*2 (validation, mod 256)
- 1 byte unknown (possibly a hash or index)
- 6 bytes 0x00
- M bytes actual string suffix (M = N - k, where k = count of non-0x00
bytes before the first 0x00 in placeholder)
The full string = placeholder[:k] + suffix.
Two layouts are handled:
- Format A (marker after string): placeholder is the Pascal string content;
0xFF*8 + metadata + suffix follow after the string.
- Format B (marker inside string): Pascal string = placeholder + 0xFF*8;
metadata + suffix follow after the string.
Returns (recovered_string, new_pos) or (None, original_pos) if no
embedded data found.
"""
placeholder_bytes = placeholder.encode('latin1', errors='replace')
# Format B: check if 0xFF*8 marker is INSIDE the placeholder
marker_idx = placeholder_bytes.find(b'\xff' * 8)
if marker_idx >= 0:
# Marker found inside string.
# prefix = placeholder_bytes[:marker_idx] (may contain text + null padding)
prefix = placeholder_bytes[:marker_idx]
n = marker_idx # N = length of prefix
k = 0
for c in prefix:
if c == 0:
break
k += 1
# Metadata + suffix should be after the Pascal string (at pos)
# Layout after string: N*2 (1 byte) + unknown (1 byte) + 0x00*6 (6 bytes) + suffix (M bytes)
if pos + 8 <= end_pos:
expected_n2 = (n * 2) & 0xFF
actual_n2 = data[pos]
if actual_n2 == expected_n2 and data[pos + 2:pos + 8] == b'\x00' * 6:
m = n - k
suffix_pos = pos + 8
if m > 0 and suffix_pos + m <= end_pos:
suffix_bytes = data[suffix_pos:suffix_pos + m]
new_pos = suffix_pos + m
try:
suffix = suffix_bytes.decode('latin1')
except Exception:
suffix = f'<{suffix_bytes.hex()}>'
return prefix[:k].decode('latin1') + suffix, new_pos
else:
return prefix[:k].decode('latin1'), pos + 8
# Metadata not found after string; return sanitized prefix
if k > 0:
return prefix[:k].decode('latin1'), pos
return None, pos
# Format A: marker after string (original behavior)
if pos + 16 > end_pos:
return None, pos
# Check for 0xFF*8 marker
marker = data[pos:pos + 8]
if len(marker) != 8 or marker != b'\xff' * 8:
return None, pos
# Validate N*2 byte
n = len(placeholder)
expected_n2 = (n * 2) & 0xFF
actual_n2 = data[pos + 8]
if actual_n2 != expected_n2:
return None, pos
# Validate 6 zero bytes at pos+10..pos+15 (stricter than v3)
if data[pos + 10:pos + 16] != b'\x00' * 6:
return None, pos
# Skip 16 bytes of embedded data (8 marker + 1 N*2 + 1 unknown + 6 zeros)
pos += 16
# Calculate k (number of non-0x00 bytes before first 0x00)
k = 0
for c in placeholder:
if c == '\x00':
break
k += 1
# Read M = N - k bytes as actual string suffix
m = n - k
if m == 0:
return placeholder[:k], pos
if pos + m > end_pos:
return None, pos
suffix_bytes = data[pos:pos + m]
pos += m
if suffix_bytes:
try:
suffix = suffix_bytes.decode('latin1')
except Exception:
suffix = f'<{suffix_bytes.hex()}>'
else:
suffix = ''
full_string = placeholder[:k] + suffix
return full_string, pos
def read_pascal_string(data, pos, end_pos):
"""Read a Pascal-style string (1 byte length + chars) from bytes buffer.
Also handles Lazarus embedded data format: if the string contains 0x00
bytes and is followed by 0xFF*8 marker, skip the embedded metadata and
read the actual string suffix.
"""
_check_ops()
if pos + 1 > end_pos:
return '', pos
slen = data[pos]
if pos + 1 + slen > end_pos:
# Truncate to available
slen = max(0, end_pos - pos - 1)
s = data[pos + 1:pos + 1 + slen]
new_pos = pos + 1 + slen
if s:
try:
result = s.decode('latin1')
except Exception:
result = f'<{s.hex()}>'
else:
result = ''
# Check for Lazarus embedded data format: if the string contains 0x00
# and/or 0xFF*8 marker, recover the actual string.
if slen > 0 and ('\x00' in result or '\xff' * 8 in result):
recovered, recovered_pos = try_recover_embedded_string(data, result, new_pos, end_pos)
if recovered is not None:
return recovered, recovered_pos
return result, new_pos
def _is_valid_lfm_identifier(s, allow_dot=False):
"""Check if a string is a valid LFM identifier (type/name/prop).
Valid identifiers contain only ASCII letters, digits, and underscores.
If allow_dot is True, dots are also allowed (for property names like
Font.Color). Any control characters or non-ASCII characters cause
immediate rejection — this prevents binary data from passing validation
due to Python's Unicode-aware isalpha()/isalnum() treating characters
like ÿ (U+00FF) as alphabetic.
"""
if not s or not isinstance(s, str):
return False
for i, c in enumerate(s):
o = ord(c)
if o < 32 or o >= 127:
return False
if not (c.isalnum() or c == '_' or (allow_dot and c == '.')):
return False
if s[0] != '_' and not s[0].isalpha():
return False
return True
def read_value(data, pos, end_pos, depth=0):
"""Read a value from the LFM bytes buffer. Bounds-checked, iteration-limited."""
_check_ops()
if depth > MAX_DEPTH:
return None, pos
if pos >= end_pos:
return None, pos
type_byte = data[pos]
pos += 1
if type_byte == VA_NIL:
return None, pos
elif type_byte == VA_LIST:
items = []
for _ in range(MAX_LIST_ITEMS):
if pos >= end_pos:
break
item_type = data[pos]
if item_type == 0:
pos += 1
break
v, pos = read_value(data, pos, end_pos, depth + 1)
items.append(v)
return items, pos
elif type_byte == VA_INT8:
if pos >= end_pos:
return 0, pos
v = data[pos]
if v >= 128:
v -= 256
return v, pos + 1
elif type_byte == VA_INT16:
if pos + 2 > end_pos:
return 0, pos
v = int.from_bytes(data[pos:pos + 2], 'little')
if v >= 0x8000:
v -= 0x10000
return v, pos + 2
elif type_byte == VA_INT32:
if pos + 4 > end_pos:
return 0, pos
v = int.from_bytes(data[pos:pos + 4], 'little')
if v >= 0x80000000:
v -= 0x100000000
return v, pos + 4
elif type_byte == VA_INT64:
if pos + 8 > end_pos:
return 0, pos
v = int.from_bytes(data[pos:pos + 8], 'little')
return v, pos + 8
elif type_byte in (VA_STRING, VA_LSTRING, VA_IDENT):
return read_pascal_string(data, pos, end_pos)
elif type_byte == VA_UTF8STRING:
if pos + 1 > end_pos:
return '', pos
slen = data[pos]
if pos + 1 + slen > end_pos:
slen = max(0, end_pos - pos - 1)
s = data[pos + 1:pos + 1 + slen]
pos += 1 + slen
if s:
try:
return s.decode('utf-8'), pos
except Exception:
return s.decode('latin1', errors='replace'), pos
return '', pos
elif type_byte == VA_WSTRING:
if pos + 4 > end_pos:
return '', pos
slen = int.from_bytes(data[pos:pos + 4], 'little')
if pos + 4 + slen > end_pos:
slen = max(0, end_pos - pos - 4)
s = data[pos + 4:pos + 4 + slen]
pos += 4 + slen
if s:
try:
return s.decode('utf-16-le'), pos
except Exception:
return f'<{s.hex()}>', pos
return '', pos
elif type_byte == VA_USTRING:
if pos + 4 > end_pos:
return '', pos
slen = int.from_bytes(data[pos:pos + 4], 'little')
if pos + 4 + slen > end_pos:
slen = max(0, end_pos - pos - 4)
s = data[pos + 4:pos + 4 + slen]
pos += 4 + slen
if s:
try:
return s.decode('utf-16-le'), pos
except Exception:
return f'<{s.hex()}>', pos
return '', pos
elif type_byte == VA_TRUE:
return True, pos
elif type_byte == VA_FALSE:
return False, pos
elif type_byte == VA_SET:
items = []
for _ in range(MAX_LIST_ITEMS):
if pos >= end_pos:
break
slen = data[pos]
if slen == 0:
pos += 1
break
if pos + 1 + slen > end_pos:
break
s = data[pos + 1:pos + 1 + slen]
pos += 1 + slen
if s:
items.append(s.decode('latin1', errors='replace'))
return '+'.join(items) if items else '', pos
elif type_byte == VA_BINARY:
if pos + 4 > end_pos:
return '<binary:0b>', pos
slen = int.from_bytes(data[pos:pos + 4], 'little')
if pos + 4 + slen > end_pos:
slen = max(0, end_pos - pos - 4)
pos += 4 + slen
return f'<binary:{slen}b>', pos
elif type_byte == VA_COLLECTION:
items = []
for _ in range(MAX_LIST_ITEMS):
if pos >= end_pos:
break
next_byte = data[pos]
if next_byte == 0:
pos += 1
break
item = {}
for _ in range(MAX_PROPS):
if pos >= end_pos:
break
prop_end = data[pos]
if prop_end == 0:
pos += 1
break
prop_name, pos = read_pascal_string(data, pos, end_pos)
if not prop_name:
break
val, pos = read_value(data, pos, end_pos, depth + 1)
item[prop_name] = val
items.append(item)
return items, pos
elif type_byte == VA_EXTENDED:
# FPC TExtended80 = 10 bytes (x87 80-bit)
if pos + 10 > end_pos:
return '<extended?>', pos
return '<extended>', pos + 10
elif type_byte == VA_SINGLE:
if pos + 4 > end_pos:
return '<single?>', pos
return '<single>', pos + 4
elif type_byte == VA_CURRENCY:
# 64-bit int (scaled by 10000)
if pos + 8 > end_pos:
return '<currency?>', pos
return '<currency>', pos + 8
elif type_byte == VA_DATE:
# 64-bit double (TDateTime)
if pos + 8 > end_pos:
return '<date?>', pos
return '<date>', pos + 8
else:
return f'<unknown:0x{type_byte:02X}>', pos
def read_object(data, pos, end_pos, depth=0):
"""Read an object definition (recursive for nested objects). Bounds-checked.
TPF0 binary format (based on FPC TReader/TWriter):
1. [0xF2 marker + line_number] (Lazarus extension, optional - see notes)
2. type_name (Pascal string) - empty string signals end of children list
3. obj_name (Pascal string)
4. Property list: each entry is (prop_name_pascal_string, value)
- Empty prop_name (0x00 byte) signals end of properties
5. Child objects list: each child is a recursive read_object call
- Empty type_name (0x00 byte) signals end of children
Lazarus LFM extensions (empirically determined from TOTALCMD64.EXE):
- 0xF1: optional root-object marker (1 byte, no line number).
Handled in parse_lfm() by skipping it if present.
- 0xF2: optional child-object marker with line-number metadata.
Format: 0xF2 + TValueType-encoded line number + standard object data.
Example: 0xF2 0x02 0x00 = marker + vaInt8(0) -> line 0
0xF2 0x02 0x01 = marker + vaInt8(1) -> line 1
Not all children have 0xF2 (observed: form-level children often do,
deeply nested children typically do not).
Note: Lazarus uses dotted property names (e.g. "Font.Color") for nested
property values rather than vaList sub-property blocks, so vaList
handling in read_value() is not exercised by these resources.
"""
_check_ops()
if depth > MAX_DEPTH:
return None, pos
# Skip optional Lazarus object markers (empirically determined):
# 0xF1: root-object marker (1 byte, no line-number payload).
# 0xF2: child-object marker + TValueType-encoded line number.
if pos < end_pos:
marker = data[pos]
if marker == 0xF1:
pos += 1 # consume 0xF1 marker (no line number follows)
elif marker == 0xF2:
pos += 1 # consume 0xF2 marker
# Skip the TValueType-encoded line number that follows.
_, pos = read_value(data, pos, end_pos, depth + 1)
type_name, pos = read_pascal_string(data, pos, end_pos)
if not type_name:
return None, pos
# Validate type name: if it looks like binary data, parser is desynced
if not _is_valid_lfm_identifier(type_name):
return None, pos
obj_name, pos = read_pascal_string(data, pos, end_pos)
# Sanitize object name: if it contains binary data, replace with placeholder
if obj_name and not _is_valid_lfm_identifier(obj_name):
obj_name = '<corrupted>'
obj = {'type': type_name, 'name': obj_name, 'props': {}, 'children': []}
# Read properties until empty property name (0x00 byte) or limit
for _ in range(MAX_PROPS):
if pos >= end_pos:
break
prop_name, pos = read_pascal_string(data, pos, end_pos)
if not prop_name:
break
# Validate prop name: if it looks like binary data, parser is desynced
if not _is_valid_lfm_identifier(prop_name, allow_dot=True):
break
val, pos = read_value(data, pos, end_pos, depth + 1)
obj['props'][prop_name] = val
# Read child objects until empty type name (0x00 byte) or limit
for _ in range(MAX_CHILDREN):
if pos >= end_pos:
break
child, pos = read_object(data, pos, end_pos, depth + 1)
if child is None:
break
obj['children'].append(child)
return obj, pos
def parse_lfm(ea):
"""Parse a TPF0 LFM resource at address ea. Returns dict with forms/error.
v4: Reads entire LFM into a bytes buffer (one IDA API call) then parses
from the buffer. This eliminates the slow byte-by-byte ida_bytes.get_byte()
calls that caused IDA to appear frozen on large/multiple LFM resources.
"""
result = {'address': ea, 'forms': [], 'error': None}
_ops_counter[0] = 0 # reset operation counter for this LFM
# Determine the extent of the LFM data
seg = ida_segment.getseg(ea)
seg_end = seg.end_ea if seg else ea + MAX_LFM_SIZE
max_end = min(ea + MAX_LFM_SIZE, seg_end)
# Read the entire LFM data into a bytes buffer (ONE API CALL)
data = ida_bytes.get_bytes(ea, max_end - ea)
if not data:
result['error'] = 'Failed to read data'
return result
if data[:4] not in (b'TPF0', b'TPF1'):
result['error'] = f'Not TPF0/TPF1: {data[:4]!r}'
return result
try:
pos = 4
end_pos = len(data)
obj, pos = read_object(data, pos, end_pos)
if obj:
result['forms'].append(obj)
except RuntimeError as e:
result['error'] = f'{e} (pos was ~{pos})'
except Exception as e:
result['error'] = f'{type(e).__name__}: {e} (pos was ~{pos})'
return result
def find_all_lfms():
"""Find TPF0/TPF1 magic using chunked reading (fast, no byte-by-byte loop)."""
found = []
seg_count = ida_segment.get_segm_qty()
for i in range(seg_count):
seg = ida_segment.getnseg(i)
if not seg:
continue
ea = seg.start_ea
end = seg.end_ea
while ea < end:
chunk_size = min(CHUNK_SIZE, end - ea)
data = ida_bytes.get_bytes(ea, chunk_size)
if not data:
ea += chunk_size
continue
offset = 0
while True:
idx = data.find(b'TPF0', offset)
if idx >= 0:
found.append((ea + idx, 'TPF0'))
offset = idx + 4
continue
idx = data.find(b'TPF1', offset)
if idx >= 0:
found.append((ea + idx, 'TPF1'))
offset = idx + 4
continue
break
ea += chunk_size
return found
def is_event_prop(name):
"""Check if a property name is an event handler."""
return name.startswith('On') and len(name) > 2 and name[2:3].isupper()
def collect_events(obj, path='', events=None):
"""Recursively collect all event handlers from an object tree."""
if events is None:
events = []
obj_path = path + (obj['name'] if obj['name'] else obj['type'])
for k, v in obj['props'].items():
if is_event_prop(k) and isinstance(v, str) and v:
events.append({
'component_path': obj_path,
'component_type': obj['type'],
'component_name': obj['name'],
'event': k,
'handler': v,
})
for child in obj.get('children', []):
collect_events(child, obj_path + '.', events)
return events
def collect_components(obj, path='', components=None):
"""Recursively collect all components."""
if components is None:
components = []
obj_path = path + (obj['name'] if obj['name'] else obj['type'])
components.append({
'path': obj_path,
'type': obj['type'],
'name': obj['name'],
'prop_count': len(obj['props']),
'child_count': len(obj.get('children', [])),
})
for child in obj.get('children', []):
collect_components(child, obj_path + '.', components)
return components
def _sanitize_str(s):
"""Sanitize a string for text file output.
Replace non-printable characters (including null bytes and high bytes)
with escape sequences to prevent binary content in the output file.
"""
if not isinstance(s, str):
return str(s)
result = []
for c in s:
o = ord(c)
if c == '\n':
result.append('\\n')
elif c == '\r':
result.append('\\r')
elif c == '\t':
result.append('\\t')
elif o < 32 or o >= 127:
result.append(f'\\x{o:02X}')
else:
result.append(c)
return ''.join(result)
def _sanitize_val(v):
"""Sanitize a value for text file output (like repr but safe)."""
if isinstance(v, str):
return f"'{_sanitize_str(v)}'"
elif isinstance(v, list):
return '[' + ', '.join(_sanitize_val(i) for i in v) + ']'
elif isinstance(v, dict):
return '{' + ', '.join(f"'{_sanitize_str(k)}': {_sanitize_val(val)}" for k, val in v.items()) + '}'
elif isinstance(v, bool):
return repr(v)
elif v is None:
return 'None'
else:
return str(v)
def _vmt_read_pascal_string(addr):
"""读取指定地址处的 Pascal ShortString。
Pascal ShortString 格式:
- 第 1 字节: 字符串长度 N (0-255)
- 后续 N 字节: 字符串内容 (单字节编码)
返回解码后的字符串, 失败返回 None。
"""
if addr == 0 or addr == idc.BADADDR or addr < 0x1000:
return None
slen = ida_bytes.get_byte(addr)
if slen == 0 or slen > 255:
return None
data = ida_bytes.get_bytes(addr + 1, slen)
if not data:
return None
try:
return data.decode('latin1')
except Exception:
return None
def verify_vmt(vmt_addr):
"""验证地址是否指向有效的 FPC 64-bit VMT。
校验条件:
1. 地址有效 (非 BADADDR, 大于 0x1000)
2. vInstanceSize != 0
3. vInstanceSize + vInstanceSize2 == 0 (校验字段, mod 2^64)
返回 True/False。
"""
if vmt_addr == idc.BADADDR or vmt_addr < 0x1000:
return False
vInstanceSize = ida_bytes.get_qword(vmt_addr)
vInstanceSize2 = ida_bytes.get_qword(vmt_addr + 8)
if vInstanceSize == 0:
return False
if (vInstanceSize + vInstanceSize2) & 0xFFFFFFFFFFFFFFFF != 0:
return False
return True
def read_vmt_fields(vmt_addr):
"""读取 VMT 的所有 13 个字段。
返回 dict: {字段名: 值(uint64)}
"""
fields = {}
for offset, name, _desc in VMT_FIELDS:
fields[name] = ida_bytes.get_qword(vmt_addr + offset)
return fields
def read_method_table_detailed(method_table_ptr):
"""读取方法表的详细信息。
方法表格式:
- +0: count (uint32) - 方法数量
- +4: entries[count], 每项 16 字节:
- +0: name_ptr (uint64) -> 方法名 Pascal ShortString
- +8: code_ptr (uint64) -> 方法代码地址
返回 list of dict, 每项包含: index, name, name_ptr, code_ptr, code_name
"""
if method_table_ptr == 0 or method_table_ptr == idc.BADADDR or method_table_ptr < 0x1000:
return []
count_data = ida_bytes.get_bytes(method_table_ptr, 4)
if not count_data or len(count_data) < 4:
return []
count = int.from_bytes(count_data, 'little')
if count == 0 or count > MAX_METHODS_PER_VMT:
return []
entries_data = ida_bytes.get_bytes(method_table_ptr + 4, count * 16)
if not entries_data or len(entries_data) < count * 16:
return []
methods = []
for i in range(count):
offset = i * 16
name_ptr = int.from_bytes(entries_data[offset:offset + 8], 'little')
code_ptr = int.from_bytes(entries_data[offset + 8:offset + 16], 'little')
method_name = _vmt_read_pascal_string(name_ptr) if name_ptr else None
code_name = idc.get_name(code_ptr) if code_ptr else ''
methods.append({
'index': i,
'name': method_name or '<unknown>',
'name_ptr': name_ptr,
'code_ptr': code_ptr,
'code_name': code_name,
})
return methods
def walk_vmt_chain(vmt_addr, max_depth=MAX_VMT_CHAIN_DEPTH):
"""遍历 VMT 父类继承链。
FPC VMT 的 vParentRef 字段指向父类的 VMT。本函数从给定 VMT 开始,
沿父类链向上遍历, 直到:
- 到达 TObject (无父类, vParentRef=0)
- 达到最大深度限制
- 遇到无效 VMT
- 检测到循环引用
返回 list of (vmt_addr, class_name) 元组, 从当前类到根类。
"""
chain = []
seen = set()
current = vmt_addr
depth = 0
while current and current != idc.BADADDR and depth < max_depth:
if current in seen:
break # 检测到循环引用
seen.add(current)
if not verify_vmt(current):
break
class_name_ptr = ida_bytes.get_qword(current + 24) # vClassName offset
class_name = _vmt_read_pascal_string(class_name_ptr) or '<unknown>'
chain.append((current, class_name))
parent_ptr = ida_bytes.get_qword(current + 16) # vParentRef offset
if parent_ptr == 0 or parent_ptr == idc.BADADDR:
break
current = parent_ptr
depth += 1
return chain
def parse_vmt_at(vmt_addr):
"""解析指定地址处的 VMT, 返回详细信息。
解析内容包括:
1. VMT 地址和符号名
2. 所有 13 个字段的值
3. 类名 (从 vClassName 指向的 Pascal ShortString 读取)
4. 方法表的所有方法 (索引、名称、代码地址、符号名)
5. 父类继承链 (沿 vParentRef 向上遍历)
参数:
vmt_addr: VMT 起始地址
返回:
dict: {'vmt_addr', 'vmt_symbol', 'class_name', 'fields',
'methods', 'parent_chain', 'error'}
"""
result = {
'vmt_addr': vmt_addr,
'vmt_symbol': idc.get_name(vmt_addr) or '',
'class_name': None,
'fields': {},
'methods': [],
'parent_chain': [],
'error': None,
}
if not verify_vmt(vmt_addr):
inst_size = ida_bytes.get_qword(vmt_addr) if vmt_addr >= 0x1000 else 0
inst_size2 = ida_bytes.get_qword(vmt_addr + 8) if vmt_addr >= 0x1000 else 0
result['error'] = (
f'地址 0x{vmt_addr:X} 不是有效的 FPC VMT '
f'(vInstanceSize=0x{inst_size:X}, vInstanceSize2=0x{inst_size2:X}, '
f'校验{"通过" if (inst_size + inst_size2) & 0xFFFFFFFFFFFFFFFF == 0 else "失败"})'
)
return result
result['fields'] = read_vmt_fields(vmt_addr)
# 读取类名
class_name_ptr = result['fields']['vClassName']
if class_name_ptr:
result['class_name'] = _vmt_read_pascal_string(class_name_ptr)
# 读取方法表
method_table_ptr = result['fields']['vMethodTable']
result['methods'] = read_method_table_detailed(method_table_ptr)
# 遍历父类继承链
result['parent_chain'] = walk_vmt_chain(vmt_addr)
return result
def format_vmt_report(result):
"""将 VMT 解析结果格式化为可读的文本报告。
报告包含:
- VMT 地址和符号
- 类名
- 所有 13 个字段的值、说明和解析后的指向内容
- 方法表的完整方法列表
- 父类继承链
"""
lines = []
vmt_addr = result['vmt_addr']
if result['error']:
lines.append('=' * 80)
lines.append('VMT 解析失败')
lines.append('=' * 80)
lines.append(result['error'])
return '\n'.join(lines)
vmt_symbol = result['vmt_symbol'] or '<unnamed>'
class_name = result['class_name'] or '<unknown>'
lines.append('=' * 80)
lines.append(f'VMT 解析报告: {class_name}')
lines.append('=' * 80)
lines.append(f'VMT 地址: 0x{vmt_addr:08X} (符号: {vmt_symbol})')
lines.append(f'类名: {class_name}')
lines.append(f'方法数: {len(result["methods"])}')
lines.append(f'继承深度: {len(result["parent_chain"]) - 1 if result["parent_chain"] else 0}')
lines.append('')
# 字段表
lines.append('-' * 80)
lines.append('VMT 字段 (13 个 qword, 共 104 字节)')
lines.append('-' * 80)
for offset, name, desc in VMT_FIELDS:
value = result['fields'].get(name, 0)
extra = ''
if name == 'vClassName' and value:
cn = _vmt_read_pascal_string(value)
if cn:
extra = f' -> "{cn}"'
elif name == 'vParentRef' and value:
if verify_vmt(value):
parent_cn_ptr = ida_bytes.get_qword(value + 24)
parent_cn = _vmt_read_pascal_string(parent_cn_ptr)
if parent_cn:
extra = f' -> {parent_cn}_VMT (0x{value:08X})'
else:
extra = f' -> 0x{value:08X}'
else:
extra = f' -> 0x{value:08X}'
elif name == 'vMethodTable' and value:
count_data = ida_bytes.get_bytes(value, 4)
if count_data and len(count_data) == 4:
count = int.from_bytes(count_data, 'little')
extra = f' -> {count} 个方法 (0x{value:08X})'
else:
extra = f' -> 0x{value:08X}'
elif name == 'vDestroy' and value:
sym = idc.get_name(value)
if sym:
extra = f' -> {sym} (0x{value:08X})'
else:
extra = f' -> 0x{value:08X}'
elif name == 'vInstanceSize2':
inst_size = result['fields'].get('vInstanceSize', 0)
expected = (-inst_size) & 0xFFFFFFFFFFFFFFFF
if value == expected:
extra = ' (校验通过)'
else:
extra = f' (校验失败! 期望 0x{expected:X})'
lines.append(f' +{offset:3d} (0x{offset:02X}) {name:20s} = 0x{value:016X} {desc}{extra}')
lines.append('')
# 方法表
lines.append('-' * 80)
lines.append(f'方法表 ({len(result["methods"])} 个方法)')
lines.append('-' * 80)
if result['methods']:
for m in result['methods']:
code_sym = m['code_name'] or '<unnamed>'
lines.append(
f' [{m["index"]:3d}] {m["name"]:35s} '
f'name@0x{m["name_ptr"]:08X} code@0x{m["code_ptr"]:08X} ({code_sym})'
)
else:
lines.append(' (无已发布方法)')
lines.append('')
# 父类继承链
lines.append('-' * 80)
lines.append('父类继承链')
lines.append('-' * 80)
if result['parent_chain']:
for i, (addr, cn) in enumerate(result['parent_chain']):
sym = idc.get_name(addr) or '<unnamed>'
indent = ' ' + ' ' * i
if i == 0:
lines.append(f'{indent}[{i}] {cn} (VMT@0x{addr:08X}, {sym}) <-- 当前类')
else:
lines.append(f'{indent}└─ [{i}] {cn} (VMT@0x{addr:08X}, {sym})')
else:
lines.append(' (无父类)')
lines.append('')
return '\n'.join(lines)
def find_vmt_for_address(ea):
"""查找包含给定地址的 VMT 起始地址。
查找策略:
1. 先检查 ea 本身是否是 VMT 起始地址
2. 若不是, 向前按 8 字节对齐搜索 (最多搜索 VMT_SEARCH_BACK_RANGE 范围)
返回 VMT 起始地址, 未找到返回 None。
"""
# 1. 检查 ea 本身
if verify_vmt(ea):
return ea
# 2. 向前搜索 (8 字节对齐)
aligned_ea = ea & ~0x7 # 对齐到 8 字节边界
for offset in range(0, VMT_SEARCH_BACK_RANGE, 8):
candidate = aligned_ea - offset
if candidate < 0x1000:
break
if verify_vmt(candidate):
return candidate
return None
def parse_vmt_main(ea=None):
"""VMT 解析入口函数。
解析指定地址或当前光标位置的 VMT, 输出详细报告到控制台和文件。
参数:
ea: VMT 地址。若为 None, 使用当前光标位置 (idc.here())。
若 ea 不是 VMT 起始地址, 自动向前查找最近的 VMT 起始。
输出:
- 控制台打印 VMT 解析报告
- 写入 VMT_REPORT_PATH 指定的文件
"""
if ea is None:
ea = idc.here()
print(f'\n解析 VMT, 起始地址: 0x{ea:08X}')
# 若 ea 不是 VMT 起始, 尝试向前查找
if not verify_vmt(ea):
found = find_vmt_for_address(ea)
if found is not None:
print(f'注意: 0x{ea:X} 不是 VMT 起始地址, 已自动向前查找至 0x{found:08X}')
ea = found
else:
print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到有效 VMT')
return None
result = parse_vmt_at(ea)
report = format_vmt_report(result)
print(report)
# 写入文件
try:
with open(VMT_REPORT_PATH, 'w', encoding='utf-8') as f:
f.write(report)
f.write('\n')
# 附加原始数据 (便于程序化处理)
f.write('\n' + '=' * 80 + '\n')
f.write('原始数据 (JSON-like)\n')
f.write('=' * 80 + '\n')
f.write(f'vmt_addr: 0x{result["vmt_addr"]:X}\n')
f.write(f'vmt_symbol: {result["vmt_symbol"]}\n')
f.write(f'class_name: {result["class_name"]}\n')
f.write(f'fields:\n')
for offset, name, desc in VMT_FIELDS:
val = result['fields'].get(name, 0)
f.write(f' {name} (0x{offset:02X}): 0x{val:016X}\n')
f.write(f'methods ({len(result["methods"])}):\n')
for m in result['methods']:
f.write(f' [{m["index"]}] {m["name"]} | name_ptr=0x{m["name_ptr"]:X} | code_ptr=0x{m["code_ptr"]:X} | symbol={m["code_name"]}\n')
f.write(f'parent_chain ({len(result["parent_chain"])}):\n')
for i, (addr, cn) in enumerate(result['parent_chain']):
f.write(f' [{i}] {cn} @ 0x{addr:X}\n')
print(f'\n报告已写入: {VMT_REPORT_PATH}')
except Exception as e:
print(f'写入文件失败: {e}')
return result
# VMT 扫描日志文件句柄 (scan_all_vmts 开始时打开, 结束时关闭)
_vmt_log_file = None
def _log_print(msg):
"""输出到 IDA Output 窗口和日志文件。
注意: IDA 中 print() 内部也调用 ida_kernwin.msg(), 两者同时使用会导致
重复输出。因此只使用 ida_kernwin.msg() 一次, 并写入日志文件。
"""
msg_str = str(msg)
ida_kernwin.msg(msg_str + '\n')
if _vmt_log_file is not None:
try:
_vmt_log_file.write(msg_str + '\n')
_vmt_log_file.flush()
except Exception:
pass
# =============================================================================
# 备用 VMT 查找函数 (旧版按类名搜索算法)
# scan_all_vmts() 已改用 _scan_vmts_direct() 直接扫描法 (1 次全段扫描),
# 以下函数保留供单类名查询或参考使用:
# _extract_class_names_from_lfms() — 从 LFM 资源提取类名
# _search_bytes() — 通用字节模式搜索
# _find_class_name_strings() — 数据段 Pascal ShortString 搜索
# _find_vmt_for_class_name() — 按类名搜索 VMT (xref + 指针搜索)
# =============================================================================
def _extract_class_names_from_lfms():
"""从所有 TPF0/TPF1 LFM 资源中提取窗体类名。
LFM 格式: [TPF0/TPF1 (4字节)] [可选 0xF1] [Pascal ShortString 类名] ...
返回 set of class_name (去重)。
"""
class_names = set()
lfm_addrs = find_all_lfms()
for ea, _magic in lfm_addrs:
# 只读头部 256 字节即可获取类名
data = ida_bytes.get_bytes(ea, 256)
if not data or len(data) < 6:
continue
if data[:4] not in (b'TPF0', b'TPF1'):
continue
pos = 4
# 跳过可选的 0xF1 根对象标记
if pos < len(data) and data[pos] == 0xF1:
pos += 1
if pos >= len(data):
continue
slen = data[pos]
if slen == 0 or slen > 255 or pos + 1 + slen > len(data):
continue
name_bytes = data[pos + 1:pos + 1 + slen]
# 验证类名: 仅含 A-Z/a-z/0-9/_, 以字母开头
if not name_bytes:
continue
if not (0x41 <= name_bytes[0] <= 0x5A or 0x61 <= name_bytes[0] <= 0x7A):
continue
if not all(0x41 <= b <= 0x5A or 0x61 <= b <= 0x7A or 0x30 <= b <= 0x39 or b == 0x5F for b in name_bytes):
continue
try:
class_names.add(name_bytes.decode('ascii'))
except Exception:
continue
return class_names
def _search_bytes(pattern):
"""在所有段中搜索字节模式, 返回匹配地址列表。
使用分块读取 + bytes.find() (C 实现), 比 Python 循环快得多。
"""
results = []
seg_count = ida_segment.get_segm_qty()
for i in range(seg_count):
seg = ida_segment.getnseg(i)
if not seg:
continue
ea = seg.start_ea
end = seg.end_ea
while ea < end:
chunk_size = min(CHUNK_SIZE, end - ea)
data = ida_bytes.get_bytes(ea, chunk_size)
if not data:
ea += chunk_size
continue
offset = 0
while True:
idx = data.find(pattern, offset)
if idx < 0:
break
results.append(ea + idx)
offset = idx + 1
ea += chunk_size
return results
_SEG_DATA_TYPE = getattr(ida_segment, 'SEG_DATA', 3)
def _find_class_name_strings():
"""搜索所有 Pascal ShortString 格式的类名字符串 (仅数据段)。
Pascal ShortString 格式: [长度字节][字符串内容]
类名特征: 以大写字母开头 (T/E/I/P 等), 仅含字母/数字/下划线, 长度 4-255。
只搜索数据段 (SEG_DATA), 跳过代码段 — 代码段中 0x54 ('T') 是
PUSH RSP 指令操作码, 会产生大量误报导致性能严重下降。
返回 dict: {字符串地址: 类名}
"""
found = {}
seg_count = ida_segment.get_segm_qty()
for seg_idx in range(seg_count):
seg = ida_segment.getnseg(seg_idx)
if not seg:
continue
# 只搜索数据段, 跳过代码段
seg_type = ida_segment.segtype(seg.start_ea)
if seg_type != _SEG_DATA_TYPE:
continue
ea = seg.start_ea
end = seg.end_ea
while ea < end:
chunk_size = min(CHUNK_SIZE, end - ea)
data = ida_bytes.get_bytes(ea, chunk_size)
if not data:
ea += chunk_size
continue
offset = 0
while offset < len(data):
idx = data.find(b'T', offset)
if idx < 0:
break
offset = idx + 1
if idx == 0:
continue
slen = data[idx - 1]
if slen < 4 or slen > 255:
continue
name_end = idx + slen
if name_end > len(data):
continue
name_bytes = data[idx:name_end]
if not all(0x41 <= b <= 0x5A or 0x61 <= b <= 0x7A or 0x30 <= b <= 0x39 or b == 0x5F for b in name_bytes):
continue
str_addr = ea + idx - 1
if str_addr in found:
continue
try:
class_name = name_bytes.decode('ascii')
except Exception:
continue
found[str_addr] = class_name
ea += chunk_size
return found
def _find_vmt_for_class_name(class_name):
"""对单个类名搜索其 VMT 地址。
与 setup_vmt_structs.py 的 find_vmt_for_class 相同的已验证算法:
1. 构造 Pascal ShortString 模式: [长度字节]+[类名]
2. 搜索该模式找到字符串地址
3. Method A: 对每个字符串地址用 XrefsTo 找 VMT 引用
4. Method B: 回退到指针搜索 (搜索指向字符串地址的 8 字节指针)
返回 (vmt_addr, str_addr) 或 (None, None)。
"""
name_bytes = class_name.encode('latin1')
pattern = bytes([len(name_bytes)]) + name_bytes
string_addrs = _search_bytes(pattern)
if not string_addrs:
return None, None
# Method A: xref
for str_addr in string_addrs:
for xref in idautils.XrefsTo(str_addr):
vmt_addr = xref.frm - 24 # vClassName 偏移
if verify_vmt(vmt_addr):
vClassName_ptr = ida_bytes.get_qword(vmt_addr + 24)
if vClassName_ptr == str_addr:
return vmt_addr, str_addr
# Method B: 指针搜索回退
for str_addr in string_addrs:
target_bytes = str_addr.to_bytes(8, 'little')
ptr_addrs = _search_bytes(target_bytes)
for ptr_addr in ptr_addrs:
vmt_addr = ptr_addr - 24
if verify_vmt(vmt_addr):
vClassName_ptr = ida_bytes.get_qword(vmt_addr + 24)
if vClassName_ptr == str_addr:
return vmt_addr, str_addr
return None, None
def _scan_vmts_direct():
"""直接扫描数据段, 通过 VMT 校验条件一次性查找所有 VMT。
VMT 校验条件 (全部必须满足):
1. vInstanceSize (offset 0, uint64) != 0
2. vInstanceSize + vInstanceSize2 (offset 8) == 0 (mod 2^64)
3. vClassName (offset 24) 指针指向合法 Pascal ShortString
4. 类名以字母开头, 仅含 A-Z/a-z/0-9/_
相比按类名逐个搜索 (N 次全段扫描), 本方法仅需 1 次全段扫描,
速度提升数百倍, 且覆盖率 100%。
返回 dict: {vmt_addr: (class_name, class_name_str_addr)}
"""
found = {}
seg_count = ida_segment.get_segm_qty()
for seg_idx in range(seg_count):
seg = ida_segment.getnseg(seg_idx)
if not seg:
continue
seg_type = ida_segment.segtype(seg.start_ea)
if seg_type != _SEG_DATA_TYPE:
continue
seg_start = seg.start_ea
seg_end = seg.end_ea
# 对齐起始地址到 8 字节边界
if seg_start % 8 != 0:
seg_start = (seg_start + 7) & ~7
ea = seg_start
while ea < seg_end:
chunk_size = min(CHUNK_SIZE, seg_end - ea)
# 多读 32 字节确保块末尾的候选 VMT 能读取完整 32 字节头部
read_size = min(chunk_size + 32, seg_end - ea)
data = ida_bytes.get_bytes(ea, read_size)
if not data:
ea += chunk_size
continue
scan_limit = len(data) - 32 # 确保能读 32 字节
offset = 0
while offset < scan_limit:
# 快速检查: vInstanceSize != 0
v0 = data[offset]
v1 = data[offset + 1]
v2 = data[offset + 2]
v3 = data[offset + 3]
if v0 == 0 and v1 == 0 and v2 == 0 and v3 == 0:
# 前 4 字节为 0, vInstanceSize 可能为 0, 跳过
# (但仍需检查高 4 字节, 但概率极低, 直接跳过加速)
offset += 8
continue
# 读取 vInstanceSize (8 字节) 和 vInstanceSize2 (8 字节)
vInstanceSize = int.from_bytes(data[offset:offset + 8], 'little')
if vInstanceSize == 0:
offset += 8
continue
vInstanceSize2 = int.from_bytes(data[offset + 8:offset + 16], 'little')
if (vInstanceSize + vInstanceSize2) & 0xFFFFFFFFFFFFFFFF != 0:
offset += 8
continue
# 初步校验通过, 读取 vClassName 指针 (offset 24)
vClassName_ptr = int.from_bytes(data[offset + 24:offset + 32], 'little')
if vClassName_ptr == 0 or vClassName_ptr == idc.BADADDR or vClassName_ptr < 0x1000:
offset += 8
continue
# 验证 vClassName 指向合法 Pascal ShortString
class_name = _vmt_read_pascal_string(vClassName_ptr)
if not class_name:
offset += 8
continue
# 类名格式校验: 以字母开头, 仅含字母/数字/下划线
if not _is_valid_lfm_identifier(class_name):
offset += 8
continue
vmt_addr = ea + offset
if vmt_addr not in found:
found[vmt_addr] = (class_name, vClassName_ptr)
offset += 8
ea += chunk_size
return found
def scan_all_vmts():
"""扫描二进制文件中所有 FPC 64-bit VMT 结构。
使用直接扫描法: 遍历所有数据段的 8 字节对齐位置, 通过 VMT 校验条件
(vInstanceSize + vInstanceSize2 == 0 且 vClassName 指向合法 ShortString)
一次性识别所有 VMT。
相比旧版按类名逐个搜索 (N 次全段扫描), 本方法仅需 1 次全段扫描,
速度提升数百倍, 且覆盖率 100%。
输出:
- IDA Output 窗口 + 日志文件 (vmt_scan_log.txt)
- 完整报告写入 VMT_SCAN_REPORT_PATH
"""
global _vmt_log_file
vmt_log_path = os.path.join(_idb_dir, 'vmt_scan_log.txt')
try:
_vmt_log_file = open(vmt_log_path, 'w', encoding='utf-8')
except Exception as e:
ida_kernwin.msg(f'警告: 无法打开日志文件 {vmt_log_path}: {e}\n')
_vmt_log_file = None
found_vmts = {}
try:
_log_print('\n=== 直接扫描法: 遍历数据段 8 字节对齐位置 ===')
_log_print('校验条件: vInstanceSize != 0, vInstanceSize + vInstanceSize2 == 0,')
_log_print(' vClassName 指针 -> 合法 Pascal ShortString 类名')
_log_print('')
found_vmts = _scan_vmts_direct()
_log_print(f'\n{"=" * 80}')
_log_print(f'扫描完成, 共发现 {len(found_vmts)} 个 VMT')
if not found_vmts:
_log_print('未找到任何 VMT')
return {}
# 按地址排序
sorted_vmts = sorted(found_vmts.items())
# 控制台输出前 50 个
_log_print(f'\n=== VMT 列表 (前 50 个, 共 {len(sorted_vmts)}) ===')
_log_print(f'{"地址":>12s} | {"实例大小":>10s} | {"类名":<35s} | {"父类":<20s}')
_log_print('-' * 90)
for vmt_addr, (class_name, _cn_ptr) in sorted_vmts[:50]:
inst_size = ida_bytes.get_qword(vmt_addr)
parent_ptr = ida_bytes.get_qword(vmt_addr + 16)
parent_name = '<none>'
if parent_ptr and parent_ptr != idc.BADADDR:
if verify_vmt(parent_ptr):
parent_cn_ptr = ida_bytes.get_qword(parent_ptr + 24)
parent_name = _vmt_read_pascal_string(parent_cn_ptr) or '<unknown>'
_log_print(f' 0x{vmt_addr:08X} | {inst_size:10d} | {class_name:<35s} | {parent_name:<20s}')
if len(sorted_vmts) > 50:
_log_print(f' ... 还有 {len(sorted_vmts) - 50} 个, 详见报告文件')
# 写入完整报告
try:
with open(VMT_SCAN_REPORT_PATH, 'w', encoding='utf-8') as f:
f.write('FPC 64-bit VMT 全量扫描报告\n')
f.write('=' * 80 + '\n\n')
f.write(f'扫描方法: 直接扫描法 (数据段 8 字节对齐位置 VMT 校验)\n')
f.write(f'VMT 总数: {len(found_vmts)}\n\n')
f.write('=' * 80 + '\n')
f.write('VMT 地址 | 实例大小 | 类名 | 父类 | 方法数 | 符号名\n')
f.write('=' * 80 + '\n')
for vmt_addr, (class_name, _cn_ptr) in sorted_vmts:
fields = read_vmt_fields(vmt_addr)
inst_size = fields['vInstanceSize']
parent_ptr = fields['vParentRef']
parent_name = '<none>'
if parent_ptr and parent_ptr != idc.BADADDR:
if verify_vmt(parent_ptr):
parent_cn_ptr = ida_bytes.get_qword(parent_ptr + 24)
parent_name = _vmt_read_pascal_string(parent_cn_ptr) or '<unknown>'
method_table_ptr = fields['vMethodTable']
method_count = 0
if method_table_ptr and method_table_ptr != idc.BADADDR:
count_data = ida_bytes.get_bytes(method_table_ptr, 4)
if count_data and len(count_data) == 4:
method_count = int.from_bytes(count_data, 'little')
vmt_symbol = idc.get_name(vmt_addr) or '<unnamed>'
f.write(f'0x{vmt_addr:08X} | {inst_size:10d} | {class_name:<35s} | {parent_name:<20s} | {method_count:4d} | {vmt_symbol}\n')
# 附加每个 VMT 的详细方法列表
f.write('\n' + '=' * 80 + '\n')
f.write('各 VMT 方法详情\n')
f.write('=' * 80 + '\n')
for vmt_addr, (class_name, _cn_ptr) in sorted_vmts:
methods = read_method_table_detailed(ida_bytes.get_qword(vmt_addr + 40))
if not methods:
continue
f.write(f'\n{class_name} (VMT@0x{vmt_addr:X}, {len(methods)} 个方法):\n')
for m in methods:
f.write(f' [{m["index"]:3d}] {m["name"]:35s} code@0x{m["code_ptr"]:08X} ({m["code_name"]})\n')
_log_print(f'\n完整报告已写入: {VMT_SCAN_REPORT_PATH}')
except Exception as e:
_log_print(f'写入报告文件失败: {e}')
return found_vmts
except Exception as e:
import traceback
_log_print(f'扫描出错: {e}')
_log_print(traceback.format_exc())
return {}
finally:
if _vmt_log_file is not None:
try:
_vmt_log_file.close()
except Exception:
pass
_vmt_log_file = None
# 弹出完成对话框
try:
n = len(found_vmts)
ida_kernwin.info(
f'VMT 扫描完成\n\n'
f'发现 VMT 数量: {n}\n'
f'日志文件: {os.path.join(_idb_dir, "vmt_scan_log.txt")}\n'
f'报告文件: {VMT_SCAN_REPORT_PATH}'
)
except Exception:
pass
# =============================================================================
# LFM 单个解析与自动检测 Part
# =============================================================================
LFM_REPORT_PATH = os.path.join(_idb_dir, 'lfm_parse_report.txt')
def is_lfm_at(ea):
"""检查指定地址处是否是 LFM (以 TPF0 或 TPF1 标记开头)。
LFM 起始标记为 4 字节 ASCII: b'TPF0' 或 b'TPF1'。
"""
if ea == idc.BADADDR or ea < 0x1000:
return False
magic = ida_bytes.get_bytes(ea, 4)
if not magic or len(magic) < 4:
return False
return magic in (b'TPF0', b'TPF1')
def find_lfm_near(ea):
"""向前查找最近的 LFM 起始地址 (搜索 TPF0/TPF1 标记)。
使用分块读取 + bytes.rfind() 快速查找, 而非逐字节扫描。
搜索范围为 ea 向前 VMT_SEARCH_BACK_RANGE 字节。
返回 LFM 起始地址, 未找到返回 None。
"""
if is_lfm_at(ea):
return ea
search_start = max(ea - VMT_SEARCH_BACK_RANGE, 0x1000)
search_size = ea - search_start + 4
if search_size <= 4:
return None
data = ida_bytes.get_bytes(search_start, search_size)
if not data:
return None
# rfind 从末尾向前查找, 找到的是离 ea 最近的标记
idx_tpf0 = data.rfind(b'TPF0')
idx_tpf1 = data.rfind(b'TPF1')
idx = max(idx_tpf0, idx_tpf1)
if idx < 0:
return None
return search_start + idx
def format_lfm_report(result):
"""将单个 LFM 解析结果格式化为可读的文本报告。
报告包含:
- LFM 地址
- 窗体类名和实例名
- 属性列表
- 事件处理器列表
- 组件树 (递归)
"""
lines = []
ea = result['address']
if result['error']:
lines.append('=' * 80)
lines.append('LFM 解析失败')
lines.append('=' * 80)
lines.append(f'地址: 0x{ea:08X}')
lines.append(f'错误: {result["error"]}')
return '\n'.join(lines)
forms = result['forms']
lines.append('=' * 80)
lines.append(f'LFM 解析报告 ({len(forms)} 个窗体)')
lines.append('=' * 80)
lines.append(f'LFM 地址: 0x{ea:08X}')
lines.append(f'标记: {ida_bytes.get_bytes(ea, 4).decode("ascii", errors="replace")}')
lines.append('')
for form in forms:
events = collect_events(form)
components = collect_components(form)
evt_count = sum(1 for k in form['props'] if is_event_prop(k))
child_count = len(form.get('children', []))
lines.append('-' * 80)
lines.append(f'窗体: {_sanitize_str(form["type"])} : {_sanitize_str(form["name"])}')
lines.append('-' * 80)
lines.append(f' 顶级事件数: {evt_count}')
lines.append(f' 子组件数: {child_count}')
lines.append(f' 总组件数: {len(components)}')
lines.append(f' 总事件数: {len(events)}')
lines.append('')
# 属性列表
non_evt_props = {k: v for k, v in form['props'].items() if not is_event_prop(k)}
if non_evt_props:
lines.append(' 属性:')
for k, v in non_evt_props.items():
lines.append(f' {_sanitize_str(k)} = {_sanitize_val(v)}')
lines.append('')
# 事件处理器
if events:
lines.append(' 事件处理器:')
for evt in events:
lines.append(
f' {_sanitize_str(evt["event"]):20s} -> '
f'{_sanitize_str(evt["handler"]):30s} '
f'({_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])})'
)
lines.append('')
# 组件树
lines.append(' 组件树:')
def _write_tree(obj, indent=1):
pad = ' ' + ' ' * indent
evt_n = sum(1 for k in obj['props'] if is_event_prop(k))
non_evt_n = len(obj['props']) - evt_n
child_n = len(obj.get('children', []))
lines.append(
f'{pad}{_sanitize_str(obj["type"])}: {_sanitize_str(obj["name"])} '
f'({non_evt_n} 属性, {evt_n} 事件, {child_n} 子组件)'
)
for child in obj.get('children', []):
_write_tree(child, indent + 1)
_write_tree(form)
lines.append('')
return '\n'.join(lines)
def parse_lfm_main(ea=None):
"""单个 LFM 解析入口函数。
解析指定地址或当前光标位置的 LFM, 输出详细报告到控制台和文件。
参数:
ea: LFM 起始地址。若为 None, 使用当前光标位置 (idc.here())。
若 ea 不是 LFM 起始 (无 TPF0/TPF1 标记), 自动向前查找最近的 LFM。
输出:
- 控制台打印 LFM 解析报告
- 写入 LFM_REPORT_PATH 指定的文件
"""
if ea is None:
ea = idc.here()
print(f'\n解析 LFM, 起始地址: 0x{ea:08X}')
if not is_lfm_at(ea):
found = find_lfm_near(ea)
if found is not None:
print(f'注意: 0x{ea:X} 不是 LFM 起始地址, 已自动向前查找至 0x{found:08X}')
ea = found
else:
print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到 LFM')
return None
result = parse_lfm(ea)
report = format_lfm_report(result)
print(report)
# 写入文件
try:
with open(LFM_REPORT_PATH, 'w', encoding='utf-8') as f:
f.write(report)
f.write('\n')
print(f'\n报告已写入: {LFM_REPORT_PATH}')
except Exception as e:
print(f'写入文件失败: {e}')
return result
def detect_data_type_at(ea):
"""检测指定地址处的数据类型 (LFM 或 VMT)。
检测策略:
1. 先检查 ea 本身是否是 LFM (TPF0/TPF1 标记)
2. 再检查 ea 本身是否是 VMT (vInstanceSize + vInstanceSize2 == 0)
3. 若都不是, 向前查找最近的 LFM 和 VMT 起始
4. 选择距离 ea 最近的数据块
返回: (类型字符串, 起始地址)
类型字符串: 'lfm' / 'vmt' / None
起始地址: int 或 None
"""
# 1. 检查 ea 本身
if is_lfm_at(ea):
return 'lfm', ea
if verify_vmt(ea):
return 'vmt', ea
# 2. 向前查找 LFM 和 VMT
lfm_addr = find_lfm_near(ea)
vmt_addr = find_vmt_for_address(ea)
# 3. 选择最近的
candidates = []
if lfm_addr is not None:
candidates.append(('lfm', lfm_addr, ea - lfm_addr))
if vmt_addr is not None:
candidates.append(('vmt', vmt_addr, ea - vmt_addr))
if not candidates:
return None, None
candidates.sort(key=lambda x: x[2])
return candidates[0][0], candidates[0][1]
def auto_parse_at(ea=None):
"""自动检测并解析指定地址处的 LFM 或 VMT。
自动判断给定地址处是 LFM (窗体资源) 还是 VMT (虚拟方法表),
然后调用相应的解析器。若地址不是数据起始, 自动向前查找。
参数:
ea: 要检测的地址。若为 None, 使用当前光标位置 (idc.here())。
输出:
- 控制台打印检测结果和解析报告
- 写入对应的报告文件 (lfm_parse_report.txt 或 vmt_parse_report.txt)
"""
if ea is None:
ea = idc.here()
print(f'\n自动检测数据类型, 地址: 0x{ea:08X}')
data_type, start_addr = detect_data_type_at(ea)
if data_type is None:
print(f'错误: 在 0x{ea:X} 附近 {VMT_SEARCH_BACK_RANGE} 字节范围内未找到 LFM 或 VMT')
return None
if start_addr != ea:
print(f'注意: 0x{ea:X} 不是数据起始, 已自动向前查找至 0x{start_addr:08X}')
if data_type == 'lfm':
print('检测结果: LFM (Lazarus Form Module 窗体资源)')
return parse_lfm_main(start_addr)
elif data_type == 'vmt':
print('检测结果: VMT (虚拟方法表)')
return parse_vmt_main(start_addr)
return None
def scan_lfms():
"""扫描所有 TPF0/TPF1 LFM 资源并输出报告。
这是默认行为: 解析所有 LFM 窗体资源, 提取组件和事件处理器,
输出到 lfm_forms.txt 文件。
"""
print('Scanning for TPF0/TPF1 LFM resources (chunked reading)...')
found_lfms = find_all_lfms()
print(f'Total LFM resources found: {len(found_lfms)}')
print()
all_forms = []
all_events = []
all_components = []
for i, (ea, magic) in enumerate(found_lfms):
result = parse_lfm(ea)
if result['error']:
print(f'LFM #{i:2d} at 0x{ea:08X}: ERROR: {result["error"]}')
continue
for form in result['forms']:
all_forms.append((ea, form))
events = collect_events(form)
all_events.extend(events)
components = collect_components(form)
all_components.extend(components)
# Progress every 10
if (i + 1) % 10 == 0:
print(f' ...parsed {i + 1}/{len(found_lfms)}')
# Print summary to stdout
print(f'=== Parsed {len(all_forms)} forms ===')
for ea, form in all_forms:
evt_count = sum(1 for k in form['props'] if is_event_prop(k))
child_count = len(form.get('children', []))
print(f'0x{ea:08X}: {_sanitize_str(form["type"]):30s} {_sanitize_str(form["name"]):30s} ({evt_count} top-level events, {child_count} children)')
print()
print(f'=== Total {len(all_events)} event handlers (showing first 60) ===')
for evt in all_events[:60]:
print(f'{_sanitize_str(evt["event"]):20s} -> {_sanitize_str(evt["handler"]):30s} ({_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])})')
# Write detailed output to file
with open(OUTPUT_PATH, 'w', encoding='utf-8') as f:
def write_obj(obj, indent=0):
pad = ' ' * indent
f.write(f'{pad}object {_sanitize_str(obj["type"])}: {_sanitize_str(obj["name"])}\n')
for k, v in obj['props'].items():
f.write(f'{pad} {_sanitize_str(k)} = {_sanitize_val(v)}\n')
for child in obj.get('children', []):
write_obj(child, indent + 1)
f.write(f'Total LFM resources: {len(found_lfms)}\n')
f.write(f'Total parsed forms: {len(all_forms)}\n')
f.write(f'Total components: {len(all_components)}\n')
f.write(f'Total event handlers: {len(all_events)}\n\n')
f.write('=' * 100 + '\n')
f.write('FORMS (top-level)\n')
f.write('=' * 100 + '\n')
for ea, form in all_forms:
evt_count = sum(1 for k in form['props'] if is_event_prop(k))
f.write(f'\n0x{ea:08X}: {_sanitize_str(form["type"])} name={_sanitize_str(form["name"])} ({evt_count} top-level events, {len(form.get("children", []))} children)\n')
f.write('\n')
f.write('=' * 100 + '\n')
f.write('ALL COMPONENTS\n')
f.write('=' * 100 + '\n')
for comp in all_components:
f.write(f'{_sanitize_str(comp["type"]):30s} {_sanitize_str(comp["name"]):30s} path={_sanitize_str(comp["path"])}\n')
f.write('\n')
f.write('=' * 100 + '\n')
f.write('EVENT HANDLERS\n')
f.write('=' * 100 + '\n')
for evt in all_events:
f.write(f'{_sanitize_str(evt["event"]):20s} -> {_sanitize_str(evt["handler"]):30s} | {_sanitize_str(evt["component_type"])}/{_sanitize_str(evt["component_name"])} (path: {_sanitize_str(evt["component_path"])})\n')
f.write('\n')
f.write('=' * 100 + '\n')
f.write('FULL FORM DETAILS\n')
f.write('=' * 100 + '\n')
for ea, form in all_forms:
f.write(f'\n--- Form at 0x{ea:08X} ---\n')
write_obj(form)
print()
print(f'Detailed output written to {OUTPUT_PATH}')
def main():
"""主入口函数。
交互模式 (在 IDA Pro 中通过 File > Script File 或 Alt+F7 运行):
弹出三按钮对话框供用户选择操作模式:
- VMT全扫描: 扫描二进制中所有 VMT (按钮1, 默认)
- LFM扫描: 扫描所有 TPF0 LFM 窗体资源 (按钮2)
- 自动解析: 自动检测指定地址或当前光标处的 LFM/VMT 并解析 (按钮3)
批处理模式 (通过 idat64 -S 运行, idc.ARGV 含脚本名以外的参数):
- --parse [地址]: 自动检测并解析 (LFM 或 VMT), 地址缺省时用当前光标
- --lfm [地址]: 解析指定地址 LFM, 地址缺省时用当前光标
- --vmt [地址]: 解析指定地址 VMT, 地址缺省时用当前光标
- --vmt-scan: VMT 全量扫描
- 无参数: LFM 扫描
也可在 IDA Python 控制台直接调用:
auto_parse_at(0xC855F8) # 自动检测并解析
auto_parse_at() # 使用当前光标位置
parse_vmt_main(0xC855F8) # 解析指定地址 VMT
parse_lfm_main(0xC855F8) # 解析指定地址 LFM
scan_all_vmts() # 扫描所有 VMT
scan_lfms() # 扫描所有 LFM
"""
print('>>> main() 已调用')
sys.stdout.flush()
args = getattr(idc, 'ARGV', [])
print(f'>>> ARGV = {args}')
# 批处理模式: idc.ARGV 含脚本名以外的参数时, 按参数分发
if len(args) > 1:
def _parse_addr_arg(flag):
"""从 args 中提取 --flag 后的地址参数, 返回 ea 或 idc.here()。"""
if flag not in args:
return None
idx = args.index(flag)
addr_str = args[idx + 1] if idx + 1 < len(args) else None
if addr_str:
try:
return int(addr_str, 16) if addr_str.startswith('0x') else int(addr_str, 0)
except ValueError:
print(f'错误: 无效的地址参数 "{addr_str}"')
return -1
return idc.here()
if '--parse' in args:
ea = _parse_addr_arg('--parse')
if ea != -1:
auto_parse_at(ea)
elif '--lfm' in args:
ea = _parse_addr_arg('--lfm')
if ea != -1:
parse_lfm_main(ea)
elif '--vmt' in args:
ea = _parse_addr_arg('--vmt')
if ea != -1:
parse_vmt_main(ea)
elif '--vmt-scan' in args:
scan_all_vmts()
else:
scan_lfms()
return
# 交互模式: 弹出对话框选择操作
# IDA 9.x ask_buttons 返回值: btn1=1(ASKBTN_YES), btn2=0(ASKBTN_NO), btn3=-1(ASKBTN_CANCEL)
# btn3 与 Escape/关闭 返回值相同 (-1), 因此将最不常用的操作放在 btn3
ASKBTN_YES = getattr(ida_kernwin, 'ASKBTN_YES', 1)
ASKBTN_NO = getattr(ida_kernwin, 'ASKBTN_NO', 0)
ASKBTN_CANCEL = getattr(ida_kernwin, 'ASKBTN_CANCEL', -1)
choice = ida_kernwin.ask_buttons(
'VMT全扫描',
'LFM扫描',
'自动解析',
ASKBTN_YES,
'请选择操作模式:\n\n'
'VMT全扫描 - 扫描二进制中所有 VMT (按钮1)\n'
'LFM扫描 - 扫描所有 TPF0 LFM 窗体资源 (按钮2)\n'
'自动解析 - 自动检测当前光标处的 LFM/VMT 并解析 (按钮3)\n\n'
'提示: 也可在 Python 控制台直接调用:\n'
' auto_parse_at(0x地址)\n'
' parse_vmt_main(0x地址)\n'
' parse_lfm_main(0x地址)\n'
' scan_all_vmts()'
)
print(f'>>> 用户选择: {choice} (1=VMT全扫描, 0=LFM扫描, -1=自动解析)')
sys.stdout.flush()
if choice == ASKBTN_YES: # btn1 = VMT全扫描
print('>>> 正在启动 VMT 全扫描...')
sys.stdout.flush()
scan_all_vmts()
elif choice == ASKBTN_NO: # btn2 = LFM扫描
scan_lfms()
elif choice == ASKBTN_CANCEL: # btn3 = 自动解析 (Escape 也触发此项)
# 弹出地址输入框, 默认为当前光标位置
current_ea = idc.here()
addr = ida_kernwin.ask_addr(
current_ea,
'请输入要解析的地址 (十六进制):\n'
'默认为当前光标位置: 0x{:X}\n\n'
'将自动检测该地址处是 LFM 还是 VMT 并解析。\n'
'若不是数据起始, 自动向前查找最近的数据块。'.format(current_ea)
)
if addr is None or addr == idc.BADADDR:
print('已取消')
return
auto_parse_at(addr)
else:
print(f'>>> 未知选择: {choice}')
print('>>> parse_lfm.py 脚本已加载')
main()
print('>>> parse_lfm.py 脚本执行完毕')
parse_lfm.py v5 功能概览
parse_lfm.py 已升级为 v5, 在原有 LFM 解析能力基础上新增 VMT 解析和自动检测功能。支持五种工作模式:
| 模式 | 入口函数 | 说明 |
|---|---|---|
| 全量 VMT 扫描 (默认) | scan_all_vmts() |
扫描二进制文件中所有 VMT, 输出汇总报告 (交互模式按钮1) |
| LFM 扫描 | scan_lfms() |
扫描所有 TPF0/TPF1 LFM 资源, 输出窗体/事件 (交互模式按钮2) |
| 自动解析 | auto_parse_at(ea) |
自动检测指定地址处是 LFM 还是 VMT, 调用相应解析器 (交互模式按钮3) |
| 单个 LFM 解析 | parse_lfm_main(ea) |
解析指定地址或当前光标位置的 LFM, 输出组件树/事件列表 |
| 单个 VMT 解析 | parse_vmt_main(ea) |
解析指定地址或当前光标位置的 VMT, 输出字段/方法表/继承链 |
自动检测原理:
- LFM 识别: 地址处前 4 字节为
TPF0(0x54504630) 或TPF1(0x54504631) - VMT 识别:
vInstanceSize != 0且vInstanceSize + vInstanceSize2 == 0(mod 2^64) - 若地址不是数据起始, 向前查找最近的数据块 (最多 4KB), 选择距离最近者
使用方法:
- 在 IDA Pro 中打开
TOTALCMD64.EXE.i64 - File > Script File... →
setup_vmt_structs.py— 设置 VMT 结构体类型 - File > Script File... →
rename_handlers.py— 重命名方法函数 - File > Script File... →
parse_lfm.py— 弹出对话框选择模式:- VMT全扫描 (按钮1, 默认): 扫描全部 VMT
- LFM扫描 (按钮2): 扫描所有 LFM 资源
- 自动解析 (按钮3): 输入地址 (或使用当前光标位置), 自动检测 LFM/VMT 并解析
如需批处理自动化, 通过命令行参数指定模式:
idat64 -A -S"parse_lfm.py --parse 0xC855F8" TOTALCMD64.EXE.i64— 自动检测并解析 (LFM 或 VMT)idat64 -A -S"parse_lfm.py --lfm 0xC855F8" TOTALCMD64.EXE.i64— 解析指定地址 LFMidat64 -A -S"parse_lfm.py --vmt 0xC855F8" TOTALCMD64.EXE.i64— 解析指定地址 VMTidat64 -A -S"parse_lfm.py --vmt-scan" TOTALCMD64.EXE.i64— 扫描全部 VMT
也可在 IDA Python 控制台直接调用:
auto_parse_at(0xC855F8)— 自动检测并解析 (LFM 或 VMT)auto_parse_at()— 使用当前光标位置自动检测parse_vmt_main(0xC855F8)— 解析指定地址 VMTparse_lfm_main(0xC855F8)— 解析指定地址 LFMscan_all_vmts()— 扫描全部 VMTscan_lfms()— 执行 LFM 扫描
输出文件:
| 文件 | 内容 |
|---|---|
lfm_forms.txt |
全部窗体、组件、事件 (5MB) |
lfm_parse_report.txt |
单个 LFM 解析报告 (选择"自动解析"检测到 LFM / --lfm / parse_lfm_main() 后产生) |
rename_report.txt |
VMT 位置、方法地址、重命名日志 |
vmt_struct_report.txt |
VMT 结构体设置报告 (运行 setup_vmt_structs.py 后产生) |
vmt_parse_report.txt |
单个 VMT 解析报告 (选择"自动解析"检测到 VMT / --vmt / parse_vmt_main() 后产生) |
vmt_scan_report.txt |
全部 VMT 扫描汇总报告 (选择"VMT全扫描"或 --vmt-scan 后产生) |
9. VMT 地址快速参考
以下 72 个窗体类 VMT 按地址排序 (有 LFM 资源对应的窗体类)。所有符号已命名, 结构体类型已应用到 IDA。 通过 scan_all_vmts() 直接扫描法可找到全部 561 个 VMT (含非窗体基础类), 完整列表见 vmt_scan_report.txt。
| VMT 地址 | 方法表 | 类名 | 方法数 | IDA 符号 |
|---|---|---|---|---|
| 0xBE3698 | 0xBE35A8 | TNASTYNAGSCREEN | 11 | TNASTYNAGSCREEN_VMT |
| 0xBE7908 | 0xBE7760 | TATTRIBDLG | 24 | TATTRIBDLG_VMT |
| 0xBE9F20 | 0xBE9CB0 | TUSERMENUCHANGE | 36 | TUSERMENUCHANGE_VMT |
| 0xBEC7B8 | 0xBEC6E0 | TDRIVEDLG | 10 | TDRIVEDLG_VMT |
| 0xBEDA10 | 0xBED8F0 | TASSOCIATEDLG | 15 | TASSOCIATEDLG_VMT |
| 0xBEF370 | 0xBEF1E0 | TDupeSelForm | 22 | TDupeSelForm_VMT |
| 0xBF1C48 | 0xBF1BC0 | TVOLLABELDLG | 7 | TVOLLABELDLG_VMT |
| 0xBF2C48 | 0xBF2B70 | TDLGUNZIPALL | 12 | TDLGUNZIPALL_VMT |
| 0xBF4B60 | 0xBF4980 | TDLGZIP | 29 | TDLGZIP_VMT |
| 0xBF7178 | 0xBF70E0 | TDLGPACKERINFO | 8 | TDLGPACKERINFO_VMT |
| 0xBF91B0 | 0xBF90A0 | TQUICKSEARCH | 8 | TQUICKSEARCH_VMT |
| 0xBFA928 | 0xBFA810 | TSTDTREEDLG | 15 | TSTDTREEDLG_VMT |
| 0xBFC190 | 0xBFBF80 | TCHANGETREEDLG | 28 | TCHANGETREEDLG_VMT |
| 0xBFDC18 | 0xBFDB50 | TINFODLG | 10 | TINFODLG_VMT |
| 0xC01258 | 0xC01200 | TVERSIONINFODLG | 4 | TVERSIONINFODLG_VMT |
| 0xC02AA0 | 0xC027C0 | TBUTTONCHANGEDLG | 43 | TBUTTONCHANGEDLG_VMT |
| 0xC05990 | 0xC057A0 | TONEBUTTONCHANGEDLG | 28 | TONEBUTTONCHANGEDLG_VMT |
| 0xC07C00 | 0xC07B60 | TDLGUNZIP | 9 | TDLGUNZIP_VMT |
| 0xC092C0 | 0xC09150 | TProgressDialog | 19 | TProgressDialog_VMT |
| 0xC0CB10 | 0xC0C9C0 | TDLG2FILEACTIONMIN | 10 | TDLG2FILEACTIONMIN_VMT |
| 0xC0E1F8 | 0xC0E130 | TExtMsgForm | 9 | TExtMsgForm_VMT |
| 0xC10B08 | 0xC10A10 | TCOMBOINPUT | 13 | TCOMBOINPUT_VMT |
| 0xC141F0 | 0xC13440 | TDLGCONFIGALL | 214 | TDLGCONFIGALL_VMT |
| 0xC3A180 | 0xC39AE8 | TLister | 62 | TLister_VMT |
| 0xC3F9F8 | 0xC3F3C0 | TFindFile | 81 | TFindFile_VMT |
| 0xC46520 | 0xC46440 | TDriveSelBox | 9 | TDriveSelBox_VMT |
| 0xC47978 | 0xC477F0 | TLISTCONF | 22 | TLISTCONF_VMT |
| 0xC4B580 | 0xC4B3C0 | TOverWriteForm | 21 | TOverWriteForm_VMT |
| 0xC4D4F0 | 0xC4D470 | TSEARCHTEXT | 7 | TSEARCHTEXT_VMT |
| 0xC50768 | 0xC506F0 | TFireWallDlg | 6 | TFireWallDlg_VMT |
| 0xC52240 | 0xC52120 | TCheckEditBox | 15 | TCheckEditBox_VMT |
| 0xC53A30 | 0xC53970 | TNewConnDlg | 11 | TNewConnDlg_VMT |
| 0xC54EF0 | 0xC54D70 | TDETAILS | 23 | TDETAILS_VMT |
| 0xC58BF0 | 0xC58B70 | TChmodForm | 7 | TChmodForm_VMT |
| 0xC5A910 | 0xC5A760 | TEditTypeForm | 24 | TEditTypeForm_VMT |
| 0xC5C7A8 | 0xC5C700 | TEdActionForm | 9 | TEdActionForm_VMT |
| 0xC5E308 | 0xC5E210 | TChooseIcon | 12 | TChooseIcon_VMT |
| 0xC5F9D0 | 0xC5F510 | TFileCompForm | 63 | TFileCompForm_VMT |
| 0xC655F8 | 0xC65550 | TPrintConfDlg | 9 | TPrintConfDlg_VMT |
| 0xC67CB0 | 0xC67B60 | TDlgCustomColors | 17 | TDlgCustomColors_VMT |
| 0xC695E8 | 0xC69530 | Thostdefinedlg | 10 | Thostdefinedlg_VMT |
| 0xC6B588 | 0xC6B4C0 | TEncodeEditBox | 11 | TEncodeEditBox_VMT |
| 0xC6D0F0 | 0xC6CC40 | TMultiRename | 69 | TMultiRename_VMT |
| 0xC718D8 | 0xC71720 | TCONNECT | 24 | TCONNECT_VMT |
| 0xC733E0 | 0xC731F0 | TInpComboDlg | 26 | TInpComboDlg_VMT |
| 0xC76C98 | 0xC76C00 | TSplitEditBox | 8 | TSplitEditBox_VMT |
| 0xC780E8 | 0xC77F30 | TPrintDlg | 23 | TPrintDlg_VMT |
| 0xC7A1E0 | 0xC7A180 | TMarginsDlg | 5 | TMarginsDlg_VMT |
| 0xC7BF18 | 0xC7B990 | TCmpForm | 68 | TCmpForm_VMT |
| 0xC82268 | 0xC821C0 | TCmtEditForm | 9 | TCmtEditForm_VMT |
| 0xC83640 | 0xC83520 | TFsPluginConfigForm | 13 | TFsPluginConfigForm_VMT |
| 0xC855F8 | 0xC84930 | TTOTAL_CMD | 99 | TTOTAL_CMD_VMT |
| 0xC8ECB0 | 0xC8EB70 | TCustomColumnForm | 17 | TCustomColumnForm_VMT |
| 0xC90E98 | 0xC90D00 | TCmdSelForm | 22 | TCmdSelForm_VMT |
| 0xC92A20 | 0xC929B0 | TDateTimePickForm | 4 | TDateTimePickForm_VMT |
| 0xC93BF8 | 0xC93B90 | TChooseEncodingForm | 5 | TChooseEncodingForm_VMT |
| 0xC95158 | 0xC94F98 | TPathSelForm | 19 | TPathSelForm_VMT |
| 0xC969E0 | 0xC96950 | TPassForm | 8 | TPassForm_VMT |
| 0xC98BC0 | 0xC98AC0 | TShellExtForm | 13 | TShellExtForm_VMT |
| 0xC9D518 | 0xC9D480 | TCrcForm | 7 | TCrcForm_VMT |
| 0xC9F090 | 0xC9EEE0 | TPluginSelForm | 22 | TPluginSelForm_VMT |
| 0xCBD598 | 0xCBD510 | TCheckMsgBox | 7 | TCheckMsgBox_VMT |
| 0xD41570 | 0xD41530 | TDriveTip | 3 | TDriveTip_VMT |
| 0xD4BE70 | 0xD4BE30 | TPrintForm | 3 | TPrintForm_VMT |
| 0xD54200 | 0xD54140 | TICONLIBCHANGEDLG | 9 | TICONLIBCHANGEDLG_VMT |
| 0xD5D3B0 | 0xD5D2E0 | TCheckComboBox | 10 | TCheckComboBox_VMT |
| 0xD5FF38 | 0xD5FEF0 | TTabIconDialog | 3 | TTabIconDialog_VMT |
| 0xD61238 | 0xD61210 | TCryptOptions | 1 | TCryptOptions_VMT |
| 0xD62328 | 0xD622D0 | TButtonEditBox | 4 | TButtonEditBox_VMT |
| 0xD643A0 | 0xD64230 | THistoryForm | 15 | THistoryForm_VMT |
| 0xD67C98 | 0xD67C30 | TSyncStartForm | 5 | TSyncStartForm_VMT |
| 0xD69310 | 0xD69240 | TSYNCEXTINPUT | 10 | TSYNCEXTINPUT_VMT |
10. VMT 解析功能详解
parse_lfm.py v5 在原有 LFM 解析能力基础上, 新增了对 FPC 64 位 VMT (虚拟方法表) 的解析与扫描功能。VMT 结构布局详见第 1 节 "FPC 64 位 VMT 结构"。
10.1 VMT 相关常量
脚本中定义了以下 VMT 相关常量:
| 常量 | 值 | 说明 |
|---|---|---|
VMT_SIZE |
104 (13 * 8) | VMT 总字节数 (13 个 qword 字段) |
MAX_VMT_CHAIN_DEPTH |
32 | 父类继承链最大深度 (用于循环引用保护) |
MAX_METHODS_PER_VMT |
1000 | 单个 VMT 方法数量上限 (异常数据保护) |
VMT_SEARCH_BACK_RANGE |
0x1000 (4KB) | 向前查找 VMT 起始的最大范围 |
VMT_FIELDS |
13 项元组列表 | VMT 字段定义 (偏移、字段名、说明) |
VMT_REPORT_PATH |
见第 8 节 | 单个 VMT 解析报告输出路径 |
VMT_SCAN_REPORT_PATH |
见第 8 节 | 全量 VMT 扫描报告输出路径 |
10.2 可用函数
| 函数 | 用途 |
|---|---|
parse_vmt_at(vmt_addr) |
解析指定地址的 VMT, 返回字段/方法/父类链 |
parse_vmt_main(ea=None) |
VMT 解析入口, 解析给定地址或当前光标 (idc.here()) 处的 VMT |
find_vmt_for_address(ea) |
查找包含给定地址的 VMT 起始地址 (向前搜索, 最多 4KB) |
scan_all_vmts() |
扫描二进制文件中所有 VMT (直接扫描法: 8字节对齐位置 VMT 校验) |
format_vmt_report(result) |
将 VMT 解析结果格式化为可读文本报告 |
walk_vmt_chain(vmt_addr) |
遍历父类 VMT 继承链 (含循环检测, 最大深度 32) |
verify_vmt(vmt_addr) |
验证 FPC 64 位 VMT 有效性 (vInstanceSize != 0 且 vInstanceSize + vInstanceSize2 == 0) |
read_vmt_fields(vmt_addr) |
读取全部 13 个 VMT 字段 |
read_method_table_detailed(method_table_ptr) |
读取方法表, 含方法名指针与代码指针 |
10.3 函数说明
parse_vmt_at(vmt_addr)
核心解析函数。读取 VMT 全部 13 个字段、解析方法表 (含每个方法的名称指针与代码地址)、并通过 walk_vmt_chain 沿父类链 (vParentRef) 遍历继承关系。返回包含 vmt_addr、vmt_symbol、class_name、fields、methods、parent_chain 等键的字典。
parse_vmt_main(ea=None)
对外入口。若 ea 为 None, 使用 idc.here() 取当前光标位置。若 ea 不是有效 VMT 起始地址, 自动调用 find_vmt_for_address 向前查找最近的 VMT 起始。解析完成后将格式化报告写入 VMT_REPORT_PATH, 报告末尾附带 JSON-like 原始数据便于程序化处理。
find_vmt_for_address(ea)
从给定地址开始向前搜索, 在 VMT_SEARCH_BACK_RANGE (4KB) 范围内查找有效 VMT 起始地址。便于在光标位于 VMT 内部某字段时, 自动定位到 VMT 起点。
scan_all_vmts()
全量扫描策略 (直接扫描法):
- 遍历所有数据段 (SEG_DATA), 分块读取 (1MB 分块, 块尾多读 32 字节处理边界)
- 在每个块中按 8 字节对齐位置检查 VMT 校验条件:
vInstanceSize != 0(快速跳过前 4 字节全零位置)vInstanceSize + vInstanceSize2 == 0(mod 2^64, 校验字段)
- 对每个候选, 验证
vClassName(offset +24) 指针指向合法 Pascal ShortString 类名 - 类名格式校验: 以字母开头, 仅含 A-Z/a-z/0-9/_ (调用
_is_valid_lfm_identifier)
相比旧版按类名逐个搜索 (N 次全段扫描), 直接扫描法仅需 1 次全段扫描, 速度提升数百倍, 且覆盖率 100%。汇总信息输出到 IDA Output 窗口和日志文件 vmt_scan_log.txt, 完整报告写入 VMT_SCAN_REPORT_PATH。
walk_vmt_chain(vmt_addr)
沿 vParentRef 字段递归遍历父类 VMT 链。内置循环检测与最大深度限制 (MAX_VMT_CHAIN_DEPTH = 32), 防止循环引用导致无限递归。返回 (vmt_addr, class_name) 元组列表, 从当前类到根类的顺序。
verify_vmt(vmt_addr)
按 FPC 64 位 VMT 有效性规则校验: vInstanceSize != 0 且 vInstanceSize + vInstanceSize2 == 0 (mod 2^64)。返回布尔值, 用于扫描时过滤候选位置。
read_vmt_fields(vmt_addr)
按 VMT_FIELDS 列表读取 13 个字段, 返回字段名到值的字典。
read_method_table_detailed(method_table_ptr)
读取 vMethodTable 指向的方法表。首 4 字节为 count, 随后是 count 个 16 字节条目, 每条目包含 name_ptr (方法名 Pascal ShortString 指针) 与 code_ptr (方法代码地址)。count 超过 MAX_METHODS_PER_VMT 时会被截断以防异常数据。
format_vmt_report(result)
将 parse_vmt_at 的返回值格式化为人类可读的文本报告, 含字段表、方法列表、父类继承链。
10.4 调用方式
parse_lfm.py 支持三种调用方式, 覆盖交互使用、批处理自动化和直接函数调用:
方式一: 交互模式 (推荐, IDA Pro GUI 中使用)
在 IDA Pro 中通过 Alt+F7 或 File > Script File 运行 parse_lfm.py, 脚本会弹出三按钮对话框:
┌─────────────────────────────────────────────────┐
│ 请选择操作模式: │
│ │
│ VMT全扫描 - 扫描二进制中所有 VMT (按钮1) │
│ LFM扫描 - 扫描所有 TPF0 LFM 窗体资源 (按钮2) │
│ 自动解析 - 自动检测当前光标处的 LFM/VMT (按钮3) │
│ │
│ [VMT全扫描] [LFM扫描] [自动解析] │
└─────────────────────────────────────────────────┘
- 选择 VMT全扫描 (按钮1, 默认): 执行
scan_all_vmts(), 扫描所有 VMT - 选择 LFM扫描 (按钮2): 执行
scan_lfms(), 扫描所有 LFM 资源 - 选择 自动解析 (按钮3): 进一步弹出地址输入框 (默认填入当前光标位置), 输入或确认后自动检测该地址处是 LFM 还是 VMT 并调用相应解析器; 若地址不是数据起始, 自动向前查找最近的数据块
- 按 Escape 或关闭对话框: 等同于选择"自动解析" (IDA 9.x
ask_buttons的 btn3 与 Escape 返回值相同)
方式二: 批处理模式 (自动化场景)
通过 idat64 -S 传递命令行参数, 适用于无人值守的批处理任务:
# 自动检测并解析 (推荐, 自动判断 LFM 或 VMT)
idat64 -A -S"parse_lfm.py --parse 0xC855F8" TOTALCMD64.EXE.i64
# 解析指定地址的 LFM
idat64 -A -S"parse_lfm.py --lfm 0xC855F8" TOTALCMD64.EXE.i64
# 解析指定地址的 VMT (十六进制地址)
idat64 -A -S"parse_lfm.py --vmt 0xC855F8" TOTALCMD64.EXE.i64
# 扫描二进制文件中所有 VMT
idat64 -A -S"parse_lfm.py --vmt-scan" TOTALCMD64.EXE.i64
# 默认模式: 执行完整 LFM 扫描
idat64 -A -S"parse_lfm.py" TOTALCMD64.EXE.i64
方式三: Python 控制台直接调用
在 IDA Python 控制台中直接调用函数, 灵活指定参数:
import parse_lfm
parse_lfm.auto_parse_at(0xC855F8) # 自动检测并解析 (LFM 或 VMT)
parse_lfm.auto_parse_at() # 使用当前光标位置自动检测
parse_lfm.parse_lfm_main(0xC855F8) # 解析指定地址 LFM
parse_lfm.parse_vmt_main(0xC855F8) # 解析指定地址 VMT
parse_lfm.scan_all_vmts() # 扫描全部 VMT
parse_lfm.find_vmt_for_address(0xC85620) # 查找包含此地址的 VMT 起点
parse_lfm.scan_lfms() # 扫描所有 LFM
10.5 输出文件说明
| 文件 | 触发方式 | 内容 |
|---|---|---|
vmt_parse_report.txt |
"自动解析"检测到 VMT / --vmt / --parse / parse_vmt_main() |
单个 VMT 解析报告: 字段表、方法列表 (含名称/代码地址/符号)、父类继承链, 末尾附 JSON-like 原始数据 |
lfm_parse_report.txt |
"自动解析"检测到 LFM / --lfm / --parse / parse_lfm_main() |
单个 LFM 解析报告: 窗体类名、属性列表、事件处理器列表、递归组件树 |
vmt_scan_report.txt |
"VMT全扫描" / --vmt-scan / scan_all_vmts() |
全部 VMT 扫描汇总报告: 扫描段统计、每个 VMT 的地址/类名/方法数/父类链、汇总信息 |
lfm_forms.txt |
"LFM扫描" / scan_lfms() |
所有 LFM 窗体的组件树和事件处理器列表 |
输出文件默认写入 IDB 所在目录 (与 .i64 文件同目录)。
报告结束。基于 IDA Pro 对 TOTALCMD64.EXE 的分析生成。

浙公网安备 33010602011771号