Skip to content

Instantly share code, notes, and snippets.

@LinkPhoenix
Last active January 29, 2026 19:26
Show Gist options
  • Select an option

  • Save LinkPhoenix/f8ff58d600d50165ad97c77767d5cd08 to your computer and use it in GitHub Desktop.

Select an option

Save LinkPhoenix/f8ff58d600d50165ad97c77767d5cd08 to your computer and use it in GitHub Desktop.
perctl, perfclean malware detection

perctl_malaware_detection.sh

Minimal detection helper for Perfctl indicators on Linux servers. Perfctl uses sophisticated evasion: userland rootkit (libfsnldev.so/libgcwrap.so via LD_PRELOAD) intercepts system calls like getdents to hide binaries from ls/cat; binaries self-delete post-execution but remain active (detectable via stat/lsof through open inodes); and trojanizes utilities (replaces top/ldd in ~/.local/bin). This script runs stat on all paths (ignoring ls visibility) to detect hidden/deleted files, groups matches by birth date (date-only), and scans root cron for persistence pointing to /root/.config/cron/perfcc.

Requirements

  • Linux + bash
  • Run as root (recommended) to read root/system cron locations

Usage

sudo bash perctl_malaware_detection.sh

What it checks

  • Known Perfctl-related paths (e.g., /tmp/.xdiag/*, /tmp/.perf.c/*, /usr/*, /root/*, /etc/ld.so.preload, etc.)
  • Birth date grouping to spot multiple suspicious binaries created the same day
  • Root cron persistence: any cron entry executing /root/.config/cron/perfcc

Interpreting results

  • Dates with multiple files: strong indicator that multiple components were dropped around the same time.
  • CRITICAL: cron infection detected: finding /root/.config/cron/perfcc in root/system cron is a major persistence indicator. Removing the cron job alone is not sufficient.
  • If warnings trigger, treat the host as compromised: isolate it, preserve evidence if needed, and consider a full OS reinstall from a clean source.

Background (Perfctl overview)

See the write-up: “Perfctl, un malware Linux tenace”.

Disclaimer

This script is not a guarantee of absence/presence of Perfctl. Use it as a triage tool alongside proper incident response procedures.

#!/bin/bash
# Malware Perfctl Detection Script - Colored Output
# Scans directories for known perfctl binaries and reports stat info with birth dates
# Groups files by birth date at the end
# More informations : https://next.ink/152853/perfctl-un-malware-linux-tenace/
declare -A birth_dates
declare -a scanned_files
infection_detected=false
# ANSI color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
echo -e "${CYAN}=== Perfctl Malware Scanner - $(date) ===${NC}"
# Function to check directory and specific files
check_dir_files() {
local dir="$1"
shift
local files=("$@")
if [ ! -d "$dir" ]; then
echo -e "${YELLOW}Directory ${RED}$dir${YELLOW} does not exist${NC}"
return
fi
echo -e "${BLUE}Scanning directory: ${PURPLE}$dir${NC}"
ls -la "$dir" 2>/dev/null | head -20 || echo -e "${YELLOW}ls failed or empty${NC}"
echo
for file in "${files[@]}"; do
fullpath="$dir/$file"
scanned_files+=("$fullpath")
echo -e "${GREEN}Checking: ${PURPLE}$fullpath${NC}"
if [ -e "$fullpath" ]; then
echo -e "${RED}FOUND!${NC} Path exists"
else
echo -e "${YELLOW}NOT FOUND${NC} (will still run stat)"
fi
# Always run stat, even if the file does not exist (to capture error/output)
stat_output=$(stat "$fullpath" 2>&1)
stat_rc=$?
if [ $stat_rc -eq 0 ]; then
# Extract birth date - get full timestamp first
birth_full=$(echo "$stat_output" | grep "Birth:" | awk '{print $2" "$3" "$4" "$5}')
if [ -n "$birth_full" ]; then
# Extract only date part (without time, minutes, seconds)
# Method: handle both ISO format (YYYY-MM-DD) and standard format (Mon DD YYYY)
birth_date=$(echo "$birth_full" | awk '{
# Check if first field is ISO date format (YYYY-MM-DD)
if ($1 ~ /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/) {
print $1 # ISO format: just take YYYY-MM-DD
} else {
# Standard format: "Mon DD HH:MM:SS YYYY" -> take "Mon DD YYYY"
# Find the year field (4 digits) and take month, day, year
for (i=1; i<=NF; i++) {
if ($i ~ /^[0-9]{4}$/) {
# Found year, take month day year
print $1" "$2" "$i
break
}
}
}
}')
birth_dates["$birth_date"]+="$fullpath "
echo -e "${BLUE}Birth (full): ${GREEN}$birth_full${NC}"
echo -e "${BLUE}Birth (date only): ${GREEN}$birth_date${NC}"
else
echo -e "${YELLOW}No Birth time available${NC}"
fi
echo "$stat_output"
else
echo -e "${YELLOW}stat failed (rc=$stat_rc)${NC}"
echo "$stat_output"
fi
echo "---"
done
}
# 1. /usr
check_dir_files "/usr/bin" "perfcc" "wizlmsh"
check_dir_files "/usr/local/bin" "ldd" "top"
check_dir_files "/usr/lib" "libfsnldev.so" "libgcwrap.so" "libpprocps.so"
# 2. /tmp
check_dir_files "/tmp/.xdiag" "cp" "elog" "exi" "p" "uid" "ver"
check_dir_files "/tmp/.xdiag/int" "e.lock"
check_dir_files "/tmp/.xdiag/hroot" "cp" "hscheck"
check_dir_files "/tmp/.xdiag/tordata" "state.tmp"
check_dir_files "/tmp" ".apid" "lgctr" "lgctr2"
check_dir_files "/tmp/.perf.c" "Loader" "perfctl"
# 3. /root
check_dir_files "/root" "sedkBrgaa"
check_dir_files "/root/.cache" "pci.ids"
check_dir_files "/root/.config/cron" "perfcc"
# 4. /etc
check_dir_files "/etc" "ld.so.preload" "profile"
# 5. Scan root crontabs for perfcc references
echo ""
echo -e "${CYAN}=== SCANNING ROOT CRONTABS ===${NC}"
cron_infection_detected=false
cron_files_found=()
# Check if running as root
if [ "$EUID" -ne 0 ]; then
echo -e "${YELLOW}Warning: Not running as root. Some crontab locations may not be accessible.${NC}"
fi
# Function to scan a crontab file for perfcc reference
scan_crontab_file() {
local file="$1"
local desc="$2"
if [ -f "$file" ] && [ -r "$file" ]; then
echo -e "${BLUE}Checking: ${PURPLE}$desc${NC} ($file)"
if grep -q "/root/.config/cron/perfcc" "$file" 2>/dev/null; then
echo -e "${RED}⚠ FOUND!${NC} Reference to /root/.config/cron/perfcc detected"
cron_infection_detected=true
cron_files_found+=("$file ($desc)")
echo -e "${YELLOW}Crontab content:${NC}"
grep -n "/root/.config/cron/perfcc" "$file" 2>/dev/null | while IFS= read -r line; do
echo -e " ${RED}$line${NC}"
done
else
echo -e "${GREEN}No perfcc reference found${NC}"
fi
elif [ -f "$file" ] && [ ! -r "$file" ]; then
echo -e "${YELLOW}File exists but is not readable: $file${NC}"
fi
}
# Scan user crontab (root)
echo -e "${BLUE}Checking root user crontab...${NC}"
if crontab -l -u root 2>/dev/null | grep -q "/root/.config/cron/perfcc"; then
echo -e "${RED}⚠ FOUND!${NC} Reference to /root/.config/cron/perfcc in root crontab"
cron_infection_detected=true
cron_files_found+=("root crontab (crontab -l)")
echo -e "${YELLOW}Crontab content:${NC}"
crontab -l -u root 2>/dev/null | grep -n "/root/.config/cron/perfcc" | while IFS= read -r line; do
echo -e " ${RED}$line${NC}"
done
else
echo -e "${GREEN}No perfcc reference in root crontab${NC}"
fi
# Scan system crontab files
scan_crontab_file "/var/spool/cron/crontabs/root" "root crontab file"
scan_crontab_file "/etc/crontab" "system crontab"
# Scan cron.d directory
if [ -d "/etc/cron.d" ]; then
echo -e "${BLUE}Scanning /etc/cron.d/* files...${NC}"
for cronfile in /etc/cron.d/*; do
if [ -f "$cronfile" ]; then
scan_crontab_file "$cronfile" "cron.d: $(basename $cronfile)"
fi
done
fi
# Scan cron directories (hourly, daily, weekly, monthly)
for crondir in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly; do
if [ -d "$crondir" ]; then
echo -e "${BLUE}Scanning $crondir/* files...${NC}"
for cronfile in "$crondir"/*; do
if [ -f "$cronfile" ] && [ -x "$cronfile" ]; then
# Check if it's a script that might call perfcc
if grep -q "/root/.config/cron/perfcc" "$cronfile" 2>/dev/null; then
echo -e "${RED}⚠ FOUND!${NC} Reference in $(basename $crondir)/$(basename $cronfile)"
cron_infection_detected=true
cron_files_found+=("$cronfile ($(basename $crondir))")
fi
fi
done
fi
done
# Display cron infection warning if detected
if [ "$cron_infection_detected" = true ]; then
echo ""
# Adaptive banner for cron infection
box_content_width=$((TERM_WIDTH - 4))
[ $box_content_width -lt 54 ] && box_content_width=54
box_total_width=$((box_content_width + 4))
n=$((box_total_width - 2)); hline=""; while [ ${#hline} -lt $n ]; do hline="${hline}="; done
_pad() { printf "%-${box_content_width}s" "$1"; }
echo -e "${RED}+${hline}+${NC}"
echo -e "${RED}|${NC} ${YELLOW}$( _pad "^^^ CRITICAL: CRON INFECTION DETECTED ^^^" )${NC} ${RED}|${NC}"
echo -e "${RED}+${hline}+${NC}"
echo -e "${RED}|${NC} $( _pad "A cron job was found that executes /root/.config/cron/perfcc" ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "This is a STRONG indicator of perfctl malware infection." ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "" ) ${RED}|${NC}"
echo -e "${RED}|${NC} ${YELLOW}$( _pad "WARNING:" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "Simply removing the cron job is NOT sufficient!" ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "The malware has likely installed other persistence" ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "mechanisms and system modifications." ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "" ) ${RED}|${NC}"
echo -e "${RED}|${NC} ${YELLOW}$( _pad "RECOMMENDED ACTION:" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> IMMEDIATELY reinstall your operating system" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Do NOT attempt manual removal" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Backup data and reinstall from clean source" )${NC} ${RED}|${NC}"
echo -e "${RED}+${hline}+${NC}"
echo ""
echo -e "${YELLOW}Affected crontab files:${NC}"
for cron_entry in "${cron_files_found[@]}"; do
echo -e " ${RED}→ $cron_entry${NC}"
done
# Set global infection flag
infection_detected=true
fi
# Summary by birth date - Table format
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN}=== BIRTH DATE MATCHING TABLE ===${NC}"
echo -e "${CYAN}=== GROUPED BY BIRTH DATE ===${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
# Get terminal width for adaptive table formatting
if command -v tput >/dev/null 2>&1; then
TERM_WIDTH=$(tput cols)
else
TERM_WIDTH=80 # Default fallback
fi
# Adjust column widths based on terminal width
if [ $TERM_WIDTH -lt 80 ]; then
DATE_WIDTH=20
COUNT_WIDTH=5
FILE_WIDTH=$((TERM_WIDTH - DATE_WIDTH - COUNT_WIDTH - 6))
elif [ $TERM_WIDTH -lt 120 ]; then
DATE_WIDTH=25
COUNT_WIDTH=6
FILE_WIDTH=$((TERM_WIDTH - DATE_WIDTH - COUNT_WIDTH - 6))
else
DATE_WIDTH=30
COUNT_WIDTH=6
FILE_WIDTH=$((TERM_WIDTH - DATE_WIDTH - COUNT_WIDTH - 6))
fi
# Ensure minimum widths
[ $DATE_WIDTH -lt 15 ] && DATE_WIDTH=15
[ $COUNT_WIDTH -lt 5 ] && COUNT_WIDTH=5
[ $FILE_WIDTH -lt 20 ] && FILE_WIDTH=20
if [ ${#birth_dates[@]} -eq 0 ]; then
echo -e "${YELLOW}No birth dates found or no files detected.${NC}"
else
# Sort dates for consistent output
IFS=$'\n' sorted_dates=($(printf '%s\n' "${!birth_dates[@]}" | sort))
# Print table header
printf "${CYAN}%-${DATE_WIDTH}s | %-${COUNT_WIDTH}s | %s${NC}\n" "BIRTH DATE" "COUNT" "FILES"
separator=$(printf "%-${DATE_WIDTH}s-+-%-${COUNT_WIDTH}s-+-%s" "" "" "" | tr ' ' '-')
echo -e "${CYAN}${separator}${NC}"
# Print each date group
for date in "${sorted_dates[@]}"; do
files="${birth_dates[$date]}"
# Count files (split by space and count non-empty elements)
file_count=$(echo "$files" | tr ' ' '\n' | grep -v '^$' | wc -l)
# Truncate date if too long
date_display="$date"
if [ ${#date_display} -gt $DATE_WIDTH ]; then
date_display="${date_display:0:$((DATE_WIDTH-3))}..."
fi
# Print date and count on first line
printf "${GREEN}%-${DATE_WIDTH}s${NC} | ${BLUE}%-${COUNT_WIDTH}s${NC} | " "$date_display" "$file_count"
# Print first file on same line, others on continuation lines
first_file=$(echo "$files" | awk '{print $1}')
remaining_files=$(echo "$files" | awk '{$1=""; print $0}' | sed 's/^ *//')
if [ -n "$first_file" ]; then
# Truncate file path if too long
first_file_display="$first_file"
if [ ${#first_file_display} -gt $FILE_WIDTH ]; then
first_file_display="...${first_file_display: -$((FILE_WIDTH-3))}"
fi
echo -e "${PURPLE}$first_file_display${NC}"
# Print remaining files on new lines with proper alignment
if [ -n "$remaining_files" ]; then
for f in $remaining_files; do
if [ -n "$f" ]; then
# Truncate file path if too long
f_display="$f"
if [ ${#f_display} -gt $FILE_WIDTH ]; then
f_display="...${f_display: -$((FILE_WIDTH-3))}"
fi
printf "${CYAN}%-${DATE_WIDTH}s | %-${COUNT_WIDTH}s | ${NC}${PURPLE}%s${NC}\n" "" "" "$f_display"
fi
done
fi
else
echo "" # New line if no files
fi
echo -e "${CYAN}${separator}${NC}"
done
# Summary statistics
total_files=0
infection_detected=false
for date in "${sorted_dates[@]}"; do
files="${birth_dates[$date]}"
count=$(echo "$files" | tr ' ' '\n' | grep -v '^$' | wc -l)
total_files=$((total_files + count))
# Check if multiple files share the same birth date (infection indicator)
if [ $count -gt 1 ]; then
infection_detected=true
fi
done
echo ""
echo -e "${CYAN}=== SUMMARY ===${NC}"
echo -e "${GREEN}Total unique dates: ${BLUE}${#birth_dates[@]}${NC}"
echo -e "${GREEN}Total files with birth date: ${BLUE}$total_files${NC}"
# Highlight dates with multiple files (potential matches)
echo ""
echo -e "${CYAN}=== DATES WITH MULTIPLE FILES (suspicious matches) ===${NC}"
found_multi=false
for date in "${sorted_dates[@]}"; do
files="${birth_dates[$date]}"
count=$(echo "$files" | tr ' ' '\n' | grep -v '^$' | wc -l)
if [ $count -gt 1 ]; then
found_multi=true
echo -e "${RED}⚠ DATE: ${YELLOW}$date${RED} - ${count} files:${NC}"
for f in $files; do
if [ -n "$f" ]; then
echo -e " ${PURPLE} → $f${NC}"
fi
done
fi
done
if [ "$found_multi" = false ]; then
echo -e "${YELLOW}No dates with multiple files found.${NC}"
fi
# Infection warning - adaptive banner so right border stays aligned in any terminal width
echo ""
if [ "$infection_detected" = true ]; then
# Box content width: fit terminal minus borders, minimum 54 for readability
box_content_width=$((TERM_WIDTH - 4))
[ $box_content_width -lt 54 ] && box_content_width=54
box_total_width=$((box_content_width + 4))
n=$((box_total_width - 2)); hline=""; while [ ${#hline} -lt $n ]; do hline="${hline}="; done
_pad() { printf "%-${box_content_width}s" "$1"; }
echo -e "${RED}+${hline}+${NC}"
echo -e "${RED}|${NC} ${YELLOW}$( _pad "^^^ WARNING: POTENTIAL INFECTION DETECTED ^^^" )${NC} ${RED}|${NC}"
echo -e "${RED}+${hline}+${NC}"
echo -e "${RED}|${NC} $( _pad "Multiple binaries were found with the same birth date." ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "This is a strong indicator of perfctl malware infection." ) ${RED}|${NC}"
echo -e "${RED}|${NC} $( _pad "" ) ${RED}|${NC}"
echo -e "${RED}|${NC} ${YELLOW}$( _pad "RECOMMENDED ACTION:" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Reinstall your operating system from a clean source" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Do NOT attempt to remove the malware manually" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Backup important data before reinstalling" )${NC} ${RED}|${NC}"
echo -e "${RED}|${NC} ${GREEN}$( _pad "-> Scan backups with antivirus before restoring" )${NC} ${RED}|${NC}"
echo -e "${RED}+${hline}+${NC}"
else
echo -e "${GREEN}✓ No suspicious date matches found.${NC}"
fi
fi
echo ""
echo -e "${CYAN}Scan complete.${NC}"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment