05
May
2013

pCTF 2013 – usbdude (for 350)

For this challenge we’re given a pcap file containing USB traffic. Initial inspection learns us this is a dump of an AVRISP mkII USB programmer for 8-bit AVR microcontrollers.

As there’s quite some data present in the pcap, and a typical use case for a programmer would be to flash or read a device’s contents, we Google for a suitable reference of the AVRISP mkII communication protocol and implement a small parser for it in Python. Turns out the pcap contains quite some CMD_PROGRAM_FLASH_ISP commands, so we decide to write the payload of those to a file, hoping to end up with a firmware dump for some 8-bit AVR.

#!/usr/bin/python2
import struct

CMDS = {
1: 'CMD_SIGN_ON',
2: 'CMD_SET_PARAMETER',
3: 'CMD_GET_PARAMETER',
5: 'CMD_OSCCAL',
6: 'CMD_LOAD_ADDRESS',
7: 'CMD_FIRMWARE_UPGRADE',
10: 'CMD_RESET_PROTECTION',
16: 'CMD_ENTER_PROGMODE_ISP',
17: 'CMD_LEAVE_PROGMODE_ISP',
18: 'CMD_CHIP_ERASE_ISP',
19: 'CMD_PROGRAM_FLASH_ISP',
20: 'CMD_READ_FLASH_ISP',
21: 'CMD_PROGRAM_EEPROM_ISP',
22: 'CMD_READ_EEPROM_ISP',
23: 'CMD_PROGRAM_FUSE_ISP',
24: 'CMD_READ_FUSE_ISP',
25: 'CMD_PROGRAM_LOCK_ISP',
26: 'CMD_READ_LOCK_ISP',
27: 'CMD_READ_SIGNATURE_ISP',
28: 'CMD_READ_OSCCAL_ISP',
29: 'CMD_SPI_MULTI'}

def decode(s):
    return ''.join(s.split(':')).decode('hex')

lines = open('capdata.txt').read().split('\n')
lines = filter(len, lines)

fp = open('fw.bin', 'wb')

i = 0
while i < len(lines):
    line = lines[i].strip()

    if not len(line):
        continue

    d = decode(line)
    cmd = ord(d[0])

    # CMD_PROGRAM_FLASH_ISP
    if cmd == 0x13:
        l, = struct.unpack('>H', d[1:3])

        # hack - often program commands span two urbs
        if (l+10) > len(d):
            i += 1
            d += decode(lines[i])

        fp.write(d[10:])

    print '%s(%02x)' % (CMDS[cmd], cmd)
    i += 1

The script above operates on a filtered hex dump of the USB pcap (host -> device traffic only), so you’ll first need to use tshark to grab just the data we’re interested in.

tshark -r usbdude.pcap -Tfields -eusb.capdata 'usb.endpoint_number.direction==0' > capdata.txt

Indeed, the fw.bin image appears to a firmware dump for an (unknown) AVR. During the CTF, we disassembled the firmware blob in IDA and annotated all the functions (easy, as most of the code is part of the V-USB stack for AVR). The firmware seems to allow an AVR to act as a USB device, which means we should be able to tell what kind of device we’re dealing with from its USB descriptor. As we’re unable to actually run the firmware, we’ll have to see if we can find the descriptor in the firmware image.

Author’s note: I’ve failed to keep notes while hunting down the USB descriptor. In short, the device presents a single HID configuration descriptor, which means we should be able to find a HID descriptor elsewhere.

At 0x34 into the firmware bin, we find the HID descriptor:

05 01 // Usage Page (Generic Desktop)
09 06 // Usage (??)
a1 01 // Collection (Application)
05 07 // Usage Page (Keyboard/Keypad Page)
19 54 // Usage Minimum 0x54
29 63 // Usage Maximum 0x63
15 00 // Logical Minimum (0)
25 01 // Logical Maximum (1)
75 01 // Report Size (1)
95 10 // Report Count (0x10)
81 02 // Input 
c0 // End Collection

So, our device claims to be a HID keypad, supporting input values of 0x54-0x63. These values can be mapped to keyboard keys using a list of USB HID keyboard scan codes. Looks like our keypad sends keys ‘/*-+@1234567890.’ with each key represented as a bit (report size), totaling 0x10 bits (report count). The ‘@’ is used as a placeholder for ‘keypad enter’.

Unfortunately, the code responsible for sending keys is a huge loop, with its loop counter representing a ‘timer’. For each iteration, it compares the counter against many different counter values, outputting or clearing a bit in its internal ‘key buffer’ whenever one of these counter values is matched, followed by sending a HID report to the USB host.

To reconstruct the ‘keyboard key’ stream produced by this logic, we’ve written some python which parses objdump output of said loop and outputs each key as it would be sent by the firmware (oh-so-dirty, but it works). To simply parsing, we’re not distinguishing between ‘keydown’ and ‘keyup’ events, which explains why the script below outputs each key twice (once for keydown, once for keyup).

#!/usr/bin/python2
import re, subprocess

KEYS = '/*-+@1234567890.'

lsb = msb = None
last = 0
out = []

def handle(op, args):
    global lsb, msb, last

    if op == 'cpi':
        lsb = int(args[1], 16)
    elif op == 'ldi':
        msb = int(args[1], 16)
    elif op in ('brne', 'breq'):
        val = (msb << 8 | lsb)
        out.append(val)
        last = val


disas = subprocess.check_output('avr-objdump -D -bbinary -mavr --start-address 0x67c --stop-address=0xac2 fw.bin', shell=True).strip()

for line in disas.split('\n'):
    line = line.strip()
    
    if not re.search('^[0-9a-f]+:', line):
        continue

    parts = re.split('\s+', line)

    op = parts[3]
    handle(op, parts[4:])

print ''.join([KEYS[x & 0xf] for x in sorted(out)])

Output:

@@44++110000@@**88@@++6622000000@@//2200000000@-@-77..15152266@*@*11000000@@//33@@++99999999@@@@@@

If we carefully examine the output, some key events seem to be out of order (which suggests sometimes a key is pressed even though another is still ‘held down’). Luckily, these cases are easy to spot, so we’ve manually corrected them:

@@44++110000@@**88@@++6622000000@@//2200000000@@--77..11552266@@**11000000@@//33@@++99999999@@@@@@

Ignoring every second key occurence (the keyup event):

@4+100@*8@+62000@/20000@-7.1526@*1000@/3@+9999@@@

This looks like something to be punched into a calculator. By rewriting enters to braces we get:

((((((((4 + 100) * 8) + 62000) / 20000) - 7.1526) * 1000) / 3) + 9999)

Let’s calculate:

>>> ((4 + 100) * 8 + 62000) / 20000.0
3.1416

>>> (ans - 7.1526) * 1000 / 3
-1337

>>> ans + 9999
8662

Some remarkable intermediate values, and a final answer (flag): 8662

{One Response to “pCTF 2013 – usbdude (for 350)”}

  1. Nicely done 😉

    Alex