2008年8月29日星期五

从Vector中删除元素

现在用stl的人越来越多, stl确实是套很漂亮的算法和数据结构库. 但是初用stl的人往往会遇上很多问题.
从一个容器中删除元素,是很常用的操作,但是也是初学者常会犯错误的地方,删除map和list中元素可能会犯迭代器失效的错误. vector是stl里很常用的一个容器, 和map,list等容器相比, 从vector中删符合某些条件的元素有更多的麻烦.
比如,我们要完成如下的任务.
有下面的类


class AA
{
public:
AA():n(0){}
AA(int b):n(b){}
int n;
};

有个vector
vector vaa;
一个list
list intList;


现在需要执行这样的操作, 删除vaa里所有成员变量n在intList里的所有元素.那么, 应该怎么做呢?我们可以有下列选择:
1 手写循环
仿照list的删除方法.

vector::iterator ite = vaa.begin();
for (; ite != vaa.end(); )
{
if (find(intList.begin(), intList.end(),ite->n) != intList.end())
vaa.erase(++ite);
else
++ite;
}



一运行就会发现不行了, vector的erase的特点是, 被删除的元素和之后的所有元素的iterator都失效了, 即使保存了后面一个iterator, 也不能继续遍历了. 对于这种连续存储的序列, 应该是把不需要的元素用需要的代替, 然后把结尾不要的元素删除.像这样:



vector::iterator ite = vaa.begin();
vector::iterator dest = ite;
for(; ite != vaa.end(); ++ite)
{
if (find(intList.begin(), intList.end(),ite->n) == intList.end())
{
*dest++ = *ite;
}
}
vaa.erase(dest, vaa.end());


2. 使用remove_if, 写一个判断函数作为条件.

像上面那样写循环,麻烦,容易错而且不好读, STL提供了一个算法remove_if可以不用自己写循环,完成上面那个循环的功能, 就是把不要的



元素用需要的元素代替, 返回结尾处的iterator.remove_if的原型为



template
ForwardIterator remove_if(ForwardIterator first, ForwardIterator last,Predicate pred);


pred是一个函数子,用来作为判断条件. 函数子是可以按照函数调用的语法来使用的类型, 它可以是一个函数指针, 也可以是一个重载了operator()的类型.这里pred要求是返回值是bool,有一个参数的函数子, 参数类型就是容器里元素的类型, 对每个元素执行这个函数, 返回true就会被remove.

所以,我们需要先写一个函数来判断一个AA类型的变量是否满足条件. 但是, 这个函数显然需要两个参数, 一个AA 和一个list,为了避免拷贝,我们用指针传递list



bool inTheList( AA aa, const list *lint)
{
return find(lint->begin(), lint->end(), aa.n) != lint->end();
}



要把这个两个参数的函数绑定上一个参数变成一个参数的函数, 可以使用stl里的bind2nd函数,原型如下



template
binder2nd
bind2nd(const AdaptableBinaryFunction& F, const T& c);


这个函数并不会被执行, 编译器只是靠它来做类型推导, 它会返回一个Adaptable unary function 类型. 它的第一个参数是一个Adaptable Binary Function, 它是一个重定义了operator()的类型,不能直接传一个函数指针, 所以我们需要ptr_fun函数,ptr_fun对双参函数指针的重载的原型为:



template
pointer_to_binary_function
ptr_fun(Result (*x)(Arg1, Arg2));


这个函数也是用来做类型推导的, 可以返回一个Adaptable unary function.



综合以上各个函数, 于是就可以这样写了:



vaa.erase(remove_if(vaa.begin(), vaa.end(),bind2nd(ptr_fun(inTheList),&intList)), vaa.end());


注意, 可能是vc6的bug, 如果inList是一个类的静态成员函数, 上面的写法在vc6里无法编译, vc6不能推导出函数子的类型,上面的写法在vc8和gcc中是可以的.对于vc6,需要显式的告诉编译器我们传的是函数指针,像下面这样



vaa.erase(remove_if(vaa.begin(), vaa.end(),bind2nd(ptr_fun(&inTheList),&intList)), vaa.end());


我们也可以让inTheList是AA的一个成员函数



bool AA::inTheList(const list *lint)
{
return find(lint->begin(), lint->end(), n) != lint->end();
}


stl提供了一套把成员函数转为单参或双参函数子的函数,mem_fun1_ref,这里我们用上面的删除操作就可以写成:



vaa.erase(remove_if(vaa.begin(), vaa.end(),bind2nd(mem_fun1_ref(&AA::inTheList),&intList)), vaa.end());




3, 还是用remove_if, 自己定义判断条件的函数子类型

上面那套转换和绑定肯定能让人抓狂, 使用函数指针来传递判断条件也比较低效. 我们可以自己定义一个类型



class InListFunctor
{
public:
InListFunctor(const list &lint):m_list(lint)
{}
bool operator ()(AA a)
{
return find(m_list.begin(), m_list.end(), a.n) != m_list.end();
}
private:
const list &m_list;
} ;




这样就可以直接传给remove_if了, InListFunctor的构造函数接受一个list的const引用, 可以把要比较的list传进去.



vaa.erase(remove_if(vaa.begin(), vaa.end(), InListFunctor(intList)), vaa.end());


通过自己定义的函数子,可以构造很复杂的比较条件,更加方便和自由.



4, 用boost::lambda, 构造匿名函数.

上面两个方法都有个共同的缺点, 要么要定义一个函数, 要么要定义一个类型, 这都会给一个类里添加不必要的东西,这在实际编程中会让人觉得不爽. 用boost::lambda可以构造匿名函数子, 不会给类的名字空间带来污染. 不过这个工作对boost::lambda来说有点复杂,需要包含下面三个boost::lambda头文件,打开boost::lambda的名字空间.



#include
#include
#include
using namespace boost::lambda;


这个删除操作可以写成:



vaa.erase(remove_if(vaa.begin(),
vaa.end(),
bind(ll::find(),
intList.begin(),
intList.end(),(&_1)->*&AA::n)!=intList.end()),
vaa.end());


看起来有点复杂,关于boost::lambda的具体用法, 可以参考它的文档. 我一句两句也说不清. 上式中_1是lambda的关键, 指的是生成的函数的第一个参数. 这里也就是AA类型的元素.

Sharing Memory Between Drivers and Applications

At one time or another, most driver writers will have the need to share memory between a driver and a user-mode program. And, as with most such things, there are a wide variety of ways to accomplish the goal of sharing a block of memory between a driver and a user-mode application. Some of these approaches are decidedly right and some are wrong. Two of the easiest techniques are:

• The application sends an IOCTL to the driver, providing a pointer to a buffer that the driver and the application thereafter share.

• The driver allocates a block of memory (from nonpaged pool for example), maps that block of memory back in the address space of a specific user-mode process, and returns the address to the application.

For the sake of brevity, we’ll restrict our discussion to these two, straightforward, techniques. Other perfectly acceptable techniques include sharing a named section that’s backed by either the paging file or a memory mapped file. Perhaps we’ll discuss those in a future article. Also, note that this article won’t specifically address sharing memory that’s resident on a device. While many of the concepts are the same, sharing device memory with a user-mode program brings with it its own set of special challenges.

Sharing Buffers Using IOCTLs
Sharing memory between a driver and a user-mode app using a buffer described with an IOCTL is the simplest form of “memory sharing”. After all, it’s identical to the way drivers support other, more typical, I/O requests. The base address and length of the buffer to be shared are specified by the application in the OutBuffer of a call to the Win32 function DeviceIoControl().

The only interesting decision for the driver writer who uses this method of buffer sharing is which buffer method (or, “transfer type” as it’s known) to specify for the IOCTL. Either METHOD_DIRECT (that is, using an MDL) or METHOD_NEITHER (using user virtual addresses) will work. If METHOD_DIRECT is used, the user buffer will be locked into memory. The driver will also need to call MmGetSystemAddressForMdlSafe() to map the described data buffer into kernel virtual address space. An advantage of this method is that the driver can access the shared memory buffer from an arbitrary process context, and at any IRQL.

There are a number of restrictions and caveats inherent in using METHOD_NEITHER to describe a shared memory buffer. Basically, these are the same ones that apply any time a driver uses this method. Chief among these is the rule that the driver must only access the buffer in the context of the requesting process. This is because access to the shared buffer is via the buffer’s user virtual address. This will almost certainly mean that the driver must be at the top of the device stack, called directly by the user application via the I/O Manager. There can be no intermediate or file system drivers layered above the driver. Again practically speaking, this probably also means that the driver is restricted to accessing the user buffer from within its dispatch routines, when called by the requesting process.

Another important restriction inherent in using METHOD_NEITHER is that access by the driver to the user buffer must always be done at IRQL PASSIVE_LEVEL. This is because the I/O manager hasn’t locked the user buffer in memory, and it could be paged out when accessed by the driver. If the driver can’t meet this requirement, it will need to build an MDL and then lock the buffer in memory.

Another, perhaps less immediately obvious, restriction to this method – regardless of the transfer type chosen – is that the memory to be shared must be allocated by the user mode application. The amount of memory that can be allocated can be restricted, for example, due to quota limitations. Additionally, user applications cannot allocate physically contiguous or non-cached memory. Still, if all a driver and a user mode application need to do is pass data back and forth using a reasonably-sized data buffer, this technique can be both easy and useful.

As easy as it is, using IOCTLs to share memory between a driver and a user-mode application is also one of the most frequently misused schemes. One common mistake new NT driver writers make when using this scheme is that they complete the IOCTL sent by the application after having retrieved the buffer address from it. This is a very bad thing. Why? What happens if the user application suddenly exits, for example, due to an exception? With no I/O operation in progress to track the reference on the user buffer, the driver could unintentionally overwrite a random chunk of memory. Another problem is that when using METHOD_DIRECT, if the IRP with the MDL is completed the buffer will no longer be mapped into system address space. An attempt to access the previously valid kernel virtual address (obtained using MmGetSystemAddressForMdlSafe()) will crash the system. This is generally to be avoided.

Mapping Kernel Memory To User Mode
That leaves us with the second scheme mentioned above: Mapping a buffer allocated in kernel mode into the user virtual address space of a specified process. This scheme is surprising easy, uses API familiar to most NT driver writers, and yet allows the driver to retain maximum control of the type of memory being allocated.

The driver uses whatever standard method it desires to allocate the buffer to be shared. For example, if the driver needs a device (logical) address appropriate for DMA, as well as a kernel virtual address for the memory block, it could allocate the memory using AllocateCommonBuffer(). If no special memory characteristics are required and the amount of memory to be shared is modest, the driver can allocate the buffer from nonpaged pool.

The driver allocates an MDL to describe the buffer using IoAllocateMdl(). In addition to allocating the MDL from the I/O Manager’s look-aside list, this function fills in the MDL’s “fixed” part. Next, to fill in the variable part of the MDL (the part with the page pointers) the driver calls MmBuildMdlForNonPagedPool().

With an MDL built that describes the buffer to be shared, the driver is now ready to map that buffer into the address space of the user process. This is accomplished using the function MmMapLockedPagesSpecifyCache() (for Win2K) or MmMapLockedPages() (for NT V4).

The only “tricks” you need to know about calling either of the MmMapLocked…() functions are (a) you must call the function from within the context of the process into which you want to map the buffer, and (b) you specify UserMode for the AccessMode parameter. The value returned from the MmMapLocked…() call is the user virtual address into which the buffer described by the MDL has been mapped. The driver can return that to the user application in a buffer in response to an IOCTL. That’s all there is to it. Put together, the code to accomplish this process is shown in Figure 1.

PVOID
CreateAndMapMemory()
{
PVOID buffer;
PMDL mdl;
PVOID userVAToReturn;

//
// Allocate a 4K buffer to share with the application
//
buffer = ExAllocatePoolWithTag(NonPagedPool,
PAGE_SIZE,
'MpaM');

if(!buffer) {
return(NULL);
}

//
// Allocate and initalize an MDL that describes the buffer
//
mdl = IoAllocateMdl(buffer,
PAGE_SIZE,
FALSE,
FALSE,
NULL);

if(!mdl) {
ExFreePool(buffer);
return(NULL);
}

//
// Finish building the MDL -- Fill in the "page portion"
//
MmBuildMdlForNonPagedPool(mdl);

#if NT_40

//
// Map the buffer into user space
//
// NOTE: This function bug checks if out of PTEs
//
userVAToReturn = MmMapLockedPages(mdl,
UserMode);

#else

//
// The preferred V5 way to map the buffer into user space
//
userVAToReturn =
MmMapLockedPagesSpecifyCache(mdl, // MDL
UserMode, // Mode
MmCached, // Caching
NULL, // Address
FALSE, // Bugcheck?
NormalPagePriority); // Priority

//
// If we get NULL back, the request didn't work.
// I'm thinkin' that's better than a bug check anyday.
//
if(!userVAToReturn) {

IoFreeMdl(mdl);
ExFreePool(buffer);
return(NULL);
}

#endif

//
// Store away both the mapped VA and the MDL address, so that
// later we can call MmUnmapLockedPages(StoredPointer, StoredMdl)
//
StoredPointer = userVAToReturn;
StoredMdl = mdl;

DbgPrint("UserVA = 0x%0x\n", userVAToReturn);

return(userVAToReturn);
}



Figure 1 — Allocating a Buffer & Mapping Into User Mode


Of course, this method does have the disadvantage that the call to MmMapLocked…() must be done in the context of the process into which you want the buffer to be mapped. This might at first make this method appear no more flexible than the method that uses an IOCTL with METHOD_NEITHER. However, unlike that method, this one only requires one function (MmMapLocked…()) to be called in the target process’ context. Because many drivers for OEM devices are in a device stacks of one above the bus (that is, there is no device above them, and no driver but the bus driver below them) this condition will be easily met. For the rare device driver that will want to share a buffer directly with a user-mode application that’s located deep within a device stack, an enterprising driver writer can probably find a safe way to call MmMapLocked…() in the context of the requesting process.

