LabyREnth CTF WriteUp - Random track

Datetime:2016-08-22 22:26:46          Topic: Python  PHP           Share

Attempting the Labyrenth challenges was an interesting experience. I completed three tracks - Windows, Docs & Random, and the others were left halfway. Among all the tracks, the random track was more interesting particularly due to the last python challenge.

Level 1 - Java

The challenge consists of a jar file. So it seems, we have to reverse java bytecode. The interesting thing is the jar will only run with Java 9 ( currently in beta ) as it uses StringConcatFactory . Attempting to decompile the file with Jad, Procyon, or CFR fails. This is due the fact it uses a new opcode InvokeDynamic . This is a fairly new opcode and as of now, the java compiler does not emit this opcode. It exists to support other dynamic languages running on the JVM. 

Only krakatau was able to decompile the file proper, although it too doesn't handle the InvokeDynamic opcode.

public class omg {
    String username;
    String levelFlag;
    
    public omg(String s)
    {
        super();
        this.levelFlag = "W5SSA2DPOBSSA6LPOUQGK3TKN54SA5DINFZSASTBOZQSAYLQOAQGC4ZAO5QXE3JNOVYC4ICUNBSSA4TFON2CA53JNRWCAYTFEBWXKY3IEBWW64TFEBZWK6DDNF2GS3THEEQFI2DFEBTGYYLHEBUXGICQIFHHWRBQL5MTA5K7IV3DG3S7IJQXGZJTGJ6Q!";
        this.username = s;
        if (s.contains((CharSequence)(Object)"-isDrunk"))
        {
            String[] a = s.split("-");
            int i = a[1].charAt(2);
            int i0 = Character.toUpperCase((char)i);
            int i1 = (char)(i0 ^ 15);
            this.levelFlag = /*invokedynamic*/null;
            StringBuilder a0 = new StringBuilder(this.levelFlag);
            int i2 = 0;
            while(i2 < 4)
            {
                Object[] a1 = new Object[1];
                int i3 = a[1].charAt(a[1].length() - 1);
                int i4 = (short)i3;
                a1[0] = Short.valueOf((short)i4);
                int i5 = (char)(Integer.parseInt(String.format("%04x", a1), 16) ^ 166);
                a0.append((char)i5);
                i2 = i2 + 1;
            }
            System.out.println(a0.toString());
            this.levelFlag = a0.toString();
        }
        else
        {
            int i6 = 0;
            while(i6 < s.length() / 2)
            {
                String s0 = this.levelFlag;
                int i7 = s.charAt(i6);
                int i8 = s.charAt(s.length() - 1);
                this.levelFlag = s0.replace((char)i7, (char)i8);
                i6 = i6 + 1;
            }
        }
    }
    
    public String getLevelFlag()
    {
        return this.levelFlag;
    }
    
    public static void main(String[] a)
    {
        omg a0 = new omg(System.getenv("Admin"));
        System.out.println(/*invokedynamic*/null);
    }
}

There is a whole bunch of decoy code. The actual flag  can be found by base32 decoding the levelFlag: 

W5SSA2DPOBSSA6LPOUQGK3TKN54SA5DINFZSASTBOZQSAYLQOAQGC4ZAO5QXE3JNOVYC4ICUNBSSA4TFON2CA53JNRWCAYTFEBWXKY3IEBWW64TFEBZWK6DDNF2GS3THEEQFI2DFEBTGYYLHEBUXGICQIFHHWRBQL5MTA5K7IV3DG3S7IJQXGZJTGJ6Q

Fig 1: Level 1 flag

This gives our flag:  PAN{D0_Y0u_Ev3n_Base32}


Level 2 - Regex

This challenge looks scary at first sight. A regular expression is given. Our task is to find a string that does not match the regex. Netcatting the string to 52.27.101.106 would give the flag.

Fig 2: That looks scary, doesn't it?

The regex seems to match everything ever thrown at it. It has 625 clauses making it unfit for manual analysis. A regex debugger like RegexBuddy or regex101 is handy for such situations.

Lets look at the clause 1:  .*[^0mglo8sc1enC3].*

This matches a single character unlimited number of times followed by a character which is not in the negation list, finally followed by a character unlimited number of times. Hence any string consisting of only characters present in the negation list will not match this clause.

Clause 2:  .{,190}

Clause 3:  .{192,}

The 2nd clause matches any string of length 190 or lower. The 3rd clause matches any string of length 192 or above. Combining these two clauses, for a non match our string should exactly 191 characters drawn from the list in clause 1.

The remaining clauses worth of interest is clause 124 and 341.

Fig 3: Clause 124
Fig 4: Clause 341

Clause 124 matches a string of length 190 chars followed by one of  e0nlCo3c8. Similarly clause 341 matches a string of length 190 followed by one of  mg1 .

Combining the above clauses the string which does not match the regex consists of 190 g followed by a solitary s.

ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggs

Netcatting this to  52.27.101.106  gives the flag PAN{th4t5_4_pr311y_dum8_w4y_10_us3_r3g3x}

Fig 5: Level 2 flag

Level 3 - Pcap

A pcap file is provided. Loading it in Wireshark reveals something interesting with the tcp sequence number of the SYN packets. The sequence numbers starts with PK\03\04 which is also the magic signature for zip files. We can set a display filter will only display the SYN packets. The same can be used to save the filtered packets to a new pcap file.

Fig 6: ZIP signature in tcp sequence number

To assemble zip file by combining the sequence number, we can use scapy.

from scapy.all import *
from binascii import unhexlify
import cStringIO

def main():
    buf = cStringIO.StringIO()
    with PcapReader('SYN-filtered.pcap') as pcap_reader:
        for pkt in pcap_reader:                                 
                buf.write(unhexlify(format(pkt.seq, 'x').zfill(8)))
    open('extracted.zip', 'wb').write(buf.getvalue())   
            
if __name__ == '__main__':
    main()

This creates a new file extracted.zip . This zip contains 853 files each containing base64 encoded text.

Fig 7: Contents of zip file

Our task is to join the files in the correct order to form a huge base64 text blob. Decoding that text blob should give us our flag. The padding character used in base64 is =. Grepping for the = character within the extracted BIN files gives one hit 339.bin . This should therefore be the last file in the combined blob.

Fig 8: 339.bin

The other point to note is that the contents of the files overlap as in Fig 9 showing the overlapped part of 531.bin with 339.bin .

Fig 9: Overlapped part

We can write a python script which would find the order of assembling the files by searching for the overlapped part.

import re

def findMatch(dct, txt):
    for i in xrange(len(txt)-1, 0, -1):
        reg = re.compile(re.escape(txt[0:i])+'$')

        for key in dct.keys():
            if reg.search(dct[key]):
                return (key, i)

    return (None, -1)


def main():
    contents = dict()

    for i in xrange(853):
        fname = '%d.txt' %i
        txt = open(fname).read()
        contents[fname] = txt

    nm = '339.txt'
    print nm

    while True:
        txt = contents[nm]
        result, matchlen = findMatch(contents, txt)

        if result == None:
            break
            
        print result, matchlen
        del contents[nm] 
        nm = result


if __name__ == '__main__':
    main()

Running this gives the order of joining the files in reverse i.e the first line ( 339.txt ) is the last file and the line ( 659.txt ) is the first file. Each line contains two comma separated values. The second value is the number of overlapping characters. The full list is available as a gist .

Based on the join order, another python script can assemble the files.

f = open('combined.txt', 'w')
lines = open('random-lev3-joinorder.txt', 'r').readlines()

try:
    for line in reversed(lines):
        fname, matchlen = line.split(' ')
        matchlen = int (matchlen)

        txt = open(fname, 'r').read()
        f.write(txt[0:len(txt)-matchlen])

except:
    f.write(open('339.txt', 'r').read()) # last file has no overlap

f.close()

The assembled file, combined.txt is a base64 text blob. Decoding it results in a zip file. The zip file has several images within.

One of them  troll_cloud.png contains the flag:   PAN{YouDiD4iT.GREATjob}

Fig 10: Level 3 flag

Level 4 - PHP

The challenge consists of solving a maze. The maze is coded in PHP and is additionally heavily obfuscated. For running this, I had to set up wamp server in my windows virtual machine. The actual php code which we are after is dynamically generated and eval 'd. Getting around this obfuscation was easy as PHP was configured with display errors to be true. This made it to spit out the eval'd php code as a warning message.

Fig 11: Dynamically generated php code

To copy the de-obfuscated php code, we need to use a tool like fiddler. We cannot directly copy the php code from the warning message as the characters are not escaped properly. The deobfuscated source can be found here . From this point it is a matter of manual source code analysis to find the correct path for solving the maze. Solving the maze gives the flag:

PAN{Life is a maze of complications. Also, puppets are sometimes involved. Deal with it.}

Fig 12: Level  4 flag

Level 5 - Python

Fig 13: Level 5 challenge - Crack APT Maker Pro

This the final challenge in the Random track and is the most interesting. As the description of the challenge points out we really  "have to be a snake charmer to crack the newest version of APT Maker Pro" . The file is a 720 KB python script which contains a zlib compressed marshalled code object which is dynamically executed by the exec statement as shown in Fig 14.

Fig 14: Dynamically executed payload

To proceed, we need the decompiled payload from the compressed code object. This is easy and my tool Easy Python Decompiler is sufficient for the task. First, decompress the zlib data into a new file, add the python magic header 03 F3 0D 0A 00 00 00 00 at the beginning, and finally feed it to the decompiler to get our decompiled source as shown in Fig 15.

Fig 15: Decompiled payload

However that is not all. The decompiled payload contains a base64 encoded, zlib compressed code object which is executed by the exec statement just like the previous case. In addition to this there is a piece of RC4 encrypted data labelled as malware as shown in Fig 16. Thus the actual challenge is to find the correct RC4 decryption key. To find the key, we need to inspect the second level payload just found.

Fig 16: The malware blob

The difficulty at this stage increases rapidly. We cannot decompile the second level payload as it has been obfuscated. A deep knowledge about CPython internals is necessary to proceed forward. As I said, the payload is no longer decompilable, hence we need to inspect it manually. Let's inspect the code object.

>>> import marshal
>>> f = open('level2.pyc', 'rb')
>>> f.seek(8)
>>> co = marshal.load(f)
>>> co.co_consts
(<code object verify_license at 00AAB2F0, file "", line -1>, None)

The code object contains another nested code object verify_license which sounds interesting. Lets dump it to a new file.

>>> of = open('level3.pyc', 'wb')
>>> of.write('\x03\xF3\x0D\x0A' + '\x00' * 4)
>>> marshal.dump(co.co_consts[0], of)
>>> of.close()

Now we need to analyze 3rd level payload level3.pyc . Similar to level2.pyc this is too obfuscated and undecompileable. Lets run some preliminary analysis.

>>> f = open('level3.pyc', 'rb')
>>> f.seek(8)
>>> co=marshal.load(f)
>>> len(co.co_consts)
37173
>>> len(co.co_names)
37124
>>> len(co.co_code)
144060

That's more than 37k constants and names!. In addition the size of bytecode instructions that gets actually executed is over 144k. Thats insane!. Who would like to analyze such a file manually unless one have tools and luckily we do have tools. Out of the 37173 constants, 37121 just store the None type and is redundant. We can write a quick python script for finding this.

>>> for i in xrange(len(co.co_consts)):
...     if co.co_consts[i] is not None:
...             print i
...             break
...
37121

The 37122th constant stores a png file which looks interesting.

>>> co.co_consts[37122][:6]
'\x89PNG\r\n'
Fig 17: Embedded PNG file

The remaining constants store various integers and are not of much interest. Lets shift our focus to the 144k long co_code . Lets disassemble the very first instruction,

>>> import opcode
>>> opcode.opname[ord(co.co_code[0])]
'EXTENDED_ARG'

That's the EXTENDED_ARG opcode. In normal python, it is very rare to encounter this opcode. This is only generated if the operand of the instruction cannot fit in a space of 2 bytes. This can happen in rare situations such as passing more than 65,535 parameters to a function. The actual opcode on which EXTENDED_ARG is operating on is located at a offset of +3. Lets see what it is.

>>> opcode.opname[ord(co.co_code[3])]
'EXTENDED_ARG'

That's even more strange. We expected to see a real opcode here. If we continue this way, we will see a huge chain of EXTENDED_ARG opcodes and the final instruction which it is operating on is a JUMP_FORWARD which as the name suggests increments the IP by an offset.

>>> opcode.opname[ord(co.co_code[144051])]
'EXTENDED_ARG'
>>> opcode.opname[ord(co.co_code[144054])]
'EXTENDED_ARG'
>>> opcode.opname[ord(co.co_code[144057])]
'JUMP_FORWARD'

To find out the target offset of the jump we need to write a python script.

import marshal

f=open('level3.pyc', 'rb')
f.seek(8)
co=marshal.load(f)
f.close()

i = 0
arg = 0
while i < len(co.co_code):
 arg = (arg << 16) | ord(co.co_code[i+1]) | (ord(co.co_code[i+2]) << 8)
 arg = arg & 0xFFFFFFFF  
 i += 3

print hex(arg)

Running this gives us the target offset which is  0xfffdcd45 or -1,44,059. That is instead of jumping forward it jumps backward within the instruction stream. The obfuscation that is applied is akin to overlapping instruction obfuscation found in native x86 executables.

Now the size of the instruction stream ( co_code ) is 144060 and a 144059 long backward jump from the rear leads to the second byte. If we disassemble this we uncover a hidden series of instructions stitched together with JUMP_FORWARD s.

>>> opcode.opname[ord(co.co_code[1])]
'LOAD_NAME'
>>> opcode.opname[ord(co.co_code[4])]
'NOP'
>>> opcode.opname[ord(co.co_code[5])]
'JUMP_FORWARD'

We need to uncover this hidden instructions, join them as one after removing the NOP and JUMP_FORWARD instructions used for stitching them. Another python script to the rescue.

# level3 extract code

import marshal
import opcode
import types

cleaned_bytecode = []


def clean(opkode, arg1, arg2):
    if opcode.opname[opkode] == 'JUMP_FORWARD' or opcode.opname[opkode] == 'NOP':
        return

    else:
        if opkode >= opcode.HAVE_ARGUMENT:
            cleaned_bytecode.append(opkode)
            cleaned_bytecode.append(arg1)
            cleaned_bytecode.append(arg2)
        else:
            cleaned_bytecode.append(opkode)


def printline(offset, opname, arg):
    if opname == 'JUMP_FORWARD' or opname == 'NOP':
        return
    if arg is not None:
        print 'loc_%06d: %s %d' %(offset, opname, arg)
    else:
        print 'loc_%06d: %s' %(offset, opname)


def modifyCodeStr(code_obj):
    co_argcount = code_obj.co_argcount
    co_nlocals = code_obj.co_nlocals
    co_stacksize = code_obj.co_stacksize
    co_flags = code_obj.co_flags

    # new code string
    co_codestring = ''.join(map(chr, cleaned_bytecode))

    # Replace png file contents to facilitate decompiling
    co_constants = list(code_obj.co_consts)
    co_constants[37122] = 'PNG FILE HERE'
    co_constants = tuple(co_constants)

    co_names = code_obj.co_names
    co_varnames = code_obj.co_varnames
    co_filename = code_obj.co_filename
    co_name = code_obj.co_name
    co_firstlineno = code_obj.co_firstlineno
    co_lnotab = code_obj.co_lnotab

    return types.CodeType(co_argcount, co_nlocals, co_stacksize, \
                          co_flags, co_codestring, co_constants, co_names, \
                          co_varnames, co_filename, co_name, co_firstlineno, co_lnotab)

