在筆者前一篇文章《驅動開發:核心檔案讀寫系列函數》
簡單的介紹了核心中如何對檔案進行基本的讀寫操作,本章我們將實現核心下遍歷檔案或目錄這一功能,該功能的實現需要依賴於ZwQueryDirectoryFile
這個核心API函數來實現,該函數可返回給定檔案控制程式碼指定的目錄中檔案的各種資訊,此類資訊會儲存在PFILE_BOTH_DIR_INFORMATION
結構下,通過遍歷該目錄即可獲取到檔案的詳細引數,如下將具體分析並實現遍歷目錄功能。
該功能也是ARK工具的最基本功能,如下圖是一款通用ARK工具的檔案遍歷功能的實現效果;
在概述中提到過,目錄遍歷的核心是ZwQueryDirectoryFile()
系列函數,該函數可返回給定檔案控制程式碼指定的目錄中檔案的各種資訊,其微軟官方定義如下;
NTSYSAPI NTSTATUS ZwQueryDirectoryFile(
[in] HANDLE FileHandle, // 返回的檔案物件的控制程式碼,表示要為其請求資訊的目錄。
[in, optional] HANDLE Event, // 呼叫方建立的事件的可選控制程式碼。
[in, optional] PIO_APC_ROUTINE ApcRoutine, // 請求的操作完成時要呼叫的可選呼叫方提供的 APC 例程的地址。
[in, optional] PVOID ApcContext, // 如果呼叫方提供 APC 或 I/O 完成物件與檔案物件關聯,則為呼叫方確定的上下文區域的可選指標。
[out] PIO_STATUS_BLOCK IoStatusBlock, // 指向 IO_STATUS_BLOCK 結構的指標,該結構接收最終完成狀態和有關操作的資訊。
[out] PVOID FileInformation, // 指向接收有關檔案的所需資訊的輸出緩衝區的指標。
[in] ULONG Length, // FileInformation 指向的緩衝區的大小(以位元組為單位)。
[in] FILE_INFORMATION_CLASS FileInformationClass, // 要返回的有關目錄中檔案的資訊型別。
[in] BOOLEAN ReturnSingleEntry, // 如果只應返回單個條目,則設定為 TRUE ,否則為 FALSE 。
[in, optional] PUNICODE_STRING FileName, // 檔案路徑
[in] BOOLEAN RestartScan // 如果掃描是在目錄中的第一個條目開始,則設定為 TRUE 。
);
該函數我們需要注意FileInformation
引數,在本例中它被設定為了PFILE_BOTH_DIR_INFORMATION
用於儲存當前節點下檔案或目錄的一些屬性,如檔名,檔案時間,檔案狀態等,其次FileInformationClass
引數也是有多種選擇的,本例中我們需要遍歷檔案或目錄則設定成FileBothDirectoryInformation
就可以,在迴圈遍歷檔案時需要將當前目錄.以及上一級目錄..排除,而pDir->FileAttributes
則用於判斷當前節點是檔案還是目錄,屬性FILE_ATTRIBUTE_DIRECTORY
代表是目錄,反之則是檔案,實現目錄檔案遍歷完整程式碼如下所示;
// 署名權
// right to sign one's name on a piece of work
// PowerBy: LyShark
// Email: [email protected]
#include <ntifs.h>
#include <ntstatus.h>
// 遍歷資料夾和檔案
BOOLEAN MyQueryFileAndFileFolder(UNICODE_STRING ustrPath)
{
HANDLE hFile = NULL;
OBJECT_ATTRIBUTES objectAttributes = { 0 };
IO_STATUS_BLOCK iosb = { 0 };
NTSTATUS status = STATUS_SUCCESS;
// 初始化結構
InitializeObjectAttributes(&objectAttributes, &ustrPath, OBJ_CASE_INSENSITIVE | OBJ_KERNEL_HANDLE, NULL, NULL);
// 開啟檔案得到控制程式碼
status = ZwCreateFile(&hFile, FILE_LIST_DIRECTORY | SYNCHRONIZE | FILE_ANY_ACCESS,
&objectAttributes, &iosb, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ | FILE_SHARE_WRITE,
FILE_OPEN, FILE_DIRECTORY_FILE | FILE_SYNCHRONOUS_IO_NONALERT | FILE_OPEN_FOR_BACKUP_INTENT,
NULL, 0);
if (!NT_SUCCESS(status))
{
return FALSE;
}
// 為節點分配足夠的空間
ULONG ulLength = (2 * 4096 + sizeof(FILE_BOTH_DIR_INFORMATION)) * 0x2000;
PFILE_BOTH_DIR_INFORMATION pDir = ExAllocatePool(PagedPool, ulLength);
// 儲存pDir的首地址
PFILE_BOTH_DIR_INFORMATION pBeginAddr = pDir;
// 獲取資訊,返回給定檔案控制程式碼指定的目錄中檔案的各種資訊
status = ZwQueryDirectoryFile(hFile, NULL, NULL, NULL, &iosb, pDir, ulLength, FileBothDirectoryInformation, FALSE, NULL, FALSE);
if (!NT_SUCCESS(status))
{
ExFreePool(pDir);
ZwClose(hFile);
return FALSE;
}
// 遍歷
UNICODE_STRING ustrTemp;
UNICODE_STRING ustrOne;
UNICODE_STRING ustrTwo;
RtlInitUnicodeString(&ustrOne, L".");
RtlInitUnicodeString(&ustrTwo, L"..");
WCHAR wcFileName[1024] = { 0 };
while (TRUE)
{
// 判斷是否是..上級目錄或是.本目錄
RtlZeroMemory(wcFileName, 1024);
RtlCopyMemory(wcFileName, pDir->FileName, pDir->FileNameLength);
RtlInitUnicodeString(&ustrTemp, wcFileName);
// 是否是.或者是..目錄
if ((0 != RtlCompareUnicodeString(&ustrTemp, &ustrOne, TRUE)) && (0 != RtlCompareUnicodeString(&ustrTemp, &ustrTwo, TRUE)))
{
// 判斷是檔案還是目錄
if (pDir->FileAttributes & FILE_ATTRIBUTE_DIRECTORY)
{
// 目錄
DbgPrint("[目錄] 建立時間: %u | 改變時間: %u 目錄名: %wZ \n", pDir->CreationTime, &pDir->ChangeTime, &ustrTemp);
}
else
{
// 檔案
DbgPrint("[檔案] 建立時間: %u | 改變時間: %u | 檔名: %wZ \n", pDir->CreationTime, &pDir->ChangeTime, &ustrTemp);
}
}
// 遍歷完畢直接跳出迴圈
if (0 == pDir->NextEntryOffset)
{
break;
}
// 每次都要將pDir指向新的地址
pDir = (PFILE_BOTH_DIR_INFORMATION)((PUCHAR)pDir + pDir->NextEntryOffset);
}
// 釋放記憶體並關閉控制程式碼
ExFreePool(pBeginAddr);
ZwClose(hFile);
return TRUE;
}
VOID UnDriver(PDRIVER_OBJECT driver)
{
DbgPrint("驅動解除安裝成功 \n");
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT Driver, PUNICODE_STRING RegistryPath)
{
DbgPrint("Hello LyShark.com \n");
// 遍歷資料夾和檔案
UNICODE_STRING ustrQueryFile;
RtlInitUnicodeString(&ustrQueryFile, L"\\??\\C:\\Windows");
MyQueryFileAndFileFolder(ustrQueryFile);
DbgPrint("驅動載入成功 \n");
Driver->DriverUnload = UnDriver;
return STATUS_SUCCESS;
}
編譯如上驅動程式並執行,則會輸出C:\\Windows
目錄下的所有檔案和目錄,以及建立時間和修改時間,輸出效果如下圖所示;
你是否會覺得很失望,為什麼不是遞迴列舉,這裡為大家解釋一下,通常情況下ARK工具並不會在核心層實現目錄與檔案的遞迴操作,而是將遞迴過程搬到了應用層,當用戶點選一個新目錄時,在應用層只需要拼接新的路徑再次傳送給驅動程式讓其重新遍歷一份即可,這樣不僅可以提高效率而且還降低了藍屏的風險,顯然在應用層遍歷是更合理的。