64位內開發第二十一講,內核下的驅動程序與驅動程序通訊

驅動程序調用驅動程序

一丶驅動調用驅動介紹.

1.1 驅動調用驅動介紹

驅動調用驅動.其實就是兩個內核內核驅動之間的通信. 比如應用程序和驅動程序通信就算爲一種通信. 應用程序可以 發送 IRP_MJ_READ 請求(ReadFile) 發送給 DrvierA程序. 然後DriverA進行相應的 IRP處理操作. 當然發送 IRP_MJ_READ請求的時候可以發送同步請求或者異步請求.這就看DriverA 如何處理這些請求了.是否支持異步.

驅動程序調用驅動程序也是一樣的. 也是 DriverA 發送請求給DriverB 然後DriverB 來處理DriverA的請求. 如果 DriverB 支持異步,那麼DriverA也可以進行異步讀取.

1.2 驅動程序調用驅動程序流程圖

如圖,應用程序調用 ReadFile的時候 就會產生 IRP_MJ_READ 請求. 此時這個請求就會發送到DriverA. DriverA進行處理,處理的方式是它讀取DriverB的內容(調用ZwReadFile) 此時DriverB處理DriverA的請求,並且將數據返回. DriverA得到數據之後就處理 應用程序的請求.比如將DriverB返回的數據填充到應用程序的Buffer中.並且返回.

1.3 內核通信方式

ZwReadFile ZwWriteFile ZwDeviceIoControlFile 都可以進行通信.在文件句柄一講中.只介紹ZwReadFile方式.其它方式是一樣的. 參數調用可以參考Ring3. ZwReadFile方式搞懂了.那麼其它的就會了.

二丶 文件句柄形式調用驅動程序

2.1 文件句柄-同步方式

2.1.1 文件句柄形式和簡介

在應用層我們訪問驅動層並且進行通信的時候. 第一步就是 CreateFile打開符號鏈接. 返回一個文件句柄,然後使用ReadFile /WriteFile/DeviceIoControl等函數來操作這個句柄發送對應的 IRP 請求給驅動.然後驅動響應這些請求來進行處理.

所謂文件句柄形式就是如上所說. 內核層也是一樣的. 不過注意的是 內核層可以使用設備名直接打開一個驅動來進行操作. 所以我們需要準備一個 DriverB驅動並將其加載.

2.1.2 文件句柄同步與異步

文件句柄方式打開設備並且進行操作.支持同步異步. 如果要使用異步.那麼我們的DrvierB也要支持異步處理.

文件句柄方式的內核操作API如下:

NTSYSAPI NTSTATUS ZwCreateFile(
  [out]          PHANDLE            FileHandle,   
  [in]           ACCESS_MASK        DesiredAccess,
  [in]           POBJECT_ATTRIBUTES ObjectAttributes,
  [out]          PIO_STATUS_BLOCK   IoStatusBlock,
  [in, optional] PLARGE_INTEGER     AllocationSize,
  [in]           ULONG              FileAttributes,
  [in]           ULONG              ShareAccess,
  [in]           ULONG              CreateDisposition,
  [in]           ULONG              CreateOptions,
  [in, optional] PVOID              EaBuffer,
  [in]           ULONG              EaLength
);

如果是同步進行驅動調用驅動操作. 那麼下面幾點必須要注意.
DesiredAccess: 必須有此 SYNCHRONIZE 標誌.
CreateOptions: 必須有 FILE_SYNCHRONOUS_IO_NONALERT 或者 FILE_SYNCHRONOUS_IO_NONALERT 標誌

NTSYSAPI NTSTATUS ZwReadFile(
  [in]           HANDLE           FileHandle,
  [in, optional] HANDLE           Event,
  [in, optional] PIO_APC_ROUTINE  ApcRoutine,
  [in, optional] PVOID            ApcContext,
  [out]          PIO_STATUS_BLOCK IoStatusBlock,
  [out]          PVOID            Buffer,
  [in]           ULONG            Length,
  [in, optional] PLARGE_INTEGER   ByteOffset,
  [in, optional] PULONG           Key
);

其實操作DriverB 和應用層操作DriverA一樣.都是利用的文件API.

2.1.3 準備DriverB驅動

首先準備一個DriverB 驅動. 並且建立符號鏈接與設備. 並且進行加載. DriverB可以處理 IRP_MJ_READ的請求. 比如請求後直接填充HelloWorld並且返回.

下載Winobj 查看設備是否創建成功.如下圖:

可以看到DrvierB已經創建了. 其設備名字爲: \Device\DriverB 這個也是我們內核層需要打開的.

DriverB 和正常驅動一樣.建立符號鏈接.建立設備鏈接. 設置緩衝區讀取方式 初始化派遣函數 設置卸載函數.

所以這裏之貼出 IRP_MJ_READ 的讀請求處理的代碼.

