diff --git a/.github/workflows/on_push.yml b/.github/workflows/on_push.yml index 9e94ae5..5531e21 100644 --- a/.github/workflows/on_push.yml +++ b/.github/workflows/on_push.yml @@ -28,9 +28,10 @@ jobs: - name: Download release artifacts uses: actions/download-artifact@v4 with: - path: release-artifacts + name: release-artifacts pattern: release-artifacts-* merge-multiple: true + path: release-artifacts - name: Upload to dev release uses: softprops/action-gh-release@v1 with: @@ -65,9 +66,10 @@ jobs: - name: Download release artifacts uses: actions/download-artifact@v4 with: - path: release-artifacts + name: release-artifacts pattern: release-artifacts-* merge-multiple: true + path: release-artifacts - name: Upload to tagged release uses: softprops/action-gh-release@v1 with: diff --git a/CHANGELOG.md b/CHANGELOG.md index 107defc..bf32f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac ## [unreleased][unreleased] - Added `firmware/docker-compose.yml` to build firmware in local docker (@taichunmin) + - Added cmd to acquire nonces for hardnested(Protocol doc need update) (@xianglin1998) - 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-) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index cff94f1..072cb3f 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -363,6 +363,44 @@ static data_frame_tx_t *cmd_processor_mf1_check_keys_of_sectors(uint16_t cmd, ui return data_frame_make(cmd, status, sizeof(out), (uint8_t *)&out); } +static data_frame_tx_t *cmd_processor_mf1_hardnested_nonces_acquire(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + typedef struct { + uint8_t slow; + uint8_t type_known; + uint8_t block_known; + uint8_t key_known[6]; + uint8_t type_target; + uint8_t block_target; + } PACKED payload_t; + if (length != sizeof(payload_t)) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + payload_t *payload = (payload_t *)data; + + // It is enough to collect 110 nonces at a time. The total transmitted data payload is 495 + 1 bytes + // Then, the total length can be controlled within 512, so that when encountering a BLE host that supports large packets, one communication can be completed. + // There is no need to send or receive packets in separate packets, which improves communication speed. + uint8_t nonces[500] = { 0x00 }; + if (length < 11) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + status = mf1_hardnested_nonces_acquire( + payload->slow, + payload->block_known, + payload->type_known, + bytes_to_num(payload->key_known, 6), + payload->block_target, + payload->type_target, + nonces + 1, + sizeof(nonces) - 1, // The upper limit of the buffer size. Here we take out the first byte to mark the number of collections. + &nonces[0] // The number of random numbers collected above + ); + if (status != STATUS_HF_TAG_OK) { + return data_frame_make(cmd, status, 0, NULL); + } + return data_frame_make(cmd, status, nonces[0] * 4.5, (uint8_t *)(nonces + 1)); +} + 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; @@ -1295,7 +1333,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { 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_MF1_HARDNESTED_ACQUIRE, before_hf_reader_run, cmd_processor_mf1_hardnested_nonces_acquire, 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 6b0ff58..7d6d076 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -68,6 +68,7 @@ #define DATA_CMD_HF14A_RAW (2010) #define DATA_CMD_MF1_MANIPULATE_VALUE_BLOCK (2011) #define DATA_CMD_MF1_CHECK_KEYS_OF_SECTORS (2012) +#define DATA_CMD_MF1_HARDNESTED_ACQUIRE (2013) // // ****************************************************************** diff --git a/firmware/application/src/rfid/reader/hf/mf1_toolbox.c b/firmware/application/src/rfid/reader/hf/mf1_toolbox.c index 32e2bf4..6658af8 100644 --- a/firmware/application/src/rfid/reader/hf/mf1_toolbox.c +++ b/firmware/application/src/rfid/reader/hf/mf1_toolbox.c @@ -17,7 +17,7 @@ // The default delay of the antenna reset static uint32_t g_ant_reset_delay = 100; -// Label information used for global operations +// tag information used for this module. static picc_14a_tag_t m_tag_info; static picc_14a_tag_t *p_tag_info = &m_tag_info; @@ -1095,4 +1095,97 @@ uint16_t mf1_toolbox_check_keys_of_sectors ( } return STATUS_HF_TAG_OK; -} \ No newline at end of file +} + +/** +* @brief : HardNested random number acquisition implementation +* @param :slow : Is it a low-speed acquisition mode? Low-speed acquisition is suitable for some non-standard cards +* @param :keyKnown : The known secret key of the card +* @param :blkKnown : The sector to which the known secret key of the card belongs +* @param :typKnown : The type of the known secret key of the card, 0x60 (A secret key) or 0x61 (B secret key) +* @param :targetBlk : The target sector for nested attack +* @param :targetTyp : The target secret key type for nested attack +* @param :nonces : The buffer for storing random numbers +* @param :noncesMax : The upper limit of the random number buffer in bytes +* @retval : STATUS_HF_TAG_OK is returned if the acquisition is successful, and non-HF_TAG_OK is returned if the acquisition is unsuccessful Value +* +*/ +uint8_t mf1_hardnested_nonces_acquire(bool slow, uint8_t blkKnown, uint8_t typKnown, uint64_t keyKnown, + uint8_t targetBlk, uint8_t targetTyp, uint8_t* nonces, uint16_t noncesMax, uint8_t* num_nonces) { + struct Crypto1State mpcs = { 0, 0 }; + struct Crypto1State *pcs = &mpcs; + uint8_t answer[] = { 0x00, 0x00, 0x00, 0x00 }; + uint8_t parity[] = { 0x00, 0x00, 0x00, 0x00 }; + uint8_t nt_par_enc = 0; + uint8_t status = STATUS_HF_TAG_NO; + uint32_t cuid = 0; // cuid can be fixed when selecting card + uint16_t len = 0; + *num_nonces = 0; // The number of random numbers currently counted must be reset + bool tag_selected = false; + uint8_t err_count = 0; + + for (uint16_t i = 0; i <= noncesMax - 9;) { + // NRF_LOG_INFO("AcquireEncryptedNonces: %d\r\n", i); + if (tag_selected) { + mf1_toolbox_report_healthy(); + if (pcd_14a_reader_fast_select(p_tag_info) != STATUS_HF_TAG_OK) { + NRF_LOG_INFO("AcquireEncryptedNonces: Tag lost\r\n"); + if (++err_count >= 15) { + return STATUS_HF_TAG_NO; + } + continue; + } + // Slow mode, delay some time? + if (slow) { + bsp_delay_us(400); + } + // First auth + if (authex(pcs, cuid, blkKnown, typKnown, keyKnown, AUTH_FIRST, NULL) != STATUS_HF_TAG_OK) { + NRF_LOG_INFO("AcquireEncryptedNonces: Auth1 error\r\n"); + if (++err_count >= 15) { + return STATUS_MF_ERR_AUTH; + } + continue; + } + // Nested auth + len = send_cmd(pcs, AUTH_NESTED, targetTyp, targetBlk, &status, answer, parity, U8ARR_BIT_LEN(answer)); + if (len != 32) { + NRF_LOG_INFO("AcquireEncryptedNonces: Auth2 error len=%d\r\n", len); + if (++err_count >= 15) { + return STATUS_HF_ERR_STAT; + } + continue; + } + // Reset err count + err_count = 0; + // merge parity + uint8_t par_enc = 0; + par_enc |= parity[3] << 4; + par_enc |= parity[2] << 5; + par_enc |= parity[1] << 6; + par_enc |= parity[0] << 7; + // copy to buffer + *num_nonces = *num_nonces + 1; + if (*num_nonces % 2) { + memcpy(nonces + i, answer, 4); + nt_par_enc = par_enc & 0xf0; + } else { + nt_par_enc |= par_enc >> 4; + memcpy(nonces + i + 4, answer, 4); + memcpy(nonces + i + 8, &nt_par_enc, 1); + i += 9; + } + } else { + // scan the tag to fixed cuid. + status = pcd_14a_reader_scan_auto(p_tag_info); + if (status != STATUS_HF_TAG_OK) { + return STATUS_HF_TAG_NO; + } + cuid = get_u32_tag_uid(p_tag_info); + tag_selected = true; + } + } + + // OK! + return STATUS_HF_TAG_OK; +} diff --git a/firmware/application/src/rfid/reader/hf/mf1_toolbox.h b/firmware/application/src/rfid/reader/hf/mf1_toolbox.h index c26fe47..128e82e 100644 --- a/firmware/application/src/rfid/reader/hf/mf1_toolbox.h +++ b/firmware/application/src/rfid/reader/hf/mf1_toolbox.h @@ -118,6 +118,9 @@ uint16_t mf1_toolbox_check_keys_of_sectors ( mf1_toolbox_check_keys_of_sectors_out_t *out ); +uint8_t mf1_hardnested_nonces_acquire(bool slow, uint8_t blkKnown, uint8_t typKnown, uint64_t keyKnown, + uint8_t targetBlk, uint8_t targetTyp, uint8_t* nonces, uint16_t noncesMax, uint8_t* num_nonces); + #ifdef __cplusplus } #endif diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index 2c99d39..2d11d5d 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -42,7 +42,18 @@ type_id_SAK_dict = {0x00: "MIFARE Ultralight Classic/C/EV1/Nano | NTAG 2xx", default_cwd = Path.cwd() / Path(__file__).with_name("bin") +def load_key_file(import_key, keys): + """ + Load key file and append its content to the provided set of keys. + Each key is expected to be on a new line in the file. + """ + with open(import_key.name, 'rb') as file: + keys.update(line.encode('utf-8') for line in file.read().decode('utf-8').splitlines()) + return keys +def load_dic_file(import_dic, keys): + return keys + def check_tools(): tools = ['staticnested', 'nested', 'darkside', 'mfkey32v2'] if sys.platform == "win32": diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index 9c9f2c2..47c062f 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -358,6 +358,18 @@ class ChameleonCMD: ] } return resp + + @expect_response(Status.HF_TAG_OK) + def mf1_hard_nested_acquire(self, slow, block_known, type_known, key_known, block_target, type_target): + """ + Collect the NT_ENC list for HardNested decryption + :return: + """ + data = struct.pack('!BBB6sBB', slow, type_known, block_known, key_known, type_target, block_target) + resp = self.device.send_cmd_sync(Command.DATA_CMD_MF1_HARDNESTED_ACQUIRE, data) + if resp.status == Status.HF_TAG_OK: + resp.parsed = resp.data # we can return the raw nonces bytes + return resp @expect_response(Status.LF_TAG_OK) def em410x_scan(self): diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index 3bb9868..ff5f693 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -69,6 +69,7 @@ class Command(enum.IntEnum): HF14A_RAW = 2010 MF1_MANIPULATE_VALUE_BLOCK = 2011 MF1_CHECK_KEYS_OF_SECTORS = 2012 + DATA_CMD_MF1_HARDNESTED_ACQUIRE = 2013 EM410X_SCAN = 3000 EM410X_WRITE_TO_T55XX = 3001 diff --git a/software/script/hardnested_utils.py b/software/script/hardnested_utils.py new file mode 100644 index 0000000..81bec0f --- /dev/null +++ b/software/script/hardnested_utils.py @@ -0,0 +1,41 @@ +hardnested_sums = [0, 32, 56, 64, 80, 96, 104, 112, 120, 128, 136, 144, 152, 160, 176, 192, 200, 224, 256] +hardnested_nonces_sum_map = [] +hardnested_first_byte_num = 0 +hardnested_first_byte_sum = 0 + + +def evenparity32(n): + """ + calc evenparity32, can replace to any fast native impl... + @param n - NT_ENC + """ + ret = 0 + for i in range(32): + if (n & (1 << i)) != 0: + ret += 1 + return ret % 2 + + +def check_nonce_unique_sum(nt, par): + """ + Check nt_enc is unique and calc first byte sum + Pay attention: thread unsafe!!! + @param nt - NT_ENC + @param par - parity of NT_ENC + """ + global hardnested_first_byte_sum, hardnested_first_byte_num + first_byte = nt >> 24 + if not hardnested_nonces_sum_map[first_byte]: + hardnested_first_byte_sum += evenparity32((nt & 0xff000000) | (par & 0x08)) + hardnested_nonces_sum_map[first_byte] = True + hardnested_first_byte_num += 1 + + +def reset(): + global hardnested_first_byte_sum, hardnested_first_byte_num, hardnested_nonces_sum_map + # clear the history + hardnested_nonces_sum_map = list() + for i in range(256): + hardnested_nonces_sum_map.append(False) + hardnested_first_byte_sum = 0 + hardnested_first_byte_num = 0 diff --git a/software/script/tests/test_hard_acquire.py b/software/script/tests/test_hard_acquire.py new file mode 100644 index 0000000..b1f0909 --- /dev/null +++ b/software/script/tests/test_hard_acquire.py @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 +import sys +sys.path.append('..') + +from chameleon_com import ChameleonCom, OpenFailException +from chameleon_cmd import ChameleonCMD +import hardnested_utils + + +def test_hardnested_acquire(): + nonces_buffer = bytearray() + acquire_count = 0 + + # known key and target block + key = bytes.fromhex("????????????") + block_known = 0x00 + type_known = 0x60 + block_target = 0x07 + type_target = 0x60 + + + # Before acquire start, we need to reset history + hardnested_utils.reset() + + # The nonces file format required by PM3: + # (4byte uid of card) - (block_target 1byte) - (type_target 1byte) - (nonces from device Nbytes) + + # ------------------------ open the device ------------------------ + try: + cml = ChameleonCom().open('com19') + except OpenFailException: + cml = ChameleonCom().open('/dev/ttyACM0') + cml_cmd = ChameleonCMD(cml) + + # ------------------------ append tag info ------------------------ + + resp = cml_cmd.hf14a_scan() + if resp is None or len(resp) == 0: + print("ISO14443-A Tag no found") + return + + uidbytes = bytearray.fromhex(resp['uid']) + uid_len = len(uidbytes) + if uid_len == 4: + nonces_buffer.extend(uidbytes[0: 4]) + if uid_len == 7: + nonces_buffer.extend(uidbytes[3: 7]) + if uid_len == 10: + nonces_buffer.extend(uidbytes[6: 10]) + + nonces_buffer.extend([block_target, type_target & 0x01]) + + # ------------------------ append nonces from device ------------------------ + + while True: + # 1, acquire from device + acquire_datas = cml_cmd.mf1_hard_nested_acquire(0, block_known, type_known, key, block_target, type_target) # slow = 0 to fast acquire... + if acquire_datas is not None: + acquire_count += 1 + print(f"Acquire success, count: {acquire_count}") + else: + raise Exception(f"acquire failed") + # 2. check data + data_check_index = 0 + while data_check_index < len(acquire_datas): + # Memory Layout: nt_enc1(4byte) - nt_enc2(4byte) - par(1byte)... + # To integer + nt_enc1 = int.from_bytes(acquire_datas[data_check_index + 0: data_check_index + 0 + 4]) + nt_enc2 = int.from_bytes(acquire_datas[data_check_index + 4: data_check_index + 4 + 4]) + par_enc = acquire_datas[data_check_index + 8] + # check unique and sum + hardnested_utils.check_nonce_unique_sum(nt_enc1, par_enc >> 4) + hardnested_utils.check_nonce_unique_sum(nt_enc2, par_enc & 0x0F) + data_check_index += 9 # The two ciphertext random numbers have a total of 8 bytes, and the parity bits corresponding to the two ciphertext random numbers occupy one byte + # 3. store data + nonces_buffer.extend(acquire_datas) + # 4. After collecting 256 possible different groups, determine whether the collected data is summed correctly. If not, it may not be an EV1 tag. + if hardnested_utils.hardnested_first_byte_num == 256: + got_match = False + for i in range(len(hardnested_utils.hardnested_sums)): + if hardnested_utils.hardnested_first_byte_sum == hardnested_utils.hardnested_sums[i]: + got_match = True # Sum matches successfully, and we can try to decrypt it next. + break + if got_match: + print(f"Acquire finish, save to file [nonces.bin], size is {len(nonces_buffer)}bytes") + break + else: + print( + f"hardnested_first_byte_num exceeds the limit but got_match is false: {hardnested_utils.hardnested_first_byte_sum}") + else: + continue # Continue acquire + + # ------------------------ write nonces to bin ------------------------ + with open("nonces.bin", mode="wb+") as fd: + fd.write(nonces_buffer) + + # You can decrypt nonce bin by pm3 client, or any app if support pm3 nonce bin format. + # TODO If CU bin can decrypt, run cmd on here... diff --git a/software/src/CMakeLists.txt b/software/src/CMakeLists.txt index 7f64487..73f97d2 100644 --- a/software/src/CMakeLists.txt +++ b/software/src/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required (VERSION 3.1) +cmake_minimum_required (VERSION 3.5) project (mifare C)