Wednesday 9 November 2016

Flare-on Challenge 2016 Write-up


The Flare-on challenge is an annual CTF style challenge with a focus on reverse engineering. Official solutions have already been published, besides that there are other writeups available too, hence I will just skim through the parts.

Challenge #1

The first was simple. This is base64 encoding with a custom charset. This online tool does the job.
Flag: sh00ting_phish_in_a_barrel@flare-on.com

Fig 1: Challenge 1

Challenge #2 - DudeLocker

This is a file encrypting ransomware. An encrypted file (BusinessPapers.doc) is provided, the task is to decrypt it. As the encryption key is hardcoded in the binary, I simply changed the CryptEncrypt call to CryptDecrypt by modifying the IAT. This decrypts the file giving the following image.
flag: cl0se_t3h_f1le_0n_th1s_One@flare-on.com 

Fig 2: Challenge 2

Challenge #3 - Unknown

The challenge is named unknown, we need to find it proper name. This can be found from the embedded pdb file path debug information. The binary implements a custom md5 hash algorithm which is used to calculate a table from the command line argument and the executable path. Since we already know the proper path, the command line argument can simply be brute forced giving us the flag Ohs0pec1alpwd@flare-on.com 
sss = map(ord, list('__FLARE On!'))

target = [0xEE613E2F, 0xDE79EB45, 0xAF1B2F3D, 0x8747BBD7, 
0x739AC49C, 0xC9A4F5AE, 0x4632C5C1, 0xA0029B24, 0xD6165059, 
0xA6B79451, 0xE79D23BA, 0x8AAE92CE, 0x85991A18, 0xFEE05899, 
0x430C7994, 0x1AB9F36F, 0x70C42481, 0x05BD27CF, 0xC4FF6E6F, 
0x5A77847C, 0xDD9277B3, 0x25843CFF, 0x5FDCA944, 0x8EE42896, 
0x2AE961C7, 0xA77731DA]

def charsprod(li):
 prod = 0
 for i in xrange(len(li)):
  prod = ((prod*37)&0xFFFFFFFF) + li[i] 
 return prod

email = ''

for i in xrange(26):
 sss[1] = ord('`') + i
 for j in xrange(1, 256):
  sss[0] = j
  if charsprod(sss) == target[i]:
   email += chr(j)
   break

print email


Challenge #4 - flareon2016challenge

A dll is provided which exports 51 functions by ordinals. Among them functions 1 to 48 & 51 changes the global state in someway or the other and must be called first. Ordinal 50 makes call to Beep and also tries to decrypt some piece of data. The task is to calls the functions in proper order so that the decryption may succeed. Additionally these  functions return an integer byte value indicating the ordinal of the next function that must be called. We can use the return values to build up the call chain order using the following script.
import ctypes

dll = ctypes.windll.LoadLibrary('flareon2016challenge')
call_chain = {}

for i in range(1, 49):
 retval = dll[i]()
 call_chain[i] = retval

print sorted(call_chain.values())
# missing is ordinal 30, should be called first

The call chain table thus found has no entry for ordinal 30, hence that is the function to be called first. After calling the functions in the correct order, an embedded executable is decrypted which just makes a series of calls to the Beep function. Setting a logging breakpoint on Beep allows us to recover the parameters passed. Calling export 50 using the same parameters gives us the flag: f0ll0w_t3h_3xp0rts@flare-on.com 
import sys
import ctypes
import os

params =[(440, 500), (440, 500), (440, 500), (349, 350) , (523, 150), (440, 500), (349, 350), (523, 150), (440, 1000), (659, 500), 
(659, 500), (659, 500), (698, 350), (523, 150), (415, 500), (349, 350), (523, 150), (440 , 1000)]

dll = ctypes.cdll.LoadLibrary('flareon2016challenge')

# call first function
retval = dll[30]()

# do not call last function
while retval != 51:
 retval = dll[retval]()

# call last func
dll[51]()

for p in params:
 dll[50](p[0], p[1])

Challenge #5 - smokestack

The provided executable is a stack based virtual machine. It takes in an argument, and prints the flag if it is correct. I reimplemented the vm in python and brute forced the flag A_p0p_pu$H_&_a_Jmp@flare-on.com
instructions = [0, 33, 2, 0, 145, 8, 0, 22, 0, 12, 9, 10, 11, 0, 0,
12, 2, 12, 0, 0, 29, 10, 11, 0, 0, 99, 2, 12, 0, 0,
24, 6, 0, 84, 8, 0, 51, 0, 41, 9, 10, 11, 0, 0, 44,
2, 12, 0, 0, 61, 10, 0, 14, 1, 11, 0, 0, 89, 2, 12,
0, 11, 0, 0, 0, 12, 1, 0, 9, 12, 0, 11, 1, 0, 2, 2,
12, 1, 11, 0, 0, 1, 3, 12, 0, 11, 0, 0, 0, 8, 0, 71,
0, 96, 9, 10, 12, 0, 11, 1, 3, 0, 93, 8, 0, 124, 0,
110, 9, 10, 11, 0, 0, 7, 3, 12, 0, 0, 91, 12, 1, 0,
135, 10, 0, 54, 12, 1, 11, 0, 11, 1, 2, 12, 1, 11, 1,
0, 88, 2, 6, 0, 249, 8, 0, 160, 0, 150, 9, 10, 11, 0,
0, 77, 6, 12, 0, 0, 174, 10, 0, 803, 0, 299, 3, 12,
1, 11, 0, 11, 1, 2, 12, 1, 12, 1, 11, 1, 11, 1, 0, 1,
3, 12, 1, 0, 3, 2, 11, 1, 0, 0, 8, 0, 178, 0, 199, 9,
10, 7, 0, 65143, 8, 0, 216, 0, 209, 9, 10, 11, 0, 0,
88, 2, 12, 0, 0, 3, 4, 0, 140, 2, 0, 24724, 8, 0, 238,
0, 231, 9, 10, 11, 0, 0, 231, 2, 12, 0, 11, 1, 2, 0,
12, 6, 0, 116, 8, 0, 263, 0, 253, 9, 10, 11, 0, 0, 9,
3, 12, 0, 0, 285, 10, 0, 10, 12, 1, 11, 1, 0, 1, 3,
12, 1, 11, 1, 0, 0, 8, 0, 267, 0, 285, 9, 10, 0, 6,
5, 0, 7616, 8, 0, 307, 0, 297, 9, 10, 11, 0, 0, 113,
2, 12, 0, 0, 317, 10, 11, 0, 0, 119, 2, 12, 0, 0, 317,
10, 0, 22, 2, 0, 14, 3, 0, 97, 8, 0, 339, 0, 332, 9,
10, 11, 0, 0, 44, 3, 12, 0, 12, 1, 11, 1, 0, 8492, 11,
1, 0, 1, 3, 12, 1, 0, 7, 3, 11, 1, 0, 0, 8, 0, 345,
0, 366, 9, 10, 0, 458, 6, 0, 8181, 8, 0, 385, 0, 378,
9, 10, 11, 0, 0, 18, 2, 12, 0, 13]

stack = []
sp = ctx1 = ctx2 = 0

def push(value):
 global sp
 sp += 1
 stack[sp] = value & 0xFFFF

def pop():
 global sp
 sp -= 1
 return stack[sp + 1]

def init_vm():
 global stack, sp, ctx1, ctx2
 stack = [ord(ch) for ch in 'kYwxCbJoLp']
 stack += [0,0,0,0]
 sp = 9
 ctx1 = ctx2 = 0

def exec_vm():
 global ctx1, ctx2
 ip = 0

 while ip < 386:
  #print 'loc_%d' %ip
  opcode = instructions[ip]
  
  if opcode == 0: # ins_load
   operand = instructions[ip + 1]
   push(operand)
   ip += 2

  elif opcode == 1: # ins_dec_sp
   pop()
   ip += 1

  elif opcode == 2: # ins_add
   v0 = pop()
   v1 = pop()
   push(v0 + v1)
   ip += 1

  elif opcode == 3: # ins_sub
   v0 = pop()
   v1 = pop()
   push(v1 - v0)
   ip += 1  

  elif opcode == 4: # ins_rotr
   v0 = pop()
   v1 = pop();
   push((v1 << (16 - v0)) | (v1 >> v0))
   ip += 1

  elif opcode == 5: # ins_rotl
   v0 = pop()
   v1 = pop();
   push((v1 >> (16 - v0)) | (v1 << v0))
   ip += 1

  elif opcode == 6: # ins_xor
   v0 = pop()
   v1 = pop();
   push(v1 ^ v0)
   ip += 1

  elif opcode == 7: # ins_not
   v0 = pop()
   push(~v0)
   ip += 1  

  elif opcode == 8: # ins_cmp
   v0 = pop()
   if v0 == pop():
    push(1)
   else:
    push(0)
   ip += 1    

  elif opcode == 9: # ins_cload
   v1 = pop()
   v0 = pop()
   if 1 == pop():
    push(v1)
   else:
    push(v0)
   ip += 1 

  elif opcode == 10: # ins_jmp
   ip = pop()

  elif opcode == 11: # ins_load_ctx
   operand = instructions[ip + 1]
   if operand == 0:
    push(ctx1)
   elif operand == 1:
    push(ctx2)
   ip += 2

  elif opcode == 12: # ins_set_ctx
   operand = instructions[ip + 1]
   v1 = pop()
   if operand == 0:
    ctx1 = v1
   elif operand == 1:
    ctx2 = v1
   ip += 2 

  elif opcode == 13: # ins_inc_ip
   ip += 1    


