Шифрование в EXT4. How It Works?

в 12:14, , рубрики: encryption, ext4, forensic analysis, Алгоритмы, криптография, Разработка под Linux

image Паранойя не лечится! Но и не преследуется по закону. Поэтому в Linux Kernel 4.1 добавлена поддержка шифрования файловой системы ext4 на уровне отдельных файлов и директорий. Зашифровать можно только пустую директорию. Все файлы, которые будут созданы в такой директории, также будут зашифрованы. Шифруются только имена файлов и содержимое, метаданные не шифруются, inline data (когда данные файла, не превышающие по размеру 60 байт, хранятся в айноде) в файлах не поддерживается. Поскольку расшифровка содержимого файла выполняется непосредственно в памяти, шифрование доступно только в том случае, когда размер кластера совпадает с PAGE_SIZE, т.е. равен 4К.

1. Как это работает

Для начала необходимо освоить несколько полезных команд

Форматирование тома с опцией шифрования

# mkfs.ext4 -O encrypt /dev/xxx

Включение опции шифрования на существующий том

# tune2fs -O encrypt /dev/xxx

Создание ключа шифрования

# mount /dev/xxx /mnt/xxx
$ e4crypt add_key
Enter passphrase (echo disabled): 
Added key with descriptor [8e679e4449bb9235]

При создании ключа том с поддержкой шифрования должен быть примонтирован, иначе e4crypt выдаст ошибку “No salt values available”. Если примонтировано несколько томов с опцией encrypt, то будут созданы ключи для каждого. Утилита e4crypt входит в состав e2fsprogs.

Ключи добавляются в Linux Kernel Keyring [1].

Чтение списка ключей

$ keyctl show
Session Keyring
 771961813 --alswrv   1000 65534  keyring: _uid_ses.1000
 771026675 --alswrv   1000 65534   _ keyring: _uid.1000
 803843970 --alsw-v   1000  1000   _ logon: ext4:8e679e4449bb9235

Ключи, используемые для шифрования, имеют тип “logon”. Содержимое (payload) ключей такого типа недоступно из пространства пользователя — keyctl команды read, pipe, print вернут ошибку. В данном примере у ключа префикс “ext4”, но может быть и “fscrypt”. Если keyctl отсутствует в системе, то необходимо установить пакет keyutils.

Создание зашифрованной директории

$ mkdir /mnt/xxx/encrypted_folder
$ e4crypt set_policy 8e679e4449bb9235 /mnt/xxx/encrypted_folder/
Key with descriptor [8e679e4449bb9235] applied to /mnt/xxx/encrypted_folder/.

Здесь в команду set_policy передается дескриптор созданного ключа без указания префикса (ext4) и типа (logon). Одним и тем же ключом можно зашифровать несколько директорий. Для шифрования разных директорий можно использовать разные ключи. Чтобы узнать, каким ключом зашифрована директория, необходимо выполнить команду:

$ e4crypt get_policy /mnt/xxx/encrypted_folder/
/mnt/xxx/encrypted_folder/: 8e679e4449bb9235

Установить другую политику безопасности на зашифрованную директорию не получится:

$ e4crypt add_key
Enter passphrase (echo disabled): 
Added key with descriptor [9dafe822ae6e7994]
$ e4crypt set_policy 9dafe822ae6e7994 /mnt/xxx/encrypted_folder/
Error [Invalid argument] setting policy.
The key descriptor [9dafe822ae6e7994] may not match the existing encryption context for directory [/mnt/xxx/encrypted_folder/].

Зато такую директорию можно беспрепятственно удалить:

$ rm -rf /mnt/xxx/encrypted_folder/
$ ll /mnt/xxx
total 24
drwxr-xr-x 3 user user  4096 Apr 21 15:14 ./
drwxr-xr-x 4 root root  4096 Mar 29 15:30 ../
drwx------ 2 root root 16384 Apr 17 12:41 lost+found/
$

Шифрование файла

