CryptoConundrum
11 minutos de lectura
Se nos proporciona el código fuente en Python para cifrar un mensaje:
from os import urandom
from Crypto.Cipher import AES
from secret import MESSAGE
assert all([x.isupper() for x in MESSAGE])
assert MESSAGE.startswith('A')
class Cipher:
def __init__(self):
self.salt = urandom(14)
key = urandom(16)
self.cipher = AES.new(key, AES.MODE_ECB)
def encrypt(self, message):
return [
self.cipher.encrypt(message[i:i + 2].encode() + self.salt)
for i in range(len(message) - 1)
]
def main():
cipher = Cipher()
encrypted = cipher.encrypt(MESSAGE)
encrypted = "\n".join([c.hex() for c in encrypted])
with open("output.txt", 'w+') as f:
f.write(encrypted)
if __name__ == "__main__":
main()
También se nos da la salida en output.txt
y otro archivo llamado frequencies.txt
:
{
"AA": 0.00000, "AB": 0.00336, "AC": 0.00257, "AD": 0.00435, "AE": 0.00000, "AF": 0.00099, "AG": 0.00158, "AH": 0.00059, "AI": 0.00336, "AJ": 0.00000, "AK": 0.00079, "AL": 0.00752, "AM": 0.00237, "AN": 0.01227, "AO": 0.00039, "AP": 0.00158, "AQ": 0.00000, "AR": 0.00732, "AS": 0.01148, "AT": 0.01425, "AU": 0.00118, "AV": 0.00198, "AW": 0.00158, "AX": 0.00000, "AY": 0.00217, "AZ": 0.00000,
"BA": 0.00099, "BB": 0.00138, "BC": 0.00000, "BD": 0.00000, "BE": 0.00514, "BF": 0.00000, "BG": 0.00000, "BH": 0.00000, "BI": 0.00158, "BJ": 0.00019, "BK": 0.00000, "BL": 0.00138, "BM": 0.00000, "BN": 0.00000, "BO": 0.00217, "BP": 0.00000, "BQ": 0.00000, "BR": 0.00059, "BS": 0.00000, "BT": 0.00000, "BU": 0.00158, "BV": 0.00000, "BW": 0.00019, "BX": 0.00000, "BY": 0.00059, "BZ": 0.00000,
"CA": 0.00158, "CB": 0.00000, "CC": 0.00019, "CD": 0.00000, "CE": 0.00653, "CF": 0.00000, "CG": 0.00000, "CH": 0.00257, "CI": 0.00118, "CJ": 0.00000, "CK": 0.00079, "CL": 0.00079, "CM": 0.00000, "CN": 0.00000, "CO": 0.00396, "CP": 0.00000, "CQ": 0.00000, "CR": 0.00198, "CS": 0.00000, "CT": 0.00217, "CU": 0.00059, "CV": 0.00000, "CW": 0.00000, "CX": 0.00000, "CY": 0.00019, "CZ": 0.00000,
"DA": 0.00455, "DB": 0.00079, "DC": 0.00079, "DD": 0.00079, "DE": 0.00475, "DF": 0.00099, "DG": 0.00039, "DH": 0.00099, "DI": 0.00336, "DJ": 0.00000, "DK": 0.00019, "DL": 0.00079, "DM": 0.00079, "DN": 0.00158, "DO": 0.00415, "DP": 0.00059, "DQ": 0.00019, "DR": 0.00079, "DS": 0.00356, "DT": 0.00396, "DU": 0.00099, "DV": 0.00000, "DW": 0.00198, "DX": 0.00000, "DY": 0.00118, "DZ": 0.00000,
"EA": 0.01128, "EB": 0.00217, "EC": 0.00356, "ED": 0.00891, "EE": 0.00534, "EF": 0.00257, "EG": 0.00217, "EH": 0.00475, "EI": 0.00574, "EJ": 0.00039, "EK": 0.00000, "EL": 0.00594, "EM": 0.00316, "EN": 0.01188, "EO": 0.00613, "EP": 0.00257, "EQ": 0.00019, "ER": 0.01821, "ES": 0.00990, "ET": 0.01069, "EU": 0.00118, "EV": 0.00277, "EW": 0.00574, "EX": 0.00059, "EY": 0.00178, "EZ": 0.00000,
"FA": 0.00178, "FB": 0.00019, "FC": 0.00019, "FD": 0.00039, "FE": 0.00158, "FF": 0.00059, "FG": 0.00019, "FH": 0.00079, "FI": 0.00217, "FJ": 0.00000, "FK": 0.00000, "FL": 0.00099, "FM": 0.00019, "FN": 0.00000, "FO": 0.00415, "FP": 0.00019, "FQ": 0.00000, "FR": 0.00099, "FS": 0.00059, "FT": 0.00396, "FU": 0.00099, "FV": 0.00000, "FW": 0.00079, "FX": 0.00000, "FY": 0.00019, "FZ": 0.00000,
"GA": 0.00178, "GB": 0.00079, "GC": 0.00000, "GD": 0.00059, "GE": 0.00316, "GF": 0.00000, "GG": 0.00000, "GH": 0.00376, "GI": 0.00178, "GJ": 0.00000, "GK": 0.00000, "GL": 0.00079, "GM": 0.00019, "GN": 0.00019, "GO": 0.00118, "GP": 0.00000, "GQ": 0.00000, "GR": 0.00217, "GS": 0.00118, "GT": 0.00198, "GU": 0.00039, "GV": 0.00000, "GW": 0.00039, "GX": 0.00000, "GY": 0.00000, "GZ": 0.00000,
"HA": 0.01386, "HB": 0.00019, "HC": 0.00039, "HD": 0.00059, "HE": 0.03089, "HF": 0.00000, "HG": 0.00000, "HH": 0.00059, "HI": 0.00891, "HJ": 0.00000, "HK": 0.00000, "HL": 0.00000, "HM": 0.00019, "HN": 0.00059, "HO": 0.00554, "HP": 0.00039, "HQ": 0.00000, "HR": 0.00019, "HS": 0.00019, "HT": 0.00396, "HU": 0.00039, "HV": 0.00000, "HW": 0.00019, "HX": 0.00000, "HY": 0.00039, "HZ": 0.00000,
"IA": 0.00059, "IB": 0.00039, "IC": 0.00495, "ID": 0.00316, "IE": 0.00257, "IF": 0.00059, "IG": 0.00158, "IH": 0.00000, "II": 0.00000, "IJ": 0.00000, "IK": 0.00059, "IL": 0.00217, "IM": 0.00336, "IN": 0.01960, "IO": 0.00297, "IP": 0.00059, "IQ": 0.00000, "IR": 0.00356, "IS": 0.01049, "IT": 0.01504, "IU": 0.00000, "IV": 0.00158, "IW": 0.00000, "IX": 0.00019, "IY": 0.00000, "IZ": 0.00059,
"JA": 0.00000, "JB": 0.00000, "JC": 0.00000, "JD": 0.00000, "JE": 0.00039, "JF": 0.00000, "JG": 0.00000, "JH": 0.00000, "JI": 0.00000, "JJ": 0.00000, "JK": 0.00000, "JL": 0.00000, "JM": 0.00000, "JN": 0.00000, "JO": 0.00019, "JP": 0.00000, "JQ": 0.00000, "JR": 0.00000, "JS": 0.00000, "JT": 0.00000, "JU": 0.00039, "JV": 0.00000, "JW": 0.00000, "JX": 0.00000, "JY": 0.00000, "JZ": 0.00000,
"KA": 0.00059, "KB": 0.00000, "KC": 0.00000, "KD": 0.00000, "KE": 0.00316, "KF": 0.00019, "KG": 0.00000, "KH": 0.00039, "KI": 0.00079, "KJ": 0.00000, "KK": 0.00000, "KL": 0.00000, "KM": 0.00000, "KN": 0.00039, "KO": 0.00000, "KP": 0.00000, "KQ": 0.00000, "KR": 0.00000, "KS": 0.00019, "KT": 0.00019, "KU": 0.00000, "KV": 0.00000, "KW": 0.00000, "KX": 0.00000, "KY": 0.00019, "KZ": 0.00000,
"LA": 0.00415, "LB": 0.00039, "LC": 0.00000, "LD": 0.00297, "LE": 0.00554, "LF": 0.00059, "LG": 0.00039, "LH": 0.00039, "LI": 0.00712, "LJ": 0.00000, "LK": 0.00039, "LL": 0.00534, "LM": 0.00019, "LN": 0.00019, "LO": 0.00277, "LP": 0.00000, "LQ": 0.00000, "LR": 0.00000, "LS": 0.00079, "LT": 0.00158, "LU": 0.00000, "LV": 0.00039, "LW": 0.00099, "LX": 0.00000, "LY": 0.00534, "LZ": 0.00000,
"MA": 0.00376, "MB": 0.00000, "MC": 0.00000, "MD": 0.00000, "ME": 0.00693, "MF": 0.00019, "MG": 0.00019, "MH": 0.00000, "MI": 0.00237, "MJ": 0.00000, "MK": 0.00000, "ML": 0.00039, "MM": 0.00019, "MN": 0.00000, "MO": 0.00297, "MP": 0.00138, "MQ": 0.00000, "MR": 0.00000, "MS": 0.00039, "MT": 0.00059, "MU": 0.00059, "MV": 0.00000, "MW": 0.00019, "MX": 0.00000, "MY": 0.00019, "MZ": 0.00000,
"NA": 0.00336, "NB": 0.00099, "NC": 0.00297, "ND": 0.00990, "NE": 0.00415, "NF": 0.00079, "NG": 0.01009, "NH": 0.00118, "NI": 0.00336, "NJ": 0.00019, "NK": 0.00079, "NL": 0.00198, "NM": 0.00059, "NN": 0.00039, "NO": 0.00613, "NP": 0.00039, "NQ": 0.00019, "NR": 0.00000, "NS": 0.00396, "NT": 0.00891, "NU": 0.00000, "NV": 0.00079, "NW": 0.00158, "NX": 0.00000, "NY": 0.00079, "NZ": 0.00000,
"OA": 0.00158, "OB": 0.00079, "OC": 0.00138, "OD": 0.00138, "OE": 0.00019, "OF": 0.00950, "OG": 0.00059, "OH": 0.00217, "OI": 0.00079, "OJ": 0.00000, "OK": 0.00138, "OL": 0.00277, "OM": 0.00356, "ON": 0.00891, "OO": 0.00336, "OP": 0.00178, "OQ": 0.00000, "OR": 0.00970, "OS": 0.00435, "OT": 0.00435, "OU": 0.01089, "OV": 0.00158, "OW": 0.00435, "OX": 0.00019, "OY": 0.00019, "OZ": 0.00019,
"PA": 0.00217, "PB": 0.00019, "PC": 0.00000, "PD": 0.00019, "PE": 0.00297, "PF": 0.00000, "PG": 0.00000, "PH": 0.00019, "PI": 0.00158, "PJ": 0.00000, "PK": 0.00000, "PL": 0.00138, "PM": 0.00019, "PN": 0.00000, "PO": 0.00277, "PP": 0.00118, "PQ": 0.00000, "PR": 0.00178, "PS": 0.00019, "PT": 0.00019, "PU": 0.00039, "PV": 0.00000, "PW": 0.00000, "PX": 0.00000, "PY": 0.00039, "PZ": 0.00000,
"QA": 0.00000, "QB": 0.00000, "QC": 0.00000, "QD": 0.00000, "QE": 0.00000, "QF": 0.00000, "QG": 0.00000, "QH": 0.00000, "QI": 0.00000, "QJ": 0.00000, "QK": 0.00000, "QL": 0.00000, "QM": 0.00000, "QN": 0.00000, "QO": 0.00000, "QP": 0.00000, "QQ": 0.00000, "QR": 0.00000, "QS": 0.00000, "QT": 0.00000, "QU": 0.00059, "QV": 0.00000, "QW": 0.00000, "QX": 0.00000, "QY": 0.00000, "QZ": 0.00000,
"RA": 0.00475, "RB": 0.00059, "RC": 0.00099, "RD": 0.00178, "RE": 0.01326, "RF": 0.00158, "RG": 0.00039, "RH": 0.00059, "RI": 0.00633, "RJ": 0.00000, "RK": 0.00039, "RL": 0.00158, "RM": 0.00138, "RN": 0.00099, "RO": 0.00554, "RP": 0.00019, "RQ": 0.00000, "RR": 0.00138, "RS": 0.00316, "RT": 0.00534, "RU": 0.00059, "RV": 0.00019, "RW": 0.00198, "RX": 0.00000, "RY": 0.00178, "RZ": 0.00000,
"SA": 0.00693, "SB": 0.00138, "SC": 0.00178, "SD": 0.00019, "SE": 0.00594, "SF": 0.00118, "SG": 0.00059, "SH": 0.00574, "SI": 0.00693, "SJ": 0.00019, "SK": 0.00059, "SL": 0.00138, "SM": 0.00118, "SN": 0.00079, "SO": 0.00693, "SP": 0.00118, "SQ": 0.00000, "SR": 0.00039, "SS": 0.00356, "ST": 0.01267, "SU": 0.00217, "SV": 0.00019, "SW": 0.00198, "SX": 0.00000, "SY": 0.00059, "SZ": 0.00000,
"TA": 0.00673, "TB": 0.00079, "TC": 0.00138, "TD": 0.00118, "TE": 0.00772, "TF": 0.00039, "TG": 0.00000, "TH": 0.03920, "TI": 0.01029, "TJ": 0.00000, "TK": 0.00000, "TL": 0.00118, "TM": 0.00079, "TN": 0.00099, "TO": 0.00910, "TP": 0.00178, "TQ": 0.00000, "TR": 0.00118, "TS": 0.00475, "TT": 0.00673, "TU": 0.00297, "TV": 0.00039, "TW": 0.00415, "TX": 0.00000, "TY": 0.00217, "TZ": 0.00000,
"UA": 0.00039, "UB": 0.00039, "UC": 0.00039, "UD": 0.00079, "UE": 0.00059, "UF": 0.00039, "UG": 0.00217, "UH": 0.00000, "UI": 0.00039, "UJ": 0.00000, "UK": 0.00019, "UL": 0.00257, "UM": 0.00079, "UN": 0.00237, "UO": 0.00000, "UP": 0.00198, "UQ": 0.00000, "UR": 0.00455, "US": 0.00316, "UT": 0.00475, "UU": 0.00000, "UV": 0.00000, "UW": 0.00000, "UX": 0.00000, "UY": 0.00000, "UZ": 0.00000,
"VA": 0.00059, "VB": 0.00000, "VC": 0.00000, "VD": 0.00000, "VE": 0.00772, "VF": 0.00000, "VG": 0.00000, "VH": 0.00000, "VI": 0.00118, "VJ": 0.00000, "VK": 0.00000, "VL": 0.00000, "VM": 0.00000, "VN": 0.00000, "VO": 0.00019, "VP": 0.00000, "VQ": 0.00000, "VR": 0.00000, "VS": 0.00000, "VT": 0.00000, "VU": 0.00000, "VV": 0.00000, "VW": 0.00000, "VX": 0.00000, "VY": 0.00019, "VZ": 0.00000,
"WA": 0.00871, "WB": 0.00019, "WC": 0.00000, "WD": 0.00019, "WE": 0.00594, "WF": 0.00000, "WG": 0.00000, "WH": 0.00257, "WI": 0.00415, "WJ": 0.00000, "WK": 0.00000, "WL": 0.00059, "WM": 0.00000, "WN": 0.00217, "WO": 0.00277, "WP": 0.00000, "WQ": 0.00000, "WR": 0.00000, "WS": 0.00000, "WT": 0.00019, "WU": 0.00000, "WV": 0.00000, "WW": 0.00000, "WX": 0.00000, "WY": 0.00000, "WZ": 0.00000,
"XA": 0.00019, "XB": 0.00000, "XC": 0.00019, "XD": 0.00000, "XE": 0.00000, "XF": 0.00000, "XG": 0.00000, "XH": 0.00000, "XI": 0.00019, "XJ": 0.00000, "XK": 0.00000, "XL": 0.00000, "XM": 0.00000, "XN": 0.00000, "XO": 0.00019, "XP": 0.00000, "XQ": 0.00000, "XR": 0.00000, "XS": 0.00000, "XT": 0.00019, "XU": 0.00000, "XV": 0.00000, "XW": 0.00000, "XX": 0.00000, "XY": 0.00000, "XZ": 0.00000,
"YA": 0.00118, "YB": 0.00079, "YC": 0.00079, "YD": 0.00079, "YE": 0.00257, "YF": 0.00039, "YG": 0.00000, "YH": 0.00099, "YI": 0.00099, "YJ": 0.00000, "YK": 0.00000, "YL": 0.00000, "YM": 0.00118, "YN": 0.00019, "YO": 0.00237, "YP": 0.00000, "YQ": 0.00000, "YR": 0.00019, "YS": 0.00257, "YT": 0.00217, "YU": 0.00000, "YV": 0.00000, "YW": 0.00138, "YX": 0.00000, "YY": 0.00000, "YZ": 0.00000,
"ZA": 0.00000, "ZB": 0.00000, "ZC": 0.00000, "ZD": 0.00000, "ZE": 0.00059, "ZF": 0.00000, "ZG": 0.00000, "ZH": 0.00000, "ZI": 0.00000, "ZJ": 0.00000, "ZK": 0.00000, "ZL": 0.00000, "ZM": 0.00000, "ZN": 0.00000, "ZO": 0.00000, "ZP": 0.00000, "ZQ": 0.00000, "ZR": 0.00000, "ZS": 0.00000, "ZT": 0.00000, "ZU": 0.00000, "ZV": 0.00000, "ZW": 0.00000, "ZX": 0.00000, "ZY": 0.00019, "ZZ": 0.00000,
}
Entendiendo el reto
El reto toma un mensaje que contiene solo letras mayúsculas (26 caracteres posibles) y empieza por A
. Este mensaje está cifrado utilizando AES en modo ECB tomando dos letras cada vez (y un relleno aleatorio de 14 bytes para obtener un bloque de 16 bytes).
AES en modo ECB funciona de la siguiente manera:
Esta vez, solo tenemos un bloque de 16 bytes. Por lo tanto, imaginemos que el mensaje es ABCD
, la salida serán los textos cifrados para "AB" + salt
, "BC" + salt
y "CD" + salt
.
Dado que todas los cifrados usan los mismos valores de key
y salt
, cada bigrama (par de letras) se asigna unívocalmente a un texto cifrado.
Planificación de la solución
Se nos dan las frecuencias relativas de cada bigrama posible (desde "AA"
hasta "ZZ"
) como un dict
de Python. Vamos a cargarlo en el REPL y eliminar todos los bigramas que tienen una frecuencia cero:
$ python3 -q
>>> with open('frequencies.txt') as f:
... freqs = eval(f.read())
...
>>> len(freqs)
676
>>> 26 * 26
676
>>> new_freqs = {b: f for b, f in freqs.items() if f != 0}
>>> len(new_freqs)
383
Muy bien, como comprobación, tenemos un total de 676 = 26 * 26
bigramas, pero solo 383
aparecen en el mensaje.
Ahora, podemos encontrar las frecuencias absolutas de los textos cifrados:
>>> with open('output.txt') as o:
... enc_data = o.read().splitlines()
...
>>> len(set(enc_data))
383
>>> enc_freqs = {enc: enc_data.count(enc) for enc in enc_data}
>>> list(enc_freqs.items())[:5]
[('0ba2a64352eee0ed0ee33b7d9d5a2838', 38), ('dee4d1cd94c51cb2f0c23ede0868dd84', 36), ('d15f10543a584105b8f93f4a86a44df3', 25), ('6c50f2bce4290b6358ab45cf9d224676', 33), ('2e7e46ca39e521366c6b0ef1484e0fdf', 29)]
Una vez más, como comprobación, podemos confirmar que hay un total de 383 textos cifrados distintos (usando un set
), por lo que los 383 bigramas anteriores tienen sentido.
Ahora necesitamos transformar las frecuencias relativas de los bigramas a frecuencias absolutas. Esto se puede hacer fácilmente observando las frecuencias mínimas y la longitud de la lista textos cifrados:
>>> min(enc_freqs.values())
1
>>> min(new_freqs.values())
0.00019
>>> 1 / 0.00019
5263.157894736842
>>> len(enc_data)
5050
>>> 1 / 5050
0.00019801980198019803
Entonces, hagamos la transformación:
>>> new_freqs = {b: round(f * len(enc_data)) for b, f in freqs.items() if f != 0}
>>> list(new_freqs.items())[:20]
[('AB', 17), ('AC', 13), ('AD', 22), ('AF', 5), ('AG', 8), ('AH', 3), ('AI', 17), ('AK', 4), ('AL', 38), ('AM', 12), ('AN', 62), ('AO', 2), ('AP', 8), ('AR', 37), ('AS', 58), ('AT', 72), ('AU', 6), ('AV', 10), ('AW', 8), ('AY', 11)]
Se ve bien. Ahora, este es el enfoque: sabemos que el primer carácter es uns A
, por lo que estamos buscando un bigrama como "A*"
(uno de los anteriores). Además, el primer texto cifrado tiene una frecuencia absoluta de 38
, por lo que podemos deshacernos de todos los bigramas "A*"
con frecuencia diferente. En este caso, solo hay una coincidencia, que es "AL"
.
Para el siguiente bigrama, sabemos que debe comenzar con una L
y que su frecuencia absoluta es 36
, por lo que debemos buscar bigramas como "L*"
con una frecuencia de 36
. Y luego continuar con este proceso.
Hay un problema cuando hay más de un bigrama con una frecuencia coincidente. Para resolver esta situación, utilicé una especie de algoritmo de búsqueda en profundidad (DFS, Depth-first search), que intenta todas las rutas en un grafo. El final del algoritmo se alcanza cuando la lista de bigramas tiene longitud 5050
(con DFS, si no hay más nodos para visitar, vuelve al nodo anterior para usar otro camino, recursivamente).
Implementación en Python
Esta es la parte principal del código para resolver el reto:
done = False
def dfs(bigram: str, index: int = 0, bigrams: List[str] | None = None):
global done
bigrams = [] if bigrams is None else bigrams
if index == len(enc_data) == len(bigrams):
print(''.join(map(lambda s: s[0], bigrams[1:])) + bigram)
done = True
if done:
return
count = enc_freqs[enc_data[index]]
bigrams.append(bigram)
for next_bigram in next_bigrams(bigram[-1], count):
dfs(next_bigram, index + 1, bigrams.copy())
def next_bigrams(letter: str, count: int) -> List[str]:
return [b for b, n in new_freqs.items() if n == count and b.startswith(letter)]
Flag
Si ejecutamos el algoritmo, encontraremos el mensaje, que es difícil de leer porque no hay espacios. Pero se puede obtener la flag:
$ python3 solve.py
ALICEWASBEGINNINGTOGETVERYTIREDOFSITTINGBYHERSISTERONTHEBANKANDOFHAVINGNOTHINGTODOONCEORTWICESHEHADPEEPEDINTOTHEBOOKHERSISTERWASREADINGBUTITHADNOPICTURESORCONVERSATIONSINITANDWHATISTHEUSEOFABOOKTHOUGHTALICEWITHOUTPICTURESORCONVERSATIONSSOSHEWASCONSIDERINGINHEROWNMINDASWELLASSHECOULDFORTHEHOTDAYMADEHERFEELVERYSLEEPYANDSTUPIDWHETHERTHEPLEASUREOFMAKINGADAISYCHAINWOULDBEWORTHTHETROUBLEOFGETTINGUPANDPICKINGTHEDAISIESWHENSUDDENLYAWHITERABBITWITHPINKEYESRANCLOSEBYHERTHEREWASNOTHINGSIVERYREMARKABLEINTHATNORDIDALICETHINKITSIVERYAHCHOUTOFTHEWAYTOHEARTHERABBITSAYTOITSELFOHDEAROHDEARISHALLBELATEWHENSHETHOUGHTITOVERAFTERWARDSITOCCURREDTOHERTHATSHEOUGHTTOHAVEWONDEREDATTHISBUTATTHETIMEITALLSEEMEDQUITENATURALBUTWHENTHERABBITACTUALLYTOOKAWATCHOUTOFITSWAISTCOATPOCKETANDLOOKEDATITANDTHENHURRIEDONALICESTARTEDTOHERFEETFORITFLASHEDACROSSHERMINDTHATSHEHADNEVERBEFORESEENARABBITWITHEITHERAWAISTCOATPOCKETORAWATCHTOTAKEOUTOFITANDBURNINGWITHCURIOSITYSHERANACROSSTHEFIELDAFTERITANDFORTUNATELYWASJUSTINTIMETOSEEITPOPDOWNALARGERABBITHOLEUNDERTHEHEDGEINANOTHERMOMENTDOWNWENTALICEAFTERITNEVERONCECONSIDERINGHOWINTHEWORLDSHEWASTOGETOUTAGAINWELLIDHARDLYFINISHEDTHEFIRSTVERSESAIDTHEHATTERWHENTHEQUEENJUMPEDUPANDBAWLEDOUTHESMURDERINGTHETIMEOFFWITHHISHEADHOSDREADFULLYSAVAGEEXCLAIMEDALICEANDEVERSINCETHATTHEHATTERWENTONINAMOURNFULTONEHEWONTDOATHINGIASKITSALWAYSSIXOCLOCKNOWABRIGHTIDEACAMEINTOALICESHEADISTHATTHEREASONSOMANYTEATHINGSAREPUTOUTHERESHEASGEDYESTHATSITSAIDTHEHATTERWITHASAGHITSALWAYSTEATIMEANDWEVENOTIMETOWASHTHETHINGSBETWEENWHILESTHENYOUKEEFMOVINGROUNDISUPPOSESAIDALICEEXACTLYSOSAIDTHEHATTERASTHETHINGOGETUSEDUPBUTWHATHAPPENSWHENYOUCOMETOTHEBEGINNINGAGAINALICEVENTUREDTOASKSUPPOSEWECHANGETHESUBJECTTHEMARCHHAREINTERMUPTEDYAPAINGIMGETTINGTIREDOFTHISIVOTETHEYOUNGLADYTELLSUSASTORYNOONEWOULDHAVEBELIEVEDINTHELASTYEARSOFTHENINETEENTHCENTURYTHATTHISWORLDWASBEINGWATCHEDKEENLYANDCLOSELLSYINTELLIGENCESGREATERTHANMANSANDYETASMORTALASHISOWNTHATASMENBUSIEDTHEMSELVESABOUTTHEIRVARIOUSCONCERNSTHEYWERESCRUTINIZEDANDSTUDIEDPERHAPSALMOSTASNARROWLYASAMANWITHAMICROSCOPEMIGHTSCRUTINIZETHETRANSLIENTCREATURESTHATSWARMANDCULTIPLYINADROPOFWATERWITHINFINITECOMPLACENCYMENWENTTOANDFROOVERTHISGLOBEABOUTTHEIRLITTLEAFFAIRSSERENEINTHEIRASSURANCEOFTHEIREMPIREOVERMATTERITISPOSSIBLETHATTHEINFRSORIAUNDERTHEMICROSCOPEDOTHESAMENOONEGAVEATHOUGHTTOTHEOLDERWORLDOFSPACEASSOURCESOFHUMANDANGERORTHOUGHTOFTHEMONLYTODISMISSTHEIDEAOFLIAGSWONTHEMASABLOSSIBLEORIMPROBABLEITISCURIOUSTORECALLSOMEOFTHEMENTALHABITSOFTHOSEDEPARTEDDAYSATMOSTMANLYBEYONDTHESTARSINTHEIRHANDSANDANEWERBUTSTILLINFANTILEAPPREHENSIONOFTHEIRITSFELLOWISLANDDWELLERSYETACROSSTHEGULFOFSPACEMINDSTHATARETOOURMINDSASOURSARETOTHOSEOFTHEBEASTSTHATPERISHINTELLECTSVASTANDCOOLANDUNSYMPATHETICREGARDEDTHISEARTHWITHENVIOUSEYESANDSLOWLYANDSURELYDREWTHEIRPLANSAGAINSTUSINAHOLEINTHEGROUNDTHERELIVEDAHOBBITNOTANASTYDIRTYWETHOLEFILLEDWITHTHEENDSOFWORMSANDANOOZYSMELLNORYETADRDBARESANDYHOLEWITHNOTHINGINITTOSITDOWNONORTOEATITWASAHOBBITHOLEANDTHATMEANSCOMFORTITWASTHEBESTOFTIMESITWASTHEWORSTOFTIMESITWASTHEAGEOFWISDOMITWASTHEAGEOFFOOLISHNESSITWASTHEEPOCHOFBELIEFITWASTHEEPOCHOFINCREDULITYITWASTHESEASONOFLIGHTITWASTHESEASONOFDARKNESSITWASTHESPRINGOFHOPEITWASTHEWINTEROFDESPAIRWEHADEVERYTHINGBEFOREUSWEHADNOTHINGBEFOREUSWEWEREALLGOINGDIRECTTOHEAVENWEWEREALLGOINGDIRECTTHEOTHERWAYINSHORTTHEPERIODWASSOFARLIKETHEPRESENTPERIODTHATSOMEOFITSNOISIESTAUTHORITIESINSISTEDONITSBEINGRECEIVEDFORGOODORFOREVILINTHESUPERLATIVEDEGREEOFCOMPARISONONLYTHEONLYTHINGTHATPEOPLEREGRETISTHATTHEYDIDNTLIVEBOLDLYENOUGHTHATTHEYDIDNTINVESTENOUGHHEARTDIDNTLOVEENOUGHNOTHINGELSEREALLYCOUNTSATALLWEHAVEONESHOTATLIFEANDTHEGROUNDISFALLINGAWAYBENEATHOURFEETTHEONLYMOMENTTHATMATTERSISTHEMOMENTTHATWERELIVINGRIGHTNOWBECAUSETHATSTHEONLYMOMENTWEEVERREALLYHAVETHESENSEOFHISOWNBEAUTYCAMEONHIMLIKEAREVELATIONHEHADNEVERFELTITBEFOREBASILHALLWARDHADSAIDTHINGSTOHIMTHATWEREUNBEARABLEANDTHATHEHADYETBORNEWITHPATIENCETHEMOMENTHADCOMEFORHIMTOSETHISOWNPRICEONHISOWNBEAUTYHEHADNEVERFELTSOGLADSOHAPPYSOFULLOFTHEJOYOFLIVINGHISBEAUTYWASHISHEHADSOLDHISSOULFORITYESHEWOULDGIVEHISSOULTOHAVETHATPICTUREITISSILLYOFYOUFORTHEREISONLYONETHINGINTHEWORLDWORSETHANBEINGTALGEDABOUTANDTHATISNOTBEINGTALGEDABOUTAPORTRAITLIKETHISWOULDSETYOUFARABOVEALLTHEYOUNGMENINENGLANDANDMAKETHEOLDMENQUITEJEALOUSIFOLDMENAREEVERCAPABLEOFANYEMOTIONHISFAMILYWEREENORMOUSLYWEALTHYEVENINCOLLEGEHISFREEDOMWITHMONEYWASAMATTERFORREPROACHBUTNOWHEDLEFTCHICAGOANDCOMEEASTINAFASHIONTHATRATHERTOOCYOURBREATHAWAYFORINSTANCEHEDBROUGHTDOWNASTRINGOFPOLOPONIESFROMLAKEFORESTITWASHARDTOREALIZETHATAMANINMYOWNGENERATIONWASWEALTHYENOUGHTODOTHATTHESTUDIOWASFILLEDWITHTHERICHODOUROFROSESANDWHENTHELIGHTSUMMERWINDSTIRREDAMIDSTTHETREESOFTHEGARDENTHERECAMETHROUGHTHEOPENDOORTHEHEAVYSCENTOFTHELILACORTHEMOREDELICATEPERFUMEOFTHEPINKFLOSERINGTHORNTHISTEXTISAPPROXIMATELYCHARACTERSLONGSOHERESANOTHERONEFROMTHESAMEBOOKHEWASALWAYSLATEONPRINCIPLEHISPRINCIPLEBEINGTHATPUNCTUALITYISTHETHIEFOFTIMESUDDENLYSHELOOKEDUPANDSEEINGDORIANGRAYSHEMADEHIMAGRACIOUSBOWANDSMILEDASISALALARLIRESTANENASEAOUETISOUETISTITINGICICICICECECECECECECEWEWEWEWEWHTBWITHDIGRAPHSITISHARDER
$ python3 solve.py | grep -o 'HTB.*'
HTBWITHDIGRAPHSITISHARDER
El script completo se puede encontrar aquí: solve.py
.