<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    EXE文件內存加載

    一顆小胡椒2021-12-02 16:22:13

    01 前言

    作為一名安全菜鳥,單純的了解某一個方面是并不合格的,安全并不僅限于某一門語言、某一個OS,現如今安全研究的技術棧要求的更深、更廣。雖說 PE 文件內存加載已經是多年前的技術,但是招不在新、有用就行,內存加載技術仍然有非常廣泛的應用(隱藏自身,至于為什么要隱藏自身,dddd),由于筆者之前認知的偏差導致對PE相關的知識僅停留在知道的地步,并沒有靜下心來去認真分析學習,借此機會補足一下技術點,同時順便為自己的惡意代碼分析的學習之旅開個頭。

    02 關鍵步驟

    0x1 Section 對齊

    因為exe以文件形式存儲的時候區段間的對齊方式與在內存中的對齊方式不盡相同,因此在手動加載exe時不能單純的將文件格式的 exe 直接拷貝到內存中,而是應當根據內存區段(page size)的對齊方式做對齊處理。

    為了驗證一下,隨便找一個 exe 文件作為學習資料。

    FileAlignment 為 0x200,實際的Section也均是以 0x200 為單位進行對齊的,實際調試時就會發現section的對齊變為了SectionAlignment的大小:0x1000

    0x2 導入表修復

    導入表是PE文件從其它第三方程序中導入API,以供本程序調用的機制(與導出表對應),在exe運行起來的時, 加載器會遍歷導入表, 將導入表中所有dll 都加載到進程中,被加載的DLL的DllMain就會被調用,通過導入表可以知道程序使用了哪些函數,導入表是一個數組,以全為零結尾。

    要理解導入表,首先要理解PE文件分為兩種編譯方式:動態鏈接、靜態鏈接。

    靜態鏈接方式:在程序執行之前完成所有的組裝工作,生成一個可執行的目標文件(EXE文件)。

    動態鏈接方式:在程序已經為了執行被裝入內存之后完成鏈接工作,并且在內存中一般只保留該編譯單元的一份拷貝。

    靜態鏈接優勢在于其可移植性較強,基本上不依賴于系統的dll(自己全打包好了),動態鏈接的優勢在于程序主體較小,占用系統資源不多。

    動態鏈接庫的兩種鏈接方法:

    (1) 裝載時動態鏈接(Load-time Dynamic Linking):這種用法的前提是在編譯之前已經明確知道要調用DLL中的哪幾個函數,編譯時在目標文件中只保留必要的鏈接信息,而不含DLL函數的代碼;當程序執行時,調用函數的時候利用鏈接信息加載DLL函數代碼并在內存中將其鏈接入調用程序的執行空間中(全部函數加載進內存),其主要目的是便于代碼共享。(動態加載程序,處在加載階段,主要為了共享代碼,共享代碼內存)

    (2) 運行時動態鏈接(Run-time Dynamic Linking):這種方式是指在編譯之前并不知道將會調用哪些DLL函數,完全是在運行過程中根據需要決定應調用哪個函數,將其加載到內存中(只加載調用的函數進內存),并標識內存地址,其他程序也可以使用該程序,并用LoadLibrary和GetProcAddress動態獲得DLL函數的入口地址。(dll在內存中只存在一份,處在運行階段)

    相較于靜態鏈接所有函數均在一個exe文件里,要調用某個函數時只需要按照寫死的偏移進行調用,動態鏈接就存在一個找函數的問題:

    當程序運行起來需要某個系統函數時,哪個dll包含該函數?dll加載到內存里之后地址是不確定的,如何按照從內存中定位到所需的函數地址。

    PE文件中的導入表就可以解決上述問題。

    要理解導入表首先要了解以下這幾個結構:

    IMAGE_DATA_DIRECTORY

    IMAGE_DATA_DIRECTORY 位于 IMAGE_Optional_header 中的最后一個字段,是一個由16個_IMAGE_DATA_DIRECTORY 結構體構成的結構體數組,每個結構體由兩個字段構成,分別為VirtualAddress和Size字段:

    • VirtualAddress字段記錄了對應數據結構的RVA。
    • Size字段記錄了該數據結構的大小。

    Offset (PE/PE32+)    Description96/112                        Export table address and size104/120                        Import table address and size112/128                        Resource table address and size120/136                        Exception table address and size128/144                        Certificate table address and size136/152                        Base relocation table address and size144/160                        Debugging information starting address and size152/168                        Architecture-specific data address and size160/176                        Global pointer register relative virtual address168/184                        Thread local storage (TLS) table address and size176/192                        Load configuration table address and size184/200                        Bound import table address and size192/208                        Import address table address and size200/216                        Delay import descriptor address and size208/224                        The CLR header address and size216/232                        Reserved
    

    根據微軟提供的信息,IMAGE_DATA_DIRECTORY 的第二項指向的就是導入表了。

    IMAGE_IMPORT_DESCRIPTOR

    既然已經找到了導入表,就需要根據導入表內的元素來加載對應的dll,獲取不同的函數地址了,此時就需要用到 IMAGE_IMPORT_DESCRIPTOR 結構了,該結構的詳細內容如下:

    Offset    Size    Field0         4       Import Lookup Table RVA4         4       Time/Date Stamp8         4       Forwarder Chain12        4       Name RVA16        4       Import Address Table RVA
    typedef struct _IMAGE_IMPORT_DESCRIPTOR {      union {          DWORD Characteristics;          DWORD OriginalFirstThunk;//(1) 指向導入名稱表(INT)的RAV*       };      DWORD   TimeDateStamp;  // (2) 時間標識      DWORD   ForwarderChain; // (3) 轉發鏈,如果不轉發則此值為0     DWORD   Name;       // (4) 指向導入映像文件的名字*      DWORD   FirstThunk; // (5) 指向導入地址表(IAT)的RAV*} IMAGE_IMPORT_DESCRIPTOR;
    

    在修復IAT的過程中最重要的兩個字段就是 OriginalFirstThunk 和 FirstThunk了:

    根據OriginalFirstThunk獲取要用到的函數名,將獲取到的函數地址填到FirstThunk中。

    03 具體分析

    為了方便理解和記憶,默認讀取的 PE 文件格式不存在問題,不做錯誤處理。

    相較于dll的內存加載,exe的內存加載簡化了很多,其中省略掉的一個大步驟就是導出表的修復。

    文件讀取

    文件讀取步驟基本上可以說條條大路通羅馬,只要將 PE 文件完整的讀取到內存中可供后續處理即可,除了把一個文件放在目錄中進行讀取以外還有很多種方式,比如將要加載的 exe 轉換成shellcode進行加載、將shellcode進行簡單xor后在內存xor回來再加載。。。

    ifstream inFile("nc.exe", ios::in | ios::binary);stringstream tmp;tmp << inFile.rdbuf();unsigned char* content = (unsigned char*)tmp.str().c_str();
    

    初始內存分配

    因為 exe 文件默認有加載基址,一般情況下在運行 exe 的時候會首先嘗試加載到默認地址上去,因此就要根據 exe 的默認加載基址和映像大小(exe加載到內存后的大小:SizeOfImage)來分配內存

    SIZE_T SizeOfImage = pFileNtHeader->OptionalHeader.SizeOfImage;// 獲取加載基址DWORD base = pFileNtHeader->OptionalHeader.ImageBase;// 分配內存unsigned char* memExeBase = (unsigned char*)VirtualAlloc((LPVOID)base, SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders);
    

    分配內存時的 VirtualAlloc指定的頁類型為 MEM_COMMIT|MEM_RESERVE這里是一個小小的延遲分配的知識點,如果是 MEM_RESERVE的話只有當對該段內存進行內存操作時才會被真正Load進入物理內存中。頁權限使用的是PAGE_EXECUTE_READWRITE,這在實際編碼過程中是一個很不好的習慣,為了更清晰的理解 exe 內存加載的核心流程,就省略了根據section來確定內存權限的步驟。

    拷貝Header

    分配內存完畢后首先要將 PE header 拷貝到相應的地址空間去,因為后續的操作均需要用到。

    memcpy(memExeBase, content, pFileNtHeader->OptionalHeader.SizeOfHeaders);PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)memExeBase;PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)(memExeBase + pDosHeader->e_lfanew);
    

    之后要根據新分配的內存地址計算新的 DOS頭 和 NT頭。

    修復ImageBase

    因為已經根據ImageBase分配了內存,所以需要將拷貝后的OptionalHeader中的ImageBase字段根據實際內存地址進行更新,如果開啟了aslr的話需要根據實際的內存地址更新ImageBase,分配到默認基址上的話沒有必要。

    pNtHeader->OptionalHeader.ImageBase = (DWORD)memExeBase;
    

    拷貝區段

    拷貝區段這部分是內存加載的第一個關鍵點,要根據內存頁的大小來將原本的文件區段進行處理。在文件中Section通常以 0x200 進行對齊,內存中頁大小單位為 0x1000, 因此內存對齊單位為 0x1000,所以當PE文件加載到內存中后需要對Section進行變換。

    // 拷貝區段    PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(pNtHeader);    int sectionSize;    for (int i = 0; i < pNtHeader->FileHeader.NumberOfSections; i++, section++) {        if (section->SizeOfRawData == 0) {            sectionSize = pNtHeader->OptionalHeader.SectionAlignment; // 最小內存Seciton單位為 SectionAlignment的大小        }        else {            sectionSize = section->SizeOfRawData;        }        if (sectionSize > 0) {            void* dest = memExeBase + section->VirtualAddress;            memcpy(dest, content + section->PointerToRawData, sectionSize);        }    }
    

    修復導入表

    OriginalFirstChunk指向的INT表表項以4字節為單位,全0結尾,如果最高位為1則代表是函數序號,反之則是一個RVA,指向IMAGE_IMPORT_BY_NAME結構,INT表實際上的功能是獲取要導入的函數名。目前沒有遇到最高位為1的情況。

    typedef struct _IMAGE_IMPORT_BY_NAME {    WORD    Hint;                     CHAR   Name[1];               //函數名稱,0結尾.} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
    

    IAT表項則直接是一個指向真實函數地址的指針。

    PIMAGE_IMPORT_DESCRIPTOR pImportDesc;bool result = true;PIMAGE_DATA_DIRECTORY pDataDir = (PIMAGE_DATA_DIRECTORY)(&pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]);    // 獲取IMAGE_DATA_DIRECTORY 位置pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(memExeBase + pDataDir->VirtualAddress);// 獲取第一個IMAGE_IMPORT_DESCRIPTORfor (;!IsBadReadPtr(pImportDesc,sizeof(IMAGE_IMPORT_DESCRIPTOR)) && pImportDesc->Name;pImportDesc++) {    uintptr_t* thunkRef;    FARPROC* funcRef;    HMODULE handle = LoadLibraryA((LPCSTR)(memExeBase + pImportDesc->Name)); // 加載dll(此處也是可以手工加載的)    if (pImportDesc->OriginalFirstThunk) {        thunkRef = (uintptr_t*)(memExeBase + pImportDesc->OriginalFirstThunk);        funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk);    }    else {        thunkRef = (uintptr_t*)(memExeBase + pImportDesc->FirstThunk);        funcRef = (FARPROC*)(memExeBase + pImportDesc->FirstThunk);    }    for (; *thunkRef; thunkRef++, funcRef++) {        if (IMAGE_SNAP_BY_ORDINAL(*thunkRef)) { // 判斷OriginalFirstThunk表項最高位為1的情況            *funcRef = GetProcAddress(handle, (LPCSTR)(IMAGE_ORDINAL(*thunkRef)));//修復導入表        }        else {            PIMAGE_IMPORT_BY_NAME thunkData = (PIMAGE_IMPORT_BY_NAME)(memExeBase + (*thunkRef));            *funcRef = GetProcAddress(handle, (LPCSTR)&thunkData->Name); //修復導入表        }        if (*funcRef == 0) {            cout << " error import " << endl;            exit(0);        }    }}
    

    上述修復導入表的代碼實際完成的工作就是遍歷導入表中的 IMAGE_IMPORT_DESCRIPTOR結構,根據dll名稱將對應的dll加載到內存中,并根據OriginalFirstThunk字段來獲取所需的函數名進而使用GetProcAddress來獲取該函數在內存中的實際地址填充到FirstThunk字段指向的空間中。

    獲取入口點啟動程序

    if (pNtHeader->OptionalHeader.AddressOfEntryPoint != 0) {    ExeEntryProc exeEntry = (ExeEntryProc)(LPVOID)(memExeBase + pNtHeader->OptionalHeader.AddressOfEntryPoint);    exeEntry();}
    

    結語

    至此exe的內存加載就已經結束了,誘發我寫下這篇文章的一個主要原因是回憶起之前看過的幾篇APT相關的分析文章,涉及到主機的遠控目前內存加載已經是標配,殺軟動態檢測的對抗方式種類繁多,靜態對抗的方法以內存加載為王。

    參考鏈接

    https://www.cnblogs.com/iBinary/p/9740757.html

    https://github.com/fancycode/MemoryModule

    https://blog.csdn.net/Apollon_krj/article/details/77417063

    https://www.cnblogs.com/tracylee/archive/2012/10/15/2723816.html

    函數調用dll文件
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    反射式DLL注入實現
    2022-05-13 15:59:21
    反射式dll注入與常規dll注入類似,而不同的地方在于反射式dll注入技術自己實現了一個reflective loader()函數來代替LoadLibaryA()函數去加載dll,示意圖如下圖所示。藍色的線表示與用常規dll注入相同的步驟,紅框中的是reflective loader()函數行為,也是下面重點描述的地方。
    在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鉤子機制來截獲和監視系統中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現相應的鉤子函數。
    全局鉤子注入在Windows大部分應用都是基于消息機制,他們都擁有一個消息過程函數,根據不同消息完成不同功能,windows通過鉤子機制來截獲和監視系統中的這些消息。一般鉤子分局部鉤子與全局鉤子,局部鉤子一般用于某個線程,而全局鉤子一般通過dll文件實現相應的鉤子函數。
    概述該樣本是來自于VirusShare網站提供,于2022年3月15日在VirusTotal上被首次提交。
    Dll注入
    2021-11-08 14:57:41
    最近太忙啦XDM,又在做一些列的分析復現工作量有點大,更新要慢一點了。一致,也不會覆蓋其他的進程信息。
    DLL 代理加載 shellcode
    2020-10-27 17:59:01
    DLL側面加載或DLL代理加載允許攻擊者濫用合法的和經過簽名的可執行文件,以在受感染的系統上執行代碼。使用上面的示例流程,將發生以下情況。簽名,目標應該是經過數字簽名的“合法”可執行文件。在運行時不安全地加載少量DLL ,可執行流必須是可劫持的,但我們不希望將超過1-3個DLL放到目標上以使我們的攻擊才能順利進行。名稱應與原始DLL名稱匹配,命名“ libnettle-7”,然后單擊“創建”。我們使用SharpDllProxy生成源代碼時定義了文件名“ ”。
    關于堆棧ShellCode操作:基礎理論002-利用fs寄存器尋找當前程序dll的入口:從動態運行的程序中定位所需dll003-尋找大兵LoadLibraryA:從定位到的dll中尋找所需函數地址004-被截斷的shellCode:加解密,解決shellCode的零字截斷問題
    要做好檢測能力,必須得熟悉你的系統環境,只有足夠了解正常行為,才能真正找出異常(Anomaly)和威脅(Threat)
    EDR(端點威脅檢測和響應)和XDR(擴展威脅檢測和響應)解決方案在幫助企業識別和減輕網絡攻擊威脅方面發揮了重要作用。然而,Lumu公司最新發布的《2023勒索軟件調查報告》數據顯示,由于EDR/XDR措施被突破而造成的數據泄露數量正在持續上升。攻擊者為何能夠屢屢得手?
    利用該漏洞可能會授予對 MOVEit Transfer 數據庫的未經授權的訪問,從而允許攻擊者操縱和泄露敏感信息。變量,則很容易受到 SQL 注入攻擊。有了這種理解,研究人員重點轉向尋找一種繞過清理并以未經清理的方式操縱 Arg01 參數的方法,因此了解 MOVEit 軟件如何處理請求非常重要。由于與未經身份驗證的操作相關而脫穎而出。
    一顆小胡椒
    暫無描述
      亚洲 欧美 自拍 唯美 另类