DLL注入實踐

Windows系統大量使用dll做爲組件複用,應用程序也會經過dll實現功能模塊的拆分。DLL注入技術是向一個正在運行的進程插入自有DLL的過程。安全

Window下的代碼注入

常見的Windows代碼注入方法以下:函數

  • 註冊表注入
    編譯註冊表中的AppInit_DLLs選項,凡是使用GUI的進程,都會讀取AppInit_DLLs內容,加載這些Dll。工具

  • Windows Hook注入
    使用 SetWindowsHookEx、UnHkkkWindowsHookEx 來進行,爲目標進程安裝鉤子,在注入dll中監聽目標進程消息。編碼

  • 遠程線程注入.net

    使用 CreateRemoteThread 函數在目標進程中建立線程,在該線程中加載注入dll。線程

  • DLL函數轉發調試

    使用僞造的dll來替換目標dll,兩個dll的導出符號徹底相同,在自定義DLL中,先利用函數轉發器將請求轉發到真實dll中,而後進行本身的一些處理。code

在本篇文章中,主要介紹 Windows Hook注入 這一種方式。在具體介紹以前,先介紹下Dll的加載順序、加載過程。blog

DLL加載順序

系統在搜索加載指定DLL以前,按照以下順序作檢查:遞歸

  1. 若是同名DLL已在內存中加載,則直接使用
  2. 若是DLL在系統DLL列表中,系統直接使用已知DLL的拷貝
  3. 若是DLL依賴其餘DLL,系統會按照名字搜索並加載依賴的DLL,待依賴的DLL加載完畢後再加載自身。

系統已知DLL列表配置位於註冊表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\KnownDLLs選項中,本機上的內容以下:

系統DLL列表.jpg

標準的DLL搜索順序取決於系統安全DLL搜索模式,該模式默認使能。按照以下順序搜索DLL:

  1. 進程對應的應用程序目錄,可經過 GetModuleFileName 得到,程序啓動後爲固定值。
  2. Windows系統目錄,通常爲 C:\Windows\system32
  3. Windows目錄,通常爲 C:\Windows
  4. 當前進程目錄,經過 GetCurrentDirectory 得到,程序可經過 SetCurrentDirectory 進行修改。
  5. PATH環境變量中的目錄

DLL加載過程

DLL加載分爲隱式加載和顯示加載。

隱式加載既爲在編譯連接選項中增長導入庫,在程序運行目錄存放待加載的dll。雙擊程序啓動時,由系統加載程序根據exe中的導入表加載對應dll到進程空間,若dll有依賴其餘dll的,會遞歸加載直到加載完成全部必需的dll,而後進行exe的導入函數地址重定位,使得可以調用dll的導出函數。

顯示加載指的是由應用程序按需加載,具體爲調用 LoadLibraryFreeLibraryGetProcAddress這三個API函數,加載並獲取dll的導出函數地址。

加載器執行流程

  1. 爲進程建立虛擬地址空間
  2. 把可執行模塊映射到進程地址空間中
  3. 檢查可執行模塊的導入段,根據DLL搜索規則,找到所需的DLL並加載。如在此階段,未找到須要的dll,彈出"沒法啓動,由於計算機中缺失XXX.dll"
  4. 檢查dll的導入表(IAT),若是該dll還依賴其餘的dll,那麼繼續去定位所需的dll並加載.
  5. 修復導入符號的引用符號。具體作法,遍歷全部模塊的導入段,針對每一個導入符號,加載程序在導出段中檢查是否存在匹配的導入符號,若存在,則取導出符號的RVA,加上對應dll的基址,獲得導出符號的真實地址,填入對應的導入表中。如在此階段未找到對應的導出符號,彈出"程序入口點XXX沒法定位到動態連接庫xxx.dll上"
  6. 可執行模塊運行期間若是調用到某個dll的導出函數,則會跳轉到IAT,獲得導出函數的地址,而後進行調用。

使用CreateRemoteThread注入DLL

使用CreateRemoteThread可以使得目前進程建立線程,但要加載注入dll,須要在目標線程的虛擬地址空間申請內存,用於保存目標dll的名稱,經過 GetProcAddress 函數在遠程線程中 kernel32.dll 模塊的LoadLibrary函數地址,用於遠程線程的入口函數,主要流程以下:

  1. 獲取 LoadLibrary 函數的地址
  2. 調用 VirtualAllocEx 函數在遠程進程中申請一段虛擬內存
  3. 調用 WriteProcessMemory 將 待加載的dll名稱寫入虛擬內存 ReadProcessMemory 讀寫進程地址內容
  4. 調用 CreateRemoteThread 建立遠程線程,回調函數爲 LoadLibrary,參數爲對應字符串地址
  5. 調用 VirtualFreeEx 釋放遠程虛擬內存

執行完本身的函數後,就要遠程卸載dll,思路與注入相似,函數變爲FreeLibrary,傳入參數對對應dll的句柄。
獲取已加載的模塊句柄(經過EnumProcessModule實現)

注入代碼示例:

// 得到指定進程名稱的進程PID
int GetProcessId(const char* pName)
{
    PROCESSENTRY32 pe;
    DWORD id = 0;
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    pe.dwSize = sizeof(PROCESSENTRY32);
    if (!Process32First(hSnapshot, &pe))
        return 0;

    while (1)
    {
        pe.dwSize = sizeof(PROCESSENTRY32);
        if (Process32Next(hSnapshot, &pe) == FALSE)
            break;

        if (strcmp(pe.szExeFile, pName) == 0)
        {
            id = pe.th32ProcessID;
            break;
        }
    }

    CloseHandle(hSnapshot);
    return id;
}

//利用遠程線程來注入dll
bool RemoteThreadDllInject()
{
    ////提權代碼,在Windows Vista 及以上的版本須要將進程的權限提高,不然打開進程會失敗
    if (!SetDebugPrivilege(TRUE))
    {
        printf("提高權限失敗\n");
        return false;
    }

    int nPid = GetProcessId(INJECT_EXE_NAME);

    // 打開目標進程
    HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
    if (NULL == hRemoteProcess)
    {
        printf("OpenProcess failed! %d", GetLastError());
        return false;
    }
    
    // 得到 LoadLibraryW 在 kernel32.dll 中的地址
    typedef HMODULE(WINAPI *pfnLoadLibrary)(LPCWSTR);
    // 這裏注意要載入寬字節版本仍是普通版本的 LoadLibrary  
    pfnLoadLibrary pfnThreadRtn2 = (pfnLoadLibrary)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "LoadLibraryA");
    // 在遠程進程中申請內存空間,用於保存遠程線程的參數
    LPVOID lpRemoteMemory = VirtualAllocEx(hRemoteProcess, 0, MAX_PATH, MEM_COMMIT, PAGE_READWRITE);

    string strInjectDllName(INJECT_DLL_NAME);
    DWORD nWritten = 0;
    BOOL bRet = WriteProcessMemory(hRemoteProcess, lpRemoteMemory, strInjectDllName.c_str(), strInjectDllName.length() + 1, &nWritten);

    HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pfnThreadRtn2, lpRemoteMemory, 0, NULL);
    WaitForSingleObject(hRemoteThread, INFINITE);
    VirtualFreeEx(hRemoteProcess, lpRemoteMemory, 0, MEM_RELEASE);

    CloseHandle(hRemoteThread);
    CloseHandle(hRemoteProcess);

    return true;
}

遠程卸載dll示例代碼:

// 得到指定進程內指定模塊信息
bool GetProcessModule(DWORD dwPid, string strModuleName, LPMODULEENTRY32 lpMe32, DWORD cbMe32)
{
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
    if (INVALID_HANDLE_VALUE == hSnapshot)
    {
        printf("CreateToolhelp32Snapshot Error");
        return false;
    }

    bool bFind = false;
    MODULEENTRY32 moduleInfo = {0};
    moduleInfo.dwSize = sizeof(MODULEENTRY32);
    if (Module32First(hSnapshot, &moduleInfo))
    {
        do 
        {
            if (strModuleName == string(moduleInfo.szModule))
            {
                memcpy(lpMe32, &moduleInfo, cbMe32);
                bFind = true;
                break;
            }
        } while (!bFind && Module32Next(hSnapshot, &moduleInfo));
    }
    
    CloseHandle(hSnapshot);

    return bFind;
}