NTSTATUS CompleteReadRequest(PDEVICE_OBJECT device, PIRP irp)
{
    UNREFERENCED_PARAMETER(irp);
    if (config.GetMySelfDevice() == device && irp != nullptr)
    {
        KdBreakPoint();
        PIO_STACK_LOCATION irp_stack = IoGetCurrentIrpStackLocation(irp);
        if (irp->MdlAddress && irp_stack)
        {
            bool is_read = false;
            PVOID pBuffer = MmGetSystemAddressForMdlSafe(irp->MdlAddress, NormalPagePriority);
            ULONG uReadLen = irp_stack->Parameters.Read.Length;
            size_t srclen = (wcslen(L"HelloWorld")+1) * 2;
            if (uReadLen > srclen)
            {
                is_read = true;
                RtlCopyMemory(pBuffer, L"HelloWorld", srclen);
            }
            if (is_read)
            {
                irp->IoStatus.Status = STATUS_SUCCESS;
                irp->IoStatus.Information = srclen;
                IoCompleteRequest(irp, IO_NO_INCREMENT);
                return STATUS_SUCCESS;
            }
            else
            {
                irp->IoStatus.Status = STATUS_UNSUCCESSFUL;
                irp->IoStatus.Information = 0;
                IoCompleteRequest(irp, IO_NO_INCREMENT);
                return STATUS_SUCCESS;
            }
        }
    return STATUS_SUCCESS;
}

可以看到我是用的 DO_DIRECT_IO 方式,並不是緩衝區方式. 這一點在創建設備並且設置緩衝區方式的時候設置的. 不瞭解 DO_DIRECT_IO 方式的 請看下其它資料.

2.1.4 DriverA的驅動代碼-同步方式

DriverA就很簡單了.直接 ZwCreateFile打開DriverB的設備.然後ZwReadFile讀取數據即可.

這樣觸發的IRP_MJ_READ請求就會被DriverB捕獲.然後DriverB就填充HelloWorld返回.DriverA自然就得到了 HelloWorld字符串了.

在上面我們 建立一個標準的驅動通訊圖. 也就是1.2小節. 那是由 應用層往下發請求來操作DriverA的.然後DriverA 讀取DriverB. 而在這裏我們爲了簡單.直接在DrierA裏面就讀取DriverB. 讀取的數據由 DbgPrint函數打印. 可以不使用應用層來通知我們.

代碼如下:

void CallDriverMethod1()
{
    /*
    同步調用Driver B
    1.winobj 確定 DriverB的設備連接名. 一般都是 \\device\\DriverB
    2.CreateFile指定同步方式打開設備 DesiredAccess:SYNCHRONIZE CreateOptions:
    FILE_SYNCHRONOUS_IO_NONALERT or 
    FILE_SYNCHRONOUS_IO_ALERT
    3.調用ReadFile發送 IRP_MJ_READ 請求即可
    */
    HANDLE device_handle = NULL;
    OBJECT_ATTRIBUTES driver_name = { 0 };
    IO_STATUS_BLOCK status_block = { 0 };
    UNICODE_STRING uc_driver_name = RTL_CONSTANT_STRING(L"\\device\\DriverB");
    InitializeObjectAttributes(&driver_name, &uc_driver_name, OBJ_CASE_INSENSITIVE, NULL, NULL);
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    TCHAR read_buf[100] = { 0 };

    do
    {

        KdBreakPoint();
        status = ZwCreateFile(&device_handle,
            FILE_GENERIC_READ | SYNCHRONIZE, //或者直接使用 FILE_GENERIC_READ 
            &driver_name,
            &status_block,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            FILE_SYNCHRONOUS_IO_NONALERT,   //必須帶有 FILE_SYNCHRONOUS_IO_ALERT 
                                            // 或 FILE_SYNCHRONOUS_IO_NONALERT
            NULL,
            0);
        if (!NT_SUCCESS(status))
            break;

        status = ZwReadFile(
            device_handle,   //文件句柄
            nullptr,
            nullptr,
            nullptr,
            &status_block,   //讀取的字節數 以及返回值
            read_buf,        //緩衝區
            sizeof(read_buf) / sizeof(read_buf[0]),//讀取的緩衝區長度
            0,               //讀取的偏移
            nullptr);
        if (!NT_SUCCESS(status))
            break;

        DbgPrint("[A] --> read value is %ws \r\n" , read_buf);
    } while (false);

    if (device_handle != nullptr)
    {
        ZwClose(device_handle);
        device_handle = nullptr;
    }
    return;
}

2.1.5 效果

2.2 文件句柄-第一種異步方式

2.2.1 異步方式簡介

學過IRP同步與異步方式.那麼我們就知道異步方式更爲方便也更爲貼合系統底層設計. 但是偏一點複雜. 在之前 IRP同步與異步篇章中我們有講到,應用層如何進行異步處理的. 分了兩種方式.

回顧第一種 ReadFile 方式. 提供一個 OVERLAPPED 和初始化裏面的一個事件. 打開設備的時候指定爲異步方式. 當ReadFile調用完畢之後 會觸發 IRP_MJ_READ請求. 請求發送給DriverA. DriverA掛起IRP的請求. 並記錄當前掛起的IRP,然後在IRP_MJ_CLEARN請求中來完成掛起的IRP請求. 此時因爲操作系統自身的機制.會設置ring3初始化的事件. 然後代碼回退到ring3這邊. ring3這邊就等待這個事件.一旦被設置就會代表請求執行完成了. 此時就可以繼續往下操作了.

第二種方式跟第一種方式相似. 唯一不同的就是 調用 ReadFileEx函數 而這個函數可以提供一個回調函數. 當內核異步處理完畢之後會調用我們的回調函數. 而我們也不需要設置 OVERLAPPED 事件了.

2.2.2 內核下的異步處理方式第一種

內核下其實和應用層的異步處理方式一樣. 借用了 ReadFileEx的回調函數方式,同時又借用了第一種方式的事件形式.

簡單來說就是 我們需要提供一個回調函數.並初始化一個事件. 當DriverB異步處理完成之後就會調用我們提供的回調函數. 但是我們需要在回調函數裏面設置這個事件. 設置之後我們 讀(ZwReadFile) 下面就等待這個事件.

其實也是利用了事件和回調函數的機制.

2.2.3 準備異步處理驅動DriverB

DriverB 驅動掛起IRP請求. 並建立一個 TimerDpc定時器.三秒執行一次Irp請求.

這點和 IRP 同步與異步一篇中講解的 IRP超時處理是一樣的,唯一不同的就是超時處理是取消IRP. 而我們這裏是完成IRP.

其中這裏只提供DriverB的兩個核心處理函數. 至於 初始化timer DPC 以及停止計時器 請移步 IRP 同步與異步 一篇中的 IRP超時處理小節.

DriverB 讀請求的處理.

NTSTATUS CompleteReadRequest(PDEVICE_OBJECT device, PIRP irp)
{
    UNREFERENCED_PARAMETER(irp);
    if (config.GetMySelfDevice() == device && irp != nullptr)
    {
        //掛起IRP
        IoMarkIrpPending(irp);
        //記錄此irp
        PDEVICE_EXTENSION device_ex = nullptr;
        device_ex = (PDEVICE_EXTENSION)device->DeviceExtension;
        if (device_ex)
        {
            device_ex->irp_ptr = irp;
        }
        else
        {
            device_ex->irp_ptr = nullptr;
        }
        //設置超時處理
        LARGE_INTEGER timeout = { 0 };
        timeout.QuadPart = -10 * 3000000;
        KeSetTimer(&device_ex->timer,
            timeout,
            &device_ex->dpc);
        //返回Pending狀態
        return STATUS_PENDING;
    }


    return STATUS_SUCCESS;
}

返回 pending狀態,並且在設備擴展中記錄此IRP.

延遲處理函數.

//延遲處理
VOID DelayDpc(
    _In_ struct _KDPC* Dpc,
    _In_opt_ PVOID DeferredContext,
    _In_opt_ PVOID SystemArgument1,
    _In_opt_ PVOID SystemArgument2
)
{
    UNREFERENCED_PARAMETER(Dpc);
    UNREFERENCED_PARAMETER(SystemArgument1);
    UNREFERENCED_PARAMETER(SystemArgument2);

    /*
    1.通過context得到當前的DeviceObje對象
    2.通過DeviceObj對象得到當前的設備擴展
    3.通過設備擴展找到記錄的IRP並且取消IRP
    */
    KdBreakPoint();
    if (DeferredContext != nullptr)
    {
        PDEVICE_OBJECT device = nullptr;
        PDEVICE_EXTENSION device_ex = nullptr;
        device = (PDEVICE_OBJECT)DeferredContext;
        if (device->DeviceExtension != nullptr)
        {
            device_ex = (PDEVICE_EXTENSION)device->DeviceExtension;
            PIRP irp = device_ex->irp_ptr;
            //完成IRP
            PVOID pBuffer = MmGetSystemAddressForMdlSafe(irp->MdlAddress,NormalPagePriority);
            RtlStringCbCopyNW((NTSTRSAFE_PWSTR)pBuffer, 100, L"HelloWorld", (wcslen(L"HelloWorld") + 1) * 2);
            irp->IoStatus.Status = STATUS_SUCCESS;
            irp->IoStatus.Information = (wcslen(L"HelloWorld")+1)*2;
            IoCompleteRequest(irp, IO_NO_INCREMENT);
            DbgPrint("[B]-->延遲完成Irp請求\r\n");
            //講device_ex記錄的 Irp置空.
            device_ex->irp_ptr = nullptr;
        }
        else
        {
            DbgPrint("Irp操作函數執行中判斷的附加數據爲空\r\n");
        }

    }
    else
    {
        DbgPrint("參數爲空\r\n");
    }
}

2.2.4 Driver A驅動代碼以及注意事項

要進行異步讀取,首先我們之前所說的 ZwCreateFile 參數中就不能帶有 同步的標誌了

其次要進行異步讀取的時候 ZwReadFile中要讀取的偏移量必須給定 否則函數會返回 STATUS_INVALID_PARAMETER 也就是錯誤碼: ((NTSTATUS)0xC000000DL)

其次我們因爲是異步讀取,當底層驅動處理完畢之後會調用我們的回調函數. 所以我們需要提供一個回調函數. 我們還需要提供一個事件. 在我們回調函數裏面設置事件. 這樣就能通過事件進行通訊了.

代碼如下:

//回調函數
VOID NTAPI Complete (
    _In_ PVOID ApcContext,
    _In_ PIO_STATUS_BLOCK IoStatusBlock,
    _In_ ULONG Reserved
    )
{
    UNREFERENCED_PARAMETER(Reserved);
    KdBreakPoint();
     DbgPrint("[A]--> 異步函數被執行\r\n");
     DbgPrint("[A]--->完成函數->讀取的字節數 = %d \r\n", IoStatusBlock->Information);
     KeSetEvent((PKEVENT)ApcContext, IO_NO_INCREMENT, FALSE);
}
//異步調用
void CallDriverMethod2()
{
    /*
    異步調用Driver B
    1.winobj 確定 DriverB的設備連接名. 一般都是 \\device\\DriverB
    2.CreateFile 異步打開 DesiredAccess:不能帶有SYNCHRONIZE 
    CreateOptions:不能帶有 FILE_SYNCHRONOUS_IO_NONALERT or FILE_SYNCHRONOUS_IO_ALERT
    3.初始化事件 設置回調函數 調用ReadFile發送 IRP_MJ_READ 請求 
    */
    HANDLE device_handle = NULL;
    OBJECT_ATTRIBUTES driver_name = { 0 };
    IO_STATUS_BLOCK status_block = { 0 };
    UNICODE_STRING uc_driver_name = RTL_CONSTANT_STRING(L"\\device\\DriverB");
    InitializeObjectAttributes(&driver_name, &uc_driver_name, OBJ_CASE_INSENSITIVE, NULL, NULL);
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    TCHAR read_buf[100] = { 0 };

    do
    {

        KdBreakPoint();
        status = ZwCreateFile(&device_handle,
            FILE_READ_ATTRIBUTES,
            &driver_name,
            &status_block,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            0,
            NULL,
            0);
        if (!NT_SUCCESS(status))
            break;
        KEVENT event = { 0 }; //初始化一個自動無信號的事件
        KeInitializeEvent(&event, SynchronizationEvent, FALSE);
        LARGE_INTEGER offset = { 0 };
        status = ZwReadFile(
            device_handle,   //文件句柄
            nullptr,
            Complete,         //設置一個完成回調
            &event,          //給完成回調傳一個event句柄.context
            &status_block,   //讀取的字節數 以及返回值
            read_buf,        //緩衝區
            sizeof(read_buf) / sizeof(read_buf[0]),//讀取的緩衝區長度
            &offset,               //讀取的偏移 比如給定.
            nullptr);
        if (status == STATUS_PENDING)
        {
            DbgPrint("[A]---> 底層驅動正在進行異步操作\r\n");
            //採用無限等待方式進行等待
            KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, 0); 
            DbgPrint("[A]---> 數據讀取完成\r\n");
            DbgPrint("[A] --> read value is %ws \r\n", read_buf);
        }

    } while (false);

    if (device_handle != nullptr)
    {
        ZwClose(device_handle);
        device_handle = nullptr;
    }
    return;
}

