Merge branch 'main' into fix-hidprox-cli-args

This commit is contained in:
Rick Console authored and GitHub committed 2026-02-02 15:58:42 -05:00
commit 997f6bcaab
21 files changed
+1299 -143

No files matched your search

+10
View File
@@ -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)
+16 -1
View File
@@ -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 },
+6
View File
@@ -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);
}
+2
View File
@@ -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)
//
// ******************************************************************
@@ -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;
}
@@ -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
@@ -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;
}
@@ -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;
}
@@ -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
@@ -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) {
@@ -72,6 +72,7 @@ typedef enum {
C1K48S,
AVIG56,
IR56,
ACTPHID,
} card_format_t;
// Structure for defined Wiegand card formats available for packing/unpacking
+12 -15
View File
@@ -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:
+15 -9
View File
@@ -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.");
+2 -1
View File
@@ -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
@@ -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)
+508 -36
View File
@@ -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()))}")
+12
View File
@@ -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():
+85 -31
View File
@@ -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
+5
View File
@@ -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",
+25 -11
View File
@@ -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()
+368
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <pthread.h>
#include <openssl/des.h>
#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 <null key ERndB (8 hex digits)> <target key ERndB (8 hex digits)> <3DES base key hex (32 hex digits)> <key segment (1-4)> <num threads>\n"
" * Reader nonce key recovery:\n"
" %s -r <ERndB (8 hex digits)> <ERndARndB' (16 hex digits)> <3DES base key hex (32 hex digits)> <key segment (1-4)> <num threads>\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;
}