Research and Development

In this post I describe how my cheap magstripe reader wouldn’t read all magstripes, only credit/debit cards. This did nothing to help me understand what data was on my hotel key card – which is what I really wanted to know. Rather than take the obvious next step or buying a better reader, I opted to open up the cheap magstripe reader, probed around a bit and found a way to read the raw data off the hotel magstripes. What that data means remains a mystery so there may be a part 2 at some stage.

About my magstripe reader

I bought the following reader for £11.85 in 2017:

Cheap reader from eBay
image-6482

Cheap reader from eBay

It connects via USB and is detected as a keyboard. If you swipe a credit/debit card through the reader, the corresponding data from the magstripe will appear if your text editor as if it had been typed. If you don’t have a text editor open at the time, you’ll get the effect of whatever the keystrokes on the magstripe reader are!

It works fine under both Windows and Linux. But only for credit/debit cards. When I swiped a hotel key card through, I got no data at all.

Why not just get a different reader?

I could have taken a couple of different (and no doubt better) approaches to this project. Finding a better magstripe reader was one option. Googling around to better understand hotel magstripes would have no-doubt been fruitful, but potentially spoils some of problem solving. I can always google later when I’m properly stuck. :-)

I since invested in an MSR605X reader/writer. I haven’t played with it much yet, but it is able to do raw reads. I’ll probably cover this reader in a future post.

Plan for the cheap reader

I figured that the magnetic read head in the cheap reader would almost certainly be able to read the data from from hotel cards or any other card with a magstripe. But the rest of the reader was only able to interpret payment card data. If I could probe the electrical signals in the right place within the reader, I should be able to see the 1′s and 0′s on the magstripe.

Magnetic read head inside card reader
image-6483

Magnetic read head inside card reader

I was a vague plan and certainly not the path of least resistance, but would ultimately work…

Probing around

I had two tools at my disposal to probe with (this was a home-based side project as opposed to an office-based project):

  • A cheap handheld Oscilloscope (Sainsmart DS202)
  • A Saleae logic analyzer

I quickly realized there were 7 points on the read head I could attach probes to.

Connection points on back of read head
image-6484

Connection points on back of read head

I figured if hooked the read head directly up to my scope, I’d be able to see an analogue signal when I swiped a card. Ultimately I hoped to do something with said signal to get the data I wanted.

Connections for scope soldered directly to read head
image-6485

Connections for scope soldered directly to read head

I tried hooking up various pairs from these 7 probe points to my scope, but didn’t find any signal at all. Maybe the signal’s too weak, or maybe (in retrospect) I did a rubbish job of identifying a suitable ground.

By this point I’d started googling the chip numbers. There were 2 x Op Amps and 1 x Analogue Comparator:

2 x Op Amps
image-6486

2 x Op Amps

Analogue Comparator (left); STM32 Microcontroller (right)
image-6487

Analogue Comparator (left); STM32 Microcontroller (right)

These chips were responsible for amplifying the signal from the read head. So if hooked up to the output of each, I might see a signal I could use to determine the data on the magstripes. I use the datasheets to identify the output pins of each IC.

The output from the 2 x Op Amps was about 0.5 – 0.6v peak-to-peak. Easily viewable using my scope, but I’d be unable to feed this into my logic analyser. I’d need about 3v peak-to-peak for that. The signal was also quite scruffy looking. Not the nice square wave I’d hoped for.

The output from the analogue comparator was exactly what I was after. A nice 3 or 4v peak-to-peak square wave that appeared only when I swiped a card. There were 4 outputs from the comparator. One never produced a signal. This seemed reasonable for a read head that could read 3-track magstripes. Time to feed this into the logic analyser and start figuring what constituted a 0 or 1…

1′s and 0′s

Progress had been quite fast up to this point. This was shaping up to be an interesting project.

Here’s the output from the comparator viewed in the Saleae Logic software:

Comparator output viewed via logic analyser
image-6488

Comparator output viewed via logic analyser

Note that we’re only seeing 1 square wave, not 3. This is because this particular card only has data on 1 of the 3 tracks.

So we have a signal to analyse, but we don’t have 1′s and 0′s yet.

Phrack37 from 1992 helps us with that:

ASCII art explanation of how 1' class=
image-6489

ASCII art explanation of how 1′s and 0′s are encoded on a magstripe

Essentially a 0 and 1 are both the same length when encoded. A 1 changed state half way through and a 0 doesn’t. Note that the 0 can be either high or low.

Rather than decode the 1′s and 0′s by eye, I wrote a Python script that parsed the exported data from the logic analyzer and dumped the 1′s and o’s. Here’s the format used by “Logic” (the Saleae software) when exporting to CSV:

Format used in CSV export from Saleae' class=
image-6490

Format used in CSV export from Saleae’s Logic software

The Python code turned out to be a pain to write because the baud rate of the data wasn’t constant. It varies depending on how fast the card is moving past the read head. I took a few wrong turns when coding this up, but eventually found a solution that worked.

Here’s a scatter graph that shows that the duration of square wave pulse doesn’t have exactly 2 values as expected. Instead a wide range of values are seen. It’s still possible to tell 0′s (long pulses, green dots) from 1′s (short pulses, blue/purple dots) – see top graph. The ratio of each successive pulse to the last was a better way to tell o’s from 1′s – see bottom graph:

Graph showing how much pulse duration (delta) takes a range of values, not just two as expected
image-6491

Graph showing how much pulse duration (delta) takes a range of values, not just two as expected

And finally, the bytes / characters

This is where my problems started. My whole life, bits have made bytes. Specifically they’ve made 8-bit bytes. This is not the case on magstripes, it turned out.

As mentioned in the phrack paper, credit cards use 2 tracks, one where characters are composed of 5 bits (4 bits + 1 parity) and another where characters are composed of 7 bits (6 bits + 1 parity).

I spent quite a bit of time coding up a decoder for credit cards. Partly because I needed to be assured that I’d read the 1′s and o’s correctly and partly because I thought I’d need the code to see if the same character types were used in hotel cards.

One of the challenges in coding this up was knowing where the data started and the preamble ended. It seems that “sentinels” are used to mark the start/end of the data. These are special characters (bit patterns) that readers can look for at the start and end of a stripe… then it made sense. This is why the card reader only worked for credit cards! The firmware is looking for the sentinels used by credit cards. It can’t decode the hotel cards because it’s not obvious where the data starts or ends; or what constitutes a character. Also, perhaps lack a valid Longitudinal Redundancy Check (LRC).

Identifying characters on hotel magstripes

I ran my credit-card code over the hotel mag stripes to see if there was odd parity with all the 5 bit chunks or all the 7 bit chunks. No, unfortunately not. Another reason the cheap magstripe reader probably failed.

Then I ran some frequency analysis on the 1′s and 0′s. Maybe a lot of the 5-bit chunks are all the same value? Or the 7-bit chunks? I’d see something similar on the credit card magstripe. There were 20 spaces on the 7-bit magstripe. So, using frequency analysis, I should have been able to guess 7-bit characters were being used.

By splitting some of the hotels magstripe data into 8-bit chunks, we notice that the same string of 1′s and 0′s occurs many more times than you’d expect. So my best guess is that 8-bit characters are used.

Below is some example output from my Python script. Not that it outputs:

  • Raw bits
  • Strings (with hex dumps) that would be right if 5-bit, 6-bit, 7-bit or 8-bit characters were used. Though “correctness” depends on parity, bit order and what character set the bit values map to. Plenty of room for error and improvement here
  • Checks for odd/even parity within characters
  • Frequency analysis looking for common characters – for varying character lengths
[+] Parsing file export.csv
 [-] Flips in channel 0: 3
 [-] Flips in channel 1: 1652
 [-] Flips in channel 2: 3
[+] Analysing swipes
 [-] Channel 0:
 [-] Channel 1:
 [-] Swipe 0: 693 bits
 [-] Swipe 1: 701 bits
 [-] Channel 2:
[+] Creating Plots
 [-] creating plots with dimensions (1637, 1637), (1637, 1637)
 [-] saving plot to file: export.csv-plot-channel-1.png
----------------------- CHANNEL 1 SWIPE 0 -----------------------
[+] Length (bits) = 266 (%5 = 1, %6 = 2, %7 = 0, % 8= 2)
[+] Data: 10100110001001100010011000100110001001100010011000100110001001100010011000100110001001100101011011100110101001001101100110100110001001100111110110111100111101100010000000100110011001101100001010010101001001001110110100000111001101011101100001100101101000101010011001
[+] Strings:
 [-] 5 bit: 5398621<4398621<43:>62<3539<;><=409<615549=1>6>36=1:6
 [-] length: 53
 [-] hex: 
 35 33 39 38 36 32 31 3c 34 33 39 38 36 32 31 3c 5398621<4398621<
 34 33 3a 3e 36 32 3c 33 35 33 39 3c 3b 3e 3c 3d 43:>62<3539<;><=
 34 30 39 3c 36 31 35 35 34 39 3d 31 3e 36 3e 33 409<615549=1>6>3
 36 3d 31 3a 36 6=1:6
[-] 6 bit: E(1C&,9RD(1CFM92;+1S;G;"D,-**DMPLW8M4,
 [-] length: 38
 [-] hex: 
 45 28 31 43 26 2c 39 52 44 28 31 43 46 4d 39 32 E(1C&,9RD(1CFM92
 3b 2b 31 53 3b 47 3b 22 44 2c 2d 2a 2a 44 4d 50 ;+1S;G;"D,-**DMP
 4c 57 38 4d 34 2c LW8M4,
[-] 7 bit: %..#...2$..#&-.....3.'..$....$-0,7.-.
 [-] length: 37
 [-] hex: 
 25 08 11 23 06 0c 19 32 24 08 11 23 26 2d 19 12 %..#...2$..#&-..
 1b 0b 11 33 1b 27 1b 02 24 0c 0d 0a 0a 24 2d 30 ...3.'..$....$-0
 2c 37 18 2d 14 ,7.-.
[-] 8 bit: .&&&&&&&&&&V....&}.. &f..$..5.e..@
 [-] length: 34
 [-] hex: 
 a6 26 26 26 26 26 26 26 26 26 26 56 e6 a4 d9 a6 .&&&&&&&&&&V....
 26 7d bc f6 20 26 66 c2 95 24 ed 07 35 d8 65 a2 &}.. &f..$..5.e.
 a6 40 .@
[+] Parity for 5 bit chars: odd: False, even: False)
[+] Parity for 6 bit chars: odd: False, even: False)
[+] Parity for 7 bit chars: odd: False, even: False)
[+] Parity for 8 bit chars: odd: False, even: False)
[+] Frequency analysis for potential char lengths:
 [-] 5 bits:
 10011: 5
 11000: 4
 01100: 4
 00110: 4
 00100: 4
 [-] 6 bits:
 100110: 5
 100010: 5
 011000: 5
 011001: 4
 001001: 4
 [-] 7 bits:
 1000100: 3
 0010011: 3
 1101100: 2
 1100010: 2
 1011010: 2
 [-] 8 bits:
 00100110: 12 < common 8-bit patter implies 8-bit chars?
 10100110: 3
...snip...

The above data is from an actual hotel card (though the hotel as since switched to using RFID locks).

I also started to look at XORing, rotating of characters (rot13 style), bit order and offsetting the whole stream of bits (in case I started at the wrong place when chunk the character up). All to no avail as yet.

That’s as far as I got. I’ll be sure to post again if I ever decode any of the data on the card. I was hoping to see my room number and checkout date in cleartext. But equally an opaque encrypted string wouldn’t surprise me either – which could be what I’m seeing on at least some of the cards.

Conclusion

Doing things the cheap and time-consuming way can be fun – and a good opportunity to write code, learn about matplotlib and dust off the logic analyser.

Further reading/viewing

If you want to know more about magstripes (before they become completely obsolete), take a look at:

I was pretty inspired by these projects.


Request to be added to the Portcullis Labs newsletter

We will email you whenever a new tool, or post is added to the site.

Your Name (required)

Your Email (required)