After the shared memory buffer has been mapped, like the method that uses the IOCTL with METHOD_DIRECT, the reference to the shared buffer can take place from an arbitrary process context, and even at elevated IRQL (because the shared buffer is not pageable).

If you use this method, there is one final thing that you’ll have to keep in mind: You will have to ensure that your driver provides a method to unmap those pages that you mapped into the user process any time the user process exits. Failure to do this will cause the system to crash as soon as the app exits, which is definitely to be avoided. One easy way that we’ve found of doing this is to unmap the pages whenever the application closes the device. Because closing the handle – expected or otherwise – always results in an IRP_MJ_CLEANUP being received by your driver for the File Object that represented the applications open instance of your device, you can be sure this will work. You want to perform this operation at CLEANUP time, no CLOSE, because you can be (relatively) assured that you will get the cleanup IRP in the context of the requesting thread.

Other Challenges
Despite the mechanism used, the driver and application will need a common method of synchronizing access to the shared buffer. This can be done in a variety of ways. Probably the simplest mechanism is sharing one or more named events. When an application calls CreateEvent(), the named event is automatically created in the Object Manager’s BaseNamedObjects directory. A driver can open, and share, these event objects by calling IoCreateNotificationEvent(), and specifying the same name as was specified in user mode (except, of course, specifying “\BaseNamedObjects” as the directory).

In Summary
We’ve looked at two methods for allowing a driver and a user-mode application to share a data buffer: Using a buffer created by a user application and passed to a driver via an IOCTL, and using a buffer created by the driver and mapped into the application’s address space using one of the MmMapLocked…() functions. Both methods are relatively simple, as long as you follow a few rules. Have fun!

2008年8月27日星期三

无法删除 NTFS 文件系统卷上的文件或文件夹

本文介绍您可能无法删除 NTFS 文件系统卷上的文件或文件夹的原因,以及如何分析造成此问题的不同原因从而解决此问题。
更多信息
注意:在内部,NTFS 将文件夹作为特殊类型的文件进行处理。因此,本文中的“文件”一词可能指文件,也可能指文件夹。
原因 1:文件使用了 ACL
如果某个文件使用了访问控制列表 (ACL),您可能无法删除该文件。要解决此问题,请更改该文件上的权限。您可能需要拥有这些文件的所有权才能更改权限。

管理员具有取得任何文件所有权的隐含能力,即使未明确向他们授予针对此文件的任何权限也是如此。文件所有者具有修改文件权限的隐含能力,即使未明确向他们授予针对此文件的任何权限也是如此。因此,您可能需要取得对某个文件的所有权,赋予自己删除文件的权限,然后才能删除文件。
由于文件使用了不规范的 ACL 而无法使用某些安全工具来显示或修改权限
要变通解决此问题,请使用另一种工具(例如,Cacls.exe 的更新版本)。

根据所属类型,ACL 中的访问控制项 (ACE) 有特定的优先顺序。例如,拒绝访问的 ACE 一般位于授予访问权的 ACE 之前。不过,我们无法防止某个程序编写按任意顺序安排 ACE 的 ACL。在 Windows 的一些早期版本中,当 Microsoft Windows 尝试读取这些“不规范”的 ACL 时会出现问题。有时,无法通过使用 Microsoft Windows 资源管理器图形化安全编辑器正确修改这些 ACL。此问题在 Windows 的更高版本中得到了纠正。如果您遇到了此问题,请使用最新版本的 Cacls.exe。即使无法显示或编辑一个现有的 ACL,也可以编写一个允许您访问该文件的新 ACL。
原因 2:该文件正在使用
如果文件正在使用中,则可能无法将其删除。要解决此问题,请确定具有打开句柄的进程,然后关闭此进程。

