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