Linux系统编程:(4)用户与组、密码加密与认证

一、用户与组

在讨论进程凭证之前,我们先来搞清楚用户与组的概念。每个用户都拥有一个唯一的用户名和一个与之相关的数值型用户标识符(UID),一个用户可以隶属于一个或者多个组。每个组也都有唯一的名称和一个组标识符(GID)。用户与组的主要用途包括以下两个方面:确定各种系统资源的所有权对赋予进程访问上述资源的权限加以控制

1. 密码文件:/etc/passwd

在这里插入图片描述
针对系统中每个用户账号,系统密码文件/etc/passwd会专列一行进行描述,每行有7个字段,分别代表以下所述:

  • 登录名:被视为人类可读的标识符,与数字用户标识符对应
  • 经过加密的密码:长度13字符,超过长度不允许登录,如果系统启用shadow密码,系统不会在/etc/passwd文件中解析该字段,以"x"字符占位,经过加密处理的密码实际上存储到shadow密码文件中。如果/etc/passwd文件中密码字段为空,该账户就无需密码登录,即便是启用了shadow密码
  • 用户ID(UID):用户数值型ID,允许同一用户ID对应多个不同的用户名,这样多个用户便可以以不同的密码访问相同的资源。
  • 组ID(GID)
  • 注释:描述性文字
  • 主目录:用户登录后所处的初始路径
  • 登录shell:一般是/bin/bash

2. shadowm密码文件:/etc/shadow

在unix中,之前/etc/passwd中维护所有的用户信息,包括加密处理的密码,因为许多非特权级别系统工具需要读取密码文件中其他的信息,密码文件不得不对所有的用户开放读取权限,这就为密码破解工具提供了可乘之机,他们可以利用同样的工具将可能的密码经过加密得出的加密密码与/etc/passwd中对应的加密密码比对,从而有可能获取密码。为了防止类似攻击,/etc/shadow出现了。
其理念是:用户的所有非敏感信息存放于“人人可读”的密码文件中,而经过加密处理的密码则有shadow密码文件单独维护,仅供具有特权的程序读取。

/etc/shadow 文件内容
登录名:经过加密的密码:其他字段

3. 组文件:/etc/group

系统中的每个组在组文件/etc/group中都对应着一条记录,每条记录包含4个字段。

组名:加密密码:组ID:用户列表在这里插入图片描述

4. 获取用户与组的信息

  • 从密码文件/etc/passwd中获取记录
    在这里插入图片描述
    上述函数分别以登录名和UID为参数,返回密码文件中对应的记录信息:
    Linux系统编程:(4)用户与组、密码加密与认证_第1张图片
    仅当未启用shadow密码的情况下,pw_passwd字段才会包含有效信息。确定shadow密码是否启用,可以在调用getpwnam(name)之后,紧接着调用getspnam(name),看看是否会返回一条shadow密码记录。

注意: getpwnam和getpwuid返回的指针指向由静态分配而成的内存,故二者不可重入。因为getpw函数在程序中自定义一块静态存储区,每调用一次getpw函数,这个静态存储区就会被重写一次,比如以下代码示例:

int main(int argc,char *argv[]) {
	struct passwd *mistr;
    struct passwd *rootstr;

    if(NULL == (mistr=getpwnam("michael")))
    err_exit("[getpwnam]:");
    if(NULL == (rootstr=getpwnam("root")))
    err_exit("[getpwnam]:");
    
    printf("name = %s\n",mistr->pw_name);
    printf("name = %s\n",rootstr->pw_name);
 
    return 0;
}

本来想要获取michael用户和roor用户的信息,创建两个指针,分贝指向两个返回结果,结果发现打印信息都是root,说明后面调用的getpwnam函数重写了struct passwd这个结构体,把前面调用getpwnam函数时写入的michael用户的信息给覆盖了。

  • 从组文件/etc/group中获取记录
    在这里插入图片描述
    以组名或者组ID获取组文件中记录:
    Linux系统编程:(4)用户与组、密码加密与认证_第2张图片
    与前述密码文件/etc/passwd函数一样,对于getgr两个函数的任何一次调用都会改写前一次调用返回该结构结果的内容

  • 从shadow文件/etc/shadow中获取记录
    在这里插入图片描述

5. 密码加密和用户认证

UNIX系统采用单向加密算法对密码进行加密,这意味着由密码的加密形式将无法还原出原始密码。因此,验证候选密码的唯一方法是使用同一算法对其进行加密,并且加密结果与存储在/etc/shadow文件中的密码进行匹配。加密算法封装在crypt()函数之中。
在这里插入图片描述
crypt函数返回的加密字符串为静态分配而成,内容即为经过加密处理的密码,因为静态分配内存,所以连续两次调用,后续调用会覆盖前面的加密密码结果。要想在Linux中使用crypt(),在编译程序的时候需要开启-lcrypt选项,以便程序链接crypt库。

加密程序示例:

/* Compile with -lcrypt */
#if ! defined(__sun)
#define _BSD_SOURCE     /* Get getpass() declaration from  */
#ifndef _XOPEN_SOURCE
#define _XOPEN_SOURCE   /* Get crypt() declaration from  */
#endif
#endif
#include 
#include 
#include 
#include 
#include "tlpi_hdr.h"
 
int main(int argc, char *argv[])
{
    char *username, *password, *encrypted, *p;
    struct passwd *pwd;
    struct spwd *spwd;
    Boolean authOk;
    size_t len;
	long lnmax;
 
   	/* Determine size of buffer required for a username, and allocate it */
   	lnmax = sysconf(_SC_LOGIN_NAME_MAX);
   	if (lnmax == -1)                    /* If limit is indeterminate */
       	lnmax = 256;                    /* make a guess */
	
	username = malloc(lnmax);
    if (username == NULL)
        errExit("malloc");

    printf("Username: ");
    fflush(stdout);
    if (fgets(username, lnmax, stdin) == NULL)
        exit(EXIT_FAILURE);             /* Exit on EOF */

    len = strlen(username);
    if (username[len - 1] == '\n')
        username[len - 1] = '\0';       /* Remove trailing '\n' */

   /* Look up password and shadow password records for username */
	pwd = getpwnam(username);
    if (pwd == NULL)
        fatal("couldn't get password record");
    spwd = getspnam(username);
    if (spwd == NULL && errno == EACCES)
        fatal("no permission to read shadow password file");

    if (spwd != NULL)           /* If there is a shadow password record */
        pwd->pw_passwd = spwd->sp_pwdp;     /* Use the shadow password */

    password = getpass("Password: ");

    /* Encrypt password and erase cleartext version immediately */
    encrypted = crypt(password, pwd->pw_passwd);
    for (p = password; *p != '\0'; )
        *p++ = '\0';

    if (encrypted == NULL)
        errExit("crypt");

    authOk = strcmp(encrypted, pwd->pw_passwd) == 0;
    if (!authOk) {
        printf("Incorrect password\n");
        exit(EXIT_FAILURE);
    }
    printf("Successfully authenticated: UID=%ld\n", (long) pwd->pw_uid);
   
    /* Now do authenticated work... */
    exit(EXIT_SUCCESS);
}

读取密码的程序应立即加密密码,并尽快将密码的铭文从内存中抹去,只有这样,才能基本杜绝一下事情发生:恶意之徒借程序崩溃之机,读取内核转储文件以获取密码。

还有其他方法可以曝光未经加密的密码:

  1. 包含密码的虚拟内存页执行交换操作,那么特级程序就能从交换文件中读取密码
  2. 拥有足够权限的进程可以通过读取/dev/mem来尝试发现密码

你可能感兴趣的:(Linux系统编程)