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.
Contents
- 1. Projects used
- 2. Levels of hiding
- 3. Original hadler calls
- 4. Hidden process detection
- 5. Hidden file detection
- 6. GUI application
- 7. Communication with DetectDriver
- 8. Format of communication and DetectDriver IOCTLs
- 9. Project structure and build
- 10. How to build
- 11. Supported Windows version and testing
- 12. Bibliography
Projects used
I would like to thank the people who developed the projects listed below – they made the implementation of this project easier:
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.
Detection method
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 nt!NtXXX
funtions.
Thus to avoid SSDT address substitution we should get the original address of nt!NtXXX
function, for example by means of MmGetSystemRoutineAddress()
function.
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, Zw
or Nt
.
MSDN says: If the call occurs in user mode, you should use the name "NtCreateFile"
instead of "ZwCreateFile"
.
Diference between ZwXXX
and NtXXX
functions.
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 Nt
and Zw
functions is in Previous Mode
parameter modification.
Schematically ZwXXX
function can be represented as follows:
ZwXXX()
{
KPROCESSOR_MODE prevMode = KeSetPreviousMode(Kernel);
NtXXX();
KeSetPreviousMode(prevMode);
}
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.
Bypassing ExGetPreviousMode()
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]
#pragma once
namespace utils
{
void CallFromWorkerThread(PVOID routine,PVOID params);
template<class ParamsType>
inline void
CallFromWorkerThreadEx(void (*routine)(ParamsType*),
ParamsType* params)
{
CallFromWorkerThreadEx((PVOID)routine,(PVOID)params);
}
}
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]
#include "drvCommon.h"
#include "WorkItemUtils.h"
#include "KernelEvent.h"
#include <string>
#include <stdexcept>
//The object associated with the driver
extern PDEVICE_OBJECT gDeviceObject;
namespace utils
{
typedef void (*OriginalRoutine)(PVOID);
struct WorkItemParams
{
OriginalRoutine Routine;
PVOID params;
char* strError;
size_t strErrorSize;
KernelNotificationEvent* finishEvent;
NTSTATUS status;
};
VOID WorkItemRoutine(IN PDEVICE_OBJECT DeviceObject,
IN PVOID Context)
{
WorkItemParams* routineParams = (WorkItemParams*)Context;
routineParams->status = STATUS_SUCCESS;
try
{
routineParams->Routine(routineParams->params);
}
catch(std::exception& ex)
{
routineParams->status = STATUS_UNSUCCESSFUL;
std::string str(ex.what());
size_t toWrite = min(str.size(),routineParams->strErrorSize - 1);
memcpy(routineParams->strError,str.c_str(),toWrite);
routineParams->strError[toWrite] = '\0';
}
routineParams->finishEvent->set();
}
void CallFromWorkerThread(PVOID routine,PVOID params)
{
PIO_WORKITEM workItem = ::IoAllocateWorkItem(gDeviceObject);
if(workItem == NULL)
throw std::exception("Can't create work item.");
utils::WorkItemGuard guard(workItem);
char errorMsg[255];
KernelNotificationEvent finishEvent;
WorkItemParams wiParams = {(OriginalRoutine)routine,params,
errorMsg,sizeof(errorMsg),&finishEvent};
IoQueueWorkItem(workItem,&WorkItemRoutine,DelayedWorkQueue,&wiParams);
finishEvent.wait();
if( !NT_SUCCESS(wiParams.status) )
{
throw std::runtime_error(errorMsg);
}
}
}
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]
#include "ServiceTableDef.h"
#include "VersionDependOffsets.h"
namespace utils
{
const UCHAR opcode_MovEax = 0xB8; // __asm move eax,Value
inline ULONG GetFunctionSSTIndex(PUNICODE_STRING function_name)
{
/*
All ZwXXX functions exported by NTOSKRNL.exe start with :
mov eax, ULONG
where ULONG is the NtXXX function index in SST
*/
// Be careful with function MmGetSystemRoutineAddress, for
// Windows XP(SP2) and lower this function throws SEH exception if
// an invalid system routine name was passed to it.
// Using __try/__except block is not good solution since SEH
// is not a formal contract for this API, so there is no guarantee
// that the OS is still in a stable state after you have caught
// the exception.
PVOID pTrueFuncPtr_ZW = MmGetSystemRoutineAddress(function_name);
if(pTrueFuncPtr_ZW == NULL)
throw std::exception(__FUNCTION__" Can't get function address.");
// Check command byte for valid opcode
// Starts from Vista DriverVerifier can substitute ntoskrnl code.
if( *( (PUCHAR)pTrueFuncPtr_ZW ) != opcode_MovEax )
throw std::exception(__FUNCTION__" Function address points to supposititious code.");
// Skip command byte, move to index byte
ULONG funcIndex = *(PULONG)((PUCHAR) pTrueFuncPtr_ZW + 1);
if( funcIndex == NULL)
throw std::exception(__FUNCTION__" Can't get function SST index");
return funcIndex;
}
inline PVOID GetFunctionSSTPtr(ULONG fncIndex)
{
PNTPROC ServiceTable=pNtoskrnl->ServiceTable;
ULONG TotalCount = pNtoskrnl->ServiceLimit;
if( fncIndex > TotalCount )
throw std::exception(__FUNCTION__" Wrong SST index");
return ServiceTable[fncIndex];
}
inline PVOID GetFunctionSSTPtr(PUNICODE_STRING function_name)
{
return GetFunctionSSTPtr( GetFunctionSSTIndex(function_name) );
}
inline PVOID GetFunctionSSTPtrEx(PUNICODE_STRING function_name)
{
ULONG fncIndex;
try
{
fncIndex = utils::GetFunctionSSTIndex(function_name);
}
catch(const std::exception&)
{
// Use predefined offsets
fncIndex = utils::GetSSTOffsetByName(function_name->Buffer,function_name->Length/2);
}
return utils::GetFunctionSSTPtr(fncIndex);
}
}
Obtaining data by means of function calls
Acording to hidden process detection algorithm, we need to call Zw
and 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]
#pragma once
#include <vector>
namespace DetectDriver
{
void GetProcessInfo(std::vector<char> *hookListBuffer,
std::vector<char> *originalListBuffer);
}
[Code from file src\DetectDriver\NativeApiWork.ัpp]
#include "drvCommon.h"
#include "NativeApiWork.h"
#include "WorkItemUtils.h"
#include "ServiceTableUtils.h"
#include "StringUtils.h"
#include <string>
extern "C"
{
typedef NTSTATUS (*NtQuerySystemInfoPtr)(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
IN OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
}// extern "C"
namespace DetectDriver
{
const wchar_t* strZwQuerySystemInfo = L"ZwQuerySystemInformation";
const wchar_t* strNtQuerySystemInfo = L"NtQuerySystemInformation";
void GetZwQuerySysInfoPointers(PVOID* sstPtr,PVOID* fncPtr)
{
std::wstring functionNameZw(strZwQuerySystemInfo);
UNICODE_STRING functionNameZwUnc = utils::wstring2UnicodeStr(functionNameZw);
std::wstring functionNameNt(strNtQuerySystemInfo);
UNICODE_STRING functionNameNtUnc = utils::wstring2UnicodeStr(functionNameNt);
*sstPtr = utils::GetFunctionSSTPtr(&functionNameZwUnc);
*fncPtr = MmGetSystemRoutineAddress(&functionNameNtUnc);
if( *fncPtr == NULL )
throw std::exception("Can't get system routine address");
}
void GetProcListsImpl(NtQuerySystemInfoPtr originalFnc,
NtQuerySystemInfoPtr hookedFnc,
std::vector<char>* originalListBuffer,
std::vector<char>* hookListBuffer)
{
ULONG retLength;
NTSTATUS status;
// Request needed size
ULONG originalDataSize;
status = originalFnc(SystemProcessesAndThreadsInformation,NULL,0,&originalDataSize);
if( status != STATUS_INFO_LENGTH_MISMATCH )
throw std::exception("Can't get original process info data size.");
ULONG hookedDataSize;
status = hookedFnc(SystemProcessesAndThreadsInformation,NULL,0,&hookedDataSize);
if( status != STATUS_INFO_LENGTH_MISMATCH )
throw std::exception("Can't get hooked process info data size.");
// Resize buffers
hookListBuffer->resize(hookedDataSize);
originalListBuffer->resize(originalDataSize);
// Request information
status = originalFnc(SystemProcessesAndThreadsInformation,
&originalListBuffer->front(),originalListBuffer->size(),&retLength);
if( !NT_SUCCESS(status) )
throw std::exception("Can't get process info by original function.");
status = hookedFnc(SystemProcessesAndThreadsInformation,
&hookListBuffer->front(),hookListBuffer->size(),&retLength);
if( !NT_SUCCESS(status) )
throw std::exception("Can't get process info by hooked function.");
}
struct GetProcListParams
{
NtQuerySystemInfoPtr originalFnc;
NtQuerySystemInfoPtr hookedFnc;
std::vector<char>* originalListBuffer;
std::vector<char>* hookListBuffer;
};
void GetProcListsProxy(GetProcListParams* params)
{
GetProcListsImpl(params->originalFnc,
params->hookedFnc,
params->originalListBuffer,
params->hookListBuffer);
}
void GetProcLists(NtQuerySystemInfoPtr originalFnc,
NtQuerySystemInfoPtr hookedFnc,
std::vector<char>* originalListBuffer,
std::vector<char>* hookListBuffer)
{
GetProcListParams params = {originalFnc,
hookedFnc,
originalListBuffer,
hookListBuffer};
utils::CallFromWorkerThread(&GetProcListsProxy,¶ms);
}
void GetProcessInfo(std::vector<char> *hookListBuffer,
std::vector<char> *originalListBuffer)
{
PVOID hookedFncPtr,originalFncPtr;
GetZwQuerySysInfoPointers(&hookedFncPtr,&originalFncPtr);
if(hookedFncPtr == originalFncPtr)
throw std::exception("No hooks detected.");
NtQuerySystemInfoPtr hookedFnc = (NtQuerySystemInfoPtr)hookedFncPtr;
NtQuerySystemInfoPtr originalFnc = (NtQuerySystemInfoPtr)originalFncPtr;
GetProcLists(originalFnc,hookedFnc,originalListBuffer,hookListBuffer);
}
}
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: IsListsEqual()
and ExamineList()
.
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]
#pragma once
#include "drvCommon.h"
#include <list>
#include <string>
namespace DetectDriver
{
typedef SYSTEM_PROCESSES_INFORMATION ProcessInfoList;
typedef ProcessInfoList *ProcessInfoListPtr;
struct ProcessInfoShort
{
ULONG pid;
std::wstring name;
};
typedef std::list<ProcessInfoShort> ProcInfoShortList;
struct ProcessInfoFull:public ProcessInfoShort
{
std::wstring imagePath;
};
typedef std::list<ProcessInfoFull> ProcInfoFullList;
void ExamineList(ProcessInfoListPtr hookedListIn,
ProcessInfoListPtr originalListIn,
ProcInfoShortList* hiddenList);
bool IsListsEqual(ProcessInfoListPtr hookedList,
ProcessInfoListPtr originalList);
}//namespace DetectDriver
You can find implementation in file src\DetectDriver\ProcessListWork.cpp.
Gather everything
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]
namespace DetectDriver
{
void Detect(ProcInfoShortList* hiddenList)
{
std::vector<char> hookListBuffer,originalListBuffer;
GetProcessInfo(&hookListBuffer,&originalListBuffer);
ProcessInfoList *hookedList = (ProcessInfoList*)&hookListBuffer[0];
ProcessInfoList *originalList = (ProcessInfoList*)&originalListBuffer[0];
if( IsListsEqual(hookedList,originalList) == false )
{
ExamineList(hookedList,originalList,hiddenList);
if( !hiddenList->empty() )
return;
}
throw std::exception("No hidden process detected.");
}
}
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.
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.
- The time of detection is much longer and is not detected in advance.
This problem causes the next one.
- Complication of algorithm of data exchange between user application and driver.
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 FindFirstFile
and 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 – Zw
, 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]
#pragma once
#include "drvCommon.h"
namespace utils
{
struct IEnvFileApi
{
virtual NTSTATUS Close(HANDLE handle)=0;
virtual NTSTATUS OpenFile(PHANDLE FileHandle,
ACCESS_MASK DesiredAccess,
POBJECT_ATTRIBUTES ObjectAttributes,
PIO_STATUS_BLOCK IoStatusBlock,
ULONG ShareAccess,
ULONG OpenOptions)=0;
virtual NTSTATUS QueryDirFile(HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID FileInformation,
ULONG Length,
FILE_INFORMATION_CLASS FileInformationClass,
BOOLEAN ReturnSingleEntry)=0;
};
}
[Code from file src\drvUtils\FileFindApi.h]
#pragma once
#include "DirInfo.h"
#include "IEnvFileApi.h"
namespace utils
{
struct FileFindBuffer
{
DirInfo dirInfo;
WCHAR names[MAX_PATH];
};
class FileFindApi
{
IEnvFileApi* envApi_;
public:
FileFindApi(IEnvFileApi* envApi)
: envApi_(envApi)
{}
HANDLE FindFirstFile(const wchar_t* fileName,
FileFindBuffer* findFileData);
BOOL FindNextFile(HANDLE hFindFile,
FileFindBuffer* findFileData);
BOOL FindClose(HANDLE hFindFile);
};
}
[Code from file src\drvUtils\FileFind.h]
#pragma once
#include <string>
#include <memory>
#include "KernelHandleGuard.h"
#include "FileFindApi.h"
namespace utils
{
struct FileFindBuffer;
}
namespace utils
{
class FileFind
{
typedef std::auto_ptr<FileFindBuffer> FileFindBufPtr;
FileFindBufPtr m_pFoundInfo;
FileFindBufPtr m_pNextInfo;
utils::HandleGuard m_hContext;
std::wstring m_strRoot;
FileFindApi findApi_;
public:
FileFind(IEnvFileApi* envApi)
: findApi_(envApi)
{}
// Properties
ULONGLONG GetEndOfFile() const;
ULONGLONG GetAllocationSize() const;
std::wstring GetFileName() const;
std::wstring GetFilePath() const;
std::wstring GetRoot() const;
BOOL IsDots() const;
DirInfo* GetAllInfo()const;
// Attributes
BOOL MatchesMask(DWORD dwMask) const;
BOOL IsReadOnly() const;
BOOL IsDirectory() const;
BOOL IsCompressed() const;
BOOL IsSystem() const;
BOOL IsHidden() const;
BOOL IsTemporary() const;
BOOL IsNormal() const;
BOOL IsArchived() const;
// Operations
void Close();
BOOL FindFile(const std::wstring& path);
BOOL FindNextFile();
private:
void CloseContext();
void CheckHasInfo()const;
};
}
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]
#pragma once
#include <string>
#include "DirInfo.h"
#include "FileFindApi.h"
namespace utils
{
struct IFileObserver
{
virtual ~IFileObserver(){}
virtual void OnFileFound(const std::wstring& filePath,DirInfo* info)=0;
virtual void OnDirFound(const std::wstring& dirPath,DirInfo* info)=0;
};
class FileSearcher
{
IEnvFileApi* envApi_;
IFileObserver* observer_;
public:
FileSearcher(IFileObserver* observer,
IEnvFileApi* envApi)
: observer_(observer)
, envApi_(envApi)
{}
void StartSearch(const std::wstring& startPath);
};
class FileEnumerator
{
IEnvFileApi* envApi_;
IFileObserver* observer_;
public:
FileEnumerator(IFileObserver* observer,
IEnvFileApi* envApi)
: observer_(observer)
, envApi_(envApi)
{}
void StartEnumeration(const std::wstring& startPath);
};
}
[Code from file src\drvUtils\FileSearcher.cpp]
#include "drvCommon.h"
#include "FileSearcher.h"
#include "FileFind.h"
#include <stack>
#include <boost/shared_ptr.hpp>
namespace utils
{
typedef boost::shared_ptr<FileFind> FileFindPtr;
struct State
{
FileFindPtr findPtr;
bool bWorking;
};
typedef std::stack<State> FileFindStack;
void FileSearcher::StartSearch(const std::wstring& startPath)
{
FileFindStack finderStack;
FileFindPtr finder( new FileFind(envApi_) );
BOOL bWorking = finder->FindFile( startPath.c_str() );
while( bWorking )
{
bWorking = finder->FindNextFile();
if( finder->IsDots() )
goto CntinueCheckRet;
if( finder->IsDirectory() )
observer_->OnDirFound( finder->GetFilePath(), finder->GetAllInfo());
else
observer_->OnFileFound( finder->GetFilePath(), finder->GetAllInfo() );
if( finder->IsDirectory() )
{
FileFindPtr newFinder(new FileFind(envApi_));
BOOL ret = newFinder->FindFile( finder->GetFilePath() + L"\\" );
if( ret )
{
State state = {finder,bWorking};
finderStack.push(state);
bWorking = true;
finder = newFinder;
continue;
}
}
CntinueCheckRet:
while( !bWorking && !finderStack.empty() )
{
State state = finderStack.top();
finderStack.pop();
finder = state.findPtr;
bWorking = state.bWorking;
}
}
}
void FileEnumerator::StartEnumeration(const std::wstring& startPath)
{
FileFind finder(envApi_);
BOOL bWorking = finder.FindFile(startPath);
while (bWorking)
{
bWorking = finder.FindNextFile();
if( finder.IsDots() )
continue;
if( finder.IsDirectory() )
observer_->OnDirFound( finder.GetFilePath(), finder.GetAllInfo());
else
observer_->OnFileFound( finder.GetFilePath(), finder.GetAllInfo() );
}
}
}
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]
#include "drvCommon.h"
#include "FileDetectAlgorithm.h"
#include "FileSearcher.h"
#include "StringUtils.h"
#include "ServiceTableUtils.h"
#include "CustomFileApi.h"
#include <list>
#include <string>
#include <boost/bind.hpp>
namespace DetectDriver
{
const wchar_t* strZwClose = L"ZwClose";
const wchar_t* strNtClose = L"NtClose";
const wchar_t* strZwOpenFile = L"ZwOpenFile";
const wchar_t* strNtOpenFile = L"NtOpenFile";
const wchar_t* strZwQueryDirFile = L"ZwQueryDirectoryFile";
const wchar_t* strNtQueryDirFile = L"NtQueryDirectoryFile";
void GetApiPairPointers(const wchar_t* strZwName,const wchar_t* strNtName,
PVOID* sstPtr,PVOID* realPtr)
{
std::wstring functionNameZw(strZwName);
UNICODE_STRING functionNameZwUnc = utils::wstring2UnicodeStr(functionNameZw);
std::wstring functionNameNt(strNtName);
UNICODE_STRING functionNameNtUnc = utils::wstring2UnicodeStr(functionNameNt);
*sstPtr = utils::GetFunctionSSTPtr(&functionNameZwUnc);
*realPtr = MmGetSystemRoutineAddress(&functionNameNtUnc);
if( *realPtr == NULL || *sstPtr == NULL)
throw std::exception("Can't get system routine address");
}
void GetFileApiPointers(PVOID* sstPtrClose,PVOID* realPtrClose,
PVOID* sstPtrOpenFile,PVOID* realPtrOpenFile,
PVOID* sstPtrQueryDir,PVOID* realPtrQueryDir)
{
GetApiPairPointers(strZwClose,strNtClose,sstPtrClose,realPtrClose);
GetApiPairPointers(strZwOpenFile,strNtOpenFile,sstPtrOpenFile,realPtrOpenFile);
GetApiPairPointers(strZwQueryDirFile,strNtQueryDirFile,sstPtrQueryDir,realPtrQueryDir);
}
struct FileRecord
{
std::wstring filePath;
bool isDir;
};
typedef std::list<FileRecord> FileNameList;
class SaveObserver: public utils::IFileObserver
{
FileNameList* fileNameList_;
public:
SaveObserver(FileNameList* fileNameList)
: fileNameList_(fileNameList)
{}
// IFileObserver interfaces
void OnFileFound(const std::wstring& filePath,DirInfo* info)
{
AddNewRecord(filePath,false);
}
void OnDirFound(const std::wstring& dirPath,DirInfo* info)
{
AddNewRecord(dirPath,true);
}
private:
void AddNewRecord(const std::wstring& path,bool isDir)
{
FileNameList::iterator it =
fileNameList_->insert(fileNameList_->end(),FileRecord());
it->filePath = path;
it->isDir = isDir;
}
};
class ProxyObserver: public utils::IFileObserver
{
IDetectObserver* detectObserver_;
public:
ProxyObserver( IDetectObserver* detectObserver )
: detectObserver_(detectObserver)
{}
// IFileObserver interfaces
void OnFileFound(const std::wstring& filePath,DirInfo* info)
{
detectObserver_->AddHiddenRecord(filePath);
}
void OnDirFound(const std::wstring& dirPath,DirInfo* info)
{
detectObserver_->AddHiddenRecord(dirPath);
}
};
void Detect(const std::wstring& dirPath,
utils::IEnvFileApi* sstApi,
utils::IEnvFileApi* realApi,
IDetectObserver* detectObserver)
{
FileNameList hookedFileList;
{
SaveObserver observer(&hookedFileList);
utils::FileEnumerator enumerator(&observer,sstApi);
enumerator.StartEnumeration(dirPath);
}
FileNameList originalFileList;
{
SaveObserver observer(&originalFileList);
utils::FileEnumerator enumerator(&observer,realApi);
enumerator.StartEnumeration(dirPath);
}
if( IsListsEqual(hookedFileList,originalFileList) )
return;
FileNameList hiddenFileList;
GetListDifference(hookedFileList,originalFileList,&hiddenFileList);
// Report result
FileNameList::const_iterator it = hiddenFileList.begin();
for( ; it != hiddenFileList.end(); ++it )
{
detectObserver->AddHiddenRecord(it->filePath);
if( !it->isDir )
continue;
// Scan all subdirectories and report all result as hidden
ProxyObserver observer(detectObserver);
utils::FileSearcher searcher(&observer,realApi);
searcher.StartSearch(it->filePath);
}
}
class DirectoryObserver: public utils::IFileObserver
{
utils::IEnvFileApi* sstApi_;
utils::IEnvFileApi* realApi_;
IDetectObserver* detectObserver_;
public:
DirectoryObserver( utils::IEnvFileApi* sstApi,
utils::IEnvFileApi* realApi,
IDetectObserver* detectObserver)
: sstApi_(sstApi)
, realApi_(realApi)
, detectObserver_(detectObserver)
{}
// IFileObserver interfaces
void OnFileFound(const std::wstring& filePath,DirInfo* info)
{
if( detectObserver_->IsStoped() )
throw StopException("Detection is stopped.");
}
void OnDirFound(const std::wstring& dirPath,DirInfo* info)
{
if( detectObserver_->IsStoped() )
throw StopException("Detection is stopped.");
detectObserver_->OnDirProcessing(dirPath);
Detect(dirPath,sstApi_,realApi_,detectObserver_);
}
};
void DetectHiddenFiles(const std::wstring& path,
IDetectObserver* detectObserver)
{
PVOID sstPtrClose,realPtrClose;
PVOID sstPtrOpenFile,realPtrOpenFile;
PVOID sstPtrQueryDir,realPtrQueryDir;
GetFileApiPointers(&sstPtrClose, &realPtrClose,
&sstPtrOpenFile, &realPtrOpenFile,
&sstPtrQueryDir, &realPtrQueryDir);
if( (sstPtrQueryDir == realPtrQueryDir) && (sstPtrOpenFile==realPtrOpenFile) )
throw std::exception("No hooks detected.");
utils::CustomFileApi sstFileApi(sstPtrClose,sstPtrOpenFile,sstPtrQueryDir);
utils::CustomFileApi realFileApi(realPtrClose,realPtrOpenFile,realPtrQueryDir);
// Scan root directory
detectObserver->OnDirProcessing(path);
Detect(path,&sstFileApi,&realFileApi,detectObserver);
// Scan all subdirectories
DirectoryObserver dirObserver(&sstFileApi,&realFileApi,detectObserver);
utils::FileSearcher searcher(&dirObserver,&realFileApi);
searcher.StartSearch(path);
}
}
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.
GUI application
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.
Installation
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.
Hide processes
To hide some processes we need to right click on ListCtrl area and choose “Add” menu item.
Then we need to:
- Open process list.
- Choose processes and click “Ok” button.
- Click “Add” button to hide selected processes.
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:
Files, which are responsible for sending request to kernel-mode:
- src/DetectDriverGUI/ProcessForm.cpp – User mode part of hidden process detection.
- src/DetectDriverGUI/FileForm.cpp – User mode part of hidden file detection.
Files, which are responsible for answers to user-mode calls:
- src/DetectDriver/ProcessDetector.cpp – Kernel mode part of hidden process detection.
- src/DetectDriver/FileDetector.cpp – Kernel mode part of hidden file detection.
[Code from file src\Common\DetectDriver_Ioctl.h]
#ifndef DETECT_DRIVER_IOCTL_H_INCLUDED
#define DETECT_DRIVER_IOCTL_H_INCLUDED
/* This file defines Input-Output Control Codes to communicate with DetectDriver. */
/*
All input strings(parameters) are UNICODE strings.
Format of output string:
First byte contains the type of information returned:
QUERY_SUCCESS or QUERY_FAIL
After this byte output information is placed.
[STATUS BYTE][INFORMATION]
*/
#define QUERY_SUCCESS 1
#define QUERY_FAIL 2
/*-----------------------------------------------------------------------*/
/* Hidden processes detect IOCTLs */
#define IOCTL_PROCESSES_QUERY_HIDEN CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x601, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL id used to query detected hidden process.
No input paramters.
*/
/*-----------------------------------------------------------------------*/
/*-----------------------------------------------------------------------*/
/* Hidden files detect IOCTLs */
#define IOCTL_FILES_START_DETECT CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x701, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL is used to start detection of hidden files.
Input parameter is path to directory where search hidden files.
*/
#define IOCTL_FILES_STOP_DETECT CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x702, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL is used to start detection of hidden files.
No input paramters.
*/
enum FilesDetectStatus
{
FILES_DETECT_STATUS_EMPTY,
FILES_DETECT_STATUS_RUNNING,
FILES_DETECT_STATUS_FINISHED,
FILES_DETECT_STATUS_ABORTED
};
enum FileDetectFlags
{
FILES_DETECT_FLAG_EMPTY,
FILES_DETECT_FLAG_NEW_HIDDEN_FILE,
FILES_DETECT_FLAG_ERROR
};
#define IOCTL_FILES_QUERY_STATUS CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x703, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL is used to query status of detection of hidden files.
No input paramters.
*/
#define IOCTL_FILES_QUERY_HIDEN CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x705, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL is used to query detected hidden files.
No input paramters.
*/
#define IOCTL_FILES_QUERY_ERROR CTL_CODE( \
FILE_DEVICE_UNKNOWN, 0x706, METHOD_BUFFERED, FILE_ANY_ACCESS)
/*
This IOCTL is used to query error string.
No input paramters.
*/
/*-----------------------------------------------------------------------*/
#endif // #ifndef DETECT_DRIVER_IOCTL_H_INCLUDED)
Project structure and build
Directory structure:
.\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.
Project structure:
How to build
- Install Windows Driver Developer Kit 2003
http://www.microsoft.com/whdc/devtools/ddk/default.mspx - 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 )
http://www.boost.org/users/download/ - 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.
Bibliography
- 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.