nullcon HackIM 2019 Writeup

Feb 3, 2019 12:35 · 850 words · 4 minute read ctf cyber-security write-up machine-learning

Captcha Forest

Problem

A baby captcha just for you.

nc misc.ctf.nullcon.net 6001

Solution

For this challenge, we need to solve 200 captchas that are using the Bill Cipher mappings.

Here is an example captcha:

The first step is to find out which symbol corresponds to which letter in the alphabet. I did a quick google search and found this:

Now with a one-to-one mapping, we can move on to solve the captchas. After some observation, I discover that one, the captcha always consists of four letters; two, the symbols are always the same.

Now with the two observations, I plan to crop each 120x27 captcha into four 30x27 blocks and match each block to the letter it corresponds to.

This method works out great. Here is my final code:

from pwn import *
from PIL import Image

context.log_level = 'error'

keys = {
  'c4a35da158e008809f852ecc813dd139': 'A',
  'a9bcd51c91b97289146158cd60c14e5d': 'B',
  '2e17618143d78f20269f3216166a9a98': 'C',
  '0fd8166e85f94c0e46ffa4ba0f29e72f': 'D',
  'cc658ac67ce22881fe6f6970c32f761b': 'E',
  '921e54169612d85f2d27ce4ebc959cdf': 'F',
  '8d0ed3c3e8dcd62632049b3454d6cf34': 'G',
  'fd486e2b20c06354bdbfc485dcf569d6': 'H',
  'e7555a5a32a9fccc6cf7c0ec72cdddbd': 'I',
  '2fc99e3aed0d0cd4fc03108fb2f9745f': 'J',
  'e5d056c179c5fb8883eb204746b9bc29': 'K',
  '6f6090fe8fe36a83d920c5be3d4a1491': 'L',
  '7bc2f0bd22445eb4546c06d5903ba185': 'M',
  '6a578dc416c036f725d4b8ce70f5b84f': 'N',
  '6c141423aced83a987ef965b126d20c6': 'O',
  'd0b3155daff4c52677ebf055d2e8cec0': 'P',
  '54a962e6fa4383fef7da22e79aadf7f0': 'Q',
  '0a8970d139a6a393889aa616cdcd8b4c': 'R',
  '2162480dcac1c231f11c93ff33bcb47d': 'S',
  'ac0a5813691cdf6aab84fb74a2e11c61': 'T',
  '93e0fd5f54239b8022044ee54f6eb7e6': 'U',
  '64d4a2ebcb7b4f87513ce1c67ad52127': 'V',
  '08f5debc5b9297b38d91d48078674417': 'W',
  '199c795a79eabaf760996065ba4ee884': 'X',
  'e58d3228b4949421f411517b62610cfc': 'Y',
  '44051309af10c3ff106dbe48c67f1e16': 'Z',
}

# https://stackoverflow.com/questions/5953373/how-to-split-image-into-multiple-pieces-in-python
def crop(path, inputFile, height, width, k):
  im = Image.open(inputFile)
  imgwidth, imgheight = im.size
  for i in range(0, imgheight, height):
    for j in range(0, imgwidth, width):
      box = (j, i, j+width, i+height)
      a = im.crop(box)
      try:
        a.save(os.path.join(path,"IMG-%s.png" % k))
      except:
        pass
      k +=1

def getHash(count):
  p = process(['md5', '-q', 'IMG-%s.png' % count])
  h = p.recvall().strip()
  p.close()
  return h

sh = remote('misc.ctf.nullcon.net', 6001)

for count in range(200):
  sh.recvuntil(' ---')
  sh.sendline('')
  image = sh.recvuntil(' ---').strip().split('\n')[0].decode('hex')
  with open('./temp.png', 'wb') as f:
    f.write(image)
  crop('./', './temp.png', 27, 30, 0)

  answer = ''
  for i in range(4):
    h = getHash(i)
    if h not in keys:
      print 'IMG-%s.png not found' % i
    else:
      answer += keys[h]
  print '{}: {}'.format(count, answer)
  sh.sendline(answer)

sh.interactive()

flag: hackim19{Since_you_are_not_a_robot_I_will_give_you_the_flag}

Captcha Forest Harder

Problem

nc miscc.ctf.nullcon.net 6002

Solution

This is similar to the last challenge, but with two differences. One, all symbols are rotated and scaled each time so their md5 hashes would no longer match. Two, this time, we only need to get 120 of the 200 captchas to get the flag.

I experimented with feature matching and image hashes, but none of my solutions are able to fully automate the process. I solved this challenge in the end through a semi-automated process.

I wrote a script that will guess the letter for each symbol and then I will manually verify it before sending it to the server. Using this method, I am able to get the flag.

Here is the semi-automated script:

from pwn import *
from PIL import Image
from PIL import ImageOps
import imagehash
import uuid

context.log_level = 'debug'

keys = {}


def getHash(img):
  return imagehash.average_hash(img, 256)

for i in range(0x41, 0x41+26):
  img = Image.open('./images/%s.png' % chr(i))
  img = ImageOps.expand(img, border = 10, fill = (255, 255, 255))
  for deg in range(-30, 30):
    keys[getHash(img.rotate(deg).crop((10, 10, 40, 37)))] = chr(i)

# https://stackoverflow.com/questions/5953373/how-to-split-image-into-multiple-pieces-in-python
def crop(path, inputFile, height, width, k):
    im = Image.open(inputFile)
    imgwidth, imgheight = im.size
    for i in range(0, imgheight, height):
      for j in range(0, imgwidth, width):
        box = (j, i, j+width, i+height)
        a = im.crop(box)
        try:
          a.save(os.path.join(path,"IMG-%s.png" % k))
        except:
          pass
        k +=1

