一、驅動程序實現思路
I/O請求包數據結構
MdlAddress(PMDL)域指向一個內存描述符表(MDL),該表描述了一個與該請求關聯的用戶模式緩衝區。如果頂級設備對象的Flags域爲DO_DIRECT_IO,則I/O管理器爲IRP_MJ_READ或IRP_MJ_WRITE請求創建這個MDL。如果一個IRP_MJ_DEVICE_CONTROL請求的控制代碼指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,則I/O管理器爲該請求使用的輸出緩衝區創建一個MDL。MDL本身用於描述用戶模式虛擬緩衝區,但它同時也含有該緩衝區鎖定內存頁的物理地址。爲了訪問用戶模式緩衝區,驅動程序必須做一點額外工作。<?xml:namespace prefix = o ns = "urn:schemas-microsoft-com:office:office" />
Flags(ULONG)域包含一些對驅動程序只讀的標誌。但這些標誌與WDM驅動程序無關。
AssociatedIrp(union)域是一個三指針聯合。其中,與WDM驅動程序相關的指針是AssociatedIrp.SystemBuffer。 SystemBuffer指針指向一個數據緩衝區,該緩衝區位於內核模式的非分頁內存中。對於IRP_MJ_READ和IRP_MJ_WRITE操作,如果頂級設備指定DO_BUFFERED_IO標誌,則I/O管理器就創建這個數據緩衝區。對於IRP_MJ_DEVICE_CONTROL操作,如果I/O控制功能代碼指出需要緩衝區,則I/O管理器就創建這個數據緩衝區。I/O管理器把用戶模式程序發送給驅動程序的數據複製到這個緩衝區,這也是創建IRP過程的一部分。這些數據可以是與WriteFile調用有關的數據,或者是DeviceIoControl調用中所謂的輸入數據。對於讀請求,設備驅動程序把讀出的數據填到這個緩衝區,然後I/O管理器再把緩衝區的內容複製到用戶模式緩衝區。對於指定了METHOD_BUFFERED的I/O控制操作,驅動程序把所謂的輸出數據放到這個緩衝區,然後I/O管理器再把數據複製到用戶模式的輸出緩衝區。
oStatus(IO_STATUS_BLOCK)是一個僅包含兩個域的結構,驅動程序在最終完成請求時設置這個結構。IoStatus.Status域將收到一個NTSTATUS代碼,而IoStatus.Information的類型爲ULONG_PTR,它將收到一個信息值,該信息值的確切含義要取決於具體的IRP類型和請求完成的狀態。Information域的一個公認用法是用於保存數據傳輸操作,如IRP_MJ_READ,的流量總計。某些PnP請求把這個域作爲指向另外一個結構的指針,這個結構通常包含查詢請求的結果。
RequestorMode將等於一個枚舉常量UserMode或KernelMode,指定原始I/O請求的來源。驅動程序有時需要查看這個值來決定是否要信任某些參數。
PendingReturned(BOOLEAN)如果爲TRUE,則表明處理該IRP的最低級派遣例程返回了STATUS_PENDING。完成例程通過參考該域來避免自己與派遣例程間的潛在競爭。
Cancel(BOOLEAN)如果爲TRUE,則表明IoCancelIrp已被調用,該函數用於取消這個請求。如果爲FALSE,則表明沒有調用IoCancelIrp函數。CancelIrql(KIRQL)是一個IQL值,表明那個專用的取消自旋鎖是在這個IRQL上獲取的。當你在取消例程中釋放自旋鎖時應參考這個域。
CancelRoutine(PDRIVER_CANCEL)是驅動程序取消例程的地址。你應該使用IoSetCancelRoutine函數設置這個域而不是直接修改該域。
UserBuffer(PVOID) 對於METHOD_NEITHER方式的IRP_MJ_DEVICE_CONTROL請求,該域包含輸出緩衝區的用戶模式虛擬地址。該域還用於保存讀寫請求緩衝區的用戶模式虛擬地址,但指定了DO_BUFFERED_IO或DO_DIRECT_IO標誌的驅動程序,其讀寫例程通常不需要訪問這個域。當處理一個METHOD_NEITHER控制操作時,驅動程序能用這個地址創建自己的MDL。
I/O堆棧
任何內核模式程序在創建一個IRP時,同時還創建了一個與之關聯的IO_STACK_LOCATION結構數組:數組中的每個堆棧單元都對應一個將處理該IRP的驅動程序,另外還有一個堆棧單元供IRP的創建者使用(見圖)。堆棧單元中包含該IRP的類型代碼和參數信息以及完成函數的地址。圖中顯示了堆棧單元的結構。
MajorFunction(UCHAR)是該IRP的主功能碼。這個代碼應該爲類似IRP_MJ_READ一樣的值,並與驅動程序對象中MajorFunction表的某個派遣函數指針相對應。如果該代碼存在於某個特殊驅動程序的I/O堆棧單元中,它有可能一開始是,例如IRP_MJ_READ,而後被驅動程序轉換成其它代碼,並沿着驅動程序堆棧發送到低層驅動程序。
MinorFunction(UCHAR)是該IRP的副功能碼。它進一步指出該IRP屬於哪個主功能類。例如,IRP_MJ_PNP請求就有約一打的副功能碼,如IRP_MN_START_DEVICE、IRP_MN_REMOVE_DEVICE,等等。
Parameters(union)是幾個子結構的聯合,每個請求類型都有自己專用的參數,而每個子結構就是一種參數。這些子結構包括Create(IRP_MJ_CREATE請求)、Read(IRP_MJ_READ請求)、StartDevice(IRP_MJ_PNP的IRP_MN_START_DEVICE子類型),等等。
DeviceObject(PDEVICE_OBJECT)是與該堆棧單元對應的設備對象的地址。該域由IoCallDriver函數負責填寫。
FileObject(PFILE_OBJECT)是內核文件對象的地址,IRP的目標就是這個文件對象。驅動程序通常在處理清除請求(IRP_MJ_CLEANUP)時使用FileObject指針,以區分隊列中與該文件對象無關的IRP。
CompletionRoutine(PIO_COMPLETION_ROUTINE)是一個I/O完成例程的地址,該地址是由與這個堆棧單元對應的驅動程序的更上一層驅動程序設置的。你絕對不要直接設置這個域,應該調用IoSetCompletionRoutine函數,該函數知道如何參考下一層驅動程序的堆棧單元。設備堆棧的最低一級驅動程序並不需要完成例程,因爲它們必須直接完成請求。然而,請求的發起者有時確實需要一個完成例程,但通常沒有自己的堆棧單元。這就是爲什麼每一級驅動程序都使用下一級驅動程序的堆棧單元保存自己完成例程指針的原因。
Context(PVOID)是一個任意的與上下文相關的值,將作爲參數傳遞給完成例程。你絕對不要直接設置該域;它由IoSetCompletionRoutine函數自動設置,其值來自該函數的某個參數。
這裏我們先定義一個控制碼:
#define IOCTL_PROTECT_CONTROL CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
這裏我們使用他來和應用程序通訊,Irp->AssociatedIrp.SystemBuffer這個結構中存放用戶模式程序發送給驅動程序的數據。這裏使用METHOD_BUFFERED方式時,I/O管理器創建一個足夠大的內核模式拷貝緩衝區(與用戶模式輸入和輸出緩衝區中最大的容量相同)。當派遣例程獲得控制時,用戶模式的輸入數據被複制到這個拷貝緩衝區。在IRP完成之前,你應該向拷貝緩衝區填入需要發往應用程序的輸出數據。當IRP完成時,你應該設置IoStatus.Information域等於放入拷貝緩衝區中的輸出字節數。然後I/O管理器把數據複製到用戶模式緩衝區並設置反饋變量:
- NTSTATUS DispatchDeviceControl(IN PDEVICE_OBJECT DeviceObject,IN PIRP Irp)
- {
- NTSTATUS ntStatus;
- PIO_STACK_LOCATION IrpStack;
- ULONG IoControlCode;
- ULONG inSize;
- ULONG outSize;
- ULONG *buff;
- IrpStack=IoGetCurrentIrpStackLocation(Irp);
- IoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode;
- switch(IoControlCode)
- {
- case IOCTL_PROTECT_CONTROL:
- inSize = IrpStack->Parameters.DeviceIoControl.InputBufferLength;
- outSize = IrpStack->Parameters.DeviceIoControl.OutputBufferLength;
- buff = (ULONG*)Irp->AssociatedIrp.SystemBuffer ;
- pid=*buff; //獲得要影藏進程的ID
- strcpy(Irp->UserBuffer,"Driver Start");
- break;
- default:
- break;
- }
- ntStatus=Irp->IoStatus.Status;
- IoCompleteRequest(Irp,IO_NO_INCREMENT);
- return ntStatus;
- }
要保護一個進程只用在調用ZwOpenProcess時返回一個STATUS_ACCESS_DENIED就可以了,相關的代碼如下
- NTSTATUS NewZwOpenProcess(
- OUT PHANDLE ProcessHandle,
- IN ACCESS_MASK DesiredAccess,
- IN POBJECT_ATTRIBUTES ObjectAttributes,
- IN PCLIENT_ID ClientId OPTIONAL
- )
- {
- NTSTATUS ntStatus;
- if(pid==(ULONG)ClientId->UniqueProcess)
- {
- DbgPrint("保護進程 PID :%d /n",pid);
- return STATUS_ACCESS_DENIED;
- }
- ntStatus=OldZwOpenProcess(
- ProcessHandle,
- DesiredAccess,
- ObjectAttributes,
- ClientId
- );
- return STATUS_SUCCESS;
- }
這裏我們要先定義相關宏:
- #pragma pack(1)
- typedef struct ServiceDescriptorEntry {
- unsigned int *ServiceTableBase;
- unsigned int *ServiceCounterTableBase; //Used only in checked build
- unsigned int NumberOfServices;
- unsigned char *ParamTableBase;
- } ServiceDescriptorTableEntry, *PServiceDescriptorTableEntry;
- #pragma pack()
- extern ServiceDescriptorTableEntry KeServiceDescriptorTable;
- #define SYSTEMSERVICE(_function)KeServiceDescriptorTable.ServiceTableBase
- [*(PUCHAR)((PUCHAR)_function+1)]
這裏通過windbg反彙編可以看出
lkd> u ZwOpenProcess
nt!ZwOpenProcess:
804e6044 b87a000000 mov eax,7Ah
804e6049 8d542404 lea edx,[esp+4]
804e604d 9c pushfd
804e604e 6a08 push 8
804e6050 e8dc150000 call nt!KiSystemService (804e7631)
804e6055 c21000 ret 10h
nt!ZwOpenProcessToken:
804e6058 b87b000000 mov eax,7Bh
804e605d 8d542404 lea edx,[esp+4]
eax後的7Ah就是索引號,便有了宏定義。
- NTSYSAPI
- NTSTATUS
- NTAPI
- ZwOpenProcess(
- OUT PHANDLE ProcessHandle,
- IN ACCESS_MASK DesiredAccess,
- IN POBJECT_ATTRIBUTES ObjectAttributes,
- IN PCLIENT_ID ClientId OPTIONAL
- );
- typedef NTSTATUS (*ZWOPENPROCESS)(
- OUT PHANDLE ProcessHandle,
- IN ACCESS_MASK DesiredAccess,
- IN POBJECT_ATTRIBUTES ObjectAttributes,
- IN PCLIENT_ID ClientId OPTIONAL
- );
- ZWOPENPROCESS OldZwOpenProcess;
- /////////////////////////////////////////////////////////////////////
- NTSTATUS NewZwOpenProcess(
- OUT PHANDLE ProcessHandle,
- IN ACCESS_MASK DesiredAccess,
- IN POBJECT_ATTRIBUTES ObjectAttributes,
- IN PCLIENT_ID ClientId OPTIONAL
- );
- //在DriverEntry中對CR0進行修改然後修改 SSDT表。
- _asm
- {
- CLI
- MOV EAX, CR0
- AND EAX, NOT 10000H
- MOV CR0, EAX
- }
- OldZwOpenProcess = (ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess)) ;
- (ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess)) = NewZwOpenProcess;
- _asm
- {
- MOV EAX, CR0
- OR EAX, 10000H
- MOV CR0, EAX
- STI
- }
在卸載例程中對ssdt進行修復驅動的功能就基本完成了:
- VOID OnUnload(IN PDRIVER_OBJECT DriverObject)
- {
- NTSTATUS ntStatus;
- UNICODE_STRING DeviceName;
- UNICODE_STRING DeviceLinkString;
- PDEVICE_OBJECT pDeviceObject;
- RtlInitUnicodeString(&DeviceLinkString,DOS_DEVICE_NAME);
- IoDeleteSymbolicLink(&DeviceLinkString);
- pDeviceObject = DriverObject->DeviceObject;
- IoDeleteDevice(pDeviceObject);
- _asm
- {
- CLI
- MOV EAX, CR0
- AND EAX, NOT 10000H
- MOV CR0, EAX
- }
- (ZWOPENPROCESS)(SYSTEMSERVICE(ZwOpenProcess)) = OldZwOpenProcess;
- _asm
- {
- MOV EAX, CR0
- OR EAX, 10000H
- MOV CR0, EAX
- STI
- }
- DbgPrint("驅動卸載完成");
- }
在應用程序中我們使用,CreateService來註冊一個服務,由於驅動註冊了符號鏈接這樣我們就可以像打開一個文件一樣打開驅動,我們用進程快照遍歷進程獲得ID然後傳遞給驅動,這樣一個簡單的保護功能就完成了。
- TCHAR name[256];
- GetCurrentDirectory(256,name);
- strcat(name,"//");
- strcat(name,DriverName);
- SC_HANDLE sh = OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS);
- if(!sh)
- {
- return ;
- }
- SC_HANDLE rh = CreateService(sh,DriverName,DriverName,
- SERVICE_ALL_ACCESS,SERVICE_KERNEL_DRIVER,SERVICE_DEMAND_START,
- SERVICE_ERROR_CRITICAL,name,NULL,NULL,NULL,NULL,NULL);
- if(!rh)
- {
- if(GetLastError()==ERROR_SERVICE_EXISTS)
- {
- rh=OpenService(sh,DriverName,SERVICE_ALL_ACCESS);
- if(!rh)
- {
- CloseServiceHandle(sh);
- return;
- }
- }
- else
- {
- CloseServiceHandle(sh);
- return;
- }
- }
- if(rh)
- {
- if(0==StartService(rh,0,NULL))
- {
- if(ERROR_SERVICE_ALREADY_RUNNING==GetLastError())
- {
- return;
- }
- CloseServiceHandle(sh);
- CloseServiceHandle(rh);
- return;
- }
- CloseServiceHandle(sh);
- CloseServiceHandle(rh);
- }
- void CProtectDriverDlg::OnStart()
- {
- char outbuffer[4096]={0};
- DWORD dw;
- // TODO: Add your control notification handler code here
- device=CreateFile("////.//HookSSDT",
- GENERIC_READ|GENERIC_WRITE,
- 0,
- 0,
- OPEN_EXISTING,
- FILE_ATTRIBUTE_SYSTEM,
- 0);
- if(device!=INVALID_HANDLE_VALUE)
- {
- FindProcessPid();
- DeviceIoControl(device,IOCTL_PROTECT_CONTROL,&pid,
- sizeof(long),outbuffer,4096,&dw,NULL);
- MessageBox("驅動已經加載開始保護","注意",NULL);
- CloseHandle(device);
- }
- return;
- }
- VOID CProtectDriverDlg::FindProcessPid()
- {
- CString ProcessName;
- GetDlgItem(IDC_EDIT)->GetWindowText(ProcessName);
- PROCESSENTRY32 pe32;
- // 在使用這個結構之前,先設置它的大小
- pe32.dwSize = sizeof(pe32);
- // 給系統內的所有進程拍一個快照
- HANDLE hProcessSnap = ::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
- if(hProcessSnap == INVALID_HANDLE_VALUE)
- {
- MessageBox("調用失敗","注意", NULL);
- return ;
- }
- // 遍歷進程快照,輪流顯示每個進程的信息
- BOOL bMore = ::Process32First(hProcessSnap, &pe32);
- while(bMore)
- {
- if(pe32.szExeFile==ProcessName)
- {
- pid=(long)pe32.th32ProcessID;
- }
- bMore = ::Process32Next(hProcessSnap, &pe32);
- }
- // 不要忘記清除掉snapshot對象
- ::CloseHandle(hProcessSnap);
- return ;
- }
二.驅動調試
下面我介紹下關於驅動的調試,這裏以windbg爲例:
1.配置windbg雙機調試環境,(VMware Workstation)
(1) 創建windbg快捷方式在目標中加上-k com:port=//./pipe/com_1,baud=115200,pipe 例如:"E:/Program Files/Debugging Tools for Windows/windbg.exe" -k com:port=//./pipe/com_1,baud=115200,pipe
(2) 在虛擬機設置裏添加一個串行端口(如圖),點擊下一步;
(3)點擊輸出到命名管道:
在高級選中,中輪詢時主動放棄CPU佔用.然後完成。
(4) 在虛擬機中系統的啓動項中添加一個啓動項:
multi(0)disk(0)rdisk(0)partition(1)/WINDOWS="Microsoft Windows XP Professional - debug" /fastdetect /debug /debugport=com1 /baudrate=115200
(5) 然後跟着我做:首先運行windbg,然後打開虛擬機客戶機,選中調試模式
(6)進入要調試的系統後我們可以在windbg中用Debug命令中的Break對目標機進行中斷,用F5我們又可以恢復客戶機的運行。
我們設置windbg的Symbol path:我的設置如下
C:/MyCodesSymbols;SRV*C:/MyLocalSymbols*http://msdl.microsoft.com/download/symbols;C:/MyLocalSymbols
這裏我把自己的.pdb文件放到C:/MyCodesSymbols下,從微軟的符號文件服務器上下載符號文件到C:/MyLocalSymbols中。
現在我們來調試一個驅動試試,吧剛纔寫的驅動和應用文件拷貝到虛擬客戶機中,在windbg中打開驅動源文件如下:
我們先下一個延遲斷點:bu hook!DriverEntry 跟普通斷點相比,推遲斷點的特色是把對modules!names的操作延遲到模塊加載時進行地址解析,而普通斷點是對地址進行操作,或直接把modules!names轉換爲地址。推遲斷點的另一個特性是系統重啓之後還能記住它們(不是記住的已經轉換的地址)。這個特性使得推遲斷點在模塊被卸載之後仍然被會記得,而普通斷點就不行了,當模塊被卸載之後斷點同時會被移除。
延遲斷點可以很方便的使我們的應用程序在特定的位置下斷。
如果我們要下一個普通斷點可以使用bp命令或者直接在源代碼 要下斷的地方按f9, 當你想查看已經設置的斷點時,可以使用bl(“Breakpoint Line”)命令.下好斷點後我們就可以按f5進行調試了,具體方法和其他調試器差不多我也就不多羅嗦了。關於windbg命令的詳細描述可以參看幫助文檔。
文章出處:http://www.diybl.com/course/3_program/c++/cppxl/2008515/116903.html