首页
社区
课程
招聘
[原创]Internet Explorer 扩展注册项与 COM 实现解析
发表于: 18小时前 459

[原创]Internet Explorer 扩展注册项与 COM 实现解析

18小时前
459

Internet Explorer 扩展注册项与 COM 实现解析

文章所述全部在KswordARK中实现,项目地址ebfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6w2f1%4N6G2M7X3c8p5c8g2k6Q4x3V1k6w2f1%4N6G2M7X3b7`.
图片描述

要得到 Internet Explorer(IE)扩展注册项及其关联 COM(Component Object Model,组件对象模型)服务器的静态记录,分为四步:

  1. 确定扩展来源、注册表根和 32 位或 64 位视图。
  2. 按值型和子键型结构读取扩展记录的原始数据。
  3. 验证 CLSID(Class Identifier,COM 类标识符),并保留它来自哪个字段和注册表位置。
  4. 查询 COM Classes 注册区,记录 InprocServer32LocalServer32 的服务器文本。

注册表是 Windows 保存系统和用户配置的分层数据库。COM Classes 注册区保存类标识符与创建该类所需服务器之间的映射。InprocServer32 表示进程内 DLL 服务器,LocalServer32 表示可单独启动的本地 EXE 服务器。

IE 扩展来源键 -> 值名、值数据或子键名 -> 有效 CLSID -> HKCU 或 HKLM Classes 注册 -> InprocServer32LocalServer32 默认值

HKCUHKEY_CURRENT_USER,保存当前登录用户的配置。HKLMHKEY_LOCAL_MACHINE,保存整台计算机的配置。两个范围都可能提供同一个 CLSID 的注册信息。

这四步得到的是注册表在读取时的静态关系。注册项存在、服务器文件可见和浏览器进程实际加载扩展是不同层次的事实,最后需要分别表述。

一、第一步:确定扩展来源、对象和读取范围

第一步要先确定 IE 从哪里发现扩展,以及每个注册表位置代表什么。COM 是 Windows 用来标识和创建可复用组件的一套规则。IE 历史上提供 BHO(Browser Helper Object,浏览器帮助对象)、Explorer Bar(浏览器停靠栏)、Toolbar(浏览器工具栏)和 URLSearchHook(地址栏搜索处理对象)等扩展点。浏览器先从对应注册表来源取得 CLSID,再通过 COM Classes 注册定位 DLL 或本地 EXE 服务器。

IE 扩展来源键 -> 值名、值数据或子键名提供 CLSID -> 查询 HKCU 或 HKLM Classes 注册 -> InprocServer32 或 LocalServer32 提供服务器文本 -> COM 激活扩展对象

1.1 IE 的扩展点通过注册表发现 COM 类

浏览器不会直接从扩展路径列表加载对象。IE 读取对应注册表位置,得到 CLSID,再通过 COM 激活该 CLSID 对应的对象。

CLSID 是用花括号包围的 GUID(Globally Unique Identifier,全局唯一标识符),例如 {01234567-89AB-CDEF-0123-456789ABCDEF}。它用于标识 COM 类,不能凭自身说明扩展名称、服务器路径或浏览器是否已加载它。注册项中出现 CLSID 后,还需要查询 Classes 注册根下的服务器键,才能找到实现对象的 DLL 或本地服务器命令文本。

IE 扩展来源键 -> CLSID -> InprocServer32(进程内 DLL)或 LocalServer32(本地 EXE 服务器)

1.2 BHO、Explorer Bar、Toolbar 和 URLSearchHook 的职责不同

扩展种类决定注册入口和接口要求。BHO 是可在浏览器进程中参与导航和文档事件的对象。Explorer Bar 通常提供停靠式侧边栏用户界面。Toolbar 关联浏览器工具栏。URLSearchHook 可参与地址栏输入的搜索处理。服务器定位步骤都遵循 COM 注册规则。

一个 CLSID 可能出现在多个来源键中。这种情况表示同一个 COM 类被不同扩展点引用,记录时应保留每次出现的来源路径、值名或子键名,而不是按 CLSID 折叠成唯一记录。服务器注册同样可能同时存在用户级和机器级、32 位和 64 位不同视图。

1.3 历史扩展配置需要保留版本和宿主边界

IE 扩展机制需要保留历史与宿主边界。现代 Windows 版本上的 IE 可用性、IE 模式和实际宿主进程取决于系统组件、策略和安装状态。注册表中保留的扩展项只证明配置存在,不能单独证明某个浏览器进程当前会调用它。

读取结果可明确写出“扩展来源”“CLSID 注册状态”“服务器文本”和“读取视图”。是否被加载、是否实现目标接口、是否在某个浏览器版本中启用,需要由实际 COM 激活、进程模块或浏览器运行时证据进一步确认。

1.4 用户级和机器级 Classes 数据要保留独立来源

HKEY_CLASSES_ROOT 提供合并后的类注册视图,底层可来自 HKCU\Software\ClassesHKLM\Software\Classes。为了看清原始配置,应分别查询这两个根。

InprocServer32 的默认值通常是 DLL 路径,DLL 需要与加载它的浏览器进程架构匹配。LocalServer32 的默认值通常是可带参数的 EXE 命令文本。读取默认值时保存其原文,不用第一个空格强行截断路径,也不把服务器文本替换为文件签名结论。

1.5 浏览器进程、扩展对象与网页内容处于不同边界

浏览器宿主和扩展对象需要分开。IE 扩展的 COM 服务器可能由浏览器进程在进程内加载,也可能由系统单独启动为本地服务器。BHO 等进程内组件与浏览器共享同一地址空间、线程和崩溃范围。网页 URL、扩展 CLSID 和 DLL 文件路径属于不同身份字段。

网页内容属于浏览器呈现和安全区域处理的对象,COM 扩展注册属于 Windows 类注册对象,DLL 或 EXE 属于文件系统对象。调查时若只得到 CLSID,最多说明存在一个注册的类身份。若要解释页面行为,还需要独立的浏览器进程、导航事件、加载模块或网络记录证据。

完成第一步后,已经有了扩展类型、用户级或机器级范围,以及 32 位和 64 位视图这三类读取边界。接下来要按来源键的真实结构读取原始数据,才能避免把值名、值数据和子键名混成同一种线索。

二、第二步:按值型和子键型结构读取扩展记录

第二步要按来源键保存记录,不能只留下看似有用的字符串。URLSearchHooksToolbar 要枚举键中的所有值。CLSID 可能是值名,值数据可能是显示名称、空字符串、二进制标志或另一个 CLSID,值名和值数据不能混为同一字段。

Browser Helper ObjectsExplorer Bars 要枚举一级子键。子键名经常是 CLSID,默认值可提供显示名称。默认值缺失不影响通过子键名定位 COM 注册。

读取时分别覆盖 HKLMHKCU 的 64 位与 32 位视图。64 位进程的默认注册表视图只覆盖一侧,显式使用 KEY_WOW64_64KEYKEY_WOW64_32KEY 才能取得两份配置。

// 意义:打开注册表子键。
// 返回:ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。
//        成功时 phkResult 接收 HKEY,调用方必须关闭它。
LSTATUS RegOpenKeyExW(
    HKEY hKey, LPCWSTR lpSubKey, DWORD ulOptions,
    REGSAM samDesired, PHKEY phkResult
);

// 意义:关闭打开成功的键句柄。
// 返回:ERROR_SUCCESS 表示成功。
LSTATUS RegCloseKey(HKEY hKey);
HKEY key64 = nullptr;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0,
              KEY_QUERY_VALUE | KEY_WOW64_64KEY, &key64);

HKEY key32 = nullptr;
RegOpenKeyExW(HKEY_LOCAL_MACHINE, sourcePath, 0,
              KEY_QUERY_VALUE | KEY_WOW64_32KEY, &key32);

2.1 读取值型扩展来源

值枚举先取得最大值名和数据长度,再按各自单位分配缓冲区。值名长度是 UTF-16 字符数,值数据长度是字节数。注册表并发变化时 ERROR_MORE_DATA 表示其中一侧容量需要扩大。

// 意义:取得键中的值数、最大值名长度和最大数据长度。
// 返回:ERROR_SUCCESS 表示成功。长度适合作为本次枚举的初始容量。
LSTATUS RegQueryInfoKeyW(
    HKEY hKey, LPWSTR lpClass, LPDWORD lpcchClass, LPDWORD lpReserved,
    LPDWORD lpcSubKeys, LPDWORD lpcbMaxSubKeyLen, LPDWORD lpcbMaxClassLen,
    LPDWORD lpcValues, LPDWORD lpcbMaxValueNameLen, LPDWORD lpcbMaxValueLen,
    LPDWORD lpcbSecurityDescriptor, PFILETIME lpftLastWriteTime
);

// 意义:按索引读取一个值名、类型和原始数据。
// 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示结束。
//        ERROR_MORE_DATA 表示名称或数据缓冲区不足。
LSTATUS RegEnumValueW(
    HKEY hKey, DWORD dwIndex, LPWSTR lpValueName, LPDWORD lpcchValueName,
    LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData
);
std::vector<wchar_t> valueName(maxValueNameChars + 1, L'\0');
std::vector<BYTE> valueData(maxValueDataBytes);
DWORD nameChars = static_cast<DWORD>(valueName.size());
DWORD dataBytes = static_cast<DWORD>(valueData.size());
DWORD type = REG_NONE;

LSTATUS status = RegEnumValueW(key, index, valueName.data(), &nameChars,
                                nullptr, &type,
                                valueData.empty() ? nullptr : valueData.data(), &dataBytes);

值数据只在类型为 REG_SZREG_EXPAND_SZ 且字节数满足 UTF-16 对齐时转换为文本。Toolbar 中的 REG_BINARY 值仍作为完整记录输出类型和字节数,不能按字符串读取。

// 错误示例:只读取字符串类型,二进制类型的 CLSID 值名会被遗漏。
if (type != REG_SZ) continue;

2.2 读取 BHO 与 Explorer Bar 子键

子键枚举使用 RegEnumKeyExW。某些扩展键只授予子键枚举权限,名称缓冲区从合理初始值开始并在 ERROR_MORE_DATA 时扩容,能避免依赖不必要的附加查询权限。

// 意义:按索引读取一级子键名。
// 返回:ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示结束。
//        ERROR_MORE_DATA 表示 lpName 的字符容量不足。
LSTATUS RegEnumKeyExW(
    HKEY hKey, DWORD dwIndex, LPWSTR lpName, LPDWORD lpcchName,
    LPDWORD lpReserved, LPWSTR lpClass, LPDWORD lpcchClass,
    PFILETIME lpftLastWriteTime
);

默认值通过空值名读取。默认值存在时保存类型和原始文本。默认值缺失时保留子键 CLSID 与“默认值未设置”状态。

DWORD type = REG_NONE;
DWORD bytes = 0;
LSTATUS status = RegQueryValueExW(childKey, L"", nullptr, &type, nullptr, &bytes);
if (status == ERROR_FILE_NOT_FOUND) {
    // 子键存在,默认值未设置。
}

2.3 区分资源、权限和长度单位

这一阶段还要把资源所有权、访问权限和长度单位分开处理。HKEY 是已打开注册表键的资源引用,RegOpenKeyExW 成功后由得到它的一方调用 RegCloseKeyREGSAM 是访问掩码,决定这次句柄允许查询值还是枚举子键。DWORD 是 32 位无符号整数,经常承载长度或访问位。

RegEnumValueWlpcchValueName 以 UTF-16 宽字符数表示名称缓冲区容量和实际长度,lpcbData 以字节表示数据缓冲区容量和实际长度。UTF-16 是 Windows 宽字符 API 使用的文本编码。返回的文本数据未必包含 NUL 终止符,因此要按 API 返回的确切长度构造字符串。只有 REG_SZREG_EXPAND_SZ 且数据字节数可被 sizeof(wchar_t) 整除时,才把原始字节解码为文本。

完成第二步后,已经取得每条扩展记录的来源根、路径、注册表视图、值名或子键名、类型和原始数据。接下来要从这些独立字段中确认 CLSID,防止任意显示文本进入 COM Classes 查询。

三、第三步:验证 CLSID 并保留来源线索

3.1 按完整 GUID 结构验证 CLSID

第三步要确认候选文本确实是 COM 类标识符。CLSID 使用 {8-4-4-4-12} 结构,共 38 个字符。两侧是花括号,四个连字符处于固定位置,余下位置只能出现十六进制字符。值名或子键名满足该结构时可以作为 CLSID。字符串值数据恰好满足该结构时也可以作为 CLSID。其它文本继续作为显示数据保存。

// 作用:判断一个 UTF-16 字符是否属于 GUID 可用的十六进制字符。
// 返回:true 表示 c 是 0-9、a-f 或 A-F。
bool IsHex(const wchar_t c)
{
    return (c >= L'0' && c <= L'9') ||
           (c >= L'a' && c <= L'f') ||
           (c >= L'A' && c <= L'F');
}

// 作用:验证 text 是否完整符合带花括号的 CLSID 文本格式。
// 输入:text 是带明确长度的 UTF-16 文本,不要求以 NUL 结尾。
// 返回:true 表示文本可作为 CLSID 候选,false 表示只能保留为原始显示数据。
bool IsClsidText(std::wstring_view text)
{
    if (text.size() != 38 || text.front() != L'{' || text.back() != L'}' ||
        text[9] != L'-' || text[14] != L'-' || text[19] != L'-' || text[24] != L'-') {
        return false;
    }

    for (std::size_t index = 1; index < 37; ++index) {
        if (index != 9 && index != 14 && index != 19 && index != 24 && !IsHex(text[index])) {
            return false;
        }
    }
    return true;
}

3.2 为有效 CLSID 保留来源字段

来源记录不能在格式验证后丢失。每个有效 CLSID 都要同时保存扩展类型、HKCUHKLM 根、32 位或 64 位视图、来源路径,以及它来自值名、值数据还是子键名。相同 CLSID 出现在多个来源时,代表多个独立引用,不能把它们合并成一条无来源的记录。

完成第三步后,已经得到可追溯的 CLSID 候选,且每个候选仍能回到具体扩展记录。接下来查询 COM Classes 注册,才能把类标识符连接到进程内 DLL 或本地 EXE 服务器文本。

四、第四步:定位 COM 服务器并区分证据范围

4.1 在对应 Classes 根和视图中查询服务器键

第四步要按同一注册表视图查询 CLSID 的 COM Classes 注册。服务器定位分别查询 HKCU\Software\Classes\CLSID\{CLSID}HKLM\Software\Classes\CLSID\{CLSID}。每个根都在当前 32 位或 64 位视图下读取 InprocServer32LocalServer32 的默认值,找到一个服务器后仍要继续读取另一种服务器类型。

const std::wstring base = L"Software\\Classes\\CLSID\\" + clsid;
const std::wstring inproc = base + L"\\InprocServer32";
const std::wstring local = base + L"\\LocalServer32";

// 对 HKCU 与 HKLM 分别以 KEY_WOW64_64KEY、KEY_WOW64_32KEY 查询两个子键的默认值。

读取默认值时使用 RegQueryValueExW。这个函数先用空数据指针取得所需字节数,再使用同一个值名读回数据。读取期间注册表内容可能变化,第二次调用返回 ERROR_MORE_DATA 时要按新的字节数扩大缓冲区后重试。

// 作用:读取已打开键中一个值的类型和数据。
// 返回:ERROR_SUCCESS 表示成功,ERROR_FILE_NOT_FOUND 表示指定值不存在,
//       ERROR_MORE_DATA 表示数据缓冲区容量不足。
// hKey:输入,已打开的键,函数不取得所有权,也不关闭它。
// lpValueName:输入,NUL 结尾 UTF-16 值名。L"" 表示默认值,可为 nullptr 表示默认值。
// lpReserved:输入,必须为 nullptr。
// lpType:输出,可为 nullptr。指向 DWORD,接收 REG_SZ、REG_EXPAND_SZ 等值类型。
// lpData:输出缓冲区,可为 nullptr。nullptr 时只查询所需长度。
// lpcbData:输入和输出,指向字节数。输入为 lpData 容量,输出为实际数据字节数。
LSTATUS RegQueryValueExW(
    HKEY hKey, LPCWSTR lpValueName, LPDWORD lpReserved,
    LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData
);

4.2 按注册、文件和运行时三个层次输出结论

InprocServer32 记录进程内服务器文本,通常指向由浏览器进程加载的 DLL。LocalServer32 记录本地服务器命令文本,通常指向可单独启动的 EXE。它们属于 COM 注册信息,原始文本应独立保存。默认值文本不能直接证明文件存在、文件具有有效签名,或 IE 已经加载了这个 COM 类。

输出结论要保持证据范围。Classes 键存在说明 COM 注册数据在读取时存在。服务器默认值能够解码说明得到了候选路径或命令文本。候选文件存在、PE 架构匹配和签名验证成功属于文件层面的结论。浏览器是否在某次会话中加载扩展、组件是否成功创建、页面是否调用接口,需要进程模块、COM 激活或浏览器运行时记录支持。

完整可运行程序在附件
3f1K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4j5h3&6Y4N6$3g2A6j5$3#2Q4x3X3g2D9j5h3&6*7L8%4g2#2i4K6u0W2j5$3!0E0i4K6u0r3K9g2p5&6c8g2p5K6P5e0j5H3z5r3#2B7
图片描述


[招生]科锐逆向工程师培训(2026年7月3日实地,远程教学同时开班, 第56期)!

上传的附件:
收藏
免费 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回