Crypto-Web – 300 pts (48 solves) – Chall author: Unknown
Seeding your PRNG function with the current time is never a good idea, although using nanosecond precision might make the possible space large enough to discourage brute-forcing. It will however, get completely ruined if you use a mask such as the owner of this website. S M H The only security here is the fact that the source code is written in Go. heh take that Go!
Check out write-ups by my teammates on K3RN3L4RMY.com
Exploration
Upon visiting the site we are greeted with a seemingly nonsensical hex string. We do note however, that the hex string changes (almost) every time we reload. Not too insightful, so time to read the source code. The following is the driver function that creates the hex string we find on the website.
func changer() {
ticker := time.NewTicker(time.Millisecond * 672).C
for range ticker {
rand.Seed(time.Now().UnixNano() & ^0x7FFFFFFFFEFFF000)
for i := 0; i < rand.Intn(32); i++ {
rand.Seed(rand.Int63())
}
var key []byte
var iv []byte
for i := 0; i < 32; i++ {
key = append(key, byte(rand.Intn(255)))
}
for i := 0; i < aes.BlockSize; i++ {
iv = append(iv, byte(rand.Intn(255)))
}
flagmu.Lock()
// The following is displayed on the website!
flag = encrypt(rawFlag, key, iv, aes.BlockSize)
flagmu.Unlock()
}
}
The ‘Ticker’ instance makes it so the code within its block is ran every ‘tick’, which in this case is set to be 672 milliseconds. That explains why the hex string changes every time we reload except for when we spam reload and catch the website twice within the same tick. Every tick, the server seeds Go’s random module and generates an IV and a key to encrypt the flag with. Let’s take a look at the server’s encrypt function.
// Apply standard PKCS5 padding
func PKCS5Padding(ciphertext []byte, blockSize int, after int) []byte {
padding := (blockSize - len(ciphertext)%blockSize)
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
// AES-CBC encryption
func encrypt(plaintext string, bKey []byte, bIV []byte, blockSize int) string {
bPlaintext := PKCS5Padding([]byte(plaintext), blockSize, len(plaintext))
block, err := aes.NewCipher(bKey)
if err != nil {
log.Println(err)
return ""
}
ciphertext := make([]byte, len(bPlaintext))
mode := cipher.NewCBCEncrypter(block, bIV)
mode.CryptBlocks(ciphertext, bPlaintext)
return hex.EncodeToString(ciphertext)
}
Quite standard, AES-CBC encryption with PKCS5 padding. So the encryption part of the server should be secure. Where is the vulnerability?
Exploitation
If the encryption of the server is secure, there is only one other part that might be vulnerable, the PRNG. All encryption parameters are generated from Go’s random module seeded as follows.
rand.Seed(time.Now().UnixNano() & ^0x7FFFFFFFFEFFF000)
Although seeding your PRNG with the current time is never a good idea, using the time up to nanosecond precision might make brute-forcing the correct seed a bit more difficult. Possibly even beyond CTF time, as opposed to polynomial time. However, the resulting value is maked by ‘^0x7FFFFFFFFEFFF000’, where ‘^’ actually inverts the hex value to just ‘0x01000FFF’. This mask is very small and limits the possible seed space to {0 … 4095, 16777216 … 16777216+4095} for a total of 8192 values. This is definitely brute-forceable, so let’s do exactly that! Note that the Go random module does not yield the same values as Python’s random module even with the same seed, so the solve script is written in Go too.
package main
import (
"fmt"
"time"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"math/rand"
)
var encflag = "7a0f231b18f46b055d02f7e9000bd1790d6da3c19f03e1677d2cdcc9ee8c4cb9"
func decrypt(ciptext string, bKey []byte, bIV []byte, blockSize int) []byte {
bCiptext, err := hex.DecodeString(ciptext)
block, err := aes.NewCipher(bKey)
if err != nil {
fmt.Println(err)
return bKey
}
plaintext := make([]byte, len(bCiptext))
mode := cipher.NewCBCDecrypter(block, bIV)
mode.CryptBlocks(plaintext, bCiptext)
return plaintext
}
func main() {
// First range
var start int64 = 0
for k := start; k < start+4096; k++ {
rand.Seed(k)
for i := 0; i < rand.Intn(32); i++ {
rand.Seed(rand.Int63())
}
var key []byte
var iv []byte
for i := 0; i < 32; i++ {
key = append(key, byte(rand.Intn(255)))
}
for i := 0; i < aes.BlockSize; i++ {
iv = append(iv, byte(rand.Intn(255)))
}
plainhex := decrypt(encflag, key, iv, aes.BlockSize)
if (string(plainhex[:5]) == "ractf") {
fmt.Println("Gottem!")
fmt.Println(string(plainhex))
break
}
}
// Second range
start = 16777216
for k := start; k < start+4096; k++ {
rand.Seed(k)
for i := 0; i < rand.Intn(32); i++ {
rand.Seed(rand.Int63())
}
var key []byte
var iv []byte
for i := 0; i < 32; i++ {
key = append(key, byte(rand.Intn(255)))
}
for i := 0; i < aes.BlockSize; i++ {
iv = append(iv, byte(rand.Intn(255)))
}
plainhex := decrypt(encflag, key, iv, aes.BlockSize)
if (string(plainhex[:5]) == "ractf") {
fmt.Println("Gottem!")
fmt.Println(string(plainhex))
break
}
}
}
Ta-da!
ractf{int3rEst1ng_M4sk_paTt3rn}