前言:本专题会从Linux内核系统最基本内存管理开始介绍,并且通过实践来深入理解其理论。在学习了基本的理论知识以后,会进入本专题的正文,来介绍实际的攻击。通过详细的理论分析和实践代码让读者深入理解Linux的管理内存的本质以及Linux攻击行文的实现。在以后的开发过程中更轻松的去管理内存避免漏洞的出现。
Linux分段机制
背景
在8086处理器诞生之前,内存寻址方式就是直接访问物理地址。这就是直观的令人能很好理解的访问模式,cpu中的地址值就是我们要访问的内存的地址值。我们称之为cpu的实模式。
由于8086处理器想要访问1MB(220)的内存,但是以前处理器只有16位(216=64kB),所以在8086时,设计师将CPU的地址总线升级为20位,但是,问题在于ALU只能处理16位数据,这时就想出了新的地址访问方法,就是分段机制。
分段机制:就是将整个一块内存分成几个段:CS, DS, SS, ES(代码段,数据段,堆栈段,附加段)。然后每个段内空间就变小,就可以对内存进行正常访问了。基本原理就是分段越多,段内需要访问的地址范围就越小。
所以,8086采用分段机制以后,物理地址的访问就变成,首先需要知道要访问的是哪个段,那个段的首地址是多少,然后在这个段内我需要访问的位置离首地址有多远。具体实现:物理地址=(段的首地址<<4) +段内偏移
因为寄存器是16位,而CPU总线为20位,所以从段寄存器中读出16位的段首地址放到CPU20位中的高16位。也就是说段首地址一定是24(16k)的倍数。然后再加上段内偏移就是我们CPU需要访问内存的实际地址。
重要问题:为什么是将段首地址向上移动4位而不是16位(整个段的长度)?
因为这里工程师进行设计的时候,只有20位的数据总线位数和16位的寄存器及ALU位数。没有20-16=4位的寄存器。所以工程师要利用16位合成20位。这里就想到了将一个16位放到20位的高16.将另一个16位放到20位的低16位。然后相加就是我们的寻址地址。这样做出现的问题就是每个段并不是独立的,而是重叠的。
举个例子。假设要访问030H地址的内存空间,可以使用段1的首地址:010H,段偏移是020H,则030H=010H+020H。
也可以使用段2的首地址:00H,段偏移是030H,则030H=000H+030H. 总之要想访问某个物理地址,只要凑出合适的段基地址和段内偏移地址,其和为该物理地址就行了。
因为段是重叠的,不同段可以访问到重叠地址的数据,所以在定义段的时候加上了段长数据来使不同段不重合。
随着时代的发展,出现了80386处理器是一个32位处理器,通常我们所说的CPU位数是指ALU可以处理数据的位数,通用寄存器的位数,数据总线的宽度中最小的一位。ALU和地址总线都是32位的,寻址空间达 4G。也就是说它可以不通过分段机制,直接访问4G的内存空间。但是为了兼容以前的访问模式,也为了方便其自己的内存访问,所以该处理器提供了两种内存访问模式:实模式和保护模式(分段机制)。所以其保留了几个分段寄存器CS,DS,SS,ES。
实模式特点:
- CPU刚上电时是出于实模式之下的,CPU访问的地址就是输入的物理。
- 实模式的物理地址=16位段基址<< 4 + 16位段偏移量
- 实模式下对任意段都具有读写权限。
- 实模式下可以访问的内存大小为1M(0x00000-0xfffff)
IA32的内存寻址机制
80386处理器提供的这种具有两种内存访问模式:实模式和保护模式。保留几个16位的分段寄存器的机制就是成为IA32架构。
在 8086 的实模式下,把某一段寄存器左移4位,然后与地址ADDR相加后被直接送到内存总线上,这个相加后的地址就是内存单元的物理地址,而程序中的这个地址就叫逻辑地址(或叫虚地址,就是CPU输出程序员写入的地址)。在IA32的保护模式下,这个逻辑地址不是被直接送到内存总线而是被送到内存管理单元(MMU)。MMU由一个或一组芯片组成,其功能是把逻辑地址映射为物理地址,即进行地址转换,如图所示。
MMU是一种硬件电路,它包含两个部件,一个是分段部件,一个是分页部件,在此,我们把它们分别叫做分段机制和分页机制,以利于从逻辑的角度来理解硬件的实现机制。分段机制把一个逻辑地址转换为线性地址;接着,分页机制把一个线性地址转换为物理地址。
IA32中有六个16位段寄存器:CS, DS, SS, ES,FS, GS.跟8086的段寄存器不同的是,这些寄存器存放的不再是某个段的基地址,而是某个段的选择符(Selector)。
分段机制的实现
1.GDT与GDTR的出现
随着CPU和操作系统的发展,人们对内存访问的要求越来越高,不仅仅要求能够访问到内存,还要求要对内存进行访问权限,段的大小等属性的设置与查看等。所以80386处理器与8086处理器的分段模式并非完全相同的,而是进行了相当大的改进,把分段目的从能访问到更大内存转变成了对内存的访问能进行控制,实现段保护,所以成为保护模式。
但是16位的段寄存器还是存储的信息是完全不够的,所以CPU规定操作系统必须提供一个表,这个表中存储了每个段的的段描述符(段描述符中包括了:段的基地址,段的界限,段的保护属性)。这张表就是我们称的GDT表。所以,我们就减轻段寄存器的工作,只需要其找到这张表上的对应的段描述符既可。所以,每个CPU一定有一个GDT表,同时为了找到这张表,CPU提供一个专门的寄存器来存储这张表的地址,成为GDTR寄存器。
2.段描述符(GDT表项)的解析
段描述符是一个8字节的数据结构。
3.实例分析
1>实验性分析
这个是gdt表的一部分,我们可以看到,第2段和第6段分别是存放64位下的内核代码和用户代码的段信息。其中保存了段大小,段的内存地址以及访问权限等信息。
不过,通过分析,在linux中,不同段的基地址都相同,为0.代表linux只是使用了分段机制的权限保护功能,而没有使用其访问更大内存的功能。
2>源码性分析
GDTR寄存器的值是GDT表的内存地址,是需要linux内核写入的。而GDT表示属于per cpu变量中gdt_page中存储的。per cpu变量是每个CPU独有的变量,是linux系统分配给cpu的。
Linux分页机制
分页的硬件支持
一个逻辑地址经过分段机制转换为一个线性地址之后,便需要分页单元将线性地址转换为实际的物理地址。
从80386开始,所有的80×86处理器都支持分页。是否开启分页通过设置cr0寄存器的PG标志来决定,当PG为0时,表示不开启分页,此时线性地址呗解释为物理地址。
分页机制管理的对象是固定大小的存储块,称之为页(page)。分页机制把整个线性地址空间及整个物理地址空间都看成由页组成,在线性地址空间中的任何一页,可以映射为物理地址空间中的任何一页(我们把物理空间中的一页叫做一个页面或页框(page frame))。
80386中每一页的大小都为4KB,每一页的起始地址都能被4K整除(低12位全为0)。因此,80386把4G的线性地址空间,划分为1M个页面。
把线性地址映射到物理地址的数据结构称为页表,页表存放在主存中,由内核进行适当的初始化。
线性地址转换
一个线性地址被分页机制解析为三部分:目录项(高10位),页表(中间10位),页内偏移量(最低12位)
- cr3寄存器:存储页目录基地址
- 目录项:存储在页目录中的偏移量
- 页表(中间10位):存储在页表内存中的偏移量
- 页内偏移量:存储在页内的偏移量
- 页目录:每一项都是一张页表的基地址
- 页表:每一项都是一个页
- 页:存储数据的地方,每一个页对应一个数据块。
在linux中使用了4级分页模型
- 页全局目录:pgd
- 页上级目录:pud
- 页中间目录:pmd
- 页表:pt
总结
本文介绍了x86架构CPU中MMU中重要得硬件机制。这是我们研究操作系统安全的基础。下面的我们会继续对Linux中的内存管理进行理论分析以及实验探究。为我们的攻击打下基础。
来源:freebuf.com 2020-07-13 13:10:48 by: 夏宇不打伞2020
请登录后发表评论
注册