細かすぎて伝わらないPerlと$0変数 - コマンド名偽装

先日、moznion氏の「実行中のプログラムの進捗度を手っ取り早く確認したい」という面白い記事を読みました。

これに影響されて、Perlと$0のウンチクを id:lesamoureuses に語ったところ地味にウケが良かったので、さらに調子に乗って、細かすぎて伝わらないPerlと$0の関係について語ります。なお、本稿の実用性はあまりありませんが、ちょっとだけあります。

概要

上述のmoznion氏のブログは、Rubyで$0をいじるとpsコマンドで見えるコマンド名が変わって便利、という話でした(「アッアッ」)。

Perlでも同様に、$0をいじることでpsコマンドで見えるCOMMAND値を変えることができます(なお時折勘違いする人がいますが、元のファイル名は変わりません。psコマンドで見える値だけです)。ただしこれはOSによって結構動作が違うので、以下しばらくLinux(CentOS)限定の話をします。

通常、Perlスクリプトを実行中にpsコマンドで見ると、次のように「[perlインタプリタ] [スクリプトファイル名]」の形で見えます。

[ozuma@cent6 ~]$ ps x
  PID TTY      STAT   TIME COMMAND
....(省略)....
1842 pts/0    S+     0:00 /usr/bin/perl ./script.pl
1843 pts/1    R+     0:00 ps x
[ozuma@cent6 ~]$

ではここで、次のようにPerlスクリプト内で、$0に"notepad.exe"という値を代入してみましょう。

#!/usr/bin/perl
use strict;
use warnings;

$0 = "notepad.exe";
sleep 100;

この状態で別端末からpsコマンドを実行すると、まるでnotepad.exeを実行しているように見えます。

[ozuma@cent6 ~]$ ps x
  PID TTY      STAT   TIME COMMAND
....(省略)....
1834 pts/0    S+     0:00 notepad.exe
1835 pts/1    R+     0:00 ps x
[ozuma@cent6 ~]$

この動きは、Perldocの Perlの組み込み変数 $0 の翻訳 - perldoc.jp にもはっきり書かれています。

psコマンドの実装

なぜこんなことが起こるかを理解するには、Linuxのpsコマンドの実装を知る必要があります。

Linux(CentOS)では、psコマンドはprocpsというパッケージに入っています。

[ozuma@cent6 fork]$ rpm -qf /bin/ps
procps-3.2.8-25.el6.x86_64

procpsの公式ページからpsコマンドのソースコードを見てみると、これは内部で/proc配下のファイルを読んでいるようです。あるいは、ソースコード読むのメンドクセーという人は、straceを付けてpsコマンドを実行してみればすぐ分かります。

$ strace ps x
....(省略)....
stat("/proc/1945", {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
open("/proc/1945/stat", O_RDONLY)       = 5
read(5, "1945 (strace) R 1706 1945 1706 3"..., 1023) = 224
close(5)                                = 0
open("/proc/1945/status", O_RDONLY)     = 5
read(5, "Name:\tstrace\nState:\tR (running)\n"..., 1023) = 894
close(5)                                = 0
open("/proc/1945/cmdline", O_RDONLY)    = 5
read(5, "strace\0ps\0x\0", 2047)        = 12
close(5)                                = 0
....(省略)....

このように、psコマンドでは/proc/(プロセスID)/cmdlineというprocファイルから、表示するCOMMAND名を取得しています。実際、Perlスクリプト中で$0を書き換えると、その時点でcmdlineファイルの内容が書き換わることが分かります(ので、各自やってみてください)。

ただし、procファイルにはもう一つ、commというファイルがあります。こちらは$0を書き換えても変わりません(ので、これも各自やってみてください)。

ここまでのまとめ
ファイル名 解釈
/proc/(プロセスID)/cmdline $0で書き換え可能。psコマンドで見える値
/proc/(プロセスID)/comm $0では変わらない。

書き換えられたcmdlineと変わらないcomm

先ほど、「psコマンドはprocファイルのcmdlineを読む」と書きましたが、これは正確ではありません。正しくは、「(一般的なオプションを付けて)psコマンドを実行すると、procファイルのcmdlineを読む」が正解です。

実は、psコマンド実行時にcという誰も知らないマイナーなオプションを付けると、commの方を表示します。これはmanでは、「本当のコマンド名を表示する(Show the true command name)」と表現されています。

[ozuma@cent6 ~]$ ps x
  PID TTY      STAT   TIME COMMAND
1597 ?        S      0:00 sshd: ozuma@pts/0
1598 pts/0    Ss     0:00 -bash
2065 ?        S      0:00 sshd: ozuma@pts/1
2066 pts/1    Ss     0:00 -bash
2096 pts/0    S+     0:00 notepad.exe
2097 pts/1    R+     0:00 ps x

[ozuma@cent6 ~]$ ps cx
  PID TTY      STAT   TIME COMMAND
1597 ?        S      0:00 sshd
1598 pts/0    Ss     0:00 bash
2065 ?        S      0:00 sshd
2066 pts/1    Ss     0:00 bash
2096 pts/0    S+     0:00 script.pl
2098 pts/1    R+     0:00 ps
[ozuma@cent6 ~]$

上の例で、PID=2096のプロセスは、はじめはnotepad.exeに見えましたが、次のcオプションではscript.plに見えます。化けの皮がはがれました。

もう一つ、psコマンドでは無くpstreeコマンドを使っても簡単に元のコマンド名を見ることができます。

[ozuma@cent6 ~]$ pstree
init─┬─NetworkManager
     ├─VBoxService───7*[{VBoxService}]
....(省略)....
     ├─rsyslogd───3*[{rsyslogd}]
     ├─sshd─┬─sshd───sshd───bash───script.pl
     │      └─sshd───sshd───bash───pstree
     ├─udevd───2*[udevd]
     └─wpa_supplicant
[ozuma@cent6 ~]$

これは、pstreeコマンドは/proc/(プロセスID)/statファイルを読み込みそこからコマンド名を取得するので、/proc/(プロセスID)/cmdlineは読まないためです。そのため、$0書き換えはpstreeコマンドには効きません。

$0を書き換える目的

この$0は、プログラムの動作状態を表示するために使うのが一番まっとうなやり方です。例えば、実行状態によってscript.pl(run)とscript.pl(wait)のように$0を適宜書き換えれば、psで監視している人は分かりやすくてハッピーです。

もう一つ、$0操作はpsコマンドからの隠蔽目的でもよく使われます。具体的には、Perlマルウェアを書いている人たちの間では、この$0いじりをするのは常套手段でありよく使われる手法です。

以下は、以前に収集した、PHP脆弱性を狙って送り込まれてきたボットネット用のPerlクライアント(PerlBot)ソースの一部です。

my $processo =("suid","/usr/sbin/sshd","rpc.idmapd","auditd","crond","klogd -x");
....(省略)....
$0="$processo"."\0"x16;;

このように、悪意のあるスクリプトを"/usr/sbin/sshd"とか、あるいはcrondなど一般的なデーモンのように見せかけることで、psコマンドで見つかりにくくするわけです。psコマンドを打って/usr/sbin/sshdがひとつ紛れ込んでいても、目でパッと見るくらいなら普通は気がつきません。

fork()すると?

もはや細かすぎて誰も興味ないと思いますが、$0を書き換えた状態でfork()すると、子プロセスへも$0改変は引き継がれます。

#!/usr/bin/perl

$0 = "notepad.exe";
fork();
sleep 100;

こうすると、notepad.exeが2つ見えます。

[ozuma@cent6 ps]$ ps x
  PID TTY      STAT   TIME COMMAND
1597 ?        S      0:00 sshd: ozuma@pts/0
1598 pts/0    Ss     0:00 -bash
1705 ?        S      0:00 sshd: ozuma@pts/1
1706 pts/1    Ss     0:00 -bash
1981 pts/0    S+     0:00 notepad.exe
1982 pts/0    S+     0:00 notepad.exe
1983 pts/1    R+     0:00 ps x
[ozuma@cent6 ps]$

だから何? って言われても困ります。

OSによる違い

冒頭に述べましたが、$0をいじったスクリプトがpsコマンドでどう見えるかは、OSによって違います。Perlのドキュメントにはっきり書かれている通り、FreeBSDでは$0をいじっても後ろに(perl x.x.x)という文字列が必ず入ります。そのためFreeBSDでは、Perlで書かれたマルウェアがps偽装するのも難しくなります。

[ozuma@freebsd9 ~]$ ps x
PID TT  STAT    TIME COMMAND
....(省略)....
621  0  S+   0:00.00 notepad.exe (perl5.16.3)
617  1  Ss   0:00.00 -bash (bash)
622  1  R+   0:00.00 ps x
[ozuma@freebsd9 ~]$

ちなみにSolarisでは(若い人は知らなくていいOSです)、以下のように$0をいじってpsコマンドで見ても、何も変わっていません。元のコマンドラインそのままが表示されます。

Solarisのpsコマンドは、Linuxとは違ってカーネル内のプロセステーブルから直接持ってくるため、Perlスクリプト内で何をしようがpsの出力は変わりません(……だったはず)。

一応最後に教訓めいたことを書くと、侵入されて悪意のあるプロセスが動いているかもしれないサーバ上でプロセス調査するには、Linuxではpsコマンドにcオプションを付けた出力も取得しておいた方が良い、ということです。またFreeBSDSolarisなら、$0偽装が効かないのでこの辺の判別がやりやすくなります。

もっともこんなケースでは、そもそもpsコマンド自体が侵入者に置き換えられてしまっているのでは……とかフォレンジック的に色々心配しないといけないことがあるので、あまり実用的とは言えない知識です。まぁ小ネタですね。

(追記) あわせて読みたい

@k_morihisaさんが、実際に仕込まれそうになったIRCBOTのソースコードを元に、偽装プロセス名の一覧を作られています。興味深い。