2.2.5 效果

2.3 文件句柄-第二種異步方式

2.3.1結構與簡介

第二種方式是利用了 文件句柄 每打開一個設備,都會伴隨着一個FILE_OBJCE,這個結構裏面記錄着文件對象,而這個對象裏面有個事件域. 我們可以利用這個事件域來進行異步. 當DriverB執行完畢之後則會設置這個事件域.

其中結構如下,簡單瞭解.

kd> dt _FILE_OBJECT
nt!_FILE_OBJECT
   +0x000 Type             : Int2B
   +0x002 Size             : Int2B
   +0x008 DeviceObject     : Ptr64 _DEVICE_OBJECT //記錄着設備
   +0x010 Vpb              : Ptr64 _VPB
   +0x018 FsContext        : Ptr64 Void
   +0x020 FsContext2       : Ptr64 Void
   +0x028 SectionObjectPointer : Ptr64 _SECTION_OBJECT_POINTERS
   +0x030 PrivateCacheMap  : Ptr64 Void
   +0x038 FinalStatus      : Int4B
   +0x040 RelatedFileObject : Ptr64 _FILE_OBJECT
   +0x048 LockOperation    : UChar
   +0x049 DeletePending    : UChar
   +0x04a ReadAccess       : UChar
   +0x04b WriteAccess      : UChar
   +0x04c DeleteAccess     : UChar
   +0x04d SharedRead       : UChar
   +0x04e SharedWrite      : UChar
   +0x04f SharedDelete     : UChar
   +0x050 Flags            : Uint4B
   +0x058 FileName         : _UNICODE_STRING //記錄了文件的名字
   +0x068 CurrentByteOffset : _LARGE_INTEGER
   +0x070 Waiters          : Uint4B
   +0x074 Busy             : Uint4B
   +0x078 LastLock         : Ptr64 Void
   +0x080 Lock             : _KEVENT
   +0x098 Event            : _KEVENT    //此事件
   +0x0b0 CompletionContext : Ptr64 _IO_COMPLETION_CONTEXT
   +0x0b8 IrpListLock      : Uint8B
   +0x0c0 IrpList          : _LIST_ENTRY
   +0x0d0 FileObjectExtension : Ptr64 Void

