最近整理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. [loj2135]幻想乡战略游戏

    以1为根建树,令$D_{i}$为$i$子树内所有节点$d_{i}$之和 令$ans_{i}$为节点$i$的答案,令$fa$为$i$的父亲,则$ans_{i}=ans_{fa}+dis(i,fa)(D_ ...

  2. Ubuntu下的磁盘管理

    采用fat的磁盘存储,插入后采用相同命令会出现sdb和sdb1 sdb:磁盘 sdb1:磁盘分区标号为1 命令 df:显示磁盘使用情况 du:查询某个文件的大小读 du-h 或du -h --max- ...

  3. springboot上传文件异常解决方案

    配置文件加入: #配置文件传输 spring.servlet.multipart.enabled =true spring.servlet.multipart.file-size-threshold ...

  4. Jenkins快速上手安装

    目录 环境准备 - JDK 安装 1. APT 安装 2. WAR包方式运行 3.Docker 方式运行 Jenkins 是一个独立的开源自动化服务器,可以用来自动化与构建.测试.交付或部署软件相关的 ...

  5. 关于"丢失的牛"这个题的教学反思

    某天上课讲到这样一个题:丢失的牛1~n,乱序排列,告诉从第二个位置到最后一个位置, 每个位置的前面的数字中比它小的数的个数,求每个位置的数字是多少N<=8000 FormatInput第一行给出 ...

  6. 【.NET 6】使用.NET 6开发minimal api以及依赖注入的实现、VS2022热重载和自动反编译功能的演示

    前言: .net 6 LTS版本发布已经有若干天了.此处做一个关于使用.net 6 开发精简版webapi(minimal api)的入门教程,以及VS2022 上面的两个强大的新技能(热重载.代码自 ...

  7. setoolkit的钓鱼实验

    1.在kali中打开setoolkit 2.在菜单中选择第一个进入社会工程学攻击 3.选择第二个模块属于网站攻击向量 4.选择第五个模块,进行web劫持攻击 5.选择第二个,进行网站克隆 6.发现访问 ...

  8. 入坑 OI 249561092 周年之际的一些感想

    2018.2.10~2021.2.10 又是一年的 2 月 10 日,今天的到来意味着我 OI 生涯的第三年已经结束,即将开启 OI 生涯的第四年了.回顾这三年以来自己由懵懂.无知慢慢变成熟的历程,感 ...

  9. Atcoder Regular Contest 089 D - ColoringBalls(DP)

    Atcoder 题面传送门 & 洛谷题面传送门 神仙题. 在下文中,方便起见,用 R/B 表示颜色序列中球的颜色,用 r/b 表示染色序列中将连续的区间染成的颜色. 首先碰到这一类计算有多少个 ...

  10. LearnPython_week4

    1.装饰器2.生成器3.迭代器4.内置方法5.可序列化6.项目规范化 1.装饰器 # -*- coding:utf-8 -*- # Author:Wong Du ### 原代码 def home(): ...