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
This commit is contained in:
towelbyte committed 2026-10-14 17:14:56 +02:00
1 parent 7f4dfe7472
commit 294fb3c504
8 files changed
+363 -4

No files matched your search

+2 -1
View File
@@ -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)
+5
View File
@@ -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
+237
View File
@@ -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
*/
+1
View File
@@ -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);
+79 -1
View File
@@ -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", "<hex>", "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}
};
+23 -2
View File
@@ -11760,6 +11760,27 @@
],
"usage": "lf cotag reader [-hv@] [-s <dec>] [--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 <hex> 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 <hex>] [-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"
}
}
+1
View File
@@ -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
+15
View File
@@ -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