Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

attachment:Hello-World.jpg

리눅스 계정 시스템

리눅스는 다중 사용자 운영체제다. 이는 동시에 여러사람이 컴퓨터의 자원을 사용할 수 있음을 의미한다. 그러므로 리눅스 운영체제는 여러사람을 관리할 수 있는 시스템을 가지고 있어야 한다. 이것을 계정 시스템이라고 한다. 계정이란 컴퓨터 자원에 접근할 수 있는 사람들에 대한 정보로 이해하면 될 것이다.

아니다. 엄밀히 따지자면, 사람이라는 표현은 잘못된것 같다. 운영체제의 계정에는 시스템 계정일반 사용자 계정 크게 두가지로 분류할 수 있는데, 사람이라하면 일반 사용자 계정만을 가리키는 것이기 때문이다. 그러므로 사람 대신 객체라고 표현하는게 옳은 것 같다. 다시 정리하자. 계정이란 컴퓨터 자원에 접근할 수 있는 객체에 대한 정보를 의미한다.

계정 정보

계정이라는 것은 컴퓨터 자원에 접근하기 위한 객체라는 것을 알게 되었다. 그렇다면, 이들 객체는 각 객체의 특징을 나타내주는 정보를 가지고 있을 것이다. 이것을 계정정보라고 하다. 계정정보는 다음과 같은 정보들을 포함하고 있을 것이다. 계정은 유저라고 부르기도 하며, 계정정보는 유저정보와 동일한 의미로 사용된다.
  • 계정이름
계정을 다른 계정과 분리시켜주는 이름
  • 권한
컴퓨터 자원은 그 한계가 있다. 무한대가 아니다. 또한 다중 사용자 운영체제인 리눅스에 의해서 접근하게 될경우, 시스템 보안데이터 보안의 차원에서 접근제어를 해야할 필요가 있다. 리눅스(:12)는 권한을 통해서 이를 관리한다. 즉 이 파일은 누구누구는 읽기만 가능하고, 어떤 그룹에 대해서는 읽기/쓰기가 모두 가능하다 라는 식으로 관리한다.
  • 패스워드
이름과 이름에 따른 권한이 주어졌다면, 이 권한을 요청한 유저가 정말로 합법적인 유저인지를 확인하는 과정을 거쳐야 할 것이다. 인증과정인 셈이다. 가장 널리 사용되는 방법은 아이디/패스워드를 통한 인증방법이다.

위의 3가지 정보를 가지고 있으면, 완전한 하나의 계정을 만들어 낼 수 있다. 몇몇 부가적인 정보들이 더 필요한 경우가 있는데, 나머지는 말 그대로 부가정보들일 뿐이다.

슈퍼유저

컴퓨터 시스템은 회사와 매우 비슷한 면이 있다. 회사처럼 자원과 계정(사람)이 있으며, 권한이 부여된다. 입출입시 인증을 요구하기도 한다.

회사에 CEO가 있다면, 운영체제(:12)에는 슈퍼유저(:12)가 있다. 슈퍼유저는 무엇이든지 할 수 있는 막강한 권한을 가진 특별한 유저를 칭한다. 회사의 경우 CEO라고 하더라도, 권한을 행사함에 있어서 여러가지 제약이 있는 반면, 슈퍼유저는 말그대로 슈퍼맨의 능력을 가지고 모든 능력을 행사할 수 있다. 다음과 같은 명령하나로 운영체제를 싹 날려버릴 수도 있다.
# rm -rf /

전통적으로 유닉스에서 슈퍼유저는 root라는 계정이름이 부여된다. 리눅스 역시 유닉스의 이러한 전통을 따르고 있다. 슈퍼유저는 파일을 만들고 삭제하고, 파티션을 나누고, 유저를 추가하고 권한을 조정하는 등의 모든 업무를 처리할 수 있는 권한을 가지게 된다. 그 권한이 워낙 막강한 관계로, 최근의 몇몇 운영체제들은 root 라도 그 권한을 제한시키는 경우가 있다.

