背景信息

网络攻击者通常会利用被攻击网站中存在的漏洞,通过在网页中植入非法暗链对网页内容进行篡改等方式,进行非法牟利或者恶意商业攻击等活动。网页被恶意篡改会影响用户正常访问网页内容,还可能会导致严重的经济损失、品牌损失甚至是政治风险。

需求及实现

网页防篡改可实时监控网站目录并通过备份恢复被篡改的文件或目录,保障重要系统的网站、系统信息不被恶意篡改,防止出现挂马、黑链、非法植入恐怖威胁、色情等内容。
防篡改支持将Linux服务器进程加入白名单,可实现进程级、目录级、文件类型的系统防护。
防篡改技术基于Linux Kernel技术进行的模块开发,相比fanotify、audit、云防护,性能损耗极低(毫秒级),效果最优。
防篡改经验证,可有效控制动态脚本文件的写入,结合相关动态脚本保护模块,可达到几乎100%的后门防护能力,如果深入开发防提权模块,可以做到100%防后门、防提权,起到到无后门的防护作用。

流程

graph TD

A(内核加载) --> J["获取HOOK表、关闭写保护"];
J --> B[hook sys method];
B --> C[Write];
B --> D[Read];
B --> E["Unlink(Unlinkat)"];
B --> F["Mkdir(Mkdirat)"];
B --> G["Creat"];
B --> H["Rmdir"];
B --> I["Move"];
C --> K["开启写保护、保存原hook函数"];
D --> K["开启写保护、保存原hook函数"];
E --> K["开启写保护、保存原hook函数"];
F --> K["开启写保护、保存原hook函数"];
G --> K["开启写保护、保存原hook函数"];
H --> K["开启写保护、保存原hook函数"];
I --> K["开启写保护、保存原hook函数"];
K --> O["等待触发相关操作"];
O --> P{"根据传递的参数FD、Path和process确定权限"};
P --> |权限允许| Q[调用并返回原hook函数]
P -- 权限不允许 --> R["记录日志并返回-EACCESS"]

技术实现

系统底层操作劫持

防篡改模块的本质其实和rootkit类似,但不同的是,rootkit最后的目的是隐藏后门以及防止被发现入侵,而防篡改的目的是防护系统的文件操作。
基于Linux Kernel 3.10、4.18,通过register_kprobe方法注册kallsyms_lookup_name探针,检测系统环境是否可以获取sys_call_table,如果可以,则获取到sys_call_table地址,并且根据对应的文件创建、删除、修改等系统底层函数的寄存器地址进行替换,将自定义的函数内容地址改写到对应寄存器上。


int get_kallsyms_lookup_name(void)
{
    int ret = register_kprobe(&kp);
    if(unlikely(ret < 0)){
        printk(KERN_ERR "%s %s. register_kprobe failed, ret:%d\n", LKM_INFO, __FUNCTION__, ret);
        return ret;
    }
    printk("%s %s. kprobe at addr:%p, ret:%d\n", LKM_INFO, __FUNCTION__, kp.addr, ret);
    orig_kallsyms_lookup_name = (kallsyms_lookup_name_t)(void*)kp.addr;
    unregister_kprobe(&kp);

    return (orig_kallsyms_lookup_name!=NULL)?0:-1;
}

调用:

if(get_kallsyms_lookup_name() < 0){
        printk(KERN_ERR "%s %s failed, load my lkm faild!\n", LKM_INFO, __FUNCTION__);
        return -1;
}
sys_call_table_ptr = (void**)orig_kallsyms_lookup_name("sys_call_table");

如rmdir对应的系统底层寄存器位置常量为__NR_rmdir,rm为__NR_unlinkat,mkdir为__NR_mkdir等。
(见/usr/include/asm*/unistd.h)

其对应操作的底层函数(函数名一般为sys_+操作名,如sys_unlinkat)以及具体参数可以参考linux/syscalls.h:

97.png

系统函数操作实现

比如上述的sys_unlinkat(为什么反复强调unlinkat?)

规则匹配函数

定义规则匹配函数,用于加载遍历规则,并匹配与当前目录进程是否符合拦截条件。

