Протокол Telegram известен своей доступностью и открытостью. У него есть множество публичных реализаций: tdlib/td, rubenlagus/TelegramApi, vysheng/tg, LonamiWebs/Telethon и другие. Однако, даже имея в распоряжении столь богатый инструментарий и объемную документацию (https://core.telegram.org/api), решить прикладную задачу, собрав из многообразия методов API нужную цепочку – не так-то просто. Сможет, например, “неподготовленный ум“ сходу догадаться, как решить прикладную задачу а-ля “поиск по номеру в Telegram“? — Скорее всего, придется потратить какое-то время на изучение API.
Официальный клиент Telegram содержит в себе массу API-цепочек, реализующих определенные пользовательские сценарии. Если подумать, взаимодействие на основе сценариев — наиболее удобный и предпочтительный способ, поэтому мы решили пойти по пути упрощения взаимодействия с Telegram на основе реализации библиотеки сценариев. Так как наша деятельность тесно связана с направлением OSINT, то в первую очередь мы решили реализовать ряд OSINT-сценариев, применимых в сети Telegram, о которых и хотим рассказать в этой статье.
Для решения задач OSINT мы еще давно начали работу над собственным клиентом для сети Telegram, который в последствии трансформировался в расширяемую библиотеку сценариев — telegram-osint-lib.
Почему пришлось делать собственный клиент?
Мы разрабатываем программные системы для сбора данных из открытых источников. Об одном из наших сервисов — Nuga — мы уже как-то рассказывали ранее.
Здесь и далее под “клиентом” подразумевается не графическое приложение, а “бот” (не путать с внутренним функционалом Telegram), управляемый из консоли, который решает определенную прикладную задачу в рамках сети Telegram с помощью предоставляемого API.
На момент первичной реализации клиента (~конец 2018 года) сторонние библиотеки-клиенты Telegram, которые мы рассматривали, обновлялись нерегулярно и не соответствовали требованиям из документации по протоколу, которая обновлялась с большой задержкой (полгода и более), как и официальные клиенты.
Часто в новоиспеченных версиях официальных клиентов можно было наблюдать кардинальные отличия от официальной документации, что сеяло зерна конспирологических сомнений. Например, API Layer 105 (выпущенный с огромным скачком от последней версии 23) наконец-то рассказал нам об опциональных полях и всех недостающих типах данных, устранив тем самым все подозрения на гипотетические закладки. Сейчас с этим стало лучше, документация обновлена, хотя некоторые детали все равно приходится определять экспериментально.
Сценарийная направленность
Занимаясь достаточно долгое время развитием собственного клиента, клиент оброс множеством оберток над группами API-вызовов. Стало понятно, что как простой набор API вызовов к Telegram он нас не особо интересует — в пределах сети Telegram гораздо интереснее выполнять комплексные операции, задействующие сразу множество API-вызовов. Таким образом был осуществлен переход от API-направленности к сценарийной направленности клиента.
Сценарий в telegram-osint-lib – это “черный ящик”, реализованный в виде последовательности API-вызовов, позволяющей достичь определенной конечной цели (output) на основе входных параметров (input). Аргументы представляют из себя понятные сущности окружающего мира (например, ключевое слово для поиска). В результате использования аргументов внутри черного ящика на выходе получается запрошенная информация (например, сообщения с указанным ключевым словом). Вся рутина по взаимодействию с API Telegram при этом инкапсулирована в реализации сценария. Сценарии могут сочетаться друг с другом, формируя более комплексные сценарии.
Концепция сценария была разработана в процессе решения задач внутри компании, однако, схожие понятия встречаются много где, например, в литературе по анализу требований — Scenario-based modeling and its applications — т.е идея “scenario-based“ сама по себе, конечно же, не нова.
Детали реализации
Библиотека telegram-osint-lib реализована по асинхронной модели и рассчитана на поддержку нескольких одновременных соединений (пример). Изначально при реализации мы следовали следующим принципам:
fail fast: при работе с проприетарным протоколом (пусть и имеющим более-менее открытую документацию) необходимо реагировать на изменения оперативно
conformity: библиотечная реализация максимально соответствует существующим клиентам и учитывает ограничения, заложенные в протокол
testability: код должен быть доступен для тестирования, а именно: быть декомпозирован, иметь низкую связность
На этапе дизайна архитектуры библиотеки, были выделены следующие уровни абстракции (от низкого к высокому):
Уровень клиента, содержащего набор нужных методов API для выполнения класса операций
Уровень сценария, комбинируемого с соседями по уровню
Уровень интерфейса пользователя (скрипт, вызывающий сценарии)
Так как клиент реализован по асинхронной модели, каждый вызов возвращает результат не напрямую, а через callback. Такой подход к реализации позволяет обрабатывать асинхронные ответы (что предусмотрено протоколом Telegram) и держать в одном потоке множество соединений.
Поддержка мультиверсионности схемы данных
Как известно, Telegram протокол описан в виде схемы на языке TL. Однако, на той же странице, мы можем получить схему и в формате JSON (которая на практике оказывается более применимой). В схеме два основных блока: constructors
и methods
. Первый описывает структуры данных, которые принимает на вход или возвращает Telegram сервер, а второй — описание методов: спецификацию типов входных параметров и тип ответа.
Несмотря на то, что сейчас протокол на сайте регулярно обновляется (в процессе написания статьи вышла новая версия TL-Schema 108, а за ней и 109), наблюдать за дельтой изменений между версиями по прежнему необходимо, для чего используется небольшой сниппет, принимающий на вход два json файла и выдающий на выходе конструкторы/методы, которые есть во втором файле, но нет в первом. На текущий момент в официальном API описано более 1100 дескрипторов (конструкторов/методов). Пользуясь такой json схемой нетрудно составить или декодировать любое сообщение.
Поддержка клиента, поддерживающего разные версии протокола, требует определенных плясок с бубном: недостаточно просто взять последнюю версию схемы с сайта. Надо поддерживать еще и старые сообщения (в обновлениях протокола бывает как удаление, так и модификация сообщений). Поэтому в нашей спецификации каждый слой (layer) мы сохраняем отдельно, чтобы прослеживать историю обновлений и чтобы бот-клиент, у которого, например, накопилась очередь сообщений на стороне сервера по старой версии протокола, мог корректно их обработать.
OSINT на примере некоторых сценариев
Большинство описанных ниже сценариев появилось в ходе покрытия внутренних нужд, однако некоторые сценарии были реализованы спонтанно в связи с обнаружением интересных функций протокола и его неочевидных мест, не реализованных либо глубоко скрытых в официальных клиентах.
Пойдем от простого к сложному и попробуем разобрать некоторые из существующих сценариев на примере реальных кейсов. Для более удобного взаимодействия со сценариями библиотека telegram-osint-lib была завернута в Docker:
docker build -t telegram-osint-lib .
docker run -d -t --name tg-osint-lib telegram-osint-lib
Преамбула: Генерируем бота-клиента
Перед тем как запустить содержимое Docker-контейнера и получить о пользователе первые данные, нам понадобится “зайти” в сеть Telegram. Для входа в сеть библиотека использует ботов (о которых уже было упомянуто выше), от имени которых производятся все действия.
Для генерации бота нам потребуется сценарий регистрации, реализующий связку auth.sendCode > auth.signIn > auth.signUp:
docker exec -i tg-osint-lib php examples/registration.php
Number: 790612***31
SMS code: 123123
На выходе получаем бота, готового к задачам OpenSource Intelligence:
AuthKey: 790612***31:aabbccdd...
Этот ключ(AuthKey) будет использоваться во всех дальнейших примерах следующим образом:
docker exec --env BOT=... -i tg-osint-lib php ...
Ниже в примерах, для краткости, бот будет указываться так: --env BOT=...
Собираем сведения о владельце номера
Начать мы решили с простого сценария — поиска пользователя Telegram по номеру телефона. Данный сценарий довольно очевидный и доступный для рядовых пользователей, однако его массовое применение несколько затруднительно “из коробки“.
Предположим, у вас есть ряд номеров, о владельцах которых вы хотели бы собрать информацию минимальными усилиями. Сеть Telegram в этом случае позволит собрать следующую информацию о телефонном номере:
Ник (который многие пользователей могут использовать и в других местах)
Фото
Общие чаты
Имя/Фамилия
“О себе”
Последнее время пребывания в Telegram
Предпочтительный язык
Этой информации уже достаточно для составления начального профиля владельца номера. Мы будем использовать сценарий из telegram-osint-lib, который будет получать на входе список телефонных номеров, пробегать по ним и запрашивать для каждого номера соответствующий аккаунт на сервере Telegram. Если аккаунт есть, мы получаем его данные. Если же аккаунт не найден, сохраняем номер телефона в отдельный список, которые будет обрабатываться в дальнейшем вручную.
Запускаем сценарий на группе номеров:
docker exec --env BOT=... -i tg-osint-lib php examples/parseNumbers.php 7985****294,7985****977,7986****777,7986****252,7988****417,7999****169,7999****869,7999****053,7999****364,7999****916,7999****475,7999****959,7985****025,7985****343,7989****207,7916****668,7926****802 > numbersInfo.txt
Всю основную работу в недрах библиотеки выполняет метод InfoClient::getInfoByPhone()
, использующий связку API вызовов import_contacts->get_user_full->delete_contacts->get_user_full
. А обработка результатов происходит в функции обратного вызова, которой в параметрах передаётся модель с данными пользователя. Если у пользователя установлено фото профиля, изображение будет загружено в папку со скриптом примера, а в поле Photo
будет указано имя файла.
Следим за присутствием пользователя
Развивая предыдущий сценарий, нетрудно догадаться, что запрашивать информацию об интересующем пользователе можно периодически и каждый раз сохранять его статус присутствия в сети Telegram. Это позволит спустя некоторое время накопить достаточно данных, чтобы составить карту активности.
Составив несколько таких карт по разным пользователям, мы сможем попробовать установить корреляцию между их заходами с целью определить возможность активного общения между ними.
Запускаем уже другой сценарий на группе номеров и наблюдаем за ними некоторое время:
docker exec --env BOT=... -i tg-osint-lib php examples/monitorNumbers.php 97155******9,...,798*****777 presence_map.txt
На выходе сценария мы получим ASCII-карту (где “+“ — состояние online в секунду времени) взаимоприсутствия интересующих пользователей в сети. Из карты видно, что вероятность активного общения между пользователями 2,4,5,9 более вероятна, чем между всеми остальными:
Откуда HackerNews черпает свои новости?
В Telegram популярны каналы новостного типа — рассылающие анонсы новостей, статей и прочих тематических постов. Но многие ли обращают внимание, откуда популярные каналы черпают информацию?
Возьмём относительно популярный канал HackerNews. Нас интересует список ресурсов, новости из которых чаще всего постились в группе за последний месяц.
В библиотеке реализовано два похожих метода для работы с сообщениями групп: InfoClient::getChannelLinks()
и InfoClient::getChannelMessages()
. Работают они практически одинаково, за исключением того, что первый метод фильтрует сообщения и отбирает только те, в которых были размещены ссылки.
Попробуем собрать все ссылки за последние несколько месяцев с целью определить основные источники информации канала:
docker exec --env BOT=... -i tg-osint-lib php examples/parseChannelLinks.php https://t.me/HNews "2019-12-01 00:00:00"
Спустя некоторое время сценарий соберет статистику по доменам и на выходе мы увидим примерно следующие результаты:
Итого, получили источники по убыванию частотности:
habr.com (45%)
xakep.ru (44%)
threatpost.com (11%)
остальное (<1%)
Теперь тот, кто подписан на HackerNews, Xakep.ru и Habrahabr может задуматься, а не подписан ли он на что-то лишнее?
Самый большой болтун
Кроме анализа информационных источников группы, немалый интерес может представлять выборка самых активных участников тематической группы. Не секрет, что 20% участников группы производят 80% активности этой группы. Получив такую информацию, можно определить наиболее приоритетных для группы людей и наладить контакт именно с ними.
Определять топ самых активных участников проще всего на промежутке времени или по числу последних сообщений (например, по последним 1000 сообщений). Собрать информацию о частоте сообщений участников группы нам поможет метод API messages.getHistory, используемый сценарием с другим целевым предназначением (сбор сообщений в группе), но кастомизируемый внешними инструментами. Запускаем очередной сценарий на одной из популярных групп с пост-фильтрацией средствами командной строки:
docker exec --env BOT=... -i tg-osint-lib php parseGroupMessages.php https://t.me/vityapelevin -- 1570207168 1580207168 --info head -n 2000 | ggrep -oP 'from [a-zA-Z0-9_]+ at' | sort | uniq -c | sort -r -n -k1 | awk '{print $1 " " $3 }' | head -n10
Что на выходе позволяет получить самых больших болтунов группы:
355 289336351
237 710806664
226 Yuliya04
216 735896305
187 Retrovertigodor
187 971662085
175 Mahmud_Abas
141 VwVwVoid
94 nikol_pelevina
85 kotenok_gaff
Общие интересы
Как можно расширить информацию о человеке, имея на руках его сетевой профиль, связанный с номером? Одной из примечательных функций Telegram является групповое тематическое общение. Все существующие чаты можно условно классифицировать по интересам (которых суммарно будет несколько сотен) и из каждого класса интересов выбрать несколько самых популярных чатов для поиска в них интересующего пользователя.
Ядром сценария, определяющего “карту интересов” пользователя, будет API-метод get_common_chats
, возвращающий список чатов, которые являются общими для бота и пользователя из наших контактов.
Итоговый алгоритм определения интересов пользователя будет следующим:
Добавить пользователя к себе в контакты
Подписать бота на множество популярных чатов, используя
join_channel
Получить информацию об общих с пользователем чатах, с помощью
get_common_chats
Сопоставить общие чаты с классами интересов
Для исполнения этого алгоритма на конкретном номере будет использоваться очередной сценарий, использующий внутри себя другой, уже задействованный ранее, сценарий поиска информации по номеру телефона:
docker exec --env BOT=... -i tg-osint-lib php examples/commonChats.php 7926****802
Изучая исходник примененного сценария можно обратить внимание на то, как происходит комбинирование простых сценариев в более сложные:
public function getCommonChats(?callable $callback = null)
{
$client = new UserContactsScenario([$this->phone], function (UserInfoModel $user) use ($callback) {
$this->infoClient->getCommonChats($user->id, $user->accessHash, 100, 0, function (AnonymousMessage $message) use ($callback) {
if (!Chats::isIt($message)) return;
$updates = new Chats($message);
foreach ($updates->getChats() as $chat) {
$this->commonChats[] = strtolower($chat->username);
}
...
});
});
$client->startActions(false);
}
По полученным общим чатам “вычисляются” классы интересов, к которым они относятся, подсчитывается количество общих чатов в каждой из категорий, и на выходе сценария получаем список интересов пользователя, упорядоченный по количеству чатов, а значит можем сделать вывод, какие из интересов наиболее предпочтительны пользователем:
Извлечение сообщений пользователя из публичных чатов и каналов
Продолжаем дальше искать новые источники открытой информации в Telegram. Какую информацию о пользователе мы смогли извлечь на данный момент:
Номер > Профиль пользователя
Профиль пользователя > Общие каналы с пользователем
Графические клиенты предоставляют возможность поиска по тексту сообщений, но также API поддерживает поиск сообщений конкретного пользователя. Самое время осуществить недостающий переход:
3. Общие каналы с пользователем > Сообщения пользователя в канале
Для этого нам поможет сценарий извлечения сообщений группы. В его основе лежит API-метод messages.getHistory. Для примера, выгрузим последние сообщения уже известного нам пользователя a_averyanova_m:
docker exec --env BOT=... -i tg-osint-lib php parseGroupMessages.php https://t.me/phuketrusa a_averyanova_m --info | head -n10
30.01.2020 13:26:17 | parseGroupMessages.php: starting group resolver for username: phuketrusa
30.01.2020 13:26:18 | TelegramOSINT\Scenario\GroupMessagesScenario: resolved user a_averyanova_m to 272425703
30.01.2020 13:26:19 | TelegramOSINT\Scenario\GroupMessagesScenario: got message 'Учиться мопед водить ?)))) \\ Ну приеду , проверю информацию )' from a_averyanova_m at 2020-01-30 12:25:48
30.01.2020 13:26:19 | TelegramOSINT\Scenario\GroupMessagesScenario: loading more messages, starting with 26451
30.01.2020 13:26:20 | TelegramOSINT\Scenario\GroupMessagesScenario: loading more messages, starting with 26332
30.01.2020 13:26:21 | TelegramOSINT\Scenario\GroupMessagesScenario: loading more messages, starting with 26219
30.01.2020 13:26:22 | TelegramOSINT\Scenario\GroupMessagesScenario: got message 'Я так поняла , тут вариантов не много, или ты катаешься без прав (наши тут с кат.авто не прокатят) и лишаешься страховки (если вдруг что)+платить штраф 500-1000бат, или идёшь и получаешь права в Тае, по времени это пару дней (когда как) и по деньгам явно дешевле, чем на лапу давать) мы по прилету , будем делать их на месте' from a_averyanova_m at 2020-01-29 14:38:40
30.01.2020 13:26:22 | TelegramOSINT\Scenario\GroupMessagesScenario: loading more messages, starting with 26099
30.01.2020 13:26:22 | TelegramOSINT\Scenario\GroupMessagesScenario: got message 'Спасибо' from a_averyanova_m at 2020-01-29 10:55:06
30.01.2020 13:26:22 | TelegramOSINT\Scenario\GroupMessagesScenario: got message 'Добрый день , еду на 2 месяца в Тай, кто какую страховку делал , при условии того, что буду ездить на байке без открытой категории. \\ И ещё , слышала, что можно открыть категорию ( получить их права) на месте , кто этим занимался, платить каждый раз по 1000бат тоже не самый классный вариант' from a_averyanova_m at 2020-01-29 10:09:10
Помимо возможностей пост-фильтрации, есть возможность выборки по диапазону дат. Пример фильтра (~с 04.10.2019 до 27.01.2020):
docker exec --env BOT=... -i tg-osint-lib php parseGroupMessages.php https://t.me/vityapelevin -- 1570207168 1580207168 --info | grep сновидения
28.01.2020 10:45:22 | TelegramOSINT\Scenario\GroupMessagesScenario: got message 'Да, там про всякие сверхвозможности, в искусстве сновидения вроде' from 735896305 at 2020-01-27 21:02:01
На этом этапе проведем небольшую ретроспективу собранных данных по одному из исходных номеров:
Первым шагом в разделе “Собираем сведения о владельце номера“ мы сопоставили профиль пользователя
a_averyanova_m
с реальным человеком, владеющим номером7926****802
Далее в разделе “Общие интересы“ определили возможные интересы человека на основе анализа общих групп (путешествия)
И, наконец, в этом разделе извлекли публичную переписку в одной из обнаруженных общих групп с ботом
Вместе вся эта информация позволяет сформировать достаточно полное представление об исходном владельце номера и даже о его планах на будущее.
Гео-разведка
Использование гео-позиции открывает интересные возможности для сбора информации, однако сама по себе гео-разведка является одним из тех направлений OSINT, разрушительный эффект от которых очевиден разработчикам по умолчанию и который они всяческими методами пытаются снижать. Разработчики Telegram здесь не стали исключением и так же постарались минимизировать возможные утечки, связанные с гео-локацией. В итоге весь потенциал направления сузился до нескольких API-методов: geochats.getLocated и contacts.getLocated, но и их в некоторых случаях может хватить для извлечения дополнительной информации о пользователе — например, где он чаще всего появляется в городе?
Завершающий сценарий, который мы рассмотрим в этой статье, позволяет определять потенциальные “места обитания” пользователя на определенной локации на основе поля гео-точек. В основе сценария будет лежать API-метод contacts.getLocated, который возвращает гео-чаты и контакты, находящиеся в определенном радиусе (эмпирическая оценка ~1 километр) от заданной гео-точки. Метод возвращает структуру Updates, благодаря которой мы можем реализовать мониторинг изменений в отслеживаемой группе пользователей.
Запускаем сценарий на поле из двух точек для конкретного пользователя:
docker exec --env BOT=... -i tg-osint-lib php geoSearch.php 55.753930,37.615714,55.756390,37.661931 b00k1ng 30 --info
...
29.01.2020 16:00:06 | TelegramOSINT\Scenario\GeoSearchScenario: found group 'Эдвард юил' near (55.753930, 37.615714)
29.01.2020 16:00:06 | TelegramOSINT\Scenario\GroupMembersScenario: searching chat 1404414249 participants for b00k1ng
29.01.2020 16:00:06 | TelegramOSINT\Scenario\GeoSearchScenario: found group 'Френдовская' near (55.753930, 37.615714)
29.01.2020 16:00:06 | TelegramOSINT\Scenario\GroupMembersScenario: searching chat 1404180655 participants for b00k1ng
29.01.2020 16:00:06 | TelegramOSINT\Scenario\GroupMembersScenario: chat 1211826903 contains user 883904218 with username b00k1ng
На основе результата можно делать предположения о том, в каких локациях пользователь гипотетически может появляться, что позволит сделать определенные выводы о его интересах и деятельности в реальной жизни.
Внутри сценарий, также как и некоторые другие, из рассмотренных выше, организован по принципу композируемости: сам сценарий GeoSearchScenario
, по сути, выполняет один запрос, а дальнейшая работа по проверке участников делегируется сценарию GroupMembersScenario
:
$groupHandler = function (GeoChannelModel $model) use (&$generator, &$finders, $username) {
$membersFinder = new GroupMembersScenario(
$model->getGroupId(),
null,
$generator,
100,
$username
);
$membersFinder->startActions(false);
$finders[] = $membersFinder;
};
$search = new GeoSearchScenario($points, $groupHandler, $generator, $limit);
$search->startActions();
Заключение
В этой статье на примере библиотеки сценариев telegram-osint-lib мы разобрали ряд сценариев OSINT-направленности в сети Telegram. Как можно было заметить, разобранные “утечки“ OSINT являются издержками исходных бизнес-требований к функционалу, а потому не могут быть устранены легко. Наверное, это одна из тех причин, по которой направление разведки по открытым источникам будет существовать постоянно в том или ином виде — это что-то вроде шума в электронных цепях, к которому все уже давно привыкли: ослабить эффект возможно, но устранить полностью экономически невыгодно.
Мы шли от более простых сценариев к более сложным, встраивая и комбинируя сценарии друг с другом. Структура библиотеки позволяет легко комбинировать как методы, так и сценарии между собой в нужную цепочку вызовов, получая на выходе необходимый сценарий. А асинхронный механизм работы уменьшает зависимость различных методов друг от друга и позволяет реализовать несколько одновременных соединений, позволяя масштабировать сценарии.
Мы рассмотрели ряд примеров, работающих автономно, без какого-либо “состояния” (State), однако добавление к сценариям состояния (до чего у нас пока не дошли руки), позволит вытворять еще более интересные штуки в сфере OSINT. Например обнаруженная в августе 2018 года “уязвимость“, позволяющая определять номер телефона по никнейму пользователя была не уязвимостью, а ничем иным, как OSINT-сценарием с наличием состояния: кто-то начал массово искать пользователей по номеру телефона, собирая базу данных (State) вида “пользователь->номер“, исходя из структуры которой определение телефона по никнейму являлось тривиальной операцией.
В завершение хотелось бы отметить, что Telegram API непрерывно развивается, открывая с каждым API Layer все новые возможности не только для OSINT-исследователя, поэтому любой желающий может присоединиться к разработке библиотеки сценариев и пополнять её новыми сценариями, скрывающими хитросплетения API-вызовов и решающими любые прикладные задачи в сети Telegram.
Bo0oM
По поводу @HNews, там раньше было множество источников типа securitylab, cnews и еще парочка менее популярных ресурсов, но из-за слишком одинаковых новостей и (или) низкого качества их всех повыпиливали :)