Apr 29, 2011

A simple botnet written in Python

If you want to see all detail of this howto, please go to the Source.

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.
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 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(Please go to the Source.). 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.

Source: http://charlesleifer.com/blog/simple-botnet-written-python/

No comments: