I wanted this to look like a Flappy Bird challenge, with the flag sitting behind a little client/server protocol. The game only starts talking to the server when the score reaches 31337. Players got the compiled binary, so the job was to work out what the client was doing and decide whether to make the game do it for you or skip the game entirely.

The challenge source. There are two solves: reverse the three responses and speak to the server yourself, or change the score in memory and let the client handle the connection.

The network path

I started looking for the socket calls and the strings passed to sscanf and snprintf. The interesting loop is short once you get past the game code: receive CHALLENGE_n: number, parse the number, calculate a response, and send RESPONSE_n: number. It repeats for three rounds, then prints whatever the server sends back.

The client also builds its server address at runtime from encoded bytes and a key derived from the score and bird coordinates. That is worth tracing if you need the original endpoint. For the protocol solve, the host and port are just connection arguments; the important part is reproducing the three responses.

There is plenty of fake work in the client. I ignored calculations that never affect the bytes sent over the socket. A good way to keep your bearings in a noisy binary is to follow one value from the receive buffer, through the arithmetic, to the response formatting call.

Reversing the three rounds

The server sends each challenge as an unsigned 32-bit number. C's unsigned int arithmetic wraps around at 32 bits. Python integers do not, so the solver masks intermediate results with 0xffffffff.

Round one multiplies the challenge by 0x1337 and XORs the result with 0xDEADBEEF:

result = ((challenge * 0x1337) ^ 0xDEADBEEF) & 0xffffffff

Round two rotates left by five bits and adds 0xCAFEBABE. The two shifts are both needed: the right shift wraps the high five bits back into the low end.

result = (((challenge << 5) | (challenge >> 27)) + 0xCAFEBABE) & 0xffffffff

Round three starts with challenge ^ 0x31337, then mixes the value 16 times. The update uses the previous value's top bit before shifting, then XORs a round-dependent constant:

result = challenge ^ 0x31337
for i in range(16):
    result = ((result << 1) ^ (result >> 31)) & 0xffffffff
    result ^= (i * 0xA5A5A5A5) & 0xffffffff

Keep the 32-bit mask inside the loop. If you leave Python's high bits around, later shifts use a different value and round three stops matching the client.

Solve 1: answer the server directly

The server does not ask the client to prove that it played the game. It only checks the three response lines. Once the formulas are known, we can skip the bird and connect directly.

#!/usr/bin/env python3
import argparse
import re
import socket

MASK = 0xffffffff

def response(x, round_number):
    if round_number == 1:
        return ((x * 0x1337) ^ 0xdeadbeef) & MASK
    if round_number == 2:
        return (((x << 5) | (x >> 27)) + 0xcafebabe) & MASK
    if round_number == 3:
        x ^= 0x31337
        for i in range(16):
            x = ((x << 1) ^ (x >> 31)) & MASK
            x ^= (i * 0xa5a5a5a5) & MASK
        return x
    raise ValueError('unexpected round')

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('host')
    parser.add_argument('port', type=int)
    args = parser.parse_args()

    with socket.create_connection((args.host, args.port), timeout=10) as sock:
        stream = sock.makefile('rb')
        for rnd in range(1, 4):
            line = stream.readline()
            match = re.fullmatch(rb'CHALLENGE_(\d+): (\d+)\s*', line)
            if not match or int(match[1]) != rnd:
                raise ValueError(f'unexpected challenge: {line!r}')
            answer = response(int(match[2]), rnd)
            sock.sendall(f'RESPONSE_{rnd}: {answer}\n'.encode())
        print(stream.read().decode(), end='')

if __name__ == '__main__':
    main()

Run it with the host and port for the challenge:

python3 solve.py HOST PORT

The important protocol detail is to process each line before reading the next one. The server waits for an answer after every challenge; it is not sending all three challenges at once. The local server returns:

VOIDB0X{fl4ppy_m4st3r_l3v3l_5_4ch13v3d}

Solve 2: set the score

The game increments the score when a pipe passes the bird, then checks whether it equals 31337. The network routine checks the same value again. That means the useful value to write is 31336: let the next pipe increment it, and both checks see 31337.

With a memory editor, search for the current score as a 32-bit integer, pass a pipe, and search again for the new score. Repeat until only the score variable remains. Set it to 31336, return to the game, and pass one more pipe. Searching for 31337 directly and setting it there is easy to get wrong because the game increments before it checks; that next pipe would take it past the target.

GDB can do the same memory edit, but the network routine checks TracerPid in /proc/self/status and exits if it sees a debugger. Edit the score while attached, detach, then pass the pipe. Once detached, the process sees TracerPid: 0 when it reaches the network routine.

A patch is another option. The caller checks the score after incrementing, and the network function checks it again, so changing only one comparison can still leave the client returning before it connects. Patching both comparisons removes the game condition altogether; patching the caller to pass 31337 into the network function is enough if the only goal is to invoke the normal network path. The client still needs to reach the server and answer its three challenges.

The score check only decides when the client connects. The server still requires all three responses before it sends the flag.

Download solve.py