Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ All notable changes to this project will be documented in this file.
This project uses the changelog in accordance with [keepchangelog](http://keepachangelog.com/). Please use this to write notable changes, which is not the same as git commit log...

## [unreleased][unreleased]
- Improved `hf iclass legbrute`: fixed multithreaded key-range partitioning so threads cover non-overlapping slices of the 40-bit keyspace, added ETA display, keyboard abort with resume hint, `_Atomic` correctness for shared state, `pthread_create` error handling, and thread count capped at available CPUs (@antiklesys)
- Added wildcard support to `hf secc sim` payloads (@antiklesys)
- Added `hf secc` to build a base for simulating basic function of iclass SE config cards (@antiklesys)
- Improved SIO parsing for `hf iclass view` based on Iceman's "Dismantling the SEOS Protocol" talk (@antiklesys)
Expand All @@ -29,7 +30,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac
- Added DESFire AID values related to LEAF (@kormax)
- Added `dict`, `ascii`, `mad` presets for `hf mfdes bruteaid` (@kormax)
- Added tag loss detection & recovery into `hf mfdes bruteaid` (@kormax)
- Added --credit option for `hf iclass legrec` command to perform a credit key recovery. This is experimental and unfinished as it only partially works.(@antiklesys)
- Added `--credit` option to `hf iclass legrec` for credit key recovery. Note: this option alone is experimental and only partially functional; the standard key recovery works normally.(@antiklesys)
- Added hardening for all host binaries. Exact level of hardening depends on the OS (@doegox)
- Added `hf aliro read` command (@kormax)
- Added `hf aliro info` command (@kormax)
Expand Down
8 changes: 4 additions & 4 deletions armsrc/secc.c
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,10 @@ bool hid_config_card_jam(const uint8_t *cmd, int len, uint8_t *dma_buf) {
int off = (pcb & 0x08) ? 2 : 1; // skip CID if present

// Select active APDU pattern and response (custom or default)
const uint8_t *match = (s_jam_apdu_len > 0) ? s_jam_apdu : s_jam_apdu_default;
int match_len = (s_jam_apdu_len > 0) ? (int)s_jam_apdu_len : (int)sizeof(s_jam_apdu_default);
const uint8_t *resp_data = (s_jam_resp_len > 0) ? s_jam_resp : s_jam_resp_default;
int resp_data_len = (s_jam_resp_len > 0) ? (int)s_jam_resp_len : (int)sizeof(s_jam_resp_default);
const uint8_t *match = (s_jam_apdu_len > 0) ? s_jam_apdu : s_jam_apdu_default;
int match_len = (s_jam_apdu_len > 0) ? (int)s_jam_apdu_len : (int)sizeof(s_jam_apdu_default);
const uint8_t *resp_data = (s_jam_resp_len > 0) ? s_jam_resp : s_jam_resp_default;
int resp_data_len = (s_jam_resp_len > 0) ? (int)s_jam_resp_len : (int)sizeof(s_jam_resp_default);

if (len < off + match_len + 2) // off + APDU pattern + 2-byte CRC
return false;
Expand Down
165 changes: 144 additions & 21 deletions client/src/cmdhficlass.c
Original file line number Diff line number Diff line change
Expand Up @@ -6063,13 +6063,18 @@ void generate_key_block_inverted(const uint8_t *startingKey, uint64_t index, uin
typedef struct {
uint8_t startingKey[8];
uint64_t index_start;
uint64_t index_end;
uint8_t CCNR1[12];
uint8_t MAC_TAG1[4];
uint8_t CCNR2[12];
uint8_t MAC_TAG2[4];
int thread_id;
int thread_count;
volatile bool *found;
uint64_t start_time;
_Atomic bool *found;
_Atomic bool *aborted;
_Atomic uint64_t *aborted_at;
bool debug;
pthread_mutex_t *log_lock;
} thread_args_t;

Expand All @@ -6082,7 +6087,31 @@ static void *brute_thread(void *args_void) {
uint8_t verification_mac[4];
uint64_t index = args->index_start;

while (!*(args->found)) {
if (args->debug) {
pthread_mutex_lock(args->log_lock);

PrintAndLogEx(INFO, "Thread[%2d] range [%" PRIu64 " - %" PRIu64 ") startingKey: %s"
, args->thread_id
, args->index_start
, args->index_end
, sprint_hex_inrow(args->startingKey, 8));

// Show first 2 candidates — different threads must start from different candidates
for (int d = 0; d < 2; d++) {
generate_key_block_inverted(args->startingKey, args->index_start + d, div_key);
PrintAndLogEx(INFO, " [index %" PRIu64 "]: %s", args->index_start + d, sprint_hex_inrow(div_key, 8));
}

// Show the midpoint of the slice — confirms byte-0 carry is reached inside this thread's range
uint64_t mid = args->index_start + (args->index_end - args->index_start) / 2;
generate_key_block_inverted(args->startingKey, mid, div_key);
PrintAndLogEx(INFO, " [index %" PRIu64 " (mid)]: %s", mid, sprint_hex_inrow(div_key, 8));

pthread_mutex_unlock(args->log_lock);
return NULL;
}

while (index < args->index_end && !*(args->found) && !*(args->aborted)) {

generate_key_block_inverted(args->startingKey, index, div_key);
doMAC(args->CCNR1, div_key, mac);
Expand All @@ -6104,15 +6133,45 @@ static void *brute_thread(void *args_void) {
}
}

if (index % 1000000 == 0 && !*(args->found)) {
uint64_t thread_progress = index - args->index_start;
if (thread_progress % 1000000 == 0 && !*(args->found)) {

if (args->thread_id == 0) {
uint64_t keyspace = (uint64_t)1 << 40;
uint64_t keyspace_m = keyspace / 1000000; // 1,099,511
uint64_t run_progress = thread_progress * (uint64_t)args->thread_count;
// Cumulative absolute position across all threads including any --index offset
uint64_t abs_done = args->index_start * (uint64_t)args->thread_count + run_progress;
uint64_t keys_left = (abs_done < keyspace) ? keyspace - abs_done : 0;
uint64_t elapsed_ms = msclock() - args->start_time;

pthread_mutex_lock(args->log_lock);
PrintAndLogEx(INPLACE, "Tested "_YELLOW_("%" PRIu64)" million keys, curr index: "_YELLOW_("%" PRIu64)", Thread[0]: %s"
, ((index / 1000000) * args->thread_count)
, (index / 1000000)
, sprint_hex_inrow(div_key, 8)
);

if (elapsed_ms > 0 && run_progress > 0) {
// speed based on keys tested in this run only
uint64_t kps = run_progress * 1000 / elapsed_ms;
uint64_t eta_s = (kps > 0) ? keys_left / kps : 0;
uint64_t eta_d = eta_s / 86400;
uint64_t eta_h = (eta_s % 86400) / 3600;
uint64_t eta_m = (eta_s % 3600) / 60;
uint64_t eta_r = eta_s % 60;
PrintAndLogEx(INPLACE, "Tested "_YELLOW_("%" PRIu64)"M / %" PRIu64 "M keys speed: "_YELLOW_("%" PRIu64)" k/s ETA: "_YELLOW_("%" PRIu64 "d %02" PRIu64 "h %02" PRIu64 "m %02" PRIu64 "s")
, abs_done / 1000000
, keyspace_m
, kps / 1000
, eta_d, eta_h, eta_m, eta_r
);
} else {
PrintAndLogEx(INPLACE, "Tested "_YELLOW_("%" PRIu64)"M / %" PRIu64 "M keys"
, abs_done / 1000000
, keyspace_m
);
}

if (kbd_enter_pressed()) {
*args->aborted_at = index;
*args->aborted = true;
}
pthread_mutex_unlock(args->log_lock);
}

Expand All @@ -6123,14 +6182,16 @@ static void *brute_thread(void *args_void) {
}

// HF iClass legbrute - Multithreaded brute-force function
static int CmdHFiClassLegBrute_MT(uint8_t epurse[8], uint8_t macs[8], uint8_t macs2[8], uint8_t startingKey[8], uint64_t index, int threads) {
static int CmdHFiClassLegBrute_MT(uint8_t epurse[8], uint8_t macs[8], uint8_t macs2[8], uint8_t startingKey[8], uint64_t index, int threads, bool debug) {

int thread_count = threads;
if (thread_count < 1) {
thread_count = 1;
}
if (thread_count > 16) {
thread_count = 16;
int max_threads = num_CPUs();
if (thread_count > max_threads) {
PrintAndLogEx(INFO, "Capping threads at available CPU count (%d)", max_threads);
thread_count = max_threads;
}
PrintAndLogEx(INFO, "Bruteforcing using " _YELLOW_("%u") " threads", thread_count);
PrintAndLogEx(NORMAL, "");
Expand All @@ -6146,42 +6207,101 @@ static int CmdHFiClassLegBrute_MT(uint8_t epurse[8], uint8_t macs[8], uint8_t ma

pthread_t tids[thread_count];
thread_args_t args[thread_count];
volatile bool found = false;
_Atomic bool found = false;
_Atomic bool aborted = false;
_Atomic uint64_t aborted_at = 0;
pthread_mutex_t log_lock;
pthread_mutex_init(&log_lock, NULL);
PrintAndLogEx(HINT, "Hint: Press " _YELLOW_("<Enter>") " to abort");

// Divide the full 40-bit keyspace into equal non-overlapping slices, one per thread.
// All threads use the same startingKey; only their index range differs.
uint64_t keyspace = (uint64_t)1 << 40;
uint64_t slice = keyspace / thread_count;
uint64_t start_time = msclock();

int nibble_range = 16 / thread_count;
for (int i = 0; i < thread_count; i++) {
memcpy(args[i].startingKey, startingKey, 8);
args[i].startingKey[0] = (startingKey[0] & 0x0F) | ((i * nibble_range) << 4);
args[i].index_start = index;
args[i].index_start = index + (uint64_t)i * slice;
args[i].index_end = (i == thread_count - 1)
? index + keyspace // last thread absorbs remainder
: index + (uint64_t)(i + 1) * slice;
memcpy(args[i].CCNR1, CCNR, 12);
memcpy(args[i].MAC_TAG1, MAC_TAG, 4);
memcpy(args[i].CCNR2, CCNR2, 12);
memcpy(args[i].MAC_TAG2, MAC_TAG2, 4);
args[i].thread_id = i;
args[i].thread_count = thread_count;
args[i].start_time = start_time;
args[i].found = &found;
args[i].aborted = &aborted;
args[i].aborted_at = &aborted_at;
args[i].debug = debug;
args[i].log_lock = &log_lock;

pthread_create(&tids[i], NULL, brute_thread, &args[i]);
if (pthread_create(&tids[i], NULL, brute_thread, &args[i]) != 0) {
PrintAndLogEx(WARNING, "Failed to create thread %d, running with %d thread(s)", i, i);
thread_count = i;
break;
}
}

if (thread_count == 0) {
pthread_mutex_destroy(&log_lock);
return PM3_ESOFT;
}

for (int i = 0; i < thread_count; i++) {
pthread_join(tids[i], NULL);
}
pthread_mutex_destroy(&log_lock);

return found ? PM3_SUCCESS : ERR;
if (debug) {
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(INFO, "Index range summary (%d threads, keyspace 2^40 = %" PRIu64 "):", thread_count, (uint64_t)1 << 40);
PrintAndLogEx(INFO, " Thread start end slice size");
for (int i = 0; i < thread_count; i++) {
PrintAndLogEx(INFO, " [%2d] %-20" PRIu64 " %-20" PRIu64 " %" PRIu64
, i
, args[i].index_start
, args[i].index_end
, args[i].index_end - args[i].index_start);
}
// Verify ranges are contiguous and non-overlapping
bool ok = true;
for (int i = 1; i < thread_count; i++) {
if (args[i].index_start != args[i - 1].index_end) {
PrintAndLogEx(WARNING, _RED_(" Gap or overlap between thread %d and %d!"), i - 1, i);
ok = false;
}
}
if (ok) {
PrintAndLogEx(SUCCESS, _GREEN_(" Ranges are contiguous and non-overlapping"));
}
return PM3_SUCCESS;
}

if (aborted) {
uint64_t resume_millions = aborted_at / 1000000;
PrintAndLogEx(NORMAL, "");
PrintAndLogEx(WARNING, "aborted via keyboard!");
PrintAndLogEx(HINT, "Hint: resume with " _YELLOW_("--index %" PRIu64 " --threads %d"), resume_millions, thread_count);
return PM3_EOPABORTED;
}

if (found == false) {
PrintAndLogEx(WARNING, "Key not found in the given keyspace");
}

return found ? PM3_SUCCESS : PM3_ESOFT;
}

// CmdHFiClassLegBrute function with CLI and multithreading support
static int CmdHFiClassLegBrute(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass legbrute",
"This command takes sniffed trace data and a partial raw key and bruteforces the remaining 40 bits of the raw key.\n"
"Complete 40 bit keyspace is 1'099'511'627'776 and command is locked down to max 16 threads currently.\n"
"A possible worst case scenario on 16 threads estimates XXX days YYY hours MMM minutes.",
"Complete 40 bit keyspace is 1'099'511'627'776.",
"hf iclass legbrute --epurse feffffffffffffff --macs1 1306cad9b6c24466 --macs2 f0bf905e35f97923 --pk B4F12AADC5301225");

void *argtable[] = {
Expand All @@ -6191,7 +6311,8 @@ static int CmdHFiClassLegBrute(const char *Cmd) {
arg_str1(NULL, "macs2", "<hex>", "MACs captured from the reader, different than the first set (with the same csn and epurse value)"),
arg_str1(NULL, "pk", "<hex>", "Partial Key from legrec or starting key of keyblock from legbrute"),
arg_int0(NULL, "index", "<dec>", "Where to start from to retrieve the key, default 0 - value in millions e.g. 1 is 1 million"),
arg_int0(NULL, "threads", "<dec>", "Number of threads to use, by default it uses the cpu's max threads (max 16)."),
arg_int0(NULL, "threads", "<dec>", "Number of threads to use, by default it uses the cpu's max threads."),
arg_lit0(NULL, "dbg", "Print first 2 key candidates and midpoint per thread, then exit (use to verify thread partitioning)"),
arg_param_end
};
CLIExecWithReturn(ctx, Cmd, argtable, false);
Expand All @@ -6215,6 +6336,7 @@ static int CmdHFiClassLegBrute(const char *Cmd) {
uint64_t index = arg_get_int_def(ctx, 5, 0);
index *= 1000000;
int threads = arg_get_int_def(ctx, 6, num_CPUs());
bool debug = arg_get_lit(ctx, 7);
CLIParserFree(ctx);

if (epurse_len && epurse_len != PICOPASS_BLOCK_SIZE) {
Expand All @@ -6237,7 +6359,7 @@ static int CmdHFiClassLegBrute(const char *Cmd) {
return PM3_EINVARG;
}

return CmdHFiClassLegBrute_MT(epurse, macs, macs2, startingKey, index, threads);
return CmdHFiClassLegBrute_MT(epurse, macs, macs2, startingKey, index, threads, debug);
}

static void generate_single_key_block_inverted_opt(const uint8_t *startingKey, uint32_t index, uint8_t *keyBlock) {
Expand Down Expand Up @@ -7503,6 +7625,7 @@ static bool match_with_wildcard(const uint8_t *data, const uint8_t *pattern, con
return true;
}


static int CmdHFiClassSAM(const char *Cmd) {
CLIParserContext *ctx;
CLIParserInit(&ctx, "hf iclass sam",
Expand Down
Loading