A simple botnet written in Python
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.
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. 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
- 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
- You can check out the source code on GitHub
- Information on running the botnet can be found in the README
- IRC protocol, RFC 1459
If you're interested in more projects like this, check out the saturday-morning hack posts.
Comments (2)
Jason | apr 25 2011, at 09:10pm
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.
Commenting has been closed.
Charles | apr 25 2011, at 09:39pm
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:
after that just follow the instructions in the project's README