nc crypto.heroctf.fr 9000^Hero{\S+}$andor.zipWould 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.
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:
a = f & k
1, the flag bit must be 1.0, the flag bit could be 0 or 1 (we don’t know if k was 0 or 1).o = f | k
0, the flag bit must be 0.1, the flag bit could be 0 or 1.So each query gives us partial information: some bits are determined, others ambiguous.
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:
a equal to 1, we know the flag bit is 1 there.o equal to 0, we know the flag bit is 0 there.To aggregate information across queries we can use:
a values
1 as soon as any query proves it must be 1.o values
0 as soon as any query proves it must be 0.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.
a and o.flag_and – half of the flag learnt from AND-ed bits.flag_or – half of the flag learnt from OR-ed bits.a, o.flag_and = flag_and | a (bitwise OR, byte by byte)flag_or = flag_or & o (bitwise AND, byte by byte)flag_and + flag_or as a candidate flag.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}