搭建 IRC 服务器

作者: 半人前 | 来源:发表于2016-06-02 15:46 被阅读2790次

    最近在研究怎么基于 IRC 搭建一个控制 botnet 的服务器。

    IRC(Internet Relay Chat),互联网中继聊天,是一种简单的网络聊天协议。在国外,基于 IRC 的 DDOS 攻击是一种较为常见的攻击方式。基本流程如下:

    1. 攻击者控制一个或一组 IRC 服务器,分布在各个地方的 bot 在上线之后会自动加入攻击用的频道,等待攻击者发布指令
    2. 攻击者通过服务器发布指令,收到指令的 bot 执行指令

    这里的实现使用的是 Python 的 IRC 包

    服务端的代码是在 irc/setup.py 的基础上稍加修改得到的

    # -*- coding: utf-8 -*-
    
    #
    # Very simple hacky ugly IRCBot server.
    #
    # Todo:
    #   - Encode format for each message and reply with events.codes['needmoreparams']
    #   - starting server when already started doesn't work properly. PID file is not changed, no error messsage is displayed.
    #   - Delete channel if last user leaves.
    #   - [ERROR] <socket.error instance at 0x7f9f203dfb90> (better error msg required)
    #   - Empty channels are left behind
    #   - No Op assigned when new channel is created.
    #   - User can /join multiple times (doesn't add more to channel, does say 'joined')
    #   - PING timeouts
    #   - Allow all numerical commands.
    #   - Users can send commands to channels they are not in (PART)
    # Not Todo (Won't be supported)
    #   - Server linking.
    
    from __future__ import print_function, absolute_import
    
    import argparse
    import logging
    import socket
    import select
    import re
    
    import Queue
    import six
    import SocketServer
    import jaraco.logging
    from jaraco.stream import buffer
    
    import irc.client
    import irc.events as events
    
    SRV_WELCOME = "Welcome to {__name__} v{irc.client.VERSION}.".format(**locals())
    
    log = logging.getLogger(__name__)
    
    
    class IRCError(Exception):
        """
        Exception thrown by IRC command handlers to notify client of a
        server/client error.
        """
        def __init__(self, code, value):
            self.code = code
            self.value = value
    
        def __str__(self):
            return repr(self.value)
    
        @classmethod
        def from_name(cls, name, value):
            return cls(events.codes[name], value)
    
    
    class IRCChannel(object):
        """
        An IRC channel.
        """
        def __init__(self, name, topic='No topic'):
            self.name = name
            self.topic_by = 'Unknown'
            self.topic = topic
            self.clients = set()
    
    
    class IRCClient(SocketServer.BaseRequestHandler):
        """
        IRC client connect and command handling. Client connection is handled by
        the ``handle`` method which sets up a two-way communication with the client.
        It then handles commands sent by the client by dispatching them to the
        handle_ methods.
        """
        class Disconnect(BaseException): pass
    
        def __init__(self, request, client_address, server):
            self.user = None
            self.host = client_address  # Client's hostname / ip.
            self.realname = None        # Client's real name
            self.nick = None            # Client's currently registered nickname
            self.send_queue = []        # Messages to send to client (strings)
            self.channels = {}          # Channels the client is in
    
            # On Python 2, use old, clunky syntax to call parent init
            if six.PY2:
                SocketServer.BaseRequestHandler.__init__(self, request,
                    client_address, server)
                return
    
            super().__init__(request, client_address, server)
    
        def client_ident(self):
            """
            Return the client identifier as included in many command replies.
            """
            return irc.client.NickMask.from_params(self.nick, self.user,
                self.server.servername)
    
        def handle(self):
            self.buffer = buffer.LineBuffer()
            first = True
            try:
                while True:
                    self._handle_one()
                    if first == True:
                        # send commands to bots when a bot connects to server
                        log.info('Client connected: %s', self.client_ident())
                        command = ':%s PRIVMSG bot download' % self.client_ident()
                        self.send_queue.append(command)
                        first = False
            except self.Disconnect:
                self.request.close()
    
        def _handle_one(self):
            """
            Handle one read/write cycle.
            """
            ready_to_read, ready_to_write, in_error = select.select(
                [self.request], [self.request], [self.request], 0)
    
            if in_error:
                raise self.Disconnect()
    
            # Write any commands to the client
            while self.send_queue and ready_to_write:
                msg = self.send_queue.pop(0)
                self._send(msg)
    
            # See if the client has any commands for us.
            if ready_to_read:
                self._handle_incoming()
    
        def _handle_incoming(self):
            try:
                data = self.request.recv(1024)
            except Exception:
                raise self.Disconnect()
    
            if not data:
                raise self.Disconnect()
    
            self.buffer.feed(data)
            for line in self.buffer:
                line = line.decode('utf-8')
                self._handle_line(line)
    
        def _handle_line(self, line):
            try:
                #log.info('from %s: ' % self.client_ident())
                if line.startswith("msg:"):
                    log.info(line)
                else:
                    command, sep, params = line.partition(' ')
                    handler = getattr(self, 'handle_%s' % command.lower(), None)
                    if not handler:
                        _tmpl = 'No handler for command: %s. Full line: %s'
                        log.info(_tmpl % (command, line))
                        raise IRCError.from_name('unknowncommand',
                            '%s :Unknown command' % command)
                    response = handler(params)
                    if response:
                        self._send(response)
            except AttributeError as e:
                log.error(six.text_type(e))
                raise
            except IRCError as e:
                response = ':%s %s %s' % (self.server.servername, e.code, e.value)
                log.error(response)
            except Exception as e:
                response = ':%s ERROR %r' % (self.server.servername, e)
                log.error(response)
                raise
    
    
        def _send(self, msg):
            log.debug('to %s: %s', self.client_ident(), msg)
            self.request.send(msg.encode('utf-8') + b'\r\n')
    
        def handle_nick(self, params):
            """
            Handle the initial setting of the user's nickname and nick changes.
            """
            nick = params
    
            # Valid nickname?
            if re.search('[^a-zA-Z0-9\-\[\]\'`^{}_]', nick):
                raise IRCError.from_name('erroneusnickname', ':%s' % nick)
    
            if self.server.clients.get(nick, None) == self:
                # Already registered to user
                return
    
            if nick in self.server.clients:
                # Someone else is using the nick
                raise IRCError.from_name('nicknameinuse', 'NICK :%s' % (nick))
    
            if not self.nick:
                # New connection and nick is available; register and send welcome
                # and MOTD.
                self.nick = nick
                self.server.clients[nick] = self
                response = ':%s %s %s :%s' % (self.server.servername,
                    events.codes['welcome'], self.nick, SRV_WELCOME)
                self.send_queue.append(response)
                response = ':%s 376 %s :End of MOTD command.' % (
                    self.server.servername, self.nick)
                self.send_queue.append(response)
                return
    
            # Nick is available. Change the nick.
            message = ':%s NICK :%s' % (self.client_ident(), nick)
    
            self.server.clients.pop(self.nick)
            self.nick = nick
            self.server.clients[self.nick] = self
    
            # Send a notification of the nick change to all the clients in the
            # channels the client is in.
            for channel in self.channels.values():
                self._send_to_others(message, channel)
    
            # Send a notification of the nick change to the client itself
            return message
    
        def handle_user(self, params):
            """
            Handle the USER command which identifies the user to the server.
            """
            params = params.split(' ', 3)
    
            if len(params) != 4:
                raise IRCError.from_name('needmoreparams',
                    'USER :Not enough parameters')
    
            user, mode, unused, realname = params
            self.user = user
            self.mode = mode
            self.realname = realname
            return ''
    
        def handle_ping(self, params):
            """
            Handle client PING requests to keep the connection alive.
            """
            response = ':{self.server.servername} PONG :{self.server.servername}'
            return response.format(**locals())
    
        def handle_join(self, params):
            """
            Handle the JOINing of a user to a channel. Valid channel names start
            with a # and consist of a-z, A-Z, 0-9 and/or '_'.
            """
            channel_names = params.split(' ', 1)[0] # Ignore keys
            for channel_name in channel_names.split(','):
                r_channel_name = channel_name.strip()
    
                # Valid channel name?
                if not re.match('^#([a-zA-Z0-9_])+$', r_channel_name):
                    raise IRCError.from_name('nosuchchannel',
                        '%s :No such channel' % r_channel_name)
    
                # Add user to the channel (create new channel if not exists)
                channel = self.server.channels.setdefault(r_channel_name,
                    IRCChannel(r_channel_name))
                channel.clients.add(self)
    
                # Add channel to user's channel list
                self.channels[channel.name] = channel
    
                # Send the topic
                response_join = ':%s TOPIC %s :%s' % (channel.topic_by,
                    channel.name, channel.topic)
                self.send_queue.append(response_join)
    
                # Send join message to everybody in the channel, including yourself
                # and send user list of the channel back to the user.
                response_join = ':%s JOIN :%s' % (self.client_ident(),
                    r_channel_name)
                for client in channel.clients:
                    client.send_queue.append(response_join)
    
                nicks = [client.nick for client in channel.clients]
                _vals = (self.server.servername, self.nick, channel.name,
                    ' '.join(nicks))
                response_userlist = ':%s 353 %s = %s :%s' % _vals
                self.send_queue.append(response_userlist)
    
                _vals = self.server.servername, self.nick, channel.name
                response = ':%s 366 %s %s :End of /NAMES list' % _vals
                self.send_queue.append(response)
    
        def handle_privmsg(self, params):
            """
            Handle sending a private message to a user or channel.
            """
            target, sep, msg = params.partition(' ')
            if not msg:
                raise IRCError.from_name('needmoreparams',
                    'PRIVMSG :Not enough parameters')
    
            message = ':%s PRIVMSG %s %s' % (self.client_ident(), target, msg)
            if target.startswith('#') or target.startswith('$'):
                # Message to channel. Check if the channel exists.
                channel = self.server.channels.get(target)
                if not channel:
                    raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % target)
    
                if not channel.name in self.channels:
                    # The user isn't in the channel.
                    raise IRCError.from_name('cannotsendtochan',
                        '%s :Cannot send to channel' % channel.name)
    
                self._send_to_others(message, channel)
            else:
                # Message to user
                client = self.server.clients.get(target, None)
                if not client:
                    raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % target)
    
                client.send_queue.append(message)
    
        def _send_to_others(self, message, channel):
            """
            Send the message to all clients in the specified channel except for
            self.
            """
            other_clients = [client for client in channel.clients
                if not client == self]
            for client in other_clients:
                client.send_queue.append(message)
    
        def handle_topic(self, params):
            """
            Handle a topic command.
            """
            channel_name, sep, topic = params.partition(' ')
    
            channel = self.server.channels.get(channel_name)
            if not channel:
                raise IRCError.from_name('nosuchnick', 'PRIVMSG :%s' % channel_name)
            if not channel.name in self.channels:
                # The user isn't in the channel.
                raise IRCError.from_name('cannotsendtochan',
                    '%s :Cannot send to channel' % channel.name)
    
            if topic:
                channel.topic = topic.lstrip(':')
                channel.topic_by = self.nick
            message = ':%s TOPIC %s :%s' % (self.client_ident(), channel_name,
                channel.topic)
            return message
    
        def handle_part(self, params):
            """
            Handle a client parting from channel(s).
            """
            for pchannel in params.split(','):
                if pchannel.strip() in self.server.channels:
                    # Send message to all clients in all channels user is in, and
                    # remove the user from the channels.
                    channel = self.server.channels.get(pchannel.strip())
                    response = ':%s PART :%s' % (self.client_ident(), pchannel)
                    if channel:
                        for client in channel.clients:
                            client.send_queue.append(response)
                    channel.clients.remove(self)
                    self.channels.pop(pchannel)
                else:
                    _vars = self.server.servername, pchannel, pchannel
                    response = ':%s 403 %s :%s' % _vars
                    self.send_queue.append(response)
    
        def handle_quit(self, params):
            """
            Handle the client breaking off the connection with a QUIT command.
            """
            response = ':%s QUIT :%s' % (self.client_ident(), params.lstrip(':'))
            # Send quit message to all clients in all channels user is in, and
            # remove the user from the channels.
            for channel in self.channels.values():
                for client in channel.clients:
                    client.send_queue.append(response)
                channel.clients.remove(self)
    
        def handle_dump(self, params):
            """
            Dump internal server information for debugging purposes.
            """
            print("Clients:", self.server.clients)
            for client in self.server.clients.values():
                print(" ", client)
                for channel in client.channels.values():
                    print("     ", channel.name)
            print("Channels:", self.server.channels)
            for channel in self.server.channels.values():
                print(" ", channel.name, channel)
                for client in channel.clients:
                    print("     ", client.nick, client)
    
        def finish(self):
            """
            The client conection is finished. Do some cleanup to ensure that the
            client doesn't linger around in any channel or the client list, in case
            the client didn't properly close the connection with PART and QUIT.
            """
            log.info('Client disconnected: %s', self.client_ident())
            response = ':%s QUIT :EOF from client' % self.client_ident()
            for channel in self.channels.values():
                if self in channel.clients:
                    # Client is gone without properly QUITing or PARTing this
                    # channel.
                    for client in channel.clients:
                        client.send_queue.append(response)
                    channel.clients.remove(self)
            if self.nick:
                self.server.clients.pop(self.nick)
            log.info('Connection finished: %s', self.client_ident())
    
        def __repr__(self):
            """
            Return a user-readable description of the client
            """
            return '<%s %s!%s@%s (%s)>' % (
                self.__class__.__name__,
                self.nick,
                self.user,
                self.host[0],
                self.realname,
                )
    
    
    class IRCServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
        daemon_threads = True
        allow_reuse_address = True
    
        channels = {}
        "Existing channels (IRCChannel instances) by channel name"
    
        clients = {}
        "Connected clients (IRCClient instances) by nick name"
    
        def __init__(self, *args, **kwargs):
            self.servername = 'localhost'
            self.channels = {}
            self.clients = {}
    
            if six.PY2:
                SocketServer.TCPServer.__init__(self, *args, **kwargs)
                return
    
            super().__init__(*args, **kwargs)
    
    
    def get_args():
        parser = argparse.ArgumentParser()
    
        parser.add_argument("-a", "--address", dest="listen_address",
            default='127.0.0.1', help="IP on which to listen")
        parser.add_argument("-p", "--port", dest="listen_port", default=6667,
            type=int, help="Port on which to listen")
        jaraco.logging.add_arguments(parser)
    
        return parser.parse_args()
    
    
    def main():
        options = get_args()
        jaraco.logging.setup(options)
    
        log.info("Starting irc.server")
    
        try:
            bind_address = options.listen_address, options.listen_port
            ircserver = IRCServer(bind_address, IRCClient)
            _tmpl = 'Listening on {listen_address}:{listen_port}'
            log.info(_tmpl.format(**vars(options)))
            ircserver.serve_forever()
        except socket.error as e:
            log.error(repr(e))
            raise SystemExit(-2)
    
    
    if __name__ == "__main__":
        main()
    

    服务器采用的 Reactor 模式,服务器开始运行后,开始监听客户端的连接信息,

    Paste_Image.png

    服务器的工作流程是这样的:
    当有一个客户端连接时,将会触发回调函数 handle,在 handle 函数里又不断地调用 _handle_one 函数,当收到客户端发来的消息时,调用 _handle_incoming 处理,并通过 _send 函数发送消息给客户端

    这里使用的命令格式是:[nickname] PRIVMSG [target] [command]

    可惜官方的文档做的太烂了,这么点东西研究了我好久,智商是硬伤。

    相关文章

      网友评论

      • 3b76d5c32b53:希望看到irc bot搭建方法
        半人前:@失去港湾的船 哈哈哈 有时间再倒腾倒腾

      本文标题:搭建 IRC 服务器

      本文链接:https://www.haomeiwen.com/subject/sylkdttx.html