一、前言
我们在Linux的安全机制、现有的认证协议等基础上,提出了基于Linux操作系统的用户认证机制,基本上完成了该课题的研究。本文重点分析了Kerberos认证系统与LDAP目录服务系统的消息格式、数据库管理、安装配置、配置文件、接口函数等。搭建了认证系统,实现了用户登录的认证。我们利用编写的客户端应用接口,用户可以完成上述认证.
近几年来,Linux操作系统以其高效性、灵活性以及开放性得到了蓬勃发展,不仅被广泛应用于PC、服务器,还广泛的应用于手机、PDA等高端嵌入设备。但是,目前的Linux版本在安全方面还存在着许多不足,其安全级别低于C2级。其新功能的不断加入及安全机制的错误配置或错误使用,都会带来很多问题。出于系统安全考虑,Linux提供的安全机制主要有:身份标识与鉴别、文件访问控制、特权管理、安全审计、IPC资源的访问控制。基于上面的事实,我们着重于研究了可插入身份认证模块,即为PAM机制。当用户在登录Linux时,首先要通过系统的PAM验证。PAM机制可以用来动态地改变身份验证的方法和要求,允许身份认证模块按需要被加载到内核中,模块在加入后即可用于对用户进行身份认证,而不需要重新编译其它公用程序。PAM体系结构的模块化设计及其定义的良好接口,使得无需改变或者干扰任何现有的登录服务就可以集成范围广泛的认证和授权机制,因此,近年来,对PAM的底层鉴别模块的扩展广泛应用于增强Linux操作系统的安全性
二、启动到登录界面的流程
1.. start_kernel介绍
这个函数从开始到cpu_idle(). 这个阶段主要是对系统的”经济基础”,即各种资源的初始化,仅由主cpu进行,到最后执行 init函数,再创建 init 进程。
start_kernel 中部分代码:
asmlinkage void __init start_kernel(void)
{
……
kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
// 执行 init函数,再创建 init 进程 。
……
}
2.. init介绍
Init()的执行,这是对系统的“上层建筑”的初始化。此段还是由主cpu执行。函数init()的代码在init/main.c中,这个函数本身并不长,但实际作用很大。我们现在关心的重点是init函数最后执行的 execve()函数部分,此部分的执行就是系统的第一个进程 init 真正执行。
static int init(void * unused)
{
…….
execve("/sbin/init",argv_init,envp_init); //red hat 9.0 中有此程序。此程序执行/etc/inittab 。
execve("/etc/init",argv_init,envp_init);
execve("/bin/init",argv_init,envp_init);
……..
}
3. /etc/inittab文件: [运行getty进程]
由注释可知道,该文件是init进程的要执行的文件。其中有系统的运行级别的配置,
和getty进程的启动,在此我们关心的是 getty 进程。现在getty进程就启动了。
# inittab This file describes how the INIT process should set up
# the system in a certain run-level.
…….
# Run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
…….
//respwan:表示如果本行的命令进程终止后,init进程应该马上重新启动相应的进程;
4.. getty进程
1) . 打开终端命令行。
2) . 输出 login提示符
getty进程 中显示login:提示的代码:
#define LOGIN " login: "/* login prompt */
/* do_prompt - show login prompt, optionally preceded by /etc/issue contents */
Void do_prompt(op, tp) // (显示提示)
{
……
(void) write(1, LOGIN, sizeof(LOGIN) - 1);/* always show login prompt */
//把 login: 提示符写到终端上,1代表标准输出,即终端。
……
}
3) . 执行login程序。
Getty程序中调用(void) execl(options.login, options.login, "--", logname, NULL);
execl()再调用__execve (path, (char *const *) argv, __environ); 执行进程调度,
就启动了,login进程。
三、LOGIN程序:
1.两个重要的数据结构:
passwd 结构 和spwd结构
2.Passwd的结构:
Passwd 数据结构是存放 /etc/passwd 中的数据的。Login程序根据输入的用户名,得到相应的数据。
/* The passwd structure. */
struct passwd *pwd;
struct passwd
{
char *pw_name;/* Username. */
char *pw_passwd;/* Password. */
__uid_t pw_uid;/* User ID. */
__gid_t pw_gid;/* Group ID. */
char *pw_gecos;/* Real name. */
char *pw_dir;/* Home directory. */
char *pw_shell;/* Shell program. */
};
3.Spwd的结构。
Spwd 数据结构是存放 /etc/shadow 中的数据的。Login程序要获取其中的密文。
struct spwd *sp;
struct spwd
{
char *sp_namp; /* login name */
char *sp_pwdp; /* encrypted password */
sptime sp_lstchg; /* date of last change */
sptime sp_min; /* minimum number of days between changes */
sptime sp_max; /* maximum number of days between changes */
sptime sp_warn; /* number of days of warning before password
expires */
sptime sp_inact; /* number of days after password expires
until the account becomes unusable. */
sptime sp_expire; /* days since 1/1/70 until account expires*/
unsigned long sp_flag; /* reserved for future use */
};
4.获得用户名.
if (*argv) { // 检查 login时是否提供了用户名,比如是 login 还是 login litao
char *p = *argv;
username = strdup(p); // 获得登录的用户名, strdup(p): 将串拷贝到新建的位置处 .
ask = 0;
/* wipe name - some people mistype their password here */
/* (of course we are too late, but perhaps this helps a little ..) */
while(*p)
*p++ = ' ';
} else
ask = 1; 运行 login时没有提供用户名,则置需要向用户提问要用户名的标志
5.读取/etc/passwd ,与/etc/shadow文件。
if (ask) {
fflag = 0;
getloginname(); 在终端上输出“login: ”,获得用户登录名,由全局变量 username
/* (void)strcpy(tbuf, username); why was this here? */
if ((pwd = getpwnam(username))) { //从/etc/passwd中获得与登录用户相关信息。
# ifdef SHADOW_PWD
struct spwd *sp;
if ((sp = getspnam(username))) //读取/etc/shadow中数据。存放到 sp中。
pwd->pw_passwd = sp->sp_pwdp;
# endif
6.显示password: 获得密码
pp = getpass(_("Password: ")); //char * getpass (prompt) :输出提示符,并读口令。
7.检测该用户的登录shell 是否为 /etc/nologin
if (pwd == NULL || pwd->pw_uid)
checknologin(); //检查有无/etc/nologin 文件,如果有则表示禁止该用户登录,输出///etc/nologin 中的内容
8.比较两个密文:
if (pwd && !strcmp(p, pwd->pw_passwd)) // 比较两个密文是否相同
break; // 相同则 break 出 L694行的循环,即认证通过,到 L829 行运行
printf(_("Login incorrect\n")); //输出“Login incorrect”,表示密码不对
badlogin(username); /* log ALL bad logins */ //在 syslog 中记录登录失败记录
failures++;
/* we allow 10 tries, but after 3 we start backing off */
if (++cnt > 3) {
if (cnt >= 10) {
sleepexit(1); // 超过 10次登录都失败,则退出 login
}
sleep((unsigned int)((cnt - 3) * 5));
四、Login中的加密算法 crypt
1.Crypt介绍
接下来我们把目光放在Login中的char *crypt(const char *key,const char *salt)函数以及具体的密码比对方法上。
我们都知道,在信息安全领域中,系统安全性是非常非常重要的。如果轻易让别人获得了root权限,那我们的系统将完全落入别人的掌控之中。所以,为了提高安全性,Linux在用户登录验证的过程中,采用了一些特别的方法。比如加密存放用户密码,或者使用嵌入式认证模块——PAM。这里,我们首先讨论一下密码加密的问题。
2.Crypt加密原理
加密函数char *crypt(const char *key,const char *salt)
显示它包含有两个参数。其中一个是key,另一个就是salt。这里的key是一个真正的明文密码,而其中的参数salt,是一个允许干扰乱用的辅助加密的字符串。不仅如此,参数salt还决定了加密函数crypt()所采用的加密算法是什么。
crypt将用户的key和salt一起适应某种算法进行加密(散列)
crypt中可以使用多种加密(散列)机制,包括最初的DES,还有后来为提高安全性引入的md5,blowfish,sha-256,sha-512.
crypt为支持不同的方式,将salt进行格式化,格式为:
$id$salt$encoded (这也是保存在密码文件中的格式)
这里不同id代表不同的算法,不同算法salt的长度也不同。
IdMethod实际加密后的密码长度
1MD5(12个salt字符)22
2aBlowfish只在某些发行版中支持
5SHA-256(12个salt字符)43
6SHA-512(12个salt字符)86
另外DES算法的salt仅由两个字符组成,这两个字符应该在[a-zA-Z0-9./]中选择。然后将用户key与salt拼接成一个新的字符串,用这个字符串作为密钥对某个原始串(通常为全0)进行DES加密,得到11个字符,然后将这11个字符接到salt后面即为用户加密后的密码。
3./etc/shadow文件密码分析。
比如我的系统etc/shadow密码为:
我的系统中密码的格式为:
第一、二个$之间的为使用的加密算法类型标示,第二、三个$之间的部分为salt,第三个$后面的部分为加密后的密码。
由$1$可以知道,crypt使用MD5算法。
根据以上的分析, Login的具体验证方法如下所示:
1, 根据用户名调用getspnam获取对应的spwd项。
2, 根据用户输入的密码key,调用crypt(key,spwd->sp_pwdp)(其中sp_pwdp中前面的部分包含salt的值)得到加密后的值encoded_str。
3, 将encoded_str与spwd->sp_pwdp进行对比,如果相等,则通过验证。
五、嵌入式认证模块——PAM
1.PAM起源
为安全起见,计算机系统只有经过授权的合法用户才能访问,在这里,如何正确的鉴别用户的真实身份是一个关键问题。其中无论是比较出名的Kerberos或者基于智能卡的鉴别系统,都有一个通病:实现鉴别功能的代码通常作为应用程序的一部分而编译。这就直接导致了一个问题:如果发现所用算法存在某些缺陷或者想用另一种鉴别方法时,用户将不得不重写(修改或者替换)然后重新编译源程序。很明显,这样的做法是非常不灵活的。
为了改善这些问题,人们开始思考其他的方法。至此,嵌入式认证模块(Pluggable Authentication Modules)应运而生了。
2.Linux-PAM的分层体系结构
PAM为了实现其插件功能和易用性,采取了分层设计思想:让各鉴别模块从应用程序中独立出来,然后通过PAM API作为两者的联系纽带。以此实现“鉴别功能,随需应变”。
PAM 体系如下图所示:
3.Linux-PAM的应用
系统管理者可以通过两种形式对Linux-PAM进行配置:单一配置文件/etc/pam.conf和etc/pam/d目录。
由于单一文件配置和基于目录的配置语法是几乎一样的,并且通过目录配置的方法具有更大的灵活性,所以我们就着重说说基于目录的配置形式。
例如:
我的/etc/pam.d/login文件内容如下:
添加一条规则如下:
account required lib/security/pam_access.so accessfile=/etc/login.conf。
这条规则的含义是用户使用pam_access模板,通过配置文件/etc/login.conf来对用户访问进行控制,accessfile参数之处了配置文件的路径是/etc/login.conf,在redhat中没有 /etc/login.conf, 需用命令 vi /etc/login.conf 新建。
添加实验用户; aaa 和bbb,
我们的目的是只允许root用户从本地登录,并且只允许wang用户从222.215.220.136远程登录系统,其他用户不允许登录本系统。则我们可以配置文件内容为:
然后退出保存。
其中配置文件含义如下:
+:root:LOCAL含义:
表示允许root用户从本地登录。
+:wang:222.215.220.136
表示允许用户wang从IP:222.215.220.136远程登录本机。
-:ALL:ALL
表示拒绝其他任何人登录本机。
由此,便可以达到只允许root用户从本地登录,并且只允许wang用户从222.215.220.136远程登录系统,其他用户不允许登录本系统的目的了。
六、附录:
[1] 李洋. Linux安全策略与实例[M] . 北京 :机械工程出版社 ,2009。
[2] 毛德操. Linux内核情景分析[M]. 北京 :机械工程出版社 ,2009。
[3] http://www.searchsecurity.com.cn/showcontent_28788.htm
[4] http://www.eefocus.com/article/09-06/75265s.html