淺談程序隱藏技術

蚁景网安实验室發表於2024-07-07

前言

在之前幾篇文章已經學習瞭解了幾種鉤取的方法

● 淺談除錯模式鉤取

● 淺談熱補丁

● 淺談內聯鉤取原理與實現

● 匯入地址表鉤取技術

這篇文章就利用鉤取方式完成程序隱藏的效果。

程序遍歷方法

在實現程序隱藏時,首先需要明確遍歷程序的方法。

CreateToolhelp32Snapshot

CreateToolhelp32Snapshot函式用於建立程序的映象,當第二個引數為0時則是建立所有程序的映象,那麼就可以達到遍歷所有程序的效果。

#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
​
int main()
{
    //設定編碼,便於後面能夠輸出中文
    setlocale(LC_ALL, "zh_CN.UTF-8");
    //建立程序映象,引數0代表建立所有程序的映象
    HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnapshot == INVALID_HANDLE_VALUE)
    {
        std::cout << "Create Error" << std::endl;
        exit(-1);
    }
​
    /*
    * typedef struct tagPROCESSENTRY32 { 
    * DWORD dwSize;               程序資訊結構體大小,首次呼叫之前必須初始化
    * DWORD cntUsage;              引用程序的次數,引用次數為0時,則程序結束
    * DWORD th32ProcessID;           程序的ID
    * ULONG_PTR th32DefaultHeapID;       程序預設堆的識別符號,除工具使用對我們沒用
    * DWORD th32ModuleID;                  程序模組的識別符號
    * DWORD cntThreads;             程序啟動的執行執行緒數
    * DWORD th32ParentProcessID;           父程序ID
    * LONG  pcPriClassBase;          程序執行緒的基本優先順序
    * DWORD dwFlags;              保留
    * TCHAR szExeFile[MAX_PATH];          程序的路徑
    * } PROCESSENTRY32; 
    * typedef PROCESSENTRY32 *PPROCESSENTRY32; 
    */
    PROCESSENTRY32 pi;
    pi.dwSize = sizeof(PROCESSENTRY32);
    //取出第一個程序
    BOOL bRet = Process32First(hSnapshot, &pi);
    while (bRet)
    {
        wprintf(L"程序路徑:%s\t程序號:%d\n", pi.szExeFile, pi.th32ProcessID);
        //取出下一個程序
        bRet = Process32Next(hSnapshot, &pi);
    }
}
​

EnumProcesses

EnumProcesses用於將所有程序號的收集。

#include <iostream>
#include <Windows.h>
#include <Psapi.h>
​
int main()
{
    setlocale(LC_ALL, "zh_CN.UTF-8");
​
    DWORD processes[1024], dwResult, size;
    unsigned int i;
    //收集所有程序的程序號
    if (!EnumProcesses(processes, sizeof(processes), &dwResult))
    {
        std::cout << "Enum Error" << std::endl;
    }
    
    //程序數量
    size = dwResult / sizeof(DWORD);
​
    for (i = 0; i < size; i++)
    {
        //判斷程序號是否為0
        if (processes[i] != 0)
        {
            //用於儲存程序路徑
            TCHAR szProcessName[MAX_PATH] = { 0 };
            //使用查詢許可權開啟程序
            HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION |
                PROCESS_VM_READ,
                FALSE,
                processes[i]);
​
            if (hProcess != NULL)
            {
                HMODULE hMod;
                DWORD dwNeeded;
                //收集該程序的所有模組控制代碼,第一個控制代碼則為檔案路徑
                if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
                    &dwNeeded))
                {
                    //根據控制代碼獲取檔案路徑
                    GetModuleBaseName(hProcess, hMod, szProcessName,
                        sizeof(szProcessName) / sizeof(TCHAR));
                }
                wprintf(L"程序路徑:%s\t程序號:%d\n", szProcessName, processes[i]);
            }
        }   
    }
}

ZwQuerySystemInfomation

ZwQuerySystemInfomation函式是CreateToolhelp32Snapshot函式與EnumProcesses函式底層呼叫的函式,也用於遍歷程序資訊。程式碼參考https://cloud.tencent.com/developer/article/1454933

#include <iostream>
#include <Windows.h>
#include <ntstatus.h>
#include <winternl.h> 
#pragma comment(lib, "ntdll.lib") 
​
//定義函式指標
typedef NTSTATUS(WINAPI* NTQUERYSYSTEMINFORMATION)(
    IN      SYSTEM_INFORMATION_CLASS SystemInformationClass,
    IN OUT   PVOID                    SystemInformation,
    IN      ULONG                    SystemInformationLength,
    OUT PULONG                   ReturnLength
    );
​
int main()
{
    //設定編碼
    setlocale(LC_ALL, "zh_CN.UTF-8");
    //獲取模組地址
    HINSTANCE ntdll_dll = GetModuleHandle(L"ntdll.dll");
    if (ntdll_dll == NULL) {
        std::cout << "Get Module Error" << std::endl;
        exit(-1);
    }
​
    NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL;
    //獲取函式地址
    ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress(ntdll_dll, "ZwQuerySystemInformation");
    if (ZwQuerySystemInformation != NULL)
    {
        SYSTEM_BASIC_INFORMATION sbi = { 0 };
        //查詢系統基本資訊
        NTSTATUS status = ZwQuerySystemInformation(SystemBasicInformation, (PVOID)&sbi, sizeof(sbi), NULL);
        if (status == STATUS_SUCCESS)
        {
            wprintf(L"處理器個數:%d\r\n", sbi.NumberOfProcessors);
        }
        else
        {
            wprintf(L"ZwQuerySystemInfomation Error\n");
        }
​
        DWORD dwNeedSize = 0;
        BYTE* pBuffer = NULL;
​
        wprintf(L"\t----所有程序資訊----\t\n");
        PSYSTEM_PROCESS_INFORMATION psp = NULL;
        //查詢程序數量
        status = ZwQuerySystemInformation(SystemProcessInformation, NULL, 0, &dwNeedSize);
        if (status == STATUS_INFO_LENGTH_MISMATCH)
        {
            pBuffer = new BYTE[dwNeedSize];
            //查詢程序資訊
            status = ZwQuerySystemInformation(SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL);
            if (status == STATUS_SUCCESS)
            {
                psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer;
                wprintf(L"\tPID\t執行緒數\t工作集大小\t程序名\n");
                do {
                    //獲取程序號
                    wprintf(L"\t%d", psp->UniqueProcessId);
                    //獲取執行緒數量
                    wprintf(L"\t%d", psp->NumberOfThreads);
                    //獲取工作集大小
                    wprintf(L"\t%d", psp->WorkingSetSize / 1024);
                    //獲取路徑
                    wprintf(L"\t%s\n", psp->ImageName.Buffer);
                    //移動
                    psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset);
                } while (psp->NextEntryOffset != 0);
                delete[]pBuffer;
                pBuffer = NULL;
            }
            else if (status == STATUS_UNSUCCESSFUL) {
                wprintf(L"\n STATUS_UNSUCCESSFUL");
            }
            else if (status == STATUS_NOT_IMPLEMENTED) {
                wprintf(L"\n STATUS_NOT_IMPLEMENTED");
            }
            else if (status == STATUS_INVALID_INFO_CLASS) {
                wprintf(L"\n STATUS_INVALID_INFO_CLASS");
            }
            else if (status == STATUS_INFO_LENGTH_MISMATCH) {
                wprintf(L"\n STATUS_INFO_LENGTH_MISMATCH");
            }
        }
    }
}

程序隱藏

透過上述分析可以知道遍歷程序的方式有三種,分別是利用CreateToolhelp32SnapshotEnumProcesses以及ZwQuerySystemInfomation函式

但是CreateToolhelp32SnapshotEnumProcesses函式底層都是呼叫了ZwQuerySystemInfomation函式,因此我們只需要鉤取該函式即可。

由於測試環境是Win11,因此需要判斷在Win11情況下底層是否還是呼叫了ZwQuerySystemInfomation函式。

可以看到在Win11下還是會呼叫ZwQuerySystemInfomation函式,在使用者態下該函式的名稱為NtQuerySystemInformation函式。

淺談程序隱藏技術

這裡採用內聯鉤取的方式對ZwQuerySystemInfomation進行鉤取處理,具體怎麼鉤取在淺談內聯鉤取原理與實現已經介紹過了,這裡就不詳細說明了。這裡對自定義的ZwQuerySystemInfomation函式進行說明。

首先第一步需要進行脫鉤處理,因為後續需要用到初始的ZwQuerySystemInfomation函式,緊接著獲取待鉤取函式的地址即可。

...
    //脫鉤
    UnHook("ntdll.dll", "ZwQuerySystemInformation", g_pOrgBytes);
    HMODULE hModule = GetModuleHandleA("ntdll.dll");
    //獲取待鉤取函式的地址
    PROC    pfnOld = GetProcAddress(hModule, "ZwQuerySystemInformation");
    //呼叫原始的ZwQuerySystemInfomation函式
    NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...

為了隱藏指定程序,我們需要遍歷程序資訊,找到目標程序並且刪除該程序資訊實現隱藏的效果。這裡需要知道的是程序資訊都儲存在SYSTEM_PROCESS_INFORMATION結構體中,該結構體是透過單連結串列對程序資訊進行連結。因此我們透過匹配程序名稱找到對應的SYSTEM_PROCESS_INFORMATION結構體,然後進行刪除即可,效果如下圖。

淺談程序隱藏技術

透過單連結串列中刪除節點的操作,取出目標程序的結構體。程式碼如下