$ echo "My secret file content" > /mnt/xxx/encrypted_folder/my_secrets.txt
$ cat /mnt/xxx/encrypted_folder/my_secrets.txt 
My secret file content
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user   23 Apr 20 14:26 my_secrets.txt

Имена файлов в директории и содержимое файла будут доступны, пока в хранилище ключей существует ключ, которым была зашифрована директория. После аннулирования ключа доступ к директории будет сильно ограничен:

$ keyctl revoke 803843970
$ keyctl show
Session Keyring
 771961813 --alswrv   1000 65534  keyring: _uid_ses.1000
 771026675 --alswrv   1000 65534   _ keyring: _uid.1000
803843970: key inaccessible (Key has been revoked)

Ключ аннулирован, читаем содержимое директории:

$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user   23 Apr 20 14:26 BhqTNRNHDBwpa9S1qCaXwC

Имя файла уже абырвалг. Но всё-таки попробуем прочитать файл:

$ cat /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC 
cat: /mnt/xxx/encrypted_folder/BhqTNRNHDBwpa9S1qCaXwC: Required key not available

NOTE: в Ubuntu 17.04 (kernel 4.10.0-19) директория остается доступной после удаления ключа до перемонтирования.

$ keyctl show
Session Keyring
 771961813 --alswrv   1000 65534  keyring: _uid_ses.1000
 771026675 --alswrv   1000 65534   _ keyring: _uid.1000
$ e4crypt get_policy /mnt/xxx/encrypted_folder/
/mnt/xxx/encrypted_folder/: 8e679e4449bb9235

Директория зашифрована ключом с дескриптором “8e679e4449bb9235”. Ключ отсутствует в хранилище. Несмотря на это, директория и содержимое файла в свободном доступе.

$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user   23 Apr 20 14:26 my_secrets.txt
$ cat /mnt/xxx/encrypted_folder/my_secrets.txt 
My secret file content

Перемонтирование:

# umount /dev/xxx
# mount /dev/xxx /mnt/xxx
$ ll /mnt/xxx/encrypted_folder/
total 12
drwxr-xr-x 2 user user 4096 Apr 20 14:25 ./
drwxr-xr-x 5 user user 4096 Apr 20 14:15 ../
-rw-r--r-- 1 user user   23 Apr 20 14:26 BhqTNRNHDBwpa9S1qCaXwC

2. Изменения в файловой системе

В Суперблоке: набор опций s_feature_incompat на томе с поддержкой шифрования содержит флаг EXT4_FEATURE_INCOMPAT_ENCRYPT,
s_encrypt_algos[4] — хранит алгоритмы шифрования; на данный момент это:
s_encrypt_algos[0] = EXT4_ENCRYPTION_MODE_AES_256_XTS;
s_encrypt_algos[1] = EXT4_ENCRYPTION_MODE_AES_256_CTS;
s_encrypt_pw_salt — также задается при форматировании.

В айноде: i_flags содержит флаг EXT4_ENCRYPT_FL и именно по нему можно определить, что объект зашифрован.

Структура зашифрованной директории

Чтобы прочитать содержимое директории, нужно по ее айноду определить ее местоположение на диске.

1. Определение номера айнода:

$ stat /mnt/xxx/encrypted_folder/
  File: /mnt/xxx/encrypted_folder/
  Size: 4096      	Blocks: 8          IO Block: 4096   directory
Device: 811h/2065d	Inode: 14          Links: 2

2. Поиск айнода в таблице айнодов.

Айнод 14 принадлежит 0-й группе, поэтому необходимо прочитать таблицу дескрипторов 0-й группы и найти в ней номер блока таблицы айнодов. Таблица дескрипторов 0-й группы находится в кластере, следующим за суперблоком:

# dd if=/dev/xxx of=gdt bs=4096 count=1 skip=1

image
Рис. 1. Таблица дескрипторов 0-й группы

Вначале пропускаем номера кластеров битмапа блоков и битмапа айнодов, номер кластера начала таблицы айнодов читаем по смещению 8 байт от начала таблицы — 0x00000424 (1060) в BigEndian формате. Айнод директории = 14, при размере айнода в 256 байт в таблице он будет находиться по смещению 0x0D00 от ее начала. Таким образом, достаточно прочитать только 1-й кластер таблицы айнодов:

# dd if=/dev/xxx of=itable bs=4096 count=1 skip=1060

image
Рис. 2. Айнод зашифрованной директории.

В айноде определяем начало поля i_block[]. Т.к. это ext4, то в первых 2 байтах i_block находится заголовок дерева экстентов — 0xF30A. Далее можно увидеть номер блока, в котором хранится зашифрованная директория — 0x00000402 (1026). (На рисунке выделено не всё поле i_block, а только информативные 24 байта — остальные 36 байт заполнены нулями.)

3. Чтение блока директории:

# dd if=/dev/xxx of=dirdata bs=4096 count=1 skip=1026

image
Рис. 3. Дамп зашифрованной директории.

Подробнее: первые две entry (выделены красным) — это записи “.” и “..”, соответственно, текущая и родительская директории. У текущей директории айнод 0x0000000E, длина записи 0x000C байт, количество символов в имени файла — 01 и тип entry 02 — это директория. Далее следует имя директории, выровненное по 4-байтовой границе — 2E000000 (2E соответствует символу ‘.’ — точка).

Следующая, родительская директория, имеет айнод 0x00000002 (корневая директория), аналогичная длина записи 0x000C, в имени 02 символа, тип также 02, после чего идет имя директории — 2E2E0000 (две точки).

Наконец, последняя entry в данной директории имеет айнод 0x0000000F, размер записи 0x0FDC, количество символов в имени 0x10, тип 01 — это и есть зашифрованный файл. Как видно его имя не соответствует созданному my_secrets.txt. К тому же, в исходном имени файла всего 14 символов, а не 16 как здесь.

NOTE: особенно внимательные читатели с калькулятором могли заметить, что т.к. зашифрованный файл является последней entry в директории, то его размер записи должен ссылаться на границу блока. Однако, 0x1000 — 0xC — 0xC = 0xFE8, а не 0xFDC. Это связано с тем, что том создавался с опцией “metadata_csum”, которая задается по умолчанию, начиная с Ubuntu 16.10. При включении этой опции в конце каждого блока директории создается 12-байтовая структура, содержащая контрольную сумму этого блока.

4. Чтение зашифрованного файла.

Из дампа директории определяем, что файл имеет айнод 15 (0xF). Ищем его в таблице айнодов и аналогично определяем его положение на диске:

image
Рис. 4. Айнод зашифрованного файла.

Читаем содержимое кластера 0x0000AA00 (43520)

# dd if=/dev/xxx of=filedata bs=4096 count=1 skip=43520

image
Рис. 5. Содержимое зашифрованного файла

И это совсем не соответствует записанной в файл информации. Настоящий размер файла можно прочитать в поле i_size айнода (отмечен синим прямоугольником на рис. 4): 0x00000017 — именно столько было записано командой echo “My secret file content” + символ перевода строки 0x0A.

3. Расшифровка

Расшифровка имени файла

Согласно EXT4 Encryption Design Document [2] расшифровка имен файлов выполняется в два этапа:

1. DerivedKey = AES-128-ECB(data=MasterKey, key=DirNonce);
2. EncFileName = AES-256-CBC-CTS(data=DecFileName, key=DerivedKey);

Т.е. на первом этапе надо получить ключ для расшифровки. Для этого используются данные Мастер-ключа, созданного при добавлении ключа в keyring, которые шифруются по AES-ECB 128-битным ключом DirNonce. На втором этапе используется фиксированный вектор инициализации (IV), заполненный нулями. Для AES-ECB вектор инициализации не нужен.

Что такое DirNonce? В айноде зашифрованной директории есть extended attribute.

image
Рис. 6. Айнод зашифрованной директории и его extended attribute

При размере айнода в 256 байт в структуре остается около сотни неиспользуемых байт (0x100 — EXT2_GOOD_OLD_INODE_SIZE — i_extra_size), в которых можно хранить информацию (красная область на рис. 6). Как видно по заголовку 0xEA020000 в первых четырех байтах этой области, здесь хранится extended attribute с индексом 09, данные которого смещены на 0x40 байт от заголовка и имеют размер 0x1C. Область данных поделена на 3 зоны: в первой (01 01 04 00) записаны алгоритмы, по которым был зашифрован айнод. Во второй — хранится 8 байт (8E 67 9E 44 49 BB 92 35), повторяющие дескриптор ключа. В третьей — содержится 16-байтовый одноразовый код (нонс [3]), используемый при шифровании Мастер-ключа.

Таким образом, для расшифровки имени файла, необходимо:

1) прочитать значение безымянного extended attribute директории с индексом 9 — получаем нонс директории;
2) по алгоритму AES-ECB зашифровать данные Мастер-ключа, используя в качестве ключа 128 бит нонса директории;
3) по алгоритму AES-CBC-CTS расшифровать имя файла, используя в качестве ключа первые 256 бит (половину) ключа, полученного на предыдущем этапе.

Расшифровка содержимого файла

Выполняется аналогично процедуре расшифровки имени файла, за исключением того, что в качестве нонса используется значение extended attribute, полученное из айнода файла. И вместо CBC содержимое дешифруется по алгоритму AES-XTS с полным 64-байтовым ключом. В качестве IV используется Logical Block Offset относительно начала файла

image
Рис. 7. Айнод зашифрованного файла и его extended attribute.

Сравнивая значение extended attribute зашифрованного файла и директории, можно заметить, что их нонсы различаются, в то время как алгоритмы шифрования и дескрипторы ключей совпадают (желтая и синяя зоны на рисунках).

Содержимое файлов шифруется постранично, поэтому для расшифровки контента обязательно использовать целый кластер файла (4K), а не размер, указанный в поле i_size айнода.

4. Реализация

Реализация дешифратора выполнена на основе Linux Kernel Crypto API [4]. В цепочке используется два вида шифраторов в зависимости от того, что прописано в /proc/crypto для алгоритмов ebc(aes), cts(cbc(aes)), xts(aes). Рассматриваем ядро 4.10.0-19: шифр ebc реализуется через blkcipher, cts(cbc) и xts — через skcipher:

$ cat /proc/crypto

$ cat /proc/crypto
name: ecb(aes)
driver: ecb(aes-aesni)
module: kernel
priority: 300
internal: no
type: blkcipher
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 0
geniv: default

name: cts(cbc(aes))
driver: cts(cbc-aes-aesni)
module: kernel
priority: 400
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 16
max keysize: 32
ivsize: 16
chunksize: 16

name: xts(aes)
driver: xts-aes-aesni
module: aesni_intel
priority: 401
internal: no
type: skcipher
async: yes
blocksize: 16
min keysize: 32
max keysize: 64
ivsize: 16
chunksize: 16

Реализация шифратора через blkcipher

typedef enum { ENCRYPT, DECRYPT } cipher_mode;

static int do_blkcrypt(const u8* cipher, const u8* key, u32 key_len, 
  void* iv, void* dst, void* src, size_t src_len, cipher_mode mode)
{
  int res;
  struct crypto_blkcipher* blk;
  struct blkcipher_desc desc;
  struct scatterlist sg_src, sg_dst;
 
  blk = crypto_alloc_blkcipher(cipher, 0, 0);
  if (IS_ERR(blk))
  {
    printk(KERN_WARNING "Failed to initialize blkcipher mode %sn", cipher);
    return PTR_ERR(blk);
  }
 
  res = crypto_blkcipher_setkey(blk, key, key_len);
  if (res)
  {
    printk(KERN_WARNING "Failed to set key. len=%#xn", key_len);
    crypto_free_blkcipher(blk);
    return res;
  }
 
  crypto_blkcipher_set_iv(blk, iv, 16);
 
  sg_init_one(&sg_src, src, src_len);
  sg_init_one(&sg_dst, dst, src_len);
 
  desc.tfm = blk;
  desc.flags = 0;
 
  if (mode == ENCRYPT)
    res = crypto_blkcipher_encrypt(&desc, &sg_dst, &sg_src, src_len);
  else
    res = crypto_blkcipher_decrypt(&desc, &sg_dst, &sg_src, src_len);
 
  crypto_free_blkcipher(blk);
 
  return res;
}

