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:
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.
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.
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.
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:
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:
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:
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:
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:
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:
- Samy Kamkar’s magspoof tool (video link) – could be useful if you need your PC to transmit bad data in through a magnetic read head.
- DEF CON 24 – Weston Hecker – Hacking Hotel Keys and Point of Sale Systems
I was pretty inspired by these projects.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:
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.
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.
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.
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:
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:
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:
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:
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:
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:
- Samy Kamkar’s magspoof tool (video link) – could be useful if you need your PC to transmit bad data in through a magnetic read head.
- DEF CON 24 – Weston Hecker – Hacking Hotel Keys and Point of Sale Systems
I was pretty inspired by these projects.