CTF-Writeups

Andor – HeroCTF Crypto (Easy, 50 pts)


Challenge description

Would you rather be inside solving challenges AND getting flags OR outside touching grass?

Connecting to the service gives us two hexadecimal strings on each round:

a = <hex>
o = <hex>
>

Sending an empty line makes the server generate a new pair a, o based on the same secret flag.

From the challenge source (in andor.zip) we learn that:

Our job is to reconstruct the original flag bits from these noisy AND/OR views.


Bitwise behaviour

Consider one bit of the flag f and the corresponding bit of the random key k.
We observe either f & k or f | k.

The possibilities are:

f k f & k f | k
0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 1

Key observations:

So each query gives us partial information: some bits are determined, others ambiguous.


Using many queries to remove ambiguity

The server lets us ask for as many (a, o) pairs as we want, each time with fresh random keys but the same flag.

For each bit position:

To aggregate information across queries we can use:

Thanks to randomness, after enough iterations every bit will eventually land in a “certain” case (either witnessed as 1 in the AND stream or 0 in the OR stream). When two consecutive iterations no longer change our accumulated values, we can assume the flag is fully recovered.


Exploit strategy

  1. Connect to the service and read the first a and o.
  2. Treat them as our current best guesses:
    • flag_and – half of the flag learnt from AND-ed bits.
    • flag_or – half of the flag learnt from OR-ed bits.
  3. In a loop:
    • Request another round of a, o.
    • Update:
      • flag_and = flag_and | a (bitwise OR, byte by byte)
      • flag_or = flag_or & o (bitwise AND, byte by byte)
    • Concatenate flag_and + flag_or as a candidate flag.
    • Stop when the candidate flag stops changing between iterations.
  4. Print the final candidate as ASCII: this is our flag.

Exploit script (no comments)

from pwn import *

HOST = "crypto.heroctf.fr"
PORT = 9000


def b_and(x: bytes, y: bytes) -> bytes:
    return bytes(a & b for a, b in zip(x, y))


def b_or(x: bytes, y: bytes) -> bytes:
    return bytes(a | b for a, b in zip(x, y))


def read_pair(io):
    line_a = io.recvlineS().strip()
    line_o = io.recvlineS().strip()
    a_hex = line_a.split("a = ")[-1]
    o_hex = line_o.split("o = ")[-1]
    return bytes.fromhex(a_hex), bytes.fromhex(o_hex)


def recover_flag(io, verbose=False) -> bytes:
    flag_a, flag_o = read_pair(io)
    current = flag_a + flag_o
    previous = None
    io.sendline(b"")
    while current != previous:
        previous = current
        a, o = read_pair(io)
        flag_a = b_or(flag_a, a)
        flag_o = b_and(flag_o, o)
        current = flag_a + flag_o
        if verbose:
            print(current)
        io.sendline(b"")
    return current


def main():
    import argparse

    parser = argparse.ArgumentParser(description="Exploit script for HeroCTF Andor")
    parser.add_argument(
        "--local",
        action="store_true",
        help="Run against local chall.py instead of remote service",
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="Print intermediate candidate flags",
    )
    args = parser.parse_args()

    if args.verbose:
        context.log_level = "debug"

    if args.local:
        io = process(["python3", "chall.py"])
    else:
        io = remote(HOST, PORT)

    flag_bytes = recover_flag(io, verbose=args.verbose)
    try:
        flag_str = flag_bytes.decode()
    except UnicodeDecodeError:
        flag_str = flag_bytes.decode(errors="replace")

    print("Recovered flag:", flag_str)


if __name__ == "__main__":
    main()

Running this script eventually stabilises and prints something like:

Recovered flag: Hero{y0u_4nd_5l33p_0r_y0u_4nd_c0ff33_3qu4l5_fl4g_4nd_p01n75}