## Level 2

To get to level 2, we first have to pass level 1. The level 1 however is just
a normal wordle game. Although the word list is larger than the standard
wordle game, it's easy to pass with existing online tools and by hand.

Reading the logic of the second game, it is generating a string of 5 emojis at
random each time at the start of a new game. The emojis are chosen from a
fixed list of 236 emojis, meaning there are $236^5\approx 7.3 \times 10^{11}$
possible combinations. We have 6 times of guess, which is clearly not enough
for guessing even with an algorithm, or we have to get really lucky.

Looking more into the code, we see that the randomzation part of chooing
emojis is done by Go's builtin `math/rand` library. We then look for the
potential of a pRNG hack. If this path is correct, we need to know two things:
what is the seed of the pRNG and where we are in the random number stream.

We found that at the start of the server, the current time in miliseconds in
Unix timestamp will be chosen as the seed for the pRNG. So the question
becomes how we can get the start time of the server. The obvious answer is to
crash the server, or to ask for a server restart. The former seems to be
against the rules, the later is rejected by the admin.

After some more digging, we finally realize that the server's TLS cert is
generated at the server start time as well, and the code will choose that time
as the cert's `NotBefore` field. Knowing this, we are able to use the server's
cert as an accurate indicator of the server start time. However, after seeing
the actual value, we realize that `NotBefore` field is only down to the
seconds, that means we have probably $\pm 1000$ miliseconds of range we need
to try, due to rounding and delay between two pieces of code.

The second piece of the puzzle, knowing where we are in the random number
stream, is rather easy. Level 1 also uses the same pRNG to generate target
words, but we know level 1 is solvable and we have the word list, so we are
able to know exactly which random number (mod list size) was generated.

Take everything together: we need to query level 1 multiple times to get some
consecutive random numbers, then brute-force the possible seeds to get a
sequence of random numbers. We then compare each sequence with the acquired
random numbers generated by the server, and whichever sequence contains the
list of acquired random numbers is the correct sequence, and we are able to
know what random numbers will come next. A few details:  
1\. We need to query level 1 multiple times consecutively, meaning we need a
automatic wordle solver. We used [ammario/wordle-
solver](https://github.com/ammario/wordle-solver) which is written in Go and
supports custom words list.  
2\. However, the solver cannot solve with 100% success rate, and there might
be other teams solving at the same time. That means the random numbers we get
are not necessarily consecutively generated by the pRNG.  
3\. That means our matching algorithm should not be a simple range matching,
but need to match the sub-list non-consecutively.

Finally we chose to query 10 times and match it against a stream of 10,000
numbers for each seed. We are able to successfully get the seed and solve
level 2 using only one guess.

**Psuedocode:**  
```python  
wordMap = word -> index  
conn = dial(server)  
t = UnixMilli(conn.TLS.Cert.NotBefore)  
rngs = []  
for 10 times:  
answer = guess level 1 until correct  
rng = wordMap[answer]  
rngs.append(rng)

for seed : t - 1000 ... t + 1000:  
prng = fromSeed(seed)  
rngsCopy = copy(rngs)  
for 10000 times:  
if rngsCopy is empty: break  
rng = prng.randN(len(wordMap))  
if rng == rngsCopy[0]:  
rngsCopy.popFront()  
if rngsCopy is empty:  
break # prng is aligned with the server

emojis = []  
for 5 times:  
emojis.append(emojiList[prngs.getN(len(emojiList))])  
guess level 2 with emojis # profit  
```

## Level 4

We were unable to solve this during the CTF, but with the hint later on that
level 3 is unsolvable, we quickly turned to other places and found where the
bug is.

The bug is a combination of two Go's quirk:  
\- non-blocking channel in `select` with a `default` path  
\- TLS library accepting connection before handshake completes

The former is referring to `server/level_server.go:129`. Normally the channel
will block the receving side if no one's sending anything on the otherside (or
there's nothing in channel for a buffered channel). In this case, the function
`sessionSearch` is waiting for an input from the channel `connErr`, but it is
in a select with a `default` path. That makes the `connErr` not actually
blocking if there's no goroutine sending to the channel, but instead enter the
`default` execution path (like how a `default` behave in a `switch`).

Then if we look back at which part of the code should send stuff to `connErr`
channel, we see the function `feedbackWriter`. The first thing this function
does is to write to the connection `conn`, and if the write fails it will send
the error to `connErr` channel. This is where the second quirk comes to play:
if we look at the implementation of `net.Conn.Write()` by `crypto/tls`
library's `*tls.Conn.Write()`, the first line we see is that it will call
`*tls.Conn.Handshake()`. This function essential "completes" the handshake in
a blocking way, meaning if the handshake is not complete, it will subsequently
block the write as well.

Therefore, we could block `feedbackWriter` by not completing the TLS handshake
on the client side, and thus making `sessionSearch` choosing the default path
and pass the check. There are multiple ways of blocking the TLS handshake,
like simply not sending anything after `ServerHello`. The easier way is to
block using `tls.Config.GetClientCertificate`, which will be called when the
server's requesting client cert:

```go  
tls.Dial("tcp", "pppordle.chal.pwni.ng:1341", &tls.Config{  
GetClientCertificate: func(info *tls.CertificateRequestInfo)
(*tls.Certificate, error) {  
time.Sleep(3 * time.Second)  
return nil, errors.New("just nope")  
},  
...  
})  
```

After that, we can connect to level 4 without actual authentication. Level 4
is quite easy to solve because the flag (wordle target) is fixed. All we need
to do is to enumerate all possible characters and record the ones that are
correct.

\---

Edit Jun 15, 2022: `feedbackWriter` calls `Write()` instead of `Read()`.
Thanks to @dumpx86.