فصل ۸ - Users and Groups

در فایل /etc/passwd به ازای هر کاربر یک خط وجود دارد و هر خط متشکل از ۷ فیلد است که با : از هم جدا شده‌اند. این ۷ فیلد به ترتیب عبارتند از :

  1. username
  2. encrypted password
  3. user ID
  4. default group ID
  5. comment
  6. user home directory
  7. user login shell

چند خط نمونه از یک فایل /etc/passwd را در زیر آورده‌ایم:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
  • DES = Data Encryption Standards
  • NIS = Network Information System
  • LDAP = Lightweight Directory Access Protocol

چند نکته:

  1. اگر فیلد encrypted password خالی یا empty باشد آن اکانت نیاز به لاگین و ورود کلمه‌ی عبور ندارد.

  2. متغیرهای محیطی SHELL و HOME مقادیر خود را از فایل /etc/passwd می‌خوانند.

  3. امکان داشتن یک UID برای چند username فراهم است اگرچه معمول نیست. این یوزرها می‌توانند در گروه‌های متفاوتی عضو باشند و با هر لاگین و یوزنیم متفاوت سطح دسترسی متفاوتی داشته باشند.

  4. ست کردن UID یک اکانت با مقدار صفر باعث می‌شود که آن اکانت دارای دسترسی root شود. یعنی در واقع یوزنیم فعال root خواهد بود ولی گروههایی که عضو است گروه‌های متعلق به یوزنیم اصلی است.

  5. ست کردن مقدار SHELL به /usr/sbin/nologin باعث می‌شود که آن یوزر دیگر امکان لاگین کردن از shell به اکانت خودش را نداشته باشد.

  6. در یونیکس username یونیک است ولی user id نه.

  7. فایل‌های مهم در مورد کاربران و گروه‌ها عبارتند از ۴ فایل:

با دو تابع کتابخانه‌ای getpwnam(3) و getpwuid(3) می‌توان رکوردهای مجزایی را از فایل /etc/passwd استخراج کرد. دقت کنید که این تابع و تابع getpwent که در ادامه معرفی می‌شود یک اشاره‌گر به یک statically allocated structure برمی‌گردانند که در فراخوانی‌های بعدی به صورت خودکار رونویسی می‌شود به همین خاطر reentrant نیستند.

مطلب مهمی در مورد توابع معادل این تابع‌ها که reentrant هستند در صفحه‌ی ۱۵۸ است که نیاز به مطالعه دقیق‌تر دارد.

دقت کنید که این فضاها برای هر تابع با نام مشخص به صورت مجزا گرفته می‌شود و با فراخوانی یک تابع با نام دیگر رونویسی نمی‌شود. این مثال را ببینید.

#include <pwd.h>

struct passwd {
    char *pw_name;      /* Login name */
    char *pw_passwd;    /* Encrypted password */
    uid_t pw_uid;       /* User ID */
    gid_t pw_gid;       /* Group ID */
    char *pw_gecos;     /* Comment (user information) */
    char *pw_dir;       /* Initial working (home) directory */
    char *pw_shell;     /* login shell */
};

struct passwd *getpwnam(const char *name);
struct passwd *getpwuid(uid_t uid);

مثال برای دو تابع getpwnameو (3) getpwuid(3)

استاندارد SUSv3 می‌گوید که اگر رکورد مورد نظر یافت نشد تابع باید NULL برگرداند و errno را بدون تغییر بگذارد. به این صورت می‌توان بین رخداد خطا و پیدا نشدن رکورد تمیز قائل شد. ولی در عمل چیزی که من تست کردم این است که در صورت موفقیت تابع و پیدا نشدن رکورد، NULL برمی‌گرداند و errno را صفر می‌کند. مثال را ببینید.

شاید بهترین راه‌حل این باشد که قبل از فراخوانی این توابع errno را مساوی صفر قرار دهیم. بعد از فراخوانی اگر errno تغییر کرده باشد یعنی خطا روی داده است و اگر تغییر نکرده باشد و همچنان صفر باشد دیگر مهم نیست که تابع آن را مجددا صفر کرده یا بدون تغییر (همانند استاندارد SUSv3) باقی گذاشته است.

مقداری که در داخل فیلد pw_passwd قرار می‌گیرد فقط در صورتی صحیح است که password shadowing فعال نشده باشد. ساده‌ترین راه برای مشخص کردن اینکه آیا password shadowing فعال است یا نه این است که دقیقا بعد از یک فراخوانی موفق (3) getpwnam(3) تابع getspnam(3) را برای همان نام کاربری صدا بزنیم.

علت نامگذاری فیلد pw_gecos دلایل تاریخی دارد. دلیل اولیه‌ی نامگذاری این فیلد از بین رفته ولی همچنان این فیلد را با همین نام برای نگهداری کامنت مربوط به کاربر به کار می‌بریم (توضیحات بیشتر صفحه ۱۵۷)

