首页
社区
课程
招聘
[原创]利用内核知识,自己实现ReadProcessMemory
发表于: 2018-2-3 23:55 32351

[原创]利用内核知识,自己实现ReadProcessMemory

2018-2-3 23:55
32351

转眼来到科锐学习已经超过一年的时间了,眼看三阶段已经进入尾声,内核的学习也快要结束,记录一下笔记和心得,也给刚接触的朋友做一个参考。当然,学习新知识最好的办法就是带着目的来学习,所以在文章后半部分,介绍如何自己实现了Windows的一个API:ReadProcessMemory,为什么要选这个API呢?首先在软件保护上,任何开发者都不会希望别人可以随意查看自己的内存内容,于是多数人会选择在这个函数上挂钩,监视并保护内存,那么我们可以绕过他的保护,用自己的函数来查看进程内存。

环境介绍:真机64位Windows10,虚拟机:32位XP_sp3

Windows算是微内核和宏内核特点都具有的操作系统。

8086处理器有三种工作模式,分别是:实模式,保护模式,虚拟86模式,其中关系为:
8086处理器有三种工作模式

16位汇编中 iret可以进入保护模式。

如何保证操作内存的动作是否合法?

操作系统通电后进入实模式,做了一系列初始化的动作后进入到保护模式,在保护模式中,CPU执行所有和内存有关的操作都会通过查表来确定操作是否合法,这个表就是GDT和LDT表,表的格式由CPU厂商决定,所以为了能兼容多款CPU,操作系统代码里多用条件宏来实现。

首先通过逻辑地址的偏移查第一次表得到线性地址,再查第二次表得到物理地址。为什么要查第二次表呢?因为第二张表实际上是为了实现虚拟内存,那么就是说这段内存可能是在磁盘上的,访问的时候会先查表,然后从磁盘上调到内存中,有些情况下(关闭了虚拟内存)查第一次表的结果等价于物理地址。

从逻辑地址转换到线性地址,是一块平坦且连续的地址,实际上对应到物理地址上,并不是连续的。

例如:

这个表也叫做分段表,结构如下图:

这种奇葩的做法来源于为了兼容286的历史遗留问题。

用于表示上述定义段的三个参数的数据结构称为描述符。每个描述符长8个字节。在保护方式下,每一个段都有一个相应的描述符来描述。

存储段是存放可由程序直接进行访问的代码和数据的段。存储段描述符描述存储段,所以存储段描述符也被称为代码和数据段描述符。

描述符是一个8个字节的结构,具体结构如下:
图片描述

整个的解析结构如下图:
图片描述

Privl(2bit):优先级位,取值从0-3,对应Ring0-Ring3级别。

Ex:可执行位,为1时表示此描述符对应是代码段,为0时为数据段。

每个任务的局部描述符表LDT含有该任务自己的代码段、数据段和堆栈段的描述符,也包含该任务所使用的一些门描述符,如任务门和调用门描述符等。随着任务的切换,系统当前的局部描述符表LDT也随之切换。

全局描述符表GDT含有每一个任务都可能或可以访问的段的描述符,通常包含描述操作系统所使用的代码段、数据段和堆栈段的描述符,也包含多种特殊数据段描述符,如各个用于描述任务LDT的特殊数据段等。在任务切换时,并不切换GDT。

通过LDT可以使各个任务私有的各个段与其它任务相隔离,从而达到受保护的目的。通过GDT可以使各任务都需要使用的段能够被共享。

GDT存储在GDTR寄存器, 通过汇编指令LGDT载入。它的操作码是一个结构的地址,这个结构描述GDT的大小和地址。共6个字节,如下:
图片描述

LDT存在LDTR寄存器中,存有局部进程的描述符表,LDTR中的内容根据线程的切换不停切换,表中的内容由操作系统来修改,若我们拿到0环权限,自己修改LDTR,改到目标进程,那么修改自己的内存就相当于修改了目标进程的内存,这是内核修改的一个经典招式。

在保护方式下,虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分组成。段选择子长16位,在32位程序下,CPU的段寄存器中保存的就是选择子,其格式如下表所示:
图片描述

现有逻辑地址:23:13ac34b,假如段寄存器中的的选择子Index为:0000000000100,RPL:11,先比对,是三环程序,继续操作,去LDT表中的第4项拿到段首地址,加上偏移13ac34b,得到线性地址。

值得一提的是Windows中并没有使用LDT,而Linux是使用了LDT的,但是有意思的是在阅读Windows源码时发现微软也留下了LDT的接口,难道微软想什么时候顺便兼容一下Linux?

80386开始支持存储器分页管理机制。分页机制是存储器管理机制的第二部分。上述的段管理机制实现虚拟地址(由段和偏移构成的逻辑地址)到线性地址的转换,分页管理机制实现线性地址到物理地址的转换。

线性地址到物理地址的转换方式受很多变量的影响,我们先以一个其中最具代表性的方式来讲解基本概念和转换流程,再来总结所有的转换方式。

流程如图所示采用了二级表的结构:
图片描述

一级表称为页目录表(Page Directory Entry),共有1024(1k)个表项,每个表项的大小是4bit,总大小为4k,表项内容包括了页表的指针和指向页表的属性。

二级表称为页表(Page Table Entry),每张页表里有1024(1k)个表项,每个表项的大小是4bit,总大小为4k,最多有1024(1k)张页表,最大占用空间为4M,而操作系统一般是动态申请页表,大小大概在1M左右。表项内容包括了物理地址的指针和属性。

控制寄存器CR3的高20位存放了指向页目录表的指针(这里存的是物理地址,如果这里存虚拟地址就会产生悖论),每个进程都会有一张PDE,切换进程其实就是CPU在切换CR3的值,这一点非常重要,是我们自己实现ReadProcessMemory的基础!

PDE和PTE的表项结构基本相似,略有差别,如下图所示:

结构基本类似,高20位存指向目标首地址,低12位表示指向目标的属性

图片描述

如何搭建双机调试环境,请自行谷歌

我们以GDT地址作为例子:
寄存器环境:
图片描述

gdt内容:
图片描述

dd :查看虚拟内存地址

现在我们有线性地址:0x8003f000,CR3:39000

!dd:查看物理内存地址, 0x200 * 4是因为表项是4个字节

以上我们所阐述的线性地址转物理地址的方法适用于没有物理地址拓展,且页表大小为4k的情况。

查表方式根据页表大小来决定,而页表大小根据以下标志决定:

PS :页表尺寸,PDE表项中的第7位

图片描述

以下图片均来自Intel手册,详细解释请参考手册

图片描述

图片描述

当符合页表大小是4M的情况下,只需要查一次PDE表再加上偏移就能得出物理地址。

开启PAE时,寻址的方式有所不同,CR3里保存的不再是PDE的首地址,而是一个保存了PDE指针的表的首地址,这张表我们称作页目录指针表(PDPT),对于线性地址的拆分也有所不同,高两位作为了PDPT的索引。

图片描述

当开启PAE且页表大小是4k的情况,需要查三次表,线性地址的21-29位作为PDE表的索引,12-20位作为PTE表的索引

图片描述

说了这么多关于表的格式和查表的方法,在实践中我们该如何利用呢?一个用处是我们经常需要把虚拟地址转换成物理地址,明白其转换原理利于分析问题,另外上面说了,每个进程都会有一套自己的分页机制,切换进程实际上是切换CR3中的内容,那么如何实现我们自己的ReadProcessMemory呢?

问题变成了以下4步:

我们知道在3环程序里,fs[0]保存的是线程环境块TEB,在0环,保存的则是处理器控制区(_KPCR),部分内核数据结构如下图:

从_EPROCESS中的进程链表我们可以遍历所有进程,当匹配到目标进程时,拿出
目标进程DirectoryTableBase中保存的地址。

切换CR3的值之前,我们需要屏蔽调当前CPU核心的中断,以防线程切换,如果是多核的CPU,每个核心都需要屏蔽掉中断。同时,为了预防内存属性不可写,暂时改掉CR0中表示所有内存属性的标志,让所有内存暂时都可写。

申请一段空间,暂存一下读取的数据,记得要检查目标内存地址是否有效

恢复内存属性,恢复中断

内核的学习也开始进入尾声,温故而知新,整理知识本身也是一种学习的过程。衷心感谢一年多以来钱老师,张老师,姚老师,戚老师,王老师,唐老师的指导。要毕业了,帮科锐宣传下,科锐30期正在招生中

 
 
 
 
 
 
 
 
 
 
 
 
