> For the complete documentation index, see [llms.txt](https://simon-6.gitbook.io/simoncyber/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://simon-6.gitbook.io/simoncyber/ctf-writeups/picoctf-2025/reverse-engineering/tap-into-hash-medium.md).

# Tap Into Hash (Medium)

### The Challenge&#x20;

<figure><img src="/files/sAvbUokTfhmih34NbeDa" alt=""><figcaption></figcaption></figure>

<pre class="language-python"><code class="lang-python">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


<strong>def pad(data, block_size):
</strong>    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)
</code></pre>

### The Libraries&#x20;

I wanted to learn all of the source code here since this looks like another dense python challenge. Hence, let's start from the libraries.&#x20;

```python
import time
import base64
import hashlib
import sys
import secrets
```

The three that stand out for me is hashlib, sys, and secrets. Hashlib just provides secure hash and message digest algorithms. For example:

```
hash_object = hashlib.sha256(text.encode())
```

The sys library provides access to system-specific parameters and functions related to the Python interpreter itself. A simple one would be sys.exit (to exit). Another is sys.argv: used to handle command-line arguments passed to a script during execution sys.argv is stored like a list environment, and contain arguments/functions of the python file.&#x20;

<figure><img src="/files/fncVMOBI8lqspazOWwvP" alt=""><figcaption></figcaption></figure>

Finally secrets is a library much like random to generate secure random numbers. It's much more secure than random, so a lot of programs tend to use this instead.&#x20;

### What Really is a Blockchain?

Since this challenge dives into a blockchain, I feel like it good to know a bit about it.&#x20;

> A **blockchain** is essentially a shared digital ledger (record book) that's distributed across many computers and designed to be extremely difficult to tamper with.

Block 1: Alice pays Bob $10 \[Hash: ABC123]\
Block 2: Bob pays Carol $5 \[Previous: ABC123, Hash: DEF456]\
Block 3: Carol pays Dave $3 \[Previous: DEF456, Hash: GHI789]

Blockchains are decentralized and transparent, perfect for something like crypto.

### The Constructor&#x20;

```python
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()
```

This is the constructor of the program. It initializes the current index, previous index, and a nonce. The calculate\_hash function gets all of the data from the initial variables, combines them all in one string, and returns the SHA256 of the string. The .encode() converts the string into bytes, sha256 produces a 32 byte hash, and hexdigest() converts the hash from binary to a hexadecimal string.&#x20;

{% hint style="info" %}
A nonce is short for "number used once". These are use in cryptographic operations, and are often the vulnerability many CTF challenges exploit. &#x20;
{% endhint %}

### Proof of Work&#x20;

```python
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
```

time.time() returns the current time from the library, and it is stored into the time stamp. The code below is creating an object from the constructor, and technically a instance of the Block class. It then calls to another function with the object, and checks if it's true/false.&#x20;

The while statement works by checking the block's hash until it's valid. It keeps incrementing its nonce by 1, and then checking the hash from the changed nonce. Every time the nonce is changed, the hash also changes. This function basically selects a hash that meet the criteria of is\_valid\_proof.

```python
def is_valid_proof(block):
    guess_hash = block.calculate_hash()
    return guess_hash[:2] == "00"
```

This checks if the hash contains the string "00" at the first 2 indexes. I'm not sure why this is valid but let's see from the rest of the code.&#x20;

### More functions&#x20;

```python
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
```

The first three functions are get/set methods. The encrypt function starts with getting a piece of text, and inserting them into a new modified plaintext. It then sends them over the pad function with a block size, which I'm only guessing just adds 16 bytes of padding. The digest() function returns the string as raw binary bytes (not a hexadecimal string).

```python
def pad(data, block_size):
    padding_length = block_size - len(data) % block_size
    padding = bytes([padding_length] * padding_length)
    return data.encode() + padding
```

The pad function first take the modulus of 16 with len(data), basically leaving whatever remains. For example, if the length of data was 33, it would leave 1. It would then become 15 because of the block size. This is the "how many bytes over we're from the last block." Since we want a block to be 16, this would then create lengths of 15, 15 times to accumulate.&#x20;

{% hint style="info" %}
The ciphertext = b'' creates an empty byte object!&#x20;
{% endhint %}

***

The for loops for the range of the amount of blocks are in the modified plaintext (remember its modified!). It takes one block of the plaintext, and puts it into the xor\_bytes with the byte block from plaintext, and the bytes of plaintext. This takes us to the xor\_bytes function.&#x20;

```
def xor_bytes(a, b):
    return bytes(x ^ y for x, y in zip(a, b))
```

Let's analysis this one by one. The x ^ y is a XOR operator, in where it flip bits in where it differ. I explained it in a previous CTF.  The zip function pairs them. Hence, for every pair of bytes, it XOR them.&#x20;

```
#### ZIP FUNCTION ####
Input: a = ["Liam", "Emma", "Noah"], b = [90, 85, 88]
Output: [("Liam", 90), ("Emma", 85), ("Noah", 88)]
```

