LegitBS 2015 Blackbox

@mrexcessive WHA
& Rob Laverick WHA

The problem

Open the box at
nc 52.17.65.252 18324

The solution
NO binary provided - probably a good thing ?!

Rob and I worked on this separately for first three challenges, Rob solved fourth and fifth and we shared work solving final challenge required both of us.

Let's take a look...

$ nc 52.17.65.252 18324
You need to open the box!
Valid characters are: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ "
Max length is: 63 characters.
Let's try some easy boxes:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>             
Password [jklmnopqrstuvwxyz0123456789!"#$%&'()*+,-./:;<=>?@[\]^_`{|]
Expected [XXm$!>=%%+$i|^iv(-=<]

We find:

  • You get 3 tries
  • You get told how your string encrypts
  • You are looking for plaintext to match their encrypted output

So ... send in a charstring
See encrypted result and target result

Are we playing blackbox ?
en.wikipedia.org/wiki/BlackBox(game)
I have played that before, but not so big a board....
We can send in 'many rays' in each of 3 turns... so should be not too hard...
however this wasn't the case ;(

So general approach is

  • Send first salvo - seeking results
  • Gather info
  • Deduce things - possibly tough
  • Send answer or possibly second salvo if needed

OK gets first one right

Built a wrapper to handle these... Core of my code - incorporating Rob's insights and algos - is:

def SendAnswer(p,state):      # send appropriate answer     problem not currently in p, is in state and globals
   global sent, received      # each array of string sent and answer received
   global characters, expected      # charset and expected result (our target)
   global mappings,diffs, nextchar  # mappings never used
   global Box5Sendbase              # Box5Sendbase just a global to 'pull from the left'
   modulus = len(characters)
   Log("SendAnswer state %i passed [%s]" % (state,p))
   if state == 0: # or \
      if boxCounter in [0,1,2,3]:
         sending = characters[:50]
      elif boxCounter in [4]:
         sending = "A" * 36
      else: # boxCounter == 5
         sending = Box5Sendbase
         nextchar = len(sending)
         sending += "A" * (62 - len(sending))
         diffs = []
      mappings = {}
   elif state == 1 or state == 2:      # XXX state 2 here... 
      if boxCounter == 0 or boxCounter == 1:
         ofs = []             # check for simple offset mod len(characters)
         if boxCounter == 0:     # BOX 0: fixed offset across
            Log("Box 0 state %i checking modOfs with:\nsent[0] = [%s]\n   recd = [%s]" % (state,sent[state-1],received[state-1]))
            for i, cSent in enumerate(sent[state-1]):
               idxSent = characters.find(cSent)
               idxRecd = characters.find(received[state-1][i])
               tryofs = (idxRecd - idxSent) % modulus
               ofs.append(tryofs)      # this should work for all the same or fixed pattern
         elif boxCounter == 1:   # BOX 1: offset for char @ posn N+1 is index of char @posn N in character set
            Log("Box 1 state %i calculating ofs with :\n   expected = [%s]" % (state,expected))
            ofsTotal = 0
            for cExpected in expected:
               idxExpected = characters.find(cExpected)
               ofsToSend = ofsTotal
               ofs.append(ofsToSend)      # this should work for all the same or fixed pattern
               ofsTotal += idxExpected
         Log("Offsets determined %s" % ofs)
         sending = ""
         iofs = 0
         for cExpected in expected:
            ofsExpected = characters.find(cExpected)
            ofsRequired = (ofsExpected - ofs[iofs]) % modulus
            cToSend = characters[ofsRequired]
            sending += cToSend
            iofs += 1
      elif boxCounter == 2:   # BOX 2:
         # so mapping is to reverse() of charset plus an offset plus message length
         if state == 1:       # send a string of the right length, just to get the correct offset..
            sending = "BCD" + "A" * (len(expected) -3)
         elif state == 2:     # now calculate offset
            received[1] = received[1][::-1]           # reverse the item received
            Log("Box 2 state %i checking modOfs with:\nsent[0] = [%s]\n   recd = [%s]" % (state,sent[state-1],received[state-1]))
            recdOffset = characters.find(received[1][0])      # what does first 'A' map to
            sentOffset = characters.find(sent[1][0])              # this should be an A
            encodingOffset = recdOffset - sentOffset     # offset for the messasge which was sent to test
            Log("recdOffset = %i, sentOffset = %i, encodingOffset = %i" % \
               (recdOffset,sentOffset,encodingOffset))
            sending = ""
            for cExpected in expected:
               cipOfs = (characters.find(cExpected) - encodingOffset) % modulus
               sending += characters[cipOfs]
            sending = sending[::-1]       # reverse the item to send
      elif boxCounter == 3:      # BOX 3:       # this block based on RobLaverick's code
         if state == 1:
            for i in xrange(1,len(sent[state-1])):
               recdOffsetThis = characters.find(received[0][i])
               sentOffsetThis = characters.find(sent[0][i])
               recdOffsetPrev = characters.find(received[0][i-1])
               sentOffsetPrev = characters.find(sent[0][i-1])
               ofs = (recdOffsetThis - sentOffsetThis) - (recdOffsetPrev - sentOffsetPrev)
               if ofs > -1:
                  Log("Found offset %i" % ofs)
                  break
            sending = ""
            total = 0
            for i in xrange(0,len(expected)):
               cidx = characters.find(expected[i])
               sending += characters[cidx - total]
               total = (total + ofs) % len(characters)
      elif boxCounter == 4:      # BOX 4:        # again Rob's algo
         if state == 1:
            recdOffsetThis = characters.find(received[0][0])
            sentOffsetThis = characters.find(sent[0][0])
            total = recdOffsetThis - sentOffsetThis
            Log("Initial offset %i" % total)
            sending = ""
            for i in xrange(0,len(expected)):
               cidx = characters.find(expected[i])
               sending += characters[cidx - total]
               total = (total + cidx) % len(characters)
            sending = sending[::-1]
      elif boxCounter == 5:      # BOX 5:
         Log("Box 5 state %i with:" % state,True)
         Log("sent = [%s]" % sent[state-1] ,True)
         Log("recd = [%s]" % received[state-1] ,True)
         Log(" ANS = [%s]" % expected,True)
         if state == 1:
            if nextchar % 4 == 0 or nextchar % 4 == 1:
               recd = characters.find(received[0][nextchar])
               wanted = characters.find(expected[nextchar])
            elif nextchar % 4 == 2:
               recd = characters.find(received[0][nextchar+1])
               wanted = characters.find(expected[nextchar+1])
            elif nextchar % 4 == 3:
               recd = characters.find(received[0][nextchar-1])
               wanted = characters.find(expected[nextchar-1])
            change = wanted - recd               
            sending = ""
            for i in xrange(0,len(expected)):
               if i == nextchar:    # if next char to resolve then fix it
                  newchar = characters[change % len(characters)]
                  Box5Sendbase += newchar
                  sending += newchar
               else:                # otherwise use previous value
                  sending += sent[0][i]
         else:
            diffdifferences = []
            for i in xrange(0,len(diffs[state-1])):
               diffdifferences.append((diffs[1][i] - diffs[0][i]) % len(characters))
            print diffdifferences
            print "----"
            return False      # ask for restart, just collecting this info.


   else:       # we have some more information, so deduce things!
      sending = "I am still a test"

   sent.append(sending)
   s.send("%s\n" % sending)
   Log("SendAnswer sent [%s]" % sending)
   return True

Some of the decodes were quick, Rob was way ahead solving 4 and 5 (which I called 3 and 4 for some zero-numbering-fetish reasons...)

We both battled with box 6 - it was unpleasant

Some insights we gleaned: 36 char blocks; characters affect their own cell by a simple offset in the character set, but they also affect all characters to their right within a 36 char block; need to treat each block of 4 chars as a unit - chars +0 and +1 map to themselves, chars +2 and +3 swap their mapping (you can see this used in code above)

So difference from char 'A' in the plaintext, in a particular position, has no complex impact on that single char position - the impact propogates to the right and mixes with all other differences.

The second char position is affected by the first char only in an involved way... not the second char. It has first-char-difference-from-A difference (so B-> + 1), etc. but mod 95

So to send a valid sequence... for each 36 chars in expected
Look at expected[0] you need to send a char[0] which is the difference in this position mod 95.
Look at expected[1] in message. It will be fecked with according to above pattern by your char[0]... you need to unfeck it (TM) by adding working out to where it maps from char[0] and sending the appropriate char to undo both feckings !
All 36 chars accumulate feckings (probably)

Time for another test...
So can I get char[0] right !

OK Yes

GetProblem state 2 passed [Password [OKrV^=m%:81aMZIzRjw*CFXLvADBHP"fg$,k0KrV^=m%:8
Expected [Orr[Y3RFCoa|)1QxECH=go.V66ZL7[uSrF{Zo}nIC<i#,0]

Box 5 state 2 checking modOfs with:
sent[0] = [TAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
   recd = [OKrV^=m%:81aMZIzRjw*CFXLvADBHP"fg$,k0KrV^=m%:8]
sent = [TAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [I~{}];e!9cU5p@y+GN3bQho&>'\q/6tW|_)?u~{}];e!9c]
 ANS = [If{8X1JB>I_bEQ6)_.Oku'}:}fM0hgcJHc^*i=]<B:a85U]

So... first char correct... (recd[0] = ANS[0])
That's the hard part, right ?

Second char...
Will be affected by multiples of first char difference from 'A'
According to our extracted sequence of multiples...
[0, 1, 4, 2, 8, 16, 64, 32, 33, 66, 74, 37, 53, 11, 44, 22, 88, 81, 39, 67, 78, 61, 54, 27, 13, 26, 9, 52, 18, 36, 49, 72, 3, 6, 24, 12]
I need to take into account offset from first char * 1, and then adjust second char using a multiple of 1 on it's own char...

So maybe that is this:

            recd = characters.find(received[0][0])
            wanted = characters.find(expected[0])
            changes[0] = wanted - recd       # this char pos should be easy...
            recd = characters.find(received[0][1])
            wanted = characters.find(expected[1])
            changes[1] = wanted + (1*changes[0]) - recd
    ; sending code for reference...
            sending = ""
            for i in xrange(0,len(expected)):
               sending += characters[changes[i] % len(characters)]

Hmmm not quite right:

sent = [T*AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [R^TJn<i#(s[`-2dO7YExJT<n#is(`[2-OdY7xETJn<i#(s]
 ANS = [Rx&B+4VHK4{cIY{]5NzHZaT_pY<m+P3)9psCrD"Uy=m%>']

and again...
sent = [TAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [I~{}];e!9cU5p@y+GN3bQho&>'\q/6tW|_)?u~{}];e!9c]
 ANS = [If{8X1JB>I_bEQ6)_.Oku'}:}fM0hgcJHc^*i=]<B:a85U]

Much more messing, exchanges of code and thoughts with Rob and then realised could just pull characters one at a time from left to right - because the complicated fecking impact of the characters to the left is already absorbed, so provided we calculate one char at a time should be easy.

Combined with Rob's insight that every four-char block has second pair input-swapped... and we get to pull the whole thing!


Box 5 state 1 with:
sent = [ThAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [PtW6t|?_)u}~{]!;e95cUp+@yGbN3Q&ho>q'\/W6t|?_)u}~{]!;e95cUp+@yG]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
Box 5 state 2 with:
sent = [TheAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Pt0_KV^r=%:m8aM1ZzRIj*CwFLvXABHDPfg"$k0,KV^r=%]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
[0, 0, 0, 30, 60, 25, 5, 50, 10, 20, 80, 40, 65, 35, 45, 70, 90, 85, 55, 75, 15, 30, 25, 60, 50, 5, 20, 10, 40, 80, 35, 65, 70, 45, 85, 90, 75, 55, 30, 15, 60, 25, 5, 50, 10, 20]
----
peter@KaliA:~/CTFs/LegitBS/blackbox$ ./try.py
Box 5 state 1 with:
sent = [TheAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Pt0_KV^r=%:m8aM1ZzRIj*CwFLvXABHDPfg"$k0,KV^r=%:m8aM1ZzRIj*CwFL]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
Box 5 state 2 with:
sent = [The AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Ptz_JT<n#is(`[2-OdY7xETJn<i#(s[`-2dO7YExJT<n#i]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
[0, 0, 94, 1, 94, 93, 87, 91, 79, 63, 62, 31, 29, 58, 42, 21, 84, 73, 7, 51, 14, 28, 17, 56, 34, 68, 82, 41, 69, 43, 77, 86, 59, 23, 92, 46, 89, 83, 47, 71, 94, 93, 87, 91, 79, 63]
----
peter@KaliA:~/CTFs/LegitBS/blackbox$ ./try.py
Box 5 state 1 with:
sent = [The AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Ptz_JT<n#is(`[2-OdY7xETJn<i#(s[`-2dO7YExJT<n#is(`[2-OdY7xETJn<]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
Box 5 state 2 with:
sent = [The fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Ptz_oyNGb3hQ&o'>q\6/Wt_|?)~u}{;]!ec95U@p+yNGb3]
 ANS = [Ptz_o^y(':<)>.J~\(\Y~&_.[PrUA9x&xRrxp vMSQ&hs`]
[0, 0, 0, 0, 0, 31, 29, 62, 58, 21, 84, 42, 73, 51, 14, 7, 28, 56, 34, 17, 68, 41, 69, 82, 43, 86, 59, 77, 23, 46, 89, 92, 83, 71, 94, 47, 93, 91, 79, 87, 63, 31, 29, 62, 58, 21]
----
peter@KaliA:~/CTFs/LegitBS/blackbox$ ./try.py
Box 5 state 1 with:
sent = [The fAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [c*8t6]!;e95cUp+@yGbN3Q&ho>q'\/W6t|?_)u}~{]!;e95cUp+@yGbN3Q&ho>]
 ANS = [c*8t6cEu*@I?i/NA~\24fm&Rqa^q_v_ng;C/2Z4#k0VKvA]
Box 5 state 2 with:
sent = [The flAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [c*8t6cpU@+GyNbQ3h&>o'q/\6W|t_?u)~}]{;!9ec5pU@+]
 ANS = [c*8t6cEu*@I?i/NA~\24fm&Rqa^q_v_ng;C/2Z4#k0VKvA]
[0, 0, 0, 0, 0, 0, 74, 37, 53, 11, 44, 22, 88, 81, 39, 67, 78, 61, 54, 27, 13, 26, 9, 52, 18, 36, 49, 72, 3, 6, 24, 12, 48, 1, 4, 2, 8, 16, 64, 32, 33, 66, 74, 37, 53, 11]
----

So then I wired it up to add the character automatically and just loop


peter@KaliA:~/CTFs/LegitBS/blackbox$ ./try.py
Box 5 state 1 with:
sent = [The flAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [EX6s2UJETn#<i(`s[-O2d7xYEJnT<#(is`-[2O7dYxJETn#<i(`s[-O2d7xYEJ]
 ANS = [EX6s2U&eHy%Q3X][x}?)^3oz@NjQ<dQD;.\)e+2"gs[`;!]
Box 5 state 2 with:
sent = [The flaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [EX6s2Uje*wFCLXAvBDPHf"$gk,K0Vr=^%m8:a1ZMzIjR*w]
 ANS = [EX6s2U&eHy%Q3X][x}?)^3oz@NjQ<dQD;.\)e+2"gs[`;!]
----
Box 5 state 1 with:
sent = [The flAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [zW2quEo&>'\q/6tW|_)?u~{}];e!9cU5p@y+GN3bQho&>'\q/6tW|_)?u~{}];]
 ANS = [zW2quED~(<^-CMq!4LrDJ`>Y)?a79_!a.(96:*y9YcU5t|]
Box 5 state 2 with:
sent = [The flaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [zW2quE%~m:a81MzZIR*jwCLFXvBADHfP"gk$,0VKr^%=m:]
 ANS = [zW2quED~(<^-CMq!4LrDJ`>Y)?a79_!a.(96:*y9YcU5t|]


A minute later ...

Box 5 state 1 with:
sent = [The flag is: Gr3aT j0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Rx&B+4VHK4{cIY{]5NzHZ                                         ]
 ANS = [Rx&B+4VHK4{cIY{]5NzHZaT_pY<m+P3)9psCrD"Uy=m%>']
Box 5 state 2 with:
sent = [The flag is: Gr3aT j0bAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [Rx&B+4VHK4{cIY{]5NzHZa1aMZIzRjw*CFXLvADBHP"fg$]
 ANS = [Rx&B+4VHK4{cIY{]5NzHZaT_pY<m+P3)9psCrD"Uy=m%>']
----
Box 5 state 1 with:
sent = [The flag is: Gr3aT j0bAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA]
recd = [i@N(7eMy^UpzFS%-JMvFRK[`-2dO7YExJT<n#is(`[2-OdY7xETJn<i#(s[`-2]
 ANS = [i@N(7eMy^UpzFS%-JMvFRKy4I1FBTELw'3Fe8lJ_l2dO"g]
Box 5 state 2 with:
sent = [The flag is: Gr3aT j0b!AAAAAAAAAAAAAAAAAAAAAAA]
recd = [i@N(7eMy^UpzFS%-JMvFRKz4IR*jwCLFXvBADHfP"gk$,0]
 ANS = [i@N(7eMy^UpzFS%-JMvFRKy4I1FBTELw'3Fe8lJ_l2dO"g]

and finally


Box 5 state 1 with:
sent = [The flag is: Gr3aT j0b! BlackBox Ma$Ter$@!!!%AAAAAAAAAAAAAAAAA]
recd = [@]Z.@;Oz|c,&(zIt@$(Q9DWqvkg!,R"-:,:%OoV 9HfP&(`s[-O2d7xYEJnT<#]
 ANS = [@]Z.@;Oz|c,&(zIt@$(Q9DWqvkg!,R"-:,:%OoV 9HfP&o]
Box 5 state 1 with:
sent = [The flag is: Gr3aT j0b! BlackBox Ma$Ter$@!!!%%AAAAAAAAAAAAAAAA]
recd = [oA9~8gU2I0-U*3Y1UioxD:hA(=S3t4d5JgI<%x5Rm4lS<#g"$k0,KV^r=%:m8a]
 ANS = [oA9~8gU2I0-U*3Y1UioxD:hA(=S3t4d5JgI<%x5Rm4lS<#]

Flag submitted 2 points