Windows內核安全編程__具有還原功能的磁盤捲過濾驅動

 


磁盤過濾驅動的概念
1.設備過濾和類過濾
在之前的文章裏,我們已經介紹過濾的概念,所謂過濾技術就是在本來已有的設備棧中加入自己的一個設備。由於Windows向任何一個設備發送IRP請求都會首先發送給這個設備所在設備棧的最上層設備,然後再依次傳遞下去,這就使得加入的設備在目標設備之前獲取Irp請求稱爲可能,這時候就可以加入自己的處理流程。在這裏把插入設備棧的用戶設備叫做過濾設備,建立這個設備並使其具有特殊功能的驅動叫做過濾驅動。
在前面已經展示瞭如何去建立一個過濾設備並將其綁定在一個有名字的設備上,這叫做設備過濾,這是對某個特定設備加以過濾的方法。但是在實際應用中,這種方法還存在一些問題,例如,Windows中有很多即插即用設備,如何在這些設備加入系統的時候就自動對他們進行綁定呢?實際上,在Windows的過濾驅動框架中,還有一種叫做類過濾驅動的驅動程序,能夠在某一類特定的設備建立時有Pnp Manager調用指定的過濾驅動代碼,並且允許用戶對此時這一類設備進行綁定。根據用戶設備在整個設備棧中的位置可以分爲上層過濾和下層過濾。
2.磁盤設備和磁盤卷設備過濾驅動
在Windows的存儲系統中,最底層的是磁盤,而在磁盤上面又有卷,卷雖然只是邏輯上的一個概念,但是Windows仍然爲其建立了設備,所以在Windows的存儲系統裏有磁盤設備和磁盤卷設備兩種類型的設備。
如果一個磁盤卷位於某個磁盤上,那麼對於磁盤卷的訪問最終也會體現在相應的磁盤上。但是這並不意味着他們在一個設備棧上,irp不會原封不動從磁盤卷設備棧上一直傳到磁盤設備棧上,更何況Windows中還存在着跨磁盤的卷,軟RAID卷等不能對應到唯一磁盤上的卷。
從驅動的角度上來講,這兩種設備受到讀/寫請求都是針對與磁盤大小或者卷大小範圍之內的請求,都是以扇區大小對齊,處理起來也沒有什麼太大的區別。在此我們選用磁盤卷設備的上層類過濾驅動。
3.註冊表和磁盤卷設備過濾驅動
在實際的系統運行過程中,一個普通的驅動程序是如何告訴Windows操作系統它是一個類過濾驅動,並且何如和相應的設備聯繫起來的呢?這就需要註冊表的幫忙了。
讀者應該很熟悉一個驅動程序作爲一個服務是如何在註冊表中存在的,在\HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Service下服務鍵的名字也就是這個服務的名字了。同時在\HKEY_LOCAL_MACHINE\SYSTEM\Control\Class下,也有許多類的名字,這些類都是一長串數字。這些數字實際上是一個ClassGUID,隨意選擇一個鍵下面都會有一個Class的值,讀者可以找到一個Class值爲“Volume”的鍵,這就是磁盤卷類。其中有一個UpperFilters的值,這個值說明了磁盤卷有哪些上層過濾驅動。

具有還原功能的磁盤捲過濾驅動
1.簡介
這裏首先介紹一下什麼是還原。在網吧,機房我們所看到的計算機,在用過之後,只要重新啓動,所有的操作都沒有了,系統又回到了設置還原點的那個狀態,這就是還原。
由於這個過濾驅動只是爲了講解而寫的,故這裏對它的使用條件比限制較多。這個驅動工作時需要只有一個硬盤,需要使用Windows XP系統,並且硬盤被分爲C盤爲主分區,D盤和E盤都爲擴展分區的分區形式,而且所有分區都必須是NTFS文件系統。本驅動只保護D盤並且會在E盤建立臨時文件,而且操作系統要安裝在C盤。
2.基本思想
爲了實現還原,一種簡單的思想如下:
*在開啓還原之後,所有對還原卷的寫操作將被寫到另一個地方,而不會真正寫在還原捲上。這裏所說的另一個地方可以稱爲轉存處。
*在開啓還原之後,所有對還原卷的讀操作將分爲兩種情況處理:一種情況是讀了開啓還原之前就存在的內容,這種情況就按照正常的讀取方式從還原捲上讀取。另一種情況是讀了開啓還原之後寫到還原捲上的內容,這種情況將會從轉存處把之前寫過來的內容讀取出來。
*上述的讀/寫必須建立在互斥的基礎上,不能出現寫了一半就開始讀的情況。
*重啓之後轉存處的數據清零,所有在還原開始後被寫過的數據也就不復存在了。

