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
中大量的使用了结构体,而且还覆写了很对系统定义的结构体,加入自己定义的属性。所在进行抽离的时候有些结构体会出现属性不存在的情况,这种就是作者进行了覆写。