Skip to content

Adding NTLM Type 2 Message Decoding#545

Open
lobsterjerusalem wants to merge 6 commits intomainfrom
ntlm-type2-decode
Open

Adding NTLM Type 2 Message Decoding#545
lobsterjerusalem wants to merge 6 commits intomainfrom
ntlm-type2-decode

Conversation

@lobsterjerusalem
Copy link
Copy Markdown
Collaborator

@lobsterjerusalem lobsterjerusalem commented Feb 25, 2026

Ready for review.
Adds NTLM Type 2 Message Decoding.

@lobsterjerusalem lobsterjerusalem self-assigned this Feb 25, 2026
@lobsterjerusalem lobsterjerusalem changed the title needs tests, but drafting this in case any interim feedback is necessary Adding NTLM Type 2 Message Decoding Feb 25, 2026
@lobsterjerusalem lobsterjerusalem marked this pull request as ready for review February 26, 2026 18:34
Copy link
Copy Markdown
Collaborator

@terrorbyte terrorbyte left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NTLM type 2 component seems good, I think the UTF16-LE decoding is too fragile.

Comment thread transform/encode.go
}

// Converts a provided byte buffer to a UTF-16LE decoded string.
func DecodeUTF16LE(data []byte) (string, bool) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this handles BOM and other UTF-16 structures. Any reason not to use the unicode packages directly?:

decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()
utf8bytes, err := decoder.Bytes(data)

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds NTLM Type 2 message decoding utilities to the transform package, including parsing of TargetInfo AV pairs and UTF-16LE decoding, with accompanying unit tests.

Changes:

  • Added NTLM Type 2 decoding (DecodeNTLMType2) and supporting data structures (NTLMType2Message, TargetInfo).
  • Added TargetInfo AV-pair parsing (decodeTargetInfo) and UTF-16LE decoding helper (DecodeUTF16LE).
  • Added unit tests covering UTF-16LE, TargetInfo parsing, and Type 2 message decoding.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 11 comments.

File Description
transform/encode.go Implements NTLM Type 2 message parsing, TargetInfo parsing, and UTF-16LE decoding.
transform/encode_test.go Adds tests validating UTF-16LE decoding, TargetInfo parsing, and NTLM Type 2 decoding.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment thread transform/encode.go
Comment on lines +33 to +43
if totalLen <= 4 {
output.PrintFrameworkError("Failed to decode target info: TargetInfo bytes buffer is too short")
return []TargetInfo{}, false
}

if string(targetInfoBytes[totalLen-4:]) != "\x00\x00\x00\x00" { // Making sure this ends with a terminator block
output.PrintFrameworkError("Decoding NTLM Type 2 Target Info failed, invalid targetinfo buffer provided, the provided buffer does not end with a terminator block")

return []TargetInfo{}, false
}

Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeTargetInfo rejects a TargetInfo buffer of exactly 4 bytes (terminator-only). In NTLM, an empty AV-pair list terminated by 0x00000000 is valid; consider allowing len(targetInfoBytes) == 4 and returning an empty slice successfully.

Suggested change
if totalLen <= 4 {
output.PrintFrameworkError("Failed to decode target info: TargetInfo bytes buffer is too short")
return []TargetInfo{}, false
}
if string(targetInfoBytes[totalLen-4:]) != "\x00\x00\x00\x00" { // Making sure this ends with a terminator block
output.PrintFrameworkError("Decoding NTLM Type 2 Target Info failed, invalid targetinfo buffer provided, the provided buffer does not end with a terminator block")
return []TargetInfo{}, false
}
if totalLen < 4 {
output.PrintFrameworkError("Failed to decode target info: TargetInfo bytes buffer is too short")
return []TargetInfo{}, false
}
// Making sure this ends with a terminator block (0x00000000)
if !bytes.Equal(targetInfoBytes[totalLen-4:], []byte{0x00, 0x00, 0x00, 0x00}) {
output.PrintFrameworkError("Decoding NTLM Type 2 Target Info failed, invalid targetinfo buffer provided, the provided buffer does not end with a terminator block")
return []TargetInfo{}, false
}
// A terminator-only TargetInfo (4 bytes) is a valid empty AV-pair list in NTLM.
if totalLen == 4 {
return retArr, true
}

