oiweiwei / go-msrpc

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

Documentation for gssapi.Mechanism #8

Closed rtpt-erikgeiser closed 1 month ago

rtpt-erikgeiser commented 3 months ago

I'm trying to implement my own authentication mechanism, and I can't figure out whether to use the methods in gssapi.Mechanism or gssapi.MechanismEx and how to implement the wrapping and signing methods.

So far, I managed to get authentication to work by implementing Init using Windows build-in authentication API InitializeSecurityContext. However, I can't figure out how to make the signing and sealing work with MakeSignature/VerifySignature/EncryptMessage/DecryptMessage. I just don't understand what part of the input token has to be encrypted/signed and how the token payloads map to the SecBufferDesc of the Windows API. It would help a lot if there was documentation on how to implement a gssapi.Mechanism.

oiweiwei commented 3 months ago

Hello, @rtpt-erikgeiser! I was not anticipating the new authentication mechanism, so it was poorly documented. But, I can give a hint here:

gssapi.Mechanism and gssapi.MechanismEx differ only in a way of parameters they accept (which is Token and TokenEx). And here the only difference is Token contains single payload buffer and TokenEx contains a list of payloads.

From RPC perspective, MechanismEx is what you need to implement (gssapi.Mechansim is rather generic GSSAPI implementation which can be used for some other purpose), this is how the RPC constructs the TokenEx:

https://github.com/oiweiwei/go-msrpc/blob/main/dcerpc/security.go#L355-L375

I'm not familiar with SSPI winapi, but I've chanced to look at heimdal code for SSP, and it resembles the SecBufferDesc in some way. First of all, in TokenEx we have 3 buffers, in my implementation I use flags like gssapi.Integrity / gssapi.Confidentiality to guide the implemented SSPs what should be included into the signature (integrity is set) and what should be in-place encrypted (Confidentiality is set).

However, in reality these 3 buffers have dedicated meaning, that is Header, Data and Trailer. So in your case, when you implement MakeSignatureEx and retrieve the TokenEx with 3 payloads, you should map them into SecBufferDesc in following way:

i = 0
// add header if header signing is enabled 
if TokenEx.Payloads[0].Capabilities.IsSet(gssapi.Integrity)
    Buffer[i] = {BufferType: SECBUFFER_STREAM_HEADER | SECBUFFER_READONLY_WITH_CHECKSUM (?), Buffer: TokenEx.Payloads[0].Payload}
    i++

// add data.
Buffer[i] = {BufferType: SECBUFFER_DATA, Buffer: TokenEx.Payloads[1].Payload}
i++

// add security trailer if header-signing is enabled
if TokenEx.Payloads[2].Capabilities.IsSet(gssapi.Integrity)
    Buffer[i] = {BufferType: SECBUFFER_STREAM_TRAILER | SECBUFFER_READONLY_WITH_CHECKSUM (?), Buffer: TokenEx.Payload[2].Payload}

// UPD: i guess buffer must be allocated to return the Signature:
i++
Buffer[i] = {BufferType: SECBUFFER_TOKEN, Buffer: make([]byte, ???)}

For EncryptData I guess it will be the same. I'm not sure where tokEx.Signature should go for verify checksum and others (should it be in STREAM_TRAILER, or in the end of DATA buffer? Looking at the examples (https://learn.microsoft.com/en-us/windows/win32/secauthn/verifying-a-message), tokEx.Signature should be placed into dedicated buffer called SECBUFFER_TOKEN, like:

i++
Buffer[i] = {BufferType: SECBUFFER_TOKEN, Buffer: TokenEx.Signature}
rtpt-erikgeiser commented 2 months ago

Sorry for the late response and thank you for your detailed reply. I will be able to try your suggestions out soon, but I was wondering about sequence numbers that have to be passed to the SSPI APIs.

As far as I am aware, sequence numbers are normally specified in and tracked by the outer protocol (e.g. SMB). However, it seems like they are tracked inside the auth providers in this library, instead of being passed through a a part of the TokenEx structure. Is this something that could be changed or do you prefer it the way it is now or am not understanding the concepts correctly?

oiweiwei commented 2 months ago

@rtpt-erikgeiser you understand the concept correctly, but:

I've decided to implement it inside Authentifier objects. I guess it's pretty simple just to maintain SeqNo for sender and receiver inside the Authentifier.

you can try with simple model like for NTLM and see how it will work for you. (see examples here: https://github.com/oiweiwei/go-msrpc/blob/main/ssp/ntlm/authentifier.go#L376-L390)

rtpt-erikgeiser commented 2 months ago

Thank you so much for you help @oiweiwei, I managed to make it work. In the end it worked a little differently, but with the information in your hints i discovered this blog post. In the end, each payload had to be SECBUFFER_DATA with additional SECBUFFER_READONLY_WITH_CHECKSUM for the header and trailer. For my use case, I pretty much only need authentication with sealing enabled.

func (auth *sspiAPI) WrapEx(ctx context.Context, token *gssapi.MessageTokenEx) (*gssapi.MessageTokenEx, error) {
    sizes, err := auth.Sizes()
    if err != nil {
        return nil, fmt.Errorf("obtain maximum signature size: %w", err)
    }

    token.Signature = make([]byte, sizes.SecurityTrailer)

    secBuffers := []SecBuffer{
        NewSecBuffer(SECBUFFER_TOKEN, token.Signature),
    }

    for _, payload := range token.Payloads {
        bufferType := SECBUFFER_DATA

        if !payload.Capabilities.IsSet(gssapi.Confidentiality) {
            bufferType |= SECBUFFER_READONLY_WITH_CHECKSUM
        }

        secBuffers = append(secBuffers, NewSecBuffer(bufferType, payload.Payload))
    }

    r, _, _ := encryptMessage.Call(uintptr(unsafe.Pointer(&auth.ctxt)), 0, uintptr(unsafe.Pointer(NewSecBufferDesc(secBuffers))), uintptr(auth.seqOut))
    if r != SEC_E_OK {
        return nil, fmt.Errorf("EncryptMessage: %w", syscall.Errno(r))
    }

    auth.seqOut++

    return token, nil
}

func (auth *sspiAPI) UnwrapEx(ctx context.Context, token *gssapi.MessageTokenEx) (*gssapi.MessageTokenEx, error) {
    sizes, err := auth.Sizes()
    if err != nil {
        return nil, fmt.Errorf("obtain maximum signature size: %w", err)
    }

    var secBuffers []SecBuffer

    for _, payload := range token.Payloads {
        bufferType := SECBUFFER_DATA

        if !payload.Capabilities.IsSet(gssapi.Confidentiality) {
            bufferType |= SECBUFFER_READONLY_WITH_CHECKSUM
        }

        secBuffers = append(secBuffers, NewSecBuffer(bufferType, payload.Payload))
    }

    secBuffers = append(secBuffers, SecBuffer{
        Type:   SECBUFFER_TOKEN,
        Size:   sizes.SecurityTrailer,
        Buffer: &token.Signature[0],
    })

    var qop uint32

    r, _, _ := decryptMessage.Call(uintptr(unsafe.Pointer(&auth.ctxt)), uintptr(unsafe.Pointer(NewSecBufferDesc(secBuffers))), uintptr(auth.seqIn), uintptr(unsafe.Pointer(&qop)))
    if r != SEC_E_OK {
        return nil, fmt.Errorf("DecryptMessage: %w", syscall.Errno(r))
    }

    auth.seqIn++

    return token, nil
}

However, I discovered some other minor issues that are relevant when implementing a security provider:

goroutine 1 [semacquire]: sync.runtime_Semacquire(0x17547a0?) /usr/lib/go/src/runtime/sema.go:71 +0x25 sync.(WaitGroup).Wait(0xc000074320?) /usr/lib/go/src/sync/waitgroup.go:118 +0x48 github.com/oiweiwei/go-msrpc/dcerpc.(transport).shutdown(0xc000074280, {0xc00002b140?, 0x124cc00?}) github.com/oiweiwei/go-msrpc/dcerpc/transport.go:575 +0x85 github.com/oiweiwei/go-msrpc/dcerpc.(transport).Close(0xc000074280, {0x13c0fd0, 0xc00002a690}) github.com/oiweiwei/go-msrpc/dcerpc/transport.go:612 +0x168 github.com/oiweiwei/go-msrpc/dcerpc.(clientConn).WritePacket(0xc0001385b0, {0x13c0fd0, 0xc00002a690}, {0x13c11c8?, 0xc000101ab0?}, 0xc0000b8201?) github.com/oiweiwei/go-msrpc/dcerpc/client_conn.go:261 +0x53 github.com/oiweiwei/go-msrpc/dcerpc.(clientConn).invoke(0xc0001385b0, {0x13c0fd0, 0xc00002a690}, {0x13c2d88, 0xc000094500}, {0x0?, 0x1896c3e0598?, 0x50?})
github.com/oiweiwei/go-msrpc/dcerpc/client_conn.go:180 +0x525 github.com/oiweiwei/go-msrpc/dcerpc.(
clientConn).Invoke(0x20?, {0x13c0fd0?, 0xc00002a690?}, {0x13c2d88, 0xc000094500}, {0x0?, 0x1789e40?, 0xc000154640?})
github.com/oiweiwei/go-msrpc/dcerpc/client_conn.go:103 +0x116 github.com/oiweiwei/go-msrpc/msrpc/icpr/icertpassage/v0.(*xxx_DefaultCertPassageClient).CertServerRequest(0xc0000875f0, {0x13c0fd0, 0xc00002a690}, 0xc000148d20? , {0x0, 0x0, 0x0}) github.com/oiweiwei/go-msrpc/msrpc/icpr/icertpassage/v0/v0.go:74 +0x12a main.main() main.go:218 +0x13

goroutine 33 [chan receive]: github.com/oiweiwei/go-msrpc/dcerpc.(transport).send(0xc000074280, {0x13c1008, 0xc0000944b0}, 0xc000101ab0) github.com/oiweiwei/go-msrpc/dcerpc/transport_conn.go:272 +0xea github.com/oiweiwei/go-msrpc/dcerpc.(transport).sendLoop(0xc000074280, {0x13c1008, 0xc0000944b0}) github.com/oiweiwei/go-msrpc/dcerpc/transport_conn.go:251 +0x15d github.com/oiweiwei/go-msrpc/dcerpc.(transport).Bind.func2() github.com/oiweiwei/go-msrpc/dcerpc/transport.go:477 +0x5b created by github.com/oiweiwei/go-msrpc/dcerpc.(transport).Bind in goroutine 1 github.com/oiweiwei/go-msrpc/dcerpc/transport.go:475 +0x1897



I managed to work around it by removing this line: https://github.com/oiweiwei/go-msrpc/blob/eb1b248662a7666b0fc40d4c3ec3b64b07dd5a8c/dcerpc/transport.go#L575
oiweiwei commented 2 months ago

@rtpt-erikgeiser glad to hear you've managed to make it work.

Currently there is a deadlock in WrapEx returns an error. Here is the trace

fixed.

The named pipe transport does actually use my mechanism, it only passes credentials to go-smb2. Since go-smb2 also optionally takes an security provider interface, go-msrpc could check if the mechanism happens to implement the go-smb2 auth provider interface and pass it through.

here I'm little bit not following, go-smb2 package doesn't use any providers and the only thing I do is try to extract the credentials passed in order to get NT hash or clear-text password which are only supported options for NTLM Initiator in go-smb2 https://pkg.go.dev/github.com/hirochachacha/go-smb2#NTLMInitiator

could you please clarify what is the issue with the approach above one more time?

rtpt-erikgeiser commented 1 month ago

I just noticed that it is possible to configure the SMB dialer myself via a custom transport, so I don't have to rely on credentials of the correct type since these credentials don't apply to my mechanism. Thank you so much for your help and for your work on this great library.