Introduction
This Windows file monitoring system aims at providing security to files in a Windows environment. I was in a need to design an application which monitors file open, close, and save operations on Windows and restricts the user from accessing a subset of file types until this utility is installed. This was achieved by hooking Windows file related APIs and then preprocessing file open, save, and close operations in Windows as per the requirement. The preprocessing may be encryption of files, corrupting the file's header section, etc. If this utility is installed on the system, then the file, initially in the encrypted format, would first be decrypted before being opened, and then would again be encrypted when the file is closed or saved. This would help prevent the file from being accessed anywhere else where this utility is not installed, thereby providing security to the file.
I found a few good applications like AvaFind, FileMon, etc. performing file monitoring functionality. But, these applications use system drivers to achieve their goals. Because of the complexities involved in writing driver programs, I tried to achieve the same through Win32 user level programming. I have achieved this by hooking the CreateProcess()
, OpenProcess()
, CreateFile()
, CloseHandle()
, and WriteFile()
functions of kernel32.dll.
Out of the various ways available for hooking Windows APIs, I chose the method of "Hooking by altering the Import Address Table (IAT)" of all the processes running on the system. The DLL containing the code to change the IAT is injected into the address space of the target process (the process in which the DLL is to be injected), by creating a remote thread in the address space of that process using the Windows API CreateRemoteThread()
.
Injecting DLLs by remote threads is well documented in Jeffrey Richter's article "Load Your 32-bit DLL into Another Process's Address Space Using INJLIB". Injecting DLLs has by far been the most widely used concept, with the greatest advantage of getting hold over the process, once your DLL is within its address space. But, this method has a disadvantage of the DLL not being injected into the target process in case kernel32.dll does not get loaded at its well known preferred load address. Hence, the method of injecting a DLL is based on the sole assumption that the load addresses of Kernel32.dll remain same for both the processes.
Design and Implementation
Coming back to the original problem...
The first step I performed was to create HookAPI.dll, which contains the code to hook Windows APIs, and then this DLL is injected in all the running processes on the system. Once injected into the target process, HookAPI.dll changes the IAT of the process and all of its loaded modules. HookAPI.dll contains a function called GetIAList()
that traverses the IAT of the process in which it is injected. It uses EnumProcessModules()
to get the list of all the modules of the process in which it is injected. Thereafter, it checks which function is to be hooked, and replaces its address in the IAT by the address of the wrapper function provided for that API. GetNewAddress()
is the function which traverses a list specifying the functions to be hooked and returns the address of the wrapper function provided for the function to be hooked.
Here are the logical steps of a replacing cycle:
- Locate the import section from the IAT of each process loaded by the process DLL modules as well as the process itself.
- Find the
IMAGE_IMPORT_DESCRIPTOR
chunk of the DLL that exports that function. - Locate the
IMAGE_THUNK_DATA
which holds the original address of the imported function. - Replace the function address with the address of the wrapper function.
void GetIAList()
{
HMODULE hMods[1024];
TCHAR szLibFile[MAX_PATH];
HANDLE hProcess = GetCurrentProcess();
HMODULE hModule = GetModuleHandle(TEXT("HookAPI.dll"));
GetModuleFileName(hModule,szLibFile,sizeof(szLibFile));
DWORD cbNeeded = 0;
multimap <CString,void*> m_mapOld;
multimap <CString,void*> :: iterator m_AcIter;
PROC pfnNewAddress = NULL;
unsigned int i = 0;
PROC* ppfn = NULL;
CString str;
ULONG ulSize = 0;
if( EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded))
{
for ( i = 0; i < (cbNeeded / sizeof(HMODULE)); i++ )
{
TCHAR szModName[MAX_PATH];
if ( GetModuleFileNameEx( hProcess, hMods[i],
szModName, sizeof(szModName)))
{
if(_tcscmp(szModName,szLibFile) == 0)
{
i++;
}
}
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(hMods[i], TRUE,
IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize);
if(NULL != pImportDesc)
{
while (pImportDesc->Name)
{
PSTR pszModName = (PSTR)((PBYTE) hMods[i] + pImportDesc->Name);
CString strModName = pszModName;
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
( (PBYTE) hMods[i] + pImportDesc->FirstThunk );
while (pThunk->u1.AddressOfData)
{
ppfn = (PROC*) &pThunk->u1.AddressOfData;
str.Format(_T("%s:%x"),strModName,*ppfn);
m_mapOld.insert( func_Pair( str, ppfn ) );
pThunk++;
}
pImportDesc++;
}
}
}
}
for(m_AcIter = m_mapOld.begin() ; m_AcIter != m_mapOld.end() ; m_AcIter++)
{
if(pfnNewAddress != NULL)
{
PROC* pfnOldAddress = (PROC*)m_AcIter -> second;
MEMORY_BASIC_INFORMATION mbi = {0};
VirtualQuery( pfnOldAddress, &mbi, sizeof(MEMORY_BASIC_INFORMATION) );
VirtualProtect( mbi.BaseAddress,mbi.RegionSize,PAGE_EXECUTE_READWRITE,
&mbi.Protect);
*pfnOldAddress = *pfnNewAddress;
DWORD dwOldProtect = 0;
VirtualProtect( mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOldProtect );
}
}
}
Once this DLL is designed, the next job is to create Injector.exe which injects the hooking DLL, HookAPI.dll, in all the running processes, by using CreateRemoteThread()
, as shown below. InjectIntoExistingProcesses()
is the function which injects the DLL in all the running processes. It uses EnumProcesses()
to get the list of all the running processes. Here, pszLibFile
contains the path of the DLL which has to be injected, which is nothing but a path to HookAPI.dll.
TCHAR pszLibFile[MAX_PATH];
GetModuleFileName(NULL,pszLibFile,sizeof(szLibFile));
_tcscpy(_tcsrchr(pszLibFile,TEXT('\\')) + 1,TEXT("HookAPI.dll"));
VirtualAllocEx()
is used to allocate memory in the address space of the remote process where the DLL is to be loaded. WriteProcessMemory()
is used to write the DLL path in the allocated memory space. GetProcAddress()
gives the address of the LoadLibrary()
API (assuming that the load address of kernel32.dll is same for all the processes), and then CreateRemoteThread()
finally creates a thread in the remote process and loads the DLL in the address space of the remote process.
void InjectIntoExistingProcesses(PCWSTR pszLibFile)
{
BOOL fOk=FALSE;
PWSTR pszLibFileRemote = NULL;
int cch = 1+lstrlenW(pszLibFile);
int cb = (cch + sizeof(WCHAR))*sizeof(WCHAR);
DWORD aProcesses[1024], cbNeeded, cProcesses;
EnumProcesses( aProcesses, sizeof(aProcesses), &cbNeeded);
cProcesses = cbNeeded / sizeof(DWORD);
for (int i = 0; i < cProcesses; i++)
{
HANDLE hRunningProcess =
OpenProcess( PROCESS_ALL_ACCESS,FALSE, aProcesses[i] );
pszLibFileRemote = (PWSTR)VirtualAllocEx(hCurrentProcess,NULL,cb,
MEM_COMMIT,PAGE_READWRITE);
SIZE_T nBytes = 0;
WriteProcessMemory(hCurrentProcess,
pszLibFileRemote,(PVOID) pszLibFile,cb,&nBytes );
LPTHREAD_START_ROUTINE pfnThreadRtn = ( LPTHREAD_START_ROUTINE )
GetProcAddress(GetModuleHandle(TEXT("Kernel32")),
"LoadLibraryW");
CreateRemoteThread(hCurrentProcess,NULL,0,
pfnThreadRtn,pszLibFileRemote,0,NULL);
if (pszLibFileRemote != NULL)
{
VirtualFreeEx(hCurrentProcess,pszLibFileRemote,0,MEM_RELEASE);
}
if (hRunningProcess != NULL)
{
CloseHandle(hRunningProcess );
}
}
}
When HookAPI.dll hooks the CreateProcess()
, OpenProcess()
, CreateFile()
, CloseHandle()
, and WriteFile()
functions of all the running processes, we get control in our wrapper functions for almost all the file operations done on the system. CreateProcess()
and OpenProcess()
are hooked to capture the creation of any new process through a running process, and then HookAPI.dll is again injected into the newly created process via the wrapper function provided for CreateProcess()
and OpenProcess()
, respectively. This is done so that the DLL gets injected in each newly created process, before any new process starts. The CreateFile()
, CloseHandle()
, and WriteFile()
functions are hooked in order to sniff the file open, close, and write operations, respectively, in any running process as well as in newly created processes. The wrapper functions provided for the hooked functions such as CreateFile()
, CloseHandle()
, and WriteFile()
may then be modified for preprocessing the file operations as per the requirement.
Issues with the first run of this utility
Initially, all the files on the system are neither in encrypted form nor in decrypted form. This utility requires that all the concerned files (files belonging to a particular subset of file types) on the system should be in encrypted form before they are opened. Hence, this encryption should be done at the time of installation of this utility. One way to do this, in case a particular noise pattern is defined for corrupting the file headers, is to check whether this noise pattern exists in the file. If yes, remove the noise, i.e., decrypt the file and proceed in the fashion mentioned above. If no, it shows that this is the first run and no preprocessing is needed. Another approach is to find all the files on the system belonging to a particular subset of file types the utility is intended to process, and encrypt them all at the time of installing this utility.
References