int is_path_protected(const char *path, ProtectType ptype) {
    struct fileList *p;
    // conivent_printf("%s", path);
    char * dash = path;
    strcat(dash, "/");
    if (protect_enabeled) {
        for (p = fileList_root; p; p = p->next) {
            //conivent_printf("%s,%s,%d,%d", dash, p->filePath, strcmp(path, p->filePath), strcmp(dash, p->filePath));
            conivent_printf("%s", path);
            //if ((strcmp(path, p->filePath) == 0 || strcmp(dash, p->filePath) == 0) && p->type == ptype) {
            if ((is_begin_with(p->filePath, path) == 1 ||is_begin_with(p->filePath, dash) == 1) && p->type == ptype) {
                //sendProtectNotiMsg(p);
                return 1;
            }
        }
    }
    return 0;
}

Debug、编写程序遇到的问题

函数参数对不上

基于实例开发会有多种参数对不上情况:

  1. proc_entry_create 创建函数在多个阶段性版本都会有改变(截止到5.22版本,proc又有新的改变)
  2. netlink同理
  3. 4.17后且64位的机器,在调用sys_系列函数时,不再直接传递,而是使用结构体pregs的寄存器来获取内存中的参数
  4. sys_hook_table 在低版本中,需要读取寄存器内容再解析是否为sys hook table,在3.10后采取prob探针获取(见上)
  5. 除参数4.17后获取方式做了改变,3.10以及4之后的版本可以通过查阅syscalls头文件得知其对应的参数

容易被"../"这种形式绕过

经过测试,市面上如宝塔的内核防篡改有存在php调用unlink("../../../")绕出网站目录的情况,
经过对unlinkat、openat等函数的参数进行测试,
在系统底层会有一个fs->pwd的这种函数,用于获取当前pwd目录,
形如unlinkat/openat这种函数会传递至少两个参数,第一个为fd,可以理解为文件ID,另外一个即目录,比如"../../../index.html",
不管文件如何绕过,落实到底部还是会被自动解析出磁盘inode位置,也可以说是文件ID(这里我的理解还不够透彻),文件ID可以使用d_path获取到绝对目录,即"/www/wwwroot/dycom/index.html",假如此时FD为-100,说明为当前目录,那么也可以直接通过pathname解析出来绝对路径了(使用../回退法)

最开始是使用strcmp比较开头的,但是存在有时候规则目录后面有"/",但是匹配删除的时候,整个目录是不会有"/"的。
所以匹配之前还要准备:

const char * dash = rule;
strcat(dash, "/");

进程获取的问题

https://stackoverflow.com/questions/26451729/how-to-get-process-id-name-and-status-using-module
自带的current->comm可以获取到进程名,通过strcmp比较是否相同。

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/sched.h> //task_pid_nr

/* This function is called when the module is loaded. */
int simple_init(void)
{
       printk(KERN_INFO "Loading Module\n");
       printk("The process id is %d\n", (int) task_pid_nr(current));
       printk("The process vid is %d\n", (int) task_pid_vnr(current));
       printk("The process name is %s\n", current->comm);
       printk("The process tty is %d\n", current->signal->tty);
       printk("The process group is %d\n", (int) task_tgid_nr(current));
       printk("\n\n");
   //return -1; //debug mode of working
   return 0;
}

/* This function is called when the module is removed. */
void simple_exit(void) {
    printk(KERN_INFO "Removing Module\n");
}

/* Macros for registering module entry and exit points. */
module_init( simple_init );
module_exit( simple_exit );

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Simple Module");
MODULE_AUTHOR("SGG");

一个简单的hook

hook.c

#include "hook.h"

#include <linux/module.h>
#include <linux/delay.h> //msleep
#include <linux/kprobes.h>
#include <linux/file.h> //fget fput
#include <linux/syscalls.h>

MODULE_AUTHOR("dwt");
MODULE_DESCRIPTION("my-lkm-test");
MODULE_LICENSE("GPL");

// sys_call_table_ptr pointer
void** sys_call_table_ptr = NULL;
open_t old_open_func = NULL;

// record hacked_sys_call reference count. while module exit, it must be zero
atomic_t ref_count;

static struct kprobe kp={
    .symbol_name = "kallsyms_lookup_name",
};
static kallsyms_lookup_name_t orig_kallsyms_lookup_name = NULL;


void disable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  clear_bit(16, &cr0);
  write_cr0(cr0);
}

void enable_write_protection(void)
{
  unsigned long cr0 = read_cr0();
  set_bit(16, &cr0);
  write_cr0(cr0);
}

bool util_init(void)
{
    return true;
}

bool util_fini(void)
{
    return true;
}

