Closed rtpt-erikgeiser closed 1 month 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}
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 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)
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:
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.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 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?
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.
I'm trying to implement my own authentication mechanism, and I can't figure out whether to use the methods in
gssapi.Mechanism
orgssapi.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 agssapi.Mechanism
.