Files
google-security-research/kernelctf/server/cuttlefish.sh
T
artmetla 7297a9e637 Submission Verification logic for Android submissions (#289)
1) Disabled AppArmor block for unpriviledged user namespaces. This hapend in ubuntu-latest: https://discourse.ubuntu.com/t/understanding-apparmor-user-namespace-restriction/58007. Added this step to "get_android_dependencies.sh".
2) Using "get_android_dependencies.sh" script to handle dependencies for all execution options: server.py, exploit build, and exploit_reproduction.
3) Optimised exploit reproduction run time, by downloading releases and installing dependencies in parallel. 
4) Conducted clean up to free up a bit of extra space for Cuttlefish runs (cleaning compilation leftovers and APT caches).
5) Using "cuttlefish.sh" to run VM in all execution options: server.py and GHA reproducer. Modified "cuttlefish.sh" to add "--test-mode" which allows easy testing by setting "falg" file permissions equivalent to exploit executable. It's disabled in prod. Improved Android16 detection logic (to enable necessary execution flags for "launch_cvd"), as apparently "launch_cvd --help" could fail sometimes. Improved cleaning logic (important as launch_cvd is super nasty and lefts a lot of tmps and random leftovers which could negatively impact next run of the VM)
6) Apparently in ubuntu-latest "sg -c" requires password. So implemented this logic canonical/data-platform-workflows@349e0c8. This is important as Cuttlefish couldn't run without user being added in groups. By default they suggest reboot to apply those changes, but it's impossible in GHA.
7) "Check PR content" logic is already updated in prod
8) Ensured that in exploit build environment NDK is added to the PATH, so simple Makefiles like:
```
exploit: exploit.c
clang --target=x86_64-linux-android35 -static $^ -o exploit
```
The whole thing was tested with dummy exploit in my fork.
9) Implemented proper timeout check (30 mins) in "cuttlefish.sh"
10) Introduced sparse-checkout for the GHA.
2025-12-04 15:21:19 +01:00

719 lines
23 KiB
Bash
Executable File

#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RELEASE_PATH=""
TEST_MODE=0
CLEANUP_RUNNING=false
cleanup_function() {
# Prevent recursive calls
if [ "$CLEANUP_RUNNING" = true ]; then
return 0
fi
CLEANUP_RUNNING=true
# Disable exit-on-error for cleanup
set +e
echo "[CLEANUP] Shutting down instance $instance_num" 1>&2
# Kill logcat monitoring if running
if [ -n "$LOGCAT_PID" ] && kill -0 "$LOGCAT_PID" 2>/dev/null; then
kill "$LOGCAT_PID" 2>/dev/null || true
sleep 0.2
kill -9 "$LOGCAT_PID" 2>/dev/null || true
fi
# Remove logcat temp file
if [ -n "$LOGCAT_FILE" ] && [ -f "$LOGCAT_FILE" ]; then
rm -f "$LOGCAT_FILE" 2>/dev/null || true
fi
# Stop cuttlefish (protect against failures)
if [ -n "$RELEASE_PATH" ] && [ -d "$RELEASE_PATH" ]; then
CUTTLEFISH_RUNTIME_LINK=$RELEASE_PATH/cuttlefish_runtime
CUTTLEFISH_CURRENT_INSTANCE=$RELEASE_PATH/cuttlefish/instances/cvd-$instance_num
if [ -d "$CUTTLEFISH_CURRENT_INSTANCE" ]; then
# Try graceful stop
(
ln -sf $CUTTLEFISH_CURRENT_INSTANCE $CUTTLEFISH_RUNTIME_LINK 2>/dev/null
HOME=$RELEASE_PATH timeout 10s $RELEASE_PATH/bin/stop_cvd 2>/dev/null
) || true
# Give it time
sleep 1
# Force kill any remaining processes for this instance
pgrep -f "cvd-$instance_num" | xargs -r kill -9 2>/dev/null || true
fi
# Clean up instance-specific data (multiple attempts)
for i in {1..3}; do
rm -rf "$RELEASE_PATH/cuttlefish_runtime.$instance_num" 2>/dev/null && break
sleep 0.5
done
for i in {1..3}; do
rm -rf "$CUTTLEFISH_CURRENT_INSTANCE" 2>/dev/null && break
sleep 0.5
done
# Clean overlays for this instance only
find "$RELEASE_PATH/cuttlefish/instances/cvd-$instance_num" -name "*overlay*.img" -delete 2>/dev/null || true
find "$RELEASE_PATH/cuttlefish/instances/cvd-$instance_num" -name "*composite*.img" -delete 2>/dev/null || true
# Clean up converted raw images if last instance
local active_instances=$(find "$RELEASE_PATH/../locks" -name "lock-inst-*" -type d ! -name "lock-inst-$instance_num" 2>/dev/null | wc -l)
if [ "$active_instances" -eq 0 ]; then
echo "[CLEANUP] Last instance, cleaning up shared .raw images" 1>&2
rm -f "$RELEASE_PATH"/*.img.raw 2>/dev/null || true
fi
# Clean up temp files
rm -rf /tmp/cf_avd_${instance_num}* 2>/dev/null || true
rm -rf /tmp/cf_env_${instance_num}* 2>/dev/null || true
rm -rf /tmp/cvd-${instance_num}* 2>/dev/null || true
rm -rf /tmp/launch_cvd_${instance_num}* 2>/dev/null || true
fi
# Release instance lock (critical - try multiple times)
if [ -n "$folder" ] && [ -d "$folder" ]; then
for i in {1..5}; do
rm -rf "$folder" 2>/dev/null && break
sleep 0.2
done
fi
if [ -n "$instance_num" ]; then
for i in {1..5}; do
rm -rf "$RELEASE_PATH/../locks/lock-inst-$instance_num" 2>/dev/null && break
sleep 0.2
done
fi
# Force sync (ignore errors)
sync 2>/dev/null || true
echo "[CLEANUP] Cleanup done" 1>&2
# Don't call exit here to avoid recursion
return 0
}
cleanup_wrapper() {
cleanup_function 1>&2
}
# Trap multiple signals to ensure cleanup always runs
trap 'cleanup_wrapper' EXIT
trap 'cleanup_wrapper; exit 130' INT # Ctrl+C
trap 'cleanup_wrapper; exit 143' TERM # kill
usage() {
echo "Usage: $0 --release_path=<release_path> --flag_path=<flag_fn> [--bin_path=<bin_path>] [--apk_path=<apk_path>] [--test-mode]"
exit 1;
}
# Function to check required groups
check_groups() {
local current_groups=$(groups)
local missing_groups=()
# Check for required groups
for group in kvm cvdnetwork render; do
if ! echo "$current_groups" | grep -qw "$group"; then
missing_groups+=("$group")
fi
done
if [ ${#missing_groups[@]} -gt 0 ]; then
echo "=========================================="
echo "ERROR: Missing required group memberships"
echo "=========================================="
echo ""
echo "Current groups: $current_groups"
echo "Missing groups: ${missing_groups[*]}"
echo ""
echo "To fix this issue:"
echo " 1. Run: sudo usermod -aG kvm,cvdnetwork,render \$USER"
echo " 2. Logout and login again (or reboot)"
echo ""
echo "Alternatively, if you just ran install_dependencies.sh,"
echo "run this script with: sudo su - \$USER -c \"cd \$PWD && ./cuttlefish.sh ...\""
echo ""
return 1
fi
return 0
}
# Function to check kernel modules
check_kernel_modules() {
local missing_modules=()
local optional_modules=()
# Check for required modules
if ! lsmod | grep -q "^kvm"; then
missing_modules+=("kvm")
fi
# Check for optional but recommended modules
if ! lsmod | grep -q "^vhost_net"; then
optional_modules+=("vhost_net")
fi
if ! lsmod | grep -q "^vhost_vsock"; then
optional_modules+=("vhost_vsock")
fi
if [ ${#missing_modules[@]} -gt 0 ]; then
echo "=========================================="
echo "ERROR: Missing required kernel modules"
echo "=========================================="
echo ""
echo "Missing modules: ${missing_modules[*]}"
echo ""
echo "To fix this issue:"
echo " sudo modprobe kvm"
echo " sudo modprobe kvm_intel # or kvm_amd for AMD CPUs"
echo ""
return 1
fi
if [ ${#optional_modules[@]} -gt 0 ]; then
echo "[WARNING] Optional modules not loaded: ${optional_modules[*]}"
echo " Cuttlefish may work but with reduced performance."
echo " To load them: sudo modprobe vhost_net vhost_vsock"
echo ""
fi
return 0
}
# Function to check device permissions
check_device_permissions() {
local permission_errors=()
# Check /dev/kvm
if [ ! -e /dev/kvm ]; then
permission_errors+=("/dev/kvm does not exist")
elif [ ! -r /dev/kvm ] || [ ! -w /dev/kvm ]; then
permission_errors+=("/dev/kvm is not readable/writable")
fi
# Check /dev/net/tun
if [ ! -e /dev/net/tun ]; then
permission_errors+=("/dev/net/tun does not exist")
elif [ ! -r /dev/net/tun ] || [ ! -w /dev/net/tun ]; then
permission_errors+=("/dev/net/tun is not readable/writable")
fi
if [ ${#permission_errors[@]} -gt 0 ]; then
echo "=========================================="
echo "ERROR: Device permission issues"
echo "=========================================="
echo ""
for error in "${permission_errors[@]}"; do
echo " - $error"
done
echo ""
echo "Current permissions:"
[ -e /dev/kvm ] && ls -la /dev/kvm || echo " /dev/kvm: Not found"
[ -e /dev/net/tun ] && ls -la /dev/net/tun || echo " /dev/net/tun: Not found"
echo ""
echo "These issues are usually resolved by:"
echo " 1. Being in the kvm and cvdnetwork groups"
echo " 2. Logging out and back in"
echo " 3. Running: sudo udevadm control --reload-rules && sudo udevadm trigger"
echo ""
return 1
fi
return 0
}
# Function to run all pre-flight checks
run_preflight_checks() {
local checks_failed=0
# Run all checks silently, only show errors
check_groups || checks_failed=1
check_kernel_modules || checks_failed=1
check_device_permissions || checks_failed=1
if [ $checks_failed -ne 0 ]; then
echo "=========================================="
echo "[FAILED] Pre-flight checks FAILED"
echo "=========================================="
echo ""
echo "Please fix the issues above before running Cuttlefish."
echo ""
exit 1
fi
return 0
}
ARGS=()
while [[ $# -gt 0 ]]; do
case $1 in
--release_path=*) RELEASE_PATH="${1#*=}"; shift;;
--bin_path=*) BIN_PATH="${1#*=}"; shift;;
--flag_path=*) FLAG_FN="${1#*=}"; shift;;
--apk_path=*) APK_PATH="${1#*=}"; shift;;
--test-mode) TEST_MODE=1; shift;;
--skip-checks) SKIP_CHECKS=1; shift;;
--) # stop processing special arguments after "--"
shift
while [[ $# -gt 0 ]]; do ARGS+=("$1"); shift; done
break
;;
-*|--*) echo "[ERROR] Unknown option $1"; usage;;
*) ARGS+=("$1"); shift;;
esac
done
set -- "${ARGS[@]}"
# Validate required parameters
if [ -z "$RELEASE_PATH" ]; then
echo "[ERROR] --release_path is required"
usage
fi
if [ -z "$FLAG_FN" ]; then
echo "[ERROR] --flag_path is required"
usage
fi
# Set default APK path if not provided
if [ -z "$APK_PATH" ]; then
APK_PATH="$SCRIPT_DIR/android_shellserver/app/build/outputs/apk/release/app-release.apk"
fi
# Validate that APK file exists
if [ ! -f "$APK_PATH" ]; then
echo "[ERROR] APK file not found at $APK_PATH"
exit 1
fi
echo "[OK] APK file found: $APK_PATH"
if [ "$TEST_MODE" -eq 1 ]; then
echo "[TEST MODE] Running in test mode - flag will be readable by exploit user"
fi
# Run pre-flight checks (unless --skip-checks is specified)
if [ -z "$SKIP_CHECKS" ]; then
run_preflight_checks
else
echo "[WARNING] Skipping pre-flight checks (--skip-checks specified)"
echo ""
fi
# Validate that RELEASE_PATH exists and is a directory
if [ ! -d "$RELEASE_PATH" ]; then
echo "[ERROR] Release path '$RELEASE_PATH' does not exist or is not a directory"
exit 1
fi
# Check that RELEASE_PATH contains required Cuttlefish files
if [ ! -f "$RELEASE_PATH/bin/launch_cvd" ]; then
echo "[ERROR] '$RELEASE_PATH' does not appear to be a valid Cuttlefish release"
echo "[ERROR] Missing: $RELEASE_PATH/bin/launch_cvd"
exit 1
fi
# Convert RELEASE_PATH to absolute path
RELEASE_PATH=$(cd "$RELEASE_PATH" && pwd)
tmp="$RELEASE_PATH"
while [[ "$tmp" == *\"* ]]; do
tmp="${tmp//\"}"
done
RELEASE_PATH="$tmp"
if [ ! -d "$RELEASE_PATH/../locks" ]; then
mkdir -p "$RELEASE_PATH/../locks"
fi
# Check for the first free instance
for i in $(seq 1 32); do
folder="$RELEASE_PATH/../locks/lock-inst-${i}"
if mkdir "$folder" 2>/dev/null; then
instance_num=$i
# Record ownership
echo $$ > "$folder/pid"
echo "$(date +%s)" > "$folder/timestamp"
echo "[OK] Acquired instance slot $instance_num"
break
fi
done
if [ -z "$instance_num" ]; then
echo "[ERROR] All instances are busy, exiting..."
exit 1
fi
# Calculate the ADB port for this instance
ADB_PORT=$((6520 + instance_num - 1))
# Create and boot virtual device with android kernel at RELEASE_PATH.
# The path to launch_cvd needs to be 108 characters or less
echo "[STARTING] Starting Cuttlefish instance..."
# Build base launch flags
LAUNCH_FLAGS="--daemon --console=true --resume=false --verbosity=ERROR --system_image_dir=\"$RELEASE_PATH\" --base_instance_num=$instance_num -report_anonymous_usage_stats=n"
# Auto-detect if we need --enable_tap_devices=false (Android 16+)
# Check kernel version string in boot.img
if [ -f "$RELEASE_PATH/boot.img" ]; then
# Extract kernel version string and check for android16 or kernel 6.12+
kernel_version=$(strings "$RELEASE_PATH/boot.img" 2>/dev/null | grep -m1 "android" | head -1)
if echo "$kernel_version" | grep -q "android16\|android1[7-9]\|android[2-9]"; then
LAUNCH_FLAGS="$LAUNCH_FLAGS --enable_tap_devices=false"
elif echo "$kernel_version" | grep -Eq "^6\.1[2-9]\.|^6\.[2-9][0-9]\.|^[7-9]\."; then
LAUNCH_FLAGS="$LAUNCH_FLAGS --enable_tap_devices=false"
fi
fi
echo "[DEBUG] Launching with flags: $LAUNCH_FLAGS"
bash -c "HOME=$RELEASE_PATH $RELEASE_PATH/bin/launch_cvd $LAUNCH_FLAGS" 2>&1 | sed '/^===/,/^===/d'
LAUNCH_EXIT=${PIPESTATUS[0]}
if [ $LAUNCH_EXIT -ne 0 ]; then
echo "[ERROR] Failed to launch Cuttlefish instance (exit code: $LAUNCH_EXIT)"
exit 1
fi
# Wait for the instance to fully start and become ready
echo -n "[WAITING] Waiting for instance to start"
max_wait=120
waited=0
while [ $waited -lt $max_wait ]; do
if [ -f "$RELEASE_PATH/cuttlefish_runtime.$instance_num/cuttlefish_config.json" ]; then
echo " done"
echo "[OK] Instance started successfully"
break
fi
echo -n "."
sleep 1
waited=$((waited + 1))
done
if [ $waited -ge $max_wait ]; then
echo " timeout"
echo "[ERROR] Instance failed to start within ${max_wait} seconds"
exit 1
fi
# Wait for ADB to connect and device to be ready
# FIX: Accept both 0.0.0.0 and 127.0.0.1 formats since Cuttlefish may report either
echo -n "[WAITING] Waiting for ADB to connect"
timeout=60
elapsed=0
device_found=0
while [ $elapsed -lt $timeout ]; do
# Check for device on the expected port (works with both 0.0.0.0 and 127.0.0.1)
if $RELEASE_PATH/bin/adb devices 2>/dev/null | grep -E "(0\.0\.0\.0|127\.0\.0\.1):${ADB_PORT}[[:space:]]+device" > /dev/null; then
device_found=1
echo " done"
# Detect which format the device is using
if $RELEASE_PATH/bin/adb devices 2>/dev/null | grep -q "0\.0\.0\.0:${ADB_PORT}"; then
DEVICE_ADDRESS="0.0.0.0:${ADB_PORT}"
echo "[INFO] Device connected as 0.0.0.0:${ADB_PORT}"
else
DEVICE_ADDRESS="127.0.0.1:${ADB_PORT}"
echo "[INFO] Device connected as 127.0.0.1:${ADB_PORT}"
fi
echo "[OK] ADB connected"
break
fi
echo -n "."
sleep 2
elapsed=$((elapsed + 2))
done
if [ $device_found -eq 0 ]; then
echo " timeout"
echo "[ERROR] ADB connection timeout after ${timeout}s"
echo "[DEBUG] Current ADB devices:"
$RELEASE_PATH/bin/adb devices
exit 1
fi
# Define helper functions for ADB operations using the detected device address
as_root="$RELEASE_PATH/bin/adb -s $DEVICE_ADDRESS shell su root"
on_guest="$RELEASE_PATH/bin/adb -s $DEVICE_ADDRESS"
# Additional wait for device to be fully ready
echo -n "[WAITING] Waiting for device to be fully booted"
$on_guest wait-for-device
echo " done"
sleep 5
if [ ! -f "$FLAG_FN" ]; then
echo "[ERROR] Flag file not found at $FLAG_FN"
exit 1
fi
# Verify root access
echo "[CHECK] Verifying root access..."
# FIX: Use proper timeout and error handling for root check
if ! timeout 10 bash -c "$as_root id" 2>/dev/null | grep -q "uid=0"; then
echo "[ERROR] Cannot execute commands as root"
echo "[DEBUG] Attempting to diagnose root access issue..."
# Try to get more information
echo "[DEBUG] Testing basic shell access:"
$on_guest shell id 2>&1 | head -3
echo "[DEBUG] Testing su availability:"
$on_guest shell "which su" 2>&1
echo "[DEBUG] Build type:"
$on_guest shell getprop ro.build.type 2>&1
echo "[DEBUG] Debuggable:"
$on_guest shell getprop ro.debuggable 2>&1
exit 1
fi
echo "[OK] Root access verified"
# Setup flag file
FLAG=$(<$FLAG_FN)
$on_guest push $FLAG_FN /data/local/tmp/flag
if [ "$TEST_MODE" -eq 1 ]; then
# Test mode: make flag readable by the exploit user
$as_root "chmod 0644 /data/local/tmp/flag"
$as_root "chown 10108:10108 /data/local/tmp/flag"
echo "[TEST MODE] Flag is readable by exploit user"
else
# Production mode: flag requires root
$as_root "chmod 0000 /data/local/tmp/flag"
$as_root "chown root:root /data/local/tmp/flag"
fi
PORT_TO_USE=$(expr $instance_num + 7000)
echo "[CHECK] Checking if port $PORT_TO_USE is available..."
if lsof -Pi :$PORT_TO_USE -sTCP:LISTEN -t >/dev/null 2>&1; then
echo "[ERROR] Port $PORT_TO_USE is already in use"
exit 1
fi
# Install APK
echo "[INSTALLING] Installing APK..."
if ! $on_guest install -g $APK_PATH 2>&1 | tee /tmp/apk_install_$instance_num.log; then
echo "[ERROR] APK installation failed"
cat /tmp/apk_install_$instance_num.log
exit 1
fi
# Verify the package is installed
echo "[VERIFYING] Checking package installation..."
if ! $on_guest shell pm list packages 2>/dev/null | grep -q "com.google.android.kernelctf.shellserver"; then
echo "[ERROR] APK package not found after installation"
exit 1
fi
echo "[OK] APK installed and verified successfully"
rm -f /tmp/apk_install_$instance_num.log
# Determine which binary path to pass to MainActivity
BINARY_PATH=""
if [ -f "$BIN_PATH" ]; then
BIN_NAME=$(basename $BIN_PATH)
BINARY_PATH="--es binary_path /data/local/tmp/$BIN_NAME"
$on_guest push $BIN_PATH /data/local/tmp
$as_root "chmod +x /data/local/tmp/$BIN_NAME"
$as_root "chown 10108:10108 /data/local/tmp/$BIN_NAME"
$as_root "chcon u:object_r:apk_data_file:s0 /data/local/tmp/$BIN_NAME"
fi
$as_root "am start -n com.google.android.kernelctf.shellserver/.MainActivity --es server_port $PORT_TO_USE $BINARY_PATH"
$on_guest forward tcp:$PORT_TO_USE tcp:$PORT_TO_USE
# Start single logcat monitor for both startup detection and crash monitoring
LOGCAT_FILE=$(mktemp)
($on_guest logcat 2>/dev/null > "$LOGCAT_FILE") &
LOGCAT_PID=$!
# Wait for android device to be ready by monitoring logcat
echo -n "[WAITING] Waiting for VM to be ready"
READY_TIMEOUT=30
READY_ELAPSED=0
VM_READY=0
while [ $READY_ELAPSED -lt $READY_TIMEOUT ]; do
if grep -q "kernelCTF_READY" "$LOGCAT_FILE" 2>/dev/null; then
VM_READY=1
echo " done"
echo "[OK] VM ready for connection"
break
fi
echo -n "."
sleep 1
READY_ELAPSED=$((READY_ELAPSED + 1))
done
if [ $VM_READY -eq 0 ]; then
echo " timeout"
echo "[ERROR] VM setup failed - kernelCTF_READY not detected within ${READY_TIMEOUT}s"
exit 1
fi
# After "VM ready for connection" and before spawning shell
echo "[DEBUG] Testing if port $PORT_TO_USE is listening..."
if nc -z 127.0.0.1 $PORT_TO_USE 2>/dev/null; then
echo "[DEBUG] Port $PORT_TO_USE is listening"
else
echo "[ERROR] Port $PORT_TO_USE is not listening!"
exit 1
fi
echo "[DEBUG] Checking what's listening on port $PORT_TO_USE..."
lsof -i :$PORT_TO_USE 2>/dev/null || echo "[DEBUG] lsof found nothing"
echo "[INFO] Connecting to exploit"
if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; then
echo "[DEBUG] CI mode: 30 min hard timeout, 60s no-output timeout, flag detection enabled"
fi
set +e
# In CI environments, we need to handle non-interactive connections
if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ]; then
echo "[DEBUG] Connecting to port $PORT_TO_USE in CI mode..."
# Create temporary file for output monitoring (CI only)
OUTPUT_FILE=$(mktemp)
CRASH_DETECTED=0
# Run socat with 30-minute timeout and capture output
timeout 1800s socat -u tcp:127.0.0.1:$PORT_TO_USE - 2>&1 | tee "$OUTPUT_FILE" &
SOCAT_PID=$!
# Monitor for flag in real-time (background process)
(
# Read the flag we're looking for
FLAG_CONTENT=$(cat "$FLAG_FN" 2>/dev/null || echo "")
if [ -z "$FLAG_CONTENT" ]; then
echo "[WARNING] Could not read flag file, won't detect early completion" 1>&2
exit 0
fi
# Watch the output file for the flag
tail -f "$OUTPUT_FILE" 2>/dev/null | while read -r line; do
if echo "$line" | grep -q "$FLAG_CONTENT"; then
echo "[SUCCESS] Flag detected! Exploit completed successfully." 1>&2
# Kill the socat process to exit early
kill $SOCAT_PID 2>/dev/null || true
break
fi
done
) &
MONITOR_PID=$!
# Activity monitor - kill if no output for 60 seconds (background process)
(
NO_OUTPUT_TIMEOUT=60
LAST_SIZE=0
STALE_COUNT=0
# Wait for connection to establish
sleep 5
while kill -0 $SOCAT_PID 2>/dev/null; do
if [ -f "$OUTPUT_FILE" ]; then
CURRENT_SIZE=$(stat -f%z "$OUTPUT_FILE" 2>/dev/null || stat -c%s "$OUTPUT_FILE" 2>/dev/null || echo "0")
if [ "$CURRENT_SIZE" -eq "$LAST_SIZE" ]; then
STALE_COUNT=$((STALE_COUNT + 1))
if [ $STALE_COUNT -ge 60 ]; then
echo "[TIMEOUT] No output for ${NO_OUTPUT_TIMEOUT}s, killing exploit" 1>&2
kill $SOCAT_PID 2>/dev/null || true
break
fi
else
STALE_COUNT=0
LAST_SIZE=$CURRENT_SIZE
fi
fi
sleep 1
done
) &
ACTIVITY_MONITOR_PID=$!
# Wait for socat to complete (either naturally, timeout, killed by monitor, or crashed)
wait $SOCAT_PID
socat_exit=$?
# Clean up monitor processes
kill $MONITOR_PID 2>/dev/null || true
wait $MONITOR_PID 2>/dev/null || true
kill $ACTIVITY_MONITOR_PID 2>/dev/null || true
wait $ACTIVITY_MONITOR_PID 2>/dev/null || true
# Analyze exit code
echo "[DEBUG] socat exited with code: $socat_exit"
# Check if VM died (connection lost)
if [ $socat_exit -ne 0 ] && [ $socat_exit -ne 124 ] && [ $socat_exit -ne 143 ] && [ $socat_exit -ne 137 ]; then
if ! timeout 5s $on_guest shell "echo test" >/dev/null 2>&1; then
echo "[CRASH] VM appears to be unresponsive or crashed"
CRASH_DETECTED=1
fi
fi
# Save output for inspection
if [ -f "$OUTPUT_FILE" ]; then
echo "[DEBUG] Last 50 lines of exploit output:" 1>&2
tail -50 "$OUTPUT_FILE" 1>&2
fi
# Clean up logcat monitoring
if [ -n "$LOGCAT_PID" ]; then
kill $LOGCAT_PID 2>/dev/null || true
wait $LOGCAT_PID 2>/dev/null || true
fi
rm -f "$LOGCAT_FILE"
# Clean up output file
rm -f "$OUTPUT_FILE"
else
# Interactive mode for local testing
# Researcher can see crash output directly in terminal
# No timeout - researcher controls with Ctrl+C
echo "[DEBUG] Connecting to port $PORT_TO_USE in interactive mode (Ctrl+C to exit)..."
socat - tcp:127.0.0.1:$PORT_TO_USE
socat_exit=$?
# Clean up logcat monitor
if [ -n "$LOGCAT_PID" ]; then
kill $LOGCAT_PID 2>/dev/null || true
wait $LOGCAT_PID 2>/dev/null || true
fi
rm -f "$LOGCAT_FILE"
fi
set -e
if [ $socat_exit -eq 124 ]; then
echo "[INFO] Connection timeout after 30 minutes (hard limit)" 1>&2
elif [ $socat_exit -eq 143 ] || [ $socat_exit -eq 137 ]; then
echo "[INFO] Connection terminated (likely flag detected or activity timeout)" 1>&2
elif [ "${CRASH_DETECTED:-0}" -eq 1 ]; then
echo "[INFO] Exploit execution ended - crash detected (post-mortem analysis)" 1>&2
elif [ $socat_exit -ne 0 ]; then
echo "[INFO] Connection closed with exit code: $socat_exit" 1>&2
else
echo "[INFO] Connection closed cleanly" 1>&2
fi