Разговоры в небе. Как самолеты передают данные по ADS-B

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Современные самолеты непрерывно передают друг другу телеметрию по протоколу ADS-B. В этой статье мы с тобой сделаем собственный воображаемый самолет, чтобы посмотреть, как работает передача данных. Конечно, будем при этом соблюдать все законы и правила безопасности.

В прошлой статье я уже говорил, что ADS-B — это часть большого протокола Mode S, который активно используется для идентификации и отслеживания воздушных судов. ADS-B позволяет отслеживать летательные аппараты, на которых установлен специальный транспондер. Он регулярно передает данные о самолете: высоту, координаты, скорость и многое другое. Принять их может любой желающий, так как никакой защиты или шифрования в протоколе не предусмотрено.

На этот раз мы полностью сосредоточимся на передаче своего сигнала ADS-B. Чтобы никому не помешать, принимать мы его тоже будем на свой приемник. Учти: это вовсе не пособие по оснащению самолетов транспондерами. Скорее мы будем использовать этот любопытный пример для демонстрации того, как кто‑то может разобрать и подделать любой незащищенный сигнал.

ТЕСТОВЫЙ ПОЛИГОН​

Тестовое окружение необходимо, чтобы видеть, что мы делаем, но при этом не влиять на оборудование аэропортов. Вряд ли они оценят наши эксперименты! Зато ущерб вполне могут оценить, а сумму выставить к оплате. Чтобы этого избежать, я взял два SDR и использовал на минимальной мощности в экранирующем помещении. К тому же сигналы на частотах около 1 ГГц, которые я использовал, практически не распространяются за горизонт.

В моем распоряжении оказались два неплохих и довольно популярных SDR: HackRF и BladeRF. Первый я буду использовать как передатчик, а второй — как приемник. Программную часть приема сделаем с помощью уже знакомой из прошлой статьи программы dump1090, которая умеет парсить сигналы Mode S (частью которого и является ADS-B) в реальном времени и выводить их в читаемом формате.

Схема установки будет выглядеть примерно так.
Схема лаборатории


Для стабильной работы на оба SDR нужно накрутить любую штыревую антенну, так как без нее ничего работать не будет или будет, но очень‑очень плохо.

Настройка BladeRF​

Пошаговую настройку BladeRF для приема мы уже разбирали в прошлой статье. Если ты повторял за мной, то у тебя уже есть утилита dump1090 и нужная прошивка в SDR. В таком случае этот раздел можно пропустить.

С заводской прошивкой BladeRF не умеет работать с dump1090 — придется их подружить вручную. Первым шагом будет скачивание прошивки, совместимой с ADS-B, с официального сайта разработчика:
Код: Скопировать в буфер обмена
wget https://www.nuand.com/fpga/adsbxA4.rbf
После этого сразу же загружаем скачанный файл в BladeRF:
Код: Скопировать в буфер обмена
bladerf-cli -i adsbxA4.rbf
Дальше нам надо установить еще две программы: bladeRF-adsb и саму dump1090. Первая нужна просто для того, чтобы транслировать данные с SDR напрямую в dump1090.

Скачиваем и собираем dump1090:
Код: Скопировать в буфер обмена
Код:
git clone https://github.com/mutability/dump1090.git
cd dump1090
make
./dump1090 --net-only --raw --interactive
После запуска dump1090 переходим к мосту:
Код: Скопировать в буфер обмена
Код:
git clone https://github.com/Nuand/bladeRF-adsb
cd bladeRF-adsb/bladeRF_adsb
wget http://nuand.com/fpga/adsbx40.rbf
wget http://nuand.com/fpga/adsbx115.rbf
make
./bladeRF_adsb
Теперь, когда все готово для приема и декодирования сигналов ADS-B, уже можно принять какие‑то сигналы от самолетов (да, даже в комнате), особенно если самолет близко или если у тебя хорошая антенна.

ViralAir​

Чтобы генерировать свои сообщения ADS-B, я создал утилиту ViralAir и выложил ее на GitHub. Чтобы повторять за мной, скачай и скомпилируй ее:
Код: Скопировать в буфер обмена
Код:
git clone https://github.com/st3rv04ka/ViralAir
cd ViralAir
go build cmd/viralair/main.go
После сборки появится файл main, который и будет генерировать готовый для передачи файл. Формат этого файла полностью подходит для HackRF, но может быть совместим и с другими SDR (я не проверял). Чтобы проверить, что все хорошо, можно создать тестовый самолет с позывным 0xDEADBE на высоте 9999 футов следующей командой:
Код: Скопировать в буфер обмена
./main -altitude 9999.0 --icao 0xDEADBE
В текущей директории должен появиться файл Samples.iq8s. Если он действительно есть, значит, все работает и можно продолжать.

СТРУКТУРА ADS-B​

Подготовка​

Давай немного освежим знания про структуру сообщения ADS-B из прошлой статьи. Сообщение может быть разной длины — 56 либо 112 бит. От длины зависит тип передаваемой информации, а сам тип записывается в поле TC (Type Code) сообщения. Обычное 112-битное сообщение выглядит следующим образом.
typecodes.png


Как ты мог заметить, никакого поля TC тут нет — и все верно, оно спрятано внутри поля ME. Дело в том, что ADS-B — это всего лишь часть большого протокола Mode S, и выше представлена структура именно сообщения Mode S, в то время как все специфичные для ADS-B поля находятся внутри поля данных (ME) Mode S.

Давай освежим в памяти назначение остальных полей пакета:
  • DF (downlink format) отвечает за тип сигнала в Mode S. Для ADS-B всегда равен 17;
  • CA (transponder capability) отвечает за тип транспондера, который мы теоретически можем поставить какой угодно, но на практике будем использовать 6 (что означает транспондер уровня 2+);
  • после CA идет ICAO — уникальный номер самолета. В эти 24 бита мы можем запихнуть что угодно, а в рамках этой статьи будем запускать самолет 0xDEADBE;
  • ME — само сообщение ABS-B. Это может быть высота, скорость или координаты. Мы будем передавать только координаты (TC равен 11), без скорости и высоты. Скорость и высота ничем не хуже, просто мне хочется разобрать подробнее кодирование CPR.

CPR​

Возможно, ты читал прошлую статью и еще помнишь, как я мучился с CPR и упрощенными картами для передачи в маленьких сообщениях. Из‑за этих карт в ADS-B есть два вида сообщений с координатами: четное и нечетное, и, только имея оба сообщения, можно определить точную локацию самолета. Создавать такие пакеты еще сложнее, чем принимать.

Давай пробежимся по моему коду на Go, который переводит координаты с CPR:
Git: Скопировать в буфер обмена
Код:
func nl(declatIn float64) float64 {
  // Близко к полюсам секторов мало, возвращаем всего один
    if math.Abs(declatIn) >= 87.0 {
        return 1.0
    }
    return math.Floor(
        (2.0 * math.Pi) * math.Pow(
            math.Acos(1.0 -
                (1.0-math.Cos(math.Pi/(2.0*latz)))
                / math.Pow(math.Cos((math.Pi/180.0) * math.Abs(declatIn)), 2)),
        -1))
}
func dlon(declatIn float64, ctype int, surface int) float64 {
    var tmp float64
    if surface == 1 {
        tmp = 90.0
    }
    else
    {
        tmp = 360.0
    }
    nlcalc := math.Max(nl(declatIn)-float64(ctype), 1)
    return tmp / nlcalc
}
// Кодируем в CPR
func CprEncode(lat float64, lon float64, ctype int, surface int) (int, int) {
    var scalar float64
    if surface == 1 {
        scalar = math.Pow(2, 19)
    } else {
        scalar = math.Pow(2, 17)
    }
    dlati := dlat(ctype, surface)
    yz := math.Floor(scalar*((math.Mod(lat, dlati))/dlati) + 0.5)
    dloni := dlon(lat, ctype, surface)
    xz := math.Floor(scalar*((math.Mod(lon, dloni))/dloni) + 0.5)
    return int(yz) & ((1 << 17) - 1), int(xz) & ((1 << 17) - 1)
}
Да, в этом коде черт ногу сломит, но на самом деле за сложной математикой стоят довольно простые алгоритмы.

