Introduction
3DOGES2 is a service from the cryptography section of the PoliCTF 2015 worth
400 points. It is written in Go and does some crypto involving 3DES in CBC
mode. The source code was given and can be found in the appendix. A typical
communication with the service looks like the following:
< Welcome! Wanna talk with John? Follow the instructions to get a Secure™ Channel.
< dd94f8bb1f51a3d7637489cc712a3725dff8e69f7aa68789ec43903a7d45d03ede21bbdda8c34ce3f1dd7008c5a1ca2ec1a86d695d493dcb38d4c80142ddf64007c08c2bcc57fe9be01435f379f139ef6d5f8944fc0cdd15ef5c027aab9c5d43
<
< You surely know what to do with this:
< 0edef9812a0b7bd409cefd4b0732aa3df368cffa92d32b3c75f0640962a7a931
<
< Insert key:
> Cyclopropenylidene
< JOHN IS ANGRY.
("< " denotes data sent from the server to the client and "> " from the client
to the server)
General overview of the service
Let's take a look at what the service actually does. In the main function we
see some cryptographic key ede2Key, which is at least 16 bytes long. This
key is processed by some obfuscation function named muchSecurity and used
as a key for 3DES in CBC mode afterwards. See function NewTripleDOGESCipher
which makes a new des.TripleDESCipher and encrypt which uses
cipher.NewCBCEncrypter after padding the input with padSlice to 64 bit
blocks.
The IV for CBC mode is randomly generated and prepended to the ciphertext.
There are two hex strings in the example communication above. Both are
encryptions of some strings. The first one is the encryption of the string
"nWelcome! Wanna talk with John? [...]", the second one contains the encrypted
challenge key we have to enter after "Insert Key:".
Our goal is to somehow decrypt the second string. For this we need the
plaintext/ciphertext pair in the first string.
Analyzing muchSecurity
For reference, here is the mentioned obfuscation function:
func muchSecurity(key []byte) []byte {
var tripleDOGESKey []byte
secondKey := make([]byte, 16)
copy(secondKey, key)
for i := 8; i < 16; i++ {
// Let's be sure it is enough complex. Complex is good, a friend told us so.
key[i] = (secondKey[i] & 0x3c) | (suchSubstitution[(secondKey[i] >> 2) & 0x0f] << 6)
key[i] |= (key[i] >> 3) & 0x03
key[i] |= ((key[i] >> 4) ^ key[i]) << 7
}
// EDE2 is required.
tripleDOGESKey = append(tripleDOGESKey, key[:8]...)
return append(tripleDOGESKey, key[:16]...)
}
Lets look at the modifications of key first. The for loop will only change
bytes 8 to 16. There are no interdependencies between the bytes of the key,
because it only accesses key[i] or secondKey[i] (which is just a needlessly
preserved copy). Rather than analysing shifts, ands, xors we can build a
lookup table for each input byte. This is possible because of the missing
interdependencies. Actually we don't need a lookup table in our attack, but
only a set of the possible values. The 16 values are: {0x44, 0xc9, 0x8d, 0xd2,
0x96, 0x1b, 0x5f, 0x20, 0x64, 0xa9, 0xad, 0xf2, 0xf6, 0x7f}. See getK3Alphabet
in our attack implementation for how to obtain them in detail.
For the final result it initializes an empty key tripleDOGESKey which will
be returned. It consists of the first 8 bytes of key, followed by the first
16 bytes of key. As a result our tripleDOGESKey, which will be used
for 3DES, will have 24 bytes. Sadly this is way too much to do an exhaustive
search on the key space.
The 24 byte key in 3DES is split into 3 three keys of 8 bytes each. We will call
them K1, K2 and K3 respectively. Each key will be used for one DES operation.
As you might already noticed, K1 and K2 are the same (we append the first 8
bytes followed by the first 16 bytes of key). This reduces the key space,
but even an 8 byte key space would still be too much for our available
computational resources. Additionally K3 depends on K1/K2, but we can ignore
this dependency in our attack (see explanation below).
The attack
Our attack simply does exhaustive search on K3, which is the only thing we have
to obtain. To see why, we have to take a look at how 3DES works. 3DES encrypts
by using three DES operations in the following way:
ciphertext = E(K3, D(K2, E(K1, plaintext)))
E stands for encryption, D for decryption. Since K1 equals K2, we can simplify
this to:
ciphertext = E(K3, plaintext)
Yay! K1 and K2 are gone, we don't have to take care of them. 3DES with equal
K1 and K2 is basically DES with K3.
As explained in the previous section, we have only 16 values for each byte of
K3. If we want to do exhaustive search, we just have to try 16^8 = (2^4)^8 =
2^32 keys. This is still computational expensive, but feasible.
Lets remember how CBC works:
+-----+ +-----+
| m0 | | m1 | [...]
+--+--+ +--+--+
| |
| |
\|/ \|/
+---->(XOR) +---->(XOR) +---->
| | | | |
| | | | |
| \|/ | \|/ |
| +----+----+ | +----+----+ |
| | E(K, *) | | | E(K, *) | |
| +----+----+ | +----+----+ |
| | | | |
| | | | |
| \|/ | \|/ |
+-----+ | +--+--+ | +--+--+ |
| IV +----+ | c0 +----+ | c1 +----+ [...]
+-----+ +-----+ +-----+
m0, m1, ... are the message blocks, c0, c1, ... the corresponding cipher
blocks. IV is the initialization vector, which is prepended to the ciphertext.
E(K, *) encrypts * (which is some message block XORed with the previous
ciphertext block or IV) with key K, in this case using DES.
To do exhaustive search on K3 we simply try all possibilities by encrypting
(m0 XOR IV) with DES and check for equality with c0. If it is equal we have
found the key K3 with very high probability (there might be a negligible chance
of finding another key with this property, but we can safely ignore that).
In our case m0 is "nWelcome", IV is "xddx94xf8xbbx1fx51xa3xd7" and
c0 is "x63x74x89xccx71x2ax37x25".
Another way would be to encrypt the whole string with DES in CBC mode, but that
would require 11 DES operations per key instead of 1, thus being much slower.
We ran our implementation in the cloud on a machine with 16 cores. It took
about 1 hour to find the key. To solve the challenge we have to decrypt the
challenge key, which needs to be entered after "Insert Key:". This can be done
easily by using DES in CBC mode with our obtained key K3. After entering the
result of the decryption we obtain the flag:
< Insert key:
> Root superpassword
> Amazing! Flag: flag{!wow-such-flag-much-crypto-amazing}
See the appendix for the implementation of this attack.
Appendix
Our implementation of the attack:
package main
import (
"bytes"
"crypto/cipher"
"crypto/des"
"encoding/hex"
"fmt"
"log"
"os"
"runtime"
)
// Change these values to what you get from the communication with the service
const PLAINTEXT = "\nWelcome! Wanna talk with John? Follow the instructions to get a Secure™ Channel.\n"
const CIPHERTEXT = "dd0d5bd3b14540169c513b276e97948b0d34f390114ebd8aea6f85610beed8d0d8120b891504b52b70a172a72438be1554b59c6ef0fdfbcaabb663e8a6481122934edfd951acbfbe44c5c51f259d0f91669701f051be53858b462cbc27d13ba1"
const CHALLENGE = "0b3dfc6a7a4945a6fea0d63cb9c7e267823c0240a5c7db8730eec4ae7cd672e6"
var suchSubstitution = [32]byte{
0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 0, 0, 1, 1, 0, 1,
0, 0, 1, 1, 1, 0, 1, 0,
0, 1, 1, 0, 1, 1, 0, 0,
}
func main() {
runtime.GOMAXPROCS(20)
exhaustiveSearchK3()
}
func exhaustiveSearchK3() {
plaintext := []byte(PLAINTEXT)
ciphertext, err := hex.DecodeString(CIPHERTEXT)
if err != nil {
log.Fatal("Could not decode hex string for plaintext/ciphertext pair")
}
if len(plaintext) < des.BlockSize || len(ciphertext) < des.BlockSize {
log.Fatal("Plaintext/ciphertext pair must at least one DES block long")
}
progressChan := make(chan uint64)
resultChan := make(chan []byte)
alphabet := getK3Alphabet()
for i := 0; i < len(alphabet); i++ {
go exhaustiveSearchK3Worker(plaintext, ciphertext, alphabet, i, progressChan, resultChan)
}
cnt := uint64(0)
maxCnt := uint64(1 << 32)
for {
select {
case threadCnt := <-progressChan:
cnt += threadCnt
fmt.Printf("\rExhausted %f%% of the key space", float64(cnt)/float64(maxCnt)*100.0)
case key := <-resultChan:
fmt.Println("\nFound K3:", key)
fmt.Println("Challenge key:", decryptChallengeKey(key))
os.Exit(0)
}
}
}
func exhaustiveSearchK3Worker(plaintext []byte, ciphertext []byte, alphabet []byte, threadNo int, progressChan chan uint64, resultChan chan []byte) {
// IV, c0 and (m0 XOR IV)
iv := ciphertext[:des.BlockSize]
c0 := ciphertext[des.BlockSize : 2*des.BlockSize]
m0XorIV := make([]byte, des.BlockSize)
for i := 0; i < des.BlockSize; i++ {
m0XorIV[i] = plaintext[i] ^ iv[i]
}
cnt := uint64(0)
key := make([]byte, 8)
out := make([]byte, des.BlockSize)
// Parallization is done by assigning the first key byte a certain value
// based on the thread number.
key[0] = alphabet[threadNo]
// Nested loop to build all key permutations. It might be somewhat ugly,
// but is very fast/easy to implement and efficient. You know, at CTFs
// time counts.
for _, i1 := range alphabet {
key[1] = i1
for _, i2 := range alphabet {
key[2] = i2
for _, i3 := range alphabet {
// For performance reasons we don't sent our count after every
// DES operation. This position is a good tradeoff between
// seeing progress and not degrading performance
progressChan <- cnt
cnt = 0
key[3] = i3
for _, i4 := range alphabet {
key[4] = i4
for _, i5 := range alphabet {
key[5] = i5
for _, i6 := range alphabet {
key[6] = i6
for _, i7 := range alphabet {
key[7] = i7
// Finally encrypt (m0 XOR IV)
cipher, err := des.NewCipher(key)
if err != nil {
log.Fatal("Could not initialize cipher")
}
cipher.Encrypt(out, m0XorIV)
// And compare it with c0.
// If it matches we (probably) have found the key.
if bytes.Equal(out, c0) {
resultChan <- key
return
}
cnt++
}
}
}
}
}
}
}
}
func getK3Alphabet() []byte {
alphabet := make([]byte, 0)
duplicates := make(map[byte]bool)
for i := 0; i < 256; i++ {
character := muchSecuritySimple(byte(i))
if duplicates[character] {
continue
}
alphabet = append(alphabet, character)
duplicates[character] = true
}
return alphabet
}
func muchSecuritySimple(k byte) byte {
k = (k & 0x3c) | (suchSubstitution[(k>>2)&0x0f] << 6)
k |= (k >> 3) & 0x03
k |= ((k >> 4) ^ k) << 7
return k
}
func decryptChallengeKey(key []byte) string {
ciphertext, err := hex.DecodeString(CHALLENGE)
if err != nil {
log.Fatal("Can't decode ciphertext hex string to get challenge key")
}
if len(ciphertext)%des.BlockSize != 0 {
log.Fatal("Encrypted challenge key must be a multiple of DES block size")
}
desCipher, err := des.NewCipher(key)
if err != nil {
log.Fatal("Can not initialize DES cipher")
}
iv := ciphertext[:des.BlockSize]
ciphertext = ciphertext[des.BlockSize:]
plaintext := make([]byte, len(ciphertext))
mode := cipher.NewCBCDecrypter(desCipher, iv)
mode.CryptBlocks(plaintext, ciphertext)
return string(plaintext)
}
The given source code of 3DOGES2:
package main
import (
"crypto/des"
"crypto/cipher"
"crypto/rand"
"errors"
"io"
"fmt"
"log"
"net"
"os"
"bytes"
"encoding/hex"
)
const (
CONN_HOST = "0.0.0.0"
CONN_PORT = "8000"
CONN_TYPE = "tcp"
)
var suchSubstitution = [32]byte{
0, 1, 1, 0, 1, 0, 0, 1,
0, 1, 0, 0, 1, 1, 0, 1,
0, 0, 1, 1, 1, 0, 1, 0,
0, 1, 1, 0, 1, 1, 0, 0,
}
func main() {
ede2Key := []byte("<-- Some supersecret encryption key goes here -->")
// Create a new instance of our magic cipher. Wow.
tripleDOGESKey := muchSecurity(ede2Key)
muchCipher, err := NewTripleDOGESCipher(tripleDOGESKey)
if err != nil {
log.Fatal(err)
}
// Listen for incoming connections.
l, err := net.Listen(CONN_TYPE, CONN_HOST+":"+CONN_PORT)
if err != nil {
fmt.Println("Error listening:", err.Error())
os.Exit(1)
}
// Close the listener when the application closes.
defer l.Close()
fmt.Println("Listening on " + CONN_HOST + ":" + CONN_PORT)
for {
// Listen for an incoming connection.
conn, err := l.Accept()
if err != nil {
fmt.Println("Error accepting: ", err.Error())
os.Exit(1)
}
// Handle connections in a new goroutine.
go handleRequest(conn, muchCipher)
}
}
// Handles incoming requests.
func handleRequest(conn net.Conn, muchCipher cipher.Block) {
// Whoops.
superkey := []byte("<-- Some amazing program key goes here -->")
success := []byte("Amazing! Flag: <-- Some incredible flag goes here -->\n")
failure := []byte("JOHN IS ANGRY.\n")
// Print things.
plain := []byte("\nWelcome! Wanna talk with John? Follow the instructions to get a Secure™ Channel.\n")
cipherthings := make([]byte, 1024)
ciphertext := encrypt(plain, muchCipher)
hex.Encode(cipherthings, ciphertext)
plain = append(plain, cipherthings...)
conn.Write(plain)
plain = []byte("\n\nYou surely know what to do with this:\n")
cipherthings = make([]byte, 1024)
ciphertext = encrypt(superkey, muchCipher)
hex.Encode(cipherthings, ciphertext)
plain = append(plain, cipherthings...)
conn.Write(plain)
request := []byte("\n\nInsert key:\n")
conn.Write(request)
// Make a buffer to hold incoming data.
buf := make([]byte, 1024)
// Read the incoming connection into the buffer.
reqLen, err := conn.Read(buf)
if err != nil {
fmt.Println("Error reading:", err.Error())
}
// Send a response back to our friends.
if bytes.Contains(buf[:reqLen], superkey) {
conn.Write(success)
} else {
conn.Write(failure)
}
// Close the connection when you're done with it.
conn.Close()
}
func NewTripleDOGESCipher(tripleDOGESKey []byte) (cipher.Block, error) {
// I can't break it, so it is unbreakable. A friend told us so.
muchCipher, err := des.NewTripleDESCipher(tripleDOGESKey)
if err != nil {
return nil, err
}
return muchCipher, nil
}
func encrypt(text []byte, block cipher.Block) []byte {
if len(text)%des.BlockSize != 0 {
text = padSlice(text)
}
ciphertext := make([]byte, des.BlockSize+len(text))
iv := ciphertext[:des.BlockSize]
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
log.Fatal(err)
}
mode := cipher.NewCBCEncrypter(block, iv)
mode.CryptBlocks(ciphertext[des.BlockSize:], text)
return ciphertext
}
func decrypt(text []byte, block cipher.Block) []byte {
if len(text) < des.BlockSize {
log.Fatal(errors.New("ciphertext too short"))
}
iv := text[:des.BlockSize]
text = text[des.BlockSize:]
// CBC mode always works in whole blocks.
if len(text)%des.BlockSize != 0 {
text = padSlice(text)
}
mode := cipher.NewCBCDecrypter(block, iv)
dst := make([]byte, len(text))
mode.CryptBlocks(dst, text)
return dst
}
func muchSecurity(key []byte) []byte {
var tripleDOGESKey []byte
secondKey := make([]byte, 16)
copy(secondKey, key)
for i := 8; i < 16; i++ {
// Let's be sure it is enough complex. Complex is good, a friend told us so.
key[i] = (secondKey[i] & 0x3c) | (suchSubstitution[(secondKey[i] >> 2) & 0x0f] << 6)
key[i] |= (key[i] >> 3) & 0x03
key[i] |= ((key[i] >> 4) ^ key[i]) << 7
}
// EDE2 is required.
tripleDOGESKey = append(tripleDOGESKey, key[:8]...)
return append(tripleDOGESKey, key[:16]...)
}
func padSlice(src []byte) []byte {
// src must be a multiple of block size.
bs := des.BlockSize
mult := int((len(src) / bs) + 1)
leng := bs * mult
src_padded := make([]byte, leng)
copy(src_padded, src)
return src_padded
}