驅動分析
1.DriverEntry函數
DriverEntry函數作爲過濾驅動的入口函數,主要負責初始化本驅動的各個分發函數。它首先會將所有的分發函數都設置成一個統一的處理函數,這個函數是對大部分irp請求的處理方式;其次,它會將本驅動關心的分發函數指定爲驅動專門實現的函數。另外,它還指定了這個驅動的AddDevice函數和驅動的Unload函數。由於這個驅動被註冊成了磁盤卷設備的上層過濾驅動,PnP manager將會在一個新的磁盤卷設備建立之後,首先調用本過濾驅動的AddDevice函數,然後在調用磁盤卷設備驅動中的AddDevice函數。這就讓過濾驅動有了在系統加入磁盤卷設備起作用之前做一些工作的機會,而Unload函數會在過濾驅動結束的時候被調用,用來做一些清理工作。不過本驅動將會一直工作到系統關機,所以Unlaod函數將不會做任何清理工作。
在DriverEntry函數的最後,還註冊了一個boot類型驅動的完成回調函數。首先需要說明一點的是,本過濾驅動是作爲一個boot類型驅動存在的。boot類型驅動程序是啓動最早的驅動程序,在系統引導時就必須加載完畢;而對於註冊爲boot類型驅動的完成回調函數的函數,將會在所有的boot類型驅動執行完畢之後被調用一次,需要注意的是,這時候仍然是系統啓動過程中比較早的時候。在這裏需要註冊這個回調函數,是因爲驅動中有些工作需要等到這個時間才能做。
NTSTATUS
DriverEntry(
    IN PDRIVER_OBJECT DriverObject,
    IN PUNICODE_STRING RegistryPath
    )
{
 int i;

 //KdBreakPoint();

 for (i = 0; i <= IRP_MJ_MAXIMUM_FUNCTION; i++)
 {
  //初始化這個驅動所有的分發函數,默認值是初始化爲DPDispatchAny
  DriverObject->MajorFunction[i] = DPDispatchAny;
 }
   
 //下面將我們特殊關注的分發函數重新賦值爲我們自己的處理函數
    DriverObject->MajorFunction[IRP_MJ_POWER] = DPDispatchPower;
    DriverObject->MajorFunction[IRP_MJ_PNP] = DPDispatchPnp;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DPDispatchDeviceControl;
    DriverObject->MajorFunction[IRP_MJ_READ] = DPDispatchReadWrite;
    DriverObject->MajorFunction[IRP_MJ_WRITE] = DPDispatchReadWrite;

 //將這個驅動的AddDevice函數初始化爲DpAddDevice函數
    DriverObject->DriverExtension->AddDevice = DPAddDevice;
 //將這個驅動的unload函數初始化爲DpUnload函數
    DriverObject->DriverUnload = DPUnload;
   
 //註冊一個boot驅動結束回調,這個回調函數會在所有的boot型驅動都運行完畢之後再去執行
 IoRegisterBootDriverReinitialization(
  DriverObject,
  DPReinitializationRoutine,
  NULL
  );

 //作爲一個過濾驅動,無論如何都要返回成功
    return STATUS_SUCCESS;
}
2.AddDevice函數
由於過濾驅動的DriverEntry函數中將驅動對象的AddDevice函數賦值成自己實現的DPAddDevice函數,這樣在任何磁盤設備建立的時候,DPAddDevice被調用的時候,實際上磁盤卷設備已經建立了起來,只是還不能被使用,也就是說,這個設備對象有了,但是不能響應大部分的irp請求。
在DPAddDevice中將建立一個過濾設備,這個設備將綁定在真正的磁盤卷設備上。並且由於這是一個上層過濾驅動,這個過濾驅動設備會將位於磁盤卷設備頂端方向上,也就是先於磁盤卷設備收到irp請求。在建立並綁定了這個過濾驅動之後,需要對這個過濾驅動做一些初始化,而過濾設備的所有基本信息都會在DP_FILTER_DEV_EXTENSION結構的類型存儲在設備擴展中。在這裏先介紹DP_FILTER_DEV_EXTENSION數據結構中的成員變量。
//用來存儲一個卷所有的相關信息的數據結構,放在過濾設備的設備擴展中
typedef struct _DP_FILTER_DEV_EXTENSION_
{
 //卷的名字,例如"C:,D:"等中的字母部分
 WCHAR     VolumeLetter;
 //這個卷是否在保護狀態
 BOOL     Protect;
 //這個卷的總大小,以byte爲單位
 LARGE_INTEGER   TotalSizeInByte;
 //這個捲上文件系統的每簇大小,以byte爲單位
 DWORD     ClusterSizeInByte;
 //這個卷的每個扇區大小,以byte爲單位
 DWORD     SectorSizeInByte;
 //這個卷設備對應的過濾設備的設備對象
 PDEVICE_OBJECT   FltDevObj;
 //這個卷設備對應的過濾設備的下層設備對象
 PDEVICE_OBJECT   LowerDevObj;
 //這個卷設備對應的物理設備的設備對象
 PDEVICE_OBJECT   PhyDevObj;
 //這個數據結構是否已經被初始化完畢了
 BOOL     InitializeCompleted;
 //這個捲上的保護系統使用的位圖的句柄
 PDP_BITMAP  Bitmap; 
 //用來轉儲的文件句柄
 HANDLE     TempFile;
 //這個捲上的保護系統使用的請求隊列
 LIST_ENTRY    ReqList;
 //這個捲上的保護系統使用的請求隊列的鎖
 KSPIN_LOCK    ReqLock;
 //這個捲上的保護系統使用的請求隊列的同步事件
 KEVENT     ReqEvent;
 //這個捲上的保護系統使用的請求隊列的處理線程之線程句柄
 PVOID     ThreadHandle;
 //這個捲上的保護系統使用的請求隊列的處理線程之結束標誌
 BOOLEAN     ThreadTermFlag;
 //這個捲上的保護系統的關機分頁電源請求的計數事件
 KEVENT     PagingPathCountEvent;
 //這個捲上的保護系統的關機分頁電源請求的計數
 LONG     PagingPathCount;
} DP_FILTER_DEV_EXTENSION, *PDP_FILTER_DEV_EXTENSION;
在上面的數據結構中可以看到有3個設備對象:過濾設備,物理設備和下層設備,其中過濾設備是本過濾驅動自己建立的;物理設備是通過AddDevice函數的參數傳遞進來的設備,是真正的磁盤卷設備;而下層設備是在將過濾設備綁定到物理設備之上後,返回的綁定之前的物理設備棧上最頂部的設備。
在DP_FILTER_DEV_EXTENSION數據結構中可以看到,針對每個過濾設備都會建立一個處理線程和相應的請求隊列,這時因爲在這個驅動中同樣採用了將所有請求依次排隊,然後使用一個單獨的線程依次處理的方式。這麼做的好處在於將所有讀/寫請求串行化,程序易於編寫而且不會出現讀/寫請求之間的同步問題。
在DPAddDevice函數中讀者還會發現初始化了PagingPathCountEvent和PagingPathCount這兩個與分頁路徑相關的變量,他們將會在Pnpirp請求的處理中被用到。
NTSTATUS
DPAddDevice(
    IN PDRIVER_OBJECT DriverObject,
    IN PDEVICE_OBJECT PhysicalDeviceObject
    )
{
 //NTSTATUS類型的函數返回值
 NTSTATUS     ntStatus = STATUS_SUCCESS;
    //用來指向過濾設備的設備擴展的指針
 PDP_FILTER_DEV_EXTENSION DevExt = NULL;
 //過濾設備的下層設備的指針對象
 PDEVICE_OBJECT    LowerDevObj = NULL;
 //過濾設備的設備指針的指針對象
 PDEVICE_OBJECT    FltDevObj = NULL;
 //過濾設備的處理線程的線程句柄
 HANDLE      ThreadHandle = NULL;

 //建立一個過濾設備,這個設備是FILE_DEVICE_DISK類型的設備並且具有DP_FILTER_DEV_EXTENSION類型的設備擴展
 ntStatus = IoCreateDevice(
  DriverObject,
  sizeof(DP_FILTER_DEV_EXTENSION),
  NULL,
  FILE_DEVICE_DISK,
  FILE_DEVICE_SECURE_OPEN,
  FALSE,
  &FltDevObj);
 if (!NT_SUCCESS(ntStatus))
  goto ERROUT;
 //將DevExt指向過濾設備的設備擴展指針
 DevExt = FltDevObj->DeviceExtension;
 //清空過濾設備的設備擴展
 RtlZeroMemory(DevExt,sizeof(DP_FILTER_DEV_EXTENSION));

 //將剛剛建立的過濾設備附加到這個卷設備的物理設備上
 LowerDevObj = IoAttachDeviceToDeviceStack(
  FltDevObj,
  PhysicalDeviceObject);
 if (NULL == LowerDevObj)
 {
  ntStatus = STATUS_NO_SUCH_DEVICE;
  goto ERROUT;
 }

 //初始化這個卷設備的分頁路徑計數的計數事件
 KeInitializeEvent(
  &DevExt->PagingPathCountEvent,
  NotificationEvent,
  TRUE);

 //對過濾設備的設備屬性進行初始化,過濾設備的設備屬性應該和它的下層設備相同
 FltDevObj->Flags = LowerDevObj->Flags;
 //給過濾設備的設備屬性加上電源可分頁的屬性
 FltDevObj->Flags |= DO_POWER_PAGABLE;
 //對過濾設備進行設備初始化
 FltDevObj->Flags &= ~DO_DEVICE_INITIALIZING;

 //將過濾設備對應的設備擴展中的相應變量進行初始化
 //卷設備的過濾設備對象
 DevExt->FltDevObj = FltDevObj;
 //卷設備的物理設備對象
 DevExt->PhyDevObj = PhysicalDeviceObject;
 //卷設備的下層設備對象
 DevExt->LowerDevObj = LowerDevObj;

 //初始化這個卷的請求處理隊列
 InitializeListHead(&DevExt->ReqList);
 //初始化請求處理隊列的鎖
 KeInitializeSpinLock(&DevExt->ReqLock);
 //初始化請求處理隊列的同步事件
 KeInitializeEvent(
  &DevExt->ReqEvent,
  SynchronizationEvent,
  FALSE
  );

 //初始化終止處理線程標誌
 DevExt->ThreadTermFlag = FALSE;
 //建立用來處理這個卷的請求的處理線程,線程函數的參數則是設備擴展
 ntStatus = PsCreateSystemThread(
  &ThreadHandle,
  (ACCESS_MASK)0L,
  NULL,
  NULL,
  NULL,
  DPReadWriteThread,
  DevExt
  );
 if (!NT_SUCCESS(ntStatus))
  goto ERROUT;

 //獲取處理線程的對象
 ntStatus = ObReferenceObjectByHandle(
  ThreadHandle,
  THREAD_ALL_ACCESS,
  NULL,
  KernelMode,
  &DevExt->ThreadHandle,
  NULL
  );
 if (!NT_SUCCESS(ntStatus))
 {
  DevExt->ThreadTermFlag = TRUE;
  KeSetEvent(
   &DevExt->ReqEvent,
   (KPRIORITY)0,
   FALSE
   );
  goto ERROUT;
 }

ERROUT:
 if (!NT_SUCCESS(ntStatus))
 { 
  //如果上面有不成功的地方,首先需要解除可能存在的附加
  if (NULL != LowerDevObj)
  {
   IoDetachDevice(LowerDevObj);
   DevExt->LowerDevObj = NULL;
  }
  //然後刪除可能建立的過濾設備
  if (NULL != FltDevObj)
  {
   IoDeleteDevice(FltDevObj);
   DevExt->FltDevObj = NULL;
  }
 }
 //關閉線程句柄,我們今後不會用到它,所有對線程的引用都通過線程對象來進行了
 if (NULL != ThreadHandle)
  ZwClose(ThreadHandle);
 //返回狀態值
    return ntStatus;
}
3.PnP請求的處理
作爲一個捲過濾驅動PnP請求是非常重要的,這是因爲Windows操作系統在某些時候迴向存儲設備發出專門的請求,如果沒有進行正確的處理將會造成系統無法正常關機等一系列的問題。
在收到PnP請求之後,由於DriverEntry中對PnP請求的處理函數特別設置成了DPDispatchPnp函數,所以DPDispatchPnp函數將會被調用。它具有兩個參數:DeviceObject和irp,分別說明了這個請求發往的設備和這個請求的具體細節。由於這是過濾驅動的PnP分發函數,所以也只有在過濾驅動所建立的設備收到PnP請求時會調用這個函數。在AddDevice函數中,每個卷的過濾設備都會被建立相應的設備擴展,裏面存儲有很多過濾設備的屬性信息,所以這個函數的一開始就需要將這些信息拿出來,同時需要通過irp參數中的irp stack成員來進一步確定irp請求的具體目的。
在獲取到了這些參數之後可以直接通過判斷irp stack中的MinorFunction來判斷這個irp請求的具體目的是什麼。在irp stack中,通常會存在MajorFunction和MinorFunction兩個請求號,其中MajorFunction是大請求號,一般類似於Read,Write,PnP,DeviceIoControl等大分類的請求;而MinorFunction是小的請求號,一般在某一個大分類的子請求號。
第一個需要處理的PnP子請求是設備移除請求,這個請求會在Windows進行設備熱插拔,均衡或關機的時候被髮送到磁盤卷設備。當然過濾驅動會先於磁盤卷驅動收到這個請求,在這個請求發送時,所有的磁盤卷設備的讀/寫請求都應該已經完成,所以在過濾驅動收到這個請求的時候只需要簡單的將曾經建立過的所有設備和初始化過的所有內部數據結構全部銷燬即可。建立過的設備主要是AddDevice函數中建立的過濾設備和由綁定而生成的下層設備,內部數據結構主要包括了在下面介紹的Bitmap數據結構,此外在AddDevice函數中爲卷設備建立的請求處理線程也需要停止。
switch(irpsp->MinorFunction)
 {
 case IRP_MN_REMOVE_DEVICE:
  //如果是PnP manager發過來的移除設備的irp,將進入這裏
  {
   //這裏主要做一些清理工作
   if (DevExt->ThreadTermFlag != TRUE && NULL != DevExt->ThreadHandle)
   {
    //如果線程還在運行的話需要停止它,這裏通過設置線程停止運行的標誌並且發送事件信息,讓線程自己終止運行
    DevExt->ThreadTermFlag = TRUE;
    KeSetEvent(
     &DevExt->ReqEvent,
     (KPRIORITY) 0,
     FALSE
     );
    //等待線程結束
    KeWaitForSingleObject(
     DevExt->ThreadHandle,
     Executive,
     KernelMode,
     FALSE,
     NULL
     );
    //解除引用線程對象
    ObDereferenceObject(DevExt->ThreadHandle);
   }
    if (NULL != DevExt->Bitmap)
    {
    //如果還有位圖,就釋放
     DPBitmapFree(DevExt->Bitmap);
    }
   if (NULL != DevExt->LowerDevObj)
   {
    //如果存在着下層設備,就先去掉掛接
    IoDetachDevice(DevExt->LowerDevObj);
   }
    if (NULL != DevExt->FltDevObj)
    {
    //如果存在過濾設備,就要刪除它
     IoDeleteDevice(DevExt->FltDevObj);
    }
   break;
  }
第二個需要處理的請求是設備通告請求,Windows操作系統會在建立或者刪除特殊文件的時候想存儲設備發出這個irp請求,作爲存儲設備捲過濾設備自然也會收到這個請求。這裏說的特殊文件包括頁面文件,休眠文件,dump文件。Windows會通過irp stack中的Parameters.UsageNotification.Type域來說明請求的是哪種文件,並且會使用Parameters.UsageNotification.InPath域來說明這個請求是在詢問設備是否可以建立這個文件,還是在刪除這個文件之後對這個設備的通知。在處理這個請求的時候,過濾驅動比較關心的是對頁面文件的處理,因爲這牽扯到過濾設備標誌位中的DO_POWER_PAGEBLE;反之,就應該加上DO_POWER_PAGEBLE。
這個請求的根本目的是,Windows操作系統用來查詢設備是否可以在其上建立特殊文件,作爲過濾驅動程序是不應該對這種詢問加以回答的,正確的處理方法是將這個請求發送給下層設備,由下層設備來回答這個問題。但是同時過濾驅動需要監視下層設備的問答,如果下層設備不支持這個請求,自然是最簡單不過的事情,過濾設備什麼都不做即可。反之,如果下層設備支持這個請求,那麼過濾設備就需要進行處理,在下層設備對第一個頁面文件建立請求回答之後,過濾設備需要對DO_POWER_PAGEBLE位進行相應的設置,並且做一個計數。這個計數會隨着頁面文件建立的請求而增加,隨着頁面文件刪除的通知而減少,當減少到最後一個計數的時候,過濾設備又需要對DO_POWER_PAGEBLE位進行相應的設置。
//這個是PnP 管理器用來詢問設備能否支持特殊文件的irp,作爲卷的過濾驅動,我們必須處理
 case IRP_MN_DEVICE_USAGE_NOTIFICATION:
  {
   BOOLEAN setPagable;
   //如果是詢問是否支持休眠文件和dump文件,則直接下發給下層設備去處理
   if (irpsp->Parameters.UsageNotification.Type != DeviceUsageTypePaging)
   {
    ntStatus = DPSendToNextDriver(
     DevExt->LowerDevObj,
     Irp);
    return ntStatus;
   }
   //這裏等一下分頁計數事件
   ntStatus = KeWaitForSingleObject(
    &DevExt->PagingPathCountEvent,
    Executive,
    KernelMode,
    FALSE,
    NULL);

   //setPagable初始化爲假,是沒有設置過DO_POWER_PAGABLE的意思
   setPagable = FALSE;
   if (!irpsp->Parameters.UsageNotification.InPath &&
    DevExt->PagingPathCount == 1 )
   {
    //如果是PnP manager通知我們將要刪去分頁文件,且我們目前只剩下最後一個分頁文件的時候會進入這裏
    if (DeviceObject->Flags & DO_POWER_INRUSH)
    {}
    else
    {
     //到這裏說明沒有分頁文件在這個設備上了,需要設置DO_POWER_PAGABLE這一位了
     DeviceObject->Flags |= DO_POWER_PAGABLE;
     setPagable = TRUE;
    }
   }
   //到這裏肯定是關於分頁文件的是否可建立查詢,或者是刪除的通知,我們交給下層設備去做。這裏需要用同步的方式給下層設備,也就是說要等待下層設備的返回
   ntStatus = DPForwardIrpSync(DevExt->LowerDevObj,Irp);

   if (NT_SUCCESS(ntStatus))
   {
    //如果發給下層設備的請求成功了,說明下層設備支持這個操作,會執行到這裏
    //在成功的條件下我們來改變我們自己的計數值,這樣就能記錄我們現在這個設備上到底有多少個分頁文件
    IoAdjustPagingPathCount(
     &DevExt->PagingPathCount,
     irpsp->Parameters.UsageNotification.InPath);
    if (irpsp->Parameters.UsageNotification.InPath)
    {
     if (DevExt->PagingPathCount == 1)
     {
      //如果這個請求是一個建立分頁文件的查詢請求,並且下層設備支持這個請求,而且這是第一個在這個設備上的分頁文件,那麼我們需要清除DO_POWER_PAGABLE位
      DeviceObject->Flags &= ~DO_POWER_PAGABLE;
     }
    }
   }
   else
   {
    //到這裏說明給下層設備發請求失敗了,下層設備不支持這個請求,這時候我們需要把之前做過的操作還原
    if (setPagable == TRUE)
    {
     //根據setPagable變量的值來判斷我們之前是否做過對DO_POWER_PAGABLE的設置,如果有的話就清楚這個設置
     DeviceObject->Flags &= ~DO_POWER_PAGABLE;
     setPagable = FALSE;
    }
   }
   //設置分頁計數事件
   KeSetEvent(
    &DevExt->PagingPathCountEvent,
    IO_NO_INCREMENT,
    FALSE
    );
   //到這裏我們就可以完成這個irp請求了
   IoCompleteRequest(Irp, IO_NO_INCREMENT);
   return ntStatus;
  }  
4.Power請求的處理
Power請求的處理應和大部分irp請求一樣,直接交給下層設備處理即可。只是在Windows Vista以前的操作系統中,下發所使用的函數是比較特殊的PoCallDriver,而且在這之前還需要使用PoStartNextPowerIrp來處理一下irp請求。這一情況在Windows Vista中得以改變,開發人員只需要使用一般的方法下發這個irp請求即可。本驅動中用了一個編譯宏來判斷當前操作系統的版本。
NTSTATUS
DPDispatchPower(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP   Irp
    )
{
 //用來指向過濾設備的設備擴展的指針
 PDP_FILTER_DEV_EXTENSION DevExt = DeviceObject->DeviceExtension;
#if (NTDDI_VERSION < NTDDI_VISTA)
 //如果是vista以前的版本的windows,需要使用特殊的向下層設備轉發的函數
 PoStartNextPowerIrp(Irp);
 IoSkipCurrentIrpStackLocation(Irp);
 return PoCallDriver(DevExt->LowerDevObj, Irp);
#else
 //如果是vista系統,可以使用和一般下發irp一樣的方法來下發
 return DPSendToNextDriver(
  DevExt->LowerDevObj,
  Irp);
#endif 
}
5.DeviceIoControl請求的處理
DeviceIoControl請求的處理函數是DPDispatchDeviceContorl,作爲一個磁盤卷設備的過濾驅動,理論上是不需要對DeviceIoControl做任何處理的,只需要如實地轉換髮給下層設備即可。但是在這裏本驅動需要截獲一個特殊的DeviceIoControl請求----IOCTL_VOLUME_ONLINE。這個請求是Windows操作系統發出的,它本身的作用是把目標卷設備設置爲在線狀態,在這個狀態設置完畢後,纔會有對這個卷的讀/寫操作發生。
對於這個以還原爲目的的驅動來說,最好是在儘量早的時候就對讀/寫操作進行處理。基於這個理由IOCTL_VOLUME_ONLINE是一個很好的機會,所以在本驅動中,大部分的數據結構等初始化工作都將被放到這個DeviceIoControl的時候完成。
讀者這時候可能會認爲在收到IOCTL_VOLUME_ONLINE這個DeviceIoControl請求的時候直接做初始化工作即可,然後再將這個請求發往下層設備。這裏有一問題在於初始化工作需要目標卷的一些信息,例如需要知道這個卷設備的卷標,因爲這個驅動只保護“D”盤;需要知道這個卷的一些信息,例如卷的大小,因爲初始化bitmap需要這個信息作爲參數,但是這一切都必須要等到過濾驅動的下層設備也就是真正的卷設備開始運行之後才能提供,而卷設備開始運行卻需要這個IOCTL_VOLUME_ONLINE的DeviceIoControl請求發下去。但實際上有一個很簡單的辦法可以解決這個問題,就是讓請求先發送下去,等下層設備處理完畢之後在進行初始化工作,同時由於下發請求的時候採用了同步的方式,因此在完成請求之前是不會有其他請求發生的。
WDM驅動框架爲實現上文所屬的操作提供了相當方便的操作方式,只需要賦值一份irp stack,設置好完成函數和一個等待事件,在調用下層設備之後就開始等待這個事件,當下層設備處理完成之後之前設置的完成函數會被調用,在完成函數中會喚醒剛纔所說的等待事件,於是一切都會順理成章地走下去,當然在完成函數裏上文所述的初始化工作就可以進行了。下面是如何設置完成函數和等待事件的代碼,也就是在DeviceIoControl的分發函數中所做的事情。
NTSTATUS
DPDispatchDeviceControl(
 IN PDEVICE_OBJECT DeviceObject,
 IN PIRP   Irp
 )
{
 //用來指向過濾設備的設備擴展的指針
 PDP_FILTER_DEV_EXTENSION DevExt = DeviceObject->DeviceExtension;
 //返回值
 NTSTATUS ntStatus = STATUS_SUCCESS;
 //用來指向irp stack的指針
 PIO_STACK_LOCATION  irpsp = IoGetCurrentIrpStackLocation(Irp);
 //用來同步IOCTL_VOLUME_ONLINE處理的事件
 KEVENT     Event;
 //用來傳給IOCTL_VOLUME_ONLINE的完成函數的上下文
 VOLUME_ONLINE_CONTEXT context;

 switch (irpsp->Parameters.DeviceIoControl.IoControlCode)
 {
 case IOCTL_VOLUME_ONLINE:
  {
   //如果是卷設備的IOCTL_VOLUME_ONLINE,會進入到這裏
   //我們打算自己處理這個irp請求,這裏先初始化一個事件用來在這個請求的完成函數裏面做同步信號
   KeInitializeEvent(&Event, NotificationEvent, FALSE);
   //給這個請求的完成函數初始化參數
   context.DevExt = DevExt;
   context.Event = &Event;
   //這裏copy一份irp stack
   IoCopyCurrentIrpStackLocationToNext(Irp);
   //設置完成函數
   IoSetCompletionRoutine(
    Irp,
    DPVolumeOnLineCompleteRoutine,
    &context,
    TRUE,
    TRUE,
    TRUE);
   //調用下層設備來處理這個irp
   ntStatus = IoCallDriver(DevExt->LowerDevObj, Irp);
   //等待下層設備處理結束這個irp
   KeWaitForSingleObject(
    &Event,
    Executive,
    KernelMode,
    FALSE,
    NULL);
   //返回
   return ntStatus;
  }
 default:
  //對於其它DeviceIoControl,我們一律調用下層設備去處理
  break;
 }
 return DPSendToNextDriver(DevExt->LowerDevObj,Irp);  
}
從上面的代碼中可以看到,如何在獲取到IOCTL_VOLUME_ONLINE請求的時候設置了名叫DPVolumeOnLineCompleteRoutine的完成函數,這個函數將在下層設備處理完成這個irp的時候被調用,下面看一下這個完成函數裏都做了什麼,這裏需要注意的是,在這個完成函數裏,下層設備所對應的磁盤卷設備已經可以工作了。
在完成函數裏首先獲取了卷的名稱,即常見的C,D,E等盤符,這是通過系統調用獲取到的,如果讀者有興趣,會發現這個系統調用是無法在IOCTL_VOLUME_ONLINE被下發之前使用的。在獲取了這些盤符之後,根據驅動設計,這裏只對"D"盤感興趣,在發現盤符“D”的卷設備之後,首先獲取這個卷的第一個扇區並分析其內容來取得所需信息。如果讀者對之前介紹的DBR還有印象的話,應該會比較容易地搞清這些信息是如何獲取的,這裏就不再對代碼進行分析了,在獲取了卷的信息之後,需要初始化一個bitmap,這個bitmap是還原功能的核心數據結構,具體的作用和實現在下面介紹,這裏讀者只需要知道初始化bitmap的時候需要卷的總大小作爲參數即可。在這些工作都完成之後,將用來表示還原卷的全局變量賦值,在今後運行的讀/寫分發函數和boot驅動回調函數等衆多函數中,都會引用這個全局變量,並根據它的內容來確定哪個是需要保護的卷。下面是完成函數的具體實現過程,在代碼中讀者可以發現,作爲參數被傳入的等待事件在最後被喚醒,這使得上面的DeviceIoControl處理代碼中的等待得以返回,系統調用得以繼續運行下去。
NTSTATUS
DPVolumeOnLineCompleteRoutine(
 IN PDEVICE_OBJECT  DeviceObject,
 IN PIRP  Irp,
 IN PVOLUME_ONLINE_CONTEXT  Context
 )
{
 //返回值
 NTSTATUS ntStatus = STATUS_SUCCESS;
 //這個卷設備的dos名字,也就是C,D等
 UNICODE_STRING  DosName = { 0 };

 //在這裏Context是不可能爲空的,爲空就是出錯了
 ASSERT(Context!=NULL);
 //下面調用我們自己的VolumeOnline處理
 //獲取這個卷的dos名字
 ntStatus = IoVolumeDeviceToDosName(Context->DevExt->PhyDevObj, &DosName);
 if (!NT_SUCCESS(ntStatus))
  goto ERROUT;
 //將dos名字變成大寫形式
 Context->DevExt->VolumeLetter = DosName.Buffer[0];
 if (Context->DevExt->VolumeLetter > L'Z')
  Context->DevExt->VolumeLetter -= (L'a' - L'A');
 //我們只保護“D”盤
 if (Context->DevExt->VolumeLetter == L'D')
 {
  //獲取這個卷的基本信息
  ntStatus = DPQueryVolumeInformation(
   Context->DevExt->PhyDevObj,
   &(Context->DevExt->TotalSizeInByte),
   &(Context->DevExt->ClusterSizeInByte),
   &(Context->DevExt->SectorSizeInByte));
  if (!NT_SUCCESS(ntStatus))
  {
   goto ERROUT;
  }
  //建立這個卷對應的位圖
  ntStatus = DPBitmapInit(
   &Context->DevExt->Bitmap,
   Context->DevExt->SectorSizeInByte,
   8,
   25600,
   (DWORD)(Context->DevExt->TotalSizeInByte.QuadPart /
   (LONGLONG)(25600 * 8 * Context->DevExt->SectorSizeInByte)) + 1);
  if (!NT_SUCCESS(ntStatus))
   goto ERROUT;
  //對全局量賦值,說明我們找到需要保護的那個設備了
  gProtectDevExt = Context->DevExt;
 }
 
ERROUT:
 if (!NT_SUCCESS(ntStatus))
 {
  if (NULL != Context->DevExt->Bitmap)
  {
   DPBitmapFree(Context->DevExt->Bitmap);
  }
  if (NULL != Context->DevExt->TempFile)
  {
   ZwClose(Context->DevExt->TempFile);
  }
 }
 if (NULL != DosName.Buffer)
 {
  ExFreePool(DosName.Buffer);
 }
 //設置等待同步事件,這樣可以讓我們等待的DeviceIoControl處理過程繼續運行
 KeSetEvent(
  Context->Event,
  0,
  FALSE);
 return STATUS_SUCCESS;
}

6.bitmap的作用和分析
在上面的分析中讀者已經多次看到了bitmap,但卻一直不知道它具體是什麼,它的作用是什麼,爲什麼要用它,他是如何實現的?下面將會解答這些問題。在做進一步說明之前需要提到的是,具體分析bitmap的實現比較複雜,如果讀者對算法沒有特殊興趣的話,則可以只看bitmap的接口說明而不去管它的具體實現過程,這並不會影響對之後內容的理解。
顧名思義,bitmap就是一個位圖。它實際上是一些內存塊,這些內存塊的每一個位用來標識一個磁盤上的最小訪問單位,一般情況下是一個扇區。每一個位可以被置位或者被清除,用來表示這個扇區對應的兩種狀態。
如果讀者對開始時所描述的還原理論還有印象的話,應該知道作爲一個還原驅動,核心的問題在於如何將寫入的數據存儲在其他地方,而在讀取時又能夠準確的從其他地方找到,爲了達到這個目的就必須使用bitmap。bitmap中的每一位對應的是磁盤上的一個扇區,有多少個扇區就有多少位。這個位爲0所代表的意義是,是這個爲所對應的扇區的數據沒有被存儲到其他地方,而反之則代表這個扇區的數據被存儲到了其他地方。在寫數據的時候,根據操作範圍可以講bitmap中對應的區域置爲1,在讀操作的時候則又會根據bitmap的內容把置爲1的扇區從轉存的地方讀回來;而對bitmap爲0的地方還是從原有設備上讀取數據,這樣bitmap就成了在這次系統啓動生命週期中所有寫操作的標誌知道重啓系統,在重啓過後bitmap又將恢復爲全0狀態,這時無論是什麼讀操作都不會從轉存處拿數據,也就達到了還原的功能。
之所以說bitmap是一些內存塊而不是一個連續的內存,是因爲在設計bitmap的時候考慮到它所表示的位圖可能對應着很大的一塊磁盤區域,即使是用一位來表示512B的數據也有可能會是很大的一片內存空間。所以在設計bitmap的時候要求它能夠按需分配內存,在用到的時候纔去分配對應的內存,這樣可以節約大量的內存空間。要知道這裏所說的內存空間都是指非分頁內存,這一部分內存即使是在內核中也是非常寶貴的。
首先來看一下bitmap的內部數據結構
typedef struct _DP_BITMAP_
{
 //這個卷中的每個扇區有多少字節,這同樣也說明了bitmap中一個位所對應的字節數
    unsigned long sectorSize;
 //每個byte裏面有幾個bit,一般情況下是
    unsigned long byteSize;
 //每個塊是多大byte,
    unsigned long regionSize;
 //這個bitmap總共有多少個塊
    unsigned long regionNumber;
 //這個塊對應了多少個實際的byte,這個數字應該是sectorSize*byteSize*regionSize
    unsigned long regionReferSize;
 //這個bitmap對應了多少個實際的byte,這個數字應該是sectorSize*byteSize*regionSize*regionNumber
    __int64 bitmapReferSize;
 //指向bitmap存儲空間的指針
    tBitmap** Bitmap;
 //用於存取bitmap的鎖
    void* lockBitmap;
} DP_BITMAP, * PDP_BITMAP;
可以看到bitmap的最上層是1字節類型的指針的指針bitmap,在這裏希望讀者把這個指針理解成一個指針數組,數組有regionSize個元素,每個元素就是一個指向所謂的內存塊的指針。在一開始的時候這些指向內存塊的指針都是空指針,這時它們代表了一個內容爲0的內存塊,只是實際的內存沒有被分配出來。當需要將其中任何一位設置爲1的時候,這個內存塊會首先被分配,在清零之後再對其中需要設置爲1的位進行設置,這就是所說的按需分配,也是節約空間的關鍵所在。下面是初始化這個數據結構的代碼,用戶通過指定bitmap的參數來初始化一個bitmap,在這裏用戶需要知道這個bitmap一共代表了多大的區域;同時需要給定一個塊的大小,這個大小設得太大可能造成分配空間的浪費,設得太小又會使得塊的數目太多,所以一般需要設一個合適的中間值。下面初始化一個bitmap的代碼
NTSTATUS DPBitmapInit(
 DP_BITMAP **     bitmap,
 unsigned long       sectorSize,
 unsigned long       byteSize,
 unsigned long       regionSize,
 unsigned long       regionNumber
 )
{
 int i = 0;
 DP_BITMAP * myBitmap = NULL;
 NTSTATUS status = STATUS_SUCCESS;

 //檢查參數,以免使用了錯誤的參數導致發生處零錯等錯誤
 if (NULL == bitmap || 0 == sectorSize ||
  0 == byteSize || 0 == regionSize  || 0 == regionNumber)
 {
  return STATUS_UNSUCCESSFUL;
 }
 __try
 {
  //分配一個bitmap結構,這是無論如何都要分配的,這個結構相當於一個bitmap的handle 
  if (NULL == (myBitmap = (DP_BITMAP*)DPBitmapAlloc(0, sizeof(DP_BITMAP))))
  {
   status = STATUS_INSUFFICIENT_RESOURCES;
   __leave;
  }
  //清空結構
  memset(myBitmap, 0, sizeof(DP_BITMAP));
  //根據參數對結構中的成員進行賦值
  myBitmap->sectorSize = sectorSize;
  myBitmap->byteSize = byteSize;
  myBitmap->regionSize = regionSize;
  myBitmap->regionNumber = regionNumber;
  myBitmap->regionReferSize = sectorSize * byteSize * regionSize;
  myBitmap->bitmapReferSize = (__int64)sectorSize * (__int64)byteSize * (__int64)regionSize * (__int64)regionNumber;
  //分配出regionNumber那麼多個指向region的指針,這是一個指針數組
  if (NULL == (myBitmap->Bitmap = (tBitmap **)DPBitmapAlloc(0, sizeof(tBitmap*) * regionNumber)))
  {
   status = STATUS_INSUFFICIENT_RESOURCES;
   __leave;
  }
  //清空指針數組
  memset(myBitmap->Bitmap, 0, sizeof(tBitmap*) * regionNumber);
  * bitmap = myBitmap;
  status = STATUS_SUCCESS;
 }
 __except(EXCEPTION_EXECUTE_HANDLER)
 {
  status = STATUS_UNSUCCESSFUL;
 }
 if (!NT_SUCCESS(status))
 {
  if (NULL != myBitmap)
  {
   DPBitmapFree(myBitmap);
  }
  * bitmap = NULL;
 }
 return status;
}
在上面的代碼中可以看出,初始化bitmap的過程中僅僅分配了很少一部分內存。而這時這個bitmap卻是完全可用的,只有在對其進行位設置的時候纔會有新的內存被分配出來。
bitmap提供了一個接口用來將其中的某一區域置位,因爲在bitmap的初始化過程中所有的位都認爲是0,而在今後使用的過程中也看不出來需要將1變成0的可能,這就使得這裏只需要提供置位的接口即可,而不需要清除位的接口。這個接口函數需要考慮的第一個問題是,在所需要的目標bitmap內存區域沒有被分配的時候需要先分配才能置位。需要考慮的第二個問題是,如何能夠儘快的完成一個對一長段連續的bitmap做位置的請求。下面請看這兩個問題的具體處理方式
NTSTATUS DPBitmapSet(
 DP_BITMAP *      bitmap,
 LARGE_INTEGER       offset,
 unsigned long       length
 )
{
 __int64 i = 0;
 unsigned long myRegion = 0, myRegionEnd = 0;
 unsigned long myRegionOffset = 0, myRegionOffsetEnd = 0;
 unsigned long myByteOffset = 0, myByteOffsetEnd = 0;
 unsigned long myBitPos = 0;
 NTSTATUS status = STATUS_SUCCESS;
 LARGE_INTEGER setBegin = { 0 }, setEnd = { 0 };

 __try
 {
  //檢查變量
  if (NULL == bitmap || offset.QuadPart < 0)
  {
   status = STATUS_INVALID_PARAMETER;
   __leave;
  }
  if (0 != offset.QuadPart % bitmap->sectorSize || 0 != length % bitmap
   ->sectorSize)
  {
   status = STATUS_INVALID_PARAMETER;
   __leave;
  }

  //根據要設置的偏移量和長度來計算需要使用到哪些region,如果需要的話,就分配他們指向的內存空間
  myRegion = (unsigned long)(offset.QuadPart / (__int64)bitmap->regionReferSize);
  myRegionEnd = (unsigned long)((offset.QuadPart + (__int64)length) / (__int64)bitmap->regionReferSize);
  for (i = myRegion; i <= myRegionEnd; ++i)
  {
   if (NULL == *(bitmap->Bitmap + i))
   {
    if (NULL == (*(bitmap->Bitmap + i) = (tBitmap*)DPBitmapAlloc(0, sizeof(tBitmap) * bitmap->regionSize)))
    {
     status = STATUS_INSUFFICIENT_RESOURCES;
     __leave;
    }
    else
    {
     memset(*(bitmap->Bitmap + i), 0, sizeof(tBitmap) * bitmap->regionSize);
    }
   }
  }

  //開始設置bitmap,首先我們需要將要設置的區域按照byte對齊,這樣可以按byte設置而不需要按bit設置,加快設置速度
  //對於沒有byte對齊的區域先手工設置掉他們
  for (i = offset.QuadPart; i < offset.QuadPart + (__int64)length; i += bitmap->sectorSize)
  {
   myRegion = (unsigned long)(i / (__int64)bitmap->regionReferSize);
   myRegionOffset = (unsigned long)(i % (__int64)bitmap->regionReferSize);
   myByteOffset = myRegionOffset / bitmap->byteSize / bitmap->sectorSize;
   myBitPos = (myRegionOffset / bitmap->sectorSize) % bitmap->byteSize;
   if (0 == myBitPos)
   {
    setBegin.QuadPart = i;
    break;
   }
   *(*(bitmap->Bitmap + myRegion) + myByteOffset) |= bitmapMask[myBitPos];
  }
  if (i >= offset.QuadPart + (__int64)length)
  {
   status = STATUS_SUCCESS;
   __leave;
  }

  for (i = offset.QuadPart + (__int64)length - bitmap->sectorSize; i >= offset.QuadPart; i -= bitmap->sectorSize)
  {
   myRegion = (unsigned long)(i / (__int64)bitmap->regionReferSize);
   myRegionOffset = (unsigned long)(i % (__int64)bitmap->regionReferSize);
   myByteOffset = myRegionOffset / bitmap->byteSize / bitmap->sectorSize;
   myBitPos = (myRegionOffset / bitmap->sectorSize) % bitmap->byteSize;
   if (7 == myBitPos)
   {
    setEnd.QuadPart = i;
    break;
   }
   *(*(bitmap->Bitmap + myRegion) + myByteOffset) |= bitmapMask[myBitPos];
  }

  if (i < offset.QuadPart || setEnd.QuadPart == setBegin.QuadPart)
  {
   status = STATUS_SUCCESS;
   __leave;
  }

  myRegionEnd = (unsigned long)(setEnd.QuadPart / (__int64)bitmap->regionReferSize);

  for (i = setBegin.QuadPart; i <= setEnd.QuadPart;)
  {
   myRegion = (unsigned long)(i / (__int64)bitmap->regionReferSize);
   myRegionOffset = (unsigned long)(i % (__int64)bitmap->regionReferSize);
   myByteOffset = myRegionOffset / bitmap->byteSize / bitmap->sectorSize;
   //如果我們設置的區域沒有跨兩個region,只需要使用memset去做按byte的設置然後跳出即可
   if (myRegion == myRegionEnd)
   {
    myRegionOffsetEnd = (unsigned long)(setEnd.QuadPart % (__int64)bitmap->regionReferSize);
    myByteOffsetEnd = myRegionOffsetEnd / bitmap->byteSize / bitmap->sectorSize;
    memset(*(bitmap->Bitmap + myRegion) + myByteOffset, 0xff, myByteOffsetEnd - myByteOffset + 1);
    break;
   }
   //如果我們設置的區域跨了兩個region,需要設置完後遞增
   else
   {
    myRegionOffsetEnd = bitmap->regionReferSize;
    myByteOffsetEnd = myRegionOffsetEnd / bitmap->byteSize / bitmap->sectorSize;
    memset(*(bitmap->Bitmap + myRegion) + myByteOffset, 0xff, myByteOffsetEnd - myByteOffset);
    i += (myByteOffsetEnd - myByteOffset) * bitmap->byteSize * bitmap->sectorSize;
   }
  }
  status = STATUS_SUCCESS;
 }
 __except(EXCEPTION_EXECUTE_HANDLER)
 {
  status = STATUS_UNSUCCESSFUL;
 }

 if (!NT_SUCCESS(status))
 {
  
 }
 return status;
}
在上面的代碼筆者略去對具體的位設置過程的講解,這只是普通的四則混合運算,請讀者根據驅動加以理解。讀者可以在上面的代碼中看到,設置位的函數是如何先通過計算確定需要使用哪些塊,並且在需要的時候分配它們的,然後是如何儘可能地按照一個字而不是按照一個位來對所需要的位進行設置。
除了位置之外,bitmap也需要提供一個能夠測試指定位圖區域是全部爲1還是全部爲0,異或兼而有之的接口。這個接口在於,用戶可以通過測試的結果決定如何進行下一步的操作。這個測試函數的代碼比較簡單,只是根據內存的數據來進行判斷,這裏就不列舉代碼了。
最後,bitmap在完成了設置和測試的功能之後,還需要提供一個獲取指定區域位圖的接口,在後面的分析中讀者可以看到,這個獲取指定區域的位圖操作一定是伴隨着磁盤讀操作而來的。之前反覆強調如果是讀操作的話,對於bitmap設置爲1取了指定區域的位圖之後,需要根據這個位圖中的0和1來決定最終生成的數據哪一部分是從原始數據中來,哪一部分是從轉存數據中來。由於使用環境的特殊性,這個接口被演變成爲將兩個內存緩衝區的內容根據指定的bitmap來進行合併操作,讀者應該很容易想到這兩個緩衝區一個是讀取自轉存的數據,一個是讀取自原始的數據。這個函數的代碼如下:
NTSTATUS DPBitmapGet(
 DP_BITMAP *    bitmap,
 LARGE_INTEGER     offset,
 unsigned long     length,
 void *            bufInOut,
 void *            bufIn
 )
{
 unsigned long i = 0;
 unsigned long myRegion = 0;
 unsigned long myRegionOffset = 0;
 unsigned long myByteOffset = 0;
 unsigned long myBitPos = 0;
 NTSTATUS status = STATUS_SUCCESS;

 __try
 {
  //檢查參數
  if (NULL == bitmap || offset.QuadPart < 0 || NULL == bufInOut || NULL == bufIn)
  {
   status = STATUS_INVALID_PARAMETER;
   __leave;
  }
  if (0 != offset.QuadPart % bitmap->sectorSize || 0 != length % bitmap->sectorSize)
  {
   status = STATUS_INVALID_PARAMETER;
   __leave;
  }

  //遍歷需要獲取的位圖範圍,如果出現了位被設置爲,就需要用bufIn參數中指向的相應位置的數據拷貝到bufInOut中
  for (i = 0; i < length; i += bitmap->sectorSize)
  {
   myRegion = (unsigned long)((offset.QuadPart + (__int64)i) / (__int64)bitmap->regionReferSize);

   myRegionOffset = (unsigned long)((offset.QuadPart + (__int64)i) % (__int64)bitmap->regionReferSize);

   myByteOffset = myRegionOffset / bitmap->byteSize / bitmap->sectorSize;

   myBitPos = (myRegionOffset / bitmap->sectorSize) % bitmap->byteSize;

   if (NULL != *(bitmap->Bitmap + myRegion) && (*(*(bitmap->Bitmap + myRegion) + myByteOffset) &bitmapMask[myBitPos]))
   {
    memcpy((tBitmap*)bufInOut + i, (tBitmap*)bufIn + i, bitmap->sectorSize);
   }
  }

  status = STATUS_SUCCESS;
 }
 __except(EXCEPTION_EXECUTE_HANDLER)
 {
  status = STATUS_UNSUCCESSFUL;
 }

 return status;
}
7.boot驅動完成回調函數和稀疏文件
到這裏爲止,離最終的讀/寫轉向處理只有最後的一點準備工作了。而這個準備工作放在boot驅動完成回/調函數中,至於爲什麼要放在這裏,則是由於本驅動採用的轉存緩衝區的機制決定的。
前面已經反覆強調這個驅動會將寫入保護磁盤的數據轉存到另一個地方,那麼這個地方在哪?在此本驅動使用了一個最爲簡單的方法----把數據轉存到另一個卷的稀疏文件中。稀疏文件是NTFS文件系統的一個特有的概念,他就好像上一節課所說的bitmap一樣,建立時可以表示很大的空間,但是卻完全不佔用實際的存儲空間,只有在向其寫入數據的時候纔會使用到真正的存儲空間。這就是說可以在一個容量只有1GB的磁盤捲上建立一個大小爲10GB的稀疏文件,程序可以對這個10GB空間中的人很一個位置進行讀/寫操作,但是寫入的總數據量不能超過1GB。至於爲什麼將這個稀疏文件放在了另一個磁盤上,主要是因爲如果放在同一個磁盤捲上,在寫入這個文件的時候勢必會被過濾驅動捕獲,這就形成了一個典型的重入。當然這種重入是很容易避免的,但是爲了不引起不必要的麻煩,這個用於教學目的的驅動就使用了另一個卷作爲轉儲的空間,這樣就從根本上避免了重入問題。
那麼在開始所說的準備工作又是什麼呢?這個工作實際上就是準備好這個稀疏文件,建立它,設置它的大小並且打開它。那麼爲什麼需要在boot驅動完成函數中做這些事情呢?這是因爲稀疏文件的操作是依賴於文件系統的,作爲文件系統的驅動程序,NTFS驅動是一個boot型驅動,但是它只有在卷設備開始工作了之後纔會將自己的處理設備附加到這個捲上,從而響應對這個卷的所有文件請求。這就說明在之前無論是AddDevice函數還是VOLUME_ONLINE的DeviceIoControl中,NTFS文件都是不能讀/寫的。而在boot驅動的完成回調函數中,所有的boot驅動都已經加載完畢,NTFS自然也不例外,這時對於NTFS文件的讀./寫就輕而易舉了。下面看一下最後一步準備工作的代碼。
VOID
DPReinitializationRoutine(
 IN PDRIVER_OBJECT DriverObject,
 IN PVOID   Context,
 IN ULONG   Count
 )
{
 //返回值
 NTSTATUS ntStatus;
 //D盤的緩衝文件名
 WCHAR    SparseFilename[] = L"\\??\\E:\\temp.dat";
 UNICODE_STRING  SparseFilenameUni;
 //建立文件時的io操作狀態值
 IO_STATUS_BLOCK     ios = { 0 };
 //建立文件時的對象屬性變量
 OBJECT_ATTRIBUTES    ObjAttr = { 0 };
 //設置文件大小的時候使用的文件結尾描述符
 FILE_END_OF_FILE_INFORMATION    FileEndInfo = { 0 };

 //打開我們將要用來做轉儲的文件
 //初始化要打開的文件名
 RtlInitUnicodeString(&SparseFilenameUni,SparseFilename);
 //初始化文件名對應的對象名,這裏需要將其初始化爲內核對象,並且大小寫不敏感
 InitializeObjectAttributes(
  &ObjAttr,
  &SparseFilenameUni,
  OBJ_KERNEL_HANDLE|OBJ_CASE_INSENSITIVE,
  NULL,
  NULL);
 //建立文件,這裏需要注意的是,要加入FILE_NO_INTERMEDIATE_BUFFERING選項,避免文件系統再緩存這個文件
 ntStatus = ZwCreateFile(
  &gProtectDevExt->TempFile,
  GENERIC_READ | GENERIC_WRITE,
  &ObjAttr,
  &ios,
  NULL,
  FILE_ATTRIBUTE_NORMAL,
  0,
  FILE_OVERWRITE_IF,
  FILE_NON_DIRECTORY_FILE |
  FILE_RANDOM_ACCESS |
  FILE_SYNCHRONOUS_IO_NONALERT |
  FILE_NO_INTERMEDIATE_BUFFERING,
  NULL,
  0);
 if(!NT_SUCCESS(ntStatus))
 {
  goto ERROUT;
 }
 //設置這個文件爲稀疏文件
 ntStatus = ZwFsControlFile(
  gProtectDevExt->TempFile,
  NULL,
  NULL,
  NULL,
  &ios,
  FSCTL_SET_SPARSE,
  NULL,
  0,
  NULL,
  0);
 if(!NT_SUCCESS(ntStatus))
 {
  goto ERROUT;
 }
 //設置這個文件的大小爲"D"盤的大小並且留出m的保護空間
 FileEndInfo.EndOfFile.QuadPart = gProtectDevExt->TotalSizeInByte.QuadPart + 10*1024*1024;
 ntStatus = ZwSetInformationFile(
  gProtectDevExt->TempFile,
  &ios,
  &FileEndInfo,
  sizeof(FILE_END_OF_FILE_INFORMATION),
  FileEndOfFileInformation
  );
 if (!NT_SUCCESS(ntStatus))
 {
  goto ERROUT;
 }
 //如果成功初始化就將這個卷的保護標誌設置爲在保護狀態
 gProtectDevExt->Protect = TRUE;
 return;
ERROUT:
 KdPrint(("error create temp file!\n"));
 return;
}
可以看到,在準備工作中首先建立了預先制定好文件名的文件,並將其屬性設置爲稀疏文件,之後通過設這文件結尾的方法將這個文件的大小變爲之前獲取到的“D”盤的大小。這時所有準備工作都已經齊備了,將保護標誌設置爲真,本驅動中最爲核心的數據轉儲過程即將開始。
8.讀/寫請求的處理
在本驅動中,最爲核心的部分就是讀/寫請求的處理部分。所有的讀/寫請求必須按照順序以同步的方式處理,只有上一個操作被處理完成之後,下一個操作纔可以開始被處理。這是因爲過濾驅動內部的bitmap設置,讀取,轉存文件的讀/寫等操作是無法做到並行處理的,如果不進行讀/寫請求的順序化,則有可能帶來讀/寫不同步的問題,即一個寫操作還沒有完成,另一個讀取操作又將來到,這會造成後來的讀取數據不正確。爲了達到這個目的,對所有流經過濾設備的磁盤卷設備讀/寫請求,除了不需要保護的卷之外,其他的必須全部順序放入一個處理隊列中,由一個處理線程對這個隊列中的請求進行順序處理。下面看一下這段代碼:
NTSTATUS
DPDispatchReadWrite(
    IN PDEVICE_OBJECT DeviceObject,
    IN PIRP   Irp
    )

 //用來指向過濾設備的設備擴展的指針
 PDP_FILTER_DEV_EXTENSION DevExt = DeviceObject->DeviceExtension;
 //返回值
 NTSTATUS ntStatus = STATUS_SUCCESS;

 if (DevExt->Protect)
 {
  //這個卷在保護狀態,
  //我們首先把這個irp設爲pending狀態
  IoMarkIrpPending(Irp);
  //然後將這個irp放進相應的請求隊列裏
  ExInterlockedInsertTailList(
   &DevExt->ReqList,
   &Irp->Tail.Overlay.ListEntry,
   &DevExt->ReqLock
   );
  //設置隊列的等待事件,通知隊列對這個irp進行處理
  KeSetEvent(
   &DevExt->ReqEvent,
   (KPRIORITY)0,
   FALSE);
  //返回pending狀態,這個irp就算處理完了
  return STATUS_PENDING;
 }
 else
 {
  //這個卷不在保護狀態,直接交給下層設備進行處理
  return DPSendToNextDriver(
   DevExt->LowerDevObj,
   Irp);
 }
}
在上面的代碼中可以看出,首先對作爲參數傳入的設備對象擴展中的保護位進行判斷,這一位是在boot驅動結束回調函數中進行設置的,並且僅僅對“D”磁盤卷的設備擴展進行設置。如果這一位爲非保護狀態,過濾驅動將會把這個讀/寫請求直接發向下層設備去處理;反之,如果這一位是保護狀態,過濾驅動將會把這個請求設置爲等待處理狀態,然後將其插入到爲了這個設備所準備的隊列中,並且通過設置隊列同步事件來通知處理線程對這個請求進行處理。
至此,處理隊列中已經塞滿了等待處理的讀/寫請求,而處理線程會忙於將這些請求分門別類地處理好。下面會講解處理線程中的代碼。由於這一段代碼是如此重要,這裏需要將其分爲好幾段來講解。
首先是處理線程函數中只運行一遍的部分,包括變量的聲明和對這個線程優先級的設置,由於這裏不需要這個線程以非常高的優先級運行,所以將線程的優先級設置爲低。
 //NTSTATUS類型的函數返回值
 NTSTATUS     ntStatus = STATUS_SUCCESS;
 //用來指向過濾設備的設備擴展的指針
 PDP_FILTER_DEV_EXTENSION DevExt = (PDP_FILTER_DEV_EXTENSION)Context;
 //請求隊列的入口
 PLIST_ENTRY   ReqEntry = NULL;
 //irp指針
 PIRP    Irp = NULL;
 //irp stack指針
 PIO_STACK_LOCATION Irpsp = NULL;
 //irp中包括的數據地址
 PBYTE    sysBuf = NULL;
 //irp中的數據長度
 ULONG    length = 0;
 //irp要處理的偏移量
 LARGE_INTEGER  offset = { 0 };
 //文件緩衝指針
 PBYTE    fileBuf = NULL;
 //設備緩衝指針
 PBYTE    devBuf = NULL;
 //io操作狀態
 IO_STATUS_BLOCK  ios;

 //設置這個線程的優先級
 KeSetPriorityThread(KeGetCurrentThread(), LOW_REALTIME_PRIORITY);
接下來就是線程中的無限循環部分。讀者應該知道對於一個線程來說,其中必須有一個不會退出的循環體作爲線程的工作主體部分,如果這個縣城需要結束的話,一般會通過退出這個循環體來結束線程。由於在線程外無法通過api調用的方式結束線程,所以在每個線程循環體裏一般會通過一個全局變量進行線程是否需要退出的判斷,如果在線程外的任何地方將這個全局變量設置爲退出,那麼在線程循環下一次運行到這個位置時候就會自己跳出循環,結束自己。
//下面是線程的實現部分,這個循環永不退出
 for (;;)
 { 
  //先等待請求隊列同步事件,如果隊列中沒有irp需要處理,我們的線程就等待在這裏,讓出cpu時間給其它線程
  KeWaitForSingleObject(
   &DevExt->ReqEvent,
   Executive,
   KernelMode,
   FALSE,
   NULL
   );
  //如果有了線程結束標誌,那麼就在線程內部自己結束自己
  if (DevExt->ThreadTermFlag)
  {
   //這是線程的唯一退出地點
   PsTerminateSystemThread(STATUS_SUCCESS);
   return;
  }
下面就輪到真正的請求處理邏輯了。首先需要從處理請求隊列中取出一個請求來,這裏通過帶有鎖機制的操作將處理請求隊列頭上的請求取出。由於在插入隊列的時候是從隊列的尾部插入,這樣就保證了是按照插入的順序來進行請求處理的。在獲取到了請求之後,可以根據請求中的參數對一些局部變量進行賦值。
  //從請求隊列的首部拿出一個請求來準備處理,這裏使用了自旋鎖機制,所以不會有衝突
  while (ReqEntry = ExInterlockedRemoveHeadList(
   &DevExt->ReqList,
   &DevExt->ReqLock
   ))
  {
   //從隊列的入口裏找到實際的irp的地址
   Irp = CONTAINING_RECORD(ReqEntry, IRP, Tail.Overlay.ListEntry);
   //取得irp stack
   Irpsp = IoGetCurrentIrpStackLocation(Irp);
   //獲取這個irp其中包含的緩存地址,這個地址可能來自mdl,也可能就是直接的緩衝,這取決於我們當前設備的io方式是buffer還是direct方式
   if (NULL == Irp->MdlAddress)
    sysBuf = (PBYTE)Irp->UserBuffer;
   else
    sysBuf = (PBYTE)MmGetSystemAddressForMdlSafe(Irp->MdlAddress, NormalPagePriority);
下面輪到了對讀請求的處理,通過前面幾節的反覆說明,讀者應該對如何處理讀請求做到心中有數。這裏首先根據需要讀取的範圍對bitmap中響應的範圍進行測試,如果測試的結果是這些數據全部在原始盤上,那麼這個請求就被直接發給下層設備去處理。如果發現這些數據全部在轉存文件中,就通過對轉存文件的讀取來獲得數據,並完成這個irp請求。這裏需要說明的是,如果出現這種情況,那麼一定是之前有寫請求將這一範圍內的數據寫入了轉存文件中。如果發現需要讀取的目標範圍中的一部分在轉存文件中,另一部分在實際磁盤上,就首先需要通過下層設備發送請求來獲取真實磁盤上的數據,然後通過讀取轉存文件來獲取轉儲的數據,最後通過bitmap的相應接口函數將兩個讀取的數據按照bitmap的指示進行合併,在完成這個讀irp請求。
   if (IRP_MJ_READ == Irpsp->MajorFunction)
   {
    //如果是讀的irp請求,我們在irp stack中取得相應的參數作爲offset和length
    offset = Irpsp->Parameters.Read.ByteOffset;
    length = Irpsp->Parameters.Read.Length;
   }
   else if (IRP_MJ_WRITE == Irpsp->MajorFunction)
   {
    //如果是寫的irp請求,我們在irp stack中取得相應的參數作爲offset和length
    offset = Irpsp->Parameters.Write.ByteOffset;
    length = Irpsp->Parameters.Write.Length;
   }
   else
   {
    //除此之外,offset和length都是
    offset.QuadPart = 0;
    length = 0;
   }
   if (NULL == sysBuf || 0 == length)
   {
    //如果傳下來的irp沒有系統緩衝或者緩衝的長度是,那麼我們就沒有必要處理這個irp,直接下發給下層設備就行了
    goto ERRNEXT;
   }
   //下面是轉儲的過程了
   if (IRP_MJ_READ == Irpsp->MajorFunction)
   {
    //這裏是讀的處理
    //首先根據bitmap來判斷這次讀操作讀取的範圍是全部爲轉儲空間,還是全部爲未轉儲空間,或者兼而有之
    long tstResult = DPBitmapTest(DevExt->Bitmap, offset, length);
    switch (tstResult)
    {
    case BITMAP_RANGE_CLEAR:
     //這說明這次讀取的操作全部是讀取未轉儲的空間,也就是真正的磁盤上的內容,我們直接發給下層設備去處理
     goto ERRNEXT;
    case BITMAP_RANGE_SET:
     //這說明這次讀取的操作全部是讀取已經轉儲的空間,也就是緩衝文件上的內容,我們從文件中讀取出來,然後直接完成這個irp
     //分配一個緩衝區用來從緩衝文件中讀取
     if (NULL == (fileBuf = (PBYTE)ExAllocatePoolWithTag(NonPagedPool, length, 'xypD')))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     RtlZeroMemory(fileBuf,length);
     ntStatus = ZwReadFile(
      DevExt->TempFile,
      NULL,
      NULL,
      NULL,
      &ios,
      fileBuf,
      length,
      &offset,
      NULL);
     if (NT_SUCCESS(ntStatus))
     {
      Irp->IoStatus.Information = length;
      RtlCopyMemory(sysBuf,fileBuf,Irp->IoStatus.Information);
      goto ERRCMPLT;
     }
     else
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     break;

    case BITMAP_RANGE_BLEND:
     //這說明這次讀取的操作是混合的,我們也需要從下層設備中讀出,同時從文件中讀出,然後混合並返回
     //分配一個緩衝區用來從緩衝文件中讀取
     if (NULL == (fileBuf = (PBYTE)ExAllocatePoolWithTag(NonPagedPool, length, 'xypD')))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     RtlZeroMemory(fileBuf,length);
     //分配一個緩衝區用來從下層設備中讀取
     if (NULL == (devBuf = (PBYTE)ExAllocatePoolWithTag(NonPagedPool, length, 'xypD')))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     RtlZeroMemory(devBuf,length);
     ntStatus = ZwReadFile(
      DevExt->TempFile,
      NULL,
      NULL,
      NULL,
      &ios,
      fileBuf,
      length,
      &offset,
      NULL);
     if (!NT_SUCCESS(ntStatus))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     //把這個irp發給下層設備去獲取需要從設備上讀取的信息
     ntStatus = DPForwardIrpSync(DevExt->LowerDevObj,Irp);
     if (!NT_SUCCESS(ntStatus))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     //將從下層設備獲取到的數據存儲到devBuf中
     memcpy(devBuf, sysBuf, Irp->IoStatus.Information);
     //把從文件獲取到的數據和從設備獲取到的數據根據相應的bitmap值來進行合併,合併的結果放在devBuf中
     ntStatus = DPBitmapGet(
      DevExt->Bitmap,
      offset,
      length,
      devBuf,
      fileBuf
      );
     if (!NT_SUCCESS(ntStatus))
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      Irp->IoStatus.Information = 0;
      goto ERRERR;
     }
     //把合併完的數據存入系統緩衝並完成irp
     memcpy(sysBuf, devBuf, Irp->IoStatus.Information);
     goto ERRCMPLT;
    default:
     ntStatus = STATUS_INSUFFICIENT_RESOURCES;
     goto ERRERR;
    }
   }
對於寫的操作處理起來很簡單,因爲只要發到這裏的請求必定需要寫到轉存文件中的。由於使用了稀疏文件,所以這個文件的可尋址範圍和被保存磁盤的大小是相同的。轉儲操作就成了只需要直接寫入文件即可。這裏需要注意的是,要先寫入轉存文件,直到寫入成功之後,纔可以設置bitmap中相應的區域;如果反過來的話,很可能出現寫入不成功但是bitmap修改成功的情況。
   else
   {
    //這裏是寫的過程
    //對於寫,我們直接寫緩衝文件,而不會寫磁盤數據,這就是所謂的轉儲,但是轉儲之後需要在bitmap中做相應的標記
    ntStatus = ZwWriteFile(
     DevExt->TempFile,
     NULL,
     NULL,
     NULL,
     &ios,
     sysBuf,
     length,
     &offset,
     NULL);
    if(!NT_SUCCESS(ntStatus))
    {
     ntStatus = STATUS_INSUFFICIENT_RESOURCES;
     goto ERRERR;
    }
    else
    {
     if (NT_SUCCESS(ntStatus = DPBitmapSet(DevExt->Bitmap, offset, length)))
     {
      goto ERRCMPLT;
     }
     else
     {
      ntStatus = STATUS_INSUFFICIENT_RESOURCES;
      goto ERRERR;
     }
    }
   }
ERRERR:
   if (NULL != fileBuf)
   {
    ExFreePool(fileBuf);
    fileBuf = NULL;
   }
   if (NULL != devBuf)
   {
    ExFreePool(devBuf);
    devBuf = NULL;
   }
   DPCompleteRequest(
    Irp,
    ntStatus,
    IO_NO_INCREMENT
    );
   continue;
ERRNEXT:
   if (NULL != fileBuf)
   {
    ExFreePool(fileBuf);
    fileBuf = NULL;
   }
   if (NULL != devBuf)
   {
    ExFreePool(devBuf);
    devBuf = NULL;
   } 
   DPSendToNextDriver(
    DevExt->LowerDevObj,
    Irp);
   continue;
ERRCMPLT:
   if (NULL != fileBuf)
   {
    ExFreePool(fileBuf);
    fileBuf = NULL;
   }
   if (NULL != devBuf)
   {
    ExFreePool(devBuf);
    devBuf = NULL;
   }
   DPCompleteRequest(
    Irp,
    STATUS_SUCCESS,
    IO_DISK_INCREMENT
    );
   continue;
   
  }
 }

}
至此,讀/寫操作的轉儲處理已經介紹完畢。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章