65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Windows API Hooking 的 Windows 文件监控系统

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.25/5 (14投票s)

2008年10月30日

CPOL

6分钟阅读

viewsIcon

118542

一个Windows文件监控系统。

引言

这个Windows文件监控系统旨在为Windows环境中的文件提供安全性。我需要设计一个应用程序,用于监控Windows上的文件打开、关闭和保存操作,并在安装此实用程序之前限制用户访问部分文件类型。这是通过挂钩Windows文件相关的API,然后根据需求预处理Windows上的文件打开、保存和关闭操作来实现的。预处理可能包括文件加密、损坏文件头部分等。如果此实用程序安装在系统中,则文件最初将以加密格式保存,在打开之前会先解密,然后在文件关闭或保存时会再次加密。这将有助于防止文件在未安装此实用程序的任何其他地方被访问,从而提供文件安全性。

我发现了一些很好的应用程序,如AvaFind、FileMon等,它们执行文件监控功能。但是,这些应用程序使用系统驱动程序来实现其目标。由于编写驱动程序程序的复杂性,我试图通过Win32用户级编程来实现。我通过挂钩kernel32.dllCreateProcess()OpenProcess()CreateFile()CloseHandle()WriteFile()函数来实现这一点。

在挂钩Windows API的各种方法中,我选择了“通过修改系统中所有正在运行进程的导入地址表 (IAT)”的挂钩方法。包含更改IAT代码的DLL通过使用Windows API CreateRemoteThread()在目标进程(要注入DLL的进程)的地址空间中创建远程线程,从而被注入到目标进程的地址空间中。

通过远程线程注入DLL在Jeffrey Richter的文章“使用INJLIB将32位DLL加载到另一个进程的地址空间”中有详细记载。DLL注入是迄今为止最广泛使用的一种概念,其最大的优势在于一旦DLL进入进程的地址空间,就可以完全控制该进程。但是,这种方法有一个缺点,即如果kernel32.dll没有加载到其众所周知的首选加载地址,则DLL将不会被注入到目标进程中。因此,DLL注入方法基于这样一个假设:Kernel32.dll在两个进程中的加载地址是相同的。

设计与实现

回到最初的问题……

我执行的第一步是创建HookAPI.dll,它包含挂钩Windows API的代码,然后将此DLL注入到系统中所有正在运行的进程中。一旦注入到目标进程中,HookAPI.dll就会更改进程及其所有已加载模块的IAT。HookAPI.dll包含一个名为GetIAList()的函数,该函数遍历注入的进程的IAT。它使用EnumProcessModules()获取注入进程的所有模块列表。之后,它检查要挂钩哪个函数,并将其在IAT中的地址替换为为该API提供的包装函数的地址。GetNewAddress()函数遍历指定要挂钩函数的列表,并返回为要挂钩的函数提供的包装函数的地址。

