Исследование протокола Apple Location Services Protocol

Исследование протокола Apple Location Services Protocol

Обычный способ исследования трафика при помощи цепочек из прокси серверов не работал, поскольку в macOS есть функция SIP.

Во время работы библиотекой Whereami меня заинтересовало, как на самом деле работают сервисы от Apple, связанные с обнаружением местонахождения. Я знаю, что этот протокол связан  locationd, поскольку в Little Snitch эта служба остается заблокированной. Обычный способ исследования трафика при помощи цепочек из прокси серверов не работал, поскольку в macOS есть функция SIP (System Integrity Protection; Защита целостности системы).

Альтернативный путь: установка Charles в качестве промежуточного прокси-сервера на iOS-устройстве. После просеивания трафика, в основном связанного с оповещениями большого брата (device phoning home), я получил информацию о запросе служб, связанных с определением местонахождением.

Запрос к службам, связанным с местонахождением

Сам по себе запрос представляет собой application/x-www-form-urlencode с некоторыми бинарными данными.

POST /clls/wloc HTTP/1.1
Host: gs-loc.apple.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 97
Proxy-Connection: keep-alive
Accept: */*
User-Agent: locationd/1756.1.15 CFNetwork/711.5.6 Darwin/14.0.0
Accept-Language: en-us
Accept-Encoding: gzip, deflate
Connection: keep-alive 
00000000: 00 01 00 05 65 6e 5f 55 53 00 13 63 6f 6d 2e 61  ....en_US..com.a
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd..
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321....
00000030: 00 00 00 2d 12 13 0a 11 62 34 3a 35 64 3a 35 30  ...-....b4:5d:50
00000040: 3a 39 34 3a 33 39 3a 62 33 12 12 0a 10 39 38 3a  :94:39:b3....98:
00000050: 31 3a 61 37 3a 65 36 3a 38 35 3a 37 30 18 00 20  1:a7:e6:85:70..

00000060: 64  

Поскольку вышеуказанные данные не содержат gzip-заголовок 0x1f8b, мое второе предположение было связано с использованием Protocol buffers (механизм сериализации структурированной информации). В конце концов, эту платформу сейчас многие используют. Попробуем выполнить декодирование.

$ xxd -r request.hex | protoc --decode_raw 
Failed to parse input. 

Не сработало. Возможно, в запросе присутствуют дополнительные данные. Логических анализ подсказывает, что Mac-адреса должны входить в этот информационный массив (выделено жирным цветом ниже).

00000000: 00 01 00 05 65 6e 5f 55 53 00 13 63 6f 6d 2e 61  ....en_US..com.a
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd..
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321....
00000030: 00 00 00 2d 12 13 0a 11 62 34 3a 35 64 3a 35 30  ...-....b4:5d:50 
00000040: 3a 39 34 3a 33 39 3a 62 33 12 12 0a 10 39 38 3a  :94:39:b3....98: 
00000050: 31 3a 61 37 3a 65 36 3a 38 35 3a 37 30 18 00 20  1:a7:e6:85:70..

00000060: 64

$ xxd -r request2.hex | protoc --decode_raw 
Failed to parse input. 

Опять не сработало. Верхняя часть выглядит как заголовок. Попробуем удалить этот кусок.

00000000: 00 01 00 05 65 6e 5f 55 53 00 13 63 6f 6d 2e 61  ....en_US..com.a 
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd.. 
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321.... 
00000030: 00 00 00 2d 12 13 0a 11 62 34 3a 35 64 3a 35 30  ...-....b4:5d:50 
00000040: 3a 39 34 3a 33 39 3a 62 33 12 12 0a 10 39 38 3a  :94:39:b3....98: 
00000050: 31 3a 61 37 3a 65 36 3a 38 35 3a 37 30 18 00 20  1:a7:e6:85:70.. 

00000060: 64

$ xxd -r request2.hex | protoc --decode_raw 

Failed to parse input.

Не сработало опять.

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

#!/bin/bash 
# 
# Try to decode hidden protocol buffers message from binary 
 
size=$(wc -c < $1)
 
for ((i=1; i<=$size; i++))
do 
    dd if=$1 bs=1 skip=$i | protoc --decode_raw
    if [[ $? == 0 ]]
    then 
        printf "\n" 
        read -p "Removed $i bytes, continue? [Yy] " -n 1 -r
        printf "\n\n" 
        if [[ ! $REPLY =~ ^[Yy]$ ]]
        then 
            exit 0
        fi 
    fi 
done 

protomower.sh
$ ./protomower.sh request.bin

Во время удаления байтов из первых трех строк ничего полезного на выходе не получалось. После перехода к удалению байтов из четвертой строки я получил осмысленный результат.

45+0 records in
45+0 records out
45 bytes transferred in 0.000063 secs (714938 bytes/sec)
2 {
  1: "b4:5d:50:94:39:b3"
}
2 {
  1: "98:1:a7:e6:85:70"
}
3: 0
4: 100 
 
Removed 52 bytes, continue? [Yy] 

Кажется, моя гипотеза оказалась близка к истине. Выделенные байты были удалены. Остальная часть, связанная с технологией protocol buffers, успешно декодирована.

00000000: 00 01 00 05 65 6e 5f 55 53 00 13 63 6f 6d 2e 61  ....en_US..com.a 
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd.. 
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321.... 
00000030: 00 00 00 2d 12 13 0a 11 62 34 3a 35 64 3a 35 30  ...-....b4:5d:50 
00000040: 3a 39 34 3a 33 39 3a 62 33 12 12 0a 10 39 38 3a  :94:39:b3....98: 
00000050: 31 3a 61 37 3a 65 36 3a 38 35 3a 37 30 18 00 20  1:a7:e6:85:70.. 
00000060: 64                                               d 

В итоге выяснилось, что в сообщении, имеющим отношение к запросу, четыре типа данных. В терминах технологии protocol buffers эти типы называются тегами. Каждое сообщение имеет четыре тега.

  • 1 – строка, содержащая mac-адрес. Скорее всего, это mac-адрес wifi-роутера.
  • 2 – встроенное сообщение, где содержимое тега 1 используется в качестве значения. Похоже на структуру или объект.
  • 3 и 4 – целые числа, смысл которых для меня остался неясен. Возможно, временной период с момента последнего подключения роутера к сети или отношение сигнала к шуму.

Чтобы доказать нашу гипотезу, попробуем сделать запрос с разными mac-адресами. Я использовал hex-редактор для редактирования бинарного файла запроса, а затем сделал POST-запрос при помощи curl.

 00000000: 00 01 00 05 65 6E 5F 55 53 00 13 63 6F 6D 2E 61  ....en_US..com.a 
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd.. 
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321.... 
00000030: 00 00 00 2d 12 13 0a 11 36 34 3a 64 38 3a 31 34  ...-....64:d8:14
00000040: 3a 37 32 3a 36 30 3a 30 63 12 13 0a 11 31 30 3a  :72:60:0c....10:
00000050: 62 64 3a 31 38 3a 35 66 3a 65 39 3a 38 33 18 00  bd:18:5f:e9:83..

00000060: 20 64                      

$ curl https://gs-loc.apple.com/clls/wloc --include --request POST --data-binary @request2.bin 
HTTP/1.1 400 Bad Request
Date: Sun, 07 May 2017 06:26:06 GMT
Cneonction: Close
Content-Type: text/plain
X-RID: 62904d6c-fe93-47d5-b579-548f9c83297c
Content-Length: 11
 
Bad Request 

Не работает. Попробуем разобраться, что не так.
Глядя на дамп, видно, что сообщение стало на 1 байт больше. То есть где-то должна быть еще контрольная сумма, что вполне очевидно. Шестнадцатеричное число 0x2d равно 45 в десятичной системе. Первоначальное сообщение было длиной 45 байт. Новое сообщение размером 46 байт, что эквивалентно 0x2e в шестнадцатеричной системе. Предположу, что контрольная сумма является целым 16-битным числом (то есть 0x002e).

00000000: 00 01 00 05 65 6E 5F 55 53 00 13 63 6F 6D 2E 61  ....en_US..com.a 
00000010: 70 70 6c 65 2e 6c 6f 63 61 74 69 6f 6e 64 00 0c  pple.locationd.. 
00000020: 38 2e 34 2e 31 2e 31 32 48 33 32 31 00 00 00 01  8.4.1.12H321.... 
00000030: 00 00 00 2e 12 13 0a 11 36 34 3a 64 38 3a 31 34  ...-....64:d8:14
00000040: 3a 37 32 3a 36 30 3a 30 63 12 13 0a 11 31 30 3a  :72:60:0c....10:
00000050: 62 64 3a 31 38 3a 35 66 3a 65 39 3a 38 33 18 00  bd:18:5f:e9:83..

00000060: 20 64 

$ curl https://gs-loc.apple.com/clls/wloc --include --request POST --data-binary @request3.bin 
HTTP/1.1 200 OK
X-RID: bb3cc16a-6680-4019-b5d0-fb52e8c8bd5a
Content-Type: text/plain
Content-Length: 4948 

 

Операция успешно завершена, и теперь мы знаем формат запросов.

[header][size][message] ([заголовок][размер][сообщение])

Сам заголовок можно разделить на части попозже. Изначально я думал, что эта информация представляла собой просто магический управляющий ASCII-код. Однако читатель из Reddit’a сориентировал меня в верном направлении. Скорее всего, в заголовке использовались байтовые строки с префиксом длины (length-prefix framed byte strings). Я также полагаю, что 0x0001 означает начало заголовка, а 0x0000 – окончание.

NUL SOH      /* 0x0001 start of header */
[length]     /* length of the locale string in bytes */
[locale]     /* en_US */
[length]     /* length of the identifier string in bytes */
[identifier] /* com.apple.locationd */
[length]     /* length of the version string in bytes
[version]    /* 8.4.1.12H321 ie. ios version and build */
NUL NUL      /* 0x0000 end of header */
NUL SOH      /* 0x0001 start of header */

NUL NUL      /* 0x0000 end of header */


Я не уверен, что означают последние 4 байта. Возможно, указатель на второй заголовок, который на данный момент пуст.

Ответ служб, связанных с местонахождением


Размер ответа довольно внушителен.

00000000: 00 01 00 00 00 01 00 00 13 4a 12 40 0a 10 36 34  .........J.@..64
00000010: 3a 64 38 3a 31 34 3a 37 32 3a 36 30 3a 63 12 2c  :d8:14:72:60:c.,
00000020: 08 80 98 f7 f8 bc ff ff ff ff 01 10 80 98 f7 f8  ................
00000030: bc ff ff ff ff 01 18 ff ff ff ff ff ff ff ff ff  ................
00000040: 01 28 ff ff ff ff ff ff ff ff ff 01 12 30 0a 11  .(...........0..
00000050: 31 30 3a 62 64 3a 31 38 3a 35 66 3a 65 39 3a 38  10:bd:18:5f:e9:8
00000060: 33 12 18 08 a1 a9 d3 40 10 a0 8c db de 26 18 39  3......@.....&.9
00000070: 20 00 28 11 30 08 58 3c 60 ec 01 a8 01 06 12 2e   .(.0.X<`.......
00000080: 0a 0f 30 3a 31 65 3a 31 33 3a 37 3a 39 30 3a 64  ..0:1e:13:7:90:d
...
000012F0: 01 01 12 2f 0a 10 30 3a 32 61 3a 31 30 3a 65 65  .../..0:2a:10:ee
00001300: 3a 35 30 3a 61 34 12 18 08 c0 a3 d5 40 10 b7 c1  :50:a4......@...
00001310: c8 de 26 18 2b 20 00 28 14 30 0e 58 3e 60 e8 01  ..&.+ .(.0.X>`..
00001320: a8 01 0b 12 2f 0a 10 30 3a 31 31 3a 32 31 3a 63  ..../..0:11:21:c
00001330: 63 3a 35 36 3a 33 32 12 18 08 8d ec c9 40 10 91  c:56:32......@..
00001340: 95 cf de 26 18 61 20 00 28 14 30 0f 58 3f 60 c3  ...&.a .(.0.X?`.

00001350: 1a a8 01 01  
Здесь нас вновь выручаем метод, который мы использовали во время декодирования запроса. Расшифрованный ответ получился размером примерно 1400 строк.

$ ./protomower.sh response.bin 
2 {
  1: "64:d8:14:72:60:c"
  2 {
    1: 18446744055709551616
    2: 18446744055709551616
    3: 18446744073709551615
    5: 18446744073709551615
  }
}
2 {
  1: "10:bd:18:5f:e9:83"
  2 {
    1: 135582881
    2: 10399172128
    3: 57
    4: 0
    5: 17
    6: 8
    11: 60
    12: 236
  }
  21: 6
}
...
 
Removed 10 bytes, continue? [Yy] 

Первое значение сразу же вводит в заблуждение. Десятичное число 18446744073709551615 в шестнадцатеричной системе равно 0xfffffffffffffff (то есть максимальное беззнаковое 64-битное значение). Вероятно, сей факт означает, что mac-адрес не найден. По поводу значения 18446744055709551616 (0xfffffffbcf1dcc00) у меня также нет никаких идей.
Смысл остальных результатов более очевиден:

  • 2-1 - mac-адрес
  • 2-2-1 - широта 135582881 * pow(10, -8) = 1.35544532
  • 2-2-2 - долгота 10399172128 * pow(10, -8) = 103.99172128
  • 2-2-3 - похоже на точность определения местонахождения.
  • 2-21 – возможно, wifi-канал

Вначале меня озадачил факт получения 101 результата. Затем до меня дошло, что я получил 100 успешных результатов. Первые два результата – отправленные mac-адреса. Остальные результаты – mac-адреса, находящиеся в ближайшей доступности от тех, которые я отправил.

Но почему 100 результатов?

Я думаю, что Apple выгружает трехсторонние измерения клиенту. Вместо ресурсоемких расчетов возвращается набор координат точек доступа.

Если как минимум три точки доступа видны клиенту, фреймворк core location может использовать уровень сигнала в качестве расстояния. Когда у нас есть координаты трех точек доступа и расстояние до цели, мы можем рассчитать местонахождение цели с приемлемой точностью.

Ниже показаны координаты точек доступа, полученных во время запроса местонахождения аэропорта Changi.



Рисунок 1: Точки доступа в районе аэропорта Changi

Если у нас есть информация о сотне точек доступа вокруг, снижается необходимость в повторном соединение с сервером служб, связанных с местонахождением. Поскольку в core location есть координаты трех видимых точек доступа, можно вычислить местонахождение нашей точки. Подобные расчеты могут быть сделаны даже оффлайне, поскольку WiFi включен.

Как использовать эту возможность?

Вы можете написать поддержку Core location на уровне пользователя для тех языков программирования, где-то подобный функционал отсутствует. Хотя есть более простые способы решить ту же самую задачу.
Более интересная задача – развернуть свой собственный сервер служб, связанных с местоположением с целью облегчения отладки приложений.

Другие статьи по теме

Application à l’analyse des données de géolocalisation envoyées par un smartphone Авторы: François-Xavier Aguessy и Côme Demoustier. Я не умею читать на французском языке, но в этой статье есть примеры файлов .proto и код на языке Python, который помог мне начать погружение в эту тему. Хотя протокол скорее всего изменился с момента публикации этой статьи.

Vulnerability Analysis and Countermeasures for WiFi-based Location Services and Application Авторы: Jun Liang (Roy) Feng и Guang Gong. Статья полезна для общего понимания того, как работает определение местоположения в WiFi-сетях.
Gaussian Processes for Signal Strength-Based Location Estimation Авторы: Brian Ferris, Dirk Hahnel и Dieter Fox. Я не понимаю и половины математических выкладок. Однако в этой статье объясняются различные проблемы внутри механизма, связанного с определением местоположения в WiFi-сетях.

Цифровые следы - ваша слабость, и хакеры это знают.

Подпишитесь и узнайте, как их замести!