记录读书心得,开始深入kernel的知识
内核基础
内核通过一些底层的体系架构机制把自己和其他运行的程序隔离开,大部分的指令集体系架构至少提供两种执行模式:权限模式和非权限模式。
内核态中的代码执行拥有所有权限,并可以访问系统中任何有效的内存地址,而在用户态中的代码执行对内存的访问则会受到如前所述的所有限制。
用户态进程和调度
一个cpu在任意指定时间点上只能有一个进程处于活动状态,通常把cpu上的时间片分配给每个进程和快速地将cpu在进程间切换。
内核为每个进程保存并关联一组表示该进程状态的信息——称为上下文信息。
cpu切换进程的操作,叫上下文切换。
调度程序是一个子系统,负责在很多等待运行的任务中挑选一个进程运行并把cpu的使用权赋给这个进程。
虚拟内存
计算机用来存储临时、易变的数据的物理内存是固定的,内存物理地址空间的范围从0到RAM的大小减 1.
现代操作系统为每个运行的进程和不同内核子系统实现了,每个进程或者子系统都以为自己拥有一个很大的私有地址空间。
该虚拟空间大小为: 在一个 n 位架构上,为 0 到 2^n-1.
虚拟内存子系统负责管理虚拟内存地址到物理内存地址的映射,强制隔离不同的地址空间。
操作系统把物理内存地址空间切分成固定大小的块,称为页帧,并把虚拟地址空间切分成同样大小的块,称为页。当一个进程需要某页内存时,虚拟内存管理系统就会给他分配一个物理的页帧。
物理的页帧到虚拟页的地址映射通过页表完成,页表会告诉一个给定的虚拟地址映射到哪个物理页。
一旦分配完所有的物理页,而此时恰好有一个进程需要一个新的物理页,系统就会挑选一个当前没有使用的物理页,然后将其分配给需要的进程。
当需要硬盘交换区上的页面时,操作系统会将另一个内存物理页的内存放到交换区,然后把需要的页面装入空出来的物理内存,这个过程称为交换。
为了改进性能,虚拟内存子系统首先为进程创建一个虚拟地址空间,在此之后只有当一个地址第一次用到时才为该地址所在的虚拟页分配一个物理页,这个方法称为页面调度。
内核漏洞分类
空指针引用
内核栈溢出
内核堆溢出
整数误用
条件竞争
DTrace调试
逻辑bug
内核生成的用户态漏洞
内核成功利用进阶
架构级概览
CPU作用为执行指令。指令包含:逻辑运算指令、控制流指令和内存操作指令。
计算机架构分两类:一类是RISC(精简指令集),这类架构只包含能够在一个时钟周期内执行完毕的简单的、固定数量的指令;
另一类是 CISC(复杂指令集计算机),这种架构包含不同数量的多种操作指令,而且这些指令的执行需要多个时钟周期。
CPU从内存中读取指令的二进制流并根据它的指令集进行译码,然后执行指令。有一个专有寄存器称为指令指针(IP) 或是程序计数器(PC),该寄存器用来追踪当前正在执行的指令。
中断和异常
软件引发的中断是同步的,给定一个特定的执行路径,总是在某个特定的时间发生;
硬件引发的中断是异步的,在任何时间都可能发生,是不可预期的。
CPU提供了一个专有寄存器来追踪一个称为中断向量表的内存地址,中断向量表将特定的中断例程和相应的中断相关联。通过注册例程程序,每次中断发生时都会通知操作系统并且将执行流程转至中断向量表中的存储的内存地址。以此在中断发生时,继续运行。
内存管理
CPU能够按照两种寻址方式:
- 线性寻址:将整个内存看成一个连续的字节序列。该方法可以简单看成物理内存和线性地址的一一映射,或是利用某种技术产生虚拟地址,并将其转化为物理地址(页式寻址是一个典型)
- 段式寻址:将整个内存看成一些不同的段,CPU要指定一块内存地址需要两个寄存器:一个保存段地址(一般保存在一个向量表中,以便根据段号查询到段地址);另一个保存偏移地址。
页式寻址利用称为页的内存单元和页表,页表描述了物理地址和线性地址的映射关系。每个线性地址分成一个或多个部分,分别与页表的相应级一一对应。
1 | | 目录 | 表 | 偏移量 | |
虚拟地址的最后部分(最后12位)指定了一个页的偏移量;最前面部分的虚拟地址指定了一个页表的索引(依赖于页表的结构)。当在指令中使用线性地址的时候,CPU把线性地址送到 MMU(Memory Management Unit,存储管理装置),任务就是运行页表进行映射以得到相应的物理地址。
从虚拟地址到物理地址的转换是必须的,但是这是耗时 的操作。所以,CPU提供了一个缓冲器用于存储最近虚拟地址到物理地址的映射关系,称之为快表(TLB)。TLB的原理非常简单:保存最近转换过的记录以便将来再需要映射相同的地址时无需再去获取物理内存而是去“快表”中直接读取。
由于用户态和内核态之间的切换是非常普通的任务,所以每次进入或跳出内核态都会引发 TLB 的刷新。为了解决这个性能损耗。操作系统实现了组合用户态/内核态地址空间,在每个进程地址空间的顶部都保存有一份内核页表的备份。这些页表的转换(从内核虚拟地址到物理地址)将在快表中标记为全局的并且不可改变。通过只允许有权限的代码对其访问来保护她,每次进程调用内核代码的时候都不需要再改变 TLB(如刷新TLB);如果出于某种原因内核直接在进程上下文中解引用了已经映射的虚拟地址空间,那么将会访问这个进程的内存。
基于x86的32位架构
启动机器后 CPU 将首先切换到保护模式,使操作系统运行在32 位的环境中。
x86-64
AMD64 向上兼容,运行用户不加修改地运行32位地应用程序和操作系统,并且有两种模式
- 传统模式:CPU行为就如32位CPU一样,将所有64位地环境都关掉
- 长模式:这是64位本地运行模式,在这种模式下,32位应用程序仍旧可以不加修改地运行在一个称为兼容模式的模式中。在兼容模式和64位模式之间来回切换既简单又快速。
64位的变化:
- 虚拟地址空间增大,因此仅使用一个子集,即仅有2^48个地址可用。这是通过将剩余的16位设置为第47位的值来实现的。这样就生成了一个从 0x7fffffffffff 到 0xffff800000000000 的虚拟地址范围。系统通常使用这个来区别用户态和内核态,将低地址部分分配给用户态,而将高地址部分分配个内核态。
- 页表表目现在是64位宽,所以每一级包含512个表目,页大小可能是 4096KB\2MB或是 1GB。需要一个新级别的表目,称为 PML4
- 在64位模式中,段式寻址基本已经废除,GS和FS段寄存器虽然存在,但是他们通常只用来保存重要数据结构的偏移地址。经常将GS用于用户态和内核态,这是因为该架构提供了一种基于进入、退出内核的状态而转换其值的方法:SWAPGS。
执行阶段
获取权限:关注内核态中保持进程权限跟踪的结构,并将其提升至超级权限
稳固系统:获取权限后,保持内核的稳定运行
- 栈溢出恢复:如果栈溢出跳转执行了shellcode,此后需要返回一个正常的状态。用户态的进程通过软件陷阱和中断到达内核态。一旦内核执行完成请求服务,就不得不返回控制进程并且恢复它的状态以便在软件陷入以后从下一条指令继续执行。从中断返回的通用方法是使用
IRETQ
指令。该指令执行的第一条指令是从栈中出栈一组值:
1 | tempRIP <- Pop() |
可以将 RIP(64位指令指针)、CS(代码段寄存器)、EFLAGS(存储各种状态信息的寄存器)、RSP(64位栈指针)、SS(栈寄存器)从栈中赋值到临时数据中。
如果内核态或是用户态使用 GS
段寄存器,SWAPGS指令需要在 IRETQ 指令之前执行。这条指令会将GS寄存器中的内容和保存在一个机器特定寄存器(MSR)中的值进行交换。
触发阶段
任意内存重写
重写全局结构函数指针
堆溢出技术:
- 重写相邻对象:相邻的对象中有可以利用的指针
- 重写控制结构:
- 本文作者: A1ex
- 本文链接: http://yoursite.com/2001/03/29/读书笔记-内核漏洞的利用与防范/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!