以前写过一个,但是一不小心删除了,哎,就当再次复习复习吧。

首先抛出一个有意思的问题:

已知所有Windows可执行文件exe都会链接子系统ntdll.dll,那么真实内存中有几份ntdll.dll?

答案很显然只有一份!所有的exe都是映射的同一份物理内存!

那么又有一个新问题:

我如果对一个进程下的ntdll.dll导出表进行hook,作用域范围是当前进程还是所有进程呢?

有很多人在没有实践下或许会认为是所有进程,可是真正结果是作用域只是在本进程内!

为什么会出现这种情况呢?不是内存中只有一份ntdll.dll吗?

其实原因就在于Windows的copy on write机制!

copy on write 大致原理:

应用程序启动多个实例,这多个进程的地址空间中,应用程序部分地址空间对应到physical storage上,都是一份(memory map的)。所以,如果一个实例尝试修改一个全局或静态变量时,如果没有copy-on-write,这个变量就会被污染。所以windows在使用 memory map装载应用程序时,对全局和静态变量识别,然后给存放这些变量的page设上copy-on-write属性,这样当一个实例尝试修改一个全局或静态 变量时,一个exceptin产生,windows捕获然后重新分配page出来给该实例使用,从而避免了污染。DLL的处理方式和这个是一模一样的。

也就是说当有程序改变公用的dll内存页时,操作系统会copy出来一份一模一样的来让程序write!所以要想实现理想中的全局EAT,必须得禁用copy-on-write!

那么如何禁用copy-on-write呢?

首先我们必须得了解Windows的内存管理以及虚拟内存与物理内存的转换过程,因为开启/关闭copy-on-write是页表PTE的一个标志位。

虚拟内存与物理内存的转换:

下面以32位Windows操作系统,未开启PAE物理地址扩展为例,分析下Windows虚拟地址与物理地址的转换问题:

首先我们看看intel手册的经典图例:

一个内存地址如图4字节32位,高10位代表页面录得索引,中间10位代表页表的索引,最后低12位代表页内偏移。

Windows的页表与页目录是连续的,可以看成一维数组,数组每一项的大小是4个字节。

0xC0300000 页目录即被映射到这个地址上,页目录本身为一页,具有1024个页目录项,每一项为4字节,整个页目录的大小为4k。每一个页目录项都描述一张页表。
0xC0000000 所有的页表都将从该地址开始映射,每4kb为一个页表,每个页表1024个项,每项描述4k的内存页,因此每个页表应当描述: 1024个页表项*4kb = 4M ,4M大小的内存,共1024张页表,因此这些页表描述了整个4G内存地址空间。而每个页表项与页目录项一样,仅占用4个字节的地址空间,因此每一张页表的占内存的大小为: 1024个页表项*4字节 = 4KB。

那么我们就可以得到一个换算公式:

1.PDE     = PDBR[Directory];
2.PTE      = PDE + Table * 4;
3.PA       = PTE + Offset;

在Windbg上完全可以很简单的得到验证。

下面我们来看一看具体获得PDE与PTE的代码

Pde = ( ((VirtualAddress)>>20) & 0xFFC ) + 0xC0300000;
Pte = ( ((VirtualAddress)>>10) & 0x003FFFFC ) + 0xC0000000;

似乎与我们所推论的不太一样,为什么Pte的计算不需要用到pde?

这是因为物理地址是不能直接访问的。也就是这个所在页表的起始物理地址(PDE),是不能访问的,因此我们没有办法通过得到的PDE地址去访问一个页表。既然页表都访问不了,那么久不可能获得一个PTE。所以这时候就需要进行变通了。所以用到了一个很有意思的数学问题:

我们可以看到:
当虚拟地址寻址页目录为第0项的时候,其页表地址其实是从第0张页表开始的,
当虚拟地址寻址页目录为第1项的时候,其页表地址其实是从第1张页表开始的。
当虚拟地址寻址页目录为第2项的时候,其页表地址其实是从第2张页表开始的。

接下来我们可以得知:
当虚拟地址寻址页目录为第0项的时候,其页表地址实际跨越了0个页表项,
当虚拟地址寻址页目录为第1项的时候,其页表地址实际跨越了1024个页表项。(跨过了第0张页表的1024个页表项)
当虚拟地址寻址页目录为第2项的时候,其页表地址实际跨越了2048个页表项。(跨过了第0张页表的1024个页表项和第1张页表的1024个页表项)

现在我们来试着计算一个地址: 
假设地址: 0x00401000
该地址二进制形式为:10000000001000000000000
取其高10bit 0000000001   (页目录牵引第一项)
取其中10bit 0000000001   (页表牵引  第一项)
其低12bit为0,即页对齐,这里为了简单点说明,因此没有页内偏移。

