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.
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: Fig 1: Level 1 flag |
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? |
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.
This creates a new file extracted.zip. This zip contains 853 files each containing base64 encoded text.
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.
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.
We can write a python script which would find the order of assembling the files by searching for the overlapped part.
Based on the join order, another python script can assemble the files.
One of them troll_cloud.png contains the flag: PAN{YouDiD4iT.GREATjob}
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 |
Fig 8: 339.bin |
Fig 9: 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 |
Fig 15: Decompiled payload |
Fig 16: The malware blob |
>>> 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 |
>>> 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_FORWARDs.
>>> 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()
The hidden instruction stream can be found here. The python script above stitches the hidden code and 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 |