diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bea1f7..6e8caaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,17 @@ 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] + - Fix for FAST_READ command for nfc - mf0 tags + - Rewrite of the dynamic and static locks logic for NTAG213, NTAG215 and NTAG216; we shouldn't take into account the block lock bits + - Fixed an issue where we wouldn't be able to change CFG0 and CFG1 for NTAG213, NTAG215 and NTG216 once a password was added even if the cfg bit was reset. - Fix for static nested key recovery (@jekkos) + - Fix LEDs being stuck on after battery check (@suut) + - Add TCP support for the CLI (@suut) + - Fix build on Android in Termux (@suut) + - Fix the issue where some reader cause CU to enter a strange state (@xianglin1998) + - The transmission performance of USB has been improved (@xianglin1998) + - Added cmd for set mf1 config 'field_off_do_reset' (@xianglin1998) + ## [v2.1.0][2025-09-02] - Added UV, formatter and linter. Contribution guidelines. (@GameTec-live) diff --git a/firmware/application/src/app_cmd.c b/firmware/application/src/app_cmd.c index a48b27e..23ee40f 100644 --- a/firmware/application/src/app_cmd.c +++ b/firmware/application/src/app_cmd.c @@ -996,7 +996,7 @@ static data_frame_tx_t *cmd_processor_mf1_write_emu_block_data(uint16_t cmd, uin uint8_t block_index = data[0]; uint8_t block_count = (length - 1) / NFC_TAG_MF1_DATA_SIZE; if (block_index + block_count > NFC_TAG_MF1_BLOCK_MAX) { - status = STATUS_PAR_ERR; + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); } tag_data_buffer_t *buffer = get_buffer_by_tag_type(TAG_TYPE_MIFARE_4096); nfc_tag_mf1_information_t *info = (nfc_tag_mf1_information_t *)buffer->buffer; @@ -1374,6 +1374,19 @@ static data_frame_tx_t *cmd_processor_mf1_set_write_mode(uint16_t cmd, uint16_t return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); } +static data_frame_tx_t *cmd_processor_mf1_get_field_off_do_reset(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + uint8_t enable = nfc_tag_mf1_is_field_off_do_reset(); + return data_frame_make(cmd, STATUS_SUCCESS, 1, &enable); +} + +static data_frame_tx_t *cmd_processor_mf1_set_field_off_do_reset(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { + if (length != 1 || data[0] >= 2) { + return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL); + } + nfc_tag_mf1_set_field_off_do_reset(data[0]); + return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL); +} + static data_frame_tx_t *cmd_processor_get_enabled_slots(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) { struct { uint8_t enabled_hf; @@ -1630,6 +1643,8 @@ static cmd_data_map_t m_data_cmd_map[] = { { DATA_CMD_MF1_SET_BLOCK_ANTI_COLL_MODE, NULL, cmd_processor_mf1_set_block_anti_coll_mode, NULL }, { DATA_CMD_MF1_GET_WRITE_MODE, NULL, cmd_processor_mf1_get_write_mode, NULL }, { DATA_CMD_MF1_SET_WRITE_MODE, NULL, cmd_processor_mf1_set_write_mode, NULL }, + { DATA_CMD_MF1_GET_FIELD_OFF_DO_RESET, NULL, cmd_processor_mf1_get_field_off_do_reset, NULL }, + { DATA_CMD_MF1_SET_FIELD_OFF_DO_RESET, NULL, cmd_processor_mf1_set_field_off_do_reset, NULL }, { DATA_CMD_MF0_NTAG_GET_UID_MAGIC_MODE, NULL, cmd_processor_mf0_ntag_get_uid_mode, NULL }, { DATA_CMD_MF0_NTAG_SET_UID_MAGIC_MODE, NULL, cmd_processor_mf0_ntag_set_uid_mode, NULL }, diff --git a/firmware/application/src/app_main.c b/firmware/application/src/app_main.c index b781e5c..d6d3388 100644 --- a/firmware/application/src/app_main.c +++ b/firmware/application/src/app_main.c @@ -559,6 +559,12 @@ static void cycle_slot(bool dec) { } // Update status only if the new card slot switch is valid tag_emulation_change_slot(slot_new, true); // Tell the analog card module that we need to switch card slots + // Turn off the LEDs in case we were showing the battery status + rgb_marquee_stop(); + uint32_t *led_pins = hw_get_led_array(); + for (int i = 0; i < RGB_LIST_NUM; i++) { + nrf_gpio_pin_clear(led_pins[i]); + } // Go back to the color corresponding to the field enablement type apply_slot_change(slot_now, slot_new); } diff --git a/firmware/application/src/data_cmd.h b/firmware/application/src/data_cmd.h index 3aa1c82..1b47183 100644 --- a/firmware/application/src/data_cmd.h +++ b/firmware/application/src/data_cmd.h @@ -139,6 +139,8 @@ #define DATA_CMD_MF0_NTAG_GET_DETECTION_LOG (4035) #define DATA_CMD_MF0_NTAG_GET_DETECTION_ENABLE (4036) #define DATA_CMD_MF0_NTAG_GET_EMULATOR_CONFIG (4037) +#define DATA_CMD_MF1_SET_FIELD_OFF_DO_RESET (4038) +#define DATA_CMD_MF1_GET_FIELD_OFF_DO_RESET (4039) // // ****************************************************************** diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c index 043f0e4..7f208ac 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.c @@ -61,7 +61,10 @@ static volatile bool m_is_responded = false; static uint8_t m_nfc_rx_buffer[MAX_NFC_RX_BUFFER_SIZE] = { 0x00 }; static uint8_t m_nfc_tx_buffer[MAX_NFC_TX_BUFFER_SIZE] = { 0x00 }; // The N -secondary connection needs to use SAK, when the "third 'bit' in SAK is 1 is 1, the logo UID is incomplete -static uint8_t m_uid_incomplete_sak[] = { 0x04, 0xda, 0x17 }; +static uint8_t m_uid_incomplete_sak[] = { 0x04, 0xda, 0x17 }; +// Reset nfc peripheral after field lost? +static bool reset_if_field_lost = false; // default is 'false', Unless there is a genuine need for a reset. + /** * @brief Calculate BCC @@ -349,7 +352,7 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { m_tag_state_14a = NFC_TAG_STATE_14A_READY; // After receiving the WUPA or REQA instruction, we need to reply to ATQA nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false); - // NRF_LOG_INFO("ATQA reply."); + // NRF_LOG_INFO("ATQA reply: %02x%02x", auto_coll_res->atqa[0], auto_coll_res->atqa[1]); } else { m_tag_state_14a = NFC_TAG_STATE_14A_IDLE; NRF_LOG_INFO("Auto anti-collision resource no exists."); @@ -397,6 +400,15 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { m_tag_state_14a = NFC_TAG_STATE_14A_IDLE; } return; + case NFC_TAG_14A_CMD_REQA: + case NFC_TAG_14A_CMD_WUPA: + // Reader is re-sending REQA/WUPA while in READY state + // This can happen if reader retries or if frame was received incorrectly + // Respond with ATQA again and stay in READY state + if (auto_coll_res != NULL) { + nfc_tag_14a_tx_bytes(auto_coll_res->atqa, 2, false); + } + return; default: { // After receiving the wrong level instruction, directly reset the status machine NRF_LOG_INFO("[MFEMUL_SELECT] Incorrect cascade level received: %02x", p_data[0]); @@ -523,6 +535,43 @@ void nfc_tag_14a_data_process(uint8_t *p_data) { } } +// Copy from nrf_nfct.c and modified for nrf52840 adapted(no verify on nrf52832) +static inline void nrf_nfct_reset(void) { + uint32_t fdm; + uint32_t int_enabled; + + // Save parameter settings before the reset of the NFCT peripheral. + fdm = nrf_nfct_frame_delay_max_get(); + int_enabled = nrf_nfct_int_enable_get(); + + // Reset the NFCT peripheral. + *(volatile uint32_t *)0x40005FFC = 0; + *(volatile uint32_t *)0x40005FFC; + *(volatile uint32_t *)0x40005FFC = 1; + + // Restore parameter settings after the reset of the NFCT peripheral. + nrf_nfct_frame_delay_max_set(fdm); + + // Use Window Grid frame delay mode. + nrf_nfct_frame_delay_mode_set(NRF_NFCT_FRAME_DELAY_MODE_WINDOWGRID); + + /* Begin: Workaround for anomaly 25 */ + /* Workaround for wrong SENSRES values require using SDD00001, but here SDD00100 is used + because it is required to operate with Windows Phone */ + nrf_nfct_sensres_bit_frame_sdd_set(NRF_NFCT_SENSRES_BIT_FRAME_SDD_00100); + /* End: Workaround for anomaly 25 */ + + // Restore interrupts. + nrf_nfct_int_enable(int_enabled); + + // Disable interrupts associated with data exchange. + nrf_nfct_int_disable(NRF_NFCT_INT_RXFRAMESTART_MASK | + NRF_NFCT_INT_RXFRAMEEND_MASK | + NRF_NFCT_INT_RXERROR_MASK | + NRF_NFCT_INT_TXFRAMESTART_MASK | + NRF_NFCT_INT_TXFRAMEEND_MASK); +} + static inline void nfc_fdt_reset(void) { // STOP TX *(volatile uint32_t *)0x40005010 = 0x01; @@ -571,6 +620,13 @@ void nfc_tag_14a_event_callback(nrfx_nfct_evt_t const *p_event) { TAG_FIELD_LED_OFF() m_tag_state_14a = NFC_TAG_STATE_14A_IDLE; + if (reset_if_field_lost) { + // Fix a bug where certain special conditions prevent triggering TX start events and actually transmit incorrect data to the card reader. + // After more more more testing, I found that simply going into sleep mode and restarting can restore work. + // Therefore, I suspect that there may be some issues with the NFC peripheral that require a reset to resolve. + nrf_nfct_reset(); + } + NRF_LOG_INFO("HF FIELD LOST"); break; } @@ -688,3 +744,11 @@ bool is_valid_uid_size(uint8_t uid_length) { uid_length == NFC_TAG_14A_UID_DOUBLE_SIZE || uid_length == NFC_TAG_14A_UID_TRIPLE_SIZE; } + +void nfc_tag_14a_set_reset_enable(bool enable) { + reset_if_field_lost = enable; +} + +bool nfc_tag_14a_is_reset_enable() { + return reset_if_field_lost; +} diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h index c103c02..e38b250 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_14a.h +++ b/firmware/application/src/rfid/nfctag/hf/nfc_14a.h @@ -115,4 +115,8 @@ void nfc_tag_14a_tx_nbit(uint8_t data, uint32_t bits); // Determine whether it is an effective UID length bool is_valid_uid_size(uint8_t uid_length); +// Reset nfc peripheral after field lost +void nfc_tag_14a_set_reset_enable(bool enable); +bool nfc_tag_14a_is_reset_enable(); + #endif diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c index 5d40c7e..dc0a319 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf0_ntag.c @@ -675,29 +675,49 @@ static void handle_fast_read_command(uint8_t block_num, uint8_t end_block_num) { int block_max = get_block_max_by_tag_type(m_tag_type, true); - if (block_num >= end_block_num || end_block_num >= block_max) { + if (block_num > end_block_num || end_block_num >= block_max) { nfc_tag_14a_tx_nbit(NAK_INVALID_OPERATION_TBV, 4); return; } NRF_LOG_INFO("HANDLING FAST READ %02x %02x", block_num, end_block_num); - - handle_any_read(block_num, end_block_num - block_num, block_max); + // FAST_READ is inclusive: read from block_num to end_block_num (both included) + handle_any_read(block_num, end_block_num - block_num + 1, block_max); } static bool check_ro_lock_on_page(int block_num) { if (block_num < 3) return true; - else if (block_num == 3) return (m_tag_information->memory[2][2] & 9) != 0; // bits 0 and 3 - else if (block_num <= MF0ICU1_PAGES) { + else if (block_num == 3) { + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + //page 3 can be locked or not independant of BL CC bit + //the BL bit only freezes the lock bytes ! + return (m_tag_information->memory[2][2] & 8) != 0; + default: + return (m_tag_information->memory[2][2] & 9) != 0; + } + // bits 0 and 3 + } else if (block_num <= MF0ICU1_PAGES) { bool locked = false; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + // pages can be locked or not independant of BL bits + //the BL bits only freezes the lock bytes ! + uint16_t lock_bits = *(uint16_t *)&m_tag_information->memory[2][2]; + return ((lock_bits >> block_num) & 0x01) == 1; + default: + // check block locking bits + if (block_num <= 9) locked |= (m_tag_information->memory[2][2] & 2) == 2; + else locked |= (m_tag_information->memory[2][2] & 4) == 4; - // check block locking bits - if (block_num <= 9) locked |= (m_tag_information->memory[2][2] & 2) == 2; - else locked |= (m_tag_information->memory[2][2] & 4) == 4; + locked |= (((*(uint16_t *)&m_tag_information->memory[2][2]) >> block_num) & 1) == 1; - locked |= (((*(uint16_t *)&m_tag_information->memory[2][2]) >> block_num) & 1) == 1; - - return locked; + return locked; + } } else { uint8_t *p_lock_bytes = NULL; int user_memory_end = 0; @@ -776,9 +796,43 @@ static bool check_ro_lock_on_page(int block_num) { bool locked_small_range = ((lock_word >> (index / dyn_lock_bit_page_cnt)) & 1) != 0; bool locked_large_range = ((p_lock_bytes[2] >> (index / dyn_lock_bit_page_cnt / 2)) & 1) != 0; - - return locked_small_range | locked_large_range; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + // For NTAG213/215/216: byte 2 contains block-locking bits (BL) which only freeze + // the lock configuration. We only check the actual lock bits (L0-L15) in bytes 0-1. + return locked_small_range; + default: + return locked_small_range | locked_large_range; + } } else { + //Check the block locking bits to see if we can touch the dynamic locks bytes for NTAG tags + if(block_num == user_memory_end) + { + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + { + uint8_t block_bytes = m_tag_information->memory[user_memory_end][2]; + uint16_t block_world = 0; + + // Each bit in block_bytes maps to 2 bits in block_world + for (int i = 0; i < 8; i++) { + if (block_bytes & (0x01 << i)) { + block_world |= (0x0003 << (i * 2)); + } + } + + p_lock_bytes = m_tag_information->memory[user_memory_end]; + uint16_t lock_word = (((uint16_t)p_lock_bytes[1]) << 8) | (uint16_t)p_lock_bytes[0]; + return (lock_word & block_world) != 0; + } + default: + break; + } + } // check CFGLCK bit int first_cfg_page = get_first_cfg_page_by_tag_type(m_tag_type); uint8_t access = m_tag_information->memory[first_cfg_page + CONF_ACCESS_PAGE_OFFSET][CONF_ACCESS_BYTE]; @@ -793,7 +847,25 @@ static bool check_ro_lock_on_page(int block_num) { static int handle_write_command(uint8_t block_num, uint8_t *p_data) { int block_max = get_block_max_by_tag_type(m_tag_type, false); - if (block_num >= block_max) { + bool out_of_bounds = false; + switch (m_tag_type) { + case TAG_TYPE_NTAG_213: + case TAG_TYPE_NTAG_215: + case TAG_TYPE_NTAG_216: + int first_cfg_page = get_first_cfg_page_by_tag_type(m_tag_type); + uint8_t cfglck = m_tag_information->memory[first_cfg_page][0] & 0x40; + // For NTAG cards we need to check CFGLCK bit for config pages + bool is_config_page = (block_num >= first_cfg_page) && (block_num <= first_cfg_page + 1); + bool config_locked = (cfglck != 0) && (!m_tag_information->config.mode_uid_magic); + bool is_beyond_user_memory = (block_num >= block_max); + out_of_bounds = (is_beyond_user_memory && !is_config_page) || (config_locked && is_config_page); + break; + default: + out_of_bounds = block_num >= block_max; + break; + } + // Reject out-of-bounds writes (except config pages) + if (out_of_bounds) { NRF_LOG_ERROR("Write failed: block_num %08x >= block_max %08x", block_num, block_max); return NAK_INVALID_OPERATION_TBV; } diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf1.c b/firmware/application/src/rfid/nfctag/hf/nfc_mf1.c index 15d27c5..ade17bd 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf1.c +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf1.c @@ -1111,6 +1111,8 @@ int nfc_tag_mf1_data_loadcb(tag_specific_type_t type, tag_data_buffer_t *buffer) .cb_reset = nfc_tag_mf1_reset_handler, }; nfc_tag_14a_set_handler(&handler_for_14a); + NRF_LOG_INFO("HF mf1 config 'field_off_do_reset' = %d", m_tag_information->config.field_off_do_reset); + nfc_tag_14a_set_reset_enable(m_tag_information->config.field_off_do_reset); NRF_LOG_INFO("HF mf1 data load finish."); } else { NRF_LOG_ERROR("nfc_tag_mf1_information_t too big."); @@ -1157,6 +1159,12 @@ bool nfc_tag_mf1_data_factory(uint8_t slot, tag_specific_type_t tag_type) { p_mf1_information->config.use_mf1_coll_res = false; p_mf1_information->config.mode_block_write = NFC_TAG_MF1_WRITE_NORMAL; p_mf1_information->config.detection_enable = false; + p_mf1_information->config.field_off_do_reset = false; + + // zero for reserved byte + p_mf1_information->config.reserved1 = 0x00; + p_mf1_information->config.reserved2 = 0x00; + p_mf1_information->config.reserved3 = 0x00; // save data to flash tag_sense_type_t sense_type = get_sense_type_from_tag_type(tag_type); @@ -1236,3 +1244,10 @@ nfc_tag_mf1_write_mode_t nfc_tag_mf1_get_write_mode(void) { return m_tag_information->config.mode_block_write; } +void nfc_tag_mf1_set_field_off_do_reset(bool enable) { + m_tag_information->config.field_off_do_reset = enable; +} + +bool nfc_tag_mf1_is_field_off_do_reset(void) { + return m_tag_information->config.field_off_do_reset; +} diff --git a/firmware/application/src/rfid/nfctag/hf/nfc_mf1.h b/firmware/application/src/rfid/nfctag/hf/nfc_mf1.h index 3521714..eff32ab 100644 --- a/firmware/application/src/rfid/nfctag/hf/nfc_mf1.h +++ b/firmware/application/src/rfid/nfctag/hf/nfc_mf1.h @@ -71,8 +71,15 @@ typedef struct { uint8_t detection_enable: 1; // Allow to write block 0 (CUID/gen2 mode) uint8_t mode_gen2_magic: 1; - // reserve - uint8_t reserved1: 4; + /** + * Should the NFC peripheral be reset after losing the RF field? + * This configuration can fix the issue where some card readers cause the CU to enter a strange state of no response/incorrect response. + * Once in this state, the device must be restarted to resolve the issue. + * Alternatively, enabling this configuration for resetting the NFC after leaving the rf field can also solve the aforementioned problem. + */ + uint8_t field_off_do_reset: 1; + // reserved + uint8_t reserved1: 3; uint8_t reserved2; uint8_t reserved3; } nfc_tag_mf1_configure_t; @@ -157,6 +164,7 @@ void nfc_tag_mf1_set_use_mf1_coll_res(bool enable); bool nfc_tag_mf1_is_use_mf1_coll_res(void); void nfc_tag_mf1_set_write_mode(nfc_tag_mf1_write_mode_t write_mode); nfc_tag_mf1_write_mode_t nfc_tag_mf1_get_write_mode(void); - +void nfc_tag_mf1_set_field_off_do_reset(bool enable); +bool nfc_tag_mf1_is_field_off_do_reset(void); #endif diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.c b/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.c index cb8579c..e33bad7 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.c +++ b/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.c @@ -18,6 +18,7 @@ #define PREAMBLE_34BIT (0x009) #define PREAMBLE_35BIT (0x005) #define PREAMBLE_36BIT (0x003) +#define PREAMBLE_ACTP (0x095) /**@brief Set a bit in the uint64 word. * @@ -548,6 +549,37 @@ static wiegand_card_t *unpack_c15001(uint64_t hi, uint64_t lo) { return d; } +static uint64_t pack_actprox(wiegand_card_t *card) { + if (card->oem == 0) { + card->oem = 900; + } + uint64_t bits = PREAMBLE_ACTP; + bits <<= 1; + bits = (bits << 10) | (card->oem & 0x3ff); + bits = (bits << 8) | (card->facility_code & 0xff); + bits = (bits << 16) | (card->card_number & 0xffff); + bits <<= 1; + if (evenparity32((bits >> 18) & 0x1ffff)) { + SET_BIT64(bits, 35); + } + if (oddparity32((bits >> 1) & 0x1ffff)) { + SET_BIT64(bits, 0); + } + return bits; +} + +static wiegand_card_t *unpack_actprox(uint64_t hi, uint64_t lo) { + if (!(IS_SET(lo, 35) == evenparity32((lo >> 18) & 0x1ffff) && + IS_SET(lo, 0) == oddparity32((lo >> 1) & 0x1ffff))) { + return NULL; + } + wiegand_card_t *d = wiegand_card_alloc(); + d->oem = (lo >> 25) & 0x3ff; + d->facility_code = (lo >> 17) & 0xff; + d->card_number = (lo >> 1) & 0xffff; + return d; +} + static uint64_t pack_s12906(wiegand_card_t *card) { uint64_t bits = PREAMBLE_36BIT; bits <<= 1; @@ -819,6 +851,7 @@ static const card_format_table_t formats[] = { {P10004, pack_p10004, unpack_p10004, 37, {0, 0x1FFF, 0x3FFFF, 0, 0}}, // HID P10004 37-bit PCSC {HGEN37, pack_hgeneric37, unpack_hgeneric37, 37, {1, 0, 0xFFFFFFFF, 0, 0}}, // HID Generic 37-bit {MDI37, pack_mdi37, unpack_mdi37, 37, {1, 0xF, 0x1FFFFFFF, 0, 0}}, // PointGuard MDI 37-bit + {ACTPHID, pack_actprox, unpack_actprox, 36, {1, 0xFF, 0xFFFF, 0x3FF, 0}}, // HID ACTProx 36-bit }; uint64_t pack(wiegand_card_t *card) { diff --git a/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.h b/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.h index 8f898cf..43f848e 100644 --- a/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.h +++ b/firmware/application/src/rfid/nfctag/lf/protocols/wiegand.h @@ -72,6 +72,7 @@ typedef enum { C1K48S, AVIG56, IR56, + ACTPHID, } card_format_t; // Structure for defined Wiegand card formats available for packing/unpacking diff --git a/firmware/application/src/usb_main.c b/firmware/application/src/usb_main.c index 3cc0d8f..d0525b3 100644 --- a/firmware/application/src/usb_main.c +++ b/firmware/application/src/usb_main.c @@ -40,19 +40,19 @@ APP_USBD_CDC_ACM_GLOBAL_DEF(m_app_cdc_acm, volatile bool g_usb_connected = false; volatile bool g_usb_port_opened = false; volatile bool g_usb_led_marquee_enable = true; +static uint8_t cdc_data_buffer[NRF_DRV_USBD_EPSIZE]; /** @brief User event handler @ref app_usbd_cdc_acm_user_ev_handler_t */ static void cdc_acm_user_ev_handler(app_usbd_class_inst_t const *p_inst, app_usbd_cdc_acm_user_event_t event) { - static uint8_t cdc_data_buffer[1]; + // app_usbd_cdc_acm_t const *p_cdc_acm = app_usbd_cdc_acm_class_get(p_inst); switch (event) { case APP_USBD_CDC_ACM_USER_EVT_PORT_OPEN: { - /* - *theProbabilityOfTheEntireUsbReceivingDataIsTheAppUsbdCdcAcmRead *AppUsbdCdcAcmReadFunctionIsNotASeriousReception,ItIsGivenAPointer,AndThenWaitForTheUsbBuffer *SoYouNeedToInitializeTheHeadPointerFirstWhenTheAppUsbdCdcAcmUserEvtPortOpenIsInitialized *IfTheAppUsbdCdcAcmUserEvtRxDoneUsesASubscribed0ToAccessTheBuffer,ItWillCauseTheFirstByteToLoseTheFirstSendEssence - */ - ret_code_t ret = app_usbd_cdc_acm_read(&m_app_cdc_acm, cdc_data_buffer, 1); + // Setup first transfer + ret_code_t ret = app_usbd_cdc_acm_read_any(&m_app_cdc_acm, cdc_data_buffer, sizeof(cdc_data_buffer)); UNUSED_VARIABLE(ret); + NRF_LOG_INFO("CDC ACM port opened"); g_usb_port_opened = true; break; @@ -68,16 +68,13 @@ static void cdc_acm_user_ev_handler(app_usbd_class_inst_t const *p_inst, app_usb break; case APP_USBD_CDC_ACM_USER_EVT_RX_DONE: { - ret_code_t ret; - //Take out the first byte first - data_frame_receive(cdc_data_buffer, 1); - do { - ret = app_usbd_cdc_acm_read(&m_app_cdc_acm, cdc_data_buffer, 1); - if (ret == NRF_SUCCESS) { - // The byte after success - data_frame_receive(cdc_data_buffer, 1); - } - } while (ret == NRF_SUCCESS); + // Get amount of data transfered to process data + size_t size = app_usbd_cdc_acm_rx_size(&m_app_cdc_acm); + data_frame_receive(cdc_data_buffer, size); + + // Setup next transfer + ret_code_t ret = app_usbd_cdc_acm_read_any(&m_app_cdc_acm, cdc_data_buffer, sizeof(cdc_data_buffer)); + UNUSED_VARIABLE(ret); break; } default: diff --git a/firmware/application/src/utils/dataframe.c b/firmware/application/src/utils/dataframe.c index a2baad0..9874f5b 100644 --- a/firmware/application/src/utils/dataframe.c +++ b/firmware/application/src/utils/dataframe.c @@ -28,6 +28,11 @@ static uint8_t compute_lrc(uint8_t *buf, uint16_t bufsize) { return 0x100 - lrc; } +// +// !!!!!!!!!!!!!!!!! NRF_LOG_HEXDUMP_INFO() printing long data can cause freezing and needs to be fixed. !!!!!!!!!!!!!!!!! +// FIXME. +// + /** * @brief: create a packet, put the created data packet into the buffer, and wait for the post to set up a non busy state * @param cmd: instructionResponse @@ -44,10 +49,11 @@ data_frame_tx_t *data_frame_make(uint16_t cmd, uint16_t status, uint16_t data_le NRF_LOG_ERROR("data_frame_make error, too much data."); return NULL; } - NRF_LOG_INFO("TX Data frame: cmd = 0x%04x (%i), status = 0x%04x, length = %d%s", cmd, cmd, status, data_length, data_length > 0 ? ", data =" : ""); - if (data_length > 0) { - NRF_LOG_HEXDUMP_INFO(data, data_length); - } + + // NRF_LOG_INFO("TX Data frame: cmd = 0x%04x (%i), status = 0x%04x, length = %d%s", cmd, cmd, status, data_length, data_length > 0 ? ", data =" : ""); + // if (data_length > 0) { + // NRF_LOG_HEXDUMP_INFO(data, data_length); + // } netdata_frame_postamble_t *tx_post = (netdata_frame_postamble_t *)((uint8_t *)&m_netdata_frame_tx_buf + sizeof(netdata_frame_preamble_t) + data_length); // sof @@ -92,7 +98,7 @@ void data_frame_receive(uint8_t *data, uint16_t length) { return; } // buffer overflow - if (m_data_rx_position + length >= sizeof(m_netdata_frame_rx_buf)) { + if (m_data_rx_position + length > sizeof(m_netdata_frame_rx_buf)) { NRF_LOG_ERROR("Data frame wait overflow."); data_frame_reset(); return; @@ -142,10 +148,10 @@ void data_frame_receive(uint8_t *data, uint16_t length) { // and we are receive completed m_data_buffer = m_data_len > 0 ? (uint8_t *)&m_netdata_frame_rx_buf.data : NULL; m_data_completed = true; - NRF_LOG_INFO("RX Data frame: cmd = 0x%04x (%i), status = 0x%04x, length = %d%s", m_data_cmd, m_data_cmd, m_data_status, m_data_len, m_data_len > 0 ? ", data =" : ""); - if (m_data_len > 0) { - NRF_LOG_HEXDUMP_INFO(m_data_buffer, m_data_len); - } + // NRF_LOG_INFO("RX Data frame: cmd = 0x%04x (%i), status = 0x%04x, length = %d%s", m_data_cmd, m_data_cmd, m_data_status, m_data_len, m_data_len > 0 ? ", data =" : ""); + // if (m_data_len > 0) { + // NRF_LOG_HEXDUMP_INFO(m_data_buffer, m_data_len); + // } } else { // data frame lrc error NRF_LOG_ERROR("Data frame finally lrc error."); diff --git a/firmware/bootloader/Makefile b/firmware/bootloader/Makefile index 52c0096..afca5ce 100644 --- a/firmware/bootloader/Makefile +++ b/firmware/bootloader/Makefile @@ -259,7 +259,8 @@ include $(TEMPLATE_PATH)/Makefile.common # tolerate warnings in newer gcc versions # need to be called after $(TEMPLATE_PATH)/Makefile.common -CC_VERSION = $(shell $(CC) -dumpversion 2>/dev/null|sed 's/\..*//') +# The return value of the Windows+msys2 build platform has carriage returns and line breaks, which need to be removed. +CC_VERSION = $(shell $(CC) -dumpversion 2>/dev/null | tr -d '\r' | cut -d. -f1) CC_VERSION := $(or $(strip $(CC_VERSION)),0) ifeq ($(shell expr $(CC_VERSION) \>= 12), 1) # avoid a couple of false warnings in nRF SDK diff --git a/firmware/nrf52_sdk/modules/nrfx/drivers/src/nrfx_nfct.c b/firmware/nrf52_sdk/modules/nrfx/drivers/src/nrfx_nfct.c index 9c30476..2435612 100644 --- a/firmware/nrf52_sdk/modules/nrfx/drivers/src/nrfx_nfct.c +++ b/firmware/nrf52_sdk/modules/nrfx/drivers/src/nrfx_nfct.c @@ -818,29 +818,9 @@ void nrfx_nfct_irq_handler(void) NRFX_NFCT_CB_HANDLE(m_nfct_cb.config.cb, nfct_evt); - /* Clear TXFRAMESTART EVENT so it can be checked in hal_nfc_send */ - nrf_nfct_event_clear(NRF_NFCT_EVENT_TXFRAMESTART); - NRFX_LOG_DEBUG("Rx fend"); } - if (NRFX_NFCT_EVT_ACTIVE(TXFRAMEEND)) - { - nrf_nfct_event_clear(NRF_NFCT_EVENT_TXFRAMEEND); - - nrfx_nfct_evt_t nfct_evt = - { - .evt_id = NRFX_NFCT_EVT_TX_FRAMEEND - }; - - /* Disable TX END event to ignore frame transmission other than READ response */ - nrf_nfct_int_disable(NRFX_NFCT_TX_INT_MASK); - - NRFX_NFCT_CB_HANDLE(m_nfct_cb.config.cb, nfct_evt); - - NRFX_LOG_DEBUG("Tx fend"); - } - if (NRFX_NFCT_EVT_ACTIVE(SELECTED)) { nrf_nfct_event_clear(NRF_NFCT_EVENT_SELECTED); @@ -913,6 +893,23 @@ void nrfx_nfct_irq_handler(void) m_nfct_cb.config.cb(&nfct_evt); } } + + if (NRFX_NFCT_EVT_ACTIVE(TXFRAMEEND)) + { + nrf_nfct_event_clear(NRF_NFCT_EVENT_TXFRAMEEND); + + nrfx_nfct_evt_t nfct_evt = + { + .evt_id = NRFX_NFCT_EVT_TX_FRAMEEND + }; + + /* Disable TX END event to ignore frame transmission other than READ response */ + nrf_nfct_int_disable(NRFX_NFCT_TX_INT_MASK); + + NRFX_NFCT_CB_HANDLE(m_nfct_cb.config.cb, nfct_evt); + + NRFX_LOG_DEBUG("Tx fend"); + } } #endif // NRFX_CHECK(NRFX_NFCT_ENABLED) diff --git a/software/script/chameleon_cli_unit.py b/software/script/chameleon_cli_unit.py index fbc5b6d..2512896 100644 --- a/software/script/chameleon_cli_unit.py +++ b/software/script/chameleon_cli_unit.py @@ -12,6 +12,7 @@ import time import serial.tools.list_ports import threading import struct +import queue from multiprocessing import Pool, cpu_count from typing import Union from pathlib import Path @@ -198,7 +199,7 @@ class DeviceRequiredUnit(BaseCLIUnit): if ret: return True else: - print("Please connect to chameleon device first(use 'hw connect').") + print("Please connect to chameleon device first (use 'hw connect').") return False @@ -452,6 +453,7 @@ class LFHIDIdArgsUnit(DeviceRequiredUnit): HIDFormat.C1K35S: [0xFFF, 0xFFFFF, 0, 0], HIDFormat.C15001: [0xFF, 0xFFFF, 0, 0x3FF], HIDFormat.S12906: [0xFF, 0xFFFFFF, 0x3, 0], + HIDFormat.ACTPHID: [0xFF, 0xFFFFFF, 0, 0x3FF], HIDFormat.SIE36: [0x3FFFF, 0xFFFF, 0, 0], HIDFormat.H10320: [0, 99999999, 0, 0], HIDFormat.H10302: [0, 0x7FFFFFFFF, 0, 0], @@ -926,7 +928,7 @@ class HFMFNested(ReaderRequiredUnit): if block_known == block_target and type_known == type_target: print(color_string((CR, "Target key already known"))) return - print(f" - Nested recover one key running...") + print(" - Nested recover one key running...") key = self.recover_a_key(block_known, type_known, key_known_bytes, block_target, type_target) if key is None: print(color_string((CY, "No key found, you can retry."))) @@ -2153,6 +2155,9 @@ class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredU log_group = parser.add_mutually_exclusive_group() log_group.add_argument('--enable-log', action='store_true', help="Enable logging of MFC authentication data") log_group.add_argument('--disable-log', action='store_true', help="Disable logging of MFC authentication data") + field_off_reset_group = parser.add_mutually_exclusive_group() + field_off_reset_group.add_argument('--enable_field_off_do_reset', action='store_true', help="Enable FIELD_OFF_DO_RESET") + field_off_reset_group.add_argument('--disable_field_off_do_reset', action='store_true', help="Disable FIELD_OFF_DO_RESET") return parser def on_exec(self, args: argparse.Namespace): @@ -2183,6 +2188,8 @@ class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredU write_mode = MifareClassicWriteMode(mfc_config["write_mode"]) detection = mfc_config["detection"] change_requested, change_done, uid, atqa, sak, ats = self.update_hf14a_anticoll(args, uid, atqa, sak, ats) + field_off_do_reset = self.cmd.mf1_get_field_off_do_reset() + if args.enable_gen1a: change_requested = True if not gen1a_mode: @@ -2256,6 +2263,22 @@ class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredU change_done = True else: print(f'{color_string((CY, "Requested logging of MFC authentication data already disabled"))}') + if args.enable_set_field_off_do_reset: + change_requested = True + if not field_off_do_reset: + field_off_do_reset = True + self.cmd.mf1_set_field_off_do_reset(field_off_do_reset) + change_done = True + else: + print(f'{color_string((CY, "Requested FIELD_OFF_DO_RESET already enabled"))}') + elif args.disable_set_field_off_do_reset: + change_requested = True + if field_off_do_reset: + field_off_do_reset = False + self.cmd.mf1_set_field_off_do_reset(field_off_do_reset) + change_done = True + else: + print(f'{color_string((CY, "Requested FIELD_OFF_DO_RESET already disabled"))}') if change_done: print(' - MF1 Emulator settings updated') @@ -2282,6 +2305,8 @@ class HFMFEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredU print(f'- {"Write mode:":40}{color_string((CR, "invalid value!"))}') print( f'- {"Log (mfkey32) mode:":40}{f"{enabled_str}" if detection else f"{disabled_str}"}') + print( + f'- {"FIELD_OFF_DO_RESET:":40}{f"{enabled_str}" if field_off_do_reset else f"{disabled_str}"}') @hf_mfu.command('ercnt') @@ -2365,7 +2390,7 @@ class HFMFURDPG(MFUAuthArgsUnit): else: try: self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x30, args.page)) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # we may lose the tag again here pass print(color_string((CR, " - Auth failed"))) @@ -2430,7 +2455,7 @@ class HFMFUWRPG(MFUAuthArgsUnit): # send a command just to disable the field. use read to avoid corrupting the data try: self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x30, args.page)) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # we may lose the tag again here pass print(color_string((CR, " - Auth failed"))) @@ -2569,7 +2594,7 @@ class HFMFUESAVE(DeviceRequiredUnit): version = self.cmd.mf0_ntag_get_version_data() fd.write(f"# Version: {version.hex()}\n") - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): pass # slot does not have version data try: @@ -2577,7 +2602,7 @@ class HFMFUESAVE(DeviceRequiredUnit): if signature != b"\x00" * 32: fd.write(f"# Signature: {signature.hex()}\n") - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): pass # slot does not have signature data page = 0 @@ -2640,7 +2665,7 @@ class HFMFURCNT(MFUAuthArgsUnit): else: try: self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x39, args.counter)) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # we may lose the tag again here pass print(color_string((CR, " - Auth failed"))) @@ -2697,14 +2722,14 @@ class HFMFUDUMP(MFUAuthArgsUnit): version = self.cmd.hf14a_raw(options=options, resp_timeout_ms=100, data=struct.pack('!B', 0x60)) if len(version) == 0: version = None - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): version = None # try sending AUTHENTICATE command and observe the result try: supports_auth = len(self.cmd.hf14a_raw( options=options, resp_timeout_ms=100, data=struct.pack('!B', 0x1A))) != 0 - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): supports_auth = False if version is not None and not supports_auth: @@ -2743,7 +2768,7 @@ class HFMFUDUMP(MFUAuthArgsUnit): print(color_string((CY, "Tag is likely NTAG 20x, reading until first error."))) stop_page = 256 - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # Regular Ultralight tag_name = 'Mifare Ultralight' stop_page = 16 @@ -2793,7 +2818,7 @@ class HFMFUDUMP(MFUAuthArgsUnit): try: resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x30, i)) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # probably lost tag, but we still need to disable rf field resp = None @@ -2893,6 +2918,457 @@ class HFMFUSIGNATURE(ReaderRequiredUnit): print(f" - Data: {resp[:32].hex()}") +@hf_mfu.command('authnonce') +class HFMFUAUTHNONCE(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Get authentication nonce from MIFARE Ultralight C tag.' + return parser + + def on_exec(self, args: argparse.Namespace): + options = { + 'activate_rf_field': 0, + 'wait_response': 1, + 'append_crc': 1, + 'auto_select': 1, + 'keep_rf_field': 0, + 'check_response_crc': 1, + } + + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=struct.pack('!BB', 0x1A, 0x00)) + # Response is 0xAF + 8 bytes nonce + 2 bytes CRC = 11 bytes + # We want to display just the 8-byte nonce (skip 0xAF prefix) + if len(resp) >= 9 and resp[0] == 0xAF: + print(f" - Nonce: {resp[1:9].hex()}") + else: + print(f" - Error: Unexpected response: {resp.hex()}") + + +class CrackEffect: + """ + A class to create a visual effect of cracking blocks of data. + """ + + def __init__(self, num_blocks: int = 4, block_size: int = 8, scramble_delay: float = 0.01): + """ + Initialize the CrackEffect class with the given parameters. + + Args: + num_blocks (int): Number of blocks to display. Default is 4. + block_size (int): Size of each block in characters. Default is 8. + scramble_delay (float): Delay between each scramble update in seconds. Default is 0.01. + """ + self.num_blocks = num_blocks + self.block_size = block_size + self.scramble_delay = scramble_delay + self.message_queue = queue.Queue() + self.revealed = [''] * num_blocks + self.stop_event = threading.Event() + self.cracked_blocks = set() + self.display_lock = threading.Lock() + self.output_enabled = True + + def generate_random_hex(self) -> str: + """Generate a random hex string of block_size length.""" + import random + hex_chars = '0123456789ABCDEF' + return ''.join(random.choice(hex_chars) for _ in range(self.block_size)) + + def format_block(self, block: str, is_cracked: bool) -> str: + """Format a block with appropriate color based on its state.""" + if is_cracked: + return f"\033[1;34m{block}\033[0m" # Bold blue + return f"\033[96m{block}\033[0m" # Bright cyan + + def draw_static_box(self): + """Draw the initial static box.""" + if not self.output_enabled: + return + width = (self.block_size + 1) * self.num_blocks + 4 + print("") # Add some padding above + print("╔" + "═" * width + "╗") + print("║" + " " * width + "║") + print("║" + " " * width + "║") + print("║" + " " * width + "║") + print("╚" + "═" * width + "╝") + # Move cursor to the middle line + sys.stdout.write("\033[3A") # Move up 3 lines to middle row + sys.stdout.flush() + + def print_above(self, data): + """Print the given data above the box and redraws the box.""" + if not self.output_enabled: + print(data) + return + with self.display_lock: + # Move cursor above the box and clean the line + sys.stdout.write("\033[2A\033[1G\033[K" + data) + self.draw_static_box() + + def display_current_state(self): + """Display the current state of all blocks.""" + if not self.output_enabled: + return + with self.display_lock: + formatted_blocks = [ + self.format_block(block, i in self.cracked_blocks) + for i, block in enumerate(self.revealed) + ] + display_text = ' '.join(formatted_blocks) + + # Update only the middle line + sys.stdout.write(f"\r║ {display_text} ║") + sys.stdout.flush() + + def scramble_effect(self): + """Run the main loop for the scrambling effect.""" + if not self.output_enabled: + return + while not self.stop_event.is_set(): + # Update all non-cracked blocks with random values + for block in range(self.num_blocks): + if block not in self.cracked_blocks: + self.revealed[block] = self.generate_random_hex() + + self.display_current_state() + time.sleep(self.scramble_delay) + + def erase_key(self): + """Erase random parts of the key.""" + if not self.output_enabled: + return + for block in range(self.num_blocks): + if block not in self.cracked_blocks: + self.revealed[block] = '.' * self.block_size + self.display_current_state() + + def process_message_queue(self): + """Process incoming cracked blocks from the queue.""" + if not self.output_enabled: + return + while not self.stop_event.is_set(): + try: + block_idx, cracked_text = self.message_queue.get(timeout=0.1) + self.revealed[block_idx] = cracked_text + self.cracked_blocks.add(block_idx) + self.display_current_state() + + # Check if all blocks are cracked + if len(self.cracked_blocks) == self.num_blocks: + self.stop_event.set() + print("\n" * 3) # Add newlines after completion + break + except queue.Empty: + continue + except Exception as e: + print(f"\nError processing message: {e}") + break + + def add_cracked_block(self, block_idx: int, text: str): + """Add a cracked block to the message queue.""" + if not 0 <= block_idx < self.num_blocks: + raise ValueError(f"Block index {block_idx} out of range") + if len(text) != self.block_size: + raise ValueError(f"Block text must be {self.block_size} characters") + self.message_queue.put((block_idx, text)) + + def start(self): + """Start the cracking effect.""" + self.draw_static_box() + + # Create and start the worker threads + scramble_thread = threading.Thread(target=self.scramble_effect) + process_thread = threading.Thread(target=self.process_message_queue) + + scramble_thread.daemon = True + process_thread.daemon = True + + scramble_thread.start() + process_thread.start() + + # Wait for both threads to complete + process_thread.join() + self.stop_event.set() + scramble_thread.join() + + +@hf_mfu.command('ulcg') +class HFMFUULCG(ReaderRequiredUnit): + def args_parser(self) -> ArgumentParserNoExit: + parser = ArgumentParserNoExit() + parser.description = 'Key recovery for Giantec ULCG and USCUID-UL cards (won\'t work on NXP cards!)' + parser.add_argument('-c', '--challenges', type=int, default=1000, + help='Number of challenges to collect (default: 1000)') + parser.add_argument('-t', '--threads', type=int, default=1, + help='Number of threads for key recovery (default: 1)') + parser.add_argument('-j', '--json', type=str, + help='Path to JSON file to load or save challenges') + parser.add_argument('-o', '--offline', action='store_true', + help='Use offline mode with pre-collected challenges') + return parser + + def on_exec(self, args: argparse.Namespace): + import json + + if not args.offline: + challenges = self.collect_challenges(args.challenges) + if challenges is None: + return + if args.json: + with open(args.json, "w") as f: + json.dump(challenges, f) + print(f"[+] Challenges saved to {args.json}.") + print("[!] Beware that the card key is now erased!") + return + else: + if not args.json: + print("[-] Error: --json required for offline mode") + return + with open(args.json, "r") as f: + challenges = json.load(f) + + self.crack_key(challenges, args.threads, args.offline) + + def collect_challenges(self, num_challenges): + """Collect challenges from the card and check if it is vulnerable.""" + # Sanity check: make sure an Ultralight C is detected + resp = self.cmd.hf14a_scan() + if resp is None or len(resp) == 0: + print("[-] Error: No tag detected") + return None + + # Check SAK for Ultralight C (SAK should be 0x00) + print("[+] Checking for Ultralight C...") + + # Check AUTH0 configuration + options = { + 'activate_rf_field': 0, + 'wait_response': 1, + 'append_crc': 1, + 'auto_select': 1, + 'keep_rf_field': 0, + 'check_response_crc': 1, + } + + # Read page 40-43 (config pages) + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x30, 0x28)) # READ page 40 + + if len(resp) < 16: + print("[-] Error: Card not unlocked. Run relay attack in UNLOCK mode first.") + return None + + # Check AUTH0 (should be >= 0x30) + minimum_auth_page = resp[8] + if minimum_auth_page < 48: + print("[-] Error: Card not unlocked. Run relay attack in UNLOCK mode first.") + return None + + # Check lock bit + is_locked_key = ((resp[1] & 0x80) >> 7) == 1 + if is_locked_key: + print("[-] Error: Card is not vulnerable (key is locked)") + return None + + print("[+] All sanity checks \033[1;32mpassed\033[0m. Checking if card is vulnerable.\033[?25l") + + # Collect 100 challenges to check for collision + challenges_collected = 0 + challenges_100 = set() + challenges = {} + collision = False + + while challenges_collected < num_challenges: + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x1A, 0x00)) + if len(resp) >= 9 and resp[0] == 0xAF: + hex_challenge = resp[1:9].hex().upper() + if hex_challenge in challenges_100: + collision = True + challenges["challenge_100"] = hex_challenge + break + else: + challenges_100.add(hex_challenge) + challenges_collected += 1 + + print(f"\r[+] Challenges collected: \033[96m{challenges_collected}\033[0m") + if collision: + print("[+] Status: \033[1;31mVulnerable\033[0m\033[?25h") + else: + print("[+] Status: \033[1;32mNot vulnerable\033[0m\033[?25h") + return None + + # Card is vulnerable, proceed with attack + print("[+] Collecting key-specific challenges...") + + # Overwrite block 47 and collect challenge_75 + self.write_block(47, b'\x00\x00\x00\x00') + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x1A, 0x00)) + if len(resp) >= 9 and resp[0] == 0xAF: + challenges["challenge_75"] = resp[1:9].hex().upper() + print("[+] 75 collection complete") + + # Overwrite block 46 and collect challenge_50 + self.write_block(46, b'\x00\x00\x00\x00') + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x1A, 0x00)) + if len(resp) >= 9 and resp[0] == 0xAF: + challenges["challenge_50"] = resp[1:9].hex().upper() + print("[+] 50 collection complete") + + # Overwrite block 45 and collect challenge_25 + self.write_block(45, b'\x00\x00\x00\x00') + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x1A, 0x00)) + if len(resp) >= 9 and resp[0] == 0xAF: + challenges["challenge_25"] = resp[1:9].hex().upper() + print("[+] 25 collection complete") + + # Overwrite block 44 and collect challenge_0 + self.write_block(44, b'\x00\x00\x00\x00') + resp = self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, + data=struct.pack('!BB', 0x1A, 0x00)) + if len(resp) >= 9 and resp[0] == 0xAF: + challenges["challenge_0"] = resp[1:9].hex().upper() + print("[+] 0 collection complete") + + return challenges + + def write_block(self, block, data): + """Write a block using hf14a_raw""" + options = { + 'activate_rf_field': 0, + 'wait_response': 1, + 'append_crc': 1, + 'auto_select': 1, + 'keep_rf_field': 0, + 'check_response_crc': 1, + } + # WRITE command (0xA2) + block number + 4 bytes of data + cmd_data = struct.pack('!BB4s', 0xA2, block, data) + self.cmd.hf14a_raw(options=options, resp_timeout_ms=200, data=cmd_data) + + def crack_key(self, challenges, num_threads, offline): + """Crack the key using collected challenges""" + import signal + import traceback + + key_segment_values = {0: "00"*4, 1: "00"*4, 2: "00"*4, 3: "00"*4} + key_found = False + + print("[+] Cracking in progress...\033[?25l") + + # Create and start the cracking effect + crack_effect = CrackEffect() + effect_thread = threading.Thread(target=crack_effect.start) + effect_thread.start() + + def signal_handler(sig, frame): + print("\n\n\n[!] Interrupt received, stopping...\033[?25h") + crack_effect.stop_event.set() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + ciphertexts = {1: challenges["challenge_25"], + 0: challenges["challenge_50"], + 3: challenges["challenge_75"], + 2: challenges["challenge_100"]} + + try: + for key_segment_idx in [1, 0, 3, 2]: + ciphertext = ciphertexts[key_segment_idx] + + cmd = [ + str(default_cwd / "mfulc_des_brute"), + "-c", + challenges['challenge_0'], + ciphertext, + "".join(key_segment_values.values()), + str(key_segment_idx + 1), + str(num_threads) + ] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=3600) + + if "Could not detect LFSR" in result.stderr: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print(f"\n\n\n[-] Error: {result.stderr}\033[?25h") + break + + if "No matching key was found" in result.stdout: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print(f"\n\n\n[-] Error: No matching key found for segment {key_segment_idx + 1}\033[?25h") + break + + if "Full key (hex): " not in result.stdout: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print("\n\n\n[-] Error: Unexpected output from mfulc_des_brute\033[?25h") + break + + # Extract the key segment from output + full_key_line = [line for line in result.stdout.split('\n') if "Full key (hex):" in line][0] + full_key = full_key_line.split("Full key (hex): ")[1].strip() + key_segment_values[key_segment_idx] = full_key[(8*key_segment_idx):][:8] + key_found = True + crack_effect.add_cracked_block(key_segment_idx, key_segment_values[key_segment_idx]) + + except subprocess.TimeoutExpired: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print(f"\n\n\n[-] Error: Timeout cracking segment {key_segment_idx + 1}\033[?25h") + break + except Exception as e: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print(f"\n\n\n[-] Error: {e}\033[?25h") + break + except Exception as e: + crack_effect.stop_event.set() + print(f"\n\n\nAn error occurred: {e}\033[?25h") + traceback.print_exc() + finally: + effect_thread.join() + + if key_found: + result_key = "".join(key_segment_values.values()) + formatted_key = f"\033[1;34m{result_key}\033[0m" + print(f"[+] Found key: {formatted_key}\033[?25h") + if offline: + print("You can restore found key on the card with appropriate write commands") + else: + # Restore the key on the card + print("[+] Restoring key to card...") + key_bytes = bytes.fromhex(result_key) + + # Need to swap endianness in 8-byte chunks before writing + # UL-C stores key with swapped endianness + key_swapped = bytearray(16) + # Swap first 8 bytes + for i in range(8): + key_swapped[i] = key_bytes[7 - i] + # Swap second 8 bytes + for i in range(8): + key_swapped[8 + i] = key_bytes[15 - i] + + # Write 4 blocks of 4 bytes each + for i in range(4): + block = 44 + i + data = bytes(key_swapped[i*4:(i+1)*4]) + self.write_block(block, data) + print("[+] Key restored on the card") + + @hf_mfu.command('econfig') class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequiredUnit): def args_parser(self) -> ArgumentParserNoExit: @@ -2937,7 +3413,7 @@ class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequired try: self.cmd.mf0_ntag_set_version_data(args.set_version) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): print(color_string((CR, "Tag type does not support GET_VERSION command."))) return @@ -2951,7 +3427,7 @@ class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequired try: self.cmd.mf0_ntag_set_signature_data(args.set_signature) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): print(color_string((CR, "Tag type does not support READ_SIG command."))) return @@ -3013,7 +3489,7 @@ class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequired write_mode = new_write_mode else: print(color_string((CY, "Requested write mode already set"))) - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): print(color_string((CR, "Failed to set write mode. Check if device firmware supports this feature."))) detection = self.cmd.mf0_ntag_get_detection_enable() @@ -3059,7 +3535,7 @@ class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequired try: write_mode = MifareUltralightWriteMode(self.cmd.mf0_ntag_get_write_mode()) print(f'- {"Write mode:":40}{color_string((CY, write_mode))}') - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): # Write mode not supported in current firmware pass @@ -3067,20 +3543,20 @@ class HFMFUEConfig(SlotIndexArgsAndGoUnit, HF14AAntiCollArgsUnit, DeviceRequired try: version = self.cmd.mf0_ntag_get_version_data().hex().upper() print(f'- {"Version:":40}{color_string((CY, version))}') - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): pass try: signature = self.cmd.mf0_ntag_get_signature_data().hex().upper() print(f'- {"Signature:":40}{color_string((CY, signature))}') - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): pass try: detection = color_string((CG, "enabled")) if self.cmd.mf0_ntag_get_detection_enable() else color_string((CR, "disabled")) print( f'- {"Log (password) mode:":40}{f"{detection}"}') - except: + except (ValueError, chameleon_com.CMDInvalidException, TimeoutError): pass @hf_mfu.command('edetect') @@ -3135,7 +3611,7 @@ class LFEMRead(ReaderRequiredUnit): def on_exec(self, args: argparse.Namespace): data = self.cmd.em410x_scan() - print(color_string((TagSpecificType(data[0])), (CG, data[1].hex()))) + print(f"{TagSpecificType(data[0])}: {color_string((CG, data[1].hex()))}") @lf_em_410x.command('write') @@ -3228,23 +3704,19 @@ class LFHIDProxEconfig(SlotIndexArgsAndGoUnit, LFHIDIdArgsUnit): format = HIDFormat[args.format] id = struct.pack(">BIBIBH", format.value, args.fc, (args.cn >> 32), args.cn & 0xffffffff, args.il, args.oem) self.cmd.hidprox_set_emu_id(id) - print(' - Set hidprox tag id success.') - fc = args.fc - cn = args.cn - il = args.il - oem = args.oem + print(' - SET hidprox tag id success.') else: (format, fc, cn1, cn2, il, oem) = self.cmd.hidprox_get_emu_id() cn = (cn1 << 32) + cn2 - print(' - Get hidprox tag id success.') + print(' - GET hidprox tag id success.') print(f" - HIDProx/{HIDFormat(format)}") - if fc > 0: - print(f" FC: {color_string((CG, fc))}") - if il > 0: - print(f" IL: {color_string((CG, il))}") - if oem > 0: - print(f" OEM: {color_string((CG, oem))}") - print(f" CN: {color_string((CG, cn))}") + if fc > 0: + print(f" FC: {color_string((CG, fc))}") + if il > 0: + print(f" IL: {color_string((CG, il))}") + if oem > 0: + print(f" OEM: {color_string((CG, oem))}") + print(f" CN: {color_string((CG, cn))}") @lf_viking.command('read') class LFVikingRead(ReaderRequiredUnit): @@ -3391,12 +3863,12 @@ class HWSlotList(DeviceRequiredUnit): cn = (cn1 << 32) + cn2 print(f" {'Format:':40}{color_string((CY, HIDFormat(format)))}") if fc > 0: - print(f" FC: {color_string((CG, fc))}") + print(f" {'FC:':40}{color_string((CG, fc))}") if il > 0: - print(f" IL: {color_string((CG, il))}") + print(f" {'IL:':40}{color_string((CG, il))}") if oem > 0: - print(f" OEM: {color_string((CG, oem))}") - print(f" CN: {color_string((CG, cn))}") + print(f" {'OEM:':40}{color_string((CG, oem))}") + print(f" {'CN:':40}{color_string((CG, cn))}") if lf_tag_type == TagSpecificType.Viking: id = self.cmd.viking_get_emu_id() print(f" {'ID:':40}{color_string((CY, id.hex().upper()))}") diff --git a/software/script/chameleon_cmd.py b/software/script/chameleon_cmd.py index f34889b..58bda57 100644 --- a/software/script/chameleon_cmd.py +++ b/software/script/chameleon_cmd.py @@ -1304,6 +1304,18 @@ class ChameleonCMD: def set_ble_pairing_enable(self, enabled: bool): data = struct.pack('!B', enabled) return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_ENABLE, data) + + @expect_response(Status.SUCCESS) + def mf1_get_field_off_do_reset(self): + resp = self.device.send_cmd_sync(Command.MF1_GET_FIELD_OFF_DO_RESET) + if resp.status == Status.SUCCESS: + resp.parsed = struct.unpack('!B', resp.data)[0] == 1 + return resp + + @expect_response(Status.SUCCESS) + def mf1_set_field_off_do_reset(self, enabled: bool): + data = struct.pack('!B', enabled) + return self.device.send_cmd_sync(Command.MF1_SET_FIELD_OFF_DO_RESET, data) def test_fn(): diff --git a/software/script/chameleon_com.py b/software/script/chameleon_com.py index 33bd8d0..6940f55 100644 --- a/software/script/chameleon_com.py +++ b/software/script/chameleon_com.py @@ -1,18 +1,29 @@ +import sys import queue import struct import threading import time -import serial +import platform from typing import Union +from enum import Enum, auto +import serial +import socket + from chameleon_utils import CR, CG, CC, CY, color_string from chameleon_enum import Command, Status +ANDROID = 'android' in platform.release() + # each thread is waiting for its data for 100 ms before looping again THREAD_BLOCKING_TIMEOUT = 0.1 # TODO: client settings DEBUG = False +class TransportType(Enum): + NONE = auto() + SERIAL = auto() + SOCKET = auto() class NotOpenException(Exception): """ @@ -57,7 +68,8 @@ class ChameleonCom: """ Create a chameleon device instance """ - self.serial_instance: Union[serial.Serial, None] = None + self.transport: Union[serial.Serial, socket.socket, None] = None + self.transport_type = TransportType.NONE self.send_data_queue = queue.Queue() self.wait_response_map = {} self.event_closing = threading.Event() @@ -68,7 +80,7 @@ class ChameleonCom: :return: """ - return self.serial_instance is not None and self.serial_instance.is_open + return self.transport is not None and (self.transport_type is TransportType.SOCKET or self.transport.is_open) def open(self, port) -> "ChameleonCom": """ @@ -82,19 +94,35 @@ class ChameleonCom: error = None try: # open serial port - self.serial_instance = serial.Serial(port=port, baudrate=115200) + if port.startswith('tcp:'): + host, _, port = port[4:].partition(':') + if not host or not port: + sys.exit(color_string(CR, 'Usage: tcp:127.0.0.1:4321')) + self.transport = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + print('Connecting to', host, int(port)) + self.transport.connect((host, int(port))) + self.transport_type = TransportType.SOCKET + else: + if ANDROID: + sys.exit(color_string(CR, 'COM port is not supported on Android, make a USB-serial to TCP communication bridge')) + self.transport = serial.Serial(port=port, baudrate=115200) + self.transport_type = TransportType.SERIAL except Exception as e: error = e finally: if error is not None: raise OpenFailException(error) - assert self.serial_instance is not None - try: - self.serial_instance.dtr = True # must make dtr enable - except Exception: - # not all serial support dtr, e.g. virtual serial over BLE - pass - self.serial_instance.timeout = THREAD_BLOCKING_TIMEOUT + assert self.transport is not None + assert self.transport_type is not TransportType.NONE + if self.transport_type is TransportType.SERIAL: + try: + self.transport.dtr = True # must make dtr enable + except Exception: + # not all serial support dtr, e.g. virtual serial over BLE + pass + self.transport.timeout = THREAD_BLOCKING_TIMEOUT + else: # SOCKET + self.transport.settimeout(THREAD_BLOCKING_TIMEOUT) # clear variable self.send_data_queue.queue.clear() self.wait_response_map.clear() @@ -136,12 +164,14 @@ class ChameleonCom: """ self.event_closing.set() try: - assert self.serial_instance is not None - self.serial_instance.close() + assert self.transport is not None + if self.transport_type is TransportType.SOCKET: + self.transport.shutdown() + self.transport.close() except Exception: pass finally: - self.serial_instance = None + self.transport = None self.wait_response_map.clear() self.send_data_queue.queue.clear() @@ -159,16 +189,29 @@ class ChameleonCom: while self.isOpen(): # receive - try: - assert self.serial_instance is not None - data_bytes = self.serial_instance.read() - except Exception as e: - if not self.event_closing.is_set(): - print(f"Serial Error {e}, thread for receiver exit.") - self.close() - break - if len(data_bytes) > 0: + assert self.transport_type is not TransportType.NONE + if self.transport_type is TransportType.SERIAL: + try: + assert self.transport is not None + data_bytes = bytearray(self.transport.read()) + except Exception as e: + if not self.event_closing.is_set(): + print(f"Serial Error {e}, thread for receiver exit.") + self.close() + break + else: # SOCKET + try: + data_bytes = bytearray(self.transport.recv(1024)) + except socket.timeout: + continue + except OSError: + print(color_string(CR, 'socket closed')) + self.transport = None + break + + while len(data_bytes) > 0: data_byte = data_bytes[0] + data_bytes = data_bytes[1:] data_buffer.append(data_byte) if data_position < struct.calcsize('!BB'): # start of frame + lrc1 if data_position == 0: @@ -267,14 +310,25 @@ class ChameleonCom: self.wait_response_map[task_cmd]['start_time'] = start_time self.wait_response_map[task_cmd]['end_time'] = start_time + task_timeout self.wait_response_map[task_cmd]['is_timeout'] = False - try: - assert self.serial_instance is not None - # send to device - self.serial_instance.write(task['frame']) - except Exception as e: - print(f"Serial Error {e}, thread for transfer exit.") - self.close() - break + assert self.transport_type is not TransportType.NONE + if self.transport_type == TransportType.SERIAL: + try: + assert self.transport is not None + # send to device + self.transport.write(task['frame']) + except Exception as e: + print(f"Serial Error {e}, thread for transfer exit.") + self.close() + break + else: # SOCKET + try: + assert self.transport is not None + self.transport.sendall(task['frame']) + except OSError as e: + self.transport = None + print(f'Socket error {e}, thread for transfer exit.') + self.close() + break # update queue status self.send_data_queue.task_done() # disconnect if DFU command has been sent diff --git a/software/script/chameleon_enum.py b/software/script/chameleon_enum.py index a66ebca..fc49bfb 100644 --- a/software/script/chameleon_enum.py +++ b/software/script/chameleon_enum.py @@ -124,6 +124,9 @@ class Command(enum.IntEnum): # FIXME: not implemented MF0_NTAG_GET_EMULATOR_CONFIG = 4037 + MF1_SET_FIELD_OFF_DO_RESET = 4038 + MF1_GET_FIELD_OFF_DO_RESET = 4039 + EM410X_SET_EMU_ID = 5000 EM410X_GET_EMU_ID = 5001 HIDPROX_SET_EMU_ID = 5002 @@ -581,6 +584,7 @@ class HIDFormat(enum.IntEnum): C1K35S = 21 C15001 = 22 S12906 = 23 + ACTPHID = 42 SIE36 = 24 H10320 = 25 H10302 = 26 @@ -614,6 +618,7 @@ class HIDFormat(enum.IntEnum): HIDFormat.C1K35S: "HID Corporate 1000 35-bit Std", HIDFormat.C15001: "HID KeyScan 36-bit", HIDFormat.S12906: "HID Simplex 36-bit", + HIDFormat.ACTPHID: "HID ACTProx 36-bit", HIDFormat.SIE36: "HID 36-bit Siemens", HIDFormat.H10320: "HID H10320 37-bit BCD", HIDFormat.H10302: "HID H10302 37-bit huge ID", diff --git a/software/src/CMakeLists.txt b/software/src/CMakeLists.txt index d19d0cb..f33c8d6 100644 --- a/software/src/CMakeLists.txt +++ b/software/src/CMakeLists.txt @@ -77,7 +77,7 @@ endif() # --- Platform specific settings --- -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") MESSAGE(STATUS "Run on linux.") if (CMAKE_BUILD_TYPE STREQUAL "Release") set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O3") @@ -126,7 +126,7 @@ endif() add_executable(nested ${COMMON_FILES} ${NESTED_UTIL} nested.c) target_include_directories(nested PRIVATE ${SRC_DIR}) target_link_libraries(nested PRIVATE ${LIBTHREAD}) # Link common thread lib -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(nested PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -138,7 +138,7 @@ endif() add_executable(staticnested ${COMMON_FILES} ${NESTED_UTIL} staticnested.c) target_include_directories(staticnested PRIVATE ${SRC_DIR}) target_link_libraries(staticnested PRIVATE ${LIBTHREAD}) # Link common thread lib -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(staticnested PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -150,7 +150,7 @@ endif() add_executable(darkside ${COMMON_FILES} ${MFKEY_UTIL} darkside.c) target_include_directories(darkside PRIVATE ${SRC_DIR}) # darkside doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(darkside PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -161,7 +161,7 @@ endif() add_executable(mfkey32 ${COMMON_FILES} mfkey32.c) target_include_directories(mfkey32 PRIVATE ${SRC_DIR}) # mfkey32 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(mfkey32 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -172,7 +172,7 @@ endif() add_executable(mfkey32v2 ${COMMON_FILES} mfkey32v2.c) target_include_directories(mfkey32v2 PRIVATE ${SRC_DIR}) # mfkey32v2 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(mfkey32v2 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -183,7 +183,7 @@ endif() add_executable(mfkey64 ${COMMON_FILES} mfkey64.c) target_include_directories(mfkey64 PRIVATE ${SRC_DIR}) # mfkey64 doesn't seem to need pthreads based on original file -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(mfkey64 PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -192,7 +192,7 @@ endif() add_executable(staticnested_1nt ${COMMON_FILES} staticnested_1nt.c) target_include_directories(staticnested_1nt PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(staticnested_1nt PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -201,7 +201,7 @@ endif() add_executable(staticnested_2x1nt_rf08s ${COMMON_FILES} staticnested_2x1nt_rf08s.c) target_include_directories(staticnested_2x1nt_rf08s PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(staticnested_2x1nt_rf08s PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") @@ -210,13 +210,27 @@ endif() add_executable(staticnested_2x1nt_rf08s_1key ${COMMON_FILES} staticnested_2x1nt_rf08s_1key.c) target_include_directories(staticnested_2x1nt_rf08s_1key PRIVATE ${SRC_DIR}) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(staticnested_2x1nt_rf08s_1key PRIVATE _GNU_SOURCE) endif() if (CMAKE_SYSTEM_NAME MATCHES "Windows") target_compile_definitions(staticnested_2x1nt_rf08s_1key PRIVATE HAVE_STRUCT_TIMESPEC) endif() +# --- mfulc_des_brute Executable --- +add_executable(mfulc_des_brute mfulc_des_brute.c) +target_include_directories(mfulc_des_brute PRIVATE ${SRC_DIR}) +target_link_libraries(mfulc_des_brute PRIVATE ${LIBTHREAD} OpenSSL::Crypto) +target_compile_options(mfulc_des_brute PRIVATE -Wno-deprecated-declarations) +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") + target_compile_definitions(mfulc_des_brute PRIVATE _GNU_SOURCE) + find_package(OpenSSL REQUIRED) +endif() +if (CMAKE_SYSTEM_NAME MATCHES "Windows") + target_compile_definitions(mfulc_des_brute PRIVATE HAVE_STRUCT_TIMESPEC) + find_package(OpenSSL REQUIRED) +endif() + # --- hardnested Executable --- add_executable(hardnested ${COMMON_FILES} ${HARDNESTED_SOURCES}) @@ -230,7 +244,7 @@ target_include_directories(hardnested PRIVATE ) target_compile_options(hardnested PRIVATE -Wall) -if (CMAKE_SYSTEM_NAME MATCHES "Linux") +if (CMAKE_SYSTEM_NAME MATCHES "Linux" OR CMAKE_SYSTEM_NAME MATCHES "Android" OR CMAKE_SYSTEM_NAME MATCHES "Darwin") target_compile_definitions(hardnested PRIVATE _GNU_SOURCE) endif() diff --git a/software/src/mfulc_des_brute.c b/software/src/mfulc_des_brute.c new file mode 100644 index 0000000..7d7b307 --- /dev/null +++ b/software/src/mfulc_des_brute.c @@ -0,0 +1,368 @@ +// noproto & doegox, 2025 +// cf "BREAKMEIFYOUCAN!: Exploiting Keyspace Reduction and Relay Attacks in 3DES and AES-protected NFC Technologies" +// for more info + +#include +#include +#include +#include +#include +#include +#include + +#define BLOCK_SIZE 8 // DES (and 3DES) block size in bytes +#define KEY_SIZE 16 // Full 2TDEA key size (K1 || K2) +#define BENCHMARK_FULL_KEYSPACE 0 + +// Global flag to signal that a key has been found. +volatile int key_found = 0; + +typedef enum { + LFSR_UNDEF = 0, + LFSR_ULCG = 1, + LFSR_USCUIDUL = 2 +} lfsr_t; + +typedef struct { + uint32_t start; // starting candidate (inclusive) + uint32_t end; // ending candidate (exclusive) + int key_mode; // 0 to 3 (i.e. brute force segment 1-4 as 0-indexed) + unsigned char init_ciphertext[BLOCK_SIZE]; + unsigned char prev_ciphertext[BLOCK_SIZE]; // "IV" of ciphertext for CBC mode in reader mode + unsigned char ciphertext[BLOCK_SIZE]; + unsigned char base_key[KEY_SIZE]; // the 3DES base key provided by the user + int thread_id; + lfsr_t lfsr_type; + bool is_reader_mode; // true for -r mode, false for -c mode +} thread_args_t; + +// Converts a hex string to bytes. The hex string must be exactly 2*len hex digits long. +static int hex_to_bytes(const char *hex, unsigned char *buf, size_t len) { + if (strlen(hex) != len * 2) + return 0; + for (size_t i = 0; i < len; i++) { + unsigned int byte; + if (sscanf(hex + 2 * i, "%2x", &byte) != 1) + return 0; + buf[i] = (unsigned char) byte; + } + return 1; +} + +// Print a byte array as hex. +static void print_hex(const unsigned char *buf, size_t len) { + for (size_t i = 0; i < len; i++) + printf("%02X", buf[i]); + printf("\n"); +} + +static bool valid_lfsr_ulcg(uint64_t x64) { + x64 = __builtin_bswap64(x64); + uint16_t x16 = x64 >> 48; + x16 = x16 << 15 | ((x16 >> 1) ^ ((x16 >> 3 ^ x16 >> 4 ^ x16 >> 6) & 1)); + if (x16 != ((x64 >> 32) & 0xFFFF)) return false; + x16 = x16 << 15 | ((x16 >> 1) ^ ((x16 >> 3 ^ x16 >> 4 ^ x16 >> 6) & 1)); + if (x16 != ((x64 >> 16) & 0xFFFF)) return false; + x16 = x16 << 15 | ((x16 >> 1) ^ ((x16 >> 3 ^ x16 >> 4 ^ x16 >> 6) & 1)); + if (x16 != (x64 & 0xFFFF)) return false; + return true; +} + +static bool valid_lfsr_uscuidul(uint64_t x64) { + x64 = __builtin_bswap64(x64); + uint16_t x16 = x64 & 0xFFFF; + for (int i = 0; i < 16; i++) x16 = x16 >> 1 | (x16 ^ x16 >> 2 ^ x16 >> 3 ^ x16 >> 5) << 15; + if (x16 != ((x64 >> 16) & 0xFFFF)) return false; + for (int i = 0; i < 16; i++) x16 = x16 >> 1 | (x16 ^ x16 >> 2 ^ x16 >> 3 ^ x16 >> 5) << 15; + if (x16 != ((x64 >> 32) & 0xFFFF)) return false; + for (int i = 0; i < 16; i++) x16 = x16 >> 1 | (x16 ^ x16 >> 2 ^ x16 >> 3 ^ x16 >> 5) << 15; + if (x16 != ((x64 >> 48) & 0xFFFF)) return false; + return true; +} + + +static bool valid_lfsr(uint64_t x64, lfsr_t lfsr_type) { + switch (lfsr_type) { + case LFSR_ULCG: + return valid_lfsr_ulcg(x64); + case LFSR_USCUIDUL: + return valid_lfsr_uscuidul(x64); + case LFSR_UNDEF: + default: + return false; + } +} + +static lfsr_t detect_lfsr_type(unsigned char *init_ciphertext) { + DES_cblock fixed_key = {0}; + DES_key_schedule fixed_schedule; + DES_set_key_unchecked(&fixed_key, &fixed_schedule); + uint64_t out; + DES_ecb_encrypt((DES_cblock *)init_ciphertext, (DES_cblock *)&out, &fixed_schedule, DES_DECRYPT); + if (valid_lfsr_ulcg(out)) { + return LFSR_ULCG; + } else if (valid_lfsr_uscuidul(out)) { + return LFSR_USCUIDUL; + } + return LFSR_UNDEF; +} + +// Worker thread function using low-level DES functions. +static void *worker(void *arg) { + thread_args_t *targs = (thread_args_t *) arg; + uint32_t start = targs->start; + uint32_t end = targs->end; + int key_mode = targs->key_mode; + + // Determine which half is being brute forced. + // For key_mode 0 or 1 the candidate is in K1; for key_mode 2 or 3 the candidate is in K2. + int candidate_in_K1 = (key_mode < 2) ? 1 : 0; + + // Determine the 4-byte offset within the variable half: + // For K1, key_mode 0 means segment1 (offset 0), key_mode 1 means segment2 (offset 4). + // For K2, key_mode 2 means segment3 (offset 0), key_mode 3 means segment4 (offset 4). + int var_offset = candidate_in_K1 ? ((key_mode % 2) * 4) : (((key_mode - 2) % 2) * 4); + + // Precompute the fixed half's DES key schedule. + DES_cblock fixed_key; + if (candidate_in_K1) { + // Fixed half is K2: bytes 8..15 of base_key. + memcpy(fixed_key, targs->base_key + 8, 8); + } else { + // Candidate in K2; fixed half is K1: bytes 0..7 of base_key. + memcpy(fixed_key, targs->base_key, 8); + } + DES_key_schedule fixed_schedule; + DES_set_key_unchecked(&fixed_key, &fixed_schedule); + uint64_t out; + uint64_t init_out; + + // For the candidate half, start with the corresponding half from the base key. + unsigned char base_half[8]; + if (candidate_in_K1) + memcpy(base_half, targs->base_key, 8); + else + memcpy(base_half, targs->base_key + 8, 8); + + // Loop over the candidate key indices in this thread's range. + for (uint32_t idx = start; idx < end; idx++) { + if (key_found && !BENCHMARK_FULL_KEYSPACE) + break; // Some other thread already found the key. + // Convert the candidate index (28 bits) into 4 bytes. + // Each candidate byte is constructed from a 7-bit chunk shifted left by 1 so that the LSB is zero. + uint8_t b0 = ((idx) & 0x7F) << 1; + uint8_t b1 = ((idx >> 7) & 0x7F) << 1; + uint8_t b2 = ((idx >> 14) & 0x7F) << 1; + uint8_t b3 = ((idx >> 21) & 0x7F) << 1; + // Build the candidate half key by starting with the fixed base half and substituting candidate bytes. + DES_cblock candidate_half; + memcpy(candidate_half, base_half, 8); + candidate_half[var_offset ] = b0; + candidate_half[var_offset + 1] = b1; + candidate_half[var_offset + 2] = b2; + candidate_half[var_offset + 3] = b3; + + // Compute the candidate half's DES key schedule. + DES_key_schedule candidate_schedule; + DES_set_key_unchecked(&candidate_half, &candidate_schedule); + + // Perform 2-key triple DES decryption on the ciphertext. + // If candidate is in K1: decryption = DES_ecb3_encrypt(cipher, out, candidate, fixed, candidate, DES_DECRYPT) + // If candidate is in K2: decryption = DES_ecb3_encrypt(cipher, out, fixed, candidate, fixed, DES_DECRYPT) + if (candidate_in_K1) { + DES_ecb3_encrypt((DES_cblock *)targs->ciphertext, (DES_cblock *)&out, + &candidate_schedule, &fixed_schedule, &candidate_schedule, DES_DECRYPT); + } else { + DES_ecb3_encrypt((DES_cblock *)targs->ciphertext, (DES_cblock *)&out, + &fixed_schedule, &candidate_schedule, &fixed_schedule, DES_DECRYPT); + } + + bool match = false; + if (targs->is_reader_mode) { + // In reader mode, also decrypt init_ciphertext and check for rotation relationship + // Apply XOR block to the second decrypted block (for CBC mode) + if (candidate_in_K1) { + DES_ecb3_encrypt((DES_cblock *)targs->init_ciphertext, (DES_cblock *)&init_out, + &candidate_schedule, &fixed_schedule, &candidate_schedule, DES_DECRYPT); + } else { + DES_ecb3_encrypt((DES_cblock *)targs->init_ciphertext, (DES_cblock *)&init_out, + &fixed_schedule, &candidate_schedule, &fixed_schedule, DES_DECRYPT); + } + // Apply XOR block to the second decrypted block (for CBC mode) + out ^= *(uint64_t *)targs->prev_ciphertext; + + // Check if out is 8-bit (1-byte) left rotated version of init_out + // Need to convert to big-endian for byte rotation, then back to little-endian + uint64_t init_be = __builtin_bswap64(init_out); + uint64_t rotated_be = (init_be << 8) | (init_be >> 56); + uint64_t rotated = __builtin_bswap64(rotated_be); + match = (out == rotated); + } else { + // In counterfeit mode, check the resulting plaintext against LFSR + match = valid_lfsr(out, targs->lfsr_type); + } + + if (match) { + key_found = 1; // signal to other threads + + // Build the full 16-byte key: start with the base key and substitute the candidate 4 bytes. + unsigned char full_key[KEY_SIZE]; + memcpy(full_key, targs->base_key, KEY_SIZE); + int seg_offset = key_mode * 4; // key_mode: 0->bytes0, 1->bytes4, 2->bytes8, 3->bytes12. + full_key[seg_offset] = b0; + full_key[seg_offset + 1] = b1; + full_key[seg_offset + 2] = b2; + full_key[seg_offset + 3] = b3; + printf("Thread %d: Found key index: %u\n", targs->thread_id, idx); + printf("Full key (hex): "); + print_hex(full_key, KEY_SIZE); + if (!BENCHMARK_FULL_KEYSPACE) + break; + } + } + return NULL; +} + +static void print_help_and_exit(const char *cmd_name) { + fprintf(stderr, + "Usage:\n" + " * Counterfeit key recovery:\n" + " %s -c <3DES base key hex (32 hex digits)> \n" + " * Reader nonce key recovery:\n" + " %s -r <3DES base key hex (32 hex digits)> \n", + cmd_name, + cmd_name); + exit(1); +} + +int main(int argc, char **argv) { + // Check for -c or -r flag first to determine expected argument count + if (argc < 2) { + print_help_and_exit(argv[0]); + } + bool is_reader_mode = false; + if (strcmp(argv[1], "-c") == 0) { + is_reader_mode = false; + if (argc != 7) { + fprintf(stderr, "Error: -c mode requires exactly 6 arguments\n"); + print_help_and_exit(argv[0]); + } + } else if (strcmp(argv[1], "-r") == 0) { + is_reader_mode = true; + if (argc != 7) { + fprintf(stderr, "Error: -r mode requires exactly 6 arguments\n"); + print_help_and_exit(argv[0]); + } + } else { + fprintf(stderr, "Error: first argument must be -c or -r\n"); + print_help_and_exit(argv[0]); + } + + unsigned char init_ciphertext[BLOCK_SIZE]; + unsigned char tmp_blocks[2 * BLOCK_SIZE]; + unsigned char ciphertext[BLOCK_SIZE]; + unsigned char base_key[KEY_SIZE]; + + if (is_reader_mode) { + // In reader mode, the first ciphertext is ERndB and the second is ERndA|ERndB' + if (!hex_to_bytes(argv[2], init_ciphertext, BLOCK_SIZE)) { + fprintf(stderr, "Error: invalid ERndB hex string.\n"); + return 1; + } + if (!hex_to_bytes(argv[3], tmp_blocks, 2 * BLOCK_SIZE)) { + fprintf(stderr, "Error: invalid ERndARndB' hex string.\n"); + return 1; + } + } else { + // In counterfeit mode, both ciphertexts are just ciphertext blocks + if (!hex_to_bytes(argv[2], init_ciphertext, BLOCK_SIZE)) { + fprintf(stderr, "Error: invalid null key ERndB hex string.\n"); + return 1; + } + if (!hex_to_bytes(argv[3], ciphertext, BLOCK_SIZE)) { + fprintf(stderr, "Error: invalid target key ERndB hex string.\n"); + return 1; + } + } + if (!hex_to_bytes(argv[4], base_key, KEY_SIZE)) { + fprintf(stderr, "Error: invalid 3DES base key hex string.\n"); + return 1; + } + + int seg = atoi(argv[5]); + if (seg < 1 || seg > 4) { + fprintf(stderr, "Error: key segment must be between 1 and 4.\n"); + return 1; + } + int num_threads = atoi(argv[6]); + if (num_threads < 1) { + fprintf(stderr, "Error: number of threads must be at least 1.\n"); + return 1; + } + + lfsr_t lfsr_type = LFSR_UNDEF; + if (!is_reader_mode) { + // Only detect LFSR type in counterfeit mode + lfsr_type = detect_lfsr_type(init_ciphertext); + switch (lfsr_type) { + case LFSR_ULCG: + printf("LFSR detection: ULCG\n"); + break; + case LFSR_USCUIDUL: + printf("LFSR detection: ULC_USCUIDUL\n"); + break; + case LFSR_UNDEF: + default: + fprintf(stderr, "LFSR detection: Could not detect LFSR!!\n"); + return 1; + } + } + + // key_mode is zero-indexed (0,1,2,3) + int key_mode = seg - 1; + + // Total candidate space: 2^28 keys. + uint32_t total = (1UL << 28); + uint32_t chunk = total / num_threads; + uint32_t remainder = total % num_threads; + + pthread_t *threads = malloc(num_threads * sizeof(pthread_t)); + thread_args_t *targs = malloc(num_threads * sizeof(thread_args_t)); + if (!threads || !targs) { + fprintf(stderr, "Allocation error.\n"); + return 1; + } + + // Divide the candidate space as equally as possible among threads. + uint32_t current = 0; + for (int i = 0; i < num_threads; i++) { + targs[i].start = current; + targs[i].end = current + chunk; + if (i == num_threads - 1) + targs[i].end += remainder; + targs[i].key_mode = key_mode; + targs[i].lfsr_type = lfsr_type; + targs[i].is_reader_mode = is_reader_mode; + memcpy(targs[i].init_ciphertext, init_ciphertext, BLOCK_SIZE); + if (is_reader_mode) { + memcpy(targs[i].prev_ciphertext, tmp_blocks, BLOCK_SIZE); + memcpy(targs[i].ciphertext, tmp_blocks + BLOCK_SIZE, BLOCK_SIZE); + } else { + memcpy(targs[i].ciphertext, ciphertext, BLOCK_SIZE); + } + memcpy(targs[i].base_key, base_key, KEY_SIZE); + targs[i].thread_id = i; + current = targs[i].end; + pthread_create(&threads[i], NULL, worker, &targs[i]); + } + + for (int i = 0; i < num_threads; i++) + pthread_join(threads[i], NULL); + + if (!key_found) + printf("No matching key was found.\n"); + + free(threads); + free(targs); + return 0; +}