e-Xpert Solutions Lausanne
Auteur : Michael Molho
Date de publication : 21 février 2019 - Dernière mise à jour : 21 février 2019
As usual at BlueHat, the challenges were crazy! This year, there was a “Zumo-Boats Battle Royale”, in which each team could built its own boat to fight all the others. At the end, there should be only one ! We could not participate to this chall, as it needed much time and as we are perfectly noobs in the fields of 3D printing/electronics.
Except the « battle royale » challenge, we noticed there were many Qrcode sticked everywhere on the walls and various objects of the venue.
When scanning these Qrcode, we get 3 values:
The challenge was to find out what to do with these “ssss” values … After some attempts, decoding, reverse, xor … Googling, we found the algorithm named “Shamir's Secret Sharing Scheme” also called SSSS. This algorithm aims to hide a secret into several pieces. Sounds good ! And guess what ? There is an online implementation😊
http://point-at-infinity.org/ssss/demo.html
Putting altogether the 3 values gave us the secret, which is an URL with a GIF : http://aka.ms/h1ghr0llerz
The tag in the GIF was the same as a tag on a real wall in the venue. So that the arrow indicates where to go. In that direction, there was a door with a security guard asking us for the secret URL. Bingo ! Behind the door, there was a completely new room, and it was a casino ! With a roulette, slot machines, lottery, blackjack tables … Crazy ! And of course, the next challenges were to hack the machines and beat the casino.
We focused on one chall : the blackjack, called here “BlueJack”. The idea was simple: we had the sources of the program which shuffled the decks used by the croupiers. We also had the version of Python used (3.6.8) and we knew that this program was ran on Linux 64b. If we can hack the code, we can guess the cards and always win at BlueJack.
Here is the code:
import random
import os
SUITS = ['CLUBS', 'DIAMONDS', 'HEARTS', 'SPADES']
FACES = ['ACE', 'TWO', 'THREE', 'FOUR',
'FIVE', 'SIX', 'SEVEN', 'EIGHT', 'NINE',
'TEN', 'JACK', 'QUEEN', 'KING']
class Card(object):
def __init__(self, suit, face):
assert suit in SUITS, suit
assert face in FACES, face
self.suit = suit
self.face = face
def __repr__(self):
return "%s of %s" % (self.face, self.suit)
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.suit == other.suit and self.face == other.face)
return False
class Deck(object):
def __init__(self, suits=SUITS, faces=FACES):
self.cards = []
for suit in suits:
for face in faces:
self.cards.append(Card(suit, face))
class Shoe(object):
def __init__(self, deck_count=6, rng=None):
self.cards = []
for _ in range(deck_count):
self.cards.extend(Deck().cards)
if not rng:
rng = random.Random(os.urandom)
self._rng = rng
self.shuffle()
def shuffle(self):
self._rng.shuffle(self.cards)
def draw(self, count=1):
if not self.cards:
return None
return [self.cards.pop() for _ in range(count)]
This code is pretty simple and straightforward. It creates a “cards” list composed of 6 decks, each deck is composed of the standard 52 cards. At the end, a stack of 312 cards. This stack is then shuffled with the “shuffle” method in the Python class “random.Random” with the argument “os.urandom”, which is a function returning random numbers using the entropy of the system (/dev/urandom).
This code is so simple and looks so standard that it’s hard to imagine it’s somehow hackable. Our first idea was to understand how exactly the deck is shuffled, so we focused on how random.Random works exactly in Python. After some research, we found that the class random.Random is internal to Python and is not really intended to be used directly :
“The functions supplied by this module are actually bound methods of a hidden instance of the random.Random class. You can instantiate your own instances of Random to get generators that don’t share state.”
Since the documentation is very limited, we had to look at the source code of Python directly to find out how it works. We see in “random.py” that the constructor of random.Random calls the method “seed()” on the parent class, which is “_random.Random”, a native C class.
class Random(_random.Random):
def __init__(self, x=None):
………
self.seed(x)
def seed(self, a=None, version=2):
…….
super().seed(a)
So we looked at the C native code “_randommodule.c” in Python and we found something interesting:
static PyObject *
random_seed(RandomObject *self, PyObject *args)
{
…….
if (PyLong_Check(arg)) {
……..
}
else {
Py_hash_t hash = PyObject_Hash(arg);
………
}
It looks like the random.Random constructor is supposed to get an integer as argument to use it as a seed for the random numbers generator. If it is not an integer, then Python tries to hash the object to get an integer from any kind of object, as long as it is hashable.
Remember, in our case, the code provided does : random.Random(os.urandom)
It means it calls the constructor of random.Random with the argument os.urandom, which is a function. Is a function hashable in Python ... ? I had no idea. But the answer is yes ! Here again, we had to look at the source code of Python to get what is the value of a hash of a function in the C native code "methodobject.c" :
static Py_hash_t
meth_hash(PyCFunctionObject *a)
{
Py_hash_t x, y;
x = _Py_HashPointer(a->m_self);
y = _Py_HashPointer((void*)(a->m_ml->ml_meth));
x ^= y;
if (x == -1)
x = -2;
return x;
}
Ok so in Python, when you hash a function, you get something related to the address of the function in memory. good ! Because the address of a Python function in memory is not 100% random, it only depends on where Python was loaded in memory by ASLR. Let’s try to see if this value is predictable enough to exploit that.
We wrote a very simple script which starts 5 threads, each threads loops in spawning a new Python process and writing the output of “hash(os.urandom)” in a file :
def showhash():
while 2>1:
cmd = "python -c \"import os; import sys;"
cmd += "seed=hash(os.urandom);"
cmd += "print(seed if seed>=0 else seed % ((sys.maxsize + 1) * 2))\" >> seeds.txt"
os.system(cmd)
thread1 = threading.Thread(target = showhash)
thread2 = threading.Thread(target = showhash)
thread3 = threading.Thread(target = showhash)
thread4 = threading.Thread(target = showhash)
thread5 = threading.Thread(target = showhash)
thread1.start()
thread2.start()
thread3.start()
thread4.start()
thread5.start()
thread1.join()
After running this code for ~1h, we noticed that there were ~1200 unique values of “hash(os.urandom)”. Bingo ! Since this value is used as a seed for the random number generator, it means we know that the seed currently used to shuffle the decks is one of these 1200 values. At the end, this code is able to generate only 1200 different stacks of decks, and we are able to generate them because we know the possible seed values.
So we wrote an exploitation script which generates 1200 CSV files containing the 1200 possible stacks (composed of 6 decks) after the shuffling based on the values we found previously for “hash(os.urandom)” :
def main():
with open("./seeds-uniq.txt", "r") as f:
for l in [x.strip() for x in f.readlines()]:
deck_id = uuid.uuid4().hex
seed = int(l)
shoe = card_shuffler.Shoe(rng=random.Random(seed))
with open('shoe_%d_guess.csv' % (seed,), "w") as csvfile:
csvwriter = csv.writer(csvfile)
csvwriter.writerow(["FACE", "SUIT"])
for card in shoe.cards:
csvwriter.writerow([card.face, card.suit])
So now we knew that when we will be in face of the croupier, the deck he is using is one of 1200 we have in our CSV files.At this point the job is done. We needed to ask the croupier to pull ~5 cards from the deck to target exactly which CSV file was the right one. And from there, we could “guess” all the following cards … And guess what? We beat the croupier 😉
Unfortunately, we could not have a look at the others challenges since it was in parallel to the conferences, and we wanted to attend the interesting talks. There was no dedicated time for the challenges, so we worked on “BlueJack” mainly the end of the first day and the evening between the first and the second day. After speaking with the guy who made the challenges (Alon Livne), he said there were 2 others challenges. One web and one crypto. At the time where we talked to him (~2PM the second day), nobody solved these two others, and ~10 teams solved the BlueJack.
The challenges were very exciting and the level was high. It was a pleasure to play, and also to get some success 😊. A big thank you and well done to Alon who built these challenges. I hope to come back next year !
Events
Archives