Todo lo necesario para reproducir un login completo de Carcelclient.exe: el magic header, el saludo, la clave, el cifrado, cómo se empaquetan los datos, el mensaje LOGIN y la lista de opcodes — byte a byte, y explicado para novatos.
Todo viaja por UDP (mensajes sueltos, sin conexión previa). El juego habla con tres «ventanillas», cada una en su puerto:
Identidad + lista de cárceles. Aquí ocurre TODO este documento.
Comprueba si el cliente está al día.
El juego: moverte, salas, combate.
En el código: PUERTO_LOGIN = 25666, PUERTO_UPDATES = 25667, PUERTO_MUNDO = 25001. El login entero (handshake + autenticación + selección de cárcel) ocurre en el 25666. Solo al pulsar «jugar» el cliente salta al mundo (25001).
Todo paquete (protocolo SNS) empieza con esta cabecera de 22 bytes (0x16). Todos los enteros van en little-endian. Detrás va el payload.
MAGIC: 0xFFFFFFFF en el handshake; 0x00000001 en la fase de datos.
Id de conexión que asigna el servidor (en fase de datos).
bit0=pide-ACK · bit1=es-ACK · bit2=lleva-datos (≤7).
Ventana de envío. Distinto de 0.
Hasta qué mensaje va confirmado (< ventana).
Id de mensaje; el ACK lo repite. En el SYN-ACK lleva f2 (clave).
Timestamp; en el SYN-ACK lleva f1 (clave).
Nº de secuencia de esta trama.
Contenido. En claro durante el handshake; cifrado (XOR) en la fase de datos.
El campo flags es un mapa de bits: cada bit es un sí/no independiente y el valor final es la suma (OR) de los que estén activos. Por eso siempre vale ≤ 7 (solo se usan 3 bits).
| Constante | Bit | Valor | Significado |
|---|---|---|---|
| FLAG_PIDE_ACK | bit0 | 1 | El emisor pide que le confirmes esta trama. |
| FLAG_ES_ACK | bit1 | 2 | Esta trama ES una confirmación (un ACK). |
| FLAG_LLEVA_DATOS | bit2 | 4 | Lleva datos de aplicación en el payload. |
Ejemplos: flags = 5 → 1 + 4 = PIDE_ACK | LLEVA_DATOS (datos que hay que confirmar) flags = 2 → = ES_ACK (un ACK puro, sin datos) flags = 0 → = (nada) (control suelto)
🧠 En cristiano: Tres casillas que se marcan o no: «confírmame», «esto es tu acuse» y «llevo carta dentro». Un número las resume todas: 4+1 = 5 significa «llevo carta y quiero acuse».
Lo que distingue un paquete de saludo de uno de datos es el «número mágico» en 0x00: si connID_lo == 0xFFFFFFFF, es handshake. En ese caso, el payload (desde 0x16) trae una sub-cabecera fija:
Paquete de HANDSHAKE (payload en claro): 0x16 u16 tipo 3=SYN · 6=SYN-ACK · 7=ACK · 4=FIN · 5=FIN-ACK 0x18 u16 const SIEMPRE 4 ← el "magic" de la sub-cabecera 0x1a u32 nonce aleatorio del cliente (el servidor lo repite) 0x1e ... datos en el SYN: usuario en ASCII terminado en '\0'
🧠 En cristiano: Dos «sellos» marcan que esto es un saludo y no datos: connID_lo = 0xFFFFFFFF (todos unos) y la constante 4 en 0x18. Si no están, el receptor lo trata como datos cifrados.
Estos son los nombres exactos que usa el emulador (protocolo_sns.hpp). Se dividen en dos grupos: la cabecera común (0x00–0x15, en todos los paquetes) y la sub-cabecera de handshake (0x16+, solo en el saludo; en la fase de datos, desde 0x16 ya va el payload cifrado).
| Constante | Offset | Tipo | Significado |
|---|---|---|---|
| TAM_CABECERA | 0x16 | — | Tamaño de la cabecera común: 22 bytes. |
| OFF_CONN_LO | 0x00 | u32 | connID_lo. MAGIC: 0xFFFFFFFF handshake, 1 en datos. |
| OFF_CONN_HI | 0x04 | u32 | connID_hi = id de conexión asignado. |
| OFF_FLAGS | 0x08 | u16 | flags: bit0 pide-ACK, bit1 es-ACK, bit2 lleva-datos. |
| OFF_VENTANA | 0x0a | u8 | ventana de envío (≠ 0). |
| OFF_ACK | 0x0b | u8 | ack: hasta qué mensaje va confirmado. |
| OFF_CAMPO_C | 0x0c | u32 | id de mensaje; en el SYN-ACK lleva f2 (clave). |
| OFF_MARCA_TIEMPO | 0x10 | u32 | timestamp; en el SYN-ACK lleva f1 (clave). |
| OFF_SECUENCIA | 0x14 | u16 | nº de secuencia de la trama. |
| OFF_PAYLOAD | 0x16 | — | Inicio del payload (mismo offset que OFF_TIPO). |
| OFF_TIPO | 0x16 | u16 | tipo de paquete: 3/6/7/4/5 (solo handshake). |
| OFF_CONST | 0x18 | u16 | constante = 4 (2º sello de handshake). |
| OFF_NONCE | 0x1a | u32 | nonce del cliente (el servidor lo repite). |
| OFF_DATOS | 0x1e | … | datos tras el nonce (p. ej. el usuario en el SYN). |
Detalle clave: OFF_PAYLOAD y OFF_TIPO valen lo mismo (0x16). No es un error: el payload arranca justo ahí, y en un paquete de saludo el primer dato del payload es precisamente el campo tipo. En un paquete de datos, ese mismo 0x16 es el primer byte cifrado.
🧠 En cristiano: Los primeros 22 bytes (0x00–0x15) son el sobre que llevan todas las cartas. Los campos tipo/const/nonce/datos solo existen cuando la carta es un «saludo»; en una carta normal, ahí empieza directamente el contenido cifrado.
El cliente abre la conexión y manda su usuario tras el nonce (en 0x1e).
connID_lo=FFFFFFFF connID_hi=FFFFFFFF flags=… 0x16 tipo=3 0x18 const=4 0x1a nonce=NNNN 0x1e "innapmine\0"
El servidor asigna un idConexión y esconde la clave K en los campos campoC (f2) y marcaTiempo (f1). Repite seq y nonce del cliente:
connID_lo=FFFFFFFF connID_hi=FFFFFFFF flags=0 ventana=1 ack=0 0x0c campoC = f2 ← trozo de clave 0x10 marca = f1 ← trozo de clave 0x14 seq = <eco> 0x16 tipo=6 0x18 const=4 0x1a nonce=<eco> 0x1e u32 idConexión ← tu número de conexión
El cliente confirma. Conexión establecida. A partir de aquí, la fase de datos.
(Cierre: el cliente manda tipo 4 (FIN) y el servidor responde tipo 5 (FIN-ACK).)
| Constante | Valor | Dirección | Qué es |
|---|---|---|---|
| TIPO_SYN | 3 | cliente → | Abre la conexión (lleva el usuario). |
| TIPO_SYN_ACK | 6 | ← servidor | Acepta: asigna connID + material de clave. |
| TIPO_ACK | 7 | cliente → | Confirma. Conexión ESTABLECIDA. |
| (FIN) | 4 | cliente → | Pide cerrar la conexión. |
| (FIN-ACK) | 5 | ← servidor | Confirma el cierre. |
Y el marcador que decide si un paquete es handshake: CONN_HANDSHAKE = 0xFFFFFFFF — el valor que lleva connID_lo (0x00) durante todo el saludo. En cuanto la conexión se establece, ese campo pasa a valer 1 y estos «tipos» dejan de existir (desde 0x16 ya va el payload cifrado).
Con la clave K, el timestamp TS y la máscara M = 0x7e34fc22, el servidor calcula dos campos y los mete en el SYN-ACK:
Servidor (mete en el SYN-ACK): f1 = ((TS ^ K) & M) ^ K → va en 0x10 (marcaTiempo) f2 = ((TS ^ K) & M) ^ TS → va en 0x0c (campoC) Cliente (recupera la clave): K = ((f1 ^ f2) & M) ^ f1
Es una constante de 32 bits grabada a fuego en el cliente (constexpr uint32_t MASCARA_CLAVE = 0x7e34fc22u). No es un número calculado ni tiene significado matemático: es un simple patrón de bits que los autores del juego eligieron, y que se sacó del binario por ingeniería inversa. Lo único que importa es que cliente y servidor usen exactamente el mismo.
0x7e34fc22 en binario:
0111 1110 0011 0100 1111 1100 0010 0010
0x7e 0x34 0xfc 0x22
El AND (&) con M deja pasar solo los bits en 1 y pone a 0 el resto.Por qué funciona (el álgebra sale sola). Llamando A = (TS ^ K) & M:
f1 = A ^ K f2 = A ^ TS f1 ^ f2 = K ^ TS (el trozo A se cancela consigo mismo) (f1 ^ f2) & M = A (volvemos a filtrar por la máscara) A ^ f1 = A ^ (A ^ K) = K ✔ recuperada la clave
Fíjate en que f1 ^ f2 siempre da K ^ TS, que un espía SÍ puede calcular. La pieza que le falta es la máscara: sin conocer M no puede dar el último paso y despejar K. Por eso la máscara es, de hecho, el secreto compartido de todo el handshake.
🧠 En cristiano: La máscara es un «colador» de bits que los dos ya traen de fábrica. No es cripto de banco: es una «clave de silbato» para que un curioso con un sniffer no lea los mensajes de un vistazo. Ambos lados acaban con la misma K de 4 bytes.
Ya establecida la conexión, el payload viaja cifrado. Es un XOR encadenado con la clave de 4 bytes k[0..3], la posición i y el byte anterior (semilla 0x3b):
k[4] = bytes de K (little-endian)
prev = 0x3b
para cada byte i:
salida = k[(i>>2) & 3] ^ entrada[i] ^ (i & 0xFF) ^ prev
prev = salida ← al CIFRAR se encadena con el byte de salida
(al DESCIFRAR, con el byte de entrada)🧠 En cristiano: Cada byte se mezcla con un trozo de la clave, con su posición y con el byte de al lado. Es reversible: el otro extremo deshace la mezcla y lee el mensaje.
El contenido de aplicación no va suelto: se envuelve en registros con su longitud delante, y la lista termina con un registro de longitud 0. En el código lo arma componerMensajeApp(salida, idConexión, datosApp, n). El cliente exige justo este formato:
payload = [idConexión:4][1:4] + M M = [longReg:4][registro] … [0:4] ← el 0 final = "fin" registro = [idConexión:4][1:4][datosApp] (longReg = 8 + len(datosApp)) datosApp = [opcode:2][ …campos… ] → El OPCODE de aplicación queda en el offset 0x14 del payload descifrado.
🧠 En cristiano: Sobres dentro del sobre grande, cada uno etiquetado con su tamaño, y un sobre vacío al final que dice «hasta aquí».
El primer mensaje de aplicación del cliente. Los offsets son relativos al payload descifrado:
0x14 u16 opcode = 0x1388 (LOGIN) 0x16 u16 ? (campo previo) 0x18 u16 longitud del bloque de credenciales 0x1a ... bloque credenciales cifradas (longitud bytes)
El «bloque» no es un hash: es el texto usuario \0 CONTRASEÑA \0 \0 (opcionalmente con un \0 PIN) tapado con un keystream XOR de 4 bytes que se repite:
cifrado[i] = claro[i] ^ KS[i % 4]
Como el servidor YA conoce el usuario (llegó en el SYN), despeja el
keystream con el propio mensaje:
KS[i % 4] = cifrado[i] ^ usuario[i]
…y con eso descifra la contraseña. Funciona con cualquier sesión.🧠 En cristiano: La contraseña va «tapada», pero como el nombre de usuario viajó antes en claro, el servidor usa esas letras conocidas para averiguar el patrón y destapar el resto.
El orden exacto de mensajes de aplicación, tal como lo procesa el servidor (→ cliente envía · ← servidor responde):
Cliente manda usuario+contraseña. Si algo falla, el servidor responde LOGINRECHAZADO (0x138a) con un código y se acabó.
Si va bien, el servidor envía la estructura de CUENTA (0x1389, con las 3 ranuras de personaje) y la lista de nombres de cárceles SERVERADDED (0x13a9).
El cliente empieza a mandar «latidos» cada ~5 s. El primero desbloquea el paso 4.
El servidor confirma con LOGINACCEPTED (0x13d5) (count=0) y manda CLASSINFO (0x13c2) (tabla de delitos/clases para la pantalla de personaje).
Al abrir la lista, el cliente pide GETAVAILABLESERVERS (0x13aa) y el servidor responde AVAILABLESERVERS (0x13ac) con el nº de reclusos.
El cliente pulsa jugar (JUGAR 0x13f1); el servidor concede ENTERINGGAMEACCEPTED (0x13f2) y el cliente se conecta al mundo (25001). Fin del login.
Detalle del desfase +1: el cliente hace dec al recibir, así que el servidor ENVÍA la etiqueta del manejador (p. ej. cuenta = 0x1389, jugar =0x13f2).
[u16 0x13d5][u32 count=0] ← forma mínima que desbloquea el login
[u16 0x138a][u8 código] (+[u32 timestamp] si código == 7)
[u16 0x13a9][u16 count] por cada prisión: [u32 id][u8 flag][u32 extra][nombre \0][nombre2 \0]
[u16 0x13ac][u8 count][u32 dataLen][data] data, por cada prisión: [u32 id][u8 nMod][ nMod × u16 reclusos_parciales ] (la SUMA de los u16 = nº de reclusos de esa prisión)
La estructura de CUENTA (0x1389) es la más grande (parte fija de 0x9cd bytes + 3 ranuras de personaje + inventario) y se envía fragmentada; se documenta aparte por su tamaño.
| 0x1388 | LOGIN | Inicia sesión (usuario + contraseña) |
| 0x13a1 | LATIDO | Heartbeat, cada ~5 s |
| 0x13aa | GETAVAILABLESERVERS | Pide la lista de cárceles |
| 0x139f | SELECCIONAR | Entra con un personaje |
| 0x1394 | CREAR_PERSONAJE | Crea personaje (nick + atributos) |
| 0x1393 | BORRAR_PERSONAJE | Borra el personaje de una ranura |
| 0x13f1 | JUGAR | Pide entrar al mundo |
| 0x1389 | CUENTA | Estructura de cuenta + personajes |
| 0x138a | LOGINREJECTED | Login rechazado (con código) |
| 0x13d5 | LOGINACCEPTED | Login aceptado |
| 0x13a9 | SERVERADDED | Nombres de las cárceles |
| 0x13ac | AVAILABLESERVERS | Reclusos por cárcel |
| 0x13c2 | CLASSINFO | Tabla de clases/delitos |
| 0x1395 | CHARCREATED | Personaje creado |
| 0x13f2 | ENTERINGGAMEACCEPTED | Permiso para entrar al mundo |
Esta wiki describe el protocolo reconstruido por ingeniería inversa del cliente. Los offsets y opcodes están tomados del emulador; pueden afinarse según se documente más.