유저 생성 과정

아마도 여러분은 adduser(1) 명령을 이용해서 유저를 생성하는 법을 알고 있을 것이다. 여기에서는 실제 유저를 생성하기 위해서 어떤 과정을 거쳐야 하는지에 대해서 알아보도록 하겠다.

유저 파일들

리눅스에서 모든 정보는 파일을 통해서 관리된다는 것을 알고 있을 것이다. 유저정보 역시 마찬가지 이며, 유저와 관련된 파일들은 /etc 밑에 존재한다. 전통적으로 유닉스 시스템에서 /etc 디렉토리는 각종 설정파일을 저장하기 위한 목적으로 사용되어지고 있다. 유저 관련 주요 파일들은 다음과 같다.
passwd 유저 이름과 패스워드가 저장된다.
shadow passwd와 비슷하지만 보안이 강화되었다.
group 그룹 정보가 저장되어있다.
adduser.conf 유저생성과 관련된 변수들이 정의되어 있다.
각 파일들에 대한 내용은 아래에 다루도록 할 것이다.

passwd 파일

유저의 기본적인 정보는 /etc/passwd파일에 저장된다. 유저정보는 printable ASCII 문자로 입력이 되기 때문에 vi(:12)와 같은 에디터를 이용해서 내용을 확인할 수 있다. 다음은 passwd 파일 내용의 일부분이다.
# cat /etc/passwd
...
yundream:x:1000:1000:yundream,,,:/home/yundream:/bin/bash
mysql:x:110:121:MySQL Server,,,:/var/lib/mysql:/bin/false
mt-daapd:x:114:65534::/var/cache/mt-daapd:/bin/true
testyun:x:1004:1004:,,,:/home/testyun:/bin/bash
myyun:x:115:65534::/home/myyun:/bin/false
...
:를 구분자로 7개의 필드로 이루어진 간단한 구조를 하고 있다. 각 필드가 포함하는 내용은 다음과 같다.
유저:패스워드:UID:GID:GECOS:디렉토리:쉘
  1. 유저 : 시스템내에서 사용되는 유저 이름
  2. 패스워드 : 유저가 사용할 패스워드
  3. UID : 유저에게 부여되는 ID로 일련의 숫자다.
  4. GID : 유저가 포함되는 그룹의 ID로 일련의 숫자다.
  5. GECOS : 유저의 부가정보로 생략가능하다.
  6. 디렉토리 : 유저의 홈 디렉토리
  7. : 유저가 사용하게 될 shell(:12) 프로그램
기본적으로 /etc/passwd 를 편집할 수 있다면, 어렵지 않게 유저를 추가시키는 프로그램을 생성할 수 있다. 마찬가지로 /etc/passwd 파일을 읽어들이는 것으로, 사용자의 정보를 얻어올 수도 있다. 파일의 권한설정등에 사용되는 값은 유저 이름이 아닌 UID이기 때문에, /etc/passwd 파일은 자주 분석될 필요가 있다. 이러한 분석 프로그램은 프로그래머가 직접 작성할 수도 있지만 몇몇 함수를 이용하면 쉽게 유저정보를 얻어올 수 있다. 이에 대해서는 마지막에 사용자 정보를 얻어오는 프로그램을 작성하는 것으로 알아보도록 할 것이다.

그런데, 이상한 점이 있다. 패스워드 영역이 x로 되어있다. 패스워드가 몽땅 x일리는 없을테고 !!?

shadow password

passwd 파일에 사용자 패스워드가 없다면, 어떻게 유저이름과 패스워드로 인증이 가능할 건지가 문제가 된다. 우선 passwd 파일의 문제점에 대해서 알아보도록 하자. 다음은 passwd 파일의 권한이다.
# ls -al /etc/passwd
-rw-r--r-- 1 root root 1522 2008-01-09 23:34 /etc/passwd
누구든지 읽을 수 있게 되어있다. 단방향 암호화 되어 있기는 하지만, 암호화된 일련의 문자를 얻을 수 있다면, 패스워드 검사 프로그램을 만들어서 패스워드를 찾아낼 수 있다. 이는 보안상 심각한 문제라고 할 수 있다.

그래서 shadow password라는 방식을 도입했다. /etc/passwd 에는 패스워드를 제외한 정보만을 저장하고, 실제 패스워드는 오직 root 계정만 읽을 수 있는 /etc/shadow파일에 저장하는 것으로 passwd 파일의 보안문제점을 해결한 방식이다. /etc/shadow 파일의 권한을 확인해 보면, 단지 root 계정만 읽을 수 있음을 알 수 있다. 일반 유저가 읽을 수 없기 때문에, 패스워드 보안을 달성하게 된다.
#ls -al /etc/shadow
-rw-r----- 1 root shadow 996 2008-01-09 23:34 /etc/shadow
사실 사용자가 컴퓨터 시스템에 로그인 할것인지 안할 것인지를 결정하는 권한은 root 사용자만 가지고 있기 때문에, 다른 유저가 읽을 필요는 없을 것이다.

단 일반 사용자도 패스워드를 제외한 다른 유저의 정보를 알아야할 필요가 있기 때문에, /etc/passwd 파일을 남겨두게 된다. 유저정보는 /etc/passwd 패스워드 정보는 /etc/shadow 를 통해서 유지된다. 다음은 shadow 파일에 저장된 정보들이다.
yundream:$1$1S6/q4Ed$7en1qZdeOVofyEjqaofy/:13663:0:99999:7:::
역시 :를 구분자로 해서 필드를 구분하고 있다. 각 필드의 정보는 getspend(3) 메뉴얼 문서를 참고하기 바란다.

패스워드는 crypt(3)와 md5(:12)를 이용해서 단방향으로 암호화 된다.

사용자 정보 얻어오기

그럼 실제 사용자 정보를 얻어오는 방법에 대해서 알아보도록 하자. 우선 사용자 정보는 /etc/passwd 파일의 내용을 읽어와서 구분자 :로 필드를 쪼개고, 이것들을 구조체에 집어 넣는 방법을 생각해볼 수 있을 것이다.

그러나 위의 방법은 너무 귀찮다. 다행히도 리눅스는 /etc/passwd 파일에서 사용자 정보를 얻어오기 위한 표준 라이브러리(:12) 함수인 getpwent함수를 제공하고 있다.
#include <pwd.h>
#include <sys/types.h>

struct passwd *getpwent(void);
getpwent 함수는 /etc/passwd 에서 패스워드 정보를 읽어들여서 passwd 구조체에 저장하고, 이것에 대한 포인터를 되돌려준다. passwd 구조체는 다음과 같이 정의되어 있다.
struct passwd
{
    char  *pw_name;     /* 유저 이름 */
    char  *pw_passwd;   /* 유저 패스워드 */
    uid_t pw_uid;       /* 유저 ID (UID) */ 
    gid_t pw_gid;       /* 그룹 ID (GID) */ 
    char  *pw_gecos;    /* 실제 이름 */
    char  *pw_dir;      /* 홈 디렉토리 */
    char  *pw_shell;    /* 사용자 쉘 */
};

다음은 getpwent()함수를 이용해서 유저정보를 읽어오는 간단한 프로그램이다.
#include <pwd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

int main(int argc, char **argv)
{
    char *name;
    if (argc != 2)
    {
        printf("Usage : %s username\n", argv[0]);
        return 1;
    }
    name = argv[1];

    struct passwd *pass_info = NULL;

    while((pass_info = getpwent()) != NULL)
    {
        if (strncmp(name, pass_info->pw_name, strlen(name)) == 0)
        {
            printf("%12s uid(%d) gid(%d) home(%s)\n",
                pass_info->pw_name,
                pass_info->pw_uid,
                pass_info->pw_gid,
                pass_info->pw_dir);
            return 0;
        }
    }
    printf ("Can not find User : %s\n", name);
    return 1;
}

사용자 패스워드 얻어오기

만약 유저인증을 위한 프로그램을 만들기를 원한다면, 패스워드까지 가져오는 프로그램을 만들어야 할 것이다. 패스워드는 /etc/shadow 에 저장되어 있으며, getspent() 함수를 이용하면, 간단하게 패스워드 정보를 얻어올 수 있다. 단 shadow 파일은 root 유저만 읽을 수 있다.
#include <shadow.h>

struct spwd *getspent();
struct spwd *getspnam(char *name);
이들 함수에 대한 자세한 설명은 getspend(3) 문서를 확인해 보기 바란다. 친절하게도 예제까지 포함하고 있다.

사용자 로그인 기록

리눅스 운영체제(:12) 다중사용자 운영체제이니 만큼, 현재 시스템에 어떤 사용자가 들어와있는지에 대한 정보가 중요하게 취급된다. 이러한 로그인 정보는 사용자 점검, 특히 보안점검을 위해서 사용할 수 있을 것이다.

리눅스 운영체제는 w(1)라는 시스템 명령을 제공하는데, 이것을 이용하면, 현재 로그인된 사용자의 정보를 얻어올 수 있다.
# w 
 23:27:41  up 35 days, 12:51,  1 user,  load average: 0.13, 0.42, 0.65
USER     TTY      FROM              LOGIN@   IDLE   JCPU   PCPU  WHAT
root     pts/4    202.150.176.150  11:27pm  0.00s  0.05s  0.00s  w 
yundream pts/1    :0               22:59    6.00s  0.34s  0.16s  w3m 
로그인된 사용자의 IP(:12) 주소, 로그인한 시간, 자원의 사용율, 현재 어떤 프로그램을 실행중인지에 대한 정보를 보여준다.

이러한 정보들은 utmp(:12)라는 리눅스 고유의 로그인 기록시스템을 이용해서 얻어올 수 있다. 리눅스는 utmp 정보에 접근할 수 있도록 getutent라는 함수를 제공한다. utmp 정보는 /var/run/utmp 파일에 기록이 된다.
#include <utmp.h>

struct utmp *getutent(void);
getuent 는 읽어들인 정보를 utmp 구조체에 채워서 되돌려준다. utmp 구조체는 다음과 같이 정의되어 있다.
struct utmp                                                       
{                                                           
    short ut_type;              /* type of login */               
    pid_t ut_pid;               /* pid of login process */         
    char ut_line[UT_LINESIZE];  /* device name of tty - "/dev/" */
    char ut_id[4];              /* init id or abbrev. ttyname */    
    char ut_user[UT_NAMESIZE];  /* user name */                     
    char ut_host[UT_HOSTSIZE];  /* hostname for remote login */
    struct exit_status ut_exit; /* The exit status of a
                                   process marked as DEAD_PROCESS. *
    long ut_session;            /* session ID, used for

    struct timeval ut_tv;       /* time entry was made.  */
    int32_t ut_addr_v6[4];      /* IP address of remote host.  */
    char pad[20];               /* Reserved for future use.  */
};
w 명령을 이용해서 얻어온 정보들을 모두 포함하고 있음을 확인할 수 있다.

다음은 utmp를 이용해서, 로그인한 사용자의 정보를 얻어오는 프로그램이다. 프로그램의 이름은 myw.c 로 했다. 이해하는데 큰 어려움은 없을 것이다.
#include <unistd.h>
#include <utmp.h> 
#include <stdio.h> 
#include <sys/types.h>
#include <sys/stat.h>
#include <time.h> 
#include <fcntl.h>
#include <string.h> 
    
int get_current_pid(int login_pid);
char *get_current_procname(int proc_num);

int main(int argc, char **argv)
{
    struct utmp *utmpfp;
    struct utmp *myutmp;

    char *tty_name = NULL;
    char *host_ip; 
    
    char now_local_time[50];

    time_t the_time;
    struct tm *tm_ptr;

    setutent();

    // getutent()를 이용해서 utmp정보를 얻어온다.
    while ((utmpfp = getutent()) != NULL)
    {
        // ut_time 은 유닉스 시간으로 저장되는데, localtime 함수를 이용해서
        // 우리가 쉽게 읽을 수 있는 시간으로 변경했다.    
        the_time = utmpfp->ut_time;
        tm_ptr = localtime(&the_time);
        sprintf(now_local_time, "%d/%02d/%02d %02d:%02d",
                                tm_ptr->tm_year+1900, tm_ptr->tm_mon+1,
                                tm_ptr->tm_mday, tm_ptr->tm_hour,
                                tm_ptr->tm_min);

        host_ip = utmpfp->ut_host;
        if (strlen(host_ip) < 1)
            host_ip = "-";
        if (utmpfp->ut_type == USER_PROCESS)
        {
            printf("%-12s %-8s %-12s %s   %s\n",
                        utmpfp->ut_user, 
                        utmpfp->ut_line,
                        host_ip,
                        now_local_time,
                        get_current_procname(get_current_pid(utmpfp->ut_pid)));
        }
    }


    return 1;
}

int get_current_pid(int login_pid)
{
    int  fd;
    char buf[255];
    char stat_file[25];
    int  field_num = 7;
    int  i, j;
    int  buf_index;
    char current_pid[11];

    memset(buf, '\0', 255);
    sprintf(stat_file, "/proc/%d/stat", login_pid);

    if ((fd = open(stat_file, O_RDONLY)) == -1)
    {
        printf("error\n");
        return -1;
    }

    read(fd, buf, 255);
    j = 0;
    for (i = 0, buf_index=0; i < 255; i++)
    {
        if (buf[i] == ' ')
        {
            j++;
        }
        else
        {
            if (j == field_num)
            {
                current_pid[buf_index] = buf[i];
                buf_index++;
            }
            if (j > field_num)
                break;
        }
    }
    close(fd);
    return atoi(current_pid);
}
    
char *get_current_procname(int proc_num)
{   
    char *buf;
    char proc_file[256];
    
    buf = (char *)malloc(256);
    memset(buf, '\0', 256);

    memset(proc_file, '\0', 256);
    sprintf(proc_file, "/proc/%d/exe", proc_num);

    readlink(proc_file, buf, 256);
    return buf;
}
다음은 실행시킨 결과다.
$ ./myw 
yundream     :0       -            2008/01/17 22:59   
yundream     pts/1    :0           2008/01/17 22:59   
yundream     pts/2    :0           2008/01/17 23:17   

환경변수

환경변수는 해당 프로세스에 전역적으로 설정된 변수들로, 주로 프로그램 실행환경을 설정하기 위해서 사용한다. 원래의 목적은 프로그램 실행환경을 설정하기 위함이지만, 특히 쉘에서 사용될 경우 사용자 계정의 환경설정과 밀접한 관련이 있기 때문에, 이번장에서 설명을 하고 넘어가려고 한다.

환경변수는 프로그램에서 사용하는 변수와 마찬가지로 key=value의 형식을 가진다.

쉘에서는 env명령을 이용해서 설정된 모든 환경변수의 값을 읽어올 수 있다.
# env
SSH_AGENT_PID=6510
KDE_MULTIHEAD=false
MALLOC_CHECK_=2
SHELL=/bin/bash
TERM=xterm
...
...
LANG=ko_KR.UTF-8
GNOME_KEYRING_PID=6453
KDE_SESSION_UID=1000
GDM_LANG=ko_KR.UTF-8
KDEDIRS=/usr/lib/kde4
GDMSESSION=kde4
HISTCONTROL=ignoreboth
SHLVL=2
또한 echo 를 이용해서 환경변수의 값을 읽어올 수도 있다.
# echo $LANG
ko_KR.UTF-8

사용자 터미널, 언어, KDE(:12)와 GNOME등의 애플리케이션에서 사용할 여러가지 환경변수가 정의되어 있음을 알 수 있다.

환경변수는 프로세스가 자식프로세스를 만들 때, 자식프로세스에게 그대로 전달된다. 예를들어서 쉘에서 w3m(:12)이라는 프로그램을 실행시킨다면, 환경변수 LANG가 w3m에 그대로 복사되어서 전달이 된다. 그러면 w3m프로그램은 문자열을 만났을때, 어떻게 인코딩 시켜야 할지를 알 수 있게 된다. 위의 경우 UTF-8로 인코딩/디코딩을 하게 될 것이다.

중요 환경 변수들

사용자가 로그인을 하면 기본적인 환경변수들이 설정이 되는데, 그중 중요한 환경변수들을 정리해 보았다.
환경변수이름 설명
SHELL 사용중인 쉘 프로그램(:12)
|| PATH || 실행할 프로그램을 찾을 경로 ||
HOME 홈디렉토리
UID 사용자의 uid
GID 사용자의 gid
SHLVL 쉘 Level (쉘의 깊이)
USERNAME 사용자의 ID
TERM 사용자의 터미널(:12)
PWD 현재 작업디렉토리의 위치
HISTSIZE 히스토리의 크기
HOSTNAME 호스트 이름

환경변수 값 읽기

리눅스에서는 getenv(3)함수를 이용해서, 원하는 환경변수의 값을 읽어올 수 있다.
#include <stdlib.h>

char *getenv(const char *name);
  • name : 환경변수의 이름
다음은 환경변수 LANG 의 값을 읽어오는 간단한 프로그램이다.
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
  char *value = NULL;
  value = getenv("LANG");
  printf("LANG is %s\n", value);
  return 0;
}

환경변수는 프로그램이 실행시 스택영역에 복사가 되는데, 그러므로 C에서는 포인터를 이용해서 접근할 수 있도록 방법을 제시하고 있다. 환경변수가 저장된 스택의 주소는 main 함수의 3번째 인자를 통해서 가져올 수 있다.
#include <stdio.h>

int main(int argc, char **argv, char **env)
{
  while(*env != NULL)
  {
    printf("%s\n", *env);
    *env++;
  }
}
쉘의 env를 실행시킨것과 동일한 결과를 보여줌을 확인할 수 있을 것이다.

환경변수 값 설정

setenv(3)를 이용하면, 환경변수의 값을 변경할 수 있다. 이때 변경된 환경변수의 값은 자신과 자신의 자식프로세스에만 유효하다. 환경변수는 자식프로세스에게 복사될 뿐이지, 공유해서 사용하는게 아니기 때문이다.
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
  1. name
환경변수의 이름
  1. value
환경변수의 값
  1. overwrite
환경변수의 이름이 이미존재할 경우 값을 덮어쓸것인지를 결정한다. 0이라면 덮어쓰지 않는다.

#include <unistd.h>

int main()
{
    setenv("TEST", "YUNDREAM", 1);
    execl("/bin/bash", "bash", NULL);
}
setenv를 이용해서 TEST라는 환경변수를 정의 했다. 그다음 execl을 이용해서 새로운 bash 쉘을 실행했다. 이제 echo $TEST 를 이용해서 TEST라는 환경변수가 설정되었음을 확인할 수 있다. 프로그램의 이름은 setenv로 하겠다.
$ echo $TEST

$ echo $SHLVL
2
$ ./setenv
$ echo $TEST
YUNDREAM
$ echo $SHLVL
3
$ exit
exit
$ echo $TEST

$ echo $SHLVL
2
환경변수가 공유되는게 아닌 복사되는 것이라는걸 확실히 테스트 할 수 있을 것이다. 또한 SHLVL을 이용해서 쉘의 깊이를 확인할 수도 있다.