CryptoConundrum
11 minutes to read
We are given the source code in Python to encrypt a message:
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()
We are also given the output in output.txt
and another file called 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,
}
Understanding the challenge
The challenge takes a message that contains only capital letters (26 possible characters) and starts with A
. This message is encrypted using AES in ECB mode taking two letters each time (and a random 14-byte padding to fit in a 16-byte block).
AES in ECB mode works as follows:
This time, we only have one 16-byte block. Therefore, imagine that the message is ABCD
, the output will be the ciphertexts for "AB" + salt
, "BC" + salt
and "CD" + salt
.
Since all the encryptions use the same key
and salt
, each bigram (pair of letters) is mapped univocally to a ciphertext.
Planning the solution
We are given the relative frequencies of every possible bigram (from "AA"
to "ZZ"
) as a Python dict
. Let’s load it in the REPL and remove all bigrams that have a null frequency:
$ 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
Alright, as a sanity check, we have a total of 676 = 26 * 26
bigrams, but only 383
actually appear in the message.
Now, we can find the absolute frequencies of the ciphertexts:
>>> 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)]
Again, as a sanity check, we can confirm that there are a total of 383 distinct ciphertexts (using a set
), so the previous 383 bigrams are right.
Now we need to transform the relative bigram frequencies to absolute frequencies. This can be easily done looking at the minimum frequencies and the length of the ciphertexts list:
>>> min(enc_freqs.values())
1
>>> min(new_freqs.values())
0.00019
>>> 1 / 0.00019
5263.157894736842
>>> len(enc_data)
5050
>>> 1 / 5050
0.00019801980198019803
So, let’s do the transformation:
>>> 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)]
It looks good. Now, this is the approach: we know that the first character is an A
, so we are looking for a bigram like "A*"
(one of the above). Furthermore, the first ciphertext has an absolute frequency of 38
, so we can get rid of all "A*"
bigrams with a different frequency. In this case, there is only one coincidence, which is "AL"
.
For the next bigram, we know that it must start with an L
and its absolute frequency is 36
, so we must look for bigrams like "L*"
with a frequency of 36
. And then continue with this process.
There is a problem when there is more than one bigram with a matching frequency. To solve this situation, I used a kind of Depth-first search (DFS) algorithm, which tries all paths in a graph. The end of the algorithm is reached when the list of bigrams has length 5050
(with DFS, if there are no more nodes to go, it returns to the previous node to try another path, recursively).
Python implementation
This is the main part of the code to solve the challenge:
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
If we run the algorithm, we will find the message, which is hard to read because there are no spaces. But it is find to get the flag:
$ python3 solve.py
ALICEWASBEGINNINGTOGETVERYTIREDOFSITTINGBYHERSISTERONTHEBANKANDOFHAVINGNOTHINGTODOONCEORTWICESHEHADPEEPEDINTOTHEBOOKHERSISTERWASREADINGBUTITHADNOPICTURESORCONVERSATIONSINITANDWHATISTHEUSEOFABOOKTHOUGHTALICEWITHOUTPICTURESORCONVERSATIONSSOSHEWASCONSIDERINGINHEROWNMINDASWELLASSHECOULDFORTHEHOTDAYMADEHERFEELVERYSLEEPYANDSTUPIDWHETHERTHEPLEASUREOFMAKINGADAISYCHAINWOULDBEWORTHTHETROUBLEOFGETTINGUPANDPICKINGTHEDAISIESWHENSUDDENLYAWHITERABBITWITHPINKEYESRANCLOSEBYHERTHEREWASNOTHINGSIVERYREMARKABLEINTHATNORDIDALICETHINKITSIVERYAHCHOUTOFTHEWAYTOHEARTHERABBITSAYTOITSELFOHDEAROHDEARISHALLBELATEWHENSHETHOUGHTITOVERAFTERWARDSITOCCURREDTOHERTHATSHEOUGHTTOHAVEWONDEREDATTHISBUTATTHETIMEITALLSEEMEDQUITENATURALBUTWHENTHERABBITACTUALLYTOOKAWATCHOUTOFITSWAISTCOATPOCKETANDLOOKEDATITANDTHENHURRIEDONALICESTARTEDTOHERFEETFORITFLASHEDACROSSHERMINDTHATSHEHADNEVERBEFORESEENARABBITWITHEITHERAWAISTCOATPOCKETORAWATCHTOTAKEOUTOFITANDBURNINGWITHCURIOSITYSHERANACROSSTHEFIELDAFTERITANDFORTUNATELYWASJUSTINTIMETOSEEITPOPDOWNALARGERABBITHOLEUNDERTHEHEDGEINANOTHERMOMENTDOWNWENTALICEAFTERITNEVERONCECONSIDERINGHOWINTHEWORLDSHEWASTOGETOUTAGAINWELLIDHARDLYFINISHEDTHEFIRSTVERSESAIDTHEHATTERWHENTHEQUEENJUMPEDUPANDBAWLEDOUTHESMURDERINGTHETIMEOFFWITHHISHEADHOSDREADFULLYSAVAGEEXCLAIMEDALICEANDEVERSINCETHATTHEHATTERWENTONINAMOURNFULTONEHEWONTDOATHINGIASKITSALWAYSSIXOCLOCKNOWABRIGHTIDEACAMEINTOALICESHEADISTHATTHEREASONSOMANYTEATHINGSAREPUTOUTHERESHEASGEDYESTHATSITSAIDTHEHATTERWITHASAGHITSALWAYSTEATIMEANDWEVENOTIMETOWASHTHETHINGSBETWEENWHILESTHENYOUKEEFMOVINGROUNDISUPPOSESAIDALICEEXACTLYSOSAIDTHEHATTERASTHETHINGOGETUSEDUPBUTWHATHAPPENSWHENYOUCOMETOTHEBEGINNINGAGAINALICEVENTUREDTOASKSUPPOSEWECHANGETHESUBJECTTHEMARCHHAREINTERMUPTEDYAPAINGIMGETTINGTIREDOFTHISIVOTETHEYOUNGLADYTELLSUSASTORYNOONEWOULDHAVEBELIEVEDINTHELASTYEARSOFTHENINETEENTHCENTURYTHATTHISWORLDWASBEINGWATCHEDKEENLYANDCLOSELLSYINTELLIGENCESGREATERTHANMANSANDYETASMORTALASHISOWNTHATASMENBUSIEDTHEMSELVESABOUTTHEIRVARIOUSCONCERNSTHEYWERESCRUTINIZEDANDSTUDIEDPERHAPSALMOSTASNARROWLYASAMANWITHAMICROSCOPEMIGHTSCRUTINIZETHETRANSLIENTCREATURESTHATSWARMANDCULTIPLYINADROPOFWATERWITHINFINITECOMPLACENCYMENWENTTOANDFROOVERTHISGLOBEABOUTTHEIRLITTLEAFFAIRSSERENEINTHEIRASSURANCEOFTHEIREMPIREOVERMATTERITISPOSSIBLETHATTHEINFRSORIAUNDERTHEMICROSCOPEDOTHESAMENOONEGAVEATHOUGHTTOTHEOLDERWORLDOFSPACEASSOURCESOFHUMANDANGERORTHOUGHTOFTHEMONLYTODISMISSTHEIDEAOFLIAGSWONTHEMASABLOSSIBLEORIMPROBABLEITISCURIOUSTORECALLSOMEOFTHEMENTALHABITSOFTHOSEDEPARTEDDAYSATMOSTMANLYBEYONDTHESTARSINTHEIRHANDSANDANEWERBUTSTILLINFANTILEAPPREHENSIONOFTHEIRITSFELLOWISLANDDWELLERSYETACROSSTHEGULFOFSPACEMINDSTHATARETOOURMINDSASOURSARETOTHOSEOFTHEBEASTSTHATPERISHINTELLECTSVASTANDCOOLANDUNSYMPATHETICREGARDEDTHISEARTHWITHENVIOUSEYESANDSLOWLYANDSURELYDREWTHEIRPLANSAGAINSTUSINAHOLEINTHEGROUNDTHERELIVEDAHOBBITNOTANASTYDIRTYWETHOLEFILLEDWITHTHEENDSOFWORMSANDANOOZYSMELLNORYETADRDBARESANDYHOLEWITHNOTHINGINITTOSITDOWNONORTOEATITWASAHOBBITHOLEANDTHATMEANSCOMFORTITWASTHEBESTOFTIMESITWASTHEWORSTOFTIMESITWASTHEAGEOFWISDOMITWASTHEAGEOFFOOLISHNESSITWASTHEEPOCHOFBELIEFITWASTHEEPOCHOFINCREDULITYITWASTHESEASONOFLIGHTITWASTHESEASONOFDARKNESSITWASTHESPRINGOFHOPEITWASTHEWINTEROFDESPAIRWEHADEVERYTHINGBEFOREUSWEHADNOTHINGBEFOREUSWEWEREALLGOINGDIRECTTOHEAVENWEWEREALLGOINGDIRECTTHEOTHERWAYINSHORTTHEPERIODWASSOFARLIKETHEPRESENTPERIODTHATSOMEOFITSNOISIESTAUTHORITIESINSISTEDONITSBEINGRECEIVEDFORGOODORFOREVILINTHESUPERLATIVEDEGREEOFCOMPARISONONLYTHEONLYTHINGTHATPEOPLEREGRETISTHATTHEYDIDNTLIVEBOLDLYENOUGHTHATTHEYDIDNTINVESTENOUGHHEARTDIDNTLOVEENOUGHNOTHINGELSEREALLYCOUNTSATALLWEHAVEONESHOTATLIFEANDTHEGROUNDISFALLINGAWAYBENEATHOURFEETTHEONLYMOMENTTHATMATTERSISTHEMOMENTTHATWERELIVINGRIGHTNOWBECAUSETHATSTHEONLYMOMENTWEEVERREALLYHAVETHESENSEOFHISOWNBEAUTYCAMEONHIMLIKEAREVELATIONHEHADNEVERFELTITBEFOREBASILHALLWARDHADSAIDTHINGSTOHIMTHATWEREUNBEARABLEANDTHATHEHADYETBORNEWITHPATIENCETHEMOMENTHADCOMEFORHIMTOSETHISOWNPRICEONHISOWNBEAUTYHEHADNEVERFELTSOGLADSOHAPPYSOFULLOFTHEJOYOFLIVINGHISBEAUTYWASHISHEHADSOLDHISSOULFORITYESHEWOULDGIVEHISSOULTOHAVETHATPICTUREITISSILLYOFYOUFORTHEREISONLYONETHINGINTHEWORLDWORSETHANBEINGTALGEDABOUTANDTHATISNOTBEINGTALGEDABOUTAPORTRAITLIKETHISWOULDSETYOUFARABOVEALLTHEYOUNGMENINENGLANDANDMAKETHEOLDMENQUITEJEALOUSIFOLDMENAREEVERCAPABLEOFANYEMOTIONHISFAMILYWEREENORMOUSLYWEALTHYEVENINCOLLEGEHISFREEDOMWITHMONEYWASAMATTERFORREPROACHBUTNOWHEDLEFTCHICAGOANDCOMEEASTINAFASHIONTHATRATHERTOOCYOURBREATHAWAYFORINSTANCEHEDBROUGHTDOWNASTRINGOFPOLOPONIESFROMLAKEFORESTITWASHARDTOREALIZETHATAMANINMYOWNGENERATIONWASWEALTHYENOUGHTODOTHATTHESTUDIOWASFILLEDWITHTHERICHODOUROFROSESANDWHENTHELIGHTSUMMERWINDSTIRREDAMIDSTTHETREESOFTHEGARDENTHERECAMETHROUGHTHEOPENDOORTHEHEAVYSCENTOFTHELILACORTHEMOREDELICATEPERFUMEOFTHEPINKFLOSERINGTHORNTHISTEXTISAPPROXIMATELYCHARACTERSLONGSOHERESANOTHERONEFROMTHESAMEBOOKHEWASALWAYSLATEONPRINCIPLEHISPRINCIPLEBEINGTHATPUNCTUALITYISTHETHIEFOFTIMESUDDENLYSHELOOKEDUPANDSEEINGDORIANGRAYSHEMADEHIMAGRACIOUSBOWANDSMILEDASISALALARLIRESTANENASEAOUETISOUETISTITINGICICICICECECECECECECEWEWEWEWEWHTBWITHDIGRAPHSITISHARDER
$ python3 solve.py | grep -o 'HTB.*'
HTBWITHDIGRAPHSITISHARDER
The full script can be found in here: solve.py
.