As of this week we instituted a regular "hackday" at my office -- anything goes, you can work on whatever you like, so at 11:30 the night before the hackday started I decided on writing a simple IRC-powered botnet. The wikipedia definition for a botnet is a little poorly-worded, so I've paraphrased it:

A botnet is a collection of infected computers or bots that have been taken over by hackers (also known as bot herders) and are used to perform malicious tasks or functions. A botnet performs actions from the client computer while the operator controls it remotely via IRC.

What I wrote really only matches the second half of this definition, as I didn't attempt to write code that "infects" machines or attempt to replicate itself. My project simply allows you to control an arbitrary number of machines via simple IRC commands. The remote-workers communicate with and report back to the control program via an IRC backchannel, while the operator of the Botnet issues commands directly to the control program.

Botnet Diagram

How it works

It's not very complicated! I was already familiar with some of the rudiments of the IRC protocol from hacking on a simple IRC bot library. The parts that I needed to figure out were:

  • ability to track when workers came on/off-line so they could be sent jobs
  • easily pass data from operator -> workers and back again

Worker registration

The diagram below shows the process or registration that happens when a worker comes online. Workers must know beforehand the nick of the command program (or have a way of finding it out) -- they then send a private message to the command program indicating their presence. The command program acknowledges this, adds the worker's nick to the registry of available workers, and sends the worker the location of the command channel. The worker then joins the channel and is able to start executing tasks from the operator.

Worker registration

In the event a worker comes online and cannot reach the command program, it will keep trying every 30 seconds until it receives an acknowledgement. Additionally, every two minutes the command program pings the workers, removing any dead ones from the list.

Task execution

Tasks are initially parsed by the command program and then dispatched to workers via the command channel. The operator can specify any number of workers to set to work on a specific task. The syntax is straightforward:

!execute (<number of workers>) <command> <arguments>

Below is a diagram of the basic workflow:

Worker task execution

Worker tasks are parsed by the worker bot and can accept any number of arbitrary arguments, which are extracted by an operator-defined regex. Here's an example of how the "run" command looks (which executes a command on the host machine):

def get_task_patterns(self):
    return (
        ('run (?P<program>.*)', self.run),
        # ... any other command patterns ...
    )

def run(self, program):
    fh = os.popen(program)
    return fh.read()

Dead simple! Tasks can return any arbitrary text which is then parsed by the worker's task runner and sent back to the command program. At any time, the operator can request the data for a given task.

A note on security

The operator must authenticate with the command program to issue commands - the password is hardcoded in the BotnetBot. Likewise, workers will only accept commands from the command program.

Example session

Below is a sample session. First step is to authenticate with the bot:

<cleifer> !auth password
<boss1337> Success

<cleifer> !status
<boss1337> 2 workers available
<boss1337> 0 tasks have been scheduled

Execute a command on one of the workers:

<cleifer> !execute 1 run vmstat
<boss1337> Scheduled task: "run vmstat" with id 1 [1 workers]
<boss1337> Task 1 completed by 1 workers

Print the data returned by the last executed command:

<cleifer> !print
<boss1337> [workerbot:{alpha}] - run vmstat
<boss1337> procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
<boss1337> r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa
<boss1337> 0  0      0 352900 583696 1298868    0    0    16    31  133  172  4  2 94  0

Find open ports on the workers hosts:

<cleifer> !execute ports
<boss1337> Scheduled task: "ports" with id 2 [2 workers]
<boss1337> Task 2 completed by 2 workers
<cleifer> !print
<boss1337> [workerbot:{alpha}] - ports
<boss1337> [22, 80, 631]
<boss1337> [workerbot_346:{rho}] - ports
<boss1337> [22, 80]

Becoming a bot herder

If you'd like to try this out yourself, feel free to grab a checkout of the source, available on GitHub. The worker is programmed with the following commands:

  • run executes the given program
  • download will download the file at the given url and save it to the host machine
  • info returns information about the host machine's operating system
  • ports does a quick port-scan of the system ports 20-1025
  • send_file streams the file on the host computer to the given host:port
  • status returns the size of the worker's task queue

Adding your own commands is really easy, though -- just add them to the tuple returned by the get_task_patterns method, which looks like this:

def get_task_patterns(self):
    return (
        ('download (?P<url>.*)', self.download),
        ('info', self.info),
        ('ports', self.ports),
        ('run (?P<program>.*)', self.run),
        ('send_file (?P<filename>[^\s]+) (?P<destination>[^\s]+)', self.send_file),
        ('status', self.status_report),

        # adding another command - this will return the system time and optionally
        # take a format parameter
        ('get_time(?: (?P<format>.+))?', self.get_time),
    )

Now define your callback, which will perform whatever task you like and optionally return a string. The returned data will be sent to the command program and made available to the operator.

def get_time(self, format=None):
    now = datetime.datetime.now() # remember to import datetime at the top of the module
    if format:
        return now.strftime(format)
    return str(now)

Here's how you might call that command:

<cleifer> !execute get_time
<boss1337> Scheduled task: "get_time" with id 1 [1 workers]
<boss1337> Task 1 completed by 1 workers
<cleifer> !print 1
<boss1337> [workerbot:{alpha}] - get_time
<boss1337> 2011-04-21 10:41:16.251871
<cleifer> !execute get_time %H:%M
<boss1337> Scheduled task: "get_time %H:%M" with id 2 [1 workers]
<boss1337> Task 2 completed by 1 workers
<cleifer> !print
<boss1337> [workerbot:{alpha}] - get_time %H:%M
<boss1337> 10:42

The bots are extensible so you can write your own commands if you want to take up bot-herding -- this tool could be used to restart web nodes, update checkouts, report on status, anything really since it can be used to execute arbitrary commands.

Happy hacking!

Links

Comments (2)

  • Jason | April 2011, at 21:10

    I am having trouble getting started with this code.. I think it looks really interesting but I can't install gevent correctly and it appears as though your software requires that.

    A step by step setup guide would be much appreciated.


  • Charles | April 2011, at 21:39

    Jason -- I just added a setup.py script to the irc library so you should be able to "pip install" both the irc library and gevent. Try the following:

    pip install irckit
    pip install gevent
    

    after that just follow the instructions in the project's README


Commenting has been closed, but please feel free to contact me