在Ring3上實現文件碎甲(解鎖)功能

一.概述:
如果一個病毒文件被植入正在運行的進程中,我們想要清除它時系統總會提供無法刪除;有時編輯文件的進程被意外中止而文件句柄沒有 正確釋放,導致此文件無法進行改寫操作。現在我們會使用Unlocker之類的小工具去解鎖,但在編寫程序的可能會需要把這些功能包含在自己的代碼中,本 文就是自己寫代碼實現”如何關閉已經被加載的DLL或是正在使用的文件”功能,使用文章中的方法能很方便的完成文件解鎖功能。
按最初的想法準備在ring0中完成這些功能,但在查找資料的過程中發現既然我們能在ring3中做,爲什麼不讓這些方法更通用一些呢。其實功能實現並不難,主要是前期從哪裏入手比較麻煩。
我們知道無論是動態庫或是文件在加載到進程中時,總會有一個指向它的指針,如果讓進程釋放這段指針,那麼這些文件就不會被系統鎖定。下面將是我們的需要實現文件解鎖功能而分解出的步驟
1.    枚舉當前系統所有進程;
2.    查找進程中打開的文件句柄和加載的動態庫句柄;
3.    通知進程關閉這些句柄。
二.詳細設計
2.1查找進程加模的動態庫模塊
Let’s go,我們來分步完成它吧。對於枚舉當前系統所有進程,在這裏就不給出代碼了,相信實現的方法很多。下面的代碼段完成查找指定進程加載動庫信息的功能(在 這裏使用了Jeffrey在《Windows核心編程》一書中提供的CToolhelp類,用它可以完成進程加載信息的分析功能,感謝Jeffrey,我 一直在使用它)

// 自定義結構,保存打開句柄的的信息
typedef struct _UNFILE_INFO    {
    int nFileType;
    DWORD dwHandle;
    char *strFileName;
} UNFILE_INFO, *PUNFILE_INFO;
//////////////////////////////////////////////////////////////////////////
// 通過PID號取得PID打開的文件句柄信息
//////////////////////////////////////////////////////////////////////////
void
GetModules(DWORD dwProcessID, CList<PUNFILE_INFO, PUNFILE_INFO> &plsUnFileInfo)
{
    CToolhelp::EnableDebugPrivilege(TRUE);
    CToolhelp th(TH32CS_SNAPALL, dwProcessID);
    // 顯示進程的詳細資料
    MODULEENTRY32 me = { sizeof(me) };
    BOOL fOk = th.ModuleFirst(&me);
    for (; fOk; fOk = th.ModuleNext(&me))
    {
        PVOID pvPreferredBaseAddr = NULL;
        pvPreferredBaseAddr =GetModulePreferredBaseAddr(dwProcessID, me.modBaseAddr);
        // 取得進程模塊信息
        PUNFILE_INFO pUnFileInfo = new UNFILE_INFO;
        // 模塊地址
        pUnFileInfo->dwHandle = (DWORD)me.modBaseAddr;
        // 模塊類型
        pUnFileInfo->nFileType = UNTYPE_DLL;
        // 模塊名稱
        pUnFileInfo->strFileName = new char[strlen(me.szExePath)+1];
        memset( pUnFileInfo->strFileName, 0, strlen(me.szExePath)+1);
        strcpy( pUnFileInfo->strFileName, me.szExePath);
        // 保存打開的模塊信息
        plsUnFileInfo.AddTail( pUnFileInfo);
    }
}

上面功能完成了枚舉進程加載的模塊功能,我們把得到的枚舉信息加入了鏈表中,以備後面使用。
2.2枚舉進程打開的文件信息
下面將分段說明如何枚舉指定進程打開的文件句柄。在這裏我們需要使用兩個DDK中提供的函數:
NTSTATUS
ZwQuerySystemInformation(
    IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
    OUT PVOID SystemInformation,
    IN ULONG SystemInformationLength,
    OUT PULONG ReturnLength OPTIONAL
);
NTSTATUS
  ZwQueryInformationFile(
    IN HANDLE  FileHandle,
    OUT PIO_STATUS_BLOCK  IoStatusBlock,
    OUT PVOID  FileInformation,
    IN ULONG  Length,
    IN FILE_INFORMATION_CLASS  FileInformationClass
);
ZwQuerySystemInformation 是個未公開函數,通過它的SYSTEM_INFORMATION_CLASS結構,我們能完成許多進程和線程的操作,下面是它的部分內容,在這時我們使用 它的SystemHandleInformation(0x10),來完成文件句柄的操作。
typedef enum _SYSTEM_INFORMATION_CLASS {
    SystemBasicInformation,                 // 0
    SystemProcessorInformation,             // 1
    SystemPerformanceInformation,             // 2

    SystemHandleInformation,                 // 16

    SystemSessionProcessesInformation        // 53
} SYSTEM_INFORMATION_CLASS;
ZwQueryInformationFile 可以根據參數FileInformationClass的不同值來返回不同的類型,在這裏我們使用 FileInformationClass=FileInformationClass來得到FILE_NAME_INFORMATION,從而得到文件 句柄指向的文件名,下面是它的結構定義。
typedef struct _FILE_NAME_INFORMATION {
  ULONG  FileNameLength;
  WCHAR  FileName[1];
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;

因爲需要在Ring3上使用DDK提供的函數,我們需要導出這些兩個函數,下面是導出函數的示例代碼:
{
    g_hNtDLL = LoadLibrary( "ntdll.dll" );
    if ( !g_hNtDLL )
    {
        return FALSE;
    }

    ZwQuerySystemInformation =
    (ZWQUERYSYSTEMINFORMATION)GetProcAddress( g_hNtDLL, "ZwQuerySystemInformation");
    if( ZwQuerySystemInformation == NULL)
    {
        return FALSE;
    }

    ZwQueryInformationFile =
    (ZWQUERYINFORMATIONFILE)GetProcAddress( g_hNtDLL, "ZwQueryInformationFile");
    if( ZwQueryInformationFile == NULL)
    {
        return FALSE;
    }
}
我們先來使用ZwQuerySystemInformation函數枚舉系統中打開的所有文件的句柄,枚舉出的信息將包含如下結構:
typedef struct _SYSTEM_HANDLE_INFORMATION {
    ULONG  ProcessId;
    UCHAR  ObjectTypeNumber;
    UCHAR  Flags;
    USHORT  Handle;
    PVOID  Object;
    ACCESS_MASK  GrantedAccess;
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
ObjectTypeNumber 定義了句柄所屬對像的類型,文件的ObjectType對不同的操作系統是不同的,所以需要先找到當前操作系統所定義的ObjectType值,這樣才能 在枚舉出的衆多句柄信息中找到哪些是文件句柄信息。通用的方法是我們CreateFile打開空設備NUL,記下它的句柄,用來比較。
UC
HAR GetFileHandleType()
{
  HANDLE                     hFile;
  PSYSTEM_HANDLE_INFORMATION Info;
  ULONG                      r;
  UCHAR                      Result = 0;

  hFile = CreateFile("NUL", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);

  if (hFile != INVALID_HANDLE_VALUE)
  {
    Info = GetInfoTable(SystemHandleInformation);

    if (Info)
    {
      for (r = 0; r < Info->uCount; r++)
      {
        if (Info->aSH[r].Handle == (USHORT)hFile &&
                                    Info->aSH[r].uIdProcess == GetCurrentProcessId())
        {
          Result = Info->aSH[r].ObjectType;
          break;
        }
      }
      HeapFree(hHeap, 0, Info);
    }
    CloseHandle(hFile);
  }
  return Result;
}
現在知道了句柄的類型就可以枚舉系統中打開的文件了。首先用句柄獲取打開的文件名:
typedef struct _NM_INFO
{
  HANDLE  hFile;
  FILE_NAME_INFORMATION Info;
  WCHAR Name[MAX_PATH];
} NM_INFO, *PNM_INFO;

// 因爲在在線程中取得文件名
DWORD WINAPI
  GetFileNameThread(PVOID lpParameter)

{
  PNM_INFO        NmInfo = lpParameter;
  IO_STATUS_BLOCK IoStatus;
  int r;

  NtQueryInformationFile(NmInfo->hFile, &IoStatus, &NmInfo->Info,
                       sizeof(NM_INFO) - sizeof(HANDLE), FileNameInformation);

  return 0;
}

void CFileProcess::GetFileName(HANDLE hFile, PCHAR TheName)
{

    HANDLE   hThread;
    PNM_INFO Info = new NM_INFO;
    char namebuf[2000] = {0};
    Info->Info = (PFILE_NAME_INFORMATION)namebuf;

    Info->hFile = hFile;
    Info->nRet = 0x00;
   
    // 在對打開的句柄調用ZwQueryInformationFile時,調用線程會等待PIPE中的消息
    // 如果PIPE中沒有消息時,線程可能會永久掛起,所以使用一個等待超時來打開句柄
    hThread = CreateThread(NULL, 0, GetFileNameThread, Info, 0, NULL);
    if (WaitForSingleObject(hThread, 100) == WAIT_TIMEOUT)
        TerminateThread(hThread, 0);
    CloseHandle(hThread);
    // 調用句柄失敗
    if( Info->nRet == 0x00)
    {
        delete Info;
        return ;
    }
   
    // 加上盤符
    wchar_t volume = GetVolumeName(hFile);
    if (volume != L'!')
    {
        wchar_t outstr[1000] = L"A:";
        outstr[0] = volume;
        memcpy(&outstr[2], Info->Info->FileName, Info->Info->FileNameLength);
        outstr[2+Info->Info->FileNameLength] = 0;
        WideCharToMultiByte(CP_ACP, 0, outstr, 2+Info->Info->FileNameLength, TheName, MAX_PATH, NULL, NULL);
    }
    delete Info;
    return ;
}

下面代碼片段用來枚舉打開的文件:
{
    // 取得操作系統文件句柄定義類型
    UCHAR ObFileType = GetFileHandleType();
    // 取得系統中打開的文件句柄列表
    PULONG buf = GetHandleList();
    if (buf == NULL)
        return ;
    Info = (PSYSTEM_HANDLE_INFORMATION)&buf[1];
    if (Info)
    {
        for (r = 0; r < buf[0]; r++, Info++)
        {
            if (Info->ObjectTypeNumber == ObFileType && Info->ProcessId == dwProcessID)
            {
                hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, Info->ProcessId);

                if (hProcess)
                {
                    // 複製句柄到當前進程中,這樣方便對當前進程句柄取出文件名
                    // 因爲共享一個FileObject,這兩個文件句柄對像將由兩個進程共享
                    if ( DuplicateHandle( hProcess, (HANDLE)Info->Handle,
                    GetCurrentProcess(), &hFile, 0, FALSE, DUPLICATE_SAME_ACCESS))
                    {
                        GetFileName(hFile, Name);
                        // 取得文件信息到鏈表中
                        if( strlen( Name)>0)
                        {
                            AfxTrace("PID=%d FileHandle %d FileName=%s", Info->ProcessId, Info->Handle, Name);
                            // 在這裏加入你要保存的文件句柄信息
                        }

                        CloseHandle(hFile);
                    }
                    CloseHandle(hProcess);
                }
            }
        }
    }
}
2.3使用遠線程方法關閉進程加載的模塊和打開的文件
通過枚舉進程加載的模塊信息和打開的文件信息,我們已經得到系統中加載的DLL和文件名信息,以下是我們在鏈表中保存的信息:
{
DWORD    ProcessID    // 進程PID
BYTE    nHandleType    // 句柄類型是文件還是加載的DLL
Char strFileName[MAX_PATH]    // 文件名
}
如果我們需要關閉正在打開的文件,只需要在這個鏈表中以文件名進行查找,即得到打開文件的進程PID。怎麼才能通知別的進程關閉相應的句柄呢,我們通過跨進程去關閉句柄是很困難的,不如直接注入代碼到需要的進程中,然後直接關閉句柄就行了。
需要再遍寫一個DLL用來注入到進程中通知進程關閉文件句柄。這個DLL不需要做別的處理,只需要在加載的地方根據不同的句柄類型使用不同的方法進行關閉即可。下面是注入到進程的DLL執行的關閉句柄函數:
Void CloseHandel( PVOID pHandle, BYTE nHandleType)
{
    If(HandleType = HANDLE_FILE)
FreeLibrary((HMODULE)pHandle);
    else
        CloseHandle((HANDLE)pHandle);
}
在DllMain中調用關閉句柄的函數:
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
    // Remove this if you use lpReserved
    UNREFERENCED_PARAMETER(lpReserved);

    if (dwReason == DLL_PROCESS_ATTACH)
    {
        DWORD nHandle=0;
        BYTE nType=0;
        TRACE0("MOPER.DLL Initializing!/n");
        AfxMessageBox("MOPER.DLL Initializing!");
        // 從碎甲程序所生成的文件句柄映射文件中取出要關閉的句柄信息
        // 方法很多我在這裏直接用共享文件實現的,就不寫出代碼了
        GetHandle( nHandle, nType)
        // 關閉句柄           
        CloseHandel((PVOID)nHandle, nType);
...
    }
...
}
下面示例取出通過文件句柄鏈表中的PID,使用遠程注入的方法向進程注入關閉句柄的DLL
{
    HANDLE hRemoteProcess = OpenProcess(PROCESS_CREATE_THREAD | //允許遠程創建線程
            PROCESS_VM_OPERATION | //允許遠程VM操作
            PROCESS_VM_WRITE,//允許遠程VM寫
            FALSE, dwProcessId);
    if( hRemoteProcess == NULL)
        return FALSE;

    PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");
    int nLen = (strlen( pszLibFile)+1)*sizeof(char);
    char * pszLibFileRemote = (char *) VirtualAllocEx( hRemoteProcess, NULL, nLen, MEM_COMMIT, PAGE_READWRITE);
    if( pszLibFileRemote != NULL &&
        pfnStartAddr !=NULL)
    {
        if( WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (PVOID) pszLibFile, nLen, NULL))
        {
            HANDLE hThread = CreateRemoteThread( hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL);
            if( !hThread)
            {
                WaitForSingleObject(hThread, INFINITE);
                bRet = TRUE;
                CloseHandle(hThread);
            }
        }
        VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE);
    }
    CloseHandle( hRemoteProcess);
}
三.備註
對查找文件句柄的方法,參考了鄙人拙譯(http://greatdong.blog.edu.cn )的“如何操作被佔用文件”在此表示感謝。
使用了Jeffrey在《Windows核心編程》一書中提供的CToolhelp類,用它可以完成進程加載信息的分析功能,感謝Jeffrey。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章