以下是替换周期的逻辑步骤

  • 定位每个进程加载的DLL模块以及进程本身的IAT中的导入部分。
  • 查找导出该函数的DLL的IMAGE_IMPORT_DESCRIPTOR块。
  • 定位保存导入函数原始地址的IMAGE_THUNK_DATA
  • 将函数地址替换为包装函数的地址。
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];
            // Get the full path to the module's file.
            if ( GetModuleFileNameEx( hProcess, hMods[i], 
                                      szModName, sizeof(szModName)))
            {
                // We must skip the IAT of HookAPI.dll
                // from being modified as it contains
                // the wrapper functions for Windows AOIs being hooked.
                if(_tcscmp(szModName,szLibFile) == 0)
                {
                    i++;
                }
            }
            // Get the address of the module's import section
            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;
                    // Get caller's IAT 
                    PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)
                                ( (PBYTE) hMods[i] + pImportDesc->FirstThunk );
                    while (pThunk->u1.AddressOfData)
                    {
                        // Get the address of the function address
                        ppfn = (PROC*) &pThunk->u1.AddressOfData;
                        str.Format(_T("%s:%x"),strModName,*ppfn);
                        // Store the dll name and address of the function from the IAT
                        // into a map in the form ("KERNEL32.dll:<address of CreateFile>",
                        // <address that contains the address of CreateFile in IAT>).
                        // The map contains the entries in the form
                        // ("KERNEL32.dll:0x110023",0x707462)
                        // ("KERNEL32.dll:0x110045",0x707234)
                        // ("KERNEL32.dll:0x110074",0x402462)
                        // ...
                        m_mapOld.insert( func_Pair( str, ppfn ) );
                        pThunk++;
                    }
                    pImportDesc++;
                }
            }
        }
    }
        
    // Traverse the map to hook the appropriate function.
    for(m_AcIter = m_mapOld.begin() ; m_AcIter != m_mapOld.end() ; m_AcIter++)
    {
        // pfnNewAddress = GetNewAddress(m_AcIter -> first);
        // m_AcIter -> first is a string that gives all the informatino 
        // about the Windows API to be hooked like the name of the DLL 
        // in which the function is actually present, its address in that DLL 
        // and its address in IAT. 
        // GetNewAddress should be implemented in such a way that it should return 
        // the address of the wrapper function for Windows API implemented in HookAPI.dll
        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);
            
            // Replace the origional address of API with the address of corresponding 
            // wrapper function 
            *pfnOldAddress = *pfnNewAddress;

            DWORD dwOldProtect = 0;
            VirtualProtect( mbi.BaseAddress, mbi.RegionSize, mbi.Protect, &dwOldProtect );
  
        }
    }
}

一旦设计好这个DLL,下一步是创建Injector.exe,它使用CreateRemoteThread()将挂钩DLLHookAPI.dll注入到所有正在运行的进程中,如下所示。InjectIntoExistingProcesses()函数将DLL注入到所有正在运行的进程中。它使用EnumProcesses()获取所有正在运行的进程列表。这里,pszLibFile包含要注入的DLL的路径,也就是HookAPI.dll的路径。

// Get the path of DLL to be injected
TCHAR pszLibFile[MAX_PATH];
GetModuleFileName(NULL,pszLibFile,sizeof(szLibFile));
_tcscpy(_tcsrchr(pszLibFile,TEXT('\\')) + 1,TEXT("HookAPI.dll"));

VirtualAllocEx()用于在远程进程的地址空间中分配内存,以便加载DLL。WriteProcessMemory()用于将DLL路径写入分配的内存空间。GetProcAddress()给出LoadLibrary() API的地址(假设kernel32.dll在所有进程中的加载地址都相同),然后CreateRemoteThread()最终在远程进程中创建线程并将DLL加载到远程进程的地址空间中。

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);

    // Get process handle for each running process. 
    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 );
          }    
    }
}

HookAPI.dll挂钩所有正在运行进程的CreateProcess()OpenProcess()CreateFile()CloseHandle()WriteFile()函数时,我们将几乎所有系统上的文件操作都控制在我们的包装函数中。CreateProcess()OpenProcess()被挂钩以捕获通过运行进程创建的任何新进程,然后HookAPI.dll通过为CreateProcess()OpenProcess()提供的包装函数再次被注入到新创建的进程中。这样做是为了在任何新进程启动之前,DLL都能被注入到每个新创建的进程中。CreateFile()CloseHandle()WriteFile()函数被挂钩,以分别嗅探任何正在运行进程以及新创建进程中的文件打开、关闭和写入操作。可以根据需要修改为被挂钩函数(如CreateFile()CloseHandle()WriteFile())提供的包装函数,以进行文件操作的预处理。

首次运行此实用程序时遇到的问题

最初,系统上的所有文件都处于未加密或未解密状态。此实用程序要求系统上所有相关文件(属于特定文件类型子集的文件)在打开前都应处于加密状态。因此,此加密应在安装此实用程序时完成。一种方法是,如果为损坏文件头定义了特定的噪声模式,则检查文件中是否存在此噪声模式。如果存在,则移除噪声,即解密文件,并按上述方式继续。如果不存在,则表明是首次运行,无需预处理。另一种方法是找到系统中属于该实用程序旨在处理的特定文件类型子集的所有文件,并在安装此实用程序时全部加密。

参考文献

© . All rights reserved.