top of page

Aerial Warflying (Guide)

Aerial Wardriving (Guide) | Black Hat HQ

How To Aerial Wardrive/Warfly


Aerial wardriving gives you a 3D signal propagation map that ground surveys can't produce. WiFi signals bleed upward through windows, roof vents, and atriums. Rooftop APs and point-to-point bridges are invisible from the parking lot but wide open from 100 meters up. This is advanced physical-layer recon and it's incredibly effective when authorized. This is a guide on how to aerial warflying / wardriving works.


The Regulatory Hard Constraints


Before touching hardware, know your ceiling. Literally.


Jurisdiction

Max Altitude

Line of Sight

Weight Limit

Key Rule

FAA (US, Part 107)

400 ft AGL

Required

55 lbs

Remote ID mandatory. No night ops without waiver.

EASA (EU)

120m AGL

Required

Varies by class

Open category. C1/C2 classification needed.

CASA (Australia)

120m AGL

Required

2-25 kg

ReOC for commercial ops.

UK CAA

120m AGL

Required

250g-25kg

Operator ID + Flyer ID required.


What this means operationally:


  • 400 ft (120m) is your altitude ceiling without a special waiver. That's plenty — WiFi reaches well above 400 ft.

  • You must maintain visual line of sight with the drone. No FPV-only flights. This limits practical survey radius to roughly 500-800 meters.

  • Remote ID (US) broadcasts your position, altitude, and control station location. This is visible to anyone with a receiver app. For a pentest, factor this into OPSEC — your client should notify local law enforcement or aviation authorities if the drone flight might attract attention near a sensitive facility.

  • Flying over people not involved in the operation is generally prohibited unless they're under covered structure or you have a specific waiver.

  • Controlled airspace (near airports) requires LAANC authorization in the US, which is instant for most altitudes but caps at specific ceilings.


Pentest-specific: Your authorization letter should explicitly mention "aerial wireless survey via UAV" as an authorized activity. A standard internal pentest authorization does not automatically cover operating an aircraft over the facility. Get it in writing.


Hardware: Drone + Payload


Drone Selection


Drone

Flight Time

Payload

GPS Accuracy

Best For

DJI Mavic 3 Enterprise

45 min

~200g external

RTK-capable

Professional, long surveys

DJI Mini 4 Pro

34 min

~50g external (risky)

Standard

Lightweight payload only

DJI Matrice 350

55 min

2.7 kg

RTK standard

Heavy payload, dual operator

Autel EVO Max

42 min

~300g

RTK option

Alternative to DJI ecosystem

DIY F450/F550 Hex

15-25 min

500g-2kg

Custom (Pixhawk)

Full control, no geo-fencing


DJI Mavic 3 Enterprise is the sweet spot. 45 minutes of flight covers a medium campus. The RTK module gives centimeter-level GPS, which means precise AP geolocation. The mechanical shutter is irrelevant for WiFi but the flight time and payload capacity matter.


Critical on DJI: Geo-fencing. DJI drones will refuse to fly in restricted zones. If the client is near an airport, prison, or government facility, test this in advance. DJI's unlock process requires submitting documentation and can take 24-48 hours for custom zones.


Payload: Raspberry Pi + WiFi + GPS + Battery


You need an independent capture rig that flies on the drone and logs GPS-tagged WiFi data. The drone's own telemetry is on different frequencies (2.4 GHz and 5.8 GHz for control/video) and won't contaminate the survey.


Components:


Raspberry Pi Zero 2 W — $15
  Lightest full-Linux SBC. 512MB RAM is enough for headless Kismet.

Alfa AWUS036ACM or Panda PAU0D — $25-40
  USB WiFi adapter with monitor mode.
  The AWUS036ACM (MT7612U) supports both 2.4 and 5 GHz.
  Remove the plastic case to save weight.

GPS module (NEO-M8N or similar) — $15-30
  Dedicated GPS for the payload, independent of drone's GPS.
  USB GPS puck or UART GPS module on Pi GPIO.

18650 battery + 5V regulator — $15
  Or a small USB power bank (1000-2000 mAh).
  The Pi Zero + WiFi adapter draws ~500mA at 5V.
  A 2000mAh USB bank runs the payload for ~4 hours.
  The drone's own battery can't power the payload (voltage mismatch,
  and tapping into drone power can cause flight controller issues).

3D-printed mount or velcro straps — $5
  Secure payload under the drone.
  Keep the WiFi antenna vertical and away from the drone body.
  Carbon fiber frames block WiFi. Plastic/magnesium frames less so.

Total payload weight: ~120-180g. Well within the DJI Mavic 3's 200g external payload capacity (though DJI officially recommends against third-party payloads — weight is the concern, not policy).


Wiring Diagram (Simplified)


[USB Battery Pack 5V]
    │
    ├──→ [Raspberry Pi Zero 2W] ← micro USB power
    │         │
    │         └──→ [Alfa AWUS036ACM] ← USB OTG adapter
    │
    └──→ [USB GPS Puck]
              │
              └──→ [Pi USB port #2]

Antenna Positioning


WiFi antennas are omnidirectional in the horizontal plane. On a drone, the ideal position is:


  • Vertical orientation: hanging below the drone, clear of the landing gear

  • Distance from drone body: at least 15 cm separation from motors, ESCs, and carbon fiber

  • Below the battery: the battery is dense metal — it's a signal blocker

  • No carbon fiber in the radiation pattern: carbon fiber is electrically conductive and absorbs RF


A 15cm SMA extension cable lets you position the antenna away from the noise while keeping the heavy Pi + battery centered:


[Drone body]
    │
    └── Velcro strap
         │
    [Payload tray: Pi + Battery + WiFi adapter]
         │
         └── 15cm SMA extension
              │
              └── [Antenna hanging vertically in clear air]

Payload Software: Headless Kismet


Kismet runs on the Pi Zero 2W without a GUI. It logs to SQLite and can sync to a ground station over WiFi.


Pi Zero Setup


bash

# Flash Raspberry Pi OS Lite (no desktop) to SD card
# Boot, connect via serial or pre-configure WiFi

# Update
sudo apt update && sudo apt upgrade -y

# Install dependencies
sudo apt install -y gpsd gpsd-clients kismet

# Add pi user to kismet group
sudo usermod -aG kismet pi

# Configure Kismet for headless capture
sudo nano /etc/kismet/kismet.conf

Minimal kismet.conf for aerial survey:


bash

# Kismet configuration for drone payload
# Only what's needed — stripped for performance on Pi Zero

# WiFi source
source=wlan1:name=drone_wifi,hop=true,channel_hop=true,channel_hoprate=3/sec

# GPS
gps=gpsd:host=localhost,port=2947

# Logging
log_prefix=/home/pi/wardrive/%d_%B_%Y_%H_%M
log_types=kismet

# Disable everything we don't need
# No web server (saves CPU/RAM)
# No Bluetooth scanning
# No alerting

GPS configuration:


bash

# gpsd should auto-detect the USB GPS puck
# Verify:
ls /dev/ttyUSB*
# Should show /dev/ttyUSB0 or /dev/ttyACM0

sudo systemctl enable gpsd
sudo nano /etc/default/gpsd

bash

# /etc/default/gpsd
START_DAEMON="true"
USBAUTO="true"
DEVICES="/dev/ttyUSB0"
GPSD_OPTIONS="-n"

Auto-Start on Power-Up


The payload should start capturing the moment it gets power — no SSH, no manual commands:


bash

# Create systemd service for Kismet
sudo nano /etc/systemd/system/kismet-drone.service

ini

[Unit]
Description=Kismet Drone Wardriving
After=network.target gpsd.service
Wants=gpsd.service

[Service]
Type=simple
User=pi
ExecStart=/usr/bin/kismet -c wlan1 --no-ncurses
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

bash

sudo systemctl enable kismet-drone.service
sudo systemctl start kismet-drone.service

# Verify it's running
sudo systemctl status kismet-drone.service
# Should show active (running)

# Check logs
tail -f /var/log/syslog | grep -i kismet

Ground Station Sync (Optional)


If you want live data on the ground while flying:


Option A: Kismet remote capture


Run Kismet on the ground laptop in remote-capture mode. The drone Pi sends packets over WiFi. The ground laptop does the heavy processing.


bash

# On drone Pi:
kismet_cap_linux_wifi --connect tcp://GROUND_STATION_IP:3501 --source wlan1

# On ground laptop:
kismet -c remote_capture:host=0.0.0.0,port=3501

This requires a WiFi link between drone and ground. DJI's OcuSync/transmission system uses 2.4/5.8 GHz — same bands you're surveying. The drone's telemetry may interfere with survey WiFi. Test this in advance.


Option B: Offline capture (recommended)


Capture everything to SD card on the drone. Download after landing. No interference, no link dependency, no extra weight.


bash

# After landing, pull the SD card or SCP the files:
scp pi@drone-payload.local:/home/pi/wardrive/*.kismet ./flight_data/

Flight Planning


Survey Patterns


Grid Pattern (Full Coverage):


     ┌─────────────────────┐
     │  → → → → → → → → → │
     │  ← ← ← ← ← ← ← ← ← │
     │  → → → → → → → → → │
     │  ← ← ← ← ← ← ← ← ← │
     └─────────────────────┘
      Building / Campus Area

  • Altitude: Survey at 3-4 altitudes (50m, 100m, 150m, 200m AGL)

  • Spacing: 30-50 meter line spacing (WiFi beam width is wide enough that tighter spacing adds little value)

  • Speed: 5-8 m/s (fast enough to cover area, slow enough for Kismet to capture multiple beacons per AP)

  • Each altitude pass: 15-30 minutes depending on campus size


Building Envelope Spiral:


     ┌─────────────────┐
     │  ╭──────────╮   │
     │  │ Building │   │
     │  ╰──────────╯   │
     └─────────────────┘
     Drone spirals outward from building center

  • Best for mapping signal leakage from a single building

  • Start at roof level + 10m, spiral outward in 10m altitude increments

  • Measures exactly where signal drops below usable threshold


Pre-Flight Checklist


bash

 Authorization letter explicitly includes UAV operations
 NOTAM filed (if required — US: not usually for Part 107 under 400ft)
 LAANC authorization obtained (if in controlled airspace)
 Airspace check: B4UFLY, AirMap, or DJI Fly app
 Weather: wind < 15 kts, no precipitation, visibility > 5km
 Drone batteries charged (3-4 packs for extended survey)
 Payload battery charged
 SD card formatted, enough space (>2 GB free)
 Kismet auto-starts on payload power-up (verified on ground)
 GPS fix obtained (both drone AND payload GPS)
 No people under flight path (clear area or schedule off-hours)
 Client security notified (don't want guards calling police on a drone)
☐ Spare SD cards in case of corruption
☐ Remote ID broadcasting (required in US — verify in DJI app)

Flight Execution


bash

# 1. Power on payload (battery plugged into Pi)
#    Wait 60 seconds for boot + Kismet start + GPS fix
#    Verify: small LED on GPS puck blinking = fix acquired

# 2. Power on drone, verify GPS, home point set

# 3. Take off, ascend to first survey altitude
#    Hover for 30 seconds at altitude (GPS averaging)
#    Verify stable hover, no vibration issues with payload

# 4. Execute survey pattern
#    Maintain constant speed (automated waypoint mission)
#    DJI Pilot 2 / UgCS / Litchi for automated grid missions
#    Litchi is particularly good for custom survey patterns
#    with programmable altitude changes

# 5. Land, swap battery, reload for next altitude

# 6. After all altitudes: hover at cardinal points
#    North, South, East, West of building at multiple altitudes
#    These are calibration points for signal triangulation

Data Processing


After landing, you have .kismet files from each flight. These are SQLite databases. Process them the same as ground wardriving data, but now you have altitude (Z-axis) for every AP.


Extract and Convert


bash

# Combine all flight databases
kismetdb_merge --in flight1.kismet --in flight2.kismet --in flight3.kismet \
  --out combined_aerial.kismet

# Export to CSV with altitude
kismetdb_dump_devices --in combined_aerial.kismet --out aerial_aps.csv

# The CSV contains: BSSID, SSID, signal (min/max/avg), locations,
# encryption, channels, first/last seen times
# Each AP may have multiple location entries (from different altitudes)

3D Visualization


This is where aerial wardriving shines. You get signal strength at multiple altitudes, creating a 3D heatmap.


python

#!/usr/bin/env python3
"""
3D signal propagation visualizer from aerial wardrive data.
Generates an interactive 3D plot of APs and signal strength.
"""

import csv
import json
import numpy as np
from collections import defaultdict

def parse_aerial_csv(csv_file):
    """Parse Kismet CSV, group by AP, collect 3D positions"""
    aps = defaultdict(list)
    
    with open(csv_file) as f:
        reader = csv.DictReader(f)
        for row in reader:
            bssid = row.get('devmac', '')
            ssid = row.get('ssid', '')
            signal = row.get('signal', '')
            lat = row.get('lat', '')
            lon = row.get('lon', '')
            alt = row.get('alt', '')  # Altitude in meters
            
            if not bssid:
                continue
            
            try:
                signal = float(signal) if signal else None
                lat = float(lat) if lat else None
                lon = float(lon) if lon else None
                alt = float(alt) if alt else None
            except (ValueError, TypeError):
                continue
            
            if all(v is not None for v in [signal, lat, lon, alt]):
                aps[bssid].append({
                    'ssid': ssid,
                    'lat': lat,
                    'lon': lon,
                    'alt': alt,
                    'signal': signal
                })
    
    return aps

def generate_signal_cone(ap_data):
    """For each AP, estimate ground-level signal propagation"""
    results = []
    
    for bssid, readings in ap_data.items():
        if len(readings) < 3:
            continue
        
        ssid = readings[0]['ssid']
        
        # Find strongest reading (closest to AP)
        strongest = max(readings, key=lambda r: r['signal'])
        
        # Estimate AP position: highest signal (closest proximity) +
        # signal falloff rate
        signals = np.array([r['signal'] for r in readings])
        lats = np.array([r['lat'] for r in readings])
        lons = np.array([r['lon'] for r in readings])
        alts = np.array([r['alt'] for r in readings])
        
        # Simple estimator: strongest signal point weighted average
        weights = np.exp((signals - signals.max()) / 6)  # 6 dB decay
        
        est_lat = np.average(lats, weights=weights)
        est_lon = np.average(lons, weights=weights)
        
        # Altitude: adjust for building height
        est_alt = alts[np.argmax(signals)]  # Altitude of strongest reading
        
        # Max distance where signal was still detected
        max_signal_reading = readings[np.argmax([
            np.sqrt((r['lat'] - est_lat)**2 + (r['lon'] - est_lon)**2)
            for r in readings
        ])]
        
        results.append({
            'bssid': bssid,
            'ssid': ssid,
            'est_lat': est_lat,
            'est_lon': est_lon,
            'est_alt': est_alt,
            'max_signal': strongest['signal'],
            'max_distance': np.sqrt(
                (max_signal_reading['lat'] - est_lat)**2 +
                (max_signal_reading['lon'] - est_lon)**2
            ) * 111000,  # Rough meters from degrees
            'num_readings': len(readings)
        })
    
    return results

def export_cesium_json(results, output_file="aerial_3d.json"):
    """Export to Cesium/CesiumJS format for 3D globe visualization"""
    features = []
    
    for ap in results:
        features.append({
            "type": "Feature",
            "geometry": {
                "type": "Point",
                "coordinates": [ap['est_lon'], ap['est_lat'], ap['est_alt']]
            },
            "properties": {
                "bssid": ap['bssid'],
                "ssid": ap['ssid'],
                "signal": ap['max_signal'],
                "radius": ap['max_distance'],
                "readings": ap['num_readings']
            }
        })
    
    geojson = {
        "type": "FeatureCollection",
        "features": features
    }
    
    with open(output_file, 'w') as f:
        json.dump(geojson, f, indent=2)
    
    print(f"[+] Exported {len(features)} APs to {output_file}")

def main():
    import sys
    csv_file = sys.argv[1] if len(sys.argv) > 1 else "aerial_aps.csv"
    
    print("[*] Parsing aerial wardrive data...")
    aps = parse_aerial_csv(csv_file)
    print(f"[+] {len(aps)} unique APs found")
    
    print("[*] Estimating AP positions and signal cones...")
    results = generate_signal_cone(aps)
    
    # Filter for client SSIDs
    client_results = [r for r in results if 'Corp' in r['ssid']]
    print(f"[+] {len(client_results)} client APs identified")
    
    # Sort by signal leakage distance
    client_results.sort(key=lambda r: r['max_distance'], reverse=True)
    
    print("\n[+] TOP SIGNAL LEAKERS:")
    print(f"{'SSID':<25} {'Est Pos':<15} {'Max Signal':<12} {'Max Distance':<15}")
    print("-" * 70)
    for ap in client_results[:10]:
        print(f"{ap['ssid']:<25} "
              f"({ap['est_lat']:.5f},{ap['est_lon']:.5f}) "
              f"{ap['max_signal']:>5.0f} dBm    "
              f"{ap['max_distance']:>5.0f} m")
    
    # Export
    export_cesium_json(results)

if __name__ == "__main__":
    main()

Google Earth Visualization


For a simpler approach that doesn't need a 3D web framework:


bash

# Kismet's kismetdb_to_kml tool includes altitude
kismetdb_to_kml --in combined_aerial.kismet --out aerial_3d.kml

# Open in Google Earth Pro
# Each AP marker is at its estimated altitude
# Use the time slider to see signal capture over flight time
# Tilt the view to see vertical signal propagation

What Aerial Reveals That Ground Doesn't


Finding

Altitude Discovery

Impact

Rooftop APs

Only visible from above roof line

APs on the roof broadcasting down — no ground coverage but attackers with drones can see them

Point-to-point bridges

Highly directional, main lobe at altitude

Inter-building links visible. Can they be intercepted?

Upper-floor signal leakage

Signal bleeding upward through windows

Executive offices, boardrooms on upper floors broadcasting credentials to the sky

Atrium/non-linear propagation

Signal escaping through central atriums

Internal APs visible from above via architectural RF paths

Rogue APs on roof

Employees installing unauthorized APs for "rooftop WiFi"

Common finding — completely invisible from ground

Vertical signal boundaries

Exact altitude where signal drops below -85 dBm

Defines the "safe altitude" for an attacker in a neighboring building or aircraft

Multi-path reflections

AP appearing at two locations (direct + reflection off glass building)

Helps identify reflective surfaces that extend signal range unpredictably


Advanced: Signal Triangulation from Multiple Positions


With a drone, you can actively triangulate AP positions by flying a box around the building:


bash

# 1. Fly a 4-point box at constant altitude (e.g., 100m AGL)
#    North edge, East edge, South edge, West edge
#    Hover at each point for 30 seconds

# 2. Record signal strength at each point

# 3. Signal strength differences between the 4 points
#    triangulate the AP's position in 3D

# 4. Kismet does this automatically with enough data points
#    The position estimate improves with more angles

The math is simple: if the signal is strongest on the North side and weakest on the South, the AP is closer to the North wall, probably on a high floor. If the signal is equally strong on all sides, the AP is near the building center. If signal is very strong from above, the AP is on the roof.


Practical Limitations


Drone noise and attention: A Mavic 3 at 120m is audible. People look up. If the pentest is covert, this is a problem. Mitigation: fly at maximum legal altitude. At 120m, the drone is a speck. The buzz is faint but present. Schedule flights during noisy times (HVAC running, traffic, construction).


Battery logistics: 45 minutes sounds like a lot. At 8 m/s survey speed with turns, you cover roughly 15-20 hectares per battery. A medium campus needs 3-4 batteries for a proper multi-altitude survey. Land, swap, relaunch. Each battery swap takes 3-5 minutes.


GPS signal under the drone: The payload GPS is under the drone body. Drone body + battery = GPS signal attenuation. If the payload GPS can't get a fix, mount the GPS puck on top of the battery with velcro, antenna pointing up through a gap. Test on the ground first — run Kismet with the payload under the powered-on drone. Verify the GPS gets a fix with the drone above it.


WiFi interference from drone telemetry: The DJI transmission system uses 2.4 GHz and 5.8 GHz. Your survey is on those same bands. The drone's telemetry will appear in your scan. Filter it out during analysis:


bash

# DJI OcuSync SSID patterns to exclude:
# "DJI-*", "Mavic-*", "Phantom-*"
# Or exclude by OUI (DJI MAC prefix: 60:60:1F, etc.)

Carbon fiber drone frames: If you're using a DIY carbon fiber frame, the frame itself is an RF shield. Hang the antenna well below the frame on a rigid extension. Test on the ground: capture with and without the drone powered on above the payload. If AP counts drop significantly, reposition the antenna.


Reporting Additions for Aerial Survey


markdown

## Aerial Wireless Survey

### Flight Parameters
- Drone: DJI Mavic 3 Enterprise
- Payload: Raspberry Pi Zero 2W + Alfa AWUS036ACM + NEO-M8N GPS
- Software: Kismet 2024.x headless
- Survey Altitudes: 50m, 100m, 150m, 200m AGL
- Pattern: Grid, 40m line spacing, 6 m/s survey speed
- Total Flight Time: 2 hours 15 minutes (3 batteries)
- Authorization: [Reference UAV waiver / Part 107 license]

### Aerial-Only Findings
1. Rooftop AP "CorpWiFi-Roof" discovered at 45m AGL (building is 40m)
   Not visible from any ground survey point.
   Radiating -55 dBm at 120m altitude directly above building.

2. Point-to-point bridge between Building A and Building B
   Operating on channel 149 (5.8 GHz)
   Main lobe interceptable at 80-100m altitude along the path.

3. Upper floor signal leakage:
   14th floor executive suite: CorpWiFi-Exec visible at 200m altitude
   with -72 dBm (still usable). This signal is not visible from ground
   but is accessible from neighboring high-rise or aerial approach.

### 3D Signal Propagation Map
[Attach aerial_3d.kml or screenshot of 3D visualization]

### Combined Ground + Aerial Assessment
Ground survey: Signal leakage to 380m horizontal at ground level
Aerial survey: Signal leakage to 200m vertical, 420m horizontal at altitude
Combined threat envelope: [provide 3D model of attack surface]

Enroll In Online Cybersecurity & Hacking Classes/Courses | Black Hat HQ

Comments


bottom of page