bool RemoteDllUnLoad()
{
    //在遠程線程執行結束後,注入的 dll 仍然存在與目標進程中,咱們須要再次使用 CreateRemoteThread,執行 FreeLibrary ,將以前注入的 dll 卸載掉
    // 實現思路:枚舉進程的模塊,根據模塊名稱找到對應模塊的句柄。
    int nPid = GetProcessId(INJECT_EXE_NAME);

    ////提權代碼,在Windows Vista 及以上的版本須要將進程的權限提高,不然打開進程會失敗
    if (!SetDebugPrivilege(TRUE))
    {
        printf("提高權限失敗\n");
        return false;
    }

    MODULEENTRY32 moduleInfo = {0};
    if (!GetProcessModule(nPid, INJECT_DLL_NAME, &moduleInfo, sizeof(moduleInfo)))
    {
        printf("can't find %s dll in %s", INJECT_DLL_NAME, INJECT_EXE_NAME);
        return false;
    }
    
    typedef BOOL(*pfnFreeLibrary)(HMODULE);
    pfnFreeLibrary pFreeLibrary = (pfnFreeLibrary)GetProcAddress(GetModuleHandle("kernel32.dll"), "FreeLibrary");

    HANDLE hRemoteProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, nPid);
    if (hRemoteProcess == NULL)
    {
        printf("OpenProcess Error");
        return false;
    }

    HANDLE hRemoteThread = CreateRemoteThread(hRemoteProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFreeLibrary, moduleInfo.hModule, 0, NULL);
    WaitForSingleObject(hRemoteThread, INFINITE);

    CloseHandle(hRemoteThread);
    CloseHandle(hRemoteProcess);
    return true;
}

注意事項

  • 遠程進程可能以Unicode編碼編譯,也多是MBSC編碼編譯,所以,要區分 LoadLibraryW 仍是 LoadLibraryA 版的函數,FreeLibrary函數無需區分不一樣版本。
  • 在得到目標進程的句柄時,須要對自身程序進程提權操做。通常須要當前登錄用戶屬於Administrator組成員,纔有能力提高至調試權限。Administrator組成員的access token中會含有一些能夠執行系統級操做,注意,非Administrator組成員建立的進程 沒法提高自身的權限。若是提權失敗,建議經過管理權權限打開VS開發環境。
  • 在注入時,須要將待注入的dll放在遠程進程同一目錄下。
  • 驗證注入,可經過在dllMain函數的加載和卸載分支中彈框提示,也能夠經過 procexp64.exe 程序來查看程序當前加載的dll信息。

工程實踐

如何得到運行過程當中不符合預期的dll

  1. 經過讀取exe的PE頭,得到依賴的dll列表,遍歷讀取dll依賴的dll,獲得集合1
  2. 在程序運行過程當中,調用 EnumProcessModules 獲得當前加載全部dll,獲得集合2
  3. 從集合2中排除掉集合1的內容,獲得生成集合3,即爲不符合預期的dll。

通常來講,輸入法(搜狗輸入法的皮膚組件PicFace.dll、資源組件Resource.dll等)、監控軟件的dll會自動加載到全部進程的dll中去。

模塊基址重定位

每一個模塊(exe和dll)在編譯輸出時,都有一個首選基址,它指示加載器將模塊映射到進程地址空間中的首選位置,通常exe的基址設定爲 0x00400000,dll模塊爲 0x10000000.當一個exe依賴於多個dll時,第一個dll被正確的加載到 0x10000000上,隨後的dll就不能在加載到0x10000000上,加載程序會對隨後的dll進行基址重定位,把它放到別的地方。基址重定位會增長程序初始化時間,所以,若是將多個模塊載入同一進程空間,能夠給不一樣模塊指定不一樣的基址。在VS開發環境中,基址配置方式:DLL工程的配置屬性-->連接器-->高級-->基址。

在全部dll編譯完成後,使用 Rebase.exe 工具,對須要載入進程地址空間的全部模塊進行基址重定位,將結果寫回到dll文件中。

一旦一個dll模塊的基址已知,那麼可直接推算出exe在使用該DLL的導出函數的真實地址。這種預先將exe和所依賴的dll綁定在一塊兒的作法,能夠提升應用程序的啓動速度。

VS提供 Bind.exe程序提供了綁定可執行文件與dll的功能。工做原理爲,讀取全部dll的基址和導出符號的RVA,計算後填充到可執行文件的導入表中。

參考文檔:多種DLL注入技術原理介紹

相關文章
相關標籤/搜索