mirror of
https://github.com/google/security-research
synced 2026-06-08 14:27:23 +00:00
707 lines
23 KiB
Bash
Executable File
707 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;
|
|
}
|
|
|
|
check_groups_and_devices() {
|
|
local permission_errors=()
|
|
local group_warnings=()
|
|
|
|
if [ ! -e /dev/kvm ]; then
|
|
permission_errors+=("/dev/kvm does not exist - install kvm kernel module")
|
|
elif ! timeout 1s dd if=/dev/kvm of=/dev/null bs=1 count=0 2>/dev/null; then
|
|
permission_errors+=("/dev/kvm exists but is not accessible")
|
|
fi
|
|
|
|
if [ -e /dev/vhost-net ] && ! timeout 1s dd if=/dev/vhost-net of=/dev/null bs=1 count=0 2>/dev/null; then
|
|
group_warnings+=("/dev/vhost-net exists but is not accessible (optional)")
|
|
fi
|
|
|
|
if [ -e /dev/vhost-vsock ] && ! timeout 1s dd if=/dev/vhost-vsock of=/dev/null bs=1 count=0 2>/dev/null; then
|
|
group_warnings+=("/dev/vhost-vsock exists but is not accessible (optional)")
|
|
fi
|
|
|
|
# Show warnings (non-fatal)
|
|
if [ ${#group_warnings[@]} -gt 0 ]; then
|
|
echo "[WARNING] Optional device access issues:"
|
|
for warning in "${group_warnings[@]}"; do
|
|
echo " - $warning"
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
# If devices are accessible, we're good regardless of group membership display
|
|
if [ ${#permission_errors[@]} -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
|
|
# If devices aren't accessible, check traditional group membership for better error message
|
|
local current_groups=$(groups)
|
|
local missing_groups=()
|
|
|
|
for group in kvm cvdnetwork render; do
|
|
if ! echo "$current_groups" | grep -qw "$group"; then
|
|
missing_groups+=("$group")
|
|
fi
|
|
done
|
|
|
|
# Report the error with helpful context
|
|
echo "=========================================="
|
|
echo "ERROR: Device access failed"
|
|
echo "=========================================="
|
|
echo ""
|
|
for error in "${permission_errors[@]}"; do
|
|
echo " - $error"
|
|
done
|
|
echo ""
|
|
echo "Current groups: $current_groups"
|
|
if [ ${#missing_groups[@]} -gt 0 ]; then
|
|
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 " 3. Ensure kernel modules loaded: sudo modprobe kvm vhost_net vhost_vsock"
|
|
else
|
|
echo ""
|
|
echo "Groups appear correct, but device access still failed."
|
|
echo "Check:"
|
|
echo " 1. Kernel modules: sudo modprobe kvm vhost_net vhost_vsock"
|
|
echo " 2. Device permissions: ls -la /dev/kvm"
|
|
echo " 3. If in container: ensure devices are mounted"
|
|
fi
|
|
echo ""
|
|
|
|
return 1
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
run_preflight_checks() {
|
|
local checks_failed=0
|
|
|
|
check_groups_and_devices || checks_failed=1
|
|
check_kernel_modules || 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[@]}"
|
|
|
|
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
|
|
|
|
if [ -z "$APK_PATH" ]; then
|
|
APK_PATH="$SCRIPT_DIR/android_shellserver/app/build/outputs/apk/release/app-release.apk"
|
|
fi
|
|
|
|
if [ ! -f "$APK_PATH" ]; then
|
|
echo "[ERROR] APK file not found"
|
|
exit 1
|
|
fi
|
|
|
|
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 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] Does not appear to be a valid Cuttlefish release"
|
|
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 [ -d "$folder" ]; then
|
|
# Check if the PID stored in the lock file still exists
|
|
if [ -f "$folder/pid" ]; then
|
|
LOCK_PID=$(cat "$folder/pid")
|
|
# If the process is not running, the lock is stale.
|
|
if ! kill -0 "$LOCK_PID" 2>/dev/null; then
|
|
echo "[INFO] Found stale lock for instance $i (PID $LOCK_PID dead). Cleaning up..."
|
|
rm -rf "$folder"
|
|
fi
|
|
else
|
|
# If there is a folder but no PID file, it's corrupt/stale.
|
|
echo "[INFO] Found empty/corrupt lock for instance $i. Cleaning up..."
|
|
rm -rf "$folder"
|
|
fi
|
|
fi
|
|
|
|
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"
|
|
#LAUNCH_FLAGS="--console=true --resume=false --verbosity=DEBUG --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
|
|
|
|
bash -c "HOME=$RELEASE_PATH $RELEASE_PATH/bin/launch_cvd $LAUNCH_FLAGS" 2>&1 | sed '/^===/,/^===/d; /Using system_image_dir of/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"
|
|
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"
|
|
exit 1
|
|
fi
|
|
|
|
# Verify root access
|
|
echo "[CHECK] Verifying root access..."
|
|
|
|
if ! timeout 10 bash -c "$as_root id" 2>/dev/null | grep -q "uid=0"; then
|
|
echo "[ERROR] Cannot execute commands as root"
|
|
exit 1
|
|
fi
|
|
|
|
echo "[OK] Root access verified"
|
|
|
|
# Setup flag file
|
|
FLAG=$(<$FLAG_FN)
|
|
if $on_guest push $FLAG_FN /data/local/tmp/flag >/dev/null 2>&1; then
|
|
echo "[OK] FLAG file upload to /data/local/tmp/flag is successful"
|
|
else
|
|
echo "[ERROR] Flag file upload to Android VM failed"
|
|
exit 1
|
|
fi
|
|
|
|
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 "[INFO] Testing if port $PORT_TO_USE is listening..."
|
|
if socat /dev/null TCP:127.0.0.1:$PORT_TO_USE,retry=0 2>/dev/null; then
|
|
echo "[INFO] Port $PORT_TO_USE is listening"
|
|
else
|
|
echo "[ERROR] Port $PORT_TO_USE is not listening!"
|
|
exit 1
|
|
fi
|
|
|
|
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
|