前言:mimikatz是法国人Gentil Kiwi编写的一款windows平台下的神器,它具备很多功能,想必很多人都尝试用过此工具,其中最亮点的功能是直接从 lsass.exe 进程里获取windows处于active状态账号的明文密码——没错你可以直接用该工具从朋友那里借来电脑然后猜出它的密码(当然这是违法行为→ܫ←)。
1.mimikatz工具基本使用
mimikatz的使用方法网上比比皆是,在此便不过多赘述。只介绍本人重点分析的功能wdigest的使用过程及相应输出:
- 首先用管理员权限打开mimikatz,进行提权操作:
privilege::debug
得到Privilege ‘20’ OK说明提权成功。 - 然后执行sekurlsa::wdigest,提取口令明文代码,即可看到本机密码了。(当然这里的提权是在win7虚拟机中执行的)
提取密码这么隐私的方式,居然只需两条指令就可以搞定,细想起来确实有点恐怖。但是这两条指令究竟干了些啥?别着急,后面将会进行调试分析。
2.调试工具及环境
本调试过程在win10下的Visual Studio 2019下进行,在调试mimikatz源码经过一些失败的尝试后,发现github上发行的源码版本是release版本,无法进行调试。因此我们需要修改为debug模式。
2.1 下载源码
在github上 https://github.com/gentilkiwi/mimikatz 下载mimikatz源码。
2.2 设置debug模式
为VS2019添加调试配置
新建debug方案:
并且将平台设置为x64
这时候项目生成会有如下错误:
点击项目,右击选择属性->配置属性->c/c++->常规,将“警告视为错误”的选项改为“否”。就可以将这些报错忽视掉了。
然后再按照下面进行相应的配置:
调试这个工具的时候,发现新版本没生成debug模式的选项,F11调试没有对应到源代码。仔细一看原来是没有生成调试文件。
设置让Release的程序也生成pdb文件即可。
另外mimikatz没有debug版,所以导致有些参数被优化了。没有办法跟进具体的变量和函数,所以还需要把以下选项调成Debug模式可用的设置。
这样就可以下断点进行调试了。
3. 源码分析及调试
3.1 wdigest总体设计
sekurlsa::wdigest这条命令其实就是从 lsass.exe 进程里获取windows处于active状态账号的明文密码。当然前提是mimikatz已经提权了。
其总体函数调用逻辑图如下:
3.2 详细代码调试及分析
1.定义了一个状态status,这里的STATUS_SUCCESS数值是1,也就是表示正常的状态。下面的len和input是在区分powershell和cmd环境,如果是Powershell环境下运行mimikatz,那么就不定义len和input这两个变量。
2.调用mimikatz_begin(),然后有一个循环。
下面看一下mimikatz_begin()这个函数:
可以看到是设置串口标题和句柄,以及输出了一些mimikatz的界面信息,如版本号和logo等等。
调试到该函数以后,可以看到命令行输出的内容:
下面看一下这个循环:
首先速览定义看这些变量的值:
再根据其命名可以看出,i从1开始增长,循环跳出的条件是i不小于第一个参数并且status的进程或者线程终结,循环中执行的内容就是读入一个参数然后在调用mimikatz_dispatchCommand函数,这个函数就是处理操作的函数,我们在后面再进行详细分析。
3.上面的循环是有命令行参数时会执行的循环,而接下来是一个while循环,也就是我们此次调试中会用到的循环。
受限看到循环跳出的条件是进程或者线程结束,然后再输出MIMIKATZ#,也就是我们每次看到的:
接下来的if条件就是读取我们的输入了,输入的内容保存到input中,长度为len。
然后将输入的换行符转换为字符串的结束符。
之后调用kprintf_inputline函数:
这里其实就是使用指向参数列表的指针写入格式化。
4.接下来就是mimikatz_dispatchCommand函数,也就是处理我们的输入指令的函数。
首先是用kull_m_file_fullPath函数将input处理以后的返回值赋给full了。
可以看到这里是调用了两个系统函数:ExpandEnvironmentStrings和LocalAlloc函数。
ExpandEnvironmentStrings 函数
扩展环境变量字符串,并使用当前用户定义的值来替换这些环境变量字符串。如果成功,返回值是存储于目标缓冲器中的TCHARS的数量,包括结尾的NULL。如果目标缓冲器太小以至于不能装载这些字符串,返回值是所需的缓冲器的大小(单位是字符)。如果函数失败,返回零值。
LocalAlloc函数
这个函数从堆中分配指定大小的字节数。
所以这个函数其实就是判断扩充环境变量后的缓冲区长度是否和原来一样。
可以看到返回值还是缓冲区中的字符串,也就是我们的输入:
然后判断第一个字符是否是’!’和’*’。都不是,进入mimikatz_doLocal函数。
5.mimikatz_doLocal函数
首先定义了一些变量:
这里有一个CommandLineToArgvW函数:
commandlinetoargvw的使用如下:
MFC也可以像控制台那样获取命令行参数:
利用GetCommandLineW()函数获得命令行参数,
利用CommandLineToArgvW()函数解析命令行参数。LPWSTR *szArglist = NULL; int nArgs = 0; szArglist = CommandLineToArgvW(GetCommandLineW(), &nArgs); if( NULL != szArglist) { //szArglist就是保存参数的数组 //nArgs是数组中参数的个数 //数组的第一个元素表示进程的path,也就是szArglist[0],其他的元素依次是输入参数。 } //取得参数后,释放CommandLineToArgvW申请的空间 LocalFree(szArglist);
所以可以介绍一些这些变量:
status:状态
argv:保存参数的数组
argc:数组中参数的个数
indexModule:选择的模组下标
indexCommand:选择的命令行下标
moduleFound和commandFound:模组和命令行是否找到。
下面一段代码是用’::’将输入分割开,前面的是module,后面的是command。如果没有module的话直接将所有的都定义为command。
然后在mimikatz的功能列表中查找有无对应的模组和命令。找到以后就会执行该命令,返回status。
接下来就是对没有找到对应的模组和命令时的操作:
最后就是free释放空间了。
在主函数的分析中我们可以看到,执行语句是在下图处:
因此,在执行到status那一行时,我们可以看一下局部变量:
按F11进入函数:
发现是调用了kuhl_m_privilege_simple函数:
从这里就可以发现,其实提权操作就是调用了RtlAdjustPrivilege这个函数。
RtlAdjustPrivilege一个实用的函数。这个函数封装在NtDll.dll中(在所有用户态DLL加载之前,被内核中的PsLocateSystemDll函数加载),没有被微软的MSDN公开。函数的定义(Winehq给出):NTSTATUS RtlAdjustPrivilege(ULONG Privilege,BOOLEAN Enable,BOOLEAN CurrentThread,PBOOLEAN Enabled)。
仅凭这一个函数就可以获得进程ACL的任意权限!
参数的含义:
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.
// 输出原来相应权限的状态(打开 | 关闭)用法很简单:
#define SE_DEBUG_PRIVILEGE 0x14 //DEBUG 权限
int s;
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE,true,false,&s);
如果提权成功,就会输出Privilege ‘20’ OK
否则就会输出失败。
执行到调用该命令时,局部变量如下:
F11进入:
3.2.3.1 kuhl_m_sekurlsa_getLogonData函数
这里需要介绍一些结构体:
KUHL_M_SEKURLSA_GET_LOGON_DATA_CALLBACK_DATA 表示获得的登录信息的反馈数据:
PKUHL_M_SEKURLSA_PACKAGE表示通过sekurlsa获得的一些数据
PKUHL_M_SEKURLSA_ENUM_LOGONDATA也就是PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA记录了所有的登录信息。
KUHL_M_SEKURLSA_LIB表示利用的模组:
KULL_M_PROCESS_VERY_BASIC_MODULE_INFORMATION表示基本模组的信息:
所以OptionalData这个变量其实主要就是用于记录登录信息和模组的信息。
3.2.3.2 kuhl_m_sekurlsa_enum函数
先定义了一些变量:
然后调用kuhl_m_sekurlsa_acquireLSA函数,获取系统LSA信息,
该函数的详细介绍请参考见3.2.3.3kuhl_m_sekurlsa_acquireLSA函数。
成功以后,设置session的信息和helper的值。
后面的主要内容就是在获取一些地址和内存的信息了,主要我们介绍这个while循环:
首先拷贝内存,然后赋值相应的sessionData的各种信息,这些信息也就是我们查询到的用户的信息,并且通过调用kull_m_process_getUnicodeString和kull_m_process_getSid来得到进程的其他信息。
接下来调用callback函数,其实这个callback函数就是之前的参数,如下图:
也就是kuhl_m_sekurlsa_enum_callback_logondata函数:
这个函数就是输出获取到的用户信息了。
在后面就是释放内存。
3.2.3.3 kuhl_m_sekurlsa_acquireLSA函数
前面定义了一些变量以后,来到一个判断:
是判断hLsassMem是否存在,也就是是否已经获取过了lsass进程的内存空间。其实这里就是一个检错的标志,按正常流程走的话这个if是一定满足的。
然后就是判断是否有其他的进程需要dump。如果没有的话就是调用
kull_m_process_getProcessIdForName函数获取”lsass.exe”进程的pid。并用OpenProcess打开进程,记录句柄。
句柄获取成功后,进入kull_m_memory_open函数
首先分配空间,然后根据之前定义的Type选择不同的操作,这里是KULL_M_MEMORY_TYPE_PROCESS,所以操作如下:
也就是为相应的句柄分配空间并且做一些定义。
再回到kuhl_m_sekurlsa_acquireLSA函数,根据Type,操作如下:
对cLsass的系统上下文做了赋值。
接下来如果没有错误的话,就会有一些设置,然后就会调用
kull_m_process_getVeryBasicModuleInformations
从函数名字可以看出,就是从进程中获取基本的模组信息,然后用kuhl_m_sekurlsa_utils_search函数通过模组查询其他的必要信息。
查询成功之后会调用kuhl_m_sekurlsa_nt6_acquireKeys,获取进程信息相应的keys。
由于kuhl_m_sekurlsa_nt6_acquireKeys函数过长,不方便截图,只好作为代码粘贴在此。
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;
if(kull_m_memory_search(&aLocalMemory, currentReference->Search.Length, &sMemory, FALSE))
{
aLsassMemory.address = (PBYTE) sMemory.result + currentReference->Offsets.off0;
#if defined(_M_ARM64)
if(aLsassMemory.address = kull_m_memory_arm64_getRealAddress(&aLsassMemory, currentReference->Offsets.armOff0))
{
#elif defined(_M_X64)
aLocalMemory.address = &offset64;
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(LONG)))
{
aLsassMemory.address = (PBYTE) aLsassMemory.address + sizeof(LONG) + offset64;
#elif defined(_M_IX86)
aLocalMemory.address = &aLsassMemory.address;
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(PVOID)))
{
#endif
aLocalMemory.address = InitializationVector;
if(kull_m_memory_copy(&aLocalMemory, &aLsassMemory, sizeof(InitializationVector)))
{
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;
}