最近整理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地址修正工作提前到之前进行。

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

利用模块加载回调函数修改PE导入表实现注入的更多相关文章

  1. 学习 easyui 之二:jQuery 的 ready 函数和 easyloader 的加载回调函数

    Ready 事件不一定 ready 使用 easyloader 的时候,必须要注意到脚本的加载时机问题,easyloader 会异步加载模块,所以,你使用的模块不一定已经加载了.比如下面的代码. &l ...

  2. js模块化/js模块加载器/js模块打包器

    之前对这几个概念一直记得很模糊,也无法用自己的语言表达出来,今天看了大神的文章,尝试根据自己的理解总结一下,算是一篇读后感. 大神的文章:http://www.css88.com/archives/7 ...

  3. 实现 如 goole closure 类似功能模块加载函数

    看过goole  closure 的同学都知道  其中定义一个类名函数时候只要  inlude("")   想加载某个模块只要require("")就可以利用: ...

  4. 0x02 Python logging模块利用配置加载logger

    目录 logging模块利用配置加载logger 方式一模板:logging.config.dictConfig(config_dict) logging模块利用配置加载logger logging. ...

  5. js与AMD模块加载

    目的: 了解AMD规范与CMD规范,写一个模块加载器雏形. 基本概念: AMD是异步模块定义规范,而CMD是通用模块定义规范.其他的还有CommonJS Modules规范. 对于具体的规范,可以参考 ...

  6. javascript中的闭包、模块与模块加载

    一.前言 闭包是基于词法作用域(  和动态作用域对应,词法作用域是由你写代码时,将变量写在哪里来决定的,因此当词法分析器处理代码时,会保持作用)书写代码时所产生的自然结果,甚至不需要为了利用闭包而有意 ...

  7. 第三课:sea.js模块加载原理

    模块加载,其实就是把js分成很多个模块,便于开发和维护.因此加载很多js模块的时候,需要动态的加载,以便提高用户体验. 在介绍模块加载库之前,先介绍一个方法. 动态加载js方法: function l ...

  8. 第三章:模块加载系统(requirejs)

    任何一门语言在大规模应用阶段,必然要经历拆分模块的过程.便于维护与团队协作,与java走的最近的dojo率先引入加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现. ...

  9. 关于前端JS模块加载器实现的一些细节

    最近工作需要,实现一个特定环境的模块加载方案,实现过程中有一些技术细节不解,便参考 了一些项目的api设计约定与实现,记录下来备忘. 本文不探讨为什么实现模块化,以及模块化相关的规范,直接考虑一些技术 ...

随机推荐

  1. 容器安全产品Aqua调研

    前言 近年来,随着云计算的发展,企业数字化的进程不断加快,业务纷纷开始上云,云原生的概念最近两年也是十分火热,在新业务场景下也随之产生了新的安全问题,如k8s安全.devsecops.微服务安全.Se ...

  2. Redis | 第一部分:数据结构与对象 下篇《Redis设计与实现》

    目录 前言 1. Redis对象概述 1.1 对象的定义 2. 字符串对象 3. 列表对象 3.1 quicklist 快速链表 4. 哈希对象 5. 集合对象 6. 有序集合对象 7. Redis对 ...

  3. 【IDEA】IntelliJ IDEA 2020.1破解版

    IntelliJ IDEA 2020.1破解版 2020-09-09  14:58:56  by冲冲 安装链接: 1. 百度网盘下载地址链接:https://pan.baidu.com/s/1cxjz ...

  4. appdata 文件夹

    appdata file AppData 的位置在 c:\Users\{UserName}\Appdata ,它是从 Windows Vista 开始引入的,直至今天的 Windows 7, 8, 1 ...

  5. Codeforces 506E - Mr. Kitayuta's Gift(神仙矩阵乘法)

    Codeforces 题目传送门 & 洛谷题目传送门 神仙题 %%%%%%%%%%%%% u1s1 感觉这道题风格很省选( 下记 \(m=|s|\),首先探讨 \(n+m\) 为偶数的情形. ...

  6. Perl语言入门14-17

    ---------第十四章 字符串与排序------------------- index查找子字符串 my $stuff = "howdy world!"; my $where ...

  7. 标准非STL容器 : bitset

    1. 概念 什么是"标准非STL容器"?标准非STL容器是指"可以认为它们是容器,但是他们并不满足STL容器的所有要求".前文提到的容器适配器stack.que ...

  8. day04 orm操作

    day04 orm操作 昨日内容回顾 小白必会三板斧 request对象方法 静态文件 请求方式 python链接数据库 django链接数据库 小白必会三板斧 HttpResponse :返回前端浏 ...

  9. win10产品密钥 win10永久激活密钥(可激活win10所有版本)

    https://www.win7w.com/win10jihuo/18178.html#download 很多人都在找2019最新win10永久激活码,其实win10激活码不管版本新旧都是通用的,也就 ...

  10. 转 Android中Activity的启动模式(LaunchMode)和使用场景

    转载请注明出处:http://blog.csdn.net/sinat_14849739/article/details/78072401本文出自Shawpoo的专栏我的简书:简书 一.为什么需要启动模 ...