Problem
Solution
Let’s start by looking at the source code:
import time
import base64
import hashlib
import sys
import secrets
class Block:
def __init__(self, index, previous_hash, timestamp, encoded_transactions, nonce):
self.index = index
self.previous_hash = previous_hash
self.timestamp = timestamp
self.encoded_transactions = encoded_transactions
self.nonce = nonce
def calculate_hash(self):
block_string = f"{self.index}{self.previous_hash}{self.timestamp}{self.encoded_transactions}{self.nonce}"
return hashlib.sha256(block_string.encode()).hexdigest()
def proof_of_work(previous_block, encoded_transactions):
index = previous_block.index + 1
timestamp = int(time.time())
nonce = 0
block = Block(index, previous_block.calculate_hash(),
timestamp, encoded_transactions, nonce)
while not is_valid_proof(block):
nonce += 1
block.nonce = nonce
return block
def is_valid_proof(block):
guess_hash = block.calculate_hash()
return guess_hash[:2] == "00"
def decode_transactions(encoded_transactions):
return base64.b64decode(encoded_transactions).decode('utf-8')
def get_all_blocks(blockchain):
return blockchain
def blockchain_to_string(blockchain):
block_strings = [f"{block.calculate_hash()}" for block in blockchain]
return '-'.join(block_strings)
def encrypt(plaintext, inner_txt, key):
midpoint = len(plaintext) // 2
first_part = plaintext[:midpoint]
second_part = plaintext[midpoint:]
modified_plaintext = first_part + inner_txt + second_part
block_size = 16
plaintext = pad(modified_plaintext, block_size)
key_hash = hashlib.sha256(key).digest()
ciphertext = b''
for i in range(0, len(plaintext), block_size):
block = plaintext[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
ciphertext += cipher_block
return ciphertext
def pad(data, block_size):
padding_length = block_size - len(data) % block_size
padding = bytes([padding_length] * padding_length)
return data.encode() + padding
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def generate_random_string(length):
return secrets.token_hex(length // 2)
random_string = generate_random_string(64)
def main(token):
key = bytes.fromhex(random_string)
print("Key:", key)
genesis_block = Block(0, "0", int(time.time()), "EncodedGenesisBlock", 0)
blockchain = [genesis_block]
for i in range(1, 5):
encoded_transactions = base64.b64encode(
f"Transaction_{i}".encode()).decode('utf-8')
new_block = proof_of_work(blockchain[-1], encoded_transactions)
blockchain.append(new_block)
all_blocks = get_all_blocks(blockchain)
blockchain_string = blockchain_to_string(all_blocks)
encrypted_blockchain = encrypt(blockchain_string, token, key)
print("Encrypted Blockchain:", encrypted_blockchain)
if __name__ == "__main__":
text = sys.argv[1]
main(text)
Here’s a few things I noted:
- The flag is located in the centre of the plaintext
- Each block is 16 bytes
- Each block is encrypted using a XOR cipher with the same part of the hashed key
So, given the key, we can decrypt each block of the encrypted blockchain and extract the flag from the centre. Lucky for us, we’re given both pieces of the puzzle:
Key: b'\x8b\x9a\x00G\xfe\xb3\xf3\x93\xdb\xa8yT\xfe\x15\x87a\xf4\xdf\x00\x8d\xee\xab\xd9\t^|\x04(%\x81\x9e\xf8'
Encrypted Blockchain: b'Z5Wo\xe9\xbd\xf4\xed<\xeb=\xcb%\xc4\xf0>S2\x0bl\xe9\xe9\xf0\xe8>\xe8n\x91q\xca\xad=\x01fRo\xbe\xba\xa2\xb5f\xb88\x90t\xc4\xaco\x041\x01n\xb3\xbd\xa1\xb4l\xee9\xc9u\x9e\xf1:O0\x035\xed\xeb\xf6\xean\xbei\xcc.\x9f\xfe8\x008\x04l\xef\xee\xf4\xe9g\xbf8\x9bt\x9f\xaanZ6\x0b8\xef\xbb\xa6\xb8>\xe9n\xcd$\xc5\xf08\x00eQ>\xef\xed\xf4\xeaf\xbdm\x9e \xcb\xfe9\x07-\x03=\xba\xb9\xa2\xbcl\xb4:\x9c&\x9e\xab8U4\x05=\xe8\xec\xa4\xeao\xefk\x9c#\x9a\xacn\x00bCd\xe8\xb7\xd6\xd8\x19\xf69\xc4x\x9f\xa2UQSae\xdd\xb1\xc7\xee\x0b\xbc*\xcbO\xa3\x91_\x08M\x03\x7f\xbf\xe1\xf6\xc4\x00\xfc\x18\xd2z\xb6\x93p Kl;\xbb\xee\xa1\xbb9\xef9\xd5t\x99\xf9;Ve\x04i\xb3\xe9\xf3\xbd;\xb8m\x91s\xcf\xff:\x019\x04=\xbd\xb9\xa4\xeff\xb5>\xcd:\xcc\xf9<Ta\x01?\xbd\xe9\xf0\xed=\xefi\x9fr\x98\xf8=[a\x00:\xbc\xba\xf3\xe9k\xeci\x90"\x98\xfbnZaPk\xed\xe9\xa6\xbf=\xe8>\x99.\xc5\xac?Wa\x02<\xbd\xb9\xa0\xeaj\xecn\x9b$\xd1\xf9:\x04dR:\xb9\xea\xa3\xedi\xebh\x9d\'\xcc\xfah\x008U9\xe8\xe8\xf6\xba>\xefk\x98#\xce\xfb;\x038\n?\xea\xb9\xf7\xbfg\xbbk\x90q\x9e\xacn[f\x06l\xef\xbb\xa7\xbaf\xbd9\x9a"\xcf\xcb\x08'
What follows is the simple script I wrote to handle the decryption.
Script
import hashlib
import re
block_size = 16
def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))
def decrypt(ciphertext, key):
plaintext = b''
key_hash = hashlib.sha256(key).digest()
for i in range(0, len(ciphertext), block_size):
# NOTE: Each block encrypted using the same part of the key
block = ciphertext[i:i + block_size]
plain_block = xor_bytes(block, key_hash)
plaintext += plain_block
return plaintext
def main():
with open("enc_flag", "r") as f:
key, c = f.readlines()
key = eval(key[5:])
c = eval(c[22:])
decrypted_blockchain = decrypt(c, key)
# The flag is the token in the middle of the plaintext!
flag = re.search(b'picoCTF{.*}', decrypted_blockchain)
print(flag.group(0).decode("utf-8"))
if __name__ == "__main__":
main()