2.3.2 API編程獲取方式

要通過文件句柄獲取FILE_OBJECT 文件對象,那麼我們離不開下面幾個API.

NTSTATUS ObReferenceObjectByHandle(
  [in]            HANDLE                     Handle,  //句柄
  [in]            ACCESS_MASK                DesiredAccess,//權限
  [in, optional]  POBJECT_TYPE               ObjectType,//要獲取的對象類型
  [in]            KPROCESSOR_MODE            AccessMode,//用戶還是內核模式
  [out]           PVOID                      *Object,//成功之後返回的對象類型的指針
  [out, optional] POBJECT_HANDLE_INFORMATION HandleInformation//句柄信息
);

此API就是通過句柄查找對應的對象結構. 比如你是線程句柄那你就可以通過線程句柄查找線程對象, 進程句柄那麼就可以查找進程對象. 文件句柄就可以查找文件對象. 等等.

它支持的類型如下,而我們只是使用到了文件句柄.

ObjectType 參數 對象指針類型
*ExEventObjectType PKEVENT
*ExSemaphoreObjectType PKSEMAPHORE
*IoFileObjectType PFILE_OBJECT
*PsProcessType PEPROCESS 或 PKPROCESS
*PsThreadType PETHREAD 或 PKTHREAD
*SeTokenObjectType PACCESS_TOKEN
*TmEnlistmentObjectType PKENLISTMENT
*TmResourceManagerObjectType 2013 年 1 月 3 日
*TmTransactionManagerObjectType PKTM
*TmTransactionObjectType PKTRANSACTION