int get_kallsyms_lookup_name(void)
{
    int ret = register_kprobe(&kp);
    if(unlikely(ret < 0)){
        printk(KERN_ERR "%s %s. register_kprobe failed, ret:%d\n", LKM_INFO, __FUNCTION__, ret);
        return ret;
    }
    printk("%s %s. kprobe at addr:%p, ret:%d\n", LKM_INFO, __FUNCTION__, kp.addr, ret);
    orig_kallsyms_lookup_name = (kallsyms_lookup_name_t)(void*)kp.addr;
    unregister_kprobe(&kp);

    return (orig_kallsyms_lookup_name!=NULL)?0:-1;
}

/* our hack open system call function */
asmlinkage int myhook_open(const char *filename, int flags, int mode)
{
    long value = old_open_func(filename, flags, mode);

    if(strcmp(my_current_proc_name(), "tailf") != 0) { // do not print tailf open log
        struct file* file = fget(value);
        if(NULL != file) {
            printk("%s myhook_open file. pid:%d, proccess:%s, file_name:%s, flags:%d\n", LKM_INFO, 
                current->tgid, my_current_proc_name(), my_get_file_shortname(file), flags);
            fput(file);
        }
    }   

    return value;
}

static int my_lkm_init(void)
{
    atomic_set(&ref_count, 1);

#ifdef RHEL_MAJOR // rhel/centos/ol
    printk("%s %s. RHEL:%d.%d\n", LKM_INFO, __FUNCTION__, RHEL_MAJOR, RHEL_MINOR);

    if (RHEL_MAJOR != 6 && RHEL_MAJOR != 7){
        printk(KERN_ERR "%s %s. current ko is not compatible for this os version.\n", LKM_INFO, __FUNCTION__);
        return -1;
    }
#else
    printk(KERN_ERR "%s %s. current ko is not compatible for this os version.\n", LKM_INFO, __FUNCTION__);
    return -1;
#endif

    /* get system call table addr. we will replace it*/
    if(get_kallsyms_lookup_name() < 0){
        printk(KERN_ERR "%s %s failed, load my lkm faild!\n", LKM_INFO, __FUNCTION__);
        return -1;
    }
    sys_call_table_ptr = (void**)orig_kallsyms_lookup_name("sys_call_table");
    printk("%s %s. kprobe sys_call_table:%p\n", LKM_INFO, __FUNCTION__, sys_call_table_ptr);
    if(unlikely(sys_call_table_ptr == NULL)){
        printk(KERN_ERR "%s %s failed, load my lkm faild!\n", LKM_INFO, __FUNCTION__);
        return -1;
    }

    // get the orign system call address
    old_open_func = (open_t)sys_call_table_ptr[__NR_open];
    printk("old_open_func:%p \n", old_open_func);

    // replace sys_call_table addr
    if(old_open_func != NULL) {
        disable_write_protection();
        sys_call_table_ptr[__NR_open] = (open_t)myhook_open;
        enable_write_protection();

        printk("hook sys_open success!\n");
        return 0;
    }

    printk("%s module load success!\n", LKM_INFO);
    return 0;
}

static void my_lkm_exit(void)
{
    util_fini();

    if(sys_call_table_ptr[__NR_open] == myhook_open) {
        disable_write_protection();
        sys_call_table_ptr[__NR_open] = old_open_func;
        enable_write_protection();

        printk(KERN_ALERT "revert sys_open success!\n");
    }

    while(atomic_read(&ref_count) > 1)
        msleep(10);

    printk("myhook module exit!\n");
}


module_init(my_lkm_init);
module_exit(my_lkm_exit);

hook.h

#ifndef _HOOK_H_
#define _HOOK_H_
#include <linux/kernel.h>
#include <linux/version.h>

#include <linux/fs.h>
//#include <linux/file.h>
#define LKM_INFO "MY_LKM"
#define LKM_VERSION "MY_LKM_TEST 1.0.0"

#if LINUX_VERSION_CODE > KERNEL_VERSION(3,10,0)
#define my_get_file_shortname(file) file->f_path.dentry->d_name.name
#else
#define my_get_file_shortname(file) file->f_dentry->d_name.name
#endif

#define my_current_proc_name() current->group_leader->comm

typedef unsigned long (*kallsyms_lookup_name_t)(const char *name);

typedef asmlinkage int (*open_t)(const char *filename, int flags, int mode);
asmlinkage int myhook_open(const char *filename, int flags, int mode);

bool util_init(void);
bool util_fini(void);

#endif //_HOOK_H_

其他

等待更新