第五章 监控Native API调用
翻译:Kendiv( fcczj@263.net )
更新:Friday, April 29, 2005
声明:转载请注明出处,并保证文章的完整性,本人保留译文的所有权利。
一个简单的Hook协议读取程序
为了帮助你编写自己的API Hook Client程序,我给出了一个简单的示例行的程序,该程序可以读取Hook协议缓冲区中的数据并在控制台窗口中显示。通过按下P、F和R键,可以实现暂停、过滤和重置功能,输出可以按照用户自定义的函数名模板进行过滤。这个示例程序叫做“SBS Windows 2000 API Hook Viewer”,其源代码位于本书光盘的\src\w2k_hook目录下。
控制Spy Device
为了方便,w2k_hook.exe程序使用了一组针对SPY_IO_HOOK_*函数的外包函数,列表5-19给出了这些函数。这些工具函数使得代码的可读性更好,并且在开发Spy Device的客户端程序时,大大降低了参数出错的可能性。
BOOL WINAPI SpyIoControl (HANDLE hDevice,
DWORD dCode,
PVOID pInput,
DWORD dInput,
PVOID pOutput,
DWORD dOutput)
{
DWORD dInfo = 0;
return DeviceIoControl (hDevice, dCode,
pInput, dInput,
pOutput, dOutput,
&dInfo, NULL)
&&
(dInfo == dOutput);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyVersionInfo (HANDLE hDevice,
PSPY_VERSION_INFO psvi)
{
return SpyIoControl (hDevice, SPY_IO_VERSION_INFO,
NULL, 0,
psvi, SPY_VERSION_INFO_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookInfo (HANDLE hDevice,
PSPY_HOOK_INFO pshi)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_INFO,
NULL, 0,
pshi, SPY_HOOK_INFO_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookInstall (HANDLE hDevice,
BOOL fReset,
PDWORD pdCount)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_INSTALL,
&fReset, BOOL_,
pdCount, DWORD_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookRemove (HANDLE hDevice,
BOOL fReset,
PDWORD pdCount)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_REMOVE,
&fReset, BOOL_,
pdCount, DWORD_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookPause (HANDLE hDevice,
BOOL fPause,
PBOOL pfPause)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_PAUSE,
&fPause, BOOL_,
pfPause, BOOL_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookFilter (HANDLE hDevice,
BOOL fFilter,
PBOOL pfFilter)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_FILTER,
&fFilter, BOOL_,
pfFilter, BOOL_);
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookReset (HANDLE hDevice)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_RESET,
NULL, 0,
NULL, 0);
}
// -----------------------------------------------------------------
DWORD WINAPI SpyHookRead (HANDLE hDevice,
BOOL fLine,
PBYTE pbData,
DWORD dData)
{
DWORD dInfo;
if (!DeviceIoControl (hDevice, SPY_IO_HOOK_READ,
&fLine, BOOL_,
pbData, dData,
&dInfo, NULL))
{
dInfo = 0;
}
return dInfo;
}
// -----------------------------------------------------------------
BOOL WINAPI SpyHookWrite (HANDLE hDevice,
PBYTE pbData)
{
return SpyIoControl (hDevice, SPY_IO_HOOK_WRITE,
pbData, lstrlenA (pbData),
NULL, 0);
}
列表5-19. Device I/O Control工具函数
在使用列表5-19中的函数之前,Spy 设备驱动程序必须首先被加载并启动。这一操作和在第四章讨论的内存Spy程序w2k_mem.exe的要求大致相同。列表5-20给出了该程序的主函数:Execute(),该函数可加载/卸载Spy 设备驱动程序、打开/关闭一个设备句柄并可通过IOCTL和设备进行交互。如果你对比一下列表5-20和列表4-29,它们在开始和结尾处显然都是相似的。只是在中间部分,有所不同,因为这部分的代码依赖于具体的程序。
void WINAPI Execute (PPWORD ppwFilters,
DWORD dFilters)
{
SPY_VERSION_INFO svi;
SPY_HOOK_INFO shi;
DWORD dCount, i, j, k, n;
BOOL fPause, fFilter, fRepeat;
BYTE abData [HOOK_MAX_DATA];
WORD awData [HOOK_MAX_DATA];
WORD awPath [MAX_PATH] = L"?";
SC_HANDLE hControl = NULL;
HANDLE hDevice = INVALID_HANDLE_VALUE;
_printf (L"\r\nLoading \"%s\" (%s) ...\r\n",
awSpyDisplay, awSpyDevice);
if (w2kFilePath (NULL, awSpyFile, awPath, MAX_PATH))
{
_printf (L"Driver: \"%s\"\r\n",
awPath);
hControl = w2kServiceLoad (awSpyDevice, awSpyDisplay,
awPath, TRUE);
}
if (hControl != NULL)
{
_printf (L"Opening \"%s\" ...\r\n",
awSpyPath);
hDevice = CreateFile (awSpyPath,
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL);
}
else
{
_printf (L"Unable to load the spy device driver.\r\n");
}
if (hDevice != INVALID_HANDLE_VALUE)
{
if (SpyVersionInfo (hDevice, &svi))
{
_printf (L"\r\n"
L"%s V%lu.%02lu ready\r\n",
svi.awName,
svi.dVersion / 100, svi.dVersion % 100);
}
if (SpyHookInfo (hDevice, &shi))
{
_printf (L"\r\n"
L"API hook parameters: 0x%08lX\r\n"
L"SPY_PROTOCOL structure: 0x%08lX\r\n"
L"SPY_PROTOCOL data buffer: 0x%08lX\r\n"
L"KeServiceDescriptorTable: 0x%08lX\r\n"
L"KiServiceTable: 0x%08lX\r\n"
L"KiArgumentTable: 0x%08lX\r\n"
L"Service table size: 0x%lX (%lu)\r\n",
shi.psc,
shi.psp,
shi.psp->abData,
shi.psdt,
shi.sdt.ntoskrnl.ServiceTable,
shi.sdt.ntoskrnl.ArgumentTable,
shi.ServiceLimit, shi.ServiceLimit);
}
SpyHookPause (hDevice, TRUE, &fPause ); fPause = FALSE;
SpyHookFilter (hDevice, TRUE, &fFilter); fFilter = FALSE;
if (SpyHookInstall (hDevice, TRUE, &dCount))
{
_printf (L"\r\n"
L"Installed %lu API hooks\r\n",
dCount);
}
_printf (L"\r\n"
L"Protocol control keys:\r\n"
L"\r\n"
L"P - pause ON/off\r\n"
L"F - filter ON/off\r\n"
L"R - reset protocol\r\n"
L"ESC - exit\r\n"
L"\r\n");
for (fRepeat = TRUE; fRepeat;)
{
if (n = SpyHookRead (hDevice, TRUE,
abData, HOOK_MAX_DATA))
{
if (abData [0] == '-')
{
n = 0;
}
else
{
i = 0;
while (abData [i] && (abData [i++] != '='));
j = i;
while (abData [j] && (abData [j] != '(')) j++;
k = 0;
while (i < j) awData [k++] = abData [i++];
awData [k] = 0;
for (i = 0; i < dFilters; i++)
{
if (PatternMatcher (ppwFilters [i], awData))
{
n = 0;
break;
}
}
}
if (!n) _printf (L"%hs\r\n", abData);
Sleep (0);
}
else
{
Sleep (HOOK_IOCTL_DELAY);
}
switch (KeyboardData ())
{
case 'P':
{
SpyHookPause (hDevice, fPause, &fPause);
SpyHookWrite (hDevice, (fPause ? abPauseOff
: abPauseOn));
break;
}
case 'F':
{
SpyHookFilter (hDevice, fFilter, &fFilter);
SpyHookWrite (hDevice, (fFilter ? abFilterOff
: abFilterOn));
break;
}
case 'R':
{
SpyHookReset (hDevice);
SpyHookWrite (hDevice, abReset);
break;
}
case VK_ESCAPE:
{
_printf (L"%hs\r\n", abExit);
fRepeat = FALSE;
break;
}
}
}
if (SpyHookRemove (hDevice, FALSE, &dCount))
{
_printf (L"\r\n"
L"Removed %lu API hooks\r\n",
dCount);
}
_printf (L"\r\nClosing the spy device ...\r\n");
CloseHandle (hDevice);
}
else
{
_printf (L"Unable to open the spy device.\r\n");
}
if ((hControl != NULL) && gfSpyUnload)
{
_printf (L"Unloading the spy device ...\r\n");
w2kServiceUnload (awSpyDevice, hControl);
}
return;
}
列表5-20. 程序的主框架
需要注意的是,列表5-20中的Execute()函数在调用CreateFile()时需要设置GENERIC_READ和GENERIC_WRITE访问标志,而列表4-29中的函数仅使用了GENERIC_READ。产生这种不同的原因是,这些应用程序使用的IOCTL编码不同。第四章的内存Spy程序仅使用了只读的IOCTL函数,而这里讨论的API Hook Viewer调用的函数可以修改系统数据,因此它需要有写权限的设备句柄。如果你检查过表5-3的第三列的IOCTL编码,你会发现它们十六进制编码,大多数在右起第四个位置上都是E,而SPY_IO_HOOK_INFO和SPY_IO_HOOK_READ却是数字6。根据第四章的图4-6,这意味着有后面的Hook管理函数需要一个读权限的设备句柄,而其余的需要读/写两种权限。设备驱动程序的设计者必须决定设备可处理的I/O请求所需的是读权限还是写权限,或者是二者的组合。修改系统的API Service Table肯定要进行写操作,所以此时客户端要求获取一个有写权限的句柄则是理所当然的了。
列表5-20中,剩余的大多数代码都很容易读懂。不过下面的特性值得重点说一下:
l SPY_IO_HOOK_READ函数以行模式进行操作,这是由列表5-20中的循环开始时调用的SpyHookRead()函数的第二个参数决定的。
l 应用程序的用户可以通过命令行提供一系列的模式字符串(通过嵌入的通配符*和?)。辅助函数PatternMatcher()顺序的使用这些模式串和每一行协议数据中的函数名进行比较,列表5-21给出了该函数。如果没有可以匹配模式的函数名,那么该行协议数据将被忽略。要查看未过滤得协议数据,请使用命令行:w2k_hook *
l 在处理完一行协议数据后,应用程序将调用sleep(0)进入休眠状态,并将它剩余的时间片归还给系统,这些时间片可供其他进程使用。
l 如果没有有效的协议数据,则应用程序会在休眠(suspend)10msec(HOOK_IOCTL_DELAY)后,再次向Spy设备进行查询。这样可及时地降低CPU的负载,并降低对Native API的使用率。
l 在列表5-20的主循环中,也会查询键盘输入。除P、E、R和ESC键外,其他的键都将被忽略。P键用于打开/关闭暂停模式(默认为:打开),F键允许/禁止过滤(默认:允许),R键将重置协议,ESC键则用来终止程序的运行。
l 如果P、F、R或Esc中的某一个键被按下,则一个分隔行将被写入协议缓冲区中(通过SPY_IO_HOOK_WRITE函数)。这个分隔行是用来表示根据输入的命令而进行的状态改变。向缓冲区中写入一个分隔行要好于直接在控制台中进行显示,因为要在屏幕上体现出状态的改变会有一定的延时。例如,如果P键被按下,此时应该停止显示了,但应用程序还是会继续生成输出数据直到从协议缓冲区中读取了所有的数据。但如果P命令在缓冲区的末尾增加一个分隔行,就可以正确的显示数据了。
l 和第四章的w2k_mem.exe程序类似,w2k_hook.exe仅在全局标志gfSpyUnload被设置的情况下,才会卸载Spy驱动程序。默认情况下,该全局标志并未被设置,至于为什么,在第四章已经解释过了。
BOOL WINAPI PatternMatcher (PWORD pwFilter,
PWORD pwData)
{
DWORD i, j;
i = j = 0;
while (pwFilter [i] && pwData [j])
{
if (pwFilter [i] != '?')
{
if (pwFilter [i] == '*')
{
i++;
if ((pwFilter [i] != '*') && (pwFilter [i] != '?'))
{
if (pwFilter [i])
{
while (pwData [j] &&
(!PatternMatcher (pwFilter + i,
pwData + j)))
{
j++;
}
}
return (pwData [j]);
}
}
if ((WORD) CharUpperW ((PWORD) (pwFilter [i])) !=
(WORD) CharUpperW ((PWORD) (pwData [j])))
{
return FALSE;
}
}
i++;
j++;
}
if (pwFilter [i] == '*') i++;
return !(pwFilter [i] || pwData [j]);
}
列表5-21. 一个简单的字符串模式匹配器
图5-6和图5-7中的例子,是w2k_hook.exe使用名字模式*file和ntclose时生成的。这将筛选出所有的对文件管理函数的调用,同时还包括NtClose()。要特别注意的是,名字模式串不影响协议数据的生成,它只在显示时对已生成的协议数据进行过滤,而Spy设备的“垃圾”过滤器是根据已注册的句柄来决定生成什么样的协议数据的。如果你指定w2k_hook.exe的名字模式串来排除某些协议项,那么是不会影响协议数据的生成的。唯一的影响就是,如果从协议缓冲区中取出的数据不符合你的要求,那么就丢掉而已。
总结和不足
Russinovich和Cogswell(Russinovich and Cogswell 1997)提出的API Hooking机制在此也是使用的,而且非常漂亮和具有独创性。下面就是此种机制中值得关注的特点:
l 在系统的API Service Tabel中安装和卸载一个Hook,只是简单的指针交换操作。
l 安装完Hook后,它将可接收到系统中所有进程发出的Native API调用,即使是在Hook安装后才启动的进程。
l 因为Hook设备运行于内核模式,所以它有最大的权限来访问系统资源。甚至可以执行特权级的CPU指令。
下面是我在开发自己的Spy device时,遇到的问题:
l 必须十分小心的设计和编写Hook device。因为在Native API一级产生的流量会经过多个程序的上下文空间,它必须向操作系统内核一样稳定才行。一个很小的疏忽也会使系统立即玩完。
l 仅有一小部分内核的API流量被记录下来。例如,由其他内核模式的模块发出的API调用不会经过系统的INT 2Eh门,因此,也不可能经过我们的Hook。还有,ntdll.dll和ntoskrnl.exe导出的很多重要函数并不属于Native API,因此,对于它们,通过Service Table是无法进行Hook的。
不完善的API覆盖率带来更多的是限制而不是对稳定性的需求。总之,通过跟踪Native API调用能收集到如此多的有关程序内部信息的有用数据,还是很令人惊讶。例如,我可以简单的通过观察由微软提供的NetWare Redirector(nwrdr.sys)对NtFsControlFile()的使用(traffic)来深入了解NetWare Core Protocol(NCP)的运作。因此,这种监控API的方法的确是一种非常专业的备选方案,通过它我们可以从Windows 2000中获取我们感兴趣的数据。
Next:
接下来,我们将开始第六章的学习:如何在用户模式下调用内核函数
……………本章完…………