Commit inicial
This commit is contained in:
commit
efe24cca72
5 changed files with 483 additions and 0 deletions
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2023 niveb
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
9
Makefile
Normal file
9
Makefile
Normal file
|
@ -0,0 +1,9 @@
|
|||
KERNEL_PATH ?= /lib/modules/$(shell uname -r)/build
|
||||
|
||||
obj-m += nocrypt.o
|
||||
|
||||
all:
|
||||
make -C $(KERNEL_PATH) M=$(PWD) modules
|
||||
|
||||
clean:
|
||||
make -C $(KERNEL_PATH) M=$(PWD) clean
|
28
README.md
Normal file
28
README.md
Normal file
|
@ -0,0 +1,28 @@
|
|||
# NoCrypt - AntiRansomware Linux Kernel Module
|
||||
A small experimental project to make a defense tool to prevent ransomware attacks on Linux systems.
|
||||
|
||||
The module hooks the system call `sys_rename` using ftrace to monitor all the files renamed on the system.
|
||||
|
||||
Ransomware often encrypt a lot of files renaming them with the same suffix.
|
||||
NoCrypt checks if a process renames a file with a known ransomware suffix, killing the process and printing a log in the message buffer of the kernel (use dmesg to view it).
|
||||
|
||||
NoCrypt has also a small behaviour check, if the same (parent) process starts to rename many files, after 12 renamed files by default, it'll be killed.
|
||||
The module monitors the parent task because often ransomware are command line tools with multiple threads (different task structs).
|
||||
|
||||
**Consider this project a proof-of-concept you can easily customize for your needs. Actually it is not recommended to put it in production.**
|
||||
|
||||
|
||||
Thanks to Immutable-file-linux project of Shubham Dubey
|
||||
|
||||
Reference for: https://nixhacker.com/hooking-syscalls-in-linux-using-ftrace
|
||||
|
||||
|
||||
## Instructions
|
||||
* Run `make` from terminal
|
||||
* Load the module using `sudo insmod nocrypt.ko "max_rename=12" "behaviour_detection=true"`
|
||||
|
||||
## Example
|
||||
Compile the example program `gcc -o example example.c`.
|
||||
Now run it `.\example`, it will try to rename several test files into .lockbit.
|
||||
Even if the input files don't exist, NoCrypt will kill the process because it's trying to rename files with a blacklisted extension.
|
||||
Check the module output with `sudo dmesg`
|
14
example.c
Normal file
14
example.c
Normal file
|
@ -0,0 +1,14 @@
|
|||
#include <stdio.h>
|
||||
|
||||
int main(){
|
||||
int i;
|
||||
char origname[20];
|
||||
char destname[20];
|
||||
for (i = 0; i < 20; i++) {
|
||||
sprintf(origname, "test%d", i);
|
||||
sprintf(destname, "test%d.lockbit", i);
|
||||
rename(origname, destname);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
411
nocrypt.c
Normal file
411
nocrypt.c
Normal file
|
@ -0,0 +1,411 @@
|
|||
|
||||
#define pr_fmt(fmt) "nocrypt: " fmt
|
||||
|
||||
#include <linux/ftrace.h>
|
||||
#include <linux/kallsyms.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <linux/linkage.h>
|
||||
#include <linux/module.h>
|
||||
#include <linux/slab.h>
|
||||
#include <linux/uaccess.h>
|
||||
#include <linux/version.h>
|
||||
#include <linux/kprobes.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/kthread.h>
|
||||
#include <linux/kernel.h>
|
||||
#include <asm/signal.h>
|
||||
#include <linux/delay.h>
|
||||
#include <linux/sched.h>
|
||||
#include <linux/sched/signal.h>
|
||||
#include <linux/mm.h>
|
||||
|
||||
MODULE_DESCRIPTION("Detect and kill ransomware");
|
||||
MODULE_AUTHOR("niveb");
|
||||
MODULE_LICENSE("GPL");
|
||||
|
||||
unsigned int max_rename = 12;
|
||||
module_param(max_rename, int, 0);
|
||||
bool behaviour_detection = false;
|
||||
module_param(behaviour_detection, bool, 0);
|
||||
|
||||
unsigned int rename_count = 0;
|
||||
unsigned int target_pid = 0;
|
||||
//Add here your custom extensions to block
|
||||
#define BLACKLIST_SIZE 7
|
||||
char *blacklist_ext[] = {"Clop","iFire","conti","monti","PUUUK", "Cheers","lockbit"};
|
||||
|
||||
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5,7,0)
|
||||
static unsigned long lookup_name(const char *name)
|
||||
{
|
||||
struct kprobe kp = {
|
||||
.symbol_name = name
|
||||
};
|
||||
unsigned long retval;
|
||||
|
||||
if (register_kprobe(&kp) < 0) return 0;
|
||||
retval = (unsigned long) kp.addr;
|
||||
unregister_kprobe(&kp);
|
||||
return retval;
|
||||
}
|
||||
#else
|
||||
static unsigned long lookup_name(const char *name)
|
||||
{
|
||||
return kallsyms_lookup_name(name);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
|
||||
#define FTRACE_OPS_FL_RECURSION FTRACE_OPS_FL_RECURSION_SAFE
|
||||
#endif
|
||||
|
||||
#if LINUX_VERSION_CODE < KERNEL_VERSION(5,11,0)
|
||||
#define ftrace_regs pt_regs
|
||||
|
||||
static __always_inline struct pt_regs *ftrace_get_regs(struct ftrace_regs *fregs)
|
||||
{
|
||||
return fregs;
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* There are two ways of preventing vicious recursive loops when hooking:
|
||||
* - detect recusion using function return address (USE_FENTRY_OFFSET = 0)
|
||||
* - avoid recusion by jumping over the ftrace call (USE_FENTRY_OFFSET = 1)
|
||||
*/
|
||||
#define USE_FENTRY_OFFSET 0
|
||||
|
||||
/**
|
||||
* struct ftrace_hook - describes a single hook to install
|
||||
*
|
||||
* @name: name of the function to hook
|
||||
*
|
||||
* @function: pointer to the function to execute instead
|
||||
*
|
||||
* @original: pointer to the location where to save a pointer
|
||||
* to the original function
|
||||
*
|
||||
* @address: kernel address of the function entry
|
||||
*
|
||||
* @ops: ftrace_ops state for this function hook
|
||||
*
|
||||
* The user should fill in only &name, &hook, &orig fields.
|
||||
* Other fields are considered implementation details.
|
||||
*/
|
||||
struct ftrace_hook {
|
||||
const char *name;
|
||||
void *function;
|
||||
void *original;
|
||||
|
||||
unsigned long address;
|
||||
struct ftrace_ops ops;
|
||||
};
|
||||
|
||||
static int fh_resolve_hook_address(struct ftrace_hook *hook)
|
||||
{
|
||||
hook->address = lookup_name(hook->name);
|
||||
|
||||
if (!hook->address) {
|
||||
pr_debug("unresolved symbol: %s\n", hook->name);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
#if USE_FENTRY_OFFSET
|
||||
*((unsigned long*) hook->original) = hook->address + MCOUNT_INSN_SIZE;
|
||||
#else
|
||||
*((unsigned long*) hook->original) = hook->address;
|
||||
#endif
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void notrace fh_ftrace_thunk(unsigned long ip, unsigned long parent_ip,
|
||||
struct ftrace_ops *ops, struct ftrace_regs *fregs)
|
||||
{
|
||||
struct pt_regs *regs = ftrace_get_regs(fregs);
|
||||
struct ftrace_hook *hook = container_of(ops, struct ftrace_hook, ops);
|
||||
|
||||
#if USE_FENTRY_OFFSET
|
||||
regs->ip = (unsigned long)hook->function;
|
||||
#else
|
||||
if (!within_module(parent_ip, THIS_MODULE))
|
||||
regs->ip = (unsigned long)hook->function;
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
* fh_install_hooks() - register and enable a single hook
|
||||
* @hook: a hook to install
|
||||
*
|
||||
* Returns: zero on success, negative error code otherwise.
|
||||
*/
|
||||
int fh_install_hook(struct ftrace_hook *hook)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = fh_resolve_hook_address(hook);
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
/*
|
||||
* We're going to modify %rip register so we'll need IPMODIFY flag
|
||||
* and SAVE_REGS as its prerequisite. ftrace's anti-recursion guard
|
||||
* is useless if we change %rip so disable it with RECURSION.
|
||||
* We'll perform our own checks for trace function reentry.
|
||||
*/
|
||||
hook->ops.func = fh_ftrace_thunk;
|
||||
hook->ops.flags = FTRACE_OPS_FL_SAVE_REGS
|
||||
| FTRACE_OPS_FL_RECURSION
|
||||
| FTRACE_OPS_FL_IPMODIFY;
|
||||
|
||||
err = ftrace_set_filter_ip(&hook->ops, hook->address, 0, 0);
|
||||
if (err) {
|
||||
pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
|
||||
return err;
|
||||
}
|
||||
|
||||
err = register_ftrace_function(&hook->ops);
|
||||
if (err) {
|
||||
pr_debug("register_ftrace_function() failed: %d\n", err);
|
||||
ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* fh_remove_hooks() - disable and unregister a single hook
|
||||
* @hook: a hook to remove
|
||||
*/
|
||||
void fh_remove_hook(struct ftrace_hook *hook)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = unregister_ftrace_function(&hook->ops);
|
||||
if (err) {
|
||||
pr_debug("unregister_ftrace_function() failed: %d\n", err);
|
||||
}
|
||||
|
||||
err = ftrace_set_filter_ip(&hook->ops, hook->address, 1, 0);
|
||||
if (err) {
|
||||
pr_debug("ftrace_set_filter_ip() failed: %d\n", err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fh_install_hooks() - register and enable multiple hooks
|
||||
* @hooks: array of hooks to install
|
||||
* @count: number of hooks to install
|
||||
*
|
||||
* If some hooks fail to install then all hooks will be removed.
|
||||
*
|
||||
* Returns: zero on success, negative error code otherwise.
|
||||
*/
|
||||
int fh_install_hooks(struct ftrace_hook *hooks, size_t count)
|
||||
{
|
||||
int err;
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < count; i++) {
|
||||
err = fh_install_hook(&hooks[i]);
|
||||
if (err)
|
||||
goto error;
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
error:
|
||||
while (i != 0) {
|
||||
fh_remove_hook(&hooks[--i]);
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
/**
|
||||
* fh_remove_hooks() - disable and unregister multiple hooks
|
||||
* @hooks: array of hooks to remove
|
||||
* @count: number of hooks to remove
|
||||
*/
|
||||
void fh_remove_hooks(struct ftrace_hook *hooks, size_t count)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
fh_remove_hook(&hooks[i]);
|
||||
}
|
||||
|
||||
#ifndef CONFIG_X86_64
|
||||
#error Currently only x86_64 architecture is supported
|
||||
#endif
|
||||
|
||||
#if defined(CONFIG_X86_64) && (LINUX_VERSION_CODE >= KERNEL_VERSION(4,17,0))
|
||||
#define PTREGS_SYSCALL_STUBS 1
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Tail call optimization can interfere with recursion detection based on
|
||||
* return address on the stack. Disable it to avoid machine hangups.
|
||||
*/
|
||||
#if !USE_FENTRY_OFFSET
|
||||
#pragma GCC optimize("-fno-optimize-sibling-calls")
|
||||
#endif
|
||||
|
||||
static char *duplicate_filename(const char __user *filename)
|
||||
{
|
||||
char *kernel_filename;
|
||||
|
||||
kernel_filename = kmalloc(4096, GFP_KERNEL);
|
||||
if (!kernel_filename)
|
||||
return NULL;
|
||||
|
||||
if (strncpy_from_user(kernel_filename, filename, 4096) < 0) {
|
||||
kfree(kernel_filename);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return kernel_filename;
|
||||
}
|
||||
|
||||
/* Send SIGKILL to the input task */
|
||||
static bool kill_task(struct task_struct *task) {
|
||||
int signum = SIGKILL;
|
||||
struct kernel_siginfo info;
|
||||
memset(&info, 0, sizeof(struct kernel_siginfo));
|
||||
info.si_signo = signum;
|
||||
int ret = send_sig_info(signum, &info, task);
|
||||
if (ret < 0)
|
||||
{
|
||||
printk(KERN_INFO "error sending signal to %d\n", target_pid);
|
||||
return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
printk(KERN_INFO "Target pid %d has been killed\n", target_pid);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if the renaming operation is linked to a ransomware behaviour or not.
|
||||
* Returns true if the operation is allowed
|
||||
* Kill the process and returns false if the operation is not allowed
|
||||
*/
|
||||
static bool check_rename(char *oldname, char *newname) {
|
||||
struct task_struct *task;
|
||||
task = current;
|
||||
// we use parent pid because the tasks can be threads
|
||||
if (target_pid == task->real_parent->pid) {
|
||||
rename_count++;
|
||||
} else {
|
||||
target_pid = task->real_parent->pid;
|
||||
rename_count = 0;
|
||||
}
|
||||
//Check for specific known extensions
|
||||
//Find null terminating char
|
||||
int index = 0;
|
||||
int point_index = 0;
|
||||
int nmax = 200;
|
||||
//loop max nmax times
|
||||
for (index = 0; index < nmax; index++) {
|
||||
if (newname[index] == 0)
|
||||
break;
|
||||
else if (newname[index] == '.') {
|
||||
point_index = index;
|
||||
}
|
||||
}
|
||||
if ((point_index > 0) && (index < nmax)) {
|
||||
char *extension = newname+point_index+1;
|
||||
for (int i = 0; i < BLACKLIST_SIZE; i++) {
|
||||
if (strcmp(extension,blacklist_ext[i]) == 0) {
|
||||
pr_info("%s ransomware detected (renaming %s to %s)\n", extension, oldname, newname);
|
||||
kill_task(task);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Behaviour check
|
||||
if (behaviour_detection) {
|
||||
// if the same process pid is renaming more than n files, kill it
|
||||
if (rename_count >= max_rename) {
|
||||
pr_info("process %d detected as possible ransomware, renaming too much files (e.g. %s to %s)\n", target_pid, oldname, newname);
|
||||
kill_task(task);
|
||||
rename_count = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef PTREGS_SYSCALL_STUBS
|
||||
static asmlinkage long (*real_sys_rename)(struct pt_regs *regs);
|
||||
|
||||
static asmlinkage long fh_sys_rename(struct pt_regs *regs)
|
||||
{
|
||||
long ret = 0;
|
||||
char *oldname = (char*)regs->di;
|
||||
char *newname = (char*)regs->si;
|
||||
if (check_rename(oldname, newname)) {
|
||||
ret = real_sys_rename(regs);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#else
|
||||
static asmlinkage long (*real_sys_rename) (const char __user *oldname, const char __user *newname);
|
||||
|
||||
static asmlinkage long fh_sys_rename(const char __user *oldname, const char __user *newname)
|
||||
{
|
||||
long ret = 0;
|
||||
if (check_rename(oldname, newname)) {
|
||||
ret = real_sys_rename(oldname, newname);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
/*
|
||||
* x86_64 kernels have a special naming convention for syscall entry points in newer kernels.
|
||||
* That's what you end up with if an architecture has 3 (three) ABIs for system calls.
|
||||
*/
|
||||
#ifdef PTREGS_SYSCALL_STUBS
|
||||
#define SYSCALL_NAME(name) ("__x64_" name)
|
||||
#else
|
||||
#define SYSCALL_NAME(name) (name)
|
||||
#endif
|
||||
|
||||
#define HOOK(_name, _function, _original) \
|
||||
{ \
|
||||
.name = SYSCALL_NAME(_name), \
|
||||
.function = (_function), \
|
||||
.original = (_original), \
|
||||
}
|
||||
|
||||
static struct ftrace_hook demo_hooks[] = {
|
||||
HOOK("sys_rename", fh_sys_rename, &real_sys_rename),
|
||||
};
|
||||
|
||||
static int fh_init(void)
|
||||
{
|
||||
int err;
|
||||
|
||||
err = fh_install_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));
|
||||
if (err)
|
||||
return err;
|
||||
|
||||
pr_info("nocrypt loaded (max_rename=%d,behaviour_detection=%d)\n",max_rename,behaviour_detection);
|
||||
|
||||
return 0;
|
||||
}
|
||||
module_init(fh_init);
|
||||
|
||||
static void fh_exit(void)
|
||||
{
|
||||
fh_remove_hooks(demo_hooks, ARRAY_SIZE(demo_hooks));
|
||||
|
||||
pr_info("nocrypt unloaded\n");
|
||||
}
|
||||
module_exit(fh_exit);
|
Loading…
Reference in a new issue