با توابع getgrnam(3) و getgrgid(3) می‌توان رکورد مشخصی را از فایل /etc/group بر مبنای نام گروه و یا group id استخراج کرد.

#include <grp.h>

strcut group {
    char *gr_name;      /* Group name */
    char *gr_passwd;    /* Encrypted password (if not password shadowing) */
    gid_t gr_gid;       /* Group id */
    char **gr_mem;      /* NULL terminated array of pointers
                   to names of members listed in /etc/group */
};

struct group *getgrnam(const char *name);
struct group *getgrgid(gid_t gid);

هر دو تابع getgrnam و getgrgid در صورت موفقیت یک اشاره‌گر به struct group که به صورت statically توسط توابع گرفته شده است برمی‌گردانند و در صورت عدم موفقیت (وجود خطا و یا پیدا نشدن رکورد) NULL برمی‌گردانند.

مثال برای دو تابع getgrnam و getgrgid

چهار تابع userIdFromName و userNameFromId و groupIdFromName و groupNameFromId در فایل lpi.c

مثال استفاده از چهار تابع فوق

برای دریافت متوالی رکورد از فایل /etc/passwd از ۳ تابع زیر استفاده می‌کنیم.

#include <pwd.h>

struct passwd *getpwent(void);

void setpwent(void);
void endpwent(void);

تابع getpwent(3) مانند سایر توابع این خانواده در صورت خطا یا پایان فایل NULL برمی‌گرداند. در اولین فراخوانی، تابع getpwent به صورت خودکار فایل /etc/passwd را باز می‌کند. وقتی کارمان با تابع getpwent تمام شد تابع endpwent را صدا می‌کنیم تا فایل فوق‌الذکر را ببندد.

لیست تمام کاربران سیستم (نام کاربری+user id)

بستن فایل /etc/passwd با فراخوانی تابع endpwent لازم و ضروری است. در غیر این صورت چون فایل باز می‌ماند و آفست فایل تغییر نمی‌کند اگر در ادامه‌ی برنامه، چه برنامه‌ی کاربر و چه توابع کتابخانه‌ای، مجددا تابع getpwent صدا زده شود دیگر نیاز به باز شدن فایل نیست و از ادامه‌ی آفست ثبت شده خواهد خواند. به احتمال بسیار زیاد این چیزی نیست که ما نیاز داریم.

توابع زیر مشابه توابع بالا ولی برای کار با فایل /etc/group هستند.

#include <grp.h>

struct group *getgrent(void);

void setgrent(void);
void endgrent(void);

نمایش کل گروه‌های تعریف شده در سیستم

برای کار با فایل /etc/shadow منطق کار مانند کار با فایل‌های passwd و group است. برای این منظور از توابع زیر استفاده می‌کنیم. توجه کنید در فایل /etc/shadow، userid ذخیره نمی‌شود و تنها واکشی متوالی و واکشی بر اساس username داریم. این توابع در SUSv3 تعریف نشده‌اند و در همه‌ی پیاده‌سازی‌های UNIX نیستند.

تابع getspnam در صورت نداشتن privilege برای خواندن فایل NULL برمی‌گرداند و errno را ست نمی‌کند.

#include <shadow.h>

struct spwd {
    char *sp_namp;          /* Login name */
    char *sp_pwdp;          /* Encrypted password */

    /* fields for password aging. (refer page 162) */
    long sp_lstchg;         /* Time of last password change
                               (days since 1 Jan 1970) */
    long sp_min;            /* Min number of days between password changes */
    long sp_max;            /* max number of days before change required */
    long sp_warn;           /* Number of days beforehand that user is warned
                               of upcomming password expiration */
    long sp_inact;          /* Number of days aftre expiration that account
                               is considered inactive and locked */
    long sp_expire;         /* Date when account expires
                               (days since 1 Jan 1970) */
    unsigned long sp_flag;  /* Reserved for futute use */
};

struct spwd *getspnam(const char *name);
        Returns pointer on success, or NULL on not found or error

struct spwd *getspent(void);
        Returns pointer on success, or NULL on not found or error

void setspent(void);
void endspent(void);

وقتی password shadowing فعال باشد هیچ password ای در داخل فایل /etc/passwd ذخیره نمی‌شود، بلکه password ها در فایل /etc/shadow ذخیره می‌شوند.

منطق الگوریتم encryption کلمه‌ی عبور در داخل تابع crypt(3) پیاده سازی شده است.

تابع crypt(3) یک رشته‌ی حداکثر ۸ کاراکتری می‌گیرد (به نظر می‌رسد در صورت طولانی‌تر بودن رشته خطا نمی‌دهد ولی کاراکترهای اضافی را نادیده می‌گیرد) salt یک رشته‌ی دو کاراکتری است. تابع یک اشاره‌گر به یک رشته‌ی statically allocated که ۱۳ کاراکتر است برمی‌گرداند.

