原文链接(GitHub的Markdown渲染效果更佳):为未公开API生成Lib并与MSBuild集成(一)
本文从Lib文件格式结构开始,描述Lib文件如何包含Dll的导出函数信息以及夹带Obj文件供链接,并从零构造Lib使得其包含任意未公开API导出函数信息。最后与MSBuild 集成,衔接到Visual Studio C/C++项目中完成闭环。
注:本文“未公开API”均特指Windows SDK中连Lib文件也没有其导入符号的未公开API。
以我们熟悉的CreateProcessInternalW
为例,它由Kernel32.dll
导出。Windows SDK里没有它的声明,Kernel32.lib里也没有它的符号。如果想调用,一般使用动态加载或生成Stub Dll借用其Lib的方式:
按照函数定义,定义其指针类型(参数略),然后通过LoadLibaray
+GetProcAddress
之类的方式动态加载Dll和寻址:
PS: 如果想像以上Demo代码一样在UM使用KM及未公开定义,可以关注SystemInformer (ProcessHacker) 的phnt ,我也积攒下了Wintexports 自用。
显然,对于支持目标系统里必然存在的函数,远不及直接CreateProcessInternalW(...)
调用方便,调用未公开API的数量越多,动态加载的代码越冗杂繁复 ,于是考虑下面的办法。
编写Stub Dll,其具有除实现为空外完全相同的函数定义(参数略):
注意,CreateProcessInternalW
和绝大多数Windows API一样使用stdcall调用约定,x86下Lib的导入符号名称与Dll导出函数名称不一样,前者带有调用约定的修饰而后者没有,要用模块定义文件 (.def) 定义其导出:
最后编译Stub Dll,补充函数定义,取其一同生成的Lib,便能直接CreateProcessInternalW(...)
这般调用了。
一眼看去,似乎仍有改进的空间,重点在于Lib的生成:
这些“改进”太微不足道,但有时候我就这么点追求。先参考一些已有的工具:
ReactOS 的spec2def 生成工具 它根据自定的spec文件描述函数原型生成Lib。spec比起手写空函数体加配套模块定义文件 (.def) 要简单的多:
据此生成汇编源文件再生成Stub Dll+Lib。卷入汇编器总比卷入C编译器生成得快。
MASM32 的inc2l工具 它根据汇编inc文件里的过程声明生成汇编源文件然后生成Stub Dll+Lib。
MSVC 库管理器 (Lib.exe) 它虽然可以根据模块定义文件 (.def) 直接生成Lib,但其生成导入符号的名称类型总指定为IMPORT_OBJECT_NAME
,即表示导入名称与导入符号名称完全相同,不适用于x86下stdcall调用约定的API(此场景下应用IMPORT_OBJECT_NAME_UNDECORATE
,表示导入名称为导入符号名称去除调用约定的修饰后的结果)。
比起以上已有的轮子还能进一步卷的,是做到能用spec2def 这样简洁的函数定义,还能不卷入汇编器/编译器这类代码生成工具,甚至还能实现一个Lib包含多个Dll的导出符号。不妨从文件结构开始,从零构建Lib。
先祭出Microsoft Learn的祖传文档——PE Format - Archive (Library) File Format - Win32 apps | Microsoft Learn 。
我已将踩过的坑对应的文档错漏提交给Microsoft Docs 并得到合入(PR #1555 , PR #1586 , PR #1622 ),此外需要注意的本文会提及。与某些单纯复制或者翻译微软文档就拿来发的博客相比,算是实践出真知了。
Lib (Archive) 文件主要包含导入符号(Import Symbol),供链接器链接。这些符号可以指向外部Dll的导出,也可以存在于Lib自身夹带的Obj文件里。
整体布局比较简单。开头固定的文件签名后,一系列成员(Archive Member)依次排列,每个成员都以成员头(IMAGE_ARCHIVE_MEMBER_HEADER
)开始,后跟成员数据。
Archive Member Header (成员头) Lib文件中所有成员都以IMAGE_ARCHIVE_MEMBER_HEADER
结构体开头,实际有用且需要的只有Name
和Size
字段,分别表示名称和大小。Name
也用以指示为上表签名后的三个特殊成员,如果名称不够16字节,剩余的用空格' '
补齐,如果超过16字节(去除末尾固定的'/'
只剩15字节),则名称字符串存放到 Longnames Member (长名称成员)里,Name
字段指示其所在偏移。Size
表示成员数据大小,不包含成员头自身大小60(IMAGE_SIZEOF_ARCHIVE_MEMBER_HDR
或sizeof(IMAGE_ARCHIVE_MEMBER_HEADER)
)字节,也不包含用于对齐的'\n'
。
1st Linker Member 与 2nd Linker Member 作用和结构都类似,记录符号的名称和所在文件偏移供链接器定位,信息上是冗余的。前者Unix Style,后者MS Style,并且后者按名称排序使得按名称查找符号更高效,MS链接器(Link.exe )使用。
Longnames Member 后续各符号的成员名如果超长,则名称形式不再是"xxxx/"
(16字节放不下),而是"/n"
(n
即为该名称字符串所在Longnames Member内容中的偏移)。
Symbol Lib包含的各个符号实际内容。前面说过,可以是指向外部Dll的导出,也可以是夹带的Obj。
指向外部Dll的导出 成员名称为Dll名称,如"KERNEL32.dll"
,内容为Import Header+导入符号名字符串+Dll名称字符串。虽然Dll名称重复出现,但后续MS链接器生成导入表时会分别使用,这两处不同或者有误会导致生成的导入表奇怪或不正确。要注意调用约定、Dll导出名、导入符号名的关系,内容示例:
此外,还要同时导出带"__imp_"前缀的符号供链接器使用,避免调用外部Dll导出时生成不必要的Jmp Stub指令。即_CreateProcessInternalW@48
与__imp__CreateProcessInternalW@48
都指向同个Import Header。
夹带Obj 成员名称为Obj在Lib中的全路径,如果在根目录,等同于Obj文件名。内容直接为Obj文件内容。如果多个Lib导入符号都在同一个夹带的Obj内,则都指向同个Symbol,链接器会在这个Symbol夹带的Obj里寻找它们。
其它注意事项
每个成员(Archive Member Header)所在偏移要2字节对齐,如果成员大小为奇数,则在其后插入'\n'
(IMAGE_ARCHIVE_PAD
),使紧随其后的成员所在偏移得以对齐。插入的'\n'
不计入成员大小。
数值有的用十进制字符串表示,有的用大端存储,MS Style的2nd Linker Member里各数值用小端存储,要看清文档说明。
按照上述基础的Lib文件结构,构造包含Dll导出符号(如_CreateProcessInternalW@48
与__imp__CreateProcessInternalW@48
)的Lib可供链接器链接,但MS链接器(Link.exe )生成的二进制导入表却缺失了对应Dll的IID(IMAGE_IMPORT_DESCRIPTOR
)。
想必从Windows SDK里最小的Dll导入Lib找线索更容易。用7-Zip 打开SAS.lib里的1.txt(也就是1st Linker Member),看到其包含符号如下:
可见,除了包含Dll导出符号(_SendSAS@4
与__imp__SendSAS@4
),还有:
它们分别位于3个不同夹带的Obj里,MS链接器(Link.exe )构造导入表时使用它们。
在一个用于链接Dll的Lib里__NULL_IMPORT_DESCRIPTOR
只需要有1个,所以存放在单独的一个Obj会比较好。成员名无所谓,不用保留名称即可。
前二者名称分别为"__IMPORT_DESCRIPTOR_[Dll名]"
和"[Dll名]_NULL_THUNK_DATA"
(注意开头字符为'\x7F'
),可以放在同个Obj里。如果Lib包含多个Dll的导出符号,它们也要对应地有多个,成员名也是Dll的名称。所在Obj需要有__NULL_IMPORT_DESCRIPTOR
的外部引用记录,否则链接Dll时__NULL_IMPORT_DESCRIPTOR
可能未被主动引入,导致的PE映像导入表没能以空IID结尾。
与Windows SDK的Lib里的Obj相比,我们可以去除其中的调试符号,也可以简化3个Obj之间的复杂关系。
我的代码实现:
KNSoft/C4Lib - C4Lib/Source/PEImage/ObjectFile.cs 中NewNullIIDObject
方法用于构造__NULL_IMPORT_DESCRIPTOR
所在Obj,NewDllImportStubObject
方法用于构造__IMPORT_DESCRIPTOR_XXX
与XXX_NULL_THUNK_DATA
所在Obj。
KNSoft/C4Lib - C4Lib/Source/PEImage/ArchiveFile.cs 是Lib构造的核心实现,调用AddImport
方法(包含多个重载)为Lib文件添加Object文件或Dll导出的导入项。
KNSoft/Precomp4C - Precomp4C/Source/Tasks/DllStubTask.cs 是MSBuild自定义生成任务,调用上述方法,根据输入的XML描述,输出Lib:
使用7-Zip 查看产出Lib的1.txt,内容如:
可以看到它同时包含了KERNEL32.dll
与SECHOST.dll
的导出,__NULL_IMPORT_DESCRIPTOR
只有一份,__IMPORT_DESCRIPTOR_XXX
与XXX_NULL_THUNK_DATA
归并到了一个Obj里,并自动生成了带__imp_
开头的别名符号,已可正常链接,功德+1。
要功德圆满,功能上就要形成闭环。我能想到好的方式是与MSBuild 集成,这也是使用C#实现的主要因素。
也许我们天天都在使用MSBuild 这个强大的生成平台,却对它了解甚少,如果稍加了解,
我想,它值得独占一篇。接下来的第二篇(未完待续)将以自定义代码生成任务为起点,了解MSBuild ,将类似spec文件的Dll导出信息通过自定义代码生成任务编译生成Lib,进而可与Visual Studio C/C++项目无缝衔接。
<br/>
文档参考:PE Format - Archive (Library) File Format - Win32 apps | Microsoft Learn
<br/>
如有谬误,恳请指正。本文与以上实现也将随以下项目不断更新:
<br/>
原文链接:为未公开API生成Lib并与MSBuild集成(一)
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 (CC BY-NC-SA 4.0) 进行许可。 <br/>Ratin <ratin@knsoft.org > 国家认证 系统架构设计师 ReactOS Contributor
/
/
typedef
BOOL
(WINAPI
*
PFNCreateProcessInternalW)(...);
NTSTATUS Status;
HMODULE Kernel32Handle;
PFNCreateProcessInternalW pfnCreateProcessInternalW;
Status
=
LdrLoadDll(NULL, NULL, &(UNICODE_STRING)RTL_CONSTANT_STRING(L
"Kernel32.dll"
), &Kernel32Handle);
if
(NT_SUCCESS(Status))
{
Status
=
LdrGetProcedureAddress(Kernel32Handle,
&(ANSI_STRING)RTL_CONSTANT_STRING(
"CreateProcessInternalW"
),
0
,
&pfnCreateProcessInternalW);
if
(NT_SUCCESS(Status))
{
/
/
pfnCreateProcessInternalW(...)
}
/
/
LdrUnloadDll(Kernel32Handle);
}
/
/
typedef
BOOL
(WINAPI
*
PFNCreateProcessInternalW)(...);
NTSTATUS Status;
HMODULE Kernel32Handle;
PFNCreateProcessInternalW pfnCreateProcessInternalW;
Status
=
LdrLoadDll(NULL, NULL, &(UNICODE_STRING)RTL_CONSTANT_STRING(L
"Kernel32.dll"
), &Kernel32Handle);
if
(NT_SUCCESS(Status))
{
Status
=
LdrGetProcedureAddress(Kernel32Handle,
&(ANSI_STRING)RTL_CONSTANT_STRING(
"CreateProcessInternalW"
),
0
,
&pfnCreateProcessInternalW);
if
(NT_SUCCESS(Status))
{
/
/
pfnCreateProcessInternalW(...)
}
/
/
LdrUnloadDll(Kernel32Handle);
}
BOOL
WINAPI
CreateProcessInternalW(...)
{
return
FALSE;
}
BOOL
WINAPI
CreateProcessInternalW(...)
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课
最后于 2023-8-28 00:00
被Ratin编辑
,原因: 勘误