From 00aabd7443ca2fbe17a71d8dddde5583d9d692e5 Mon Sep 17 00:00:00 2001 From: iceman1001 Date: Wed, 16 Sep 2026 12:40:47 +0200 Subject: [PATCH] hf mfu chk: one dictionary check that detects UL-C or UL-AES hf mfu cchk and hf mfu aeschk were the same command twice. Ninety lines of dictionary loading and chunked device calls were duplicated, and the only real difference was which key slot they targeted: aeschk took --idx, cchk hardcoded the Ultralight C slot. Both are replaced by hf mfu chk, which reads the tag type off the card with GetHF14AMfU_Type() and picks the slot itself. Ultralight AES still honours --idx, 0 DataProtKey, 1 UIDRetrKey, 2 OriginalityKey. Ultralight C holds a single key, so --idx is rejected there rather than silently ignored. Any other tag is refused with a pointer to hf mfu info. Without -f the dictionary is mfulc_default_keys.dic for both tag types. A segment check keeps needing an explicit -f, because the segment dictionaries hold four byte keys and the default one does not. aeschk used to advertise mfulaes_default_keys.dic in its help, which has never been shipped. Collapsing the two bodies also fixes --retries. firstChunk and lastChunk were declared outside the retry loop and never reset, so only the first pass was correct. From the second pass on, every chunk went out with firstchunk clear and lastchunk set, which on the device side means no select and a field teardown after each chunk, against a field the previous pass had already dropped. They are now scoped to the pass. client/pyscripts/mfulaes_mask_recovery.py called aeschk and now calls chk. doc/commands.md and doc/commands.json are regenerated. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 1 + client/pyscripts/mfulaes_mask_recovery.py | 2 +- client/src/cmdhfmfu.c | 309 +++++++++------------- doc/commands.json | 38 +-- doc/commands.md | 3 +- 5 files changed, 132 insertions(+), 221 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb276a0b..8df0c2863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ 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] +- Changed `hf mfu cchk` and `hf mfu aeschk` into one `hf mfu chk` which detects UL-C vs UL-AES (@iceman1001) - Added `hf 14b view` - MyKey / COGES keys on SRIX4K now decode (@iceman1001) - Added `hf 14b view --selftest` - runs the MyKey parser self tests (@iceman1001) - Removed `hf 14b valid` - the SRIX4K scrambler stub it wrapped is finished and lives in the MyKey parser (@iceman1001) diff --git a/client/pyscripts/mfulaes_mask_recovery.py b/client/pyscripts/mfulaes_mask_recovery.py index 363771b6b..0bb4811c4 100755 --- a/client/pyscripts/mfulaes_mask_recovery.py +++ b/client/pyscripts/mfulaes_mask_recovery.py @@ -80,7 +80,7 @@ def bruteforce_key(p, key_segment, idx, segment, retries=5, bitflips=2): sys.stdout.flush() key = construct_key(key_segment, segment) console_debug(p, - f'hf mfu aeschk -i {idx} ' + f'hf mfu chk -i {idx} ' f'-f mfulaes_segment_hw{bitflips}.dic ' f'--segment {segment} ' f'--key {key} ' diff --git a/client/src/cmdhfmfu.c b/client/src/cmdhfmfu.c index 33ad59c99..9debb66c1 100644 --- a/client/src/cmdhfmfu.c +++ b/client/src/cmdhfmfu.c @@ -67,6 +67,7 @@ #define MIFAREU3P_KEY_SIZE 16 #define MIFAREULC_KEY_INDEX 3 +#define MFU_DEFAULT_KEY_DIC "mfulc_default_keys.dic" // The Capability Container sits in block 3, the NDEF data area starts at block 4 // and READ takes a one byte block number, so block 255 is the last one reachable. @@ -4721,57 +4722,12 @@ static int CmdHF14AMfUCAuth(const char *Cmd) { return isok; } -static int CmdHF14AMfUCAuthChk(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hf mfu cchk", - "It checks MIFARE Ultralight C tags keys against a dictionary file with keys\n", - "hf mfu cchk -f mfulc_default_keys.dic"); - - void *argtable[] = { - arg_param_begin, - arg_str0("f", "file", "", "filename of dictionary"), - arg_int0("s", "segment", "<0..3>", "Segment index (full key if not specified)"), - arg_int0("r", "retries", "<0..255>", "Number of retries (def: 0)"), - arg_str0("k", "key", "", "Starting key, 16 hex bytes (def: zero key), for segment check"), - arg_lit0("x", "xor", "XOR starting key with segment candidates (def: override)"), - arg_lit0("n", "nocheck", "Skip checking tag answer correctness"), - arg_lit0("0", "read0", "Use fast READ0 (skip anticol)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - int fnlen = 0; - char filename[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); - int segment = arg_get_int_def(ctx, 2, -1); // -1 means full key - int retries = arg_get_int_def(ctx, 3, 0); - int ref_keylen = 0; - uint8_t ref_key[16] = {0}; - CLIGetHexWithReturn(ctx, 4, ref_key, &ref_keylen); - bool xor_ref_key = arg_get_lit(ctx, 5); - bool check_answer = !arg_get_lit(ctx, 6); - bool use_fastread0 = arg_get_lit(ctx, 7); - CLIParserFree(ctx); - - if (fnlen == 0) { - PrintAndLogEx(ERR, "No dictionary file specified"); - return PM3_EFILE; - } - if (segment < -1 || segment > 3) { - PrintAndLogEx(ERR, "Invalid segment (must be 0..3)"); - return PM3_EINVARG; - } - if (retries < 0 || retries > 255) { - PrintAndLogEx(ERR, "Invalid retries (must be 0..255)"); - return PM3_EINVARG; - } - if (ref_keylen && ref_keylen != MIFAREU3P_KEY_SIZE) { - PrintAndLogEx(WARNING, "Key must be %i hex bytes. Got %d", MIFAREU3P_KEY_SIZE, ref_keylen); - return PM3_EINVARG; - } - if (ref_keylen == 0) { - ref_keylen = MIFAREU3P_KEY_SIZE; - } +// Runs one dictionary against one key slot. The device can only hold a few keys +// per frame, so the dictionary is fed to it in chunks: the first chunk selects the +// card, the last one drops the field. +static int mfu_auth_chk(uint8_t key_index, const char *filename, int fnlen, int segment, + int retries, uint8_t *ref_key, bool xor_ref_key, bool check_answer, + bool use_fastread0) { uint8_t *keyBlock = NULL; uint32_t keycnt = 0; @@ -4792,9 +4748,6 @@ static int CmdHF14AMfUCAuthChk(const char *Cmd) { max_chunk = MIFAREU3P_CHKKEY_MAX_KEYS; } uint32_t chunksize = (keycnt > max_chunk) ? max_chunk : keycnt; - bool firstChunk = true, lastChunk = false; - - int i = 0; // time uint32_t auths = 0; @@ -4802,7 +4755,12 @@ static int CmdHF14AMfUCAuthChk(const char *Cmd) { // main keychunk loop for (int r = 0; r < retries + 1; r++) { - for (i = 0; i < keycnt; i += chunksize) { + + // every pass needs its own select and its own teardown + bool firstChunk = true, lastChunk = false; + + for (uint32_t i = 0; i < keycnt; i += chunksize) { + if (kbd_enter_pressed()) { clearCommandBuffer(); SendCommandNG(CMD_BREAK_LOOP, NULL, 0); @@ -4812,22 +4770,22 @@ static int CmdHF14AMfUCAuthChk(const char *Cmd) { goto out; } - uint32_t nkeys = ((keycnt - i) > chunksize) ? chunksize : keycnt - i; + uint32_t nkeys = ((keycnt - i) > chunksize) ? chunksize : keycnt - i; // last chunk? if (nkeys == keycnt - i) { lastChunk = true; } - int res = mfu_3pass_check_keys(MIFAREULC_KEY_INDEX, firstChunk, lastChunk, nkeys, segment, ref_key, xor_ref_key, keyBlock + (i * keysize), false, true, &auths, &ms, check_answer, use_fastread0); - if (firstChunk) - firstChunk = false; + + int res = mfu_3pass_check_keys(key_index, firstChunk, lastChunk, nkeys, segment, ref_key, xor_ref_key, keyBlock + (i * keysize), false, true, &auths, &ms, check_answer, use_fastread0); + firstChunk = false; // all keys, aborted if (res == PM3_SUCCESS || res == 2) { PrintAndLogEx(NORMAL, ""); goto out; } - PrintAndLogEx(INPLACE, "Testing %5i/%5i ( " _YELLOW_("%02.1f %%") " )", i, keycnt, (float)i * 100 / keycnt); + PrintAndLogEx(INPLACE, "Testing %5u/%5u ( " _YELLOW_("%02.1f %%") " )", i, keycnt, (float)i * 100 / keycnt); } // end chunks of keys } PrintAndLogEx(NORMAL, ""); @@ -4840,6 +4798,102 @@ out: PrintAndLogEx(NORMAL, ""); return PM3_SUCCESS; } + +static int CmdHF14AMfUAuthChk(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "hf mfu chk", + "Checks MIFARE Ultralight C / Ultralight AES tag keys against a dictionary file.\n" + "The tag type is read off the card and picks the key slot to attack.\n" + " UL-C ..... single 3DES key, --idx does not apply\n" + " UL-AES ... key index 0 DataProtKey (default)\n" + " key index 1 UIDRetrKey\n" + " key index 2 OriginalityKey\n" + "Without -f, " _YELLOW_(MFU_DEFAULT_KEY_DIC) " is used for both tag types.\n" + "A segment check (-s) holds 4 byte keys, so it needs its own dictionary via -f.", + "hf mfu chk\n" + "hf mfu chk -f mfulc_default_keys.dic\n" + "hf mfu chk -f mfulaes_hw1.dic --idx 1\n" + "hf mfu chk -f mfulc_segment_hw1.dic -s 0"); + + void *argtable[] = { + arg_param_begin, + arg_str0("f", "file", "", "filename of dictionary"), + arg_int0("i", "idx", "<0..2>", "Key index, Ultralight AES only (def: 0)"), + arg_int0("s", "segment", "<0..3>", "Segment index (full key if not specified)"), + arg_int0("r", "retries", "<0..255>", "Number of retries (def: 0)"), + arg_str0("k", "key", "", "Starting key, 16 hex bytes (def: zero key), for segment check"), + arg_lit0("x", "xor", "XOR starting key with segment candidates (def: override)"), + arg_lit0("n", "nocheck", "Skip checking tag answer correctness"), + arg_lit0("0", "read0", "Use fast READ0 (skip anticol)"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + int fnlen = 0; + char filename[FILE_PATH_SIZE] = {0}; + CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); + int key_idx = arg_get_int_def(ctx, 2, -1); // -1 means not given + int segment = arg_get_int_def(ctx, 3, -1); // -1 means full key + int retries = arg_get_int_def(ctx, 4, 0); + int ref_keylen = 0; + uint8_t ref_key[16] = {0}; + CLIGetHexWithReturn(ctx, 5, ref_key, &ref_keylen); + bool xor_ref_key = arg_get_lit(ctx, 6); + bool check_answer = !arg_get_lit(ctx, 7); + bool use_fastread0 = arg_get_lit(ctx, 8); + CLIParserFree(ctx); + + if (key_idx < -1 || key_idx > 2) { + PrintAndLogEx(ERR, "Invalid key index (must be 0..2)"); + return PM3_EINVARG; + } + if (segment < -1 || segment > 3) { + PrintAndLogEx(ERR, "Invalid segment (must be 0..3)"); + return PM3_EINVARG; + } + if (retries < 0 || retries > 255) { + PrintAndLogEx(ERR, "Invalid retries (must be 0..255)"); + return PM3_EINVARG; + } + if (ref_keylen && ref_keylen != MIFAREU3P_KEY_SIZE) { + PrintAndLogEx(WARNING, "Key must be %i hex bytes. Got %d", MIFAREU3P_KEY_SIZE, ref_keylen); + return PM3_EINVARG; + } + + if (fnlen == 0) { + if (segment != -1) { + PrintAndLogEx(ERR, "A segment check needs a segment dictionary, pass one with " _YELLOW_("-f")); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("mfulc_segment_hw1.dic") "` or `" _YELLOW_("mfulaes_segment_hw1.dic") "`"); + return PM3_EFILE; + } + fnlen = snprintf(filename, sizeof(filename), MFU_DEFAULT_KEY_DIC); + PrintAndLogEx(INFO, "No dictionary given, using " _YELLOW_("%s"), filename); + } + + uint64_t tagtype = GetHF14AMfU_Type(); + if (tagtype == MFU_TT_UL_ERROR) { + PrintAndLogEx(WARNING, "No Ultralight tag found"); + return PM3_ESOFT; + } + ul_print_type(tagtype, 0); + + uint8_t key_index; + if ((tagtype & MFU_TT_UL_AES) == MFU_TT_UL_AES) { + key_index = (key_idx == -1) ? 0 : (uint8_t)key_idx; + } else if ((tagtype & MFU_TT_UL_C) == MFU_TT_UL_C) { + if (key_idx != -1) { + PrintAndLogEx(ERR, "Ultralight C holds a single key, " _YELLOW_("--idx") " is for Ultralight AES"); + return PM3_EINVARG; + } + key_index = MIFAREULC_KEY_INDEX; + } else { + PrintAndLogEx(ERR, "Tag is neither Ultralight C nor Ultralight AES"); + PrintAndLogEx(HINT, "Hint: Try `" _YELLOW_("hf mfu info") "` to see what this tag is"); + return PM3_EINVARG; + } + + return mfu_auth_chk(key_index, filename, fnlen, segment, retries, ref_key, xor_ref_key, check_answer, use_fastread0); +} //------------------------------------------------------------------------------- // Ultralight AES Methods //------------------------------------------------------------------------------- @@ -4931,134 +4985,6 @@ static int CmdHF14AMfUAESAuth(const char *Cmd) { -static int CmdHF14AMfUAESAuthChk(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hf mfu aeschk", - "It checks MIFARE Ultralight AES tags keys against a dictionary file with keys\n" - " Key index 0... DataProtKey (default)\n" - " Key index 1... UIDRetrKey\n" - " Key index 2... OriginalityKey\n", - "hf mfu aeschk -f mfulaes_default_keys.dic"); - - void *argtable[] = { - arg_param_begin, - arg_str0("f", "file", "", "filename of dictionary"), - arg_int0("i", "idx", "<0..2>", "Key index (def: 0)"), - arg_int0("s", "segment", "<0..3>", "Segment index (full key if not specified)"), - arg_int0("r", "retries", "<0..255>", "Number of retries (def: 0)"), - arg_str0("k", "key", "", "Starting key, 16 hex bytes (def: zero key), for segment check"), - arg_lit0("x", "xor", "XOR starting key with segment candidates (def: override)"), - arg_lit0("n", "nocheck", "Skip checking tag answer correctness"), - arg_lit0("0", "read0", "Use fast READ0 (skip anticol)"), - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - int fnlen = 0; - char filename[FILE_PATH_SIZE] = {0}; - CLIParamStrToBuf(arg_get_str(ctx, 1), (uint8_t *)filename, FILE_PATH_SIZE, &fnlen); - int key_index = arg_get_int_def(ctx, 2, 0); - int segment = arg_get_int_def(ctx, 3, -1); // -1 means full key - int retries = arg_get_int_def(ctx, 4, 0); - int ref_keylen = 0; - uint8_t ref_key[16] = {0}; - CLIGetHexWithReturn(ctx, 5, ref_key, &ref_keylen); - bool xor_ref_key = arg_get_lit(ctx, 6); - bool check_answer = !arg_get_lit(ctx, 7); - bool use_fastread0 = arg_get_lit(ctx, 8); - CLIParserFree(ctx); - - if (fnlen == 0) { - PrintAndLogEx(ERR, "No dictionary file specified"); - return PM3_EFILE; - } - if (key_index < 0 || key_index > 2) { - PrintAndLogEx(ERR, "Invalid key index (must be 0..2)"); - return PM3_EINVARG; - } - if (segment < -1 || segment > 3) { - PrintAndLogEx(ERR, "Invalid segment (must be 0..3)"); - return PM3_EINVARG; - } - if (retries < 0 || retries > 255) { - PrintAndLogEx(ERR, "Invalid retries (must be 0..255)"); - return PM3_EINVARG; - } - if (ref_keylen && ref_keylen != MIFAREU3P_KEY_SIZE) { - PrintAndLogEx(WARNING, "Key must be %i hex bytes. Got %d", MIFAREU3P_KEY_SIZE, ref_keylen); - return PM3_EINVARG; - } - if (ref_keylen == 0) { - ref_keylen = MIFAREU3P_KEY_SIZE; - } - - uint8_t *keyBlock = NULL; - uint32_t keycnt = 0; - int keysize = segment != -1 ? MIFAREU3P_KEY_SIZE / 4 : MIFAREU3P_KEY_SIZE; - int ret = mfu_3pass_load_keys(&keyBlock, &keycnt, filename, fnlen, keysize); - if (ret != PM3_SUCCESS) { - return ret; - } - if (keycnt == 0) { - PrintAndLogEx(ERR, "Dictionary contains no keys"); - free(keyBlock); - return PM3_ESOFT; - } - - // cap by what fits in one frame, then by what the nkeys field can announce - uint32_t max_chunk = (g_conn.max_cmd_data_size - MIFAREU3P_CHKKEY_HEADER) / keysize; - if (max_chunk > MIFAREU3P_CHKKEY_MAX_KEYS) { - max_chunk = MIFAREU3P_CHKKEY_MAX_KEYS; - } - uint32_t chunksize = (keycnt > max_chunk) ? max_chunk : keycnt; - bool firstChunk = true, lastChunk = false; - - int i = 0; - - uint32_t auths = 0; - uint32_t ms = 0; - - // main keychunk loop - for (int r = 0; r < retries + 1; r++) { - for (i = 0; i < keycnt; i += chunksize) { - if (kbd_enter_pressed()) { - clearCommandBuffer(); - SendCommandNG(CMD_BREAK_LOOP, NULL, 0); - SendCommandNG(CMD_FPGA_MAJOR_MODE_OFF, NULL, 0); // field is still ON if not on last chunk - PrintAndLogEx(NORMAL, ""); - PrintAndLogEx(WARNING, "\naborted via keyboard!"); - goto out; - } - - uint32_t nkeys = ((keycnt - i) > chunksize) ? chunksize : keycnt - i; - - // last chunk? - if (nkeys == keycnt - i) { - lastChunk = true; - } - - int res = mfu_3pass_check_keys(key_index, firstChunk, lastChunk, nkeys, segment, ref_key, xor_ref_key, keyBlock + (i * keysize), false, true, &auths, &ms, check_answer, use_fastread0); - if (firstChunk) - firstChunk = false; - - // all keys, aborted - if (res == PM3_SUCCESS || res == 2) { - PrintAndLogEx(NORMAL, ""); - goto out; - } - PrintAndLogEx(INPLACE, "Testing %5i/%5i ( " _YELLOW_("%02.1f %%") " )", i, keycnt, (float)i * 100 / keycnt); - } // end chunks of keys - } - PrintAndLogEx(NORMAL, ""); -out: - PrintAndLogEx(INFO, "Time spent " _YELLOW_("%.1fs"), (float)(ms / 1000.0)); - PrintAndLogEx(INFO, "Authentication attempts: %u", auths); - PrintAndLogEx(INFO, "Speed: %.1f auths/s", (float)(auths * 1000.0 / ms)); - - free(keyBlock); - PrintAndLogEx(NORMAL, ""); - return PM3_SUCCESS; -} static int CmdHF14AMfUAESGetUID(const char *Cmd) { CLIParserContext *ctx; @@ -6929,7 +6855,9 @@ static int CmdHF14AMfuEv1CounterTearoff(const char *Cmd) { } } - } else if (fixed != -1) delay_bd = fixed; + } else if (fixed != -1) { + delay_bd = fixed; + } if (ul_select(&card) == false) { PrintAndLogEx(NORMAL, ""); @@ -9081,11 +9009,10 @@ static command_t CommandTable[] = { {"otptear", CmdHF14AMfuOtpTearoff, IfPm3Iso14443a, "Tear-off test on OTP bits"}, {"countertear", CmdHF14AMfuEv1CounterTearoff, IfPm3Iso14443a, "Tear-off test on Ev1/NTAG Counter bits"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("operations") " -----------------------"}, + {"chk", CmdHF14AMfUAuthChk, IfPm3Iso14443a, "Ultralight C/AES - Authentication dictionary check"}, {"cauth", CmdHF14AMfUCAuth, IfPm3Iso14443a, "Ultralight-C - Authentication"}, - {"cchk", CmdHF14AMfUCAuthChk, IfPm3Iso14443a, "Ultralight-C - Authentication dictionary check"}, {"desbrute", CmdHF14AMfUCDesBrute, AlwaysAvailable, "Ultralight-C - 3DES key segment brute force"}, {"aesauth", CmdHF14AMfUAESAuth, IfPm3Iso14443a, "Ultralight-AES - Authentication"}, - {"aeschk", CmdHF14AMfUAESAuthChk, IfPm3Iso14443a, "Ultralight-AES - Authentication dictionary check"}, {"aesgetuid", CmdHF14AMfUAESGetUID, IfPm3Iso14443a, "Ultralight-AES - Get UID when RID in use"}, {"setkey", CmdHF14AMfUSetKey, IfPm3Iso14443a, "Ultralight C/AES - Set 3DES/AES keys"}, {"dump", CmdHF14AMfUDump, IfPm3Iso14443a, "Dump MIFARE Ultralight family tag to binary file"}, diff --git a/doc/commands.json b/doc/commands.json index 04920c6a4..279d72959 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -9185,26 +9185,6 @@ ], "usage": "hf mfu aesauth [-hlkn0] [--key ] [-i <0..2>] [--schann] [-r ]" }, - "hf mfu aeschk": { - "command": "hf mfu aeschk", - "description": "It checks MIFARE Ultralight AES tags keys against a dictionary file with keys Key index 0... DataProtKey (default) Key index 1... UIDRetrKey Key index 2... OriginalityKey", - "notes": [ - "hf mfu aeschk -f mfulaes_default_keys.dic" - ], - "offline": false, - "options": [ - "-h, --help This help", - "-f, --file filename of dictionary", - "-i, --idx <0..2> Key index (def: 0)", - "-s, --segment <0..3> Segment index (full key if not specified)", - "-r, --retries <0..255> Number of retries (def: 0)", - "-k, --key Starting key, 16 hex bytes (def: zero key), for segment check", - "-x, --xor XOR starting key with segment candidates (def: override)", - "-n, --nocheck Skip checking tag answer correctness", - "-0, --read0 Use fast READ0 (skip anticol)" - ], - "usage": "hf mfu aeschk [-hxn0] [-f ] [-i <0..2>] [-s <0..3>] [-r <0..255>] [-k ]" - }, "hf mfu aesgetuid": { "command": "hf mfu aesgetuid", "description": "Retreives real UID on Mifare Ultralight AES tags when random ID is enabled. Uses key index 1 (UIDRetrKey). If no key is specified, null key will be tried.", @@ -9266,16 +9246,20 @@ ], "usage": "hf mfu cauth [-hlkn0] [--key ] [-r ] [--noauth] [--reset] [--collect] [--pair ]..." }, - "hf mfu cchk": { - "command": "hf mfu cchk", - "description": "It checks MIFARE Ultralight C tags keys against a dictionary file with keys", + "hf mfu chk": { + "command": "hf mfu chk", + "description": "Checks MIFARE Ultralight C / Ultralight AES tag keys against a dictionary file. The tag type is read off the card and picks the key slot to attack. UL-C ..... single 3DES key, --idx does not apply UL-AES ... key index 0 DataProtKey (default) key index 1 UIDRetrKey key index 2 OriginalityKey Without -f, mfulc_default_keys.dic is used for both tag types. A segment check (-s) holds 4 byte keys, so it needs its own dictionary via -f.", "notes": [ - "hf mfu cchk -f mfulc_default_keys.dic" + "hf mfu chk", + "hf mfu chk -f mfulc_default_keys.dic", + "hf mfu chk -f mfulaes_hw1.dic --idx 1", + "hf mfu chk -f mfulc_segment_hw1.dic -s 0" ], "offline": false, "options": [ "-h, --help This help", "-f, --file filename of dictionary", + "-i, --idx <0..2> Key index, Ultralight AES only (def: 0)", "-s, --segment <0..3> Segment index (full key if not specified)", "-r, --retries <0..255> Number of retries (def: 0)", "-k, --key Starting key, 16 hex bytes (def: zero key), for segment check", @@ -9283,7 +9267,7 @@ "-n, --nocheck Skip checking tag answer correctness", "-0, --read0 Use fast READ0 (skip anticol)" ], - "usage": "hf mfu cchk [-hxn0] [-f ] [-s <0..3>] [-r <0..255>] [-k ]" + "usage": "hf mfu chk [-hxn0] [-f ] [-i <0..2>] [-s <0..3>] [-r <0..255>] [-k ]" }, "hf mfu countertear": { "command": "hf mfu countertear", @@ -16606,8 +16590,8 @@ } }, "metadata": { - "commands_extracted": 924, + "commands_extracted": 923, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-16T09:42:55+00:00" + "extracted_on": "2026-09-16T10:14:24+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index c597f647b..f67dd5841 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -748,11 +748,10 @@ Check column "offline" for their availability. |`hf mfu pwdgen `|Y |`Generate pwd from known algos` |`hf mfu otptear `|N |`Tear-off test on OTP bits` |`hf mfu countertear `|N |`Tear-off test on Ev1/NTAG Counter bits` +|`hf mfu chk `|N |`Ultralight C/AES - Authentication dictionary check` |`hf mfu cauth `|N |`Ultralight-C - Authentication` -|`hf mfu cchk `|N |`Ultralight-C - Authentication dictionary check` |`hf mfu desbrute `|Y |`Ultralight-C - 3DES key segment brute force` |`hf mfu aesauth `|N |`Ultralight-AES - Authentication` -|`hf mfu aeschk `|N |`Ultralight-AES - Authentication dictionary check` |`hf mfu aesgetuid `|N |`Ultralight-AES - Get UID when RID in use` |`hf mfu setkey `|N |`Ultralight C/AES - Set 3DES/AES keys` |`hf mfu dump `|N |`Dump MIFARE Ultralight family tag to binary file`