注意當獲取對象成功之後其對象的引用計數會+1. 所以我們還需要配合下面的函數來減少引用計數.

void ObDereferenceObject(
  [in]  a    //指向對象的指針,減少引用計數.
);

2.3.3 DriverA異步處理2代碼實現.

//異步調用2
void CallDriverMethod3()
{
    /*
    異步調用Driver B
    1.winobj 確定 DriverB的設備連接名. 一般都是 \\device\\DriverB
    2.CreateFile 異步打開 DesiredAccess:不能帶有SYNCHRONIZE
    CreateOptions:不能帶有 FILE_SYNCHRONOUS_IO_NONALERT or FILE_SYNCHRONOUS_IO_ALERT
    3.初始化事件 設置回調函數 調用ReadFile發送 IRP_MJ_READ 請求
    */
    HANDLE device_handle = NULL;
    OBJECT_ATTRIBUTES driver_name = { 0 };
    IO_STATUS_BLOCK status_block = { 0 };
    UNICODE_STRING uc_driver_name = RTL_CONSTANT_STRING(L"\\device\\DriverB");
    InitializeObjectAttributes(&driver_name, &uc_driver_name, OBJ_CASE_INSENSITIVE, NULL, NULL);
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    TCHAR read_buf[100] = { 0 };

    do
    {

        KdBreakPoint();
        status = ZwCreateFile(&device_handle,
            FILE_READ_ATTRIBUTES,
            &driver_name,
            &status_block,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            0,
            NULL,
            0);
        if (!NT_SUCCESS(status))
            break;

        LARGE_INTEGER offset = { 0 };
        status = ZwReadFile(
            device_handle,   //文件句柄
            nullptr,
            nullptr,         
            nullptr,          
            &status_block,   //讀取的字節數 以及返回值
            read_buf,        //緩衝區
            sizeof(read_buf) / sizeof(read_buf[0]),//讀取的緩衝區長度
            &offset,               //讀取的偏移
            nullptr);
        if (status == STATUS_PENDING)
        {
            DbgPrint("[A]---> 底層驅動正在進行異步操作\r\n");
            PFILE_OBJECT fileobj = nullptr;
            status = ObReferenceObjectByHandle(device_handle, EVENT_MODIFY_STATE, *IoFileObjectType, KernelMode,(PVOID*)&fileobj, nullptr);
            if (NT_SUCCESS(status))
            {
                DbgPrint("[A]---> 獲取文件對象成功,檢查事件域\r\n");
                //採用無限等待方式進行等待
                KeWaitForSingleObject(&fileobj->Event, Executive, KernelMode, FALSE, 0);
                DbgPrint("[A]---> 數據讀取完成\r\n");
                DbgPrint("[A] --> read value is %ws \r\n", read_buf);
                ObDereferenceObject(fileobj); //減少引用
            }
            else
            {
                DbgPrint("[A]---> 文件對象等待失敗\r\n");
            }

        }

    } while (false);

    if (device_handle != nullptr)
    {
        ZwClose(device_handle);
        device_handle = nullptr;
    }
    return;
}

2.3.4 效果

3.1文件句柄-符號鏈接方式

3.1.1 符號鏈接方式簡介

在進行驅動通信的時候,有時候並不能通過設備名來打開設備. 這點尤其在WDM中常見.

所以我們可以通過查找符號鏈接裏面的設備名來打開設備從而進行通信.

主要使用了兩個API.

打開符號鏈接對象,通過符號鏈接名字.

