Proxmark3(Iceman Fork)MFU 二进制 Dump 格式解析:新旧两种二进制布局、Plain 格式与 JSON 格式详解
【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3
本篇基于 Proxmark3 仓库中的doc/mfu_binary_format_notes.md,完整梳理 MIFARE Ultralight / NTAG(下称 MFU)标签 dump 文件的三代二进制格式(New / Old / Plain)以及面向外部工具的 JSON 格式。读完本文,你将能够独立解析 PM3 dump 出的 MFU 二进制文件:识别 56 字节新头部与各字段偏移、区分三种格式的自动检测原理(BCC 校验)、理解 PACK/计数器/撕裂标志在内存中的归位逻辑,以及编写兼容 PM3 格式转换逻辑的工具。
背景:为什么 MFU dump 需要“头部 + 内存数据”两种结构
PM3 读取 MFU 家族标签(MIFARE Ultralight、Ultralight EV1、NTAG 2xx 系列等)时,除了可读的页内存,还有一批“标签元数据”对模拟真实标签至关重要:
- PACK:标签密码。PM3 dump 时可以拿到 pwd/pack,但 PACK 在标签上是不可读的内存区域。PM3 的做法是把它写回它在标签内存中的“正常位置”,因此 dump 文件中的内存并非严格逐字节的真实内存,而是“假如所有内存都可读,它本应是什么样”(What it should have looked like)。
- 计数器与撕裂标志(counter / tearing byte):不同厂商的标签能力不一致——例如 UL-EV1 有 3 组计数器和撕裂字节,而 NTAG 只有 1 组。这正是新一代二进制格式诞生要解决的问题:用统一的“最大能力”布局容纳不同厂商的差异。
- signature:UL-EV1 等标签返回的 32 字节签名,用于模拟时的身份还原。
围绕这些字段,MFU dump 格式经历了 Plain → Old → New 的演进,并面向 libnfc 等外部工具生态推荐了 JSON 格式。三代二进制格式在当前代码中仍然全部支持,由客户端在读入 dump 时自动检测并转换。
三代格式总览
| 格式 | 前缀长度 | 定义位置 | 说明 |
|---|---|---|---|
| New mfu format | 56 字节(MFU_DUMP_PREFIX_LENGTH) | include/mifare.h | 当前格式,PACK 已并入数据区 |
| Old mfu format | 48 字节(OLD_MFU_DUMP_PREFIX_LENGTH) | client/src/cmdhfmfu.h | 头部独立保存 tearing/pack,仅用于转换 |
| Plain mfu format | 0 字节 | client/src/fileutils.c | 纯内存 dump,无任何元数据 |
| JSON(future) | 不适用 | doc/mfu_binary_format_notes.md | 面向外部应用/工具的推荐格式 |
相关全局常量定义在 client/src/cmdhfmfu.h:
#define MFU_BLOCK_SIZE 0x04 // MFU 页大小,4 字节 #define MFU_MAX_BLOCKS 0xFF #define MFU_MAX_BYTES (MFU_MAX_BLOCKS * MFU_BLOCK_SIZE) // 1024MFU 的“块”即页(page),每页 4 字节;data[1024]对应最多 256 页。
New mfu format:当前二进制格式
定义见 include/mifare.h(注释与文档一致:长度必须对齐到 4 字节的 UL/NTAG 页):
// New Ultralight/NTAG dump file format // Length must be aligned to 4 bytes (UL/NTAG page) #define MFU_DUMP_PREFIX_LENGTH 56 typedef struct { uint8_t version[8]; uint8_t tbo[2]; uint8_t tbo1[1]; uint8_t pages; // max page number in dump uint8_t signature[32]; uint8_t counter_tearing[3][4]; // 3 bytes counter, 1 byte tearing flag uint8_t data[1024]; } PACKED mfu_dump_t;字段偏移逐段说明:
| 偏移 | 长度 | 字段 | 说明 |
|---|---|---|---|
| 0 | 8 | version | 标签 Version 页数据(页 4 的可读部分 + 固定位),NTAG 上对应页 3 版本信息 |
| 8 | 2 | tbo | Tearing Bytes(0) |
| 10 | 1 | tbo1 | Tearing Bytes(1) |
| 11 | 1 | pages | dump 覆盖的最大页号(0 起)。文件总长 =56 + (pages+1) * 4字节 |
| 12 | 32 | signature | UL-EV1 签名 |
| 44 | 12 | counter_tearing[3][4] | 3 组“3 字节计数器 + 1 字节撕裂标志”;无此能力的标签对应组为空/零 |
| 56 | ≤1024 | data | 页内存数据(对齐到 4 字节),PACK 位于其最后一页的末尾 |
设计要点(对应文档 "New mfu format" 一节):
- PACK 被移出头部。它只是标签内存的一部分(只是不可读),PM3 在 dump 时若有 pwd/pack,就直接写入它在内存中的正常位置(末页末尾 2 字节)。这样头部不再携带“特殊位置”的字段,语义统一。
- 头部尺寸由“最大能力”决定。
counter_tearing[3][4]按 UL-EV1 的 3 组计数器设计,NTAG 只用其中 1 组即可,从而“补偿不同厂商标签功能的差异”。 data声明为 1024 字节(256 页),但实际文件长度按pages字段裁剪,m页的 dump 文件共56 + (m+1) * 4字节。
客户端打印 dump 时也会显示该头部尺寸,见 client/src/cmdhfmfu.c 中mfu_print_dump()的Header size..... 56 bytes输出。
Old mfu format:48 字节头部,仅保留用于转换
定义见 client/src/cmdhfmfu.h:
// Old Ultralight/NTAG dump file format // It is used only for converting #define OLD_MFU_DUMP_PREFIX_LENGTH 48 typedef struct { uint8_t version[8]; uint8_t tbo[2]; uint8_t tearing[3]; uint8_t pack[2]; uint8_t tbo1[1]; uint8_t signature[32]; //uint8_t counter[3]; uint8_t data[1024]; } PACKED old_mfu_dump_t;与 New 格式的差异(字段偏移对照):
| 偏移(Old) | 长度 | 字段 | 在新格式中的去向 |
|---|---|---|---|
| 0 | 8 | version | 原样拷贝 |
| 8 | 2 | tbo | 原样拷贝 |
| 10 | 3 | tearing | 拆散放入counter_tearing[i][3] |
| 12 | 2 | pack | 移入数据区末页末尾(见下方转换代码) |
| 14 | 1 | tbo1 | 原样拷贝 |
| 15 | 32 | signature | 原样拷贝 |
| 48 | ≤1024 | data | 原样拷贝 |
文档指出旧格式保存这些额外数据的目的是让 Proxmark3 能够模拟(simulate)真实标签;注意结构体中被注释掉的counter[3],说明旧格式根本没有计数器概念,转换时计数器组保持零值。
旧格式 → 新格式的自动转换
转换实现位于 client/src/fileutils.c 的convert_old_mfu_dump(),关键动作:
size_t old_data_len = *dumplen - OLD_MFU_DUMP_PREFIX_LENGTH; size_t new_dump_len = old_data_len + MFU_DUMP_PREFIX_LENGTH; ... for (int i = 0; i < 3; i++) { mfu_dump->counter_tearing[i][3] = old_mfu_dump->tearing[i]; } memcpy(mfu_dump->data, old_mfu_dump->data, sizeof(mfu_dump->data)); mfu_dump->pages = old_data_len / 4 - 1; // Add PACK to last block of memory. memcpy(mfu_dump->data + (mfu_dump->pages * 4 + MFU_DUMP_PREFIX_LENGTH), old_mfu_dump->pack, 2);即:3 个 tearing 字节分散写入三组counter_tearing[i][3](计数器本身为 0);PACK 按新格式约定写入数据区最后一页的最后 2 字节;pages由旧数据长度重新计算。转换后文件长度增加 8 字节(56 − 48)。
Plain mfu format:最原始的纯内存 dump
文档 "Plain mfu format" 一节说明,MFU 最早的二进制格式就是标签从第 0 页到末页的直接内存转储,没有任何头部:
uint8_t data[1024];Plain 格式没有任何元数据(无 version、signature、计数器、PACK),无法完整还原标签身份。PM3 读入时会把它包装成 New 格式,实现见 client/src/fileutils.c 的convert_plain_mfu_dump():
memcpy(mfu->data, *dump, *dumplen); mfu->pages = *dumplen / 4 - 1; *dump = (uint8_t *)mfu; *dumplen += MFU_DUMP_PREFIX_LENGTH;即分配一个全零的mfu_dump_t,把原始数据搬进data,头部其余字段留零。
格式自动检测:BCC 校验如何区分三种布局
三种二进制格式没有魔数(magic number),区分布局只能靠数据本身。PM3 的检测函数是 client/src/fileutils.c 的detect_mfu_dump_format(),策略是依次假设数据区起点,用 MFU 页 0/1 中 UID 的 BCC 校验字节验证假设是否成立:
uint8_t ct = 0x88; // UID BCC 常数 // detect new:假设页 0 位于偏移 56 bcc0 = ct ^ new->data[0] ^ new->data[1] ^ new->data[2]; bcc1 = new->data[4] ^ new->data[5] ^ new->data[6] ^ new->data[7]; if (bcc0 == new->data[3] && bcc1 == new->data[8]) { retval = MFU_DF_NEWBIN; } ... // detect old:假设页 0 位于偏移 48,同一套 BCC 逻辑 // detect plain:假设页 0 位于偏移 0,同一套 BCC 逻辑原理:MFU 标签第 0 页是 UID 高 3 字节 + BCC,第 1 页是 UID 低 3 字节 + BCC。BCC 页(第 0 页第 3 字节)等于0x88 ^ UID[0..2],另一 BCC 等于UID[4..6]的异或。若按某一偏移解释内存时两处校验同时通过,则该偏移就是数据区起点。检测顺序为新 → 旧 → Plain,并返回MFU_DF_NEWBIN / MFU_DF_OLDBIN / MFU_DF_PLAINBIN枚举。
代码中还保留了一个厂商特例分支:NTAG I2C 1K/2K plus(SAK 0x00、ATQA44 00)的内存布局与常规 NTAG 不同,其页 7/8/9 为00 44 00特征,若命中则直接判定为新格式,见 client/src/fileutils.c。
统一入口convert_mfu_dump_format()(client/src/fileutils.c)在新格式命中时直接放行、旧/Plain 格式命中时执行对应转换,无法识别时返回错误——所以第三方生成的 MFU dump 只要 UID 区完整、BCC 正确,就能被 PM3 自动接受。
Future mfu format:面向外部工具的 JSON 格式
文档 "future mfu format" 一节明确给出结论:对于 libnfc 之类的外部应用与工具,不推荐使用二进制格式,而应采用 JSON,因为它对新标签功能的变化更灵活。示例(原文档原样继承):
{ "Created": "proxmark3", "FileType": "mfu", "Card": { "UID": "04F654CAFC388", "Version": "0004030101000B0", "TBO_0": "000", "TBO_1": "0", "Signature": "BC9BFD4B550C16B2B5A5ABA10B644A027B4CB03DDB46F94D992DC0FB02E0C3F", "Counter0": "00000", "Tearing0": "BD", "Counter1": "00000", "Tearing1": "BD", "Counter2": "00000", "Tearing2": "BD" }, "blocks": { "0": "04F6542", "1": "CAFC388", "2": "8E48000", "3": "E110120", "4": "0103A00", "5": "340300F", "6": "0000000", "7": "0000000", "8": "0000000", "9": "0000000", "10": "0000000", "11": "0000000", "12": "1122334", "13": "0000000", "14": "0000000", "15": "0000000", "16": "000000F", "17": "0005000", "18": "0000000", "19": "0000000" } }字段与二进制格式的对应关系:
Card.UID来自data的页 0–1(示例中04 F6 54+CA FC 38=04F654CAFC38);Card.Version对应version[8];TBO_0/TBO_1对应tbo/tbo1;Counter0..2+Tearing0..2对应counter_tearing[3][4]——示例中三组均给出,体现 JSON 用“可选字段”天然消化厂商差异:NTAG 标签省略 Counter1/2 即可,无需像二进制格式那样固定占位;blocks以页号为键、每页 4 字节十六进制为值,与二进制data区一一对应。
PM3 客户端在准备 JSON 文件时确实写入"FileType": "mfu"字段,见 client/src/fileutils.c 的prepareJSON()。
实战:解析与使用 MFU dump 文件的操作要点
- 判断格式:看文件大小。
(len - 56) % 4 == 0且 len > 56 优先按新格式;(len - 48) % 4 == 0按旧格式;两者皆否但能被 4 整除的按 Plain。严格判定则复现 BCC 检测逻辑(见上文)。 - 提取纯内存:新/旧格式跳过 56/48 字节前缀;
pages = (len - 56) / 4 - 1(新格式)。PM3 自身读入时同理,例如 client/src/cmdhfmfu.c 中pages = (bytes_read - MFU_DUMP_PREFIX_LENGTH) / MFU_BLOCK_SIZE。 - 提取/写回 PACK:位于
data区最后一页的最后 2 字节(mem->data + (bytes_read - MFU_DUMP_PREFIX_LENGTH - 2),见 client/src/cmdhfmfu.c)。模拟带密码标签时必须保留这 2 字节。 - 数据区页索引偏移:在把 dump 文件直接喂给模拟器内存接口时,注意页索引要平移
MFU_DUMP_PREFIX_LENGTH / MFU_BLOCK_SIZE = 14页(即 56 字节 / 4),该注释见 client/src/cmdhfmfu.c。 - 兼容旧文件:无需手工转换,PM3 的
pm3_load_dump()路径会经过convert_mfu_dump_format()自动把 Old/Plain 提升为 New 格式;但如果你自己写解析器,建议同时实现两套前缀偏移。
小结
- **New 格式(56 字节前缀,
mfu_dump_t)**是当前标准:PACK 归位内存、3 组计数器/撕裂标志容纳 UL-EV1 与 NTAG 的能力差异,pages字段决定文件裁剪长度; - **Old 格式(48 字节前缀,
old_mfu_dump_t)**头部独立保存tearing[3]/pack[2],现仅作为转换目标保留; - Plain 格式是零头部的纯内存转储,信息量最少;
- JSON 格式是面向外部生态(如 libnfc)的推荐方案,以可选字段的方式优雅处理厂商功能差异。
四者并非互斥:PM3 在读入时通过 UID 的 BCC 校验自动识别二进制布局并完成旧 → 新转换(client/src/fileutils.c),因此围绕该格式开发工具时,对齐mfu_dump_t的字段偏移并保留末页 PACK,即可获得与 PM3 客户端一致的兼容性。
【免费下载链接】proxmark3Iceman Fork - Proxmark3项目地址: https://gitcode.com/GitHub_Trending/pr/proxmark3
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考