Начнем по порядку, с функции nl(). Она рассчитывает количество зон на заданной нами широте. Логика простая: чем ближе к полюсам, тем меньше зон заданного размера поместится на одной широте. Вот так выглядят реальные зоны в CPR.
Карта зон в CPR


Чтобы не морочить себе голову сложной математикой, при широте больше 87 градусов будем возвращать количество делений равное единице. Формула для расчета взята из официальной документации к протоколу, и в самом документе она выглядит довольно страшно.

Формула для расчета количества зон


После nl() идет функция dlon(). Количество делений на широте у нас уже есть, а теперь нужно рассчитать интервал, чтобы равномерно распределить эти деления по кругу и чтобы все они были одинаковыми. При этом важно учитывать, где находится самолет, — на земле или в воздухе. Если самолет в воздухе, мы делим 360 на количество интервалов минус тип сообщения (even или odd). Полученный результат возвращается в конце функции. Функция dlat() делает плюс‑минус то же самое для долготы, так что не будем останавливаться на ней отдельно.

Теперь перейдем к основной функции расчета CPR. После получения количества зон можно рассчитать непосредственно координаты самолета. Для этого учтем множитель, который можно найти в документации протокола (он скрыт глубоко в формулах). Для воздуха он будет равен 217, а для земли — 219.

Формула для воздуха


Формула для земли


Полученное после кодирования число нужно сжать до 17 бит, что и делается при возврате результатов из функции.

PI​

Если ты выдохнул с мыслью, что все сложное уже позади, вдыхай обратно! Вторая сложность при передаче своих сигналов — это расчет контрольной суммы (CRC), или parity bits. Нужно посчитать и добавить в конец сообщения 24 бита, которые используются для проверки целостности сообщения. Именно на них ориентируется, например, dump1090. Загвоздка в том, что формула для расчета выглядит так.
Алгоритм подсчета parity bits


В псевдокоде эта функция выглядит примерно так:
Код: Скопировать в буфер обмена
Код:
generator = 1111111111111010000001001
# 11 + 3 нулевых байта
data_hex = 8D406B902015A678D4D220[000000]
# 88 бит
data = 1000110101000000011010 1110010000001000000001
       0101101001100111100011 0101001101001000100000
      [000000000000000000000000]  # 24 бита
FOR i FROM 0 TO (112-24):
  IF data[i] IS 1:
    data[i:i+24] = data[i:i+24] XOR generator
remainder = data[-24:]
# Результат: 101010100100101111011010, или AA4BDA в HEX
Генератор — это константа, которую нашли специально для этого алгоритма и которая наиболее эффективна. В цикле мы просто проходим по всем битам от 0 до 88 (112 минус 24, потому что последние 24 бита мы сейчас и заполняем) и применяем исключающее ИЛИ с генератором. Полученные 24 бита нужно добавить к нашему сообщению, чтобы у нас был законченный пакет данных. Вот как я это сделал в ViralAir:
Код: Скопировать в буфер обмена
Код:
const ( GENERATOR = "1111111111111010000001001" )
func Crc(msg string, encode bool) string {
    msgbin := []rune(misc.Hex2bin(msg))
    generator := []int{}
    for _, char := range GENERATOR {
        generator = append(generator, int(char-'0'))
    }
    if encode {
        for i := len(msgbin) - 24; i < len(msgbin); i++ {
            msgbin[i] = '0'
        }
    }
    for i := 0; i < len(msgbin)-24; i++ {
        if msgbin[i] == '1' {
            for j := range generator {
                msgbin[i+j] = rune('0' + (int(msgbin[i+j]-'0') ^ generator[j]))
            }
        }
    }
    reminder := string(msgbin[len(msgbin)-24:])
    return reminder
}
Реализация на Go похожа на наш псевдокод, так что разбирать подробно ее не буду.

Собираем всё вместе​

Теперь, когда у нас есть все служебные функции, можно создавать пакет! Начинается всё с кодирования высоты, за это отвечает функция encodeAltModes().
Код: Скопировать в буфер обмена
Код:
// Encode altitude
func encodeAltModes(alt float64, surface int) int {
    mbit := 0
    qbit := 1
    encalt := int((int(alt) + 1000) / 25)
    var tmp1, tmp2 int
    if surface == 1 {
        tmp1 = (encalt & 0xfe0) << 2
        tmp2 = (encalt & 0x010) << 1
    } else {
        tmp1 = (encalt & 0xff8) << 1
        tmp2 = 0
    }
    return (encalt & 0x0F) | tmp1 | tmp2 | (mbit << 6) | (qbit << 4)
}
В зависимости от высоты применяются разные делители. Для обычных самолетов — 25, но есть и другие, например 100. Они нужны для самолетов, которые летают выше, чем обычные. За выбор делителя отвечает параметр qbit. Смысл делителя — указать, какого размера взят интервал для высоты.

Поскольку указывать высоту в нормальных величинах, а не в футах — это слишком просто, авторы протокола придумали делить высоту на интервалы по N футов и указывать номер такого интервала, а N — это и есть наш делитель (сколько футов в одном интервале). Делитель в 25 футов для нашего самолета дает точность около 7,6 метра.

По поводу самого qbit документация протокола говорит следующее:
This field will contain barometric altitude encoded in 25 or 100-foot increments (as indicated by the Q Bit). All zeroes in this field will indicate that there is no altitude data.
Нажмите, чтобы раскрыть...
То есть если qbit установить в 0, то делитель станет равным 100, что бывает полезно, если высота слишком большая и ее сложно уместить в сообщение. В ViralAir предусмотрен только делитель 25, хотя добавить 100 можно довольно легко — попробуй реализовать это сам на досуге!

Результат работы функции для высоты в 9999 футов ты можешь увидеть ниже:
Код: Скопировать в буфер обмена
Код:
2024/01/15 03:40:37 [+] Encode altitude [9999.000000] with the surface flag [0]
2024/01/15 03:40:37 [+] Encoded altitude [0x377]
Следующая задача — создать два сообщения (even и odd), и начнем мы с even. Первая часть любого сообщения ADS-B — это его тип. DF (тип сообщения Mode S) для ADS-B всегда равен 17, так что это константа в коде, а после него идут CA (уровень транспондера) и ICAO (номер самолета). CA и ICAO можно задать свои как аргументы при запуске ViralAir.
Код: Скопировать в буфер обмена
Код:
// Format + CA + ICAO
dataEven = append(dataEven, byte((format<<3)|ca))
dataEven = append(dataEven, byte((icao>>16)&0xff))
dataEven = append(dataEven, byte((icao>>8)&0xff))
dataEven = append(dataEven, byte((icao)&0xff))
Теперь добавим к фрейму долготу, широту и высоту:
Код: Скопировать в буфер обмена
Код:
// Even
log.Printf("[+] Encode even frame with lat [%f] and lon [%f]", lat, lon)
evenLat, evenLon := cpr.CprEncode(lat, lon, 0, surface)
log.Printf("[+] Encoded even frame lat [0x%02x] and lon [0x%02x]", evenLat, evenLon)
// Odd
log.Printf("[+] Encode odd frame with lat [%f] and lon [%f]", lat, lon)
oddLat, oddLon := cpr.CprEncode(lat, lon, 1, surface)
log.Printf("[+] Encoded odd frame lat [0x%02x] and lon [0x%02x]", oddLat, oddLon)
При запуске программы закодированные значения выводятся в консоль:
Код: Скопировать в буфер обмена
Код:
./main -altitude 9999.0 -icao 0xDEADBE -latitude 11.33 -longitude 11.22
<...>
2024/01/15 03:40:37 [+] Encode even frame with lat [11.330000] and lon [11.220000]
2024/01/15 03:40:37 [+] Encoded even frame lat [0x1c6d4] and lon [0x19d86]
2024/01/15 03:40:37 [+] Encode odd frame with lat [11.330000] and lon [11.220000]
2024/01/15 03:40:37 [+] Encoded odd frame lat [0x1b6b6] and lon [0x18d91]
<...>
Байты для обоих пакетов готовы, добавляем их к нашему фрейму:
Код: Скопировать в буфер обмена
Код:
// Lat + Lot + Alt (even)
dataEven = append(dataEven, byte((tc<<3)|(ss<<1)|nicsb))
dataEven = append(dataEven, byte((encAlt>>4)&0xff))
dataEven = append(dataEven, byte((encAlt&0xf)<<4|(time<<3)|(ff<<2)|(evenLat>>15)))
dataEven = append(dataEven, byte((evenLat>>7)&0xff))
dataEven = append(dataEven, byte(((evenLat&0x7f)<<1)|(evenLon>>16)))
dataEven = append(dataEven, byte((evenLon>>8)&0xff))
dataEven = append(dataEven, byte((evenLon)&0xff))
Данные фрейма готовы, осталось дописать к ним контрольную сумму.
Код: Скопировать в буфер обмена
Код:
// Convert to hex
var sbEven strings.Builder
for _, b := range dataEven[:11] {
    sbEven.WriteString(fmt.Sprintf("%02x", b))
}
dataEvenString := sbEven.String()
log.Printf("[+] Even frame without CRC [%s]", dataEvenString)
// Calculate CRC
dataEvenCRC := misc.Bin2int(crc.Crc(dataEvenString+"000000", true))
log.Printf("[+] Even data CRC [%02x]", dataEvenCRC)
// Append CRC
dataEven = append(dataEven, byte((dataEvenCRC>>16)&0xff))
dataEven = append(dataEven, byte((dataEvenCRC>>8)&0xff))
dataEven = append(dataEven, byte((dataEvenCRC)&0xff))
log.Printf("[+] Even data [%02x]", dataEven)
После этого в терминал вывалится полный фрейм Mode S, который можно использовать как угодно: передать, декодировать онлайн или даже распечатать и повесить на стенку.
Код: Скопировать в буфер обмена
Код:
2024/01/15 03:40:37 [+] Even data [8ddeadbe5837738da99d861b04b3]
2024/01/15 03:40:37 [+] Odd data [8ddeadbe5837776d6d8d9121b103]
От отправки сигнала нас отделает всего одна мелочь: нужно смодулировать наш сигнал.

ПЕРЕДАЧА​

Модуляция​

Чтобы передать наши байты в эфир, нужно преобразовать их в формат, который поймет HackRF. Формат очень простой: нужно закодировать все байты в комплексные числа и сохранить их в файл.

Для ADS-B применяется манчестерское кодирование, то есть единица кодируется как 01, а ноль как 10. Чтобы лучше это понять, посмотри на картинку ниже.
Кодирование битов


