1、从汇编语言到Windows内核编程笔记(1)
汇编部分
1、call 的本质相当于push+jmp,ret的本质相当于pop+jmp。
2、Windows中,不管哪种调用方式都是返回值放在eax中,然后返回。外部从eax中得到值。
3、Ebp总是被我们用来保存这个函数执行之前的esp的值。
4、把局部变量区域初始化成全0cccccccch,0cch实际是int 3 指令的机器码,这是一个断点中断指令。
5、任何一段中间不加任何跳转,连续的mov和加减乘除指令一般都可以还原为一个C表达式。
如果有下面的代码段,说明可能是含有数组或结构体。
Mov eax,<数组下标>
Inul eax,eax,<结构大小>
Mov ecx, <结构数组开始的地址>
Mov eax, dword ptr [ecx + eax]
6、分析汇编指令时:
与堆栈操作相关的,call,ret等相关指令,我们叫做函数调用([函数])指令:F
流程控制代码,涉及判断和跳转指令:C
数据处理指令,其它一般为数据处理指令:D。
内核基础
基本概念
首先需要安装DDK (Device Driver Kit),这里我选择Microsoft Windows Server 2003 SP1 DDK。
Windows 驱动分成两类,一类是不支持即插即用的NT式驱动,一类是支持即插即用的WDM((Windows Driver Model))驱动。NT式驱动的安装是基于服务的,可以通过修改注册表进行,也可以直接通过服务函数,如CreateService进行安装;但WDM 式驱动不同,它安装的时候需要通过编写一个inf文件进行控制。
Driver.h头文件中包含了开发NT式驱动所需要的NTDDK.h,此外还定义了几个标志来指明函数和变量分配在分页内存还是非分页内存中。Windows驱动程序的入口函数是DriverEntry函数。
有两种编译驱动的办法,一种是用DDK环境来编译,需要在源代码所在目录下创建两个文件makefile和Sources,功能是引入DDK的bin目录下 的makefile.def文件,然后在开始菜单中选择“Windows XP Checked Build Environment”编译环境,进入需要编译的目录,输入”build“命令就可以;第二种编译方式是使用VC++进行编译。[1]]
为了调试方便,最好安装一个虚拟机。[2]
Windows的驱动模型概念,本来是就驱动程序的行为而言的。比如WDM驱动,必须要满足提供n种被要求的特性(如电源管理、即插即用)才被称为WDM驱动。如果不提供这些功能,那么统一称为NT式驱动。同样的,WDF驱动也有它的一系列规范。
WDF(Windows Driver Foundation)驱动是可以调用传统型驱动所调用的内核API的,WDF可以视为传统型的升级版。[3]
WDK = DDK (Driver Development Kit) + HCT Kit (Hardware Compatibility Test) + WDF (Windows Driver Foundation) + DTM (Driver Test Manager) + WDF Driver Verification Tools + IFS Kit (Installable File Systems Kit) + Free ISO image download - Visual Studio 2005 out of the box integration[4]
基本语法[5]
字符串
在驱动开发中四处可见的是Unicode字符串。因此可以说:Windows的内核是使用Uincode编码的。ANSI_STRING仅仅在某些碰到窄字符的场合使用。而且这种场合非常罕见。一个定义如下:
typedef struct _UNICODE_STRING {
USHORT Length; // 字符串的长度(字节数)
USHORT MaximumLength; // 字符串缓冲区的长度(字节数)
PWSTR Buffer; // 字符串缓冲区
} UNICODE_STRING, *PUNICODE_STRING;
UNICODE_STRING并不保证Buffer中的字符串是以空结束的。因此,类似下面的做法都是错误的,可能会会导致内核崩溃:
UNICODE_STRING str;
…
len = wcslen(str.Buffer); // 试图求长度。
DbgPrint(“%ws”,str.Buffer); // 试图打印str.Buffer。
如果要用以上的方法,必须在编码中保证Buffer始终是以空结束。但这又是一个麻烦的问题。所以,使用微软提供的Rtl系列函数来操作字符串,才是正确的方法。[6]
内存与链表
ExAllocatePoolWithTag
内存分配:
LIST_ENTRY中的数据成员Flink指向下一个LIST_ENTRY。
整个链表中的最后一个LIST_ENTRY的Flink不是空。而是指向头节点。得到LIST_ENTRY之后,要用CONTAINING_RECORD来得到链表节点中的数据。
锁一般不会定义成局部变量。可以使用静态变量、全局变量,或者分配在堆中。
文件操作
在内核中不能调用用户层的Win32 API函数来操作文件。在这里必须改用一系列与之对应的内核函数。
VOID InitializeObjectAttributes(
OUT POBJECT_ATTRIBUTES InitializedAttributes,
IN PUNICODE_STRING ObjectName,
IN ULONG Attributes,
IN HANDLE RootDirectory,
IN PSECURITY_DESCRIPTOR SecurityDescriptor);
Windows内核中,无论是打开文件,还是注册表,设备等,都会先调用初始化一个OUT POBJECT_ATTRIBUTES。
OBJ_KERNEL_HANDLE表明打开的文件句柄一个“内核句柄”。内核文件句柄比应用层句柄使用更方便,可以不受线程和进程的限制。在任何线程中都可以读写。同时打开内核文件句柄不需要顾及当前进程是否有权限访问该文件的问题(如果是有安全权限限制的文件系统)。
路径并不是像应用层一样直接写“C:\\a.dat”,而是写成了“\\??\\C:\\a.dat”。这是因为ZwCreateFile使用的是对象路径。“C:”是一个符号链接对象。符号链接对象一般都在“\\??\\”路径下。
注册表[5]
应用编程中对应的子键 驱动编程中的路径写法
HKEY_LOCAL_MACHINE \Registry\Machine
HKEY_USERS \Registry\User
HKEY_CLASSES_ROOT 没有对应的路径
HKEY_CURRENT_USER 没有简单的对应路径,但是可以求得
ZwOpenKey
ZwQueryValueKey
ZwSetValueKey
时间
void MyGetTickCount (PULONG msec)
{
LARGE_INTEGER tick_count;
ULONG myinc = KeQueryTimeIncrement();
KeQueryTickCount(&tick_count);
tick_count.QuadPart *= myinc;
tick_count.QuadPart /= 10000;
*msec = tick_count.LowPart;
}
KeSetTimer
内核的代码始终运行在某个“中断级”上。Dispatch > APC > Passive
参考
[1] http://www.cnblogs.com/phinecos/archive/2009/02/19/1393803.html
[2] http://www.cnblogs.com/qsilence/archive/2009/06/11/1501511.html
[3 http://msdn.microsoft.com/en-us/library/ff557565%28VS.85%29.aspx
[4] http://www.cnblogs.com/wanghao111/archive/2009/05/25/1489041.html
[5] Windows驱动编程基础教程.doc
[6] Windows DDK