And after it runs that, it adds block (byte), by block to the new cipher text.&#x20;

***

```python
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)
```

The "return secrets.token\_hex(length // 2)" generates n bytes. They then divide it by half to get the hex length.&#x20;

{% hint style="info" %}
One byte is 2 hex characters!
{% endhint %}

Next is the main function, where I'll suspect is the actual vulnerability. It first converts it back to bytes, creates a instance of the Block class, which is called the genesis block. The genesis block is the first block that is manually created in the entire blockchain.&#x20;

The for loop creates 4 new additional blocks using the past blocks.

{% hint style="info" %}
If there's confusion between what each type mention in this challenge looks like: This is why it's needed to convert to bytes (binary) to do XOR.&#x20;
{% endhint %}

<figure><img src="/files/G1CnRigSbUHBpB7tcgIt" alt=""><figcaption></figcaption></figure>

### The Vulnerability&#x20;

The thing with this code is that there's isn't actual encryption happening. However, since I'm fairly knew, even though I knew what was happening, I couldn't quite think of way of reversing it via writing my own script.

{% embed url="<https://www.youtube.com/watch?v=oS9mfG0CtcM>" %}

I used this video to help me guide. It was a lot simpler then I thought previously.&#x20;

```python
import hashlib

def xor_bytes(a, b):
    return bytes(x ^ y for x, y in zip(a, b))

def decrypt(ciphertext, key):
    block_size = 16
    key_hash = hashlib.sha256(key).digest()
    
    plaintext = b''
    
    for i in range(0, len(ciphertext), block_size):
        block = ciphertext[i:i + block_size]
        plain_block = xor_bytes(block, key_hash)
        plaintext += plain_block
    
    return plaintext

encrypted_data = b'o\x14>\xda\x16\xc7\xce\xd784,.\x8f2\x80@cD?\xd3L\x90\x9f\x87l0yy\xdam\x85J1\x139\x88\x10\x95\x9f\x82ke*.\xda>\xd3\x195O?\xd3\x10\xc4\x94\x83kd| \x882\x86IzFk\xd2@\x95\xcd\x83:by{\x8c3\x81\x1e6E8\xdaC\xc2\xcf\xd087||\x8em\xd0\x1c3Cj\xde\x10\xc0\x99\x89;4+{\x8fn\x84\x1abN9\xdc\x16\xc5\xc8\xd0mc}+\x81o\xd7J1[k\xda\x12\x94\xce\x89m3}(\xdbh\x84KeDh\x8fF\x9e\xc9\x84i1.*\x8f2\x87\x1d4\x15+\x83\x17\xc9\xef\xe5Iz*t\xd7h\xd9\'d%\t\x82"\xcf\xfe\xd3[09{\xe0T\xea-=;k\x98@\x9f\xcf\xf9Pp\x0bb\xd5A\xe8\x02\x15=\x04\x8eL\x96\x9f\x86n0ze\x89m\x81A6Bi\x88B\x91\xce\x8279q~\x8d:\x84OfAn\x8fA\x95\xc9\xd76d|}\x95;\x82@oFb\xddE\x93\x98\xd2jdz/\x88h\xd7\x1a6@o\xdd\x12\x94\x94\xd2m`}y\x8c>\x82LaCm\x8f\x10\x9f\x9f\xd3>0py\xde2\x87AoGh\x88\x11\x91\x9a\x84=3z*\xde&\x82Hb@n\xddM\xc3\xce\x89me.(\x808\xd0KaGo\x8bG\x91\x9b\x81?`.|\xde=\xd6@b\x17m\x8e\x11\x9f\x95\x80>d+.\x88>\x80\x1d4\x14m\xdd\x12\x96\xcf\x85m1q-\x8ao\xb0z'

key = b"\x1br\t;\x0f\xb5\x9f\xaa\xd1'\xaf\x86[\xf0\xe6\xd9'D\xf9\x8d\x17g\xeb>_gG.\xd4\xc3\xdc\x83"

decrypted_text = decrypt(encrypted_data, key)
print('decrypted text: ', decrypted_text.decode(errors='ignore'))
```

What we basically did is get the essential stuff from the code. Pretty much everything was just a red herring, and the thing we needed to focus on was the encryption (as expected). We kept the XOR\_bytes function (as it can be applies inversely), and we basically just flipped everything with the previous encrypt function to a decrypt function. Since the flag is slapped right middle in the key, once we get the unencrypted key the flag is right there in the middle.&#x20;

***

To explain the decrypt function, we get the ciphertext, and the key. These two are both "encrypted", so we're passing the encrypted versions! We establish the plaintext, and we loop through block sizes of the cipher text. We UN-XOR them (I'm not sure the wording of that) of each block of encrypted data, and add them to our plaintext. Finally we get left with chains of hashes, in which include the flag!&#x20;

Overall this challenge taught me a lot honestly, and although I struggled with it for the past 2 days, I liked it.&#x20;