То есть нужно двоичное представление каждого байта закодировать по описанной выше схеме — и дело в шляпе. Все это базовые операции с числами, которые можно легко сделать на любом языке. Ниже — моя реализация на Go.
Код: Скопировать в буфер обмена
Код:
func Frame1090esPpmModulate(even, odd []byte) []byte {
    var ppm []byte
    for i := 0; i < 48; i++ {
        ppm = append(ppm, 0)
    }
    ppm = append(ppm, 0xA1, 0x40)
    for _, byteVal := range even {
        word16 := misc.Packbits(manchesterEncode(^byteVal))
        ppm = append(ppm, word16[0])
        ppm = append(ppm, word16[1])
    }
    for i := 0; i < 100; i++ {
        ppm = append(ppm, 0)
    }
    ppm = append(ppm, 0xA1, 0x40)
    for _, byteVal := range odd {
        word16 := misc.Packbits(manchesterEncode(^byteVal))
        ppm = append(ppm, word16[0])
        ppm = append(ppm, word16[1])
    }
    for i := 0; i < 48; i++ {
        ppm = append(ppm, 0)
    }
    return ppm
}
Нужно только не забыть о наличии преамбулы — специальной последовательности битов в начале фрейма, по которой приемник определяет начало сообщения. В нашем случае это 0xA1 0х40, что соответствует преамбуле Mode S.

Полученные биты нужно модулировать в PPM. Как работает PPM и чем отличается от других способов модуляции, наглядно видно на картинке ниже.

PPM-модуляция


Только вот кодировать мы будем не аналоговый сигнал, а уже готовый цифровой, то есть представлять наш сигнал в виде комплексных чисел. Углубляться во все их тонкости мы сейчас не станем, поскольку для наших задач нужно только два таких числа, с которыми мы даже не будем производить никаких расчетов. Комплексное число состоит из двух компонент (I и Q), которые для бита с высоким уровнем мы установим в максимальное значение, а для низкого — в минимальное:
Код: Скопировать в буфер обмена
Код:
func GenerateSDROutput(ppm []byte) []byte {
    bits := misc.Unpackbits(ppm)
    var signal []byte
    for _, bit := range bits {
        var I, Q byte
        if bit == 1 {
            I = byte(127)
            Q = byte(127)
        }
        else
        {
            I = 0
            Q = 0
        }
        signal = append(signal, I, Q)
    }
    return signal
}
Теперь записываем результат в файл и получаем наш заветный Samples.iq8s.

В эфир​

Ну вот, теперь можешь выдыхать с облегчением. После сборки тестового стенда, генерации файла с сигналом и настройки всего оборудования мы наконец‑то можем передать данные о нашем самолете самим себе!

Включаем прием, как описано в начале, и передаем сигнал через HackRF:
Код: Скопировать в буфер обмена
Код:
~/P/ViralAir (main)> hackrf_transfer -t Samples.iq8s -f 1090000000 -s 2000000 -x 47 -R
call hackrf_set_sample_rate(2000000 Hz/2.000 MHz)
call hackrf_set_hw_sync_mode(0)
call hackrf_set_freq(1090000000 Hz/1090.000 MHz)
Stop with Ctrl-C
 3.9 MiB / 1.000 sec =  3.9 MiB/second, average power -6.5 dBfs
 3.9 MiB / 1.000 sec =  3.9 MiB/second, average power -6.5 dBfs
 3.9 MiB / 1.000 sec =  3.9 MiB/second, average power -6.5 dBfs
 4.2 MiB / 1.000 sec =  4.2 MiB/second, average power -6.5 dBfs
Аргумент -t — это файл с сигналом, который мы сгенерировали; -f задает частоту передачи (для ADS-B это 1090 МГц, или 1 090 000 000 Гц), а -s — частоту дискретизации. Последние два аргумента отвечают за мощность передачи и ее зацикливание. Зачем зацикливать? Ведь у нас всего два сообщения, и сразу после окончания передачи они пропадут из окна dump1090.
Давай теперь взглянем на приемник!
Вывод dump1090


В таблице виден наш самолет DEADBE! Он летит на высоте 9975 футов (сказывается неточность кодирования высоты), и его даже можно найти на карте.
nigeria.png


Прямо сейчас он летит за тобой где‑то над Нигерией.

Источник xakep.ru
Автор @qeewqq
t.me/qeewqw
 
Сверху Снизу