mimikatz源码调试以及MSV功能原理


0x00 S-Mimikatz源码调试

前期准备

  • mimikatz源码:地址
  • 调试环境:vs2019
  • 几点设置:

  • 因为官方项目没有debug方案,所以需要手动添加debug配置
    image-20211128165143816.png

  • 项目属性配置
    image-20211128165237182.png
    image-20211128165248490.png
    image-20211128165302598.png

程序入口

调试以privilege::debug为例。打开调试->mimikatz调试属性->配置属性->调试->命令参数
image-20211128165311755.png
wmain()是mimikatz`的入口函数。
image-20211128165319580.png

命令分发

从上面的循环中获取到请求参数之后就进入到命令分发的mimikatz_dispatchCommand()函数。
image-20211128165328586.png
这里首先有一个kull_m_file_fullPath方法,然后进行匹配,暂时不知道具体作用是什么,之后进入mimikatz_doLocal()方法。
image-20211128165336906.png

命令执行

在对命令进行请求分发之后获取到modulecommond两个参数,之后就进入了命令执行的阶段,这个地方涉及到结构体的知识。
image-20211128165345891.png
首先mimikatz_modules[]是一个数组,数组里面存放的是每一个模块的结构体的指针。那么第210行就是将module的值和每个模块结构体中定义的shortName进行比较,如果相同,返回0。
image-20211128165353077.png
image-20211128165405536.png
结构体的结构在kuhl_m.h这个头文件中进行定义。
image-20211128165412262.png
之后第213和214两行相同的方式去寻找同一个模块下存在的command,每个模块都预先定义一个数组,存放全部的可执行方法的信息。
image-20211128165419656.png
最重要的就是第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这个函数。
image-20211128165430312.png
490680811238072.png
可以看到的是对于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模块

模块介绍

mimikatzmsv模块的作用是枚举LMNTLM凭证,KUHL_M_C结构体中的描述是Lists LM & NTLM credentials,根据之前分析的命令分发过程,sekurlsa::msv最终通过函数指针调用函数kuhl_m_sekurlsa_msv()
image-20211128165832742.png

  • 函数文件位置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的重点模块。
    image-20211128165842766.png

  • 结构体_KIWI_BASIC_SECURITY_LOGON_SESSION_DATA
    image-20211128165850458.png

msv原理

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

代码调试

  • 首先需要将程序以管理员权限进入调试模式,所以还需要进行简单的设置。
    image-20211128165938846.png

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

在功能入口下断点,然后一步步进入重要的功能函数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函数
    image-20211128165953622.png

  • 一路步过,最后停留在kull_m_process_getProcessIdForName()函数

这个函数的作用就是通过进程名去获取进程ID,调用路线kull_m_process_getProcessIdForName()->kull_m_process_getProcessInformation()->kull_m_process_NtQuerySystemInformation()->NtQuerySystemInformation(),所以最终调用的是NtQuerySystemInformation函数,这个函数是Ntdll.dll中的一个未公开的API方法,调用过程有点复杂,在之后自己复现msv中会写到。

image-20211128170007779.png
image-20211128170019159.png
image-20211128170026828.png
经过上述的调用过程,返回之后就可以根据这个lsass.exe进程名,找到对于的pid560,每个机器这玩意都不一样,但是没关系。之后根据进程id利用hData = OpenProcess(processRights, FALSE, pid)函数,获取句柄。

  • 程序继续运行
    image-20211128170036571.png

打开句柄之后,首先调用这个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的基地址。
    image-20211128170048414.png

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

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

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

首先看看这个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函数当中,这个函数的作用就是寻找那两个全局变量了。
    image-20211128170210091.png
    436281316223510.png
    image-20211128170245805.png
    image-20211128170226510.png

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

mimikatz这种神作肯定还是要自己调试才能领悟其中一些精妙的地方,自己调试的时候便只有一句话,作者NB。即便调试了很多次,调试了很久,弄清了一个大概的流程,但是其中还有很多神奇的地方没有完全领会,而上述的内容可能也存在一些错误,欢迎指出。

粗糙的将MSV功能分离

在考虑到对mimikatz进行免杀的时候,由于mimikatz功能较多,整体免杀的效果并不会很好,所以在考虑将常用的功能抽离出来,然后对这些功能进行分开免杀,这样的话效果可能会更好一些。在上述理解了msv的基本原理之后,动手将msv粗糙的抽离出来,忽略了亿点点细节,复用了亿点点代码。效果如下。代码地址:Ghost2097221-selfMimikatz

image-20211128170614464.png
image-20211128170418673.png

  • 几个踩坑

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

评论

misift_Zero 2021-12-01 18:31:50

tql,大佬带带我

4ra1n 2021-12-02 01:10:07

tql,大佬带带我

张三爱李四

这个人很懒,没有留下任何介绍

twitter weibo github wechat

随机分类

APT 文章:6 篇
企业安全 文章:40 篇
安全开发 文章:83 篇
业务安全 文章:29 篇
Exploit 文章:40 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

草莓

收藏一下,目前见过最全的资料了

Y4tacker

Ps:纠错不是每个环境都是那样,之前很多时候/proc/self这些都可以,但是

苦咖啡

感谢大佬分享 863558996@qq.com

m1yuu

H

HaCky

stomppe 这部分有点问题,和官方文档有出入,文档中介绍开启这项功能,则会混

目录