我们按照上面得出的结论,以页表项为最小单位进行计算,0000000001为第一个页目录项,即其指向了第一张页表,针对所有页表的总基址而言,这张页表的所处位置已经跨越了1024个页表项(属于第0张页表的1024个页表项),所以我们可以写出:
0000000001 * 1024个页表项 * 4(每个页表项4字节) = 4096个字节。
即相对于所有页表的总基址0xC0000000的偏移。

现在我们得到了页表的地址,接下来再看中10bit 的0000000001,其含义为我们需要的是这张页表中的第1项,因为不是使用第0项,所以我们略过第个0页表项所占用的4个字节。取第一项即得到该虚拟地址真实所在的内存页物理地址。

总结为:
页表项相对页表总基址的偏移为 = 4096个字节 + 4个字节 = 4100个字节。
好,接下来我们用作者的方式来计算这个偏移:
0x00401000 右移 12位后 等于 0x401
0x401 * 4 = 0x1004 = 4100个字节。

结果是很明显的,是正确的。

页目录与页表

的结构:

得到的页表后只需要把第1位置为1就可以停止此页面的copy on write机制了

NTSTATUS DispatchIoctl(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST;
PCOPY_ON_WRITE_MEMORY pMM ;
ULONG ulMemSize=0;
PVOID pMem=0;
ULONG ulPDEB=0;
ULONG ulPTEB=0;
BOOLEAN bPDEB=FALSE;
BOOLEAN bPTEB=FALSE;
ULONG ulPDE=0;
ULONG ulPTE=0;
BOOLEAN bPDE=FALSE;
BOOLEAN bPTE=FALSE;
BOOLEAN PAE = FALSE; // 取得IRP的I/O堆栈指针
PIO_STACK_LOCATION pIrpStack = IoGetCurrentIrpStackLocation(pIrp); // 取得I/O控制码
ULONG uIoControlCode = pIrpStack->Parameters.DeviceIoControl.IoControlCode;
// 取得I/O缓冲区指针和它的长度
PVOID pIoBuffer = pIrp->AssociatedIrp.SystemBuffer;
ULONG uInSize = pIrpStack->Parameters.DeviceIoControl.InputBufferLength;
ULONG uOutSize = pIrpStack->Parameters.DeviceIoControl.OutputBufferLength;
// 获取缓冲区
pMM = (PCOPY_ON_WRITE_MEMORY)pIoBuffer;
// 获取待修改数据区地址和大小
pMem = pMM->Base;
ulMemSize = pMM->Size; DbgPrint("memory : 0X%08X \n", pMem); PAE=ExIsProcessorFeaturePresent(PF_PAE_ENABLED);
if(PAE==TRUE)
{
DbgPrint("PAE page mode.\n");
// 按照PAE page mode尝试计算PDE和PTE,并查看虚拟地址是否在同一页面
//要修改的地址起始处
ulPDEB = ( (((ULONG)pMem)>>18) & 0x3FF8 ) + 0xC0600000;
ulPTEB = ( (((ULONG)pMem)>>9) & 0x7FFFF8 ) + 0xC0000000;
bPDEB = MmIsAddressValid((PVOID)ulPDEB);
bPTEB = MmIsAddressValid((PVOID)ulPTEB);
//要修改的地址后边界
ulPDE = ( ((((ULONG)pMem+ulMemSize))>>18) & 0x3FF8 ) + 0xC0600000;
ulPTE = ( ((((ULONG)pMem+ulMemSize))>>9) & 0x7FFFF8 ) + 0xC0000000;
bPDE = MmIsAddressValid((PVOID)ulPDE);
bPTE = MmIsAddressValid((PVOID)ulPTE);
if ((bPDEB && bPTEB && bPTE))
{
DbgPrint("PDE(%d) : 0X%08X -> 0X%08X\n", bPDEB, ulPDEB, *(PULONG)ulPDEB);
DbgPrint("PTE(%d) : 0X%08X -> 0X%08X\n", bPTEB, ulPTEB,ulPTE );
}
else
return STATUS_UNSUCCESSFUL;
}
else
{
DbgPrint("Non PAE page mode.\n");
// 按照Non PAE page mode尝试计算PDE和PTE
ulPDEB = ( (((ULONG)pMem)>>20) & 0xFFC ) + 0xC0300000; //cr3寄存器起始地址
ulPTEB = ( (((ULONG)pMem)>>10) & 0x3FFFFC ) + 0xC0000000;
bPDEB = MmIsAddressValid((PVOID)ulPDEB);
bPTEB = MmIsAddressValid((PVOID)ulPTEB); ulPDE = ( (((ULONG)pMem+ulMemSize)>>20) & 0xFFC ) + 0xC0300000;
ulPTE = ( (((ULONG)pMem+ulMemSize)>>10) & 0x3FFFFC ) + 0xC0000000;
bPDE = MmIsAddressValid((PVOID)ulPDE);
bPTE = MmIsAddressValid((PVOID)ulPTE); if ((bPDEB && bPTEB && bPTE))
{
DbgPrint("PDE(%d) : 0X%08X -> 0X%08X\n", bPDEB, ulPDEB, *(PULONG)ulPDEB);
DbgPrint("PTE(%d) : 0X%08X -> 0X%08X\n", bPTEB, ulPTEB, *(PULONG)ulPTEB);
}
else
return STATUS_UNSUCCESSFUL;
} switch(uIoControlCode)
{
case IOCTL_MODIFY_MEMORY:
{
// 禁用指定内存的Copy on write机制
if (bPTE==bPTEB)//物理页面是否存在有效
{
*(PULONG)ulPTEB |=0x00000002; //修改PTE使指定页Copy on write机制失效
DbgPrint("The copy-on-write attrib in address 0X%08X has been forbidden!\n", pMem);
status = STATUS_SUCCESS;
}
else
{
*(PULONG)ulPTEB |=0x00000002;
*(PULONG)ulPTE |=0x00000002;
DbgPrint("The copy-on-write attrib has been forbidden!\n");
status = STATUS_SUCCESS;
}
break;
}
case IOCTL_RECOVER_MEMORY:
{
// 恢复指定内存的Copy on write机制
if (bPTE==bPTEB)
{
*(PULONG)ulPTEB &= ~0x00000002; //修改PTE恢复指定页的Copy on write机制
status = STATUS_SUCCESS;
}
else
{
*(PULONG)ulPTEB &= ~0x00000002;
*(PULONG)ulPTE &= ~0x00000002;
status = STATUS_SUCCESS;
}
break;
}
} if(status == STATUS_SUCCESS)
pIrp->IoStatus.Information = uOutSize;
else
pIrp->IoStatus.Information = 0;
// 完成请求
pIrp->IoStatus.Status = status;
IoCompleteRequest(pIrp, IO_NO_INCREMENT); return status;
}