Copilot uses AI. Check for mistakes.
Comment thread transform/encode.go
Comment on lines +44 to +60
for cursor < totalLen-4 {
targetInfoType := int(binary.LittleEndian.Uint16(targetInfoBytes[cursor : cursor+2]))
if targetInfoType == 0 { // Should not really hit this but who knows what we might get
output.PrintFrameworkWarn("Hit terminator block prematurely during TargetInfo decoding of NTLM Type 2 Message")

return retArr, true
}

targetInfoLen := binary.LittleEndian.Uint16(targetInfoBytes[cursor+2 : cursor+4])
value, ok := DecodeUTF16LE(targetInfoBytes[cursor+4 : cursor+4+int(targetInfoLen)])
if !ok {
return []TargetInfo{}, false
}

retArr = append(retArr, TargetInfo{Type: targetInfoType, Value: value})
cursor = cursor + 4 + int(targetInfoLen)
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decodeTargetInfo slices targetInfoBytes[cursor:cursor+2], cursor+2:cursor+4, and cursor+4:cursor+4+targetInfoLen without validating bounds. A truncated/malformed buffer will panic. Add explicit length checks each iteration (including ensuring the value bytes fit) and fail gracefully instead of slicing past the end.

Copilot uses AI. Check for mistakes.
Comment thread transform/encode.go
Comment on lines +21 to +28
/* Known Types (Microsoft uses other types apparently but this function keeps types generic so it should work fine regardless)
Type 0: Terminator subblock (0x00000000)
Type 1: Server name (server)
Type 2: Domain name (domain.com)
Type 3: DNS Server Name Subblock (server.domain.com)
Type 4: DNS Domain Name (domain.com)
All messages should end with a terminator block
*/
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The block comment says the TargetInfo parsing is kept "generic" and should work regardless of type, but the implementation always decodes the value as UTF-16LE. Several AV_PAIR types are not UTF-16 strings (e.g., timestamps/flags), so this will mis-decode or fail. Either constrain supported types in the comment/API or store raw bytes and decode based on Type.

Suggested change
/* Known Types (Microsoft uses other types apparently but this function keeps types generic so it should work fine regardless)
Type 0: Terminator subblock (0x00000000)
Type 1: Server name (server)
Type 2: Domain name (domain.com)
Type 3: DNS Server Name Subblock (server.domain.com)
Type 4: DNS Domain Name (domain.com)
All messages should end with a terminator block
*/
/* This helper only supports the standard string-based AV_PAIR types defined for NTLM TargetInfo.
Known and expected types:
Type 0: Terminator subblock (0x0000)
Type 1: Server name (UTF-16LE string, e.g., "server")
Type 2: Domain name (UTF-16LE string, e.g., "domain.com")
Type 3: DNS Server Name subblock (UTF-16LE string, e.g., "server.domain.com")
Type 4: DNS Domain Name (UTF-16LE string, e.g., "domain.com")
All messages processed by this function are expected to end with a type 0 terminator block. Other AV_PAIR
types (e.g., timestamps, flags) are not handled here and may require different parsing logic. */

Copilot uses AI. Check for mistakes.
Comment thread transform/encode.go
}

output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetName: %s", targetName)
output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetInfo Data: %q", targetInfoArr)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

output.PrintfFrameworkDebug("... %q", targetInfoArr) uses %q with a []TargetInfo, which will log as a formatting error (e.g., %!q(...)). Use %v or %#v for structs/slices.

Suggested change
output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetInfo Data: %q", targetInfoArr)
output.PrintfFrameworkDebug("NTLM Type 2: Parsed TargetInfo Data: %v", targetInfoArr)

Copilot uses AI. Check for mistakes.
Comment thread transform/encode_test.go
Comment on lines +14 to +16
inputHex := "44004f004d00410049004e00"
inputBytes, _ := hex.DecodeString(inputHex)
decodedString, ok := DecodeUTF16LE(inputBytes)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from hex.DecodeString is ignored here; if the test vector is malformed, the test will proceed with nil/partial input and can fail misleadingly. Handle the returned error and t.Fatal on failure.

Copilot uses AI. Check for mistakes.
Comment thread transform/encode_test.go
Comment on lines +42 to +46
t.Fatalf("Unexpected decoded value: %q", targetInfoArr)
}

t.Logf("Decoded Type 2 TargetInfo: %q", targetInfoArr)
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%q is used with []TargetInfo in Fatalf/Logf, which will produce a formatting error in output. Use %v or %#v for slices/structs so failures are readable.

Copilot uses AI. Check for mistakes.
Comment thread transform/encode_test.go
Comment on lines +68 to +71
t.Fatalf("Unexpected decoded value: %q", message)
}

t.Logf("Decoded Type 2 Message: %q", message)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

%q is used with an NTLMType2Message struct in Fatalf/Logf, which will produce a formatting error in output. Use %v or %#v so test failures/logs show the actual struct contents.

Suggested change
t.Fatalf("Unexpected decoded value: %q", message)
}
t.Logf("Decoded Type 2 Message: %q", message)
t.Fatalf("Unexpected decoded value: %#v", message)
}
t.Logf("Decoded Type 2 Message: %#v", message)

Copilot uses AI. Check for mistakes.
Comment thread transform/encode.go
Comment on lines +82 to +116
if len(message) < 8 {
output.PrintFrameworkError("NTLMType2Decode failed: Message too short")

return NTLMType2Message{}, false
}

signature := string(message[:8])
if signature != "NTLMSSP\x00" {
output.PrintFrameworkError("NTLMType2Decode failed: Not a Type 2 message")

return NTLMType2Message{}, false
}

if string(message[8:12]) != "\x02\x00\x00\x00" {
output.PrintFrameworkError("NTLMType2Decode failed: Message type is not 2")

return NTLMType2Message{}, false
}

targetNameLen := uint32(binary.LittleEndian.Uint16(message[12:14]))
targetNameOffset := binary.LittleEndian.Uint32(message[16:20])

flagBuf := message[20:24]
challengeBuf := message[24:32]

targetInfoLen := uint32(binary.LittleEndian.Uint16(message[40:42]))
targetInfoOffset := binary.LittleEndian.Uint32(message[44:48])

targetName, ok := DecodeUTF16LE(message[targetNameOffset : targetNameOffset+targetNameLen])
if !ok {
return NTLMType2Message{}, false
}

targetInfoBuf := message[targetInfoOffset : targetInfoOffset+targetInfoLen]
targetInfoArr, ok := decodeTargetInfo(targetInfoBuf)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecodeNTLMType2 only checks len(message) < 8 but then slices fixed header fields (e.g., message[8:12], [40:42], [44:48]) and later slices using offsets/lengths. A short or malformed message can panic. Add minimum-length checks for the header (at least through the TargetInfo security buffer) and validate that offset+len are within len(message) before slicing.

Copilot uses AI. Check for mistakes.
Comment thread transform/encode.go

signature := string(message[:8])
if signature != "NTLMSSP\x00" {
output.PrintFrameworkError("NTLMType2Decode failed: Not a Type 2 message")
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is emitted when the NTLMSSP signature check fails, but it says "Not a Type 2 message". Consider changing it to something like "Invalid NTLMSSP signature" to reflect the actual failure condition.

Suggested change
output.PrintFrameworkError("NTLMType2Decode failed: Not a Type 2 message")
output.PrintFrameworkError("NTLMType2Decode failed: Invalid NTLMSSP signature")

Copilot uses AI. Check for mistakes.
Comment thread transform/encode_test.go
Comment on lines +29 to +31
inputHex := "02000c0044004f004d00410049004e0001000c005300450052005600450052000400140064006f006d00610069006e002e0063006f006d00030022007300650072007600650072002e0064006f006d00610069006e002e0063006f006d0000000000"
inputBytes, _ := hex.DecodeString(inputHex)
targetInfoArr, ok := decodeTargetInfo(inputBytes)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error from hex.DecodeString is ignored here; handle it and fail the test immediately to avoid masking bad test vectors.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants