├── LICENSE
├── README.md
├── formats
├── cad.md
├── card-specific
│ ├── 125khz
│ │ └── 125khz.md
│ ├── magstripe
│ │ └── magstripe.md
│ ├── mifare-classic.md
│ ├── mifare-desfire.md
│ └── mifare-plus.md
├── cardholder
│ ├── cardholder.md
│ └── substitution-table.bin
└── mes.md
├── misc-tools
├── README.md
├── atmel-uart-dump
│ ├── Makefile
│ └── ard.cpp
└── gdb.py
├── protocols
├── cardax-iv
│ ├── cardax-iv.md
│ ├── cdxiv-decode.py
│ ├── message1.png
│ ├── message2.png
│ └── message3.png
├── ernie-types.md
├── gbus
│ ├── gbus-decode.py
│ └── gbus.md
├── hbus
│ ├── hbus.md
│ └── unframe-hbus.py
└── reader-types.md
├── sdr
├── antenna.jpg
├── grc.png
├── plot.png
└── sdr.md
├── software.md
└── timing-attack
├── beetle.jpg
├── plot.png
├── sampler-grapher
├── README.md
├── requirements.txt
└── sampler-grapher.py
├── sampler
├── Makefile
├── README.md
└── main.cpp
└── timing-attack.md
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Matthew Daley
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## About
2 |
3 | This repository is a collection of notes and source code produced as part of research on the Gallagher (aka Cardax) access control system. It is intended for use by other security researchers, but may prove useful to system operators and installers as well.
4 |
5 | Each area of research (such as a protocol or format) is described in a separate directory. For an overview of the totality of the research, please see below.
6 |
7 | You can help! If you see errors or omissions in the notes or encounter problems with the code please send an issue and/or pull request :)
8 |
9 |
10 | ## Talk
11 |
12 | This research was presented as talk at [Kawaiicon](https://kawaiicon.org/), a New Zealand information security conference. **You can view the talk and see the slides [here](https://www.youtube.com/watch?v=brhXqyidiKo).**
13 |
14 |
15 | ## Overview for attackers
16 |
17 | This overview assumes that you are an unauthorised person wanting to gain access to a Gallagher-secured site (i.e. as part of a legal physical security audit). There is a well-defined process that must be undertaken, outlined here:
18 |
19 | ### 1. Get card credential data
20 |
21 | The key piece of data used as an authentication credential in the Gallagher system is what is defined here as "card credential data". This consists of a tuple of 4 items:
22 |
23 | * A 4-bit *region code* (RC), usually displayed as a letter from A-P.
24 | * A 16-bit *facility code* (FC), usually displayed alongside the region code (e.g. `A12345`).
25 | * A 24-bit *card number* (CN). These nominally start from 1 and are usually seen in the field to range up to ~50,000.
26 | * A 4-bit *issue level* (IL). These are intended to start at 1.
27 |
28 | The region and facility codes are intended to represent a unique site installation, while the card number and issue level represent a single card credential. The issue level starts at 1 for any given card credential and is intended to be incremented each time the credential needs to be re-issued (e.g. due to a card being lost or stolen).
29 |
30 | There are several ways to gain access to this data, but these usually either target a site's cards or readers.
31 |
32 | #### Targeting site cards
33 |
34 | The obvious solution is to attempt to read these credentials off an existing valid card. This repository has the [formats](formats/) for each kind of supported card in the Gallagher system. To gain access to the data in the first place, one of three forms of attacks can be performed:
35 |
36 | ##### Reading a card directly
37 |
38 | If you have access to a valid credential, it is a simple matter of reading the card using a tool such as a Proxmark3 (not gonna try and link one of the many variants) to dump the required encoding/sectors/files off the card. As not all kinds of cards are fully supported for reading, it may be necessary to manually send commands (APDUs) to some kinds of card to read them.
39 |
40 | ##### Reading a card from a distance (skimming)
41 |
42 | It is well-known that it is possible to read both low-freqency and high-frequency RFID cards from some distance. Possible tools to do so include a Proxmark3 or a software-defined radio (SDR) with a suitable antenna.
43 |
44 | ##### Observing a card-reader transaction from a distance (interception)
45 |
46 | The limitation in performing the previously-described skimming attack is on powering the card from a distance. The power required to energise the card increases with distance, making it almost impossible at reasonable distances, especially for high-frequency cards.
47 |
48 | The alternative is to intercept a transaction between a card and a legitimate reader. While the reader powers the card only a short distance, the actually communications between the card and reader can be seen from a much longer distance, allowing this attack to be more useful in the field. To do so, a software-defined radio (SDR) with a suitable long-range antenna and software to transcieve signals was the approach taken as part of this research and is outlined [here](sdr/sdr.md).
49 |
50 | #### Targeting site controllers
51 |
52 | Less obvious is the ability to ascertain the required card credential information from legitmate controllers themselves. Readers are connected (eventually) to controllers, meaning that if one has physical access to a reader, there is also access to the controller via the reader-controller connection. The protocols used in these connections are outlined [here](protocols/). With knowledge of these protocols it is possible to perform a brute-force attack upon the controller. With hints leaked via a timing-based side channel, it is possible to narrow down the otherwise large search space and find valid facility codes and card numbers in a feasible amount of time. This approach is outlined [here](timing-attack/timing-attack.md).
53 |
54 | ### 2. Decode, modify, and re-encode card data (optional)
55 |
56 | The card credential data obtained in step 1 will not be the raw tuple given earlier, but instead an obfuscated block of data. While not necessary, it may be desired to decode this data into the raw tuple so that it is possible to modify the individual credential elements. For example, once one card credential is gained, it is possible to decode it, modify the card number, and re-encode it. This allows the capture of one card's data to lead to compromise of all cards for that site, potentially allowing an escalation of privilege.
57 |
58 | The encoded format of the card credential data is given [here](formats/cardholder/cardholder.md) alongside [scripts](formats/cardholder/) for decoding and encoding the data to and from its constituent parts.
59 |
60 | ### 3. Send card credential data
61 |
62 | The last step is to make use of captured (and potentially modified) data. To do so, one can encode the data to a new physical RFID card, or simply replay it (aka. emulate it) to a reader.
63 |
64 | #### Encoding
65 |
66 | It is possible to use devices such as a Proxmark3 to encode several formats of RFID card. Since the support is not as extensive as it is for reading, it may be necessary to again manually send commands (APDUs) to some kinds of card to encode them.
67 |
68 | #### Replaying (emulation)
69 |
70 | The same devices usually support replaying the card data over the air to a reader which cuts down on the effort required.
71 |
72 |
73 | ## Overview for defenders
74 |
75 | This overview assumes that you are an authorised person wanting to secure access to a Gallagher-secured site (i.e. as part of a facility security team).
76 |
77 | The attacks given in this research mainly come down to the use of weak RFID card formats and settings. As outlined by Gallagher, the only form of MIFARE-based credential that is currently considered secure against attack is the MIFARE DESFire or MIFARE Plus card **with** a non-default MIFARE site key. (There are non-MIFARE credentials that are also considered secure, such as PIV or FIDO-based ones).
78 |
79 | If other forms of card (i.e. Cardax LF or MIFARE Classic) are in use, it is trivial to bypass the protection (if any) the card attempts to mount against an illegitate reader from reading the card credential data.
80 |
81 | If a MIFARE DESFire or MIFARE Plus card is in use on a site *but* the default MIFARE site key is used, then it is still possible, with knowledge of this default site key, to read card credential data from the cards.
82 |
83 | Gallagher provides resources to system operators and installers looking to secure their system:
84 |
85 | * Hardening guides, namely the *Gallagher Command Centre Hardening Guide* and the *Gallagher Controller 6000 Hardening Guide*, provided as part of the documentation of a Command Centre installation.
86 | * The *[Gallagher Security Health Check](https://security.gallagher.com/products/security-health-check)*. This first-of-its-kind tool allows information to be gathered on the security posture of a site, where it can then be sent to Gallagher for analysis and transformation into a [report](https://security.gallagher.com/media/2129/shc-sample-report.pdf). All of the recommendations made above are checked by this tool, and I highly recommend its use.
87 |
88 |
89 | ## Future directions
90 |
91 | While the information found so far is definitely useful in the field, there are more technologies and subsystems to look at! Eventually, I would like to additionally look at:
92 |
93 | * The HBUS protocol in more depth (i.e. beyond the authentication phase)
94 | * Bluetooth credentials
95 | * PIV credentials (unlikely though - requires specialised hardware)
96 |
97 |
98 | ## Licensing
99 |
100 | All source code in this repository is [MIT licensed](LICENSE).
101 |
--------------------------------------------------------------------------------
/formats/cad.md:
--------------------------------------------------------------------------------
1 | # Card Application Directory
2 |
3 | ## Purpose
4 |
5 | The Gallagher system supports having multiple site-specific credentials encoded onto a single MIFARE card.
6 |
7 | MIFARE cards have the *[MIFARE Application Directory](https://www.nxp.com/docs/en/application-note/AN10787.pdf)* (MAD), a NXP-defined standard for a per-card directory that allows each sector on the card to be identified. However, each sector can only be identified by a 2 byte *application ID* (Gallagher uses `0x4811` and `0x4812`). This does not allow an indication as to which sector contains which cardholder credential for a given site (region code (RC) and facility code (FC) pair).
8 |
9 | Hence, the Gallagher-specific *Card Application Directory* (CAD) exists. This directory takes up one of the sectors on the card and allows such a mapping of (RC, FC) to sector numbers to be held. This allows readers to quickly find which sector holds the cardholder credential for a given site without the need to read each sector, avoiding delays when a user presents a card to a reader for access.
10 |
11 | If you have access to Gallagher documentation, this is described (but the format not outlined) in the *Encoding MIFARE Multiple Applications* document.
12 |
13 |
14 | ## Format
15 |
16 | The directory is encoded onto one sector (64 bytes) as follows:
17 |
18 | ### Header
19 |
20 | * CRC (2 bytes): This is a CRC-16 with values as follows over the next 0x2E bytes of the directory (i.e. the rest of blocks 0-2):
21 | - Polynomial: 0x8408 (0x18408)
22 | - Initial value: 0xFFFF
23 | - Input bytes reflected
24 |
25 | * Unknown (1 byte): only seen to be `0x00` in the field.
26 |
27 | * Unknown (1 byte): only seen to be `0x01` in the field.
28 |
29 | ### Mappings
30 |
31 | Next, 12 (RC, FC) -> sector number mappings are encoded. Each is 3.5 bytes long, giving a total of 3.5 * 12 = 42 = 0x2A bytes. Each one is as follows:
32 |
33 | * Region code (4 bits)
34 |
35 | * Facility code (2 bytes)
36 |
37 | * Sector number (1 byte). A sector number of 0 indicates that there are no more mappings (0 can't be a cardholder credential sector as it's reserved for the MAD).
38 |
39 | ### Padding
40 |
41 | Lastly, the last 2 bytes are set to 0, completing blocks 0 - 2.
42 |
43 | ### Block 3
44 |
45 | Block 3 of the sector is set in the usual MIFARE-specific way, with the following settings:
46 |
47 | * Key A: `0xA0A1A2A3A4A5` (This is the same as the MAD's key A.)
48 |
49 | * Access rights: `0x787788`. [This indicates](https://cardinfo.barkweb.com.au/index.php?location=19&sub=20):
50 | - Key A: read access to data blocks and access bits
51 | - Key B: read access to data blocks and access bits, and write access to data blocks and keys
52 |
53 | * User byte: `0xC1` (but `0x00` seen in the field)
54 |
55 | * Key B: `0xB0B1B2B3B4B5` (This is the same as the MAD's key B.)
56 |
57 |
58 | ## Example
59 |
60 | Here's an example CAD dumped from a MIFARE Classic Gallagher card:
61 |
62 | ```
63 | 1B 58 00 01 C1 33 70 FD 13 38 0D 00 00 00 00 00
64 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
65 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
66 | 00 00 00 00 00 00 78 77 88 00 00 00 00 00 00 00
67 | ```
68 |
69 | We can check the CRC (0x1B58) by calculating it over bytes 0x2 - 0x2F:
70 |
71 | ```bash
72 | $ echo """
73 | 00 01 C1 33 70 FD 13 38 0D 00 00 00 00 00
74 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
75 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
76 | """ | xxd -r -p | python3 -c """
77 | import sys, crcmod
78 | print(hex(crcmod.mkCrcFun(0x18408, 0xFFFF)(sys.stdin.buffer.read())))
79 | """
80 | 0x1b58
81 | ```
82 |
83 | We can see two mappings, one with value `0xC13370F`, and another with value `0xD13380D`. These can be parsed into the following:
84 |
85 | | RC | FC | Sector |
86 | |-------|----------|--------|
87 | | `0xC` | `0x1337` | `0x0F` |
88 | | `0xD` | `0x1338` | `0x0D` |
89 |
90 | Giving:
91 |
92 | 1. (RC 0xC (M), FC 0x1337 = 4919) has a cardholder credential in sector 0x0F
93 | 2. (RC 0xD (N), FC 0x1338 = 4920) has a cardholder credential in sector 0x0D.
94 |
--------------------------------------------------------------------------------
/formats/card-specific/125khz/125khz.md:
--------------------------------------------------------------------------------
1 | # Cardax 125kHz
2 |
3 | ## About
4 |
5 | Low-frequency (125kHz) Cardax cards use this format. The cards output manchested encoded data at a RF/32 clock speed (that is, there are 32 RF cycles per bit, giving a data rate of 125kHz / 32 ~= 3.9kHz)
6 |
7 |
8 | ## Format
9 |
10 | These cards output a simple stream of bits:
11 |
12 | First, the fixed sequence `0111111111101010` (`0x7FEA`) is emitted.
13 |
14 | This is followed by the [8 byte cardholder credential data](../../cardholder/cardholder.md) that has been expanded so that every 8 bits are followed by the inverse of the least significant bit.
15 |
16 | Finally, a 1 byte CRC of the (unexpanded) credential data follows, with settings:
17 |
18 | * Initial value = 0x2C
19 | * Polynomial = 0x107
20 |
21 |
22 | ## Example
23 |
24 | ### Encoding
25 |
26 | Assume that we have the same cardholder credential data as given in the encoding example [here](../../cardholder/cardholder.md#Decoding), that is (RC 0 (A), FC 9876, CN 1234, IL 1). Using the procedure described there, we get the final encoded data of `0xA397935AA380A349`.
27 |
28 | Then this is output by a 125kHz card as follows:
29 |
30 | * Fixed sequence: `0111111111101010`
31 |
32 | * Expanded cardholder credential bytes:
33 | - `A3` -> `10100011 0`
34 | - `97` -> `10010111 0`
35 | - `93` -> `10010011 0`
36 | - `5A` -> `01011010 1`
37 | - `A3` -> `10100011 0`
38 | - `80` -> `10000000 1`
39 | - `A3` -> `10100011 0`
40 | - `49` -> `01001001 0`
41 |
42 | * CRC: `00101011` (0x2B)
43 |
44 | Collating these bits up into bytes, we get the value: `0x7FEAA34BA4CB5A34068C922B`.
45 |
46 | ### Decoding
47 |
48 | Take the following bitstream from a card:
49 |
50 | ```
51 | 011111111110101010100011010001010110001010101001
52 | 011010100011010100011010100011000101100111010010
53 | ```
54 |
55 | As expected, the first 12 bits are the fixed sequence `0111111111101010`. We can then split out the expanded cardholder credential bytes as
56 |
57 | ```
58 | 101000110100010101100010101010010110101000110101000110101000110001011001
59 | ```
60 |
61 | and the CRC as `11010010`.
62 |
63 | Aligning the expanded bytes as done in the encoding section gives:
64 |
65 | ```
66 | 10100011 0
67 | 10001010 1
68 | 10001010 1
69 | 01001011 0
70 | 10100011 0
71 | 10100011 0
72 | 10100011 0
73 | 00101100 1
74 | ```
75 |
76 | As expected, each byte of the original cardholder credential data is followed by the inverse of its least significant bit, which we can discard. This gives the cardholder data `0xA38A8A4BA3A3A32C`.
77 |
78 | The CRC, 0xD2, matches what is expected for the CRC over the data `0xA38A8A4BA3A3A32C`.
79 |
80 | Finally, we can decode the cardholder data using the procedure outlined [here](../../cardholder/cardholder.md#Example). There, we can see that the encoded cardholder data `0xA38A8A4BA3A3A32C` corresponds to the cardholder credential (RC 4 (D), FC 2222, CN 1111, IN 3) (and all unknown fields set to 0).
81 |
--------------------------------------------------------------------------------
/formats/card-specific/magstripe/magstripe.md:
--------------------------------------------------------------------------------
1 | # Cardax Magstripe
2 |
3 | ## About
4 |
5 | Although barely seen out in the field anymore, the Cardax system at one time supported magstripe-based cards. These don't use the common [card credential format](../../cardholder/cardholder.md) that the other cards do, but instead use a magstripe-specific format.
6 |
7 | ## Format
8 |
9 | Only [track 1](https://en.wikipedia.org/wiki/Magnetic_stripe_card#Financial_cards) of the card is used, and uses the DEC SIXBIT encoding.
10 |
11 | ### Step 1
12 |
13 | First, the following block of data is awkwardly generated by slicing up the various-length data items into five-bit characters:
14 |
15 | | | Char 0 | Char 1 | Char 2 | Char 3 | Char 4 | Char 5 | Char 6 | Char 7 |
16 | |---------|--------|--------|--------|--------|--------|--------|--------|-------- |
17 | | **+0** | RC | FC11-15 | FC6-10 | FC1-5 | FC0,
`0x00` | `0x00` | `0x00` | `0x00` |
18 | | **+8** | `0x00` | `0x00` | `0x00` | CN13-15 | CN8-12 | CN3-7 | CN0-2,
IL2-3 | IL0-1,
`0x00` |
19 | | **+16** | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00`,
CS14-15 | CS9-13 |
20 | | **+24** | CS4-8 | CS0-3,
`0x00` |
21 |
22 | Note that *XY-Z* indicates the *Yth* to *Zth* bits of *X*, and *A, B* indicates the two values *A* and *B* concatenated into a single character.
23 |
24 | Note that the CN in this case is limited to 16 bits, hence magstripe card numbers can only range up to 65,535 (nominally 50,000).
25 |
26 | Here, CS stands for the checksum, which is given by the following algorithm:
27 |
28 | ```
29 | r <- 0
30 | for c in input:
31 | r <- (r ^ 0xA5963C) << 8
32 | repeat 24 times:
33 | r <- r << 1
34 | if no carry out:
35 | r <- r ^ c
36 | c <- c ^ 0xFF
37 | result <- r & 0xFFFF
38 | ```
39 |
40 | This algorithm is run over the following (8-bit byte) input:
41 |
42 | | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 |
43 | |---------|----|----|----|----|----|----|----|---- |
44 | | **+0** | `P` | `A` | `1` | RC | FC8-15 | FC0-7 | `0x00` | `0x00` |
45 | | **+8** | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | CN8-15 |
46 | | **+16** | CN0-7 | IL | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` |
47 | | **+24** | `0x00` | `0x00` |
48 |
49 | ### Step 2
50 |
51 | The index of each character is added to each character itself, modulo 0x1F. That is:
52 | * For the 0th character, nothing changes.
53 | * For the 1st character, 1 is added.
54 | * For the 2nd character, 2 is added.
55 | * And so on...
56 |
57 | ### Step 3
58 |
59 | The entire block is mapped through a substitution table, where each 7-bit character produces the final output character:
60 |
61 | | | ..000 | ..001 | ..010 | ..011 | ..100 | ..101 | ..110 | ..111 |
62 | |-----------|-------|-------|-------|-------|-------|-------|-------|-------|
63 | | **00...** | `4` | `V` | `/` | `Q` | `5` | `A` | `0` | ` ` |
64 | | **01...** | `.` | `9` | `W` | `(` | `2` | `I` | `-` | `X` |
65 | | **10...** | `S` | `U` | `G` | `8` | `J` | `7` | `Y` | `P` |
66 | | **11...** | `O` | `R` | `H` | `T` | `E` | `1` | `B` | `Z` |
67 |
68 | ### Step 4
69 |
70 | Finally, the resulting track 1 data is simply (!) the string `PA1` followed by the mapped data.
71 |
72 |
73 | ## Example
74 |
75 | Take the following data read off track 1 of a magstripe card: `PA12Q-BJA0 .9WGE-/ SUG8J7P7SU`
76 |
77 | We can see and strip off the fixed `PA1` header, leading to the mapped data `2Q-BJA0 .9WGE-/ SUG8J7P7SU`. Unmapping this through the substitution table, we get the bitstream:
78 |
79 | ```
80 | 01100 00011 01110 11110 10100 00101 00110 00111 01000 01001
81 | 01010 10010 11100 01110 00010 00111 10000 10001 10010 10011
82 | 10100 10101 10111 10101 10000 10001
83 | ```
84 |
85 | We then subtract the position of each character from the characters themselves, giving:
86 |
87 | ```
88 | 01100 00010 01100 11011 10000 00000 00000 00000 00000 00000
89 | 00000 00111 10000 00001 10100 11000 00000 00000 00000 00000
90 | 00000 00000 00001 11110 11000 11000
91 | ```
92 |
93 | This can be arranged as follows:
94 |
95 | | | Char 0 | Char 1 | Char 2 | Char 3 | Char 4 | Char 5 | Char 6 | Char 7 |
96 | |---------|--------|--------|--------|--------|--------|--------|--------|--------|
97 | | **+0** | RC | FC11-15 | FC6-10 | FC1-5 | FC0,
`0x00` | `0x00` | `0x00` | `0x00` |
98 | | | 01100 | 00010 | 01100 | 11011 | 10000 | 00000 | 00000 | 00000 |
99 | | **+8** | `0x00` | `0x00` | `0x00` | CN13-15 | CN8-12 | CN3-7 | CN0-2,
IL2-3 | IL0-1,
`0x00` |
100 | | | 00000 | 00000 | 00000 | 00111 | 10000 | 00001 | 10100 | 11000 |
101 | | **+16** | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00`,
CS14-15 | CS9-13 |
102 | | | 00000 | 00000 | 00000 | 00000 | 00000 | 00000 | 00001 | 11110 |
103 | | **+24** | CS4-8 | CS0-3,
`0x00` |
104 | | | 11000 | 11000 |
105 |
106 | Reading off the fields, we get:
107 |
108 | * RC = 0b1100 = 12 (M)
109 | * FC = 0b0001001100110111 = 0x1337 = 4919
110 | * CN = 0b1111000000001101 = 0xF00D = 61453
111 | * IL = 0b0011 = 3
112 | * CS = 0b0111110110001100 = 0x7D8C
113 |
114 | We can verify the checksum by arranging the data for the checksum input:
115 |
116 | | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 |
117 | |---------|----|----|----|----|----|----|----|---- |
118 | | **+0** | `P` | `A` | `1` | RC | FC8-15 | FC0-7 | `0x00` | `0x00` |
119 | | | `P` | `A` | `1` | `0x0C` | `0x13` | `0x37` | `0x00` | `0x00` |
120 | | **+8** | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | CN8-15 |
121 | | | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0xF0` |
122 | | **+16** | CN0-7 | IL | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` |
123 | | | `0x0D` | `0x3` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` | `0x00` |
124 | | **+24** | `0x00` | `0x00` |
125 | | | `0x00` | `0x00` |
126 |
127 | Then run it on the checksum algorithm:
128 |
129 | ```python
130 | >>> d = 'PA1'.encode('ascii')
131 | >>> d += bytearray.fromhex('0C 13 37 00 00 00 00 00 00 00 00 00 F0 0D 03 00 00 00
132 | 00 00 00 00 00')
133 | >>> r = 0
134 | >>> for c in d:
135 | ... r = (r ^ 0xA5963C) << 8
136 | ... for _ in range(24):
137 | ... r <<= 1
138 | ... if not r & (1 << 32):
139 | ... r ^= c
140 | ... c ^= 0xFF
141 | ...
142 | >>> hex(r & 0xFFFF)
143 | '0x7d8c'
144 | ```
145 |
--------------------------------------------------------------------------------
/formats/card-specific/mifare-classic.md:
--------------------------------------------------------------------------------
1 | # MIFARE Classic
2 |
3 | ## About
4 |
5 | The first of the MIFARE card range supported by the Gallagher system was the [MIFARE Classic](https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-classic:MC_41863).
6 |
7 |
8 | ## Sectors
9 |
10 | The cards will have a valid [MIFARE Application Directory](https://www.nxp.com/docs/en/application-note/AN10787.pdf). The following application IDs are used by the Gallagher system:
11 |
12 | * `0x4811`: [Card Application Directory](../cad.md) (CAD) sector
13 | * `0x4812`: Site-specific card data sector (see below).
14 |
15 | There will be at least one cardholder credential data sector; usually this is sector 15.
16 |
17 | If there is a CAD sector, it will usually be in sector 14.
18 |
19 | Finally, there may be additional cardholder credential data sectors; usually these start at sector 13 and move downward.
20 |
21 |
22 | ## Site-specific Card Data Sector
23 |
24 | This sector holds a cardholder information for a specific site. The format is as follows:
25 |
26 | ### Block 0
27 |
28 | Contains an 8 byte [cardholder credential data](../cardholder/cardholder.md) block, followed by its bitwise inverse.
29 |
30 | ### Block 1
31 |
32 | Contains the literal string www.cardax.com (note the 2 padding spaces).
33 |
34 | ### Block 2
35 |
36 | If enabled for the site, contains a 16 byte [MIFARE Enhanced Security](../mes.md) block. Otherwise, should contain all zeroes. However, cards have been seen in the field that appear to contain uninitialised data from the stack during the encoding process!
37 |
38 | ### Block 3
39 |
40 | Block 3 is set in the usual MIFARE-specific way, with the following settings:
41 |
42 | * Key A: `0x160A91D29A9C`
43 |
44 | * Access rights: `0x787788`. [This indicates](https://cardinfo.barkweb.com.au/index.php?location=19&sub=20):
45 | - Key A: read access to data blocks and access bits
46 | - Key B: read access to data blocks and access bits, and write access to data blocks and keys
47 |
48 | * User byte: `0x1D` if MES present, else `0xC1`
49 |
50 | * Key B: `0xB7BF0C13066E`
51 |
52 |
53 | ## Example
54 |
55 | Here's an example MIFARE Classic card, sector-by-sector.
56 |
57 | First, sector 0 contains the MAD:
58 |
59 | ```
60 | E3 51 54 3C DA 08 04 00 01 6F 01 6D 45 68 F8 1D
61 | BD 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
62 | 00 00 00 00 00 00 00 00 00 00 12 48 11 48 12 48
63 | 00 00 00 00 00 00 78 77 88 C1 00 00 00 00 00 00
64 | ```
65 |
66 | As we can see, sector 14 holds a CAD (AID `0x4811`) and sectors 13 and 15 site-specific data (AID `0x4812`).
67 |
68 | Looking at sector 14, we can see the CAD:
69 |
70 | ```
71 | 1B 58 00 01 C1 33 70 FD 13 38 0D 00 00 00 00 00
72 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
73 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
74 | 00 00 00 00 00 00 78 77 88 00 00 00 00 00 00 00
75 | ```
76 |
77 | [Decoding the CAD](../cad.md#example), we see the following information:
78 |
79 | | RC | FC | Sector |
80 | |-------|----------|--------|
81 | | `0xC` | `0x1337` | `0x0F` |
82 | | `0xD` | `0x1338` | `0x0D` |
83 |
84 | Sector 15 gives the actual credential data:
85 |
86 | ```
87 | A3 B4 B0 C1 51 B0 A3 1B 5C 4B 4F 3E AE 4F 5C E4
88 | 77 77 77 2E 63 61 72 64 61 78 2E 63 6F 6D 20 20
89 | 4B C7 41 C0 E3 A7 63 B6 F1 29 84 8C 56 44 AC B0
90 | 00 00 00 00 00 00 78 77 88 1D 00 00 00 00 00 00
91 | ```
92 |
93 | Here we see the card credential data of `0xA3B4B0C151B0A31B` and a MES block of `0x4BC741C0E3A763B6F129848C5644ACB0`.
94 |
95 | The card credential data [decodes](../cardholder/cardholder.md) to (RC 12 (M), FC 0x1337 = 4919, CN 0xF00D = 61453, IL 1).
96 |
--------------------------------------------------------------------------------
/formats/card-specific/mifare-desfire.md:
--------------------------------------------------------------------------------
1 | # MIFARE DESFire
2 |
3 | ## About
4 |
5 | The [MIFARE DESfire](https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-desfire:MC_53450) card is the latest MIFARE version supported by the Gallagher system. When a site has been configured with a non-default *MIFARE Site Key*, the use of such cards is considered secure by Gallagher (and by this research!)
6 |
7 | These cards hold data in a different structure than the MIFARE Classic and MIFARE Plus cards; instead of a sector/block-based memory layout, they hold individual files and keys in application (i.e. card use-case) specific structures.
8 |
9 |
10 | ## Keys
11 |
12 | The keys used in the applications are either fixed or generated by diversifying the MIFARE Site Key using application- and key-specific data, as outlined below.
13 |
14 | The algorithm appears to have meant to be the NXP-defined [AN10922](https://www.nxp.com/docs/en/application-note/AN10922.pdf) algorithm, but it has a slight difference: the last output block of diversification input is re-ciphered before XORing with K2 (using the notation in the document linked above). Whether this is intentional or a mistake is hard to tell.
15 |
16 | For most keys, the input to the diversification algorithm can take one of two forms:
17 |
18 | * Card serial number (CSN) included: in which case, the input is the 4 byte CSN, followed by the 1 byte key number, followed by the 3 byte AID.
19 |
20 | * Card serial number excluded: in which case, the input is the 1 byte key number, followed by the 3 byte AID.
21 |
22 | Finally, the default application key is the result of diversifiying the bytes `0x03 0x00 0x00 0x00`.
23 |
24 |
25 | ## Applications
26 |
27 | A Gallagher-encoded DESFire card will have at least two applications, and potentially more if more than one site is supported by the card.
28 |
29 | ### Card application
30 |
31 | The card-wide card application has the NXP-defined AID of `0x000000`. It has one key:
32 |
33 | * Key 0x0: Card Master Key (CMK): diversified without the CSN.
34 |
35 | ### Cardax card data application
36 |
37 | These applications have an application ID (AID) between `0xF48120` and `0xF4812B`, inclusive. Note that these use an NXP-defined mapping of Gallagher's 2 byte AIDs used in the MIFARE Classic and MIFARE Plus MIFARE Application Directory (MAD) into the 3 byte AIDs used in DESFire.
38 |
39 | The application has a configuration byte of `0x0B`.
40 |
41 | There are three keys defined for the application:
42 |
43 | * Key 0x0: Application Master Key (AMK): diversified with the CSN.
44 | * Key 0x1: "UID Discovery": diversified without the CSN.
45 | * Key 0x2: Cardax read: diversified with the CSN.
46 |
47 | There are two files:
48 |
49 | * File 0x0: "Cardax standard". This contains an 8 byte [cardholder credential data](../cardholder/cardholder.md) block, followed by its bitwise inverse (like block 0 of a [MIFARE Classic](mifare-classic.md) card). It has permissions `0x2000`.
50 | * File 0x1: "Cardax enhanced". This contains a 16 byte [MIFARE Enhanced Security](../mes.md) block, if enabled for the site (otherwise the file is not present). It also has permissions `0x2000`.
51 |
52 | ### Card Application Directory (CAD)
53 |
54 | The card application directory application (!) has AID `0xF4812F` (again, this is a mapped 2 byte ID). The application holds data similarly to (but *not* the same format as) the MIFARE Classic and MIFARE Plus [card application directories](cad.md).
55 |
56 | The application has a configuration byte of `0x0B`.
57 |
58 | There is one key defined for the application:
59 |
60 | * Key 0x0: Application Master Key (AMK).
61 |
62 | There is at least one and up to three files:
63 |
64 | * Files 0x0 - 0x2: CAD. This contains up to six entries, each of which contain:
65 | - 1 byte RC
66 | - 2 byte FC
67 | - 3 byte AID, written in reverse byte order.
68 |
69 | It has permissions `0xE000`.
70 |
71 | ### General user info
72 |
73 | This application, defined by NXP in [AN10787](https://www.nxp.com/docs/en/application-note/AN10787.pdf), contains general informtion on the holder and uses of the card. It has AID `0xFFFFFF`.
74 |
75 | There is one key defined for the application:
76 |
77 | * Key 0x0: Application Master Key (AMK): diversified with the CSN.
78 |
79 | There are two files defined:
80 |
81 | * File 0x0: MAD version. Contains the bytes `0x00 0x00 0x03` and has permissions `0xE000`.
82 | * File 0x2: card publisher. Contains the string www.cardax.com followed by a `0x00` byte (note the two spaces, and has permissions `0xE000`.
83 |
84 |
85 | ## Example
86 |
87 | Here is the contents of an example MIFARE DESFire card:
88 |
89 | ### Application `0xF48120`
90 |
91 | #### File `0x0`
92 |
93 | Contains `A3 B4 B0 C1 51 B0 A3 34 5C 4B 4F 3E AE 4F 5C CB`.
94 |
95 | We can see the cardholder credential block `0xA3B4B0C151B0A334` (followed by its bitwise inverse), which [decodes to](../cardholder/cardholder.md) (RC 12 (M), FC 0x1337 = 4919, CN 0xF00D = 61453, IL = 3).
96 |
97 | #### File `0x1`
98 |
99 | Contains `1A D0 8D D6 2F B3 E4 38 BE 7A 05 E7 CB 0B 1B C7`.
100 |
101 | This is a [MES block](../mes.md).
102 |
103 | ### Application `0xF48121`
104 |
105 | #### File `0x0`
106 |
107 | Contains `A3 B4 B0 C8 51 B0 A3 A2 5C 4B 4F 37 AE 4F 5C 5D`.
108 |
109 | We can see the cardholder credential block `0xA3B4B0C851B0A3A2` (followed by its bitwise inverse), which [decodes to](../cardholder/cardholder.md) (RC 13 (N), FC 0x1338 = 4920, CN 0xF00E = 61454, IL = 1).
110 |
111 | #### File `0x1`
112 |
113 | Contains `1A D0 8D D6 2F B3 E4 38 BE 7A 05 E7 CB 0B 1B C7`.
114 |
115 | This is a [MES block](../mes.md).
116 |
117 | ### Application `0xF4812F`
118 |
119 | Contains:
120 |
121 | ```
122 | 0C 13 37 20 81 F4 0D 13 38 21 81 F4 00 00 00 00
123 | 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
124 | 00 00 00 00
125 | ```
126 |
127 | We can see 2 6-byte entries: `0x0C13372081F4` and `0x0D13382181F4`, which [decode to](#card-application-directory-cad):
128 |
129 | | RC | FC | AID |
130 | |-----|----------|------------|
131 | | `C` | `0x1337` | `0x2081F4` |
132 | | `D` | `0x1338` | `0x2181F4` |
133 |
134 | This corresponds with what we read in the previous application files.
135 |
136 | ### Application `0xFFFFFF`
137 |
138 | This file contains the [general user info](#general-user-info):
139 |
140 | #### File `0x0`
141 |
142 | Contains `00 00 03`, as expected.
143 |
144 | #### File `0x2`
145 |
146 | Contains `77 77 77 2e 63 61 72 64 61 78 2e 63 6f 6d 20 20 00`, as expected.
147 |
--------------------------------------------------------------------------------
/formats/card-specific/mifare-plus.md:
--------------------------------------------------------------------------------
1 | # MIFARE Plus
2 |
3 | ## About
4 |
5 | The second of the MIFARE card range supported by the Gallagher system was the [MIFARE Plus](https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-plus:MC_57609).
6 |
7 | This card acts very similarly to a MIFARE Classic card except for one key area: the security subsystem. The CRYPTO1-based encryption and the 6 byte keys have been replaced with 128 bit AES in a backwards-compatible way.
8 |
9 |
10 | ## Sectors
11 |
12 | The actual data held on a MIFARE Plus card is identical to that of a MIFARE Classic card, so refer to [that](mifare-classic.md) documentation for more on this.
13 |
14 |
15 | ## Keys
16 |
17 | The area that *has* changed is the keys. The fixed keys used previously have been replaced with diversified keys generated from the card's serial number (CSN) and a site-specific *MIFARE Site Key*.
18 |
19 | The algorithm used to diversify the keys is the same as that used in the MIFARE DESFire cards, so refer to [that](mifare-desfire.md) documentation for the algorithm.
20 |
21 | The inputs used for the algorithm to diversify the keys for each sector are as follows:
22 |
23 | For the following keys, the input is simply the 2 byte key number (aka the sector the key is stored in):
24 |
25 | * MAD key B (key numbers 0x4001 and 0x4021)
26 | * Proximity check (key number 0xA001)
27 | * VC polling encryption (key number 0xA080)
28 | * VC polling MAC (key number 0xA081)
29 |
30 | For the following keys, the input is the 4 byte CSN followed by the 2 byte key number:
31 |
32 | * Non-MAD sector key A/B, card master (key number 0x9000)
33 | * Card config (key number 0x9001)
34 | * L2 switch (key number 0x9002)
35 | * L3 switch (key number 0x9003)
36 | * SL1 card auth (key number 0x9004)
37 | * Select VC (key number 0xA000)
38 |
39 | Finally, these are fixed keys:
40 |
41 | * MAD sector A (key numbers 0x4000 and 0x4020): `0xA0A1A2A3A4A5A6A7A0A1A2A3A4A5A6A7`
42 |
--------------------------------------------------------------------------------
/formats/cardholder/cardholder.md:
--------------------------------------------------------------------------------
1 | # Cardholder Credential Format
2 |
3 | ## Data
4 |
5 | The key piece of data used to represent a cardholder in a Gallagher system is a "cardholder credential". This consists of a tuple of items:
6 |
7 | * A 4-bit *region code* (RC). Usually displayed as a letter from `A`-`P`.
8 | * A 16-bit *facility code* (FC). Usually displayed alongside the region code (e.g. `A12345`).
9 | * A 24-bit *card number* (CN). These nominally start from 1 and are usually seen in the field to range up to ~50,000.
10 | * A 4-bit *issue level* (IL). These are intended to start at 1 and be incremented each time the credential needs to be re-issued (e.g. due to a card being lost or stolen).
11 |
12 | The region and facility codes pair represents a unique site installation, while the card number and issue level pair represents a unique physical card.
13 |
14 | There are also some unknown data items referred to in firmware which have only ever been seen set to 0 in the field, here labeled UB, UC, UD, UE and UX (for historical research reasons). It appears to be safe to always set these to 0.
15 |
16 |
17 | ## Encoding
18 |
19 | This tuple of data is not encoded directly onto cards but is instead obfuscated into an 8 byte format.
20 |
21 | First, the items (but not UX) are arranged into 8 bytes as follows:
22 |
23 | | | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
24 | |------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|-----------------|
25 | | **Byte 0** | CN23 | CN22 | CN21 | CN20 | CN19 | CN18 | CN17 | CN16 |
26 | | **Byte 1** | FC11 | FC10 | FC9 | FC8 | FC7 | FC6 | FC5 | FC4 |
27 | | **Byte 2** | CN10 | CN9 | CN8 | CN7 | CN6 | CN5 | CN4 | CN3 |
28 | | **Byte 3** | CN2 | CN1 | CN0 | RC3 | RC2 | RC1 | RC0 | UB3 |
29 | | **Byte 4** | UB2 | UB1 | UB0 | CN15 | CN14 | CN13 | CN12 | CN11 |
30 | | **Byte 5** | UE3 | UE2 | UE1 | UE0 | FC15 | FC14 | FC13 | FC12 |
31 | | **Byte 6** | UC3 | UC2 | UC1 | UC0 | UD3 | UD2 | UD1 | UD0 |
32 | | **Byte 7** | FC3 | FC2 | FC1 | FC0 | IL3 | IL2 | IL1 | IL0 |
33 |
34 | (Here, *XY* represents the *Y*th bit of *X*, where the 0th is the least significant bit.)
35 |
36 | Then, the 8 resultant bytes are mapped through a substitution table (aka an [S-box](https://en.wikipedia.org/wiki/S-box)) to produce the final 8 byte output:
37 |
38 | | | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F |
39 | |--------|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|----|
40 | | **00** | A3 | B0 | 80 | C6 | B2 | F4 | 5C | 6C | 81 | F1 | BB | EB | 55 | 67 | 3C | 05 |
41 | | **10** | 1A | 0E | 61 | F6 | 22 | CE | AA | 8F | BD | 3B | 1F | 5E | 44 | 04 | 51 | 2E |
42 | | **20** | 4D | 9A | 84 | EA | F8 | 66 | 74 | 29 | 7F | 70 | D8 | 31 | 7A | 6D | A4 | 00 |
43 | | **30** | 82 | B9 | 5F | B4 | 16 | AB | FF | C2 | 39 | DC | 19 | 65 | 57 | 7C | 20 | FA |
44 | | **40** | 5A | 49 | 13 | D0 | FB | A8 | 91 | 73 | B1 | 33 | 18 | BE | 21 | 72 | 48 | B6 |
45 | | **50** | DB | A0 | 5D | CC | E6 | 17 | 27 | E5 | D4 | 53 | 42 | F3 | DD | 7B | 24 | AC |
46 | | **60** | 2B | 58 | 1E | A7 | E7 | 86 | 40 | D3 | 98 | 97 | 71 | CB | 3A | 0F | 01 | 9B |
47 | | **70** | 6E | 1B | FC | 34 | A6 | DA | 07 | 0C | AE | 37 | CA | 54 | FD | 26 | FE | 0A |
48 | | **80** | 45 | A2 | 2A | C4 | 12 | 0D | F5 | 4F | 69 | E0 | 8A | 77 | 60 | 3F | 99 | 95 |
49 | | **90** | D2 | 38 | 36 | 62 | B7 | 32 | 7E | 79 | C0 | 46 | 93 | 2F | A5 | BA | 5B | AF |
50 | | **A0** | 52 | 1D | C3 | 75 | CF | D6 | 4C | 83 | E8 | 3D | 30 | 4E | BC | 08 | 2D | 09 |
51 | | **B0** | 06 | D9 | 25 | 9E | 89 | F2 | 96 | 88 | C1 | 8C | 94 | 0B | 28 | F0 | 47 | 63 |
52 | | **C0** | D5 | B3 | 68 | 56 | 9C | F9 | 6F | 41 | 50 | 85 | 8B | 9D | 59 | BF | 9F | E2 |
53 | | **D0** | 8E | 6A | 11 | 23 | A1 | CD | B5 | 7D | C7 | A9 | C8 | EF | DF | 02 | B8 | 03 |
54 | | **E0** | 6B | 35 | 3E | 2C | 76 | C9 | DE | 1C | 4B | D1 | ED | 14 | C5 | AD | E9 | 64 |
55 | | **F0** | 4A | EC | 8D | F7 | 10 | 43 | 78 | 15 | 87 | E4 | D7 | 92 | E1 | EE | E3 | 90 |
56 |
57 | (The raw 256 byte subtitution table is available [here](substitution-table.bin)).
58 |
59 | These 8 bytes are then stored on the card using the required [card-specific format](../card-specific).
60 |
61 |
62 | ## Example
63 |
64 | ### Encoding
65 |
66 | Take the cardholder credential (RC 0 (A), FC 9876, CN 1234, IL 1).
67 |
68 | First, arrange the data into 8 bytes as follows:
69 |
70 | | | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
71 | |------------|-------|-------|-------|-------|-------|-------|-------|-------|
72 | | **Byte 0** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
73 | | **Byte 1** | 0 | 1 | 1 | 0 | 1 | 0 | 0 | 1 |
74 | | **Byte 2** | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 |
75 | | **Byte 3** | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
76 | | **Byte 4** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
77 | | **Byte 5** | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
78 | | **Byte 6** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
79 | | **Byte 7** | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
80 |
81 | This gives the 8 byte value `0x00699A4000020041`. Next, map each byte through the substitution table:
82 |
83 | `00` -> `A3`
84 |
85 | `69` -> `97`
86 |
87 | `9A` -> `93`
88 |
89 | `40` -> `5A`
90 |
91 | `00` -> `A3`
92 |
93 | `02` -> `80`
94 |
95 | `00` -> `A3`
96 |
97 | `41` -> `49`
98 |
99 | This gives the final result, `0xA397935AA380AA49`.
100 |
101 | ### Decoding
102 |
103 | Take `0xA38A8A4BA3A3A32C` as input. First, map each byte through the inverted subsitution table:
104 |
105 | `A3` -> `00`
106 |
107 | `8A` -> `8A`
108 |
109 | `8A` -> `8A`
110 |
111 | `4B` -> `E8`
112 |
113 | `A3` -> `00`
114 |
115 | `A3` -> `00`
116 |
117 | `A3` -> `00`
118 |
119 | `2C` -> `E3`
120 |
121 | This gives `0x008A8AE8000000E3`. Next, arrange the data and read out the constituent parts:
122 |
123 | | | Bit 7 | Bit 6 | Bit 5 | Bit 4 | Bit 3 | Bit 2 | Bit 1 | Bit 0 |
124 | |------------|-------|-------|-------|-------|-------|-------|-------|-------|
125 | | **Byte 0** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
126 | | **Byte 1** | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
127 | | **Byte 2** | 1 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
128 | | **Byte 3** | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 |
129 | | **Byte 4** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
130 | | **Byte 5** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
131 | | **Byte 6** | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
132 | | **Byte 7** | 1 | 1 | 1 | 0 | 0 | 0 | 1 | 1 |
133 |
134 | This gives a resulting credential of (RC 4 (D), FC 2222, CN 1111, IN 3) (and all unknown fields set to 0).
135 |
--------------------------------------------------------------------------------
/formats/cardholder/substitution-table.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/formats/cardholder/substitution-table.bin
--------------------------------------------------------------------------------
/formats/mes.md:
--------------------------------------------------------------------------------
1 | # MIFARE Enhanced Security
2 |
3 | ## Purpose
4 |
5 | *MIFARE Enhanced Security* (MES) was introduced by Gallagher in an attempt to strengthen the security of MIFARE Classic cards. At the time, attacks had been found on the MIFARE Classic card, namely the [darkside attack](https://eprint.iacr.org/2009/137) and the [hardnested attack](http://www.cs.ru.nl/~rverdult/Ciphertext-only_Cryptanalysis_on_Hardened_Mifare_Classic_Cards-CCS_2015.pdf). MES was intended to counteract these in a limited way.
6 |
7 | The MES encodes the same data as the normal [cardholder credential](cardholder/cardholder.md), but with some additional data, and is itself encrypted with a site-specific key, the *MIFARE Enhanced Security Site Key* (note that this is not the same as the *MIFARE Site Key*!).
8 |
9 | This encrypted blob of data protects against:
10 |
11 | * Simple card cloning, where the cardholder credential sector is cloned, but the UID of the card itself (held in the meant-to-be-read-only block 0 of sector 0) is not cloned (and is different on the destination card).
12 | * Modification of the card data, as this would require the knowledge of the MES key.
13 |
14 | However, it does not protect against modern card cloning where the card UID *is* copied to the destination card, as the cards are then identical. Note that the MES key is not required to perform this, only if one wants to modify the data held within the MES.
15 |
16 |
17 | ## Format
18 |
19 | The MES is an AES-encrypted 16 byte block of data.
20 |
21 | ### Data
22 |
23 | | | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
24 | |--------|---|---|---|---|---|---|---|---|
25 | | **+0** | `0x01` | CN0 | CN1 | CN2 | FC0 | FC1 | RC,
IL | PO,
UX |
26 | | **+8** | UB,
UC | UD,
UE | CSN0 | CSN1 | CSN2 | CSN3 | R0 | R1 |
27 |
28 | Note that *XY* indicates the *Y*th byte of *X*, and *A, B* indicates the two 4 bit values *A* and *B* concatenated into a single byte (with *A* being the most significant bits and *B* the least).
29 |
30 | Each field is named as defined in the [cardholder credential documentation](cardholder/cardholder.md), with the following additions:
31 |
32 | * PO is the pin-offset, which is something that I Don't Understand yet, but has always been seen as 0 in the field.
33 | * CSN is the Card Serial Number.
34 | * R is 2 random bytes. These are obviously ignored on MES verification.
35 |
36 | ### Key
37 |
38 | These 16 bytes are AES-encrypted with a diversified (i.e. modified per-card) key derived from the MES key and the CSN by XORing the CSN into the start of the MES key. That is, pad the CSN to 16 bytes long with 0x00s, and XOR it with the MES key.
39 |
40 |
41 | ## Example
42 |
43 | Here is an example MES block:
44 |
45 | ```
46 | 4F 36 B7 4E FF CD 76 EF ED A5 74 58 C8 B4 E3 04
47 | ```
48 |
49 | I know that the CSN of the card is `0x3C5451E3` and the MES site key for this card is `0x1337D00D1337D00D1337D00D1337D00D`. We can decrypt the block using the following command line:
50 |
51 | ```bash
52 | $ echo '4F 36 B7 4E FF CD 76 EF ED A5 74 58 C8 B4 E3 04' |
53 | xxd -r -p | openssl aes-128-ecb -d -K 2f6381ee1337d00d1337d00d1337d00d
54 | -nopad | hd
55 | 00000000 01 00 f0 0d 13 37 c1 00 00 00 3c 54 51 e3 07 48 |.....7....0 | CN1 | CN2 | FC0 | FC1 | RC,
IL | PO,
UX |
64 | | | `0x01` | `0x00` | `0xF0` | `0x0D` | `0x13` | `0x37` | `0xC1` | `0x00` |
65 | | **+8** | UB,
UC | UD,
UE | CSN0 | CSN1 | CSN2 | CSN3 | R0 | R1 |
66 | | | `0x00` | `0x00` | `0x3C` | `0x54` | `0x51` | `0xE3` | `0x07` | `0x48` |
67 |
68 | This gives the following pieces of data:
69 |
70 | * RC = 12 (M)
71 | * FC = 0x1337 = 4919
72 | * CN = 0xF00D = 61453
73 | * IL = 1
74 | * PO = 0
75 | * CSN = 0x3C5451E3
76 | * R = 0x0748 (which is then ignored)
77 | * All unknown fields are 0.
78 |
--------------------------------------------------------------------------------
/misc-tools/README.md:
--------------------------------------------------------------------------------
1 | This directory contains other miscellaneous tools that I built during this research. Probably won't make much sense unless you know you'll need them - and hey, they're not documented x)
2 |
--------------------------------------------------------------------------------
/misc-tools/atmel-uart-dump/Makefile:
--------------------------------------------------------------------------------
1 | # This makefile depends on Arduino-Makefile
2 | # (https://github.com/sudar/Arduino-Makefile).
3 | #
4 | # You will need to set the ARDMK_DIR variable to the location of
5 | # Arduino-Makefile. Additionally, you will probably want to set some other
6 | # variables used by Arduino-Makefile (i.e. BOARD_TAG, BOARD_SUB and
7 | # MONITOR_PORT).
8 | #
9 | # For example, first set ARDMK_DIR:
10 | # On Debian:
11 | # `export ARDMK_DIR=/usr/share/arduino`
12 | # On macOS with Homebrew:
13 | # `export ARDMK_DIR=/usr/local/Cellar/arduino-mk/[version]`
14 | # Then compile and upload the sketch (e.g. for an Arduino Nano):
15 | # `BOARD_TAG=nano BOARD_SUB=atmega328 MONITOR_PORT=/dev/tty.usbserial-XXXXXXXX make upload`
16 |
17 | include $(ARDMK_DIR)/Arduino.mk
18 |
--------------------------------------------------------------------------------
/misc-tools/atmel-uart-dump/ard.cpp:
--------------------------------------------------------------------------------
1 | /* Dumps an Atmel chip via UART via Arduino */
2 |
3 | #include
4 | #include
5 |
6 | void setup () {
7 | SoftwareSerial ss(4, 3);
8 |
9 | pinMode(2, OUTPUT);
10 | pinMode(5, OUTPUT);
11 |
12 | digitalWrite(5, LOW);
13 |
14 | Serial.begin(9600);
15 | while (!Serial);
16 |
17 | ss.begin(9600);
18 |
19 | delay(1000);
20 |
21 | digitalWrite(2, HIGH);
22 | delay(100);
23 | digitalWrite(2, LOW);
24 | delay(10);
25 |
26 | Serial.write("go\r\n");
27 | ss.write("U");
28 | delay(100);
29 | ss.write(":050000040000FFFF00F9\r\n");
30 |
31 | for (;;) {
32 | if (ss.available())
33 | Serial.write(ss.read());
34 | if (Serial.available())
35 | ss.write(Serial.read());
36 | }
37 | }
38 |
39 | void loop () {
40 | }
41 |
--------------------------------------------------------------------------------
/misc-tools/gdb.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | # Dumps code-readout protected memory via register readout technique
4 | # See https://www.youtube.com/watch?v=DTuzuaiQL_Q
5 |
6 | import random
7 | import struct
8 |
9 | import gdb
10 |
11 |
12 | def determine_read_op(a):
13 | for r in range(13):
14 | n = 0xde000000 + random.randint(0, 0xffffff)
15 |
16 | gdb.execute('set {int}0x20009124 = 0x%08X' % n)
17 |
18 | for r2 in range(13):
19 | gdb.execute('set $r%d = 0x%08X' % (r2, 0x20009124 if r2 == r else 0x1337f00d))
20 |
21 | gdb.execute('set $pc = 0x%x' % a)
22 | gdb.execute('si')
23 |
24 | for r2 in range(13):
25 | if r2 == r:
26 | continue
27 | v = int(gdb.parse_and_eval("$r%d" % r2)) & 0xffffffff
28 | if v == n:
29 | return r, r2
30 |
31 | return None
32 |
33 |
34 | def find_read():
35 | for a in range(0x8004201, 0x8004001 + 0x10000, 2):
36 | print 'Addr 0x%08X' % a
37 |
38 | n = 0xde000000 + random.randint(0, 0xffffff)
39 |
40 | gdb.execute('set {int}0x20009124 = 0x%08X' % n)
41 |
42 | for r in range(13):
43 | gdb.execute('set $r%d = 0x20009124' % r)
44 |
45 | gdb.execute('set $pc = 0x%x' % a)
46 | gdb.execute('si')
47 |
48 | for r in range(13):
49 | v = int(gdb.parse_and_eval("$r%d" % r)) & 0xffffffff
50 | if v != 0x20009124:
51 | print 'Reg %d changed to 0x%08X' % (r, v)
52 | if v == n:
53 | op = determine_read_op(a)
54 | if op:
55 | return a, op
56 |
57 | return None
58 |
59 |
60 | def determine_write_op(a):
61 | for r in range(13):
62 | n = 0xde000000 + (random.randint(0, 0xffffff) & 0xfffff0)
63 |
64 | gdb.execute('set {int}0x20000000 = 0')
65 |
66 | for r2 in range(13):
67 | gdb.execute('set $r%d = 0x%08X' % (r2, 0x20000000 if r2 == r else n + r2))
68 |
69 | gdb.execute('set $pc = 0x%x' % a)
70 | gdb.execute('si')
71 |
72 | v = int(gdb.parse_and_eval("*(unsigned int *)0x20000000")) & 0xffffffff
73 | if n <= v <= n + 13 and r != v - n:
74 | return r, v - n
75 |
76 | return None
77 |
78 |
79 | def find_write():
80 | for a in range(0x8004201, 0x8004001 + 0x10000, 2):
81 | print 'Addr 0x%08X' % a
82 |
83 | n = 0xde000000 + (random.randint(0, 0xffffff) & 0xfffff0)
84 |
85 | gdb.execute('set {int}0x20000000 = 0')
86 |
87 | for r in range(13):
88 | gdb.execute('set $r%d = 0x20000000' % r)
89 |
90 | gdb.execute('set $pc = 0x%x' % a)
91 | gdb.execute('si')
92 |
93 | v = int(gdb.parse_and_eval("*(unsigned int *)0x20000000")) & 0xffffffff
94 | if v != 0:
95 | print 'Mem changed to 0x%08X' % v
96 | if v == 0x20000000:
97 | op = determine_write_op(a)
98 | if op:
99 | return a, op
100 |
101 | return None
102 |
103 |
104 | def dump(aop, f):
105 | gdb.execute('set logging file /dev/null')
106 | gdb.execute('set logging redirect on')
107 | gdb.execute('set logging on')
108 |
109 | addr, (reg_from, reg_to) = aop
110 | r = ''
111 | for m in range(0, 512 * 1024, 4):
112 | if not m & 0x3ff:
113 | gdb.execute('set logging off')
114 | print '%dkB done...' % (m / 1024)
115 | gdb.execute('set logging on')
116 | f.flush()
117 | gdb.execute('set $r%d = 0x%08X' % (reg_from, 0x8000000 + m))
118 | gdb.execute('set $pc = 0x%x' % addr)
119 | gdb.execute('si')
120 | f.write(struct.pack(' R' if direction else 'R -> C',
39 | print '|', tick / 1.0e6
40 | if not len(recv[gpio]) % 8:
41 | d = ''.join(chr(int(recv[gpio][i:i+8], 2))
42 | for i in range(0, len(recv[gpio]),
43 | 8))
44 | hexdump.hexdump(d)
45 | else:
46 | print 'Partial:', recv[gpio]
47 |
48 | del start_tick[gpio]
49 | del recv[gpio]
50 | elif bool(level) != direction:
51 | start_tick[gpio] = tick
52 | elif gpio in start_tick:
53 | recv.setdefault(gpio, '')
54 | recv[gpio] += str(int(tick - start_tick[gpio] >= 100))
55 |
56 |
57 | def go(pig):
58 | pig.set_mode(4, pigpio.OUTPUT)
59 | pig.set_mode(17, pigpio.INPUT)
60 | pig.set_mode(18, pigpio.OUTPUT)
61 | pig.set_mode(23, pigpio.INPUT)
62 |
63 | pig.callback(17, pigpio.EITHER_EDGE, cb)
64 | pig.set_watchdog(17, 3)
65 | pig.callback(23, pigpio.EITHER_EDGE, cb)
66 | pig.set_watchdog(23, 1)
67 |
68 | binc = lambda x: ('0' * 8 + bin(x)[2:])[-8:]
69 |
70 | if len(sys.argv) >= 1 + 2:
71 | time.sleep(.2)
72 |
73 | pig.wave_clear()
74 | p = ([pigpio.pulse(0, 1 << 18, 20000)]
75 | if sys.argv[1] == 'from_r' else
76 | [pigpio.pulse(1 << 4, 0, 20000)])
77 | d = sys.argv[1:]
78 | for i in range(len(d)):
79 | if d[i] == 'l':
80 | d[i] = len(d) - 1
81 | d = [int(x, 16) if isinstance(x, basestring) and len(x) == 2
82 | else x for x in d]
83 | if d[-1] == 'x':
84 | d[-1] = functools.reduce(operator.xor, d[1:-1])
85 | print 'Output: %s' % ' '.join('%02x' % x for x in d)
86 | pig.wave_add_generic(p + pulses(''.join(binc(x) for x in d),
87 | sys.argv[1] == 'from_r') + p)
88 | pig.wave_send_once(pig.wave_create())
89 |
90 | try:
91 | #time.sleep(.2)
92 | time.sleep(9e99)
93 | except KeyboardInterrupt:
94 | pass
95 |
96 |
97 | pig = pigpio.pi()
98 | try:
99 | go(pig)
100 | finally:
101 | pig.stop()
102 |
--------------------------------------------------------------------------------
/protocols/cardax-iv/message1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/protocols/cardax-iv/message1.png
--------------------------------------------------------------------------------
/protocols/cardax-iv/message2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/protocols/cardax-iv/message2.png
--------------------------------------------------------------------------------
/protocols/cardax-iv/message3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/protocols/cardax-iv/message3.png
--------------------------------------------------------------------------------
/protocols/ernie-types.md:
--------------------------------------------------------------------------------
1 | # Ernie Types
2 |
3 | These types are used in the GBUS and HBUS protocols to indicate the types of devices.
4 |
5 | | Ernie type ID | Name (from firmware) |
6 | |---------------|----------------------|
7 | | `0x00` | Controller 5000 |
8 | | `0x01` | Strike Controller |
9 | | `0x02` | Prox IDT |
10 | | `0x03` | Prox |
11 | | `0x04` | Magstripe IDT |
12 | | `0x05` | Magstripe |
13 | | `0x06` | URI |
14 | | `0x07` | I/O Board |
15 | | `0x08` | Input Board |
16 | | `0x09` | Relay Board |
17 | | `0x0A` | Intercom |
18 | | `0x0B` | Camera |
19 | | `0x0C` | Mifare IDT |
20 | | `0x0D` | 125 IDT |
21 | | `0x0E` | 8 Door Interface |
22 | | `0x0F` | Controller 3000 |
23 | | `0x32` | 8Input/4Output Interface |
24 | | `0x33` | 8Input Interface |
25 | | `0x34` | 8Output Interface |
26 | | `0x35` | Stoval Reader |
27 | | `0x36` | GBUS URI |
28 | | `0x37` | 16Input/16Output Interface |
29 | | `0x38` | GBUS URI Wiegand |
30 | | `0x39` | Aperio Unit |
31 | | `0x3A` | RAT |
32 | | `0x3B` | Fence Controller |
33 | | `0x3D` | Controller 5000 GL |
34 | | `0x3E` | Controller 6000 |
35 | | `0x3F` | Controller 6000 8R |
36 | | `0x40` | Controller 6000 4R |
37 | | `0x41` | H-BUS Card Reader |
38 | | `0x42` | H-BUS Terminal |
39 | | `0x4F` | H-BUS Alarms Terminal |
40 | | `0x43` | Sensor Device |
41 | | `0x44` | Controller 6000 4H |
42 | | `0x45` | Controller 6000 8H |
43 | | `0x46` | H-BUS Taut Wire Sensor |
44 | | `0x47` | F22 Fence Controller |
45 | | `0x4A` | H-BUS IO 8 In 2 Out |
46 | | `0x4B` | H-BUS IO 16 In 16 Out |
47 | | `0x4C` | H-BUS IO 8 In 4 Out |
48 | | `0x4D` | H-BUS IO 8 In |
49 | | `0x49` | RSI Device |
50 | | `0x4E` | OSDP Device |
51 | | `0x50` | F31 Fence Controller |
52 | | `0x51` | F32 Fence Controller |
53 | | `0x52` | F41 Fence Controller |
54 | | `0x53` | F42 Fence Controller |
55 | | `0x54` | H-BUS Contact Card Reader |
56 | | `0x55` | Class 5 End-of-Line Module |
57 |
--------------------------------------------------------------------------------
/protocols/gbus/gbus-decode.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import crcmod
4 | import hexdump
5 |
6 | import itertools
7 | import socket
8 | import struct
9 | import sys
10 | import time
11 |
12 |
13 | def prefix(s, p):
14 | return '\n'.join('%s%s' % (p, l) for l in s.splitlines())
15 |
16 |
17 | def indent(s, n=3, tab=False):
18 | return prefix(s, ('\t' if tab else ' ') * n)
19 |
20 |
21 | def nth(iterable, n, default=None):
22 | return next(itertools.islice(iterable, n, None), default)
23 |
24 |
25 | def make_crc():
26 | return crcmod.Crc(0x11021, 0)
27 |
28 |
29 | def lcg(m, a, s):
30 | while True:
31 | s = ((s * m) + a) & 0xff
32 | yield s
33 |
34 |
35 | def primes():
36 | n = 2
37 | while True:
38 | if all(n % i for i in range(2, n - 1)):
39 | yield n
40 | n += 1
41 |
42 |
43 | CDXIV_FROM_CONTROLLER_COMMANDS = {
44 | 0x07: 'Deny Mifare',
45 | 0x08: 'Deny 125',
46 | 0x0a: 'Enable admin card',
47 | 0x0b: 'Use Mifare extended data format',
48 | 0x0c: "Use high security",
49 | 0x0d: "Don't use high security",
50 | 0x0e: 'Use CSN',
51 | 0x0f: "Don't use CSN",
52 | 0x10: 'Stop buzzer',
53 | 0x11: 'Start buzzer',
54 | 0x18: 'Granted',
55 | 0x19: 'Denied',
56 | 0x21: 'Red LED on',
57 | 0x22: 'Green LED off',
58 | 0x23: 'Red LED off',
59 | 0x24: 'Green LED on',
60 | 0x49: 'Not secure',
61 | 0x4a: 'Hide over bell',
62 | 0x4b: 'Hide under bell',
63 | 0x58: 'Fail',
64 | 0x59: 'Secure',
65 | 0x5a: 'Show over bell',
66 | 0x5b: 'Show under bell',
67 | 0x81: 'Set secure access',
68 | 0x82: 'PIN only',
69 | 0x83: 'Free access',
70 | 0x8a: 'Show wait & failure',
71 | 0x8f: 'Flash all',
72 | 0x90: 'Show default',
73 | 0x91: 'Ask for PIN',
74 | 0x92: 'Display wrong PIN',
75 | 0x93: 'Request second card',
76 | 0x95: 'Flash over bell & under bell & card, show bell',
77 | 0x96: 'Flash over bell & under bell & keypad, show bell',
78 | 0x97: 'Flash bell, start alarm',
79 | 0x98: 'Display set failed',
80 | 0x9f: 'Display standby',
81 | 0xa3: 'Keystroke ACK',
82 | 0xa5: 'Beep',
83 | 0xb1: 'Config MF key? 1',
84 | 0xc0: 'Config MF key? 2',
85 | 0xd5: 'Config MF key? 3',
86 | 0xe0: 'Config MF key? 0 nibble',
87 | 0xe1: 'Config MF key? 1 nibble',
88 | 0xe2: 'Config MF key? 2 nibble',
89 | 0xe3: 'Config MF key? 3 nibble',
90 | 0xe4: 'Config MF key? 4 nibble',
91 | 0xe5: 'Config MF key? 5 nibble',
92 | 0xe6: 'Config MF key? 6 nibble',
93 | 0xe7: 'Config MF key? 7 nibble',
94 | 0xe8: 'Config MF key? 8 nibble',
95 | 0xe9: 'Config MF key? 9 nibble',
96 | 0xea: 'Config MF key? A nibble',
97 | 0xeb: 'Config MF key? B nibble',
98 | 0xec: 'Config MF key? C nibble',
99 | 0xed: 'Config MF key? D nibble',
100 | 0xee: 'Config MF key? E nibble',
101 | 0xef: 'Config MF key? F nibble',
102 | 0xff: 'Heartbeat'
103 | }
104 |
105 | CARD_TYPES = {
106 | 0x01: '26-bit HID',
107 | 0x02: '37-bit HID',
108 | 0x03: 'Motorola',
109 | 0x04: 'EM (Deister)',
110 | 0x05: 'CasiRusco',
111 | 0xf5: 'Facility code list?',
112 | 0xf7: 'Mifare extended data format',
113 | 0xf8: 'Mifare enhanced security',
114 | 0xfa: 'Data low',
115 | 0xfb: 'Data high',
116 | 0xfc: 'CardKey',
117 | 0xfd: 'Third party',
118 | 0xfe: 'Cardax magstripe',
119 | 0xff: 'Cardax Prox',
120 | }
121 |
122 | DEVICE_TYPES = {
123 | 0x00: 'Controller 5000',
124 | 0x01: 'Strike Controller',
125 | 0x02: 'Prox IDT',
126 | 0x03: 'Prox',
127 | 0x04: 'Magstripe IDT',
128 | 0x05: 'Magstripe',
129 | 0x06: 'URI',
130 | 0x07: 'I/O Board',
131 | 0x08: 'Input Board',
132 | 0x09: 'Relay Board',
133 | 0x0a: 'Intercom',
134 | 0x0b: 'Camera_0',
135 | 0x0c: 'Mifare IDT',
136 | 0x0d: '125 IDT',
137 | 0x0e: '8 Door Interface',
138 | 0x0f: 'Controller 3000',
139 | 0x32: '8Input/4Output Interface',
140 | 0x33: '8Input Interface',
141 | 0x34: '8Output Interface',
142 | 0x35: 'Stoval Reader',
143 | 0x36: 'GBUS URI',
144 | 0x37: '16Input/16Output Interface',
145 | 0x38: 'GBUS URI Wiegand',
146 | 0x39: 'Aperio Unit',
147 | 0x3a: 'RAT',
148 | 0x3b: 'Fence Controller',
149 | 0x3d: 'Controller 5000 GL',
150 | 0x3e: 'Controller 6000',
151 | 0x3f: 'Controller 6000 8R',
152 | 0x40: 'Controller 6000 4R',
153 | 0x41: 'H-BUS Card Reader',
154 | 0x42: 'H-BUS Terminal',
155 | 0x4f: 'H-BUS Alarms Terminal',
156 | 0x43: 'Sensor Device',
157 | 0x44: 'Controller 6000 4H',
158 | 0x45: 'Controller 6000 8H',
159 | 0x46: 'H-BUS Taut Wire Sensor',
160 | 0x47: 'F22 Fence Controller',
161 | 0x4a: 'H-BUS IO 8 In 2 Out',
162 | 0x4b: 'H-BUS IO 16 In 16 Out',
163 | 0x4c: 'H-BUS IO 8 In 4 Out',
164 | 0x4d: 'H-BUS IO 8 In',
165 | 0x49: 'RSI Device',
166 | 0x4e: 'OSDP Device',
167 | 0x50: 'F31 Fence Controller',
168 | 0x51: 'F32 Fence Controller',
169 | 0x52: 'F41 Fence Controller',
170 | 0x53: 'F42 Fence Controller',
171 | 0x54: 'H-BUS Contact Card Reader',
172 | 0x55: 'Class 5 End-of-Line Module',
173 | }
174 |
175 |
176 | def show_packet(opcode, data):
177 | if not opcode:
178 | assert not len(data) % 4
179 | return ('Ident: ' + ', '.join('v%d.%02d//b%d' % struct.unpack('!BBH', data[i:i+4]) for i in range(0, len(data), 4)))
180 |
181 | if opcode == 1:
182 | assert data
183 | if not data[0]:
184 | assert len(data) == 2
185 | r = 'invalid packet?, opcode = 0x%02X' % data[1]
186 | elif data[0] == 1:
187 | r = 'bad packet?, packet?:\n' + indent(hexdump.hexdump(data[1:], 'return'))
188 | elif data[0] == 2:
189 | assert len(data) == 3
190 | r = 'wrong length?, opcode = 0x%02X, length = %d' % (data[1], data[2])
191 | elif data[0] == 3:
192 | r = 'data underflow?'
193 | if len(data) > 1:
194 | r += ':\n' + indent(hexdump.hexdump(data[1:], 'return'))
195 | else:
196 | assert False
197 |
198 | return 'Notification of invalid packet: ' + r
199 |
200 | if opcode == 2:
201 | assert data
202 | r = 'Diagnostic: test type = %d' % data[0]
203 | if len(data) > 1:
204 | r += ', rest:\n' + indent(hexdump.hexdump(data[1:], 'return'))
205 | return r
206 |
207 | if opcode == 3:
208 | assert not len(data) % 2
209 | r = 'Beep:'
210 | return ('Beep: ' + ', '.join('len %d pitch %d' % struct.unpack('!BB', data[i:i+2]) for i in range(0, len(data), 2)))
211 |
212 | if opcode == 4:
213 | assert data
214 | r = 'Display: flag = %d, data:\n' % data[0]
215 | r += indent(hexdump.hexdump(data, 'return'))
216 | return r
217 |
218 | if opcode == 5:
219 | assert len(data) == 2
220 | return 'I/O event: number = %d, %s' % (data[0], 'closed' if data[1] else 'open')
221 |
222 | if opcode == 6:
223 | assert len(data) == 6
224 | val_id, val_val = struct.unpack('!HI', data)
225 | return 'Set/notify configuration: 0x%04X = 0x%08X' % (val_id, val_val)
226 |
227 | if opcode == 7:
228 | assert len(data) == 2
229 | val_id = struct.unpack('!H', data)
230 | return 'Get configuration: 0x%04X' % val_id
231 |
232 | if opcode == 8:
233 | assert len(data) >= 2 and not len(data) % 2
234 | return 'Get character bitmap(s): ' + ', '.join('0x%04X' % struct.unpack('!H', data[i:i+2]) for i in range(0, len(data), 2))
235 |
236 | if opcode == 9:
237 | assert len(data) >= 2
238 | r = 'Character bitmap: record 0x%04X, bitmap:\n' % struct.unpack('!H', data[:2])[0]
239 | r += indent(hexdump.hexdump(data[2:], 'return'))
240 | return r
241 |
242 | if opcode == 0xa:
243 | assert len(data) == 2
244 | return 'Initial download data: unknown1 0x%02X, unknown2 0x%02X' % tuple(data)
245 |
246 | if opcode == 0xb:
247 | return 'Continuing download data: data:\n' + indent(hexdump.hexdump(data, 'return'))
248 |
249 | if opcode == 0xc:
250 | r = ''
251 |
252 | if data[0] & 0x01:
253 | r += 'Alarm history / Power too low'
254 | if data[0] & 0x02:
255 | r += 'Alarm active / Power too high'
256 | if data[0] & 0x04:
257 | r += 'Front tampered'
258 | if data[0] & 0x08:
259 | r += 'Rear tampered'
260 | if data[0] & 0x10:
261 | r += 'Tampered'
262 |
263 | for i, d in enumerate(data[1:]):
264 | if r:
265 | r += '\n'
266 | r += 'Input %d: change %d, state %d' % (i, d >> 4, d & 0xf)
267 |
268 | return 'Input status:\n' + indent(r)
269 |
270 | if opcode == 0xd:
271 | assert len(data) == 2
272 | return 'Key press: key 0x%02X%s, reader %d' % (data[0], ' (%s)' % chr(data[0]) if 0x20 <= data[0] < 0x7f else '', data[1])
273 |
274 | if opcode == 0xe:
275 | assert len(data) >= 2
276 | return 'Unit event: event ID %d, priority %r, msg %r' % (data[0], {0: 'event', 1: 'warning', 2: 'failure'}.get(data[1], str(data[1])), data[2:].decode('iso-8859-1'))
277 |
278 | if opcode == 0xf:
279 | assert len(data) >= 1
280 | r = 'Card data (old packet): reader %d, data:\n' % data[0]
281 | r += indent(hexdump.hexdump(data[1:], 'return'))
282 | return r
283 |
284 | if opcode == 0x10:
285 | assert len(data) == 3
286 | n = (data[0] << 16) | (data[1] << 8) | data[2]
287 | return 'Request archive: thing = %d' % n
288 |
289 | if 0x11 <= opcode <= 0x13:
290 | assert data
291 | r = 'Localbus message:\n'
292 | r += indent(hexdump.hexdump(data, 'return'))
293 | return r
294 |
295 | if opcode == 0x14:
296 | return 'Unknown event thing'
297 |
298 | if opcode == 0x15:
299 | assert len(data) == 2
300 | return 'CDXIV command: reader %d, command 0x%02X (%s)' % (data[0], data[1], CDXIV_FROM_CONTROLLER_COMMANDS.get(data[1], 'unknown'))
301 |
302 | if opcode == 0x16:
303 | assert len(data) >= 4
304 | #assert struct.unpack('!H', data[-2:])[0] == 8 * (len(data) - 4)
305 |
306 | r = 'Card data: reader %d, type 0x%02X (%s), data:\n' % (data[0], data[1], CARD_TYPES.get(data[1], 'unknown'))
307 | r += indent(hexdump.hexdump(data[2:-2], 'return'))
308 | r += 'last 2 %04X' % struct.unpack('!H', data[-2:])
309 | return r
310 |
311 | if opcode == 0x17:
312 | assert data
313 | r = ''
314 | for i, d in enumerate(data[::-1]):
315 | for j in range(8):
316 | r += 'Reader %d: %s\n' % (i * 8 + j, 'offline' if d & (1 << j) else 'online / N/A')
317 |
318 | return 'URI reader status:\n' + indent(r)
319 |
320 | if opcode == 0x21:
321 | assert len(data) <= 4
322 | r = ''
323 | for i, d in enumerate(data):
324 | for j in range(4):
325 | v = {0: 'online', 1: 'offline', 2: 'unknown reader status', 3: 'unknown'}[(d >> (j * 2)) & 3]
326 | r += 'Reader %d: %s\n' % (i * 4 + j, v)
327 |
328 | return 'Reader status:\n' + indent(r)
329 |
330 | # TODO: these are HBUS?:
331 |
332 | if opcode == 0x22:
333 | assert len(data) == 10
334 | return 'LEDPattern: unknown = 0x%02X, colour A = %d, duration A = %d, colour B = %d, duration B = %d, repeat count = %d, gap time = %d' % struct.unpack('!BBHBHBH', data)
335 |
336 | if opcode == 0x23:
337 | return 'Play: tune = %r' % data.decode('iso-8859-1')
338 |
339 | if data:
340 | r = 'Unknown packet type 0x%02X:\n' % opcode
341 | r += indent(hexdump.hexdump(data, 'return'))
342 | else:
343 | r = 'Unknown packet type %d' % opcode
344 | return r
345 |
346 |
347 | class Device(object):
348 |
349 | def __init__(self, addr):
350 | self.addr = addr
351 | self.type = None
352 | self.last_poll_recv = time.time()
353 | self.next_seq = 0
354 | self.in_flight = 0
355 |
356 | self.reset_lcg()
357 | self.poss_lcg_params = set()
358 |
359 | def reset_lcg(self):
360 | self.next_seeds = {True: [None] * 4, False: [None] * 4}
361 | self.poss_lcg_params = {(0x43, 0x11)}
362 | self.next_lcg_params = [None, None]
363 | self.last_resp_seq = None
364 |
365 | def handle_frame(self, frame):
366 | r = ''
367 |
368 | #print('in: %r' % self.__dict__)
369 |
370 | is_poll = bool(frame[0] & 0x40)
371 | assert bool(frame[0] & 0x80) != is_poll
372 | assert frame[0] & 0xf == 0x4 if is_poll else 0xe
373 | seq = (frame[0] >> 4) & 0x3
374 |
375 | if frame[1] != 0xfe:
376 | if frame[1] != self.type:
377 | assert self.type in {0xfe, None}
378 | r += 'Device found: 0x%02X (%s)\n' % (frame[1], DEVICE_TYPES.get(frame[1], 'unknown'))
379 | self.type = frame[1]
380 | elif self.type != 0xfe:
381 | if self.type is not None:
382 | r += 'Device lost\n'
383 | self.type = frame[1]
384 | self.reset_lcg()
385 |
386 | b = frame[3:]
387 |
388 | if len(self.poss_lcg_params) != 1 and self.next_seeds[is_poll][seq] is not None:
389 | r += 'Guessing LCG\n'
390 | ps = set()
391 | for m in PRIMES:
392 | for a in PRIMES:
393 | if nth(lcg(m, a, self.next_seeds[is_poll][seq]), len(b) - 1) == b[-1]:
394 | ps.add((m, a))
395 | if not self.poss_lcg_params:
396 | self.poss_lcg_params = ps
397 | else:
398 | self.poss_lcg_params &= ps
399 | if len(self.poss_lcg_params) == 1:
400 | r += 'Found LCG: %r\n' % (list(self.poss_lcg_params)[0],)
401 |
402 | if len(self.poss_lcg_params) == 1 and self.next_seeds[is_poll][seq] is not None:
403 | p = list(self.poss_lcg_params)[0] + (self.next_seeds[is_poll][seq],)
404 | b = bytes(x ^ y for x, y in zip(b, lcg(*p)))
405 |
406 | if not b[-1]:
407 | b = b[:-1]
408 | if is_poll:
409 | r += self.handle_poll(b, seq)
410 | self.last_poll_recv = time.time()
411 | self.in_flight += 1
412 | else:
413 | r += self.handle_response(b, seq)
414 | self.last_resp_seq = seq
415 | self.in_flight -= 1
416 | else:
417 | r += 'Bad unLCG %s:\n' % ('poll' if is_poll else 'response')
418 | r += indent(hexdump.hexdump(frame, 'return'))
419 | else:
420 | r += 'Unable to unLCG %s:\n' % ('poll' if is_poll else 'response')
421 | r += indent(hexdump.hexdump(frame, 'return'))
422 |
423 | if is_poll:
424 | self.next_seeds[False][seq] = frame[2]
425 | else:
426 | self.next_seq = (seq + 1) % 4
427 | self.next_seeds[True][self.next_seq] = frame[2]
428 |
429 | #print('out: %r' % self.__dict__)
430 | return r
431 |
432 | def poll(self, packets):
433 | if self.type is None or self.in_flight >= 2:
434 | print('(Device %d resetting for poll)' % self.addr)
435 | print()
436 | #time.sleep(1)
437 | self.type = 0xfe
438 | self.in_flight = 0
439 | self.reset_lcg()
440 | self.next_seq = 0
441 | self.next_seeds[True][self.next_seq] = 0
442 |
443 | b = bytes(x ^ y for x, y in zip(packets + bytes([0]), lcg(*list(self.poss_lcg_params)[0] + (self.next_seeds[True][self.next_seq],))))
444 |
445 | return bytes([0x44 | (self.next_seq << 4), self.type, 3]) + b
446 |
447 |
448 | class PassiveDevice(Device):
449 |
450 | def handle_poll(self, packets, seq):
451 | r = ''
452 |
453 | if packets:
454 | r += 'Poll (%d):\n' % seq
455 | #return r + indent(hexdump.hexdump(packets, 'return')) # TODO remove
456 | for m in range(2):
457 | n = 0
458 | while n < len(packets):
459 | opcode = packets[n]
460 | l = packets[n + 1]
461 |
462 | if not m and l > len(packets) - n - 2:
463 | r += 'Bad packet(s) in frame:\n' + indent(hexdump.hexdump(packets, 'return')) + '\n'
464 | return r
465 | data = packets[n+2:n+2+l]
466 |
467 | if m:
468 | if opcode == 6:
469 | val_id, val_val = struct.unpack('!HI', data)
470 | if val_id in {3, 4}:
471 | self.next_lcg_params[0 if val_id == 3 else 1] = val_val
472 |
473 | r += indent(show_packet(opcode, data)) + '\n'
474 |
475 | n += l + 2
476 |
477 | return r
478 |
479 | def handle_response(self, packets, seq):
480 | r = ''
481 |
482 | global respd
483 | respd += 1
484 |
485 | if packets:
486 | r += 'Response (%d):\n' % seq
487 | for m in range(2):
488 | n = 0
489 | while n < len(packets):
490 | opcode = packets[n]
491 | l = packets[n + 1]
492 |
493 | if not m and l > len(packets) - n - 2:
494 | r += 'Bad packet(s) in frame:\n' + indent(hexdump.hexdump(packets, 'return')) + '\n'
495 | return r
496 | data = packets[n+2:n+2+l]
497 |
498 | if m:
499 | r += indent(show_packet(opcode, data)) + '\n'
500 |
501 | n += l + 2
502 |
503 | if self.next_lcg_params[0] is not None:
504 | self.poss_lcg_params = {(self.next_lcg_params[0], list(self.poss_lcg_params)[0][1])}
505 | if self.next_lcg_params[1] is not None:
506 | self.poss_lcg_params = {(list(self.poss_lcg_params)[0][0], self.next_lcg_params[1])}
507 |
508 | self.next_lcg_params = [None, None]
509 |
510 | return r
511 |
512 |
513 | PRIMES = list(itertools.takewhile(lambda p: p < 0x100, primes()))[1:]
514 | assert len(PRIMES) == 0x35
515 |
516 | STX = 0x02
517 | ETX = 0x03
518 | DLE = 0x10
519 |
520 | MODE_IDLE = 0
521 | MODE_FRAME = 1
522 | MODE_FRAME_ESCAPED = 2
523 | MODE_CRC_1 = 3
524 | MODE_CRC_2 = 4
525 |
526 |
527 | BUS_MASTER = True
528 | POLL_INTERVAL = 0.1
529 |
530 | devices = {}
531 |
532 | devices[0] = PassiveDevice(0)
533 | devices[1] = PassiveDevice(1)
534 | devices[2] = PassiveDevice(2)
535 |
536 |
537 | def handle_frame(frame):
538 | addr = frame[0] & 0xf
539 | assert (frame[0] >> 4) == 1
540 |
541 | if addr not in devices:
542 | devices[addr] = PassiveDevice(addr)
543 |
544 | device = devices[addr]
545 |
546 | if device.type is not None:
547 | t = '0x%02X (%s)' % (device.type, DEVICE_TYPES.get(device.type, 'unknown'))
548 | else:
549 | t = 'unseen'
550 |
551 | r = device.handle_frame(frame[1:])
552 | if r:
553 | print('Device %d (%s):' % (addr, t))
554 | print(indent(r.rstrip('\n')))
555 | print()
556 |
557 |
558 | def frame_escape(b):
559 | r = b''
560 | for c in b:
561 | if STX <= c <= ETX or c == DLE:
562 | r += bytes([DLE])
563 | r += bytes([c])
564 | return r
565 |
566 |
567 | def main(argv):
568 | global respd
569 |
570 | io = sys.stdin.buffer if argv[1] == '-' else socket.create_connection(argv[1].split(':'))
571 |
572 | br = 0
573 | mode = MODE_IDLE
574 | crc = make_crc()
575 | nn = 0
576 | while True:
577 | if argv[1] != '-' and BUS_MASTER:
578 | # TODO: this ordering of polling is broken
579 | es = {addr: time.time() - device.last_poll_recv for addr, device in devices.items()}
580 | if all(e >= POLL_INTERVAL for e in es.values()):
581 | device = devices[max(es, key=lambda k: es[k])]
582 | out = b''
583 | frame = bytes([device.addr | 0x10]) + device.poll(out)
584 | wire = bytes([STX]) + frame_escape(frame) + bytes([ETX])
585 | out_crc = make_crc()
586 | out_crc.update(wire)
587 | wire += out_crc.digest()[::-1]
588 | io.sendall(wire)
589 | handle_frame(frame)
590 |
591 | io.settimeout(POLL_INTERVAL / 100) # TODO ugh
592 | try:
593 | c = io.recv(1)
594 | except socket.timeout:
595 | continue
596 | else:
597 | c = io.read(1)
598 |
599 | if not c:
600 | break
601 | c = c[0]
602 |
603 | crc.update(bytes([c]))
604 |
605 | if mode == MODE_IDLE:
606 | if c == STX:
607 | frame = b''
608 | crc = make_crc()
609 | crc.update(bytes([c]))
610 | mode = MODE_FRAME
611 | elif mode == MODE_FRAME:
612 | if c == DLE:
613 | mode = MODE_FRAME_ESCAPED
614 | elif c == ETX:
615 | mode = MODE_CRC_1
616 | else:
617 | frame += bytes([c])
618 | elif mode == MODE_FRAME_ESCAPED:
619 | frame += bytes([c])
620 | mode = MODE_FRAME
621 | elif mode == MODE_CRC_1:
622 | mode = MODE_CRC_2
623 | elif mode == MODE_CRC_2:
624 | if not crc.crcValue:
625 | handle_frame(frame)
626 | else:
627 | print('Dropping frame at %d: bad CRC:' % br)
628 | print(indent(hexdump.hexdump(frame, 'return')))
629 | print()
630 |
631 | mode = MODE_IDLE
632 |
633 | br += 1
634 |
635 |
636 | if __name__ == '__main__':
637 | main(sys.argv)
638 |
--------------------------------------------------------------------------------
/protocols/gbus/gbus.md:
--------------------------------------------------------------------------------
1 | # G-BUS
2 |
3 | Coming soon! :) Just need to sort out my notes.
4 |
--------------------------------------------------------------------------------
/protocols/hbus/hbus.md:
--------------------------------------------------------------------------------
1 | # H-BUS
2 |
3 | Coming soon! :) I have not investigated past the authentication phase so far (see the talk).
4 |
--------------------------------------------------------------------------------
/protocols/hbus/unframe-hbus.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import crcmod
4 | import hexdump
5 |
6 | import socket
7 | import struct
8 | import sys
9 | import time
10 |
11 |
12 | def prefix(s, p):
13 | return '\n'.join('%s%s' % (p, l) for l in s.splitlines())
14 |
15 |
16 | def indent(s, n=3, tab=False):
17 | return prefix(s, ('\t' if tab else ' ') * n)
18 |
19 |
20 | STX = 0x02
21 | ETX = 0x03
22 | DLE = 0x10
23 |
24 | MODE_IDLE = 0
25 | MODE_FRAME = 1
26 | MODE_FRAME_ESCAPED = 2
27 | MODE_TRAILER = 3
28 |
29 |
30 | def handle_frame(frame):
31 | crc_fn = crcmod.mkCrcFun(0x11EDC6F41, 0, True, 0xFFFFFFFF)
32 | calc_crc = crc_fn(frame[:-4])
33 | crc = struct.unpack('!I', frame[-4:])[0]
34 | assert calc_crc == crc
35 |
36 | to_addr, from_addr, unk1, seq1, seq2, thing = struct.unpack('!HHBBBH',
37 | frame[:9])
38 |
39 | unk2 = thing >> 9
40 | length = thing & ((1 << 9) - 1)
41 | assert length == len(frame[9:]) - 4
42 |
43 | print(('0x%04X -> 0x%04X | sentseq %d nextrecvseq %d | unk1 %02x '
44 | 'unk2 %02x' % (from_addr, to_addr, seq1, seq2, unk1, unk2)))
45 | print(indent(hexdump.hexdump(frame[:9], 'return')))
46 | if length:
47 | print(indent(hexdump.hexdump(frame[9:9+length], 'return')))
48 | print()
49 |
50 |
51 | def main(argv):
52 | io = sys.stdin.buffer if argv[1] == '-' else socket.create_connection(argv[1].split(':'))
53 |
54 | br = 0
55 | mode = MODE_IDLE
56 | while True:
57 | if argv[1] != '-':
58 | pass
59 | else:
60 | c = io.read(1)
61 |
62 | if not c:
63 | break
64 | c = c[0]
65 |
66 | if mode == MODE_IDLE:
67 | if c == STX:
68 | frame = b''
69 | mode = MODE_FRAME
70 | else:
71 | print('Discarded: %02x' % c)
72 | print()
73 | elif mode == MODE_FRAME:
74 | if c == DLE:
75 | mode = MODE_FRAME_ESCAPED
76 | elif c == ETX:
77 | mode = MODE_TRAILER
78 | else:
79 | frame += bytes([c])
80 | elif mode == MODE_FRAME_ESCAPED:
81 | frame += bytes([c ^ DLE])
82 | assert frame[-1] in {DLE, STX, ETX}
83 | mode = MODE_FRAME
84 | elif mode == MODE_TRAILER:
85 | assert c == 0xFF
86 | handle_frame(frame)
87 | mode = MODE_IDLE
88 |
89 | br += 1
90 |
91 |
92 | if __name__ == '__main__':
93 | main(sys.argv)
94 |
--------------------------------------------------------------------------------
/protocols/reader-types.md:
--------------------------------------------------------------------------------
1 | # Reader Types
2 |
3 | These types are used in certain protocol messages to indicate reader (and implicitly, the card) types.
4 |
5 | | Reader type ID | Name | Name in Command Centre | Data format |
6 | |----------------|------|------------------------|-------------|
7 | | `0x01` | 26-bit HID | HID 48 | [12B raw (manchester encoded) HID prox] |
8 | | `0x02` | 37-bit HID | HID 96 | [24B raw (manchester encoded) HID prox] |
9 | | `0x03` | Indala | Motorola | `0xA0 0x00 0x00 0x00` [restB ?] |
10 | | `0x04` | EM4100 | EM (Deister) | `0x95 0x55` [restB ?] |
11 | | `0x05` | ? | CasiRusco | ? |
12 | | `0x81` | ? | ? | ? | ? |
13 | | `0x82` | ? | Unknown | ? |
14 | | `0x83` | ? | ? | ? | ? |
15 | | `0x84` | ? | ? | ? | ? |
16 | | `0xF5` | (Facility code list?) | ? | ([4bit RC] [2B FC])*16 [2B CRC (little-endian)] |
17 | | `0xF6` | ? | ? | ? | ? |
18 | | `0xF7` | MIFARE extended data | ? | [see below](#0xF7-data-format) |
19 | | `0xF8` | MIFARE Enhanced Security | ? | [see below](#0xF8-data-format) |
20 | | `0xF9` | ? | "Unknown" | ? |
21 | | `0xFA` | ? | Data Low | ? |
22 | | `0xFB` | ? | Data High | ? |
23 | | `0xFC` | ? | CardKey | ? |
24 | | `0xFD` | (Used for CSN reads) | Third Party | [4/7B CSN] |
25 | | `0xFE` | Cardax Magstripe | ? | [29B magstripe track 1] |
26 | | `0xFF` | Cardax Prox | ? | [8B cardholder credential block] |
27 |
28 |
29 | ## `0xF8` data format
30 |
31 | ```
32 | [16B MES block] [4/7B CSN] [2B CRC (little-endian)]
33 | ```
34 |
35 | CRC:
36 |
37 | * Is over *all* of a Cardax IV packet (i.e. include opcode and length: `0x03 0x19 0xF8 [data...]`) (even for GBUS!)
38 | * Polynomial = 0x1021 (0x11021)
39 | * Initial value = 0
40 | * Input & result reflected
41 |
42 |
43 | ## `0xF7` data format
44 |
45 | ```
46 | [2B subtype] [8B optional cardholder credential block]
47 | [16B optional MES block] [4/7/10B optional CSN] [2B CRC (little-endian)]
48 | ```
49 |
50 | Subtype:
51 |
52 | * Bits 0-3: MIFARE card type
53 | - 0b00 = MIFARE Classic
54 | - 0b01 = MIFARE Plus (in SL3)
55 | - 0b10 = MIFARE DESFire
56 |
57 | * Bit 4: ? (seen 0 for MIFARE Classic, 1 otherwise)
58 |
59 | * Bit 5: card was proximity checked
60 |
61 | * Bits 8-9: CSN len
62 | - 0b00 = 4B
63 | - 0b01 = 7B
64 | - 0b10 = 10B
65 |
66 | * Bit 13: has cardholder credential block
67 |
68 | * Bit 14: has MES block
69 |
70 | * Bit 15: has CSN
71 |
72 | CRC:
73 |
74 | * Polynomial = 0x1021 (0x11021)
75 | * Initial value = 0
76 | * Input & result reflected
77 |
--------------------------------------------------------------------------------
/sdr/antenna.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/sdr/antenna.jpg
--------------------------------------------------------------------------------
/sdr/grc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/sdr/grc.png
--------------------------------------------------------------------------------
/sdr/plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/sdr/plot.png
--------------------------------------------------------------------------------
/sdr/sdr.md:
--------------------------------------------------------------------------------
1 | # Using Software-Defined Radio to Capture Card Data
2 |
3 | Coming soon! I need to tidy up my source code. (See the talk for an overview in the meantime)
4 |
5 | 
6 |
7 | 
8 |
9 | 
10 |
--------------------------------------------------------------------------------
/software.md:
--------------------------------------------------------------------------------
1 | # Software
2 |
3 | This page outlines the various software components of the Gallagher system, including, where known, their codenames and underlying technologies.
4 |
5 | | Version ID string | Name | Codename | Technologies |
6 | |-------------------|------|----------|-------------- |
7 | | `BT 7.70 b111` | Controller 5000*/3000* | "Bert" | x86, ETS |
8 | | `CA 2.00 b00` | Camera DSP |
9 | | `CB 2.00 b00` | Camera DSP |
10 | | `CC 2.00 b00` | Camera Z181 | | Z180 |
11 | | `CF 1.00 b21` | Class 5 End-of-Line Module (ELM) | | ARM Thumb |
12 | | `CM 2.00 b00` | Camera DSP |
13 | | `ED 2.00 b02` | I/O Interface | | 8051 |
14 | | `EL 7.70 b568` | Command Centre | "Elmo" | x86/x64, Win32, COM |
15 | | `GR 7.70 b121` | Controller 6000 | "Grover" | ARM, WinCE, PE |
16 | | `GU 2.05 b02` | GBUS Universal Reader Interface (URI) |
17 | | `GW 2.02 b03` | GBUS Universal Reader Interface (URI) Wiegand |
18 | | `HBT 1.00 b010` | BLE module | | ARM Thumb |
19 | | `HC 1.01 b41` | F31/32/41/42 Fence Controller | "Frazzle" | ARM Thumb |
20 | | `HF 2.01 b29` | F22 Fence Controller | "Frazzle" | ARM Thumb |
21 | | `HI 1.02 b12` | HBUS I/O | | ARM Thumb |
22 | | `HM 3.04 b75` | HBUS Reader | | ARM Thumb |
23 | | `HP 1.03 b11` | HBUS Module | "Pilchard"? |
24 | | `HT 4.04 b56` | T20 Terminal | | ARM, Linux |
25 | | `HTGA 4.04 b30` | T20 Terminal Gallagher-specific native apps | | ARM, Linux, ELF |
26 | | `HTQT 1.1 b15` | T20 Terminal QT runtime | | ARM, Linux |
27 | | `HTRT 2.0 b57` | T20 Terminal root filesystem | | ARM, Linux |
28 | | `HTST 4.4 b37` | T20 Terminal firmware? | | ARM Thumb |
29 | | `HTWA 4.3 b9` | T20 Terminal Gallagher-specific web apps | | HTML, JS |
30 | | `IE 3.00 b03` | I/O Expansion / 8 Input Expansion |
31 | | `IE 3.02 b09` | High Density I/O Expansion |
32 | | `MP 3.03 b01` | Intelligent Door Terminal (IDT) Mifare Series | | 8051 |
33 | | `NK 3.11 b10` | WinCE kernel | | ARM |
34 | | `OT 2.00 b01` | Intelligent Door Terminal (IDT) 125kHz Series | | 8051 |
35 | | `PL 3.00 b02` | Reader Plug-in Module | "Pilchard"? |
36 | | `PP 2.00 b01` | Intelligent Door Terminal (IDT) Tiris Series | | 8051 |
37 | | `RT 4.02 b00` | Remote Arming Terminal (RAT) |
38 | | `SCTEO 1.0 b007` | Smart Card Tool (Enroll only) | | x86, Win32 |
39 | | `TF 5.05 b01` | Trophy Fence Controller |
40 | | `TW 1.01 b76` | Z10 Tension Sensor | ARM Thumb |
41 | | `UB 1.01 b6033` | Controller 6000 bootloader (U-BOOT?) |
42 | | `W 8.03` | Universal Reader Interface (non-GBUS?; Cardax Commander II era) |
43 |
--------------------------------------------------------------------------------
/timing-attack/beetle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/timing-attack/beetle.jpg
--------------------------------------------------------------------------------
/timing-attack/plot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/megabug/gallagher-research/a83c35e61276432f3ffabdfae367e3aa198084ac/timing-attack/plot.png
--------------------------------------------------------------------------------
/timing-attack/sampler-grapher/README.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | This tool is designed to read the output of the [sampler](../sampler/) tool and produce visual output. This output can be observed to find possible side-channel information leaks in an attempt to find possible timing attack vulnerabilities.
4 |
5 |
6 | ## Requirements
7 |
8 | The [Matplotlib](https://matplotlib.org/) library is required.
9 |
10 |
11 | ## Usage
12 |
13 | ```bash
14 | sampler-grapher.py [valid-facility-code] [output-directory] < sampler-output.json
15 | ```
16 |
17 | `valid-facility-code` is the numeric facility code that is known to be accepted by the controller. Card data messages sent by the sampler that used this facility code will be highlighted in the output.
18 |
19 | `output-directory` is the path to place the output images.
20 |
--------------------------------------------------------------------------------
/timing-attack/sampler-grapher/requirements.txt:
--------------------------------------------------------------------------------
1 | matplotlib
2 |
--------------------------------------------------------------------------------
/timing-attack/sampler-grapher/sampler-grapher.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import json
4 | import os
5 | import sys
6 |
7 | from matplotlib.collections import PatchCollection
8 | from matplotlib.patches import Patch, Rectangle
9 | from matplotlib.cm import Paired
10 | import matplotlib.pyplot as plt
11 |
12 |
13 | CDXIV_FROM_CONTROLLER_COMMANDS = {
14 | 0x07: 'Deny Mifare',
15 | 0x08: 'Deny 125',
16 | 0x0a: 'Enable admin card',
17 | 0x0b: 'Use Mifare extended data format',
18 | 0x0c: "Use high security",
19 | 0x0d: "Don't use high security",
20 | 0x0e: 'Use CSN',
21 | 0x0f: "Don't use CSN",
22 | 0x10: 'Stop buzzer',
23 | 0x11: 'Start buzzer',
24 | 0x18: 'Granted',
25 | 0x19: 'Denied',
26 | 0x21: 'Red LED on',
27 | 0x22: 'Green LED off',
28 | 0x23: 'Red LED off',
29 | 0x24: 'Green LED on',
30 | 0x49: 'Not secure',
31 | 0x4a: 'Hide over bell',
32 | 0x4b: 'Hide under bell',
33 | 0x58: 'Fail',
34 | 0x59: 'Secure',
35 | 0x5a: 'Show over bell',
36 | 0x5b: 'Show under bell',
37 | 0x81: 'Set secure access',
38 | 0x82: 'PIN only',
39 | 0x83: 'Free access',
40 | 0x8a: 'Show wait & failure',
41 | 0x8f: 'Flash all',
42 | 0x90: 'Show default',
43 | 0x91: 'Ask for PIN',
44 | 0x92: 'Display wrong PIN',
45 | 0x93: 'Request second card',
46 | 0x95: 'Flash over bell & under bell & card, show bell',
47 | 0x96: 'Flash over bell & under bell & keypad, show bell',
48 | 0x97: 'Flash bell, start alarm',
49 | 0x98: 'Display set failed',
50 | 0x9f: 'Display standby',
51 | 0xa3: 'Keystroke ACK',
52 | 0xa5: 'Beep',
53 | 0xb1: 'Config MF key? 1',
54 | 0xc0: 'Config MF key? 2',
55 | 0xd5: 'Config MF key? 3',
56 | 0xe0: 'Config MF key? 0 nibble',
57 | 0xe1: 'Config MF key? 1 nibble',
58 | 0xe2: 'Config MF key? 2 nibble',
59 | 0xe3: 'Config MF key? 3 nibble',
60 | 0xe4: 'Config MF key? 4 nibble',
61 | 0xe5: 'Config MF key? 5 nibble',
62 | 0xe6: 'Config MF key? 6 nibble',
63 | 0xe7: 'Config MF key? 7 nibble',
64 | 0xe8: 'Config MF key? 8 nibble',
65 | 0xe9: 'Config MF key? 9 nibble',
66 | 0xea: 'Config MF key? A nibble',
67 | 0xeb: 'Config MF key? B nibble',
68 | 0xec: 'Config MF key? C nibble',
69 | 0xed: 'Config MF key? D nibble',
70 | 0xee: 'Config MF key? E nibble',
71 | 0xef: 'Config MF key? F nibble',
72 | 0xff: 'Heartbeat'
73 | }
74 |
75 |
76 | PREDICTORS = {}
77 |
78 | def predictor(f):
79 | f.color = 'C%d' % len(PREDICTORS)
80 | PREDICTORS[f.__name__.replace('_', '-').replace('-predictor', '')] = f
81 | return f
82 |
83 |
84 | @predictor
85 | def beep_mute_delay_predictor(recvs, last_send_at):
86 | last_recv_beep = None
87 | for recv in recvs:
88 | opcode = recv['recv']['opcode']
89 | if opcode == 0xa5:
90 | last_recv_beep = recv
91 | elif opcode == 0x10:
92 | return (last_recv_beep is None or
93 | recv['at'] - last_recv_beep['at'] >= 50e3 * 1.5)
94 |
95 | return True
96 |
97 |
98 | @predictor
99 | def red_led_predictor(recvs, last_send_at):
100 | return any(recv['recv']['opcode'] in {0x21, 0x23} for recv in recvs)
101 |
102 |
103 | @predictor
104 | def low_recvs_predictor(recvs, last_send_at):
105 | return len(recvs) <= 3
106 |
107 |
108 | @predictor
109 | def immediate_gap_predictor(recvs, last_send_at):
110 | return not recvs or recvs[0]['at'] - last_send_at >= 50e3
111 |
112 |
113 | @predictor
114 | def high_delay_predictor(recvs, last_send_at):
115 | return any(recv.get('_delay', 0) >= 50e3 * 3.5 for recv in recvs)
116 |
117 |
118 | @predictor
119 | def high_rel_delay_predictor(recvs, last_send_at):
120 | for recv in recvs:
121 | if '_delay' in recv:
122 | if abs((recv['_delay'] + 50e3 / 2) % 50e3 - 50e3 / 2) >= 5e3:
123 | return True
124 | return False
125 |
126 |
127 | TESTS = json.load(sys.stdin)
128 |
129 | CORRECT_FC = int(sys.argv[1])
130 |
131 |
132 | for test in TESTS:
133 | events = sorted(test['runs'], key=lambda e: e['at'])
134 |
135 | print('%dus: %d' % (test['send-wait-time'], len(events)))
136 |
137 | RECVS = [event for event in events if 'recv' in event]
138 | SENDS = [event for event in events if 'send' in event]
139 |
140 | WIDTH = 15e6
141 |
142 | CNS = {x: Paired(i) for i, x in enumerate(set(recv['recv']['opcode']
143 | for recv in RECVS))}
144 |
145 | CORRECT_SENDS = [send for send in SENDS if send['send']['fc'] ==
146 | CORRECT_FC]
147 |
148 | for a, b in zip(RECVS[:-1], RECVS[1:]):
149 | a['_delay'] = b['at'] - a['at']
150 |
151 | last_send = None
152 | rs = []
153 | for predictor in PREDICTORS.values():
154 | predictor.tps = predictor.tns = predictor.fps = predictor.fns = 0
155 | for event in events + [{'_dummy': True, 'at': events[-1]['at']}]:
156 | if 'send' in event or '_dummy' in event:
157 | if last_send is not None:
158 | last_send['_width'] = event['at'] - last_send['at']
159 |
160 | for predictor_name, predictor in PREDICTORS.items():
161 | if predictor(rs, last_send['at']):
162 | last_send.setdefault('_predictors', []).append(
163 | predictor)
164 | if last_send['send']['fc'] == CORRECT_FC:
165 | predictor.tps += 1
166 | else:
167 | predictor.fps += 1
168 | else:
169 | if last_send['send']['fc'] == CORRECT_FC:
170 | predictor.fns += 1
171 | else:
172 | predictor.tns += 1
173 |
174 | rs = []
175 | last_send = event
176 | elif 'recv' in event:
177 | rs.append(event)
178 |
179 | NOT_REPEATED_CORRECT_SENDS = []
180 | for correct_send in CORRECT_SENDS:
181 | if not NOT_REPEATED_CORRECT_SENDS or (correct_send['at'] -
182 | NOT_REPEATED_CORRECT_SENDS[-1]['at']) >= 50e3 * 30:
183 | NOT_REPEATED_CORRECT_SENDS.append(correct_send)
184 |
185 | f = plt.figure(figsize=(30, len(NOT_REPEATED_CORRECT_SENDS)))
186 | f.suptitle("%dus send wait time, %d repeat(s)" %
187 | (test['send-wait-time'], test.get('send-repeats', 1)),
188 | fontsize=16)
189 |
190 | handles=([Patch(color=c, label='0x%02X (%s)' %
191 | (l, CDXIV_FROM_CONTROLLER_COMMANDS[l]))
192 | for l, c in sorted(CNS.items())] +
193 | [Patch(color=predictor.color,
194 | label=('%s (%d/%.1f%% TP, %d/%.1f%% TN, %d/%.1f%% FP, '
195 | '%d/%.1f%% FN)' % (
196 | predictor_name,
197 | predictor.tps, predictor.tps / len(CORRECT_SENDS) * 100,
198 | predictor.tns, predictor.tns / (len(SENDS) -
199 | len(CORRECT_SENDS)) * 100,
200 | predictor.fps, predictor.fps / (len(SENDS) -
201 | len(CORRECT_SENDS)) * 100,
202 | predictor.fns, predictor.fns / len(CORRECT_SENDS) * 100)))
203 | for predictor_name, predictor in PREDICTORS.items()])
204 | f.legend(handles=handles)
205 |
206 | for i, base in enumerate(NOT_REPEATED_CORRECT_SENDS):
207 | print('%d/%d ' % (i + 1, len(NOT_REPEATED_CORRECT_SENDS)), end='',
208 | flush=True)
209 |
210 | a = f.add_subplot(len(NOT_REPEATED_CORRECT_SENDS), 1, i + 1)
211 | a.set_xlim(-WIDTH / 2, WIDTH / 2)
212 | a.set_ylim(0, 4)
213 | a.set_xticks(range(-int(WIDTH / 2), int(WIDTH / 2), int(50e3)), True)
214 |
215 | xs = []
216 | ys = []
217 | cs = []
218 | jxs = []
219 | jys = []
220 | jmys = []
221 | correct_send_patches = []
222 | predictor_patches = {predictor_name: []
223 | for predictor_name in PREDICTORS.keys()}
224 | for event in events:
225 | pos = event['at'] - base['at']
226 | if abs(pos) < WIDTH / 2:
227 | if 'send' in event:
228 | a.axvline(pos, zorder=0, color='#d0d0d0')
229 | if event['send']['fc'] == CORRECT_FC:
230 | correct_send_patches.append(Rectangle((pos, 0),
231 | width=event['_width'], height=4))
232 |
233 | for j, (predictor_name, predictor) in enumerate(
234 | PREDICTORS.items()):
235 | if predictor in event.get('_predictors', []):
236 | predictor_patches[predictor_name].append(
237 | Rectangle((pos, 3.75 - j / 4),
238 | width=event['_width'], height=0.25))
239 | elif 'recv' in event:
240 | xs.append(pos)
241 | ys.append(1 if event['recv']['opcode'] in {0x21, 0x23}
242 | else 0.5)
243 | cs.append(CNS[event['recv']['opcode']])
244 | if '_delay' in event:
245 | jxs.append(pos)
246 | jys.append(event['_delay'] / 100e3)
247 | jmys.append(((event['_delay'] + 50e3 / 2) % 50e3 -
248 | 50e3 / 2) / 25e3 + 2)
249 |
250 | a.add_collection(PatchCollection(correct_send_patches,
251 | facecolor='#e8f0e8'))
252 | for predictor_name, patches in predictor_patches.items():
253 | a.add_collection(PatchCollection(patches,
254 | facecolor=PREDICTORS[predictor_name].color))
255 |
256 | a.plot(jxs, jys, c='#d0d0f0', zorder=1)
257 | a.plot(jxs, jmys, c='#f0d0d0', zorder=1)
258 |
259 | a.scatter(xs, ys, 15, cs, 'o', zorder=2)
260 |
261 | print()
262 |
263 | plt.tight_layout()
264 |
265 | fn = 'send-wait-time-%dus_repeats-%d' % (test['send-wait-time'],
266 | test.get('send-repeats', 1))
267 | i = 0
268 | while True:
269 | ffn = os.path.join(sys.argv[2], '%s_N%03d.png' % (fn, i))
270 | if not os.path.exists(ffn):
271 | break
272 | i += 1
273 | plt.savefig(ffn)
274 |
275 | plt.close()
276 |
--------------------------------------------------------------------------------
/timing-attack/sampler/Makefile:
--------------------------------------------------------------------------------
1 | # This makefile depends on Arduino-Makefile
2 | # (https://github.com/sudar/Arduino-Makefile).
3 | #
4 | # You will need to set the ARDMK_DIR variable to the location of
5 | # Arduino-Makefile. Additionally, you will probably want to set some other
6 | # variables used by Arduino-Makefile (i.e. BOARD_TAG, BOARD_SUB and
7 | # MONITOR_PORT).
8 | #
9 | # For example, first set ARDMK_DIR:
10 | # On Debian:
11 | # `export ARDMK_DIR=/usr/share/arduino`
12 | # On macOS with Homebrew:
13 | # `export ARDMK_DIR=/usr/local/Cellar/arduino-mk/[version]`
14 | # Then compile and upload the sketch (e.g. for an Arduino Nano):
15 | # `BOARD_TAG=nano BOARD_SUB=atmega328 MONITOR_PORT=/dev/tty.usbserial-XXXXXXXX make upload`
16 |
17 | include $(ARDMK_DIR)/Arduino.mk
18 |
19 | CXXFLAGS += -Wall -Wextra
20 |
--------------------------------------------------------------------------------
/timing-attack/sampler/README.md:
--------------------------------------------------------------------------------
1 | ## Purpose
2 |
3 | This tool is designed to run on an Arduino connected to a Gallagher controller as a reader. It will repeatedly send a range of valid/invalid card data to the controller and accurately record all its responses, outputting JSON-based logs. These logs can then in turn be processed by the [sampler-grapher](../sampler-grapher/) tool in an attempt to find possible timing attack vulnerabilities.
4 |
5 |
6 | ## Requirements
7 |
8 | An installation of [Arduino-Makefile](https://github.com/sudar/Arduino-Makefile) and the [TimerOne](https://github.com/PaulStoffregen/TimerOne) library are required.
9 |
10 |
11 | ## Building
12 |
13 | Check the makefile for instructions.
14 |
--------------------------------------------------------------------------------
/timing-attack/sampler/main.cpp:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 |
5 | #include
6 |
7 | constexpr uint8_t CARDAX_PROX_MAP[] = {
8 | 0x44, 0x04, 0x51, 0x2E, 0x4D, 0x9A, 0x84, 0xEA,
9 | 0xF8, 0x66, 0x74, 0x29, 0x7F, 0x70, 0xD8, 0x31,
10 | 0x7A, 0x6D, 0xA4, 0x00, 0x82, 0xB9, 0x5F, 0xB4,
11 | 0x16, 0xAB, 0xFF, 0xC2, 0x39, 0xDC, 0x19, 0x65,
12 | 0x57, 0x7C, 0x20, 0xFA, 0x5A, 0x49, 0x13, 0xD0,
13 | 0xFB, 0xA8, 0x91, 0x73, 0xB1, 0x33, 0x18, 0xBE,
14 | 0x21, 0x72, 0x48, 0xB6, 0xDB, 0xA0, 0x5D, 0xCC,
15 | 0xE6, 0x17, 0x27, 0xE5, 0xD4, 0x53, 0x42, 0xF3,
16 | 0xDD, 0x7B, 0x24, 0xAC, 0x2B, 0x58, 0x1E, 0xA7,
17 | 0xE7, 0x86, 0x40, 0xD3, 0x98, 0x97, 0x71, 0xCB,
18 | 0x3A, 0x0F, 0x01, 0x9B, 0x6E, 0x1B, 0xFC, 0x34,
19 | 0xA6, 0xDA, 0x07, 0x0C, 0xAE, 0x37, 0xCA, 0x54,
20 | 0xFD, 0x26, 0xFE, 0x0A, 0x45, 0xA2, 0x2A, 0xC4,
21 | 0x12, 0x0D, 0xF5, 0x4F, 0x69, 0xE0, 0x8A, 0x77,
22 | 0x60, 0x3F, 0x99, 0x95, 0xD2, 0x38, 0x36, 0x62,
23 | 0xB7, 0x32, 0x7E, 0x79, 0xC0, 0x46, 0x93, 0x2F,
24 | 0xA5, 0xBA, 0x5B, 0xAF, 0x52, 0x1D, 0xC3, 0x75,
25 | 0xCF, 0xD6, 0x4C, 0x83, 0xE8, 0x3D, 0x30, 0x4E,
26 | 0xBC, 0x08, 0x2D, 0x09, 0x06, 0xD9, 0x25, 0x9E,
27 | 0x89, 0xF2, 0x96, 0x88, 0xC1, 0x8C, 0x94, 0x0B,
28 | 0x28, 0xF0, 0x47, 0x63, 0xD5, 0xB3, 0x68, 0x56,
29 | 0x9C, 0xF9, 0x6F, 0x41, 0x50, 0x85, 0x8B, 0x9D,
30 | 0x59, 0xBF, 0x9F, 0xE2, 0x8E, 0x6A, 0x11, 0x23,
31 | 0xA1, 0xCD, 0xB5, 0x7D, 0xC7, 0xA9, 0xC8, 0xEF,
32 | 0xDF, 0x02, 0xB8, 0x03, 0x6B, 0x35, 0x3E, 0x2C,
33 | 0x76, 0xC9, 0xDE, 0x1C, 0x4B, 0xD1, 0xED, 0x14,
34 | 0xC5, 0xAD, 0xE9, 0x64, 0x4A, 0xEC, 0x8D, 0xF7,
35 | 0x10, 0x43, 0x78, 0x15, 0x87, 0xE4, 0xD7, 0x92,
36 | 0xE1, 0xEE, 0xE3, 0x90, 0xA3, 0xB0, 0x80, 0xC6,
37 | 0xB2, 0xF4, 0x5C, 0x6C, 0x81, 0xF1, 0xBB, 0xEB,
38 | 0x55, 0x67, 0x3C, 0x05, 0x1A, 0x0E, 0x61, 0xF6,
39 | 0x22, 0xCE, 0xAA, 0x8F, 0xBD, 0x3B, 0x1F, 0x5E
40 | };
41 |
42 | constexpr auto PIN_IN = 2, PIN_OUT = 3;
43 |
44 | constexpr auto RING_LEN = 1 << 6;
45 |
46 | uint16_t in_buf;
47 | auto in_bit_len = 0U;
48 | unsigned long in_start, in_bit_start;
49 | volatile unsigned long in_start_ring[RING_LEN];
50 | volatile uint8_t in_opcode_ring[RING_LEN];
51 | volatile auto in_ring_head = 0U, in_ring_tail = 0U;
52 |
53 | template
54 | constexpr size_t countof (T const (&)[N]) {
55 | return N;
56 | }
57 |
58 | constexpr unsigned long ms2us (unsigned long t) {
59 | return t * 1000;
60 | }
61 |
62 | constexpr unsigned long s2ms (unsigned long t) {
63 | return t * 1000;
64 | }
65 |
66 | uint8_t xor_bytes (const uint8_t *b, size_t len) {
67 | auto r = 0U;
68 |
69 | while (len--) {
70 | r ^= *(b++);
71 | }
72 |
73 | return r;
74 | }
75 |
76 | char hex_to_char (unsigned int i, bool upper = true) {
77 | if (i < 10) {
78 | return '0' + i;
79 | } else if (i < 16) {
80 | return (upper ? 'A' : 'a') + i - 10;
81 | } else {
82 | assert(false);
83 | }
84 | }
85 |
86 | void in_isr () {
87 | if (PIND & (1 << PIN_IN)) {
88 | in_buf <<= 1;
89 | if (micros() - in_bit_start >= 400) {
90 | in_buf |= 1;
91 | }
92 | ++in_bit_len;
93 | } else {
94 | in_bit_start = micros();
95 | if (!in_bit_len) {
96 | in_start = in_bit_start;
97 | }
98 | }
99 | }
100 |
101 | void timer_isr () {
102 | if (micros() - in_bit_start < 3000) {
103 | return;
104 | }
105 |
106 | if (in_bit_len == 16 && ((in_buf >> 8) & 0xff) == (~in_buf & 0xff) &&
107 | ((in_ring_head + 1) & (RING_LEN - 1))
108 | != in_ring_tail) {
109 | in_opcode_ring[in_ring_head] = in_buf >> 8;
110 | in_start_ring[in_ring_head] = in_start;
111 | in_ring_head = (in_ring_head + 1) & (RING_LEN - 1);
112 | }
113 |
114 | in_buf = 0;
115 | in_bit_len = 0U;
116 | }
117 |
118 | bool recv_opcode (uint8_t& opcode, unsigned long& start) {
119 | #ifdef FAKE_INPUT
120 | delay(random(s2ms(1)));
121 | opcode = random(256);
122 | start = micros();
123 | return true;
124 | #endif
125 |
126 | auto r = false;
127 |
128 | noInterrupts();
129 |
130 | if (in_ring_tail != in_ring_head) {
131 | opcode = in_opcode_ring[in_ring_tail];
132 | start = in_start_ring[in_ring_tail];
133 | in_ring_tail = (in_ring_tail + 1) & (RING_LEN - 1);
134 | r = true;
135 | }
136 |
137 | interrupts();
138 |
139 | return r;
140 | }
141 |
142 | void send_bytes (const uint8_t * const bytes, size_t len,
143 | unsigned long *start_time = nullptr) {
144 | if (start_time) {
145 | *start_time = micros();
146 | }
147 |
148 | for (auto i = 0U; i < len; ++i) {
149 | for (auto j = 0; j < 8; ++j) {
150 | const auto t = bytes[i] & (1 << (7 - j)) ? 168 : 47;
151 |
152 | PORTD |= 1 << PIN_OUT;
153 | delayMicroseconds(t);
154 | PORTD &= ~(1 << PIN_OUT);
155 | delayMicroseconds(231 - t);
156 | }
157 | }
158 | }
159 |
160 | uint8_t map_cardax_prox (uint8_t in) {
161 | return CARDAX_PROX_MAP[(in + 0xE4) & 0xFF];
162 | }
163 |
164 | void write_cardax_prox (uint8_t * const out, uint8_t region_code,
165 | uint16_t facility_code, uint32_t card_number,
166 | uint8_t issue_level, uint8_t unk_b = 0,
167 | uint8_t unk_c = 0, uint8_t unk_d = 0,
168 | uint8_t unk_e = 0) {
169 | assert(region_code <= 0xF);
170 | assert(card_number <= 0xFFFFFF);
171 | assert(issue_level <= 0xF);
172 | assert(unk_b <= 0xF);
173 | assert(unk_c <= 0xF);
174 | assert(unk_d <= 0xF);
175 | assert(unk_e <= 0xF);
176 |
177 | out[0] = map_cardax_prox(card_number >> 16);
178 | out[1] = map_cardax_prox(facility_code >> 4);
179 | out[2] = map_cardax_prox(card_number >> 3);
180 | out[3] = map_cardax_prox(
181 | ((card_number & 0xF) << 5) |
182 | (region_code << 1) |
183 | (unk_b >> 4));
184 | out[4] = map_cardax_prox(
185 | ((unk_b & 0xF) << 5) |
186 | ((card_number >> 11) & 0x1F));
187 | out[5] = map_cardax_prox(
188 | (unk_e << 4) |
189 | (facility_code >> 12));
190 | out[6] = map_cardax_prox(
191 | (unk_c << 4) |
192 | unk_d);
193 | out[7] = map_cardax_prox(
194 | ((facility_code & 0xF) << 4) |
195 | issue_level);
196 | }
197 |
198 | void setup () {
199 | Serial.begin(115200);
200 | while(!Serial);
201 |
202 | pinMode(PIN_IN, INPUT_PULLUP);
203 | attachInterrupt(digitalPinToInterrupt(PIN_IN), in_isr, CHANGE);
204 | Timer1.initialize(1500);
205 | Timer1.attachInterrupt(timer_isr);
206 |
207 | pinMode(PIN_OUT, OUTPUT);
208 | digitalWrite(PIN_OUT, LOW);
209 |
210 | randomSeed(123);
211 |
212 | Serial.println("ready");
213 | }
214 |
215 | void do_run (const unsigned long send_wait_time, const size_t send_repeats,
216 | const uint16_t min_fc, const uint16_t max_fc,
217 | const uint32_t base_cn, bool& first) {
218 | for (auto fc = min_fc; fc <= max_fc; ++fc) {
219 | for (auto i = 0U; i < send_repeats; ++i) {
220 | const auto cn = base_cn - i;
221 |
222 | uint8_t card_read[1 + 8 + 1];
223 | card_read[0] = 0x01;
224 | write_cardax_prox(&card_read[1], 0, fc, cn, 1);
225 | card_read[1 + 8] = xor_bytes(&card_read[1], 8);
226 |
227 | unsigned long send_start_time;
228 | send_bytes(card_read, countof(card_read),
229 | &send_start_time);
230 |
231 | if (!first) {
232 | Serial.println(',');
233 | }
234 | first = false;
235 |
236 | Serial.print("\t\t{\"at\": ");
237 | Serial.print(send_start_time);
238 | Serial.print(", \"send\": {\"fc\": ");
239 | Serial.print(fc);
240 | Serial.print(", \"cn\": ");
241 | Serial.print(cn);
242 | Serial.print("}}");
243 |
244 | while (micros() - send_start_time < send_wait_time) {
245 | uint8_t opcode;
246 | unsigned long recv_start_time;
247 | if (recv_opcode(opcode, recv_start_time)) {
248 | Serial.println(',');
249 |
250 | Serial.print("\t\t{\"at\": ");
251 | Serial.print(recv_start_time);
252 | Serial.print(", \"recv\": "
253 | "{\"opcode\": ");
254 | Serial.print(opcode);
255 | Serial.print("}}");
256 | }
257 | }
258 | }
259 | }
260 | }
261 |
262 | void loop () {
263 | Serial.println("[");
264 |
265 | const auto send_repeats = 3;
266 |
267 | bool first1 = true;
268 | for (auto send_wait_time = ms2us(200); send_wait_time <= ms2us(200);
269 | send_wait_time += ms2us(50)) {
270 | if (!first1) {
271 | Serial.println(',');
272 | }
273 | first1 = false;
274 |
275 | Serial.print("\t{\"send-wait-time\": ");
276 | Serial.print(send_wait_time);
277 | Serial.print(", \"send-repeats\": ");
278 | Serial.print(send_repeats);
279 | Serial.println(", \"runs\": [");
280 |
281 | delay(s2ms(10));
282 | uint8_t trash;
283 | unsigned long trash2;
284 | while (recv_opcode(trash, trash2));
285 |
286 | bool first2 = true;
287 | for (auto run = 0; run < 50; ++run) {
288 | do_run(send_wait_time, send_repeats, 50, 70,
289 | (1UL << 23) - 1, first2);
290 | }
291 |
292 | Serial.println();
293 | Serial.print("\t]}");
294 | }
295 |
296 | Serial.println();
297 | Serial.println("]");
298 |
299 | Serial.println("done");
300 | for (;;);
301 | }
302 |
--------------------------------------------------------------------------------
/timing-attack/timing-attack.md:
--------------------------------------------------------------------------------
1 | # Determining valid FCs and CNs via timing attack
2 |
3 | Coming soon! I need to tidy my code up and make the tool more useful friendly...
4 |
5 | 
6 |
7 | 
8 |
--------------------------------------------------------------------------------