-
-
[原创]ms08-067及msf exploit调试与分析
-
2021-6-22 15:25 11950
-
netapi32.dll版本: Win XP SP2
1. 漏洞存在位置
NetpwPathCanonicalize
→ CanonicalizePathName
→ RemoveLegacyFolder
RemoveLegacyFolder
函数用于移除路径中的.\
和..\
。
2. 漏洞原理
测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | #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 ; } |
2.1 与漏洞无关的问题
一开始没注意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
没有正常输出的原因。
2.2 漏洞函数功能实现分析
以下是RemoveLegacyFolder
的注释版本代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | 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 ; } |
关键变量的位置如图所示:
2.2.1 消除./
只需要执行一次wcscpy(cur_pos, cur_pos+2)
,然后继续向后扫描即可。
2.2.2 消除../
先执行一次wcscpy(slash_pre_pos, cur_pos+2)
,然后更新slash_pos=slash_pre_pos
, cur_pos=slash_pre_pos
,最后前向扫描,更新slash_pre_pos
。
2.3 漏洞触发尝试
漏洞就出现在前向扫描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。
2.4 msf exploit调试与分析
2.4.1 确认前向搜索斜杠位置
调试方法参考了这篇文章,在被调试的机器上执行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
这里的斜杠是怎么来的。
2.4.2 斜杠的由来
把快照回退到刚调用CanonicalizePathName
的时候,逐步调试,发现在执行完CheckDosPathType
之后,0x0014AF444
的内容变成了5C
。
这个函数的参数就是完整的路径,在开头就调用了RtlIsDosDeviceName_U
的函数,进一步调试发现5C
就是在这个函数调用完之后出现的。接下来看一下相关函数的代码:
1 2 3 4 5 6 7 8 9 10 11 | int __stdcall RtlIsDosDeviceName_U(PCWSTR String) { int result; / / eax UNICODE_STRING DestinationString; / / [esp + 0h ] [ebp - 8h ] BYREF if ( RtlInitUnicodeStringEx(&DestinationString, String) < 0 ) result = 0 ; else result = RtlIsDosDeviceName_Ustr(&DestinationString); return result; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | int __stdcall RtlInitUnicodeStringEx(PUNICODE_STRING DestinationString, PCWSTR SourceString) { size_t len ; / / eax unsigned __int16 nbytes; / / ax if ( !SourceString ) { DestinationString - >Length = 0 ; DestinationString - >MaximumLength = 0 ; DestinationString - > Buffer = 0 ; return 0 ; } len = wcslen(SourceString); if ( len < = 0x7FFE ) { nbytes = 2 * len ; DestinationString - >Length = nbytes; DestinationString - >MaximumLength = nbytes + 2 ; DestinationString - > Buffer = (wchar_t * )SourceString; return 0 ; } return 0xC0000106 ; } |
可以看到RtlIsDosDeviceName_U
函数在栈中分配一个空间作为DestinationString
的存储空间,然后将其作为参数传入了RtlInitUnicodeStringEx
函数。RtlInitUnicodeStringEx
函数在DestinationString
中设置了长度信息,然后把路径参数(SourceString
)复制了进去。
在OD中,观察到DestinationString
位于地址0x0014AF458
,该地址位于我们的目标地址0x0014AF444
的高处,不会覆盖目标地址,因此RtlInitUnicodeStringEx
函数不是我们想要找的函数,继续往下调试。
接下来调用了RtlIsDosDeviceName_Ustr
函数,注意这里有一个判断条件,RtlInitUnicodeStringEx
函数的返回值要≥0,根据代码来看只要路径长度不超过0x7FFE
就没问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | int __stdcall RtlIsDosDeviceName_Ustr(UNICODE_STRING * String) { / / [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL - "+" TO EXPAND] v1 = String - > Buffer ; v2 = 0 ; v3 = RtlDetermineDosPathNameType_U(v1); if ( v3 > = 0 ) { if ... if ... } str = String - > Buffer ; * (_DWORD * )&DestinationString.Length = * (_DWORD * )&String - >Length; / / 这里修改的其实也是 0x0014AF444 ,只不过内容还不是 5C w_len = String - >Length >> 1 ; DestinationString. Buffer = str ; if ... if ... / / 忽略路径最后可能存在的 ':' ,payload中没有 if ... last_char = str [w_len - 1 ]; do... / / 忽略路径最后可能存在的 '.' 或者空格,payload中没有 v7 = 0 ; if ( w_len ) { for ( i = & str [w_len - 1 ]; i > = str ; - - i ) / / 从后向前遍历 { v9 = * i; if ( * i = = '\\' || v9 == ' / ' || v9 == ' : ' && i == str + 1 )// 搜索' \'字符能够找到 { next_char = i + 1 ; v11 = * next_char | 0x20 ; / / 转大写 if ( v11 ! = 'l' && v11 ! = 'c' && v11 ! = 'p' && v11 ! = 'a' && v11 ! = 'n' ) / / '\'后面的字母应该为' L ' ' C ' ' P ' ' A ' ' N'中的一个,否则直接返回 return 0 ; v7 = (char * )next_char - (char * ) str ; RtlInitUnicodeString(&DestinationString, next_char); / / 关键步骤在这个函数中 str = DestinationString. Buffer ; w_len = (DestinationString.Length >> 1 ) - v2; DestinationString.Length + = - 2 * v2; break ; } } |
调试RtlIsDosDeviceName_Ustr
函数,可以发现该函数中的DestinationString
所在地址正好就在0x0014AF444
,所以关键代码就在函数RtlInitUnicodeString
中,该函数的第一个参数指向了我们关注的目标地址,用于存储UNICODE结果字符串,第二个参数是payload最后一段字符串(最后一个\
字符后面)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | / / 记住参数Destination指向的就是 0x0014AF444 void __stdcall RtlInitUnicodeString(PUNICODE_STRING DestinationString, PCWSTR SourceString) { PCWSTR v2; / / edi int v3; / / ecx bool v4; / / zf unsigned int v5; / / ecx v2 = SourceString; * (_DWORD * )&DestinationString - >Length = 0 ; / / 这里设置目标地址为 0 DestinationString - > Buffer = (wchar_t * )SourceString; if ( SourceString ) { v3 = - 1 ; do / / 找到字符串末尾 { if ( !v3 ) break ; v4 = * v2 + + = = 0 ; - - v3; } while ( !v4 ); v5 = 2 * ~v3; / / 计算所占字节长度 if ( v5 > 65534 ) LOWORD(v5) = - 2 ; DestinationString - >MaximumLength = v5; DestinationString - >Length = v5 - 2 ; / / 这里设置了 0x0014AF444 的数值为所占字节长度 - 2 } } |
该函数的最后一句代码修改了0x0014AF444
!!!
2.4.3 payload分析
继续调试回到正常的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
查询和修改。
1 2 3 4 5 6 | ULONG ExecuteFlags = MEM_EXECUTE_OPTION_ENABLE; ZwSetInformationProcess( NtCurrentProcess(), / / (HANDLE) - 1 ProcessExecuteFlags, / / 0x22 &ExecuteFlags, / / ptr to 0x2 sizeof(ExecuteFlags)); / / 0x4 |
这里有一个问题,NtSetInformationProcess
的第三个参数是一个指向0x2
的地址,这个地址通过寄存器ebp
获得,因此ebp+0x8
需要指向一个可写的地址,这也是我们前面提到pop ebp
的原因。
关闭DEP之后,返回地址为0x58FBF727
,这里执行了call esi
的指令,这里就是跳板指令了,此时esi
指向了0x01A0F496
,这个地址在payload
中,执行了一个jmp
指令EB 62
,跳转到真正的代码(前面还有一部分垃圾指令),之后程序进行了解码操作,然后跳转到解码后的代码处:
总结下来payload在内存中的变化情况如下:
其中黄色字段的几个关键值用红色标记,解释如下:
2.4.4 总结
根据上面的分析,payload的总体结构如下:
[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法