Bilingual Sums

@mrexcessive WHA

Trend Micro CTF 2015 Programming 200

 

The problem

nc ctfquest.trendmicro.co.jp 51740

Do all the sums.

The solution

We need to repeatedly evaluate sums provided by challenge server and send back the answer.

It starts simply enough with sums like:

1 + 4 =
4 * 0 =

Then it starts to escalate.

First of by including brackets/braces, then using numbers with commas every three digits to separate thousands; then roman numerals are used; then words - so we get sums such as:

two * 5 = 
CXIV + 174 =
7 - three hundred * 5 =

The answers are either right or wrong. It's a fairly simple loop to start with:

  1. Socket receive - get the sum
  2. Calculate using Python eval()
  3. Send the answer

Parsing the input to deal with Roman numerals and words. Words are tricky because there are space separators in numbers like three thousand two hundred twenty one but it's still fairly straightforward.

I found Python code on the interwebs for Roman numbers and numbers as words, it just needed joining in and the spaces dealing with. Links are in the code below.

At the end the server starts mixing up the types of input, so normal decimals; Roman numbers and words.

The final question before flag given - preceeded by about 80 other questions - was:

67095485 * 478545 - 41558 * ( 714363 + 3 ) - twenty one thousand eight hundred sixty two * ( 845 - 82751 ) * 622576 + 9419063254 * four
Going to eval [67095485*478545-41558*(714363+3)-21862 *(845-82751)*622576+9419063254*4]
--> 1146918820371985
Congratulations!
The flag is TMCTF{U D1D 17!}

The code I used was:

#!/usr/bin/python
# calc.py for TrendJP 2015 CTF Pwn 200 - challenge response calculator
#@mrexcessive

import os, sys, code
import readline, rlcompleter
import socket
import time
import struct
import telnetlib

SERVER = "ctfquest.trendmicro.co.jp"      # the actual challenge server
PORT = 51740

pauseDebugging = False     # use this when debugging locally
goTelnetAtEnd = True       # enable once you have some kind of expectation that it isn't all screwed up
                           # and you might actually get a shell

localtest = False
if localtest:
   #TESTING LOCALLY
   SERVER = "localhost"
   PORT = 7777

debug = True
loweralphas = "abcdefghijklmnopqrstuvwxyz"
alphanums = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
printables =   alphanums + ".,<>?/!$%^&*()_-+=@'#][{}`#"
s = None

numwords = None

def DoConnect():
   global s
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   s.connect((SERVER,PORT))
   assert(s <> None)


def GetResponse(timeout=2):
   global s
   s.setblocking(0)
   total_data=""
   begin = time.time()
   while True:
      if total_data and time.time() - begin > timeout:      # wait timeout sec if we have something
         break
      elif time.time() - begin > timeout * 2:               # wait 2xtimeout if nothing
         break
      try:
         data = s.recv(1024)
         if data:
            total_data += data
            if "=" in total_data:         # specific to this CTF
               break
            begin = time.time()
         else:
            time.sleep(0.1)
      except:
         pass
   return total_data

def rom_to_int(string):    # from http://codereview.stackexchange.com/questions/5091/converting-roman-numerals-to-integers-and-vice-versa
   table=[['M',1000],['CM',900],['D',500],['CD',400],['C',100],['XC',90],['L',50],['XL',40],['X',10],['IX',9],['V',5],['IV',4],['I',1]]
   returnint=0
   for pair in table:
      continueyes=True
      while continueyes:
         if len(string)>=len(pair[0]):
            if string[0:len(pair[0])]==pair[0]:
               returnint+=pair[1]
               string=string[len(pair[0]):]
            else:
               continueyes=False
         else:
            continueyes=False
   return returnint

def int_to_roman (integer):
   returnstring=''
   table=[['M',1000],['CM',900],['D',500],['CD',400],['C',100],['XC',90],['L',50],['XL',40],['X',10],['IX',9],['V',5],['IV',4],['I',1]]
   for pair in table:
      while integer-pair[1]>=0:
         integer-=pair[1]
         returnstring+=pair[0]
   return returnstring


def text2int(textnum):    # from http://www.tutorialspoint.com/python/string_isalpha.htm
    global numwords
    if not numwords:       # only setup once
      numwords = {}
      print "Setting up numwords"
      units = [
        "zero", "one", "two", "three", "four", "five", "six", "seven", "eight",
        "nine", "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen",
        "sixteen", "seventeen", "eighteen", "nineteen",
      ]

      tens = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"]

      scales = ["hundred", "thousand", "million", "billion", "trillion"]

      numwords["and"] = (1, 0)
      for idx, word in enumerate(units):    numwords[word] = (1, idx)
      for idx, word in enumerate(tens):     numwords[word] = (1, idx * 10)
      for idx, word in enumerate(scales):   numwords[word] = (10 ** (idx * 3 or 2), 0)
    current = result = 0
    for word in textnum.split():
        if word not in numwords:
          raise Exception("Illegal word: " + word)
        scale, increment = numwords[word]
        current = current * scale + increment
        if scale > 100:
            result += current
            current = 0
    return result + current

def DoMaths():
   while True:
      r = GetResponse()
      print r

      r,drop = r.split(" =")
      if "," in r:
         print "*** removed commas"
         r = r.replace(",","")
         print r

      bits = r.split(" ")
      r = ""
      # these state things for text number parsing
      state = 0      # state 0 is not got a word yet, state 1 is last one was a word
      joining = ""   # joining words together here
      prevans = 0

      for onebit in bits:
         notdone = True
         try:     # try parsing text to numbers first
            test = text2int(joining + " " + onebit)
            state = 1         # success from text2int() if gets to here without exception
            joining = joining + " " + onebit
            prevans = test
            notdone = False      # we used this chunk
         except:
            if state == 0:    # no previous word, so just number
               pass           # so fall through, with notdone == True
            else:             # was a previous word, but this one failed
               r += str(prevans) + " "    # so put previous word translation there... 
               state = 0
               joining = ""               # and reset state
               # and drop through again with notdone == True for current chunk

         if notdone:    # if not used it yet, either roman or symbol or "digits" number
            if "L" in onebit or "I" in onebit or "V" in onebit or "X" in onebit or "C" in onebit or "D" in onebit or "M" in onebit: # ROMAN ?
               r += "%i" % rom_to_int(onebit)
            else:
               r += onebit    # just use the bit

      # after loop through bits
      if state == 1:    # may need to dump out final thing...
         r += str(prevans)

      print "Going to eval [%s]" % r


      # now eval
      ans = eval(r)
      # and send
      print "--> %d" % ans
      s.send("%d\n" % ans)
#      else:
#         reroman = int_to_roman(ans)
#         print "--[ROMAN %i]--> %s" % (ans,reroman)
#         s.send("%s\n" % reroman)

if __name__ == "__main__":
   vars = globals()
   vars.update(locals())
   readline.set_completer(rlcompleter.Completer(vars).complete)
   readline.parse_and_bind("tab: complete")
   shell = code.InteractiveConsole(vars)
   # any startups

   if True:
      DoConnect()
      DoMaths()
   #print text2int("fifty two")  # testing

   if goTelnetAtEnd:
      t = telnetlib.Telnet()
      t.sock = s
      t.interact()

   # go interactive   
   #shell.interact()    # exit... cos... reasons
Congratulations! The flag is TMCTF{U D1D 17!}