1. 程式人生 > 其它 >[Rootkit] 程序隱藏 - 記憶體載入(寄生&殭屍程序)

[Rootkit] 程序隱藏 - 記憶體載入(寄生&殭屍程序)

眾所周知,windows下可執行檔案必須符合一定的格式要求,微軟官方稱之為PE檔案(關於PE檔案的詳細介紹這裡就不贅述了,google一下可以找到大把);使用者在介面雙擊exe時,有個叫做explorer的程序會監測並接受到這個事件,然後根據登錄檔中的資訊取得檔名,再以Explorer.exe這個檔名呼叫CreateProcess函式去執行使用者雙擊的exe;PC中使用者一般都是這樣執行exe的,所以很多使用者態的exe都是exlporer的子程序,用process hacker截圖如下:

在這裡插入圖片描述

那麼這個explorer究竟是怎麼成功“執行”這個exe的了?這裡面涉及到大量細枝末節就不深究了,本文先把主幹思路捋一遍!

  • 分配記憶體  
    既然是執行,肯定是需要放在記憶體的,所以首先要開闢記憶體空間,才能把exe從磁碟載入進來;以32位為例,由於每個程序都有自己的4GB虛擬空間,所以還涉及到新生成頁表、填充CR3等瑣碎的細節工作;

  • 載入到記憶體
    記憶體分配好後,接著就該把exe從磁碟讀取到記憶體了;

  • 重定位(文章末尾有擴充套件,詳細介紹imagebase、VA、RVA、PointerToRawData、foa等概念)
    這一步我個人覺得是最關鍵、最容易出錯的了!PE檔案在編譯器編譯的時候,編譯器是不知道檔案會被載入到那個VA的(一般exe預設從40000開始,這個還好;但是dll預設從100000開始,這個就不同了。一個exe一般會呼叫多個dll,後面載入的dll肯定會和前面載入dll的imagebase衝突),這個時候只能把dll或exe載入到其他虛擬地址;一旦改變了imagebase,涉及到地址硬編碼的地方都要改了,包括:全域性/靜態變數、子函式呼叫;所以PE檔案裡面單獨有個relc段,標明瞭需要重新定位和生成VA的地址;由於硬編碼存放的都是相對地址,所以重定位後新VA的計算公式也很簡單,如下:新imagebase-舊imagebase+RAV

  • 填寫匯入表
    一個exe的執行,很多時候要依賴作業系統提供的函式,舉個最簡單的例子:比如我要列印一段string,console下要用到printf或cout,MFC要用到messagebox,這些都是作業系統提供的API,編譯器編譯時也是不知道這些系統函式究竟被作業系統放在了記憶體的哪個地方,call的時候該往哪跳轉了?所以只能把需要用到的這些系統函式統一放在一張叫做匯入表的表格,explorer載入的時候還要挨個遍歷匯入表,一旦發現該PE檔案用到了某些系統API,需要用這些API在記憶體的真實地址替換PE檔案中call的地址(這也是用OD、x96dbg這些常見的偵錯程式能找到這些系統函式的根本原因:都是系統提供的嘛,函式名必須儲存起來,否則載入的時候沒法替換成記憶體中真正的地址)!

好了,到此為止exe被載入的核心步驟都縷過了;具體實現上,explorer呼叫了createPorcess來載入和執行exe,這就直接導致了一個後果:被工作管理員或process hacker檢測到(這裡和通過loadLibrary類似:只要是通過windows提供的API使用記憶體,都會在某些地方被記錄,這也是windows常見的記憶體管理方式之一,用了必須記錄!所以規避檢測的方式之一就是自己實現exe或dll的載入和執行,不依賴window的API)!為了躲避工作管理員或process hacker的監察,只能不呼叫createProcess,而是自己模擬PE載入的思路重新實現一遍了(類似於自己重新openProcess函式一樣吧)!

自己實現PE loader核心思路程式碼如下(參考第5個連結):

int main()
{
    char szFileName[] = "D:\\software\\PELoader-master1\\test.exe";

    //開啟檔案,設定屬性可讀可寫
    HANDLE hFile = CreateFileA(szFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL);

    if (INVALID_HANDLE_VALUE == hFile)
    {
        printf("檔案開啟失敗\n");
        return 1;
    }

    //獲取檔案大小
    DWORD dwFileSize = GetFileSize(hFile, NULL);

    //申請空間
    char* pData = new char[dwFileSize];
    if (NULL == pData)
    {
        printf("空間申請失敗\n");
        return 2;
    }

    //將檔案讀取到記憶體中
    DWORD dwRet = 0;
    ReadFile(hFile, pData, dwFileSize, &dwRet, NULL);
    CloseHandle(hFile);


    //將記憶體中exe載入到程式中
    char* chBaseAddress = RunExe(pData, dwFileSize);


    delete[] pData;
    system("pause");
    return 0;
}

其他程式碼如下(老規矩:精華都在註釋了):

#include <windows.h>
#include <stdio.h>


//跳轉到入口點執行
bool CallEntry(char* chBaseAddress)
{
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    char* ExeEntry = (char*)(chBaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);

    // 跳轉到入口點處執行
    __asm
    {
        mov eax, ExeEntry
        jmp eax
    }

    return TRUE;
}


//設定預設載入基址
bool SetImageBase(char* chBaseAddress)
{
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    pNt->OptionalHeader.ImageBase = (ULONG32)chBaseAddress;

    return TRUE;
}


//填寫匯入表
bool ImportTable(char* chBaseAddress)
{
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDos +
        pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

    // 迴圈遍歷DLL匯入表中的DLL及獲取匯入表中的函式地址
    char* lpDllName = NULL;
    HMODULE hDll = NULL;
    PIMAGE_THUNK_DATA lpImportNameArray = NULL;
    PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;
    PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;
    FARPROC lpFuncAddress = NULL;
    DWORD i = 0;

    while (TRUE)
    {
        if (0 == pImportTable->OriginalFirstThunk)
        {
            break;
        }

        // 獲取匯入表中DLL的名稱並載入DLL
        lpDllName = (char*)((DWORD)pDos + pImportTable->Name);
        //看看這個dll是否已經載入
        hDll = GetModuleHandleA(lpDllName);
        //如果沒有載入,那麼先載入到記憶體
        if (NULL == hDll)
        {
            hDll = LoadLibraryA(lpDllName);
            if (NULL == hDll)
            {
                pImportTable++;
                continue;
            }
        }

        i = 0;
        // 獲取OriginalFirstThunk以及對應的匯入函式名稱表首地址
        lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->OriginalFirstThunk);
        // 獲取FirstThunk以及對應的匯入函式地址表首地址
        lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)pDos + pImportTable->FirstThunk);
        while (TRUE)
        {
            if (0 == lpImportNameArray[i].u1.AddressOfData)
            {
                break;
            }

            // 獲取IMAGE_IMPORT_BY_NAME結構
            lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDos + lpImportNameArray[i].u1.AddressOfData);

            // 判斷匯出函式是序號匯出還是函式名稱匯出
            if (0x80000000 & lpImportNameArray[i].u1.Ordinal)
            {
                // 序號匯出
                // 當IMAGE_THUNK_DATA值的最高位為1時,表示函式以序號方式輸入,這時,低位被看做是一個函式序號
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));
            }
            else
            {
                // 名稱匯出
                lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name);
            }
            // 注意此處的函式地址表的賦值,要對照PE格式進行裝載,不要理解錯了!!!
            // 把需要呼叫其他dll函式的VA寫回匯入表,就能通過call跳轉到這裡執行了
            lpImportFuncAddrArray[i].u1.Function = (DWORD)lpFuncAddress;
            i++;
        }

        pImportTable++;
    }

    return TRUE;


}


//修復重定位表
bool RelocationTable(char* chBaseAddress)
{
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)chBaseAddress;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(chBaseAddress + pDos->e_lfanew);
    PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)(chBaseAddress + pNt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);

    //判斷是否有重定位表
    if ((char*)pLoc == (char*)pDos)
    {
        return TRUE;
    }

    while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //開始掃描重定位表
    {
        WORD* pLocData = (WORD*)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION));
        //計算需要修正的重定位項(地址)的數目
        int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);

        for (int i = 0; i < nNumberOfReloc; i++)
        {
            if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //這是一個需要修正的地址
            {
                DWORD* pAddress = (DWORD*)((PBYTE)pDos + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF));
                DWORD dwDelta = (DWORD)pDos - pNt->OptionalHeader.ImageBase;//實際的imageBase減去pe檔案裡面標識的imagebase得到“移動的距離”
                *pAddress += dwDelta;//把移動的距離在原地址加上去
            }
        }

        //轉移到下一個節進行處理
        pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock);
    }

    return TRUE;
}


//將記憶體中的檔案對映到程序記憶體空間中
bool MapFile(char* pFileBuff, char* chBaseAddress)
{
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuff;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pFileBuff + pDos->e_lfanew);
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);

    //所有頭 + 結表頭的大小
    DWORD dwSizeOfHeaders = pNt->OptionalHeader.SizeOfHeaders;

    //獲取區段數量
    int nNumerOfSections = pNt->FileHeader.NumberOfSections;

    // 將前一部分都拷貝過去
    RtlCopyMemory(chBaseAddress, pFileBuff, dwSizeOfHeaders);

    char* chSrcMem = NULL;
    char* chDestMem = NULL;
    DWORD dwSizeOfRawData = 0;

    for (int i = 0; i < nNumerOfSections; i++)
    {
        if ((0 == pSection->VirtualAddress) ||
            (0 == pSection->SizeOfRawData))
        {
            pSection++;
            continue;
        }

        // 拷貝節區
        chSrcMem = (char*)((DWORD)pFileBuff + pSection->PointerToRawData);
        chDestMem = (char*)((DWORD)chBaseAddress + pSection->VirtualAddress);
        dwSizeOfRawData = pSection->SizeOfRawData;
        RtlCopyMemory(chDestMem, chSrcMem, dwSizeOfRawData);

        pSection++;
    }

    return TRUE;
}


//獲取映象大小,傳入的是檔案的開始地址
DWORD GetSizeOfImage(char* pFileBuff)
{
    DWORD dwSizeOfImage = 0;
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pFileBuff;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pFileBuff + pDos->e_lfanew);
    dwSizeOfImage = pNt->OptionalHeader.SizeOfImage;

    return dwSizeOfImage;
}


