From e470958373d97900a6c5b31b55079b0276800cf7 Mon Sep 17 00:00:00 2001 From: taichunmin Date: Wed, 17 Jan 2024 23:06:26 +0800 Subject: [PATCH 1/8] Added command to check keys of multiple sectors at once --- CHANGELOG.md | 1 + docs/protocol.md | 7 ++ firmware/application/src/app_cmd.c | 19 ++- firmware/application/src/data_cmd.h | 1 + .../src/rfid/reader/hf/mf1_toolbox.c | 85 ++++++++++++- .../src/rfid/reader/hf/mf1_toolbox.h | 25 +++- .../application/src/rfid/reader/hf/rc522.c | 10 +- .../application/src/rfid/reader/hf/rc522.h | 6 +- software/script/chameleon_cli_unit.py | 114 ++++++++++++++++++ software/script/chameleon_cmd.py | 39 ++++++ software/script/chameleon_enum.py | 1 + 11 files changed, 297 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6781907..6f8b388 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] + - Added command to check keys of multiple sectors at once (@taichunmin) - Skip already used items `hf mf elog --decrypt` (@p-l-) - Parallelize mfkey32v2 processes called from CLI (@p-l-) - Added support for mifare classic value block operations (@taichunmin) diff --git a/docs/protocol.md b/docs/protocol.md index 8905e58..2eef2a6 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -294,6 +294,13 @@ Notes: * Command: 21 bytes: `src_type|src_block|src_key[6]|operator|operand[4]|dst_type|dst_block|dst_key[6]`. Key as 6 bytes. Type=`0x60` for key A, `0x61` for key B. Operator=`0xC0` for decrement, `0xC1` for increment, `0xC2` for restore. Operand as I32 in Network byte order. * Response: no data * CLI: cf `hf mf value` +### 2012: MF1_CHECK_KEYS_OF_SECTORS +* Command: 10+N*6 bytes: `mask[10]|keys[N][6]` (1<=N<=83) + * `mask`: 40 sectors, 2 bits/sector, MSB: `0A|0B|1A|1B|...|39A|39B`. `0b1` represent to skip checking the key. +* Response: 490 bytes: `found[10]|sectorKey[40][2][6]`. + * `found`: 40 sectors, 2 bits/sector, MSB: `0A|0B|1A|1B|...|39A|39B`. `0b1` represent the key is found. + * `sectorKey`: 40 sectors, 2 keys/sector, 6 bytes/key: `key0A[6]|key0B[6]|key1A[6]|key1B[6]|...|key39A[6]|key39B[6]` +* CLI: cf `hf mf fchk` ### 3000: EM410X_SCAN * Command: no data * Response: 5 bytes. `id[5]`. ID as 5 bytes. diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index 45b833a..8a7e9ab 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -31,7 +31,6 @@ static void change_slot_auto(uint8_t slot) { set_slot_light_color(RGB_RED); } - static data_frame_tx_t *cmd_processor_get_app_version(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { struct { uint8_t version_major; @@ -347,6 +346,23 @@ static data_frame_tx_t *cmd_processor_mf1_auth_one_key_block(uint16_t cmd, uint1 return data_frame_make(cmd, status, 0, NULL); } +static data_frame_tx_t *cmd_processor_mf1_check_keys_of_sectors(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length < 16 || (length - 10) % 6 != 0) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + + // init + mf1_toolbox_check_keys_of_sectors_in_t in = { + .mask = *(mf1_toolbox_check_keys_of_sectors_mask_t *) &data[0], + .keys_len = (length - 10) / 6, + .keys = (mf1_key_t *) &data[10] + }; + mf1_toolbox_check_keys_of_sectors_out_t out; + status = mf1_toolbox_check_keys_of_sectors(&in, &out); + + return data_frame_make(cmd, status, sizeof(out), (uint8_t *)&out); +} + static data_frame_tx_t *cmd_processor_mf1_read_one_block(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { typedef struct { uint8_t type; @@ -1068,6 +1084,7 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_MF1_WRITE_ONE_BLOCK, before_hf_reader_run, cmd_processor_mf1_write_one_block, after_hf_reader_run }, { DATA_CMD_HF14A_RAW, before_reader_run, cmd_processor_hf14a_raw, NULL }, { DATA_CMD_MF1_MANIPULATE_VALUE_BLOCK, before_hf_reader_run, cmd_processor_mf1_manipulate_value_block, after_hf_reader_run }, + { DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS, before_hf_reader_run, cmd_processor_mf1_check_keys_of_sectors, after_hf_reader_run }, { DATA_CMD_EM410X_SCAN, before_reader_run, cmd_processor_em410x_scan, NULL }, { DATA_CMD_EM410X_WRITE_TO_T55XX, before_reader_run, cmd_processor_em410x_write_to_t55XX, NULL }, diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index e605ff9..10313b1 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -67,6 +67,7 @@ #define DATA_CMD_MF1_WRITE_ONE_BLOCK (2009) #define DATA_CMD_HF14A_RAW (2010) #define DATA_CMD_MF1_MANIPULATE_VALUE_BLOCK (2011) +#define DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS (2012) // // ****************************************************************** diff --git a/firmware/application/src/rfid/reader/hf/mf1_toolbox.c b/firmware/application/src/rfid/reader/hf/mf1_toolbox.c index f82b742..32e2bf4 100644 --- a/firmware/application/src/rfid/reader/hf/mf1_toolbox.c +++ b/firmware/application/src/rfid/reader/hf/mf1_toolbox.c @@ -1005,7 +1005,7 @@ uint8_t static_nested_recover_key(uint64_t keyKnown, uint8_t blkKnown, uint8_t t * @retval : validationResults * */ -uint8_t auth_key_use_522_hw(uint8_t block, uint8_t type, uint8_t *key) { +uint16_t auth_key_use_522_hw(uint8_t block, uint8_t type, uint8_t *key) { // Each verification of a block must re -find a card if (pcd_14a_reader_scan_auto(p_tag_info) != STATUS_HF_TAG_OK) { return STATUS_HF_TAG_NO; @@ -1013,3 +1013,86 @@ uint8_t auth_key_use_522_hw(uint8_t block, uint8_t type, uint8_t *key) { // After finding the card, we start to verify! return pcd_14a_reader_mf1_auth(p_tag_info, type, block, key); } + +inline void mf1_toolbox_antenna_restart () { + pcd_14a_reader_reset(); + pcd_14a_reader_antenna_on(); + bsp_delay_ms(8); +} + +inline void mf1_toolbox_report_healthy () { + bsp_wdt_feed(); + while (NRF_LOG_PROCESS()); +} + +uint16_t mf1_toolbox_check_keys_of_sectors ( + mf1_toolbox_check_keys_of_sectors_in_t *in, + mf1_toolbox_check_keys_of_sectors_out_t *out +) { + memset(out, 0, sizeof(mf1_toolbox_check_keys_of_sectors_out_t)); + uint8_t trailer[18] = {}; // trailer 16 bytes + padding 2 bytes + + // keys unique + uint8_t i, j, maskSector, maskShift, trailerNo; + for (i = 0; i < in->keys_len; i++) { + for (j = i + 1; j < in->keys_len; j++) { + if (memcmp(&in->keys[i], &in->keys[j], sizeof(mf1_key_t)) != 0) continue; + + // key duplicated, replace with last key + if (j != in->keys_len - 1) in->keys[j] = in->keys[in->keys_len - 1]; + in->keys_len--; + j--; + } + } + + uint16_t status = STATUS_HF_TAG_OK; + bool skipKeyB; + for (i = 0; i < 40; i++) { + maskShift = 6 - i % 4 * 2; + maskSector = (in->mask.b[i / 4] >> maskShift) & 0b11; + trailerNo = i < 32 ? i * 4 + 3 : i * 16 - 369; // trailerNo of sector + skipKeyB = (maskSector & 0b1) > 0; + if ((maskSector & 0b10) == 0) { + for (j = 0; j < in->keys_len; j++) { + mf1_toolbox_report_healthy(); + if (status != STATUS_HF_TAG_OK) mf1_toolbox_antenna_restart(); + + status = auth_key_use_522_hw(trailerNo, PICC_AUTHENT1A, in->keys[j].key); + if (status != STATUS_HF_TAG_OK) { // auth failed + if (status == STATUS_HF_TAG_NO) return STATUS_HF_TAG_NO; + continue; + } + // key A found + out->found.b[i / 4] |= 0b10 << maskShift; + out->keys[i][0] = in->keys[j]; + // try to read keyB from trailer of sector + status = pcd_14a_reader_mf1_read(trailerNo, trailer); + // key B not in trailer + if (status != STATUS_HF_TAG_OK || 0 == *(uint64_t*) &trailer[10]) break; + // key B found + skipKeyB = true; + out->found.b[i / 4] |= 0b1 << maskShift; + out->keys[i][1] = *(mf1_key_t*)&trailer[10]; + break; + } + } + if (skipKeyB) continue; + + for (j = 0; j < in->keys_len; j++) { + mf1_toolbox_report_healthy(); + if (status != STATUS_HF_TAG_OK) mf1_toolbox_antenna_restart(); + + status = auth_key_use_522_hw(trailerNo, PICC_AUTHENT1B, in->keys[j].key); + if (status != STATUS_HF_TAG_OK) { // auth failed + if (status == STATUS_HF_TAG_NO) return STATUS_HF_TAG_NO; + continue; + } + // key B found + out->found.b[i / 4] |= 0b1 << maskShift; + out->keys[i][1] = in->keys[j]; + break; + } + } + + return STATUS_HF_TAG_OK; +} \ No newline at end of file diff --git a/firmware/application/src/rfid/reader/hf/mf1_toolbox.h b/firmware/application/src/rfid/reader/hf/mf1_toolbox.h index 48dfb7c..c26fe47 100644 --- a/firmware/application/src/rfid/reader/hf/mf1_toolbox.h +++ b/firmware/application/src/rfid/reader/hf/mf1_toolbox.h @@ -57,6 +57,24 @@ typedef struct { uint8_t ar[4]; } PACKED DarksideCore_t; +typedef struct { + uint8_t key[6]; +} PACKED mf1_key_t; + +typedef struct { + uint8_t b[10]; // 80 bits: 40 sectors * 2 keys +} PACKED mf1_toolbox_check_keys_of_sectors_mask_t; + +typedef struct { + mf1_toolbox_check_keys_of_sectors_mask_t mask; + uint8_t keys_len; + mf1_key_t *keys; +} mf1_toolbox_check_keys_of_sectors_in_t; + +typedef struct { + mf1_toolbox_check_keys_of_sectors_mask_t found; + mf1_key_t keys[40][2]; // 6 bytes * 2 keys * 40 sectors = 480 bytes +} PACKED mf1_toolbox_check_keys_of_sectors_out_t; #ifdef __cplusplus extern "C" { @@ -93,7 +111,12 @@ uint8_t static_nested_recover_key(NESTED_CORE_PARAM_DEF, mf1_static_nested_core_ uint8_t check_prng_type(mf1_prng_type_t *type); uint8_t check_std_mifare_nt_support(); void antenna_switch_delay(uint32_t delay_ms); -uint8_t auth_key_use_522_hw(uint8_t block, uint8_t type, uint8_t *key); +uint16_t auth_key_use_522_hw(uint8_t block, uint8_t type, uint8_t *key); + +uint16_t mf1_toolbox_check_keys_of_sectors ( + mf1_toolbox_check_keys_of_sectors_in_t *in, + mf1_toolbox_check_keys_of_sectors_out_t *out +); #ifdef __cplusplus } diff --git a/firmware/application/src/rfid/reader/hf/rc522.c b/firmware/application/src/rfid/reader/hf/rc522.c index d08a01e..17f6dd8 100644 --- a/firmware/application/src/rfid/reader/hf/rc522.c +++ b/firmware/application/src/rfid/reader/hf/rc522.c @@ -958,7 +958,7 @@ uint8_t pcd_14a_reader_gen1a_uplock(void) { * PSNR: Card serial number, 4 bytes * @retval : The status value STATUS_HF_TAG_OK is successful, tag_errauth fails, and other returns indicate some abnormalities related to communication errors! */ -uint8_t pcd_14a_reader_mf1_auth(picc_14a_tag_t *tag, uint8_t type, uint8_t addr, uint8_t *pKey) { +uint16_t pcd_14a_reader_mf1_auth(picc_14a_tag_t *tag, uint8_t type, uint8_t addr, uint8_t *pKey) { uint8_t dat_buff[12] = { type, addr }; uint16_t data_len = 0; @@ -991,7 +991,7 @@ void pcd_14a_reader_mf1_unauth(void) { * p : Read data, 16 bytes * @retval : Status value hf_tag_ok, success */ -uint8_t pcd_14a_reader_mf1_read_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p) { +uint16_t pcd_14a_reader_mf1_read_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p) { uint8_t status; uint16_t len; uint8_t dat_buff[MAX_MIFARE_FRAME_SIZE] = { cmd, addr }; @@ -1028,7 +1028,7 @@ uint8_t pcd_14a_reader_mf1_read_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p) { * p : Read data, 16 bytes * @retval : Status value hf_tag_ok, success */ -uint8_t pcd_14a_reader_mf1_read(uint8_t addr, uint8_t *p) { +uint16_t pcd_14a_reader_mf1_read(uint8_t addr, uint8_t *p) { // Standard M1 Card Reading Card Reading return pcd_14a_reader_mf1_read_by_cmd(PICC_READ, addr, p); } @@ -1261,14 +1261,14 @@ inline void pcd_14a_reader_antenna_off(void) { } /** -* @brief : Qi Dian school inspection enabled +* @brief : Enable the parity bit check. */ inline void pcd_14a_reader_parity_on(void) { clear_register_mask(MfRxReg, 0x10); } /** -* @brief : Qi Tong school inspection position closed +* @brief : Disable the parity bit check. */ inline void pcd_14a_reader_parity_off(void) { set_register_mask(MfRxReg, 0x10); diff --git a/firmware/application/src/rfid/reader/hf/rc522.h b/firmware/application/src/rfid/reader/hf/rc522.h index 2c42d1b..98ec525 100644 --- a/firmware/application/src/rfid/reader/hf/rc522.h +++ b/firmware/application/src/rfid/reader/hf/rc522.h @@ -221,14 +221,14 @@ uint8_t pcd_14a_reader_ats_request(uint8_t *pAts, uint16_t *szAts, uint16_t szAt uint8_t pcd_14a_reader_atqa_request(uint8_t *resp, uint8_t *resp_par, uint16_t resp_max_bit); // M1 tag operation -uint8_t pcd_14a_reader_mf1_auth(picc_14a_tag_t *tag, uint8_t type, uint8_t addr, uint8_t *pKey); +uint16_t pcd_14a_reader_mf1_auth(picc_14a_tag_t *tag, uint8_t type, uint8_t addr, uint8_t *pKey); void pcd_14a_reader_mf1_unauth(void); // writeCardOperation uint8_t pcd_14a_reader_mf1_write_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p); uint8_t pcd_14a_reader_mf1_write(uint8_t addr, uint8_t *pData); // cardReadingOperation -uint8_t pcd_14a_reader_mf1_read_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p); -uint8_t pcd_14a_reader_mf1_read(uint8_t addr, uint8_t *pData); +uint16_t pcd_14a_reader_mf1_read_by_cmd(uint8_t cmd, uint8_t addr, uint8_t *p); +uint16_t pcd_14a_reader_mf1_read(uint8_t addr, uint8_t *pData); // value block operation uint8_t pcd_14a_reader_mf1_manipulate_value_block(uint8_t operator, uint8_t addr, int32_t operand); uint8_t pcd_14a_reader_mf1_transfer_value_block(uint8_t addr); diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index c42b80d..cfb72ff 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -14,6 +14,7 @@ from multiprocessing import Pool, cpu_count from typing import Union from pathlib import Path from platform import uname +from datetime import datetime import chameleon_com import chameleon_cmd @@ -851,6 +852,119 @@ class HFMFDarkside(ReaderRequiredUnit): return +@hf_mf.command('fchk') +class HFMFFCHK(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + + mifare_type_group = parser.add_mutually_exclusive_group() + mifare_type_group.add_argument('--mini', help='MIFARE Classic Mini / S20', action='store_const', dest='maxSectors', const=5) + mifare_type_group.add_argument('--1k', help='MIFARE Classic 1k / S50 (default)', action='store_const', dest='maxSectors', const=16) + mifare_type_group.add_argument('--2k', help='MIFARE Classic/Plus 2k', action='store_const', dest='maxSectors', const=32) + mifare_type_group.add_argument('--4k', help='MIFARE Classic 4k / S70', action='store_const', dest='maxSectors', const=40) + + parser.add_argument(dest='keys', help='Key (in hex[12] format)', metavar='', type=str, nargs='*') + parser.add_argument('-f', '--file', type=argparse.FileType('rb'), help='Read keys from file') + + file_type_group = parser.add_mutually_exclusive_group() + file_type_group.add_argument('--hex', action='store_const', dest='fileType', const='hex', help='Type of keys file is dictionary (in hex[12] format) (default)') + file_type_group.add_argument('-b', '--bin', action='store_const', dest='fileType', const='bin', help='Type of keys file is binary') + + parser.add_argument('-m', '--mask', help='Which sectorKey to be skip, 1 bit per sectorKey. `0b1` represent to skip to check. (in hex[20] format)', type=str, default='00000000000000000000', metavar='') + + parser.set_defaults(maxSectors=16, fileType='hex') + return parser + + def check_keys(self, mask: bytearray, keys: list[bytes], chunkSize=20): + sectorKeys = dict() + + for i in range(0, len(keys), chunkSize): + # print("mask = {}".format(mask.hex(sep=' ', bytes_per_sep=1))) + chunkKeys = keys[i:i+chunkSize] + print(f' - progress of checking keys... {CY}{i}{C0} / {len(keys)} ({CY}{100 * i / len(keys):.1f}{C0} %)') + resp = self.cmd.mf1_check_keys_of_sectors(mask, chunkKeys) + # print(resp) + + if resp["status"] != Status.HF_TAG_OK: + print(f' - check interrupted, reason: {CR}{str(Status(resp["status"]))}{C0}') + break + elif 'sectorKeys' not in resp: + print(f' - check interrupted, reason: {CG}All sectorKey is found or masked{C0}') + break + + for j in range(10): + mask[j] |= resp['found'][j] + sectorKeys.update(resp['sectorKeys']) + + return sectorKeys + + def on_exec(self, args: argparse.Namespace): + # print(args) + + keys = set() + + # keys from args + for key in args.keys: + if not re.match(r'^[a-fA-F0-9]{12}$', key): + print(f' - {CR}Key should in hex[12] format, invalid key is ignored{C0}, key = "{key}"') + continue + keys.add(bytes.fromhex(key)) + + # keys from file + if args.file is not None: + buf = bytearray() + + if args.fileType == 'bin': + buf.extend(args.file.read()) + elif args.fileType == 'hex': + text = re.sub(r'#.*$', '', args.file.read().decode('utf-8'), flags=re.MULTILINE) + # print(text) + buf.extend(bytearray.fromhex(text)) + + if len(buf) % 6 != 0: + print(f' - {CR}keys from file not align for 6 bytes{C0}') + return + + for i in range(0, len(buf), 6): + keys.add(bytes(buf[i:i+6])) + + if len(keys) == 0: + print(f' - {CR}No keys{C0}') + return + + print(f" - loaded {CG}{len(keys)}{C0} keys") + + # mask + if not re.match(r'^[a-fA-F0-9]{1,20}$', args.mask): + print(f' - {CR}mask should in hex[20] format{C0}, mask = "{args.mask}"') + return + mask = bytearray.fromhex(f'{args.mask:0<20}') + for i in range(args.maxSectors, 40): + mask[i // 4] |= 3 << (6 - i % 4 * 2) + + # check keys + startedAt = datetime.now() + sectorKeys = self.check_keys(mask, list(keys)) + endedAt = datetime.now() + duration = endedAt - startedAt + print(f" - elapsed time: {CY}{duration.total_seconds():.3f}s{C0}") + + # print sectorKeys + print(f"\n - {CG}result of key checking:{C0}\n") + print("-----+-----+--------------+---+--------------+----") + print(" Sec | Blk | key A |res| key B |res ") + print("-----+-----+--------------+---+--------------+----") + for sectorNo in range(args.maxSectors): + blk = (sectorNo * 4 + 3) if sectorNo < 32 else (sectorNo * 16 - 369) + keyA = sectorKeys.get(2 * sectorNo, None) + keyA = f"{CG}{keyA.hex().upper()}{C0} | {CG}1{C0}" if keyA else f"{CR}------------{C0} | {CR}0{C0}" + keyB = sectorKeys.get(2 * sectorNo + 1, None) + keyB = f"{CG}{keyB.hex().upper()}{C0} | {CG}1{C0}" if keyB else f"{CR}------------{C0} | {CR}0{C0}" + print(f" {CY}{sectorNo:03d}{C0} | {blk:03d} | {keyA} | {keyB} ") + print("-----+-----+--------------+---+--------------+----") + print(f"( {CR}0{C0}: Failed, {CG}1{C0}: Success )\n\n") + + @hf_mf.command('rdbl') class HFMFRDBL(MF1AuthArgsUnit): def args_parser(self) -> ArgumentParserNoExit: diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 12c11cc..2ef363d 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -300,6 +300,45 @@ class ChameleonCMD: resp.parsed = resp.status == Status.HF_TAG_OK return resp + @expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO]) + def mf1_check_keys_of_sectors(self, mask: bytes, keys: list[bytes]): + """ + Check keys of sectors. + :return: + """ + if len(mask) != 10: + raise ValueError("len(mask) should be 10") + if len(keys) < 1 or len(keys) > 83: + raise ValueError("Invalid len(keys)") + data = struct.pack(f'!10s{6*len(keys)}s', mask, b''.join(keys)) + + bitsCnt = 80 # maximum sectorKey_to_be_checked + for b in mask: + while b > 0: + [bitsCnt, b] = [bitsCnt - (b & 0b1), b >> 1] + if bitsCnt < 1: + # All sectorKey is masked + return chameleon_com.Response( + cmd=Command.MF1_CHECK_KEYS_OF_SECTORS, + status=Status.HF_TAG_OK, + parsed={ 'status': Status.HF_TAG_OK }, + ) + # base timeout: 1s + # auth: len(keys) * sectorKey_to_be_checked * 0.1s + # read keyB from trailer block: 0.1s + timeout = 1 + (bitsCnt + 1) * len(keys) * 0.1 + + resp = self.device.send_cmd_sync(Command.MF1_CHECK_KEYS_OF_SECTORS, data, timeout=timeout) + resp.parsed = { 'status': resp.status } + if len(resp.data) == 490: + found = ''.join([format(i, '08b') for i in resp.data[0:10]]) + # print(f'{found = }') + resp.parsed.update({ + 'found': resp.data[0:10], + 'sectorKeys': {k: resp.data[6 * k + 10:6 * k + 16] for k, v in enumerate(found) if v == '1'} + }) + return resp + @expect_response(Status.HF_TAG_OK) def mf1_static_nested_acquire(self, block_known, type_known, key_known, block_target, type_target): """ diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index 58d6359..2c13b8f 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -68,6 +68,7 @@ class Command(enum.IntEnum): MF1_WRITE_ONE_BLOCK = 2009 HF14A_RAW = 2010 MF1_MANIPULATE_VALUE_BLOCK = 2011 + MF1_CHECK_KEYS_OF_SECTORS = 2012 EM410X_SCAN = 3000 EM410X_WRITE_TO_T55XX = 3001 From 3e6a2b505bc81fad8ec464b0da46cbe83fa42d5a Mon Sep 17 00:00:00 2001 From: you Date: Sun, 28 Jan 2024 17:48:18 +0100 Subject: [PATCH 2/8] Add fchk argument to export found keys to file --- software/script/chameleon_cli_unit.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index cfb72ff..e3dbb52 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -865,6 +865,7 @@ class HFMFFCHK(ReaderRequiredUnit): parser.add_argument(dest='keys', help='Key (in hex[12] format)', metavar='', type=str, nargs='*') parser.add_argument('-f', '--file', type=argparse.FileType('rb'), help='Read keys from file') + parser.add_argument('-e', '--export-file', type=argparse.FileType('w'), help='Export found keys to file', default=None) file_type_group = parser.add_mutually_exclusive_group() file_type_group.add_argument('--hex', action='store_const', dest='fileType', const='hex', help='Type of keys file is dictionary (in hex[12] format) (default)') @@ -962,8 +963,14 @@ class HFMFFCHK(ReaderRequiredUnit): keyB = f"{CG}{keyB.hex().upper()}{C0} | {CG}1{C0}" if keyB else f"{CR}------------{C0} | {CR}0{C0}" print(f" {CY}{sectorNo:03d}{C0} | {blk:03d} | {keyA} | {keyB} ") print("-----+-----+--------------+---+--------------+----") - print(f"( {CR}0{C0}: Failed, {CG}1{C0}: Success )\n\n") + print(f"( {CR}0{C0}: Failed, {CG}1{C0}: Success )\n") + if args.export_file: + for sectorNo in range(args.maxSectors): + keyA = sectorKeys.get(2 * sectorNo, None) + keyB = sectorKeys.get(2 * sectorNo + 1, None) + args.export_file.write(f"{keyA.hex()}:{keyB.hex()}\n") + print(f" - keys exported to: {CG}{args.export_file.name}{C0}\n") @hf_mf.command('rdbl') class HFMFRDBL(MF1AuthArgsUnit): From a1a49ed953c807c44eb85ac94652748aaa97168c Mon Sep 17 00:00:00 2001 From: you Date: Sun, 28 Jan 2024 17:54:20 +0100 Subject: [PATCH 3/8] Add CLI command to dump a MF tag to file (bin/hex) --- software/script/chameleon_cli_unit.py | 56 +++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index e3dbb52..110bda2 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1005,6 +1005,62 @@ class HFMFWRBL(MF1AuthArgsUnit): else: print(f" - {CR}Write fail.{C0}") +@hf_mf.command('dump') +class HFMFDump(MF1AuthArgsUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Mifare Classic dump tag' + parser.add_argument('-t', '--dump-file-type', type=str, required=False, help="Dump file content type", choices=['bin', 'hex']) + parser.add_argument('-f', '--dump-file', type=argparse.FileType("wb"), required=True, + help="Dump file to write data from tag") + parser.add_argument('-k', '--key-file', type=argparse.FileType("r"), required=True, + help="File containing keys of tag to write (exported with fchk --export)") + return parser + + def on_exec(self, args: argparse.Namespace): + # check dump type + if args.dump_file_type is None: + if args.dump_file.name.endswith('.bin'): + content_type = 'bin' + elif args.dump_file.name.endswith('.eml'): + content_type = 'hex' + else: + raise Exception("Unknown file format, Specify content type with -t option") + else: + content_type = args.type + + # read keys from file + keys = list() + for line in args.key_file.readlines(): + a, b = [bytes.fromhex(h) for h in line[:-1].split(":")] + keys.append((a, b)) + + # data to write from dump file + buffer = bytearray() + + # iterate over sectors + for s in range(16): + # iterate over blocks + for b in range(4): + resp = None + try: + # first try with key B + resp = self.cmd.mf1_read_one_block(4*s + b, MfcKeyType.B, keys[s][1]) + except UnexpectedResponseError: + # ignore read errors at this stage as we want to try key A + pass + if not resp: + # try with key A if B was unsuccessful + # this will raise an exception if key A fails too + resp = self.cmd.mf1_read_one_block(4*s + b, MfcKeyType.A, keys[s][0]) + # add data to buffer + if content_type == 'bin': + buffer.extend(resp) + elif content_type == 'hex': + buffer.extend(resp.hex()) + # write buffer to file + args.dump_file.write(buffer) + @hf_mf.command('value') class HFMFVALUE(ReaderRequiredUnit): From 8a02d6f4f84b84ebc4a4605503b5c843045339e5 Mon Sep 17 00:00:00 2001 From: you Date: Sun, 28 Jan 2024 17:55:01 +0100 Subject: [PATCH 4/8] Add CLI command to clone a MF tag from file (bin/hex) --- software/script/chameleon_cli_unit.py | 70 +++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 110bda2..324d900 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1061,6 +1061,76 @@ class HFMFDump(MF1AuthArgsUnit): # write buffer to file args.dump_file.write(buffer) +@hf_mf.command('clone') +class HFMFClone(MF1AuthArgsUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Mifare Classic clone tag from dump' + parser.add_argument('-t', '--dump-file-type', type=str, required=False, help="Dump file content type", choices=['bin', 'hex']) + parser.add_argument('-a', '--clone-access', type=bool, default=False, help="Write ACL from original dump too (/!\ could brick your tag)") + parser.add_argument('-f', '--dump-file', type=argparse.FileType("rb"), required=True, + help="Dump file containing data to write on new tag") + parser.add_argument('-k', '--key-file', type=argparse.FileType("r"), required=True, + help="File containing keys of tag to write (exported with fchk)") + return parser + + def on_exec(self, args: argparse.Namespace): + if args.dump_file_type is None: + if args.dump_file.name.endswith('.bin'): + content_type = 'bin' + elif args.dump_file.name.endswith('.eml'): + content_type = 'hex' + else: + raise Exception("Unknown file format, Specify content type with -t option") + else: + content_type = args.type + + # data to write from dump file + buffer = bytearray() + if content_type == 'bin': + buffer.extend(args.dump_file.read()) + if content_type == 'hex': + buffer.extend(bytearray.fromhex(args.dump_file.read().decode())) + if len(buffer) % 16 != 0: + raise Exception("Data block not align for 16 bytes") + if len(buffer) / 16 > 256: + raise Exception("Data block memory overflow") + + # keys to use from file + keys = list() + for line in args.key_file.readlines(): + a, b = [bytes.fromhex(h) for h in line[:-1].split(":")] + keys.append((a, b)) + + # iterate over sectors + for s in range(16): + # iterate over blocks + for b in range(4): + data = buffer[(4*s+b)*16:(4*s+b+1)*16] + # special case for last block of each sector + if b == 3: + # check ACL option + if not args.clone_access: + # if option is not specified, use generic ACL to be able to write again + data = data[:6] + bytes.fromhex("08778F") + data[9:] + ok = False + try: + # try B key first + resp = self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.B, keys[s][1], data) + if resp: + ok = True + except UnexpectedResponseError: + pass + if not ok: + # try with key B + try: + resp = self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.A, keys[s][0], data) + if resp: + ok = True + except UnexpectedResponseError: + pass + if not ok: + print(f"[!] error writing block {4*s + b}") @hf_mf.command('value') class HFMFVALUE(ReaderRequiredUnit): From 6ddda661d870c1ce0cda11003948c366388767dc Mon Sep 17 00:00:00 2001 From: you Date: Tue, 23 Apr 2024 23:45:13 +0200 Subject: [PATCH 5/8] tosqash --- software/script/chameleon_cli_unit.py | 32 --------------------------- 1 file changed, 32 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 711d71e..17a1930 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -910,23 +910,6 @@ class HFMFFCHK(ReaderRequiredUnit): continue keys.add(bytes.fromhex(key)) -<<<<<<< HEAD - # keys from file - if args.file is not None: - buf = bytearray() - - if args.fileType == 'bin': - buf.extend(args.file.read()) - elif args.fileType == 'hex': - text = re.sub(r'#.*$', '', args.file.read().decode('utf-8'), flags=re.MULTILINE) - # print(text) - buf.extend(bytearray.fromhex(text)) - - if len(buf) % 6 != 0: - print(f' - {CR}keys from file not align for 6 bytes{C0}') - return - -======= # read keys from key format file if args.import_key is not None: buf = args.import_key.read() @@ -942,7 +925,6 @@ class HFMFFCHK(ReaderRequiredUnit): if len(buf) % 6 != 0: print(f' - {CR}Failed to parse keys from {args.import_dic.name} (as .dic format){C0}') return ->>>>>>> 767f6e2f7e643fee25c40b2fd6e04240a19a9e00 for i in range(0, len(buf), 6): keys.add(bytes(buf[i:i+6])) @@ -967,8 +949,6 @@ class HFMFFCHK(ReaderRequiredUnit): duration = endedAt - startedAt print(f" - elapsed time: {CY}{duration.total_seconds():.3f}s{C0}") -<<<<<<< HEAD -======= if args.export_key is not None: unknownkey = bytes(6) for sectorNo in range(args.maxSectors): @@ -982,7 +962,6 @@ class HFMFFCHK(ReaderRequiredUnit): args.export_dic.write(key.hex().upper() + '\n') print(f" - result exported to: {CG}{args.export_dic.name}{C0} (as .dic format)") ->>>>>>> 767f6e2f7e643fee25c40b2fd6e04240a19a9e00 # print sectorKeys print(f"\n - {CG}result of key checking:{C0}\n") print("-----+-----+--------------+---+--------------+----") @@ -996,19 +975,8 @@ class HFMFFCHK(ReaderRequiredUnit): keyB = f"{CG}{keyB.hex().upper()}{C0} | {CG}1{C0}" if keyB else f"{CR}------------{C0} | {CR}0{C0}" print(f" {CY}{sectorNo:03d}{C0} | {blk:03d} | {keyA} | {keyB} ") print("-----+-----+--------------+---+--------------+----") -<<<<<<< HEAD - print(f"( {CR}0{C0}: Failed, {CG}1{C0}: Success )\n") - - if args.export_file: - for sectorNo in range(args.maxSectors): - keyA = sectorKeys.get(2 * sectorNo, None) - keyB = sectorKeys.get(2 * sectorNo + 1, None) - args.export_file.write(f"{keyA.hex()}:{keyB.hex()}\n") - print(f" - keys exported to: {CG}{args.export_file.name}{C0}\n") -======= print(f"( {CR}0{C0}: Failed, {CG}1{C0}: Success )\n\n") ->>>>>>> 767f6e2f7e643fee25c40b2fd6e04240a19a9e00 @hf_mf.command('rdbl') class HFMFRDBL(MF1AuthArgsUnit): From 4568e8fd576a1be8ab37dbc7f853cb71dc025c27 Mon Sep 17 00:00:00 2001 From: you Date: Wed, 24 Apr 2024 00:41:13 +0200 Subject: [PATCH 6/8] Fix dump and clone commands accordingly to PR#199 --- software/script/chameleon_cli_unit.py | 98 ++++++++++++++++----------- 1 file changed, 57 insertions(+), 41 deletions(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 17a1930..bcf25a5 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1019,8 +1019,8 @@ class HFMFDump(MF1AuthArgsUnit): parser.add_argument('-t', '--dump-file-type', type=str, required=False, help="Dump file content type", choices=['bin', 'hex']) parser.add_argument('-f', '--dump-file', type=argparse.FileType("wb"), required=True, help="Dump file to write data from tag") - parser.add_argument('-k', '--key-file', type=argparse.FileType("r"), required=True, - help="File containing keys of tag to write (exported with fchk --export)") + parser.add_argument('-d', '--dic', type=argparse.FileType("r"), required=True, + help="Read keys (to communicate with tag to dump) from .dic format file") return parser def on_exec(self, args: argparse.Namespace): @@ -1033,37 +1033,44 @@ class HFMFDump(MF1AuthArgsUnit): else: raise Exception("Unknown file format, Specify content type with -t option") else: - content_type = args.type + content_type = args.dump_file_type # read keys from file - keys = list() - for line in args.key_file.readlines(): - a, b = [bytes.fromhex(h) for h in line[:-1].split(":")] - keys.append((a, b)) + keys = [bytes.fromhex(line[:-1]) for line in args.dic.readlines()] # data to write from dump file buffer = bytearray() # iterate over sectors for s in range(16): - # iterate over blocks - for b in range(4): - resp = None + # try all keys for this sector + typ = None + for key in keys: + # first try key B try: - # first try with key B - resp = self.cmd.mf1_read_one_block(4*s + b, MfcKeyType.B, keys[s][1]) + self.cmd.mf1_read_one_block(4*s, MfcKeyType.B, key) + typ = MfcKeyType.B + break except UnexpectedResponseError: # ignore read errors at this stage as we want to try key A pass - if not resp: - # try with key A if B was unsuccessful - # this will raise an exception if key A fails too - resp = self.cmd.mf1_read_one_block(4*s + b, MfcKeyType.A, keys[s][0]) + # try with key A if B was unsuccessful + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.A, key) + typ = MfcKeyType.A + break + except UnexpectedResponseError: + pass + else: + raise Exception(f"No key found for sector {s}") + # iterate over blocks + for b in range(4): + block_data = self.cmd.mf1_read_one_block(4*s + b, typ, key) # add data to buffer if content_type == 'bin': - buffer.extend(resp) + buffer.extend(block_data) elif content_type == 'hex': - buffer.extend(resp.hex()) + buffer.extend(block_data.hex().encode("utf-8")) # write buffer to file args.dump_file.write(buffer) @@ -1076,8 +1083,8 @@ class HFMFClone(MF1AuthArgsUnit): parser.add_argument('-a', '--clone-access', type=bool, default=False, help="Write ACL from original dump too (/!\ could brick your tag)") parser.add_argument('-f', '--dump-file', type=argparse.FileType("rb"), required=True, help="Dump file containing data to write on new tag") - parser.add_argument('-k', '--key-file', type=argparse.FileType("r"), required=True, - help="File containing keys of tag to write (exported with fchk)") + parser.add_argument('-d', '--dic', type=argparse.FileType("r"), required=True, + help="Read keys (to communicate with tag to write) from .dic format file") return parser def on_exec(self, args: argparse.Namespace): @@ -1089,7 +1096,7 @@ class HFMFClone(MF1AuthArgsUnit): else: raise Exception("Unknown file format, Specify content type with -t option") else: - content_type = args.type + content_type = args.dump_file_type # data to write from dump file buffer = bytearray() @@ -1103,40 +1110,49 @@ class HFMFClone(MF1AuthArgsUnit): raise Exception("Data block memory overflow") # keys to use from file - keys = list() - for line in args.key_file.readlines(): - a, b = [bytes.fromhex(h) for h in line[:-1].split(":")] - keys.append((a, b)) + keys = [bytes.fromhex(line[:-1]) for line in args.dic.readlines()] # iterate over sectors for s in range(16): + # try all keys for this sector + keyA, keyB = None, None + for key in keys: + # first try key B + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.B, key) + keyB = key + except UnexpectedResponseError: + # ignore read errors at this stage as we want to try key A + pass + # try with key A if B was unsuccessful + try: + self.cmd.mf1_read_one_block(4*s, MfcKeyType.A, key) + keyA = key + except UnexpectedResponseError: + pass + # both keys were found, no need to continue iterating + if keyA and keyB: + break + # neither A or B key was found + if not keyA and not keyB: + raise Exception(f"No key found for sector {s}") # iterate over blocks for b in range(4): - data = buffer[(4*s+b)*16:(4*s+b+1)*16] + block_data = buffer[(4*s+b)*16:(4*s+b+1)*16] # special case for last block of each sector if b == 3: # check ACL option if not args.clone_access: # if option is not specified, use generic ACL to be able to write again - data = data[:6] + bytes.fromhex("08778F") + data[9:] - ok = False + block_data = block_data[:6] + bytes.fromhex("08778F") + block_data[9:] try: # try B key first - resp = self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.B, keys[s][1], data) - if resp: - ok = True + self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.B, keyB, block_data) + continue except UnexpectedResponseError: pass - if not ok: - # try with key B - try: - resp = self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.A, keys[s][0], data) - if resp: - ok = True - except UnexpectedResponseError: - pass - if not ok: - print(f"[!] error writing block {4*s + b}") + self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.A, keyA, block_data) + @hf_mf.command('value') class HFMFVALUE(ReaderRequiredUnit): From 0686d0816d3b3eca1baec06d7202a5ce81b8fd8d Mon Sep 17 00:00:00 2001 From: you Date: Wed, 24 Apr 2024 00:42:33 +0200 Subject: [PATCH 7/8] Fix default ACL (suggested by @taichunmin) --- software/script/chameleon_cli_unit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index bcf25a5..8e691b9 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -1144,7 +1144,7 @@ class HFMFClone(MF1AuthArgsUnit): # check ACL option if not args.clone_access: # if option is not specified, use generic ACL to be able to write again - block_data = block_data[:6] + bytes.fromhex("08778F") + block_data[9:] + block_data = block_data[:6] + bytes.fromhex("ff0780") + block_data[9:] try: # try B key first self.cmd.mf1_write_one_block(4*s + b, MfcKeyType.B, keyB, block_data) From fcbf474e19bd9aa577884cdc391ae7992b95c447 Mon Sep 17 00:00:00 2001 From: you Date: Wed, 24 Apr 2024 00:50:17 +0200 Subject: [PATCH 8/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 135277e..ab575ae 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] + - Added commands to dump and clone Mifare tags - Added command to check keys of multiple sectors at once (@taichunmin) - Fixed unused target key type parameter for nested (@petepriority) - Skip already used items `hf mf elog --decrypt` (@p-l-)