您的位置:首页 > 运维架构

驱动入门实战演练--在驱动下实现自己的CopyFile

2013-02-20 22:24 483 查看

一、一些废话

第一篇驱动类的博文,希望大家多多支持~!

入门驱动不久,感觉这一阶段还是遇到了挺多困难,看书的过程中书本上的知识多多少少会和亲身实践有差别,尤其是进到R0级后不再像R3下能那么'为所欲为'了,细节方面的东西特别多,而且一不小心就给蓝了,还好调试时在虚拟机下,鼠标点两下就能重启。其实驱动说难也难,说简单也简单,多动动手,多码几个字,多瞄两眼,熟悉了也就不难了。

废话不多说,先推荐些Windows驱动入门必备利器:

1.《天书夜读》Windows驱动编程基础教程 想要快速入门,这个当之无愧。

2.《从汇编语言到Windows内核编程》 很经典的一本书,本文就是在其基础上写成的(个人很喜欢的一本书)

3.《寒江独钓 Windows内核安全编程》.(谭文等) 相当不错的一本驱动入门书籍

4. 竹林蹊径:深入浅出Windows驱动开发 这本书个人觉得不如前几本好,跳跃性有点大

本文的目的是通过实现驱动下的CopyFile熟悉驱动编程。

关键词: 驱动(内核)编程 内核字符串 应用层与驱动的通信 IRP请求

二、前缀知识

在上代码前让我们先快速回顾下驱动基础知识。

①、驱动环境搭建

网上关于驱动环境搭建的教程已有很多,而且也并不复杂,无论是xp还是win7都有很好的教程,这里就不再重复了。
我搭建的驱动环境是: 主机--win7 64位 wdk vs2008 虚拟机--xp wdk vs2008,使用windbg双机调试

②、Windbg常用命令

windbg是在windows平台下,强大的用户态和内核态调试工具。相比较于VS,它是一个轻量级的调试工具,所谓轻量级指的是它的安装文件大小较小,但是其调试功能,却比VS更为强大。它的另外一个用途是可以用来分析dump数据。
由于windbg命令很多,而在一般调试中很多都不会用到,这里仅仅介绍一些最基本也是最常用的一些命令

最常用的一些命令如下: 
dv -- 查看局部变量 dv /i
--查看局部变量, 并显示符号的类型和参数类型
bl -- 查看所有断点 bc*
--清除所有断点
bu FileName!Function --下延迟断点
bp FileName!Function / bp +地址 --设置条件断点

③、内核中的UNICODE_STRING

