Fun With Kernel Cipher Hacking

This is probably not practically useful but it’s a neat proof of concept solution to a real-life problem! We run a Ceph cluster for the majority of our data and we’re looking into using a managed backup-as-a-service thingy, which is fine, but we have requirements that the backup service isn’t allowed to see or store any of our plaintext data. Totally reasonable but a little hard to swing on Ceph — although the backing disks are encrypted the actual disk images hosted there aren’t.

A normal solution would probably look like grabbing chunks of each image on a separate server, encrypting them, and then giving those to the backup agent to store. Very much like what OVH does. But the reason OVH went this route was because of limitations of Duplicity, and the backup agent we’re using handles huge block devices just fine. So we’re going through this just to encrypt some data in-flight and then we have to do some awkward reassembly dance when we restore. If only we were trying to decrypt the data it would be no issue. We would just use dm-crypt and the kernel would decrypt on the fly for us and the backup agent would be none the wiser.

Can we run this process backwards?

Shout out to Arno Wagner for this thread posted in 2013. Assuming your problem is still unimplemented 7 years later I got you sorted! After a lot of back-and-forth we finally get to a potential way of accomplishing this.

I think that quick hack to try it would be to write simple kernel cipher module (or wrapper), where you only change cipher name (so it will not mix up with normal implementation, name like reverse_aes or so) and just switch encrypt/decrypt callbacks.

I am afraid you will need to avoid LUKS and IV where encryption is used (ESSIV) (or at least you must analyze if encrypt/decrypt change for the given cipher is safe for use there).

I’m going to describe how to implement what the poster suggests. At the end I will have a block cipher revaes and I will use it like cryptsetup plainOpen --cipher revaes /dev/sdb1 sdb1_enc. Can any Linux kernel or crypto enthusiasts call in advance why this won’t work? And if you get that, can you call in advance how to use an existing cipher to do what I want?

Let’s start with a bare-bones Makefile to actually build the module.

obj-m += revaes.o

all:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
	make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

Wonderful! So all we need to do is write a file revaes.c with the module and we’re done. Let’s poke around the kernel source and see if there’s anything we can use as a guide. Looks like [aes_ti.c](https://github.com/torvalds/linux/blob/b25c6644bfd3affd7d0127ce95c5c96c155a7515/crypto/aes_ti.c) is short, sweet, and has all the bits I need. Let’s just touch it up a bit.

// SPDX-License-Identifier: GPL-2.0-only
/*
 * Reversed AES core transform
 *
 * Copyright (C) 2017 Linaro Ltd <ard.biesheuvel@linaro.org>
 */

#include <crypto/aes.h>
#include <linux/crypto.h>
#include <linux/module.h>

static int revaes_set_key(struct crypto_tfm *tfm, const u8 *in_key,
			 unsigned int key_len)
{
	struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	return aes_expandkey(ctx, in_key, key_len);
}

static void revaes_encrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	const struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	aes_encrypt(ctx, out, in);
}

static void revaes_decrypt(struct crypto_tfm *tfm, u8 *out, const u8 *in)
{
	const struct crypto_aes_ctx *ctx = crypto_tfm_ctx(tfm);

	aes_decrypt(ctx, out, in);
}

static struct crypto_alg aes_alg = {
	.cra_name			    = "revaes",
	.cra_driver_name	= "revaes-generic",
	.cra_priority			= 100,
	.cra_flags			  = CRYPTO_ALG_TYPE_CIPHER,
	.cra_blocksize		= AES_BLOCK_SIZE,
	.cra_ctxsize			= sizeof(struct crypto_aes_ctx),
	.cra_module			  = THIS_MODULE,
	.cra_cipher.cia_min_keysize	= AES_MIN_KEY_SIZE,
	.cra_cipher.cia_max_keysize	= AES_MAX_KEY_SIZE,
	.cra_cipher.cia_setkey		= revaes_set_key,
	.cra_cipher.cia_encrypt		= revaes_decrypt,
	.cra_cipher.cia_decrypt		= revaes_encrypt
};

static int __init aes_init(void)
{
	return crypto_register_alg(&aes_alg);
}

static void __exit aes_fini(void)
{
	crypto_unregister_alg(&aes_alg);
}

module_init(aes_init);
module_exit(aes_fini);

MODULE_DESCRIPTION("Generic reversed AES");
MODULE_AUTHOR("Ard Biesheuvel <ard.biesheuvel@linaro.org>");
MODULE_LICENSE("GPL v2");

Sweet! Let’s build it, load it, and try using it!

make
sudo insmod revaes.ko
sudo cryptsetup plainOpen --cipher revaes /dev/sdb1 sdb1_enc
sudo cryptsetup plainOpen --cipher aes /dev/mapper/sdb1_enc sdb1_plain
mount /dev/mapper/sdb1_pain /mnt/tmp
# Err. Bad superblock.

Welp. What went wrong?

cryptsetup status sdb1_enc
/dev/mapper/sdb1_env is active.
  type:    PLAIN
  cipher:  aes-cbc-plain
  keysize: 256 bits
  key location: dm-crypt
  device:  /dev/sdb1
  sector size:  512
  offset:  0 sectors
  size:    12345 sectors
  mode:    read/write

This is the point where I banged my head for a while and where the crypto nerds should be frustratedly shouting at the screen like when Dora should clearly be able to see that the largo plank fits in the hole in the bridge.

Let’s talk about cipher modes or chainmodes as the kernel calls them. If you look in the kernel docs you’ll see the very underdescribed dm-crypt cipher specification.

cipher[:keycount]-chainmode-ivmode[:ivopts]
or
capi:cipher_api_spec-ivmode[:ivopts]

Zero idea what these are. And outside of the examples it’s not even clear what values are permissible in each of the slots. Is is clear I probably shouldn’t even be touching crypto code? But where’s the fun in that?! How else does one learn? Luckily Wikipedia has my back with a fantastic article on the subject.

Here’s the short explanation. Naively when you have a block cipher and you want to encrypt lots of data you can just chunk the data into blocks, encrypt each one, and call it a day. But if you have two identical blocks in this system they encrypt to the same thing which reveals information about the plaintext. So the idea was generalized into cipher modes and initialization vectors (IV). The cipher mode is a function that takes a block cipher and an optional alg-specific argument (the IV) and produces a function that can encrypt more than one block.

Here is the relevant diagram for CBC, the default cipher mode on my system.

the cbc encryption diagram

the cbc decryption diagram

So just because I reversed the block cipher (aes) doesn’t mean I reversed the cipher mode. If we look at the bottom diagram and replace decryption with encryption it doesn’t suddenly become the encryption diagram which is what we wanted. In fact, if we were to reverse the cipher mode we don’t even need to reverse the block cipher! Well that was a waste. Are there by chance any cipher modes that are actually reversible by only reversing the block cipher? YES! The naive alg which is called ECB. It looks like it would be a terrible idea to actually use this in prod but does it work though?

make
sudo insmod revaes.ko
sudo cryptsetup plainOpen --cipher revaes-ecb /dev/sdb1 sdb1_enc
sudo cryptsetup plainOpen --cipher aes-ecb /dev/mapper/sdb1_enc sdb1_plain
mount /dev/mapper/sdb1_pain /mnt/tmp
# Success!

Neat! Now let’s create a revcbc from [cbc.c](https://github.com/torvalds/linux/blob/b25c6644bfd3affd7d0127ce95c5c96c155a7515/crypto/cbc.c) and try that.

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * CBC: Cipher Block Chaining mode
 *
 * Copyright (c) 2006-2016 Herbert Xu <herbert@gondor.apana.org.au>
 */

#include <crypto/algapi.h>
#include <crypto/cbc.h>
#include <crypto/internal/skcipher.h>
#include <linux/err.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/log2.h>
#include <linux/module.h>

static inline void crypto_cbc_encrypt_one(struct crypto_skcipher *tfm,
                                          const u8 *src, u8 *dst)
{
	crypto_cipher_encrypt_one(skcipher_cipher_simple(tfm), dst, src);
}

static int crypto_cbc_encrypt(struct skcipher_request *req)
{
	return crypto_cbc_encrypt_walk(req, crypto_cbc_encrypt_one);
}

static inline void crypto_cbc_decrypt_one(struct crypto_skcipher *tfm,
                                          const u8 *src, u8 *dst)
{
	crypto_cipher_decrypt_one(skcipher_cipher_simple(tfm), dst, src);
}

static int crypto_cbc_decrypt(struct skcipher_request *req)
{
	struct crypto_skcipher *tfm = crypto_skcipher_reqtfm(req);
	struct skcipher_walk walk;
	int err;

	err = skcipher_walk_virt(&walk, req, false);

	while (walk.nbytes) {
		err = crypto_cbc_decrypt_blocks(&walk, tfm,
		                                crypto_cbc_decrypt_one);
		err = skcipher_walk_done(&walk, err);
	}

	return err;
}

static int crypto_cbc_create(struct crypto_template *tmpl, struct rtattr **tb)
{
	struct skcipher_instance *inst;
	struct crypto_alg *alg;
	int err;

	inst = skcipher_alloc_instance_simple(tmpl, tb);
	if (IS_ERR(inst))
		return PTR_ERR(inst);

	alg = skcipher_ialg_simple(inst);

	err = -EINVAL;
	if (!is_power_of_2(alg->cra_blocksize))
		goto out_free_inst;

	inst->alg.encrypt = crypto_cbc_decrypt;
	inst->alg.decrypt = crypto_cbc_encrypt;

	err = skcipher_register_instance(tmpl, inst);
	if (err) {
		out_free_inst:
		inst->free(inst);
	}

	return err;
}

static struct crypto_template crypto_cbc_tmpl = {
	.name = "revcbc",
	.create = crypto_cbc_create,
	.module = THIS_MODULE,
};

static int __init crypto_cbc_module_init(void)
{
	return crypto_register_template(&crypto_cbc_tmpl);
}

static void __exit crypto_cbc_module_exit(void)
{
	crypto_unregister_template(&crypto_cbc_tmpl);
}

subsys_initcall(crypto_cbc_module_init);
module_exit(crypto_cbc_module_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Reversed CBC block cipher mode of operation");
MODULE_ALIAS_CRYPTO("revcbc");

Add revcbc.o to our Makefile and let’s try again.

make
sudo insmod revcbc.ko
sudo cryptsetup plainOpen --cipher aes-revcbc-plain /dev/sdb1 sdb1_enc
sudo cryptsetup plainOpen --cipher aes-cbc-plain /dev/mapper/sdb1_enc sdb1_plain
mount /dev/mapper/sdb1_pain /mnt/tmp
# Success #2!

Now we actually have something workable! Let me just quickly deploy this to prod real quick.

Why All This Was Unnecessary

Well it turns out that there are these neat little things called steam ciphers. Rather than encrypting the plaintext with the block cipher they encrypt blocks of a stream of pseudorandom numbers and XOR them with the plaintext. Technically speaking dm-crypt doesn’t actually support any stream ciphers but they do support two cipher modes OFB and CTR which make a stream cipher out of any block cipher. Steam ciphers are perfect for this application because encryption == decryption. If you take some data and XOR it with the same thing twice you get your original data back!

Here’s the diagram for OFB .

ofb encryption diagram

ofb decryption diagram

make
sudo insmod revcbc.ko
sudo cryptsetup plainOpen --cipher aes-ofb-plain /dev/sdb1 sdb1_enc
sudo cryptsetup plainOpen --cipher aes-ofb-plain /dev/mapper/sdb1_enc sdb1_plain
mount /dev/mapper/sdb1_pain /mnt/tmp
# Success #3! Look ma, no kernel module!

Final Thoughts

This was a lot of fun! It really scratches that itch of making something a little silly work. I can actually imagine the latter solution being used in prod but I doubt my hacked-together kernel module will make the cut.