This commit is contained in:
root
2026-04-19 00:21:08 +00:00
parent 9c653108c3
commit c20c40159a
3 changed files with 1996 additions and 0 deletions
Executable
+910
View File
@@ -0,0 +1,910 @@
#!/bin/bash
###############################################################################
# perfctl / perfcc Malware Cleanup Script
# Targets: LD_PRELOAD rootkit (libgcwrap.so), cryptominer, proxyjacking,
# SSH backdoors, SUID backdoors, trojanized tools, cron persistence
#
# Usage:
# ROOTFS=/path/to/mounted/rootfs ./clean.sh
#
# Cold-disk only. Intended for container rootfs scanning — OpenVZ 7
# (e.g. /vz/root/<CTID>) or Incus (e.g. /var/lib/incus/storage-pools/
# <pool>/containers/<name>/rootfs), or any extracted rootfs tarball.
#
# The script is non-destructive to user data (MySQL, nginx configs, node apps,
# /home, /var/www, /var/backups). It only targets known perfctl IoCs.
###############################################################################
set -euo pipefail
if [ -z "${ROOTFS:-}" ]; then
echo "ERROR: ROOTFS is not set. Refusing to run against live '/'." >&2
echo "Usage: ROOTFS=/mnt/ct-cleanup $0" >&2
exit 1
fi
R="$ROOTFS"
LOG="/var/log/perfctl-cleanup-${ROOTFS//\//_}-$(date +%Y%m%d-%H%M%S).log"
FORENSICS_DIR="${FORENSICS_DIR:-/root/forensics-$(date +%Y%m%d-%H%M%S)}"
FOUND=0
REMOVED=0
ERRORS=0
# ---------- helpers ----------------------------------------------------------
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
warn() { echo "[$(date '+%H:%M:%S')] WARNING: $*" | tee -a "$LOG"; }
ok() { echo "[$(date '+%H:%M:%S')] OK: $*" | tee -a "$LOG"; }
fail() { echo "[$(date '+%H:%M:%S')] FAIL: $*" | tee -a "$LOG"; ERRORS=$((ERRORS+1)); }
remove_file() {
local f="$R$1"
[ -e "$f" ] || [ -L "$f" ] || return 0
FOUND=$((FOUND+1))
log " removing: $f"
chattr -i "$f" 2>/dev/null || true
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
}
remove_dir() {
local d="$R$1"
[ -d "$d" ] || return 0
FOUND=$((FOUND+1))
log " removing dir: $d"
chattr -R -i "$d" 2>/dev/null || true
rm -rf "$d" && REMOVED=$((REMOVED+1)) || fail "could not remove $d"
}
save_forensic() {
local f="$R$1"
[ -e "$f" ] || return 0
mkdir -p "$FORENSICS_DIR"
local base
base=$(echo "$1" | tr '/' '_')
cp -a "$f" "$FORENSICS_DIR/$base" 2>/dev/null && \
log " saved forensic copy: $FORENSICS_DIR/$base" || true
}
###############################################################################
echo "You are about to run cleanup for '${R:-/}' system."
read -r -p "Confirm /y/n to proceed: " _confirm
case "$_confirm" in
y|Y|yes|YES) ;;
*) echo "Aborted."; exit 1 ;;
esac
log "=========================================="
log "perfctl cleanup started"
log "ROOTFS prefix: '${R:-/}'"
log "Forensics dir: $FORENSICS_DIR"
log "=========================================="
# ---------- 0. strip immutable flags ----------------------------------------
log ""
log "=== PHASE 0: Strip immutable flags ==="
for d in etc lib usr/lib bin usr/bin sbin usr/sbin tmp root var/spool; do
[ -d "$R/$d" ] && chattr -R -i "$R/$d" 2>/dev/null || true
done
ok "immutable flags cleared"
# ---------- 1. save forensic samples ----------------------------------------
log ""
log "=== PHASE 1: Save forensic samples ==="
for f in \
/etc/ld.so.preload \
/lib/libgcwrap.so \
/bin/perfcc \
/bin/wizlmsh \
/tmp/.perf.c/pctl \
; do
save_forensic "$f"
done
if [ -d "$R/tmp/.xdiag" ]; then
mkdir -p "$FORENSICS_DIR"
tar czf "$FORENSICS_DIR/_tmp_.xdiag.tgz" -C "$R" tmp/.xdiag 2>/dev/null && \
log " saved forensic copy: $FORENSICS_DIR/_tmp_.xdiag.tgz" || true
fi
# ---------- 2. remove rootkit + payload files --------------------------------
log ""
log "=== PHASE 2: Remove rootkit, payload, and dropper files ==="
# ld.so.preload (the rootkit injector)
remove_file /etc/ld.so.preload
remove_file /etc/ld.so.preload_
remove_file /etc/ld.so.preload.bak
# userland rootkit shared objects
for so in libgcwrap.so libfsnldev.so libpprocps.so; do
remove_file "/lib/$so"
remove_file "/usr/lib/$so"
done
# main malware installer
remove_file /bin/perfcc
remove_file /usr/bin/perfcc
# SUID backdoor
remove_file /bin/wizlmsh
remove_file /usr/bin/wizlmsh
# other known dropper/miner binaries
remove_file /bin/kkbush
remove_file /usr/bin/kkbush
remove_file /tmp/.apid
remove_file /tmp/gd.sh
remove_file /tmp/ccrl
remove_file /tmp/kubeupd
remove_file /tmp/javax64
remove_file /tmp/wttwe
remove_file /dev/shm/libfsnldev.so
# ---------- 3. remove malware directories ------------------------------------
log ""
log "=== PHASE 3: Remove malware directories ==="
remove_dir /tmp/.xdiag
remove_dir /tmp/.perf.c
remove_dir /tmp/.dmesg
remove_dir /bin/wbin
remove_dir /bin/.local
remove_dir /bin/.atmp
remove_dir /root/.config/cron
remove_dir /root/.config/traffmonetizer
# ---------- 4. find and remove hidden miner binaries -------------------------
log ""
log "=== PHASE 4: Scan for hidden miner binaries ==="
# statically-linked unpackaged ELFs in system dirs (miner signature)
while IFS= read -r f; do
[ -z "$f" ] && continue
if file "$f" 2>/dev/null | grep -q "statically linked"; then
if ! dpkg -S "$f" >/dev/null 2>&1; then
log " SUSPECT STATIC BINARY: $f ($(stat -c%s "$f" 2>/dev/null) bytes)"
save_forensic "${f#$R}"
FOUND=$((FOUND+1))
chattr -i "$f" 2>/dev/null || true
rm -f "$f" && REMOVED=$((REMOVED+1)) && log " removed: $f" || fail "could not remove $f"
fi
fi
done < <(find "$R/usr/bin" "$R/usr/sbin" "$R/bin" "$R/sbin" -xdev -type f 2>/dev/null)
# ---------- 5. remove proxyjacking container rootfs --------------------------
log ""
log "=== PHASE 5: Remove proxyjacking leftovers ==="
# known proxyjacking rootfs paths (attacker renames to hide)
remove_dir /var/-nonrpk
remove_dir /var/.r.rpk
# Docker/containerd proxyjacking remnants
for d in /bin/wbin /tmp/kubeupd; do
remove_dir "$d"
done
# scan /var/ and /dev/shm/ for ANY hidden files/dirs the attacker may have stashed
log " scanning /var/ for hidden entries..."
KNOWN_VAR_HIDDEN=".updated|.placeholder|.gitkeep|.gitignore|.prettierrc|.prettierignore|.dockerignore|.env"
while IFS= read -r entry; do
[ -z "$entry" ] && continue
base=$(basename "$entry")
# skip known-safe dotfiles
if echo "$base" | grep -qE "^($KNOWN_VAR_HIDDEN)$"; then
continue
fi
# auto-remove known perfctl proxyjacking dirs
case "$base" in
.r.rpk|.rpk|.-nonrpk|.nonrpk|.proxy*|.bitping*|.earnfm*|.speedshare*|.repocket*)
log " removing known proxyjacking dir: $entry"
chattr -R -i "$entry" 2>/dev/null || true
rm -rf "$entry" && REMOVED=$((REMOVED+1)) || fail "could not remove $entry"
FOUND=$((FOUND+1))
;;
*)
warn "UNKNOWN HIDDEN ENTRY in /var/: $entry (review manually)"
ls -la "$entry" 2>/dev/null | tee -a "$LOG"
;;
esac
done < <(find "$R/var" -maxdepth 2 -name '.*' -not -name '.' -not -name '..' \
-not -path '*/lib/*' -not -path '*/cache/*' -not -path '*/log/*' \
-not -path '*/spool/*' -not -path '*/backups/*' -not -path '*/www/*' \
2>/dev/null)
log " scanning /dev/shm/ for hidden entries..."
while IFS= read -r entry; do
[ -z "$entry" ] && continue
base=$(basename "$entry")
case "$base" in
.ICE-unix|.X11-unix|.XIM-unix|.font-unix) continue ;; # standard X11
esac
warn "HIDDEN ENTRY in /dev/shm/: $entry (review manually)"
ls -la "$entry" 2>/dev/null | tee -a "$LOG"
# auto-remove if it matches known malware patterns
if file "$entry" 2>/dev/null | grep -qE 'ELF|executable|shared object'; then
log " removing suspicious binary: $entry"
save_forensic "${entry#$R}"
rm -rf "$entry" && REMOVED=$((REMOVED+1)) || fail "could not remove $entry"
FOUND=$((FOUND+1))
fi
done < <(find "$R/dev/shm" -maxdepth 2 -name '.*' -not -name '.' -not -name '..' 2>/dev/null)
# ---------- 6. clean trojanized tools ---------------------------------------
log ""
log "=== PHASE 6: Remove trojanized tool wrappers ==="
# perfctl drops SHC-compiled wrappers for top, ldd, lsof, crontab
for f in top ldd lsof crontab htop strace mount; do
for d in /bin/.local/bin /bin/.atmp; do
remove_file "$d/$f"
done
done
# ---------- 7. clean .profile / shell RC hooks -------------------------------
log ""
log "=== PHASE 7: Clean shell RC hooks ==="
for rc in \
/root/.profile \
/root/.bashrc \
/root/.bash_profile \
/etc/profile \
/etc/bash.bashrc \
; do
f="$R$rc"
[ -f "$f" ] || continue
if grep -qE 'perfcc|FPROF|AAZHDE|perfctl|libgcwrap|\.perf\.c|\.xdiag' "$f" 2>/dev/null; then
log " cleaning $rc"
sed -i.bak '/perfcc\|FPROF\|AAZHDE\|perfctl\|libgcwrap\|\.perf\.c\|\.xdiag/d' "$f"
rm -f "${f}.bak" 2>/dev/null
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
fi
done
# ---------- 8. neuter 'news' / system-account SSH backdoor -------------------
log ""
log "=== PHASE 8: Remove SSH backdoors on system accounts ==="
# trailing-space nologin binary (copy of dash used as backdoor shell)
find "$R/usr/sbin" "$R/sbin" -maxdepth 1 -name 'nologin *' -print 2>/dev/null | while read -r f; do
log " removing backdoor binary: $f"
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
FOUND=$((FOUND+1))
done
# trailing-space entry in /etc/shells
if [ -f "$R/etc/shells" ]; then
if grep -qE '^/usr/sbin/nologin +$|^/sbin/nologin +$' "$R/etc/shells" 2>/dev/null; then
log " removing trailing-space nologin from /etc/shells"
sed -i -E '/^\/usr\/sbin\/nologin +$/d; /^\/sbin\/nologin +$/d' "$R/etc/shells"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
fi
fi
# trailing space in /etc/passwd news line
if [ -f "$R/etc/passwd" ]; then
if grep -qP '^news:.*nologin +$' "$R/etc/passwd" 2>/dev/null; then
log " fixing news account trailing space in /etc/passwd"
sed -i -E 's|(^news:.*:/usr/sbin/nologin) +$|\1|' "$R/etc/passwd"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
fi
fi
# remove attacker authorized_keys from system accounts
SYS_USERS="news bin daemon sys lp mail games uucp proxy www-data backup list irc gnats nobody systemd-network"
for u in $SYS_USERS; do
home=$(awk -F: -v u="$u" '$1==u{print $6}' "$R/etc/passwd" 2>/dev/null)
[ -n "$home" ] && [ -d "$R$home/.ssh" ] && {
log " removing .ssh from system account: $u ($home)"
save_forensic "$home/.ssh/authorized_keys"
rm -rf "$R$home/.ssh"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
}
done
# ---------- 9. clean cron persistence ----------------------------------------
log ""
log "=== PHASE 9: Clean cron persistence ==="
# named cron files
for f in \
/etc/cron.d/perfclean \
/etc/cron.hourly/perfclean \
/etc/cron.daily/perfclean \
/etc/cron.weekly/perfclean \
/etc/cron.monthly/perfclean \
; do
remove_file "$f"
done
# clean root crontab entries
if [ -f "$R/var/spool/cron/crontabs/root" ]; then
if grep -qE 'perfcc|perfctl|libgcwrap|AAZHDE|FPROF|\.xdiag|\.perf\.c' "$R/var/spool/cron/crontabs/root" 2>/dev/null; then
log " cleaning root crontab"
chattr -i "$R/var/spool/cron/crontabs/root" 2>/dev/null || true
sed -i '/perfcc\|perfctl\|libgcwrap\|AAZHDE\|FPROF\|\.xdiag\|\.perf\.c/d' "$R/var/spool/cron/crontabs/root"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
fi
fi
# remove system-user crontabs (shouldn't exist)
if [ -d "$R/var/spool/cron/crontabs" ]; then
for ct in "$R"/var/spool/cron/crontabs/*; do
[ -f "$ct" ] || continue
base=$(basename "$ct")
[ "$base" = "root" ] && continue
log " removing system-user crontab: $base"
chattr -i "$ct" 2>/dev/null || true
rm -f "$ct"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
done
fi
# ---------- 10. remove systemd persistence -----------------------------------
log ""
log "=== PHASE 10: Remove systemd persistence ==="
SUSP_PATTERN='perfcc\|perfctl\|libgcwrap\|kubeupd\|kkbush\|AAZHDE\|ExecStart=/tmp'
for sdir in "$R/etc/systemd" "$R/usr/lib/systemd" "$R/lib/systemd"; do
[ -d "$sdir" ] || continue
grep -rl "$SUSP_PATTERN" "$sdir" 2>/dev/null | while read -r u; do
log " removing systemd unit: $u"
base=$(basename "$u")
find "$R/etc/systemd" -name "$base" -lname '*' -delete 2>/dev/null
rm -f "$u"
FOUND=$((FOUND+1)); REMOVED=$((REMOVED+1))
done
done
# ---------- 11. full filesystem IoC scan -------------------------------------
log ""
log "=== PHASE 11: Full filesystem IoC scan ==="
REMAINING=$(find "$R/" -xdev \( \
-name 'libgcwrap*' -o -name 'perfcc*' -o -name 'perfctl*' -o -name 'perfclean*' \
-o -name 'libfsnldev*' -o -name 'libpprocps*' -o -name 'wizlmsh*' \
-o -name 'javax64*' -o -name '.xdiag' -o -name '.perf.c' \
-o -name 'kkbush*' -o -name 'kubeupd*' -o -name 'ld.so.preload*' \
-o -name 'zipgrepjournalctl*' \
\) 2>/dev/null || true)
if [ -n "$REMAINING" ]; then
warn "IoC files still found after cleanup:"
echo "$REMAINING" | while read -r f; do
warn " $f"
chattr -i "$f" 2>/dev/null || true
rm -rf "$f" 2>/dev/null && log " force-removed: $f" || fail " STUCK: $f"
done
else
ok "no IoC files remaining on disk"
fi
# ---------- 12. grep sweep for residual strings ------------------------------
log ""
log "=== PHASE 12: Grep sweep for residual rootkit strings ==="
GREP_HITS=$(grep -rlE 'libgcwrap|perfcc|perfctl|AAZHDE|FPROF|\.xdiag|\.perf\.c|wizlmsh|zipgrepjournalctl' \
"$R/etc" "$R/root" "$R/var/spool" 2>/dev/null | \
grep -v '\.log$\|\.tgz$\|forensics' || true)
if [ -n "$GREP_HITS" ]; then
warn "files still containing rootkit strings (review manually):"
echo "$GREP_HITS" | tee -a "$LOG"
else
ok "no residual rootkit strings in config/cron/home"
fi
# ---------- 13. PAM / NSS hijack check --------------------------------------
log ""
log "=== PHASE 13: PAM / NSS hijack check ==="
# PAM modules: flag any .so in security/ that isn't a pam_<name>.so from
# libpam-modules* / libpam-runtime
for secdir in "$R/lib/security" "$R"/lib/*/security "$R/usr/lib/security" "$R"/usr/lib/*/security; do
[ -d "$secdir" ] || continue
for so in "$secdir"/*.so; do
[ -f "$so" ] || continue
base=$(basename "$so")
case "$base" in
pam_*.so) ;;
*)
warn "non-standard file in PAM security dir: $so"
save_forensic "${so#$R}"
;;
esac
done
done
# pam.d configs referencing absolute paths (modules are normally bare names)
if [ -d "$R/etc/pam.d" ]; then
PAM_ABS_HITS=$(grep -rEn '^[^#]*\s/(tmp|opt|usr/local|home|var/tmp)/' "$R/etc/pam.d/" 2>/dev/null || true)
if [ -n "$PAM_ABS_HITS" ]; then
warn "pam.d references suspicious absolute paths:"
echo "$PAM_ABS_HITS" | tee -a "$LOG"
fi
fi
# NSS modules: list libnss_*.so not matching a known package provider
for libdir in "$R"/lib/*/ "$R"/usr/lib/*/ "$R/lib" "$R/usr/lib"; do
[ -d "$libdir" ] || continue
for so in "$libdir"/libnss_*.so*; do
[ -f "$so" ] || continue
base=$(basename "$so")
case "$base" in
libnss_compat.*|libnss_dns.*|libnss_files.*|libnss_hesiod.*|libnss_nis.*|libnss_nisplus.*|libnss_db.*|libnss_mdns*|libnss_myhostname.*|libnss_resolve.*|libnss_systemd.*|libnss_mymachines.*|libnss_sss.*|libnss_ldap.*|libnss_winbind.*|libnss_wins.*) ;;
*)
warn "non-standard NSS module: $so"
save_forensic "${so#$R}"
;;
esac
done
done
ok "PAM/NSS check complete"
# ---------- 14. SSH server configuration tampering --------------------------
log ""
log "=== PHASE 14: SSH server configuration tampering ==="
SSHD_FILES=""
[ -f "$R/etc/ssh/sshd_config" ] && SSHD_FILES="$R/etc/ssh/sshd_config"
if [ -d "$R/etc/ssh/sshd_config.d" ]; then
for f in "$R"/etc/ssh/sshd_config.d/*.conf; do
[ -f "$f" ] && SSHD_FILES="$SSHD_FILES $f"
done
fi
if [ -n "$SSHD_FILES" ]; then
# dangerous directives
SSH_HITS=$(grep -HnE '^\s*(PermitRootLogin\s+yes|PasswordAuthentication\s+yes|PermitEmptyPasswords\s+yes|ForceCommand|AuthorizedKeysCommand|AuthorizedKeysFile\s+(/tmp|/opt|/home|/var/tmp)|Subsystem\s+.*(/tmp|/opt|/dev/shm))' $SSHD_FILES 2>/dev/null || true)
if [ -n "$SSH_HITS" ]; then
warn "sshd_config contains suspicious directives:"
echo "$SSH_HITS" | tee -a "$LOG"
fi
# top-level Match blocks (can override per-user policy)
MATCH_HITS=$(grep -HnE '^\s*Match\s+' $SSHD_FILES 2>/dev/null || true)
if [ -n "$MATCH_HITS" ]; then
log " sshd_config Match blocks (review):"
echo "$MATCH_HITS" | tee -a "$LOG"
fi
# trailing-space paths in sshd_config (same backdoor trick as nologin )
TS_HITS=$(grep -HnE ' +$' $SSHD_FILES 2>/dev/null || true)
if [ -n "$TS_HITS" ]; then
warn "sshd_config lines with trailing whitespace (possible backdoor):"
echo "$TS_HITS" | tee -a "$LOG"
fi
fi
ok "sshd_config check complete"
# ---------- 15. sudoers tampering -------------------------------------------
log ""
log "=== PHASE 15: sudoers tampering ==="
SUDO_FILES=""
[ -f "$R/etc/sudoers" ] && SUDO_FILES="$R/etc/sudoers"
if [ -d "$R/etc/sudoers.d" ]; then
for f in "$R"/etc/sudoers.d/*; do
[ -f "$f" ] || continue
case "$(basename "$f")" in
README|*.dpkg-*|*.ucf-*) continue ;;
esac
SUDO_FILES="$SUDO_FILES $f"
done
fi
if [ -n "$SUDO_FILES" ]; then
NOPASSWD_HITS=$(grep -HnE '^[^#]*NOPASSWD:' $SUDO_FILES 2>/dev/null || true)
if [ -n "$NOPASSWD_HITS" ]; then
warn "sudoers NOPASSWD rules (review):"
echo "$NOPASSWD_HITS" | tee -a "$LOG"
fi
DANGEROUS_HITS=$(grep -HnE '^[^#]*(Defaults\s+!authenticate|Defaults\s+targetpw|runas_default)' $SUDO_FILES 2>/dev/null || true)
if [ -n "$DANGEROUS_HITS" ]; then
warn "sudoers dangerous defaults:"
echo "$DANGEROUS_HITS" | tee -a "$LOG"
fi
# include directives pointing outside sudoers.d
INC_HITS=$(grep -HnE '^\s*[#@]include(dir)?\s+' $SUDO_FILES 2>/dev/null | grep -vE '/etc/sudoers\.d' || true)
if [ -n "$INC_HITS" ]; then
warn "sudoers include directives outside /etc/sudoers.d:"
echo "$INC_HITS" | tee -a "$LOG"
fi
fi
ok "sudoers check complete"
# ---------- 16. account hardening audit -------------------------------------
log ""
log "=== PHASE 16: Account hardening audit ==="
if [ -f "$R/etc/passwd" ]; then
# UID 0 accounts (only root should exist)
UID0=$(awk -F: '$3==0 && $1!="root" {print}' "$R/etc/passwd" 2>/dev/null || true)
if [ -n "$UID0" ]; then
warn "non-root UID 0 accounts:"
echo "$UID0" | tee -a "$LOG"
fi
# accounts with real login shells (excluding system defaults)
LOGIN_SHELLS=$(awk -F: '$7 ~ /(bash|zsh|ksh|fish|sh)$/ && $1!="root" {print}' "$R/etc/passwd" 2>/dev/null || true)
if [ -n "$LOGIN_SHELLS" ]; then
log " accounts with real login shells (review):"
echo "$LOGIN_SHELLS" | tee -a "$LOG"
fi
fi
if [ -f "$R/etc/shadow" ]; then
# empty-password accounts
EMPTY_PW=$(awk -F: '$2=="" {print $1}' "$R/etc/shadow" 2>/dev/null || true)
if [ -n "$EMPTY_PW" ]; then
warn "accounts with empty password fields in /etc/shadow:"
echo "$EMPTY_PW" | tee -a "$LOG"
fi
# weak/unusual hash prefixes (strong: $6$/$y$/$7$)
WEAK_PW=$(awk -F: '$2 ~ /^\$[135]\$/ {print $1":"substr($2,1,3)}' "$R/etc/shadow" 2>/dev/null || true)
if [ -n "$WEAK_PW" ]; then
warn "accounts with weak password hashes (MD5/SHA1/Blowfish):"
echo "$WEAK_PW" | tee -a "$LOG"
fi
fi
if [ -f "$R/etc/group" ]; then
for grp in sudo wheel adm docker lxd incus-admin; do
members=$(awk -F: -v g="$grp" '$1==g {print $4}' "$R/etc/group" 2>/dev/null)
if [ -n "$members" ]; then
log " group $grp members: $members"
fi
done
fi
# authorized_keys for ALL users (Phase 8 only handled system users)
if [ -f "$R/etc/passwd" ]; then
while IFS=: read -r user _ uid _ _ home _; do
[ "$uid" -ge 1000 ] || [ "$user" = "root" ] || continue
[ -n "$home" ] && [ -d "$R$home/.ssh" ] || continue
for akf in authorized_keys authorized_keys2; do
ak="$R$home/.ssh/$akf"
[ -f "$ak" ] || continue
keycount=$(grep -cE '^(ssh-|ecdsa-|sk-)' "$ak" 2>/dev/null || echo 0)
log " $user ($home/.ssh/$akf): $keycount keys"
# flag forced commands and restrictive/exfiltration options
forced=$(grep -En '^(command=|no-pty|no-agent-forwarding|permitopen=|ProxyCommand)' "$ak" 2>/dev/null || true)
if [ -n "$forced" ]; then
warn " $user has authorized_keys with forced commands/options:"
echo "$forced" | tee -a "$LOG"
fi
done
# ~/.ssh/config ProxyCommand is suspicious on a server account
if [ -f "$R$home/.ssh/config" ]; then
proxy=$(grep -HnE '^\s*(ProxyCommand|LocalCommand|PermitLocalCommand)' "$R$home/.ssh/config" 2>/dev/null || true)
if [ -n "$proxy" ]; then
warn "$user ~/.ssh/config contains proxy/local-command directives:"
echo "$proxy" | tee -a "$LOG"
fi
fi
done < "$R/etc/passwd"
fi
ok "account audit complete"
# ---------- 17. other persistence vectors -----------------------------------
log ""
log "=== PHASE 17: Other persistence vectors ==="
PERSIST_TARGETS=""
[ -f "$R/etc/rc.local" ] && PERSIST_TARGETS="$PERSIST_TARGETS $R/etc/rc.local"
for f in \
"$R"/etc/profile.d/*.sh \
"$R"/etc/xdg/autostart/*.desktop \
"$R"/etc/systemd/system/*.timer \
"$R"/usr/lib/systemd/system/*.timer \
"$R"/lib/systemd/system/*.timer \
"$R"/etc/systemd/system/*.service.d/*.conf \
"$R"/usr/share/dbus-1/system-services/*.service \
"$R"/etc/polkit-1/rules.d/*.rules \
"$R"/etc/cron.allow \
"$R"/etc/cron.deny \
; do
[ -f "$f" ] && PERSIST_TARGETS="$PERSIST_TARGETS $f"
done
if [ -n "$PERSIST_TARGETS" ]; then
# auto-remove any file matching SUSP_PATTERN (BRE — matches Phase 10 convention)
for f in $PERSIST_TARGETS; do
if grep -q "$SUSP_PATTERN" "$f" 2>/dev/null; then
log " removing persistence file matching IoC pattern: $f"
save_forensic "${f#$R}"
chattr -i "$f" 2>/dev/null || true
rm -f "$f" && REMOVED=$((REMOVED+1)) || fail "could not remove $f"
FOUND=$((FOUND+1))
fi
done
# list timers (they're easy to miss in Phase 10's content-grep sweep)
for t in "$R"/etc/systemd/system/*.timer "$R"/usr/lib/systemd/system/*.timer "$R"/lib/systemd/system/*.timer; do
[ -f "$t" ] || continue
log " systemd timer present: ${t#$R}"
done
# flag any non-shipped init.d script
if [ -d "$R/etc/init.d" ]; then
for f in "$R"/etc/init.d/*; do
[ -f "$f" ] || continue
base=$(basename "$f")
case "$base" in
README|rc|rcS|skeleton|hwclock.sh|single|sendsigs|umountfs|umountnfs.sh|umountroot|checkfs.sh|checkroot.sh|checkroot-bootclean.sh|mountkernfs.sh|mountdevsubfs.sh|mountall.sh|mountall-bootclean.sh|mountnfs.sh|mountnfs-bootclean.sh|procps|hostname.sh|networking|ssh|cron|rsyslog|dbus|mysql|nginx|apache2|postfix|postgresql|redis-server|memcached|php*-fpm) continue ;;
esac
log " unexpected init.d script (review): $f"
done
fi
fi
ok "persistence vector scan complete"
# ---------- 18. file capability audit ---------------------------------------
log ""
log "=== PHASE 18: File capability audit ==="
if command -v getcap >/dev/null 2>&1; then
CAP_HITS=$(getcap -r "$R/usr" "$R/bin" "$R/sbin" 2>/dev/null || true)
if [ -n "$CAP_HITS" ]; then
while IFS= read -r line; do
[ -z "$line" ] && continue
f="${line%% =*}"
base=$(basename "$f")
case "$base" in
ping|ping6|mtr-packet|arping|traceroute6.iputils|clockdiff|systemd-detect-virt|suexec) continue ;;
esac
warn "file capability (review): $line"
# auto-flag catastrophic caps
if echo "$line" | grep -qE 'cap_sys_admin|cap_dac_read_search|cap_dac_override|cap_setuid|cap_setgid|cap_sys_module|cap_sys_ptrace'; then
warn " ^ catastrophic capability — likely backdoor"
fi
done <<< "$CAP_HITS"
fi
ok "capability audit complete"
else
log " getcap not installed on host — skipping"
fi
# ---------- 19. network-level IoC (config files only) -----------------------
log ""
log "=== PHASE 19: Network-level IoC ==="
# /etc/hosts: anything non-loopback and non-self
if [ -f "$R/etc/hosts" ]; then
HOSTS_ODD=$(grep -vE '^\s*#|^\s*$|^\s*(127\.|::1|::ffff:127\.|fe80:|ff[0-9a-f]{2}:)' "$R/etc/hosts" 2>/dev/null || true)
if [ -n "$HOSTS_ODD" ]; then
log " /etc/hosts non-loopback entries (review):"
echo "$HOSTS_ODD" | tee -a "$LOG"
fi
fi
# nameservers outside known-safe set
NS_FILES=""
[ -f "$R/etc/resolv.conf" ] && NS_FILES="$R/etc/resolv.conf"
[ -f "$R/etc/systemd/resolved.conf" ] && NS_FILES="$NS_FILES $R/etc/systemd/resolved.conf"
if [ -d "$R/etc/systemd/resolved.conf.d" ]; then
for f in "$R"/etc/systemd/resolved.conf.d/*.conf; do
[ -f "$f" ] && NS_FILES="$NS_FILES $f"
done
fi
if [ -n "$NS_FILES" ]; then
NS_HITS=$(grep -HnE '^\s*(nameserver|DNS=)' $NS_FILES 2>/dev/null | \
grep -vE '(1\.1\.1\.1|1\.0\.0\.1|8\.8\.8\.8|8\.8\.4\.4|9\.9\.9\.9|149\.112\.112\.112|208\.67\.222\.222|208\.67\.220\.220|127\.0\.0\.[0-9]+|::1|fe80:|169\.254\.|192\.168\.|10\.|172\.1[6-9]\.|172\.2[0-9]\.|172\.3[01]\.)' || true)
if [ -n "$NS_HITS" ]; then
warn "non-standard DNS nameservers configured:"
echo "$NS_HITS" | tee -a "$LOG"
fi
fi
# C2/pool string sweep across network-relevant config
NET_CONFIG_DIRS=""
for d in "$R/etc/hosts" "$R/etc/hosts.allow" "$R/etc/hosts.deny" \
"$R/etc/NetworkManager" "$R/etc/systemd/network" \
"$R/etc/iptables" "$R/etc/nftables.conf" \
"$R/etc/network" "$R/etc/netplan"; do
[ -e "$d" ] && NET_CONFIG_DIRS="$NET_CONFIG_DIRS $d"
done
if [ -n "$NET_CONFIG_DIRS" ]; then
C2_HITS=$(grep -rlEi 'solscan|xmrpool|nanopool|monerohash|supportxmr|minexmr|moneroocean|c3pool|hashvault|:(3333|5555|7777|14444|14433|9000)\b' $NET_CONFIG_DIRS 2>/dev/null || true)
if [ -n "$C2_HITS" ]; then
warn "network config files containing mining-pool / C2 strings:"
echo "$C2_HITS" | tee -a "$LOG"
fi
fi
ok "network IoC scan complete"
# ---------- 20. anti-forensic / timestamp anomalies -------------------------
log ""
log "=== PHASE 20: Anti-forensic anomalies ==="
# files with mtime > 1h in the future (clock-skew tolerant)
FUTURE_BOUNDARY=$(date -u -d '+1 hour' '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date -u -v+1H '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
FUTURE_FILES=""
[ -n "$FUTURE_BOUNDARY" ] && FUTURE_FILES=$(find "$R/etc" "$R/usr/bin" "$R/usr/sbin" "$R/bin" "$R/sbin" \
-xdev -newermt "$FUTURE_BOUNDARY" 2>/dev/null | head -50 || true)
if [ -n "$FUTURE_FILES" ]; then
warn "files with future mtime (anti-forensic timestamp tampering):"
echo "$FUTURE_FILES" | tee -a "$LOG"
fi
# history clearing
for h in "$R/root/.bash_history" "$R/root/.zsh_history"; do
[ -e "$h" ] || continue
if [ -L "$h" ]; then
target=$(readlink "$h")
case "$target" in
/dev/null|/dev/zero) warn "shell history symlinked to $target: $h" ;;
esac
elif [ ! -s "$h" ]; then
warn "shell history file is empty: $h"
fi
done
if [ -d "$R/home" ]; then
for userhome in "$R"/home/*/; do
[ -d "$userhome" ] || continue
for h in "$userhome.bash_history" "$userhome.zsh_history"; do
[ -e "$h" ] || continue
if [ -L "$h" ]; then
target=$(readlink "$h")
case "$target" in
/dev/null|/dev/zero) warn "shell history symlinked to $target: $h" ;;
esac
elif [ ! -s "$h" ]; then
warn "shell history file is empty: $h"
fi
done
done
fi
# unusual xattrs on system binaries
if command -v getfattr >/dev/null 2>&1; then
XATTR_HITS=$(getfattr -R -m '^user\.' --absolute-names "$R/etc" "$R/usr/bin" "$R/bin" 2>/dev/null | grep -B1 '^user\.' || true)
if [ -n "$XATTR_HITS" ]; then
warn "system files with user.* xattrs (review — can hide data):"
echo "$XATTR_HITS" | head -40 | tee -a "$LOG"
fi
fi
ok "anti-forensic scan complete"
# ---------- 21. Docker-in-container artifacts -------------------------------
log ""
log "=== PHASE 21: Docker-in-container artifacts ==="
if [ -d "$R/var/lib/docker" ] || [ -d "$R/var/lib/containerd" ]; then
warn "container runtime dirs present inside this container — unusual"
if [ -d "$R/var/lib/docker/containers" ]; then
for cfg in "$R"/var/lib/docker/containers/*/config.v2.json; do
[ -f "$cfg" ] || continue
img=$(grep -oE '"Image":"[^"]+"' "$cfg" 2>/dev/null | head -1)
log " docker container config: $cfg image=$img"
if echo "$img" | grep -qiE 'xmrig|bitping|repocket|traffmonetiz|peer2profit|earnapp|honeygain|packetstream|mining|miner'; then
warn " ^ known proxyjacking/mining image"
fi
done
fi
if [ -f "$R/etc/docker/daemon.json" ]; then
if grep -qE '"hosts"\s*:\s*\[[^]]*tcp://' "$R/etc/docker/daemon.json" 2>/dev/null; then
warn "docker daemon.json exposes TCP socket:"
grep -nE '"hosts"' "$R/etc/docker/daemon.json" | tee -a "$LOG"
fi
fi
else
log " no container runtime artifacts present"
fi
ok "container-runtime scan complete"
# ---------- 22. optional third-party scanners (gated by env) ----------------
log ""
log "=== PHASE 22: Optional third-party scanners ==="
if [ "${RUN_CHKROOTKIT:-0}" = "1" ] && command -v chkrootkit >/dev/null 2>&1; then
log " running chkrootkit against $R (may take several minutes)..."
chkrootkit -r "$R" 2>&1 | tee -a "$LOG" || true
fi
if [ "${RUN_RKHUNTER:-0}" = "1" ] && command -v rkhunter >/dev/null 2>&1; then
log " running rkhunter against $R (may take several minutes)..."
rkhunter --rootdir "$R" --check --sk --nocolors 2>&1 | tee -a "$LOG" || true
fi
if [ -n "${YARA_RULES:-}" ] && [ -f "$YARA_RULES" ] && command -v yara >/dev/null 2>&1; then
log " running yara with rules $YARA_RULES..."
yara -r "$YARA_RULES" "$R/usr" "$R/bin" "$R/lib" "$R/tmp" "$R/opt" 2>&1 | tee -a "$LOG" || true
fi
if [ "${RUN_CLAMAV:-0}" = "1" ] && command -v clamscan >/dev/null 2>&1; then
log " running clamscan against $R (slow)..."
clamscan -ri "$R" --exclude-dir='^'"$R"'/(proc|sys|dev)' 2>&1 | tee -a "$LOG" || true
fi
if [ "${RUN_CHKROOTKIT:-0}" != "1" ] && [ "${RUN_RKHUNTER:-0}" != "1" ] \
&& [ -z "${YARA_RULES:-}" ] && [ "${RUN_CLAMAV:-0}" != "1" ]; then
log " no third-party scanners requested (set RUN_CHKROOTKIT=1, RUN_RKHUNTER=1,"
log " YARA_RULES=/path/rules.yar, or RUN_CLAMAV=1 to enable)"
fi
ok "third-party scanner phase complete"
# ---------- 23. debsums package verification (cold-disk) --------------------
if command -v debsums >/dev/null 2>&1; then
log ""
log "=== PHASE 23: Package integrity verification (debsums) ==="
DEBSUMS_MISMATCHES=$(debsums --root="$R" -c 2>&1 || true)
if [ -n "$DEBSUMS_MISMATCHES" ]; then
warn "debsums found mismatched files:"
echo "$DEBSUMS_MISMATCHES" | tee -a "$LOG"
else
ok "all package files match expected checksums"
fi
else
log ""
log "=== PHASE 23: debsums not installed on host — skipping ==="
fi
# ---------- 24. SUID/SGID audit (cold-disk) ---------------------------------
log ""
log "=== PHASE 24: SUID/SGID binary audit ==="
find "$R" -xdev \( -perm -4000 -o -perm -2000 \) -type f 2>/dev/null | while read -r f; do
case "$(basename "$f")" in
mount.cifs|mount.nfs*|unix_chkpwd|su|sudo|mount|umount|passwd|chsh|chfn|gpasswd|newgrp|pkexec|ping|ping6|fusermount|fusermount3|ssh-keysign|at|expiry|crontab|chage) ;;
*) warn "SUID/SGID binary (review): $f ($(ls -la "$f" 2>/dev/null))" ;;
esac
done
ok "SUID/SGID audit complete"
# ---------- summary ----------------------------------------------------------
log ""
log "=========================================="
log "CLEANUP COMPLETE"
log " IoC items found: $FOUND"
log " IoC items removed: $REMOVED"
log " Errors: $ERRORS"
log " Log saved to: $LOG"
log " Forensics saved: $FORENSICS_DIR"
log "=========================================="
if [ "$ERRORS" -gt 0 ]; then
warn "some items could not be removed — review log"
exit 1
fi
log ""
log "RECOMMENDED NEXT STEPS:"
log " 1. Rotate ALL credentials for this container (SSH keys, DB passwords,"
log " API tokens) — the rootkit's TTY keylogger + PAM hook captured everything"
log " 2. Check $FORENSICS_DIR for saved malware samples"
log " 3. Submit libgcwrap.so to VirusTotal for attribution"
log " 4. Re-run this script against the same rootfs to verify cleanup"
log " 5. Before starting the container again, confirm no persistence remains"
exit 0
Executable
+434
View File
@@ -0,0 +1,434 @@
#!/bin/bash
###############################################################################
# detect.sh — cold-disk malware detection and family-classification scanner
#
# Read-only. Scans a mounted container rootfs (OpenVZ 7 / Incus / extracted
# tarball) and classifies findings by known family. Runs YARA
# (/srv/perfctl.yar), chkrootkit, rkhunter, debsums, and static-IoC checks.
#
# Usage:
# ROOTFS=/vz/root/101 ./detect.sh
# ROOTFS=/var/lib/incus/storage-pools/default/containers/c1/rootfs ./detect.sh
#
# Opt-in extras (slow):
# RUN_CLAMAV=1 ROOTFS=... ./detect.sh
# RUN_RKHUNTER=0 ROOTFS=... ./detect.sh # skip rkhunter
#
# Exit codes:
# 0 = no evidence of known malware families
# 1 = suspicious (review recommended)
# 2 = confirmed infection (see verdict)
###############################################################################
set -uo pipefail
if [ -z "${ROOTFS:-}" ]; then
echo "ERROR: ROOTFS is not set." >&2
echo "Usage: ROOTFS=/path/to/mounted/rootfs $0" >&2
exit 2
fi
if [ ! -d "$ROOTFS" ]; then
echo "ERROR: ROOTFS='$ROOTFS' is not a directory" >&2
exit 2
fi
R="$ROOTFS"
YARA_RULES="${YARA_RULES:-/srv/perfctl.yar}"
LOG="/var/log/malware-detect-${ROOTFS//\//_}-$(date +%Y%m%d-%H%M%S).log"
# opt-in / opt-out scanner controls
RUN_YARA="${RUN_YARA:-1}"
RUN_CHKROOTKIT="${RUN_CHKROOTKIT:-1}"
RUN_RKHUNTER="${RUN_RKHUNTER:-1}"
RUN_DEBSUMS="${RUN_DEBSUMS:-1}"
RUN_CLAMAV="${RUN_CLAMAV:-0}"
FAMILIES="perfctl xmrig proxyjacking ldpreload_rootkit lkm_rootkit ssh_backdoor generic_compromise"
declare -A FAMILY_SCORE
declare -A FAMILY_EVIDENCE
for f in $FAMILIES; do
FAMILY_SCORE[$f]=0
FAMILY_EVIDENCE[$f]=""
done
# ---------- helpers ---------------------------------------------------------
log() { echo "[$(date '+%H:%M:%S')] $*" | tee -a "$LOG"; }
warn() { echo "[$(date '+%H:%M:%S')] WARN: $*" | tee -a "$LOG"; }
ok() { echo "[$(date '+%H:%M:%S')] OK: $*" | tee -a "$LOG"; }
hit() { echo "[$(date '+%H:%M:%S')] HIT: $*" | tee -a "$LOG"; }
evidence() {
local family="$1" weight="$2" detail="$3"
FAMILY_SCORE[$family]=$((FAMILY_SCORE[$family] + weight))
FAMILY_EVIDENCE[$family]="${FAMILY_EVIDENCE[$family]} [+${weight}] ${detail}"$'\n'
hit "[$family +$weight] $detail"
}
need() { command -v "$1" >/dev/null 2>&1; }
# ---------- preflight -------------------------------------------------------
log "=========================================="
log "malware detect (cold-disk) started"
log "ROOTFS: $R"
log "Log: $LOG"
log "YARA rules: $YARA_RULES"
log "=========================================="
# ---------- PHASE 1 — static IoC file presence ------------------------------
log ""
log "=== PHASE 1: Static IoC file presence ==="
for f in \
"$R/etc/ld.so.preload" \
"$R/lib/libgcwrap.so" "$R/usr/lib/libgcwrap.so" \
"$R/lib/libfsnldev.so" "$R/usr/lib/libfsnldev.so" \
"$R/lib/libfsnkdev.so" "$R/usr/lib/libfsnkdev.so" \
"$R/lib/libpprocps.so" "$R/usr/lib/libpprocps.so" \
"$R/dev/shm/libfsnldev.so" \
"$R/bin/perfcc" "$R/usr/bin/perfcc" \
"$R/bin/wizlmsh" "$R/usr/bin/wizlmsh" \
"$R/bin/kkbush" "$R/usr/bin/kkbush" \
"$R/tmp/.apid" "$R/tmp/gd.sh" "$R/tmp/ccrl" \
"$R/tmp/kubeupd" "$R/tmp/javax64" "$R/tmp/wttwe" "$R/tmp/smpr" \
"$R/root/.config/cron/perfcc" \
; do
if [ -e "$f" ] || [ -L "$f" ]; then
evidence perfctl 10 "file present: $f"
fi
done
for d in "$R/tmp/.xdiag" "$R/tmp/.perf.c" "$R/tmp/.dmesg" \
"$R/bin/wbin" "$R/bin/.local" "$R/bin/.atmp" \
"$R/root/.config/cron" "$R/root/.config/traffmonetizer" \
"$R/var/-nonrpk" "$R/var/.r.rpk"; do
if [ -d "$d" ]; then
case "$d" in
*/traffmonetizer) evidence proxyjacking 8 "dir present: $d" ;;
*/-nonrpk|*/.r.rpk) evidence proxyjacking 8 "dir present: $d" ;;
*) evidence perfctl 8 "dir present: $d" ;;
esac
fi
done
if [ -f "$R/etc/ld.so.preload" ] && [ -s "$R/etc/ld.so.preload" ]; then
preload_content=$(cat "$R/etc/ld.so.preload" 2>/dev/null)
log " ld.so.preload content: $preload_content"
if echo "$preload_content" | grep -qE 'libgcwrap|libfsnldev|libfsnkdev|libpprocps'; then
evidence perfctl 15 "ld.so.preload points at known perfctl rootkit"
elif echo "$preload_content" | grep -qE '^/(tmp|dev/shm|var/tmp|home|opt)/'; then
evidence ldpreload_rootkit 10 "ld.so.preload points at non-system path: $preload_content"
fi
fi
ok "static IoC scan done"
# ---------- PHASE 2 — YARA scan ---------------------------------------------
log ""
log "=== PHASE 2: YARA scan ==="
if [ "$RUN_YARA" = "1" ] && need yara && [ -f "$YARA_RULES" ]; then
YARA_EXCLUDE="${YARA_EXCLUDE:-\.claude/|\.cache/|\.mozilla/|var/log/|var/backups/|/root/forensics|/share/rkhunter|/share/chkrootkit|/share/clamav}"
YARA_OUT=$(mktemp)
YARA_RAW=$(mktemp)
for p in etc bin sbin usr/bin usr/sbin lib usr/lib lib64 \
tmp var/tmp dev/shm opt root home var/spool/cron; do
target="$R/$p"
[ -e "$target" ] || continue
timeout 120 yara -r -w -N "$YARA_RULES" "$target" 2>/dev/null >> "$YARA_RAW" || true
done
grep -vE "$YARA_EXCLUDE" "$YARA_RAW" > "$YARA_OUT" 2>/dev/null || true
rm -f "$YARA_RAW"
YARA_HITS=$(grep -E '^[A-Za-z_][A-Za-z0-9_]*[[:space:]]+/' "$YARA_OUT" || true)
if [ -n "$YARA_HITS" ]; then
log " yara hits:"
echo "$YARA_HITS" | tee -a "$LOG"
log ""
while IFS= read -r line; do
rule=$(echo "$line" | awk '{print $1}')
file=$(echo "$line" | cut -d' ' -f2-)
case "$rule" in
perfctl_known_hashes|perfctl_libgcwrap_rootkit|perfctl_perfcc_installer|perfctl_wizlmsh_suid_backdoor)
evidence perfctl 20 "yara $rule on $file" ;;
perfctl_known_filenames|perfctl_dropper_paths_in_any_file|perfctl_cron_persistence|perfctl_network_indicators)
evidence perfctl 12 "yara $rule on $file" ;;
XMRIG_Monero_Miner|PUA_LNX_XMRIG_CryptoMiner|XMRIG_Monero_Miner_Config)
evidence xmrig 15 "yara $rule on $file" ;;
SUSP_XMRIG_String|CoinMiner_Strings|PUA_CryptoMiner_cpuminer|PUA_Crypto_Mining_CommandLine_Indicators|linux_miner_pool_urls)
evidence xmrig 8 "yara $rule on $file" ;;
linux_proxyjacking_agents|linux_proxyjacking_container_images)
evidence proxyjacking 10 "yara $rule on $file" ;;
linux_ldpreload_rootkit_generic|linux_ldpreload_config_file)
evidence ldpreload_rootkit 12 "yara $rule on $file" ;;
linux_ssh_backdoor_authorized_keys_in_binary|linux_pam_credential_stealer)
evidence ssh_backdoor 12 "yara $rule on $file" ;;
linux_lkm_rootkit_known_names)
evidence lkm_rootkit 15 "yara $rule on $file" ;;
*)
evidence generic_compromise 5 "yara $rule on $file" ;;
esac
done <<< "$YARA_HITS"
else
ok "no yara hits"
fi
rm -f "$YARA_OUT"
else
log " skipped (yara not installed, disabled, or rules file missing)"
fi
# ---------- PHASE 3 — chkrootkit (cold-disk with -r) ------------------------
if [ "$RUN_CHKROOTKIT" = "1" ]; then
log ""
log "=== PHASE 3: chkrootkit ==="
if need chkrootkit; then
CHK_OUT=$(mktemp)
timeout 300 chkrootkit -r "$R" > "$CHK_OUT" 2>&1 || true
# match only real hits — "INFECTED" is chkrootkit's positive-finding keyword.
# Skip descriptive "Searching for suspicious..." progress lines.
INFECTED=$(grep -E '\bINFECTED\b' "$CHK_OUT" 2>/dev/null | head -40 || true)
if [ -n "$INFECTED" ]; then
warn "chkrootkit findings:"
echo "$INFECTED" | tee -a "$LOG"
hits=$(echo "$INFECTED" | wc -l)
if echo "$INFECTED" | grep -qiE 'suckit|adore|kbeast|suterusu|diamorphine|reptile|lrk'; then
evidence lkm_rootkit 25 "chkrootkit named rootkit"
fi
evidence generic_compromise "$((hits > 10 ? 10 : hits))" "chkrootkit: $hits INFECTED lines"
else
ok "chkrootkit: no findings"
fi
rm -f "$CHK_OUT"
else
log " chkrootkit not installed on host — skipping (apt install chkrootkit)"
fi
fi
# ---------- PHASE 4 — rkhunter (cold-disk with --rootdir) -------------------
if [ "$RUN_RKHUNTER" = "1" ]; then
log ""
log "=== PHASE 4: rkhunter ==="
if need rkhunter; then
RK_OUT=$(mktemp)
timeout 600 rkhunter --rootdir "$R" --check --sk --nocolors > "$RK_OUT" 2>&1 || true
# Score only rkhunter lines that BOTH name a rootkit AND carry a
# positive-finding status ([ Warning ]/[ Found ]/[ Suspect ]/[ Infected ]).
# rkhunter emits "Diamorphine LKM [ Not found ]" on every run — matching
# the name alone would false-positive against that progress output.
RK_NAMED=$(grep -iE '(suckit|adore|kbeast|suterusu|diamorphine|reptile|xzres|maliroot).*\[ *(Warning|Found|Suspect|Infected) *\]' "$RK_OUT" 2>/dev/null | head -10 || true)
if [ -n "$RK_NAMED" ]; then
warn "rkhunter named a rootkit with positive status:"
echo "$RK_NAMED" | tee -a "$LOG"
evidence lkm_rootkit 25 "rkhunter named rootkit"
fi
# Log warning count for analyst context but do not score
RK_WARN_COUNT=$(grep -cE '\[ *Warning *\]' "$RK_OUT" 2>/dev/null || echo 0)
log " rkhunter: $RK_WARN_COUNT warning lines (not scored — review $RK_OUT-style output manually)"
if [ "$RK_WARN_COUNT" -gt 0 ]; then
grep -E '\[ *Warning *\]' "$RK_OUT" 2>/dev/null | head -20 | tee -a "$LOG" > /dev/null
fi
rm -f "$RK_OUT"
else
log " rkhunter not installed on host — skipping (apt install rkhunter)"
fi
fi
# ---------- PHASE 5 — debsums (cold-disk with --root) -----------------------
if [ "$RUN_DEBSUMS" = "1" ]; then
log ""
log "=== PHASE 5: debsums ==="
if need debsums; then
DS_OUT=$(mktemp)
timeout 300 debsums --root="$R" -c > "$DS_OUT" 2>&1 || true
if [ -s "$DS_OUT" ]; then
warn "debsums package-integrity mismatches (trojanized binaries likely):"
head -30 "$DS_OUT" | tee -a "$LOG"
hits=$(wc -l < "$DS_OUT")
if grep -qE '/(top|ldd|lsof|crontab|htop|ps|strace)$' "$DS_OUT"; then
evidence perfctl 15 "debsums: classic userland tools trojanized (top/ldd/lsof/ps)"
fi
evidence generic_compromise "$((hits > 10 ? 10 : hits))" "debsums: $hits package-file mismatches"
else
ok "debsums: all package files match"
fi
rm -f "$DS_OUT"
else
log " debsums not installed on host — skipping (apt install debsums)"
fi
fi
# ---------- PHASE 6 — persistence string-sweep ------------------------------
log ""
log "=== PHASE 6: Persistence string-sweep ==="
SUSP_PATTERN='perfcc\|perfctl\|libgcwrap\|libfsnldev\|libfsnkdev\|libpprocps\|kubeupd\|kkbush\|wizlmsh\|AAZHDE\|FPROF\|A2ZNODE\|ABWTRX\|\.xdiag\|\.perf\.c'
for tgt in \
"$R/root/.bashrc" "$R/root/.bash_profile" "$R/root/.profile" \
"$R/etc/profile" "$R/etc/bash.bashrc" \
"$R/etc/rc.local" "$R/etc/crontab" \
"$R/var/spool/cron/crontabs/root" \
; do
[ -f "$tgt" ] || continue
if grep -qE "$SUSP_PATTERN" "$tgt" 2>/dev/null; then
evidence perfctl 12 "IoC string in ${tgt#$R}"
fi
done
for crond in "$R/etc/cron.d" "$R/etc/cron.hourly" "$R/etc/cron.daily" \
"$R/etc/cron.weekly" "$R/etc/cron.monthly" \
"$R/etc/systemd/system" "$R/usr/lib/systemd/system" "$R/lib/systemd/system"; do
[ -d "$crond" ] || continue
HITS=$(grep -rlE "$SUSP_PATTERN" "$crond" 2>/dev/null | head -10 || true)
if [ -n "$HITS" ]; then
while IFS= read -r h; do
evidence perfctl 10 "IoC string in ${h#$R}"
done <<< "$HITS"
fi
done
# hidden SSH keys in system accounts
if [ -f "$R/etc/passwd" ]; then
for u in news bin daemon sys lp mail games uucp proxy www-data backup list irc gnats nobody systemd-network; do
home=$(awk -F: -v u="$u" '$1==u{print $6}' "$R/etc/passwd" 2>/dev/null)
[ -n "$home" ] && [ -f "$R$home/.ssh/authorized_keys" ] && {
keys=$(grep -cE '^(ssh-|ecdsa-)' "$R$home/.ssh/authorized_keys" 2>/dev/null || echo 0)
evidence ssh_backdoor 15 "system account $u has $keys authorized_keys entries"
}
done
fi
# trailing-space nologin backdoor
if [ -f "$R/etc/shells" ]; then
if grep -qE '^/usr/sbin/nologin +$|^/sbin/nologin +$' "$R/etc/shells" 2>/dev/null; then
evidence ssh_backdoor 18 "trailing-space nologin backdoor in /etc/shells"
fi
fi
ok "persistence sweep done"
# ---------- PHASE 7 — ClamAV (opt-in, slow) ---------------------------------
if [ "$RUN_CLAMAV" = "1" ]; then
log ""
log "=== PHASE 7: ClamAV ==="
if need clamscan; then
CAV_OUT=$(mktemp)
timeout 1800 clamscan -ri "$R" \
--exclude-dir='^'"$R"'/(proc|sys|dev)' \
> "$CAV_OUT" 2>&1 || true
INFECTED=$(grep 'FOUND$' "$CAV_OUT" 2>/dev/null | head -40 || true)
if [ -n "$INFECTED" ]; then
warn "ClamAV FOUND:"
echo "$INFECTED" | tee -a "$LOG"
hits=$(echo "$INFECTED" | wc -l)
if echo "$INFECTED" | grep -qiE 'xmrig|coinminer|cryptominer'; then
evidence xmrig 10 "ClamAV miner signature"
fi
if echo "$INFECTED" | grep -qiE 'rootkit|backdoor'; then
evidence generic_compromise 10 "ClamAV rootkit/backdoor signature"
fi
evidence generic_compromise "$((hits > 10 ? 10 : hits))" "ClamAV $hits detections"
else
ok "ClamAV: no detections"
fi
rm -f "$CAV_OUT"
else
log " clamscan not installed on host — skipping (apt install clamav)"
fi
fi
# ---------- VERDICT ---------------------------------------------------------
log ""
log "=========================================="
log "SCORECARD"
log "=========================================="
TOTAL=0
BEST_FAMILY=""
BEST_SCORE=0
for f in $FAMILIES; do
s=${FAMILY_SCORE[$f]}
printf " %-22s %d\n" "$f" "$s" | tee -a "$LOG"
TOTAL=$((TOTAL + s))
if [ "$s" -gt "$BEST_SCORE" ]; then
BEST_SCORE=$s
BEST_FAMILY=$f
fi
done
log ""
log "=========================================="
log "VERDICT"
log "=========================================="
if [ "$TOTAL" -eq 0 ]; then
log "NO EVIDENCE OF KNOWN MALWARE FAMILIES DETECTED."
log " This does NOT prove the rootfs is clean — bespoke/novel malware"
log " will not match these signatures."
exit 0
elif [ "$BEST_SCORE" -ge 30 ]; then
log "CONFIRMED INFECTION — primary family: $BEST_FAMILY (score=$BEST_SCORE)"
log ""
log "Top family evidence:"
echo -n "${FAMILY_EVIDENCE[$BEST_FAMILY]}" | tee -a "$LOG"
log ""
log "Secondary families with non-zero score:"
for f in $FAMILIES; do
[ "$f" = "$BEST_FAMILY" ] && continue
s=${FAMILY_SCORE[$f]}
[ "$s" -eq 0 ] && continue
log " $f ($s):"
echo -n "${FAMILY_EVIDENCE[$f]}" | tee -a "$LOG"
done
log ""
case "$BEST_FAMILY" in
perfctl)
log "RECOMMENDED ACTION:"
log " - run: ROOTFS=$R /srv/clean.sh"
log " - rotate ALL credentials for the workload in this container"
log " - review forensic samples after clean.sh"
;;
xmrig)
log "RECOMMENDED ACTION:"
log " - run: ROOTFS=$R /srv/clean.sh (covers dropper paths + persistence)"
log " - check for associated rootkit (miners rarely run alone)"
;;
proxyjacking)
log "RECOMMENDED ACTION:"
log " - run: ROOTFS=$R /srv/clean.sh (removes known proxyjacking dirs)"
log " - audit /etc/systemd/system and cron for the persistence vector"
;;
ldpreload_rootkit|lkm_rootkit)
log "RECOMMENDED ACTION:"
log " - run: ROOTFS=$R /srv/clean.sh"
log " - inspect /root/forensics-* for samples; submit to VirusTotal"
;;
ssh_backdoor)
log "RECOMMENDED ACTION:"
log " - inspect /etc/shells, system-account .ssh/ dirs"
log " - rotate keys; audit recent SSH logins from retained journal"
;;
esac
exit 2
else
log "SUSPICIOUS — low-confidence signals (score=$TOTAL, top=$BEST_FAMILY @$BEST_SCORE)"
log ""
for f in $FAMILIES; do
s=${FAMILY_SCORE[$f]}
[ "$s" -eq 0 ] && continue
log " $f ($s):"
echo -n "${FAMILY_EVIDENCE[$f]}" | tee -a "$LOG"
done
log ""
log "Recommend manual review of flagged items and/or running with RUN_CLAMAV=1"
exit 1
fi
+652
View File
@@ -0,0 +1,652 @@
/*
* perfctl.yar — YARA rules for perfctl/perfcc Linux userland rootkit family
* and adjacent threats commonly seen on the same compromised hosts
* (cryptominers, proxyjacking agents, LD_PRELOAD rootkits, SSH backdoors).
*
* Usage with clean.sh:
* YARA_RULES=/srv/perfctl.yar ROOTFS=/vz/root/101 ./clean.sh
*
* Or standalone:
* yara -r /srv/perfctl.yar /mnt/target-rootfs/
*
* SOURCES:
* - Aqua Nautilus, "perfctl: A Stealthy Malware Targeting Millions of Linux
* Servers" (October 2024) — full IoC table: file paths, hashes, IPs,
* domains, env vars. https://www.aquasec.com/blog/perfctl-a-stealthy-
* malware-targeting-millions-of-linux-servers/
* - Florian Roth / Nextron Systems, signature-base, XMRig + cryptominer
* rules. https://github.com/Neo23x0/signature-base — DRL 1.1 licensed,
* attribution retained in meta.
* - Yara-Rules community repo (MALW_XMRIG_Miner).
* https://github.com/Yara-Rules/rules
* - bleepingcomputer / thehackernews / darkreading perfctl writeups
* (Oct 2024) for independent IoC confirmation.
* - Remaining rules (proxyjacking agents, PAM stealer, LKM names,
* perfctl-specific binary rules) written for this file based on the
* published IoCs — no equivalent public rules exist as of April 2026.
*/
import "hash"
// =========================================================================
// perfctl / perfcc rootkit family — specific rules
// =========================================================================
rule perfctl_known_hashes
{
meta:
description = "File matches a known perfctl/perfcc MD5 from Aqua IoC table"
author = "clean.sh incident response"
family = "perfctl"
severity = "critical"
reference = "https://www.aquasec.com/blog/perfctl-a-stealthy-malware-targeting-millions-of-linux-servers/"
condition:
// main malware
hash.md5(0, filesize) == "656e22c65bf7c04d87b5afbe52b8d800" or
// cryptominer
hash.md5(0, filesize) == "6e7230dbe35df5b46dcd08975a0cc87f" or
// rootkit (libgcwrap.so)
hash.md5(0, filesize) == "835a9a6908409a67e51bce69f80dd58a" or
// trojanized ldd
hash.md5(0, filesize) == "cf265a3a3dd068d0aa0c70248cd6325d" or
// trojanized top
hash.md5(0, filesize) == "da006a0b9b51d56fa3f9690cf204b99f" or
// wizlmsh persistence binary
hash.md5(0, filesize) == "ba120e9c7f8896d9148ad37f02b0e3cb"
}
rule perfctl_libgcwrap_rootkit
{
meta:
description = "perfctl userland rootkit — LD_PRELOAD shared object"
author = "clean.sh incident response"
family = "perfctl"
severity = "critical"
reference = "https://www.aquasec.com/blog/perfctl-a-stealthy-malware-targeting-millions-of-linux-servers/"
strings:
$soname = "libgcwrap.so"
// operator-bypass env vars (from Aqua IoC table)
$env1 = "AAZHDE"
$env2 = "FPROF"
$env3 = "A2ZNODE"
$env4 = "VEI"
$env5 = "ABWTRX"
$hide1 = "perfctl"
$hide2 = "perfcc"
$hook1 = "readdir"
$hook2 = "readdir64"
$hook3 = "__xstat"
$hook4 = "__lxstat"
$hook5 = "getdents"
$hook6 = "getdents64"
$preload = "/etc/ld.so.preload"
condition:
uint32(0) == 0x464C457F and
filesize < 5MB and
(
$soname or
( 1 of ($env*) and 3 of ($hook*) ) or
( any of ($hide*) and $preload )
)
}
rule perfctl_perfcc_installer
{
meta:
description = "perfctl main installer / dropper binary (perfcc)"
author = "clean.sh incident response"
family = "perfctl"
severity = "critical"
strings:
$n1 = "perfcc"
$n2 = "perfctl"
$n3 = "libgcwrap"
$d1 = "/tmp/.xdiag"
$d2 = "/tmp/.perf.c"
$d3 = "/tmp/.dmesg"
$d4 = "/tmp/smpr"
$d5 = "/root/.config/cron/perfcc"
$p1 = "ld.so.preload"
$p2 = "AAZHDE"
$p3 = "FPROF"
$p4 = "A2ZNODE"
$p5 = "ABWTRX"
condition:
uint32(0) == 0x464C457F and
filesize < 50MB and
( 2 of ($n*) or (1 of ($n*) and any of ($d*)) or (any of ($p*) and any of ($d*)) )
}
rule perfctl_wizlmsh_suid_backdoor
{
meta:
description = "perfctl SUID backdoor (wizlmsh / kkbush / zipgrepjournalctl)"
author = "clean.sh incident response"
family = "perfctl"
severity = "critical"
strings:
$n1 = "wizlmsh"
$n2 = "kkbush"
$n3 = "zipgrepjournalctl"
$s1 = "setuid"
$s2 = "execve"
$s3 = "/bin/sh"
condition:
uint32(0) == 0x464C457F and
filesize < 5MB and
any of ($n*) and 2 of ($s*)
}
rule perfctl_dropper_paths_in_any_file
{
meta:
description = "File of any type references known perfctl dropper paths"
author = "clean.sh incident response"
family = "perfctl"
severity = "high"
strings:
$p1 = "/tmp/.xdiag"
$p2 = "/tmp/.perf.c"
$p3 = "/tmp/.apid"
$p4 = "/tmp/gd.sh"
$p5 = "/tmp/ccrl"
$p6 = "/tmp/javax64"
$p7 = "/tmp/kubeupd"
$p8 = "/tmp/wttwe"
$p9 = "/tmp/smpr"
$p10 = "/dev/shm/libfsnldev.so"
$p11 = "libfsnldev.so"
$p12 = "libfsnkdev.so" // variant spelling seen in Aqua IoC
$p13 = "libpprocps.so"
$p14 = "/root/.config/cron/perfcc"
condition:
filesize < 20MB and 2 of them
}
rule perfctl_cron_persistence
{
meta:
description = "Cron job referencing perfctl/perfcc persistence"
author = "clean.sh incident response"
family = "perfctl"
severity = "high"
strings:
$c1 = "perfcc"
$c2 = "perfctl"
$c3 = "perfclean"
$c4 = "libgcwrap"
$c5 = "AAZHDE"
$c6 = "FPROF"
$c7 = "A2ZNODE"
$c8 = "ABWTRX"
$cron1 = "* * * * *"
$cron2 = "@reboot"
$cron3 = "@hourly"
$cron4 = "@daily"
condition:
filesize < 100KB and
1 of ($cron*) and 1 of ($c*)
}
rule perfctl_known_filenames
{
meta:
description = "File matches a known perfctl IoC filename (catch-all)"
author = "clean.sh incident response"
family = "perfctl"
severity = "critical"
strings:
$n1 = "libgcwrap.so"
$n2 = "libfsnldev.so"
$n3 = "libfsnkdev.so"
$n4 = "libpprocps.so"
$n5 = "wizlmsh"
$n6 = "kkbush"
$n7 = "kubeupd"
$n8 = "perfcc"
$n9 = "perfctl"
$n10 = "perfclean"
$n11 = "zipgrepjournalctl"
$n12 = "javax64"
condition:
filesize < 100MB and any of them
}
rule perfctl_network_indicators
{
meta:
description = "Binary or script references published perfctl IPs / C2 domains"
author = "clean.sh incident response"
family = "perfctl"
severity = "high"
reference = "Aqua Nautilus IoC table (Oct 2024)"
strings:
// C2 / download servers
$ip1 = "211.234.111.116"
$ip2 = "46.101.139.173"
$ip3 = "104.183.100.189"
$ip4 = "198.211.126.180"
// Tor relay nodes the malware connects to
$ip5 = "80.67.172.162"
$ip6 = "176.10.107.180"
$ip7 = "78.47.18.110"
$ip8 = "95.217.109.36"
$ip9 = "145.239.41.102"
// proxyjacking SaaS domains the malware installs clients for
$d1 = "bitping.com"
$d2 = "earn.fm"
$d3 = "speedshare.app"
$d4 = "repocket.com"
condition:
// require ELF or shebang — rule is IoC-aware, not a plain grep
( uint32(0) == 0x464C457F or uint16(0) == 0x2123 ) and
filesize < 10MB and any of them
}
// =========================================================================
// XMRig / cryptominer — Neo23x0 signature-base (DRL 1.1, Florian Roth)
// =========================================================================
rule XMRIG_Monero_Miner : HIGHVOL
{
meta:
description = "Detects Monero mining software (XMRig)"
license = "Detection Rule License 1.1 https://github.com/Neo23x0/signature-base/blob/master/LICENSE"
author = "Florian Roth (Nextron Systems)"
reference = "https://github.com/xmrig/xmrig/releases"
date = "2018-01-04"
modified = "2022-11-10"
hash1 = "5c13a274adb9590249546495446bb6be5f2a08f9dcd2fc8a2049d9dc471135c0"
hash2 = "08b55f9b7dafc53dfc43f7f70cdd7048d231767745b76dc4474370fb323d7ae7"
hash3 = "f3f2703a7959183b010d808521b531559650f6f347a5830e47f8e3831b10bad5"
hash4 = "0972ea3a41655968f063c91a6dbd31788b20e64ff272b27961d12c681e40b2d2"
id = "71bf1b9c-c806-5737-83a9-d6013872b11d"
strings:
$s1 = "'h' hashrate, 'p' pause, 'r' resume" fullword ascii
$s2 = "--cpu-affinity" ascii
$s3 = "set process affinity to CPU core(s), mask 0x3 for cores 0 and 1" ascii
$s4 = "password for mining server" fullword ascii
$s5 = "XMRig/%s libuv/%s%s" fullword ascii
condition:
( uint16(0) == 0x5a4d or uint16(0) == 0x457f ) and filesize < 10MB and 2 of them
}
rule XMRIG_Monero_Miner_Config
{
meta:
description = "XMRig config.json file"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
reference = "https://github.com/xmrig/xmrig/releases"
date = "2018-01-04"
id = "374efe7f-9ef2-5974-8e24-f749183ab2d0"
strings:
$s2 = "\"cpu-affinity\": null, // set process affinity to CPU core(s), mask \"0x3\" for cores 0 and 1" fullword ascii
$s5 = "\"nicehash\": false // enable nicehash/xmrig-proxy support" fullword ascii
$s8 = "\"algo\": \"cryptonight\", // cryptonight (default) or cryptonight-lite" fullword ascii
condition:
( uint16(0) == 0x0a7b or uint16(0) == 0x0d7b ) and filesize < 5KB and 1 of them
}
rule PUA_LNX_XMRIG_CryptoMiner
{
meta:
description = "Detects XMRIG CryptoMiner (Linux ELF)"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
reference = "Internal Research"
date = "2018-06-28"
modified = "2023-01-06"
hash1 = "10a72f9882fc0ca141e39277222a8d33aab7f7a4b524c109506a407cd10d738c"
id = "bbdeff2e-68cc-5bbe-b843-3cba9c8c7ea8"
strings:
$x1 = "number of hash blocks to process at a time (don't set or 0 enables automatic selection o" fullword ascii
$s2 = "'h' hashrate, 'p' pause, 'r' resume, 'q' shutdown" fullword ascii
$s3 = "* THREADS: %d, %s, aes=%d, hf=%zu, %sdonate=%d%%" fullword ascii
$s4 = ".nicehash.com" ascii
condition:
uint16(0) == 0x457f and filesize < 8000KB and ( 1 of ($x*) or 2 of them )
}
rule SUSP_XMRIG_String
{
meta:
description = "Suspicious XMRIG string in executable"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
date = "2018-12-28"
modified = "2026-04-18 (clean.sh — require ELF/PE magic to reduce FP)"
id = "8c6f3e6e-df2a-51b7-81b8-21cd33b3c603"
strings:
$x1 = "xmrig.exe" fullword ascii
$x2 = "XMRig" fullword ascii
condition:
// only flag executables, not arbitrary text files
( uint16(0) == 0x457f or uint16(0) == 0x5a4d ) and
filesize < 10MB and 1 of them
}
rule CoinMiner_Strings : SCRIPT HIGHVOL
{
meta:
description = "Detects mining pool protocol strings in executable"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
reference = "https://minergate.com/faq/what-pool-address"
date = "2018-01-04"
modified = "2021-10-26"
id = "ac045f83-5f32-57a9-8011-99a2658a0e05"
strings:
$sa1 = "stratum+tcp://" ascii
$sa2 = "stratum+udp://" ascii
$sa3 = "stratum+ssl://" ascii
$sb1 = "\"normalHashing\": true,"
condition:
filesize < 3000KB and 1 of them
}
rule PUA_CryptoMiner_cpuminer
{
meta:
description = "cpuminer-style crypto miner"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
date = "2019-01-31"
hash1 = "ede858683267c61e710e367993f5e589fcb4b4b57b09d023a67ea63084c54a05"
id = "aebfdce9-c2dd-5f24-aa25-071e1a961239"
strings:
$s1 = "Stratum notify: invalid Merkle branch" fullword ascii
$s2 = "-t, --threads=N number of miner threads (default: number of processors)" fullword ascii
$s3 = "User-Agent: cpuminer/" ascii
$s4 = "hash > target (false positive)" fullword ascii
$s5 = "thread %d: %lu hashes, %s khash/s" fullword ascii
condition:
filesize < 5000KB and 1 of them
}
rule PUA_Crypto_Mining_CommandLine_Indicators : SCRIPT
{
meta:
description = "Command-line flags used by crypto miners"
license = "Detection Rule License 1.1"
author = "Florian Roth (Nextron Systems)"
reference = "https://www.poolwatch.io/coin/monero"
date = "2021-10-24"
id = "afe5a63a-08c3-5cb7-b4b1-b996068124b7"
strings:
$s01 = " --cpu-priority="
$s02 = "--donate-level=0"
$s03 = " -o pool."
$s04 = " -o stratum+tcp://"
$s05 = " --nicehash"
$s06 = " --algo=rx/0 "
// base64'd versions
$se1 = "LS1kb25hdGUtbGV2ZWw9"
$se2 = "0tZG9uYXRlLWxldmVsP"
$se3 = "tLWRvbmF0ZS1sZXZlbD"
$se4 = "c3RyYXR1bSt0Y3A6Ly"
$se5 = "N0cmF0dW0rdGNwOi8v"
$se6 = "zdHJhdHVtK3RjcDovL"
$se7 = "c3RyYXR1bSt1ZHA6Ly"
$se8 = "N0cmF0dW0rdWRwOi8v"
$se9 = "zdHJhdHVtK3VkcDovL"
condition:
filesize < 5000KB and 1 of them
}
rule linux_miner_pool_urls
{
meta:
description = "Binary or config references known Monero mining pools"
author = "clean.sh incident response"
family = "cryptominer"
severity = "medium"
strings:
$p1 = "pool.supportxmr.com"
$p2 = "xmr.nanopool.org"
$p3 = "xmr.2miners.com"
$p4 = "xmrpool.eu"
$p5 = "monerohash.com"
$p6 = "monero.crypto-pool.fr"
$p7 = "minexmr.com"
$p8 = "moneroocean.stream"
$p9 = "pool.hashvault.pro"
$p10 = "c3pool.com"
$p11 = "solscan.io"
$p12 = "pool.minexmr.com"
$p13 = "gulf.moneroocean.stream"
$p14 = "auto.c3pool.org"
condition:
filesize < 50MB and any of them
}
// =========================================================================
// Proxyjacking agents (residential-proxy SaaS abused for bandwidth theft)
// =========================================================================
rule linux_proxyjacking_agents
{
meta:
description = "Proxyjacking agent binary or install script (requires ELF or shell-script magic)"
author = "clean.sh incident response"
family = "proxyjacking"
severity = "high"
reference = "Aqua Nautilus perfctl report + Akamai/Sysdig proxyjacking writeups"
strings:
$a1 = "peer2profit" nocase
$a2 = "traffmonetizer" nocase
$a3 = "repocket" nocase
$a4 = "earnapp" nocase
$a5 = "honeygain" nocase
$a6 = "packetstream" nocase
$a7 = "bitping" nocase
$a8 = "speedshare" nocase
$a9 = "earnfm" nocase
$a10 = "earn.fm" nocase
$a11 = "IPRoyal" nocase
$a12 = "mysterium" nocase
$a13 = "urnetwork" nocase
$a14 = "proxyrack" nocase
$a15 = "pawns.app" nocase
$a16 = "traffmonetizer.com"
condition:
// require ELF binary or shell/python shebang — reject plain text
( uint32(0) == 0x464C457F or uint16(0) == 0x2123 ) and
filesize < 100MB and any of them
}
rule linux_proxyjacking_container_images
{
meta:
description = "Docker/container config referencing proxyjacking images"
author = "clean.sh incident response"
family = "proxyjacking"
severity = "high"
strings:
$i1 = "xmrig/xmrig" nocase
$i2 = "minerd" nocase
$i3 = "bitpinger" nocase
$i4 = "peer2profit/p2pclient" nocase
$i5 = "repocket/repocket" nocase
$i6 = "traffmonetizer" nocase
$i7 = "packetstream/psclient" nocase
$i8 = "honeygain/honeygain" nocase
$i9 = "iproyal/pawns" nocase
$cfg1 = "\"Image\":"
$cfg2 = "config.v2.json"
$cfg3 = "\"Cmd\":"
condition:
filesize < 10MB and any of ($i*) and any of ($cfg*)
}
// =========================================================================
// Generic LD_PRELOAD rootkit indicators (beyond perfctl-specific)
// =========================================================================
rule linux_ldpreload_rootkit_generic
{
meta:
description = "Generic LD_PRELOAD userland rootkit traits (tight condition to avoid libc FP)"
author = "clean.sh incident response"
family = "rootkit.ldpreload"
severity = "high"
strings:
$h1 = "readdir"
$h2 = "readdir64"
$h3 = "getdents"
$h4 = "getdents64"
$h5 = "__xstat"
$h6 = "__lxstat"
$d1 = "RTLD_NEXT"
$d2 = "dlsym"
$p1 = "/etc/ld.so.preload"
// distinctive rootkit config-string markers that do NOT appear in libc
$c1 = "HIDE_PROC" fullword
$c2 = "HIDE_FILE" fullword
$c3 = "MAGIC_STRING" fullword
$c4 = "hide_pid" fullword
$c5 = "hide_port" fullword
$c6 = "HIDE_TCP" fullword
$c7 = "HIDE_UDP" fullword
condition:
uint32(0) == 0x464C457F and
filesize < 5MB and
// require at least one distinctive rootkit marker — libc has the hook
// names and dlsym but not these config keywords
( 2 of ($h*) and any of ($d*) and any of ($c*) ) or
( 2 of ($h*) and $p1 and any of ($c*) )
}
rule linux_ldpreload_config_file
{
meta:
description = "ld.so.preload file pointing at known rootkit (tight — matches only known malicious SO names)"
author = "clean.sh incident response"
family = "rootkit.ldpreload"
severity = "critical"
strings:
$s1 = "libgcwrap"
$s2 = "libfsnldev"
$s3 = "libfsnkdev"
$s4 = "libpprocps"
condition:
filesize < 1KB and any of them
}
// =========================================================================
// SSH backdoors and credential stealers
// =========================================================================
rule linux_ssh_backdoor_authorized_keys_in_binary
{
meta:
description = "Binary contains embedded SSH public key (credential implant)"
author = "clean.sh incident response"
family = "backdoor.ssh"
severity = "high"
strings:
$k1 = "ssh-rsa AAAA"
$k2 = "ssh-ed25519 AAAA"
$k3 = "ecdsa-sha2-nistp256 AAAA"
$path1 = ".ssh/authorized_keys"
$path2 = "/root/.ssh"
condition:
uint32(0) == 0x464C457F and
filesize < 20MB and
any of ($k*) and any of ($path*)
}
rule linux_pam_credential_stealer
{
meta:
description = "PAM module that logs plaintext credentials to disk"
author = "clean.sh incident response"
family = "backdoor.pam"
severity = "critical"
strings:
$sym1 = "pam_sm_authenticate"
$sym2 = "pam_get_item"
$sym3 = "PAM_AUTHTOK"
$w1 = "fopen"
$w2 = "fprintf"
$w3 = "/tmp/"
$w4 = "/var/tmp/"
$w5 = "/dev/shm/"
$d1 = ".passwd"
$d2 = ".creds"
$d3 = ".log"
$d4 = "passwords.txt"
condition:
uint32(0) == 0x464C457F and
filesize < 2MB and
2 of ($sym*) and any of ($w*) and any of ($d*)
}
// =========================================================================
// LKM rootkit name match (cold-disk: filename-content only)
// =========================================================================
rule linux_lkm_rootkit_known_names
{
meta:
description = "Known Linux LKM rootkit by filename or symbol"
author = "clean.sh incident response"
family = "rootkit.lkm"
severity = "critical"
strings:
$n1 = "diamorphine"
$n2 = "reptile_module" nocase
$n3 = "suterusu" nocase
$n4 = "nuk3gh0st" nocase
$n5 = "kbeast" nocase
$n6 = "module_hide"
$n7 = "module_hidden"
$n8 = "adore-ng" nocase
condition:
filesize < 10MB and any of them
}