出于对安全的考虑,在驱动中几乎是UNICODE_STRING的天下,UNICODE_STRING是结构体类型,具体结构如下:
typedef struct _UNICODE_STRING {
USHORT  Length; //The length in bytes of the string stored in Buffer.
USHORT  MaximumLength;  //The length in bytes of Buffer
PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;


这里要注意的就是上面红色字体bytes,为什么说这个字很重要呢,因为Buffer中是WSTR类型的字符串,在获取字符串长度时常常会用sizeof( .. ),而sizeof得出的结果是以WSTR为单位的,也就是说结果和Length的值存在两倍的关系。 比如: PWSTR a="1111"; sizeof(a)=4; 而Length的值却为 4*2=8; 这点在给UNICODE_STRING手动赋值时要特别注意。

在得到UNICODE_STRING结构后的第一件事就是要对其进行初始化,在初始化时最常用的有如下:
1、RTL_CONSTANT_STRING
这是一个已经定义好的宏,用在常量赋值上(注意只能在初始化定义字符串时使用)

2、RtlInitUnicodeString
上面那个宏只能用在定义常量上,而RtlInitUnicodeString则能用在随时定义变量上。

初始化完就该操作这些UNICODE_STRING了,基本操作有拷贝、连接、打印等等
1、RtlCopyUnicodeString
这个函数用来拷贝字符串。

2、RtlAppendUnicodeToString
这个函数将一个常量字符串连接至一个UNICODE_STRING后

3、RtlAppendUnicodeStringToString
这个与上面那个相似,都是用来连接字符串的,区别在于这个是连接两个UNICODE_STRING

当然还有很多字符串操作的函数,限于篇幅就不一 一介绍了。

④、OBJECT_ATTRIBUTE结构体

在操作文件、注册表时OBJECT_ATTRIBUTE结构体显得至关重要,R3层操作文件只是简简单单的传入文件路径就能完成,而在R0下却要填写这样一个结构体。其初始化操作由InitializeObjectAttributes函数执行。具体结构如下:
VOID
InitializeObjectAttributes(
OUT POBJECT_ATTRIBUTES  InitializedAttributes, //被初始化的OBJECT_ATTRIBUTES结构体
IN PUNICODE_STRING  ObjectName, //如果是操作文件则为文件名
IN ULONG  Attributes,
IN HANDLE  RootDirectory, //相对目录
IN PSECURITY_DESCRIPTOR  SecurityDescriptor
);
主要内容已经注释,这里有一定要特别注意: 在内核中文件路径不能像在应用层那样 写"C:\\aa.txt" 而是写成"\\??\\C:\\aa.txt" 或 "\\DosDevices\\C:\\aa.txt" ,因为在内核中使用对象路径,"C:\\"只是一个符号链接对象(仅仅对用户而言有意义),链接对象一般都在\\??\\
(也可写成\\DosDevices\\) 所以在内核中要写完整的对象路径。

⑤、使用DeviceIoControl与驱动交互

说到驱动与应用层的交互当然不能不提IRP(I/O Request Package),相当于R3层的消息,与消息的消息处理函数对应,IRP也有相应的处理例程(驱动下可以简单认为例程就是函数啦)。不同IRP当然就该有不同的标示符,这个标示符就是IRP的“控制码”,我们也可以自定义控制码,定义控制码方法如下:
//用户自定义控制码 用于和应用层的通信
#define MYCOPYFILE_CODE \
(ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,\
0xa01,METHOD_BUFFERED,FILE_READ_DATA|FILE_WRITE_DATA)
其中MYCOPYFILE_CODE即我们自定义的控制码,在初始化时需给他一个‘代号’,即上面的'0xa01'(这个可以自己随意定,只要不与系统的重复就好)。定义控制码的其余部分在现阶段可以认为都一样。

控制码可以连通应用层和驱动内核,应用层与驱动的交互有多种方法,这里主要介绍使用DeviceIoControl,其结构如下:
BOOL WINAPI DeviceIoControl(
__in          HANDLE hDevice, //设备句柄  要通过CreateFile得到
__in          DWORD dwIoControlCode, //这里就写我们自定义控制码
__in          LPVOID lpInBuffer, //输入缓冲区
__in          DWORD nInBufferSize, //输入缓冲区大小
__out         LPVOID lpOutBuffer, //输出缓冲区
__in          DWORD nOutBufferSize, //输出缓冲区大小
__out         LPDWORD lpBytesReturned, //在输出缓冲区接收到的的大小
__in          LPOVERLAPPED lpOverlapped
);
注意:这里的缓冲区大小也是以byte为单位。
使用DeviceIoControl的好处是这个函数能为输入输出提供了统一的接口,无需使用WriteFile写入而用ReadFile来读出。
我们可以定义自己的数据结构然后转成LPVOID,再传给驱动,具体操作见完整代码。

三、完整代码及详细注释

本次实践的功能很简单,仅仅是在驱动下接受应用层传入的文件名,然后拷贝源文件至目标文件,没啥好说的就直接看代码吧。

驱动代码:
///////////////////////////////////////////////////////////////////////////////
///
/// Original filename: DriverCopyFile.cpp
/// Project : DriverCopyFile
/// Date of creation : 2013-02-18
/// Author(s) : CouLd
///
///////////////////////////////////////////////////////////////////////////////

// $Id$

#ifdef __cplusplus
extern "C" {
#endif
#include <ntddk.h>
#include <ntdef.h>
#include <string.h>
#ifdef __cplusplus
}; // extern "C"
#endif

#include "DriverCopyFile.h"
#define MEM_TAG 'MyT'//定义内存标志
//用户自定义控制码 用于和应用层的通信 #define MYCOPYFILE_CODE \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,\ 0xa01,METHOD_BUFFERED,FILE_READ_DATA|FILE_WRITE_DATA)

//设备名
UNICODE_STRING my_device_name=RTL_CONSTANT_STRING(L"\\Device\\MyCF");
//符号链接名 应用层使用 DosDevices注意别忘了s
UNICODE_STRING symb_link_name=RTL_CONSTANT_STRING(L"\\DosDevices\\MyCF");

#ifdef __cplusplus
namespace { // anonymous namespace to limit the scope of this global variable!
#endif
PDRIVER_OBJECT pdoGlobalDrvObj = 0;
#ifdef __cplusplus
}; // anonymous namespace
#endif

NTSTATUS DRIVERCOPYFILE_DispatchCreateClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
NTSTATUS status = STATUS_SUCCESS;
DbgPrint("Create Close");
//返回最简单的IRP三部曲:
Irp->IoStatus.Status = status;//(1)
Irp->IoStatus.Information = 0;//(2)
IoCompleteRequest(Irp, IO_NO_INCREMENT);//(3)
return status;
}

