#!/usr/bin/env bash
# Read-only IOC check for the cloud VM incident documented on the blog.
# Supports systemd-based Linux hosts (including Debian and AlmaLinux).
# It does not delete files, stop services, change firewall rules, or contact a network.

set -u

readonly PAYLOAD='/var/cache/.locale-cache-4h6cu8/hwdb-index.dat'
readonly UNIT_NAME='systemd-hwdb-sync.service'
readonly UNIT_PATH='/usr/lib/systemd/system/systemd-hwdb-sync.service'
readonly ALT_UNIT_PATH='/etc/systemd/system/systemd-hwdb-sync.service'
readonly PORT='45139'
readonly PROCESS_NAME='kworker/0:5H'
readonly PAYLOAD_SHA256='f9c7ede43c0bccb7d6299a5c4a18966131fb8dff90640198ab2cc833daffd0ad'
readonly UNIT_SHA256='659137a061062acb6fda782ab464509b606c84cc915da622a3262ea5b7c7f90e'

findings=0
warnings=0

info() { printf '[信息] %s\n' "$*"; }
warn() { printf '[注意] %s\n' "$*"; warnings=$((warnings + 1)); }
hit() { printf '[可疑] %s\n' "$*"; findings=$((findings + 1)); }

if [[ ${1:-} == '--help' || ${1:-} == '-h' ]]; then
  cat <<'HELP'
用法：sudo bash check-cloud-vm-incident-iocs.sh

只读检查本次云主机事件的已知 IOC：恶意文件/哈希、systemd 持久化、
进程名和 TCP 45139 监听，以及相关防火墙规则。不会删除、隔离或修改任何内容，
也不会联网。建议以 root 运行以读取完整进程和网络信息。

退出码：0 未匹配本次已知 IOC；1 匹配到至少一项可疑指标。
未匹配不代表主机一定干净；此脚本不检查云服务商控制面、完整日志或其他未知恶意行为。
HELP
  exit 0
fi

printf '%s\n' '云主机事件 IOC 只读检查' '========================'
if [[ ${EUID:-$(id -u)} -ne 0 ]]; then
  warn '当前不是 root；部分进程、监听进程名和防火墙信息可能不可见。'
fi

# Exact malware payload path and content hash.
if [[ -e "$PAYLOAD" || -L "$PAYLOAD" ]]; then
  hit "发现恶意程序特征路径：$PAYLOAD"
  if [[ -f "$PAYLOAD" ]] && command -v sha256sum >/dev/null 2>&1; then
    observed=$(sha256sum -- "$PAYLOAD" 2>/dev/null | awk '{print $1}')
    if [[ "$observed" == "$PAYLOAD_SHA256" ]]; then
      hit "恶意程序 SHA-256 与本次样本完全匹配：$PAYLOAD_SHA256"
    elif [[ -n "$observed" ]]; then
      warn "该路径文件哈希不同于已归档样本；仍需调查该路径和文件。SHA-256=$observed"
    fi
  fi
else
  info '未发现本次恶意程序的精确文件路径。'
fi

# Exact unit paths and behavior markers. Do not print arbitrary unit contents.
unit_found=0
for unit_file in "$UNIT_PATH" "$ALT_UNIT_PATH"; do
  if [[ -e "$unit_file" || -L "$unit_file" ]]; then
    unit_found=1
    hit "发现可疑 systemd unit 路径：$unit_file"
    if [[ -f "$unit_file" ]] && command -v sha256sum >/dev/null 2>&1; then
      observed=$(sha256sum -- "$unit_file" 2>/dev/null | awk '{print $1}')
      if [[ "$observed" == "$UNIT_SHA256" ]]; then
        hit "unit SHA-256 与本次取证文件完全匹配：$UNIT_SHA256"
      fi
    fi
    if [[ -f "$unit_file" ]] && grep -Eiq '45139|hwdb-index\.dat|kworker/0:5H|iptables[[:space:]].*--dport' "$unit_file"; then
      hit 'unit 文件包含本次事件的端口、程序路径或伪装进程特征。'
    fi
  fi
done
if (( unit_found == 0 )); then
  info '未发现本次 systemd unit 的精确路径。'
