-
-
[原创]ms08-067及msf exploit调试与分析
-
发表于: 2021-6-22 15:25 12809
-
netapi32.dll版本: Win XP SP2
NetpwPathCanonicalize
→ CanonicalizePathName
→ RemoveLegacyFolder
RemoveLegacyFolder
函数用于移除路径中的.\
和..\
。
测试代码:
一开始没注意0day提供的源码,直接对之前调试payload的代码进行了一些修改,结果不管怎么改在输出can_path
的时候结果都不对。经过调试,发现代码中获取的can_path
的地址是错误的。
以我浅薄的编程经验,觉得问题只能出在MYPROC
这个类型的声明或者Trigger
的调用上,因为这里没有按照正常的函数调用语法来写,而是自己定义了一个函数类型。但是具体是什么原因就不清楚了。
最终经过和0day提供的源码的比较与多次修改实验,最终才发现MYPROC
这个类型的定义上面使用了__stdcall
调用约定。
查找资料注意到:
__cdecl
是C/C++和MFC程序默认使用的调用约定,也可以在函数声明时加上__cdecl
关键字来手工指定。采用__cdecl
约定时,函数参数按照从右到左的顺序入栈,并且由调用函数者把参数弹出栈以清理堆栈。因此,实现可变参数的函数只能使用该调用约定。
__stdcall
调用约定用于调用Win32 API函数。采用__stdcal
约定时,函数参数按照从右到左的顺序入栈,被调用的函数在返回前清理传送参数的栈,函数参数个数固定。由于函数体本身知道传进来的参数个数,因此被调用的函数可以在返回前用一条ret n
指令直接清理传递参数的堆栈。
也就是说NetpwPathCannonicalize
函数已经自己完成了堆栈的清理,如果没有使用__stdcall
调用约定,那么我们的代码还会自己再做一次堆栈清理,结果堆栈就不平衡了。这就是导致can_path
没有正常输出的原因。
以下是RemoveLegacyFolder
的注释版本代码:
关键变量的位置如图所示:
只需要执行一次wcscpy(cur_pos, cur_pos+2)
,然后继续向后扫描即可。
先执行一次wcscpy(slash_pre_pos, cur_pos+2)
,然后更新slash_pos=slash_pre_pos
, cur_pos=slash_pre_pos
,最后前向扫描,更新slash_pre_pos
。
漏洞就出现在前向扫描for ( j = slash_pre_pos - 1; *j != '\\' && j != path; --j )
这里,如果输入参数path开始是\\aaa\\..\\bbb
这样的形式(\\..\\
前面只出现一次斜杠,且斜杠位于首位),那么slash_pre_pos
就指向了开头的斜杠,减一之后就直接超出了path
的范围。
我们要做的就是在slash_pre_pos
指向path
外部之后,让程序执行wcscpy
,且复制的目的地址使用的是slash_pre_pos
。
看一下漏洞函数中主循环的执行流程,其中黄色方块会更新slash_pre_pos为slash_pos,所以在漏洞发生后,不能让程序执行到这里,发生我们期望的wcscpy调用位于蓝色方块,因此我们需要让程序的执行路径符合途中红线的描述:
因此构造这样一个路径:\aaa\..\..\bbb。执行之后得到的结果:
虽然返回结果是成功的,但是从获得的路径结果来看,第二次..\的消除确实出现了问题,接下来拖到OD里面验证一下:
上面这张图片执行到了前向搜索循环的位置,此时已经找到了一个前面的斜杠,从图中可以看到,输入参数path
位于0x0012F718
的位置,返回地址位于0x0012F6FC
,而程序找到的斜杠位于0x0012F1EE
的位置。
也就是说从\bbb
开始,我们需要0x0012F6FC-0x0012F1EE=0x50e=1294
个字节,才能覆盖到返回地址。在之后wcscpy
函数调用情况:
程序确实将后面的\bbb
复制到了搜索到的0x0012F1EE
这里了。
虽然能够成功将可控的字符串复制到path
的外部,但是1294个字节显然太大了,远远超过了path
允许的长度,所以现在面临一个问题,怎样在栈的低址放入一个\
。
通过搜索,找到了一个方法:在调用NetpwPathCanonicalize
之前,先调用NetpwNameCompare
函数,但是问题在于,这种方法在本地测试固然有效,可是如果想要远程利用,又该怎么办呢?所以我决定尝试调试msf提供的exploit。
调试方法参考了这篇文章,在被调试的机器上执行wmic process where caption="svchost.exe" get caption,handle,commandline
,得到提供SMB服务的svchost进程(netsvcs):
然后使用OD附加到这个进程上,在NetpwPathCanonicalize
函数上设置断点,同时找到 CanonicalizePathName
和 RemoveLegacyFolder
函数,设置断点,F9继续运行。
在kali上配置好MSF,exploit开始攻击(在正是开始之前我先实验了一下,攻击是可以成功的,注意打好快照):
OD就会断在NetpwPathCanonicalize
函数上,然后F9,最终到达RemoveLegacyFolder
函数,再逐步调试,找到程序前向搜索的位置,设置条件断点WORD [EAX] == 0x5C
,F9
让程序断在条件断点处:
可以看到程序搜索到的斜杠位于0x0014AF444
处,返回地址位于0x14AF478
处,原路径参数位于0x14AF494
。斜杠的位置距离当前栈顶可以说非常近了,应该就是在调用本函数之前调用其他函数留下来的数据。
现在想要弄明白0x0014AF444
这里的斜杠是怎么来的。
把快照回退到刚调用CanonicalizePathName
的时候,逐步调试,发现在执行完CheckDosPathType
之后,0x0014AF444
的内容变成了5C
。
这个函数的参数就是完整的路径,在开头就调用了RtlIsDosDeviceName_U
的函数,进一步调试发现5C
就是在这个函数调用完之后出现的。接下来看一下相关函数的代码:
可以看到RtlIsDosDeviceName_U
函数在栈中分配一个空间作为DestinationString
的存储空间,然后将其作为参数传入了RtlInitUnicodeStringEx
函数。RtlInitUnicodeStringEx
函数在DestinationString
中设置了长度信息,然后把路径参数(SourceString
)复制了进去。
在OD中,观察到DestinationString
位于地址0x0014AF458
,该地址位于我们的目标地址0x0014AF444
的高处,不会覆盖目标地址,因此RtlInitUnicodeStringEx
函数不是我们想要找的函数,继续往下调试。
接下来调用了RtlIsDosDeviceName_Ustr
函数,注意这里有一个判断条件,RtlInitUnicodeStringEx
函数的返回值要≥0,根据代码来看只要路径长度不超过0x7FFE
就没问题。
调试RtlIsDosDeviceName_Ustr
函数,可以发现该函数中的DestinationString
所在地址正好就在0x0014AF444
,所以关键代码就在函数RtlInitUnicodeString
中,该函数的第一个参数指向了我们关注的目标地址,用于存储UNICODE结果字符串,第二个参数是payload最后一段字符串(最后一个\
字符后面)。
该函数的最后一句代码修改了0x0014AF444
!!!
继续调试回到正常的exploit流程(这里我理解有误,结果导致要重新调试,所以基址发生了变化),在要执行wcscpy
函数的时候,F7步入。
为什么这里要F7步入?
导致我重新调试的原因就在这里,我一开始一直F8步过,结果程序总是直接退出了,后来我才意识到原因。
之前实验中接触的栈溢出,使用strcpy(dest, src)
才实现溢出,dest
位于当前栈帧栈顶的高地址处,所以溢出时覆盖的是当前栈帧的放回地址。但是这次实验发生溢出的目标地址位于当前栈帧栈顶的低地址处,发生溢出时覆盖的实际上是调用wcscpy
函数栈帧所对应的返回地址。
这里有一点后面会提到,就是在retn
之前的pop ebp
指令,将返回地址前面的0x00020408
放入了ebp
中。
可以看到返回地址被覆盖成了0x58FC16E2
(通过查看内存,这个地址位于AcGenral.dll
中),进入这个地址,可以看到程序调用了NtSetInformationProcess
,
在0day关于绕过DEP的部分介绍过这个方法:
一个进程的DEP设置标识位于KPROCESS
结构中的_KEXECUTE_OPTIONS
上,该标识可以通过API函数ZwQueryInformationProcess
和ZwSetInformationProcess
查询和修改。
这里有一个问题,NtSetInformationProcess
的第三个参数是一个指向0x2
的地址,这个地址通过寄存器ebp
获得,因此ebp+0x8
需要指向一个可写的地址,这也是我们前面提到pop ebp
的原因。
关闭DEP之后,返回地址为0x58FBF727
,这里执行了call esi
的指令,这里就是跳板指令了,此时esi
指向了0x01A0F496
,这个地址在payload
中,执行了一个jmp
指令EB 62
,跳转到真正的代码(前面还有一部分垃圾指令),之后程序进行了解码操作,然后跳转到解码后的代码处:
总结下来payload在内存中的变化情况如下:
其中黄色字段的几个关键值用红色标记,解释如下:
根据上面的分析,payload的总体结构如下:
#include <windows.h>
#include <stdio.h>
typedef
int
(__stdcall
*
MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);
int
main(
int
argc, char
*
argv[])
{
WCHAR path[
256
];
WCHAR can_path[
256
];
DWORD
type
=
1000
;
int
retval;
HMODULE handle
=
LoadLibrary(
".\\netapi32.dll"
);
MYPROC Trigger
=
NULL;
if
(NULL
=
=
handle)
{
wprintf(L
"Fail to load library!\n"
);
return
-
1
;
}
Trigger
=
(MYPROC)GetProcAddress(handle,
"NetpwPathCanonicalize"
);
if
(NULL
=
=
Trigger)
{
FreeLibrary(handle);
wprintf(L
"Fail to get api address!\n"
);
return
-
1
;
}
path[
0
]
=
0
;
wcscpy(path, L
"\\aaa\\..\\..\\bbbb"
);
can_path[
0
]
=
0
;
type
=
1000
;
wprintf(L
"BEFORE: %s\n"
, path);
retval
=
(Trigger)(path, can_path,
1000
, NULL, &
type
,
1
);
wprintf(L
"AFTER : %s\n"
, can_path);
wprintf(L
"RETVAL: %s(0x%X)\n\n"
, retval?L
"FAIL"
:L
"SUCCESS"
, retval);
FreeLibrary(handle);
return
0
;
}
#include <windows.h>
#include <stdio.h>
typedef
int
(__stdcall
*
MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);
int
main(
int
argc, char
*
argv[])
{
WCHAR path[
256
];
WCHAR can_path[
256
];
DWORD
type
=
1000
;
int
retval;
HMODULE handle
=
LoadLibrary(
".\\netapi32.dll"
);
MYPROC Trigger
=
NULL;
if
(NULL
=
=
handle)
{
wprintf(L
"Fail to load library!\n"
);
return
-
1
;
}
Trigger
=
(MYPROC)GetProcAddress(handle,
"NetpwPathCanonicalize"
);
if
(NULL
=
=
Trigger)
{
FreeLibrary(handle);
wprintf(L
"Fail to get api address!\n"
);
return
-
1
;
}
path[
0
]
=
0
;
wcscpy(path, L
"\\aaa\\..\\..\\bbbb"
);
can_path[
0
]
=
0
;
type
=
1000
;
wprintf(L
"BEFORE: %s\n"
, path);
retval
=
(Trigger)(path, can_path,
1000
, NULL, &
type
,
1
);
wprintf(L
"AFTER : %s\n"
, can_path);
wprintf(L
"RETVAL: %s(0x%X)\n\n"
, retval?L
"FAIL"
:L
"SUCCESS"
, retval);
FreeLibrary(handle);
return
0
;
}
int
__stdcall RemoveLegacyFolder(wchar_t
*
path)
{
/
/
[COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL
-
"+"
TO EXPAND]
start_pos
=
path;
cur
=
*
path;
slash_pos_
=
0
;
slash_pre_pos
=
0
;
slash_pos
=
0
;
if
(
*
path
=
=
'\\' || cur == '
/
' )
{
pos_2
=
path[
1
];
if
( pos_2
=
=
'\\' || pos_2 == '
/
' ) // 如果path以'
\\\\
'或'
/
/
'开头,直接定位到下一个斜杠处
/
/
这里是针对驱动的符号链接名吗?
{
for
( i
=
path
+
2
; ;
+
+
i )
{
cur_pos
=
*
i;
if
(
*
i
=
=
'\\' || cur_pos == '
/
' )
/
/
找到了下一个斜杠
break
;
if
( !cur_pos )
return
0
;
}
if
( !
*
i )
return
0
;
start_pos
=
i
+
1
;
/
/
斜杠的下一个位置,按理应该是正常的路径字符了
cur
=
*
start_pos;
path
=
start_pos;
if
(
*
start_pos
=
=
'\\' || cur == '
/
' ) // 如果斜杠的下一个位置还是斜杠,即路径以'
\\\\\\
'或'
/
/
/
'开头,就失败直接返回
0
return
0
;
}
}
cur_pos_
=
start_pos;
if
( !cur )
return
1
;
while
(
1
)
{
if
( cur
=
=
'\\'
)
{
if
( slash_pos_
=
=
cur_pos_
-
1
)
/
/
路径中存在双斜杠就失败
return
0
;
slash_pre_pos
=
slash_pos_;
slash_pos
=
cur_pos_;
goto next_pos;
}
if
( cur !
=
'.'
|| slash_pos_ !
=
cur_pos_
-
1
&& cur_pos_ !
=
start_pos )
goto next_pos;
v6
=
cur_pos_
+
1
;
v7
=
cur_pos_[
1
];
if
( v7
=
=
'.'
)
/
/
遇到
'..'
的情况,v7是第二个
'.'
{
v8
=
cur_pos_[
2
];
if
( v8
=
=
'\\'
|| !v8 )
{
if
( !slash_pre_pos )
/
/
如果在遇到
'/../'
之前没有遇到其他的斜杠,就出错返回
return
0
;
_wcscpy(slash_pre_pos, cur_pos_
+
2
);
/
/
消除了
'/../'
if
( !v8 )
return
1
;
slash_pos
=
slash_pre_pos;
cur_pos_
=
slash_pre_pos;
for
( j
=
slash_pre_pos
-
1
;
*
j !
=
'\\'
&& j !
=
path;
-
-
j )
/
/
向前扫描,找到前一个斜杠的位置
/
/
漏洞就在这里,slash_pre_pos
-
1
可能已经在path的外面了
;
start_pos
=
path;
slash_pre_pos
=
(wchar_t
*
)(
*
j
=
=
'\\'
? (unsigned
int
)j :
0
);
/
/
更新slash_pre_pos的值
}
goto next_pos;
}
if
( v7 !
=
'\\'
)
break
;
if
( slash_pos_ )
/
/
遇到
'.'
的情况
{
v14
=
slash_pos_;
}
else
{
/
/
遇到
'./'
之前,前面没有其他的斜杠
v6
=
cur_pos_
+
2
;
/
/
'./'
之后的第一个字符
v14
=
cur_pos_;
}
_wcscpy(v14, v6);
/
/
消除了
'./'
start_pos
=
path;
check_no_end:
cur
=
*
cur_pos_;
if
( !
*
cur_pos_ )
return
1
;
slash_pos_
=
slash_pos;
}
if
( v7 )
{
next_pos:
+
+
cur_pos_;
goto check_no_end;
}
if
( slash_pos_ )
cur_pos_
=
slash_pos_;
*
cur_pos_
=
0
;
return
1
;
}
int
__stdcall RemoveLegacyFolder(wchar_t
*
path)
{
/
/
[COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL
-
"+"
TO EXPAND]
start_pos
=
path;
cur
=
*
path;
slash_pos_
=
0
;
slash_pre_pos
=
0
;
slash_pos
=
0
;
if
(
*
path
=
=
'\\' || cur == '
/
' )
{
pos_2
=
path[
1
];
if
( pos_2
=
=
'\\' || pos_2 == '
/
' ) // 如果path以'
\\\\
'或'
/
/
'开头,直接定位到下一个斜杠处
/
/
这里是针对驱动的符号链接名吗?
{
for
( i
=
path
+
2
; ;
+
+
i )
{
cur_pos
=
*
i;
if
(
*
i
=
=
'\\' || cur_pos == '
/
' )
/
/
找到了下一个斜杠
break
;
if
( !cur_pos )
return
0
;
}
if
( !
*
i )
return
0
;
start_pos
=
i
+
1
;
/
/
斜杠的下一个位置,按理应该是正常的路径字符了
cur
=
*
start_pos;
path
=
start_pos;
if
(
*
start_pos
=
=
'\\' || cur == '
/
' ) // 如果斜杠的下一个位置还是斜杠,即路径以'
\\\\\\
'或'
/
/
/
'开头,就失败直接返回
0
return
0
;
}
}
cur_pos_
=
start_pos;
if
( !cur )
return
1
;
while
(
1
)
{
if
( cur
=
=
'\\'
)
{
if
( slash_pos_
=
=
cur_pos_
-
1
)
/
/
路径中存在双斜杠就失败
return
0
;
slash_pre_pos
=
slash_pos_;
slash_pos
=
cur_pos_;
goto next_pos;
}
if
( cur !
=
'.'
|| slash_pos_ !
=
cur_pos_
-
1
&& cur_pos_ !
=
start_pos )
goto next_pos;
v6
=
cur_pos_
+
1
;
v7
=
cur_pos_[
1
];
if
( v7
=
=
'.'
)
/
/
遇到
'..'
的情况,v7是第二个
'.'
{
v8
=
cur_pos_[
2
];
if
( v8
=
=
'\\'
|| !v8 )
{
if
( !slash_pre_pos )
/
/
如果在遇到
'/../'
之前没有遇到其他的斜杠,就出错返回
return
0
;
_wcscpy(slash_pre_pos, cur_pos_
+
2
);
/
/
消除了
'/../'
if
( !v8 )
return
1
;
slash_pos
=
slash_pre_pos;
cur_pos_
=
slash_pre_pos;
for
( j
=
slash_pre_pos
-
1
;
*
j !
=
'\\'
&& j !
=
path;
-
-
j )
/
/
向前扫描,找到前一个斜杠的位置
/
/
漏洞就在这里,slash_pre_pos
-
1
可能已经在path的外面了
;
start_pos
=
path;
slash_pre_pos
=
(wchar_t
*
)(
*
j
=
=
'\\'
? (unsigned
int
)j :
0
);
/
/
更新slash_pre_pos的值
}
goto next_pos;
}
if
( v7 !
=
'\\'
)
break
;
if
( slash_pos_ )
/
/
遇到
'.'
的情况
{
v14
=
slash_pos_;
}
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!