332 lines
12 KiB
Bash
Executable file
332 lines
12 KiB
Bash
Executable file
#!/bin/bash
|
|
#
|
|
# Fuzzing Test for System Binaries
|
|
#
|
|
# Tests CLI programs by running them with random options and test files.
|
|
# Reports only real crashes (segfaults, memory errors).
|
|
# Safe: skips dangerous commands (rm, dd, mkfs, etc.)
|
|
#
|
|
# psklenar@redhat.com
|
|
# Copyright (C) 2026 Petr Sklenar
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
#
|
|
# AI was used to generate this script.
|
|
|
|
set -uo pipefail
|
|
|
|
# Configuration
|
|
BIN_DIRS="/usr/bin /usr/sbin"
|
|
WORKSPACE="${HOME}/fuzz_lab"
|
|
LOG_FILE="${WORKSPACE}/segfault_fuzz_$(date +%Y%m%d_%H%M%S).log"
|
|
SUMMARY_FILE="${WORKSPACE}/segfault_summary.txt"
|
|
RUNS_PER_BIN=20
|
|
MAX_PARALLEL=10
|
|
TIMEOUT_SEC=5
|
|
KILL_AFTER=10
|
|
|
|
# Programs to skip (dangerous, interactive, GUI, or already reported)
|
|
SKIP_NAMES=(
|
|
bash rm dd mkfs reboot shutdown poweroff halt init telinit
|
|
sfdisk fdisk parted cfdisk gdisk sgdisk mkswap
|
|
login kill pkill killall systemd-ask-password sulogin chronyc chronyd
|
|
xfs_freeze fsck e2fsck xfs_repair
|
|
ip nmcli nmtui ifconfig route arp brctl bridge
|
|
iptables ip6tables nft ebtables arptables firewall-cmd
|
|
dhclient dhcpcd wpa_supplicant hostapd
|
|
dnf yum rpm zypper apt apt-get dpkg pacman
|
|
vi vim nvim nano emacs ed
|
|
gpg gpg2 gpg-agent ssh-keygen openssl
|
|
createuser dropuser createdb dropdb psql mysql mariadb mysqladmin mysql_secure_installation postgres pg_dump pg_restore mongosh redis-cli
|
|
passwd chpasswd useradd usermod userdel adduser deluser
|
|
aws gcloud kubectl docker podman
|
|
sql env_parallel parallel niceload parsort
|
|
unoconv ibus-setup ffmpeg ffprobe ffplay
|
|
gnome-keyring-3 gnome-boxes unix_chkpwd unix_update
|
|
cracklib-check code2color fsidd stund vimcolor
|
|
# Regex patterns for GUI programs
|
|
'.re:^gnome-|^gtk|^gdk|^gio-|^gsettings'
|
|
'.re:^kde|^plasma|^kwin|^dolphin|^konsole'
|
|
'.re:^xfce|^mate-|^cinnamon|^lxde|^lxqt'
|
|
'.re:^qt|^Qt|^wayland|^weston'
|
|
'.re:^X|^x11|^xorg|^xinit|^xterm'
|
|
'.re:^vlc|^totem|^rhythmbox|^brasero'
|
|
'.re:^firefox|^thunderbird|^chrome|^chromium'
|
|
'.re:^gedit|^kate|^kwrite|^pluma'
|
|
'.re:gui|^beakerlib'
|
|
'.re:^ip-|^nm-|^NetworkManager|^ovs-'
|
|
'.re:^[iI]'
|
|
'.re:^[hH]'
|
|
)
|
|
|
|
# Check if program should be skipped
|
|
should_skip() {
|
|
local name=$(basename "$1")
|
|
for skip in "${SKIP_NAMES[@]}"; do
|
|
if [[ "$skip" == .re:* ]]; then
|
|
[[ "$name" =~ ${skip#.re:} ]] && return 0
|
|
else
|
|
[[ "$name" == "$skip" ]] && return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Setup
|
|
echo "Setting up fuzzing test..."
|
|
mkdir -p "$WORKSPACE"
|
|
ulimit -n 65535 2>/dev/null || true
|
|
|
|
# Create test files
|
|
echo "Creating test files..."
|
|
touch "$WORKSPACE/empty.dat"
|
|
printf 'line1\nline2\n123\ntest\n' > "$WORKSPACE/small.txt"
|
|
cat > "$WORKSPACE/bad.json" <<'EOF'
|
|
{"key": "value", "broken": [1, 2, 3,
|
|
EOF
|
|
cat > "$WORKSPACE/bad.xml" <<'EOF'
|
|
<?xml version="1.0"?>
|
|
<root><unclosed><tag>value
|
|
EOF
|
|
dd if=/dev/urandom of="$WORKSPACE/random.bin" bs=1K count=10 2>/dev/null
|
|
printf '\x00\x00\xff\xff\xde\xad\xbe\xef\x00\x00' > "$WORKSPACE/nulls.bin"
|
|
dd if=/dev/zero of="$WORKSPACE/large.dat" bs=1M count=50 2>/dev/null
|
|
printf '\xc3\x28\xe2\x82\x28\xf0\x28\x8c\x28' > "$WORKSPACE/bad_utf8.txt"
|
|
printf '%%s%%s%%s%%n%%x%%x' > "$WORKSPACE/format.txt"
|
|
printf '../../../etc/passwd\n..\\..\\..\\windows\\system32' > "$WORKSPACE/paths.txt"
|
|
printf "' OR 1=1 --\n\"; DROP TABLE users; --" > "$WORKSPACE/sql_injection.txt"
|
|
printf '; ls -la\n| cat /etc/passwd\n`whoami`' > "$WORKSPACE/cmd.txt"
|
|
python3 -c "print('A' * 100000)" > "$WORKSPACE/longline.txt"
|
|
seq 1 100000 > "$WORKSPACE/manylines.txt"
|
|
cat "$WORKSPACE/small.txt" "$WORKSPACE/random.bin" > "$WORKSPACE/mixed.dat"
|
|
printf '\x1f\x8b\x08\x00\x00\x00\x00\x00' > "$WORKSPACE/gzip.dat"
|
|
printf '\x50\x4b\x03\x04' > "$WORKSPACE/zip.dat"
|
|
printf '\xff\xd8\xff\xe0\x00\x10JFIF' > "$WORKSPACE/fake.jpg"
|
|
printf '\x89PNG\r\n\x1a\n' > "$WORKSPACE/fake.png"
|
|
printf '\x7fELF\x02\x01\x01\x00' > "$WORKSPACE/fake.elf"
|
|
printf '#!/bin/sh\necho test' > "$WORKSPACE/script.sh"
|
|
printf -- '---\n-\n--help\n--version\n-v\n' > "$WORKSPACE/dashes.txt"
|
|
|
|
FUZZ_FILE_COUNT=22
|
|
FUZZ_FILE_NAMES="empty.dat small.txt bad.json bad.xml random.bin nulls.bin large.dat bad_utf8.txt format.txt paths.txt sql_injection.txt cmd.txt longline.txt manylines.txt mixed.dat gzip.dat zip.dat fake.jpg fake.png fake.elf script.sh dashes.txt"
|
|
|
|
# Tracking files
|
|
STOP_LOG="$WORKSPACE/script_stopped.txt"
|
|
CURRENT_RUN_FILE="$WORKSPACE/current_run.txt"
|
|
trap 'ec=$?; echo "$(date -Iseconds) EXIT code=$ec" >> "$STOP_LOG"' EXIT
|
|
trap 'echo "$(date -Iseconds) SIGNAL RECEIVED" >> "$STOP_LOG"; exit 129' HUP INT TERM
|
|
|
|
# Find programs to test
|
|
echo "Finding programs to test..."
|
|
ALL_BINS=()
|
|
while IFS= read -r bin; do
|
|
should_skip "$bin" && continue
|
|
ALL_BINS+=("$bin")
|
|
done < <(find $BIN_DIRS -maxdepth 1 -executable -type f 2>/dev/null | sort -u)
|
|
|
|
TOTAL=${#ALL_BINS[@]}
|
|
echo ""
|
|
echo "===================="
|
|
echo "Total programs to test: $TOTAL"
|
|
echo "Runs per program: $RUNS_PER_BIN"
|
|
echo "Max parallel jobs: $MAX_PARALLEL"
|
|
echo "Test files: $FUZZ_FILE_COUNT"
|
|
echo "Log file: $LOG_FILE"
|
|
echo "===================="
|
|
echo ""
|
|
|
|
> "$LOG_FILE"
|
|
|
|
# Fuzzing function - tests one program with random options and files
|
|
fuzz_binary() {
|
|
local bin="$1"
|
|
local name=$(basename "$bin")
|
|
local pkg=$(rpm -qf "$bin" --qf '%{NAME}-%{VERSION}-%{RELEASE}' 2>/dev/null || echo "unknown")
|
|
|
|
# Get command-line flags from --help
|
|
local flags=()
|
|
local tmp=$(mktemp)
|
|
timeout -k 5 3s "$bin" --help 2>&1 | tr -d '\0' | \
|
|
grep -aoE -e '--[a-zA-Z0-9][a-zA-Z0-9_-]*' -e '-[a-zA-Z0-9]' | \
|
|
grep -avE 'help|version|usage' | sort -u | head -n 50 > "$tmp"
|
|
while IFS= read -r f; do flags+=("$f"); done < "$tmp"
|
|
rm -f "$tmp"
|
|
[[ ${#flags[@]} -eq 0 ]] && flags=(-v -a -q -f -d -i -o)
|
|
|
|
# Run multiple test iterations
|
|
for ((r=0; r<RUNS_PER_BIN; r++)); do
|
|
# Pick 1-10 random flags (more options = better crash detection)
|
|
local n=$((1 + RANDOM % 10))
|
|
local chosen=$(printf '%s\n' "${flags[@]}" | shuf -n "$n" | tr '\n' ' ')
|
|
local args="$chosen"
|
|
local use_stdin=0
|
|
|
|
# Add test file if flags suggest it or randomly (33%)
|
|
local needs_file=0
|
|
[[ "$chosen" =~ (--file|--input|--config|--from|-f|-i|-c|--load|--read) ]] && needs_file=1
|
|
|
|
if [[ $needs_file -eq 1 ]] || [[ $((RANDOM % 3)) -eq 0 ]]; then
|
|
local farr=($FUZZ_FILE_NAMES)
|
|
local fidx=$((RANDOM % ${#farr[@]}))
|
|
local ffile="$WORKSPACE/${farr[$fidx]}"
|
|
|
|
if [[ $((RANDOM % 5)) -eq 0 ]]; then
|
|
use_stdin=1
|
|
args+=" -"
|
|
else
|
|
args+=" $ffile"
|
|
fi
|
|
fi
|
|
|
|
local err=$(mktemp)
|
|
local exit_code=0
|
|
|
|
echo "BIN: $bin | ARGS: $args | PKG: $pkg" > "$CURRENT_RUN_FILE"
|
|
|
|
# Run test with safety limits
|
|
(
|
|
ulimit -v 512000 2>/dev/null || true
|
|
ulimit -f 102400 2>/dev/null || true
|
|
export MALLOC_CHECK_=3 LC_ALL=C
|
|
unset DISPLAY WAYLAND_DISPLAY
|
|
|
|
if [[ $use_stdin -eq 1 ]]; then
|
|
local first="$WORKSPACE/$(echo $FUZZ_FILE_NAMES | awk '{print $1}')"
|
|
timeout -k $KILL_AFTER --foreground ${TIMEOUT_SEC}s bash -c \
|
|
"cat $first | $bin $args" >/dev/null 2>"$err"
|
|
else
|
|
timeout -k $KILL_AFTER --foreground ${TIMEOUT_SEC}s \
|
|
"$bin" $args >/dev/null 2>"$err"
|
|
fi
|
|
)
|
|
exit_code=$?
|
|
|
|
# Check for real crashes (exit codes: 139=SIGSEGV, 134=SIGABRT, etc.)
|
|
if [[ $exit_code -eq 139 ]] || [[ $exit_code -eq 134 ]] || [[ $exit_code -eq 136 ]] || \
|
|
[[ $exit_code -eq 132 ]] || [[ $exit_code -eq 133 ]] || [[ $exit_code -eq 135 ]] || \
|
|
grep -qiE 'segmentation fault|segfault|core dumped|double free|heap corruption|buffer overflow|stack smashing|memory corruption|use after free|ASAN|UBSAN' "$err" 2>/dev/null; then
|
|
|
|
# Log crash
|
|
{
|
|
echo "=================================================="
|
|
echo "REAL CRASH DETECTED"
|
|
echo "COMMAND: $bin $args"
|
|
echo "PACKAGE: $pkg"
|
|
echo "EXIT CODE: $exit_code"
|
|
case $exit_code in
|
|
139) echo "SIGNAL: SIGSEGV (Segmentation fault)" ;;
|
|
134) echo "SIGNAL: SIGABRT (Abort)" ;;
|
|
136) echo "SIGNAL: SIGFPE (Floating point exception)" ;;
|
|
132) echo "SIGNAL: SIGILL (Illegal instruction)" ;;
|
|
133) echo "SIGNAL: SIGTRAP (Trace trap)" ;;
|
|
135) echo "SIGNAL: SIGBUS (Bus error)" ;;
|
|
esac
|
|
echo "TIMESTAMP: $(date -Iseconds)"
|
|
echo "--------------------------------------------------"
|
|
echo "ERROR OUTPUT:"
|
|
head -n 30 "$err"
|
|
echo "=================================================="
|
|
echo ""
|
|
}
|
|
fi
|
|
|
|
rm -f "$err"
|
|
done
|
|
}
|
|
|
|
# Run tests in parallel
|
|
echo "Starting fuzzing tests..."
|
|
echo ""
|
|
|
|
running=0
|
|
for bin in "${ALL_BINS[@]}"; do
|
|
while [[ $running -ge $MAX_PARALLEL ]]; do
|
|
wait -n 2>/dev/null || true
|
|
((running--))
|
|
done
|
|
|
|
{ fuzz_binary "$bin" >> "$LOG_FILE" 2>&1; } &
|
|
((running++))
|
|
done
|
|
|
|
wait
|
|
|
|
echo ""
|
|
echo "Fuzzing complete!"
|
|
echo ""
|
|
|
|
# Generate summary
|
|
echo "Generating summary..."
|
|
|
|
crashes=$(grep -c "REAL CRASH DETECTED" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
segfaults=$(grep -c "SIGSEGV" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
aborts=$(grep -c "SIGABRT" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
fpexc=$(grep -c "SIGFPE" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
illins=$(grep -c "SIGILL" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
buserr=$(grep -c "SIGBUS" "$LOG_FILE" 2>/dev/null || echo 0)
|
|
|
|
{
|
|
echo "SEGFAULT-FOCUSED FUZZ TEST SUMMARY - $(date)"
|
|
echo "============================================"
|
|
echo "Programs tested: $TOTAL"
|
|
echo "Runs per program: $RUNS_PER_BIN"
|
|
echo "Total test iterations: $((TOTAL * RUNS_PER_BIN))"
|
|
echo "Test files used: $FUZZ_FILE_COUNT"
|
|
echo ""
|
|
echo "REAL CRASHES FOUND:"
|
|
echo " Total: $crashes"
|
|
echo " Segmentation faults: $segfaults"
|
|
echo " Aborts (SIGABRT): $aborts"
|
|
echo " FP exceptions: $fpexc"
|
|
echo " Illegal instruction: $illins"
|
|
echo " Bus errors: $buserr"
|
|
echo ""
|
|
|
|
if [[ $crashes -gt 0 ]]; then
|
|
echo "AFFECTED PACKAGES:"
|
|
grep "PACKAGE:" "$LOG_FILE" | sed 's/.*PACKAGE: //' | sort | uniq -c | sort -rn
|
|
echo ""
|
|
echo "ALL CRASHES:"
|
|
grep "COMMAND:" "$LOG_FILE"
|
|
echo ""
|
|
fi
|
|
|
|
echo "Full log: $LOG_FILE"
|
|
} > "$SUMMARY_FILE"
|
|
|
|
# Collect coredumps
|
|
if [[ $crashes -gt 0 ]] && command -v coredumpctl &>/dev/null; then
|
|
echo "Collecting coredumps..."
|
|
COREDUMP_DIR="$WORKSPACE/coredumps_$(date +%Y%m%d_%H%M%S)"
|
|
mkdir -p "$COREDUMP_DIR"
|
|
|
|
grep "COMMAND:" "$LOG_FILE" | sed 's/^.*COMMAND: //' | awk '{print $1}' | sort -u | while read -r cbin; do
|
|
[[ -z "$cbin" ]] && continue
|
|
cname=$(basename "$cbin")
|
|
out="$COREDUMP_DIR/coredump_${cname}.dump"
|
|
if coredumpctl dump "$cbin" -o "$out" 2>/dev/null; then
|
|
echo " Saved: $cname"
|
|
fi
|
|
done
|
|
|
|
echo "Coredumps saved to: $COREDUMP_DIR"
|
|
fi
|
|
|
|
# Show results
|
|
echo ""
|
|
echo "========================================"
|
|
cat "$SUMMARY_FILE"
|
|
echo "========================================"
|
|
echo ""
|
|
echo "Done! Check $SUMMARY_FILE for summary."
|