← All Posts

Linux Forensics Script: Evidence Capture

Code & Tools
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."