This article is the continue of the previously posted project Hide Driver. Like the first article this one doesn't pretend to be full and original. The main purpose of it is to represent the complicated info in some more popular way.
The method of hiding described in the previous article is very simple and widely known. Now I pretend to describe the method of detection of such hidden files and processes in simple and easy to understand way. This method is accompanied by the code developed to illustrate the words.
- Projects used
- Levels of hiding
- Original hadler calls
- Hidden process detection
- Hidden file detection
- GUI application
- Communication with DetectDriver
- Format of communication and DetectDriver IOCTLs
- Project structure and build
- How to build
- Supported Windows version and testing
I would like to thank the people who developed the projects listed below - they made the implementation of this project easier:
- Windows Kernel Debugger booster for Virtual Machines
Levels of hiding
So let's start. First of all we should consider where the interception can be performed and how it can be avoided.
In this exapmle I described levels of file hiding, but it is valid for process hiding too, with a little correction.
Method described in this article works at the SSDT level. It is one of the simpliest level to make some changes (particullary hide objects) and that's why it's natural to consider it first.
Note that to detect a hidden information we should work at the level which is lower than the level where hiding is performed.
The hiding method that uses SSDT is based on the fact that all calls in the system come through this table (see more deatils in Hide Driver article). To avoid the interception at the SSDT level we should understand how this table is used.
System Service Descriptor Table
This table stores pointers to system functions. This table is used to find a function by index.
Example. Use WinDBG to see what SSDT is storing.
kd> dds KiServiceTable L100 80501030 8059849a nt!NtAcceptConnectPort 80501034 805e5666 nt!NtAccessCheck ... 805010c4 8056d14c nt!NtCreateFile ...
Note that all pointers point to
Thus to avoid SSDT address substitution we should get the original address of
nt!NtXXX function, for example by means of
Original hadler calls
But before we start with the calls of original functions let's consider how they work.
If you are not acquainted with the driver development for Windows, you may be interested to know that there are functions in kernel mode that differ only by prefix,
MSDN says: If the call occurs in user mode, you should use the name
"NtCreateFile" instead of
To find difference we can use Windbg.
The most important information is marked in the example. The call in the first case is translated to
nt!KiSystemService function, and 25h is the index of this function in SSDT table for test system. Thus we can conclude that
ZwXXX function calls
NtXXX function using SSDT.
Proceeding in this way we can discover that the only difference between
Zw functions is in
Previous Mode parameter modification.
ZwXXX function can be represented as follows:
It means that to call the original function we should not only know its address but also set
previous mode = kernel mode beforehand.
Note that Previous Mode affects the level of priveleges. Kernel handles are not accesible for the thread with Previous Mode equal to user mode.
To bypass the check of Previous Mode we use the fact that all system threads have Previous Mode equal to kernel mode.
There are two methods to start some code in the system context: Work Items and System Threads. I use both. Work Items is better for hidden process detection because it is faster, and thus corresponding actions take a very short time. For hidden file detection it's better to use System Threads because detection can take very long.
Utility for function start in the system context
These utilities start the given function in the system context using Work Items (see
IoQueueWorkItem() function). As soon as С++ is used and exceptions are the essential part of it, these utilities also transmit
std::exception and ones inherited from it, which were thrown from Work Item, to the point where the function was called from. The input parameters are function pointer and parameters that should be transmitted to this funcion.
[Code from file src\drvUtils\WorkItemUtils.h]
CallFromWorkerThreadEx function is an auxiliary one, its task is to check if the pointer to the function transmitted as the first parameter accepts the parameters transmitted with the second parameter.
The implementation of these functions is given below.
[Code from file src\drvUtils\WorkItemUtils.сpp]
Hidden process detection
The detection of the hidden processes on the SSDT level is performed by comparing the obtained results of the original function and the function which address is indicated in the SSDT table.
In general we can divide the hidden process detection into three stages:
- Receiving the function pointer.
- Function call in the system context.
- Analysis of the data obtained from the two functions.
Receiving the function pointer
Receiving the pointer for the function from the SSDT table by its name is implemented in the code below. It should be performed because the function can have different indices in SSDT table in different OS versions.
To minimize the volume of code shown, the content of the ServiceTableDef.h and VersionDependOffsets.h files is omitted.
[Code from file src\drvUtils\ServiceTableUtils.h]
Obtaining data by means of function calls
Acording to hidden process detection algorithm, we need to call
Nt functions and compare results. You can see these operations in the code below.
I want to remind that the original functions should be called with
PreviousMode = KernelMode and we can do it by calling them in the system context.
[Code from file src\DetectDriver\NativeApiWork.h]
[Code from file src\DetectDriver\NativeApiWork.сpp]
Analysis of data obtained from the two functions
After information from two functions is recieved we can compare these data. Analysis is performed by means of two functions:
Here is their definitions but implementations are not shown here because logic of those functions is very simple.
[Code from file src\DetectDriver\ProcessListWork.h]
You can find implementation in file src\DetectDriver\ProcessListWork.cpp.
Now we can use all code described above to detect hidden processes. Header files and functions for interaction with a user are omitted.
In code below you can see two actions: receiving and analysis. The result of these actions will be the list of hidden processes.
[Code from file src\DetectDriver\ProcessListWork.cpp]
All main stages of hidden processes detection are shown now. The rest of stages is gui stuff or translation between user mode and kernel mode. All of them can be found in sources attached.
Hidden file detection
Hidden file detection is very simular to hidden process detection. The main principle is the same: to compare the call of original function and intercepted one. So, the algorithm of hidden file detection resembles the one for file search on the disk. We just will have two lists of files and folders obtained by the original function handler and the interceptor.
But there are some essencial differences:
- Complication of algorithm for hidden resource search.
- The time of detection is much longer and is not detected in advance.
- Complication of algorithm of data exchange between user application and driver.
One of the pecularities of the kernel mode programming, comparing to user mode programming, is that we should pay attention on the limitation of the stack size, that is 12kb. To avoid this problem I decided to refuse from the recursive algorithm of file search but to use std::stack - container from STL, it transformed the recursive algorithm into the non-recursive one.
The algorithm description will be given in the next topic.
This problem causes the next one.
When the hidden processes are detected the result is the list of them, which can be returned by user request. When the hidden files are detected the driver cannot act like that as far as the process of hidden file search can take a very long time. Thus it should periodically notify the user part of the application about the state of hidden file detection. This functionality is not described in this article as soon as it's rather simple.
File search architecture
Before I started the development I had researched the solutions of the simular tasks at the user level. I had found that the solution from Microsoft was very suitable. The MFC library contains very useful class
CFileFind. Continuing research I had discovered that there are no functions
FindNextFile in kernel mode, and there is function
ZwQueryDirectoryFile instead. It meant that I had to develop my own analogues for these functions. And also I had to take into account that there was also
NtQueryDirectoryFile function and thus I had to abstruct away from the specific version.
Taking into account all above mentioned I created such architecture:
IEnvFileApi - Interface that abstructs away from the specific file API. (It defines such functions: QueryDirFile,OpenFile,Close). FileFindApi - Class that implements functions FindFirstFile, FindNextFile, FindClose. It receives IEnvFileApi as a parameter. FileFind - Class that implements logic like CFileFind class in MFC. It receives FileFindApi as a parameter.
Thus I managed to share the responsibilities and make the usage of
FileFind class independent from the specific file API -
Nt and so on.
The code below illustrates the classes discussed. You can find their implementations in the .cpp files of the same names.
[Code from file src\drvUtils\IEnvFileApi.h]
[Code from file src\drvUtils\FileFindApi.h]
[Code from file src\drvUtils\FileFind.h]
Utils for file search in kernel mode
Now, when we've already considered the auxilary classes, we can proceed to
FileSearcher class, which implements non-recursive file search, and
FileEnumerator class, which implements the file enumeration in one directory.
[Code from file src\drvUtils\FileSearcher.h]
[Code from file src\drvUtils\FileSearcher.cpp]
Hidden file detection algorithm
This algorithm combines utilities for file search in kernel mode and abstraction of file API.
Key point of the algorithm is that we use jointly
FileEnumerator class for breadth-first search and
FileSearcher class for depth-first search.
[Code from file src\DetectDriver\FileDetectAlgorithm.cpp]
All main stages of hidden file detection are shown now. The rest of stages is gui stuff or translation between user mode and kernel mode. All of them can be found in sources attached.
After the description of the algorithms we can now proceed to consider the results of their work.
For the demonstration we first need to hide some processes. Below I show the sequence of actions needed to hide the processes and then detect them.
First of all we install and run both of drivers - HideDriver and DetectDriver. Installation is identical for HideDriver and DetectDriver.
- Choose driver file to install.
- Install driver on system.
- Run previosly installed driver.
To hide some processes we need to right click on ListCtrl area and choose "Add" menu item.
- Open process list.
- Choose processes and click "Ok" button.
- Click "Add" button to hide selected processes.
Then we need to:
Detect hidden processes
To detect hidden processes we need to click on "Start" button in DetectDriver exchange utility. You can see an example of hiding all processes on screenshots below .
When everything described above is performed you can see the following records in DebugView or WinDbg:
------HIDE DRIVER START------ ------DETECT DRIVER START------ -HideDriver- IRP_MJ_CREATE -HideDriver- Add Rule - Input string: *;*;* -HideDriver- IRP_MJ_CLOSE -DetectDriver- IRP_MJ_CREATE -DetectDriver- Query Hidden Processes -DetectDriver- IRP_MJ_CLOSE
Communication with DetectDriver
IOCTLs and the
DeviceIoCotrol() routine should be used for communication between the user-mode application and the driver.
For more information see the same topic in the previous article HideDriver.
Format of communication and DetectDriver IOCTLs
Format of cummunication is shown below, also you can find additional information in such files:
- src/DetectDriverGUI/ProcessForm.cpp - User mode part of hidden process detection.
- src/DetectDriverGUI/FileForm.cpp - User mode part of hidden file detection.
- src/DetectDriver/ProcessDetector.cpp - Kernel mode part of hidden process detection.
- src/DetectDriver/FileDetector.cpp - Kernel mode part of hidden file detection.
Files, which are responsible for sending request to kernel-mode:
Files, which are responsible for answers to user-mode calls:
[Code from file src\Common\DetectDriver_Ioctl.h]
Project structure and build
.\bin - folder with binary files
.\lib - folder with library files
.\obj - folder with object files
.\src - folder with source files
|-> .\Common - Files that are shared between projects.
|-> .\STLPort - Directory with STLPort 4.6 ported for using in windows drivers.
|-> .\drvCppLib - Kernel Library to develop driver in C++.
|-> .\drvCppLibTest - Kernel Driver to test drvCppLib.
|-> .\drvUtils - Kernel Library with utils for kernel mode projects.
|-> .\HideDriver - Kernel Driver installed by Gui App. Performs hiding work.
|-> .\HideDriverGUI - Win32 Application used to run hide driver and communicate with it.
|-> .\DetectDriver - Kernel Driver installed by Gui App. Performs detection work.
|-> .\DetectDriverGUI - Win32 Application used to run detect driver and communicate with it.
|-> .\Utils - Win32 Library with utils for user mode projects.
|-> .\UtilsPortable - Directory with headers for user mode and kernel mode projects.
|-> .\UtilsPortableUnitTest - Win32 Application with unit test for UtilsPortable.
How to build
- Install Windows Driver Developer Kit 2003
- Set global environment variable "BASEDIR" to path of installed DDK.
Computer -> Properties -> Advanced -> Environment variables ->System Variables -> New
Like this: BASEDIR -> c:\winddk\3790
(You have to restart your computer after this)
- Download and install boost( tested with 1.41 version )
- Set global environment variable "BOOST" to path of installed boost.
After this you can use the file "DetectDriver_vs9.sln" in Visual Studio 2008.
Supported Windows version and testing
All tests were performed with Driver Verifier Enabled with all options ON except low resource simulation.
- Windows 2000, SP4
- Windows XP, SP3
- Windows 2003 Server, R2
- Windows Vista, SP0,SP1
- Windows 2008 Server
- Windows 7
All versions are x86, x64 windows version is not supported because of PatchGuard.
- Mark Russinovich, David Solomon. Microsoft Windows Internals.
- Greg Hoglund, Jamie Butler. Rootkits: Subverting the Windows Kernel.
- Gary Nebbett. Windows NT/2000 Native API Reference.
- Sven B. Schreiber. Undocumented Windows 2000 Secrets - A Programmer's Cookbook.