1#!/usr/bin/python3 2 3# SPDX-License-Identifier: Apache-2.0 4# Copyright 2019 IBM Corp. 5 6import json 7import struct 8import sys 9from argparse import ArgumentParser 10from collections import namedtuple 11from enum import Enum 12from itertools import islice 13 14from scapy.all import rdpcap 15 16RawMessage = namedtuple("RawMessage", "endian, header, data") 17FixedHeader = namedtuple( 18 "FixedHeader", "endian, type, flags, version, length, cookie" 19) 20CookedHeader = namedtuple("CookedHeader", "fixed, fields") 21CookedMessage = namedtuple("CookedMessage", "header, body") 22TypeProperty = namedtuple("TypeProperty", "field, type, nature") 23TypeContainer = namedtuple("TypeContainer", "type, members") 24Field = namedtuple("Field", "type, data") 25 26 27class MessageEndian(Enum): 28 LITTLE = ord("l") 29 BIG = ord("B") 30 31 32StructEndianLookup = { 33 MessageEndian.LITTLE.value: "<", 34 MessageEndian.BIG.value: ">", 35} 36 37 38class MessageType(Enum): 39 INVALID = 0 40 METHOD_CALL = 1 41 METHOD_RETURN = 2 42 ERROR = 3 43 SIGNAL = 4 44 45 46class MessageFlags(Enum): 47 NO_REPLY_EXPECTED = 0x01 48 NO_AUTO_START = 0x02 49 ALLOW_INTERACTIVE_AUTHORIZATION = 0x04 50 51 52class MessageFieldType(Enum): 53 INVALID = 0 54 PATH = 1 55 INTERFACE = 2 56 MEMBER = 3 57 ERROR_NAME = 4 58 REPLY_SERIAL = 5 59 DESTINATION = 6 60 SENDER = 7 61 SIGNATURE = 8 62 UNIX_FDS = 9 63 64 65class DBusType(Enum): 66 INVALID = 0 67 BYTE = ord("y") 68 BOOLEAN = ord("b") 69 INT16 = ord("n") 70 UINT16 = ord("q") 71 INT32 = ord("i") 72 UINT32 = ord("u") 73 INT64 = ord("x") 74 UINT64 = ord("t") 75 DOUBLE = ord("d") 76 STRING = ord("s") 77 OBJECT_PATH = ord("o") 78 SIGNATURE = ord("g") 79 ARRAY = ord("a") 80 STRUCT = ord("(") 81 VARIANT = ord("v") 82 DICT_ENTRY = ord("{") 83 UNIX_FD = ord("h") 84 85 86DBusContainerTerminatorLookup = { 87 chr(DBusType.STRUCT.value): ")", 88 chr(DBusType.DICT_ENTRY.value): "}", 89} 90 91 92class DBusTypeCategory(Enum): 93 FIXED = { 94 DBusType.BYTE.value, 95 DBusType.BOOLEAN.value, 96 DBusType.INT16.value, 97 DBusType.UINT16.value, 98 DBusType.INT32.value, 99 DBusType.UINT32.value, 100 DBusType.INT64.value, 101 DBusType.UINT64.value, 102 DBusType.DOUBLE.value, 103 DBusType.UNIX_FD.value, 104 } 105 STRING = { 106 DBusType.STRING.value, 107 DBusType.OBJECT_PATH.value, 108 DBusType.SIGNATURE.value, 109 } 110 CONTAINER = { 111 DBusType.ARRAY.value, 112 DBusType.STRUCT.value, 113 DBusType.VARIANT.value, 114 DBusType.DICT_ENTRY.value, 115 } 116 RESERVED = { 117 DBusType.INVALID.value, 118 } 119 120 121TypePropertyLookup = { 122 DBusType.BYTE.value: TypeProperty(DBusType.BYTE, "B", 1), 123 # DBus booleans are 32 bit, with only the LSB used. Extract as 'I'. 124 DBusType.BOOLEAN.value: TypeProperty(DBusType.BOOLEAN, "I", 4), 125 DBusType.INT16.value: TypeProperty(DBusType.INT16, "h", 2), 126 DBusType.UINT16.value: TypeProperty(DBusType.UINT16, "H", 2), 127 DBusType.INT32.value: TypeProperty(DBusType.INT32, "i", 4), 128 DBusType.UINT32.value: TypeProperty(DBusType.UINT32, "I", 4), 129 DBusType.INT64.value: TypeProperty(DBusType.INT64, "q", 8), 130 DBusType.UINT64.value: TypeProperty(DBusType.UINT64, "Q", 8), 131 DBusType.DOUBLE.value: TypeProperty(DBusType.DOUBLE, "d", 8), 132 DBusType.STRING.value: TypeProperty(DBusType.STRING, "s", DBusType.UINT32), 133 DBusType.OBJECT_PATH.value: TypeProperty( 134 DBusType.OBJECT_PATH, "s", DBusType.UINT32 135 ), 136 DBusType.SIGNATURE.value: TypeProperty( 137 DBusType.SIGNATURE, "s", DBusType.BYTE 138 ), 139 DBusType.ARRAY.value: TypeProperty(DBusType.ARRAY, None, DBusType.UINT32), 140 DBusType.STRUCT.value: TypeProperty(DBusType.STRUCT, None, 8), 141 DBusType.VARIANT.value: TypeProperty(DBusType.VARIANT, None, 1), 142 DBusType.DICT_ENTRY.value: TypeProperty(DBusType.DICT_ENTRY, None, 8), 143 DBusType.UNIX_FD.value: TypeProperty(DBusType.UINT32, None, 8), 144} 145 146 147def parse_signature(sigstream): 148 sig = ord(next(sigstream)) 149 assert sig not in DBusTypeCategory.RESERVED.value 150 if sig in DBusTypeCategory.FIXED.value: 151 ty = TypePropertyLookup[sig].field, None 152 elif sig in DBusTypeCategory.STRING.value: 153 ty = TypePropertyLookup[sig].field, None 154 elif sig in DBusTypeCategory.CONTAINER.value: 155 if sig == DBusType.ARRAY.value: 156 ty = DBusType.ARRAY, parse_signature(sigstream) 157 elif sig == DBusType.STRUCT.value or sig == DBusType.DICT_ENTRY.value: 158 collected = list() 159 ty = parse_signature(sigstream) 160 while ty is not StopIteration: 161 collected.append(ty) 162 ty = parse_signature(sigstream) 163 ty = DBusType.STRUCT, collected 164 elif sig == DBusType.VARIANT.value: 165 ty = TypePropertyLookup[sig].field, None 166 else: 167 assert False 168 else: 169 assert chr(sig) in DBusContainerTerminatorLookup.values() 170 return StopIteration 171 172 return TypeContainer._make(ty) 173 174 175class AlignedStream(object): 176 def __init__(self, buf, offset=0): 177 self.stash = (buf, offset) 178 self.stream = iter(buf) 179 self.offset = offset 180 181 def align(self, tc): 182 assert tc.type.value in TypePropertyLookup 183 prop = TypePropertyLookup[tc.type.value] 184 if prop.field.value in DBusTypeCategory.STRING.value: 185 prop = TypePropertyLookup[prop.nature.value] 186 if prop.nature == DBusType.UINT32: 187 prop = TypePropertyLookup[prop.nature.value] 188 advance = ( 189 prop.nature - (self.offset & (prop.nature - 1)) 190 ) % prop.nature 191 _ = bytes(islice(self.stream, advance)) 192 self.offset += len(_) 193 194 def take(self, size): 195 val = islice(self.stream, size) 196 self.offset += size 197 return val 198 199 def autotake(self, tc): 200 assert tc.type.value in DBusTypeCategory.FIXED.value 201 assert tc.type.value in TypePropertyLookup 202 self.align(tc) 203 prop = TypePropertyLookup[tc.type.value] 204 return self.take(prop.nature) 205 206 def drain(self): 207 remaining = bytes(self.stream) 208 offset = self.offset 209 self.offset += len(remaining) 210 if self.offset - self.stash[1] != len(self.stash[0]): 211 print( 212 "(self.offset - self.stash[1]): %d, len(self.stash[0]): %d" 213 % (self.offset - self.stash[1], len(self.stash[0])), 214 file=sys.stderr, 215 ) 216 raise MalformedPacketError 217 return remaining, offset 218 219 def dump(self): 220 print( 221 "AlignedStream: absolute offset: {}".format(self.offset), 222 file=sys.stderr, 223 ) 224 print( 225 "AlignedStream: relative offset: {}".format( 226 self.offset - self.stash[1] 227 ), 228 file=sys.stderr, 229 ) 230 print( 231 "AlignedStream: remaining buffer:\n{}".format(self.drain()[0]), 232 file=sys.stderr, 233 ) 234 print( 235 "AlignedStream: provided buffer:\n{}".format(self.stash[0]), 236 file=sys.stderr, 237 ) 238 239 def dump_assert(self, condition): 240 if condition: 241 return 242 self.dump() 243 assert condition 244 245 246def parse_fixed(endian, stream, tc): 247 assert tc.type.value in TypePropertyLookup 248 prop = TypePropertyLookup[tc.type.value] 249 val = bytes(stream.autotake(tc)) 250 try: 251 val = struct.unpack("{}{}".format(endian, prop.type), val)[0] 252 return bool(val) if prop.type == DBusType.BOOLEAN else val 253 except struct.error as e: 254 print(e, file=sys.stderr) 255 print("parse_fixed: Error unpacking {}".format(val), file=sys.stderr) 256 print( 257 ( 258 "parse_fixed: Attempting to unpack type {} " 259 + "with properties {}" 260 ).format(tc.type, prop), 261 file=sys.stderr, 262 ) 263 stream.dump_assert(False) 264 265 266def parse_string(endian, stream, tc): 267 assert tc.type.value in TypePropertyLookup 268 prop = TypePropertyLookup[tc.type.value] 269 size = parse_fixed(endian, stream, TypeContainer(prop.nature, None)) 270 # Empty DBus strings have no NUL-terminator 271 if size == 0: 272 return "" 273 # stream.dump_assert(size > 0) 274 val = bytes(stream.take(size + 1)) 275 try: 276 stream.dump_assert(len(val) == size + 1) 277 try: 278 return struct.unpack("{}{}".format(size, prop.type), val[:size])[ 279 0 280 ].decode() 281 except struct.error as e: 282 stream.dump() 283 raise AssertionError(e) 284 except AssertionError as e: 285 print( 286 "parse_string: Error unpacking string of length {} from {}".format( 287 size, val 288 ), 289 file=sys.stderr, 290 ) 291 raise e 292 293 294def parse_type(endian, stream, tc): 295 if tc.type.value in DBusTypeCategory.FIXED.value: 296 val = parse_fixed(endian, stream, tc) 297 elif tc.type.value in DBusTypeCategory.STRING.value: 298 val = parse_string(endian, stream, tc) 299 elif tc.type.value in DBusTypeCategory.CONTAINER.value: 300 val = parse_container(endian, stream, tc) 301 else: 302 stream.dump_assert(False) 303 304 return val 305 306 307def parse_array(endian, stream, tc): 308 arr = list() 309 length = parse_fixed(endian, stream, TypeContainer(DBusType.UINT32, None)) 310 stream.align(tc) 311 offset = stream.offset 312 while (stream.offset - offset) < length: 313 elem = parse_type(endian, stream, tc) 314 arr.append(elem) 315 if (stream.offset - offset) < length: 316 stream.align(tc) 317 return arr 318 319 320def parse_struct(endian, stream, tcs): 321 arr = list() 322 stream.align(TypeContainer(DBusType.STRUCT, None)) 323 for tc in tcs: 324 arr.append(parse_type(endian, stream, tc)) 325 return arr 326 327 328def parse_variant(endian, stream): 329 sig = parse_string(endian, stream, TypeContainer(DBusType.SIGNATURE, None)) 330 tc = parse_signature(iter(sig)) 331 return parse_type(endian, stream, tc) 332 333 334def parse_container(endian, stream, tc): 335 if tc.type == DBusType.ARRAY: 336 return parse_array(endian, stream, tc.members) 337 elif tc.type in (DBusType.STRUCT, DBusType.DICT_ENTRY): 338 return parse_struct(endian, stream, tc.members) 339 elif tc.type == DBusType.VARIANT: 340 return parse_variant(endian, stream) 341 else: 342 stream.dump_assert(False) 343 344 345def parse_fields(endian, stream): 346 sig = parse_signature(iter("a(yv)")) 347 fields = parse_container(endian, stream, sig) 348 # The header ends after its alignment padding to an 8-boundary. 349 # https://dbus.freedesktop.org/doc/dbus-specification.html#message-protocol-messages 350 stream.align(TypeContainer(DBusType.STRUCT, None)) 351 return list(map(lambda v: Field(MessageFieldType(v[0]), v[1]), fields)) 352 353 354class MalformedPacketError(Exception): 355 pass 356 357 358def parse_header(raw, ignore_error): 359 assert raw.endian in StructEndianLookup.keys() 360 endian = StructEndianLookup[raw.endian] 361 fixed = FixedHeader._make( 362 struct.unpack("{}BBBBLL".format(endian), raw.header) 363 ) 364 astream = AlignedStream(raw.data, len(raw.header)) 365 fields = parse_fields(endian, astream) 366 data, offset = astream.drain() 367 if (not ignore_error) and fixed.length > len(data): 368 raise MalformedPacketError 369 return CookedHeader(fixed, fields), AlignedStream(data, offset) 370 371 372def parse_body(header, stream): 373 assert header.fixed.endian in StructEndianLookup 374 endian = StructEndianLookup[header.fixed.endian] 375 body = list() 376 for field in header.fields: 377 if field.type == MessageFieldType.SIGNATURE: 378 sigstream = iter(field.data) 379 try: 380 while True: 381 tc = parse_signature(sigstream) 382 val = parse_type(endian, stream, tc) 383 body.append(val) 384 except StopIteration: 385 pass 386 break 387 return body 388 389 390def parse_message(raw): 391 try: 392 header, data = parse_header(raw, False) 393 try: 394 body = parse_body(header, data) 395 return CookedMessage(header, body) 396 except AssertionError as e: 397 print(header, file=sys.stderr) 398 raise e 399 except AssertionError as e: 400 print(raw, file=sys.stderr) 401 raise e 402 403 404def parse_packet(packet): 405 data = bytes(packet) 406 raw = RawMessage(data[0], data[:12], data[12:]) 407 try: 408 msg = parse_message(raw) 409 except MalformedPacketError: 410 print("Got malformed packet: {}".format(raw), file=sys.stderr) 411 # For a message that is so large that its payload data could not be 412 # parsed, just parse its header, then set its data field to empty. 413 header, data = parse_header(raw, True) 414 msg = CookedMessage(header, []) 415 return msg 416 417 418CallEnvelope = namedtuple("CallEnvelope", "cookie, origin") 419 420 421def parse_session(session, matchers, track_calls): 422 calls = set() 423 for packet in session: 424 try: 425 cooked = parse_packet(packet) 426 if not matchers: 427 yield packet.time, cooked 428 elif any(all(r(cooked) for r in m) for m in matchers): 429 if cooked.header.fixed.type == MessageType.METHOD_CALL.value: 430 s = [ 431 f 432 for f in cooked.header.fields 433 if f.type == MessageFieldType.SENDER 434 ][0] 435 calls.add(CallEnvelope(cooked.header.fixed.cookie, s.data)) 436 yield packet.time, cooked 437 elif track_calls: 438 responseTypes = { 439 MessageType.METHOD_RETURN.value, 440 MessageType.ERROR.value, 441 } 442 if cooked.header.fixed.type not in responseTypes: 443 continue 444 rs = [ 445 f 446 for f in cooked.header.fields 447 if f.type == MessageFieldType.REPLY_SERIAL 448 ][0] 449 d = [ 450 f 451 for f in cooked.header.fields 452 if f.type == MessageFieldType.DESTINATION 453 ][0] 454 ce = CallEnvelope(rs.data, d.data) 455 if ce in calls: 456 calls.remove(ce) 457 yield packet.time, cooked 458 except MalformedPacketError: 459 pass 460 461 462def gen_match_type(rule): 463 mt = MessageType.__members__[rule.value.upper()] 464 return lambda p: p.header.fixed.type == mt.value 465 466 467def gen_match_sender(rule): 468 mf = Field(MessageFieldType.SENDER, rule.value) 469 return lambda p: any(f == mf for f in p.header.fields) 470 471 472def gen_match_interface(rule): 473 mf = Field(MessageFieldType.INTERFACE, rule.value) 474 return lambda p: any(f == mf for f in p.header.fields) 475 476 477def gen_match_member(rule): 478 mf = Field(MessageFieldType.MEMBER, rule.value) 479 return lambda p: any(f == mf for f in p.header.fields) 480 481 482def gen_match_path(rule): 483 mf = Field(MessageFieldType.PATH, rule.value) 484 return lambda p: any(f == mf for f in p.header.fields) 485 486 487def gen_match_destination(rule): 488 mf = Field(MessageFieldType.DESTINATION, rule.value) 489 return lambda p: any(f == mf for f in p.header.fields) 490 491 492ValidMatchKeys = { 493 "type", 494 "sender", 495 "interface", 496 "member", 497 "path", 498 "destination", 499} 500MatchRule = namedtuple("MatchExpression", "key, value") 501 502 503# https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules 504def parse_match_rules(exprs): 505 matchers = list() 506 for mexpr in exprs: 507 rules = list() 508 for rexpr in mexpr.split(","): 509 rule = MatchRule._make( 510 map(lambda s: str.strip(s, "'"), rexpr.split("=")) 511 ) 512 assert rule.key in ValidMatchKeys, f"Invalid expression: {rule}" 513 rules.append(globals()["gen_match_{}".format(rule.key)](rule)) 514 matchers.append(rules) 515 return matchers 516 517 518def packetconv(obj): 519 if isinstance(obj, Enum): 520 return obj.value 521 raise TypeError 522 523 524def main(): 525 parser = ArgumentParser() 526 parser.add_argument( 527 "--json", 528 action="store_true", 529 help="Emit a JSON representation of the messages", 530 ) 531 parser.add_argument( 532 "--no-track-calls", 533 action="store_true", 534 default=False, 535 help="Make a call response pass filters", 536 ) 537 parser.add_argument("file", help="The pcap file") 538 parser.add_argument( 539 "expressions", nargs="*", help="DBus message match expressions" 540 ) 541 args = parser.parse_args() 542 stream = rdpcap(args.file) 543 matchers = parse_match_rules(args.expressions) 544 try: 545 if args.json: 546 for _, msg in parse_session( 547 stream, matchers, not args.no_track_calls 548 ): 549 print("{}".format(json.dumps(msg, default=packetconv))) 550 else: 551 for time, msg in parse_session( 552 stream, matchers, not args.no_track_calls 553 ): 554 print("{}: {}".format(time, msg)) 555 print() 556 except BrokenPipeError: 557 pass 558 559 560if __name__ == "__main__": 561 main() 562