1 # packets.py -- OpenChange RPC-over-HTTP implementation
3 # Copyright (C) 2012 Wolfgang Sourdeau <wsourdeau@inverse.ca>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 from socket import _socketobject, MSG_WAITALL
21 from struct import pack, unpack_from
27 PFC_PENDING_CANCEL = 4
28 PFC_SUPPORT_HEADER_SIGN = 4
31 PFC_DID_NOT_EXECUTE = 32
34 PFC_FLAG_LABELS = ("PFC_FIRST_FRAG",
36 "PFC_(PENDING_CANCEL|SUPPORT_HEADER_SIGN)",
39 "PFC_DID_NOT_EXECUTE",
44 # taken from dcerpc.idl
45 DCERPC_PKT_REQUEST = 0
47 DCERPC_PKT_RESPONSE = 2
49 DCERPC_PKT_WORKING = 4
53 DCERPC_PKT_CL_CANCEL = 8
55 DCERPC_PKT_CANCEL_ACK = 10
57 DCERPC_PKT_BIND_ACK = 12
58 DCERPC_PKT_BIND_NAK = 13
60 DCERPC_PKT_ALTER_RESP = 15
61 DCERPC_PKT_AUTH_3 = 16
62 DCERPC_PKT_SHUTDOWN = 17
63 DCERPC_PKT_CO_CANCEL = 18
64 DCERPC_PKT_ORPHANED = 19
66 DCERPC_PKG_LABELS = ("DCERPC_PKT_REQUEST",
68 "DCERPC_PKT_RESPONSE",
74 "DCERPC_PKT_CL_CANCEL",
76 "DCERPC_PKT_CANCEL_ACK",
78 "DCERPC_PKT_BIND_ACK",
79 "DCERPC_PKT_BIND_NAK",
81 "DCERPC_PKT_ALTER_RESP",
83 "DCERPC_PKT_SHUTDOWN",
84 "DCERPC_PKT_CO_CANCEL",
85 "DCERPC_PKT_ORPHANED",
90 RTS_FLAG_OTHER_CMD = 2
91 RTS_FLAG_RECYCLE_CHANNEL = 4
92 RTS_FLAG_IN_CHANNEL = 8
93 RTS_FLAG_OUT_CHANNEL = 0x10
96 RTS_FLAG_LABELS = ("RTS_FLAG_PING",
98 "RTS_FLAG_RECYCLE_CHANNEL",
99 "RTS_FLAG_IN_CHANNEL",
100 "RTS_FLAG_OUT_CHANNEL",
104 RTS_CMD_RECEIVE_WINDOW_SIZE = 0
105 RTS_CMD_FLOW_CONTROL_ACK = 1
106 RTS_CMD_CONNECTION_TIMEOUT = 2
108 RTS_CMD_CHANNEL_LIFETIME = 4
109 RTS_CMD_CLIENT_KEEPALIVE = 5
113 RTS_CMD_NEGATIVE_ANCE = 9
115 RTS_CMD_CLIENT_ADDRESS = 11
116 RTS_CMD_ASSOCIATION_GROUP_ID = 12
117 RTS_CMD_DESTINATION = 13
118 RTS_CMD_PING_TRAFFIC_SENT_NOTIFY = 14
120 RTS_CMD_SIZES = (8, 28, 8, 20, 8, 8, 8, 4, 8, 4, 4, 8, 20, 8, 8)
121 RTS_CMD_DATA_LABELS = ("ReceiveWindowSize",
133 "AssociationGroupId",
135 "PingTrafficSentNotify")
137 RPC_C_AUTHN_NONE = 0x0
138 RPC_C_AUTHN_GSS_NEGOTIATE = 0x9 # SPNEGO
139 RPC_C_AUTHN_WINNT = 0xa # NTLM
140 RPC_C_AUTHN_GSS_SCHANNEL = 0xe # TLS
141 RPC_C_AUTHN_GSS_KERBEROS = 0x10 # Kerberos
142 RPC_C_AUTHN_NETLOGON = 0x44 # Netlogon
143 RPC_C_AUTHN_DEFAULT = 0xff # (NTLM)
145 RPC_C_AUTHN_LEVEL_DEFAULT = 0
146 RPC_C_AUTHN_LEVEL_NONE = 1
147 RPC_C_AUTHN_LEVEL_CONNECT = 2
148 RPC_C_AUTHN_LEVEL_CALL = 3
149 RPC_C_AUTHN_LEVEL_PKT = 4
150 RPC_C_AUTHN_LEVEL_PKT_INTEGRITY = 5
151 RPC_C_AUTHN_LEVEL_PKT_PRIVACY = 6
154 class RTSParsingException(IOError):
155 """This exception occurs when a serious issue occurred while parsing an
163 class RPCPacket(object):
164 def __init__(self, data, logger=None):
171 # parsed offset from the start of the "data" blob
174 # header is common to all PDU
178 def from_file(input_file, logger=None):
179 """This static method acts as a constructor and returns an input
180 packet with the proper class, based on the packet headers.
181 The "input_file" parameter must either be a file or a sockect object.
185 if isinstance(input_file, _socketobject):
186 def read_file(count):
187 return input_file.recv(count, MSG_WAITALL)
188 elif hasattr(file, "read") and callable(file.read):
189 def read_file(count):
190 return input_file.read(count)
192 raise ValueError("'input_file' must either be a socket object or"
193 " provide a 'read' method")
195 fields = ("rpc_vers", "rpc_vers_minor", "ptype", "pfc_flags", "drep",
196 "frag_length", "auth_length", "call_id")
198 header_data = read_file(16)
199 # TODO: value validation
200 values = unpack_from("<bbbblhhl", header_data)
201 if values[2] == DCERPC_PKT_FAULT:
202 packet_class = RPCFaultPacket
203 elif values[2] == DCERPC_PKT_BIND_ACK:
204 packet_class = RPCBindACKPacket
205 elif values[2] == DCERPC_PKT_BIND_NAK:
206 packet_class = RPCBindNAKPacket
207 elif values[2] == DCERPC_PKT_RTS:
208 packet_class = RPCRTSPacket
210 packet_class = RPCPacket
211 body_data = read_file(values[5] - 16)
213 packet = packet_class(header_data + body_data, logger)
214 packet.header = dict(zip(fields, values))
216 packet.size = values[5]
224 def pretty_dump(self):
225 (fields, values) = self.make_dump_output()
227 output = ["%s: %s" % (fields[pos], str(values[pos]))
228 for pos in xrange(len(fields))]
230 return "; ".join(output)
232 def make_dump_output(self):
235 ptype = self.header["ptype"]
236 values.append(DCERPC_PKG_LABELS[ptype])
238 flags = self.header["pfc_flags"]
240 values.append("None")
243 for exp in xrange(7):
246 flag_values.append(PFC_FLAG_LABELS[exp])
247 values.append(", ".join(flag_values))
249 fields = ["ptype", "pfc_flags", "drep", "frag_length",
250 "auth_length", "call_id"]
251 for field in fields[2:]:
252 values.append(self.header[field])
254 return (fields, values)
259 class RPCFaultPacket(RPCPacket):
260 def __init__(self, data, logger=None):
261 RPCPacket.__init__(self, data, logger)
264 # bind_ack PDU (incomplete)
265 class RPCBindACKPacket(RPCPacket):
266 def __init__(self, data, logger=None):
267 RPCPacket.__init__(self, data, logger)
268 self.ntlm_payload = None
271 auth_offset = self.header["frag_length"] - self.header["auth_length"]
272 self.ntlm_payload = self.data[auth_offset:]
275 # bind_nak PDU (stub)
276 class RPCBindNAKPacket(RPCPacket):
277 def __init__(self, data, logger=None):
278 RPCPacket.__init__(self, data, logger)
281 # FIXME: command parameters are either int32 values or binary blobs, both when
282 # parsing and when producing
283 class RPCRTSPacket(RPCPacket):
286 def __init__(self, data, logger=None):
287 RPCPacket.__init__(self, data, logger)
293 fields = ("flags", "nbr_commands")
294 values = unpack_from("<hh", self.data, self.offset)
295 self.offset = self.offset + 4
296 self.header.update(zip(fields, values))
298 for counter in xrange(self.header["nbr_commands"]):
299 self._parse_command()
301 if (self.size != self.offset):
302 raise RTSParsingException("sizes do not match: expected = %d,"
304 % (self.size, self.offset))
306 def _parse_command(self):
307 (command_type,) = unpack_from("<l", self.data, self.offset)
308 if command_type < 0 or command_type > 15:
309 raise RTSParsingException("command type unknown: %d"
311 self.offset = self.offset + 4
313 command = {"type": command_type}
314 command_size = RTS_CMD_SIZES[command_type]
316 data_size = command_size - 4
317 if command_type in self.parsers:
318 parser = self.parsers[command_type]
319 data_value = parser(self, data_size)
321 # commands with int32 values
322 (data_value,) = unpack_from("<l", self.data, self.offset)
323 self.offset = self.offset + 4
325 raise RTSParsingException("command is badly handled: %d"
328 data_label = RTS_CMD_DATA_LABELS[command_type]
329 command[data_label] = data_value
331 self.commands.append(command)
333 def _parse_command_flow_control_ack(self, data_size):
334 data_blob = self.data[self.offset:self.offset+data_size]
335 self.offset = self.offset + data_size
339 def _parse_command_cookie(self, data_size):
340 data_blob = self.data[self.offset:self.offset+data_size]
341 self.offset = self.offset + data_size
345 def _parse_command_padding_data(self, data_size):
346 # the length of the padding bytes is specified in the
347 # ConformanceCount field
348 (count,) = unpack_from("<l", self.data, self.offset)
349 self.offset = self.offset + 4
351 data_blob = self.data[self.offset:self.offset+count]
352 self.offset = self.offset + count
356 def _parse_command_client_address(self, data_blob):
357 (address_type,) = unpack_from("<l", self.data, self.offset)
358 self.offset = self.offset + 4
360 if address_type == 0: # ipv4
362 elif address_type == 1: # ipv6
365 raise RTSParsingException("unknown client address type: %d"
368 data_blob = self.data[self.offset:self.offset+address_size]
370 # compute offset with padding, which is ignored
371 self.offset = self.offset + address_size + 12
375 def make_dump_output(self):
376 (fields, values) = RPCPacket.make_dump_output(self)
377 fields.extend(("flags", "nbr_commands"))
379 flags = self.header["flags"]
380 if flags == RTS_FLAG_NONE:
381 values.append("RTS_FLAG_NONE")
384 for exp in xrange(7):
387 flags_value.append(RTS_FLAG_LABELS[exp])
388 values.append(", ".join(flags_value))
390 values.append(self.header["nbr_commands"])
392 return (fields, values)
395 # Those are the parser method for commands with a size > 4. They are defined
396 # here since the "RPCRTSPacket" symbol is not accessible as long as the class
397 # definition is not over
398 RPCRTSPacket.parsers = {RTS_CMD_FLOW_CONTROL_ACK: RPCRTSPacket._parse_command_flow_control_ack,
399 RTS_CMD_COOKIE: RPCRTSPacket._parse_command_cookie,
400 RTS_CMD_ASSOCIATION_GROUP_ID: RPCRTSPacket._parse_command_cookie,
401 RTS_CMD_PADDING: RPCRTSPacket._parse_command_padding_data,
402 RTS_CMD_CLIENT_ADDRESS: RPCRTSPacket._parse_command_client_address}
408 # bind PDU (strict minimum required for NTLMSSP auth)
409 class RPCBindOutPacket(object):
410 def __init__(self, logger=None):
417 self.ntlm_payload = None
420 if self.data is None:
421 self._make_packet_data()
425 def _make_packet_data(self):
426 if self.ntlm_payload is None:
427 raise ValueError("'ntlm_payload' attribute must not be None")
429 ntlm_payload_size = len(self.ntlm_payload)
430 align_modulo = ntlm_payload_size % 4
432 padding = (4 - align_modulo) * "\0"
435 len_padding = len(padding)
438 # rfr: 1544f5e0-613c-11d1-93df-00c04fd7bd09, v1
439 # mgmt: afa8bd80-7d8a-11c9-bef4-08002b102989, v1
440 svc_guid = UUID('{afa8bd80-7d8a-11c9-bef4-08002b102989}')
441 iface_version_major = 1
442 iface_version_minor = 0
444 p_content_elem = ("\x01\x00\x00\x00\x00\x00\x01\x00"
446 "\x04\x5d\x88\x8a\xeb\x1c\xc9\x11\x9f\xe8\x08\x00"
447 "\x2b\x10\x48\x60\x02\x00\x00\x00"
448 % (svc_guid.bytes_le,
451 iface_version_minor)))
452 # p_content_elem = ("\x00\x00\x00\x00")
453 len_p_content_elem = len(p_content_elem)
455 header_data = pack("<bbbbbbbbhhl hhl %ds bbbbl" % len_p_content_elem,
458 5, 0, # rpc_vers, rpc_vers_minor
459 DCERPC_PKT_BIND, # ptype
463 | PFC_SUPPORT_HEADER_SIGN,
465 # drep: RPC spec chap14.htm (Data Representation Format Label)
466 (1 << 4) | 0, 0, 0, 0,
467 (32 + ntlm_payload_size + len_padding + len_p_content_elem), # frag_length
468 ntlm_payload_size + len_padding, # auth_length
469 self.call_id, # call_id
472 4088, 4088, # max_xmit/recv_frag
478 # p_context_elem (flattened to int32):
482 RPC_C_AUTHN_WINNT, # auth_verifier.auth_type
483 # auth_verifier.auth_level:
484 RPC_C_AUTHN_LEVEL_CONNECT,
485 len_padding, # auth_verifier.auth_pad_length
486 0, # auth_verifier.auth_reserved
487 1 # auth_verifier.auth_context_id
489 self.size = len(header_data) + ntlm_payload_size + len_padding
490 self.data = header_data + self.ntlm_payload + padding
494 class RPCAuth3OutPacket(object):
495 def __init__(self, logger=None):
501 self.pfc_flags = PFC_FIRST_FRAG | PFC_LAST_FRAG
504 self.ntlm_payload = None
507 if self.data is None:
508 self._make_packet_data()
512 def _make_packet_data(self):
513 if self.ntlm_payload is None:
514 raise ValueError("'ntlm_payload' attribute must not be None")
516 ntlm_payload_size = len(self.ntlm_payload)
517 align_modulo = ntlm_payload_size % 4
519 len_padding = (4 - align_modulo)
523 header_data = pack("<bbbbbbbbhhl 4s bbbbl",
524 5, 0, # rpc_vers, rpc_vers_minor
525 DCERPC_PKT_AUTH_3, # ptype
528 # drep: RPC spec chap14.htm (Data Representation Format Label)
529 (1 << 4) | 0, 0, 0, 0,
530 (28 + ntlm_payload_size + len_padding), # frag_length
531 ntlm_payload_size + len_padding, # auth_length
532 self.call_id, # call_id
538 RPC_C_AUTHN_WINNT, # auth_verifier.auth_type
539 # auth_verifier.auth_level:
540 RPC_C_AUTHN_LEVEL_CONNECT,
541 len_padding, # auth_verifier.auth_pad_length
542 0, # auth_verifier.auth_reserved
543 1 # auth_verifier.auth_context_id
545 self.size = len(header_data) + ntlm_payload_size + len_padding
546 self.data = header_data + self.ntlm_payload + len_padding * "\x00"
550 class RPCPingOutPacket(object):
551 def __init__(self, logger=None):
557 self.pfc_flags = PFC_FIRST_FRAG | PFC_LAST_FRAG
561 if self.data is None:
562 self._make_packet_data()
566 def _make_packet_data(self):
567 header_data = pack("<bbbbbbbbhhl",
570 5, 0, # rpc_vers, rpc_vers_minor
571 DCERPC_PKT_PING, # ptype
574 # drep: RPC spec chap14.htm (Data Representation Format Label)
575 (1 << 4) | 0, 0, 0, 0,
578 self.call_id # call_id
580 self.size = len(header_data)
581 self.data = header_data
585 class RPCRTSOutPacket(object):
586 def __init__(self, logger=None):
591 self.flags = RTS_FLAG_NONE
592 self.command_data = []
595 if self.command_data is None:
596 raise RTSParsingException("packet already returned")
600 data = "".join(self.command_data)
601 data_size = len(data)
603 if (data_size != self.size):
604 raise RTSParsingException("sizes do not match: declared = %d,"
605 " actual = %d" % (self.size, data_size))
606 self.command_data = None
608 if self.logger is not None:
609 self.logger.info("returning packet: %s" % repr(data))
613 def _make_header(self):
614 header_data = pack("<bbbbbbbbhhlhh",
615 5, 0, # rpc_vers, rpc_vers_minor
616 DCERPC_PKT_RTS, # ptype
617 PFC_FIRST_FRAG | PFC_LAST_FRAG, # pfc_flags
618 # drep: RPC spec chap14.htm (Data Representation Format Label)
619 (1 << 4) | 0, 0, 0, 0,
620 (20 + self.size), # frag_length
624 len(self.command_data))
625 self.command_data.insert(0, header_data)
626 self.size = self.size + 20
628 def add_command(self, command_type, *args):
629 if command_type < 0 or command_type > 15:
630 raise RTSParsingException("command type unknown: %d (%s)" %
631 (command_type, str(type(command_type))))
633 self.size = self.size + 4
635 values = [pack("<l", command_type)]
637 command_size = RTS_CMD_SIZES[command_type]
639 if command_type == RTS_CMD_FLOW_CONTROL_ACK:
640 data = self._make_command_flow_control_ack(args[0])
641 elif (command_type == RTS_CMD_COOKIE
642 or command_type == RTS_CMD_ASSOCIATION_GROUP_ID):
643 data = self._make_command_cookie(args[0])
644 elif command_type == RTS_CMD_PADDING:
645 data = self._make_command_padding_data(args[0])
646 elif command_type == RTS_CMD_CLIENT_ADDRESS:
647 data = self._make_command_client_address(args[0])
649 # command with int32 value
650 data = pack("<l", args[0])
651 self.size = self.size + 4
654 self.command_data.append("".join(values))
656 def _make_command_flow_control_ack(self, data_blob):
658 len_data = len(data_blob)
660 raise RTSParsingException("expected a length of %d bytes,"
661 " received %d" % (24, len_data))
662 self.size = self.size + len_data
666 def _make_command_cookie(self, data_blob):
668 len_data = len(data_blob)
670 raise RTSParsingException("expected a length of %d bytes,"
671 " received %d" % (16, len_data))
672 self.size = self.size + len_data
676 def _make_command_padding_data(self, data_blob):
677 len_data = len(data_blob)
678 data = pack("<l", len_data) + data_blob
679 self.size = self.size + 4 + len_data
683 def _make_command_client_address(self, data_blob):
684 len_data = len(data_blob)
686 address_type = 0 # ipv4
688 address_type = 1 # ipv6
690 raise RTSParsingException("cannot deduce address type from data"
691 " length: %d" % len_data)
693 data = pack("<l", address_type) + data_blob + 12 * chr(0)
694 self.size = self.size + 4 + len_data + 12