Merge branch 'master' into fix-readable-mfc-keyb

Signed-off-by: oSPANNERo <25675033+oSPANNERo@users.noreply.github.com>
This commit is contained in:
oSPANNERo
2026-08-18 18:37:11 -04:00
committed by GitHub
12 changed files with 2369 additions and 15 deletions
+5
View File
@@ -107,6 +107,11 @@ jobs:
- uses: actions/checkout@v6
# TODO workaround: "error: linker script file 'xxxx' appears multiple times"
# This should be reverted once MSYS2 ships a non-broken version of the linker.
- name: Downgrade ARM linker (Windows, temporary)
run: pacman -U --noconfirm https://repo.msys2.org/mingw/ucrt64/mingw-w64-ucrt-x86_64-arm-none-eabi-binutils-2.46.1-1-any.pkg.tar.zst
- name: make clean
run: make clean
+3 -1
View File
@@ -3,7 +3,9 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...
## [unreleased][unreleased]
- Fixed `hf mf dump` replacing readable sector trailer Key B values with values from the key file (@oSPANNERo)
- Fixed `hf mf dump` preserving readable sector trailer Key B data instead of overwriting it with values from the supplied key file (@oSPANNERo)
- Changed `magic_cards_notes.md` - documented the USCUID-UL helper scripts (`hf_mfu_uscuid` / `hf_mf_uscuid_prog`) and how to set the tag signature, replacing the outdated "No implemented commands" note (@c-barron)
- Fixed `hf_mf_uscuid_prog.lua` - corrected the script name shown in its usage text (@c-barron)
- Added `hf felica seacauth1` command
- Added `lf trovan` commands to support Trovan Animal ID (@iceman1001)
- Added `hf mf gdmgetblk/gdmgethidblk/gdmsethidblk/gdmsetuid/gdmwipe/gdmsetsig` (@0x6r1an0y)
+1
View File
@@ -2259,6 +2259,7 @@ FC9418BF788B
# Guest Cashless Prepaid Arcade Payment Cards
168168168168
198407157610
248B4520504D
4E4F584D2101
4E4F584D2105
686B35333376
+1 -1
View File
@@ -44,7 +44,7 @@ example = [[
3. script run hf_mf_uscuid_prog -S 0
]]
usage = [[
script run hf_mf_uscuid_uid_prog [-h] [-u <uid>] [-t] [-3] [-s <signature>] [-w 1] [-R -B <blk>] [-S -E <sec>] [-g -c -b -2 -7 -d -a -n -r <0/1>]
script run hf_mf_uscuid_prog [-h] [-u <uid>] [-t] [-3] [-s <signature>] [-w 1] [-R -B <blk>] [-S -E <sec>] [-g -c -b -2 -7 -d -a -n -r <0/1>]
]]
arguments = [[
-h this help
+1 -1
View File
@@ -251,7 +251,7 @@
"AID": "F47300",
"Vendor": "Inner Range Pty Ltd",
"Country": "AU",
"Name": "SIFER-P / SIFER-U Credential",
"Name": "SIFER-P / SIFER-U / SIFER-C Credential",
"Description": "Inner Range Access Control",
"Type": "pacs"
},
+11
View File
@@ -3715,6 +3715,17 @@
"android://eu.sharry.swp.sharryworkplace"
]
},
{
"AID": "A0000007800007",
"Vendor": "Translink systems",
"Country": "The Netherlands",
"Name": "OV Pas",
"Description": "New card for Dutch transit system, closed-loop EMV replacement of the MIFARE Classic 4K OV-Chipkaart",
"Type": "transport",
"Sources": [
"https://blog.mobielstraat.nl/quick-look-at-the-new-ov-pas"
]
},
{
"AID": "A0000000791000",
"Vendor": "HID Global",
+4
View File
@@ -37,6 +37,10 @@
"id": "250091",
"name": "CTS Strasbourg"
},
{
"id": "250303",
"name": "TAO / Orléans"
},
{
"id": "250502",
"name": "Oura",
File diff suppressed because it is too large Load Diff
+115 -4
View File
@@ -21,6 +21,7 @@
#include <string.h>
#include <ctype.h>
#include <inttypes.h>
#include <time.h>
#include "cmdparser.h" // command_t
#include "comms.h"
#include "cmdtrace.h"
@@ -82,6 +83,7 @@
#define FELICA_POLLING_REQUEST_SYSTEM_CODE 0x01U
#define FELICA_SYSTEM_LIST_JSON "felica/felica_system_code_list"
#define FELICA_IC_CODE_LIST_JSON "felica/felica_ic_code_list"
#define FELICA_CONTAINER_ISSUE_LIST_JSON "felica/felica_container_issue_information_list"
#define FELICA_REQUEST_SERVICE_DISCOVERY_BATCH_SIZE 32U
#define FELICA_MAX_NODE_NUMBER 0x03FFU
@@ -457,6 +459,8 @@ static json_t *felica_system_list = NULL;
static bool felica_system_list_loaded = false;
static json_t *felica_ic_code_list = NULL;
static bool felica_ic_code_list_loaded = false;
static json_t *felica_container_issue_list = NULL;
static bool felica_container_issue_list_loaded = false;
static void set_last_known_card(felica_card_select_t card) {
last_known_card = card;
@@ -771,6 +775,100 @@ static json_t *felica_get_ic_code_list(void) {
return felica_ic_code_list;
}
static json_t *felica_get_container_issue_list(void) {
if (felica_container_issue_list_loaded) {
return felica_container_issue_list;
}
felica_container_issue_list_loaded = true;
char *path = NULL;
if (searchFile(&path, RESOURCES_SUBDIR, FELICA_CONTAINER_ISSUE_LIST_JSON, ".json", true) != PM3_SUCCESS) {
return NULL;
}
json_error_t error;
json_t *root = json_load_file(path, 0, &error);
if (root == NULL) {
PrintAndLogEx(WARNING, "Failed to parse `%s` line %d: %s", path, error.line, error.text);
free(path);
return NULL;
}
if (json_is_array(root) == false) {
PrintAndLogEx(WARNING, "Invalid `%s` format, expected array root", path);
json_decref(root);
free(path);
return NULL;
}
felica_container_issue_list = root;
free(path);
return felica_container_issue_list;
}
static bool felica_container_issue_matches(const felica_get_container_issue_info_response_t *response,
const json_t *entry) {
if (response == NULL || json_is_object(entry) == false) {
return false;
}
const char *format_hex = felica_get_json_string(entry, "format_version_carrier_info");
const char *model = felica_get_json_string(entry, "mobile_phone_model_info");
if (format_hex == NULL || model == NULL ||
strlen(format_hex) != (sizeof(response->format_version_carrier_information) * 2U)) {
return false;
}
uint8_t expected_format[sizeof(response->format_version_carrier_information)] = {0};
size_t expected_format_len = 0;
if (hexstr_to_byte_array(format_hex, expected_format, &expected_format_len) == false ||
expected_format_len != sizeof(expected_format) ||
memcmp(response->format_version_carrier_information, expected_format, sizeof(expected_format)) != 0) {
return false;
}
const size_t model_len = strlen(model);
if (model_len == 0 || model_len > sizeof(response->mobile_phone_model_information) ||
memcmp(response->mobile_phone_model_information, model, model_len) != 0) {
return false;
}
for (size_t i = model_len; i < sizeof(response->mobile_phone_model_information); i++) {
if (response->mobile_phone_model_information[i] != 0x00) {
return false;
}
}
return true;
}
static void felica_print_container_issue_annotations(const felica_get_container_issue_info_response_t *response) {
json_t *list = felica_get_container_issue_list();
if (response == NULL || list == NULL) {
return;
}
size_t index = 0;
json_t *entry = NULL;
json_array_foreach(list, index, entry) {
if (felica_container_issue_matches(response, entry) == false) {
continue;
}
const char *name = felica_get_json_string(entry, "name");
if (name == NULL) {
continue;
}
const char *carrier = felica_get_json_string(entry, "carrier");
PrintAndLogEx(INFO, " Device Model.......... " _GREEN_("%s"), name);
if (carrier) {
PrintAndLogEx(INFO, " Carrier............... " _GREEN_("%s"), carrier);
}
}
}
static const json_t *felica_find_ic_annotation(uint8_t rom_type, uint8_t ic_type) {
json_t *ic_code_list = felica_get_ic_code_list();
if (ic_code_list == NULL) {
@@ -2397,6 +2495,18 @@ static int info_felica(bool verbose) {
PrintAndLogEx(INFO, " Code......... " _GREEN_("%s"), sprint_hex_inrow(card.code, sizeof(card.code)));
}
PrintAndLogEx(INFO, " NFCID2....... " _GREEN_("%s"), sprint_hex_inrow(card.uid, sizeof(card.uid)));
const int16_t days_since_2000 = (int16_t)(((uint16_t)card.IDm[5] << 8) | card.IDm[4]);
const time_t felica_epoch = (time_t)946684800; // 2000-01-01 UTC
const time_t now = time(NULL);
if (days_since_2000 >= 0 && now != (time_t)-1 && now >= felica_epoch &&
(time_t)days_since_2000 <= (now - felica_epoch) / 86400) {
const time_t manufacturing_time = felica_epoch + ((time_t)days_since_2000 * 86400);
const struct tm *date = gmtime(&manufacturing_time);
char date_string[11] = {0};
if (date != NULL && strftime(date_string, sizeof(date_string), "%Y-%m-%d", date) != 0) {
PrintAndLogEx(INFO, " Mfg date..... " _GREEN_("%s"), date_string);
}
}
PrintAndLogEx(INFO, "PMM............ " _YELLOW_("%s"), sprint_hex_inrow(card.PMm, sizeof(card.PMm)));
PrintAndLogEx(INFO, " IC code...... " _GREEN_("%s") " ( %s )",
sprint_hex_inrow(card.iccode, sizeof(card.iccode)),
@@ -2469,18 +2579,19 @@ static int info_felica(bool verbose) {
sizeof(container_issue_info_response.mobile_phone_model_information),
model_ascii,
sizeof(model_ascii)
);
);
PrintAndLogEx(INFO, "Container issue info:");
PrintAndLogEx(INFO, " Format/Carrier... " _YELLOW_("%s"),
PrintAndLogEx(INFO, " Format/Carrier info... " _YELLOW_("%s"),
sprint_hex_inrow(container_issue_info_response.format_version_carrier_information,
sizeof(container_issue_info_response.format_version_carrier_information)));
if (model_is_ascii) {
PrintAndLogEx(INFO, " Model............ " _GREEN_("%s") " (ASCII)", model_ascii);
PrintAndLogEx(INFO, " Model info............ " _GREEN_("%s") " (ASCII)", model_ascii);
} else {
PrintAndLogEx(INFO, " Model............ " _YELLOW_("%s") " (HEX)",
PrintAndLogEx(INFO, " Model info............ " _YELLOW_("%s") " (HEX)",
sprint_hex_inrow(container_issue_info_response.mobile_phone_model_information,
sizeof(container_issue_info_response.mobile_phone_model_information)));
}
felica_print_container_issue_annotations(&container_issue_info_response);
}
const uint16_t container_properties[] = {0x0000, 0x0001};
+24 -4
View File
@@ -1937,8 +1937,12 @@ static int CmdHFiClassELoad(const char *Cmd) {
}
if (verbose) {
print_picopass_header((picopass_hdr_t *) dump);
print_picopass_info((picopass_hdr_t *) dump);
if (bytes_read < sizeof(picopass_hdr_t)) {
PrintAndLogEx(FAILED, "Error, dump file is too small to be a valid iCLASS dump - bytes: %zu, expected at least: %zu", bytes_read, sizeof(picopass_hdr_t));
} else {
print_picopass_header((picopass_hdr_t *) dump);
print_picopass_info((picopass_hdr_t *) dump);
}
}
PrintAndLogEx(NORMAL, "");
@@ -2059,8 +2063,12 @@ static int CmdHFiClassEView(const char *Cmd) {
}
if (verbose) {
print_picopass_header((picopass_hdr_t *) dump);
print_picopass_info((picopass_hdr_t *) dump);
if (bytes < sizeof(picopass_hdr_t)) {
PrintAndLogEx(FAILED, "Error, only %u bytes downloaded, too small to be a valid iCLASS dump - expected at least: %zu", bytes, sizeof(picopass_hdr_t));
} else {
print_picopass_header((picopass_hdr_t *) dump);
print_picopass_info((picopass_hdr_t *) dump);
}
}
PrintAndLogEx(NORMAL, "");
@@ -2326,6 +2334,18 @@ static int CmdHFiClassDecrypt(const char *Cmd) {
return res;
}
// Below (and in iclass_decode_credentials()) we unconditionally probe
// fixed offsets up through block 9 -- app_issuer_area (block 5), the
// aa1_encryption flag and block 7 in iclass_decode_credentials(), and
// the block 9 PACS/PIN check here -- regardless of what applimit or
// decryptedlen/8 say the "real" block count is. All 10 blocks (0-9)
// must actually be present in the loaded file before any of that runs.
if (decryptedlen < 10 * PICOPASS_BLOCK_SIZE) {
PrintAndLogEx(FAILED, "Error, dump file is too small - bytes: %zu, expected at least: %d", decryptedlen, 10 * PICOPASS_BLOCK_SIZE);
free(decrypted);
return PM3_EFILE;
}
have_file = true;
}
+21 -3
View File
@@ -639,7 +639,7 @@ static int CmdDesGetSessionParameters(CLIParserContext *ctx, DesfireContext_t *d
return PM3_SUCCESS;
}
static int DesfirePCRun(DesfireContext_t *dctx, const uint8_t proximity_key[MFDES_PC_KEY_LEN], uint8_t rounds, bool verbose) {
static int DesfirePCRun(DesfireContext_t *dctx, const uint8_t proximity_key[MFDES_PC_KEY_LEN], uint8_t rounds, bool activate_field, bool verbose) {
if (dctx == NULL || proximity_key == NULL || rounds < 1 || rounds > MFDES_PC_MAX_ROUNDS) {
return PM3_EINVARG;
}
@@ -655,7 +655,7 @@ static int DesfirePCRun(DesfireContext_t *dctx, const uint8_t proximity_key[MFDE
uint8_t prepare_resp[APDU_RES_LEN] = {0};
size_t prepare_resp_len = 0;
uint8_t respcode = 0xFF;
res = DesfireExchangeEx(true, dctx, MFDES_PREPARE_PC, NULL, 0, &respcode, prepare_resp, &prepare_resp_len, true, 0);
res = DesfireExchangeEx(activate_field, dctx, MFDES_PREPARE_PC, NULL, 0, &respcode, prepare_resp, &prepare_resp_len, true, 0);
if (res != PM3_SUCCESS) {
uint16_t sw = status(respcode);
PrintAndLogEx(ERR, "Prepare proximity check command failed. Result: %d %s", res, DesfireGetErrorString(res, &sw));
@@ -4591,6 +4591,7 @@ static int CmdHF14ADesPC(const char *Cmd) {
"This command uses plain communication with a dedicated 16-byte AES proximity key.",
"hf mfdes pc --key 00000000000000000000000000000000\n"
"hf mfdes pc --key 00112233445566778899aabbccddeeff --rounds 4\n"
"hf mfdes pc --aid 123456 --key 00112233445566778899aabbccddeeff\n"
"hf mfdes pc --key 00112233445566778899aabbccddeeff -c native -a");
void *argtable[] = {
@@ -4600,6 +4601,7 @@ static int CmdHF14ADesPC(const char *Cmd) {
arg_str1("k", "key", "<hex>", "Key (AES-128, exactly 16 bytes)"),
arg_int0("r", "rounds", "<dec>", "Number of rounds (1..8), default 8"),
arg_str0("c", "ccset", "<native|niso>", "Communication command set (default from `hf mfdes default`)"),
arg_str0(NULL, "aid", "<hex>", "Application ID (3 hex bytes, big endian)"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, true);
@@ -4635,6 +4637,13 @@ static int CmdHF14ADesPC(const char *Cmd) {
return PM3_EINVARG;
}
uint32_t appid = 0;
bool appid_present = false;
if (CLIGetUint32Hex(ctx, 6, 0, &appid, &appid_present, 3, "AID must have 3 bytes length")) {
CLIParserFree(ctx);
return PM3_EINVARG;
}
SetAPDULogging(APDULogging);
CLIParserFree(ctx);
@@ -4643,7 +4652,16 @@ static int CmdHF14ADesPC(const char *Cmd) {
DesfireSetCommMode(&dctx, DCMPlain);
DesfireSetSecureChannel(&dctx, DACNone);
int res = DesfirePCRun(&dctx, proximity_key, rounds, verbose);
int res = PM3_SUCCESS;
if (appid_present) {
res = DesfireSelectAndAuthenticateAppW(&dctx, DACNone, ISW6bAID, appid, true, verbose);
if (res != PM3_SUCCESS) {
DropField();
return res;
}
}
res = DesfirePCRun(&dctx, proximity_key, rounds, !appid_present, verbose);
DropField();
return res;
}
+21 -1
View File
@@ -783,6 +783,7 @@ Here is how the IC can be configured:
* MF-8 (RU)
* MF-3 (RU) - not susceptible to "field reset bug", a way to detect [OTP](#fuid) chips.
* MF-3.2 (RU) - static nonce `01200145`, potentially fixed chip which can bypass Iron Logic's filters.
* M+ (CopyKEY) - chips presented as a universal alternative to more advanced Chinese ICs; only real difference is presence of rewritable sectors 16+17 and block 255 (readable after any auth)
`
### Identify
@@ -2233,7 +2234,26 @@ hf 14a raw -akb 7 40; hf 14a raw -k 43; hf 14a raw -ck A2F2000000BD; hf 14a raw
^[Top](#top)
No implemented commands at time of writing
Two helper scripts drive USCUID-UL cards:
* `script run hf_mfu_uscuid` (Python) - read/parse config, change emulated type, set UID, set signature, and raw backdoor read/write.
* `script run hf_mf_uscuid_prog` (Lua) - equivalent functionality. See `-h` for its options.
Backdoor operations (set UID, set signature, raw hidden-block read/write) require the gen1a backdoor to be enabled - a config block starting with `7AFF`, see the [USCUID-UL configuration guide](#uscuid-ul-configuration-guide) - and a magic wakeup to be selected: for the Python script, `--gen1a` (`40`/`43`) or `--gdm` (`20`/`23`).
Example - write the tag signature (32 bytes / 64 hexsymbols) using the `40:43` wakeup:
```
script run hf_mfu_uscuid -s <signature, 64 hexsymbols> --gen1a
```
Verify with `hf mfu info` and look for `Signature verification: successful`.
The same result can be achieved manually with raw commands (magic wakeup, then write the eight signature pages `F8`-`FF`):
```
hf 14a raw -akb 7 40; hf 14a raw -k 43; hf 14a raw -ck A2F8<4 bytes>; ...; hf 14a raw -c A2FF<4 bytes>
```
### libnfc commands