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指令后的栈布局
图四,输入过长的字符串
图五,返回地址被覆盖后,发生段错误