浅学LoRa与Zigbee
题目链接:https://pan.baidu.com/s/1GgUs_rSYwfIFIE06IWs23Q 提取码: 0322
一、查看题目

目标是向网关注入一条伪造的 LoRa 维护报文,使网关为 Zigbee 睡眠终端 0x7A31 排队 DIAG_DUMP (0x17),然后在设备 poll 窗口内发送正确的 Zigbee 应用层确认帧,最终拿到 flag。
不是只发送 Zigbee APS ACK。服务端会严格检查 7F 应用确认中的来源地址、seq、token、状态字节和 CRC16。
二、解题步骤
1 附件分析
1.1 lorawan.txt
文件中给出:
1 | AppKey=00112233445566778899AABBCCDDEEFF |
拿到 AppKey 直接作为应用层 HMAC 密钥使用,因此脚本中使用:
1 | KEY = bytes.fromhex("00112233445566778899aabbccddeeff") |
设备短地址来自题面和日志:0x7A31。
1.2 gateway.log
关键时间线:
1 | 03:14:20.000 poll-window closed (轮询窗口关闭) |

这说明 Zigbee 终端是 Sleepy End Device:平时休眠,只在 poll 时接收网关排队的下行数据。服务端中,LoRa 报文成功后会等待 2 秒打开 poll 窗口,窗口持续 400 ms。(poll窗口机制)(轮询窗口)
1 | 现实 Zigbee 逻辑 |
注意:早了:sleepy‑device;晚了:poll‑window‑expired
1.3 capture.pcapng
抓包里的 LoRa 应用层格式为:
1 | 55 AA 01 | dst(2)[Zigbee 目标短地址] | seq(1) [报文序列号]| op(1) | token(4) | len(1) | data | HMAC8 |
1 | 55 aa 01:帧头 |
报文序列号seq解释
1 | 每收到一条合法报文,这个 dst 的 seq 计数器就 +1。 |
题目里的字段均为 little-endian(小端序),dst=0x7A31。抓包最后一条合法维护帧的 seq=0xFF,因此下一条可接受序号是:
1 | expected = (0xFF + 1) & 0xFF = 0x00 #seq 是 1 字节无符号整数,只能存 0~255(也就是 0x00 ~ 0xFF) |

1.4 gateway_fw.bin
用strings查看即可看到提示字符串:

核心漏洞是:网关固件校验 LoRa 维护帧 HMAC 校验值时,本来应该比较 8 字节 HMAC,却只比较前 2 字节。也就是说,报文末尾只需要满足:
1 | supplied_hmac[:2] == HMAC-SHA256(key, body)[:2] |
剩余 6 字节可以任意填写。平均约尝试 2^16 次即可找到匹配值。
2. 构造恶意 LoRa 报文
目标字段:
1 | dst = 0x7A31 |
因此 HMAC 计算前的 body 是:
1 | 55 aa 01 31 7a 00 17 11 22 33 44 04 53 54 41 54 |
注意 dst=0x7A31 使用 little-endian,所以在线上传输时是 31 7a。
最终报文为:
1 | body || 8-byte-hmac #HMAC‑SHA256(key, body) |
其中 8-byte-hmac 的前两个字节必须和真实 HMAC 相同,后六个字节可以设为 00,也可以直接发送真实 HMAC。
3. Zigbee 确认帧(0x7F 应用层确认帧)
前面我们伪造 LoRa 报文只是把cmd=17(诊断转储指令)压入队列;必须发送这条 7F 帧,才会真正输出 flag
网关排队命令后,必须等 2 秒进入 poll 窗口,再发送:
1 | 7F | seq(1) | token(4) | status(1) | crc16(2) |
本题中:
1 | seq = 00 |
CRC16 使用 Modbus 多项式 0xA001,初值 0xFFFF,计算范围是前 7 个字节,即:
1 | 7f 00 11 22 33 44 00 |
发送格式:
1 | ZB 7A31 <7F确认帧十六进制> |
4. 一键利用脚本
下面脚本会自动寻找 HMAC 前 16 位匹配值,连接服务,注入 LoRa 报文,等待 2.1 秒,再计算并发送 Zigbee 确认。
1 | 发送伪造LORA报文,利用 HMAC 漏洞,把cmd=0x17(DIAG_DUMP)压入网关队列 |
运行:
1 | python3 solve.py |
输出:
1 | OK queued dst=7A31 cmd=17; poll-window in 2.0s |

1 | 整体逻辑 |
三、得到答案
flag{sleepy_zed_ack_is_not_aps_ack}