一般情况下,每个进程都加载了Kernel32.dll这个模块,并且绝大多数情况下Kernel32在每个进程中所加载的基址都一样,在物理内存中,也只有一份Kernel32的映象,所以可以让用户程序LoadLibrary后,把Kernel32的基地址发到Ring 0的驱动程序中,让驱动程序来修改相应PTE,禁了Copy-On-Write后再修改相应的API指令就行了,但是,为了防止某种可能,比如:之前有一个进程也对它进行了写操作,让系统中有了两份或多份Kernel32的映象,而在用户级LoadLibrary,最多只可能修改到某一个映象,所以,我从内核中枚举了所有的EPROCESS结构,再根据PEB_LDR_DATA结构中找到它的所加载的模信息,对其修改。 直接操作各个进程地址空间的数据,很不方便,可以用Windows 未公开API,KeAttachProcess, 函数来切换到指定进程的内存上下文环境。把CreateProcessW的入口处改成了JMP XXX,但是,跳到哪去呢?程序工作在Ring 0下,CreateProcessW不可能直接那里边的一个函数中的,但是,PE文件中每个节都会存在一些“空洞”,kernel32也不例外,就把代码Copy到Kernel32的某个节区的“空洞”中去吧。如果“空洞”太小,怎么办呢?可以把我们的代码写成一个DLL,在那个“空洞”中放上一小段代码来Load这个DLL。

 

禁用copy on write实现全局EAT HOOK的更多相关文章

  1. C#使用全局钩子(hook),SetWindowsHookEx返回0、不回调的解决

    http://www.csharpwin.com/csharpspace/3766r5747.shtml 在.net 2005平台下 在使用全局hook时,总是遇见SetWindowsHookEx的返 ...

  2. MFC线程钩子和全局钩子[HOOK DLL]

    第一部分:API函数简介 1.       SetWindowsHookEx函数 函数原型 HHOOK SetWindowsHookEx( int idHook,        // hook typ ...

  3. ***CI异常记录到日志:CodeIgniter中设计一个全局exception hook

    在CodeIgniter中,当发生异常时,经常要通知系统管理员,因此有必要在全局的高度上 捕捉异常,因此可以写一个hook, 比如在config目录的hook.php中,加入: $hook['pre_ ...

  4. texbox 禁用copy paster cut

    <TextBox CommandManager.PreviewExecuted="textBox_PreviewExecuted" ContextMenu="{x: ...

  5. EAT/IAT Hook

    标 题: EAT/IAT Hook 作 者: Y4ng 时 间: 2013-08-21 链 接: http://www.cnblogs.com/Y4ng/p/EAT_IAT_HOOK.html #in ...

  6. hook 9大类

    HOOK技术主要分为两大类,一是内核层HOOK,一是用户层HOOK. 用户层HOOK也就是在ring3环境下hook kenerl32.dll.User3.dll.Gui32.dll.Advapi.d ...

  7. Hook基本知识

    一.什么是HOOK(钩子) Windows系统,建立在事件驱动机制上,就是整个系统都是通过消息传递实现的.hook(钩子)是一种特殊的消息处理机制,它可以监视系统或者进程中的各种事件消息,截获发往目标 ...

  8. Hook入门

    Hook入门 2014-07-24 基本概念 Windows消息机制 Hook(钩子) 运行机制 核心函数 C# hook示例 基本概念[1] Windows消息机制[5] Windows操作系统是建 ...

  9. 进程隐藏与进程保护(SSDT Hook 实现)(一)

    读了这篇文章终于明白大致怎么回事了 文章目录:                   1. 引子 – Hook 技术: 2. SSDT 简介: 3. 应用层调用 Win32 API 的完整执行流程: 4 ...

随机推荐

  1. [cf1495D]BFS Trees

    记$d_{G}(x,y)$表示无向图$G$中从$x$到$y$的最短路,设给定的图为$G=(V,E)$,$T$为其生成树,$E_{T}$为$T$的边集 下面,考虑计算$f(x,y)$-- 首先,对于一棵 ...

  2. [luogu7092]计数题

    由于$\mu(i)$,因此每一个素数最多存在1次,当$k=0$答案必然为0 根据莫比乌斯和欧拉函数的积性,答案与对素数的划分无关,仅与每一个素数是否出现有关,换言之枚举素数出现的集合$P'$,答案即为 ...

  3. 【Design Patterns】(1)概述

    设计模式 -- 概述 2019-07-17  22:43:32  by冲冲 1. 简介 ① 设计模式 是软件开发人员在软件开发过程中,针对一般问题的最佳解决方案,该方案能够被程序员反复应用于解决类似问 ...

  4. npm 配置 registry 以及使用 nrm

    由于众所周知的原因,我们的内网链接互联网时非常不稳定,速度慢而且经常下载失败.为了提高下载安装 npm 包的体验,很多人都会把 npm 的 registry 配置成国内镜像,我们一般用的比较多的就是淘 ...

  5. 一次奇怪的的bug排查过程

    公司对底层基础库进行了重构,线上稳定跑了几天,在查看订单系统的log时,有几条error信息非常的奇怪, orderID:80320180 statemachine error: no event [ ...

  6. No 'Access-Control-Allow-Origin' header: 跨域问题踩坑记录

    前言 前两周在服务器上部署一个系统时,遇到了跨域问题,这也不是第一次遇到跨域问题了,本来以为解决起来会很顺利,没想到解决过程中遇到了很多坑,所以觉得有必要写一篇博客记录一下这个坑. 问题产生原因 本来 ...

  7. 学军中学csp-noip2020模拟5

    Problem List(其实这几场全是附中出的) 这场比赛的题目相当有价值,特别是前两题,相当的巧妙. A.路径二进制 数据范围这么小,当然是搜索. \(30pts:\)大力搜索出奇迹,最后统计答案 ...

  8. 【R】如何将重复行转化为多列(一对一转化一对多)?

    目录 需求 方法一 方法二 需求 一个数据框一列或多列中有重复行,如何将它的重复行转化为多列?即本来两列一对一的关系,如何转化为一对多的关系?普通的spread函数实现较为麻烦. 示例数据如下: It ...

  9. c/c++在线编译Output Limit Exceeded(OLE)错误

    提示输出错误,有如下两个可能情况: 1. 不符合题目给出的输出格式,自己输出了多余的内容或者格式不正确 2. 输入数据的时候,未考虑到输入错误的情况 针对2,有如下的例子: 错误的情况: 1 int ...

  10. Vue3 中有哪些值得深究的知识点?

    众所周知,前端技术一直更新很快,这不 vue3 也问世这么久了,今天就来给大家分享下vue3中值得注意的知识点.喜欢的话建议收藏,点个关注! 1.createApp vue2 和 vue3 在创建实例 ...