第六章 在用户模式下调用内核API函数
翻译:Kendiv( [email protected] )
更新:Friday, May 06, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
通往用户模式的桥梁
现在,内核调用接口的演化已经缓慢的到达了终点----至少已经涉及内核模式(kernel-mode)。让我们总结一下到目前为止,我们已经获得了什么:
l 名为SpyCallEx()的函数(见列表6-3)将收到一个SPY_CALL_INPUT控制块,该控制块中包含目标地址和一些函数所需的参数。SpyCallEx()调用指定的地址,并且通过一个SPY_CALL_OUTPUT控制块将结果返回。
l 一种按名字查找导出的系统函数和变量的方法,该方法由SpyModuleSymbolEx()函数实现(见列表6-11)。
现在,最后一个问题是:“我们如何让用户模式下的应用程序访问这些资源?”回答当然是:“通过设备I/O控制(Device I/O Control)”。到现在为止,Spy device提供了一组IOCTL函数,表6-1列出了这些函数。该表是第四章的表4-2的摘要,表4-2包含w2k_spy.sys提供的所有IOCTL函数。列表6-12给出了与SpyDispatcher()函数相关的部分,第四章的列表4-7给出了SpyDispatcher()函数的具体细节。
表6-1最后一行中,名为SPY_IO_CALL的函数将作为通向用户模式的桥梁。我相信一旦Spy device可以访问这些极具价值的信息,它将很容易使用户模式的应用程序获取这些数据。就像在第四章和第五章中一样,下面我们将对新引入的IOCTL函数作一个简短的介绍。
表6-1. 与内核调用接口相关的IOCTL函数
函数名 |
ID |
IOCTL编码 |
描 述 |
SPY_IO_MODULE_INFO |
19 |
0x 8000604C |
返回有关已加载的系统模块的信息 |
SPY_IO_PE_HEADER |
20 |
0x80006050 |
返回IMAGE_NT_HEADERS数据 |
SPY_IO_PE_EXPORT |
21 |
0x80006054 |
返回IMAGE_EXPORT_DIRECTORY数据 |
SPY_IO_PE_SYMBOL |
22 |
0x80006058 |
返回一个导出符号的地址 |
SPY_IO_CALL |
23 |
0x8000E 05C |
调用一个位于模块(已加载)内部的函数 |
NTSTATUS SpyDispatcher (PDEVICE_CONTEXT pDeviceContext,
DWORD dCode,
PVOID pInput,
DWORD dInput,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_MEMORY_BLOCK smb;
SPY_PAGE_ENTRY spe;
SPY_CALL_INPUT sci;
PHYSICAL_ADDRESS pa;
DWORD dValue, dCount;
BOOL fReset, fPause, fFilter, fLine;
PVOID pAddress;
PBYTE pbName;
HANDLE hObject;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
MUTEX_WAIT (pDeviceContext->kmDispatch);
*pdInfo = 0;
switch (dCode)
{
// unrelated IOCTL functions ommitted (cf. Listing 4-7)
case SPY_IO_MODULE_INFO:
{
if ((ns = SpyInputPointer (&pbName,
pInput, dInput))
== STATUS_SUCCESS)
{
ns = SpyOutputModuleInfo (pbName,
pOutput, dOutput, pdInfo);
}
break;
}
case SPY_IO_PE_HEADER:
{
if ((ns = SpyInputPointer (&pAddress,
pInput, dInput))
== STATUS_SUCCESS)
{
ns = SpyOutputPeHeader (pAddress,
pOutput, dOutput, pdInfo);
}
break;
}
case SPY_IO_PE_EXPORT:
{
if ((ns = SpyInputPointer (&pAddress,
pInput, dInput))
== STATUS_SUCCESS)
{
ns = SpyOutputPeExport (pAddress,
pOutput, dOutput, pdInfo);
}
break;
}
case SPY_IO_PE_SYMBOL:
{
if ((ns = SpyInputPointer (&pbName,
pInput, dInput))
== STATUS_SUCCESS)
{
ns = SpyOutputPeSymbol (pbName,
pOutput, dOutput, pdInfo);
}
break;
}
case SPY_IO_CALL:
{
if ((ns = SpyInputBinary (&sci, SPY_CALL_INPUT_,
pInput, dInput))
== STATUS_SUCCESS)
{
ns = SpyOutputCall (&sci,
pOutput, dOutput, pdInfo);
}
break;
}
}
MUTEX_RELEASE (pDeviceContext->kmDispatch);
return ns;
}
列表6-12. Spy driver的Hook Command Dispatcher(摘录)
IOCTL函数SPY_IO_MODULE_INFO
IOCTL函数SPY_IO_MODULE_INFO接收一个模块基地址,并返回一个SPY_MODULE_INFO结构(如果该地址指向了一个有效的PE Image)。列表6-13给出了该结构的定义以及与其相关的SpyOutputModuleInfo()帮助函数(列表6-12中的SpyDispatcher()将调用该函数)。SpyOutputModuleInfo()基于SpyModuleFind()函数(参见列表6-9),SpyModuleFind()函数返回它从ZwQuerySystemInformation()获取的MODULE_INFO数据。MODULE_INFO数据将被转换为SPY_MODULE_INFO格式后发送给调用者。
typedef struct _SPY_MODULE_INFO
{
PVOID pBase;
DWORD dSize;
DWORD dFlags;
DWORD dIndex;
DWORD dLoadCount;
DWORD dNameOffset;
BYTE abPath [MAXIMUM_FILENAME_LENGTH];
}
SPY_MODULE_INFO, *PSPY_MODULE_INFO, **PPSPY_MODULE_INFO;
#define SPY_MODULE_INFO_ sizeof (SPY_MODULE_INFO)
// -----------------------------------------------------------------
NTSTATUS SpyOutputModuleInfo (PBYTE pbModule,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_MODULE_INFO smi;
PMODULE_LIST pml;
PMODULE_INFO pmi;
DWORD dIndex;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if ((pbModule != NULL) && SpyMemoryTestAddress (pbModule) &&
((pml = SpyModuleFind (pbModule, &dIndex, &ns)) != NULL))
{
pmi = pml->aModules + dIndex;
smi.pBase = pmi->pBase;
smi.dSize = pmi->dSize;
smi.dFlags = pmi->dFlags;
smi.dIndex = pmi->wIndex;
smi.dLoadCount = pmi->wLoadCount;
smi.dNameOffset = pmi->wNameOffset;
strcpyn (smi.abPath, pmi->abPath, MAXIMUM_FILENAME_LENGTH);
ns = SpyOutputBinary (&smi, SPY_MODULE_INFO_,
pOutput, dOutput, pdInfo);
SpyMemoryDestroy (pml);
}
return ns;
}
列表6-13. SPY_IO_MODULE_INFO的实现方式
IOCTL函数SPY_IO_PE_HEADER
IOCTL函数SPY_IO_PE_HEADER只是一个简单的IOCTL外包函数,其核心部分是ntoskrnl.exe导出的RtlImageNtHeader()函数,如列表6-14所示。和SPY_IO_MODULE_INFO类似,SPY_IO_PE_HEADER也需要一个模块基地址。返回的数据是模块的IMAGE_NT_HEADER结构。
NTSTATUS SpyOutputPeHeader (PVOID pBase,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
PIMAGE_NT_HEADERS pinh;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if ((pBase != NULL) && SpyMemoryTestAddress (pBase) &&
((pinh = RtlImageNtHeader (pBase)) != NULL))
{
ns = SpyOutputBinary (pinh, IMAGE_NT_HEADERS_,
pOutput, dOutput, pdInfo);
}
return ns;
}
列表6-14. SPY_IO_PE_HEADER的实现方式
IOCTL函数SPY_IO_PE_EXPORT
这个函数比上一个IOCTL函数有趣多了。该函数返回与调用者提供的模块基地址相关的IMAGE_EXPORT_DIRECTORY结构。仔细观察列表6-15给出的该函数的实现方式,你会发现它和列表6-10中的SpyModuleExport()极其相似。不过,SpyOutputPeExport()需要完成了更多的工作。这是因为IMAGE_EXPORT_DIRECTORY包含相对地址的缘故,这一点前面已经解释过。在数据被复制到独立的缓冲区之后,调用者还是无法使用这些偏移量,这是因为与这些偏移量相关的基地址已经改变了。在PE表头中没有其他附加的地址信息,因此不可能计算出一个新的与之匹配的基地址。为了减少调用者的工作,SpyOutputPeExport()将所有指向导出节内部的偏移量转换为相对于导出节起始位置的偏移量,这是通过减去它们在IMAGE_DATA_DIRECTORY结构中的VirtualAddress而得到的。地址数组中的数据项必须采用不同的方法进行处理,因为它们引用的是PE Image中的其他节区。因此,SpyOutputPeExport()将它们加上Image的基地址,从而将它们转换为绝对线性地址。
NTSTATUS SpyOutputPeExport (PVOID pBase,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
PIMAGE_NT_HEADERS pinh;
PIMAGE_DATA_DIRECTORY pidd;
PIMAGE_EXPORT_DIRECTORY pied;
PVOID pData;
DWORD dData, dBias, i;
PDWORD pdData;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if ((pBase != NULL) && SpyMemoryTestAddress (pBase) &&
((pinh = RtlImageNtHeader (pBase)) != NULL))
{
pidd = pinh->OptionalHeader.DataDirectory
+ IMAGE_DIRECTORY_ENTRY_EXPORT;
if (pidd->VirtualAddress &&
(pidd->Size >= IMAGE_EXPORT_DIRECTORY_))
{
pData = (PBYTE) pBase + pidd->VirtualAddress;
dData = pidd->Size;
if ((ns = SpyOutputBinary (pData, dData,
pOutput, dOutput, pdInfo))
== STATUS_SUCCESS)
{
pied = pOutput;
dBias = pidd->VirtualAddress;
pied->Name -= dBias;
pied->AddressOfFunctions -= dBias;
pied->AddressOfNames -= dBias;
pied->AddressOfNameOrdinals -= dBias;
pdData = PTR_ADD (pied, pied->AddressOfFunctions);
for (i = 0; i < pied->NumberOfFunctions; i++)
{
pdData [i] += (DWORD) pBase;
}
pdData = PTR_ADD (pied, pied->AddressOfNames);
for (i = 0; i < pied->NumberOfNames; i++)
{
pdData [i] -= dBias;
}
}
}
else
{
ns = STATUS_DATA_ERROR;
}
}
return ns;
}
列表6-15. SPY_IO_PE_EXPORT的实现细节
IOCTL函数SPY_IO_PE_SYMBOL
该函数使得用户模式下的应用程序可以访问内核调用接口的符号查找引擎。列表6-16给出了该函数的实现方式,但看起来并不是很让人兴奋,因为它只是列表6-11中的SpyModuleSymbolEx()的外包函数而已。调用者必须传入一个形如”module!symbol”的字符串或者一个”symbol”(如果该symbol属于ntoskrnl.exe)。如果该函数调用成功,函数将返回与符号相关的线性地址,如果调用者提供的symbol无效或者发生了其他错误,则函数返回NULL。
NTSTATUS SpyOutputPeSymbol (PBYTE pbSymbol,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
PVOID pAddress;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if ((pbSymbol != NULL) && SpyMemoryTestAddress (pbSymbol)
&&
((pAddress = SpyModuleSymbolEx (pbSymbol, NULL, &ns))
!= NULL))
{
ns = SpyOutputPointer (pAddress,
pOutput, dOutput, pdInfo);
}
return ns;
}
列表6-16. SPY_IO_PE_SYMBOL的实现细节
IOCTL函数SPY_IO_CALL
最后是我们等待良久的SPY_IO_CALL函数了。列表6-17提供了该函数的实现细节。如果传入的字符串地址正确,此函数将调用SpyModuleSymbolEx(),如果传入的字符串可以被解析,则继续调用SpyCallEx()。和SPY_IO_PE_SYMBOL类似,此函数期望一个形如”module!symbol”或一个简单的”symbol”作为符号名,符号字符串将作为SPY_CALL_INPUT结构的一部分被初始化。如果成功,SPY_IO_CALL返回一个SPY_CALL_OUTPUT结构,该结构中将包含函数调用的结果(如果传入的符号引用的是一个API函数),或者目标变量的值(如果符号引用的是一个公共变量,如NtBuildNumber或KeServiceDescriptorTable)。
如果SPY_IO_CALL调用失败,则将不会返回任何数据。调用者必须适当的处理这种情况。忽略这一错误意味着从内核调用接口返回的是无效数据。如果将这样的数据传递给其他内核函数,将会出现错误。如果你很走运,则错误将由SpyCallEx()内的异常处理例程捕获。如果你很不走运,则整个进程都将挂起在Spy device的IOCTL调用中。而且,这里还存在着出现蓝屏的可能性。但是不要过于担心,在下一节中,将展示如何在用户模式的应用程序中恰当的使用内核调用接口。
NTSTATUS SpyOutputCall (PSPY_CALL_INPUT psci,
PVOID pOutput,
DWORD dOutput,
PDWORD pdInfo)
{
SPY_CALL_OUTPUT sco;
NTSTATUS ns = STATUS_INVALID_PARAMETER;
if (psci->pbSymbol != NULL)
{
psci->pEntryPoint =
(SpyMemoryTestAddress (psci->pbSymbol)
? SpyModuleSymbolEx (psci->pbSymbol, NULL, &ns)
: NULL);
}
if ((psci->pEntryPoint != NULL) &&
SpyMemoryTestAddress (psci->pEntryPoint) &&
((ns = SpyCallEx (psci, &sco)) == STATUS_SUCCESS))
{
ns = SpyOutputBinary (&sco, SPY_CALL_OUTPUT_,
pOutput, dOutput, pdInfo);
}
return ns;
}
列表6-17. SPY_IO_CALL的实现细节
Next:
在下一节中,将讨论如何将内核调用接口封装到DLL中。
………….待续………..