文章总结: 本文详细记录了小米安全芯片MJA1的黑盒逆向工程过程,通过硬件识别、I2C嗅探、固件逆向和指令暴力破解,揭示了其通信协议、指令集及CRC16校验算法,发现两个隐藏指令并指出模拟侧信道是潜在攻击面。 综合评分: 85 文章分类: 逆向分析,IoT安全,嵌入式安全,硬件安全,二进制安全
小米安全芯片MJA1黑盒探秘:从零开始逆向一款无文档加密芯片
幻泉之洲
2026年6月23日 05:19 北京
在小说阅读器读本章
去阅读
小米在自家摄像头里塞了一颗叫MJA1的安全芯片,号称能抗重放、抗中间人、抗暴力破解。问题是,这颗芯片没有任何公开文档。我们从硬件识别、I2C嗅探、固件逆向到指令暴力破解,一步步拆解了它的通信协议,找到两个隐藏指令,还顺手把它的CRC校验算法扒了下来。这颗芯片在软件协议层面确实做得挺扎实——但这只是前门,真正有趣的攻击面在芯片的模拟侧信道。
发现MJA1:一颗神秘的安全芯片
逛小米商城寻找新的研究目标时,一款智能摄像头的产品页上提到了MJA1安全芯片。
从宣传来看,这颗芯片为敏感数据和设备通信提供硬件级保护,能防重放攻击、中间人攻击、暴力破解,每颗芯片还有自己独有的私钥和证书。
但它是一颗私有芯片——网上找不到任何文档或数据手册。截止本文写作时,没有任何公开的研究成果,只在RoboCoffee的博客里见过一张照片。
没有任何公开资料,我们的目标就变成了:通过黑盒分析搞明白MJA1是怎么工作的,再评估它的安全性能。
锁定目标
要做测试,先得找个合适的目标设备。条件很简单:集成了MJA1芯片,价格合理,容易拆机,还能获取root权限。
最终选中的设备就不详说了,重点是它用的系统是Buildroot Linux,MJA1挂在一路I2C总线上。这给了我们一个干净的测试环境。
硬件侦察
拿到设备后要做几件事:确认MJA1在哪儿、知道它在哪条I2C总线上、搞清楚Linux用什么驱动跟它通信、抓到它的通信数据。
找到芯片和I2C总线
拆开设备,主板上最显眼的是一颗Marvell的88DE0013-BTK2 SoC,很常见。但旁边有颗小芯片,丝印MJA1——就是它。
接下来要确定MJA1挂在哪条I2C总线上。正常情况下,I2C设备要么在设备树里声明,要么由内核在启动时自动探测。但扫描/proc/device-tree里所有i2c节点,没找到任何跟mja1或xiaomi相关的字符串。用i2cdetect把所有总线扫一遍,也没扫到东西。
这有点奇怪。既然软件探测找不到,我们就直接用逻辑分析仪接到芯片引脚上抓信号。I2C需要两根线:数据线SDA和时钟线SCL。把分析仪的地线接到板子地之后,轮流触碰可疑引脚看波形。SCL是周期性的整齐方波,SDA的波形不规律,而且只在设备启动时才出现。
抓到波形后测频率——标准的100kHz I2C时钟。这就确认了,MJA1的通信走的是标准I2C协议,地址0x2A。Linux之所以探测不到,是因为内核里有段黑名单,启动时把这颗芯片屏蔽掉了。不过这不影响我们直接在用户空间操作I2C设备节点来跟它通信。
驱动分析
接下来找主机端是什么程序在跟MJA1对话。我们从系统里的miio_client服务入手。miio_client负责连接小米云服务,同时也要用MJA1做身份认证,它通过/dev/mjac这个设备节点跟芯片通信。
顺藤摸瓜找到内核模块mjac_driver.ko。逆向发现它并不是一个标准I2C驱动——它注册的是一个misc设备,给用户空间暴露open、read、write、ioctl几个接口。真正的读写操作通过工作队列异步完成。
这里有个关键细节:miio_client和mjac_driver之间共享一个应用层协议。mjac_driver在中间做了一层薄薄的封装,负责把上层指令封包、计算CRC16校验、再发给芯片。这层协议恰恰定义了MJA1支持哪些指令。
I2C嗅探
搞清楚主机端谁在跟芯片通信之后,就该抓数据了。我们把逻辑分析仪直接挂在MJA1的I2C引脚上,触发设备执行需要跟安全芯片交互的操作——比如生成设备凭证、签名——然后记录所有I2C通信数据。
抓到几个关键特征:
- 所有写入事务都从一个单字节的命令ID开始(0x00、0x05、0x16等),后面跟一堆载荷数据,尾部是两个CRC16校验字节。
- 读取操作的长度固定,状态码为0x00表示成功。
- 主机写完之后,芯片会把SDA线拉低一小段时间(大约3到20毫秒)来拉长时钟——这叫时钟延展,说明芯片在处理请求时需要额外时间,这是它独立于主SoC运行的一个特征。
逆向miio_client
光靠嗅探看不清指令结构,要恢复完整的指令格式和参数含义,必须逆向主机端代码。
MJA1的库包含三个核心组件:
- mjac_cmd_build_* 系列函数:构建各种支持的指令
- mjac_resp_parse_data:解析芯片响应
- mjac_crc16_ccitt:计算帧完整性用的CRC16
已识别的指令
通过分析mjac_cmd_build_*系列函数,我们把每个指令ID和功能对应上了:
| ID | 指令 | | — | — | | 0x00 | Echo(回显测试) | | 0x02 | 生成随机数 | | 0x05 | 读取 | | 0x0D | 休眠 | | 0x11 | 生成密钥 | | 0x14 | 查询 | | 0x16 | 生成签名 | | 0x17 | 验证签名 | | 0x18 | 建立密钥 |
这些指令可以分成三类:
- 工具类:Echo测通信、生成随机数提供密码学安全的随机值、休眠进低功耗模式
- 数据访问类:读取特定数据区的数据、查询芯片信息(序列号、产品版本等)
- 密码运算类:生成密钥(ECDH用的临时ECC密钥对)、建立密钥(推导共享秘密)、生成签名、验证签名
解析一条指令的结构
上面那张表看起来干干净净,实际上还原每条指令的精确格式没那么简单。即使函数名保留着,每个mjac_cmd_build_函数都会把参数塞到一个充满魔术偏移量的结构里,对特定范围计算CRC,返回的长度也各不相同。
拿mjac_cmd_build_establish_key举例:
int mjac_cmd_build_establish_key (__mjac_cmd_establish_key *_cmd, uint _cmd_len, void *_src, int _src_len) { ushort crc; int ret; if (_src == (void *)0x0) return 0; if (_src_len == 0x45) { if (_cmd != (__mjac_cmd_establish_key *)0x0) { ret = 0; _cmd->field0_0x0 = 0x18; _cmd->field1_0x1 = 0xff; memcpy(_cmd->___key,_src,0x45); crc = mjac_crc16_ccitt(&_cmd->field0_0x0,0x47); _cmd->_crc16_lo = (char)crc; _cmd->_crc16_hi = (char)(crc >> 8); ret = 0x49; } } return ret; }
仔细读这段代码能看出不少东西:首字节0x18是指令ID,确认建立密钥就是0x18;第二字节0xff是个含义不明的常量;接下来0x45字节是真正的指令载荷,从调用者缓冲区复制过来;CRC16对前0x47字节计算,结果以小端序两个字节追加到末尾;整个帧长度0x49字节,也就是1(ID)+1(常量)+0x45(载荷)+2(CRC)。
对每条指令都这样仔细抠,是恢复协议的唯一可靠途径。
还原CRC算法
每条指令的完整性都由mjac_crc16_ccitt保护。在我们能发出一条有效指令之前,必须先有一个能正确计算CRC的实现。函数名暗示是标准CRC-16/CCITT变体,但CRC-16的变体太多了,差异在于四个参数:多项式、初始值、输入输出是否反射、最终异或值。
从反编译代码里我们识别出了全部参数:移位加异或的模式加上256项查找表,说明是反射型CRC实现(低位在前,寄存器右移);查找表编码的多项式为0x8408,这是0x1021的位反转形式,正是经典CCITT多项式;寄存器初始化为0xFFFF;最终结果按位取反后才返回。
这对应的是CRC-16/X-25(也叫CRC-16/IBM-SDLC)变体。它是标准预设,但并非大多数人听到”CRC-16/CCITT”时第一反应的那个——所以我们只能从代码推导,靠猜不行。
我们用C语言重新实现了它:
static const uint16_t crc16_table[256] = { 0x0000, 0x1189, 0x2312, 0x329B, 0x4624, 0x57AD, 0x6536, 0x74BF, 0x8C48, 0x9DC1, 0xAF5A, 0xBED3, 0xCA6C, 0xDBE5, 0xE97E, 0xF8F7, /* … 剩余240项 … */ 0x7BC7, 0x6A4E, 0x58D5, 0x495C, 0x3DE3, 0x2C6A, 0x1EF1, 0x0F78, };
uint16_t crc16(const uint8_t *data, int len) { uint16_t crc = 0xFFFF; for (uint32_t i = 0; i < len; i++) crc = (crc >> 8) ^ crc16_table[(crc ^ data[i]) & 0xFF]; return ~crc; }
公开固件的交叉验证
上面这些工作做完之后,我们才在搜索函数名时偶然发现了一个GitHub仓库:github.com/iomonad/handshow-firmware。这是台用了小米米家BLE SDK的第三方设备,里面恰好有MJA1的封装源码——mjac_wrapper.c/h和mjac_defs.h。
早点发现能省不少事。不过这份源码验证了我们的指令列表,还补上了指令参数和响应码的最后拼图。
指令格式详解
读取指令(0x05)
以读取指令为例,说明通用的指令/响应结构。
它从选定的数据区按偏移量和长度读数据,需要CRC16校验。参数含义:
- Index:选择数据区——0为设备证书,1为厂商证书,2为根证书,3为产品数据,4为用户数据
- Offset:数据区内的起始位置
- Length:读取字节数(受响应不能超过512字节的限制)
这三个证书都是公钥证书,对应的私钥永远不会通过I2C暴露。主机可以读取芯片身份并验证证书链,但私钥材料始终留在芯片内部,仅供密码运算指令内部使用。这正是安全芯片该守住的信任边界。我们尝试过读取这些定义区域之外的内容,芯片一致报错。
指令响应
所有指令的响应都遵循统一结构:
响应状态码包括:0x00表示成功,0x01表示CRC无效,0x02表示参数无效,0x04表示不支持的指令,0x06表示长度过大等等。
给个实际例子,我们观察到的读指令交互:
请求:READ(Index=3, Offset=2, Length=8, CRC=0x58EF)
原始响应:[ 00 | 000A | 00 00 00 00 41 FF E5 6F | 71C8 ]
解析后:状态OK(0x00),长度0x000A,产品ID为0x41FFE56F,CRC为0x71C8。这条指令读的就是设备的产品ID。
主动测试
到此为止我们只是被动嗅探通信。要深入必须主动发指令。
测试环境搭建
我们用了另一台也搭载MJA1芯片的设备——小米智能摄像头C301,已经在它上面拿到了root权限。
写了个小C程序,通过Linux的I2C用户空间接口直接跟芯片通信。打开/dev/i2c-0,用ioctl把从设备地址设成0x2A,然后直接write()和read()收发帧。
int fd = open(“/dev/i2c-0”, O_RDWR); ioctl(fd, I2C_SLAVE, 0x2A); write(fd, cmd, cmd_len); read(fd, resp, resp_len);
在这个I/O核心之上,我们搭了一个小的分发表,把每个指令名映射到对应的构建函数(cmd_build_read、cmd_build_update、cmd_build_generate_signature等)。每个构建函数接收高层参数,按我们逆向出的格式打包,附加CRC。最终得到一个干净的交互式命令行:
$ ./mjac_send read 3 2 8 # 读zone=3, offset=2, length=8 $ ./mjac_send query # 查芯片信息 $ ./mjac_send custom 130000… # 发任意帧做模糊测试
程序用Buildroot工具链交叉编译,匹配摄像头架构,通过SSH传到设备上。
碰到一个头疼的限制:发完一条指令后,芯片就再也没反应了,必须整机重启才能再次通信。被动嗅探时没这问题,说明是主动交互特有的。我们花了不少时间排查——改I2C事务的发起方式、拆分或合并读写阶段、调整间隔延时,还拿逻辑分析仪抓每种变体跟合法miio_client的通信对比。即使波形一样,芯片还是在一次成功交互后就拒绝响应。
根因始终没找到。不过尽管有这限制,我们仍然能一条一条发指令并观察响应。
暴力破解指令空间
有了主动测试能力,下一步自然是挖掘文档之外的隐藏指令。
我们把0x00到0xFF所有候选指令ID都轮了一遍,每条带合法CRC发给芯片,看响应码。返回”不支持指令”(0x04)的就算不存在,其他响应都值得深挖。
为了应对芯片不响应的问题,我们写了Python控制器跑在笔记本上,自动完成每条指令的完整流程:SSH连摄像头、跑C测试程序发候选指令、抓响应记录、如果芯片没反应就SSH发重启命令、等设备上线(反复尝试SSH连接)、然后处理下一个ID。
结果挖出两条不在miio_client列表里的指令:
- 0x06 Update:写入数据到某数据区
- 0x13 未知:始终返回一个未文档化的错误码0x0F
0x13这条更有意思。它不是”不支持”,而是返回自定义错误码,说明这个指令确实存在、部分被处理了,但有什么东西阻挡了正常执行。可能缺特定前置条件或参数。针对这条指令做参数模糊测试,是后续研究的好方向。
Update指令(0x06)
名字听起来跟固件更新有关,实际上毫无关系。Update指令的结构和前面读指令一样:
它向选定数据区的指定偏移写入数据,带CRC16校验。但大量测试后发现,只有用户数据区(Index=4)能真正被修改,其他区——证书和产品数据——全是只读。数据长度受I2C缓冲区限制,偏移量也被用户数据区的大小约束。
换句话说,芯片对关键区域做了强写保护:设备证书、厂商证书、根证书还有产品数据,从主机侧看全是不可变的。
结论与下一步
从一颗零文档的芯片出发,我们识别了它的通信接口,dump并逆向了对端主机固件,恢复了包含CRC变体在内的完整指令格式,还暴力扫描指令空间找到两条隐藏指令。
从主机侧看,这颗芯片表现得挺规矩:证书和产品数据只读,私钥绝不跨越I2C总线,写操作被限制在用户数据区,畸形参数被正确拒绝。协议层面没发现明显漏洞——但协议只是正门而已。
接下来有几个值得深入的方向:
- 把芯片从板子上吹下来,用单片机搭个独立测试板。这能解决发一条指令就要重启设备的问题,让所有指令和参数的模糊测试能跑起来,尤其是那条神秘的0x13。
- 故障注入——电压毛刺、电磁脉冲、时钟毛刺。芯片对各种数据区的读写做了严格访问控制。如果在读指令检查区域索引的那一瞬间注入毛刺,能不能绕过检查读到不该读的东西?比如芯片内部存储、私钥或者固件?
- 侧信道分析——功耗和电磁辐射。芯片在执行生成密钥、建立密钥、生成签名时会做ECC运算,这是功耗分析和电磁侧信道攻击的经典目标,目标就是恢复私钥材料。
说实话,协议层面的分析算轻松的。真正有意思的工作得等芯片被物理隔离、通过它的模拟侧信道来剖析时才刚开始。
希望这篇文章能成为后续侧信道实战研究的垫脚石。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:幻泉之洲 《小米安全芯片MJA1黑盒探秘:从零开始逆向一款无文档加密芯片》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论