深入剖析 api-ms-* 系列动态链接库
字数 1342 2025-08-24 20:49:31

深入剖析 api-ms-* 系列动态链接库

概述

api-ms-* 系列动态链接库是Windows操作系统中一种特殊的DLL文件,它们实际上并不包含实际的函数实现,而是作为API转发器存在。这些DLL文件的主要作用是为Windows API提供版本控制和兼容性支持。

核心机制

API转发机制

api-ms-* DLL并不包含实际代码,而是将API调用转发到其他系统DLL中。这种设计允许微软在不破坏向后兼容性的情况下更新API实现。

ApiSetSchema.dll 的作用

所有api-ms-* DLL的转发关系都记录在ApiSetSchema.dll中,该文件位于System32目录下。它包含了一个完整的转发关系索引表,即使某些api-ms-* DLL文件不存在于磁盘上,系统也能通过这个索引表找到正确的转发目标。

内核实现细节

初始化过程

  1. 内核在初始化阶段使用nt!MiInitializeApiSets函数
  2. 通过nt!PsLoadedModuleList函数在ldr双链表中搜索ApiSetSchema.dll
  3. 获取其访问控制权后,将其视为PE结构解析
  4. 提取.apiset节区数据并映射到内核空间
  5. 映射完成后卸载ApiSetSchema模块

PEB中的ApiSetMap

在PEB(进程环境块)偏移0x68处存在一个ApiSetMap结构体指针,它指向ApiSetSchema.dll.apiset节区的数据。

数据结构解析

主要结构体

typedef struct _API_SET_NAMESPACE {
    uint32_t Version;
    uint32_t Size;
    uint32_t Flags;
    uint32_t Count;         // API_SET_NAMESPACE_ENTRY数组数量
    uint32_t EntryOffset;   // API_SET_NAMESPACE_ENTRY入口偏移
    uint32_t HashOffset;    // API_SET_HASH_ENTRY入口偏移
    uint32_t HashFactor;
} API_SET_NAMESPACE, *PAPI_SET_NAMESPACE;

typedef struct _API_SET_HASH_ENTRY {
    uint32_t Hash;
    uint32_t Index;
} API_SET_HASH_ENTRY, *PAPI_SET_HASH_ENTRY;

typedef struct _API_SET_NAMESPACE_ENTRY {
    uint32_t Flags;
    uint32_t NameOffset;    // 原始DLL名偏移
    uint32_t NameLength;    // 原始DLL名长度
    uint32_t HashedLength;
    uint32_t ValueOffset;   // API_SET_VALUE_ENTRY入口偏移
    uint32_t ValueCount;    // 转发目标DLL数量
} API_SET_NAMESPACE_ENTRY, *PAPI_SET_NAMESPACE_ENTRY;

typedef struct _API_SET_VALUE_ENTRY {
    uint32_t Flags;
    uint32_t NameOffset;
    uint32_t NameLength;
    uint32_t ValueOffset;   // 转发目标DLL名偏移
    uint32_t ValueLength;   // 转发目标DLL名长度
} API_SET_VALUE_ENTRY, *PAPI_SET_VALUE_ENTRY;

解析流程

  1. 首先解析API_SET_NAMESPACE结构体
  2. 根据EntryOffset定位到API_SET_NAMESPACE_ENTRY数组
  3. 遍历数组,通过NameOffset获取原始DLL名
  4. 通过ValueOffset定位到API_SET_VALUE_ENTRY结构体
  5. 通过ValueOffset获取转发目标DLL名

实践解析方法

使用010 Editor模板解析

提供了一个010 Editor模板,可以直观地解析.apiset节区数据:

// 模板代码见原文

使用步骤:

  1. ApiSetSchema.dll中提取.apiset节区数据
  2. 在010 Editor中应用模板
  3. 查看解析结果

使用C++代码解析

完整C++解析代码示例:

#include <LIEF/LIEF.hpp>
#include <Windows.h>

// 结构体定义见上文