根据文件的打开方式(例如,以独占访问而不是共享访问方式打开),您可能无法删除使用中的文件。无论何时,您都可以使用多种工具来帮助确定拥有文件的打开句柄的进程。 要进一步了解可帮助确定拥有文件打开句柄的进程的工具,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章: 242131 (http://support.microsoft.com/kb/242131/) 如何显示具有已打开文件的进程的列表 172710 (http://support.microsoft.com/kb/172710/) 如何使用 Windows NT 4.0 资源工具包中的 OH 工具 此问题的症状可能多种多样。您可以使用“删除”命令来删除一个文件,但在打开该文件的进程释放该文件之前,该文件实际上并未删除。另外,您可能无法访问正在删除中的文件的“安全”对话框。要解决此问题,请确定具有打开句柄的进程,然后关闭此进程。
原因 3:文件系统损坏使您无法访问此文件
如果文件系统被破坏,则可能无法删除此文件。要解决此问题,请在磁盘卷上运行 Chkdsk 实用工具以纠正任何存在的错误。

磁盘上的坏区、其他硬件故障或者软件错误都可能会损坏文件系统,使文件出现问题。典型操作失败的方式可能各不相同。当文件系统检测到有损坏时,它将在事件日志中记录一个事件,而且您通常会收到一条提示您运行 Chkdsk 的消息。根据损坏的性质,Chkdsk 可能能够也可能无法恢复文件数据;不过,Chkdsk 可以使文件系统返回到一种在内部一致的状态。 有关如何使用 Chkdsk 实用工具的其他信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章: 176646 (http://support.microsoft.com/kb/176646/) 错误消息:The file or directory is corrupt...(文件或目录已损坏...) 187941 (http://support.microsoft.com/kb/187941/) 对 CHKDSK 和新的 /C 和 /I 开关的解释
原因 4:文件位于比 MAX_PATH 字符更深的路径中
如果文件路径存在问题,则可能无法打开、编辑或删除该文件。
解决方案 1:使用自动生成的 8.3 名称访问该文件
要解决此问题,您可能需要使用自动生成的 8.3 名称访问该文件。如果路径深的原因是文件夹名称太长,则这可能是最简便的解决方案。如果 8.3 路径也太长,或者 8.3 名称已在该卷上被禁用,则请转到解决方案 2。 有关在 NTFS 卷上禁用 8.3 文件名的其他信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章: 121007 (http://support.microsoft.com/kb/121007/) 如何在 NTFS 分区上禁止创建 8.3 名称
解决方案 2:重命名或移动路径深的文件夹
重命名文件夹,以使目标文件的路径深度不超过 MAX_PATH。如要这样做,请从根文件夹(或任何其他方便的位置)开始,然后重命名文件夹,以缩短它们的名称。如果此步骤不能解决此问题(例如,如果文件的深度超过 128 个文件夹),请转到解决方案 4。
解决方案 3:将驱动器映射到路径结构中的一个文件夹
将驱动器映射到目录文件或文件夹的路径结构中的一个文件夹。该方法可缩短实际上的路径。

例如,假定您有一个类似如下结构的路径: \\ServerName\SubfolderName1\SubfolderName2\SubfolderName3\SubfolderName4\...在该路径中,总字符数超过 255 个。要将该路径的长度缩短为 73 个字符,请将驱动器映射到 SubfolderName4。
解决方案 4:使用与文件夹具有相同深度的网络共享
如果解决方案 1、2 或 3 不太方便,或者不能解决问题,请创建一个网络共享,使其位于文件夹树中尽可能深的位置,然后通过访问此共享来重命名文件夹。
解决方案 5:使用能够遍历深路径的工具
许多 Windows 程序都预期最大路径长度少于 255 个字符。因此,这些程序仅分配足够的内部存储来处理这些典型路径。NTFS 没有此限制,它可以处理比这长得多的路径。

如果您在文件夹结构中一个已经相当深的位置创建一个共享,然后通过使用此共享在此位置之下创建一个深层结构,则可能会遇到此问题。某些在文件夹树上执行本地操作的工具可能无法遍历从根文件夹开始的整个文件夹树。您可能需要以特殊方式使用这些工具,以便它们能够遍历该共享位置。(CreateFile API 文档介绍了一种在此情形下遍历整个文件夹树的方法。)

一般情况下,您可以通过使用创建文件的软件来管理这些文件。如果有一个程序,它可以创建超过 MAX_PATH 的文件,则一般情况下您可以使用此同一程序来删除或管理这些文件。您通常可以通过使用共享来删除在此同一共享上创建的文件。
原因 5:文件名中包括 Win32 命名空间中的一个保留名称
如果文件名中包括 Win32 命名空间中的一个保留名称(例如“lpt1”),则可能无法删除此文件。要解决此问题,请使用非 Win32 程序重命名该文件。您可以使用 POSIX 工具或其他任何使用适当内部语法的工具来使用此文件。

此外,如果您使用特定的语法指定文件的路径,则可能能够使用一些内置的命令绕过典型的 Win32 保留名称检查。例如,如果在 Windows XP 中使用 Del 命令,则在使用以下特定的语法指定该文件的完整路径的情况下,可以删除名为“lpt1”的文件: del \\?\c:\path_to_file\lpt1有关在 Windows NT 和 Windows 2000 下删除具有保留名称的文件的更多信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章: 120716 (http://support.microsoft.com/kb/120716/) 如何在 Windows 中删除具有保留名称的文件 有关在 Windows XP 下删除具有保留名称的文件的更多信息,请单击下面的文章编号,以查看 Microsoft 知识库中相应的文章: 315226 (http://support.microsoft.com/kb/315226/) 如何在 Windows XP 中删除具有保留名称的文件 如果使用典型的 Win32 CreateFile 机制打开到某个文件的句柄,则某些文件名称是为旧式 DOS 设备保留的。为了向后兼容,不允许使用这些文件名,也不能使用典型的 Win32 文件调用来创建它们。不过,此问题并不是 NTFS 的一种限制。

通过使用遍历超过 MAX_PATH 的文件夹时所用的那种技巧,您或许能够使用 Win32 程序绕过在创建(或删除)文件时执行的典型名称检查。另外,有些 POSIX 工具不接受这些名称检查。
原因 6:文件名包括 Win32 命名空间中的无效名称
如果文件名中包括无效名称,您可能无法删除该文件(例如,文件名有一个尾随空格,或一个尾随句点,或者文件名仅由一个空格组成)。要解决此问题,请用一种使用适当内部语法的工具删除此文件。您可以将“\\?\”语法用于某些工具,以便对这些文件进行操作,例如: del "\\?\c:\path_to_file_that contains a trailing space.txt "导致此问题的原因与原因 4 类似。然而,如果使用典型的 Win32 语法打开名称中有尾随空格或尾随句点的文件,则尾随空格或句点在打开实际文件之前会被去掉。因此,如果在同一个文件夹中有两个分别名为“AFile.txt”和“AFile.txt ”的文件(注意文件名后面的空格),如果您尝试使用标准 Win32 调用打开第二个文件,实际上会打开第一个文件。同样,如果文件的名称仅是“ ”(一个空格字符),当尝试使用标准 Win32 调用打开它时,实际上会打开该文件的父文件夹。在此情况下,如果您尝试更改这些文件上的安全设置,要么无法更改,要么会意外更改其他文件上的设置。如果发生此行为,您可能会认为您对一个实际上具有限制性 ACL 的文件拥有操作权限。
多种原因
有时,您可能会同时遇到上述多个原因,这会使删除文件的过程更复杂。例如,如果您作为计算机管理员登录,可能会同时遇到原因 1(无权删除文件)和原因 5(文件名包含一个尾随字符,导致文件访问重定向到其他文件或者不存在的文件),可能无法删除该文件。如果尝试通过取得文件的所有权和添加权限来解决原因 1 的问题,可能仍然无法删除该文件,因为用户界面中的 ACL 编辑器由于原因 6 而无法访问到正确的文件。

在此情形中,您可以使用 Subinacl 实用工具和 /onlyfile 开关(此实用工具包括在资源工具包中)来更改文件的所有权和操作权限(若不更改则无法访问),例如: subinacl /onlyfile "\\?\c:\path_to_problem_file" /setowner=domain\administrator /grant=domain\administrator=F注意:此命令是单行命令,这里进行了换行是为了便于阅读。

此示例命令行修改了包含尾随空格的 C:\path_to_problem_file 文件,使 domain\administrator 帐户成了该文件的所有者并对该文件拥有完全控制权限。现在您可以使用 Del 命令以及同一“\\?\”语法来删除此文件。

Ring3 下结束进程的技巧

公布一些 Ring3 下结束进程的技巧
所有的 OpenProcess/ZwOpenProcess/OpenThread/ZwOpenThread 都可以替换为 ZwQuerySystemInformation->ZwOpenProcess->ZwDuplicateObject 。具体是为什么请自己研究。

(Zw)OpenProcess(PID+0/1/2/3)->(Zw)TerminateProcess

(Zw)OpenProcess->CreateRemoteThread(ZwCreateThread)->ExitProcess(ZwTerminateProcess)

(Zw)OpenProcess->VirtualProtect(ZwProtectVirtualMemory)->WriteProcessMemory(ZwWriteVirtualMemory)

Thread32First/Thread32Next(ZwQuerySystemInformation)->(Zw)OpenThread->(Zw)TerminateThread

DebugActiveProcess

(Zw)OpenProcess->DbgUiDebugActiveProcess

(Zw)OpenProcess->(Zw)AssignProcessToJobObject->(Zw)TerminateJobObject

(Zw)OpenProcess->ZwUnmapViewOfSection

(Zw)OpenProcess->(Zw)SetContextThread

(Zw)OpenProcess->QueueUserAPC(ZwQueueApcThread)

/* Window Attacking */

PostMessage(SendMessage) WM_CLOSE/WM_QUIT/NC_DESTORY

SetParent->DestoryWindow

EndTask // Will make a direct call to the Win32 subsystem

PostMessage(SendMessage) 0x19 // by MJ0011, for MFC Application

for (int i=0; i<65536; i++)
{
PostMessage(HWND, i, 0, 0); // Message Flood
}

SetWindowLong(HWND, GWL_WNDPROC, (WNDPROC)YourDeadLock_Or_Crash_Function);

PostMessage

while(1)
{
HANDLE hWnd = ::FindWindow(NULL,"IceSword");
if(hProcess!=NULL)
{
::PostMessage(hWnd,WM_QUIT,0,0);
::PostMessage(hWnd,100,0,0);
::PostMessage(hWnd,101,0,0);
}
::Sleep(200);
}

只会在is启动的瞬间起作用

在程序中动态调用API函数

这是KsSafetyCenter中的一个技术,代码的注释比较少,自己看着理解吧^。^,算是我自己一次大的开源吧。。
目前仅仅是STDCALL的函数调用,其它类型的函数调用大家可以自己写出。
代码可能有错,欢迎指出。


// 注:代码必须用VC编译,否则像DEV C++这样的编译器不会通过编译的。



//----------------------------------------KsRunFunc.h----------------------------------//
/* 作者:Lightning(kxsystem@163.com) */
/* 转载和使用请声明,谢谢 */
#define FUNC_NAME_LENGTH 64 // 函数名最大长度
#define FUNC_PARAM_MAX 16 // 参数个数最大值

#define KSPARAM_DWORD 0x0 // 参数类型
#define KSPARAM_STRING 0x1





// KS是我的代号kxsystem的简写。

// 信息头
struct KSFUNC_CALL
{
KSFUNC_CALL* pNext; // 可以用一个链表将所有调用过的函数连接起来,便于制作"调用堆栈"
DWORD dwSize; // 本结构大小,包括后面缓冲区大小
BOOL bRaiseError; // 出错了吗??
BOOL bIsKernel; // 内核模式? 现在还没有使用.
char pszFuncName[FUNC_NAME_LENGTH]; // 函数名
DWORD pfn; // 函数指针
DWORD hModule;// 模块句柄
DWORD dwReturn; // 返回值
int nParamCount; // 参数个数
char cTypeOfParams[FUNC_PARAM_MAX]; // 参数类型
DWORD dwRVAs[FUNC_PARAM_MAX]; // 参数地址偏移,使用时需要加上GetBuffer()
PBYTE GetBuffer(){ return (PBYTE)(this+1); } // 返回本结构以后的内存区.
};

#ifndef _KERNEL

DWORD htoi(const char* szHex)
{
DWORD temp;
sscanf(szHex, "%x", &temp);

return temp;
}



下面这个函数的代码很难理解,我又比较L,与主要技术无关,主要是对字符串的解析.具体代码就不列出了.

主要的功能为:解析字符串,将函数地址,函数模块,函数名,函数参数及其偏移地址,以及是否对参数进行取值等信息写入到KSFUNC_CALL结构中.



BOOL InitFunctionCall( char* lpString, KSFUNC_CALL* fc)
{


}



#endif

DWORD CallFunction(KSFUNC_CALL* fc)
{


DWORD Addr=fc->pfn;
int nCount=fc->nParamCount;
DWORD dwPop=nCount*sizeof(DWORD) ;
DWORD dwRet;
DWORD Value;
__try
{

for(int b=fc->nParamCount-1;b>=0 ;b--)
{
Value=(DWORD)(fc->GetBuffer()+fc->dwRVAs[b]);
if(fc->cTypeOfParams [b]==KSPARAM_DWORD)
{
Value=*(DWORD*)Value;
}

_asm mov edx,Value
_asm push edx
}
_asm
{
mov eax,Addr
call eax
// add esp,dwPop
mov dwRet,eax
}
fc->dwReturn =dwRet;
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
fc->bRaiseError =TRUE;
}

return dwRet;
}





有了以上的头文件,其它的就简单得多了,下面是一个简单的console程序。



#include
#include
#include "KsRunFunc.h"



int main()
{
char st[0x100];
memset(st,0,0x100);
KSFUNC_CALL* p=(KSFUNC_CALL*)st;

if(InitFunctionCall("user32.dll!MessageBoxA(0,\"Hello World!\",\"App\",0);",p))
{
printf("Function Name=%s\n",p->pszFuncName);
printf("Function Module=0x%0X\n",p->hModule);
printf("Function Address=0x%0X\n",p->pfn);
if(p->nParamCount !=0)
{
for(int b=0;bnParamCount ;b++)
{
if(p->cTypeOfParams [b]==KSPARAM_DWORD)
printf("Function Parameter %d = 0x%0X\n",b+1,*(DWORD*)(p->GetBuffer()+p->dwRVAs[b]) );
else
printf("Function Parameter %d = %s\n",b+1,(char*)(p->GetBuffer()+p->dwRVAs[b]));
}
}
printf("Return =0x%0X",CallFunction(p));
}



getchar();

return 0;
}

Find PspTerminateProcess

#include
#include


int main(void)
{
ULONG hNtoskrnl=(ULONG)::LoadLibrary("ntoskrnl.exe");
if(hNtoskrnl == NULL)
return 0;

// 这个东西在查SSDT表时会得到,这里先用硬编码

ULONG KernelBase=0x804d5000;
ULONG pTerminateJobObject=0x8062c91f -KernelBase+hNtoskrnl;

PBYTE p=NULL;
BOOL bNext=FALSE;
ULONG PspTerminateAllProcessesInJob=0;
ULONG PspTerminateProcess=0;
// Ex***
ULONG ExAcquireResourceExclusiveLite=KernelBase-hNtoskrnl+(ULONG)::GetProcAddress((HMODULE)hNtoskrnl,"ExAcquireResourceExclusiveLite");
for(p=(PBYTE)pTerminateJobObject;(ULONG)p<0x100+pTerminateJobObject;p++)
{
if(*p == 0xE8)
{
printf("Find 0xE8 at 0x%0X , JMP to 0x%0X\n",p-hNtoskrnl+KernelBase,*(ULONG*)(p + 1)+((ULONG)p + 5 - hNtoskrnl + KernelBase));
if(bNext)
{
PspTerminateAllProcessesInJob=*(ULONG*)(p+1)+(ULONG)p-hNtoskrnl+KernelBase+5;
break;
}
if(*(ULONG*)(p + 1)+((ULONG)p + 5 - hNtoskrnl + KernelBase) == ExAcquireResourceExclusiveLite)
{
bNext=TRUE;
}
}
}
bNext=FALSE;
if(PspTerminateAllProcessesInJob != 0)
{
printf("Find PspTerminateAllProcessesInJob = 0x%0X\n",PspTerminateAllProcessesInJob);
ULONG temp=PspTerminateAllProcessesInJob-KernelBase+hNtoskrnl;
for(p=(PBYTE)(temp);(ULONG)p<0x100+temp;p++)
{
if(*p == 0xE8)
{
if(bNext)
{
PspTerminateProcess=*(ULONG*)(p+1)+(ULONG)p-hNtoskrnl+KernelBase+5;
break;
}
bNext=TRUE;
}
}
if(PspTerminateProcess != 0)
{
printf("Find PspTerminateProcess =0x%0X\n",PspTerminateProcess);
}

}
getchar();
return 0;

}

ring3ScanMem

#include
#include

BOOL IsAddressValid(PVOID pAddr)
{
DWORD Temp = 0;
if(::ReadProcessMemory(::GetCurrentProcess(), pAddr, &Temp, sizeof(DWORD),NULL))
return TRUE;
else
return FALSE;
}
void ScanModule()
{
for(DWORD pAddr = 0x10000; pAddr < 0x80000000; pAddr+= 0x10000)
{
if(!IsAddressValid((PVOID)pAddr))
continue;
if(*(USHORT*)pAddr == IMAGE_DOS_SIGNATURE)
{
IMAGE_NT_HEADERS32* pNtHeader = (IMAGE_NT_HEADERS32*)((DWORD)pAddr + ((IMAGE_DOS_HEADER*)pAddr)->e_lfanew);
if(pNtHeader->Signature == IMAGE_NT_SIGNATURE)
{
IMAGE_OPTIONAL_HEADER* pOptHeader = (IMAGE_OPTIONAL_HEADER*)&pNtHeader->OptionalHeader;
if(pAddr == pOptHeader->ImageBase)
{
printf("Find New Module at address: 0x%08lX\r\n",pAddr);
printf("Module File Size = 0x%08lX\r\n\r\n",pOptHeader->SizeOfImage);
// printf("Module Export Function = %s",pOptHeader->DataDirectory
}
}
}
}
}

int main(void)
{
ScanModule();
system("pause");
return 0;
}

Inline Hook 检测与对抗

第一回合:
Inline Hook 的检测是一个比较古老的话题,一种基本的检测思路是先与模块镜像进行比较,如果不同的话调用反汇编引擎解析内存中这个函数的指令,如果发现跳转指令则计算其跳转目的地,再结合ZwQuerySystemInformation(用户模式下可以用ZwQueryVirtualMemory)定位做HOOK的模块。但是像一些PUSH XXXX + RET之类类似于跳转功能的指令却无法正确其跳转地址。
不过好的是PUSH + RET格式的Inline Hook 被使用得太多了(包括一些AV都在用),我们可以专门针对这样的指令“出台”对应的方案。

当然这种做法没有什么大用,我们可以简单地使用如下指令:(假如我们想跳转到0x12345678)
sub esp,4
mov dword ptr [esp], 12340000H
add dword ptr [esp], 5678H
pop eax
jmp eax

第二回合:
这样,Inline Hook 检测器必须学会一定量的数学计算才行,仅仅从反汇编引擎中直接得不出什么好的效果。况且我们的Inline Hook 可以使用一些花指令来迷惑反汇编器,比方说:
mov eax, 123456H
cmp eax, 654321H
jl code
db e8H; // 这里可以是任意字节,只要不是单字节指令就行
code:
// 这里才是我们的代码

Inline Hook 检测器除了数学计算还得有逻辑判断,还得知道如果搞掉假的E8 CALL。况且这样的花指令代码花样非常之多,可填充的无意义字节每次都会导致不同的反汇编结果。这样一来,最终的结果是Inline Hook 检测器得自己模拟一个CPU来运行各种指令,最终判断是否跳到别的地方。

第三回合:
但是更加令人困惑的是,假设我们花了很多时间真的做出来一个CPU模拟器,可我们却分析静态指令,不可能知道Inline Hook 执行前的堆栈、寄存器的值。所以当有的模块这样做HOOK时我们无能为力了。

// 这是一个内核态钩子
mov eax, [esp + 4]
cmp eax, 80000000H
ja code: // 用户态钩子这里改成jb
db e9H
code:
// 我们的代码

[esp + 4]里面的调用这个函数压入的返回地址,很明显这个地址必须大于 0x80000000,而CPU模拟器面对静态代码时不可能是知道堆栈中有什么,所以无法做出动态判断。
当然把上面的方法“记录在案”可以让Inline Hook 检测器正确检测出它来。可是我们可以造出千万个类似的代码,这样就使检测器囧掉了。

第四回合:
不过检测器也不是没有办法,在检测函数时,使用调试器的方法下断以获取堆栈和寄存器。但是必须等到这个函数被执行才行。如果检测NtLoadDriver,则必须有驱动加载才会被断,这个频率非常之低。
检测器还可以遍历所有可能的条件跳转,即不去判断条件而把所有可能的解释枚举出来再判断其合理性,可是我们精心设计一个根本不会被执行的死循环就可以让检测器挂得很惨……

就像编写病毒和检测病毒的关系一样,编写病毒不需要考虑太多,而检测病毒却是一道非常高深的算法题目,因此大家无需在此方面总是骂AVs的技术还不如病毒。确实,如果让病毒作者去写杀毒软件,能有正确、高效思路的未必很多。

第五回合:
(由大家来说)

shimeng注入

#include "ntifs.h"

typedef unsigned long DWORD;
typedef unsigned short WORD;
typedef unsigned char BYTE;

#define SEC_IMAGE 0x01000000
#define SHIM_DEBUG 1

__declspec(dllimport) DWORD PsLookupProcessByProcessId(HANDLE ProcessId,PEPROCESS* pProcess);
__declspec(dllimport) DWORD ZwOpenProcess(PHANDLE ProcessHandle,ULONG DesiredAccess,POBJECT_ATTRIBUTES ObjectAttributes,PCLIENT_ID Cid);
__declspec(dllimport) DWORD ZwQueryInformationProcess(HANDLE ProcessHandle,PROCESSINFOCLASS ProcessInformationClass,PVOID ProcessInformation,ULONG ProcessInformationLength,PULONG ReturnLength);
__declspec(dllimport) BYTE *PsGetProcessPeb(PEPROCESS Process);


#ifdef _WIN64
#define SHIM_OFFSET 0x2d8
#else*/
#define SHIM_OFFSET 0x1e8
#endif

UNICODE_STRING shimRegRoot;

WCHAR shimName[]=L"shimeng.dll";
WCHAR shimSection[]=L"\\knowndlls\\shimeng.dll";
UNICODE_STRING shimDllRegKey;
HANDLE hShimSection;


#ifdef _WIN64
WCHAR shimSection32[]=L"\\knowndlls32\\shimeng.dll";
UNICODE_STRING shimDll32RegKey;
HANDLE hShimSection32;
#endif

void processNotify(HANDLE parentId,HANDLE processId,BOOLEAN createFlag){
PVOID memAddress=0;
SIZE_T memSize=sizeof(shimName);
HANDLE hProcess;
BYTE *pPeb;
OBJECT_ATTRIBUTES objAttr;
CLIENT_ID idClient;
PEPROCESS pEProcess;
DWORD ntStatus;
KAPC_STATE kaps;

if(createFlag){
RtlZeroMemory(&objAttr,sizeof(OBJECT_ATTRIBUTES));
idClient.UniqueProcess=processId;
idClient.UniqueThread=0;
if(ZwOpenProcess(&hProcess,PROCESS_DUP_HANDLE,&objAttr,&idClient)!=0){
return;
}
if(PsLookupProcessByProcessId(processId,&pEProcess)!=0){
return;
}
pPeb=PsGetProcessPeb(pEProcess);
if(pPeb==0){
return;
}
ntStatus=ZwAllocateVirtualMemory(hProcess,&memAddress,0,&memSize,MEM_COMMIT,PAGE_READWRITE);
if(ntStatus!=0){
#ifdef SHIM_DEBUG
DbgPrint("ZwAllocateVirtualMemory fails");
#endif
return;
}
KeStackAttachProcess(pEProcess,&kaps);
__try
{
memcpy(memAddress,shimName,sizeof(shimName));
*(WCHAR**)(pPeb+SHIM_OFFSET)=memAddress;
}__except(EXCEPTION_EXECUTE_HANDLER)
{
#ifdef SHIM_DEBUG
DbgPrint("Write lMemory fails");
#endif
}
KeUnstackDetachProcess(&kaps);
}
return;
}

void driverUnload(PDRIVER_OBJECT DriverObject){
PsSetCreateProcessNotifyRoutine(processNotify,TRUE);
ZwClose(hShimSection);

#ifdef _WIN64
ZwClose(hShimSection32);
#endif
#ifdef SHIM_DEBUG
DbgPrint("shim injector: unloaded");
#endif
return;
}

NTSTATUS mapShim(HANDLE *pSection,PUNICODE_STRING dllName,WCHAR *sectionName)
{
HANDLE hFile;
OBJECT_ATTRIBUTES objAttr;
IO_STATUS_BLOCK ioStatusBlock;
UNICODE_STRING usFileName;
DWORD ntStatus;
HANDLE hRegKey;
PKEY_VALUE_PARTIAL_INFORMATION regKeyInfo=0;
DWORD regKeySize=sizeof(KEY_VALUE_PARTIAL_INFORMATION)+0x200;
ULONG retSize=0;
WCHAR wdllImage[128]={L"\0"};

InitializeObjectAttributes(&objAttr,&shimRegRoot,0,0,0);
ntStatus=ZwOpenKey(&hRegKey,KEY_QUERY_VALUE,&objAttr);
if(ntStatus!=STATUS_SUCCESS){
#ifdef SHIM_DEBUG
DbgPrint("ZwOpenKey fails");
#endif
return ntStatus;
}
regKeyInfo=(KEY_VALUE_PARTIAL_INFORMATION*)ExAllocatePoolWithTag(PagedPool,regKeySize,'SHIE');
if(regKeyInfo==0){
#ifdef SHIM_DEBUG
DbgPrint("Allocate memory fails");
#endif
ZwClose(hRegKey);
return ntStatus;
}


ntStatus=ZwQueryValueKey(hRegKey,dllName,KeyValuePartialInformation,(PVOID)regKeyInfo,regKeySize,&retSize);
ZwClose(hRegKey);
if(ntStatus!=STATUS_SUCCESS){
#ifdef SHIM_DEBUG
DbgPrint("ZwQueryValueKey fails");
#endif
ExFreePool(regKeyInfo);
return ntStatus;
}
#ifdef SHIM_DEBUG
DbgPrint("shim injector: shim %ls added",regKeyInfo->Data);
#endif
/*
ntStatus = RtlMultiByteToUnicodeN(wdllImage,128,0,regKeyInfo->Data,strlen(regKeyInfo->Data));
if(ntStatus != STATUS_SUCCESS)
{
#ifdef SHIM_DEBUG
DbgPrint("RtlMultiByteToUnicodeN fails");;
#endif
ExFreePool(regKeyInfo);
return ntStatus;
}*/

DbgPrint("\n%ws\n",regKeyInfo->Data);
if(wcsncmp((PCWSTR)regKeyInfo->Data,L"\\??\\",wcslen(L"\\??\\"))!= 0)
{
wcscat(wdllImage,L"\\??\\");
wcscat(wdllImage,(PCWSTR)regKeyInfo->Data);
}
else
{
wcscat(wdllImage,(PCWSTR)regKeyInfo->Data);
}
DbgPrint("\n%ws\n",wdllImage);
RtlInitUnicodeString(&usFileName,wdllImage);
InitializeObjectAttributes(&objAttr,&usFileName,OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,0,0);
ntStatus=ZwOpenFile(&hFile,FILE_GENERIC_EXECUTE,&objAttr,&ioStatusBlock,FILE_SHARE_READ,FILE_SYNCHRONOUS_IO_NONALERT);
if(ntStatus!=STATUS_SUCCESS){
#ifdef SHIM_DEBUG
DbgPrint("Opel file fails");
#endif
ExFreePool(regKeyInfo);
return ntStatus;
}
RtlInitUnicodeString(&usFileName,sectionName);
ntStatus=ZwCreateSection(pSection,SECTION_ALL_ACCESS,&objAttr,0,PAGE_EXECUTE,SEC_IMAGE,hFile);
ZwClose(hFile);
ExFreePool(regKeyInfo);
return ntStatus;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject,PUNICODE_STRING RegistryPath)
{
DWORD ntStatus;
#ifdef SHIM_DEBUG
DbgPrint("shim injector: loaded");
#endif
RtlInitUnicodeString(&shimRegRoot,L"\\REGISTRY\\MACHINE\\SOFTWARE\\shim");
RtlInitUnicodeString(&shimDllRegKey,L"shimdll");


#ifdef _WIN64
RtlInitUnicodeString(&shimDll32RegKey,L"shimdll32");
#endif

ntStatus=mapShim(&hShimSection,&shimDllRegKey,shimSection);


#ifdef _WIN64
if(ntStatus==STATUS_SUCCESS)
{
ntStatus=mapShim(&hShimSection32,&shimDll32RegKey,shimSection32);
}
#endif

if(ntStatus==STATUS_SUCCESS)
{
ntStatus=PsSetCreateProcessNotifyRoutine(processNotify,FALSE);
}

if(ntStatus!=STATUS_SUCCESS)
{
ZwClose(hShimSection);

#ifdef _WIN64
ZwClose(hShimSection32);
#endif
#ifdef SHIM_DEBUG
DbgPrint("shim injector: unloaded (0x%x)",ntStatus);
#endif
}
DriverObject->DriverUnload=driverUnload;
return ntStatus;
}
在HKEY_LOCAL_MACHINE\software建立Shim项,在shim项下建立一为shimdll的字符串值,其值存为要注入的dll的全路径
测试了下,对于dll有特殊的要求,自己随便写的dll在进程创建时会发送0xc0000005的错误而利用系统的shimeng.dll会注入成功,写一个马甲dll也不成功,不知道什么原因??

2008年8月26日星期二

shimeng

先说什么是shim engine....
最直接的方式就是下载这个附件...运行test目录下面的quake3.exe ....
呃...实际上她是windows自己的winver.exe的一个copy而已...
我只是rename成了quake3.exe.....
运行出来界面了..仔细动动..看看有什么不同的地方....
呵呵..你会发现alt+tab 不能用了...或许你要认为我作了手脚...
其实没有..你随便选一个(其实并不是所有的exe都能正常的运行)exe文件...
放到test目录下面..rename成quake3.exe...
然后运行你会发现alt tab还是不能用

是不是很惊奇...如果还不放心的话...自己写一个程序..
仍然放到test目录下面rename成quake3.exe...再运行..

现在总算是相信不是我作手脚了吧...
既然不是我,那是谁呢....当然只有windows了..

都知道xp,2000 sp4,2003以及更高版本的程序右键对话筐里面
有个兼容性的选项...下面的文字就是描述这个东西了...
主要的内容就是描述windows是怎么实现这一套东西的....

呃补充一句...附件里的程序只能在nt平台下运行...
9x系统就别try了...
一来9x下面没有这个东西...
二来我本来就是用unicode编译的...嘿.
请不要用multi byte code 编译源代码...
虽然不会有编译错误..
但是你看不到正确的执行结果...
嗯..附件里面的源代码是一个sdb文件的查看工具.什么是sdb文件..往下看

windows 现在的几个主流操作系统都提供一种兼容模式..
方便早期的程序能比较正常的运行..
那这个功能是怎么作到的呢
概括的讲...windows保存了一份有问题的程序列表..
同时保存着修正这些问题的方式..
在程序开始运行的时候(其实并不只是这个时刻)进行修正也就是打补丁
windows保存的信息放在以sdb为后缀的文件里面
他们都分布在windows下的apppatch目录下面
其中apphelp.sdb是显示help信息用的
drvmain.sdb是用于驱动程序的..他紧跟着hal.dll跟ntoskrnl.exe
被osloader加载到内容...很特别吧...他只是一个data file而不是一个image
sysmain.sdb是最主要的文件了..放的是应用程序的信息
msimain.sdb放的是msi安装包的一些信息..
这里主要的是讨论的是sysmain.sdb..其他的暂时无视..
那么sdb文件是一个什么样子的结构呢..他又起一个什么样的作用呢..
一个一个的解释...
sdb是一个tree一样的结构...在sdbreader目录下面有个table.txt的文件..里面有说明
结构先放放.有我画的图..加上源代码..加上你自己的调试跟踪..相信很容易能了解....
先看功能....

如果你attach一个debugger到test目录下的quake3.exe...
你就会发现有几个很不寻常的dll存在...
Shimeng.dll AcGenral.dll AcXtrnal.dll
就是他们一起完成了令你吃惊的功能....

说了半天..你不要头晕了啊..哈哈

前面说了...windows实现是靠patch完成的..
最主要的方式就是修改exe和dll的import table..
大部分的程序兼容性都是因为不正确使用api所造成的..
所以windows的shim engine也是在这个上面下功夫..

windows把有问题的程序收集起来...
在创建新系统的时候检查将要运行的程序是不是会有问题
如果有..那么就打上补丁..主要就是加载一个额外的辅助dll(他们都放在apppatch目录下面)
额外的辅助dll准备好一份要修改的函数信息...由shimeng.dll完成import table的修改工作

如果你还对刚刚的alt tab问题耿耿于怀的话....那我解释下...
其实是AcGenral.dll创建了一个辅助的线程...并且安装了一个low level的keyboard hook
过滤了那几个按键而已...你可以用ctrl+shift+esc调出任务管理器来切换进程

从上来来看...windows要完成两个方面的工作
第一是判断将要运行的程序是否需要打补丁
第二是要给当前将要运行的程序打上补丁(其实在程序运行过程中也会有可能发生同样的事情)

我们也分两步来看...
首先是第一个问题....

刚刚说了..windows保存有一份有问题的程序列表..你可能觉得只是判断一下就行了..
但是不会那么简单...细心的同学已经注意到了..我的test目录下面有个Extras目录
下面还有help目录..还有两个文件...你改改他们试试...会发现alt+tab又能用了..
嗯...只是希望你明白..windows判断一个程序是否有问题的条件是很丰富的..
基本看来是比较有效的...

呵呵...其实..将来你会发现..我用来作演示的这个东西..
是windows为quake3准备的...
具体的讲
windows检查将要运行的程序的名字是否是quake3.exe..同时检查同目录下面是否有个
Extras目录...在它下面是否有个Help目录..下面是否有BotCommands.htm跟
Dedicated Server.htm两个问题存在...
如果条件都满足..那么windows就会加载AcGenral.dll...它会关上alt tab
windows还会加载AcXtrnal.dll(如果你的显卡没有装opengl驱动的话)...
如果你dumpbin一下它的exports就会发现
它其实是一个opengl的替换品..里面有一个用d3d8包装的opengl
(ps.这个实现其实很次,如果有debug版的dx..可以看到有无数的内存泄漏..呵呵)
上面是希望你认识到..windows用来判断一个程序是否有问题..并不局限于大家所了解的
比如文件名字..文件大小..文件版本..checksum等等这些数据..
将来你会看到判断的条件其实是有无数多的....
单一文件的条件大概有20多个...但是你可以设置好多的文件..
就像上面的例子一样..它会检查3个文件..

好了...现在你了解了windows有一份列表...
并且有丰富的手段来判断程序是否存在兼容性问题...
那么实际上这个工作是怎么完成的呢...

其实是很多个模块一起完成的..
首先是kernel32.dll...
在你创建一个新进程的时候..kernel32.dll在调用ntdll的zwcreateprocess之前..
就会检查将要运行的程序是否有兼容性问题
从win32的createprocess api开始一段代码以后会
调用CreateProcessInternalW函数

在我的机器上是在这个地址
[code]
.text:77E22043 ; __stdcall CreateProcessInternalW(x,x,x,x,x,x,x,x,x,x,x,x)
.text:77E22043 public _CreateProcessInternalW@48
.text:77E22043 _CreateProcessInternalW@48 proc near
[/code]

往下走走走...
[code]
.text:77E22556 call _BasepCheckBadapp@36
[/code]

参数的部分先无视了....进入这个CheckBadapp函数里面
[code]
.text:77E23CB0 ; __stdcall BasepCheckBadapp(x,x,x,x,x,x,x,x,x)
.text:77E23CB0 _BasepCheckBadapp@36 proc near
[/code]

继续往下
先判断shim是否disable了....如果没有就继续
[code]
.text:77E23CBE call _IsShimInfrastructureDisabled@0
.text:77E23CC3 test eax, eax
.text:77E23CC5 jnz loc_77E23BA9
[/code]

然后check shim的cache
[code]
.text:77E23D3F call _BaseCheckAppcompatCache@16
[/code]

如果都跳过了就进入到这里
[code]
.text:77E23C92 call _BaseCheckRunApp@40
[/code]

[code]
.text:77E3C3BB ; __stdcall BaseCheckRunApp(x,x,x,x,x,x,x,x,x,x)
.text:77E3C3BB _BaseCheckRunApp@40 proc near
[/code]

继续往下
[code]
.text:77E3C52E call ds:__imp__CsrClientCallServer@16
[/code]

CsrClientCallServer...这个调用会在另外的一个进程里面完成
(关于这个client server部分...等有机会了我再写一个文章解释解释csrss进程的东西)

暂时的讲...真正实现这个功能检查的在csrss这个进程...由basesrv.dll接受这次调用
要调试的话...用softice一类的才可以哟..因为csrss是一个很关键的进程..
如果冻结了的话..整个ring3系统都没有响应了...

废话少说....最终来到basesrv.dll的这里
[code]
.text:7596A7FD ; __stdcall BaseSrvCheckApplicationCompatibility(x,x)
.text:7596A7FD _BaseSrvCheckApplicationCompatibility@8 proc near
[/code]

继续跟踪...你又发现..这个家伙又是调用别人的...
[code]
.text:7596A907 call _pfnCheckRunApp
[/code]

这个_pfnCheckRunApp指向什么地方呢...它其实是apphelp.dll的一个export的函数
[code]
.text:75D637E8 ; __stdcall ApphelpCheckRunApp(x,x,x,x,x,x,x,x,x,x)
.text:75D637E8 public _ApphelpCheckRunApp@40
.text:75D637E8 _ApphelpCheckRunApp@40 proc near
[/code]

终于到关键的函数了...
[code]
.text:75D63812 call _InternalCheckRunApp@68
[/code]
转到internal的函数

这个函数比较大...而且不知道ms用的个什么版的编译器...
代码很乱...到处分布得都是...跟踪调试起来非常麻烦..

它得大致得步骤这样
1.open sdb
2.check exe match
3.parse check result
4.close sdb
5.return result

暂时知道如果apphelp.dll在sdb文件里面找到了一份match的数据..就会生成一个长度为
188h字节的结构返回..再由basesrv.dll返回..最终由kernel32.dll获取到这份数据

然后kernel32.dll开始正常的进程创建工作...
等到进程创建出来了...kernel32.dll把这个数据copy到新进程的process heap里面
同时在新进程的peb里面保存这个数据指针...
types一下peb看看在+1e8h的offset的地方是一个void* pShimData的指针...
它放的就是刚刚由apphelp生成的数据...

新进程创建出来了..开始运行了..进入到新进程的context...
进入ntdll.dll的
[code]
.text:77F493C1 ; __stdcall LdrpInitializeProcess(x,x)
.text:77F493C1 _LdrpInitializeProcess@8 proc near
[/code]

ebx = peb
[code]
.text:77F493CC mov eax, large fs:18h ; teb
.text:77F493D2 push ebx
.text:77F493D3 mov ebx, [eax+30h] ; ebx = peb
[/code]

ecx = pShimData,保存到[ebp-14h]的地方
[code]
.text:77F49462 lea eax, [ebx+1E8h] ; [peb+1e8h] = pShimData
.text:77F49468 mov ecx, [eax]
.....................................
.text:77F49487 mov [ebp-14h], ecx ; [ebp-14h] = pShimData
[/code]

检查[ebp-14h]的值不是0就跳转
[code]
.text:77F49CA7 loc_77F49CA7: ; CODE XREF: LdrpInitializeProcess(x,x)+93Cj
.text:77F49CA7 mov edi, [ebp-14h]
.text:77F49CAA test edi, edi
.text:77F49CAC lea eax, [ebp+40h]
.text:77F49CAF jnz loc_77F492BD
[/code]

到loc_77F492BD..然后加载shim engine
[code]
.text:77F492BD and dword ptr [ebx+1ECh], 0
.text:77F492C4 push edi
.text:77F492C5 push eax
.text:77F492C6 push edi
.text:77F492C7 call _LdrpLoadShimEngine@12
[/code]

顺便提一下..从这里开始可以在ring3下面设置新进程的断点了
[code]
.text:77F492A7 loc_77F492A7: ; CODE XREF: LdrpInitializeProcess(x,x)+8D7j
.text:77F492A7 call _DbgBreakPoint@0 ; DbgBreakPoint()
.text:77F492AC mov eax, [ebx+68h]
[/code]
那个_DbgBreakPoint@0调用就是通知debugger可以调试新进程了...

进入
[code]
.text:77F57269 ; __stdcall LdrpLoadShimEngine(x,x,x)
.text:77F57269 _LdrpLoadShimEngine@12 proc near
[/code]

往下...加载shimeng.dll
[code]
.text:77F5727A push 0
.text:77F5727C push offset _g_pShimEngineModule
.text:77F57281 lea eax, [ebp+var_8]
.text:77F57284 push eax
.text:77F57285 push 0
.text:77F57287 push 0
.text:77F57289 push 0
.text:77F5728B call _LdrpLoadDll@24
[/code]

获取shimeng.dll的导出函数..有一个叫SE_InstallBeforeInit的
调用它
[code]
.text:77F57294 call _LdrpGetShimEngineInterface@0 ; LdrpGetShimEngineInterface()
.text:77F57299 mov eax, _g_pfnSE_InstallBeforeInit
.text:77F5729E test eax, eax
.text:77F572A0 jz short locret_77F572AA
.text:77F572A2 push [ebp+arg_8]
.text:77F572A5 push [ebp+arg_4]
.text:77F572A8 call eax
[/code]

进去
[code]
.text:71A5CA10 ; __stdcall SE_InstallBeforeInit(x,x)
.text:71A5CA10 public _SE_InstallBeforeInit@8
.text:71A5CA10 _SE_InstallBeforeInit@8 proc near
[/code]

这个函数很简单

1.获取shim data...从peb里面直接读出来
[code]
.text:71A5CA47 call _SeiGetShimData@16
[/code]

2.调用下面这个函数
[code]
.text:71A5CAA7 call _SeiInit@24
[/code]

3.free掉shim data的buffer

关键的函数在SeiInit里面...也很复杂
简单的讲
1.初始化全局的数据
2.建立将要用到的几个重要数据结构
3.准备好要patch的dll...获取要patch的函数名字地址..等等
4.按照一定的条件把当前已经加载的模块打上补丁调用PatchNewModules
5.完成..

以后如果又有新的module加载到进程地址空间
ntdll总会调用shimeng.dll的SE_DllLoaded的导出函数
这个函数调用PatchNewModules(其实是jmp)..给新的module打补丁...

可以看到两个操作分别由两个函数完成..
因为这两个函数大量依赖sdb文件格式..所以我就没有列出他们的代码来..

题外话
如果你要问...我的那些代码里面的函数名字啊什么的怎么来的...
嗯.windows有提供所有的操作系统的文件的pdb下载...就是program database
里面有函数的名字参数信息等等...比较推荐的是下载checked build版的pdb
信息丰富些...有部分pdb还带有type info...那种简直就跟完整的源代码一样了
下载工具嘛..当然是softice带的Symbol Retriever了...
反汇编的工具...ida...4.6以后的版本都能非常顺利的加载pdb文件了..
4.7的ida还能自动提示你下载呢...嗯嗯...广告时间...

好了...你现在也大概的了解了整个的流程...现在来具体的看看..
先说sdb的文件格式..

sdb文件里面存放了有问题的app的列表已经修改更正的方法..
如果你不习惯我写的程序的话呢
用ms自己的吧...xp的用户在你的安装盘下面的support目录的tools下面有个act20.exe的
运行它..完成安装...然后运行compatadmin.exe在开始菜单里面就有...
2003的用户.....很不幸...我没有找到合适的下载安装...xp的那个不能安装..
但是compatadmin.exe却能在2003下面运行...我的附件里面有一个check build xp下的
compatadmin....不过也很不幸....ms没有准备这个程序的pdb...哈哈

运行看看界面吧..大致了解下sdb里面都保存有什么样子的信息先...

我们还是继续..
sdb文件的数据存放是一块一块的..每一块有一个头..描述这个块的数据类型
如果这个块是一个容器类型的..那么它就可以跟其他的块构成一种继承的体系结构
也就是我在table.txt里面描述的那样..

ms定义有9种块类型..每个块的类型用占有了4个bits..保存到了块的头里面
[code]
union _tag_header
{
struct _small
{
WORD m_wTag;
};

struct _big
{
WORD m_wTag;
DWORD m_dwSize;
};
};
[/code]

m_wTag & 0xf000得到的就是块的类型
ms定义的块类型有如下几种
0x1000 = Indicator; 0x2000 = BYTE; 0x3000 = WORD;0x4000 = DWORD;0x5000 = QWORD
0x6000 = String Index to String Table;
0x7000 = List or Table(String Table and Index Table);
0x8000 = String ; 0x9000 = Binary Data Buffer

前面6种都使用_small格式的header..因为他们的数据大小是固定的.分别是0,1,2,4,8,4字节
后面三种使用_big格式的header..大小在header里面给出..

先看简单的
0x1000的没有数据跟在头的后面.它主要用来作一个标记的作用

0x2000后面有一个字节的数据
0x3000是2个字节
0x4000是4个,0x5000也8个
这三个是最主要的数据类型..用来记录程序的信息.比如大小(DWORD),文件时间(QWORD).等等

0x6000也是4个..他用作string table的偏移量

0x8000是一个单独的String.在header后面紧跟着unicode的字符串...他主要是当作string
table的一个表项来处理..ms把文件里面使用的string 收集起来放到一个表里面..
然后用一个偏移量来表示一个string

0x9000是二进制的数据...在header后面紧跟着2进制的数据.主要用来放patch的code
(msi部分也有用他来放文件数据的)

而0x7000的就是文件结构的构架者了...他们把sdb文件形成一个tree一样的继承结构
他们本身并不保存数据..只是当作一个容器使用..比如下面这样
wTag dwSize wTag wValue
[0x7001][0x0000000c][0x4001][0x00012345][0x3002][0x1234][0x1001]
[0x7002][0x00000112][0x7003][0x00000030].....

0x7001描述的list大小有12个字节..也就是说[0x4001]跟[0x3002]跟[0x1001]
这些都属于他的范围(加起来刚刚好12个字节)..也就是说后面三个都是他的child
而0x7002已经不属于他的范围了..是他的兄弟..而0x7003还在0x7002的范围里面
所以是0x7002的child...画成tree图
[code]

+----7001
| +----4001
| +----3002
| +----1001
|
+----7002
+----7003
[/code]

一定要明白这种结构..
如果觉得有些困难的话...参考我的源代码...那么函数的名字都参考了ms的名字
Tag表示0x7001这样的东西..TagID其实就是一个文件的offset..

sdb的tree结构的整体可能情况我已经准备到table.txt里面了.同时运行我的程序
选一个sdb文件.如果不选..默认是c盘windows下apppatch下的sysmain.sdb
然后出来的tree or app选yes就能看到sdb的结构了

这里作个简单的描述
root node是一个假想的node.他有3个children..排头的是index table..
这个用来加速搜索的..暂时先放放..接着是database...主要的数据都放在这个的下面
然后是string table

database下面放的就几乎是全部的信息了...sysmain.sdb里面database下面出现的内容
主要是library layer 跟exe...排头的几个数据描述的是关于这个sdb文件的信息..

强烈建议你运行我的程序对比table.txt跟我的描述来看.很多你一看就明白是什么东西了..
程序运行方法如上..

先看exe的部分...这个个list 保存的是所以记录在案的有问题的exe文件
*n表示有很多个这样的list
里面放有基本的比如文件名字..通配的文件名字(比如你想凡是叫*_MP.exe的都进行某种操作)
应用程序的名字(这个是一个文本的名字.前面那个是比如xxx.exe的名字)
vendor的名字..guid(这个用来读取注册表的..你可以在注册表里面设定某些flags
ms用这个guid作为key的名字去获取这些flags)
等等等的基本信息...在table.txt里面..我写着有check的就表示..
在检查exe匹配的时候(也就是上面的由apphelp.dll完成的第一步)要进行检查的项目
没有标记check的就表示不需要检查匹配的.
除了一些基本的信息以外..
还有几个很重要的信息..
其中0x7008是一个匹配的列表..里面放了所有必须要进行匹配的项目,同样用check标记出来
这些都很简单..都是些标准的数据类型..一看就明白
只是注意一个0x1003的表示是使用not操作..就是说要不匹配列举出来的项目才通过
File Match List也可以有很多个..这样你就可以指定某个exe文件的匹配程度
比如quake3的那个就必须匹配3个文件..而他只是要求这3个文件存在就行
也就是每个list下面只有一个0x6001的string ,描述文件的名字(相对路径)
这一点你可以看ms的compatadmin来了解..或者用我的程序打开sysmain.sdb
tree or app的message box选no..然后左边的list里面按q..就会看到Quake III
然后看右边列举的...很简单.看看就明白...
右边的tree ctrl里面...如果是要check的..我就用[V]标记了

了解了这个match list就能想象apphelp.dll的工作了..
从exe的名字先找到对应的exe list..然后找到file match list..
然后遍历每个list的每个属性..检查匹配..很容易吧..
至于怎么从xxx找到xxx再找到xxx...呵呵..如果你对sdb的结构很了解了就很容易了
看我的源代码吧...

我们来看看apphelp.dll的工作..
[code]
.text:75D63820 ; __stdcall InternalCheckRunApp(x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x,x)
.text:75D63820 _InternalCheckRunApp@68 proc near
[/code]

往下..打开sdb文件
[code]
.text:75D63968 call _SdbInitDatabaseEx@12
[/code]

寻找match的exe
[code]
.text:75D63992 call _SdbGetMatchingExe@24
[/code]

分析结果...生成peb->pShimData
[code]
.text:75D639F1 call _ParseSdbQueryResult@20
[/code]

进入
[code]
.text:75D61AC4 ; __stdcall SdbGetMatchingExe(x,x,x,x,x,x)
.text:75D61AC4 _SdbGetMatchingExe@24 proc near

................................

.text:75D61C5B mov eax, 7007h ; EXE_LIST_TAG
.text:75D61C60 push eax
.text:75D61C61 push dword ptr [esi+4]
.text:75D61C64 push esi
.text:75D61C65 call _SdbpSearchDB@32
[/code]
到这里...搜索7007h也就是exe list

搜索部分使用了index table...先不管...最终找到exe list的tagid(也就是文件偏移)
[code]
.text:75D6AB4C call _SdbpCheckExe@44
[/code]

[code]
.text:75D64078 ; __stdcall SdbpCheckExe(x,x,x,x,x,x,x,x,x,x,x)
.text:75D64078 _SdbpCheckExe@44 proc near

..................

检查 4021h...如果存在的话

.text:75D640BB push 4021h ; Runtime Platform ID
.text:75D640C0 push [ebp+ExeListTagID]
.text:75D640C3 push edi
.text:75D640C4 call _SdbFindFirstTag@12

........
同上
.text:75D640D1 push 4022h ; OS SKU
.text:75D640D6 push [ebp+ExeListTagID]
.text:75D640D9 push edi
.text:75D640DA call _SdbFindFirstTag@12

.......
还是同上
.text:75D640EA push 401Fh ; OS Service Pack
.text:75D640EF push [ebp+ExeListTagID]
.text:75D640F2 push edi
.text:75D640F3 call _SdbFindFirstTag@12

.........
如果上面三个条件都通过了
.text:75D64115 call _SdbpCheckForMatch@28

..................
.text:75D64131 ; __stdcall SdbpCheckForMatch(x,x,x,x,x,x,x)
.text:75D64131 _SdbpCheckForMatch@28 proc near

..............
获取match file list
.text:75D64172 push 7008h ; MATCH_FILE_LIST_TAG
.text:75D64177 push [ebp+ExeTagID]
.text:75D6417A push [ebp+ShimDB]
.text:75D6417D call _SdbFindFirstTag@12

...............
检查 logic not 标识
.text:75D6418D push 1003h ; MATCH_LOGIC_NOT_TAG
.text:75D64192 push eax
.text:75D64193 push [ebp+ShimDB]
.text:75D64196 call _SdbFindFirstTag@12 ; SdbFindFirstTag(x,x,x)
.text:75D6419B xor ecx, ecx
.text:75D6419D cmp eax, ebx
.text:75D6419F setnz cl
.text:75D641A2 mov [ebp+UseLogicalNot], ecx
...........
下面有*通配符的处理..就是说*出现在file match里面的话..就用exe的名字替换
.................
然后还有%的处理..用环境变量替换..或者是system目录替换(%system32%)

收集好所有的要进行match的file以后(一直loop 到 .text:75d64172)

然后检查所有的attributes

.text:75D64A8C call _SdbpCheckAllAttributes@16

.................
.text:75D64B0D ; __stdcall SdbpCheckAllAttributes(x,x,x,x)
.text:75D64B0D _SdbpCheckAllAttributes@16 proc near

..................
用eax作为下标遍历_g_rgAttributeTags数组里面的所有tag...
如果存在的话..根据类型读取值出来

.text:75D64B2E mov eax, [ebp+var_4]
.text:75D64B31 xor esi, esi
.text:75D64B33 mov si, _g_rgAttributeTags[eax]
.text:75D64B3A push esi
.text:75D64B3B push [ebp+arg_8]
.text:75D64B3E push edi
.text:75D64B3F call _SdbFindFirstTag@12
.text:75D64B44 mov edx, eax
.text:75D64B46 test edx, edx
.text:75D64B48 jnz short loc_75D64B5D

...........
.text:75D64B5D loc_75D64B5D: ; CODE XREF: SdbpCheckAllAttributes(x,x,x,x)+3Bj
.text:75D64B5D mov ecx, esi
.text:75D64B5F and ecx, 0F000h
.text:75D64B65 xor eax, eax
.text:75D64B67 cmp ecx, 4000h
.text:75D64B6D jz loc_75D66591
.text:75D64B73 cmp ecx, 5000h
.text:75D64B79 jnz loc_75D66263
............
下面有诸如这样的函数调用读取值
.text:75D64B85 call _SdbReadQWORDTag@16

............
.text:75D66591 loc_75D66591: ; CODE XREF: SdbpCheckAllAttributes(x,x,x,x)+60j
.text:75D66591 push 0
.text:75D66593 push edx
.text:75D66594 push edi
.text:75D66595 call _SdbReadDWORDTag@12

............
值读出来了就进入到
.text:75D64B9B call _SdbpCheckAttribute@16

...............
.text:75D64BA9 ; __stdcall SdbpCheckAttribute(x,x,x,x)
.text:75D64BA9 _SdbpCheckAttribute@16 proc near

..................
他根据tag表示的意思一一获取每个属性,有些是文件大小(用windows获取文件大小的函数)
有些是文件版本信息..用version.dll提供的函数..
有些是pe文件信息(checksum,pe checksum,linker version等等)..获取pe文件头
然后从里面读...
.text:75D64BF2 call _SdbpGetAttribute@12

分支成3个部分
.text:75D64D02 call _SdbpGetVersionAttributes@8
.................
.text:75D66697 call _SdbpGetHeaderAttributes@8
.................
.text:75D64CAA call _SdbpGetFileDirectoryAttributes@4

获取完了就进行检查
也是3个部分...普通的dword qword直接比较

版本信息检查函数
.text:75D64E2F call _SdbpCheckVersion@16
...................
.text:75D65309 call _SdbpCheckUptoVersion@16
.............
字符串匹配
.text:75D64C68 call _SdbpPatternMatch@8

检查完了就返回一个true or false
然后在SdbpCheckForMatch里面

.text:75D64A94 loc_75D64A94: ; CODE XREF: SdbpCheckForMatch(x,x,x,x,x,x,x)+211Bj
.text:75D64A94 test eax, eax
.text:75D64A96 jz loc_75D64E0C
.text:75D64A9C test esi, esi ; esi = Logical not
.text:75D64A9E jnz loc_75D66251

检查返回值...同时检查logcial not标记...
然后正确的结果...
再出来以后就是把结果打包到一个ShimData里面..里面包含有这个
exe使用的exe list的tagID的值...这个值会在获取exe的patch信息的时候使用..
只是省略些步骤而已...

至于要检查哪些tag呢..看看那个数组就知道了
.data:75D7E060 _g_rgAttributeTags dw 4001h ; DATA XREF: SdbpCheckAllAttributes(x,x,x,x)+26r
.data:75D7E060 ; TagToIndex(x):loc_75D64C8Er
.data:75D7E062 dw 4003h
.data:75D7E064 dw 5002h
.data:75D7E066 dw 5003h
.data:75D7E068 dw 6011h
.data:75D7E06A dw 6012h
.data:75D7E06C dw 6009h
.data:75D7E06E dw 6010h
.data:75D7E070 dw 6013h
.data:75D7E072 dw 6014h
.data:75D7E074 dw 6015h
.data:75D7E076 dw 6016h
.data:75D7E078 dw 4007h
.data:75D7E07A dw 4008h
.data:75D7E07C dw 4009h
.data:75D7E07E dw 400Ah
.data:75D7E080 dw 4006h
.data:75D7E082 dw 400Bh
.data:75D7E084 dw 401Ch
.data:75D7E086 dw 6017h
.data:75D7E088 dw 6020h
.data:75D7E08A dw 500Dh
.data:75D7E08C dw 5006h
.data:75D7E08E dw 401Dh
.data:75D7E090 dw 401Eh
.data:75D7E092 dw 4012h
.data:75D7E094 dw 0
.data:75D7E096 dw 0

别忘了计算在调用checkallattribute之前检查的3个属性...
[/code]

检查的部分就到这里...很简单..你了解了file match的结构以后都能想象出来了已经..

下面看使用的部分...
首先要了解的就是shim ref list
他放在shim info的reference..也就是他只是保存了一个ref而已..
并不是value本身...这就好像是一个指针...间接索引..

ms把一个patch叫作一个shim...shim都有一个名字...
比如quake3使用的两个shim 一个叫IgnoreAltTab一个叫EmulateOpenGL
ref list里面得那个dword就是真正得shim info所在得文件得offset..也就是tagID
这个dword是可选的...如果不存在的话..就得用shim得名字去搜索位于library list下面的
所有shim的list.

而真正的shim也是两个主要的成员..一个是名字...上面的搜索过程就是要比较这个名字
另外一个是完成shim功能的dll的名字..比如quake3的IgnoreAltTab是有AcGenral.dll完成的
还有一个command line..会作为一个参数传递..看下面的解释
同时这里还有include跟exclude list,这个是用来指定哪些文件的import table需要被修改
哪些又不需要...同时注意到library下面也有一个include list..那个是全局的..

然后还有patch ref list..他的使用方法也跟shim 差不多..他是一个binary的buffer
里面放有patch的code...他由若干个变长的结构构成...
[code]
struct _patch_item
{
DWORD m_dwOp; // 0 = nop 2 = patch 4 = compare
DWORD m_dwNextItemOffset;
DWORD m_dwPatchSize;
DWORD m_dwPatchStartAddress;
DWORD m_dwPadding;
TCHAR m_szModuleName[32];
BYTE m_byCode[1];
};
op指定了当前这个item要完成的操作..一般的顺序是4,2,4,2.....0
就是说..对于一个文件..我先检查某一部分的特征数据(op=4)如果匹配了
表示文件是可以作memory patch的 然后跟一个op = 2的进行patch..然后循环..最后用0结尾
因为patch是变长的..所以m_dwNextItemOffset指定了下一个item相对于当前的偏移
m_dwPatchStartAddress是从模块的DosHeader开始计算的
(也就是通常用GetModuleHandle返回的值)...m_szModuleName指定module的名字
如果是数组第一个元素就是0,那么使用当前exe的DosHeader..这个可以从peb里面获取
解释的具体方法可以参考我的源代码..那个数组是用来作compile time assert的
确保m_byCode的offset是0x54..#pragma是用来关闭unreferenced local var的警告的
[/code]

至于flags ref list也同上面两个寻找方式差不多..他主要是用过运行环境的..
设置一些开关...这个部分我也没有怎么跟踪...

而layer ref list稍微不同...他不在library下面..而在database下面..
至于什么是layer...其实就是shim patch flags的组合..
比如一个layer叫win95..那么你就可能会要求有很多shim要使用..而有很多的程序都
运行在这种环境下..你就可以把这些shim整合成一个layer..而直接在exe下面放一个layer
ref list就可以了...

然后要说的就是include 跟 exclude list了...
知道shim是要修改import table的..可能你要求某些dll的import table不去修改.或者
只是修改某个dll的import table而不是默认的修改几乎全部的dll..那么include exclude
list就是一个好的东西....
两种list都有相同的结构..唯一不同的就是
如果在list下面有一个include indicator(0x1001)那么这个就是include..
如果没有..就是一个exclude...
然后有三个特殊的通配符...*是用来清空一个list的...$是用来通配当前的exe文件的
NOSF是表示不检查受windows保护的系统文件的..他只能跟include使用..跟exclude放到
一起就被无视了...

这样一来的检查规则就是
5 = include* 也就是只检查exclude list,记住*是用来情况一个list
如果一个dll出现在了这个list里面...就不对他作patch动作

3 = exclude* 只是检查include list.如果在里面就patch.不在就不patch

4 = include NOSF + exclude* 只是检查include list 只是对里面的作patch

1 = include and exclude 都检查 在include list patch.在exclude里面不patch.
都不在的话..如果是在system32或者SxS目录下面就不patch.否则patch

2 = include and exclude NOSF 同上.只是要额外检查windows的保护

NOSF的含义就是必须要作额外的windows 保护的检查
这个部分大致是这样...我没有作深入的分析..只是走马的看了看

最后了...patch是怎么完成的呢...
首先作memory patch..也就是上面patch ref list里面的工作
然后是修改import table.

对于每个shim list里面的dll..shimeng.dll加载他们..他们会export一个叫
GetHookApis的函数

shimeng.dll调用他们..传递参数
第一个是command line..这个是在shim list里面指定的
第二是shim的名字比如IgnoreAltTab这样的

shim dll返回一个HookApi的结构...里面放有要hook的函数的名字等等信息..
同时返回要hook的函数的个数...

至于修改import table就容易了
dos header + 3c 取出来 加到 dos header上 再偏移80h的地方取到import table的rva
再加到dos header上面...然按需要修改first chunk数组里面的值...当然得作些检查比较...
这个部分的技术都已经是烂到不能再烂的了...不多说了..看看实际的代码吧

关键函数
[code]
.text:71A5BEB0 ; __stdcall SeiInit(x,x,x,x,x,x)
.text:71A5BEB0 _SeiInit@24 proc near
.............
全局初始化
.text:71A5BF70 call _SeiInitGlobals@4

收集使用到的shim的tagid值到数组里面
.................
.text:71A5BFDE call _SeiBuildShimRefArray@24

..................
收集全局的include list....
.text:71A5C0D6 call _SeiBuildGlobalInclList@4

.....................
收集单个shim的include跟exclude list
全局的include list会被copy进来...
当然如果有include *这样的.自然会被清空了
.text:71A5C37B call _SeiBuildInclExclList@16

....................
获取shim dll的path
.text:71A5C3DD call _SdbGetDllPath@16

............
获取shim dll的handle..如果没有加载则加载之
.text:71A5C40C call _LdrGetDllHandle@16
...............
.text:71A5C433 call _LdrLoadDll@16

...............
shim command line
.text:71A5C494 call _SeiGetShimCommandLine@12

............
获取shim dll的GetHookApis函数地址
.text:71A5C509 call _LdrGetProcedureAddress@16

.......
调用他...返回值是一个HookApi结构数组地址..count填充第三个参数指定的地址
.text:71A5C522 lea ecx, [esp+10Ch+var_E4]
.text:71A5C526 push ecx ; hook count
.text:71A5C527 lea edx, [esp+110h+var_B0]
.text:71A5C52B push edx ; shim name
.text:71A5C52C push edi ; command line
.text:71A5C52D mov [esp+118h+var_E4], 0
.text:71A5C535 call eax

...............
load memory patch
.text:71A5C7E9 call _SeiLoadPatches@8

...............
真正开始patch module
.text:71A5C861 call _PatchNewModules@4

.............
解析hook api的真正内存地址
.text:71A5A58D call _SeiResolveAPIs@4

...........
作memory patch
.text:71A5A5B3 call _SeiAttemptPatches@0

...........
然后会检查要patch的module的路径是否是system32,是否是SxS等等
为include exclude检查作准备
最后调用
.text:71A5A710 call _SeiHookImports@24
...........
.text:71A5A200 ; __stdcall SeiHookImports(x,x,x,x,x,x)
.text:71A5A200 _SeiHookImports@24 proc near
.............

.text:71A5A2C9 mov edi, [esp+0B0h] ; edi = NtHeader
.text:71A5A2D0 mov eax, [edi+3Ch] ; eax = PE Header offset
.text:71A5A2D3 mov esi, [eax+edi+80h] ; esi = import table RVA

.........
遍历import table寻找要hook的api的module(由shim dll的GetHookApis返回的结构里面指出)

..............
找到这个module了..
遍历export函数表寻找要修改的函数

调用它先转换序号
.text:71A5A3C1 call _SeiGetOriginalImport@4

然后是它
.text:71A5A3CE call _SeiConstructChain@12
.text:71A5A3D3 mov esi, eax
.text:71A5A3D5 test esi, esi ; esi = pHookApi

来完成寻找工作...
如果找到了..返回值是要hook的api的结构信息(就是由GetHookApis返回的那个)

...........
检查exclude
.text:71A5A3EA push ecx ; ModuleName
.text:71A5A3EB call _SeiIsExcluded@12

如果也通过了
............
修改内存保护属性
.text:71A5A438 mov ebx, ds:__imp__NtProtectVirtualMemory@20
...........
.text:71A5A45D call ebx ; __declspec(dllimport) NtProtectVirtualMemory(x,x,x,x,x)
修改之...edi是要修改FirstChunk的地址...
esi是HookApi结构地址...offset + 8 是新的函数地址
............
.text:71A5A463 mov eax, [esi+8]
.text:71A5A466 mov [edi], eax

..................
改回来保护属性
.text:71A5A482 call ebx ; __declspec(dllimport) NtProtectVirtualMemory(x,x,x,x,x)

.........
往上循环遍历....直到完成
[/code]

以后凡是有新的dll被加载到进程地址空间..
ntdll都会调用shimeng.dll的SE_DllLoaded函数
ntdll会准备好一个加载的dll的链表
为什么是一个链表而不是一个文件?因为在加载一个dll的时候可能它也会import别的dll
如果单个文件的话..dll的reference不解析的话..
first chunk并没有被修改指向函数地址而是指向name 数组的话..
修改了import table也没有用...

[code]
.text:71A5A9F0 ; __stdcall SE_DllLoaded(x)
.text:71A5A9F0 public _SE_DllLoaded@4
.text:71A5A9F0 _SE_DllLoaded@4 proc near
..................

.text:71A5AA2A jmp _PatchNewModules@4
[/code]

终于完了......不知道这么多东西堆积起来有没有人愿意看...呵呵
代码太多了...我也没有办法作完整的全部的分析..也不能深入到每个细节
有些部分我就跳过去了....

最后几个比较有用的环境变量SHIM_DEBUG_LEVEL设置一个比较大得正数..能看到shim的调试信息
至于ShowDebugInfo就不用了..那点信息少得可怜..嗯..它不是环境变量..是一个regedit的
还有几个全局得变量..似乎是编译得时候指定..没有看见shimeng.dll自己有修改
_g_DebugLevel _g_bDbgPrintEnabled都指定一个正数就ok了..这个得用调试器了..

虚拟机技术

一:分类
近年来,虚拟机技术已经逐渐成为人们关注的热点,正受到越来越多的关注和重视,如VMware 已经被80%以上的全球百强企业所采纳。随着多年来研究的深入,虚拟机技术已经在企业计算、灾难恢复、分布式计算和系统安全领域得到了广泛应用。现在对虚拟机技术有很多种分类方式,本文认为虚拟机的本质特征是利用下次应用(或系统)的支持为上层应用(或系统)提供不同的接口,因此按照接口来分类应该更能反映虚拟机的特点。按照虚拟机系统对上层应用所提供接口的不同(如图1所示),形成了不同层次的虚拟机技术,主要包括硬件抽象层虚拟机、操作系统层虚拟机、API(应用程序编程接口,Application Programming Interface)层虚拟机,以及编程语言层虚拟机等四类


图1 层次化的虚拟机分类硬件抽象层的虚拟机。对上层软件(即客户操作系统)而言,硬件抽象层的虚拟机构造了一个完整的计算机硬件系统,这种虚拟机与客户操作系统的接口即为处理器指令。操作系统层的虚拟机。通过在动态复制操作系统环境,此类虚拟机能够创建多个虚拟运行容器。而对运行在每个容器之上的软件而言,此类虚拟机均提供了一个完整的操作系统运行环境,而它与上层软件的接口即为系统调用接口。API层的虚拟机。此类虚拟机为上层应用软件提供了特定操作系统运行环境的模拟,但这种模拟并不是对处理器指令的仿真,而是模拟实现该操作系统的各类用户态API。编程语言层虚拟机。此类虚拟机通过解释或即时编译技术(Just-In-Time,JIT)来运行语言虚拟机指令,从而实现软件的跨平台特性。

二:硬件抽象层次虚拟机
早在上世纪70 年代,IBM System 360、370、CP-40、CP-67[1-4]等系统就已经实现了硬件抽象层的虚拟机技术,它最初是为了弥补系统架构上的不足而发展起来的,而随着技术的发展和对虚拟机需求的增加,硬件抽象层虚拟机在强隔离功能和安全控制方面得到了长足发展和广泛应用。

如前所述,运行在硬件抽象层虚拟机之上的软件即为客户操作系统,硬件抽象层的虚拟机技术利用客户系统环境和虚拟机宿主平台的相似性来减少执行客户系统指令的延迟。目前,大多数的商业服务器虚拟化产品,都是通过使用这种技术来实现高效、实用的虚拟化系统。这种技术利用虚拟机监视器(Virtual Machine Monitor,VMM)作为隔离代码运行环境的中间层。

这类虚拟机通过VMM 提供了一个物理机器的抽象,它允许操作系统假设自身可以直接在硬件上运行,VMM为其上运行的客户操作系统提供硬件映射。从操作系统的角度看,运行在虚拟机上与运行在其对应的物理计算机系统上一样。



图1 Type I VMM与Type II VMM体系结构

按照Goldberg的定义[5],虚拟机监视器是能够为计算机系统创建高效、隔离的副本的软件。这些副本即为虚拟机(Virtual Machine,VM),在虚拟机内虚拟处理器的指令集的一个子集能够直接在物理处理器上执行。根据VMM 在整个物理系统中的实现位置和实现方法的不同,Goldberg定义了两种虚拟机监视器模型,即Type I VMM和Type II VMM,具体结构如图1所示。Type I VMM 在操作系统之前预先安装,然后在此虚拟机监视器之上安装客户操作系统,它可以在硬件支持下拥有最佳性能,如IBM VM/370[1-3],VMware ESX Server[6],Xen[7-9],Denali[10-12]等均属于这样的虚拟机。Type I VMM通常都是以一个轻量级操作系统的形式实现。Type II VMM 则是安装在已有的主机操作系统(宿主操作系统)之上,此类虚拟机监视器通过宿主主操作系统来管理和访问各类资源(如文件和各类I/O设备等),如VMware Workstation[13]、Parallel Workstation[14]等。

从实现的角度,VMM实现从虚拟资源到物理资源的映射,并利用本地物理计算机系统进行实际计算。当客户操作系统通过特权指令访问关键系统资源时,VMM将接管其请求,并进行相应的模拟处理。为了使这种机制能够有效地工作,每条特权指令的执行都需要产生自陷(Trap)以便VMM能够捕获该指令,从而使得VMM能够进行相应的指令模拟执行。VMM通过模拟特权指令的执行,并返回处理结果给指定的客户虚拟系统的方式,实现了不同虚拟机的运行上下文保护与切换,从而能够虚拟出多个硬件系统,保证了各个客户虚拟系统的有效隔离。

然而,Intel x86体系结构的处理器并不是完全支持虚拟化的[15],因为某些x86特权指令在低特权级上下文执行执行时,不能产生自陷,导致VMM无法直接捕获特权指令的执行。

目前,针对这一问题的解决方案主要有三种:基于动态指令转换(Dynamic Instruction Translation)的完全虚拟化(Full-virtualization)技术,半虚拟化(Para-virtualization)技术和硬件辅助(Hardware Assisted)技术。

许多商业的虚拟化产品都采用了基于动态指令转换的完全虚拟化技术,例如EMC公司的VMwareESX Server、VMware Workstation和Microsoft的Virtual Server系列产品。基于动态指令转换的完全虚拟化技术通过在运行时动态执行指令扫描以发现特权指令,然后依据VMM状态执行指令转换,使得特权指令的执行跳转到等价模拟代码段处,从而实现与自陷相同的目标。

由于完全虚拟化不需要修改客户操作系统,因此具有很好的兼容性,而完全虚拟化技术的性能也主要依赖于动态指令转换引擎的设计和实现。

与完全虚拟化技术技术不同,半虚拟化技术通过修改操作系统代码使特权指令产生自陷。半虚拟化技术最初由Denali[10]和Xen 项目[7] 在x86 体系架构上实现。Denali最先提出半虚拟化技术,Xen是由剑桥大学计算机实验室发起的开源虚拟机项目。它的开发得到了Intel、HP、IBM等公司的支持。Xen是在x86平台上支持同时运行多个虚拟系统的高性能VMM,它支持x86_32、x86_64、IA64等多种平台。

Xen采用半虚拟化技术,通过对客户操作系统的内核进行适当的修改,使其能够在VMM的管理下尽可能地直接访问本地硬件平台。Xen利用半虚拟化技术降低了由于虚拟化而引入的系统性能损失。如图2所示,Xen的 半虚拟化技术的主要实现思路是:对于内存分段管理的虚拟化,要求客户操作系统对硬件分段描述符的更新由Xen进行验证,这也就要求客户操作系统不能有高于Xen的特权级别和不允许访问Xen的保留地址空间;对于内存分页管理的虚拟化,要求客户操作系统可以直接读取硬件页表,但对页表的更新需要Xen进行验证和处理,Xen支持客户虚拟系统可以分布在不连续的物理内存上;对于客户虚拟系统,其只能运行在低于Xen的特权级别上;客户虚拟系统需要注册一个异常(Exception)处理函数的描述符表,直接支持Xen的虚拟化;客户虚拟系统的硬件中断机制被Xen中的Event处理机制代替;每个客户虚拟系统都有自己的时钟接口,并且可以了解真实的时间和虚拟的时间;客户虚拟系统通过异步I/O rings的内存区域和外部设备(网络、硬盘)来传递数据,采用事件处理机制代替硬件中断通知机制。



图2 Xen 3.0 体系结构

目前,Xen的最新的3.x版本已经支持Intel VT和AMD Pacifica技术,基于Intel VT技术避免了对客户操作系统内核的修改[8, 16],这一技术将使得能够在Xen之上运行各种非开源操作系统(如Windows等)。Xen作为高性能的虚拟机软件[8],越来越受到业界的关注,并在企业计算和计算机安全领域都得到了广泛的应用[9, 17-28]。

第三种方案是通过修改x86 CPU指令的语义使其直接支持虚拟化,也就是Intel的VT(Virtualization Technology)技术[29-31]和AMD的Pacifica[32]技术的目标。Intel 公司推出了VT-i(支持Ltanium 架构)和VT-x(支持X86 架构)引入新的处理器操作,称为VMX(Virtual Machine Extensions)来支持虚拟化。AMD 推出了新的处理器模式和新的内存管理模式支持虚拟化技术。VT的核心思想是给x86 CPU的各种特权指令的执行,都增加可以进行trap的可能。

AMD在虚拟化技术方面的Pacifica技术规范,是AMD计划用于其64位产品中的虚拟化技术。该技术将用于基于x86架构的服务器、台式机和笔记本电脑等系列产品。

从技术角度看,不论是 VT技术外部架构规范,还是Pacifica技术规范,它们强调的核心功能都是RISC处理器早就实现了分区(Partition)功能,即基于该技术平台实现在独立分区中高效运行多个操作系统和应用程序,使一个计算机系统像多个虚拟系统一样运行。

除了基于VMM的硬件抽象层的虚拟机技术,还有一种基于软件模拟指令集方式实现的所谓ISA(Instruction Set Architecture)层虚拟机系统。一个典型的计算机系统由处理器、内存、总线、硬盘控制器、时钟、各种I/O设备组成。ISA层的虚拟化软件的实现方式是截获客户操作系统发出的指令,并把它们“翻译”成宿主平台上的可用指令进行执行(包括处理器内部指令和I/O指令)。由于这种指令的模拟方式,ISA层的虚拟化技术可以完全模拟一台真实机器,这种实现方式的好处在于,分离了操作系统和硬件平台的紧绑定关系。

这方面具有代表性的系统有很多。Bochs[33]是用C++语言编写的开源的x86平台的PC模拟器,可以方便地在多种平台上模拟IA32 PC系统。它能够模拟多种版本的x86系统,如386、486、Pentium、Pentium Pro、SSE、SSE2等指令。Bochs解释客户系统从开机到关机的全部指令,模拟了Intel x86 CPU、BIOS以及PC设备。因此,在客户操作系统看来,就好像是运行在一台真实的机器上一样。虽然Bochs系统的性能问题,使其很难有广泛的应用,但它也在某些方面有着重要的应用价值,如在非x86平台上运行Windows系统,进行新开发的操作系统的debug工作,进行老式x86系统的兼容性测试等。

QEMU [34]是一个采用动态指令转换技术的快速的模拟器,它支持两种工作模式:用户空间模拟和全系统模拟。在用户空间模式下,QEMU可以在物理 CPU上执行为其他CPU编译的程序。在全系统模拟的模式下,它支持模拟x86、ARM、PowerPC、Sparc等结构。它可以快速地把客户操作系统的指令动态地翻译成本地指令进行执行。QEMU进行动态指令转换的基本思想是把每条指令分解成少量的简单指令。每条简单指令由一段C代码实现,通过动态代码生成器把这些简单指令的目标文件连接起来,构建指定的功能。

与基于VMM的硬件抽象层虚拟机不同,ISA层的虚拟机技术模拟运行所有客户操作系统执行的处理器指令。因此,这种虚拟机技术最大的不足就是性能问题,它的性能远远低于硬件抽象层的虚拟机,所以主要应用于动态指令转换等研究领域。

硬件抽象层的虚拟化技术有着高度的客户虚拟系统的隔离性(包括客户操作系统之间,客户操作系统和宿主操作系统之间)。这种隔离性使得在同一个物理平台上,可以同时运行不同类型的操作系统,而且它们的重启等操作不会互相影响。在用户看来,隔离性使得物理平台被划分成不同虚拟机器。由于用户面对的是虚拟机器,用户需要更多的系统安装和配置工作。


参考文献

[1] Schaefer M, Gold B, Linde R, et al. Program Confinement in KVM/370[C]. Proceedings of the 1977 ACM Annual Conference, 1977. 1977: 404–410.

[2] Gold B D, Linde R R, Schaefer M, et al. VM/370 Security Retrofit Rrogram[C]. Proceedings of the 1977 ACM Annual Conference, 1977. 1977: 411-418.

[3] Seawright L H, Mackinnon R A. VM/370 - a Study of Multiplicity and Usefulness[J]. IBM Systems Journal. 1979.

[4] Creasy R J. the Origin of theVM/370 Time-sharing System[J]. IBM Journal of Research and Development. 1981.

[5] Goldberg R P. Architecture of Virtual Machines[C]. Proceedings of the Workshop on Virtual Computer Systems, 1973. 1973: 74-112.

[6] Waldspurger C A. Memory Resource Management in VMware ESX Server[C]. Proceedings of the 5th Symposium on Operating Systems Design and Implementation (OSDI '02), 2002. 2002: 181-194.

[7] Barham P, Dragovic B, Fraser K, et al. Xen and the Art of Virtualization[C]. Proceedings of the 19th ACM Symposium on Operating Systems Principles (SOSP'03), 2003. 2003: 164-177.

[8] Ian P, Keir F, Steve H, et al. Xen 3.0 and the Art of Virtualization[C]. Proceedings of the Ottawa Linux Symposium, 2005. 2005: .

[9] Clark B, Deshane T, Dow E, et al. Xen and the Art of Repeated Research[C]. Proceedings of the USENIX Annual Technical Conference, 2004. 2004: 47-56.

[10] Whitaker A, Shaw M, Gribble S D. Denali: A Scalable Isolation Kernel[C]. Proceedings of the 10th ACM SIGOPS European Workshop, 2002. 2002: 10-15.

[11] Whitaker A, Shaw M, Gribble S D. Denali: Lightweight Virtual Machines for Distributed and Networked Applications[R]. University of Washington Technical Report 02-02-012002.

[12] Whitaker A, Shaw M, Gribble S D. Scale and Performance in the Denali Isolation Kernel[J]. ACM SIGOPS Operating Systems Review, OSDI '02: Proceedings of the 5th Symposium on Operating Systems Design and Implementation. 2002, 36(SPECIAL ISSUE: Virtual machines).

[13] Sugerman J, Venkitachalam G, Lim B H. Virtualizing I/O Devices on VMware Workstation's Hosted Virtual Machine Monitor[C]. Proceedings of the 2001 USENIX Annual Technical Conference, 2001. 2001: 1-14.

[14] Swsoft. Parallel Workstation. http://www.parallels.com/en/products/workstation[Z].

[15] Robin J S, Irvine C E. Analysis of the Intel Pentium's Ability to Support a Secure Virtual Machine Monitor[C]. Proceedings of the 9th USENIX Security Symposium, 2000. 2000: 129–144.

[16] Yaozu D, Shaofan L, Asit M, et al. Extending Xen with Intel Virtualization Technology[J]. Intel Technology Journal. 2006, 10(3): 193-203.

[17] Gupta D, Gardner R, Cherkasova L. XenMon: QoS Monitoring and Performance Profiling Tool[R]. Tech Report: HPL-2005-1872005.

[18] Haifeng X, Sihan Q, Huanguo Z. XEN Virtual Machine Technology and Its Security Analysis[J]. Wuhan University Journal of Natural Sciences. 2007, 12.

[19] Anwar Z, Campbell R H. Secure Reincarnation of Compromised Servers using Xen Based Time-Forking Virtual Machines[C]. 5th IEEE International Conference on Pervasive Computing and Communications Workshops (PerComW'07), 2007. 2007: 477-482.

[20] Fraser K, Hand S, Neugebauer R, et al. Safe Hardware Access with the Xen Virtual Machine Monitor[C]. 2004. 2004: 1-10.

[21] Quynh N A, Takefuji Y. A Novel Approach for a File-system Integrity Monitor Tool of Xen Virtual Machine[C]. Proceedings of the 2nd ACM Symposium on Information, Computer and Communications Security, 2007. 2007: 194-202.

[22] Gardner L C. Measuring CPU Overhead for I/O Processing in the Xen Virtual Machine Monitor[C]. USENIX 2005 Annual Technical Conference, 2005. 2005: 387-390.

[23] Chen H, Chen R, Zhang F, et al. Live Updating Operating Systems Using Virtualization[C]. Proceedings of the 2st ACM/USENIX International Conference on Virtual Execution Environments, 2006. 2006: 35-44.

[24] Kourai K, Chiba S. HyperSpector: Virtual Distributed Monitoring Environments for Secure Intrusion Detection[C]. Proceedings of the 1st ACM/USENIX International Conference on Virtual Execution Environments (VEE'05), 2005. 2005: 197-207.

[25] Youseff L, Wolski R, Gorda B, et al. Evaluating the Performance Impact of Xen on MPI and Process Execution for HPC Systems[C]. the First International Workshop on Virtualization Technology in Distributed Computing (VTDC), held in conjunction with Supercomputing (SC06), 2006. 2006: 1-8.

[26] Gupta D, Cherkasova L, Gardner R, et al. Enforcing Performance Isolation Across Virtual Machines in Xen[C]. Proceeding of the ACM/IFIP/USENIX 7th International Middleware Conference (Middleware'06), 2006. 2006: 342-362.

[27] Menon A, Santos J R, Turner Y, et al. Diagnosing Performance Overheads In the Xen Virtual Machine Environment[C]. Proceedings of the 1st ACM/USENIX International Conference on Virtual Execution Environments, 2005. 2005: 13-23.

[28] Sailer R, Jaeger T, Valdez E, et al. Building a MAC-Based Security Architecture for the Xen Open-Source Hypervisor[C]. Proceedings of the 21st Annual Computer Security Applications Conference (ACSAC'05), 2005. 2005: 276-285.

[29] Uhlig R, Neiger G, Rodgers D, et al. Intel Virtualization Technology[J]. IEEE Computer. 2005, 38(5): 48-56.

[30] Abramson D, Jackson J, Muthrasanallur S, et al. Intel Virtualization Technology for Directed I/O[J]. Intel Technology Journal. 2006, 10(3): 179–192.

[31] Neiger G, Santoni A, Leung F, et al. Intel Virtualization Technology: Hardware Support for Efficient Processor Virtualization[J]. Intel Technology Journal. 2006, 10(3): 167–177.

[32] Amd. AMD64 Vrtualization Codenamed "pacifica" Technology: Secure Virtual Machine Architecture Reference Manual[Z]. 2005.

[33] Lawton K. the Bochs IA-32 Emulator Project, http://bochs.sourceforge.net[Z].

[34] Bellard F. QEMU, a Fast and Portable Dynamic Translator[C]. Proceedings of the USENIX Annual Technical Conference (USENIX'05), 2005. 2005: 41-46.

三:操作系统级虚拟机

一个典型的应用程序运行环境包括:操作系统、用户函数库、文件系统、环境设置等。如果一个运行环境包含了所有这些关键组件,应用程序自身是无法区分是运行在物理系统内,还是运行在虚拟系统中。操作系统层虚拟化技术的主要思想就在于此,即在宿主操作系统上动态复制软件运行环境,以此来创建多个虚拟系统。

Jail[1, 2]是FreeBSD上的操作系统层虚拟机技术。它把操作系统划分成多个独立的环境,称之为Jail。每个Jail都可以独立管理典型的操作系统资源,如进程、文件系统、网络资源等。而用户对资源的访问范围则被限制在该用户所在Jail的内部。Jail是通过Jail系统调用创建的,Jail 内的第一个进程的所有子进程都属于该Jail,任何一个进程不能同时属于多个Jail。Jail虚拟化技术在隔离应用程序方面有一定的应用价值。

Solaris操作系统提供的区域(Zone)技术 [3, 4]也采用了类似的机制。区域是指在 Solaris 操作系统的单个实例中创建的一个虚拟的操作系统环境,是一种用于虚拟化操作系统服务的分区技术,目的是提供安全的隔离环境以便承载和运行各种应用程序。区域有两种类型:全局区域(global zone)和非全局区域 (non-global zone)。通过系统硬件引导的 Solaris安装过程所安装的即是全局区域,一个系统中只能运行一个全局区域。全局区域管理员可使用 zonecfg和zoneadm来创建非全局区域。全局区域控制所有非全局区域的安装、维护、操作和损毁。Solaris 区域功能可为在非全局区域中运行的进程提供虚拟服务和名称空间隔离。它可以将在某个非全局区域中运行的进程与在其他区域中运行的进程隔离开,这种隔离可防止在某个非全局区域中运行的进程监视或影响在其他区域中运行的进程。对于在非全局区域中运行的进程,即使具有超级用户权限也不能查看或影响其他区域中的活动。区域还提供一个抽象层,用以分隔应用程序与部署应用程序的计算机的物理属性,例如物理设备路径和网络接口名称等。

Virtual Private Server(VPS)[5]技术把服务器的操作系统环境分割成多个彼此隔离的虚拟运行容器,称之为VPS。管理员可以为每个VPS分配指定数量的内存、磁盘、网络带宽等资源,还能够在物理服务器与虚拟环境或物理服务器之间进行客户虚拟系统的迁移。VPS技术在网站的服务器整合,提高资源利用率等方面有很好的应用。


图2.4 User Mode Linux体系结构

UML(User Mode Linux)[6-8]是让一个Linux作为一个独立进程运行在另一个Linux(宿主Linux)之上的开源项目,它是一种在同一时刻运行多 Linux 的虚拟化方式。UML不需要额外的虚拟化软件,它只需要在Linux内核源码上打上相关的补丁。UML的补丁把Linux标准内核转化成一个可以作为独立进程执行的操作系统。当运行UML内核时,需要为其分配一个完整的文件系统。新的系统内核作为一个用户态的应用程序运行。UML体系结构如图2.4所示,UML内核接收来自应用程序的系统调用请求,然后发送到宿主Linux内核进行处理。由于UML的内核和用户态进程在同一地址空间内,因此,需要把内核的代码和数据段放在UML进程通常不会使用的地方。为了让UML之间共享内核数据,UML内核被映射到一个文件,然后将这个文件映射到UML进程中。目前,UML主要被用于系统软件的调试和测试。

参考文献



[1] Kamp P H, Watson R N. Jails: Confining the omnipotent root[C]. 2nd International System Administration and Network Engineering Conference (SANE'00), 2000. 2000: 1-15.

[2] Evan S. Securing freeBSD using jail[J]. Sys Admin. 2001, 10(5): 31-37.

[3] Price D, Tucker A. Solaris Zones: Operating System Support for Consolidating Commercial Workloads[C]. USENIX 18th Large Installation System Administration Conference (LISA'04), 2004. 2004: 241-254.

[4] Tucker A, Comay D. Solaris Zones: Operating System Support for Server Consolidation[C]. USENIX 3rd Virtual Machine Research and Technology Symposium (VM'04), 2004. 2004: 1-2.

[5] Linux-VServer, http://linux-vserver.org[Z].

[6] Dike J. A User-mode Port of the Linux Kernel[C]. Proceedings of the 4th Annual Linux Showcase & Conference, Atlanta, Georgia, USA, 2000. Atlanta, Georgia, USA: 2000: 7-16.

[7] Hoxer H J, Buchacker K, Sieh V. Implementing a User-Mode Linux with Minimal Changes from Original Kernel[C]. Proceedings of the 2002 International Linux System Technology Conference, 2002. 2002: 72-82.

[8] Jeff D. User Mode Linux[M]. Prentice Hall, 2006.