NTSTATUS GetProcessDirBase(IN DWORD dwPID, OUT PDWORD pDirBase)
{
    PEPROCESS Process;
    PEPROCESS CurProcess;
    CHAR  *pszImageName;
    DWORD dwCurPID;
    DWORD i;

    __try
    {        
        __asm
        {
            //ETHREAD
            mov eax, fs:[124h] 
            //Current EPROCESS 
            mov eax, [eax + 44h] 
            mov Process, eax
        }

        CurProcess = Process;
        i = 0;
        //traversing  EPROCESS
        do
        {
            pszImageName = (char*)CurProcess + 0x174;
            dwCurPID = (*(DWORD*)((char*)CurProcess + 0x084));

            if (dwCurPID == dwPID)
            {
                *pDirBase = (*(DWORD*)((char*)CurProcess + 0x018));
                return STATUS_SUCCESS;
            }
             //Next
            CurProcess = (*(DWORD*)((char*)CurProcess + 0x088)) - 0x88;
        } while (CurProcess != Process);
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        dprintf("[MyReadProcessMemory] GetProcessDirBase __except \r\n");
    }

    return STATUS_INVALID_DEVICE_REQUEST;
}
        __asm
        {
            //Shielding interrupt
            cli     
            //close memory protect         
            mov eax, cr0                
            and eax, not 10000h
            mov cr0, eax
            mov eax, cr3
            mov dwOldDirBase, eax
            //swap CR3
            mov eax, dwDirBase
            mov cr3, eax
        }
        //Alloc ring0 Buff
        char* szRing0Buf = (char*)MmAllocateNonCachedMemory(dwBufSize);
        //check address invalid
        if (MmIsAddressValid(dwTargetAdddress))
        {
            RtlCopyMemory(szRing0Buf, dwTargetAdddress, dwBufSize);
            bIsRead = TRUE;
        }
        __asm
        {
            mov eax, dwOldDirBase
            mov cr3, eax
            //Reset  memory protect
            mov eax, cr0                
            or  eax, 10000h
            mov cr0, eax
            //Restore interrupt
            sti                         
        }

环境介绍:真机64位Windows10,虚拟机:32位XP_sp3

16位汇编中 iret可以进入保护模式。

这种奇葩的做法来源于为了兼容286的历史遗留问题。

值得一提的是Windows中并没有使用LDT,而Linux是使用了LDT的,但是有意思的是在阅读Windows源码时发现微软也留下了LDT的接口,难道微软想什么时候顺便兼容一下Linux?

如何搭建双机调试环境,请自行谷歌

以下图片均来自Intel手册,详细解释请参考手册

  • 介绍内核基础知识
  • 自己实现ReadProcessMemory
  • 宏内核:追求性能,大部分系统代码放在0环,代表:Linux
  • 微内核:追求维护性,大部分系统代码在3环,代表:Minix(Linux作者老师的作品),其中著名的设计:动态链接库,在Windows中也使用

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
免费 3
支持
分享
最新回复 (46)
雪    币: 23080
活跃值: (3432)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
2
干货很多,希望以后多来分享!
2018-2-4 10:08
0
雪    币: 58
活跃值: (76)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
3
666666666,向大佬学习,以后大佬的笔记就是我的工具书?
2018-2-4 14:25
0
雪    币: 5
活跃值: (11)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
4
很详细,收走了
2018-2-4 14:25
0
雪    币: 1
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
看了帖子感觉学到了很多遗漏的东西。对自己的知识又有了补充。
2018-2-4 16:45
0
雪    币: 5676
活跃值: (1303)
能力值: ( LV17,RANK:1185 )
在线值:
发帖
回帖
粉丝
6
这个强
2018-2-4 19:22
0
雪    币: 43
活跃值: (388)
能力值: ( LV9,RANK:140 )
在线值:
发帖
回帖
粉丝
7
我的方案是改pte  map远程进程的内存到本地进程。想干啥就干啥
2018-2-4 22:16
0
雪    币: 575
活跃值: (112)
能力值: ( LV5,RANK:140 )
在线值:
发帖
回帖
粉丝
8
malokch 我的方案是改pte map远程进程的内存到本地进程。想干啥就干啥
想了下,确实是个好方法
2018-2-4 22:34
0
雪    币: 48
活跃值: (85)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
9
围观
2018-2-4 22:38
0
雪    币: 1485
活跃值: (1135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
难道没有办法防?
2018-2-5 09:48
0
雪    币: 12848
活跃值: (9147)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
11
冰雄 难道没有办法防?
有,硬件虚拟化,直接保护物理地址,不过处理很复杂就是了
顺便这篇文章没有涉及到一点64位内存管理的东西,全程在讲32位,放到2004年大家都在用xp的年代或许不错。但是今天已经是2018年了,至少也该讲讲64位的东西了吧
2018-2-5 10:48
0
雪    币: 1485
活跃值: (1135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
12
HOOK这个RtlCopyMemory函数也是可以防吧
2018-2-5 11:03
0
雪    币: 1485
活跃值: (1135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
13
最看到利用VT可以突破任何  驱动防护。。
2018-2-5 11:04
0
雪    币: 300
活跃值: (2477)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
14
mark
2018-2-5 14:03
0
雪    币: 12848
活跃值: (9147)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
15
冰雄 最看到利用VT可以突破任何 驱动防护。。
本来就是矛和盾的故事,在同样的竞争下没有谁一定会更强一点,一般来说都是哪边花的精力/钱多哪边就更强
2018-2-5 14:18
0
雪    币: 1485
活跃值: (1135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
16
@hzqst    随着内核的深入学习,发现目前主流的杀软也太弱  了  。。测试了一下。通过PID  就kill  TX管家。大公司的安全软件都那样。小公司怎么生存。任人鱼肉了
  MyKillProcess(HANDLE  Pid)
{
       HANDLE  hProcess;
       NTSTATUS  ret=  MyOpenProcess(Pid,&hProcess,1);
       MyTerminateProcess(hProcess);
       return  ret;
}
2018-2-5 14:34
0
雪    币: 12848
活跃值: (9147)
能力值: ( LV9,RANK:280 )
在线值:
发帖
回帖
粉丝
17
冰雄 @hzqst 随着内核的深入学习,发现目前主流的杀软也太弱 了 。。测试了一下。通过PID 就kill TX管家。大公司的安全软件都那样。小公司怎么生存。任人鱼肉了 MyKillProcess( ...
1、你kill的是GUI进程,主防进程并么有kill掉
2、如果你kill的是主防进程,那么你一定加载自己的驱动了。你加载驱动的时候大数字拦截并询问了吗,你大概是点了允许加载驱动吧。
2018-2-5 15:08
0
雪    币: 5039
活跃值: (2626)
能力值: ( LV12,RANK:290 )
在线值:
发帖
回帖
粉丝
18
hzqst 1、你kill的是GUI进程,主防进程并么有kill掉 2、如果你kill的是主防进程,那么你一定加载自己的驱动了。你加载驱动的时候大数字拦截并询问了吗,你大概是点了允许加载驱动吧。
Bypass  Zhudongfangyu  x64  可没那么好搞~
2018-2-5 16:01
0
雪    币: 697
活跃值: (60)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
Mark
2018-2-20 10:44
0
雪    币: 6818
活跃值: (153)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
20
2018-2-20 21:18
0
雪    币: 78
活跃值: (1875)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
21
2018-2-24 16:07
0
雪    币: 70
活跃值: (72)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
22
”屏蔽调当前CPU核心的中断,以防线程切换,如果是多核的CPU,每个核心都需要屏蔽掉中断“


我只有一个问题,假设待读的内存页当前处于被交换状态你该如何处理,我只粗略看了一下你的4个小标题。

如果是我 我大概会这样做,先挂靠目标进程,然后映射当前进程空间的待读内存到内核然后锁一下(如果嫌麻烦就直接拷贝一份到内核空间),再切回原进程之后去读那个映射/拷贝,最后回收一下资源。


挂靠目标进程是个比较大的过程,估计是做了 插apc,修改当前线程所属,陷入等待 唤醒之类的一系列玩意组成的(具体我没看,猜测是这个过程),

切CR3 也只是 KiSwapProcess 里面的小小一环,暴力的代价很大(以前测过,最少有一半的目标进程内存是读不到的),为了那一点可有可无的隐蔽性,老实说很亏。

如果说仅仅是为了读带保护属性的内存也有其它更好的解决思路。

最后于 2018-2-27 04:22 被FadeTrack编辑 ,原因:
2018-2-27 04:06
0
雪    币: 199
活跃值: (65)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
23
mark一下,up主写的还是很辛苦的    鼓励  鼓励
2018-2-28 21:17
0
雪    币: 58
活跃值: (1135)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
24
都是一些老方法了,现在的新系统都不行了。加上win10的的VBS功能,这些东西都没什么用武之地了。
2018-2-28 22:50
0
雪    币: 273
活跃值: (472)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
去看看TP大保护的DXF你就晓得你这样根本就读不到内存因为页面已经给你  替换了  哈哈.
2018-3-1 06:04
0
游客
登录 | 注册 方可回帖
返回
//