NTSYSAPI NTSTATUS ZwOpenSymbolicLinkObject(
  [out] PHANDLE            LinkHandle,  //打開成功之後傳出的句柄
  [in]  ACCESS_MASK        DesiredAccess,//權限
  [in]  POBJECT_ATTRIBUTES ObjectAttributes//要打開的符號鏈接
);

通過符號鏈接名字查找設備名字

NTSYSAPI NTSTATUS ZwQuerySymbolicLinkObject(
  [in]            HANDLE          LinkHandle,  //符號鏈接句柄
  [in, out]       PUNICODE_STRING LinkTarget,  //傳出的找到的設備名字,必須外面申請
  [out, optional] PULONG          ReturnedLength//傳出的返回值
);

所以下面只是用這兩個API進行 設備的查找.代碼也就是填充這兩個API所需要的參數.

至於同步異步等 跟文件句柄方式一樣.

注意: 關閉句柄的時候 請用 ZwClose .內核下要注意資源的釋放.

3.1.2 Driver A代碼演示

void CallDriverMethod4()
{
    /*
    異步調用Driver B
    1.winobj 確定 DriverB的設備連接名. 一般都是 \\device\\DriverB
    2.CreateFile 異步打開 DesiredAccess:不能帶有SYNCHRONIZE
    CreateOptions:不能帶有 FILE_SYNCHRONOUS_IO_NONALERT or FILE_SYNCHRONOUS_IO_ALERT
    3.初始化事件 設置回調函數 調用ReadFile發送 IRP_MJ_READ 請求
    */
    HANDLE device_handle = NULL;
    OBJECT_ATTRIBUTES symbolic_name = { 0 };
    OBJECT_ATTRIBUTES device_name_attribute = { 0 };
    IO_STATUS_BLOCK status_block = { 0 };
    UNICODE_STRING uc_symbolic_name = RTL_CONSTANT_STRING(L"\\??\\DriverB");
    InitializeObjectAttributes(&symbolic_name, &uc_symbolic_name, OBJ_CASE_INSENSITIVE, NULL, NULL);
    NTSTATUS status = STATUS_UNSUCCESSFUL;
    TCHAR read_buf[100] = { 0 };

    HANDLE link_handle = nullptr;

    UNICODE_STRING uc_device_name = { 0 };
    ULONG device_name_real_size = 100;
    do
    {

        KdBreakPoint();
        status = ZwOpenSymbolicLinkObject(&link_handle, FILE_ALL_ACCESS, &symbolic_name);
        if (!NT_SUCCESS(status))
            break;
        uc_device_name.Buffer = (PWCH)ExAllocatePoolWithTag(NonPagedPool, device_name_real_size, (ULONG)0);
        if (uc_device_name.Buffer == nullptr)
        {
            break;
        }
        uc_device_name.Length = (USHORT)device_name_real_size;
        uc_device_name.MaximumLength = (USHORT)device_name_real_size;
        status = ZwQuerySymbolicLinkObject(link_handle, &uc_device_name, &device_name_real_size);
        if (!NT_SUCCESS(status))
            break;
        DbgPrint("通過符號鏈接名查找設備成功\r\n");
        DbgPrint("設備名字 = %wZ\r\n",&uc_device_name);
        InitializeObjectAttributes(&device_name_attribute, &uc_device_name, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, nullptr, nullptr);
        status = ZwCreateFile(&device_handle,
            FILE_READ_ATTRIBUTES,
            &device_name_attribute,
            &status_block,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            0,
            NULL,
            0);
        if (!NT_SUCCESS(status))
            break;


        LARGE_INTEGER offset = { 0 };
        status = ZwReadFile(
            device_handle,   //文件句柄
            nullptr,
            nullptr,
            nullptr,
            &status_block,   //讀取的字節數 以及返回值
            read_buf,        //緩衝區
            sizeof(read_buf) / sizeof(read_buf[0]),//讀取的緩衝區長度
            &offset,               //讀取的偏移
            nullptr);
        if (status == STATUS_PENDING)
        {
            DbgPrint("[A]---> 底層驅動正在進行異步操作\r\n");
            PFILE_OBJECT fileobj = nullptr;
            status = ObReferenceObjectByHandle(device_handle, EVENT_MODIFY_STATE, *IoFileObjectType, KernelMode, (PVOID*)&fileobj, nullptr);
            if (NT_SUCCESS(status))
            {
                DbgPrint("[A]---> 獲取文件對象成功,檢查事件域\r\n");
                //採用無限等待方式進行等待
                KeWaitForSingleObject(&fileobj->Event, Executive, KernelMode, FALSE, 0);
                DbgPrint("[A]---> 數據讀取完成\r\n");
                DbgPrint("[A] --> read value is %ws \r\n", read_buf);
                ObDereferenceObject(fileobj); //減少引用
            }
            else
            {
                DbgPrint("[A]---> 文件對象等待失敗\r\n");
            }

        }

    } while (false);

    if (uc_device_name.Buffer != nullptr)
    {
        ExFreePoolWithTag(uc_device_name.Buffer, 0);
        uc_device_name.Buffer = 0;
        uc_device_name.Length = 0;
        uc_device_name.MaximumLength = 0;
    }
    if (link_handle != nullptr)
    {
        ZwClose(link_handle);
        link_handle = nullptr;
    }
    if (device_handle != nullptr)
    {
        ZwClose(device_handle);
        device_handle = nullptr;
    }
    return;
}

3.1.3 效果演示.

三丶高級驅動程序調用IRP方式

3.1 設備調用方式-同步調用

3.1.1 IRP方式調用簡介

所謂IRP方式就是自己申請IRP 然後發送IRP請求去調用DriverB程序

也就是說讓我們自己實現 ZwCreateFile ZwReadFile (ZwWriteFile ZwDeviceIoControFile) 等API.

ZwCreateFile被調用的時候,內部會產生 IRP_MJ_CREATE 請求 而ZwReadFile則會產生一個IRP_MJ_READ的請求. 並且將其發送給DriverB的派遣函數中.

所以我們就要模擬它們的調用

3.1.2 API簡介

模擬它們的調用就需要一下幾個API了. 如下:

1.通過設備名獲取設備結構以及文件結構

NTSTATUS IoGetDeviceObjectPointer(
  [in]  PUNICODE_STRING ObjectName,
  [in]  ACCESS_MASK     DesiredAccess,
  [out] PFILE_OBJECT    *FileObject,
  [out] PDEVICE_OBJECT  *DeviceObject
);

2.手動創建IRP請求.

  • 創建同步請求IRP
__drv_aliasesMem PIRP IoBuildSynchronousFsdRequest(
  [in]           ULONG            MajorFunction,  //功能號
  [in]           PDEVICE_OBJECT   DeviceObject,   //要發送的給的設備
  [in, out]      PVOID            Buffer,         //對於功能號IRP_MJ_READ 則戴代表輸入緩衝區
  [in, optional] ULONG            Length,         //長度
  [in, optional] PLARGE_INTEGER   StartingOffset,//讀取的偏移
  [in]           PKEVENT          Event,         //事件域
  [out]          PIO_STATUS_BLOCK IoStatusBlock  //狀態
);

其實你就可以把這個函數看作是 ZwReadFile 或者 ZwWriteFile 此函數的功能號只支持

IRP_MJ_READ IRP_MJ_WRITE IRP_MJ_PNP IRP_MJ_FLUSH_BUFFERS IRP_MJ_SHUTDOWN

通過這些功能號也就知道變相的等於一個函數頂替了幾個函數.

值得注意的是這個函數是創建同步類型的IRP 如何保證同步,那就要用到這個函數中的事件域的參數.

3.獲取下一層IO堆棧

__drv_aliasesMem PIO_STACK_LOCATION IoGetNextIrpStackLocation(
  [in] PIRP Irp
);

通過IRP返回他的下一層的堆棧.因爲要模擬調用.所以我們要必須填寫IRP的結構. 好在使用

IoBuildSynchronousFsdRequest 函數的時候我們並不需要填寫很多. 而是由這個函數填寫好了. 我們只需要填寫必要的即可.

4.發送IRP請求

其實這個是核心

NTSTATUS IofCallDriver(
  PDEVICE_OBJECT        DeviceObject, //要給那個設備發送
  __drv_aliasesMem PIRP Irp           //要發送的IRP
);

在驅動中我們是使用的宏.這也是爲了兼容各個版本. 參數參考上面的

#define IoCallDriver(a,b)   \
        IofCallDriver(a,b)
);

3.1.3 模擬調用核心思想

首先模擬調用就是 通過獲取設備指針.和文件結構對象. 然後申請對應的IRP事件. 最後設置一下IRP. 然後進行IoCallDriver調用.

總結一下,分爲如下幾個步驟.

  • 1.IoGetDeviceObjectPointer通過設備名獲取設備對象指針和文件結構對象

    設備名--->如果設備名不好找則可以參考上面說的通過符號鏈接來查找

    除了調用IoGetDeviceObjectPointer 也可以調用ZwCreateFile. 只不過我們獲取的是一個句柄,我們要根據這個句柄來使用 ObReferenceByHandle獲取設備結構和文件結構對象.

  • 2.申請對應的IRP 這個小節是申請同步類型IRP.所以使用 IoBuildSynchronousFsdRequest

  • 3.設置下一層堆棧的文件對象

    通過 IoGetNextIrpStackLocation 獲取下一層堆棧,然後將堆棧中的文件對象結構的域設置爲第一步獲取出來的文件對象結構

  • 4.調用IoCallDriver發送IRP請求.

  • 資源釋放,因爲IoGetDeviceObjectPointer會對對象進行引用計數+1(第一次爲打開以後就是引用計數+1) 所以我們必須釋放對象的引用.

3.1.4 DriverA的核心代碼

DriverB還是一直在使用的支持異步掛起的驅動.核心在於DriverA的編寫.

代碼如下:

//直接IRP調用-同步方式
void CallDriverMethod5()
{



    NTSTATUS status = STATUS_UNSUCCESSFUL;
    IO_STATUS_BLOCK status_block = { 0 };
    TCHAR read_buf[100] = { 0 };
    PFILE_OBJECT file_object;
    PDEVICE_OBJECT device_obj;
    do
    {

        KdBreakPoint();
        //1.構建設備名字
        UNICODE_STRING uc_device_name = RTL_CONSTANT_STRING(L"\\device\\DriverB");
        //2.根據設備名字獲取實際的設備對象和文件對象,等價於直接調用
        //CreateFile(返回的句柄可以通過句柄找對象方式獲得設備對象和文件對象)
        status = IoGetDeviceObjectPointer(&uc_device_name, FILE_ALL_ACCESS, &file_object, &device_obj);
        if (!NT_SUCCESS(status))
            break;
        //3.申請 IRP接口 可以生成 IRP_MJ_READ 請求
        LARGE_INTEGER offset = { 0 };
        KEVENT event = { 0 };
        KeInitializeEvent(&event, NotificationEvent, FALSE);
        PIRP irp = IoBuildSynchronousFsdRequest(IRP_MJ_READ, device_obj, read_buf, 100, &offset, &event, &status_block);
        PIO_STACK_LOCATION irp_stack = IoGetNextIrpStackLocation(irp);
        irp_stack->FileObject = file_object;
        status =  IoCallDriver(device_obj, irp);

        if (status == STATUS_PENDING)
        {

                DbgPrint("[A]---> 獲取文件對象成功,檢查事件\r\n");
                //採用無限等待方式進行等待
                KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, 0);
                DbgPrint("[A]---> 數據讀取完成\r\n");
                DbgPrint("[A] --> read value is %ws \r\n", read_buf);
        }
    } while (false);
    ObDereferenceObject(file_object); //減少引用
    ObDereferenceObject(device_obj);
    return;
}

在這裏生成的功能號是 IRP_MJ_READ 所以變相的是相當於我們的 IoBuildSynchronousFsdRequest 函數是 ZwReadFile.

最後程序直接調用 IoCallDriver進行了發送.

3.1.5 效果

3.2 設備調用方式-異步方式

3.2.1 異步IRP申請說明

其中對於API來說. 在同步調用裏面都已經說了. 異步調用方式就是申請IRP的方式的不同.採用的API不同.

API如下:

__drv_aliasesMem PIRP IoBuildAsynchronousFsdRequest(
  [in]           ULONG            MajorFunction,
  [in]           PDEVICE_OBJECT   DeviceObject,
  [in, out]      PVOID            Buffer,
  [in, optional] ULONG            Length,
  [in, optional] PLARGE_INTEGER   StartingOffset,
  [in, optional] PIO_STATUS_BLOCK IoStatusBlock
);

唯一不同的就是沒有事件域了.對於 IoBuildAsynchronousFsdRequest 創建的IRP請求,當請求結束的時候,操作系統不會在進行事件通知了. 不過我們想要接受到事件的通知. 那麼就要使用 IRP->UserEvent 操作系統會檢查這個域是否爲空,如果不是空則設置. 所以這個地方當我們的同步點使用. 當請求結束的時候(DriverB --> IoCompleteRequest則會設置這個域) 則會被設置.

3.2.2 異步IRP代碼演示

void CallDriverMethod6()
{



    NTSTATUS status = STATUS_UNSUCCESSFUL;
    IO_STATUS_BLOCK status_block = { 0 };
    TCHAR read_buf[100] = { 0 };
    PFILE_OBJECT file_object;
    PDEVICE_OBJECT device_obj;
    do
    {

        KdBreakPoint();
        //1.構建設備名字
        UNICODE_STRING uc_device_name = RTL_CONSTANT_STRING(L"\\device\\DriverB");
        //2.根據設備名字獲取實際的設備對象和文件對象,等價於直接調用
        //CreateFile(返回的句柄可以通過句柄找對象方式獲得設備對象和文件對象)
        status = IoGetDeviceObjectPointer(&uc_device_name, FILE_ALL_ACCESS, &file_object, &device_obj);
        if (!NT_SUCCESS(status))
            break;
        //3.申請 IRP接口 可以生成 IRP_MJ_READ 請求
        LARGE_INTEGER offset = { 0 };
        KEVENT event = { 0 };
        KeInitializeEvent(&event, SynchronizationEvent, FALSE);
        PIRP irp = IoBuildAsynchronousFsdRequest(IRP_MJ_READ, device_obj, read_buf, 100, &offset, &status_block);
        irp->UserEvent = &event;  //注意此位置,設置一個事件對象的值
        PIO_STACK_LOCATION irp_stack = IoGetNextIrpStackLocation(irp);
        irp_stack->FileObject = file_object;
        status = IoCallDriver(device_obj, irp);

        if (status == STATUS_PENDING)
        {

            DbgPrint("[A]---> 獲取文件對象成功,檢查事件\r\n");
            //採用無限等待方式進行等待
            KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, 0);
            DbgPrint("[A]---> 數據讀取完成\r\n");
            DbgPrint("[A] --> read value is %ws \r\n", read_buf);
        }
    } while (false);
    ObDereferenceObject(file_object); //減少引用
    ObDereferenceObject(device_obj); //減少引用
    return;
}

效果同上.

未完待續

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