深入汇编理解缓冲区溢出攻击

来源:互联网 发布:淘宝生活服务类目活动 编辑:程序博客网 时间:2024/05/26 02:21

1.基本知识
    子汇编程序里,调用函数使用CALL伪指令,原始的传递参数的方法可以是使用寄存器和全局标记(和高级语言,如C中的全局变量,在.data段定义的标记)。但是由于这样子函数不能模块化,而且如果程序功能稍大的话,代码将非常难于理解和维护,所以后来统一使用栈来管理函数调用,包括函数的参数传递,返回地址,局部变量。这样函数就可以模块化,并且可以写在另一个文件中。不过,在Linux内核中,系统调用都是使用寄存器传递参数的。
2.写一个简单的C程序stack.c,如下
------------------------stack.c--------------------------
#include<stdio.h>
void test_func(int,char,char);
int main(){
    test_func(1,'B','C');
    return 0;
}

void test_func(int a,char b,char c){
    int func_a=a;
    char func_b=b;
    char func_c=c;
}
------------------------------------------------------------
3.编译这个文件到汇编语言
    $gcc -S statk.c
    编译后生成汇编文件stack.s,我使用的是gcc4.4.1(Ubuntu-9.10(Karmic Koala)),内容如下
--------------------------stack.s----------------------------
    .file    "stack.c"
    .text
.globl main        #全局标记声明,gcc使用main标记作为程序入口,而gas使用_start标记。
    .type    main, @function
main:
    pushl    %ebp
    movl    %esp, %ebp     #规范化的函数调用开头:首先保存栈顶指针
    andl    $-16, %esp        #Intel推荐16字节对齐,为了更快的预取
    subl    $16, %esp          #栈向下增长16字节,留出新参的内存,栈很特别,它从高内存向低内存增长               
    movl    $66, 8(%esp)    #将3个参数入栈,入栈是反序的,参数1即在栈最上端(低地址)
    movl    $65, 4(%esp)
    movl    $1, (%esp)         #从这里可以知道,参数的增长方式是从低地址往高地址增长,否则
                                              #这个int参数1就要超过栈顶指针了,从另一方面理解,Intel是小端
                                             #对齐的,所有数据的地址都是低字节地址,所以其余字节都放在高内存
    call    test_func              #调用函数,call伪指令会把返回地址入栈
    movl    $0, %eax          #从这里算起要修改3行,否则使用gas,ld编译链接后的程序最后找不到出口,会发生段错误,但是并不影响我们。
    leave               
    ret
    .size    main, .-main
.globl test_func
    .type    test_func, @function
test_func:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $24, %esp                 #在前面栈已经16字节对齐,所以这里不需要了
    movl    12(%ebp), %edx    #将参数'B'放入EDX寄存器,这里为什么会取得'A'将在后面用图
                            #来表示
    movl    16(%ebp), %eax    #将参数'C'放入EAX寄存器
    movb    %dl, -20(%ebp)    #'B'是字符,所以只取一个字节,暂存
    movb    %al, -24(%ebp)    #同上
    movl    8(%ebp), %eax     #取参数1,放入EAX寄存器
    movl    %eax, -8(%ebp)    #给func_a赋值
    movzbl    -20(%ebp), %eax    #扩展传送,表示将一个字节传送到寄存器EAX,EAX高24位补0
    movb    %al, -1(%ebp)            #赋值给func_b
    movzbl    -24(%ebp), %eax   
    movb    %al, -2(%ebp)            #赋值给func_c
    leave
    ret
    .size    test_func, .-test_func
    .ident    "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
    .section    .note.GNU-stack,"",@progbits
-------------------------------------------------------------------------
3.修改程序
    为了便于调试,我们将stack.s文件修改为gas格式,也就是把main换成_start即可,可以使用vi的替换功能方便的完成,并且还要修改__main的退出代码,见注释。修改以后的文件如下:
----------------------------------stack.s------------------------------
    .file    "stack.c"
    .text
.globl _start
    .type _start, @function
_start:
    pushl    %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $16, %esp
    movl    $67, 8(%esp)
    movl    $65, 4(%esp)
    movl    $1, (%esp)
    call    test_func
    pushl    $0                #这两行是修改后的
    call    exit
    .size    _start, .-_start
.globl test_func
    .type    test_func, @function
test_func:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $24, %esp
    movl    12(%ebp), %edx
    movl    16(%ebp), %eax
    movb    %dl, -20(%ebp)
    movb    %al, -24(%ebp)
    movl    8(%ebp), %eax
    movl    %eax, -8(%ebp)
    movzbl    -20(%ebp), %eax
    movb    %al, -1(%ebp)
    movzbl    -24(%ebp), %eax
    movb    %al, -2(%ebp)
    leave
    ret
    .size    test_func, .-test_func
    .ident    "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
    .section    .note.GNU-stack,"",@progbits
-------------------------------------------------------------------------
使用如下命令生成可以调试的程序
    $as -o stack.o stack.s -gstabs
    $ld -dynamic-linker /lib/ld-linux.so.2 -o stack stack.o -lc
解释:as即gas汇编器,-gstabs参数生成可供gdb调试的信息;
    ld是连接器,由于使用了C库,使用-l参数指定库libc,由于需要动态连接库,所以还要指定动态连接程序,使用参数-dynamic-linker来指定。
4.调试程序
    $gdb stack
    gdb的使用方法不再详细介绍,下面查看一些关键的结果。使用一个图来表示栈,如下(由于不能上传图片,见相册:Ubuntu栈)

5.缓冲区溢出
    上面的程序,只是用来详细介绍函数调用过程中,堆栈的变化,缓冲区溢出攻击的原理就是使用超出缓冲区的字符来填充缓冲区,这样就有可能覆盖栈中存放的函数返回地址,覆盖以后,函数返回时就不能执行原来的程序,而是恶意攻击者安排的代码,为什么会覆盖?关键是函数局部变量的增长方式是从低地址向高地址增长,而这时候,call伪指令已经把函数的返回地址入栈了,所以它位于局部变量的高端,当局部变量错误的增长时,就可能覆盖返回地址。下面举例
-----------------------------overflow.c--------------------------------
#include<stdio.h>
void func();
int main(){
    func();
    return 0;
}
void func(){
    char input[4];
    gets(input);
}
-------------------------------------------------------------------------
    main函数中调用了一个有名的会发生缓冲区溢出的函数gets,所以不用我们自己去编写一个函数,使用如下命令
    $gcc -S overflow.c    //生成汇编文件overflow.s,修改汇编文件中main为_start以及程序出口,修改后如下
---------------------------------overflow.s-----------------------------
    .file    "overflow.c"
    .text
.globl _start
    .type    _start, @function
_start:
    pushl    %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    call    func
    movl    $0, %eax
    movl    %ebp, %esp
    popl    %ebp
    pushl    $0
    call    exit
    .size    _start, .-_start
.globl func
    .type    func, @function
func:
    pushl    %ebp
    movl    %esp, %ebp
    subl    $40, %esp
    leal    -12(%ebp), %eax    #leal表示将源操作数地址发送到目的,由于gets函数的参数是地址
    movl    %eax, (%esp)        #将参数(一个地址)放入栈中,前面说过,所有调用函数的参数都必须倒序入栈。
    call    gets                            #调用函数gets
    leave
    ret
    .size    func, .-func
    .ident    "GCC: (Ubuntu 4.4.1-4ubuntu8) 4.4.1"
    .section    .note.GNU-stack,"",@progbits
-----------------------------------------------------------------------
    $as -o overflow.o overflow.s -gstabs    //编译汇编文件到目标文件
    $ld -dynamic-linker /lib/ld-linux.so.2 -o overflow overflow.o -lc
    执行完上述命令以后,就可以使用gdb来调试程序overflow了,在链接文件的时候,会提示gets很危险,不应该使用它。
6.实验结果
    下面使用gdb来调试这个程序,gdb的具体使用方法就不介绍了,下面是一些截图,能够说明栈是怎样被覆盖的(由于不能上传图片,见相册Ubuntu)

图一,传递参数给函数gets

图二,使用call指令调用函数gets

图三,执行call指令后的栈布局

图四,输入过长的字符串

图五,返回地址被覆盖后,发生段错误