Commit 1fa6e150 authored by 易初's avatar 易初 🖐🏻

Add new file

parent e01648b2
#!/bin/bash
# incident_containment_cleanup_v1.sh
# Temporary containment and recoverable cleanup for the 2026-09-11 campaign.
# Compatible with CentOS 6/7 era Bash and common GNU userland.
#
# IMPORTANT:
# - Dry-run is the default. Mutations require --apply.
# - This does not make a kernel-compromised host trustworthy.
# - Loaded kernel modules are NOT unloaded unless both --unload-modules and
# --ack-unload-risk are supplied. Unloading hostile modules can crash a host.
# - No RPM uninstall scriptlets are executed. Suspect files are quarantined so
# the operation remains recoverable until the machine is rebuilt.
set -u
umask 077
export LC_ALL=C
VERSION="1.0.1"
APPLY=0
BLOCK_IOCS=1
REMOVE_KEYS=1
UNLOAD_MODULES=0
ACK_UNLOAD=0
BASE_OUT="/root"
EXTRA_BLOCK_IPS=""
usage() {
cat <<'EOF'
Usage:
incident_containment_cleanup_v1.sh [options]
Modes:
(no --apply) Dry-run. Collect state and show planned changes.
--apply Apply recoverable containment and cleanup.
Options:
--output DIR Evidence/quarantine parent (default: /root)
--no-firewall Do not add runtime OUTPUT rejects for known IOC IPs
--keep-ssh-keys Do not remove the two confirmed campaign public keys
--block-ip IPv4 Add another confirmed IOC IPv4; repeat as needed
--unload-modules Attempt to unload campaign modules after containment
--ack-unload-risk Acknowledge possible kernel panic/network disruption
-h, --help Show this help
Safe first run:
bash incident_containment_cleanup_v1.sh --output /root
Apply online containment without unloading kernel modules:
bash incident_containment_cleanup_v1.sh --apply --output /root
High-risk maintenance-window mode (console access and service drain required):
bash incident_containment_cleanup_v1.sh --apply --unload-modules \
--ack-unload-risk --output /root
EOF
}
while test "$#" -gt 0; do
case "$1" in
--apply) APPLY=1; shift ;;
--output)
test "$#" -ge 2 || { usage >&2; exit 1; }
BASE_OUT="$2"; shift 2 ;;
--no-firewall) BLOCK_IOCS=0; shift ;;
--keep-ssh-keys) REMOVE_KEYS=0; shift ;;
--block-ip)
test "$#" -ge 2 || { usage >&2; exit 1; }
case "$2" in
*[!0-9.]*|'') echo "Invalid IPv4 value: $2" >&2; exit 1 ;;
esac
EXTRA_BLOCK_IPS="${EXTRA_BLOCK_IPS}${EXTRA_BLOCK_IPS:+ }$2"
shift 2 ;;
--unload-modules) UNLOAD_MODULES=1; shift ;;
--ack-unload-risk) ACK_UNLOAD=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
esac
done
if test "$UNLOAD_MODULES" -eq 1 && test "$ACK_UNLOAD" -ne 1; then
echo '--unload-modules requires --ack-unload-risk' >&2
exit 2
fi
if test "$UNLOAD_MODULES" -eq 1 && test "$APPLY" -ne 1; then
echo '--unload-modules also requires --apply' >&2
exit 2
fi
if test "$(id -u 2>/dev/null || echo 1)" -ne 0; then
echo 'Run as root so evidence and containment are complete.' >&2
exit 1
fi
have() { command -v "$1" >/dev/null 2>&1; }
sanitize() { printf '%s' "$1" | tr '\t\r\n' ' '; }
hash_file() {
if have sha256sum; then
sha256sum "$1" 2>/dev/null | awk '{print $1}'
elif have openssl; then
openssl dgst -sha256 "$1" 2>/dev/null | awk '{print $NF}'
fi
}
HOST_NAME="$(hostname 2>/dev/null || echo unknown-host)"
SAFE_HOST="$(printf '%s' "$HOST_NAME" | tr -c 'A-Za-z0-9._-' '_')"
STAMP="$(date +%Y%m%d-%H%M%S)"
RUN_DIR="${BASE_OUT%/}/incident-containment-${SAFE_HOST}-${STAMP}"
EVIDENCE="$RUN_DIR/evidence"
QUARANTINE="$RUN_DIR/quarantine"
LOG="$RUN_DIR/actions.log"
PLAN="$RUN_DIR/plan.tsv"
SUMMARY="$RUN_DIR/summary.txt"
BEFORE="$EVIDENCE/before.txt"
AFTER="$EVIDENCE/after.txt"
MANIFEST="$EVIDENCE/quarantine_manifest.tsv"
MODE="DRY-RUN"
test "$APPLY" -eq 1 && MODE="APPLY"
mkdir -p "$EVIDENCE" "$QUARANTINE/files" || exit 1
: > "$LOG"
printf 'action\ttarget\tstatus\tdetail\n' > "$PLAN"
printf 'sha256\toriginal_path\tquarantine_path\n' > "$MANIFEST"
log() {
printf '%s %s\n' "$(date '+%F %T %z')" "$*" | tee -a "$LOG"
}
result() {
printf '%s\t%s\t%s\t%s\n' \
"$(sanitize "$1")" "$(sanitize "$2")" "$(sanitize "$3")" \
"$(sanitize "$4")" >> "$PLAN"
}
capture_state() {
local output
output="$1"
{
echo "version=$VERSION"
echo "mode=$MODE"
echo "host=$HOST_NAME"
echo "time=$(date '+%F %T %z')"
uname -a 2>/dev/null || true
test -r /sys/class/dmi/id/product_uuid && { printf 'product_uuid='; cat /sys/class/dmi/id/product_uuid; }
test -r /etc/machine-id && { printf 'machine_id='; cat /etc/machine-id; }
echo '===== suspicious loaded modules ====='
grep -E '^(ata_linkq|nvme_hctxq|xen_gntq|YL4Qr|9mLHs|3ja8C|8bmOn)[[:space:]]' /proc/modules 2>/dev/null || true
echo '===== suspicious processes ====='
ps -ef 2>/dev/null | grep -E \
'[A]liyunSysAssist|[i]rqbalance|[r]ngd|/tmp/[d]bg|nvme_sqmap|scsi_cmdq|usb_portctl' || true
echo '===== suspicious sockets ====='
if have ss; then
ss -anp 2>/dev/null | grep -E \
'hpgoc2|101\.201\.148\.142|8\.217\.173\.211|AliyunSysAssist|irqbalance|rngd' || true
elif have netstat; then
netstat -anp 2>/dev/null | grep -E \
'hpgoc2|101\.201\.148\.142|8\.217\.173\.211|AliyunSysAssist|irqbalance|rngd' || true
fi
echo '===== suspect files ====='
for path in \
/usr/sbin/irqbalance /usr/lib64/libnuma_hint.so /usr/lib64/libcpu_balance.so \
/usr/lib/libnuma_hint.so /usr/lib/libcpu_balance.so \
/usr/local/share/aliyun-sys-assist/AliyunSysAssist \
/usr/sbin/aliyun-sys-assist-setup \
/usr/lib/nvme_sqmap /usr/lib/scsi_cmdq /usr/lib/usb_portctl \
/etc/modules-load.d/ata_linkq.conf /etc/modules-load.d/nvme_hctxq.conf \
/etc/modules-load.d/xen_gntq.conf /etc/sysconfig/modules/vmware-tools.modules
do
test -e "$path" || test -L "$path" || continue
stat "$path" 2>&1 || true
test -f "$path" && echo "sha256=$(hash_file "$path") path=$path"
done
echo '===== package verification ====='
have rpm && rpm -V irqbalance aliyun-sys-assist ata_linkq nvme_hctxq xen_gntq 2>&1 || true
echo '===== firewall ====='
have iptables-save && iptables-save 2>/dev/null || true
} > "$output" 2>&1
}
capture_state "$BEFORE"
backup_copy() {
local source destination digest
source="$1"
test -e "$source" || test -L "$source" || return 1
destination="$EVIDENCE/originals$source"
mkdir -p "$(dirname "$destination")"
cp -a "$source" "$destination" 2>/dev/null || return 1
digest=""
test -f "$source" && digest="$(hash_file "$source")"
printf '%s\t%s\t%s\n' "$digest" "$source" "$destination" >> "$MANIFEST"
return 0
}
quarantine_path() {
local source destination digest
source="$1"
test -e "$source" || test -L "$source" || return 0
destination="$QUARANTINE/files$source"
digest=""
test -f "$source" && digest="$(hash_file "$source")"
if test "$APPLY" -ne 1; then
result quarantine "$source" planned "sha256=$digest destination=$destination"
return 0
fi
mkdir -p "$(dirname "$destination")"
if mv "$source" "$destination" >> "$LOG" 2>&1; then
printf '%s\t%s\t%s\n' "$digest" "$source" "$destination" >> "$MANIFEST"
result quarantine "$source" applied "sha256=$digest destination=$destination"
log "quarantined $source"
else
result quarantine "$source" failed "destination=$destination"
fi
}
disable_service() {
local service
service="$1"
if test "$APPLY" -ne 1; then
result disable_service "$service" planned 'disable only; no unit ExecStop invoked'
return 0
fi
if have systemctl; then
systemctl disable "$service" >> "$LOG" 2>&1 || true
fi
if have chkconfig; then
chkconfig "$service" off >> "$LOG" 2>&1 || true
fi
result disable_service "$service" applied 'disable requested; service scripts not invoked'
}
block_ip() {
local address
address="$1"
if test "$BLOCK_IOCS" -ne 1; then
result firewall "$address" skipped '--no-firewall'
return 0
fi
if ! have iptables; then
result firewall "$address" failed 'iptables not found'
return 0
fi
if iptables -C OUTPUT -d "$address" -j REJECT >/dev/null 2>&1; then
result firewall "$address" present 'OUTPUT REJECT already exists'
return 0
fi
if test "$APPLY" -ne 1; then
result firewall "$address" planned 'iptables -I OUTPUT 1 -d IP -j REJECT'
elif iptables -I OUTPUT 1 -d "$address" -j REJECT >> "$LOG" 2>&1; then
result firewall "$address" applied 'runtime OUTPUT REJECT; not persisted across reboot'
log "blocked outbound IOC $address"
else
result firewall "$address" failed 'iptables command failed'
fi
}
KNOWN_BLOCK_IPS="101.201.148.142 8.217.173.211${EXTRA_BLOCK_IPS:+ $EXTRA_BLOCK_IPS}"
for address in $KNOWN_BLOCK_IPS; do
block_ip "$address"
done
# Disable known persistence before moving binaries. We deliberately avoid
# `systemctl stop` and init-script stop actions because those are attacker-owned.
for service in \
aliyun-sys-assist irqbalance nvme_sqmap.service scsi_cmdq.service usb_portctl.service
do
disable_service "$service"
done
KNOWN_PROCESS_HASHES='aa0eb857cbb40a1516ba0f6ac33d2265d0b49e9067abeec4264057193e440e09 87f629925728922e2b092036b214b364b2419452a514d0b44f033ebe4a0d873c a3d832da2bd83bff45fb1df576de814f57b004777ea0dd7d1beaa46340d8ca49 79b10ffb3c14b7745d7a7eb5e90c708b2d5eca52fe2ff65106ba4f391c02eacd 8ca077298ca5a46655be209bd993268e5e224f0bc10a079825e023e036d342fe'
is_known_process_hash() {
case " $KNOWN_PROCESS_HASHES " in
*" $1 "*) return 0 ;;
*) return 1 ;;
esac
}
is_known_process_path() {
case "$1" in
/usr/local/share/aliyun-sys-assist/*|/usr/lib/aliyun-sys-assist-payload/*|\
/usr/lib/nvme_sqmap|/usr/lib/scsi_cmdq|/usr/lib/usb_portctl|\
'/tmp/dbg (deleted)'|/tmp/dbg) return 0 ;;
*) return 1 ;;
esac
}
terminate_confirmed_processes() {
local proc pid exe clean_exe digest matched
for proc in /proc/[0-9]*; do
test -d "$proc" || continue
pid="${proc#/proc/}"
test "$pid" = "$$" && continue
exe="$(readlink "$proc/exe" 2>/dev/null || true)"
test -n "$exe" || continue
clean_exe="${exe% (deleted)}"
digest=""
test -r "$proc/exe" && digest="$(hash_file "$proc/exe")"
matched=0
is_known_process_path "$exe" && matched=1
test -n "$digest" && is_known_process_hash "$digest" && matched=1
test "$matched" -eq 1 || continue
{
echo "===== PID $pid ====="
ps -o user,pid,ppid,lstart,etime,stat,cmd -p "$pid" 2>&1 || true
echo "exe=$exe"
echo "sha256=$digest"
tr '\0' ' ' < "$proc/cmdline" 2>/dev/null || true
echo
cat "$proc/maps" 2>/dev/null || true
ls -l "$proc/fd" 2>/dev/null || true
} >> "$EVIDENCE/terminated_processes.txt"
if test "$APPLY" -ne 1; then
result terminate "pid=$pid" planned "exe=$exe sha256=$digest"
continue
fi
kill -TERM "$pid" >> "$LOG" 2>&1 || true
sleep 1
if kill -0 "$pid" 2>/dev/null; then
kill -KILL "$pid" >> "$LOG" 2>&1 || true
fi
if kill -0 "$pid" 2>/dev/null; then
result terminate "pid=$pid" failed "still running exe=$exe sha256=$digest"
else
result terminate "pid=$pid" applied "exe=$exe sha256=$digest"
log "terminated confirmed malicious PID $pid ($clean_exe)"
fi
done
}
terminate_confirmed_processes
remove_campaign_keys() {
local keyfile temp changed mode uid gid
if test "$REMOVE_KEYS" -ne 1; then
result ssh_key all skipped '--keep-ssh-keys'
return 0
fi
find /root /home -xdev -type f \( -name authorized_keys -o -name authorized_keys2 \) -print 2>/dev/null \
| while IFS= read -r keyfile; do
if ! grep -Eq \
'AAAAB3NzaC1yc2EAAAADAQABAAABAQCF8tdOgKz0byyI/1wsvYH7/dAH71F7QDr4W7PQu8aCH8|AAAAB3NzaC1yc2EAAAADAQABAAABAQC4evWpxa5XJ8KiL1\+E32x1MB7FlijaDTHVhBOwL7xhr0GvZGLj' \
"$keyfile" 2>/dev/null; then
continue
fi
if test "$APPLY" -ne 1; then
result ssh_key "$keyfile" planned 'remove exact campaign key material; preserve other lines'
continue
fi
backup_copy "$keyfile" || true
temp="$(mktemp "${keyfile}.incident.XXXXXX")" || continue
awk '
index($0,"AAAAB3NzaC1yc2EAAAADAQABAAABAQCF8tdOgKz0byyI/1wsvYH7/dAH71F7QDr4W7PQu8aCH8")==0 &&
index($0,"AAAAB3NzaC1yc2EAAAADAQABAAABAQC4evWpxa5XJ8KiL1+E32x1MB7FlijaDTHVhBOwL7xhr0GvZGLj")==0
' "$keyfile" > "$temp"
changed=0
cmp -s "$keyfile" "$temp" || changed=1
if test "$changed" -eq 1; then
mode="$(stat -c %a "$keyfile" 2>/dev/null || echo 600)"
uid="$(stat -c %u "$keyfile" 2>/dev/null || echo 0)"
gid="$(stat -c %g "$keyfile" 2>/dev/null || echo 0)"
chmod "$mode" "$temp" && chown "$uid:$gid" "$temp"
have chcon && chcon --reference="$keyfile" "$temp" 2>/dev/null || true
mv "$temp" "$keyfile"
result ssh_key "$keyfile" applied 'campaign key lines removed; original preserved in evidence/originals'
log "removed campaign SSH key material from $keyfile"
else
rm -f "$temp"
fi
done
}
remove_campaign_keys
# Remove malicious library preload references while preserving unrelated lines.
neutralize_ld_preload() {
local file temp mode uid gid
file=/etc/ld.so.preload
test -f "$file" || return 0
grep -Eq 'libnuma_hint\.so|libcpu_balance\.so|aliyun-sys-assist' "$file" || return 0
if test "$APPLY" -ne 1; then
result ld_preload "$file" planned 'remove confirmed campaign library references only'
return 0
fi
backup_copy "$file" || true
temp="$(mktemp "${file}.incident.XXXXXX")" || return 1
grep -Ev 'libnuma_hint\.so|libcpu_balance\.so|aliyun-sys-assist' "$file" > "$temp" || true
mode="$(stat -c %a "$file" 2>/dev/null || echo 644)"
uid="$(stat -c %u "$file" 2>/dev/null || echo 0)"
gid="$(stat -c %g "$file" 2>/dev/null || echo 0)"
chmod "$mode" "$temp" && chown "$uid:$gid" "$temp" && mv "$temp" "$file"
result ld_preload "$file" applied 'campaign references removed'
}
neutralize_ld_preload
# Quarantine exact persistence/configuration paths first.
for path in \
/etc/modules-load.d/ata_linkq.conf \
/etc/modules-load.d/nvme_hctxq.conf \
/etc/modules-load.d/xen_gntq.conf \
/etc/sysconfig/modules/vmware-tools.modules \
/usr/lib/systemd/system/nvme_sqmap.service \
/usr/lib/systemd/system/scsi_cmdq.service \
/usr/lib/systemd/system/usb_portctl.service \
/etc/systemd/system/multi-user.target.wants/nvme_sqmap.service \
/etc/systemd/system/multi-user.target.wants/scsi_cmdq.service \
/etc/systemd/system/multi-user.target.wants/usb_portctl.service
do
quarantine_path "$path"
done
# Known userland payloads and malicious/replaced runtime components.
for path in \
/usr/local/share/aliyun-sys-assist \
/usr/lib/aliyun-sys-assist-payload \
/usr/sbin/aliyun-sys-assist-setup \
/usr/lib64/libnuma_hint.so \
/usr/lib64/libcpu_balance.so \
/usr/lib/libnuma_hint.so \
/usr/lib/libcpu_balance.so \
/usr/lib/nvme_sqmap \
/usr/lib/scsi_cmdq \
/usr/lib/usb_portctl \
/tmp/dbg \
/tmp/dbg-el6 \
/etc/init.d/aliyun-sys-assist \
/etc/rc.d/init.d/aliyun-sys-assist \
/usr/lib/systemd/system/aliyun-sys-assist.service \
/etc/systemd/system/multi-user.target.wants/aliyun-sys-assist.service
do
quarantine_path "$path"
done
# Quarantine irqbalance only when its executable hash is one of the confirmed
# malicious variants. A legitimate binary is left untouched.
if test -f /usr/sbin/irqbalance; then
IRQ_HASH="$(hash_file /usr/sbin/irqbalance)"
if is_known_process_hash "$IRQ_HASH"; then
quarantine_path /usr/sbin/irqbalance
else
result quarantine /usr/sbin/irqbalance skipped "hash not in confirmed malicious set: $IRQ_HASH"
fi
fi
# Quarantine campaign modules by known filename, known hash, or exact campaign
# metadata. This does not affect code already resident in the running kernel.
quarantine_modules() {
local path digest info suspect
find /lib/modules -xdev -type f -name '*.ko' -print 2>/dev/null \
| while IFS= read -r path; do
digest="$(hash_file "$path")"
suspect=0
case "$(basename "$path")" in
ata_linkq.ko|nvme_hctxq.ko|xen_gntq.ko|YL4Qr.ko|9mLHs.ko|3ja8C.ko|8bmOn.ko)
suspect=1 ;;
esac
case "$digest" in
eacf3c216834e224f705fe7d61d818410ffc4c3c41d5dfb05867d94c9886ff8e|\
df468fc0879398035905481d447d64d27a3888f062ddd96d1eb13532d551ba42|\
d1a68e7dcb64fa78ca60d19e2b85ba24f5f5aebf3a2338c4e4237a8d9000e9b9|\
2705768f4593821ba83bc81b0ea1b863f12b935d9344213672e3a9ddc151a63|\
0c773febfeec861998535001b828350e67bffb92c6fc62d5d06f7515c258d14a|\
f85ecbea07fbf8e352911f6e84f76aa07daecabda6ef57417fa8774c0d6a0f10|\
bbcfa09f6fc861e3f18be8a0bc8c31c657318b44460473e052ead100ce53919e)
suspect=1 ;;
esac
if test "$suspect" -eq 0 && have modinfo; then
info="$(modinfo "$path" 2>/dev/null || true)"
printf '%s\n' "$info" | grep -Eqi \
'Bootstrap WorkNodeTable|LkmOssStatusGet|HTTP Helper ELF URL|N-OSS-DUAL|srcversion:[[:space:]]*(5EC4D6BBEB191EC83CDAA54|F4E542BE70BAF973912A0D2)' \
&& suspect=1
fi
test "$suspect" -eq 1 && quarantine_path "$path"
done
}
quarantine_modules
if test "$UNLOAD_MODULES" -eq 1; then
log 'HIGH-RISK module unload requested and acknowledged'
# Second-stage random modules first, then network/helper modules.
for module in 9mLHs 3ja8C 8bmOn YL4Qr ata_linkq nvme_hctxq xen_gntq; do
if ! grep -q "^${module}[[:space:]]" /proc/modules 2>/dev/null; then
result unload "$module" absent 'not visible in /proc/modules'
continue
fi
if have modprobe && modprobe -r "$module" >> "$LOG" 2>&1; then
result unload "$module" applied 'modprobe -r succeeded'
elif have rmmod && rmmod "$module" >> "$LOG" 2>&1; then
# Non-forced rmmod is required as a fallback after the .ko has already
# been quarantined and depmod no longer has an index entry for it.
result unload "$module" applied 'non-forced rmmod fallback succeeded'
elif grep -q "^${module}[[:space:]]" /proc/modules 2>/dev/null; then
result unload "$module" failed 'module remains loaded; see actions.log; no forced removal attempted'
else
result unload "$module" applied 'module disappeared while unload was attempted'
fi
done
else
for module in ata_linkq nvme_hctxq xen_gntq YL4Qr 9mLHs 3ja8C 8bmOn; do
grep -q "^${module}[[:space:]]" /proc/modules 2>/dev/null \
&& result unload "$module" deferred 'still loaded; rebuild remains mandatory'
done
fi
if test "$APPLY" -eq 1; then
# Rebuild dependency metadata only after any requested unload attempts. On
# older CentOS, rebuilding it before modprobe -r can make an already-loaded
# quarantined module appear "not found" to modprobe.
have depmod && depmod -a >> "$LOG" 2>&1 || true
have systemctl && systemctl daemon-reload >> "$LOG" 2>&1 || true
fi
capture_state "$AFTER"
REMAINING_MODULES="$(grep -Ec '^(ata_linkq|nvme_hctxq|xen_gntq|YL4Qr|9mLHs|3ja8C|8bmOn)[[:space:]]' /proc/modules 2>/dev/null || true)"
FAILED_ACTIONS="$(awk -F '\t' 'NR > 1 && $3 == "failed" {n++} END {print n+0}' "$PLAN")"
APPLIED_ACTIONS="$(awk -F '\t' 'NR > 1 && $3 == "applied" {n++} END {print n+0}' "$PLAN")"
{
echo "Host: $HOST_NAME"
echo "Time: $(date '+%F %T %z')"
echo "Version: $VERSION"
echo "Mode: $MODE"
echo "Output: $RUN_DIR"
echo "Applied actions: $APPLIED_ACTIONS"
echo "Failed actions: $FAILED_ACTIONS"
echo "Campaign modules still visible as loaded: $REMAINING_MODULES"
echo
if test "$APPLY" -ne 1; then
echo 'Assessment: DRY-RUN ONLY; NO CONTAINMENT CHANGES WERE MADE'
elif test "$REMAINING_MODULES" -gt 0; then
echo 'Assessment: PARTIALLY CONTAINED; HOST REMAINS KERNEL-COMPROMISED'
elif test "$FAILED_ACTIONS" -gt 0; then
echo 'Assessment: CONTAINMENT ATTEMPTED WITH FAILURES; REVIEW REQUIRED'
else
echo 'Assessment: CAMPAIGN COMPONENTS NEUTRALIZED AS FAR AS THIS HOST CAN REPORT; REBUILD STILL REQUIRED'
fi
echo
echo 'Important: host-local results are not authoritative while hostile kernel code has run.'
echo 'Firewall blocks are runtime-only and can be bypassed or removed by kernel malware.'
echo 'Do not treat this script as an alternative to external isolation and trusted rebuild.'
echo
cat "$PLAN"
} > "$SUMMARY"
cat "$SUMMARY"
exit 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment