From 294fb3c5046d8eea090552eee8aeb989ec905494 Mon Sep 17 00:00:00 2001 From: towelbyte Date: Wed, 14 Oct 2026 17:14:56 +0200 Subject: [PATCH] Simulate COTAG tag implementation New CLI command `lf cotag sim` with COTAG tag simulation implementation. Detects COTAG reader activation sequence and emulates a COTAG tag. Signed-off-by: andrej@towelbyte.net --- CHANGELOG.md | 3 +- armsrc/appmain.c | 5 + armsrc/lfops.c | 237 ++++++++++++++++++++++++++++++++++++++++ armsrc/lfops.h | 1 + client/src/cmdlfcotag.c | 80 +++++++++++++- doc/commands.json | 25 ++++- doc/commands.md | 1 + include/pm3_cmd.h | 15 +++ 8 files changed, 363 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5c284c67..35fbb36bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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] +- Added `lf cotag sim` - simulate COTAG tag (@towelbyte) - Added standalone mode `GDB_GATEWAY` (PM5): GDB Remote Serial Protocol server on the USB CDC link, flash and SRAM access, no probe needed (@nieldk) - Fixed `lf t55xx detect -p` - the password is no longer sent to a tag whose PWD bit could not be confirmed, which could set it and destroy the tag's data; pass `-o` to override (@mfcarroll) - Fixed `hf mf hardnested`/`hf mf autopwn` on PM5 with hardened MIFARE Classic EV1 (#3595, #3664) (@ByteOrderMarc) @@ -293,7 +294,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac - Add support for parsing Finnish Helsinki Regional Transport (HRT) travel cards (@sanduuz) - Added standalone mode `HF_DOEGOX_COMMIT` - DESFire suspended commit without relay (@doegox) - Changed `hf 14b info`- now support Innovatron protocol detection (@kormax) -- Improved `lf cotag reader` and `lf cotag demod` - Reimplementation and enhancement of proxmark3 COTAG support +- Improved `lf cotag reader` and `lf cotag demod` - Reimplementation and enhancement of proxmark3 COTAG support (@towelbyte) - Added `hf felica sim` command (@kormax) - Added `mad read`, `mad write`, `mad verify`, `mad decode`, `mad encode` commands with typed struct MAD API (@AlxCzl) - Added `hf mfdes getversion` command (@kormax) diff --git a/armsrc/appmain.c b/armsrc/appmain.c index 09f06dbec..67e69c371 100644 --- a/armsrc/appmain.c +++ b/armsrc/appmain.c @@ -1865,6 +1865,11 @@ static void PacketReceived(PacketCommandNG *packet) { Cotag(payload->mode, true); break; } + case CMD_LF_COTAG_SIMULATE: { + SimulateCotag((const lf_cotagsim_t *)packet->data.asBytes, true); + reply_ng(CMD_LF_COTAG_SIMULATE, PM3_EOPABORTED, NULL, 0); + break; + } #endif #ifdef WITH_HITAG diff --git a/armsrc/lfops.c b/armsrc/lfops.c index f06142b4f..016d91f91 100644 --- a/armsrc/lfops.c +++ b/armsrc/lfops.c @@ -2980,6 +2980,243 @@ void Cotag(uint32_t arg0, bool ledcontrol) { if (ledcontrol) LEDsoff(); } +#define COTAG_STEP_K 32 // samples per box of the step filter + +// a detected activation, for debug output +typedef struct { + uint32_t dt; // samples between the two rises + int32_t thr; // threshold the second rise crossed +} cotag_detect_val_t; + +// Block until the reader's activation sequence is detected. Return false on button press or USB data. +static bool cotag_activation_sequence_detect(const lf_cotagsim_t *params, cotag_detect_val_t *out) { + + // Reader's tag activation sequence: + // - ON 800 us, OFF 2200 us + // - ON ~5800 us (the tag is expected to respond in this window) + // - OFF ~16500 us (reader's pause if no tag detected, then repeat) + // + // We detect 2 rising edges between ON(800),OFF(2200),ON(5800), i.e. approx 3000us apart. + // Plus minus some tolerance, thus win_lo and win_hi. + + // Number of samples between 2 rising edges to look for an activation sequence + const uint32_t win_lo = (3000 - 300) * 12 / (LF_COTAG_DIVISOR + 1); + const uint32_t win_hi = (3000 + 300) * 12 / (LF_COTAG_DIVISOR + 1); + + // Samples ring buffer + uint8_t ring[2 * COTAG_STEP_K] = {0}; + uint8_t idx = 0; // points to the oldest sample + int32_t sum_new = 0, sum_old = 0; // sum of the newest (oldest) COTAG_STEP_K samples + + int32_t peak_recent_step = 0; // peak of step, scaled by 65536 so the decay below cannot truncate to zero + uint32_t sample_cnt = 0; + uint32_t last_rise = 0; // sample_cnt at the previous detected rising edge + bool armed = true; + uint16_t checked = 0; + + for (;;) { + WDT_HIT(); + + if (++checked >= 4000) { + checked = 0; + if (BUTTON_PRESS() || data_available()) { + return false; + } + } + + if (FPGA_SSC_RX_Ready() == false) { + continue; + } + + uint8_t sample = FPGA_SSC_RX_Value(); + + // Recalculate sum_new and sum_old. + // sample (new) added to sum_new; mid subtracted from sum_new and added to sum_old, + // ring[idx] (oldest) subtracted from sum_old and discarded. + uint8_t mid = ring[(idx + COTAG_STEP_K) % (2 * COTAG_STEP_K)]; + sum_new += sample - mid; + sum_old += mid - ring[idx]; + ring[idx] = sample; + idx = (idx + 1) % (2 * COTAG_STEP_K); + + if (++sample_cnt < 2 * COTAG_STEP_K) { + continue; // wait for the ring buffer to fill up first + } + + // Step filter + int32_t step = sum_new - sum_old; + if (step * 65536 > peak_recent_step) { + peak_recent_step = step * 65536; // attack at once + } else { + peak_recent_step -= peak_recent_step >> 13; // release slowly, tau 2^13 samples + } + + // use threshold as half the recent peak step (adapting to reader distance) + // (undo scaling by 65536 aka 2^16, then divide by 2: thus >> 17) + int32_t thr = MAX(params->thr_floor, peak_recent_step >> 17); + + // Rising edge detection: Schmitt trigger so that we get one event per rise: + // trigger once when step >= thr, then disarm, only re-arm if step falls below thr/4 + if (armed && step >= thr) { + armed = false; + uint32_t dt = sample_cnt - last_rise; // number of samples since the previous rise + + if (last_rise && dt >= win_lo && dt <= win_hi) { + // Activation sequence detected + out->dt = dt; + out->thr = thr; + return true; + } + last_rise = sample_cnt; + + } else if (step <= thr / 4) { + // step enough below the thr, re-arm Schmitt trigger + armed = true; + } + } +} + +#define COTAG_TX_HALFBIT_CYCLES (384 / 2) // Number of samples per half bit +#define COTAG_TX_SYNC_HALFBITS 4 // two leading sync 0 bits +#define COTAG_TX_MAX_HALFBITS (COTAG_TX_SYNC_HALFBITS + 2 * 8 * LF_COTAG_SIM_DATA_LEN) + +#define COTAG_SSC_STALL 100000 +#define COTAG_SETTLE_US 10000 + +// Discard count samples ("wait count samples"). Return false on error. +static bool cotag_skip_samples(uint32_t count) { + while (count--) { + uint32_t guard = 0; + while (FPGA_SSC_RX_Ready() == false) { + if (++guard > COTAG_SSC_STALL) { + return false; + } + } + (void)FPGA_SSC_RX_Value(); + } + return true; +} + +static uint16_t cotag_build_halfbits(uint8_t *half_bits, const uint8_t *data, uint8_t nbits) { + + // Two leading/sync 0 bits, manchester encoded + half_bits[0] = 0; + half_bits[1] = 1; + half_bits[2] = 0; + half_bits[3] = 1; + + uint16_t nhb = COTAG_TX_SYNC_HALFBITS; + for (uint8_t i = 0; i < nbits; i++) { + // bits in reverse order: + uint8_t src = nbits - 1 - i; + uint8_t bit = (data[src >> 3] >> (7 - (src & 7))) & 1; + // manchester encode the bit: + half_bits[nhb++] = bit; + half_bits[nhb++] = bit ^ 1; + } + return nhb; +} + +// Send COTAG frame's half bits. Return false on button press, USB data, or error. +static bool cotag_tx_frame(const uint8_t *half_bits, uint16_t nhb, uint8_t repeats, uint16_t hb_delay_us) { + + Gpio_SSC_DOUT_High(); + FpgaSendCommand(FPGA_CMD_SET_DIVISOR, LF_COTAG_TX_DIVISOR); + FpgaWriteConfWord(FPGA_MAJOR_MODE_LF_ADC | FPGA_LF_ADC_READER_FIELD); + + bool ok = true; + for (uint8_t r = 0; r < repeats && ok; r++) { + // Send precomputed half bits. Leading sync bits are sent only at first repetition + for (uint16_t i = (r == 0) ? 0 : COTAG_TX_SYNC_HALFBITS; i < nhb; i++) { + if (half_bits[i]) { + Gpio_SSC_DOUT_Low(); + } else { + Gpio_SSC_DOUT_High(); + } + + if (hb_delay_us) { + SpinDelayUsPrecision(hb_delay_us); + } else if (cotag_skip_samples(COTAG_TX_HALFBIT_CYCLES) == false) { + ok = false; + break; + } + + if (BUTTON_PRESS()) { + ok = false; + break; + } + } + + WDT_HIT(); + if (data_available()) { + ok = false; + break; + } + } + + Gpio_SSC_DOUT_High(); + FpgaWriteConfWord(FPGA_MAJOR_MODE_LF_ADC); + Gpio_SSC_DOUT_Low(); // back to "listening" + return ok; +} + +void SimulateCotag(const lf_cotagsim_t *params, bool ledcontrol) { + + LFSetupFPGAForADC(LF_COTAG_DIVISOR, false); + FpgaWriteConfWord(FPGA_MAJOR_MODE_LF_ADC); + gpio_fpga_mod_only_setup(); + Gpio_SSC_DOUT_Low(); + + BigBuf_free(); + BigBuf_Clear_ext(false); + + uint8_t *half_bits = BigBuf_calloc(COTAG_TX_MAX_HALFBITS); + if (half_bits == NULL) { + if (g_dbglevel >= DBG_ERROR) DbpString("SimulateCotag: failed to allocate buffer"); + FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); + return; + } + + uint8_t nbits = MIN(params->nbits, 8 * LF_COTAG_SIM_DATA_LEN); + + // Precompute the Manchester halfbits to send + uint16_t nhb = cotag_build_halfbits(half_bits, params->data, nbits); + + const uint32_t settle = COTAG_SETTLE_US * 12 / (LF_COTAG_DIVISOR + 1); + + cotag_detect_val_t val; + + while (cotag_activation_sequence_detect(params, &val)) { + if (nbits) { + if (ledcontrol) LED_C_ON(); + + // Transmit frame + bool ok = cotag_tx_frame(half_bits, nhb, params->repeats, params->hb_delay_us); + + if (ledcontrol) LED_C_OFF(); + + if (ok == false) { + break; + } + + // Done transmitting. Back to our RX sampling divisor: + FpgaSendCommand(FPGA_CMD_SET_DIVISOR, LF_COTAG_DIVISOR); + + cotag_skip_samples(settle); + } + + // Debug print only after done TXing frame + if (params->verbose || nbits == 0) { + Dbprintf("COTAG reader activation sequence detected (rise-to-rise " _YELLOW_("%u") " us, threshold %d)", + val.dt * (LF_COTAG_DIVISOR + 1) / 12, val.thr); + } + } + + FpgaWriteConfWord(FPGA_MAJOR_MODE_OFF); + if (ledcontrol) LEDsoff(); + BigBuf_free(); +} + /* * EM4305 support */ diff --git a/armsrc/lfops.h b/armsrc/lfops.h index 6e34c7b00..e8472846a 100644 --- a/armsrc/lfops.h +++ b/armsrc/lfops.h @@ -79,6 +79,7 @@ void EM4xProtectWord(uint32_t data, uint32_t pwd, uint8_t usepwd, bool ledcontro void cotag_start_pulse(void); void Cotag(uint32_t arg0, bool ledcontrol); +void SimulateCotag(const lf_cotagsim_t *params, bool ledcontrol); void setT55xxConfig(uint8_t persist, const t55xx_configurations_t *c); t55xx_configurations_t *getT55xxConfig(void); void printT55xxConfig(void); diff --git a/client/src/cmdlfcotag.c b/client/src/cmdlfcotag.c index 4b1b25822..0703836ff 100644 --- a/client/src/cmdlfcotag.c +++ b/client/src/cmdlfcotag.c @@ -38,12 +38,15 @@ #define XSTR(x) #x #define STR(x) XSTR(x) -#define LF_COTAG_DIVISOR 90 // 132 kHz #define LF_COTAG_CLOCK 768 #define LF_COTAG_DATA_LEN 128 #define LF_COTAG_DEF_SAMPLES_READ 500000 +#define LF_COTAG_SIM_FLOOR 128 +#define LF_COTAG_SIM_MIN_BYTES 8 +#define LF_COTAG_SIM_MAX_REPEATS 16 + static int CmdHelp(const char *Cmd); static int demod_cotag(int32_t *samples, int num_samples, int clock, int clock_start, double threshold, bool verbose); @@ -540,10 +543,85 @@ static int CmdCOTAGReader(const char *Cmd) { return ret; } +static int CmdCOTAGSim(const char *Cmd) { + CLIParserContext *ctx; + CLIParserInit(&ctx, "lf cotag sim", + "Enables simulation of a COTAG tag.\n" + "Simulation runs until the button is pressed or another USB command is issued.\n" + "Without " _YELLOW_("`-r`") _CYAN_(" it only reports detected reader activation sequence.\n") + _CYAN_("Note: COTAG tag full repeating sequence is 128 bits, but readers seem to care about LSB 64 bits only."), + "lf cotag sim --> only detect the reader\n" + "lf cotag sim -r A37E0D37000C1337 --> send 64 bits\n" + "lf cotag sim -r 0000000000000002A37E0D37000C1337\n" + "lf cotag sim --floor 32 --> more sensitive, for a reader further away\n" + "lf cotag sim -r A37E0D37000C1337 --hb_delay_us 2909 --> time the half bit instead of counting cycles\n" + ); + + void *argtable[] = { + arg_param_begin, + arg_str0("r", "raw", "", "raw data to answer with, " STR(LF_COTAG_SIM_MIN_BYTES) " to " STR(LF_COTAG_SIM_DATA_LEN) " bytes (64-128 bits)"), + arg_int0("n", "repeat", "<1-" STR(LF_COTAG_SIM_MAX_REPEATS) ">", "repeat data frame this many times per answer (def 1)"), + arg_int0(NULL, "floor", "<16-65535>", "minimum rise threshold (def " STR(LF_COTAG_SIM_FLOOR) ")"), + arg_int0(NULL, "hb_delay_us", "<2900-2918>", "custom half bit delay [us] (should be around 2909)"), + arg_lit0("v", "verbose", "report detected reader activation sequence while answering"), + arg_param_end + }; + CLIExecWithReturn(ctx, Cmd, argtable, true); + + uint8_t data[LF_COTAG_SIM_DATA_LEN] = {0}; + int dlen = 0; + CLIGetHexWithReturn(ctx, 1, data, &dlen); + int repeats = arg_get_int_def(ctx, 2, 1); + int thr_floor = arg_get_int_def(ctx, 3, LF_COTAG_SIM_FLOOR); + int hb_delay = arg_get_int_def(ctx, 4, 0); + bool verbose = arg_get_lit(ctx, 5); + CLIParserFree(ctx); + + if (dlen && (dlen < LF_COTAG_SIM_MIN_BYTES || dlen > LF_COTAG_SIM_DATA_LEN)) { + PrintAndLogEx(ERR, "data must be %d to %d bytes (64 to 128 bits), got %d" + , LF_COTAG_SIM_MIN_BYTES, LF_COTAG_SIM_DATA_LEN, dlen); + return PM3_EINVARG; + } + + if (repeats < 1 || repeats > LF_COTAG_SIM_MAX_REPEATS) { + PrintAndLogEx(ERR, "repeat must be between 1 and %d", LF_COTAG_SIM_MAX_REPEATS); + return PM3_EINVARG; + } + + if (thr_floor < 16 || thr_floor > 0xFFFF) { + PrintAndLogEx(ERR, "floor must be between 16 and 65535"); + return PM3_EINVARG; + } + + if (hb_delay && (hb_delay < 2900 || hb_delay > 2918)) { + PrintAndLogEx(ERR, "hb_delay_us must be between 2900 and 2918 us"); + return PM3_EINVARG; + } + + lf_cotagsim_t payload = { + .thr_floor = (uint16_t)thr_floor, + .hb_delay_us = (uint16_t)hb_delay, + .nbits = (uint8_t)(dlen * 8), + .repeats = (uint8_t)repeats, + .verbose = verbose, + }; + memcpy(payload.data, data, dlen); + + PrintAndLogEx(INFO, "Simulating COTAG tag"); + if (dlen == 0) { + PrintAndLogEx(INFO, "No " _YELLOW_("`-r`") " given, detecting reader activation sequence only"); + } + + clearCommandBuffer(); + SendCommandNG(CMD_LF_COTAG_SIMULATE, (uint8_t *)&payload, sizeof(payload)); + return lfsim_wait_check(CMD_LF_COTAG_SIMULATE); +} + static command_t CommandTable[] = { {"help", CmdHelp, AlwaysAvailable, "This help"}, {"demod", CmdCOTAGDemod, AlwaysAvailable, "demodulate a COTAG tag"}, {"reader", CmdCOTAGReader, IfPm3Lf, "attempt to read and extract tag data"}, + {"sim", CmdCOTAGSim, IfPm3Lf, "simulate a COTAG tag"}, {NULL, NULL, NULL, NULL} }; diff --git a/doc/commands.json b/doc/commands.json index c9d10014f..d0428d74c 100644 --- a/doc/commands.json +++ b/doc/commands.json @@ -11760,6 +11760,27 @@ ], "usage": "lf cotag reader [-hv@] [-s ] [--divisor <19-255>]" }, + "lf cotag sim": { + "command": "lf cotag sim", + "description": "Enables simulation of a COTAG tag. Simulation runs until the button is pressed or another USB command is issued. Without `-r` it only reports detected reader activation sequence. Note: COTAG tag full repeating sequence is 128 bits, but readers seem to care about LSB 64 bits only.", + "notes": [ + "lf cotag sim -> only detect the reader", + "lf cotag sim -r A37E0D37000C1337 -> send 64 bits", + "lf cotag sim -r 0000000000000002A37E0D37000C1337", + "lf cotag sim --floor 32 -> more sensitive, for a reader further away", + "lf cotag sim -r A37E0D37000C1337 --hb_delay_us 2909 -> time the half bit instead of counting cycles" + ], + "offline": false, + "options": [ + "-h, --help This help", + "-r, --raw raw data to answer with, 8 to 16 bytes (64-128 bits)", + "-n, --repeat <1-16> repeat data frame this many times per answer (def 1)", + "--floor <16-65535> minimum rise threshold (def 128)", + "--hb_delay_us <2900-2918> custom half bit delay [us] (should be around 2909)", + "-v, --verbose report detected reader activation sequence while answering" + ], + "usage": "lf cotag sim [-hv] [-r ] [-n <1-16>] [--floor <16-65535>] [--hb_delay_us <2900-2918>]" + }, "lf destron clone": { "command": "lf destron clone", "description": "clone a Destron tag to a T55x7, Q5/T5555 or EM4305/4469 tag.", @@ -16785,8 +16806,8 @@ } }, "metadata": { - "commands_extracted": 935, + "commands_extracted": 936, "extracted_by": "PM3Help2JSON v1.00", - "extracted_on": "2026-09-27T19:09:12+00:00" + "extracted_on": "2026-10-11T13:59:06+00:00" } } diff --git a/doc/commands.md b/doc/commands.md index 5fb6c6f9d..180881561 100644 --- a/doc/commands.md +++ b/doc/commands.md @@ -1119,6 +1119,7 @@ Check column "offline" for their availability. |`lf cotag help `|Y |`This help` |`lf cotag demod `|Y |`demodulate a COTAG tag` |`lf cotag reader `|N |`attempt to read and extract tag data` +|`lf cotag sim `|N |`simulate a COTAG tag` ### lf destron diff --git a/include/pm3_cmd.h b/include/pm3_cmd.h index b1dbf03de..541d3b857 100644 --- a/include/pm3_cmd.h +++ b/include/pm3_cmd.h @@ -380,6 +380,20 @@ typedef struct { uint8_t data[]; } PACKED lf_hitag_t; +#define LF_COTAG_DIVISOR 90 +#define LF_COTAG_TX_DIVISOR 181 + +// For CMD_LF_COTAG_SIMULATE +#define LF_COTAG_SIM_DATA_LEN 16 // 128 bits, the longest repeating COTAG frame +typedef struct { + uint16_t thr_floor; // minimum rise threshold + uint16_t hb_delay_us; // optional custom half bit delay [us] + uint8_t repeats; // repeat data frame this many times per answer + bool verbose; + uint8_t nbits; // number of data bits + uint8_t data[LF_COTAG_SIM_DATA_LEN]; +} PACKED lf_cotagsim_t; + // For CMD_LF_SNIFF_RAW_ADC and CMD_LF_ACQ_RAW_ADC #define LF_SAMPLES_BITS 30 #define MAX_LF_SAMPLES ((((uint32_t)1u) << LF_SAMPLES_BITS) - 1) @@ -1092,6 +1106,7 @@ typedef struct { #define CMD_LF_VIKING_CLONE 0x0222 #define CMD_LF_T55XX_WAKEUP 0x0224 #define CMD_LF_COTAG_READ 0x0225 +#define CMD_LF_COTAG_SIMULATE 0x022B #define CMD_LF_T55XX_SET_CONFIG 0x0226 #define CMD_LF_SAMPLING_PRINT_CONFIG 0x0227 #define CMD_LF_SAMPLING_GET_CONFIG 0x0228