def main():
 global stack
 start = ord('A') - 1
 end = ord('z') + 1

 for pos in range(10):
  ret_vals = [None for i in xrange(start, end)]
  for i in range(start, end):
   init_vm()
   stack[pos] = i
   exec_vm()
   ret_vals[i-start] = tuple([i, ctx1])

  for e in ret_vals:
   if e[1] != ret_vals[0][1]:
    print chr(e[0]),
    break 


if __name__ == '__main__':
 main()

Challenge #6 - khaki

The challenge presents a piece of obfuscated python bytecode and by far this is best challenge. The file provided is a py2exe'd executable which can be easily unpacked to get the embedded pyc file. This pyc is obfuscated and cannot be easily decompiled. The reason for this is it has been sprinkled with NOPs , two POP_TOP, two ROT_TWO, and three ROT_THREE instructions. I developed a peephole optimizer to remove these instructions and make the file decompile-able using the bytecode-graph library developed by fireeye.
import bytecode_graph
import marshal
import opcode

def remove_nops(bcg, nodes):
 for i in xrange(len(nodes) - 1):
  node = nodes[i]
  if node.opcode == opcode.opmap['NOP']:
   bcg.delete_node(node)
   return True
 return False


def peephole_load_const(bcg, nodes):
 for i in xrange(len(nodes) - 1):
  node = nodes[i]
  # Peephole optimization (remove sequence of load and pop instructions)  
  if node.opcode == opcode.opmap['LOAD_CONST'] and nodes[i+1].opcode == opcode.opmap['POP_TOP']:
   bcg.delete_node(node)
   bcg.delete_node(nodes[i+1])
   return True
 return False   

def peephole_rot_two(bcg, nodes):
 for i in xrange(len(nodes) - 1):
  node = nodes[i]

  # Peephole optimization (remove two consecutive ROT_TWO)  
  if node.opcode == opcode.opmap['ROT_TWO'] and nodes[i+1].opcode == opcode.opmap['ROT_TWO']:
   bcg.delete_node(node)
   bcg.delete_node(nodes[i+1])
   return True
 return False

def peephole_rot_three(bcg, nodes):
 for i in xrange(len(nodes) - 2):
  node = nodes[i]

  # Peephole optimization (remove two consecutive ROT_THREE)  
  if node.opcode == opcode.opmap['ROT_THREE'] and nodes[i+1].opcode == opcode.opmap['ROT_THREE'] and nodes[i+2].opcode == opcode.opmap['ROT_THREE']:
   bcg.delete_node(node)
   bcg.delete_node(nodes[i+1]) 
   bcg.delete_node(nodes[i+2]) 
   return True

 return False  


def main():
 pyc_file = open('poc.pyc', 'rb').read()
 pyc = marshal.loads(pyc_file[8:])
 bcg = bytecode_graph.BytecodeGraph(pyc)

 nodes = [x for x in bcg.nodes()]

 while remove_nops(bcg, nodes) == True:
  nodes = [x for x in bcg.nodes()]

 while peephole_load_const(bcg, nodes) == True:
  nodes = [x for x in bcg.nodes()]
 
 while peephole_rot_two(bcg, nodes) == True:
  nodes = [x for x in bcg.nodes()]


 while peephole_rot_three(bcg, nodes) == True:
  nodes = [x for x in bcg.nodes()]

 deobf_code = bcg.get_code()
 f = open('poc-deobf.pyc', 'wb')
 f.write('\x03\xf3\x0d\x0a\0\0\0\0')
 marshal.dump(deobf_code, f)
 f.close()


if __name__ == '__main__':
 main()

Using this we can obtain the following deobfuscated code.
# Embedded file name: poc.py
import sys, random
__version__ = 'Flare-On ultra python obfuscater 2000'
target = random.randint(1, 101)
count = 1
error_input = ''
while True:
    print '(Guesses: %d) Pick a number between 1 and 100:' % count,
    input_num = sys.stdin.readline()
    try:
        input_num = int(input_num, 0)
    except:
        error_input = input_num
        print 'Invalid input: %s' % error_input
        continue

    if target == input_num:
        break
    if input_num < target:
        print 'Too low, try again'
    else:
        print 'Too high, try again'
    count += 1

if target == input_num:
    win_msg = 'Wahoo, you guessed it with %d guesses\n' % count
    sys.stdout.write(win_msg)
if count == 1:
    print 'Status: super guesser %d' % count
    #sys.exit(1)
if count > 25:
    print 'Status: took too long %d' % count
    sys.exit(1)
else:
    print 'Status: %d guesses' % count

if error_input != '':
    tmp = ''.join((chr(ord(x) ^ 66) for x in error_input)).encode('hex')
    if tmp != '312a232f272e27313162322e372548':
        sys.exit(0)
    stuffs = [67,139,119,165,232,86,207,61,
    import hashlib
    stuffer = hashlib.md5(win_msg + tmp).digest()
    for x in range(len(stuffs)):
        print chr(stuffs[x] ^ ord(stuffer[x % len(stuffer)])),

    print

Another python script to brute force the flag.
import hashlib

for i in xrange(100):
 win_msg = 'Wahoo, you guessed it with %d guesses\n' %i
 tmp = '312a232f272e27313162322e372548'

 stuffs = [67,139,119,165,232,86,207,61,79,67,45,58,230,190,181,74,65,148,71,243,246,67,142,60,61,92,58,115,240,226,171]
 stuffer = hashlib.md5(win_msg + tmp).digest()

 s = ''
 for x in range(len(stuffs)):
  s += chr(stuffs[x] ^ ord(stuffer[x % len(stuffer)]))
 if s.endswith('.com'):
  print s
  break
Flag 1mp0rt3d_pygu3ss3r@flare-on.com

Challenge #7 - hashes

The challenge is a x86 ELF (linux binary) developed in the go language. However unlike the standard go compiler gc, this has been compiled with gccgo and requires libgo.so.7 in order to be able to run. Now my local linux vm is ubuntu 14.04 and libgo7 is only available for ubuntu 16.04 and above. However I was not willing to download and install a complete new distro just for running this single binary. Hence a workaround was necessary. I powered on cloud9 vm, wgetted the deb directly bypassing the package manager. Although dpkg could not install the package, I got the much needed file libgo.so.7. Using it I could debug the binary in my local ubuntu 14.04 vm.

Fig 7 - Satisfying the dependencies
With that out of the picture, the objective of the challenge is to crack the SHA1 hash of the flag applied three times recursively. Since we know, that the flag ends in @flare-on.com, all that is required is to bruteforce the first few characters. Taking the good boy message "You have hashed the hashes" as a cue, I quickly brute forced the flag h4sh3d_th3_h4sh3s@flare-on.com

Challenge #8 - chimera

The name of the challenge immediately reminded me of the movie Mission: Impossible II wherein IMF agent Ethan Hunt must track and destroy a biological weapon Chimera along with its anti-dote Bellerophon and prevent it from being misused. While the actual challenge had nothing to do with the movie but certainly it was equally engrossing. Instead of the chimera virus, here we have an PE executable with a the relevant code hidden up the sleeves in the DOS stub. Once this is figured out, all that is left to disassemble the obfuscated 16 bit code to understand its workings. Dosbox along with its debugger proved much helpful in solving this problem. To get the flag I used the following script.
table = [255, 21, 116, 32, 64, 0, 137, 236, 93, 195, 66, 70,
192, 99, 134, 42, 171, 8, 191, 140, 76, 37, 25, 49,
146, 176, 173, 20, 162, 182, 103, 221, 57, 216, 95,
63, 123, 92, 194, 178, 246, 46, 117, 155, 97, 148, 207,
206, 106, 152, 80, 242, 91, 240, 69, 48, 14, 56, 235,
59, 108, 102, 127, 36, 61, 223, 136, 151, 185, 179,
241, 203, 131, 153, 26, 13, 239, 177, 3, 85, 158, 154,
122, 16, 224, 54, 232, 211, 228, 50, 193, 120, 7, 183,
107, 199, 112, 201, 44, 160, 145, 53, 109, 254, 115,
94, 244, 164, 217, 219, 67, 105, 245, 141, 238, 68,
125, 72, 181, 220, 75, 2, 161, 227, 210, 166, 33, 62,
47, 163, 215, 187, 132, 90, 251, 143, 18, 28, 65, 40,
197, 118, 89, 156, 247, 51, 6, 39, 10, 11, 175, 113,
22, 74, 233, 159, 79, 111, 226, 15, 190, 43, 231, 86,
213, 83, 121, 45, 100, 23, 149, 167, 189, 124, 29, 88,
147, 165, 101, 248, 24, 19, 234, 188, 229, 243, 55,
4, 150, 168, 30, 1, 41, 130, 81, 60, 104, 31, 142, 218,
138, 5, 34, 114, 73, 250, 135, 169, 84, 98, 198, 170,
9, 180, 253, 214, 209, 172, 133, 17, 71, 58, 157, 230,
77, 27, 204, 82, 128, 35, 252, 237, 139, 126, 96, 205,
110, 87, 186, 222, 174, 202, 196, 119, 12, 78, 212,
208, 200, 225, 184, 249, 38, 144, 129, 52]

target = [56, 225, 74, 27, 12, 26, 70, 70, 10, 150, 41, 115, 115, 164, 105, 3, 0, 27, 168, 248, 184, 36, 22, 214, 9, 203]

flag = map(ord, list('A'*26))

def rol(n):
 b = (n >> 7) & 1
 n = ((n << 1) | b) & 0xFF
 return n

def ror(n):
 b = n & 1
 n = ((n >> 1) | (b << 7)) & 0xFF
 return n 


def calc():
 for i in xrange(len(flag)-1, -1, -1):
  if i == len(flag) - 1:
   v1 = rol(rol(rol(0x97)))
  else:
   v1 = rol(rol(rol(flag[i+1])))

  v2 = table[v1]
  v2 = table[v2]

  flag[i] ^= v2  

 for i in xrange(len(flag)):
  if i == 0:
   flag[i] ^= 0xC5
  else:
   flag[i] ^= flag[i-1]

 print map(hex, flag)


def reverse():
 for i in xrange(len(target)-1, -1, -1):
  if i == 0:
   target[i] ^= 0xC5
  else:
   target[i] ^= target[i-1]

 for i in xrange(len(target)-1, -1, -1):
  if i == len(target) - 1:
   v1 = rol(rol(rol(0x97)))
  else:
   v1 = rol(rol(rol(save)))

  v2 = table[v1]
  v2 = table[v2]
  save = target[i]
  target[i] ^= v2 

 print ''.join(map(chr, target))


if __name__ == '__main__':
 reverse()
flag retr0_hack1ng@flare-on.com 


Challenge #9 - GUI

The challenge consists of a .net executable with ConfuserEx thrown in for a change. DnSpy along with NoFuserEx is sufficient to extract all the necessary strings for reconstructing back the shared secret.

Share:1-d8effa9e8e19f7a2f17a3b55640b55295b1a327a5d8aebc832eae1a905c48b64
Share:2-f81ae6f5710cb1340f90cd80d9c33107a1469615bf299e6057dea7f4337f67a3
Share:3-523cb5c21996113beae6550ea06f5a71983efcac186e36b23c030c86363ad294
Share:4-04b58fbd216f71a31c9ff79b22f258831e3e12512c2ae7d8287c8fe64aed54cd
Share:5-5888733744329f95467930d20d701781f26b4c3605fe74eefa6ca152b450a5d3
Share:6-a003fcf2955ced997c8741a6473d7e3f3540a8235b5bac16d3913a3892215f0a

Flag Shamir_1s_C0nfused@flare-on.com

Challenge #10 - flava

This was the final challenge, and is composed of many sub challenges. The first part requires to get through three layers of obfuscated javascript with an obfuscated Diffie Hellman (courtesy of Angler EK) for more distress. So unless, one figures out what the heck is with all the obfuscated javascript it is a dead end. Even if one manages to guess that, breaking the Diffie Hellman is more pain. Luckily, Kaspersky researches have already done the hard work before and it requires a bit of Googling to locate the code necessary to break the diffie hellman.
After three layers of javascript there are three more layers of actionscript. While the first layer is straightforward the second and third layers are obfuscated. The challenge in this part is to identify that the RC4 key is reused.  Once we know that, we can simply xor the plain text and ciphertext to get the keystream, and xor the resultant keystream with the second ciphertext to get back the plain text. The third actionscript layer simply prints the flag angl3rcan7ev3nprim3@flare-on.com

Final Words

Overall, the challenges this year were certainly more difficult than those of the preceding year. Some parts required bruteforcing hashes and guesswork which I detest. Another point of notice is that there were no 64 bit binaries. There were also no challenges involving kernel drivers. Finally, I would like to extend my thanks to everyone who helped me through the course of the challenges.

4 comments:

  1. Great write-up, as always! :) And thanks for all the hints.

    ReplyDelete
    Replies
    1. Thanks for the compliment! Your work in this field has always been a source of inspiration.

      Delete
  2. good Write-up,thanks for your generosity and sharing.Learn a lot.

    Best regards
    Sound

    ReplyDelete