sh = remote('miscc.ctf.nullcon.net', 6002)

sh.recvuntil(' ---')
sh.sendline('')

for count in range(200):
  data = sh.recvuntil(' ---').strip().split('\n')
  image = data[0].decode('hex')
  with open('./temp.png', 'wb') as f:
    f.write(image)
  crop('./', './temp.png', 27, 30, 0)

  answer = ''
  for i in range(4):
    h = getHash(Image.open('IMG-%s.png' % i))
    minV = -1
    minC = ''
    for kh, hc in keys.iteritems():
      v = abs(h - kh)
      # print v
      if v < minV or minV < 0:
        minV = v
        minC = hc
    
    os.system('imgcat ./IMG-%s.png' % i)
    value = raw_input('is it {}? '.format(minC)).strip()
    if len(value) < 1:
      answer += minC
    else:
      answer += value
    os.system('mkdir -p ./images/{} && cp ./IMG-{}.png ./images/{}/{}'.format(answer[-1], i, answer[-1], uuid.uuid4()))
  
  sh.sendline(answer)
  sh.recvline()
  result = sh.recvline()
  sh.recvline()
  if 'Correct' in result:
    sh.sendline('')
  print '{}: {} - {}'.format(count, answer, result)

sh.interactive()

Here’s how it looks in action:

flag: hackim19{I_guess_I_will_stop_making_captchas_now}

mlAuth

Problem

download

Solution

This is similar to the dog or frog challenge in picoctf 2018 where we need to find the input for a neural network in order to get a certain output.

I took the code I wrote for “dog or frog” and tweaked it a bit. Here is my final solution:

import keras.backend as K
import numpy as np
from keras.models import load_model


model = load_model('./dist/keras_model')
model.summary()

profile = '0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x80x750xfe0xdc0x590x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00xd0x5f0xd40xfd0xfd0xfd0x9d0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x100x5f0xd10xfd0xfd0xfd0xf50x7d0x120x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x280x600xce0xfd0xfe0xfd0xfd0xc60x400x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x2c0xb60xf00xfd0xfd0xfd0xfe0xfd0xc60x180x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00xf0x3c0x3c0xa80xfd0xfd0xfe0xc80x170x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x460xf70xfd0xfd0xf50x150x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x4b0xcf0xfd0xfd0xcf0x5c0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x4f0xdb0xfd0xfd0xfd0x8a0x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x690xfa0xfd0xfd0xfd0x220x10x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x5f0xfe0xfe0xfe0xfe0x5e0x00x00x00x00x00x30xd0xd0xd0x80x00x00x00x00x00x00x00x00x00x00x00x00x6b0xfd0xfd0xfd0xcc0xf0x00x00x00x00x150xa60xfd0xfd0xfd0xd40x190x00x00x00x00x00x00x00x00x00x00x210xd90xfd0xfd0x840x400x00x00x120x2b0x9d0xab0xfd0xfd0xfd0xfd0xfd0xa00x20x00x00x00x00x00x00x00x00x30xa60xfd0xfd0xf20x310x110x310x9e0xd20xfe0xfd0xfd0xfd0xfd0xfd0xfd0xfd0xfd0xb0x00x00x00x00x00x00x00x00xa0xe30xfd0xfd0xcf0xf0xac0xfd0xfd0xfd0xfe0xf70xc90xfd0xd20xd20xfd0xfd0xaf0x40x00x00x00x00x00x00x00x00xa0xe40xfd0xfd0xe00x570xf20xfd0xfd0xb80x3c0x360x90x3c0x230xb60xfd0xfd0x340x00x00x00x00x00x00x00x00x00xd0xfd0xfd0xfd0xfd0xe70xfd0xfd0xfd0x5d0x560x560x560x6d0xd90xfd0xfd0x860x50x00x00x00x00x00x00x00x00x00x20x730xfd0xfd0xfd0xfd0xfd0xfd0xfd0xfd0xfe0xfd0xfd0xfd0xfd0xfd0x860x50x00x00x00x00x00x00x00x00x00x00x00x30xa60xfd0xfd0xfd0xfd0xfd0xfd0xfd0xfe0xfd0xfd0xfd0xaf0x340x50x00x00x00x00x00x00x00x00x00x00x00x00x00x70x230x840xe10xfd0xfd0xfd0xc30x840x840x840x6e0x40x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x0'

np.set_printoptions(suppress=True)
prof_h = profile.split('0x')
ip = [int(_, 16) for _ in prof_h[1:]]
ip = np.array(ip, dtype='float32')/255

ip = ip.reshape([1,28,28,1])

model_input_layer = model.layers[0].input
model_output_layer = model.layers[-1].output

cost_function = model_output_layer[0][1]
gradient_function = K.gradients(cost_function, model_input_layer)[0]
grab_cost_and_gradients_from_model = K.function([model_input_layer, K.learning_phase()], [cost_function, gradient_function])

learning_rate = 0.01
cost = 0.0

while cost < 0.99:
    cost, gradients = grab_cost_and_gradients_from_model([ip, 0])

    ip += np.sign(gradients) * learning_rate

    ip = np.clip(ip, 0, 1.0)

    print("value: {:.8}%".format(cost * 100))

predicted = model.predict(ip)[0][1]
print predicted
ip = ip*255
ip = ip.reshape(784)
ip = ip.astype(int)
ip = ''.join([hex(x)[:-1] for x in ip])
print ip

For more details, you can read my writeup for dog or frog.

flag: hackim19{wh0_kn3w_ml_w0uld_61v3_y0u_1337_fl465}

tweet Share