HackFu 2015 - Badge Loyalty System

First up I'd like to thank MWR for the chance to come and play in their CTF event, and Alek for making the badges which I had so much fun messing with (when I find I've gotten out of bed at 5am after 4 hours of sleep to write python I know someone did something awesome right?)

Secondly I think I need to mention that for a challenge based in a hardware device I'm using surprisingly little actual hardware hacking here. I know that other teams were dumping firmware and re-writing it with additional features and security which isn't the way I went with my work on this. I'll mention some of the things I heard of other people doing as we go through just to show how much there was to explore in this small aspect of HackFu, but I don't want people thinking that was my work.

As some background HackFu was prison themed this year, and each participant was given a prisoner ID, a bank account, a fictional back story, and a prison uniform (orange brings out my pallid completion really nicely :-P )
So what was the challenge, well actually there were at least two roles that the badges played in the HackFu:

1) Each badge contained a list of that players attributes, Skills, Convictions, Addictions and Tattoos
2) Each badge generated loyalty when in range of a base station which was both a points system and a currency which could be spent on items in the shop to help with challenges.

Teams were also provided with a pair of SRF-Sticks (http://shop.ciseco.co.uk/srf-stick-868-915-mhz-easy-to-use-usb-radio/) which provide a serial interface to read write data to the radio network that the badges were using.

The first thing to do was to was take a look and see what was going on out there, so plug that bad boy in and hit cat /dev/ttyACM0

  �ping�B    �ping�B    �ping�B
�ping��
��!��
         �ping��
                     �ping��
                                 �ping��
                                             �ping��
                                                         �ping�T
                                                                     �ping�T
 �ping�T
�ping�ping�i�ping�i�ping�i�ping�i�ping��ping��ping��ping��ping:
                                 �ping:
                                              �ping:
                                                           �ping:

Well that looks like something that shouldn't be too hard to follow along with, seems some kind of ping response framework.

What you can't see from this still shot is that the pings are coming over at a pretty constant rate, so it's clearly packets of info in transit so time to look at some hex and try pulling it apart and see what kind of data we're getting out of this.

Hexdump is a simple place to start, but I think I prefer doing this kinda thing with python because once I've got a sensible way to split out the data in python I'm left with some good code to test on future captures and as a basis for other code I might need to write down the line.

While I'm writing and playing with this I've got a dump going on to file so I've got plenty of data to test this on later.

cat /dev/ttyACM0 | tee data.dmp

So lets look at some of what's coming back and see what it's starting to feel like:

src = file('data.dmp')
raw = src.read()
raw[:100]
'\x10\x02\t\x02\xc7\x00ping\x00\x10\x03\x90B\x10\x02\t\x02\xc7\x00ping\x00\x10\x03\x90B\x10\x02\t\x02\xc7\x00ping\x00\x10\x03\x90B\x10\x02\n\x02\xc8\x00ping\x00\x10\x03\xf8\xd9\x10\x02\x02\n\xc8\x80!\x10\x03\xaf\xee\x10\x02\x0b\x02\xc9\x00ping\x00\x10\x03\xe0\xb5\x10\x02\x0b\x02\xc9\x00ping\x00\x10\x03\xe0'

So working with small sections of the file to start with you can see that the text we saw before ping is surrounded by null bytes, let's treat those as separators and see where that gets us.

lines = raw.split("\x00")
for s in lines[:20]:
    s

'\x10\x02\t\x02\xc7'
'ping'
'\x10\x03\x90B\x10\x02\t\x02\xc7'
'ping'
'\x10\x03\x90B\x10\x02\t\x02\xc7'
'ping'
'\x10\x03\x90B\x10\x02\n\x02\xc8'
'ping'
'\x10\x03\xf8\xd9\x10\x02\x02\n\xc8\x80!\x10\x03\xaf\xee\x10\x02\x0b\x02\xc9'
'ping'
'\x10\x03\xe0\xb5\x10\x02\x0b\x02\xc9'
'ping'
'\x10\x03\xe0\xb5\x10\x02\x0b\x02\xc9'
'ping'
'\x10\x03\xe0\xb5\x10\x02\x0b\x02\xc9'
'ping'
'\x10\x03\xe0\xb5\x10\x02\x0c\x02\xca'
'ping'
'\x10\x03\x96T\x10\x02\x0c\x02\xca'
'ping'

OK, so let's ignore the pings and make the rest a little easier to look at as just hex.

msgs = []

for s in lines:
    if s != "ping":
        msgs.append(''.join([hex(ord(v))[2:].rjust(2,'0')+' ' for v in s]))

for s in msgs[:20]:
    s

'10 02 09 02 c7 '
'10 03 90 42 10 02 09 02 c7 '
'10 03 90 42 10 02 09 02 c7 '
'10 03 90 42 10 02 0a 02 c8 '
'10 03 f8 d9 10 02 02 0a c8 80 21 10 03 af ee 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0c 02 ca '
'10 03 96 54 10 02 0c 02 ca '
'10 03 96 54 10 02 0c 02 ca '
'10 03 96 54 10 02 0c 02 ca '
'10 03 96 54 10 02 0d 02 cb '
'10 03 8e 38 10 02 0d 02 cb '
'10 03 8e 38 10 02 0d 02 cb '
'10 03 8e 38 10 02 0e 02 cc '
'10 03 99 69 10 02 0e 02 cc '
'10 03 99 69 10 02 0e 02 cc '
'10 03 99 69 10 02 0e 02 cc '
'10 03 99 69 10 02 0f 02 cd '

This looks like the message format is 10 03 WW XX 10 02 YY 02 ZZ where W-Z are various data items, though there's clearly a subset of longer ones.

YY and ZZ are clearly counting up, but WW XX I've got no idea at this point, lets look at some packet lengths and see what we can find

[len(x) for x in lines if x != "ping"]
[5, 9, 9, 9, 20, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 10, 10, 10, 10, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 2, 6, 2, 6, 2, 6, 2, 6, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 20, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 8, 0, 8, 0, 8, 0, 9, 9 .....

so I'm seeing lots of 9's, so that's probably our target length, there are a few shorter ones but they seem to be always in pairs which add up to 8 (2+6, 0+8) which probably means that they are packets which contained a 0x00 which we've split on when we shouldn't, there are also some 10 length ones too, let's take a look at one of those and see what it looks like:

'10 03 81 05 10 02 0f 02 cd '
'10 03 81 05 10 02 0f 02 cd '
'10 03 81 05 10 02 10 10 02 ce '
'10 03 3a 0b 10 02 10 10 02 ce '
'10 03 3a 0b 10 02 10 10 02 ce '
'10 03 3a 0b 10 02 10 10 02 ce '
'10 03 3a 0b 10 02 11 02 cf '
'10 03 22 67 10 02 11 02 cf '

OK, so that looks like 10 is being escaped by doubling it 0f 02 cd should be followed by 10 02 cd but instead is followed by 10 10 02 cd, this looks to me like 10 is our separator not 00 so we probably want to change our decoder a little.

Before we do that let's look at some of the longer ones too and see what they have to tell us:

'10 03 90 42 10 02 09 02 c7 '
'10 03 90 42 10 02 0a 02 c8 '
'10 03 f8 d9 10 02 02 0a c8 80 21 10 03 af ee 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0b 02 c9 '
'10 03 e0 b5 10 02 0c 02 ca '
...
'10 03 63 bb 10 02 3a 02 f8 '
'10 03 63 bb 10 02 3b 02 f9 '
'10 03 7b d7 10 02 02 3b f9 80 21 10 03 b3 80 10 02 3c 02 fa '
'10 03 0d 36 10 02 3c 02 fa '
'10 03 0d 36 10 02 3c 02 fa '
'10 03 0d 36 10 02 3c 02 fa '
'10 03 0d 36 10 02 3d 02 fb '
...
'10 03 1a b7 10 02 6b 02 29 '
'10 03 1a b7 10 02 6c 02 2a '
'10 03 6c 56 10 02 02 6c 2a 80 21 10 03 0d cb 10 02 6d 02 2b '
'10 03 74 3a 10 02 6d 02 2b '
'10 03 74 3a 10 02 6e 02 2c '
'10 03 63 6b 10 02 6e 02 2c '

So at first glance the longer lines look to have YY and the following 02 swapped around and then another section added on the end.

Now remember we're actually listening to radio here so we're actually seeing a number of things going on in the channel, and there's going to be more than one person talking, and relevant to this task there's also going to be responses to some of these pings.

One other thing to note the end of the longer lines actually seems to be duplicated on the lines below, so maybe our boundaries are wrong, after watching the raw data coming through it becomes clear that the text ping is actually in the middle of the packets not at the end, so let's make some changes to our splitting code and see what happens:

msgs = []
last = 0
while True:
    if not sep in raw[last + 1:]: break
    i = raw.index(sep, last + 1)
    while raw[i + 1] == '\x10':
        if not sep in raw[i + 2:]: break
        i = raw.index(sep, i + 2)
    msgs.append(''.join([hex(ord(v))[2:].rjust(2,'0')+' ' for v in raw[last:i]]))
    last = i

So this gives us a nice clean structure, it seems that every 10 02 is followed by a 10 03 packet, and the same 10 02 packet results in the same 10 03 packet.

Given we think we've got a structure here, and given that we've got a badge that will receive these things, let's have a look and see if we can send one that our badge will respond to...

The badges have a few screens, the important one here is the maintenance screen, it's got an ID, a time since last ping, as well as "Good Rx" and "Bad Rx" counters.

My badge lists an ID number of 143, so let's take one of these ping packets, and substitute in my 143 (0x8f) for each of the counters and see what it does.

python -c 'print "\x10\x02\x09\x02\xc7\x00ping\x00\x10\x03\x90\x42"' > /dev/ttyACM0 

Results in no change.

python -c 'print "\x10\x02\x09\x02\x8f\x00ping\x00\x10\x03\x90\x42"' > /dev/ttyACM0 

This results in adding one to by Bad Rx counter, so clearly my first try for addressing it has worked, but it's bad in some way.

I search through my logged packets looking for one with my address in and I find these:

for i in xrange(len(msgs)):
    if '8f 00' in msgs[i]:
        msgs[i:i+2]

['10 02 5b 02 8f 00 70 69 6e 67 00 ', '10 03 1e 89 ']
['10 02 5b 02 8f 00 70 69 6e 67 00 ', '10 03 1e 89 ']
['10 02 5b 02 8f 00 70 69 6e 67 00 ', '10 03 1e 89 ']
['10 02 5b 02 8f 00 70 69 6e 67 00 ', '10 03 1e 89 ']
['10 02 d1 02 8f 00 70 69 6e 67 00 ', '10 03 9b 1a ']
['10 02 d1 02 8f 00 70 69 6e 67 00 ', '10 03 9b 1a ']
['10 02 d1 02 8f 00 70 69 6e 67 00 ', '10 03 9b 1a ']
['10 02 d1 02 8f 00 70 69 6e 67 00 ', '10 03 9b 1a ']

sending these didn't cause Bad Rx message, though any change to the first half did, and swapping the second halves caused the errors too, someone mentioned that in the badge briefing (which I didn't attend) there had been mention of CRC's so then I spent some time trying to work out the CRC algorithm in use.

I found this page which had a bunch of standard CRC implementations (http://www.lammertbies.nl/comm/info/crc-calculation.html) which didn't get me anywhere but could have worked so it's worth remembering for next time I see a CRC, and I spent some time reading stuff about how to brute force work out what CRC mechanism is in use, which is probably best kept for a whole article on it's own if I ever get a reason to pursue it again.

While I was working through this material, one of my team mates had been attacking the badge directly. Using the serial interface on the badge asked for a 5 digit pin, which we'd brute forced to get us a URL to download some documentation on the badge.

enter 5-digit pin number.
No need for CR or LF:

The documentation contained a lua file for wireshark, as well as a script for recording the traffic into wireshark for analysis and a pdf file detailing how to set it up.

All very handy (and showed us there was a bunch of other types of messages that the protocol supported not just ping) but unfortunately it didn't include the actual CRC implementation being use, just a notation that it was indeed a CRC field.

After spending some time on some other tasks, we heard rumour that there was more information in the files we'd been given than we'd found, and a little closer examination showed that there was a .gitignore file a which wasn't what it claimed to be, it's a python file!

Inside this was a utility for pinging a badge, it was windows based and didn't work natively for me on the Kali laptop I was using. One of my team mates Adam who was using a Windows laptop (A loaner since he's between laptops himself at the moment) tested it and jumped into extending it to implement some other the other interesting messages we'd seen in the lua file.

There was a bunch of things you could do with the badges over the radio links, one of which was CHPID which changed the colour of a badge which other teams were already exploiting (hey how did I end up on green team!) and the others were mostly interrogating the information held on the badges which were relevant to other parts of HackFu.

I spend a little time working out the CRC code and copied what we found and turned it into this which I could use to confirm the CRC, and indeed calculate my own.

class crcGen(object):

  def lo8(self, x):
    return x & 0xff

  def hi8(self, x):
    return (x >> 8)

  def crcByte(self, crc, data):
    data = self.lo8(crc) ^ data
    data = self.lo8(data << 4) ^ data

    return ((data << 8) | self.hi8(crc)) ^ ((data >> 4) ^ (data << 3))

crc_init = 65535

def docrc(x):
        g = crcGen()
        crc = crc_init
        for i in x:
                crc = g.crcByte(crc, ord(i))
    return chr(g.hi8(crc)) + chr(g.lo8(crc))

so with applying that I could now generate a CRCs to ping my badge:

docrc("\x5b\x02\x8f\x00ping\x00\x10\x03")
'\x1e\x89'
docrc("\xd1\x02\x8f\x00ping\x00\x10\x03")
'\x9b\x1a'

and a couple of quick tests worked just fine too! yay!

python -c 'print "\x10\x02\x01\x02\x8f\x00ping\x00\x10\x03\xb5\x41"' > /dev/ttyACM0
python -c 'print "\x10\x02\x02\x02\x8f\x00ping\x00\x10\x03\x4b\xf2"' > /dev/ttyACM0
python -c 'print "\x10\x02\x03\x02\x8f\x00ping\x00\x10\x03\x1e\x63"' > /dev/ttyACM0

So now i can ping things on command so what can I do to actually get some more points.

So a quick recap of what we've found, each team/colour is on a separate PANID (which were helpfully listed in the lua file as:

A3BB    RED
29FF    BLUE
954A    YELLOW
DDE3    GREEN
6B45    PURPLE

And watching the radio network the base station is cycling through each PANID(color), and then pinging a range of devices from 01 to FF on that PANID

Think of this as it asking "Badge Yellow 1 are you there?" and waiting for a reply and if not then moving on to "Badge Yellow 2 are you there?" etc etc.

Each badge has a color and a number assigned, from the maintenance screen mine is 954A-143, so this is how people are benefiting from changing people's badge colours (by using the CHPID radio command we've now seen above) making other peoples badges answer in the affirmative for their colour badge

So the immediate question to my mind was, well what's stopping me responding to all ping requests for yellow badges, that way rather than the 20 or so on our team plus or minus any that have been converted from or to other teams, could we not just have 255?

Let's look at the thing we captured before that looked like a response and see if we can work out the format:

# Ping
['10 02 0a 02 c8 00 70 69 6e 67 00 ', '10 03 f8 d9 ']
# Response
['10 02 02 0a c8 80 21 ', '10 03 af ee ']

From this and the information we got from the lua file we can see the correct way to respond to pings, so let's write a program which listens to the radio and does the responses depending on input.

#!/usr/bin/python

import serial
import time

counter = 0

state = 0
s = ''
src = serial.Serial('/dev/ttyACM0', 9600, timeout=2)
dst = src
while True:
    last = s
    s = src.read(1)
    if state == '':
        time.sleep(1)
    if state == 0 and s != '':
        print 'state 0'
        if s == '\x10':
            state = 1
            s = src.read(1)
        else:
            print '%i] %s => %s' % (counter,hex(ord(s)), s)
            counter += 1
    if state == 1:
        print 'state 1'
        if s == '\x02':
            state = 2
        if s == '\x03':
            crc = src.read(2)
            state = 0
    if state == 2:
        print 'state 2'
        route_info = src.read(3)
        state = 4
    if state == 4:
        print 'state 4'
        cmd = src.read(1)
        if cmd == '\x00':
            state = 5
        else:
            state = 0
    if state == 5:
        print 'state 5'
        cmd = ''
        c = ''
        while c != '\x00':
            cmd += c
            c = src.read(1)
            if c == '':
                break
        if cmd == 'ping':
            state = 6
        else:
            print 'Unknown CMD: %s' % cmd
            state = 0
    if state == 6:
        print 'PING!!'
        response = '\x10\x02' + route_info[1] + route_info[0] + route_info[2] + '\x80\x21\x10\x03'
        response += docrc(response[2:])
        print [hex(ord(x)) for x in response]
        dst.write(response)
    state = 0

I've omitted the definition of docrc (it's the same as we already discussed before) and I recognise that there are a few deficiencies here, not least of which is that I'm not dealing with the escaped 0x10 chars correctly, but it was a competition and there were other things competing for my time.

To give a brief overview of the code, I'm doing state transitions based on what I receive because there's so much data coming through an unreliable transport mechanism so I need to be able to recover from failures etc.

Start at state zero listen for a 0x10, if it's a 0x03 read the two bytes and ignore it and wait for the next 0x10 (crc checks are for people who care ;-) ) if it's a 0x02 move to the next state

Once we make it to state 3 we read the three info bytes (destination, src and ID) we check for a 0x00 and then read to the next 0x00 and check if it's a ping command. There's a number of reasons that's actually wrong, but it's right enough to get us there for the moment.

If it's unknown drop back to state 0 and wait for the next 0x10, otherwise it's a ping time to generate a response, which we do by sending an ! response packet as we worked out above.

OK, so let's do this! I set the script running and then wait for the nerve racking 40 minutes for the base station to cycle round to asking for yellow badges again and then wander over to watch the base station showing it's receiving responses for every packet! A check on the scores 20 minutes later show we've made 700 points!

While we were looking at the lua we also noticed that the 0x80 that's being sent is actually a flags field not a constant value (and the 0x00 we're using as delimiters before ping is actually just blank flags! :-) ) and the upper 4 bits are marked as server flags and the lower 4 bits are listed as Loyalty level.

So time to experiment (700 is good but more would be better right?) so we did a run with 0x81 which netted us ~1250. After some consideration I used 0x82 next as wanted to do something that wouldn't have the lowest bit set in case it went back down, it might be flags rather than a number in some way, and low and behold we got ~2500 points.

Time to pull out all the stops loyalty level 16 (0x8f) was deployed and we scored 16,000+

My script wasn't very reliable, and the version here has had some things wrong with it fixed as the day went on, but as noted above has a number of issues still causing problems, 0x10 not parsed correctly as an example. But it's not something I can experiment with after the fact given I don't have a base station and a badge to play with :-)

Discussions with other teams after the fact showed that they were doing all kinds of fun hardware hackery that I didn't get into, dumping the firmware from the devices and making it ignore CHPID commands for example so people can't steal your loyalty (unless someone's doing what I'm doing ;-) ) and even writing code that runs on the badge to do things like CHPID other badges or query them for information.

The information stored on the badges was valuable, as the whole point of HackFu was basically a big game of Cluedo where you won tokens to get clues like "The traitors do not have a tattoo on the back of their right hand" so knowledge of all the badge information was critical to success.

There are probably all kinds of other things that we could have explored, for example would the server have responded to other commands other than ping acks, could we have asked the base station for info on other badges, etc.

And talking with Alek about how things worked behind the scenes it turns out that the server wasn't doing a simple count of responses. It was keeping a list of badges each null to start with, then each time it heard a positive response it would set that badge to be that colour. What this means in practice is that our code was in fact setting all badges to be yellow in the servers eyes, but anyone who responded after our response would have overwritten our values.

This meant that because of the order that the colours were scanned (the same order as presented from the lua file above) any responses from Red and Blue teams were overwritten by us, and more worryingly if the other teams had caught on and implemented the same attack that responses from Green and Purple would have overwritten our values! Being the last to cycle if purple had executed this same attack no other team would have scored any loyalty points in any cycle they ran it!

Fortunately I was the only person to think of this approach, otherwise I'm not sure what solution I could have come up with, other than maybe a DOS on the wifi network!