Proxmark3 EM4x70 位级命令协议剖析:任意 LF EM 命令抽象与 Trace 日志体系
2026/9/17 18:48:55 网站建设 项目流程

Proxmark3 EM4x70 位级命令协议剖析:任意 LF EM 命令抽象与 Trace 日志体系

【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3

本文基于 Proxmark3 仓库中的 arbitrary_lf_em_commands.md 展开,完整梳理 EM4x70(ID48/Megamos)低频芯片的六条位级命令序列、LIW/HEADER/ACK 等特殊时序处理的设计考量,并结合 armsrc/em4x70.c 源码说明"任意命令序列"抽象的实际落地:位流(bitstream)描述结构、三种应答等待模式、以及逐比特 trace 日志系统。读完本文,你将掌握 EM4x70 读写/认证/解锁的位级时序、命令参数含义,以及如何在 Proxmark3 上开启调试日志来验证每一次lf em 4x70命令实际收发比特。

1. 背景与设计目标

EM4x70 是低频(125 kHz)可编程 ID 芯片的总称,常见商品名为 ID48(IDCLASS 之外的 Megamos 系列)。Proxmark3 通过lf em 4x70命令族对它进行识别、写入、认证、PIN 操作和解锁。原实现中每个命令各自硬编码了"发送哪些比特、等多久、收多少比特",导致:

  • 命令与响应缺少完整日志,难以复现和排查问题;
  • 修改命令序列(如增加命令奇偶位)时缺乏回归验证手段;
  • 新增任意 LF EM 命令没有统一的测试入口。

arbitrary_lf_em_commands.md 明确了三个目标与五阶段方法论:

目标

  1. 改进lf em命令及其响应的日志记录;
  2. 提高命令序列的确定性;
  3. 使新命令的测试更简单。

方法论

  1. 记录现有代码实际使用的命令序列;
  2. 记录现有日志 API;
  3. 将少量对时序敏感的函数定义为抽象;
  4. 实现这些抽象;
  5. 加入日志。

本文第 2~3 节对应"记录命令序列",第 4~5 节对应"抽象定义与实现",第 6 节对应"日志 API 与 trace 验证",第 7 节给出客户端侧的完整用法。

2. EM4x70 的六条命令序列

文档首先给出结论:现有代码实际只使用六条命令序列。命令 ID 定义(见 armsrc/em4x70.c#L95-L100):

#define EM4X70_COMMAND_ID 0x01 #define EM4X70_COMMAND_UM1 0x02 #define EM4X70_COMMAND_AUTH 0x03 #define EM4X70_COMMAND_PIN 0x04 #define EM4X70_COMMAND_WRITE 0x05 #define EM4X70_COMMAND_UM2 0x07

这些 ID 来自 EM4170 数据手册。需要特别注意命令奇偶位(parity)问题:某些芯片版本要求命令后跟一个偶校验位,某些则不要求,因此命令实际只保留最低三位(掩码0x07)。源码中为每个命令同时列出了两种编码:

// // w/o parity with parity #define EM4X70_COMMAND_ID 0x01 // 0b0001 --> 0b001'1 #define EM4X70_COMMAND_UM1 0x02 // 0b0010 --> 0b010'1 #define EM4X70_COMMAND_AUTH 0x03 // 0b0011 --> 0b011'0 #define EM4X70_COMMAND_PIN 0x04 // 0b0100 --> 0b100'1 #define EM4X70_COMMAND_WRITE 0x05 // 0b0101 --> 0b101'0 #define EM4X70_COMMAND_UM2 0x07 // 0b0111 --> 0b111'1

芯片型号差异(从 armsrc/em4x70.c#L102-L110 注释可确认):

  • 命令 ID 与行为在 EM4170 与 V4070/EM4070 上相同;
  • V4070/EM4070 不支持 PIN 命令、不能读 UM2,且 WRITE 只限 block 0..9(其 10 个块可能是 OTP);
  • EM4170 增加了 PIN 与 UM2,共 16 个块(include/em4x70.h#L26-L30 定义EM4X70_NUM_BLOCKS 16、PIN 低字地址 10、高字地址 11)。

以下逐条给出文档记录的命令序列表("RM" 是 Reader/Modulation 前导位,"LIW" 是 Listen Window 收听窗口,"HEADER" 是 16 位响应头0b1111'1111'1111'0000)。

2.1 ID 命令(读取 32 位卡号)

等待LIW,在下一个LIW开始时发送:

sourcebitscomment
tagLIWlisten window sync
reader0b00RM
reader0b001CMD
reader0b1command parity bit
tagHEADERHEADER (0b1111'1111'1111'0000)
tag32-bitsID (D31..D0)
tagLIWtag 回到待命,可接收下一条命令

2.2 UM1 命令(读取 User Memory 1)

sourcebitscomment
tagLIWlisten window
reader0b00RM
reader0b010CMD
reader0b1command parity bit
tag16-bitsHEADER
tag32-bitsUM1 data
tagLIWtag 回到待命

UM1 的 32 位中高位包含锁定位(Lockbit 0/1),Proxmark3 用它判断卡是否 LOCKED。

2.3 UM2 命令(读取 User Memory 2)

sourcebitscomment
tagLIWlisten window
reader0b00RM
reader0b111CMD
reader0b1command parity bit
tag16-bitsHEADER
tag64-bitsUM2 data
tagLIWtag 回到待命

2.4 Auth 命令(加密认证)

sourcebitscomment
tagLIWlisten window
reader0b00RM
reader0b011CMD
reader0b0command parity bit
reader56-bitsRN(读者随机数)
reader7-bitsTdiv == 0b0000000(恒为零)
reader28-bitsf(RN)(密钥派生值)
tag16-bitsHEADER
tag20-bitsg(RN)(标签应答)
tagLIWtag 回到待命

这是唯一带长随机载荷的读取命令:发送共 95 位(不含 RM),接收 20 位 g(RN),认证算法见 doc/md/em4x70/lf_em4x70_trace_notes.md 中的完整 trace 对照。

2.5 Write Word 命令(写入一个 16 位块)

sourcebitscomment
tagLIWlisten window
reader0b00RM
reader0b101CMD
reader0b0command parity bit
reader4-bits目标地址/块
reader1-bit地址奇偶位
reader25-bits5x5 数据(含行、列奇偶)
tagACK等待 (TWA) 后出现第一个 ACK
tagACK等待 (WEE) 后出现第二个 ACK
tagLIWtag 回到待命

数据部分是 16 位有效数据按 5 行 × 5 列排布:每个 4 位半字节后跟 1 位行奇偶(共 4×5=20 位),再加 4 位列奇偶和 1 位填充 0,合计 25 位。文档中给出的等待逻辑为:

WaitTicks(EM4X70_T_TAG_TWA); if (check_ack()) { WaitTicks(EM4X70_T_TAG_WEE); if (check_ack()) { return PM3_SUCCESS; } }

这与当前 send_bitstream_wait_ack_wait_ack() 的实现完全一致:先等 TWA(写访问时间)查第一次 ACK,再等 WEE(EEPROM 写时间)查第二次 ACK。

2.6 PIN 命令(设置/验证 PIN 解锁)

sourcebitscomment
tagLIWlisten window
reader0b00RM
reader0b100CMD
reader0b1command parity bit
reader32-bits标签 ID
reader32-bitsPIN
tagACK等待 (TWALB) 后出现 ACK
tagHEADERDELAYED (TWEE) 后出现 HEADER
tag32-bits标签 ID(回读)
tagLIWtag 回到待命

注意 PIN 命令要求先通过 ID 命令读回标签 ID再填充发送序列——这正是em4x70_unlockem4x70_write_pin入口函数里先调用em4x70_read_id()的原因(见 armsrc/em4x70.c#L1523-L1555)。

3. 时序常量与 tick 体系

文档将 LIW、ACK、HEADER 列为"需要特殊处理"的三类对象,其核心都是时序。源码把数据手册中的时间全部换算为 tick(见 armsrc/em4x70.c#L54-L83):

// 1 us = 1.5 ticks // 1 RF Period (FC) = 8 us = 12 Ticks #define TICKS_PER_FC 12 #define EM4X70_T_TAG_QUARTER_PERIOD (8 * TICKS_PER_FC) #define EM4X70_T_TAG_HALF_PERIOD (16 * TICKS_PER_FC) #define EM4X70_T_TAG_THREE_QUARTER_PERIOD (24 * TICKS_PER_FC) #define EM4X70_T_TAG_FULL_PERIOD (32 * TICKS_PER_FC) // 1 Bit Period #define EM4X70_T_TAG_TWA (128 * TICKS_PER_FC) // Write Access Time #define EM4X70_T_TAG_DIV (224 * TICKS_PER_FC) // Divergency Time #define EM4X70_T_TAG_AUTH (4224 * TICKS_PER_FC) // Authentication Time #define EM4X70_T_TAG_WEE (3072 * TICKS_PER_FC) // EEPROM write Time #define EM4X70_T_TAG_TWALB (672 * TICKS_PER_FC) // Write Access Time of Lock Bits #define EM4X70_T_TAG_BITMOD (4 * TICKS_PER_FC) // 发送 0 时先停调制的时长 #define EM4X70_T_TAG_TOLERANCE (8 * TICKS_PER_FC) // 接收/LIW 容差 #define EM4X70_T_DELAY_FROM_LIW_TO_RM (72 * TICKS_PER_FC) // 从 LIW 到发送 RM 的默认延迟 #define EM4X70_T_PULSES_TO_SEARCH_FOR_LIW 50 // 搜索收听窗口的脉冲数 #define EM4X70_COMMAND_LIW_SEARCH_RETRIES 5 // 发送/读取命令的重试次数 #define EM4X70_MAX_SEND_BITCOUNT 96u // Auth 最长发送序列 #define EM4X70_MAX_RECEIVE_BITCOUNT 64u // 最长接收(不含 16 位 HEADER)

要点:1 bit 周期 = 32 个 RF 周期 = 256 µs,即约 3906 bit/s;所有WaitTicks()的等待都基于这套换算,EM4X70_T_TAG_TOLERANCE提供 ±8 RF 周期的脉宽容差,保证接收判定在不同天线耦合条件下仍然稳定。

4. 抽象设计:从"特殊处理清单"到位流引擎

文档"Abstraction required"一节列出需要抽象的六类要素:

  • bits to send:待发送比特数 + 存储这些比特的缓冲区;
  • bits to receive:期望接收的比特数 + 接收缓冲区;
  • LIW:同步下一条命令的特殊处理;
  • ACK:等待 ACK 的特殊处理;
  • HEADER:等待 HEADER 的特殊处理;
  • DELAY:处理下一项之前延迟的 tick 数。

当前实现正是围绕这六类要素组织,核心数据结构是"命令位流"(armsrc/em4x70.c#L528-L568):

typedef struct _em4x70_bitstream_t { uint8_t bitcount; // 发送=要发的位数;接收=期望的位数 uint8_t one_bit_per_byte[EM4X70_MAX_BITSTREAM_BITS]; // 一字节存一位,避免时序敏感代码里做位移 } em4x70_bitstream_t; typedef struct _em4x70_command_bitstream { uint8_t command; // 三位命令值,用于选择收发处理函数 em4x70_bitstream_t to_send; em4x70_bitstream_t to_receive; uint8_t received_data_converted_to_bytes[...]; // 接收比特转字节(逆序存储) } em4x70_command_bitstream_t;

设计上有两点值得注意:

  1. 一字节一位one_bit_per_byte的注释说明,这是在时序敏感代码中刻意避免位移运算、让发送/接收路径尽可能简单的取舍;
  2. 位流可预生成em4x70_command_generators_t是一组函数指针表(id/um1/um2/auth/pin/write 各一个生成器),在发送前就把整条待发送位流构造完毕。文档中的判断得到印证:"只需要定义三种与标签交互的序列,而且读者可以在发出任何比特之前预先生成整条位流"。

三种交互模式(对应文档中"只有三种交互序列"的结论):

模式函数适用命令行为
发后直接读send_bitstream_and_read()ID / UM1 / UM2 / AUTH发送位流后立即同步 HEADER 并读取 N 位
发—等 ACK—等 HEADER—读send_bitstream_wait_ack_wait_read()PIN等 TWALB 查 ACK,再等 WEE 同步 HEADER 读 32 位
发—等 ACK—等 ACKsend_bitstream_wait_ack_wait_ack()WRITE等 TWA 查 ACK,再等 WEE 查第二个 ACK,不接收数据

各生成器都带参数校验,例如 create_legacy_em4x70_bitstream_for_cmd_write() 校验地址必须只有低 4 位有效,并强制发送位数为 34 位(不含 RM);AUTH 生成器 强制 95 位(命令 4 + RN 56 + Tdiv 7 + f(RN) 28),任何构造错误都会打INTERNAL ERROR日志并返回失败,而不是发出半条命令。send_bitstream_and_read()还特别处理了 AUTH 收到 20 位(非字节整数倍)的编码问题:向上取整到 24 位解码,保持与客户端既有行为兼容(armsrc/em4x70.c#L677-L692)。

5. LIW、HEADER、ACK 的特殊处理

文档指出这三类对象不能按普通比特流处理,原因和实现如下。

5.1 LIW:同步而非数据

LIW 是标签发射的"收听窗口"长脉冲,读者必须在标签调制器开启的窗口内发送。find_listen_window() 通过脉宽特征识别它:连续两个 80 FC(64+16)的上升沿脉冲、一个 96 FC 的下降沿脉冲、一个 64 FC 的下降沿脉冲。识别成功后:

  1. WaitTicks(EM4X70_T_DELAY_FROM_LIW_TO_RM)—— 等 72 个 RF 周期(源码注释记录实测 24~40 FC 也能成功,取 72 作为默认);
  2. find_listen_window直接发出两位 RM00
  3. 调用方 send_bitstream_internal() 在"TIMING SENSITIVE SECTION"内以最小延迟逐位发送,失败则最多重试EM4X70_COMMAND_LIW_SEARCH_RETRIES(5)次——注意重试的只是找 LIW,不重发命令

5.2 HEADER:带过渡失配的同步头

HEADER 是 12 个1加 4 个0。文档特别提醒:等待 HEADER 时可能因标签离场而长时间收不到脉冲(如 PIN 命令),且"读取 HEADER 时可能漏掉过渡期间的最初几位,因此必须特殊处理"。em4x70_receive() 的处理是:先跳过约半个头(WaitTicks(6 * EM4X70_T_TAG_FULL_PERIOD),容忍起始噪声),然后寻找1→0过渡(1.5 个全周期的脉冲)来同步,再消费剩余 3 个0,之后按脉宽解码数据:

  • 脉宽 1 个全周期 → 1 位;
  • 脉宽 1.5 个全周期 → 两位相同值 + 翻转边沿检测方向;
  • 脉宽 2 个全周期 → 两位互补值。

这正是 Manchester 编码的位恢复过程,EM4X70_T_TAG_TOLERANCE保证 ±8 FC 内仍正确归类;遇到 LIW 长脉冲或非法脉宽即结束接收并返回已收位数。

5.3 ACK:当前仍是"延迟时间"而非"超时等待"

文档对此的评述值得保留:"ACK目前是一个 time-to-delay。它应该改为等待 ACK 的最大时间吗?当前如果无卡在场,check_ack()没有超时,可能长时间坐等。" 当前 check_ack() 按脉宽特征判别:连续两个 2 全周期(64+64 FC)的下降沿脉冲判为 ACK,其余视为 NAK/LIW;源码中也留有 TODO:"Add similar function that will wait for an ACK/NAK up to a given timeout"。也就是说文档中提出的改进方向(带超时的 ACK 等待)在仓库中仍属于已知待办,读者在分析"写命令偶发卡住"类问题时可以把这一点纳入考虑。

6. 位级日志与 Trace 对照验证

文档的目标之一是"能轻松测试新 LF 命令",配套手段就是全量比特日志。实现分两层:

记录层em4x70_log_t结构记录每次传输/接收的start_tick / end_tick / bits_used / bit[](armsrc/em4x70.c#L387-L398)。发送路径中em4x70_send_bit()唯一实际切换调制的函数,每个比特的起止 tick 与值在此被记录;接收路径在em4x70_receive()首末比特处打点。

输出层log_dump()按调试级别输出,格式与 lf_em4x70_trace_notes.md 中的"General format of the output"一致:

[#] sent >>>: [ 17169 .. 19545 ] ( 2376 ) 6 bits: 000001 ^^^^^^^^ ^^^^^^ .. ^^^^^^ ^^^^^^ ^^ ^^^^^ direction 首个比特的 tick 末个比特的 tick (END-START) 比特数 实际比特

日志级别通过客户端命令hw dbg -N调节(armsrc/em4x70.c#L29-L47 的DPRINTF_*宏族;开发时也可在编译期把FORCE_ENABLE_LOGGING置为true)。另外bitstream_dump()会打印"计划发送/接收"的位流,与实际日志格式刻意保持一致,便于肉眼比对"应发"与"实发"。

6.1 Trace 笔记揭示的历史问题

lf_em4x70_trace_notes.md 逐条记录了各lf em 4x70子命令在无--par/ 有--par时的实际发送位,并拆解出 RM/CMD/Addr/Data 各字段。其中两条"潜在 bug"记录非常典型:

  1. FRN 末四位恒为 0xF 而非 0xC(已修复):现象是auth命令的 f(RN) 最低半字节始终发0b1111。根因是日志缓冲区太小——由于包含 2 位 RM,auth 需要 98 位容量。这条说明 trace 系统的价值:它发现的是观测链路缺陷而非协议缺陷;
  2. 命令奇偶位应用不一致(记录在案):trace 显示 ID/UM1/UM2 无校验位,而 WRITE/AUTH/PIN 恒带第 5 校验位。这与当前代码状态吻合:legacy_*生成器把"命令+奇偶位"固化进常量(如 ID 发0x3 = 0b001'1、AUTH 发0x6 = 0b011'0),而--par选项已被标记为弃用并忽略(armsrc/em4x70.c#L48-L49 的g_deprecated_command_parity恒被置false,各入口函数对--par都会打印 "non-functional" 警告)。

该笔记还记录了一个实操细节:setpin/unlock/setkey的第一条 ID 命令在旧代码中曾按 3 位命令+校验位发出,会被标签误解为 AUTH 命令而拒绝。修改lf em 4x70相关代码后,用同一份 trace 做回归比对("确保没有改变实际发送/接收内容")就是文档方法论的收尾验证步骤。

7. 客户端命令与实战

客户端解析与帮助文本位于 client/src/cmdlfem4x70.c,常用子命令(示例摘自源码帮助文本):

lf em 4x70 info lf em 4x70 write -b 15 -d c0de # 写 'c0de' 到块 15 lf em 4x70 auth --rnd 7D5167003571F8 --frn 982DBCC0 # autorecovery 测试密钥 lf em 4x70 setpin -p 11223344 lf em 4x70 unlock -p 11223344 lf em 4x70 setkey -k 022A028C02BE000102030405 # autorecovery 测试密钥 lf em 4x70 brute -b 7 --rnd 7D5167003571F8 --frn 982DBCC0 # 爆破 16 位部分密钥 lf em 4x70 recover ... # 由部分密钥位恢复完整密钥

其中三组公开测试密钥可用于复现文档全部 trace:pm3 测试密钥F32AA98CF5BE4ADFA6D3480B、research paper 密钥A090A0A02080000000000000、autorecovery 测试密钥022A028C02BE000102030405(对应--rnd/--frn组合见 client/src/cmdlfem4x70.c 帮助文本)。

写键与写 PIN 的流程约束(从 armsrc/em4x70.c 的em4x70_write_key/em4x70_write_pin入口可确认):

  • setkey:先read_id探活,再依次写 block 9→4 共 6 个字,任一步失败即中止;
  • setpin:先read_id,依次写 PIN 高字(block 11)、低字(block 10),然后立即用新 PIN 发送 PIN 命令验证,最后回读 UM1/UM2;
  • unlock:先read_id(PIN 命令依赖 ID),发 PIN 命令,成功后回读 UM1/UM2;
  • write:写入成功后自动回读 ID/UM1/UM2,客户端据此打印分块信息表与锁定位状态。

做回归测试前,可参考 lf_em4x70_trace_notes.md 的"Initialization of the tag"脚本把卡恢复到已知状态(UM2 全AAAA、PIN/KEY 全AAAAAAAA、固定 ID78B8E012、UM121DF5678解锁态),关键顺序约束是最后写 block 1(UM1 锁定位所在块,先写数据块再写 UM1):

lf em 4x70 write -b 15 -d AAAA lf em 4x70 write -b 14 -d AAAA lf em 4x70 write -b 13 -d AAAA lf em 4x70 write -b 12 -d AAAA lf em 4x70 write -b 11 -d AAAA lf em 4x70 write -b 10 -d AAAA lf em 4x70 write -b 9 -d AAAA lf em 4x70 write -b 8 -d AAAA lf em 4x70 write -b 7 -d AAAA lf em 4x70 write -b 6 -d AAAA lf em 4x70 write -b 5 -d AAAA lf em 4x70 write -b 4 -d AAAA lf em 4x70 write -b 3 -d 78B8 lf em 4x70 write -b 2 -d E012 lf em 4x70 write -b 0 -d 5678 lf em 4x70 write -b 1 -d 21DF

8. 小结

arbitrary_lf_em_commands.md 的价值在于把 EM4x70 驱动中"隐性的时序知识"显性化:六条命令序列的位级结构、LIW/HEADER/ACK 三类特殊对象、以及"预生成位流 + 三种交互模式"的抽象边界。对照 armsrc/em4x70.c 可以看到这套抽象已经完整落地——位流生成器负责构造与校验、find_listen_window/em4x70_receive/check_ack负责时序敏感段、log_dump/bitstream_dump负责全比特 trace。对要扩展新 LF 命令或排查偶发通信失败的读者,建议的工作路径是:先按第 2 节表格核对协议结构,再打开hw dbg高调试级别用第 6 节的日志格式比对"应发/实发",最后用第 7 节的初始化脚本保证每次测试从同一已知状态出发。

【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询