#define _XOPEN_SOURCE
#include <unistd.h>

char *crypt(const char *key, const char *salt);
            Returns pointer to statically allocated string containing encrypted
            password on success, or NULL on error

Both the salt argument and the encrypted password are composed of characters selected from the 64-character set [a-zA-Z0-9/.]. Thus, the 2-character salt argument can cause the encryption algorithm to vary in any of 64*64 = 4096 different ways. A cracker would need to check the password against 4096 encrypted versions of the dictionary.

The encrypted password returned by crypt contains a copy of the original salt value as its first two characters. This means that when encrypting a candidate password, we can obtain the approporiate salt value from the encrypted password value already stored in /etc/shadow.

Programs such as passwd(1) generate a random salt value when encrypting a new password.

در حقیقت تابع crypt اگر طول salt بیشتر از ۲ کاراکتر باشد کاراکترهای اضافه را نادیده می‌گیرد. بنابر این به سادگی می‌توانیم خود کلمه‌ی عبور کد شده را به عنوان salt به تابع بدهیم.

برای استفاده از تابع crypt در لینوکس باید برنامه را با سوییچ -lcrypt کامپایل کنیم. در این صورت برنامه به کتابخانه‌ی crypt لینک می‌شود.

در واقع چیزی که من تست کردم این است که نیازی به feature test macro به صورت #define _XOPEN_SOURCE نیست. تابع crypt در سرفایل <crypt.h> تعریف شده است اگر چه در <unistd.h> هم هست. علاوه بر این گویا salt فرمت خاصی دارد که در صورت تغییر این فرمت password های بیشتر از ۱۳ کاراکتر هم تولید می‌شود. به همین خاطر هنگام بررسی password جدید و انطباق آن با password کد شده بهتر است کل password کد شده را به عنوان salt به تابع crypt(3) بدهیم.

برنامه‌هایی که از ورودی password می‌خوانند باید به سرعت password را کد کنند و password کد نشده را از حافظه پاک کنند (محتوای خانه‌های حاوی password را NULL کنند) این کار باعث می‌شود که احتمال اینکه برنامه کرش کند و کلمه‌ی عبور کد نشده در داخل core dump قرار بگیرد کمتر شود.

... /dev/mem a virtual device that presents the physical memory of a computer as a sequentail stream of bytes.

دیتای حساس به جای فایل /etc/passwd در فایل /etc/shadow نگهداری می‌شود.

The crypt(3) function encrypts a password in the same manner as the standard login program, which is useful for programs that need to authenticate users.

تابع getpass(3) ابتدا echo کردن و پردازش کاراکترهای ویژه مثل Control-C را در ترمینال غیر فعال می‌کند. prompt را در ترمینال می‌نویسد و سپس یک خط از ورودی می‌خواند. کاراکتر new line را حذف می‌کند و رشته را NULL terminated می‌کند. رشته در داخل یک متغیر statically allocated برمی‌گردد. قبل از بازگشت تمام تغییرات اعمال شده بر روی ترمینال را به حالت اولیه‌ی خود برمی‌گرداند.

#define _BSD_SOURCE
#include <unistd.h>

char *getpass(const char *prompt);
                Returns pointer to statically allocated input password string
                                                 on success, or NULL on error

پیاده‌سازی لاگین با استفاده از توابع این فصل

مثال ۸.۱ صفحه‌ی ۱۶۶ علی رغم اینکه می‌گوید باید هر دو عدد یکسان چاپ شود اینگونه نیست. گرچه کاملا قابل فهم است و به خاطر مشخص نبودن ترتیب پردازش آرگومانهای تابع و نیز فضای statically allocated تابع getpwnam درست این است که کد به این صورت نوشته نشود.

پیاده‌سازی تمرین ۸.۱

پیاده سازی تمرین ۸.۲

مطالعه‌ی بیشتر

  1. chfn(1) (با دستور chfn می‌توان اطلاعات comment در فایل passwd را تغییر داد)
  2. chsh(1) با دستور chsh می‌توان default shell کاربر را تغییر داد.
  3. shadow(5)
  4. gshadow(5)
  5. gpasswd(1)
  6. newgrp(1) (اضافه کردن کاربر به یک گروه جدید)
  7. adduser(8)
  8. addgroup(8)
  9. crypt(3)
  10. group(5)
نوشته شده در: 1405-03-10 (1 هفته 22 ساعت 4 دقیقه پیش)

من محسن هستم؛ برنامه‌نویس تفننی!

برای ارتباط با من یا در همین سایت کامنت بگذارید و یا به dokaj.ir(at)gmail.com ایمیل بزنید.

پست قبلی: فصل ۱۰ - Time

در مورد این مطلب یادداشتی بنویسید.