#!/usr/bin/env python #Copyright 2006 Sebastian Hagen # This file is part of eucharis. # eucharis is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 # as published by the Free Software Foundation # eucharis is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with eucharis; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # Rudimentary implementation of the eggdrop botnet protocol as of 1.6.17 import time import re import md5 import logging import socket import socket_management from socket_management import timers_add import input_handlers from data_formatting import seconds_hr_relative class eggdrop_botnet_connection_base(socket_management.sock_stream_connection_linebased): def __init__(self, address, input_handler, close_handler, parent_loggername=''): socket_management.sock_stream_connection_linebased.__init__(self, line_delimiters=['\n']) #command mapping taken from eggdrop 1.6.15 source code. self.loggername = parent_loggername + '.base' self.logger = logging.getLogger(self.loggername) self.eggdrop_command_in_mapping = { #legacy commands 'actchan': 'bot_actchan', 'chan': 'bot_chan2', 'chat': 'bot_chat', 'error': 'bot_error', 'filereject': 'bot_filereject', 'filereq': 'bot_filereq', 'filesend': 'bot_filesend', 'handshake': 'bot_handshake', 'idle': 'bot_idle', 'info?': 'bot_infoq', 'join': 'bot_join', 'link': 'bot_link', 'linked': 'bot_linked', 'motd': 'bot_motd', 'nlinked': 'bot_nlinked', 'part': 'bot_part', 'ping': 'bot_ping', 'pong': 'bot_pong', 'priv': 'bot_priv', 'reject': 'bot_reject', 'thisbot': 'bot_thisbot', 'trace': 'bot_trace', 'traced': 'bot_traced', 'uf-no': 'bot_ufno', 'unaway': 'bot_away', 'unlink': 'bot_unlink', 'unlinked': 'bot_unlinked', 'update': 'bot_update', 'userfile?': 'bot_old_userfile', 'zapf': 'bot_zapf', 'zapf-broad': 'bot_zapfbroad', 'who': 'bot_who', #token commands 'a': 'bot_actchan', 'aw': 'bot_away', 'away': 'bot_away', 'bye': 'bot_bye', 'c': 'bot_chan2', 'ct': 'bot_chat', 'e': 'bot_error', 'el': 'bot_endlink', 'f!': 'bot_filereject', 'fr': 'bot_filereq', 'fs': 'bot_filesend', 'h': 'bot_handshake', 'i': 'bot_idle', 'i?': 'bot_infoq', 'j': 'bot_join', 'l': 'bot_link', 'm': 'bot_motd', 'n': 'bot_nlinked', 'nc': 'bot_nickchange', 'p': 'bot_priv', 'pi': 'bot_ping', 'po': 'bot_pong', 'pt': 'bot_part', 'r': 'bot_reject', 's': 'bot_share', 't': 'bot_trace', 'tb': 'bot_thisbot', 'td': 'bot_traced', 'u': 'bot_update', 'ul': 'bot_unlink', 'un': 'bot_unlinked', 'v': 'bot_versions', 'w': 'bot_who', 'z': 'bot_zapf', 'zb': 'bot_zapfbroad', } self.eggdrop_command_out_mapping = { 'bot_endlink': 'el', 'bot_zapfbroad': 'zb', 'bot_filereject': 'f!', 'bot_away': 'aw', 'bot_chat': 'ct', 'bot_part': 'pt', 'bot_unlink': 'ul', 'bot_away': 'away', 'bot_nickchange': 'nc', 'bot_unlinked': 'un', 'bot_traced': 'td', 'bot_ping': 'pi', 'bot_thisbot': 'tb', 'bot_pong': 'po', 'bot_filereq': 'fr', 'bot_filesend': 'fs', 'bot_infoq': 'i?', 'bot_actchan': 'a', 'bot_chan2': 'c', 'bot_bye': 'bye', 'bot_error': 'e', 'bot_idle': 'i', 'bot_handshake': 'h', 'bot_join': 'j', 'bot_motd': 'm', 'bot_link': 'l', 'bot_nlinked': 'n', 'bot_priv': 'p', 'bot_share': 's', 'bot_reject': 'r', 'bot_update': 'u', 'bot_trace': 't', 'bot_who': 'w', 'bot_versions': 'v', 'bot_zapf': 'z' } self.close_handler = close_handler self.input_handler = self.handle_input self.input_handler_2 = input_handler self.connection_init(address) def handle_input(self): for line_raw in self.buffer_lines: if ((line_raw == '') or (line_raw == '\r')): continue if (line_raw[-1] == '\r'): line = line_raw[:-1] telnet_init = 1 else: line = line_raw telnet_init = 0 line_split = line.split(' ',1) if (len(line_split) > 0): commandstring = line_split[0] else: continue if (len(line_split) > 1): line_data = line_split[1] else: line_data = None if (commandstring in self.eggdrop_command_in_mapping): eggdrop_command = self.eggdrop_command_in_mapping[commandstring] else: eggdrop_command = commandstring self.logger.log(10, '> %r' % line) self.input_handler_2(line_raw=line ,command=eggdrop_command, data=line_data) self.buffer_lines = [] def send_command(self, command, data=None): if (type(command) == str): if (command in self.eggdrop_command_out_mapping): output_string = self.eggdrop_command_out_mapping[command] else: output_string = command if ((type(data) == str) and (data != '')): output_string = output_string + ' ' + data output_string = output_string + '\n' self.send_data(output_string) class eggdrop_botnet_connection: def __init__(self, address, botname, passstring=None, software='botnet_interface', network_name='unknown', reconnect_delay=100): self.goal = 'continue' self.loggername = 'botnet_interface' self.reconnect_delay = reconnect_delay self.logger = logging.getLogger(self.loggername) self.handlers = handlers_botnet_interface(parent=self, output=self.send_command, parent_loggername=self.loggername) self.peer_settings = { 'reconnect_delay':100.0, 'name':botname, 'virtual_eggdrop_version':(2 ,0, 0, 0), 'handle_length':9, 'software':software, 'software_version': '0.0.1', 'network_name':network_name, 'passstring':passstring } self.uplink_data = { 'address':address, 'name':None, 'version':None, 'handle_length':None } self.timestamps = { 'init':time.time() } self.connection_base = None self.connection_state = 'down' self.out_queue = [] def send_command(self, command, data=None, force_send=False): if (self.connection_base != None): if ((self.connection_state == 'up') or force_send): if ((type(data) == str) and ((command == 'bot_zapf') or (command == 'bot_zapfbroad'))): data = '%s %s' % (self.peer_settings['name'], data) self.connection_base.send_command(command, data) else: self.out_queue.append((command, data)) def connection_close_handler(self, old_stream_socket, old_fd): self.connection_base = None self.connection_state = 'down' self.handlers.event_connection(statechange='down', data=None) if (self.goal == 'continue'): timers_add(delay=self.reconnect_delay, callback_handler=self.connection_init, parent=self) def connection_init(self): address = self.uplink_data['address'] self.uplink_data = { 'address':address } self.out_queue = [] self.network_peers = {} self.connection_base = None try: self.connection_base = eggdrop_botnet_connection_base(address=address, input_handler=self.input_process, close_handler=self.connection_close_handler, parent_loggername=self.loggername) except socket.error: self.logger.log(40, 'Connection attempt to %s failed:' % (address,), exc_info=True) if (self.goal == 'continue'): socket_management.timers_add(self.peer_settings['reconnect_delay'], callback_handler=self.connection_init, parent=self) else: self.send_command(command=self.peer_settings['name'], force_send=True) def connection_handle_sync(self): self.connection_state = 'up' for queue_element in self.out_queue: self.send_command(command=queue_element[0], data=queue_element[1]) self.out_queue = [] self.handlers.event_connection(statechange='up', data=None) def input_process(self, line_raw, command, data): if (type(data) == str): data_split = data.split() else: data_split = data if (command == ''): pass elif (command == 'bot_ping'): self.send_command(command='bot_pong') elif (command == 'bot_nlinked'): if (len(data_split) < 3): self.logger.log(30, 'Received invalid bot_nlinked commandline (too few arguments): %r' % line_raw) else: self.peer_tracking_add(uplink=data_split[1], peername=data_split[0], datastring=data_split[2]) elif (command == 'bot_join'): if (len(data_split) < 5): self.logger.log(30, 'Received invalid bot_join commandline (too few arguments): %r' % line_raw) self.user_tracking_add(peername=data_split[0].lstrip('!'), nick=data_split[1], channel=data_split[2], flagsocket_string=data_split[3], userhost=data_split[4]) elif (command == 'bot_part'): if (len(data_split) < 3): self.logger.log(30, 'Received invalid bot_part commandline (too few arguments): %r' % line_raw) else: self.user_tracking_remove(peername=data_split[0], socket_string=data_split[2]) elif (command == 'bot_idle'): if (len(data_split) < 3): self.logger.log(30, 'Received invalid bot_idle commandline (too few arguments): %r' % line_raw) else: self.user_tracking_adjust(peername=data_split[0], socket_string=data_split[1], change_type='idle', data=data_split[2]) elif (command == 'bot_infoq'): if (len(data_split) < 1): self.logger.log(30, 'Received invalid bot_infoq commandline (too few arguments): %r' % line_raw) else: target = data_split[0] response_string = '%s v%s <%s> [UP %s]' % (self.peer_settings['software'], '.'.join(map(str, self.peer_settings['virtual_eggdrop_version'])), self.peer_settings['network_name'], seconds_hr_relative(seconds=time.time() - self.timestamps['init'], precision=2)) self.send_command(command='bot_priv', data=' '.join((self.peer_settings['name'], target, response_string))) elif (command == 'version'): if (len(data_split) < 2): self.logger.log(30, 'Received invalid version commandline (too few arguments): ' + line_raw) else: try: self.uplink_data['handle_length'] = int(data_split[1]) uplink_version = int(data_split[0]) except ValueError: self.logger.log(30, 'Received invalid version commandline (unable to convert handle_length or version to integer): ' + line_raw) else: self.uplink_data['version'] = self.parse_eggdrop_versionstring(version_string=uplink_version, base64=0) elif (command == 'bot_thisbot'): if ((len(data_split) < 1) or (data_split[0] == '')): self.logger.log(30, 'Received invalid bot_thisbot commandline: ' + line_raw) else: self.uplink_data['name'] = data_split[0] self.network_peers[data_split[0]] = { 'uplink':None, 'version':self.uplink_data['version'], 'users':{} } elif (command == 'bot_handshake'): self.peer_settings['passstring'] = data self.logger.log(45, 'Connection password was not set; negotiated it to be %r. You should probably set this in auth.py.' % (data,)) self.introduce_peer_self() elif (command == 'passreq'): if ((len(data) < 3) or (data[0] != '<') or (data[-1] != '>')): self.logger.log(30, 'Received invalid passreq commandline (non md5-capaple peer ?): ' + line_raw) else: if (type(self.peer_settings['passstring']) == str): self.send_command(command='digest', data=md5.new(data + self.peer_settings['passstring']).hexdigest(), force_send=True) else: self.send_command(command='-') self.introduce_peer_self() elif (command == 'bot_endlink'): self.connection_handle_sync() elif (command == '*hello!'): pass elif ((command == 'bot_zapfbroad') or (command == 'bot_zapf')): pass self.handlers.process_input(parent=self, command=command, data=data) def peer_tracking_add(self, uplink, peername, datastring, base64=1): new_peer = { 'uplink':uplink, 'users':{} } if (len(datastring) > 0): new_peer['sharestatus'] = datastring[0] if (len(datastring) > 1): version_string = datastring[1:] if (base64): new_peer['version'] = self.parse_eggdrop_versionstring(version_string=version_string) else: new_peer['version'] = self.parse_eggdrop_versionstring(base64=base64) self.network_peers[peername] = new_peer def user_tracking_add(self, peername, nick, channel, flagsocket_string, userhost): if (len(flagsocket_string) < 1): self.logger.log(30, 'Invalid flagsocket_string passed to user_tracking_add: %r %r %r %r %r' % (peername, nick, channel, flagsocket_string, userhost)) return 0 user_flag = flagsocket_string[0] try: socket_string = flagsocket_string[1:] user_socket = self.process_numeric(numeric_string=socket_string) except ValueError: self.logger.log(30, 'Invalid flagsocket_string passed to user_tracking_add: %r %r %r %r %r' % (peername, nick, channel, flagsocket_string, userhost)) return 0 try: user_channel = self.process_numeric(numeric_string=channel) except ValueError: self.logger.log(30, 'Invalid channel string passed to user_tracking_add. Other parameters: %r %r %r %r %r' % ( peername, nick, channel, flagsocket_string, userhost)) return 0 if not (peername in self.network_peers): self.logger.log(30, "Bot %r for new user on socket %r doesn't exist." % (peername, user_socket)) return 0 self.network_peers[peername]['users'][user_socket] = { 'nick':nick, 'userhost':userhost, 'channel':user_channel, 'idle':0 } def user_tracking_adjust(self, peername, socket_string, change_type, data): try: socket = self.process_numeric(numeric_string=socket_string) except ValueError: self.logger.log(30, 'Invalid socket string passed to user_tracking_adjust: ' + str(socket_string)) return 1 if not (peername in self.network_peers): self.logger.log(30, 'Unknown peername passed to user_tracking_adjust: ' + peername) return 1 if not (socket in self.network_peers[peername]['users']): self.logger.log(30, 'Unknown socket passed to user_tracking_adjust for peer %r: %r (%r)' % (peername, socket_string, socket)) return 1 if (change_type == 'idle'): try: new_idle_time = self.process_numeric(numeric_string=data) except ValueError: self.logger.log(30, 'Invalid idletime passed to user_tracking_adjust: ' + data) else: self.network_peers[peername]['users'][socket]['idle'] = new_idle_time def user_tracking_remove(self, peername, socket_string): try: socket = self.process_numeric(numeric_string=socket_string) except ValueError: self.logger.log(30, 'Invalid socket string passed to user_tracking_remove: ' + str(socket_string)) return 1 if not (peername in self.network_peers): self.logger.log(30, 'Unknown peername passed to user_tracking_remove: ' + peername) return 1 if not (socket in self.network_peers[peername]['users']): self.logger.log(30, 'Unknown socket %r passed to user_tracking_remove for peer %r.' % (socketstring, peername)) return 1 del(self.network_peers[peername]['users'][socket]) def parse_eggdrop_versionstring(self, version_string, base64=1): if (base64): try: version_string = self.process_numeric(numeric_string=version_string) except ValueError: pass try: version_string = '%08d' % version_string version = (int(version_string[:2]), int(version_string[2:4]), int(version_string[4:6]), int(version_string[6:8])) except ValueError: return (0,0,0,0) else: return version def introduce_peer_self(self): #print tuple(self.peer_settings['virtual_eggdrop_version'][:4]) #print tuple(self.peer_settings['virtual_eggdrop_version'][:4]) + (self.peer_settings['handle_length'], self.peer_settings['software'], self.peer_settings['software_version'], self.peer_settings['network_name']) self.send_command(command='version', data='%.1u%.2u%.2u%.2u %s %s v%s <%s>' % (tuple(self.peer_settings['virtual_eggdrop_version'][:4]) + (self.peer_settings['handle_length'], self.peer_settings['software'], self.peer_settings['software_version'], self.peer_settings['network_name'])), force_send=True) def process_numeric(self, numeric_string): if (not numeric_string.isdigit()): try: output_value = self.convert_base64_decimal(string=numeric_string) except ValueError: pass else: return output_value else: try: output_value = self.convert_base64_decimal(string=numeric_string) except ValueError: try: output_value = int(numeric_string) except ValueError: output_value = 0 return output_value def convert_base64_decimal(self, string): #mapping table taken from eggdrop 1.6.15 src/botcmd.c mapping_table = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 62, 0, 63, 0, 0, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ] return_value = 0 try: factor = 1 while(len(string) > 0): return_value = return_value + mapping_table[ord(string[-1])] * factor string = string[:-1] factor *= 64 except IndexError, ValueError: raise ValueError return return_value def clean_up(self): self.goal = 'halt' socket_management.timers_remove_all(data_type=socket_management.TIMERS_PARENT, data=self) self.connection_base.clean_up() class handlers_botnet_interface(input_handlers.handlers_base): def __init__(self, parent, output, parent_loggername=''): self.parent = parent self.loggername = parent_loggername + '.handlers' self.logger = logging.getLogger(self.loggername) self.send_command = output self.handlers = { 'command':{}, 'bot_message':{}, 'bot_broadcast':{} } def output(self, command=None, data=None, force_send=False): """Send output, if possible. Otherwise, silently discard it.""" if (self.send_command != None): self.send_command(command=command, data=data, force_send=force_send) def event_connection(self, statechange, data): self.handle_event(event_type='statechange_connection', event=statechange, data=data) def process_input(self, parent, command, data): self.handle_event(event_type='command', event=command, data=data) if (type(data) == str): data_split = data.split() if (len(data_split) > 1): source = data_split[0] arguments = data_split[1:] if (command == 'bot_zapf'): if (len(arguments) < 1): self.logger.log(30, 'Invalid bot_zapf commandline: too few arguments (expected at least one string (the target).') else: target = arguments[0] arguments = arguments[1:] self.handle_event(event_type='bot_message', event=arguments[0], data={'source':source, 'target':target, 'arguments':arguments}) elif (command == 'bot_zapfbroad'): self.handle_event(event_type='bot_broadcast', event=arguments[0], data={'source':source, 'arguments':arguments})