查看原文
其他

CPU阿甘之缓冲区溢出

老刘 码农翻身 2018-10-25

我是大家的老朋友CPU阿甘, 每天你一开机,我就忙得不亦乐乎,从内存中读取一条条的指令,挨个执行。


最早的时候我认为程序都是顺序执行的,后来发现并不是这样,经常会出现一条跳转指令,让我到另外一个内存地址处去下一条指令去执行。



时间久了我就明白这是人类代码中的if ... else ,或者for ,while等循环导致的。


这样跳来跳去,让我觉得有点头晕,不过没有办法,这是人类做出的规定。


后来我发现,有些指令经常会出现重复,尤其是下面这几个:


pushl    %ebp

movl     %esp %ebp

call  xxxx

ret


正当我疑惑的时候,内存炫耀地说:这些指令是为了函数调用,建立栈帧所所必需的啊。


“函数调用?这是什么鬼?”


“函数调用你都不知道? 我告诉你吧,现在的计算机语言,甭管你是面向对象还是函数式、动态还是静态、解释还是编译,只要想在我们冯诺依曼体系结构下运行,最终都得变成顺序、循环、分支,以及函数调用!”


内存说着给我举了一个例子:



这个例子非常简单,一看就明白。


“但是栈帧是什么?”


“阿甘你知道栈是什么意思吧?”


“不就是一个先进后出的数据结构吗?”



“对,通俗来说:一个栈帧就是这个栈中的一个元素,表示了一个函数在运行时的结构。” 内存继续给我科普:



“你这种画法好古怪,怎么倒过来了,栈底在上方,栈顶反而在下方!”


“这也是人类规定的,一个进程的虚拟内存中有个区域,就是栈,这个栈就是从高地址向低地址发展的啊。”


“奥,原来我执行的代码在一个叫做代码区的地方存放着啊,执行的时候会操作你的栈,对不对?”


“没错,我再给你看看那个栈帧的内部结构吧!”



这张图看起来很复杂,但是和代码一对应,还是比较清楚的。


我心中模拟了一下这个执行过程,hello()函数正在被执行,当要调用add函数的时候,需要准备参数,即x = 10, y=20 。


还要记录下返回地址,即printf(....)这个指令在内存的地址。当add函数调用完成以后,就可以返回到这里执行了。


真正开始执行add函数的时候,也需要给它建立一个栈帧(其中要记录下上个函数栈帧的开始地址),还有这个函数的参数,在栈帧也会分配内存空间,例如sum, buf等。


等到执行结束,add函数的栈帧就废弃了(相当于从栈中弹出),找到返回地址,继续执行printf指令。


hello函数执行完毕,也会废弃掉,回到上一个函数的栈帧,继续执行,如此持续下去....


我对内存说:“明白了,我已经迫不及待地想执行一下这个函数,看看效果了。”


内存说:“真的明白了?正好,操作系统老大已经发出指令,让我们运行了,开始吧!”


建立hello函数的栈帧,调用add函数,建立add栈帧,执行add函数的代码, 一切都很顺利。


add函数中调用了scanf ,要求用户输入一些数据,人类是超级慢的,我耐心等待。


用户输入了8个字符A,我把他们都放到了buf所在的内存中:



但是人类还在输入,接下里是一些很奇怪的数据,其长度远远超过了char buf[8]中的8个字节。


可是我还得把数据给放到内存中啊,于是函数栈帧就变成了这个样子。



(注:用户输入的数据是从低地址向高地址存放的。)


我觉得特别古怪的是,这个返回地址也被冲掉了,被改写了。


这个用户到底要干啥?


add函数执行完毕,要返回到hello函数了, 我明明知道返回地址已经被改掉, 可是我没有选择,还得把那个新的(用户输入的)返回地址给取出来, 老老实实地去那个地址取出下一条指令去执行。


完了,这根本就不是原来的prinf函数,而是一段恶意代码的入口!



与此同时....


黑客三兄弟中的老三大叫: 大哥二哥,我的这次缓冲区溢出攻击实验成功了!


“不错啊,你是怎么搞的?” 老大问道。


“正如二哥说的,那个scanf函数没有边界检查,我成功地把代码注入到了栈帧中,并且修改了返回地址!于是程序就跳到我指定的地方执行了。”


推荐阅读:

黑客三兄弟

黑客三兄弟(续)

CPU阿甘


你看到的只是冰山一角, 更多精彩文章,请移步《2016文章精华》或者《2017文章精华


码农翻身

用故事讲述技术

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存