Solar Designer's Non-executable stack的实现机理分析 作者:alert7 from m4in security teams 本文只是从正面来讲述Solar Designer's Non-executable stack的实现机理, 本文假设你已经比较了解386的保护模式,对Linux的内核有一定的了解. 本文讨论的只针对linux-2.0.30的那个补丁,其他版本的补丁有点不同. +#define USER_HUGE_CS 0x32 +#define USER_HUGE_SS 0x3A +++ linux/arch/i386/kernel/head.S Mon Apr 28 09:22:00 1997 @@ -400,10 +400,17 @@ .quad 0x0000000000000000 /* not used */ .quad 0xc0c39a000000ffff /* 0x10 kernel 1GB code at 0xC0000000 */ .quad 0xc0c392000000ffff /* 0x18 kernel 1GB data at 0xC0000000 */ +#ifdef CONFIG_STACKEXEC + .quad 0x00cafa000000ffff /* 0x23 user 2.75GB code at 0 */ 这里把用户代码段的界限从3Gb减小到2.75GB,从线性地址0开始,DPL为3。 该段的段选择子为0x23 #define USER_CS 0x23 #define USER_DS 0x2B + .quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 */ 用户的数据段没有改变。 + .quad 0x00cbda000000ffff /* 0x32 user 3GB code, DPL=2 */ 定义了一个界限为3Gb的代码段,从线性地址0开始,DPL=2 + .quad 0x00cbd2000000ffff /* 0x3a user 3GB stack, DPL=2 */ 定义了一个界限为3Gb的堆栈段,从线性地址0开始,DPL=2 +#else .quad 0x00cbfa000000ffff /* 0x23 user 3GB code at 0x00000000 */ .quad 0x00cbf2000000ffff /* 0x2b user 3GB data at 0x00000000 */ .quad 0x0000000000000000 /* not used */ .quad 0x0000000000000000 /* not used */ +#endif 因为把用户的代码段描述符的界限从3G减小到2.75G, 本来EIP处于代码段的0-3GB都是合法的,现在任何时候只要EIP大于0xAFFFFFFF(2.75Gb-1) 就会产生段越界的一般保护故障(GFP)。控制就会交给一个名为do_general_protection()的 函数。 asmlinkage void do_general_protection(struct pt_regs * regs, long error_code) { +#ifdef CONFIG_STACKEXEC + unsigned long retaddr; +#endif + if (regs->eflags & VM_MASK) { handle_vm86_fault((struct vm86_regs *) regs, error_code); return; } + +#ifdef CONFIG_STACKEXEC +/* Check if it was return from a signal handler */ + if (regs->cs == USER_CS || regs->cs == USER_HUGE_CS) 判断选择子是否是USER_CS或者是USER_HUGE_CS + if (get_seg_byte(USER_DS, (char *)regs->eip) == 0xC3) 这里你可能会疑惑,EIP应该用USER_CS做段选择子的,怎么这里会使用USER_DS? get_seg_byte(USER_DS, (char *)regs->eip)的意思是用USER_DS做选择子用regs->eip 做偏移量得到一个字节。0xc3为ret指令。一般缓冲区益处攻击大多发生在函数返回时, 所以这个补丁只能拦截到ret到特定地址的攻击。 如果这里用USER_CS而不是USER_DS,如果eip大于0xAFFFFFFF,get_seg_byte将再次触发 一般保护故障,将再次调用do_general_protection(),后果很严重。 + if (!verify_area(VERIFY_READ, (void *)regs->esp, 4)) 判断esp指向的四个字节是否可读。 + if ((retaddr = get_seg_long(USER_DS, (char *)regs->esp)) == + MAGIC_SIGRETURN) { MAGIC_SIGRETURN是内核向用户进程发送信号时,在用户堆栈上建立的一个特定的MAGIC数。 在linux/arch/i386/kernel/signal.c的sys_sigreturn中安装这个MAGIC数。 在这里用这个MAGIC_SIGRETURN来判断是否是需要从信号过程返回。 +/* + * Call sys_sigreturn() to restore the context. It would definitely be better + * to convert sys_sigreturn() into an inline function accepting a pointer to + * pt_regs, making this faster... + */ + regs->esp += 8; 退掉用户堆栈的返回地址与参数。 + __asm__("movl %3,%%esi;" + "subl %1,%%esp;" + "movl %2,%%ecx;" + "movl %%esp,%%edi;" + "cld; rep; movsl;" + "call sys_sigreturn;" + "leal %3,%%edi;" + "addl %1,%%edi;" + "movl %%esp,%%esi;" + "movl (%%edi),%%edi;" + "movl %2,%%ecx;" + "cld; rep; movsl;" + "movl %%esi,%%esp" + : +/* %eax is returned separately */ + "=a" (regs->eax) + : + "i" (sizeof(*regs)), + "i" (sizeof(*regs) >> 2), + "m" (regs) + : + "cx", "dx", "si", "di", "cc", "memory"); 以上一段汇编代码模拟返回时的正常状态,并替用户执行信号返回 + return; + } + +#ifdef CONFIG_STACKEXEC_LOG +/* + * Check if we're returning to the stack area, which is only likely to happen + * when attempting to exploit a buffer overflow. + */ 假如不是信号返回的情况下: + else if (regs->cs == USER_CS && + (retaddr & 0xF0000000) == 0xB0000000) { 判断返回地址是否在0xB0000000-0xBFFFFFFFF内 如果是的话,就把它记录下来。 + static unsigned long warning_time = 0, no_flood_yet = 0; + +/* Make sure at least one minute passed since the last warning logged */ + if (!warning_time || jiffies - warning_time > 60 * HZ) { + warning_time = jiffies; no_flood_yet = 1; + printk( + KERN_INFO + "Possible buffer overflow exploit attempt:\n" + KERN_INFO + "Process %s (pid %d, uid %d, euid %d).\n", + current->comm, current->pid, + current->uid, current->euid); + } else if (no_flood_yet) { + warning_time = jiffies; no_flood_yet = 0; + printk( + KERN_INFO + "More possible buffer overflow exploit " + "attempts.\n"); + } + } +#endif +#endif + die_if_kernel("general protection",regs,error_code); + +#if defined(CONFIG_STACKEXEC) && defined(CONFIG_STACKEXEC_AUTOENABLE) +/* + * Switch to the original huge code segment (and allow code execution on the + * stack for this entire process), if the faulty instruction is a call %reg, + * except for call %esp. + */ 注释已经讲了很清楚,这样做的是因为gcc允许生成跳板(trampolines)代码. trampolines的解释可以在http://www.science.uva.nl/~mes/jargon/t/trampoline.html 找到,不过一般程序很少用到。在我打了没有处理跳板(trampolines)代码的补丁后, 运行良好,但不保证永远不出错:) + if (regs->cs == USER_CS) + if (get_seg_byte(USER_DS, (char *)regs->eip) == 0xFF && + (get_seg_byte(USER_DS, (char *)(regs->eip + 1)) & 0xD8) == 0xD0 && + get_seg_byte(USER_DS, (char *)(regs->eip + 1)) != 0xD4) { 判断是否是call %reg,除了call %esp,因为%esp是很容易被hacker控制的。 + current->flags |= PF_STACKEXEC; + regs->cs = USER_HUGE_CS; regs->ss = USER_HUGE_SS; 恢复成大的代码段,大的数据段,继续执行。 + return; + } +#endif + current->tss.error_code = error_code; current->tss.trap_no = 13; force_sig(SIGSEGV, current); +++ linux/arch/i386/kernel/signal.c Tue Apr 29 11:06:14 1997 @@ -83,10 +83,10 @@ #define COPY_SEG(x) \ if ( (context.x & 0xfffc) /* not a NULL selectors */ \ && (context.x & 0x4) != 0x4 /* not a LDT selector */ \ - && (context.x & 3) != 3 /* not a RPL3 GDT selector */ \ + && (context.x & 3) < 2 /* not a RPL3 or RPL2 GDT selector */ \ ) goto badframe; COPY(x); #define COPY_SEG_STRICT(x) \ -if (!(context.x & 0xfffc) || (context.x & 3) != 3) goto badframe; COPY(x); +if (!(context.x & 0xfffc) || (context.x & 3) < 2) goto badframe; COPY(x); struct sigcontext_struct context; struct pt_regs * regs; @@ -167,16 +167,20 @@ unsigned long * frame; frame = (unsigned long *) regs->esp; - if (regs->ss != USER_DS && sa->sa_restorer) + if (regs->ss != USER_DS && regs->ss != USER_HUGE_SS && sa->sa_restorer) frame = (unsigned long *) sa->sa_restorer; frame -= 64; if (verify_area(VERIFY_WRITE,frame,64*4)) do_exit(SIGSEGV); /* set up the "normal" stack seen by the signal handler (iBCS2) */ +#ifdef CONFIG_STACKEXEC + put_user((unsigned long)MAGIC_SIGRETURN, frame); 简单的把MAGIC_SIGRETURN的返回地址放进构造的信号帧里, 原来里面是包含信号过程返回的sigreturn系统调用代码。 +#else #define __CODE ((unsigned long)(frame+24)) #define CODE(x) ((unsigned long *) ((x)+__CODE)) put_user(__CODE,frame); +#endif if (current->exec_domain && current->exec_domain->signal_invmap) put_user(current->exec_domain->signal_invmap[signr], frame+1); else @@ -204,19 +208,17 @@ /* non-iBCS2 extensions.. */ put_user(oldmask, frame+22); put_user(current->tss.cr2, frame+23); +#ifndef CONFIG_STACKEXEC /* set up the return code... */ put_user(0x0000b858, CODE(0)); /* popl %eax ; movl $,%eax */ put_user(0x80cd0000, CODE(4)); /* int $0x80 */ put_user(__NR_sigreturn, CODE(2)); #undef __CODE #undef CODE +#endif /* Set up registers for signal handler */ - regs->esp = (unsigned long) frame; - regs->eip = (unsigned long) sa->sa_handler; - regs->cs = USER_CS; regs->ss = USER_DS; - regs->ds = USER_DS; regs->es = USER_DS; - regs->gs = USER_DS; regs->fs = USER_DS; + start_thread(regs, (unsigned long)sa->sa_handler, (unsigned long)frame); regs->eflags &= ~TF_MASK; } 以上是这个补丁实现的主要的部分,我们来回顾一下如何实现的: 将用户的码段描述符的界限从3G减小到2.75G,因此当用户企图执行处于堆栈空间 0xB0000000-0xBFFFFFFFF范围内的指令时,就会产生段越界的一般保护故障(GFP)。 在do_general_protection处理中: 如果指令是ret并且要返回的地址为MAGIC_SIGRETURN,表明是信号返回的情况下, 那么模拟返回时的正常状态,并替用户执行信号返回。 如果指令是ret并且要返回的地址在xB0000000-0xBFFFFFFFF之中,那么就认为 它有攻击的可能,把它记录下来。 如果是call %reg并且不是call %esp,就表明这是trampoline代码,所以恢复 成大的代码段,大的数据段,继续执行。 我们看到: 如果我们的返回的地址不在xB0000000-0xBFFFFFFFF之中,那么 就可以绕过这个补丁的保护,具体可以自行参考: 国外Rafal Wojtczuk写的<> 国内的waring3写的<<绕过linux不可执行堆栈保护的方法浅析>> 该补丁不能保护Heap/BSS溢出攻击请参考waring3的<>。 还有一个问题我还没有搞明白: USER_HUGE_CS和USER_HUGE_SS的特权级为什么要设为2? 照我的理解如果是3的话,应该不会对这个补丁会有什么影响。 当然这就需要改一下处理特权级的地方了。 有什么理解不对的地方,欢迎email:alert7@21cn.com 感谢所有帮助过我成长的人. 特别是绿盟的四哥scz,linuxforum上Linux内核技术版版主jkl。 此致敬礼!