Chinaunix首页 | 论坛 | 博客
  • 博客访问: 376807
  • 博文数量: 44
  • 博客积分: 2060
  • 博客等级: 上尉
  • 技术积分: 528
  • 用 户 组: 普通用户
  • 注册时间: 2008-04-17 20:50
文章分类
文章存档

2011年(1)

2010年(28)

2008年(15)

分类:

2010-03-08 13:59:01

2.5 我们知道栈了,那么堆呢?
 

看了栈的例子,举一反三就能知道,SIGSEGV和堆的关系取决于你的内存分配器,通常这意味着取决于C库的实现。

  1 #include

  2 #include

  3    

  4 #define K 1024

  5 int main () {

  6     char* c;

  7     int i = 0;

  8        

  9     c = malloc (1);

 10     while (1) {

 11         c += i*K;

 12         *c = 'a';

 13         printf ("overflow %dK\n", i);

 14         i ++;

 15     }

 16 }

上面这个例子在笔者机器上于15K时产生SIGSEGV。让我们改变初次malloc的内存大小,当初次分配16M时,SIGSEGV推迟到了溢出180K;当初次分配160M时,SIGSEGV推迟到了溢出571K。我们知道内存分配器在分配不同大小的内存时通常有不同的机制,这个例子从某种角度证明了这点。此例SIGSEGV在图2中的流程为:

1 -à 3 -à 4 -à 5 -à 11 -à 10

用一个野指针在堆里胡乱访问很少见,更多被问起的是“为什么我访问一块free()后的内存却没发生SIGSEGV”,比如下面这个例子:

  1 #include

  2 #include

  3    

  4 #define K 1024

  5 int main () {

  6     int* a;

  7    

  8     a = malloc (sizeof(int));

  9     *a = 100;

 10     printf ("%d\n", *a);

 11     free (a);

 12     printf ("%d\n", *a);

 13 }

SIGSEGV没有发生,但free()a指向的内存被清零了,一个合理的解释是为了安全。相信不会再有人问SIGSEGV没发生的原因。是的,free()后的内存不一定就立即归还给了操作系统,在真正的归还发生前,它一直在那儿。

 

2.6如果是指向全局区的野指针呢?
 

看了上面两个例子,我觉得这实在没什么好讲的。

 

2.7 函数跳转到了一个非法的地址上执行
 

这也是产生SIGSEGV的常见原因,来看下面的例子:

  1 #include

  2 #include

  3 #include

  4

  5 void foo () {

  6     char c;

  7

  8     memset (&c, 0x55, 128);

  9 }

 10

 11 int main () {

 12     foo();

 13 }  

通过栈溢出,我们将函数foo的返回地址覆盖成了0x55555555,函数跳转到了一个非法地址执行,最终引发SIGSEGV。非法地址执行,在图2中的流程中的可能性就太多了,从1-à3 -à4 -à … -à10,从410之间,几乎每条路径都可能出现。当然对于此例,0x55555555所指向的页面并不在内存之中,其在图2的流程为:

1-à3 -à4 -à5--à11-à10

如果非法地址对应的页面(页面属于用户态地址空间)存在于内存中,它又是可执行的[*],则程序会执行一大堆随机的指令。在这些指令执行过程中一旦访问内存,其产生SIGSEGV的流程几乎就无法追踪了(除非你用调试工具跟进)。看到这里,一个很合理的问题是:为什么程序在非法地址中执行的是随机指令,而不是非法指令呢?在一块未知的内存上执行,遇到非法指令可能性比较大吧,这样应该收到SIGILL信号啊?

[*]如果不用段寄存器的type checking,只用页表保护,传统32bit IA32可读即可执行。在NX技术出现后页级也可以控制是否可以执行。

事实并非如此,我们的IA32架构使用了如此复杂的指令集,以至于找到一条非法指令的编码还真不容易。在下例子中:

  1 #include

  2 #include

  3

  4 int main() {

  5     char buf[128] = "asdfaowerqoweurqwuroahfoasdbaoseur20   234123akfhasbfqower53453";

  6     sleep(1);

  7 }

笔者在buf中随机的敲入了一些字符,反汇编其内容得到的结果是:

0xbffa9e00:     popa  

0xbffa9e01:     jae    0xbffa9e67

0xbffa9e03:     popaw 

0xbffa9e05:     outsl  %ds:(%esi),(%dx)

0xbffa9e06:     ja     0xbffa9e6d

0xbffa9e08:     jb     0xbffa9e7b

0xbffa9e0a:     outsl  %ds:(%esi),(%dx)

0xbffa9e0b:     ja     0xbffa9e72

0xbffa9e0d:     jne    0xbffa9e81

0xbffa9e0f:     jno    0xbffa9e88

0xbffa9e11:     jne    0xbffa9e85

0xbffa9e13:     outsl  %ds:(%esi),(%dx)

0xbffa9e14:     popa  

0xbffa9e15:     push   $0x73616f66

0xbffa9e1a:     bound  %esp,%fs:0x6f(%ecx)

0xbffa9e1e:     jae    0xbffa9e85

0xbffa9e20:     jne    0xbffa9e94

0xbffa9e22:     xor    (%eax),%dh

0xbffa9e24:     and    %ah,(%eax)

0xbffa9e26:     and    %dh,(%edx)

0xbffa9e28:     xor    (%ecx,%esi,1),%esi

0xbffa9e2b:     xor    (%ebx),%dh

0xbffa9e2d:     popa  

0xbffa9e2e:     imul   $0x61,0x68(%esi),%esp

0xbffa9e32:     jae    0xbffa9e96

0xbffa9e34:     data16

0xbffa9e35:     jno    0xbffa9ea6

0xbffa9e37:     ja     0xbffa9e9e

0xbffa9e39:     jb     0xbffa9e70

0xbffa9e3b:     xor    0x33(,%esi,1),%esi

0xbffa9e42:     add    %al,(%eax)

0xbffa9e44:     add    %al,(%eax)

0xbffa9e46:     add    %al,(%eax)

0xbffa9e48:     add    %al,(%eax)

0xbffa9e4a:     add    %al,(%eax)

0xbffa9e4c:     add    %al,(%eax)

0xbffa9e4e:     add    %al,(%eax)

0xbffa9e50:     add    %al,(%eax)

0xbffa9e52:     add    %al,(%eax)

0xbffa9e54:     add    %al,(%eax)

0xbffa9e56:     add    %al,(%eax)

0xbffa9e58:     add    %al,(%eax)

0xbffa9e5a:     add    %al,(%eax)

0xbffa9e5c:     add    %al,(%eax)

0xbffa9e5e:     add    %al,(%eax)

 

…………………………………………………………………………

 

一条非法指令都没有!大家也可以自己构造一些随机内容试试,看能得到多少非法指令。故在实际情况中,函数跳转到非法地址执行时,遇到SIGSEGV的概率是远远大于SIGILL的。

我们来构造一个遭遇SIGILL的情况,如下例:

#include

#include

#include

 

#define GET_EBP(ebp)    \

    do {    \

        asm volatile ("movl %%ebp, %0\n\t" : "=m" (ebp));  \

    } while (0)

 

char buf[128];

 

 

void foo () {

    printf ("Hello world\n");

}

 

void build_ill_func() {

    int i = 0;

 

    memcpy (buf, foo, sizeof(buf));

    while (1) {

        /*

         * Find *call* instruction and replace it with

         * *ud2a* to generate a #UD exception

         */

        if ( buf[i] == 0xffffffe8 ) {

            buf[i] = 0x0f;

            buf[i+1] = 0x0b;

            break;

        }

        i ++;

    }

}

 

void overflow_ret_address () {

    unsigned long ebp;

    unsigned long addr = (unsigned long)buf;

    int i;

 

    GET_EBP (ebp);

 

    for ( i=0; i<16; i++ )

        memcpy ((void*)(ebp + i*sizeof(addr)), &addr, sizeof(addr));

    printf ("ebp = %#x\n", ebp);

}

 

int main() {

    printf ("%p\n", buf);

    build_ill_func ();

    overflow_ret_address ();

}

 

我们在一块全局的buf里填充了一些指令,其中有一条是ud2a,它是IA32指令集中用来构造一个非法指令陷阱。在overflow_ret_address()中,我们通过栈溢出覆盖函数的返回地址,使得函数返回时跳转到buf执行,最终执行到ud2a指令产生一个SIGILL信号。注意此例使用了ebp框架指针寄存器,在编译时不能使用-fomit-frame-pointer参数,否则得不到期望的结果。

 

2.8非法的系统调用参数
 

这是一种较为特殊的情况。特殊是指前面的例子访问非法内存都发生在用户态。而此例中,对非法内存的访问却发生在内核态。通常是执行copy_from_user()copy_to_user()时。其流程在图2中为:

1 -à …. -à 11 -à 12 -à 13

内核使用fixup[*]的技巧来处理在处理此类错误。ULK说通常的处理是发送一个SIGSEGV信号,但实际大多数系统调用都可以返回EFAULTbad address)码,从而避免用户态程序被终结。这种情况就不举例了,笔者一时间想不出哪个系统调用可以模拟此种情况而不返回EFAULT错误。

[*]关于fixup的技巧可以参考笔者另一篇文章《Linker Script in Linux》,

 
2.9还有什么?
 

我们已经总结了产生SIGSEGV的大多数情况,在实际编程中,即使现象不一样,最终发生SIGSEGV的原因都可以归到上述几类。掌握了这些基本例子,我们可以避免大多数的SIGSEGV

阅读(5826) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

安何2010-04-18 08:47:02