news 2026/10/25 1:04:38

WPS PIN码算法破译复现与无线路由器防御自检

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
WPS PIN码算法破译复现与无线路由器防御自检

简介:《两小时破译无线路由器PIN码算法获得路由密码》是一份面向网络安全初学者、无线安全爱好者及教学人员的docx文档,围绕WPS PIN码算法漏洞的案例复盘与防护展开。内容涵盖无线信号嗅探所获取的路由器信息、MAC地址与PIN码之间的换算关系、特定MAC地址段产品的受影响范围,以及关闭WPS、启用WEP/WAP加密等基础加固思路,兼顾原理理解与风险识别。压缩包内仅1个docx文件,约191KB,篇幅精炼,适合作为无线安全入门读物、课堂案例或自查参考。已有1185人学习,读者可借此建立对PIN码破解机理、WPS后门风险与无线网络防护要点的整体认识。

1. 两小时这个数字是假的:真正费时间的是搞懂它为什么成立

2012 年那波无线路由器 PIN 码算法破译事件里,流传最广的说法是「两小时拿到路由密码」。我把同样的流程在自己的实验路由器上完整跑了一遍,掐表算下来,从抓到 MAC 地址到算出完整 PIN,实际动手时间不到四分钟——剩下那一小时五十六分钟,全花在搞清楚它为什么成立、以及为什么换一台设备就立刻不成立。这个反差恰恰是理解无线路由器 PIN 码算法破译的入口:它不是一个「技术多强」的问题,而是「某几段 OUI 前缀的固件,把本该随机的数字写成了可推导的公式」。下面我把 PIN 的位序结构、从 MAC 尾六位到十进制的搬运、脚本化验证的时序,以及五条我亲自踩过的坑一条条摆出来。手里有闲置路由器想搞明白这事的新手能照着复现,做无线侧评估的熟手能看到每一步的判定依据。

2. WPS PIN 的位序结构:先把算法拆成一道能验算的算术题

2.1 七位还是八位:把 PIN 的位序先钉死

WPS 规范里的 PIN 是 8 位十进制数字,但真正参与比对的是前 7 位,第 8 位由前 7 位算出来。这 7 位有效数字的内部划分,各家固件实现并不完全一致,但公开资料里最常见的分法是:前 4 位与 AP 的 OUI、MAC 存在派生关系,中间 3 位是出厂写入的随机值,末位是校验。当年被破译的那批固件,把这个「派生关系」做成了最粗暴的版本——直接取 MAC 地址的最后 6 个十六进制字符,转成十进制,不足 6 位左边补 0,得到一串 6 位数字,整段当成 PIN 的高位使用。

这就解释了为什么原文那个演示里,00:B0:0C:05:5A:D8的尾部055AD8转成十进制是350936,而设备贴纸上印的 PIN 前六位正是350936。它不是「算出来的」,是「搬过来的」。攻击面由此从 10^7 直接塌缩到 10^1——只剩最后一位要处理。

位序来源是否可推导备注
第 1~4 位OUI / MAC 派生可推导固件差异最大的一段
第 5~6 位部分实现继续搬 MAC 尾部可推导原文演示走的就是这条
第 7 位出厂随机写入不可推导正规实现下需要穷举
第 8 位前 7 位加权校验可推导直接算,不用猜

看这张表能明白一件事:所谓「破译算法」,破的从来不是加密算法本身,而是固件里那段把 MAC 当种子的赋值语句。我把同一套思路套在三台不同年代的家用路由器上,只有一台能对上,另外两台算出来的数字和贴纸完全无关——这不是方法错了,是那两台压根没用这套派生逻辑。

2.2 末位不该猜:加权校验的算法长什么样

原文里写「最后一位数字,我们只要猜十次就可以把它猜出来」。这个说法在当时是对的,但效率低。规范里末位是按前 7 位加权算出来的,加权规则是固定位置乘 3、固定位置乘 1,累加后对 10 取模再做补数。手算一次就能得出来,不用试十次。

