# SPDX-License-Identifier: GPL-2.0-or-later # This file is part of Scapy # See https://scapy.net/ for more information # Copyright (C) Gabriel Potter # scapy.contrib.description = DCE/RPC # scapy.contrib.status = loads """ DCE/RPC Distributed Computing Environment / Remote Procedure Calls Based on [C706] - aka DCE/RPC 1.1 https://pubs.opengroup.org/onlinepubs/9629399/toc.pdf And on [MS-RPCE] https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-rpce/290c38b1-92fe-4229-91e6-4fc376610c15 .. note:: Please read the documentation over `DCE/RPC `_ """ import collections import importlib import inspect import struct from enum import IntEnum from functools import partial from uuid import UUID from scapy.base_classes import Packet_metaclass from scapy.config import conf from scapy.compat import bytes_encode, plain_str from scapy.error import log_runtime from scapy.layers.dns import DNSStrField from scapy.layers.ntlm import ( NTLM_Header, NTLMSSP_MESSAGE_SIGNATURE, ) from scapy.packet import ( Packet, Raw, bind_bottom_up, bind_layers, bind_top_down, NoPayload, ) from scapy.fields import ( _FieldContainer, BitEnumField, BitField, ByteEnumField, ByteField, ConditionalField, EnumField, Field, FieldLenField, FieldListField, FlagsField, IntField, LEIntEnumField, LEIntField, LELongField, LEShortEnumField, LEShortField, LenField, MultipleTypeField, PacketField, PacketLenField, PacketListField, PadField, ReversePadField, ShortEnumField, ShortField, SignedByteField, StrField, StrFixedLenField, StrLenField, StrLenFieldUtf16, StrNullField, StrNullFieldUtf16, TrailerField, UUIDEnumField, UUIDField, XByteField, XLEIntField, XLELongField, XLEShortField, XShortField, XStrFixedLenField, ) from scapy.sessions import DefaultSession from scapy.supersocket import StreamSocket from scapy.layers.kerberos import ( KRB_InnerToken, Kerberos, ) from scapy.layers.gssapi import ( GSSAPI_BLOB, GSSAPI_BLOB_SIGNATURE, GSS_S_COMPLETE, GSS_S_FLAGS, GSS_C_FLAGS, SSP, ) from scapy.layers.inet import TCP from scapy.contrib.rtps.common_types import ( EField, EPacket, EPacketField, EPacketListField, ) # Typing imports from typing import ( Optional, Union, ) # the alignment of auth_pad # This is 4 in [C706] 13.2.6.1 but was updated to 16 in [MS-RPCE] 2.2.2.11 _COMMON_AUTH_PAD = 16 # the alignment of the NDR Type 1 serialization private header # ([MS-RPCE] sect 2.2.6.2) _TYPE1_S_PAD = 8 # DCE/RPC Packet DCE_RPC_TYPE = { 0: "request", 1: "ping", 2: "response", 3: "fault", 4: "working", 5: "no_call", 6: "reject", 7: "acknowledge", 8: "connectionless_cancel", 9: "frag_ack", 10: "cancel_ack", 11: "bind", 12: "bind_ack", 13: "bind_nak", 14: "alter_context", 15: "alter_context_resp", 16: "auth3", 17: "shutdown", 18: "co_cancel", 19: "orphaned", } _DCE_RPC_4_FLAGS1 = [ "reserved_01", "last_frag", "frag", "no_frag_ack", "maybe", "idempotent", "broadcast", "reserved_7", ] _DCE_RPC_4_FLAGS2 = [ "reserved_0", "cancel_pending", "reserved_2", "reserved_3", "reserved_4", "reserved_5", "reserved_6", "reserved_7", ] DCE_RPC_TRANSFER_SYNTAXES = { UUID("00000000-0000-0000-0000-000000000000"): "NULL", UUID("6cb71c2c-9812-4540-0300-000000000000"): "Bind Time Feature Negotiation", UUID("8a885d04-1ceb-11c9-9fe8-08002b104860"): "NDR 2.0", UUID("71710533-beba-4937-8319-b5dbef9ccc36"): "NDR64", } DCE_RPC_INTERFACES_NAMES = {} DCE_RPC_INTERFACES_NAMES_rev = {} class DCERPC_Transport(IntEnum): """ Protocols identifiers currently supported by Scapy """ NCACN_IP_TCP = 0x07 NCACN_NP = 0x0F # TODO: add more.. if people use them? # [C706] Appendix I with names from Appendix B DCE_RPC_PROTOCOL_IDENTIFIERS = { 0x0: "OSI OID", # Special 0x0D: "UUID", # Special # Transports # 0x2: "DNA Session Control", # 0x3: "DNA Session Control V3", # 0x4: "DNA NSP Transport", # 0x5: "OSI TP4", 0x06: "NCADG_OSI_CLSN", # [C706] 0x07: "NCACN_IP_TCP", # [C706] 0x08: "NCADG_IP_UDP", # [C706] 0x09: "IP", # [C706] 0x0A: "RPC connectionless protocol", # [C706] 0x0B: "RPC connection-oriented protocol", # [C706] 0x0C: "NCALRPC", 0x0F: "NCACN_NP", # [MS-RPCE] 0x11: "NCACN_NB", # [C706] 0x12: "NCACN_NB_NB", # [MS-RPCE] 0x13: "NCACN_SPX", # [C706] 0x14: "NCADG_IPX", # [C706] 0x16: "NCACN_AT_DSP", # [C706] 0x17: "NCADG_AT_DSP", # [C706] 0x19: "NCADG_NB", # [C706] 0x1A: "NCACN_VNS_SPP", # [C706] 0x1B: "NCADG_VNS_IPC", # [C706] 0x1F: "NCACN_HTTP", # [MS-RPCE] } def _dce_rpc_endianness(pkt): """ Determine the right endianness sign for a given DCE/RPC packet """ if pkt.endian == 0: # big endian return ">" elif pkt.endian == 1: # little endian return "<" else: return "!" class _EField(EField): def __init__(self, fld): super(_EField, self).__init__(fld, endianness_from=_dce_rpc_endianness) class DceRpc(Packet): """DCE/RPC packet""" @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): if _pkt and len(_pkt) >= 1: ver = ord(_pkt[0:1]) if ver == 4: return DceRpc4 elif ver == 5: return DceRpc5 return DceRpc5 @classmethod def tcp_reassemble(cls, data, metadata, session): if data[0:1] == b"\x05": return DceRpc5.tcp_reassemble(data, metadata, session) return DceRpc(data) bind_bottom_up(TCP, DceRpc, sport=135) bind_layers(TCP, DceRpc, dport=135) class _DceRpcPayload(Packet): @property def endianness(self): if not self.underlayer: return "!" return _dce_rpc_endianness(self.underlayer) # sect 12.5 _drep = [ BitEnumField("endian", 1, 4, ["big", "little"]), BitEnumField("encoding", 0, 4, ["ASCII", "EBCDIC"]), ByteEnumField("float", 0, ["IEEE", "VAX", "CRAY", "IBM"]), ByteField("reserved1", 0), ] class DceRpc4(DceRpc): """ DCE/RPC v4 'connection-less' packet """ name = "DCE/RPC v4" fields_desc = ( [ ByteEnumField( "rpc_vers", 4, {4: "4 (connection-less)", 5: "5 (connection-oriented)"} ), ByteEnumField("ptype", 0, DCE_RPC_TYPE), FlagsField("flags1", 0, 8, _DCE_RPC_4_FLAGS1), FlagsField("flags2", 0, 8, _DCE_RPC_4_FLAGS2), ] + _drep + [ XByteField("serial_hi", 0), _EField(UUIDField("object", None)), _EField(UUIDField("if_id", None)), _EField(UUIDField("act_id", None)), _EField(IntField("server_boot", 0)), _EField(IntField("if_vers", 1)), _EField(IntField("seqnum", 0)), _EField(ShortField("opnum", 0)), _EField(XShortField("ihint", 0xFFFF)), _EField(XShortField("ahint", 0xFFFF)), _EField(LenField("len", None, fmt="H")), _EField(ShortField("fragnum", 0)), ByteEnumField("auth_proto", 0, ["none", "OSF DCE Private Key"]), XByteField("serial_lo", 0), ] ) # Exceptionally, we define those 3 here. class NL_AUTH_MESSAGE(Packet): # [MS-NRPC] sect 2.2.1.3.1 name = "NL_AUTH_MESSAGE" fields_desc = [ LEIntEnumField( "MessageType", 0x00000000, { 0x00000000: "Request", 0x00000001: "Response", }, ), FlagsField( "Flags", 0, -32, [ "NETBIOS_DOMAIN_NAME", "NETBIOS_COMPUTER_NAME", "DNS_DOMAIN_NAME", "DNS_HOST_NAME", "NETBIOS_COMPUTER_NAME_UTF8", ], ), ConditionalField( StrNullField("NetbiosDomainName", ""), lambda pkt: pkt.Flags.NETBIOS_DOMAIN_NAME, ), ConditionalField( StrNullField("NetbiosComputerName", ""), lambda pkt: pkt.Flags.NETBIOS_COMPUTER_NAME, ), ConditionalField( DNSStrField("DnsDomainName", ""), lambda pkt: pkt.Flags.DNS_DOMAIN_NAME, ), ConditionalField( DNSStrField("DnsHostName", ""), lambda pkt: pkt.Flags.DNS_HOST_NAME, ), ConditionalField( # What the fuck? Why are they doing this # The spec is just wrong DNSStrField("NetbiosComputerNameUtf8", ""), lambda pkt: pkt.Flags.NETBIOS_COMPUTER_NAME_UTF8, ), ] class NL_AUTH_SIGNATURE(Packet): # [MS-NRPC] sect 2.2.1.3.2/2.2.1.3.3 name = "NL_AUTH_(SHA2_)SIGNATURE" fields_desc = [ LEShortEnumField( "SignatureAlgorithm", 0x0077, { 0x0077: "HMAC-MD5", 0x0013: "HMAC-SHA256", }, ), LEShortEnumField( "SealAlgorithm", 0xFFFF, { 0xFFFF: "Unencrypted", 0x007A: "RC4", 0x001A: "AES-128", }, ), XLEShortField("Pad", 0xFFFF), ShortField("Flags", 0), XStrFixedLenField("SequenceNumber", b"", length=8), XStrFixedLenField("Checksum", b"", length=8), ConditionalField( XStrFixedLenField("Confounder", b"", length=8), lambda pkt: pkt.SealAlgorithm != 0xFFFF, ), MultipleTypeField( [ ( StrFixedLenField("Reserved2", b"", length=24), lambda pkt: pkt.SignatureAlgorithm == 0x0013, ), ], StrField("Reserved2", b""), ), ] # [MS-RPCE] sect 2.2.1.1.7 # https://learn.microsoft.com/en-us/windows/win32/rpc/authentication-service-constants # rpcdce.h class RPC_C_AUTHN(IntEnum): NONE = 0x00 DCE_PRIVATE = 0x01 DCE_PUBLIC = 0x02 DEC_PUBLIC = 0x04 GSS_NEGOTIATE = 0x09 WINNT = 0x0A GSS_SCHANNEL = 0x0E GSS_KERBEROS = 0x10 DPA = 0x11 MSN = 0x12 KERNEL = 0x14 DIGEST = 0x15 NEGO_EXTENDED = 0x1E PKU2U = 0x1F LIVE_SSP = 0x20 LIVEXP_SSP = 0x23 CLOUD_AP = 0x24 NETLOGON = 0x44 MSONLINE = 0x52 MQ = 0x64 DEFAULT = 0xFFFFFFFF class RPC_C_AUTHN_LEVEL(IntEnum): DEFAULT = 0x0 NONE = 0x1 CONNECT = 0x2 CALL = 0x3 PKT = 0x4 PKT_INTEGRITY = 0x5 PKT_PRIVACY = 0x6 DCE_C_AUTHN_LEVEL = RPC_C_AUTHN_LEVEL # C706 name class RPC_C_IMP_LEVEL(IntEnum): DEFAULT = 0x0 ANONYMOUS = 0x1 IDENTIFY = 0x2 IMPERSONATE = 0x3 DELEGATE = 0x4 # C706 sect 13.2.6.1 class CommonAuthVerifier(Packet): name = "Common Authentication Verifier" fields_desc = [ ByteEnumField( "auth_type", 0, RPC_C_AUTHN, ), ByteEnumField("auth_level", 0, RPC_C_AUTHN_LEVEL), ByteField("auth_pad_length", None), ByteField("auth_reserved", 0), XLEIntField("auth_context_id", 0), MultipleTypeField( [ # SPNEGO ( PacketLenField( "auth_value", GSSAPI_BLOB(), GSSAPI_BLOB, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x09 and pkt.parent and # Bind/Alter pkt.parent.ptype in [11, 12, 13, 14, 15, 16], ), ( PacketLenField( "auth_value", GSSAPI_BLOB_SIGNATURE(), GSSAPI_BLOB_SIGNATURE, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x09 and pkt.parent and ( # Other not pkt.parent or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] ), ), # Kerberos ( PacketLenField( "auth_value", Kerberos(), Kerberos, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x10 and pkt.parent and # Bind/Alter pkt.parent.ptype in [11, 12, 13, 14, 15, 16], ), ( PacketLenField( "auth_value", KRB_InnerToken(), KRB_InnerToken, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x10 and pkt.parent and ( # Other not pkt.parent or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] ), ), # NTLM ( PacketLenField( "auth_value", NTLM_Header(), NTLM_Header, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type in [0x0A, 0xFF] and pkt.parent and # Bind/Alter pkt.parent.ptype in [11, 12, 13, 14, 15, 16], ), ( PacketLenField( "auth_value", NTLMSSP_MESSAGE_SIGNATURE(), NTLMSSP_MESSAGE_SIGNATURE, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type in [0x0A, 0xFF] and pkt.parent and ( # Other not pkt.parent or pkt.parent.ptype not in [11, 12, 13, 14, 15, 16] ), ), # NetLogon ( PacketLenField( "auth_value", NL_AUTH_MESSAGE(), NL_AUTH_MESSAGE, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x44 and pkt.parent and # Bind/Alter pkt.parent.ptype in [11, 12, 13, 14, 15], ), ( PacketLenField( "auth_value", NL_AUTH_SIGNATURE(), NL_AUTH_SIGNATURE, length_from=lambda pkt: pkt.parent.auth_len, ), lambda pkt: pkt.auth_type == 0x44 and ( # Other not pkt.parent or pkt.parent.ptype not in [11, 12, 13, 14, 15] ), ), ], PacketLenField( "auth_value", None, conf.raw_layer, length_from=lambda pkt: pkt.parent and pkt.parent.auth_len or 0, ), ), ] def is_protected(self): if not self.auth_value: return False if self.parent and self.parent.ptype in [11, 12, 13, 14, 15, 16]: return False return True def is_ssp(self): if not self.auth_value: return False if self.parent and self.parent.ptype not in [11, 12, 13, 14, 15, 16]: return False return True def default_payload_class(self, pkt): return conf.padding_layer # [MS-RPCE] sect 2.2.2.13 - Verification Trailer _SECTRAILER_MAGIC = b"\x8a\xe3\x13\x71\x02\xf4\x36\x71" class DceRpcSecVTCommand(Packet): name = "Verification trailer command" fields_desc = [ BitField("SEC_VT_MUST_PROCESS_COMMAND", 0, 1, tot_size=-2), BitField("SEC_VT_COMMAND_END", 0, 1), BitEnumField( "Command", 0, -14, { 0x0001: "SEC_VT_COMMAND_BITMASK_1", 0x0002: "SEC_VT_COMMAND_PCONTEXT", 0x0003: "SEC_VT_COMMAND_HEADER2", }, end_tot_size=-2, ), LenField("Length", None, fmt="> 4 if endian not in [0, 1]: return length = struct.unpack(("<" if endian else ">") + "H", data[8:10])[0] if len(data) >= length: if conf.dcerpc_session_enable: # If DCE/RPC sessions are enabled, use them ! if "dcerpcsess" not in session: session["dcerpcsess"] = dcerpcsess = DceRpcSession() else: dcerpcsess = session["dcerpcsess"] return dcerpcsess.process(DceRpc5(data)) return DceRpc5(data) # sec 12.6.3.1 class DceRpc5AbstractSyntax(EPacket): name = "Presentation Syntax (p_syntax_id_t)" fields_desc = [ _EField( UUIDEnumField( "if_uuid", None, ( # Those are dynamic DCE_RPC_INTERFACES_NAMES.get, lambda x: DCE_RPC_INTERFACES_NAMES_rev.get(x.lower()), ), ) ), _EField(IntField("if_version", 3)), ] class DceRpc5TransferSyntax(EPacket): name = "Presentation Transfer Syntax (p_syntax_id_t)" fields_desc = [ _EField( UUIDEnumField( "if_uuid", None, DCE_RPC_TRANSFER_SYNTAXES, ) ), _EField(IntField("if_version", 3)), ] class DceRpc5Context(EPacket): name = "Presentation Context (p_cont_elem_t)" fields_desc = [ _EField(ShortField("cont_id", 0)), FieldLenField("n_transfer_syn", None, count_of="transfer_syntaxes", fmt="B"), ByteField("reserved", 0), EPacketField("abstract_syntax", None, DceRpc5AbstractSyntax), EPacketListField( "transfer_syntaxes", None, DceRpc5TransferSyntax, count_from=lambda pkt: pkt.n_transfer_syn, endianness_from=_dce_rpc_endianness, ), ] class DceRpc5Result(EPacket): name = "Context negotiation Result" fields_desc = [ _EField( ShortEnumField( "result", 0, ["acceptance", "user_rejection", "provider_rejection"] ) ), _EField( ShortEnumField( "reason", 0, _DCE_RPC_REJECTION_REASONS, ) ), EPacketField("transfer_syntax", None, DceRpc5TransferSyntax), ] class DceRpc5PortAny(EPacket): name = "Port Any (port_any_t)" fields_desc = [ _EField(FieldLenField("length", None, length_of="port_spec", fmt="H")), _EField(StrLenField("port_spec", b"", length_from=lambda pkt: pkt.length)), ] # sec 12.6.4.3 class DceRpc5Bind(_DceRpcPayload): name = "DCE/RPC v5 - Bind" fields_desc = [ _EField(ShortField("max_xmit_frag", 5840)), _EField(ShortField("max_recv_frag", 8192)), _EField(IntField("assoc_group_id", 0)), # p_cont_list_t _EField( FieldLenField("n_context_elem", None, count_of="context_elem", fmt="B") ), StrFixedLenField("reserved", 0, length=3), EPacketListField( "context_elem", [], DceRpc5Context, endianness_from=_dce_rpc_endianness, count_from=lambda pkt: pkt.n_context_elem, ), ] bind_layers(DceRpc5, DceRpc5Bind, ptype=11) # sec 12.6.4.4 class DceRpc5BindAck(_DceRpcPayload): name = "DCE/RPC v5 - Bind Ack" fields_desc = [ _EField(ShortField("max_xmit_frag", 5840)), _EField(ShortField("max_recv_frag", 8192)), _EField(IntField("assoc_group_id", 0)), PadField( EPacketField("sec_addr", None, DceRpc5PortAny), align=4, ), # p_result_list_t _EField(FieldLenField("n_results", None, count_of="results", fmt="B")), StrFixedLenField("reserved", 0, length=3), EPacketListField( "results", [], DceRpc5Result, endianness_from=_dce_rpc_endianness, count_from=lambda pkt: pkt.n_results, ), ] bind_layers(DceRpc5, DceRpc5BindAck, ptype=12) # sec 12.6.4.5 class DceRpc5Version(EPacket): name = "version_t" fields_desc = [ ByteField("major", 0), ByteField("minor", 0), ] class DceRpc5BindNak(_DceRpcPayload): name = "DCE/RPC v5 - Bind Nak" fields_desc = [ _EField( ShortEnumField("provider_reject_reason", 0, _DCE_RPC_REJECTION_REASONS) ), # p_rt_versions_supported_t _EField(FieldLenField("n_protocols", None, count_of="protocols", fmt="B")), EPacketListField( "protocols", [], DceRpc5Version, count_from=lambda pkt: pkt.n_protocols, endianness_from=_dce_rpc_endianness, ), # [MS-RPCE] sect 2.2.2.9 ConditionalField( ReversePadField( _EField( UUIDEnumField( "signature", None, { UUID( "90740320-fad0-11d3-82d7-009027b130ab" ): "Extended Error", }, ) ), align=8, ), lambda pkt: pkt.fields.get("signature", None) or ( pkt.underlayer and pkt.underlayer.frag_len >= 24 + pkt.n_protocols * 2 + 16 ), ), ] bind_layers(DceRpc5, DceRpc5BindNak, ptype=13) # sec 12.6.4.1 class DceRpc5AlterContext(_DceRpcPayload): name = "DCE/RPC v5 - AlterContext" fields_desc = DceRpc5Bind.fields_desc bind_layers(DceRpc5, DceRpc5AlterContext, ptype=14) # sec 12.6.4.2 class DceRpc5AlterContextResp(_DceRpcPayload): name = "DCE/RPC v5 - AlterContextResp" fields_desc = DceRpc5BindAck.fields_desc bind_layers(DceRpc5, DceRpc5AlterContextResp, ptype=15) # [MS-RPCE] sect 2.2.2.10 - rpc_auth_3 class DceRpc5Auth3(Packet): name = "DCE/RPC v5 - Auth3" fields_desc = [StrFixedLenField("pad", b"", length=4)] bind_layers(DceRpc5, DceRpc5Auth3, ptype=16) # sec 12.6.4.7 class DceRpc5Fault(_DceRpcPayload): name = "DCE/RPC v5 - Fault" fields_desc = [ _EField(IntField("alloc_hint", 0)), _EField(ShortField("cont_id", 0)), ByteField("cancel_count", 0), FlagsField("reserved", 0, -8, {0x1: "RPC extended error"}), _EField(LEIntEnumField("status", 0, _DCE_RPC_ERROR_CODES)), IntField("reserved2", 0), ] bind_layers(DceRpc5, DceRpc5Fault, ptype=3) # sec 12.6.4.9 class DceRpc5Request(_DceRpcPayload): name = "DCE/RPC v5 - Request" fields_desc = [ _EField(IntField("alloc_hint", 0)), _EField(ShortField("cont_id", 0)), _EField(ShortField("opnum", 0)), ConditionalField( PadField( _EField(UUIDField("object", None)), align=8, ), lambda pkt: pkt.underlayer and pkt.underlayer.pfc_flags.PFC_OBJECT_UUID, ), ] bind_layers(DceRpc5, DceRpc5Request, ptype=0) # sec 12.6.4.10 class DceRpc5Response(_DceRpcPayload): name = "DCE/RPC v5 - Response" fields_desc = [ _EField(IntField("alloc_hint", 0)), _EField(ShortField("cont_id", 0)), ByteField("cancel_count", 0), ByteField("reserved", 0), ] bind_layers(DceRpc5, DceRpc5Response, ptype=2) # --- API DceRpcOp = collections.namedtuple("DceRpcOp", ["request", "response"]) DCE_RPC_INTERFACES = {} class DceRpcInterface: def __init__(self, name, uuid, version_tuple, if_version, opnums): self.name = name self.uuid = uuid self.major_version, self.minor_version = version_tuple self.if_version = if_version self.opnums = opnums def __repr__(self): return "" % ( self.name, self.major_version, self.minor_version, ) def register_dcerpc_interface(name, uuid, version, opnums): """ Register a DCE/RPC interface """ version_tuple = tuple(map(int, version.split("."))) assert len(version_tuple) == 2, "Version should be in format 'X.X' !" if_version = (version_tuple[1] << 16) + version_tuple[0] if (uuid, if_version) in DCE_RPC_INTERFACES: # Interface is already registered. interface = DCE_RPC_INTERFACES[(uuid, if_version)] if interface.name == name: if set(opnums) - set(interface.opnums): # Interface is an extension of a previous interface interface.opnums.update(opnums) else: log_runtime.warning( "This interface is already registered: %s. Skip" % interface ) return else: raise ValueError( "An interface with the same UUID is already registered: %s" % interface ) else: # New interface DCE_RPC_INTERFACES_NAMES[uuid] = name DCE_RPC_INTERFACES_NAMES_rev[name.lower()] = uuid DCE_RPC_INTERFACES[(uuid, if_version)] = DceRpcInterface( name, uuid, version_tuple, if_version, opnums, ) # bind for build for opnum, operations in opnums.items(): bind_top_down(DceRpc5Request, operations.request, opnum=opnum) def find_dcerpc_interface(name) -> DceRpcInterface: """ Find an interface object through the name in the IDL """ try: return next(x for x in DCE_RPC_INTERFACES.values() if x.name == name) except StopIteration: raise AttributeError("Unknown interface !") COM_INTERFACES = {} class ComInterface: if_version = 0 def __init__(self, name, uuid, opnums): self.name = name self.uuid = uuid self.opnums = opnums def __repr__(self): return "" % (self.name,) def register_com_interface(name, uuid, opnums): """ Register a COM interface """ COM_INTERFACES[uuid] = ComInterface( name, uuid, opnums, ) # bind for build for opnum, operations in opnums.items(): bind_top_down(DceRpc5Request, operations.request, opnum=opnum) def find_com_interface(name) -> ComInterface: """ Find an interface object through the name in the IDL """ try: return next(x for x in COM_INTERFACES.values() if x.name == name) except StopIteration: raise AttributeError("Unknown interface !") # --- NDR fields - [C706] chap 14 def _set_ctx_on(f, obj): if isinstance(f, _NDRPacket): f.ndr64 = obj.ndr64 f.ndrendian = obj.ndrendian if isinstance(f, list): for x in f: if isinstance(x, _NDRPacket): x.ndr64 = obj.ndr64 x.ndrendian = obj.ndrendian def _e(ndrendian): return {"big": ">", "little": "<"}[ndrendian] class _NDRPacket(Packet): __slots__ = ["ndr64", "ndrendian", "deferred_pointers", "request_packet"] def __init__(self, *args, **kwargs): self.ndr64 = kwargs.pop("ndr64", conf.ndr64) self.ndrendian = kwargs.pop("ndrendian", "little") # request_packet is used in the session, so that a response packet # can resolve union arms if the case parameter is in the request. self.request_packet = kwargs.pop("request_packet", None) self.deferred_pointers = [] super(_NDRPacket, self).__init__(*args, **kwargs) def do_dissect(self, s): _up = self.parent or self.underlayer if _up and isinstance(_up, _NDRPacket): self.ndr64 = _up.ndr64 self.ndrendian = _up.ndrendian else: # See comment above NDRConstructedType return NDRConstructedType([]).read_deferred_pointers( self, super(_NDRPacket, self).do_dissect(s) ) return super(_NDRPacket, self).do_dissect(s) def post_dissect(self, s): if self.deferred_pointers: # Can't trust the cache if there were deferred pointers self.raw_packet_cache = None return s def do_build(self): _up = self.parent or self.underlayer for f in self.fields.values(): _set_ctx_on(f, self) if not _up or not isinstance(_up, _NDRPacket): # See comment above NDRConstructedType return NDRConstructedType([]).add_deferred_pointers( self, super(_NDRPacket, self).do_build() ) return super(_NDRPacket, self).do_build() def default_payload_class(self, pkt): return conf.padding_layer def clone_with(self, *args, **kwargs): pkt = super(_NDRPacket, self).clone_with(*args, **kwargs) # We need to copy deferred_pointers to not break pointer deferral # on build. pkt.deferred_pointers = self.deferred_pointers pkt.ndr64 = self.ndr64 pkt.ndrendian = self.ndrendian return pkt def copy(self): pkt = super(_NDRPacket, self).copy() pkt.deferred_pointers = self.deferred_pointers pkt.ndr64 = self.ndr64 pkt.ndrendian = self.ndrendian return pkt def show2(self, dump=False, indent=3, lvl="", label_lvl=""): return self.__class__( bytes(self), ndr64=self.ndr64, ndrendian=self.ndrendian ).show(dump, indent, lvl, label_lvl) def getfield_and_val(self, attr): try: return Packet.getfield_and_val(self, attr) except ValueError: if self.request_packet: # Try to resolve the field from the request on failure try: return self.request_packet.getfield_and_val(attr) except AttributeError: pass raise def valueof(self, request): """ Util to get the value of a NDRField, ignoring arrays, pointers, etc. """ val = self for ndr_field in request.split("."): fld, fval = val.getfield_and_val(ndr_field) val = fld.valueof(val, fval) return val class _NDRAlign: def padlen(self, flen, pkt): return -flen % self._align[pkt.ndr64] def original_length(self, pkt): # Find the length of the NDR frag to be able to pad properly while pkt: par = pkt.parent or pkt.underlayer if par and isinstance(par, _NDRPacket): pkt = par else: break return len(pkt.original) class NDRAlign(_NDRAlign, ReversePadField): """ ReversePadField modified to fit NDR. - If no align size is specified, use the one from the inner field - Size is calculated from the beginning of the NDR stream """ def __init__(self, fld, align, padwith=None): super(NDRAlign, self).__init__(fld, align=align, padwith=padwith) class _VirtualField(Field): # Hold a value but doesn't show up when building/dissecting def addfield(self, pkt, s, x): return s def getfield(self, pkt, s): return s, None class _NDRPacketMetaclass(Packet_metaclass): def __new__(cls, name, bases, dct): newcls = super(_NDRPacketMetaclass, cls).__new__(cls, name, bases, dct) conformants = dct.get("DEPORTED_CONFORMANTS", []) if conformants: amount = len(conformants) if amount == 1: newcls.fields_desc.insert( 0, _VirtualField("max_count", None), ) else: newcls.fields_desc.insert( 0, FieldListField( "max_counts", [], _VirtualField("", 0), count_from=lambda _: amount, ), ) return newcls # type: ignore class NDRPacket(_NDRPacket, metaclass=_NDRPacketMetaclass): """ A NDR Packet. Handles pointer size & endianness """ __slots__ = ["_align"] # NDR64 pad structures # [MS-RPCE] 2.2.5.3.4.1 ALIGNMENT = (1, 1) # [C706] sect 14.3.7 - Conformants max_count can be added to the beginning DEPORTED_CONFORMANTS = [] # Primitive types class _NDRValueOf: def valueof(self, pkt, x): return x class _NDRLenField(_NDRValueOf, Field): """ Field similar to FieldLenField that takes size_of and adjust as arguments, and take the value of a size on build. """ __slots__ = ["size_of", "adjust"] def __init__(self, *args, **kwargs): self.size_of = kwargs.pop("size_of", None) self.adjust = kwargs.pop("adjust", lambda _, x: x) super(_NDRLenField, self).__init__(*args, **kwargs) def i2m(self, pkt, x): if x is None and pkt is not None and self.size_of is not None: fld, fval = pkt.getfield_and_val(self.size_of) f = fld.i2len(pkt, fval) x = self.adjust(pkt, f) elif x is None: x = 0 return x class NDRByteField(_NDRLenField, ByteField): pass class NDRSignedByteField(_NDRLenField, SignedByteField): pass class _NDRField(_NDRLenField): FMT = "" ALIGN = (0, 0) def getfield(self, pkt, s): return NDRAlign( Field("", 0, fmt=_e(pkt.ndrendian) + self.FMT), align=self.ALIGN ).getfield(pkt, s) def addfield(self, pkt, s, val): return NDRAlign( Field("", 0, fmt=_e(pkt.ndrendian) + self.FMT), align=self.ALIGN ).addfield(pkt, s, self.i2m(pkt, val)) class NDRShortField(_NDRField): FMT = "H" ALIGN = (2, 2) class NDRSignedShortField(_NDRField): FMT = "h" ALIGN = (2, 2) class NDRIntField(_NDRField): FMT = "I" ALIGN = (4, 4) class NDRSignedIntField(_NDRField): FMT = "i" ALIGN = (4, 4) class NDRLongField(_NDRField): FMT = "Q" ALIGN = (8, 8) class NDRSignedLongField(_NDRField): FMT = "q" ALIGN = (8, 8) class NDRIEEEFloatField(_NDRField): FMT = "f" ALIGN = (4, 4) class NDRIEEEDoubleField(_NDRField): FMT = "d" ALIGN = (8, 8) # Enum types class _NDREnumField(_NDRValueOf, EnumField): # [MS-RPCE] sect 2.2.5.2 - Enums are 4 octets in NDR64 FMTS = ["H", "I"] def getfield(self, pkt, s): fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] return NDRAlign(Field("", 0, fmt=fmt), align=(2, 4)).getfield(pkt, s) def addfield(self, pkt, s, val): fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] return NDRAlign(Field("", 0, fmt=fmt), align=(2, 4)).addfield( pkt, s, self.i2m(pkt, val) ) class NDRInt3264EnumField(NDRAlign): def __init__(self, *args, **kwargs): super(NDRInt3264EnumField, self).__init__( _NDREnumField(*args, **kwargs), align=(2, 4) ) class NDRIntEnumField(_NDRValueOf, NDRAlign): # v1_enum are always 4-octets, even in NDR32 def __init__(self, *args, **kwargs): super(NDRIntEnumField, self).__init__( LEIntEnumField(*args, **kwargs), align=(4, 4) ) # Special types class NDRInt3264Field(_NDRLenField): FMTS = ["I", "Q"] def getfield(self, pkt, s): fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] return NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield(pkt, s) def addfield(self, pkt, s, val): fmt = _e(pkt.ndrendian) + self.FMTS[pkt.ndr64] return NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( pkt, s, self.i2m(pkt, val) ) class NDRSignedInt3264Field(NDRInt3264Field): FMTS = ["i", "q"] # Pointer types class NDRPointer(_NDRPacket): fields_desc = [ MultipleTypeField( [(XLELongField("referent_id", 1), lambda pkt: pkt and pkt.ndr64)], XLEIntField("referent_id", 1), ), PacketField("value", None, conf.raw_layer), ] class NDRFullPointerField(_FieldContainer): """ A NDR Full/Unique pointer field encapsulation. :param EMBEDDED: This pointer is embedded. This means that it's representation will not appear after the pointer (pointer deferral applies). See [C706] 14.3.12.3 - Algorithm for Deferral of Referents """ EMBEDDED = False EMBEDDED_REF = False def __init__(self, fld, ref=False, fmt="I"): self.fld = fld self.ref = ref self.default = None def getfield(self, pkt, s): fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] remain, referent_id = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( pkt, s ) # No value if referent_id == 0 and not self.EMBEDDED_REF: return remain, None # With value if self.EMBEDDED: # deferred ptr = NDRPointer( ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, referent_id=referent_id ) pkt.deferred_pointers.append((ptr, partial(self.fld.getfield, pkt))) return remain, ptr remain, val = self.fld.getfield(pkt, remain) return remain, NDRPointer( ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, referent_id=referent_id, value=val ) def addfield(self, pkt, s, val): if val is not None and not isinstance(val, NDRPointer): raise ValueError( "Expected NDRPointer in %s. You are using it wrong!" % self.name ) fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] fld = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)) # No value if val is None and not self.EMBEDDED_REF: return fld.addfield(pkt, s, 0) # With value _set_ctx_on(val.value, pkt) s = fld.addfield(pkt, s, val.referent_id) if self.EMBEDDED: # deferred pkt.deferred_pointers.append( ((lambda s: self.fld.addfield(pkt, s, val.value)), val) ) return s return self.fld.addfield(pkt, s, val.value) def any2i(self, pkt, x): # User-friendly helper if x is not None and not isinstance(x, NDRPointer): return NDRPointer( referent_id=0x20000, value=self.fld.any2i(pkt, x), ) return x # Can't use i2repr = Field.i2repr and so on on PY2 :/ def i2repr(self, pkt, val): return repr(val) def i2h(self, pkt, x): return x def h2i(self, pkt, x): return x def i2len(self, pkt, x): if x is None: return 0 return self.fld.i2len(pkt, x.value) def valueof(self, pkt, x): if x is None: return x return self.fld.valueof(pkt, x.value) class NDRFullEmbPointerField(NDRFullPointerField): """ A NDR Embedded Full pointer. Same as NDRFullPointerField with EMBEDDED = True. """ EMBEDDED = True class NDRRefEmbPointerField(NDRFullPointerField): """ A NDR Embedded Reference pointer. Same as NDRFullPointerField with EMBEDDED = True and EMBEDDED_REF = True. """ EMBEDDED = True EMBEDDED_REF = True # Constructed types # Note: this is utterly complex and will drive you crazy # If you have a NDRPacket that contains a deferred pointer on the top level # (only happens in non DCE/RPC structures, such as in MS-PAC, where you have an NDR # structure encapsulated in a non-NDR structure), there will be left-over deferred # pointers when exiting dissection/build (deferred pointers are only computed when # reaching a field that extends NDRConstructedType, which is normal: if you follow # the DCE/RPC spec, pointers are never deferred in root structures) # Therefore there is a special case forcing the build/dissection of any leftover # pointers in NDRPacket, if Scapy detects that they won't be handled by any parent. # Implementation notes: I chose to set 'handles_deferred' inside the FIELD, rather # than inside the PACKET. This is faster to compute because whether a constructed type # should handle deferral or not is computed only once when loading, therefore Scapy # knows in advance whether to handle deferred pointers or not. But it is technically # incorrect: with this approach, a structure (packet) cannot be used in 2 code paths # that have different pointer managements. I mean by that that if there was a # structure that was directly embedded in a RPC request without a pointer but also # embedded with a pointer in another RPC request, it would break. # Fortunately this isn't the case: structures are never reused for 2 purposes. # (or at least I never seen that... ) class NDRConstructedType(object): def __init__(self, fields): self.handles_deferred = False self.ndr_fields = fields self.rec_check_deferral() def rec_check_deferral(self): # We iterate through the fields within this constructed type. # If we have a pointer, mark this field as handling deferrance # and make all sub-constructed types not. for f in self.ndr_fields: if isinstance(f, NDRFullPointerField) and f.EMBEDDED: self.handles_deferred = True if isinstance(f, NDRConstructedType): f.rec_check_deferral() if f.handles_deferred: self.handles_deferred = True f.handles_deferred = False def getfield(self, pkt, s): s, fval = super(NDRConstructedType, self).getfield(pkt, s) if isinstance(fval, _NDRPacket): # If a sub-packet we just dissected has deferred pointers, # pass it to parent packet to propagate. pkt.deferred_pointers.extend(fval.deferred_pointers) del fval.deferred_pointers[:] if self.handles_deferred: # This field handles deferral ! s = self.read_deferred_pointers(pkt, s) return s, fval def read_deferred_pointers(self, pkt, s): # Now read content of the pointers that were deferred q = collections.deque() q.extend(pkt.deferred_pointers) del pkt.deferred_pointers[:] while q: # Recursively resolve pointers that were deferred ptr, getfld = q.popleft() s, val = getfld(s) ptr.value = val if isinstance(val, _NDRPacket): # Pointer resolves to a packet.. that may have deferred pointers? q.extend(val.deferred_pointers) del val.deferred_pointers[:] return s def addfield(self, pkt, s, val): try: s = super(NDRConstructedType, self).addfield(pkt, s, val) except Exception as ex: try: ex.args = ( "While building field '%s': " % self.name + ex.args[0], ) + ex.args[1:] except (AttributeError, IndexError): pass raise ex if isinstance(val, _NDRPacket): # If a sub-packet we just dissected has deferred pointers, # pass it to parent packet to propagate. pkt.deferred_pointers.extend(val.deferred_pointers) del val.deferred_pointers[:] if self.handles_deferred: # This field handles deferral ! s = self.add_deferred_pointers(pkt, s) return s def add_deferred_pointers(self, pkt, s): # Now add content of pointers that were deferred q = collections.deque() q.extend(pkt.deferred_pointers) del pkt.deferred_pointers[:] while q: addfld, fval = q.popleft() s = addfld(s) if isinstance(fval, NDRPointer) and isinstance(fval.value, _NDRPacket): q.extend(fval.value.deferred_pointers) del fval.value.deferred_pointers[:] return s class _NDRPacketField(_NDRValueOf, PacketField): def m2i(self, pkt, m): return self.cls(m, ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, _parent=pkt) class _NDRPacketPadField(PadField): # [MS-RPCE] 2.2.5.3.4.1 Structure with Trailing Gap # Structures have extra alignment/padding in NDR64. def padlen(self, flen, pkt): if pkt.ndr64: return -flen % self._align[1] else: return 0 class NDRPacketField(NDRConstructedType, NDRAlign): def __init__(self, name, default, pkt_cls, **kwargs): self.DEPORTED_CONFORMANTS = pkt_cls.DEPORTED_CONFORMANTS self.fld = _NDRPacketField(name, default, pkt_cls=pkt_cls, **kwargs) NDRAlign.__init__( self, _NDRPacketPadField(self.fld, align=pkt_cls.ALIGNMENT), align=pkt_cls.ALIGNMENT, ) NDRConstructedType.__init__(self, pkt_cls.fields_desc) def getfield(self, pkt, x): # Handle deformed conformants max_count here if self.DEPORTED_CONFORMANTS: # C706 14.3.2: "In other words, the size information precedes the # structure and is aligned independently of the structure alignment." fld = NDRInt3264Field("", 0) max_counts = [] for _ in self.DEPORTED_CONFORMANTS: x, max_count = fld.getfield(pkt, x) max_counts.append(max_count) res, val = super(NDRPacketField, self).getfield(pkt, x) if len(max_counts) == 1: val.max_count = max_counts[0] else: val.max_counts = max_counts return res, val return super(NDRPacketField, self).getfield(pkt, x) def addfield(self, pkt, s, x): # Handle deformed conformants max_count here if self.DEPORTED_CONFORMANTS: mcfld = NDRInt3264Field("", 0) if len(self.DEPORTED_CONFORMANTS) == 1: max_counts = [x.max_count] else: max_counts = x.max_counts for fldname, max_count in zip(self.DEPORTED_CONFORMANTS, max_counts): if max_count is None: fld, val = x.getfield_and_val(fldname) max_count = fld.i2len(x, val) s = mcfld.addfield(pkt, s, max_count) return super(NDRPacketField, self).addfield(pkt, s, x) return super(NDRPacketField, self).addfield(pkt, s, x) # Array types class _NDRPacketListField(NDRConstructedType, PacketListField): """ A PacketListField for NDR that can optionally pack the packets into NDRPointers """ islist = 1 holds_packets = 1 __slots__ = ["ptr_pack", "fld"] def __init__(self, name, default, pkt_cls, **kwargs): self.ptr_pack = kwargs.pop("ptr_pack", False) if self.ptr_pack: self.fld = NDRFullEmbPointerField(NDRPacketField("", None, pkt_cls)) else: self.fld = NDRPacketField("", None, pkt_cls) PacketListField.__init__(self, name, default, pkt_cls=pkt_cls, **kwargs) NDRConstructedType.__init__(self, [self.fld]) def m2i(self, pkt, s): remain, val = self.fld.getfield(pkt, s) if val is None: val = NDRNone() # A mistake here would be to use / instead of add_payload. It adds a copy # which breaks pointer defferal. Same applies elsewhere val.add_payload(conf.padding_layer(remain)) return val def any2i(self, pkt, x): # User-friendly helper if isinstance(x, list): x = [self.fld.any2i(pkt, y) for y in x] return super(_NDRPacketListField, self).any2i(pkt, x) def i2m(self, pkt, val): return self.fld.addfield(pkt, b"", val) def i2len(self, pkt, x): return len(x) def valueof(self, pkt, x): return [ self.fld.valueof(pkt, y if not isinstance(y, NDRNone) else None) for y in x ] class NDRFieldListField(NDRConstructedType, FieldListField): """ A FieldListField for NDR """ islist = 1 def __init__(self, *args, **kwargs): kwargs.pop("ptr_pack", None) # TODO: unimplemented if "length_is" in kwargs: kwargs["count_from"] = kwargs.pop("length_is") elif "size_is" in kwargs: kwargs["count_from"] = kwargs.pop("size_is") FieldListField.__init__(self, *args, **kwargs) NDRConstructedType.__init__(self, [self.field]) def i2len(self, pkt, x): return len(x) def valueof(self, pkt, x): return [self.field.valueof(pkt, y) for y in x] class NDRVaryingArray(_NDRPacket): fields_desc = [ MultipleTypeField( [(LELongField("offset", 0), lambda pkt: pkt and pkt.ndr64)], LEIntField("offset", 0), ), MultipleTypeField( [ ( LELongField("actual_count", None), lambda pkt: pkt and pkt.ndr64, ) ], LEIntField("actual_count", None), ), PacketField("value", None, conf.raw_layer), ] class _NDRVarField: """ NDR Varying Array / String field """ LENGTH_FROM = False COUNT_FROM = False def __init__(self, *args, **kwargs): # We build the length_is function by taking into account both the # actual_count (from the varying field) and a potentially provided # length_is field. if "length_is" in kwargs: _length_is = kwargs.pop("length_is") length_is = lambda pkt: (_length_is(pkt.underlayer) or pkt.actual_count) else: length_is = lambda pkt: pkt.actual_count # Pass it to the sub-field (actually subclass) if self.LENGTH_FROM: kwargs["length_from"] = length_is elif self.COUNT_FROM: kwargs["count_from"] = length_is super(_NDRVarField, self).__init__(*args, **kwargs) def getfield(self, pkt, s): fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] remain, offset = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield(pkt, s) remain, actual_count = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( pkt, remain ) final = NDRVaryingArray( ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, offset=offset, actual_count=actual_count, _underlayer=pkt, ) remain, val = super(_NDRVarField, self).getfield(final, remain) final.value = super(_NDRVarField, self).i2h(pkt, val) return remain, final def addfield(self, pkt, s, val): if not isinstance(val, NDRVaryingArray): raise ValueError( "Expected NDRVaryingArray in %s. You are using it wrong!" % self.name ) fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] _set_ctx_on(val.value, pkt) s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield(pkt, s, val.offset) s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( pkt, s, val.actual_count is None and super(_NDRVarField, self).i2len(pkt, val.value) or val.actual_count, ) return super(_NDRVarField, self).addfield( pkt, s, super(_NDRVarField, self).h2i(pkt, val.value) ) def i2len(self, pkt, x): return super(_NDRVarField, self).i2len(pkt, x.value) def any2i(self, pkt, x): # User-friendly helper if not isinstance(x, NDRVaryingArray): return NDRVaryingArray( value=super(_NDRVarField, self).any2i(pkt, x), ) return x # Can't use i2repr = Field.i2repr and so on on PY2 :/ def i2repr(self, pkt, val): return repr(val) def i2h(self, pkt, x): return x def h2i(self, pkt, x): return x def valueof(self, pkt, x): return super(_NDRVarField, self).valueof(pkt, x.value) class NDRConformantArray(_NDRPacket): fields_desc = [ MultipleTypeField( [(LELongField("max_count", None), lambda pkt: pkt and pkt.ndr64)], LEIntField("max_count", None), ), MultipleTypeField( [ ( PacketListField( "value", [], conf.raw_layer, count_from=lambda pkt: pkt.max_count, ), ( lambda pkt: pkt.fields.get("value", None) and isinstance(pkt.fields["value"][0], Packet), lambda _, val: val and isinstance(val[0], Packet), ), ) ], FieldListField( "value", [], LEIntField("", 0), count_from=lambda pkt: pkt.max_count ), ), ] class NDRConformantString(_NDRPacket): fields_desc = [ MultipleTypeField( [(LELongField("max_count", None), lambda pkt: pkt and pkt.ndr64)], LEIntField("max_count", None), ), StrField("value", ""), ] class _NDRConfField: """ NDR Conformant Array / String field """ CONFORMANT_STRING = False LENGTH_FROM = False COUNT_FROM = False def __init__(self, *args, **kwargs): # when conformant_in_struct is True, we remove the level of abstraction # provided by NDRConformantString / NDRConformantArray because max_count # is a proper field in the parent packet. self.conformant_in_struct = kwargs.pop("conformant_in_struct", False) # size_is/max_is end up here, and is what defines a conformant field. if "size_is" in kwargs: size_is = kwargs.pop("size_is") if self.LENGTH_FROM: kwargs["length_from"] = size_is elif self.COUNT_FROM: kwargs["count_from"] = size_is super(_NDRConfField, self).__init__(*args, **kwargs) def getfield(self, pkt, s): # [C706] - 14.3.7 Structures Containing Arrays fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] if self.conformant_in_struct: return super(_NDRConfField, self).getfield(pkt, s) remain, max_count = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).getfield( pkt, s ) remain, val = super(_NDRConfField, self).getfield(pkt, remain) return remain, ( NDRConformantString if self.CONFORMANT_STRING else NDRConformantArray )(ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, max_count=max_count, value=val) def addfield(self, pkt, s, val): if self.conformant_in_struct: return super(_NDRConfField, self).addfield(pkt, s, val) if self.CONFORMANT_STRING and not isinstance(val, NDRConformantString): raise ValueError( "Expected NDRConformantString in %s. You are using it wrong!" % self.name ) elif not self.CONFORMANT_STRING and not isinstance(val, NDRConformantArray): raise ValueError( "Expected NDRConformantArray in %s. You are using it wrong!" % self.name ) fmt = _e(pkt.ndrendian) + ["I", "Q"][pkt.ndr64] _set_ctx_on(val.value, pkt) if val.value and isinstance(val.value[0], NDRVaryingArray): value = val.value[0] else: value = val.value s = NDRAlign(Field("", 0, fmt=fmt), align=(4, 8)).addfield( pkt, s, val.max_count is None and super(_NDRConfField, self).i2len(pkt, value) or val.max_count, ) return super(_NDRConfField, self).addfield(pkt, s, value) def _subval(self, x): if self.conformant_in_struct: value = x elif ( not self.CONFORMANT_STRING and x.value and isinstance(x.value[0], NDRVaryingArray) ): value = x.value[0] else: value = x.value return value def i2len(self, pkt, x): return super(_NDRConfField, self).i2len(pkt, self._subval(x)) def any2i(self, pkt, x): # User-friendly helper if self.conformant_in_struct: return super(_NDRConfField, self).any2i(pkt, x) if self.CONFORMANT_STRING and not isinstance(x, NDRConformantString): return NDRConformantString( value=super(_NDRConfField, self).any2i(pkt, x), ) elif not isinstance(x, NDRConformantArray): return NDRConformantArray( value=super(_NDRConfField, self).any2i(pkt, x), ) return x # Can't use i2repr = Field.i2repr and so on on PY2 :/ def i2repr(self, pkt, val): return repr(val) def i2h(self, pkt, x): return x def h2i(self, pkt, x): return x def valueof(self, pkt, x): return super(_NDRConfField, self).valueof(pkt, self._subval(x)) class NDRVarPacketListField(_NDRVarField, _NDRPacketListField): """ NDR Varying PacketListField. Unused """ COUNT_FROM = True class NDRConfPacketListField(_NDRConfField, _NDRPacketListField): """ NDR Conformant PacketListField """ COUNT_FROM = True class NDRConfVarPacketListField(_NDRConfField, _NDRVarField, _NDRPacketListField): """ NDR Conformant Varying PacketListField """ COUNT_FROM = True class NDRConfFieldListField(_NDRConfField, NDRFieldListField): """ NDR Conformant FieldListField """ COUNT_FROM = True class NDRConfVarFieldListField(_NDRConfField, _NDRVarField, NDRFieldListField): """ NDR Conformant Varying FieldListField """ COUNT_FROM = True # NDR String fields class _NDRUtf16(Field): def h2i(self, pkt, x): encoding = {"big": "utf-16be", "little": "utf-16le"}[pkt.ndrendian] return plain_str(x).encode(encoding) def i2h(self, pkt, x): encoding = {"big": "utf-16be", "little": "utf-16le"}[pkt.ndrendian] return bytes_encode(x).decode(encoding, errors="replace") class NDRConfStrLenField(_NDRConfField, _NDRValueOf, StrLenField): """ NDR Conformant StrLenField. This is not a "string" per NDR, but an a conformant byte array (e.g. tower_octet_string). For ease of use, we implicitly convert it in specific cases. """ CONFORMANT_STRING = True LENGTH_FROM = True class NDRConfStrLenFieldUtf16(_NDRConfField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16): """ NDR Conformant StrLenFieldUtf16. See NDRConfStrLenField for comment. """ CONFORMANT_STRING = True ON_WIRE_SIZE_UTF16 = False LENGTH_FROM = True class NDRVarStrLenField(_NDRVarField, StrLenField): """ NDR Varying StrLenField """ LENGTH_FROM = True class NDRVarStrLenFieldUtf16(_NDRVarField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16): """ NDR Varying StrLenFieldUtf16 """ ON_WIRE_SIZE_UTF16 = False LENGTH_FROM = True class NDRConfVarStrLenField(_NDRConfField, _NDRVarField, _NDRValueOf, StrLenField): """ NDR Conformant Varying StrLenField """ LENGTH_FROM = True class NDRConfVarStrLenFieldUtf16( _NDRConfField, _NDRVarField, _NDRValueOf, StrLenFieldUtf16, _NDRUtf16 ): """ NDR Conformant Varying StrLenFieldUtf16 """ ON_WIRE_SIZE_UTF16 = False LENGTH_FROM = True class NDRConfVarStrNullField(_NDRConfField, _NDRVarField, _NDRValueOf, StrNullField): """ NDR Conformant Varying StrNullField """ NULLFIELD = True class NDRConfVarStrNullFieldUtf16( _NDRConfField, _NDRVarField, _NDRValueOf, StrNullFieldUtf16, _NDRUtf16 ): """ NDR Conformant Varying StrNullFieldUtf16 """ ON_WIRE_SIZE_UTF16 = False NULLFIELD = True # Union type class NDRUnion(_NDRPacket): fields_desc = [ IntField("tag", 0), PacketField("value", None, conf.raw_layer), ] class _NDRUnionField(MultipleTypeField): __slots__ = ["switch_fmt", "align"] def __init__(self, flds, dflt, align, switch_fmt): self.switch_fmt = switch_fmt self.align = align super(_NDRUnionField, self).__init__(flds, dflt) def getfield(self, pkt, s): fmt = _e(pkt.ndrendian) + self.switch_fmt[pkt.ndr64] remain, tag = NDRAlign(Field("", 0, fmt=fmt), align=self.align).getfield(pkt, s) fld, _ = super(_NDRUnionField, self)._find_fld_pkt_val(pkt, NDRUnion(tag=tag)) remain, val = fld.getfield(pkt, remain) return remain, NDRUnion( tag=tag, value=val, ndr64=pkt.ndr64, ndrendian=pkt.ndrendian, _parent=pkt ) def addfield(self, pkt, s, val): fmt = _e(pkt.ndrendian) + self.switch_fmt[pkt.ndr64] if not isinstance(val, NDRUnion): raise ValueError( "Expected NDRUnion in %s. You are using it wrong!" % self.name ) _set_ctx_on(val.value, pkt) # First, align the whole tag+union against the align param s = NDRAlign(Field("", 0, fmt=fmt), align=self.align).addfield(pkt, s, val.tag) # Then, compute the subfield with its own alignment return super(_NDRUnionField, self).addfield(pkt, s, val) def _find_fld_pkt_val(self, pkt, val): fld, val = super(_NDRUnionField, self)._find_fld_pkt_val(pkt, val) return fld, val.value # Can't use i2repr = Field.i2repr and so on on PY2 :/ def i2repr(self, pkt, val): return repr(val) def i2h(self, pkt, x): return x def h2i(self, pkt, x): return x def valueof(self, pkt, x): fld, val = self._find_fld_pkt_val(pkt, x) return fld.valueof(pkt, x.value) class NDRUnionField(NDRConstructedType, _NDRUnionField): def __init__(self, flds, dflt, align, switch_fmt): _NDRUnionField.__init__(self, flds, dflt, align=align, switch_fmt=switch_fmt) NDRConstructedType.__init__(self, [x[0] for x in flds] + [dflt]) def any2i(self, pkt, x): # User-friendly helper if x: if not isinstance(x, NDRUnion): raise ValueError("Invalid value for %s; should be NDRUnion" % self.name) else: x.value = _NDRUnionField.any2i(self, pkt, x) return x # Misc class _ProxyArray: # Hack for recursive fields DEPORTED_CONFORMANTS field __slots__ = ["getfld"] def __init__(self, getfld): self.getfld = getfld def __len__(self): try: return len(self.getfld()) except AttributeError: return 0 def __iter__(self): try: return iter(self.getfld()) except AttributeError: return iter([]) class _ProxyTuple: # Hack for recursive fields ALIGNMENT field __slots__ = ["getfld"] def __init__(self, getfld): self.getfld = getfld def __getitem__(self, name): try: return self.getfld()[name] except AttributeError: raise KeyError def NDRRecursiveClass(clsname): """ Return a special class that is used for pointer recursion """ # Get module where this is called frame = inspect.currentframe().f_back mod = frame.f_globals["__loader__"].name getcls = lambda: getattr(importlib.import_module(mod), clsname) class _REC(NDRPacket): ALIGNMENT = _ProxyTuple(lambda: getattr(getcls(), "ALIGNMENT")) DEPORTED_CONFORMANTS = _ProxyArray( lambda: getattr(getcls(), "DEPORTED_CONFORMANTS") ) @classmethod def dispatch_hook(cls, _pkt=None, *args, **kargs): return getcls() return _REC # The very few NDR-specific structures class NDRContextHandle(NDRPacket): ALIGNMENT = (4, 4) fields_desc = [ LEIntField("attributes", 0), StrFixedLenField("uuid", b"", length=16), ] def guess_payload_class(self, payload): return conf.padding_layer class NDRNone(NDRPacket): # This is only used in NDRPacketListField to act as a "None" pointer, and is # a workaround because the field doesn't support None as a value in the list. name = "None" ALIGNMENT = (4, 8) fields_desc = [ NDRInt3264Field("ptr", 0), ] # --- Type Serialization Version 1 - [MSRPCE] sect 2.2.6 def _get_ndrtype1_endian(pkt): if pkt.underlayer is None: return "<" return {0x00: ">", 0x10: "<"}.get(pkt.underlayer.Endianness, "<") class NDRSerialization1Header(Packet): fields_desc = [ ByteField("Version", 1), ByteEnumField("Endianness", 0x10, {0x00: "big", 0x10: "little"}), LEShortField("CommonHeaderLength", 8), XLEIntField("Filler", 0xCCCCCCCC), ] # Add a bit of goo so that valueof() goes through the header def _ndrlayer(self): cur = self while cur and not isinstance(cur, _NDRPacket) and cur.payload: cur = cur.payload if isinstance(cur, NDRPointer): cur = cur.value return cur def getfield_and_val(self, attr): try: return Packet.getfield_and_val(self, attr) except ValueError: return self._ndrlayer().getfield_and_val(attr) def valueof(self, name): return self._ndrlayer().valueof(name) class NDRSerialization1PrivateHeader(Packet): fields_desc = [ EField( LEIntField("ObjectBufferLength", 0), endianness_from=_get_ndrtype1_endian ), XLEIntField("Filler", 0), ] def ndr_deserialize1(b, cls, ptr_pack=False): """ Deserialize Type Serialization Version 1 [MS-RPCE] sect 2.2.6 :param ptr_pack: pack in a pointer to the structure. """ if issubclass(cls, NDRPacket): # We use an intermediary class because it uses NDRPacketField which handles # deported conformant fields if ptr_pack: hdrlen = 20 class _cls(NDRPacket): fields_desc = [NDRFullPointerField(NDRPacketField("pkt", None, cls))] else: hdrlen = 16 class _cls(NDRPacket): fields_desc = [NDRPacketField("pkt", None, cls)] hdr = NDRSerialization1Header(b[:8]) / NDRSerialization1PrivateHeader(b[8:16]) endian = {0x00: "big", 0x10: "little"}[hdr.Endianness] padlen = (-hdr.ObjectBufferLength) % _TYPE1_S_PAD # padlen should be 0 (pad included in length), but some implementations # implement apparently misread the spec return ( hdr / _cls( b[16 : hdrlen + hdr.ObjectBufferLength], ndr64=False, # Only NDR32 is supported in Type 1 ndrendian=endian, ).pkt / conf.padding_layer(b[hdrlen + padlen + hdr.ObjectBufferLength :]) ) return NDRSerialization1Header(b[:8]) / cls(b[8:]) def ndr_serialize1(pkt, ptr_pack=False): """ Serialize Type Serialization Version 1 [MS-RPCE] sect 2.2.6 :param ptr_pack: pack in a pointer to the structure. """ pkt = pkt.copy() endian = getattr(pkt, "ndrendian", "little") if not isinstance(pkt, NDRSerialization1Header): if not isinstance(pkt, NDRPacket): return bytes(NDRSerialization1Header(Endianness=endian) / pkt) if isinstance(pkt, NDRPointer): cls = pkt.value.__class__ else: cls = pkt.__class__ val = pkt pkt_len = len(pkt) # ObjectBufferLength: # > It MUST include the padding length and exclude the header itself pkt = NDRSerialization1Header( Endianness=endian ) / NDRSerialization1PrivateHeader( ObjectBufferLength=pkt_len + (-pkt_len) % _TYPE1_S_PAD ) else: cls = pkt.value.__class__ val = pkt.payload.payload pkt.payload.remove_payload() # See above about why we need an intermediary class if ptr_pack: class _cls(NDRPacket): fields_desc = [NDRFullPointerField(NDRPacketField("pkt", None, cls))] else: class _cls(NDRPacket): fields_desc = [NDRPacketField("pkt", None, cls)] ret = bytes(pkt / _cls(pkt=val, ndr64=False, ndrendian=endian)) return ret + (-len(ret) % _TYPE1_S_PAD) * b"\x00" class _NDRSerializeType1: def __init__(self, *args, **kwargs): self.ptr_pack = kwargs.pop("ptr_pack", False) super(_NDRSerializeType1, self).__init__(*args, **kwargs) def i2m(self, pkt, val): return ndr_serialize1(val, ptr_pack=self.ptr_pack) def m2i(self, pkt, s): return ndr_deserialize1(s, self.cls, ptr_pack=self.ptr_pack) def i2len(self, pkt, val): return len(self.i2m(pkt, val)) class NDRSerializeType1PacketField(_NDRSerializeType1, PacketField): __slots__ = ["ptr_pack"] class NDRSerializeType1PacketLenField(_NDRSerializeType1, PacketLenField): __slots__ = ["ptr_pack"] class NDRSerializeType1PacketListField(_NDRSerializeType1, PacketListField): __slots__ = ["ptr_pack"] def i2len(self, pkt, val): return sum(len(self.i2m(pkt, p)) for p in val) # --- DCE/RPC session class DceRpcSession(DefaultSession): """ A DCE/RPC session within a TCP socket. """ def __init__(self, *args, **kwargs): self.rpc_bind_interface: Union[DceRpcInterface, ComInterface] = None self.rpc_bind_is_com: bool = False self.ndr64 = False self.ndrendian = "little" self.support_header_signing = kwargs.pop("support_header_signing", True) self.header_sign = conf.dcerpc_force_header_signing self.ssp = kwargs.pop("ssp", None) self.sspcontext = kwargs.pop("sspcontext", None) self.auth_level = kwargs.pop("auth_level", None) self.sent_cont_ids = [] self.cont_id = 0 # Currently selected context self.auth_context_id = 0 # Currently selected authentication context self.map_callid_opnum = {} self.frags = collections.defaultdict(lambda: b"") self.sniffsspcontexts = {} # Unfinished contexts for passive if conf.dcerpc_session_enable and conf.winssps_passive: for ssp in conf.winssps_passive: self.sniffsspcontexts[ssp] = None super(DceRpcSession, self).__init__(*args, **kwargs) def _up_pkt(self, pkt): """ Common function to handle the DCE/RPC session: what interfaces are bind, opnums, etc. """ opnum = None opts = {} if DceRpc5Bind in pkt or DceRpc5AlterContext in pkt: # bind => get which RPC interface self.sent_cont_ids = [x.cont_id for x in pkt.context_elem] for ctx in pkt.context_elem: if_uuid = ctx.abstract_syntax.if_uuid if_version = ctx.abstract_syntax.if_version try: self.rpc_bind_interface = DCE_RPC_INTERFACES[(if_uuid, if_version)] self.rpc_bind_is_com = False except KeyError: try: self.rpc_bind_interface = COM_INTERFACES[if_uuid] self.rpc_bind_is_com = True except KeyError: self.rpc_bind_interface = None log_runtime.warning( "Unknown RPC interface %s. Try loading the IDL" % if_uuid ) elif DceRpc5BindAck in pkt or DceRpc5AlterContextResp in pkt: # bind ack => is it NDR64 for i, res in enumerate(pkt.results): if res.result == 0: # Accepted # Context try: self.cont_id = self.sent_cont_ids[i] except IndexError: self.cont_id = 0 finally: self.sent_cont_ids = [] # Endianness self.ndrendian = {0: "big", 1: "little"}[pkt[DceRpc5].endian] # Transfer syntax if res.transfer_syntax.sprintf("%if_uuid%") == "NDR64": self.ndr64 = True elif DceRpc5Request in pkt: # request => match opnum with callID opnum = pkt.opnum if self.rpc_bind_is_com: self.map_callid_opnum[pkt.call_id] = ( opnum, pkt[DceRpc5Request].payload.payload, ) else: self.map_callid_opnum[pkt.call_id] = opnum, pkt[DceRpc5Request].payload elif DceRpc5Response in pkt: # response => get opnum from table try: opnum, opts["request_packet"] = self.map_callid_opnum[pkt.call_id] del self.map_callid_opnum[pkt.call_id] except KeyError: log_runtime.info("Unknown call_id %s in DCE/RPC session" % pkt.call_id) # Bind / Alter request/response specific if ( DceRpc5Bind in pkt or DceRpc5AlterContext in pkt or DceRpc5BindAck in pkt or DceRpc5AlterContextResp in pkt ): # Detect if "Header Signing" is in use if pkt.pfc_flags & 0x04: # PFC_SUPPORT_HEADER_SIGN self.header_sign = True return opnum, opts # [C706] sect 12.6.2 - Fragmentation and Reassembly # Since the connection-oriented transport guarantees sequentiality, the receiver # will always receive the fragments in order. def _defragment(self, pkt, body=None): """ Function to defragment DCE/RPC packets. """ uid = pkt.call_id if pkt.pfc_flags.PFC_FIRST_FRAG and pkt.pfc_flags.PFC_LAST_FRAG: # Not fragmented return body if pkt.pfc_flags.PFC_FIRST_FRAG or uid in self.frags: # Packet is fragmented if body is None: body = pkt[DceRpc5].payload.payload.original self.frags[uid] += body if pkt.pfc_flags.PFC_LAST_FRAG: return self.frags[uid] else: # Not fragmented return body # C706 sect 12.5.2.15 - PDU Body Length # "The maximum PDU body size is 65528 bytes." MAX_PDU_BODY_SIZE = 4176 def _fragment(self, pkt, body): """ Function to fragment DCE/RPC packets. """ if len(body) > self.MAX_PDU_BODY_SIZE: # Clear any PFC_*_FRAG flag pkt.pfc_flags &= 0xFC # Iterate through fragments cur = None while body: # Create a fragment pkt_frag = pkt.copy() if cur is None: # It's the first one pkt_frag.pfc_flags += "PFC_FIRST_FRAG" # Split cur, body = ( body[: self.MAX_PDU_BODY_SIZE], body[self.MAX_PDU_BODY_SIZE :], ) if not body: # It's the last one pkt_frag.pfc_flags += "PFC_LAST_FRAG" yield pkt_frag, cur else: yield pkt, body # [MS-RPCE] sect 3.3.1.5.2.2 # The PDU header, PDU body, and sec_trailer MUST be passed in the input message, in # this order, to GSS_WrapEx, GSS_UnwrapEx, GSS_GetMICEx, and GSS_VerifyMICEx. For # integrity protection the sign flag for that PDU segment MUST be set to TRUE, else # it MUST be set to FALSE. For confidentiality protection, the conf_req_flag for # that PDU segment MUST be set to TRUE, else it MUST be set to FALSE. # If the authentication level is RPC_C_AUTHN_LEVEL_PKT_PRIVACY, the PDU body will # be encrypted. # The PDU body from the output message of GSS_UnwrapEx represents the plain text # version of the PDU body. The PDU header and sec_trailer output from the output # message SHOULD be ignored. # Similarly the signature output SHOULD be ignored. def in_pkt(self, pkt): # Check for encrypted payloads body = None if conf.raw_layer in pkt.payload: body = bytes(pkt.payload[conf.raw_layer]) # If we are doing passive sniffing if conf.dcerpc_session_enable and conf.winssps_passive: # We have Windows SSPs, and no current context if pkt.auth_verifier and pkt.auth_verifier.is_ssp(): # This is a bind/alter/auth3 req/resp for ssp in self.sniffsspcontexts: self.sniffsspcontexts[ssp], status = ssp.GSS_Passive( self.sniffsspcontexts[ssp], pkt.auth_verifier.auth_value, req_flags=GSS_S_FLAGS.GSS_S_ALLOW_MISSING_BINDINGS | GSS_C_FLAGS.GSS_C_DCE_STYLE, ) if status == GSS_S_COMPLETE: self.auth_level = DCE_C_AUTHN_LEVEL( int(pkt.auth_verifier.auth_level) ) self.ssp = ssp self.sspcontext = self.sniffsspcontexts[ssp] self.sniffsspcontexts[ssp] = None elif ( self.sspcontext and pkt.auth_verifier and pkt.auth_verifier.is_protected() and body ): # This is a request/response if self.sspcontext.passive: self.ssp.GSS_Passive_set_Direction( self.sspcontext, IsAcceptor=DceRpc5Response in pkt, ) if pkt.auth_verifier and pkt.auth_verifier.is_protected() and body: if self.sspcontext is None: return pkt if self.auth_level in ( RPC_C_AUTHN_LEVEL.PKT_INTEGRITY, RPC_C_AUTHN_LEVEL.PKT_PRIVACY, ): # note: 'vt_trailer' is included in the pdu body # [MS-RPCE] sect 2.2.2.13 # "The data structures MUST only appear in a request PDU, and they # SHOULD be placed in the PDU immediately after the stub data but # before the authentication padding octets. Therefore, for security # purposes, the verification trailer is considered part of the PDU # body." if pkt.vt_trailer: body += bytes(pkt.vt_trailer) # Account for padding when computing checksum/encryption if pkt.auth_padding: body += pkt.auth_padding # Build pdu_header and sec_trailer pdu_header = pkt.copy() sec_trailer = pdu_header.auth_verifier # sec_trailer: include the sec_trailer but not the Authentication token authval_len = len(sec_trailer.auth_value) # Discard everything out of the header pdu_header.auth_padding = None pdu_header.auth_verifier = None pdu_header.payload.payload = NoPayload() pdu_header.vt_trailer = None # [MS-RPCE] sect 2.2.2.12 if self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: _msgs = self.ssp.GSS_UnwrapEx( self.sspcontext, [ # "PDU header" SSP.WRAP_MSG( conf_req_flag=False, sign=self.header_sign, data=bytes(pdu_header), ), # "PDU body" SSP.WRAP_MSG( conf_req_flag=True, sign=True, data=body, ), # "sec_trailer" SSP.WRAP_MSG( conf_req_flag=False, sign=self.header_sign, data=bytes(sec_trailer)[:-authval_len], ), ], pkt.auth_verifier.auth_value, ) body = _msgs[1].data # PDU body elif self.auth_level == RPC_C_AUTHN_LEVEL.PKT_INTEGRITY: self.ssp.GSS_VerifyMICEx( self.sspcontext, [ # "PDU header" SSP.MIC_MSG( sign=self.header_sign, data=bytes(pdu_header), ), # "PDU body" SSP.MIC_MSG( sign=True, data=body, ), # "sec_trailer" SSP.MIC_MSG( sign=self.header_sign, data=bytes(sec_trailer)[:-authval_len], ), ], pkt.auth_verifier.auth_value, ) # Put padding back into the header if pkt.auth_padding: padlen = len(pkt.auth_padding) body, pkt.auth_padding = body[:-padlen], body[-padlen:] # Put back vt_trailer into the header, if present. if _SECTRAILER_MAGIC in body: body, pkt.vt_trailer = pkt.get_field("vt_trailer").getfield( pkt, body ) # If it's a request / response, could be fragmented if isinstance(pkt.payload, (DceRpc5Request, DceRpc5Response)) and body: body = self._defragment(pkt, body) if not body: return # Get opnum and options opnum, opts = self._up_pkt(pkt) # Try to parse the payload if opnum is not None and self.rpc_bind_interface: # use opnum to parse the payload is_response = DceRpc5Response in pkt try: cls = self.rpc_bind_interface.opnums[opnum][is_response] except KeyError: log_runtime.warning( "Unknown opnum %s for interface %s" % (opnum, self.rpc_bind_interface) ) pkt.payload[conf.raw_layer].load = body return pkt if body: orpc = None if self.rpc_bind_is_com: # If interface is a COM interface, start off by dissecting the # ORPCTHIS / ORPCTHAT argument from scapy.layers.msrpce.raw.ms_dcom import ORPCTHAT, ORPCTHIS # [MS-DCOM] sect 2.2.13 # "ORPCTHIS and ORPCTHAT structures MUST be marshaled using # the NDR (32) Transfer Syntax" if is_response: orpc = ORPCTHAT(body, ndr64=False) else: orpc = ORPCTHIS(body, ndr64=False) body = orpc.load orpc.remove_payload() # Dissect payload using class try: payload = cls( body, ndr64=self.ndr64, ndrendian=self.ndrendian, **opts ) except Exception: if conf.debug_dissector: log_runtime.error("%s dissector failed", cls.__name__) if cls is not None: raise payload = conf.raw_layer(body, _internal=1) pkt.payload[conf.raw_layer].underlayer.remove_payload() if conf.padding_layer in payload: # Most likely, dissection failed. log_runtime.warning( "Padding detected when dissecting %s. Looks wrong." % cls ) pad = payload[conf.padding_layer] pad.underlayer.payload = conf.raw_layer(load=pad.load) if orpc is not None: pkt /= orpc pkt /= payload # If a request was encrypted, we need to re-register it once re-parsed. if not is_response and self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: self._up_pkt(pkt) elif not cls.fields_desc: # Request class has no payload pkt /= cls(ndr64=self.ndr64, ndrendian=self.ndrendian, **opts) elif body: pkt.payload[conf.raw_layer].load = body return pkt def out_pkt(self, pkt): assert DceRpc5 in pkt # Register opnum and options self._up_pkt(pkt) # If it's a request / response, we can frag it if isinstance(pkt.payload, (DceRpc5Request, DceRpc5Response)): # The list of packet responses pkts = [] # Take the body payload, and eventually split it body = bytes(pkt.payload.payload) for pkt, body in self._fragment(pkt, body): if pkt.auth_verifier is not None: # Verifier already set pkts.append(pkt) continue # Sign / Encrypt if self.sspcontext: signature = None if self.auth_level in ( RPC_C_AUTHN_LEVEL.PKT_INTEGRITY, RPC_C_AUTHN_LEVEL.PKT_PRIVACY, ): # Remember that vt_trailer is included in the PDU if pkt.vt_trailer: body += bytes(pkt.vt_trailer) # Account for padding when computing checksum/encryption if pkt.auth_padding is None: padlen = (-len(body)) % _COMMON_AUTH_PAD # authdata padding pkt.auth_padding = b"\x00" * padlen else: padlen = len(pkt.auth_padding) # Remember that padding IS SIGNED & ENCRYPTED body += pkt.auth_padding # Add the auth_verifier pkt.auth_verifier = CommonAuthVerifier( auth_type=self.ssp.auth_type, auth_level=self.auth_level, auth_context_id=self.auth_context_id, auth_pad_length=padlen, # Note: auth_value should have the correct length because # when using PFC_SUPPORT_HEADER_SIGN, auth_len # (and frag_len) is included in the token.. but this # creates a dependency loop as you'd need to know the token # length to compute the token. Windows solves this by # setting the 'Maximum Signature Length' (or something # similar) beforehand, instead of the real length. # See `gensec_sig_size` in samba. auth_value=b"\x00" * self.ssp.MaximumSignatureLength(self.sspcontext), ) # Build pdu_header and sec_trailer pdu_header = pkt.copy() pdu_header.auth_len = len(pdu_header.auth_verifier) - 8 pdu_header.frag_len = len(pdu_header) sec_trailer = pdu_header.auth_verifier # sec_trailer: include the sec_trailer but not the # Authentication token authval_len = len(sec_trailer.auth_value) # sec_trailer.auth_value = None # Discard everything out of the header pdu_header.auth_padding = None pdu_header.auth_verifier = None pdu_header.payload.payload = NoPayload() pdu_header.vt_trailer = None signature = None # [MS-RPCE] sect 2.2.2.12 if self.auth_level == RPC_C_AUTHN_LEVEL.PKT_PRIVACY: _msgs, signature = self.ssp.GSS_WrapEx( self.sspcontext, [ # "PDU header" SSP.WRAP_MSG( conf_req_flag=False, sign=self.header_sign, data=bytes(pdu_header), ), # "PDU body" SSP.WRAP_MSG( conf_req_flag=True, sign=True, data=body, ), # "sec_trailer" SSP.WRAP_MSG( conf_req_flag=False, sign=self.header_sign, data=bytes(sec_trailer)[:-authval_len], ), ], ) s = _msgs[1].data # PDU body elif self.auth_level == RPC_C_AUTHN_LEVEL.PKT_INTEGRITY: signature = self.ssp.GSS_GetMICEx( self.sspcontext, [ # "PDU header" SSP.MIC_MSG( sign=self.header_sign, data=bytes(pdu_header), ), # "PDU body" SSP.MIC_MSG( sign=True, data=body, ), # "sec_trailer" SSP.MIC_MSG( sign=self.header_sign, data=bytes(sec_trailer)[:-authval_len], ), ], pkt.auth_verifier.auth_value, ) s = body else: raise ValueError("Impossible") # Put padding back in the header if padlen: s, pkt.auth_padding = s[:-padlen], s[-padlen:] # Put back vt_trailer into the header if pkt.vt_trailer: vtlen = len(pkt.vt_trailer) s, pkt.vt_trailer = s[:-vtlen], s[-vtlen:] else: s = body # now inject the encrypted payload into the packet pkt.payload.payload = conf.raw_layer(load=s) # and the auth_value if signature: pkt.auth_verifier.auth_value = signature else: pkt.auth_verifier = None # Add to the list pkts.append(pkt) return pkts else: return [pkt] def process(self, pkt: Packet) -> Optional[Packet]: """ Used when DceRpcSession is used for passive sniffing. """ pkt = super(DceRpcSession, self).process(pkt) if pkt is not None and DceRpc5 in pkt: rpkt = self.in_pkt(pkt) if rpkt is None: # We are passively dissecting a fragmented packet. Return # just the header showing that it was indeed, fragmented. pkt[DceRpc5].payload.remove_payload() return pkt return rpkt return pkt class DceRpcSocket(StreamSocket): """ A Wrapper around StreamSocket that uses a DceRpcSession """ def __init__(self, *args, **kwargs): self.transport = kwargs.pop("transport", None) self.session = DceRpcSession( ssp=kwargs.pop("ssp", None), auth_level=kwargs.pop("auth_level", None), support_header_signing=kwargs.pop("support_header_signing", True), ) super(DceRpcSocket, self).__init__(*args, **kwargs) def send(self, x, **kwargs): for pkt in self.session.out_pkt(x): if self.transport == DCERPC_Transport.NCACN_NP: # In this case DceRpcSocket wraps a SMB_RPC_SOCKET, call it directly. self.ins.send(pkt, **kwargs) else: super(DceRpcSocket, self).send(pkt, **kwargs) def recv(self, x=None): pkt = super(DceRpcSocket, self).recv(x) if pkt is not None: return self.session.in_pkt(pkt) # --- TODO cleanup below # Heuristically way to find the payload class # # To add a possible payload to a DCE/RPC packet, one must first create the # packet class, then instead of binding layers using bind_layers, he must # call DceRpcPayload.register_possible_payload() with the payload class as # parameter. # # To be able to decide if the payload class is capable of handling the rest of # the dissection, the classmethod can_handle() should be implemented in the # payload class. This method is given the rest of the string to dissect as # first argument, and the DceRpc packet instance as second argument. Based on # this information, the method must return True if the class is capable of # handling the dissection, False otherwise class DceRpc4Payload(Packet): """Dummy class which use the dispatch_hook to find the payload class""" _payload_class = [] @classmethod def dispatch_hook(cls, _pkt, _underlayer=None, *args, **kargs): """dispatch_hook to choose among different registered payloads""" for klass in cls._payload_class: if hasattr(klass, "can_handle") and klass.can_handle(_pkt, _underlayer): return klass log_runtime.warning("DCE/RPC payload class not found or undefined (using Raw)") return Raw @classmethod def register_possible_payload(cls, pay): """Method to call from possible DCE/RPC endpoint to register it as possible payload""" cls._payload_class.append(pay) bind_layers(DceRpc4, DceRpc4Payload)