プログラムで実行権限を変更するための調査

SPECIAL


はじめに

Linux で プログラムを実行する際に、途中でユーザ権限を交換できたら楽そうだなぁ…、ということでそのあたりをちょこっと調べてみました。

もっとも、Linux のプログラム自体が実は初めてというなんとも無謀な状態なので、ぜんぜん役に立たない可能性が大きいのですけど、まあ実験の記録ということで…。とりあえず行き当たりばったりでやってみた感じなので、気をつけてください^^;;;

 

UID および GID の変更…

とあるユーザのホームディレクトリ内のデータの操作をおこないたくて、ファイルを作成したりしたときなんかに、所有者設定とかするのは何かと大変そう…。ということで、その作業の再にはそのユーザになりきってしまえば、何かと楽に操作を行えるのでは…?

ということで、頭の中では sudo みたいなのをプログラム内で行うような感じを思い描いてたのですけど、しらべてみたところ、微妙にそれっぽい関数 (システムコール) が目に付きました。

uid_t getuid(void); 現在のプロセスの実 UID を取得する。
uid_t geteuid(void); 現在のプロセスの実効 UID を取得する。
uid_t getgid(void); 現在のプロセスの実 GID を取得する。
uid_t getegid(void); 現在のプロセスの実効 GID を取得する。

 

実 UID と実効 UID の2つが存在するようですけど、とりあえず get 系があるのならもしかすると set 系も存在するのかな…、ということで調べてみたらありました。

int setuid(uid_t uid); 現在のプロセスの実 UID を設定する。
int seteuid(uid_t uid); 現在のプロセスの実効 UID を設定する。
int setgid(gid_t gid); 現在のプロセスの実 GID を設定する。
int setegid(gid_t gid); 現在のプロセスの実効 GID を設定する。
int setreuid(uid_t ruid, uid_t euid); 現在のプロセスの実 UID (ruid) と実効 UID (euid) を設定する。
int setregid(gid_t rgid, gid_t egid); 現在のプロセスの実 GID (rgid) と実効 UID (egid) を設定する。

この関数は正常に設定できれば 0 を、失敗した場合は -1 を返します。エラーの際には perror() 等でエラー情報を取り出すことが出来るようになっているようです。

とりあえず細かい調査は抜きにして、実際にこの辺りをてきとうに実験してみることにしました。

 

権限を変更してみる(実験)

とりあえず、権限を変更してその状態でファイルを作成してみる、ということをやってみることにしました。

プログラムの内容は非常に簡単で、まず変更前にファイルを作成し、setuid() および setgid() によって権限を変えた後で再び別のファイルを作成する、を繰り返していく感じです。

setuid 系が失敗したときにそれを知れるように perror() もコードに組み込んであります。

 

下準備

とりあえず、コードを簡単にするため、次のような関数を定義しておきます。

// ダミーファイルを作成する際に使用する文字列バッファ

char buffer_dummyfile[100];

 

// ダミーファイルを作成するときに使うマクロ

#define MAKE_DUMMYFILE make_dummyfile(make_filename(buffer_dummyfile));

 

// 権限情報を表示するのに使用するマクロ

#define PRINT_INFO print_id(); print_who()

 

/* --------------------------------------------

作成するファイルのファイル名を決定します。

ファイル名は "./dummy/dummy.uid.txt" となります。

--------------------------------------------- */

char* make_filename(char* buffer)

{

sprintf(buffer, "./dummy/dummy.%i.txt", getuid());

return buffer;

}

 

/* --------------------------------------------

ファイルを作成します。

--------------------------------------------- */

void make_dummyfile(const char* filename)

{

std::ofstream ofs(filename);

ofs << "dummy file." << std::endl;

}

 

/* --------------------------------------------

現在の UID と GID を表示します。

--------------------------------------------- */

void print_id(void)

{

    char buffer[100];

    std::cout << "UID(EUID): " << getuid() << "(" << get

}

 

/* --------------------------------------------

現在有効なユーザ名を表示する whoami コマンドと、

ホームディレクトリを表示します。

 

単純に system() でコマンドを実行させているだけです。

--------------------------------------------- */

void print_who(void)

{

system("whoami");

system("cd; pwd");

}

また、次ぎのファイルがインクルードされているものとします。

#include <iostream>

#include <fstream>

 

#include <sys/type.h>

#include <unistd.h>

プログラムは C++ で作成し、実行は root 権限で行ってみます。

UID を変更してみる実験

まずは UID の変更が可能かどうかを調べてみました。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setuid(1000)) perror("setuid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

実行してみたところ、結果は次のようになりました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

/root

UID(EUID): 1000(1000)

GID(EGID): 0(0)

dummy1

/root

いちおう、UID = 1000 は dummy1 というユーザに割り当てられています。これをみると、実 UID を変更した時点で実 UID と実行 UID の両方が変更され、whoami コマンドによってちゃんとそのユーザに権限が変更されていることがわかります。ただ、環境変数等は特に変化がないせいか、ホームディレクトリ情報は以前のまま (/root) でした。

ファイルはあらかじめ用意した dummy/ ディレクトリに作成されるようにしてあるのですけど、調べてみると root 権限の時にだけ作成した "dummy.0.txt" ファイルが、root.root 所有として作成されていました。

 

UID を変更した後に行ったファイル作成の方は作成された感じが無かったのですけど、調べてみたところあらかじめ作成した dummy/ ディレクトリが root 所有でパーミッションが drwx--x--x になっていました。

なので drwxrwxrwx と、誰もが読み書きできるように設定しなおしたところ、今度は "dummy.1000.txt" ファイルが dummy1.root 所有として作成されていました。グループが root なのは、GID は変更していないからです。

なにはともあれ UID を変更しただけでちゃんとパーミッションの制限を受けているようなので、なかなかいい感じです。

 

GID を変更してみる

今度は GID も変更してみることにします。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setuid(1000)) perror("setuid:1");

if (setgid(100)) perror("setgid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

これで実行してみると、仕込んであったエラー処理が発動しました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

/root

setgid:1: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 0(0)

dummy1

/root

setgid の設定のところで権限がないとのことです。これはどうやらその直前の setuid によって GID を変更するだけの権限が失われてしまったせいのようです。なので setuid と setgid の実行順番を交換して、root ユーザのうちに GID をまず変更するようにしてみました。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setgid(100)) perror("setgid:1");

if (setuid(1000)) perror("setuid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

こうしてみるとエラーメッセージは表示されず、ちゃんと UID と GID が変更されているようでした。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

/root

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

いったんファイルを消さないとダメみたいでしたけど、実行してみたところ今度は dummy.1000.txt の権限がちゃんと dummy1.users と、users グループ所有のファイルとなりました。

相変わらずホームディレクトリは /root のままです。

ID を連続で変更してみる

権限をいったん設定した後、再び ID を root のものに変更してみることにします。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setgid(100)) perror("setgid:1");

if (setuid(1000)) perror("setuid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度権限を root へ変更、権限情報を表示

 

if (setgid(0)) perror("setgid:3");

if (setuid(0)) perror("setuid:3");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

すると root へ戻そうとした部分は UID, GID ともにエラーとなりました。どうやら root に戻る権限は持ち合わせていないようです。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

/root

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

setgid:3: Operation not permitted

setuid:3: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

 

では、root 権限に戻す前に、もう一度別のユーザへ変更してみるとどうなるでしょう。一般ユーザから一般ユーザへ変更できるかどうかの実験です。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setgid(100)) perror("setgid:1");

if (setuid(1000)) perror("setuid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度一般ユーザへ変更、権限情報を表示

 

if (setgid(500)) perror("setgid:2");

if (setuid(100)) perror("setuid:2");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度権限を root へ変更、権限情報を表示

 

if (setgid(0)) perror("setgid:3");

if (setuid(0)) perror("setuid:3");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

するとこの場合、実質変更のない GID の方はエラーなく通りましたが、UID の方はエラーがでて変更できませんでした。一般ユーザが別のユーザに入れ替われないのは当然かもしれないですけど、まあそんな感じになりました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

/root

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

setuid:2: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

setgid:3: Operation not permitted

setuid:3: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

/root

 

ホームディレクトリの取得は…

せっかく権限を交換できるのだから、ホームディレクトリ情報も取得できればいいのですが…。と調べてみたところ、UID からそのユーザに関する情報を取得する関数があるようなので、そのあたりを実験してみることにしました。

struct passwd*
 getpwnam(const char* name);
ユーザ名からそれに関連する /etc/passwd 情報を passwd 構造体へのポインタにて取得します。
struct passwd*
 getpwuid(uid_t uid);
UID からそれに関連する /etc/passwd 情報を passwd 構造体へのポインタにて取得します。

これらの関数は、該当するパスワードが見つからなかった場合 NULL が返されるそうです。

一見、パスワードのみを取得できそうな関数名ですけど、調べてみたところユーザ情報をいろいろ取得してくれるようです。取得されるデータの構造体は次のようになっています。

 

■ struct passwd

char* pw_name ユーザ名
char* pw_passwd パスワード (暗号化済みのはず。SHADOW の場合は x らしい…)
uid_t pw_uid UID
gid_t pw_gid GID
char* pw_gecos 本名(?)
char* pw_dir ホームディレクトリ
char* pw_shell シェルプログラム

これならユーザ ID またはユーザ名さえ知っていれば容易にホームディレクトリを知ることが出来そうです。

 

なのでさっそく、それらの情報を表示してみることにしました。それにともなって、あらかじめ用意した print_who() 関数の実装部分を変更します。main() 関数などそれ以外の部分は変更なしです。いちおう、表示情報は実効 UID のものを取り扱うものとします。

また、これを利用するにあたって、pwd.h をインクルードする必要があります。

#include <pwd.h>

 

/* --------------------------------------------

現在有効なユーザ名を表示する whoami コマンドと、

実効 UID に関するユーザ情報を表示します。

--------------------------------------------- */

void print_who(void)

{

system("whoami");

 

passwd *pw = getpwuid(geteuid());

printf("UID:%i, GID:%i, NAME:%s, HOME:%s, SHELL:%s\n", pw->pw_uid, pw->pw_gid, pw->pw_name, pw->pw_dir, pw->pw_shell);

}

さてこれで実行してみると、次のような幹事に情報を取得することが出来ました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

UID:0, GID:0, NAME:root, HOME:/root, SHELL:/bin/bash

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

setuid:2: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

setgid:3: Operation not permitted

setuid:3: Operation not permitted

UID(EUID): 1000(1000)

GID(EGID): 100(100)

dummy1

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

 

パスワードの取得または照合は…

上でちょこっとだけ触れましたけど、getpwuid() によって取得できるパスワードは /etc/passwd に記載されているパスワード、すなわち SHADOW がかかっている場合は単純に "x" が取れてしまいます。

ユーザアカウントを取り扱うにあたり、認証時にシステムアカウントのパスワードで照合できたらいいな…ということで、そのあたりを調べてみました。

 

■ putpwent() 関数…?

調べ始めてみたところ、パスワード情報を登録するための関数 setpwent() なる関数がありました。当然ながらパスワードも設定できるはずでしょうけど、そのパスワード設定、の部分が気になったのでとりあえず実験してみよう…。

と思ったのですけど、この関数がとる引数は const struct passwd* 型、つまりは passwd 構造体のポインタ、すなわち配列を渡すこともありうるということのようです。

これの姉妹品(?)に getpwent() なる関数があるようなのですけど、こちらも、一行ずつ取り出すとはいえ getpwent() / setpwent() / endpwent() のセットで次々とアカウント情報を取り出すことが出来るようです。

普通、読み込みと書き込みは対になる感じがあるので、仮にこれらもセットで利用するとすると…。ちょっと丸ごと全部を配列として渡すのには無理がありそうですけど、うっかり書き込んだ1つのみが登録された /etc/passwd が出来上がってしまうと悲しいかも。

 

とりあえず、getpwent() によってどんな情報が取り出せるか、というか getpwuid() と同じ情報が得られるのか、または違う情報が得られるのか、そのあたりを調べてみることにしました。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <pwd.h>

 

int main()

{

passwd *pw = getpwent();

 

printf("UID:%i, GID:%i, NAME:%s, PASS:%s, HOME:%s, SHELL:%s\n", pw->pw_uid, pw->pw_gid, pw->pw_name, pw->pw_passwd, pw->pw_dir, pw->pw_shell);

 

endpwent();

}

こんな感じのを実行してみましたけど、パスワードは getpwuid() の時と同様、"x" が手に入るのでした。

 

では、パスワード設定のときはどうなるのでしょう…。指定したパスワードは SHADOW パスワードとして設定されるのか、または /etc/passwd に直接設定されてしまうのか…。

勘ですけど渡す構造体は1つのものへのポインタでよさそうな感じがするのでそのように渡してみることにしますけど、うっかり全滅すると怖いのでそのあたりを注意してやってみます。幸い FILE 型にてパスワードファイルを指定できるようになっているようなので、あらかじめ /etc/passwd ファイルと /etc/shadow ファイルをバックアップして、それに対して操作を行うことにしました。

でもなんだか、/etc/passwd ファイルのストリームしか渡せない辺り、SHADOW パスワードには対応してなさそうですが…。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <pwd.h>

 

int main()

{

passwd pw;

 

pw.pw_name = "dummy3";

pw.pw_passwd = "dummy";

pw.pw_uid = 700;

pw.pw_gid = 100;

pw.pw_dir = "/dev/null";

pw.pw_shell = "/bin/false";

 

FILE *fl = fopen("./backup/passwd", "a");

 

if (fl != NULL)

{

if (putpwent(%pw, fl)) perror("putpwent");

fclose(fl);

}

else

{

perror("open");

}

 

return 0;

}

組んでいて思ったのですけど、ファイルストリームを渡す前に passwd ファイルを開くわけで、その際に "w" なんかで開いてしまうと明らかに全滅ですね^^;; 逆に "a" で開けば…、全滅し無そうなことが察せます。

そして実際に実行してみると、./backup/passwd ファイルの最後の行に次の行が追加されました。

dummy3:dummy:700:100::/dev/null:/bin/false

はい、パスワードの部分にしっかりと指定した文字列が入りました。とりあえず暗号化くらいはこちらで自力でやるとしても、SHADOW パスワードに関与していなかったのは残念でした。実際、./backup/shadow および /etc/shadow にはなんら影響はありませんでした。

なお、もう一度プログラムを実行すると同様の行がもうひとつ追加されるので、名前やら UID やらがダブらないかのチェックは自分でしないといけないようです。他にも ":" などの無効な文字がユーザ名などに入っていてもそのままフィールドに書き込んでしまうため、そのあたりのチェックも行わないといけないようです。

 

■ shadow.h

pwd.h にはそれっぽい関数が見当たらなかったので、ならば shadow.h なるファイルはないかと調べてみたところ、ありました。そしてその中にはそれっぽいものがありました。

  • void setspent(void);
  • void endspent(void);
  • struct spwd* getspent(void);
  • struct spwd* fgetspent(FILE* __stream);
  • struct spwd* getspnam(__const char* __name);
  • int putspent(__const struct spwd* __p, FILE* __stream);
  • int lckpwdf(void);
  • int ulckpwdf(void);

気になったのは UID による取得が出来無そうな感じですけど、それ以外は普通の /etc/passwd の時と同じような関数が見られるので、問題なく利用することが出来そうですね。

あと、spwd 構造体は次のような感じです。

char* sp_namp ユーザ名
char* sp_pwdp 暗号化済みパスワード
long sp_lstchg 1970/01/01 から最後にパスワードを変更した日時までの日数
int sp_min パスワードが変更できるようになるまでの日数
int sp_max パスワードの変更が必要になるまでの日数
int sp_warn パスワードの変更期限を知らせるメッセージを表示するまでの日数
int sp_inact パスワードが無効になるまでの日数
int sp_expire アカウントが使用不能となるまでの日数
int sp_flag (未定)

なにやら利用用途がよくわからないものが多いですけど、これをみる感じ、/etc/shadow にのみ関する関数のようですね。たしかにこれなら UID から情報を検索することは出来なそう。とすると pwd.h と平行して使うという感じでしょうか。

とりあえずどんな風に情報が取得できるのかやってみることにします。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <shadow.h>

 

int main()

{

spwd *pw = getspnam("dummy4");

 

printf("NAME:%s, PASS:%s\n", pw->sp_namp, pw->sp_pwdp);

printf("lastchg:%i, min:%i, max:%i, warn: %i, inact:%i, expire:%i\n", pw->sp_lstchg, pw->sp_min, pw->sp_max, pw->sp_warn, pw->sp_inact, pw->sp_expire);

 

return 0;

}

これを実行してみると次のような結果が得られました。

NAME:dummy4, PASS:$1$1mn0YTu8$IVr8Jb.6MvAMmrhcw10ks0

lstchg:12266, min:0, max:99999, warn:7, inact:-1, expire:-1

当たり前かもしれないですけど、今度はちゃんと暗号化済みパスワードを入手することが出来ました。他、lstchg とか min とかは特に設定されていない状態だったので、これがディフォルトのようです。

 

さて、/etc/shadow ファイルは root 以外が参照することが出来なくなっています。

では、root 以外で getspnam() などを呼び出すことは出来るのでしょうか…。ということでさっそく (?) setuid を使って実ユーザを一般ユーザへ落として実験してみることにします。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <shadow.h>

 

int main()

{

if (setuid(1000)) perror("setuid");

 

spwd *pw = getspnam("dummy4");

 

printf("NAME:%s, PASS:%s\n", pw->sp_namp, pw->sp_pwdp);

printf("lastchg:%i, min:%i, max:%i, warn: %i, inact:%i, expire:%i\n", pw->sp_lstchg, pw->sp_min, pw->sp_max, pw->sp_warn, pw->sp_inact, pw->sp_expire);

 

return 0;

}

すると…、セグメンテーションフォルトです^^;;;;;; おそらくエラー処理が甘いせいで NULL ポインタを操作しているのでしょう…。ということで改めてエラー処理を追加します。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <shadow.h>

 

int main()

{

if (setuid(1000)) perror("setuid");

 

spwd *pw = getspnam("dummy4");

 

if (pw == NULL)

{

perror("getspnam");

exit(1);

}

 

printf("NAME:%s, PASS:%s\n", pw->sp_namp, pw->sp_pwdp);

printf("lastchg:%i, min:%i, max:%i, warn: %i, inact:%i, expire:%i\n", pw->sp_lstchg, pw->sp_min, pw->sp_max, pw->sp_warn, pw->sp_inact, pw->sp_expire);

 

return 0;

}

すると次ぎのような結果です。

getspnam: Permission denied

見事(?)、権限なしでアクセスが拒否されました。

 

■ パスワードの照合

パスワードの照合は、入力されたパスワードを crypt() 関数で暗号化して、暗号化済みパスワード同士が一致するかどうかで調べます。それを行うにあたって、パスワードの暗号化に関するお話から…。

パスワードの暗号化は、平文パスワードと 引数 salt によって行われます。この salt、従来は大文字小文字を含めたアルファベットと数字および "." と "/" からなる2文字の文字列を指定するものだったらしいですけど、glibc2 では高度な暗号化をかけるためにどうやら "$1$" + 先ほど示した文字セットを使った8文字をつかうようです。

salt に指定された文字列は 3+8 文字以上は無視されるようです。厳密には "$1$" から始まって "$" で終わる文字列として定義されているようで、有効な salt 文字列はその約束で判断されているような気がします。

従来の2文字の場合は "DES" という方式の暗号化で、"$1$" + 8文字の方は "MD5" という方式だそうです。

パスワードを設定する際、salt の文字列は無作為に設定されるわけなのですけど、この salt 部分は、暗号化済みパスワードの先頭 8+11 文字にもまったく同じものが現れるようになっているようなので、暗号化前パスワードと、その暗号化済みパスワードがわかれば、それに使った salt がわかる (暗号化済みパスワードの先頭 8+11 字) ようになっています。

これから、暗号化パスワードを導き出します。

 

上のほうで実験した SHADOW パスワードを取得した dummy4 というアカウントですけど、このパスワードは "dummy_pwd" として登録してありました。これを暗号化して、暗号化済みパスワードと一致するかどうかを調べてみます。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#include <string>

 

#include <pwd.h>

#include <shadow.h>

 

int main()

{

char name[100];

char pass[100];

 

// 今回は文字リテラルでユーザ名とパスワードを指定

std::strcpy(name, "dummy4");

std::strcpy(pass, "dummy_pwd");

 

printf("NAME(constant): %s\n", name);

printf("PASS(constant:TEXT): %s\n", pass);

 

// /etc/shadow から暗号化パスワードを取得

spwd *pw = getspnam(name);

 

printf("PASS(constant:/etc/shadow): %s\n", pw->sp_pwdp);

 

// crypt() 関数を使って平文パスワードを暗号化

char *made_pass = crypt(pass, pw->sp_pwdp);

 

printf("PASS(made by crypt): %s\n", made_pass);

 

// パスワードが一致するかどうかのチェック

printf("IS EQUAL: %s\n", (std::strcmp(made_pass, pw->sp_pwdp) == 0 ? "true" : "false"));

 

return 0;

}

ここでつかった crypt() 関数は <unistd.h> をインクルードすることで使えるようになりますが、コンパイル時 (リンク時) にはさらに -lcrypt オプションをつけてライブラリファイルをリンクしてやる必要があります。また、引数が const char* であって、戻りも char* であることから、内部にパスワードを保存するバッファが用意されているような感じですね。

これを実行してみると次のような結果が得られました。

NAME(constant): dummy4

PASS(constant:TEXT): dummy_pwd

PASS(constant:/etc/shadow): $1$1mn0YTu8$IVr8Jb.6MvAMmrhcw10ks0

PASS(made by cript): $1$1mn0YTu8$IVr8Jb.6MvAMmrhcw10ks0

IS EQUAL: true

以上で認証処理は問題なさそうです。

今回の平文パスワードは使い捨てなのでかまわないのですけど、何もしないとメモリ内にしばらく残ることになるので、チェックし終わったらすぐに memset(pass, 0, 100); などを実行してメモリ内にパスワードを残さないようにしましょう。

 

実 ID と実効 ID の違いは…

なかなか実用性がありそうなので、最後にもう少し深く調べておくことにします。

権限の変更関数には、実 ID を対称にするものと実効 ID を対称にするものとがあるようです。今までの実験では実 ID のみを変更して、それにつられて自動的に実効 ID も変動するという感じでした。

実 ID が変化すればそれにつられて実効 ID も変わるというのはなんとも自然な感じがしますけど、では実効 ID の方だけを変えた場合はどうなるのか、操作の制限は実効 ID によって左右されるのか、実効 ID を再び実 ID へ戻すことが出来るのか、このあたりがチェックポイントです。

実効 ID を変更してファイルを作成する

まずはファイル作成のプログラムです。

ソースコートは上記で実験していたものを利用します。変更点は基本的には setgid を setegid に、setuid を seteuid に変更するだけなのですけど、別の実験もかねてさらに seteuid の行と setegid の行を交換してあります。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (seteuid(1000)) perror("seteuid:1");

if (setegid(100)) perror("setegid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度一般ユーザへ変更、権限情報を表示

 

if (seteuid(100)) perror("seteuid:2");

if (setegid(500)) perror("setegid:2");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度権限を root へ変更、権限情報を表示

 

if (seteuid(0)) perror("seteuid:3");

if (setegid(0)) perror("setegid:3");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

これによってどの権限によってファイル作成処理が行われたかを簡単に知ることが出来るはずです。./dummy/ ディレクトリに保存されていたファイルをいったん消去してから実行してみると、次のような結果が得られました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

UID:0, GID:0, NAME:root, HOME:/root, SHELL:/bin/bash

setegid:1: Operation not permitted

UID(EUID): 0(1000)

GID(EGID): 0(0)

root

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

seteuid:2: Operation not permitted

setegid:2: Operation not permitted

UID(EUID): 0(1000)

GID(EGID): 0(0)

root

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

UID(EUID): 0(0)

GID(EGID): 0(0)

root

UID:0, GID:0, NAME:root, HOME:/root, SHELL:/bin/bash

とりあえずファイル権限は後回しとして、次のような興味深い点があります。

  • あたりまえかもしれないですけど実効 ID にのみ変化が見られて、実 ID の方はそれにつられていないということ。
  • 実 ID が root であっても、一般権限の実効 ID から別のユーザの実効 ID には変更できないということ。これは seteuid:2 や setegid:2 のエラーメッセージの他、setegid:1 からも伺えます。
  • 実効 ID を実 ID である root へ復帰できていること。
  • whoami コマンドは常に root と返していること。これは whoami の仕様を知らないのでなんともいえないですけど、プログラムの権限がスイッチするのではなく、一時的に権限が落とされているというような感じがうけられます。

さて、ファイル権限の方ですけど、これは念のため setegid の行と seteuid の行を交換してからもう一度確認してみることにします。念のため、2つとも権限の変更が成功するように、root への復帰を UID = 500 にする前へ移動することにします。

int main()

{

// まずはそのまま権限情報を表示

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// 続いて権限を変更して、再び権限情報を表示

 

if (setegid(100)) perror("setegid:1");

if (seteuid(1000)) perror("seteuid:1");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度権限を root へ変更、権限情報を表示

 

if (setegid(0)) perror("setegid:3");

if (seteuid(0)) perror("seteuid:3");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

// もう一度一般ユーザへ変更、権限情報を表示

 

if (setegid(500)) perror("setegid:2");

if (seteuid(100)) perror("seteuid:2");

 

PRINT_INFO;

MAKE_DUMMYFILE;

 

return 0;

}

あらためて dummy/ ディレクトリ内のファイルを削除してから実行してみると、次のような結果が得られました。

UID(EUID): 0(0)

GID(EGID): 0(0)

root

UID:0, GID:0, NAME:root, HOME:/root, SHELL:/bin/bash

UID(EUID): 0(1000)

GID(EGID): 0(100)

root

UID:1000, GID:100, NAME:dummy1, HOME:/home/dummy1, SHELL:/bin/bash

UID(EUID): 0(0)

GID(EGID): 0(0)

root

UID:0, GID:0, NAME:root, HOME:/root, SHELL:/bin/bash

UID(EUID): 0(500)

GID(EGID): 0(100)

root

UID:500, GID:500, NAME:dummy2, HOME:/home/dummy2, SHELL:/bin/bash

つまり、権限の変更にエラーなしです。最後の GID:500 の部分が GID(EGID) の値とずれていますが、これは dummy2 のアカウント情報自体が GID=500 として登録されているからです。なにはともあれ、権限を変更する際は GID を先に変更して続いて UID を変更するという感じにするのがいいみたいですね。

そして肝心のファイル権限ですけど、こちらもばっちり、実効 ID の方が有効になりました。

 

一応のまとめ

以上のことから、root 権限で走らせなくてはいけないサーバプログラムで安全性を少しでも増すために、直後に実効 ID を固有のものに変更しておいて、パスワード参照とかそういった場合にはいったん実効 ID を root に戻すといった感じにするのがいいかもですね。

 

プロセスの状態

ちょこっと気になることがあったので追加実験です。

whoami を system() 関数で呼び出したときに常に実 ID のユーザ名が返されていたので、次のようなソースコードを使って実験してみることにしました。

#include <iostream>

 

#include <sys/types.h>

#include <unistd.h>

 

#define KEY_STOP(x) std::cout << x << ": (hit any key)" << std::endl; getchar()

 

int main()

{

// ID 変更前です。

KEY_STOP("default");

 

// 実効 ID を変更します。

setegid(100);

seteuid(1000);

 

KEY_STOP("change Effective ID");

 

// 実 ID を変更します(変更前にいったん実効 ID を元に戻します)

setegid(0);

seteuid(0);

 

setgid(100);

setuid(1000);

 

KEY_STOP("change Real ID");

 

return 0;

}

さて、これを実行します。

権限を調整するたびにキー入力待ちになるので、Ctrl+Z を押してプログラムの処理を中断します。そのときにプロセスがどのような権限で実行されているか ps -au コマンドを使って表示してみます。

なお中断されたプロセス(ジョブ) は、jobs コマンドによってジョブ番号を確認し、fg コマンドで復帰させることが出来ます。

表示ラベル プロセスの状態
default root
change Effective ID [dummy1]
change Real ID [dummy1]

とりあえずどちらとも、変更後は dummy1 のプロセスに見えるようです。なにやら [ ] にてくくられていますけど、とりあえず ps コマンドの権限表示は実効 ID の方で表示されるようですね。