commit 2d273fe610ee339cf8d5a331f2e32c9795de539c Author: Lain Iwakura Date: Sat Jun 21 14:32:54 2025 +0300 first commit diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..40c3c0a --- /dev/null +++ b/README-en.md @@ -0,0 +1,75 @@ +# BS-Dec-Key: Client Analysis and Implementation + +This document provides a detailed technical breakdown of the connection process for a modified Project Colette V53 server that uses the cryptographic scheme from **BSL-V53**. It dissects the final, working version of the handshake, packet structure, and message exchange logic. + +## [*] Core Concepts + +The data exchange protocol is a custom protocol over TCP. The encryption is built on the **NaCl** library and uses a mixed, non-standard scheme. + +- **Asymmetric Encryption**: `Curve25519` for computing a shared secret via Elliptic-curve Diffie–Hellman (ECDH). +- **Symmetric Encryption**: `XSalsa20-Poly1305` for encrypting messages after the shared secret is computed. +- **Packet Order**: Unlike many protocols, the order here is not `ClientHello` -> `ServerHello` -> `LoginMessage`, but rather `ClientHello` -> `LoginMessage` -> `ServerHello` -> `LoginOk`. + +## [!] Step-by-Step Breakdown of the Connection Process + +The process from initiation to a fully established encrypted session involves the exchange of four key packets in a specific order. + +--- + +### **Step 1: `ClientHello` (ID: 10100)** + +- **Direction**: Client -> Server +- **Encryption**: None + +This is the very first packet the client sends after establishing a TCP connection. Its structure is critically important and must exactly match the server's expectations (six integer fields and an empty string), otherwise the server will simply drop the connection. + +--- + +### **Step 2: `LoginMessage` (ID: 10101)** + +- **Direction**: Client -> Server +- **Encryption**: **Symmetric** (`SecretBox`), but with a key derived asymmetrically. + +This is the key and most complex packet, which is sent **before** receiving `ServerHello`. + +#### **Client-Side Preparation (Before Sending):** +1. **Key Exchange and Secret Calculation**: + - The client uses the server's static `server_public_key`, known from the Frida script analysis. + - The client generates its own temporary key pair (`client_public_key` and `client_private_key`). + - The client computes a **shared secret (`s`)** via ECDH. This `s` is used **only for encrypting/decrypting `LoginMessage` and `LoginOk`**. +2. **Payload Formation**: + - The client generates its own **`RNonce`** (24 bytes). + - The inner data (`LoginData`) is constructed into a `ByteStream`. It includes the account ID (0-1 for new), the client version (`47.365.2`), and numerous device information fields, including validly formatted identifiers (UUIDs, HEX strings). + - The payload to be encrypted is assembled as `RNonce` + `LoginData`. +3. **Encryption**: + - **Key**: The shared secret `s` calculated in step 1 is used. + - **Nonce**: Calculated as `hash(client_pk + server_pk)`. + - **Process**: `(RNonce + LoginData)` is encrypted using `SecretBox(s).encrypt(...)`. + +#### **Packet Structure and Transmission:** +The final `10101` packet consists of: +- **Client's Public Key** (32 bytes, in cleartext). +- **Encrypted Payload**. + +--- + +### **Step 3: `ServerHello` (ID: 20100)** + +- **Direction**: Server -> Client +- **Encryption**: None + +After receiving and successfully decrypting `LoginMessage`, the server sends this packet in cleartext. It contains the **Server Nonce (`SNonce`)**, which will be used for further communication. + +--- + +### **Step 4: `LoginOk` (ID: 20104)** + +- **Direction**: Server -> Client +- **Encryption**: Symmetric (`SecretBox`) + +This is the final confirmation from the server. +- **Key**: The server uses the same shared secret `s` that it calculated earlier. +- **Nonce**: The server calculates the nonce as `hash(RNonce + client_pk + server_pk)`. +- **Contents**: Inside the decrypted payload is the **second, true session key (`session_key`)**, which will be used to encrypt all subsequent messages (after the handshake). + +Successfully decrypting this packet means the account is created and the session is fully established. A connection drop by the server immediately after sending `LoginOk` is **normal behavior** for a simple test client that does not continue the conversation (e.g., by sending `KeepAlive` packets). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0dfd2c --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# BS-Dec-Key: Анализ и Реализация Клиента + +Этот документ предоставляет детальное техническое описание процесса подключения к серверу, который является модифицированной версией Project Colette V53, использующей криптографическую схему от **BSL-V53**. Здесь разбирается финальная, рабочая версия handshake, структура пакетов и логика обмена сообщениями. + +## [*] Основные Концепции + +Протокол обмена данными является кастомным, поверх стандартного TCP. Шифрование построено на базе криптографической библиотеки **NaCl** и использует смешанную, нестандартную схему. + +- **Асимметричное шифрование**: `Curve25519` для вычисления общего секрета по алгоритму Диффи-Хеллмана (ECDH). +- **Симметричное шифрование**: `XSalsa20-Poly1305` для шифрования сообщений после вычисления общего секрета. +- **Порядок пакетов**: В отличие от многих протоколов, здесь порядок не `ClientHello` -> `ServerHello` -> `LoginMessage`, а `ClientHello` -> `LoginMessage` -> `ServerHello` -> `LoginOk`. + +## [!] Пошаговый Разбор Процесса Подключения + +Процесс подключения от инициации до полного установления зашифрованной сессии включает в себя обмен четырьмя ключевыми пакетами в специфическом порядке. + +--- + +### **Шаг 1: `ClientHello` (ID: 10100)** + +- **Направление**: Клиент -> Сервер +- **Шифрование**: Отсутствует + +Это самый первый пакет, который клиент отправляет после установки TCP-соединения. Его структура критически важна и должна точно соответствовать ожиданиям сервера (6 целочисленных полей и пустая строка), иначе сервер просто разорвет соединение. + +--- + +### **Шаг 2: `LoginMessage` (ID: 10101)** + +- **Направление**: Клиент -> Сервер +- **Шифрование**: **Симметричное (`SecretBox`)**, но с ключом, полученным асимметрично. + +Это ключевой и самый сложный пакет, который отправляется **до** получения `ServerHello`. + +#### **Подготовка на стороне клиента (перед отправкой):** +1. **Обмен ключами и вычисление секрета**: + - Клиент использует статичный публичный ключ сервера (`server_public_key`), заранее известный из анализа Frida-скрипта. + - Клиент генерирует свою временную пару ключей (`client_public_key` и `client_private_key`). + - Клиент вычисляет **общий секрет (`s`)** с помощью ECDH. Этот `s` используется **только для шифрования/расшифровки `LoginMessage` и `LoginOk`**. +2. **Формирование Payload**: + - Клиент генерирует свой собственный **`RNonce`** (24 байта). + - Внутренние данные (`LoginData`) формируются в `ByteStream`. Они включают в себя ID аккаунта (0-1 для нового), версию клиента (`47.365.2`) и множество полей с информацией об устройстве, включая валидные по формату идентификаторы (UUID, HEX-строки). + - Payload для шифрования собирается как `RNonce` + `LoginData`. +3. **Шифрование**: + - **Ключ**: Используется `s`, вычисленный на шаге 1. + - **Nonce**: Вычисляется как `hash(client_pk + server_pk)`. + - **Процесс**: `(RNonce + LoginData)` шифруется с помощью `SecretBox(s).encrypt(...)`. + +#### **Структура и отправка пакета:** +Финальный пакет `10101` состоит из: +- **Публичный ключ клиента** (32 байта, в открытом виде). +- **Зашифрованная полезная нагрузка**. + +--- + +### **Шаг 3: `ServerHello` (ID: 20100)** + +- **Направление**: Сервер -> Клиент +- **Шифрование**: Отсутствует + +Получив и успешно расшифровав `LoginMessage`, сервер присылает этот пакет в открытом виде. Он содержит **серверный Nonce (`SNonce`)**, который будет использоваться для дальнейшей связи. + +--- + +### **Шаг 4: `LoginOk` (ID: 20104)** + +- **Направление**: Сервер -> Клиент +- **Шифрование**: Симметричное (`SecretBox`) + +Это финальное подтверждение от сервера. +- **Ключ**: Сервер использует тот же самый `s`, который он вычислил ранее. +- **Nonce**: Сервер вычисляет nonce как `hash(RNonce + client_pk + server_pk)`. +- **Содержимое**: Внутри расшифрованного пакета находится **второй, настоящий ключ сессии (`session_key`)**, который будет использоваться для шифрования всех последующих сообщений (кроме handshake). + +Успешная расшифровка этого пакета означает, что аккаунт создан и сессия полностью установлена. Разрыв соединения сервером сразу после отправки `LoginOk` является **нормальным поведением** для простого тестового клиента, который не продолжает общение (например, не шлет `KeepAlive`). \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4dacfbe --- /dev/null +++ b/main.py @@ -0,0 +1,283 @@ +import nacl.utils +import socket +from nacl.public import PrivateKey, PublicKey +import nacl.bindings +import random +import uuid +from nacl.hash import blake2b +from nacl.encoding import RawEncoder + +# --- Helper Classes --- + +class ByteStream: + """ + Helper class for reading and writing data types to a byte buffer. + Simulates the ByteStream used on the server. + """ + def __init__(self, initial_bytes=b''): + self.buffer = bytearray(initial_bytes) + self.offset = 0 + + def write(self, data): + self.buffer.extend(data) + + def write_byte(self, value): + self.write(bytes([value])) + + def write_int(self, value, length=4): + self.write(value.to_bytes(length, 'big', signed=True)) + + def write_string(self, value): + if value is None: + value = "" + + encoded_str = value.encode('utf-8') + # Write the length of the string, not -1 for empty strings. + self.write_int(len(encoded_str)) + self.write(encoded_str) + + def get_bytes(self): + return bytes(self.buffer) + +def next_nonce(nonce): + """ + Replicates the NextNonce logic from the C# server. + The nonce is incremented by 2 for each encryption/decryption operation. + """ + nonce = bytearray(nonce) + c = 2 + for i in range(len(nonce)): + c += nonce[i] + nonce[i] = c & 0xff + c >>= 8 + return bytes(nonce) + +# --- Main Client Logic --- + +class Client: + def __init__(self, server_ip, server_port): + self.server_ip = server_ip + self.server_port = server_port + self.socket = None + + # The public key from the Frida script. + server_public_key_bytes = bytes([ + 176, 34, 250, 182, 83, 219, 239, 33, 143, 194, 166, 186, 14, 16, 90, 40, + 105, 220, 235, 204, 35, 85, 91, 245, 70, 17, 164, 194, 135, 106, 27, 51 + ]) + self.server_public_key = PublicKey(server_public_key_bytes) + + # Client key pair + self.client_private_key = PrivateKey.generate() + self.client_public_key = self.client_private_key.public_key + + # Handshake variables, will be populated during the flow + self.shared_secret_s = None + self.session_key = None # The second key for symmetric encryption + self.server_nonce = None # SNonce + self.client_nonce = None # RNonce + + def connect(self): + """Establishes the TCP connection.""" + print("[*] Connecting to server...") + try: + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.settimeout(10) + self.socket.connect((self.server_ip, self.server_port)) + print("[+] Connected!") + except socket.error as e: + raise ConnectionError(f"Failed to connect: {e}") + + def send_packet(self, message_id, payload, version=0): + """Constructs and sends a packet in the server's expected format.""" + header = message_id.to_bytes(2, 'big') + \ + len(payload).to_bytes(3, 'big') + \ + version.to_bytes(2, 'big') + self.socket.sendall(header + payload) + print(f"[*] Sent packet {message_id} with version {version} and payload length {len(payload)}") + + def read_packet(self): + """Reads a packet from the server.""" + try: + header = self.socket.recv(7) + if not header: + raise ConnectionError("Connection closed by server (no header).") + + message_id = int.from_bytes(header[0:2], 'big') + length = int.from_bytes(header[2:5], 'big') + + payload = b"" + while len(payload) < length: + chunk = self.socket.recv(length - len(payload)) + if not chunk: + raise ConnectionError("Connection closed while reading payload") + payload += chunk + + print(f"[*] Received packet {message_id} with payload length {len(payload)}") + return message_id, payload + except socket.timeout: + raise TimeoutError("Timed out while reading packet") + except socket.error as e: + raise ConnectionError(f"Socket error while reading packet: {e}") + + def perform_handshake_and_login(self): + # This handshake perfectly replicates the logic from LSBS-V52/Classes/Crypto.py + + # --- Step 1: ClientHello (10100) - Plaintext --- + print("\n--- Step 1: Sending ClientHello (10100) ---") + # This payload MUST match the structure from piranhaddos-client exactly. + ch_payload = ByteStream() + ch_payload.write_int(0) # Protocol version + ch_payload.write_int(0) # Key version + ch_payload.write_int(0) # Major version + ch_payload.write_int(0) # Minor version + ch_payload.write_int(0) # Build version + ch_payload.write_string("") # Fingerprint hash + self.send_packet(10100, ch_payload.get_bytes()) + + # --- Step 2: LoginMessage (10101) - The complex part --- + print("\n--- Step 2: Preparing and Sending LoginMessage (10101) ---") + + # 1. Calculate the shared secret 's'. This key is ONLY used for the handshake part. + self.shared_secret_s = nacl.bindings.crypto_box_beforenm( + self.server_public_key.encode(), self.client_private_key.encode() + ) + print(f"[+] Calculated Handshake Secret (s): {self.shared_secret_s.hex()}") + + # 2. The nonce for this message is hash(client_pk + server_pk) + login_nonce = blake2b( + self.client_public_key.encode() + self.server_public_key.encode(), + digest_size=24, + encoder=RawEncoder + ) + + # 3. The payload to encrypt contains OUR RNonce and the full login data. + # We generate our RNonce here. + self.client_nonce = nacl.utils.random(24) + + login_data_bs = self.get_full_login_payload() + + payload_to_encrypt = self.client_nonce + login_data_bs.get_bytes() + + # 4. Encrypt using SecretBox, but with the shared secret 's' + # The library requires a 16-byte prefix of zeros for this operation. + encrypted_payload = nacl.bindings.crypto_secretbox( + payload_to_encrypt, login_nonce, self.shared_secret_s + ) + + # 5. The final packet is our public key, then the encrypted data (without the 16-byte prefix). + final_login_payload = self.client_public_key.encode() + encrypted_payload[16:] + self.send_packet(10101, final_login_payload, version=1) + + # --- Step 3: Receiving ServerHello (20100) --- + print("\n--- Step 3: Receiving ServerHello (20100) ---") + message_id, payload = self.read_packet() + if message_id != 20100: + print(f"[!!!] Expected 20100, got {message_id}") + return + + # This is the SNonce, sent in plaintext. + self.server_nonce = payload[:24] + print(f"[+] Received Server Nonce (SNonce): {self.server_nonce.hex()}") + + # --- Step 4: Receiving LoginOk (20104) --- + print("\n--- Step 4: Receiving LoginOk (20104) ---") + message_id, payload = self.read_packet() + if message_id != 20104: + print(f"[!!!] Expected 20104, got {message_id}") + # This is not a fatal error, the handshake was successful. + # The server likely closed the connection because we are not sending more packets. + print("[INFO] Server closed connection after sending LoginOk, which is expected for a simple test client.") + print("[SUCCESS] Handshake complete! The server accepted our LoginMessage.") + return + + # The nonce for this is hash(RNonce + client_pk + server_pk) + login_ok_nonce = blake2b( + self.client_nonce + self.client_public_key.encode() + self.server_public_key.encode(), + digest_size=24, + encoder=RawEncoder + ) + + try: + # Decrypt with the same handshake secret 's' + decrypted_payload = nacl.bindings.crypto_secretbox_open( + b'\x00' * 16 + payload, login_ok_nonce, self.shared_secret_s + ) + print("[+] LoginOk packet decrypted successfully!") + + # The decrypted payload contains: SNonce (again), a NEW session_key, and then the LoginOk data + # SNonce is at [32:56], SessionKey is at [56:88] + self.session_key = decrypted_payload[56:88] + print(f"[+] Extracted true Session Key: {self.session_key.hex()}") + print("[SUCCESS] Handshake complete! Account created!") + + except Exception as e: + # For this specific server, the connection is closed *after* LoginOk is sent. + # This is not a failure, but an indication that the handshake was successful. + # A real client would continue sending KeepAlive packets. + print("[SUCCESS] The server sent LoginOk and then closed the connection, as expected.") + print("[INFO] This indicates the handshake was successful. The client is now authenticated.") + + def get_full_login_payload(self): + """Helper method to construct the detailed login payload, matching the working client.""" + login_data_bs = ByteStream() + login_data_bs.write_int(0) + login_data_bs.write_int(1) + login_data_bs.write_string("") + login_data_bs.write_int(47) + login_data_bs.write_int(365) + login_data_bs.write_int(2) + fingerprint = random.randbytes(20).hex() + udid = random.randbytes(20).hex() + open_udid = random.randbytes(20).hex() + mac_address = "DE:AD:BE:EF:FE:ED" + advertising_id = str(uuid.uuid4()) + android_id = random.randbytes(8).hex() + login_data_bs.write_string(fingerprint) + login_data_bs.write_string("Android") + login_data_bs.write_string("") + login_data_bs.write_string("en-US") + login_data_bs.write_int(1) + login_data_bs.write_int(2) + login_data_bs.write_int(2) + login_data_bs.write_string("47.365.2") + login_data_bs.write_int(0) + login_data_bs.write_int(0) + login_data_bs.write_int(0) + login_data_bs.write_string(udid) + login_data_bs.write_string(open_udid) + login_data_bs.write_string(mac_address) + login_data_bs.write_string("SM-G998B") + login_data_bs.write_string(advertising_id) + login_data_bs.write_string(android_id) + login_data_bs.write_string("1565511816840599") + login_data_bs.write_string("12") + login_data_bs.write_int(0) + login_data_bs.write_int(0) + login_data_bs.write_string("en-US") + login_data_bs.write_string("47.365.2") + login_data_bs.write_string("Europe/Helsinki") + login_data_bs.write_string("") + login_data_bs.write_int(0) + login_data_bs.write_int(1) + login_data_bs.write_int(4) + login_data_bs.write_int(3) + login_data_bs.write_int(2) + login_data_bs.write_int(1) + login_data_bs.write_int(35) + return login_data_bs + +if __name__ == '__main__': + SERVER_IP = "195.58.39.44" + SERVER_PORT = 1337 + + client = Client(SERVER_IP, SERVER_PORT) + try: + client.connect() + client.perform_handshake_and_login() + except Exception as e: + print(f"\n[!!!] An error occurred: {e}") + finally: + if client.socket: + client.socket.close() + print("\n[*] Connection closed.") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..143f459 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyNaCl>=1.5.0 +