缓冲区溢出攻击和实战war-ftp
前言
最近看到看雪学院有篇博客写的FUZZ测试和漏洞利用,里面有介绍war-ftp 1.65版本存在缓冲区漏洞,之前有做过war-ftp 1.65的缓冲区攻击,不过当时应付差事,匆忙一做,最近学了一段时间的逆向,现在重新做做,重新理解一下。(本来这篇博客快做好了,想着有些累就睡觉了,结果一觉醒来上传到搜狗图床的图没了,淦,结果又重做了一次截了图,思路都打断了,MD)
正文
什么是缓冲区溢出
这里引用一下百度的解释
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上。理想的情况是:程序会检查数据长度,而且并不允许输入超过缓冲区长度的字符。但是绝大多数程序都会假设数据长度总是与所分配的储存空间相匹配,这就为缓冲区溢出埋下隐患。操作系统所使用的缓冲区,又被称为“堆栈”,在各个操作进程之间,指令会被临时储存在“堆栈”当中,“堆栈”也会出现缓冲区溢出。
解释的很到位,但是如果了解学习过逆向和汇编的同学,我觉得下面这张图(从上面介绍的那篇博客中引用)可能会有帮助:
原博主的话:
图中函数F1在调用F2之前,F1会保存F2结束以后的返回地址,然后移交给F2执行,F2执行完毕后,根据记录的返回地址复原栈帧,继续执行F1函数。在图1中可以看出,当向一个有栈溢出漏洞的程序发送大量数据时,ESP、EBP等寄存器都可以被垃圾数据淹没。由于EIP寄存器存放下一条指令的地址,因此可以编写一个Exploit程序,使EIP指向预设的一段叫做Shellcode的指令,从而达到控制系统的目的。由于栈的布局原因,通常使用JMP ESP指令来覆盖返回地址,并且将Shellcode存放到ESP处,从而引导CPU执行Shellcode。
在函数被调用的的过程中,首先将参数放到寄存器或者压入栈中,然后调用call,对于栈来说:
PUSH EIP
PUSH EBP
MOV EBP,ESP
那么此时EBP所指向的地址中保存的就是old EBP的值,用来之后恢复栈,然后call之后会在栈中压入此时执行函数的局部变量,函数执行完毕时, 可以调用leave指令当结语,也可以调整ESP的位置,释放局部变量,最后使用ret指令返回,将CPU的控制权由被调函数返回给调用函数,对于栈结构来说则会:
MOV ESP,EBP
POP EBP
POP EIP
如何利用缓冲区溢出
如果被调用函数的局部变量超过了程序给它分配的空间,因为数据是从较低内存地址向较高内存地址增长的,而栈的结构是从较高内存地址向较低内存地址增长的,那么此时数据就会覆盖掉用来恢复原本栈结构的EIP内容,造成被调函数返回时,调用函数不能正确的恢复EIP的内容。
而此时发现当被调用函数返回时候,寄存器EIP中内容可控,那么攻击者则可以精心构造被调用函数的局部变量,从而使用特定的值来覆盖原本保存EIP的值。
为了能让CPU执行shellcode,有一种方式就是利用篡改的EIP的值,让EIP内的值指向JMP ESP指令,从而执行它,而ESP内的值是在POP EIP后,指向比覆盖EIP值的内存地址高的栈空间中,也是就在构造的覆盖EIP字节之后的几个字节中,只要找到偏移量,就可以定位到ESP指向的值,然后来设计shellcode
实战
这里我们用war-ftp 1.65版本来做实验,这个软件可谓是经典缓冲区溢出攻击靶场软件(笑),软件是用的类C语言编写的轻量级免费开源的FTP服务器软件,FTP客户端登陆时用户名存在缓冲区溢出漏洞:
(上图搜索结果显示password也有缓冲区溢出,看介绍应该是存在Windows 2000系统上,有机会测试测试)
测试环境:
操作系统:Windows XP SP3
测试软件:war-ftp 1.65
调试软件:Immunity debugger
ip地址:192.168.247.128
先FUZZ测试一下
可以看到当username过长时,程序中断崩溃,在immunity debugger上看到:
寄存器内容被覆盖(EIP内41是A的ASCII码)
这样,我们可以借用网上的POC
1 | import socket, sys |
测试结果:
输入1000时候,服务器崩溃连不上,那么说明在上一次500字符用户名的时候就已经出现缓冲区溢出了。
为了确定在字符中第几个能覆盖到EIP和EBP,使用mona插件来生成1000个字符计算偏移:
生成目录在immunity debugger安装目录下
用python测试一下pattern
1 | from ftplib import FTP |
测试结果:
可以发现 EIP=32714131 [ESP]=71413471 用mona插件来计算一下偏移:
EIP的偏移是485
ESP的偏移是493
这意味着,向用户名发送485个字节可以定位到EIP,8个字节之后则是ESP,而485个字节后的EIP寄存器开始被缓冲区覆盖,那么EIP中486-489字节是我们想要的目标。
接下来确定JMP ESP的地址,用immunity debugger中的executable modules来找到kernel32.dll系统文件,在kernel32.dll寻找JMP ESP的地址
发现有一条JMP ESP指令的地址是0x7C86467B,那么可以给EIP赋值为0x7C86467B,则CPU运行指令就会到ESP所指向的地址,也就是输入的第493个字节之后写入shellcode
接下来生成shellcode,这里我用的是Metasploit直接生成的方式,在kali机中输入
msfvenom -p windows/exec CMD=calc.exe EXITFUNC=thread -a x86 --platform windows -b '\x00\x0d\x0a\x40' -f c
其中,“-p”是指攻击载荷(payload),在windows/exec方式下可以打开计算器程序(calc.exe);“EXITFUNC”指定退出方式;“-a x86 –platform windows”指程序运行平台,“x86、windows”是默认选项,对于本程序来说可以忽略不填,可根据实际需要填写;“-b”是排除坏字符,因为传递的都是字符串,必须要遵守字符限制的FTP 协议。这就意味着没有空,返回,换行,或是@符号,他们用16进制的表示为\x00,\x0d,\x0a,\x40。“-f”是指以C程序的方式生成Shellcode,略作修改即可在Python中使用。
那么现在可以来写exp了,首先我们要对缓冲区写入485个’A’来定位到EIP,然后写入在kernel32.dll中指令JMP ESP的地址0x7C86467B,x86程序使用小端字节序为:\x7B\x46\x86\x7C,然后再填充4个字节以便到达shellcode入口处,为了能让shellcode顺利执行,我们需要填充多个nop指令,也就是\x90字节,让CPU滑到shellcode执行。
1 | from ftplib import FTP |
可以看到原程序崩溃,计算器以线程方式弹出
最后,我想说的是,其实ESP之后的栈空间也是具有限制的,比如本次实验中可以发现,当以1200+个字节内容进行测试时,最终停留在了一个名为msvcrt.dll的DLL文件中,而这里的EIP与41414141完全不同。另外,当payload大小为1100字节或以下时,仍然控制着EIP。
可以通过16进制计算得到此时EIP指向的地址在msvcrt.dll文件中
由此可以计算出exp可利用的空间,这一点在写payload的时候非常重要。