Skip to content

Reverse Engineering A USB HID RFID Reader Writer

charlysan edited this page Mar 7, 2019 · 13 revisions

Introduction

A couple of years ago I bought a very cheap Chinese USB HID 125Khz RFID Reader/Writer capable of reading RFID tag's ids and copy/duplicate them using EM4305 or T5577 re-writable transponders.

The device doesn't come with any software (you have too look for it on the web), and the only one that seems to be available (called IDRW V3) works only on Windows, and it doesn't have any kind of documentation. Anyway, the tool is very straightforward and both things work pretty well; I've used them several times to write EM4305 tags without any problem. However, it was a pain to be stuck with Windows every time I needed to copy a tag; so I decided to analyze the communication protocol between the device and the PC and try to reverse it and create a tool that could work on Linux and MacOS.

The device

There are several similar devices like this one, some of them can only read tags and they work like USB HID Keyboards, and some others can read/write and work as a USB to UART Bridge. The one I have is recognized as a USB HID-Compliant Device but it does not behave as a keyboard. You can see some pictures below:

Windows Setup

Under Windows XP I get this device information:

So, summarizing:

- vendor_id: 0xFFFF
- product_id: 0x0035
- Product Name: USB Reader

Recognition

When you first plug the device the LED starts to blink from red to green, until it gets stuck in red and at that point you should hear a double beep; that means the device is ready to be used. No drivers are needed to be installed, IDRW V3 Tool will communicate directly with the reader as a HID-compliant device.

Linux setup

For connivance I will use a Raspberry Pi (with Raspbian installed) that I already had connected to my router, and I will work remotely over SSH from my Mac; but you should be able to reproduce the same behavior from other Linux distributions and even using a Virtual Machine.

When you first plug the device on a Linux machine it starts to blink just like it does on Windows, but it stays in that state forever. That seems to be related on how Linux and Windows enumerate the USB devices, it looks like Linux doesn't automatically try to get the Device HID Descriptor as Windows does.

Important: I suggest to read Device Class Definition for Human Interface Devices (HID) document before going on. That document will give you a very detailed information about how HID devices work. Another useful reading is USB in a NutShell by Beyond Logic.

So, let's see what lsusb says:

$ lsusb

Bus 001 Device 004: ID ffff:0035
Bus 001 Device 003: ID 0423:ec10 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0423:8534 Standard Microsystems Corp.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

We can verify that the the device (ffff:0035) is connected to Bus 1. Let's try to get more information from lsusb (you'll need sudo privileges):

$ sudo lsusb -vd ffff:0035

Bus 001 Device 004: ID ffff:0035
Device Descriptor:
  bLength                18
  bDescriptorType         1
  bcdUSB               2.00
  bDeviceClass            0 (Defined at Interface level)
  bDeviceSubClass         0
  bDeviceProtocol         0
  bMaxPacketSize0         8
  idVendor           0xffff
  idProduct          0x0035
  bcdDevice            1.00
  iManufacturer           0
  iProduct                1 USB Reader
  iSerial                 0
  bNumConfigurations      1
  Configuration Descriptor:
    bLength                 9
    bDescriptorType         2
    wTotalLength           27
    bNumInterfaces          1
    bConfigurationValue     1
    iConfiguration          0
    bmAttributes         0xa0
      (Bus Powered)
      Remote Wakeup
    MaxPower              200mA
    Interface Descriptor:
      bLength                 9
      bDescriptorType         4
      bInterfaceNumber        0
      bAlternateSetting       0
      bNumEndpoints           0
      bInterfaceClass         3 Human Interface Device
      bInterfaceSubClass      0 No Subclass
      bInterfaceProtocol      0 None
      iInterface              0
        HID Device Descriptor:
          bLength                 9
          bDescriptorType        33
          bcdHID               1.00
          bCountryCode            0 Not supported
          bNumDescriptors         1
          bDescriptorType        34 Report
          wDescriptorLength      28
          Report Descriptor: (length is 28)
            Item(Global): Usage Page, data= [ 0xa0 0xff ] 65440
                            (null)
            Item(Local ): Usage, data= [ 0x01 ] 1
                            (null)
            Item(Main  ): Collection, data= [ 0x01 ] 1
                            Application
            Item(Local ): Usage, data= [ 0x03 ] 3
                            (null)
            Item(Global): Report ID, data= [ 0x01 ] 1
            Item(Global): Report Size, data= [ 0x08 ] 8
            Item(Global): Report Count, data= [ 0xff ] 255
            Item(Main  ): Feature, data= [ 0x02 ] 2
                            Data Variable Absolute No_Wrap Linear
                            Preferred_State No_Null_Position Non_Volatile Bitfield
            Item(Local ): Usage, data= [ 0x03 ] 3
                            (null)
            Item(Global): Report ID, data= [ 0x02 ] 2
            Item(Global): Report Size, data= [ 0x08 ] 8
            Item(Global): Report Count, data= [ 0xff ] 255
            Item(Main  ): Feature, data= [ 0x02 ] 2
                            Data Variable Absolute No_Wrap Linear
                            Preferred_State No_Null_Position Non_Volatile Bitfield
            Item(Main  ): End Collection, data=none
Device Status:     0x0000
  (Bus Powered)

Now we got all the device information and the HID Descriptors, and after 1 or 2 seconds you will hear a double beep, and the LED will change to red; the device should be ready to be used.

It is important to notice from the above dump that the HID Descriptor states that there are two available Reports:

Report ID, data= [ 0x01 ] 1
Report ID, data= [ 0x02 ] 2

This information will be useful later.

The next steps will be to sniff USB traffic from IDRW V3 Tool to the device when reading/writing a tag, and see if we can reproduce the same behavior under Linux.

USB Analyzing

I will try to reverse-engineer the communication between the IDRW V3 Tool and the reader doing some USB Analyzing. There are a couple of options to do this task in Windows:

As I already had some experience with Wireshark, I chose USBPcap. Installation is pretty straightforward, you can capture using the command line USBPcapCMD.exe or right away from Wireshark. I will do it from Wireshark, selecting USBPcap 2 interface.

Note: I couldn't capture using Windows XP, luckily I had another old laptop with Windows 7 where USBPcap worked perfect.

A.1 - Capturing a Read Tag operation

After you start capturing you will see a lot of URB_BULK stuff; we don't care about that, and if you have read the Device Class Definition for Human Interface Devices (HID) document you must have seen that HID requests are issued through the Control Pipe (See chapter 7). So, let's filter out every URB_BULK transfer by adding this filter to Wireshark:

usb.transfer_type != URB_BULK

Now run IDRW V3 tool and hit the Read button. I had previously written a tag with these ids:

customer_id: 77
uid: 1234567890

And now we start to see some action in Wireshark!

A.1.1 - First Request (Set Feature Report)

Wireshark capture - set_request 01

Going back to the HID Definition Doc, we can double check that we are in front of a Set_Request operation:

bmRequetType = 0x21
bRequest = 9
wValue = 0x301  --> 0x3 << 8 | 0x01

This is a Set Feature Report 1 HID operation. You can get that from wValue:

The wValue field specifies the Report Type in the high byte and the Report ID in the low byte.

So, now we know that the tool issues a Set_Request for Feature Report #1, and with a Report Length of 256 bytes. But, there is a problem; there is no payload in that request. Where are those 256 bytes?

If we move to the next capture, it is also a URB_Control out request, but from the device to the host; and it has a 256 bytes payload! So, this must be the missing payload. There must be something wrong with the USBPcap capture and/or the decoding. Actually, the USBPcap documentation states that there are some limitations. For the moment I will consider that this is part of the payload of the first Set_Request, let's check what that payload looks like:

Wireshark capture - set_request 01 payload

There are a couple of bytes in that payload that we should write down. So, Summarizing, the first request looks something like this:

Control Transfer Set Request
bmRequetType = 0x21
bRequest = 9
wValue = 0x301
wIndex = 0
Data = 01 00 00 00 00 00 08 00 aa 00 03 25 00 00 26 bb

A.1.2 - Second Request (Get Feature Report)

Let's now move on to the next capture:

Wireshark capture - get_request 01

This time the tool issues a Get_Request operation for Feature Report 2. Let's check what's in the next capture coming from the device:

Wireshark capture - get_request 01 payload

Those bytes marked in red look very familiar:

0x4d --> 77
0x499602d2 --> 1234567890

We got the tag id from the device, and we know how to read a tag without IDRW V3 tool. Let's summarize the reading routine:

Reading Routine

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 25 00 00 26 bb

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

If we look further in Wireshark we'll find another Set_Request:

Wireshark capture - beep

When I first saw this I had no clue about what it could be because I already had the tag uid and customer_id, anyway I wrote it down:

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 01 01 8a bb

This command will make sense very soon ;)

Before going on with the Writing routing let's try to read a tag from Linux using some Python code. We'll get back to this later.

USB access from Python

I'll use pyusb. It is a very easy to use lib, and it has a good documentation and even a tutorial on how to communicate with a USB Device.

To install the library in Linux you'll need Python >= 2.4, pip and libusb.

libusb instalation

$ sudo apt-get install libusb-1.0-0-dev

pyusb installation

$ pip install pyusb

Reading a tag from Python

I assume that the device has been plugged and it is ready to be used (after running sudo lsusb -vd ffff:0035). Let's try to connect to it with this simple code:

import usb.core

ret =  usb.core.find(idVendor=0xffff, idProduct=0x0035)
print ret
$ python read.py

DEVICE ID ffff:0035 on Bus 001 Address 006 =================
 bLength                :   0x12 (18 bytes)
 bDescriptorType        :    0x1 Device
 bcdUSB                 :  0x200 USB 2.0
 bDeviceClass           :    0x0 Specified at interface
 bDeviceSubClass        :    0x0
 bDeviceProtocol        :    0x0
 bMaxPacketSize0        :    0x8 (8 bytes)
 idVendor               : 0xffff
 idProduct              : 0x0035
 bcdDevice              :  0x100 Device 1.0
 iManufacturer          :    0x0
 iProduct               :    0x1 Error Accessing String
 iSerialNumber          :    0x0
 bNumConfigurations     :    0x1
  CONFIGURATION 1: 200 mA ==================================
   bLength              :    0x9 (9 bytes)
   bDescriptorType      :    0x2 Configuration
   wTotalLength         :   0x1b (27 bytes)
   bNumInterfaces       :    0x1
   bConfigurationValue  :    0x1
   iConfiguration       :    0x0
   bmAttributes         :   0xa0 Bus Powered, Remote Wakeup
   bMaxPower            :   0x64 (200 mA)
    INTERFACE 0: Human Interface Device ====================
     bLength            :    0x9 (9 bytes)
     bDescriptorType    :    0x4 Interface
     bInterfaceNumber   :    0x0
     bAlternateSetting  :    0x0
     bNumEndpoints      :    0x0
     bInterfaceClass    :    0x3 Human Interface Device
     bInterfaceSubClass :    0x0
     bInterfaceProtocol :    0x0
     iInterface         :    0x0

It works. We can connect to the device and get some info. As we can see there is only one configuration, so there is no need to set the configuration manually, it will use the one by default. Let's now try to read the tag using the routing from A.1.2. To issue a Get_request and Set_request we can use control::ctrl_transfer() function:

import usb.core
import usb.control