int main() {
    // 1. 加载ApiSetSchema.dll
    std::string path = "apisetschema.dll";
    std::unique_ptr<LIEF::PE::Binary> bin = LIEF::PE::Parser::parse(path);
    
    // 2. 获取.apiset节区数据
    LIEF::PE::Section sec = bin->get_section(".apiset");
    std::vector<uint8_t> sec_data = sec.content();
    
    // 3. 解析API_SET_NAMESPACE结构
    PAPI_SET_NAMESPACE pnamespace = (PAPI_SET_NAMESPACE)(sec_data.data());
    UINT_PTR namespace_addr = (UINT_PTR)pnamespace;
    PAPI_SET_NAMESPACE_ENTRY pnamespace_entry = 
        (PAPI_SET_NAMESPACE_ENTRY)(namespace_addr + pnamespace->EntryOffset);
    
    // 4. 遍历所有条目并打印转发关系
    UNICODE_STRING origin_name, forward_name;
    for (uint32_t i = 0; i < pnamespace->Count; i++) {
        origin_name.Buffer = (wchar_t*)(namespace_addr + pnamespace_entry->NameOffset);
        origin_name.Length = pnamespace_entry->NameLength;
        origin_name.MaximumLength = pnamespace_entry->NameLength;
        printf("%wZ.dll -> ", &origin_name);
        
        PAPI_SET_VALUE_ENTRY pvalue_entry = 
            (PAPI_SET_VALUE_ENTRY)(namespace_addr + pnamespace_entry->ValueOffset);
        
        for (uint32_t j = 0; j < pnamespace_entry->ValueCount; j++) {
            forward_name.Buffer = (wchar_t*)(namespace_addr + pvalue_entry->ValueOffset);
            forward_name.Length = pvalue_entry->ValueLength;
            forward_name.MaximumLength = pvalue_entry->ValueLength;
            printf("%wZ", &forward_name);
            
            if ((j + 1) != pnamespace_entry->ValueCount) {
                printf(", ");
            }
            
            if (pvalue_entry->NameLength != 0) {
                origin_name.Buffer = (wchar_t*)(namespace_addr + pvalue_entry->NameOffset);
                origin_name.Length = pvalue_entry->NameLength;
                origin_name.MaximumLength = pvalue_entry->NameLength;
                printf(" [%wZ]", &origin_name);
            }
            pvalue_entry++;
        }
        printf("\n");
        pnamespace_entry++;
    }
    return 0;
}

注意事项

  1. 字符串使用UNICODE_STRING结构而非普通的宽字符字符串,因为DLL名称不以\0结尾
  2. 一个api-ms-* DLL可能转发到多个目标DLL,由ValueCount字段指定
  3. 哈希表(API_SET_HASH_ENTRY)用于系统快速检索转发关系

实际应用场景

  1. 逆向分析:理解API调用链,追踪函数实际实现位置
  2. 兼容性研究:分析不同Windows版本间的API变化
  3. 安全研究:检测API调用劫持或挂钩
  4. 软件开发:解决DLL依赖问题,理解Windows API版本控制机制

参考资源

  1. Runtime DLL name resolution: ApiSetSchema (Part I)
  2. Runtime DLL name resolution: ApiSetSchema (Part II)
深入剖析 api-ms-* 系列动态链接库 概述 api-ms-* 系列动态链接库是Windows操作系统中一种特殊的DLL文件,它们实际上并不包含实际的函数实现,而是作为API转发器存在。这些DLL文件的主要作用是为Windows API提供版本控制和兼容性支持。 核心机制 API转发机制 api-ms-* DLL并不包含实际代码,而是将API调用转发到其他系统DLL中。这种设计允许微软在不破坏向后兼容性的情况下更新API实现。 ApiSetSchema.dll 的作用 所有api-ms-* DLL的转发关系都记录在 ApiSetSchema.dll 中,该文件位于 System32 目录下。它包含了一个完整的转发关系索引表,即使某些api-ms-* DLL文件不存在于磁盘上,系统也能通过这个索引表找到正确的转发目标。 内核实现细节 初始化过程 内核在初始化阶段使用 nt!MiInitializeApiSets 函数 通过 nt!PsLoadedModuleList 函数在ldr双链表中搜索 ApiSetSchema.dll 获取其访问控制权后,将其视为PE结构解析 提取 .apiset 节区数据并映射到内核空间 映射完成后卸载 ApiSetSchema 模块 PEB中的ApiSetMap 在PEB(进程环境块)偏移0x68处存在一个 ApiSetMap 结构体指针,它指向 ApiSetSchema.dll 中 .apiset 节区的数据。 数据结构解析 主要结构体 解析流程 首先解析 API_SET_NAMESPACE 结构体 根据 EntryOffset 定位到 API_SET_NAMESPACE_ENTRY 数组 遍历数组,通过 NameOffset 获取原始DLL名 通过 ValueOffset 定位到 API_SET_VALUE_ENTRY 结构体 通过 ValueOffset 获取转发目标DLL名 实践解析方法 使用010 Editor模板解析 提供了一个010 Editor模板,可以直观地解析 .apiset 节区数据: 使用步骤: 从 ApiSetSchema.dll 中提取 .apiset 节区数据 在010 Editor中应用模板 查看解析结果 使用C++代码解析 完整C++解析代码示例: 注意事项 字符串使用 UNICODE_STRING 结构而非普通的宽字符字符串,因为DLL名称不以 \0 结尾 一个api-ms-* DLL可能转发到多个目标DLL,由 ValueCount 字段指定 哈希表( API_SET_HASH_ENTRY )用于系统快速检索转发关系 实际应用场景 逆向分析 :理解API调用链,追踪函数实际实现位置 兼容性研究 :分析不同Windows版本间的API变化 安全研究 :检测API调用劫持或挂钩 软件开发 :解决DLL依赖问题,理解Windows API版本控制机制 参考资源 Runtime DLL name resolution: ApiSetSchema (Part I) Runtime DLL name resolution: ApiSetSchema (Part II)