Linux Forensics Script: Evidence Capture

linuxf cover

1. Project Overview

This project involves creating a Bash script for digital forensics on Linux systems, capable of collecting critical system information with minimal privileges. The script gathers data such as network connections, running processes, recent files, system logs, user information, and system details, saving them to a timestamped folder for analysis by a Security Operations Center (SOC) team.

2. Script Breakdown and Forensic Relevance

Security Note: Always review code before executing. Verify integrity with the SHA256 hash provided for the full script below.

2.1 Setup and Output Directory

Relevance: A timestamped output directory organizes evidence and ensures traceability, critical for maintaining the chain of custody. The script checks for writable directories and sufficient disk space to avoid failures during evidence collection.


# Default settings
OUTPUT_DIR="/tmp"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
EVIDENCE_DIR="${OUTPUT_DIR}/evidence_${TIMESTAMP}"

# Validate output directory
if [[ ! -d "$OUTPUT_DIR" || ! -w "$OUTPUT_DIR" ]]; then
    echo "Error: Output directory '$OUTPUT_DIR' is not writable"
    exit 1
fi

# Check disk space (require at least 100MB)
if ! df -m "$OUTPUT_DIR" | tail -1 | awk '{if ($4 < 100) exit 1}'; then
    echo "Error: Insufficient disk space in $OUTPUT_DIR"
    exit 1
fi

# Create evidence directory with restrictive permissions
mkdir -m 700 -p "$EVIDENCE_DIR" || { echo "Error: Failed to create $EVIDENCE_DIR"; exit 1; }
echo "Collecting evidence in $EVIDENCE_DIR..."
            
evidence dir shot

2.2 Network Information

Relevance: Capturing network connections and firewall rules can reveal active communications with command-and-control servers or unauthorized services, aiding in the identification of compromised systems.


# Network Information
command_exists netstat && {
    save_output "netstat_listening" "netstat -tulnp"
    save_output "netstat_all" "netstat -nap"
}
save_output "ss_listening" "ss -tulnp"
save_output "ss_all" "ss -nap"
command_exists iptables && save_output "iptables" "iptables -L -v -n"
command_exists nft && save_output "nftables" "nft list ruleset"
save_output "network_interfaces" "ip addr show"
save_output "routing_table" "ip route show"
            

2.3 Process Information

Relevance: A snapshot of running processes and services can uncover malicious applications or unauthorized cron jobs, providing insights into potential persistence mechanisms.


# Process Information
save_output "processes" "ps aux"
save_output "top_snapshot" "top -b -n 1"
if command_exists systemctl; then
    save_output "systemd_services" "systemctl list-units --type=service"
else
    save_output "services" "service --status-all 2>/dev/null || ls /etc/init.d/"
fi
save_output "cron_jobs" "cat /etc/crontab 2>/dev/null; ls -l /etc/cron.* 2>/dev/null; crontab -l 2>/dev/null"
            

2.4 File System Information

Relevance: Recent files and SUID binaries can indicate malicious activity, such as temporary files created by attackers or exploitable binaries. Disk usage data helps assess system impact.


# File System Information
save_output "recent_files" "find /home /etc /var -mtime -7 -ls 2>/dev/null"
save_output "suid_binaries" "find / -perm -u=s -type f 2>/dev/null"
save_output "tmp_files" "ls -la /tmp /var/tmp /dev/shm 2>/dev/null"
save_output "disk_usage" "df -h; du -sh /home /etc /var 2>/dev/null"
            

2.5 Logs

Relevance: System and authentication logs can reveal unauthorized access attempts or system changes, critical for reconstructing the timeline of an incident.


# Logs
collect_logs "/var/log/auth.log" "/var/log/secure"
collect_logs "/var/log/syslog" "/var/log/messages"
save_output "dmesg" "dmesg"
[[ -r "/var/log/audit/audit.log" ]] && save_output "audit_log" "cat /var/log/audit/audit.log"
            

2.6 User Information

Relevance: User accounts, SSH keys, and login history can uncover unauthorized accounts or access, helping identify compromised credentials or backdoors.


# User Information
save_output "users" "cat /etc/passwd"
[[ -r "/etc/sudoers" ]] && save_output "sudoers" "cat /etc/sudoers"
save_output "ssh_keys" "find /home /root -name authorized_keys -exec cat {} \; 2>/dev/null"
save_output "last_logins" "last -a"
            

2.7 System Information

Relevance: System details like kernel version, CPU, memory, and loaded modules provide context for vulnerabilities or anomalies, aiding in forensic analysis.


# System Information
save_output "system_info" "uname -a; lscpu; cat /proc/meminfo"
save_output "uptime" "uptime"
command_exists sensors && save_output "sensors" "sensors"
save_output "loaded_modules" "lsmod"
            

3. Tips and Lessons Learned

Practical advice and insights gained from developing the script:

4. Conclusion

The Linux forensics script provides a robust solution for collecting critical system information with minimal privileges, ideal for rapid incident response in restricted environments. Its modular design and compatibility across distributions make it a valuable tool for SOC teams. Future enhancements could include additional evidence types and automated analysis features.

5. Full Code

Security Note: Always review code before executing. Verify integrity with SHA256 Hash: 30213F9D371F25399C6F59B7276D6D541F6DE42026E6C6CD70520951E5216169.


#!/bin/bash

# Script to collect forensic evidence from Linux devices
# Compatible across distributions, handles errors, and consolidates output
# Usage: ./collect_evidence.sh [-o output_dir] [-t type] [-h]

# Default settings
OUTPUT_DIR="/tmp"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
EVIDENCE_DIR="${OUTPUT_DIR}/evidence_${TIMESTAMP}"
COLLECT_TYPES="all" # Options: all, network, processes, filesystem, logs, users, system
COMPRESS=true
CLEANUP=false
JSON_OUTPUT=false

# Help message
usage() {
    echo "Usage: $0 [-o output_dir] [-t type] [-j] [-n] [-c] [-h]"
    echo "  -o  Output directory (default: /tmp)"
    echo "  -t  Evidence type: all, network, processes, filesystem, logs, users, system"
    echo "  -j  Output in JSON format"
    echo "  -n  No compression"
    echo "  -c  Clean up evidence directory after compression"
    echo "  -h  Show this help"
    exit 1
}

# Parse arguments
while getopts "o:t:jnc" opt; do
    case $opt in
        o) OUTPUT_DIR="$OPTARG" ;;
        t) COLLECT_TYPES="$OPTARG" ;;
        j) JSON_OUTPUT=true ;;
        n) COMPRESS=false ;;
        c) CLEANUP=true ;;
        h) usage ;;
        *) usage ;;
    esac
done

# Validate output directory
if [[ ! -d "$OUTPUT_DIR" || ! -w "$OUTPUT_DIR" ]]; then
    echo "Error: Output directory '$OUTPUT_DIR' is not writable"
    exit 1
fi

# Check disk space (require at least 100MB)
if ! df -m "$OUTPUT_DIR" | tail -1 | awk '{if ($4 < 100) exit 1}'; then
    echo "Error: Insufficient disk space in $OUTPUT_DIR"
    exit 1
fi

# Create evidence directory with restrictive permissions
mkdir -m 700 -p "$EVIDENCE_DIR" || { echo "Error: Failed to create $EVIDENCE_DIR"; exit 1; }
echo "Collecting evidence in $EVIDENCE_DIR..."

# Check if running as root
if [[ $EUID -ne 0 ]]; then
    echo "Warning: Running as non-root user; some commands may fail"
fi

# Function to check if a command exists
command_exists() {
    command -v "$1" >/dev/null 2>&1
}

# Function to save command output
save_output() {
    local cmd_name="$1"
    local cmd="$2"
    local output_file="${EVIDENCE_DIR}/${cmd_name}.txt"
    local json_entry=""

    # Check if command is available
    local cmd_bin=$(echo "$cmd" | awk '{print $1}')
    if ! command_exists "$cmd_bin"; then
        echo "Warning: $cmd_bin not found, skipping $cmd_name" >&2
        echo "Error: $cmd_bin not installed" > "$output_file"
        return 1
    fi

    echo "Running $cmd_name..." >&2
    {
        echo "Command: $cmd"
        echo "Timestamp: $(date)"
        echo "----------------------------------------"
        if ! eval "$cmd" 2>/tmp/err.log; then
            echo "Error: Command failed or insufficient permissions"
            cat /tmp/err.log
        fi
    } > "$output_file" 2>/dev/null

    # Compute hash
    if [[ -s "$output_file" ]]; then
        sha256sum "$output_file" >> "${EVIDENCE_DIR}/hashes.txt"
    fi

    # JSON output
    if [[ "$JSON_OUTPUT" = true ]]; then
        json_entry=$(jq -n --arg cmd "$cmd" --arg file "$output_file" \
            --arg time "$(date)" '{command: $cmd, file: $file, timestamp: $time}')
        echo "$json_entry" >> "${EVIDENCE_DIR}/evidence.json"
    fi
}

# Function to collect logs with fallbacks
collect_logs() {
    local log_file="$1"
    local alt_file="$2"
    if [[ -r "$log_file" ]]; then
        save_output "$(basename "$log_file")" "cat $log_file"
    elif [[ -n "$alt_file" && -r "$alt_file" ]]; then
        save_output "$(basename "$alt_file")" "cat $alt_file"
    elif command_exists journalctl; then
        save_output "journalctl" "journalctl -n 1000"
    else
        echo "Warning: No log files accessible and journalctl unavailable" >&2
    fi
}

# Collect evidence based on type
collect_evidence() {
    case $COLLECT_TYPES in
        all|network)
            # Network Information
            command_exists netstat && {
                save_output "netstat_listening" "netstat -tulnp"
                save_output "netstat_all" "netstat -nap"
            }
            save_output "ss_listening" "ss -tulnp"
            save_output "ss_all" "ss -nap"
            command_exists iptables && save_output "iptables" "iptables -L -v -n"
            command_exists nft && save_output "nftables" "nft list ruleset"
            save_output "network_interfaces" "ip addr show"
            save_output "routing_table" "ip route show"
            ;;
    esac

    case $COLLECT_TYPES in
        all|processes)
            # Process Information
            save_output "processes" "ps aux"
            save_output "top_snapshot" "top -b -n 1"
            if command_exists systemctl; then
                save_output "systemd_services" "systemctl list-units --type=service"
            else
                save_output "services" "service --status-all 2>/dev/null || ls /etc/init.d/"
            fi
            save_output "cron_jobs" "cat /etc/crontab 2>/dev/null; ls -l /etc/cron.* 2>/dev/null; crontab -l 2>/dev/null"
            ;;
    esac

    case $COLLECT_TYPES in
        all|filesystem)
            # File System Information
            save_output "recent_files" "find /home /etc /var -mtime -7 -ls 2>/dev/null"
            save_output "suid_binaries" "find / -perm -u=s -type f 2>/dev/null"
            save_output "tmp_files" "ls -la /tmp /var/tmp /dev/shm 2>/dev/null"
            save_output "disk_usage" "df -h; du -sh /home /etc /var 2>/dev/null"
            ;;
    esac

    case $COLLECT_TYPES in
        all|logs)
            # Logs
            collect_logs "/var/log/auth.log" "/var/log/secure"
            collect_logs "/var/log/syslog" "/var/log/messages"
            save_output "dmesg" "dmesg"
            [[ -r "/var/log/audit/audit.log" ]] && save_output "audit_log" "cat /var/log/audit/audit.log"
            ;;
    esac

    case $COLLECT_TYPES in
        all|users)
            # User Information
            save_output "users" "cat /etc/passwd"
            [[ -r "/etc/sudoers" ]] && save_output "sudoers" "cat /etc/sudoers"
            save_output "ssh_keys" "find /home /root -name authorized_keys -exec cat {} \; 2>/dev/null"
            save_output "last_logins" "last -a"
            ;;
    esac

    case $COLLECT_TYPES in
        all|system)
            # System Information
            save_output "system_info" "uname -a; lscpu; cat /proc/meminfo"
            save_output "uptime" "uptime"
            command_exists sensors && save_output "sensors" "sensors"
            save_output "loaded_modules" "lsmod"
            ;;
    esac
}

# Initialize JSON output
if [[ "$JSON_OUTPUT" = true ]]; then
    echo "[]" > "${EVIDENCE_DIR}/evidence.json"
fi

# Run evidence collection
collect_evidence

# Compress evidence
if [[ "$COMPRESS" = true ]]; then
    tar -czf "${OUTPUT_DIR}/evidence_${TIMESTAMP}.tar.gz" -C "$OUTPUT_DIR" "evidence_${TIMESTAMP}" || {
        echo "Error: Failed to compress evidence"
        exit 1
    }
    sha256sum "${OUTPUT_DIR}/evidence_${TIMESTAMP}.tar.gz" >> "${EVIDENCE_DIR}/hashes.txt"
    echo "Evidence compressed to ${OUTPUT_DIR}/evidence_${TIMESTAMP}.tar.gz"
fi

# Cleanup
if [[ "$CLEANUP" = true && "$COMPRESS" = true ]]; then
    rm -rf "$EVIDENCE_DIR"
    echo "Evidence directory cleaned up"
fi

# Instructions
echo "To analyze, copy ${OUTPUT_DIR}/evidence_${TIMESTAMP}.tar.gz to a secure system and extract it."
echo "Verify integrity using ${EVIDENCE_DIR}/hashes.txt (if not cleaned up)."
echo "Review each .txt file for signs of compromise."