Реализация шифратора через skcipher

struct tcrypt_result {
    struct completion completion;
    int err;
};
 
static void crypt_complete_cb(struct crypto_async_request* req, int error)
{
    struct tcrypt_result* res = req->data;
 
    if (error == -EINPROGRESS)
        return;
 
    res->err = error;
    complete(&res->completion);
}
 
static int do_skcrypt(const u8* cipher, const u8* key, u32 key_len, 
  void* iv, void* dst, void* src, size_t src_len, cipher_mode mode)
{
    struct scatterlist          src_sg, dst_sg;
    struct crypto_skcipher*     tfm;
    struct skcipher_request*    req = 0;
    struct tcrypt_result        crypt_res;
    int res = -EFAULT;
 
    tfm = crypto_alloc_skcipher(cipher, 0, 0);
    if (IS_ERR(tfm))
    {
        printk(KERN_WARNING "Failed to initialize skcipher mode %sn", cipher);
        res = PTR_ERR(tfm);
        tfm = NULL;
        goto out;
    }
 
    req = skcipher_request_alloc(tfm, GFP_NOFS);
    if (!req)
    {
        printk(KERN_WARNING "Couldn't allocate skcipher handlen");
        res = -ENOMEM;
        goto out;
    }
 
    skcipher_request_set_callback(req, CRYPTO_TFM_REQ_MAY_BACKLOG | CRYPTO_TFM_REQ_MAY_SLEEP, 
                                  crypt_complete_cb, &crypt_res);
 
    if (crypto_skcipher_setkey(tfm, key, key_len))
    {
        printk(KERN_WARNING "Failed to set keyn");
        res = -EINVAL;
        goto out;
    }
 
    sg_init_one(&src_sg, src, src_len);
    sg_init_one(&dst_sg, dst, src_len);
 
    skcipher_request_set_crypt(req, &src_sg, &dst_sg, src_len, iv);
    init_completion(&crypt_res.completion);
 
    if (mode == ENCRYPT)
        res = crypto_skcipher_encrypt(req);
    else
        res = crypto_skcipher_decrypt(req);
 
    switch (res)
    {
    case 0: break;
    case -EINPROGRESS:
    case -EBUSY:
        wait_for_completion(&crypt_res.completion);
        if (!res && !crypt_res.err)
        {
            reinit_completion(&crypt_res.completion);
            break;
        }
    default:
        printk("Skcipher %scrypt returned with err = %d, result %#xn", 
mode == ENCRYPT ? "en" : "de", res, crypt_res.err);
        break;
    }
 
out:
    if (tfm)
        crypto_free_skcipher(tfm);
    if (req)
        skcipher_request_free(req);
 
    return res;
}

Чтение данных (payload) Мастер-ключа

#define MASTER_KEY_SIZE 64

static int GetMasterKey(const u8* descriptor, u8* raw)
{
  struct key* keyring_key = NULL;
  const struct user_key_payload* ukp;
  struct fscrypt_key* master_key;
 
  keyring_key = request_key(&key_type_logon, descriptor, NULL);
 
  if (IS_ERR(keyring_key))
    return -EINVAL;
 
  if (keyring_key->type != &key_type_logon)
  {
    printk_once(KERN_WARNING "%s: key type must be 'logon'n", __func__);
    return -EINVAL;
  }
 
  down_read(&keyring_key->sem);
  ukp = user_key_payload(keyring_key);
 
  master_key = (struct fscrypt_key*)ukp->data;
  up_read(&keyring_key->sem);
 
  if (master_key->size != MASTER_KEY_SIZE)
  {
    printk(KERN_WARNING "Wrong Master key size %#xn", master_key->size);
    return -EINVAL;
  }
 
  memcpy(raw, master_key->raw, master_key->size);
 
  return 0;
}

