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

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

 PE文件在運行時會根據導入表來進行dll庫的「動態連接」,那麼若是咱們修改PE導入表結構,就能夠實現對咱們本身動態庫的導入,從而實現注入。程序員

 

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

 每個模塊加載前都能觸發SetLoadImageNotifyRoutine註冊的回調函數,而後得到PE文件基地址,構造PE文件就能夠實現注入了。app

 

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

 

 PE文件導入表this

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

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地址修正工做提早到以前進行。

   因此也就是說在取消綁定導入表後,強制操做系統按導入表進行導入。那麼也就成功了。

相關文章
相關標籤/搜索