Tags: rng 

Rating: 5.0

# Doomed to Repeat It

We have a classic game of memory, where you can only see one tile
at a time. Once you unveil two identical tiles in succession, they stay face up.

The goal is to find all pairs.

![gameplay](https://i.imgur.com/2hGmv4K.gif)

The caveat : there are 56 tiles and you can only use 60 moves. Gotta be lucky.

## The source code

We have the source code, so let's dive into that.

There are a lot of red herrings in comments, but the biggest red flag should be
the custom RNG. after examination, turns out there is a problem with the implementation.

### Bad RNG

This function is used to get random bits from the os in order to generate a seed.
```
// OsRand gets some randomness from the OS.
func OsRand() (uint64, error) {
// 64 ought to be enough for anybody
var res uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &res;; err != nil {
return 0, fmt.Errorf("couldn't read random uint64: %v", err)
}
// Mix in some of our own pre-generated randomness in case the OS runs low.
// See Mining Your Ps and Qs for details.
res *= 14496946463017271296
return res, nil
}
```
It gets a 64bit uint from the os, and then multiply it with some "pregenerated randomness"

the pre-generated randomnessis quite problematic. It is not random at all.

![xkcd](https://i.imgur.com/uvslFTg.png)

(https://xkcd.com/221/)

* `14496946463017271296`'s prime factorisation is `2^47 * 103007`.

*They are multiplying the os's randomness with 2^47!*

* Doing modulo multiplication on your randomness is bad.
* Doing it with a very large power of two completely breaks it.

#### An example

Let's look at one multiplication to see why this is such a problem :

random number multiplied by 2^47:
```
8979835648265197673 111110010011110110010001111000111100100001111001001100001101001
* 140737488355328 100000000000000000000000000000000000000000000000
----------------------------------------------------------------------------------------
5491154583159832576 100110000110100100000000000000000000000000000000000000000000000
```

As we can see, this completely eliminates the 47 least significant bits of the randomly
generated number.

Instead of having `2^64` possible seed, we now have `2^17` possible seeds (and thus `2^17` possible layouts!) thats 131072,
and that's totally manageable.

#### Test the theory
This is pretty easy to test : if we patch the OsRand function and replace the
"pregenerated randomness" with an even bigger power of two :

```
func OsRand() (uint64, error) {
// 64 ought to be enough for anybody
var res uint64
if err := binary.Read(rand.Reader, binary.LittleEndian, &res;; err != nil {
return 0, fmt.Errorf("couldn't read random uint64: %v", err)
}
// Mix in some of our own pre-generated randomness in case the OS runs low.
// See Mining Your Ps and Qs for details.
res *= 1 << 63 // multiplying by 2^63 should squash 63 of the random number's 64 bits
return res, nil
}
```
With this modification, the game only ever generate one of two possible layouts.

We're on to something!

## Boards enumeration

We already have code that takes care of taking a seed and generating a board layout out of it.
Let's keep it simple, and re-use that. We patch the app's source code to have it dump every
possible layouts.

A small modification to `newBoard()` and the random's library `New()` function should do it.

This will generate all possible numbers between that have their 47 least significant
bits set to zero, and use each of them as a source of entropy for the seed to generate one
board.

```
func newBoard() (*board, error) {

var b *board
var i uint64
var upper uint64
upper = 1<<17 // 2 ^ 64
for i= 1; i< upper; i++ { // add a loop over all board
var s uint64
s = i << 47 // generate false_seed

rand, err := random.New(s) //send that false_seed in the
// RNG to be used as the 'os random bits'

if err != nil {
return nil, fmt.Errorf("couldn't create random: %v", err)
}
b = &board{
nums: make([]int, BoardSize),
visible: make([]bool, BoardSize),
}
for i, _ := range b.nums {
b.nums[i] = i / 2
}
for i := BoardSize - 1; i > 0; i-- {
j := rand.UInt64n(uint64(i) + 1)

b.nums[i], b.nums[j] = b.nums[j], b.nums[i]
}

// and we add a print statement to dump the board do stdout
log.Printf("[%d] %v",s, b) // print the board!

}
return b, nil
}
```

We modify the New function so that it uses our specially crafted uint64
instead of the OSrand function

```
func New(proto_seed uint64 ) (*Rand, error) {
// osr, err := OsRand()
// if err != nil {
// return nil, fmt.Errorf("couldn't get OS randomness: %v", err)
// }
// return NewFromRawSeed(osr)
return NewFromRawSeed(proto_seed)
}
```

With these modifications in place, running a game will print 2^17 boards to stdout.
with a little bit of parsing, we can get them in a nice format :

```
24 3 24 18 27 26 17 9 23 7 6 0 2 11 14 8 20 5 16 19 18 5 20 19 22 21 1 10 13 8 13 0 9 2 27 14 23 12 15 16 25 12 17 7 21 22 6 15 25 1 11 4 3 26 10 4
26 20 24 9 0 18 27 15 0 24 21 1 6 27 4 5 25 20 12 3 17 7 7 22 4 2 11 11 17 23 14 8 13 10 8 19 9 14 15 10 2 18 6 21 23 19 1 16 3 26 13 12 22 16 5 25
```

A little python script will load them all in an dictionnary, with the first 4 tiles as key :
```

BOARD_FILE = 'boards.txt'

from collections import defaultdict

boards = defaultdict(list)
def init():
with open(BOARD_FILE) as f:
for line in f:
permutation = line.strip().split(" ")
board = [int(item) for item in permutation]
board_key = tuple(board[:4])
boards[board_key].append(board)

def get_all(key):
return boards[key]
```

This gives us a simple API to query the list of boards. for instance, this will get us all
boards that have the following tiles starting top left corner `2,43,21,23`
```
boards.init()
boards.get_all( (2,43,21,23) )
```

## Interracting with the game server

The game front-end interract with the game server via websockets. This was done with
the python websockets library

some important points to note :

1. The websocket library used needs to understand the websocket handshake, as we
don't want to implement the http-> websocket upgrade handshake
2. The hostname needs to be specified or else the handshake won't be valid, and the server
will reject the query

### Connect to the game server
This piece of (asyncio) code will connect:
```
async with websockets.client.connect(
URI,
origin='https://doomed.web.ctfcompetition.com') as ws:
pass
```

### Play a game
These co-routines can be used to initiate the game state and send some guesses

```
async def get_info(ws):

qry = {"op":"info"}
await ws.send(json.dumps(qry))
response = await ws.recv()
return json.loads(response)

async def send_guess(x,y, ws):

qry = {"op":"guess",
"body":{"x":x,
"y":y}}
await ws.send(json.dumps(qry))
response = await ws.recv()
return json.loads(response)

```

At this point, it's just a matter of trying the first four tiles, infering the
correct game board, and then solve a game where we know the position of all tiles!

![Imgur](https://i.imgur.com/LjXJItZ.gif)

Full python code :

```

#!/usr/bin/env python3

import websockets
import asyncio
import json

URI = 'wss://doomed.web.ctfcompetition.com/ws'

import boards
boards.init()

async def send_guess(x,y, ws):

qry = {"op":"guess",
"body":{"x":x,
"y":y}}
await ws.send(json.dumps(qry))
response = await ws.recv()
return json.loads(response)

async def get_info(ws):

qry = {"op":"info"}
await ws.send(json.dumps(qry))
response = await ws.recv()
return json.loads(response)

def xy_to_linear(x,y):
return y*7 + x

def linear_to_xy(linear):
x = linear % 7
y = linear // 7
return(x,y)

async def find_board(ws):
board_key = []
for x in range(0,4):
response = await send_guess(x,0, ws)
index = xy_to_linear(x,0)
board = response['board']
board_key.append(board[index])

return tuple(board_key)

async def play():

async with websockets.client.connect(URI,
origin='https://doomed.web.ctfcompetition.com'
) as ws:

game = await get_info(ws)

board_key = await find_board(ws)

print (f"Board key : {board_key}")
valid_boards = boards.get_all(board_key)
print ("found {} valid boards".format(len(valid_boards)))
order = solve(valid_boards[0])
print(valid_boards[0])
print(order)

for i in order:
x,y = linear_to_xy(i)
game = await send_guess(x,y, ws)
print_board(game["board"])
if game["message"]:
print('\n\t' + game["message"] + '\n\n')

def print_board(board):
print(" ".join([f'{x: >2}' for x in board] ))

def solve(board):

order = []
for item in range(0,28):
order.extend([i for i, x in enumerate(board) if x == item])
return order

asyncio.get_event_loop().run_until_complete(play())
```