...
        pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation);
        while (true)
        {
            if (!lstrcmpi(pCur->ImageName.Buffer, L"test.exe"))
            {
                //需要隱藏的程序是最後一個節點
                if (pCur->NextEntryOffset == 0)
                    pPrev->NextEntryOffset = 0;
                //不是最後一個節點,則將該節點取出
                else
                    pPrev->NextEntryOffset += pCur->NextEntryOffset;
​
            }
            //不是需要隱藏的節點,則繼續遍歷
            else
                pPrev = pCur;
            //連結串列遍歷完畢
            if (pCur->NextEntryOffset == 0)
                break;
            pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset);
        }
...

完整程式碼:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c

但是採用內聯鉤取的方法去鉤取工作管理員就會出現一個問題,這裡將斷點取消,利用內聯鉤取的方式去隱藏程序。

首先利用bl命令檢視斷點

淺談程序隱藏技術

緊著利用 bc [ID]刪除斷點

淺談程序隱藏技術

在注入之後工作管理員會在複製的時候發生異常

淺談程序隱藏技術

在經過一番除錯後發現,由於多執行緒共同執行導致原本需要可寫許可權的段被修改為只讀許可權

windbg可以用使用!vprot + address檢視指定地址的許可權,可以看到由於程式往只讀許可權的地址進行複製處理,所以導致了異常。

淺談程序隱藏技術

但是在執行複製階段是先修改了該地址為可寫許可權,那麼導致該原因的情況就是其他執行緒執行了許可權恢復後切換到該執行緒中進行寫,所以導致了這個問題。

淺談程序隱藏技術

因此內聯鉤取是存在多執行緒安全的問題,此時可以使用微軟自己構建的鉤取庫Detours,可以在鉤取過程中確保執行緒安全。

【----幫助網安學習,以下所有學習資料免費領!加vx:dctintin,備註 “部落格園” 獲取!】

 ① 網安學習成長路徑思維導圖
 ② 60+網安經典常用工具包
 ③ 100+SRC漏洞分析報告
 ④ 150+網安攻防實戰技術電子書
 ⑤ 最權威CISSP 認證考試指南+題庫
 ⑥ 超1800頁CTF實戰技巧手冊
 ⑦ 最新網安大廠面試題合集(含答案)
 ⑧ APP客戶端安全檢測指南(安卓+IOS)

Detours

專案地址:https://github.com/microsoft/Detours

環境配置

參考:https://www.cnblogs.com/linxmouse/p/14168712.html

使用vcpkg下載

vcpkg.exe install detours:x86-windows
vcpkg.exe install detours:x64-windows
vcpkg.exe integrate install

例項

掛鉤

利用Detours掛鉤非常簡單,只需要根據下列順序,並且將自定義函式的地址與被掛鉤的地址即可完成掛鉤處理。

...
        //用於確保在 DLL 注入或載入時,恢復被 Detours 修改的程序映象,保持穩定性
        DetourRestoreAfterWith();
        //開始一個新的事務來附加或分離
        DetourTransactionBegin();
        //進行執行緒上下文的更新
        DetourUpdateThread(GetCurrentThread());
        //掛鉤
        DetourAttach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
        //提交事務
        error = DetourTransactionCommit();
...

脫鉤

然後根據順序完成脫鉤即可。

...
        //開始一個新的事務來附加或分離
        DetourTransactionBegin();
        //進行執行緒上下文的更新
        DetourUpdateThread(GetCurrentThread());
        //脫鉤
        DetourDetach(&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx);
        //提交事務
        error = DetourTransactionCommit();
...

掛鉤的原理

從上述可以看到,Detours是透過事務確保了在DLL載入與解除安裝時後的原子性,但是如何確保多執行緒安全呢?後續透過除錯去發現。

可以利用x ntdl!ZwQuerySystemInformation檢視函式地址,可以看到函式的未被掛鉤前的情況如下圖。

淺談程序隱藏技術

掛鉤之後原始的指令被修改為一個跳轉指令把前八個位元組覆蓋掉,剩餘的3位元組用垃圾指令填充。

淺談程序隱藏技術

該地址裡面又是一個jmp指令,並且完成間接定址的跳轉。

淺談程序隱藏技術

該地址是自定義函式ZwQuerySystemInformationEx,因此該間接跳轉是跳轉到的自定義函式內部。

淺談程序隱藏技術

跳轉到TrueZwQuerySystemInformation內部發現ZwQuerySystemInformation函式內部的八位元組指令被移動到該函式內部。緊接著又完成一個跳轉。

淺談程序隱藏技術

該跳轉到ZwQuerySystemInformation函式內部緊接著完成ZwQuerySystemInformation函式的呼叫。

淺談程序隱藏技術

綜上所述,整體流程如下圖。實際上Detours實際上使用的是熱補丁的思路,但是Detours並不是直接在原始的函式空間中進行補丁,而是開闢了一段臨時空間,將指令儲存在裡面。因此在掛鉤後不需要進行脫鉤處理就可以呼叫原始函式。因此就不存在多執行緒中掛鉤與脫鉤的衝突。

淺談程序隱藏技術

完整程式碼:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c

更多網安技能的線上實操練習,請點選這裡>>

相關文章