//執行檔案
char* RunExe(char* pFileBuff, DWORD dwSize)
{
    char* chBaseAddress = NULL;

    //獲取映象大小
    DWORD dwSizeOfImage = GetSizeOfImage(pFileBuff);

    //根據映象大小在程序中開闢一塊記憶體空間
    chBaseAddress = (char*)VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

    if (NULL == chBaseAddress)
    {
        printf("申請程序空間失敗\n");
        return NULL;
    }

    //將申請的程序空間全部填0
    RtlZeroMemory(chBaseAddress, dwSizeOfImage);

    //將記憶體中的exe資料對映到peloader程序記憶體中,避免重新生成一個程序,這是隱藏exe的方式之一
    if (FALSE == MapFile(pFileBuff, chBaseAddress))
    {
        printf("記憶體對映失敗\n");
        return NULL;
    }

    //修復重定位
    if (FALSE == RelocationTable(chBaseAddress))
    {
        printf("重定位修復失敗\n");
        return NULL;
    }

    //填寫匯入表
    if (FALSE == ImportTable(chBaseAddress))
    {
        printf("填寫匯入表失敗\n");
        return NULL;
    }

    //將頁屬性都設定為可讀可寫可執行
    DWORD dwOldProtect = 0;
    if (FALSE == VirtualProtect(chBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
    {
        printf("設定頁屬性失敗\n");
        return NULL;
    }

    //設定預設載入基址
    if (FALSE == SetImageBase(chBaseAddress))
    {
        printf("設定預設載入基址失敗\n");
        return NULL;
    }

    //跳轉到入口點執行
    if (FALSE == CallEntry(chBaseAddress))
    {
        printf("跳轉到入口點失敗\n");
        return NULL;
    }

    return chBaseAddress;

}

從程式碼看:這個pe loader本質上是在loader的程序開闢空間,然後執行exe的,所以exe的程式碼和資料其實都在loader的空間,並未單獨生成一個程序,所以工作管理員、process hacker是都查不到的!這裡也是把exe想辦法當成了shellcode在用!整體感覺就像“寄生”一樣!

效果如下:單獨雙擊執行test.exe:這就是最終呈現的效果;

最後,編譯exe的時候出於安全考慮,建議隨機基址選是,編譯生成的exe每次被載入的時候imagebase都是變化的,能在一定程度上增加逆向的難度,讓逆向變得很繁瑣,有效消耗逆向人員的時間和精力!

擴充套件:很多小夥伴剛接觸PE的時候,分不清楚imagebase、VA、RVA、PointerToRawData、foa等概念,這裡來縷一縷;

(1)imageBase:整個檔案(比如pe、sys、dll等)在虛擬記憶體中的起始地址;以32位為例,exe預設都是從400000開始的;OD中查詢PE檔案頭就是imageBase;上面說的重定位也是從imageBase這裡開始重新計算新地址;

(2)virtualAddress:OD中左邊的地址列就是VA,也就是在虛擬記憶體中的地址;

(3)RVA: related virtual address,翻譯成中文就是相對虛擬地址;這個“相對”怎麼理解了?“相對”就是VA和當前所在區段的距離;比如一個VA=0x401010,很明顯是屬於text段的,由於text段的基址是401000,那麼這個地址的RVA=0x401010-0x401000=0x10;

(4)PointerToRawData:我也不知道怎麼翻譯成中文合適,所以乾脆不翻譯了;為什麼會有這麼一個概念了? 或則說這個概念想表達啥了?由於歷史原因,很久以前磁碟的價格是很貴的,為了節約磁碟空間,pe檔案儘量“壓縮”式地存放在磁碟中。為了標註各個段在磁碟中的位置,就衍生出了PointerToRawData:即磁碟中,每個段頭部相對於檔案開始位置的距離;當執行程式時,需要把檔案載入到記憶體。由於採用了虛擬地址、頁交換等技術,虛擬記憶體空間大很多,沒必要“節約”著用了,為了提高cpu定址的效率,就需要記憶體對齊了,直觀感覺就是下圖中綠色的部分;這就導致了另一個問題:同樣一個段,在磁碟中相對檔案起始的距離,和記憶體中相對imageBase的距離是不一樣的(因為地址對齊,拉伸了)! 用010editor這種軟體是可以查到PointerToRawData的,如下:

(5)FOA: file offset address,又叫file address,簡稱FA,也就是磁碟檔案內部的地址,計算出這個地址有利於靜態查詢和破解打補丁(比如改if跳轉邏輯)。比如我們用OD找到了一個記憶體虛擬地址,怎麼根據這個地址在磁碟的檔案中找到同樣的地址了?原理很簡單,如下:

先計算出RAV,也就是當前虛擬地址相對於所在段的距離,比如上面的0x401010-0x401000=0x10,也就是這個地址距離text段的偏移是0x10;現在問題就轉換成了怎麼找text段在檔案中的起始地址了?也很簡單,直接查PointerToRawData唄!比如這個值是0x200,那麼FA=PointerToRawData+RVA=0x200+0x10=0x210!在磁碟檔案內部0x210的位置就能找到了!

文章出處:https://www.cnblogs.com/lyshark
許可協議: 文章中的程式碼均為學習時整理的筆記,部落格中除去明確標註有參考文獻的文章,其他文章【均為原創】作品,轉載請務必【添加出處】,您添加出處是我創作的動力!

防惡意轉載:如果發現您在轉載時,沒有新增本人部落格連結,本人將通過爬蟲批量爬取您部落格中所有內容,打上本人原創版權水印,並進行二次發行,請相互尊重,你尊重我的勞動成果,我才會尊重你。