NTSTATUS MyDeviceIOControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
NTSTATUS status = STATUS_SUCCESS;
WCHAR temp_sourcebuf[100],temp_destbuf[100],temp_sourcebuf2[100],temp_destbuf2[100];
UNICODE_STRING sourcebuffer,destinbuffer;
UNICODE_STRING sourcehead,destinhead;
UNICODE_STRING head=RTL_CONSTANT_STRING(L"\\??\\");
//初始化UNICODE_STRING空串
RtlInitEmptyUnicodeString(&sourcebuffer,temp_sourcebuf,sizeof(WCHAR)*100);
RtlInitEmptyUnicodeString(&destinbuffer,temp_destbuf,sizeof(WCHAR)*100);
RtlInitEmptyUnicodeString(&sourcehead,temp_sourcebuf2,sizeof(WCHAR)*100);
RtlInitEmptyUnicodeString(&destinhead,temp_destbuf2,sizeof(WCHAR)*100);
RtlCopyUnicodeString(&sourcehead,&head);
RtlCopyUnicodeString(&destinhead,&head);

//得到irpsp的目的是为了得到功能号、输入/输出缓冲区长度等内容
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Irp);
ULONG code=irpSp->Parameters.DeviceIoControl.IoControlCode;

//得到输入/输出缓冲区长度
ULONG in_len=irpSp->Parameters.DeviceIoControl.InputBufferLength;
ULONG out_len=irpSp->Parameters.DeviceIoControl.OutputBufferLength;

//输入/输出缓冲区是公用内存空间的
PVOID buffer=Irp->AssociatedIrp.SystemBuffer;

//DbgPrint(((PMyMessage)buffer)->SourceFileName));
switch(code)
{
case MYCOPYFILE_CODE: //如果是我们自定义的控制码
DbgPrint("MYCOPYFILE_CODE_SUCCESS");

//因为传进来的字符串以空为结尾所以可以用wcscpy
//初始化UNICODE_STRING字符串
wcscpy(sourcebuffer.Buffer,((MyMessage*)buffer)->SourceFileName);
wcscpy(destinbuffer.Buffer,((MyMessage*)buffer)->DestinFileName);

//UNICODE_STRING 中几个字段的MSDN解释如下:
//Length :The length in bytes of the string stored in Buffer.
//MaximumLength : The length in bytes of Buffer.
//很明了了,要转换成真正的长度要乘2
sourcebuffer.Length=2*wcslen(sourcebuffer.Buffer);
destinbuffer.Length=2*wcslen(destinbuffer.Buffer);

//这里要特别注意:应用层传过来的文件路径不能直接使用,
//要在字符串前面加上"//??//" 字符串连接要用到RtlAppendUnicodeStringToString

status=RtlAppendUnicodeStringToString(
&sourcehead,&sourcebuffer);
if (status!=STATUS_SUCCESS)
{
DbgPrint("Append Unicode String Error!");
return status;
}
status=RtlAppendUnicodeStringToString(
&destinhead,&destinbuffer);
if (status!=STATUS_SUCCESS)
{
DbgPrint("Append Unicode String Error!");
return status;
}

MyCopyFile(&sourcehead,&destinhead);
Irp->IoStatus.Information=0;
Irp->IoStatus.Status=STATUS_SUCCESS;
break;
default:
Irp->IoStatus.Status = STATUS_INVALID_DEVICE_REQUEST;
Irp->IoStatus.Information = 0;
break;
}

status = Irp->IoStatus.Status;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return status;
}

VOID DRIVERCOPYFILE_DriverUnload(
IN PDRIVER_OBJECT DriverObject
)
{
PDEVICE_OBJECT pdoNextDeviceObj = DriverObject->DeviceObject;
IoDeleteSymbolicLink(&symb_link_name);//删除符号链接

// 卸载驱动对象
while(pdoNextDeviceObj)
{
PDEVICE_OBJECT pdoThisDeviceObj = pdoNextDeviceObj;
pdoNextDeviceObj = pdoThisDeviceObj->NextDevice;
IoDeleteDevice(pdoThisDeviceObj);
}
}

NTSTATUS MyCopyFile(
PUNICODE_STRING pSourceFile,
PUNICODE_STRING pDestinFile)
{
HANDLE HSourceFile,HDestinFile;
/*内核中读写文件等操作不能直接进行,要填写OBJECT_ATTRIBUTES结构
初始化时调用InitializeObjectAttributes函数并传入*/
OBJECT_ATTRIBUTES ObjectAttrSource,ObjectAttrDestin;

/*IO_STATUS_BLOCK在内核开发中经常使用,表示一个操作的结果
返回结果在其中的Status字段中,成STATUS_SUCCESS,否则为错误码
错误码存在Information字段中*/
IO_STATUS_BLOCK io_status={0};

/*内核中常用到的长长整型数据
LARGE_INTEGER--共用体 能很方便的表示高32位、低32位
在运算比较的时候用QuadPart字段(直接表示64位值)即可
*/
LARGE_INTEGER offset={0};
PVOID buffer=NULL;
ULONG Length;
NTSTATUS status = STATUS_SUCCESS;

InitializeObjectAttributes(
&ObjectAttrSource,//被初始化的OBJECT_ATTRIBUTES
pSourceFile,//对象名字字符串(文件路径)不能像在应用层那样直接用路径应写成 "\\??\\C:\\aa.txt"
OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,//前者表示名字不区分大小写,后者表示打开内核句柄
NULL,//用于相对打开的情况
NULL);//用于设置安全描述符

/*
ZwCreateFile中第二个字段ACCESS_MASK表示申请的权限具体如下:
打开写文件--FILE_WRITE_DATA
打开读文件--FILE_READ_DATA
删除文件--DELETE
设置文件属性--FILE_READ_ATTRIBUTES
常用宏:GENERIC_READ--常用读权限 GENERIC_WRITE--常用写权限 GENERIC_ALL--全部权限
*/
status=ZwCreateFile(
&HSourceFile,
GENERIC_READ|GENERIC_WRITE,//申请的权限
&ObjectAttrSource,
&io_status,//返回操作结果
NULL,
FILE_ATTRIBUTE_NORMAL,//控制新建的文件属性
FILE_SHARE_READ,//共享访问
FILE_OPEN_IF,//打开或新建
FILE_NON_DIRECTORY_FILE|FILE_RANDOM_ACCESS| //表示打开的是文件
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if(!NT_SUCCESS(status))//是否顺利打开
{
DbgPrint("Open Source File Error!");
return status;
}
InitializeObjectAttributes(
&ObjectAttrDestin,
pDestinFile,
OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
NULL,
NULL);
status=ZwCreateFile(
&HDestinFile,
GENERIC_READ|GENERIC_WRITE,
&ObjectAttrDestin,
&io_status,
NULL,
FILE_ATTRIBUTE_NORMAL,
FILE_SHARE_READ,
FILE_OPEN_IF,
FILE_NON_DIRECTORY_FILE|FILE_RANDOM_ACCESS|
FILE_SYNCHRONOUS_IO_NONALERT,
NULL,
0
);
if(!NT_SUCCESS(status))//是否顺利打开
{
DbgPrint("Open Destin File Error!");
return status;
}

Length=4*1024; //每次读取4KB
//为临时buffer分配内存
buffer=ExAllocatePoolWithTag(NonPagedPool,Length,MEM_TAG);

/*打开文件后要进行循环读写文件操作*/
while(1)
{
status=ZwReadFile(
HSourceFile,NULL,NULL,NULL,
&io_status,
buffer,//存内容的buffer
Length,
&offset,//要读取的文件的偏移
NULL);
if(!NT_SUCCESS(status))
{
//如果状态为STATUS_END_OF_FILE说明文件读取成功结束
if(status==STATUS_END_OF_FILE)
{
status=STATUS_SUCCESS;
break;
}
else
{
DbgPrint("Read File Error!");
break;
}
}
//获取实际读取到的长度
Length=io_status.Information;

//把读取到的内容写入文件
status=ZwWriteFile(
HDestinFile,NULL,NULL,NULL,
&io_status,
buffer,Length,&offset,NULL);
if(!NT_SUCCESS(status))//是否顺利打开
{
DbgPrint("Write File Error!");
break;
}
//偏移量后移直至读取到文件结尾
offset.QuadPart+=Length;
}

//退出前注意要手动释放资源
if (HSourceFile!=NULL)
{
ZwClose(HSourceFile);
}
if (HDestinFile!=NULL)
{
ZwClose(HDestinFile);
}
if (buffer!=NULL)
{
ExFreePool(buffer);
}
return status;
}

#ifdef __cplusplus
extern "C" {
#endif
NTSTATUS DriverEntry(
IN OUT PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath
)
{
PDEVICE_OBJECT pdoDeviceObj = 0;
NTSTATUS status = STATUS_UNSUCCESSFUL;

// 创建设备对象
status = IoCreateDevice(
DriverObject,//生成该设备的驱动对象
0,//当用户需要在每个设备上记录一些额外信息时使用
&my_device_name,
FILE_DEVICE_UNKNOWN,//表示设备类型 现在用不到
FILE_DEVICE_SECURE_OPEN,
FALSE,//必须为FALSE
&pdoDeviceObj//生成的设备对象指针
);
if(!NT_SUCCESS(status))
{
return status;
}
// 创建符号链接
status = IoCreateSymbolicLink(
&symb_link_name,
&my_device_name
);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(pdoDeviceObj);
return status;
}

DriverObject->MajorFunction[IRP_MJ_CREATE] =
DriverObject->MajorFunction[IRP_MJ_CLOSE] = DRIVERCOPYFILE_DispatchCreateClose;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceIOControl;//设置为自己定义的分发函数
DriverObject->DriverUnload = DRIVERCOPYFILE_DriverUnload;

return STATUS_SUCCESS;
}
#ifdef __cplusplus
}; // extern "C"
#endif


DriverCopyFile.h 中的主要代码(有些是vs2008自动生成的就不贴出来了)

typedef struct mymessage
{
PWSTR SourceFileName;
PWSTR DestinFileName;
}MyMessage,*PMyMessage;

NTSTATUS MyCopyFile(
PUNICODE_STRING pSourceFile,
PUNICODE_STRING pDestinFile);


应用层测试代码:
#include <stdio.h>
#include <Windows.h>
//用户自定义控制码 用于和应用层的通信 #define MYCOPYFILE_CODE \ (ULONG)CTL_CODE(FILE_DEVICE_UNKNOWN,\ 0xa01,METHOD_BUFFERED,FILE_READ_DATA|FILE_WRITE_DATA)
typedef struct mymessage
{
PWSTR SourceFileName;
PWSTR DestinFileName;
}MyMessage,*PMyMessage;
int main()
{
BOOL ret;
DWORD length=0;
MyMessage input_buffer;
//用CreateFile打开驱动
HANDLE device=CreateFileW(
L"\\\\.\\MyCF", //应用层应写成这种形式
GENERIC_READ|GENERIC_WRITE,0,0,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,0);
if (device==INVALID_HANDLE_VALUE)
{
printf("open device error! %d",GetLastError());
system("pause");
return -1;
}

//填写自定义buffer
input_buffer.DestinFileName=L"D:\\MyCopyFile.txt";
input_buffer.SourceFileName=L"D:\\output.txt";
printf("%d",sizeof(input_buffer));

ret=DeviceIoControl(
device,
MYCOPYFILE_CODE,//我们自定义的控制码
(PVOID)&input_buffer,//输入缓冲区
sizeof(input_buffer),
NULL,//没有输出缓冲区
0,//输出缓冲区的长度
&length,
NULL
);
if (!ret)
{
printf("Device IO error!");
return -2;
}
CloseHandle(device);

system("pause");
return 0;
}


四、总结

本文通过实现驱动下的CopyFile让大家熟悉了驱动下的基本操作。驱动编程只有自己实际动手写写才会知道自己错在哪,其实IT行业也就是这样,多动手比只看出好得多,毕竟“绝知此事要躬行”啊!
本文仅仅是驱动编程的冰山一脚,另外本人能力有限,也是初学驱动,难免有些遗漏及不足之处,还请大家多多指教!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: