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

最近api-ms-*系列的动态链接引起了我的兴趣,起因是我在 kernel32.dll 的导入表中发现了大量api-ms-*依赖。

带着好奇心在磁盘中找到一个api-ms-win-core-rtlsupport-l1-1-0.dll,将其拖入到 010 中解析,奇怪的是通用的 EXE 模板居然报错了,没法子只有用 IDA 看看,在 IDA 中神奇的发现每个导出表的实际内容是字符串。看到这个我幡然醒悟,原来api-ms-*这个系列的 DLL 只是起转发作用的啊。

本以为这事到这里就结束了,结果我发现了一个根本无法用上述理论解释的现象。在 kernel32.dll 导入表中的某些 DLL 不存在于磁盘上,比如 api-ms-onecoreuap-settingsync-status-l1-1-0.dll,再比如 api-ms-win-appmodel-identity-l1-2-0.dll。我上面说了api-ms-*是起转发作用的,但磁盘上要是不存在某个 DLL,那谁又能知道它转发到哪里去了呢

带着这个问题遨游在搜索引擎数小时后,发现 Quarkslab 这位大佬在 2012 就已经研究过这个问题了。将他两篇文章融会贯通后,我得出了以下结论:

api-ms-* 系列的 DLL 确实起的是转发作用,但其本身可以不存在于操作系统,因为在 System32 目录下存在一个名为 ApiSetSchema.dll 的 DLL,它记录了所有api-ms-*系列动态库转发到的目的 DLL。简言之就是 ApiSetSchema.dll 为 api-ms-* 系类的 DLL 名和转发后的 DLL 名建立了一个索引表。

部分内核机制

按大佬的说法,内核初始化阶段使用 nt!MiInitializeApiSets 中的 nt!PsLoadedModuleList 函数在 ldr 双链表里搜索名为 ApiSetSchema.dll 的模块并获取其访问控制权。随后将其视为 PE 结构解析,获取.apiset的节区并将其映射到内核空间,映射完成后卸载掉 ApiSetSchema 模块。最后解析 .apiset 节区的数据即可获得完整的 DLL 转发关系。

本文的核心在于如何解析.apiset这个节区的数据,内核初始化过程这部分我就不作验证了。

除此之外,在 PEB 偏移 0x68 的位置存在一个 ApiSetMap 的结构体,这个结构体存放的是 ApiSetSchema.dll 中 .apiset 节区的所有数据。验证过程如下,首先用 IDA 打开 C:\Windows\System32\apisetschema.dll,并定位到 .apiset 节区:

然后 Windbg 附加到 notepad.exe ,使用命令!peb查看 Peb 的位置在310ab4d000

再使用命令dt nt!_peb 310ab4d000将 Peb 的完整结构打印出来:

在偏移 +0x68 处找到 ApiSetMap ,其地址为22ec26c0000,使用命令dd 22ec26c0000查看数据,发现和 IDA 中查看的数据是一样的:

ApiSetSchema解析

根据上面的描述,现在有两种方式来获取整个操作系统api-ms-*系列 DLL 的转发表。一是在自己进程的 Peb+0x68 处获取到.apiset的节数据,二是解析 System32 目录下的 ApiSetSchema.dll 动态库。我这里以方法二来演示,因为文件解析是可以跨平台的。

在解析 .apiset 的节区前需要对这个节区的数据结构有所了解。可惜的是微软没给出文档,就只有自己找找有没有前辈已经做了这个工作的。短短的几小时后,发现了在 lucasg 大佬的文章中提及了 .apiset 对应的数据结构。

010模板解析

耐心看完lucasg大佬的文章,发现他也引用了 Quarkslab 大佬的文章,真是前人栽树,后人乘凉啊。并且大佬还给出了一个 010 的解析模板,模板代码如下:

// Api namespace header
typedef struct {
    ULONG Version;
    ULONG Size <format=hex>;
    ULONG Flags;
    ULONG Count <format=hex>;
    ULONG EntryOffset <format=hex>;
    ULONG HashOffset <format=hex>;
    ULONG HashFactor <format=hex>;
} API_SET_NAMESPACE;

typedef struct {
    ULONG Hash;
    ULONG Index;
} API_SET_HASH_ENTRY;

typedef struct {
    ULONG Flags;
    ULONG NameOffset;
    ULONG NameLength;
    ULONG HashedLength;
    ULONG ValueOffset;
    ULONG ValueCount;
} API_SET_NAMESPACE_ENTRY;

typedef struct {
    ULONG Flags;
    ULONG NameOffset;
    ULONG NameLength;
    ULONG ValueOffset;
    ULONG ValueLength;
} API_SET_VALUE_ENTRY;


typedef struct {
    for (i=0; i<ApiSetMap.Count; i++) {
        API_SET_NAMESPACE_ENTRY Entry;
    }
} API_SET_NAMESPACE_ENTRIES;

typedef struct {
    for (i=0; i<ApiSetMap.Count; i++) {
        API_SET_HASH_ENTRY HashEntry;
    }
} API_SET_NAMESPACE_HASH_ENTRIES;

typedef struct (API_SET_VALUE_ENTRY &ValueEntry) {
    local int StartAddress = FTell();
    local int ValueLength = ValueEntry.ValueLength;

    CHAR Value[ValueEntry.ValueLength + 2];
} API_SET_ENTRY_VALUE;

typedef struct (API_SET_NAMESPACE_ENTRY& Entry) {
    local int ValueCount = Entry.ValueCount;
    local int NameLength = Entry.NameLength;
    local int StartAddress = FTell();
    local int HasValues = false;

    CHAR Name[Entry.NameLength + 2];

    FSeek(StartAddr + Entry.ValueOffset);

    API_SET_VALUE_ENTRY ValueEntry;
    FSeek(StartAddr + ValueEntry.ValueOffset);
    if (ValueEntry.ValueOffset)
    {
        HasValues = true;
        for (j = 0; j < Entry.ValueCount;j++)
        {
            API_SET_ENTRY_VALUE ValueName(ValueEntry);
        }
    }
    if (i < ApiSetMap.Count - 1){
        FSeek(StartAddr + Entries.Entry[i+1].NameOffset);
    }
    else {
        FSeek(StartAddr + ApiSetMap.Size);
    }

} API_SET_ENTRY_PARSED <read=ReadApiSetEntry>;

string ReadApiSetEntry(API_SET_ENTRY_PARSED & ParsedEntry)
{
    local string sApiSetName = ReadWString(ParsedEntry.StartAddress, ParsedEntry.NameLength);
        if (ParsedEntry.HasValues) {
        local string sApiSetValue = ReadWString(ParsedEntry.ValueName[0].StartAddress, ParsedEntry.ValueName[0].ValueLength/2);
        return sApiSetName + " -> " + sApiSetValue;
    }

    return sApiSetName;
}

// main entry point
LittleEndian();
Printf("Parse api set schema Begin.\n");
local int StartAddr = FTell();
local int i =0;
local int j =0;

API_SET_NAMESPACE ApiSetMap <fgcolor=cGreen>;
FSeek(StartAddr + ApiSetMap.EntryOffset);

// Enumerate API_SET_NAMESPACE_ENTRY entries
API_SET_NAMESPACE_ENTRIES Entries <fgcolor=cPurple>;

// Traverse API_SET_NAMESPACE_ENTRY entries and retrieve
// corresponding API_SET_VALUE_ENTRY entry in order to dump
// api-set and hosts library names.
for (i=0; i<ApiSetMap.Count; i++) {
    FSeek(StartAddr + Entries.Entry[i].NameOffset);
    API_SET_ENTRY_PARSED ParsedEntry(Entries.Entry[i]) <fgcolor=cBlue>;
}

// Enumerate API_SET_HASH_ENTRY entries
FSeek(StartAddr + ApiSetMap.HashOffset);
API_SET_NAMESPACE_HASH_ENTRIES HashEntries <fgcolor=cRed>;

Printf("Parse api set schema End.\n");

010 的模板大伙都会用吧,需要注意的是,这个模板解析的是.apiset节的数据,所以需要提前将 ApiSetSchema.dll 的数据抠出来,然后再使用该模板,使用效果如下:

对照着模板就很好理解了,前 28 字节是一个名为 API_SET_NAMESPACE 的结构,核心字段 CountEntryOffsetHashOffet 分别表示 API_SET_NAMESPACE_ENTRY 结构体数组的个数、API_SET_NAMESPACE_ENTRY 入口偏移以及 API_SET_HASH_ENTRY 入口偏移,其中哈希表的作用是操作系统用于快速检索转发后的 DLL 名。

上边说了 EntryOffset 偏移过去指向的是 API_SET_NAMESPACE_ENTRY 数组且大小为 Count 个。其核心字段 NameOffsetValueOffset 分别表示欲转发的原始 DLL 名的偏移以及 API_SET_VALUE_ENTRY 的入口偏移。

API_SET_VALUE_ENTRY 中使用 ValueOffset 表示转发后 DLL 名的偏移。

需要注意的是,某个api-ms-*动态库可能转发到多个其它的动态库中,至于具体是多少个,由 API_SET_NAMESPACE_ENTRY 中的 ValueCount 指明。

Cpp代码解析

接下来我就对照着 010 模板写一份 Cpp 解析 ApiSetSchema.dll 的工具。首先是将结构体拷出来,如下所示:

typedef struct _API_SET_NAMESPACE
{
    uint32_t Version;
    uint32_t Size;
    uint32_t Flags;
    uint32_t Count;
    uint32_t EntryOffset;
    uint32_t HashOffset;
    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;
    uint32_t NameLength;
    uint32_t HashedLength;
    uint32_t ValueOffset;
    uint32_t ValueCount;
} 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;
    uint32_t ValueLength;
} API_SET_VALUE_ENTRY, *PAPI_SET_VALUE_ENTRY;

然后给出 ApiSetSchema.dll 的路径,并解析其 PE 结构。为了更快更方便更稳定的解析 PE 结构,我使用了跨平台的LIEF框架:

#include <LIEF/LIEF.hpp>

int main() {
    std::string path = "apisetschema.dll";
    std::unique_ptr<LIEF::PE::Binary> bin = LIEF::PE::Parser::parse(path);
    return 0;
}

解析 PE 结构后定位到.apiset节区,并获取其所有的数据:

LIEF::PE::Section sec = bin->get_section(".apiset");
std::vector<uint8_t> sec_data = sec.content();

将节区的前 28 字节对应到 API_SET_NAMESPACE 结构体,并获取到 API_SET_NAMESPACE_ENTRY 数组的数量和偏移:

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);

遍历 API_SET_NAMESPACE_ENTRY 结构体数组,大小为 Count ,打印其原始 DLL 名。然后获取其转发后的 DLL 名,并打印转发后的 ValueCount 个 DLL 名。注意:因为 ApiSetSchema 用于内核中,所以应该使用 UNICODE_STRING 来解析字符串。如果使用wchar_t*或者wstring打印结果,会得到不正确的结果,原因是 DLL 的名称不会以00 00作为结束标识,而是以 NameLength 作为结束标识(此坑我已踩):

uint32_t i = 0, j = 0;
UNICODE_STRING origin_name, forward_name;
for (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 (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++;
}

完整代码如下:

#include <LIEF/LIEF.hpp>

typedef struct _UNICODE_STRING
{
    uint16_t Length;
    uint16_t MaximumLength;
    _Field_size_bytes_part_(MaximumLength, Length) wchar_t *Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

#if defined(_WIN64)
typedef __int64 INT_PTR, *PINT_PTR;
typedef unsigned __int64 UINT_PTR, *PUINT_PTR;

typedef __int64 LONG_PTR, *PLONG_PTR;
typedef unsigned __int64 ULONG_PTR, *PULONG_PTR;

#define __int3264 __int64

#else
typedef _W64 int INT_PTR, *PINT_PTR;
typedef _W64 unsigned int UINT_PTR, *PUINT_PTR;

typedef _W64 long LONG_PTR, *PLONG_PTR;
typedef _W64 unsigned long ULONG_PTR, *PULONG_PTR;

#define __int3264 __int32
#endif

typedef struct _API_SET_NAMESPACE
{
    uint32_t Version;
    uint32_t Size;
    uint32_t Flags;
    uint32_t Count;
    uint32_t EntryOffset;
    uint32_t HashOffset;
    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;
    uint32_t NameLength;
    uint32_t HashedLength;
    uint32_t ValueOffset;
    uint32_t ValueCount;
} 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;
    uint32_t ValueLength;
} API_SET_VALUE_ENTRY, *PAPI_SET_VALUE_ENTRY;

int main()
{
    std::string path = "apisetschema.dll";
    std::unique_ptr<LIEF::PE::Binary> bin = LIEF::PE::Parser::parse(path);

    LIEF::PE::Section sec = bin->get_section(".apiset");
    std::vector<uint8_t> sec_data = sec.content();

    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);

    uint32_t i = 0, j = 0;
    UNICODE_STRING origin_name, forward_name;
    for (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 (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;
}

运行结果如图所示:

参考链接

点击收藏 | 3 关注 | 2
  • 动动手指,沙发就是你的了!
登录 后跟帖