
Cryptohack Challenges: General Encoding
Siguiendo con la serie de resolución de ejercicios de Cryptohack, continuamos con los ejercicios del apartado General.
Encoding
El propósito de este ejercicio es aprender acerca de ASCII
, como paso básico en prácticamente todos los sistemas de cifrado moderno.
Nos facilitan la siguiente lista de números, que se deberán de convertir a carácteres ASCII
:
[99, 114, 121, 112, 116, 111, 123, 65, 83, 67, 73, 73, 95, 112, 114, 49, 110, 116, 52, 98, 108, 51, 125]
Además nos dicen que podemos utilizar tanto la función chr()
un número a carácter y la función ord()
para realizar la conversión opuesta.
Veamos cómo lo resolvemos.
Primero introduciremos la lista tal cual se nos presenta como valor de una variable:
numbers = [99, 114, 121, 112, 116, 111, 123, 65, 83, 67, 73, 73, 95, 112, 114, 49, 110, 116, 52, 98, 108, 51, 125]
Crearemos también una variable que será la que contenga el mensaje en texto.
Tras esto, necesitaremos recorrer la lista de números y transformarlo en carácteres con la función chr()
, para esto utilizaremos un bucle for, y añadiremos los carácteres utilizando el operador de adhesión +=
.
for n in numbers:
message += chr(n)
Imprimimos el mensaje en la terminal con print()
, el script entero sería:
#!/bin/env python
numbers = [99, 114, 121, 112, 116, 111, 123, 65, 83, 67, 73, 73, 95, 112, 114, 49, 110, 116, 52, 98, 108, 51, 125]
message = ""
for n in numbers:
message += chr(n)
print(message)
Hex
Este es en realidad similar al anterior, sólo que en vez de una lista de números, tenemos una lista (aunque no presentada como lista) hexadecimal.
En esta tarea se deberá recorrer la cadena de texto en pares de carácteres, transformar el valor hexadecimal a carácter, y concatenar el carácter a una variable que contendrá el mensaje.
En la descripción del ejercicio nos sugieren que podemos utilizar las funciones bytes.fromhex()
para pasar la cadena de hexadecimal a bytes, y .hex()
para pasar de byte a hexadecimal.
En realidad pensé que iba a ser más complicado, pero tras resolverlo creo que aquí Python hace que realmente este ejercicio sea muy trivial. Seguramente con otros lenguajes sería algo más complicado.
Vamos a ver cómo lo resolvemos.
Introducimos la lista en una variable, y crearemos una variable que contendrá el mensaje en texto claro:
hex_string = '63727970746f7b596f755f77696c6c5f62655f776f726b696e675f776974685f6865785f737472696e67735f615f6c6f747d'
message = ''
En este punto, me planteé realizar un bucle for utilizando la función range()
, marcando como máximo la longitud de la variable hex_string
, y después hacer un slicing de la variable para ir extrayendo carácteres de dos en dos, convertirlos a números enteros, etc.
Pero que va, utilizamos la función bytes.fromhex()
propuesta por el enunciado del ejercicio:
bytes.fromhex(hex_string)
Al ser tódo carácteres imprimibles, creo que Python entiende que se trata de una cadena de texto, por lo que podemos imprimir directamente el resultado. La solución podría quedar así:
#!/bin/env python
hex_string = '63727970746f7b596f755f77696c6c5f62655f776f726b696e675f776974685f6865785f737472696e67735f615f6c6f747d'
message = bytes.fromhex(hex_string)
print(str(message))
Base 64
En este ejercicio, se deberá de transformar una cadena de texto en base 64 decodificándola a texto plano, pasándolo antes a bytes. Para ello, el enunciado del ejercicio nos propone utilizar base64.b64encode()
de la librería base64
.
Veamos qué solución le damos.
Primeramente, pondremos la cadena de texto en base64 como valor de una variable:
b64 = '72bca9b68fc16ac7beeb8f849dca1d8a783e8acf9679bf9269f7bf'
Pasaremos el valor a un array de bytes con la función vista en el ejercicio anterior, bytes.fromhex()
. Tras eso, utilizaremos el método base64.b64encode()
para convertir del array de bytes a una cadena de carácteres.
La resolución quedaría tal que así:
#!/bin/env python
import base64
b64 = '72bca9b68fc16ac7beeb8f849dca1d8a783e8acf9679bf9269f7bf'
b64_bytes = bytes.fromhex(b64)
message = base64.b64encode(b64_bytes)
print(message)
Bytes and Big Integers
En este ejercicio, debemos convertir un gran número de vuelta a un mensaje en texto claro, entendemos que no está cifrado si no que es un tema puramente de representación (codificación).
Nos sugieren que utilicemos los métodos bytes_to_long()
y long_to_bytes()
, pero nos advierten que antes deberemos de importar la librería Crypto
con from Crypto.Util.number import *
.
Primero se deberá instalar la librería, dentro de un entorno virtual:
python -m venv .venv/
source .venv/bin/activate
pip install PyCryptodome
Y ahora en el script, importamos la librería como nos indica el enunciado, y como de costumbre pondremos el valor a convertir en una variable:
from Crypto.Util.number import *
text = 11515195063862318899931685488813747395775516287289682636499965282714637259206269
Sólo queda realizar la conversión de número a array de bytes utilizando el método proporcionado en el enunciado:
#!/bin/env python
from Crypto.Util.number import *
text = 11515195063862318899931685488813747395775516287289682636499965282714637259206269
message = long_to_bytes(text)
print(message)
Encoding Challenge
En este último ejercicio se pondrá en práctica todo lo aprendido en los ejercicios anteriores. Automatizando los diferentes niveles, cien según el enunciado del ejercicio.
Nos permiten ver el código que está corriendo tras el servicio:
#!/usr/bin/env python3
from Crypto.Util.number import bytes_to_long, long_to_bytes
from utils import listener # this is cryptohack's server-side module and not part of python
import base64
import codecs
import random
FLAG = "crypto{????????????????????}"
ENCODINGS = [
"base64",
"hex",
"rot13",
"bigint",
"utf-8",
]
with open('/usr/share/dict/words') as f:
WORDS = [line.strip().replace("'", "") for line in f.readlines()]
class Challenge():
def __init__(self):
self.no_prompt = True # Immediately send data from the server without waiting for user input
self.challenge_words = ""
self.stage = 0
def create_level(self):
self.stage += 1
self.challenge_words = "_".join(random.choices(WORDS, k=3))
encoding = random.choice(ENCODINGS)
if encoding == "base64":
encoded = base64.b64encode(self.challenge_words.encode()).decode() # wow so encode
elif encoding == "hex":
encoded = self.challenge_words.encode().hex()
elif encoding == "rot13":
encoded = codecs.encode(self.challenge_words, 'rot_13')
elif encoding == "bigint":
encoded = hex(bytes_to_long(self.challenge_words.encode()))
elif encoding == "utf-8":
encoded = [ord(b) for b in self.challenge_words]
return {"type": encoding, "encoded": encoded}
#
# This challenge function is called on your input, which must be JSON
# encoded
#
def challenge(self, your_input):
if self.stage == 0:
return self.create_level()
elif self.stage == 100:
self.exit = True
return {"flag": FLAG}
if self.challenge_words == your_input["decoded"]:
return self.create_level()
return {"error": "Decoding fail"}
import builtins; builtins.Challenge = Challenge # hack to enable challenge to be run locally, see https://cryptohack.org/faq/#listener
listener.start_server(port=13377)
También nos proponen un inicio de solución:
from pwn import * # pip install pwntools
import json
r = remote('socket.cryptohack.org', 13377, level = 'debug')
def json_recv():
line = r.recvline()
return json.loads(line.decode())
def json_send(hsh):
request = json.dumps(hsh).encode()
r.sendline(request)
received = json_recv()
print("Received type: ")
print(received["type"])
print("Received encoded value: ")
print(received["encoded"])
to_send = {
"decoded": "changeme"
}
json_send(to_send)
json_recv()
Si analizamos el script de la parte del servidor:
- Se importan varias librerías.
- Se inicializan variables, una con la bandera que debemos descubrir, y otra con una lista de codificaciones, que son las que necesitaremos implementar en nuestra solución:
- base64
- hex
- rot13
- bigint
- utf-8
- Se recogen todas las palabras del fichero
/usr/share/dict/words
y se almacenan en la variableWORDS
. - Se crea una clase Challenge que es la que gestionará todos los niveles, explicado someramente, la clase permite:
- Crear un nivel:
- Incrementa en uno el número del nivel.
- Selecciona 3 palabras aleatorias de la lista
WORDS
y las concatena con_
. - Selecciona un encoding aleatorio de la lista
ENCODING
. - Aplica el encoding al texto anterior.
- Retorna un JSON con el formato
{"type": encoding, "encoded": encoded}
- Comprueba si el nivel ha llegado a cien, de ser así, muestra la bandera.
- Crear un nivel:
- Arranca el servicio en el puerto
13377
.
Sabiendo cómo funciona la parte del servicio, podemos definir una lógica que nos lleve al éxito en la resolución de este problema. La lógica de la solución será la siguiente:
- Hasta que no haya resuelto 100 problemas de codificación, o bien la respuesta sea la bandera (optaremos por esta segunda opción).
- Se conectará al servidor y recogerá el json del problema actual.
- Revisará el método de codificación recibido para utilizarlo en la respuesta.
- Revisará el texto codificado para decodificarlo después.
- Utilizará el método de decodificación para el texto anterior y utilizando el método recogido anteriormente.
- Enviará la respuesta al servidor.
- Si la respuesta es la bandera, muestra por pantalla, de otra forma volver a empezar.
La implementación que realicé es la siguiente:
#!/bin/env python
from pwn import * # pip install pwntools
from Crypto.Util.number import *
import json
import base64
import codecs
# Conexión con el servidor remoto
r = remote('socket.cryptohack.org', 13377)
# Función que recibe la información del servidor
def json_recv():
line = r.recvline()
return json.loads(line.decode())
# Función que envía la información al servidor
def json_send(hsh):
request = json.dumps(hsh).encode()
r.sendline(request)
# Función para decodificar según el mecanismo
def decode_data(mechanism, data):
decoded = ""
match received["type"]:
case 'base64':
b64_bytes = bytes(data, "utf-8")
decoded = base64.b64decode(data).decode('utf-8')
case "hex":
decoded = bytes.fromhex(data).decode('utf-8')
case "rot13":
decoded = codecs.decode(data, 'rot_13')
case "bigint":
decoded = long_to_bytes(int(data, 16)).decode('utf-8')
case "utf-8":
for n in data:
decoded += chr(n)
return decoded
# Recibe el primer challenge y establece un contador a 1
received = json_recv()
c = 1
# Mientras en la respuesta del servidor, no esté la bandera, decodifica y envía
while not 'flag' in received.keys():
number = '{:000}'.format(c)
print(f"{number} - Decoding value {received["encoded"]} with {received["type"]}")
decoded = decode_data(received["type"], received["encoded"])
print(f"{number} - Going to send {decoded}")
to_send = { "decoded": decoded }
json_send(to_send)
received = json_recv()
c+=1
# Muestra la bandera
print (f"flag: {received["flag"]}")