def main(): 
    f=open('level3.pyc', 'rb')
    f.seek(8)
    co=marshal.load(f)
    f.close()
    kode = map(ord, list(co.co_code))
    offset = 1

    while offset < len(kode):
        opkode = kode[offset]
        opname = opcode.opname[opkode]

        if opkode >= opcode.HAVE_ARGUMENT:
            arg1 = kode[offset+1]
            arg2 = kode[offset+2]
            arg = (arg2 << 8) | arg1 # Little endian
            printline(offset, opname, arg)
            offset += 3

        else:
            arg = arg1 = arg2 = None
            printline(offset, opname, arg)
            offset += 1

        clean(opkode, arg1, arg2)

        if opname == 'JUMP_FORWARD':
            offset += arg

        elif opname == 'RETURN_VALUE':
            break

    newCodeObj = modifyCodeStr(co)
    f = open('level3_deobf.pyc', 'wb')
    f.write('\x03\xF3\x0D\x0A' + '\x00'*4)
    marshal.dump(newCodeObj, f)
    f.close()


if __name__ == '__main__':
    main()

In addition to stitching the hidden code, it replaces the PNG file contents with a dummy string to facilitate decompiling, else decompiler would choke. Lets decompile the produced  level3_deobf.pyc selecting pycdc as the engine. It gives the following code.

# File: l (Python 2.7)

   = license_key[0]
    = 'PNG FILE HERE'[542]
exec       =    ==    

   = license_key[1]
    = 'PNG FILE HERE'[379]
exec       =    ==    

   = license_key[2]
    = 'PNG FILE HERE'[1020]
exec       =    ==    

   = license_key[3]
    = 'PNG FILE HERE'[457]
exec       =    ==    

   = license_key[4]
    = 'PNG FILE HERE'[203]
exec       =    ==    

   = license_key[5]
    = 'PNG FILE HERE'[203]
exec       =    ==    

   = license_key[6]
    = 'PNG FILE HERE'[39]
exec       =    ==    

   = license_key[7]
    = 'PNG FILE HERE'[379]
exec       =    ==    

   = license_key[8]
    = 'PNG FILE HERE'[65]
exec       =    ==    

   = license_key[9]
    = 'PNG FILE HERE'[54]
exec       =    ==    

   = license_key[10]
    = 'PNG FILE HERE'[379]
exec       =    ==    

   = license_key[11]
    = 'PNG FILE HERE'[40]
exec       =    ==    

   = license_key[12]
    = 'PNG FILE HERE'[262]
exec       =    ==    

   = license_key[13]
    = 'PNG FILE HERE'[54]
exec       =    ==    

   = license_key[14]
    = 'PNG FILE HERE'[379]
exec       =    ==    

   = license_key[15]
    = 'PNG FILE HERE'[250]
exec       =    ==    

   = license_key[16]
    = 'PNG FILE HERE'[704]
exec       =    ==    

   = license_key[17]
    = 'PNG FILE HERE'[1110]
exec       =    ==    

   = license_key[18]
    = 'PNG FILE HERE'[141]
exec       =    ==    

   = license_key[19]
    = 'PNG FILE HERE'[379]
exec       =    ==    

   = license_key[20]
    = 'PNG FILE HERE'[65]
exec       =    ==    

   = license_key[21]
    = 'PNG FILE HERE'[54]
exec       =    ==    

   = license_key[22]
    = 'PNG FILE HERE'[285]
exec       =    ==    

   = license_key[23]
    = 'PNG FILE HERE'[1215]
exec       =    ==    

   = license_key[24]
    = 'PNG FILE HERE'[840]
exec       =    ==    

  =   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &   &

The variable names are missing, but it is fairly evident what the code does. It compares the characters of the license key with some bytes of the PNG file. For success, each of these checks must succeed. Joining the characters we get the  license key  1_W4nnA_b3_Th3_vERy_b3ST! . Feeding this, APT Maker Pro becomes registered as shown in Fig 18.

Fig 18: APT Maker Pro is licensed

Clicking on Generate APT drops the malware payload as  EVIL_MALWARE_ CYBER_PATHOGEN .pyc. Decompiling it we get the file containing the flag for this level 

PAN{l1Ke_n0_oN3_ev3r_Wa5}

Fig 19: The flag




About List