PCTF 2012 – The Game Writeup
During the weekend we had a lot of fun with many of the PCTF challs, althought we were a bit dissapointed with the lack of web-based exploitation challenges [which, incidentally, as a websec team, severely limited our performance ;P].
Either way, there were also some interesting non-websec challenges, namely "The Game".
The Game worked like this:
You connected to a port on their gameservers, and the "game" asked you the following:
You have gotten 0 of 75
Choice 1 = 92c16e01df933c0e8b4d164e28364a9070
Choice 2 = 725d88cb65043a82efd6487dede693678a
Which one is bigger? (1 or 2)
After some experimenting, we realised that the values in the choices were in fact transcendent large "variable" names - each one had a numeric value, but that value had no correlation to the name of the variable [which incidentally was made to look like a hash - leading to quite a lot of confusion in the beginning].
Instead of going all out and programming something, I decided to sit and theoretically go through the steps. The most obvious solution, which was also my first thought, was to create a big dictionary/list/array with all the values, then with every new piece of information move the corresponding value up and down the list, until we get a near accurate represantation of the hierarchy of values. At the time I came up with some strange explanation as to why the approach would fail, but as I now write this writeup I realise that my failure scenario depended on the game giving contradictory statements, which would not have happened. Either way, this is the first solution, which I did not implement for said reason, but which I think would work [and would be fairly much easier to implement than my solution].
a) Let's say we shorten the variables to x, y, qand z. Now we get a question regarding q and x, and we guess it. We either answer correctly, or incorrectly, and from that we learn whether q is greater than x [q > x]. If q now is greater than x, then we can add to the following to our list [from smallest to greatest]:
list = [x, q]
Now, we get more info, and we learn that z is smaller than q [z < q]. Immediately we have a problem - should we place z before or after x? For the time being, we can just generally give a rule saying to place it at the "middle" - now we have:
list = [x, z, q]
Now we get even more info - y is greater than x [y > x]- we do the same thing
list = [x, z, y, q]
Now, one last piece of info - y is greater than q [y > q].
list = [x, z, q, y]
As more information is given, the list also becomes more accurate, and for every increase in accuracy you can just also start guessing answers using the list. After a while, you will have a very low error rate!
Now, my solution was a bit more complicated. In essence, it relies on creating a list with all the "relationships" between the variables/hashes. After you have enough of these relationships, you can use them to compute "chains" between two "unknown" hashes - which you do not know the relationship between. Here is a quick explanation:
If x > y, and y > q, then you can immediately figure out that x > q. This approach only works if the "chain" consists entirely of less than alternatively greater than "links". The issue with this approach is that as you get more relationships [in this case many thousands], the chains can become very long. The algorithm I implemented to search for these chains unfortunately went down the full length of every "branch" in the relationship tree, first going down "increasing" [greater than] branches, then "decreasing" [smaller than] branches. This meant that to find a chain that was decreasing, it would first have to compute through all the increasing chains, and check if it finds anything. A better approach (I assume, at least), would have been to implement a width-first chain-searching algorithm - ie looking through each "level" completely, then moving onto the next one [looking through all 1-length chains first, then all 2-length chains].
All in all, my implementation was not exactly very efficient, but due to lack of time the only "efficiency" patch I had time to implement was a limit on the length of the chains, and a few lines to "re-add" the ends of the chain to the relations table for the beginning of the chain, in an attempt to make look-ups faster. [This may actually have increased the lookup time - didn't have time to run any tests or do any calculations on it].
You could obviously also compute and apply a statistical approach to the challenge - for example; if x is the smallest "number" we assume we know, then the chance of a completely unknown number being smaller than x is smaller than the chance of said number being greater than x. If anyone wants to do this, or even just implement some math into this [how many "relations" would you need to be able to accurately predict answer with an accuracy of x%? what is the optimum chain length? what is the optimum solution (least requests)?], feel free to do so! (read: please do it!)
Here is a quick copy and paste of the script - fairly unadultered from the one I used during the competition, haven't had time to install any code-parsing highlighter yet, will do that soon. If you want highlights - check it out here on pastebin instead [http://pastebin.com/RtLFGWpc]
import socket
import pickle
HOST = '23.22.16.34' # The remote host
PORT = 6969 # The same port as used by the server
# Too lazy to be consistent, just gave some variables their default values here!
previous_answer = []
relations = {}
previous_answer.append('Nope')
previous_answer.append('Nope')
def opensocket():
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
return s
def senddata(s, answer=1):
s.sendall(str(answer) + "\n")
def recdata(s):
data = []
data.append(s.recv(256))
data.append(s.recv(256))
data = ''.join(data)
#print data
return data
def getnext(relations, curr, type, current_answer, level):
if not relations[curr][type]:
#print "No level " + str(level) + " neighbours!"
return 0
for x in relations[curr][type]:
if(x == current_answer[1]):
#print "FOUND CHAIN!"
return 1
break
else:
if(level < 6): #Make the longest chains 5! if(getnext(relations, x, type, current_answer, (level+1)) == 1): return 1 break return 0 def maprelations(relations, current_answer): continuebool = 1 try: relations[current_answer[0]] except KeyError: #print "No firstlevel neighbours" continuebool = 0 return 0 if(continuebool == 1): for x in relations[current_answer[0]]['greater']: if(x == current_answer[1]): #print "FOUND CHAIN!" return 1 break else: if(getnext(relations, x, 'greater', current_answer, 1) == 1): relations[current_answer[0]]['greater'].insert(0,current_answer[1]) relations[current_answer[1]]['smaller'].insert(0,current_answer[0]) return 1 for x in relations[current_answer[0]]['smaller']: if(x == current_answer[1]): #print "FOUND CHAIN!" return 2 break else: if(getnext(relations, x, 'smaller', current_answer, 1) == 1): relations[current_answer[0]]['smaller'].insert(0,current_answer[1]) relations[current_answer[1]]['greater'].insert(0,current_answer[0]) return 2 #print "No n-level neighbours" return 0 def parseresponse(s, response, first=0): global previous_answer global relations current_answer = [] current_answer.append(0) current_answer.append(0) #print response response = response.split("\n") if(previous_answer[0] == 'Nope'): current_answer[0] = response[1][11:] current_answer[1] = response[2][11:] elif(previous_answer[0] == 'Known'): current_answer[0] = response[4][11:] current_answer[1] = response[5][11:] print "\n+++++++++++++++++\nSanity check: We were - '" + str(response[0]) + ":" + response[1] + "'\n+++++++++++++++++\n" #Sanity check, we should not have to use this to get info! else: current_answer[0] = response[4][11:] current_answer[1] = response[5][11:] #Now we put the answer to memory! #print "Current record is: '" + response[3] + "'!" #print "Previous guess was - '" + response[1] + "'" print "\n+++++++++++++++++\nHad to guess, and I guessed: " + response[1] + "\n+++++++++++++++++\n" if(response[1] == 'Wrong
'): bigger = 0 else: bigger = 1 try: relations[previous_answer[0]] except KeyError: relations[previous_answer[0]] = {} relations[previous_answer[0]]['greater'] = [] relations[previous_answer[0]]['smaller'] = [] try: relations[previous_answer[1]] except KeyError: relations[previous_answer[1]] = {} relations[previous_answer[1]]['greater'] = [] relations[previous_answer[1]]['smaller'] = [] #print "[DEBUG] Saving New Relations!" if (bigger == 1): #print "[DEBUG] 1 is bigger than 2!" relations[previous_answer[0]]['greater'].insert(0,previous_answer[1]) relations[previous_answer[1]]['smaller'].insert(0,previous_answer[0]) else: #print "[DEBUG] 1 is smaller than 2!" relations[previous_answer[0]]['smaller'].insert(0,previous_answer[1]) relations[previous_answer[1]]['greater'].insert(0,previous_answer[0]) guess = maprelations(relations, current_answer) if(guess>0):
previous_answer[0] = 'Known'
return guess
else:
previous_answer = current_answer
return 1
socket = opensocket()
result = parseresponse(socket, recdata(socket),1)
x = 1 #Starting iteration!
#Load old relations!
pkl_file = open('relations.pkl', 'rb')
relations = pickle.load(pkl_file)
pkl_file.close()
#print relations
try:
while 1:
print "[DEBUG] Iteration:" + str(x)
senddata(socket, result)
result = parseresponse(socket, recdata(socket))
x=x+1
except KeyboardInterrupt:
#Save relations!
output = open('relations.pkl', 'wb')
pickle.dump(relations, output)
output.close()
socket.close()
May 4th, 2012 - 07:50
What was the objective of the challenge? It is not clear.
May 21st, 2012 - 19:09
very nice post. good stuff.