def parse(self): try: #Parsing the "connect" command arguments amfCmd = self.get("connect") for arg in amfCmd.args: if type(arg) == dict: for prop in arg: self.RTMP[prop] = arg[prop] else: if type(arg) == str: extra_type = "S:" if type(arg) == bool: extra_type = "B:" if type(arg) == int: extra_type = "N:" self.RTMP["extra"] += extra_type + str(arg) + " " #Parsing the "play" command arguments amfCmd = self.get("play") for arg in amfCmd.args: if arg: self.RTMP["playPath"] = arg break self.RTMP["url"] = os.path.join(self.RTMP["tcUrl"], self.RTMP["playPath"]) except Exception as e: logger.error("Error during the RTMP properties parsing: %s" % e)
def PacketHandler(pkt): global streams global rtmp_port global out_mode global quit_first if pkt.haslayer(TCP) and pkt.haslayer(Raw): #Skipping if the rtmp_port is defined and is different from the packet dest port if rtmp_port != 0 and pkt[TCP].dport != rtmp_port: return sport = pkt[TCP].sport #hexdump(pkt.load) """ The easiest way to follow the TCP streams is to use the source port as distinction element. So i will consider each packet with the same source port as part of the same TCP stream """ if sport not in streams: stream = Stream(pkt.load) streams[sport] = stream else: streams[sport].appendData(pkt.load) if streams[sport].dontScanAgain: return #This is the mininium size that an RTMP stream must have to contains interesting data... if streams[sport].size > 0x600*2: logger.debug("Dissecting stream: %s" % sport) rtmp = rtmpParser() try: amfCmds = rtmp.rtmpParseStream(streams[sport]) #If I have 2 AMF commands (play and connect), I can print the results if amfCmds.count() == 2: logger.info("\n* RTMP Stream found!") amfCmds.printOut(out_mode) streams[sport].dontScanAgain = True if quit_first: sys.exit(0) else: streams[sport].offset = 0 except StreamNoMoreBytes: logger.debug("No more bytes to read from the stream!") except Exception as e: logger.error("Error parsing the RTMP stream: %s" % e)
def PacketHandler(pkt): global streams global rtmp_port global out_mode global quit_first if pkt.haslayer(TCP) and pkt.haslayer(Raw): #Skipping if the rtmp_port is defined and is different from the packet dest port if rtmp_port != 0 and pkt[TCP].dport != rtmp_port: return sport = pkt[TCP].sport #hexdump(pkt.load) """ The easiest way to follow the TCP streams is to use the source port as distinction element. So i will consider each packet with the same source port as part of the same TCP stream """ if sport not in streams: stream = Stream(pkt.load) streams[sport] = stream else: streams[sport].appendData(pkt.load) if streams[sport].dontScanAgain: return #This is the mininium size that an RTMP stream must have to contains interesting data... if streams[sport].size > 0x600 * 2: logger.debug("Dissecting stream: %s" % sport) rtmp = rtmpParser() try: amfCmds = rtmp.rtmpParseStream(streams[sport]) #If I have 2 AMF commands (play and connect), I can print the results if amfCmds.count() == 2: logger.info("\n* RTMP Stream found!") amfCmds.printOut(out_mode) streams[sport].dontScanAgain = True if quit_first: sys.exit(0) else: streams[sport].offset = 0 except StreamNoMoreBytes: logger.debug("No more bytes to read from the stream!") except Exception as e: logger.error("Error parsing the RTMP stream: %s" % e)
logger.info("Andrea Fabrizi - [email protected]\n") streams = dict() #Not sniffing, reading from dump file if args.pcapfile: logger.info("Reading packets from dump file '%s'..." % args.pcapfile) sniff(offline=args.pcapfile, filter="tcp", prn = PacketHandler) #Sniffing on the specified device elif args.device: logger.info("Starting sniffing on %s..." % args.device) try: sniff(iface=args.device, prn = PacketHandler) except socket.error as e: logger.error("Error opening %s for sniffing: %s" % (args.device, e)) logger.info("Are you root and the device is correct?") sys.exit(1) #Default action, sniffing on all the devices else: logger.info("Starting sniffing on all devices...") try: sniff(prn = PacketHandler) except socket.error as e: logger.error("Error opening device for sniffing: %s" % e) logger.info("Are you root?") sys.exit(1)
def rtmpParsePacket (self, stream): """ Looking for the packet header byte 1 BB BBBBBB The first 2 bit indicates the header type as following: b00 = 12 byte header (full header). b01 = 8 bytes - like type b00. not including message ID (4 last bytes). b10 = 4 bytes - Basic Header and timestamp (3 bytes) are included. b11 = 1 byte - only the Basic Header is included. The last 6 bit indicates the chunk stream ID """ byte = stream.getByte() header_type = byte >> 6 chunk_stream_id = byte << 2 chunk_stream_id = chunk_stream_id >> 2 #Header type b00 if header_type == 0: timestamp = Utils.str2num(stream.getBytes(3)) body_size = Utils.str2num(stream.getBytes(3)) packet_type = stream.getByte() stream_id = Utils.str2num(stream.getBytes(4)) #Header type b01 elif header_type == 1: timestamp = Utils.str2num(stream.getBytes(3)) body_size = Utils.str2num(stream.getBytes(3)) packet_type = stream.getByte() """ Header type b10 or b11 Maybe this means that we reached the end of the stream with the setBufferLenght command, so simply I will set the entire stream "as readed" to end the parsing process """ elif header_type == 2 or header_type == 3: stream.offset = stream.size return None else: logger.error("RTMP header type not supported: %d", header_type) return None #Now reading the RTMP payload from the stream magic_byte = 0xC0 + chunk_stream_id magic_bytes_count = body_size / 128 rtmp_payload = stream.getBytes(body_size + magic_bytes_count) if rtmp_payload == None: return None #Unchunking the payload n = 0 while (n<len(rtmp_payload)): if (n % 128 == 0) and (n != 0): if rtmp_payload[n] == chr(magic_byte): rtmp_payload = rtmp_payload[:n] + rtmp_payload[n+1:] else: logger.debug("Expected RTMP magic byte not found in the payload: %d" % n) return None n = n + 1 """ Now parsing the payload! I will create a new Stream object, containing only the payload, to pass to the parsing function """ rtmp_payload_stream = Stream(rtmp_payload) #If it's an AMF/AMF3 command if packet_type == self.AMF_COMMAND or packet_type == self.AMF3_COMMAND: amf = amfCommand() #rtmp_payload_stream.dump() """ In case of AMF3 command, there is an extra byte at the beginning of the body So, lets get it! """ if packet_type == self.AMF3_COMMAND: rtmp_payload_stream.getByte() """ The structure of the RTMP Command is: (String) <Command Name> (Number) <Transaction Id> (Mixed) <Argument> ex. Null, String, Object: {key1:value1, key2:value2 ... } """ #Reading AMF Command amf.name = self.rtmpParseObject(rtmp_payload_stream) #We are interested only in "connect" and "play" objects for our purpose if amf.name not in ["connect","play"]: logger.debug("Found an unuseful command, skypping!: %s" % amf.name) return None #Reading AMF Transaction ID amf.transaction_id = self.rtmpParseObject(rtmp_payload_stream) #Reading all the AMF arguments while (rtmp_payload_stream.haveBytes()): amf.args.append(self.rtmpParseObject(rtmp_payload_stream)) return amf #Otherwise, I can discard it... else: logger.debug("Found an unuseful packet type, skypping!: %d" % packet_type) return None
def rtmpParseObject(self, p): #Object type b = p.getBytes(1) #STRING if (b == self.AMF_STRING): strlen = Utils.str2num(p.getBytes(2)) string = p.getBytes(strlen) logger.debug("Found a string [%s]..." % string) return string #NUMBER #Numbers are stored as 8 byte (big endian) float double elif (b == self.AMF_NUMBER): number = struct.unpack('>d',p.getBytes(8)) logger.debug("Found a number [%d]..." % number) return int(number[0]) #BOOLEAN elif (b == self.AMF_BOOLEAN): boolean = False if (p.getBytes(1) == chr(0)) else True logger.debug("Found a boolean (%s)..." % boolean) return boolean #OBJECT elif (b == self.AMF_OBJECT): logger.debug("Found an object...") obj = dict() #Reading all the object properties, until End Of Object marker is reached while (p.readBytes(3) != "\x00\x00\x09"): #Property name strlen = Utils.str2num(p.getBytes(2)) key = p.getBytes(strlen) logger.debug("Property name [%s]..." % key) #Property value val = self.rtmpParseObject(p) obj[key] = val #Eating the End Of Object marker p.getBytes(3) return obj #NULL elif (b == self.AMF_NULL): logger.debug("Found a NULL byte...") return None #ARRAY #I don't care about it... elif (b == self.AMF_ARRAY): arraylen = Utils.str2num(p.getBytes(4)) logger.debug("Found an array...") while (p.readBytes(3) != "\x00\x00\x09"): pass p.getBytes(3) return 0 #Unknown object else: logger.error("Found an unknown RTMP object: 0x%x" % ord(b)) return None
logger.info("rtmpSnoop v%s - The RTMP Sniffer!" % VERSION) logger.info("Andrea Fabrizi - [email protected]\n") streams = dict() #Not sniffing, reading from dump file if args.pcapfile: logger.info("Reading packets from dump file '%s'..." % args.pcapfile) sniff(offline=args.pcapfile, filter="tcp", prn=PacketHandler) #Sniffing on the specified device elif args.device: logger.info("Starting sniffing on %s..." % args.device) try: sniff(iface=args.device, prn=PacketHandler, store=0) except socket.error as e: logger.error("Error opening %s for sniffing: %s" % (args.device, e)) logger.info("Are you root and the device is correct?") sys.exit(1) #Default action, sniffing on all the devices else: logger.info("Starting sniffing on all devices...") try: sniff(prn=PacketHandler, store=0) except socket.error as e: logger.error("Error opening device for sniffing: %s" % e) logger.info("Are you root?") sys.exit(1)