go-msrpc icon indicating copy to clipboard operation
go-msrpc copied to clipboard

Documentation for gssapi.Mechanism

Open rtpt-erikgeiser opened this issue 1 year ago • 1 comments
trafficstars

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.

rtpt-erikgeiser avatar Aug 16 '24 14:08 rtpt-erikgeiser

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}

oiweiwei avatar Aug 17 '24 14:08 oiweiwei

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?

rtpt-erikgeiser avatar Sep 05 '24 07:09 rtpt-erikgeiser

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

  • for simplicity
  • for sequence numbers of SSP for DCE/RPC are irrelevant
  • and also the change of sequence numbers can be different for different mechanisms - for NTLM it can be +1 that is two sequence counters, for Netlogon it should be +2 that is one sequence counter is maintained instead of two)

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)

oiweiwei avatar Sep 05 '24 13:09 oiweiwei

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:

  • 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.
  • Currently there is a deadlock in WrapEx returns an error. Here is the trace:
fatal error: all goroutines are asleep - deadlock!

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

rtpt-erikgeiser avatar Sep 13 '24 12:09 rtpt-erikgeiser

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

oiweiwei avatar Sep 13 '24 16:09 oiweiwei

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.

rtpt-erikgeiser avatar Sep 19 '24 08:09 rtpt-erikgeiser