case-insensitive

This was the one problem I downloaded but failed, but it’s probably worth mentioning:

from flag import flag
import signal
import bcrypt

def check_and_upper(message):
    if len(message) > 24:
        return None
    message = message.upper()
    for c in message:
        c = ord(c)
        if ord("A") > c or c > ord("Z"):
            return None
    return message

signal.alarm(600)
while True:
    mode = input(
        """1. sign
2. verify
mode: """
    ).strip()

    ## sign mode ##
    if mode == "1":
        message = check_and_upper(input("message: ")) # case insensitive

        if message == None:
            print("invalid")
            continue

        salt = bcrypt.gensalt(5)
        print("mac:", bcrypt.hashpw((message + flag).encode(), salt).decode("utf-8"))

    ## verify mode ##
    else:
        mac = input("mac: ")
        message = check_and_upper(input("message: ")) # case insensitive

        if message is None:
            print("invalid")
            continue

        print("result:", bcrypt.checkpw((message + flag).encode(), mac.encode()))

Some kind of library bug? It doesn’t seem to support $2$ or anything like that. Some unicode fuckery with the mac we give? No, it bans \x00s, it bans >127 characters… At least the package I installed. (I tried to do this one offline, so I don’t know what was used on the server, and I saw there’s a lot of other less-popular bcrypt implementations, but didn’t have the energy to check all of them, and I doubt that’s the solution?)

Surely it can’t be a timing thing? (We can set the rounds, but how does that help?) I can’t help but think the code is really suspicious because it’s “childishly” simple (the check_and_uppercase()), like it’s trying really hard to look innocuous. But I just ended up scratching my head and I am out of energy. I’d appreciate if anyone gives me a hint for some later attempt.

Edit: poiko got the solution and gave me a pretty heavy hint about looking into the length of message.upper() and indeed:

>>> [(ord(c), c, len(c.upper()), mapl(ord, c.upper())) for c in map(chr, range(0x110000)) if len(c.upper()) > 2]
[(912, 'ΐ', 3, [921, 776, 769]), (944, 'ΰ', 3, [933, 776, 769]), (8018, 'ὒ', 3, [933, 787, 768]), (8020, 'ὔ', 3, [933, 787, 769]), (8022, 'ὖ', 3, [933, 787, 834]), (8119, 'ᾷ', 3, [913, 834, 921]), (8135, 'ῇ', 3, [919, 834, 921]), (8146, 'ῒ', 3, [921, 776, 768]), (8147, 'ΐ', 3, [921, 776, 769]), (8151, 'ῗ', 3, [921, 776, 834]), (8162, 'ῢ', 3, [933, 776, 768]), (8163, 'ΰ', 3, [933, 776, 769]), (8167, 'ῧ', 3, [933, 776, 834]), (8183, 'ῷ', 3, [937, 834, 921]), (64259, 'ffi', 3, [70, 70, 73]), (64260, 'ffl', 3, [70, 70, 76])]

This solves the task because bcrypt does a quiet string = string[:72] truncation on its side, so flag can be solved byte-by-byte.

I had no idea about upper()/lower() changing the length (it only happens with one character for lowercase), though 20-20 says I’m dumb for not checking it because,

  1. I was already suspicious of the “style” of the code, i.e. the style of code strongly hints there’s some subtle trickery going on, like in this case the .upper() happening after the length check, and
  2. I did see the truncation in bcrypt so I knew that if the string could be made longer the task would be solved…