oiweiwei / go-msrpc

The DCE/RPC / MS-RPC Codegen/Client for Go
MIT License
42 stars 1 forks source link

PAC for Windows #10

Closed dodgyturtle closed 2 weeks ago

dodgyturtle commented 3 weeks ago

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:

&{Version:0 LogonInformation:0xc000254140 ServerChecksum:0xc000298540 KDCChecksum:0xc000298700 
ClientNameAndTicketInformation:0xc0002988c0 ConstrainedDelegationInformation:<nil> 
UPNAndDNSInformation:0xc000234380 TicketChecksum:<nil> Attributes:<nil> RequestorSID:S-1-5-21-640530401- 
2225873020-3008125168-1104 ExtendedKDCChecksum:<nil> RequestorGUID:}

PAC in Base64:

BwAAAAAAAAABAAAA0AEAAHgAAAAAAAAABgAAABAAAABIAgAAAAAAAAcAAAAQAAAAWAIAAAAAAAAKAAAAGAAAAGgCAAAAAAAADAAAAIgAAACAAgAAAAAAABEAAAAIAAAACAMAAAAAAAASAAAAHAAAABADAAAAAAAAARAIAMzMzMzAAQAAAAAAAAAAAgBcqMw9QgPbAf////////9//////////3/BKUU2CP3aAcHprmDR/doB/////////38OAA4ABAACAA4ADgAIAAIAAAAAAAwAAgAAAAAAEAACAAAAAAAUAAIAAAAAABgAAgApAAAAUAQAAAECAAABAAAAHAACACAAAAAAAAAAAAAAAAAAAAAAAAAADAAOACAAAgAOABAAJAACACgAAgAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACwAAgAAAAAAAAAAAAAAAAAHAAAAAAAAAAcAAAB3AGkAbgB1AHMAZQByAAAABwAAAAAAAAAHAAAAdwBpAG4AdQBzAGUAcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAgAABwAAAAcAAAAAAAAABgAAAEEARABIAE8AUwBUAAgAAAAAAAAABwAAAEMATwBOAFQATwBTAE8AAAAEAAAAAQQAAAAAAAUVAAAA4bctJnwgrITwWEyzAQAAADAAAgAHAAAAAQAAAAEBAAAAAAASAQAAAAAAAAAQAAAAoqX5P1vXIWQbmpFyEAAAAEB/ab4bAZM9zVY47YDoqkBJA9sBDgB3AGkAbgB1AHMAZQByACYAGAAWAEAAAgAAAA4AWAAcAGgAAAAAAHcAaQBuAHUAcwBlAHIAQABDAE8ATgBUAE8AUwBPAC4AQwBPAE0AAABDAE8ATgBUAE8AUwBPAC4AQwBPAE0AAAB3AGkAbgB1AHMAZQByAAAAAQUAAAAAAAUVAAAA4bctJnwgrITwWEyzUAQAAAAAAAACAAAAAQAAAAEFAAAAAAAFFQAAAOG3LSZ8IKyE8FhMs1AEAAAAAAAA

Struct:

&{Version:0 LogonInformation:0xc000142140 ServerChecksum:0xc000131dc0 KDCChecksum:0xc000131f80 ClientNameAndTicketInformation:0xc00016a140 ConstrainedDelegationInformation:<nil> UPNAndDNSInformation:0xc0001602a0 TicketChecksum:0xc00016b340 Attributes:<nil> RequestorSID:S-1-0-0 ExtendedKDCChecksum:0xc00016b500 RequestorGUID:}

PAC in Base64:

BwAAAAAAAAABAAAA0AEAAHgAAAAAAAAABgAAABAAAABIAgAAAAAAAAcAAAAQAAAAWAIAAAAAAAAKAAAAGAAAAGgCAAAAAAAADAAAAIgAAACAAgAAAAAAABAAAAAQAAAACAMAAAAAAAATAAAAEAAAABgDAAAAAAAAARAIAMzMzMzAAQAAAAAAAAAAAgDlwbVASQPbAf////////9//////////3/BKUU2CP3aAcHprmDR/doB/////////38OAA4ABAACAA4ADgAIAAIAAAAAAAwAAgAAAAAAEAACAAAAAAAUAAIAAAAAABgAAgAqAAAAUAQAAAECAAABAAAAHAACACAAAAAAAAAAAAAAAAAAAAAAAAAADAAOACAAAgAOABAAJAACACgAAgAAAAAAAAAAABACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAACwAAgAAAAAAAAAAAAAAAAAHAAAAAAAAAAcAAAB3AGkAbgB1AHMAZQByAAAABwAAAAAAAAAHAAAAdwBpAG4AdQBzAGUAcgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAABAgAABwAAAAcAAAAAAAAABgAAAEEARABIAE8AUwBUAAgAAAAAAAAABwAAAEMATwBOAFQATwBTAE8AAAAEAAAAAQQAAAAAAAUVAAAA4bctJnwgrITwWEyzAQAAADAAAgAHAAAAAQAAAAEBAAAAAAASAQAAAAAAAAAQAAAAliPZdao3/msALytTEAAAADi+s1QVfKejjSC1fgCjQH9JA9sBDgB3AGkAbgB1AHMAZQByACYAGAAWAEAAAgAAAA4AWAAcAGgAAAAAAHcAaQBuAHUAcwBlAHIAQABDAE8ATgBUAE8AUwBPAC4AQwBPAE0AAABDAE8ATgBUAE8AUwBPAC4AQwBPAE0AAAB3AGkAbgB1AHMAZQByAAAAAQUAAAAAAAUVAAAA4bctJnwgrITwWEyzUAQAAAAAAAAQAAAAfzYRafhnEkm3tadkEAAAAIhYEUM/2kurr+PKMA==
oiweiwei commented 3 weeks ago

@dodgyturtle, added support for PAC Attribute Info structure marshaling / unmarshaling: https://github.com/oiweiwei/go-msrpc/commit/3cf99cbe187503276d68a5a36a7203e86420f079

dodgyturtle commented 3 weeks ago

Were you able to check the result after unmarshal/marshal? Are they the same?

oiweiwei commented 3 weeks ago

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.

oiweiwei commented 3 weeks ago

same for UPN and DNS information structure, it computes its own offsets in the raw buffer, so it will also be slightly different.

krasnovu commented 3 weeks ago

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?

oiweiwei commented 3 weeks ago

@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.

oiweiwei commented 3 weeks ago

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.

oiweiwei commented 3 weeks ago

@krasnovu I've added zeroed-out signatures data the way it is implemented in gokrb5 library, maybe it will help to verify the data.

krasnovu commented 3 weeks ago

@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.

oiweiwei commented 3 weeks ago

@krasnovu fixed

krasnovu commented 3 weeks ago

@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.

oiweiwei commented 3 weeks ago

@krasnovu i think u r right, fixed here: https://github.com/oiweiwei/go-msrpc/commit/7ef66382cbd35e28be4fd58227e6248f331ce733

krasnovu commented 3 weeks ago

@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)]).

oiweiwei commented 3 weeks ago

@krasnovu correct, i forgot to push the actual change: https://github.com/oiweiwei/go-msrpc/commit/37c3e0cabd2e735f892f0e6f5b672926f9096eb9

krasnovu commented 3 weeks ago

@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

krasnovu commented 3 weeks ago

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?

oiweiwei commented 3 weeks ago

@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.

oiweiwei commented 3 weeks ago

@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)
}
krasnovu commented 3 weeks ago

@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]

oiweiwei commented 3 weeks ago

@dodgyturtle @krasnovu good to hear!