首页
社区
课程
招聘
[原创]ms08-067及msf exploit调试与分析
2021-6-22 15:25 11950

[原创]ms08-067及msf exploit调试与分析

2021-6-22 15:25
11950

netapi32.dll版本: Win XP SP2

1. 漏洞存在位置

NetpwPathCanonicalizeCanonicalizePathNameRemoveLegacyFolder

 

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 函数上设置断点,同时找到 CanonicalizePathNameRemoveLegacyFolder 函数,设置断点,F9继续运行。

 

在kali上配置好MSF,exploit开始攻击(在正是开始之前我先实验了一下,攻击是可以成功的,注意打好快照):

 

图片描述

 

OD就会断在NetpwPathCanonicalize 函数上,然后F9,最终到达RemoveLegacyFolder 函数,再逐步调试,找到程序前向搜索的位置,设置条件断点WORD [EAX] == 0x5CF9让程序断在条件断点处:

 

图片描述

 

可以看到程序搜索到的斜杠位于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函数ZwQueryInformationProcessZwSetInformationProcess查询和修改。

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虚拟机自动化脱壳的方法

最后于 2021-6-22 15:34 被LarryS编辑 ,原因: 添加了一段代码
收藏
点赞5
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回