zoukankan      html  css  js  c++  java
  • linux下进程堆栈下溢出判断及扩展实现

    一、堆栈扩展
    在进程创建的时候,内核并没有为进程分配太多的堆栈,即使是逻辑地址空间也没有,这样做的好处就是如果说用户态的程序堆栈向下溢出(对386来说,就是访问了更低地址的内存空间),这样内核可以比较容易的检测出这种错误,尽管这种错误出现的可能性要比向上溢出的概率小的多。记得在之前使用VS编译器的时候,编译器还有一个堆栈探测过程,就是对于局部变量大小超过一个页面的函数,编译器会生成额外的probe指令来预先来踩一脚这些页面,可能是windows内核中只允许一次向下扩展一个页面的堆栈空间?不管如何,这个我们就不验证了,因为我现在没有装windows的编译器啊,所以只能看这个Linux下的这种实现了。
    二、内核中判断
    linux-2.6.21mmmmap.c文件中的
    int expand_stack(struct vm_area_struct *vma, unsigned long address)
    函数负责对堆栈进行扩展,这个名字贴切而拉轰,以至于我一眼就就找到了它。在其中访问地址和堆栈的判断,其中对于限制的代码并不多,大致来说是集中在
    acct_stack_growth
    函数中。这个函数中并没有检测一个页面限制,所以在这个里面是没有对向下溢出多少发生错误提供信息。
    三、真正判断位置
    事实上,这个是一个硬件相关的一个特殊判断
    1、386体系结构实现
    对于我们常见的386来说,其实现位于linux-2.6.21archi386mmfault.c
    fastcall void __kprobes do_page_fault(struct pt_regs *regs,
                          unsigned long error_code)
        if (error_code & 4) {
            /*
             * Accessing the stack below %esp is always a bug.
             * The large cushion allows instructions like enter
             * and pusha to work.  ("enter $65535,$31" pushes
             * 32 pointers and then decrements %esp by 65535.)
             */
            if (address + 65536 + 32 * sizeof(unsigned long) < regs->esp)
                goto bad_area;
        }
    也就是这里判断是如果访问地址在386栈顶位置向下32 * sizeof(unsigned long),则认为是越界访问,当然,这里处理也是有些简单粗暴了,因为这里更加详细的做法应该是判断一下指令,这里所说的指令应该在大部分时间都是不用的,所以这个判断应该相对是一个过于宽泛的限制,这个65536相当于16个大小为4KB的页面,所以还是比较宽泛的。
    2、powerpc实现
    我们再看一下powepc的处理
        /*
         * N.B. The POWER/Open ABI allows programs to access up to
         * 288 bytes below the stack pointer.
         * The kernel signal delivery code writes up to about 1.5kB
         * below the stack pointer (r1) before decrementing it.
         * The exec code can write slightly over 640kB to the stack
         * before setting the user r1.  Thus we allow the stack to
         * expand to 1MB without further checks.
         */
        if (address + 0x100000 < vma->vm_end) {对于堆栈空间刚开始的1M空间之下的内容,可以随意扩展而不加检测
            /* get user regs even if this fault is in kernel mode */
            struct pt_regs *uregs = current->thread.regs;
            if (uregs == NULL)
                goto bad_area;

            /*
             * A user-mode access to an address a long way below
             * the stack pointer is only valid if the instruction
             * is one which would update the stack pointer to the
             * address accessed if the instruction completed,
             * i.e. either stwu rs,n(r1) or stwux rs,r1,rb
             * (or the byte, halfword, float or double forms).
             *
             * If we don't check this then any write to the area
             * between the last mapped region and the stack will
             * expand the stack rather than segfaulting.
             */
            if (address + 2048 < uregs->gpr[1]
                && (!user_mode(regs) || !store_updates_sp(regs)))如果堆栈已经扩展到1M一下,这里检测开始加强,只能访问2048字节之下范围,否则这里真的判断了特殊指令,所以这个应该比较准确,可能是由于RISC机型的指令操作比较简单
                goto bad_area;
        }
    四、386系统调用时寄存器使用情况和这个判断的关系
    386处理器有8个通用寄存器 E[ABCD]X,E[SD]I,E[SB]P,这八个寄存器,但是现在的系统调用使用寄存器传递的话,可以看到386系统中最多只是用了7个寄存器,这里唯一没有使用的就是ESP寄存器,通过这里我们可以猜测,如果用户态把ESP也作为寄存器传递入内核的话,那么内核在栈顶判断的时候这里可能就不准确,因为用户态的代码是可能缺页的。
    五、验证一下
    [tsecer@Harry stackflow]$ cat stackflow.c
    #include <stdio.h>
    int overflower()
    {
    //char placeholder[0x1000*19];
    char localvar,*localvaraddr=&localvar,page;
    int myesp;
    __asm__ (
    "movl %%esp,%0"
    :"=r"(myesp)); //这里使用汇编语言获得栈顶指针ESP
    printf("myesp is %#x,most acc %#x ",myesp,myesp-65536-32*sizeof(unsigned long));//模拟内核的计算规则,算出可以访问的最低位置
    for(page =0 ; ;page++)//以页面为单位访问ESP之下内存,看何时触发内核段错误
    {
        printf("probing %#x ",localvaraddr-(page<<12));
        localvaraddr[-(page<<12)] = 0;
    }
    return 0;
    }
    int main()
    {
    return overflower();
    }
    [tsecer@Harry stackflow]$ gcc  stackflow.c -g -o stackflow.c.exe -static
    [tsecer@Harry stackflow]$ ./stackflow.c.exe 
    myesp is 0xbfb45c10,most acc 0xbfb35b90
    probing 0xbfb45c23
    probing 0xbfb44c23
    probing 0xbfb43c23
    probing 0xbfb42c23
    probing 0xbfb41c23
    probing 0xbfb40c23
    probing 0xbfb3fc23
    probing 0xbfb3ec23
    probing 0xbfb3dc23
    probing 0xbfb3cc23
    probing 0xbfb3bc23
    probing 0xbfb3ac23
    probing 0xbfb39c23
    probing 0xbfb38c23
    probing 0xbfb37c23
    probing 0xbfb36c23
    probing 0xbfb35c23
    probing 0xbfb34c23根据计算,这个地址是不能访问的,但是这里并没有触发异常
    probing 0xbfb33c23
    probing 0xbfb32c23
    probing 0xbfb31c23这里总共访问了大致21个页面,与我们假设内容不同
    Segmentation fault (core dumped)
    [tsecer@Harry stackflow]$ 
    六、现象分析(进程初始化堆栈空间多大)
    这一点要说到内核为一个可执行文件建立一个堆栈的时候,这个堆栈空间是多大,也就是内核为一个进程启动的过程中一次性分配了多少vma区间。这个问题在内核的
    linux-2.6.21fsexec.c:setup_arg_pages
    函数中有决定性影响:
        arg_size += EXTRA_STACK_VM_PAGES * PAGE_SIZE;
    ……
    #ifdef CONFIG_STACK_GROWSUP
            mpnt->vm_start = stack_base;
            mpnt->vm_end = stack_base + arg_size;
    #else
            mpnt->vm_end = stack_top;
            mpnt->vm_start = mpnt->vm_end - arg_size;
    #endif
    其中
    #define EXTRA_STACK_VM_PAGES    20    /* random */
    也就是说,内核在为一个进程分配堆栈的时候,将会在实际使用的堆栈基础之上在额外增加20个页面的虚拟地址空间。由于实际上一个进程真正使用的参数空间一般小于一个页面(4KB),所以通常一个进程最开始堆栈分配的空间为21个页面,我们随便找一个程序看一下:
    [tsecer@Harry stackflow]$ cat /proc/self/maps
    0017b000-0017c000 r-xp 00000000 00:00 0          [vdso]
    001e8000-00206000 r-xp 00000000 fd:00 1280       /lib/ld-2.11.2.so
    00206000-00207000 r--p 0001d000 fd:00 1280       /lib/ld-2.11.2.so
    00207000-00208000 rw-p 0001e000 fd:00 1280       /lib/ld-2.11.2.so
    0020a000-0037c000 r-xp 00000000 fd:00 1282       /lib/libc-2.11.2.so
    0037c000-0037d000 ---p 00172000 fd:00 1282       /lib/libc-2.11.2.so
    0037d000-0037f000 r--p 00172000 fd:00 1282       /lib/libc-2.11.2.so
    0037f000-00380000 rw-p 00174000 fd:00 1282       /lib/libc-2.11.2.so
    00380000-00383000 rw-p 00000000 00:00 0 
    08048000-08053000 r-xp 00000000 fd:00 68967      /bin/cat
    08053000-08055000 rw-p 0000a000 fd:00 68967      /bin/cat
    09af0000-09b11000 rw-p 00000000 00:00 0          [heap]
    b75b8000-b77b8000 r--p 00000000 fd:00 100518     /usr/lib/locale/locale-archive
    b77b8000-b77b9000 rw-p 00000000 00:00 0 
    b77ce000-b77cf000 rw-p 00000000 00:00 0 
    bfc1f000-bfc34000 rw-p 00000000 00:00 0          [stack]这个空间也是21个页面,同一个系统中多次执行这个命令,栈顶的位置并不确定,这是因为load_elf_binary在调用setup_arg_pages的时候执行了randomize_stack_top,所以这个堆栈区间不确定,但是大小确定
    回到我们刚才说的那个问题,由于堆栈向下的20个页面都是在初始堆栈的虚拟地址空间中的,所以它不会触发堆栈扩展检测,因为这个本来已经在堆栈区间中了。
    七、再次模拟
    把overflower函数中的
    //char placeholder[0x1000*19];
    注释打开,从而让堆栈尽可能多的占用更多的堆栈空间,也就是迫使esp濒临原始堆栈vma区间下边界,然后开始逐步探测,此时计算结果会达到预期结果
    [tsecer@Harry stackflow]$ ./stackflow.c.exe 
    myesp is 0xbf9f4b40,most acc 0xbf9e4ac0
    probing 0xbf9f4b53
    probing 0xbf9f3b53
    probing 0xbf9f2b53
    probing 0xbf9f1b53
    probing 0xbf9f0b53
    probing 0xbf9efb53
    probing 0xbf9eeb53
    probing 0xbf9edb53
    probing 0xbf9ecb53
    probing 0xbf9ebb53
    probing 0xbf9eab53
    probing 0xbf9e9b53
    probing 0xbf9e8b53
    probing 0xbf9e7b53
    probing 0xbf9e6b53
    probing 0xbf9e5b53
    probing 0xbf9e4b53
    probing 0xbf9e3b53访问出错位置在预测位置之下
    Segmentation fault (core dumped)

  • 相关阅读:
    [NOIP2002 提高组] 均分纸牌
    洛谷 P1303 A*B Problem
    OpenJudge 1.6.5 年龄与疾病
    hdu 3340 线段树思路活用
    poj 2464 线段树统计区间..两棵树
    hdu 4419 矩形面积覆盖颜色
    经典动态规划 dp Rqnoj 57
    最基础二维线段树 hdu 1823 (简单)
    hdu 3564 线段树+dp
    spoj 1557 线段树 区间最大连续和 (不重复数)
  • 原文地址:https://www.cnblogs.com/tsecer/p/10486208.html
Copyright © 2011-2022 走看看