Created
February 12, 2026 01:26
-
-
Save ghmendonca/eb09ba4a7eb69a23e9935d66b877d8bf to your computer and use it in GitHub Desktop.
NPM Supply Chain Malware Scanner (Cross-Chain TxDataHiding / OmniStealer / DPRK APT) - Detects blockchain C2 malware, IDE injection, credential theft. Based on Ransom-ISAC YARA rules. 16 detection checks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # NPM Supply Chain Malware Detection Script (Cross-Chain TxDataHiding / OmniStealer) | |
| # Reference: https://ransom-isac.org/blog/cross-chain-txdatahiding-crypto-heist-part-2/ | |
| # YARA Rules: Actor_APT_DPRK_Unknown_MAL (Ransom-ISAC) | |
| # Detects malware variant that spreads via compromised npm packages | |
| # Usage: ./detect-infection.sh | |
| # Supports: macOS, Linux, Windows (Git Bash / MSYS2 / Cygwin) | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' | |
| INFECTED=0 | |
| # ======================================== | |
| # OS DETECTION | |
| # ======================================== | |
| detect_os() { | |
| case "$(uname -s)" in | |
| Linux*) OS="linux";; | |
| Darwin*) OS="macos";; | |
| CYGWIN*) OS="windows";; | |
| MINGW*) OS="windows";; | |
| MSYS*) OS="windows";; | |
| *) OS="unknown";; | |
| esac | |
| # Double-check for WSL (reports as Linux but is Windows-adjacent) | |
| if [ "$OS" = "linux" ] && grep -qi microsoft /proc/version 2>/dev/null; then | |
| OS="wsl" | |
| fi | |
| } | |
| detect_os | |
| # ======================================== | |
| # PLATFORM-SPECIFIC PATH RESOLUTION | |
| # ======================================== | |
| # Resolve Windows home directory | |
| if [ "$OS" = "windows" ]; then | |
| # In Git Bash/MSYS, $HOME is set but $USERPROFILE has the native path | |
| WIN_HOME="${USERPROFILE:-$HOME}" | |
| # Convert Windows paths to Unix-style for Git Bash | |
| WIN_HOME_UNIX=$(cygpath -u "$WIN_HOME" 2>/dev/null || echo "$HOME") | |
| EFFECTIVE_HOME="$WIN_HOME_UNIX" | |
| WIN_APPDATA=$(cygpath -u "$APPDATA" 2>/dev/null || echo "$EFFECTIVE_HOME/AppData/Roaming") | |
| WIN_LOCALAPPDATA=$(cygpath -u "$LOCALAPPDATA" 2>/dev/null || echo "$EFFECTIVE_HOME/AppData/Local") | |
| WIN_PROGRAMFILES=$(cygpath -u "$PROGRAMFILES" 2>/dev/null || echo "/c/Program Files") | |
| WIN_TEMP=$(cygpath -u "$TEMP" 2>/dev/null || echo "/tmp") | |
| elif [ "$OS" = "wsl" ]; then | |
| EFFECTIVE_HOME="$HOME" | |
| # Access Windows filesystem from WSL | |
| WIN_USER=$(cmd.exe /C "echo %USERNAME%" 2>/dev/null | tr -d '\r') | |
| WIN_HOME_MNT="/mnt/c/Users/$WIN_USER" | |
| WIN_APPDATA="$WIN_HOME_MNT/AppData/Roaming" | |
| WIN_LOCALAPPDATA="$WIN_HOME_MNT/AppData/Local" | |
| WIN_PROGRAMFILES="/mnt/c/Program Files" | |
| WIN_TEMP="$WIN_HOME_MNT/AppData/Local/Temp" | |
| else | |
| EFFECTIVE_HOME="$HOME" | |
| fi | |
| # Known malware indicators from YARA rules | |
| TRON_WALLETS="TMfKQEd7TJJa5xNZJZ2Lep838vrzrs7mAP|TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG|TLmj13VL4p6NQ7jpxz8d9uYY6FUKCYatS" | |
| APTOS_ADDRS="be037400670fbf1c32364f762975908dc43eeb38759263e7dfcdabc76380811e|3f0e5781d0855fb460661ac63257376db1941b2bb522499e4757ecb3ebd5dce3|3414a658f13b652f24301e986f9e0079ef506992472c1d5224180340d8105837" | |
| BSC_HASHES="f46c86c886bbf9915f4841a8c27b38c519fe3ce54ba69c98d233d0ffc94d19fc|d33f78662df123adf2a178628980b605a0026c0d8c4f4e87e43e724cda258fef|a8cdabea3616a6d43e0893322112f9dca05b7d2f88fd1b7370c33c79076216ff" | |
| MALWARE_MARKERS="C250617A|C250618A|C5-benefit|CHQG3L42MMQ" | |
| OBFS_PATTERNS="_\\\$af402041|_\\\$af813180|_\\\$_2d00" | |
| TELEGRAM_TOKEN="7870147428:AAGbYG_eYkiAziCKRmkiQF-" | |
| echo "========================================" | |
| echo " NPM SUPPLY CHAIN MALWARE SCANNER" | |
| echo " (Cross-Chain TxDataHiding Variant)" | |
| echo "========================================" | |
| echo "" | |
| echo " Detected OS: $OS" | |
| echo " Home: $EFFECTIVE_HOME" | |
| [ "$OS" = "windows" ] || [ "$OS" = "wsl" ] && echo " Windows Home: ${WIN_HOME:-$WIN_HOME_MNT}" | |
| echo "" | |
| # ======================================== | |
| # HELPER FUNCTIONS (cross-platform) | |
| # ======================================== | |
| # Cross-platform process listing | |
| list_processes() { | |
| if [ "$OS" = "windows" ]; then | |
| # tasklist + wmic for command line args | |
| tasklist /V /FO CSV 2>/dev/null || wmic process get ProcessId,CommandLine 2>/dev/null | |
| else | |
| ps aux 2>/dev/null | |
| fi | |
| } | |
| # Cross-platform network connections | |
| list_connections() { | |
| if [ "$OS" = "windows" ]; then | |
| netstat -ano 2>/dev/null | |
| elif command -v lsof &>/dev/null; then | |
| lsof -i 2>/dev/null | |
| elif command -v ss &>/dev/null; then | |
| ss -tunap 2>/dev/null | |
| else | |
| netstat -tunap 2>/dev/null | |
| fi | |
| } | |
| # Cross-platform: find node PIDs | |
| get_node_pids() { | |
| if [ "$OS" = "windows" ]; then | |
| tasklist /FI "IMAGENAME eq node.exe" /FO CSV /NH 2>/dev/null | \ | |
| awk -F'","' '{gsub(/"/, "", $2); print $2}' | grep -E '^[0-9]+$' | |
| else | |
| pgrep -x node 2>/dev/null | |
| fi | |
| } | |
| # Cross-platform: count connections for a PID | |
| count_pid_connections() { | |
| local pid=$1 | |
| if [ "$OS" = "windows" ]; then | |
| netstat -ano 2>/dev/null | grep -c "$pid" | tr -d ' ' | |
| else | |
| lsof -p "$pid" 2>/dev/null | grep -E "IPv[46]" | wc -l | tr -d ' ' | |
| fi | |
| } | |
| # Cross-platform: check if file is executable | |
| is_executable() { | |
| if [ "$OS" = "windows" ]; then | |
| [ -f "$1" ] && echo "$1" | |
| else | |
| find "$(dirname "$1")" -maxdepth 1 -name "$(basename "$1")" -type f -perm /111 2>/dev/null | |
| fi | |
| } | |
| # ======================================== | |
| # BUILD PATHS TO SCAN | |
| # ======================================== | |
| # --- node_modules persistence paths --- | |
| NODE_MODULES_PATHS=("$EFFECTIVE_HOME/.node_modules") | |
| if [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| NODE_MODULES_PATHS+=( | |
| "$WIN_H/.node_modules" | |
| "$WIN_H/node_modules" | |
| # NOTE: %APPDATA%/npm/node_modules is the STANDARD global npm directory on Windows. | |
| # The malware uses hidden/non-standard paths. We scan %APPDATA%/node_modules (no npm/) | |
| # and %LOCALAPPDATA%/node_modules which are NOT standard and indicate persistence. | |
| "$WIN_APPDATA/node_modules" | |
| "$WIN_LOCALAPPDATA/node_modules" | |
| ) | |
| fi | |
| # --- VSCode / Cursor deviceid injection paths --- | |
| DEVICEID_PATHS=() | |
| if [ "$OS" = "macos" ]; then | |
| DEVICEID_PATHS=( | |
| "/Applications/Visual Studio Code.app/Contents/Resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| "/Applications/Cursor.app/Contents/Resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| ) | |
| elif [ "$OS" = "linux" ]; then | |
| DEVICEID_PATHS=( | |
| "/usr/share/code/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| ) | |
| elif [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| DEVICEID_PATHS=( | |
| # VS Code - user install (most common on Windows) | |
| "$WIN_LOCALAPPDATA/Programs/Microsoft VS Code/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| # VS Code - system install | |
| "$WIN_PROGRAMFILES/Microsoft VS Code/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| # Cursor - user install | |
| "$WIN_LOCALAPPDATA/Programs/cursor/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| "$WIN_LOCALAPPDATA/Cursor/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| # Also check under home for scoop/portable installs | |
| "$WIN_H/scoop/apps/vscode/current/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| ) | |
| # Also keep Linux paths for WSL-native VS Code | |
| if [ "$OS" = "wsl" ]; then | |
| DEVICEID_PATHS+=( | |
| "/usr/share/code/resources/app/node_modules/@vscode/deviceid/dist/index.js" | |
| ) | |
| fi | |
| fi | |
| # --- Lock file paths --- | |
| LOCK_FILES=("/tmp/tmp7A863DD1.tmp") | |
| if [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| LOCK_FILES+=( | |
| "$WIN_TEMP/tmp7A863DD1.tmp" | |
| "${WIN_HOME_UNIX:-$WIN_HOME_MNT}/AppData/Local/Temp/tmp7A863DD1.tmp" | |
| ) | |
| fi | |
| # --- Shell profile paths --- | |
| SHELL_FILES=("$EFFECTIVE_HOME/.bashrc" "$EFFECTIVE_HOME/.bash_profile" "$EFFECTIVE_HOME/.zshrc" "$EFFECTIVE_HOME/.zprofile" "$EFFECTIVE_HOME/.profile" "$EFFECTIVE_HOME/.zshenv" "$EFFECTIVE_HOME/.bash_login") | |
| if [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| SHELL_FILES+=( | |
| # Git Bash profiles on Windows | |
| "$WIN_H/.bashrc" | |
| "$WIN_H/.bash_profile" | |
| "$WIN_H/.profile" | |
| "$WIN_H/.minttyrc" | |
| # PowerShell profiles | |
| "$WIN_H/Documents/PowerShell/Microsoft.PowerShell_profile.ps1" | |
| "$WIN_H/Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1" | |
| ) | |
| fi | |
| # --- Config search directories --- | |
| CONFIG_SEARCH_DIRS=("$EFFECTIVE_HOME") | |
| if [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| # Add Windows home if different from EFFECTIVE_HOME | |
| if [ "$WIN_H" != "$EFFECTIVE_HOME" ]; then | |
| CONFIG_SEARCH_DIRS+=("$WIN_H") | |
| fi | |
| fi | |
| # --- Chrome extension paths --- | |
| CHROME_PROFILE_DIRS=() | |
| if [ "$OS" = "macos" ]; then | |
| CHROME_PROFILE_DIRS=("$HOME/Library/Application Support/Google/Chrome") | |
| elif [ "$OS" = "linux" ]; then | |
| CHROME_PROFILE_DIRS=("$HOME/.config/google-chrome") | |
| elif [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| CHROME_PROFILE_DIRS=( | |
| "$WIN_LOCALAPPDATA/Google/Chrome/User Data" | |
| ) | |
| fi | |
| # --- Suspicious Python paths --- | |
| PYTHON_SEARCH_DIRS=("/tmp" "/var/tmp") | |
| PYTHON3127_DIRS=() | |
| if [ "$OS" = "macos" ]; then | |
| PYTHON3127_DIRS=( | |
| "$HOME/Library/Python/Python3127" | |
| "/tmp/Python3127" | |
| "$HOME/.local/Python3127" | |
| ) | |
| elif [ "$OS" = "linux" ] || [ "$OS" = "wsl" ]; then | |
| PYTHON3127_DIRS=( | |
| "/tmp/Python3127" | |
| "$HOME/.local/Python3127" | |
| ) | |
| elif [ "$OS" = "windows" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| PYTHON3127_DIRS=( | |
| "$WIN_H/Python3127" | |
| "$WIN_LOCALAPPDATA/Python3127" | |
| "$WIN_APPDATA/Python3127" | |
| "$WIN_TEMP/Python3127" | |
| ) | |
| PYTHON_SEARCH_DIRS+=("$WIN_TEMP") | |
| fi | |
| # ======================================== | |
| # CHECKS BEGIN | |
| # ======================================== | |
| # 1. Check for malicious node -e processes with malware signature | |
| echo -n "[1/16] Checking for malicious node -e processes... " | |
| if [ "$OS" = "windows" ]; then | |
| MALICIOUS_NODE=$(wmic process where "name='node.exe'" get ProcessId,CommandLine /FORMAT:LIST 2>/dev/null | \ | |
| grep -iE "node.*-e" | grep -iE "global\[|_V.*=|atob|eval\(") | |
| if [ -n "$MALICIOUS_NODE" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$MALICIOUS_NODE" | head -5 | while read line; do | |
| echo " $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| else | |
| MALICIOUS_NODE=$(ps aux 2>/dev/null | grep -E "node\s+-e" | grep -v grep) | |
| if echo "$MALICIOUS_NODE" | grep -qE "global\[|_V.*=|atob|eval\("; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$MALICIOUS_NODE" | while read line; do | |
| PID=$(echo "$line" | awk '{print $2}') | |
| echo " PID: $PID" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| fi | |
| # 2. Check for C2 connections (known IP ranges from IOC list) | |
| echo -n "[2/16] Checking for C2 server connections... " | |
| C2_IPS="198\.105\.127\.|23\.27\.20\.143|136\.0\.9\.8|23\.27\.202\.27|166\.88\.4\.2|45\.125\.|103\.136\." | |
| if [ "$OS" = "windows" ]; then | |
| C2_CONN=$(netstat -ano 2>/dev/null | grep -E "$(echo "$C2_IPS" | sed 's/\\\././g')" | head -5) | |
| else | |
| C2_CONN=$(lsof -i 2>/dev/null | grep -E "$C2_IPS" | grep -v grep | head -5) | |
| fi | |
| if [ -n "$C2_CONN" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$C2_CONN" | while read line; do | |
| echo " $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 3. Check for hidden node_modules with malware payload (ALL platform paths) | |
| echo -n "[3/16] Checking for malware persistence in node_modules... " | |
| NM_INFECTED="" | |
| for NM_PATH in "${NODE_MODULES_PATHS[@]}"; do | |
| if [ -d "$NM_PATH" ]; then | |
| MALWARE_FILES="" | |
| MALWARE_INDICATOR="" | |
| if [ -f "$NM_PATH/package.json" ]; then | |
| SUSP_PKG=$(cat "$NM_PATH/package.json" 2>/dev/null | grep -iE "postinstall|preinstall|eval|atob" | head -1) | |
| if [ -n "$SUSP_PKG" ]; then | |
| MALWARE_INDICATOR="suspicious package.json" | |
| MALWARE_FILES="$NM_PATH/package.json" | |
| fi | |
| fi | |
| MARKER_FILES=$(grep -rl "C250618A\|C250617A\|global\['_V'\]\|global\['_H'\]\|5-3-276\|eval.*atob" "$NM_PATH" 2>/dev/null) | |
| if [ -n "$MARKER_FILES" ]; then | |
| MALWARE_INDICATOR="malware signature found" | |
| MALWARE_FILES="$MARKER_FILES" | |
| fi | |
| if [ -z "$MALWARE_FILES" ]; then | |
| OBFUSCATED_FILES=$(find "$NM_PATH" -name "*.js" -exec sh -c 'if awk "length>500 {found=1; exit} END {exit !found}" "$1" 2>/dev/null; then echo "$1"; fi' _ {} \; 2>/dev/null) | |
| if [ -n "$OBFUSCATED_FILES" ]; then | |
| MALWARE_INDICATOR="obfuscated JS files" | |
| MALWARE_FILES="$OBFUSCATED_FILES" | |
| fi | |
| fi | |
| if [ -n "$MALWARE_INDICATOR" ]; then | |
| NM_INFECTED="$NM_INFECTED | |
| $NM_PATH ($MALWARE_INDICATOR)" | |
| echo "$MALWARE_FILES" | head -5 | while read f; do | |
| NM_INFECTED="$NM_INFECTED | |
| - $f" | |
| done | |
| fi | |
| fi | |
| done | |
| if [ -n "$NM_INFECTED" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$NM_INFECTED" | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 4. Check for blockchain C2 endpoint connections | |
| echo -n "[4/16] Checking for blockchain RPC connections... " | |
| BLOCKCHAIN_PATTERN="trongrid\|aptoslabs\|bsc-dataseed\|bsc-rpc\|publicnode\|binance" | |
| if [ "$OS" = "windows" ]; then | |
| # On Windows, netstat doesn't show hostnames by default, so also check DNS cache | |
| BLOCKCHAIN_CONN=$(netstat -ano 2>/dev/null | grep -iE "trongrid|aptoslabs|bsc-dataseed|bsc-rpc|publicnode|binance" | head -3) | |
| if [ -z "$BLOCKCHAIN_CONN" ]; then | |
| # Check Windows DNS cache for these domains | |
| BLOCKCHAIN_CONN=$(ipconfig /displaydns 2>/dev/null | grep -iE "trongrid|aptoslabs|bsc-dataseed|bsc-rpc|publicnode|binance" | head -3) | |
| fi | |
| else | |
| BLOCKCHAIN_CONN=$(lsof -i 2>/dev/null | grep -iE "trongrid|aptoslabs|bsc-dataseed|bsc-rpc|publicnode|binance" | grep -v grep | head -3) | |
| fi | |
| if [ -n "$BLOCKCHAIN_CONN" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$BLOCKCHAIN_CONN" | while read line; do | |
| echo " $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 5. Check for the malware lock file (ALL platform paths) | |
| echo -n "[5/16] Checking for malware lock files... " | |
| LOCK_FOUND="" | |
| for lf in "${LOCK_FILES[@]}"; do | |
| if [ -f "$lf" ]; then | |
| LOCK_FOUND="$LOCK_FOUND $lf" | |
| fi | |
| done | |
| if [ -n "$LOCK_FOUND" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $LOCK_FOUND; do | |
| echo " Found: $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 6. Check VSCode/Cursor for injection (C250617A marker) | |
| echo -n "[6/16] Checking IDE for payload injection... " | |
| IDE_INFECTED="" | |
| for path in "${DEVICEID_PATHS[@]}"; do | |
| if [ -f "$path" ]; then | |
| if grep -q "C250617A\|C250618A\|global\['e'\]='vscode-eval'" "$path" 2>/dev/null; then | |
| IDE_INFECTED="$IDE_INFECTED $path" | |
| fi | |
| fi | |
| done | |
| if [ -n "$IDE_INFECTED" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $IDE_INFECTED; do | |
| echo " - $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 7. Check config files for malware payload (optimized, multi-dir) | |
| echo -n "[7/16] Checking config files for embedded malware... " | |
| MALWARE_CONFIG="" | |
| CONFIG_FILES="" | |
| for search_dir in "${CONFIG_SEARCH_DIRS[@]}"; do | |
| if [ -d "$search_dir" ]; then | |
| FOUND=$(timeout 10 find "$search_dir" -maxdepth 5 \( -name "tailwind.config.*" -o -name "next.config.*" -o -name "vite.config.*" \) -type f 2>/dev/null | head -50) | |
| CONFIG_FILES="$CONFIG_FILES $FOUND" | |
| fi | |
| done | |
| for f in $CONFIG_FILES; do | |
| if grep -q "eval.*atob\|global\['_V'\]\|global\['_H'\]" "$f" 2>/dev/null; then | |
| MALWARE_CONFIG="$MALWARE_CONFIG $f" | |
| fi | |
| done | |
| if [ -n "$MALWARE_CONFIG" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $MALWARE_CONFIG; do | |
| echo " - $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 8. Check for YARA-derived malware indicators in config files | |
| echo -n "[8/16] Checking for DPRK malware signatures... " | |
| YARA_FOUND="" | |
| for f in $CONFIG_FILES; do | |
| if grep -qE "$TRON_WALLETS|$MALWARE_MARKERS|$OBFS_PATTERNS|\['_V'\]|\['_R'\]|\['_H'\]" "$f" 2>/dev/null; then | |
| YARA_FOUND="$YARA_FOUND $f" | |
| fi | |
| done | |
| if [ -n "$YARA_FOUND" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $YARA_FOUND; do | |
| echo " - $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 9. Check for persistence mechanisms (OS-specific) | |
| echo -n "[9/16] Checking for persistence mechanisms... " | |
| SUSPICIOUS_PERSIST="" | |
| if [ "$OS" = "macos" ]; then | |
| # macOS: LaunchAgents / LaunchDaemons | |
| for dir in "$HOME/Library/LaunchAgents" "/Library/LaunchAgents"; do | |
| if [ -d "$dir" ]; then | |
| SUSP=$(find "$dir" -name "*.plist" -mtime -30 2>/dev/null | while read f; do | |
| if grep -qiE "node|npm|pnpm|yarn|.node_modules|python.*-c" "$f" 2>/dev/null; then | |
| if ! echo "$f" | grep -qE "watchman|com\.facebook\.|com\.github\.facebook|com\.volta\.|com\.nvm\."; then | |
| echo "$f" | |
| fi | |
| fi | |
| done) | |
| [ -n "$SUSP" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST $SUSP" | |
| fi | |
| done | |
| elif [ "$OS" = "linux" ] || [ "$OS" = "wsl" ]; then | |
| # Linux: crontab, systemd user services, XDG autostart | |
| SUSP_CRON=$(crontab -l 2>/dev/null | grep -iE "node|npm|pnpm|yarn|\.node_modules|python.*-c" | grep -v "^#") | |
| [ -n "$SUSP_CRON" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST [crontab] $SUSP_CRON" | |
| if [ -d "$HOME/.config/systemd/user" ]; then | |
| SUSP=$(find "$HOME/.config/systemd/user" -name "*.service" -mtime -30 2>/dev/null | while read f; do | |
| if grep -qiE "node|npm|\.node_modules" "$f" 2>/dev/null; then | |
| echo "$f" | |
| fi | |
| done) | |
| [ -n "$SUSP" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST $SUSP" | |
| fi | |
| if [ -d "$HOME/.config/autostart" ]; then | |
| SUSP=$(find "$HOME/.config/autostart" -name "*.desktop" -mtime -30 2>/dev/null | while read f; do | |
| if grep -qiE "node|npm|\.node_modules|python.*-c" "$f" 2>/dev/null; then | |
| echo "$f" | |
| fi | |
| done) | |
| [ -n "$SUSP" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST $SUSP" | |
| fi | |
| elif [ "$OS" = "windows" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| # Windows: Registry Run keys | |
| for key in \ | |
| "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" \ | |
| "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\RunOnce" \ | |
| "HKLM\\Software\\Microsoft\\Windows\\CurrentVersion\\Run"; do | |
| REG_OUT=$(reg query "$key" 2>/dev/null | grep -iE "node|npm|pnpm|yarn|node_modules|python.*-c") | |
| [ -n "$REG_OUT" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST [Registry: $key] $REG_OUT" | |
| done | |
| # Windows: Startup folder | |
| STARTUP_DIR="$WIN_APPDATA/Microsoft/Windows/Start Menu/Programs/Startup" | |
| if [ -d "$STARTUP_DIR" ]; then | |
| # Only check text-based scripts (not .lnk shortcuts which are binary and can't be inspected) | |
| SUSP=$(find "$STARTUP_DIR" \( -name "*.bat" -o -name "*.cmd" -o -name "*.vbs" -o -name "*.js" -o -name "*.ps1" -o -name "*.wsf" \) -mtime -30 2>/dev/null | while read f; do | |
| if grep -qiE "node|npm|node_modules|python.*-c|eval.*atob" "$f" 2>/dev/null; then | |
| echo "$f" | |
| fi | |
| done) | |
| [ -n "$SUSP" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST $SUSP" | |
| fi | |
| # Windows: Task Scheduler | |
| SCHED_OUT=$(schtasks /query /FO CSV /NH 2>/dev/null | grep -iE "node|npm|node_modules" | head -5) | |
| [ -n "$SCHED_OUT" ] && SUSPICIOUS_PERSIST="$SUSPICIOUS_PERSIST [Scheduled Tasks] $SCHED_OUT" | |
| fi | |
| if [ -n "$SUSPICIOUS_PERSIST" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$SUSPICIOUS_PERSIST" | tr ' ' '\n' | grep -v '^$' | head -10 | while read line; do | |
| echo " - $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 10. Check shell profiles for malware injection (ALL platform paths) | |
| echo -n "[10/16] Checking shell profiles for injection... " | |
| SUSPICIOUS_SHELL="" | |
| SHELL_GREP_PATTERN="eval.*\\\$\(|node\s+-e|\.node_modules|atob|curl.*\|.*sh|wget.*\|.*sh|base64.*decode|python.*-c.*import" | |
| SHELL_EXCLUDE="rbenv init|pyenv init|nvm|brew shellenv|conda init|sdkman|asdf|fnm|starship init|zoxide init|fzf|thefuck" | |
| for f in "${SHELL_FILES[@]}"; do | |
| if [ -f "$f" ]; then | |
| # For PowerShell profiles, also check for Invoke-Expression, IEX, DownloadString | |
| if echo "$f" | grep -q "\.ps1$"; then | |
| SUSPICIOUS_LINES=$(grep -niE "Invoke-Expression|IEX |DownloadString|\.node_modules|node.*-e|atob|base64" "$f" 2>/dev/null | head -3) | |
| else | |
| SUSPICIOUS_LINES=$(grep -nE "$SHELL_GREP_PATTERN" "$f" 2>/dev/null | \ | |
| grep -vE "$SHELL_EXCLUDE" | head -3) | |
| fi | |
| if [ -n "$SUSPICIOUS_LINES" ]; then | |
| SUSPICIOUS_SHELL="$SUSPICIOUS_SHELL $f" | |
| fi | |
| fi | |
| done | |
| if [ -n "$SUSPICIOUS_SHELL" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $SUSPICIOUS_SHELL; do | |
| echo " - $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 11. Check for suspicious Python installations (OmniStealer) | |
| echo -n "[11/16] Checking for suspicious Python installations... " | |
| SUSP_PYTHON="" | |
| for d in "${PYTHON3127_DIRS[@]}"; do | |
| if [ -d "$d" ]; then | |
| SUSP_PYTHON="$SUSP_PYTHON Python3127 found at $d" | |
| fi | |
| done | |
| # Check for python executable in temp directories | |
| for search_dir in "${PYTHON_SEARCH_DIRS[@]}"; do | |
| if [ -d "$search_dir" ]; then | |
| if [ "$OS" = "windows" ]; then | |
| HIDDEN_PYTHON=$(find "$search_dir" -maxdepth 3 -name "python*" -type f 2>/dev/null | head -3) | |
| else | |
| HIDDEN_PYTHON=$(find "$search_dir" -maxdepth 3 -name "python*" -type f -perm /111 2>/dev/null | head -3) | |
| fi | |
| [ -n "$HIDDEN_PYTHON" ] && SUSP_PYTHON="$SUSP_PYTHON $HIDDEN_PYTHON" | |
| fi | |
| done | |
| if [ -n "$SUSP_PYTHON" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$SUSP_PYTHON" | tr ' ' '\n' | grep -v '^$' | while read line; do | |
| echo " - $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 12. Check git config for tampering | |
| echo -n "[12/16] Checking git config for tampering... " | |
| GIT_TAMPERED="" | |
| # Check all possible home directories | |
| GIT_HOMES=("$EFFECTIVE_HOME") | |
| if [ "$OS" = "windows" ] || [ "$OS" = "wsl" ]; then | |
| WIN_H="${WIN_HOME_UNIX:-$WIN_HOME_MNT}" | |
| [ "$WIN_H" != "$EFFECTIVE_HOME" ] && GIT_HOMES+=("$WIN_H") | |
| fi | |
| for gh in "${GIT_HOMES[@]}"; do | |
| if [ -f "$gh/.gitconfig" ]; then | |
| if grep -qiE "alias.*curl|alias.*wget|alias.*node.*-e|core\.hooksPath.*\.node_modules" "$gh/.gitconfig" 2>/dev/null; then | |
| GIT_TAMPERED="$GIT_TAMPERED $gh/.gitconfig" | |
| fi | |
| fi | |
| if [ -d "$gh/.git/hooks" ]; then | |
| if [ "$OS" = "windows" ]; then | |
| SUSP_HOOKS=$(find "$gh/.git/hooks" -type f 2>/dev/null | xargs grep -l "curl\|wget\|node.*-e\|eval\|atob" 2>/dev/null) | |
| else | |
| SUSP_HOOKS=$(find "$gh/.git/hooks" -type f -perm /111 2>/dev/null | xargs grep -l "curl\|wget\|node.*-e\|eval\|atob" 2>/dev/null) | |
| fi | |
| [ -n "$SUSP_HOOKS" ] && GIT_TAMPERED="$GIT_TAMPERED $SUSP_HOOKS" | |
| fi | |
| done | |
| if [ -n "$GIT_TAMPERED" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| for f in $GIT_TAMPERED; do | |
| echo " - $f" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 13. Check for excessive socket connections (malware opens 100+ sockets) | |
| echo -n "[13/16] Checking for excessive socket connections... " | |
| NODE_PIDS=$(get_node_pids) | |
| EXCESSIVE_SOCKETS="" | |
| for pid in $NODE_PIDS; do | |
| SOCKET_COUNT=$(count_pid_connections "$pid") | |
| SOCKET_COUNT=${SOCKET_COUNT:-0} | |
| if [ "$SOCKET_COUNT" -gt 50 ]; then | |
| if [ "$OS" = "windows" ]; then | |
| PROC_CMD=$(wmic process where "ProcessId=$pid" get CommandLine 2>/dev/null | tail -2 | head -1 | head -c 80) | |
| else | |
| PROC_CMD=$(ps -p "$pid" -o args= 2>/dev/null | head -c 80) | |
| fi | |
| EXCESSIVE_SOCKETS="$EXCESSIVE_SOCKETS | |
| PID $pid: $SOCKET_COUNT sockets - $PROC_CMD" | |
| fi | |
| done | |
| if [ -n "$EXCESSIVE_SOCKETS" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$EXCESSIVE_SOCKETS" | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 14. Check for unusual network activity | |
| echo -n "[14/16] Checking for unusual network activity... " | |
| if [ "$OS" = "windows" ]; then | |
| # Get node.exe PIDs and count their connections | |
| NODE_PID_LIST=$(tasklist /FI "IMAGENAME eq node.exe" /FO CSV /NH 2>/dev/null | awk -F'","' '{gsub(/"/, "", $2); print $2}' | grep -E '^[0-9]+$') | |
| TOTAL_NODE_CONNS=0 | |
| EXTERNAL_CONNS=0 | |
| for pid in $NODE_PID_LIST; do | |
| COUNT=$(netstat -ano 2>/dev/null | grep "$pid" | wc -l | tr -d ' ') | |
| EXT=$(netstat -ano 2>/dev/null | grep "$pid" | grep -v "127\.0\.0\.1\|::1\|\[::\]" | wc -l | tr -d ' ') | |
| TOTAL_NODE_CONNS=$((TOTAL_NODE_CONNS + COUNT)) | |
| EXTERNAL_CONNS=$((EXTERNAL_CONNS + EXT)) | |
| done | |
| else | |
| TOTAL_NODE_CONNS=$(lsof -i 2>/dev/null | grep "^node" | wc -l | tr -d ' ') | |
| TOTAL_NODE_CONNS=${TOTAL_NODE_CONNS:-0} | |
| EXTERNAL_CONNS=$(lsof -i 2>/dev/null | grep "^node" | grep -v "localhost\|127\.0\.0\.1\|::1\|\*:" | wc -l | tr -d ' ') | |
| EXTERNAL_CONNS=${EXTERNAL_CONNS:-0} | |
| fi | |
| if [ "$EXTERNAL_CONNS" -gt 20 ]; then | |
| echo -e "${RED}INFECTED${NC} ($EXTERNAL_CONNS external connections)" | |
| if [ "$OS" = "windows" ]; then | |
| for pid in $NODE_PID_LIST; do | |
| netstat -ano 2>/dev/null | grep "$pid" | grep -v "127\.0\.0\.1\|::1\|\[::\]" | head -5 | while read line; do | |
| echo " $line" | |
| done | |
| done | |
| else | |
| lsof -i 2>/dev/null | grep "^node" | grep -v "localhost\|127\.0\.0\.1\|::1\|\*:" | head -10 | while read line; do | |
| echo " $line" | |
| done | |
| fi | |
| INFECTED=1 | |
| elif [ "$TOTAL_NODE_CONNS" -gt 100 ]; then | |
| echo -e "${YELLOW}WARNING${NC} ($TOTAL_NODE_CONNS total node connections)" | |
| else | |
| echo -e "${GREEN}Clean${NC} ($TOTAL_NODE_CONNS node connections)" | |
| fi | |
| # 15. Check for browser extension data theft indicators | |
| echo -n "[15/16] Checking for extension data access... " | |
| EXTENSION_ACCESS="" | |
| WALLET_EXTENSIONS="nkbihfbeogaeaoehlefnkodbefgpgknn|bfnaelmomeimhlpmgjnjophhpkkoljpa|ibnejdfjmmkpcnlpebklmnkoeoihofec|hnfanknocfeofbddgcijnmhnfnkdnaad" | |
| for chrome_dir in "${CHROME_PROFILE_DIRS[@]}"; do | |
| if [ -d "$chrome_dir" ]; then | |
| RECENT_ACCESS=$(find "$chrome_dir" -name "*.ldb" -mmin -60 2>/dev/null | head -3) | |
| if [ -n "$RECENT_ACCESS" ]; then | |
| if [ "$OS" = "windows" ]; then | |
| # On Windows, check with handle.exe or just flag LDB access from non-chrome processes | |
| ACCESSOR=$(tasklist /V /FO CSV 2>/dev/null | grep -iE "Extensions" | grep -vi "chrome.exe" | head -3) | |
| else | |
| ACCESSOR=$(lsof 2>/dev/null | grep -E "Chrome.*Extensions" | grep -v "Google Chrome" | head -3) | |
| fi | |
| if [ -n "$ACCESSOR" ]; then | |
| EXTENSION_ACCESS="$ACCESSOR" | |
| fi | |
| fi | |
| fi | |
| done | |
| if [ -n "$EXTENSION_ACCESS" ]; then | |
| echo -e "${YELLOW}WARNING${NC} (non-Chrome process accessing extensions)" | |
| echo "$EXTENSION_ACCESS" | while read line; do | |
| echo " $line" | |
| done | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # 16. Check running processes for RAT/stealer indicators | |
| echo -n "[16/16] Checking processes for RAT signatures... " | |
| RAT_FOUND="" | |
| if [ "$OS" = "windows" ]; then | |
| # Use a temp file to avoid grep seeing itself in the process list | |
| PROC_DUMP=$(mktemp 2>/dev/null || echo "/tmp/_malware_proc_$$") | |
| wmic process get ProcessId,CommandLine /FORMAT:LIST 2>"$PROC_DUMP.err" | \ | |
| grep -vE "grep\.exe|detect-infection|wmic" > "$PROC_DUMP" 2>/dev/null | |
| PROC_CHECK=$(grep -E "\['_V'\]|\['_R'\]|C250617A|C5-benefit" "$PROC_DUMP" 2>/dev/null) | |
| TG_CHECK=$(grep -F "$TELEGRAM_TOKEN" "$PROC_DUMP" 2>/dev/null) | |
| rm -f "$PROC_DUMP" "$PROC_DUMP.err" 2>/dev/null | |
| else | |
| # Exclude grep and the scanner script itself from results | |
| PROC_CHECK=$(ps aux 2>/dev/null | grep -E "\['_V'\]|\['_R'\]|C250617A|C5-benefit|_\\\$af" | grep -v grep | grep -v "detect-infection") | |
| TG_CHECK=$(ps auxe 2>/dev/null | grep -F "$TELEGRAM_TOKEN" | grep -v grep | grep -v "detect-infection") | |
| fi | |
| [ -n "$PROC_CHECK" ] && RAT_FOUND="$PROC_CHECK" | |
| [ -n "$TG_CHECK" ] && RAT_FOUND="$RAT_FOUND Telegram exfil bot detected" | |
| if [ -n "$RAT_FOUND" ]; then | |
| echo -e "${RED}INFECTED${NC}" | |
| echo "$RAT_FOUND" | head -5 | while read line; do | |
| echo " $line" | |
| done | |
| INFECTED=1 | |
| else | |
| echo -e "${GREEN}Clean${NC}" | |
| fi | |
| # ======================================== | |
| # RESULTS | |
| # ======================================== | |
| echo "" | |
| echo "========================================" | |
| if [ $INFECTED -eq 1 ]; then | |
| echo -e " RESULT: ${RED}SYSTEM IS INFECTED${NC}" | |
| echo "========================================" | |
| echo "" | |
| echo "Recommended actions:" | |
| echo " 1. Disconnect from network immediately" | |
| echo " 2. Kill malicious processes:" | |
| if [ "$OS" = "windows" ]; then | |
| echo " taskkill /PID <PID> /F" | |
| else | |
| echo " kill -9 <PID>" | |
| fi | |
| echo " 3. Remove persistence:" | |
| if [ "$OS" = "windows" ]; then | |
| echo " rd /s /q \"%USERPROFILE%\\.node_modules\"" | |
| echo " rd /s /q \"%USERPROFILE%\\node_modules\"" | |
| echo " rd /s /q \"%APPDATA%\\node_modules\" (NOT %APPDATA%\\npm\\node_modules - that is legitimate)" | |
| echo " rd /s /q \"%LOCALAPPDATA%\\node_modules\"" | |
| echo " Check: reg query HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run" | |
| echo " Check: Task Scheduler (schtasks /query)" | |
| echo " Check: %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\" | |
| else | |
| echo " rm -rf ~/.node_modules" | |
| fi | |
| echo " 4. Check and reinstall VSCode/Cursor" | |
| echo " 5. Review and restore shell profiles" | |
| if [ "$OS" = "windows" ]; then | |
| echo " Also check PowerShell profiles:" | |
| echo " %USERPROFILE%\\Documents\\PowerShell\\Microsoft.PowerShell_profile.ps1" | |
| echo " %USERPROFILE%\\Documents\\WindowsPowerShell\\Microsoft.PowerShell_profile.ps1" | |
| fi | |
| echo " 6. Rotate ALL credentials/tokens/SSH keys" | |
| echo " 7. Check browser extensions & wallets" | |
| echo " 8. Review git config: git config --global --list" | |
| echo "" | |
| echo "C2 IPs to block:" | |
| echo " - 198.105.127.0/24" | |
| echo " - 23.27.20.143:27017" | |
| echo " - 136.0.9.8:27017" | |
| echo " - 23.27.202.27:27017" | |
| echo " - 166.88.4.2:27017" | |
| echo "" | |
| echo "Blockchain C2 domains:" | |
| echo " - api.trongrid.io" | |
| echo " - fullnode.mainnet.aptoslabs.com" | |
| echo " - bsc-dataseed.binance.org" | |
| echo " - bsc-rpc.publicnode.com" | |
| echo "" | |
| echo "Malware wallet addresses:" | |
| echo " - TMfKQEd7TJJa5xNZJZ2Lep838vrzzrs7mAP (Tron)" | |
| echo " - TXfxHUet9pJVU1BgVkBAbrES4YUc1nGzcG (Tron)" | |
| echo " - TLmj13VL4p6NQ7jpxz8d9uYY6FUKCYatSe (Tron)" | |
| else | |
| echo -e " RESULT: ${GREEN}SYSTEM IS CLEAN${NC}" | |
| echo "========================================" | |
| fi | |
| exit $INFECTED |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment