在某个教室里,正在上一门特殊的课一网络攻防。说它特殊,其实只是对外人而言,因为在他们看来,这门课显得有些神秘与高深。
让我们一起推开教室的门,感悟一下他们的学习和讨论氛围吧。嘘!小声点!
啤酒和杯子一一缓冲区溢出原理
第一节课,各位同学都规规矩矩的坐在位置上,看着老师,大气也不敢出。老师带着眼镜,一副精干的样子,看出了大家紧张的神情,于是说道:“大家这么紧张干嘛是怕这门课的技术还是怕这门课的老师一一我啊我很好说话的,都是年轻人嘛;而技术,就比我更好啦一一它是不会说话的。”“
呵呵!是呀!是呀!”大家听了老师的独白都笑了,气氛一下活跃了不少。
老师停了一下,问道:“同学们,这门课叫‘网络攻防’,那你们知道网络上的攻击手段哪些么”
大家听了,马上七嘴八舌的应到:“一些安全杂志上有介绍,好像有注入、缓冲区溢出、劫持、嗅探……”
“哈哈,对!”老师满意的说道,“大家说得很好!攻防是个全方位的问题,涉及到诸多技术,但我们课时有限,第一学期只能讲几个有限的部分。现在最流行的有攻击手段两种方式,一种是SQL注入,另一种就是缓冲区溢出攻击。我们选一个作为首先讨论的主题吧!”
“好啊,好啊!”
“不过讲什么呢好像都挺有意思的。”一位女生说道。
“不知道,投硬币吧!”一个胖乎乎的同学说。
老师眼镜一亮,说:“对,真是个好办法!”
老师摸出一个硬币,拈在手中说:“如果是正面就先讲缓冲区溢出,反面就先讲SQL注入。”语音刚落,硬币就在空中划出一道美妙的弧线,落在地上转了几圈后停了下来。
前排的几个同学壮着胆子围上去看,叫道:“是正面也!”
“好!那就先讲缓冲区溢出!注入留在以后讲!”老师说道。
“不过似乎很难也……”一个瘦瘦的但很精神的同学说。
“不用怕!大家只要有信心、有毅力,就一定能战胜,Followme!”
“好,首先让我们来认识一下缓冲区以及缓冲区溢出吧!”老师说。
“先作下类比,如果某个人把一瓶啤酒全部倒入一个小杯子中,那装不下的啤酒就会四处冒出,流到桌子上,这个大家都清楚吧!”
“是啊!是啊!”男生们都遗憾的说到,“那好浪费啊!”
那个胖胖的同学说:“这么好的天,啤酒、小菜,再加上超爆DVD大片,好舒服啊!”
“就是啊,谁这么做的拖出来打一顿!”大家的情绪被带动起来了。
“安静!安静!”老师好不容易把秩序维护下来,然后兴致索然的说,“我只是打个比方而已!你们什么都能联想到吃。那位同学,你叫什么名字呢”
那位胖胖的同学老实的答道:“我叫玉波。”玉波果然像名字一样,长的白白胖胖的,一张圆乎乎的脸,挺可爱的样子。
“不过我很理解你们,食堂的饭菜是太难吃了。”老师挺同情的说道,“大学食堂就是伤心太平洋啊!”“哈哈哈哈……”同学们都乐了,教室里充满了愉快的笑声。
等大家平稳下来后,老师继续说道:“同样的道理,在计算机内部,输入数据通常被存放在一个临时空间内,这个临时存放空间就被称为缓冲区,缓冲区的长度事先已经被程序或者操作系统定义好了。缓冲区就很像那个啤酒杯,用来装东西,而且大小固定。”
“向缓冲区内填充数据,如果数据的长度很长(如同那瓶啤酒),超过了缓冲区(那个啤酒杯)本身的容量,那么结果就如同啤酒一样,四处溢出,数据也会溢出存储空间!装不下的啤酒会流到桌子上,而装不下的数据则会覆盖在合法数据上,这就是缓冲区和缓冲区溢出的道理。”
“当然在理想的情况下,程序检查每个数据的长度,并且不允许超过缓冲区的长度大小,就像在倒啤酒的时候,啤酒要冒出杯子时我们就停止。但有些程序会假设数据长度总是与所分配的存储空间相匹配,而不作检查,从而为缓冲区溢出埋下隐患。”
“0K,那我们如何利用缓冲区溢出呢?在一般情况下,就像啤酒会到处流满桌面一样,溢出的数据会覆盖掉任何数据、指针或内容。除了破坏之外,对攻击者来说没有任何好处。但我们可引导溢出的数据,使计算机执行我们想要的命令。这就是很多漏洞公告上说的:‘黑客可以用精心构造的数据……’。道理就是这样。“
“如果初学,还不熟悉这个概念,可先把缓存区溢出利用理解为:允许攻击者往某个程序变量中放一个比期望长度要长的值,由此以当前运行该程序的用户的特权执行任意命令。”
“而具体的利用和方法,就是如何精心构造。”老师最后说道。
“大家都听明白了吗”老师问大家。
“呜啦!”同学们高高兴兴的说,“明白什么是缓冲区溢出了!就是啤酒和杯子嘛!那我们就开始学习注入了吧,老师!”
“晕哦!这只是最基本的缓冲区溢出原理,要想掌握利用还早着呢!都坐下!”老师又好气又好笑的说。
待大家又安静下来后,老师说:“学缓冲区溢出要有信心,但也不能浮躁,知道缓冲区的系统结构吗?知道怎么执行我们想要的程序吗?”
大家摇摇头:“不知道。”
“好,既然不知道,那就安安静静的坐好,马上就是最关键的地方了。”
同学们问道:“什么关键的地方呢”
“就是Windows系统内部处理缓冲区的机制,我们明白了这个之后,就可利用缓冲区溢出漏洞来控制别人的主机。”
“哇,好哦!”教室里一阵欢呼。
“为了讲清楚Windows是如何处理缓冲区的,我们先看一段很简单的程序。考虑到大家的初学基础,我会尽量少提复杂的代码,多作感性的讲解。”
“是啊,一看复杂的代码就头大,这样好啊!”又一阵欢呼。
“对,我的目的就是让你们知道那些复杂代码是如何来的,而理解了下面这个很简单的程序,再看其他的那些程序时,会发现思路基本上都是一样的。”
“嗯!好!那快给我们看看那段程序吧!”大家都迫不及待了。
“大家的热情都很高嘛,好的!”
溢出例子一一报错对话框
“这个很简单的程序是这样的。”
- /*很简单的程序:*/
- #include <stdio.h>
- #include <string.h>
- char name[] = "ww0803" ;
- int main()
- {
- char output[8];
- strcpy(output, name);
- for(int i=0;i<8&&output;i++)
- printf("\\0x%x",output);
- return 0;
- }
复制代码 “这个程序,还比较简单吧?”老师小心翼翼的问台下的同学。台下一阵默然,同学们都在头晕中呢……
“好,我来解释一下!这是用C语言写的程序!”老师说。
台下一阵狂晕:“地球人都知道!”
“不要急,我一句一句的来。首先,#include<stdio.h>和#include<string.h>是包含这两个头文件进来,因为后面使用的strcpy函数和printf函数是这两个头文件中定义的。”
“嗯,这个清楚了。”
“然后,char name[] =“ww0803”;是把‘name’这个数组赋值,往里面放入‘ww0803’这几个字符。”
“哦,为什么要放入‘ww0803’而不放其他的呢?”大家疑惑不解。
“哈哈!这个并不重要,什么都可以放,后面大家就会看到的。然后是int main() {……},这个是重点啦,这就是我们常说的主函数!程序进来就是先找到这个地方,执行里面的语句。”
“接下来的char output[8]; strcpy(output, name);就是让系统给output变量分配8个char的空间,然后把‘name’里面装的字符拷贝给它。”
“strcpy(des,source)这个拷贝函数是把第二个参数source的值拷给第一个参数des。它不检查拷贝的长度,它会一直拷贝,直到source到结尾。这就是它的弱点了!”
“下面的 for(int i=0; i<8&&output;i++) printf("\\0x%x",output);只是让大家方便检查output里面的值而已,我把它以16进制的形式打出来。”
台下听得聚精会神,一片安静。
“好,让我们运行一下,看看结果吧!在VC中编译、链接、执行,如下图”
1
“看,打出来的是\0x77\0x77\0x30\0x38\0x30\0x33,77就是“w”的16进制表示。而30、38、30、33就是0803的16进制表示。程序运行后一切正常,把‘ww0803’的16进制打出后,安全退出了。”
“嗯,呼……”台下长出了一口气,连几个女生也说道,“对,明白了,明白了。”
“呵呵,大家明白了就好。刚才大家不是问为什么要输入‘ww0803’吗,好!那我稍微改一下,改成其他的。“
“这次我把‘name[]’的值赋成‘abcdefgh’,大家再看看运行的结果有什么问题没有!如下图。”
台下的同学使劲的看啊看,什么都没看出来,最后玉波小心翼翼的问:“只是把‘abcdefgh’的十六进制61、62、63等打出来嘛,有什么问题吗?”
老师瞟了一眼,然后说道:“对!其实就是没有问题!”
台下狂倒……
“不是啊,不是啊”老师忙解释,“目的是让大家清楚的看下面真正的玄机。”
“这次我把‘name’再改长点,改成‘abcdefghijklmnopqrstuvwxyz’再运行,如下图”
3
“哦哦哦……不得了了,出错了!”台下一阵恐慌。
“哈哈!不要急。”老师摆出一副天塌下来自己顶的模样。“大家来仔细看看这个出错的警告是什么。”
“是0x706f6e6d引用的0x706f6e6d内存,该内存不能为read。”玉波喃喃的念道。
“你们不觉得‘6d6e6f70’这些很熟悉吗?我们的第二个程序中打出的……”老师提示道。
“哦!‘abcd’是‘61626364’,那么‘6d6e6f70’就应该是mnop了。”那位瘦瘦的同学一阵埋头苦算后说道!
“Good,这位同学能不能给大家介绍一下自己呢?”
那位瘦瘦的同学说道,“我叫古风。”
从古风灰灰的衣服、黑黑的脸上能看出他是从农村来的。他的眼神里带着中华民族勤奋刻苦的优良传统。
“大家都要向古风同学习啊!”老师说道,“我给一个对应转换表吧,以后大家直接查就可以了。”老师打出下面所示的表格:
4
堆栈和溢出
“现在大家清楚了,刚才的程序是输入到mnop时出错了,那究竟是什么原因呢?在‘name[]’比较短时不会有问题,反而在比较长的时候出错。”
大家眉头紧缩:“不知道。”
老师说:“嗯,这个就涉及到Windows的运行机制了。”
“哇!岂不是很难啊!”
“No,只要理解了两个概念,再结合实际分析一下,就很简单了。我会用很简单的语言给大家解释。”
“第一个概念是中断。我举一个日常生活中的例子来说明,假如你正在给朋友写信,电话铃响了。这时,你放下手中的笔去接电话。通话完毕,再继续写信。这个例子就表现了中断及其处理过程:电话铃声使你暂时中止当前的工作,而去处理更为急需处理的事情(接电话),把急需处理的事情处理完毕之后,再回头来继续原来的事情。”
“第二个概念是堆栈。计算机为了能回头继续处理原来的事情,就需要把原来指令的指针EIP保存在堆栈中;当要回去原来的地方时,就把保存在堆栈中的EIP恢复即可。并且各个函数的局部变量的分配也是在堆栈中。”
台下似懂非懂。
“好,我们看看刚才那个程序就清楚了。
小知识——PUSH和POP
堆栈是一数据结构,遵循“先进后出,后进先出”的规则,就像我们平时叠盘子一样,先放在下面的最后才能取出来,最后放上去的最先取出来。而在操作系统中,存和取的动作就是PUSH和POP。PUSH放一个数据到堆栈中去,POP取一个堆栈中的数据出来。
溢出报错的原因分析
“第一次我们输入的只是‘abcdefgh’。因为要进入main函数,所以系统把之前的EIP和EBP保存在堆栈中,便于以后恢复;然后为‘output[8]’在堆栈中分配8个char,拷贝‘abcdefgh’到其中。要注意的是,Windows下堆栈的分配是高址往低址分配的,其结构如下图。”
“这样在执行完main函数后,只要把保存在堆栈中的EBP、EIP恢复回去,就可继续原来的执行过程而没有任何问题。”
同学们点点头:“大概明白了,呵呵!”
“好,那第二次输入‘abcdefghijklmnopqrstuvwxyz’时,output分配的还是8个字节,但却拷了26个字母进来,和前面比较,其结果如下图所示。”
6
“大家注意了!由于拷贝的字母过长,不仅把分配给output的8个字节占据完了,而且还继续往下,把保存的EBP和EIP给占据了。”
“当执行完main函数后,系统要恢复EBP、EIP,而EIP已经被我们覆盖成ponm(即6d6e6f70)了。但系统不知道,就会去执行‘6d6e6f70’位置的东东。而那个位置是不可读的,所以就会出错。”
“乌拉!就是啊!”台下一片欢腾。
“呵呵,现在大家想想,我们可通过覆盖EIP为任意值来让程序运行到一个错误的地方,那如果我们特意把EIP覆盖成我们想去的程序的地方,那会怎么样呢?”
“我想,应该会运行我们‘想要的程序’吧!”一位浓眉大眼的同学说道,他叫宇强。
“很好!”老师以赞许的目光看了一眼宇强,“就是这样的。我们来看看‘想要的程序’的编写吧!”
Shell Code编写简介
“一般来说,我们把‘想要的程序’称为Shell Code。Shell最先指人机交互界面,而这里的Shell Code不仅仅指交互了,还可以是实现任意功能的代码。”
“Shell Code的编写很深奥,涉及很多方面,在以后的课程中我们会作详细讨论,这里就不多说了。只给个例子。”
“我们‘想要的程序’功能最好是能够开一个DOS窗口,那我们就可以做很多事情,比如下面这个程序。”
- #include<windows.h>
- int main()
- {
- LoadLibrary("msvcrt.dll");
- system ("command.com");
- return 0;
- }
复制代码 “大家看!执行一个command.com就可获得一个DOS窗口,在C库函数里面,语句system(“command.com”);将完成我们需要的功能。”
小知识:
Windows不像Unix那样使用系统调用来实现关键函数。Windows通过动态链接库来提供系统函数,就是所谓的Dll。
“system函数由msvcrt.dll (the Microsoft Visual C++Runtime library)提供,所以要想执行system,必须首先使用LoadLibrary(“msvcrt.dll”);装载动态链接库msvcrt.dll,之后才能调用system函数。”
“OK我们执行,看看效果吧!弹出一个DOS对话框!如下图,可以执行dir、copy等命令。”
7
“乌拉,太神奇了!”大家一片欢腾,都觉得不可思议。
“呵呵,现在我们把程序改为机器码,可能你们过去也看到过,别人的程序中有很多诸如\x01\xff\x3f\rff一类的东东,那些就是程序的机器码。也把我们的程序变成机器码吧!”
“可是怎么变呢?”几位女生有些疑惑。
“很简单!”老师说道,“在VC中按F10调试,然后在Debug工具栏中点击最后一个按钮‘Disassemble’,这样就出现了源程序的汇编代码;再在代码窗口上点击鼠标右键,在弹出菜单中选择‘Code Bytes’,这样就出现了机器码,如下图!”
8
“哦,那我们把它抄下来就可以了?”古风高兴的说道,埋头就要写。
“不!”老师阻止到,“其实还要作相关的一些工作之后才能直接抄取机器码,ShellCode的编写将在后面的课程中讲到。”
“哦!”古风耸耸肩,遗憾的说道,“我不怕辛苦,不怕做累人的活。”
“呵呵,以后有机会的,大家先这么认为ShellCode是这样生成的吧,我直接给大家一个开DOS窗口的机器码。”老师在影屏上打出来。
- char ShellCode[]=
- {
- 0x8B,0xE5,0x55,0x8B,0xEC,0x83,0xEC,0x0C,0xB8,0x63,0x6F,0x6D,0x6D,0x6D,0x6D,0x6F,0x63,0x89,0x45,0xF4,0xB8,0x61,0x6E,0x64,0x2E,0x89,0x45,0xF8,0xB8,0x63,0x6F,0x6D,0x22,0x89,0x45,0xFC,0x33,0xD2,0x88,0x55,0xFF,0x8D,0x45,0xF4,0x50,0xB8,0x24,0x98,0x01,0x78,0xFF,0xD0
- };
复制代码
老师说:“接下来,我们把这些背景知识连起来,写一个真正的利用程序!”
窥豹一斑一一本地缓冲区溢出简单利用
“首先,我们分析一下现在拥有的资源。”
“1.我们知道了‘有问题程序’返回点的精确位置,意思就是我们可把它覆盖成任意地址,让计算机执行那个地址的代码。”
“2.我们有了ShellCode(—个可以提供DOS窗口的代码)。”
“3.那接下来,大家想想,我们应该做什么呢?”
“嗯……”同学们陷入了沉思。
宇强紧锁眉头,突然灵感一亮,说道:“莫非把‘有问题程序’的返回点地址覆盖成我们ShellCode的地址?”
“VeryGood!这三步就是缓冲区溢出攻击的基本原理和精髓!”
ShellCode的定位
老师说:“现在我们有了前两步,返回点定位和ShellCode的编写,现在只需完成第三步一把返回点覆盖成ShellCode的地址,就可成功利用缓冲区溢出了!”
“哈哈,太好了!”玉波的口水都要流出来啦……
“现在的问题就是:ShellCode所在地址是多少呢?即我们把返回地址覆盖成多少?”
“呜……好像不好办啊……”
“嗯,在以前很多朋友提出了不少方法来定位ShellCode,但都不精确。随着技术的发展,1999年dark spyrit AKA Barnaby Jack提出了一个天才的想法:用系统核心dll里的指令来完成跳转!这一技巧开创了一个崭新的Windows缓冲区溢出思路!”
小知识:
过去(尤其是在Unix下),提出过的覆盖方法主要有两种:
1.NNNNNNNNNSSSSSSSSSS*RRRRRRRRRRRRRR型。适合于大缓冲区,“N”代表空指令,也就是0x90,在实际运行中,程序将什么也不做,而是一直延着这些NOPS运行下去,直到遇到不是NOPS的指令再执行之;“S”代表ShellCode;“R”代表覆盖的返回地址,思路是把返回地址R覆盖为nops的大概位置,这样就会跳到Nop中,然后继续执行,直到我们的ShellCode中。但这种方法由于定位不准确,所以使用起来也不准确。
2. RRRRRRRRRRNNNNNNNNNNNSSSSSSSSSS型。是用大量的“R”填满整个缓冲区,然后大量的Nop,最后是ShellCode。这里,“R”往后跳到Nop中,再顺着往下执行就会到ShellCode中。但在Windows下,“R”中必定会含有0,这样,整个构造就会被截断,只能用于Unix中。
Windows的系统核心dll包括kernel32.dll、user32.dll、gdi32.dll。这些dll—直位于内存中,而且对应于固定的版本,Windows加载的位置是固定的。
老师继续说:“我们来看看在Windows下如何利用系统核心dll里的指令来完成跳转吧。我们用系统核心dll中的jmp esp地址来覆盖返回地址,而把ShellCode紧跟在后面,这样就可跳转到我们的ShellCode中。其利用格式是NNNNNNRSSSSSS,N=Nop,S=ShellCode,R=jmpesp的地址”
同学们急了:“等一下,为什么用JMP ESP的地址覆盖就可以跳到后面的ShellCode中呢?”
“这里是关键的地方,理解了这个就理解了整个缓冲区溢出攻击!下面是详细的讲解,大家注意跟上。你们看,覆盖后的缓冲区如下图所示:
...未完
|