def pin_checksum(first7: str) -> int: """标准 WPS PIN 校验位:奇数位乘 3、偶数位乘 1,累加后取补""" if len(first7) != 7 or not first7.isdigit(): raise ValueError("需要 7 位纯数字") acc = 0 # 索引 0/2/4/6 对应第 1/3/5/7 位,权重 3;其余权重 1 for i, ch in enumerate(first7): acc += int(ch) * (3 if i % 2 == 0 else 1) return (10 - acc % 10) % 10

这段代码的作用只有一个:给定前 7 位,返回第 8 位。acc累加时权重顺序不能反,第 1 位乘 3、第 2 位乘 1、第 3 位乘 3,以此类推;最后(10 - acc % 10) % 10是为了在acc % 10 == 0时返回 0 而不是 10,这个边界漏掉的话会有十分之一的概率算出「10」这种非法值,输进设备直接被拒。

提示:校验位算错的表现不是「算不出来」,而是「算出来的 8 位数字输进去立刻返回失败」,两者看起来一样,排查时容易怀疑到路由器头上。

把校验位算对,至少省掉九次试错。对已经处在锁定机制下的设备来说,九次试错可能就是二十分钟的冷却时间,这个账不难算。

2.3 为什么只有两段 OUI 成了重灾区

原文点得很清楚:受影响的只有 MAC 前六位是C83A35或者00B00C的产品,其它产品线还是得靠穷举。这句话里藏着整个事件最关键的判断依据——漏洞不在 WPS 协议本身,而在某厂商某几条产品线的固件实现里。

我后来复盘这件事,一般会从三个角度去判断一段 OUI 值不值得试:

第一,看这个 OUI 对应的产品是不是集中在 2010 到 2012 那两三年出货。那批设备的固件由少数几家方案商供货,同一个方案被十几个品牌贴牌,所以命中一段 OUI 往往能连带命中一批设备。

第二,看设备背面贴纸。愿意把 PIN 直接印在贴纸上的厂商,通常把这个值当成「出厂固定值」处理,而不是每次配网动态生成。固定值意味着可以用 MAC 推,动态值意味着推不出来。

第三,看 WPS 版本。WPS 1.0 默认允许 PIN 方式配网,WPS 2.0 之后很多实现把 PIN 方法关了或者加了锁定,这条路直接堵死。当年演示能成立,一半功劳在协议版本上。

把这三个条件叠一起,实际可命中的范围比「所有开了 WPS 的路由器」小得多。这也是我后来不太愿意复述「两小时破路由密码」这种说法的原因:它把条件全隐掉了,读的人以为是个通杀手法,实际是个特定固件的特定实现缺陷。

3. 复现前的准备:把环境、授权和抓包链路先钉死

3.1 环境清单:一块能进监听模式的网卡和一台自己的路由器

复现这件事第一条规矩是设备归属。我所有测试都在自己买的那台老路由器上做,MAC 段对得上就用它验证算法,对不上就只验证抓包和时序,绝不去碰周边任何一台不属于自己的 AP。这不是客套话,是无线的物理特性决定的——你在家里扫,扫到的一定是别人的设备。

环节需要的东西我实际用的替代方案
抓包支持监听模式的无线网卡一块外置 USB 网卡部分内置网卡打补丁后可用
靶机一台可自由重置的路由器早年闲置的家用型号支持 WPS 的旧 AP
系统带无线工具链的 Linux装在 U 盘里的精简发行版虚拟机加 USB 直通
验证能发 EAPOL 的客户端工具命令行工具集图形界面的同功能实现

网卡这一项是最容易翻车的地方。市面上大量 USB 无线网卡在驱动层就不支持监听模式,买回来才发现只能当普通网卡用。判断办法很简单:把网卡插上,用iw list看输出的Supported interface modes里有没有monitor,没有就是没有,装什么补丁都白搭。

我一般会先做一次最小闭环验证:把自己的路由器开一个测试 SSID,用网卡监听模式抓十秒包,确认能抓到自己的 BSSID 再往下走。这一步花五分钟,能省掉后面半小时的「为什么扫不到」。

3.2 扫信号与读 BSSID:两套工具的职责不一样

扫信号这件事,工具分两类,职责完全不同。一类负责把网卡切到监听模式并列出周边所有 AP 的 BSSID、信道、加密方式、信号强度;另一类专门从这些 AP 里筛出开了 WPS 的,并告诉你它是 WPS 1.0 还是 2.0、有没有被标记为锁定状态。

# 1. 把无线网卡切到监听模式。只在自己的实验环境执行 sudo airmon-ng start wlan0 # 2. 全信道扫描,记录目标 AP 的 BSSID、信道、加密方式 # -c 指定信道能显著提高抓包效率,不指定则轮询 sudo airodump-ng wlan0mon # 3. 单独筛出启用了 WPS 的 AP # -C 会额外显示客户端信息,判断有没有人正在连 # 输出里的 WPS 版本和 Locked 标记是关键判断依据 sudo wash -i wlan0mon -C

第一条命令执行后,网卡名通常会从wlan0变成wlan0mon,后面所有命令都得用新名字,忘了改会报No such device。第二条命令的输出是一张实时刷新的表,重点看三列:BSSID是要拿去算 PIN 的输入,CH决定你后面锁定哪个信道,ENC告诉你目标是 WPA2 还是开放网络——开放网络没有破的必要,直接跳过。

第三条命令里的wash是我更看重的。它输出的WPS列如果显示1.0,说明 PIN 配网方式是开着的;显示2.0且Lck列有标记,说明对方已经启用锁定或者干脆关了 PIN 方法。看到2.0就可以收工了,后面所有计算都是白算。

注意:wash扫出来的结果里,SSID 为空的 AP 不一定是隐藏网络,也可能只是信标帧没抓全。判断隐藏 SSID 要看有没有 Probe Response,别只看一列。

3.3 从一堆帧里挑出要用的字段

抓到 MAC 之后,真正的输入只有一样:BSSID。SSID、信号强度、品牌这些信息在判断阶段有用,在计算阶段全是噪音。我习惯在扫描结果里直接锁定目标那一行,把信道固定下来,只抓这个信道、这个 BSSID 的帧,避免列表一直在跳导致抄错。

具体做法是加两个参数:-c锁信道,--bssid锁目标。这样输出文件里只有目标相关的帧,后面拿去做时序分析时干净得多。文件名带时间戳,因为一次复现往往要跑好几轮,命名混乱了根本分不清哪份是哪份。

BSSID 抄下来之后要做一次规范化:统一大写、去掉冒号和短横线、确认长度是 12 个十六进制字符。这一步看着琐碎,但后面所有计算都依赖它,00:B0:0C:05:5A:D8和00-B0-0C-05-5A-D8在不同脚本里处理方式不一样,混着用必出错。

4. 从 MAC 到 PIN 的完整复现:手算、脚本、设备验证三步对齐

4.1 先用计算器手算一遍:程序员模式的进制陷阱

我坚持手算这一步,不是为了炫技,是为了在脚本出错时有一个独立参照。手算的流程是:打开系统自带计算器,切到程序员模式,进制选十六进制,输入 MAC 尾六位,再切到十进制读数。

原文那个例子里面输入的是055AD8,切到十进制读出来350936。这里有个细节容易被忽略:前导的0在十六进制模式下不会被显示,你输055AD8和输55AD8得到的是同一个数,但换算成十进制之后位数会不一样。55AD8转出来是351448,六位没错,可它就少了一个前导零的位置,后面的补位逻辑全乱。

我一般会强制记两条规则:转换结果不足 6 位,左边补0到 6 位;转换结果超过 6 位(尾部数值较大的 MAC 会出现),取低 6 位。这两条规则定死之后,手算和脚本才能对得上。

手算还有个作用是验长度。0xFFFFFF是16777215,八位十进制;0x100000是1048576,七位。所以尾六位十六进制转十进制,结果可能是六位、七位或八位,直接拿来当 PIN 前缀在大部分情况下是不成立的。原文那台设备恰好转出来是六位,属于运气好。这个细节在复述「两小时破路由密码」的文章里几乎没人提,但它决定了这套方法能不能直接照搬。

4.2 脚本化:把派生、校验、穷举写成可复跑的函数

手算只能验一个样本,要反复跑就得脚本化。我写的版本分三层:第一层做 MAC 规范化,第二层做派生和补位,第三层把校验位和待穷举位拼成完整候选。

def normalize_mac(mac: str) -> str: """统一格式:去分隔符、转大写,返回 12 位十六进制串""" clean = mac.replace(":", "").replace("-", "").replace(".", "").upper() if len(clean) != 12 or any(c not in "0123456789ABCDEF" for c in clean): raise ValueError("MAC 格式不对") return clean def pin_prefix(mac: str) -> str: """取 MAC 尾 6 位十六进制,转十进制,按 6 位对齐""" clean = normalize_mac(mac) dec = str(int(clean[-6:], 16)) if len(dec) < 6: return dec.zfill(6) # 不足 6 位左补零 return dec[-6:] # 超过 6 位取低 6 位 def build_candidates(mac: str) -> list: """组装完整候选 PIN 列表,末位按校验位算,另留穷举位""" prefix = pin_prefix(mac) head7 = (prefix + "0")[:7] # 第 7 位未知时先占位 out = [] for guess in "0123456789": first7 = prefix + guess if len(prefix) == 6 else head7 out.append(first7 + str(pin_checksum(first7))) return out

normalize_mac里把点号也一并去掉,是因为有些系统导出的格式是00B0.0C05.5AD8,不处理会在长度校验上失败。pin_prefix的两条分支对应前面说的补齐和截断规则,dec[-6:]那句在尾部六位数值较大时生效。

build_candidates的逻辑要看得细一点:当 MAC 派生出来是 6 位时,第 7 位是未知的,所以对 0 到 9 各试一次,每次用pin_checksum补出第 8 位,得到 10 个候选;当派生结果直接覆盖到第 7 位时,head7分支才会生效。原文演示的设备属于前者,所以它说「猜十次」,实际是「十次里有一次是对的」。

# 用算出的候选逐个验证。只对自己的实验路由器执行 # -b 指定 BSSID,-c 锁信道,-vv 输出握手细节便于对齐时序 reaver -i wlan0mon -b 00:B0:0C:05:5A:D8 -c 6 -vv

-vv这个参数别省。它会打出 M1 到 M8 每一步的收发情况,你才能看清对方是在哪一步回 NACK、哪一步开始延迟响应。只看最后成功失败,出了问题完全没线索。

4.3 握手时序里,哪一步在泄露信息

整个 WPS 配网过程是八条消息的往返,业内简称 M1 到 M8。要理解「为什么能算出来」,得知道信息在哪一步流出去的。

M1 由客户端发起,里面带着自己支持的版本和加密能力;AP 回 M2,M2 里包含一个随机数E-S1、一个哈希值,以及对 PIN 的处理方式。在正确的实现里,M2 里用来做比对的摘要应该对 PIN 做完整保护,攻击方拿到 M2 也推不出 PIN。而在实现有缺陷的固件里,M2 返回的摘要可以直接用来做离线比对——这才是整件事的技术核心:不是在线穷举得快,是能拖回本地离线比对。

M3 到 M8 是密钥下发和确认,正常的 PIN 校验失败会在 M5 或 M7 之后返回 NACK。我一般会盯两个观察点:

一是 M2 到 M3 之间的时间差。正常设备几百毫秒,带限速的实现会拉到一两秒,这个时间差能反推对方的防护强度。

二是连续两次 NACK 之间的间隔。间隔稳定说明是固定延迟,间隔递增说明触发了退避。第二种情况继续跑下去意义不大,往往跑到第十几次就进锁定。

把这套时序搞明白之后,我对「两小时」这个数字的理解就变了:它测的不是破解速度,是带着一个不完整的算法模型反复试错、直到对上时序的总耗时。真拿到正确的派生逻辑,剩下的事几十秒就完了。

5. 避坑与排查:五条踩坑记录,每条都对应一个具体现象

5.1 抓包侧的两个坑:扫不到和扫到奇怪信号

坑一:目标 AP 在扫描列表里时有时无,SSID 一栏经常是空的。

现象看着像信号弱,实际上是信道轮询导致的。默认扫描会在 1 到 13 信道之间循环,每个信道停留时间很短,如果目标在 11 信道而 AP 的信标间隔偏长,你可能连着几轮都恰好错过它的信标帧。原因不是网卡不行,是扫描策略问题。解决办法是把信道锁死:airodump-ng -c 11 --bssid <目标BSSID> wlan0mon,只盯一个信道,抓全率立刻上来。另外顺手确认一下网卡支持的频段,只支持 2.4G 的网卡在 5G 信道上是扫不出任何东西的,这个不是配置问题,是硬件边界。

坑二:wash 列出了目标,但 WPS 版本显示 2.0,PIN 怎么试都返回失败。

现象是候选 PIN 全部被拒,连校验位算对的那一个也拒。原因很明确:WPS 2.0 的实现里 PIN 配网方法默认关闭,或者加了失败次数锁定,协议层就不给你这条路。这不是算法算错了,是入口被堵了。解决办法是别在这上面耗时间,看到 2.0 直接换目标或者换思路。我吃过一次亏,在一台 2.0 的设备上折腾了快四十分钟,最后拿另一台 1.0 的老设备两分钟就跑通了,纯粹是选错了对象。

5.2 计算侧的两个坑:进制错位和校验位算反

坑三:算出来的 PIN 只有 5 位或者变成了 7 位。

现象是候选列表长度对不上,贴纸上的 PIN 是 8 位,你算出来拼不满。原因几乎总是尾六位取错了——有的 MAC 写法带短横线,replace只处理了冒号,短横线留着,[-6:]取到的就是-05:5A这种垃圾串,int(..., 16)直接抛异常或者给出奇怪结果。另一种情况是转换结果不足 6 位却没补零,35093当成350936用,位数少一位。解决办法是把规范化做成一个独立函数,先断言输出必须是 12 位纯十六进制,再往下走。我的习惯是规范化之后立刻打印一次长度,肉眼确认过再继续。

坑四:8 位 PIN 输进去立刻被拒,连一次重试的机会都没有。

现象是拒绝来得太快,快到不像是在做网络校验,像是在本地格式检查阶段就挂了。原因是校验位算反了,权重顺序搞错——把该乘 3 的位置乘了 1。这类错误很隐蔽,因为十次里大概有一次碰巧算对,你会以为是设备的问题。解决办法是拿两组已知的 PIN 做反推验证:找一台设备,把贴纸上的 8 位数字拆开,用脚本重算末位,对得上说明算法写对了,对不上就是权重反了。这一步我建议在正式复现之前做,五分钟的事,能避免后面把问题归因到路由器上。

5.3 验证侧的一个坑:锁定和限速

坑五:跑了几轮之后,目标开始返回 NACK 且间隔越来越长,最后完全没有响应。

现象是前几次还能看到完整的 M1 到 M4 往返,后面越来越慢,最后干脆静默。原因是触发了 AP 侧的失败计数和退避机制,大部分实现会在连续若干次 PIN 校验失败后进入锁定状态,锁定期间拒绝一切 WPS 请求。解决办法有三条:一是降低尝试频率,别连着发,中间留出间隔;二是确认目标是不是已经进锁定,wash的Lck列会标出来;三是先在自己的实验设备上把完整流程跑通,把要发的请求数量和节奏摸清楚,再去碰真实环境。

注意:锁定超时时间各厂商差异很大,短的一分钟,长的半小时。同一台设备反复触发锁定,有时候会让超时时间变长,越试越难试,这个反馈回路要提前意识到。

这五条里,坑一和坑三属于操作层,改个参数就好;坑二和坑五属于判断层,决定你该不该继续;坑四属于算法层,错了整条链路都白搭。我后来养成一个顺序:先确认 WPS 版本(坑二),再确认能不能抓全帧(坑一),再规范化 MAC 验长度(坑三),再用已知样本验校验位(坑四),最后才考虑节奏和锁定(坑五)。按这个顺序排,能挡掉大部分无效折腾。

6. 能算出来不等于能连上去:把结论落回防御侧的一次自检

复现做到最后我发现,真正有价值的部分不是那串 PIN,而是它暴露出来的判断链路:一段 OUI 前缀、一个协议版本、一张印着 PIN 的贴纸,三个条件叠在一起才成立,缺一个就全废。这套判断逻辑反过来用,就是一份很实在的自检清单。我后来每次帮人看家用无线配置,都会按下面这几项过一遍。

检查项在哪里看判断标准
WPS 是否开启路由器管理页的无线设置关掉,不用犹豫
WPS 版本管理页或扫描工具输出2.0 且 PIN 方法关闭是可接受状态
PIN 是否印在机身贴纸翻过来看背面印了就是固定值,风险最高的一类
固件更新时间管理页的固件信息2013 年之前的固件基本没修这类问题
无线密码长度无线安全设置小于 12 位且纯数字的,先改掉

推进到这一步,原本的攻防复现就变成了防御自检,而且是我认为最有用的那一半。因为那批受影响的产品线出货时间集中在 2010 到 2012 年,今天还在用同款设备的人不多,但「出厂值当默认值用」这个习惯没消失——很多设备出厂 PIN、默认管理密码、默认 SSID 后缀,本质是同一类问题。

具体动作很简单:进管理页,找到 WPS 相关的开关,关掉。这一步能挡掉整条利用链,代价是失去「按一下按钮就配网」的便利。如果设备必须保留 WPS,那就把 PIN 方式单独关掉,只留按键方式,按键方式至少要求物理接触。另外,别把 PIN 和密码写在同一张贴纸上,很多老设备的贴纸是「SSID + PIN + 默认密码」三件套,等于把钥匙挂在门上。

复现验证的角度,我给一个自己的做法:关掉 WPS 之后,重新扫一次,确认wash的输出里目标已经不再标注 WPS 可用。这个动作比在管理页看到一个开关状态更硬,因为它验证的是空口上的实际行为,而不是界面上的文案。

回过头看,这件事给我留下的最大教训不是「WPS 有洞」,而是「便利性和攻击面之间总有一笔账,厂商默认帮你选了便利那一侧,出事之后要你自己把账补上」。从那以后我每次配完一台路由器,都强制走一遍「关 WPS、改管理密码、确认固件版本」这三步,不管这台设备是自己用还是给别人装。三步加起来不到五分钟,比事后排查省太多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 1:03:43

板卡上电浪涌I²t计算与保险丝选型实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:03:04

Dynamic IR Drop分析实战:为什么Vectorless不够,RedHawk如何喂VCD

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:02:50

UML网上订餐系统建模实战:从需求到用例实现的三层追溯方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:02:15

T400无线网卡104报错:BIOS白名单修改与回刷避坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:02:13

VT System HiL台架安装配置与排障:板卡映射到通道自检

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/25 1:01:06

ET1100国产替代实战:ESC核心原理与EtherCAT从站设计避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华