Implement Gallagher MIFARE Classic card writing and update MAD sector functionality

This commit is contained in:
pingu2211
2026-03-10 15:13:42 +11:00
parent e4443c68cd
commit c07d688597
9 changed files with 902 additions and 65 deletions
+1
View File
@@ -359,6 +359,7 @@ set (TARGET_SOURCES
${PM3_ROOT}/client/src/mifare/desfirecore.c
${PM3_ROOT}/client/src/mifare/desfiretest.c
${PM3_ROOT}/client/src/mifare/gallaghercore.c
${PM3_ROOT}/client/src/mifare/gallaghertest.c
${PM3_ROOT}/client/src/uart/ringbuffer.c
${PM3_ROOT}/client/src/uart/uart_common.c
${PM3_ROOT}/client/src/uart/uart_posix.c
+1
View File
@@ -795,6 +795,7 @@ SRCS = mifare/aiddesfire.c \
mifare/desfiresecurechan.c \
mifare/desfiretest.c \
mifare/gallaghercore.c \
mifare/gallaghertest.c \
mifare/mad.c \
mifare/mfkey.c \
mifare/mifare4.c \
+1
View File
@@ -279,6 +279,7 @@ set (TARGET_SOURCES
${PM3_ROOT}/client/src/mifare/desfirecore.c
${PM3_ROOT}/client/src/mifare/desfiretest.c
${PM3_ROOT}/client/src/mifare/gallaghercore.c
${PM3_ROOT}/client/src/mifare/gallaghertest.c
${PM3_ROOT}/client/src/uart/ringbuffer.c
${PM3_ROOT}/client/src/uart/uart_common.c
${PM3_ROOT}/client/src/uart/uart_posix.c
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -37,8 +37,8 @@ int CmdHFGallagher(const char *cmd);
* @param keyOut Buffer to copy the diversified key into (must be 16 bytes).
* @return PM3_SUCCESS if successful, PM3_EINVARG if an argument is invalid.
*/
int hfgal_diversify_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len,
uint8_t key_num, uint32_t aid, uint8_t *key_output);
int hfgal_diversify_desfire_key(uint8_t *site_key, uint8_t *uid, uint8_t uid_len,
uint8_t key_num, uint32_t aid, uint8_t *key_output);
// The response code when an invalid key is used for authentication
// Returned in /client/src/mifare/desfirecore.c, line 1185 (if DesfireExchangeEx fails)
+171
View File
@@ -16,8 +16,10 @@
// Common functionality for low/high-frequency GALLAGHER tag encoding & decoding.
//-----------------------------------------------------------------------------
#include "gallaghercore.h"
#include "aes.h"
#include "common.h"
#include "ui.h"
#include <string.h>
static void scramble(uint8_t *arr, uint8_t len) {
const uint8_t lut[] = {
@@ -69,6 +71,51 @@ static void descramble(uint8_t *arr, uint8_t len) {
}
}
int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output) {
memcpy(key_output, site_key, 16);
for (int i = 0; i < csn_len; i++) {
key_output[i] = site_key[i] ^ csn[i];
}
return PM3_SUCCESS;
}
int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key) {
creds->region_code = region;
creds->facility_code = facility;
creds->card_number = card;
creds->issue_level = issue;
creds->mes = mes;
memcpy(creds->csn, csn, csn_len);
memcpy(creds->site_key, site_key, 16);
return PM3_SUCCESS;
}
int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility) {
// CAD entries are 3.5 bytes each (28 bits: 4-bit RC, 16-bit FC, 8-bit sector).
// Packed in pairs of 7 bytes starting at byte 4, up to 6 pairs (12 entries).
for (int pair = 0; pair < 6; pair++) {
int base = 4 + pair * 7;
// Even entry (first 3.5 bytes of pair)
uint8_t rc = (cad[base] >> 4) & 0x0F;
uint16_t fc = ((cad[base] & 0x0F) << 12) | (cad[base + 1] << 4) | ((cad[base + 2] >> 4) & 0x0F);
uint8_t sector = ((cad[base + 2] & 0x0F) << 4) | ((cad[base + 3] >> 4) & 0x0F);
if (rc == region && fc == facility) {
return sector;
}
// Odd entry (last 3.5 bytes of pair)
rc = cad[base + 3] & 0x0F;
fc = (cad[base + 4] << 8) | cad[base + 5];
sector = cad[base + 6];
if (rc == region && fc == facility) {
return sector;
}
}
return -1;
}
void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds) {
uint8_t *arr = eight_bytes;
@@ -107,6 +154,112 @@ void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds)
scramble(eight_bytes, 8);
}
int gallagher_encode_mes(uint8_t *sixteen_bytes, GallagherCredentials_t *creds) {
// unknown parameters from the research these might be for UUID's longer than 4 bytes?
uint8_t UB = 0x00;
uint8_t UC = 0x00;
uint8_t UD = 0x00;
uint8_t UE = 0x00;
uint8_t PO = 0x00; // Pin offset
uint8_t UX = 0x00;
uint16_t R = 0x0748;
uint8_t mes[16];
uint8_t diversified_site_key[16];
if (creds->csn_len > 4) {
PrintAndLogEx(ERR, "Credential could not be encoded into a Mifare Enhanced Encryption block. only 4 byte UUID's are supported");
return PM3_ENOTIMPL;
}
mes[0] = 0x01;
mes[1] = (creds->card_number & 0xFF0000) >> 16;
mes[2] = (creds->card_number & 0x00FF00) >> 8;
mes[3] = creds->card_number & 0x0000FF;
mes[4] = (creds->facility_code & 0xFF00) >> 8;
mes[5] = creds->facility_code & 0x00FF;
mes[6] = ((creds->region_code & 0x0F) << 4) | (creds->issue_level & 0x0F);
mes[7] = (PO & 0x0F) | ((UX & 0x0F) << 4);
mes[8] = (UB & 0x0F) | ((UC & 0x0F) << 4);
mes[9] = (UD & 0x0F) | ((UE & 0x0F) << 4);
mes[10] = creds->csn[0];
mes[11] = creds->csn[1];
mes[12] = creds->csn[2];
mes[13] = creds->csn[3];
mes[14] = (R & 0xFF00) >> 8;
mes[15] = R & 0x00FF;
PrintAndLogEx(DEBUG, "MES before encryption %s", sprint_hex_ascii(mes, 16));
gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key);
mbedtls_aes_context actx;
mbedtls_aes_init(&actx);
if (mbedtls_aes_setkey_enc(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY;
if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_ENCRYPT, mes, sixteen_bytes) != 0) return PM3_ENOKEY;;
mbedtls_aes_free(&actx);
PrintAndLogEx(DEBUG, "MES after encryption %s", sprint_hex_ascii(sixteen_bytes, 16));
return PM3_SUCCESS;
}
int gallagher_decode_mes(uint8_t *block, GallagherCredentials_t *creds) {
// unknown parameters from the research these might be for UUID's longer than 4 bytes?
// uint8_t UB = 0x00;
// uint8_t UC = 0x00;
// uint8_t UD = 0x00;
// uint8_t UE = 0x00;
// uint8_t PO = 0x00;
// uint8_t UX = 0x00;
uint16_t R = 0x0748;
uint8_t mes[16];
uint8_t diversified_site_key[16];
gallagher_diversify_classic_key(creds->site_key, creds->csn, creds->csn_len, diversified_site_key);
if (creds->csn_len > 4) {
PrintAndLogEx(WARNING, "UUID length is > 4, this may not be a valid gallagher credential?");
}
// AES decrypt 16 bytes
mbedtls_aes_context actx;
mbedtls_aes_init(&actx);
if (mbedtls_aes_setkey_dec(&actx, diversified_site_key, 128) != 0) return PM3_ENOKEY;
if (mbedtls_aes_crypt_ecb(&actx, MBEDTLS_AES_DECRYPT, block, mes) != 0) return PM3_ENOKEY;;
mbedtls_aes_free(&actx);
PrintAndLogEx(DEBUG, "MES after decryption %s", sprint_hex_ascii(mes, 16));
if (mes[0] != 0x01) {
PrintAndLogEx(ERR, "MES block is not valid");
return PM3_EWRONGANSWER;
}
creds->card_number = mes[1] << 16 | mes[2] << 8 | mes[3];
creds->facility_code = mes[4] << 8 | mes[5];
creds->region_code = (mes[6] & 0xF0) >> 4;
creds->issue_level = mes[6] & 0x0F;
// PO = mes[7] & 0x0F;
// UX = (mes[7] & 0xF0) >> 4;
// UB = mes[8] & 0x0F;
// UC = (mes[8] & 0xF0) >> 4;
// UD = mes[9] & 0x0F;
// UE = (mes[9] & 0xF0) >> 4;
// csn is already verified by key diversification
// csn[0] = mes[10];
// csn[1] = mes[11];
// csn[2] = mes[12];
// csn[3] = mes[13];
R = mes[14] << 8 | mes[15];
if (R != 0x0748) {
PrintAndLogEx(WARNING, "R value is different from 0x0748, this hasn't been seen in the wild \n https://github.com/megabug/gallagher-research/blob/master/formats/mes.md");
}
return PM3_SUCCESS;
}
bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds) {
return gallagher_is_valid_creds(creds->region_code, creds->facility_code, creds->card_number, creds->issue_level);
}
bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level) {
bool is_valid = true;
@@ -129,3 +282,21 @@ bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint
}
return is_valid;
}
void print_gallagher_creds(GallagherCredentials_t *creds) {
if (!gallagher_is_valid_creds_struct(creds)) {
PrintAndLogEx(ERR, "Invalid Gallagher credential");
return;
}
PrintAndLogEx(SUCCESS, "Gallagher - region: " _GREEN_("%c") " ( " _GREEN_("%u") " )"
", facility: " _GREEN_("%u")
", card number: " _GREEN_("%u")
", issue level: " _GREEN_("%u"),
'A' + creds->region_code,
creds->region_code,
creds->facility_code,
creds->card_number,
creds->issue_level
);
}
+19
View File
@@ -17,6 +17,7 @@
#define MIFARE_GALLAGHERCORE_H__
#include "common.h"
#include "crypto/libpcrypto.h"
#include <stdint.h>
typedef struct {
@@ -24,12 +25,30 @@ typedef struct {
uint16_t facility_code;
uint32_t card_number;
uint8_t issue_level;
bool mes;
uint8_t csn[10];
size_t csn_len;
uint8_t site_key[16];
} GallagherCredentials_t;
int gallagher_diversify_classic_key(uint8_t *site_key, uint8_t *csn, size_t csn_len, uint8_t *key_output);
int gallagher_read_cad(uint8_t *cad, uint8_t region, uint16_t facility);
void gallagher_encode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds);
void gallagher_decode_creds(uint8_t *eight_bytes, GallagherCredentials_t *creds);
int gallagher_construct_credential(GallagherCredentials_t *creds, uint8_t region, uint16_t facility, uint32_t card, uint8_t issue, bool mes, uint8_t *csn, size_t csn_len, uint8_t *site_key);
int gallagher_encode_mes(uint8_t *sector, GallagherCredentials_t *creds);
int gallagher_decode_mes(uint8_t *sector, GallagherCredentials_t *creds);
bool gallagher_is_valid_creds(uint64_t region_code, uint64_t facility_code, uint64_t card_number, uint64_t issue_level);
bool gallagher_is_valid_creds_struct(GallagherCredentials_t *creds);
void print_gallagher_creds(GallagherCredentials_t *creds);
#endif
+196
View File
@@ -0,0 +1,196 @@
#include "gallaghertest.h"
#include <unistd.h>
#include <string.h> // memcpy memset
#include "ui.h"
#include "crc.h"
#include "mifare/gallaghercore.h"
static bool creds_match(GallagherCredentials_t *a, GallagherCredentials_t *b) {
return a->region_code == b->region_code &&
a->facility_code == b->facility_code &&
a->card_number == b->card_number &&
a->issue_level == b->issue_level;
}
static bool test_CAD(void) {
// Example CAD sector from https://github.com/megabug/gallagher-research/blob/master/formats/card-specific/mifare-classic.md
uint8_t cad[] = {0x1B, 0x58, 0x00, 0x01, 0xC1, 0x33, 0x70, 0xFD, 0x13, 0x38, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
// Entry 0: RC=0xC, FC=0x1337, Sector=0x0F
int result = gallagher_read_cad(cad, 0xC, 0x1337);
if (result != 0x0F) {
PrintAndLogEx(INFO, "Gallagher CAD test 1 failed: expected sector 0x0F, got 0x%02X", result);
return false;
}
// Entry 1: RC=0xD, FC=0x1338, Sector=0x0D
result = gallagher_read_cad(cad, 0xD, 0x1338);
if (result != 0x0D) {
PrintAndLogEx(INFO, "Gallagher CAD test 2 failed: expected sector 0x0D, got 0x%02X", result);
return false;
}
// Non-existent entry should return -1
result = gallagher_read_cad(cad, 0xA, 0x1234);
if (result != -1) {
PrintAndLogEx(INFO, "Gallagher CAD test 3 failed: expected -1, got %d", result);
return false;
}
return true;
}
static bool test_creds(void) {
GallagherCredentials_t creds1 = {
.region_code = 0x0,
.facility_code = 0x0,
.card_number = 0x0,
.issue_level = 0x0,
};
GallagherCredentials_t creds2 = {
.region_code = 0x1,
.facility_code = 0x2,
.card_number = 0x20,
.issue_level = 0x1,
};
GallagherCredentials_t cred_result = {0};
uint8_t bytes_result[8] = {0};
gallagher_encode_creds(bytes_result, &creds1);
gallagher_decode_creds(bytes_result, &cred_result);
if (!creds_match(&cred_result, &creds1)) {
PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 1 failed");
return false;
}
gallagher_encode_creds(bytes_result, &creds2);
gallagher_decode_creds(bytes_result, &cred_result);
if (!creds_match(&cred_result, &creds2)) {
PrintAndLogEx(INFO, "Gallagher encode/decode roundtrip test 2 failed");
return false;
}
return true;
}
// Test decode/encode against known real-world data from the documentation
static bool test_known_vector_creds(void) {
// From doc: 0xA3B4B0C151B0A31B decodes to RC=12, FC=4919(0x1337), CN=61453(0xF00D), IL=1
uint8_t known_bytes[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B};
GallagherCredentials_t expected = {
.region_code = 12,
.facility_code = 4919,
.card_number = 61453,
.issue_level = 1,
};
// Test decode
GallagherCredentials_t result = {0};
gallagher_decode_creds(known_bytes, &result);
if (!creds_match(&expected, &result)) {
PrintAndLogEx(INFO, "Known vector decode failed: RC=%d FC=%d CN=%d IL=%d",
result.region_code, result.facility_code, result.card_number, result.issue_level);
return false;
}
// Test encode roundtrip
uint8_t encoded[8] = {0};
gallagher_encode_creds(encoded, &expected);
if (memcmp(encoded, known_bytes, 8) != 0) {
PrintAndLogEx(INFO, "Known vector encode failed");
return false;
}
// Verify bitwise inverse (block 0 format: 8-byte creds + 8-byte inverse)
uint8_t known_block0[] = {0xA3, 0xB4, 0xB0, 0xC1, 0x51, 0xB0, 0xA3, 0x1B,
0x5C, 0x4B, 0x4F, 0x3E, 0xAE, 0x4F, 0x5C, 0xE4
};
for (int i = 0; i < 8; i++) {
if ((uint8_t)(known_block0[i] ^ 0xFF) != known_block0[i + 8]) {
PrintAndLogEx(INFO, "Bitwise inverse check failed at byte %d", i);
return false;
}
}
return true;
}
// Test MAD CRC against known sector 0 data from the documentation
static bool test_mad_crc(void) {
// Full sector 0 from documentation (blocks 0-2, excluding trailer)
uint8_t sector0[64] = {
// Block 0 (manufacturer)
0xE3, 0x51, 0x54, 0x3C, 0xDA, 0x08, 0x04, 0x00, 0x01, 0x6F, 0x01, 0x6D, 0x45, 0x68, 0xF8, 0x1D,
// Block 1 (MAD: CRC, info byte, AIDs 1-7)
0xBD, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// Block 2 (MAD: AIDs 8-15)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x48, 0x11, 0x48, 0x12, 0x48,
// Block 3 (sector trailer - not part of CRC)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x77, 0x88, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
// MAD v1 CRC: computed over sector0[17..47] (info byte + 15 AID pairs = 31 bytes)
uint8_t expected_crc = sector0[16]; // 0xBD
uint8_t computed_crc = CRC8Mad(&sector0[16 + 1], 15 + 16);
if (computed_crc != expected_crc) {
PrintAndLogEx(INFO, "MAD CRC test failed: expected 0x%02X, got 0x%02X", expected_crc, computed_crc);
return false;
}
return true;
}
static bool test_MES(void) {
uint8_t csn[] = {0x3C, 0x54, 0x51, 0xE3};
uint8_t csn_len = 4;
uint8_t site_key[] = {0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D, 0x13, 0x37, 0xD0, 0x0D};
GallagherCredentials_t known_cred;
gallagher_construct_credential(&known_cred, 12, 0x1337, 0xF00D, 1, true, csn, csn_len, site_key);
GallagherCredentials_t result_creds = {0};
gallagher_construct_credential(&result_creds, 0, 0, 0, 0, true, csn, csn_len, site_key);
uint8_t sector_result[16] = {0};
uint8_t known_sector[16] = {0x4F, 0x36, 0xB7, 0x4E, 0xFF, 0xCD, 0x76, 0xEF, 0xED, 0xA5, 0x74, 0x58, 0xC8, 0xB4, 0xE3, 0x04};
// Test encode
gallagher_encode_mes(sector_result, &known_cred);
if (memcmp(sector_result, known_sector, 16) != 0) {
PrintAndLogEx(INFO, "Gallagher MES encode test failed");
PrintAndLogEx(INFO, "Expected: %s", sprint_hex_ascii(known_sector, 16));
PrintAndLogEx(INFO, "Got: %s", sprint_hex_ascii(sector_result, 16));
return false;
}
// Test decode
if (gallagher_decode_mes(known_sector, &result_creds) != PM3_SUCCESS) {
PrintAndLogEx(INFO, "Gallagher MES decode test failed");
return false;
}
if (!creds_match(&known_cred, &result_creds)) {
PrintAndLogEx(INFO, "Gallagher MES decoded different creds than expected");
return false;
}
return true;
}
bool GallagherTest(bool verbose) {
bool result = true;
result &= test_CAD();
result &= test_creds();
result &= test_known_vector_creds();
result &= test_mad_crc();
result &= test_MES();
return result;
}
+27
View File
@@ -0,0 +1,27 @@
//-----------------------------------------------------------------------------
// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// See LICENSE.txt for the text of the license.
//-----------------------------------------------------------------------------
// tests for desfire
//-----------------------------------------------------------------------------
#ifndef __GALLAGHERTEST_H__
#define __GALLAGHERTEST_H__
#include <stdbool.h>
#include "common.h"
bool GallagherTest(bool verbose);
#endif /* __GALLAGHERTEST_H__ */