Skip to content

Reverse Engineering A USB HID RFID Reader Writer Part 2

charlysan edited this page Mar 5, 2019 · 5 revisions

Analyzing the payload more closely

SOM/EOM and Length

In the first part we captured the following payloads:

Read operation

read

01 00 00 00 00 00 08 00 aa 00 03 25 00 00 26 bb

Beep

beep

01 00 00 00 00 00 08 00 aa 00 03 89 01 01 8a bb

Write operation

read

01 00 00 00 00 00 1f 00 aa 1a 21 00 01 01 02 4d 49 96 02 d2 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fb bb

The three payloads have a couple of things in common.

  • Character 0xAA at position at the end of the header
  • Character 0xBB at the end of the payload

Those two are control codes used to mark the end of the header (start of message [EOM]) and the end of the text (end of message [EOM])

  • A value at position 1 after EOM.

This number is the length of the message:

-- Read message length (0x03 --> 3)

[25 00 00]

-- Beep message length (0x03 --> 3)

[89 01 01]

-- Write message length (0x1a --> 26)

[21 00 01 01 02 4d 49 96 02 d2 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00]
  • A value before the EOM

This is the payload CRC. We have already seen this in the write payload, but as we will see in the next section it is present and calculated in every payload.

CRC

In the first tutorial I made the following assumption:

CRC = 0xb9 XOR cid XOR uid_b3 uid_b2 uid_b1 uid_b0 

And for the example used there:

CRC = 0xb9 ^ 0x4d ^ 0x49 ^ 0x96 ^ 0x02 ^ 0xd2

where 0xb9 was a hardcoded "initial value".

This worked, but It was not correct. That initial value is actually the result of xoring the first and the last bytes of the payload, along with the length:

Data:

[21 00 01 01 02 4d 49 96 02 d2 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00]

Length: 1a

0x1a ^ 0x21 ^ 0x01 ^ 0x01 ^ 0x02 ^ 0x80 = 0xb9

So, basically, the CRC routine will look like this:

CRC = XOR([data_length] + [data])

For that particular example would be:

0x1a ^ 0x21 ^ 0x01 ^ 0x01 ^ 0x02 ^ 0xb9 ^ 0x4d ^ 0x49 ^ 0x96 ^ 0x02 ^ 0xd2 ^ 0x80 = 0xfb

Analog procedure for the beep and read operations:

Data (beep):

[89 01 01]

Length: 3

CRC = 0x03 ^ 0x89 ^ 0x01 ^ 0x01 = 0x8a

Data (read):

[25 00 00]

Length: 3

CRC = 0x03 ^ 0x25  = 0x26

Trying to find commands using brute force

Now that we know how to properly calculate the CRC for any payload data, we could try to send to the device every possible command (1 byte) and check for the expected response. Let's use the writing routine explained in first part as an example:

Control Transfer Set Request
bmRequetType = 0x21
bRequest = 9
wValue = 0x301
wIndex = 0
wLength = 256
Data = 01 00 00 00 00 00 08 00 aa 00 03 89 05 01 8e bb

Control Transfer Get Request
bmRequetType = 0xa1
bRequest = 1
wValue = 0x302
wIndex = 0
wLength = 256

Response Data: 03 00 00 00 00 00 00 00 02 00 02 00 80 82 03

Control Transfer Set Request
bmRequetType = 0x21
bRequest = 9
wValue = 0x301
wIndex = 0
wLength = 256
Data = 01 00 00 00 00 00 1f 00 aa 1a 21 00 01 01 02 4d 49 96 02 d2 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 fb bb

Control Transfer Get Request
bmRequetType = 0xa1
bRequest = 1
wValue = 0x302
wIndex = 0
wLength = 256

Response Data: 03 00 00 00 00 00 00 00 02 00 02 00 80 82 03

The above includes a write command and a beep command, and summarizing it does the following:

1 - Set feature report 1 (beep command)

2 - Read from feature report 2

3 - Set feature report 1 (write command)

4 - Read from feature report 2

#2 and #4 return the following sequence of bytes:

03 00 00 00 00 00 00 00 02 00 02 00 80 82 03

And if you set feature report 1 containing an invalid CRC you will get this response:

03 00 00 00 00 00 00 00 02 00 02 00 8F 8C 03

So, we could iterate through every possible 1 byte length command and check the value at position 12:

from rfidhid.usb_hid import HID
from rfidhid.core import RfidHid
from time import sleep

def main():
    try:
        # Try to open RFID device using default vid:pid (ffff:0035)
        rfid = RfidHid()
    except Exception as e:
        print(e)
        exit()

    # Initialize device
    print('Initializing device...')
    rfid.init()
    sleep(2)
    
    cmds = []

    payload = [0x00] * 0x03 # Set length to 3
    payload[0x01] = 0x01
    payload[0x02] = 0x01

    for cmd in range (0x00,0xff):
        payload[0x00] = cmd 
        buff = rfid._initialize_write_buffer(payload)

        # Write Feature Report 1
        response = rfid.hid.set_feature_report(1, buff)

        if response != rfid.BUFFER_SIZE:
            raise ValueError('Communication Error.')

        # Read from Feature Report 2
        response = rfid.hid.get_feature_report(2, rfid.BUFFER_SIZE).tolist()
        cmd_found = ""

        if response[12] != 143 :
            cmd_found = "CMD FOUND! " + hex(cmd)
            cmds.append(cmd)

        print('CMD: ' + hex(cmd) + ' response: ' + str(response) + ' ' + cmd_found)
        sleep(0.02)

    print('Found ' + str(len(cmds)) + ' commands:')
    print([hex(x) for x in cmds])

if __name__ == "__main__": 
    main()

The execution of the above script returns these results:

Found 3 commands:
['0x21', '0x25', '0x89']

So, it looks like there are no other supported commands available (at least for my device)

Trying different command parameters combinations

The beep payload data looks like this:

[89 01 01]

First value is the command, and the other two are parameters. So, we could expect that changing those parameters would affect the beep (e.g. length, repetition). Using a similar approach as the one used for trying to find available commands, we could iterate through every possible parameters combination. I have already tried that, but I didn't get any different result (just one short beep).