探索黑客技术攻防,实战研究与安全创新

导航菜单

Windows中的跨进程数据操作

大家都知道,现代操作系统不同进程之间的地址空间是互相隔离的,比如进程1和进程2同是地址0x40000000,其对应的内容并不相同。但如果你用过金山游侠,会知道在该软件里,选择一个要挂接的进程,就可以查看被挂接进程的内存,这是如何实现的呢?这就涉及到Windows中的跨进程数据操作。

为了搞清楚Windows是如何实现跨进程数据操作的,首先我们要搞清Windows中进程之间是如何实现隔离的。所谓的隔离,就是两个进程用户空间的隔离(因为每个进程的内核空间是一致的,所以不涉及到隔离),如进程1与进程2中同样是0x40000000的地址,其存储的内容并不相同,Windows实现进程间隔离的实质在于经过页目录/页表映射后,其背后对应的物理内存并不相同,如图1所示。

D1.png

按说这种设计理念就是为了保证进程之间的独立性:一个进程就像自家院子,管好自己这一摊子就可以了(当然除了公用的部分),但是在Windows的实际实现中,往往还会有超出这种规矩的需求,比如进程1需要读取/修改进程2地址空间中的数据,问题也恰恰在于Windows提供了满足这种需求的机制。从根本上说,读取/修改数据就是要读取/修改对应物理内存中的数据,隔离的实质是通过页目录/页表的映射,将同一地址映射为不同的物理地址,即有了页目录/页表的映射机制,导致我们无法定位到真实物理内存中的地址,一个很自然的思路是如果进程1使用进程2的页目录/页表,那么不就相当于可以直接读取/修改进程2中的内容了吗?实际上,Windows实现跨进程操作根本的思路也是如此,下面我们看看Windows的实现。

在此我们有必要回顾一下Windows中的寻址机制。在我的文章“一次艰辛的寻址之旅”中,我们看到每个进程都有单独的页目录/页表,如图2所示,CR3寄存器里存储的内容实际上就是页目录的物理地址,由页目录进而过渡到页表,直至具体的页面。如果CR3指向的地址不同,就会导致选用不同的页目录,从而页表,具体的页面也就会完全的不同。因此进程切换,为了更改到新进程的地址空间,就需要更改CPU中的CR3寄存器,从而切换到新进程的页目录/页表,完成地址空间的切换。说得直白一些,页目录/页表的不同在于CR3中的内容,修改CR3,也就更改了页目录/页表,实际上也就更改了进程的地址空间,因此可以说CR3是寻址的原点。CR3寄存器只有一个,在CPU里,当进程1中的线程处于运行期间,CR3中存放的是进程1页目录的物理地址,当需要切换到另外一个进程2中的线程时,系统获取进程2对应的进程管理结构KPROCESS中的DirectoryTableBase[0]成员内容,这个内容就是该目标进程得到调度时需要填充入CR3寄存器中的值,然后将该数值放于eax中,其后,使用指令MOVCR3,EAX切换CR3,完成进程地址空间的切换。

D2.png

下面我们看下Windows在实现跨进程操作中的具体实现。在Windows中,跨进程数据操作最典型的应用是ReadProcessMemory和WriteProcessMemory两个函数,这两个函数在具体实现上非常相似,下面我们以WriteProcessMemory函数为例,其原型如下:


WriteProcessMemory
(hProcess,lpBaseAddress,lpBuffer,nSize,lpNumberOfBytesWritten)


其中hProcess表示目标进程的句柄,lpBaseAddress表示要写入的起始地址,lpBuffer表示写入的缓存区地址,nSize表示要写入缓存区的大小,lpNumberOfBytesWritten表示返回实际写入的字节。

为了实现这个函数,我们需要解决两个问题:

1)进程1的触角如何伸到进程2的空间;

2)如何将进程1需要写入的数据(lpBuffer地址中的数据)传递到进程2的用户空间。

对于问题1,思路是这样的,当进程1执行WriteProcessMemory函数代码时,进入内核空间,而内核代码空间是进程1和进程2共有的,在内核空间进程1中的代码通过目标进程的句柄,得到保存在该进程对应结构KPROCESS中的DirectoryTableBase[0]内容,并将该内容填充到CR3,进而更改CR3,完成地址空间的切换,此时代码停留在公用部分,但是其用户空间已经转化为进程2的,因此此时的内核代码对用户空间地址的操作就是对进程2的用户地址空间的操作。当然,一个进程对另外一个进程的操作是一件很有风险的工作,毕竟是人家的地盘,容易产生矛盾,制造不安定因素,所以Windows在进程调用跨进程操作,会对发起者的身份权限进行严格的审查,这里就不赘述了。

对于问题2,数据的传递要将待写入数据(lpBuffer地址中的数据)写入进程2的用户空间,显然在两个进程之间直接拷贝,如将数据从0x40000000(进程1)à0x40000000(进程2),是不可能实现的,实际使用的方法只有一个途径,就是使用进程间的公共部分——内核空间,其思路是将进程1中的待拷贝数据映射或拷贝进入进程2的内核空间,当切换CR3后,再将这部分数据拷贝进进程2的用户空间地址中。那好,思路有了,我们结合图3将WriteProcessMemory的实现过程简要概括如下:

D3.png

1)利用MDL技术,将用户进程空间的数据映射到内核空间,这样的结果是该数据在用户空间和内核空间各有一个地址,减少了一次将用户空间数据拷贝入内核空间的操作。

2)切换CR3,进入进程2的地址空间;

3)将待拷贝的数据拷入进程2的用户空间的目的地址,完成本次操作后,切换回CR3,恢复进程1的地址空间。

注意,这些动作都是在WriteProcessMemory函数的内核空间部分中实现的,正因为是在内核空间,才能够有权利对用户和内核空间的数据进行拷贝映射。如果简略的描述Windows中跨进程操作的实质,就是将实际执行的代码和需要传递的数据都从进程1过渡到

进程的公用空间——内核空间中,再切换CR3,完成地址空间的切换,从而实现数据的跨进程操作。

(完)