• Points: 2400

Description

The final battle! The elves want revenge for their lost game! They have enhanced the tic-tac-toe game with blockchain technology. Cyber Security on the Blockchain will revolutionize everything, but most importantly ensure they will win this time. No cheating Santa!

Visit https://24.adventofctf.com to start the challenge.

Recon

When opening the page, we’re greeted with what looks like the same screen as we had on challenge 20. The only noticeable difference is the addition of blockchain? True in the footer.

If we take a look at the source, we also find a comment with the following python code:

# <!-- Development notes: Do not let santa see!

def hash_string(string):
    return hashlib.md5(string.encode('utf-8')).hexdigest()

def hash_row(row):
    conv = lambda i : i or ' '
    res = [conv(i) for i in row]
    return hash_string(' '.join(res))

def hash_board(board):
    acc = ""
    for row in board:
        acc += hash_row(row)
    return acc

def verify_chain(game):
    board=game["board"]
    chain = game["chain"]

    if len(chain) > 0:
        if board != chain[-1]["board"]:
            return False

    for i in range(len(chain)):
        block=chain[i]
        h = hash_board(block["board"])
        h = hash_string(h + block["prev"])
        if h != block["hash"]:
            return False
    return True

# -->

Just like in challenge 20, we alse find a game cookie:

game=gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg==

Finding the vulnerability

Using the same method as in challenge 20, we can take a look at the board:

In [1]: import pickle

In [2]: import base64

In [3]: pickle.loads(base64.b64decode("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhoBGgGaAZlZVgEAAAAdHVybnEIaAZYCAAAAGZpbmlzaGVkcQmJWAYAAAB3aW5uZXJxClgAAAAAcQtYBAAAAHNhbmVxDIhYCgAAAGJsb2NrY2hhaW5xDYhYBQAAAGNoYWlucQ5dcQ8ofXEQKGgBXXERKF1xEihOTk5lXXETKE5OTmVdcRQoTk5oBmVlWAQAAABwcmV2cRVYIAAAAGNlZjIxNWM1YmU4Y2Y2M2ZjZjNkNDNlY2YyNTEwYjMzcRZYBAAAAGhhc2hxF1ggAAAAZTdkYzhlMWY3YTY3ODhiYzBjYjY4NDE1MzhiMjE2ZThxGHV9cRkoaAFdcRooXXEbKGgETk5lXXEcKE5OTmVdcR0oTk5oBmVlaBVoGGgXWCAAAABmYzkzMjM2YjVlZWE1ZjFkNTVlMmI1YjMwOGQ2NzM5MHEedX1xHyhoAV1xIChdcSEoaAROTmVdcSIoTmgGTmVdcSMoTk5oBmVlaBVoHmgXWCAAAABhOGRjMGQzZGEyOTBkMWU4OTRlYWFmZmNiOTgzOThjOXEkdX1xJShoAV1xJihdcScoaARoBE5lXXEoKE5oBk5lXXEpKE5OaAZlZWgVaCRoF1ggAAAAZTc0ZjViMjJmNTIxM2JhNGMyNDQ5NzU5Y2U5MWMyYWFxKnV9cSsoaAFdcSwoXXEtKGgEaAROZV1xLihOaAZoBmVdcS8oTk5oBmVlaBVoKmgXWCAAAABlZjI1NTE0ZGZmYmY4MjQ3Y2ZmNjA2M2JlOTBmMmQ1NHEwdX1xMShoAV1xMihdcTMoaARoBE5lXXE0KGgEaAZoBmVdcTUoTk5oBmVlaBVoMGgXWCAAAABlM2E0YzAzN2JkZjE1NGIzNDRlZDliZDE2NDNhNjI5ZHE2dX1xNyhoAV1xOChdcTkoaARoBE5lXXE6KGgEaAZoBmVdcTsoTmgGaAZlZWgVaDZoF1ggAAAAYzI0MGZhMTYxNzM3Yzk2N2VjZTVmZDk0NjcyYWIwZjhxPHV9cT0oaAFdcT4oXXE/KGgEaAROZV1xQChoBGgGaAZlXXFBKGgEaAZoBmVlaBVoPGgXWCAAAAAyNGJhNzE1ZGMwNTY4M2NlNDViOWUxOTFlZDE4OGI5Y3FCdWV1Lg=="))

This time our board is quite a bit larger:

{
  "blockchain": True,
  "board": [
    ["O", "O", None],
    ["O", "X", "X"],
    [None, "X", "X"]
  ],
  "chain": [
    {
      "board": [
        [None, None, None],
        [None, None, None],
        [None, None, "X"]
      ],
      "hash": "e7dc8e1f7a6788bc0cb6841538b216e8",
      "prev": "cef215c5be8cf63fcf3d43ecf2510b33"
    },
    {
      "board": [
        ["O", None, None],
        [None, None, None],
        [None, None, "X"]
      ],
      "hash": "fc93236b5eea5f1d55e2b5b308d67390",
      "prev": "e7dc8e1f7a6788bc0cb6841538b216e8"
    },
    {
      "board": [
        ["O", None, None],
        [None, "X", None],
        [None, None, "X"]
      ],
      "hash": "a8dc0d3da290d1e894eaaffcb98398c9",
      "prev": "fc93236b5eea5f1d55e2b5b308d67390"
    },
    {
      "board": [
        ["O", "O", None],
        [None, "X", None],
        [None, None, "X"]
      ],
      "hash": "e74f5b22f5213ba4c2449759ce91c2aa",
      "prev": "a8dc0d3da290d1e894eaaffcb98398c9"
    },
    {
      "board": [
        ["O", "O", None],
        [None, "X", "X"],
        [None, None, "X"]
      ],
      "hash": "ef25514dffbf8247cff6063be90f2d54",
      "prev": "e74f5b22f5213ba4c2449759ce91c2aa"
    },
    {
      "board": [
        ["O", "O", None],
        ["O", "X", "X"],
        [None, None, "X"]
      ],
      "hash": "e3a4c037bdf154b344ed9bd1643a629d",
      "prev": "ef25514dffbf8247cff6063be90f2d54"
    },
    {
      "board": [
        ["O", "O", None],
        ["O", "X", "X"],
        [None, "X", "X"]
      ],
      "hash": "c240fa161737c967ece5fd94672ab0f8",
      "prev": "e3a4c037bdf154b344ed9bd1643a629d"
    }
  ],
  "finished": False,
  "sane": True,
  "turn": "O",
  "winner": ""
}

We can see that a chain value has been added. From the title of this challenge, we can say that this is the blockchain that we likely have to bypass.

Exploit

Cracking this shouldn’t be too hard. Let’s start by copying our script from challenge 20 and resetting the chain:

import base64
import pickle

board_b64 = "gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg=="

game = pickle.loads(base64.b64decode(board_b64))

game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
game["chain"] = []
game["finished"] = True
game["winner"] = 'X'
game["turn"] = 'O'

print(base64.b64encode(pickle.dumps(data)))

Sadly, just submitting the result of this script doesn’t work. We have to fill the chain.

To find out how the chain works, let’s take another look at the verify_chain() function:

def verify_chain(game):
    board=game["board"]
    chain = game["chain"]

    if len(chain) > 0:
        if board != chain[-1]["board"]:
            return False

    for i in range(len(chain)):
        block=chain[i]
        h = hash_board(block["board"])
        h = hash_string(h + block["prev"])
        if h != block["hash"]:
            return False
    return True

We can see that, to verify the game, the function loops through all the blocks in the chain. It then calculates a hash based on the board, and the previous hash and compares that to the actual hash in the block (the user provided one).

To crack this, we can simply reverse this algorithm. To do this, I grabbed the script from the HTML source, and added a crack method like so:

import hashlib
import pickle
import base64


def load_base64(base64_string):
    return pickle.loads(base64.b64decode(base64_string))


def export_base64(game):
    return base64.b64encode(pickle.dumps(game)).decode()


def hash_string(string):
    return hashlib.md5(string.encode('utf-8')).hexdigest()


def hash_row(row):
    def conv(i): return i or ' '
    res = [conv(i) for i in row]
    return hash_string(' '.join(res))


def hash_board(board):
    acc = ""
    for row in board:
        acc += hash_row(row)
    return acc


def verify_chain(game):
    board = game["board"]
    chain = game["chain"]

    if len(chain) > 0:
        if board != chain[-1]["board"]:
            return False

    for block in chain:
        h = hash_board(block["board"])
        h = hash_string(h + block["prev"])
        if h != block["hash"]:
            return False
    return True


def crack(game):
    game["board"] = [['X', 'X', 'X'], ['O', None, 'O'], [None, None, None]]
    game["chain"] = []
    game["finished"] = True
    game["winner"] = 'X'
    game["turn"] = 'O'

    board = game["board"]

    h = hash_board(board)
    h = hash_string(h)

    game["chain"].append({
        "board": board,
        "hash": h,
        "prev": ""
    })


if __name__ == "__main__":
    game = load_base64("gAN9cQAoWAUAAABib2FyZHEBXXECKF1xAyhYAQAAAE9xBGgETmVdcQUoaARYAQAAAFhxBmgGZV1xByhOaAZoBmVlWAQAAAB0dXJucQhoBFgIAAAAZmluaXNoZWRxCYlYBgAAAHdpbm5lcnEKWAAAAABxC1gEAAAAc2FuZXEMiFgKAAAAYmxvY2tjaGFpbnENiFgFAAAAY2hhaW5xDl1xDyh9cRAoaAFdcREoXXESKE5OTmVdcRMoTk5OZV1xFChOTmgGZWVYBAAAAHByZXZxFVggAAAAY2VmMjE1YzViZThjZjYzZmNmM2Q0M2VjZjI1MTBiMzNxFlgEAAAAaGFzaHEXWCAAAABlN2RjOGUxZjdhNjc4OGJjMGNiNjg0MTUzOGIyMTZlOHEYdX1xGShoAV1xGihdcRsoaAROTmVdcRwoTk5OZV1xHShOTmgGZWVoFWgYaBdYIAAAAGZjOTMyMzZiNWVlYTVmMWQ1NWUyYjViMzA4ZDY3MzkwcR51fXEfKGgBXXEgKF1xIShoBE5OZV1xIihOaAZOZV1xIyhOTmgGZWVoFWgeaBdYIAAAAGE4ZGMwZDNkYTI5MGQxZTg5NGVhYWZmY2I5ODM5OGM5cSR1fXElKGgBXXEmKF1xJyhoBGgETmVdcSgoTmgGTmVdcSkoTk5oBmVlaBVoJGgXWCAAAABlNzRmNWIyMmY1MjEzYmE0YzI0NDk3NTljZTkxYzJhYXEqdX1xKyhoAV1xLChdcS0oaARoBE5lXXEuKE5oBmgGZV1xLyhOTmgGZWVoFWgqaBdYIAAAAGVmMjU1MTRkZmZiZjgyNDdjZmY2MDYzYmU5MGYyZDU0cTB1fXExKGgBXXEyKF1xMyhoBGgETmVdcTQoaARoBmgGZV1xNShOTmgGZWVoFWgwaBdYIAAAAGUzYTRjMDM3YmRmMTU0YjM0NGVkOWJkMTY0M2E2MjlkcTZ1fXE3KGgBXXE4KF1xOShoBGgETmVdcTooaARoBmgGZV1xOyhOaAZoBmVlaBVoNmgXWCAAAABjMjQwZmExNjE3MzdjOTY3ZWNlNWZkOTQ2NzJhYjBmOHE8dWV1Lg==")
    crack(game)
    print(export_base64(game))

After running this script, we get the following result:

gASVsgAAAAAAAAB9lCiMBWJvYXJklF2UKF2UKIwBWJRoBGgEZV2UKIwBT5ROaAZlXZQoTk5OZWWMBHR1cm6UaAaMCGZpbmlzaGVklIiMBndpbm5lcpRoBIwEc2FuZZSIjApibG9ja2NoYWlulIiMBWNoYWlulF2UfZQojAVib2FyZJRoAowEaGFzaJSMIDZkZTM2NDM0OTAwZTI4YTdlYWYwNDhhYjBhY2JlNjA0lIwEcHJldpSMAJR1YXUu

When we set this and refresh the page, we get the following message: "Game goes to: X NOVI{blockchain_cyb3r_security} Thank you for playing Advent of CTF! I hope you have a great christmas!".

Solution

We got the flag! It is NOVI{blockchain_cyb3r_security}.