缓冲区溢出攻击详细讲解Word格式.docx
- 文档编号:19706633
- 上传时间:2023-01-09
- 格式:DOCX
- 页数:11
- 大小:208.64KB
缓冲区溢出攻击详细讲解Word格式.docx
《缓冲区溢出攻击详细讲解Word格式.docx》由会员分享,可在线阅读,更多相关《缓冲区溢出攻击详细讲解Word格式.docx(11页珍藏版)》请在冰豆网上搜索。
二、函数栈帧
栈的主要功能是实现函数的调用。
因此在介绍栈溢出原理之前,需要弄清函数调用时栈空间发生了怎样的变化。
每次函数调用时,系统会把函数的返回地址(函数调用指令后紧跟指令的地址),一些关键的寄存器值保存在栈内,函数的实际参数和局部变量(包括数据、结构体、对象等)也会保存在栈内。
这些数据统称为函数调用的栈帧,而且是每次函数调用都会有个独立的栈帧,这也为递归函数的实现提供了可能。
图2
函数栈帧
如图所示,我们定义了一个简单的函数function,它接受一个整形参数,做一次乘法操作并返回。
当调用function(0)时,arg参数记录了值0入栈,并将callfunction指令下一条指令的地址0x00bd16f0保存到栈内,然后跳转到function函数内部执行。
每个函数定义都会有函数头和函数尾代码,如图绿框表示。
因为函数内需要用ebp保存函数栈帧基址,因此先保存ebp原来的值到栈内,然后将栈指针esp内容保存到ebp。
函数返回前需要做相反的操作——将esp指针恢复,并弹出ebp。
这样,函数内正常情况下无论怎样使用栈,都不会使栈失去平衡。
subesp,44h指令为局部变量开辟了栈空间,比如ret变量的位置。
理论上,function只需要再开辟4字节空间保存ret即可,但是编译器开辟了更多的空间(这个问题很诡异,你觉得呢?
)。
函数调用结束返回后,函数栈帧恢复到保存参数0时的状态,为了保持栈帧平衡,需要恢复esp的内容,使用addesp,4将压入的参数弹出。
之所以会有缓冲区溢出的可能,主要是因为栈空间内保存了函数的返回地址。
该地址保存了函数调用结束后后续执行的指令的位置,对于计算机安全来说,该信息是很敏感的。
如果有人恶意修改了这个返回地址,并使该返回地址指向了一个新的代码位置,程序便能从其它位置继续执行。
三、栈溢出基本原理
上边给出的代码是无法进行溢出操作的,因为用户没有“插足”的机会。
但是实际上很多程序都会接受用户的外界输入,尤其是当函数内的一个数组缓冲区接受用户输入的时候,一旦程序代码未对输入的长度进行合法性检查的话,缓冲区溢出便有可能触发!
比如下边的一个简单的函数。
1.void
fun(unsigned
char
*data)
2.{
3.unsigned
buffer[BUF_LEN];
4.strcpy((char*)buffer,(char*)data);
//溢出点
5.}
这个函数没有做什么有“意义”的事情(这里主要是为了简化问题),但是它是一个典型的栈溢出代码。
在使用不安全的strcpy库函数时,系统会盲目地将data的全部数据拷贝到buffer指向的内存区域。
buffer的长度是有限的,一旦data的数据长度超过BUF_LEN,便会产生缓冲区溢出。
图3
缓冲区溢出
由于栈是低地址方向增长的,因此局部数组buffer的指针在缓冲区的下方。
当把data的数据拷贝到buffer内时,超过缓冲区区域的高地址部分数据会“淹没”原本的其他栈帧数据,根据淹没数据的内容不同,可能会有产生以下情况:
1、淹没了其他的局部变量。
如果被淹没的局部变量是条件变量,那么可能会改变函数原本的执行流程。
这种方式可以用于破解简单的软件验证。
2、淹没了ebp的值。
修改了函数执行结束后要恢复的栈指针,将会导致栈帧失去平衡。
3、淹没了返回地址。
这是栈溢出原理的核心所在,通过淹没的方式修改函数的返回地址,使程序代码执行“意外”的流程!
4、淹没参数变量。
修改函数的参数变量也可能改变当前函数的执行结果和流程。
5、淹没上级函数的栈帧,情况与上述4点类似,只不过影响的是上级函数的执行。
当然这里的前提是保证函数能正常返回,即函数地址不能被随意修改(这可能很麻烦!
如果在data本身的数据内就保存了一系列的指令的二进制代码,一旦栈溢出修改了函数的返回地址,并将该地址指向这段二进制代码的其实位置,那么就完成了基本的溢出攻击行为。
图4
基本栈溢出攻击
通过计算返回地址内存区域相对于buffer的偏移,并在对应位置构造新的地址指向buffer内部二进制代码的其实位置,便能执行用户的自定义代码!
这段既是代码又是数据的二进制数据被称为shellcode,因为攻击者希望通过这段代码打开系统的shell,以执行任意的操作系统命令——比如下载病毒,安装木马,开放端口,格式化磁盘等恶意操作。
四、栈溢出攻击
上述过程虽然理论上能完成栈溢出攻击行为,但是实际上很难实现。
操作系统每次加载可执行文件到进程空间的位置都是无法预测的,因此栈的位置实际是不固定的,通过硬编码覆盖新返回地址的方式并不可靠。
为了能准确定位shellcode的地址,需要借助一些额外的操作,其中最经典的是借助跳板的栈溢出方式。
根据前边所述,函数执行后,栈指针esp会恢复到压入参数时的状态,在图4中即data参数的地址。
如果我们在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令jmpesp——跳板。
那么函数返回后,会执行该指令并跳转到esp所在的位置——即data的位置。
我们可以将缓冲区再多溢出一部分,淹没data这样的函数参数,并在这里放上我们想要执行的代码!
这样,不管程序被加载到哪个位置,最终都会回来执行栈内的代码。
图5
借助跳板的栈溢出攻击
借助于跳板的确可以很好的解决栈帧移位(栈加载地址不固定)的问题,但是跳板指令从哪找呢?
“幸运”的是,在Windows操作系统加载的大量dll中,包含了许多这样的指令,比如kernel32.dll,ntdll.dll,这两个动态链接库是Windows程序默认加载的。
如果是图形化界面的Windows程序还会加载user32.dll,它也包含了大量的跳板指令!
而且更“神奇”的是Windows操作系统加载dll时候一般都是固定地址,因此这些dll内的跳板指令的地址一般都是固定的。
我们可以离线搜索出跳板执行在dll内的偏移,并加上dll的加载地址,便得到一个适用的跳板指令地址!
//查询dll内第一个jmp
esp指令的位置
int
findJmp(char*dll_name)
{
char*
handle=(char*)LoadLibraryA(dll_name);
//获取dll加载地址
for(int
pos=0;
;
pos++)//遍历dll代码空间
if(handle[pos]==(char)0xff&
&
handle[pos+1]==(char)0xe4)//寻找0xffe4
=
jmp
esp
return
(int)(handle+pos);
}
这里简化了搜索算法,输出第一个跳板指令的地址,读者可以选取其他更合适位置。
LoadLibraryA库函数返回值就是dll的加载地址,然后加上搜索到的跳板指令偏移pos便是最终地址。
jmpesp指令的二进制表示为0xffe4,因此搜索算法就是搜索dll内这样的字节数据即可。
虽然如此,上述的攻击方式还不够好。
因为在esp后继续追加shellcode代码会将上级函数的栈帧淹没,这样做并没有什么好处,甚至可能会带来运行时问题。
既然被溢出的函数栈帧内提供了缓冲区,我们还是把核心的shellcode放在缓冲区内,而在esp之后放上跳转指令转移到原本的缓冲区位置。
由于这样做使代码的位置在esp指针之前,如果shellcode中使用了push指令便会让esp指令与shellcode代码越来越近,甚至淹没自身的代码。
这显然不是我们想要的结果,因此我们可以强制抬高esp指针,使它在shellcode之前(低地址位置),这样就能在shellcode内正常使用push指令了。
图6
调整shellcode与栈指针
调整代码的内容很简单:
add
esp,-X
第一条指令抬高了栈指针到shellcode之前。
X代表shellcode起始地址与esp的偏移。
如果shellcode从缓冲区起始位置开始,那么就是buffer的地址偏移。
这里不使用subesp,X指令主要是避免X的高位字节为0的问题,很多情况下缓冲区溢出是针对字符串缓冲区的,如果出现字节0会导致缓冲区截断,从而导致溢出失败。
第二条指令就是跳转到shellcode的起始位置继续执行。
(又是jmpesp!
)
通过上述方式便能获得一个较为稳定的栈溢出攻击。
五、shellcode构造
shellcode实质是指溢出后执行的能开启系统shell的代码。
但是在缓冲区溢出攻击时,也可以将整个触发缓冲区溢出攻击过程的代码统称为shellcode,按照这种定义可以把shellcode分为四部分:
1、核心shellcode代码,包含了攻击者要执行的所有代码。
2、溢出地址,是触发shellcode的关键所在。
3、填充物,填充未使用的缓冲区,用于控制溢出地址的位置,一般使用nop指令填充——0x90表示。
4、结束符号0,对于符号串shellcode需要用0结尾,避免溢出时字符串异常。
前边一直在围绕溢出地址讨论,并解决了shellcode组织的问题,而最核心的代码如何构造并未提及——即攻击成功后做的事情。
其实一旦缓冲区溢出攻击成功后,如果被攻击的程序有系统的root权限——比如系统服务程序,那么攻击者基本上可以为所欲为了!
但是我们需要清楚的是,核心shellcode必须是二进制代码形式。
而且shellcode执行时是在远程的计算机上,因此shellcode是否能通用是一个很复杂的问题。
我们可以用一段简单的代码实例来说明这个问题。
缓冲区溢出成功后,一般大家都会希望开启一个远程的shell控制被攻击的计算机。
开启shell最直接的方式便是调用C语言的库函数system,该函数可以执行操作系统的命令,就像我们在命令行下执行命令那样。
假如我们执行cmd命令——在远程计算机上启动一个命令提示终端(我们可能还不能和它交互,但是可以在这之前建立一个远程管道等),这里仅作为实例测试。
为了使system函数调用成功,我们需要将“cmd”字符串内容压入栈空间,并将其地址压入作为system函数的参数,然后使用call指令调用system函数的地址,完成函数的执行。
但是这样做还不够,如果被溢出的程序没有加载C语言库的话,我们还需要调用Windows的APILoadlibrary加载C语言的库msvcrt.dll,类似的我们也需要为字符串“msvcrt.dll”开辟栈空间。
1.xor
ebx,ebx
//ebx=0
2.
3.push
0x3f3f6c6c
//ll?
?
4.push
0x642e7472
//rt.d
5.push
0x6376736d
//msvc
6.mov
[esp+10],ebx
//'
'
-&
gt;
0'
7.mov
[esp+11],ebx
8.mov
eax,esp
//"
msvcrt.dll"
地址
9.push
eax
10.mov
eax,0x77b62864
//kernel32.dll:
LoadLibraryA
11.call
//LoadLibraryA("
12.add
esp,16
13.push
0x3f646d63
cmd?
"
14.mov
[esp+3],ebx
\0'
15.mov
eax,esp;
cmd"
16.push
17.mov
eax,0x774ab16f
//msvcrt.dll:
system
18.call
//system("
19.add
esp,8
上述汇编代码实质上是如下两个函数调用语句:
1.Loadlibrary(“msvcrt.dll”);
2.system(“cmd”);
3.
不过在构造这段汇编代码时需要注意不能出现字节0,为了填充字符串的结束字符,我们使用已经初始化为0的ebx寄存器代替。
另外,在对库函数调用的时候需要提前计算出函数的地址,如Loadlibrary函数的0x77b62864。
计算方式如下:
1.int
findFunc(char*dll_name,char*func_name)
3.HINSTANCE
handle=LoadLibraryA(dll_name);
4.return
(int)GetProcAddress(handle,func_name);
这个函数地址是在本地计算的,如果被攻击计算机的操作系统版本差别较大的话,这个地址可能是错误的。
不过在《0day安全:
软件漏洞分析技术》中,作者提供了一个更好的方式,感兴趣的读者可以参考该书提供的代码。
因此构造一个通用的shellcode并非十分容易,如果想让攻击变得有效的话。
六、汇编语言自动转换
写出shellcode后(无论是简单的还是通用的),我们还需要将这段汇编代码转换为机器代码。
如果读者对x86汇编十分熟悉的话,选择手工敲出二进制代码的话也未尝不可。
不过我们都希望能让计算机帮助做完这些事,既然开发环境提供了编译器,用它们帮忙何乐而不为呢?
既不用OllyDbg工具,也不适用其他的第三方工具,我们写一个简单的函数来完成这个工作。
1.//将内嵌汇编的二进制指令dump到文件,style指定输出数组格式还是二进制形式,返回代码长度
2.int
dumpCode(unsigned
char*buffer)
3.{
4.goto
END
//略过汇编代码
5.BEGIN:
6.__asm
7.{
8.//在这里定义任意的合法汇编代码
}END:
//确定代码范围UINT
begin,end;
__asm{mov
eax,BEGIN
mov
begin,eax
eax,END
end,eax
}//输出int
len=end-begin;
memcpy(buffer,(void*)begin,len);
//四字节对齐int
fill=(len-len%4)%4;
while(fill--)buffer[len+fill]=0x90;
//返回长度return
len+fill;
}
因为C++是支持嵌入式汇编代码的,因此在函数内的汇编代码都会被整成编译为二进制代码。
实现二进制转换的基本思想是读取编译器最终生成的二进制代码段数据,将数据导出到指定的缓冲区内。
为了锁定嵌入式汇编代码的位置和长度,我们定义了两个标签BEGIN和END。
这两个标签在汇编语言级别会被解析为实际的线性地址,但是在高级语言级是无法直接使用这两个标签值的,只能使用goto语句跳转使用它们。
但是我们可以顺水推舟,使用两个局部变量在汇编级记录这两个标签的值!
1.//确定代码范围
2.UINT
3.__asm
4.{
5.mov
9.}
这样就可以得到嵌入式汇编的代码范围了,使用memcpy操作将代码数据拷贝到目标缓冲区即可(后边还用nop指令将代码按照四字节对齐)。
不过我们还需要注意一个问题,嵌入式汇编在函数执行时也会执行,这显然不可以,我们只是把它当作数据而已(是数据?
还是代码?
),因此在函数开始的地方我们使用goto语句直接跳转到嵌入式会变语句的结尾——END标签!
七、攻击测试
按照上述内容,相信不难构造出一个简单的shellcode并攻击之前提供的漏洞函数。
但是如果使用VS2010测试的话可能会碰到很多问题。
经过大量的调试和资料查询,我们需要设置三处VS的项目属性。
1、配置->
配置属性->
C/C++->
基本运行时检查=默认值,避免被检测栈帧失衡。
2、配置->
缓冲区安全检查=否,避免识别缓冲区溢出漏洞。
3、配置->
链接器->
高级->
数据执行保护(DEP)=否,避免堆栈段不可执行。
从这三处设置看来,目前的编译器已经针对缓冲区溢出攻击做了大量的保护工作(显然这会降低程序的执行性能,因此允许用户配置),使得传统的缓冲区溢出攻击变得没那么“猖狂”了,但是在计算机安全领域,“道高一尺,魔高一丈”,总有人会找到更隐蔽的攻击方式让编译器开发者措手不及。
本文除了分析缓冲区溢出攻击的原理之外,更希望读者能从中感受到代码安全的重要性,并结合编译器提供的安全功能让自己的代码更加安全高效。
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 缓冲区 溢出 攻击 详细 讲解
![提示](https://static.bdocx.com/images/bang_tan.gif)