利用模塊加載回調函數修改PE導入表實現注入


 最近整理PE文件相關代碼的時候,想到如果能在PE剛剛讀進內存的時候再去修改內存PE鏡像,那不是比直接對PE文件進行操作隱秘多了么?

 PE文件在運行時會根據導入表來進行dll庫的“動態鏈接”,那么如果我們修改PE導入表結構,就可以實現對我們自己動態庫的導入,從而實現注入。

 

 那么問題來了,選擇一個合適的時機顯得很重要,網上搜索了一下,大部分都是直接在文件上進行修改,有位同學說用LoadImageNotifyRoutine可以來實現。

 每一個模塊加載前都能觸發SetLoadImageNotifyRoutine注冊的回調函數,然后獲得PE文件基地址,構造PE文件就可以實現注入了。

 

 下面簡單復習一下PE文件導入表以及系統回調。

 

 PE文件導入表

 微軟對導入表結構體的定義

typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
        DWORD Characteristics; // 0 for terminating null import descriptor
        DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA)
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 0 if not bound,
                     // -1 if bound, and realdate\time stamp
                     // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
                     // O.W. date/time stamp ofDLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name;
DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR

值得注意的是:上述結構的是導入表數組中的一項,每個導入的 DLL 都會有一個結構,也就是說,一個這樣的結構對應一個導入的 DLL。


Characteristics 和 OriginalFirstThunk:一個聯合體,如果是數組的最后一項 Characteristics 為0,否則 OriginalFirstThunk 保存一個 RVA,指向一個 IMAGE_THUNK_DATA 的數組, 這個數組中的每一項表示一個導入函數。
TimeDateStamp:              映象綁定前,這個值是0,綁定后是導入模塊的時間戳。
ForwarderChain:                                轉發鏈,如果沒有轉發器,這個值是-1。
Name:                                                一個 RVA,指向導入模塊的名字,所以一個 IMAGE_IMPORT_DESCRIPTOR 描 述一個導入的 DLL。
FirstThunk :                                       也是一個RVA,也指向一個IMAGE_THUNK_DATA 數組 。

 

既然OriginalFirstThunk與FirstThunk都指向一個IMAGE_THUNK_DATA數組,而且這兩個域的名字都長得很像,他倆有什么區別呢?

為了解答這個問題, 先來認識一下 IMAGE_THUNK_DATA 結構:

