11
IMPLEMENTING AND ATTACKING CRYPTOGRAPHY

Image

A conversation about security isn’t complete without exploring cryptography. When organizations use cryptographic practices, they can help conserve the integrity, confidentiality, and authenticity of their information and systems alike. As a tool developer, you’d likely need to implement cryptographic features, perhaps for SSL/TLS communications, mutual authentication, symmetric-key cryptography, or password hashing. But developers often implement cryptographic functions insecurely, which means the offensive-minded can exploit these weaknesses to compromise sensitive, valuable data, such as social security or credit card numbers.

This chapter demonstrates various implementations of cryptography in Go and discusses common weaknesses you can exploit. Although we provide introductory information for the different cryptographic functions and code blocks, we’re not attempting to explore the nuances of cryptographic algorithms or their mathematical foundations. That, frankly, is far beyond our interest in (or knowledge of) cryptography. As we’ve stated before, don’t attempt anything in this chapter against resources or assets without explicit permission from the owner. We’re including these discussions for learning purposes, not to assist in illegal activities.

Reviewing Basic Cryptography Concepts

Before we explore crypto in Go, let’s discuss a few basic cryptography concepts. We’ll make this short to keep you from falling into a deep sleep.

First, encryption (for the purposes of maintaining confidentiality) is just one of the tasks of cryptography. Encryption, generally speaking, is a two-way function with which you can scramble data and subsequently unscramble it to retrieve the initial input. The process of encrypting data renders it meaningless until it’s been decrypted.

Both encryption and decryption involve passing the data and an accompanying key into a cryptographic function. The function outputs either the encrypted data (called ciphertext) or the original, readable data (called cleartext). Various algorithms exist to do this. Symmetric algorithms use the same key during the encryption and decryption processes, whereas asymmetric algorithms use different keys for encryption and decryption. You might use encryption to protect data in transit or to store sensitive information, such as credit card numbers, to decrypt later, perhaps for convenience during a future purchase or for fraud monitoring.

On the other hand, hashing is a one-way process for mathematically scrambling data. You can pass sensitive information into a hashing function to produce a fixed-length output. When you’re working with strong algorithms, such as those in the SHA-2 family, the probability that different inputs produce the same output is extremely low. That is, there is a low likelihood of a collision. Because they’re nonreversible, hashes are commonly used as an alternative to storing cleartext passwords in a database or to perform integrity checking to determine whether data has been changed. If you need to obscure or randomize the outputs for two identical inputs, you use a salt, which is a random value used to differentiate two identical inputs during the hashing process. Salts are common for password storage because they allow multiple users who coincidentally use identical passwords to still have different hash values.

Cryptography also provides a means for authenticating messages. A message authentication code (MAC) is the output produced from a special one-way cryptographic function. This function consumes the data itself, a secret key, and an initialization vector, and produces an output unlikely to have a collision. The sender of a message performs the function to generate a MAC and then includes the MAC as part of the message. The receiver locally calculates the MAC and compares it to the MAC they received. A match indicates that the sender has the correct secret key (that is, that the sender is authentic) and that the message was not changed (the integrity has been maintained).

There! Now you should know enough about cryptography to understand the contents of this chapter. Where necessary, we’ll discuss more specifics relevant to the given topic. Let’s start by looking at Go’s standard crypto library.

Understanding the Standard Crypto Library

The beautiful thing about implementing crypto in Go is that the majority of cryptographic features you’ll likely use are part of the standard library. Whereas other languages commonly rely on OpenSSL or other third-party libraries, Go’s crypto features are part of the official repositories. This makes implementing crypto relatively straightforward, as you won’t have to install clumsy dependencies that’ll pollute your development environment. There are two separate repositories.

The self-contained crypto package contains a variety of subpackages used for the most common cryptographic tasks and algorithms. For example, you could use the aes, des, and rc4 subpackages for implementing symmetric-key algorithms; the dsa and rsa subpackages for asymmetric encryption; and the md5, sha1, sha256, and sha512 subpackages for hashing. This is not an exhaustive list; additional subpackages exist for other crypto functions, as well.

In addition to the standard crypto package, Go has an official, extended package that contains a variety of supplementary crypto functionality: golang.org/x/crypto. The functionality within includes additional hashing algorithms, encryption ciphers, and utilities. For example, the package contains a bcrypt subpackage for bcrypt hashing (a better, more secure alternative for hashing passwords and sensitive data), acme/autocert for generating legitimate certificates, and SSH subpackages to facilitate communications over the SSH protocol.

The only real difference between the built-in crypto and supplementary golang.org/x/crypto packages is that the crypto package adheres to more stringent compatibility requirements. Also, if you wish to use any of the golang.org/x/crypto subpackages, you’ll first need to install the package by entering the following:

$ go get -u golang.org/x/crypto/bcrypt

For a complete listing of all the functionality and subpackages within the official Go crypto packages, check out the official documentation at https://golang.org/pkg/crypto/ and https://godoc.org/golang.org/x/crypto/.

The next sections delve into various crypto implementations. You’ll see how to use Go’s crypto functionality to do some nefarious things, such as crack password hashes, decrypt sensitive data by using a static key, and brute-force weak encryption ciphers. You’ll also use the functionality to create tools that use TLS to protect your in-transit communications, check the integrity and authenticity of data, and perform mutual authentication.

Exploring Hashing

Hashing, as we mentioned previously, is a one-way function used to produce a fixed-length, probabilistically unique output based on a variable-length input. You can’t reverse this hash value to retrieve the original input source. Hashes are often used to store information whose original, cleartext source won’t be needed for future processing or to track the integrity of data. For example, it’s bad practice and generally unnecessary to store the cleartext version of the password; instead, you’d store the hash (salted, ideally, to ensure randomness between duplicate values).

To demonstrate hashing in Go, we’ll look at two examples. The first attempts to crack a given MD5 or SHA-512 hash by using an offline dictionary attack. The second example demonstrates an implementation of bcrypt. As mentioned previously, bcrypt is a more secure algorithm for hashing sensitive data such as passwords. The algorithm also contains a feature that reduces its speed, making it harder to crack passwords.

Cracking an MD5 or SHA-256 Hash

Listing 11-1 shows the hash-cracking code. (All the code listings at the root location of / exist under the provided github repo https://github.com/blackhat-go/bhg/.) Since hashes aren’t directly reversible, the code instead tries to guess the cleartext value of the hash by generating its own hashes of common words, taken from a word list, and then comparing the resulting hash value with the hash you have in hand. If the two hashes match, you’ve likely guessed the cleartext value.

 var md5hash = "77f62e3524cd583d698d51fa24fdff4f"
   var sha256hash =
   "95a5e1547df73abdd4781b6c9e55f3377c15d08884b11738c2727dbd887d4ced"

   func main() {
       f, err := os.Open("wordlist.txt")
       if err != nil {
           log.Fatalln(err)
       }  
       defer f.Close()

     scanner := bufio.NewScanner(f)
       for scanner.Scan() {
           password := scanner.Text()
           hash := fmt.Sprintf("%x", md5.Sum([]byte(password)))
         if hash == md5hash {
               fmt.Printf("[+] Password found (MD5): %s
", password)
           }  

           hash = fmt.Sprintf("%x", sha256.Sum256([]byte(password)))
         if hash == sha256hash {
               fmt.Printf("[+] Password found (SHA-256): %s
", password)
           }  
       }  

       if err := scanner.Err(); err != nil {
           log.Fatalln(err)
       }  
   }

Listing 11-1: Cracking MD5 and SHA-256 hashes (/ch-11/hashes/main.go)

You start by defining two variables that hold the target hash values. One is an MD5 hash, and the other is a SHA-256. Imagine that you acquired these two hashes as part of post-exploitation and you’re trying to determine the inputs (the cleartext passwords) that produced them after being run through the hashing algorithm. You can often determine the algorithm by inspecting the length of the hash itself. When you find a hash that matches the target, you’ll know you have the correct input.

The list of inputs you’ll try exists in a dictionary file you’ll have created earlier. Alternatively, a Google search can help you find dictionary files for commonly used passwords. To check the MD5 hash, you open the dictionary file and read it, line by line, by creating a bufio.Scanner on the file descriptor . Each line consists of a single password value that you wish to check. You pass the current password value into a function named md5.Sum(input []byte) . This function produces the MD5 hash value as raw bytes, so you use the fmt.Sprintf() function with the format string %x to convert it to a hexadecimal string. After all, your md5hash variable consists of a hexadecimal string representation of the target hash. Converting your value ensures that you can then compare the target and calculated hash values . If these hashes match, the program displays a success message to stdout.

You perform a similar process to calculate and compare SHA-256 hashes. The implementation is fairly similar to the MD5 code. The only real difference is that the sha256 package contains additional functions to calculate various SHA hash lengths. Rather than calling sha256.Sum() (a function that doesn’t exist), you instead call sha256.Sum256(input []byte) to force the hash to be calculated using the SHA-256 algorithm. Much as you did in the MD5 example, you convert your raw bytes to a hex string and compare the SHA-256 hashes to see whether you have a match .

Implementing bcrypt

The next example shows how to use bcrypt to encrypt and authenticate passwords. Unlike SHA and MD5, bcrypt was designed for password hashing, making it a better option for application designers than the SHA or MD5 families. It includes a salt by default, as well as a cost factor that makes running the algorithm more resource-intensive. This cost factor controls the number of iterations of the internal crypto functions, increasing the time and effort needed to crack a password hash. Although the password can still be cracked using a dictionary or brute-force attack, the cost (in time) increases significantly, discouraging cracking activities during time-sensitive post-exploitation. It’s also possible to increase the cost over time to counter the advancement of computing power. This makes it adaptive to future cracking attacks.

Listing 11-2 creates a bcrypt hash and then validates whether a cleartext password matches a given bcrypt hash.

   import (
       "log"
       "os"
     "golang.org/x/crypto/bcrypt"
   )

 var storedHash = "$2a$10$Zs3ZwsjV/nF.KuvSUE.5WuwtDrK6UVXcBpQrH84V8q3Opg1yNdWLu"

   func main() {
       var password string
       if len(os.Args) != 2 {
           log.Fatalln("Usage: bcrypt password")
       }  
       password = os.Args[1]

     hash, err := bcrypt.GenerateFromPassword(
           []byte(password),
           bcrypt.DefaultCost,
       )
       if err != nil {
           log.Fatalln(err)
       }  
       log.Printf("hash = %s
", hash)

     err = bcrypt.CompareHashAndPassword([]byte(storedHash), []byte(password))
       if err != nil {
           log.Println("[!] Authentication failed")
           return
       }  
       log.Println("[+] Authentication successful")
   }

Listing 11-2: Comparing bcrypt hashes (/ch-11/bcrypt/main.go)

For most of the code samples in this book, we’ve omitted the package imports. We’ve included them in this example to explicitly show that you’re using the supplemental Go package, golang.org/x/crypto/bcrypt , because Go’s built-in crypto package doesn’t contain the bcrypt functionality. You then initialize a variable, storedHash , that holds a precomputed, encoded bcrypt hash. This is a contrived example; rather than wiring our sample code up to a database to get a value, we’ve opted to hardcode a value for demonstrative purposes. The variable could represent a value that you’ve found in a database row that stores user authentication information for a frontend web application, for instance.

Next, you’ll produce a bcrypt-encoded hash from a cleartext password value. The main function reads a password value as a command line argument and proceeds to call two separate bcrypt functions. The first function, bcrypt.GenerateFromPassword() , accepts two parameters: a byte slice representing the cleartext password and a cost value. In this example, you’ll pass the constant variable bcrypt.DefaultCost to use the package’s default cost, which is 10 at the time of this writing. The function returns the encoded hash value and any errors produced.

The second bcrypt function you call is bcrypt.CompareHashAndPassword() , which does the hash comparison for you behind the scenes. It accepts a bcrypt-encoded hash and a cleartext password as byte slices. The function parses the encoded hash to determine the cost and salt. It then uses these values with the cleartext password value to generate a bcrypt hash. If this resulting hash matches the hash extracted from the encoded storedHash value, you know the provided password matches what was used to create the storedHash.

This is the same method you used to perform your password cracking against SHA and MD5—run a given password through the hashing function and compare the result with the stored hash. Here, rather than explicitly comparing the resulting hashes as you did for SHA and MD5, you check whether bcrypt.CompareHashAndPassword() returns an error. If you see an error, you know the computed hashes, and therefore the passwords used to compute them, do not match.

The following are two sample program runs. The first shows the output for an incorrect password:

$ go run main.go someWrongPassword
2020/08/25 08:44:01 hash = $2a$10$YSSanGl8ye/NC7GDyLBLUO5gE/ng51l9TnaB1zTChWq5g9i09v0AC
2020/08/25 08:44:01 [!] Authentication failed

The second shows the output for the correct password:

$ go run main.go someC0mpl3xP@ssw0rd
2020/08/25 08:39:29 hash = $2a$10$XfeUk.wKeEePNAfjQ1juXe8RaM/9EC1XZmqaJ8MoJB29hZRyuNxz.
2020/08/25 08:39:29 [+] Authentication successful

Those of you with a keen eye for detail may notice that the hash value displayed for your successful authentication does not match the value you hardcoded for your storedHash variable. Recall, if you will, that your code is calling two separate functions. The GenerateFromPassword() function produces the encoded hash by using a random salt value. Given different salts, the same password will produce different resulting hashes. Hence the difference. The CompareHashAndPassword() function performs the hashing algorithm by using the same salt and cost as the stored hash, so the resulting hash is identical to the one in the storedHash variable.

Authenticating Messages

Let’s now turn our focus to message authentication. When exchanging messages, you need to validate both the integrity of data and the authenticity of the remote service to make sure that the data is authentic and hasn’t been tampered with. Was the message altered during transmission by an unauthorized source? Was the message sent by an authorized sender or was it forged by another entity?

You can address these questions by using Go’s crypto/hmac package, which implements the Keyed-Hash Message Authentication Code (HMAC) standard. HMAC is a cryptographic algorithm that allows us to check for message tampering and verify the identity of the source. It uses a hashing function and consumes a shared secret key, which only the parties authorized to produce valid messages or data should possess. An attacker who does not possess this shared secret cannot reasonably forge a valid HMAC value.

Implementing HMAC in some programming languages can be a little tricky. For example, some languages force you to manually compare the received and calculated hash values byte by byte. Developers may inadvertently introduce timing discrepancies in this process if their byte-by-byte comparison is aborted prematurely; an attacker can deduce the expected HMAC by measuring message-processing times. Additionally, developers will occasionally think HMACs (which consume a message and key) are the same as a hash of a secret key prepended to a message. However, the internal functionality of HMACs differs from that of a pure hashing function. By not explicitly using an HMAC, the developer is exposing the application to length-extension attacks, in which an attacker forges a message and valid MAC.

Luckily for us Gophers, the crypto/hmac package makes it fairly easy to implement HMAC functionality in a secure fashion. Let’s look at an implementation. Note that the following program is much simpler than a typical use case, which would likely involve some type of network communications and messaging. In most cases, you’d calculate the HMAC on HTTP request parameters or some other message transmitted over a network. In the example shown in Listing 11-3, we’re omitting the client-server communications and focusing solely on the HMAC functionality.

var key = []byte("some random key") 

func checkMAC(message, recvMAC []byte) bool { 
    mac := hmac.New(sha256.New, key) 
    mac.Write(message)
    calcMAC := mac.Sum(nil)

    return hmac.Equal(calcMAC, recvMAC)
}

func main() {
    // In real implementations, we'd read the message and HMAC value from network source
    message := []byte("The red eagle flies at 10:00") 
    mac, _ := hex.DecodeString("69d2c7b6fbbfcaeb72a3172f4662601d1f16acfb46339639ac8c10c8da64631d") 
    if checkMAC(message, mac) { 
        fmt.Println("EQUAL")
    } else {
        fmt.Println("NOT EQUAL")
    }  
}

Listing 11-3: Using HMAC for message authentication (/ch-11/hmac/main.go)

The program begins by defining the key you’ll use for your HMAC cryptographic function . You’re hardcoding the value here, but in a real implementation, this key would be adequately protected and random. It would also be shared between the endpoints, meaning both the message sender and receiver are using this same key value. Since you aren’t implementing full client-server functionality here, you’ll use this variable as if it were adequately shared.

Next, you define a function, checkMAC() , that accepts a message and the received HMAC as parameters. The message receiver would call this function to check whether the MAC value they received matches the value they calculated locally. First, you call hmac.New() , passing to it sha256.New, which is a function that returns a hash.Hash instance, and the shared secret key. In this case, the hmac.New() function initializes your HMAC by using the SHA-256 algorithm and your secret key, and assigns the result to a variable named mac. You then use this variable to calculate the HMAC hash value, as you did in the earlier hashing examples. Here, you call mac.Write(message) and mac.Sum(nil), respectively. The result is your locally calculated HMAC, stored in a variable named calcMAC.

The next step is to evaluate whether your locally calculated HMAC value is equal to the HMAC value you received. To do this in a secure manner, you call hmac.Equal(calcMAC, recvMAC) . A lot of developers would be inclined to compare the byte slices by calling bytes.Compare(calcMAC, recvMAC). The problem is, bytes.Compare() performs a lexicographical comparison, walking and comparing each element of the given slices until it finds a difference or reaches the end of a slice. The time it takes to complete this comparison will vary based on whether bytes.Compare() encounters a difference on the first element, the last, or somewhere in between. An attacker could measure this variation in time to determine the expected HMAC value and forge a request that’s processed legitimately. The hmac.Equal() function solves this problem by comparing the slices in a way that produces nearly constant measurable times. It doesn’t matter where the function finds a difference, because the processing times will vary insignificantly, producing no obvious or perceptible pattern.

The main() function simulates the process of receiving a message from a client. If you were really receiving a message, you’d have to read and parse the HMAC and message values from the transmission. Since this is just a simulation, you instead hardcode the received message and the received HMAC , decoding the HMAC hex string so it’s represented as a []byte. You use an if statement to call your checkMAC() function , passing it your received message and HMAC. As detailed previously, your checkMAC() function computes an HMAC by using the received message and the shared secret key and returns a bool value for whether the received HMAC and calculated HMAC match.

Although the HMAC does provide both authenticity and integrity assurance, it doesn’t ensure confidentiality. You can’t know for sure that the message itself wasn’t seen by unauthorized resources. The next section addresses this concern by exploring and implementing various types of encryption.

Encrypting Data

Encryption is likely the most well-known cryptographic concept. After all, privacy and data protection have garnered significant news coverage due to high-profile data breaches, often resulting from organizations storing user passwords and other sensitive data in unencrypted formats. Even without the media attention, encryption should spark the interest of black hats and developers alike. After all, understanding the basic process and implementation can be the difference between a lucrative data breach and a frustrating disruption to an attack kill chain. The following section presents the varying forms of encryption, including useful applications and use cases for each.

Symmetric-Key Encryption

Your journey into encryption will start with what is arguably its most straightforward form—symmetric-key encryption. In this form, both the encryption and decryption functions use the same secret key. Go makes symmetric cryptography pretty straightforward, because it supports most common algorithms in its default or extended packages.

For the sake of brevity, we’ll limit our discussion of symmetric-key encryption to a single, practical example. Let’s imagine you’ve breached an organization. You’ve performed the necessary privilege escalation, lateral movement, and network recon to gain access to an e-commerce web server and the backend database. The database contains financial transactions; however, the credit card number used in those transactions is obviously encrypted. You inspect the application source code on the web server and determine that the organization is using the Advanced Encryption Standard (AES) encryption algorithm. AES supports multiple operating modes, each with slightly different considerations and implementation details. The modes are not interchangeable; the mode used for decryption must be identical to that used for encryption.

In this scenario, let’s say you’ve determined that the application is using AES in Cipher Block Chaining (CBC) mode. So, let’s write a function that decrypts these credit cards (Listing 11-4). Assume that the symmetric key was hardcoded in the application or set statically in a configuration file. As you go through this example, keep in mind that you’ll need to tweak this implementation for other algorithms or ciphers, but it’s a good starting place.

func unpad(buf []byte) []byte { 
    // Assume valid length and padding. Should add checks
    padding := int(buf[len(buf)-1])
    return buf[:len(buf)-padding]
}

func decrypt(ciphertext, key []byte) ([]byte, error) { 
    var (
        plaintext []byte
        iv        []byte
        block     cipher.Block
        mode      cipher.BlockMode
        err       error
    )
       
    if len(ciphertext) < aes.BlockSize { 
        return nil, errors.New("Invalid ciphertext length: too short")
    }

    if len(ciphertext)%aes.BlockSize != 0 { 
        return nil, errors.New("Invalid ciphertext length: not a multiple of blocksize")
    }

    iv = ciphertext[:aes.BlockSize] 
    ciphertext = ciphertext[aes.BlockSize:]

    if block, err = aes.NewCipher(key); err != nil { 
        return nil, err
    }

    mode = cipher.NewCBCDecrypter(block, iv) 
    plaintext = make([]byte, len(ciphertext))
    mode.CryptBlocks(plaintext, ciphertext) 
    plaintext = unpad(plaintext) 

    return plaintext, nil
}

Listing 11-4: AES padding and decryption (/ch-11/aes/main.go)

The code defines two functions: unpad() and decrypt(). The unpad() function is a utility function scraped together to handle the removal of padding data after decryption. This is a necessary step, but beyond the scope of this discussion. Do some research on Public Key Cryptography Standards (PKCS) #7 padding for more information. It’s a relevant topic for AES, as it’s used to ensure that our data has proper block alignment. For this example, just know that you’ll need the function later to clean up your data. The function itself assumes some facts that you’d want to explicitly validate in a real-world scenario. Specifically, you’d want to confirm that the value of the padding bytes is valid, that the slice offsets are valid, and that the result is of appropriate length.

The most interesting logic exists within the decrypt() function , which takes two byte slices: the ciphertext you need to decrypt and the symmetric key you’ll use to do it. The function performs some validation to confirm that the ciphertext is at least as long as your block size . This is a necessary step, because CBC mode encryption uses an initialization vector (IV) for randomness. This IV, like a salt value for password hashing, doesn’t need to remain secret. The IV, which is the same length as a single AES block, is prepended onto your ciphertext during encryption. If the ciphertext length is less than the expected block size, you know that you either have an issue with the cipher text or are missing the IV. You also check whether the ciphertext length is a multiple of the AES block size . If it’s not, decryption will fail spectacularly, because CBC mode expects the ciphertext length to be a multiple of the block size.

Once you’ve completed your validation checks, you can proceed to decrypt the ciphertext. As mentioned previously, the IV is prepended to the ciphertext, so the first thing you do is extract the IV from the ciphertext . You use the aes.BlockSize constant to retrieve the IV and then redefine your ciphertext variable to the remainder of your ciphertext via ciphertext = [aes.BlockSize:]. You now have your encrypted data separate from your IV.

Next, you call aes.NewCipher(), passing it your symmetric-key value . This initializes your AES block mode cipher, assigning it to a variable named block. You then instruct your AES cipher to operate in CBC mode by calling cipher.NewCBCDecryptor(block, iv) . You assign the result to a variable named mode. (The crypto/cipher package contains additional initialization functions for other AES modes, but you’re using only CBC decryption here.) You then issue a call to mode.CryptBlocks(plaintext, ciphertext) to decrypt the contents of ciphertext and store the result in the plaintext byte slice. Lastly, you remove your PKCS #7 padding by calling your unpad() utility function. You return the result. If all went well, this should be the plaintext value of the credit card number.

A sample run of the program produces the expected result:

$ go run main.go
key        = aca2d6b47cb5c04beafc3e483b296b20d07c32db16029a52808fde98786646c8
ciphertext = 7ff4a8272d6b60f1e7cfc5d8f5bcd047395e31e5fc83d062716082010f637c8f21150eabace62
--snip--
plaintext  = 4321123456789090

Notice that you didn’t define a main() function in this sample code. Why not? Well, decrypting data in unfamiliar environments has a variety of potential nuances and variations. Are the ciphertext and key values encoded or raw binary? If they’re encoded, are they a hex string or Base64? Is the data locally accessible, or do you need to extract it from a data source or interact with a hardware security module, for example? The point is, decryption is rarely a copy-and-paste endeavor and often requires some level of understanding of algorithms, modes, database interaction, and data encoding. For this reason, we’ve chosen to lead you to the answer with the expectation that you’ll inevitably have to figure it out when the time is right.

Knowing just a little bit about symmetric-key encryption can make your penetrations tests much more successful. For example, in our experience pilfering client source-code repositories, we’ve found that people often use the AES encryption algorithm, either in CBC or Electronic Codebook (ECB) mode. ECB mode has some inherent weaknesses and CBC isn’t any better, if implemented incorrectly. Crypto can be hard to understand, so often developers assume that all crypto ciphers and modes are equally effective and are ignorant of their subtleties. Although we don’t consider ourselves cryptographers, we know just enough to implement crypto securely in Go—and to exploit other people’s deficient implementations.

Although symmetric-key encryption is faster than asymmetric cryptography, it suffers from inherent key-management challenges. After all, to use it, you must distribute the same key to any and all systems or applications that perform the encryption or decryption functions on the data. You must distribute the key securely, often following strict processes and auditing requirements. Also, relying solely on symmetric-key cryptography prevents arbitrary clients from, for example, establishing encrypted communications with other nodes. There isn’t a good way to negotiate the secret key, nor are there authentication or integrity assurances for many common algorithms and modes.1 That means anyone, whether authorized or malicious, who obtains the secret key can proceed to use it.

This is where asymmetric cryptography can be of use.

Asymmetric Cryptography

Many of the problems associated with symmetric-key encryption are solved by asymmetric (or public-key) cryptography, which uses two separate but mathematically related keys. One is available to the public and the other is kept private. Data encrypted by the private key can be decrypted only by the public key, and data encrypted by the public key can be decrypted only by the private key. If the private key is protected properly and kept, well, private, then data encrypted with the public key remains confidential, since you need the closely guarded private key to decrypt it. Not only that, but you could use the private key to authenticate a user. The user could use the private key to sign messages, for example, which the public could decrypt using the public key.

So, you might be asking, “What’s the catch? If public-key cryptography provides all these assurances, why do we even have symmetric-key cryptography?” Good question, you! The problem with public-key encryption is its speed; it’s a lot slower than its symmetric counterpart. To get the best of both worlds (and avoid the worst), you’ll often find organizations using a hybrid approach: they’ll use asymmetric crypto for the initial communications negotiation, establishing an encrypted channel through which they create and exchange a symmetric key (often called a session key). Because the session key is fairly small, using public-key crypto for this process requires little overhead. Both the client and server then have a copy of the session key, which they use to make future communications faster.

Let’s look at a couple of common use cases for public-key crypto. Specifically, we’ll look at encryption, signature validation, and mutual authentication.

Encryption and Signature Validation

For this first example, you’ll use public-key crypto to encrypt and decrypt a message. You’ll also create the logic to sign a message and validate that signature. For simplicity, you’ll include all of this logic in a single main() function. This is meant to show you the core functionality and logic so that you can implement it. In a real-world scenario, the process is a little more complex, since you’re likely to have two remote nodes communicating with each other. These nodes would have to exchange public keys. Fortunately, this exchange process doesn’t require the same security assurances as exchanging symmetric keys. Recall that any data encrypted with the public key can be decrypted only by the related private key. So, even if you perform a man-in-the-middle attack to intercept the public-key exchange and future communications, you won’t be able to decrypt any of the data encrypted by the same public key. Only the private key can decrypt it.

Let’s take a look at the implementation shown in Listing 11-5. We’ll elaborate on the logic and cryptographic functionality as we review the example.

func main() {
    var (
        err                                              error
        privateKey                                       *rsa.PrivateKey
        publicKey                                        *rsa.PublicKey
        message, plaintext, ciphertext, signature, label []byte
    )  

    if privateKey, err = rsa.GenerateKey(rand.Reader, 2048); err != nil {
        log.Fatalln(err)
    }  
    publicKey = &privateKey.PublicKey 

    label = []byte("")
    message = []byte("Some super secret message, maybe a session key even")
    ciphertext, err = rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, message, label) 
    if err != nil {
        log.Fatalln(err)
    }
    fmt.Printf("Ciphertext: %x
", ciphertext)

    plaintext, err = rsa.DecryptOAEP(sha256.New(), rand.Reader, privateKey, ciphertext, label) 
    if err != nil {
        log.Fatalln(err)
    }  
    fmt.Printf("Plaintext: %s
", plaintext)

    h := sha256.New()
    h.Write(message)
    signature, err = rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, h.Sum(nil), nil) 
    if err != nil {
        log.Fatalln(err)
    }  
    fmt.Printf("Signature: %x
", signature)

    err = rsa.VerifyPSS(publicKey, crypto.SHA256, h.Sum(nil), signature, nil)
    if err != nil {
        log.Fatalln(err)
    }  
    fmt.Println("Signature verified")
}

Listing 11-5: Asymmetric, or public-key, encryption (/ch-11/public-key/main.go/)

The program demonstrates two separate but related public-key crypto functions: encryption/decryption and message signing. You first generate a public/private key pair by calling the rsa.GenerateKey() function . You supply a random reader and a key length as input parameters to the function. Assuming the random reader and key lengths are adequate to generate a key, the result is an *rsa.PrivateKey instance that contains a field whose value is the public key. You now have a working key pair. You assign the public key to its own variable for the sake of convenience .

This program generates this key pair every time it’s run. In most circumstances, such as SSH communications, you’ll generate the key pair a single time, and then save and store the keys to disk. The private key will be kept secure, and the public key will be distributed to endpoints. We’re skipping key distribution, protection, and management here, and focusing only on the cryptographic functions.

Now that you’ve created the keys, you can start using them for encryption. You do so by calling the function rsa.EncryptOAEP() , which accepts a hashing function, a reader to use for padding and randomness, your public key, the message you wish to encrypt, and an optional label. This function returns an error (if the inputs cause the algorithm to fail) and our ciphertext. You can then pass the same hashing function, a reader, your private key, your ciphertext, and a label into the function rsa.DecryptOAEP() . The function decrypts the ciphertext by using your private key and returns the cleartext result.

Notice that you’re encrypting the message with the public key. This ensures that only the holder of the private key will have the ability to decrypt the data. Next you create a digital signature by calling rsa.SignPSS() . You pass to it, again, a random reader, your private key, the hashing function you’re using, the hash value of the message, and a nil value representing additional options. The function returns any errors and the resulting signature value. Much like human DNA or fingerprints, this signature uniquely identifies the identity of the signer (that is, the private key). Anybody holding the public key can validate the signature to not only determine the authenticity of the signature but also validate the integrity of the message. To validate the signature, you pass the public key, hash function, hash value, signature, and additional options to rsa.VerifyPSS() . Notice that in this case you’re passing the public key, not the private key, into this function. Endpoints wishing to validate the signature won’t have access to the private key, nor will validation succeed if you input the wrong key value. The rsa.VerifyPSS() function returns nil when the signature is valid and an error when it’s invalid.

Here is a sample run of the program. It behaves as expected, encrypting the message by using a public key, decrypting it by using a private key, and validating the signature:

$ go run main.go
Ciphertext: a9da77a0610bc2e5329bc324361b480ba042e09ef58e4d8eb106c8fc0b5
--snip--
Plaintext: Some super secret message, maybe a session key even
Signature: 68941bf95bbc12edc12be369f3fd0463497a1220d9a6ab741cf9223c6793
--snip--
Signature verified

Next up, let’s look at another application of public-key cryptography: mutual authentication.

Mutual Authentication

Mutual authentication is the process by which a client and server authenticate each other. They do this with public-key cryptography; both the client and server generate public/private key pairs, exchange public keys, and use the public keys to validate the authenticity and identity of the other endpoint. To accomplish this feat, both the client and server must do some legwork to set up the authorization, explicitly defining the public key value with which they intend to validate the other. The downside to this process is the administrative overhead of having to create unique key pairs for every single node and ensuring that the server and the client nodes have the appropriate data to proceed properly.

To begin, you’ll knock out the administrative tasks of creating key pairs. You’ll store the public keys as self-signed, PEM-encoded certificates. Let’s use the openssl utility to create these files. On your server, you’ll create the server’s private key and certificate by entering the following:

$ openssl req -nodes -x509 -newkey rsa:4096 -keyout serverKey.pem -out serverCrt.pem -days 365

The openssl command will prompt you for various inputs, to which you can supply arbitrary values for this example. The command creates two files: serverKey.pem and serverCrt.pem. The file serverKey.pem contains your private key, and you should protect it. The serverCrt.pem file contains the server’s public key, which you’ll distribute to each of your connecting clients.

For every connecting client, you’ll run a command similar to the preceding one:

$ openssl req -nodes -x509 -newkey rsa:4096 -keyout clientKey.pem -out clientCrt.pem -days 365

This command also generates two files: clientKey.pem and clientCrt.pem. Much as with the server output, you should protect the client’s private key. The clientCrt.pem certificate file will be transferred to your server and loaded by your program. This will allow you to configure and identify the client as an authorized endpoint. You’ll have to create, transfer, and configure a certificate for each additional client so that the server can identify and explicitly authorize them.

In Listing 11-6, you set up an HTTPS server that requires a client to provide a legitimate, authorized certificate.

func helloHandler(w http.ResponseWriter, r *http.Request) { 
    fmt.Printf("Hello: %s
", r.TLS.PeerCertificates[0].Subject.CommonName) 
    fmt.Fprint(w, "Authentication successful")
}

func main() {
    var (
        err        error
        clientCert []byte
        pool       *x509.CertPool
        tlsConf    *tls.Config
        server     *http.Server
    )  

    http.HandleFunc("/hello", helloHandler)

    if clientCert, err = ioutil.ReadFile("../client/clientCrt.pem"); err != nil {
        log.Fatalln(err)
    }  
    pool = x509.NewCertPool()
    pool.AppendCertsFromPEM(clientCert) 

    tlsConf = &tls.Config{ 
        ClientCAs:  pool,
        ClientAuth: tls.RequireAndVerifyClientCert,
    }  
    tlsConf.BuildNameToCertificate() 

    server = &http.Server{
        Addr:      ":9443",
        TLSConfig: tlsConf, 
    }  
    log.Fatalln(server.ListenAndServeTLS("serverCrt.pem", "serverKey.pem"))
}

Listing 11-6: Setting up a mutual authentication server (/ch-11/mutual-auth/cmd/server/main.go)

Outside the main() function, the program defines a helloHandler() function . As we discussed way back in Chapters 3 and 4, the handler function accepts an http.ResponseWriter instance and the http.Request itself. This handler is pretty boring. It logs the common name of the client certificate received . The common name is accessed by inspecting the http.Request’s TLS field and drilling down into the certificate PeerCertificates data. The handler function also sends the client a message indicating that authentication was successful.

But how do you define which clients are authorized, and how do you authenticate them? The process is fairly painless. You first read the client’s certificate from the PEM file the client created previously . Because it’s possible to have more than one authorized client certificate, you create a certificate pool and call pool.AppendCertsFromPEM(clientCert) to add the client certificate to your pool . You perform this step for each additional client you wish to authenticate.

Next, you create your TLS configuration. You explicitly set the ClientCAs field to your pool and configure ClientAuth to tls.RequireAndVerifyClientCert . This configuration defines your pool of authorized clients and requires clients to properly identify themselves before they’ll be allowed to proceed. You issue a call to tlsConf.BuildNameToCertificate() so that the client’s common and subject alternate names—the domain names for which the certificate was generated—will properly map to their given certificate . You define your HTTP server, explicitly setting your custom configuration , and start the server by calling server.ListenAndServeTLS(), passing to it the server certificate and private-key files you created previously . Note that you don’t use the client’s private-key file anywhere in the server code. As we’ve said before, the private key remains private; your server will be able to identify and authorize clients by using only the client’s public key. This is the brilliance of public-key crypto.

You can validate your server by using curl. If you generate and supply a bogus, unauthorized client certificate and key, you’ll be greeted with a verbose message telling you so:

$ curl -ik -X GET --cert badCrt.pem --key badKey.pem 
  https://server.blackhat-go.local:9443/hello
curl: (35) gnutls_handshake() failed: Certificate is bad

You’ll also get a more verbose message on the server, something like this:

http: TLS handshake error from 127.0.0.1:61682: remote error: tls: unknown certificate authority

On the flip side, if you supply the valid certificate and the key that matches the certificate configured in the server pool, you’ll enjoy a small moment of glory as it successfully authenticates:

$ curl -ik -X GET --cert clientCrt.pem --key clientKey.pem 
  https://server.blackhat-go.local:9443/hello
HTTP/1.1 200 OK
Date: Fri, 09 Oct 2020 16:55:52 GMT
Content-Length: 25
Content-Type: text/plain; charset=utf-8

Authentication successful

This message tells you the server works as expected.

Now, let’s have a look at a client (Listing 11-7). You can run the client on either the same system as the server or a different one. If it’s on a different system, you’ll need to transfer clientCrt.pem to the server and serverCrt.pem to the client.

func main() {
    var (
        err              error
        cert             tls.Certificate
        serverCert, body []byte
        pool             *x509.CertPool
        tlsConf          *tls.Config
        transport        *http.Transport
        client           *http.Client
        resp             *http.Response
    )  

    if cert, err = tls.LoadX509KeyPair("clientCrt.pem", "clientKey.pem"); err != nil { 
        log.Fatalln(err)
    }  

    if serverCert, err = ioutil.ReadFile("../server/serverCrt.pem"); err != nil { 
        log.Fatalln(err)
    }  

    pool = x509.NewCertPool()
    pool.AppendCertsFromPEM(serverCert) 

    tlsConf = &tls.Config{ 
        Certificates: []tls.Certificate{cert},
        RootCAs:      pool,
    }  
    tlsConf.BuildNameToCertificate()

    transport = &http.Transport{ 
        TLSClientConfig: tlsConf,
    }  
    client = &http.Client{ 
        Transport: transport,
    }  

    if resp, err = client.Get("https://server.blackhat-go.local:9443/hello"); err != nil { 
        log.Fatalln(err)
    }  
    if body, err = ioutil.ReadAll(resp.Body); err != nil { 
        log.Fatalln(err)
    }  
    defer resp.Body.Close()

    fmt.Printf("Success: %s
", body)
}

Listing 11-7: The mutual authentication client (/ch-11/mutual-auth/cmd/client/main.go)

A lot of the certificate preparation and configuration will look similar to what you did in the server code: creating a pool of certificates and preparing subject and common names. Since you won’t be using the client certificate and key as a server, you instead call tls.LoadX509KeyPair("clientCrt.pem", "clientKey.pem") to load them for use later . You also read the server certificate, adding it to the pool of certificates you wish to allow . You then use the pool and client certificates to build your TLS configuration , and call tlsConf.BuildNameToCertificate() to bind domain names to their respective certificates .

Since you’re creating an HTTP client, you have to define a transport , correlating it with your TLS configuration. You can then use the transport instance to create an http.Client struct . As we discussed in Chapters 3 and 4, you can use this client to issue an HTTP GET request via client.Get("https://server.blackhat-go.local:9443/hello") .

All the magic happens behind the scenes at this point. Mutual authentication is performed—the client and the server mutually authenticate each other. If authentication fails, the program returns an error and exits. Otherwise, you read the HTTP response body and display it to stdout . Running your client code produces the expected result, specifically, that there were no errors thrown and that authentication succeeds:

$ go run main.go
Success: Authentication successful

Your server output is shown next. Recall that you configured the server to log a hello message to standard output. This message contains the common name of the connecting client, extracted from the certificate:

$ go run main.go
Hello: client.blackhat-go.local

You now have a functional sample of mutual authentication. To further enhance your understanding, we encourage you to tweak the previous examples so they work over TCP sockets.

In the next section, you’ll dedicate your efforts to a more devious purpose: brute-forcing RC2 encryption cipher symmetric keys.

Brute-Forcing RC2

RC2 is a symmetric-key block cipher created by Ron Rivest in 1987. Prompted by recommendations from the government, the designers used a 40-bit encryption key, which made the cipher weak enough that the US government could brute-force the key and decrypt communications. It provided ample confidentiality for most communications but allowed the government to peep into chatter with foreign entities, for example. Of course, back in the 1980s, brute-forcing the key required significant computing power, and only well-funded nation states or specialty organizations had the means to decrypt it in a reasonable amount of time. Fast-forward 30 years; today, the common home computer can brute-force a 40-bit key in a few days or weeks.

So, what the heck, let’s brute force a 40-bit key.

Getting Started

Before we dive into the code, let’s set the stage. First of all, neither the standard nor extended Go crypto libraries have an RC2 package intended for public consumption. However, there’s an internal Go package for it. You can’t import internal packages directly in external programs, so you’ll have to find another way to use it.

Second, to keep things simple, you’ll make some assumptions about the data that you normally wouldn’t want to make. Specifically, you’ll assume that the length of your cleartext data is a multiple of the RC2 block size (8 bytes) to avoid clouding your logic with administrative tasks like handling PKCS #5 padding. Handling the padding is similar to what you did with AES previously in this chapter (see Listing 11-4), but you’d need to be more diligent in validating the contents to maintain the integrity of the data you’ll be working with. You’ll also assume that your ciphertext is an encrypted credit card number. You’ll check the potential keys by validating the resulting plaintext data. In this case, validating the data involves making sure the text is numeric and then subjecting it to a Luhn check, which is a method of validating credit card numbers and other sensitive data.

Next, you’ll assume you were able to determine—perhaps from pilfering filesystem data or source code—that the data is encrypted using a 40-bit key in ECB mode with no initialization vector. RC2 supports variable-length keys and, since it’s a block cipher, can operate in different modes. In ECB mode, which is the simplest mode, blocks of data are encrypted independently of other blocks. This will make your logic a little more straightforward. Lastly, although you can crack the key in a nonconcurrent implementation, if you so choose, a concurrent implementation will be far better performing. Rather than building this thing iteratively, showing first a nonconcurrent version followed by a concurrent one, we’ll go straight for the concurrent build.

Now you’ll install a couple of prerequisites. First, retrieve the official RC2 Go implementation from https://github.com/golang/crypto/blob/master/pkcs12/internal/rc2/rc2.go. You’ll need to install this in your local workspace so that you can import it into your brute-forcer. As we mentioned earlier, the package is an internal package, meaning that, by default, outside packages can’t import and use it. This is a little hacky, but it’ll prevent you from having to use a third-party implementation or—shudder—writing your own RC2 cipher code. If you copy it into your workspace, the non-exported functions and types become part of your development package, which makes them accessible.

Let’s also install a package that you’ll use to perform the Luhn check:

$ go get github.com/joeljunstrom/go-luhn

A Luhn check calculates checksums on credit card numbers or other identification data to determine whether they’re valid. You’ll use the existing package for this. It’s well-documented and it’ll save you from re-creating the wheel.

Now you can write your code. You’ll need to iterate through every combination of the entire key space (40-bits), decrypting your ciphertext with each key, and then validating your result by making sure it both consists of only numeric characters and passes a Luhn check. You’ll use a producer/consumer model to manage the work—the producer will push a key to a channel and the consumers will read the key from the channel and execute accordingly. The work itself will be a single key value. When you find a key that produces properly validated plaintext (indicating you found a credit card number), you’ll signal each of the goroutines to stop their work.

One of the interesting challenges of this problem is how to iterate the key space. In our solution, you iterate it using a for loop, traversing the key space represented as uint64 values. The challenge, as you’ll see, is that uint64 occupies 64 bits of space in memory. So, converting from a uint64 to a 40-bit (5-byte) []byte RC2 key requires that you crop off 24 bits (3 bytes) of unnecessary data. Hopefully, this process becomes clear once you’ve looked at the code. We’ll take it slow, breaking down sections of the program and working through them one by one. Listing 11-8 begins the program.

   import (
       "crypto/cipher"
       "encoding/binary"
       "encoding/hex"
       "fmt"
       "log"
       "regexp"
       "sync"

      luhn "github.com/joeljunstrom/go-luhn"

      "github.com/bhg/ch-11/rc2-brute/rc2"
   )

 var numeric = regexp.MustCompile(`^d{8}$`)

 type CryptoData struct {
       block cipher.Block
       key   []byte
   }

Listing 11-8: Importing the RC2 brute-force type (/ch-11/rc2-brute/main.go)

We’ve included the import statements here to draw attention to the inclusion of the third-party go-luhn package , as well as the inclusion of the rc2 package you cloned from the internal Go repository. You also compile a regular expression that you’ll use to check whether the resulting plaintext block is 8 bytes of numeric data.

Note that you’re checking 8 bytes of data and not 16 bytes, which is the length of your credit card number. You’re checking 8 bytes because that’s the length of an RC2 block. You’ll be decrypting your ciphertext block by block, so you can check the first block you decrypt to see whether it’s numeric. If the 8 bytes of the block aren’t all numeric, you can confidently assume that you aren’t dealing with a credit card number and can skip the decryption of the second block of ciphertext altogether. This minor performance improvement will significantly reduce the time it takes to execute millions of times over.

Lastly, you define a type named CryptoData that you’ll use to store your key and a cipher.Block. You’ll use this struct to define units of work, which producers will create and consumers will act upon.

Producing Work

Let’s look at the producer function (Listing 11-9). You place this function after your type definitions in the previous code listing.

 func generate(start, stop uint64, out chan <- *CryptoData,
   done <- chan struct{}, wg *sync.WaitGroup) {
     wg.Add(1)
     go func() {
         defer wg.Done()
           var (
               block cipher.Block
               err   error
               key   []byte
               data  *CryptoData
           )
         for i := start; i <= stop; i++ {
               key = make([]byte, 8)
             select {
             case <- done:
                   return
             default:
                 binary.BigEndian.PutUint64(key, i)
                   if block, err = rc2.New(key[3:], 40); err != nil {
                       log.Fatalln(err)
                   }
                   data = &CryptoData{
                       block: block,
                       key:   key[3:],
                   }
                 out <- data
               }
           }
       }()

       return
   }

Listing 11-9: The RC2 producer function (/ch-11/rc2-brute/main.go)

Your producer function is named generate() . It accepts two uint64 variables used to define a segment of the key space on which the producer will create work (basically, the range over which they’ll produce keys). This allows you to break up the key space, distributing portions of it to each producer.

The function also accepts two channels: a *CryptData write-only channel used for pushing work to consumers and a generic struct channel that’ll be used for receiving signals from consumers. This second channel is necessary so that, for example, a consumer that identifies the correct key can explicitly signal the producer to stop producing. No sense creating more work if you’ve already solved the problem. Lastly, your function accepts a WaitGroup to be used for tracking and synchronizing producer execution. For each concurrent producer that runs, you execute wg.Add(1) to tell the WaitGroup that you started a new producer.

You populate your work channel within a goroutine , including a call to defer wg.Done() to notify your WaitGroup when the goroutine exits. This will prevent deadlocks later as you try to continue execution from your main() function. You use your start() and stop() values to iterate a subsection of the key space by using a for loop . Every iteration of the loop increments the i variable until you’ve reached your ending offset.

As we mentioned previously, your key space is 40 bits, but i is 64 bits. This size difference is crucial to understand. You don’t have a native Go type that is 40 bits. You have only 32- or 64-bit types. Since 32 bits is too small to hold a 40-bit value, you need to use your 64-bit type instead, and account for the extra 24 bits later. Perhaps you could avoid this whole challenge if you could iterate the entire key space by using a []byte instead of a uint64. But doing so would likely require some funky bitwise operations that may overcomplicate the example. So, you’ll deal with the length nuance instead.

Within your loop, you include a select statement that may look silly at first, because it’s operating on channel data and doesn’t fit the typical syntax. You use it to check whether your done channel has been closed via case <- done . If the channel is closed, you issue a return statement to break out of your goroutine. When the done channel isn’t closed, you use the default case to create the crypto instances necessary to define work. Specifically, you call binary.BigEndian.PutUint64(key, i) to write your uint64 value (the current key) to a []byte named key.

Although we didn’t explicitly call it out earlier, you initialized key as an 8-byte slice. So why are you defining the slice as 8 bytes when you’re dealing with only a 5-byte key? Well, since binary.BigEndian.PutUint64 takes a uint64 value, it requires a destination slice of 8 bytes in length or else it throws an index-out-of-range error. It can’t fit an 8-byte value into a 5-byte slice. So, you give it an 8-byte slice. Notice throughout the remainder of the code, you use only the last 5 bytes of the key slice; even though the first 3 bytes will be zero, they will still corrupt the austerity of our crypto functions if included. This is why you call rc2.New(key[3:], 40) to create your cipher initially; doing so drops the 3 irrelevant bytes and also passes in the length, in bits, of your key: 40. You use the resulting cipher.Block instance and the relevant key bytes to create a CryptoData object, and you write it to the out worker channel .

That’s it for the producer code. Notice that in this section you’re only bootstrapping the relevant key data needed. Nowhere in the function are you actually attempting to decrypt the ciphertext. You’ll perform this work in your consumer function.

Performing Work and Decrypting Data

Let’s review the consumer function now (Listing 11-10). Again, you’ll add this function to the same file as your previous code.

 func decrypt(ciphertext []byte, in <- chan *CryptoData, 
   done chan struct{}, wg *sync.WaitGroup) {
       size := rc2.BlockSize
       plaintext := make([]byte, len(ciphertext))
     wg.Add(1)
       go func() {  
         defer wg.Done()
         for data := range in {
               select {
             case <- done:
                   return
             default:
                 data.block.Decrypt(plaintext[:size], ciphertext[:size])
                 if numeric.Match(plaintext[:size]) {
                     data.block.Decrypt(plaintext[size:], ciphertext[size:])
                     if luhn.Valid(string(plaintext)) && 
                       numeric.Match(plaintext[size:]) {
                           fmt.Printf("Card [%s] found using key [%x]
", /
                           plaintext, data.key)
                           close(done)
                           return
                       }
                   }
               }
           }
       }()
   }

Listing 11-10: The RC2 consumer function (/ch-11/rc2-brute/main.go)

Your consumer function, named decrypt() , accepts several parameters. It receives the ciphertext you wish to decrypt. It also accepts two separate channels: a read-only *CryptoData channel named in that you’ll use as a work queue and a channel named done that you’ll use for sending and receiving explicit cancellation signals. Lastly, it also accepts a *sync.WaitGroup named wg that you’ll use for managing your consumer workers, much like your producer implementation. You tell your WaitGroup that you’re starting a worker by calling wg.Add(1) . This way, you’ll be able to track and manage all the consumers that are running.

Next, inside your goroutine, you call defer wg.Done() so that when the goroutine function ends, you’ll update the WaitGroup state, reducing the number of running workers by one. This WaitGroup business is necessary for you to synchronize the execution of your program across an arbitrary number of workers. You’ll use the WaitGroup in your main() function later to wait for your goroutines to complete.

The consumer uses a for loop to repeatedly read CryptoData work structs from the in channel. The loop stops when the channel is closed. Recall that the producer populates this channel. As you’ll see shortly, this channel closes after the producers have iterated their entire key space subsections and pushed the relative crypto data onto the work channel. Therefore, your consumer loops until the producers are done producing.

As you did in the producer code, you use a select statement within the for loop to check whether the done channel has been closed , and if it has, you explicitly signal the consumer to stop additional work efforts. A worker will close the channel when a valid credit card number has been identified, as we’ll discuss in a moment. Your default case performs the crypto heavy lifting. First, it decrypts the first block (8 bytes) of ciphertext , checking whether the resulting plaintext is an 8-byte, numeric value . If it is, you have a potential card number and proceed to decrypt the second block of ciphertext . You call these decryption functions by accessing the cipher.Block field within your CryptoData work object that you read in from the channel. Recall that the producer instantiated the struct by using a unique key value taken from the key space.

Lastly, you validate the entirety of the plaintext against the Luhn algorithm and validate that the second block of plaintext is an 8-byte, numeric value . If these checks succeed, you can be reasonably sure that you found a valid credit card number. You display the card number and the key to stdout and call close(done) to signal the other goroutines that you’ve found what you’re after.

Writing the Main Function

By this point, you have your producer and consumer functions, both equipped to execute with concurrency. Now, let’s tie it all together in your main() function (Listing 11-11), which will appear in the same source file as the previous listings.

func main() {
    var (
        err        error
        ciphertext []byte
    )

    if ciphertext, err = hex.DecodeString("0986f2cc1ebdc5c2e25d04a136fa1a6b"); err != nil { 
        log.Fatalln(err)
    }

    var prodWg, consWg sync.WaitGroup 
    var min, max, prods = uint64(0x0000000000), uint64(0xffffffffff), uint64(75)
    var step = (max - min) / prods

    done := make(chan struct{})
    work := make(chan *CryptoData, 100)
    if (step * prods) < max { 
        step += prods
    }
    var start, end = min, min + step
    log.Println("Starting producers...")
    for i := uint64(0); i < prods; i++ { 
        if end > max {
            end = max
        }
        generate(start, end, work, done, &prodWg) 
        end += step
        start += step
    }
    log.Println("Producers started!")
    log.Println("Starting consumers...")
    for i := 0; i < 30; i++ { 
        decrypt(ciphertext, work, done, &consWg) 
    }
    log.Println("Consumers started!")
    log.Println("Now we wait...")
    prodWg.Wait()
    close(work)
    consWg.Wait()
    log.Println("Brute-force complete")
}

Listing 11-11: The RC2 main() function (/ch-11/rc2-brute/main.go)

Your main() function decodes your ciphertext, represented as a hexadecimal string . Next, you create several variables . First you create WaitGroup variables used for tracking both producer and consumer goroutines. You also define several uint64 values for tracking the minimum value in a 40-bit key space (0x0000000000), the maximum value in the key space (0xffffffffff), and the number of producers you intend to start, in this case 75. You use these values to calculate a step or range, which represents the number of keys each producer will iterate, since your intent is to distribute these efforts uniformly across all your producers. You also create a *CryptoData work channel and a done signaling channel. You’ll pass these around to your producer and consumer functions.

Since you’re doing basic integer math to calculate your step value for the producers, there’s a chance that you’ll lose some data if the key space size isn’t a multiple of the number of producers you’ll spin up. To account for this—and to avoid losing precision while converting to a floating-point number for use in a call to math.Ceil()—you check whether the maximum key (step * prods) is less than your maximum value for the entire key space (0xffffffffff) . If it is, a handful of values in the key space won’t be accounted for. You simply increase your step value to account for this shortage. You initialize two variables, start and end, to maintain the beginning and ending offsets you can use to break apart the key space.

The math to arrive at your offsets and step size isn’t precise by any means, and it could cause your code to search beyond the end of the maximum allowable key space. However, you fix that within a for loop used to start each of the producers. In the loop, you adjust your ending step value, end, should that value fall beyond the maximum allowed key space value. Each iteration of the loop calls generate() , your producer function, and passes to it the start (start) and end (end) key space offsets for which the producer will iterate. You also pass it your work and done channels, as well as your producer WaitGroup. After calling the function, you shift your start and end variables to account for the next range of key space that will be passed to a new producer. This is how you break up your key space into smaller, more digestible portions that the program can process concurrently, without overlapping efforts between goroutines.

After your producers are spun up, you use a for loop to create your workers . In this case, you’re creating 30 of them. For each iteration, you call your decrypt() function , passing to it the ciphertext, the work channel, the done channel, and the consumer WaitGroup. This spins up your concurrent consumers, which begin to pull and process work as the producers create it.

Iterating through the entire key space takes time. If you don’t handle things correctly, the main() function will assuredly exit before you discover a key or exhaust key space. So, you need to make sure the producers and consumers have adequate time to either iterate the entire key space or discover the correct key. This is where your WaitGroups come in. You call prodWg.Wait() to block main() until the producers have completed their tasks. Recall that the producers have completed their tasks if they either exhaust the key space or explicitly cancel the process via the done channel. After this completes, you explicitly close the work channel so the consumers won’t deadlock continually while trying to read from it. Finally, you block main() again by calling consWg.Wait() to give adequate time for the consumers in your WaitGroup to complete any remaining work in the work channel.

Running the Program

You’ve completed your program! If you run it, you should see the following output:

$ go run main.go
2020/07/12 14:27:47 Starting producers...
2020/07/12 14:27:47 Producers started!
2020/07/12 14:27:47 Starting consumers...
2020/07/12 14:27:47 Consumers started!
2020/07/12 14:27:47 Now we wait...
2020/07/12 14:27:48 Card [4532651325506680] found using key [e612d0bbb6]
2020/07/12 14:27:48 Brute-force complete

The program starts the producers and consumers and then waits for them to execute. When a card is found, the program displays the cleartext card and the key used to decrypt that card. Since we assume this key is the magical key for all cards, we interrupt execution prematurely and celebrate our success by painting a self-portrait (not shown).

Of course, depending on the key value, brute-forcing on a home computer can take a significant amount of time—think days or even weeks. For the preceding sample run, we narrowed the key space to find the key more quickly. However, completely exhausting the key space on a 2016 MacBook Pro takes approximately seven days. Not too bad for a quick-and-dirty solution running on a laptop.

Summary

Crypto is an important topic for security practitioners, even though the learning curve can be steep. This chapter covered symmetric and asymmetric crypto, hashing, password handling with bcrypt, message authentication, mutual authentication, and brute-forcing RC2. In the next chapter, we’ll get into the nitty-gritty of attacking Microsoft Windows.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset