├── Makefile ├── README.md ├── .gitignore ├── LICENSE └── immutable_file.c /Makefile: -------------------------------------------------------------------------------- 1 | KERNEL_PATH ?= /lib/modules/$(shell uname -r)/build 2 | 3 | obj-m += immutable_file.o 4 | 5 | all: 6 | make -C $(KERNEL_PATH) M=$(PWD) modules 7 | 8 | clean: 9 | make -C $(KERNEL_PATH) M=$(PWD) clean 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Immutable-file-linux 2 | A small fun project to make any file in linux system immutable/non writable by hooking `sys_openat` and `sys_write` system calls using ftrace. 3 | 4 | ## Instructions 5 | * Change the target file path(default set to /tmp/test.txt) in `fh_sys_openat` funtion. 6 | * Run `make` from terminal 7 | * Load the module using `sudo insmod immutable_file.ko` 8 | 9 | Reference for: https://nixhacker.com/hooking-syscalls-in-linux-using-ftrace 10 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Shubham Dubey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /immutable_file.c: -------------------------------------------------------------------------------- 1 | 2 | #define pr_fmt(fmt) "immutable_file: " fmt 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | MODULE_DESCRIPTION("Protect write to a file and kill the responsible process"); 22 | MODULE_AUTHOR("Shubham "); 23 | MODULE_LICENSE("GPL 3"); 24 | 25 | unsigned int target_fd = 0; 26 | unsigned int target_pid = 0; 27 | #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0) 28 | static unsigned long lookup_name(const char *name) 29 | { 30 | struct kprobe kp = { 31 | .symbol_name = name 32 | }; 33 | unsigned long retval; 34 | 35 | if (register_kprobe(&kp) < 0) return 0; 36 | retval = (unsigned long) kp.addr; 37 | unregister_kprobe(&kp); 38 | return retval; 39 | } 40 | #else 41 | static unsigned long lookup_name(const char *name) 42 | { 43 | return kallsyms_lookup_name(name); 44 | } 45 | #endif 46 | 47 | #if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0) 48 | #define FTRACE_OPS_FL_RECURSION FTRACE_OPS_FL_RECURSION_SAFE 49 | #endif 50 | 51 | #if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0) 52 | #define ftrace_regs pt_regs 53 | 54 | static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs) 55 | { 56 | return fregs; 57 | } 58 | #endif 59 | 60 | /* 61 | * There are two ways of preventing vicious recursive loops when hooking: 62 | * - detect recusion using function return address (USE_FENTRY_OFFSET = 0) 63 | * - avoid recusion by jumping over the ftrace call (USE_FENTRY_OFFSET = 1) 64 | */ 65 | #define USE_FENTRY_OFFSET 0 66 | 67 | /** 68 | * struct ftrace_hook - describes a single hook to install 69 | * 70 | * @name: name of the function to hook 71 | * 72 | * @function: pointer to the function to execute instead 73 | * 74 | * @original: pointer to the location where to save a pointer 75 | * to the original function 76 | * 77 | * @address: kernel address of the function entry 78 | * 79 | * @ops: ftrace_ops state for this function hook 80 | * 81 | * The user should fill in only &name, &hook, &orig fields. 82 | * Other fields are considered implementation details. 83 | */ 84 | struct ftrace_hook { 85 | const char *name; 86 | void *function; 87 | void *original; 88 | 89 | unsigned long address; 90 | struct ftrace_ops ops; 91 | }; 92 | 93 | static int fh_resolve_hook_address(struct ftrace_hook *hook) 94 | { 95 | hook->address = lookup_name(hook->name); 96 | 97 | if (!hook->address) { 98 | pr_debug("unresolved symbol: %s\n", hook->name); 99 | return -ENOENT; 100 | } 101 | 102 | #if USE_FENTRY_OFFSET 103 | *((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE; 104 | #else 105 | *((unsigned long*) hook->original) = hook->address; 106 | #endif 107 | 108 | return 0; 109 | } 110 | 111 | static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip, 112 | struct ftrace_ops *ops, struct ftrace_regs *fregs) 113 | { 114 | struct pt_regs *regs = ftrace_get_regs(fregs); 115 | struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops); 116 | 117 | #if USE_FENTRY_OFFSET 118 | regs->ip = (unsigned long)hook->function; 119 | #else 120 | if (!within_module(parent_ip, THIS_MODULE)) 121 | regs->ip = (unsigned long)hook->function; 122 | #endif 123 | } 124 | 125 | /** 126 | * fh_install_hooks() - register and enable a single hook 127 | * @hook: a hook to install 128 | * 129 | * Returns: zero on success, negative error code otherwise. 130 | */ 131 | int fh_install_hook(struct ftrace_hook *hook) 132 | { 133 | int err; 134 | 135 | err = fh_resolve_hook_address(hook); 136 | if (err) 137 | return err; 138 | 139 | /* 140 | * We're going to modify %rip register so we'll need IPMODIFY flag 141 | * and SAVE_REGS as its prerequisite. ftrace's anti-recursion guard 142 | * is useless if we change %rip so disable it with RECURSION. 143 | * We'll perform our own checks for trace function reentry. 144 | */ 145 | hook->ops.func = fh_ftrace_thunk; 146 | hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS 147 | | FTRACE_OPS_FL_RECURSION 148 | | FTRACE_OPS_FL_IPMODIFY; 149 | 150 | err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0); 151 | if (err) { 152 | pr_debug("ftrace_set_filter_ip() failed: %d\n", err); 153 | return err; 154 | } 155 | 156 | err = register_ftrace_function(&hook->ops); 157 | if (err) { 158 | pr_debug("register_ftrace_function() failed: %d\n", err); 159 | ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); 160 | return err; 161 | } 162 | 163 | return 0; 164 | } 165 | 166 | /** 167 | * fh_remove_hooks() - disable and unregister a single hook 168 | * @hook: a hook to remove 169 | */ 170 | void fh_remove_hook(struct ftrace_hook *hook) 171 | { 172 | int err; 173 | 174 | err = unregister_ftrace_function(&hook->ops); 175 | if (err) { 176 | pr_debug("unregister_ftrace_function() failed: %d\n", err); 177 | } 178 | 179 | err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0); 180 | if (err) { 181 | pr_debug("ftrace_set_filter_ip() failed: %d\n", err); 182 | } 183 | } 184 | 185 | /** 186 | * fh_install_hooks() - register and enable multiple hooks 187 | * @hooks: array of hooks to install 188 | * @count: number of hooks to install 189 | * 190 | * If some hooks fail to install then all hooks will be removed. 191 | * 192 | * Returns: zero on success, negative error code otherwise. 193 | */ 194 | int fh_install_hooks(struct ftrace_hook *hooks, size_t count) 195 | { 196 | int err; 197 | size_t i; 198 | 199 | for (i = 0; i < count; i++) { 200 | err = fh_install_hook(&hooks[i]); 201 | if (err) 202 | goto error; 203 | } 204 | 205 | return 0; 206 | 207 | error: 208 | while (i != 0) { 209 | fh_remove_hook(&hooks[--i]); 210 | } 211 | 212 | return err; 213 | } 214 | 215 | /** 216 | * fh_remove_hooks() - disable and unregister multiple hooks 217 | * @hooks: array of hooks to remove 218 | * @count: number of hooks to remove 219 | */ 220 | void fh_remove_hooks(struct ftrace_hook *hooks, size_t count) 221 | { 222 | size_t i; 223 | 224 | for (i = 0; i < count; i++) 225 | fh_remove_hook(&hooks[i]); 226 | } 227 | 228 | #ifndef CONFIG_X86_64 229 | #error Currently only x86_64 architecture is supported 230 | #endif 231 | 232 | #if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0)) 233 | #define PTREGS_SYSCALL_STUBS 1 234 | #endif 235 | 236 | /* 237 | * Tail call optimization can interfere with recursion detection based on 238 | * return address on the stack. Disable it to avoid machine hangups. 239 | */ 240 | #if !USE_FENTRY_OFFSET 241 | #pragma GCC optimize("-fno-optimize-sibling-calls") 242 | #endif 243 | 244 | static char *duplicate_filename(const char __user *filename) 245 | { 246 | char *kernel_filename; 247 | 248 | kernel_filename = kmalloc(4096, GFP_KERNEL); 249 | if (!kernel_filename) 250 | return NULL; 251 | 252 | if (strncpy_from_user(kernel_filename, filename, 4096) < 0) { 253 | kfree(kernel_filename); 254 | return NULL; 255 | } 256 | 257 | return kernel_filename; 258 | } 259 | 260 | 261 | #ifdef PTREGS_SYSCALL_STUBS 262 | static asmlinkage long (*real_sys_write)(struct pt_regs *regs); 263 | 264 | static asmlinkage long fh_sys_write(struct pt_regs *regs) 265 | { 266 | long ret; 267 | struct task_struct *task; 268 | task = current; 269 | int signum = SIGKILL; 270 | if (task->pid == target_pid) 271 | { 272 | if (regs->di == target_fd) 273 | { 274 | pr_info("write done by process %d to target file.\n", task->pid); 275 | struct kernel_siginfo info; 276 | memset(&info, 0, sizeof(struct kernel_siginfo)); 277 | info.si_signo = signum; 278 | int ret = send_sig_info(signum, &info, task); 279 | if (ret < 0) 280 | { 281 | printk(KERN_INFO "error sending signal\n"); 282 | } 283 | else 284 | { 285 | printk(KERN_INFO "Target has been killed\n"); 286 | return 0; 287 | } 288 | } 289 | } 290 | 291 | ret = real_sys_write(regs); 292 | 293 | return ret; 294 | } 295 | #else 296 | static asmlinkage long (*real_sys_write)(unsigned int fd, const char __user *buf, 297 | size_t count); 298 | 299 | static asmlinkage long fh_sys_write(unsigned int fd, const char __user *buf, 300 | size_t count) 301 | { 302 | long ret; 303 | struct task_struct *task; 304 | task = current; 305 | int signum = SIGKILL; 306 | if (task->pid == target_pid) 307 | { 308 | if (fd == target_fd) 309 | { 310 | pr_info("write done by process %d to target file.\n", task->pid); 311 | struct kernel_siginfo info; 312 | memset(&info, 0, sizeof(struct kernel_siginfo)); 313 | info.si_signo = signum; 314 | int ret = send_sig_info(signum, &info, task); 315 | if (ret < 0) 316 | { 317 | printk(KERN_INFO "error sending signal\n"); 318 | } 319 | else 320 | { 321 | printk(KERN_INFO "Target has been killed\n"); 322 | return 0; 323 | } 324 | } 325 | } 326 | 327 | ret = real_sys_write(fd, buf, count); 328 | 329 | 330 | return ret; 331 | } 332 | #endif 333 | 334 | 335 | #ifdef PTREGS_SYSCALL_STUBS 336 | static asmlinkage long (*real_sys_openat)(struct pt_regs *regs); 337 | 338 | static asmlinkage long fh_sys_openat(struct pt_regs *regs) 339 | { 340 | long ret; 341 | char *kernel_filename; 342 | struct task_struct *task; 343 | task = current; 344 | 345 | kernel_filename = duplicate_filename((void*) regs->si); 346 | if (strncmp(kernel_filename, "/tmp/test.txt", 13) == 0) 347 | { 348 | pr_info("our file is opened by process with id: %d\n", task->pid); 349 | pr_info("opened file : %s\n", kernel_filename); 350 | kfree(kernel_filename); 351 | ret = real_sys_openat(regs); 352 | pr_info("fd returned is %ld\n", ret); 353 | target_fd = ret; 354 | target_pid = task->pid; 355 | return ret; 356 | 357 | } 358 | 359 | kfree(kernel_filename); 360 | ret = real_sys_openat(regs); 361 | 362 | return ret; 363 | } 364 | #else 365 | static asmlinkage long (*real_sys_openat)(int dfd, const char __user *filename, 366 | int flags, umode_t mode); 367 | 368 | static asmlinkage long fh_sys_openat(int dfd, const char __user *filename, 369 | int flags, umode_t mode) 370 | { 371 | long ret; 372 | char *kernel_filename; 373 | struct task_struct *task; 374 | task = current; 375 | 376 | kernel_filename = duplicate_filename(filename); 377 | if (strncmp(kernel_filename, "/tmp/test.txt", 13) == 0) 378 | { 379 | pr_info("our file is opened by process with id: %d\n", task->pid); 380 | pr_info("opened file : %s\n", kernel_filename); 381 | kfree(kernel_filename); 382 | ret = real_sys_openat(dfd, filename, flags, mode); 383 | pr_info("fd returned is %ld\n", ret); 384 | target_fd = ret; 385 | target_pid = task->pid; 386 | return ret; 387 | 388 | } 389 | 390 | kfree(kernel_filename); 391 | 392 | ret = real_sys_openat(filename, flags, mode); 393 | 394 | return ret; 395 | } 396 | #endif 397 | 398 | 399 | /* 400 | * x86_64 kernels have a special naming convention for syscall entry points in newer kernels. 401 | * That's what you end up with if an architecture has 3 (three) ABIs for system calls. 402 | */ 403 | #ifdef PTREGS_SYSCALL_STUBS 404 | #define SYSCALL_NAME(name) ("__x64_" name) 405 | #else 406 | #define SYSCALL_NAME(name) (name) 407 | #endif 408 | 409 | #define HOOK(_name, _function, _original) \ 410 | { \ 411 | .name = SYSCALL_NAME(_name), \ 412 | .function = (_function), \ 413 | .original = (_original), \ 414 | } 415 | 416 | static struct ftrace_hook demo_hooks[] = { 417 | HOOK("sys_write", fh_sys_write, &real_sys_write), 418 | HOOK("sys_openat", fh_sys_openat, &real_sys_openat), 419 | }; 420 | 421 | static int fh_init(void) 422 | { 423 | int err; 424 | 425 | err = fh_install_hooks(demo_hooks, ARRAY_SIZE(demo_hooks)); 426 | if (err) 427 | return err; 428 | 429 | pr_info("module loaded\n"); 430 | 431 | return 0; 432 | } 433 | module_init(fh_init); 434 | 435 | static void fh_exit(void) 436 | { 437 | fh_remove_hooks(demo_hooks, ARRAY_SIZE(demo_hooks)); 438 | 439 | pr_info("module unloaded\n"); 440 | } 441 | module_exit(fh_exit); 442 | --------------------------------------------------------------------------------