显示当前进程DLL加载信息的系统调用
经过前面对一些研究背景,windows进程相关数据结构,windows DLL基本原理的介绍,本文的读者将会清楚的理解接下来将要做的:写一个显示当前进程DLL的加载信息的windows系统调用。这部分内容主要分3步进行,如下:
1. 编写调用该系统调用的应用程序。
为了方便验证我们即将写的系统调用显示了当前进程的所有DLL信息,所以在这里我
首先要写一个自己的DLL文件,再写一个加载该DLL的简单应用程序。
(1)在VC6.0里我们新建一个Win32 Dynamic-Link Library工程,就叫Mydll吧。新建一个文件C++ 源文件,也就叫Mydll.cpp吧。代码如下:
- #include "windows.h"
- extern "C" _declspec(dllexport)int add(int a,int b)
- {
- return a+b;
- }
这个DLL中只有一个导出函数add,实现两个整数相加。编译,连接后在该项目的Debug目录下会产生Mydll.dll,Mydll.lib,Mydll.exp三个文件。
(2)在VC6.0中新建一个Win32 Console Application工程,就叫RecordDll吧。新建一个RecordDll.c文件。代码如下:
- #include<stdio.h>
- #include<stdlib.h>
- extern _declspec(dllexport) int add(int a,int b);
- #pragma comment(lib,"Mydll.lib") //隐式加载DLL
- int main()
- {
- int v1=1,v2=2;
- int v3=0;
- v3=add(v1,v2);
- _asm{
- mov eax,297 //297为系统调用号,将系统调用号放入eax寄存器
- int 0x2E //陷入系统调用
- }
- printf("1+2=%d",v3);
- system("pause");//暂停进程
- }
将Mydll.lib文件拷贝到RecordDll工程目录下,进行编译,连接。第三,四行将为隐式加载Dll。其中嵌入了两行汇编代码:mov eax,297 297为系统调用号,将系统调用号放入eax寄存器中,int 0x2E 处理器执行int 0x2E指令来激活Windows系统服务调用。最后一行的system("pause");就是将当前这个简单的进程暂停,方便之后用某些观察DLL加载信息的小工具来验证我们的系统调用。
到此,我们已经将应用程序编写好了,将RecordDll.exe和Mydll.dll保存好,后面将会用到。
2.编写记录DLL加载信息的系统调用
在这个步骤里将详细介绍编写一个windows系统调用的步骤。
(1) 首先在源代码中找到systable.asm文件(我选的是i386文件夹下的那个,还有一个amd64的)。在第393行加入一行代码:TABLE_ENTRY RecordDll ,0 ,0 两个0分别代表没有参数,参数个数为0。并将下面的TABLE_END 296加一改成297。
(2) 其次在源代码中找到psimpers.c文件(当然在别的文件中也行,但是要注意相应的头文件是否包含)加入我们自己写的:
- NTSTATUS
- NtRecordDll()
- {
- NTSTATUS Status = STATUS_SUCCESS;
- PEPROCESS myProcess;
- PPEB myPeb;
- PPEB_LDR_DATA myLdr;
- PLIST_ENTRY ListHead=NULL,Current=NULL;
- PLDR_DATA_TABLE_ENTRY Entry=NULL;
- PAGED_CODE();
- DbgPrint("*********Doing*********/n/n");
- myProcess=PsGetCurrentProcess();
- myPeb=myProcess->Peb;
- myLdr=myPeb->Ldr;
- ListHead=&myLdr->InInitializationOrderModuleList;
- Current=ListHead->Flink;
- DbgPrint("Now is the InInitializationOrderModuleList/n/n");
- while(Current!=ListHead)
- {
- Entry = CONTAINING_RECORD( Current, LDR_DATA_TABLE_ENTRY, InInitializationOrderLinks);
- DbgPrint("FullDllName: %ws/n/n",Entry->FullDllName.Buffer);
- DbgPrint("BaseDllName: %ws/n/n",Entry->BaseDllName.Buffer);
- DbgPrint("DllBase: 0x%X/n/n",Entry->DllBase);
- DbgPrint("SizeOfImage: %d/n/n",Entry->SizeOfImage);
- DbgPrint("LoadCount: %d/n/n",Entry->LoadCount);
- DbgPrint("EntryPoint: 0x%X/n/n/n/n/n",Entry->EntryPoint);
- Current=Entry->InInitializationOrderLinks.Flink;
- }
- return Status ;
- }
190行使用PsGetCurrentProcess();函数返回当前进程。191行直接通过当前进程的指针获得当前进程的PEB数据结构,192行PEB中有一个_PEB_LDR_DATA结构指针,该数据结构为本进程维护着3个模块链表即InLoadOrderModuleList、InMemoryOrderModuleList、InInitializationOrderModuleList所谓“模块”就是PE格式的可执行映像,包括EXE映像和DLL映像。前两个是模块队列,内容相同,只不过一个是按照在内存中的顺序排列,一个是按照加载顺序排列。第三个是初始化队列,每当一个进程装入一个模块,即EXE映像或DLL映像时,就要为其分配一个LDR_DATA_TABLE_ENTRY数据结构,并立刻将其挂入InLoadOrderModuleList队列。完成动态连接之后,再将其挂入InInitializationOrderModuleList队列,以便一次调用它们的初始化函数。相应的LDR_DATA_TABLE_ENTRY结构中有三个队列的队列头,因而可以方便挂在三个队列中。193行将InInitializationOrderModuleList队列的队列头得到,194行,将队列的第一个结点元素的指针得到。197行到208行,遍历这个队列的每一个LDR_DATA_TABLE_ENTRY数据结构,将其中加载的DLL相关信息显示出来,比如有:DLL的全名(FullDllName),DLL的文件名(BaseDllName),DLL映像在该进程虚拟空间中的地址(DllBase),DLL映像大小(SizeOfSection)等等。最后该系统调用函数返回STATUS_SUCCESS。
3编译,替换新内核,在Windbg中观察实验结果。
(1) 在C:/WRK-v1.2/base/ntos目录下输入命令:nmake x86= ,会自动编译刚刚写好的新内核。生成的新内核是C:/WRK-v1.2/base/ntos/BUILD/EXE下的 wrkx86.exe
(2) 启动虚拟机Windows 2003 server,将物理机上刚刚编译好的新内核wrkx86.exe覆盖到虚拟机上的C:/WINDOWS/system32目录下。顺便也将我们最开始写的RecordDll.exe和Mydll.dll一起从物理机上拷贝到虚拟机的桌面上,关闭虚拟机。
(3) 打开Windbg,进入Kernel Debug模式。再次启动虚拟机Windows 2003 server Debug版。
这时候程序暂停。我们再来看看Windbg中的显示结果:
这个结果显示我们这个小程序只加载了3个DLL,他们分别为:ntdll.dll,kernel32.dll,Mydll.dll。并且他们在进程虚拟空间的起始地址为分别为:0x7C800000,0x77E40000,0x10000000。至此验证我们的系统调用功能实现成功!
实验总结
通过这次实验,我熟悉了Windows内核描述进程的相关数据结构,了解了Windows Dll的基本使用原理,也明确的知道了Windows Dll在一个进程中是如何描述的。但是知道这些也是不够的,对于我们的最终目的:写一个系统调用,将进程的运行时状态保存在文件中,必要的时候可以还原回去并继续执行。我最好是能完全弄懂Windows Dll的加载过程,在将一个进程记录到文件里时,不需要将DLL的内容写出去而是记录几个Dll相关信息就行。就我目前所了解到的:Windows DLL(除了ntdll.dll之外)都是通过ntdll.dll中的LdrInitializeThunk这个函数实现的。但是目前微软还没有公开这部分代码,所以给我们的学习带来的不便。网上也有一些零零散散的LdrInitializeThunk相关资料,我会继续学习下去。