• 利用模块加载回调函数修改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地址修正工作提前到之前进行。

       所以也就是说在取消绑定导入表后,强制操作系统按导入表进行导入。那么也就成功了。

  • 相关阅读:
    vps云服务器建站后绑定域名的方法?
    怎么做局域网远程联机?
    解决服务器Active Directory环境里Windows登录性能问题办法
    搭建dns域名服务器过程
    服务器电源常见故障判断及处理方法
    有些网站为什么要使用CDN,CDN又是什么呢
    独立服务器使用技巧介绍
    云服务器和虚拟主机的区别
    CDN在中国的发展的九个年头的点点滴滴
    网络安全的攻防战争
  • 原文地址:https://www.cnblogs.com/zibility/p/5657762.html
Copyright © 2020-2023  润新知