fi

if command -v systemctl >/dev/null 2>&1; then
  unit_state=$(systemctl show "$UNIT_NAME" -p LoadState -p ActiveState -p UnitFileState 2>/dev/null || true)
  if [[ -n "$unit_state" && "$unit_state" != *'LoadState=not-found'* ]]; then
    hit "systemd 当前识别到 $UNIT_NAME：${unit_state//$'\n'/; }"
  fi
else
  warn '未找到 systemctl，无法查询 systemd 单元状态。'
fi

# A fake kernel-worker process name was used by the sample. Never print full argv.
process_hits=0
for proc_dir in /proc/[0-9]*; do
  [[ -r "$proc_dir/comm" ]] || continue
  pid=${proc_dir##*/}
  IFS= read -r comm < "$proc_dir/comm" || comm=''
  cmdline=$(tr '\0' ' ' < "$proc_dir/cmdline" 2>/dev/null || true)
  if [[ "$comm" == "$PROCESS_NAME" || "$cmdline" == *"$PAYLOAD"* ]]; then
    exe=$(readlink -f "$proc_dir/exe" 2>/dev/null || true)
    exe_name=${exe##*/}
    hit "发现匹配进程：PID=$pid，名称=$comm，执行文件=${exe_name:-不可读取}。未输出完整命令行。"
    process_hits=$((process_hits + 1))
  fi
done
if (( process_hits == 0 )); then
  info '未发现匹配的进程名或程序路径。'
fi

# TCP listener only; the archived sample is a TCP SOCKS5 proxy.
if command -v ss >/dev/null 2>&1; then
  socket_hits=$(ss -H -ltnp 2>/dev/null | awk -v p="$PORT" 'index($4, ":" p) && $4 ~ (":" p "$") {print $4 " " $6}')
  if [[ -n "$socket_hits" ]]; then
    while IFS= read -r row; do hit "发现 TCP $PORT 监听：$row"; done <<< "$socket_hits"
  else
    info "未发现 TCP $PORT 监听。"
  fi
else
  warn '未找到 ss，无法检查 TCP 监听端口。'
fi

# Check for the specific firewall opening behavior used by the service.
firewall_hit=0
if command -v iptables-save >/dev/null 2>&1 && iptables-save 2>/dev/null | grep -Eq -- "--dport[[:space:]]+$PORT([[:space:]]|$)"; then
  hit "iptables 规则引用 TCP $PORT。"
  firewall_hit=1
fi
if command -v nft >/dev/null 2>&1 && nft list ruleset 2>/dev/null | grep -Eq -- "dport[[:space:]]+$PORT([[:space:]]|$)"; then
  hit "nftables 规则引用 TCP $PORT。"
  firewall_hit=1
fi
if (( firewall_hit == 0 )); then
  info "未在可读取的防火墙规则中发现 TCP $PORT。"
fi

# This is exposure context, not an IOC or proof of compromise.
if command -v sshd >/dev/null 2>&1; then
  ssh_settings=$(sshd -T 2>/dev/null | awk '$1 == "passwordauthentication" || $1 == "permitrootlogin" {print $1 "=" $2}')
  if [[ -n "$ssh_settings" ]]; then
    info '当前 sshd 基础有效设置（Match 规则可能使具体连接结果不同）：'
    while IFS= read -r setting; do printf '  %s\n' "$setting"; done <<< "$ssh_settings"
    if grep -qx 'passwordauthentication yes' <<< "$ssh_settings" || grep -Eq '^permitrootlogin yes$' <<< "$ssh_settings"; then
      warn 'SSH 密码或 root 登录策略较宽松；这是暴露面提示，不代表已发现入侵。'
    fi
  fi
fi

printf '\n发现可疑项：%d；检查受限提示：%d\n' "$findings" "$warnings"
if (( findings > 0 )); then
  printf '%s\n' '结论：命中本次事件相关 IOC。请先保全证据并隔离评估，不要直接删除文件或运行样本。'
  exit 1
fi
printf '%s\n' '结论：未命中本次已知 IOC；这不等于证明主机干净。'
exit 0
