TACIX.AT About
Asymmetric Crypto - Ransomware in Golang Part 2
2020/12/16 13:37

If you’re late to class, check out parts 0 and 1 at tacix.at.

Public Key Cryptography

Two types of encryption schemes to be aware of are public key (or asymmetric) cryptography, and symmetric-key cryptography.

Symmetric cryptography uses the same key for encryption and decryption. For example, you have some plaintext and some key, ciphertext = encrypt(plaintext, key). Going the other direction, with your ciphertext, plaintext = decrypt(ciphertext, key). The tricky thing here is both parties need to have the secret key, so that needs to be sent over some secure channel.

Asymmetric cryptography is a little different. Each party has a public key and private key. They are as their names describe, one is sharable and the other is to be kept secret. The public key can encrypt data, and the private key can decrypt it. So if you had a message for me, and you had my public key, you could encrypt that message and only I (or whoever has gained possession of my private key) could decrypt it. Conversely, you can also sign and verify. You sign data using the private key, and anyone with the public key can verify that signature.

In the context of our ransomware, the client (the malware that runs on the victim’s computer) will encrypt their own private key with the server’s public key. This way, the server will hold the key (eh? eh?) to unlocking the victim’s files.

RSA

RSA is a cryptosystem taking it’s name from three individuals - Rivest, Shamir, and Adleman respectively. There is also a company called RSA, and they throw a big conference also called RSA where vendors go to sell security offerings to each other. Here we are talking about the cryptosystem though.

Trail of Bits sings RSA’s praises in their blog post Seriously, stop using RSA. You should definitely read that post, you will learn some shit. We are going to disregard their advice though and use RSA because we are cowboy malware authors. At worst, our toy malware will have a crypto flaw in it that Brian Krebs will write a dissertation on after he doxxes us and a few other people who might be us for good measure, and then we will fix the flaw and carry on with our lives.

More likely, Golang’s RSA library will have sane defaults and everything will be OK. Until, of course, a flaw is found in that library like these cool XML/SAML vulns. C’est la vie.

Less talk, more typie typie

Alright, 程序猿 reporting for duty. Here’s a little scratch program that generates a key, encrypts a short plaintext, then decrypts it.

// scratch/asym.go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"log"
	"fmt"
)

func main() {
	privateKey, err := rsa.GenerateKey(rand.Reader, 1024)
	if err != nil {
		log.Fatal(err)
	}
	publicKey := privateKey.PublicKey

	p := []byte{0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5}
	fmt.Printf("Plaintext:\t%v\n", p)

	c, err := rsa.EncryptOAEP(
		sha256.New(), rand.Reader, &publicKey, p, nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Ciphertext:\t%v\n", c)

	pd, err := rsa.DecryptOAEP(
		sha256.New(), rand.Reader, privateKey, c, nil)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Decrypted:\t%v\n", pd)
}

First things first, we’re importing crypto/rand. This is a cryptographically secure random number generator and not a psuedo random number generator. Pseudo random number generators (i.e. math/random) use some cute math to produce seemingly random numbers which are often predictable. This is fine in cases where it doesnt matter but cryptography is a case where it absolutely does matter.

Next we generate our private key by passing our source of random bytes and a key size in bits to rsa.GenerateKey(). Here we are using 1024 which is small but hasn’t been factored yet. I think these can be any size but in practice you only ever seen multiples of 1024 (i.e. 2048, 3072, 4096). I’ve seen keys smaller than 1024 that weren’t multiples of 1024 but you get the point, generally powers of 2.

Then we get the public key out of the private key struct, and also make a 16 byte plaintext.

Onto the encryption. We’re using rsa.EncryptOAEP() from Go’s crypto/rsapackage. Take a look at the docs, get comfortable reading docs when writing Go. They are your friend.

We have two choices for encryption in the rsa package. EncryptPKCS1v15() and EncryptOAEP(). Reading the package overview, or the EncryptPKCS1v15 description, or the Trail of Bits blog post should guide you to EncryptOAEP(). From the package description -

The original specification for encryption and signatures with RSA is PKCS #1 and the terms “RSA encryption” and “RSA signatures” by default refer to PKCS #1 version 1.5. However, that specification has flaws and new designs should use version 2, usually called by just OAEP and PSS, where possible.

We’ve decided on OAEP, let’s review some of the parameters.

c, err := rsa.EncryptOAEP(
		sha256.New(), rand.Reader, &publicKey, p, nil)

The first is a hash function, which is used to hash the data gotten from the random.Reader which is passed in as the second parameter. This randomness is used to make sure the same plaintext doesn’t encrypt to the same ciphertext. If it did, and say you were encrypting each letter you typed, someone could do a frequency analysis on the ciphertexts and figure out which ones corresponded to which keys. This would be bad.

Next we pass in a pointer to our public key and the plaintext. The last parameter is a label, if we were encrypting multiple things we might want to label them. That would ensure that one type of encrypted thing couldn’t be confused as the other. We aren’t though so that is nil.

There is also a spooky statement at the end of the description of EncryptOAEP() - The message must be no longer than the length of the public modulus minus twice the hash length, minus a further 2.. Alright, time for some math. Our public modulus is 1024 bits, a SHA256 is 32 bytes, and there are 8 bits in a byte. 1024 bits - (32 bytes * 2 * 8 bits/byte) - 2 bits = 510 bit message. 510 bits / 8 bits/byte = 63.75. Cool, so we can encrypt up to 63 bytes.

What happens if we try to encrypt 64? You can change the p := []byte{...} line to p := make([]byte, 64) and give it a shot. The function fails with the error crypto/rsa: message too long for RSA public key size.

Wow, look at all that stuff we just learned. Glad we chose to use RSA. Don’t roll your own crypto is how they keep you IGNORANT of the government’s MATH backdoors.

Disclaimer: We aren’t rolling our own crypto here.

I’ll do a post on ECDSA and U2F tokens in the future and we can learn a little bit about the government’s new backdoored math.

Also Disclaimer: I have no evidence of backdoors.

Standard Ransomware Crypto Scheme

I mentioned in Post 0 that we’ll be using the standard ransomware encryption scheme. We will encrypt files with a symmetric key, we will encrypt those keys (they’re small enough!) with the client’s public key, and the client’s private key will be encrypted with the server’s public key. Follow that?

So the server’s private key can decrypt the client’s private key, which can in turn unlock the files’ encryption keys.

De/Serialization

That means we have to generate the server’s keypair, save them, and be able to load them back up whenever the server starts. We also want to output the server’s public key as bytes so that we can load them. Let’s learn some terms.

X.509 - This is a standard that defines the format of public key certificates.

PKCS #1 - Public Key Cryptography Standards numero Uno. This defined the RSA algorithm and properties.

ASN.1 - Abstract Syntax Notation 1 is an interface description format for defining data structures.

DER - Distinguished Encoding Rules is a way to encode ASN.1 defined data. Described in the X.690 standard, nice0.

Key Generation

Let’s get to it then! We’ll be generating a keypair, writing our RSA / PKCS1 keys out in an ASN.1 DER encoded format. For this one we’re upping the bits to 2k48 because we’ll be encrypting the client key (1k24 bits) and from our formula (2048 bits - (32 bytes * 2 * 8 bits/byte) - 2) = 1534 bits, AKA enough space to fit a 1024 bit key. We could go as low as a 1538 bit server key but using something that isn’t a power of 2 would offend the divine numerology and our malware would no longer be blessed.

First, we don’t want to overwrite our server keypair so we’re going to check if those files exist.

	_, errPub := os.Stat("key.pub")
	_, errPrv := os.Stat("key.prv")

	if errPub == nil && errPrv == nil {
		pub, err := ioutil.ReadFile("key.pub")
		if err != nil {
			log.Fatal(err)
		}
		printBytes(pub)
		log.Fatal("keypair exists, will not overwrite")	
	}

Following that, we generate a key, get the DER encoded bytes of the public key, write them to a file, ditto for the private key.

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		log.Fatal(err)
	}

	pub := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
	err = ioutil.WriteFile("key.pub", pub, 0644)
	if err != nil {
		log.Fatal(err)
	}

	prv := x509.MarshalPKCS1PrivateKey(privateKey)
	err = ioutil.WriteFile("key.prv", prv, 0644)
	if err != nil {
		log.Fatal(err)
	}

We also add a cute little printBytes() function to pretty print our public key in a way we can just paste into our client. Note, we also print the bytes when the keys exist because we might want to reprint those.

// util/genkey.go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"log"
	"os"
)

func printBytes(pub []byte) {
	fmt.Printf("\tpub := []byte{")
	for i := range pub {
		if i > 0 {
			fmt.Printf(",")
		}

		if i%8 == 0 {
			fmt.Printf("\n\t\t")
		}

		fmt.Printf(" 0x%02x", pub[i])
	}
	fmt.Println(" }")
}

func main() {
	_, errPub := os.Stat("key.pub")
	_, errPrv := os.Stat("key.prv")

	if errPub == nil && errPrv == nil {
		pub, err := ioutil.ReadFile("key.pub")
		if err != nil {
			log.Fatal(err)
		}
		printBytes(pub)
		log.Fatal("keypair exists, will not overwrite")
	}

	privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		log.Fatal(err)
	}

	pub := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
	err = ioutil.WriteFile("key.pub", pub, 0644)
	if err != nil {
		log.Fatal(err)
	}

	prv := x509.MarshalPKCS1PrivateKey(privateKey)
	err = ioutil.WriteFile("key.prv", prv, 0644)
	if err != nil {
		log.Fatal(err)
	}

	printBytes(pub)
}

Now we run it. I’m going to do so in the ~/prog/rw/server/ directory which doesn’t exist yet.

Worry if your’s outputs the same key as mine and don’t worry about the converse.

$ mkdir server
$ cd server
$ go run ../util/genkey.go
	pub := []byte{
		0x30, 0x82, 0x01, 0x0a, 0x02, 0x82, 0x01, 0x01,
		0x00, 0xb4, 0x10, 0x46, 0xf7, 0x7e, 0x73, 0x72,
		0x22, 0x4a, 0xcf, 0x98, 0xc8, 0x60, 0x67, 0x6c,
		0x96, 0x0b, 0xf9, 0x42, 0x5d, 0x05, 0xbf, 0x99,
		0x96, 0xe8, 0x69, 0xed, 0x95, 0xa4, 0x95, 0xbb,
		0xd2, 0x62, 0xa5, 0x35, 0x77, 0x97, 0x19, 0xc7,
		0x09, 0x92, 0x61, 0xd9, 0x5f, 0xea, 0x3e, 0x49,
		0xf5, 0x3f, 0x6f, 0x84, 0x21, 0x94, 0xc9, 0xda,
		0xfd, 0x20, 0x62, 0xc4, 0x61, 0x7c, 0x86, 0xfc,
		0xd6, 0x1f, 0xc8, 0x35, 0x2a, 0x78, 0xd5, 0x3e,
		0xb8, 0xd3, 0x3b, 0xa7, 0x3c, 0x9e, 0x82, 0x55,
		0xe3, 0x5b, 0x5c, 0x82, 0x50, 0xa5, 0x06, 0xf4,
		0x42, 0xfe, 0x93, 0xad, 0x61, 0x80, 0xff, 0xf2,
		0x8e, 0xe3, 0x78, 0xcb, 0xec, 0x91, 0x3f, 0x40,
		0xae, 0x71, 0x1f, 0x50, 0xb3, 0x1c, 0x1e, 0xdc,
		0x99, 0xed, 0xbb, 0x33, 0xe4, 0x6c, 0xb4, 0x84,
		0x18, 0x87, 0x51, 0xc7, 0x42, 0x0e, 0xa5, 0x80,
		0x4c, 0x36, 0xd2, 0xf2, 0x52, 0x58, 0x08, 0x26,
		0x6c, 0x36, 0x4b, 0x15, 0x22, 0x91, 0xd1, 0x92,
		0xcb, 0x82, 0x0f, 0xa8, 0x3f, 0xbe, 0x57, 0x2a,
		0xd0, 0xf2, 0x51, 0xa6, 0x3c, 0x92, 0xb9, 0x00,
		0x25, 0x23, 0xf4, 0x48, 0xa4, 0x8f, 0x09, 0xb4,
		0x5f, 0x42, 0x3f, 0x7d, 0x2d, 0xf8, 0xb0, 0x61,
		0x03, 0xcc, 0x93, 0x29, 0x63, 0x6a, 0xce, 0x1c,
		0x3f, 0x19, 0x7b, 0x03, 0x13, 0xd2, 0xd9, 0x99,
		0x85, 0x55, 0x2c, 0xfa, 0x19, 0x92, 0x18, 0x8a,
		0x39, 0x57, 0x4e, 0x22, 0xa3, 0x39, 0x93, 0xaa,
		0x5b, 0xaa, 0x2f, 0x2a, 0x41, 0xcb, 0xb6, 0xbc,
		0xde, 0x29, 0xe1, 0xbf, 0x4b, 0xbb, 0xac, 0x38,
		0x00, 0x1b, 0x4f, 0xb8, 0x4f, 0x39, 0xf6, 0xb0,
		0x0d, 0x92, 0x49, 0x0b, 0x60, 0x25, 0x15, 0xd2,
		0xad, 0xfa, 0x56, 0xb1, 0x0a, 0x94, 0xfc, 0xdc,
		0x55, 0xb9, 0x52, 0xe6, 0x64, 0x93, 0x85, 0x36,
		0x23, 0x02, 0x03, 0x01, 0x00, 0x01 }
$ ls 
key.prv  key.pub

Conclusion

Hopefully now we understand what asymmetric encryption is. We know that RSA has many pitfalls but Go provides some sane defaults as long as you can read. We can also generate the keypair for our server (yay).

In the next post we will get to write our ransomware client (also yay).

Now fuck off and do some exercise in the sun.