有趣的windows日历 2000 下有一个有趣...

《Undocumented Windows 2000 Secrets》翻译 --- 第二章(1) - 王朝网络 -
分享&&当前位置: &&&&&&&&《Undocumented Windows 2000 Secrets》翻译 --- 第二章(1)&&&  第二章
The Windows 2000 Native API  翻译:Kendiv  更新:Saturday, January 29, 2005  本章对于Windows 2000 Native API的讨论,主要集中在这些API和系统模块之间的关系,将重点介绍Windows 2000采用的中断机制。Windows 2000利用此机制将对内核服务的请求从用户模式向内核模式传递。另外,Win32K接口和一些与Native API相关的主要运行时库也会被提及,同时还将介绍一些经常使用的数据类型。  
有关Windows 2000架构的详细讨论已经很多。许多有关Windows NT的讨论同样适用于Windows 2000。《Inside Windows NT》(Custer 1993, Solomon 1998)的第一、二版都是有关此方面的好书,同样的还有《Inside Windows 2000》(Solomon and Russinovich 2000)。  NT*()和Zw*()函数集  
有关Windows 2000架构的一个有趣的事实是:它模拟了多个操作系统。Windows 2000内置三个子系统来支持Win32、POSIX和OS/2应用程序。Win32子系统是最流行的一个,因此它更多的被开发人员和操作系统所关照。在Windows 9x中,Win32接口实际上是作为整个系统的基础结构来实现的。但是,Windows 2000的设计却有很大不同。尽管Win32子系统包含一个名为kernel32.dll的系统模块,但这并不是实际的操作系统内核。它仅仅是Win32子系统的一个基本组件。在很多编程书籍中,Windows NT/2000的软件开发被简化为与Win32 API打交道的工作,NT平台暴露出的一个隐藏的事实是存在另一个更为基础的调用接口:Native API。相信编写kernel-mode driver或file system driver的开发人员已经对Native API非常熟悉了,因为kernel-mode模块位于更低的系统层,在那里子系统是不可见的。然而,你并不需要到驱动程序一层才能访问此接口----即使一个普通的Win32应用程序也可在任何时候调用Native API。这没什么技术上的限制----仅仅是微软不支持此种应用程序开发模式而已。因此,有关此话题的信息并不是很多,neither SDK nor the DDK make the Native API available to Win32 Application.  未文档化的级别  
本书中的多数东西都来自被称为未文档化的信息。这通常意味着微软没有公开发布这些信息。然而,未文档化也存在几个级别,这是因为可能被公布的有关庞大的操作系统(如Windows 2000)的信息非常的多。我个人的系统分类如下:  l
正式文档:这些信息来自微软出版的书、文件或者开发工具。大多数重要信息来自SDK、DDK和MSDN。  l
半文档化的(Semidocumented):尽管不是正式文档,但这些信息还是可以从微软正式发布的文件中挖掘出来的。例如,Windows 2000的很多函数和结构体并没有在SDK或DDK文档中提到,但出现在一些头文件或示列程序中。以Windows 2000为例,很多重要的半文档化信息都源自头文件ntddk.h和ntdef.h,这两个文件都是DDK的一部分。  l
未文档化,但并没有隐藏:这些信息不能在任何官方文档和开发文档中找到,但其中的一部分对调试工具是可用的。可执行文件或符号文件中的所有符号化信息都属于这一部分。最好的例子是内核调试器的!processfields和!threadfields命令,这两个命令会给出两个未文档化的结构:EPROCESS和ETHREAD的成员名称及其偏移量。  l
完全未文档化的:微软很好的隐藏了某些信息,要获得它们只能通过逆向工程和推理。此类信息包含很多实现细节的信息,没有人认为Windows 2000开发人员需要关注它们,但是这些信息对于系统开发人员和开发调试软件的人来说却非常宝贵。挖掘系统内部的信息是非常困难的,但同样是非常有趣的。  本书讨论的Windows 2000的内部细节覆盖了上述系统分类的后三个。  系统服务分配器(System Service Dispatcher)  
Win32子系统和Native API之间的关系可以由Win32核心模块与Windows 2000内核模块之间的依赖关系很好的解释。图2-1展示了模块间的依赖关系,方框表示系统模块,箭头表示模块间的依赖关系。如果一个箭头从模块A指向模块B,这表示A依赖于B,即,模块A调用B中的函数。模块由双向箭头连接,表示二者之间相互依赖。在图2-1中,模块:user32.dll、advapi32.dll、gdi32.dll、rpcrt4.dll以及kernel32.dll实现了基本的Win32 API。当然,还有其他的DLL(如version.dll、shell32.dll和comctl32.dll)也为Win32 API提供支持,为了更清晰些,我省略了它们。图2-1表现出的一个特性非常有趣,所有的Win32 API调用最后都转移到了ntdll.dll,而ntdll.dll又将其转移到了ntoskrnl.exe。  
Ntdll.dll是一个操作系统组件,它为Native API准确地提供服务,ntdll.dll是Native API在用户模式下的前端。Native API真正的接口在ntoskrnl.exe中实现。从其文件名可以猜出它就是NT操作系统内核。事实上,内核模式驱动程序对系统服务的请求多数时候都会进入该模块。Ntdll.dll的主要任务就是为运行于用户模式的程序提供一个确定的内核函数的子集,这其中就包括Win32子系统DLLs。在图2-1中,从ntdll.dll指向ntoskrnl.exe的箭头旁标注的INT 2eh表示Windows 2000使用此中断将CPU特权级从用户模式切换到内核模式。开发内核(kernel-mode)模式程序的人员认为用户模式的代码是具有攻击性的、充满错误的和危险的。因此,必须让这些代码远离内核函数。而通过在调用API的过程中将特权级别从用户模式切换到内核模式是一种可控制这些问题的方式。调用程序从来不可能触及内核,它只能察看它们。  
例如,由kernel32.dll导出的Win32 API函数DeviceIoControl()最终会调用由ntdll.dll导出的NtDeviceIoControlFile()。通过反编译该函数会发现此函数令人惊讶的实现方式—它是如此的简单!示列2-1展示了这些。首先,CPU寄存器EAX被装入了一个“魔术”数字0x38,这是一个分派ID。接下来,寄存器EDX被设置指向堆栈中的某处,其地址为堆栈指针ESP加上4,因此,EDX将指向堆栈中返回地址的后面,该返回地址在进入NtDeviceIoControlFile()时将被立即保存下来。显而易见,EDX指向的位置是用来临时存放传递进来的参数的。接下来的指令是一个简单的INT 2eh,该指令将跳转到中断描述符表(Interrupt Descriptor Table,IDT)的0x2e位置上存放的中断处理例程(interrupt handler)中。这看上去是不是很熟悉?事实上,这有些像DOS下的INT 21h 调用。然而,Windows 2000的INT 2eh接口要远比一个简单的API调用有用,分配器(dispatcher)利用它从用户模式进入内核模式。请注意,这种模式切换方式是x86处理器特有的。在Alpha平台上,有不同的方式来实现此种功能。    NtDeviceIoControlFile:  
eax, 38h  
edx, [esp+4]  
28h  示列 2-1.
ntdll.NtDeviceIoControlFile()的实现方式  
  Windows 2000 Native API由248个函数组成,这些函数都采用上述方式进入内核。与Windows NT 4.0相比多出了37个。你很容易在ntdll.dll的导出列表中通过Nt前缀来认出它们。Ntdll.dll总共导出了249个这样的符号。多出的那个函数是NtCurrentTeb(),该函数是一个纯粹的用户模式函数,它无需进入内核。附录B中的表B-1列出了所有可用的Native API。该表同时还指出那个函数是由ntoskrnl.exe导出的。令人奇怪的是,在处于内核模式的模块中,只能调用Native API的一个子集。另一方面,ntoskrnl.exe导出了两个ntdll.dll没有提供的Nt*符号(指以Nt开头的符号):NtBuildNumber和NtGlobalFlag。这两个符号都没有指向函数的入口地址,而是指向ntoskrnl.exe中的变量。驱动模块(driver module)可以使用C编译器的extern关键字来导入这些变量。Window 2000采用此种方式导出了很多变量,稍后我将给出一个示例代码来使用其中的几个。  
你可能会奇怪为什么表B-1(位于附录B中)分别为ntdll.dll和ntoskrnl.exe提供了两列,其名称分别为:ntdll.Nt*、ntdll.Zw*和ntoskrnl.Nt*、ntoskrnl.Zw*。原因是,这两个模块导出了两组相互关联的Native API符号。在表B-1(位于附录B中)的最左列给出了所有名字中包含Nt前缀的符号。另一个集合包含相似的名字,不过由Zw前缀代替了Nt。反编译ndll.dll可看出每对符号都指向相同的代码。这看起来似乎是浪费内存。然而,如果你反编译ntoskrnl.exe,你就会发现Nt*符号指向实际的代码而Zw*指向INT 2eh stubs(如示列2-1列出的)。这意味着Zw*函数集合将从用户模式转入内核模式,而Nt*符号直接指向的代码会在模式切换后被执行。  
表B-1(位于附录B中)中有两件事需要特别注意。首先,NtCurrentTeb()函数没有对应的Zw*函数。这不是什么大问题,因为ntdll.dll以相似的方式导出Nt*和Zw*函数。其次,ntoskrnl.exe不再一贯的成对的导出Nt/Zw函数。其中的一些仅以Nt*或Zw*的形式出现。我不知道为什么会这样,我猜测ntoskrnl.exe仅导出了在Windows 2000 DDK中有文档记录的函数以及其它系统模块必须的那些函数。注意,保留的Native API函数仍然实现于ntoskrnl.exe的内部。这些函数并没有公开的进入点,但可通过INT 2eh到达他们。  服务描述符表(The Service Descriptor Tables)  
从示例2-1给出的反编译代码可看出,INT 2eh随同传入CPU寄存器EAX和EDX的两个参数一起被调用。我已经提到过EAX中的“魔术”数字是一个分派ID。除NtCurrentTeb()之外的所有Native API都采用此种方式,处理INT 2eh的代码必须确定每个调用将被分配到那个函数。这就是提供分派ID的原因。位于ntoskrnl.exe中的中断处理例程将EAX中的数值作为一个索引来查询一个特定的表。这个表被称作系统服务表(System Service Table, SST)该表对应的C结构体---SYSTEM_SERVICE_TABLE的定义在列表2-1中给出。在该列表中还包含SERVICE_DESCRIPTOR_TABLE结构的定义,该结构共有四个SST类型的数组,其中的前两个用于特定目的。  
尽管上述的两个表是系统基本的数据类型,但他们在Windows 2000 DDK中并没有相应的文档记载,本书中出现的许多代码片断都包含未文档化的数据类型和函数。因此,不能保证这些信息是完全真实可信的。所有符号化的信息,如结构名称、结构成员和参数都是如此。在创建这些符号时,我试图使用适当的名称,这些名称基于从已知符号的一个很小的子集(包括从符号文件中得到的那些)中得出的命名方案。然而,在很多场合这种启发式方法并不成功。只有在原始的代码中包含所有的信息,但我无法得到它们。实际上,我并不打算阅读这些源代码,因为这需要和微软签订一个NDA(Non-Disclosure Agreement,,不可泄漏协议),由于该NDA的限制,将很难写出一本有关非文档化信息的书。    typedef NTSTATUS (NTAPI*NTPROC)();  typedef NTPROC* PNTPROC;  #define NTPROC_ sizeof(NTPROC)  typedef struct _SYSTEM_SERVICE_TABLE  {  
// array of entry points  
// array of usage counters  
// number of table entries  
// array of byte counts  }  SYSTEM_SERVICE_TABLE,  *PSYSTEM_SERVICE_TABLE,  **PPSYSTEM_SERVICE_TABLE;  //-----------------------------------------------------------------------------------------------------------  typedef struct _SERVICE_DESCRIPTOR_TABLE  {  
SYSTEM_SERVICE_TABLE
// ntoskrnl.exe ( native api )  
SYSTEM_SERVICE_TABLE win32k;
// win32k.sys (gdi/user support)  
SYSTEM_SERVICE_TABLE Table3;
// not used  
SYSTEM_SERVICE_TABLE Table4;
// not used  }  SYSTEM_DESCRIPTOR_TABLE,  *PSYSTEM_DESCRIPTOR_TABLE,  **PPSYSTEM_DESCRIPTOR_TABLE;  列表2-1 系统服务描述符表的结构定义  
现在,回到SDT(Service Descriptor Table)的秘密上来。从列表2-1给出的该结构的定义可看出该结构的头两个数组保留给了ntoskrnl.exe和Win32子系统(位于win32k.sys)中的内核模式(kernel-mode)部分。来自gdi32.dll和user32.dll的调用都通过Win32k的系统服务表(SST)进行分派。Ntolkrnl.exe导出了一个指针(符号为KeServiceDescriptorTable)指向其主服务描述符表(Main SDT)。内核还维护了一个替代的SDT,其名称为:KeServiceDescriptorTableShadow,但这个SDT并没有被导出。从处于内核模式的模块中访问主服务描述符表(SDT)非常容易,你只需要两个C指令,如列表2-2所示。首先是由extern关键字修饰的变量说明,这告诉链接器该变量并不包含在此模块中,而且不需要在链接时解析相应的符号名称。当该模块被加载到进程的地址空间后,针对该符号的引用才会动态连接到相应的模块中。列表2-2中第二个C指令就是这样的一个引用。将类型为PSERVER_DESCRIPTOR_TABLE的变量赋值为KeServiceDescriptorTable时,就会和ntoskrnl.exe建立一个动态连接。这很像调用一个DLL中的API函数。    // Import SDT pointer  extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorT  // Create SDT reference  PSERVICE_DESCRIPTOR_TABLE psdt = KeServiceDescriptorT  列表2-2
访问系统服务描述符表  
SDT中的每个SST的ServiceTable成员都是一个指针,指向一个由函数指针构成的数组,此函数指针的类型为:NTPROC,这为Native API提供了占位符,这种方式和在Win32编程中使用的PROC类型很相似。NTPROC的定义在前面的列表2-1中给出。Native API函数通常返回一个NTSTATUS类型的代码并且使用NTAPI调用方式,NTAPI实际上就是_stdcall。ServiceLimit成员保存在ServieTable数组中发现的入口地址的个数。在Windows 2000中,其默认值为248。ArgumentTable成员是一个BTYE类型的数组,它和ServiceTable所指的数组一一对应,并给出其中每个函数指针所需的参数在调用者的堆栈中的字节数。此信息随EDX寄存器提供的指针一起使用。当内核从调用者的堆栈中复制参数到自己的堆栈时就需要这些信息。CounterTable成员在Windows 2000的Free Build版中不被使用。在Debug Build版中,该成员指向一个DWORD类型的数组,作为每个函数的使用计数器(usage counters)。This information can be used for profiling purposes.  
使用Windows 2000的内核调试器可方便的显示SDT中的内容。如果你还没有设置好这个有用的程序,那请参考第一章。在示列2-2中,我首次使用了dd
KeServiceDescriptorTable命令。调试器会将此公开符号解析为0x8046AB80,同时显示该地址之后的32个DWORD的16进制转储。不过仅有前面的四行才是有意义的,它们分别对应列表2-1中的四个SDT成员。为了更清晰些,它们都将以黑体显示。如果你仔细观察,你会发现第五行与第一行十分相像,这是另一个SDT吗?这是测试内核调试器的ln命令的好机会。在示列2-2中,在显示完KeServiceDescriptorTable的十六进制dump之后,我输入ln 8046abc0命令。显然,调试器知道地址0x8046abc0,它将此地址转化为对应的符号KeServiceDescriptorTableShadow可以看出,这是内核维护的第二个SDT。二者之间的显著区别是:第二个SDT包含Win32k.sys的入口地址。这两个表的的第三和第四个成员都是空的。Ntoskrnl.exe提供了一个函数KeAddSystemServiceTabel()来填充这两个成员。  注意,我截断了ln命令的输出信息,仅保留了基本的信息。  
从地址0x8046ab88开始,是KeServiceDescriptorTable的十六进制转储,在那儿可以找到ServiceLimit成员,可看到其值为0xF8(十进制248),这和我们预期的一样。ServiceTable和ArgumentTable的值分别指向地址0x和0x804708bc。用ln命令察看着两个地址,可得到其符号:KiServiceTable和KiArgumentTable。这两个符号都没有从ntoskrnl.exe中导出,但是调试器可通过察看Windows 2000的符号文件识别它们。ln命令还可应用到Win32k SST指针上,针对其ServiceTable和ArgumentTable成员,调试器分别给出了其对应的符号w32pServiceTable和W32pArgumenTable。这两个符号都来自Win32k.sys的符号文件。如果调试器无法解析这些地址,可使用.reload命令强制重新加载所有可用符号文件,然后再进行解析。  
示例2-2的剩余部分是KiServiceTable和KiArgumentTable最前面的128个字节的十六进制转储。到目前为止,如果我说的有关Native API的东西都是正确的,那么NtClose()函数的地址应位于KiServiceTable数组的第24个位置上,其地址为0x。在该地址处,可发现其值为0x,在dd KiServiceTable的输出中,该地址以黑体标记。用ln察看0x,会看到其对应的符号正是NtClose()。    kd& dd KeServiceDescriptorTable  f8 804708bc  00   8046aba0
00   8046abb0
00   8046abc0
000f8 804708bc  8046abd0
a7f a0187840  8046abe0
00   8046abf0
00   kd& ln 8046abc0  (8046abc0)
nt!KeServiceDescriptorTableShadow
  kd& ln   ()
nt!KiServiceTable
  kd& ln 804708bc  (804708bc)
nt!KiArgumentTable
  kd& ln a0186bc0  (a0186bc0)
win32k!W32pServiceTable   kd& ln a0187840  (a0187840)
win32k!W32pArgumentTable
  kd& dd KiServiceTable  
804ab3bf 804ae86b 804bdef3   
804c11f4 0c2ff 8050c33f  
804b581c fc7e2  9c8a6 a8d50  4b6bfb 804f0cef 804fcb95 8040189a  4d06cb f69d4 8049e0cc  44c422 ab849 804aa9da  f4bd5 ca7a5  kd& db KiArgumentTable  804708bc
18 20 2c 2c 40 2c 40 44-0c 18 18 08 04 04 0c 10
. ,,@,@D........  804708cc
18 08 08 0c 08 08 04 04-04 0c 04 20 08 0c 14 0c
........... ....  804708dc
2c 10 0c 1c 20 10 38 10-14 20 24 1c 14 10 20 10
,... .8.. $... .  804708ec
34 14 08 04 04 04 0c 08-28 04 1c 18 18 18 08 18
4.......(.......  804708fc
0c 08 0c 04 10 00 0c 10-28 08 08 10 00 1c 04 08
........(.......  8047090c
0c 04 10 00 08 04 08 0c-28 10 04 0c 0c 28 24 28
........(....($(  8047091c
30 0c 0c 0c 18 0c 0c 0c-0c 30 10 0c 0c 0c 0c 10
0........0......  8047092c
10 0c 0c 14 0c 14 18 14-08 14 08 08 04 2c 1c 24
.............,.$  kd& ln   ()
nt!NtClose
  示例2-2
检查服务描述符表    译注:  
在Windows XP中,KeServiceDescriptorTable和KeServiceDescriptorTableShadow和Windows 2000有所区别。在XP中,后者位于前者的前面,而在W2K中,后者位于前者的后面。  INT 2eh系统服务处理例程(System Service Handler)  
隐藏在内核模式中的INT 2eh中断处理例程为KiSystemService()。再强调一次,这是一个内部符号,ntoskrnl.exe并没有导出该符号,不过,它却包含在Windows 2000的符号文件中。因此,内核调试器可以正确的解析该符号。从本质上来看,KiSystemService()将执行如下操作:  1.
从当前线程的控制块(thread’s control block)中检索SDT指针。  2.
通过测试EAX寄存器中的分派ID的第12、14位来确定使用SDT中的那个SST(SDT中有四个SST)。如果分派ID位于0xFFF,将选择ntoskrnl表;位于0xFFF则选择Win32k表。0xFFF和0xFFF由SDT的Table3和Table4保留。如果分配ID超过了0x3FFF,在分派前多余的位将被屏蔽掉。  3.
通过检查分派ID的0到11位来确定该ID在所选SST中对应的ServiceLimit成员。如果ID超出了范围,将返回错误代码:STATUS_INVALID_SYSTEM_SERVICE。在一个未使用的SST中,ServiceLimit成员始终是0,从而为所有可能的分派ID产生一个错误代码。  4.
通过检查EDX中保存的参数堆栈指针,来取得MmUserProbeAddress的值。这是由ntoskrnl.exe导出的一个公开变量。参数指针通常会与0x7FFF0000进行比较。如果没有低于该地址,那么将返回STATUS_ACCESS_VIOLATION。  5.
根据在SST的ArgumentTable中查找到的参数堆栈的字节数,将所有函数参数从调用者堆栈中复制到当前的内核堆栈中。  6.
在从服务调用(Service Call)中返回后,将控制权传递给内部函数KiServiceExit()  非常有趣的是INT 2eh中断处理例程并不使用全局SDT(即KeServiceDescriptorTable),而是使用线程专属的指针替代之。显然,每个线程可以拥有不同的SDT。在线程初始化时,KeInitializeThread()会将KeServiceDescriptorTable的指针写入线程控制块(Thread Control Block)中。不过,此默认值在稍后可能会改变,如改为指向KeServiceDescriptorTableShadow。    Win32 内核模式接口(Win32 Kernel-mode Interface)  
从前面对SDT的讨论,可看出存在着与Native API相关的第二个主内核模式接口(main Kernel-mode Interface)。该接口将Win32子系统的图形设备接口(Graphics Device Interface, GDI)、窗口管理器(即User模块)连接至内核组件---Win32K(即Win32k.sys).,该组件随同Windows NT 4.0引入。引入该组件是为了克服Win32图形引擎固有的性能限制(由于Windows NT子系统的最初设计导致)。在Windows NT 3.x中,Win32子系统采用的是客户-服务器模式(Client-Server model),这样就必须从用户模式切换到内核模式才能进行内核调用(Kernel Involved)。通过将图形引擎的绝大部分移至内核组件---Win32k.sys,从而避免了大部分因内核切换导致的性能损失。  Win32K分派ID(Win32K Dispatch IDs)  
现在该介绍Win32k.sys了,也是该更新图2-1的时候了。图2-2基于图2-1,但在ntoskrnl.exe左面加入了Win32k.sys。同时我还加入了从GDI32.DLL和USER32.DLL指向Win32k.sys的箭头。当然,这不是百分之百正确,因为这些模块中的INT 2eh调用实际上指向ntoskrnl.exe,在ntoskrnl.exe中才有该中断的处理例程。然而,调用最后还是由Win32k.sys管理,这也是箭头这样指的原因。  
稍早提到过,Win32K接口同样基于INT 2eh分派器(INT 2eh Dispatcher),这与Native API非常相似。仅有的区别在于Win32K使用另一区段的分派ID。尽管与所有Native API调用相关的分派ID都位于0x0000----0x0FFF,而Win32K分派ID位于0x1000---0x1FFF之间。如图2-2所示,Win32K的主要客户端是GDI32.DLL和USER32.DLL。因此,通过反编译这些模块(指gdi32.dll和user32.dll)可能会找到与Win32K分派ID相关的符号化名称。通过反编译可发现在这些模块(gdi32.dll和user32.dll)的导出节(export sections)中仅包含INT 2eh调用的一个很小的子集,看来是时候再次使用内核调试器了。如示例2-3所示,我通过使用dd W32pServiceTable命令,来确定Win32k.sys的符号是可用的,在此之前请先使用.reload命令以加载所有可用符号文件。  
在示例2-3的最后三行中,我使用ln命令显示与W32pServiceTable的第一个入口地址相关的符号。显然,可看到分派ID为0的Win32K函数为NtGdiAbortDoc()。你可以针对所有639个ID来重复此过程,但是最好能自动进行符号的查找。现在,我已经为你完成了这项工作,所有分派ID对应的符号名称都收录在附录B的表B-2中。符号从gdi32.dll和user32.dll映射到win32k.sys十分简单:GDI符号可通过在其前面添加NtGdi前缀就可转换为Win32K符号,USER符号则添加NtUser前缀。然而,有一少部分例外。例如,如果一个GDI符号以Gdi开始,那么其前缀就减少为Nt,这可能是为了避免出现NtGdiGdi这样的字符序列。在其他的一些例子中,字符的大小写会有些不同(比如EnableEUDC()转化后则变成了NtGdiEnableEudc()),或者用符号名称尾部的W来表示没有对应的Unicode函数(如,CopyAcceleratorTableW()转化后成为NtUserCopyAcceleratorTable())。  
提供Win32K API的详细文档需要很大的努力。这些函数几乎是Native API的三倍。或许某天有人会为这些API编写一本不错的参考手册,就像Gary Nebbett编写的Native API手册。不过,在本书范围内,有关这些API的信息已经足够了。&&&&&&&今日推荐&&第二章
The Windows 2000 Native API
翻译:Kendiv
更新:Saturday, January 29, 2005
本章对于Windows 2000 Native API的讨论,主要集中在这些API和系统模块之间的关系,将重点介绍Windows 2000采用的中断机制。Windows 2000利用此机制将对内核服务的请求从用户模式向内核模式传递。另外,Win32K接口和一些与Native API相关的主要运行时库也会被提及,同时还将介绍一些经常使用的数据类型。
有关Windows 2000架构的详细讨论已经很多。许多有关Windows NT的讨论同样适用于Windows 2000。《Inside Windows NT》(Custer 1993, Solomon 1998)的第一、二版都是有关此方面的好书,同样的还有《Inside Windows 2000》(Solomon and Russinovich 2000)。
NT*()和Zw*()函数集
有关Windows 2000架构的一个有趣的事实是:它模拟了多个操作系统。Windows 2000内置三个子系统来支持Win32、POSIX和OS/2应用程序。Win32子系统是最流行的一个,因此它更多的被开发人员和操作系统所关照。在Windows 9x中,Win32接口实际上是作为整个系统的基础结构来实现的。但是,Windows 2000的设计却有很大不同。尽管Win32子系统包含一个名为kernel32.dll的系统模块,但这并不是实际的操作系统内核。它仅仅是Win32子系统的一个基本组件。在很多编程书籍中,Windows NT/2000的软件开发被简化为与Win32 API打交道的工作,NT平台暴露出的一个隐藏的事实是存在另一个更为基础的调用接口:Native API。相信编写kernel-mode driver或file system driver的开发人员已经对Native API非常熟悉了,因为kernel-mode模块位于更低的系统层,在那里子系统是不可见的。然而,你并不需要到驱动程序一层才能访问此接口----即使一个普通的Win32应用程序也可在任何时候调用Native API。这没什么技术上的限制----仅仅是微软不支持此种应用程序开发模式而已。因此,有关此话题的信息并不是很多,neither SDK nor the DDK make the Native API available to Win32 Application.
未文档化的级别
本书中的多数东西都来自被称为未文档化的信息。这通常意味着微软没有公开发布这些信息。然而,未文档化也存在几个级别,这是因为可能被公布的有关庞大的操作系统(如Windows 2000)的信息非常的多。我个人的系统分类如下:
正式文档:这些信息来自微软出版的书、文件或者开发工具。大多数重要信息来自SDK、DDK和MSDN。
半文档化的(Semidocumented):尽管不是正式文档,但这些信息还是可以从微软正式发布的文件中挖掘出来的。例如,Windows 2000的很多函数和结构体并没有在SDK或DDK文档中提到,但出现在一些头文件或示列程序中。以Windows 2000为例,很多重要的半文档化信息都源自头文件ntddk.h和ntdef.h,这两个文件都是DDK的一部分。
未文档化,但并没有隐藏:这些信息不能在任何官方文档和开发文档中找到,但其中的一部分对调试工具是可用的。可执行文件或符号文件中的所有符号化信息都属于这一部分。最好的例子是内核调试器的!processfields和!threadfields命令,这两个命令会给出两个未文档化的结构:EPROCESS和ETHREAD的成员名称及其偏移量。
完全未文档化的:微软很好的隐藏了某些信息,要获得它们只能通过逆向工程和推理。此类信息包含很多实现细节的信息,没有人认为Windows 2000开发人员需要关注它们,但是这些信息对于系统开发人员和开发调试软件的人来说却非常宝贵。挖掘系统内部的信息是非常困难的,但同样是非常有趣的。
本书讨论的Windows 2000的内部细节覆盖了上述系统分类的后三个。
系统服务分配器(System Service Dispatcher)
Win32子系统和Native API之间的关系可以由Win32核心模块与Windows 2000内核模块之间的依赖关系很好的解释。图2-1展示了模块间的依赖关系,方框表示系统模块,箭头表示模块间的依赖关系。如果一个箭头从模块A指向模块B,这表示A依赖于B,即,模块A调用B中的函数。模块由双向箭头连接,表示二者之间相互依赖。在图2-1中,模块:user32.dll、advapi32.dll、gdi32.dll、rpcrt4.dll以及kernel32.dll实现了基本的Win32 API。当然,还有其他的DLL(如version.dll、shell32.dll和comctl32.dll)也为Win32 API提供支持,为了更清晰些,我省略了它们。图2-1表现出的一个特性非常有趣,所有的Win32 API调用最后都转移到了ntdll.dll,而ntdll.dll又将其转移到了ntoskrnl.exe。
Ntdll.dll是一个操作系统组件,它为Native API准确地提供服务,ntdll.dll是Native API在用户模式下的前端。Native API真正的接口在ntoskrnl.exe中实现。从其文件名可以猜出它就是NT操作系统内核。事实上,内核模式驱动程序对系统服务的请求多数时候都会进入该模块。Ntdll.dll的主要任务就是为运行于用户模式的程序提供一个确定的内核函数的子集,这其中就包括Win32子系统DLLs。在图2-1中,从ntdll.dll指向ntoskrnl.exe的箭头旁标注的INT 2eh表示Windows 2000使用此中断将CPU特权级从用户模式切换到内核模式。开发内核(kernel-mode)模式程序的人员认为用户模式的代码是具有攻击性的、充满错误的和危险的。因此,必须让这些代码远离内核函数。而通过在调用API的过程中将特权级别从用户模式切换到内核模式是一种可控制这些问题的方式。调用程序从来不可能触及内核,它只能察看它们。
[url=http://www./bbsdetail_60454.html][img]http://blog.csdn.net/images/blog_csdn_net/kendiv/87093/r_2-1.JPG[/img][/url]
例如,由kernel32.dll导出的Win32 API函数DeviceIoControl()最终会调用由ntdll.dll导出的NtDeviceIoControlFile()。通过反编译该函数会发现此函数令人惊讶的实现方式—它是如此的简单!示列2-1展示了这些。首先,CPU寄存器EAX被装入了一个“魔术”数字0x38,这是一个分派ID。接下来,寄存器EDX被设置指向堆栈中的某处,其地址为堆栈指针ESP加上4,因此,EDX将指向堆栈中返回地址的后面,该返回地址在进入NtDeviceIoControlFile()时将被立即保存下来。显而易见,EDX指向的位置是用来临时存放传递进来的参数的。接下来的指令是一个简单的INT 2eh,该指令将跳转到中断描述符表(Interrupt Descriptor Table,IDT)的0x2e位置上存放的中断处理例程(interrupt handler)中。这看上去是不是很熟悉?事实上,这有些像DOS下的INT 21h 调用。然而,Windows 2000的INT 2eh接口要远比一个简单的API调用有用,分配器(dispatcher)利用它从用户模式进入内核模式。请注意,这种模式切换方式是x86处理器特有的。在Alpha平台上,有不同的方式来实现此种功能。
NtDeviceIoControlFile:
edx, [esp+4]
ntdll.NtDeviceIoControlFile()的实现方式
Windows 2000 Native API由248个函数组成,这些函数都采用上述方式进入内核。与Windows NT 4.0相比多出了37个。你很容易在ntdll.dll的导出列表中通过Nt前缀来认出它们。Ntdll.dll总共导出了249个这样的符号。多出的那个函数是NtCurrentTeb(),该函数是一个纯粹的用户模式函数,它无需进入内核。附录B中的表B-1列出了所有可用的Native API。该表同时还指出那个函数是由ntoskrnl.exe导出的。令人奇怪的是,在处于内核模式的模块中,只能调用Native API的一个子集。另一方面,ntoskrnl.exe导出了两个ntdll.dll没有提供的Nt*符号(指以Nt开头的符号):NtBuildNumber和NtGlobalFlag。这两个符号都没有指向函数的入口地址,而是指向ntoskrnl.exe中的变量。驱动模块(driver module)可以使用C编译器的extern关键字来导入这些变量。Window 2000采用此种方式导出了很多变量,稍后我将给出一个示例代码来使用其中的几个。
你可能会奇怪为什么表B-1(位于附录B中)分别为ntdll.dll和ntoskrnl.exe提供了两列,其名称分别为:ntdll.Nt*、ntdll.Zw*和ntoskrnl.Nt*、ntoskrnl.Zw*。原因是,这两个模块导出了两组相互关联的Native API符号。在表B-1(位于附录B中)的最左列给出了所有名字中包含Nt前缀的符号。另一个集合包含相似的名字,不过由Zw前缀代替了Nt。反编译ndll.dll可看出每对符号都指向相同的代码。这看起来似乎是浪费内存。然而,如果你反编译ntoskrnl.exe,你就会发现Nt*符号指向实际的代码而Zw*指向INT 2eh stubs(如示列2-1列出的)。这意味着Zw*函数集合将从用户模式转入内核模式,而Nt*符号直接指向的代码会在模式切换后被执行。
表B-1(位于附录B中)中有两件事需要特别注意。首先,NtCurrentTeb()函数没有对应的Zw*函数。这不是什么大问题,因为ntdll.dll以相似的方式导出Nt*和Zw*函数。其次,ntoskrnl.exe不再一贯的成对的导出Nt/Zw函数。其中的一些仅以Nt*或Zw*的形式出现。我不知道为什么会这样,我猜测ntoskrnl.exe仅导出了在Windows 2000 DDK中有文档记录的函数以及其它系统模块必须的那些函数。注意,保留的Native API函数仍然实现于ntoskrnl.exe的内部。这些函数并没有公开的进入点,但可通过INT 2eh到达他们。
服务描述符表(The Service Descriptor Tables)
从示例2-1给出的反编译代码可看出,INT 2eh随同传入CPU寄存器EAX和EDX的两个参数一起被调用。我已经提到过EAX中的“魔术”数字是一个分派ID。除NtCurrentTeb()之外的所有Native API都采用此种方式,处理INT 2eh的代码必须确定每个调用将被分配到那个函数。这就是提供分派ID的原因。位于ntoskrnl.exe中的中断处理例程将EAX中的数值作为一个索引来查询一个特定的表。这个表被称作系统服务表(System Service Table, SST)该表对应的C结构体---SYSTEM_SERVICE_TABLE的定义在列表2-1中给出。在该列表中还包含SERVICE_DESCRIPTOR_TABLE结构的定义,该结构共有四个SST类型的数组,其中的前两个用于特定目的。
尽管上述的两个表是系统基本的数据类型,但他们在Windows 2000 DDK中并没有相应的文档记载,本书中出现的许多代码片断都包含未文档化的数据类型和函数。因此,不能保证这些信息是完全真实可信的。所有符号化的信息,如结构名称、结构成员和参数都是如此。在创建这些符号时,我试图使用适当的名称,这些名称基于从已知符号的一个很小的子集(包括从符号文件中得到的那些)中得出的命名方案。然而,在很多场合这种启发式方法并不成功。只有在原始的代码中包含所有的信息,但我无法得到它们。实际上,我并不打算阅读这些源代码,因为这需要和微软签订一个NDA(Non-Disclosure Agreement,,不可泄漏协议),由于该NDA的限制,将很难写出一本有关非文档化信息的书。
typedef NTSTATUS (NTAPI*NTPROC)();
typedef NTPROC* PNTPROC;
#define NTPROC_ sizeof(NTPROC)
typedef struct _SYSTEM_SERVICE_TABLE
// array of entry points
// array of usage counters
// number of table entries
// array of byte counts
SYSTEM_SERVICE_TABLE,
*PSYSTEM_SERVICE_TABLE,
**PPSYSTEM_SERVICE_TABLE;
//-----------------------------------------------------------------------------------------------------------
typedef struct _SERVICE_DESCRIPTOR_TABLE
SYSTEM_SERVICE_TABLE
// ntoskrnl.exe ( native api )
SYSTEM_SERVICE_TABLE win32k;
// win32k.sys (gdi/user support)
SYSTEM_SERVICE_TABLE Table3;
// not used
SYSTEM_SERVICE_TABLE Table4;
// not used
SYSTEM_DESCRIPTOR_TABLE,
*PSYSTEM_DESCRIPTOR_TABLE,
**PPSYSTEM_DESCRIPTOR_TABLE;
列表2-1 系统服务描述符表的结构定义
现在,回到SDT(Service Descriptor Table)的秘密上来。从列表2-1给出的该结构的定义可看出该结构的头两个数组保留给了ntoskrnl.exe和Win32子系统(位于win32k.sys)中的内核模式(kernel-mode)部分。来自gdi32.dll和user32.dll的调用都通过Win32k的系统服务表(SST)进行分派。Ntolkrnl.exe导出了一个指针(符号为KeServiceDescriptorTable)指向其主服务描述符表(Main SDT)。内核还维护了一个替代的SDT,其名称为:KeServiceDescriptorTableShadow,但这个SDT并没有被导出。从处于内核模式的模块中访问主服务描述符表(SDT)非常容易,你只需要两个C指令,如列表2-2所示。首先是由extern关键字修饰的变量说明,这告诉链接器该变量并不包含在此模块中,而且不需要在链接时解析相应的符号名称。当该模块被加载到进程的地址空间后,针对该符号的引用才会动态连接到相应的模块中。列表2-2中第二个C指令就是这样的一个引用。将类型为PSERVER_DESCRIPTOR_TABLE的变量赋值为KeServiceDescriptorTable时,就会和ntoskrnl.exe建立一个动态连接。这很像调用一个DLL中的API函数。
// Import SDT pointer
extern PSERVICE_DESCRIPTOR_TABLE KeServiceDescriptorT
// Create SDT reference
PSERVICE_DESCRIPTOR_TABLE psdt = KeServiceDescriptorT
访问系统服务描述符表
SDT中的每个SST的ServiceTable成员都是一个指针,指向一个由函数指针构成的数组,此函数指针的类型为:NTPROC,这为Native API提供了占位符,这种方式和在Win32编程中使用的PROC类型很相似。NTPROC的定义在前面的列表2-1中给出。Native API函数通常返回一个NTSTATUS类型的代码并且使用NTAPI调用方式,NTAPI实际上就是_stdcall。ServiceLimit成员保存在ServieTable数组中发现的入口地址的个数。在Windows 2000中,其默认值为248。ArgumentTable成员是一个BTYE类型的数组,它和ServiceTable所指的数组一一对应,并给出其中每个函数指针所需的参数在调用者的堆栈中的字节数。此信息随EDX寄存器提供的指针一起使用。当内核从调用者的堆栈中复制参数到自己的堆栈时就需要这些信息。CounterTable成员在Windows 2000的Free Build版中不被使用。在Debug Build版中,该成员指向一个DWORD类型的数组,作为每个函数的使用计数器(usage counters)。This information can be used for profiling purposes.
使用Windows 2000的内核调试器可方便的显示SDT中的内容。如果你还没有设置好这个有用的程序,那请参考第一章。在示列2-2中,我首次使用了dd
KeServiceDescriptorTable命令。调试器会将此公开符号解析为0x8046AB80,同时显示该地址之后的32个DWORD的16进制转储。不过仅有前面的四行才是有意义的,它们分别对应列表2-1中的四个SDT成员。为了更清晰些,它们都将以黑体显示。如果你仔细观察,你会发现第五行与第一行十分相像,这是另一个SDT吗?这是测试内核调试器的ln命令的好机会。在示列2-2中,在显示完KeServiceDescriptorTable的十六进制dump之后,我输入ln 8046abc0命令。显然,调试器知道地址0x8046abc0,它将此地址转化为对应的符号KeServiceDescriptorTableShadow可以看出,这是内核维护的第二个SDT。二者之间的显著区别是:第二个SDT包含Win32k.sys的入口地址。这两个表的的第三和第四个成员都是空的。Ntoskrnl.exe提供了一个函数KeAddSystemServiceTabel()来填充这两个成员。
注意,我截断了ln命令的输出信息,仅保留了基本的信息。
从地址0x8046ab88开始,是KeServiceDescriptorTable的十六进制转储,在那儿可以找到ServiceLimit成员,可看到其值为0xF8(十进制248),这和我们预期的一样。ServiceTable和ArgumentTable的值分别指向地址0x和0x804708bc。用ln命令察看着两个地址,可得到其符号:KiServiceTable和KiArgumentTable。这两个符号都没有从ntoskrnl.exe中导出,但是调试器可通过察看Windows 2000的符号文件识别它们。ln命令还可应用到Win32k SST指针上,针对其ServiceTable和ArgumentTable成员,调试器分别给出了其对应的符号w32pServiceTable和W32pArgumenTable。这两个符号都来自Win32k.sys的符号文件。如果调试器无法解析这些地址,可使用.reload命令强制重新加载所有可用符号文件,然后再进行解析。
示例2-2的剩余部分是KiServiceTable和KiArgumentTable最前面的128个字节的十六进制转储。到目前为止,如果我说的有关Native API的东西都是正确的,那么NtClose()函数的地址应位于KiServiceTable数组的第24个位置上,其地址为0x。在该地址处,可发现其值为0x,在dd KiServiceTable的输出中,该地址以黑体标记。用ln察看0x,会看到其对应的符号正是NtClose()。
kd& dd KeServiceDescriptorTable
f8 804708bc
000f8 804708bc
a7f a0187840
kd& ln 8046abc0
(8046abc0)
nt!KeServiceDescriptorTableShadow
nt!KiServiceTable
kd& ln 804708bc
(804708bc)
nt!KiArgumentTable
kd& ln a0186bc0
(a0186bc0)
win32k!W32pServiceTable
kd& ln a0187840
(a0187840)
win32k!W32pArgumentTable
kd& dd KiServiceTable
804ab3bf 804ae86b 804bdef3
804c11f4 0c2ff 8050c33f
804b581c fc7e2
9c8a6 a8d50
4b6bfb 804f0cef 804fcb95 8040189a
4d06cb f69d4 8049e0cc
44c422 ab849 804aa9da
f4bd5 ca7a5
kd& db KiArgumentTable
18 20 2c 2c 40 2c 40 44-0c 18 18 08 04 04 0c 10
. ,,@,@D........
18 08 08 0c 08 08 04 04-04 0c 04 20 08 0c 14 0c
........... ....
2c 10 0c 1c 20 10 38 10-14 20 24 1c 14 10 20 10
,... .8.. $... .
34 14 08 04 04 04 0c 08-28 04 1c 18 18 18 08 18
4.......(.......
0c 08 0c 04 10 00 0c 10-28 08 08 10 00 1c 04 08
........(.......
0c 04 10 00 08 04 08 0c-28 10 04 0c 0c 28 24 28
........(....($(
30 0c 0c 0c 18 0c 0c 0c-0c 30 10 0c 0c 0c 0c 10
0........0......
10 0c 0c 14 0c 14 18 14-08 14 08 08 04 2c 1c 24
.............,.$
nt!NtClose
检查服务描述符表
在Windows XP中,KeServiceDescriptorTable和KeServiceDescriptorTableShadow和Windows 2000有所区别。在XP中,后者位于前者的前面,而在W2K中,后者位于前者的后面。
INT 2eh系统服务处理例程(System Service Handler)
隐藏在内核模式中的INT 2eh中断处理例程为KiSystemService()。再强调一次,这是一个内部符号,ntoskrnl.exe并没有导出该符号,不过,它却包含在Windows 2000的符号文件中。因此,内核调试器可以正确的解析该符号。从本质上来看,KiSystemService()将执行如下操作:
从当前线程的控制块(thread’s control block)中检索SDT指针。
通过测试EAX寄存器中的分派ID的第12、14位来确定使用SDT中的那个SST(SDT中有四个SST)。如果分派ID位于0xFFF,将选择ntoskrnl表;位于0xFFF则选择Win32k表。0xFFF和0xFFF由SDT的Table3和Table4保留。如果分配ID超过了0x3FFF,在分派前多余的位将被屏蔽掉。
通过检查分派ID的0到11位来确定该ID在所选SST中对应的ServiceLimit成员。如果ID超出了范围,将返回错误代码:STATUS_INVALID_SYSTEM_SERVICE。在一个未使用的SST中,ServiceLimit成员始终是0,从而为所有可能的分派ID产生一个错误代码。
通过检查EDX中保存的参数堆栈指针,来取得MmUserProbeAddress的值。这是由ntoskrnl.exe导出的一个公开变量。参数指针通常会与0x7FFF0000进行比较。如果没有低于该地址,那么将返回STATUS_ACCESS_VIOLATION。
根据在SST的ArgumentTable中查找到的参数堆栈的字节数,将所有函数参数从调用者堆栈中复制到当前的内核堆栈中。
在从服务调用(Service Call)中返回后,将控制权传递给内部函数KiServiceExit()
非常有趣的是INT 2eh中断处理例程并不使用全局SDT(即KeServiceDescriptorTable),而是使用线程专属的指针替代之。显然,每个线程可以拥有不同的SDT。在线程初始化时,KeInitializeThread()会将KeServiceDescriptorTable的指针写入线程控制块(Thread Control Block)中。不过,此默认值在稍后可能会改变,如改为指向KeServiceDescriptorTableShadow。
Win32 内核模式接口(Win32 Kernel-mode Interface)
从前面对SDT的讨论,可看出存在着与Native API相关的第二个主内核模式接口(main Kernel-mode Interface)。该接口将Win32子系统的图形设备接口(Graphics Device Interface, GDI)、窗口管理器(即User模块)连接至内核组件---Win32K(即Win32k.sys).,该组件随同Windows NT 4.0引入。引入该组件是为了克服Win32图形引擎固有的性能限制(由于Windows NT子系统的最初设计导致)。在Windows NT 3.x中,Win32子系统采用的是客户-服务器模式(Client-Server model),这样就必须从用户模式切换到内核模式才能进行内核调用(Kernel Involved)。通过将图形引擎的绝大部分移至内核组件---Win32k.sys,从而避免了大部分因内核切换导致的性能损失。
Win32K分派ID(Win32K Dispatch IDs)
现在该介绍Win32k.sys了,也是该更新图2-1的时候了。图2-2基于图2-1,但在ntoskrnl.exe左面加入了Win32k.sys。同时我还加入了从GDI32.DLL和USER32.DLL指向Win32k.sys的箭头。当然,这不是百分之百正确,因为这些模块中的INT 2eh调用实际上指向ntoskrnl.exe,在ntoskrnl.exe中才有该中断的处理例程。然而,调用最后还是由Win32k.sys管理,这也是箭头这样指的原因。
[url=http://www./bbsdetail_60454.html][img]http://blog.csdn.net/images/blog_csdn_net/kendiv/87093/r_2-2.JPG[/img][/url]
稍早提到过,Win32K接口同样基于INT 2eh分派器(INT 2eh Dispatcher),这与Native API非常相似。仅有的区别在于Win32K使用另一区段的分派ID。尽管与所有Native API调用相关的分派ID都位于0x0000----0x0FFF,而Win32K分派ID位于0x1000---0x1FFF之间。如图2-2所示,Win32K的主要客户端是GDI32.DLL和USER32.DLL。因此,通过反编译这些模块(指gdi32.dll和user32.dll)可能会找到与Win32K分派ID相关的符号化名称。通过反编译可发现在这些模块(gdi32.dll和user32.dll)的导出节(export sections)中仅包含INT 2eh调用的一个很小的子集,看来是时候再次使用内核调试器了。如示例2-3所示,我通过使用dd W32pServiceTable命令,来确定Win32k.sys的符号是可用的,在此之前请先使用.reload命令以加载所有可用符号文件。
[url=http://www./bbsdetail_60454.html][img]http://blog.csdn.net/images/blog_csdn_net/kendiv/87093/r_examp2-3.JPG[/img][/url]
在示例2-3的最后三行中,我使用ln命令显示与W32pServiceTable的第一个入口地址相关的符号。显然,可看到分派ID为0的Win32K函数为NtGdiAbortDoc()。你可以针对所有639个ID来重复此过程,但是最好能自动进行符号的查找。现在,我已经为你完成了这项工作,所有分派ID对应的符号名称都收录在附录B的表B-2中。符号从gdi32.dll和user32.dll映射到win32k.sys十分简单:GDI符号可通过在其前面添加NtGdi前缀就可转换为Win32K符号,USER符号则添加NtUser前缀。然而,有一少部分例外。例如,如果一个GDI符号以Gdi开始,那么其前缀就减少为Nt,这可能是为了避免出现NtGdiGdi这样的字符序列。在其他的一些例子中,字符的大小写会有些不同(比如EnableEUDC()转化后则变成了NtGdiEnableEudc()),或者用符号名称尾部的W来表示没有对应的Unicode函数(如,CopyAcceleratorTableW()转化后成为NtUserCopyAcceleratorTable())。
提供Win32K API的详细文档需要很大的努力。这些函数几乎是Native API的三倍。或许某天有人会为这些API编写一本不错的参考手册,就像Gary Nebbett编写的Native API手册。不过,在本书范围内,有关这些API的信息已经足够了。&  免责声明:本文仅代表作者个人观点,与王朝网络无关。王朝网络登载此文出于传递更多信息之目的,并不意味着赞同其观点或证实其描述,其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。&&&日版宠物情人2017的插曲,很带节奏感,日语的,女生唱的。
最后听见是在第8集的时候女主手割伤了,然后男主用嘴帮她吸了一下,插曲就出来了。
歌手:Def...老钟家的两个儿子很特别,就是跟其他的人不太一样,魔一般的执着。兄弟俩都到了要结婚的年龄了,不管自家老爹怎么磨破嘴皮子,兄弟俩说不娶就不娶,老父母为兄弟两操碎了心...把牛仔裤磨出有线的破洞
1、具体工具就是磨脚石,下面垫一个硬物,然后用磨脚石一直磨一直磨,到把那块磨薄了,用手撕开就好了。出来的洞啊很自然的。需要猫须的话调几...先来看下敬业福和爱国福
今年春节,支付宝再次推出了“五福红包”活动,表示要“把欠大家的敬业福都还给大家”。
今天该活动正式启动,和去年一样,需要收集“五福”...有时候我们打开冰箱就会闻到一股异味,冰箱里的这种异味是因为一些物质发出的气味的混合体,闻起来让人恶心。 产生这些异味的主要原因有以下几点。
1、很多人有这种习...?&?&?&?&&&&&为你推荐&&&转载本文&UBB代码&HTML代码复制到剪贴板...&更多内容??????????&&&热帖排行&&&频道精选&微信扫码关注本站微信公众号 wangchaonetcn&王朝女性&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&王朝分栏&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&王朝编程&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&王朝导购&&|&&|&&|&&|&&|&&|&&|&&|&&|&&|&王朝其他&&|&&|&&|&&|&&|&&|&&&&2005-&&版权所有&

参考资料

 

随机推荐