NOTE: В версиях ядра младше 4.4 отсутствует функция user_key_payload. Данные ключа можно прочитать непосредственно из struct key* keyring_key.

Расшифровка имени файла

int err;
u8 iv[16] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
u8 nonce_dir[16] = { ... };
u8 master_key[64], derived_key[64];
u8 dec_file_name[] = { ... };
u8 enc_file_name[sizeof(dec_file_name)];
 
err = do_blkcrypt("ecb(aes)", nonce_dir, 16, iv, derived_key, master_key, 
                  MASTER_KEY_SIZE, ENCRYPT);
if (err)
  return err;
 
err = do_skcrypt("cts(cbc(aes))", derived_key, MASTER_KEY_SIZE / 2, iv, 
                 dec_file_name, enc_file_name, sizeof(dec_file_name), DECRYPT);

return err;

Расшифровка контента

Для упрощения опущена работа с памятью. Предположим, 2 x PAGE_SIZE нам дали на стеке.

u8 nonce_file[16] = { ... };
u8 enc_file_data[PAGE_SIZE] = { ... };
u8 dec_file_data[PAGE_SIZE];
 
err = do_blkcrypt("ecb(aes)", nonce_file, 16, iv, derived_key, master_key, 
                  MASTER_KEY_SIZE, ENCRYPT);
if (err)
  return err;

err = do_skcrypt("xts(aes)", derived_key, MASTER_KEY_SIZE, iv, 
                  dec_file_data, enc_file_data, PAGE_SIZE, DECRYPT);
 
return err;

Используемые заголовочные файлы (актуально для 4.10.0-19)

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/scatterlist.h>
#include <linux/fscrypto.h>

Makefile

obj-m += ciphertest.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

5. Результаты

Исходные данные:

u8 master_key[MASTER_KEY_SIZE] = {
  0xa5, 0xb5, 0xc9, 0x23, 0x02, 0x14, 0xfc, 0xf7,
  0x28, 0xdc, 0x90, 0x25, 0x24, 0x9e, 0xe6, 0xbc,
  0x7c, 0xa8, 0xf8, 0xe1, 0x94, 0xf6, 0x67, 0x32,
  0x33, 0xc4, 0xc1, 0xe8, 0x78, 0x59, 0xab, 0xfb,
  0xae, 0xb0, 0xbf, 0x5d, 0x2c, 0x69, 0xc3, 0x8f,
  0x51, 0x37, 0x26, 0x3f, 0xd1, 0xce, 0x37, 0xef,
  0x3f, 0x80, 0xe3, 0x2d, 0xd5, 0xfd, 0x78, 0x45,
  0x62, 0xf3, 0xa5, 0x24, 0x6b, 0xcf, 0x4a, 0x88 
};
u8 enc_file_name[] = {
  0x41, 0xa8, 0x4e, 0x4d, 0xd4, 0x1c, 0x43, 0x00, 
  0xa7, 0x5a, 0x2f, 0xd5, 0xaa, 0xa0, 0x5d, 0xb0
};
u8 nonce_dir[] = {
  0x37, 0xba, 0x14, 0x16, 0x3e, 0xa8, 0xd5, 0x48, 
  0xd1, 0x3c, 0xb5, 0x6a, 0x01, 0xb7, 0x7c, 0x41
};
u8 nonce_file[] = {
  0x61, 0x63, 0xb8, 0x31, 0xf4, 0xf5, 0xfc, 0x99, 
  0x1e, 0x3c, 0xf1, 0x8a, 0x23, 0xaf, 0x1e, 0xa8
};

Закодированное имя файла enc_file_name получено из дампа директории (рис. 3).
Нонс директории nonce_dir получен из дампа айнода директории (рис. 6)
Нонс файла nonce_file получен из дампа айнода файла (рис. 7)

Мастер-ключ показан здесь полностью для наглядности. Его можно получить при отладке e4crypt:

image

Результат работы созданного драйвера

image

Ссылки

Автор: shs

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js