Closed dodgyturtle closed 2 weeks ago
@dodgyturtle, added support for PAC Attribute Info structure marshaling / unmarshaling: https://github.com/oiweiwei/go-msrpc/commit/3cf99cbe187503276d68a5a36a7203e86420f079
Were you able to check the result after unmarshal/marshal? Are they the same?
it will still be slightly different because logon information is marshaled in different but compatible way: there is a little chance to have same binary representation even using Microsoft code, as logon information contain pointers which can be arbitrary value.
In turn, this may lead to checksum validation failure for the PAC info, as PAC buffer has changed during this Marhsal/Unmarshal.
same for UPN and DNS information structure, it computes its own offsets in the raw buffer, so it will also be slightly different.
Have a nice day! For example ServerSignature, first we need to Unmarshal the incoming PAC, then zeroize ServerSignature, KDCSignature, then Marshal PAC and finally VerifyChecksum. Do Windows developers really allow that the data is marshaled differently each time. Why do we need these signatures if we cannot verify them? Maybe there is another way to verify the signature that we don't know, we would appreciate it?
@krasnovu there are different ways how to do it, but I guess verifying the payload is different from parsing the PAC data to extract some information:
in case, when you need to verify the checksum, you can just parse the buffer layout (first make a copy of your input data into byte slice b
):
var pac PACType
if err := ndr.Unmarshal(b, &pac, ndr.Opaque); err != nil {
return fmt.Errorf("unmarshal_pac: headers: %w", err)
}
then identify where the server checksum resides:
var chkBuffer *PACInfoBuffer
for _, buffer := range pac.Buffers {
if buffer.Type != 0x00000006 {
chkBuffer = buffer
break
}
}
Then unmarshal the checksum buffer:
if chkBuffer != nil {
var v PACSignatureData
if err := ndr.Unmarshal(b, &v, ndr.Opaque); err != nil {
return fmt.Errorf("unmarhsal_pac: server_checksum: %w", err)
}
p.ServerChecksum = &v
}
then clear the buffer data where signature resides:
clear(b[chkBuffer.Offset : chkBuffer.Offset+uint64(chkBuffer.BufferLength)])
and then verify the checksum using input b
based on the zeroed-out signatures data. (i guess you need to zero-out all the signatures in the payload.
in other words, you don't have to marshal unmarshalled data if you have no purpose to alter the contents of the PAC buffer, you can use exactly the same binary data, else if you need to alter the PAC data you will have to recompute the server checksum and it will be different disregarding of what I've been writing about different logon information marshaling and so on.
@krasnovu I've added zeroed-out signatures data the way it is implemented in gokrb5 library, maybe it will help to verify the data.
@oiweiwei Thanks for your help! I wanted to check it out, but your go.mod has 1.20 version.
clear requires go1.21 or later compiler UnsupportedFeature
.
@krasnovu fixed
@oiweiwei, Thank you! But so far the server signature check has failed. Maybe I am wrong, but it seems to me that it is necessary to zero not the whole buffer, but only the Signature field in PAC_SIGNATURE_DATA, while SignatureType should be left untouched. See if I understand the assumption correctly? Assumed from these three links: one, two, three.
@krasnovu i think u r right, fixed here: https://github.com/oiweiwei/go-msrpc/commit/7ef66382cbd35e28be4fd58227e6248f331ce733
@oiweiwei It is correct that you should not zeroize TicketSignature
, but I was talking about something else in my post. Each signature (TicketSignature
, ExtendedKDCSignature
, ServerSignature
, KDCSignature
) is a PAC_SIGNATURE_DATA
structure with two fields SignatureType
and Signature
. In turn, this structure is referenced by an indentation in the Offset
field of the PAC_INFO_BUFFER
structure.
clear(p.ZeroSignatureRaw[buffer.Offset : buffer.Offset+uint64(buffer.BufferLength)])
.
This line removes the entire PAC_SIGNATURE_DATA
structure by PAC_INFO_BUFFER.Offset
, while only the PAC_SIGNATURE_DATA.Signature
field is needed.
I think if you correct it this way it will be correct clear(p.ZeroSignatureRaw[buffer.Offset+4 : buffer.Offset+uint64(buffer.BufferLength)])
.
@krasnovu correct, i forgot to push the actual change: https://github.com/oiweiwei/go-msrpc/commit/37c3e0cabd2e735f892f0e6f5b672926f9096eb9
@oiweiwei I checked it out. KDCSignature is always checked - that's easy since it's a co-signature.
ServerSignature is checked if you do this in the last commit:
if buffer.Type == 0x00000006 || buffer.Type == 0x00000007 /*||| buffer.Type == 0x00000013*/ {
But this is not how ExtendedKDCSignature is checked.
But if you return if buffer.Type == 0x00000006 || buffer.Type == 0x00000007 || buffer.Type == 0x00000013 {
, then ExtendedKDCSignature starts to be checked correctly, but ServerSignature stops being checked. :-) References: one(order in p.2), two
This is a test of the incoming PAC. Now how do I make the PAC myself so that the signatures are valid? I make a PAC with empty signatures, marshal the PAC into bytes, make a ServerSignature from these bytes. Then make a KDCSignature. Now I need to insert these signatures into the PAC. I make a PAC with the same data, insert the signatures and marshal it. But as we have found out above at the second marshal the bytes can differ from the signed ones, how to be here, how to do it better?
@krasnovu the marshaling result in the library will produce exactly the same result every time. difference would be only between different implementations of the marshaler (say, smb and microsoft and go-msrpc).
ideally, there should be an API that do following:
you construct PAC data with signatures empty.
generate the binary data and also receive this binary data layout (all offsets and lengths)
binaryData, pacBufferInfo, err := p.Marshal()
then using pacBufferInfo you can navigate over the binary data, insert signatures, generate new signatures and so on.
@krasnovu sort of like here: https://github.com/oiweiwei/go-msrpc/commit/eb1b248662a7666b0fc40d4c3ec3b64b07dd5a8c
You marshal the buffer only once, and then use Buffers variable being set to find and zero-out the signatures in the binary data:
p.Unmarshal(b)
// validate server signature.
for _, buf := range b.Buffers {
if buf.Type == 0x6 {
pac.ZeroOutSignatureData(b, buf)
// perform signature validation.
break
}
}
same for marshaling:
b, _ := p.Marshal()
var kdc, ekdc, srv *pac.PACBufferInfo
for _, buf := range p.Buffers {
if buf.Type == 0x06 {
srv = buf
}
// same for kdc, ekdc
}
if ekdc != nil {
// ... compute ekdc and write it
sign := ComputeExtendedKDCServerSignature(b)
FillInSignatureData(b, ekdc, sign)
}
if srv != nil {
// ... compute server signature and write it
sign := ComputeServerSignature(b)
FillInSignatureData(b, srv, sign)
}
@oiweiwei Thank you very much! You helped me a lot with the verification and signature generation. I think it was mutually beneficial, now your library is even better! :-)
I was able to verify three signatures out of four. 2024/09/11 21:29:08 !!! isValid KDC Signature = [true] 2024/09/11 21:29:08 !!! isValid Server Signature = [true] 2024/09/11 21:29:08 !!! isValid Extended Signature = [true] 2024/09/11 21:29:08 !!! isValid Ticket Signature = [false]
@dodgyturtle @krasnovu good to hear!
Hello. We are trying to create some PAC that will work with Windows Server 2022 Standard 10.0.20348. We get PAC from Windows, then unmarshal it to struct and this struct marshal back to PAC. And we have different result. I think, it is because package has no marshal/unmarshal for PACAttributesInfo. Could you please help to understand the problem.
PAC from Windows and struct after unmarshal.
Struct:
PAC in Base64:
Struct:
PAC in Base64: