Vietnamese Developers' Blog

Các user ID của một tiến trình trong Unix

Posted in Unix/Linux/BSD by kiennguyen on the June 8th, 2009

Ba loại user ID của một tiến trình

Một tiến trình (process) trong Unix sở hữu 3 user ID sau đây:

Real user ID (user ID thật): Là user ID của người khởi động tiến trình.
Effective user ID (user ID hiệu dụng): Là một loại user ID được dùng trong những trường hợp sau đây:

  • Khi tiến trình truy nhập một file, hệ điều hành kiểm tra xem effective user của tiến trình có quyền truy nhập file hay không.
  • Khi tiến trình tạo mới một file, hệ điều hành lấy effective user của tiến trình làm chủ sở hữu (owner) của file.
  • Khi tiến trình muốn gửi tín hiệu đến một tiến trình khác, hệ điều hành kiểm tra xem effective user của tiến trình gửi có quyền gửi tín hiệu hay không.

Saved user ID: Là giá trị của effective user ID được lưu trong bảng các tiến trình (process table). Mỗi tiến trình có một vùng dữ liệu tương ứng trong bảng các tiến trình. Saved user ID đóng vai trò như một bản backup cho effective user ID, được dùng khi tiến trình muốn quay lại effective user ID ban đầu.

Tương ứng với 3 loại user ID này là ba loại group ID: real group ID, effective group ID và saved group ID.

Vì sao cần effective user ID?
Effective user là một cơ chế quản lí truy nhập của Unix (access control hay authorization). Xem ví dụ sau đây: Alice là một kế toán và cô ta cần có quyền thay đổi file dữ liệu kế toán account_data của công ty. Tuy nhiên, để đảm bảo tính toàn vẹn dữ liệu, Alice không được phép thay đổi dữ liệu kế toán một cách thủ công mà phải thông qua một chương trình tên là account_program. Như vậy:

  • Alice không có quyền ghi đối với account_data.
  • account_program có quyền ghi đối với account_data.
  • Alice có quyền chạy account_program.

Tuy nhiên, trong Unix chỉ có người dùng mới được coi là các chủ thể được cấp quyền sử dụng tài nguyên. Bởi vậy, giải pháp trong Unix là tạo ra một người dùng có quyền ghi đối với account_data, thiết lập effective user ID của account_program bằng ID của người dùng đó và trao cho Alice quyền chạy account_program.

Effective user ID được thiết lập như thế nào?
Giá trị của effective user ID được thiết lập bằng hai cách:

Thông qua bit suid: Mỗi một file chương trình có một thuộc tính gọi là suid. Nếu bit suid được thiết lập thì khi chạy file chương trình, tiến trình tương ứng sẽ có effective user và saved user là chủ sở hữu của file chương trình. Ngược lại nếu bit suid không được thiết lập, effective user và saved user của tiến trình sẽ là người khởi động tiến trình (chú ý rằng chủ sở hữu file và người chạy file thường là hai người dùng khác nhau).

Thông qua hàm setuid(): Hàm setuid() dùng để thay đổi effective user ID của một tiến trình trong thời gian chạy (runtime).

#include <sys/types.h>
#include <unistd.h>
 
int setuid( uid_t uid );

Tham số uid là giá trị user ID mới. Kết quả thực hiện setuid() là như sau:

  • Trường hợp 1: Nếu effective user hiện thời của tiến trình là root, cả ba giá trị real user ID, effective user ID và saved user ID đều được gán giá trị uid.
  • Trường hợp 2: Nếu effective user hiện thời của tiến trình không phải là root, effective user ID được gán giá trị uid nếu uid bằng real user ID hoặc saved user ID hiện thời của tiến trình. Nếu uid không bằng một trong hai giá trị này, setuid() trả về giá trị lỗi.

Hiểu một cách “nôm na” thì một tiến trình đang có effective user là root có thể “hoá thân” thành bất kì user nào. Trong khi đó một tiến trình “bình thường” chỉ có thể chuyển hoá qua lại giữa hai giá trị real user ID và effective user ID ban đầu mà thôi.

Một tiến trình cần thay đổi effective user ID nhằm đảm bảo nguyên tắc “đặc quyền tối thiểu” (least privilege – một trong 8 nguyên tắc đảm bảo tính bảo mật của phần mềm [2]), theo đó một người dùng hay một tiến trình chỉ được cấp quyền tối thiểu để thực hiện một nhiệm vụ nào đó. Bởi vậy, một tiến trình chỉ sử dụng effective user là root khi thực sự cần thiết mà thôi. Theo trường hợp 2 ở trên, khi một tiến trình chuyển từ effective user root sang một effective user thông thường, nó sẽ không thể lấy lại trạng thái root bằng hàm setuid().

Xem ví dụ minh họa sau đây (lấy từ [1]):
user1 và user2 là hai người dùng trong hệ thống có user ID lần lượt là 501 và 502. user1 và user2 lần lượt là chủ sở hữu của các file fuser1 và fuser2, chỉ chủ sở hữu mới có quyền đọc hai file này. File chương trình testsuid.c sau khi dịch thuộc quyền sở hữu của user2 nhưng user1 có quyền chạy.

Trước hết chúng ta kiểm tra giá trị của effective user ID trước khi gọi hàm setuid()

/* testsuid.c */
#include <unistd.h>
#include <stdio.h>
 
int main()
{
  int uid, euid, fuser1, fuser2;
 
  uid  = getuid();   /* get real UID */
  euid = geteuid(); /* get effective UID */
  printf( "uid: %d, euid: %d\n", uid, euid );
 
  return 0;
}

Dịch chương trình và lưu file chạy vào một thư mục chung sao cho user1 và user2 đều truy nhập được. Khi bit suid chưa được thiết lập, effective user ID luôn bằng real user ID:

Kết quả khi user1 chạy chương trình:

testsuid
uid: 502, euid: 502

Kết quả khi user1 chạy chương trình:

testsuid
uid: 501, euid: 501

Bây giờ chúng ta thiết lập bit suid cho testsuid như sau (thực hiện bởi user2):

chmod u+s testsuid
ls -l testsuid
-rwsr-xr-x 1 user2 users 5404 Jun  6 21:28 testsuid

Chữ s trong phần quyền truy nhập file cho thấy testsuid là một chương trình suid. Kết quả là effective user ID của tiến trình luôn bằng user ID của user2:

Kết quả khi user1 chạy chương trình:

testsuid
uid: 501, euid: 502

Tiếp theo, hàm setuid() được đưa vào testsuid.c như sau:

#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
 
int main()
{
  int uid, euid, fuser1, fuser2;
 
  uid  = getuid(); /* get real UID */
  euid = geteuid(); /* get effective UID */
  printf( "uid: %d, euid: %d\n", uid, euid );
 
  fuser1 = open( "fuser1", O_RDONLY );
  fuser2 = open( "fuser2", O_RDONLY );
  printf( "fuser1: %d, fuser2: %d\n", fuser1, fuser2 );
 
  setuid( uid );
  printf( "after setuid(%d), uid: %d, euid: %d\n", uid, getuid(), geteuid() );
 
  fuser1 = open( "fuser1", O_RDONLY );
  fuser2 = open( "fuser2", O_RDONLY );
  printf( "fuser1: %d, fuser2: %d\n", fuser1, fuser2 );
 
  setuid( euid );
  printf( "after setuid(%d), uid: %d, euid: %d\n", euid, getuid(), geteuid() );
 
  return 0;
 
}

Kết quả khi user1 chạy chương trình:

testsuid
uid: 501, euid: 502
fuser1: -1, fuser2: 3
after setuid(501), uid: 501, euid: 501
fuser1: 4, fuser2: -1
after setuid(502), uid: 501, euid: 502

Ban đầu real user ID của tiến trình bằng user ID của user1 (501) và effective user ID của tiến trình bằng user ID của user2 (502). Bởi vậy, tiến trình không đọc được fuser1 nhưng đọc được fuser2. Hàm setuid( uid ) gán effective user ID bằng real user ID (501). Do đó, sau lời gọi hàm, tiến trình đọc được fuser1 mà không đọc được fuser2. Lời gọi setuid( euid ) lấy lại giá trị ban đầu cho effective user ID (chú ý rằng euid đang được lưu trong saved user ID).

Kết quả khi user2 chạy chương trình là:

testsuid
uid: 502, euid: 502
fuser1: -1, fuser2: 3
after setuid(502), uid: 502, euid: 502
fuser1: -1, fuser2: 4
after setuid(502), uid: 502, euid: 502

Tiến trình không đọc được fuser1 vì effective user của nó luôn là user2.

Các chương trình suid trên Unix
Một trong số các chương trình suid sẵn có trong Unix là passwd, được sử dụng để thay đổi password cho người dùng.

ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 19876 Jul 17  2006 /usr/bin/passwd

passwd thuộc sở hữu root, do vậy khi chạy sẽ có effective user là root. Người dùng không được phép ghi trực tiếp lên file password mà phải thông qua passwd.

Nhược điểm của effective user ID
effective user là một lỗ hổng bảo mật của Unix. Các lập trình viên thường viết các chương trình có effective user là root kể cả khi không cần thiết và đó là điểm yếu cho những kẻ phá hoại khai thác. Một số lời khuyên khi thiết kế các chương trình suid được trình bày trong [3].

Tài liệu tham khảo
[1] The Design of The UNIX Operating System của Maurice J. Bach.
[2] The Protection of Information in Computer Systems của Saltzer & Schroeder.
[3] Practical UNIX & Internet Security của Simson Garfinkel & Gene Spafford.

Leave a Reply