20169215 《Linux内核原理与分析》 第十一周作业
设备与模块
设备驱动和设备管理的四种内核成分:
- 设备类型:在所有Unix系统中为了统一普遍设备的操作锁采用的分类。
- 模块:Linux内核中用于按需加载和卸载目标码的机制。
- 内核对象:内核数据结构中支持面向对象的简单操作,还支持维护对象之间的父子关系。
- sysfs:表示系统中设备树的一个文件系统。
Linux设备被分为三种类型:
- 块设备:可以以块为单位寻址,支持重定位操作(对数据的随机访问)。
- 字符设备:不可寻址,仅提供数据的流式访问。
- 网络设备:通过物理适配器和特定协议提供对网络的访问,通过套接字API访问网络设备打破了Unix“一切皆文件”的设计原则。
Linux内核是模块化组成的,允许内核在运行时动态地向其中插入或从中删除代码。
模块的所有初始化函数必须符合int my_init(void)
形式,它是模块的入口点,通过module_init()
例程注册到系统中。退出函数必须符合void my_exit(void)
形式,它是模块的出口函数,由module_exit
例程注册到系统。如果文件被静态编译到内核映像中,退出函数将不被包含和调用(代码不需要从内核中卸载)。
在/drivers的某个目录下的Makefile文件中添加obj-m += fishing/
用来告诉模块构建系统,在编译模块时需要进入fishing/子目录中,然后再对应的fishing/目录下添加新的Makefile文件,添加指令obj-m += fishing.o
然后构建系统运行将会进入fishing/目录下并将fishing.c便以为fishing.ko模块。
编译后的模块将被装入到目录/lib/modules/version/kernel下,kernel/目录下每一个目录都对应着内核源码树中的模块位置。make moudules_install
命令用来安装编译的模块到合适的目录下。
向产生内核以来关系的信息,root用户可运行命令depmod
,只为新模块生成依赖信息可以运行命令depmod -A
。
载入模块最简单的方法是通过insmod
命令。使用方法是以root身份运行命令insmod module.ko
,module.ko是要载入的模块名称。写在一个模块可以使用rmmod
命令,同样要以root身份运行rmod module
。
Linux允许驱动程序声明参数,定义一个模块参数可以通过宏module_param(name, type, perm)
完成。其中name是用户可见的参数名,也是模块中存放模块参数的变量名。参数type存放了参数的类型。参数perm制定了模块在sysfs文件系统下对应文件的权限。如果模块的外部参数名称和对应的内部变量名称不同,就该使用宏module_param_named(name, variable, type, perm)
来定义。
模块载入后,就会被动态的连接到内核。只有被显式导出的外部函数才可以被动态库调用。内核中,到处内核函数需要使用特殊的指令EXPORT_SYMBOL()
和EXPORT_SYMBOL_GPL()
。
2.6内核引入了统一设备模型。该模型提供了一个独立的机制专门表示设备,并描述其在系统中的拓扑结构,使得系统有了一下优点:
- 代码重复最小化。
- 提供诸如引用计数这样的统一机制。
- 可以列举系统中的所有的设备,观察他们的状态,并且查看他们链接的总线。
- 可以将系统中的全部设备结构以树的形式完整,有效地展现出来。
- 可以将设备和其对应的驱动联系起来,反之亦然。
- 可以将设备按照类型加以归类。
- 可以沿设备树的叶子向其根的方向依次遍历,以保证能以正确顺序关闭各设备的电源(最初动机)。
设备模型的核心就是kboject,由struct kobject
结构体表示。kobject对象被关联到ktype,ktype的存在是为了描述一族kobject锁具有的普遍性。kset是kobject对象的集合体,kobject的kset指针指向相应的kset集合,kset结构体中list连接该集合中所有的kobject对象。
kobject通过函数kobject_init()
进行初始化,通过memset()
清空kobject。大多数情况下应该调用kobject_create()
或调用相关辅助函数创建kobject。
kobject提供了一个统一的引用计数系统,增加引用计数获得对象的引用,减少引用计数释放对象的引用。增加引用计数可通过kobject_get()
完成,减少引用计数通过kobject_put()
完成。
sysfs文件系统是一个处于内存中的虚拟文件系统,提供了kobject对象层次结构的视图。sysfs通过kobject对象中的dentry字段把kobject对象与目录项联系起来。sysfs根目录下最重要的目录是devices,该目录将设备模型导出到用户空间。
想要把kobject导入sysfs,需要函数kobject_add()
,删除需要使用函数kobject_del()
。kobject在sysfs中的位置取决于kobject在对象层次结构中的位置。
可移植性
Linux差不多所有的接口和核心代码都是独立于硬件体系结构的C语言代码,但是对性能要求很严格的部分,内核的特性会根据不同的硬件体系进行调整。一般来说暴露在外的内核结构往往是和硬件体系结构无关的。
写可移植性好、简洁、合适的内核代码,要注意两点:
- 编码尽量选取最大公因子:假定任何事情都可能发生,任何潜在的约束也都存在。
- 编码尽量选取最小公约数:不要假定给定的内核特性是可用的,仅仅需要最小的体系结构功能。
编写可移植的代码要考虑字长、数据类型、填充、堆起、字节次序、符号、字节顺序、页大小以及处理器的加载/存储排序。
Linux编码风格
- 缩进风格使用制表位(Tab)每次缩进8个字符长度。
- Linux编码风格规定,空格放在关键字周围,函数名和圆括号之间无空格。函数、宏以及与函数相像的关键字在关键字和圆括号之间没有空格。在括号内参数前后也不加空格。大多数二元或者三元操作符两边加上括号,一元操作符操作数和操作符之间没空格。提领运算符周围加上合适的空格尤为重要。
- 每行代码长度不超过80个字符。
- 命名规范如果局部变量能够清楚地表明它的用途,那么选取idx甚至是i这样的名称都是可行的。全局变量和函数应该选择包含描述性内容的名称,并且使用小写字母,必要时加上下划线区分单词。
- 函数代码长度不应该超过两屏,局部变量不应超过10个。
- 在源码中要减少ifdef的使用。
- 结构初始化时候必须在它的成员前加上结构标识符。
SET-UID程序漏洞实验
Set-UID 是Unix系统中的一个重要的安全机制。当一个Set-UID程序运行的时候,它被假设为具有拥有者的权限。例如,如果程序的拥有者是root,那么任何人运行这个程序时都会获得程序拥有者的权限。
首先把/usr/bin/passwd拷贝到/tmp目录下,会发现文件的权限发生了变化。在/tmp目录下运行的复件没有了修改密码的权限
以root方式登录,拷贝/bin/zsh 到/tmp, 同时设置拷贝到tmp目录下的zsh为set-uid root权限,然后以普通用户登录,运行/tmp/zsh,会发现普通用户通过运行tmp下的zsh获得了root权限
而同样的操作,对于运行tmp目录下的bash不能获得root权限
从上面步骤可以看出,/bin/bash有某种内在的保护机制可以阻止Set-UID机制的滥用。为了能够体验这种内在的保护机制出现之前的情形,我们打算使用另外一种shell程序——/bin/zsh。以下指令会把默认的shell指向zsh:
$sudo su
Password:
\#cd /bin
\#rm sh
\#ln -s zsh sh
system(const char * cmd)
系统调用函数被内嵌到一个程序中执行一个命令,system()调用/bin/sh来执行shell程序,然后shell程序去执行cmd命令。但是在一个Set-UID程序中system()函数调用shell是非常危险的,这是因为shell程序的行为可以被环境变量影响,比如PATH;而这些环境变量可以在用户的控制当中。通过控制这些变量,用心险恶的用户就可以控制Set-UID程序的行为。
下面的Set-UID程序被用来执行/bin/ls命令;然后程序员可以为ls命令使用相对路径,而不是绝对路径。
int main()
{
system("ls");
return 0;
}
把/bin/sh拷贝到/tmp目录下面重命名为ls(先要确保/bin/目录下的sh 符号链接到zsh,而不是bash),将环境变量PATH设置为当前目录/tmp,运行编译的程序test。就可以获得root权限:
修改/bin/sh使得其返回到/bin/bash,重复上面的攻击,运行test程序,就无法获得root权限:
现在我们来研究下system()这个函数。如果查找一些资料,我们很容易就能得知这个函数是十分不提倡在有SUID/SGID权限的程序时使用,因为system()会继承环境变量,通过环境变量可能会造成系统安全的问题。
int system(const char * cmdstring)
{
pid_t pid;
int status;
if(cmdstring == NULL)
{
return (1); //如果cmdstring为空,返回非零值,一般为1
}
if((pid = fork())<0)
{
status = -1; //fork失败,返回-1
}
else if(pid == 0)
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
_exit(127);//exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦~~
}
else //父进程
{
while(waitpid(pid, &status, 0) < 0)
{
if(errno != EINTR)
{
status = -1; //如果waitpid被信号中断,则返回-1
break;
}
}
}
return status; //如果waitpid成功,则返回子进程的返回状态
}
为了更好的理解system()函数返回值,需要了解其执行过程,实际上system()函数执行了三步操作:
- fork一个子进程;
- 在子进程中调用exec函数去执行command;
- 在父进程中调用wait去等待子进程结束。 对于fork失败,system()函数返回-1。 如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。(注意,command顺利执行不代表执行成功,比如command:"rm debuglog.txt",不管文件存不存在该command都顺利执行了) 如果exec执行失败,也即command没有顺利执行,比如被信号中断,或者command命令根本不存在,system()函数返回127. 如果command为NULL,则system()函数返回非0值,一般为1。
system()函数调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell,比如bash,-c选项是告诉shell从字符串command中读取命令; 在该command执行期间,SIGCHLD是被阻塞的,好比在说:hi,内核,这会不要给我送SIGCHLD信号,等我忙完再说; 在该command执行期间,SIGINT和SIGQUIT是被忽略的,意思是进程收到这两个信号后没有任何动作。
下面我们可以来理解为什么会通过system("ls")获得root权限了。我们改变了环境变量,fork产生的子进程继承了这个环境变量,这个子进程相当于重新打开了一个shell而且由于是suid程序,是以root权限创建的子进程,打开的shell也有root权限。但是因为我们修改了环境变量,子进程执行命令时候发现不存在"ls"命令,就会返回当前的子进程,也就是用root权限打开的shell,当然拥有root权限了。
下面研究sytem()和execve()的不同。下面这段代码:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
char *v[3];
if(argc < 2)
{
printf("Please type a file name.\n");
return 1;
}
v[0] = "/bin/cat"; v[1] = argv[1]; v[2] = 0;
//Set q = 0 for Question a, and q = 1 for Question b
int q = 0;
if (q == 0)
{
char *command = malloc(strlen(v[0]) + strlen(v[1]) + 2);
sprintf(command, "%s %s", v[0], v[1]);
system(command);
}
else execve(v[0], v, 0);
return 0 ;
}
程序中的q=0。程序会使用system()调用命令行。这个命令不安全,普通用户可以修改只有root用户才可以运行的一些文件。比如截图中:file文件只有root用户有读写权限,但普通用户通过运行该程序,阅读并重命名了file文件:
修改为q=1后,不会有效。前面步骤之所以有效,是因为system()函数调用/bin/sh,链接至zsh,具有root权限执行了cat file文件后,接着执行mv file file_new命令。而当令q=1, execve()函数会把file; mv file file_new看成是一个文件名,系统会提示不存在这个文件:
为了保证Set-UID程序在LD_PRELOAD环境的操纵下是安全的,动态链接器会忽略环境变量,但是在某些条件下是例外的。
我们建立一个动态链接库。把下面的程序命名为mylib.c,放在/tmp目录下。在UNIX的动态链接库的世界中,LD_PRELOAD就是这样一个环境变量,它可以影响程序的运行时的链接(Runtime linker), 它允许你定义在程序运行前优先加载的动态链接库。这个功能主要就是用来有选择性的载入不同动态链接库中的相同函数。通过这个环境变量,我们可以在主程序和 其动态链接库的中间加载别的动态链接库,甚至覆盖正常的函数库。在函数库libc中重载了sleep函数:
#include <stdio.h>
void sleep (int s)
{
printf("I am not sleeping!\n");
}
我们用下面的命令编译上面的程序:
gcc -fPIC -g -c mylib.c
gcc -shared -Wl,-soname,libmylib.so.1 \-o libmylib.so.1.0.1 mylib.o –lc
-fPIC 作用于编译阶段,告诉编译器产生与位置无关代码(Position-Independent Code)
-shared 表示的是调用动态库
-Wl.option 此选项传递option给连接程序;如果option中间有逗号,就将option分成多个选项,然后传递给会连接程序;
-soname 则指定了动态库的soname(简单共享名,Short for shared object name)
-l 是直接加上某库的名称,如-lc是libc库
把下面的程序命名为myprog.c,放在/tmp目录下:
int main()
{
sleep(1);
return 0;
}
把myprog编译成一个普通用户下的程序在普通用户下运行,它会使用LD_PRELOAD环境变量,重载sleep函数:
把myprog编译成一个Set-UID root的程序在普通用户下运行,在这种情况下,忽略LD_PRELOAD环境变量,不重载sleep函数,使用系统自带的sleep函数:
把myprog编译成一个Set-UID root的程序在root下运行,在这种情况下,使用LD_PRELOAD环境变量,使用重载的sleep函数:
在一个普通用户下把myprog编译成一个Set-UID普通用户的程序在另一个普通用户下运行,在这种情况下,不会重载sleep函数:
由以上四种情况可见:只有用户自己创建的程序自己去运行,才会使用LD_PRELOAD环境变量,重载sleep函数,否则的话忽略LD_PRELOAD环境变量,不会重载sleep函数。
为了更加安全,Set-UID程序通常会调用setuid()系统调用函数永久的清除它们的root权限。然而有些时候,这样做是远远不够的。在root用户下,在/tmp目录新建一个空文件cww。在root用户下将下面代码命名为test.c,放在/tmp目录下,编译这个程序,给这个程序设置root权限。在一个普通的用户下,运行这个程序。
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
int main(){
int fd;
fd = open("/tmp/zzz",O_RDWR|O_APPEND);
sleep(1);
setuid(getuid());
pid_t pid ;
if( ( pid = fork() ) < 0 )
perror("fork error");
else if( pid == 0 ){
// child process
write( fd , "shiyanlou!" , 10 );
}
int status=waitpid(pid,0,0);
close(fd);
return 0;
}
结果如图:
如图所示文件被修改了,原因在于设置uid前,cww文件就已经被打开了。只要将语句setuid(getuid())移至调用open函数之前,就能避免这个问题。
实验总结
通过本次试验复习了修改文件权限的相关内容,同时也了解到了SET-UID程序的漏洞,通过这些漏洞可以使本来没有权限的用户获得他们本不该有的权限。