def read_tag(dev):
    BUFFER_SIZE = 256
    buff = [0x00] * BUFFER_SIZE

    # Set up payload for reading routing
    buff[0x00] = 0x01
    buff[0x06] = 0x08
    buff[0x08] = 0xaa
    buff[0x0a] = 0x03
    buff[0x0b] = 0x25
    buff[0x0e] = 0x26
    buff[0x0f] = 0xbb

    # Write to Feature Report 1
    ret = dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)
    if ret != BUFFER_SIZE:
    	raise ValueError('Communication Error.')

    # Read from Feature Report 2
    return dev.ctrl_transfer(0xa1, 0x01, 0x0302, 0, BUFFER_SIZE)

dev = usb.core.find(idVendor=0xffff, idProduct=0x0035)
ret = read_tag(dev)
print ret

Let's execute that:

$ sudo python test.py

array('B', [3, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0, 77, 73, 150, 2, 210, 68, 3])

Let's print that in hex by adding this function:

def array_to_hex_string(input):
    return ' '.join([hex(x) for x in input])

print array_to_hex_string(ret)
$ sudo python read.py

0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x6 0x0 0x4d 0x49 0x96 0x2 0xd2 0x44 0x3

We got our tag's ids!

0x4d -> 77
0x499602d2 --> 1234567890

But the device did not emit any beep. Maybe that extra Set_Request after the reading was the command for a beep? Let's try that out:

import usb.core
import usb.control

def beep(dev):
    BUFFER_SIZE = 256
    buff = [0x00] * BUFFER_SIZE

    # Set up payload for beep
    buff[0x00] = 0x01
    buff[0x06] = 0x08
    buff[0x08] = 0xaa
    buff[0x0a] = 0x03
    buff[0x0b] = 0x89
    buff[0x0c] = 0x01
    buff[0x0d] = 0x01
    buff[0x0e] = 0x8a
    buff[0x0f] = 0xbb

    # Write to Feature Report 1
    return  dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)

dev = usb.core.find(idVendor=0xffff, idProduct=0x0035)
beep(dev)

Run it again and you will get the BEEP ;)

So far so good, we have reversed the reading and beep routines and replicated them using Python on a Linux machine. Now let's see if we can write a tag, for that we are going back to Wireshark.

A.2 - Capturing a Write Tag operation

Following the same procedure we took in section A.1 let's start a capture in Wireshark and click on the write button under IDRW V3 tool using the same data (pid=77, uid=1234567890), and I will personally use EM4395 as it is the only tag I have.

A.2.1 - First Request (Set Feature Report)

This is the first stage of the routine, and it is needed in order to configure the device for a writing operation.

Wireshark capture - write set_request 01

So, for the first request we end up with this:

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

A.2.2 - Second Request (Get Feature Report)

Wireshark capture - write set_request 01

Notice the response from the device:

Wireshark capture - write get_request 01

That response might be used by the tool to verify that the device is configured correctly.

A.2.3 - Third Request (Set Feature Report)

The tool now sends the payload containing the tag's id:

Wireshark capture - write set_request 02

After that there is another Get_Request that might be for verification, and then a final Set_Request for the beep. So, the writing routine (for 77:1234567890) could be summarized like this:

Writing Routine

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

Let's code that in python...

Writing a tag from Python

The new function should be very straightforward following the writing routine in section A.2.3:

import usb.core
import usb.control
import struct

def array_to_hex_string(input):
    return ' '.join([hex(x) for x in input])

def write_tag(dev, cid, uid):
    BUFFER_SIZE = 256 
    buff = [0x00] * BUFFER_SIZE

    # Setup payload for writing routine
    buff[0x00] = 0x01
    buff[0x06] = 0x08
    buff[0x08] = 0xaa
    buff[0x0a] = 0x03
    buff[0x0b] = 0x89
    buff[0x0c] = 0x05
    buff[0x0d] = 0x01
    buff[0x0e] = 0x8e
    buff[0x0f] = 0xbb

    # Write to Feature Report 1
    ret_value = dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)
    if ret_value != BUFFER_SIZE:
        raise ValueError('Communication Error.')

    # Read from Feature Report 2    
    ret = dev.ctrl_transfer(0xa1, 0x01, 0x0302, 0, BUFFER_SIZE)
    print array_to_hex_string(ret)

    buff = [0x00] * BUFFER_SIZE

    id_data = [cid] + [ord(x) for x in list(struct.pack('>I', uid))]
    print 'id data: ' + array_to_hex_string(id_data)

    # Payload containing uid and customer_id
    buff[0x00] = 0x01
    buff[0x06] = 0x1f
    buff[0x08] = 0xaa
    buff[0x0a] = 0x1a
    buff[0x0b] = 0x21
    buff[0x0d] = 0x01
    buff[0x0e] = 0x01
    buff[0x0f] = 0x02
    buff[0x10] = id_data[0] 
    buff[0x11] = id_data[1]
    buff[0x12] = id_data[2]
    buff[0x13] = id_data[3]
    buff[0x14] = id_data[4]
    buff[0x15] = 0x80
    buff[0x25] = 0xfb
    buff[0x26] = 0xbb

    dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)

    # Read from Feature Report 2    
    ret = dev.ctrl_transfer(0xa1, 0x01, 0x0302, 0, BUFFER_SIZE)
    print array_to_hex_string(ret)

    return ret

dev = usb.core.find(idVendor=0xffff, idProduct=0x0035)

ret = write_tag(dev, 77, 1234567890)
print array_to_hex_string(ret)

The only "tricky" part might be this:

id_data = [cid] + [ord(x) for x in list(struct.pack('>I', uid))]

Struct::pack() will generate a list containing UID bytes in ASCII format. >I means unsigned int with little-endian notation. the ord() function will convert the ASCII format to INT. So, for our input the data list will look like this:

[0x4d 0x49 0x96 0x02 0xd2]

Note: the above works in Python 2.7, for Python 3 list() function returns integers, so you don't need to call ord():

# python 3
id_data = [cid] + list(struct.pack('>I', uid))

Let's execute it and see what happens (I've previously written the tag with a different uid using IDRW V3 tool):

$ sudo python write.py
0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x0 0x80 0x82 0x3
id data: 0x4d 0x49 0x96 0x2 0xd2
0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x0 0x80 0x82 0x3
0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x0 0x80 0x82 0x3

The cid and uid look good, and the three responses too. Let's read the tag to verify it:

$ sudo python read.py

0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x6 0x0 0x4d 0x49 0x96 0x2 0xd2 0x44 0x3

It worked!

Let's try another cid/uid combination. I will change the write_tag() calling line to this:

ret = write_tag(dev, 11, 12345)
$ sudo python write.py

0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x0 0x80 0x82 0x3
id data: 0xb 0x0 0x0 0x30 0x39
0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x1 0x85 0x86 0x3
0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x2 0x1 0x85 0x86 0x3

Now we got a different response (have a look at the last bytes). Let's read the tag again:

$ sudo python read.py

0x3 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x2 0x0 0x6 0x0 0x4d 0x49 0x96 0x2 0xd2 0x44 0x3

It still has 77:1234567890. The last write operation didn't work. We are missing something...

A.2.4 - CRC

To find out what was going on I went back to Windows and start writing different cid/uid combinations and I discovered something:

Wireshark capture - write crc

That byte marked in red (position 0x25) changes based on the cid/uid combination I use. So I wrote a little table with the results from several tests:

cid uidb3 uidb2 uidb1 uidb0  -> byte 0x25

00 00 00 00 01 -> b8
00 00 00 00 02 -> bb
00 00 00 00 03 -> ba
00 00 00 00 04 -> bd
00 00 00 00 05 -> bc
00 00 00 00 06 -> bf
00 00 00 00 07 -> be
00 00 00 00 08 -> b1
00 00 00 00 09 -> b0
00 00 00 00 0a -> b3
00 00 00 00 ff -> 46

01 00 00 00 01 -> b9
02 00 00 00 01 -> ba
03 00 00 00 01 -> bb
04 00 00 00 01 -> bc
ff 00 00 00 01 -> 47
ef 00 00 00 01 -> 57
df 00 00 00 01 -> 57

00 00 00 01 01 -> b9
00 01 01 01 01 -> b9 
ff 01 01 01 01 -> 47

At first glance it doesn't make any sense... but have a look at these three again:

01 00 00 00 01 -> b9
00 00 00 01 01 -> b9
00 01 01 01 01 -> b9 

The three combinations returned the same value 0xb9. So, that value looks like the initial value for some operation, and that operation looks a LOT like a XOR. Let's check that out:

00 00 00 00 01 -> b8  --> b9 XOR 01
00 00 00 00 02 -> bb  --> b9 XOR 02
00 00 00 00 03 -> ba  --> b9 XOR 03
00 00 00 00 04 -> bd  --> b9 XOR 04
00 00 00 00 ff -> 46  --> b9 XOR ff
01 00 00 00 01 -> b9  --> b9 XOR 1 XOR 01
02 00 00 00 01 -> ba  --> b9 XOR 1 XOR 02
ff 00 00 00 01 -> 47  --> b9 XOR 1 XOR ff
00 00 00 01 01 -> b9  --> b9 XOR 01 XOR 01

So, basically they are "XORing" every byte using an initial value of 0xb9:

0xb9 XOR cid XOR uid_b3 uid_b2 uid_b1 uid_b0

Let's check it with IPython using 77:1234567890:

~ ipython
Python 3.6.3 (default, Oct  4 2017, 06:09:15) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.2.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: hex(0xb9 ^ 0x4d ^ 0x49 ^ 0x96 ^ 0x02 ^ 0xd2)
Out[1]: '0xfb'

It is correct, 0xfb was the number I was hard-coding into payload byte 0x25 when writing 77:1234567890.

This is a very common procedure called Cyclic redundancy check. It is a simple technique to check that the data we are sending hasn't got corrupted on the way. So, the device will get 77:1234567890 and 0xfb; it will calculate the CRC sum and compare it against 0xfb, if they don't match it will abort the operation.

Let's write a CRC sum calculation function to replace the hard-coded value:

def calculate_crc_sum(payload, init_val=0xb9):
	tmp = init_val
	for x in payload:
		tmp = tmp ^ x 
	return tmp

Our final writing script will look like this:

import usb.core
import usb.control
import struct

def array_to_hex_string(input):
    return ' '.join([hex(x) for x in input])

def calculate_crc_sum(payload, init_val=0xb9):
    tmp = init_val
    for x in payload:
        tmp = tmp ^ x 
    return tmp    

def write_tag(dev, cid, uid):
    BUFFER_SIZE = 256 
    buff = [0x00] * BUFFER_SIZE

    # Setup payload for writing routine
    buff[0x00] = 0x01
    buff[0x06] = 0x08
    buff[0x08] = 0xaa
    buff[0x0a] = 0x03
    buff[0x0b] = 0x89
    buff[0x0c] = 0x05
    buff[0x0d] = 0x01
    buff[0x0e] = 0x8e
    buff[0x0f] = 0xbb

    # Write to Feature Report 1
    ret_value = dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)
    if ret_value != BUFFER_SIZE:
        raise ValueError('Communication Error.')

    # Read from Feature Report 2    
    ret = dev.ctrl_transfer(0xa1, 0x01, 0x0302, 0, 256)
    print array_to_hex_string(ret)

    buff = [0x00] * BUFFER_SIZE

    id_data = [cid] + [ord(x) for x in list(struct.pack('>I', uid))]
    print 'id data: ' + array_to_hex_string(id_data)

    # Payload containing uid, customer_id and CRC
    buff[0x00] = 0x01
    buff[0x06] = 0x1f
    buff[0x08] = 0xaa
    buff[0x0a] = 0x1a
    buff[0x0b] = 0x21
    buff[0x0d] = 0x01
    buff[0x0e] = 0x01
    buff[0x0f] = 0x02
    buff[0x10] = id_data[0] 
    buff[0x11] = id_data[1]
    buff[0x12] = id_data[2]
    buff[0x13] = id_data[3]
    buff[0x14] = id_data[4]
    buff[0x15] = 0x80
    buff[0x25] = calculate_crc_sum(id_data)
    buff[0x26] = 0xbb

    dev.ctrl_transfer(0x21, 0x09, 0x0301, 0, buff)

    # Read from Feature Report 2    
    ret = dev.ctrl_transfer(0xa1, 0x01, 0x0302, 0, BUFFER_SIZE)
    print array_to_hex_string(ret)

    return ret

dev = usb.core.find(idVendor=0xffff, idProduct=0x0035)

ret = write_tag(dev, 77, 1234567890)
print array_to_hex_string(ret)

Now you should be able to successfully write any CID/UID combination.

Bonus - Reversing IDRW V3 Tool

This should be quick. I was wondering if all the readers/writers like mine have the same vid:pid (0xffff:0x0035), and that IDRW V3 tool always recognizes the device by those ids, or using some other technique; so I could not help to disassemble the tool and have a look at it. The tool has 3 binary files (one .exe and two .dll), and they are very tiny; so spotting the routine that opens the device should not be difficult. My first guess would be to look for two consecutive "pushes" to the stack, containing 0xffff and 0x0035 before some call. Let's disassemble USB.dll:

objdump -print-imm-hex  -d USB.dll > usb_dll_dump.asm

At the very beginning of the dump you will find this:

USB.dll:    file format COFF-i386

 Disassembly of section .text:
 .text:
 10001000:    b8 01 00 00 00           movl    $0x1, %eax
 10001005:    c2 0c 00                 retl    $0xc
 10001008:    90                       nop
 10001009:    90                       nop
 1000100a:    90                       nop
 1000100b:    90                       nop
 1000100c:    90                       nop
 1000100d:    90                       nop
 1000100e:    90                       nop
 1000100f:    90                       nop
 10001010:    8b c1                    movl    %ecx, %eax
 10001012:    c7 00 18 51 00 10        movl    $0x10005118, (%eax)
 10001018:    66 c7 40 06 ff ff        movw    $0xffff, 0x6(%eax)
 1000101e:    66 c7 40 04 35 00        movw    $0x35, 0x4(%eax)
 10001024:    c7 40 08 ff ff ff ff     movl    $0xffffffff, 0x8(%eax)
 1000102b:    c7 40 4c 00 00 00 00     movl    $0x0, 0x4c(%eax)
 10001032:    c3                       retl

This piece of code moves 0xffff and 0x0035 to a memory block pointed by EAX register. If you edit the file with some hex editor, look for 66 c7 40 04 35 00 sequence and change the 35 for a 34 (for example), and then run the tool, you will notice that the reading mechanism no longer works; the tool cannot find the device. In addition, if you follow the call reference for address 10001010h you will notice that there are no other fall-backs. So, from this quick test I can be almost certain that the recognition is made using that unique vid:pid combination, and that all the devices like mine around there that works with IDRW V3 tool must have the same vid:pid.

Final notes

There is also a CRC byte included in the response of a read operation:

wireshark read crc

The initial value for that CRC is not the same as the one for the writing operation, and I leave it to the reader to discover it.

I have covered the main concepts on how to reverse a USB HID device and control it from Linux using Python. From this point, writing a cross-platform tool to fully control this Device (support T5577, continuous reading, write verification, etc.) should be straightforward.

References

Device Class Definition for Human Interface Devices (HID)

Beyond Logic - USB in a NutShell

HidGlobal - Understanding Card Formats

Hackaday - Talking USB from Python

EM4100 Datasheet

EM4305 Datasheet

Atmel T5577 Datasheet

LinuxVoice - Drive it yourself: USB car

USBPcap