Homomurphy's Law
13 minutes to read
We are given the source code of a ransomware project.
Source code analysis
This is ransomware.py
:
from Crypto import Random
from encryption import *
import os
MBEGIN = "---BEGIN MORPHEUS KEY---"
MEND = "---END MORPHEUS KEY---"
GBEGIN = "----BEGIN GPUBLIC KEY---"
GEND = "---END GPUBLIC KEY---"
with open("note.txt", "r") as f:
note = f.read()
aes = AESP()
gm = GM()
obf = OBF()
def encrypt(key):
obf_key = obf.obfuscate(key)
enc_key = gm.encrypt(obf_key)
return enc_key
def pwn(directory):
for file in os.listdir(directory):
file = directory + "/" + file
with open(file, "rb") as f:
data = f.read()
enc_data = aes.encrypt(data)
with open(file, "wb") as f:
f.write(enc_data)
os.rename(file, file + r'.IBKFZ')
# Here will be the flag and other files
pwn("sensitive_data")
enc_key = encrypt(aes.key)
enc_key = "\n".join([str(x) for x in enc_key])
with open("sensitive_data/custom_note.txt", "w") as f:
data = note + "\n"
data += MBEGIN + "\n"
data += enc_key + "\n"
data += MEND + "\n"
data += "\n"
data += GBEGIN + "\n"
data += hex(gm.n)[2:] + "\n"
data += hex(gm.x)[2:] + "\n"
data += GEND
f.write(data)
This script defines some instances AESP
, GM
and OBF
. The function named pwn
takes every file in sensitive_data
and encrypts it with AES. Then, the AES key is obfuscated with obf.obfuscate
and encrypts it with gm.encrypt
. The result is saved as “MORPHEUS KEY” and “GPUBLIC KEY” in sensitive_data/custom_note.txt
:
---= MORPHEUS V5.0.4 =---
***********************UNDER NO CIRCUMSTANCES DO NOT DELETE THIS FILE, UNTIL ALL YOUR DATA IS RECOVERED***********************
*****FAILING TO DO SO, WILL RESULT IN YOUR SYSTEM CORRUPTION, IF THERE ARE DECRYPTION ERRORS*****
Attention!
All your files, documents, photos, databases and other important files are encrypted and have the extension: .IBKFZ
The only method of recovering files is to purchase an unique private key. Only we can give you this key and only we can recover your files.
The server with your key is in a closed network TOR. You can get there by the following ways:
----------------------------------------------------------------------------------------
| 0. Download Tor browser - https://www.torproject.org/
| 1. Install Tor browser
| 2. Open Tor Browser
| 3. Open link in TOR browser:
| 4. Follow the instructions on this page
----------------------------------------------------------------------------------------
On our page you will see instructions on payment and get the opportunity to decrypt 1 file for free.
ATTENTION!
IN ORDER TO PREVENT DATA DAMAGE:
* DO NOT MODIFY ENCRYPTED FILES
* DO NOT CHANGE DATA BELOW
---BEGIN MORPHEUS KEY---
2f2a8daa5d3f2d3901604c7075c8471c2e3f142c1a80bb1a23aa4c6919d9dcd4af7b490002c9ad2ebbd6426484c98a304dbfe67478beb289e6cb3a01d81500d0558eea3a2807ce507b403cf5837b9870a6b84455d3fbf89f566fd2c255252b912cab5a739db2b96b4c5d738bdb77da5824c6290fecda1dde09f969c3348d035730774206be7f264bacf8ab673fe2f4ed435d3c3fe235d3257c177c593d106d2b3412f9e0395f285c0564d5714070f902d6726e9d885a030e8b79ce585bb99195c41a4477b72082bd10d97205379901f8ce113ccc9d0d118d07c05694d89317be43da78f86ad53fb8cbce56fc766dc3167accb3750d4e62cee8bae22527e32512
1cc27c4153df5fe1c875b5f594db39220a646bf225ca72e2237e41ddc6b8200c4d8a4335f4f8b3914f3d31eefca7cbf28769cd178b6e68eb4b2eac5b985bd629e208fb8d4478098eadef0a4a12b0c4b6c35735ce484a5a05540d8f635ee0bbdee7df513dfc3542c0cfbaf76fe26f01cd86d1a88e58d1712dc5228e1791da0847e839919a8fc4b827b8c25d921746d2b8e7c6508f262831a14eeef410fc9e2d3662c18a883d28f860a23fdccb9b409ada5b7a02a284b0288133ba3ae81eec7470eca71e1e96e68fe85c4e1db01e679a6814a7ae76bc9883a1650aee919b18ef46512536a5063adc6f10796eedfc9ba527b93d2afefc39c9587fc85c5017d4b21
4227fb66ff1396089d6953d7297d70484cb02ef7e8c6a428dd1bb8447ceb24339c9e74679010e68dfe74ac17b4acf36c6818d0ca24f99c35754001cb424cbb922a7b6cb8f3c5d752bc7ea75871ffe37713dd3ad5e58678f3e6117a9385016d1a00645a94637b5c029c81240e9e3b571b76375dfe9c031fb45cba73bfd59183dcd36b7ea580537f00f1517828018fba3c48ddf724b3000dd937423f4cf77f752c17ba5548f237f86141bab089a4299be42e7357bd0c8179318845d6b54ecf51240db89033e2e3cef9d01b1009b8c2678d5cb7f9814517699ed21369e6adbc51dedbd5fef5c53c08c99cde0b25e7f24f463d029764d22a67d0167af26ca4f5c8fa
...
6f317a1ae90ef57ee81accf22211fa1fbba8bc72de1fc18d0add4afc52d85de9e5d7f514d7c169b054f51a3d36e89c0a84124d23a44228405507298a750f566c4acfb0a2ed79f5a0875fb00875e92fedf06ebd5fa38f32a2c1392f00520f41d1040cf5f9c2d89555bb90ea88f0360193110bbbbf650006b3633a2626549cc3d13e37340f2d9e0ee6ea2d1e1157aeade4df6eec626abc360a257aebcdd6047027f2d6671df02eac767f58cb7b6e9c9bcbcd58eb3039cf3d4b693edc49c556e21717ec8d60fe44f62a345dddb8a1181ab7c0583d62f58f7838351e0e860defeb8c0b099b936957203c9dfe6bd7cdf006bcb76a8ad97e40c0d0dd1bebc0a768a207
272fa06fbc0b58ce8838789d62faef1fdd0f511dffe8c2286a683be4a5b93f8b943abe084cd5abb848789d10520da5f6265f435de1c9d8aab6945a3921cc3fdc9c9b33595845d24fd8793a97d4629fe03e4ead32f4160278f495a5764449532574eea5355fc9e649c0f4d58e2b1d3074d3f8aabf93c3c98877fe022f4c528205900b5582c5f3822720fba2ca8fd364727cad4c4c7d02835497ddb8336b64a847acb335c96cb0de631e48ac14ecdaa1c25c3270d72e24e93c70579555e1705e6fe87ef231c2c992a50a8c0f720c5c6c349303bc3a9d60ff21b855b3d58da09dce21cb63e8144626bb117275b0a42b03d0b7d05e42680b67208f7d5e1ebcbbcf32
---END MORPHEUS KEY---
----BEGIN GPUBLIC KEY---
9074fd8be583014d8048a1d79bfc727e13786a64b0374f7c24ba68572b8a1f64ed2360be46dc52b3c1c183843bca63b5427e39a092b2077426ab36880e456525dcc603ed18335e44f57d5eda11161982d3dca2f999d72aae0fe2b1f50f6dbff7db0395406ccf27179d3c345fe04b14e24846ee5b156457d7b4da66f922d8a72969138c09675854ddee14b8999113b223792dea8468d3afdd40a6925adc65f24758c3a1130098e50ca879f4dc421e3dca4b1e6dc3bacb5692ae2a70d8fdce2f009df6e6ab8334e541c8e50c7a9e9b949d21ce7fcad6426b596f06d8a03646150dd511d4f8b442518b6b2e74df73e50fc5dbd55765247d2f703eedf407dc7ba5fb
6badcfa5896d8c1a309e98ad2ec56d274db2dabaf8ee9253bb5f65d9949c125a153ba3e12b0a136a0a3dff2247f1b9b2d890e91613ab880bcd9dcf68f2e5f15303e8b27ea5565403674eae32c9c4a2e24bb69a5dd406b0b8386d2ec91952c3a4daff355aba68d91f7ed5b7ea687e9b964dd73fe673c348b4dba554abb0a6804c08e6b89be1d5feca76b1552bcbbb8ce6c67814bdf31a5107c91c22d055114a03a007467c5bfd664cac5b7f1ba4a97e43e3b602d18cf12c36820a2462d5868992f1cf2739198d4a1984b19adf764480a306bec8c840db8314354c74017b0fdb8697078a801b9ea8d75655e7f0b73c7343379c8f6a8c0946f2b854494b7e776662
---END GPUBLIC KEY---
Then, we have another script called server.py
:
def main(s):
gm = GM()
obf = OBF()
while True:
aesp, aesc = AESP(), AESC()
try:
sendMessage(s, "Awaiting for encryption key\n\n")
enc_key = [recieveMessage(s, "> ") for _ in range(128)]
enc_key = [int(i) for i in enc_key]
obf_key = gm.decrypt(enc_key)
key = obf.deobfuscate(obf_key)
choice = recieveMessage(
s, "\nChoose what library you want to benchmark:\n\n"
"[1] - PyCrypto AES Encrypt\n"
"[2] - cryptography AES Encrypt\n\n> ")
if choice == "1":
time, enc_file = AESEncrypt(aesp, key)
elif choice == "2":
time, enc_file = AESEncrypt(aesc, key)
else:
sendMessage(s, "Invalid choice!\n")
exit()
sendMessage(s, enc_file.hex() + "\n")
sendMessage(s, str(time) + "\n")
except Exception as e:
sendMessage(s, "Unexpected error.\n")
print(e)
Basically, it allows us to encrypt a known plaintext (note.txt
) with a controlled key. However, this key is supposed to be obfuscated and encrypted with GM
. That’s why the server takes our input, decrypts it with GM
and deobfuscates it to get the AES key. The file note.txt
is not provided, but we can guess its contents from ransomware.py
. It is basically the above sensitive_data/custom_note.txt
removing the keys:
---= MORPHEUS V5.0.4 =---
***********************UNDER NO CIRCUMSTANCES DO NOT DELETE THIS FILE, UNTIL ALL YOUR DATA IS RECOVERED***********************
*****FAILING TO DO SO, WILL RESULT IN YOUR SYSTEM CORRUPTION, IF THERE ARE DECRYPTION ERRORS*****
Attention!
All your files, documents, photos, databases and other important files are encrypted and have the extension: .IBKFZ
The only method of recovering files is to purchase an unique private key. Only we can give you this key and only we can recover your files.
The server with your key is in a closed network TOR. You can get there by the following ways:
----------------------------------------------------------------------------------------
| 0. Download Tor browser - https://www.torproject.org/
| 1. Install Tor browser
| 2. Open Tor Browser
| 3. Open link in TOR browser:
| 4. Follow the instructions on this page
----------------------------------------------------------------------------------------
On our page you will see instructions on payment and get the opportunity to decrypt 1 file for free.
ATTENTION!
IN ORDER TO PREVENT DATA DAMAGE:
* DO NOT MODIFY ENCRYPTED FILES
* DO NOT CHANGE DATA BELOW
Encryption
One curious thing is that there are two implementations of AES available:
class AESP():
def __init__(self):
self.key = aes_key
self.iv = Random.new().read(16)
def encrypt(self, msg):
cipher = PYAES.new(self.key, PYAES.MODE_CBC, self.iv)
return self.iv + cipher.encrypt(pad(msg, 16))
def decrypt(self, ct):
cipher = PYAES.new(self.key, PYAES.MODE_CBC, ct[:16])
return cipher.decrypt(ct[16:])
def setKey(self, key):
self.key = key
class AESC():
def __init__(self):
self.key = aes_key
self.iv = Random.new().read(16)
def encrypt(self, msg):
cipher = Cipher(algorithms.AES(self.key), modes.CBC(self.iv))
encryptor = cipher.encryptor()
ct = encryptor.update(pad(msg, 16)) + encryptor.finalize()
return self.iv + ct
def decrypt(self, ct):
cipher = PYAES.new(self.key, PYAES.MODE_CBC, ct[:16])
return cipher.decrypt(ct[16:])
def setKey(self, key):
self.key = key
In fact, we can choose which one to use to “benchmark” them:
def AESEncrypt(aes, key):
aes.setKey(key)
data = readFile("note.txt")
ts = time.time()
for _ in range(1000):
enc_file = aes.encrypt(data)
te = time.time()
return te - ts, enc_file
When I saw this, I thought that the attack involved a time-based approach… But it was not, as far as I know.
The obfuscation implementation is here:
class OBF():
def __init__(self):
self.seed = rseed % 2**7
seed(self.seed)
self.pin = self.genPin()
def genPin(self):
pin = []
initial = [[randint(1, 256) for _ in range(128)] for _ in range(8)]
initial = self.transpose(initial)
for i in range(128):
tmp = initial[i]
shuffle(tmp)
pin.append(tmp[0])
return ([i % 2 for i in pin])
def transpose(self, bits):
return [row for row in map(list, zip(*bits))]
def xor(self, bits, key):
return [a ^ b for a, b in zip(bits, key)]
def bytesToBits(self, bytes):
bits = bin(int(bytes.hex(), 16))[2:].zfill(len(bytes) * 8)
bits = [int(i) for i in bits]
return bits
def bitsToBytes(self, bits):
bits = "".join([str(i) for i in bits])
bits = [int(bits[i:i + 8], 2) for i in range(0, len(bits), 8)]
return bytes(bits)
def obfuscate(self, bytes):
bits = self.bytesToBits(bytes)
bits = self.xor(bits, self.pin)
return bits
def deobfuscate(self, bytes):
return self.bitsToBytes(self.obfuscate(bytes))
Basically, it takes a random seed rseed
and uses rseed % 2**7
as a new seed for the PRNG. Then it uses genPin
to generate a pin. Although we don’t know the pin, we can apply brute force because the rseed % 2**7
is an integer value between 0
and 127
. This pin is transformed to bits and used to encrypt other bits with XOR (obfuscate
). Notice that deobfuscate
calls obfuscate
because XOR is equal to its inverse.
Last but not least, we have GM
, which stands for Goldwasser–Micali. One can find this by reading the challenge’s name: “Homomurphy’s Law”. There is a type of encryption that is called homomorphic, which allows computations to be performed on encrypted data without first having to decrypt it. Anyway, here is the implementation:
class GM():
def __init__(self):
self.p = p
self.q = q
self.n = self.p * self.q
self.x = self.jacobi_symbol()
def jacobi_symbol(self):
tmp = getRandomRange(0, self.n)
while legendre(tmp, self.p) != -1 or legendre(tmp, self.q) != -1:
tmp = getRandomRange(0, self.n)
return tmp
def encrypt(self, bits):
ct = []
for bit in bits:
y = getRandomRange(0, self.n)
tmp = pow(y, 2) * pow(self.x, int(bit)) % self.n
ct.append(format(tmp, 'x'))
return ct
def decrypt(self, ct):
m = 0
for c in ct:
m <<= 1
if legendre(c % self.p, self.p) != 1 or legendre(
c % self.q, self.q) != 1:
m += 1
h = '%x' % m
return bytes.fromhex(h.zfill(32))
Goldwasser–Micali cryptosystem works as follows:
- The modulus $n$ is the public key such that $n = p q$, with $p$ and $q$ two prime numbers kept as private key
- The quadratic residue $x$ is also a public key (as a Jacobi symbol: $\left(\frac{x}{n}\right) = 1$)
- Let $\mathcal{E}: \{0, 1\} \to \mathbb{Z}/n\mathbb{Z}$ be the encryption function
- A plaintext bit $b$ is encrypted as follows: $\mathcal{E}(b) = x^b y^2 \mod{n}$ for some random value $y \in \{0, 1, \dots, n - 1\}$
The encryption function might look weak because there are only two cases: $\mathcal{E}(0) = y^2 \mod{n}$ or $\mathcal{E}(1) = x y^2 \mod{n}$. However, $x$ was chosen to be a quadratic residue, so there is no way of figuring out which result comes from $b = 0$ or $b = 1$.
The homomorphic property is the following one:
$$ \begin{align} \mathcal{E}(b_1) \cdot \mathcal{E}(b_2) & = (x^{b_1} y_1^2) \cdot (x^{b_2} y_2^2) \mod{n} \\ & = x^{b_1 + b_2} (y_1 y_2)^2 \mod{n} \\ & = \mathcal{E}(b_1 \oplus b_2) \end{align} $$
The decryption method uses $p$ and $q$ to determine if the residue is a Legendre symbol with both $p$ and $q$ (notice that $y$ can’t be a quadratic residue modulo $p$ and $q$ because these ones are primes and $y < n$).
Attack development
Recall that we can enter any obfuscated and encrypted key to encrypt note.txt
. This ability will allow us to find the AES key used to encrypt the flag.
First of all, we will find out the pin from OBF
(brute force from 0
to 127
). To do this, we can use GM
to encrypt a pin candidate, and then send it to the server, which will decrypt it back to the pin candidate. Then, the server will use OBF
to deobfuscate it to get the AES key. Once we have the correct pin, the AES key will be all null bytes, so we can compare outputs to check it.
Getting the pin
Maybe the implementation is clearer:
def h2d(h: str) -> int:
return int(h, 16)
def aes_decrypt(key: bytes, enc_data: bytes) -> bytes:
iv, ct = enc_data[:16], enc_data[16:]
cipher = AES.new(key, AES.MODE_CBC, iv)
return cipher.decrypt(ct)
def main():
host, port = sys.argv[1].split(':')
io = remote(host, int(port))
with open('sensitive_data/custom_note.txt') as f:
custom_note = f.read()
note = custom_note.split(f'\n{MBEGIN}')[0]
e_k_xor_pin = list(map(h2d, re.findall(
f'{MBEGIN}\n(.*?)\n{MEND}', custom_note, re.DOTALL)[0].split()))
n, x = map(h2d, re.findall(
f'{GBEGIN}\n(.*?)\n{GEND}', custom_note, re.DOTALL)[0].split())
gm = GM(n, x)
prog = log.progress('PIN seed')
index_prog = log.progress('Sending key index')
for s in range(128):
prog.status(str(s))
obf = OBF(s)
io.recvuntil(b'Awaiting for encryption key')
for i, p in enumerate(gm.encrypt(obf.pin)):
index_prog.status(str(i))
io.sendlineafter(b'> ', str(h2d(p)).encode())
io.sendlineafter(b'AES Encrypt\n\n> ', b'1')
enc_data = bytes.fromhex(io.recvline().decode().strip())
if note.encode() in aes_decrypt(b'\0' * 16, enc_data):
break
prog.success(str(s))
With the above, we can find the pin (it might take around 10 minutes):
$ python3 solve.py 104.248.174.109:32511
[+] Opening connection to 104.248.174.109 on port 32511: Done
[+] PIN seed: 114
[▆] Sending key index: 127
[*] Closed connection to 104.248.174.109 port 32511
Finding the AES key
Now we have these values:
zero_key = bytes.fromhex(hex(int(''.join(map(str, obf.pin)), 2))[2:])
log.success(f'OBF pin: {obf.pin}')
log.info(f'Zero key: {zero_key}')
$ python3 solve.py 104.248.174.109:32511
[+] Opening connection to 104.248.174.109 on port 32511: Done
[+] PIN seed: 114
[\] Sending key index: 127
[+] OBF pin: [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1]
[*] Zero key: b'\xc9\xbc\xeeq\x12\x98\xdf\x7f9K\xb4\x046\xc1R\x1b'
[*] Closed connection to 104.248.174.109 port 32511
The “Zero key” is the AES key that results when we send all encrypted 0
to the server. Notice that GM
will decrypt to all 0
and OBF
will use XOR with the pin we already have, so we have the resulting AES key. Using this fact, we can find the AES key used to encrypt the flag byte by byte.
For the first byte, we can do the following:
key = []
io.recvuntil(b'Awaiting for encryption key\n\n')
for i, k in enumerate(e_k_xor_pin):
index_prog.status(str(i))
io.sendlineafter(b'> ', str(k if 0 == i // 8 else k * k).encode())
io.sendlineafter(b'AES Encrypt\n\n> ', b'1')
enc_data = bytes.fromhex(io.recvline().decode().strip())
for k in range(256):
test_key = bytes([k]) + zero_key[1:]
if note.encode() in aes_decrypt(test_key, enc_data):
key.append(k)
print(bytes(key).hex())
break
The key point is here:
for i, k in enumerate(e_k_xor_pin):
index_prog.status(str(i))
io.sendlineafter(b'> ', str(k if 0 == i // 8 else k * k).encode())
We are taking the first 8
values (0
to 7
) from e_k_xor_pin
(which is the encrypted AES key and obfuscated) and sending k
. For the rest of the indices (8
to 127
), we send k * k
. Here comes the homomorphic property into place. Notice that
$$ k \cdot k = \mathcal{E}(b) \cdot \mathcal{E}(b) = \mathcal{E}(b \oplus b) = \mathcal{E}(0) $$
So, the decryption function $\mathcal{D}: \mathbb{Z}/n\mathbb{Z} \to \{0, 1\}$ will behave as
$$ \mathcal{D}(k \cdot k) = \mathcal{D}(\mathcal{E}(0)) = 0 $$
As a result, only 8 bits will contain the relevant AES key, whereas the rest of the bits will be part of the “Zero key” ($0 \oplus \mathtt{obf.pin}$). So, we can do a bit of brute force (from 0
to 255
) to find the byte from the AES key.
If we run the above code, we will get the first byte of the AES key (\x13
):
$ python3 solve.py 104.248.174.109:32511
[+] Opening connection to 104.248.174.109 on port 32511: Done
[+] PIN seed: 114
[.....\..] Sending key index: 127
[+] OBF pin: [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1]
[*] Zero key: b'\xc9\xbc\xeeq\x12\x98\xdf\x7f9K\xb4\x046\xc1R\x1b'
13
[*] Closed connection to 104.248.174.109 port 32511
Let’s automate it:
key = []
prog = log.progress('AES key')
for j in range(16):
io.recvuntil(b'Awaiting for encryption key\n\n')
for i, k in enumerate(e_k_xor_pin):
index_prog.status(str(i))
io.sendlineafter(b'> ', str(k if j == i // 8 else k * k).encode())
io.sendlineafter(b'AES Encrypt\n\n> ', b'1')
enc_data = bytes.fromhex(io.recvline().decode().strip())
for k in range(256):
test_key = zero_key[:j] + bytes([k]) + zero_key[j + 1:]
if note.encode() in aes_decrypt(test_key, enc_data):
key.append(k)
prog.status(bytes(key).hex())
break
index_prog.success()
prog.success(bytes(key).hex())
Finally, if everything worked well, the key should be valid to decrypt the flag:
with open('sensitive_data/flag.txt.IBKFZ', 'rb') as f:
flag = aes_decrypt(bytes(key), f.read())
log.success('Flag: ' + unpad(flag, 16).decode())
Flag
Let’s run it:
$ python3 solve.py 104.248.174.109:32511
[+] Opening connection to 104.248.174.109 on port 32511: Done
[+] PIN seed: 114
[+] Sending key index: Done
[+] OBF pin: [1, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1]
[*] Zero key: b'\xc9\xbc\xeeq\x12\x98\xdf\x7f9K\xb4\x046\xc1R\x1b'
[+] AES key: 13ac6164903991ced058cd2d91c36809
[+] Flag: HTB{d4n9320u5_p20p327135_c4n_134d_70_d3c2yp71n9_3v32y7h1n9}
[*] Closed connection to 104.248.174.109 port 32511
The full script can be found in here: solve.py
.