typedef struct _IMAGE_THUNK_DATA32 {
union {
        DWORD ForwarderString; // PBYTE
        DWORD Function; // PDWORD
        DWORD Ordinal;
        DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;

ForwarderString :是轉發用的,暫時不用考慮。

Function :            表示函數地址。

Ordinal :              如果是按序號導入 Ordinal 就有用了。如果 Ordinal 的最高位是1, 就是按序號導入的,這時候,低16位就是導入序號,如果最高位是0,則 AddressOfData 是一個RVA,指向一個IMAGE_IMPORT_BY_NAME結構,用來保存名字信息,

AddressOfData:   若是按名字導入  便指向名字信息。

可以看出這個結構體 就是一個大的 union,大家都知道 union 雖包含多個域但是在不同時刻代表不同的意義那到 底應該是名字還是序號,該如何區分呢?可以通過 Ordinal 判斷,由於Ordinal 和 AddressOfData 實際上是同一個內存空間,所以 AddressOfData 其實只有低31位可以表示RVA,但是一個 PE 文件不可能超過2G,所以最高位永遠為0,這樣設計很合理的利用了空間 。 實際編寫代碼的時候微軟提供兩個宏定義處理序號導入:IMAGE_SNAP_BY_ORDINAL 判斷是否按序號導入,IMAGE_ORDINAL 用來獲取導入序 號。

 

這時我們可以回頭看看 OriginalFirstThunk 與 FirstThunk,OriginalFirstThunk 指向的 IMAGE_THUNK_DATA 數組包含導入信息,在這個數組中只有 Ordinal 和 AddressOfData 是有用的,因此可以通過 OriginalFirstThunk 查找到函數的地址。FirstThunk 則略有不同, 在 PE 文件加載以前或者說在導入表未處理以前,他所指向的數組與 OriginalFirstThunk 中 的數組雖不是同一個,但是內容卻是相同的,都包含了導入信息,而在加載之后,FirstThunk 中的 Function 開始生效,他指向實際的函數地址,因為 FirstThunk 實際上指向 IAT 中的一 個位置,IAT 就充當了 IMAGE_THUNK_DATA 數組,加載完成后,這些 IAT 項就變成了實 際的函數地址,即 Function 的意義。

 

一圖勝千言:

 

 這也就是為什么說導入表的是雙橋結構了。

 

1.導入表其實是一個 IMAGE_IMPORT_DESCRIPTOR 的數組,每個導入的 DLL 對應 一個 IMAGE_IMPORT_DESCRIPTOR。

2. IMAGE_IMPORT_DESCRIPTOR 包含兩個 IMAGE_THUNK_DATA 數組,數組中 的每一項對應一個導入函數。

3. 加載前OriginalFirstThunk與FirstThunk的數組都指向名字信息,加載后FirstThunk 數組指向實際的函數地址。

 

好了,回顧了這么多PE導入表知識點,下面看看系統回調。

 

系統回調

 系統回調就是由系統執行回調函數,這個回調函數可以是用戶編寫的,但是必須是由系統調用

 比如下面這幾種

 LoadImageNotifyRoutine            模塊加載回調

 CreateProcessNotifyRoutine        進程創建回調

 CreateThreadNotifyRoutine         線程創建回調

 CmRegisterCallback                     注冊表回調

 IoRegisterFsRegistrationChange 文件系統回調

 ......

 

 由程序員注冊回調,系統函數在觸發條件下調用

 

 所以就提供了注冊模塊加載回調然后獲得修改PE文件的條件

 

 下面看看在回調函數中做了些什么

VOID Start (
    IN PUNICODE_STRING    FullImageName,
    IN HANDLE    ProcessId, // where image is mapped
    IN PIMAGE_INFO    ImageInfo
    )
{
    NTSTATUS ntStatus;
    PIMAGE_IMPORT_DESCRIPTOR pImportNew;
    HANDLE hProcessHandle;
    int nImportDllCount = 0;
    int size;
    IMAGE_IMPORT_DESCRIPTOR Add_ImportDesc;
    PULONG ulAddress;
    ULONG oldCr0;
    ULONG Func;
    PIMAGE_IMPORT_BY_NAME ptmp;
    IMAGE_THUNK_DATA *pOriginalThunkData;
    IMAGE_THUNK_DATA *pFirstThunkData;
    PIMAGE_BOUND_IMPORT_DESCRIPTOR pBoundImport;

    if(wcsstr(FullImageName->Buffer,L"calc.exe")!=NULL)
    {
        lpBuffer = NULL;
        lpDllName = NULL;
        lpExportApi = NULL;
        lpTemp = NULL;
        lpTemp2=NULL;

        g_eprocess = PsGetCurrentProcess();
        g_ulPid = (ULONG)ProcessId;
        ulBaseImage = (ULONG)ImageInfo->ImageBase;// 進程基地址
        pDos = (PIMAGE_DOS_HEADER) ulBaseImage;
        pHeader = (PIMAGE_NT_HEADERS)(ulBaseImage+(ULONG)pDos->e_lfanew);
        pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((ULONG)pHeader->OptionalHeader.DataDirectory[1].VirtualAddress + ulBaseImage);
        nImportDllCount = pHeader->OptionalHeader.DataDirectory[1].Size / sizeof(IMAGE_IMPORT_DESCRIPTOR);
                            // 把原始值保存。
        g_psaveDes = pImportDesc;

        ntStatus = ObOpenObjectByPointer(g_eprocess, OBJ_KERNEL_HANDLE, NULL, PROCESS_ALL_ACCESS , //PROCESS_WRITECOPY
                                    NULL, KernelMode, &hProcessHandle);
        if(!NT_SUCCESS(ntStatus))
            return ;
//      加上一個自己的結構。
        size = sizeof(IMAGE_IMPORT_DESCRIPTOR) * (nImportDllCount + 1);
            //  分配導入表
        ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpBuffer, 0, &size,
                                         MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
        if(!NT_SUCCESS(ntStatus)) 
        {
            ZwClose(hProcessHandle);
            return ;
        }
        RtlZeroMemory(lpBuffer,sizeof(IMAGE_IMPORT_DESCRIPTOR) * (nImportDllCount + 1));
        size = 20;
            // 分配當前進程空間。
        ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpDllName, 0, &size,
                                                 MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
        if(!NT_SUCCESS(ntStatus)) 
        {
            ZwClose(hProcessHandle);
            return ;
        }
        RtlZeroMemory(lpDllName,20);

        size = 20;
        ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpExportApi, 0, &size,
                                                         MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
        if(!NT_SUCCESS(ntStatus)) 
        {
            ZwClose(hProcessHandle);
            return ;
        }
        RtlZeroMemory(lpExportApi,20);
            // 分配當前進程空間。
        size = 20;
        ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpTemp, 0, &size,
                                                                MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
        if(!NT_SUCCESS(ntStatus)) 
        {
            ZwClose(hProcessHandle);
            return ;
        }
        RtlZeroMemory(lpTemp,20);
        // 分配當前進程空間。
        size = 20;
        ntStatus = ZwAllocateVirtualMemory(hProcessHandle, &lpTemp2, 0, &size,
                                                    MEM_COMMIT|MEM_TOP_DOWN, PAGE_EXECUTE_READWRITE);
        if(!NT_SUCCESS(ntStatus)) 
        {
            ZwClose(hProcessHandle);
            return ;
        }
        RtlZeroMemory(lpTemp2,20);

        pImportNew = lpBuffer;
        // 把原來數據保存好。
        RtlCopyMemory(pImportNew , pImportDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR) * nImportDllCount );

            // 構造自己的DLL    IMAGE_IMPORT_DESCRIPTOR結構
                                                                    

        pOriginalThunkData = (PIMAGE_THUNK_DATA)lpTemp;
        pFirstThunkData = (PIMAGE_THUNK_DATA)lpTemp2;

        ptmp = (PIMAGE_IMPORT_BY_NAME)lpExportApi;
        ptmp->Hint = 0;
            // 至少要一個導出API
        RtlCopyMemory(ptmp->Name,"HelloShine",strlen("HelloShine"));
        pOriginalThunkData[0].u1.AddressOfData = (ULONG)ptmp-ulBaseImage;
        pFirstThunkData[0].u1.AddressOfData = (ULONG)ptmp-ulBaseImage;

        Add_ImportDesc.FirstThunk = (ULONG)pFirstThunkData-ulBaseImage;
        Add_ImportDesc.TimeDateStamp = 0;
        Add_ImportDesc.ForwarderChain = 0;
//
// DLL名字的RVA

        RtlCopyMemory(lpDllName,"D:\\Dll.dll",strlen("D:\\Dll.dll"));
        Add_ImportDesc.Name = (ULONG)lpDllName-ulBaseImage;
        Add_ImportDesc.Characteristics = (ULONG)pOriginalThunkData-ulBaseImage;

         pImportNew += (nImportDllCount-1);
         RtlCopyMemory(pImportNew, &Add_ImportDesc, sizeof(IMAGE_IMPORT_DESCRIPTOR));
 
         pImportNew += 1;
         RtlZeroMemory(pImportNew, sizeof(IMAGE_IMPORT_DESCRIPTOR));

        __asm {
            cli;
            mov eax, cr0;
            mov oldCr0, eax;
            and eax, not 10000h;
            mov cr0, eax
            }
        // 改導出表
        pHeader->OptionalHeader.DataDirectory[1].Size += sizeof(IMAGE_IMPORT_DESCRIPTOR);
        pHeader->OptionalHeader.DataDirectory[1].VirtualAddress = (ULONG)( pImportNew - nImportDllCount) - ulBaseImage;

        pBoundImport = (PIMAGE_BOUND_IMPORT_DESCRIPTOR)((ULONG)pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress
            + ulBaseImage);

        if( (ULONG)pBoundImport != ulBaseImage)
        {
            //取消綁定輸入表里的所有東西
            pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].VirtualAddress = 0;
            pHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT].Size = 0;
        }

        __asm {
            mov eax, oldCr0;
            mov cr0, eax;
            sti;
            }

        ZwClose(hProcessHandle);
        hProcessHandle = NULL;
    }
}

  

 *需要注意一點:綁定導入表

   當時實踐的時候怎么都不成功,熬了一晚上最后都沒有結果,真是崩潰,最后再次查看《WindowsPE權威指南》才發現綁定導入表的問題。

   學知識看來總是得多實踐才能發現問題,以前總以為自己知道綁定導入表的問題,可是真正遇到問題就忘了,更坑的是有些問題搜索不到或者寥寥無幾。

IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11

   指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結構數組,對應於這個映像綁定的每個DLL。數組元素中的時間戳允許加載器快速判斷綁定是否是新的。如果不是,加載器忽略綁定信息並且按正常方式解決導入API。

   也就是說,綁定導入是提高PE加載的一項技術,如果PE文件中導入的函數比較多,PE加載速度就會變慢。綁定導入的目的就是把由Windows加載程序負責的IAT地址修正工作提前到之前進行。

   所以也就是說在取消綁定導入表后,強制操作系統按導入表進行導入。那么也就成功了。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM