Tags: misc 

Rating: 0

## Challenge Introduction
This challenge was quite easy to understand. You telnet to a service and get an ascii-representation of a Vault with a 20-digit lock. Your job is obviously to open the vault somehow. At first I tried to take a recording of the vault operation using asciinema and tried multiple different inputs. Unfortunately I failed to find the proper solution, until a set of hints was published:

```
# HINT Santa filed the following bug-report for 'vault1':
"Ho Ho Ho! Sometimes, when I move a wheel by more than 26 positions, it doesn't do the full animation. Is this a bug?"
- Each value should be in the range -100 to 100.
- The access code is not fixed, it is random per telnet connection.
- After every guess, the wheels are randomized but the access code remains the same.
- There is a very subtle information leak....
- If your eyes are still bleeding, try this a few times and pay close attention: 20 times value 27
```

Now with that information it is actually rather easy to find the solution:

## Keep spamming 20 times 27
Following the last hint we keep entering 20 times 27 until we notice something strage. And indeed after a while we notice that (as indicated by another hint) some wheels only rotate one position, instead of 27. That does not change the end state in any way, so one might consider it a harmless bug. But since this is the only observation to make, we have to try and work with it. The first thing is to verify if the behavior is reproducable. Since the code changes at every connection we do have to do all of the following within a single session:

1. Enter 20 times 27 until we detect one wheel that moves only one position instead of 27. Remember the start and end digit for that wheel.
2. For the next random number calculate the number you would need to reach the previous end digit.
- If you moved from A to B before and now see the value X, the distance would be 4 for example.
3. Enter the distance + 26 (which should not modify the result) and spin the wheel (keep all other values to zero to make monitoring the wheel easier).
4. Check if the wheel spins the additional 26 times or not.
5. Reproduce steps 2-4 two or more times to be sure.
6. Now try the same approach to every other number to see if there is any other number on the wheel that shows the same behavior.
7. Afterwards you can be sure that this bug indicates a single number per wheel, which is exatly what you are looking for.

## Time to break that safe
Keeping in mind that the combination for the save changes with every connection, we need to determine the entire decryption key within a single run. And thanks to the annoyig timing of the vault you have to assume that a single spin of the wheels (with numbers > 26) will easily take >30s. Just trying the 26 possible letters takes more than 15 minutes.
These two aspects convinced me that this challenge had to be solved by an automatic solution. Therefore I implemented the following (really ugly) python script to communicate with the vault and find the correct key. To do that it takes the following approach:

1. Connect to the vault and find out which values the wheels have in the beginning.
2. For every letter(referenced as X) in the alphabet:
1. For every wheel calculate the distance between the current letter and the X.
2. Add 26 to the distance to force the additional turn
3. Enter the values as input, so that all wheels are set to X.
4. Check how far the wheels are spinning.
5. If any of the wheels does not spint for at least 26 characters, we have found the correct value for this wheel.
3. Once we know the correct letter for every wheel, we can calculate the distances needed to set every wheel to the correct value.
4. After entering the correct value, wait for the vault to open and print the output.

```python
#!/bin/python

from pwn import *
import time
import logging

key={}
logging.basicConfig(filename='vault_breaker.log', level=logging.DEBUG)

def parseIntoResult(all_data):
result=[{}]
currentLine=1000
currentRow=1000
for x in all_data:
string = str(x)
if string == "[39m" or string == "\" " or not ';' in string or not 'H' in string:
continue

split= string.split(";")
line = int(split[0][1:])
column= 0
if len(split[1]) == 4:
column=int(split[1][0:2])
elif len(split[1]) == 5:
column=int(split[1][0:3])
elif len(split[1]) == 3:
column=int(split[1][0:1])
else:
#No valid element for our purposes
continue

if currentLine > line:
currentLine=line # We have started reading from a new Terminal now:
result.append({})
dataRow={}

if currentLine < line:
currentLine=line # We have moved on to the next line
result[-1][line]=dataRow
dataRow={}

char=split[1][-1]
dataRow[column]=char
return result

def searchForKeyInColumn(col, result, startingState, targetIndex):
tmp=True
for row in range(8, 11):
if tmp==False:
break
logging.debug("How many turns did it take to reach: "+ chr(targetIndex+64) +" from:" +startingState[int(((col-3)/4) -1)])
for term in result:
if row in term and (len(term[row].keys()) > 1 or list(term[row].values())[0].isspace() or col not in term[row]):
continue
if row in term and col in term[row]:
logging.debug("term["+str(row)+"]: "+str(term[row]))
if row in term and col in term[row] and term[row][col]==startingState[int(((col-3)/4) - 1)]:
logging.debug("Column %i does not expect: %s" % (col, chr(targetIndex+64)))
tmp=False
break
return tmp

def searchForKeys(rawInput, startingState, targetIndex):
#Decide which of them did not go at least one full circle
ins=rawInput[2:].decode("utf-8")
data=ins.split("\x1b")
result = parseIntoResult(data)

tmp={}
for col in range(7,84,4):
if col in key:
continue # No need to check this column any longer, we already know the key for this wheel
tmp[col]=True
for row in range(8, 11):
logging.debug("How many turns did it take to reach: "+ chr(targetIndex+64) +" from:" +startingState[int(((col-3)/4) -1)])
if tmp[col]==False:
break
counter=0
for term in result:
if row in term and len(term[row].keys())==1 and list(term[row].values())[0].isspace():
continue
if row in term and col in term[row] and len(term[row].keys()) == 1:
logging.debug("term["+str(row)+"]: "+str(term[row]))
counter+=1
logging.debug("Turned by "+str(counter)+"positions")
if counter >= 26:
logging.debug("Ruling out value %s for Column %i" % (chr(targetIndex+64), col))
tmp[col]=False
break
if tmp[col]==True:
print("FOUND PART OF THE KEY: Position %i must have value: %s" % (col, chr(targetIndex+64)))
logging.warning("FOUND PART OF THE KEY: %i must have value: %s" % (col, chr(targetIndex+64)))
key[col]=targetIndex

def findInputSequence(startingState, targetIndex):
submitString=""
logging.debug("Input Sequence for targetIndex:"+str(targetIndex)+" and InputSequence: "+startingState)
for c in startingState:
if c != ' ':
index=ord(c)-64
target=((targetIndex - index) % 26) + 26
submitString+=" "+str(target)
else:
submitString+=" 0"
logging.debug(submitString)
return submitString

def getStartingStateForSingleColumn(finalResult, col):
returnString=""
for i in range(7, 85, 4):
if i != col:
returnString+=' '
else:
returnString+=finalResult[i]

return returnString

def getStartingState(finalResult):
returnString=""
for i in range(7, 85, 4):
if i in key:
returnString+=' '
else:
returnString+=finalResult[i]

return returnString

telnet = remote("18.205.93.120", 1201)
inb=telnet.readuntil(b".")
ins = inb[2:].decode("utf-8")
all_data=ins.split("\x1B")

print("DEBUG: Data read")
result=parseIntoResult(all_data)
print("DEBUG: Data parsed")
#Now to find our starting state:
term=result[-1]

#######################BRUTE FORCE SOLUTION #####################################
startingState = getStartingState(term[7])
targetIndex=1

submitString=findInputSequence(startingState, targetIndex)

print("DEBUG: Sending string")
telnet.write(submitString + "\n")
print("DEBUG: Reading response")
inb = telnet.readuntil(b"!") # This is the reaction to our input sequence
print("DEBUG: Search for Keys")
searchForKeys(inb, startingState, targetIndex) # See if we can add new information to the keys list.

for targetIndex in range(2,27):
print("Starting with searchIndex: "+str(targetIndex))
inb = telnet.readuntil(b"!")
ins = inb[2:].decode("utf-8")
all_data=ins.split("\x1B")

result = parseIntoResult(all_data)
term=result[-1]
startingState= getStartingState(term[7])
submitString=findInputSequence(startingState, targetIndex)

telnet.write(submitString + "\n")
inb=telnet.readuntil(b"!") # This is the reaction to our input sequence
searchForKeys(inb, startingState, targetIndex) # See if we can add new information to the keys list

print("Finished processing:")
print(key)

inb = telnet.readuntil(b"!")
ins = inb[2:].decode("utf-8")
all_data=ins.split("\x1B")

result = parseIntoResult(all_data)
term=result[-1]
startingState= term[7]

keyInput=""
for i in range(7, 85, 4):
c1=ord(startingState[i]) - 64
if i in key:
target=key[i]
else:
target=c1
offset=target-c1
keyInput+=" "+str(offset)

logging.warning("Key values should be: "+str(key))
logging.warning("Keystring should be: "+keyInput)
telnet.write(keyInput + "\n")

telnet.interactive()
finalOutput = telnet.readuntil(b"}")
printableOutput = finalOutput.decode("utf-8", "ignore")
logging.debug(finalOutput)
print(printableOutput)
```