0x0G is Google’s annual “Hacker Summer Camp” event. Normally this would be in Las Vegas during the week of DEF CON and Black Hat, but well, pandemic rules apply. I’m one of the organizers for the CTF we run during the event, and I thought I’d write up solutions to some of my challenges here.
gRoulette is a simplified Roulette game online. Win enough and you’ll get the flag. The source code is provided, and the entire thing is run over a WebSocket connection to the server.
Examining the websocket flow, we series of messages:
1
2
3
{"message_type":"register","balance":100,"round_id":1991705954}
{"message_type":"round_result","round_result":{"roundid":1991705954,"space":"31","next_roundid":520308631}}
{"message_type":"round_result","round_result":{"roundid":520308631,"space":"8","next_roundid":439000315}}
Taking a look at the source code, we see that the rounds are handled by a
function in roulette.go
:
1
2
3
4
5
6
7
8
9
10
11
12
13
func (g *RouletteGame) PlayRound() {
...
// Finish the round
finishedRound := g.CurrentRound
space := g.NextSpace()
g.CurrentRound = g.prng.Next()
res := &RoundResult{
RoundID: finishedRound,
NextRoundID: g.CurrentRound,
Space: space,
}
...
This tells us the space where the “ball” lands is computed using a NextSpace function:
1
2
3
4
5
6
7
func (g *RouletteGame) NextSpace() SpaceID {
num := g.prng.BoundedNext(37)
if num == 37 {
return "00" // Special case double 0.
}
return SpaceID(fmt.Sprintf("%d", num))
}
Of interest is that both the CurrentRound
and Space
values are derived from
the same PRNG instance. Depending on the security of the RNG, it may be
possible to predict the next space(s) based on the current state of the RNG.
The source of the PRNG is provided as well:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main
import (
"crypto/rand"
"encoding/binary"
)
const (
PrngModulus uint32 = 0x7FFFFFFF
PrngMultiplier uint32 = 48271
)
type PRNG uint32
func NewPRNG(seed uint32) *PRNG {
if seed == 0 {
if err := binary.Read(rand.Reader, binary.BigEndian, &seed); err != nil {
panic(err)
}
}
p := PRNG(seed)
return &p
}
/*
Algorithm certified by Nanopolis Gaming Commission
*/
func (p *PRNG) Next() uint32 {
tmp := uint64(*p) * uint64(PrngMultiplier)
tmp %= uint64(PrngModulus)
*p = PRNG(tmp)
return uint32(tmp)
}
func makeBitmask(v uint32) uint32 {
rv := uint32(0)
for v != 0 {
rv = rv << 1
rv |= 1
v = v >> 1
}
return rv
}
func (p *PRNG) BoundedNext(max uint32) uint32 {
mask := makeBitmask(max)
for {
tmp := p.Next() & mask
if tmp <= max {
return tmp
}
}
}
The Next
method is responsible for advancing the PRNG. It multiplies by a
constant, then takes a modulus. Searching for the constants reveals that this
is an implementation of a well-known Linear Congruential
Generator. This
implementation is similar to the MINSTD
RNG, and exposes the entire state in a
call to Next
. Notably the RoundID
is entirely an output of the PRNG, so
every subsequent value can be known. Consequently, we can call the PRNG with
our own inputs to find out what the next spins will be.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main
import (
"fmt"
"os"
"strconv"
)
func main() {
seed64, err := strconv.ParseInt(os.Args[1], 10, 32)
if err != nil {
panic(err)
}
round := uint32(seed64)
p := NewPRNG(round)
for i := 0; i < 8; i++ {
roll := p.BoundedNext(37)
v := fmt.Sprintf("%d", roll)
if roll == 37 {
v = "00"
}
fmt.Printf("%d: %s\n", round, v)
round = p.Next()
}
}
When we run it, this will print out the next 8 rolls:
1
2
3
4
5
6
7
8
9
% go run . 520308631
520308631: 8
439000315: 0
2059893773: 33
1060020398: 5
1254119902: 32
1689320946: 2
918638365: 28
114073520: 20
Just max bet each one and you’ll have the requisite money in no time. :) (Feel free to automate it. I just did it manually.)
1
FLAG: 0x0G{maybe_vegas_next_year_for_real!}