0x00 S-Mimikatz源码调试
前期准备
- mimikatz源码:地址
- 调试环境:
vs2019 -
几点设置:
-
因为官方项目没有debug方案,所以需要手动添加debug配置

-
项目属性配置



程序入口
调试以
privilege::debug为例。打开调试->mimikatz调试属性->配置属性->调试->命令参数
wmain()是mimikatz`的入口函数。
命令分发
从上面的循环中获取到请求参数之后就进入到命令分发的
mimikatz_dispatchCommand()函数。
这里首先有一个kull_m_file_fullPath方法,然后进行匹配,暂时不知道具体作用是什么,之后进入mimikatz_doLocal()方法。
命令执行
在对命令进行请求分发之后获取到
module和commond两个参数,之后就进入了命令执行的阶段,这个地方涉及到结构体的知识。
首先mimikatz_modules[]是一个数组,数组里面存放的是每一个模块的结构体的指针。那么第210行就是将module的值和每个模块结构体中定义的shortName进行比较,如果相同,返回0。
结构体的结构在kuhl_m.h这个头文件中进行定义。
之后第213和214两行相同的方式去寻找同一个模块下存在的command,每个模块都预先定义一个数组,存放全部的可执行方法的信息。
最重要的就是第215行,status = mimikatz_modules[indexModule]->commands[indexCommand].pCommand(argc - 1, argv + 1);,执行这个模块和命令。mimikatz_modules[indexModule]->commands[1]这一步相当于找到了kuhl_m_c_privilege[]这个数组的第一个元素,然后这个const KUHL_M_C kuhl_m_c_privilege[]数组,是一个结构体数组,这个第一项表示的是一个指针函数,那后面的.pCommand(argc - 1, argv + 1)就是去调用kuhl_m_privilege_debug这个函数。
可以看到的是对于privilege::debug这个功能,执行的函数是kuhl_m_privilege_simple(),而最后调用的系统API是RtlAdjustPrivilege()。
NTSTATUS kuhl_m_privilege_simple(ULONG privId)
{
ULONG previousState;
NTSTATUS status = RtlAdjustPrivilege(privId, TRUE, FALSE, &previousState);
if(NT_SUCCESS(status))
kprintf(L"Privilege \'%u\' OK\n", privId);
else PRINT_ERROR(L"RtlAdjustPrivilege (%u) %08x\n", privId, status);
return status;
}
至此,整个简单的流程分析已经结束了,关于
mimikatz的请求流程,和命令分发已经了解清楚了。
0x01 S-Mimikatz_msv模块
模块介绍
在
mimikatz中msv模块的作用是枚举LM和NTLM凭证,KUHL_M_C结构体中的描述是Lists LM & NTLM credentials,根据之前分析的命令分发过程,sekurlsa::msv最终通过函数指针调用函数kuhl_m_sekurlsa_msv()
- 函数文件位置
kuhl_m_seckurlsa_msv1_0.c
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}};
const PKUHL_M_SEKURLSA_PACKAGE kuhl_m_sekurlsa_msv_single_package[] = {&kuhl_m_sekurlsa_msv_package};
NTSTATUS kuhl_m_sekurlsa_msv(int argc, wchar_t * argv[])
{
return kuhl_m_sekurlsa_getLogonData(kuhl_m_sekurlsa_msv_single_package, 1);
}
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_PACKAGE,此处可以看到ModuleName的值设置为lsasrv.dll,这个也是抓取NTML的重点模块。

-
结构体
_KIWI_BASIC_SECURITY_LOGON_SESSION_DATA

msv原理
在上面给结构体赋值的时候可以看到
msv模块用的的module是lsasrv.dll。而msv模块的原理便是首先从LSASS.exe进程中计算出lsasrv.dll这个模块的基地址,然后在lsasrv.dll模块中找到两个全局变量LogonSessionList和LogonSessionListCount,这个LogonSessionList中应该就保存当前活动的 Windows 登录会话列表。至于如何找这两个变量可以看参考文章中介绍的叫《内存签名》的方法。
个人理解就是在lsasrv.dll这个模块中找到一个函数LogonSessionListLock()同时使用了LogonSessionList和LogonSessionListCount两个变量作为参数,那么只要根据这个LogonSessionListLock()这个函数位置,加上偏移位置,就可以获取两个全局变量的为位置。通俗理解:LogonSessionListLock函数的起始地址是80065926,LogonSessionListCount变量的起始地址是80065922,LogonSessionList变量的起始地址8006593D,那经过计算LogonSessionListCount相对LogonSessionListLock的偏移是-4,LogonSessionList相对LogonSessionListLock的偏移是23,这个也正好对于mimikatz中的定义。那首先通过找到LSASS.exe进程,然后列举进程中全部的dll模块,计算出lsasrv.dll模块的基地址,然后根据LogonSessionListLock函数在lsasrv.dll模块中的便宜了找到这个函数的位置,然后再根据两个全局变量的相对位置,找到两个全局变量再内存中的位置。
代码调试
-
首先需要将程序以管理员权限进入调试模式,所以还需要进行简单的设置。

-
入口断点,通过命令分发进入功能模块。
kuhl_m_sekurlsa_msv()->kuhl_m_sekurlsa_getLogonData()->kuhl_m_sekurlsa_enum()->kuhl_m_sekurlsa_acquireLSA()

在功能入口下断点,然后一步步进入重要的功能函数
kuhl_m_sekurlsa_acquireLSA,调用路径kuhl_m_sekurlsa_msv()->kuhl_m_sekurlsa_getLogonData()->kuhl_m_sekurlsa_enum()->kuhl_m_sekurlsa_acquireLSA(),接下来着重看这个kuhl_m_sekurlsa_acquireLSA函数,这个函数在其他的模块中也相当重要。
-
kuhl_m_sekurlsa_acquireLSA函数

-
一路步过,最后停留在
kull_m_process_getProcessIdForName()函数
这个函数的作用就是通过进程名去获取进程ID,调用路线
kull_m_process_getProcessIdForName()->kull_m_process_getProcessInformation()->kull_m_process_NtQuerySystemInformation()->NtQuerySystemInformation(),所以最终调用的是NtQuerySystemInformation函数,这个函数是Ntdll.dll中的一个未公开的API方法,调用过程有点复杂,在之后自己复现msv中会写到。
经过上述的调用过程,返回之后就可以根据这个lsass.exe进程名,找到对于的pid为560,每个机器这玩意都不一样,但是没关系。之后根据进程id利用hData = OpenProcess(processRights, FALSE, pid)函数,获取句柄。
- 程序继续运行

打开句柄之后,首先调用这个
kull_m_memory_open给&cLsass.hLsassMem分配一块内存(KUHL_M_SEKURLSA_CONTEXT cLsass = {NULL, {0, 0, 0}};),然后对cLsass.osContext.MajorVersion等等三个属性赋值,这个赋值保存的是windows的相关信息。MIMIKATZ_NT_BUILD_NUMBER=19042,MIMIKATZ_NT_MINOR_VERSION=0,MIMIKATZ_NT_MAJOR_VERSION=10,不同机器这个值可能产生差异。
-
之后进入
kull_m_process_getVeryBasicModuleInformations(),这个函数用来获取LSASS.exe这个进程的基础信息,也会找到LSASRV.dll的基地址。

-
首先通过
kull_m_process_peb()方法获取LSASS.exe进程的peb位置,实际也是调用了NtQueryInformationProcess函数。


-
在
PEB的结构中有一个PEB.Ldr.InMemoryOrderModuleList的列表,这个列表记录了进程加载的模块地址和大小,接下来就是通国遍历来查找需要的LSASRV.dll模块。

-
当查找到
lsasrv.dll模块时,进入callback回调函数->kuhl_m_sekurlsa_findlibs()


首先看看这个
lsassPackages变量,是一个PKUHL_M_SEKURLSA_PACKAGE结构体,赋值如:const PKUHL_M_SEKURLSA_PACKAGE lsassPackages[] = {&kuhl_m_sekurlsa_msv_package,&kuhl_m_sekurlsa_tspkg_package,&kuhl_m_sekurlsa_wdigest_package,&kuhl_m_sekurlsa_credman_package,&kuhl_m_sekurlsa_kdcsvc_package,};。 而其中的&kuhl_m_sekurlsa_msv_package初始化的值为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}};。可以看到在这个结构体中是定义了mimikatz不同模块会使用的dll模块,和其余一些信息。在kuhl_m_sekurlsa_findlibs()函数中查找到sekurlsa::msv功能使用的是lsasrv.dll,且在LSASS.exe这进程中可以找到,便会将pModuleInformation这个结构体的信息存入kuhl_m_sekurlsa_msv_package这个结构体当中。
-
成功查找到
lsasrv.dll模块的相关信息便返回,然后进入kuhl_m_sekurlsa_utils_search函数当中,这个函数的作用就是寻找那两个全局变量了。




-
经过上面的查找,以及
msv的实现原理,基本上已经完成了任务,至于之后会进入到lsassLocalHelper->AcquireKeys(&cLsass, &lsassPackages[0]->Module.Informations)函数,感觉是用于计算加解密之类的,具体功能没有太理解。最后一步就是处理两个全局变量获取活动凭据信息,然后打印了。



像
mimikatz这种神作肯定还是要自己调试才能领悟其中一些精妙的地方,自己调试的时候便只有一句话,作者NB。即便调试了很多次,调试了很久,弄清了一个大概的流程,但是其中还有很多神奇的地方没有完全领会,而上述的内容可能也存在一些错误,欢迎指出。
粗糙的将MSV功能分离
在考虑到对
mimikatz进行免杀的时候,由于mimikatz功能较多,整体免杀的效果并不会很好,所以在考虑将常用的功能抽离出来,然后对这些功能进行分开免杀,这样的话效果可能会更好一些。在上述理解了msv的基本原理之后,动手将msv粗糙的抽离出来,忽略了亿点点细节,复用了亿点点代码。效果如下。代码地址:Ghost2097221-selfMimikatz
-
几个踩坑
-
NtQueryInformationProcess的加载方式
switch (memory->type)
{
case KULL_M_MEMORY_TYPE_PROCESS:
HMODULE hModule = LoadLibraryA("Ntdll.dll");//需要通过LoadLibraryA的方式引入dll,然后加载相关函数。
PFUN_NtQueryInformationProcess pfun = (PFUN_NtQueryInformationProcess)GetProcAddress(hModule, "NtQueryInformationProcess");
NTSTATUS a = pfun(hProcess, info, buffer, szBuffer, &szInfos);
if ((szInfos == szBuffer) && processInformations.PebBaseAddress)
{
aProcess.address = processInformations.PebBaseAddress;
status = kull_m_memory_copy(&aBuffer, &aProcess, szPeb);
}
break;
}
RtlGetNtVersionNumbers的加载方式
void GetSysInfo() {
//DWORD* MIMIKATZ_NT_MAJOR_VERSION, DWORD* MIMIKATZ_NT_MINOR_VERSION, DWORD* MIMIKATZ_NT_BUILD_NUMBER
//获取系统信息
HMODULE hDll = ::LoadLibrary("ntdll.dll");
typedef void (WINAPI* getver)(DWORD*, DWORD*, DWORD*);
getver RtlGetNtVersionNumbers = (getver)GetProcAddress(hDll, "RtlGetNtVersionNumbers");
RtlGetNtVersionNumbers(&MIMIKATZ_NT_MAJOR_VERSION, &MIMIKATZ_NT_MINOR_VERSION, &MIMIKATZ_NT_BUILD_NUMBER);
MIMIKATZ_NT_BUILD_NUMBER &= 0x00007fff;
}
mimikatz中大量的使用了结构体,而且还覆写了很对系统定义的结构体,加入自己定义的属性。所在进行抽离的时候有些结构体会出现属性不存在的情况,这种就是作者进行了覆写。
跳跳糖


















