-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.json
1 lines (1 loc) · 946 KB
/
search.json
1
[{"title":"AES-CBC密文填充攻击—深入理解和编程实现","url":"/2020/12/01/AES-CBC-PaddingOracleAttack/","content":"<p>密文填充攻击 (Padding Oracle Attack) 可能是现代密码学史上的最有名也最成功的攻击方法。攻击者利用密文的填充验证反馈信息,实现密文破解。这里简单回顾密文填充攻击的发展历史,然后深入剖析AES-CBC工作模式下的攻击原理,最后给出了Python的编程实现示例。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Every secret creates a potential failure point.</strong><br> <strong>— <em>Bruce Schneier</em>(布鲁斯·施奈尔,美国密码学和信息安全专家、作家)</strong></p>\n</div>\n<h3 id=\"密文填充攻击简史\">密文填充攻击简史</h3>\n<p>早在1998年,在贝尔实验室工作的密码学家丹尼尔·布莱肯巴赫(Daniel Bleichenbacher)首次成功设计和实现了使用选择密文攻击破解基于RSA公钥加密标准PKCS #1 v1.5的密码<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>。这种攻击依赖于密文接受方反馈的填充格式正确与否信息,持续发送构造的密文以精准地定位明文。虽然当时并未见攻击所造成实质危害的报道,但是这种新颖的攻击手段引起了密码学家和保密通信协议研究者的广泛关注。</p>\n<p>三年后,澳大利亚电讯研究实验室的詹姆斯·马格(James Manger)发现了应用类似技术攻击更新后的PKCS #1 v2.0加密标准的方法<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>。攻击的有效性取决于在解密和完整性检测过程之间所泄漏的信息,而该版本协议的设计使得实际实现中出现这种情况的可能性极高。这次攻击的目标仍旧是非对称的RSA公钥加密协议,然而已经有研究者着力于使用相似的攻击手段破解对称加密协议。</p>\n<p>2002年,瑞士洛桑联邦理工学院的密码学家塞尔日·瓦德奈(Serge Vaudenay)在欧洲密码学年会(EUROCRYPT)上发表论文<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>,详述他发明的使用密文填充验证应答消息攻击CBC模式的对称分组加密密码。由于这种密文填充攻击的原理和实现都比较简单,同时CBC模式的分组密码应用非常普遍,瓦德奈的发明很快成为密码学家和信息安全专家的研究热点,也被许多黑客/极客投入实践中。在接下来的十多年里,源于这种攻击方法所发现的CBC模式相关的保密通信安全漏洞不断地涌现,如下所列:</p>\n<ul>\n<li>2003年,结合时序攻击,破解IMAP电邮服务器与Outlook客户端之间的SSL/TLS加密信道,盗取用户密码。</li>\n<li>2004年,证明攻击适用于ISO CBC模式加密和填充标准,可以有效地破译出明文。</li>\n<li>2007年,攻击只加密不认证的IPsec网络配置,成功破解有效载荷(payload)明文。</li>\n<li>2009年,利用消息认证码(MAC)执行时间的微小差别区分填充验证结果,攻破OpenSSL和GnuTLS的DTLS实现,恢复全部明文。</li>\n<li>2010年,成功攻破验证码(CAPTCHA)系统,为后续拒绝服务攻击扫清道路;破译JavaServer Faces加密的访问状态;应用衍生的CBC-R加密算法,伪造可被服务器解密及填充验证无误的访问状态。</li>\n<li>2011年,发现微软ASP.NET网页应用框架中的多个安全设计漏洞,可被攻击者利用破解未认证的加密小信息块(cookie)及伪造访问状态用以盗取服务器上限制访问目录中的文件。这给当时25%的互联网站点的安全性带来巨大冲击。</li>\n<li>2012年,攻击硬件辅助RSA或CBC加密设备如USB安全令牌、智能卡和电子ID卡等,破解导入的密码。</li>\n<li>2013年,全新变种“幸运十三攻击”出现,其针对TLS协议中的 HMAC-SHA1 MAC验证的软件实现进行侧信道时序攻击。开源TLS/DTLS软件库包OpenSSL、GnuTLS、PolarSSL、CyaSSL及亚马逊AWS所开发的s2n都需要打补丁修复相应的漏洞。</li>\n<li>2014年,谷歌安全团队报告“贵宾犬漏洞”(POODLE)。攻击者可利用软件实现中为照顾不同版本互通性而牺牲安全性的机制, 采用中间人攻击方式将客户端连接降级到SSL 3.0,再进行密文填充攻击破解密文。几个月后,又出现了一个新变种,攻击TLS 1.0-1.2服务器协议实现中的一些漏洞。</li>\n<li>2016年,Steam游戏平台被曝光其客户端连接存在严重安全漏洞,可让攻击者组合重放攻击与密文填充攻击破解用户密码和其它保密信息;同年,常见漏洞与披露报告<a href=\"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2107\">CVE-2016-2107</a>揭示了OpenSSL针对“幸运十三攻击”的补丁中引入了另一个CBC模式密文填充攻击漏洞。</li>\n</ul>\n<p>从密文填充攻击的历史进程可以看出,虽然这种攻击方法始于非对称的加密协议,但是作用于分组加密CBC模式下,其攻击力和影响力都得到大幅提升。这并非是因为分组加密比RSA公钥加密弱,实际上<a href=\"https://www.keylength.com/en/4/\">AES-128自身的安全强度高于当前通用的2048比特模数RSA</a>。真正的原因在于分组加密与CBC模式的既有广泛应用、对认证加密重要性的忽视,以及加密协议实现者对应用密码学学习和网络信息安全性认识不足。有鉴于此,要预防下一个密文填充攻击安全漏洞,密码学家和网络信息安全专家们建议从以下几个方面改进和更新:</p>\n<ol type=\"1\">\n<li>采用认证加密方法,如具有最高安全性的Encrypt-then-MAC(EtM):首先对明文进行加密,然后根据得到的密文生成MAC,密文和它的MAC一起发送;对密文的随意改动将造成消息认证错误,会被马上抛弃。比如SSHv2支持的[email protected]算法。或者直接应用<strong>带有关联数据的认证加密</strong>(Authenticated Encryption with Associated Data,<strong>AEAD</strong>),既能保护数据私密性,又可以同时认证密文和明文。</li>\n<li>弃用TLS中的所有CBC密码套件(Cipher Suite),阻断密文填充攻击的可能性。如内容分发网络(Content Delivery Network, CDN)服务提供商Cloudflare从2016年开始全面转向支持分组加密AES-GCM和流加密ChaCha20-Poly1305,两者都是基于AEAD的密码套件。</li>\n<li>升级到<a href=\"https://tools.ietf.org/html/rfc5246\">TLS 1.2</a>或<a href=\"https://tools.ietf.org/html/rfc8446\">TLS 1.3</a>,禁用之前版本的SSH/TLS协议,同时禁止降级协商,强制安全性高于互通性。TLS 1.3不允许任何非AEAD加密算法。2021年3月,IETF发布当前最佳实践<a href=\"https://datatracker.ietf.org/doc/rfc8996/\">RFC 8996</a>,正式宣布弃用 TLS 1.0 和 TLS 1.1。根据<a href=\"https://www.ssllabs.com/ssl-pulse/\">SSL Labs</a>2021年9月的扫描统计,在全球最流行150,000个启用SSL/TLS的网站中99.6%支持TLS 1.2,48.9%的站点支持TLS 1.3。</li>\n</ol>\n<h3 id=\"aes-cbc工作模式\">AES-CBC工作模式</h3>\n<h4 id=\"aes标准\">AES标准</h4>\n<p>瓦德奈发明的密文填充攻击适用于任何CBC工作模式下的对称分组密码,知名的有DES、3DES、RC6、Rijndael和TwoFish等。由于DES已被暴力破解,而NIST认定3DES只有80位的安全性,它们都已被列入遭淘汰的分组加密算法名单中。在NIST1997年开始组织的高级加密标准(Advanced Encryption Standard,缩写AES)评选过程中,经过五年的公开征募、选拔、分析和评估,最终Rijndael算法胜过RC6、TwoFish和其它候选者,修订后成为选定的AES标准(<a href=\"http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf\">FIPS PUB 197</a>)。从此AES成为最流行的对称密钥加密算法。</p>\n<p>AES算法建立于代换-置换网络的设计原则之上,大部分计算在一个特殊的有限域完成的。AES标准规定的区块长度固定为128比特,密钥长度则可以是128、192或256比特。加密过程中使用的扩展密钥是由AES密钥生成方案产生。AES将输入的128比特区块明文按字节组合为一个4×4字节矩阵,加密运算就操作于该矩阵之上。加密过程包括重复的加密循环,称为轮(round),除最后一轮外均包含AddRoundKey、SubBytes、ShiftRows和MixColumns四个步骤,保障高效的混淆与扩散(confusion and diffusion)。解密的过程使用相同的密钥,应用一组逆转的轮次和运算步骤恢复明文。AES密钥长度决定了总共的轮次,如下所示:</p>\n<ul>\n<li>密钥长度128比特,10轮</li>\n<li>密钥长度192比特,12轮</li>\n<li>密钥长度256比特,14轮</li>\n</ul>\n<p>显然密钥长度越大,密码的安全性越高。迄今为止,AES加密算法的安全性经受住了考验,没有发现直接破解密文的有效方法。</p>\n<h4 id=\"填充格式\">填充格式</h4>\n<p>然而在实际应用中,AES还必须与特定的分组密码工作模式一起运行,才能支持变长数据。AES-CBC工作模式要求数据必须先被划分为定长的区块,最后一块数据往往需要使用合适填充方式将长度补足。分组密码常用由<a href=\"https://datatracker.ietf.org/doc/html/rfc2315#section-10.3\">PKCS #7</a>定义的填充方式,摘录如下:</p>\n<blockquote>\n<pre><code> 2. Some content-encryption algorithms assume the\n input length is a multiple of k octets, where k > 1, and\n let the application define a method for handling inputs\n whose lengths are not a multiple of k octets. For such\n algorithms, the method shall be to pad the input at the\n trailing end with k - (l mod k) octets all having value k -\n (l mod k), where l is the length of the input. In other\n words, the input is padded at the trailing end with one of\n the following strings:\n 01 -- if l mod k = k-1\n 02 02 -- if l mod k = k-2\n .\n .\n .\n k k ... k k -- if l mod k = 0</code></pre>\n</blockquote>\n<p>这种填充方式可理解为:将原始数据分割成一系列定长的区块,算出最后一块与区块长度的字节数差值;如果差值为零,用区块长度对应的字节填充一个完整的新区块加到尾部;否则,用差值对应字节填满最后一块。以AES为例,区块长度为16字节(128比特),下表给出了填充示例(XX表示数据字节):</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">差值</th>\n<th style=\"text-align: center;\">填充字节</th>\n<th style=\"text-align: center;\">尾块(* 表示新尾部区块)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">0x10</td>\n<td style=\"text-align: center;\"><code>10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10</code>*</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0x01</td>\n<td style=\"text-align: center;\"><code>XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX 01</code></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">0x02</td>\n<td style=\"text-align: center;\"><code>XX XX XX XX XX XX XX XX XX XX XX XX XX XX 02 02</code></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">...</td>\n<td style=\"text-align: center;\">...</td>\n<td style=\"text-align: center;\"><code>....</code></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">14</td>\n<td style=\"text-align: center;\">0x0E</td>\n<td style=\"text-align: center;\"><code>XX XX 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E</code></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">15</td>\n<td style=\"text-align: center;\">0x0F</td>\n<td style=\"text-align: center;\"><code>XX 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F</code></td>\n</tr>\n</tbody>\n</table>\n<p style=\"text-align: center;\">\n表1 PKCS #7协议填充格式\n</p>\n<p>TLS协议为CBC分组密码套件所指定的填充格式与PKCS #7大同小异。<a href=\"https://datatracker.ietf.org/doc/html/rfc5246#section-6.2.3.2\">TLS 1.2记录层协议规范</a>定义了如下类C语言的区块结构:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> {</span></span><br><span class=\"line\"> opaque IV[SecurityParameters.record_iv_length];</span><br><span class=\"line\"> block-ciphered <span class=\"class\"><span class=\"keyword\">struct</span> {</span></span><br><span class=\"line\"> opaque content[TLSCompressed.length];</span><br><span class=\"line\"> opaque MAC[SecurityParameters.mac_length];</span><br><span class=\"line\"> uint8 padding[GenericBlockCipher.padding_length];</span><br><span class=\"line\"> uint8 padding_length;</span><br><span class=\"line\"> };</span><br><span class=\"line\">} GenericBlockCipher;</span><br></pre></td></tr></table></figure>\n<p>TLS协议规定用户数据、MAC、填充序列及填充长度一起构成待加密的明文<code>block-ciphered</code>。这里填充长度是PKCS #7所没有的,总是位于最后一个字节。填充序列字节数值就是填充长度。相应地,TLS协议下AES-CBC的填充格式变为:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">填充长度</th>\n<th style=\"text-align: center;\">填充字节</th>\n<th style=\"text-align: center;\">尾块(* 表示新尾部区块)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">无</td>\n<td style=\"text-align: center;\"><code>XX XX XX XX XX XX XX XX XX XX XX XX XX XX XX 00</code></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0x01</td>\n<td style=\"text-align: center;\"><code>XX XX XX XX XX XX XX XX XX XX XX XX XX XX 01 01</code></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">0x02</td>\n<td style=\"text-align: center;\"><code>XX XX XX XX XX XX XX XX XX XX XX XX XX 02 02 02</code></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">...</td>\n<td style=\"text-align: center;\">...</td>\n<td style=\"text-align: center;\"><code>...</code></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">14</td>\n<td style=\"text-align: center;\">0x0E</td>\n<td style=\"text-align: center;\"><code>XX 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E 0E</code></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">15</td>\n<td style=\"text-align: center;\">0x0F</td>\n<td style=\"text-align: center;\"><code>0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F</code>*</td>\n</tr>\n</tbody>\n</table>\n<p style=\"text-align: center;\">\n表2 TLS协议CBC分组密码填充格式\n</p>\n<p>可以看到,无论是在PKCS #7还是TLS协议下,都不可能出现无任何填充的情况,填充长度本身也是一种填充。这样设计的原因,是为了避免当明文长度正好为区块长度整数倍时,末端的内容被误认为填充的错误。</p>\n<h4 id=\"工作模式\">工作模式</h4>\n<p>填充满足了分组密码只能对确定长度的数据块进行处理的要求,接下来的工作模式描述了加密每一数据块的过程。</p>\n<p>最简单的模式是电子密码本(Electronic Codebook,缩写ECB)模式。顾名思义,在此模式下每个区块单独应用密钥进行加密,相同的明文块加密成同样的密文块,就如同参照电子密码本编译码一样。显然,这里区块长度不能太小,否则会被字典攻击暴力破解。AES使用128比特区块,可以打消这一顾虑。</p>\n<p>即便如此,ECB模式的简单性带来一个致命缺陷,因为它不能很好地隐藏数据模式,可能破坏严格的数据保密性。如下典型例子显示,左边是Linux吉祥物企鹅位图(bitmap)格式的图片,中间是经过ECB模式加密后样子。因为同样的颜色编码序列被加密成相同的密文,所以生成的文件重现原图的大致模式,失去保密性。此外,如果攻击者事先知晓了ECB模式密文对照的明文,可以使用重放攻击达到某些欺诈的目的。</p>\n<img src=\"AES-ECB-CBC.jpg\" />\n<p style=\"text-align: center;\">\n(来源:维基百科条目“分组密码工作模式”)\n</p>\n<p>为了弥补ECB模式的缺陷,1976年几位在IBM工作的密码学家发明了密码分组链接(Cipher Block Chaining,缩写CBC)工作模式。在这种模式下,每个明文块先与前一个密文块进行异或,结果再输入到分组加密模块产生自身的密文块。对于第一个明文块,由于不存在前一个密文块,需要使用初始化向量(Initialization Vector,缩写IV)代替。下图就是CBC工作模式的加密流程</p>\n<img src=\"AES-CBC-ENC.png\" />\n<p style=\"text-align: center;\">\nCBC加密流程(来源:维基百科条目“分组密码工作模式”)\n</p>\n<p>这种方法使得每个密文块都依赖于它前面的所有明文块,实现了跨区块的混淆与扩散。而对于同样的明文块序列和加密密钥,只要初始化向量不同,所得到的密文块序列就完全不同,数据的保密性大大增强。在实际的应用中,必须使用随机化的初始化向量,以保证安全。上面企鹅图片组中,最右边就是经过CBC模式加密后的结果,图片呈现随机噪声特征,无疑比ECB模式安全得多。</p>\n<p>解密过程包括对应的反向处理:每个密文块先输入到分组解密模块,然后将此中间结果与前一个密文块进行异或,得到相应的明文块;对于第一个密文块,使用初始化向量进行异或操作。下图显示CBC工作模式的解密流程</p>\n<img src=\"AES-CBC-DEC.png\" />\n<p style=\"text-align: center;\">\nCBC解密流程(来源:维基百科条目“分组密码工作模式”)\n</p>\n<p>注意到解密时每一块的处理只依赖于前一块密文,由于所有密文块都已经知晓,所以解密可以实现并行处理,也能支持对单独明文块解密的随机读访问。</p>\n<p>CBC工作模式的加密过程可以用数学公式表示为:</p>\n<p><span class=\"math display\">\\[\\begin{align}\nC_0&=IV\\\\\nC_i&=E_K(P_i⊕C_{i-1})\\tag{1}\n\\end{align}\\]</span></p>\n<p>这里<span class=\"math inline\">\\(P\\)</span>和<span class=\"math inline\">\\(C\\)</span>代表明文和密文,下标<span class=\"math inline\">\\(0、i-1、i\\)</span>为区块索引编号,<span class=\"math inline\">\\(E_K\\)</span>及<span class=\"math inline\">\\(D_K\\)</span>代表使用密钥<span class=\"math inline\">\\(K\\)</span>进行单块加密和解密运算函数(对于AES,即前述“AES标准”中所介绍的加解密算法)。对应解密过程的公式如下:</p>\n<p><span class=\"math display\">\\[\\begin{align}\nC_0&=IV\\\\\nP_i&=D_K(C_i)⊕C_{i-1}\\tag{2}\n\\end{align}\\]</span></p>\n<p>公式<span class=\"math inline\">\\((1)\\)</span>表明,初始化向量或明文中的单个比特位的变化会影响后续全部的密文,这体现很好的扩散效应。而上面公式<span class=\"math inline\">\\((2)\\)</span>也清楚地说明,解密本块明文只需要本块密文和上一块密文。</p>\n<h3 id=\"理解密文填充攻击\">理解密文填充攻击</h3>\n<p>熟悉了AES-CBC工作模式,现在就可以讲解针对它的密文填充攻击原理了。</p>\n<h4 id=\"攻击破解密文\">攻击破解密文</h4>\n<p>参考上一节CBC工作模式的解密过程公式<span class=\"math inline\">\\((2)\\)</span>,假定攻击者截获了密文块<span class=\"math inline\">\\(C_i\\)</span>和<span class=\"math inline\">\\(C_{i-1}\\)</span>,其目标是破解密文得到明文<span class=\"math inline\">\\(P_i\\)</span>。如果把应用对称密钥<span class=\"math inline\">\\(K\\)</span>解密当前块<span class=\"math inline\">\\(C_i\\)</span>后的中间结果记为<span class=\"math inline\">\\(P_i'\\)</span>,即<span class=\"math inline\">\\(P_i'=D_K(C_i)\\)</span>,则公式<span class=\"math inline\">\\((2)\\)</span>变成 <span class=\"math display\">\\[P_i=P_i'⊕C_{i-1}\\tag{3}\\]</span> 很显然,如果攻击者能得到<span class=\"math inline\">\\(P_i'\\)</span>,明文<span class=\"math inline\">\\(P_i\\)</span>就自然破解出来了。那么如何得到<span class=\"math inline\">\\(P_i'\\)</span>呢?直接从<span class=\"math inline\">\\(C_i\\)</span>分析得出<span class=\"math inline\">\\(P_i'\\)</span>是不可能的,那等同于破解AES。但是攻击者可以利用系统实现中的漏洞,通过操纵<span class=\"math inline\">\\(C_{i-1}\\)</span>来推导出正确的<span class=\"math inline\">\\(P_i'\\)</span>。</p>\n<p>将修改后的<span class=\"math inline\">\\(C_{i-1}\\)</span>记为<span class=\"math inline\">\\(C_{i-1}'\\)</span>,接收方处理<span class=\"math inline\">\\(C_{i-1}'\\)</span>后的结果记为<span class=\"math inline\">\\(P_i^*\\)</span>,则有 <span class=\"math display\">\\[P_i^*=P_i'⊕C_{i-1}'\\]</span> 由异或运算的定义导出 <span class=\"math display\">\\[P_i'=P_i^*⊕C_{i-1}'\\]</span> 由此可见,知道了<span class=\"math inline\">\\(P_i^*\\)</span>就能算出<span class=\"math inline\">\\(P_i'\\)</span>,从而回到<span class=\"math inline\">\\((3)\\)</span>式解出<span class=\"math inline\">\\(P_i\\)</span>。注意到,因为异或本身是比特位上的运算,这一结论对整个区块和区块内的单个字节都成立。</p>\n<p>那么怎么才能知道<span class=\"math inline\">\\(P_i^*\\)</span>呢?再看看CBC解密流程图,如果发送方使用PKCS #7格式填充,则接收方需要先解密所有密文块、验证尾部填充符合PKCS #7规范,随后移除填充,最后返回解密后的明文信息给应用程序。 <strong>为了系统容错和调试的需要,接收方(服务器端)的实现在验证填充失败时,常常返回特定的“填充无效”出错代码。</strong>这本是为方便系统管理员和用户的一个设计,可就是这一功能成为被攻击者利用来实现密文破解的突破口。</p>\n<p>虽然收到“填充无效”出错代码不能说明什么,因为无效填充的字节序列太多了;但是没有收到“填充无效”消息却告诉攻击者一个重要的信息:<span class=\"math inline\">\\(P_i^*\\)</span>的末端字节一定是16种可能组合之一。特别地,攻击者操纵<span class=\"math inline\">\\(C_{i-1}\\)</span>的单个末端字节,发送<span class=\"math inline\">\\(C_{i-1}'\\)</span>多次尝试,观测填充验证的结果,可以完全定位<span class=\"math inline\">\\(P_i^*\\)</span>某个末端字节的数值,进而破解<span class=\"math inline\">\\(P_i\\)</span>的对应字节。破解单个末端字节之后,攻击者还能构造出新的准填充字节序列,重复此过程从右到左逐个破解余下的字节。</p>\n<p>这正是瓦德奈发现的密文填充攻击的机理。下面针对AES-CBC工作模式做出详细解释。假定攻击者截获的密文包含<span class=\"math inline\">\\(n\\)</span>个区块,具体的攻击破解过程如下:</p>\n<ol type=\"1\">\n<li><p><strong>确定尾块填充的长度:</strong>对于最后一个区块(即尾块)<span class=\"math inline\">\\(C_n\\)</span>,可以肯定的是它一定包含填充,最少一个0x01,最多16个0x10(参见前面表1)。攻击者从<span class=\"math inline\">\\(C_{n-1}\\)</span>的左边第一个字节开始,将其修改成 <span class=\"math display\">\\[C_{n-1}'[0]=C_{n-1}[0]⊕{\\mathrm {0x10}}\\tag{4}\\]</span> 然后将<span class=\"math inline\">\\(C_{n-1}'\\)</span>与<span class=\"math inline\">\\(C_n\\)</span>一起发给接收方。接收方执行AES-CBC解密流程得到左边第一个字节为 <span class=\"math display\">\\[\\begin{align}\nP_n^*[0]&=P_n'[0]⊕C_{n-1}'[0]\\\\\n &=P_n'[0]⊕(C_{n-1}[0]⊕{\\mathrm {0x10}})\\\\\n &=(P_n'[0]⊕C_{n-1})[0]⊕{\\mathrm {0x10}}\\\\\n &=P_n[0]⊕{\\mathrm {0x10}}\n\\end{align}\\]</span> 以上推演基于异或运算满足交换律的特性。这时如果接收方报告“填充无效”,表明<span class=\"math inline\">\\(P_n^*[0]\\)</span>为填充的一部分,填充长度一定是16。如果接收方没有返回此错误,那么<span class=\"math inline\">\\(C_{n-1}[0]\\)</span>的变化不影响填充验证,填充长度一定小于16,攻击者再按照<span class=\"math inline\">\\((4)\\)</span>式修改第二个字节,再次发给接收方。这时如果接收方报告“填充无效”,则填充长度一定是15。依此类推,从左到右逐字节递进,攻击者就可以得出尾块填充的确切长度。</p></li>\n<li><p><strong>破解尾块数据明文:</strong>得到尾块填充长度后,攻击者就可以从右到左逐个破解尾块数据明文。假设填充长度为<span class=\"math inline\">\\(L\\)</span>,由PKCS #7规范得出明文块最后<span class=\"math inline\">\\(L\\)</span>个字节数值全为<span class=\"math inline\">\\(L\\)</span>,为通用起见记为<span class=\"math inline\">\\(P_n[j]=M\\)</span>。攻击者先如下修改<span class=\"math inline\">\\(C_{n-1}\\)</span>的最后<span class=\"math inline\">\\(L\\)</span>个字节 <span class=\"math display\">\\[C_{n-1}'[j]=C_{n-1}[j]⊕M⊕(L+1)\\qquad j=(16-L),\\cdots,15\\tag{5}\\]</span> 如此接收方执行AES-CBC解密后得到最后<span class=\"math inline\">\\(L\\)</span>个字节为 <span class=\"math display\">\\[\\begin{align}\nP_n^*[j]&=(P_n'[j]⊕C_{n-1}[j])⊕M⊕(L+1)\\\\\n &=P_n[j]⊕M⊕(L+1)\\\\\n &=M⊕M⊕(L+1)\\\\\n &=L+1\n\\end{align}\\]</span> 所以这样做的结果等于强制解密后最后<span class=\"math inline\">\\(L\\)</span>个字节为<span class=\"math inline\">\\(L+1\\)</span>。然后,攻击者尝试对<span class=\"math inline\">\\(C_{n-1}\\)</span>倒数第<span class=\"math inline\">\\(L+1\\)</span>个字节(就是下一个要破解的字节)执行 <span class=\"math display\">\\[\\begin{align}\nC_{n-1}'[15-L]&=C_{n-1}[15-L]⊕X\\tag{6}\\\\\nP_n^*[15-L]&=(P_n'[15-L]⊕C_{n-1}[15-L])⊕X\\\\\n &=P_n[15-L]⊕X\\tag{7}\\\\\n\\end{align}\\]</span> <span class=\"math inline\">\\(X\\)</span>的取值范围为0x00-0xFF。在这个区间有且仅有一个<span class=\"math inline\">\\(X\\)</span>,使得<span class=\"math inline\">\\(P_n^*[15-L]\\)</span>为<span class=\"math inline\">\\(L+1\\)</span>,即倒数第<span class=\"math inline\">\\(L+1\\)</span>个字节也为<span class=\"math inline\">\\(L+1\\)</span>。这时整个区块以<span class=\"math inline\">\\(L+1\\)</span>个<span class=\"math inline\">\\(L+1\\)</span>结尾,是唯一攻击者不会收到“填充无效”应答的情况。根据<span class=\"math inline\">\\((7)\\)</span>式导出 <span class=\"math display\">\\[P_n[15-L]=(L+1)⊕X\\tag{8}\\]</span> 所以只要找到<span class=\"math inline\">\\(X\\)</span>,攻击者马上可以算出明文倒数第<span class=\"math inline\">\\(L+1\\)</span>个字节为<span class=\"math inline\">\\((L+1)⊕X\\)</span>。接下来将<span class=\"math inline\">\\(L\\)</span>递增,重复此过程,继续破解倒数第<span class=\"math inline\">\\(L+1\\)</span>个明文字节。如下,攻击者就能破解尾块全部数据字节:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">.. .. .. .. .. .. .. .. .. .. .. .. .. .. XX 02</span><br><span class=\"line\">.. .. .. .. .. .. .. .. .. .. .. .. .. XX 03 03</span><br><span class=\"line\">.. .. ..</span><br><span class=\"line\">.. XX 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F</span><br><span class=\"line\">XX 10 10 10 10 10 10 10 10 10 10 10 10 10 10 10</span><br></pre></td></tr></table></figure> 可以看出,在这种攻击方法下破解每一个明文字节XX平均需要发送128次修改的密文。这是很有效的。</p></li>\n<li><p><strong>破解非尾块数据明文:</strong>对于非尾部区块,即<span class=\"math inline\">\\(i<n\\)</span>时,攻击的方式没有什么本质不同。攻击者依照<span class=\"math inline\">\\((6)\\)</span>式更改<span class=\"math inline\">\\(C_{i-1}\\)</span>的最后一个字节(即<span class=\"math inline\">\\(C_{i-1}'[15]\\)</span>),然后发送<span class=\"math inline\">\\((C_{i-1}',C_i)\\)</span>至服务器。这时<span class=\"math inline\">\\(C_i\\)</span>会被视为尾块,当接收方返回“填充无效”的消息,攻击者尝试下一个<span class=\"math inline\">\\(X\\)</span>。当接收方没有返回“填充无效”的消息时,需要区分下面几种情况:</p>\n<ul>\n<li><p>如果攻击者事先知道明文数据字节都大于0x10,比如都是ASCII表中可打印的字符,那么可以确信解密后的最后一个字节<span class=\"math inline\">\\(P_i^*[15]\\)</span>为0x01。这是因为系统认定填充一定存在,而0x01是唯一可能通过验证的单字节填充情况。根据<span class=\"math inline\">\\((8)\\)</span>式得到<span class=\"math display\">\\[{P_i[15]=\\mathrm {0x01}}⊕X\\tag{9}\\]</span>就此破解了最右边字节。</p></li>\n<li><p>如果明文数据字节没有取值范围限制,则解密后的最后一个字节<span class=\"math inline\">\\(P_i^*[15]\\)</span>要么为0x01,要么为0x02-0x10之间的某个值。前者一定会出现,后者会碰巧当该明文块数据为以下15种之一的式样时发生</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">.. .. .. .. .. .. .. .. .. .. .. .. .. .. 02 ..</span><br><span class=\"line\">.. .. .. .. .. .. .. .. .. .. .. .. .. 03 03 ..</span><br><span class=\"line\">.. .. ..</span><br><span class=\"line\">.. 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F ..</span><br><span class=\"line\">10 10 10 10 10 10 10 10 10 10 10 10 10 10 10 ..</span><br></pre></td></tr></table></figure> 这时接收方一样也认为填充正确。如何分辨这两者呢?有一个简单的办法。攻击者在没有收到“填充无效”消息时,修改<span class=\"math inline\">\\(C_{i-1}\\)</span>的倒数第二个字节<span class=\"math inline\">\\(C_{i-1}[14]\\)</span>,再重发给接收者。如果是前者,接收方还是不会返回“填充无效”的消息,因为倒数第二个字节的变化对此没有影响;如果是后者,倒数第二个字节的变化破坏了填充格式,接收方返回“填充无效”的消息。攻击者鉴别出前者后,应用<span class=\"math inline\">\\((9)\\)</span>式就可以了。</p></li>\n</ul>\n<p>接下来对非尾块其它数据明文字节的破解,遵照上面第二步(破解尾块数据明文)相同的处理,参考<span class=\"math inline\">\\((5-8)\\)</span>式从右到左逐个字节破解。说明一下,对第一个密文块<span class=\"math inline\">\\(C_1\\)</span>,攻击需要的前一块<span class=\"math inline\">\\(C_0\\)</span>就是<span class=\"math inline\">\\(IV\\)</span>。如果攻击者无法获取<span class=\"math inline\">\\(IV\\)</span>,就不能破解<span class=\"math inline\">\\(C_1\\)</span>。</p>\n<p>总结统计攻击非尾块时,如果明文数据字节都大于0x10,破解一个AES-CBC密文块平均需要发送2048(128x16)次查询;如果明文数据字节没有取值范围限制,则额外需要多一次查询。</p></li>\n</ol>\n<p>以上密文填充攻击的步骤,稍加调整同样适用于TLS协议规定的CBC分组密码填充格式。TLS协议下,第一步<span class=\"math inline\">\\((4)\\)</span>式要改为 <span class=\"math display\">\\[C_{n-1}'[0]=C_{n-1}[0]⊕{\\mathrm {0x0F}}\\]</span> 在确定尾块填充的长度<span class=\"math inline\">\\(L\\)</span>后,实际的填充字节不为<span class=\"math inline\">\\(L\\)</span>,而是<span class=\"math inline\">\\(L-1\\)</span>。由此,第二和第三步中的<span class=\"math inline\">\\((5)\\)</span>、<span class=\"math inline\">\\((8)\\)</span>和<span class=\"math inline\">\\((9)\\)</span>式相应变成 <span class=\"math display\">\\[\\begin{align}\nC_{n-1}'[j]&=C_{n-1}[j]⊕M⊕L\\qquad j=(16-L),\\cdots,15\\\\\nP_n[15-L]&=L⊕X\\\\\nP_n[15]&=\\mathrm {0x00}⊕X=X\n\\end{align}\\]</span> 其它细节变化参考上面每一步的说明,这里不再赘述。</p>\n<h4 id=\"cbc-r伪造明文\">CBC-R伪造明文</h4>\n<p>CBC密文填充攻击的目的是破解截获的密文,从而无需密钥就可以恢复明文。那么它可以用来伪造能被对方接受的明文吗?两位信息安全专家Juliano Rizzo和Thai Duong给出了肯定的答案<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>。仔细观察<span class=\"math inline\">\\((3)\\)</span>式,很显然密文填充攻击完全恢复了明文<span class=\"math inline\">\\(P_i\\)</span>和中间结果<span class=\"math inline\">\\(P_i'\\)</span>。作为选择密文攻击,攻击者当然可以任意更改<span class=\"math inline\">\\(C_{i-1}\\)</span>以控制接收方解密后所见的<span class=\"math inline\">\\(P_i\\)</span>。也就是说,攻击者可以利用CBC密文填充攻击,在密钥未知的情况加密任意长度的消息。</p>\n<p>Rizzo和Duong为这种变种攻击设计了流程,并命名为CBC-R(R代表Reverse,即逆向操作)。CBC-R的流程如下图所示</p>\n<p><img src=\"CBC-R-ENC.png\" /></p>\n<p>开始时,攻击者选择一段随机的密文块<span class=\"math inline\">\\(C_i\\)</span>,执行密文填充攻击得到中间结果,标记这一操作为<span class=\"math inline\">\\(D_{PaddingOracle}(C_i)\\)</span>。因为 <span class=\"math display\">\\[P_i=D_{PaddingOracle}(C_i)⊕C_{i−1}\\]</span> 所以攻击者控制<span class=\"math inline\">\\(C_{i-1}\\)</span>之后,可以让<span class=\"math inline\">\\(P_i\\)</span>变成任何值。假设攻击者想将<span class=\"math inline\">\\(P_i\\)</span>设置为<span class=\"math inline\">\\(P_x\\)</span>,只要使 <span class=\"math display\">\\[C_{i−1}=P_x⊕D_{PaddingOracle}(C_i)\\]</span> 就行了。但是这样也会让<span class=\"math inline\">\\(C_{i-1}\\)</span>解密后的结果不是攻击者想要的<span class=\"math inline\">\\(P_{i-1}\\)</span>,怎么办?没问题,只要重复把<span class=\"math inline\">\\(C_{i-1}\\)</span>输入<span class=\"math inline\">\\(D_{PaddingOracle}()\\)</span>中去产生新的中间结果,再生成 <span class=\"math display\">\\[C_{i−2}=P_{i-1}⊕D_{PaddingOracle}(C_{i-1})\\]</span> 这样逆向循环迭代,就能构造伪造明文对应的整个密文序列。</p>\n<p>对于16字节AES块,CBC-R伪造明文攻击的算法伪代码如下</p>\n<blockquote>\n<ol type=\"1\">\n<li>选择要构造的明文消息,分割成<span class=\"math inline\">\\(N\\)</span>个16字节块<span class=\"math inline\">\\(P_1,P_2,..,P_n\\)</span></li>\n<li>选择随机字节<span class=\"math inline\">\\(r_1,r_2,..,r_{16}\\)</span>,设定<span class=\"math inline\">\\(C_n=r_1|r_2|...|r_{16}\\)</span></li>\n<li>从 <span class=\"math inline\">\\(i=n\\)</span> 递减到 <span class=\"math inline\">\\(2\\)</span>:<br />\n<span class=\"math inline\">\\(C_{i−1}=P_i⊕D_{PaddingOracle}(C_i)\\)</span></li>\n<li><span class=\"math inline\">\\(IV=P_1⊕D_{PaddingOracle}(C_1)\\)</span></li>\n<li>输出<span class=\"math inline\">\\(IV\\)</span>和密文序列<span class=\"math inline\">\\(C =C_1|C_2|...|C_n\\)</span></li>\n</ol>\n</blockquote>\n<h4 id=\"现实中的攻击\">现实中的攻击</h4>\n<p>实际中,攻击者根本不需要入侵ISP或其它复杂的网络流量截取手段,只要在同样的本地网上借助ARP欺骗技术就可以实施密文填充攻击。攻击者诱导目标主机将本应该发到路由器的数据包转到攻击者的主机,就可以查看、修改加密的数据并度量浏览器发到服务器的消息所需的时间。攻击者还能通过插入JavaScript脚本到非加密的网站的方式,让用户的浏览器重复发送到目标HTTPS站点的请求。这些请求包含用作攻击的登录小信息块和CSRF(跨站请求伪造)标识。CBC-R就被用来攻击ASP.NET网页应用框架构建的网站,攻击者利用伪造的会话盗取服务器上限制访问的文件和资源。</p>\n<p>在常见的TLS应用中,服务器处理收到的AES-CBC密文时,对填充验证和消息认证的不同结果会做出不同的反应。对如下三种情况,</p>\n<ol type=\"1\">\n<li>无效填充</li>\n<li>填充正确,HMAC错误</li>\n<li>填充正确,HMAC正确</li>\n</ol>\n<p>旧版TLS服务器对以上1和2返回不同的出错代码,正是这看似一点点的侧信道(side-channel)信息泄漏,使密文填充攻击成为可能。在漏洞得到大规模曝光后,TLS服务器端迅速做了软件更新,对1和2返回同样的出错代码,以图杜绝此类攻击。</p>\n<p>然而,攻击者很快挖掘出了另一个可用的侧信道——时序(timing)。攻击者注意到,尽管现在1和2返回同样的出错代码,但是由于2执行了附加的HMAC计算,其返回的时间更长一些。虽然时间度量会受到多种因素的影响,只要攻击者尝试足够多的次数,一定可以从统计上将1和2区分开来,密文填充攻击还是可能的。对此,TLS实现者马上又做了修改,即使填充无效也要执行HMAC计算。此时假定0填充,对全部明文数据计算HMAC。实现者借此希望1、2和3的执行时间恒定。</p>\n<p>不幸的是,这只是他们的一厢情愿。2013年,“<a href=\"http://www.isg.rhul.ac.uk/tls/Lucky13.html\">幸运十三攻击</a>”出现,攻击的着眼点还是时序。研究者们通过大量的测试和实验观察到,对不同长度的数据HMAC的运算时间是不同的,这样就重开了密文填充攻击的大门。要堵住这个漏洞,必须实现与数据长度独立的、完全时间恒定的HMAC函数,这是具有相当复杂性和挑战性的程序实现。尽管最后TLS软件开发者们找到了解决方案,但是整个业界对CBC工作模式与认证后加密(MAC-then-encrypt)相结合的应用已失去信心。各大网络服务站点纷纷将CBC密码套件下架,而TLS 1.3完全摒弃非AEAD的密码套件也提供了佐证。</p>\n<h3 id=\"python编程实现\">Python编程实现</h3>\n<p>最后一部分,让我们来编程实践一下AES-CBS密文填充攻击。</p>\n<h4 id=\"工具和辅助函数\">工具和辅助函数</h4>\n<p>首先我们需要一个实现字节序列异或操作的函数。利用Python的内建函数<code>zip()</code>,可将输入的两个字节序列中对应的字节打包成一个个元组(tuple)并组成列表(list),再应用<code>for</code>循环对列表成员逐个异或,结果添加到新的字节序列。由此得到的代码如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">bytes_xor</span>(<span class=\"params\">x, y</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''XOR two given byte strings of same length'''</span></span><br><span class=\"line\"> ret = <span class=\"string\">b''</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> b1, b2 <span class=\"keyword\">in</span> <span class=\"built_in\">zip</span>(x, y):</span><br><span class=\"line\"> ret += <span class=\"built_in\">bytes</span>([b1 ^ b2])</span><br><span class=\"line\"> <span class=\"keyword\">return</span> ret</span><br></pre></td></tr></table></figure>\n<p>其次,AES-CBC工作模式需要一个函数能将给定字节序列按照指定块大小分组。使用列表推导式(list comprehension)可以写出简洁的单行代码,实现对输入字节序列切片的功能:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">AES_128_BLOCK_SIZE = <span class=\"number\">16</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">split_bytes_blocks</span>(<span class=\"params\">bytes_str, block_size=AES_128_BLOCK_SIZE</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> Split bytes into blocks for given block size, note the</span></span><br><span class=\"line\"><span class=\"string\"> last block might be a short one if the total length is</span></span><br><span class=\"line\"><span class=\"string\"> not a multiple of block size.</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> [bytes_str[i:i + block_size]</span><br><span class=\"line\"> <span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">0</span>, <span class=\"built_in\">len</span>(bytes_str), block_size)]</span><br></pre></td></tr></table></figure>\n<p>以上实现中块大小默认为AES区块长度16字节(128比特),函数返回字节序列列表。除了最后一个字节序列可能不足块大小以外,其他字节序列都是定长的。</p>\n<p>为了测试的目的,另外还需要一个辅助函数生成指定长度的随机字符串。Python3的<code>string</code>模块提供了可打印字符串常量<code>printable</code>,与<code>random</code>模块的<code>choice()</code>函数相结合,也可以一行实现:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">get_random_string</span>(<span class=\"params\">length</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> Return random string of given length. String has the</span></span><br><span class=\"line\"><span class=\"string\"> combination of all printable ASCII characters.</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"string\">''</span>.join(choice(printable) <span class=\"keyword\">for</span> _ <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(length))</span><br></pre></td></tr></table></figure>\n<h4 id=\"填充及验证函数\">填充及验证函数</h4>\n<p>PKCS #7填充规则的Python实现很直观,两行程序就可以了。第一行将数据字节长度对区块长度取余,再将余数从区块长度中减去,得到的就是填充字节的长度,也是填充字节本身的数值;第二行函数返回尾部添加重复填充后的字节序列:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">pkcs7_padding</span>(<span class=\"params\">data, block_size=AES_128_BLOCK_SIZE</span>):</span></span><br><span class=\"line\"> padding_length = block_size - <span class=\"built_in\">len</span>(data) % block_size</span><br><span class=\"line\"> <span class=\"keyword\">return</span> data + <span class=\"built_in\">bytes</span>([padding_length]) * padding_length</span><br></pre></td></tr></table></figure>\n<p>PKCS #7填充的验证和去除也很简单。我们可以自定义特殊类型的填充异常类,然后根据不同的出错条件引发。出错条件包括:</p>\n<ol type=\"1\">\n<li>数据字节总长度不是区块长度的整数倍</li>\n<li>填充字节本身的数值为0或大于区块长度</li>\n<li>数据字节序列尾部并非重复填充后的字节</li>\n</ol>\n<p>如果没有出现以上任何错误,就返回去除重复填充字节的数据字节序列。函数的实现如下所示:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">PaddingError</span>(<span class=\"params\">Exception</span>):</span></span><br><span class=\"line\"> <span class=\"keyword\">pass</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">pkcs7_stripping</span>(<span class=\"params\">data, block_size=AES_128_BLOCK_SIZE</span>):</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"built_in\">len</span>(data) % block_size > <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"keyword\">raise</span> PaddingError</span><br><span class=\"line\"></span><br><span class=\"line\"> padding_length = data[-<span class=\"number\">1</span>]</span><br><span class=\"line\"> <span class=\"keyword\">if</span> padding_length > block_size <span class=\"keyword\">or</span> padding_length < <span class=\"number\">1</span>:</span><br><span class=\"line\"> <span class=\"keyword\">raise</span> PaddingError</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">not</span> data.endswith(<span class=\"built_in\">bytes</span>([padding_length]) * padding_length):</span><br><span class=\"line\"> <span class=\"keyword\">raise</span> PaddingError</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> data[:-padding_length]</span><br></pre></td></tr></table></figure>\n<p>为了仿真密文填充攻击,必须写一个函数模拟验证应答。函数的填充验证条件和上面的出错条件2、3一样,但是出错时不引发异常,而只是返回布尔值<code>False</code>:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">pkcs7_padding_oracle</span>(<span class=\"params\">data, block_size=AES_128_BLOCK_SIZE</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''Padding Oracle API: return False for bad padding'''</span></span><br><span class=\"line\"> padding_length = data[-<span class=\"number\">1</span>]</span><br><span class=\"line\"> <span class=\"keyword\">if</span> padding_length > block_size <span class=\"keyword\">or</span> padding_length < <span class=\"number\">1</span>:</span><br><span class=\"line\"> ret = <span class=\"literal\">False</span></span><br><span class=\"line\"> <span class=\"keyword\">elif</span> <span class=\"keyword\">not</span> data.endswith(<span class=\"built_in\">bytes</span>([padding_length]) * padding_length):</span><br><span class=\"line\"> ret = <span class=\"literal\">False</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> ret = <span class=\"literal\">True</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> ret</span><br></pre></td></tr></table></figure>\n<h4 id=\"加密和解密函数\">加密和解密函数</h4>\n<p>虽然Python编程实现AES加密和解密并非特别复杂,但是这里的重点是仿真攻击AES-CBC工作模式,所以我们可以尝试调用现存的密码库进行加解密。<a href=\"https://cryptography.io/en/latest/\">Cryptography</a>是一个强大的开源Python模块包(支持Python 3.6+),其设计目标是成为开发者的“密码学标准库”。Cryptography包括高级的安全“菜谱”(recipes)层和低级的有风险“密码学原语”层(也被称为hazardous materials layer,简称hazmat)。低级hazmat层为资深研发者提供了广泛的API实现各种密码学功能,其后端与OpenSSL直接对接。</p>\n<p>这里的技巧是,如果我们对单个128比特的数据分组调用AES-ECB模式的加解密函数,实质就等同于黑箱实现了AES的加解密功能。以下就是应用Cryptography密码库API的128比特密钥加解密函数代码:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">from</span> cryptography.hazmat.primitives.ciphers <span class=\"keyword\">import</span> Cipher</span><br><span class=\"line\"><span class=\"keyword\">from</span> cryptography.hazmat.primitives.ciphers.algorithms <span class=\"keyword\">import</span> AES</span><br><span class=\"line\"><span class=\"keyword\">from</span> cryptography.hazmat.primitives.ciphers.modes <span class=\"keyword\">import</span> ECB</span><br><span class=\"line\"><span class=\"keyword\">from</span> cryptography.hazmat.backends <span class=\"keyword\">import</span> default_backend</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">aes_128_encrypt</span>(<span class=\"params\">key, msg_block</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> Single 128-byte block AES encryption leveraging Cryptography</span></span><br><span class=\"line\"><span class=\"string\"> library Cipher object AES-ECB APIs</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> aes_cipher = Cipher(AES(key), ECB(), default_backend())</span><br><span class=\"line\"> aes_encryptor = aes_cipher.encryptor()</span><br><span class=\"line\"> <span class=\"keyword\">return</span> aes_encryptor.update(msg_block) + aes_encryptor.finalize()</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">aes_128_decrypt</span>(<span class=\"params\">key, cipher_block</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> Single 128-byte block AES decryption leveraging Cryptography</span></span><br><span class=\"line\"><span class=\"string\"> library Cipher object AES-ECB APIs</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> aes_cipher = Cipher(AES(key), ECB(), default_backend())</span><br><span class=\"line\"> aes_decryptor = aes_cipher.decryptor()</span><br><span class=\"line\"> <span class=\"keyword\">return</span> aes_decryptor.update(cipher_block) + aes_decryptor.finalize()</span><br></pre></td></tr></table></figure>\n<p>AES-128加解密函数就绪之后,参考CBC工作模式下加解密的流程图,就可以应用迭代很快写出AES-CBC-128的加解密函数了:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">aes_128_cbc_encrypt</span>(<span class=\"params\">iv, key, msg_text</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''AES-CBC-128 encryption with given IV and Key'''</span></span><br><span class=\"line\"> <span class=\"comment\"># padding then split the cipher_text to block of size 16-bytes</span></span><br><span class=\"line\"> msg_text = pkcs7_padding(msg_text, AES_128_BLOCK_SIZE)</span><br><span class=\"line\"> blocks = split_bytes_blocks(msg_text)</span><br><span class=\"line\"></span><br><span class=\"line\"> prev = iv</span><br><span class=\"line\"> cipher_text = <span class=\"string\">b''</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> block <span class=\"keyword\">in</span> blocks:</span><br><span class=\"line\"> block = bytes_xor(prev, block)</span><br><span class=\"line\"> encipher = aes_128_encrypt(key, block)</span><br><span class=\"line\"> prev = encipher</span><br><span class=\"line\"> cipher_text += encipher</span><br><span class=\"line\"> <span class=\"keyword\">return</span> cipher_text</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">aes_128_cbc_decrypt_oracle</span>(<span class=\"params\">iv, key, cipher_text</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> AES-CBC-128 decryption with given IV and Key then do padding oracle:</span></span><br><span class=\"line\"><span class=\"string\"> Return boolean:</span></span><br><span class=\"line\"><span class=\"string\"> True : Decrypted plain text has valid padding</span></span><br><span class=\"line\"><span class=\"string\"> False : Padding error seen with plain text decrypted</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> <span class=\"comment\"># split the cipher_text to block of size 16-bytes</span></span><br><span class=\"line\"> blocks = split_bytes_blocks(cipher_text)</span><br><span class=\"line\"></span><br><span class=\"line\"> prev = iv</span><br><span class=\"line\"> plain_text = <span class=\"string\">b''</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> block <span class=\"keyword\">in</span> blocks:</span><br><span class=\"line\"> decipher = aes_128_decrypt(key, block)</span><br><span class=\"line\"> plain_text += bytes_xor(prev, decipher)</span><br><span class=\"line\"> prev = block</span><br><span class=\"line\"> <span class=\"keyword\">return</span> pkcs7_padding_oracle(plain_text)</span><br></pre></td></tr></table></figure>\n<p>说明,在上述AES-CBC-128加密函数中,先对输入的完整明文字节序列进行填充、分组,然后迭代,输出加密后的密文;而AES-CBC-128解密函数预设输入的密文字节序列的总长度是16的倍数,直接分组再迭代,解密后明文送到前面定义的填充验证应答函数<code>pkcs7_padding_oracle()</code>,其输出就是整个函数的输出结果。所以<code>aes_128_cbc_decrypt_oracle()</code>返回True表明解密后的明文尾部填充无误,返回False时标示其存在填充错误。</p>\n<h4 id=\"密文填充攻击函数\">密文填充攻击函数</h4>\n<p>与前述的AES-CBC攻击破解过程对应,下面的函数实现第一步“<strong>确定尾块填充的长度</strong>”。函数输入为最后的两个密文区块及IV和密钥,输出为尾块填充的长度。内嵌的注释给出了简要说明。</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">crack_padding_length</span>(<span class=\"params\">seclast_cblock, ending_cblock, iv, key</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> This function returns the padding length in the ending ciphertext</span></span><br><span class=\"line\"><span class=\"string\"> block. It needs last two ciphertext blocks. The input IV and key</span></span><br><span class=\"line\"><span class=\"string\"> are passed to the AES-CBC-128 padding oracle function invoked inside.</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\"> To find out the padding length, it starts from the 1st byte of the</span></span><br><span class=\"line\"><span class=\"string\"> ending block and makes single byte change each round via scrambling</span></span><br><span class=\"line\"><span class=\"string\"> the corresponding byte of the penultimate block because</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\"> P(n) = D(C(n), k) ^ C(n-1)</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\"> The forged ciphertext blocks are fed to the oracle. If that returns</span></span><br><span class=\"line\"><span class=\"string\"> False, meaning it breaks the padding rule, the padding length would</span></span><br><span class=\"line\"><span class=\"string\"> be BLOCK size minus the index from the left.</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> c_array = <span class=\"built_in\">bytearray</span>(seclast_cblock)</span><br><span class=\"line\"> padding_len = <span class=\"number\">0</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">16</span>):</span><br><span class=\"line\"> c_array[i] ^= <span class=\"number\">16</span></span><br><span class=\"line\"> feed_ctxt = <span class=\"built_in\">bytes</span>(c_array) + ending_cblock</span><br><span class=\"line\"> <span class=\"keyword\">if</span> <span class=\"keyword\">not</span> aes_128_cbc_decrypt_oracle(iv, key, feed_ctxt):</span><br><span class=\"line\"> padding_len = <span class=\"number\">16</span> - i</span><br><span class=\"line\"> <span class=\"keyword\">break</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> padding_len</span><br></pre></td></tr></table></figure>\n<p>攻击破解过程的的第二和第三步实现,可以组合在一个通用函数中。如下所示,函数<code>crack_cipher_block()</code>接受前一个密文块、当前密文块、IV、密钥和当前块的填充长度,运行结束给出破解出来的当前块明文。如果当前块是尾块,<code>plen</code>就是上面<code>crack_padding_length()</code>的输出;否则总是0。如果当前块是第一块,则前一个密文块就是IV。需要特别指出的是,为简化起见,此函数实现假定明文数据字节都大于0x10,不考虑第三步“<strong>破解非尾块数据明文</strong>”提到的“如果明文数据字节没有取值范围限制”的情况。</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">crack_cipher_block</span>(<span class=\"params\">prev_ctxt, curr_ctxt, iv, key, plen</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> This function takes two cipher blocks and forge the 1st one to</span></span><br><span class=\"line\"><span class=\"string\"> hack out the plain text of the 2nd block, one byte at a time.</span></span><br><span class=\"line\"><span class=\"string\"> Return the cracked plain text of the 2nd block.</span></span><br><span class=\"line\"><span class=\"string\"> Input:</span></span><br><span class=\"line\"><span class=\"string\"> prev_ctxt The previous ciphertext block</span></span><br><span class=\"line\"><span class=\"string\"> curr_ctxt The current ciphertext block as the target</span></span><br><span class=\"line\"><span class=\"string\"> iv 16-byte AES-128 IV</span></span><br><span class=\"line\"><span class=\"string\"> key 16-byte AES-128 Key</span></span><br><span class=\"line\"><span class=\"string\"> plen The padding length of the target block</span></span><br><span class=\"line\"><span class=\"string\"> Output:</span></span><br><span class=\"line\"><span class=\"string\"> Byte array of the plain text block</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> plain_txt = <span class=\"built_in\">bytearray</span>(<span class=\"number\">16</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> plen == <span class=\"number\">16</span>:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"string\">b'\\x10'</span> * <span class=\"number\">16</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># This is to crack last block. First time we need to set all</span></span><br><span class=\"line\"> <span class=\"comment\"># last plen bytes of plain_txt to the value of plen.</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> plen != <span class=\"number\">0</span>:</span><br><span class=\"line\"> plain_txt[-plen:] = [plen] * plen</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">16</span> - plen):</span><br><span class=\"line\"> <span class=\"comment\"># The target byte index is (16-plen-i-1)</span></span><br><span class=\"line\"> <span class=\"comment\"># First presetting the bytes behind the target byte</span></span><br><span class=\"line\"> prev_array = <span class=\"built_in\">bytearray</span>(prev_ctxt)</span><br><span class=\"line\"> <span class=\"keyword\">for</span> j <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(plen + i):</span><br><span class=\"line\"> prev_array[-j - <span class=\"number\">1</span>] ^= (plen + i + <span class=\"number\">1</span>) ^ plain_txt[-j - <span class=\"number\">1</span>]</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># Now cracking the target byte</span></span><br><span class=\"line\"> target_original = prev_array[-plen - i - <span class=\"number\">1</span>]</span><br><span class=\"line\"> <span class=\"keyword\">for</span> k <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">256</span>):</span><br><span class=\"line\"> prev_array[-plen - i - <span class=\"number\">1</span>] = target_original ^ (k ^ (plen + i + <span class=\"number\">1</span>))</span><br><span class=\"line\"> feed_ctxt = <span class=\"built_in\">bytes</span>(prev_array) + curr_ctxt</span><br><span class=\"line\"> <span class=\"comment\"># feed to padding oracle</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> aes_128_cbc_decrypt_oracle(iv, key, feed_ctxt):</span><br><span class=\"line\"> plain_txt[<span class=\"number\">15</span> - plen - i] = k</span><br><span class=\"line\"> <span class=\"keyword\">break</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">bytes</span>(plain_txt)</span><br></pre></td></tr></table></figure>\n<h4 id=\"攻击程序汇总\">攻击程序汇总</h4>\n<p>至此,所有的函数模块都已准备就绪,我们可以将它们集成到一个完整的仿真测试过程中:</p>\n<ol type=\"1\">\n<li>调用<code>get_random_string()</code>生成随机长度可打印字符串</li>\n<li>将该字符串转化为明文字节序列</li>\n<li>调用库函数<code>urandom()</code>生成随机IV和密钥</li>\n<li>调用<code>aes_128_cbc_encrypt()</code>加密明文字节序列</li>\n<li>调用<code>split_bytes_blocks()</code>分割上一步输出的密文字节序列,输出密文块列表</li>\n<li>将IV转化为字节序列,添加到上一步生成的密文块列表的头部</li>\n<li>将密文块列表的最后两个块输入到<code>crack_padding_length()</code>,得出尾块填充长度</li>\n<li>启动循环,从密文块列表尾部开始,调用<code>crack_cipher_block()</code>逐个破解</li>\n<li>调用<code>pkcs7_stripping()</code>从破解的明文块尾部去除PKCS #7填充</li>\n<li>比较破解出的明文和原始明文字节序列</li>\n</ol>\n<p>程序实现如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">ptxt = get_random_string(randint(<span class=\"number\">10</span>,<span class=\"number\">100</span>));</span><br><span class=\"line\">p_bytes = <span class=\"built_in\">bytes</span>(ptxt, <span class=\"string\">'utf-8'</span>)</span><br><span class=\"line\">p_len = <span class=\"number\">16</span> - <span class=\"built_in\">len</span>(p_bytes) % <span class=\"number\">16</span></span><br><span class=\"line\">iv = urandom(<span class=\"number\">16</span>)</span><br><span class=\"line\">key = urandom(<span class=\"number\">16</span>)</span><br><span class=\"line\">ctxt = aes_128_cbc_encrypt(iv, key, p_bytes)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"Plaintext: "</span>, p_bytes);</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"Ciphertext:"</span>, hexlify(ctxt));</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\"># Split and prepend IV for normalized processing</span></span><br><span class=\"line\">c_blocks = split_bytes_blocks(ctxt)</span><br><span class=\"line\">c_blocks = [<span class=\"built_in\">bytes</span>(iv)] + c_blocks</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\"># Find the exact padding length in the last block</span></span><br><span class=\"line\">padding_len = crack_padding_length(c_blocks[-<span class=\"number\">2</span>], c_blocks[-<span class=\"number\">1</span>], iv, key)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"\\nPlaintext length: "</span>, <span class=\"built_in\">len</span>(p_bytes));</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"Cracked padding length:"</span>, padding_len)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> padding_len == p_len</span><br><span class=\"line\"></span><br><span class=\"line\">cracked_ptxt = <span class=\"string\">b''</span></span><br><span class=\"line\"><span class=\"keyword\">for</span> m <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"built_in\">len</span>(c_blocks) - <span class=\"number\">1</span>):</span><br><span class=\"line\"> <span class=\"keyword\">if</span> m != <span class=\"number\">0</span>:</span><br><span class=\"line\"> padding_len = <span class=\"number\">0</span></span><br><span class=\"line\"> cracked_block = crack_cipher_block(c_blocks[-m - <span class=\"number\">2</span>], c_blocks[-m - <span class=\"number\">1</span>],</span><br><span class=\"line\"> iv, key, padding_len)</span><br><span class=\"line\"> cracked_ptxt = cracked_block + cracked_ptxt</span><br><span class=\"line\"></span><br><span class=\"line\">cracked_bytes = pkcs7_stripping(cracked_ptxt)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"Cracked plaintext:\\n"</span>, cracked_bytes)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> p_bytes == cracked_bytes</span><br></pre></td></tr></table></figure>\n<p>程序运行两次的输出示例如下</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">>>> </span><br><span class=\"line\">==================== RESTART: aes-cbc-poa.py ====================</span><br><span class=\"line\">Plaintext: b<span class=\"string\">"h'd;g*wL<-9_;ghpcZ2~!{B=L[G^8r\\t?"</span></span><br><span class=\"line\">Ciphertext: b<span class=\"string\">'d563a4db02142bc19d52dfcaf185f0a84fe1e0ee371b83da3cad84d47a03fdda56d7c936c90a9bbd369f4ec7723f115b'</span></span><br><span class=\"line\"></span><br><span class=\"line\">Plaintext length: 32</span><br><span class=\"line\">Cracked padding length: 16</span><br><span class=\"line\">Cracked plaintext:</span><br><span class=\"line\"> b<span class=\"string\">"h'd;g*wL<-9_;ghpcZ2~!{B=L[G^8r\\t?"</span></span><br><span class=\"line\">>>> </span><br><span class=\"line\">==================== RESTART: aes-cbc-poa.py ====================</span><br><span class=\"line\">Plaintext: b<span class=\"string\">'9N\\rN%!r?t"s[i6o# \\tydHG$\\'</span>\\np?z~>0UU<E\\<span class=\"string\">'9nnK\\'</span>d/M<WE5wi~hBX/&`6sKy(|\\n.\\t@<span class=\"string\">'</span></span><br><span class=\"line\"><span class=\"string\">Ciphertext: b'</span>bfb5e397839a0b6650201e66eeba82cadff91b7af0276f8e0129b74f6189b2f9c0a9ec8da83b5b7847a0ef6cd9d9d2d0fba7efbc28a6c73b59ea5aafa458d0c619d17b8e1cf1eff0851e510d58cc256a<span class=\"string\">'</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\">Plaintext length: 67</span></span><br><span class=\"line\"><span class=\"string\">Cracked padding length: 13</span></span><br><span class=\"line\"><span class=\"string\">Cracked plaintext:</span></span><br><span class=\"line\"><span class=\"string\"> b'</span>9N\\rN%!r?t<span class=\"string\">"s[i6o# \\tydHG$\\'\\np?z~>0UU<E\\'9nnK\\'d/M<WE5wi~hBX/&`6sKy(|\\n.\\t@'</span></span><br></pre></td></tr></table></figure>\n<p>完整的程序可点击这里下载:<a href=\"aes-cbc-poa.py.gz\">aes-cbc-poa.py.gz</a></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>D. Bleichenbacher. <em>Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption Standard PKCS#1</em>. In Advances in Cryptology CRYPTO’98, Santa Barbara, California, U.S.A., Lectures Notes in Computer Science 1462, pp. 1–12, Springer-Verlag, 1998.<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>J. Manger. <em>A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption Padding (OAEP) as Standardized in PKCS#1 v2.0</em>. In Advances in Cryptology CRYPTO’01, Santa Barbara, California, U.S.A., Lectures Notes in Computer Sci- ence 2139, pp. 230–238, Springer-Verlag, 2001.<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>S. Vaudenay, <em>Security Flaws Induced by CBC Padding - Applications to SSL, IPSEC, WTLS...</em>. In Advances in Cryptology- EUROCRYPT 2002. Springer, 2002, pp. 534–545.<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>J. Rizzo; T. Duong, <em>Practical Padding Oracle Attacks</em>. USENIX WOOT 2010.<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["学习体会"],"tags":["密码学","网络安全","Python编程"]},{"title":"解析IPv4和IPv6报文首部校验和算法","url":"/2020/11/24/Analyze-IPv4-IPv6-checksum/","content":"<p>关于IP报文首部校验和(checksum)算法,简单的说就是16位累加的反码运算,但具体是如何实现的,许多资料不得其详。TCP和UDP数据报首部也使用相同的校验算法,但参与运算的数据与IP报文首部不一样。此外,IPv6对校验和的运算与IPv4又有些许不同。因此有必要对IP分组的校验和算法作全面的解析。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Nothing in life is to be feared, it is only to be understood.</strong><br> <strong>— <em>Marie Curie</em>(居里夫人,波兰裔法国籍物理学家、化学家,两届诺贝尔奖得主)</strong></p>\n</div>\n<h2 id=\"ipv4首部校验和\">IPv4首部校验和</h2>\n<p>IPv4报文首部的结构如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">0 1 2 3 </span><br><span class=\"line\">0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 </span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">|Version| IHL |Type of Service| Total Length |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Identification |Flags| Fragment Offset |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Time to Live | Protocol | Header Checksum |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Source Address |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Destination Address |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Options | Padding |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure>\n<p>其中的<code>Header Checksum</code>域即为首部校验和部分。当要计算IPv4报文首部校验和时,发送方先将其置为全0,然后按16位逐一累加至IPv4报文首部结束,累加和保存于一个32位的数值中。如果总的字节数为奇数,则最后一个字节单独相加。累加完毕将结果中高16位再加到低16位上,重复这一过程直到高16位为全0。最后将结果取反存入首部校验和域。</p>\n<p>下面用实际截获的IPv4分组来演示整个计算过程:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">0x0000: 00 60 47 41 11 c9 00 09 6b 7a 5b 3b 08 00 45 00 </span><br><span class=\"line\">0x0010: 00 1c 74 68 00 00 80 11 59 8f c0 a8 64 01 ab 46 </span><br><span class=\"line\">0x0020: 9c e9 0f 3a 04 05 00 08 7f c5 00 00 00 00 00 00 </span><br><span class=\"line\">0x0030: 00 00 00 00 00 00 00 00 00 00 00 00</span><br></pre></td></tr></table></figure>\n<p>在上面的16进制转储中,起始为以太网 (Ethernet) 帧的开头。IPv4报文首部从地址偏移量0x000e开始,第一个字节为0x45,最后一个字节为0xe9。根据以上的算法描述,我们可以作如下计算:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">(1) 0x4500 + 0x001c + 0x7468 + 0x0000 + 0x8011 +</span><br><span class=\"line\"> 0x0000 + 0xc0a8 + 0x6401 + 0xab46 + 0x9ce9 = 0x3a66d</span><br><span class=\"line\">(2) 0xa66d + 0x3 = 0xa670</span><br><span class=\"line\">(3) 0xffff - 0xa670 = 0x598f</span><br></pre></td></tr></table></figure>\n<p>注意在第一步我们用<u>0x0000</u>设置首部校验和部分。可以看出这一报文首部的校验和与收到的值完全一致。以上的过程仅用于发送方计算初始的校验和,实际中对于中间转发的路由器和最终接收方,可将收到的IPv4报文首部校验和部分直接按同样算法相加,如果结果为<u>0xffff</u>,则校验正确。</p>\n<h2 id=\"c语言实现\">C语言实现</h2>\n<p>如何编程计算 IPv4 首部校验和?<a href=\"https://tools.ietf.org/html/rfc1071\">RFC 1071</a> (Computing the Internet Checksum) 给出了一个C语言的参考实现,如下所示:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\">{</span><br><span class=\"line\"> <span class=\"comment\">/* Compute Internet Checksum for "count" bytes</span></span><br><span class=\"line\"><span class=\"comment\"> * beginning at location "addr".</span></span><br><span class=\"line\"><span class=\"comment\"> */</span></span><br><span class=\"line\"> <span class=\"keyword\">register</span> <span class=\"keyword\">long</span> sum = <span class=\"number\">0</span>;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span>( count > <span class=\"number\">1</span> ) {</span><br><span class=\"line\"> <span class=\"comment\">/* This is the inner loop */</span></span><br><span class=\"line\"> sum += * (<span class=\"keyword\">unsigned</span> <span class=\"keyword\">short</span> *) addr++;</span><br><span class=\"line\"> count -= <span class=\"number\">2</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">/* Add left-over byte, if any */</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> ( count > <span class=\"number\">0</span> )</span><br><span class=\"line\"> sum += * (<span class=\"keyword\">unsigned</span> <span class=\"keyword\">char</span> *) addr;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">/* Fold 32-bit sum to 16 bits */</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (sum>><span class=\"number\">16</span>)</span><br><span class=\"line\"> sum = (sum & <span class=\"number\">0xffff</span>) + (sum >> <span class=\"number\">16</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> checksum = ~sum;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>在实际的网络连接中,源点设备可以调用以上代码产生初始IPv4报文首部校验和。而后在每一步的路由跳转中,因为路由器必须递减首部存活时间 (Time To Live,简写TTL) 字段,所以要更新首部校验和。<a href=\"https://tools.ietf.org/html/rfc1141\">RFC 1141</a> (Incremental Updating of the Internet Checksum) 给出了快速更新校验和的参考实现:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">unsigned</span> <span class=\"keyword\">long</span> sum;</span><br><span class=\"line\">ipptr->ttl--; <span class=\"comment\">/* decrement ttl */</span></span><br><span class=\"line\">sum = ipptr->Checksum + <span class=\"number\">0x100</span>; <span class=\"comment\">/* increment checksum high byte*/</span></span><br><span class=\"line\">ipptr->Checksum = (sum + (sum>><span class=\"number\">16</span>)); <span class=\"comment\">/* add carry */</span></span><br></pre></td></tr></table></figure>\n<h2 id=\"tcpudp首部校验和\">TCP/UDP首部校验和</h2>\n<p>对于TCP和UDP的数据报,其首部也包含16位的校验和,由目的地接收端验证。校验算法与IPv4报文首部完全一致,但参与校验的数据不同。这时校验和不仅包含整个TCP/UDP数据报,还覆盖了一个伪首部。IPv4伪首部的定义如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\"> 0 7 8 15 16 23 24 31 </span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| source address |</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| destination address |</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| zero |protocol| TCP/UDP length |</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br></pre></td></tr></table></figure>\n<p>其中有IP源地址,IP目的地址,协议号(TCP:6/UDP:17)及TCP或UDP数据报的总长度(首部+数据)。将伪首部加入校验的目的,是为了再次核对数据报是否到达正确的目的地,并防止IP欺骗攻击 (spoofing)。另外对于IPv4,UDP首部校验和是可选的,不用时该字段应被填充为全0。</p>\n<h2 id=\"ipv6的不同\">IPv6的不同</h2>\n<p>IPv6是网际协议第6版,其设计的主要目的是为了解决IPv4地址枯竭问题,当然它在其他方面也有许多改进。虽然IPv6的使用量增长缓慢,但是其趋势不可阻挡。IPv6的最新互联网标准由<a href=\"https://tools.ietf.org/html/rfc8200\">RFC 8200</a> (Internet Protocol, Version 6 (IPv6) Specification)规范。IPv6报文首部的结构如下所示:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">|Version| Traffic Class | Flow Label |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Payload Length | Next Header | Hop Limit |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ Source Address +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ Destination Address +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure>\n<p>注意到IPv6首部并没有包含校验和字段,这也是与IPv4的一个显著不同点。IPv6协议的设计延展了互联网设计端到端原则,取消首部校验和字段简化了路由器的处理过程,加快了IPv6报文网络传输。对报文数据完整度的保护可由链路层或端点间高层协议(TCP/UDP)的差错检测功能完成。这也是为什么IPv6强制要求UDP层设定首部校验和字段的原因。</p>\n<p>对于IPv6数据报TCP/UDP首部校验和的计算,其IPv6伪首部的定义如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ Source Address +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ Destination Address +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Upper-Layer Packet Length |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| zero | Next Header |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure>\n<h2 id=\"udp-lite应用\">UDP-Lite应用</h2>\n<p>在实际的IPv6网络应用中,为了兼顾差错检测和传输效率,可以采用UDP-Lite(Lightweight UDP,轻量用户数据报协议)。UDP-Lite有自己的IP协议号136,其规范定义于 <a href=\"https://tools.ietf.org/html/rfc8200\">RFC 3828</a> (The Lightweight User Datagram Protocol (UDP-Lite))。参考以下的UDP-Lite首部格式,它使用与UDP相同的<a href=\"https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml\">端口分配</a>,但是它将原来UDP首部的“长度”字段重定义为“校验和覆盖”(Checksum Coverage)域,这样就可以允许应用层自行控制需要计算校验和的数据长度,从而容许没被覆盖的数据部分可能有损地传输。</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\"> 0 15 16 31</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| Source | Destination |</span><br><span class=\"line\">| Port | Port |</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| Checksum | |</span><br><span class=\"line\">| Coverage | Checksum |</span><br><span class=\"line\">+--------+--------+--------+--------+</span><br><span class=\"line\">| |</span><br><span class=\"line\">: Payload :</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-----------------------------------+</span><br></pre></td></tr></table></figure>\n<p>UDP-Lite协议规定了“校验和覆盖”域的取值(以8位组为单位):</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">校验和覆盖值</th>\n<th style=\"text-align: center;\">校验和覆盖区域</th>\n<th style=\"text-align: center;\">说明</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">整个UDP-Lites数据报</td>\n<td style=\"text-align: center;\">计算要包括IP伪首部</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1-7</td>\n<td style=\"text-align: center;\">(无效值)</td>\n<td style=\"text-align: center;\">接收方必须抛弃数据报</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">UDP-Lites首部</td>\n<td style=\"text-align: center;\">计算要包括IP伪首部</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">> 8</td>\n<td style=\"text-align: center;\">UDP-Lites 首部 + 部分负载数据 (payload)</td>\n<td style=\"text-align: center;\">计算要包括IP伪首部</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">> IP 数据报长度</td>\n<td style=\"text-align: center;\">(无效值)</td>\n<td style=\"text-align: center;\">接收方必须抛弃数据报</td>\n</tr>\n</tbody>\n</table>\n<p>对于多媒体应用,采用VoIP或流视频数据传输协议,接收有一定程度损坏的数据比没接收到任何数据要好。另一个实例,是思科(Cisco)的无线局域网控制器和无线接入点之间的连接所基于的 <a href=\"https://tools.ietf.org/html/rfc5415\">CAPWAP</a> 协议规范,它就规定了当连接建立于IPv6网络之上时,其数据通道缺省使用校验和覆盖值为8的UDP-Lite协议。</p>\n<p>最后,分享一小段C程序,示例如何初始化Berkeley套接字 (socket) 以建立 IPv6 UDP-Lite 连接:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><sys/socket.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><netinet/in.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><net/udplite.h></span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">int</span> udplite_conn = socket(AF_INET6, SOCK_DGRAM, IPPROTO_UDPLITE);</span><br><span class=\"line\"><span class=\"keyword\">int</span> val = <span class=\"number\">8</span>; <span class=\"comment\">/* 校验和只覆盖8字节的UDP-Lite首部 */</span></span><br><span class=\"line\">(<span class=\"keyword\">void</span>)setsockopt(udplite_conn, IPPROTO_UDPLITE, UDPLITE_SEND_CSCOV, &val, <span class=\"keyword\">sizeof</span> val);</span><br><span class=\"line\">(<span class=\"keyword\">void</span>)setsockopt(udplite_conn, IPPROTO_UDPLITE, UDPLITE_RECV_CSCOV, &val, <span class=\"keyword\">sizeof</span> val);</span><br></pre></td></tr></table></figure>\n<p>这里 <code>IPPROTO_UDPLITE</code> 为协议号136,用它和<code>AF_INET6</code>地址集参数一起调用<code>socket()</code>函数来创建 IPv6 套接字。<code>UDPLITE_SEND_CSCOV</code>(10) 和 <code>UDPLITE_RECV_CSCOV</code>(11) 为套接字选项设置函数<code>setsockopt()</code>的控制参数,分别用来指定发送和接受时的校验和覆盖值。注意收发双方必须设置同样的数值,否则接受方无法正确验证校验和。</p>\n","categories":["学习体会"],"tags":["TCP/IP","C/C++编程"]},{"title":"AddressSanitizer — 程序员检测内存访问错误的利器","url":"/2021/08/03/ASAN-intro/","content":"<p>内存访问错误是最常见的软件错误,常常造成程序崩溃。程序员们一直在找寻优秀的内存访问错误检测工具,以便及时定位和排除错误以提高软件的可靠性。2012年由谷歌工程师开发的一款AddressSanitizer工具,以其覆盖面广、高效率和低开销的特性,已成为C/C++程序员们的首选。这里对其原理和使用方法做一个简要的介绍。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>One man's \"magic\" is another man's engineering. \"Supernatural\" is a null word.</strong><br> <strong>— <em>Robert Anson Heinlein</em>(罗伯特·安森·海因莱因,美国硬科幻小说作家,人称“科幻先生”)</strong></p>\n</div>\n<h3 id=\"工具概述\">工具概述</h3>\n<p>C/C++语言允许程序员对存储器进行低端控制,这种直接内存管理使编写高效应用软件成为可能。然而,由此也让内存访问错误,包括缓冲区溢出、访问释放后的内存和内存泄漏等,成为程序设计和实现中必须面对的严重问题。虽然有一些工具软件提供了检测这类错误的能力,但是它们的运行效率和功能覆盖常常不太理想。</p>\n<p>2012年,谷歌工程师Konstantin Serebryany和团队成员一起发布了名为AddressSanitizer<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>的开源C/C++程序内存访问错误检测器。AddressSanitizer(简称ASan)应用新的内存分配、映射和代码插桩技术,能高效地检测几乎所有的内存访问错误。使用SPEC 2006基准分析软件包测量,ASan运行过程中的减速比均值不超过2、内存消耗约为2.4倍。相比之下,另一个知名的检测工具<a href=\"http://valgrind.org/\">Valgrind</a>的减速比均值约为20,几乎无法投入实用。</p>\n<p>下表总结了ASan能检测的C/C++程序内存访问错误类型:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">错误类型</th>\n<th style=\"text-align: center;\">英文</th>\n<th style=\"text-align: center;\">简称</th>\n<th style=\"text-align: center;\">说明</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">堆内存释放后使用</td>\n<td style=\"text-align: center;\">heap use after free</td>\n<td style=\"text-align: center;\">UAF</td>\n<td style=\"text-align: center;\">内存释放后继续访问(悬空指针)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">堆内存缓冲区溢出</td>\n<td style=\"text-align: center;\">heap buffer overflow</td>\n<td style=\"text-align: center;\">Heap OOB</td>\n<td style=\"text-align: center;\">动态分配内存越界读写</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">堆内存泄漏</td>\n<td style=\"text-align: center;\">heap memory leak</td>\n<td style=\"text-align: center;\">HML</td>\n<td style=\"text-align: center;\">内存使用完毕未被释放</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">全局缓冲区溢出</td>\n<td style=\"text-align: center;\">global buffer overflow</td>\n<td style=\"text-align: center;\">Global OOB</td>\n<td style=\"text-align: center;\">全局对象越界读写</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">堆栈作用域后使用</td>\n<td style=\"text-align: center;\">stack use after scope</td>\n<td style=\"text-align: center;\">UAS</td>\n<td style=\"text-align: center;\">局部对象在作用域外访问</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">堆栈返回后使用</td>\n<td style=\"text-align: center;\">stack use after return</td>\n<td style=\"text-align: center;\">UAR</td>\n<td style=\"text-align: center;\">局部对象在返回后访问</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">堆栈缓冲区溢出</td>\n<td style=\"text-align: center;\">stack buffer overflow</td>\n<td style=\"text-align: center;\">Stack OOB</td>\n<td style=\"text-align: center;\">局部对象越界读写</td>\n</tr>\n</tbody>\n</table>\n<div class=\"note info\"><p>其实ASan本身并不包括检测堆内存泄漏功能,只是当集成ASan到编译器时,基于其对内存分配函数的修改,编译工具原来的泄漏检测功能与ASan互相融合到一起了。所以,编译时加入ASan选项也默认打开了泄漏检测功能。</p>\n</div>\n<p>这涵盖了除“读未初始化内存”(uninitialized memory reads,简称UMR)之外所有的常见内存访问错误。ASan检测这些错误的误报率(false positive)为0,这是相当出众的成绩。此外,ASan还能检测一些C++特有的内存访问错误:</p>\n<ul>\n<li><em>初始化次序错误</em>(<a href=\"https://isocpp.org/wiki/faq/ctors#static-init-order\">Initialization Order Fiasco</a>):当两个静态对象定义在不同的源文件,且一个对象的构造函数调用另一个对象的方法时,如果前者的编译单元先初始化,就会产生程序崩溃。</li>\n<li><em>容器访问溢出</em>(Container Overflow):给定libc++/libstdc++容器container,访问[container.end(), container.begin() + container.capacity())],即超出[container.begin(), container.end()]区域但仍在动态分配的内存区内。</li>\n<li><em>删除不匹配</em>(Delete Mismatch):使用<code>new foo[n]</code>创建的数组对象,不应该调用<code>delete foo</code>删除,必须调用<code>delete [] foo</code>。</li>\n</ul>\n<p>ASan的高可靠性和高性能,使它一经问世就得到编译器和集成开发环境开发者的首肯。现今ASan已经集成到全部四大编译工具集中:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">编译器/IDE</th>\n<th style=\"text-align: center;\">起始支持版本</th>\n<th style=\"text-align: center;\">操作系统</th>\n<th style=\"text-align: center;\">适用平台</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">Clang/LLVM<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a></td>\n<td style=\"text-align: center;\">3.1</td>\n<td style=\"text-align: center;\">Unix-like</td>\n<td style=\"text-align: center;\">跨平台</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">GCC</td>\n<td style=\"text-align: center;\">4.8</td>\n<td style=\"text-align: center;\">Unix-like</td>\n<td style=\"text-align: center;\">跨平台</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">Xcode</td>\n<td style=\"text-align: center;\">7.0</td>\n<td style=\"text-align: center;\">Mac OS X</td>\n<td style=\"text-align: center;\">苹果公司产品</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">MSVC</td>\n<td style=\"text-align: center;\">16.9</td>\n<td style=\"text-align: center;\">Windows</td>\n<td style=\"text-align: center;\">IA-32、x86-64和ARM</td>\n</tr>\n</tbody>\n</table>\n<p>ASan的研发者最早使用Chromium开源浏览器做常规测试,在10个月的时间里发现了300多个内存访问错误。在集成到主流编译工具之后,它报告了众多流行的开源软件中隐藏已久的错误,如Mozilla Firefox、Perl、Vim、PHP和MySQL等。有趣的是,ASan还找出了LLVM和GCC编译器本身代码中一些内存访问错误。现在,许多软件公司已经将运行ASan加入到必备的质量控制流程中。</p>\n<h3 id=\"工作原理\">工作原理</h3>\n<p>2012年Serebryany发表的USENIX会议论文<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>,全面阐述了ASan的设计原理、算法思想和编程实现。在整体结构上,ASan由两部分构成:</p>\n<ol type=\"1\">\n<li>编译器插桩(compiler instrumentation)模块 — 修改代码用以核对每次内存访问时的影子内存(shadow memory)状态,并在全局和堆栈对象边缘创建毒化的红区(poisoned redzones)以检测向上或向下溢出的情况。</li>\n<li>运行时库(run-time library)替换模块 — 替换内存分配/释放(<code>malloc/free</code>)及其相关函数,用以在动态分配的堆内存区域边缘创建毒化的红区、延迟释放后内存区域的重用并生成出错报告。</li>\n</ol>\n<p>这里影子内存、编译器插桩和内存分配函数替换都是之前已经存在的技术,那么ASan是如何创新地应用它们以实现高效的错误检测的呢?让我们来看看细节。</p>\n<h4 id=\"影子内存\">影子内存</h4>\n<p>许多检测工具使用分离的影子内存记录程序内存的元数据,然后应用插桩在内存访问时检查影子内存,以确认读写是否安全。不同的是,ASan使用了更有效的<strong>直接映射影子内存</strong>。</p>\n<p>ASan的设计者们注意到,典型情况下<code>malloc</code>函数返回的内存地址至少是8字节对齐的。比如申请20个字节的内存,会划分24字节内存,实际返回指针的最后3比特全为0。此外,任何一个对齐的8字节序列只会有9种不同状态:前 <span class=\"math inline\">\\(k\\,(0\\leq k \\leq 8)\\)</span> 字节可访问,后 <span class=\"math inline\">\\(8-k\\)</span> 不可。由此他们想到了一个更紧凑的影子内存映射和使用方案:</p>\n<ul>\n<li>预留八分之一的虚拟地址空间给影子内存</li>\n<li>使用除以8再加上偏移量的公式直接映射应用程序内存到影子内存\n<ul>\n<li>32位应用程序:<code>Shadow = (Mem >> 3) + 0x20000000;</code></li>\n<li>64位应用程序:<code>Shadow = (Mem >> 3) + 0x7fff8000;</code></li>\n</ul></li>\n<li>影子内存的每个字节记录对应8字节内存块的9种状态之一\n<ul>\n<li>全部8字节可访问,值为0</li>\n<li>全部8字节不可访问(已毒化),值为负</li>\n<li>仅首 <span class=\"math inline\">\\(k\\,(1\\leq k \\leq 7)\\)</span> 字节可访问,值为 <span class=\"math inline\">\\(k\\)</span></li>\n</ul></li>\n</ul>\n<p>下图显示了ASan的地址空间布局和映射关系。留意中间的Bad区,这是影子内存自身映射后的地址段。因为影子内存对应用程序是不可见的,ASan使用页保护机制将之设定为不可访问。</p>\n<p><img src=\"asan-mm.png\" style=\"width:40.0%;height:40.0%\" /></p>\n<h4 id=\"编译器插桩\">编译器插桩</h4>\n<p>确定了影子内存的设计,检测动态内存访问错误的编译器插桩实现就很容易了。对于8字节的内存访问,在原读写代码前插入指令检查影子内存字节,如不为0则报错。对于不足8字节的内存访问,插桩较复杂一点,这时要比较影子内存的字节数值与读写地址的最后三个比特。这种情况也被称为“慢通道”(slow path),示例代码如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// Check the cases where we access first k bytes of the qword</span></span><br><span class=\"line\"><span class=\"comment\">// and these k bytes are unpoisoned.</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">bool</span> <span class=\"title\">SlowPathCheck</span><span class=\"params\">(shadow_value, address, kAccessSize)</span> </span>{</span><br><span class=\"line\"> last_accessed_byte = (address & <span class=\"number\">7</span>) + kAccessSize - <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (last_accessed_byte >= shadow_value);</span><br><span class=\"line\">}</span><br><span class=\"line\">...</span><br><span class=\"line\"></span><br><span class=\"line\">byte *shadow_address = MemToShadow(address);</span><br><span class=\"line\">byte shadow_value = *shadow_address;</span><br><span class=\"line\"><span class=\"keyword\">if</span> (shadow_value) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (SlowPathCheck(shadow_value, address, kAccessSize)) {</span><br><span class=\"line\"> ReportError(address, kAccessSize, kIsWrite);</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\">*address = ...; <span class=\"comment\">// or: ... = *address;</span></span><br></pre></td></tr></table></figure>\n<p>对于全局和堆栈(局部)对象,ASan设计了不同的插桩检测它们的越界访问错误。全局对象周边的红区由编译器在编译时加入,其地址在应用程序启动时被传递到运行时库,运行库函数再毒化红区并记下地址以便生成错误报告。堆栈对象是在函数调用时创建的,相应地其红区的建立和毒化也在运行时完成。另外,因为堆栈对象在函数返回时被删除,插桩代码还必须将其映射到的影子内存清零。</p>\n<p>在实际实现中,ASan编译器插桩的过程被置于编译器优化流水线的末端,这样插桩就只适用于变量和循环优化之后剩余的内存访问指令。在最新的GCC发行版中,ASan编译器插桩代码位于gcc子目录下的两个文件中<code>gcc/asan.[ch]</code>。</p>\n<h4 id=\"运行库替换\">运行库替换</h4>\n<p>运行库需要加入管理影子内存的代码。在应用程序启动时,要初始化影子内存自身映射到的地址段,以禁止程序的其他部分访问影子内存。运行库替换了旧的内存分配和释放函数,还加入了一些错误报告函数如<code>__asan_report_load8</code>等。</p>\n<p>替换后新的内存分配函数<code>malloc</code>将在请求的内存块前后分配额外的存储区作为红区,并设置红区为不可寻址。这就是所谓的毒化过程。实际中,因为内存分配器要维护对应不同对象大小的可用内存列表,如果某个对象列表为空时,操作系统会一次性分配一大组内存块及其红区。由此前后内存块的红区会相连,如下图所示,<span class=\"math inline\">\\(n\\)</span> 个内存块只需要分配 <span class=\"math inline\">\\(n+1\\)</span> 个红区:</p>\n<p><img src=\"asan-redzone.png\" /></p>\n<p>新的<code>free</code>函数在内存释放后需要毒化整个存储区并将其置于一个隔离(quarantine)队列。这样可以避免存储区被立即分配。否则,如果存储区马上就被重用,就无法检测出对上次释放后内存的错误访问。隔离队列的大小决定了存储区处于隔离状态的时间,越大则检测UAF错误的能力越强!</p>\n<p>默认情况下,为了在错误报告中提供更详尽的信息,<code>malloc</code>和<code>free</code>函数都会记录其调用栈。<code>malloc</code>的调用栈保存在所分配内存左边的红区中,故而大的红区可以保留更多的调用栈帧结构。<code>free</code>的调用栈则保存在所分配内存区的起始处。</p>\n<p>集成到GCC编译器中,ASan运行库替换的源代码位于libsanitizer子目录下<code>libsanitizer/asan/*</code>,编译后产生的运行库名为<code>libasan.so</code>。</p>\n<h3 id=\"应用示例\">应用示例</h3>\n<p>ASan的使用非常方便,下面以运行于x86_64虚拟机上的Ubuntu Linux 20.4 + GCC 9.3.0系统为例,演示检测各种内存访问错误的能力。</p>\n<h4 id=\"测试用例\">测试用例</h4>\n<p>如下所示,测试程序编写了7个函数,各自引入不同的错误类型。函数名与错误类型一一对照:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/*</span></span><br><span class=\"line\"><span class=\"comment\"> * PakcteMania https://packetmania.github.io</span></span><br><span class=\"line\"><span class=\"comment\"> *</span></span><br><span class=\"line\"><span class=\"comment\"> * gcc asan-test.c -o asan-test -fsanitize=address -g</span></span><br><span class=\"line\"><span class=\"comment\"> */</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdio.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdlib.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><unistd.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><strings.h></span></span></span><br><span class=\"line\"><span class=\"comment\">/* #include <sanitizer/lsan_interface.h> */</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">int</span> ga[<span class=\"number\">10</span>] = {<span class=\"number\">1</span>};</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">global_buffer_overflow</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">return</span> ga[<span class=\"number\">10</span>];</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">heap_leak</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span>* k = (<span class=\"keyword\">int</span> *)<span class=\"built_in\">malloc</span>(<span class=\"number\">10</span>*<span class=\"keyword\">sizeof</span>(<span class=\"keyword\">int</span>));</span><br><span class=\"line\"> <span class=\"keyword\">return</span>;</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">heap_use_after_free</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span>* u = (<span class=\"keyword\">int</span> *)<span class=\"built_in\">malloc</span>(<span class=\"number\">10</span>*<span class=\"keyword\">sizeof</span>(<span class=\"keyword\">int</span>));</span><br><span class=\"line\"> u[<span class=\"number\">9</span>] = <span class=\"number\">10</span>;</span><br><span class=\"line\"> <span class=\"built_in\">free</span>(u);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> u[<span class=\"number\">9</span>];</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">heap_buffer_overflow</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span>* h = (<span class=\"keyword\">int</span> *)<span class=\"built_in\">malloc</span>(<span class=\"number\">10</span>*<span class=\"keyword\">sizeof</span>(<span class=\"keyword\">int</span>));</span><br><span class=\"line\"> h[<span class=\"number\">0</span>] = <span class=\"number\">10</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> h[<span class=\"number\">10</span>];</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">stack_buffer_overflow</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> s[<span class=\"number\">10</span>];</span><br><span class=\"line\"> s[<span class=\"number\">0</span>] = <span class=\"number\">10</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> s[<span class=\"number\">10</span>];</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">int</span> *gp;</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">stack_use_after_return</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> r[<span class=\"number\">10</span>];</span><br><span class=\"line\"> r[<span class=\"number\">0</span>] = <span class=\"number\">10</span>;</span><br><span class=\"line\"> gp = &r[<span class=\"number\">0</span>];</span><br><span class=\"line\"> <span class=\"keyword\">return</span>;</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">stack_use_after_scope</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> {</span><br><span class=\"line\"> <span class=\"keyword\">int</span> c = <span class=\"number\">0</span>;</span><br><span class=\"line\"> gp = &c;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> *gp = <span class=\"number\">10</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>测试程序调用<code>getopt</code>库函数支持单字母命令行选项,可让用户选择要测试的错误类型。命令行选项使用信息如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">\b$ ./asan-test</span><br><span class=\"line\"></span><br><span class=\"line\">Test AddressSanitizer</span><br><span class=\"line\">usage: asan-test [ -bfloprs ]</span><br><span class=\"line\"></span><br><span class=\"line\">-b\theap buffer overflow</span><br><span class=\"line\">-f\theap use after free</span><br><span class=\"line\">-l\theap memory leak</span><br><span class=\"line\">-o\tglobal buffer overflow</span><br><span class=\"line\">-p\tstack use after scope</span><br><span class=\"line\">-r\tstack use after <span class=\"built_in\">return</span></span><br><span class=\"line\">-s\tstack buffer overflow</span><br></pre></td></tr></table></figure>\n<p>测试程序的GCC编译命令很简单,只要加上两个编译选项就够了</p>\n<ul>\n<li><code>-fsanitize=address</code>:激活ASan工具</li>\n<li><code>-g</code>:启动调试功能,保留调试信息</li>\n</ul>\n<h4 id=\"oob测试\">OOB测试</h4>\n<p>对于Heap OOB错误,运行结果是</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ./asan-test -b</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57360==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x604000000038 at pc 0x55bf46fd64ed bp 0x7ffced908dc0 sp 0x7ffced908db0</span><br><span class=\"line\">READ of size 4 at 0x604000000038 thread T0</span><br><span class=\"line\"> <span class=\"comment\">#0 0x55bf46fd64ec in heap_buffer_overflow /home/zixi/coding/asan-test.c:34</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x55bf46fd6a3f in main /home/zixi/coding/asan-test.c:88</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x7fd16f6560b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x55bf46fd628d in _start (/home/zixi/coding/asan-test+0x128d)</span></span><br><span class=\"line\"></span><br><span class=\"line\">0x604000000038 is located 0 bytes to the right of 40-byte region [0x604000000010,0x604000000038)</span><br><span class=\"line\">allocated by thread T0 here:</span><br><span class=\"line\"> <span class=\"comment\">#0 0x7fd16f92ebc8 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10dbc8)</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x55bf46fd646c in heap_buffer_overflow /home/zixi/coding/asan-test.c:32</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x55bf46fd6a3f in main /home/zixi/coding/asan-test.c:88</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x7fd16f6560b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"></span><br><span class=\"line\">SUMMARY: AddressSanitizer: heap-buffer-overflow /home/zixi/coding/asan-test.c:34 <span class=\"keyword\">in</span> heap_buffer_overflow</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0c087fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0c087fff8000: fa fa 00 00 00 00 00[fa]fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57360==ABORTING</span><br></pre></td></tr></table></figure>\n<p>参考<code>heap-buffer-overflow</code>函数实现,可以看到它申请了40个字节的内存,以容纳10个32位整型数。然而在函数返回时,代码越界读取所分配内存之后的数据。如上运行记录显示,程序检测到了Heap OOB错误并立即中止,ASan报告了出错的代码文件名和行号<code>asan-test.c:34</code>,也准确地列出了动态内存的原始分配函数调用栈。报告的总结(SUMMARY)部分,还打印出了相关地址对应的影子内存数据(观察<code>=></code>标注的行)。要读的地址是0x604000000038,其映射后的影子内存地址0x0c087fff8007保存的是负值0xfa(已毒化,不可访问)。正因如此,ASan报错并中止程序运行。</p>\n<p>Stack OOB测试用例如下所示。ASan报告了局部对象越界读错误。由于局部变量位于堆栈空间中,所以列出了函数<code>stack_buffr_overflow</code>的起始行号<code>asan-test.c:37</code>。与Heap OOB报告不同的是,局部变量的前后红区的影子内存毒化值是不一样的,之前<code>Stack left redzone</code>为0xf1,之后<code>Stack right redzone</code>为0xf3。使用不同的毒化值(都是0x80之后的负值),有利于快速区分不同的错误类型。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ./asan-test -s</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57370==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7f1cf5044058 at pc 0x55d8b7e9d601 bp 0x7ffc830c29e0 sp 0x7ffc830c29d0</span><br><span class=\"line\">READ of size 4 at 0x7f1cf5044058 thread T0</span><br><span class=\"line\"> <span class=\"comment\">#0 0x55d8b7e9d600 in stack_buffer_overflow /home/zixi/coding/asan-test.c:40</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x55d8b7e9daec in main /home/zixi/coding/asan-test.c:108</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x7f1cf87760b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x55d8b7e9d28d in _start (/home/zixi/coding/asan-test+0x128d)</span></span><br><span class=\"line\"></span><br><span class=\"line\">Address 0x7f1cf5044058 is located <span class=\"keyword\">in</span> stack of thread T0 at offset 88 <span class=\"keyword\">in</span> frame</span><br><span class=\"line\"> <span class=\"comment\">#0 0x55d8b7e9d505 in stack_buffer_overflow /home/zixi/coding/asan-test.c:37</span></span><br><span class=\"line\"></span><br><span class=\"line\"> This frame has 1 object(s):</span><br><span class=\"line\"> [48, 88) <span class=\"string\">'s'</span> (line 38) <== Memory access at offset 88 overflows this variable</span><br><span class=\"line\">HINT: this may be a <span class=\"literal\">false</span> positive <span class=\"keyword\">if</span> your program uses some custom stack unwind mechanism, swapcontext or vfork</span><br><span class=\"line\"> (longjmp and C++ exceptions *are* supported)</span><br><span class=\"line\">SUMMARY: AddressSanitizer: stack-buffer-overflow /home/zixi/coding/asan-test.c:40 <span class=\"keyword\">in</span> stack_buffer_overflow</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0fe41ea007b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea007c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea007d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea007e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea007f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0fe41ea00800: f1 f1 f1 f1 f1 f1 00 00 00 00 00[f3]f3 f3 f3 f3</span><br><span class=\"line\"> 0x0fe41ea00810: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea00820: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea00830: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea00840: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe41ea00850: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> Stack left redzone: f1</span><br><span class=\"line\"> Stack mid redzone: f2</span><br><span class=\"line\"> Stack right redzone: f3</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57370==ABORTING</span><br></pre></td></tr></table></figure>\n<p>以下Global OOB测试结果,也清晰地显示了出错行<code>asan-test.c:16</code>、全局变量名<code>ga</code>和其定义代码位置<code>asan-test.c:13:5</code>,还可以看到全局对象的红区毒化值为0xf9。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ./asan-test -o</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57367==ERROR: AddressSanitizer: global-buffer-overflow on address 0x564363ea4048 at pc 0x564363ea1383 bp 0x7ffc0d6085d0 sp 0x7ffc0d6085c0</span><br><span class=\"line\">READ of size 4 at 0x564363ea4048 thread T0</span><br><span class=\"line\"> <span class=\"comment\">#0 0x564363ea1382 in global_buffer_overflow /home/zixi/coding/asan-test.c:16</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x564363ea1a6c in main /home/zixi/coding/asan-test.c:98</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x7f8cb43890b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x564363ea128d in _start (/home/zixi/coding/asan-test+0x128d)</span></span><br><span class=\"line\"></span><br><span class=\"line\">0x564363ea4048 is located 0 bytes to the right of global variable <span class=\"string\">'ga'</span> defined <span class=\"keyword\">in</span> <span class=\"string\">'asan-test.c:13:5'</span> (0x564363ea4020) of size 40</span><br><span class=\"line\">SUMMARY: AddressSanitizer: global-buffer-overflow /home/zixi/coding/asan-test.c:16 <span class=\"keyword\">in</span> global_buffer_overflow</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0ac8ec7cc7b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc7c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc7d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc7e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc7f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0ac8ec7cc800: 00 00 00 00 00 00 00 00 00[f9]f9 f9 f9 f9 f9 f9</span><br><span class=\"line\"> 0x0ac8ec7cc810: 00 00 00 00 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9</span><br><span class=\"line\"> 0x0ac8ec7cc820: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9</span><br><span class=\"line\"> 0x0ac8ec7cc830: f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 f9 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc840: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0ac8ec7cc850: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> Stack left redzone: f1</span><br><span class=\"line\"> Stack mid redzone: f2</span><br><span class=\"line\"> Stack right redzone: f3</span><br><span class=\"line\"> Stack after <span class=\"built_in\">return</span>: f5</span><br><span class=\"line\"> Stack use after scope: f8</span><br><span class=\"line\"> Global redzone: f9</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57367==ABORTING</span><br></pre></td></tr></table></figure>\n<p>注意在这个例子中,全局数组<code>int ga[10] = {1};</code>是被初始化过的,如果是未初始化的会怎么样?将代码稍作改动</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">int</span> ga[<span class=\"number\">10</span>];</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">global_buffer_overflow</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> ga[<span class=\"number\">0</span>] = <span class=\"number\">10</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> ga[<span class=\"number\">10</span>];</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>令人意外的是,ASan没有报告出这里明显的Global OOB错误。为什么?</p>\n<p>原因与GCC对全局变量的处理方式有关。编译器将函数和已初始化的变量视为强符号(Strong symbols),而<strong>未初始化的变量默认是弱符号</strong>(Weak symbols)。因为弱符号在不同的源文件中的定义可能相异,所以其需要空间的大小未知。<u>编译器无法为弱符号在BSS段分配空间,就采用COMMON块的机制,让所有弱符号共享一个COMMON存储区,由此ASan无法插入红区</u>。在链接过程中,链接器读取所有输入目标文件以后,就可以确定弱符号的大小,在最终输出文件的BSS段为其分配空间。</p>\n<p>幸运的是,GCC的<code>-fno-common</code>选项可以关闭COMMON块机制,使编译器直接将所有未初始化的全局变量加入目标文件的BSS段,也能让ASan正常工作。这一选项也禁止链接器合并弱符号,所以当链接器发现目标文件有重复定义的全局变量编译单元时直接报错。</p>\n<p>实测证实了这一点,对上一段代码修改GCC命令行为</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">gcc asan-test.c -o asan-test -fsanitize=address -fno-common -g</span><br></pre></td></tr></table></figure>\n<p>编译链接后运行ASan,成功地报告了Global OOB错误。</p>\n<h4 id=\"uaf测试\">UAF测试</h4>\n<p>下面是UAF错误检测的运行记录。这里不仅报告了出错的代码信息,还给出了动态内存的原始分配函数和释放函数的调用栈。记录表明内存由<code>asan-test.c:25</code>分配,在<code>asan-test.c:27</code>处被释放,却又在<code>asan-test.c:28</code>被读出。后面打印的影子内存数据表明所填充的数据是负值0xfd,这也是内存释放后毒化的结果。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ \u0007./asan-test -\bf</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57363==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000000034 at pc 0x558b4a45444e bp 0x7ffccf4ca790 sp 0x7ffccf4ca780</span><br><span class=\"line\">READ of size 4 at 0x604000000034 thread T0</span><br><span class=\"line\"> <span class=\"comment\">#0 0x558b4a45444d in heap_use_after_free /home/zixi/coding/asan-test.c:28</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x558b4a454a4e in main /home/zixi/coding/asan-test.c:91</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x7fc7cc98b0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x558b4a45428d in _start (/home/zixi/coding/asan-test+0x128d)</span></span><br><span class=\"line\"></span><br><span class=\"line\">0x604000000034 is located 36 bytes inside of 40-byte region [0x604000000010,0x604000000038)</span><br><span class=\"line\">freed by thread T0 here:</span><br><span class=\"line\"> <span class=\"comment\">#0 0x7fc7ccc637cf in __interceptor_free (/lib/x86_64-linux-gnu/libasan.so.5+0x10d7cf)</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x558b4a454412 in heap_use_after_free /home/zixi/coding/asan-test.c:27</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x558b4a454a4e in main /home/zixi/coding/asan-test.c:91</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x7fc7cc98b0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"></span><br><span class=\"line\">previously allocated by thread T0 here:</span><br><span class=\"line\"> <span class=\"comment\">#0 0x7fc7ccc63bc8 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10dbc8)</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x558b4a4543bd in heap_use_after_free /home/zixi/coding/asan-test.c:25</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x558b4a454a4e in main /home/zixi/coding/asan-test.c:91</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x7fc7cc98b0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"></span><br><span class=\"line\">SUMMARY: AddressSanitizer: heap-use-after-free /home/zixi/coding/asan-test.c:28 <span class=\"keyword\">in</span> heap_use_after_free</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0c087fff7fb0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fc0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fd0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7fe0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0c087fff7ff0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0c087fff8000: fa fa fd fd fd fd[fd]fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8010: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8020: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8030: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8040: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\"> 0x0c087fff8050: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57363==ABORTING</span><br></pre></td></tr></table></figure>\n<h4 id=\"hml测试\">HML测试</h4>\n<p>内存泄漏的测试结果如下。与其他测试用例不同,输出记录末尾没有打印<code>ABORTING</code>。这是因为默认情况下,ASan只在程序终止(进程结束)时生成内存泄漏报告。如果想要在运行中检查是否有泄漏,可以调用ASan自己的库函数<code>__lsan_do_recoverable_leak_check</code>,其定义位于头文件<code>sanitizer/lsan_interface.h</code>中。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ./asan-test -l</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57365==ERROR: LeakSanitizer: detected memory leaks</span><br><span class=\"line\"></span><br><span class=\"line\">Direct leak of 40 byte(s) <span class=\"keyword\">in</span> 1 object(s) allocated from:</span><br><span class=\"line\"> <span class=\"comment\">#0 0x7f06b85b1bc8 in malloc (/lib/x86_64-linux-gnu/libasan.so.5+0x10dbc8)</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x5574a8bcd3a0 in heap_leak /home/zixi/coding/asan-test.c:20</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x5574a8bcda5d in main /home/zixi/coding/asan-test.c:94</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x7f06b82d90b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"></span><br><span class=\"line\">SUMMARY: AddressSanitizer: 40 byte(s) leaked <span class=\"keyword\">in</span> 1 allocation(s).</span><br></pre></td></tr></table></figure>\n<h4 id=\"uas测试\">UAS测试</h4>\n<p>参见<code>stack_use_after_scope</code>函数代码,局部变量<code>c</code>所在的存储区在其作用域外被写入。测试记录准确地报告了变量定义所在的行号<code>line 54</code>及错误写的代码位置<code>asan-test.c:57</code>:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">./asan-test -\bp</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57368==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7f06f0a9b020 at pc 0x56121a7548d9 bp 0x7ffd1de0d050 sp 0x7ffd1de0d040</span><br><span class=\"line\">WRITE of size 4 at 0x7f06f0a9b020 thread T0</span><br><span class=\"line\"> <span class=\"comment\">#0 0x56121a7548d8 in stack_use_after_scope /home/zixi/coding/asan-test.c:57</span></span><br><span class=\"line\"> <span class=\"comment\">#1 0x56121a754a7b in main /home/zixi/coding/asan-test.c:101</span></span><br><span class=\"line\"> <span class=\"comment\">#2 0x7f06f42cd0b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span></span><br><span class=\"line\"> <span class=\"comment\">#3 0x56121a75428d in _start (/home/zixi/coding/asan-test+0x128d)</span></span><br><span class=\"line\"></span><br><span class=\"line\">Address 0x7f06f0a9b020 is located <span class=\"keyword\">in</span> stack of thread T0 at offset 32 <span class=\"keyword\">in</span> frame</span><br><span class=\"line\"> <span class=\"comment\">#0 0x56121a7547d0 in stack_use_after_scope /home/zixi/coding/asan-test.c:52</span></span><br><span class=\"line\"></span><br><span class=\"line\"> This frame has 1 object(s):</span><br><span class=\"line\"> [32, 36) <span class=\"string\">'c'</span> (line 54) <== Memory access at offset 32 is inside this variable</span><br><span class=\"line\">HINT: this may be a <span class=\"literal\">false</span> positive <span class=\"keyword\">if</span> your program uses some custom stack unwind mechanism, swapcontext or vfork</span><br><span class=\"line\"> (longjmp and C++ exceptions *are* supported)</span><br><span class=\"line\">SUMMARY: AddressSanitizer: stack-use-after-scope /home/zixi/coding/asan-test.c:57 <span class=\"keyword\">in</span> stack_use_after_scope</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0fe15e14b5b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b5c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b5d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b5e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b5f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0fe15e14b600: f1 f1 f1 f1[f8]f3 f3 f3 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b610: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b620: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b630: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0fe15e14b650: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> Stack left redzone: f1</span><br><span class=\"line\"> Stack mid redzone: f2</span><br><span class=\"line\"> Stack right redzone: f3</span><br><span class=\"line\"> Stack after <span class=\"built_in\">return</span>: f5</span><br><span class=\"line\"> Stack use after scope: f8</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57368==ABORTING</span><br></pre></td></tr></table></figure>\n<h4 id=\"uar测试\">UAR测试</h4>\n<p>UAR测试有其特殊性。因为函数返回后其堆栈内存会被立即重用,所以要检测局部对象在返回后访问错误,必须设置动态内存分配的“伪堆栈”,具体细节可查询ASan的相关Wiki页<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>。由于这种算法变化对性能冲击不小,所以ASan默认不会检测UAR错误。如果真的需要,可以在运行前设定环境变量<code>ASAN_OPTIONS</code>为<code>detect_stack_use_after_return=1</code>。对应测试记录如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ export ASAN_OPTIONS=detect_stack_use_after_return=1</span><br><span class=\"line\">$ env | grep ASAN</span><br><span class=\"line\">ASAN_OPTIONS=detect_stack_use_after_return=1</span><br><span class=\"line\">$ ./asan-test -\br</span><br><span class=\"line\">=================================================================</span><br><span class=\"line\">==57369==ERROR: AddressSanitizer: stack-use-after-return on address 0x7f5493e93030 at pc 0x55a356890ac9 bp 0x7ffd22c5cf30 sp 0x7ffd22c5cf20</span><br><span class=\"line\">READ of size 4 at 0x7f5493e93030 thread T0</span><br><span class=\"line\"> #0 0x55a356890ac8 in main /home/zixi/coding/asan-test.c:105</span><br><span class=\"line\"> #1 0x7f54975c50b2 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x270b2)</span><br><span class=\"line\"> #2 0x55a35689028d in _start (/home/zixi/coding/asan-test+0x128d)</span><br><span class=\"line\"></span><br><span class=\"line\">Address 0x7f5493e93030 is located in stack of thread T0 at offset 48 in frame</span><br><span class=\"line\"> #0 0x55a356890682 in stack_use_after_return /home/zixi/coding/asan-test.c:45</span><br><span class=\"line\"></span><br><span class=\"line\"> This frame has 1 object(s):</span><br><span class=\"line\"> [48, 88) 'r' (line 46) <== Memory access at offset 48 is inside this variable</span><br><span class=\"line\">HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork</span><br><span class=\"line\"> (longjmp and C++ exceptions *are* supported)</span><br><span class=\"line\">SUMMARY: AddressSanitizer: stack-use-after-return /home/zixi/coding/asan-test.c:105 in main</span><br><span class=\"line\">Shadow bytes around the buggy address:</span><br><span class=\"line\"> 0x0feb127ca5b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca5c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca5d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca5e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca5f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">=>0x0feb127ca600: f5 f5 f5 f5 f5 f5[f5]f5 f5 f5 f5 f5 f5 f5 f5 f5</span><br><span class=\"line\"> 0x0feb127ca610: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca620: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca630: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\"> 0x0feb127ca650: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00</span><br><span class=\"line\">Shadow byte legend (one shadow byte represents 8 application bytes):</span><br><span class=\"line\"> Addressable: 00</span><br><span class=\"line\"> Partially addressable: 01 02 03 04 05 06 07 </span><br><span class=\"line\"> Heap left redzone: fa</span><br><span class=\"line\"> Freed heap region: fd</span><br><span class=\"line\"> Stack left redzone: f1</span><br><span class=\"line\"> Stack mid redzone: f2</span><br><span class=\"line\"> Stack right redzone: f3</span><br><span class=\"line\"> Stack after return: f5</span><br><span class=\"line\"> ...</span><br><span class=\"line\">==57369==ABORTING</span><br></pre></td></tr></table></figure>\n<p>ASan支持许多其他的编译器标示和运行时环境变量选项,以控制和调整检测的功能和范围,感兴趣者请参考ASan标示Wiki页<a href=\"#fn5\" class=\"footnote-ref\" id=\"fnref5\" role=\"doc-noteref\"><sup>5</sup></a>。</p>\n<p>完整的测试程序的压缩包在此下载:<a href=\"asan-test.c.gz\">asan-test.c.gz</a></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p><a href=\"https://github.com/google/sanitizers/wiki/AddressSanitizer\">AddressSanitizer Wiki</a><a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p><a href=\"https://clang.llvm.org/docs/AddressSanitizer.html\">Clang 13 documentation: ADDRESSSANITIZER</a><a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>Serebryany, K.; Bruening, D.; Potapenko, A.; Vyukov, D. \"<a href=\"https://www.usenix.org/system/files/conference/atc12/atc12-final39.pdf\"><em>AddressSanitizer: a fast address sanity checker</em></a>\". In USENIX ATC, 2012<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p><a href=\"https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn\">AddressSanitizerUseAfterReturn</a><a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn5\" role=\"doc-endnote\"><p><a href=\"https://github.com/google/sanitizers/wiki/AddressSanitizerFlags\">AddressSanitizerFlags</a><a href=\"#fnref5\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["工具使用"],"tags":["C/C++编程","系统编程"]},{"title":"C++编程实现的距离矢量路由协议仿真程序","url":"/2020/11/25/CC-simulate-DV-routing/","content":"<p>距离矢量 (Distance-vector) 和链路状态 (Link-state) 是路由协议的两大分类。距离矢量路由协议在互联网早期得到广泛应用,之后一些协议实现逐渐演变成为标准化的“<a href=\"https://tools.ietf.org/html/rfc2453\">路由信息协议</a>” (Routing Information Protocol,缩写 RIP)。由于其简单和实用性,RIP 现今仍旧是小型网络配置的首选。</p>\n<span id=\"more\"></span>\n<p>许多年以前,笔者在南加州大学 (University of Southern California,缩写USC) 攻读计算机工程硕士学位时,选修了编号<em>CS551: Computer Communication</em> 的课程。这是一门面向研究生的计算机网络通信课程,非常受到学生欢迎<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>。但是CS551也以其难度较大的软件课件项目而著称,让不少缺乏编程经验的非EE/CS专业的学生望而却步。笔者那一学期的两个项目,第一个就是用C/C++编程实现距离矢量路由协议的仿真。完成这一课件作业,的确让自己增长了不少网络编程的经验,也加深了对距离矢量路由协议的理解。现在总结共享出来,希望对其他人有所帮助。</p>\n<div class=\"note success no-icon\"><p><strong>Mathematicians makes natural questions precise.</strong><br> <strong>— <em>Richard Bellman</em>(理查德·贝尔曼,美国应用数学家,动态规划的创始人)</strong></p>\n</div>\n<h3 id=\"路由算法\">路由算法</h3>\n<p>距离矢量路由协议的核心是贝尔曼-福特算法 (Bellman–Ford algorithm),以美国两位数学家理查德·贝尔曼(Richard Bellman) 和小莱斯特·福特 (Lester Ford Jr.) 命名。贝尔曼1958年发布最短路径路由算法的论文,而福特与另一位美国数学家德尔伯特·富尔克森 (Delbert Fulkerson) 先于1956年在他们的网络流著作中提出了计算最大流通量的分布式贪心算法。二者相结合就产生了距离矢量路由协议,用于计算网络的最佳路由。全球互联网的鼻祖ARPANET就是使用的距离矢量路由协议。</p>\n<p>先来看看这一算法是如何工作的。对于给定的网络拓扑图及其顶点集合 <span class=\"math inline\">\\(V\\)</span> 和带权重的边集合 <span class=\"math inline\">\\(E\\)</span>,目的是要求得从每一个顶点到其它顶点的最短路径。贝尔曼-福特算法以松弛操作为基础,先预估到其它顶点的路径最大值,然后逐次计算出更加准确的最短路径值替换原来的估计值,重复迭代最终得到最优解。算法的伪代码描述如下:</p>\n<figure class=\"highlight pascal\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">procedure</span> <span class=\"title\">BellmanFord</span><span class=\"params\">(list vertices, list edges, vertex source)</span></span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"comment\">// 输入由n个顶点(vertice)和边(edge)的列表构成的图,执行算法找到从源点到</span></span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"comment\">// 其它顶点的最短路径,保存到距离(distance)和前向顶点(predecessor)数组</span></span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"title\">distance</span> :</span>= list <span class=\"keyword\">of</span> size n</span><br><span class=\"line\"> predecessor := list <span class=\"keyword\">of</span> size n</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 第一步:初始化图</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> each vertex v <span class=\"keyword\">in</span> vertices:</span><br><span class=\"line\"> distance[v] := infinity</span><br><span class=\"line\"> predecessor[v] := null</span><br><span class=\"line\"> </span><br><span class=\"line\"> distance[source] := <span class=\"number\">0</span> <span class=\"comment\">// 源点到自身的距离为0</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 第二步:重复松弛操作</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> i from <span class=\"number\">1</span> <span class=\"keyword\">to</span> size(vertices)-<span class=\"number\">1</span>:</span><br><span class=\"line\"> <span class=\"keyword\">for</span> each edge (u, v) <span class=\"keyword\">with</span> weight w <span class=\"keyword\">in</span> edges:</span><br><span class=\"line\"> <span class=\"keyword\">if</span> distance[u] + w < distance[v]:</span><br><span class=\"line\"> distance[v] := distance[u] + w</span><br><span class=\"line\"> predecessor[v] := u</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 第三步:检查是否有负权重的回路</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> each edge (u, v) <span class=\"keyword\">with</span> weight w <span class=\"keyword\">in</span> edges:</span><br><span class=\"line\"> <span class=\"keyword\">if</span> distance[u] + w < distance[v]:</span><br><span class=\"line\"> error "图包含负权重的回路"</span><br><span class=\"line\"> </span><br><span class=\"line\"> return distance, predecessor</span><br></pre></td></tr></table></figure>\n<p>在以上的算法描述中,松弛操作循环每次都是作用于所有边,重复次数实际上对应所得到的最短路径的深度。以 <span class=\"math inline\">\\(|V|\\)</span> 和 <span class=\"math inline\">\\(|E|\\)</span> 分别代表节点和边的数量,则贝尔曼-福特算法的时间复杂度可以表示为 <span class=\"math inline\">\\(O(|V|\\cdot|E|)\\)</span>。另外还注意到,算法的基本操作实质上是在广度上探寻,所以负权重的边不会影响运算结果。</p>\n<p>那么距离矢量在哪里?其实对于计算机网络这样的分布式系统,每个网络节点 (就是图中的顶点) 最初只有与自己相邻节点的距离 (就是图中边的权重) 信息。所以要执行贝尔曼-福特算法,节点就必须向其邻接点发送路由信息,这样邻接点才能实现松弛操作运算。路由信息包括<strong>本节点到达所有其它节点的最短路径值序列,也就是距离矢量</strong>。每当节点收到邻接点发来距离矢量,就执行一轮松弛操作运算。如果运算结果产生了新的最短距离,就更新路由表并发出新的距离矢量给所有邻接点。如此往复,直到收敛得到最短距离,算法结束。</p>\n<p>下面以一个6节点的网络来说明距离矢量路由协议的执行细节:</p>\n<pre class=\"mermaid\">\ngraph LR\n\nsubgraph 网络拓扑图\n G((A)) --- |1| H((B))\n G((A)) --- |4| I((C))\n G((A)) --- |6| J((D))\n H --- |1| I\n I --- |1| J\n J --- |1| K((E))\n I --- |1| K\n K --- |1| L((F))\n I --- |4| L\n H --- |5| L\nend\n\n</pre>\n<p>上图由6个节点 <span class=\"math inline\">\\(A-F\\)</span> 和 <span class=\"math inline\">\\(10\\)</span> 条链路构成网络连接拓扑。每个节点的距离矢量组合成一个 <span class=\"math inline\">\\(6\\times6\\)</span> 距离矩阵。如下第一个表 (Init) 所示,链路的权重值确定了距离矩阵的初始化状态。矩阵是沿着左上到右下对角线对称的,对角线上的元素代表源点到自身的距离,所以全为0。第一行元素为节点 <span class=\"math inline\">\\(A\\)</span> 到其它节点的距离,即它的距离矢量。因为 <span class=\"math inline\">\\(A\\)</span>只与 <span class=\"math inline\">\\(B/C/D\\)</span> 相邻,距离为 <span class=\"math inline\">\\(1/4/6\\)</span>。<span class=\"math inline\">\\(A\\)</span> 到 <span class=\"math inline\">\\(E/F\\)</span> 的距离初始设置为无穷大。</p>\n<p><span class=\"math display\">\\[\n% outer vertical array of arrays\n\\begin{array}{c}\n% inner horizontal array of arrays\n\\begin{array}{cc}\n% inner array of minimum values\n\\begin{array}{c|ccccc}\n\\text{Init} & A & B & C & D & E & F\\\\\n\\hline\nA & 0 & 1 & 4 & 6 & \\infty & \\infty\\\\\nB & 1 & 0 & 1 & \\infty & \\infty & 5\\\\\nC & 4 & 1 & 0 & 1 & 1 & 4\\\\\nD & 6 & \\infty & 1 & 0 & 1 & \\infty\\\\\nE & \\infty & \\infty & 1 & 1 & 0 & 1\\\\\nF & \\infty & 5 & 4 & \\infty & 1 & 0\n\\end{array}\n&\n&\n&\n% inner array of maximum values\n\\begin{array}{c|ccccc}\n\\text{No.1} & A & B & C & D & E & F\\\\\n\\hline\nA & 0 & 1 & \\color{fuchsia}{2} & \\color{fuchsia}{5} & \\color{fuchsia}{5} & \\color{fuchsia}{6}\\\\\nB & 1 & 0 & 1 & \\color{fuchsia}{2} & \\color{fuchsia}{2} & 5\\\\\nC & \\color{fuchsia}{2} & 1 & 0 & 1 & 1 & \\color{fuchsia}{2}\\\\\nD & \\color{fuchsia}{5} & \\color{fuchsia}{2} & 1 & 0 & 1 & \\color{fuchsia}{2} \\\\\nE & \\color{fuchsia}{5} & \\color{fuchsia}{2} & 1 & 1 & 0 & 1\\\\\nF & \\color{fuchsia}{6} & 5 & \\color{fuchsia}{2} & \\color{fuchsia}{2} & 1 & 0\n\\end{array}\n\\end{array}\n\\\\[2ex]\n\\\\\n% inner horizontal array of arrays\n\\begin{array}{cc}\n% inner array of minimum values\n\\begin{array}{c|ccccc}\n\\text{No.2} & A & B & C & D & E & F\\\\\n\\hline\nA & 0 & 1 & 2 & \\color{fuchsia}{3} & \\color{fuchsia}{3} & 6 \\\\\nB & 1 & 0 & 1 & 2 & 2 & \\color{fuchsia}{3}\\\\\nC & 2 & 1 & 0 & 1 & 1 & 2\\\\\nD & \\color{fuchsia}{3} & 2 & 1 & 0 & 1 & 2\\\\\nE & \\color{fuchsia}{3} & 2 & 1 & 1 & 0 & 1\\\\\nF & 6 & \\color{fuchsia}{3} & 2 & 2 & 1 & 0\n\\end{array}\n&\n&\n&\n% inner array of maximum values\n\\begin{array}{c|ccccc}\n\\text{No.3} & A & B & C & D & E & F\\\\\n\\hline\nA & 0 & 1 & 2 & 3 & 3 & \\color{fuchsia}{4}\\\\\nB & 1 & 0 & 1 & 2 & 2 & 3\\\\\nC & 2 & 1 & 0 & 1 & 1 & 2 \\\\\nD & 3 & 2 & 1 & 0 & 1 & 2\\\\\nE & 3 & 2 & 1 & 1 & 0 & 1\\\\\nF & \\color{fuchsia}{4} & 3 & 2 & 2 & 1 & 0\n\\end{array}\n\\end{array}\n\\end{array}\n\\]</span></p>\n<p>接下来第一轮路由信息交换,<span class=\"math inline\">\\(A\\)</span> 收到 <span class=\"math inline\">\\(B\\)</span> 的距离矢量。<span class=\"math inline\">\\(B\\)</span> 到 <span class=\"math inline\">\\(C/F\\)</span> 的距离为 <span class=\"math inline\">\\(1/5\\)</span>,<span class=\"math inline\">\\(A\\)</span> 执行松弛操作运算,得到新的距离值 <span class=\"math inline\">\\(2/6\\)</span>。这小于 <span class=\"math inline\">\\(A\\)</span> 当前到 <span class=\"math inline\">\\(C/F\\)</span> 的距离 <span class=\"math inline\">\\(4/\\infty\\)</span>,所以 <span class=\"math inline\">\\(A\\)</span> 更新它的距离矢量。同理,<span class=\"math inline\">\\(A\\)</span> 在处理完收到的 <span class=\"math inline\">\\(C\\)</span> 的距离矢量之后,将它到 <span class=\"math inline\">\\(D/E\\)</span> 的最短距离更新为 <span class=\"math inline\">\\(5/5\\)</span>。这就是以上表No.1里第一行颜色高亮所示的变化。第一轮里其它节点也同时执行松弛操作运算,其距离矢量的变化也由颜色高亮显示。</p>\n<p>重复这一过程,每一轮的最短距离变化都由颜色高亮显示。至第三轮(No.3)结束,距离矩阵不再变化,算法收敛完毕。最后生成的最短路径如下图中的粗实线所示:</p>\n<pre class=\"mermaid\">\ngraph LR\n\nsubgraph 最短路径图\n A((A)) === |1| B((B))\n A((A)) --- |4| C((C))\n A((A)) --- |6| D((D))\n B === |1| C\n C === |1| D\n D === |1| E((E))\n C === |1| E\n E === |1| F((F))\n C --- |4| F\n B --- |5| F\nend\n\n</pre>\n<p>这时节点 <span class=\"math inline\">\\(A\\)</span> 的路由表如下:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">目的节点</th>\n<th style=\"text-align: center;\">下一跳</th>\n<th style=\"text-align: center;\">链路开销</th>\n<th style=\"text-align: center;\">路径</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">A-B</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">C</td>\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">A-B-C</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">D</td>\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">3</td>\n<td style=\"text-align: center;\">A-B-C-D</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">E</td>\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">3</td>\n<td style=\"text-align: center;\">A-B-C-E</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">F</td>\n<td style=\"text-align: center;\">B</td>\n<td style=\"text-align: center;\">4</td>\n<td style=\"text-align: center;\">A-B-C-E-F</td>\n</tr>\n</tbody>\n</table>\n<h3 id=\"算法改进\">算法改进</h3>\n<p>贝尔曼-福特算法本身是无瑕的,应用于距离矢量路由协议也发挥了有效的路由功能。然而,在现实的网络部署中,由于系统的动态和分布式特质,距离矢量路由协议在实际运行中暴露出来了一些问题。下面对两个突出的问题做一些简单讨论:</p>\n<ul>\n<li><strong>反弹效应</strong>:参考下图的4节点网络,最短路径为A-B-C-D。现在C和D之间的链路突然中断,C马上检测到这一故障,并把到D的距离改为无穷大。但是在C发出新的距离矢量之前,先收到来自B的距离矢量。这是很常见的,因为许多协议实现规定周期性的发送距离矢量报文,以防丢失。由于B的距离矢量里到D的距离为2,所以C将到D的距离更新为3,并把B设为到D的下一跳。然后C又发出路由更新到B。B随之更新到D的距离为4,并依然将C设为下一跳。这就形成了一个循环,A/B/C所有到D的数据包将在B和C间反复传输,直至“存活时间” (TTL) 超时而被抛弃。这就是“反弹效应”。只有当B计算出通过C到达D的距离大于7时,B才将D直接设为下一跳,循环中止。</li>\n</ul>\n<pre class=\"mermaid\">\ngraph LR\n\nsubgraph 反弹效应\n E((A)) --- |1| F((B))\n F --- |1| G((C))\n F --- |7| H((D))\n G -.- |1| H\nend\n\n</pre>\n<ul>\n<li><strong>计数到无穷大</strong>:同样的4节点网络,假定现在B到C和D之间的链路同时中断了,网络被完全分隔成两个独立子网A/B及C/D。当反弹效应产生时,因为不存在到达另一个子网中任何节点的真正可达路径,每个子网里的循环都不会中止。由于无法收敛,在A和B的路由表里到C和D距离会一直增大下去。C和D之间也可能出现类似的现象,它们到A和B距离会一直循环往复增大。这种过程被称为“计数到无穷大”。当出现这一情况时,网路数据传输处于极度混乱状态,大量数据包被循环发送,链路拥塞,路由更新也会因此而丢失。</li>\n</ul>\n<pre class=\"mermaid\">\ngraph LR\n\nsubgraph 计数到无穷大\n A((A)) --- |1| B((B))\n B -.- |1| C((C))\n B -.- |7| D((D))\n C --- |1| D\nend\n</pre>\n<p>反弹效应和计数到无穷大问题,对距离矢量路由协议在实际网络中的功效带来了困扰。对此,研究人员采纳了一些技术措施来将这些不利影响降低至最小。具体应用到RIP协议中的有“水平分割” (split-horizon) 和“触发更新” (triggered-updates)等。</p>\n<p>水平分割的思想是,如果节点A到目的地X的下一跳是节点B,那么A不应该告知B它有一条更短的路径到达X。在实现上,A可以从它发给B的距离矢量消息中拿掉到X的路由。还有一种更积极的方法,称为“毒性反转水平分割” (split-horizon with poisonous reverse),是让A继续发出到X的路由,但是将其距离设定为无穷大。这样就可以立即消除两个节点间的循环。触发更新指定节点在察觉到链路中断时,立即发出更新消息,而不用等到下一个发送周期开始。这当然可以加快收敛速度,大幅减少路由循环的出现。</p>\n<p>然而,即使使用毒性反转水平分割和触发更新,也不能完全消除路由循环。在以上的4节点网络中,如果A到B的连接掉线,在出现路由更新丢失或不对等时延的情况下,B/C/D仍然可能会形成B-D-C-B的三点路由循环。所以,距离矢量路由协议还是必须设置一个路径距离的上限,以及时认定计数到无穷大的发生并马上中止循环。对于以跳转次数作为距离度量的RIP协议,规定最大距离值为15,超过15即被视为不可达。</p>\n<p>还有其他的研究者给出了不同的环路解构方案。在1989年的ACM SIGCOMM会议上,陈俊祥 (Chunhsiang Cheng) 等人<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>提出了一种<strong>扩展的贝尔曼-福特算法</strong>以消除环路。新算法在贝尔曼-福特算法的基础上添加了“源跟踪”功能。其设想是在路由表和路由更新里加入路径头 (head) 信息,在他们的论文中对路径头的定义是:</p>\n<blockquote>\n<p>The <em>head</em> of a path <span class=\"math inline\">\\(R_{ij}\\)</span> is defined to be the last node preceding node j in the sequence of nodes in <span class=\"math inline\">\\(R_{ij}\\)</span> (i.e., if <span class=\"math inline\">\\(R_{ij}=(i,n_1,n_2,..,n_r,j)\\)</span>, then head of <span class=\"math inline\">\\(R_{ij}\\)</span> is <span class=\"math inline\">\\(n_r\\)</span> if r > 0, and equal to i if r=0).</p>\n</blockquote>\n<p>显然路径头就是到目的地的路径中逆向的第一个节点。如果目的地是直接相邻的节点,本地节点就是路径头。将路径头加入到路由更新里,就会在网络中随着距离矢量一直传播到所有节点。那么如何检测环路呢?论文给出名为<strong>IN_PATH</strong>的函数伪代码:</p>\n<blockquote>\n<p><strong>Function</strong> IN_PATH(<span class=\"math inline\">\\(Node,Neighbor,Dest\\)</span>);<br />\n(* return true or false *)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span> <strong>begin</strong><br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <span class=\"math inline\">\\(h \\gets HEAD_{Node}(Dest)\\)</span>;<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> (* find head from Node to Dest *)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <strong>if</strong> <span class=\"math inline\">\\(h=Node\\)</span> <strong>then</strong><br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> (* Neighbor is not in <span class=\"math inline\">\\(R_{NodeDest}\\)</span> *)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <strong>return</strong>(false)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <strong>else if</strong> <span class=\"math inline\">\\(h=Neighbor\\)</span> <strong>then</strong><br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> (* Neighbor is in <span class=\"math inline\">\\(R_{NodeDest}\\)</span> *)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <strong>return</strong>(true)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> <strong>else</strong><br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> IN_PATH(<span class=\"math inline\">\\(Node,Neighbor, HEAD_{Node}(h)\\)</span>);<br />\n<span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span><span class=\"math inline\">\\(\\qquad\\)</span> (* cannot determine yet,try again *)<br />\n<span class=\"math inline\">\\(\\qquad\\)</span> <strong>end;</strong></p>\n</blockquote>\n<p>当节点 (Node) 想要向邻接点 (Neighbor) 发布去往目的地 Dest 的路由消息时,就执行IN_PATH函数。函数先取出目的地的路径头节点,检查其是否为节点本身,是就返回false;否则看看其是否为邻接点,是就返回true;两者都不是,就将路径头节点作为目的地,取出新的路径头节点,递归调用函数自身。所以当函数返回true时,表明路由消息的接收者就是路径头,节点完全没有必要发布此路由。换而言之,我们检测到一个环路,此时节点应该将距离值设为无穷大。当函数返回false时,节点正常发布路由消息。</p>\n<p>“源跟踪”算法可以更有效的解构环路,但是却增加了不少计算量,这与RIP协议简单通用和易于实现的设计原则相违背。此外,实际局域网的路由广播特性和子网聚合配置,不能保证提供和传播准确的路径头信息。所以这一类扩展的贝尔曼-福特算法并没有投入实用。然而,它却是很好的网络路由协议学习和实验素材,CS551 软件课件项目就是要求仿真实现<strong>扩展的贝尔曼-福特算法</strong>!</p>\n<h3 id=\"仿真设计\">仿真设计</h3>\n<h4 id=\"项目要求和建议\">项目要求和建议</h4>\n<p>以下是 CS551 任课教师 <a href=\"https://nsl.usc.edu/people/ramesh/\">Ramesh Govindan</a><a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a> 教授给出的课件项目要求和参考建议:</p>\n<ol type=\"1\">\n<li>写一个简单的<strong>管理员</strong>程序读入网络连接描述文件,然后生成几个子进程。每个子进程仿真一个路由器。\n<ul>\n<li>网络连接描述文件的格式是:\n<ul>\n<li>第一行包含单个整数<span class=\"math inline\">\\(N\\)</span>,表示网络中有<span class=\"math inline\">\\(N\\)</span>个节点,地址从<span class=\"math inline\">\\(0\\)</span>到<span class=\"math inline\">\\(N-1\\)</span></li>\n<li>后面每一行描述一个网络中的点对点链路。每一行有三个空格分开的整数:<span class=\"math inline\">\\(X\\)</span>、<span class=\"math inline\">\\(Y\\)</span>和<span class=\"math inline\">\\(C\\)</span>。<span class=\"math inline\">\\(X\\)</span>和<span class=\"math inline\">\\(Y\\)</span>是<span class=\"math inline\">\\([0,N-1]\\)</span>范围内的节点编号,<span class=\"math inline\">\\(C\\)</span>是一个代表<span class=\"math inline\">\\(X\\)</span>和<span class=\"math inline\">\\(Y\\)</span>之间链路开销的正整数</li>\n</ul></li>\n</ul></li>\n</ol>\n<ul>\n<li>每个路由器都开启一个UDP套接字 (socket),用以与邻接路由器交换路由信息;在此之前,路由器必须知道其地址和邻接表信息</li>\n<li>你必须实现一个简单的协议,让管理员告知路由器这一信息。建议的方法是:\n<ul>\n<li>在每个路由器启动之后,建立一个TCP连接到管理员 (想想如何实现)</li>\n<li>路由器向管理员发送消息,消息包含它的UDP端口号</li>\n<li>管理员回复路由器的地址和邻接表信息 (使用自己定义的消息格式)</li>\n</ul></li>\n</ul>\n<ol start=\"2\" type=\"1\">\n<li>在每个仿真路由器收到它的邻接表后,开始执行<strong>扩展的贝尔曼-福特算法</strong> (参见陈俊祥的论文):\n<ul>\n<li>每个路由器都需要与邻接路由器交换距离矢量信息,设计你的路由表和距离矢量消息格式</li>\n<li>你可以假定不会出现节点失误或链路断线的情况,不需要仿真由此触发的路由更新</li>\n<li>你必须仔细阅读和理解协议处理规则,先在简单的拓扑图上熟悉算法</li>\n<li>每个路由器在收到邻接表后,发出第一个路由更新,之后只在路由表变化才发出路由更新</li>\n<li>你可以设置一定时长的定时器,以决定仿真结束时刻,结束后所有进程都必须及时终止</li>\n</ul></li>\n<li>仿真结束输出两类文件:\n<ul>\n<li>一个名为 ports 的文件,有<span class=\"math inline\">\\(N\\)</span>行。每行列出以空格分开的两个数字<span class=\"math inline\">\\(X\\)</span>和<span class=\"math inline\">\\(Y\\)</span>,<span class=\"math inline\">\\(X\\)</span>为节点地址,<span class=\"math inline\">\\(Y\\)</span>为其UDP端口号。全部行以地址从低到高排序。</li>\n<li>每个路由器的路由表文件,文件名为路由器地址 (<span class=\"math inline\">\\([0,N-1]\\)</span>),总共有<span class=\"math inline\">\\(N\\)</span>个文件\n<ul>\n<li>每个路由表文件有<span class=\"math inline\">\\(N\\)</span>行,每行对应一条到目标路由器的路由</li>\n<li>路由格式是:“X Y C P1,P2,P3”</li>\n<li>X是目标路由器地址,Y是到从本路由器到X的下一跳地址,C是链路开销</li>\n<li>P1,P2,P3是从X到本路由器的 (逆向) 路径,以逗号分隔</li>\n</ul></li>\n</ul></li>\n</ol>\n<p>以上的要求和建议其实给出了仿真程序设计的框架和运行流程,在此基础上可以进一步考虑管理员和路由器进程的许多设计细节。</p>\n<h4 id=\"管理员进程\">管理员进程</h4>\n<p>从流程上看,管理员进程是主进程,负责读入网络连接信息;同时它也是父进程,为每个路由器生成子进程。管理员要开启TCP套接字,以接收每个路由器所监听的UDP端口号。那么路由器怎么知道管理员的TCP端口号呢?答案就在生成子进程的过程里。当管理员调用<code>fork()</code>生成子进程时,立即传递自己的TCP套接字文件描述符和总节点数目给路由器函数。而当路由器连接到管理员后,管理员也会从<code>accept()</code>的返回值得到路由器的TCP套接字文件描述符,这样管理员就可以发送后续的邻接表信息。</p>\n<p>理清了这一过程之后,整个管理员进程的运行时序就很清晰了。以下列出穿插关键数据结构定义的完整流程:</p>\n<ol type=\"1\">\n<li><p>启动,读入命令行参数<code>argv[1]</code>,也就是网络连接描述文件名。</p></li>\n<li><p>初始化网络节点邻接表:一个结构类型<code>source</code>的数组,记录节点地址<code>id</code>、相邻节点数<code>numNeighbor</code>以及邻接点信息的链表<code>link</code>;邻接点信息定义为另一个结构类型<code>neighbor</code>,包含节点地址<code>id</code>、链路开销<code>cost</code>和下一个邻接点指针:</p>\n<figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">neighbor</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> id;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> cost;</span><br><span class=\"line\"> neighbor *next;</span><br><span class=\"line\">};</span><br><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">source</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> id;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> numNeighbor;</span><br><span class=\"line\"> neighbor *link;</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure></li>\n<li><p>读入网络连接描述文件,分析后构造网络节点邻接表。</p></li>\n<li><p>创建TCP套接字,然后调用绑定<code>bind()</code>和监听<code>listen()</code>函数。</p></li>\n<li><p>循环调用<code>fork()</code>生成全部路由器子进程,传递TCP套接字文件描述符和总节点数目给路由器启动函数:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// fork the routers</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (i = <span class=\"number\">0</span>; i <numNode; i++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> ((childpid = fork())==<span class=\"number\">0</span>) {</span><br><span class=\"line\"> <span class=\"built_in\">router</span>(i, listenfd, numNode);</span><br><span class=\"line\"> <span class=\"built_in\">exit</span>(<span class=\"number\">0</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br></pre></td></tr></table></figure></p></li>\n<li><p>循环调用<code>accept()</code>接收每个路由器发过来的UDP端口号,保存到 ports 文件;同时记录每个路由器的TCP连接文件描述符。</p></li>\n<li><p>循环给每个路由器发送邻接信息:</p>\n<ul>\n<li><p>先是路由器地址和相邻节点总数:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// This is header message to each router to tell it its own</span></span><br><span class=\"line\"><span class=\"comment\">// ID and how many neighbors it has.</span></span><br><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">message</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> routerId;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> numNeighbor;</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure></p></li>\n<li><p>后面是每一个邻接点的<em><节点地址,链路开销,UDP端口号></em>信息三元组:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// This message is used for the manager to tell each router</span></span><br><span class=\"line\"><span class=\"comment\">// its neighbor information including <id, cost, UDP_port>.</span></span><br><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">nb_tuple</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> neighborId;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> cost;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> UDP_port;</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure></p></li>\n</ul></li>\n<li><p>循环调用<code>wait()</code>等待每个路由器子进程结束,然后整个仿真过程结束。</p></li>\n</ol>\n<h4 id=\"路由器进程\">路由器进程</h4>\n<p>路由器进程的设计和实现要复杂得多。这里要创建距离矢量矩阵和路由表、实现节点间距离矢量的交换,还要处理距离矢量信息、更新路由表并重发距离矢量。这是一个典型的异步多进程软件设计的问题。另外还要记得必须实现<strong>扩展的贝尔曼-福特算法</strong>的环路解构功能。</p>\n<p>先来看看核心数据结构的定义和初始化代码:</p>\n<ul>\n<li>距离矢量结构及其二维矩阵初始化\n<ul>\n<li><p>距离矢量包含距离度量值和环路解构需要的路径头节点地址:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">Dis_matrix</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> distance;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> headId;</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure></p></li>\n<li><p>初始化 [总节点数] <span class=\"math inline\">\\(\\times\\)</span> [本节点的相邻节点数] 的二维矩阵 (-1代表不可达):</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// create distance matrix entries</span></span><br><span class=\"line\">Dis_matrix **entry;</span><br><span class=\"line\">entry = <span class=\"keyword\">new</span> Dis_matrix*[numNode];</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// initialize distance matrix</span></span><br><span class=\"line\"><span class=\"keyword\">for</span> (i=<span class=\"number\">0</span>; i<numNode; i++) {</span><br><span class=\"line\"> entry[i] = <span class=\"keyword\">new</span> Dis_matrix[msg.numNeighbor];</span><br><span class=\"line\"> <span class=\"keyword\">for</span> (j=<span class=\"number\">0</span>; j<msg.numNeighbor; j++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (tup[j].neighborId==i) {</span><br><span class=\"line\"> entry[i][j].distance=tup[j].cost;</span><br><span class=\"line\"> entry[i][j].headId=id;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> entry[i][j].distance=<span class=\"number\">-1</span>;</span><br><span class=\"line\"> entry[i][j].headId=<span class=\"number\">-1</span>;</span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></p></li>\n</ul></li>\n<li>路由表结构及其初始化\n<ul>\n<li><p>路由表项是一个<em><目的节点,链路开销,下一跳,路径头节点></em>的四元组:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">RtableEntry</span> {</span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> dest;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> cost;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> nexthop;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> head;</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure></p></li>\n<li><p>初始化为 [总节点数] 一维数组:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// initialize routing table</span></span><br><span class=\"line\">RtableEntry *table = <span class=\"keyword\">new</span> RtableEntry[numNode];</span><br><span class=\"line\"><span class=\"keyword\">for</span> (i=<span class=\"number\">0</span>; i<numNode; i++) {</span><br><span class=\"line\"> <span class=\"keyword\">int</span> tmp_cost = <span class=\"number\">-1</span>; </span><br><span class=\"line\"> <span class=\"keyword\">int</span> tmp_next = tup[<span class=\"number\">0</span>].neighborId; </span><br><span class=\"line\"> <span class=\"keyword\">int</span> tmp_head = <span class=\"number\">-1</span>; </span><br><span class=\"line\"> <span class=\"keyword\">for</span> (j=<span class=\"number\">0</span>; j<msg.numNeighbor; j++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (entry[i][j].distance != <span class=\"number\">-1</span>) {</span><br><span class=\"line\"> tmp_cost = entry[i][j].distance;</span><br><span class=\"line\"> tmp_head = entry[i][j].headId;</span><br><span class=\"line\"> tmp_next = tup[j].neighborId;</span><br><span class=\"line\"> <span class=\"keyword\">break</span>;</span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\"> table[i].dest = i;</span><br><span class=\"line\"> table[i].cost = tmp_cost;</span><br><span class=\"line\"> table[i].nexthop = tmp_next;</span><br><span class=\"line\"> table[i].head = tmp_head;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></p></li>\n</ul></li>\n</ul>\n<p>接下来一个重要设计是实现论文中的IN_PATH函数以检测环路。原文的伪代码用到了递归,是为了说明的方便。所有的递归都可以转化为迭代,从性能上考虑迭代更好。仿真程序实现的IN_PATH函数如下:</p>\n<figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">bool</span> <span class=\"title\">IN_PATH</span> <span class=\"params\">(<span class=\"keyword\">int</span> NB, <span class=\"keyword\">int</span> dest, RtableEntry *table, <span class=\"keyword\">int</span> id)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> head=table[dest].head;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (head==<span class=\"number\">-1</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">false</span>;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> ((head!=id) && (head!=NB) && (head!=<span class=\"number\">-1</span>))</span><br><span class=\"line\"> head=table[head].head;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (head==id || head==<span class=\"number\">-1</span>) <span class=\"keyword\">return</span> <span class=\"literal\">false</span>;</span><br><span class=\"line\"> <span class=\"keyword\">else</span> <span class=\"keyword\">return</span> <span class=\"literal\">true</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>函数里的变量名与论文中的的基本一致。可以看到,其实这是一个简单的代码实现,但是其中蕴含的思想却很重要。如论文中所述,这个函数在每次向邻接点发送路由更新时都要被调用。</p>\n<p>在以上这些都就绪后,整个路由器的运行流程就可以清楚地表述如下:</p>\n<ol type=\"1\">\n<li><p>创建TCP套接字,连接到管理员进程。</p></li>\n<li><p>创建UDP套接字,绑定<code>bind()</code>后调用<code>getsockname()</code>取得端口号并发给管理员。</p></li>\n<li><p>从管理员接收相邻节点信息,包括链路开销和UDP端口号</p></li>\n<li><p>初始化距离矢量矩阵和路由表,开始路由。</p></li>\n<li><p>先发送第一个距离矢量消息给每个邻接点,消息是字符串格式:</p>\n<ul>\n<li>开头:\"<本节点地址>[空格]<邻节点地址>[空格]<总节点数><strong>*</strong>\"</li>\n<li>然后对每一个目的地重复:\"<目的节点地址>[空格]<链路开销>[空格]<路径头节点地址><strong>#</strong>\"</li>\n</ul></li>\n<li><p>调用<code>select()</code>开始事件循环,并设置定时为三倍总节点数的秒数。当在超时前收到邻接点发来的距离矢量时,<code>select()</code>返回大于0的值,路由器作如下处理:</p>\n<ul>\n<li><p>分析收到的距离矢量,更新自身距离矢量矩阵,然后生成新的路由表,原始的贝尔曼-福特算法的实现就在这里:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">UpdateDisMatrix</span> <span class=\"params\">(<span class=\"keyword\">char</span>* recv, Dis_matrix **Entry,<span class=\"keyword\">int</span> numNode, <span class=\"keyword\">int</span> id,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\"> <span class=\"keyword\">int</span> numNeighbor, nb_tuple *tup)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> sour, dest, tempDest, tempDist, tempHead, i, j, k;</span><br><span class=\"line\"> <span class=\"keyword\">char</span> *temp;</span><br><span class=\"line\"> </span><br><span class=\"line\"> temp=<span class=\"built_in\">strchr</span>(recv,<span class=\"string\">'*'</span>)+<span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"built_in\">sscanf</span>(recv,<span class=\"string\">"%d %d %d*"</span>,&sour,&dest,&numNode);</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> (i=<span class=\"number\">0</span>;i<numNode;i++) {</span><br><span class=\"line\"> <span class=\"built_in\">sscanf</span>(temp,<span class=\"string\">"%d %d %d"</span>,&tempDest,&tempDist,&tempHead);</span><br><span class=\"line\"> temp=<span class=\"built_in\">strchr</span>(temp,<span class=\"string\">'#'</span>)+<span class=\"number\">1</span>;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">if</span> ((tempDest==id)||(tempDest==sour))</span><br><span class=\"line\"> <span class=\"keyword\">continue</span>;</span><br><span class=\"line\"> </span><br><span class=\"line\">\t <span class=\"keyword\">for</span> (j=<span class=\"number\">0</span>; j<numNode; j++) {</span><br><span class=\"line\">\t <span class=\"keyword\">if</span> (j==tempDest) {</span><br><span class=\"line\">\t <span class=\"keyword\">for</span> (k=<span class=\"number\">0</span>;k<numNeighbor; k++)</span><br><span class=\"line\">\t <span class=\"keyword\">if</span> (tup[k].neighborId==sour)</span><br><span class=\"line\">\t <span class=\"keyword\">break</span>;</span><br><span class=\"line\">\t <span class=\"keyword\">if</span> (tempDist==<span class=\"number\">-1</span>) {</span><br><span class=\"line\">\t Entry[j][k].distance=<span class=\"number\">-1</span>;</span><br><span class=\"line\">\t Entry[j][k].headId=<span class=\"number\">-1</span>;</span><br><span class=\"line\">\t } <span class=\"keyword\">else</span> {</span><br><span class=\"line\">\t Entry[j][k].distance=tempDist+tup[k].cost;</span><br><span class=\"line\">\t Entry[j][k].headId=tempHead;</span><br><span class=\"line\">\t }</span><br><span class=\"line\">\t <span class=\"keyword\">break</span>;</span><br><span class=\"line\">\t }</span><br><span class=\"line\">\t }</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br><span class=\"line\"> </span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">CreateRTfromDM</span> <span class=\"params\">(RtableEntry *rtEntry_new, Dis_matrix **Entry,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\"> nb_tuple *tup, <span class=\"keyword\">int</span> numDest, <span class=\"keyword\">int</span> numNB)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> i,j, tmp, k;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> (i=<span class=\"number\">0</span>; i<numDest; i++) {</span><br><span class=\"line\"> tmp=Entry[i][<span class=\"number\">0</span>].distance;</span><br><span class=\"line\"> k=<span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">for</span> (j=<span class=\"number\">1</span>;j<numNB; j++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (Entry[i][j].distance==<span class=\"number\">-1</span>)</span><br><span class=\"line\"> <span class=\"keyword\">continue</span>;</span><br><span class=\"line\"> <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (tmp==<span class=\"number\">-1</span>||Entry[i][j].distance<tmp) {</span><br><span class=\"line\"> tmp=Entry[i][j].distance;</span><br><span class=\"line\"> k=j;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> \t}</span><br><span class=\"line\"> rtEntry_new[i].dest=i;</span><br><span class=\"line\"> rtEntry_new[i].cost=tmp;</span><br><span class=\"line\"> rtEntry_new[i].nexthop=tup[k].neighborId;</span><br><span class=\"line\"> rtEntry_new[i].head=Entry[i][k].headId;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></p></li>\n<li><p>比较新旧路由表,如果有变化就替换掉旧的,并发送新的距离矢量给所有邻节点。发送代码调用IN_PATH函数实现<strong>扩展的贝尔曼-福特算法</strong>:</p>\n<p><figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">for</span> (i=<span class=\"number\">0</span>; i<numNode; i++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (<span class=\"built_in\">IN_PATH</span>(tup[j].neighborId, i, table, id)) {</span><br><span class=\"line\"> tmpRT[i].cost=<span class=\"number\">-1</span>;</span><br><span class=\"line\"> tmpRT[i].head=<span class=\"number\">-1</span>;</span><br><span class=\"line\"> tmpRT[i].dest=i;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> tmpRT[i].cost=table[i].cost;</span><br><span class=\"line\"> tmpRT[i].head=table[i].head;</span><br><span class=\"line\"> tmpRT[i].dest=i;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></p></li>\n<li><p>重新开始定时的事件循环。</p></li>\n</ul></li>\n<li><p>如果<code>select()</code>返回0,事件循环超时,这时假定网络路由已收敛,输出自己的路由表。</p></li>\n<li><p>路由器进程结束。</p></li>\n</ol>\n<h3 id=\"程序运行\">程序运行</h3>\n<p>原始的仿真程序当年在Sun SPARK工作站中编译,通过了教师提供的10节点网络测试用例。之后又测试自己编写的6节点 (就是前面路由算法举例的网络图) 和12节点测试用例,才最后提交。几天后从教学助理那里得知,仿真程序获得了满分!</p>\n<p>现在将仿真程序重新拿出来,在Red Hat Linux和macOS系统中编译链接,两个系统的编译运行环境如下:</p>\n<ul>\n<li>Red Hat Enterprise Linux 8.1 (Ootpa):\n<ul>\n<li>内核: Linux 4.18.0-147.3.1.el8_1.x86_64</li>\n<li>体系结构: x86-64</li>\n<li>处理器: Intel(R) Xeon(R) CPU E5-2667 v4 @ 3.20GHz</li>\n<li>编译器: g++ (GCC) 8.3.1 20190507 (Red Hat 8.3.1-4)</li>\n</ul></li>\n<li>macOS Catalina Version 10.15.7:\n<ul>\n<li>内核: Darwin 19.6.0: Tue Nov 10 00:10:30 PST 2020</li>\n<li>体系结构: x86_64</li>\n<li>处理器: 2.2 GHz 6-Core Intel Core i7</li>\n<li>编译器: Apple clang version 12.0.0 (clang-1200.0.32.28)</li>\n</ul></li>\n</ul>\n<p>在macOS上需要对程序原文件做一个小改动<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>才编译成功。而在链接阶段发现在两个系统上都不需要原来的socket和nsl目标库文件,因为它们都已经被包含在缺省加载的标准libc库中。清除这些障碍后,运行时却发现路由器收不到相邻节点的距离矢量消息,而发送方调用<code>sendto()</code>时并没有报错,在两个系统中的症状一样。</p>\n<p>困扰了一天半后,终于找到原因了。原来的代码里,路由器从管理员收到相邻节点的UDP端口号后,保存时做了一个Endianness转换:</p>\n<figure class=\"highlight c++\"><table><tr><td class=\"code\"><pre><span class=\"line\">neibaddr[i].sin_port = <span class=\"built_in\">htons</span>(tup[i].UDP_port);</span><br></pre></td></tr></table></figure>\n<p>Sun SPARK是<code>Big Endian</code>系统,而现在运行Red Hat Linux和macOS的系统是用的Intel x86_64体系结构,它们都是<code>Little Endian</code>的。注意到UDP端口号是从网络数据包中直接取出来的,所以这里可能不需要转换。果然,拿掉<code>htons()</code>之后,路由器之间的消息传递恢复正常,同样的三个测试用例全部通过。</p>\n<p>参考前面的路由算法讨论例图,以下是6节点网络的测试输入用例。节点A-F对应路由器ID 0-5:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">cc-simulate-dv:9 > cat test6.txt </span><br><span class=\"line\">6</span><br><span class=\"line\">0 1 1</span><br><span class=\"line\">0 2 4</span><br><span class=\"line\">1 2 1</span><br><span class=\"line\">0 3 6</span><br><span class=\"line\">2 3 1</span><br><span class=\"line\">3 4 1</span><br><span class=\"line\">2 4 1</span><br><span class=\"line\">5 1 5</span><br><span class=\"line\">2 5 4</span><br><span class=\"line\">4 5 1</span><br></pre></td></tr></table></figure>\n<p>仿真程序编译和运行的记录如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">cc-simulate-dv:10 > make</span><br><span class=\"line\">g++ -O2 -c manager.cc</span><br><span class=\"line\">g++ -O2 -c router.cc </span><br><span class=\"line\">g++ -O2 -o manager manager.o router.o </span><br><span class=\"line\">cc-simulate-dv:11 > manager test6.txt</span><br><span class=\"line\">numnode is: 6</span><br><span class=\"line\">0 3: 3.6 2.4 1.1 </span><br><span class=\"line\">1 3: 5.5 2.1 0.1 </span><br><span class=\"line\">2 5: 5.4 4.1 3.1 1.1 0.4 </span><br><span class=\"line\">3 3: 4.1 2.1 0.6 </span><br><span class=\"line\">4 3: 5.1 2.1 3.1 </span><br><span class=\"line\">5 3: 4.1 2.4 1.5 </span><br><span class=\"line\">R1 begin routing</span><br><span class=\"line\">R2 begin routing</span><br><span class=\"line\">R3 begin routing</span><br><span class=\"line\">R4 begin routing</span><br><span class=\"line\">R0 begin routing</span><br><span class=\"line\">R5 begin routing</span><br><span class=\"line\">R4 timeout!</span><br><span class=\"line\">R2 timeout!</span><br><span class=\"line\">R3 timeout!</span><br><span class=\"line\">R5 timeout!</span><br><span class=\"line\">R0 timeout!</span><br><span class=\"line\">R1 timeout!</span><br></pre></td></tr></table></figure>\n<p>运行结果显示节点A与F的路由表与逆向路径完全正确:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">cc-simulate-dv:11 > cat ports </span><br><span class=\"line\">0 37352</span><br><span class=\"line\">1 24551</span><br><span class=\"line\">2 45772</span><br><span class=\"line\">3 33265</span><br><span class=\"line\">4 38391</span><br><span class=\"line\">5 36583</span><br><span class=\"line\">cc-simulate-dv:12 > cat 0</span><br><span class=\"line\">0 -1 0</span><br><span class=\"line\">1 1 1 1,0</span><br><span class=\"line\">2 1 2 2,1,0</span><br><span class=\"line\">3 1 3 3,2,1,0</span><br><span class=\"line\">4 1 3 4,2,1,0</span><br><span class=\"line\">5 1 4 5,4,2,1,0</span><br><span class=\"line\">cc-simulate-dv:13 > cat 5</span><br><span class=\"line\">0 4 4 0,1,2,4,5</span><br><span class=\"line\">1 4 3 1,2,4,5</span><br><span class=\"line\">2 4 2 2,4,5</span><br><span class=\"line\">3 4 2 3,4,5</span><br><span class=\"line\">4 4 1 4,5</span><br><span class=\"line\">5 -1 0</span><br></pre></td></tr></table></figure>\n<p>完整的修正过的仿真程序打包下载链接在此:<a href=\"cc-simulate-dv.tgz\">cc-simulate-dv.tgz</a></p>\n<p>总结这一项目的完成,加深了对距离矢量路由协议的深刻理解,也熟悉了Unix类系统上的网络编程的许多规范和细节,笔者收获很大。另一方面,从课程设计上看,此软件项目还可以做一些优化和扩展实验:</p>\n<ul>\n<li>创建面向对象的路由器类,实现完全模块化的路由器</li>\n<li>处理距离矢量时直接更新原来的路由表,不必生成新的路由表再比较</li>\n<li>设计仿真节点失误或链路断线的情况,验证协议实现的收敛性</li>\n<li>在同样的仿真架构下,实现链路状态路由协议 (<a href=\"https://en.wikipedia.org/wiki/Open_Shortest_Path_First\">OSPF</a>)</li>\n<li>修改仿真架构,实现路径矢量路由协议 (<a href=\"https://en.wikipedia.org/wiki/Border_Gateway_Protocol\">BGP</a>)</li>\n</ul>\n<p>回顾起来,CS551讲解的计算机网络知识点和软件作业都让人受益良多。总体上看,CS551可能是笔者在USC上的最具有挑战性、但也学到最多东西的一门课程。这样的学习和训练为笔者之后长期的网络研发工作打下了坚实的基础。非常感谢任课教师 Ramesh Govindan 教授!</p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>时至今日,CS551依然如当年一样火热,报名需要资格审批 (Clearance) ,之后可能还要通过一个预考 (Placement Exam) 才能正式注册。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>C. Cheng, R. Riley, S. P. R. Kumar, and J. J. Garcia-Luna-Aceves. <em><a href=\"https://dl.acm.org/doi/abs/10.1145/75247.75269\">A loop-free Bellman-Ford routing protocol without bouncing effect</a></em>. In ACM SIGCOMM '8g, pages 224-237, September 1989<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>Govindan教授专注于大型网络路由基础设施和无线及移动网络体系结构研究。他是IEEE和ACM双会士 (Fellow),曾任 IEEE <em>移动计算</em> 会刊主编。Govindan教授2018年荣获 IEEE Internet Award。他现今依然活跃在科研和教学的一线。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>在macOS系统中,socket系列API的接口定义使用<code>socklen_t *restrict address_len</code>。这要求调用者传递严格类型定义的变量给地址长度参数<code>address_len</code>。<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["技术小札"],"tags":["C/C++编程","路由与交换"]},{"title":"思科 Catalyst Wi-Fi 6 MU-MIMO 带你畅享速度与激情","url":"/2021/06/05/Cisco-WiFi6-MuMIMO/","content":"<p>思科的技术博客网站设立了<a href=\"https://blogs.cisco.com/tag/wi-fi-6\">Wi-Fi 6专题页</a>,讲解Wi-Fi 6技术要点及思科相应产品功能。近期的一篇由思科高级无线工程师<a href=\"https://blogs.cisco.com/author/shreyastrivedi\">史瑞亚斯·特里维迪</a>和octoScope公司首席科学家<a href=\"http://www.linkedin.com/in/ehsshearer\">史蒂夫·希勒</a>合作的<a href=\"https://blogs.cisco.com/networking/too-fast-too-furious-with-catalyst-wi-fi-6-mu-mimo\">博文</a>,结合测试实例介绍OFDMA与MU-MIMO技术相结合,在多用户环境下达到两倍多总吞吐量的能力。文章简洁明了,可以让读者快速领悟Wi-Fi 6的关键技术革新,特此翻译为中文,分享给感兴趣的朋友。 <span id=\"more\"></span></p>\n<hr />\n<p><em>贡献者 – Ming Chong 和 Santa Chowdhury (octoScope公司)</em></p>\n<p><em>我们特别感谢 Nilesh Doshi(高级无线AP经理)的指导。</em></p>\n<p>由于前导码和其它机制产生的附加开销趋向于占主导地位,使用非Wi-Fi 6为许多传输小数据包的客户端提供服务是低效的。OFDMA<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>是这种情况下的理想解决方案,因为它将信道划分以同时服务多达37个用户(对应于80MHz带宽),开销也得到均摊。OFDMA提高了系统效率,但不一定能提高吞吐量。</p>\n<p>MU-MIMO(多用户、多输入、多输出)在发送器和小数量接收器中的每一个之间创建空间分离的独立信道,这样每个接收器只会收听到针对自己的信息,而非针对其它接收器的信息. 这意味着发送器可以通过叠加方式同时向几个接收器发送数据,从而提高总吞吐量,而增加的比例取决于接收器的数量。</p>\n<p>思科Catalyst 9800系列无线局域网控制器即将发布的IOS XE 17.6.1版本(当前处于Beta测试阶段)引入了新潮的接入点调度器设计,可同时高效地为多个客户端提供服务。系统激活此功能时生成最低水平的探测开销,由此即使在密集的用户环境中,也能收益接近物理层速率的数据速率。目前这些新特性在Catalyst 9130和Catalyst 9124系列接入点上得到支持。以下让我们先了解MU-MIMO的概念,然后评估其性能。</p>\n<h3 id=\"波束成形和mu-mimo\">波束成形和MU-MIMO</h3>\n<p>应用相控天线阵列的无线电波束成形技术已为人所知数十年。最近,这些技术理论已用于设计MU-MIMO,其中就包含了使用多个同时波束为每个用户提供独立信道的思想。</p>\n<p>类似的原理适用于音频领域,可以对扬声器定相以将声音引导到特定位置。想法是调整每个扬声器的相位,使声音在收听者所在的点构造性地增加,而在所有其它位置则解构性消减。</p>\n<p>考虑一个声源<span class=\"math inline\">\\(Sr\\)</span>通过四个扬声器的阵列播放,每个扬声器的声音各自由相量<span class=\"math inline\">\\(Q_{1r}\\sim Q_{4r}\\)</span>调整,以使红色收听者的信号强度<span class=\"math inline\">\\(L_r\\)</span>最大化,并且在蓝色收听者处<span class=\"math inline\">\\(L_b\\)</span>被最小化。</p>\n<p><img src=\"redphasor.png\" /></p>\n<p>相似地,我们选择一组相量<span class=\"math inline\">\\(Q_{1b}\\sim Q_{4b}\\)</span>最大化蓝色收听者处的信号,同时最小化红色收听者处的信号。</p>\n<p><img src=\"bluephasor.png\" /></p>\n<p>使用叠加,我们可以对每条消息施加适当的相位调整,并在信号进入扬声器之前聚合。通过这种方式,我们可以同时发送两条不同的消息,但每个听众只会听到针对他们的消息。</p>\n<p><img src=\"superposition.png\" /></p>\n<p>请注意空间分离的重要性—<span class=\"math inline\">\\(L_b\\)</span>和<span class=\"math inline\">\\(L_r\\)</span>听到他们各自的消息,因为相量经过优化以将每种声音传送到它们的特定位置。如果其中一位收听者离开他的位置,他将不再能收听到他的信息。</p>\n<p>如果第三个人进入图中的场景并站在靠近扬声器的位置,他将同时听到两条消息的乱声。</p>\n<p>对应于Wi-Fi环境中,扬声器被天线取代,相量控制由信号处理实现,数字消息以特定数据速率生成,这一切都在AP中完成。由于两个消息可以同时传输,理论上可以使聚合数据速率加倍。同样的方法可以用来同时服务更多的客户端,那么上限在哪里呢?实际上,可设置相量的精度存在限制,还有导致“串扰”的反射和其它限制都制约了可获得的吞吐量增益。</p>\n<p>空间位置的重要性决定了MU-MIMO环境中的嗅探更加复杂。请注意,将嗅探器靠近AP将得到我们之前提到的乱码效果。嗅探探头必须放置在靠近被嗅探设备的物理位置,通常每个设备都需要一个嗅探探头。</p>\n<h3 id=\"系统概述和测试基础设施\">系统概述和测试基础设施</h3>\n<p>在这个MU-MIMO测试中,我们使用了octoScope(现在已并入思博伦<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>)STACK-MAX 测试台。在基础设施方面,使用运行IOS-XE 17.6.1(Beta测试版代码)的Catalyst 9800无线局域网控制器和Catalyst 9130接入点。C9130 AP支持最多8×8上行链路和下行链路MU-MIMO,以及8个空间流。Pal-6E支持Wi-Fi 6,最多可仿真256个站点或充当嗅探器探头。</p>\n<p><img src=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture1-4.png\" /></p>\n<p>STApal是基于Intel AX210芯片组的具有完整功能STA<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>,运行在自己的硬件平台上。所有测试室都与外界完全隔离,它们之间的信号路径由全屏蔽衰减器控制,因此可以进行可靠且可重复的测量。腔室内衬射频(RF)吸收泡沫,以显著减少内部反射并防止驻波。</p>\n<p>对于此MU-MIMO测试,我们使用多达4个STA。RF路径将来自C9130 AP的信号连接到各个STA。我们在LOS或IEEE通道模型A模式下使用多路径仿真器 (MPE)。每对天线都馈入一组四个客户端,如下图所示。我们已经看到空间分离是MU-MIMO操作成功的必要条件。通过将天线放置在消声测试室的角落,实现了最佳空间分离。这允许四个独立的MU-MIMO流发送到四组、每组四个的STA。</p>\n<p><img srcset=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture2-1-300x186.png 300w, https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture2-1.png 592w\" sizes=\"(max-width: 592px) 100vw, 592px\" loading=\"lazy\" class=\"aligncenter wp-image-366831 size-full\" src=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture2-1.png\" alt=\"\" width=\"592\" height=\"368\"></p>\n<h3 id=\"实际测试\">实际测试</h3>\n<p>为了演示MU-MIMO增益效果,我们将C9130 AP放置在腔室的中心,并传输下行链路UDP数据到连接到角落天线上的STA。</p>\n<p>首先,我们关闭MU-MIMO并且只开启一个STA。我们注意到吞吐量仅略高于1000 Mbps,略低于1200 Mbps的物理层速率。20秒后,我们引入了另一个STA,看到总吞吐量保持在1000 Mbps,但两个STA共享信道,每个STA达到了500 Mbps。20秒后,我们引入了第三个STA。同样,总吞吐量保持为1000 MBps,三个STA共享信道,每个STA的速度略高于300 Mbps。第四个STA的引入遵循相同的模式,聚合保持不变,每个STA得到250 Mbps。</p>\n<p><img src=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture3.png\" /></p>\n<p>我们重复了这个实验,这次开启了MU-MIMO。</p>\n<p>从一个STA开始,我们实现了熟悉的1000 Mbps。20秒后,我们引入了第二个STA,并观察到聚合速率已增加到2000 Mbps,这明显高于物理层速率。我们还注意到,每个STA的接收速度仍接近之前的1000 Mbps。与之前的STA共享信道的实验不同,在本实验中,它们每个都能够相互独立地充分利用自己的信道。</p>\n<p><img src=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture4.png\" /></p>\n<p>添加第三个STA将聚合速度增加到2200 Mbps。三个STA中的每一个仍接收730 Mbps。添加第四个STA导致总吞吐量2100 Mbps的,每个STA接收525 Mbps,比单用户操作增加了两倍。</p>\n<p>下图总结了测试结果:</p>\n<p><img src=\"https://storage.googleapis.com/blogs-images/ciscoblogs/1/2021/05/Picture5-468x200.png\" /></p>\n<h3 id=\"结论\">结论</h3>\n<p>MU-MIMO利用接收器的空间分离引导独立的消息同时发送到每个接收器。这样可以更有效地使用介质,并提升网络可以实现的聚合数据率。Catalyst 9130 AP的开创性调度程序设计,在多用户传输场景中提供了卓越的吞吐量增益。这是更高MCS速率、低探测开销和高效动态分组调度的结果。</p>\n<p>WLAN上默认启用DL和UL MU-MIMO以及OFDMA。这些功能在现有版本的9800系列无线控制器上可用,但上述增强功能将从 17.6.1版本开始提供。</p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>正交频分多址(英文Orthogonal Frequency Division Multiple Access,缩写OFDMA),是无线通信系统中的一种多重接入技术,其结合频域和时域的多路访问机制非常适合宽带无线网络。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>思博伦通信公司(Spirent Communications plc),一家全球化网络自动化测试和质量保障方案提供商。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>IEEE 802.11 (Wi-Fi) 标准术语,通指运行802.11协议功能的站点(Station,缩写STA)。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["技术小札"],"tags":["思科技术","Wi-Fi"]},{"title":"迪菲—赫尔曼密钥交换是使用和RSA相似的技术吗?","url":"/2020/12/01/DH-and-RSA/","content":"<p>近日在一次研发小组内部的 WPA3 技术介绍会上,演讲者提到加密无线开放网络的 <a href=\"https://tools.ietf.org/html/rfc8110\">OWE</a> 技术基于迪菲—赫尔曼密钥交换,并随口说迪菲—赫尔曼密钥交换是使用和 RSA 相似的技术。这种说法是错误的!<span id=\"more\"></span>虽然迪菲—赫尔曼密钥交换和 RSA 加密算法都属于公钥密码技术,它们的工作机理和应用场景是不同的。作为网络安全相关的研发工程技术人员,有必要清楚地了解两者的工作机制和所基于的数学原理,以及它们之间的区别和联系。</p>\n<div class=\"note success no-icon\"><p><strong>A cryptographic system should be secure even if everything about the system, except the key, is public knowledge.</strong><br> <strong>— <em>Auguste Kerckhoffs</em>(奥古斯特·柯克霍夫,荷兰语言学家和密码学家,提出密码学的“柯克霍夫原则”)</strong></p>\n</div>\n<h2 id=\"迪菲赫尔曼密钥交换\">迪菲—赫尔曼密钥交换</h2>\n<p>迪菲—赫尔曼密钥交换 (Diffie–Hellman key exchange,缩写为DH) 是一种保密通信协议,通过它可以让通信双方在没有任何预知信息的情况下,在不安全的公共信道上交换消息以创建共享密码。这个密码可以用于产生后续双方使用对称加密技术 (如 AES) 通信的密钥。</p>\n<p>这种公钥分配以达成共享秘密的思想最早由斯坦福大学教授马丁·赫尔曼 (Martin Hellman) 的博士研究生瑞夫·墨克 (Ralph Merkle) 提出,而后赫尔曼教授的研究助理惠特菲尔德·迪菲 (Whitfield Diffie) 和赫尔曼教授共同发明了实用的密钥交换协议。迪菲与赫尔曼于1976在 IEEE 信息论会刊上受邀共同发表了论文《<a href=\"https://ieeexplore.ieee.org/document/1055638\">密码学的新方向</a>》,为公开密钥密码学体制奠基,也正式宣告了迪菲—赫尔曼密钥交换这一新技术的诞生。</p>\n<p>迪菲—赫尔曼密钥交换的工作原理,是基于数论中的<a href=\"https://zh.wikipedia.org/wiki/整数模n乘法群\">整数模 <em>n</em> 乘法群</a>及其<a href=\"https://zh.wikipedia.org/wiki/原根\">原根</a>的模幂 (modular exponentiation) 运算。下面以一个简单而具体的例子来描述:</p>\n<ol type=\"1\">\n<li>爱丽丝选定一个素数 <span class=\"math inline\">\\(p=71\\)</span>,再选定整数模 <span class=\"math inline\">\\(p\\)</span> 乘法群的一个原根 <span class=\"math inline\">\\(g=7\\)</span></li>\n<li>爱丽丝选定一个小于 <span class=\"math inline\">\\(p\\)</span> 的随机数 <span class=\"math inline\">\\(a=17\\)</span>,计算 <span class=\"math inline\">\\(A=g^a\\;mod\\;p=7^{17}\\;mod\\;71 = 62\\)</span></li>\n<li>爱丽丝将 <span class=\"math inline\">\\((p,g,A)\\)</span> 一起发给鲍勃</li>\n<li>鲍勃也选定一个小于 <span class=\"math inline\">\\(p\\)</span> 的随机数 <span class=\"math inline\">\\(b=39\\)</span>,计算 <span class=\"math inline\">\\(B=g^b\\;mod\\;p=7^{39}\\;mod\\;71 = 13\\)</span></li>\n<li>鲍勃将 <span class=\"math inline\">\\(B\\)</span> 发回给爱丽丝</li>\n<li>爱丽丝计算 <span class=\"math inline\">\\(s=B^a\\;mod\\;p=13^{17}\\;mod\\;71 = 42\\)</span></li>\n<li>鲍勃计算 <span class=\"math inline\">\\(s=A^b\\;mod\\;p=62^{39}\\;mod\\;71 = 42\\)</span></li>\n</ol>\n<details class=\"note primary\"><summary><p><strong>计算 <span class=\"math inline\">\\(\\color{#93F}{\\bf62^{39}\\;mod\\;71}\\)</span> 很麻烦吗?其实很容易……</strong></p>\n</summary>\n<p>记住模算数有保持基本运算的性质: <span class=\"math display\">\\[(a⋅b)\\;mod\\;m = [(a\\;mod\\;m)⋅(b\\;mod\\;m)]\\;mod\\;m\\]</span> 结合<a href=\"https://zh.wikipedia.org/wiki/平方求幂\">平方求幂</a>原理,可以应用<a href=\"https://zh.wikipedia.org/zh-cn/模幂#从右到左二位算法\">从右到左二进制位算法</a>快速计算: <span class=\"math display\">\\[\\begin{align}\n62^{39}\\;mod\\;71 & = (62^{2^0}⋅62^{2^1}⋅62^{2^2}⋅62^{2^5})\\;mod\\;71\\\\\n& = (62⋅10⋅(62^{2^1}⋅62^{2^1})⋅(62^{2^4}⋅62^{2^4}))\\;mod\\;71\\\\\n& = (62⋅10⋅(10⋅10)⋅(62^{2^3}⋅62^{2^3}⋅62^{2^4}))\\;mod\\;71\\\\\n& = (62⋅10⋅29⋅(29⋅29⋅62^{2^3}⋅62^{2^4}))\\;mod\\;71\\\\\n& = (62⋅10⋅29⋅(60⋅60⋅62^{2^4}))\\;mod\\;71\\\\\n& = (62⋅10⋅29⋅(50⋅50))\\;mod\\;71\\\\\n& = (62⋅10⋅29⋅15)\\;mod\\;71\\\\\n& = 42\n\\end{align}\\]</span></p>\n\n</details>\n<p>如同魔法一般,爱丽丝和鲍勃都得到了同样的 <span class=\"math inline\">\\(s\\)</span> 值 <span class=\"math inline\">\\(42\\)</span>,这就是两个人的共享秘密!此后,爱丽丝和鲍勃就可用 <span class=\"math inline\">\\(s\\)</span> 的哈希值作为对称密钥进行加密通讯,第三者无法知晓。</p>\n<p>为什么会这样?因为乘法群模幂运算的性质,在模 <span class=\"math inline\">\\(p\\)</span> 下 <span class=\"math inline\">\\(g^{ab}\\)</span> 和 <span class=\"math inline\">\\(g^{ba}\\)</span> 相等:</p>\n<p><span class=\"math display\">\\[A^b\\;mod\\;p=g^{ab}\\;mod\\;p=g^{ba}\\;mod\\;p=B^a\\;mod\\;p\\]</span></p>\n<p>所以计算出来的 <span class=\"math inline\">\\(s\\)</span> 值一定相同。当然,真正的应用中会使用大得多的 <span class=\"math inline\">\\(p\\)</span>,否则攻击者可以穷举其所有的余数,去试图破解对称加密的密文。</p>\n<p>注意 <span class=\"math inline\">\\((p,g,A,B)\\)</span> 是公开的,<span class=\"math inline\">\\((a,b,s)\\)</span> 是秘密的。现在假定一个窃听者伊芙可以看到爱丽丝和鲍伯之间的全部消息,那么她可以推导出 <span class=\"math inline\">\\(s\\)</span> 吗?答案是只有当 <span class=\"math inline\">\\((p,a,b)\\)</span> 值很小时才是实际可能的。伊芙首先必须从她知道的 <span class=\"math inline\">\\((p,g,A,B)\\)</span> 倒推出 <span class=\"math inline\">\\((a,b)\\)</span> :</p>\n<ul>\n<li><span class=\"math inline\">\\(A=g^a\\;mod\\;p\\Rightarrow \\color{fuchsia}{a = log_g A\\;mod\\;p}\\)</span></li>\n<li><span class=\"math inline\">\\(B=g^b\\;mod\\;p\\Rightarrow \\color{fuchsia}{b = log_g B\\;mod\\;p}\\)</span></li>\n</ul>\n<p>这就是著名的<strong>离散对数问题</strong>。这是一个公认的计算难题,当前并没有找到多项式时间效率的算法来计算离散对数。所以只要选择了合适的 <span class=\"math inline\">\\((p,a,b)\\)</span>,这个协议被认为是窃听安全的。<a href=\"https://tools.ietf.org/html/rfc3526\">RFC 3526</a> 推荐了6个大素数的模幂 DH 群可供实际应用,其中最小的素数就有1536比特位!</p>\n<p>还要强调的一点是,迪菲-赫尔曼密钥交换本身并不要求通信双方的身份验证,因此它很容易受到中间人攻击。如果攻击者可以在信道的中央窜改两边收发的消息,就可以假扮身份完成两次迪菲-赫尔曼密钥交换。这样攻击者就可以解密全部的信息。因此,通常实际应用中需要加入身份验证机制来防止这类攻击。</p>\n<p>迪菲-赫尔曼密钥交换技术是对现代密码学至关重要的贡献。2015年,在这项发明公布39年后,迪菲和赫尔曼共同荣获被誉为“计算机界的诺贝尔奖” 的 ACM 图灵奖。ACM 的颁奖海报上直接称他们“发明了公钥密码技术”。</p>\n<p><img src=\"acm-turing-2015.jpeg\" /></p>\n<h2 id=\"rsa加密算法\">RSA加密算法</h2>\n<p>RSA 是一种公钥加密算法,以此为核心技术构成的同名公钥加密系统,被广泛应用于保密数据传输。今天,互联网的全面发展已经在社会的各个层面为大众提供极大便利。不论你是在网上冲浪、游戏、娱乐、购物、还是与亲友即时通讯、管理银行账户、投资买卖金融证券,或者只是简单地收发电子邮件,都有 RSA 在幕后运行保障你的隐私和数据安全。</p>\n<p>RSA 实际上是三个人姓氏的缩写,他们是美国密码学家罗纳德·李维斯特 (Ronald <strong>R</strong>ivest)、以色列密码学家阿迪·沙米尔 (Adi <strong>S</strong>hamir) 和美国计算机科学家伦纳德·阿德曼 (Leonard Max <strong>A</strong>dleman) 。1977年,李维斯特、沙米尔和阿德曼三人在麻省理工学院 (MIT) 合作共同发明了 RSA 加密算法。算法最先发布在MIT的公开技术报告里,后来整理发表在1978年二月的 <em>ACM 通讯</em> 杂志,标题为《<a href=\"https://dl.acm.org/doi/10.1145/359340.359342\">一种获取数字签名和公钥密码系统的方法</a>》。</p>\n<p>RSA 的基本思想是使用者创建由一个公钥和一个私钥组成的密钥对。公钥自由发布,私钥必须秘密保存。任何人都可以用公钥来加密消息,而生成的密文只有私钥持有者才能解读。另一方面,以私钥加密的信息,公钥都可以解开。由于我们假定私钥只是特定对象才能持有,所以使用私钥加密相当于生成数字签名,用公钥解密等效于验证签名。</p>\n<p>RSA 加密算法包括四步操作过程:密钥生成、密钥分配、加密和解密。下面也举一个简单而具体的例子来说明:</p>\n<ol type=\"1\">\n<li>爱丽丝随机选择两个素数 <span class=\"math inline\">\\(p=127\\)</span> 和 <span class=\"math inline\">\\(q=5867\\)</span>,计算 <span class=\"math inline\">\\(N=pq=745109\\)</span></li>\n<li>爱丽丝计算 <span class=\"math inline\">\\(N\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/卡邁克爾函數\">卡迈克尔函数</a> <span class=\"math inline\">\\(\\lambda(N)=\\lambda(745109)=52794\\)</span>\n<ul>\n<li>当 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 都为素数时,通常 <span class=\"math inline\">\\(\\lambda(pq)=\\mathrm{lcm}(p − 1, q − 1)\\)</span></li>\n<li><span class=\"math inline\">\\(\\mathrm{lcm}\\)</span> 是求最小公倍数的函数,可以用欧几里得算法得出</li>\n<li><span class=\"math inline\">\\(\\mathrm{lcm}(126,5866)=52794\\)</span></li>\n</ul></li>\n<li>爱丽丝选择一个小于 <span class=\"math inline\">\\(\\lambda(N)\\)</span> 且与之互素的数 <span class=\"math inline\">\\(e=5\\)</span>,并求得 <span class=\"math inline\">\\(e\\)</span> 关于 <span class=\"math inline\">\\(\\lambda(N)\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/模反元素\">模逆元</a> <span class=\"math inline\">\\(d\\equiv e^{-1}\\pmod {\\lambda(N)}\\)</span>,得到 <span class=\"math inline\">\\(d=10559\\)</span>\n<ul>\n<li>模逆元的定义是,找到 <span class=\"math inline\">\\(d\\)</span> 使得 <span class=\"math inline\">\\((d⋅e)\\;\\bmod\\;\\lambda(N)=1\\)</span></li>\n<li><span class=\"math inline\">\\(d=10559\\equiv 5^{-1}\\pmod {52794}\\)</span></li>\n</ul></li>\n<li><span class=\"math inline\">\\(\\pmb{(N,e)}\\)</span> <strong>就是爱丽丝的公钥</strong>,<span class=\"math inline\">\\(\\pmb{(N,d)}\\)</span> <strong>是她的私钥</strong>\n<ul>\n<li>爱丽丝将她的公钥 <span class=\"math inline\">\\((745109,5)\\)</span> 发给鲍勃</li>\n<li>爱丽丝密藏她的私钥 <span class=\"math inline\">\\((745109,10559)\\)</span></li>\n<li>爱丽丝销毁所有 <span class=\"math inline\">\\(p,q,\\lambda(N)\\)</span> 的记录</li>\n</ul></li>\n<li>当鲍勃想给爱丽丝送一个消息 <span class=\"math inline\">\\(M\\)</span> 时,先按照双方约定好的编码格式将 <span class=\"math inline\">\\(M\\)</span> 转化为一个或多个小于 <span class=\"math inline\">\\(N\\)</span> 的正整数 <span class=\"math inline\">\\(m\\)</span>,然后使用爱丽丝的公钥逐个计算出密文 <span class=\"math inline\">\\(c\\)</span>。计算公式是 <span class=\"math inline\">\\(\\pmb{c\\equiv m^e\\pmod N}\\)</span>\n<ul>\n<li>假定 <span class=\"math inline\">\\(M\\)</span> 为“<em>CACC 9678</em>”,编码格式是空格为0、a-z/A-Z(忽略大小写)为1-26、0-9为27-36</li>\n<li>转化后得到正整数串 “030103 030036 333435”,注意每个都小于745109</li>\n<li>加密后的密文整数串 “184539 741303 358095”\n<ul>\n<li><span class=\"math inline\">\\(184539 \\equiv 30103^5\\pmod {745109}\\)</span></li>\n<li><span class=\"math inline\">\\(741303 \\equiv 30036^5\\pmod {745109}\\)</span></li>\n<li><span class=\"math inline\">\\(358095 \\equiv 333435^5\\pmod {745109}\\)</span></li>\n</ul></li>\n</ul></li>\n<li>爱丽丝收到密文整数串后,使用自己的私钥逐个计算出明文 <span class=\"math inline\">\\(m\\)</span>,计算公式是 <span class=\"math inline\">\\(\\pmb{m\\equiv c^d\\pmod N}\\)</span>\n<ul>\n<li><span class=\"math inline\">\\(30103 \\equiv 184539^{10559}\\pmod {745109}\\)</span></li>\n<li><span class=\"math inline\">\\(30036 \\equiv 741303^{10559}\\pmod {745109}\\)</span></li>\n<li><span class=\"math inline\">\\(333435 \\equiv 358095^{10559}\\pmod {745109}\\)</span></li>\n</ul></li>\n</ol>\n<details class=\"note primary\"><summary><p><strong>以上第三步从 <span class=\"math inline\">\\(\\color{#93F}{\\bf(d\\cdot 5)\\;mod\\;52794=1}\\)</span> 算出 <span class=\"math inline\">\\(d\\)</span>,这是怎么做到的?</strong></p>\n</summary>\n<p>应用<a href=\"https://zh.wikipedia.org/zh-cn/扩展欧几里得算法\">扩展欧几里得算法</a>可以快速求解模逆元。参考该网页,根据互素的前提条件,可以写下关系式 (<span class=\"math inline\">\\(gcd\\)</span> 为最大公约数函数):</p>\n<p><span class=\"math display\">\\[52794s+5t=\\mathrm{gcd}(5, 52794)=1\\]</span></p>\n<p>我们要求满足上式的最小正整数 <span class=\"math inline\">\\(t\\)</span>。下表演示算法的迭代过程:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">序号 <span class=\"math inline\">\\(i\\)</span></th>\n<th style=\"text-align: left;\">商 <span class=\"math inline\">\\(q_{i-1}\\)</span></th>\n<th style=\"text-align: left;\">余数 <span class=\"math inline\">\\(r_i\\)</span></th>\n<th style=\"text-align: left;\"><span class=\"math inline\">\\(s_i\\)</span></th>\n<th style=\"text-align: left;\"><span class=\"math inline\">\\(t_i\\)</span></th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: left;\"></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(52794\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(1\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(0\\)</span></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: left;\"></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(5\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(0\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(1\\)</span></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(52794 \\div5 = 10558\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(4\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(1 - 10558\\times 0 = 1\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(0 - 10558\\times 1 = -10558\\)</span></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">3</td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(5 \\div4 = 1\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(1\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(0-1\\times1 = -1\\)</span></td>\n<td style=\"text-align: left;\"><span class=\"math inline\">\\(1 - 1\\times (-10558) = \\bf10559\\)</span></td>\n</tr>\n</tbody>\n</table>\n<p>只需要两步迭代就得到余数<span class=\"math inline\">\\(1\\)</span>,算法结束。最后的 <span class=\"math inline\">\\(t\\)</span> 就是我们要的 <span class=\"math inline\">\\(5^{-1}\\pmod {52794}\\)</span>。</p>\n\n</details>\n<p>解码后串起来得到同样的信息“<em>CACC 9678</em>”。为什么爱丽丝解密后的消息会与鲍勃发送的完全一致呢?原因就在模幂运算里。首先因为 <span class=\"math inline\">\\(c\\equiv m^e\\pmod N\\)</span>,可以得到 <span class=\"math inline\">\\(c^d\\equiv (m^e)^d \\equiv m^{ed} \\pmod N\\)</span>。由于 <span class=\"math inline\">\\((d⋅e)\\;mod\\;\\lambda(N)=1\\)</span>,推导出 <span class=\"math inline\">\\(ed = 1 + h\\lambda(N)\\)</span> (<span class=\"math inline\">\\(h\\)</span> 为非负整数)。综合两式</p>\n<p><span class=\"math display\">\\[\\Rightarrow m^{ed} = m^{(1+h\\lambda(N))} = \\color{fuchsia}{m(m^{\\lambda(N)})^h \\equiv m(1)^h}\\equiv m\\pmod N\\]</span></p>\n<p>以上倒数第二个同余等式 (符号 <span class=\"math inline\">\\(\\equiv\\)</span>) 的依据是<a href=\"https://zh.wikipedia.org/zh-cn/欧拉定理_(数论)\">欧拉定理</a>。这样就证明了解密公式 <span class=\"math inline\">\\({m\\equiv c^d\\pmod N}\\)</span> 的正确性!还可以看到,<span class=\"math inline\">\\(e\\)</span> 和 <span class=\"math inline\">\\(d\\)</span> 的次序对于 <span class=\"math inline\">\\(m^{ed}\\;mod\\;N\\)</span> 的结果无关,所以爱丽丝用私钥加密的消息,鲍勃可以拿爱丽丝的公钥解开。这也证明了数字签名的可行性。</p>\n<p>安全性方面,如果第三方能从爱丽丝的公钥 <span class=\"math inline\">\\((N,e)\\)</span> 推算出 <span class=\"math inline\">\\(d\\)</span>,那就破解了这一算法。但是破解的前提是先要从 <span class=\"math inline\">\\(N\\)</span> 里面分离出 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,这在 <span class=\"math inline\">\\(N\\)</span> 很大时是非常困难的。实际上,这就是著名的<a href=\"https://zh.wikipedia.org/zh-cn/整数分解\"><strong>大数素因数分解问题</strong></a>,另一个公认的计算难题。迄今为止,“已知最好的算法比指数数量级时间要快,比多项式数量级时间要慢”。<a href=\"https://en.wikipedia.org/wiki/RSA_Factoring_Challenge\">RSA 大数分解挑战</a> 网站公布的最新纪录,是2020二月破解了 <a href=\"https://en.wikipedia.org/wiki/RSA_numbers#RSA-250\">RSA-250</a>,一个829比特的大数。这一进展表明1024比特 <span class=\"math inline\">\\(N\\)</span> 值公钥的安全性已经岌岌可危。有鉴于此,美国国家标准技术研究所 (National Institute of Standards and Technology,简写为NIST) 建议实际应用中的 RSA 密钥长度不少于2048比特。</p>\n<p>另一方面,虽然公钥不需要保密传送,但却要求可靠地分配。否则,伊芙可以假扮爱丽丝,将自己的公钥发给鲍勃。如果鲍勃信以为真,伊芙就可以拦截所有鲍勃传递给爱丽丝的消息,用她自己的私钥解密。伊芙再将这个消息用爱丽丝的公钥加密后传给她。爱丽丝和鲍勃无法发现这样的中间人攻击。解决这一问题的方案,是建立可信赖的第三方机构签发证书来确保公钥的可靠性。这就是公钥基础架构 (Public Key Infrastructure,缩写 PKI) 的由来。</p>\n<p>RSA 公钥加密算法,是三位密码学家和计算机科学家的天才创造。它的发明是公钥密码技术新的里程碑,也成为现代互联网安全通信的基石。李维斯特、沙米尔和阿德曼的杰出贡献,为他们赢得了2002 年的 ACM 图灵奖,比迪菲和赫尔曼早了足足13年!</p>\n<p><img src=\"acm-turing-2002.jpeg\" /></p>\n<h2 id=\"区别与联系\">区别与联系</h2>\n<p>下表总结了迪菲-赫尔曼密钥交换与 RSA 公钥加密算法的对比:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">密码技术</th>\n<th style=\"text-align: center;\">迪菲-赫尔曼密钥交换</th>\n<th style=\"text-align: center;\">RSA加密算法</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">技术类型</td>\n<td style=\"text-align: center;\">非对称,公钥技术</td>\n<td style=\"text-align: center;\">非对称,公钥技术</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">数学原理</td>\n<td style=\"text-align: center;\">整数模 <span class=\"math inline\">\\(n\\)</span> 乘法群,原根</td>\n<td style=\"text-align: center;\">卡迈克尔函数,模逆元,欧拉定理</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">数学运算</td>\n<td style=\"text-align: center;\">模幂,平方求幂</td>\n<td style=\"text-align: center;\">模幂,平方求幂,扩展欧几里得算法</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">公开密钥</td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\((p,g,A,B)\\)</span></td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\((N,e)\\)</span></td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">私有密钥</td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\((a,b,s)\\)</span></td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\((N,d)\\)</span></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">安全保障</td>\n<td style=\"text-align: center;\">离散对数难题</td>\n<td style=\"text-align: center;\">大数素因数分解难题</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">典型应用</td>\n<td style=\"text-align: center;\">密钥交换</td>\n<td style=\"text-align: center;\">加密/解密,数字签名</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">密钥长度</td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\(\\ge2048\\)</span> 比特</td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\(\\ge2048\\)</span> 比特</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">身份验证</td>\n<td style=\"text-align: center;\">需要外部支持</td>\n<td style=\"text-align: center;\">需要 PKI 支持公钥分配</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">前向保密</td>\n<td style=\"text-align: center;\">支持</td>\n<td style=\"text-align: center;\">不支持</td>\n</tr>\n</tbody>\n</table>\n<p>可以看到,两者都属于非对称的公钥技术,都有一个公钥和私钥密钥对。它们都用到了模幂和平方求幂数学运算,RSA 公钥加密算法还需要应用扩展欧几里得算法求解模逆元。尽管有这些相似点,它们所基于数学原理是不同的,其安全性对应的计算难题也是性质相异的。这些特质决定了迪菲-赫尔曼密钥交换可用于密钥交换,但是不能用来加密/解密;而 RSA 公钥加密算法既可以加密/解密,又能支持数字签名。所以,综合来看二者使用相似的技术的说法不能成立。</p>\n<div class=\"note info\"><p>基于迪菲-赫尔曼密钥交换演化出来的 ElGamal 加密算法,可以用来加密/解密消息,但是由于一些历史原因以及 RSA 公钥加密算法的巨大商业成功,ElGamal 加密算法并不流行。</p>\n</div>\n<p>在现代密码学中,密钥长度定义为加密算法使用的密钥的比特数。理论上,因为所有的算法都可能会被暴力破解,所以密钥长度确定了一个加密算法的安全性上限。密码分析学研究表明,迪菲-赫尔曼密钥交换和 RSA 公钥加密算法的密钥强度大致相同。破解离散对数和分解大数素因子的计算强度是可比拟的。因此,在实际应用中对这两种密码技术的推荐密钥长度都是至少2048比特。</p>\n<p>对于身份验证,迪菲-赫尔曼密钥交换需要外部支持,否则无法抗击中间人攻击。RSA 公钥加密算法虽然可以用于验证数字签名,但前提是有 PKI 支持可靠的公钥分配。当前 PKI 的体系已经相当成熟,有专门的证书认证机构 (Certificate Authority,缩写 CA) 承担公钥体系中公钥合法性检验的责任,发放和管理 X.509 格式的公钥数字证书。</p>\n<p>RSA 公钥加密算法在实际应用中存在一个问题,即它没有<a href=\"https://zh.wikipedia.org/wiki/前向保密\">前向保密</a>的功能。前向保密 (Forward Secrecy),有时也被称为完全前向保密 (Perfect Forward Secrecy),是保密通信协议的一种安全属性,指的是长期使用的主密钥泄漏不会导致过去的会话信息泄漏。如果系统具有前向保密性,就可以保护在私钥泄露时历史通信纪录的安全。设想一下这样的情况,虽然伊芙无法破解爱丽丝与鲍勃之间用 RSA 加密的消息,伊芙可以存档全部的过往消息密文。未来某一天,爱丽丝的私钥因为某种原因被泄漏,那么伊芙就可以解密所有的消息记录。</p>\n<p>解决这个问题的办法,就是迪菲-赫尔曼密钥交换!记得迪菲-赫尔曼密钥交换的公钥里的 <span class=\"math inline\">\\((A,B)\\)</span> 由双方从各自的私钥 <span class=\"math inline\">\\((a,b)\\)</span> 生成,那么如果每次会话时都产生随机的 <span class=\"math inline\">\\((a,b)\\)</span> 值,未来的密钥泄漏并不会破解之前的会话密钥。这说明迪菲-赫尔曼密钥交换是支持前向保密的!如果我们结合迪菲-赫尔曼密钥交换的前向保密性与 RSA 公钥加密算法的数字签名功能,就可以实现带有身份验证保护的密钥交换。这一过程可以简化示例如下:</p>\n<ol type=\"1\">\n<li>爱丽丝与鲍勃双方交换经过认证的 RSA 公钥证书</li>\n<li>爱丽丝与鲍勃各自产生随机的 <span class=\"math inline\">\\((a,b)\\)</span> 值,用共享的迪菲-赫尔曼 <span class=\"math inline\">\\((p,g)\\)</span> 计算出 <span class=\"math inline\">\\((A,B)\\)</span></li>\n<li>爱丽丝用自己的 RSA 私钥加密 <span class=\"math inline\">\\(A\\)</span> 生成数字签名,将之与 <span class=\"math inline\">\\(A\\)</span> 一起发给鲍勃</li>\n<li>鲍勃用自己的 RSA 私钥加密 <span class=\"math inline\">\\(B\\)</span> 生成数字签名,将之与 <span class=\"math inline\">\\(B\\)</span> 一起发给爱丽丝</li>\n<li>爱丽丝用鲍勃的 RSA 公钥验证签名,确认 <span class=\"math inline\">\\(B\\)</span> 来自鲍勃,用 <span class=\"math inline\">\\((p,a,B)\\)</span> 算出 <span class=\"math inline\">\\(s\\)</span></li>\n<li>鲍勃用爱丽丝的 RSA 公钥验证签名,确认 <span class=\"math inline\">\\(A\\)</span> 来自爱丽丝,用 <span class=\"math inline\">\\((p,b,A)\\)</span> 算出 <span class=\"math inline\">\\(s\\)</span></li>\n<li>爱丽丝和鲍勃达成共享秘密,生成后续对称加密 (AES) 的会话密钥,进行保密通信</li>\n</ol>\n<p>这里 RSA 数字签名保障了密钥交换不受中间人攻击。另外以上的第二步中,如果每次会话都产生新的随机数,那么即使有一天爱丽丝或鲍勃的RSA 私钥泄漏,也不会威胁以前会话的安全性,因为窃密者还是必须要去求解离散对数的难题。我们也实现了前向保密。实际上,这就是无处不在的传输层安全性协议 (Transport Layer Security,缩写 TLS) 所定义的 DHE-RSA 密码套件的工作机理。</p>\n<h2 id=\"dhe-rsa加密套件\">DHE-RSA加密套件</h2>\n<p>传输层安全性协议 (TLS) 及其前身安全套接层协议 (Secure Sockets Layer,缩写 SSL) 是一种为互联网通信提供安全及数据完整性保障的安全协议。TLS 广泛使用在浏览器、电子邮件、即时通信、VoIP、虚拟专用网 (VPN) 等应用程序中,已成为事实上的互联网保密通信工业标准。目前 <a href=\"https://tools.ietf.org/html/rfc5246\">TLS 1.2</a> 是得到普遍支持的协议版本,支持建立于 TCP 之上的安全连接 。针对 UDP 的应用也定义了数据报传输层安全性协议 (Datagram Transport Layer Security,缩写 DTLS)。DTLS 与 TLS 大同小异,主要在可靠性和安全性方面为无连接的 UDP 传输做了一些扩展。<a href=\"https://tools.ietf.org/html/rfc6347\">DTLS 1.2</a> 与 TLS 1.2 功能相匹配。</p>\n<p>TLS 协议采用主从式(客户机/服务器)架构模型。它的工作模式,是使用 X.509 认证和非对称加密算法对通信方做身份认证,之后交换密钥生成对称加密的会话密钥。这个会话密钥就用来加密通信双方交换的数据,保证信息的保密性和可靠性,不必担心被第三方攻击或窃听。为了标识方便,TLS 1.2 协议将使用的<em>身份验证、密钥交换、批量加密和消息认证码算法</em> 组合成<strong>密码套件 (Cipher Suite) </strong>名称。每个密码套件被赋予一个双字节的编码。<a href=\"https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4\">TLS 密码套件注册表</a>提供了全部登记在录的密码套件命名参考表,参考表以编码值从小到大排序。</p>\n<div class=\"note info\"><p>由于非对称加密算法 (RSA 等) 计算强度远远高于对称加密算法 (AES 等),从性能上考虑实际应用几乎总是使用对称加密算法批量加密消息。</p>\n</div>\n<p>TLS 1.2 协议支持一系列组合迪菲-赫尔曼密钥交换与 RSA 公钥加密算法的密码套件。它们都以 TLS_DH_RSA 或 TLS_DHE_RSA 作为开头。DHE 中的 “E” 代表 “Ephemeral” (临时的),其意义是要求每次会话都要产生随机的 <span class=\"math inline\">\\((a,b)\\)</span> 值。所以 TLS_DHE_RSA 密码套件能提供前向保密,而 TLS_DH_RSA 不可以,实际应用中应该优先选择前者。</p>\n<p>这里以典型的 TLS_DHE_RSA_WITH_AES_128_CBC_SHA (编码 0x00,0x33) 密码套件为例,解析迪菲-赫尔曼与 RSA 协同工作建立 DTLS 会话的过程。首先解释一下密码套件的构成:</p>\n<ul>\n<li>DHE:临时 DH 实现密钥交换</li>\n<li>RSA:签名认证 DHE 的公钥</li>\n<li>AES_128_CBC:128比特密码分组链接模式 AES 加密</li>\n<li>SHA:160比特 HMAC-SHA1 散列消息认证码</li>\n</ul>\n<p>参考从网络端口截取的数据包文件 <a href=\"dtls-dhe-rsa.pcap\">dtls-dhe-rsa.pcap</a>,可得到以下握手协议消息时序图:</p>\n<pre class=\"mermaid\">\nsequenceDiagram\n\nautonumber\nparticipant C as 客户机 (Client)\nparticipant S as 服务器 (Server)\nNote over C,S: 握手协议\nrect rgb(230, 250, 255)\nC->>S: Client Hello (Cr, Cipher Suites))\nS-->>C: Hello Verify Request (Cookie)\nC->>S: Client Hello (Cr, Cookie, Cipher Suites)\nS-->>C: Server Hello (Sr, Cipher Suite), Certificate (Sn, Se)\nS-->>C: Server Key Exchange (p,g,A,Ss)\nS-->>C: Certificate Request, Server Hello Done\nC->>S: Certificate (Cn, Ce)\nC->>S: Client Key Exchange (B)\nC->>S: Certificate Verify (Cs)\nend\nNote over C,S: 加密通道建立\nrect rgb(239, 252, 202)\nC->>S: Change Cipher Spec, Encrypted Handshake Message\nS-->>C: Change Cipher Spec, Encrypted Handshake Message\nC->>S: Application Data\nS-->>C: Application Data\nend\n \n</pre>\n<p>参考时序图中数据包编号解析如下:</p>\n<ul>\n<li>数据包 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{1}-\\enclose{circle}{3}\\)</span> 实现初始握手信息交换:\n<ul>\n<li>客户机先发出问候消息,消息包含随机数 <span class=\"math inline\">\\(C_r\\)</span> 和所支持的密码套件列表</li>\n<li>服务器回应一个问候验证请求消息,消息包含一个信息块 (Cookie)</li>\n<li>客户机收到验证请求后重发问候消息,包括上次的全部内容外加复制的信息块</li>\n</ul></li>\n</ul>\n<div class=\"note info\"><p>问候验证是 DTLS 特有的,目的是防止拒绝服务攻击。协议规定,服务器只有在收到包含复制的信息块的问候消息后,才会继续为该客户机提供服务。</p>\n</div>\n<ul>\n<li>数据包 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{4}-\\enclose{circle}{6}\\)</span> 显示服务器进入验证和密钥交换阶段:\n<ul>\n<li>服务器先发出问候消息回应,消息包含了随机数 <span class=\"math inline\">\\(S_r\\)</span> 和所选定的密码套件\n<ul>\n<li>如下图所示,服务器选择了 TLS_DHE_RSA_WITH_AES_128_CB_SHA<img src=\"dtls-server-hello.png\" /></li>\n</ul></li>\n<li>同一数据包还包含了服务器证书消息,证书一般较大,会分成多个分片 (fragment)</li>\n<li>服务器证书提供了可验证其签名的 RSA 公钥 <span class=\"math inline\">\\((S_N,\\;S_e)\\)</span></li>\n<li>接下来服务器发出密钥交换消息,消息包含了其 DH 公钥 <span class=\"math inline\">\\((p,g,A)\\)</span> 和签名<span class=\"math inline\">\\(Ss\\)</span>\n<ul>\n<li>下图中 <span class=\"math inline\">\\(p\\)</span> 的长度256字节,说明密钥长度为2048比特,<span class=\"math inline\">\\(Pubkey\\)</span> 就是 <span class=\"math inline\">\\(A\\)</span></li>\n<li>图中也可看到签名选用算法是 SHA512 和 RSA,</li>\n<li>操作是先计算 <span class=\"math inline\">\\(\\operatorname{SHA512}(Cr,Sr,p,g,A)\\)</span>,再用服务器 RSA 私钥加密<img src=\"dtls-server-key.png\" /></li>\n</ul></li>\n<li>之后是服务器发的证书请求消息和问候结束消息\n<ul>\n<li>服务器请求客户机发送可验证其签名的 RSA 公钥证书</li>\n</ul></li>\n</ul></li>\n</ul>\n<div class=\"note warning\"><p><strong>注意:</strong>如果是使用 DH-RSA 密码套件,则服务器端的 DH 公钥参数 <span class=\"math inline\">\\((p,g,A)\\)</span> 都是不变的,会直接包含在其证书消息中, 这时服务器不会发出密钥交换消息<span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{5}\\)</span>。对于 DHE-RSA,每次会话的 <span class=\"math inline\">\\(A\\)</span> 值都不一样。</p>\n</div>\n<ul>\n<li>数据包 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{7}-\\enclose{circle}{9}\\)</span> 显示客户机进入验证和密钥交换阶段:\n<ul>\n<li>客户机先发出证书消息,证书包括 RSA 公钥 <span class=\"math inline\">\\((C_N,\\;C_e)\\)</span>,也会分成多个分片</li>\n<li>客户机再发出密钥交换消息,消息包含了其 DH 公钥 <span class=\"math inline\">\\(B\\)</span>\n<ul>\n<li>下图中的<span class=\"math inline\">\\(Pubkey\\)</span> 就是 <span class=\"math inline\">\\(B\\)</span><img src=\"dtls-client-key.png\" /></li>\n</ul></li>\n<li>客户机最后发出证书验证消息,消息包含了签名 <span class=\"math inline\">\\(Cs\\)</span>\n<ul>\n<li>签名覆盖除最开始的客户机问候 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{1}\\)</span> 及问候验证请求 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{2}\\)</span> 之外的全部过往消息</li>\n<li>签名操作同样是先计算 SHA512,再用客户机 RSA 私钥加密</li>\n</ul></li>\n</ul></li>\n<li>数据包 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{10}-\\enclose{circle}{11}\\)</span> 完成握手并建立加密通道:\n<ul>\n<li>双方先各自验证对方发过来的签名</li>\n<li>验证成功后运行 DH 算法生成同样的预备主密钥 (pre_master_secret)</li>\n<li>双方调用<a href=\"https://tools.ietf.org/html/rfc5246#page-14\">伪随机函数(PRF)</a>从预备主密钥生成48字节主密钥 (master_secret): <span class=\"math display\">\\[master\\_secret = \\operatorname{PRF}(pre\\_master\\_secret,\\unicode{x201C}master\\;secret\\unicode{x201D},Cr+Sr)[0..47]\\]</span></li>\n<li>双方再次调用 PRF 从主密钥生成72字节密钥块 (key_block): <span class=\"math display\">\\[key\\_block = \\operatorname{PRF}(master\\_secret,\\unicode{x201C}key\\;expansion\\unicode{x201D},Sr+Cr)[0..71]\\]</span></li>\n<li>密钥块分配给 HMAC-SHA1 和 AES_128_CBC 功能模块:\n<ul>\n<li>客户机写消息验证码 (MAC) 密钥:20字节</li>\n<li>服务器写消息验证码 (MAC) 密钥:20字节</li>\n<li>客户机写加密密钥:16字节</li>\n<li>服务器写加密密钥:16字节</li>\n</ul>\n注意TLS/DTLS 1.2规定此密码套件使用显式初始向量 (IV),不需要分配密钥块</li>\n<li>客户机产生更新密码规范 (Change Cipher Spec) 消息,表明开始使用加密和 MAC 模块</li>\n<li>客户机第三次调用 PRF 生成用于主密钥和握手消息验证的12字节握手结束验证码,验证码打包成握手结束消息,输入到加密和 MAC 模块: <span class=\"math display\">\\[\\operatorname{PRF}(master\\_secret,finished\\_label,\\operatorname{SHA256}(handshake\\_messages))[0..11]\\]</span></li>\n<li>客户机发送更新密码规范消息和加密后的握手结束消息到服务器</li>\n<li>服务器验证收到的客户机握手结束消息后,重复上面三步,生成自己的更新密码规范消息和加密后的握手结束消息,发送给客户机</li>\n<li>客户机验证收到的服务器握手结束消息完成握手,加密通道建成</li>\n</ul></li>\n<li>数据包 <span class=\"math inline\">\\(\\require{enclose}\\enclose{circle}{12}-\\enclose{circle}{13}\\)</span> 显示加密的应用数据交换正式开始</li>\n</ul>\n<p>这就是使用 TLS_DHE_RSA_WITH_AES_128_CBC_SHA 密码套件建立安全信息通道的完整过程。DHE 实现了有前向保密保护的密钥交换,而 RSA 数字签名为 DHE 提供了身份验证功能,二者结合打造了一种安全通信的解决方案。清楚了解了这些之后,我们就会更好地掌握迪菲-赫尔曼与 RSA 的工作机理,有效应用于实际工作中并避免不必要的失误。</p>\n","categories":["学习体会"],"tags":["密码学","网络安全"]},{"title":"Endianness一点通","url":"/2020/11/24/Endianness/","content":"<p>Endianness 的问题实质就是关于计算机如何存储大的数值的问题。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>I do not fear computers. I fear lack of them.</strong><br> <strong>— <em>Isaac Asimov</em>(艾萨克·阿西莫夫,美籍犹太裔生化学家、科幻和科普作家)</strong></p>\n</div>\n<p>我们知道一个基本存储单元可以保存一个字节,每个存储单元对应一个地址。对于大于十进制 255(16进制 0xff)的整数,需要多个存储单元。例如,4660 对应于 0x1234,需要两个字节。不同的计算机系统使用不同的方法保存这两个字节。在我们常用的 PC 机中,低位的字节 0x34 保存在低地址的存储单元,高位的字节 0x12 保存在高地址的存储单元;而在 Sun 工作站中,情况恰恰相反,0x34 位于高地址的存储单元,0x12 位于低地址的存储单元。前一种就被称为<code>Little Endian</code>,后一种就是<code>Big Endian</code>。</p>\n<p>如何记住这两种存储模式?其实很简单。首先记住我们所说的存储单元的地址总是由低到高排列。对于多字节的数值,如果先见到的是低位的字节,则系统就是<code>Little Endian</code>的,Little 就是\"小,少\"的意思,也就对应\"低\"。相反就是<code>Big Endian</code>,这里 Big \"大\"对应\"高\"。</p>\n<h2 id=\"程序实例\">程序实例</h2>\n<p>为了加深对 Endianness 的理解,让我们来看下面的C程序例子:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">char</span> a = <span class=\"number\">1</span>; \t \t \t </span><br><span class=\"line\"><span class=\"keyword\">char</span> b = <span class=\"number\">2</span>; </span><br><span class=\"line\"><span class=\"keyword\">short</span> c = <span class=\"number\">255</span>;\t<span class=\"comment\">/* 0x00ff */</span></span><br><span class=\"line\"><span class=\"keyword\">long</span> d = <span class=\"number\">0x44332211</span>;</span><br></pre></td></tr></table></figure>\n<p>在基于Intel 80x86的系统上, 变量a,b,c,d对应的内存映像如下表所示:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th>地址偏移量</th>\n<th>内存映像</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td>0x0000</td>\n<td>01 02 FF 00</td>\n</tr>\n<tr class=\"even\">\n<td>0x0004</td>\n<td>11 22 33 44</td>\n</tr>\n</tbody>\n</table>\n<p>显然我们可以马上判定这一系统是<code>Little Endian</code>的。对于16位的整形数<code>short c</code>,我们先见到其低位的0xff,下一个才是 0x00。同样对于32位长整形数<code>long d</code>,在最低的地址 0x0004 存的是最低位字节 0x11。如果是在<code>Big Endian</code>的计算机中,则地址偏移量从 0x0000 到 0x0007 的整个内存映像将为:<em>01 02 00 FF 44 33 22 11</em>。</p>\n<p>所有计算机处理器都必须在这两种 Endian 间作出选择。但某些处理器(如 ARM, MIPS 和 IA-64)支持两种模式,可由编程者通过软件或硬件设置一种 Endian。以下是一个处理器类型与对应的 Endian 的简表:</p>\n<ul>\n<li>纯<code>Big Endian</code>: Sun SPARC, Motorola 68000,Java 虚拟机</li>\n<li>Bi-Endian, 运行<code>Big Endian</code>模式: MIPS 运行 IRIX, PA-RISC,大多数 Power 和 PowerPC 系统</li>\n<li>Bi-Endian, 运行<code>Little Endian</code>模式: ARM, MIPS 运行 Ultrix,大多数 DEC Alpha, IA-64 运行 Linux</li>\n<li><code>Little Endian</code>: Intel x86,AMD64,DEC VAX</li>\n</ul>\n<p>如何在程序中检测本系统的 Endianess?可调用下面的函数来快速验证,如果返回值为1,则为<code>Little Endian</code>;为0则是<code>Big Endian</code>:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">test_endian</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> x = <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> *((<span class=\"keyword\">char</span> *)&x);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h2 id=\"网络序\">网络序</h2>\n<p>Endianness 对于网络通信也很重要。试想当<code>Little Endian</code>系统与<code>Big Endian</code>的系统通信时,如果不做适当处理,接收方与发送方对数据的解释将完全不一样。比如对以上 C 程序段中的变量d,<code>Little Endian</code>发送方发出 <em>11 22 33 44</em> 四个字节,<code>Big Endian</code>接收方将其转换为数值 0x11223344。这与原始的数值大相径庭。为了解决这个问题,TCP/IP 协议规定了专门的“网络字节次序”(简称“网络序”),即无论计算机系统支持何种 Endian,在传输数据时,总是数值最高位的字节最先发送。从定义可以看出,网络序其实是对应<code>Big Endian</code>的。</p>\n<p>为了避免因为 Endianness 造成的通信问题,及便于软件开发者编写易于平台移植的程序,特别定义了一些C语言预处理的宏来实现网络字节与本机字节次序之间的相互转换。<code>htons()</code>和<code>htonl()</code>用来将本机字节次序转成网络字节次序,前者应用于16位无符号数,后者应用于32位无符号数。<code>ntohs()</code>和<code>ntohl()</code>实现反方向的转换。这四个宏的原型定义可参考如下(Linux 系统中可在<code>netinet/in.h</code>文件里找到):</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">if</span> defined(BIG_ENDIAN) && !defined(LITTLE_ENDIAN)</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> htons(A) (A)</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> htonl(A) (A)</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> ntohs(A) (A)</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> ntohl(A) (A)</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">elif</span> defined(LITTLE_ENDIAN) && !defined(BIG_ENDIAN)</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> htons(A) ((((uint16)(A) & 0xff00) >> 8) | \\</span></span><br><span class=\"line\"><span class=\"meta\"> (((uint16)(A) & 0x00ff) << 8))</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> htonl(A) ((((uint32)(A) & 0xff000000) >> 24) | \\</span></span><br><span class=\"line\"><span class=\"meta\"> (((uint32)(A) & 0x00ff0000) >> 8) | \\</span></span><br><span class=\"line\"><span class=\"meta\"> (((uint32)(A) & 0x0000ff00) << 8) | \\</span></span><br><span class=\"line\"><span class=\"meta\"> (((uint32)(A) & 0x000000ff) << 24))</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> ntohs htons</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> ntohl htohl</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">else</span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">error</span> <span class=\"meta-string\">"Either BIG_ENDIAN or LITTLE_ENDIAN must be #defined, but not both."</span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br></pre></td></tr></table></figure>\n","categories":["学习体会"],"tags":["C/C++编程","系统编程","计算机体系结构","网络通信"]},{"title":"费马小定理的归纳法证明和应用","url":"/2021/02/14/Fermats-Little-Theorem/","content":"<p>在数学的发展史上,皮埃尔·德·费马(Pierre de Fermat)是一位特别的人物。他的正式职业是律师,却格外爱好数学。虽然是业余的,费马在数学上的成就不低于同时代的职业数学家。他对于现代微积分、解析几何、概率论和数论都有贡献。尤其是在数论领域,费马最有兴趣也成果最突出。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Logic is the foundation of the certainty of all the knowledge we acquire.</strong><br> <strong>— <em>Leonhard Euler</em>(莱昂哈德·欧拉,瑞士数学家和物理学家,近代数学先驱之一)</strong></p>\n</div>\n<p>作为“业余数学家之王”,费马提出了一些著名的数论上的论断,但却没有给出强有力的证明。最著名的莫过于<strong>费马大定理</strong><a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>。尽管费马声称他已找到一个精妙的证明,只是页边没有足够的空位写下来,但实际上历经350多年数学家们的不懈努力,直至1995年才由英国数学家安德鲁·怀尔斯(Andrew John Wiles)和其学生理查·泰勒(Richard Taylor)发布广为认可的证明。</p>\n<figure>\n<img src=\"Fermat-on-stamp.jpg\" alt=\"邮票上的费马和费马大定理\" /><figcaption aria-hidden=\"true\">邮票上的费马和费马大定理</figcaption>\n</figure>\n<p>与之相区别地,还有一个<strong>费马小定理</strong>。1640年10月,费马在一封给友人的信中第一次写下了与下面同义的文字:</p>\n<blockquote>\n<p><em>如果 <span class=\"math inline\">\\(p\\)</span> 是素数且 <span class=\"math inline\">\\(a\\)</span> 是任意不可被 <span class=\"math inline\">\\(p\\)</span> 整除的整数, 那么 <span class=\"math inline\">\\(a^{p−1}−1\\)</span> 可被 <span class=\"math inline\">\\(p\\)</span>整除。</em></p>\n</blockquote>\n<p>同样地,费马也没有在信中给出证明。将近百年后,完整的证明第一次由大数学家欧拉于1736年公开发表。而后来,人们从另一位大数学家莱布尼茨未发表的手稿中,发现他在1683年以前已经得到几乎是相同的证明。</p>\n<p>费马小定理是初等数论的一个基本结论。这一定理可以用来生成素数判定法则和相应验证算法。进入20世纪七十年代末,公钥密码学兴起,费马小定理辅助证明了RSA加密算法的正确性。之后,研究者将其与中国余数定理相结合,还发现了一种RSA快速解密方法。以下对这些作一些展开介绍。</p>\n<h3 id=\"定理和推论\">定理和推论</h3>\n<p>费马小定理的完整表述是:<strong>设定 <span class=\"math inline\">\\(\\pmb{p}\\)</span> 为素数,对于任意整数 <span class=\"math inline\">\\(\\pmb{a}\\)</span>,<span class=\"math inline\">\\(\\pmb{a^p−a}\\)</span> 是 <span class=\"math inline\">\\(\\pmb{p}\\)</span> 的倍数,用模算数等式表示为 <span class=\"math inline\">\\(\\pmb{a^p\\equiv a\\pmod p}\\)</span>,如果 <span class=\"math inline\">\\(\\pmb{a}\\)</span> 不是 <span class=\"math inline\">\\(\\pmb{p}\\)</span> 的倍数,则有 <span class=\"math inline\">\\(\\pmb{a^{p-1}\\equiv 1\\pmod p}\\)</span>。</strong></p>\n<p>从 <span class=\"math inline\">\\(a^{p-1}\\equiv 1\\pmod p\\)</span> 可以推导出 <strong><span class=\"math inline\">\\(\\pmb{a^{p-2}\\equiv a^{-1}\\pmod p}\\)</span></strong>,这个新的同余等式正好给出了求 <span class=\"math inline\">\\(a\\)</span> 对同余 <span class=\"math inline\">\\(p\\)</span> 之模逆元的一种方法。这是费马小定理的一个直接推论。</p>\n<p>另一个重要的推论是:<strong>如果 <span class=\"math inline\">\\(\\pmb{a}\\)</span> 不是 <span class=\"math inline\">\\(\\pmb{p}\\)</span> 的倍数且 <span class=\"math inline\">\\(\\pmb{n=m\\bmod {(p-1)}}\\)</span>,那么 <span class=\"math inline\">\\(\\pmb{a^n\\equiv a^m\\pmod p}\\)</span>。</strong>这一推论看起来不是很直观,但其实证明很简单:</p>\n<ol type=\"1\">\n<li>因为 <span class=\"math inline\">\\(n=m\\bmod {(p-1)}\\)</span>,得出 <span class=\"math inline\">\\(m = k⋅(p-1)+n\\)</span></li>\n<li>将第一步的结果代入幂运算,<span class=\"math inline\">\\(a^m=a^{k⋅(p-1)+n}=(a^{(p-1)})^k⋅a^n\\)</span></li>\n<li>再代入模幂运算,并依据费马小定理,<span class=\"math inline\">\\(a^m=(a^{(p-1)})^k⋅a^n\\equiv (1)^ka^n\\equiv a^n\\pmod p\\)</span></li>\n<li>所以 <span class=\"math inline\">\\(a^n\\equiv a^m\\pmod p\\)</span>,证毕。</li>\n</ol>\n<h3 id=\"归纳法证明\">归纳法证明</h3>\n<p>有多种方法证明费马小定理,其中基于二项式定理的数学归纳法最为直观。首先,对于 <span class=\"math inline\">\\(a=1\\)</span>,很显然 <span class=\"math inline\">\\(1^p \\equiv 1\\pmod{p}\\)</span> 成立。现在假设 <span class=\"math inline\">\\(a^p \\equiv a \\pmod{p}\\)</span> 为真,只要在此条件下证明 <span class=\"math inline\">\\((a+1)^p\\equiv a+1\\pmod{p}\\)</span>,则命题成立。</p>\n<p>根据二项式定理 <span class=\"math display\">\\[(a+1)^p = a^p + {p \\choose 1} a^{p-1} + {p \\choose 2} a^{p-2} + \\cdots + {p \\choose p-1} a + 1\\]</span> 其中二项式系数的定义为 <span class=\"math inline\">\\({p \\choose k}= \\frac{p!}{k! (p-k)!}\\)</span>。注意到因为 <span class=\"math inline\">\\(p\\)</span> 为素数,对于 <span class=\"math inline\">\\(1 \\le k \\le p-1\\)</span>,每个系数 <span class=\"math inline\">\\({p \\choose k}\\)</span> 都是 <span class=\"math inline\">\\(p\\)</span> 的倍数。</p>\n<p>这样再取模<span class=\"math inline\">\\(\\bmod p\\)</span>,所有的中间项都消失了,只剩下 <span class=\"math inline\">\\(a^p+1\\)</span>, <span class=\"math display\">\\[(a+1)^p \\equiv a^p + 1 \\pmod{p}\\]</span> 依据前面的假设 <span class=\"math inline\">\\(a^p \\equiv a\\pmod{p}\\)</span>,得出 <span class=\"math inline\">\\((a+1)^p \\equiv a+1 \\pmod{p}\\)</span>,证毕。</p>\n<h3 id=\"定理应用\">定理应用</h3>\n<h4 id=\"竞赛题解\">竞赛题解</h4>\n<p>费马小定理为一些看似繁杂的计算问题提供了简明的解法。先看一个简单的例子:如果今天是星期日,问 <span class=\"math inline\">\\(2^{100}\\)</span> 天以后是星期几?一周有 7 天,根据费马小定理有 <span class=\"math inline\">\\(2^{7−1}≡1\\bmod 7\\)</span>,由此可以得到 <span class=\"math display\">\\[2^{100}=2^{16×6+4} ≡ 1^{16}×2^4≡16≡2\\pmod 7\\]</span> 所以答案是星期二。这实际上就是以具体的数字重复了上面第二个推论的证明过程。运用此推论可以极大地加快模幂运算,比如要求计算 <span class=\"math inline\">\\(49^{901}\\bmod 151\\)</span>,由于 <span class=\"math inline\">\\(901\\bmod(151-1)=1\\)</span>,据此马上可以得出 <span class=\"math display\">\\[49^{901}\\equiv 49^1\\equiv 49\\pmod {151}\\]</span> 这当然比将 <span class=\"math inline\">\\(901\\)</span> 转化为二进制数再应用平方模幂运算快得多。</p>\n<p>现在看一道似乎难一点的题:给定等式 <span class=\"math inline\">\\(133^5+110^5+84^5+27^5=n^{5}\\)</span>,求 <span class=\"math inline\">\\(n\\)</span> 的数值。</p>\n<p>初看起来好像没有头绪,那就从基础的奇偶性检查开始。等式左边两项为奇两项为偶,所以总和为偶数,这也决定了<span class=\"math inline\">\\(n\\)</span>一定为偶数。再看指数<span class=\"math inline\">\\(5\\)</span>为一个素数,联想到费马小定理得出 <span class=\"math inline\">\\(n^5≡n\\pmod 5\\)</span>,因此 <span class=\"math display\">\\[133^5+110^5+84^5+27^5≡n\\pmod 5\\]</span> <span class=\"math display\">\\[3+0+4+2≡4≡n\\pmod 5\\]</span> 继续再对<span class=\"math inline\">\\(3\\)</span>取模,同样依据费马小定理的推论有 <span class=\"math inline\">\\(n^5≡n^{5\\mod(3-1)}≡n\\pmod 3\\)</span>,所以 <span class=\"math display\">\\[133^5+110^5+84^5+27^5≡n\\pmod 3\\]</span> <span class=\"math display\">\\[1+2+0+0≡0≡n\\pmod 3\\]</span> 好了,到此总结一下:</p>\n<ol type=\"1\">\n<li><span class=\"math inline\">\\(n\\)</span> 应该大于<span class=\"math inline\">\\(133\\)</span>,且 <span class=\"math inline\">\\(n\\)</span> 为偶数</li>\n<li><span class=\"math inline\">\\(n\\)</span> 为<span class=\"math inline\">\\(3\\)</span>的倍数,因此所有数位相加为<span class=\"math inline\">\\(3\\)</span>的倍数</li>\n<li><span class=\"math inline\">\\(n\\)</span> 除以<span class=\"math inline\">\\(5\\)</span>余<span class=\"math inline\">\\(4\\)</span>,个位应为<span class=\"math inline\">\\(4\\)</span> (<span class=\"math inline\">\\(9\\)</span>不满足偶数条件)</li>\n</ol>\n<p>综合得出 <span class=\"math inline\">\\(n = 144\\)</span> 或 <span class=\"math inline\">\\(n\\geq 174\\)</span>,显然 174 太大了,所以 <span class=\"math inline\">\\(n\\)</span> 只能是 <span class=\"math inline\">\\(\\boxed{144}\\)</span>。</p>\n<p>此题实际上出现在1989年美国数学邀请赛中,这是一项面向中学生的数学竞赛。有趣的是,题目的解答正好反证了<a href=\"https://zh.wikipedia.org/zh-cn/欧拉猜想\">欧拉猜想</a>不成立。</p>\n<h4 id=\"素性检测\">素性检测</h4>\n<p>许多加密算法的应用需要“随机”的大素数,而大素数的生成,常用的方法是随机生成一个整数,然后对其进行素性测试。 由于费马小定理成立的前提条件是 <span class=\"math inline\">\\(p\\)</span> 为素数,这就提供了一种素数判定法则,称为费马素性检验。检验的算法为:</p>\n<blockquote>\n<p><strong>输入:</strong><span class=\"math inline\">\\(n\\)</span> - 需要检验的数,<span class=\"math inline\">\\(n>3\\)</span>;<span class=\"math inline\">\\(k\\)</span> - 检验重复次数<br />\n<strong>输出:</strong><span class=\"math inline\">\\(n\\)</span>是<u>合数</u>,否则<u>可能是素数</u><br />\n重复<span class=\"math inline\">\\(k\\)</span>次:<br />\n<span class=\"math inline\">\\(\\quad\\quad\\)</span>在<span class=\"math inline\">\\([2, n − 2]\\)</span>范围内随机选取 <span class=\"math inline\">\\(a\\)</span><br />\n<span class=\"math inline\">\\(\\quad\\quad\\)</span>如果 <span class=\"math inline\">\\(a^{n-1}\\not \\equiv 1{\\pmod n}\\)</span>,返回<u>合数</u><br />\n返回<u>可能是素数</u></p>\n</blockquote>\n<p>可以看出,费马素性检验并非确定性的,它利用随机化算法判断一个数是合数还是可能是素数。输出为合数时,结果一定正确;但是那些检验出可能为素数的数,也许实际为合数,这样的数被称为费马伪素数。最小的费马伪素数是<span class=\"math inline\">\\(341\\)</span>,<span class=\"math inline\">\\(2^{340}\\equiv1\\pmod {341}\\)</span>,而 <span class=\"math inline\">\\(341=11×31\\)</span>。所以事实上,<strong>费马小定理给出的是关于素数判定的必要但不充分条件</strong>。只能说检验重复次数越多,则被检验数是素数的概率越大。</p>\n<p>还有一类费马伪素数 <span class=\"math inline\">\\(n\\)</span>,它们本身为合数,但是对于所有跟之互素的整数 <span class=\"math inline\">\\(x\\)</span>,都满足费马小定理 <span class=\"math inline\">\\(x^{n-1}\\equiv 1\\pmod n\\)</span>。数论上称它们为卡迈克尔数 (Carmichael number) 或绝对伪素数。最小的卡迈克尔数是 <span class=\"math inline\">\\(561\\)</span>,等于 <span class=\"math inline\">\\(3\\times11\\times17\\)</span>。卡迈克尔数可能骗过费马素性检验,使得检验变得不可靠。幸好这样的数很稀少,统计表明,在前<span class=\"math inline\">\\(10^{12}\\)</span>个自然数中只有<span class=\"math inline\">\\(8241\\)</span>个卡迈克尔数。</p>\n<p>加密通讯程序PGP在算法当中使用了费马素性检验。在需要大素数的网络通信应用中,常常先用费马素性检验方法作预测试,而后调用效率更高的<a href=\"https://zh.wikipedia.org/zh-cn/米勒-拉宾检验\">米勒-拉宾素性测试</a> (Miller–Rabin primality test) 以保证高准确度。</p>\n<h4 id=\"证明rsa算法\">证明RSA算法</h4>\n<p>费马小定理也可以用来证明RSA加密算法的正确性,即解密计算公式可以完整无误地从密文 <span class=\"math inline\">\\(c\\)</span> 还原出明文 <span class=\"math inline\">\\(m\\)</span>: <span class=\"math display\">\\[c^d=(m^{e})^{d}\\equiv m\\pmod {pq}\\]</span> 这里 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 为不同的素数,<span class=\"math inline\">\\(e\\)</span> 和 <span class=\"math inline\">\\(d\\)</span> 是满足 <span class=\"math inline\">\\(ed≡1\\pmod {λ(pq)}\\)</span> 的正整数,而 <span class=\"math inline\">\\(λ(pq)=\\mathrm{lcm}(p−1,q−1)\\)</span>,<span class=\"math inline\">\\(\\mathrm{lcm}\\)</span> 为最小公倍数函数。</p>\n<p>在证明开始前,先介绍一个<a href=\"https://zh.wikipedia.org/wiki/中国余数定理\">中国余数定理</a>的推论:<strong>如果整数 <span class=\"math inline\">\\(\\pmb{n_1,n_2,...,n_k}\\)</span> 两两互素,并且 <span class=\"math inline\">\\(\\pmb{n=n_{1}n_{2}...n_{k}}\\)</span>,那么对于任意整数 <span class=\"math inline\">\\(\\pmb x\\)</span> 和 <span class=\"math inline\">\\(\\pmb y\\)</span>,当且仅当 <span class=\"math inline\">\\(\\pmb{x≡y\\pmod{n_i}}\\)</span> 对每个 <span class=\"math inline\">\\(\\pmb{i=1,2,...k}\\)</span> 都成立时,有 <span class=\"math inline\">\\(\\pmb{x≡y\\pmod n}\\)</span>。</strong>这个推论的证明不难,可留待读者思考解答<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>。依据此推论,如果 <span class=\"math inline\">\\(m^{ed}≡m\\pmod p\\)</span> 和 <span class=\"math inline\">\\(m^{ed}≡m\\pmod q\\)</span> 都为真,则一定有 <span class=\"math inline\">\\(m^{ed}≡m\\pmod{pq}\\)</span>。</p>\n<p>现在看证明的第一步,从 <span class=\"math inline\">\\(e\\)</span> 和 <span class=\"math inline\">\\(d\\)</span> 的关系得到 <span class=\"math inline\">\\(ed-1\\)</span> 可以被 <span class=\"math inline\">\\(p-1\\)</span> 和 <span class=\"math inline\">\\(q-1\\)</span> 整除,即存在非负整数 <span class=\"math inline\">\\(h\\)</span> 和 <span class=\"math inline\">\\(k\\)</span> 满足等式:<span class=\"math display\">\\[ed-1=h(p-1)=k(q-1)\\]</span></p>\n<p>第二步目标 <span class=\"math inline\">\\(m^{ed}≡m\\pmod p\\)</span>,考虑两种情况:</p>\n<ol type=\"1\">\n<li>如果 <span class=\"math inline\">\\(m≡ 0\\pmod p\\)</span>,即 <span class=\"math inline\">\\(m\\)</span> 是 <span class=\"math inline\">\\(p\\)</span> 的整数倍,自然 <span class=\"math inline\">\\(m^{ed}≡0≡m\\pmod p\\)</span></li>\n<li>如果 <span class=\"math inline\">\\(m\\not \\equiv 0\\pmod p\\)</span>,则可以推导出 <span class=\"math display\">\\[m^{ed}=m^{ed-1}m=m^{h(p-1)}m=(m^{p-1})^{h}m\\equiv 1^{h}m\\equiv m{\\pmod {p}}\\]</span> 这里应用了费马小定理 <span class=\"math inline\">\\(m^{p−1}≡1\\pmod p\\)</span></li>\n</ol>\n<p>第三步目标 <span class=\"math inline\">\\(m^{ed}≡m\\pmod q\\)</span>,和上一步的推导过程相似,一样可得出</p>\n<ol type=\"1\">\n<li>如果 <span class=\"math inline\">\\(m≡ 0\\pmod q\\)</span>,即 <span class=\"math inline\">\\(m\\)</span> 是 <span class=\"math inline\">\\(q\\)</span> 的整数倍,自然 <span class=\"math inline\">\\(m^{ed}≡0≡m\\pmod q\\)</span></li>\n<li>如果 <span class=\"math inline\">\\(m\\not \\equiv 0\\pmod q\\)</span>,则可以推导出 <span class=\"math display\">\\[m^{ed}=m^{ed-1}m=m^{k(q-1)}m=(m^{q-1})^{k}m\\equiv 1^{k}m\\equiv m{\\pmod {q}}\\]</span></li>\n</ol>\n<p>既然 <span class=\"math inline\">\\(m^{ed}≡m\\pmod p\\)</span> 和 <span class=\"math inline\">\\(m^{ed}≡m\\pmod q\\)</span> 都被证实,<span class=\"math inline\">\\(m^{ed}≡m\\pmod{pq}\\)</span> 成立,证毕!</p>\n<h4 id=\"加速rsa解密\">加速RSA解密</h4>\n<p>费马小定理和中国余数定理相结合,不仅可以验证RSA加密算法的正确性,还能推演出一种加速解密的方法。</p>\n<p>RSA加密算法里模数<span class=\"math inline\">\\(N\\)</span>是两个素数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>的乘积,所以对于小于<span class=\"math inline\">\\(N\\)</span>的任何数<span class=\"math inline\">\\(m\\)</span>,设定 <span class=\"math inline\">\\(m_1=m\\bmod p\\)</span> 和 <span class=\"math inline\">\\(m_2=m\\bmod q\\)</span>,则<span class=\"math inline\">\\(m\\)</span>由<span class=\"math inline\">\\((m_1,m_2)\\)</span>唯一确定。根据中国余数定理,我们可以使用通解公式从<span class=\"math inline\">\\((m_1,m_2)\\)</span>推算出<span class=\"math inline\">\\(m\\)</span>。因为<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>各自的比特数只有<span class=\"math inline\">\\(N\\)</span>的一半,模运算将比直接计算<span class=\"math inline\">\\(c^d\\equiv m\\pmod N\\)</span>快得多。而在计算<span class=\"math inline\">\\((m_1,m_2)\\)</span>的过程中,应用费马小定理的推论得到: <span class=\"math display\">\\[\\begin{align}\nm_1&=m\\bmod p=(c^d\\bmod N)\\bmod p\\\\\n&=c^d\\bmod p=c^{d\\mod(p-1)}\\bmod p\\tag{1}\\label{eq1}\\\\\nm_2&=m\\bmod q=(c^d\\bmod N)\\bmod q\\\\\n&=c^d\\bmod q=c^{d\\mod(q-1)}\\bmod q\\tag{2}\\label{eq2}\\\\\n\\end{align}\\]</span> 很明显,在上面<span class=\"math inline\">\\((1)\\)</span>和<span class=\"math inline\">\\((2)\\)</span>式里,指数从<span class=\"math inline\">\\(d\\)</span>分别降阶到 <span class=\"math inline\">\\(d_P=d\\bmod (p-1)\\)</span> 和 <span class=\"math inline\">\\(d_Q=d\\bmod (q-1)\\)</span>,这进一步加快运算。最后,计算<span class=\"math inline\">\\(m\\)</span>的步骤再运用加纳算法<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>(Garner Algorithm)优化: <span class=\"math display\">\\[\\begin{align}\nq_{\\text{inv}}&=q^{-1}\\pmod {p}\\\\\nh&=q_{\\text{inv}}(m_{1}-m_{2})\\pmod {p}\\\\\nm&=m_{2}+hq\\pmod {pq}\\tag{3}\\label{eq3}\n\\end{align}\\]</span> 注意到给定<span class=\"math inline\">\\((p,q,d)\\)</span>,就确定了<span class=\"math inline\">\\((d_P,d_Q,q_\\text{inv})\\)</span>的数值。所以可以预先算出它们保存好,解密时只要计算<span class=\"math inline\">\\((m_1,m_2,h)\\)</span>代入到以上的<span class=\"math inline\">\\((3)\\)</span>式就行了。</p>\n<p>这其实正是RSA密码技术规程 <a href=\"https://tools.ietf.org/html/rfc8017\">RFC 8017</a> (PKCS #1 v2.2) 所指定的解密算法,该规程定义的ASN.1格式的密钥数据序列与上面描述的完全对应 (<span class=\"math inline\">\\(d_P\\)</span> - exponent1,<span class=\"math inline\">\\(d_Q\\)</span> - exponent2,<span class=\"math inline\">\\(q_{\\text{inv}}\\)</span> - coefficient):</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">RSAPrivateKey ::= SEQUENCE {</span><br><span class=\"line\"> version Version,</span><br><span class=\"line\"> modulus INTEGER, -- n</span><br><span class=\"line\"> publicExponent INTEGER, -- e</span><br><span class=\"line\"> privateExponent INTEGER, -- d</span><br><span class=\"line\"> prime1 INTEGER, -- p</span><br><span class=\"line\"> prime2 INTEGER, -- q</span><br><span class=\"line\"> exponent1 INTEGER, -- d mod (p-1)</span><br><span class=\"line\"> exponent2 INTEGER, -- d mod (q-1)</span><br><span class=\"line\"> coefficient INTEGER, -- (inverse of q) mod p</span><br><span class=\"line\"> otherPrimeInfos OtherPrimeInfos OPTIONAL</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>广泛应用的开源软件库包<a href=\"https://www.openssl.org/\">OpenSSL</a>实现了这种高效实用的解密算法。如下所示,用OpenSSL命令行工具产生的密钥数据与PKCS #1标准一致:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\"># Generate 512-bit RSA keys saved in PEM format file.</span></span><br><span class=\"line\"><span class=\"comment\"># For demo only, DON'T USE 512-bit KEYS IN PRODUCTION!</span></span><br><span class=\"line\">$ openssl genrsa -out private-key.pem 512</span><br><span class=\"line\">Generating RSA private key, 512 bit long modulus</span><br><span class=\"line\">.++++++++++++</span><br><span class=\"line\">......................++++++++++++</span><br><span class=\"line\">e is 65537 (0x10001)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\"># Inspect RSA keys saved in a PEM format file.</span></span><br><span class=\"line\">$ openssl pkey -<span class=\"keyword\">in</span> private-key.pem -text</span><br><span class=\"line\">-----BEGIN PRIVATE KEY-----</span><br><span class=\"line\">MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEA7HwgswSjqvDRPWj3</span><br><span class=\"line\">vVIxMZDAtXJCa7Qx+2jFv7e7GXB8+fa3MTBL36YjIcAgLeCHAyIzWkPndxvTJE2l</span><br><span class=\"line\">WvYzRQIDAQABAkBCUp2pF0f/jQJhwqqYQhDh4cLqIF1Yb3UFGWE8X37tpwCifAqg</span><br><span class=\"line\">t8NEpaXWkct5M+YxqjKfdOKYy0TVcJRlyS+RAiEA9xujHmh+bOvl0xWDFoARDAHw</span><br><span class=\"line\">v94qRCpeRNveHFpNvPsCIQD0/qFpeSjRWj/4vjCkIOv1RbbhDHVsgsF9HRJNW2Rc</span><br><span class=\"line\">vwIgaGIAUcQKQ7CScMxRh5upl8zqCeKrMAhFsgi+lnN/CykCIDMdAL4Jmht7ccdK</span><br><span class=\"line\">nslPWQs1/T6co878xLN+ojfjbl/vAiEAhmp4YDX1g8kFh6cVtTIDT5AGtzqwB2Jw</span><br><span class=\"line\">cCq+IoKDYBc=</span><br><span class=\"line\">-----END PRIVATE KEY-----</span><br><span class=\"line\">Private-Key: (512 bit)</span><br><span class=\"line\">modulus:</span><br><span class=\"line\"> 00:ec:7c:20:b3:04:a3:aa:f0:d1:3d:68:f7:bd:52:</span><br><span class=\"line\"> 31:31:90:c0:b5:72:42:6b:b4:31:fb:68:c5:bf:b7:</span><br><span class=\"line\"> bb:19:70:7c:f9:f6:b7:31:30:4b:df:a6:23:21:c0:</span><br><span class=\"line\"> 20:2d:e0:87:03:22:33:5a:43:e7:77:1b:d3:24:4d:</span><br><span class=\"line\"> a5:5a:f6:33:45</span><br><span class=\"line\">publicExponent: 65537 (0x10001)</span><br><span class=\"line\">privateExponent:</span><br><span class=\"line\"> 42:52:9d:a9:17:47:ff:8d:02:61:c2:aa:98:42:10:</span><br><span class=\"line\"> e1:e1:c2:ea:20:5d:58:6f:75:05:19:61:3c:5f:7e:</span><br><span class=\"line\"> ed:a7:00:a2:7c:0a:a0:b7:c3:44:a5:a5:d6:91:cb:</span><br><span class=\"line\"> 79:33:e6:31:aa:32:9f:74:e2:98:cb:44:d5:70:94:</span><br><span class=\"line\"> 65:c9:2f:91</span><br><span class=\"line\">prime1:</span><br><span class=\"line\"> 00:f7:1b:a3:1e:68:7e:6c:eb:e5:d3:15:83:16:80:</span><br><span class=\"line\"> 11:0c:01:f0:bf:de:2a:44:2a:5e:44:db:de:1c:5a:</span><br><span class=\"line\"> 4d:bc:fb</span><br><span class=\"line\">prime2:</span><br><span class=\"line\"> 00:f4:fe:a1:69:79:28:d1:5a:3f:f8:be:30:a4:20:</span><br><span class=\"line\"> eb:f5:45:b6:e1:0c:75:6c:82:c1:7d:1d:12:4d:5b:</span><br><span class=\"line\"> 64:5c:bf</span><br><span class=\"line\">exponent1:</span><br><span class=\"line\"> 68:62:00:51:c4:0a:43:b0:92:70:cc:51:87:9b:a9:</span><br><span class=\"line\"> 97:cc:ea:09:e2:ab:30:08:45:b2:08:be:96:73:7f:</span><br><span class=\"line\"> 0b:29</span><br><span class=\"line\">exponent2:</span><br><span class=\"line\"> 33:1d:00:be:09:9a:1b:7b:71:c7:4a:9e:c9:4f:59:</span><br><span class=\"line\"> 0b:35:fd:3e:9c:a3:ce:<span class=\"built_in\">fc</span>:c4:b3:7e:a2:37:e3:6e:</span><br><span class=\"line\"> 5f:ef</span><br><span class=\"line\">coefficient:</span><br><span class=\"line\"> 00:86:6a:78:60:35:f5:83:c9:05:87:a7:15:b5:32:</span><br><span class=\"line\"> 03:4f:90:06:b7:3a:b0:07:62:70:70:2a:be:22:82:</span><br><span class=\"line\"> 83:60:17</span><br></pre></td></tr></table></figure>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>也被称为“费马猜想”,其概要为:当整数<span class=\"math inline\">\\(n > 2\\)</span>时,关于<span class=\"math inline\">\\(x,y,z\\)</span>的不定方程 <span class=\"math inline\">\\(x^{n}+y^{n}=z^{n}\\)</span> 无正整数解。在1995年被最终证明其正确性之后,它就被称为“费马最后的定理”。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>提示:如果两个数同余,那么它们之间的差值一定可以整除模数。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>Garner, H., \"<a href=\"https://ieeexplore.ieee.org/document/5219515\">The Residue Number System</a>\", IRE Transactions on Electronic Computers, Volume EC-8, Issue 2, pp.140-147, DOI 10.1109/TEC.1959.5219515, June 1959<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["学习体会"],"tags":["密码学"]},{"title":"克劳德·香农如何发明未来","url":"/2021/01/03/How-Shannon-invent-future/","content":"<p>2020年底,斯坦福大学工程学院的 David Tse 教授在知名的在线科普出版物《量子杂志》(Quanta Magazine)上发表<a href=\"https://www.quantamagazine.org/how-claude-shannons-information-theory-invented-the-future-20201222/\">专栏文章</a>,纪念美国数学家、电子工程师和密码学家、信息论的创始人克劳德·香农(Claude Shannon)博士。确实,给我们今天的生活带来极大便利的全球互联网和高速无线通信网络的开创和发展,都要归功于香农于1948年创立的现代信息理论。<span id=\"more\"></span>香农如此大名鼎鼎,却又异常低调,乃至许多学生和学者误以为他早已作古。在1985年的IEEE信息学年会上,当与会者得知香农现身在会议中时,会场一片轰动。当年的会议主席回忆:“就好像牛顿出现在物理学会议中。”香农最终于2001年去世,终年84岁。巨星陨落,但他的学术遗产已经由一代又一代的科学家、数学家和工程师所继承和发扬,继续照耀人类前进的道路。特此翻译此文,向先驱致敬!</p>\n<hr />\n<blockquote>\n<p><strong>感谢一位独到的天才的开创性工作,让今天的信息时代成为可能。</strong></p>\n</blockquote>\n<p>科学寻求自然的基本定律。数学在旧的基础上寻找新的定理。工程学构建解决人类需求的系统。这三个学科相互依存但又截然不同。很少有个人同时在这三个学科做出核心贡献 — 但是克劳德·香农就是这样一个难得的个人。</p>\n<p>虽然是最近的纪录片《<a href=\"http://www.thebitplayer.com/\">比特玩家</a>》(The Bit Player) 的主角 — 而他的工作和研究理念也激发了我自己的职业生涯 — 香农并不是一个家喻户晓的人物。他从未获得过诺贝尔奖,而且在2001年去世前后,他都不是像阿尔伯特·爱因斯坦或理查德·费曼那样的名人。然而70多年前,在一篇划时代的论文中,他为构成现代信息时代根本的通信基础设施奠基。</p>\n<p>香农于1916年出生于密歇根州的盖洛德市,是一位当地商人和一名教师的儿子。从密歇根大学获得电气工程和数学学位后,他在麻省理工学院撰写了一篇<a href=\"https://www.cs.virginia.edu/~evans/greatworks/shannon38.pdf\">硕士论文</a>,将名为布尔代数的数学理论应用于开关电路的分析和综合。这是一项变革性的工作,将电路设计从一门艺术变成了科学,现在被认为是数字电路设计的起点。</p>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/12/MIT-Claude-Portrait_v0.jpg\" style=\"width:50.0%;height:50.0%\" />\n<p style=\"text-align: center;\">\n年轻的克劳德·香农\n</p>\n<p>接下来,香农将目光投向了一个更大的目标:通信。</p>\n<p>通信是人类最基本的需求之一。从烟雾信号到信鸽,再到电话和电视,人类一直在寻求更远、更快和更可靠的通信方法。但是,通信系统的工程学过去总是与特定的信息源和物理介质相关联。而香农提出的问题是:“是否有一个大一统的通信理论?”1939年,在给导师万尼瓦尔·布什(Vannevar Bush)的信中,香农概述了他关于“用于情报传递的通用系统的基本特性”的一些初步想法。经过十年的研究,香农终于在1948年发表了他的开创性杰作《<a href=\"http://people.math.harvard.edu/~ctm/home/text/others/shannon/entropy/entropy.pdf\">通信的数学理论</a>》。</p>\n<p>他的理论的核心是一个简单但非常通用的通信模型:发送器将信息编码为信号,该信号会被噪声破坏,然后被接收器解码。虽然简单,香农的模型融合了两个关键见解:将信息源和噪声源与要设计的通信系统隔离开来,并对这两个源进行概率建模。他设想信息源会生成许多可能的消息来进行通信,每条消息都有一定的概率。概率化噪声进一步增加了接收器解析的随机性。</p>\n<p>在香农之前,通信问题主要被视为确定性信号重建问题:如何转换被物理介质扭曲的接收信号,以尽可能准确地重建原始信号。香农的天才在于,他洞察到通信的关键是不确定性。毕竟,如果你提前知道了我在本专栏中对你说的话,那么还有什么必要写它呢?</p>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/12/Shannon-Communication_v1.jpg\" style=\"width:70.0%;height:70.0%\" />\n<p style=\"text-align: center;\">\n香农的通信模型示意图\n</p>\n<p>这一观察将通信从实体问题转向到抽象问题,从而使香农可以使用概率对不确定性进行建模。这给当时的通信工程师们带来了极大的冲击。</p>\n<p>确定了不确定性和概率论的框架,香农在其具有里程碑意义的论文中着手系统地确定通信的基本限定。他的回答分为三个部分。在这三个方面都扮演着重要角色的,是信息“比特”(bit) 的概念。香农将其用作不确定性的基本单位。 比特好比是“二进制数字”的两格式旅行箱,可以是1或0,香农的论文是第一个使用该词的人(尽管他说数学家约翰·图基(John Tukey)先在一个备忘录中用到了该词)。</p>\n<p>首先,香农提出了一个用于表示信息量的每秒最少比特数的公式,这个数字他称之为熵率 <span class=\"math inline\">\\(H\\)</span>。该数字量化了确定信息源将生成哪个消息所涉及的不确定性。熵率越低,不确定性越小,因此将消息压缩成较短的内容越容易。例如,以每分钟100个英文字母的速率发送短信,意味着每分钟发送有<span class=\"math inline\">\\(26^{100}\\)</span>种可能的消息,每条消息均由100个字母的序列表示。因为<span class=\"math inline\">\\(2^{470}≈26^{100}\\)</span>,人们可以将所有这些可能性编码为470比特。如果所有序列的可能性相同,那么香农的公式表明熵率确切值是每分钟470比特。现实中,由于某些序列比其他序列出现的可能性高得多,所以熵率要低得多,可以进行更大的压缩。</p>\n<p>其次,他给出了一个噪声条件下可以可靠通信的每秒最大比特数的公式,他将其称为系统的容量 <span class=\"math inline\">\\(C\\)</span>。这是接收机可以有效解析消息不确定性的最大速率,也就是实际上的通信速度的上限。</p>\n<p>最后,他表明当且只当 <span class=\"math inline\">\\(H<C\\)</span> 时,来自信息源的信息才能在噪声条件下进行可靠的通信。因此,信息就像水一样:如果流量小于管道的容量,就能可靠地通过。</p>\n<p>虽然这是一种通信理论,但同时也是一种有关信息如何产生和转换的理论 — <a href=\"https://www.quantamagazine.org/tag/information-theory/\">信息论</a>。因此,香农现在被誉为“信息论之父”。</p>\n<p>他的定理得出了一些与直觉相反的结论。假设你在嘈杂的地方讲话,确保你的消息能够传达的最佳方法是什么?也许重复多次?当然,这是每个人在喧哗的餐厅中的第一个本能,但事实证明这不是很有效。当然,你重复自己的次数越多,通信越可靠。但是你牺牲了速度来换取可靠性。香农向我们展示了我们可以做得更好。重复消息只是使用编码传输消息的一个例子,通过使用不同且更复杂的编码,可以在维持既定可靠性的同时,快速地通信 — 直到速度上限 <span class=\"math inline\">\\(C\\)</span>。</p>\n<p>香农的理论得出的另一个意外结论是,无论信息的特质是什么 — 莎士比亚十四行诗、贝多芬第五交响曲的录音、还是黑泽明的电影 — 在传输之前,将其编码为比特总是最有效的。因此以无线电系统为例,即使初始的声音和通过空中发送的电磁信号都是模拟波形式的,香农定理也表明最好先将声波数字化为比特,然后将这些比特映射到电磁波。这一令人惊讶的结果是现代数字信息时代的基石,在这个时代里,作为信息通用货币的比特无所不在。</p>\n<p>香农的通用信息理论是如此自然,以至于他似乎发现了宇宙的通信定律,而不是发明了它们。他的理论与自然界的物理定律一样基本。从这个意义上说,他是一名科学家。</p>\n<p>香农发明了新的数学来描述通信定律。他引入了一些新思想,比如概率模型的熵率,这些新思想已应用于研究动力学系统长期行为的遍历数学等广泛的数学分支。从这个意义上说,香农是一个数学家。</p>\n<p>但最重要的是,香农是一名工程师。他的理论受到实际工程问题的启发。尽管对当时的工程师来说是深奥的,但香农的理论现已成为所有现代通信系统(光学、水下、甚至行星际)基础的标准框架。就我个人而言,我很幸运地参与了将香农的理论应用和扩展到无线通信的全球性努力中,历经几代标准将通信速度提高了两个数量级。确实,当前推出的5G标准使用的不是一种而是两种实用的编码,它们被证明可以达到香农的速度上限。</p>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/12/MIT-Shannon-Saturn_v1.jpg\" style=\"width:70.0%;height:70.0%\" />\n<p style=\"text-align: center;\">\n晚年香农手持土星的照片\n</p>\n<p>香农在70多年前就为所有这一切奠定了基础。他是怎么做到的?通过不懈地专注于问题的根本特征,忽略所有其它方面。他的通信模型的简单性就是这种风格的很好例证。他还知道要专注于可能,而不是立即可行的事情。</p>\n<p>香农的工作说明了顶级科学研究的真正作用。当我开始读研究生时,我的导师告诉我,最好的工作是修剪知识树,而不是扩张知识树。那时候我不知道该怎么做;我一直认为研究人员的工作就是添加自己的树枝。但是在我的职业生涯中,由于我有机会在自己的工作中运用这种思想,我开始理解了。</p>\n<p>当香农开始研究通信时,工程师已经掌握了大量技术。正是他的统一工作,将所有这些知识枝条修剪成了一棵贯通而优美的树 — 这棵树为几代科学家、数学家和工程师带来了累累硕果。</p>\n","categories":["致敬先驱"],"tags":["通信系统"]},{"title":"IPv6动态地址分配机制详解","url":"/2020/12/01/IPv6-Addressing/","content":"<p>IPv6支持多个地址,地址分配更加灵活方便。与 IPv4 仅仅依赖 DHCP 协议的地址分配方法不同,IPv6 加入了原生的无状态地址自动配置 (<a href=\"https://tools.ietf.org/html/rfc4862\">Sateless Address Autoconfiguration</a>,简写 SLAAC) 协议。SLAAC 既可以单独工作为主机提供 IPv6 地址,又能与 <a href=\"https://tools.ietf.org/html/rfc8415\">DHCPv6</a> 协同运作产生新的分配方案。这里对 IPv6 动态地址分配机制做一个全面分析。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Who the hell knew how much address space we needed?</strong><br> <strong>— <em>Vint Cerf</em>(温特·瑟夫,美国互联网先驱,公认的“互联网之父”之一)</strong></p>\n</div>\n<h2 id=\"ipv6-地址概述\">IPv6 地址概述</h2>\n<h3 id=\"地址构成\">地址构成</h3>\n<p>IPv6 与 IPv4 最显著的不同,就是其超大的地址空间。IPv4 有32比特 (4字节),允许大约42.9亿(2<sup>32</sup>)个地址。而 IPv6 定义了128比特 (16字节),支持大约340 x 10<sup>36</sup>个地址。这是一个相当可观的数字,在可预见的未来都不会出现地址枯竭的问题。典型的 IPv6 地址可被划分成两部分。如下图所示,开头的64比特用来代表网络,后面的64比特用作接口标识:<img src=\"ipv6-addr-format.png\" />接口标识可以有多种方式产生:</p>\n<ul>\n<li>静态手工设置</li>\n<li><a href=\"https://en.wikipedia.org/wiki/IPv6_address#Modified_EUI-64\">链路层地址 (MAC) 转化为64比特 EUI-64</a></li>\n<li>由 DHCPv6 指定</li>\n<li>由隐私扩展或加密协议生成</li>\n</ul>\n<p>为了便于书写,IPv6 推荐末端加上网络前缀长度的压缩格式标记,据此上面的地址可以缩短表示为: 2001:db8:130f<strong>::</strong>7000:<strong>0</strong>:140b/<strong>64</strong></p>\n<h3 id=\"地址类型\">地址类型</h3>\n<p><a href=\"https://tools.ietf.org/html/rfc4291\">RFC 4291</a> 区分了三种 IPv6 地址类型:</p>\n<ol type=\"1\">\n<li>单播 (unicast):网络地址和网络节点一一对应,点对点连接</li>\n<li>任播 (anycast):发送地址对应一群接收节点,但只有最近一个接收到</li>\n<li>组播 (multicast):发送地址对应一群接收可以复制信息的节点</li>\n</ol>\n<p>留意到 IPv6 取消了广播 (broadcast) 地址类型,对应的 IPv4 广播功能可以更好地由组播实现。任播地址与单播并无不同,应用场景非常有限。任播的一个典型应用是设定 DNS 根服务器,以便让主机就近查询域名。对于单播和组播地址,它们之间可由不同的网络前缀所辨认:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: left;\">地址类型</th>\n<th style=\"text-align: left;\">英文名称</th>\n<th style=\"text-align: left;\">二进制</th>\n<th style=\"text-align: left;\">16进制</th>\n<th style=\"text-align: left;\">应用</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: left;\">链路本地地址(单播)</td>\n<td style=\"text-align: left;\">Link-local address</td>\n<td style=\"text-align: left;\">1111 1110 10</td>\n<td style=\"text-align: left;\">fe80::/10</td>\n<td style=\"text-align: left;\">单链路通信</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\">唯一本地地址(单播)</td>\n<td style=\"text-align: left;\">Unique local address</td>\n<td style=\"text-align: left;\">1111 1101</td>\n<td style=\"text-align: left;\">fd00::/8</td>\n<td style=\"text-align: left;\">本地网络通信</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: left;\">全局单播地址</td>\n<td style=\"text-align: left;\">Global unicast address</td>\n<td style=\"text-align: left;\">001</td>\n<td style=\"text-align: left;\">2000::/3</td>\n<td style=\"text-align: left;\">互联网通信</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\">组播地址</td>\n<td style=\"text-align: left;\">Multicast address</td>\n<td style=\"text-align: left;\">1111 1111</td>\n<td style=\"text-align: left;\">ff00::/8</td>\n<td style=\"text-align: left;\">群组通信,流媒体视频</td>\n</tr>\n</tbody>\n</table>\n<p>主机的每个接口都必须有一个链路本地地址,此外接口还可以以手工或动态自动配置方式获得唯一本地地址和全局单播地址。这样 IPv6 接口很自然地就拥有多个单播地址。唯一本地地址由本地网管管理,全局单播地址则需要 IANA 指定的地区注册中心负责分配。参考下图,当前所有的全局单播地址都从2000::/3地址块里分配,地址前面48比特标识服务提供商的全局路由网络,之后的16比特标识企业或校园内部子网: <img src=\"ipv6-addr-alloc.png\" /> 因为 IPv6 组播地址只能用作目的地址,所以其比特位定义与单播不同。参见 RFC 4291,组播地址包含4比特特征标志位、4比特应用范围标志及最后112比特群组标识: <img src=\"ipv6-multicast-addr.png\" />另外同一协议还规定了一些<a href=\"https://tools.ietf.org/html/rfc4291#page-15\">预留的 IPv6 组播地址</a>,其中最重要的有:</p>\n<ul>\n<li>本地网段所有节点 — ff02::1</li>\n<li>本地网段所有路由器 — ff02::2</li>\n<li>本地请求节点地址 — ff02::1:ffxx:xxxx</li>\n</ul>\n<h2 id=\"动态分配方案\">动态分配方案</h2>\n<h3 id=\"ndp-协议\">NDP 协议</h3>\n<p>IPv6 动态地址分配依赖于邻居发现协议 (<a href=\"https://tools.ietf.org/html/rfc4861\">Neighbor Discovery Protocol</a>,简称NDP)。NDP作用于数据链路层,负责在链路上发现其他节点和相应的 IPv6 地址,并确定可用路由和维护其他活动节点的信息可达性。它为 IPv6 网络提供了等效于 IPv4 网络中地址解析协议 (ARP) 与 ICMP 路由器发现和重定向协议的功能。然而,NDP加入了许多改进以及新的功能。 NDP 定义了五种 <a href=\"https://tools.ietf.org/html/rfc4443\">ICMPv6</a> 消息类型:</p>\n<ol type=\"1\">\n<li>路由器请求 (Router Solicitation,简称 RS)</li>\n<li>路由器通告 (Router Advertisement,简称 RA)</li>\n<li>邻居请求 (Neighbor Solicitation)</li>\n<li>邻居通告 (Neighbor Advertisement)</li>\n<li>重定向 (Redirect)</li>\n</ol>\n<p>这里的头二个消息类型 RS 和 RA,就是实现 IPv6 动态地址分配的关键。主机会发送 RS 消息到本地网段所有路由器组播地址 ff02::2,请求路由信息。当路由器收到网络节点发出的 RS 时,会即时发送 RA 回应。RA 的消息格式如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\"> 0 1 2 3</span><br><span class=\"line\"> 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Type | Code | Checksum |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Cur Hop Limit |M|O| Reserved | Router Lifetime |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Reachable Time |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Retrans Timer |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Options ...</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-</span><br></pre></td></tr></table></figure>\n<p>它定义了两个特殊比特位 M 和 O,其意义如下:</p>\n<ul>\n<li>M — 受监管的地址配置标志,设为1时地址从 DHCPv6 获取。</li>\n<li>O — 其他配置标志,设为1时指示其他配置信息从 DHCPv6 获取</li>\n</ul>\n<p>RA 消息的最后是选项部分 (Options),最初有源链路层地址、MTU 和前缀信息三个选项。后来,<a href=\"https://tools.ietf.org/html/rfc8106\">RFC 8106</a> (取代 RFC 6106) 又加上了递归域名服务器 (RDNSS) 和域名服务搜索列表 (DNSSL) 两个选项。这其中前缀信息选项直接决定了主机接口的动态地址配置,它的格式如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\"> 0 1 2 3</span><br><span class=\"line\"> 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Type | Length | Prefix Length |L|A| Reserved1 |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Valid Lifetime |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Preferred Lifetime |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| Reserved2 |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ Prefix +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+ +</span><br><span class=\"line\">| |</span><br><span class=\"line\">+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+</span><br></pre></td></tr></table></figure>\n<p>这里前缀长度 (Prefix Length) 和前缀 (Prefix) 联合决定了 IPv6 地址的网络前缀。此外,前缀信息选项也定义了两个特殊比特位 L 和 A:</p>\n<ul>\n<li>L — “在链路” (on-link) 标志,设为1时指示前缀可用于“在链路”判定</li>\n<li>A — 自动地址配置标志,设为1时指示前缀可用于 SLAAC</li>\n</ul>\n<p>类似于 IPv4 的子网掩码功能,“在链路”判定的意义在于让主机确定某个接口可以接入哪些网络。缺省情况下,主机只将链路本地地址所在的网络视为“在链路”。如果无法判定一个目的地址的“在链路”状态,主机默认将 IPv6 数据报转发给默认网关(或缺省路由器)。当主机收到 RA 消息时,如果一个前缀信息选项的“在链路”标志设为1,并且有效使用期限 (Valid Lifetime) 也是非0值,那么主机就会在前缀列表中为之创建一个新的前缀网络条目。所有未过期的前缀网络条目都是“在链路”的。</p>\n<h3 id=\"消息时序\">消息时序</h3>\n<p>了解了 NDP 协议及 RA 消息所传递的信息之后,来看看它们是如何引导网络节点实现动态地址分配的。</p>\n<p>网络中的路由器会周期性的发送 RA 消息到本地网段所有节点组播地址 (ff02::1)。但是,为了避免时延,主机启动完成后会马上发送一个或多个 RS 消息到本地网段所有路由器。协议规定路由器要在0.5秒内回应 RA 消息。之后,根据所收到的 RA 消息中的 M/O/A 比特位的取值,主机会决定如何动态配置接口的唯一本地地址和全局单播地址,以及如何获取其他配置信息。在某些比特位取值组合下,主机需要运行 DHCPv6 客户端软件,连接到服务器以获取地址分配和/或其他配置信息。整个过程如以下消息时序图所示:</p>\n<pre class=\"mermaid\">\nsequenceDiagram\n\nparticipant R as 路由器\nparticipant H as 主机\nparticipant S as DHCPv6 服务器\nNote over R,H: 路由器请求\nrect rgb(239, 252, 202)\nH->>R: Router Solitication\nR-->>H: Router Advertisement\nend\nNote over H,S: 地址请求\nrect rgb(230, 250, 255)\nH->>S: DHCPv6 Solicit\nS-->>H: DHCPv6 Advertise\nH->>S: DHCPv6 Request\nS-->>H: DHCPv6 Reply\nend\nNote over H,S: 其他信息请求\nrect rgb(230, 250, 255)\nH->>S: DHCPv6 Information-request\nS-->>H: DHCPv6 Reply\nend\n\n</pre>\n<div class=\"note warning\"><p><strong>注意:</strong> 与 IPv4 DHCP 协议不同,DHCPv6 客户端使用 UDP 端口546,服务器使用 UDP 端口547。</p>\n</div>\n<p>以下详细解释由 M/O/A 比特位的取值组合所确定的三种动态分配方案:</p>\n<ul>\n<li>SLAAC</li>\n<li>SLAAC + 无状态 DHCPv6</li>\n<li>有状态 DHCPv6</li>\n</ul>\n<h3 id=\"slaac\">SLAAC</h3>\n<p>SLAAC是最简单的 IPv6 地址自动分配方案,不需要任何服务器。其工作原理是主机启动后发送 RS 消息请求,路由器回送 RA 消息至本地网段所有节点。如果 RA 消息包含如下设置:</p>\n<ul>\n<li><strong><mark>首部的 M 比特和 O 比特都清零</mark></strong></li>\n<li><strong>前缀信息选项的 L 比特和 A 比特置为1</strong></li>\n</ul>\n<p>那么主机收到这个 RA 消息后,执行如下操作实现 SLAAC:</p>\n<ol type=\"1\">\n<li>组合网络前缀与本地接口标识,生成唯一本地地址或全局单播地址</li>\n<li>安装默认网关(或缺省路由)指向路由器地址 (RA消息的源地址)</li>\n<li>将此接口设为对应网络前缀的“在链路”,也是以上默认网关的下一跳接口</li>\n<li>如果包含 RDNSS 和/或 DNSSL 选项,安装域名服务器和域名后缀</li>\n</ol>\n<p>这样,主机就获得了一个或多个 IPv6 唯一本地地址或全局单播地址,以及默认网关和域名服务信息,可以完成各种互联网连接。</p>\n<p>下面是思科 Catalyst 9300 多层接入交换机上的 SLAAC 配置示例:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">ipv6 unicast-routing</span><br><span class=\"line\">interface Vlan10</span><br><span class=\"line\"> ipv6 enable</span><br><span class=\"line\"> ipv6 address 2001:ABCD:1000::1/64</span><br><span class=\"line\"> ipv6 nd ra dns server 2001:4860:4860::8888 infinite</span><br><span class=\"line\"> ipv6 nd ra dns search-list example.com</span><br></pre></td></tr></table></figure>\n<p>思科多层交换机的第三层接口提供路由功能。可以看到,当在 VLAN 10 的第三层接口激活 IPv6 之后,其默认的地址自动分配方案就是 SLAAC。从该接口发出的 RA 消息的控制比特位全部按照 SLAAC 方案设置,其配置的每个 IPv6 地址的网络前缀都会被自动加入到 RA 前缀信息选项列表中。当然,网络管理员也可以用单独的接口配置命令排除某些网络前缀。示例的最后两行配置命令指定了 RDNSS 和 DNSSL,它们也被加入到 RA 消息选项中。</p>\n<p>这时如果主机接入 VLAN 10 的端口,就会马上获得一个网络前缀为 2001:ABCD:1000::/64 全局单播地址,同时其默认网关的地址被设定为 2001:ABCD:1000::1。打开浏览器输入一个网址,它就会向指定的域名服务器 2001:4860:4860::8888(谷歌的公共域名服务器地址)发出域名解析请求,以获取目的网址的 IPv6 地址建立连接。</p>\n<h3 id=\"slaac-无状态-dhcpv6\">SLAAC + 无状态 DHCPv6</h3>\n<p>SLAAC 自动地址分配快捷方便,为中小型网络部署提供了即插即用的 IPv6 部署方案。但是如果网络节点需要获得其他一些配置信息,比如 NTP/SNTP 服务器、TFTP 服务器和 SIP 服务器地址,或者其功能依赖某些厂商特定的信息选项 (Vendor-specific Information Option) 时,就必须选择 <u>SLAAC + 无状态 DHCPv6</u> 的方案。</p>\n<p>这一方案依然使用 SLAAC 自动地址分配,但是路由器会指示主机去连接 DHCPv6 服务器以获取其他配置信息。这时路由器回送的 RA 消息设置变为:</p>\n<ul>\n<li><strong>首部的 M 比特清零,<mark>O 比特置为1</mark></strong></li>\n<li><strong>前缀信息选项的 L 比特和 A 比特置为1</strong></li>\n</ul>\n<p>主机收到这个 RA 消息后,执行如下操作:</p>\n<ol type=\"1\">\n<li>组合网络前缀与本地接口标识,生成唯一本地地址或全局单播地址</li>\n<li>安装默认网关(或缺省路由)指向路由器地址 (RA消息的源地址)</li>\n<li>将此接口设为对应网络前缀的“在链路”,也是以上默认网关的下一跳接口</li>\n<li>如果包含 RDNSS 和/或 DNSSL 选项,安装域名服务器和域名后缀</li>\n<li><mark>启动 DHCPv6 客户端,连接 DHCPv6 服务器请求其他配置信息</mark></li>\n<li><mark>保存 DHCPv6 服务器回复的其他配置信息</mark></li>\n</ol>\n<p>可以看到,<u>SLAAC + 无状态 DHCPv6</u> 在地址分配上与 SLAAC 并没有什么不同。DHCPv6 只是提供附加配置信息,不会分配 IPv6 地址。所以 DHCPv6 服务器不会追踪网络节点的地址分配状况,这就是“无状态”的含义。</p>\n<p>相应的 Catalyst 9300 交换机上的配置命令如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">ipv6 unicast-routing</span><br><span class=\"line\">ipv6 dhcp pool vlan-10-clients</span><br><span class=\"line\"> dns-server 2001:4860:4860::8888</span><br><span class=\"line\"> domain-name example.com</span><br><span class=\"line\"> sntp address 2001:DB8:2000:2000::33</span><br><span class=\"line\">interface Vlan10</span><br><span class=\"line\"> ipv6 enable</span><br><span class=\"line\"> ipv6 address 2001:ABCD:1000::1/64</span><br><span class=\"line\"> ipv6 nd other-config-flag</span><br><span class=\"line\"> ipv6 dhcp server vlan-10-clients</span><br><span class=\"line\"> # ipv6 dhcp relay destination 2001:9:6:40::1</span><br></pre></td></tr></table></figure>\n<p>与 SLAAC 的例子不同点在于,VLAN 10 接口配置命令<code>ipv6 nd other-config-flag</code>明确指定置位 RA 消息的 O 比特。其下一条命令<code>ipv6 dhcp server vlan-10-clients</code>激活接口的 DHCPv6 服务器响应功能,对应服务器的共用资源名称为<code>vlan-10-clients</code>。DHCPv6 服务器的配置在接口配置的上方,从<code>ipv6 dhcp pool vlan-10-clients</code>处开始,包含了 DNS 服务器地址、DNS 域名和 SNTP 服务器地址。</p>\n<p>如果使用交换机之外的位于其他网段的 DHCPv6 服务器,可以删除<code>ipv6 dhcp server</code>命令,启用示例中下一行的<code>ipv6 dhcp relay destination</code>命令指定转发 DHCPv6 请求至外部服务器的地址。</p>\n<h3 id=\"有状态-dhcpv6\">有状态 DHCPv6</h3>\n<p>许多大型企业应用 DHCP 管理设备的 IPv4 地址,所以部署 DHCPv6 集中分配和管理 IPv6 地址是自然的优先选择。这就是<u>有状态 DHCPv6</u>的用武之地。这一方案同样需要路由器发送的 RA 消息,但是不会仅仅依靠网络前缀进行自动地址分配。RA 消息的控制比特位设置是:</p>\n<ul>\n<li><strong><mark>首部的 M 比特置为1</mark>,O 比特无所谓</strong></li>\n<li><strong>前缀信息选项的 L/A 比特可按需要设为1或0</strong></li>\n</ul>\n<p>收到这个 RA 消息后,主机执行操作如下:</p>\n<ol type=\"1\">\n<li>如果有前缀信息选项 A 比特设为1,则组合生成唯一本地地址或全局单播地址</li>\n<li>安装默认网关(或缺省路由)指向路由器地址 (RA消息的源地址)</li>\n<li>如果有前缀信息选项 L 比特设为1,将此接口设为对应网络前缀的“在链路”</li>\n<li>如果包含 RDNSS 和/或 DNSSL 选项,安装域名服务器和域名后缀</li>\n<li><mark>启动 DHCPv6 客户端,连接服务器请求地址和其他配置信息</mark></li>\n<li><mark>将 DHCPv6 服务器分配的地址设置到此接口</li>\n<li><mark>保存 DHCPv6 服务器回复的其他配置信息</mark></li>\n</ol>\n<p>Catalyst 9300 交换机上的<u>有状态 DHCPv6</u> 配置命令示例如下:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">ipv6 unicast-routing</span><br><span class=\"line\">ipv6 dhcp pool vlan-10-clients</span><br><span class=\"line\"> address prefix FD09:9:5:90::/64</span><br><span class=\"line\"> address prefix 2001:9:5:90::/64</span><br><span class=\"line\"> dns-server 2001:9:5:90::115</span><br><span class=\"line\"> domain-name test.com</span><br><span class=\"line\">interface Vlan10</span><br><span class=\"line\"> ipv6 enable</span><br><span class=\"line\"> ipv6 address 2001:ABCD:1:1::1/64</span><br><span class=\"line\"> ipv6 nd prefix 2001:ABCD:1:1::/64 no-advertise</span><br><span class=\"line\"> ipv6 nd managed-config-flag</span><br><span class=\"line\"> ipv6 dhcp server vlan-10-clients</span><br></pre></td></tr></table></figure>\n<p>与 <u>SLAAC + 无状态 DHCPv6</u> 相比,这里接口配置去掉了<code>ipv6 nd other-config-flag</code>,改用<code>ipv6 nd managed-config-flag</code>命令。这对应于将 RA 消息首部的 M 比特置为1。DHCPv6 服务器的配置加入了两条<code>address prefix</code>命令设置网络前缀。同时接口配置的<code>ipv6 nd prefix 2001:ABCD:1:1::/64 no-advertise</code>指定路由器不包含 2001:ABCD:1:1::/64 前缀信息选项到 RA 中。所以,这个例子主机接口不会生成 SLAAC 地址,只会产生来自 DHPCv6 的两个地址:一个是网络前缀为 FD09:9:5:90::/64 的唯一本地地址,另一个是网络前缀为 2001:9:5:90::/64 的全局单播地址。这两个地址的接口标识也分别由 DHPCv6 指定。</p>\n<p>如何分辨主机接口动态分配的地址来源?方法很简单。要记住的一点是,DHPCv6 不会发送网络前缀长度给请求者,所以从 DHPCv6 拿到的地址的网络前缀长度都是128。而 SLAAC 生成的地址网络前缀长度不会是128的。请看下面 Linux 主机上 wired0 接口实例:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">ifconfig wired0</span><br><span class=\"line\">wired0 Link encap:Ethernet HWaddr A0:EC:F9:6C:D9:30 </span><br><span class=\"line\"> inet6 addr: 2001:20::53c7:1364:a4d8:fd91/128 Scope:Global</span><br><span class=\"line\"> inet6 addr: 2001:20::a2ec:f9ff:fe6c:d930/64 Scope:Global</span><br><span class=\"line\"> inet6 addr: fe80::a2ec:f9ff:fe6c:d930/64 Scope:Link</span><br><span class=\"line\"> UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1</span><br><span class=\"line\"> RX packets:510 errors:0 dropped:0 overruns:0 frame:0</span><br><span class=\"line\"> TX packets:1213 errors:0 dropped:0 overruns:0 carrier:0</span><br><span class=\"line\"> collisions:0 txqueuelen:0 </span><br><span class=\"line\"> RX bytes:93670 (91.4 KiB) TX bytes:271979 (265.6 KiB)</span><br></pre></td></tr></table></figure>\n<p>我们马上可以判定,该接口使用的是<u>有状态 DHCPv6</u> 地址分配,但也用收到的相同的网络前缀2001:20::/64生成了 SLAAC 地址:</p>\n<ul>\n<li>2001:20::53c7:1364:a4d8:fd91/128 — DHCPv6 地址,接口标识为随机数</li>\n<li>2001:20::a2ec:f9ff:fe6c:d930/64 — SLAAC 地址,接口标识为 MAC EUI-64 格式</li>\n<li>fe80::a2ec:f9ff:fe6c:d930/64 — 链路本地地址,接口标识为 MAC EUI-64 格式</li>\n</ul>\n<div class=\"note warning\"><p><strong>注意:</strong>DHPCv6 服务器也不提供任何 IPv6 默认网关信息,主机需要从 RA 消息获知动态默认网关。</p>\n</div>\n<h3 id=\"总结比较\">总结比较</h3>\n<p>下表列出了 RA 消息的控制比特组合与地址分配和其他配置获取方式的关系:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">M-比特</th>\n<th style=\"text-align: center;\">O-比特</th>\n<th style=\"text-align: center;\">A-比特</th>\n<th style=\"text-align: center;\">主机地址</th>\n<th style=\"text-align: center;\">其他配置</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">静态设置</td>\n<td style=\"text-align: center;\">手工配置</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">前缀由 RA 指定,自动生成</td>\n<td style=\"text-align: center;\">手工配置</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">静态设置</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">前缀由 RA 指定,自动生成</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">有状态 DHCPv6</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">有状态 DHCPv6 和/或 自动生成</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">0</td>\n<td style=\"text-align: center;\">有状态 DHCPv6</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">有状态 DHCPv6 和/或 自动生成</td>\n<td style=\"text-align: center;\">DHCPv6</td>\n</tr>\n</tbody>\n</table>\n<p>总结三种动态分配方案:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">分配方案</th>\n<th style=\"text-align: center;\">特点</th>\n<th style=\"text-align: center;\">适用场景</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">SLAAC</td>\n<td style=\"text-align: center;\">简单实用,快速部署</td>\n<td style=\"text-align: center;\">中小企业、消费类产品联网、物联网 (IoT)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">SLAAC + 无状态 DHCPv6</td>\n<td style=\"text-align: center;\">自动配置,扩展服务</td>\n<td style=\"text-align: center;\">中小企业需要附加网络服务</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">有状态 DHCPv6</td>\n<td style=\"text-align: center;\">集中管理和控制</td>\n<td style=\"text-align: center;\">大型企业、事业单位和校园网</td>\n</tr>\n</tbody>\n</table>\n<div class=\"note warning\"><p><strong>注意:</strong>由于 IPv6 网络接口可以有多个地址(一个链路本地地址,加上一个或多个唯一本地地址和/或全局单播地址),在建立外部连接时,如何选择源地址变得非常重要。<a href=\"https://tools.ietf.org/html/rfc6724\">RFC 6724</a> 给出了详细的 IPv6 源地址选择规则。在嵌入式系统的开发中,与同一远端设备连接的控制平面和数据平面常常由不同的功能组件实现。比如控制平面直接调用 Linux 用户空间套接字建立连接,连接使用的 IPv6 源地址由 TCP/IP 协议栈选定,而数据平面直接在内核空间实现数据封装处理和传输。这时要及时将控制平面所选择的 IPv6 源地址同步到数据平面,否则用户数据无法送达同一目的地。</p>\n</div>\n<h2 id=\"排错指南\">排错指南</h2>\n<p>在思科路由器和交换机上通用的 IPv6 动态地址分配调试和排错命令如下表所示:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: left;\">命令行</th>\n<th style=\"text-align: left;\">用途</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: left;\"><code>show ipv6 interface brief</code></td>\n<td style=\"text-align: left;\">显示全部接口 IPv6 地址信息简表</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\"><code>show ipv6 interface [type] [num]</code></td>\n<td style=\"text-align: left;\">显示单个接口 IPv6 和 NDP 配置和状态信息</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: left;\"><code>show ipv6 interface [type] [num] prefix</code></td>\n<td style=\"text-align: left;\">显示单个接口 IPv6 网络前缀信息</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\"><code>show ipv6 dhcp pool</code></td>\n<td style=\"text-align: left;\">显示 DHCPv6 共用资源配置信息</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: left;\"><code>show ipv6 dhcp binding</code></td>\n<td style=\"text-align: left;\">显示 DHCPv6 服务器保存的 IPv6 与主机绑定信息</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\"><code>show ipv6 dhcp interface [type] [num]</code></td>\n<td style=\"text-align: left;\">显示单个接口 DHCPv6 配置和状态信息</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: left;\"><code>debug ipv6 nd</code></td>\n<td style=\"text-align: left;\">调试 IPv6 NDP 协议</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\"><code>debug ipv6 dhcp</code></td>\n<td style=\"text-align: left;\">调试 DHCPv6 服务器</td>\n</tr>\n</tbody>\n</table>\n<p>下面这个控制终端 NDP 协议调试记录显示路由器收到来自主机 FE80::5850:6D61:1FB:EF3A 的 RS 消息,回应 RA 消息到本网络全部节点组播地址 FF02::1:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">Router# debug ipv6 nd</span><br><span class=\"line\"> ICMP Neighbor Discovery events debugging is on</span><br><span class=\"line\">Router# show logging | include RS</span><br><span class=\"line\"> ICMPv6-ND: Received RS on GigabitEthernet0/0/0 from FE80::5850:6D61:1FB:EF3A</span><br><span class=\"line\">Router# show logging | include RA</span><br><span class=\"line\"> ICMPv6-ND: Sending solicited RA on GigabitEthernet0/0/0</span><br><span class=\"line\"> ICMPv6-ND: Request to send RA for FE80::C801:EFFF:FE5A:8</span><br><span class=\"line\"> ICMPv6-ND: Setup RA from FE80::C801:EFFF:FE5A:8 to FF02::1 on GigabitEthernet0/0/0</span><br></pre></td></tr></table></figure>\n<p>而下一段记录显示输入<code>debug ipv6 dhcp</code>调试命令之后,观察到的<u>无状态 DHCPv6</u> 的例子。主机 FE80::5850:6D61:1FB:EF3A 向 DHCPv6 服务器发出信息请求 (INFORMATION-REQUEST) 消息,服务器选择源地址 FE80::C801:B9FF:FEF0:8 发出回应消息:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">Router#debug ipv6 dhcp</span><br><span class=\"line\"> IPv6 DHCP debugging is on</span><br><span class=\"line\"></span><br><span class=\"line\">IPv6 DHCP: Received INFORMATION-REQUEST from FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br><span class=\"line\">IPv6 DHCP: Option VENDOR-CLASS(16) is not processed</span><br><span class=\"line\">IPv6 DHCP: Using interface pool LAN_POOL</span><br><span class=\"line\">IPv6 DHCP: Source Address from SAS FE80::C801:B9FF:FEF0:8</span><br><span class=\"line\">IPv6 DHCP: Sending REPLY to FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br></pre></td></tr></table></figure>\n<p>以下是一段<u>有状态 DHCPv6</u> 的调试记录,第1、15、16和26行显示两次消息交换 (SOLICIT/ADVERTISE,REQUEST/REPLY) 的完整过程:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">IPv6 DHCP: Received SOLICIT from FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br><span class=\"line\">IPv6 DHCP: Option UNKNOWN(39) is not processed</span><br><span class=\"line\">IPv6 DHCP: Option VENDOR-CLASS(16) is not processed</span><br><span class=\"line\">IPv6 DHCP: Using interface pool LAN_POOL</span><br><span class=\"line\">IPv6 DHCP: Creating binding for FE80::5850:6D61:1FB:EF3A in pool LAN_POOL</span><br><span class=\"line\">IPv6 DHCP: Binding for IA_NA 0E000C29 not found</span><br><span class=\"line\">IPv6 DHCP: Allocating IA_NA 0E000C29 in binding for FE80::5850:6D61:1FB:EF3A</span><br><span class=\"line\">IPv6 DHCP: Looking up pool 2001:ABCD::/64 entry with username '000100011F3E8772000C29806CCC0E000C29'</span><br><span class=\"line\">IPv6 DHCP: Poolentry for user not found</span><br><span class=\"line\">IPv6 DHCP: Allocated new address 2001:ABCD::D9F7:61C:D803:DCF1</span><br><span class=\"line\">IPv6 DHCP: Allocating address 2001:ABCD::D9F7:61C:D803:DCF1 in binding for FE80::5850:6D61:1FB:EF3A, IAID 0E000C29</span><br><span class=\"line\">IPv6 DHCP: Updating binding address entry for address 2001:ABCD::D9F7:61C:D803:DCF1</span><br><span class=\"line\">IPv6 DHCP: Setting timer on 2001:ABCD::D9F7:61C:D803:DCF1 for 60 seconds</span><br><span class=\"line\">IPv6 DHCP: Source Address from SAS FE80::C801:B9FF:FEF0:8</span><br><span class=\"line\">IPv6 DHCP: Sending ADVERTISE to FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br><span class=\"line\">IPv6 DHCP: Received REQUEST from FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br><span class=\"line\">IPv6 DHCP: Option UNKNOWN(39) is not processed</span><br><span class=\"line\">IPv6 DHCP: Option VENDOR-CLASS(16) is not processed</span><br><span class=\"line\">IPv6 DHCP: Using interface pool LAN_POOL</span><br><span class=\"line\">IPv6 DHCP: Looking up pool 2001:ABCD::/64 entry with username '000100011F3E8772000C29806CCC0E000C29'</span><br><span class=\"line\">IPv6 DHCP: Poolentry for user found</span><br><span class=\"line\">IPv6 DHCP: Found address 2001:ABCD::D9F7:61C:D803:DCF1 in binding for FE80::5850:6D61:1FB:EF3A, IAID 0E000C29</span><br><span class=\"line\">IPv6 DHCP: Updating binding address entry for address 2001:ABCD::D9F7:61C:D803:DCF1</span><br><span class=\"line\">IPv6 DHCP: Setting timer on 2001:ABCD::D9F7:61C:D803:DCF1 for 172800 seconds</span><br><span class=\"line\">IPv6 DHCP: Source Address from SAS FE80::C801:B9FF:FEF0:8</span><br><span class=\"line\">IPv6 DHCP: Sending REPLY to FE80::5850:6D61:1FB:EF3A on FastEthernet0/0</span><br></pre></td></tr></table></figure>\n<p>对于难以厘清是主机、路由器还是 DHCPv6 服务器问题的复杂情况,推荐使用免费开源网络数据包分析软件 <a href=\"https://www.wireshark.org\">Wireshark</a> 截取整个流程的数据包作分析。在用 Wireshark 分析数据包时,可以应用关键词过滤功能:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: left;\">过滤关键词</th>\n<th style=\"text-align: left;\">只显示</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: left;\">icmpv6.type=133</td>\n<td style=\"text-align: left;\">ICMPv6 RS</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: left;\">icmpv6.nd.ra.flag</td>\n<td style=\"text-align: left;\">ICMPv6 RA</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: left;\">dhcpv6</td>\n<td style=\"text-align: left;\">DHCPv6 数据包</td>\n</tr>\n</tbody>\n</table>\n<p>我们可以直接在主机端运行 Wireshark,也可以在网络侧用交换机提供的交换端口分析器 (Switched Port Analyzer,简写 SPAN) 将指定端口数据包集中重定向到运行 Wireshark 的监控端口截取。思科 Catalyst 9300 系列交换机还直接<a href=\"https://www.cisco.com/c/en/us/td/docs/switches/lan/catalyst9300/software/release/17-4/configuration_guide/nmgmt/b_174_nmgmt_9300_cg/configuring_packet_capture.html\">集成了 Wireshark 软件</a>,能够在线截取和分析过滤数据包,使用起来十分方便。</p>\n<p>这里提供三种分配方案的样例数据包文件供下载学习: <a href=\"slaac.pcap\">slaac.pcap</a>,<a href=\"stateless-dhcpv6.pcap\">stateless-dhcpv6.pcap</a>,<a href=\"stateful-dhcpv6.pcap\">stateful-dhcpv6.pcap</a></p>\n<h2 id=\"参考资料\">参考资料</h2>\n<h3 id=\"ipv6-产品认证测试\">IPv6 产品认证测试</h3>\n<p>精准而实效的 IPv6 产品测试是确保 IPv6 基础架构高互操作性、安全性和可靠性部署的关键。 <strong>IPv6 Ready</strong> 标识是 <a href=\"https://www.ipv6forum.com\">IPv6 论坛</a> 创立的 IPv6 测试和认证项目。它的目标是定义 IPv6 的一致性和互操作测试规范、提供自测试工具集、建立<a href=\"https://www.ipv6ready.org.cn\">全球IPv6测试中心</a>并提供产品验证服务,以及最后颁发认证证书。</p>\n<p>2020年5月 <a href=\"https://www.ipv6ready.org\">IPv6 Ready 标识项目</a>发布新的<a href=\"https://www.ipv6ready.org/resources.html\">5.0版测试规范</a>:</p>\n<ul>\n<li>IPv6 核心协议一致性测试规范(Conformance)</li>\n<li>IPv6 核心协议互操作测试规范(Interoperability)</li>\n</ul>\n<p>伴随着这两个新的测试序列,项目组还申明了两个永久变化:</p>\n<ol type=\"1\">\n<li>测试在纯 IPv6 环境下完成,不会提供任何 IPv4 网络环境</li>\n<li>被测设备必须在缺省态激活系统及所有接口的 IPv6 功能</li>\n</ol>\n<p>不出意外地,在新的5.0版测试规范中有专门的章节定义 SLAAC 测试用例,以验证这一 IPv6 核心协议。</p>\n<h3 id=\"ipv6-核心协议-rfc-列表\">IPv6 核心协议 RFC 列表</h3>\n<p>粗体显示的部分由 IPv6 Ready 5.0版核心协议测试规范直接覆盖:</p>\n<ul>\n<li>RFC 4191 Default Router Preferences and More-Specific Routes</li>\n<li>RFC 4193 Unique Local IPv6 Unicast Addresses</li>\n<li><strong>RFC 4291 IP Version 6 Addressing Architecture</strong></li>\n<li><strong>RFC 4443 Internet Control Message Protocol (ICMPv6) for the Internet Protocol Version 6 (IPv6) Specification</strong></li>\n<li><strong>RFC 4861 Neighbor Discovery for IP version 6 (IPv6)</strong></li>\n<li><strong>RFC 4862 IPv6 Stateless Address Autoconfiguration</strong></li>\n<li>RFC 4941 Privacy Extensions for Stateless Address Autoconfiguration in IPv6</li>\n<li>RFC 5095 Deprecation of Type 0 Routing Headers in IPv6</li>\n<li>RFC 6724 Default Address Selection for Internet Protocol Version 6 (IPv6)</li>\n<li>RFC 6980 Security Implications of IPv6 Fragmentation with IPv6 Neighbor Discovery</li>\n<li>RFC 7217 A Method for Generating Semantically Opaque Interface Identifiers with IPv6 Stateless Address Autoconfiguration (SLAAC)</li>\n<li>RFC 8064 Recommendation on Stable IPv6 Interface Identifiers</li>\n<li>RFC 8106 IPv6 Router Advertisement Options for DNS Configuration</li>\n<li><strong>RFC 8200 Internet Protocol, Version 6 (IPv6) Specification</strong></li>\n<li><strong>RFC 8201 Path MTU Discovery for IP version 6</strong></li>\n<li>RFC 8415 Dynamic Host Configuration Protocol for IPv6 (DHCPv6)</li>\n</ul>\n","categories":["学习体会"],"tags":["TCP/IP","思科技术"]},{"title":"用ISIC测试网络设备IP协议栈的健壮性","url":"/2020/11/25/ISIC-test-IP-stack/","content":"<p>ISIC是一个开放源码的IP协议栈健壮性测试工具集。许多网络设备的安全漏洞 (比如拒绝服务等) 源于IP协议栈的软件设计或实现错误,应用此工具集可以及时发现和修复这些差错,确保系统运行的稳定性和可靠性。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Testing leads to failure, and failure leads to underatanding.</strong><br> <strong>— <em>Burt Rutan</em>(伯特·鲁坦,美国航空工程师,2004年首次率领民间团队制造载人航天器飞上太空)</strong></p>\n</div>\n<h3 id=\"工具概要\">工具概要</h3>\n<p><a href=\"http://isic.sourceforge.net/\">ISIC</a>是英文全称 <strong>I</strong>P <strong>S</strong>tack <strong>I</strong>ntegrity <strong>C</strong>hecker 的首字母缩写,翻译过来就是“IP协议栈完整性检查器”。这是一个用于测试IPv4和IPv6及其附属协议族 (TCP,UDP,ICMP等) 软硬件实现的工具集。其思想是产生目标协议的部分参数可控的随机数据包,然后将数据包发送到指定的被测试设备,观察设备的输出结果和运行状态。被测试设备可以是路由器、交换机、网络防火墙、入侵检测/预防系统 (IDS/IPS) 或者任何联网主机。ISIC也包含一个产生原始以太网帧的工具,用于检验以太网底层的软硬件实现。</p>\n<p>ISIC工具集包含的十个可执行的命令行工具如下,它们覆盖了几乎所有的TCP/IP核心协议:</p>\n<ul>\n<li>isic:IPv4协议栈完整性检查器</li>\n<li>tcpsic:TCP协议栈完整性检查器</li>\n<li>udpsic:UDP协议栈完整性检查器</li>\n<li>esic:以太网协议栈完整性检查器</li>\n<li>icmpsic:ICMP协议栈完整性检查器</li>\n<li>multisic: 组播UDP协议栈完整性检查器</li>\n<li>isic6:IPv6协议栈完整性检查器</li>\n<li>tcpsic6:IPv6 TCP协议栈完整性检查器</li>\n<li>udpsic6:IPv6 UDP协议栈完整性检查器</li>\n<li>icmpsic6:ICMPv6协议栈完整性检查器</li>\n</ul>\n<p>ISIC支持Unix类 (Linux/BSD/macOS) 系统,其编译和运行都需要<a href=\"https://github.com/libnet/libnet\">Libnet库</a> (版本1.1以上) 。Libnet是一个可移植的底层网络数据包构造和注入框架,ISIC调用它的应用程序接口 (API) 来创建和发送网络层数据包和链路层数据帧。ISIC在其主页上以源代码的压缩包发布,也可以使用RPM安装。ISIC的最后版本是0.07,它以BSD风格的许可证发布,对使用者的限制很少。</p>\n<p>ISIC最早的0.01版在1999年由迈克·弗兰岑 (Mike Frantzen) 在Redhat Linux 5.1上用了两周的时间写成。一开始弗兰岑用它来检测防火墙的漏洞,但是意外地发现它竟然造成许多设备崩溃。弗兰岑把ISIC放到网上后,得到了很好反响,许多IDS/IPS研发和测试工程师都在工作中使用它。直到0.05版,ISIC一直由弗兰岑自己维护和更新。2004年初,Libnet的作者迈克·希夫曼 (Mike Schiffman) 重写了这一API库,新的1.1.x版本不再向后兼容。至此,ISIC无法再与最新的Libnet库编译和链接。</p>\n<p>2004年晚些时候,思科公司的安全测试工程师<a href=\"https://www.linkedin.com/in/shu-xiao-7925a3/\">Shu Xiao</a>将一份补丁发送给弗兰岑,使ISIC能够工作于新版Libnet之上,补丁也包含了对其它一些错误的修复。这成为了弗兰岑移交责任给Xiao的完美时机,0.06版就此诞生。Xiao继续进一步增强ISIC的功能。他添加了新的IPv6协议测试工具 (*sic6),以及组播测试工具 (multisic),同时还改进了随机性和发送效率。最终的0.07版由Xiao于2006年12月发布。</p>\n<p>虽然原来的开发目的是做防火墙的功能测试,但ISIC在发现IP协议栈完整性漏洞方面的惊人表现,使得它成为一个网络系统安全评估的一个重要工具。<a href=\"https://sourceforge.net/projects/isic/\">ISIC在Sourceforge的项目页面</a>显示,截止到2020年一月0.07版下载量接近两万。如果包含RPM的下载安装,实际的使用量应该远远大于这个数目。搜索网上<u>公共漏洞和暴露</u> (Common Vulnerabilities and Exposures,缩写CVE) 数据库,可以找到下面由ISIC工具集披露的安全漏洞:</p>\n<table>\n<colgroup>\n<col style=\"width: 9%\" />\n<col style=\"width: 29%\" />\n<col style=\"width: 19%\" />\n<col style=\"width: 19%\" />\n<col style=\"width: 22%\" />\n</colgroup>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">编号 (CVE-ID)</th>\n<th style=\"text-align: center;\">CVSS<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a> 评分</th>\n<th style=\"text-align: center;\">相关产品/系统</th>\n<th style=\"text-align: left;\">问题描述</th>\n<th style=\"text-align: center;\">ISIC工具</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">CVE-2000-0451</td>\n<td style=\"text-align: center;\">5.0 MEDIUM</td>\n<td style=\"text-align: center;\">英特尔 Express 8100 ISDN 路由器</td>\n<td style=\"text-align: left;\">允许远程攻击者使用过大或分片ICMP数据包进行拒绝服务攻击</td>\n<td style=\"text-align: center;\">icmpsic</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">CVE-2000-0463</td>\n<td style=\"text-align: center;\">5.0 MEDIUM</td>\n<td style=\"text-align: center;\">BeOS 5.0 操作系统</td>\n<td style=\"text-align: left;\">允许远程攻击者使用分片TCP数据包进行拒绝服务攻击</td>\n<td style=\"text-align: center;\">tcpsic</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">CVE-2008-1746</td>\n<td style=\"text-align: center;\">7.8 HIGH</td>\n<td style=\"text-align: center;\">思科统一通信管理器 (CUCM)</td>\n<td style=\"text-align: left;\">允许远程攻击者通过一系列格式错误的UDP数据包导致拒绝服务 (核心转储和服务重启)</td>\n<td style=\"text-align: center;\">udpsic</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">CVE-2013-7470</td>\n<td style=\"text-align: center;\">7.1 HIGH</td>\n<td style=\"text-align: center;\">Linux 内核 <3.11.7版本</td>\n<td style=\"text-align: left;\">允许攻击者造成拒绝服务 (无限循环和崩溃)</td>\n<td style=\"text-align: center;\">icmpsic</td>\n</tr>\n</tbody>\n</table>\n<p>由于存在被黑客利用的风险,大量CVE都不给出所使用工具的信息。所以,有理由相信用ISIC找到的安全弱点及漏洞也远不止上面这些。比如,在网上对<a href=\"https://attackerkb.com/topics/12890azSfK/cve-2019-12256-vxworks-ipv4-options-buffer-overflow\">2019年风河系统VxWorks的IPv4选项堆栈溢出漏洞</a>的讨论中,就有人问到为什么VxWorks没有做“isic\":</p>\n<blockquote>\n<p><em>In this particular case, it’s really surprising however that <strong>VxWorks did not just isic</strong>, which has been around for years and years to find a vulnerability like this: http://isic.sourceforge.net/</em></p>\n</blockquote>\n<p>从计算机科学和工程的学术研究上看,ISIC可被归类为自动软件测试技术中的模糊 (fuzzing) 测试。<a href=\"https://scholar.google.com/scholar?hl=en&as_sdt=0%2C5&q=%22IP+Stack+Integrity+Checker%22&oq=I\">许多研究文献</a>都提及对ISIC的直接应用或间接参考。这种技术的基本思想是将无效、未被考虑或随机的数据输入到计算机程序,然后监视程序运行是否发生异常,例如崩溃、死循环、内置代码断言失败 (assert) 或潜在的内存泄漏等。从缓冲区溢出到跨站点脚本攻击的大多数安全漏洞,通常是对用户提供的输入数据验证不足的结果。模糊测试发现的网络系统安全漏洞常常是严重的,如果不及时发现和清除,会给用户造成重大损失。有证据显示,一些主要的网络设备厂商都集成了ISIC到质量控制的测试工程实践中。</p>\n<h3 id=\"安装过程\">安装过程</h3>\n<p>这里以树莓派 (Respberry Pi 4 Model B) 目标系统为例,说明ISIC的安装过程。首先,使用几个命令查看系统软硬件和GCC编译器版本信息:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~\u0007\u001b $\u001b cat /proc/device0tree/model</span><br><span class=\"line\">Raspberry Pi 4 Model B Rev 1.4</span><br><span class=\"line\">pi@raspberrypi:~\u0007\u001b $\u001b uname -a</span><br><span class=\"line\">Linux raspberrypi 4.19.118-v7l+ <span class=\"comment\">#1311 SMP Mon Apr 27 14:26:42 BST 2020 armv7l GNU/Linux</span></span><br><span class=\"line\">pi@raspberrypi:~ $\u001b gcc --version</span><br><span class=\"line\">gcc (Raspbian 8.3.0-6+rpi1) 8.3.0</span><br><span class=\"line\">Copyright (C) 2018 Free Software Foundation, Inc.</span><br><span class=\"line\">This is free software; see the <span class=\"built_in\">source</span> <span class=\"keyword\">for</span> copying conditions. There is NO</span><br><span class=\"line\">warranty; not even <span class=\"keyword\">for</span> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.</span><br></pre></td></tr></table></figure>\n<p>如前所述,ISIC的编译和运行需要Libnet库的支持。最新的Libnet 1.2版可以从其<a href=\"https://github.com/libnet/libnet/releases\">GitHub项目发布主页下载</a>,然后执行以下步骤编译和安装:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">tar xf libnet-1.2.tar.gz</span><br><span class=\"line\"><span class=\"built_in\">cd</span> libnet-x.y.z/</span><br><span class=\"line\">./configure && make</span><br><span class=\"line\">sudo make install</span><br></pre></td></tr></table></figure>\n<p>安装完可以在<code>/usr/local/lib</code>目录下找到Libnet的静态库文件<code>libnet.a</code>和动态链接库文件<code>libnet.so</code>,而所有的编程需要的头文件位于<code>/usr/local/include</code>目录下。这些验证无误后,接下来就可以开始ISIC的安装:</p>\n<ol type=\"1\">\n<li><p>从ISIC的主页 (<a href=\"http://isic.sourceforge.net/\">http://isic.sourceforge.net/</a>) 下载压缩的打包文件<strong>isic-0.07.tgz</strong></p></li>\n<li><p>用<code>tar xzvf isic-0.07.tgz</code>命令解压展开:</p></li>\n<li><p>进入<code>isic-0.07</code>目录,执行<code>./configure</code>命令配置编译环境:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~/Downloads/isic-0.07\u0007\u001b $\u001b ./configure </span><br><span class=\"line\">creating cache ./config.cache</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> gcc... gcc</span><br><span class=\"line\">checking whether the C compiler (gcc ) works... yes</span><br><span class=\"line\">checking whether the C compiler (gcc ) is a cross-compiler... no</span><br><span class=\"line\">checking whether we are using GNU C... yes</span><br><span class=\"line\">checking whether gcc accepts -g... yes</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> a BSD compatible install... /usr/bin/install -c</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> /usr/<span class=\"built_in\">local</span>/lib/libnet.a... no</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> -lnet... no</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> libnet_init <span class=\"keyword\">in</span> -lnet... yes</span><br><span class=\"line\">checking how to run the C preprocessor... gcc -E</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> ANSI C header files... no</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> u_int16_t... yes</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> u_int32_t... yes</span><br><span class=\"line\">checking <span class=\"keyword\">for</span> in_addr_t... no</span><br><span class=\"line\">checking whether gcc needs -traditional... no</span><br><span class=\"line\">updating cache ./config.cache</span><br><span class=\"line\">creating ./config.status</span><br><span class=\"line\">creating Makefile</span><br></pre></td></tr></table></figure></p></li>\n<li><p>执行<code>make</code>命令编译和链接:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~/Downloads/isic-0.07\u0007\u001b $\u001b make</span><br><span class=\"line\">gcc -o isic isic.c -Wall -W -g -O2 -I/usr/<span class=\"built_in\">local</span>/include `libnet-config --cflags` -DHAVE_LIBNET=1 -Din_addr_t=u_int32_t `libnet-config --defines` -DVERSION=\\"0.07\\" -lnet -L/usr/<span class=\"built_in\">local</span>/lib `libnet-config --libs` </span><br><span class=\"line\">gcc -o tcpsic tcpsic.c -Wall -W -g -O2 -I/usr/<span class=\"built_in\">local</span>/include `libnet-config --cflags` -DHAVE_LIBNET=1 -Din_addr_t=u_int32_t `libnet-config --defines` -DVERSION=\\"0.07\\" -lnet -L/usr/<span class=\"built_in\">local</span>/lib `libnet-config --libs` </span><br><span class=\"line\">tcpsic.c:\u001b In <span class=\"keyword\">function</span> ‘\u001bmain\u001b’:</span><br><span class=\"line\">tcpsic.c:274:7:\u001b error: \u001bdereferencing pointer to incomplete <span class=\"built_in\">type</span> ‘\u001bstruct tcphdr\u001b’</span><br><span class=\"line\"> tcp\u001b->\u001bth_off = rand() & 0xf;</span><br><span class=\"line\"> \u001b^~\u001b</span><br><span class=\"line\">make: *** [Makefile:27: tcpsic] Error 1</span><br></pre></td></tr></table></figure> 糟糕!有一个编译错误!不必惊慌,这是Linux系统的TCP/IP头文件的变化造成的 (毕竟从最后的0.07版发布到现在已经十多年了)。如下修改<code>isi\u0007c.h</code>文件就可以清除这个错误:</p>\n<p><figure class=\"highlight diff\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi\u001b:~/Downloads/isic-0.07 $ git diff isi\u0007c.h.bak isic.h</span><br><span class=\"line\"><span class=\"comment\">diff --git a/isic.h.bak b/isic.h</span></span><br><span class=\"line\"><span class=\"comment\">index c12f716..8a8641c 100644\u001b</span></span><br><span class=\"line\"><span class=\"comment\">--- a/isic.h\u001b.bak</span></span><br><span class=\"line\"><span class=\"comment\">+++ b/isic.h</span></span><br><span class=\"line\"><span class=\"meta\">@@ -10,6 +10,8 @@</span>\u001b</span><br><span class=\"line\"> #include <netinet/icmp6.h>\u001b</span><br><span class=\"line\"> #include <netinet/ip6.h>\u001b</span><br><span class=\"line\"> #include <netinet/if_ether.h>\u001b</span><br><span class=\"line\"><span class=\"addition\">+#include <netinet/tcp.h>\u001b</span></span><br><span class=\"line\"><span class=\"addition\">+#include <netinet/udp.h>\u001b</span></span><br><span class=\"line\"></span><br><span class=\"line\"> #ifndef ETHER_FRAME_SIZE</span><br><span class=\"line\"> #define ETHER_FRAME_SIZE 1500\u001b</span><br></pre></td></tr></table></figure></p></li>\n<li><p>下一步执行<code>sudo make install</code>,将可执行文件安装到<code>/usr/local/bin</code>目录,同时将用户手册安装到<code>/usr/local/man/man1</code>目录:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi\u001b:\u001b~/Downloads/isic-0.07 $\u001b sudo make install</span><br><span class=\"line\">/usr/bin/install -c -m 0755 -d /usr/<span class=\"built_in\">local</span>/bin</span><br><span class=\"line\">/usr/bin/install -c -m 0755 -c isic tcpsic udpsic icmpsic esic multisic isic6 tcpsic6 udpsic6 icmpsic6 /usr/<span class=\"built_in\">local</span>/bin</span><br><span class=\"line\">/usr/bin/install -c -m 0755 -d /usr/<span class=\"built_in\">local</span>/man/man1</span><br><span class=\"line\">/usr/bin/install -c -m 0755 -c isic.1 /usr/<span class=\"built_in\">local</span>/man/man1</span><br></pre></td></tr></table></figure></p></li>\n</ol>\n<p>至此安装过程结束,运行<code>man isic</code>可以在终端直接在线查询<a href=\"https://linux.die.net/man/1/isic\">用户手册</a>。<strong>注意,因为ISIC工具集调用原始套接字 (raw socket) 发送数据包,运行命令行需要超级用户权限,或者授予普通用户特殊权限以<code>sudo</code>方式运行。</strong></p>\n<h3 id=\"用户指南\">用户指南</h3>\n<h4 id=\"命令行选项\">命令行选项</h4>\n<p>下表给出了通用的命令行选项的使用说明:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">选项字符</th>\n<th style=\"text-align: center;\">相关参数</th>\n<th style=\"text-align: center;\">说明</th>\n<th style=\"text-align: center;\">默认值</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">c</td>\n<td style=\"text-align: center;\">数据包数目</td>\n<td style=\"text-align: center;\">只适用于esic</td>\n<td style=\"text-align: center;\"><span class=\"math inline\">\\(2^{32}\\)</span></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">d</td>\n<td style=\"text-align: center;\">目的MAC/IP地址</td>\n<td style=\"text-align: center;\">对esic可选,对其它为必需。设为“rand”选择随机地址</td>\n<td style=\"text-align: center;\">对esic为FF:FF:FF:FF:FF:FF</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">i</td>\n<td style=\"text-align: center;\">接口标识</td>\n<td style=\"text-align: center;\">对esic和multisic为必需,其它不适用</td>\n<td style=\"text-align: center;\">由系统路由选定</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">k</td>\n<td style=\"text-align: center;\">跳过的数据包数目</td>\n<td style=\"text-align: center;\">指定跳过(不发送)的初始数据包数目</td>\n<td style=\"text-align: center;\">0</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">l</td>\n<td style=\"text-align: center;\">数据包长度</td>\n<td style=\"text-align: center;\">只适用于esic</td>\n<td style=\"text-align: center;\">1500</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">m</td>\n<td style=\"text-align: center;\">打印速率或最大速率</td>\n<td style=\"text-align: center;\">指定esic的打印间隙,对其它为最大速率 (kB/s)</td>\n<td style=\"text-align: center;\">对esic为1000</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">p</td>\n<td style=\"text-align: center;\">协议号或数据包数目</td>\n<td style=\"text-align: center;\">指定esic的上层协议号,对其它为数据包数目</td>\n<td style=\"text-align: center;\">对esic为0x0800 (IP),对其它为<span class=\"math inline\">\\(2^{32}\\)</span></td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">r</td>\n<td style=\"text-align: center;\">随机数种子</td>\n<td style=\"text-align: center;\">指定伪随机序列的种子</td>\n<td style=\"text-align: center;\">当前进程标识 (ID)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">s</td>\n<td style=\"text-align: center;\">源MAC/IP地址</td>\n<td style=\"text-align: center;\">对esic可选,对其它为必需。设为“rand”选择随机地址</td>\n<td style=\"text-align: center;\">对esic为接口的MAC地址</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">x</td>\n<td style=\"text-align: center;\">重复次数</td>\n<td style=\"text-align: center;\">适用于esic以外的全部工具,设置数据包的重复次数</td>\n<td style=\"text-align: center;\">1</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">v</td>\n<td style=\"text-align: center;\">无</td>\n<td style=\"text-align: center;\">打印当前版本号</td>\n<td style=\"text-align: center;\">无</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">z</td>\n<td style=\"text-align: center;\">源MAC地址</td>\n<td style=\"text-align: center;\">只适用于multisic,设置源MAC地址</td>\n<td style=\"text-align: center;\">指定接口的MAC地址</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">D</td>\n<td style=\"text-align: center;\">无</td>\n<td style=\"text-align: center;\">适用于esic以外的全部工具,打印调试输出</td>\n<td style=\"text-align: center;\">无</td>\n</tr>\n</tbody>\n</table>\n<p>有几点要特别解释一下:</p>\n<ol type=\"1\">\n<li><span class=\"math inline\">\\(p/k/x\\)</span>三个参数决定了总计发送多少个数据包。假设数据包总数为<span class=\"math inline\">\\(N\\)</span>,那么 <span class=\"math inline\">\\(N=(p-k)\\cdot x\\)</span>。所以<span class=\"math inline\">\\(k\\)</span>一定要小于<span class=\"math inline\">\\(p\\)</span>,不然就没有数据包发出。</li>\n<li>如果<span class=\"math inline\">\\(r\\)</span>参数固定,伪随机序列的种子不变,那么ISICI构造和发送的数据包集合就完全一致。应用这一点可以重复测试序列。</li>\n<li>实际的数据包传输速率由网络接口卡决定,ISIC不能控制,但是它可以指定每秒生成和发送数据包的数量。</li>\n</ol>\n<p>此外,除esic以外的全部工具还支持特定的百分比选项,说明如下:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">选项字符</th>\n<th style=\"text-align: center;\">相关百分比参数</th>\n<th style=\"text-align: center;\">说明</th>\n<th style=\"text-align: center;\">默认值 (%)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">i</td>\n<td style=\"text-align: center;\">错误的ICMP校验和</td>\n<td style=\"text-align: center;\">只用于icmpsic和icmpsic6</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">t</td>\n<td style=\"text-align: center;\">错误的TCP校验和</td>\n<td style=\"text-align: center;\">只用于tcpsic和tcpsic6</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">u</td>\n<td style=\"text-align: center;\">TCP高优先级 (URG) 标志符置位</td>\n<td style=\"text-align: center;\">只用于tcpsic和tcpsic6</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">F</td>\n<td style=\"text-align: center;\">分片的数据包</td>\n<td style=\"text-align: center;\">对isic6意味着包括随机的分片扩展报头</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">I</td>\n<td style=\"text-align: center;\">IP选项或随机长度值</td>\n<td style=\"text-align: center;\">对isic为IP报头随机长度值,对其它为IP选项</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">T</td>\n<td style=\"text-align: center;\">有TCP选项的数据包</td>\n<td style=\"text-align: center;\">只用于tcpsic和tcpsic6</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">U</td>\n<td style=\"text-align: center;\">错误的UDP校验和</td>\n<td style=\"text-align: center;\">只用于udpsic和udpsic6</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">V</td>\n<td style=\"text-align: center;\">错误的IP版本号</td>\n<td style=\"text-align: center;\">适用于全部工具</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n</tbody>\n</table>\n<h4 id=\"命令行示例\">命令行示例</h4>\n<ul>\n<li><p>使用esic生成具有首部包含随机协议编号的以太网帧,并通过eth0接口发送出去;在帧中,源MAC地址固定为01:02:34:56:07:89,目的MAC地址是默认的广播地址;每5000帧会有一条打印输出行:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">esic -i eth0 -s 01:02:34:56:07:89 -p rand -m 5000</span><br></pre></td></tr></table></figure></p></li>\n<li><p>指定isic生成带有随机源地址的100个IP数据包,目的地址固定为10.11.12.13;随机种子设置为10;一半的数据包将是分片报文;发送时,将跳过前20个数据包,从第21个数据包开始:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">isic -s rand -d 10.11.12.13 -F 50 -p 100 -k 20 -r 10</span><br></pre></td></tr></table></figure></p></li>\n<li><p>指定tcpsic生成源地址为1.2.3.4和源TCP端口69,目的地址21.22.23.24和随机目的TCP端口的数据包;每个数据包将被发送两次;整体最大速度为1000kB/s;在所有生成的TCP数据包中,30%的数据包将具有随机的TCP选项;而50%的TCP校验和将不正确。</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">tcpsic -s 1.2.3.4,69 -d 21.22.23.24 -x 2 -m 1000 -T 30 -t 50</span><br></pre></td></tr></table></figure></p></li>\n<li><p>使用以下multisic命令向组播地址224.0.0.5发送50000个UDP数据包;随机源地址和源/目的UDP端口;输出接口被强制为eth2;50%的传出数据包将是分片报文;并且源MAC地址设置为ff:ff:ff:ff:ff:ff:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">multisic -s rand -d 224.0.0.5 -i eth2 -p 50000 -F 50 -z \tff:ff:ff:ff:ff:ff</span><br></pre></td></tr></table></figure></p></li>\n<li><p>使用以下udpsic6命令发送100万个具有随机源地址和源UDP端口的IPv6 UDP数据包;目的地址为2001:1:2:3:4::2,目的UDP端口161 (SNMP端口);90%的传出数据包将具有随机的IPv6目的选项标头,而总数据包中的20%将包含不正确的UDP校验和:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">udpsic6 -s rand -d 2001:1:2:3:4::2,161 -p 1000000 -I 90 -U 20</span><br></pre></td></tr></table></figure></p></li>\n</ul>\n<div class=\"note danger\"><p>当心:请务必谨慎使用ISIC工具集,尽量在封闭的实验网络中运行,以避免给共享的工作环境造成破坏,否则可能要为给团体或他人所带来的损失负责!</p>\n</div>\n<p>最后,给出一段完整的<code>isic</code>工具运行记录及解读:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ sudo isic -v</span><br><span class=\"line\">Version 0.07 </span><br><span class=\"line\">pi@raspberrypi:~ $ sudo isic -s rand -d 10.0.23.1 -p 5000</span><br><span class=\"line\">Using random <span class=\"built_in\">source</span> IP<span class=\"string\">'s</span></span><br><span class=\"line\"><span class=\"string\">Compiled against Libnet 1.2</span></span><br><span class=\"line\"><span class=\"string\">Installing Signal Handlers.</span></span><br><span class=\"line\"><span class=\"string\">Seeding with 1445</span></span><br><span class=\"line\"><span class=\"string\">No Maximum traffic limiter</span></span><br><span class=\"line\"><span class=\"string\">Bad IP Version = 10% Odd IP Header Length = 10% Frag'</span>d Pcnt = 10%</span><br><span class=\"line\"> 1000 @ 18317.7 pkts/sec and 11797.1 k/s</span><br><span class=\"line\"> 2000 @ 20195.5 pkts/sec and 12885.9 k/s</span><br><span class=\"line\"> 3000 @ 26723.7 pkts/sec and 17110.4 k/s</span><br><span class=\"line\"> 4000 @ 27171.0 pkts/sec and 17936.8 k/s</span><br><span class=\"line\"></span><br><span class=\"line\">Wrote 5000 packets <span class=\"keyword\">in</span> 0.22s @ 23122.03 pkts/s</span><br></pre></td></tr></table></figure>\n<p>从上面的记录可以看到,该<code>isic</code>命令指定发送5000个的IP数据包。数据包源地址随机,目的地址为10.0.23.1。输出显示<code>isic</code>工具使用Libnet 1.2版编译和链接,随机数种子默认为进程号1445。之后是三个百分比选项:错误的IP版本号、IP报头随机长度值和分片数据包,都取默认值10%。每一千个数据包会打印一行,显示累计数据包发送速率和当前一千个数据包的字节发送速率,单位分别为pkt/s和k(B)/s。全部数据包发送完毕,打印总计运行时间0.22秒和累计数据包发送速率约23k pkts/s。</p>\n<h4 id=\"使用经验\">使用经验</h4>\n<ul>\n<li>ISIC的基本应用场景总结如下:\n<ul>\n<li>检验防火墙是否有数据包泄漏</li>\n<li>IDS/IPS 的穿透性测试</li>\n<li>路由器、交换机和主机协议栈的健壮性测试</li>\n</ul></li>\n<li>ISIC的主要延伸应用,是对网络安全极为重要的脆弱性/漏洞审查。可以运行下面的测试,并观察是否会发生系统崩溃或死机状态:\n<ul>\n<li>模糊 (fuzzing) 测试</li>\n<li>充溢 (flooding) 测试</li>\n<li>拒绝服务 (DoS) 攻击</li>\n</ul></li>\n<li>另外还可以在测试进行中,并行监测被测设备的健康性,主要指标为:\n<ul>\n<li>CPU和内/外存资源利用率</li>\n<li>响应SNMP查询的能力</li>\n<li>命令行接口/图形用户界面的访问性能</li>\n</ul></li>\n<li>当出现问题时,可以使用同样的<code>-r</code>参数,然后组合<code>-p/c/k</code>选项运用二分查找算法定位相关的数据包。比如,假定发<code>isic</code>送1000个数据包造成被测设备异常,那么将<code>-p</code>减半为500运行。如果发生同样的异常,则继续减半搜索;如果异常没有出现,就将<code>-p</code>改回1000并设定<code>-k 500</code>跳过前半段数据包运行。重复这一过程,不超过10步就可以找到造成问题的单个 IP数据包。</li>\n</ul>\n<h3 id=\"工具扩展\">工具扩展</h3>\n<p>未来如果有需求且有开源开发者愿意贡献,可以考虑加入以下功能扩展:</p>\n<ul>\n<li>新的选项指定isic或isic6报文首部IP协议号 (建议为<code>-P</code>)</li>\n<li>开发前端图形用户界面 (GUI),参考<a href=\"https://nmap.org/zenmap/\">Zenmap</a></li>\n<li>支持多协议标签交换 (MPLS) 的帧格式</li>\n<li>支持因特网组管理协议 (IGMP) 的报文</li>\n<li>支持IEEE 802.1Q以及虚拟局域网标签 (VLAN Tagging) 的帧格式</li>\n<li>支持调试打印输出的详细程度 (verbosity) 分级</li>\n<li>支持加载预先捕获的<a href=\"https://gitlab.com/wireshark/wireshark/-/wikis/Development/LibpcapFileFormat\">pcap格式</a>的网络数据包文件</li>\n<li>与其它的测试工具集成为完整的网络安全测试方案</li>\n</ul>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>公共漏洞评分系统 (Common Vulnerability Scoring System,缩写CVSS) 是一种用于评估计算机系统安全漏洞严重性的开放行业标准。CVSS为每一个漏洞的严重性评分,分数范围是0到10,其中最严重的是10。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["工具使用"],"tags":["网络安全","TCP/IP","C/C++编程","软件测试","树莓派"]},{"title":"勇踏IP未至之境","url":"/2021/03/06/Internet-in-space/","content":"<p>协调世界时(UTC)2021年2月18日20时55分,美国宇航局(NASA)制造的<strong>毅力号</strong>火星探测器成功登陆火星。之前的2月10日,中国首次自主发射的<strong>天问一号</strong>火星探测器成功进入火星轨道,之后也将择机展开着陆、巡视等科学探测任务。人类对最近的邻星的科学探测活动,正在紧锣密鼓地进行中。到2030年,人类将开始执行载人航天器登陆火星计划,这将是我们实现卡尔·萨根走出“暗淡蓝点”、探寻太空家园的梦想的第一个里程碑!<span id=\"more\"></span>在这一切的背后,是数代科学家和工程师的不懈努力和持久奉献。特别地,那些创造了互联网,从而为我们的生活带来翻天覆地变化的先驱们,还在活跃于科研和工程的一线,引领团队打造太空网络的基础设施。2020年10月《量子杂志》发表<a href=\"https://www.quantamagazine.org/vint-cerfs-plan-for-building-an-internet-in-space-20201021/\">专栏文章</a>,介绍和访谈美国互联网先驱温顿·瑟夫(Vinton Cerf)在拓展互联网到太空方面的重要工作。特此翻译精校如下,向先驱致敬!</p>\n<hr />\n<blockquote>\n<p><strong>40年前温顿·瑟夫帮助创建了互联网,如今他仍在努力联接世界各地、以及这个世界之外的人们。</strong></p>\n</blockquote>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/10/Vint-Cerf_2880_Lede.jpg\" />\n<p style=\"text-align: center;\">\n“互联网之父”之一的温顿·瑟夫在建立行星际互联网方面也发挥了举足轻重的作用\n</p>\n<p>太空探索困难重重,通信难度尤甚。宇航员需要与控制中心联络,理想情况下是通过视频通讯;而太空飞行器则需要回送收集的数据,最好是高速且时延尽可能低方式。起初,太空探索团队设计并部署了自己独特的通信系统,虽然效果很好,但这并不是效率的典范。直到1998年的一天,互联网先驱<a href=\"https://research.google/people/author32412/\">温顿·瑟夫</a>设想了一种新型网络,可以提供更强大的功能支持不断增长的太空人群体和载具数量。行星际互联网的梦想就此诞生了。</p>\n<p>但将互联网扩展到太空,可不仅仅是在火箭上安装Wi-Fi那么简单。科学家们面临着新的障碍:涉及的距离是确确实实的天文数字,且行星持续移动并可能阻塞信号。地球上任何人,如果想要向其他星球上人或物发送消息,都必须对付经常中断的通信路径。</p>\n<p>“我们开始对互联网标准进行数学计算,这套协议在地球上运转良好。然而,光速太慢了,”瑟夫回忆他与<a href=\"http://ipnsig.org/\">行星际联网特别兴趣小组</a>同事们的早期工作时说。克服这个问题将是一项艰巨的任务,但是这位美国计算机科学家和前斯坦福大学教授已经习惯了迎接重大挑战。</p>\n<p>几十年前,两位“互联网之父”瑟夫和罗伯特·卡恩(Robert Kahn)设计和开发了地面互联网的架构和协议套件,即为人熟知的传输控制协议/网际协议(TCP/IP)。尽管瑟夫很快就推辞了这一极高荣誉的头衔,但每一位曾经在网上冲浪、发送电子邮件或下载应用程序的人都应该感谢他们。“很多人都为互联网的创立做出了贡献,”他用平和的语调说。</p>\n<p>为了在地球互联网上传输数据,TCP/IP需要路由器构建的完整的端到端路径,这一路径通过铜缆、光纤或蜂窝数据网络之类的链路转发信息包。瑟夫和卡恩并未设计用于存储数据的互联网,部分原因是在1970年代初期内存太昂贵了。因此,如果路径中的某个链路断开,路由器将丢弃该数据包,而后从源点重发。这在地球的低时延、高连通性环境中效果很好。然而,太空中的网络更易受到干扰,需要采用不同的方法。</p>\n<p>“TCP/IP在行星际距离上不起作用,”瑟夫说,“因此,我们设计了一套可行的协议。”</p>\n<p>2003年,瑟夫和一个研究小组引入了新的<a href=\"https://ieeexplore.ieee.org/abstract/document/1204759\">捆绑协议</a>。“捆绑”是一种具有中断/时延容忍性的网络连接(disruption/delay-tolerant networking,简写DTN)协议,具有将互联网拓展到这个世界之外的能力。就像构成地球互联网的协议一样,捆绑是基于分组交换机制的。这意味着数据包沿着由路由器控制切换的路径,一路从数据源转发到目的地。但是,捆绑具有地面互联网所不具备的属性,比如可以存储信息的节点。</p>\n<p>瑟夫解释说,例如,从地球到木星的数据包可能会途经火星上的中继站。但是,当数据包到达约4千万英里之外的中继站时(才完成总路程4亿英里的一小部分),火星当时的朝向可能并不合适,无法将数据包发送到木星。“为什么要丢掉这些信息,而不是暂存直到木星出现?” 瑟夫说。这种存储转发功能,使得捆绑数据包在大规模中断和拖延条件下,仍旧可以顽强地一次次跳转到目的地。他在这一研究方向上的<a href=\"https://ieeexplore.ieee.org/abstract/document/8396643\">最新论文</a>,强调了Loon SDN(一种能够管理在空中移动的网络的技术)在NASA下一代太空通信体系结构中的适用性。</p>\n<p>除了行星际互联网,年过70的瑟夫还专注于作为谷歌首席互联网布道师的日常工作。这是他所喜欢的一个别致职称。充满了传教士般的热忱和渴望,他希望通过全球政策制定,将互联网带到数十亿还没有接触到它的人们手中。他对严肃工作一丝不苟,却又保持着顽皮一面。尽管他给人的典型印象是修剪整齐的胡须和三件套西装(有人说他正是电影《黑客帝国》中像神一样的<a href=\"https://matrix.fandom.com/wiki/The_Architect\">架构师</a>形象的灵感来源),他也曾经在主题演讲开场时,像超人一样解开他的外套和衬衫露出T恤,上面写着“I P on Everything!”<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a></p>\n<p>瑟夫<a href=\"https://cacm.acm.org/careers/243977-vint-cerf-tweets-good-news-on-coronavirus-recovery/fulltext\">从COVID-19中康复</a>后不久,《量子杂志》就在<a href=\"https://www.youtube.com/watch?v=6CkgsfVJ3l4\">他参加虚拟海德堡获奖者论坛</a><a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>之前联系上他。为简明起见,以下访谈进行了缩减和编辑。</p>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/10/Vint-Cerf_2K_JPL-Cal.jpg\" />\n<p style=\"text-align: center;\">\n瑟夫对探索太阳系充满热情,他与喷气推进实验室、行星际网络特别兴趣小组<br>以及其他机构合作,为太空中的宇航员和计算机带来了可容忍时延的互联网\n</p>\n<p><strong>行星际互联网的想法从哪儿来的?</strong></p>\n<p>1998年春季,我们9个人在喷气推进实验室(Jet Propulsion Laboratory,简写JPL)聚会讨论:展望从现在开始25年后的太空探索可能的需要,我们应该怎么做?曾在JPL任职,后来又服务于NASA总部的阿德里安·胡克(Adrian Hooke)是真正支持并推动这一工作的人。他几年前去世,但正是他将这支团队凝聚在一起。</p>\n<p>我们已经研究了数十年的太阳系,但是无论是直接载人还是使用机器人,这种探索通常涉及无线电通信,要么直接点对点要么通过所谓的“弯曲的管道”:由无线电中继站接收信号并重新广播,以提高到达地球的可能性。</p>\n<p>我们的小组问:我们可以做得更好吗?我们能否利用互联网技术来改善太空通信,特别是随着航天器数量的持续增加,或者当我们开始在月球或火星上安置定居点时,我们该如何做?</p>\n<p><strong>那么,在构思了捆绑协议二十年之后,行星际互联网是否已经建立并开始运行?</strong></p>\n<p>我们不必构建整个事物,然后希望有人使用它。我们寻求建立与互联网类似的标准、免费发布这些标准,而后实现互操作性,以便各个航天国家可以互相帮助。</p>\n<p>对于多任务基础架构,我们正在迈出下一步:设计行星际主干网的功能。你建造下一个任务所需的设备。随着航天器的建造和部署,它们将携带成为行星际骨干网一部分的标准协议。然后,当这些航天器完成其首要的科学任务后,便被重新定位为骨干网中的节点。随着时间的推移,我们累积增生行星际骨干网。</p>\n<p><strong>这种重新定位已经开始吗?</strong></p>\n<p>2004年,火星探测车本应通过深空网络直接将数据传输回地球——位于澳大利亚、西班牙和加利福尼亚的三根巨大的70米长天线已为此做好准备。然而,该信道的可用数据速率仅为每秒28KB,这实在有限。当火星探测车开启无线电传输时,出现过热现象。它们被迫放弃,这意味着发回的数据更少。那使科学家们情绪很坏。</p>\n<p>JPL的一位工程师使用一款超酷的原型软件,对亿万英里以外的探测车和轨道飞行器进行重新编程。我们建立了一个小型的存储转发行星际互联网,该网络实质上只有三个节点:火星表面的探测车、轨道飞行器和地球上的深空网络。从那以后一直在运行。</p>\n<p><strong>从那时起,它变得越来越大,对吧?</strong></p>\n<p>我们一直在完善这些协议的设计,协议的实现和测试也在持续进行中。最新协议正在地球与国际空间站之间来回中继运行。我们还进行了其他一些非常酷的测试。一艘近距离探测彗星的航天器EPOXI,在飞离地球大约81光秒时,有人告诉我们:“如果你们上载协议并在那个航天器上进行测试,我们没意见。”我们也照做了。</p>\n<p>我们在国际空间站进行了另一项测试,当时宇航员正在控制位于德国的一辆小型机器人车辆。通常,你不会这样做:如果你试着驾驶在火星上的车辆,信号到达那里需要20分钟,那么你可能会转动方向盘,而在20分钟后,车辆转弯并落下悬崖。然后,再过20分钟,你才发现你的造价60亿美元的车辆报废了。但这种直接操控在国际空间站和地球之间没问题,因为距离只有几百英里。这项测试的目的,在于辅助宇航员无需降落至其他行星就可以完成某些任务。他们只要绕着其轨道运行,就可以实时部署行星表面上的远程设备。</p>\n<iframe width=\"461\" height=\"259\" src=\"https://www.youtube.com/embed/SnETgd4ePXI\" frameborder=\"0\" allow=\"accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture\" allowfullscreen>\n</iframe>\n<p style=\"text-align: center;\">\n温顿·瑟夫谈创建行星际互联网面临的挑战\n</p>\n<p><strong>用户体验如何?行星际互联网的捆绑协议的使用感受和地球互联网的TCP/IP一样吗?</strong></p>\n<p>很快你就不再处于交互模式。您要么处于“通话完毕”模式,要么处于“这是我几个小时前录制的一个不错的视频记录”模式,就像电子邮件一样。这些协议的着眼点,是认识到时延消除了交互的可能性。这给协议设计带来了限制。</p>\n<p><strong>看来您已经解决了主要问题,但是还有什么待决的问题吗?</strong></p>\n<p>在技术设计上达成共识并实现协议是一回事,在需要它们的地方应用则是另一回事。对于新事物的尝试有很多阻力,因为“新”意味着:“那可能有风险!”和“给我看看它真的有用!” 除非你愿意冒险,否则无法做出证明。</p>\n<p>我们正在努力说服设计太空任务的人员,这些东西已经过充分的测试。那是一个艰难的过程,还有很多工作要做。我们必须为支持太空探索的商业公司提供现成的功能。我们必须让设计任务的科学家说:“这就是我们现在能做到的。”</p>\n<p>这样,你就可以利用我们已拥有行星际骨干网的假设,规划更加雄心勃勃的项目。如果你预设自己做不到,那么你将设计出通信能力有限的任务。</p>\n<p><strong>行星际互联网会催生新的太空探索方法,进而带来新发现吗?</strong></p>\n<p>行星际互联网是旨在支持行星际活动的基础设施,它可能是研究性质的,但有朝一日也可能商业化。它是一种基础设施,就像地球互联网一样。互联网不会发明或发现任何东西。它只是促进人们协同工作和发现新事物的媒介。</p>\n<p><strong>那么在地球上,DTN协议在这里也有用吗?</strong></p>\n<p>瑞典的一位工程师设想试用DTN协议来追踪拉普兰地区的驯鹿,萨米人<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>在那里放牧了8000年。驯鹿四处游荡,在无线电覆盖区域中进进出出。这是一个不可预测的环境,与能够应用准确的航天动力学计算预测可能发生的接触的环境大不相同。正在拉普兰进行的测试,可以研究在通讯的可预测性较差时,DTN的机会性功能。</p>\n<p>同样,在海洋研究中,你在海洋表面或海床上部署数据采集和积累的设备,但却未必能与其一直保持连接。对于地球观测,传感器可以间歇地扫描林地,但无法连续广播。在完全连接的互联网环境中,生成的数据会马上发送出去。这在具有间歇性连接的环境中不适用。你需要一个协议说:“不要惊慌!没关系,先暂存在上面。” 实际上,出于能源效率的考虑,电池驱动的设备都不应该持续传输。</p>\n<p>间歇性存储转发功能在地面上非常有用,尤其是在发生重大灾害后,你可能没有太多的通信功能选项。在资源不足以提供TCP/IP方式的覆盖时,使用DTN快速恢复网络连接。</p>\n<img src=\"https://d2r55xnwy6nx47.cloudfront.net/uploads/2020/10/Vint-Cerf_2K_Home.jpg\" />\n<p style=\"text-align: center;\">\n除了从事行星际互联网的工作之外,瑟夫还是谷歌的<br>首席互联网布道师,致力于拓展地面互联网的全球覆盖\n</p>\n<p><strong>对于整个地球的互联网,从TCP/IP转到DTN有意义吗?</strong></p>\n<p>我们已经证明,即使DTN协议比传统的TCP/IP协议有更多的开销,它也可以高速运行。但是很难在任何地方都引入DTN,因为要看那里有多少TCP/IP在运行。互联网方面还发展了另一套称为QUIC的协议,该协议不仅可以实现更高的数据传输速率,而且还能够更快地从故障或中断中恢复。但是,这种演进不在DTN的发展方向上。</p>\n<p>另一方面,对于连通性还很差的移动设备,DTN功用可能相当不错。我们现在正在研究在移动环境中实现DTN。</p>\n<p><strong>作为互联网之父和布道师,您是否对自己的创作有任何担忧?</strong></p>\n<p>滥用互联网、虚假信息和恶意软件、伤害性攻击、网络钓鱼攻击、勒索软件。意识到人们会利用如此有前景、且不负所望的基础设施去做坏事,这很令人痛心和沮丧。不幸的是,这是人的本性。</p>\n<p>我们需要附加的手段治理在线环境,但那很难。中国人建造了一个大防火墙,然后将所有人置于其中监管。那不一定是我们其他人想要生活的社会,但我们仍然必须解决这个问题。在不走极端的情况下,我们该如何应对?我没有一个好答案。我希望我有,但很可惜。</p>\n<p>这些就是我现在所想的。实际上,思考行星际的网络协议是一个令人耳目一新的转变,因为我们在那里关注的几乎完全是纯粹的科学成果。</p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>这是故意搞笑的双关语。字母I和P连在一起,就是网际协议的缩写IP,“IP on Everything”意即IP运行于任何链路和物理媒介之上,代表当时(1998年)对IP一统天下的展望,这已经成为互联网的现实。另一方面,I就是英文的“我”,P与英文的小便pee谐音,瑟夫借此跟大家开个玩笑。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>由海德堡获奖者论坛基金会举办的年度国际高端青年学术论坛,旨在促进数学和计算机科学的研究和发展,为青年一代学者提供与荣获阿贝尔奖、内万林纳奖、菲尔兹奖和图灵奖等国际顶级奖项的科学家交流学术的机会。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>北欧地区的原住民,也是欧洲目前仅存的游牧民族,许多人仍旧以牧养驯鹿维生。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["致敬先驱"],"tags":["TCP/IP"]},{"title":"C编程实现带有越界检查功能的内存管理函数","url":"/2021/03/28/Memory-overrun-detection/","content":"<p>C语言具有高效、灵活、功能丰富和可移植性强等特点,在程序设计尤其是系统软件开发中备受青睐。它的高效灵活性很大程度上得益于其通过指针对存储器进行低端控制的功能,但代价是程序员必须格外谨慎处理内存的访问细节,避免内存泄漏和缓冲区溢出等运行错误。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Writing in C or C++ is like running a chain saw with all the safety guards removed. It’s powerful, but it’s easy to cut off your fingers.</strong><br> <strong>— <em>Bob Gray</em>(senior director of consulting firm Virtual Solutions, cited in Byte (1998) Vol 23, Nr 1-4. p . 70. )</strong></p>\n</div>\n<p>为了克服C语言程序设计的这一弱点,研发人员开发了许多运行期内存调试工具,以便快速检测内存泄漏及实时定位缓冲区溢出错误。知名的开源免费内存调试器有<a href=\"https://www.valgrind.org\">Valgrind</a>、<a href=\"https://dmalloc.com\">dmalloc</a>和<a href=\"https://packetmania.github.io/2021/08/03/ASAN-intro/\">AddressSanitizer</a>等。内存调试工具的一个通用的设计思想是,在分配内存时预留一些存储空间保存相关信息,然后在运行期和内存释放时利用这些信息做状态检查。</p>\n<p>特别地,如果内存分配函数在返回给调用者使用的内存区域前后保留一些存储块,填入固定的字节序列作为边界标识符,那么就可以在后续程序运行时实时查验,看看前后边界标识符是否被改动。如果是,就说明出现了缓冲区溢出错误,马上报告内存使用存在问题。这些保留的存储块也被称为红区(redzone),言下之意当内存访问越界时,好比程序运行踩了红线,应当告警。</p>\n<h3 id=\"程序目标\">程序目标</h3>\n<p>出于学习的目的,这里演示用C语言编程实现的、支持越界检查功能的内存管理函数。程序要达成的目标是:</p>\n<ul>\n<li>编写自己的内存分配和释放函数,内部封装标准库函数<code>malloc()/free()</code></li>\n<li>分配内存时,在返回的内存区域首尾各添加4字节的红区\n<ul>\n<li>首端(header)写入<code>0x0D0C0B0A</code></li>\n<li>尾部(footer)写入<code>0x12345678</code></li>\n</ul></li>\n<li>写一个审查函数,对给定的内存分配地址进行核对,出错则告警</li>\n<li>释放内存时,再检查红区是否被修改,如是则告警</li>\n<li>编写测试代码验证以上内存管理函数完成所设计的功能</li>\n</ul>\n<h3 id=\"设计实现\">设计实现</h3>\n<p>下面来讲叙程序的实现细节。首先,我们了解标准库函数<code>malloc()</code>返回所分配的内存地址,内存区域大小由输入参数<code>size</code>决定:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> *<span class=\"title\">malloc</span><span class=\"params\">(<span class=\"keyword\">size_t</span> size)</span></span>;</span><br></pre></td></tr></table></figure>\n<p>考虑到首尾各4字节的红区,显然新的内存分配函数必须请求额外的8个字节的内存。但这还不够,要检查尾部红区,审查函数必须知道所分配内存的大小,不然无法定位尾部。所以还要再多分配4个字节,保存内存区域大小<code>size</code>。由此,整个内存分配的结构如下图所示:</p>\n<p><img src=\"memcheck.jpg\" /></p>\n<p>所以实际需要传递给标准库函数<code>malloc()</code>的内存大小值,是申请的内存量加上12。而新的内存分配函数返回的可用内存地址,是<code>malloc()</code>返回的指针加地址偏移量8。掌握这些关键细节之后,新的内存分配函数的实现就呼之欲出了:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* redzone patterns */</span></span><br><span class=\"line\"><span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> header = <span class=\"number\">0x0D0C0B0A</span>;</span><br><span class=\"line\"><span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> footer = <span class=\"number\">0x12345678</span>;</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span>* <span class=\"title\">my_malloc</span> <span class=\"params\">(<span class=\"keyword\">size_t</span> sz)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">void</span> *p = <span class=\"built_in\">malloc</span> (<span class=\"number\">4</span> + <span class=\"number\">4</span> + sz + <span class=\"number\">4</span>);</span><br><span class=\"line\"> *(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> *)p = header;</span><br><span class=\"line\"> *(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> *)(p + <span class=\"number\">4</span>) = sz;</span><br><span class=\"line\"> *(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)(p + <span class=\"number\">8</span> + sz) = footer;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> p + <span class=\"number\">8</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>相应的内存审查和释放函数的实现也就简单了。内存审查函数可以调用断言<code>assert()</code>库函数,确认可用内存大小值和首尾红区字节序列没有被更改。释放内存时做同样的检查,没有差错后再释放。这两个函数的实现如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">my_check</span> <span class=\"params\">(<span class=\"keyword\">void</span> *p, <span class=\"keyword\">size_t</span> sz)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> assert(*(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)(p - <span class=\"number\">8</span>) == header);</span><br><span class=\"line\"> assert(*(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)(p - <span class=\"number\">4</span>) == sz);</span><br><span class=\"line\"> assert(*(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> *)(p + sz) == footer);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">my_free</span> <span class=\"params\">(<span class=\"keyword\">void</span> *p)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">void</span> *real_p = p - <span class=\"number\">8</span>;</span><br><span class=\"line\"> assert(*(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)real_p == header);</span><br><span class=\"line\"> <span class=\"keyword\">size_t</span> sz = *(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)(real_p + <span class=\"number\">4</span>);</span><br><span class=\"line\"> assert(*(<span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span>*)(real_p + <span class=\"number\">8</span> + sz) == footer);</span><br><span class=\"line\"> <span class=\"built_in\">free</span>(real_p);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>接下来就是写测试代码。软件调试和测试时,常常需要以16进制格式打印存储区域的内容,下面的<code>my_hexdump()</code>提供了此项辅助功能(宏<code>HDMP_COLS</code>定义了每行打印16个字节):</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifndef</span> HDMP_COLS</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> HDMP_COLS 16</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">my_hexdump</span> <span class=\"params\">(<span class=\"keyword\">void</span> *mem, <span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> len)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">unsigned</span> <span class=\"keyword\">int</span> i, j, extra;</span><br><span class=\"line\"> extra = (len % HDMP_COLS) ? (HDMP_COLS - len % HDMP_COLS) : <span class=\"number\">0</span>;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> (i = <span class=\"number\">0</span>; i < len + extra; i++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (i % HDMP_COLS == <span class=\"number\">0</span>) {</span><br><span class=\"line\"> <span class=\"comment\">/* print address */</span></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"\\n%p: "</span>, mem + i); </span><br><span class=\"line\"> } </span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">if</span> (i < len) {</span><br><span class=\"line\"> <span class=\"comment\">/* print hex data */</span></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"%02x "</span>, <span class=\"number\">0xFF</span> & ((<span class=\"keyword\">char</span>*)mem)[i]);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> <span class=\"comment\">/* print 3 space chars for alignment */</span></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">" "</span>);</span><br><span class=\"line\"> } </span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (i % HDMP_COLS == (HDMP_COLS - <span class=\"number\">1</span>)) {</span><br><span class=\"line\"> <span class=\"comment\">/* print ASCII dump */</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (j = i - (HDMP_COLS - <span class=\"number\">1</span>); j <= i; j++) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (j >= len) {</span><br><span class=\"line\"> <span class=\"comment\">/* end of block */</span></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"\\n\\n"</span>);</span><br><span class=\"line\"> <span class=\"keyword\">return</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (<span class=\"built_in\">isprint</span>(((<span class=\"keyword\">char</span>*)mem)[j])) {</span><br><span class=\"line\"> <span class=\"comment\">/* printable char */</span></span><br><span class=\"line\"> <span class=\"built_in\">putchar</span>(<span class=\"number\">0xFF</span> & ((<span class=\"keyword\">char</span>*)mem)[j]);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> <span class=\"comment\">/* other char, print '.' instead */</span></span><br><span class=\"line\"> <span class=\"built_in\">putchar</span>(<span class=\"string\">'.'</span>);</span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\"> } </span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>下面代码段显示,主函数先对新的内存管理函数进行正面测试,即没有越界写操作,不会出现断言错误:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">main</span> <span class=\"params\">()</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> size = <span class=\"number\">32</span>;</span><br><span class=\"line\"> <span class=\"keyword\">void</span> *ptr = my_malloc(size);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>( <span class=\"string\">"Usable memory start at %p\\n"</span>, ptr);</span><br><span class=\"line\"> my_hexdump(ptr - <span class=\"number\">8</span>, size + <span class=\"number\">12</span>);</span><br><span class=\"line\"> my_check(ptr, size);</span><br><span class=\"line\"> my_free(ptr);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Basic test passed!\\n"</span>);</span><br><span class=\"line\"> </span><br><span class=\"line\"> strcpy_test();</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>而最后一行,主函数调用<code>strcpy_test()</code>(实现代码如下)。这是一个负面测试函数。它使用不安全的字符串复制库函数<code>strcpy()</code>,复制一个12个字符的字符串到动态分配的缓冲区中,缓冲区的大小也是12个字节。但是,因为<code>strcpy()</code>会连带复制字符串结尾的终止符 ('\\0'),就产生了缓冲区溢出错误。所以,如果新的带有越界检查的释放函数<code>my_free()</code>功能运行正确,我们将会看到程序在此出现断言错误。</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">strcpy_test</span> <span class=\"params\">(<span class=\"keyword\">void</span>)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">char</span> *msg = <span class=\"string\">"Hello world!"</span>;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> mlen = <span class=\"built_in\">strlen</span>(msg);</span><br><span class=\"line\"> <span class=\"keyword\">char</span> *buffer = my_malloc(mlen);</span><br><span class=\"line\"> <span class=\"built_in\">strcpy</span>(buffer, msg);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"%s\\n"</span>, buffer);</span><br><span class=\"line\"> my_hexdump(buffer<span class=\"number\">-8</span>, mlen+<span class=\"number\">12</span>);</span><br><span class=\"line\"> my_free(buffer);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"编译运行\">编译运行</h3>\n<p>在红帽企业Linux 8.1的系统环境下,使用GCC 8.3.1编译链接程序及最后的运行结果是:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">> gcc -v</span><br><span class=\"line\">Using built-in specs.</span><br><span class=\"line\">COLLECT_GCC=gcc</span><br><span class=\"line\">COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper</span><br><span class=\"line\">OFFLOAD_TARGET_NAMES=nvptx-none</span><br><span class=\"line\">OFFLOAD_TARGET_DEFAULT=1</span><br><span class=\"line\">Target: x86_64-redhat-linux</span><br><span class=\"line\">Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --enable-plugin --enable-initfini-array --with-isl --disable-libmpx --enable-offload-targets=nvptx-none --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux</span><br><span class=\"line\">Thread model: posix</span><br><span class=\"line\">gcc version 8.3.1 20190507 (Red Hat 8.3.1-4) (GCC) </span><br><span class=\"line\">></span><br><span class=\"line\">> gcc -o memcheck memcheck.c</span><br><span class=\"line\">> </span><br><span class=\"line\">> memcheck</span><br><span class=\"line\">Usable memory start at 0x1823268</span><br><span class=\"line\"></span><br><span class=\"line\">0x1823260: 0a 0b 0c 0d 20 00 00 00 00 00 00 00 00 00 00 00 .... ...........</span><br><span class=\"line\">0x1823270: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................</span><br><span class=\"line\">0x1823280: 00 00 00 00 00 00 00 00 78 56 34 12 ........xV4.</span><br><span class=\"line\"></span><br><span class=\"line\">Basic <span class=\"built_in\">test</span> passed!</span><br><span class=\"line\">Hello world!</span><br><span class=\"line\"></span><br><span class=\"line\">0x18236b0: 0a 0b 0c 0d 0c 00 00 00 48 65 6c 6c 6f 20 77 6f ........Hello wo</span><br><span class=\"line\">0x18236c0: 72 6c 64 21 00 56 34 12 rld!.V4.</span><br><span class=\"line\"></span><br><span class=\"line\">memcheck: memcheck.c:86: my_free: Assertion `*(unsigned int*)(real_p + 8 + sz)==footer<span class=\"string\">' failed.</span></span><br><span class=\"line\"><span class=\"string\">Abort</span></span><br></pre></td></tr></table></figure>\n<p>可以看到,输出的<code>Basic test passed!</code>表明正面测试通过,之前打印出的存储区域也显示正确的红区字节序列和可用内存区大小值0x20。注意,该主机系统是<code>Little Endian</code>的,所以红区字节序列与程序中定义的次序正好相反。最后是负面测试用例的输出结果,可以看到,使用<code>strcpy()</code>造成了缓冲区溢出,将尾部红区的第一个字节0x78更改为0x00,这一错误被释放函数<code>my_free()</code>抓到了,程序在执行尾部字节序列查验时断言出错,提前退出了(Abort)。</p>\n<p>总结一下,我们这里学习并编程实践了一种基本的内存越界检查方法。虽然其工作原理很简单,但这是理解和应用更高级内存调试工具的基础。类似问题也许会出现在程序员面试中,充分掌握了上述所有内容,你就可以做出准确无误的回答。</p>\n<p>完整的程序可点击这里下载:<a href=\"memcheck.c.gz\">memcheck.c.gz</a></p>\n","categories":["技术小札"],"tags":["C/C++编程","系统编程"]},{"title":"程序员面试题精解(1)— 比特位计数","url":"/2021/05/30/PGITVW-1-bitcount/","content":"<p>现代计算机的硬件设计建立于数字电路的基础之上,而数字电路采用以2为基数的二进制记数系统。由此,二进制及其数字位(称为比特)的运算构成计算机系统软件的基石。一个常见的程序员面试题,就是比特位计数,即计算给定整数的二进制表示中比特1的个数。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>People who deal with bits should expect to get bitten.</strong><br> <strong>— <em>Jon Bentley</em>(乔恩·本特利,美国计算机科学家,计算机科学经典名著《编程珠玑》的作者,也以基于启发式的分区算法k-d树而知名)</strong></p>\n</div>\n<p>这并不是一道很难的题目,但是要给出令人印象深刻的答案,却需要对二进制及比特位运算的深入理解,以及坚实的系统编程技巧和经验。实际上,比特位计数功能有悠久的历史。在早期的晶体管计算机时代,就有专门执行该功能的指令。如果主机的CPU指令集不支持此类指令,就必须使用软件代码实现。由于这一功能也常常被称为“种群统计”(population count),本文中所有解法的函数命名都以<code>popcount_</code>为前缀。下面我们来解析和评介这一面试题的各种解法和C语言实现。</p>\n<h3 id=\"常规解法\">常规解法</h3>\n<p>常规的解法是每一个初级程序员都必须掌握的。以32位整数2418146236为例,</p>\n<ul>\n<li>十六进制表示:0x9021FBBC</li>\n<li>二进制表示:0b 10010000 00100001 11111011 10111100</li>\n<li>比特1的个数:16</li>\n</ul>\n<p>如果熟悉基本的比特位运算操作(<em>取反、与、或、异或、左/右移位</em>),就可以很快找到一个解题的思路:生成一个单比特数0b1,将它与要计数的整数做<em>位与</em>运算,结果为1说明整数最右边的比特为1,否则为0;将此单比特数<em>左移</em>一位得到0b10,再与原整数做<em>位与</em>运算,用结果判定从右边起的第二位比特是否为1;重复此<em>左移</em>和<em>位与</em>运算,就能够从右到左扫描到所有的比特1并计数。如下的代码实现了这一简单直接的解法:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_common1</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> c = <span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> i = <span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (i < <span class=\"number\">32</span>) { </span><br><span class=\"line\"> <span class=\"keyword\">if</span> (n & (<span class=\"number\">1</span> << i))</span><br><span class=\"line\"> c++;</span><br><span class=\"line\"> i++;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> c;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>可以看到,这种解法的运算量只取决于输入整数类型的总比特数,而与整数本身的数值无关。如上的函数定义,因为输入为32位无符号数,所以代码中循环和移位次数也为32。</p>\n<p>与之类似的解法,是固定单比特数0b1,而将输入整数逐次<em>右移</em>,这样所有的高位比特都可以移到最右边来检测。利用比特位<em>右移</em>运算时最左边补上符号位的特点,此解法可能不需要循环32次而提前结束,循环的次数等于最高位比特1与最右边比特的比特距离。以下的代码展示了这一解法:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_common2</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> c = <span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (n) {</span><br><span class=\"line\"> c += n & <span class=\"number\">1</span>;</span><br><span class=\"line\"> n >>= <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> c;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>对于上面的第二种常规解法的C函数实现,有三点要提醒注意:</p>\n<ol type=\"1\">\n<li>解法利用了<em>右移</em>时最左边补上符号位0的特点,所以可以根据移位后数是否为0,来确定需不需要提前结束循环。这是优于第一种常规解法的地方。但是也由此限定了输入n必须为无符号数,否则当输入为负数时,<em>右移</em>时最左边的符号位会移到数据位,而且还会又补上符号位1。这样最终n会变为0xFFFFFFFF,程序落入死循环!解决此问题的办法是,如果函数定义的输入是<code>int32_t</code>,要先将之强制类型赋值到无符号数<code>uint32_t</code>,再执行相同的检测和计数步骤。基于此处理,本文余下内容所提到的整数皆为无符号数。</li>\n<li>以上代码第5行不是应该写成<code>if (n & 1) c++;</code>吗?为什么变成<code>c += n & 1;</code>?其实两种写法结果一样,但实际中后者可能会优于前者。现代计算机处理器都是基于流水线(pipeline)的设计以提高指令执行速度,而<code>if</code>语句编译成的分支指令,会造成流水线冲突而拉低执行效率。当然,依赖于特定的计算机体系结构和编译器优化能力,这一区别也有可能太小乃至可以忽略不计。</li>\n<li>以上代码第6行<em>右移</em>一位,等价于将整数除以2。那么请问可以把它写成<code>n /= 2;</code>吗?绝对不行!这是面试官设置的陷阱。只要学过计算机组成的基础知识,就应该知道除法的执行效率远低于移位运算,所以实际中要尽量避免使用除法。</li>\n</ol>\n<h3 id=\"快速解法\">快速解法</h3>\n<p>实现常规解法可以验证面试者是否拥有初级程序员的基本技能。然而,对于中高级程序员的职位,面试官会要求效率更高的解法。那么,存在更快的解法吗?答案是肯定的。</p>\n<p>考虑一个单字节8比特的整数n, 其数值为0x94,以二进制表示为0b10010100。如下图所示,如果将n减去1,得到0b10010011,即0x93。仔细观察二者的区别,减1后原整数最右边的比特1及后面的比特0全部取反,而其之前的比特位均保持不变。这时,如果将二者做<em>位与</em>运算,结果是0b10010000(0x90),最右的比特1被清除了!重复减1后<em>位与</em>操作,会得到0b10000000 (0x80),原整数从右边数第二个比特1被清除了。第三次同样的操作完成,得到0,即原来的全部三个比特1都被清零。</p>\n<p><img src=\"bitcount1.png\" style=\"width:50.0%;height:50.0%\" /></p>\n<p>总结这一规律:将一个整数减去1,再同原整数做<em>位与</em>运算,得到的结果等于原整数最右边的比特1清零;在最终结果变成0之前,能重复进行这样的操作的次数,就是原整数中比特1的数目。基于这一规律,我们可以写出新的计数函数代码:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_fast1</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> c = <span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (n) {</span><br><span class=\"line\"> c++;</span><br><span class=\"line\"> n &= (n<span class=\"number\">-1</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> c;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>毫无疑问,由于循环的次数取决于比特1的数目,这种解法当然比常规解法要快。在比特1个数不多的场合,这是优选的快速解法。</p>\n<p>无独有偶,还有一种与之对称的解法,适用于比特1个数较多(即比特0较少)的情况。参考下图,整数数值为0xBD,二进制表示为0b10111101。如果将n加上1,得到0b10111110,即0xBE。对比二者,加1后原整数最右边的比特0及后面的比特1全部取反,而其之前的比特位均保持不变。这时,如果将二者做<em>位或</em>运算,结果是0b10111111(0xBF),最右的比特0被置位了!重复加1后<em>位与</em>操作,会得到0b11111111 (0xFF),原整数从右边数第二个比特0被置位了。这时原来的全部两个比特0都被置位,解法终止,原整数中比特1的个数等于总比特数减去重复操作的次数。</p>\n<p><img src=\"bitcount2.png\" style=\"width:50.0%;height:50.0%\" /></p>\n<p>总结新的规律:将一个整数加上1,再同原整数做<em>位或</em>运算,得到的结果等于原整数最右边的比特0置位;在最终结果变成全比特1(等同于有符号数-1)之前,能重复进行这样的操作的次数,就是原整数中比特0的数目,而原整数中比特1的个数等于总比特数减去比特0的数目。这一规律对应的计数函数代码如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_fast2</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> c = <span class=\"number\">32</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (n != <span class=\"number\">-1</span>) {</span><br><span class=\"line\"> c--;</span><br><span class=\"line\"> n |= (n+<span class=\"number\">1</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> c;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>同理,由于循环的次数取决于比特0的数目,在比特1个数较多的场合,这是优选的快速解法。</p>\n<p>在具体的应用场景中,如果要处理的数据量很大,达到GB甚至TB量级,并且已知绝大部分数值的比特1个数都是较少(或较多)的状况,还可以将循环语句展开,进一步加快运行速度。</p>\n<p>以上两种快速解法的循环展开实现如下。代码先定义宏<code>f(x)</code>和<code>g(x)</code>用来检测结果并即时返回计数值,然后在函数实现中拿掉<code>while</code>循环,取而代之的是分别反复调用<code>f(x)</code>和<code>g(x)</code>。在任何一步满足<code>if</code>条件为真,就会马上返回。这实际上是一种<strong>以空间换时间</strong>的方案。虽然函数代码及编译后的指令数目会增加不少,但是在运行过程中,因为每次检测都减少了一个算术运算指令(用于更新计数变量),所以运行会更快。</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* If most bits are 0, use the following unrolled solution.*/</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> f(x) <span class=\"meta-keyword\">if</span> ((n &= (n-1)) == 0) return x;</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_fast1_unrolled</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (n == <span class=\"number\">0</span>) <span class=\"keyword\">return</span> <span class=\"number\">0</span>;</span><br><span class=\"line\"> f( <span class=\"number\">1</span>) f( <span class=\"number\">2</span>) f( <span class=\"number\">3</span>) f( <span class=\"number\">4</span>) f( <span class=\"number\">5</span>) f( <span class=\"number\">6</span>) f( <span class=\"number\">7</span>) f( <span class=\"number\">8</span>)</span><br><span class=\"line\"> f( <span class=\"number\">9</span>) f(<span class=\"number\">10</span>) f(<span class=\"number\">11</span>) f(<span class=\"number\">12</span>) f(<span class=\"number\">13</span>) f(<span class=\"number\">14</span>) f(<span class=\"number\">15</span>) f(<span class=\"number\">16</span>)</span><br><span class=\"line\"> f(<span class=\"number\">17</span>) f(<span class=\"number\">18</span>) f(<span class=\"number\">19</span>) f(<span class=\"number\">20</span>) f(<span class=\"number\">21</span>) f(<span class=\"number\">22</span>) f(<span class=\"number\">23</span>) f(<span class=\"number\">24</span>)</span><br><span class=\"line\"> f(<span class=\"number\">25</span>) f(<span class=\"number\">26</span>) f(<span class=\"number\">27</span>) f(<span class=\"number\">24</span>) f(<span class=\"number\">29</span>) f(<span class=\"number\">30</span>) f(<span class=\"number\">31</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">32</span>;</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">/* If most bits are 1, use the following unrolled solution.*/</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> g(x) <span class=\"meta-keyword\">if</span> ((n |= (n+1)) == -1) return 32-x;</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_fast2_unrolled</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (n == <span class=\"number\">-1</span>) <span class=\"keyword\">return</span> <span class=\"number\">32</span>;</span><br><span class=\"line\"> g( <span class=\"number\">1</span>) g( <span class=\"number\">2</span>) g( <span class=\"number\">3</span>) g( <span class=\"number\">4</span>) g( <span class=\"number\">5</span>) g( <span class=\"number\">6</span>) g( <span class=\"number\">7</span>) g( <span class=\"number\">8</span>)</span><br><span class=\"line\"> g( <span class=\"number\">9</span>) g(<span class=\"number\">10</span>) g(<span class=\"number\">11</span>) g(<span class=\"number\">12</span>) g(<span class=\"number\">13</span>) g(<span class=\"number\">14</span>) g(<span class=\"number\">15</span>) g(<span class=\"number\">16</span>)</span><br><span class=\"line\"> g(<span class=\"number\">17</span>) g(<span class=\"number\">18</span>) g(<span class=\"number\">19</span>) g(<span class=\"number\">20</span>) g(<span class=\"number\">21</span>) g(<span class=\"number\">22</span>) g(<span class=\"number\">23</span>) g(<span class=\"number\">24</span>)</span><br><span class=\"line\"> g(<span class=\"number\">25</span>) g(<span class=\"number\">26</span>) g(<span class=\"number\">27</span>) g(<span class=\"number\">24</span>) g(<span class=\"number\">29</span>) g(<span class=\"number\">30</span>) g(<span class=\"number\">31</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">0</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"分而治之\">分而治之</h3>\n<p>如前所述,快速解法是比特1个数较少或较多的情况下的优选。在平均计数值为总比特数的一半时,仍然需要相当多的指令完成任务。对于前例的<code>popcount_fast1</code>和<code>popcount_fast2</code>函数,平均需要循环16次,每次执行三次算术运算和一次比较/分支操作,最少64条操作指令。还有没有更好的、需要更少指令的通用解法呢?</p>\n<p>当然有!早在40多年以前,三位美国计算机科学家Reingold、Nievergelt和Deo所著的《组合算法:理论和实践》<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>一书中,就详细讨论了一种应用“分而治之”(Divide and Conquer)策略的解法。这种解法是如此精妙,乃至应该将其升格以算法称呼才合适。下图演示了以16位的双字节数0xBFA6为例的计算过程,算法首先把相邻的两个比特位组合为一个位段,将这两个比特位的值相加,结果置于这个宽度为2的比特位段中。下一步把相邻的两个双比特位段的值相加,结果置于宽度为4的比特位段中。以此类推,四步之后就得到数值11(0x000B),正好就是原整数中比特1的总数。</p>\n<p><img src=\"bitcount3.png\" style=\"width:70.0%;height:70.0%\" /></p>\n<p>这一看似帽子戏法的算法,是“分而治之”策略的典型应用。一个16位整数的比特1计数问题,被转化为两个8位整数的计数问题,分别解决后再将结果相加合并。递归运用此策略,直至分解到单个比特计数的问题。然后反复运用并行位运算求解再合并,即可求出最终的计数值。很明显,给定N比特位整数,此算法的时间复杂性是<span class=\"math inline\">\\(O(\\log N)\\)</span>。</p>\n<p>对于32位的整数,上图中的算法用C语言写出来就是:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* Divide-and-Conquer - original (20 arithmetic operations) */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_dnq1</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x55555555</span>) + ((n >> <span class=\"number\">1</span>) & <span class=\"number\">0x55555555</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x33333333</span>) + ((n >> <span class=\"number\">2</span>) & <span class=\"number\">0x33333333</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x0F0F0F0F</span>) + ((n >> <span class=\"number\">4</span>) & <span class=\"number\">0x0F0F0F0F</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x00FF00FF</span>) + ((n >> <span class=\"number\">8</span>) & <span class=\"number\">0x00FF00FF</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x0000FFFF</span>) + ((n >> <span class=\"number\">16</span>) & <span class=\"number\">0x0000FFFF</span>);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> n;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>值得说明的一点是,此函数实现中的圆括号都是必要的。这是因为在C语言运算符的优先级次序表里,加减(<code>+</code>、<code>-</code>)要高于移位(<code><<</code>、<code>>></code>),而后者又高于比特位运算(<code>&</code>、<code>^</code>、<code>|</code>)<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>。在<code>n >> 1</code>两边加圆括号则有助于增强代码的可读性。</p>\n<p>另外,函数的第一行(上面代码段行号4)按照算法描述本来应该写成<code>(n & 0xAAAAAAAA) >> 1</code>,之所以这么写是为了避免在寄存器中生成两个大的常数。如果使用0xAAAAAAAA,在不支持单个“and not”指令的计算机里,就必须多耗费一条指令先将0x55555555<em>取反</em>,再与n做<em>位与</em>运算。后面四行的写法也出于同样的考虑。统计下来,这种“分而治之”算法的实现运算量恒定,总共只有20次算术运算操作,运行速度达到了快速解法平均速度的三倍多!</p>\n<p>到这里,如果面试者对以上全部的解法都有清晰的思路并写下无错的代码,应该可以得到面试官的首肯。再往下的知识点,就是属于能给面试官带来惊喜的技能了。</p>\n<h3 id=\"锦上添花\">锦上添花</h3>\n<p>问还可以更快吗?</p>\n<p>是的,上述“分而治之”算法的代码实现还能进一步优化:</p>\n<ol type=\"1\">\n<li>仔细审查函数<code>popcount_dnq1</code>返回语句之前的三行(代码段行号6、7、8),你会发现一些<em>位与</em>操作是多余的。一则,在这些步骤中比特位段求和的结果不可能向左边的比特位段进位;二则,在最后一步,高位的比特值是可忽略的,计数结果只在最右边的m位比特中,这里<span class=\"math inline\">\\(m=\\log N+1\\)</span>,所以可以省却一个<em>位与</em>运算。</li>\n<li>函数<code>popcount_dnq1</code>的第一行(代码段行号4)还有另一种写法,可以减少一次算术运算。其依据是下面的“种群统计”数学公式<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>: <span class=\"math display\">\\[\\operatorname{popcount}(n)=n-\\Bigl\\lfloor{\\frac{n}{2}}\\Bigr\\rfloor-\\Bigl\\lfloor\\frac{n}{4}\\Bigr\\rfloor-\\cdots-\\Bigl\\lfloor\\frac{n}{2^{N-1}}\\Bigr\\rfloor\\tag{1}\\]</span> N为整数n的比特位数目,<span class=\"math inline\">\\(\\lfloor\\space\\rfloor\\)</span>为向下取整操作。此公式的前两项可以用来并行计算宽度为2的比特位段,以一个四比特位数为例: <span class=\"math display\">\\[\\begin{align}\nn&=a_3⋅2^3+a_2⋅2^2+a_1⋅2^1+a_0⋅2^0\\tag{2}\\\\\n\\Bigl\\lfloor{\\frac{n}{2}}\\Bigr\\rfloor&=a_3⋅2^2+a_2⋅2^1+a_1⋅2^0\\tag{3}\\\\\n\\Bigl\\lfloor{\\frac{n}{2}}\\Bigr\\rfloor\\space\\&\\space0\\mathbf{x}5&=a_3⋅2^2+a_1⋅2^0\\tag{4}\\\\\nn-(\\Bigl\\lfloor{\\frac{n}{2}}\\Bigr\\rfloor\\space\\&\\space0\\mathbf{x}5)&=a_3⋅(2^3-2^2)+a_2⋅2^2+a_1⋅(2^1-2^0)+a_0⋅2^0\\\\\n&=(a_3+a_2)⋅2^2+(a_1+a_0)⋅2^0\\tag{5}\n\\end{align}\\]</span> 显然上式(3)等同于将n右移一位。稍加思考,也可以推断出上式(5)得到就是宽度为2的比特位段计数值。依据这一分析,我们可以重写第一行为<code>n = n - ((n >> 1) & 0x55555555);</code></li>\n</ol>\n<p>综合上述两点,优化后的代码如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* Divide-and-Conquer - improved (15 arithmetic operations) */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_dnq2</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> n = n - ((n >> <span class=\"number\">1</span>) & <span class=\"number\">0x55555555</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x33333333</span>) + ((n >> <span class=\"number\">2</span>) & <span class=\"number\">0x33333333</span>);</span><br><span class=\"line\"> n = (n + (n >> <span class=\"number\">4</span>)) & <span class=\"number\">0x0F0F0F0F</span>;</span><br><span class=\"line\"> n = n + (n >> <span class=\"number\">8</span>);</span><br><span class=\"line\"> n = n + (n >> <span class=\"number\">16</span>);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> n & <span class=\"number\">0x3F</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>新的<code>popcount_dnq2</code>函数实现比<code>popcount_dnq1</code>少了5个算术运算操作,速度加快了25%。</p>\n<p>还有优化的余地吗?</p>\n<p>真的有!有经验的系统程序员都知道,许多现代计算机处理器的设计都支持单条乘法指令。由此把一个数乘以0x01010101,等于将其每个长度为8的比特位段相加求和,结果置于最高的字节中,所以只要右移24位就可以得到我们要的计数值。根据这一点,我们可以得到“分而治之”算法的最优实现版本:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* Divide-and-Conquer - final (12 arithmetic operations) */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_dnq3</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> n = n - ((n >> <span class=\"number\">1</span>) & <span class=\"number\">0x55555555</span>);</span><br><span class=\"line\"> n = (n & <span class=\"number\">0x33333333</span>) + ((n >> <span class=\"number\">2</span>) & <span class=\"number\">0x33333333</span>);</span><br><span class=\"line\"> n = (n + (n >> <span class=\"number\">4</span>)) & <span class=\"number\">0x0F0F0F0F</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (n * <span class=\"number\">0x01010101</span>) >> <span class=\"number\">24</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>它只需要12次算术运算操作就完成任务,真的非常快!</p>\n<p>使用GCC编译器特定选项,可以生成混合C代码及对照汇编语言的列表(list)文件,用来验证<code>popcount_dnq3</code>实现的优越性。在运行于Intel处理器的Ubuntu Linux x86-64虚拟机上,执行以下命令:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">gcc -Wa,-adhln -g -march=native popcount.c > popcount.s</span><br></pre></td></tr></table></figure>\n<p>然后打开产生的列表文件<code>popcount.s</code>,找到<code>popcount_dnq2</code>和<code>popcount_dnq3</code>相关的部分:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"number\">106</span>:popcount.<span class=\"function\">c **** <span class=\"keyword\">int</span> <span class=\"title\">popcount_dnq2</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\">107:popcount.c **** </span>{</span><br><span class=\"line\">...</span><br><span class=\"line\"><span class=\"number\">111</span>:popcount.c **** n = n + (n >> <span class=\"number\">8</span>);</span><br><span class=\"line\"><span class=\"number\">972</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">111</span> <span class=\"number\">16</span></span><br><span class=\"line\"><span class=\"number\">973</span> <span class=\"number\">0786</span> <span class=\"number\">8B</span>45FC \t\tmovl\t<span class=\"number\">-4</span>(%rbp), %eax</span><br><span class=\"line\"><span class=\"number\">974</span> <span class=\"number\">0789</span> C1E808 \t\tshrl\t$<span class=\"number\">8</span>, %eax</span><br><span class=\"line\"><span class=\"number\">975</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">111</span> <span class=\"number\">7</span></span><br><span class=\"line\"><span class=\"number\">976</span> <span class=\"number\">078</span>c <span class=\"number\">0145F</span>C \t\taddl\t%eax, <span class=\"number\">-4</span>(%rbp)</span><br><span class=\"line\"><span class=\"number\">112</span>:popcount.c **** n = n + (n >> <span class=\"number\">16</span>);</span><br><span class=\"line\"><span class=\"number\">977</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">112</span> <span class=\"number\">16</span></span><br><span class=\"line\"><span class=\"number\">978</span> <span class=\"number\">078f</span> <span class=\"number\">8B</span>45FC \t\tmovl\t<span class=\"number\">-4</span>(%rbp), %eax</span><br><span class=\"line\"><span class=\"number\">979</span> <span class=\"number\">0792</span> C1E810 \t\tshrl\t$<span class=\"number\">16</span>, %eax</span><br><span class=\"line\"><span class=\"number\">980</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">112</span> <span class=\"number\">7</span></span><br><span class=\"line\"><span class=\"number\">981</span> <span class=\"number\">0795</span> <span class=\"number\">0145F</span>C \t\taddl\t%eax, <span class=\"number\">-4</span>(%rbp)</span><br><span class=\"line\"><span class=\"number\">113</span>:popcount.c **** <span class=\"keyword\">return</span> n & <span class=\"number\">0x3F</span>;</span><br><span class=\"line\"><span class=\"number\">982</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">113</span> <span class=\"number\">14</span></span><br><span class=\"line\"><span class=\"number\">983</span> <span class=\"number\">0798</span> <span class=\"number\">8B</span>45FC \t\tmovl\t<span class=\"number\">-4</span>(%rbp), %eax</span><br><span class=\"line\"><span class=\"number\">984</span> <span class=\"number\">079b</span> <span class=\"number\">83E03</span>F \t\tandl\t$<span class=\"number\">63</span>, %eax</span><br><span class=\"line\">...</span><br><span class=\"line\"><span class=\"number\">117</span>:popcount.<span class=\"function\">c **** <span class=\"keyword\">int</span> <span class=\"title\">popcount_dnq3</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\">118:popcount.c **** </span>{</span><br><span class=\"line\">...</span><br><span class=\"line\"><span class=\"number\">122</span>:popcount.<span class=\"function\">c **** <span class=\"title\">return</span> <span class=\"params\">(n * <span class=\"number\">0x01010101</span>)</span> >> 24</span>;</span><br><span class=\"line\"><span class=\"number\">1034</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">122</span> <span class=\"number\">15</span></span><br><span class=\"line\"><span class=\"number\">1035</span> <span class=\"number\">07e7</span> <span class=\"number\">8B</span>45FC \t\tmovl\t<span class=\"number\">-4</span>(%rbp), %eax</span><br><span class=\"line\"><span class=\"number\">1036</span> <span class=\"number\">07</span>ea <span class=\"number\">69</span>C00101 \t\timull\t$<span class=\"number\">16843009</span>, %eax, %eax</span><br><span class=\"line\"><span class=\"number\">1036</span> <span class=\"number\">0101</span></span><br></pre></td></tr></table></figure>\n<p>可以看到,<code>popcount_dnq2</code>实现里右移8/16位相加各自有三条指令(行号973、974、976和978、979、981),最后的<em>位与</em>0x3F有两条指令(行号983、984),一共8条指令;而在<code>popcount_dnq3</code>实现中只有两条:一条加载指令<code>movl</code>和一条乘法指令<code>imull</code>。<code>popcount_dnq3</code>完胜!</p>\n<p>还有一种<strong>以空间换时间</strong>的“分而治之”方案,虽然看上去比较无趣,但是其执行效率却可与上面的其他算法比肩。这种解决方案预设一个256项的数组,填入0~255之间每个数的比特位计数值,之后对于输入的整数依次取出单个字节查表,然后将结果相加。以下是无比较/分支指令的代码实现:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">const</span> <span class=\"keyword\">char</span> popcount_tab[<span class=\"number\">256</span>] =</span><br><span class=\"line\">{</span><br><span class=\"line\"> <span class=\"number\">0</span>,<span class=\"number\">1</span>,<span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,</span><br><span class=\"line\"> <span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,</span><br><span class=\"line\"> <span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,</span><br><span class=\"line\"> <span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,</span><br><span class=\"line\"> <span class=\"number\">1</span>,<span class=\"number\">2</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,</span><br><span class=\"line\"> <span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,</span><br><span class=\"line\"> <span class=\"number\">2</span>,<span class=\"number\">3</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,</span><br><span class=\"line\"> <span class=\"number\">3</span>,<span class=\"number\">4</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,<span class=\"number\">4</span>,<span class=\"number\">5</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,<span class=\"number\">5</span>,<span class=\"number\">6</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,<span class=\"number\">6</span>,<span class=\"number\">7</span>,<span class=\"number\">7</span>,<span class=\"number\">8</span></span><br><span class=\"line\">};</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">/* Table lookup method */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">popcount_tablelookup</span> <span class=\"params\">(<span class=\"keyword\">uint32_t</span> n)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">return</span> popcount_tab[n & <span class=\"number\">0xFF</span>] +</span><br><span class=\"line\"> popcount_tab[(n >> <span class=\"number\">8</span>) & <span class=\"number\">0xFF</span>] +</span><br><span class=\"line\"> popcount_tab[(n >> <span class=\"number\">16</span>) & <span class=\"number\">0xFF</span>] +</span><br><span class=\"line\"> popcount_tab[(n >> <span class=\"number\">24</span>)];</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>透彻掌握本节的内容,一定可以让面试官对你刮目相看,因为你写出的代码,实现的就是广为认可的高效算法。不信去下载GCC最新的11.1.0版本,看看库文件<code>libgcc/libgcc2.c</code>里的比特位计数函数(复制如下),与前面的代码实质上是一样的:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">int</span></span><br><span class=\"line\">__popcountSI2 (UWtype x)</span><br><span class=\"line\">{</span><br><span class=\"line\"> <span class=\"comment\">/* Force table lookup on targets like AVR and RL78 which only</span></span><br><span class=\"line\"><span class=\"comment\"> pretend they have LIBGCC2_UNITS_PER_WORD 4, but actually</span></span><br><span class=\"line\"><span class=\"comment\"> have 1, and other small word targets. */</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">if</span> __SIZEOF_INT__ > 2 && defined (POPCOUNTCST) && __CHAR_BIT__ == 8</span></span><br><span class=\"line\"> x = x - ((x >> <span class=\"number\">1</span>) & POPCOUNTCST (<span class=\"number\">0x55</span>)); </span><br><span class=\"line\"> x = (x & POPCOUNTCST (<span class=\"number\">0x33</span>)) + ((x >> <span class=\"number\">2</span>) & POPCOUNTCST (<span class=\"number\">0x33</span>));</span><br><span class=\"line\"> x = (x + (x >> <span class=\"number\">4</span>)) & POPCOUNTCST (<span class=\"number\">0x0F</span>);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (x * POPCOUNTCST (<span class=\"number\">0x01</span>)) >> (W_TYPE_SIZE - __CHAR_BIT__);</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">else</span> </span></span><br><span class=\"line\"> <span class=\"keyword\">int</span> i, ret = <span class=\"number\">0</span>;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (i = <span class=\"number\">0</span>; i < W_TYPE_SIZE; i += <span class=\"number\">8</span>)</span><br><span class=\"line\"> ret += __popcount_tab[(x >> i) & <span class=\"number\">0xff</span>];</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> ret;</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"总结和应用\">总结和应用</h3>\n<p>下表总结比较了本文全部的解法和算法的函数实现:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">分类</th>\n<th style=\"text-align: center;\">函数名</th>\n<th style=\"text-align: center;\">方法概要</th>\n<th style=\"text-align: center;\">性能</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">常规解法</td>\n<td style=\"text-align: center;\">popcount_common1</td>\n<td style=\"text-align: center;\">从右到左单比特扫描</td>\n<td style=\"text-align: center;\">最慢</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">常规解法</td>\n<td style=\"text-align: center;\">popcount_common2</td>\n<td style=\"text-align: center;\">输入移位单比特扫描</td>\n<td style=\"text-align: center;\">慢</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">快速解法</td>\n<td style=\"text-align: center;\">popcount_fast1</td>\n<td style=\"text-align: center;\">减一再<em>位与</em>清零最右1,适用较少1</td>\n<td style=\"text-align: center;\">快(非通用)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">快速解法</td>\n<td style=\"text-align: center;\">popcount_fast2</td>\n<td style=\"text-align: center;\">加一再<em>位或</em>置位最右0,适用较少0</td>\n<td style=\"text-align: center;\">快(非通用)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">快速解法</td>\n<td style=\"text-align: center;\">popcount_fast1_unrolled</td>\n<td style=\"text-align: center;\">fast1循环展开(以空间换时间)</td>\n<td style=\"text-align: center;\">更快(非通用)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">快速解法</td>\n<td style=\"text-align: center;\">popcount_fast2_unrolled</td>\n<td style=\"text-align: center;\">fast2循环展开(以空间换时间)</td>\n<td style=\"text-align: center;\">更快(非通用)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">分而治之</td>\n<td style=\"text-align: center;\">popcount_dnq1</td>\n<td style=\"text-align: center;\">位段并行计数合并、递归</td>\n<td style=\"text-align: center;\">快(通用)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">分而治之</td>\n<td style=\"text-align: center;\">popcount_dnq2</td>\n<td style=\"text-align: center;\">位段并行计数合并、递归(优化)</td>\n<td style=\"text-align: center;\">更快(通用)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">分而治之</td>\n<td style=\"text-align: center;\">popcount_dnq3</td>\n<td style=\"text-align: center;\">位段并行计数合并、递归(最优化)</td>\n<td style=\"text-align: center;\">最快(通用)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">分而治之</td>\n<td style=\"text-align: center;\">popcount_tablelookup</td>\n<td style=\"text-align: center;\">单字节查表法(以空间换时间)</td>\n<td style=\"text-align: center;\">最快(通用)</td>\n</tr>\n</tbody>\n</table>\n<p>比特位计数当然不仅仅是用来考验面试者的,它广泛应用于信息学、纠错编码和密码学等多个领域。实际上,“种群统计”就是二进制符号数据的汉明重量<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>。而两个数据比特串的<strong>汉明距离</strong>,就定义为二者对应位置上不同比特的个数,即二者相<em>异或</em>后的汉明重量。基于汉明距离的分析是密码分析学的一个关键技术,是历史上破解许多密码的关键。从比特位计数的算法,也衍生出一些快速计算汉明距离的算法,感兴趣者可以留言讨论。</p>\n<p>事实上,比特位计数是如此重要,乃至GCC和Clang编译器都提供了内建库函数<code>__builtin_popcount</code>。Intel、AMD和ARM处理器也都为之设定了专门的指令。如下在运行于Intel处理器的Ubuntu Linux x86-64虚拟机上,指定GCC选项<code>-march=native</code>后<code>__builtin_popcount</code>函数编译成单一指令<code>popcntl</code>:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"number\">159</span>:popcount.c **** count = __builtin_popcount(num);</span><br><span class=\"line\"><span class=\"number\">1204</span> \t\t.loc <span class=\"number\">1</span> <span class=\"number\">159</span> <span class=\"number\">17</span></span><br><span class=\"line\"><span class=\"number\">1205</span> <span class=\"number\">08b</span>4 F30FB845 \t\tpopcntl\t<span class=\"number\">-8</span>(%rbp), %eax</span><br></pre></td></tr></table></figure>\n<p>对比特位计数功能应用方面的了解,有助于扩展知识面,会成为面试时的加分项。</p>\n<p>完整程序(包含以上所有计数函数及测试代码)和示例列表文件的压缩包在此下载:<a href=\"popcount.tar.gz\">popcount.tar.gz</a></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>Reingold, Edward M., Nievergelt, Jurg, and Deo, Narsingh. <em>Combinational Algorithms: Theory and Practice</em>. Prentice-Hall, 1977.<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>比特位<em>取反</em>操作(<code>~</code>)是一个例外,C语言定义其优先级高于乘除、取余和加减运算。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>这个公式的证明不难,需要讨论的请在评论区留言。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>命名于美国数学家理查德·汉明(Richard Hamming),他对计算机和通信工程贡献突出。<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["面试指南"],"tags":["C/C++编程","系统编程"]},{"title":"程序员面试题精解(2)— 平方根运算","url":"/2021/07/23/PGITVW-2-sqrt/","content":"<p>遇到面试题“实现开平方根的函数”时,如果回答调用库函数<code>sqrt()</code>就可以了,那你就会错意了。很显然,面试官要求你实现自己的平方根运算函数。这时,如果再问为什么要自己写,那你的这次面试就危险了😌。<span id=\"more\"></span>因为你忘记了,电子计算机最初就是为了因应科学计算的需求才发明出来的。掌握基本科学计算的算法,是程序员必备的技能。</p>\n<div class=\"note success no-icon\"><p><strong>The purpose of computing is insight, not numbers.</strong><br> <strong>— <em>Richard Hamming</em>(理查德·汉明,美国数学家,在计算机科学和通信工程领域贡献突出,美国计算机协会(ACM)的创立人之一,1968年图灵奖得主)</strong></p>\n</div>\n<h3 id=\"整数平方根\">整数平方根</h3>\n<p>理解这道面试题的意义之后,我们先来看看较简单的求整数的平方根问题。数学上,“整数平方根”函数定义为<span class=\"math inline\">\\(\\lfloor\\sqrt{x}\\rfloor\\)</span>,即对给定正整数的平方根执行向下取整操作,得出不大于该值的最大正整数。如果这样的定义读着有点拗口,那么就来看看刷题网站<a href=\"https://leetcode-cn.com/problems/sqrtx/\">力扣</a>(<a href=\"https://leetcode.com/problems/sqrtx/\">LeetCode</a>)上的问题描述:</p>\n<blockquote>\n<p>实现 int sqrt(int x) 函数。<br />\n计算并返回 x 的平方根,其中 x 是非负整数。<br />\n由于返回类型是整数,结果只保留整数的部分,小数部分将被舍去。<br />\n<strong>示例 1:</strong><br />\n<span class=\"math inline\">\\(\\quad\\)</span> <strong>输入:</strong> x = 4<br />\n<span class=\"math inline\">\\(\\quad\\)</span> <strong>输出:</strong> 2<br />\n<strong>示例 2:</strong><br />\n<span class=\"math inline\">\\(\\quad\\)</span> <strong>输入:</strong> x = 8<br />\n<span class=\"math inline\">\\(\\quad\\)</span> <strong>输出:</strong> 2<br />\n<span class=\"math inline\">\\(\\quad\\)</span> <strong>说明:</strong> 8 的平方根是 2.82842..., 由于返回类型是整数,小数部分将被舍去。</p>\n</blockquote>\n<p>这道题的难度级别是“容易”,但是现实中发现不少求职者都在此栽倒。一个不需要思考的解法就是暴力搜索:从数值1开始自乘再比较,如果小于输入值就逐次加一重复此过程,直到结果相等或大于输入值。结果相等就直接输出当前值,结果大于输入值就输出当前值减一。暴力搜索解法的时间复杂度是<span class=\"math inline\">\\(\\mathrm {O} (\\sqrt{n})\\)</span>。对于一个32位整数,可能需要64万多次乘法,这无疑太慢了。</p>\n<p>另一个看起来似乎很机智的方法,是利用等差数列的求和公式 <span class=\"math display\">\\[\\sum_{i=1}^n (2i-1)=1+3+5+\\cdots+(2n-1)=n^2\\]</span> 这样简单地从1开始累加奇数并比较,循环往复就可以找到整数平方根。它的好处是每次都用到了上一轮的结果,而且移位加减显然比乘法快。可惜,它的时间复杂度仍然是 <span class=\"math inline\">\\(\\mathrm {O} (\\sqrt{n})\\)</span>,面试官还是会拒绝这样的答案,要求更有时间效率的解法。</p>\n<h4 id=\"二分查找法\">二分查找法</h4>\n<p>整数序列本身是有序的,所以一定可以应用二分查找算法。具体应用到这个问题,我们需要首先设定上下两个边界,然后将猜测值设置为二者之间的中点。若中点的平方大于输入参数,将上界移到中点,否则将下界移到中点。循环重复直到上下边界值差1时,算法结束,下界数值就是我们要的输出。</p>\n<p>虽然二分查找法仍然要执行乘法操作,但是其时间复杂度缩减为<span class=\"math inline\">\\(\\mathrm {O}(\\log n)\\)</span>。32位整数输入最多需要16次乘法,这是非常快的。要注意的是初始值的选择,必须保证每次循环时有:</p>\n<ul>\n<li>下界 <span class=\"math inline\">\\(low\\leqslant \\lfloor\\sqrt{x}\\rfloor+1\\)</span></li>\n<li>上界 <span class=\"math inline\">\\(high\\geqslant \\lfloor\\sqrt{x}\\rfloor\\)</span></li>\n</ul>\n<p>不然算法不能收敛。一个合理且实用的选择是:<span class=\"math inline\">\\(low=1,high=x\\div32+8\\)</span>。另外,上界也不能超过输入的无符号整数类型的最大值的平方根,否则会产生乘法溢出。据此,二分查找法的完整C语言代码如下(提交LeetCode后通过无误):</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* integer square root - bisection method */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">uint32_t</span> <span class=\"title\">isqrt_bist</span><span class=\"params\">(<span class=\"keyword\">uint32_t</span> a)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">uint32_t</span> low, high, mid;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a <= <span class=\"number\">1</span>) <span class=\"keyword\">return</span> a;</span><br><span class=\"line\"></span><br><span class=\"line\"> low = <span class=\"number\">1</span>;</span><br><span class=\"line\"> high = (a >> <span class=\"number\">5</span>) + <span class=\"number\">8</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (high > <span class=\"number\">65535</span>) high = <span class=\"number\">65535</span>; <span class=\"comment\">/* adjust upper bound */</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (high >= low) {</span><br><span class=\"line\"> mid = (low + high) >> <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (mid * mid > a) {</span><br><span class=\"line\"> high = mid - <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> high;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h4 id=\"牛顿迭代法\">牛顿迭代法</h4>\n<p>第二种知名的高效解法就是牛顿迭代法,是基于数值分析中在实数域上近似求解方程的牛顿-拉弗森方法(Newton-Raphson method)。这里概要介绍一下其原理:假定一个可导的实变量函数 <span class=\"math inline\">\\(f(x)\\)</span>,求满足 <span class=\"math inline\">\\(f(x)=0\\)</span> 的 <span class=\"math inline\">\\(x\\)</span> 值,即函数的零点。只要先估计一个与零点相近的值 <span class=\"math inline\">\\(x_n\\)</span>,代入下面的公式,就可以得到下一个更为接近的估算值 <span class=\"math inline\">\\(x_{n+1}\\)</span>: <span class=\"math display\">\\[x_{n+1}=x_n-{\\frac {f(x_n)}{f'(x_n)}}\\tag{1}\\]</span> 这个公式是怎么得出来的呢?很简单!我们知道导数 <span class=\"math inline\">\\(f'(x_n)\\)</span> 的数值就是函数 <span class=\"math inline\">\\(f(x)\\)</span> 在 <span class=\"math inline\">\\(x=x_n\\)</span>处切线的斜率。如下图所示,将切线与 <span class=\"math inline\">\\(x\\)</span> 轴的交点记为 <span class=\"math inline\">\\(x_{n+1}\\)</span>,可以看到 <span class=\"math inline\">\\(x_{n+1}\\)</span>比 <span class=\"math inline\">\\(x=x_n\\)</span>更接近零点。</p>\n<p><img src=\"Newton_iteration.png\" /></p>\n<p>依据斜率的定义有: <span class=\"math display\">\\[f'(x_n)=\\frac {f(x_n)}{x_n-x_{n+1}}\\tag{2}\\]</span></p>\n<p>显然公式(1)就是上式(2)的变体,证毕。</p>\n<p>初等数学的知识告诉我们,计算实数 <span class=\"math inline\">\\(a\\)</span> 的平方根等同于求函数 <span class=\"math inline\">\\(f(x)=x^2-a\\)</span> 的零点。套用公式(1)推导出: <span class=\"math display\">\\[x_{n+1}=x_{n}-\\frac{x_n^2-a}{2x_n}=\\frac{1}{2}(x_n+\\frac{a}{x_n})\\tag{3}\\]</span></p>\n<p>这就是计算 <span class=\"math inline\">\\(\\sqrt a\\)</span> 的迭代公式。此牛顿迭代式呈平方收敛,每轮迭代之后,精确数位的个数翻倍。</p>\n<p>令人惊喜的是,数学家证明了牛顿迭代法对求整数平方根一样有效。这时的迭代公式是: <span class=\"math display\">\\[x_{n+1}=\\lfloor(x_n+\\lfloor a/x_n\\rfloor)/2\\rfloor\\tag{4}\\]</span> 其中 <span class=\"math inline\">\\(x_{n+1}\\)</span>、<span class=\"math inline\">\\(x_n\\)</span> 和 <span class=\"math inline\">\\(a\\)</span> 均为正整数,而收敛的条件是:以 <span class=\"math inline\">\\(x_0\\geqslant\\lfloor\\sqrt a\\rfloor\\)</span> 开始,当 <span class=\"math inline\">\\(x_{n+1}\\geqslant x_n\\)</span> 时序列收敛,<span class=\"math inline\">\\(x_n\\)</span> 就是我们要的整数平方根。实现牛顿迭代法求整数平方根时的难点,在于要确保首次估值满足以上的初始条件。一个有效的办法,是将首次估值 <span class=\"math inline\">\\(x_0\\)</span> 设成不小于 <span class=\"math inline\">\\(\\sqrt a\\)</span> 且值最小的2的幂。下面给出C函数代码实现:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* integer square root - Newton iteration */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">uint32_t</span> <span class=\"title\">isqrt_nwtn</span><span class=\"params\">(<span class=\"keyword\">uint32_t</span> a)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">uint32_t</span> a1;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> s, x0, x1;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a <= <span class=\"number\">1</span>) <span class=\"keyword\">return</span> a;</span><br><span class=\"line\"></span><br><span class=\"line\"> s = <span class=\"number\">1</span>;</span><br><span class=\"line\"> a1 = a - <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a1 > <span class=\"number\">65535</span>) { s += <span class=\"number\">8</span>; a1 >>= <span class=\"number\">16</span>; }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a1 > <span class=\"number\">255</span>) { s += <span class=\"number\">4</span>; a1 >>= <span class=\"number\">8</span>; }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a1 > <span class=\"number\">15</span>) { s += <span class=\"number\">2</span>; a1 >>= <span class=\"number\">4</span>; }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a1 > <span class=\"number\">3</span>) { s += <span class=\"number\">1</span>; }</span><br><span class=\"line\"></span><br><span class=\"line\"> x0 = <span class=\"number\">1</span> << s; <span class=\"comment\">/* first guess 2**s */</span></span><br><span class=\"line\"> x1 = (x0 + (a >> s)) >> <span class=\"number\">1</span>; <span class=\"comment\">/* x1 = (x0+a/x0)/2 */</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (x1 < x0) {</span><br><span class=\"line\"> x0 = x1;</span><br><span class=\"line\"> x1 = (x0 + (a/x0)) >> <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x0;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>说明几点:</p>\n<ol type=\"1\">\n<li>第9到16行实现首次估值的设置,这是一个利用比较和移位操作的精巧设计!变量<code>s</code>保存不小于 <span class=\"math inline\">\\(\\sqrt a\\)</span> 且值最小的2的幂指数,<code>1 << s</code>(<span class=\"math inline\">\\(2^s\\)</span>)就是首次估值!</li>\n<li>如果存在快速的前导0计数指令或函数<code>nlz()</code>,第9到14行可以用<code>s = 16 - nlz(a - 1)/2</code><a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>代替。</li>\n<li>如第17行所示,因为首次估值是2的幂,第一次迭代里的除法用移位代替。</li>\n<li>此算法最多执行5次除法,当 <span class=\"math inline\">\\(a\\leqslant(2^{24}-1)\\)</span> 时,最多4次。</li>\n</ol>\n<h4 id=\"移位相减法\">移位相减法</h4>\n<p>求平方根的解法其实很多,有一种<a href=\"https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Digit-by-digit_calculation\">逐个数位的计算方法</a>特别适合于手算(或心算),而且还适用于任意进位制。这种方法包括内在的搜索和测试循环,从高到低逐级判定单个数位。其基本的计算公式是二项平方展开式: <span class=\"math display\">\\[(r+e)^2=r^2+2re+e^2\\le x\\]</span> 给定当前的 <span class=\"math inline\">\\(r\\)</span>,找到下一个数位 <span class=\"math inline\">\\(e\\)</span>,使得结果最接近 <span class=\"math inline\">\\(x\\)</span>。</p>\n<p>应用到二进制数位系统,搜索和测试的过程变得更高效,因为单个比特位都是2的幂,所有乘积都可以用快速比特移位操作实现。James Ulery写过一篇短文<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>,详细地推演了计算整数平方根的二进制算法。下面以整数200为例,演绎计算过程:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">a0=200 (0xC8), x0=0, m0=0x40</span><br><span class=\"line\">b_n=x_n+m_n, x_{n+1}=x_n>>1</span><br><span class=\"line\"><span class=\"keyword\">if</span> a_n>b_n; a_{n+1}=a_n-b_n, x_{n+1}=x_{n+1}+m_n</span><br><span class=\"line\"><span class=\"keyword\">else</span> a_{n+1}=a_n, x_{n+1} no change</span><br><span class=\"line\">m_{n+1}=m_n>>2</span><br><span class=\"line\"></span><br><span class=\"line\"> 1100 1000 a0 0000 0000 x0 0100 0000 m0</span><br><span class=\"line\">- 1 b0 0000 0000 x1 (x0>>1)</span><br><span class=\"line\">-----------</span><br><span class=\"line\"> 1000 1000 a1 0100 0000 x1 0001 0000 m1</span><br><span class=\"line\">- 101 b1 0010 0000 x2 (x1>>1)</span><br><span class=\"line\">-----------</span><br><span class=\"line\"> 0011 1000 a2 0011 0000 x2 0000 0100 m2</span><br><span class=\"line\">- 11 01 b2 0001 1000 x3 (x2>>1)</span><br><span class=\"line\">-----------</span><br><span class=\"line\"> 0000 0100 a3 0001 1100 x3 0000 0001 m3</span><br><span class=\"line\">- 1 1101 b3 0000 1110 x4 (x3>>1)</span><br><span class=\"line\">----------- (Cannot substract)</span><br><span class=\"line\"> 0000 0100 a4 0000 1110 x4 (stop since m=0)</span><br><span class=\"line\"> </span><br><span class=\"line\">x4 = 14 (<span class=\"built_in\">integer</span> square root), a4 = 4 (remainder)</span><br></pre></td></tr></table></figure>\n<p>最后一轮 <span class=\"math inline\">\\(x\\)</span> 保存的就是整数平方根,<span class=\"math inline\">\\(a\\)</span> 为余数。如果应用64位寄存器组合变量 <span class=\"math inline\">\\(x\\)</span> 和 <span class=\"math inline\">\\(a\\)</span>,可以用硬件辅助实现快速求解。以下为对应使用C语言的软件实现,注意到整个过程需要16次迭代:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* integer square root - shift-and-substract method */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">uint32_t</span> <span class=\"title\">isqrt_sfsb</span><span class=\"params\">(<span class=\"keyword\">uint32_t</span> a)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">uint32_t</span> m, x, b;</span><br><span class=\"line\"></span><br><span class=\"line\"> m = <span class=\"number\">0x40000000</span>;</span><br><span class=\"line\"> x = <span class=\"number\">0</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (m != <span class=\"number\">0</span>) {</span><br><span class=\"line\"> b = x | m;</span><br><span class=\"line\"> x >>= <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a >= b) {</span><br><span class=\"line\"> a -= b;</span><br><span class=\"line\"> x |= m;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> m >>= <span class=\"number\">2</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>这种解法不太直观,迭代次数恒定,效率亦并非最高。但是了解其原理并写出正确的代码,展示了应聘者深厚的数学功底和熟练的编程技能,一定会给面试加分。</p>\n<h3 id=\"浮点数平方根\">浮点数平方根</h3>\n<p>比求整数平方根更普遍的是求任意实数的平方根。这时输入值可能有小数位,输出值也会是带小数点的实数。准备这样的面试题,需要先复习浮点数的概念和应用。</p>\n<h4 id=\"ieee-754\">IEEE 754</h4>\n<p>浮点数是计算机对实数的近似值数值表现法。IEEE 754是现今最广泛使用的二进制浮点数运算标准。它定义了表示浮点数的格式、反常值、特殊数值、以及这些数值的“浮点数运算符”。IEEE 754标准指定的浮点数格式建立于二进制科学计数法的基础之上,其数值表示式为: <span class=\"math display\">\\[\\mathrm{Value}=(-1)^s2^{e}(1+m)\\]</span> 这里 <span class=\"math inline\">\\(s\\)</span> 为符号位,<span class=\"math inline\">\\(e\\)</span> 是指数,<span class=\"math inline\">\\(m\\)</span> 被称为尾数。以IEEE 754标准规定的32位单精度浮点数为例,如下图,从右到左第31位为符号位表示正负,中间8位(第23到30位)表示指数加偏移量127后的数值,后23位储存尾数的有效数位(最高的第22位对应 <span class=\"math inline\">\\(2^{-1}\\)</span>,最低的第0位对应 <span class=\"math inline\">\\(2^{-23}\\)</span>):</p>\n<img src=\"Float_example.svg\" />\n<p style=\"text-align: center;\">\n(来源:英文维基百科条目“IEEE 754”)\n</p>\n<p>由此可以算出实际数值 <span class=\"math inline\">\\((-1)^02^{124-127}(1+2^{-2})=0.15625\\)</span>。从十进制实数转换为浮点数也不难,具体过程可参考<a href=\"https://sandbox.mc.edu/~bennet/cs110/flt/dtof.html\">此网页</a>。还可以使用一些<a href=\"https://www.h-schmidt.net/FloatConverter/IEEE754.html\">在线转换工具</a>验证手算的结果。</p>\n<p>值得注意的是,<strong>浮点数并非实数</strong>,它所表示的数会与实际的数值存在偏差。<strong>32位单精确度浮点数只可以保证7位十进制有效数字,而64位双精度浮点数可以保证15位十进制有效数字</strong>。在下面的在线工具截图中,我们输入精确到小数点后八位的 <span class=\"math inline\">\\(\\pi\\)</span> 值3.14159265,转换后得到单精确度浮点数0x40490fdb,此数字实际代表的值为3.1415927410...。这证实了单精确度浮点数的精确度判断。</p>\n<p><img src=\"IEEE754-Pi.jpg\" /></p>\n<p>C语言的<code>float</code>类型对应IEEE 754标准规定的32位单精确度浮点数,<code>double</code>类型对应64位双精确度浮点数。在下面的分析中,预设的前提条件是输入的浮点数都大于零。</p>\n<h4 id=\"二分搜索法\">二分搜索法</h4>\n<p>基于实数和其浮点数表示法的特点,应用二分搜索法计算平方根的过程与整数平方根有一些不同:</p>\n<ul>\n<li>初始值设定:这里要区分输入参数大于或小于等于1的情况\n<ul>\n<li>如果输入参数小于1,其平方根比自身要大,所以要将上界设为1,下界为其自身。</li>\n<li>输入参数大于1的情况正好相反,可将上界设为自身,下界为1。</li>\n</ul></li>\n<li>循环结束条件:为避免出现死循环,要注意以下两点\n<ul>\n<li>由于上下界都为浮点数,必须要定义一个误差范围,比如 <span class=\"math inline\">\\(10^{-6}\\)</span>。当上下界差值不超过此值时,立即结束循环。</li>\n<li>浮点数的精度是有限的。32位浮点数<code>float</code>只可以保证7位十进制有效数字。比如当下界为141.421356、上界为141.421371时,计算出来的中间值还是141.421356。所以还必须加上强制退出的判定。</li>\n</ul></li>\n</ul>\n<p>考虑上述这些,我们的浮点数平方根二分搜索法实现如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> ERROR_BOUND 1e-6</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">/* real number square root - bisection method */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">float</span> <span class=\"title\">rsqrt_bist</span><span class=\"params\">(<span class=\"keyword\">float</span> x)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">float</span> low, high, <span class=\"keyword\">float</span>;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">/* set initial lower and upper bounds */</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (x > <span class=\"number\">1</span>) {</span><br><span class=\"line\"> low = <span class=\"number\">1</span>;</span><br><span class=\"line\"> high = x;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> <span class=\"comment\">/* square root of any number less than 1 is bigger</span></span><br><span class=\"line\"><span class=\"comment\"> * than the number itself, e.g. 0.01^0.5 = 0.1 */</span></span><br><span class=\"line\"> low = x;</span><br><span class=\"line\"> high = <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> ((high - low) > ERROR_BOUND) {</span><br><span class=\"line\"> mid = (high + low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (mid == low) {</span><br><span class=\"line\"> <span class=\"keyword\">break</span>; <span class=\"comment\">/* force exit */</span></span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (mid * mid > x) {</span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> low = mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> mid;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h4 id=\"巴比伦解法\">巴比伦解法</h4>\n<p>面试者要掌握的第二种计算浮点数平方根的方法是巴比伦解法。这是发源于古典世界的、有悠久历史的算法。据信将近四千年前的巴比伦人就知晓了这种求平方根的方法<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>,但是直到公元一世纪才由古希腊数学家希罗给出明确的描述。</p>\n<p>巴比伦解法的朴素思想是:如果估计值 <span class=\"math inline\">\\(x\\)</span> 大于非负实数 <span class=\"math inline\">\\(a\\)</span> 的平方根 <span class=\"math inline\">\\(r\\)</span>,那么 <span class=\"math inline\">\\(a/x\\)</span> 一定是小于 <span class=\"math inline\">\\(r\\)</span> 的,而二者的均值将更接近 <span class=\"math inline\">\\(r\\)</span>。因为算术平均数总是大于或等于几何平均值,所以这一算法一定收敛。巴比伦解法的实际流程可写为:</p>\n<ol type=\"1\">\n<li>预测一个平方根值 <span class=\"math inline\">\\(x\\)</span>(优选接近实际平方根的数值),初始 <span class=\"math inline\">\\(y=a/x\\)</span></li>\n<li>计算 <span class=\"math inline\">\\(x=(x+y)/2\\)</span>(使用算术平均值近似几何平均值)</li>\n<li>比较 <span class=\"math inline\">\\(x\\)</span> 和 <span class=\"math inline\">\\(y\\)</span>,如果差值达不到精度,重复以上步骤</li>\n</ol>\n<p>仔细观察上面第2步,这不就是前面讲到的牛顿迭代法公式(3)么!所以巴比伦解法与牛顿迭代法本质上是等同的。想想在牛顿发明微积分之前还早两千多年时,古巴比伦人已经会熟练地应用此方法计算出平方根了,这样的智慧实在是让人佩服。</p>\n<p>明白了巴比伦解法的原理和流程,我们就能写出简洁的代码实现:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* real number square root - Newton/Babylonian */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">float</span> <span class=\"title\">rsqrt_nwtn</span><span class=\"params\">(<span class=\"keyword\">float</span> x)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">float</span> val = x;</span><br><span class=\"line\"> <span class=\"keyword\">float</span> last = <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (<span class=\"built_in\">fabs</span>(val - last) > ERROR_BOUND) {</span><br><span class=\"line\"> val = (val + last) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> last = x / val;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> val;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"平方根倒数\">平方根倒数</h3>\n<p>在3D游戏程序开发中,依据计算机图形学的原理,需要使用规一化向量来实现光照和投影效果。由此可能每秒要做上百万次平方根倒数运算,所以找到一种快速平方根倒数的计算方法至关重要。此外,平方根倒数也广泛应用在量化神经网络、深度学习、气象数据处理及基准测试(benchmarking)软件中。</p>\n<h4 id=\"速算法实现\">速算法实现</h4>\n<p>很明显,直接使用1去除以浮点数平方根函数的输出是低效的。幸运的是,早在上个世纪80年代后期,工作于一些3D图形显示软硬件公司的程序员(们)就发明了“平方根倒数速算法”(Fast Inverse Square Root,简称“Fast InvSqrt()”)。对于同一精度的近似值,此算法比直接使用浮点数除法要快四倍!</p>\n<p>下面就是对应于32位单精度浮点数的“平方根倒数速算法”C语言实现:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* fast inverse square root function for 32-bit IEEE 754</span></span><br><span class=\"line\"><span class=\"comment\"> * standard floating-point numerical value */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">float</span> <span class=\"title\">fast_inv_sqrt</span><span class=\"params\">(<span class=\"keyword\">float</span> x)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">float</span> halfx = <span class=\"number\">0.5f</span> * x;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> i = *(<span class=\"keyword\">int</span> *)&x; <span class=\"comment\">/* transfer bits of float to int */</span></span><br><span class=\"line\"> i = <span class=\"number\">0x5f375a86</span> - (i >> <span class=\"number\">1</span>); <span class=\"comment\">/* initial guess with magic */</span></span><br><span class=\"line\"> x = *(<span class=\"keyword\">float</span> *)&i; <span class=\"comment\">/* bit transfer back to float */</span> </span><br><span class=\"line\"> x = x * (<span class=\"number\">1.5f</span> - halfx * x * x); <span class=\"comment\">/* Newton step */</span></span><br><span class=\"line\"> x = x * (<span class=\"number\">1.5f</span> - halfx * x * x); <span class=\"comment\">/* Repeat (optional) */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> x;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>虽然平方根倒数速算法不太可能在面试中被问到,但是理解这个精巧的算法会极大地巩固和加深程序员的知识面。如果你有机会能准确而清晰地讲述其机理,一定会给面试官留下深刻印象。</p>\n<h4 id=\"速算法解析\">速算法解析</h4>\n<p>那么该如何理解这段简短的程序呢?它又是如何以令人意想不到的速度完成平方根倒数运算的呢?让我们来条分缕析。</p>\n<h5 id=\"牛顿迭代\">牛顿迭代</h5>\n<p>先从最后两行看起。第9行和第10行完全一样,注释标明是牛顿(迭代)步骤。我们来验证一下它是否符合迭代公式。计算实数 <span class=\"math inline\">\\(a\\)</span> 的平方根倒数等同于求函数 <span class=\"math inline\">\\(f(x)=x^{-2}-a\\)</span> 的零点。套用公式(1)推导出: <span class=\"math display\">\\[\\begin{align}\nx_{n+1}&=x_{n}-\\frac{x_n^2-a}{-2x_n^{-3}}\\\\\n&=x_n+\\frac{1}{2}x_n-\\frac 1 2ax_n^3\\\\\n&=x_n(1.5-\\frac a 2 x_n^2)\\tag{5}\n\\end{align}\\]</span> 公式(5)和代码正好对上!所以程序在此执行了两步牛顿迭代。由此也可推断出第8行的变量<code>x</code>储存着平方根倒数的初始估计值。进一步,我们可以判定程序的第6、7、8三行应该就是用来生成这个预估值的。问题是整数<code>i</code>是做什么用的?那个0x5f375a86被称为魔术数字,它又是从哪里冒出来的呢?</p>\n<h5 id=\"类型转化\">类型转化</h5>\n<p>要弄懂这三行代码,就需要了解将IEEE 754格式的浮点数转化为整数时发生了什么。如第6行的代码所示,转化通过取别名存储的方式实现。原浮点数的所有比特都保留不动,只是被重新解析为整数<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>。回顾前述“IEEE 754”一节讲到的浮点数数值表示法,不考虑符号位,如果标记原浮点数为<span class=\"math inline\">\\(x=2^{e_x}(1+m_x)\\)</span>,则转化后的整数可表示为<span class=\"math inline\">\\(I_x=E_xL+M_x\\)</span>。这些参数之间的关系为(括弧内为单精度时的取值): <span class=\"math display\">\\[\\begin{align}\nE_x&=e_x+B &(B=127)\\\\\nM_x&=m_xL &(L=2^{23})\n\\end{align}\n\\]</span> 还是以0.15625为例,从其浮点数表达式 <span class=\"math inline\">\\(2^{-3}(1+0.25)\\)</span> 得出 <span class=\"math inline\">\\({e_x}=-3\\)</span>、<span class=\"math inline\">\\(m_x=0.25\\)</span>。由此导出 <span class=\"math inline\">\\({E_x}=-3+127\\)</span>、<span class=\"math inline\">\\(M_x=0.25\\cdot2^{23}\\)</span> 及 <span class=\"math inline\">\\(I_x=124\\cdot2^{23}+2^{21}=1042284544\\)</span>,这正好对应浮点数0.15625存储数据的十六进制数值0x3e200000。</p>\n<h5 id=\"线性近似\">线性近似</h5>\n<p>另一个要用到的知识点,是特定对数函数的线性近似。如下图所示,在<span class=\"math inline\">\\([0,1]\\)</span>区间内,<span class=\"math inline\">\\(\\log _{2}{(1+x)}\\)</span> 与 <span class=\"math inline\">\\({x}\\)</span> 很接近。事实上,越接近端点相差越小。为了使平均误差最小,可以考虑将 <span class=\"math inline\">\\({x}\\)</span> 加上一个矫正值 <span class=\"math inline\">\\(\\sigma\\)</span>。从图形上看,这等同于将直线上移,平均误差确实变小了。 由此得到关系式 <span class=\"math inline\">\\(\\log_{2}{(1+x)}\\cong x+\\sigma\\)</span>。记住这一式子,因为我们马上就要用到。</p>\n<img src=\"log-plots.png\" />\n<p style=\"text-align: center;\">\n蓝线为 <span class=\"math inline\">\\(\\log_2(1+x)\\)</span>,绿线为 <span class=\"math inline\">\\(x\\)</span>,红线为 <span class=\"math inline\">\\(x+\\sigma\\)</span>\n</p>\n<h5 id=\"魔术数字\">魔术数字</h5>\n<p>有了以上这些预备知识,魔术数字就可以推到出来了。首先,如果将浮点数 <span class=\"math inline\">\\(x\\)</span> 的平方根倒数的结果记为 <span class=\"math inline\">\\(y\\)</span>,则有 <span class=\"math display\">\\[y=\\frac{1}{\\sqrt{x}}\\]</span> 对等式的两边取以2为底的对数,得到 <span class=\"math display\">\\[\\log_2{(y)}=-\\frac{1}{2}\\log_2{(x)}\\]</span> 因为 <span class=\"math inline\">\\(x\\)</span> 和 <span class=\"math inline\">\\(y\\)</span> 都是浮点数,下一步用它们各自的浮点数标记 <span class=\"math inline\">\\(2^{e}(1+m)\\)</span> 代入 <span class=\"math display\">\\[\\log_2(1+m_y)+e_y=-\\frac{1}{2}\\log_2{(1+m_x)}-\\frac{1}{2}e_x\\]</span> 注意,这里应用对数的运算性质,乘方运算已化为加法运算。接下来将上一节的线性近似关系式代入,得出 <span class=\"math display\">\\[m_y+\\sigma+e_y=-\\frac{1}{2}m_x-\\frac{1}{2}\\sigma-\\frac{1}{2}e_x\\]</span> 下面一步非常关键,参考“类型转化”一节提到的关系式,用 <span class=\"math inline\">\\(E\\)</span> 及 <span class=\"math inline\">\\(M\\)</span> 替换 <span class=\"math inline\">\\(e\\)</span> 及 <span class=\"math inline\">\\(m\\)</span> <span class=\"math display\">\\[M_y+(E_y-B)L=-\\frac{3}{2}\\sigma{L}-\\frac{1}{2}M_x-\\frac{1}{2}(E_x-B)L\\]</span> 移项整理后变成 <span class=\"math display\">\\[E_yL+M_y=\\frac{3}{2}(B-\\sigma)L-\\frac{1}{2}(E_xL+M_x)\\]</span> 仔细观察上式,左边不就是浮点数 <span class=\"math inline\">\\(y\\)</span> 转化为整数 <span class=\"math inline\">\\(I_y\\)</span> 的表达式 吗?而右边也包含 <span class=\"math inline\">\\(I_x\\)</span>。标记 <span class=\"math inline\">\\(R=\\frac{3}{2}(B-\\sigma)L\\)</span>,得到整数 <span class=\"math inline\">\\(I_y\\)</span> 与 <span class=\"math inline\">\\(I_x\\)</span> 的关系式: <span class=\"math display\">\\[I_y=R-\\frac{1}{2}I_x\\tag{6}\\]</span> 再看看函数代码的第7行<code>i = 0x5f375a86 - (i >> 1);</code>,BINGO!给定线性近似的矫正值 <span class=\"math inline\">\\(\\sigma\\)</span>,就可以从公式(6)确定整数 <span class=\"math inline\">\\(R\\)</span>,即魔术数字。而第8行<code>x = *(float *)&i;</code>所做的只是将预估值转化回浮点数,即从 <span class=\"math inline\">\\(I_y\\)</span> 到 <span class=\"math inline\">\\(y\\)</span>, 以便下面的牛顿迭代。</p>\n<p>平方根倒数速算法,本质上是对输入浮点数做整数转化再进行一次移位操作,然后从一个精心挑选的整数常数中减去,结果转化回浮点数后就是其平方根倒数的近似值,最后根据精度需要进行一次或两次牛顿迭代。</p>\n<p>余下的问题是,怎么找到一个合适的 <span class=\"math inline\">\\(\\sigma\\)</span> 以计算出 <span class=\"math inline\">\\(R\\)</span>,从而提供有足够精度初始估计值?</p>\n<p>早在2003年,当平方根倒数速算法的源码开始在某些网络论坛上出现时,普渡大学的数学博士Chris Lomont就对此做过专门研究<a href=\"#fn5\" class=\"footnote-ref\" id=\"fnref5\" role=\"doc-noteref\"><sup>5</sup></a>。那时流传的经典代码用到的 <span class=\"math inline\">\\(R\\)</span> 值是0x5f3759df(对应 <span class=\"math inline\">\\(\\sigma=0.045046568\\)</span>),这个数值已经提供了相当好的精确度。Chris最初理论推导出一个的<span class=\"math inline\">\\(R\\)</span> 值,但是实测结果竟然比经典 <span class=\"math inline\">\\(R\\)</span> 值要差!Chris无奈在暴力搜索后终于得出最优值为0x5f375a86(对应 <span class=\"math inline\">\\(\\sigma=0.045033296\\)</span>),在牛顿迭代后所得的结果比经典值更精确。2018年,乌克兰、波兰和印度的几位科学家联名发表了期刊文章<a href=\"#fn6\" class=\"footnote-ref\" id=\"fnref6\" role=\"doc-noteref\"><sup>6</sup></a>,全面分析了单精度浮点数平方根倒数速算法中魔术数字的搜寻过程。他们通过缜密的计算证明0x5f375a86确实是最优值,并推导出了一次牛顿迭代后相对误差不超过 <span class=\"math inline\">\\(1.75\\cdot 10^{-3}\\)</span>、二次迭代后上界为 <span class=\"math inline\">\\(4.60\\cdot 10^{-6}\\)</span>。</p>\n<p>但是,迄今为止谁也不知道最初是由谁找到0x5f3759df的。想象在1980年代末,一个或几个程序员在嘈杂、幽闷的机房里,面对现在看来奇慢无比的计算机和分辨率极低的显示器,一遍又一遍孜孜不倦地推演和编程计算、实验平方根倒数速算法并寻找一个最优减法常数,最后终于找到这个魔术数字,之后二、三十年计算机图形和图像技术的发展都受益于此。这注定成为一段程序员的传奇故事!</p>\n<h3 id=\"单元测试\">单元测试</h3>\n<p>单元测试是是保障软件开发质量的重要环节,是程序员的职责。面试者应该知道如何测试所写的代码。</p>\n<p>对于整数平方根,参考前述函数实现,我们要专门测试二分法的上界条件,确保其收敛性。此外,测试必须涵盖完全平方数(perfect square)和非完全平方数。因为没有标准的整数平方根库函数,我们可以先随机产生一个16位无符号整数,再自乘生成完全平方数保存。然后随机产生第二个16位无符号整数并对第一个整数取余,余数与完全平方数相加另存。这样保存的两个数的整数平方根都是第一个整数,可以用来测试。基于此的整数平方根测试代码如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* test integer square root */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">test_isqrt</span><span class=\"params\">(<span class=\"keyword\">void</span>)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">uint32_t</span> num, offset, isqr1, isqr2, i;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">/* test a special boundary case */</span></span><br><span class=\"line\"> num = <span class=\"number\">4294838221</span>;</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"\\nInteger Square Root of %u:\\n\\tNewton iteration\\t%u"</span></span><br><span class=\"line\"> <span class=\"string\">"\\n\\tBisection method\\t%u\\n\\tShift-and-substract\\t%u\\n"</span>,</span><br><span class=\"line\"> num, isqrt_nwtn(num), isqrt_bist(num), isqrt_sfsb(num));</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">/* test perfect and non-perfect square numbers, 1000 each */</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (<span class=\"keyword\">int</span> i=<span class=\"number\">0</span>; i<<span class=\"number\">1000</span>; i++) {</span><br><span class=\"line\"> num = random() & <span class=\"number\">0xFFFF</span>; <span class=\"comment\">/* random 16-bit integer */</span></span><br><span class=\"line\"> offset = (random() & <span class=\"number\">0xFFFF</span>) % num;</span><br><span class=\"line\"> isqr1 = num * num; <span class=\"comment\">/* test perfect square */</span></span><br><span class=\"line\"> isqr2 = num * num + offset;</span><br><span class=\"line\"> assert(isqrt_nwtn(isqr1) == num);</span><br><span class=\"line\"> assert(isqrt_nwtn(isqr2) == num);</span><br><span class=\"line\"> assert(isqrt_bist(isqr1) == num);</span><br><span class=\"line\"> assert(isqrt_bist(isqr2) == num);</span><br><span class=\"line\"> assert(isqrt_sfsb(isqr1) == num);</span><br><span class=\"line\"> assert(isqrt_sfsb(isqr2) == num);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Integer Square Root function test passes!\\n"</span>);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>测试浮点数平方根函数则方便得多,可以调用平方根库函数<code>sqrt()</code>,然后比较结果是否在给定的精度之内。下面的程序使用熟知的2的平方根及其标准倍数实现测试:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* test real number square root */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">test_rsqrt</span><span class=\"params\">()</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">float</span> num, r0, r1, r2;</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Number\\t\\tGlibc library sqrt\\tNewton iteration\\tBisection method\\n"</span>);</span><br><span class=\"line\"> num = <span class=\"number\">0.0002</span>; <span class=\"comment\">/* test sequence: 0.0002, 0.02, 2, 200, 20000 */</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (<span class=\"keyword\">int</span> i=<span class=\"number\">1</span>; i<<span class=\"number\">6</span>; i++) {</span><br><span class=\"line\"> r0 = <span class=\"built_in\">sqrt</span>(num);</span><br><span class=\"line\"> r1 = rsqrt_nwtn(num);</span><br><span class=\"line\"> r2 = rsqrt_bist(num);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"%-10.4f\\t%.15f\\t%.15f\\t%.15f\\n"</span>, num, r0, r1, r2);</span><br><span class=\"line\"> assert(r0 - r1 < ERROR_BOUND);</span><br><span class=\"line\"> assert(r0 - r2 < ERROR_BOUND);</span><br><span class=\"line\"> num *= <span class=\"number\">100</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Real Number Square Root function test passes!\\n"</span>);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>快速平方根倒数的测试代码也不复杂。值得一提的是如何生成指定范围内的随机浮点数(可能成为单独的面试问题),这由下面程序段的第11行完成。测试也调用了<code>sqrt()</code>,以计算相对误差,如第15行所示:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">define</span> REAL_RANGE 1000</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">/* measure fast inverse square root accuracy */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">measure_invsqrt</span><span class=\"params\">(<span class=\"keyword\">void</span>)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">float</span> real, rt, ir1, ir2, acu;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"\\nInverse Square Root accuracy test:\\n"</span>);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Real Number\\t1/sqrt()\\tFast-InvSqrt\\tError\\n"</span>);</span><br><span class=\"line\"> <span class=\"keyword\">for</span> (<span class=\"keyword\">int</span> i=<span class=\"number\">0</span>; i<<span class=\"number\">10</span>; i++) {</span><br><span class=\"line\"> real = (<span class=\"keyword\">float</span>)random()/(<span class=\"keyword\">float</span>)(RAND_MAX/REAL_RANGE);</span><br><span class=\"line\"> rt = <span class=\"built_in\">sqrt</span>(real);</span><br><span class=\"line\"> ir1 = <span class=\"number\">1</span> / rt;</span><br><span class=\"line\"> ir2 = fast_inv_sqrt(real);</span><br><span class=\"line\"> acu = <span class=\"built_in\">fabs</span>(ir2 * rt - <span class=\"number\">1</span>); <span class=\"comment\">/* relative error */</span></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"%f\\t%.8f\\t%.8f\\t%.8f\\n"</span>, real, ir1, ir2, acu);</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>单元测试的运行结果记录如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ./sqrts</span><br><span class=\"line\"></span><br><span class=\"line\">Integer Square Root of 4294838221:</span><br><span class=\"line\">Newton iteration 65535</span><br><span class=\"line\">Bisection method 65535</span><br><span class=\"line\">Shift-and-substract 65535</span><br><span class=\"line\">Integer Square Root <span class=\"keyword\">function</span> <span class=\"built_in\">test</span> passes!</span><br><span class=\"line\"></span><br><span class=\"line\">Number Glibc library sqrt Newton iteration Bisection method</span><br><span class=\"line\">0.0002 0.014142135158181 0.014142150059342 0.014142313972116</span><br><span class=\"line\">0.0200 0.141421347856522 0.141421347856522 0.141421005129814</span><br><span class=\"line\">2.0000 1.414213538169861 1.414213538169861 1.414213657379150</span><br><span class=\"line\">200.0000 14.142135620117188 14.142135620117188 14.142135620117188</span><br><span class=\"line\">20000.0000 141.421356201171875 141.421356201171875 141.421356201171875</span><br><span class=\"line\">Real Number Square Root <span class=\"keyword\">function</span> <span class=\"built_in\">test</span> passes!</span><br><span class=\"line\"></span><br><span class=\"line\">Inverse Square Root accuracy <span class=\"built_in\">test</span>:</span><br><span class=\"line\">Real Number 1/sqrt() Fast-InvSqrt Error</span><br><span class=\"line\">625.969788 0.03996900 0.03996884 0.00000405</span><br><span class=\"line\">35.369572 0.16814545 0.16814519 0.00000149</span><br><span class=\"line\">607.420593 0.04057469 0.04057455 0.00000340</span><br><span class=\"line\">863.205933 0.03403633 0.03403633 0.00000000</span><br><span class=\"line\">878.772766 0.03373352 0.03373352 0.00000000</span><br><span class=\"line\">248.785355. 0.06339975 0.06339949 0.00000417</span><br><span class=\"line\">498.417389 0.04479230 0.04479229 0.00000036</span><br><span class=\"line\">142.394669 0.08380176 0.08380162 0.00000167</span><br><span class=\"line\">24.309278 0.20282148 0.20282085 0.00000310</span><br><span class=\"line\">299.332336 0.05779938 0.05779938 0.00000006</span><br></pre></td></tr></table></figure>\n<p>可以看到,计算浮点数平方根时,与库函数<code>sqrt()</code>的输出相比,巴比伦解法(牛顿迭代法)的结果精度比二分搜索法更高一些,但二者都在程序设定的绝对误差范围(<span class=\"math inline\">\\(10^{-6}\\)</span>)之内。对于快速平方根倒数算法,其结果精度真的很高,计算出来的相对误差确实在理论的上界 (<span class=\"math inline\">\\(4.60\\cdot10^{-6}\\)</span>) 之内。这就是算法的力量!</p>\n<p>完整程序(包含以上所有函数及测试代码)的压缩包在此下载:<a href=\"sqrts.c.gz\">sqrts.c.gz</a></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>这是因为“nlz”本质上就是以2为底的整数对数函数。对于大于0的整数 <span class=\"math inline\">\\(a\\)</span>,有 <span class=\"math inline\">\\(\\lceil\\log_2(x)\\rceil=32-\\mathrm{nlz}(x-1)\\)</span>,由此导出 <span class=\"math inline\">\\(\\lceil\\log_2(\\sqrt x)\\rceil=16-\\mathrm{nlz}(x-1)/2\\)</span>。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>James Ulery, <a href=\"http://www.azillionmonkeys.com/qed/ulerysqroot.pdf\">Computing Integer Square Roots</a>, University of Toronto, 2006<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>耶鲁大学收藏的一块古巴比伦黏土板(编号YBC 7289),上面以六十进制记载了单位正方形对角线长的准确估计值<span class=\"math inline\">\\({\\textstyle 1;24,51,10}\\)</span>。这个数的估算误差小于两百万分之一。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>注意这种类型转化与C语言的强制类型转换不同,后者会直接舍弃小数位。<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn5\" role=\"doc-endnote\"><p>Chris Lomont. Fast inverse square root. Technical report, Indiana: Purdue University, 2003.<a href=\"#fnref5\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn6\" role=\"doc-endnote\"><p>Moroz, Leonid V., et al. \"Fast calculation of inverse square root with the use of magic constant–analytical approach.\" Applied Mathematics and Computation 316 (2018): 245-255.<a href=\"#fnref6\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["面试指南"],"tags":["C/C++编程","系统编程"]},{"title":"程序员面试题精解(3)— 二分查找","url":"/2022/12/29/PGITVW-3-binsearch/","content":"<p>正如“算法分析之父”高德纳所言:尽管二分查找的基本思想相对简单,但细节可能出乎意料地棘手。在实际面试中,有非常多的程序员无法写出正确无误的二分查找程序。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky.</strong><br> <strong>— <em>Donald Knuth</em>(高德纳,著名计算机科学家,现代计算机科学的先驱人物,名著《计算机程序设计艺术》的作者,1974年图灵奖得主)</strong></p>\n</div>\n<h2 id=\"基本算法\">基本算法</h2>\n<p>二分查找,也称二分搜索或折半搜索,是一种在有序数组中查找给定元素的搜索算法。它的基本思想是:从数组的中间元素开始与目标值其进行比较,如果相等则搜索过程结束;否则,排除目标值不在其中的那一半,继续在余下的一半内查找,再次取中间元素与目标值进行比较,反复进行直到找到目标值;如果某一步骤时余下的数组为空,则表明给定元素代表不在数组中。二分查找算法的时间复杂度是 <span class=\"math inline\">\\(O(\\log n)\\)</span>,是很有效率的搜索算法。</p>\n<p>可惜的是,仅仅了解二分查找的算法思想并不能保证你可以写出准确无误的程序。美国计算机科学家乔恩·本特利(Jon Bentley)在他的计算机科学经典名著《<a href=\"https://amzn.to/3UgnCkS\">编程珠玑</a>》中提到,在给专业程序员的培训课程中布置二分查找作业,90%的人在数小时后仍然未能提供正确的答案。</p>\n<p>二分查找思路虽然简单,但是从各种要求和条件中产生的差异常常令人难以招架。这里我们借助契约编程的理念,解析二分查找的解法框架和两种基本算法实现,目标就是要将细节中魔⿁关在笼子里。</p>\n<h3 id=\"契约编程\">契约编程</h3>\n<p>要编写出二分查找正确实现,首先要掌握契约编程的概念。契约编程是一种计算机软件设计的方法。这种方法要求软件设计者为程序组件定义正式的、精确的并且可验证的接口。具体到二分查找函数的这样模块,就包括先决条件(precondition)、后置条件(postcondition)、循环不变式(loop invariant)和限定函数(bound function)这些关键要素。它们的解释如下</p>\n<ul>\n<li><strong>先决条件</strong>:函数调用之前必须成立的条件。如果先决条件被违反了,则代码将产生未定义行为,其运行的结果将偏离设计目标。不正确的先决条件还可能引发安全问题。</li>\n<li><strong>后置条件</strong>:指在执行一段代码后必须成立的条件,其正确性由函数在终止执行时保证。简单的说,后置条件就是函数对其输出结果的承诺。</li>\n<li><strong>循环不变式</strong>:指在循环开始和循环中,每一次迭代时为真的性质,也是在循环开始和结束后始终成立的条件。</li>\n<li><strong>限定函数</strong>:定义为仍需执行的迭代次数的上限。可以把它看作是一个表达式,其值随着循环的进行而单调地减少。当它达到或小于零时,循环结束。</li>\n</ul>\n<p>对于二分查找函数,显然输入必须是有序数组,这是常常被忽视的先决条件。函数的后置条件就是清晰定义的搜索结果。如果给定元素在数组中,就返回其索引(或指针)值,不然就输出预先定义的表示不存在的数值。此数值不应该与数组的索引值混淆。二分查找的循环不变式要求“如果目标值存在于数组中,那么目标值就一定存在于当前的搜索区间内”,这是正确代码实现的最重要的要素。如果这条不能总成立,那么函数必然出错!最后的限定函数由当前区间决定,因为每次的搜索空间折半,迭代次数无疑是按照对数单调递减的。</p>\n<h3 id=\"解法框架\">解法框架</h3>\n<p>现在来看看二分查找的基本解法框架,参考C语言的函数示例</p>\n<!---\nc 二分查找 - 基本解法框架\nint binSearch(int a[], int n, int v)\n{\n int low, mid, high;\n low = 0;\n high = ...;\n \n while (...) {\n mid = low + (high - low) / 2;\n if (a[mid] < v) {\n low = ...\n } else if (a[mid] > v) {\n high = ... \n } else { /* a[mid] == v */\n ...\n }\n return ...;\n}\n-->\n<script src=\"https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js\"></script>\n<pre><code class=\"prettyprint language-c linenums\">int binSearch(int a[], int n, int v)\n{\n int low, mid, high;\n low = 0;\n high = <span style=\"background-color:yellow\">...</span>;\n \n while (<span style=\"background-color:yellow\">...</span>) {\n mid = low + (high - low) / 2;\n if (a[mid] < v) {\n low = <span style=\"background-color:yellow\">...</span>;\n } else if (a[mid] > v) {\n high = <span style=\"background-color:yellow\">...</span>; \n } else { /* a[mid] == v */\n <span style=\"background-color:yellow\">...</span>;\n }\n return <span style=\"background-color:yellow\">...</span>;\n}</code></pre>\n<p>对应于契约编程的原理,我们可以如下说明各项要素:</p>\n<ul>\n<li>先决条件:函数输入已经预先排好的升序整数数组<code>a[]</code>,数组一共有<code>n</code>个元素,要查找的数值为<code>v</code>。</li>\n<li>后置条件:如果<code>v</code>存在于数组<code>a[]</code>中,输出其索引值;否则返回-1。</li>\n<li>循环不变式:如果<code>v</code>存在于数组<code>a[]</code>中,则其一定在每次循环的当前区间内,即位于上下界<code>low</code>和<code>high</code>之间。</li>\n<li>限定函数:因为下界<code>low</code>和上界<code>high</code>会根据<code>v</code>与当前的中间元素<code>a[mid]</code> 比较的结果而改变,总体区间是不断缩减的,<code>while</code>循环被限定。</li>\n</ul>\n<p>无疑这些给我们理清了二分查找编程实现的思路,但是其中5个标记 ... 的部分,就是可能出现细节问题的地⽅。它们直接影响循环不变式的成立和限定函数的功用,细微的不同就会产生错误的结果,从而破坏后置条件。严重时还会造成无限循环或程序崩溃。</p>\n<p>在具体的函数实现中,循环不变式是用特定的变量和数值来正式确定的。依据不同的“当前区间”的表示方法,二分查找有两大类实现方式:对称边界解法和不对称边界解法。</p>\n<h3 id=\"对称边界解法\">对称边界解法</h3>\n<p>对称边界解法是二分查找函数的最直观实现。对称边界解法定义的“当前区间”是 <span class=\"math inline\">\\(a[low] <= a[i] <= a[high]\\)</span>,即如果要找的元素存在于数组中,那么它的索引值 <span class=\"math inline\">\\(i∈[low,high]\\)</span>,此循环不变式应该一直成立!</p>\n<p>由此,<code>low</code>被初始化为<code>0</code>,<code>high</code>被初始化为<code>n-1</code>。下面是这个解法的C语言实现:</p>\n<figure class=\"highlight c\"><figcaption><span>二分查找基本解法 - 对称边界、闭合区间 [low, high]</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_sym</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n - <span class=\"number\">1</span>;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low <= high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] < v) {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (a[mid] > v) {</span><br><span class=\"line\"> high = mid - <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* a[mid] == v */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">-1</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>因为搜索区间是闭合的,当前区间的上下界可能相等,所以<code>while</code>语句内继续循环的条件应该是<code>low <= high</code>,而不是<code>low < high</code>,即二者相等时还要再搜索。</p>\n<p>在循环体内,中间元素<code>a[mid]</code>小于目标值<code>v</code>时,下次搜索的区间下界一定在<code>mid</code>的右侧,所以<code>low = mid + 1</code>;反之当中间元素<code>a[mid]</code>大于目标值<code>v</code>时,下次搜索的区间上界一定在<code>mid</code>的左侧,所以<code>high = mid - 1</code>。这样保证了循环不变式持续为真。</p>\n<p>最后,如果<code>low</code>大于<code>high</code>,区间为空,循环中止,函数返回-1。这满足了后置条件。同时,因为上下界一直在相互靠近,当前区间不断缩小,函数实现隐含了限定函数对循环的限制作用。</p>\n<div class=\"note warning\"><p><strong>注意:</strong> 上面循环代码的中间元素数组索引值的计算式是 <code>mid = low + (high - low) / 2</code>,而非 <code>mid = (low + high) / 2</code>。这是为了避免整数溢出的问题。</p>\n</div>\n<h3 id=\"不对称边界解法\">不对称边界解法</h3>\n<p>不对称边界解法是更为常见、应用更广的二分查找算法实现。不对称边界解法中的循环不变式基于的“当前区间”是 <span class=\"math inline\">\\(a[low] <= a[i] < a[high]\\)</span>,即如果要找的元素存在于数组中,那么它的索引值 <span class=\"math inline\">\\(i∈[low,high)\\)</span>。</p>\n<p>不对称边界解法使用左闭右开的半闭合(或称半开放)搜索区间,这样做有很多好处,它使许多操作变得非常简单易懂:</p>\n<ul>\n<li>将一个区间一分为二,可以简单地选择一个支点并在两个新区间引用该支点: [low, high) -> [low, pivot), [pivot, high)</li>\n<li>区间内元素的计数可由一个单一的减法完成:high - low</li>\n<li>空区间的上下界索引都是相同的:[x, x)</li>\n</ul>\n<p>下面给出了这个解法的两种C语言实现。第一种对应于数组输入,第二种适用于指针输入:</p>\n<div class=\"tabs\" id=\"不对称边界解法\"><ul class=\"nav-tabs\"><li class=\"tab active\"><a href=\"#不对称边界解法-1\">数组输入</a></li><li class=\"tab\"><a href=\"#不对称边界解法-2\">指针输入</a></li></ul><div class=\"tab-content\"><div class=\"tab-pane active\" id=\"不对称边界解法-1\"><figure class=\"highlight c\"><figcaption><span>二分查找基本解法 - 数组输入,不对称边界、半闭合区间 [low, high)</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_asym</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] < v) {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (a[mid] > v) {</span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* a[mid] == v */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">-1</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></div><div class=\"tab-pane\" id=\"不对称边界解法-2\"><figure class=\"highlight c\"><figcaption><span>二分查找基本解法 - 指针输入,不对称边界、半闭合区间 [low, high)</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_asym_ptr</span><span class=\"params\">(<span class=\"keyword\">int</span> *a, <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> *low, *mid, *high;</span><br><span class=\"line\"> low = a;</span><br><span class=\"line\"> high = a + n;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + ((high - low) >> <span class=\"number\">1</span>);</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (*mid < v) {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (*mid > v) {</span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* *mid == v */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">NULL</span>;</span><br></pre></td></tr></table></figure></div></div></div>\n<p>在数组输入的实现代码中,为了满足“当前区间”的条件,<code>low</code>被初始化为<code>0</code>,<code>high</code>被初始化为<code>n</code>。</p>\n<p>因为搜索区间是半闭合的,当前区间的上下界不可能相等,所以<code>while</code>语句内继续循环的条件应该是<code>low < high</code>,而不是<code>low <= high</code>,即二者相等时就不必再搜索了。</p>\n<p>在循环体内,中间元素<code>a[mid]</code>小于目标值<code>v</code>时,下次搜索的区间下界一定在<code>mid</code>的右侧,所以<code>low = mid + 1</code>;反之当中间元素<code>a[mid]</code>大于目标值<code>v</code>时,下次搜索的区间上界就是<code>mid</code>,所以<code>high = mid</code>,此时上界还是开放的,<span class=\"math inline\">\\(i∈[low,high)\\)</span> 依然成立。这同样保证了循环不变式持续为真。</p>\n<p>最后,如果<code>low</code>不小于<code>high</code>,区间为空,循环中止,函数返回-1。这满足了后置条件。同理,因为上下界一直在相互靠近,当前区间不断缩小,函数实现隐含了限定函数对循环的限制作用。</p>\n<p>因为指针寻址比数组下标索引运算要快,许多二分查找函数使用指针输入。在这种情况下,下界<code>low</code>被赋值为第一个元素的指针<code>a</code>,上界<code>high</code>则为<code>a + n</code>。循环体内中间元素的指针的计算要小心,下面的两种写法都是错误的:</p>\n<ol type=\"1\">\n<li><code>mid = (low + high) / 2</code> - 这里问题不是运行时整数溢出,而是根本无法编译,因为将两个指针相加没有意义。</li>\n<li><code>mid = low + (high - low) >> 1</code> - C语言的移位运算符的优先级低于算术运算符,所以这一行等效于将<code>high</code>右移一位,显然是错误的。</li>\n</ol>\n<p>正确的实现应该是 <code>mid = low + ((high - low) >> 1)</code>。</p>\n<p>另外,在指针输入的情况下,后置条件是不一样的。二分查找函数要返回空指针以表明找不到目标值。</p>\n<h2 id=\"变形问题\">变形问题</h2>\n<p>掌握了契约编程的思想和循环不变式的原则,不仅可以编写出正确的基本算法实现,还能够解决一些变形的二分查找问题。</p>\n<h3 id=\"边界搜索\">边界搜索</h3>\n<p>当有序数组中存在多个重复元素与目标值相同时,常常需要找到第一个(最左边)或最后一个(最右边)的元素的位置,这正是不对称边界解法展示威力的时候。</p>\n<p>应用相同的循环不变式 <span class=\"math inline\">\\(a[low] <= a[i] < a[high]\\)</span>,也可以找到目标值的左右边界。不一样的地方是,如果中间值相同时,还要继续循环,以便定位第一个或最后一个元素。</p>\n<h4 id=\"定位左边界\">定位左边界</h4>\n<p>要定位左边界时,如果中间元素小于目标值,则要找的元素的左边界一定在中间元素的右侧,需要将下界加一(右移一位)。如果中间元素大于目标值,要将上界设为中间元素的位置,继续搜索。而当中间元素与目标值相等时,因为其左边可能还有相同的元素,也要将上界设为中间元素的位置。在这些操作后,因为最后要找的元素索引值总是满足 <span class=\"math inline\">\\(i∈[low,high)\\)</span> 的条件,循环不变式恒为真。</p>\n<p>而在循环结束时,为符合后置条件。需要对下界做一些检查。如果下界值为n,此数组索引值已越界,说明目标值比所有元素都大,函数应该返回-1。如果下界对应的元素正好目标值一样,输出下界索引值;否则返回-1。</p>\n<p>以有序数组 [2, 5, 5, 5, 7, 7, 7, 8, 10] 为例,查找目标值5时的算法的执行过程如下表所示</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">循环次数</th>\n<th style=\"text-align: center;\">0</th>\n<th style=\"text-align: center;\">1</th>\n<th style=\"text-align: center;\">2</th>\n<th style=\"text-align: center;\">3</th>\n<th style=\"text-align: center;\">4</th>\n<th style=\"text-align: center;\">5</th>\n<th style=\"text-align: center;\">6</th>\n<th style=\"text-align: center;\">7</th>\n<th style=\"text-align: center;\">8</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0(初始化)</td>\n<td style=\"text-align: center;\">2 (low)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10 (high)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">2 (low)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7 (high)</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">2 (low)</td>\n<td style=\"text-align: center;\">5 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">5 (high)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">3</td>\n<td style=\"text-align: center;\">2 (low/<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">5 (high)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">4(结束)</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">5 (<mark>low/high</mark>)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n</tbody>\n</table>\n<p>可以看到,三轮循环过后上下界的索引值相等,都为1。对应的数组元素与目标值一样,所以输出为1。结果正确。以下是C语言实现的能定位左边界的二分查找函数</p>\n<figure class=\"highlight c\"><figcaption><span>二分查找 - 定位左边界</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_leftbound</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] < v) {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* a[mid] >= v */</span></span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (low == n) <span class=\"keyword\">return</span> <span class=\"number\">-1</span>; <span class=\"comment\">/* v is greater than all */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> a[low] == v ? low <span class=\"comment\">/* find the left bound */</span></span><br><span class=\"line\"> : <span class=\"number\">-1</span>; <span class=\"comment\">/* v is less than all */</span></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>定位左边界的解法还可以用于定位最后一个小于目标值的元素。比如,给定目标值为7,从左到右最后一个小于目标值的元素是第三个5(索引值为3),即目标值7的左边界的左邻居。而给定目标值为5时,从左到右最后一个小于目标值的元素是2,即搜索目标值5时循环结束后下界索引值减一。由此可见,这里循环不变式完全一样,只是后置条件有所不同:</p>\n<ul>\n<li>如果目标值不大于数组序列首个元素,返回-1。</li>\n<li>否则返回搜索结果的下界索引值减一。</li>\n</ul>\n<p>对应的C语言实现如下</p>\n<figure class=\"highlight c\"><figcaption><span>二分查找 - 定位最后一个小于目标值的元素</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_lastlesser</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] < v) {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* a[mid] >= v */</span></span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> low == <span class=\"number\">0</span> ? <span class=\"number\">-1</span> <span class=\"comment\">/* v is not greater than all */</span></span><br><span class=\"line\"> : low - <span class=\"number\">1</span>; <span class=\"comment\">/* find the last lesser */</span></span><br></pre></td></tr></table></figure>\n<h4 id=\"定位右边界\">定位右边界</h4>\n<p>要定位右边界时,如果中间元素大于目标值,则要找的元素的右边界一定在中间元素的左侧,需要将上界设为中间元素的位置。如果中间元素小于目标值,要将下界加一(右移一位),继续搜索。而当中间元素与目标值相等时,因为其右边可能还有相同的元素,也要将下界设为中间元素的位置。在这些操作后,因为最后要找的元素索引值总是满足 <span class=\"math inline\">\\(i∈[low,high)\\)</span> 的条件,循环不变式恒为真。</p>\n<p>同样在循环结束时,为符合后置条件。需要对下界做一些检查。如果下界值为0,说明目标值比所有元素都小,函数应该返回-1。如果下界的左边对应的元素正好目标值一样,输出下界左移一位的索引值;否则返回-1。</p>\n<p>以相同的有序数组 [2, 5, 5, 5, 7, 7, 7, 8, 10] 为例,查找目标值7时的算法的执行过程如下表所示</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">循环次数</th>\n<th style=\"text-align: center;\">0</th>\n<th style=\"text-align: center;\">1</th>\n<th style=\"text-align: center;\">2</th>\n<th style=\"text-align: center;\">3</th>\n<th style=\"text-align: center;\">4</th>\n<th style=\"text-align: center;\">5</th>\n<th style=\"text-align: center;\">6</th>\n<th style=\"text-align: center;\">7</th>\n<th style=\"text-align: center;\">8</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">0(初始化)</td>\n<td style=\"text-align: center;\">2 (low)</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10 (high)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7 (low)</td>\n<td style=\"text-align: center;\">7 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">8</td>\n<td style=\"text-align: center;\">10 (high)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7 (low)</td>\n<td style=\"text-align: center;\">8 (<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">10 (high)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">3</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7 (low/<strong>mid</strong>)</td>\n<td style=\"text-align: center;\">8 (high)</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">4(结束)</td>\n<td style=\"text-align: center;\">2</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">7</td>\n<td style=\"text-align: center;\">8 (<mark>low/high</mark>)</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n</tbody>\n</table>\n<p>可以看到,三轮循环过后上下界的索引值相等,都为7。对应左边的数组元素与目标值一样,所以输出为6。结果正确。以下是C语言实现的能定位右边界的二分查找函数</p>\n<figure class=\"highlight c\"><figcaption><span>二分查找 - 定位右边界</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_rightbound</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] > v) {</span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (high == <span class=\"number\">0</span>) <span class=\"keyword\">return</span> <span class=\"number\">-1</span>; <span class=\"comment\">/* v is lesser than all */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> a[high<span class=\"number\">-1</span>] == v ? (high - <span class=\"number\">1</span>) <span class=\"comment\">/* find the right bound */</span></span><br><span class=\"line\"> : <span class=\"number\">-1</span>; <span class=\"comment\">/* v is greater than all */</span></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>同理,定位右边界的解法还可以用于定位第一个大于目标值的元素。比如,给定目标值为7,从左到右第一个大于目标值的元素是8(索引值为7),即目标值7的右边界的右邻居。而给定目标值为6时,从左到右第一个大于目标值的元素是第一个7,即搜索目标值6时循环结束后上界索引值。同样,这里循环不变式完全一样,只是后置条件有所不同:</p>\n<ul>\n<li>如果目标值不小于数组序列末尾元素,返回-1。</li>\n<li>否则返回搜索结果的上界索引值。</li>\n</ul>\n<p>对应的C语言实现如下</p>\n<figure class=\"highlight c\"><figcaption><span>二分查找 - 定位第一个大于目标值的元素</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">binSearch_firstgreater</span><span class=\"params\">(<span class=\"keyword\">int</span> a[], <span class=\"keyword\">int</span> n, <span class=\"keyword\">int</span> v)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> low, mid, high;</span><br><span class=\"line\"> low = <span class=\"number\">0</span>;</span><br><span class=\"line\"> high = n;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (low < high) {</span><br><span class=\"line\"> mid = low + (high - low) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (a[mid] > v) {</span><br><span class=\"line\"> high = mid;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> high == n ? <span class=\"number\">-1</span> <span class=\"comment\">/* v is not lesser than all */</span></span><br><span class=\"line\"> : high; <span class=\"comment\">/* find the first greater */</span></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"旋转数组\">旋转数组</h3>\n<p>如果输入的数组并非是完全升序的,二分查找算法还能达到特定的搜索目的吗?这要取决于数组的序列特征。</p>\n<p>考虑数组 [0,1,2,4,5,6,7],旋转4次后变成 [4,5,6,7,0,1,2],即后4位元素依次循环移位到前面。如果给定一个没有重复元素的旋转数组,我们可不可以使用二分查找算法找到其最小元素的位置(从而也推断出它的旋转次数)?答案是肯定的。</p>\n<p>仔细观察旋转数组,我们可以发现一个普遍规律:当旋转次数不为零时,如果中间值大于左边(下界)的值,那么最小值就出现在中间值的右侧(例如,对于[4,5,6,7,0,1,2],中间值是7,最小值0出现在7的右侧);如果中间值小于左边(下界)的值,最小值一定不会出现在中间值的右侧。据此,初始化low为0,high为n-1,我们可以将循环不变式表述为:最小值的索引<span class=\"math inline\">\\(i\\)</span>总是包含在由low和high界定的子数组中,即 <span class=\"math inline\">\\(i∈[low,high]\\)</span>。</p>\n<p>相应的Python函数实现如下</p>\n<figure class=\"highlight python\"><figcaption><span>二分查找 - 寻找旋转数组的最小元素</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">findMin</span>(<span class=\"params\">nums: <span class=\"built_in\">list</span>[<span class=\"built_in\">int</span>]</span>) -> <span class=\"built_in\">int</span>:</span></span><br><span class=\"line\"> low, high = <span class=\"number\">0</span>, <span class=\"built_in\">len</span>(nums) - <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> low < high:</span><br><span class=\"line\"> <span class=\"keyword\">if</span> nums[low] < nums[high]: <span class=\"comment\"># no rotation</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> nums[low]</span><br><span class=\"line\"> mid = (high + low) // <span class=\"number\">2</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> nums[mid] >= nums[low]:</span><br><span class=\"line\"> low= mid + <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>: <span class=\"comment\"># nums[mid] < nums[low]</span></span><br><span class=\"line\"> high = mid</span><br><span class=\"line\"> <span class=\"keyword\">return</span> nums[low]</span><br></pre></td></tr></table></figure>\n<p>程序说明:</p>\n<ul>\n<li>当<code>nums[low] < nums[high]</code>时,当前区间内数组无旋转(比如 [0,1,2,3,4,5,6,7] 或 [0,1,2]),从循环不变式的条件可以马上推断,<code>nums[low]</code>就是最小值。返回值满足后置条件。</li>\n<li>对于第二个<code>if</code>语句,当<code>nums[mid]>= nums[low]</code>时,通过总结的规律,我们知道最小值出现在中间值的右手边。这时候因为先决条件是数组没有重复元素,而<code>nums[mid]>= nums[low]</code>(等号只有在<code>low</code>与<code>mid</code>相同时才成立,这在区间只有两个元素是会发生),同时第一个<code>if</code>语句又确定了<code>nums[high]</code>比<code>nums[low]</code>小,所以中间值不可能是最小值。因此,我们可以将下界<code>low</code>设置为<code>mid+1</code>,循环不变式保持不变。</li>\n<li>对于<code>nums[mid] < nums[low]</code>的情况,我们知道,最小值出现在中间值的左手边。然而,中间值可以是最小值,因此我们将上界<code>high</code>设置为中间值以保持循环不变式为真。</li>\n<li>当<code>low</code>与 <code>high</code>相等时,上下界指向同一元素。这时退出循环,函数可返回<code>nums[low]</code>。</li>\n</ul>\n<p>实际上,这就是刷题网站<a href=\"https://ssg.leetcode-cn.com/problems/find-minimum-in-rotated-sorted-array/\">力扣(LeetCode)上第153号问题(寻找旋转排序数组中的最小值)</a>的答案。</p>\n<p>其实换一种思路,还可以得到此题的第二种解法。如果将中间值与最右侧(上界)进行比较,当中间值大于最右侧值时,最小值一定出现在中间值的右侧;当中间值小于最右侧值时,最小值一定不会出现在中间值的右侧。这一结论对于旋转次数为零及区间只剩两个元素时都成立!从这里我们可以修改Python实现如下</p>\n<figure class=\"highlight python\"><figcaption><span>二分查找 - 寻找旋转数组的最小元素(解法2)</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">findMin2</span>(<span class=\"params\">nums: <span class=\"built_in\">list</span>[<span class=\"built_in\">int</span>]</span>) -> <span class=\"built_in\">int</span>:</span></span><br><span class=\"line\"> low, high = <span class=\"number\">0</span>, <span class=\"built_in\">len</span>(nums) - <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> low < high:</span><br><span class=\"line\"> mid = (high + low) // <span class=\"number\">2</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> nums[mid] > nums[high]:</span><br><span class=\"line\"> low= mid + <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> high = mid</span><br><span class=\"line\"> <span class=\"keyword\">return</span> nums[low]</span><br></pre></td></tr></table></figure>\n<p>此解法的代码看起来更简洁一些,提交LeetCode后同样通过无误。</p>\n<h3 id=\"k邻近元素\">K邻近元素</h3>\n<p>下一个应用二分查找实现高效解法的例子,是<a href=\"https://leetcode.cn/problems/find-k-closest-elements/\">力扣上第658号问题:找到 K 个最接近的元素</a>。题目的要求描述如下</p>\n<blockquote>\n<p>给定一个排序好的数组 arr,两个整数 k 和 x,从数组中找到最靠近 x(两数之差最小)的 k 个数。返回的结果必须要是按升序排好的。</p>\n<p>整数 a 比整数 b 更接近 x 需要满足:</p>\n<ul>\n<li>|a - x| < |b - x| 或者<br />\n</li>\n<li>|a - x| == |b - x| 且 a < b</li>\n</ul>\n<p><strong>示例 1:</strong></p>\n<p><strong>输入:</strong>arr = [1,2,3,4,5], k = 4, x = 3<br />\n<strong>输出:</strong>[1,2,3,4]</p>\n<p><strong>示例 2:</strong></p>\n<p><strong>输入:</strong>arr = [1,2,3,4,5], k = 4, x = -1<br />\n<strong>输出:</strong>[1,2,3,4]</p>\n<p><strong>提示:</strong></p>\n<ul>\n<li>1 <= k <= arr.length</li>\n<li>1 <= arr.length <= <span class=\"math inline\">\\(10^4\\)</span></li>\n<li>arr 按升序排列</li>\n<li><span class=\"math inline\">\\(-10^4\\)</span> <= arr[i], x <= <span class=\"math inline\">\\(10^4\\)</span></li>\n</ul>\n</blockquote>\n<p>此题的难度为中等,解法比较多。比较直观的一种解法是计算每个数组元素和 x 的差值的绝对值(即距离),然后最小堆实现找出距离最近的 k 个元素。无疑,这种解法时间复杂度是 <span class=\"math inline\">\\(O(n)\\)</span>。它没有利用数组升序排列的性质,效率不高。</p>\n<p>第二种解法是使用双指针的排除法。由于最后要保留 k 个元素,我们就需要删除 n - k 个元素。由于数组是升序排列的,这些被排除的元素一定都在两端,所以可以使用左右两边相互碰撞的方法。从最左端和最右端开始碰撞对比, 谁与目标 x 的距离小,谁就保留,另一个数则删除。经过 n - k 次重复之后就找到了左边界,输出 arr[low, low+k]。这种解法时间复杂度依然是 <span class=\"math inline\">\\(O(n)\\)</span>。</p>\n<p>还有更好的解法吗?其实应用二分查找,确实可以得到时间复杂度是 <span class=\"math inline\">\\(O(\\log n)\\)</span> 的最优解。以 n = 7 的数组序列 [3,10,19,57,68,71,72] 为例,仔细观察 k = 3 时不同 x 值情况下的输出:</p>\n<ul>\n<li>x = 5 => [3,10,19]</li>\n<li>x = 20 => [10,19,57]</li>\n<li>x = 45 => [19,57,68]</li>\n<li>x = 64 => [57,68,71]</li>\n<li>x = 72 => [68,71,72]</li>\n</ul>\n<p>可以看到,不论输入值 x 的变化如何,输出结果一定是输入数组序列的子数组,其长度为 k。这相当于应用一个滑动窗口从输入数组中截取一个长度为 k 的子集。此子集的起始位置索引值一定在 [0, n - k] 范围内。所以定位符合要求的子数组,等同于查找一个最优的起始位置 <span class=\"math inline\">\\(i∈[0,n-k]\\)</span>。因为数组是升序排列的,只要每次循环中保证最优的起始位置一定在收缩的区间内,二分查找就是可行的。</p>\n<p>然而,在循环体内如何判定舍弃哪一半呢?这似乎是一个难题。对此,我们可以考察 [mid, mid + k] 区间,比较两端(即arr[mid] 与 arr[mid + k])和 x 的距离,来决定最优解的窗口应该朝哪个方向滑动。以同样数组序列 [3,10,19,57,68,71,72] 和输入 k and x 数值为例,下表总结了五种可能的情况:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">x</th>\n<th style=\"text-align: center;\">arr[0]</th>\n<th style=\"text-align: center;\">arr[1]</th>\n<th style=\"text-align: center;\">arr[2]</th>\n<th style=\"text-align: center;\">arr[3]</th>\n<th style=\"text-align: center;\">arr[4]</th>\n<th style=\"text-align: center;\">arr[5]</th>\n<th style=\"text-align: center;\">arr[6]</th>\n<th style=\"text-align: center;\">判定</th>\n<th style=\"text-align: center;\">更新</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">5</td>\n<td style=\"text-align: center;\">3(low)</td>\n<td style=\"text-align: center;\">10</td>\n<td style=\"text-align: center;\"><u>19</u>(mid)</td>\n<td style=\"text-align: center;\"><u>57</u></td>\n<td style=\"text-align: center;\"><u>68</u>(high)</td>\n<td style=\"text-align: center;\"><u>71</u></td>\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">x 位于区间左侧边界外</td>\n<td style=\"text-align: center;\">舍弃右半区间 (high = mid)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">20</td>\n<td style=\"text-align: center;\">3(low)</td>\n<td style=\"text-align: center;\">10</td>\n<td style=\"text-align: center;\"><u>19</u>(mid)</td>\n<td style=\"text-align: center;\"><u>57</u></td>\n<td style=\"text-align: center;\"><u>68</u>(high)</td>\n<td style=\"text-align: center;\"><u>71</u></td>\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">x 在区间内,靠近左侧边界</td>\n<td style=\"text-align: center;\">舍弃右半区间 (high = mid)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">45</td>\n<td style=\"text-align: center;\">3(low)</td>\n<td style=\"text-align: center;\">10</td>\n<td style=\"text-align: center;\"><u>19</u>(mid)</td>\n<td style=\"text-align: center;\"><u>57</u></td>\n<td style=\"text-align: center;\"><u>68</u>(high)</td>\n<td style=\"text-align: center;\"><u>71</u></td>\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">x 在区间内中心位置</td>\n<td style=\"text-align: center;\">舍弃右半区间 (high = mid)</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">64</td>\n<td style=\"text-align: center;\">3(low)</td>\n<td style=\"text-align: center;\">10</td>\n<td style=\"text-align: center;\"><u>19</u>(mid)</td>\n<td style=\"text-align: center;\"><u>57</u></td>\n<td style=\"text-align: center;\"><u>68</u>(high)</td>\n<td style=\"text-align: center;\"><u>71</u></td>\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">x 在区间内,靠近右侧边界</td>\n<td style=\"text-align: center;\">舍弃左半区间 (low = mid + 1)</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">3(low)</td>\n<td style=\"text-align: center;\">10</td>\n<td style=\"text-align: center;\"><u>19</u>(mid)</td>\n<td style=\"text-align: center;\"><u>57</u></td>\n<td style=\"text-align: center;\"><u>68</u>(high)</td>\n<td style=\"text-align: center;\"><u>71</u></td>\n<td style=\"text-align: center;\">72</td>\n<td style=\"text-align: center;\">x 位于区间右侧边界外</td>\n<td style=\"text-align: center;\">舍弃左半区间 (low = mid + 1)</td>\n</tr>\n</tbody>\n</table>\n<p>说明:</p>\n<ul>\n<li>对于此输入数组序列,n = 7, k = 3,所以初始化二分查找的上下界分别为 0 和 4,中间位置索引值 mid = 2。考察的区间为 [2, 5],对应子数组 [19,57,68,71]。这些元素在上表中都用下划线标出。</li>\n<li>当输入 x 数值为5时,x 位于考察区间对应的子数组左侧边界外。无疑最优解的起始位置不可能在元素 19 右侧,因为那边的元素的数值离 x 越来越远。我们可以放心地舍弃右半区,更新 high = mid。</li>\n<li>当输入 x 数值为20时,(20 - 19) < (71 - 20),所以 x 在区间内且靠近左侧边界。此时最优解的起始位置也不可能在元素 19 右侧。同样我们舍弃右半区,更新 high = mid。</li>\n<li>当输入x 数值为45时,(45 - 19) = (71 - 45),所以 x 正好在考察区间中点。依据题目的要求,左右距离相等时选择左侧的点。我们一样舍弃右半区,更新 high = mid。</li>\n<li>当输入 x 数值为64或72时,x 要么位于区间内靠近右侧边界,要么位于区间右侧边界外。此时最优解的起始位置不可能在元素 57 左侧。我们可以舍弃左半区,更新 low = mid + 1</li>\n</ul>\n<p>确定了循环更新的判定后,就可以直接编写Python代码实现了。对于上面的五种情况,虽然每一种都可以写出单独的比较语句,但这样程序效率太低了。实际上,舍弃左半区的两种情况用一个判定语句 (x - arr[mid]) > (arr[mid + k] - x) 就可以了。x 位于区间内靠近右侧边界时,这一条件判定当然为真。而当 x 位于区间右侧边界外时,此式的右边为负、左边为正,判定还是为真。对于其他三种舍弃右半区的情况,此判定都为假。由此我们得到Python函数实现如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">findClosestElements</span>(<span class=\"params\">arr: <span class=\"built_in\">list</span>[<span class=\"built_in\">int</span>], k: <span class=\"built_in\">int</span>, x: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">list</span>[<span class=\"built_in\">int</span>]:</span></span><br><span class=\"line\"> low, high = <span class=\"number\">0</span>, <span class=\"built_in\">len</span>(arr) - k</span><br><span class=\"line\"> <span class=\"keyword\">while</span> low < high:</span><br><span class=\"line\"> mid = (low + high) // <span class=\"number\">2</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (x - arr[mid]) > (arr[mid + k] - x):</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> high = mid</span><br><span class=\"line\"> <span class=\"keyword\">return</span> arr[low:(low + k)]</span><br></pre></td></tr></table></figure>\n<p>此代码中 while 循环继续的条件是 <code>low < high</code>,似乎与对称边界(<span class=\"math inline\">\\(i∈[0,n-k]\\)</span>)的一般解法不符。其实这是因为当<code>low</code>和<code>high</code>相等时,我们已经得到了最优解,不必再进入循环体。</p>\n<h3 id=\"n次方根\">N次方根</h3>\n<p>另一类可用二分查找算法解决的变形问题,就是求方根。在前文“<a href=\"https://www.packetmania.net/2021/07/23/PGITVW-2-sqrt/\">平方根运算</a>”,就分别讲解了计算整数和浮点数平方根的二分查找/搜索算法实现。这里再给出一个通用的计算整数 n 次方根的二分查找算法。</p>\n<p>对于整数 n 次方根,契约编程的各项要素是不同于数组输入的二分查找算法的。以下逐一加以说明:</p>\n<ul>\n<li>先决条件:函数输入是两个整数 v 和 n,目标是求 v 的 n 次方根的整数部分。</li>\n<li>后置条件:输出为一个不小于零的整数,为 v 的 n 次方根取整后的值。</li>\n<li>循环不变式:这里的搜索空间是从1到 n 的整个自然数集合。每次循环中,v 的整数 n 次方根都会在当前左闭右开区间内 [low, high)\n<ul>\n<li>下界 <span class=\"math inline\">\\(low\\leqslant \\lfloor\\sqrt[n]{x}\\rfloor\\)</span></li>\n<li>上界 <span class=\"math inline\">\\(high >\\lfloor\\sqrt[n]{x}\\rfloor\\)</span></li>\n</ul></li>\n<li>限定函数:因为上下界会根据 v 与当前的中间元素比较的结果而改变,总体区间不断缩减的,循环次数被限定。</li>\n</ul>\n<p>显然,在循环体内确定上界或下界更新的断定条件,应该是中间值的 n 次幂和目标值 v 的比较结果。由此,得到求整数 n 次方根的不对称边界解法的Python语言实现如下:</p>\n<figure class=\"highlight python\"><figcaption><span>二分查找 - 求整数n次方根</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">binSearch_introot</span>(<span class=\"params\">v: <span class=\"built_in\">int</span>, n: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">int</span>:</span></span><br><span class=\"line\"> <span class=\"string\">"""</span></span><br><span class=\"line\"><span class=\"string\"> Use the binary search method to find out the integer</span></span><br><span class=\"line\"><span class=\"string\"> component of the n'th root of v, an integer i such that</span></span><br><span class=\"line\"><span class=\"string\"> i ** n <= v < (i + 1) ** n.</span></span><br><span class=\"line\"><span class=\"string\"> """</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span>(v >= <span class=\"number\">0</span>)</span><br><span class=\"line\"> </span><br><span class=\"line\"> low, mid, high = <span class=\"number\">0</span>, <span class=\"number\">0</span>, v</span><br><span class=\"line\"> <span class=\"keyword\">while</span> low < high:</span><br><span class=\"line\"> mid = (low + high) // <span class=\"number\">2</span></span><br><span class=\"line\"> power = mid ** n</span><br><span class=\"line\"> <span class=\"keyword\">if</span> power < v:</span><br><span class=\"line\"> low = mid + <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">elif</span> power > v:</span><br><span class=\"line\"> high = mid</span><br><span class=\"line\"> <span class=\"keyword\">else</span>: <span class=\"comment\"># power == v</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> mid</span><br><span class=\"line\"> <span class=\"keyword\">return</span> low - <span class=\"number\">1</span> <span class=\"keyword\">if</span> low > <span class=\"number\">1</span> <span class=\"keyword\">else</span> low</span><br></pre></td></tr></table></figure>\n<p>此外,在循环结束后,为满足后置条件,我们要检查<code>low</code>的数值。因为<code>low</code>的值在循环终止前被赋值<code>mid + 1</code>,而此操作使得<code>low < high</code>不再为真,所以必须减一。例外是输入 v 值为 1 的情况。</p>\n<h2 id=\"常见错误\">常见错误</h2>\n<p>下面是面试者在回答二分查找的编程实现问题时经常出现的错误:</p>\n<ul>\n<li>不能正确处理零元素或单个元素数组的情况(这时会出现上下界与中间位置重合)</li>\n<li>不能在数组的第一个或最后一个元素中找到目标值的情况,可能是初始化上下界造成的问题</li>\n<li>不能处理数组中的重复元素,特别是重复元素跨越两个子区间时</li>\n<li>不能处理目标值不存在造成的搜索失败的情况,原因也可能源自错误的上下界初始值\n<ul>\n<li>对称边界解法使用闭合区间 <span class=\"math inline\">\\(i∈[low,high]\\)</span>,通常应该设定<code>low = 0</code>和<code>high = n - 1</code></li>\n<li>不对称边界解法使用半闭合区间 <span class=\"math inline\">\\(i∈[low,high)\\)</span>,通常应该设定<code>low = 0</code>和<code>high = n</code></li>\n</ul></li>\n<li>没有正确设定 while 循环继续的条件,不理解使用 <= 和 < 的区别\n<ul>\n<li>对称边界解法一般使用<code>low <= high</code> (但也有例外,参见<a href=\"#旋转数组\">旋转数组</a>和<a href=\"#K邻近元素\">K邻近元素</a>)</li>\n<li>不对称边界解法一般使用<code>low < high</code>,因为 low 应该总是小于 high。</li>\n</ul></li>\n<li>在探测后缩小区间时,没有正确设定子区间的端点,导致无限循环\n<ul>\n<li>如果循环继续的条件是<code>low <= high</code>,则更新式一般为<code>low = mid + 1</code>或<code>high = mid - 1</code></li>\n<li>如果循环继续的条件是<code>low < high</code>,则更新式一般为<code>low = mid + 1</code>或<code>high = mid</code></li>\n</ul></li>\n<li>不能正确计算当前区间中间位置的索引值、地址(指针)值或用作比较的元素值\n<ul>\n<li>计算中间位置索引时整数溢出:<code>(low + high) / 2</code> 或<code>(low + high) >> 1</code>(C/C++/Java编程实现时会发生,Python用 <code>(low + high) // 2</code> OK)</li>\n<li>计算中间位置地址时做错误地使用指针相加运算:<code>(low + high) / 2</code>(C/C++编程实现时会发生)</li>\n<li>计算中间位置元素值时结果溢出:<code>mid * mid</code>(C/C++/Java编程实现时会发生,Python OK)</li>\n</ul></li>\n</ul>\n<p>如果深入理解契约编程,牢记二分查找的基本解题框架,写出正确的循环不变式并将其保持为真的思想贯彻在编程中,再特别留意以上几点容易出错的地方,就一定可以提交令人满意的答案。</p>\n<h2 id=\"glibc-库函数\">Glibc 库函数</h2>\n<p>在实际的应用系统开发时,一般不需要自己去写二分查找程序,更普遍的是调用已有的库函数。最后,介绍一下流行的Glibc库提供的二分查找工具函数 — bsearch():</p>\n<figure class=\"highlight c\"><figcaption><span>Glibc bsearch</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdlib.h></span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> *<span class=\"title\">bsearch</span><span class=\"params\">(<span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *key, <span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *base,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\"> <span class=\"keyword\">size_t</span> nmemb, <span class=\"keyword\">size_t</span> size,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\"> <span class=\"keyword\">int</span> (*compar)(<span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *, <span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *))</span></span>; </span><br></pre></td></tr></table></figure>\n<p>对它的简要说明如下</p>\n<blockquote>\n<p><strong>描述</strong></p>\n<p>bsearch()函数在由nmemb对象组成的数组中搜索一个与key指向的对象匹配的成员。数组的第一个成员的位置由base指定,每个成员的存储空间大小为size。</p>\n<p>数组的内容应该根据比较函数compar以升序排序。 compar函数应该有两个参数,依次指向key对象和数组成员。如果发现key对象分别小于、等于或大于数组的成员,函数应该返回一个小于、等于或大于零的整数。</p>\n<p><strong>返回值</strong></p>\n<p>bsearch()函数返回一个指向数组中匹配成员的指针,如果没有找到匹配的成员,则返回NULL。如果有多个成员匹配key对象,返回的元素是不确定的。</p>\n</blockquote>\n<p>可以看到,应用此函数时调用者必须传递一个与数组元素类型相关的比较函数compar()。如果面试时的问题给出了数组元素类型的结构定义,求职者应该能够写出正确的compar()实现和调用bsearch()的完整工作无误的程序。</p>\n<p>作为参考,bsearch()函数的完整实现如下</p>\n<figure class=\"highlight c\"><figcaption><span>glibc/bits/stdlib-bsearch.h</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\">__extern_inline <span class=\"keyword\">void</span> *</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"title\">bsearch</span> <span class=\"params\">(<span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *__key, <span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *__base, <span class=\"keyword\">size_t</span> __nmemb, <span class=\"keyword\">size_t</span> __size,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\">\t <span class=\"keyword\">__compar_fn_t</span> __compar)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">size_t</span> __l, __u, __idx;</span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *__p;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> __comparison;</span><br><span class=\"line\"> __l = <span class=\"number\">0</span>;</span><br><span class=\"line\"> __u = __nmemb;</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (__l < __u)</span><br><span class=\"line\"> {</span><br><span class=\"line\"> __idx = (__l + __u) / <span class=\"number\">2</span>;</span><br><span class=\"line\"> __p = (<span class=\"keyword\">const</span> <span class=\"keyword\">void</span> *) (((<span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *) __base) + (__idx * __size));</span><br><span class=\"line\"> __comparison = (*__compar) (__key, __p);</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (__comparison < <span class=\"number\">0</span>)</span><br><span class=\"line\">\t__u = __idx;</span><br><span class=\"line\"> <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (__comparison > <span class=\"number\">0</span>)</span><br><span class=\"line\">\t__l = __idx + <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">else</span></span><br><span class=\"line\">\t{</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">if</span> __GNUC_PREREQ(4, 6)</span></span><br><span class=\"line\"><span class=\"meta\"># <span class=\"meta-keyword\">pragma</span> GCC diagnostic push</span></span><br><span class=\"line\"><span class=\"meta\"># <span class=\"meta-keyword\">pragma</span> GCC diagnostic ignored <span class=\"meta-string\">"-Wcast-qual"</span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\">\t <span class=\"keyword\">return</span> (<span class=\"keyword\">void</span> *) __p;</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">if</span> __GNUC_PREREQ(4, 6)</span></span><br><span class=\"line\"><span class=\"meta\"># <span class=\"meta-keyword\">pragma</span> GCC diagnostic pop</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\">\t}</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">NULL</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>很明显,它使用了不对称边界解法。注意第12行,计算中间索引值的表达式<code>(__l + __u) / 2</code>似乎会产生整数溢出。实际上因为<code>__nmemb</code>不会大到超出段地址的范围,所以这不会发生。库函数的实现者这里以性能作为优先考虑。</p>\n","categories":["面试指南"],"tags":["Python编程","C/C++编程"]},{"title":"Python编程实现的教科书RSA","url":"/2021/03/01/Python-Textbook-RSA/","content":"<p>RSA加密算法是现代公钥密码学的核心技术之一,在互联网中应用广泛。作为公钥密码学的经典算法,教科书RSA的编程实现可以帮助我们迅速掌握其数学机理和设计思想,并积累重要的密码技术软件实现经验。这里详述Python3.8编程环境下教科书RSA的实现示例。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Random numbers should not be generated with a method chosen at random.</strong><br> <strong>— <em>Donald Knuth</em>(高德纳,著名计算机科学家,现代计算机科学的先驱人物,1974年图灵奖得主)</strong></p>\n</div>\n<h3 id=\"生成大素数\">生成大素数</h3>\n<p>RSA加密算法的安全性建立在大数素因数分解的数学难题之上。构造RSA加密系统的第一步,就是生成两个大的素数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>并计算模数<span class=\"math inline\">\\(N=pq\\)</span>。<span class=\"math inline\">\\(N\\)</span>就是RSA的密钥长度,越大越安全。现在实用的系统要求密钥长度不小于2048比特,对应的 <span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>各为1024比特左右。生成如此大的随机素数,一种通用的实效方法是基于概率的随机化算法,其过程如下:</p>\n<ol type=\"1\">\n<li>预选择设定比特长度的随机数</li>\n<li>用小素数做初级素性检测 (<a href=\"https://zh.wikipedia.org/zh-cn/埃拉托斯特尼筛法\">埃拉托斯特尼筛法</a>)\n<ul>\n<li>如果通过,继续第三步</li>\n<li>如果失败,返回第一步</li>\n</ul></li>\n<li>执行高级素性检测 (<a href=\"https://zh.wikipedia.org/zh-cn/米勒-拉宾检验\">米勒-拉宾算法</a>)\n<ul>\n<li>如果通过,输出认定的素数</li>\n<li>如果失败,返回第一步</li>\n</ul></li>\n</ol>\n<p>在这里的软件实现中,第一步可以直接生成奇数。同时出于演示的目的,第二步采用大于2的前50个素数做初级素性检测。整个过程如下面的流程图所示: <img src=\"finding-prime.jpg\" style=\"width:35.0%;height:35.0%\" /></p>\n<p>第一步的Python函数编程,需要从<code>random</code>库导入库函数<code>randrange()</code>。函数将输入的比特数n设置为2的指数,用来指定<code>randrange()</code>的起始和结束值,并把步长定为2以确保只输出n比特数随机奇数值:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">from</span> random <span class=\"keyword\">import</span> randrange</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">generate_n_bit_odd</span>(<span class=\"params\">n: <span class=\"built_in\">int</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''Generate a random odd number in the range [2**(n-1)+1, 2**n-1]'''</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span> n > <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> randrange(<span class=\"number\">2</span> ** (n - <span class=\"number\">1</span>) + <span class=\"number\">1</span>, <span class=\"number\">2</span> ** n, <span class=\"number\">2</span>)</span><br></pre></td></tr></table></figure>\n<p>第二步的代码很简单。先定义一个数组,元素是2以后的50个素数。然后在函数中用双循环实现初级素性检测,内部<code>for</code>循环用素数数组的元素逐个筛选,失败就马上中止回到外循环,再调用第一步的函数生成下一个候选奇数重新测试:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\"># The first 50 prime numbers after 2</span></span><br><span class=\"line\">first_50_primes = [<span class=\"number\">3</span>, <span class=\"number\">5</span>, <span class=\"number\">7</span>, <span class=\"number\">11</span>, <span class=\"number\">13</span>, <span class=\"number\">17</span>, <span class=\"number\">19</span>, <span class=\"number\">23</span>, <span class=\"number\">29</span>, <span class=\"number\">31</span>,</span><br><span class=\"line\"> <span class=\"number\">37</span>, <span class=\"number\">41</span>, <span class=\"number\">43</span>, <span class=\"number\">47</span>, <span class=\"number\">53</span>, <span class=\"number\">59</span>, <span class=\"number\">61</span>, <span class=\"number\">67</span>, <span class=\"number\">71</span>, <span class=\"number\">73</span>,</span><br><span class=\"line\"> <span class=\"number\">79</span>, <span class=\"number\">83</span>, <span class=\"number\">89</span>, <span class=\"number\">97</span>, <span class=\"number\">101</span>, <span class=\"number\">103</span>, <span class=\"number\">107</span>, <span class=\"number\">109</span>, <span class=\"number\">113</span>, <span class=\"number\">127</span>,</span><br><span class=\"line\"> <span class=\"number\">131</span>, <span class=\"number\">137</span>, <span class=\"number\">139</span>, <span class=\"number\">149</span>, <span class=\"number\">151</span>, <span class=\"number\">157</span>, <span class=\"number\">163</span>, <span class=\"number\">167</span>, <span class=\"number\">173</span>, <span class=\"number\">179</span>,</span><br><span class=\"line\"> <span class=\"number\">181</span>, <span class=\"number\">191</span>, <span class=\"number\">193</span>, <span class=\"number\">197</span>, <span class=\"number\">199</span>, <span class=\"number\">211</span>, <span class=\"number\">223</span>, <span class=\"number\">227</span>, <span class=\"number\">229</span>, <span class=\"number\">233</span>]</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">get_lowlevel_prime</span>(<span class=\"params\">n</span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Generate a prime candidate not divisible by first primes"""</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> <span class=\"literal\">True</span>:</span><br><span class=\"line\"> <span class=\"comment\"># Obtain a random odd number</span></span><br><span class=\"line\"> c = generate_n_bit_odd(n)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># Test divisibility by pre-generated primes</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> divisor <span class=\"keyword\">in</span> first_50_primes:</span><br><span class=\"line\"> <span class=\"keyword\">if</span> c % divisor == <span class=\"number\">0</span> <span class=\"keyword\">and</span> divisor ** <span class=\"number\">2</span> <= c:</span><br><span class=\"line\"> <span class=\"keyword\">break</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> <span class=\"comment\"># The for loop did not encounter a break statement,</span></span><br><span class=\"line\"> <span class=\"comment\"># so it passes low level primality test.</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> c</span><br></pre></td></tr></table></figure>\n<p>第三步的米勒-拉宾素性检验<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>,是当前普遍使用的一种素数判定法则。它利用随机化算法判断一个数是合数还是可能是素数。虽然同样基于<a href=\"https://packetmania.github.io/2021/02/14/Fermats-Little-Theorem/\">费马小定理</a>,米勒-拉宾素性检验比费马素性检验效率高得多。在展示米勒-拉宾素性检验的Python实现之前,简要介绍一下其工作原理。</p>\n<p>根据费马小定理 ,对于一个素数<span class=\"math inline\">\\(n\\)</span> ,如果整数<span class=\"math inline\">\\(a\\)</span>不是<span class=\"math inline\">\\(n\\)</span>的倍数,则有<span class=\"math inline\">\\(a^{n-1}\\equiv 1\\pmod n\\)</span>。从这里出发,如果<span class=\"math inline\">\\(n>2\\)</span>,<span class=\"math inline\">\\(n-1\\)</span>是一个偶数,一定可以被表示为<span class=\"math inline\">\\(2^{s}*d\\)</span>的形式,<span class=\"math inline\">\\(s\\)</span>和<span class=\"math inline\">\\(d\\)</span>都是正整数且<span class=\"math inline\">\\(d\\)</span>是奇数。由此得到 <span class=\"math display\">\\[a^{2^{s}*d}\\equiv 1\\pmod n\\]</span> 这时如果不断对上式左边取平方根再取模,总会得到<span class=\"math inline\">\\(1\\)</span>或<span class=\"math inline\">\\(-1\\)</span><a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>。如果得到了<span class=\"math inline\">\\(-1\\)</span> ,意味着下面②式成立;如果从未得到<span class=\"math inline\">\\(-1\\)</span>,则①式成立: <span class=\"math display\">\\[a^{d}\\equiv 1{\\pmod {n}}{\\text{ ①}}\\]</span> <span class=\"math display\">\\[a^{2^{r}d}\\equiv -1{\\pmod {n}}{\\text{ ②}}\\]</span> 其中<span class=\"math inline\">\\(r\\)</span>是位于<span class=\"math inline\">\\([0, s-1]\\)</span>区间的某个整数。所以,如果<span class=\"math inline\">\\(n\\)</span>是大于<span class=\"math inline\">\\(2\\)</span>的素数,一定有①或②式成立。这一规律的<u>逆否命题</u>也为真,即<strong>如果我们能找到这样一个<span class=\"math inline\">\\(\\pmb{a}\\)</span>,使得对任意<span class=\"math inline\">\\(\\pmb{0\\leq r\\leq s-1}\\)</span>以下两个式子均满足: <span class=\"math display\">\\[\\pmb{a^{d}\\not \\equiv 1\\pmod n}\\]</span> <span class=\"math display\">\\[\\pmb{a^{2^{r}d}\\not \\equiv -1\\pmod n}\\]</span> 那么<span class=\"math inline\">\\(\\pmb{n}\\)</span>一定不是一个素数</strong>。这就是米勒-拉宾素性测试的机理。对于待测数<span class=\"math inline\">\\(n\\)</span>,算出<span class=\"math inline\">\\(s\\)</span>和<span class=\"math inline\">\\(d\\)</span>的值后,随机选取基数<span class=\"math inline\">\\(a\\)</span>,迭代检测以上两式。如果都不成立,<span class=\"math inline\">\\(n\\)</span>为合数,否则<span class=\"math inline\">\\(n\\)</span>可能为素数。重复这一过程,<span class=\"math inline\">\\(n\\)</span>为真素数的概率会越来越大。计算表明,经过<span class=\"math inline\">\\(k\\)</span>轮测试,米勒-拉宾素性检验的差错率最高不超过<span class=\"math inline\">\\(4^{-k}\\)</span>。</p>\n<p>Python实现的米勒-拉宾素性检验函数如下,代码中的变量<code>n,s,d,k</code>与上面的说明对应:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">miller_rabin_primality_check</span>(<span class=\"params\">n, k=<span class=\"number\">20</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''Miller-Rabin Primality Test wwith specified round of test </span></span><br><span class=\"line\"><span class=\"string\"> Input:</span></span><br><span class=\"line\"><span class=\"string\"> n - n > 3, an odd integer to be tested for primality</span></span><br><span class=\"line\"><span class=\"string\"> k - the number of rounds of testing to perfor</span></span><br><span class=\"line\"><span class=\"string\"> Output:</span></span><br><span class=\"line\"><span class=\"string\"> True - passed (n is a strong probable prime)</span></span><br><span class=\"line\"><span class=\"string\"> False - failed (n is a composite)'''</span></span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\"># For a given odd integer n > 3, write n as (2^s)*d+1,</span></span><br><span class=\"line\"> <span class=\"comment\"># where s and d are positive integers and d is odd.</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span> n > <span class=\"number\">3</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> n % <span class=\"number\">2</span> == <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">False</span></span><br><span class=\"line\"> </span><br><span class=\"line\"> s, d = <span class=\"number\">0</span>, n - <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> d % <span class=\"number\">2</span> == <span class=\"number\">0</span>:</span><br><span class=\"line\"> d >>= <span class=\"number\">1</span></span><br><span class=\"line\"> s += <span class=\"number\">1</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> _ <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(k):</span><br><span class=\"line\"> a = randrange(<span class=\"number\">2</span>, n - <span class=\"number\">1</span>)</span><br><span class=\"line\"> x = <span class=\"built_in\">pow</span>(a, d, n)</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">if</span> x == <span class=\"number\">1</span> <span class=\"keyword\">or</span> x == n - <span class=\"number\">1</span>:</span><br><span class=\"line\"> <span class=\"keyword\">continue</span></span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"keyword\">for</span> _ <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(s):</span><br><span class=\"line\"> x = <span class=\"built_in\">pow</span>(x, <span class=\"number\">2</span>, n)</span><br><span class=\"line\"> <span class=\"keyword\">if</span> x == n - <span class=\"number\">1</span>:</span><br><span class=\"line\"> <span class=\"keyword\">break</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> <span class=\"comment\"># The for loop did not encounter a break statement,</span></span><br><span class=\"line\"> <span class=\"comment\"># so it fails the test, it must be a composite</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">False</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># Passed the test, it is a strong probable prime</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">True</span></span><br></pre></td></tr></table></figure>\n<p>综合以上所有,可以将整个过程包装到以下的函数,函数输入为比特数,输出为认定的随机大素数:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">get_random_prime</span>(<span class=\"params\">num_bits</span>):</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> <span class=\"literal\">True</span>:</span><br><span class=\"line\"> pp = get_lowlevel_prime(num_bits)</span><br><span class=\"line\"> <span class=\"keyword\">if</span> miller_rabin_primality_check(pp):</span><br><span class=\"line\"> <span class=\"keyword\">return</span> pp</span><br></pre></td></tr></table></figure>\n<h3 id=\"工具函数\">工具函数</h3>\n<ol type=\"1\">\n<li><p><strong>最大公约数函数<code>gcd(a,b)</code>和最小公倍数函数<code>lcm(a,b)</code>:</strong><br />\nRSA加密算法需要计算模数<span class=\"math inline\">\\(N\\)</span>的卡迈克尔函数<span class=\"math inline\">\\(\\lambda(N)\\)</span>,其公式是<span class=\"math inline\">\\(\\lambda(pq)=\\operatorname{lcm}(p − 1, q − 1)\\)</span>,这里用到了最小公倍数函数。最小公倍数与最大公约数的关系是:<span class=\"math display\">\\[\\operatorname{lcm}(a,b)={\\frac{(a\\cdot b)}{\\gcd(a,b)}}\\]</span> 而求最大公约数有高效的欧几里得算法 (辗转相除法),其原理是:两个整数的最大公约数等于其中较小的数和两数相除余数的最大公约数。欧几里得算法的具体实现可以使用迭代或递归方法。这里应用迭代实现最大公约数函数,两个函数的Python代码如下:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">gcd</span>(<span class=\"params\">a, b</span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''Computes the Great Common Divisor using the Euclid's algorithm'''</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> b:</span><br><span class=\"line\"> a, b = b, a % b</span><br><span class=\"line\"> <span class=\"keyword\">return</span> a</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">lcm</span>(<span class=\"params\">a, b</span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Computes the Lowest Common Multiple using the GCD method."""</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> a // gcd(a, b) * b</span><br></pre></td></tr></table></figure></p></li>\n<li><p><strong>扩展欧几里得算法<code>exgcd(a,b)</code>和模逆元函数<code>invmod(e,m)</code>:</strong><br />\nRSA密钥对满足等式<span class=\"math inline\">\\((d⋅e)\\bmod \\lambda(N)=1\\)</span>,即二者互为关于<span class=\"math inline\">\\(\\lambda(N)\\)</span>的模逆元。应用扩展欧几里得算法可以快速求解公钥指数<span class=\"math inline\">\\(e\\)</span>的模逆元<span class=\"math inline\">\\(d\\)</span>。算法原理是给定整数<span class=\"math inline\">\\(a、b\\)</span>,可以在求得<span class=\"math inline\">\\(a、b\\)</span>的最大公约数的同时,找到整数<span class=\"math inline\">\\(x、y\\)</span> (其中一个很可能是负数),使它们满足裴蜀等式:<span class=\"math display\">\\[a⋅x+b⋅y=\\gcd(a, b)\\]</span>代入RSA加密算法的参数<span class=\"math inline\">\\(a=e\\)</span>,<span class=\"math inline\">\\(b=m=\\lambda(N)\\)</span>,又由于<span class=\"math inline\">\\(e\\)</span>与<span class=\"math inline\">\\(\\lambda(N)\\)</span>互素,可以得到:<span class=\"math display\">\\[e⋅x+m⋅y=1\\]</span>这样解出的<span class=\"math inline\">\\(x\\)</span>就是<span class=\"math inline\">\\(e\\)</span>的模逆元<span class=\"math inline\">\\(d\\)</span>。下面给出这两个函数的Python实现:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">exgcd</span>(<span class=\"params\">a, b</span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Extended Euclidean Algorithm that can give back all gcd, s, t </span></span><br><span class=\"line\"><span class=\"string\"> such that they can make Bézout's identity: gcd(a,b) = a*s + b*t</span></span><br><span class=\"line\"><span class=\"string\"> Return: (gcd, s, t) as tuple"""</span></span><br><span class=\"line\"> old_s, s = <span class=\"number\">1</span>, <span class=\"number\">0</span></span><br><span class=\"line\"> old_t, t = <span class=\"number\">0</span>, <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> b:</span><br><span class=\"line\"> q = a // b</span><br><span class=\"line\"> s, old_s = old_s - q * s, s</span><br><span class=\"line\"> t, old_t = old_t - q * t, t</span><br><span class=\"line\"> a, b = b, a % b</span><br><span class=\"line\"> <span class=\"keyword\">return</span> a, old_s, old_t</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">invmod</span>(<span class=\"params\">e, m</span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Find out the modular multiplicative inverse x of the input integer</span></span><br><span class=\"line\"><span class=\"string\"> e with respect to the modulus m. Return the minimum positive x"""</span></span><br><span class=\"line\"> g, x, y = exgcd(e, m)</span><br><span class=\"line\"> <span class=\"keyword\">assert</span> g == <span class=\"number\">1</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># Now we have e*x + m*y = g = 1, so e*x ≡ 1 (mod m).</span></span><br><span class=\"line\"> <span class=\"comment\"># The modular multiplicative inverse of e is x.</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> x < <span class=\"number\">0</span>:</span><br><span class=\"line\"> x += m</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x</span><br></pre></td></tr></table></figure> 同样地,这里应用了迭代方法实现扩展欧几里得算法,模逆元函数调用了前者。</p></li>\n<li><p><strong>整数与字节序列相互转换函数:</strong><br />\nRSA加解密算法本身是操作于整数的模幂运算,而要加密的消息明文通常以字节序列表示,所以需要两个转换函数。加密时调用<code>uint_from_bytes()</code>先将字节序列转换为整数,再用公钥指数进行模幂运算;解密时次序相反,先用私钥指数进行模幂运算,再调用<code>uint_to_bytes()</code>将整数结果转换为字节序列,这样就可以恢复消息明文。Python实现的转换函数如下:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">uint_from_bytes</span>(<span class=\"params\">xbytes: <span class=\"built_in\">bytes</span></span>) -> <span class=\"built_in\">int</span>:</span></span><br><span class=\"line\"> <span class=\"string\">"""This works only for unsigned (non-negative) integers."""</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">int</span>.from_bytes(xbytes, <span class=\"string\">'big'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">uint_to_bytes</span>(<span class=\"params\">x: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">bytes</span>:</span></span><br><span class=\"line\"> <span class=\"string\">"""This works only for unsigned (non-negative) integers.</span></span><br><span class=\"line\"><span class=\"string\"> It does not work for 0."""</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> x == <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">bytes</span>(<span class=\"number\">1</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x.to_bytes((x.bit_length() + <span class=\"number\">7</span>) // <span class=\"number\">8</span>, <span class=\"string\">'big'</span>)</span><br></pre></td></tr></table></figure></p></li>\n</ol>\n<h3 id=\"实现rsa类\">实现RSA类</h3>\n<div class=\"note danger\"><p><strong>注意</strong>:教科书RSA有内在的安全漏洞。这里给出的Python语言参考实现仅用于学习和演示目的,不可用在实际的应用系统中,否则可能会造成严重的信息安全事故,切记!</p>\n</div>\n<p>基于面向对象的编程思想,可以设计将RSA的密钥数据和所有运算操作封装到一个Python类中。RSA类的解密和签名生成各自实现了常规和快速两种方法。快速方法基于中国余数定理和费马小定理 (参考《<a href=\"https://packetmania.github.io/2021/02/14/Fermats-Little-Theorem/\">费马小定理的归纳法证明和应用</a>》)。以下描述了RSA类的实现细节:</p>\n<ol type=\"1\">\n<li><p><strong>对象初始化函数</strong><br />\n初始化函数<code>__init__()</code>的用户自定义参数和缺省值如下:</p>\n<ul>\n<li>密钥长度:2048 (<span class=\"math inline\">\\(N\\)</span>)</li>\n<li>公钥指数:65537 (<span class=\"math inline\">\\(e\\)</span>)</li>\n<li>快速解密/签名:False (不设定)</li>\n</ul>\n<p>函数内部调用<code>get_random_prime()</code>函数生成两个大约为密钥长度一半的随机大素数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>,计算它们的卡迈克尔函数值并验证其是否与<span class=\"math inline\">\\(e\\)</span>互素,不是就重复直到找到为止。然后计算模数<span class=\"math inline\">\\(N\\)</span>和使用模逆元函数<code>invmod()</code>算出私钥指数<span class=\"math inline\">\\(d\\)</span>。如果需要快速解密/签名,还要计算出三个额外的参数: <span class=\"math display\">\\[\\begin{align}\nd_P&=d\\bmod (p-1)\\\\\nd_Q&=d\\bmod (q-1)\\\\\nq_{\\text{inv}}&=q^{-1}\\pmod {p}\n\\end{align}\\]</span></p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">RSA_DEFAULT_EXPONENT = <span class=\"number\">65537</span></span><br><span class=\"line\">RSA_DEFAULT_MODULUS_LEN = <span class=\"number\">2048</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"class\"><span class=\"keyword\">class</span> <span class=\"title\">RSA</span>:</span></span><br><span class=\"line\"> <span class=\"string\">"""Implements the RSA public key encryption/decryption with default</span></span><br><span class=\"line\"><span class=\"string\"> exponent 65537 and default key size 2048"""</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">__init__</span>(<span class=\"params\">self, key_length=RSA_DEFAULT_MODULUS_LEN,</span></span></span><br><span class=\"line\"><span class=\"params\"><span class=\"function\"> exponent=RSA_DEFAULT_EXPONENT, fast_decrypt=<span class=\"literal\">False</span></span>):</span></span><br><span class=\"line\"> self.e = exponent</span><br><span class=\"line\"> self.fast = fast_decrypt</span><br><span class=\"line\"> t = <span class=\"number\">0</span></span><br><span class=\"line\"> p = q = <span class=\"number\">2</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> gcd(self.e, t) != <span class=\"number\">1</span>:</span><br><span class=\"line\"> p = get_random_prime(key_length // <span class=\"number\">2</span>)</span><br><span class=\"line\"> q = get_random_prime(key_length // <span class=\"number\">2</span>)</span><br><span class=\"line\"> t = lcm(p - <span class=\"number\">1</span>, q - <span class=\"number\">1</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> self.n = p * q</span><br><span class=\"line\"> self.d = invmod(self.e, t)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (fast_decrypt):</span><br><span class=\"line\"> self.p, self.q = p, q</span><br><span class=\"line\"> self.d_P = self.d % (p - <span class=\"number\">1</span>)</span><br><span class=\"line\"> self.d_Q = self.d % (q - <span class=\"number\">1</span>)</span><br><span class=\"line\"> self.q_Inv = invmod(q, p)</span><br></pre></td></tr></table></figure></p></li>\n<li><p><strong>加解密成员函数</strong><br />\nRSA加密和常规解密公式为 <span class=\"math display\">\\[\\begin{align}\nc\\equiv m^e\\pmod N\\\\\nm\\equiv c^d\\pmod N\n\\end{align}\\]</span> Python的内建指数函数<code>pow()</code>支持模幂运算,只需要先做相应的整数与字节序列转换,再使用公钥指数或私钥指数调用<code>pow()</code>就可以实现上面两式:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">encrypt</span>(<span class=\"params\">self, binary_data: <span class=\"built_in\">bytes</span></span>):</span></span><br><span class=\"line\"> int_data = uint_from_bytes(binary_data)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">pow</span>(int_data, self.e, self.n)</span><br><span class=\"line\">\t</span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">decrypt</span>(<span class=\"params\">self, encrypted_int_data: <span class=\"built_in\">int</span></span>):</span></span><br><span class=\"line\"> int_data = <span class=\"built_in\">pow</span>(encrypted_int_data, self.d, self.n)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> uint_to_bytes(int_data)</span><br></pre></td></tr></table></figure> 对于快速解密,需要一些额外的步骤 <span class=\"math display\">\\[\\begin{align}\nm_1&=c^{d_P}\\pmod {p}\\tag{1}\\label{eq1}\\\\\nm_2&=c^{d_Q}\\pmod {q}\\tag{2}\\label{eq2}\\\\\nh&=q_{\\text{inv}}(m_1-m_2)\\pmod {p}\\tag{3}\\label{eq3}\\\\\nm&=m_{2}+hq\\pmod {pq}\\tag{4}\\label{eq4}\n\\end{align}\\]</span> 实际中,如果上面<span class=\"math inline\">\\((3)\\)</span>式中的<span class=\"math inline\">\\(m_1-m_2<0\\)</span>,需要加上<span class=\"math inline\">\\(p\\)</span>调整为正数。还可以看到,由于快速解密方法的模数和指数都降阶大致一半,加速比理论上会接近<span class=\"math inline\">\\(4\\)</span>。考虑额外计算步骤,实际的加速比估计值要减掉一个修正值 <span class=\"math inline\">\\(\\varepsilon\\)</span>,记为 <span class=\"math inline\">\\(4-\\varepsilon\\)</span>。快速解密函数的代码如下:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">decrypt_fast</span>(<span class=\"params\">self, encrypted_int_data: <span class=\"built_in\">int</span></span>):</span></span><br><span class=\"line\"> <span class=\"comment\"># Use Chinese Remaider Theorem + Fermat's Little Theorem to</span></span><br><span class=\"line\"> <span class=\"comment\"># do fast RSA description</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span> self.fast == <span class=\"literal\">True</span></span><br><span class=\"line\"> m1 = <span class=\"built_in\">pow</span>(encrypted_int_data, self.d_P, self.p)</span><br><span class=\"line\"> m2 = <span class=\"built_in\">pow</span>(encrypted_int_data, self.d_Q, self.q)</span><br><span class=\"line\"> t = m1 - m2</span><br><span class=\"line\"> <span class=\"keyword\">if</span> t < <span class=\"number\">0</span>:</span><br><span class=\"line\"> t += self.p</span><br><span class=\"line\"> h = (self.q_Inv * t) % self.p</span><br><span class=\"line\"> m = (m2 + h * self.q) % self.n</span><br><span class=\"line\"> <span class=\"keyword\">return</span> uint_to_bytes(m)</span><br></pre></td></tr></table></figure></p></li>\n<li><p><strong>签名生成和验证成员函数</strong><br />\nRSA数字签名的生成和验证函数与加密和常规解密很相似,不同的只是将公钥指数和私钥指数对调使用而已。签名生成使用私钥指数,而验证函数使用公钥指数。快速签名的实现与快速解密步骤一致,但是输入和输出的数据要做相应的转换调整。具体的实现如下:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">generate_signature</span>(<span class=\"params\">self, encoded_msg_digest: <span class=\"built_in\">bytes</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Use RSA private key to generate Digital Signature for given</span></span><br><span class=\"line\"><span class=\"string\"> encoded message digest"""</span></span><br><span class=\"line\"> int_data = uint_from_bytes(encoded_msg_digest)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">pow</span>(int_data, self.d, self.n)</span><br><span class=\"line\">\t</span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">generate_signature_fast</span>(<span class=\"params\">self, encoded_msg_digest: <span class=\"built_in\">bytes</span></span>):</span></span><br><span class=\"line\"> <span class=\"comment\"># Use Chinese Remaider Theorem + Fermat's Little Theorem to</span></span><br><span class=\"line\"> <span class=\"comment\"># do fast RSA signature generation</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span> self.fast == <span class=\"literal\">True</span></span><br><span class=\"line\"> int_data = uint_from_bytes(encoded_msg_digest)</span><br><span class=\"line\"> s1 = <span class=\"built_in\">pow</span>(int_data, self.d_P, self.p)</span><br><span class=\"line\"> s2 = <span class=\"built_in\">pow</span>(int_data, self.d_Q, self.q)</span><br><span class=\"line\"> t = s1 - s2</span><br><span class=\"line\"> <span class=\"keyword\">if</span> t < <span class=\"number\">0</span>:</span><br><span class=\"line\"> t += self.p</span><br><span class=\"line\"> h = (self.q_Inv * t) % self.p</span><br><span class=\"line\"> s = (s2 + h * self.q) % self.n</span><br><span class=\"line\"> <span class=\"keyword\">return</span> s</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">verify_signature</span>(<span class=\"params\">self, digital_signature: <span class=\"built_in\">int</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">"""Use RSA public key to decrypt given Digital Signature"""</span></span><br><span class=\"line\"> int_data = <span class=\"built_in\">pow</span>(digital_signature, self.e, self.n)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> uint_to_bytes(int_data)</span><br></pre></td></tr></table></figure></p></li>\n</ol>\n<h3 id=\"功能测试\">功能测试</h3>\n<p>完成了RSA类,就可以进行测试了。为测试基本的加解密功能,先初始化一个RSA对象,初始化参数为:</p>\n<ul>\n<li>密钥长度:512比特 (模数<span class=\"math inline\">\\(N\\)</span>)</li>\n<li>公钥指数:3</li>\n<li>快速解密/签名:True (设定)</li>\n</ul>\n<p>接下来就可以调用RSA对象实例的加密函数<code>encrypt()</code>加密定义好的消息,然后将密文分别输入到解密函数<code>decrypt()</code>和快速解密函数<code>decrypt_fast()</code>,并用<code>assert</code>语句比较结果和消息原文。代码如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\"># ---- Test RSA class ----</span></span><br><span class=\"line\">alice = RSA(<span class=\"number\">512</span>, <span class=\"number\">3</span>, <span class=\"literal\">True</span>)</span><br><span class=\"line\">msg = <span class=\"string\">b'Textbook RSA in Python'</span></span><br><span class=\"line\">ctxt = alice.encrypt(msg)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> alice.decrypt(ctxt) == msg</span><br><span class=\"line\"><span class=\"keyword\">assert</span> alice.decrypt_fast(ctxt) == msg</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"RSA message encryption/decryption test passes!"</span>)</span><br></pre></td></tr></table></figure>\n<p>同理,还可以测试签名功能。这时需要把下面<code>import</code>语句添加到文件首部</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">from</span> hashlib <span class=\"keyword\">import</span> sha1</span><br></pre></td></tr></table></figure>\n<p>这样就可以用库函数<code>sha1()</code>生成消息摘要,然后分别调用RSA对象实例的<code>generate_signature()</code> 函数和<code>generate_signature_fast()</code>函数产生签名。两个签名输入到验证函数<code>verify_signature()</code>得到的结果应该都与原消息摘要一致。如下所示:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">mdg = sha1(msg).digest()</span><br><span class=\"line\">sign1 = alice.generate_signature(mdg)</span><br><span class=\"line\">sign2 = alice.generate_signature_fast(mdg)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">assert</span> alice.verify_signature(sign1) == mdg</span><br><span class=\"line\"><span class=\"keyword\">assert</span> alice.verify_signature(sign2) == mdg</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"RSA signature generation/verification test passes!"</span>)</span><br></pre></td></tr></table></figure>\n<p>如果没有看到报告<code>AssertionError</code>,就会得到下面的输出,表明加解密和签名测试都通过了:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">RSA message encryption/decryption <span class=\"built_in\">test</span> passes!</span><br><span class=\"line\">RSA signature generation/verification <span class=\"built_in\">test</span> passes!</span><br></pre></td></tr></table></figure>\n<h3 id=\"性能测试\">性能测试</h3>\n<p>功能测试通过之后,可以来看看快速解密的性能如何。我们关心的是实际能达到的加速比是多少,这就需要对代码的执行计时。计时测量要从Python内建库<code>os</code>和<code>timeit</code>分别导入库函数<code>urandom()</code>和<code>timeit()</code>:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">from</span> os <span class=\"keyword\">import</span> urandom</span><br><span class=\"line\"><span class=\"keyword\">from</span> timeit <span class=\"keyword\">import</span> timeit</span><br></pre></td></tr></table></figure>\n<p><code>urandom()</code>用于产生随机字节序列,<code>timeit()</code>提供了对给定代码段的执行计时功能。作为辅助,先将要计时的RSA解密函数打包在两个函数中:</p>\n<ul>\n<li><code>decrypt_norm()</code> - 常规解密方法</li>\n<li><code>decrypt_fast()</code> - 快速解密方法</li>\n</ul>\n<p>两个函数都用<code>assert</code>语句核对结果,代码如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">decrypt_norm</span>(<span class=\"params\">tester, ctxt: <span class=\"built_in\">bytes</span>, msg: <span class=\"built_in\">bytes</span></span>):</span></span><br><span class=\"line\"> ptxt = tester.decrypt(ctxt)</span><br><span class=\"line\"> <span class=\"keyword\">assert</span> ptxt == msg</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">decrypt_fast</span>(<span class=\"params\">tester, ctxt: <span class=\"built_in\">bytes</span>, msg: <span class=\"built_in\">bytes</span></span>):</span></span><br><span class=\"line\"> ptxt = tester.decrypt_fast(ctxt)</span><br><span class=\"line\"> <span class=\"keyword\">assert</span> ptxt == msg</span><br></pre></td></tr></table></figure>\n<p>计时代码设置两个嵌套的<code>for</code>循环:</p>\n<ul>\n<li><p>外层循环遍历不同的密钥长度<code>klen</code>,从512比特到4096比特共5级,相应的RSA对象<code>obj</code>初始化为:</p>\n<ul>\n<li>密钥长度:<code>klen</code> (模数<span class=\"math inline\">\\(N\\)</span>)</li>\n<li>公钥指数:65537</li>\n<li>快速解密/签名:True (设定)</li>\n</ul>\n<p>外层循环里也设定变量<code>rpt</code>为密钥长度的平方根,并清零计时变量<code>t_n</code>和<code>t_f</code>。</p></li>\n<li><p>内层也循环5次,每次执行的操作是:</p>\n<ul>\n<li>调用<code>urandom()</code>生成比特长度为密钥长度一半、内容随机的字节序列<code>mg</code></li>\n<li>调用加密函数<code>obj.encrypt()</code>生成密文<code>ct</code></li>\n<li>调用<code>timeit()</code>并分别输入打包函数<code>decrypt_norm()</code>和<code>decrypt_fast()</code>及解密相关的参数<code>obj</code>、<code>ct</code>和<code>mg</code>,同时设置代码重复运行次数为<code>rpt</code></li>\n<li><code>timeit()</code>函数的返回值累加保存到<code>t_n</code>和<code>t_f</code></li>\n</ul></li>\n</ul>\n<p>每次内层循环结束,就打印当前的密钥长度、计时统计的均值和计算出来的加速比<code>t_n/t_f</code>。对应的实际程序段如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"Start RSA fast decryption profiling..."</span>)</span><br><span class=\"line\"><span class=\"keyword\">for</span> klen <span class=\"keyword\">in</span> [<span class=\"number\">512</span>, <span class=\"number\">1024</span>, <span class=\"number\">2048</span>, <span class=\"number\">3072</span>, <span class=\"number\">4096</span>]:</span><br><span class=\"line\"> rpt = <span class=\"built_in\">int</span>(klen ** <span class=\"number\">0.5</span>)</span><br><span class=\"line\"> obj = RSA(klen, <span class=\"number\">65537</span>, <span class=\"literal\">True</span>)</span><br><span class=\"line\"> t_n = t_f = <span class=\"number\">0</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> _ <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">5</span>):</span><br><span class=\"line\"> mg = urandom(<span class=\"built_in\">int</span>(klen/<span class=\"number\">16</span>))</span><br><span class=\"line\"> ct = obj.encrypt(mg)</span><br><span class=\"line\"> t_n += timeit(<span class=\"keyword\">lambda</span>: decrypt_norm(obj, ct, mg), number=rpt)</span><br><span class=\"line\"> t_f += timeit(<span class=\"keyword\">lambda</span>: decrypt_fast(obj, ct, mg), number=rpt) </span><br><span class=\"line\"> <span class=\"built_in\">print</span>(<span class=\"string\">"Key size %4d => norm %.4fs, fast %.4fs\\tSpeedup: %.2f"</span></span><br><span class=\"line\"> % (klen, t_n/<span class=\"number\">5</span>/rpt, t_f/<span class=\"number\">5</span>/rpt, t_n/t_f))</span><br></pre></td></tr></table></figure>\n<p>下面是在Macbook Pro上的运行结果:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">Start RSA fast decryption profiling...</span><br><span class=\"line\">Key size 512 => norm 0.0008s, fast 0.0003s Speedup: 2.43</span><br><span class=\"line\">Key size 1024 => norm 0.0043s, fast 0.0015s Speedup: 2.88</span><br><span class=\"line\">Key size 2048 => norm 0.0273s, fast 0.0085s Speedup: 3.19</span><br><span class=\"line\">Key size 3072 => norm 0.0835s, fast 0.0240s Speedup: 3.48</span><br><span class=\"line\">Key size 4096 => norm 0.1919s, fast 0.0543s Speedup: 3.53</span><br></pre></td></tr></table></figure>\n<p>测试结果证实了快速解密方法的有效性。随着密钥长度的增加,计算强度逐渐加大,核心解密运算所占的运行时间比重也更突出,所以加速比对应增长。但最终加速比趋于稳定值3.5左右,这与理论估计的上界 (<span class=\"math inline\">\\(4-\\varepsilon\\)</span>) 相一致。</p>\n<p>教科书RSA的Python编程实现,帮我们强化了所学的基础数论知识,也有助于深入理解RSA加密算法的机理。在此基础上,参考前文《<a href=\"https://packetmania.github.io/2020/12/01/RSA-attack-defense/\">RSA的攻与防(一)</a>》,还可以扩展试验一些RSA初级攻防技巧,进一步掌握这一公钥密码学的关键技术。完整的程序点击这里下载:<a href=\"textbook-rsa.py.gz\">textbook-rsa.py.gz</a></p>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>卡内基梅隆大学的计算机系教授加里·米勒 (Gary Lee Miller) 首先提出了基于广义黎曼猜想的确定性算法,由于广义黎曼猜想并没有被证明,其后由以色列耶路撒冷希伯来大学的迈克尔·拉宾 (Michael O. Rabin) 教授作出修改,提出了不依赖于该假设的随机化算法。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>这是因为从 <span class=\"math inline\">\\(x^2\\equiv 1\\pmod n\\)</span> 可以推导出 <span class=\"math inline\">\\((x-1)(x+1)=x^{2}-1\\equiv 0\\pmod n\\)</span>,又由于<span class=\"math inline\">\\(n\\)</span>是素数,根据<a href=\"https://zh.wikipedia.org/zh-cn/欧几里得引理\">欧几里得引理</a>,它必然整除 <span class=\"math inline\">\\(x-1\\)</span> 和 <span class=\"math inline\">\\(x+1\\)</span>其中的一个,所以 <span class=\"math inline\">\\(x\\bmod n\\)</span> 一定是 <span class=\"math inline\">\\(1\\)</span> 或 <span class=\"math inline\">\\(-1\\)</span>。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["技术小札"],"tags":["密码学","Python编程"]},{"title":"自己动手使用树莓派搭建家用NAS和流媒体服务器","url":"/2021/12/19/RPi-NAS-Plex/","content":"<p>网络附接存储(NAS)通过计算机网络提供对异构网络用户的文件级数据访问。随着硬盘价格的持续下降,NAS设备已经走入大众家庭。领先中小企业及家用NAS市场的品牌产商如群晖科技(Synology),其产品价格从低端<span class=\"tex2jax_ignore\">$</span>300到高端<span class=\"tex2jax_ignore\">$700</span>不等。但如果你是树莓派玩家,只需要最低价一半左右的成本,就可以搭建很不错的家用NAS及流媒体播放服务。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>纸上得来终觉浅,绝知此事要躬行。</strong><br> <strong>— <em>陆游</em>《冬夜读书示子聿》</strong></p>\n</div>\n<p>这篇博文记录了构建Raspberry Pi NAS和家庭媒体服务器的整个过程,包括项目规划、系统实现和性能评估。这里还总结了一些重要的经验和相关选购信息,希望对任何想要尝试这个DIY项目的人有所帮助。</p>\n<h2 id=\"项目规划\">项目规划</h2>\n<p><strong><a href=\"https://www.raspberrypi.com/products/raspberry-pi-4-model-b/\">树莓派(Raspberry Pi)4B</a></strong>的处理器升级为1.8GHz的博通BCM2711(四核Cortex-A72),其板载内存容量最高达8GB,配备了两个更新的USB 3.0接口和全速千兆以太网,电源也采用了较新的USB-C接口。这些极大地提高了系统吞吐量和整体综合性能,我们可以用它打造全功能家用NAS。</p>\n<p><img src=\"RPi-4B.png\" style=\"width:65.0%;height:65.0%\" /></p>\n<p>在NAS系统软件方面,<strong><a href=\"https://www.openmediavault.org\">OpenMediaVault</a></strong>(OMV)是基于Debian Linux的完整NAS解决方案。它是知名的开源NAS服务器系统FreeNAS(基于FreeBSD)的Linux重写版。OMV的显著特点有</p>\n<ul>\n<li>开箱即用,安装和管理无需网络和存储系统专业知识</li>\n<li>适用于x86-64和ARM平台,具有完整的Web管理界面</li>\n<li>支持多种协议(如SFTP、SMB/CIFS或NFS)下的文件存储访问</li>\n<li>可以通过SSH进行管理,能控制用户和组的访问权</li>\n</ul>\n<p>OMV主要用于家庭环境或小型家庭办公室,但不限于这些场景。该系统基于模块化设计,安装基本系统后即可使用插件轻松扩展。很显然,OMV就是我们需要的NAS服务器系统软件。</p>\n<p>NAS系统配上媒体播放系统,可以在家庭网络环境下提供极佳的音/视频点播体验。<strong><a href=\"https://support.plex.tv/articles/categories/plex-media-server/\">Plex媒体服务器</a></strong>软件整合互联网媒体服务(YouTube、Vimeo和TED等)和本地多媒体资料库,为用户的各种设备提供流媒体播放服务。Plex管理本地资料库的特色是</p>\n<ul>\n<li>集中管理,单一库轻松共享</li>\n<li>Web界面媒体资料导航,流播放</li>\n<li>实时保存和恢复播放进程</li>\n<li>多用户支持及分级播放权限设置</li>\n</ul>\n<p>Plex媒体服务器软件本身是免费的,也支持众多操作系统,非常适合与家用NAS相集成。</p>\n<p>以上这些涵盖了我们的NAS项目所需的所有软件,但是这对一个完整的NAS系统是不够的。我们还需要一个优选的机壳,否则树莓派NAS只能裸机运行。虽然市场上树莓派4B可用的机壳很多,作为NAS系统我们需要能容纳至少1-2个内置式SSD/HDD的机壳套件,此外还必须拥有良好的散热设计。</p>\n<p>经过一些比对,确定选用Geekworm的<a href=\"https://wiki.geekworm.com/NASPi\"><strong>NASPi 树莓派4B NAS存储套件</strong></a>。NASPi是专为最新的树莓派4B设计的的NUC(Next Unit of Computing)风格的NAS存储解决方案。它由三个部件组成:</p>\n<ol type=\"1\">\n<li>X823存储屏蔽板,支持2.5英寸SATA 固态硬盘(SDD)或机械硬盘(HDD)</li>\n<li>X-C1适配板,将所有树莓派4B接口调整到机壳背面,并提供安全关机按钮</li>\n<li>基于温控PWM(Pulse-Width Modulation,脉冲宽度调制)风扇的散热系统</li>\n</ol>\n<p>所有这些部件最后组装到铝合金制成并经过表面阳极氧化处理的外壳内。</p>\n<p>就此我们的NAS项目规划如下:</p>\n<ul>\n<li>硬件系统:\n<ul>\n<li>树莓派4B 8GB RAM主硬件系统</li>\n<li>32GB microSD用于树莓派OS存储</li>\n<li>NASPi NAS存储套件</li>\n<li>15-20W USB-C 电源适配器</li>\n<li>500GB内置式SSD(USB 3.0)</li>\n<li>2TB外置式HDD(USB 3.0)</li>\n</ul></li>\n<li>软件系统:\n<ul>\n<li>轻量级树莓派OS(无桌面环境)</li>\n<li>OMV用于NAS文件服务器</li>\n<li>Plex媒体服务器提供流媒体服务</li>\n</ul></li>\n</ul>\n<p>要特别指出的是,NAS服务器一般都是无头系统(headless system),无需键盘、鼠标和显示器。这对软硬件系统的安装、配置和调试都提出一些挑战。实际实现中,如下一节的描述所示,我们应用SSH终端连接完成基本项目实现过程。</p>\n<h2 id=\"系统实现\">系统实现</h2>\n<p>此项目实现分为四个阶段,逐段详细描述如下。</p>\n<h3 id=\"预备树莓派4b\">预备树莓派4B</h3>\n<p>第一阶段我们需要预先准备树莓派操作系统,并做一些基本的单元测试。这个很重要,如果等到完整的NSAPi套件组装完毕再测试,那时发现树莓派的问题就很麻烦。</p>\n<h4 id=\"预备操作系统\">预备操作系统</h4>\n<p>首先将microSD卡插入到USB适配器并连接到macOS电脑,然后到树莓派网站下载<a href=\"https://www.raspberrypi.org/software/\">Raspberry Pi Imager</a>软件运行。从应用程序的界面,逐级点击<strong>CHOOSE OS > Raspberry Pi OS (other) > Raspberry Pi OS Lite (32-bit)</strong>,选择不需要桌面环境的轻量级树莓派操作系统,再点击<strong>CHOOSE STORAGE</strong>选择microSD卡。</p>\n<p>接下来是一个小窍门——敲入<code>ctrl-shift-x</code>组合键,就会弹出如下的高级选项对话框 <img src=\"RPi-Imager-advopt.png\" style=\"width:70.0%;height:70.0%\" /> 这里正好就有我们需要的启动时启用SSH选项<strong>Enable SSH</strong>,还能为默认的用户名<code>pi</code>预设密码(默认为raspberry)。设置完毕点击<strong>SAVE</strong>回到主页面,再点击<strong>WRITE</strong>就开始格式化microSD卡和写入系统。完成之后将microSD卡取出再插入到树莓派中,连接以太网加电启动。</p>\n<h4 id=\"探测ip地址\">探测IP地址</h4>\n<p>这时我们碰到一个问题:由于安装的系统没有桌面环境,没法连上键盘、鼠标和显示器,那么我们如何找到其IP地址呢?有两个办法:</p>\n<ol type=\"1\">\n<li>连接到家用路由器的管理网页,查看主机名raspberry对应的地址。</li>\n<li>应用nmap工具扫描对应网段,对比树莓派启动前后的变化。</li>\n</ol>\n<p>nmap工具的运行记录如下。注意到扫描出新的IP地址192.168.2.4,再对其单独运行nmap发现TCP端口22已打开。据此可以大致判定这就是我们新上线的树莓派:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ nmap -sn 192.168.2.0/24</span><br><span class=\"line\">Starting Nmap 7.92 ( https://nmap.org ) at 2021-11-28 21:07 PST</span><br><span class=\"line\">Nmap scan report <span class=\"keyword\">for</span> router.sx.com (192.168.2.1)</span><br><span class=\"line\">Host is up (0.0050s latency).</span><br><span class=\"line\">Nmap scan report <span class=\"keyword\">for</span> 192.168.2.3</span><br><span class=\"line\">Host is up (0.0048s latency).</span><br><span class=\"line\">Nmap scan report <span class=\"keyword\">for</span> 192.168.2.4 <span class=\"comment\">## New IP after Raspberry Pi boots up</span></span><br><span class=\"line\">Host is up (0.0057s latency).</span><br><span class=\"line\">Nmap <span class=\"keyword\">done</span>: 256 IP addresses (3 hosts up) scanned <span class=\"keyword\">in</span> 15.31 seconds</span><br><span class=\"line\"></span><br><span class=\"line\">❯ nmap 192.168.2.4</span><br><span class=\"line\">Nmap scan report <span class=\"keyword\">for</span> 192.168.2.4</span><br><span class=\"line\">Host is up (0.0066s latency).</span><br><span class=\"line\">Not shown: 999 closed tcp ports (conn-refused)</span><br><span class=\"line\">PORT STATE SERVICE</span><br><span class=\"line\">22/tcp open ssh</span><br></pre></td></tr></table></figure>\n<h4 id=\"系统更新升级\">系统更新升级</h4>\n<p>下一步尝试SSH连接</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ ssh [email protected]</span><br><span class=\"line\">[email protected]<span class=\"string\">'s password:</span></span><br><span class=\"line\"><span class=\"string\">Linux raspberrypi 5.10.63-v7l+ #1488 SMP Thu Nov 18 16:15:28 GMT 2021 armv7l</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\">The programs included with the Debian GNU/Linux system are free software;</span></span><br><span class=\"line\"><span class=\"string\">the exact distribution terms for each program are described in the</span></span><br><span class=\"line\"><span class=\"string\">individual files in /usr/share/doc/*/copyright.</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\">Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent</span></span><br><span class=\"line\"><span class=\"string\">permitted by applicable law.</span></span><br><span class=\"line\"><span class=\"string\">Last login: Fri Dec 24 19:46:15 2021 from 192.168.2.3</span></span><br><span class=\"line\"><span class=\"string\">pi@raspberrypi:~ $</span></span><br></pre></td></tr></table></figure>\n<p>确认后,就可以在树莓派里执行以下命令更新和升级系统:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ sudo apt update && sudo apt upgrade</span><br></pre></td></tr></table></figure>\n<h4 id=\"网络连接测试\">网络连接测试</h4>\n<p>本阶段的最后,是测试树莓派4B系统以太网连接的稳定性。在macOS电脑上用简单的ping命令,设定<code>-i 0.05</code>选项指定每秒20个数据包,以及<code>-t 3600</code>选项运行1个小时的测试</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ sudo ping -i 0.05 192.168.2.4 -t 3600</span><br></pre></td></tr></table></figure>\n<p>在没有无线连接的网段上,应该不会有超过1%数据包丢失或超时,否则应该检查排错。事实上,在实测中真的出现了将近10%的ping数据包的情况,SSH连接也时常掉线。上网搜索发现,类似的树莓派4B以太网连接丢包问题已有不少报告。相关论坛上人们给出的分析和建议集中在以下几点:</p>\n<ol type=\"1\">\n<li>供电不稳定造成丢包,需要更换为15W以上可靠的USB-C电源适配器。</li>\n<li>高能效以太网(Energy-Efficient Ethernet)机能失常,关闭即可解决。</li>\n<li>全速千兆以太网连接功能故障,必须降速到100Mbit/s使用。</li>\n</ol>\n<p>实际中试验了上面所有这些,收效不大。后来发现,连接树莓派4B的家用路由器,是2011年产的Belkin N750 DB。虽然它提供 Wi-Fi双频段802.11n和4个千兆以太网端口,可是生产年限过于久远,不由得让人怀疑其互操作性问题。而以上报告的第2、3点本质上也属于互操作性问题。想到这些,马上订购了普联(TP-Link)TL-SG105 5端口千兆以太网交换机。收到后,用TL-SG105扩展N750的千兆以太网端口,树莓派4B连接到TL-SG105上,再重新测试。果然这一次ping丢包率不到0.1%,SSH连接也很稳固。</p>\n<p>结论是,树莓派4B千兆以太网接口与一些旧式设备可能存在兼容性问题,可通过在二者之间插入互操作良好的交换机解决。</p>\n<h3 id=\"nsapi套件组装\">NSAPi套件组装</h3>\n<p>第二阶段组装NSAPi存储套件,目标是装配全部硬件,完成独立的NAS机体。</p>\n<h4 id=\"预备内置ssd\">预备内置SSD</h4>\n<p>NSAPi支持一个内置SSD或HDD。项目选择的是三星870 EVO 500GB 内置SSD,这里必须先确保SSD能单独正常工作,否则就得要拆机才能更换。可以将SSD挂接到Windows系统检查一下文件系统和基本读写操作。如果是新买的SSD,在Windows系统上可执行下列的步骤快速格式化:</p>\n<ol type=\"1\">\n<li>点击<strong>Start</strong>或<strong>Windows</strong>按钮,选择<strong>Control Panel > System and Security</strong></li>\n<li>选择<strong>Administrative Tools > Computer Management > Disk management</strong></li>\n<li>选中要格式化的硬盘, 点击右键选择<strong>Format</strong></li>\n<li>在出现的对话框中选择\n<ul>\n<li><strong>File System → NTFS</strong><br />\n</li>\n<li><strong>Allocation Unit Size → Default</strong></li>\n<li><strong>Volume Label →(指定卷名)</strong></li>\n<li><strong>Perform a quick format</strong></li>\n</ul></li>\n<li>点击OK开始快速格式化SSD</li>\n</ol>\n<p>⚠️注意:这里选择的文件系统是NTFS,这是OMV支持挂载(mount)文件系统之一。</p>\n<h4 id=\"pwm风扇控制\">PWM风扇控制</h4>\n<p>在实际的硬件组装之前,还要安装Geekworm提供的一个特殊软件——PWM风扇控制脚本。基于温度变化的PWM风扇转速控制是NASPi区别于其他硬件方案的一大特色,所以这一步很重要。</p>\n<p>参考Geekworm的<a href=\"https://wiki.geekworm.com/X-C1_Software\">X-C1软件维基页</a>,SSH连接到树莓派4B系统后的安装命令序列如下</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">sudo apt-get install -y git pigpio </span><br><span class=\"line\">sudo apt-get install -y python3-pigpio</span><br><span class=\"line\">sudo apt-get install -y python3-smbus</span><br><span class=\"line\">git <span class=\"built_in\">clone</span> https://github.com/geekworm-com/x-c1.git</span><br><span class=\"line\"><span class=\"built_in\">cd</span> x-c1</span><br><span class=\"line\">sudo chmod +x *.sh</span><br><span class=\"line\">sudo bash install.sh</span><br><span class=\"line\"><span class=\"built_in\">echo</span> <span class=\"string\">"alias xoff='sudo /usr/local/bin/x-c1-softsd.sh'"</span> >> ~/.bashrc</span><br></pre></td></tr></table></figure>\n<p>如果在树莓派4B上不能直接做<code>git clone</code>,可以先在SSH客户端下载X-C1软件,然后用scp传送到树莓派4B,再执行后续命令</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ scp -r x-c1 [email protected]:/home/pi/</span><br></pre></td></tr></table></figure>\n<details class=\"note primary\"><summary><p><strong>X-C1软件是如何控制PWM风扇的?</strong></p>\n</summary>\n<p>X-C1软件的核心是一个名为fan.py的Python程序,代码如下</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">#!/usr/bin/python</span></span><br><span class=\"line\"><span class=\"keyword\">import</span> pigpio</span><br><span class=\"line\"><span class=\"keyword\">import</span> time</span><br><span class=\"line\"></span><br><span class=\"line\">servo = <span class=\"number\">18</span></span><br><span class=\"line\"></span><br><span class=\"line\">pwm = pigpio.pi()</span><br><span class=\"line\">pwm.set_mode(servo, pigpio.OUTPUT)</span><br><span class=\"line\">pwm.set_PWM_frequency( servo, <span class=\"number\">25000</span> )</span><br><span class=\"line\">pwm.set_PWM_range(servo, <span class=\"number\">100</span>)</span><br><span class=\"line\"><span class=\"keyword\">while</span>(<span class=\"number\">1</span>):</span><br><span class=\"line\"> <span class=\"comment\">#get CPU temp</span></span><br><span class=\"line\"> file = <span class=\"built_in\">open</span>(<span class=\"string\">"/sys/class/thermal/thermal_zone0/temp"</span>)</span><br><span class=\"line\"> temp = <span class=\"built_in\">float</span>(file.read()) / <span class=\"number\">1000.00</span></span><br><span class=\"line\"> temp = <span class=\"built_in\">float</span>(<span class=\"string\">'%.2f'</span> % temp)</span><br><span class=\"line\"> file.close()</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp > <span class=\"number\">30</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">40</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp > <span class=\"number\">50</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">50</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp > <span class=\"number\">60</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">70</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp > <span class=\"number\">70</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">80</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp > <span class=\"number\">75</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">100</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span>(temp < <span class=\"number\">30</span>):</span><br><span class=\"line\"> pwm.set_PWM_dutycycle(servo, <span class=\"number\">0</span>)</span><br><span class=\"line\"> time.sleep(<span class=\"number\">1</span>)</span><br></pre></td></tr></table></figure>\n<p>它的逻辑其实很简单。通过导入pigpio模块,先初始化一个PWM控制对象,然后开始周期1秒的循环。每次循环时读取CPU温度,根据温度高低设置PWM的占空比,从而控制风扇转速。低于30℃时占空比为0,风扇停止;高于75℃时占空比为100,风扇全速转动。用户可以修改程序中的温度阈值和占空比参数,定制PWM风扇控制。</p>\n\n</details>\n<p>此外,下面的pi-temp.sh脚本能读出GPU和CPU的温度,也很有用</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ cat ./pi-temp.sh</span><br><span class=\"line\"><span class=\"comment\">#!/bin/bash</span></span><br><span class=\"line\"><span class=\"comment\"># Script: pi-temp.sh</span></span><br><span class=\"line\"><span class=\"comment\"># Purpose: Display the ARM CPU and GPU temperature of Raspberry Pi</span></span><br><span class=\"line\"><span class=\"comment\"># -------------------------------------------------------</span></span><br><span class=\"line\">cpu=$(</sys/class/thermal/thermal_zone0/temp)</span><br><span class=\"line\"><span class=\"built_in\">echo</span> <span class=\"string\">"<span class=\"subst\">$(date)</span> @ <span class=\"subst\">$(hostname)</span>"</span></span><br><span class=\"line\"><span class=\"built_in\">echo</span> <span class=\"string\">"-------------------------------------------"</span></span><br><span class=\"line\"><span class=\"built_in\">echo</span> <span class=\"string\">"GPU => <span class=\"subst\">$(vcgencmd measure_temp)</span>"</span></span><br><span class=\"line\"><span class=\"built_in\">echo</span> <span class=\"string\">"CPU => temp=<span class=\"subst\">$((cpu/1000)</span>)’C"</span></span><br><span class=\"line\"></span><br><span class=\"line\">pi@raspberrypi:~ $ ./pi-temp.sh</span><br><span class=\"line\">Mon 29 Nov 06:59:17 GMT 2021 @ raspberrypi</span><br><span class=\"line\">-------------------------------------------</span><br><span class=\"line\">GPU => temp=33.1<span class=\"string\">'C</span></span><br><span class=\"line\"><span class=\"string\">CPU => temp=32’C</span></span><br></pre></td></tr></table></figure>\n<h4 id=\"硬件组装过程\">硬件组装过程</h4>\n<p>下图是Geekworm NASPi开箱后零部件快照(第二排最右的树莓派4B和右下角的螺丝刀除外)</p>\n<p><img src=\"NASPi-unbox.jpg\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>第二排里的三个关键的部件,从左到右分别是</p>\n<ul>\n<li>X-C1 V1.3适配板提供电源管理、接口适配和安全关机功能</li>\n<li>X823 V1.5存储屏蔽板提供2.5英寸SSD/HDD存储功能(支持UASP)</li>\n<li>4010 PWM风扇和金属风扇支架</li>\n</ul>\n<p>组装过程主要参考Youtube上的<a href=\"https://youtu.be/ithz2Mg5Vrc\">NASPi安装教学视频</a>按部就班进行,大致步骤如下:</p>\n<ol type=\"1\">\n<li>插入SSD到X823的SATA III接口,翻转到另一面用螺丝固定</li>\n<li>在此面加装空间分隔块后安装树莓派4B,7针排线穿过二者之间</li>\n<li>在树莓派4B上面安装PWM风扇,以及额外的空间分隔块</li>\n<li>连接X-C1和树莓派4B,插入7针排线右端到X-C1 GPIO端口,插入3针排线到X-C1 FAN端口</li>\n<li>对齐插入7针排线左端的子板到树莓派4B的GPIO端口,用螺丝固定</li>\n<li>插入USB 3.0连接器,连通X823与树莓派4B的对应USB 3.0端口</li>\n</ol>\n<p>就此内部组件安装完成,得到如下视图</p>\n<p><img src=\"NASPi-internal.jpg\" style=\"width:60.0%;height:60.0%\" /></p>\n<p>此时加上USB-C电源启动系统,按动按键开关,应该会看到PWM风扇开始转动。转动速率时快时慢,并非恒定,这正是温度感应的PWM风扇正常工作的表现。</p>\n<p>前端内嵌蓝色LED的按键开关控制整个系统的开关机,可做如下测试</p>\n<ul>\n<li>加电后点按开关,系统启动</li>\n<li>运行中按住开关1-2秒,系统重启</li>\n<li>运行中按住开关3秒,安全关机</li>\n<li>运行中按住开关7-8秒,强制关机</li>\n</ul>\n<p>在SSH连接终端上,运行xoff命令也可以安全关机。但是不能直接用Linux的shutdown关机,因为这样无法解除X-C1的供电。</p>\n<p>开关机测试完成,拔出USB 3.0连接器,就可以将整个模块插入机壳。加上后背板拧紧螺丝后,再重新插入USB 3.0连接器,就此NASPi系统套件组装完毕,下面是Geekworm提供的完整系统的前后视图(标注了全部接口和通风孔)</p>\n<p><img src=\"NASPi-outside.jpg\" style=\"width:60.0%;height:60.0%\" /></p>\n<h3 id=\"omv安装和配置\">OMV安装和配置</h3>\n<p>第三阶段安装和配置NAS系统的核心软件——OMV,目标是实现基本的网络文件访问服务。在重新开机之前,先插入已格式化好NTSF文件系统的希捷2TB外置HDD到余下的另一个USB 3.0端口。启动后,从macOS连接SSH到NASPi,执行下面的流程。</p>\n<h4 id=\"安装omv软件包\">安装OMV软件包</h4>\n<p>安装OMV的方法很简单,直接在SSH连接的终端运行下面的命令行:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">wget -O - https://raw.githubusercontent.com/OpenMediaVault-Plugin-Developers/installScript/master/install | sudo bash</span><br></pre></td></tr></table></figure>\n<p>由于OMV整个软件包很大,这一安装过程会持续较长的时间。安装结束后,系统的IP地址可能会变化,此时需要重连SSH:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">(Reading database ... 51781 files and directories currently installed.)</span><br><span class=\"line\">Purging configuration files <span class=\"keyword\">for</span> dhcpcd5 (1:8.1.2-1+rpt3) ...</span><br><span class=\"line\">Purging configuration files <span class=\"keyword\">for</span> raspberrypi-net-mods (1.3.2) ...</span><br><span class=\"line\">Enable and start systemd-resolved ...</span><br><span class=\"line\">Unblocking wifi with rfkill ...</span><br><span class=\"line\">Adding eth0 to openmedivault database ...</span><br><span class=\"line\">IP address may change and you could lose connection <span class=\"keyword\">if</span> running this script via ssh.</span><br><span class=\"line\">client_loop: send disconnect: Broken pipe\t</span><br></pre></td></tr></table></figure>\n<p>重连后可用<code>dpkg</code>查看OMV的软件包。可以看到,这里安装的OMV是当前最新的6.0.5版本:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ dpkg -l | grep openme</span><br><span class=\"line\">ii openmediavault 6.0.5-1 all openmediavault - The open network attached storage solution</span><br><span class=\"line\">ii openmediavault-flashmemory 6.0.2 all folder2ram plugin <span class=\"keyword\">for</span> openmediavault</span><br><span class=\"line\">ii openmediavault-keyring 1.0 all GnuPG archive keys of the OpenMediaVault archive</span><br><span class=\"line\">ii openmediavault-omvextrasorg 6.0.4 all OMV-Extras.org Package Repositories <span class=\"keyword\">for</span> OpenMediaVault</span><br></pre></td></tr></table></figure>\n<h4 id=\"连接管理界面\">连接管理界面</h4>\n<p>这时OMV的工作台已经上线了,在macOS电脑打开浏览器输入IP地址后回车,就可以打开漂亮的登录界面(点击右上角🌍图标可以选择用户界面语言): <img src=\"OMV-login-default.png\" style=\"width:70.0%;height:70.0%\" /></p>\n<p>用上图显示的默认用户名和密码登录后,会看到工作台界面。此时第一件事应该是点击右上角的⚙️图标,弹出设置菜单后点击“更改密码”。你也可以在这里修改语言,如下为选择“简体中文”后的结果 <img src=\"OMV-start-cn.png\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>点击设置菜单中的“仪表盘”可以选择开启相关的组件。界面左侧边的菜单系列为管理员提供任务导航,不需要时可以隐藏。完整的OMV管理手册可参考<a href=\"https://openmediavault.readthedocs.io/en/latest/index.html\">在线文档</a></p>\n<h4 id=\"配置文件服务\">配置文件服务</h4>\n<p>接下来就是配置NAS的关键流程,包括下面5个步骤:</p>\n<ol type=\"1\">\n<li><p><strong>扫描系统挂接的硬盘</strong></p>\n<p>从侧栏菜单点击<strong>存储器 > 磁盘</strong>,进入硬盘管理页面。如果有刚刚插入的USB存储设备,可在这里点击🔍扫描出来。本系统的扫描结果如下,内置三星500GB SSD和外置希捷2TB HDD都被检测出来了,装载整个系统的32GB microSD也列在最上边: <img src=\"OMV-Storage-Disks-cn.png\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>在SSH终端上可看到对应的硬盘挂接信息:</p>\n<p><figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ df -h | grep disk</span><br><span class=\"line\">/dev/sdb2 466G 13G 454G 3% /srv/dev-disk-by-uuid-D0604B68604B547E</span><br><span class=\"line\">/dev/sda1 1.9T 131G 1.7T 7% /srv/dev-disk-by-uuid-DEB2474FB2472B7B</span><br></pre></td></tr></table></figure></p></li>\n<li><p><strong>挂载硬盘文件系统</strong></p>\n<p>从侧栏菜单点击<strong>存储器 > 文件系统</strong>,进入文件系统管理页面。如果存储设备还没有文件系统,可点击⨁新建(Create)或挂载(Mount)文件系统。OMV可以新建/挂载ext4、ext3、jfs、xfs文件系统,但对ntfs文件系统只支持挂载。下图显示OMV正确无误地挂载SSD和HDD的ntfs文件系统: <img src=\"OMV-Storage-FS-cn.png\" style=\"width:80.0%;height:80.0%\" /></p></li>\n<li><p><strong>设置共享文件夹</strong></p>\n<p>从侧栏菜单点击<strong>存储器 > 共享文件夹</strong>,进入共享文件夹管理页面。这里点击⨁创建共享文件夹。创建时要指定名称、对应的文件系统和相对路径,还可以添加注释。选择已创建的文件夹,再点击铅笔图标可以编辑相关信息。本系统为SSD和HDD分别设定共享文件夹相对路径Zixi-Primary和Zixi-Secondary <img src=\"OMV-Storage-SharedFolders-cn.png\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>注意到上图顶上的橙黄色警示,它提醒管理者设置已变更,必须点击✔️图标才能生效。</p></li>\n<li><p><strong>添加文件共享访问用户</strong></p>\n<p>从侧栏菜单点击<strong>用户管理 > 用户</strong>,进入用户管理页面。系统的默认用户pi具有root权限,基于安全考量,不可用于文件共享访问。所以需要另外添加一个新用户。在此页面点击⨁新建用户,新建时只有用户名和密码是必须的,其它可选填。建好后选中此用户,点击第三个带钥匙的文件夹图标(提示“共享文件夹权限”),进入如下的权限设置页面 <img src=\"OMV-User-authorize-cn.png\" style=\"width:80.0%;height:80.0%\" /> 如图所示,针对此新用户zixi,管理员可以设定每一个共享文件夹的读写访问权限。</p></li>\n<li><p><strong>启动文件共享服务</strong></p>\n<p>展开导航菜单的“服务”项,可以看到OMV管理五种服务:FTP、NFS、Rsync、SMB/CIFS和SSH。SSH是系统初始就启用的。NFS和SMB/CIFS是最通用的网络文件共享协议,macOS也都支持二者。这里以SMB/CIFS为例。从侧栏菜单点击<strong>服务 > SMB/CIFS</strong>,进入管理页面。页面包含两个按钮:设置和共享。先点击“设置”,在新页面启动SMB/CIFS服务并配置工作组名称,其它选项可先保持默认值。保存后回到SMB/CIFS管理页面。然后点击“共享”,在新页面点击⨁添加共享文件夹Zixi-Primary和Zixi-Secondary并保存。之后点击跳出的橙黄色警示条的✔️图标使所有配置更新生效,最终得到如下图的结果 <img src=\"OMV-SMB-Shares-cn.png\" style=\"width:80.0%;height:80.0%\" /></p></li>\n</ol>\n<p>到此我们的树莓派NAS系统的文件共享配置完毕,SMB/CIFS的服务已启动。选择开启相关的组件后,我们的仪表盘实时监控显示如下<img src=\"OMV-Dashboard-cn.png\" style=\"width:85.0%;height:85.0%\" /></p>\n<h4 id=\"客户端设置\">客户端设置</h4>\n<p>服务器端好了后,我们要在客户机端加载网络共享文件夹,方法如下:</p>\n<ul>\n<li>Windows PC 客户端\n<ul>\n<li>打开 File Explore,点击“This PC”</li>\n<li>在右边空白处点鼠标右键, 从弹出菜单中选\"Add a network location”</li>\n<li>在“Internet or network address\"输入框中,键入“\\\\<IP地址>\\<共享文件夹名>”</li>\n<li>输入用户名和密码</li>\n</ul></li>\n<li>MacBook 客户端(截屏示例如下)\n<ul>\n<li>打开 Finder,点击菜单项 Go</li>\n<li>点击“Connect to Server...”</li>\n<li>输入URL“smb://<IP地址>/<共享文件夹名>”,点击 Connect</li>\n<li>输入用户名和密码<br />\n<img src=\"macOS-SMB.png\" style=\"width:80.0%;height:80.0%\" /></li>\n</ul></li>\n</ul>\n<p>设置好客户机端后,用户就可以如同本地目录一样在网络共享文件夹上执行各种操作,如预览、新建、打开或复制文件等,也可创建新的子目录或删除现存子目录。</p>\n<h3 id=\"plex安装和配置\">Plex安装和配置</h3>\n<p>最后一个阶段安装和配置Plex媒体服务器,实现网络流媒体服务。</p>\n<h4 id=\"安装媒体服务器\">安装媒体服务器</h4>\n<p>安装Plex媒体服务器过程需要HTTPS传输支持,由此我们必须先安装https-transport软件包。SSH到我们树莓派NAS,执行安装命令</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">sudo apt-get install apt-transport-https</span><br></pre></td></tr></table></figure>\n<p>接下来给系统添加Plex仓库,这需要先下载Plex签名密钥。下面是相应的命令和运行记录</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ curl https://downloads.plex.tv/plex-keys/PlexSign.key | sudo apt-key add -</span><br><span class=\"line\"> % Total % Received % Xferd Average Speed Time Time Time Current</span><br><span class=\"line\"> Dload Upload Total Spent Left Speed</span><br><span class=\"line\"> 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0</span><br><span class=\"line\"> Warning: apt-key is deprecated. Manage keyring files <span class=\"keyword\">in</span> trusted.gpg.d instead (see apt-key(8)).</span><br><span class=\"line\">100 3072 100 3072 0 0 10039 0 --:--:-- --:--:-- --:--:-- 10039</span><br><span class=\"line\">OK</span><br></pre></td></tr></table></figure>\n<p>用同样的apt-key命令查看加入的Plex签名密钥</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ apt-key list</span><br><span class=\"line\">Warning: apt-key is deprecated. Manage keyring files <span class=\"keyword\">in</span> trusted.gpg.d instead (see apt-key(8)).</span><br><span class=\"line\">/etc/apt/trusted.gpg</span><br><span class=\"line\">...</span><br><span class=\"line\">pub rsa4096 2015-03-22 [SC]</span><br><span class=\"line\"> CD66 5CBA 0E2F 88B7 373F 7CB9 9720 3C7B 3ADC A79D</span><br><span class=\"line\">uid [ unknown] Plex Inc.</span><br><span class=\"line\">sub rsa4096 2015-03-22 [E]</span><br><span class=\"line\">...</span><br></pre></td></tr></table></figure>\n<p>可以看到Plex使用4096比特的RSA密钥。对于上面记录里的告警信息“apt-key is deprecated...”,可以暂时忽略,感兴趣的可以去阅读<a href=\"https://askubuntu.com/questions/1286545/what-commands-exactly-should-replace-the-deprecated-apt-key?newreg=20085e604ada43c2a3466bb51eb4349a\">askubuntu论坛上的讨论</a>。</p>\n<p>下一步添加Plex仓库到系统仓库列表 ,然后更新软件包 <figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">echo deb https://downloads.plex.tv/repo/deb public main | sudo tee /etc/apt/sources.list.d/plexmediaserver.list</span><br><span class=\"line\">sudo apt-get update</span><br></pre></td></tr></table></figure></p>\n<p>现在就可以开始实质的Plex媒体服务器安装,安装命令和记录如下</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ sudo apt install plexmediaserver</span><br><span class=\"line\">Reading package lists... Done</span><br><span class=\"line\">Building dependency tree... Done</span><br><span class=\"line\">Reading state information... Done</span><br><span class=\"line\">The following NEW packages will be installed:</span><br><span class=\"line\"> plexmediaserver</span><br><span class=\"line\">0 upgraded, 1 newly installed, 0 to remove and 20 not upgraded.</span><br><span class=\"line\">Need to get 66.1 MB of archives.</span><br><span class=\"line\">After this operation, 146 MB of additional disk space will be used.</span><br><span class=\"line\">Get:1 https://downloads.plex.tv/repo/deb public/main armhf plexmediaserver armhf 1.25.0.5282-2edd3c44d [66.1 MB]</span><br><span class=\"line\">Fetched 66.1 MB <span class=\"keyword\">in</span> 28s (2392 kB/s)</span><br><span class=\"line\">Selecting previously unselected package plexmediaserver.</span><br><span class=\"line\">(Reading database ... 51783 files and directories currently installed.)</span><br><span class=\"line\">Preparing to unpack .../plexmediaserver_1.25.0.5282-2edd3c44d_armhf.deb ...</span><br><span class=\"line\">PlexMediaServer install: Pre-installation Validation.</span><br><span class=\"line\">PlexMediaServer install: Pre-installation Validation complete.</span><br><span class=\"line\">Unpacking plexmediaserver (1.25.0.5282-2edd3c44d) ...</span><br><span class=\"line\">Setting up plexmediaserver (1.25.0.5282-2edd3c44d) ...</span><br><span class=\"line\"></span><br><span class=\"line\">Configuration file <span class=\"string\">'/etc/apt/sources.list.d/plexmediaserver.list'</span></span><br><span class=\"line\"> ==> File on system created by you or by a script.</span><br><span class=\"line\"> ==> File also <span class=\"keyword\">in</span> package provided by package maintainer.</span><br><span class=\"line\"> What would you like to <span class=\"keyword\">do</span> about it ? Your options are:</span><br><span class=\"line\"> Y or I : install the package maintainer<span class=\"string\">'s version</span></span><br><span class=\"line\"><span class=\"string\"> N or O : keep your currently-installed version</span></span><br><span class=\"line\"><span class=\"string\"> D : show the differences between the versions</span></span><br><span class=\"line\"><span class=\"string\"> Z : start a shell to examine the situation</span></span><br><span class=\"line\"><span class=\"string\"> The default action is to keep your current version.</span></span><br><span class=\"line\"><span class=\"string\">*** plexmediaserver.list (Y/I/N/O/D/Z) [default=N] ?</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: PlexMediaServer-1.25.0.5282-2edd3c44d - Installation starting.</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install:</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Now installing based on:</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Installation Type: New</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Process Control: systemd</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Plex User: plex</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Plex Group: plex</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Video Group: video</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Metadata Dir: /var/lib/plexmediaserver/Library/Application Support</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Temp Directory: /tmp</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Lang Encoding: en_US.UTF-8</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Nvidia GPU card: Not Found</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install:</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: Completing final configuration.</span></span><br><span class=\"line\"><span class=\"string\">Created symlink /etc/systemd/system/multi-user.target.wants/plexmediaserver.service → /lib/systemd/system/plexmediaserver.service.</span></span><br><span class=\"line\"><span class=\"string\">PlexMediaServer install: PlexMediaServer-1.25.0.5282-2edd3c44d - Installation successful. Errors: 0, Warnings: 0</span></span><br></pre></td></tr></table></figure>\n<p>中间会被问到Plex媒体服务器列表(plexmediaserver.list)的问题,选默认的N就可以了。看到“Installation successful”,我们知道安装成功。这时Plex流媒体服务也启动了,从macOS端再运行nmap扫描,发现TCP服务端口32400已开</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ nmap -p1-65535 192.168.2.4 | grep open</span><br><span class=\"line\">22/tcp open ssh</span><br><span class=\"line\">80/tcp open http</span><br><span class=\"line\">111/tcp open rpcbind</span><br><span class=\"line\">139/tcp open netbios-ssn</span><br><span class=\"line\">445/tcp open microsoft-ds</span><br><span class=\"line\">2049/tcp open nfs</span><br><span class=\"line\">5357/tcp open wsdapi</span><br><span class=\"line\">32400/tcp open plex</span><br></pre></td></tr></table></figure>\n<h4 id=\"配置媒体服务器\">配置媒体服务器</h4>\n<p>Plex媒体服务器的配置是在网页上完成的。在macOS电脑端启动浏览器,键入网址<strong>http://<IP-地址>:32400/web</strong>,不出意外的话会看到如下的页面 <img src=\"Plex-notice.png\" style=\"width:80.0%;height:80.0%\" /> 我们可以用Google、Facebook或Apple账户登录,也可以输入电邮创建新账户。逐步跟随页面的指示,不需要负任何费用,很快到达<strong>Server Setup</strong>页面。这里可配置服务器名称和添加资料库。一般情况下我们没必要从外网访问家庭媒体服务器, 所以记住在这一步取消勾选“Allow me to access my media outside my home”。添加资料库时,先选定资料库类型(电影、电视剧集、音乐和照片等),然后点击“BROWSE FOR MEDIA FOLDER”按钮浏览和选定对应的文件夹。资料库添加完毕,相应的媒体文件立即就会出现在本地的服务目录中,截图如下 <img src=\"Plex-movie.png\" style=\"width:90.0%;height:90.0%\" /> 这里我们的树莓派NAS的本地服务器名为ZIXI-RPI-NAS,资料库里的电影目录中显示《黑客帝国》三部曲,并且正在播放第一部<em>The Matrix</em>。将鼠标移到服务器名称上,名称右边会出现➕图标,点击可以继续添加新的媒体库。</p>\n<p>Plex媒体服务器配置好了后,我们可以从家庭网络上的任意设备打开浏览器做流媒体点播,不需要下载额外的应用程序。整个体验就如同我们自己的专有家庭奈飞(Netflix)服务一样!</p>\n<h2 id=\"性能评估\">性能评估</h2>\n<p>连接macOS笔记本电脑到TL-SG105余下的某个端口,就能执行一些简单的同网段内测试,以全面评估此NAS系统的性能。</p>\n<h3 id=\"系统压力测试\">系统压力测试</h3>\n<p>参考Geekworm的<a href=\"https://wiki.geekworm.com/Naspi_stress_test\">NASPi压力测试维基页</a>,在SSH连接的终端执行下列的命令,从GitHub克隆测试程序运行压力测试:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">git <span class=\"built_in\">clone</span> https://github.com/geekworm-com/rpi-cpu-stress</span><br><span class=\"line\"><span class=\"built_in\">cd</span> rpi-cpu-stress</span><br><span class=\"line\">chmod +x stress.sh</span><br><span class=\"line\">sudo ./stress.sh</span><br></pre></td></tr></table></figure>\n<p>同时建立第二个SSH连接,运行htop监控系统负荷状态。下图是接近5分钟时的截图(左边是htop实时显示,右边为压力测试输出) <img src=\"RPi-stress-test.png\" style=\"width:80.0%;height:80.0%\" /> 右边输出的<code>temp</code>值除以1000即是CPU的温度。可以看到,测试过程中所有4个CPU核都达到100%满负荷,而最高温度不操过70℃。这时抚摸机身没有明显热感。键入<code>ctrl-c</code>中止压力测试,再单独执行测温脚本:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">pi@raspberrypi:~ $ ./pi-temp.sh</span><br><span class=\"line\">Fri Dec 24 15:59:21 PST 2021 @ raspberrypi</span><br><span class=\"line\">-------------------------------------------</span><br><span class=\"line\">GPU => temp=39.9<span class=\"string\">'C</span></span><br><span class=\"line\"><span class=\"string\">CPU => temp=40'</span>C</span><br></pre></td></tr></table></figure>\n<p>系统温度回到较低的数值范围。测试结果确认本系统符合设计要求。</p>\n<h3 id=\"文件传输测速\">文件传输测速</h3>\n<p>用远程文件复制工具scp可以粗略测量文件传输速度。首先在macOS客户端上运行<code>mkfile</code>命令创建1GB大小的文件,然后用<code>scp</code>命令将其复制到远端 NAS 系统的用户目录:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ mkfile 1G test-nas.dmg</span><br><span class=\"line\">❯ ls -al test-nas.dmg</span><br><span class=\"line\">rw------- 1 sxiao staff 1073741824 Dec 19 20:53 test-nas.dmg</span><br><span class=\"line\">❯ scp test-nas.dmg [email protected]:/home/pi/</span><br><span class=\"line\">[email protected]<span class=\"string\">'s password:</span></span><br><span class=\"line\"><span class=\"string\">test-nas.dmg 100% 1024MB 19.2MB/s 00:53</span></span><br></pre></td></tr></table></figure>\n<p>复制成功后会输出计时和由此算出的速度。反向运用就可以得到从 NAS 系统接收文件的速度</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ scp [email protected]:/home/pi/test-nas.dmg test-nas-rx.dmg</span><br><span class=\"line\">[email protected]<span class=\"string\">'s password:</span></span><br><span class=\"line\"><span class=\"string\">test-nas.dmg 100% 1024MB 65.7MB/s 00:15</span></span><br></pre></td></tr></table></figure>\n<p>来回重复三次得到的结果如下</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">传输类型</th>\n<th style=\"text-align: center;\">服务器操作</th>\n<th style=\"text-align: center;\">计时(秒)</th>\n<th style=\"text-align: center;\">速度(MB/s)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">发送</td>\n<td style=\"text-align: center;\">写</td>\n<td style=\"text-align: center;\">53</td>\n<td style=\"text-align: center;\">19.2</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">发送</td>\n<td style=\"text-align: center;\">写</td>\n<td style=\"text-align: center;\">45</td>\n<td style=\"text-align: center;\">22.5</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">发送</td>\n<td style=\"text-align: center;\">写</td>\n<td style=\"text-align: center;\">50</td>\n<td style=\"text-align: center;\">20.4</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">接收</td>\n<td style=\"text-align: center;\">读</td>\n<td style=\"text-align: center;\">15</td>\n<td style=\"text-align: center;\">65.7</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">接收</td>\n<td style=\"text-align: center;\">读</td>\n<td style=\"text-align: center;\">16</td>\n<td style=\"text-align: center;\">60.3</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">接收</td>\n<td style=\"text-align: center;\">读</td>\n<td style=\"text-align: center;\">15</td>\n<td style=\"text-align: center;\">66.3</td>\n</tr>\n</tbody>\n</table>\n<p>统计系统远程写入文件的速度在 20MB/s 左右,而远程读出文件的速度可达到 60MB/s 以上。考虑到scp相关的加解密处理在通用树莓派系统上都是软件实现的,这一结果可算是差强人意。</p>\n<h3 id=\"硬盘读写测速\">硬盘读写测速</h3>\n<p>真正能检验NAS本身功能表现的是网络硬盘读写测速。对此,可在苹果的应用商店下载的AmorphousDiskMark应用。这是一款简便高效的硬盘测速软件,以MB/s和IOPS(每秒输入/输出操作)度量存储设备的读/写性能。它具有四种类型的测试:</p>\n<ol type=\"1\">\n<li>顺序读写1MB块,队列深度8</li>\n<li>顺序读写1MB块,队列深度1</li>\n<li>随机读写4KB块,队列深度64</li>\n<li>随机读写4KB块,队列深度1</li>\n</ol>\n<p>以上的队列深度是默认值,也有其他值可选。此外,用户还能修改测试文件大小和持续时间。</p>\n<p>在macOS客户端运行该应用程序,并在顶端分别选择远程SMB文件夹Zixi-Primary (三星 SSD)和Zixi-Secondary(希捷 HDD)文件夹,再点击左上角的<code>All</code>按钮启动NAS硬盘测速过程。二者的测试结果并列比较如下图所示</p>\n<div class=\"group-picture\"><div class=\"group-picture-row\"><div class=\"group-picture-column\"><img src=\"ADM-SSD.png\" /></div><div class=\"group-picture-column\"><img src=\"ADM-HDD.png\" /></div></div></div>\n<p>由此可以观察到以下几点:</p>\n<ul>\n<li>NAS硬盘读的速度比写要快,随机访问下的差别非常大。</li>\n<li>无论顺序访问还是随机访问,SSD的性能都胜过HDD。</li>\n<li>大的队列深度可以加快读,对随机访问尤甚,但对写的影响不大。</li>\n<li>对于SSD和HDD,都是顺序读写的效率要大大好于随机读写。</li>\n<li>对于SSD和HDD,都是大队列深度下顺序读写达到最高速度。</li>\n</ul>\n<p>这些其实并不意外,与macOS笔记本电脑直接外挂SSD和HDD的测试结果相一致,只是数值较低。在此NAS系统下,SSD和HDD都是通过USB 3.0接口连接的。USB 3.0支持高达5Gbit/s的传输速度,所以系统的性能瓶颈在于网络接口带宽和处理器能力。</p>\n<p>另一方面,SSD和HDD都在1MB顺序读及队列深度8的情况下达到超过900Mbit/s传送速度,接近千兆以太网接口带宽上限。这一存储速度可支持单路帧率为60fps的1080p60视频流或者并行2路帧率为25fps的1080i50视频流,对于家庭流媒体服务来说已经足够了。实测同时三台电脑点播高清视频和一部手机播放MP3音乐,并无任何卡顿的现象,此NAS系统的表现令人满意。</p>\n<h2 id=\"项目总结\">项目总结</h2>\n<p>至此我们的树莓派家用NAS项目顺利完成,现在可以将之移到较永久的位置,方便为全家提供网络文件和流媒体服务:</p>\n<p><img src=\"NASPi-final.jpg\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>从经济上来看,我们的家用NAS的成本统计如下表(不计SSD/HDD)</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">设备</th>\n<th style=\"text-align: center;\">作用</th>\n<th style=\"text-align: center;\">成本($)</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">树莓派4B 2/4/8GB 内存</td>\n<td style=\"text-align: center;\">主硬件系统</td>\n<td style=\"text-align: center;\">45/55/75</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">三星 32GB EVO+ Class-10 Micro SDHC</td>\n<td style=\"text-align: center;\">操作系统存储</td>\n<td style=\"text-align: center;\">10</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">Geekworm NASPi 树莓派4B NAS 存储套件</td>\n<td style=\"text-align: center;\">机壳、扩展版和PWM风扇</td>\n<td style=\"text-align: center;\">60</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">Geekworm 20W 5V 4A USB-C 电源适配器</td>\n<td style=\"text-align: center;\">供电电源</td>\n<td style=\"text-align: center;\">15</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">普联 TL-SG105 5端口千兆以太网交换机</td>\n<td style=\"text-align: center;\">桌面交换机</td>\n<td style=\"text-align: center;\">15</td>\n</tr>\n</tbody>\n</table>\n<p>即使采用最高内存配置的树莓派4B,整个开销也才$175,比市面上所售低端品牌NAS价格的一半多一点。实际上,除非有需要流媒体服务的客户端设备很多,一般内存消耗都在2GB一下,所以2GB的树莓派4B应该可以满足绝大多数的家用场景。这样算下来只要$145,低于市售价的一半。</p>\n<p>另一方面,此DIY项目非常好地锻炼了动手实践能力,帮助我们积累了构建网络连接、配置系统软硬件及调整和测试应用层服务的宝贵直观经验。总结下来,树莓派4B集成OMV搭建的家用NAS与Plex媒体服务器相结合,为家庭网络提供了成本效益好的文件备份和流媒体服务解决方案。</p>\n<p>附录:相关设备清单及亚马逊链接</p>\n<blockquote>\n<p><strong><em>Disclosure</em></strong>: <em>This blog site is reader-supported. When you buy through the affiliate links below, as an Amazon Associate, I earn a tiny commission from qualifying purchases. Thank you.</em></p>\n<p><strong>CanaKit 树莓派 4B 8GB RAM + 128GB MicroSD 极限套件</strong> <a href=\"https://amzn.to/3DUeDfm\">https://amzn.to/3DUeDfm</a><br />\n<strong>三星(Samsung)32GB EVO+ Class 10 Micro SDHC with Adapter</strong> <a href=\"https://amzn.to/3FLkTb7\">https://amzn.to/3FLkTb7</a><br />\n<strong>Geekworm NASPi 2.5英寸 SATA HDD/SSD 树莓派 4B NAS 存储套件</strong> <a href=\"https://amzn.to/3m5djAi\">https://amzn.to/3m5djAi</a><br />\n<strong>Geekworm 树莓派 4 20W 5V 4A USB-C 电源适配器</strong> <a href=\"https://amzn.to/3m1EXOf\">https://amzn.to/3m1EXOf</a><br />\n<strong>普联(TP-Link)TL-SG105 5端口千兆以太网交换机</strong> <a href=\"https://amzn.to/3pRkBsi\">https://amzn.to/3pRkBsi</a><br />\n<strong>三星(Samsung)870 EVO 500GB 2.5英寸 SATA III 内置式 SSD</strong> <a href=\"https://amzn.to/3DPKnCl\">https://amzn.to/3DPKnCl</a><br />\n<strong>希捷(Seagate)便携式 2TB USB 3.0 外置式 HDD</strong> <a href=\"https://amzn.to/3EYegl4\">https://amzn.to/3EYegl4</a><br />\n<strong>群晖科技(Synology)2-Bay 2GB NAS DiskStation DS220+</strong> <a href=\"https://amzn.to/3Jp5qjd\">https://amzn.to/3Jp5qjd</a><br />\n<strong>群晖科技(Synology)5-Bay 8GB NAS DiskStation DS1520+</strong> <a href=\"https://amzn.to/3qniQDm\">https://amzn.to/3qniQDm</a></p>\n</blockquote>\n","categories":["动手实践"],"tags":["树莓派","NAS"]},{"title":"RSA的攻与防(二)","url":"/2023/11/13/RSA-attack-defense-2/","content":"<p>这是RSA攻防战的第二篇。本文首先补充说明特定情况下的两种大数分解方法,介绍它们的算法精要和适用场景,并给出Python参考实现。接下来深入解析典型的低私钥指数攻击算法——维纳攻击,详细讲解其数学基础、攻击原理及流程,也提供了完整的Python程序。文中还引用了近年最新研究的维纳攻击成立时私钥指数新上限,还使用测试用例验证了这一上限的正确性。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>The enemy knows the system being used.</strong><br> <strong>— <em>Claude Shannon</em>(克劳德·香农,美国数学家、电子工程师和密码学家,信息论的创始人。)</strong></p>\n</div>\n<p>上篇:<a href=\"https://www.packetmania.net/2020/12/01/RSA-attack-defense/\">RSA的攻与防(一)</a></p>\n<h2 id=\"大数分解补充\">大数分解(补充)</h2>\n<p>即使RSA加密算法所使用的模数<span class=\"math inline\">\\(N\\)</span>是很大的数值(有足够多的比特位),如果其素因数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>差距太小或太大,也会产生问题。这时有特定的因数分解算法,可以有效地从公钥模数<span class=\"math inline\">\\(N\\)</span>中分离出<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>。</p>\n<h3 id=\"费马因数分解法\">费马因数分解法</h3>\n<p>当素因数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>相距太近时,应用费马因数分解法能在一个很短的时间内分解模数<span class=\"math inline\">\\(N\\)</span>。费马因数分解法(Fermat's factorization method)以法国数学家<a href=\"https://www.packetmania.net/2021/02/14/Fermats-Little-Theorem/\">皮埃尔·德·费马</a>(Pierre de Fermat)的名字命名。其出发点是每一个正奇整数都可以表示为两个完全平方数之差,即 <span class=\"math display\">\\[N=a^2-b^2\\]</span> 右边适用代数因式分解后得到 <span class=\"math inline\">\\((a+b)(a-b)\\)</span>。如果这两个因子都不为1,则它们是<span class=\"math inline\">\\(N\\)</span>的非平凡因子。对于RSA的模数<span class=\"math inline\">\\(N\\)</span>,假定 <span class=\"math inline\">\\(p>q\\)</span>,对应就有 <span class=\"math inline\">\\(p=a+b\\)</span>和 <span class=\"math inline\">\\(q=a-b\\)</span>。反过来,可以导出 <span class=\"math display\">\\[N=\\left({\\frac {p+q}{2}}\\right)^{2}-\\left({\\frac {p-q}{2}}\\right)^{2}\\]</span> 费马因数分解法的思想就是<strong>从<span class=\"math inline\">\\(\\lceil{\\sqrt N}\\rceil\\)</span>开始,逐次加一尝试 <span class=\"math inline\">\\(a\\)</span>的各种值,验证是否有 <span class=\"math inline\">\\(a^{2}-N=b^{2}\\)</span></strong>。如果为真,就找到了两个非平凡因子,也就是 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>。这种方法所需的步数大约为 <span class=\"math display\">\\[{\\frac{p+q}{2}}-{\\sqrt N}=\\frac{({\\sqrt p}-{\\sqrt q})^{2}}{2}=\\frac{({\\sqrt N}-q)^{2}}{2q}\\]</span></p>\n<p>一般情况下,费马因数分解法比试除法好不到哪去,最坏情况下可能更慢。然而,<strong>当 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 的差值不大时,<span class=\"math inline\">\\(q\\)</span> 很接近<span class=\"math inline\">\\(\\sqrt N\\)</span>,计算的步数就非常少了</strong>。极端的例子是,如果 <span class=\"math inline\">\\(q\\)</span> 与<span class=\"math inline\">\\(\\sqrt N\\)</span>的差异小于<span class=\"math inline\">\\({\\left(4N\\right)}^{\\frac 1 4}\\)</span>,此方法只需要一步就结束了。</p>\n<p>以下是费马因数分解法的Python实现,以及应用于分解RSA模数<span class=\"math inline\">\\(N\\)</span>的示例</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">import</span> gmpy2</span><br><span class=\"line\"><span class=\"keyword\">import</span> time</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">FermatFactor</span>(<span class=\"params\">n</span>):</span></span><br><span class=\"line\"> <span class=\"keyword\">assert</span> n % <span class=\"number\">2</span> != <span class=\"number\">0</span></span><br><span class=\"line\"></span><br><span class=\"line\"> a = gmpy2.isqrt(n) + <span class=\"number\">1</span></span><br><span class=\"line\"> b2 = gmpy2.square(a) - n</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> <span class=\"keyword\">not</span> gmpy2.is_square(b2):</span><br><span class=\"line\"> a += <span class=\"number\">1</span></span><br><span class=\"line\"> b2 = gmpy2.square(a) - n</span><br><span class=\"line\"></span><br><span class=\"line\"> b = gmpy2.isqrt(b2)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> a + b, a - b</span><br><span class=\"line\"></span><br><span class=\"line\">p = <span class=\"number\">7422236843002619998657542152935407597465626963556444983366482781089760760914403641211700959458736191688739694068306773186013683526913015038631710959988771</span></span><br><span class=\"line\">q = <span class=\"number\">7422236843002619998657542152935407597465626963556444983366482781089760759017266051147512413638949173306397011800331344424158682304439958652982994939276427</span></span><br><span class=\"line\">N = p * q</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"N ="</span>, N)</span><br><span class=\"line\"></span><br><span class=\"line\">start = time.process_time()</span><br><span class=\"line\">(p1, q1) = FermatFactor(N)</span><br><span class=\"line\">end = time.process_time()</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'Elapsed time <span class=\"subst\">{end - start:<span class=\"number\">.3</span>f}</span>s.'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">assert</span>(p == p1)</span><br><span class=\"line\"><span class=\"keyword\">assert</span>(q == q1)</span><br></pre></td></tr></table></figure>\n<p>程序开始定义的<code>FermatFactor()</code>函数实现费马因数分解法。它调用了gmpy2的三个库函数:<code>isqrt()</code>求整数平方根,<code>square()</code>实现平方运算,<code>is_square()</code>验证是否为平方数。后面给出两个154个十进制数位的大素数 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,二者相乘得到<span class=\"math inline\">\\(N\\)</span>。然后将<span class=\"math inline\">\\(N\\)</span>输入到<code>FermatFactor()</code>函数,同时计时。函数返回后打印耗时并确认分解出数值。</p>\n<p>此程序在 MacBook Pro(2019)上的运行结果是</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">N = 55089599753625499150129246679078411260946554356961748980861372828434789664694269460953507615455541204658984798121874916511031276020889949113155608279765385693784204971246654484161179832345357692487854383961212865469152326807704510472371156179457167612793412416133943976901478047318514990960333355366785001217</span><br><span class=\"line\">Elapsed time 27.830s.</span><br></pre></td></tr></table></figure>\n<p>可以看到,不到半分钟的时间,这个308个十进制数位(约为1024比特)的大数就被成功分解出来了!再回头审查 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,可以发现这两个154个十进制数位的大素数前面71位完全一样。这正是费马因数分解法发挥力量的场景。如果简单修改<code>FermatFactor()</code>函数,保存起始的<span class=\"math inline\">\\(a\\)</span>值与循环结束后的值比较,得到循环次数60613989。如此之小的数值,难怪这么快分解就完成了。</p>\n<p>因此,大素数 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 的选择不仅要随机,还必须相距足够远。在得到两个大素数后,必须检查它们之间的差值。如果太小就要重新生成,以防止被攻击者使用费马因数分解法爆破。</p>\n<h3 id=\"波拉德-rho-算法\">波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法</h3>\n<p>物极必反。虽然大素因数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>要相距足够远,但如果差距太大也是危险的,这时可能会被波拉德 <span class=\"math inline\">\\(\\rho\\)</span>(rho)算法破解。此算法由英国数学家约翰·波拉德<a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a>于1975年发明。它仅需要少量的存储空间,其预期运行时间与被分解合数的最小素因数的平方根成正比。</p>\n<p>波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法核心思想是利用遍历序列的碰撞规律来搜索因子,其随机性和递归性使得它可以在相对较低的复杂度内有效分解整数。首先,对于<span class=\"math inline\">\\(N=pq\\)</span>,假定 <span class=\"math inline\">\\(p\\)</span> 是较小那个非平凡因子。算法定义了一个基于模<span class=\"math inline\">\\(N\\)</span>的多项式<span class=\"math display\">\\[f(x)=(x^{2}+c){\\pmod N}\\]</span> 用此多项式做递归调用可以生成一个伪随机序列,序列的生成公式是 <span class=\"math inline\">\\(x_{n+1}=f(x_n)\\)</span>。比如给定一个的初始值 <span class=\"math inline\">\\(x_0=2\\)</span> 和常数 <span class=\"math inline\">\\(c=1\\)</span>,就有 <span class=\"math display\">\\[\\begin{align}\nx_1&=f(2)=5\\\\\nx_2&=f(x_1)=f(f(2))=26\\\\\nx_3&=f(x_2)=f(f(f(2)))=677\\\\\n\\end{align}\\]</span> 对于这样产生的序列中的两个数 <span class=\"math inline\">\\(x_i\\)</span> 和 <span class=\"math inline\">\\(x_j\\)</span>,如果 <span class=\"math inline\">\\(x_i\\neq x_j\\)</span> 并且 <span class=\"math inline\">\\(x_i\\equiv x_j{\\pmod p}\\)</span>,则 <span class=\"math inline\">\\(|x_i-x_j|\\)</span> 一定是 <span class=\"math inline\">\\(p\\)</span> 的倍数。这时做最大公约数运算 <span class=\"math inline\">\\(\\gcd(|x_i-x_j|,N)\\)</span>,结果就是 <span class=\"math inline\">\\(p\\)</span>。依据<a href=\"https://zh.wikipedia.org/zh-cn/%E7%94%9F%E6%97%A5%E5%95%8F%E9%A1%8C\">生日悖论</a>,在最坏情形下,期望在生成大约<span class=\"math inline\">\\(\\sqrt p\\)</span>个数后,就会出现两个在模 <span class=\"math inline\">\\(p\\)</span>下相同的数,从而成功分解<span class=\"math inline\">\\(N\\)</span>。但是,由于要执行两两比较,此时的时间复杂度还是不令人满意。另外,当<span class=\"math inline\">\\(N\\)</span>很大时,保存这么多数也是一个麻烦。</p>\n<p>如何解决这些问题?这正是波拉德 <span class=\"math inline\">\\(\\rho\\)</span>算法的精妙所在。波拉德发现,这个伪随机数生成器生成的序列有两个特性:</p>\n<ol type=\"1\">\n<li><p>因为每个数仅取决于其前面的值,同时模运算下生成的数又是有限的,所以迟早会进入循环。如下所示,最终生成的序列会构成与希腊字母 <span class=\"math inline\">\\(\\rho\\)</span>形状相似的有向图,算法名称也由此而来。<img src=\"Pollard_rho_cycle.png\" style=\"width:50.0%;height:50.0%\" alt=\"形如希腊字母 ρ 的环路图\" /></p></li>\n<li><p>当 <span class=\"math inline\">\\(|x_i-x_j| \\equiv 0 \\pmod p\\)</span> 时,一定有 <span class=\"math display\">\\[|f(x_i)-f(x_j)|=|{x_i}^2-{x_j}^2|=|x_i+x_j|\\cdot|x_i-x_j|\\equiv 0 \\pmod p\\]</span> 这说明序列中如果两个数满足模运算条件,所有等距的数对都满足同样的条件。</p></li>\n</ol>\n<p>洞悉这两个特性的波拉德利用<a href=\"https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare\">Floyd环路判定算法</a>(也称龟兔赛跑算法),设置快慢两个节点 <span class=\"math inline\">\\(x_h\\)</span> 和 <span class=\"math inline\">\\(x_t\\)</span>。从相同的初始值 <span class=\"math inline\">\\(x_0\\)</span> 出发,慢节点 <span class=\"math inline\">\\(x_t\\)</span> 每步移动到序列下一个节点,而快节点 <span class=\"math inline\">\\(x_h\\)</span> 一次向前移动两个节点,即 <span class=\"math display\">\\[\\begin{align}\nx_t&=f(x_t)\\\\\nx_h&=f(f(x_h))\\\\\n\\end{align}\\]</span> 之后,计算 <span class=\"math inline\">\\(\\gcd(|x_h-x_t|,N)\\)</span>,大于1且小于<span class=\"math inline\">\\(N\\)</span>的结果就是 <span class=\"math inline\">\\(p\\)</span>,否则继续同样的步骤。在这样的设计下,因为每次移动都相当于检查一个新的节点间距,这样就不必要进行两两比较了。如果没有找到,最终快慢节点会在环上相遇,这时求最大公约数的结果为<span class=\"math inline\">\\(N\\)</span>。此时算法的建议是退出,然后换一个初始值或常数 <span class=\"math inline\">\\(c\\)</span> 重新生成伪随机数序列再试。</p>\n<p>这就是经典的波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法。它的时间复杂度是<span class=\"math inline\">\\(𝑂(\\sqrt p\\log N)\\)</span>(<span class=\"math inline\">\\(\\log\\)</span>来源于所需的<span class=\"math inline\">\\(\\gcd\\)</span>运算)。对于RSA模数<span class=\"math inline\">\\(N\\)</span>,显然 <span class=\"math inline\">\\(p\\leq \\sqrt N\\)</span>,于是时间复杂度上限可写为<span class=\"math inline\">\\(𝑂(N^{\\frac 1 4}\\log N)\\)</span>。波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法的时间复杂度表达式说明:被分解合数的最小素因数越小,预期的分解就越快,太小的 <span class=\"math inline\">\\(p\\)</span> 是极不安全的。</p>\n<p>波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法的编程不难,下面的Python程序给出了算法的函数实现<code>PollardRhoFactor()</code>和一些测试用例</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">import</span> gmpy2</span><br><span class=\"line\"><span class=\"keyword\">import</span> time</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">PollardRhoFactor</span>(<span class=\"params\">n, seed, c</span>):</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> n % <span class=\"number\">2</span> == <span class=\"number\">0</span>: <span class=\"keyword\">return</span> <span class=\"number\">2</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> gmpy2.is_prime(n): <span class=\"keyword\">return</span> n</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> <span class=\"literal\">True</span>:</span><br><span class=\"line\"> f = <span class=\"keyword\">lambda</span> x: (x**<span class=\"number\">2</span> + c) % n</span><br><span class=\"line\"> t = h = seed</span><br><span class=\"line\"> d = <span class=\"number\">1</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> d == <span class=\"number\">1</span>:</span><br><span class=\"line\"> t = f(t) <span class=\"comment\"># Tortoise</span></span><br><span class=\"line\"> h = f(f(h)) <span class=\"comment\"># Hare</span></span><br><span class=\"line\"> d = gmpy2.gcd(h - t, n)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> d != n:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> d <span class=\"comment\"># find a non-trivial factor</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># start a new round with updated seed and c</span></span><br><span class=\"line\"> seed = h</span><br><span class=\"line\"> c += <span class=\"number\">1</span></span><br><span class=\"line\"></span><br><span class=\"line\">N = [<span class=\"number\">10967535067</span>, <span class=\"number\">18446744073709551617</span>, <span class=\"number\">97546105601219326301</span>,</span><br><span class=\"line\"> <span class=\"number\">780002082420246798979794021150335143</span>]</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f"<span class=\"subst\">{<span class=\"string\">'N'</span>:<<span class=\"number\">37</span>}</span><span class=\"subst\">{<span class=\"string\">'P'</span>:<<span class=\"number\">16</span>}</span><span class=\"subst\">{<span class=\"string\">'Elapsed Time (s)'</span>:}</span>"</span>)</span><br><span class=\"line\"><span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">0</span>, <span class=\"built_in\">len</span>(N)):</span><br><span class=\"line\"> start = time.process_time()</span><br><span class=\"line\"> p = PollardRhoFactor(N[i], <span class=\"number\">2</span>, <span class=\"number\">1</span>)</span><br><span class=\"line\"> end = time.process_time()</span><br><span class=\"line\"> <span class=\"built_in\">print</span>(<span class=\"string\">f'<span class=\"subst\">{N[i]:<<span class=\"number\">37</span>}</span><span class=\"subst\">{p:<<span class=\"number\">16</span>}</span><span class=\"subst\">{end - start:<span class=\"number\">16.3</span>f}</span>'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">F8 = <span class=\"number\">2</span>**(<span class=\"number\">2</span>**<span class=\"number\">8</span>) + <span class=\"number\">1</span> <span class=\"comment\"># A 78-digit Fermat number</span></span><br><span class=\"line\">start = time.process_time()</span><br><span class=\"line\">p = PollardRhoFactor(F8, <span class=\"number\">2</span>, <span class=\"number\">1</span>)</span><br><span class=\"line\">end = time.process_time()</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'\\nF8 = <span class=\"subst\">{F8}</span>\\np = <span class=\"subst\">{p}</span>\\nElapsed time <span class=\"subst\">{end - start:<span class=\"number\">.3</span>f}</span>s'</span>)</span><br></pre></td></tr></table></figure>\n<p>函数<code>PollardRhoFactor()</code>接受三个参数:<code>n</code>是待分解的合数,<code>seed</code>是伪随机序列的初始值,<code>c</code>是生成多项式中的常数值。函数内部使用两个<code>while</code>构成双循环:外循环定义生成多项式<code>f</code>和快慢节点<code>h</code>及<code>t</code>,内循环实现节点移动和最大公约数运算。只有当最大公约数<code>d</code>不为1时,才结束内循环。此时如果<code>d</code>不等于<code>n</code>,它就是要找的非平凡因子,函数返回<code>d</code>。<code>d</code>等于<code>n</code>时,快慢节点已在环上相遇。这时在外循环重设<code>seed</code>为快节点的数值,并将<code>c</code>加一,就此重新开始新的一轮搜寻。</p>\n<p>在 MacBook Pro(2019)上运行以上代码,输出如下</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">N P Elapsed Time (s)</span><br><span class=\"line\">10967535067 104729 0.001</span><br><span class=\"line\">18446744073709551617 274177 0.002</span><br><span class=\"line\">97546105601219326301 9876543191 0.132</span><br><span class=\"line\">780002082420246798979794021150335143 244300526707007 6.124</span><br><span class=\"line\"></span><br><span class=\"line\">F8 = 115792089237316195423570985008687907853269984665640564039457584007913129639937</span><br><span class=\"line\">p = 1238926361552897</span><br><span class=\"line\">Elapsed time 64.411s</span><br></pre></td></tr></table></figure>\n<p>此结果证明了波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法的有效性。特别是最后一个测试,函数的输入是费马数 <span class=\"math inline\">\\(F_8\\)</span>(定义是 <span class=\"math inline\">\\(F_{n}=2^{2^{n}}+1\\)</span>,其中 <span class=\"math inline\">\\(n\\)</span> 为非负整数)。在1980年,波拉德与澳大利亚数学家理查德·布伦特<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>一起应用此算法首次分解<span class=\"math inline\">\\(F_8\\)</span>。当时他们使用的是UNIVAC 1100/42计算机,耗时2小时。而现在,在一台普及版的笔记本电脑上,波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法用时64.4秒就分解出了<span class=\"math inline\">\\(F_8\\)</span>的较小的素因子1238926361552897。</p>\n<p>后续波拉德与布伦特还对算法做了进一步的改进。他们观察到,如果<span class=\"math inline\">\\(\\gcd(d, N)>1\\)</span>,对于任意正整数 <span class=\"math inline\">\\(k\\)</span>,也有<span class=\"math inline\">\\(\\gcd(kd, N)>1\\)</span>。所以将连续 <span class=\"math inline\">\\(k\\)</span> 个<span class=\"math inline\">\\((|x_h-x_t| \\pmod N)\\)</span>相乘,用得到的积做模<span class=\"math inline\">\\(N\\)</span>运算,再与<span class=\"math inline\">\\(N\\)</span>求最大公约数,可以得到一样的结果。这种方法用<span class=\"math inline\">\\((k-1)\\)</span>次模<span class=\"math inline\">\\(N\\)</span>上的乘法和一次<span class=\"math inline\">\\(\\gcd\\)</span>代替 <span class=\"math inline\">\\(k\\)</span> 次<span class=\"math inline\">\\(\\gcd\\)</span>,从而达到加速效果。缺点是,有时它可能会因引入一个重复因子而导致算法失败。在这种情况下,可以将 <span class=\"math inline\">\\(k\\)</span> 重设为1,返回常规波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法。</p>\n<p>下面的Python函数实现了改进的波拉德 <span class=\"math inline\">\\(\\rho\\)</span>算法。它加入了额外的一个<code>for</code>循环实现 <span class=\"math inline\">\\(k\\)</span> 个差值模<span class=\"math inline\">\\(N\\)</span>上的连乘,得到的积存在变量<code>mult</code>中。<code>mult</code>再与<span class=\"math inline\">\\(N\\)</span>求最大公约数,结果赋值给<code>d</code>。如果失败,<span class=\"math inline\">\\(k\\)</span> 在外循环里被置为1。</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">PollardRhoFactor2</span>(<span class=\"params\">n, seed, c, k</span>):</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> n % <span class=\"number\">2</span> == <span class=\"number\">0</span>: <span class=\"keyword\">return</span> <span class=\"number\">2</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> gmpy2.is_prime(n): <span class=\"keyword\">return</span> n</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> <span class=\"literal\">True</span>:</span><br><span class=\"line\"> f = <span class=\"keyword\">lambda</span> x: (x**<span class=\"number\">2</span> + c) % n</span><br><span class=\"line\"> t = h = seed</span><br><span class=\"line\"> d = <span class=\"number\">1</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> d == <span class=\"number\">1</span>:</span><br><span class=\"line\"> mult = <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">for</span> _ <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(k):</span><br><span class=\"line\"> t = f(t) <span class=\"comment\"># Tortoise</span></span><br><span class=\"line\"> h = f(f(h)) <span class=\"comment\"># Hare</span></span><br><span class=\"line\"> mult = (mult * <span class=\"built_in\">abs</span>(h - t)) % n</span><br><span class=\"line\"> </span><br><span class=\"line\"> d = gmpy2.gcd(mult, n)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> d != n:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> d <span class=\"comment\"># find a non-trivial factor</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># start a new round with updated seed and c</span></span><br><span class=\"line\"> seed = h</span><br><span class=\"line\"> c += <span class=\"number\">1</span></span><br><span class=\"line\"> k = <span class=\"number\">1</span> <span class=\"comment\"># fall back to regular rho algorithm</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f"<span class=\"subst\">{<span class=\"string\">'N'</span>:<<span class=\"number\">37</span>}</span><span class=\"subst\">{<span class=\"string\">'P'</span>:<<span class=\"number\">16</span>}</span><span class=\"subst\">{<span class=\"string\">'Elapsed Time (s)'</span>:}</span>"</span>)</span><br><span class=\"line\"><span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"number\">0</span>, <span class=\"built_in\">len</span>(N)):</span><br><span class=\"line\"> start = time.process_time()</span><br><span class=\"line\"> p = PollardRhoFactor2(N[i], <span class=\"number\">2</span>, <span class=\"number\">1</span>, <span class=\"number\">100</span>)</span><br><span class=\"line\"> end = time.process_time()</span><br><span class=\"line\"> <span class=\"built_in\">print</span>(<span class=\"string\">f'<span class=\"subst\">{N[i]:<<span class=\"number\">37</span>}</span><span class=\"subst\">{p:<<span class=\"number\">16</span>}</span><span class=\"subst\">{end - start:<span class=\"number\">16.3</span>f}</span>'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">F8 = <span class=\"number\">2</span>**(<span class=\"number\">2</span>**<span class=\"number\">8</span>) + <span class=\"number\">1</span> <span class=\"comment\"># A 78-digit Fermat number</span></span><br><span class=\"line\">start = time.process_time()</span><br><span class=\"line\">p = PollardRhoFactor2(F8, <span class=\"number\">2</span>, <span class=\"number\">1</span>, <span class=\"number\">100</span>)</span><br><span class=\"line\">end = time.process_time()</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'\\nF8 = <span class=\"subst\">{F8}</span>\\np = <span class=\"subst\">{p}</span>\\nElapsed time <span class=\"subst\">{end - start:<span class=\"number\">.3</span>f}</span>s'</span>)</span><br></pre></td></tr></table></figure>\n<p>使用同样的测试用例,调用时设<span class=\"math inline\">\\(k\\)</span>为100,程序运行的结果是</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">N P Elapsed Time (s)</span><br><span class=\"line\">10967535067 104729 0.001</span><br><span class=\"line\">18446744073709551617 274177 0.002</span><br><span class=\"line\">97546105601219326301 9876543191 0.128</span><br><span class=\"line\">780002082420246798979794021150335143 244300526707007 5.854</span><br><span class=\"line\"></span><br><span class=\"line\">F8 = 115792089237316195423570985008687907853269984665640564039457584007913129639937</span><br><span class=\"line\">p = 1238926361552897</span><br><span class=\"line\">Elapsed time 46.601s</span><br></pre></td></tr></table></figure>\n<p>可以看到,对于比较小的合数<span class=\"math inline\">\\(N\\)</span>,改进并不明显。当<span class=\"math inline\">\\(N\\)</span>变大时,速度明显加快。对于十进制78位的费马数 <span class=\"math inline\">\\(F_8\\)</span>,改进的波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法用时仅为46.6秒,对比常规算法加速比超过27%。改进的波拉德 <span class=\"math inline\">\\(\\rho\\)</span>算法确实带来了速度的显着提升。</p>\n<p>总结以上波拉德 <span class=\"math inline\">\\(\\rho\\)</span>算法的分析、实现和测试,在生成RSA所用的素数 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 时,有必要设置一个数值下限。如果其中任何一个太小,必须重新生成,不然就可能被攻击者应用波拉德 <span class=\"math inline\">\\(\\rho\\)</span> 算法破解。</p>\n<h2 id=\"低私钥指数攻击\">低私钥指数攻击</h2>\n<p>对于一些特殊的应用场景(如智能卡和物联网),受限于设备的计算能力和低功耗要求,需要小一些的私钥指数 <span class=\"math inline\">\\(d\\)</span> 以加快解密或生成数字签名的速度。然而,太低的私钥指数非常危险,有一些巧妙的攻击方法可以完全瓦解RSA加密系统。</p>\n<h3 id=\"维纳攻击\">维纳攻击</h3>\n<p>1990年,加拿大密码学家迈克尔·维纳(Michael J. Wiener)构思了一种基于<strong>连分数逼近</strong>的破解方案<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>,在一定条件下可以有效地从RSA的公开密钥<span class=\"math inline\">\\((N,e)\\)</span>解出私钥指数<span class=\"math inline\">\\(d\\)</span>。在讲解这种攻击的工作原理之前,先简单介绍一下连分数的概念和关键特性。</p>\n<h4 id=\"连分数\">连分数</h4>\n<p>连分数本身只是一种数学表达式,但它引入了一种研究实数的新视角。以下就是典型的连分数<span class=\"math display\">\\[x = a_0 + \\cfrac{1}{a_1 + \\cfrac{1}{a_2 + \\cfrac{1}{\\ddots\\,}}} \\]</span> 其中 <span class=\"math inline\">\\(a_{0}\\)</span>是整数,而所有其他的 <span class=\"math inline\">\\(a_{i}(i=1,\\ldots ,n)\\)</span>都是正整数。连分数也常常简写作<span class=\"math inline\">\\(x=[a_0;a_1,a_2,\\ldots,a_n]\\)</span>。连分数有如下性质:</p>\n<ol type=\"1\">\n<li><p>有理数可以表示为有限连分数,即有限数目的<span class=\"math inline\">\\(a_{i}\\)</span>。无理数可用一种精确的方式表示为无限连分数。下面是两个例子: <span class=\"math display\">\\[\\begin{align}\n\\frac {68} {75}&=0+\\cfrac {1} {1+\\cfrac {1} {\\small 9+\\cfrac {1} {\\scriptsize 1+\\cfrac {1} {2+\\cfrac {1} {2}}}}}=[0;1,9,1,2,2]\\\\\nπ&=[3;7,15,1,292,1,1,1,2,…]\n\\end{align}\\]</span></p></li>\n<li><p>计算正有理数 <span class=\"math inline\">\\(f\\)</span> 的连分数表示时,先减去 <span class=\"math inline\">\\(f\\)</span> 的整数部分,然后反复倒转余数并减去整数部分,直到余数为零。设 <span class=\"math inline\">\\(a_i\\)</span> 为整数商,<span class=\"math inline\">\\(r_i\\)</span> 为第 <span class=\"math inline\">\\(i\\)</span> 步的余数,<span class=\"math inline\">\\(n\\)</span> 为倒转步数,则有 <span class=\"math display\">\\[\\begin{align}\na_0 &= \\lfloor f \\rfloor, &r_0 &= f - a_0\\\\\na_i&={\\large\\lfloor} \\frac 1 {r_{i-1}} {\\large\\rfloor}, &r_i &=\\frac 1 {r_{i-1}} - a_i \\quad (i = 1, 2, ..., n)\\\\\n\\end{align}\\]</span> 用Python语言实现的正有理数的连分数扩展函数如下</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">cf_expansion</span>(<span class=\"params\">nm: <span class=\"built_in\">int</span>, dn:<span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">list</span>:</span></span><br><span class=\"line\"> <span class=\"string\">""" Continued Fraction Expansion of Rationals</span></span><br><span class=\"line\"><span class=\"string\"> Parameters:</span></span><br><span class=\"line\"><span class=\"string\"> nm - numerator</span></span><br><span class=\"line\"><span class=\"string\"> dn - denomainator</span></span><br><span class=\"line\"><span class=\"string\"> Return:</span></span><br><span class=\"line\"><span class=\"string\"> List for the abbreviated notation of the continued fraction</span></span><br><span class=\"line\"><span class=\"string\"> """</span></span><br><span class=\"line\"> cf = []</span><br><span class=\"line\"> a, r = nm // dn, nm % dn</span><br><span class=\"line\"> cf.append(a)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> r != <span class=\"number\">0</span>:</span><br><span class=\"line\"> nm, dn = dn, r</span><br><span class=\"line\"> a = nm // dn</span><br><span class=\"line\"> r = nm % dn</span><br><span class=\"line\"> cf.append(a)</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> cf</span><br></pre></td></tr></table></figure></p></li>\n<li><p>无论是有理数还是无理数,其连分数表示的初始段计算出来的有理数都提供了对这个数本身逐次的逼近。这些有理数被称为连分数的<strong>收敛子</strong>(convergent,也译为“渐进分数”)。所有偶数编号的收敛子都小于原数,而奇数编号的收敛子都大于它。记连分数第 <span class=\"math inline\">\\(i\\)</span> 个收敛子的分子为 <span class=\"math inline\">\\(h_i\\)</span>,分母为 <span class=\"math inline\">\\(k_i\\)</span>,并且设定 <span class=\"math inline\">\\(h_{-1}=1,h_{-2}=0\\)</span> 和 <span class=\"math inline\">\\(k_{-1}=0,k_{-2}=1\\)</span>,则收敛子的递归计算公式为 <span class=\"math display\">\\[\\frac{h_i}{k_i}= \\frac{a_i h_{i-1}+h_{i-2}}{a_i k_{i-1}+k_{i-2}}\\]</span> 比如对于上面的例子<span class=\"math inline\">\\(\\frac {68} {75}\\)</span>,其连分数的连续收敛子依次为 <span class=\"math display\">\\[\\begin{align}\n\\frac {h_0} {k_0} &= [0] = \\frac 0 1 = 0<\\frac {68} {75}\\\\\n\\frac {h_1} {k_1} &= [0;1] = \\frac 1 1 = 1>\\frac {68} {75}\\\\\n\\frac {h_2} {k_2} &= [0;1,9] = \\frac 9 {10}<\\frac {68} {75}\\\\\n\\frac {h_3} {k_3} &= [0;1,9,1] = \\frac {10} {11}>\\frac {68} {75}\\\\\n\\frac {h_4} {k_4} &= [0;1,9,1,2] = \\frac {29} {32}<\\frac {68} {75}\\\\\n\\end{align}\\]</span>可以验证这些收敛子满足前述的奇偶性质,而且越来越接近真实值。下面的Python函数实现了一个给定连分数的收敛子生成器,它返回收敛子分子和分母元组对象。</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">cf_convergent</span>(<span class=\"params\">cf: <span class=\"built_in\">list</span></span>) -> (<span class=\"built_in\">int</span>, <span class=\"built_in\">int</span>):</span></span><br><span class=\"line\"> <span class=\"string\">""" Calculates the convergents of a continued fraction</span></span><br><span class=\"line\"><span class=\"string\"> Parameters:</span></span><br><span class=\"line\"><span class=\"string\"> cf - list for the continued fraction expansion</span></span><br><span class=\"line\"><span class=\"string\"> Return:</span></span><br><span class=\"line\"><span class=\"string\"> A generator object of the convergent tuple</span></span><br><span class=\"line\"><span class=\"string\"> (numerator, denominator)</span></span><br><span class=\"line\"><span class=\"string\"> """</span></span><br><span class=\"line\"> nm = [] <span class=\"comment\"># Numerator</span></span><br><span class=\"line\"> dn = [] <span class=\"comment\"># Denominators</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> i <span class=\"keyword\">in</span> <span class=\"built_in\">range</span>(<span class=\"built_in\">len</span>(cf)):</span><br><span class=\"line\"> <span class=\"keyword\">if</span> i == <span class=\"number\">0</span>:</span><br><span class=\"line\"> ni, di = cf[i], <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">elif</span> i == <span class=\"number\">1</span>:</span><br><span class=\"line\"> ni, di = cf[i]*cf[i-<span class=\"number\">1</span>] + <span class=\"number\">1</span>, cf[i]</span><br><span class=\"line\"> <span class=\"keyword\">else</span>: <span class=\"comment\"># i > 1</span></span><br><span class=\"line\"> ni = cf[i]*nm[i-<span class=\"number\">1</span>] + nm[i-<span class=\"number\">2</span>]</span><br><span class=\"line\"> di = cf[i]*dn[i-<span class=\"number\">1</span>] + dn[i-<span class=\"number\">2</span>]</span><br><span class=\"line\"></span><br><span class=\"line\"> nm.append(ni)</span><br><span class=\"line\"> dn.append(di)</span><br><span class=\"line\"> <span class=\"keyword\">yield</span> ni, di</span><br></pre></td></tr></table></figure></p></li>\n<li><p>关于连分数的收敛子还有一个重要的勒让德定理<a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>(Legendre Theorem):对于正有理数 <span class=\"math inline\">\\(f\\)</span>,如果整数 <span class=\"math inline\">\\(a\\)</span> 和正整数 <span class=\"math inline\">\\(b\\)</span>(即 <span class=\"math inline\">\\(a∈ \\mathbb Z, b ∈ \\mathbb Z^+\\)</span>)满足<span class=\"math display\">\\[\\left\\lvert\\,f - \\frac a b\\right\\rvert< \\frac 1 {2b^2}\\]</span>那么<span class=\"math inline\">\\(\\frac a b\\)</span>等于 <span class=\"math inline\">\\(f\\)</span> 的连分数的一个收敛子。</p></li>\n</ol>\n<h4 id=\"攻击原理\">攻击原理</h4>\n<p>现在解析维纳攻击的工作原理。从RSA公私钥之间的关系 <span class=\"math inline\">\\(ed\\equiv 1 {\\pmod {\\varphi (N)}}\\)</span>,可以导出存在整数 <span class=\"math inline\">\\(k\\)</span> 使得<span class=\"math display\">\\[ed - k\\varphi (N) = 1\\]</span> 两边同时除以 <span class=\"math inline\">\\(d\\varphi (N)\\)</span> 得到<span class=\"math display\">\\[\\left\\lvert\\frac e {\\varphi (N)} - \\frac k d\\right\\rvert = \\frac 1 {d{\\varphi (N)}}\\]</span> 仔细观察上式,因为<span class=\"math inline\">\\(\\varphi (N)\\)</span>本身很大,而且<span class=\"math inline\">\\(\\gcd(k,d)=1\\)</span>,<span class=\"math inline\">\\(\\frac k d\\)</span>与<span class=\"math inline\">\\(\\frac e {\\varphi (N)}\\)</span>非常接近。此外<span class=\"math display\">\\[\\varphi (N)=(p-1)(q-1)=N-(p+q)+1\\]</span>其与<span class=\"math inline\">\\(N\\)</span>的差值也相对很小,所以<span class=\"math inline\">\\(\\frac k d\\)</span>与<span class=\"math inline\">\\(\\frac e N\\)</span>也相差不大。RSA的<span class=\"math inline\">\\((N,e)\\)</span>都是公开的,由此维纳大胆地构想——<strong>如果对<span class=\"math inline\">\\(\\frac e N\\)</span>进行连分数展开,有可能<span class=\"math inline\">\\(\\frac k d\\)</span>就是其中的某个收敛子!</strong></p>\n<p>那么如何验证某个收敛子确实就是<span class=\"math inline\">\\(\\frac k d\\)</span>呢?有了 <span class=\"math inline\">\\(k\\)</span> 和 <span class=\"math inline\">\\(d\\)</span>,就可以算出 <span class=\"math inline\">\\(\\varphi (N)\\)</span>,从而得到 <span class=\"math inline\">\\((p+q)\\)</span>。既然 <span class=\"math inline\">\\((p+q)\\)</span> 和 <span class=\"math inline\">\\(pq\\)</span> 都已知,构造一个简单的一元二次方程<a href=\"#fn5\" class=\"footnote-ref\" id=\"fnref5\" role=\"doc-noteref\"><sup>5</sup></a>就能解出 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>。如果二者相乘等于<span class=\"math inline\">\\(N\\)</span>,<span class=\"math inline\">\\(k\\)</span> 和 <span class=\"math inline\">\\(d\\)</span> 就是对的,攻击就成功了。</p>\n<p>维纳攻击成立的条件是什么?参考上一节讲到的勒让德定理,可以得出如果有 <span class=\"math display\">\\[\\left\\lvert\\frac e N - \\frac k d\\right\\rvert < \\frac 1 {2{d^2}}\\]</span>则<span class=\"math inline\">\\(\\frac k d\\)</span>一定是<span class=\"math inline\">\\(\\frac e N\\)</span>的某个收敛子。由此式还能推导出攻击破解私钥指数 <span class=\"math inline\">\\(d\\)</span> 的上限。<a href=\"https://scholar.google.com/scholar?cluster=14819867265705249637&hl=en&as_sdt=0,5\">维纳的原始论文</a>指出上界为 <span class=\"math inline\">\\(N^{\\frac 1 4}\\)</span>,但是没有具体的分析。1999年,美国密码学家丹·博内<a href=\"#fn6\" class=\"footnote-ref\" id=\"fnref6\" role=\"doc-noteref\"><sup>6</sup></a>提供了<a href=\"https://crypto.stanford.edu/~dabo/papers/RSA-survey.pdf\">第一个严格证明的上限</a>,表明在 <span class=\"math inline\">\\(q<p<2q\\)</span> 和 <span class=\"math inline\">\\(e<\\varphi (N)\\)</span> 的约束下,维纳攻击适用于 <span class=\"math inline\">\\(d<\\frac 1 3 N^{\\frac 1 4}\\)</span> 的情况。2019年,澳大利亚伍伦贡大学(University of Wollongong)的几位研究者发表的一篇<a href=\"https://ink.library.smu.edu.sg/cgi/viewcontent.cgi?article=8411&context=sis_research\">新论文</a>进一步拓展相同约束条件下的上限为 <span class=\"math display\">\\[d\\leq \\frac 1 {\\sqrt[4]{18}} N^\\frac 1 4=\\frac 1 {2.06...}N^\\frac 1 4\\]</span></p>\n<p>注意,出于简洁的考虑,以上维纳攻击原理的解析基于欧拉函数 <span class=\"math inline\">\\(\\varphi (N)\\)</span>。实际中RSA密钥对的生成经常使用卡迈克尔函数<span class=\"math inline\">\\(\\lambda(N)\\)</span>。二者之间的关系是 <span class=\"math display\">\\[\\varphi (N)=\\lambda(n)\\cdot\\gcd(p-1,q-1)\\]</span> 可以证明从 <span class=\"math inline\">\\(ed\\equiv 1 {\\pmod {\\lambda (N)}}\\)</span> 出发,能得到同样的结论。实际上,维纳就是基于<span class=\"math inline\">\\(\\lambda(N)\\)</span>推导的,感兴趣的读者请去仔细阅读他的原始论文。</p>\n<h4 id=\"攻击流程\">攻击流程</h4>\n<p>理解了维纳攻击的原理,其攻击流程可以总结如下:</p>\n<ol type=\"1\">\n<li>将<span class=\"math inline\">\\(\\frac e N\\)</span>作连分数展开</li>\n<li>生成此连分数的连续收敛子序列</li>\n<li>依次检查每个收敛子的分子 <span class=\"math inline\">\\(k\\)</span> 和分母 <span class=\"math inline\">\\(d\\)</span>\n<ul>\n<li>如果<span class=\"math inline\">\\(k\\)</span>为零,或<span class=\"math inline\">\\(d\\)</span>为偶数,或不满足 <span class=\"math inline\">\\(ed\\equiv 1 \\pmod k\\)</span>,跳过此收敛子</li>\n<li>计算 <span class=\"math inline\">\\(\\varphi (N) = \\frac {ed-1} k\\)</span>,求 <span class=\"math inline\">\\(x^2−(N−\\varphi (N)+1)x+N\\)</span> 的整数根 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span></li>\n<li>验证 <span class=\"math inline\">\\(N=p\\cdot q\\)</span>,如果为真,攻击成功,返回 <span class=\"math inline\">\\((p,q,d)\\)</span>;否则继续</li>\n</ul></li>\n<li>如果全部收敛子都检查完毕,维纳攻击失败。</li>\n</ol>\n<p>对应的完整Python程序实现如下</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">import</span> gmpy2</span><br><span class=\"line\"><span class=\"keyword\">import</span> random</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">solve_rsa_primes</span>(<span class=\"params\">s: <span class=\"built_in\">int</span>, m: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">tuple</span>:</span></span><br><span class=\"line\"> <span class=\"string\">""" Solve RSA prime numbers (p, q) from the quadratic equation</span></span><br><span class=\"line\"><span class=\"string\"> p^2 - s * p + m = 0 with the formula p = s/2 +/- sqrt((s/2)^2 - m)</span></span><br><span class=\"line\"><span class=\"string\"> Parameters:</span></span><br><span class=\"line\"><span class=\"string\"> s - sum of primes (p + q)</span></span><br><span class=\"line\"><span class=\"string\"> m - product of primes (p * q)</span></span><br><span class=\"line\"><span class=\"string\"> Return: (p, q)</span></span><br><span class=\"line\"><span class=\"string\"> """</span></span><br><span class=\"line\"> half_s = s >> <span class=\"number\">1</span></span><br><span class=\"line\"> tmp = gmpy2.isqrt(half_s ** <span class=\"number\">2</span> - m)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">int</span>(half_s + tmp), <span class=\"built_in\">int</span>(half_s - tmp)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">wiener_attack</span>(<span class=\"params\">n: <span class=\"built_in\">int</span>, e: <span class=\"built_in\">int</span></span>) -> (<span class=\"built_in\">int</span>, <span class=\"built_in\">int</span>, <span class=\"built_in\">int</span>):</span></span><br><span class=\"line\"> <span class=\"string\">""" Wiener's Attack on RSA public key cryptosystem</span></span><br><span class=\"line\"><span class=\"string\"> Parameters:</span></span><br><span class=\"line\"><span class=\"string\"> N - RSA modulus N = p*q</span></span><br><span class=\"line\"><span class=\"string\"> e - RSA public exponent</span></span><br><span class=\"line\"><span class=\"string\"> Return:</span></span><br><span class=\"line\"><span class=\"string\"> A tuple of (p, q, d)</span></span><br><span class=\"line\"><span class=\"string\"> p, q - the two prime factors of RSA modulus N</span></span><br><span class=\"line\"><span class=\"string\"> d - RSA private exponent</span></span><br><span class=\"line\"><span class=\"string\"> """</span></span><br><span class=\"line\"> cfe = cf_expansion(e, n) <span class=\"comment\"># Convert e/n into a continued fraction</span></span><br><span class=\"line\"> cvg = cf_convergent(cfe) <span class=\"comment\"># Get all of its convergents</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> k, d <span class=\"keyword\">in</span> cvg:</span><br><span class=\"line\"> <span class=\"comment\"># Check if k and d meet the requirements</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> k == <span class=\"number\">0</span> <span class=\"keyword\">or</span> d % <span class=\"number\">2</span> == <span class=\"number\">0</span> <span class=\"keyword\">or</span> (e * d) % k != <span class=\"number\">1</span>:</span><br><span class=\"line\"> <span class=\"keyword\">continue</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\"># assume ed ≡ 1 (mod ϕ(n))</span></span><br><span class=\"line\"> phi = (e * d - <span class=\"number\">1</span>) // k </span><br><span class=\"line\"> p, q = solve_rsa_primes(n - phi + <span class=\"number\">1</span>, n)</span><br><span class=\"line\"> <span class=\"keyword\">if</span> n == p * q:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> p, q, d</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">None</span></span><br><span class=\"line\"> </span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">uint_to_bytes</span>(<span class=\"params\">x: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">bytes</span>:</span></span><br><span class=\"line\"> <span class=\"string\">""" This works only for unsigned (non-negative) integers.</span></span><br><span class=\"line\"><span class=\"string\"> It does not work for 0."""</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> x == <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">bytes</span>(<span class=\"number\">1</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x.to_bytes((x.bit_length() + <span class=\"number\">7</span>) // <span class=\"number\">8</span>, <span class=\"string\">'big'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">N = <span class=\"built_in\">int</span>(</span><br><span class=\"line\"> <span class=\"string\">'6727075990400738687345725133831068548505159909089226'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'9093081511054056173840933739311418333016536024767844'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'14065504536979164089581789354173719785815972324079'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">e = <span class=\"built_in\">int</span>(</span><br><span class=\"line\"> <span class=\"string\">'4805054278857670490961232238450763248932257077920876'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'3637915365038611552743522891345050097418639182479215'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'15546177391127175463544741368225721957798416107743'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">c = <span class=\"built_in\">int</span>(</span><br><span class=\"line\"> <span class=\"string\">'5928120944877154092488159606792758283490469364444892'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'1679423458017133739626176287570534122326362199676752'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'56510422984948872954949616521392542703915478027634'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">p, q, d = wiener_attack(N, e)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> <span class=\"keyword\">not</span> d <span class=\"keyword\">is</span> <span class=\"literal\">None</span>, <span class=\"string\">"Wiener's Attack failed!"</span></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"p ="</span>, p)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"q ="</span>, q)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"d ="</span>, d)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(uint_to_bytes(<span class=\"built_in\">pow</span>(c, d, N)))</span><br><span class=\"line\"></span><br><span class=\"line\">N = <span class=\"built_in\">int</span>(</span><br><span class=\"line\"> <span class=\"string\">'22836858353287668091920368816286415778103964252589'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'28295130420474999022996621982166664596581454018899'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'48429922376560732622754871538043874356270300826321'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'16650572564937978011181394388679265524940467869924'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'85473650038355720409426235584833584188449224331698'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'63569900296911605460645581176522325967221393273906'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'69673188457131381644120787783215342848744792830245'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'01805598140668893320307200136190794138325132168722'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'14217943474001731747822701596634040292342194986951'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'94551646668806852454006312372413658692027515557841'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'41440661232146905186431357112566536770669381756925'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'38179415478954522854711968599279014482060579354284'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'55238863726089083'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">e = <span class=\"built_in\">int</span>(</span><br><span class=\"line\"> <span class=\"string\">'17160819308904585327789016134897914235762203050367'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'34632679585567058963995675965428034906637374660531'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'64750599687461192166424505919293706011293378320096'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'43372382766547546926535697752805239918767190684796'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'26509298669049485976118315666126871681847641670872'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'58895073919139366379901867664076540531765577090231'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'67209821832859747419658344363466584895316847817524'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'24703257392651850823517297420382138943770358904660'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'59442300191228592937251734592732623207324742303631'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'32436274414264865868028527840102483762414082363751'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'87208612632105886502393648156776330236987329249988'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'11429508256124902530957499338336903951924035916501'</span>\\</span><br><span class=\"line\"> <span class=\"string\">'53661610070010419'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">d = wiener_attack(N, e)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> <span class=\"keyword\">not</span> d <span class=\"keyword\">is</span> <span class=\"literal\">None</span>, <span class=\"string\">"Wiener's attack failed!"</span></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"d ="</span>, d)</span><br><span class=\"line\"></span><br><span class=\"line\">old_b = <span class=\"built_in\">int</span>(gmpy2.root(N, <span class=\"number\">4</span>)/<span class=\"number\">3</span>)</span><br><span class=\"line\">new_b = <span class=\"built_in\">int</span>(gmpy2.root(N, <span class=\"number\">4</span>)/gmpy2.root(<span class=\"number\">18</span>, <span class=\"number\">4</span>))</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"old_b ="</span>, old_b)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"new_b ="</span>, new_b)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> d > old_b <span class=\"keyword\">and</span> d <= new_b</span><br></pre></td></tr></table></figure>\n<p>上面的代码最后是两个测试用例。参考下面的程序运行输出,第一个用例给定一个较小的RSA模数 <span class=\"math inline\">\\(N\\)</span>,而 <span class=\"math inline\">\\(e\\)</span> 却相对较大,这正是维纳攻击发挥作用的场景。程序调用攻击函数<code>wiener_attack()</code>,迅速算出了 <span class=\"math inline\">\\(d\\)</span> 为7并解密恢复了原文<code>Wiener's attack success!</code>。</p>\n<p>第二个用例设定了2048比特的 <span class=\"math inline\">\\(N\\)</span> 和 <span class=\"math inline\">\\(e\\)</span>,维纳攻击也很快成功了。同时程序还检验了破解出来的 <span class=\"math inline\">\\(d\\)</span>(511比特),确认它是大于旧的上限<code>old_b</code>(<span class=\"math inline\">\\(N^{\\frac 1 4}\\)</span>),但略小于新的上限<code>new_b</code>(<span class=\"math inline\">\\(\\frac 1 {\\sqrt[4]{18}} N^\\frac 1 4\\)</span>)。这证实了伍伦贡大学研究者的结论。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">p = 105192975360365123391387526351896101933106732127903638948310435293844052701259</span><br><span class=\"line\">q = 63949859459297920725542167940404754256294386312715512490347273751054137071981</span><br><span class=\"line\">d = 7</span><br><span class=\"line\">b<span class=\"string\">"Wiener's attack success!"</span></span><br><span class=\"line\">d = 5968166949079360555220268992852191823920023811474288738674370592596189517443887780023653031793516493806462114248181371416016184480421640973439863346079123</span><br><span class=\"line\">old_b = 4097678063688683751669784036917434915284399064709500941393388469932708726583832656910141469383433913840738001283204519671690533047637554279688711463501824</span><br><span class=\"line\">new_b = 5968166949079360962136673400587903792234115710617172051628964885379180548131448950677569697264501402772121272285767654845001503996650347315559383468867584</span><br></pre></td></tr></table></figure>\n<p>无疑,这两个测试用例证明了维纳攻击的有效性和成立条件。要预防维纳攻击,就必须保证RSA私钥指数 <span class=\"math inline\">\\(d\\)</span> 大于能使攻击成立的上限值。有可能现在知道的上限也并非最终的边界,所以选择 <span class=\"math inline\">\\(d\\)</span> 不小于 <span class=\"math inline\">\\(N^\\frac 1 2\\)</span> 是较为稳妥的方案。实际应用中,常常使用<a href=\"https://www.packetmania.net/2021/02/14/Fermats-Little-Theorem/#%E5%8A%A0%E9%80%9Frsa%E8%A7%A3%E5%AF%86\">费马小定理和中国余数定理相结合推演出的一种加速解密方法</a>,这样即使是较大的 <span class=\"math inline\">\\(d\\)</span> 也能实现快速解密和数字签名。</p>\n<blockquote>\n<p>未完待续,敬请期待下篇:RSA的攻与防(三)</p>\n</blockquote>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>约翰·波拉德(John Pollard),英国数学家,1999年因对整数分解和离散对数的代数密码分析的重大贡献获得<a href=\"https://en.wikipedia.org/wiki/RSA_Award_for_Excellence_in_Mathematics\">RSA数学卓越奖</a>。<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>理查德·布伦特(Richard Brent),澳大利亚数学家和计算机科学家,其研究领域包括数论(特别是因式分解)、随机数生成器、计算机体系结构和算法分析,IEEE 和 ACM 双会士,澳大利亚科学院院士。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>M. Wiener, “<a href=\"https://scholar.google.com/scholar?cluster=14819867265705249637&hl=en&as_sdt=0,5\">Cryptanalysis of short RSA secret exponents</a>,” <em>IEEE Trans. Inform. Theory</em>, vol. 36, pp. 553–558, May 1990<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>阿德里安-马里·勒让德(Adrien-Marie Legendre),法国数学家,在统计学、数论、抽象代数与数学分析上贡献诸多。<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn5\" role=\"doc-endnote\"><p>参见前文:<a href=\"https://www.packetmania.net/2022/06/17/picoCTF-Sum-O-Primes/\">巧解picoCTF的RSA挑战题Sum-O-Primes</a><a href=\"#fnref5\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn6\" role=\"doc-endnote\"><p>丹·博内(Dan Boneh),以色列裔美国应用密码学家和计算机安全专家,美国数学学会会士,美国国家工程院院士,斯坦福大学教授。2005年获RSA数学卓越奖,2013年获哥德尔奖。<a href=\"#fnref6\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["技术小札"],"tags":["密码学","网络安全","Python编程"]},{"title":"RSA的攻与防(三)","url":"/2023/12/29/RSA-attack-defense-3/","content":"<p>继续RSA攻防战,本篇专注介绍科波史密斯攻击(Coppersmith) <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Mathematics, rightly viewed, possesses not only truth, but supreme beauty.</strong><br> <strong>— <em>Bertrand Russell</em>(伯特兰·罗素,英国哲学家、数学家和逻辑学家,致力于哲学的大众化、普及化,经典著作《西方哲学史》的作者。)</strong></p>\n</div>\n<h2 id=\"section\"></h2>\n","categories":["技术小札"],"tags":["密码学","网络安全","Python编程"]},{"title":"RSA的攻与防(一)","url":"/2020/12/01/RSA-attack-defense/","content":"<p>RSA加密算法是一种非对称加密算法,1977年由麻省理工学院的三位密码学家和计算机科学家共同发明。RSA公钥加密算法和加密系统提供了数据保密和签名验证功能,在互联网中得到广泛使用。从诞生时起,RSA就开始成为现代密码学的一个主要研究对象。许多密码分析学家和信息安全专家一直在研究其可能的理论缺陷和技术漏洞,以保障实际应用中的安全性和可靠性。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>今有物不知其数,三三数之剩二;五五数之剩三;七七数之剩二。问物几何?</strong><br> <strong>— <em>《孙子算经》卷下·二十六</em></strong></p>\n</div>\n<p>幸运的是,历经40多年的广泛研究和现实应用的考验,虽然发现了不少精巧的攻击手段,总体上RSA是安全的。这些攻击手段都是利用RSA的不当使用或软硬件实现中脆弱性,并不能动摇其加密算法的安全性根基。另一方面,对这些攻击手段的研究表明,实现一个安全而牢固的RSA应用并不是一个简单的任务。实际上,密码学和网络安全软硬件工程实践中的一个共识就是:<strong>不要试图从头开始实现RSA!</strong><a href=\"#fn1\" class=\"footnote-ref\" id=\"fnref1\" role=\"doc-noteref\"><sup>1</sup></a> 恰当的方案,是使用现有的、久经测试并有可靠维护的库或API,去实现RSA算法和协议的应用。</p>\n<p>这里将常见的攻击RSA的手段、攻击所基于的数学机理和相应的防护措施做一个简要的调研。参考<a href=\"https://packetmania.github.io/2020/12/01/DH-and-RSA/\">前文</a>,开始我们先复习一下RSA的工作机制和流程:</p>\n<ol type=\"1\">\n<li>选择两个大的素数 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,计算 <span class=\"math inline\">\\(N=pq\\)</span></li>\n<li>计算 <span class=\"math inline\">\\(N\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/卡邁克爾函數\">卡迈克尔函数</a> <span class=\"math inline\">\\(\\lambda(N)\\)</span>\n<ul>\n<li>当 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span> 都为素数时,通常 <span class=\"math inline\">\\(\\lambda(pq)=\\mathrm{lcm}(p − 1, q − 1)\\)</span></li>\n<li><span class=\"math inline\">\\(\\mathrm{lcm}\\)</span> 是求最小公倍数的函数,可以用欧几里得算法得出</li>\n</ul></li>\n<li>选择一个小于 <span class=\"math inline\">\\(\\lambda(N)\\)</span> 且与之互素的数 <span class=\"math inline\">\\(e\\)</span>,并求得 <span class=\"math inline\">\\(e\\)</span> 关于 <span class=\"math inline\">\\(\\lambda(N)\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/模反元素\">模逆元</a> <span class=\"math inline\">\\(d\\equiv e^{-1}\\pmod {\\lambda(N)}\\)</span>\n<ul>\n<li>模逆元的定义是,找到 <span class=\"math inline\">\\(d\\)</span> 使得 <span class=\"math inline\">\\((d⋅e)\\bmod\\lambda(N)=1\\)</span></li>\n<li>模逆元可以用<a href=\"https://zh.wikipedia.org/zh-cn/扩展欧几里得算法\">扩展的欧几里得算法</a>得出</li>\n</ul></li>\n<li><span class=\"math inline\">\\(\\pmb{(N,e)}\\)</span> <strong>是公钥</strong>,<span class=\"math inline\">\\(\\pmb{(N,d)}\\)</span> <strong>是私钥</strong>\n<ul>\n<li>公钥公开,私钥必须密藏</li>\n<li>销毁所有 <span class=\"math inline\">\\(p,q,\\lambda(N)\\)</span> 的记录</li>\n</ul></li>\n<li>发送方先按照双方约定好的编码格式将消息转化为一个小于<span class=\"math inline\">\\(N\\)</span>的正整数<span class=\"math inline\">\\(m\\)</span>,然后使用接收方的公钥计算出密文<span class=\"math inline\">\\(c\\)</span>,计算公式是 <span class=\"math inline\">\\(\\pmb{c\\equiv m^e\\pmod N}\\)</span></li>\n<li>接收方收到密文后,使用自己的私钥计算出明文<span class=\"math inline\">\\(m\\)</span>,计算公式是 <span class=\"math inline\">\\(\\pmb{m\\equiv c^d\\pmod N}\\)</span>,然后解码成原始消息</li>\n<li>使用私钥加密的消息也可以由公钥解密,即如果 <span class=\"math inline\">\\(\\pmb{s\\equiv m^d\\pmod N}\\)</span>,则 <span class=\"math inline\">\\(\\pmb{m\\equiv s^e\\pmod N}\\)</span>。这就是所支持的数字签名功能</li>\n</ol>\n<p>注意,<a href=\"http://people.csail.mit.edu/rivest/Rsapaper.pdf\">原始的RSA论文</a>里第二步和第三步采用 <span class=\"math inline\">\\(N\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/欧拉函数\">欧拉函数</a> <span class=\"math inline\">\\(\\varphi(N)\\)</span>。这两个函数之间的关系是: <span class=\"math display\">\\[\\varphi(N)=\\lambda(N)⋅\\mathrm{gcd}(p-1,q-1)\\]</span> 这里 <span class=\"math inline\">\\(\\mathrm{gcd}\\)</span> 为最大公约数函数。使用 <span class=\"math inline\">\\(\\lambda(N)\\)</span> 可以求得最小可用的私钥指数 <span class=\"math inline\">\\(d\\)</span>,有利于高效的解密和签名运算。无论是使用欧拉函数还是卡迈克尔函数,遵循以上流程的实现常常被称为“<strong>教科书RSA</strong>”。</p>\n<p>教科书RSA是不安全的,有许多简单而有效的攻击手段。在详细讨论教科书RSA的安全漏洞之前,有必要审视一下人所共知的首要攻击方法 — 大数分解!</p>\n<h2 id=\"大数分解\">大数分解</h2>\n<p>RSA加密算法安全性的理论基石,就是<a href=\"https://zh.wikipedia.org/zh-cn/整数分解\"><strong>大数素因数分解问题</strong></a>。如果能从已知的<span class=\"math inline\">\\(N\\)</span>里面分离出 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,就可以马上推导出私钥指数<span class=\"math inline\">\\(d\\)</span>,从而完全破解RSA。大数分解是公认的计算难题,已知最好的渐近线运行时间算法是<a href=\"https://zh.wikipedia.org/zh-cn/普通数域筛选法\">普通数域筛选法</a> (General Number Field Sieve,简写GNFS),其时间复杂度是<span class=\"math inline\">\\({\\displaystyle \\exp \\left(\\left(c+o(1)\\right)(\\ln N)^{\\frac {1}{3}}(\\ln \\ln N)^{\\frac {2}{3}}\\right)}\\)</span>,这里常数 <span class=\"math inline\">\\(c = 4/\\sqrt[3]{9}\\)</span>,<span class=\"math inline\">\\(\\displaystyle \\exp\\)</span>为自然常数 (2.718) 的幂函数。</p>\n<p>对于给定的大数,精确地估计应用GNFS算法的实际复杂度是困难的。但是依据启发式的复杂性经验估计,我们可以粗略看出计算时间复杂性的增长趋势:</p>\n<ul>\n<li>对于1024比特的大数,有两个各约500比特的素因数,分解需要 <span class=\"math inline\">\\(2^{70}\\)</span> 数量级的基本运算操作</li>\n<li>对于2048比特的大数,有两个各约1000比特的素因数,分解需要 <span class=\"math inline\">\\(2^{90}\\)</span> 数量级的基本运算操作,比1024比特数慢一百万倍</li>\n</ul>\n<p>计算机软硬件技术的飞速发展,让许多过去看似不可能的任务成为现实。查询<a href=\"https://en.wikipedia.org/wiki/RSA_Factoring_Challenge\">RSA大数分解挑战</a>网站公布的最新纪录,2020二月法国计算数学家保罗·齐默尔曼 (Paul Zimmermann) 领导的团队成功分解了250位十进制数字 (<strong>829比特</strong>) 的大数 <a href=\"https://en.wikipedia.org/wiki/RSA_numbers#RSA-250\">RSA-250</a>:</p>\n<figure class=\"highlight plaintext\"><table><tr><td class=\"code\"><pre><span class=\"line\">RSA-250 = 6413528947707158027879019017057738908482501474294344720811685963202453234463</span><br><span class=\"line\"> 0238623598752668347708737661925585694639798853367</span><br><span class=\"line\"> × 3337202759497815655622601060535511422794076034476755466678452098702384172921</span><br><span class=\"line\"> 0037080257448673296881877565718986258036932062711</span><br></pre></td></tr></table></figure>\n<p>齐默尔曼发布的通告表示,以英特尔 Xeon Gold 6130 主频 2.1GHz 的处理器为参照,完成这一任务总的计算时间约为2700处理器核年 (core-year)。这一数字好像很大,但在当今集群计算、网格计算和云计算普及到大众的时代,拥有强大财力支持的组织机构将计算时间缩减到以小时乃至分钟计并非天方夜谭。作为例子,去免费开源数学软件系统 <a href=\"https://sagecell.sagemath.org\">SageMath 的在线工具网站</a>,输入以下前5行Sage Python代码:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">p=random_prime(<span class=\"number\">2</span>**<span class=\"number\">120</span>)</span><br><span class=\"line\">q=random_prime(<span class=\"number\">2</span>**<span class=\"number\">120</span>)</span><br><span class=\"line\">n=p*q</span><br><span class=\"line\"><span class=\"built_in\">print</span>(n)</span><br><span class=\"line\">factor(n)</span><br><span class=\"line\"><span class=\"comment\"># 输出结果</span></span><br><span class=\"line\"><span class=\"number\">28912520751034191277571809785701738245635791077300278534278526509273423</span></span><br><span class=\"line\"><span class=\"number\">38293227899687810929829874029597363</span> * <span class=\"number\">755029605411506802434801930237797621</span></span><br></pre></td></tr></table></figure>\n<p>在几分钟之内就得到了结果,一个72位十进制数字 (240比特) 的大数被分解出来了。要知道,在1977年的RSA论文里,提到分解一个75位十进制数字大约需要104天。人类的技术进步是如此惊人!</p>\n<p>当攻方的矛越来越锋利时,守方的盾就必须越来越厚重。所以,1024比特RSA已经不安全,应用系统不应该使用少于2048比特的公钥 <span class=\"math inline\">\\(N\\)</span>值。而当需要高安全性时,选择4096比特RSA。</p>\n<h2 id=\"初级攻击\">初级攻击</h2>\n<p>虽然大数分解是所有人都知道的攻击方方式,但是在RSA的应用中常见的一些低级错误所造成的安全漏洞,使得使用简单的攻击手段就可以得逞,下面对一些典型的初级攻击方式加以说明。</p>\n<ul>\n<li><p>在RSA的发展初期,基于当时计算能力的落后,找寻大的素数需要不少时间。因此,一些系统实现试图共用模数<span class=\"math inline\">\\(N\\)</span>。想法是只生成一组<span class=\"math inline\">\\((p,q)\\)</span>,然后所有的用户使用同样的<span class=\"math inline\">\\(N=pq\\)</span>值,由大家都信任的中心机构为每个用户 <span class=\"math inline\">\\(i\\)</span> 分配密钥对<span class=\"math inline\">\\((e_i,d_i)\\)</span>,只要各自的私钥 <span class=\"math inline\">\\(d_i\\)</span>保存好就不会出问题。很不幸,这是一个灾难性的错误!这种实现方案有两个巨大的安全漏洞:</p>\n<ol type=\"1\">\n<li><p>用户 <span class=\"math inline\">\\(i\\)</span> 可以用自己的密钥对<span class=\"math inline\">\\((e_i,d_i)\\)</span>分解出<span class=\"math inline\">\\(N\\)</span>。无论<span class=\"math inline\">\\(d\\)</span>是使用欧拉函数<span class=\"math inline\">\\(\\varphi(N)\\)</span>还是卡迈克尔函数<span class=\"math inline\">\\(\\lambda(N)\\)</span>生成的,都有很快地从给定的<span class=\"math inline\">\\(d\\)</span>推导出素因数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>的算法<a href=\"#fn2\" class=\"footnote-ref\" id=\"fnref2\" role=\"doc-noteref\"><sup>2</sup></a>。而一旦知晓了<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>,用户 <span class=\"math inline\">\\(i\\)</span> 可以用任何其他用户的公钥<span class=\"math inline\">\\((N,e_j)\\)</span>计算出其私钥<span class=\"math inline\">\\(d_j\\)</span>。至此,所有其他用户对用户 <span class=\"math inline\">\\(i\\)</span> 毫无秘密可言。</p></li>\n<li><p>即使所有用户都没有知识和技能去分解<span class=\"math inline\">\\(N\\)</span>,或者“好心”地不去了解其他用户的私钥,黑客还是可以实施<mark>共模攻击</mark>破解用户的消息。设定两个用户爱丽丝和鲍勃的公钥分别为<span class=\"math inline\">\\(e_1\\)</span>和<span class=\"math inline\">\\(e_2\\)</span>,而且<span class=\"math inline\">\\(e_1\\)</span>和<span class=\"math inline\">\\(e_2\\)</span>恰好互素 (这是非常可能的),那么根据<a href=\"https://zh.wikipedia.org/zh-cn/貝祖等式\">裴蜀定理</a> (Bézout's identity),窃听者伊芙可以找到 <span class=\"math inline\">\\(s\\)</span> 和 <span class=\"math inline\">\\(t\\)</span> 满足:<span class=\"math display\">\\[e_{1}s+e_{2}t=\\mathrm{gcd}(e_1,e_2)=1\\]</span>这时如果有人给爱丽丝和鲍勃发出同样的消息<span class=\"math inline\">\\(m\\)</span>,伊芙记录两段密文<span class=\"math inline\">\\(c_1\\)</span>和<span class=\"math inline\">\\(c_2\\)</span>后,执行下面的运算就可以解密出<span class=\"math inline\">\\(m\\)</span>:<span class=\"math display\">\\[c_1^s⋅c_2^t\\equiv(m^{e_1})^s⋅(m^{e_2})^t\\equiv m^{e_{1}s+e_{2}t}\\equiv m\\pmod N\\]</span>对应的Python函数代码如下:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">common_modulus</span>(<span class=\"params\">e1, e2, N, c1, c2</span>):</span></span><br><span class=\"line\"> <span class=\"comment\"># 调用扩展的欧几里得算法函数</span></span><br><span class=\"line\"> g, s, t = gymp2.gcdext(e1, e2)</span><br><span class=\"line\"> <span class=\"keyword\">assert</span> g == <span class=\"number\">1</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> s < <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"comment\"># 求c1的模逆元</span></span><br><span class=\"line\"> re = <span class=\"built_in\">int</span>(gmpy2.invert(c1, N))</span><br><span class=\"line\"> c1 = <span class=\"built_in\">pow</span>(re, s*(-<span class=\"number\">1</span>), N)</span><br><span class=\"line\"> c2 = <span class=\"built_in\">pow</span>(c2, t, N)</span><br><span class=\"line\"> <span class=\"keyword\">else</span>:</span><br><span class=\"line\"> <span class=\"comment\"># t为负数, 求c2的模逆元</span></span><br><span class=\"line\"> re = <span class=\"built_in\">int</span>(gmpy2.invert(c2, N))</span><br><span class=\"line\"> c2 = <span class=\"built_in\">pow</span>(re, t*(-<span class=\"number\">1</span>), N)</span><br><span class=\"line\"> c1 = <span class=\"built_in\">pow</span>(c1, a, N)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (c1*c2) % N</span><br></pre></td></tr></table></figure> 这里调用了gmpy2<a href=\"#fn3\" class=\"footnote-ref\" id=\"fnref3\" role=\"doc-noteref\"><sup>3</sup></a>两个库函数:gcdext()实现扩展的欧几里得算法,invert()求模逆元。注意,Python的指数函数pow()支持模幂运算,但是指数不可为负数。因为<span class=\"math inline\">\\(s\\)</span>或<span class=\"math inline\">\\(t\\)</span>必然有一个为负数,所以要先调用invert()将<span class=\"math inline\">\\(c_1\\)</span>或<span class=\"math inline\">\\(c_2\\)</span>转化为对应的模逆元,再将负数取反求模幂。比如上面第7、8行就实现了<span class=\"math inline\">\\(c_1^s=(c_1^{-1})^{-s}\\bmod N\\)</span>。</p></li>\n</ol></li>\n<li><p>共用模数<span class=\"math inline\">\\(N\\)</span>被证明是不安全的,那么只重复使用<span class=\"math inline\">\\(p\\)</span>或<span class=\"math inline\">\\(q\\)</span>可以吗?这样似乎避免了共模攻击又保证每个用户的公钥<span class=\"math inline\">\\(N\\)</span>值唯一。大错特错了!这是一个更糟糕的主意!攻击者拿到所有用户的公开<span class=\"math inline\">\\(N\\)</span>值,只要两两组合<span class=\"math inline\">\\((N_1,N_2)\\)</span>执行欧几里得算法 (辗转相除法) 求解最大公约数,求解成功就得到一个素因数<span class=\"math inline\">\\(p\\)</span>,再简单地用除法就得出另一个素因数<span class=\"math inline\">\\(q\\)</span>。有了<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>,攻击者就可以马上算出用户的私钥<span class=\"math inline\">\\(d\\)</span>。这就是<mark>模不互素攻击</mark>。</p></li>\n<li><p>应用教科书RSA时,如果公钥指数<span class=\"math inline\">\\(e\\)</span>和明文<span class=\"math inline\">\\(m\\)</span>都很小,乃至<span class=\"math inline\">\\(c=m^e<N\\)</span>,直接对密文<span class=\"math inline\">\\(c\\)</span>开<span class=\"math inline\">\\(e\\)</span>次方即可得到明文<span class=\"math inline\">\\(m\\)</span>。即使<span class=\"math inline\">\\(m^e>N\\)</span>但不是足够大,则因为 <span class=\"math inline\">\\(m^e=c+k⋅N\\)</span>,可以循环尝试小的<span class=\"math inline\">\\(k\\)</span>值进行<mark>暴力开方破解</mark>。下面是Python例程:</p>\n<p><figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">crack_small</span>(<span class=\"params\">c, e, N, repeat</span>)</span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"title\">times</span> = 0</span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"title\">msg</span> = 0</span></span><br><span class=\"line\"><span class=\"function\"> <span class=\"title\">for</span> <span class=\"title\">k</span> <span class=\"title\">in</span> <span class=\"title\">range</span>(<span class=\"params\">repeat</span>):</span></span><br><span class=\"line\"> m, is_exact = gmpy2.iroot(c + times, e)</span><br><span class=\"line\"> <span class=\"keyword\">if</span> is_exact <span class=\"keyword\">and</span> <span class=\"built_in\">pow</span>(m, e, N) == c:</span><br><span class=\"line\"> msg = <span class=\"built_in\">int</span>(m)</span><br><span class=\"line\"> <span class=\"keyword\">break</span></span><br><span class=\"line\"> times += N</span><br><span class=\"line\"> <span class=\"keyword\">return</span> msg</span><br></pre></td></tr></table></figure> 这里调用了gmpy2库函数iroot(),以求得<span class=\"math inline\">\\(e\\)</span>次方根。</p></li>\n<li><p>教科书RSA是<strong>确定性</strong>的,意即同样的明文<span class=\"math inline\">\\(m\\)</span>总是生成同样的密文<span class=\"math inline\">\\(c\\)</span>。这就使<mark>密码本攻击</mark>成为可能:攻击者预先计算出全部或部分的 <span class=\"math inline\">\\(m\\to c\\)</span> 对照表保存,然后搜索截获的密文匹配即可。确定性也意味着教科书RSA不是语义安全的,密文会泄露明文的某些信息。密文重复出现,表明发送方在重复发送相同的消息。</p></li>\n<li><p>教科书RSA具有<strong>延展性</strong> (malleable),对密文进行特定形式的代数运算,结果会反映到解密的明文中。比如有两段明文 <span class=\"math inline\">\\(m_1\\)</span>和 <span class=\"math inline\">\\(m_2\\)</span>,加密后产生 <span class=\"math inline\">\\(c_1=m_1^e\\bmod N\\)</span> 和 <span class=\"math inline\">\\(c_2=m_2^e\\bmod N\\)</span>,那么 <span class=\"math inline\">\\((c_1⋅c_2)\\)</span> 解密会得到什么?看如下等式: <span class=\"math display\">\\[(c_1⋅c_2)^d\\equiv m_1^{ed}⋅m_2^{ed}\\equiv m_1⋅m_2\\pmod N\\]</span> 所以两段密文的乘积解密后得到的明文,等于两段明文的乘积。这一特性对一般的RSA加密系统是有害的,它为<mark>选择密文攻击</mark> (chosen-ciphertext attack) 提供了机会。下面举例两种攻击场景:</p>\n<ol type=\"1\">\n<li><p>设想有一个RSA解密机可以用内部保存的私钥<span class=\"math inline\">\\((N,d)\\)</span>解密消息。基于安全考虑,解密机会拒绝同样的密文重复输入。攻击者马文发现一段密文<span class=\"math inline\">\\(c\\)</span>,直接输入到解密机被拒绝,因为密文<span class=\"math inline\">\\(c\\)</span>以前被解密过。马文找到一种办法破解。他自己准备一段明文<span class=\"math inline\">\\(r\\)</span>,用公钥<span class=\"math inline\">\\((N,e)\\)</span>加密生成新的密文<span class=\"math inline\">\\(c'={r^e}c\\bmod N\\)</span>,然后将密文<span class=\"math inline\">\\(c'\\)</span>输入到解密机。解密机没有解过这一段新的密文,所以不会拒绝。解密的结果是<span class=\"math display\">\\[m'\\equiv (c')^d\\equiv r^{ed}c^d\\equiv rm\\pmod N\\]</span>现在马文有了<span class=\"math inline\">\\(m'\\)</span>,他用公式 <span class=\"math inline\">\\(m\\equiv m'r^{-1}\\pmod N\\)</span>就可以计算出<span class=\"math inline\">\\(m\\)</span>。</p></li>\n<li><p>假设马文想让鲍勃在一段消息<span class=\"math inline\">\\(m\\)</span>上签名,但是鲍勃在看过消息内容后拒绝了。马文可以使用称为<mark>盲签名</mark><a href=\"#fn4\" class=\"footnote-ref\" id=\"fnref4\" role=\"doc-noteref\"><sup>4</sup></a>的攻击手段可以达成他的目标。他挑选一段随机的消息<span class=\"math inline\">\\(r\\)</span>,生成 <span class=\"math inline\">\\(m'={r^e}m\\bmod N\\)</span>,然后把<span class=\"math inline\">\\(m'\\)</span>拿给鲍勃签名。鲍勃可能觉得<span class=\"math inline\">\\(m'\\)</span>无关紧要,就签了。鲍勃签名的结果是 <span class=\"math inline\">\\(s'=(m')^d\\bmod N\\)</span>。现在马文用公式 <span class=\"math inline\">\\(s=s'r^{-1}\\bmod N\\)</span> 就拿到了鲍勃对原来消息<span class=\"math inline\">\\(m\\)</span>的签名。为什么?原因是<span class=\"math display\">\\[s^e\\equiv (s')^er^{-e}\\equiv (m')^{ed}r^{-e}\\equiv m'r^{-e}\\equiv m\\pmod N\\]</span></p></li>\n</ol></li>\n</ul>\n<p>以上这些绝非完整的初级攻击方法清单,但已经可以说明问题了。在实际RSA应用中,我们必须非常小心,应该做到:</p>\n<ul>\n<li>单独给每个用户生成唯一的公钥模数<span class=\"math inline\">\\(N\\)</span>,防止共模攻击</li>\n<li>不可重复使用素因数生成公钥模数<span class=\"math inline\">\\(N\\)</span>,杜绝模不互素攻击</li>\n</ul>\n<p>对于教科书RSA的确定性和延展性缺陷,及可能的暴力开方破解漏洞,可采用随机元素填充 (padding with random elements) 方法防护,其机理是:</p>\n<ul>\n<li>填充可确保被加密的消息数值比特数接近<span class=\"math inline\">\\(N\\)</span>,同时不使用小的<span class=\"math inline\">\\(e\\)</span>值,让暴力开方破解失效</li>\n<li>随机填充使得同样的明文产生不一样的密文,保障语义安全性,使得密码本攻击成为不可能</li>\n<li>随机填充也能防止共模攻击,因为被加密的消息不同,依据裴蜀定理的攻击无效</li>\n<li>严格格式定义的填充破坏了延展性,降低了选择密文攻击可能性。比如填充后开头几个字节必须为给定值,那对相应的密文进行代数运算后,解密出的数据极大可能不符合预定的格式,这就瓦解了选择密文攻击</li>\n</ul>\n<h2 id=\"低公钥指数攻击\">低公钥指数攻击</h2>\n<p>使用小的公钥指数是危险的,在不填充或不当填充的情况下,即使暴力开方破解不能成功,也还有一些高级的攻击手段。</p>\n<h3 id=\"广播攻击\">广播攻击</h3>\n<p>由瑞典理论计算科学家约翰·霍斯塔德<a href=\"#fn5\" class=\"footnote-ref\" id=\"fnref5\" role=\"doc-noteref\"><sup>5</sup></a>发现,因此也被称为<mark>霍斯塔德广播攻击</mark>。考虑这样一种简化的场景,假设爱丽丝需要发送同一条消息<span class=\"math inline\">\\(m\\)</span>给鲍伯、卡罗尔和戴夫。三位接收者的公钥分别为<span class=\"math inline\">\\((N_1,3)\\)</span>、<span class=\"math inline\">\\((N_2,3)\\)</span>和<span class=\"math inline\">\\((N_3,3)\\)</span>,即公钥指数都为3,公钥模数各不相同。消息不填充,爱丽丝直接用其他三人的公钥加密并发出三段密文<span class=\"math inline\">\\(c_1,c_2,c_3\\)</span>: <span class=\"math display\">\\[\\begin{cases}\nc_1=m^3\\bmod N_1\\\\\nc_2=m^3\\bmod N_2\\\\\nc_3=m^3\\bmod N_3\n\\end{cases}\\]</span> 这时伊芙偷偷记下三段密文,标记<span class=\"math inline\">\\(M=m^3\\)</span>,如果她可以恢复<span class=\"math inline\">\\(M\\)</span>,开三次方根自然就得到明文<span class=\"math inline\">\\(m\\)</span>。显然这里共模攻击不成立,我们也可以假设模数两两互素,否则使用模不互素攻击分解模数就可以了。那么伊芙有办法算出<span class=\"math inline\">\\(M\\)</span>吗?答案是肯定的。</p>\n<p>实际上,这里求解<span class=\"math inline\">\\(M\\)</span>的等同问题是:已知某个数分别除以三个数的余数,而且这三个数两两互素,有没有有效的算法解出这个数?这个有效的算法就是<a href=\"https://zh.wikipedia.org/zh-cn/中国剩余定理\"><strong>中国余数定理</strong></a>!</p>\n<p>中国余数定理给出了一元线性同余方程组有解的准则以及求解方法。对于以下的一元线性同余方程组 (注意不要与上面描述攻击场景的数学符号混淆): <span class=\"math display\">\\[(S) : \\quad \\left\\{ \n\\begin{matrix} x \\equiv a_1 \\pmod {m_1} \\\\\nx \\equiv a_2 \\pmod {m_2} \\\\\n\\vdots \\qquad\\qquad\\qquad \\\\\nx \\equiv a_n \\pmod {m_n} \\end\n{matrix} \\right.\\]</span> 假设整数<span class=\"math inline\">\\(m_1,m_2,\\ldots,m_n\\)</span>其中任两数互素,则对任意的整数<span class=\"math inline\">\\(a_1,a_2,\\ldots,a_n\\)</span>,方程组<span class=\"math inline\">\\((S)\\)</span>有解,并且通解可以用如下四步构造: <span class=\"math display\">\\[\\begin{align}\nM &= m_1 \\times m_2 \\times \\cdots \\times m_n = \\prod_{i=1}^n m_i \\tag{1}\\label{eq1}\\\\\nM_i &= M/m_i, \\; \\; \\forall i \\in \\{1, 2, \\cdots , n\\}\\tag{2}\\label{eq2}\\\\\nt_i M_i &\\equiv 1\\pmod {m_i}, \\; \\; \\forall i \\in \\{1, 2, \\cdots , n\\}\\tag{3}\\label{eq3}\\\\\nx &=kM+\\sum_{i=1}^n a_i t_i M_i\\tag{4}\\label{eq4}\n\\end{align}\\]</span> 以上最后一行 (4) 式给出了方程组的通解形式。在模<span class=\"math inline\">\\(M\\)</span>的意义下,唯一解为 <span class=\"math inline\">\\(\\sum_{i=1}^n a_i t_i M_i \\bmod M\\)</span>。</p>\n<details class=\"note primary\"><summary><p>试试用中国剩余定理解出本文开头的“物不知其数”问题</p>\n</summary>\n<p>首先将变量符号与数值对应: <span class=\"math display\">\\[m_1=3,a_1=2;\\quad m_2=5,a_2=3;\\quad m_3=7,a_3=2\\]</span> 然后算出<span class=\"math inline\">\\(M=3\\times5\\times7=105\\)</span>,进而推导出: <span class=\"math display\">\\[\\begin{align}\nM_1 &=M/m_1=105/3=35,\\quad t_1=35^{-1}\\bmod 3 = 2\\\\\nM_2 &=M/m_2=105/5=21,\\quad t_2=21^{-1}\\bmod 5 = 1\\\\\nM_3 &=M/m_3=105/7=15,\\quad t_3=15^{-1}\\bmod 7 = 1\\\\\n\\end{align}\\]</span> 最后带入通解公式: <span class=\"math display\">\\[x=k⋅105+(2⋅35⋅2+3⋅21⋅1+2⋅15⋅1)=k⋅105+233\\]</span> 所以在模105内的最小正整数解为 <span class=\"math inline\">\\(233\\bmod 105=23\\)</span>。</p>\n<p>明朝数学家程大位在《算法统宗》中,将宋朝数学家秦九韶记载在《数书九章》中的解法编成易于上口的《孙子歌诀》:<em>三人同行七十希,五树梅花廿一支,七子团圆正半月,除百零五便得知。</em></p>\n<p>这里我们必须佩服中国古人的智慧,在没有现代数学符号系统的条件下,能够推导总结出如此巧妙的解法,为人类贡献了一个重要的数学定理。</p>\n\n</details>\n<p>所以伊芙只要套用中国余数定理的解法,算出<span class=\"math inline\">\\(M\\)</span>,然后求得其立方根就得到了明文<span class=\"math inline\">\\(m\\)</span>,攻击成功。更一般地,设定接收者的数目为<span class=\"math inline\">\\(k\\)</span>,如果全部接收者使用同样的<span class=\"math inline\">\\(e\\)</span>,那么只要<span class=\"math inline\">\\(k\\ge e\\)</span>,这种广播攻击都是可行的。</p>\n<p>霍斯塔德还进一步证明,即便使用填充来预防广播攻击,如果填充方案产生的消息相互之间有线性关系,比如用公式<span class=\"math inline\">\\(m_i=i2^b+m\\)</span> (<span class=\"math inline\">\\(b\\)</span>为<span class=\"math inline\">\\(m\\)</span>的比特数)生成发给接收者 <span class=\"math inline\">\\(i\\)</span> 的消息,那么只要<span class=\"math inline\">\\(k>e\\)</span>,广播攻击仍然可以恢复明文<span class=\"math inline\">\\(m\\)</span>。这种情况下的广播攻击还是基于中国余数定理,但具体的破解方法有赖于线性关系的信息。</p>\n<p>总结以上的分析,为了避免广播攻击,我们必须使用大一些的公钥指数<span class=\"math inline\">\\(e\\)</span>,同时应用随机填充。现在通用的公钥指数<span class=\"math inline\">\\(e\\)</span>为65537 (<span class=\"math inline\">\\(2^{16}+1\\)</span>),可以兼顾消息加密或签名验证运算的效率和安全性。</p>\n<p>最后,给出仿真广播攻击的Python例程:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">solve_crt</span>(<span class=\"params\">ai: <span class=\"built_in\">list</span>, mi: <span class=\"built_in\">list</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''中国余数定理求解函数,参考https://zh.wikipedia.org/zh-cn/中国剩余定理</span></span><br><span class=\"line\"><span class=\"string\"> mi,ai分别表示模数和取模后的余数,都为列表结构。函数工作的前提是mi两两互素'''</span></span><br><span class=\"line\"> M = reduce(<span class=\"keyword\">lambda</span> x, y: x * y, mi)</span><br><span class=\"line\"> ti = [a * (M//m) * <span class=\"built_in\">int</span>(gmpy2.invert(M//m, m)) <span class=\"keyword\">for</span> (m, a) <span class=\"keyword\">in</span> <span class=\"built_in\">zip</span>(mi, ai)]</span><br><span class=\"line\"> <span class=\"keyword\">return</span> reduce(<span class=\"keyword\">lambda</span> x, y: x + y, ti) % M</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">rsa_broadcast_attack</span>(<span class=\"params\">ctexts: <span class=\"built_in\">list</span>, moduli: <span class=\"built_in\">list</span></span>):</span></span><br><span class=\"line\"> <span class=\"string\">'''RSA广播攻击:应用中国余数定理破解e=3'''</span></span><br><span class=\"line\"> c0, c1, c2 = ctexts[<span class=\"number\">0</span>], ctexts[<span class=\"number\">1</span>], ctexts[<span class=\"number\">2</span>]</span><br><span class=\"line\"> n0, n1, n2 = moduli[<span class=\"number\">0</span>], moduli[<span class=\"number\">1</span>], moduli[<span class=\"number\">2</span>]</span><br><span class=\"line\"> m0, m1, m2 = n1 * n2, n0 * n2, n0 * n1</span><br><span class=\"line\"> t0 = (c0 * m0 * <span class=\"built_in\">int</span>(gmpy2.invert(m0, n0)))</span><br><span class=\"line\"> t1 = (c1 * m1 * <span class=\"built_in\">int</span>(gmpy2.invert(m1, n1)))</span><br><span class=\"line\"> t2 = (c2 * m2 * <span class=\"built_in\">int</span>(gmpy2.invert(m2, n2)))</span><br><span class=\"line\"> c = (t0 + t1 + t2) % (n0 * n1 * n2)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">int</span>(gmpy2.iroot(c, <span class=\"number\">3</span>)[<span class=\"number\">0</span>])</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">uint_to_bytes</span>(<span class=\"params\">x: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">bytes</span>:</span></span><br><span class=\"line\"> <span class=\"string\">'''转换无符号整数到字节数组'''</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> x == <span class=\"number\">0</span>:</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">bytes</span>(<span class=\"number\">1</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> x.to_bytes((x.bit_length() + <span class=\"number\">7</span>) // <span class=\"number\">8</span>, <span class=\"string\">'big'</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">quote = <span class=\"string\">b'The cosmos is within us. We are made of star stuff. - Carl Sagan'</span></span><br><span class=\"line\">bob = RSA(<span class=\"number\">1024</span>, <span class=\"number\">3</span>)</span><br><span class=\"line\">carol = RSA(<span class=\"number\">1024</span>, <span class=\"number\">3</span>)</span><br><span class=\"line\">dave = RSA(<span class=\"number\">1024</span>, <span class=\"number\">3</span>)</span><br><span class=\"line\">cipher_list = [bob.encrypt(quote), carol.encrypt(quote), dave.encrypt(quote)]</span><br><span class=\"line\">modulus_list = [bob.n, carol.n, dave.n]</span><br><span class=\"line\"></span><br><span class=\"line\">cracked_cipher = solve_crt(cipher_list, modulus_list)</span><br><span class=\"line\">cracked_int = <span class=\"built_in\">int</span>(gmpy2.iroot(cracked_cipher, <span class=\"number\">3</span>)[<span class=\"number\">0</span>])</span><br><span class=\"line\"><span class=\"keyword\">assert</span> cracked_int == rsa_broadcast_attack(cipher_list, modulus_list)</span><br><span class=\"line\"></span><br><span class=\"line\">hacked_quote = uint_to_bytes(cracked_int)</span><br><span class=\"line\"><span class=\"keyword\">assert</span> hacked_quote == quote</span><br></pre></td></tr></table></figure>\n<p>程序使用了两种方法仿真广播攻击。一种调用通用的中国余数定理求解函数<code>solve_crt()</code>,再将结果开立方根;另一种调用专门的针对公钥指数<span class=\"math inline\">\\(e=3\\)</span>的广播攻击函数<code>rsa_broadcast_attack()</code>,直接输出破解的明文数值。这两个函数内部实现都是基于中国余数定理的通解公式,输出结果应该完全一致。破解的明文数值再输入到<code>uint_to_bytes()</code>函数,转换成字节数组与原文<code>quote</code>比较。注意,程序中使用了RSA类生成的对象模拟接收者鲍伯、卡罗尔和戴夫,鉴于篇幅所限RSA类的实现代码这里省略了。</p>\n<blockquote>\n<p>下篇:<a href=\"https://www.packetmania.net/2023/11/13/RSA-attack-defense-2/\">RSA的攻与防(二)</a></p>\n</blockquote>\n<section class=\"footnotes\" role=\"doc-endnotes\">\n<hr />\n<ol>\n<li id=\"fn1\" role=\"doc-endnote\"><p>美国计算机科学家和安全技术专家加里·麦格劳(Gary McGraw)对软件开发者的一句著名忠告是 “<a href=\"http://web.archive.org/web/20030629085904/http://www-106.ibm.com/developerworks/library/s-everything.html#author1\">never roll your own cryptography</a>”<a href=\"#fnref1\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn2\" role=\"doc-endnote\"><p>RSA原始论文(第IX节C部分)就提到了由已知 <span class=\"math inline\">\\(d\\)</span> 分解 <span class=\"math inline\">\\(N\\)</span>的<a href=\"https://www.cs.cmu.edu/~glmiller/Publications/Papers/Mi76.pdf\">米勒算法</a>。这一算法同样适用于由卡迈克尔函数<span class=\"math inline\">\\(\\lambda(N)\\)</span>生成的 <span class=\"math inline\">\\(d\\)</span>。<a href=\"#fnref2\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn3\" role=\"doc-endnote\"><p>gmpy2是一个用C语言写的Python扩展模块,支持多精度算术。<a href=\"#fnref3\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn4\" role=\"doc-endnote\"><p>在一些特殊情况下,盲签名可用来有效地保护隐私。比如电子选举和数字现金应用中,签名者和消息作者可以是不同的。<a href=\"#fnref4\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n<li id=\"fn5\" role=\"doc-endnote\"><p>约翰·霍斯塔德(Johan Håstad),瑞典理论计算科学家,皇家理工学院教授,美国数学学会(AMS)和计算机协会(ACM)双会士。<a href=\"#fnref5\" class=\"footnote-back\" role=\"doc-backlink\">↩︎</a></p></li>\n</ol>\n</section>\n","categories":["技术小札"],"tags":["密码学","网络安全","Python编程"]},{"title":"请马上停止使用TLS 1.0和TLS 1.1!","url":"/2022/05/01/Stop-TLS1-0-TLS1-1/","content":"<p>2021年3月,互联网工程任务组(IETF)发布分类为当前最佳实践的<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>,正式宣布弃用TLS 1.0和 TLS 1.1协议。如果你的应用程序和网站服务还在使用这些协议,请马上停止并立即更新到TLS 1.2或TLS 1.3协议版本,以消除可能存在的安全隐患。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>One single vulnerability is all an attacker needs.</strong><br> <strong>— <em>Window Snyder</em>(窗·斯奈德,美国计算机安全专家,曾任微软、苹果、英特尔等公司的顶级安全策略师及首席软件安全官等职务)</strong></p>\n</div>\n<h3 id=\"rfc解读\">RFC解读</h3>\n<p><a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>的文档标题非常直接,就是“弃用TLS 1.0和 TLS 1.1”。那么它给出的理据是什么?下面来做一个简单的解读。</p>\n<p>首先,看看其摘要的中文译文:</p>\n<blockquote>\n<p>本文档正式弃用传输层安全性协议(Transport Layer Security,简写TLS)1.0(RFC 2246)和1.1(RFC 4346)版本。 相应地,那些RFC文档已被移至历史状态。 这些协议版本缺乏对当前推荐的加密算法和机制的支持,而且现在许多政府和行业都要求使用TLS的各种应用避开这些旧的TLS版本。</p>\n<p>TLS 1.2版本在2008年成为IETF协议的推荐版本(随后在2018年被TLS 1.3版本所替代),这为从旧版本过渡提供了足够的时间。从应用实现中移除对旧版本的支持可以减少攻击面、降低错误配置的机会和提高库和产品的维护效率。</p>\n<p>本文档还废除了数据报传输层安全性协议(Datagram TLS)1.0版本(RFC 4347),但DTLS 1.2版本仍然有效。DTLS 1.1版本并不存在。</p>\n<p>如文中所述,本文档更新了许多规范性地引用TLS 1.0或1.1版本的RFC。本文档也更新了RFC 7525中关于TLS使用的最佳实践。因此,它是BCP 195的一部分。</p>\n</blockquote>\n<p>这里给出的信息很明确,弃用它们的原因完全是技术上的。TLS 1.0和TLS 1.1不能支持更强的加密算法和机制,无法胜任新时代各类网络应用的高安全需求。TLS是基于TCP的,对应于基于UDP的DTLS协议,<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>也宣布弃用DTLS 1.0协议。</p>\n<p>正文的序言部分列出了技术原因的一些细节:</p>\n<ol type=\"1\">\n<li>这些旧版本要求实现一些过时的密码套件,而这些密码套件已经被密码学认定不再可取。比如TLS 1.0强制必须支持TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA。</li>\n<li>缺乏对当前推荐的密码套件的支持,特别是<a href=\"https://en.wikipedia.org/wiki/Authenticated_encryption\">带有关联数据的认证加密(AEAD)</a>算法。TLS对它们的支持从1.2版本才开始。</li>\n<li>握手过程的完整性取决于SHA-1散列值。</li>\n<li>对对方真实性的认证取决于SHA-1签名。</li>\n<li>支持四个版本的TLS协议使得错误配置的机会上升。</li>\n<li>至少一个普遍使用的编程库正计划在即将发布的新版本中放弃对TLS 1.0和TLS 1.1的支持。</li>\n</ol>\n<p>上述的第5、6条很清楚,无需进一步解释。</p>\n<p>对第1条提到的3DES,虽然它使用总长168位的三个独立密钥,但是考虑到可能的<a href=\"https://en.wikipedia.org/wiki/Meet-in-the-middle_attack\">中途相遇攻击</a>,它的有效安全性仅为112位。此外,3DES的加密块长度仍然是64位,这使得它在<a href=\"https://en.wikipedia.org/wiki/Birthday_attack\">生日攻击</a>面前显得异常脆弱(参见<a href=\"https://sweet32.info/\">Sweet32</a>)。NIST规定单个3DES密钥组只能用于加密<span class=\"math inline\">\\(2^{20}\\)</span>个数据块(即8MB)。这当然太小了,最终NIST于2017决定在IPSec和TLS的协议中弃用3DES。</p>\n<p>3DES只是一个例子,另一大类早先就被淘汰的就是使用RC4流密码的密码套件,详情可见<a href=\"https://tools.ietf.org/html/rfc7465\">RFC 7465</a>。此外,还有块密码CBC模式实现中的种种问题,也常常被攻击者利用破解TLS会话。对TLS 1.0和TLS 1.1的各类攻击及应对措施的总结,在<a href=\"https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf\">NIST800-52r2</a>和<a href=\"https://tools.ietf.org/html/rfc7457\">RFC7457</a>中有详细的阐述。这两个参考文档为弃用提供了关键理据。显然,任何强制要求实现不安全密码套件的协议都应该被列入被淘汰的清单中。</p>\n<p>在文档的第2部分,更是直接引用了<a href=\"https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-52r2.pdf\">NIST800-52r2</a>的1.1节“TLS的历史”中的内容(简略如下表所示):</p>\n<table>\n<colgroup>\n<col style=\"width: 36%\" />\n<col style=\"width: 36%\" />\n<col style=\"width: 28%\" />\n</colgroup>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">TLS版本</th>\n<th style=\"text-align: center;\">协议文档</th>\n<th style=\"text-align: left;\">关键特征更新</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">1.1</td>\n<td style=\"text-align: center;\"><a href=\"https://tools.ietf.org/html/rfc4346\">RFC 4346</a></td>\n<td style=\"text-align: left;\">改进初始化向量选择和填充差错处理的过程,以修补TLS 1.0中发现的CBC模式下的种种缺陷。</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">1.2</td>\n<td style=\"text-align: center;\"><a href=\"https://tools.ietf.org/html/rfc5246\">RFC 5246</a></td>\n<td style=\"text-align: left;\">改进的加密算法,特别在散列函数方面支持SHA-2系列算法来进行散列、MAC和伪随机函数计算,增加了AEAD密码套件</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">1.3</td>\n<td style=\"text-align: center;\"><a href=\"https://tools.ietf.org/html/rfc8446\">RFC 8446</a></td>\n<td style=\"text-align: left;\">TLS的重大改变,旨在清除多年来出现的众多威胁。变化包括新的握手协议、新的使用基于HMAC的提取和扩展密钥派生函数(HKDF)的密钥派生过程,以及删除各种使用RSA密钥传输、静态迪菲—赫尔曼密钥交换、CBC操作模式或SHA-1的密码套件。</td>\n</tr>\n</tbody>\n</table>\n<p>AEAD是一种能够同时保证数据的保密性、完整性和真实性的加密模式,典型的如<a href=\"https://en.wikipedia.org/wiki/CCM_mode\">CCM</a>和<a href=\"https://en.wikipedia.org/wiki/Galois/Counter_Mode\">GCM</a>等。TLS 1.2引入了一系列AEAD密码套件,而它的高安全性使得其成为TLS 1.3的排他性选择。这些注释了技术原因的第2条。</p>\n<p>技术原因的第3、4条点名SHA-1,那么SHA-1到底有什么问题?文档的第3部分引用了两名法国的研究者<a href=\"https://www.mitls.org/downloads/transcript-collisions.pdf\">Karthikeyan Bhargavan和Gaetan Leurent的论文</a>做出了解答。</p>\n<p>作为一种密码散列函数,SHA-1由美国国家安全局(NSA)设计,而后被美国国家标准技术研究所(NIST)发布为联邦信息处理标准(FIPS)。SHA-1可将最大不超过<span class=\"math inline\">\\(2^{64}\\)</span>比特的消息生成一个160位(20字节)散列值,即消息摘要。由此,基于<a href=\"https://en.wikipedia.org/wiki/Birthday_attack\">生日攻击</a>的暴力破解需要复杂度为<span class=\"math inline\">\\(2^{80}\\)</span>的操作。2005年,中国密码学家王小云及其研究团队在此领域获得突破。她们发表的高效率SHA-1攻击法,能在<span class=\"math inline\">\\(2^{63}\\)</span>个计算复杂度内找到散列碰撞。这给SHA-1的安全性带来巨大的冲击,但这并不表示该破解法已经可以进入实用阶段。</p>\n<p>网络安全协议(如TLS、IKE和SSH等)依赖于密码散列函数的次原像抗性(second preimage resistance),即从计算角度无法找到任何与特定输入值有着相同输出的二次输入值。例如,对于密码散列函数<span class=\"math inline\">\\(h(x)\\)</span>及给定输入<span class=\"math inline\">\\(x\\)</span>,很难找到满足<span class=\"math inline\">\\(h(x) = h(x^′)\\)</span>的次原像<span class=\"math inline\">\\(x^′ ≠ x\\)</span>。因为找到散列碰撞并不意味着就能定位出次原像,所以实践中人们一度认为继续使用SHA-1问题不大。</p>\n<p>然而,2016年Bhargavan和Leurent(就是他们实现了前述的针对64位块密码的Sweet32攻击)发现了一类新的攻击密钥交换协议的方法,打破了这一认知。此方法建立于<a href=\"https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack\">选择前缀碰撞攻击</a>原理之上,即给定两个不同的前缀<span class=\"math inline\">\\(p_1\\)</span>和<span class=\"math inline\">\\(p_2\\)</span>, 找到两个附件<span class=\"math inline\">\\(m_1\\)</span>和<span class=\"math inline\">\\(m_2\\)</span>满足<span class=\"math inline\">\\(h(p_1 ∥ m_1) = hash(p_2 ∥ m_2)\\)</span>。应用此方法,他们演示了针对TLS客户端和服务器的中间人攻击用以实现盗取敏感资料,也证实了攻击可以用来在TLS 1.1、IKEv2和SSH-2会话握手过程中实现伪装和降级。特别地,他们计算出只需要<span class=\"math inline\">\\(2^{77}\\)</span>次操作,就可以破解使用SHA-1或者MD5与SHA-1级联散列值的握手协议。</p>\n<p>由于TLS 1.0和TLS 1.1都不允许会话双方为服务器密钥交换(ServerKeyExchange)消息或证书验证(CertificateVerify)消息选择更强的密码散列函数,IETF确认使用较新协议版本为唯一的升级路径。</p>\n<p>文档的第4、5部分再次明确不得使用TLS 1.0和TLS 1.1、不允许从任何TLS版本协商到TLS 1.0或TLS 1.1。这意味着TLS客户端和服务器各自发出的ClientHello.client_version和ServerHello.server_version不得为{03,01}(TLS 1.0)或{03,02}(TLS 1.1)。如果收到对方发来的Hello消息里的协议版本号为{03,01}或{03,02},本地必须以“protocol_version”告警消息回应并关闭连接。</p>\n<p>值得注意的是,由于历史原因,TLS规范并没有明确客户端发送ClientHello消息时记录层版本号(TLSPlaintext.version)的取值。 所以为了最大限度地提高互操作性,TLS服务器必须接受任何值 {03,XX} (包括 {03,00}) 作为ClientHello消息的记录层版本号,但它们不得协商TLS 1.0或TLS 1.1。</p>\n<p>文档的第6部分声明了对以前发布的<a href=\"https://tools.ietf.org/html/rfc7525\">RFC 7525</a>(TLS和DTLS的安全使用推荐)的文字修订。在该RFC的三个地方将实现时协商TLS 1.0、TLS 1.1和DTLS 1.0从“不应”(SHOULD NOT)改为“不得”(MUST NOT)。最后余下部分是标准的RFC操作和安全考虑总结。</p>\n<h3 id=\"业界动态\">业界动态</h3>\n<p>在大型公共在线服务的业界,GitHub是最先行动的。他们早在2018年2月就开始在所有HTTPS连接中禁用TLS 1.0和TLS 1.1,同时还在SSH连接服务中淘汰了不安全的<code>diffie-hellman-group1-sha1</code>和<code>diffie-hellman-group14-sha1</code>密钥交换算法。2018年8月,Mozilla火狐的首席技术官Eric Rescorla发布TLS 1.3技术规范<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8446</a>。两个月之后,Mozilla就与苹果、谷歌和微软三大巨头一起协同发布声明,将废除TLS 1.0和TLS 1.1提上日程。</p>\n<p>以下是对几个相关知名企业的动态简单汇总:</p>\n<ul>\n<li><strong>微软:</strong>对于Office 365服务,受COVID-19的影响,曾经暂停了对商业客户的TLS 1.0和1.1的禁用。2020年10月15日重新启动了TLS 1.2的强制推广。SharePoint和OneDrive的用户需要更新和配置.NET以支持TLS 1.2。Teams Rooms的用户推荐升级应用程序到4.0.64.0版。Surface Hub在2019年5月就发布了对TLS 1.2的支持。Edge浏览器84版本开始默认不使用TLS 1.0/1.1 ,而Azure云计算服务则于2022年3月31日起永久废弃TLS 1.0/1.1。</li>\n<li><strong>谷歌:</strong>早于2018年,就开始在Chrome 70加入了TLS 1.3的功能。从Chrome 84开始,全面移除对TLS 1.0和TLS 1.1支持。在搜索引擎、Gmail电邮、YouTube和其他各种谷歌服务中运行TLS 1.3一段时间后,于2020年正式推出了TLS 1.3作为所有新的和现有的云CDN和全球负载均衡客户的默认配置。</li>\n<li><strong>苹果:</strong> 2021年9月宣布,在iOS 15、iPadOS 15、macOS 12、watchOS 8及tvOS 15中废止对TLS 1.0和TLS 1.1的支持,后续版本将彻底移除。如果用户的应用程序在所有的连接中都激活App Transport Security(ATS)功能,则无须任何改变。同时通告用户确保网络服务器支持更新的TLS版本,并从应用中删除以下已废弃的<code>Security.framework</code>符号:\n<ul>\n<li>tls_protocol_version_t.TLSv10</li>\n<li>tls_protocol_version_t.TLSv11</li>\n<li>tls_protocol_version_t.DTLSv10</li>\n</ul></li>\n<li><strong>Mozilla:</strong>从火狐浏览器78版开始,默认配置的最低TLS版本为TLS 1.2。在2020年初,Mozilla曾短暂完全去除火狐的TLS 1.0和TLS 1.1,但是这造成了许多用户无法打开一些COVID-19疫情公共信息网站,所以不得不恢复相关的功能。在此之后,Mozilla在其技术支持页提供帮助信息,指导用户自行按需修改默认配置中的最低TLS版本号。</li>\n<li><strong>思科:</strong>Cisco Umbrella(从OpenDNS更名而来)服务在2020年3月31日中止对TLS 1.2之前所有版本的支持。此后,只有兼容TLS 1.2的客户端才能进行连接。在路由器和交换机的产品线,2020年前后基本都实现了网页管理只允许TLS 1.2或后续版本。\n<ul>\n<li>思科的无线接入点(AP)与无线局域网控制器(WLC)之间的CAPWAP连接建立与DTLS之上。从2015年到最近发布的全部802.11ac Wave 2和802.11ax AP都支持DTLS 1.2。AireOS WLC在8.3.11x.0版加入DTLS 1.2功能,而运行IOS-XE的新一代C9800 WLC则一开始就支持DTLS 1.2。注意,因为存在大量使用旧设备和软件版本的既有网络部署,为保护用户投资,目前AP和WLC还不能马上去除对DTLS 1.0的支持。但是,DTLS 1.2已经是AP和WLC默认的最优选择。</li>\n</ul></li>\n</ul>\n<h3 id=\"协议测试\">协议测试</h3>\n<p>不论是TLS/DTLS的客户端还是服务器,都需要测试验证其实现是否遵循<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>的当前最佳实践。</p>\n<h4 id=\"ssl-labs-测试\">SSL Labs 测试</h4>\n<p>Qualys公司源自非商业的<a href=\"https://www.ssllabs.com/index.html\">SSL Labs项目</a>。他们提供免费的简单客户端和服务器测试服务,还有一个<a href=\"https://www.ssllabs.com/ssl-pulse/\">监控面板</a>报告最流行互联网网站的TLS/SSL安全性扫描统计。下面是最近的2022年5月的协议支持统计图表:</p>\n<p><img src=\"SSL-Lab-SSL-TLS.png\" style=\"width:70.0%;height:70.0%\" /></p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">协议版本</th>\n<th style=\"text-align: center;\">安全性</th>\n<th style=\"text-align: center;\">站点支持(2022年1月)</th>\n<th style=\"text-align: center;\">站点支持(2022月5月)</th>\n<th style=\"text-align: center;\">变化</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">SSL 2.0</td>\n<td style=\"text-align: center;\">不安全</td>\n<td style=\"text-align: center;\">486(0.4%)</td>\n<td style=\"text-align: center;\">404(0.3%)</td>\n<td style=\"text-align: center;\">-0.1%</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">SSL 3.0</td>\n<td style=\"text-align: center;\">不安全</td>\n<td style=\"text-align: center;\">3,864(2.8%)</td>\n<td style=\"text-align: center;\">3,434(2.5%)</td>\n<td style=\"text-align: center;\">-0.3%</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">TLS 1.0</td>\n<td style=\"text-align: center;\">弃用</td>\n<td style=\"text-align: center;\">53,520(39.3%)</td>\n<td style=\"text-align: center;\">50,216(37.1)</td>\n<td style=\"text-align: center;\">-2.2%</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">TLS 1.1</td>\n<td style=\"text-align: center;\">弃用</td>\n<td style=\"text-align: center;\">58,646(43.0%)</td>\n<td style=\"text-align: center;\">54,926(40.6%)</td>\n<td style=\"text-align: center;\">-2.4%</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">TLS 1.2</td>\n<td style=\"text-align: center;\">取决于密码套件和客户端</td>\n<td style=\"text-align: center;\">135,842(99.6)</td>\n<td style=\"text-align: center;\">134,937(99.7)</td>\n<td style=\"text-align: center;\">+0.1%</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">TLS 1.3</td>\n<td style=\"text-align: center;\">安全</td>\n<td style=\"text-align: center;\">70,073(51.4%)</td>\n<td style=\"text-align: center;\">73,355(54.2%)</td>\n<td style=\"text-align: center;\">+2.8%</td>\n</tr>\n</tbody>\n</table>\n<p>可以看到,几乎100%的站点都能运行TLS 1.2,支持TLS 1.3的百分比也超过了50%。这是非常鼓舞人心的数据。虽然极少数网站还在运行SSL 2.0/3.0,并且TLS 1.0/1.1都还有40%左右的支持率,总体上它们所占比例是在持续下降的,这一好趋势应该会持续下去。</p>\n<p>本博客网站的服务由GitHub Page提供,输入网址到<a href=\"https://www.ssllabs.com/ssltest/\">SSL Lab的服务器测试页</a>,提交后得到的测试结果总结如下:</p>\n<p><img src=\"SSL-Report-Sum.png\" /></p>\n<p>站点的整体安全评级达到最高的A+。在证书和协议支持方面是满分,而在密钥交换和密码强度上也都得到90分。这说明GitHub兑现了对用户的安全承诺,值得程序员们的信任。</p>\n<p>报告的配置部分,给出了如下协议支持和密码套件的测试结果细节:</p>\n<p><img src=\"SSL-Report-Conf.png\" /></p>\n<p>这进一步确认了GitHub Page只支持TLS 1.2/1.3,符合<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>的要求。还可以看到,在“密码套件”(Cipher Suites)小标题下,TLS 1.3显示的两项GCM、一项ChaCha20-Poly1305都是AEAD算法,同类的三个密码套件也是服务器首选的TLS 1.2密码套件。这正是当前普遍采纳的安全密码算法配置。</p>\n<h4 id=\"用户自测\">用户自测</h4>\n<p>如果怀疑私有服务器还在使用过时的TLS/SSL协议,可以用命令行工具<code>curl</code>做个简单测试,示例如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ curl https://www.cisco.com -svo /dev/null --tls-max 1.1</span><br><span class=\"line\">* Trying 104.108.67.95:443...</span><br><span class=\"line\">* Connected to www.cisco.com (104.108.67.95) port 443 (<span class=\"comment\">#0)</span></span><br><span class=\"line\">* ALPN, offering h2</span><br><span class=\"line\">* ALPN, offering http/1.1</span><br><span class=\"line\">* successfully <span class=\"built_in\">set</span> certificate verify locations:</span><br><span class=\"line\">* CAfile: /etc/ssl/cert.pem</span><br><span class=\"line\">* CApath: none</span><br><span class=\"line\">* (304) (OUT), TLS handshake, Client hello (1):</span><br><span class=\"line\">} [151 bytes data]</span><br><span class=\"line\">* error:1404B42E:SSL routines:ST_CONNECT:tlsv1 alert protocol version</span><br><span class=\"line\">* Closing connection 0</span><br></pre></td></tr></table></figure>\n<p>这里输入命令行选项<code>--tls-max 1.1</code>设定最高的协议版本1.1,并连接到思科公司主页。输出结果显示连接失败,并收到“protocol version”告警信息。这说明服务器拒绝了TLS 1.1连接请求,其响应正好就是<a href=\"https://tools.ietf.org/html/rfc8996\">RFC 8996</a>所要求的。</p>\n<p>通用的开源密码学和安全通信工具箱<a href=\"https://www.openssl.org\">OpenSSL</a>提供的<code>openssl</code>命令行工具也可以做同样的测试。为了检测服务器是否支持TLS 1.2协议,可使用选项<code>s_client</code>仿真TLS/SSL客户端,同时输入<code>-tls1_2</code>指定只用TLS 1.2。命令行运行的记录如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ openssl s_client -connect www.cisco.com:443 -tls1_2</span><br><span class=\"line\">CONNECTED(00000005)</span><br><span class=\"line\">depth=2 C = US, O = IdenTrust, CN = IdenTrust Commercial Root CA 1</span><br><span class=\"line\">verify <span class=\"built_in\">return</span>:1</span><br><span class=\"line\">depth=1 C = US, O = IdenTrust, OU = HydrantID Trusted Certificate Service, CN = HydrantID Server CA O1</span><br><span class=\"line\">verify <span class=\"built_in\">return</span>:1</span><br><span class=\"line\">depth=0 CN = www.cisco.com, O = Cisco Systems Inc., L = San Jose, ST = California, C = US</span><br><span class=\"line\">verify <span class=\"built_in\">return</span>:1</span><br><span class=\"line\">---</span><br><span class=\"line\">Certificate chain</span><br><span class=\"line\"> 0 s:/CN=www.cisco.com/O=Cisco Systems Inc./L=San Jose/ST=California/C=US</span><br><span class=\"line\"> i:/C=US/O=IdenTrust/OU=HydrantID Trusted Certificate Service/CN=HydrantID Server CA O1</span><br><span class=\"line\"> 1 s:/C=US/O=IdenTrust/OU=HydrantID Trusted Certificate Service/CN=HydrantID Server CA O1</span><br><span class=\"line\"> i:/C=US/O=IdenTrust/CN=IdenTrust Commercial Root CA 1</span><br><span class=\"line\"> 2 s:/C=US/O=IdenTrust/CN=IdenTrust Commercial Root CA 1</span><br><span class=\"line\"> i:/C=US/O=IdenTrust/CN=IdenTrust Commercial Root CA 1</span><br><span class=\"line\">---</span><br><span class=\"line\">Server certificate</span><br><span class=\"line\">-----BEGIN CERTIFICATE-----</span><br><span class=\"line\">MIIHrzCCBpegAwIBAgIQQAF9KqwAKOKNhDf17h+WazANBgkqhkiG9w0BAQsFADBy</span><br><span class=\"line\">...</span><br><span class=\"line\">4TY7</span><br><span class=\"line\">-----END CERTIFICATE-----</span><br><span class=\"line\">subject=/CN=www.cisco.com/O=Cisco Systems Inc./L=San Jose/ST=California/C=US</span><br><span class=\"line\">issuer=/C=US/O=IdenTrust/OU=HydrantID Trusted Certificate Service/CN=HydrantID Server CA O1</span><br><span class=\"line\">---</span><br><span class=\"line\">No client certificate CA names sent</span><br><span class=\"line\">Server Temp Key: ECDH, P-256, 256 bits</span><br><span class=\"line\">---</span><br><span class=\"line\">SSL handshake has <span class=\"built_in\">read</span> 5765 bytes and written 322 bytes</span><br><span class=\"line\">---</span><br><span class=\"line\">New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES128-GCM-SHA256</span><br><span class=\"line\">Server public key is 2048 bit</span><br><span class=\"line\">Secure Renegotiation IS supported</span><br><span class=\"line\">Compression: NONE</span><br><span class=\"line\">Expansion: NONE</span><br><span class=\"line\">No ALPN negotiated</span><br><span class=\"line\">SSL-Session:</span><br><span class=\"line\"> Protocol : TLSv1.2</span><br><span class=\"line\"> Cipher : ECDHE-RSA-AES128-GCM-SHA256</span><br><span class=\"line\"> Session-ID: 1656D7D14447C1D5E68943F614A697455E60A036957D8D8C18F3B198DF42969F</span><br><span class=\"line\"> Session-ID-ctx:</span><br><span class=\"line\"> Master-Key: BB1209155344C55792077A4337964661FCA4F3F5BBF3185112F5E235BD07AD63838D24F5CF97161E696CB57398CAF478</span><br><span class=\"line\"> TLS session ticket lifetime hint: 83100 (seconds)</span><br><span class=\"line\"> TLS session ticket:</span><br><span class=\"line\"> 0000 - 00 00 0b 33 d4 56 15 3d-64 e8 fa 1d cf c1 1c 04 ...3.V.=d.......</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> 0090 - 1b 96 9c 25 82 70 a8 ed-24 1d 70 c9 28 56 84 59 ...%.p..$.p.(V.Y</span><br><span class=\"line\"></span><br><span class=\"line\"> Start Time: 1653265585</span><br><span class=\"line\"> Timeout : 7200 (sec)</span><br><span class=\"line\"> Verify <span class=\"built_in\">return</span> code: 0 (ok)</span><br><span class=\"line\">---</span><br></pre></td></tr></table></figure>\n<p>此记录很详细,格式的可读性也非常好。从输出中,可以了解到思科主页服务器的数字证书是由根证书认证机构IdenTrust提供数字签名认证的。客户端与服务器的会话是建立在TLS 1.2协议之上的,选中的密码套件是AEAD类型的ECDHE-RSA-AES128-GCM-SHA256,与GitHub Page提供的首选项完全一致。</p>\n<h4 id=\"浏览器测试\">浏览器测试</h4>\n<p>如果不放心你使用的浏览器的安全性,想测一测看是否还支持TLS 1.2之前的协议,可以在浏览器的地址拦输入下面的网址:</p>\n<ul>\n<li><a href=\"https://tls-v1-0.badssl.com\">https://tls-v1-0.badssl.com</a> (只支持TLS 1.0)</li>\n<li><a href=\"https://tls-v1-1.badssl.com\">https://tls-v1-1.badssl.com</a> (只支持TLS 1.1)</li>\n</ul>\n<p>用默认配置的火狐浏览器连到第二个网址后,页面显示如下</p>\n<blockquote>\n<p><strong>Secure Connection Failed</strong></p>\n<p>An error occurred during a connection to tls-v1-1.badssl.com:1011. Peer using unsupported version of security protocol.</p>\n<p>Error code: SSL_ERROR_UNSUPPORTED_VERSION</p>\n<ul>\n<li>The page you are trying to view cannot be shown because the authenticity of the received data could not be verified.</li>\n<li>Please contact the website owners to inform them of this problem.</li>\n</ul>\n<p><mark>This website might not support the TLS 1.2 protocol, which is the minimum version supported by Firefox.</mark></p>\n</blockquote>\n<p>此错误消息清楚地表明,在此配置下火狐浏览器运行的最低TLS协议版本为1.2,由于对方只运行TLS 1.1,所以双方无法建立连接。</p>\n<p>那么当浏览器真的还保留TLS 1.0/1.1的功能时,连接的结果是怎样的?</p>\n<p>出于测试的目的,可以先将火狐的默认TLS偏好值改成1.1,步骤如下(参考下图):</p>\n<ol type=\"1\">\n<li>打开一个新标签页中,在地址栏中输入<strong>about:config</strong>,然后按回车/返回键。</li>\n<li>页面提示“Proceed with Caution”,点击<strong>Accept the Risk and Continue</strong>按钮。</li>\n<li>在页面上方的搜索框中,键入TLS,显示过滤后的列表。</li>\n<li>找到<strong>security.tls.version.min</strong>偏好选项,点击<strong>Edit</strong>图标修改最低TLS版本。TLS版本与偏好值的对应关系是\n<ul>\n<li>TLS 1.0 => 1</li>\n<li>TLS 1.1 => 2</li>\n<li>TLS 1.2 => 3</li>\n<li>TLS 1.3 => 4</li>\n</ul></li>\n</ol>\n<p><img src=\"Firefox-TLS-MIN-VER.png\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>此时再连到<a href=\"https://tls-v1-1.badssl.com\">https://tls-v1-1.badssl.com</a>,结果是</p>\n<p><img src=\"TLS-V1-1-BADSSL.png\" style=\"width:80.0%;height:80.0%\" /></p>\n<p>这个醒目的大红页面告诉你,当前使用的浏览器没有禁用TLS 1.1,存在安全风险,能不用就尽量不用。</p>\n<p>测试完毕,不要忘记恢复火狐浏览器的默认TLS最低版本设置(3)。</p>\n<h3 id=\"参考书籍\">参考书籍</h3>\n<blockquote>\n<p><strong><em>Disclosure</em></strong>: <em>This blog site is reader-supported. When you buy through the affiliate links below, we may earn a tiny commission. Thank you.</em></p>\n</blockquote>\n<p>除了NIST和RFC文档之外,如果需要深入学习TLS协议规范、系统实现及应用和部署,推荐仔细阅读下面的三本书:</p>\n<div class=\"group-picture\"><div class=\"group-picture-row\"><div class=\"group-picture-column\"><img src=\"SSL-TLS.jpg\" /></div><div class=\"group-picture-column\"><img src=\"Implement-TLS.jpg\" /></div><div class=\"group-picture-column\"><img src=\"Bulletproof-TLS.jpg\" /></div></div></div>\n<ol type=\"1\">\n<li><a href=\"https://amzn.to/3EIfeo0\">SSL and TLS: Theory and Practice, Second Edition</a>《SSL和TLS:理论与实践》(第二版,2016)—— 本书对SSL、TLS和DTLS协议进行了全面讨论,提供了关于协议的理论和实践的完整细节,使读者对其设计原则和操作模式有了坚实的了解。书中还综合论述了这些协议与其他互联网安全协议相比的优势和劣势,并提供了正确实施协议所需的细节。</li>\n<li><a href=\"https://amzn.to/3tGCfBx\">Implementing SSL/TLS Using Cryptography and PKI</a> 《使用密码学和PKI实现SSL/TLS》(2011)—— 本书是为程序员准备的、用C语言编程实现互联网安全的SSL和TLS协议的实践性指南。即使缺乏密码学的知识,在本书的指导下也可以很好地完成工作。书中涵盖了TLS 1.2,包括实现相关加密协议、安全散列、证书解析和证书生成等内容。</li>\n<li><a href=\"https://amzn.to/3tLhaG6\">Bulletproof TLS and PKI, Second Edition: Understanding and Deploying SSL/TLS and PKI to Secure Servers and Web</a> 《防弹TLS和PKI:理解和部署SSL/TLS和PKI以确保服务器和网站安全》(第二版,2022)—— 这是最新的一本关于使用TLS加密和PKI来部署安全服务器和网络应用的完整指南,作者Ivan Ristić就是 SSL Labs 网站的创建者。本书教你如何保护系统免受窃听和仿冒攻击。书中还特别介绍了许多知名的协议或实现中的漏洞和弱点信息,并给出相应的防范方案和部署建议。</li>\n</ol>\n","categories":["技术评论"],"tags":["密码学","网络安全"]},{"title":"C编程支持16位存取接口的存储设备","url":"/2021/04/10/Support-16bit-access/","content":"<p>在计算机网络设备和嵌入式系统的研发中,常常需要硬件和软件设计人员紧密配合,以实现精准而有效的平台支持。特别地,对于需要数据存取的设备,了解设备的基本工作原理、控制和数据线路的连接方式及信号流程,对于可靠的软件设计和实现必不可少。否则,如果硬件电路设计或软件程序编写基于错误的假定,将需要更多的时间调试、排错和补救,可能严重影响项目的进度。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>Low-level programming is good for the programmer's soul.</strong><br> <strong>— <em>John Carmack</em>(约翰·卡马克,美国电玩游戏程序员、id Software的创始人之一)</strong></p>\n</div>\n<h3 id=\"存取模式\">存取模式</h3>\n<p>这里以东芝TC55VBM416静态随机存取存储器(Static Random Access Memory,简称SRAM)为例,说明系统开发中硬软件协同工作的重要性。由于SRAM有快速和极低功耗的特点,它非常适合用于低储存密度的数据保存应用。比如使用电池供电,SRAM可以用来储存系统配置文件等,达到等同于非易失性随机存取存储器(Non-Volatile RAM,简称NVRAM)的功效。TC55VBM416 SRAM的存储容量为16,777,216 (<span class=\"math inline\">\\(16\\times2^{20}\\)</span>) 比特位,提供1,048,576 (1M) 个16位字存取或2,097,152 (2M) 个8位字节存取单元。</p>\n<p>那么,我们如何在系统硬件设计中选择哪一种存取模式呢?答案就在<a href=\"https://www.datasheetarchive.com/TC55VBM416-datasheet.html\">TC55VBM416的数据表</a>(datasheet)里。参考数据表中对48个针脚的功能描述:</p>\n<p><img src=\"TC55VBM416-pins.png\" style=\"width:45.0%;height:45.0%\" /></p>\n<p>结合数据表中操作模式的说明,TC55VBM416的存取模式是这样工作的:当<span class=\"math inline\">\\(\\overline{\\mathsf{BYTE}}\\)</span>针脚的输入为高电平时,芯片执行16位字存取模式,针脚A0~A19的输入可用来寻址1M地址空间,16比特数据通过针脚I/O1~I/O16读写;当<span class=\"math inline\">\\(\\overline{\\mathsf{BYTE}}\\)</span>针脚的输入为低电平时,激活8位字节存取模式,这时针脚A-1和A0~A19组合一同寻址2M的地址空间,单字节数据通过针脚I/O1~I/O8读写。注意I/O16与A-1实际是一个针脚复用,因为这两种工作模式互斥,所以不会冲突。</p>\n<h3 id=\"硬件设计\">硬件设计</h3>\n<p>了解了TC55VBM416的工作原理,再来看看嵌入式系统硬件设计是如何使用它的。网上搜索到工业及车载应用中使用的富士通MB91F467C微控制器,其硬件系统设计就包括了东芝TC55VBM416 SRAM芯片,相关的电路图截取如下:</p>\n<p><img src=\"TV55VBM416-schematic.png\" style=\"width:70.0%;height:70.0%\" /></p>\n<p>从此电路图可以看出:</p>\n<ul>\n<li>系统地址线A1~A20接到SRAM针脚A0~A19</li>\n<li>系统数据线D31~D17接到SRAM针脚I/O1~I/O15</li>\n<li>跳线器JP94选择系统地址线A0或数据线D16连到I/O16/A-1针脚</li>\n<li>跳线器JP60选择高电平(VDD35)或低电平(GND)连到<span class=\"math inline\">\\(\\overline{\\mathsf{BYTE}}\\)</span>针脚</li>\n</ul>\n<p>很明显,这一设计由跳线器决定SRAM的存取模式。当JP60设定上端连通高电平到<span class=\"math inline\">\\(\\overline{\\mathsf{BYTE}}\\)</span>针脚时,相应的JP94应该设定下端连通D16到I/O16针脚,芯片执行16位字存取。由于系统地址线A0不连接到SRAM,软件系统将使用偶寻址存取双字节16比特数据。反之,当JP60设定下端连通低电平到<span class=\"math inline\">\\(\\overline{\\mathsf{BYTE}}\\)</span>针脚时,相应的JP94应该设定上端连通系统地址线A0到A-1针脚,芯片执行8位字节存取。这时系统地址线A0~A19全部用来寻址,程序可以直接存取单字节8比特数据。</p>\n<h3 id=\"软件支持\">软件支持</h3>\n<p>对于平台软件的开发人员,如果不事先审核系统硬件功能规范、不与硬件设计工程师沟通好存储设备的存取模式,就会出现意想不到的错误,耽误工程的进展。设想对于上面的硬件设计,系统默认SRAM的存取模式为16位,而软件程序员并不知情,继续沿用单字节8寻址的代码读写SRAM中的配置信息:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">inline</span> <span class=\"keyword\">char</span> <span class=\"title\">read_conf_byte</span> <span class=\"params\">(<span class=\"keyword\">char</span> *confddr)</span> </span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">return</span> *confaddr</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">inline</span> <span class=\"keyword\">void</span> write_conf_byte (<span class=\"keyword\">char</span> *confddr, <span class=\"keyword\">char</span> c) </span><br><span class=\"line\">{</span><br><span class=\"line\"> *confaddr = c</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>那么当<code>confaddr</code>为奇数时,实际读写的地址为<code>confaddr-1</code>,因为系统地址线A0不参与寻址。这样的程序测试时当然会错误百出,表现为有些数据对、有些数据错,而写对的数据有时也可能读错等等。解决的办法是同时更改两个跳线器,将存取模式设置为8位。</p>\n<p>在类似这种情况下,如果硬件设计不能动态调整,就需要修改软件支持不同的存取模式。对于16位存取接口,要更新单个字节,可以先读取对应偶地址的双字节,再根据原始的写请求地址及系统的<a href=\"https://packetmania.github.io/2020/11/24/Endianness/\">字节顺序</a>(Endianness)决定要替换高低哪一个字节,更新完毕后将16位数据一步写回对应偶地址。这一算法简单表述如下:</p>\n<blockquote>\n<p>用给定地址计算出对应的偶地址,然后从该地址读取双字节</p>\n<ul>\n<li>如果给定地址为奇数,同时为大端序;或给定地址为偶数,同时为小端序\n<ul>\n<li>将双字节数据低8位清零,再写入单个字节</li>\n<li>将新的双字节数据写入对应的偶地址</li>\n</ul></li>\n<li>如果给定地址为偶数,同时为大端序;或给定地址为偶数,同时为大端序\n<ul>\n<li>将双字节数据高8位清零,再写入单个字节</li>\n<li>将新的双字节数据写入对应的偶地址</li>\n</ul></li>\n</ul>\n</blockquote>\n<p>据此我们可以重写<code>write_conf_byte()</code>函数,使之同时支持8位和16位两种写入模式:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">typedef</span> <span class=\"class\"><span class=\"keyword\">enum</span> {</span></span><br><span class=\"line\"> CONF_8BIT_ACCESS = <span class=\"number\">1</span>,</span><br><span class=\"line\"> CONF_16BIT_ACCESS = <span class=\"number\">2</span></span><br><span class=\"line\">} conf_access_type;</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">test_endian</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">int</span> x = <span class=\"number\">1</span>;</span><br><span class=\"line\"> <span class=\"keyword\">return</span> *((<span class=\"keyword\">char</span> *)&x);</span><br><span class=\"line\">}</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span> <span class=\"title\">write_conf_byte</span> <span class=\"params\">(<span class=\"keyword\">char</span> *confaddr, <span class=\"keyword\">char</span> c, conf_access_type type)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> ushort *d_16;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (type == CONF_16BIT_ACCESS) {</span><br><span class=\"line\"> d_16 = (ushort *)((<span class=\"keyword\">uintptr_t</span>)confaddr & (~<span class=\"number\">0x1</span>));</span><br><span class=\"line\"> <span class=\"keyword\">if</span> ((((<span class=\"keyword\">uintptr_t</span>)confaddr & <span class=\"number\">0x01</span>) && (test_endian() == <span class=\"number\">0</span>)) ||</span><br><span class=\"line\"> ((((<span class=\"keyword\">uintptr_t</span>)confaddr & <span class=\"number\">0x01</span>) == <span class=\"number\">0</span>) && test_endian())) {</span><br><span class=\"line\"> <span class=\"comment\">/* (odd address + big endian) or (even address + little endian) */</span></span><br><span class=\"line\"> *d_16 = (*d_16 & <span class=\"number\">0xFF00</span>) | c;</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> *d_16 = (*d_16 & <span class=\"number\">0x00FF</span>) | (c<<<span class=\"number\">8</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* CONF_8BIT_ACCESS */</span></span><br><span class=\"line\"> *confaddr = c;</span><br><span class=\"line\"> }</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>这里还给出另一个复制字节序列到基于SRAM的配置存储地址的函数<code>confcpy()</code>。它也支持8位和16位两种存取接口类型,但只工作于大端序的系统环境。作为练习,读者可自行修改代码让它也可以在小端序系统中运行。</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/*</span></span><br><span class=\"line\"><span class=\"comment\"> * This function copies a given length of bytes from some source memory location</span></span><br><span class=\"line\"><span class=\"comment\"> * to configuration storage. Here we assume the source has 8-bit memory interface,</span></span><br><span class=\"line\"><span class=\"comment\"> * while the destination access type could be 8-bit or 16-bit.</span></span><br><span class=\"line\"><span class=\"comment\"> *</span></span><br><span class=\"line\"><span class=\"comment\"> * It works for Big Endian system only. Changes are needed to make it work for</span></span><br><span class=\"line\"><span class=\"comment\"> * Litte Endian system.</span></span><br><span class=\"line\"><span class=\"comment\"> */</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">void</span>* <span class=\"title\">confcpy</span> <span class=\"params\">(<span class=\"keyword\">char</span> *dst, <span class=\"keyword\">char</span> *src, <span class=\"keyword\">int</span> len, <span class=\"keyword\">int</span> type)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> uchar *s = src;</span><br><span class=\"line\"> uchar *d = dst;</span><br><span class=\"line\"> ushort *d_16 = (ushort *)((<span class=\"keyword\">uintptr_t</span>)d & ~<span class=\"number\">0x1</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (!s || !d) {</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (<span class=\"literal\">NULL</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (type == CONF_16BIT_ACCESS) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> ((<span class=\"keyword\">uintptr_t</span>)d & <span class=\"number\">0x01</span>) {</span><br><span class=\"line\"> *d_16 = (*d_16 & <span class=\"number\">0xFF00</span>) | *s;</span><br><span class=\"line\"> s++;</span><br><span class=\"line\"> d_16++;</span><br><span class=\"line\"> len--;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">while</span> (len >= <span class=\"number\">2</span>) {</span><br><span class=\"line\"> *d_16 = (*s<<<span class=\"number\">8</span>) | *((uchar *)(s+<span class=\"number\">1</span>));</span><br><span class=\"line\"> d_16++;</span><br><span class=\"line\"> s += <span class=\"number\">2</span>;</span><br><span class=\"line\"> len -= <span class=\"number\">2</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (len ==<span class=\"number\">1</span>) {</span><br><span class=\"line\"> *d_16 = (*d_16 & <span class=\"number\">0x00FF</span>) | ((*s)<<<span class=\"number\">8</span>);</span><br><span class=\"line\"> }</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> { <span class=\"comment\">/* CONF_8BIT_ACCESS */</span></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (len > <span class=\"number\">0</span>) {</span><br><span class=\"line\"> *d++ = *s++;</span><br><span class=\"line\"> len--;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> (dst);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<h3 id=\"测试程序\">测试程序</h3>\n<p>最后,下面一段小程序可用来测试<code>write_conf_byte()</code>函数。它定义了两个4字节的缓冲区<code>buffer8</code>和<code>buffer16</code>,分别用来仿真8位和16位存取的存储设备。程序用一个<code>for</code>循环调用<code>write_conf_byte()</code>函数逐个写入4个单字节的数据,调用时对应不同缓冲区使用不同的存取类别参数(<code>CONF_8BIT_ACCESS</code>和<code>CONF_16BIT_ACCESS</code>)。写入完毕后使用断言(<code>assert()</code>)函数比较二者数值是否一致,最后打印缓冲区内容:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">main</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> <span class=\"keyword\">char</span> *buffer8, *buffer16;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"%s Endian system\\n"</span>, test_endian() ? <span class=\"string\">"Little"</span> : <span class=\"string\">"Big"</span>);</span><br><span class=\"line\"> buffer8 = <span class=\"built_in\">calloc</span>(<span class=\"number\">4</span>, <span class=\"number\">1</span>);</span><br><span class=\"line\"> buffer16 = <span class=\"built_in\">calloc</span>(<span class=\"number\">4</span>, <span class=\"number\">1</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">for</span> (<span class=\"keyword\">int</span> i=<span class=\"number\">0</span>; i<<span class=\"number\">4</span>; i++) {</span><br><span class=\"line\"> write_conf_byte(buffer8+i, (<span class=\"keyword\">char</span>)i, CONF_8BIT_ACCESS);</span><br><span class=\"line\"> write_conf_byte(buffer16+i, (<span class=\"keyword\">char</span>)i, CONF_16BIT_ACCESS);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> assert(*(uint *)buffer8 == *(uint *)buffer16);</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Buffer content: 0x%08x\\n"</span>, *(uint *)buffer16);</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">0</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>在Intel 64位虚拟机加载Ubuntu 20.04的环境里,编译链接后程序运行的结果如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ uname -a</span><br><span class=\"line\">Linux zixi-VirtualBox 5.8.0-48-generic <span class=\"comment\">#54~20.04.1-Ubuntu SMP Sat Mar 20 13:40:25 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux</span></span><br><span class=\"line\">$ gcc -v</span><br><span class=\"line\">Using built-in specs.</span><br><span class=\"line\">COLLECT_GCC=gcc</span><br><span class=\"line\">COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper</span><br><span class=\"line\">OFFLOAD_TARGET_NAMES=nvptx-none:hsa</span><br><span class=\"line\">OFFLOAD_TARGET_DEFAULT=1</span><br><span class=\"line\">Target: x86_64-linux-gnu</span><br><span class=\"line\">Configured with: ../src/configure -v --with-pkgversion=<span class=\"string\">'Ubuntu 9.3.0-17ubuntu1~20.04'</span> --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-9-HskZEa/gcc-9-9.3.0/debian/tmp-nvptx/usr,hsa --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu</span><br><span class=\"line\">Thread model: posix</span><br><span class=\"line\">gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04)</span><br><span class=\"line\">$ gcc -o support-16bit-access support-16bit-access.c</span><br><span class=\"line\">$ ./support-16bit-access</span><br><span class=\"line\">Little Endian system</span><br><span class=\"line\">Buffer content: 0x03020100</span><br></pre></td></tr></table></figure>\n<p>这一结果正确。由于系统是小端序的,先写入的为低位字节,所以结果以32位整数表示为0x03020100。</p>\n<p>在同一系统中怎么测试大端序的场景呢?这就需要安装交叉编译的工具链了。如果选用大端序的MIPS处理器,可以安装对应的Ubuntu软件包<code>gcc-multilib-mips-linux-gnu</code>、<code>gcc-mips-linux-gnu</code>和<code>qemu-user</code>,支持以MIPS为目标架构的程序编译链接和仿真运行:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ sudo apt-get install gcc-multilib-mips-linux-gnu gcc-mips-linux-gnu qemu-user</span><br><span class=\"line\">...</span><br><span class=\"line\">$ mips-linux-gnu-gcc -v</span><br><span class=\"line\">Using built-in specs.</span><br><span class=\"line\">COLLECT_GCC=mips-linux-gnu-gcc</span><br><span class=\"line\">COLLECT_LTO_WRAPPER=/usr/lib/gcc-cross/mips-linux-gnu/9/lto-wrapper</span><br><span class=\"line\">Target: mips-linux-gnu</span><br><span class=\"line\">Configured with: ../src/configure -v --with-pkgversion=<span class=\"string\">'Ubuntu 9.3.0-17ubuntu1~20.04'</span> --with-bugurl=file:///usr/share/doc/gcc-9/README.Bugs --enable-languages=c,ada,c++,go,d,fortran,objc,obj-c++,gm2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-9 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libsanitizer --disable-libquadmath --disable-libquadmath-support --enable-plugin --with-system-zlib --without-target-system-zlib --enable-libpth-m2 --enable-multiarch --disable-werror --enable-multilib --with-arch-32=mips32r2 --with-fp-32=xx --with-lxc1-sxc1=no --enable-targets=all --with-arch-64=mips64r2 --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=mips-linux-gnu --program-prefix=mips-linux-gnu- --includedir=/usr/mips-linux-gnu/include</span><br><span class=\"line\">Thread model: posix</span><br><span class=\"line\">gcc version 9.3.0 (Ubuntu 9.3.0-17ubuntu1~20.04) </span><br><span class=\"line\">$ mips-linux-gnu-gcc -o support-16bit-access-mips support-16bit-access.c -static</span><br><span class=\"line\">$ ./support-16bit-access-mips</span><br><span class=\"line\">Big Endian system</span><br><span class=\"line\">Buffer content: 0x00010203</span><br></pre></td></tr></table></figure>\n<p>上面的记录显示,使用安装好的<code>mips-linux-gnu-gcc</code>编译器,成功编译链接同一个C程序。运行结果打印出大端序的系统信息,以及正确的缓冲区数据内容。注意到编译链接的命令行使用了静态链接选项<code>-static</code>,这是必须的。因为此时宿主系统为Intel x86_64架构,如果使用动态链接,程序将无法找到相应的MIPS动态链接库文件,运行失败。</p>\n<p>比较两个生成的可执行文件,可看到MIPS文件<code>support-16bit-access-mips</code>远远大于x86_64文件<code>support-16bit-access</code>。再使用<code>file</code>命令检查二者,印证了<code>support-16bit-access-mips</code>确实是MIPS GCC静态链接生成的:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ ls -al support-16bit-access*</span><br><span class=\"line\">-rwxrwxr-x 1 zixi zixi 16984 Apr 11 19:31 support-16bit-access</span><br><span class=\"line\">-rw-rw-r-- 1 zixi zixi 1282 Apr 11 19:30 support-16bit-access.c</span><br><span class=\"line\">-rwxrwxr-x 1 zixi zixi 611592 Apr 11 19:31 support-16bit-access-mips</span><br><span class=\"line\">$ file support-16bit-access</span><br><span class=\"line\">support-16bit-access: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=883f851beccb99c970c0d0924ff23479080f12b1, <span class=\"keyword\">for</span> GNU/Linux 3.2.0, not stripped</span><br><span class=\"line\">$ file support-16bit-access-mips</span><br><span class=\"line\">support-16bit-access-mips: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, BuildID[sha1]=dceaf3d5f41f3208046b5a56b1187f03f8f63114, <span class=\"keyword\">for</span> GNU/Linux 3.2.0, not stripped</span><br></pre></td></tr></table></figure>\n<p>还可以调整系统配置,直接运行MIPS GCC动态链接生成的可执行文件,详细见此文:<a href=\"https://web.archive.org/web/20220531113945/https://ownyourbits.com/2018/06/13/transparently-running-binaries-from-any-architecture-in-linux-with-qemu-and-binfmt_misc/\">Transparently running binaries from any architecture in Linux with QEMU and binfmt_misc</a></p>\n<p>完整的程序可点击这里下载:<a href=\"support-16bit-access.c.gz\">support-16bit-access.c.gz</a></p>\n","categories":["技术小札"],"tags":["C/C++编程","系统编程"]},{"title":"启用 TLS 1.3 提升网络应用的安全性和性能","url":"/2023/07/04/TLS1-3-intro/","content":"<p>TLS(传输层安全)是一种用于保护网络通信的加密协议,TLS 1.3 是 TLS 协议的最新版本。TLS 1.3 的引入旨在提供比以前版本更强大的安全性、隐私保护和性能。这里对比其所取代的 1.2 版本,对 TLS 1.3 做一个简单的介绍。并且,针对基于 OpenSSL 的网络应用,给出了使用和实现 TLS 1.3 的示例。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>It takes 20 years to build a reputation and a few minutes of cyber-incident to ruin it.</strong><br> <strong>— <em>Stéphane Nappo</em>(斯特凡·纳波,法国SEB集团副总裁兼全球信息安全主管,2018年度全球CISO)</strong></p>\n</div>\n<h2 id=\"tls-1.3-简介\">TLS 1.3 简介</h2>\n<p>TLS 1.3 是最新推荐的加密协议,适用于保护各种网络通信,包括网页浏览、电子邮件、在线交易、即时通讯、移动支付和其他许多应用。通过使用 TLS 1.3,可以建立更安全、更可靠的通信连接,确保数据的私密性和完整性。它于2018年8月由互联网工程任务组(IETF)标准化,发布于文档 <a href=\"https://datatracker.ietf.org/doc/html/rfc8446\">RFC 8446</a>。</p>\n<p>TLS 1.3 引入了与 TLS 1.2 相比的一些重要改进。下表呈现了二者的简要比较:</p>\n<table>\n<thead>\n<tr class=\"header\">\n<th style=\"text-align: center;\">比较方面</th>\n<th style=\"text-align: center;\">TLS 1.2</th>\n<th style=\"text-align: center;\">TLS 1.3</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">协议设计</td>\n<td style=\"text-align: center;\">请求-响应模型</td>\n<td style=\"text-align: center;\">减少往返次数</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">握手过程</td>\n<td style=\"text-align: center;\">多次往返</td>\n<td style=\"text-align: center;\">单次往返</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">密码套件</td>\n<td style=\"text-align: center;\">支持范围广泛,包括一些不安全的算法</td>\n<td style=\"text-align: center;\">着重于更强的密码算法</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">安全性</td>\n<td style=\"text-align: center;\">有已知漏洞(如CBC漏洞等)</td>\n<td style=\"text-align: center;\">解决了先前问题,更强的安全性</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">性能</td>\n<td style=\"text-align: center;\">多次往返造成高延迟</td>\n<td style=\"text-align: center;\">连接建立更快</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">抵御攻击</td>\n<td style=\"text-align: center;\">易受到降级攻击和填充应答攻击</td>\n<td style=\"text-align: center;\">有额外防护减少攻击风险</td>\n</tr>\n<tr class=\"odd\">\n<td style=\"text-align: center;\">兼容性</td>\n<td style=\"text-align: center;\">在不同平台上得到广泛支持</td>\n<td style=\"text-align: center;\">支持率逐渐提升,老旧系统可能不可用</td>\n</tr>\n<tr class=\"even\">\n<td style=\"text-align: center;\">实现支持</td>\n<td style=\"text-align: center;\">有许多密码库可用</td>\n<td style=\"text-align: center;\">各种库软件都提供支持</td>\n</tr>\n</tbody>\n</table>\n<p>可以看出,强化的安全性和性能提升是 TLS 1.3 最显著的特性,以下对此展开作一些介绍。</p>\n<h3 id=\"安全强化\">安全强化</h3>\n<h4 id=\"密码套件\">密码套件</h4>\n<p>TLS 1.3 的协议设计理念以增强安全性为首要目标。由此,TLS 1.3 大幅减少了支持的密码套件数量。它移除了不安全和有弱点的密码套件,仅保留了更安全和现代的密码套件。这样有助于提高通信的安全性,避免使用过时或易受攻击的密码套件。</p>\n<p>具体地说,TLS 1.3 删除了各种使用RSA密钥传输、静态迪菲—赫尔曼密钥交换、CBC操作模式或SHA-1的密码套件。它只采纳了有限的几个带有关联数据的认证加密(AEAD)密码套件。AEAD 能够同时保证数据的保密性、完整性和真实性,其高安全性使其成为TLS 1.3 的排他性选择。</p>\n<p>另一方面,以前 TLS 版本中的密码套件命名中包括密钥交换、数字签名、加密及消息验证全部算法。每个密码套件在一个名为 \"互联网编号分配机构\"(IANA)的组织维护的表格中都拥有一个编码位。每推出一种新的密码算法,就需要在列表中添加一系列新组合。这导致了总体有效选择的编码位数目激增,整个列表已经变得非常庞大。这种情况也使得密码套件的取舍变得复杂而混乱。</p>\n<p>TLS 1.3 的设计改变了密码套件的概念。它将密钥交换机制和数字签名认证剥离出来,新的密码套件命名规则是<code>TLS_<AEAD算法>_<散列算法></code>。其中散列算法用于 TLS 1.3 新定义的密钥派生函数 <a href=\"https://en.wikipedia.org/wiki/HKDF\">HKDF</a> 和生成握手阶段的消息验证码 (MAC)。TLS 1.3 协议所支持的密码套件有:</p>\n<figure class=\"highlight c\"><figcaption><span>RFC 8446 - Appendix B.4. Cipher Suites</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\">+------------------------------+-------------+</span><br><span class=\"line\">| Description | Value |</span><br><span class=\"line\">+------------------------------+-------------+</span><br><span class=\"line\">| TLS_AES_128_GCM_SHA256 | {<span class=\"number\">0x13</span>,<span class=\"number\">0x01</span>} |</span><br><span class=\"line\">| | |</span><br><span class=\"line\">| TLS_AES_256_GCM_SHA384 | {<span class=\"number\">0x13</span>,<span class=\"number\">0x02</span>} |</span><br><span class=\"line\">| | |</span><br><span class=\"line\">| TLS_CHACHA20_POLY1305_SHA256 | {<span class=\"number\">0x13</span>,<span class=\"number\">0x03</span>} |</span><br><span class=\"line\">| | |</span><br><span class=\"line\">| TLS_AES_128_CCM_SHA256 | {<span class=\"number\">0x13</span>,<span class=\"number\">0x04</span>} |</span><br><span class=\"line\">| | |</span><br><span class=\"line\">| TLS_AES_128_CCM_8_SHA256 | {<span class=\"number\">0x13</span>,<span class=\"number\">0x05</span>} |</span><br><span class=\"line\">+------------------------------+-------------+</span><br></pre></td></tr></table></figure>\n<p>这种简化的密码套件定义和大幅减少的协商参数集,也可以加速 TLS 1.3 的握手过程,从而提高了整体性能。</p>\n<h4 id=\"密钥交换\">密钥交换</h4>\n<p>TLS 1.3 强调前向保密性(forward secrecy),确保通信的私密性即使在长期密钥泄露的情况下也能得到保护。它只允许基于临时迪菲-赫尔曼密钥交换(DHE)或临时椭圆曲线迪菲-赫尔曼密钥交换(ECDHE)的共享密钥方法。二者都具有前向保密特性。并且,协议明确限制了密钥交换所使用的有安全保障的椭圆曲线群和有限域群:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">/* Elliptic Curve Groups (ECDHE) */</span></span><br><span class=\"line\">secp256r1(<span class=\"number\">0x0017</span>), secp384r1(<span class=\"number\">0x0018</span>), secp521r1(<span class=\"number\">0x0019</span>),</span><br><span class=\"line\">x25519(<span class=\"number\">0x001D</span>), x448(<span class=\"number\">0x001E</span>),</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">/* Finite Field Groups (DHE) */</span></span><br><span class=\"line\">ffdhe2048(<span class=\"number\">0x0100</span>), ffdhe3072(<span class=\"number\">0x0101</span>), ffdhe4096(<span class=\"number\">0x0102</span>),</span><br><span class=\"line\">ffdhe6144(<span class=\"number\">0x0103</span>), ffdhe8192(<span class=\"number\">0x0104</span>),</span><br></pre></td></tr></table></figure>\n<p>以上用于 ECDHE 的椭圆曲线群由<a href=\"https://www.rfc-editor.org/rfc/rfc8422\">RFC 8422</a>指定。前三个是 FIPS.186-4 规范定义的,对应的 NIST 命名为 P-256/P-384/P-512,而后两种(x25519/x448)是 ANSI.X9-62.2005 推荐的。<a href=\"https://www.rfc-editor.org/rfc/rfc7919\">RFC7919</a>详细说明了用于 DHE 的四个有限域群(ffdhe####)。这些有限域群中的素数都是安全素数(safe prime)。</p>\n<div class=\"note info\"><p>如果素数 <span class=\"math inline\">\\(p\\)</span> 满足 <span class=\"math inline\">\\(q = (p-1) /2\\)</span> 也是素数的条件,那它就是安全素数。</p>\n</div>\n<h4 id=\"签名验证\">签名验证</h4>\n<p>对于密钥交换阶段的签名验证,TLS 1.3 引入了更多的签名算法套件,以适应不同的安全需求:</p>\n<ul>\n<li><strong>RSA 签名算法</strong>:TLS 1.3 仍然支持基于RSA的签名算法,包括RSA-PKCS1-SHA256、RSA-PKCS1-SHA384等。这些算法使用RSA密钥来进行数字签名。</li>\n<li><strong>ECDSA 签名算法</strong>:TLS 1.3 引入了更多的基于椭圆曲线密码学(ECC)的签名算法,如ECDSA-SHA256、ECDSA-SHA384等。这些算法使用椭圆曲线密钥来进行数字签名,通常在安全性和性能方面优于RSA。</li>\n<li><strong>EdDSA 签名算法</strong>:TLS 1.3 还引入了基于 Edwards 曲线的 EdDSA(Edwards-curve Digital Signature Algorithm)签名算法。它具有高效性能和强大的安全性,适用于移动设备和资源受限的环境。</li>\n<li><strong>RSASSA-PSS 签名算法</strong>:除了传统的 RSA-PKCS1 签名算法,TLS 1.3 还引入了 RSASSA-PSS 签名算法,它是基于 RSA 的一种更安全的签名方式,具有更好的抗攻击性。</li>\n<li><strong>PSK 签名算法</strong>:TLS 1.3 支持基于预共享密钥(PSK)的签名算法,适用于 PSK 握手模式。这种方式不涉及数字签名,而是使用预共享密钥进行验证。</li>\n</ul>\n<p>TLS 1.3 停止使用 DSA(Digital Signature Algorithm)签名算法。这也是与 TLS 1.2 的一个显著不同。 DSA 存在一些安全性和性能方面的限制,并且在实践中很少使用,因此 TLS 1.3 移除了对 DSA 证书的支持。</p>\n<h4 id=\"其他改进\">其他改进</h4>\n<p>此外,TLS 1.3 还包括以下改进增强安全性</p>\n<ul>\n<li>TLS 1.3 不允许数据压缩。TLS 早期版本中的数据压缩功能可能导致 <a href=\"https://zh.wikipedia.org/wiki/CRIME\">CRIME</a> 攻击等安全问题。为了避免这种风险,TLS 1.3 完全取消了对数据压缩的支持。</li>\n<li>与早期版本的TLS不同,TLS 1.3 禁止在连接建立后进行再协商。这有助于减少安全风险和复杂性。重新协商可能引入新的安全漏洞,而且在连接过程中的频繁协商也可能导致性能问题。</li>\n<li>TLS 1.3 握手过程中服务器问候<code>ServerHello</code>消息之后的所有握手信息现在都被加密。新引入的加密扩展<code>EncryptedExtensions</code>消息使以前用明文发送的各种扩展信息也能得到加密保护。</li>\n<li>TLS 1.3 对服务器发送到客户端的证书消息实施非对称加密保护。这种加密可以防止中间人攻击、信息泄露和证书伪造等威胁,进一步增强连接的安全性和隐私保护。</li>\n</ul>\n<h3 id=\"性能提升\">性能提升</h3>\n<h4 id=\"简化的握手过程\">简化的握手过程</h4>\n<p>高速移动互联网的发展大趋势要求尽可能地使用 HTTPS/TLS 保护所有流量的私密性。这样做的不利之处是新连接会变得有点慢。为了让客户机和网络服务器达成一致的密钥,双方需要通过 TLS “握手过程” 交换加密数据。在 TLS 1.2 及其之前的所有协议中,初始的握手过程都要求至少两次往返消息传递。与单纯的 HTTP 相比,HTTPS 的 TLS 握手过程所产生的额外延迟会对注重性能的应用程序非常不利。</p>\n<p>TLS 1.3 大幅简化了握手过程,在大多数情况下仅需要一次往返,从而加快了连接建立速度和降低了延迟。每个 TLS 1.3 连接都将使用基于 (EC)DHE 的密钥交换,而服务器支持的参数可能很容易猜到(比如 ECDHE + x25519 或 P-256)。由于选择有限,客户端可以直接在第一条信息中发送 (EC)DHE 密钥共享信息,而不用等到服务器确认它愿意支持哪种密钥交换。这样,服务器就可以提前一轮推导出共享密钥并发送加密数据。</p>\n<p>下面是 TLS 1.2 和 TLS 1.3 的握手过程消息序列对比图,TLS 1.3 的握手使用以下借用自RFC 8446规范的符号:‘+’表示值得注意的扩展名;‘*’指可选消息或扩展名;‘[]’、‘()’和‘{}’表示加密过的消息,其中用于加密的密钥各不相同。</p>\n<figure>\n<img src=\"TLS1_2vs1_3-handshake.jpg\" alt=\"TLS 1.2 握手(左)对比 TLS 1.3 握手(右)\" /><figcaption aria-hidden=\"true\">TLS 1.2 握手(左)对比 TLS 1.3 握手(右)</figcaption>\n</figure>\n<p>参考此图,说明下面几点:</p>\n<ul>\n<li>可以明显看出 TLS 1.3 删除了 TLS 1.2 使用的几条消息:<code>ServerHelloDone</code>、<code>ChangeCipherSpec</code>、<code>ServerKeyExchange</code>和<code>ClientKeyExchange</code>。TLS 1.2 的服务器密钥交换(<code>ServerKeyExchange</code>)和客户端密钥交换(<code>ClientKeyExchange</code>)消息的内容随协商的身份验证和密钥共享方法而变化。在 TLS 1.3 中,这些信息被移至<code>ClientHello</code>和<code>ServerHello</code>消息的扩展中。TLS 1.3 完全弃用了<code>ServerHelloDone</code>和 <code>ChangeCipherSpec</code>消息,没有替换。</li>\n<li>对于 TLS 1.3 基于公钥的验证模式可能是最重要的。它总是使用 (EC)DHE 来实现前向保密。图中显示了必须在这种模式下使用的<code>ClientHello</code>中的四个扩展:<code>key_share</code>、<code>signature_algorithms</code>、<code>supported_groups</code>和<code>support_versions</code>。</li>\n<li>在 TLS 1.2 握手过程中,控制数据的交换需要在客户端和服务器之间多次往返。TLS 1.2 的<code>ClientKeyExchange</code>和<code>ChangeCipherSpec</code>消息由分开的数据包传输,完成(<code>Finished</code>)消息是第一条(也是唯一一条)加密的握手消息。整个过程要传输5~7个数据包。</li>\n<li>在 TLS 1.3 握手过程中,加密的应用数据(<code>Application Data</code>)已在第一轮往返后由客户端发送。如前所述,加密扩展(<code>EncryptedExtensions</code>)为 TLS 早期版本中 <code>ServerHello</code> 中的许多扩展提供了私密保护。如果需要相互验证(这在物联网部署中很常见),服务器将使用证书请求(<code>CertificateRequest</code>)消息。</li>\n<li>TLS 1.3 中的证书(<code>Certificate</code>)、证书验证(<code>CertificateVerify</code>)和完成(<code>Finished</code>)消息保留了早期 TLS 版本的语义,但是它们都是使用非对称加密的消息。通过加密证书和证书验证消息,TLS 1.3 能够更好地防止中间人攻击和伪造证书攻击,同时增强连接的隐私。这也是TLS 1.3 设计中的一个重要安全特性。</li>\n</ul>\n<p>在极少数情况下,当服务器不支持客户端发送的某个密钥共享方法时,服务器可以发送一条新消息<code>HelloRetryRequest</code>,让客户端知道它支持哪些群。由于群列表已大幅缩减,预计这种情况不会经常发生。</p>\n<h4 id=\"rtt-会话恢复\">0-RTT 会话恢复</h4>\n<p>TLS 1.3 中的 0-RTT(Zero Round Trip Time,0往返时间)是一种特殊的握手模式。它允许客户端在握手阶段就发送加密数据,从而减少了连接建立所需的往返次数,实现更快的会话恢复。下面是 0-RTT 工作模式的简要解释:</p>\n<ol type=\"1\">\n<li><strong>记录会话票据</strong>:在正常的 TLS 1.3 握手过程中,客户端和服务器会在握手期间生成一个称为\"会话票据\"(Session Ticket)的数据结构。会话票据包含了有关连接的信息,包括密钥参数和加密套件等。服务器会将客户端提供的会话票据存储起来。</li>\n<li><strong>0-RTT 握手</strong>:当客户端重新连接服务器时,它会在<code>ClientHello</code>消息的<code>early_data</code>扩展中包含之前记录的会话票据,同时附带加密的应用数据。客户端使用之前的连接中获得的预共享密钥(PSK)来加密 0-RTT 数据。</li>\n<li><strong>服务器响应</strong>:服务器接收到此消息后,如果支持 0-RTT 模式并且能够识别和验证会话票据,它会发送一个<code>EncryptedExtensions</code>消息,然后在<code>Finished</code>消息中确认连接。这样,服务器可以在0个往返的情况下立即建立安全连接。服务器接着也可以立即向客户端发送数据,以实现 0-RTT 数据的传输。</li>\n</ol>\n<p>TLS 1.3 的 0-RTT 会话恢复和数据传输过程的消息序列如下:</p>\n<figure>\n<img src=\"TLS1_3-0-RTT.jpg\" style=\"width:50.0%;height:50.0%\" alt=\"TLS 1.3 0-RTT\" /><figcaption aria-hidden=\"true\">TLS 1.3 0-RTT</figcaption>\n</figure>\n<h3 id=\"常见问题\">常见问题</h3>\n<ul>\n<li><p><strong>TLS 1.3 协议允许使用 RSA 数字证书吗?</strong></p>\n<p>一个常见的误解,是说“TLS 1.3 不兼容 RSA 数字证书”。显然,以上的“签名验证”一节的描述说明这是错误的。TLS 1.3 仍然支持使用 RSA 进行密钥交换和身份验证。然而,考虑到 RSA 的安全性问题,推荐在构建和部署新的TLS 1.3应用程序时,首选 ECC 密钥交换算法和数字证书来实现更高的安全性和性能。</p></li>\n<li><p><strong>TLS 1.3 握手过程中,服务器如何指定要求客户端提供证书?</strong></p>\n<p>在某些场景下,服务器也需要验证客户端的身份,确保只有合法的客户端才能访问服务器资源。这就是使用mTLS(Mutual TLS)的情况。TLS 1.3 握手过程中,服务器可以通过发送一个特殊的证书请求(<code>CertificateRequest</code>)扩展来指定要求客户端提供证书。当服务器决定要求客户端提供证书时,它在<code>ServerHello</code>消息之后发送<code>CertificateRequest</code>扩展消息。这个扩展消息包含了一些必要的参数,如支持的证书类型列表、可接受的证书颁发机构列表等。客户端收到后会知道服务器要求它提供证书,它可以选择性地响应请求。如果客户端也配置了对 mTLS 的支持,决定提供证书,就通过发送<code>Certificate</code>消息来提供其证书链。</p></li>\n<li><p><strong>0-RTT 是否会有遭受重放(replay)攻击的风险?</strong></p>\n<p>TLS 1.3 的 0-RTT 会话恢复模式没有互动,在某些情况下确实会面临重放攻击的风险。攻击者可能重复之前发送的数据,以模拟合法的请求。为了最大程度地避免和减少重放攻击的风险,TLS 1.3 提供了一些保护措施和建议:</p>\n<ol type=\"1\">\n<li>最简单的防重放方法是服务器只允许每个会话票据使用一次。 例如,服务器可以维护一个包含所有尚未使用的有效票据的数据库,在每张票据被使用时将其从数据库中删除。如果收到了未知的票据,服务器就会退回到完全握手状态。</li>\n<li>服务器可以限制接受会话票据的时间窗口,即允许 0-RTT 数据有效的时间范围。这样可以减小攻击者成功重放的可能性。</li>\n<li>客户端和服务器还应该将 0-RTT 数据仅用于无状态的请求,即不会对服务器状态产生影响的请求如 HTTP GET。对于需要修改服务器状态或产生影响的请求,限制仅使用正常的握手模式。</li>\n<li>防重放的另一种方法是记录从 ClientHello 导出的唯一值(通常是随机值或 PSK 捆绑值),并拒绝重复。 记录所有 ClientHellos 会导致状态无限制增长,但结合上面的第2条,服务器可以在给定的时间窗口内记录 ClientHellos,并使用 \"obfuscated_ticket_age\"(混淆票据年龄)来确保票据不会在窗口外被重复使用。</li>\n</ol></li>\n<li><p><strong>如果客户端不知道服务器是否支持 TLS 1.3,应该如何选择 TLS 握手方式?</strong></p>\n<p>TLS 协议提供了一种内置机制协商端点之间的运行版本。TLS 1.3 继续了这一传统。RFC 8446 附录 D.1 “Negotiating with an Older Server”给出具体的说明:</p>\n<blockquote>\n<p>A TLS 1.3 client who wishes to negotiate with servers that do not support TLS 1.3 will send a normal TLS 1.3 ClientHello containing 0x0303 (TLS 1.2) in ClientHello.legacy_version but with the correct version(s) in the \"supported_versions\" extension. <strong>If the server does not support TLS 1.3, it will respond with a ServerHello containing an older version number.</strong> If the client agrees to use this version, the negotiation will proceed as appropriate for the negotiated protocol.</p>\n</blockquote>\n<p>这段话的意思是,如果 TLS 1.3 客户端希望与不支持 TLS 1.3 的服务器进行协商,则发送一个包含 TLS 1.2 版本号的正常<code>ClientHello</code>消息,同时在该消息的<code>supported_versions</code>扩展中填入所支持的版本号(TLS 1.3 和 TLS 1.2)。如果服务器不支持 TLS 1.3,它就会回应一个包含 TLS 1.2 版本编号的 <code>ServerHello</code>。 如果客户端同意使用该版本,握手过程将按照协商协议的适当方式继续进行。</p>\n<p>下面的 TLS 1.3 <code>ClientHello</code>数据包分析截图清晰地展现了这一点。在左边显示的握手消息的版本号为“Version: TLS 1.2 (0x0303)”,同时可以看到密码套件部分先列出了3个 TLS 1.3 AEAD密码套件,后面跟着14个 TLS 1.2 常规密码套件。右边有<code>key_share</code>、<code>signature_algorithms</code>、<code>supported_groups</code>和<code>support_versions</code>4个扩展。<code>support_versions</code>扩展包括 TLS 1.3 和 TLS 1.2 两个版本号。这就是给出了供服务器选择的 TLS 版本。另外<code>key_share</code>扩展列出了客户端首选的密钥共享方法为<code>x25519</code>和<code>secp256r1</code>(即 NIST P-256)</p>\n<p><img src=\"TLS1_3-clienthello.jpg\" /></p></li>\n<li><p><strong>TLS 1.3 协议适用于 UDP 和 EAP 吗?</strong></p>\n<p>TLS 最初是为 TCP 连接设计的,后来引入了适用于 UDP 的变体 DTLS(Datagram Transport Layer Security)。以 TLS 1.3 为基础,IETF 推出了相应的升级版 DTLS 1.3 协议<a href=\"https://www.rfc-editor.org/rfc/rfc9147\">RFC 9147</a>。DTLS 1.3 的设计目标是提供“[与 TLS 1.3] 同等的安全保证,但次序保证和不可重放性除外”。此协议发布于2022年4月,目前支持它的软件库还不多。</p>\n<p>TLS 也可以在多种 EAP 类型中用作认证和加密的协议,如 EAP-TLS、EAP-FAST 和 PEAP 等。对应 TLS 1.3,IETF 也公布了两个技术标准文档:</p>\n<ul>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9190\">RFC 9190</a>: EAP-TLS 1.3: Using the Extensible Authentication Protocol with TLS 1.3(2022年2月)</li>\n<li><a href=\"https://www.rfc-editor.org/rfc/rfc9427\">RFC 9427</a>: TLS-Based Extensible Authentication Protocol (EAP) Types for Use with TLS 1.3(2023年6月)</li>\n</ul>\n<p>这两个协议也都很新,支持它们的软件库更新还有待时日。</p></li>\n</ul>\n<h2 id=\"应用和实现\">应用和实现</h2>\n<p>TLS 1.3 强化的安全性和优化的性能使其成为各类网络应用程序安全通信的首选。下面先针对典型的三大网页服务器软件 Apache、Nginx 和 Lighttpd,简单介绍一下如何启用它们 TLS 1.3 功能。最后给出了在自己开发的应用软件中实现 TLS 1.3 的程序示例。</p>\n<div class=\"note warning\"><p><strong>注意:</strong>许多网络应用程序的安全通信实现依赖于第三方的 SSL/TLS 编程软件库,比如wolfSSL、GnuTLS、NSS 和 OpenSSL等。因此要开启这些应用的 TLS 1.3 功能,需要确保它们使用的链接库支持 TLS 1.3 。比如2018年9月,流行的 OpenSSL 项目发布了 1.1.1 版本的库,其中支持 TLS 1.3 是其 \"首要的新功能\"。</p>\n</div>\n<h3 id=\"apache-http-服务器应用\">Apache HTTP 服务器应用</h3>\n<p>Apache HTTP 服务器是Apache软件基金会的一个开放源码的网页服务器软件。由于其跨平台和安全性,Apache HTTP 服务器被广泛使用,是最流行的Web服务器软件之一。Apache支持多种特性,其中许多通过编译的模块实现核心功能的扩展,比如身份验证方案、代理服务器、URL 重写、SSL/TLS 支持,以及将Perl/Python等解释器编译到服务器中等。</p>\n<p>Apache HTTP 服务器从 2.4.36 版本开始内建对 TLS 1.3 的支持,无需安装任何附加模块或补丁。下面的命令可用来验证服务器的版本</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ apache2ctl -v </span><br><span class=\"line\">Server version: Apache/2.4.41 (Ubuntu)</span><br><span class=\"line\">Server built: 2020-04-13T17:19:17</span><br></pre></td></tr></table></figure>\n<p>版本验证无误后,就可以更新配置文件的<code>SSLProtocol</code>行。如下将使 Apache HTTP 服务器只支持 TLS 1.3 协议</p>\n<figure class=\"highlight nginx\"><figcaption><span>/etc/apache2/mods-available/ssl.conf</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\"># 只启用TLS 1.3 版本协议</span></span><br><span class=\"line\"><span class=\"attribute\">SSLProtocol</span> -all +TLSv1.<span class=\"number\">3</span></span><br></pre></td></tr></table></figure>\n<p>如果需要兼容支持 TLS 1.2 的客户端,可以加上<code>+TLSv1.2</code>。更新配置之后,重启服务</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ sudo service apache2 restart</span><br></pre></td></tr></table></figure>\n<h3 id=\"nginx-应用\">Nginx 应用</h3>\n<p>Nginx 是基于异步框架和模块化设计的高性能网页服务器,也可以用于反向代理、负载均衡器和HTTP缓存应用。它是根据类BSD许可证条款发布的免费开源软件。Nginx 使用异步事件驱动的方法来处理请求,可以在高负载下提供更可预测的性能。当前 Nginx 的市场占有率与 Apache HTTP 服务器几乎持平。</p>\n<p>Nginx 从 1.13.0 版本 开始支持 TLS 1.3。下面的命令可用来验证 Nginx 的版本</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ nginx -v</span><br><span class=\"line\">nginx version: nginx/1.17.10 (Ubuntu)</span><br></pre></td></tr></table></figure>\n<p>在 Nginx 的配置文件中,找到server块并修改<code>ssl_protocols</code>行就可以启用 TLS 1.3:</p>\n<figure class=\"highlight nginx\"><figcaption><span>/etc/nginx/nginx.conf</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"section\">server</span> {</span><br><span class=\"line\"> <span class=\"attribute\">listen</span> <span class=\"number\">443</span> ssl http2;</span><br><span class=\"line\"> <span class=\"attribute\">listen</span> [::]:<span class=\"number\">443</span> ssl http2;</span><br><span class=\"line\"> <span class=\"attribute\">server_name</span> example.com;</span><br><span class=\"line\"> <span class=\"attribute\">root</span> /var/www/example.com/public;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"attribute\">ssl_certificate</span> /path/to/your/certificate.crt;</span><br><span class=\"line\"> <span class=\"attribute\">ssl_certificate_key</span> /path/to/your/private-key.key;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\"># 添加对 TLS 1.3 的支持</span></span><br><span class=\"line\"> <span class=\"attribute\">ssl_protocols</span> TLSv1.<span class=\"number\">2</span> TLSv1.<span class=\"number\">3</span>;</span><br><span class=\"line\"> </span><br><span class=\"line\"> <span class=\"comment\"># ...其他配置...</span></span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>如果不需要继续支持 TLS 1.2,可以删除前面的<code>TLSv1.2</code>。修改完成,可以运行以下命令测试Nginx 的配置,然后重启服务</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ sudo nginx -t</span><br><span class=\"line\">nginx: the configuration file /etc/nginx/nginx.conf syntax is ok</span><br><span class=\"line\">nginx: configuration file /etc/nginx/nginx.conf <span class=\"built_in\">test</span> is successful</span><br><span class=\"line\"></span><br><span class=\"line\">$ sudo service nginx restart</span><br></pre></td></tr></table></figure>\n<h3 id=\"lighttpd-应用\">Lighttpd 应用</h3>\n<p>Lighttpd(发音为\"lighty\")是一个轻量级的开源网页服务器软件。它专注于高性能、低内存占用和快速的响应速度。Lighttpd适用于各种规模的网页应用和静态内容的提供。它的设计目标是提供一个高效、灵活和可扩展的网页服务器,尤其适用于高负载和资源受限(如嵌入式系统)的环境。</p>\n<p>第一个支持 TLS 1.3 的 Lighttpd 发布是 1.4.56 版。从这个版本开始,Lighttpd 默认支持 TLS 的最低版本是 TLS 1.2。也就是说,如果不做相应的配置文件修改,这时 Lighttpd 支持 TLS 1.2 和 TLS 1.3。</p>\n<p>要限定只使用 Lighttpd 的 TLS 1.3 功能,先确保 mod_openssl 模块已加载。然后在配置文件 lighttpd.conf 中 ,找到<code>server.modules</code>部分,添加以下<code>ssl.openssl.ssl-conf-cmd</code>行:</p>\n<figure class=\"highlight nginx\"><figcaption><span>/etc/lighttpd/lighttpd.conf</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\">server.<span class=\"attribute\">modules</span> += (<span class=\"string\">"mod_openssl"</span>)</span><br><span class=\"line\">$SERVER[<span class=\"string\">"socket"</span>] == <span class=\"string\">":443"</span> {</span><br><span class=\"line\"> ssl.<span class=\"attribute\">engine</span> = <span class=\"string\">"enable"</span> </span><br><span class=\"line\"> ssl.pemfile = <span class=\"string\">"/path/to/your/cert.pem"</span></span><br><span class=\"line\"> ssl.privkey = <span class=\"string\">"/path/to/your/privkey.pem"</span></span><br><span class=\"line\"> ssl.openssl.ssl-conf-cmd = (<span class=\"string\">"MinProtocol"</span> => <span class=\"string\">"TLSv1.3"</span>,</span><br><span class=\"line\"> <span class=\"string\">"Options"</span> => <span class=\"string\">"-ServerPreference"</span>)</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>这将设定 Lighttpd 支持的最低版本是 TLS 1.3。最后保存并重新加载 Lighttpd 配置,使更改生效:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">sudo lighttpd -t -f /etc/lighttpd/lighttpd.conf <span class=\"comment\"># 检查配置是否正确</span></span><br><span class=\"line\">sudo systemctl reload lighttpd <span class=\"comment\"># 重新加载Lighttpd配置</span></span><br></pre></td></tr></table></figure>\n<h3 id=\"软件实现\">软件实现</h3>\n<p>如果是自己设计和实现的网络安全应用,先确认使用的链接库是否支持 TLS 1.3。以 OpenSSL 库为例,执行如下的两个<code>openssl</code>命令行检查系统安装的版本和所支持的 TLS 1.3 密码套件:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ openssl version</span><br><span class=\"line\">OpenSSL 1.1.1f 31 Mar 2020</span><br><span class=\"line\"></span><br><span class=\"line\">$ openssl ciphers -v | grep TLSv1.3</span><br><span class=\"line\">TLS_AES_256_GCM_SHA384 TLSv1.3 Kx=any Au=any Enc=AESGCM(256) Mac=AEAD</span><br><span class=\"line\">TLS_CHACHA20_POLY1305_SHA256 TLSv1.3 Kx=any Au=any Enc=CHACHA20/POLY1305(256) Mac=AEAD</span><br><span class=\"line\">TLS_AES_128_GCM_SHA256 TLSv1.3 Kx=any Au=any Enc=AESGCM(128) Mac=AEAD</span><br></pre></td></tr></table></figure>\n<p>如上第一个命令的输出结果显示所使用的 OpenSSL 库的版本是 1.1.1f,是支持 TLS 1.3 的。第二个命令的输出也证实了默认可以使用的三个 TLS 1.3 密码套件。</p>\n<p>下面是用C语言编写的 TLS 1.3 客户端和服务器的简单示例程序:</p>\n<div class=\"tabs\" id=\"openssl-实现-tls1.3-示例\"><ul class=\"nav-tabs\"><li class=\"tab active\"><a href=\"#openssl-实现-tls1.3-示例-1\">客户端</a></li><li class=\"tab\"><a href=\"#openssl-实现-tls1.3-示例-2\">服务器</a></li></ul><div class=\"tab-content\"><div class=\"tab-pane active\" id=\"openssl-实现-tls1.3-示例-1\"><figure class=\"highlight c\"><figcaption><span>tls_client.c</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdio.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdlib.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><unistd.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><string.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><arpa/inet.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><openssl/ssl.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><openssl/err.h></span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">main</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> SSL_CTX *ctx;</span><br><span class=\"line\"> SSL *ssl;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> client_fd;</span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr_in</span> <span class=\"title\">server_addr</span>;</span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 初始化OpenSSL库</span></span><br><span class=\"line\"> SSL_library_init();</span><br><span class=\"line\"> SSL_load_error_strings();</span><br><span class=\"line\"> ERR_load_BIO_strings();</span><br><span class=\"line\"> OpenSSL_add_all_algorithms();</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建TLS 1.3上下文</span></span><br><span class=\"line\"> ctx = SSL_CTX_new(TLS_client_method());</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建socket</span></span><br><span class=\"line\"> client_fd = socket(AF_INET, SOCK_STREAM, <span class=\"number\">0</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 连接服务器</span></span><br><span class=\"line\"> <span class=\"built_in\">memset</span>(&server_addr, <span class=\"number\">0</span>, <span class=\"keyword\">sizeof</span>(server_addr));</span><br><span class=\"line\"> server_addr.sin_family = AF_INET;</span><br><span class=\"line\"> server_addr.sin_port = htons(<span class=\"number\">4433</span>);</span><br><span class=\"line\"> inet_pton(AF_INET, <span class=\"string\">"127.0.0.1"</span>, &server_addr.sin_addr);</span><br><span class=\"line\"></span><br><span class=\"line\"> connect(client_fd, (struct sockaddr*)&server_addr, <span class=\"keyword\">sizeof</span>(server_addr));</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建TLS连接</span></span><br><span class=\"line\"> ssl = SSL_new(ctx);</span><br><span class=\"line\"> SSL_set_fd(ssl, client_fd);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 握手</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (SSL_connect(ssl) <= <span class=\"number\">0</span>) {</span><br><span class=\"line\"> ERR_print_errors_fp(<span class=\"built_in\">stderr</span>);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 发送和接收数据</span></span><br><span class=\"line\"> <span class=\"keyword\">char</span> message[] = <span class=\"string\">"Hello from client!"</span>;</span><br><span class=\"line\"> SSL_write(ssl, message, <span class=\"built_in\">strlen</span>(message));</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">char</span> buffer[<span class=\"number\">1024</span>] = {<span class=\"number\">0</span>};</span><br><span class=\"line\"> SSL_read(ssl, buffer, <span class=\"keyword\">sizeof</span>(buffer));</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Received: %s\\n"</span>, buffer);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 关闭连接</span></span><br><span class=\"line\"> SSL_shutdown(ssl);</span><br><span class=\"line\"> SSL_free(ssl);</span><br><span class=\"line\"> close(client_fd);</span><br><span class=\"line\"> SSL_CTX_free(ctx);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">0</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></div><div class=\"tab-pane\" id=\"openssl-实现-tls1.3-示例-2\"><figure class=\"highlight c\"><figcaption><span>tls_server.c</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdio.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><stdlib.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><unistd.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><string.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><arpa/inet.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><openssl/ssl.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><openssl/err.h></span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">main</span><span class=\"params\">()</span> </span>{</span><br><span class=\"line\"> SSL_CTX *ctx;</span><br><span class=\"line\"> SSL *ssl;</span><br><span class=\"line\"> <span class=\"keyword\">int</span> server_fd, client_fd;</span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr_in</span> <span class=\"title\">server_addr</span>, <span class=\"title\">client_addr</span>;</span></span><br><span class=\"line\"> <span class=\"keyword\">socklen_t</span> client_len;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 初始化OpenSSL库</span></span><br><span class=\"line\"> SSL_library_init();</span><br><span class=\"line\"> SSL_load_error_strings();</span><br><span class=\"line\"> ERR_load_BIO_strings();</span><br><span class=\"line\"> OpenSSL_add_all_algorithms();</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建TLS 1.3上下文</span></span><br><span class=\"line\"> ctx = SSL_CTX_new(TLS_server_method());</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 加载证书和私钥</span></span><br><span class=\"line\"> SSL_CTX_use_certificate_file(ctx, <span class=\"string\">"server.crt"</span>, SSL_FILETYPE_PEM);</span><br><span class=\"line\"> SSL_CTX_use_PrivateKey_file(ctx, <span class=\"string\">"server.key"</span>, SSL_FILETYPE_PEM);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建socket</span></span><br><span class=\"line\"> server_fd = socket(AF_INET, SOCK_STREAM, <span class=\"number\">0</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 绑定地址</span></span><br><span class=\"line\"> <span class=\"built_in\">memset</span>(&server_addr, <span class=\"number\">0</span>, <span class=\"keyword\">sizeof</span>(server_addr));</span><br><span class=\"line\"> server_addr.sin_family = AF_INET;</span><br><span class=\"line\"> server_addr.sin_port = htons(<span class=\"number\">4433</span>);</span><br><span class=\"line\"> server_addr.sin_addr.s_addr = INADDR_ANY;</span><br><span class=\"line\"></span><br><span class=\"line\"> bind(server_fd, (struct sockaddr*)&server_addr, <span class=\"keyword\">sizeof</span>(server_addr));</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 监听</span></span><br><span class=\"line\"> listen(server_fd, <span class=\"number\">10</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Server is listening on port 4433...\\n"</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">while</span> (<span class=\"number\">1</span>) {</span><br><span class=\"line\"> <span class=\"comment\">// 接受客户端连接</span></span><br><span class=\"line\"> client_len = <span class=\"keyword\">sizeof</span>(client_addr);</span><br><span class=\"line\"> client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &client_len);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Client connected.\\n"</span>);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 创建TLS连接</span></span><br><span class=\"line\"> ssl = SSL_new(ctx);</span><br><span class=\"line\"> SSL_set_fd(ssl, client_fd);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 握手</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (SSL_accept(ssl) <= <span class=\"number\">0</span>) {</span><br><span class=\"line\"> ERR_print_errors_fp(<span class=\"built_in\">stderr</span>);</span><br><span class=\"line\"> } <span class=\"keyword\">else</span> {</span><br><span class=\"line\"> <span class=\"comment\">// 发送和接收数据</span></span><br><span class=\"line\"> <span class=\"keyword\">char</span> buffer[<span class=\"number\">1024</span>] = {<span class=\"number\">0</span>};</span><br><span class=\"line\"> SSL_read(ssl, buffer, <span class=\"keyword\">sizeof</span>(buffer));</span><br><span class=\"line\"> <span class=\"built_in\">printf</span>(<span class=\"string\">"Received: %s\\n"</span>, buffer);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">char</span> response[] = <span class=\"string\">"Hello from server!"</span>;</span><br><span class=\"line\"> SSL_write(ssl, response, <span class=\"built_in\">strlen</span>(response));</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"comment\">// 关闭连接</span></span><br><span class=\"line\"> SSL_shutdown(ssl);</span><br><span class=\"line\"> SSL_free(ssl);</span><br><span class=\"line\"> close(client_fd);</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"> SSL_CTX_free(ctx);</span><br><span class=\"line\"> close(server_fd);</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"number\">0</span>;</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure></div></div></div>\n<p>上述示例仅提供了 TLS 1.3 最基本的客户端和服务器实现。实际应用中,需要预先生成有效的证书和私钥,并根据具体需求进一步优化和扩展代码。</p>\n<p>还可以观察到在上面的示例中,客户端和服务器的程序都没有明确指定 TLS 1.3 版本。为什么?这是因为在 OpenSSL 1.1.1 及以上版本中,TLS 1.3 是默认启用的。因此在 TLS 客户端和服务器的上下文<code>ctx</code>中,不需要显式指定 TLS 版本。在使用OpenSSL库时,默认情况下会使用最高版本的 TLS,即 TLS 1.3(如果双方都支持)。如果对方不支持 TLS 1.3,它会尝试使用较旧的 TLS 版本来建立连接。</p>\n<p>如果希望限定 TLS 版本,可以在上下文中明确指定所需的 TLS 版本。例如,在 TLS 客户端示例中,如果你希望明确指定只能使用 TLS 1.3 版本,可以像这样修改代码:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// 创建TLS 1.3上下文</span></span><br><span class=\"line\">ctx = SSL_CTX_new(TLS_client_method());</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// 明确指定TLS 1.3版本</span></span><br><span class=\"line\">SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);</span><br><span class=\"line\">SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);</span><br></pre></td></tr></table></figure>\n<p>在 TL S服务器示例中,你也可以使用类似的方式明确指定 TLS 1.3 版本:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// 创建TLS 1.3上下文</span></span><br><span class=\"line\">ctx = SSL_CTX_new(TLS_server_method());</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// 明确指定TLS 1.3版本</span></span><br><span class=\"line\">SSL_CTX_set_min_proto_version(ctx, TLS1_3_VERSION);</span><br><span class=\"line\">SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);</span><br></pre></td></tr></table></figure>\n<p>如果想要限定协议的最低版本为 TLS 1.2,可以修改<code>SSL_CTX_set_min_proto_version()</code>的调用参数</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">// 创建TLS 上下文</span></span><br><span class=\"line\">ctx = SSL_CTX_new(...);</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\">// 指定最低 TLS 1.2 和 最高 TLS 1.3</span></span><br><span class=\"line\">SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);</span><br><span class=\"line\">SSL_CTX_set_max_proto_version(ctx, TLS1_3_VERSION);</span><br></pre></td></tr></table></figure>\n<p>总之,通过调用<code>SSL_CTX_set_min_proto_version()</code>和<code>SSL_CTX_set_max_proto_version()</code>函数,我们可以精准限定所支持 TLS 协议的版本范围。</p>\n","categories":["技术小札"],"tags":["密码学","网络安全","C/C++编程"]},{"title":"iTerm2 + Oh-My-Zsh + Powerlevel10k 打造酷炫macOS终端","url":"/2021/11/13/iTerm2-OMZ-Powerlevel10k/","content":"<p>熟练使用基于终端的各种命令行工具,可以让程序员的工作效率倍增。在苹果macOS上,集成终端应用iTerm2、Oh-My-Zsh配置框架和Powerlevel10k主题,能打造出非常酷的资深程序员专业终端。 <span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>工欲善其事,必先利其器。</strong><br> <strong>— <em>孔子《论语·卫灵公》</em> </strong></p>\n</div>\n<h3 id=\"iterm2仿真终端\">iTerm2仿真终端</h3>\n<p>苹果计算机系统的默认仿真终端是<code>Terminal.app</code>。虽然它实现了所有必要的基本命令行终端功能,也提供了一些定制选项(Profiles),但是可配置的内容非常有限,其界面对资深程序员也实在了无生趣。iTerm2是macOS平台上非常优秀的仿真终端替代方案。<a href=\"https://iterm2.com/index.html\">iTerm2官方网站</a>的自我介绍是:</p>\n<blockquote>\n<p>iTerm2 is a replacement for Terminal and the successor to iTerm. It works on Macs with macOS 10.14 or newer. iTerm2 brings the terminal into the modern age with features you never knew you always wanted.</p>\n</blockquote>\n<p>即iTerm2是将给你带来惊喜的现代化终端应用。下面来看看iTerm2的安装和功能概览。</p>\n<h4 id=\"应用安装\">应用安装</h4>\n<p>iTerm2官方网提供最新稳定版(3.4.12)的下载链接:<a href=\"https://iterm2.com/downloads/stable/latest\">https://iterm2.com/downloads/stable/latest</a>。下载后的文件名为<code>iTerm2-3_4_12.zip</code>,大小约为24.1MB。macOS会自动将其解压为应用程序<code>iTerm.app</code>,此时文件变为72.6MB。把该应用程序复制或直接拖拽到<code>Applications</code>目录,就会在Lauchpad见到iTerm的图标,点击即可启动。</p>\n<h4 id=\"功能概览\">功能概览</h4>\n<p>iTerm2功能特性十分丰富,全面的说明请看其<a href=\"https://iterm2.com/documentation.html\">在线文档</a>。这里简单描述一些新用户可以快速上手的特性:</p>\n<ul>\n<li><p><strong>分割页面(Split Panes)</strong>:使用<code>cmd-d</code>或<code>cmd-shift-d</code>快捷键,能迅速垂直或水平分割当前终端会话页面(Tab)。要在分割后的子窗口(Pane)之间游走,可以使用<code>cmd-opt-→/←/↑/↓</code>或<code>cmd-[/]</code>快捷键组合。快捷键<code>cmd-shift-enter</code>将当前子窗口最大化——页面其它子窗口全部被隐藏,再次按同样的快捷键恢复隐藏的子窗口。 快捷键<code>cmd-enter</code>可以让当前页面覆盖全屏,重复输入回到原来页面状态。</p></li>\n<li><p><strong>时间戳</strong>:切换<strong>View > Show Timestamps</strong>菜单选项,或者使用切换快捷键<code>cmd+shift+e</code>,可让iTerm2直接在终端窗口打印每一步操作的时间戳。比如对于单个<code>ping -c 5</code>命令的每一个重复的操作都会打印一个时间戳。这方便用户了解终端上的命令执行历史和时间间隔。</p></li>\n<li><p><strong>密码管理器</strong>:iTerm2可以将你的密码保存在macOS的钥匙串(Keychain)应用中。Use the <strong>Window > Password Manager</strong>菜单选项能打开密码管理器输入密码。</p></li>\n<li><p><strong>文本选择和搜索</strong>:可以使用鼠标快速选择记录缓冲区的文本,双击选择连续字段,三击选择当前行全部字符。选中即复制,无需再点鼠标右侧或按<code>cmd+c</code>复制到剪贴板。快捷键<code>cmd-f</code>打开搜索框,可以搜索整个记录缓冲区,还支持正则表达式输入(Regular Expression)。搜索匹配的内容会自动高亮显示。</p></li>\n<li><p><strong>快速打开</strong>:输出记录及缓冲区内的字段如果是URL、目录或文件名,将鼠标移到其上方并按住<code>cmd(⌘)</code>键,该字段会马上变成可点击状态。点击鼠标会自动用预设的应用(浏览器、Finder或Preview)打开对应的网址、目录或文件。</p></li>\n<li><p><strong>撤销关闭</strong>:如果不小心关闭了会话页面,在一定的时间内用户可以按<code>cmd-z</code>撤销并恢复页面。默认的时间是5秒钟。如果想加长延时,可以通过菜单路径<strong>Preferences > Profiles > Session</strong>修改。</p></li>\n<li><p><strong>Tmux集成</strong>:iTerm2与流行的开源终端复用器tmux紧密集成,可以让用户的tmux窗口获得iTerm2原生窗口或页面的操作特性,而tmux自身优秀功能也得到很好的保留。具体使用细节可参考<a href=\"https://iterm2.com/documentation-tmux-integration.html\">iTerm2-tmux集成文档</a>。</p></li>\n<li><p><strong>色彩主题</strong>:iTerm2外观色彩由菜单项<strong>Preferences > Profiles > Colors</strong>进行配置,用户可以通过<strong>Color Presets</strong>列表选择预设色彩主题。如果不喜欢所列的所有主题,可以上网下载其它iTerm2色彩主题再导入选择。如果对某个主题的个别颜色设定不满意,可以对其作单独调整并导出(Export)保存。</p></li>\n<li><p><strong>状态条</strong>:iTerm2提供了可配置及可由脚本控制的状态条,以便即时显示工作环境的状态信息。状态条通过菜单路径<strong>Preferences > Profiles > Session</strong>设置,点击复选框<strong>Status bar enabled</strong>开启状态条,再点击<strong>Configure Status Bar</strong>选择和配置要加入状态条的组件。iTerm2给出的常用组件有处理器和内存利用率、剩余电量、网络吞吐量、时钟、当前主机/用户/目录/任务名称、git状态、搜索框等。iTerm2还支持用户使用其Python API编写的自定义组件。组件选择和配置对话框里的<strong>Auto-Rainbow</strong>列表可让用户设置状态条组件的配色方案,一般选用Automatic(自动)即可。</p></li>\n</ul>\n<p>下面就是macOS自带的Terminal应用与iTerm2的页面快照比较。这里iTerm2使用的是个人比较喜欢<code>Solarized Dark</code>色彩主题,对比度不太强,眼睛不会太疲劳。注意iTerm2页面顶上状态条显示3个彩色组件:处理器利用率、内存利用率和网络吞吐量(蓝色为下载速率,红色为上载速率)。</p>\n<p><img src=\"Terminal-iTerm2.jpg\" style=\"width:70.0%;height:70.0%\" /></p>\n<h3 id=\"oh-my-zsh配置框架\">Oh-My-Zsh配置框架</h3>\n<p>Z shell(Zsh)是从Bourne shell改进出来的新一款命令解释器,同时它也加入了Bash、ksh及tcsh的某些功能。2019年自macOS Catalina(10.15版)起,苹果的Mac系统默认Shell以zsh取代之前的Bash。Zsh能定制的內容非常多,但是对新手来说设置过于复杂,因此一开始不太受人欢迎人。后来有人开发了一个配置套件,大大简化了定制设置的过程,zsh才由此真正变得流行起来。这一配置套件就是<a href=\"https://ohmyz.sh\">Oh-My-Zsh</a>。</p>\n<p>完整意义上,Oh-My-Zsh是一个建立于zsh之上的、用户社区驱动的开源管理配置框架。它综合了众多的功能插件和配置主题,并提供优选的初始化设置。截止于2021年11月,其<a href=\"https://github.com/ohmyzsh/ohmyzsh\">GitHub源站点</a>总共绑定了<a href=\"https://github.com/ohmyzsh/ohmyzsh/wiki/Plugins\">275+个插件</a>和<a href=\"https://github.com/ohmyzsh/ohmyzsh/wiki/Themes\">150种主题</a>,同时也带有更新已安装插件及主题的自动更新工具。</p>\n<h4 id=\"安装oh-my-zsh\">安装Oh-My-Zsh</h4>\n<p>新用户可以选择使用curl或wget工具安装,其命令行分别如下所示:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ sh -c <span class=\"string\">"<span class=\"subst\">$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)</span>"</span></span><br><span class=\"line\">$ sh -c <span class=\"string\">"<span class=\"subst\">$(wget https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh -O -)</span>"</span></span><br></pre></td></tr></table></figure>\n<p>安装过程的记录如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">~ % sh -c <span class=\"string\">"<span class=\"subst\">$(curl -fsSL https://raw.github.com/ohmyzsh/ohmyzsh/master/tools/install.sh)</span>"</span></span><br><span class=\"line\">Cloning Oh My Zsh...</span><br><span class=\"line\">Cloning into <span class=\"string\">'/Users/zixi/.oh-my-zsh'</span>...</span><br><span class=\"line\">remote: Enumerating objects: 1244, <span class=\"keyword\">done</span>.</span><br><span class=\"line\">remote: Counting objects: 100% (1244/1244), <span class=\"keyword\">done</span>.</span><br><span class=\"line\">remote: Compressing objects: 100% (1205/1205), <span class=\"keyword\">done</span>.</span><br><span class=\"line\">remote: Total 1244 (delta 19), reused 1178 (delta 18), pack-reused 0</span><br><span class=\"line\">Receiving objects: 100% (1244/1244), 877.00 KiB | 670.00 KiB/s, <span class=\"keyword\">done</span>.</span><br><span class=\"line\">Resolving deltas: 100% (19/19), <span class=\"keyword\">done</span>.</span><br><span class=\"line\"></span><br><span class=\"line\">Looking <span class=\"keyword\">for</span> an existing zsh config...</span><br><span class=\"line\">Using the Oh My Zsh template file and adding it to ~/.zshrc.</span><br><span class=\"line\"></span><br><span class=\"line\"> __ __ </span><br><span class=\"line\"> ____ / /_ ____ ___ __ __ ____ _____/ /_ </span><br><span class=\"line\"> / __ \\/ __ \\ / __ `__ \\/ / / / /_ / / ___/ __ \\ </span><br><span class=\"line\">/ /_/ / / / / / / / / / / /_/ / / /_(__ ) / / / </span><br><span class=\"line\">\\____/_/ /_/ /_/ /_/ /_/\\__, / /___/____/_/ /_/ </span><br><span class=\"line\"> /____/ ....is now installed!</span><br><span class=\"line\"></span><br><span class=\"line\"></span><br><span class=\"line\">Before you scream Oh My Zsh! look over the `.zshrc` file to select plugins, themes, and options.</span><br><span class=\"line\"></span><br><span class=\"line\">• Follow us on Twitter: https://twitter.com/ohmyzsh</span><br><span class=\"line\">• Join our Discord community: https://discord.gg/ohmyzsh</span><br><span class=\"line\">• Get stickers, t-shirts, coffee mugs and more: https://shop.planetargon.com/collections/oh-my-zsh</span><br></pre></td></tr></table></figure>\n<p>安装实际上从GitHub源站点克隆了Oh-My-Zsh到本机用户主目录下,生成.oh-my-zsh子目录。检查子目录内容可以看到Oh-My-Zsh仓储的git分支工作空间:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ ls -anl .oh-my-zsh</span><br><span class=\"line\">total 128</span><br><span class=\"line\">drwxr-xr-x 22 501 20 704 Nov 14 15:19 .</span><br><span class=\"line\">drwxr-xr-x+ 33 501 20 1056 Nov 22 22:30 ..</span><br><span class=\"line\">-rw-r--r-- 1 501 20 115 Nov 14 15:19 .editorconfig</span><br><span class=\"line\">drwxr-xr-x 13 501 20 416 Nov 22 21:49 .git</span><br><span class=\"line\">drwxr-xr-x 7 501 20 224 Nov 14 15:19 .github</span><br><span class=\"line\">-rw-r--r-- 1 501 20 77 Nov 14 15:19 .gitignore</span><br><span class=\"line\">-rw-r--r-- 1 501 20 131 Nov 14 15:19 .gitpod.Dockerfile</span><br><span class=\"line\">-rw-r--r-- 1 501 20 259 Nov 14 15:19 .gitpod.yml</span><br><span class=\"line\">-rw-r--r-- 1 501 20 3374 Nov 14 15:19 CODE_OF_CONDUCT.md</span><br><span class=\"line\">-rw-r--r-- 1 501 20 8281 Nov 14 15:19 CONTRIBUTING.md</span><br><span class=\"line\">-rw-r--r-- 1 501 20 1142 Nov 14 15:19 LICENSE.txt</span><br><span class=\"line\">-rw-r--r--@ 1 501 20 13448 Nov 14 15:19 README.md</span><br><span class=\"line\">-rw-r--r-- 1 501 20 705 Nov 14 15:19 SECURITY.md</span><br><span class=\"line\">drwxr-xr-x 6 501 20 192 Nov 14 15:19 cache</span><br><span class=\"line\">drwxr-xr-x 5 501 20 160 Nov 14 15:19 custom</span><br><span class=\"line\">drwxr-xr-x 21 501 20 672 Nov 14 15:19 lib</span><br><span class=\"line\">drwxr-xr-x 3 501 20 96 Nov 22 21:02 <span class=\"built_in\">log</span></span><br><span class=\"line\">-rw-r--r-- 1 501 20 5670 Nov 14 15:19 oh-my-zsh.sh</span><br><span class=\"line\">drwxr-xr-x 305 501 20 9760 Nov 14 15:19 plugins</span><br><span class=\"line\">drwxr-xr-x 3 501 20 96 Nov 14 15:19 templates</span><br><span class=\"line\">drwxr-xr-x 144 501 20 4608 Nov 14 15:19 themes</span><br><span class=\"line\">drwxr-xr-x 9 501 20 288 Nov 14 15:19 tools</span><br></pre></td></tr></table></figure>\n<p>而用户的zsh资源文件也被修改为指向Oh-My-Zsh子目录,默认加入<a href=\"https://github.com/ohmyzsh/ohmyzsh/tree/master/plugins/git\">git插件</a>和设置主题为robbyrussell,最后引导那里的oh-my-zsh.sh文件进行初始化:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">❯ grep <span class=\"string\">"^[^#;]"</span> .zshrc</span><br><span class=\"line\"><span class=\"built_in\">export</span> ZSH=<span class=\"string\">"/Users/zixi/.oh-my-zsh"</span></span><br><span class=\"line\">ZSH_THEME=<span class=\"string\">"robbyrussell"</span></span><br><span class=\"line\">plugins=(git)</span><br><span class=\"line\"><span class=\"built_in\">source</span> <span class=\"variable\">$ZSH</span>/oh-my-zsh.sh</span><br></pre></td></tr></table></figure>\n<h4 id=\"安装定制插件\">安装定制插件</h4>\n<p>定制插件即并非Oh-My-Zsh自身绑定的插件。这里安装两个非常好用的zsh定制插件:<a href=\"https://github.com/zsh-users/zsh-autosuggestions\">zsh-autosuggestions</a>和<a href=\"https://github.com/zsh-users/zsh-syntax-highlighting\">zsh-syntax-highlighting</a>。</p>\n<ol type=\"1\">\n<li><p><strong>zsh-autosuggestions</strong>:此插件在用户键入时,基于命令行历史和完整命令名推荐余下的输入。安装时先从GitHub源克隆仓储到<code>$ZSH_CUSTOM/plugin</code>目录(默认为~/.oh-my-zsh/custom/plugins)</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">git <span class=\"built_in\">clone</span> https://github.com/zsh-users/zsh-autosuggestions <span class=\"variable\">${ZSH_CUSTOM:-~/.oh-my-zsh/custom}</span>/plugins/zsh-autosuggestions</span><br></pre></td></tr></table></figure>\n<p>然后,编辑~/.zshrc文件把它加到插件列表</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">plugins=( </span><br><span class=\"line\"> <span class=\"comment\"># other plugins...</span></span><br><span class=\"line\"> zsh-autosuggestions</span><br><span class=\"line\">)</span><br></pre></td></tr></table></figure></li>\n<li><p><strong>zsh-syntax-highlighting</strong>:此插件对zsh提示符后的输入自动匹配语法高亮,可以让用户在实际命令运行前审核输入行,以便及时发现和改正语法错误。安装时同样先从GitHub源克隆仓储到<code>$ZSH_CUSTOM/plugin</code>目录(默认为~/.oh-my-zsh/custom/plugins)</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">git <span class=\"built_in\">clone</span> https://github.com/zsh-users/zsh-syntax-highlighting.git <span class=\"variable\">${ZSH_CUSTOM:-~/.oh-my-zsh/custom}</span>/plugins/zsh-syntax-highlighting</span><br></pre></td></tr></table></figure>\n<p>然后,编辑~/.zshrc文件把它加到插件列表</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">plugins=( </span><br><span class=\"line\"> <span class=\"comment\"># other plugins...</span></span><br><span class=\"line\"> zsh-syntax-highlighting</span><br><span class=\"line\">)</span><br></pre></td></tr></table></figure>\n<p>⚠️注意:与zsh-autosuggestions不同,为了保证语法高亮的效果,<strong>zsh-syntax-highlighting插件必须置于插件列表的最后一项!</strong></p></li>\n</ol>\n<p>定制插件安装完毕后,需要退出再重启iTerm2才能生效。</p>\n<h3 id=\"powerlevel10k主题\">Powerlevel10k主题</h3>\n<p><a href=\"https://github.com/romkatv/powerlevel10k\">Powerlevel10k</a>是为zsh定做的主题,它的显著特点是高速、灵活及开箱即用的用户体验。用Powerlevel10k定制的提示行包含色彩丰富的图形化符号,可在左右两边都实时显示当前系统和工作目录下的状态,还能与多种工具集成让程序员的工作环境产生惊艳的效果。</p>\n<p>在Oh-My-Zsh环境下的安装很简单,直接从Github源克隆Powerlevel10k仓储到<code>$ZSH_CUSTOM/themes</code>目录(默认为~/.oh-my-zsh/custom/themes)</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">git <span class=\"built_in\">clone</span> --depth=1 https://github.com/romkatv/powerlevel10k.git <span class=\"variable\">${ZSH_CUSTOM:-<span class=\"variable\">$HOME</span>/.oh-my-zsh/custom}</span>/themes/powerlevel10k</span><br></pre></td></tr></table></figure>\n<p>中国大陆用户可以使用 gitee.com 上的官方镜像加速下载:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">git <span class=\"built_in\">clone</span> --depth=1 https://gitee.com/romkatv/powerlevel10k.git <span class=\"variable\">${ZSH_CUSTOM:-<span class=\"variable\">$HOME</span>/.oh-my-zsh/custom}</span>/themes/powerlevel10k</span><br></pre></td></tr></table></figure>\n<p>接下来使用编辑器打开~/.zshrc文件,更新ZSH_THEME</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$ vi ~/.zshrc</span><br><span class=\"line\"><span class=\"comment\"># Set name of the theme to load</span></span><br><span class=\"line\">ZSH_THEME=<span class=\"string\">"powerlevel10k/powerlevel10k"</span>`</span><br></pre></td></tr></table></figure>\n<p>安装完成后重启iTerm2,会自动触发Powerlevel10k配置向导(Configuration Wizard)。这个向导设计得非常直观和人性化,一步一步引领用户设定界面,所选择的效果一目了然。Powerlevel10k配置向导采用问答的方式进行,大致的流程解说如下:</p>\n<ol type=\"1\">\n<li><p><strong>安装Powerline字体</strong> - 配置向导第一次运行时,会问用户是否需要安装推荐的字体。为了让提示行的图标正常显示,当然要选(y)。这时Powerlevel10k就自动下载<em>Meslo Nerd Font</em>并设置到iTerm2的Profile中,然后提示用户<code>cmd+q</code>关闭再重启iTerm2使其生效。用户可以打开<strong>Preferences > Profiles > Text > Font</strong>看到字体已经设定为<em>MesloLGS NF</em>。</p></li>\n<li><p><strong>确认特殊符号是否正常</strong> - 配置向导会与用户核对特殊图标是否能正常显示,如果第1步的字体安装无误,所有特殊符号和图标(菱形、🔒、<a href=\"https://zh.wikipedia.org/wiki/File:Debian-OpenLogo.svg\">Debian标识</a>及字符与图标相嵌)都应该正常呈现。</p></li>\n<li><p><strong>选择提示行风格</strong> - 有简约到丰富渲染程度不等的四种风格(Lean、Classic、Rainbow和Pure)可选,偏好因人而异。个人较喜欢彩虹(Rainbow)风格。</p></li>\n<li><p><strong>选择字符集</strong> - 有Unicode和ASCII两种选择。如果需要在提示行显示许多花俏的图标,一定要选Unicode。</p></li>\n<li><p><strong>其它风格细节设定</strong> - 包括是否在右端显示时间及何种时制、提示行内分隔符和末端的形狀、提示行与输入命令是否分隔成两行、命令执行结束记录上滚后是否去除提示行(Transient Prompt)等等。</p></li>\n</ol>\n<p>跟着Powerlevel10k的配置向导完成设置,你就能获得视效满满的终端提示行。在此之后如果想变一下口味,可以输入命令<code>p10k configure</code>启动配置向导重新设定。Powerlevel10k最后生成的配置存放在~/.p10k.zsh中,用户可以编辑此文件作细节微调。</p>\n<p>至此,全部必要的软件工具都已经安装配置完毕。最后的.zshrc文件的有效内容如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">> grep <span class=\"string\">"^[^#;]"</span> .zshrc</span><br><span class=\"line\"><span class=\"keyword\">if</span> [[ -r <span class=\"string\">"<span class=\"variable\">${XDG_CACHE_HOME:-<span class=\"variable\">$HOME</span>/.cache}</span>/p10k-instant-prompt-<span class=\"variable\">${(%):-%n}</span>.zsh"</span> ]]; <span class=\"keyword\">then</span></span><br><span class=\"line\"> <span class=\"built_in\">source</span> <span class=\"string\">"<span class=\"variable\">${XDG_CACHE_HOME:-<span class=\"variable\">$HOME</span>/.cache}</span>/p10k-instant-prompt-<span class=\"variable\">${(%):-%n}</span>.zsh"</span></span><br><span class=\"line\"><span class=\"keyword\">fi</span></span><br><span class=\"line\"><span class=\"built_in\">export</span> ZSH=<span class=\"string\">"/Users/zixi/.oh-my-zsh"</span></span><br><span class=\"line\">ZSH_THEME=<span class=\"string\">"powerlevel10k/powerlevel10k"</span></span><br><span class=\"line\">plugins=(</span><br><span class=\"line\"> git</span><br><span class=\"line\"> zsh-autosuggestions</span><br><span class=\"line\"> zsh-syntax-highlighting</span><br><span class=\"line\">)</span><br><span class=\"line\"><span class=\"built_in\">source</span> <span class=\"variable\">$ZSH</span>/oh-my-zsh.sh</span><br><span class=\"line\">[[ ! -f ~/.p10k.zsh ]] || <span class=\"built_in\">source</span> ~/.p10k.zsh</span><br></pre></td></tr></table></figure>\n<h3 id=\"综合调整和效果展示\">综合调整和效果展示</h3>\n<p>根据个人喜好的不同,用户可以对 iTerm2 + Oh-My-Zsh + Powerlevel10k 组合作一些整体调节,以打造自己满意的终端界面。个人使用的是iTerm2预设的<code>Solarized Dark</code>色彩主题。虽然它的颜色配置对眼睛较柔和,但缺点是无法看清Oh-My-Zsh自动推荐插件输出的内容。这是由于<code>Solarized Dark</code>色彩主题里<strong>Basic Colors > Background</strong>与<strong>ANSI Colors > Bright Black</strong>太接近造成的。矫正的方法是打开iTerm2设置对话框<strong>Preferences > Profiles > Colors</strong>,将<strong>ANSI Colors > Bright Black</strong>调亮一些。对<strong>ANSI Colors > Bright</strong>颜色列表的Green、Yellow、Blue及Cyan也可作相应的调整。矫正结果如下截屏所示</p>\n<p><img src=\"iTerm-profile-color.png\" style=\"width:70.0%;height:70.0%\" /></p>\n<p>此外,个人还对Powerlevel10k的配置文件~/.p10k.zsh做了了如下微小改动,将硬盘使用率(disk_usage)起始注释符号#去掉,以将其加到提示行右端实时显示:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">vi ~/.p10k.zsh</span><br><span class=\"line\"><span class=\"comment\">#</span></span><br><span class=\"line\"><span class=\"built_in\">typeset</span> -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> <span class=\"comment\"># load # CPU load</span></span><br><span class=\"line\"> disk_usage <span class=\"comment\"># disk usage</span></span><br><span class=\"line\"> <span class=\"comment\"># ram # free RAM</span></span><br><span class=\"line\"> ...</span><br><span class=\"line\"> time <span class=\"comment\"># current time</span></span><br><span class=\"line\"> <span class=\"comment\"># ip # ip address and bandwidth usage for a specified network interface</span></span><br><span class=\"line\"> <span class=\"comment\"># public_ip # public IP address</span></span><br><span class=\"line\"> <span class=\"comment\"># proxy # system-wide http/https/ftp proxy</span></span><br><span class=\"line\"> <span class=\"comment\"># battery # internal battery</span></span><br><span class=\"line\"> ...</span><br><span class=\"line\">)</span><br></pre></td></tr></table></figure>\n<p>注意这里load和ram与iTerm2页面状态条的处理器利用率和内存利用率组件显示的信息重复,所以不需要。如果对网络地址分配感兴趣,可以用同样的操作把以上配置段里的内网地址(ip)和公网地址(public_ip)加到提示行右端。</p>\n<p>最终,iTerm2 + Oh-My-Zsh + Powerlevel10k 生成的色彩生动的终端界面如下</p>\n<p><img src=\"iTerm-hexo-git-ping-search.png\" style=\"width:85.0%;height:85.0%\" /> 此图中展现出来的功能特性有:</p>\n<ol type=\"1\">\n<li><strong>语法高亮</strong>:ping、vi、hexo及gst(git status的别名)命令都用绿色显示。hexo和gst命令的输出也都有对应的色彩变化。</li>\n<li><strong>彩色状态条</strong>:顶上的状态条用不同颜色显示处理器利用率和内存利用率。</li>\n<li><strong>文本搜索</strong>:顶上状态条左侧搜索框显示查找\"git\"字符串的结果,在整个记录缓冲区内找到206个。在窗口内匹配的内容自动反色突出高亮显示。</li>\n<li><strong>彩虹提示行</strong>:底部是彩虹风格的提示行。左侧显示苹果图标、目录图标和路径,还有git分支名及工作空间状态指示(分支为master,“!1 ?1”表明一个现存文件被修改,及一个未被记录的新文件)。右侧第一个✓图标指示最近的命令运行成功,否则会显示✗及错误码;第二个图标显示上个ping命令的执行时间3s;下一个为硬盘使用率14%;最后是12小时制的当前时间。</li>\n<li><strong>自动推荐</strong>:底部输入vi命令时,根据命令行历史会显示自动推荐的文件名参数。这时键入<code>→</code>,光标会马上跳到参数末尾。如果要部分修改,可使用<code>delete</code>键左移光标到相关位置,再输入即可。注意光标前后的字符颜色的不同。后面是自动推荐部分,其颜色就是上述调整过的Bright Black。</li>\n</ol>\n<p>下面的页面截图,演示了iTerm2与tmux集成后双栏4个子窗口的运行效果:</p>\n<ul>\n<li>左上 - 运行htop实时监控系统信息</li>\n<li>右上 - <a href=\"https://github.com/dylanaraps/neofetch\">neofetch</a>(一种美化的系统快照工具)的执行结果,以及简单命令行脚本的语法高亮效果</li>\n<li>左下 - <a href=\"https://github.com/Peltoche/lsd\">lsd</a>(LSDeluxe—彩色精装版ls)的输出,其中目录和可执行文件分别为亮蓝色和亮绿色,这正是上述Bright Blue和Bright Green调整后的结果</li>\n<li>右下 - <a href=\"https://github.com/sharkdp/bat\">bat</a>(搭配语法高亮和Git集成的cat)的输出</li>\n</ul>\n<p><img src=\"iTerm-tmux.png\" style=\"width:90.0%;height:90.0%\" /> 很显然,iTerm2 + Oh-My-Zsh + Powerlevel10k 组合实现的终端工作环境能够充分地发挥色彩丰富的各类应用的巨大潜力。</p>\n","categories":["工具使用"],"tags":["编程工具"]},{"title":"巧解picoCTF的RSA挑战题Sum-O-Primes","url":"/2022/06/17/picoCTF-Sum-O-Primes/","content":"<p>一个偶然的机会,接触到一道picoCTF的RSA挑战题Sum-O-Primes。这道题不难,了解RSA的基本算法就能做出来。另外,如果熟悉RSA算法演变的历史,还能找到第二种巧妙的快速解法。<span id=\"more\"></span></p>\n<figure>\n<img src=\"https://imgs.xkcd.com/comics/security.png\" alt=\"xkcd-security\" /><figcaption aria-hidden=\"true\">xkcd-security</figcaption>\n</figure>\n<h3 id=\"picoctf项目\">picoCTF项目</h3>\n<p><a href=\"https://picoctf.org/\">picoCTF</a>是由卡内基梅隆大学的安全和隐私专家创建的一个免费的计算机安全教育项目,其通过建立于CTF(Capture the Flag,夺旗赛)框架之上的原创内容,以各种挑战的形式,为参与者提供系统学习网络安全知识并获取实践经验的宝贵机会。</p>\n<p>picoCTF的练习题集合被称为picoGym。通用的题解是从给定的信息中搜索或破解出格式为<strong>“picoCTF{...}”</strong>的字符串,即要夺取的旗标。如下图所示,当前picoGym包含了271道网络安全挑战练习,内容涵盖一般技能、密码学、逆向工程、取证等领域。</p>\n<p><img src=\"picoGym.png\" style=\"width:40.0%;height:40.0%\" /></p>\n<h3 id=\"sum-o-primes挑战\">Sum-O-Primes挑战</h3>\n<p>picoGym中密码学相关的挑战题有50道,其中一个就是Sum-O-Primes。此挑战的谜面很简单,其说明如下:</p>\n<blockquote>\n<p>We have so much faith in RSA we give you not just the product of the primes, but their sum as well!</p>\n<ul>\n<li><a href=\"https://artifacts.picoctf.net/c/180/gen.py\">gen.py</a></li>\n<li><a href=\"https://artifacts.picoctf.net/c/180/output.txt\">output.txt</a></li>\n</ul>\n</blockquote>\n<p>就是说我们不仅给出RSA所用的两个素数的乘积,还告诉你它们的和。具体是怎么给出的呢?这需要解题者自己去从剩下的信息中去发掘。点击给出的两个链接下载后,打开第一个Python文件:</p>\n<figure class=\"highlight python\"><figcaption><span>gen.py</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">#!/usr/bin/python</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">from</span> binascii <span class=\"keyword\">import</span> hexlify</span><br><span class=\"line\"><span class=\"keyword\">from</span> gmpy2 <span class=\"keyword\">import</span> mpz_urandomb, next_prime, random_state</span><br><span class=\"line\"><span class=\"keyword\">import</span> math</span><br><span class=\"line\"><span class=\"keyword\">import</span> os</span><br><span class=\"line\"><span class=\"keyword\">import</span> sys</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"keyword\">if</span> sys.version_info < (<span class=\"number\">3</span>, <span class=\"number\">9</span>):</span><br><span class=\"line\"> <span class=\"keyword\">import</span> gmpy2</span><br><span class=\"line\"> math.gcd = gmpy2.gcd</span><br><span class=\"line\"> math.lcm = gmpy2.lcm</span><br><span class=\"line\"></span><br><span class=\"line\">FLAG = <span class=\"built_in\">open</span>(<span class=\"string\">'flag.txt'</span>).read().strip()</span><br><span class=\"line\">FLAG = <span class=\"built_in\">int</span>(hexlify(FLAG.encode()), <span class=\"number\">16</span>)</span><br><span class=\"line\">SEED = <span class=\"built_in\">int</span>(hexlify(os.urandom(<span class=\"number\">32</span>)).decode(), <span class=\"number\">16</span>)</span><br><span class=\"line\">STATE = random_state(SEED)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">get_prime</span>(<span class=\"params\">bits</span>):</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> next_prime(mpz_urandomb(STATE, bits) | (<span class=\"number\">1</span> << (bits - <span class=\"number\">1</span>)))</span><br><span class=\"line\"></span><br><span class=\"line\">p = get_prime(<span class=\"number\">1024</span>)</span><br><span class=\"line\">q = get_prime(<span class=\"number\">1024</span>)</span><br><span class=\"line\"></span><br><span class=\"line\">x = p + q</span><br><span class=\"line\">n = p * q</span><br><span class=\"line\"></span><br><span class=\"line\">e = <span class=\"number\">65537</span></span><br><span class=\"line\"></span><br><span class=\"line\">m = math.lcm(p - <span class=\"number\">1</span>, q - <span class=\"number\">1</span>)</span><br><span class=\"line\">d = <span class=\"built_in\">pow</span>(e, -<span class=\"number\">1</span>, m)</span><br><span class=\"line\"></span><br><span class=\"line\">c = <span class=\"built_in\">pow</span>(FLAG, e, n)</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'x = <span class=\"subst\">{x:x}</span>'</span>)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'n = <span class=\"subst\">{n:x}</span>'</span>)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">f'c = <span class=\"subst\">{c:x}</span>'</span>)</span><br></pre></td></tr></table></figure>\n<p>如果你具备初级Python编程技能,并了解RSA算法的基本原理,应该可以很快读懂上面的程序。它所做的事情是:</p>\n<ol type=\"1\">\n<li>打开旗帜文件<code>flag.txt</code>读入内容。再使用<code>hexlify</code>和<code>int</code>函数将其转换为整数,结果存入变量<code>FLAG</code>。</li>\n<li>调用<code>get_prime</code>函数生成两个素数<code>p</code>和<code>q</code>,二者之和存入<code>x</code>,之积存入<code>n</code>。然后取<code>e</code>值为65537,计算出RSA私钥<code>d</code>。</li>\n<li>运用标准的<code>pow</code>函数执行模幂运算,实现RSA加密,将明文<code>FLAG</code>加密为密文<code>c</code>。</li>\n<li>打印输出<code>x</code>、<code>n</code>和<code>c</code>。</li>\n</ol>\n<p>打开第二个文件,显然它就是Python第一个程序输出的结果:</p>\n<figure class=\"highlight bash\"><figcaption><span>output.txt</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\">x = 154ee809a4dc337290e6a4996e0717dd938160d6abfb651736d9f5d524812a659b310ad1f221196ee8ab187fa746a1b488a4079cddfc5db08e78be0d96c83c01e9bb42420b40d6f0ad9f220633459a6dc058bb01c517386bfbd2d4811c9b08558b0e05534768581a74884758d15e15b4ef0dbd6a338bf1f52eed4f137957737d2</span><br><span class=\"line\">n = 6ce91e471f1df651b0d275d6d5522703feecdd77e7821a2caf9514104c059781c1b2e64772d9220addd657ecbd4e6cb8b5941608f6ab54bd5760074a5cd5854920439422192d2ee8912f1ebcc0d97714f209ee2a22e2da60e071541cb7e0772373cfea71831673378ee6432e63abfd14db0d4aa601928923253f9edd419ce96f4d68ce0aa3e6d6b530cd46eefbdac93038ce949c9dd2e573a47471cf8223f88b96e00a92f4d47fd277c42c4075b5e99b41a9f279f442bc0d533b9ddc50592e369e7026b3f7afaa8edf8972f0c3055f4de67a0eea963f099a32e1539de1d1727abadd9235f66371998ec883d1f89b8d907270842818cae49cd5c7f906c4752e81</span><br><span class=\"line\">c = 48b89662b9718fb391c96527272bf74c27810edaca09b63e694af9d11608010b1db9aedd1c867849371121941a1ccac610f7b28b92fa2f981babe816e6d3ecfab83514ed7e18e2b23fc3b96c7002ff47da897e9f2a9cb1b4e245396589e0b72affb73568a2016031555d2a46557919e44a15cd43fe9e1881d40dce1d1e36625e63b1472d3c317898102943072e06d79688c96b6ee2e584002c66497a9cdc48c38aa0548a7bc4fed9b4c23fcd493f38ece68788ef37a559b7f20c6941fcf8e567d9f50807259a7f11fa7a01d3125a1f7609cd94781f224ec8351605354b11c6b078fe015826342c3271ee3af4b99bb0a538b1e6b845594ee6546be8abd22ef2bd</span><br></pre></td></tr></table></figure>\n<p>理解了题意,就能马上做出判断——如果能从密文<code>c</code>解密出<code>FLAG</code>,就可以得到原始<code>flag.txt</code>的内容,即夺旗。</p>\n<h3 id=\"常规解法\">常规解法</h3>\n<p>RSA解密需要私钥指数<span class=\"math inline\">\\(d\\)</span>。参考下面的RSA算法步骤,很明显这要求先解出大素数<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>。</p>\n<ol type=\"1\">\n<li>选择两个大素数 <span class=\"math inline\">\\(p\\)</span> 和 <span class=\"math inline\">\\(q\\)</span>,计算 <span class=\"math inline\">\\(n=pq\\)</span></li>\n<li>计算 <span class=\"math inline\">\\(n\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/卡邁克爾函數\">卡迈克尔函数</a> <span class=\"math inline\">\\(\\lambda(pq)=\\operatorname{lcm}(p − 1, q − 1)\\)</span>,<span class=\"math inline\">\\(\\operatorname{lcm}\\)</span> 是求最小公倍数的函数</li>\n<li>选择一个小于 <span class=\"math inline\">\\(\\lambda(n)\\)</span> 且与之互素的数 <span class=\"math inline\">\\(e\\)</span>,并求得 <span class=\"math inline\">\\(e\\)</span> 关于 <span class=\"math inline\">\\(\\lambda(n)\\)</span> 的<a href=\"https://zh.wikipedia.org/zh-cn/模反元素\">模逆元</a> <span class=\"math inline\">\\(d\\equiv e^{-1}\\pmod {\\lambda(n)}\\)</span></li>\n<li><span class=\"math inline\">\\((n,e)\\)</span> 是RSA公钥,<span class=\"math inline\">\\((n,d)\\)</span> 是RSA私钥</li>\n<li>使用公钥将明文<span class=\"math inline\">\\(m\\)</span>加密的计算公式是 <span class=\"math inline\">\\(c\\equiv m^e\\pmod n\\)</span></li>\n<li>使用私钥将密文<span class=\"math inline\">\\(c\\)</span>解密的计算公式是 <span class=\"math inline\">\\(m\\equiv c^d\\pmod n\\)</span></li>\n</ol>\n<p>由此,挑战题转化为已知两个大素数的乘积<span class=\"math inline\">\\(n\\)</span>与和<span class=\"math inline\">\\(x\\)</span>,求这两个大素数。即求解二元一次方程组 <span class=\"math display\">\\[\n\\left\\{\n\\begin{aligned}\np+q &=n \\\\ \np*q &=x\n\\end{aligned} \n\\right. \n\\]</span> 运用初等数学的知识,可将以上的方程组变成一元二次方程` 显然<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>就是它的两个解。根据解题公式得到 <span class=\"math display\">\\[(p,q)={\\frac {x}{2}}\\pm {\\sqrt {\\left({\\frac {x}{2}}\\right)^{2}-n}}\\]</span> 得到<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>,余下的工作就很简单了。由<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>算出<span class=\"math inline\">\\(d\\)</span>的代码可以直接从gen.py中第28、30和31行复制过来。最后的完整Python解题代码如下:</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">import</span> math</span><br><span class=\"line\"></span><br><span class=\"line\">file = <span class=\"built_in\">open</span>(<span class=\"string\">'output.txt'</span>, <span class=\"string\">'r'</span>)</span><br><span class=\"line\">Lines = file.readlines()</span><br><span class=\"line\">file.close()</span><br><span class=\"line\"></span><br><span class=\"line\">x = <span class=\"built_in\">int</span>((Lines[<span class=\"number\">0</span>].split())[<span class=\"number\">2</span>], <span class=\"number\">16</span>) <span class=\"comment\"># x = p + q</span></span><br><span class=\"line\">n = <span class=\"built_in\">int</span>((Lines[<span class=\"number\">1</span>].split())[<span class=\"number\">2</span>], <span class=\"number\">16</span>) <span class=\"comment\"># n = p * q</span></span><br><span class=\"line\">c = <span class=\"built_in\">int</span>((Lines[<span class=\"number\">2</span>].split())[<span class=\"number\">2</span>], <span class=\"number\">16</span>) <span class=\"comment\"># Ciphertext</span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">def</span> <span class=\"title\">solve_rsa_primes</span>(<span class=\"params\">s: <span class=\"built_in\">int</span>, m: <span class=\"built_in\">int</span></span>) -> <span class=\"built_in\">tuple</span>:</span></span><br><span class=\"line\"> <span class=\"string\">'''</span></span><br><span class=\"line\"><span class=\"string\"> Solve RSA prime numbers (p, q) from the quadratic equation</span></span><br><span class=\"line\"><span class=\"string\"> p^2 - s * p + m = 0 with the formula p = s/2 +/- sqrt((s/2)^2 - m)</span></span><br><span class=\"line\"><span class=\"string\"></span></span><br><span class=\"line\"><span class=\"string\"> Input: s - sum of primes, m - product of primes</span></span><br><span class=\"line\"><span class=\"string\"> Output: (p, q)</span></span><br><span class=\"line\"><span class=\"string\"> '''</span></span><br><span class=\"line\"> half_s = s >> <span class=\"number\">1</span></span><br><span class=\"line\"> tmp = math.isqrt(half_s ** <span class=\"number\">2</span> - m)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"built_in\">int</span>(half_s + tmp), <span class=\"built_in\">int</span>(half_s - tmp); </span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"comment\"># Now run with the real input</span></span><br><span class=\"line\">p, q = solve_rsa_primes(x, n)</span><br><span class=\"line\">m = math.lcm(p - <span class=\"number\">1</span>, q - <span class=\"number\">1</span>)</span><br><span class=\"line\">e = <span class=\"number\">65537</span></span><br><span class=\"line\">d = <span class=\"built_in\">pow</span>(e, -<span class=\"number\">1</span>, m)</span><br><span class=\"line\">FLAG = <span class=\"built_in\">pow</span>(c, d, n)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(FLAG.to_bytes((FLAG.bit_length() + <span class=\"number\">7</span>) // <span class=\"number\">8</span>, <span class=\"string\">'big'</span>))</span><br></pre></td></tr></table></figure>\n<p>上面的程序定义了一个通用的函数<code>solve_rsa_primes</code>求解两个大素数。在得出<span class=\"math inline\">\\(d\\)</span>之后,同样调用<code>pow</code>函数解密,最后将明文从大的整数转化为字节序列并打印输出。程序运行的结果是</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"string\">b'picoCTF{pl33z_n0_g1v3_c0ngru3nc3_0f_5qu4r35_92fe3557}'</span></span><br></pre></td></tr></table></figure>\n<p>BINGO!夺旗成功!</p>\n<div class=\"note warning\"><p><strong>注意:</strong> 函数<code>solve_rsa_primes</code>调用了<code>math.isqrt</code>计算整数平方根,这是必须的!如果误写成<code>math.sqrt</code>,就会出现如下的溢出错误</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">>>></span><br><span class=\"line\">=============== RESTART: /Users/zixi/Downloads/Sum-O-Primes.py ==============</span><br><span class=\"line\">Traceback (most recent call last):</span><br><span class=\"line\"> File <span class=\"string\">"/Users/zixi/Downloads/Sum-O-Primes.py"</span>, line <span class=\"number\">35</span>, <span class=\"keyword\">in</span> <module></span><br><span class=\"line\"> p, q = solve_rsa_primes(x, n)</span><br><span class=\"line\"> File <span class=\"string\">"/Users/zixi/Downloads/Sum-O-Primes.py"</span>, line <span class=\"number\">31</span>, <span class=\"keyword\">in</span> solve_rsa_primes</span><br><span class=\"line\"> tmp = math.sqrt(<span class=\"built_in\">int</span>(half_s ** <span class=\"number\">2</span> - m))</span><br><span class=\"line\">OverflowError: <span class=\"built_in\">int</span> too large to convert to <span class=\"built_in\">float</span></span><br></pre></td></tr></table></figure>\n<p>这是因为<code>math.sqrt</code>是使用浮点数运算,但这里它无法将大的整数转换为浮点数。</p>\n</div>\n<h3 id=\"快速解法\">快速解法</h3>\n<p>本题的常规解法要解一元二次方程,所以整数平方根运算必不可少。那么有不需要平方根运算的解法吗?答案是肯定的。</p>\n<p>在<a href=\"http://people.csail.mit.edu/rivest/Rsapaper.pdf\">原始的RSA论文</a>中,公钥指数<span class=\"math inline\">\\(e\\)</span>和私钥指数<span class=\"math inline\">\\(d\\)</span>之间的关系是 <span class=\"math display\">\\[d⋅e≡1\\pmod{\\varphi(n)}\\]</span> 这里模数为欧拉函数<span class=\"math inline\">\\(\\varphi(n)=(p-1)(q-1)\\)</span>。由于<span class=\"math inline\">\\(\\varphi(N)\\)</span>总是能被<span class=\"math inline\">\\(\\lambda(n)\\)</span>整除,任何满足上式的<span class=\"math inline\">\\(d\\)</span>也满足<span class=\"math inline\">\\(d⋅e≡1\\pmod{\\lambda(n)}\\)</span>,所以<strong>私钥指数<span class=\"math inline\">\\(d\\)</span>并不唯一</strong>。虽然这时算出的<span class=\"math inline\">\\(d>\\lambda(n)\\)</span>,但是应用到Sum-O-Primes题解,却可以避免平方根运算。这是因为</p>\n<p><span class=\"math display\">\\[\n\\begin{aligned}\n\\varphi(n)&=(p-1)(q-1)\\\\\n&=pq-(p+q)+1\\\\\n&=n-x+1\n\\end{aligned}\n\\]</span> 由此私钥指数<span class=\"math inline\">\\(d\\)</span>的计算公式变成 <span class=\"math display\">\\[\n\\begin{aligned}\nd&≡e^{-1}\\pmod{\\varphi(n)}\\\\\n&≡e^{-1}\\pmod{(n-x+1)}\n\\end{aligned}\n\\]</span> 既然<span class=\"math inline\">\\(n\\)</span>和<span class=\"math inline\">\\(x\\)</span>都是现成的,这种方法完全不必求出<span class=\"math inline\">\\(p\\)</span>和<span class=\"math inline\">\\(q\\)</span>,自然也就不需要平方根运算。此新解法的Python代码非常简洁</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">d1 = <span class=\"built_in\">pow</span>(e, -<span class=\"number\">1</span>, n - x + <span class=\"number\">1</span>)</span><br><span class=\"line\">FLAG = <span class=\"built_in\">pow</span>(c, d1, n)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(FLAG.to_bytes((FLAG.bit_length() + <span class=\"number\">7</span>) // <span class=\"number\">8</span>, <span class=\"string\">'big'</span>))</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"d = "</span>, d)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"d1 = "</span>, d1)</span><br><span class=\"line\"><span class=\"keyword\">assert</span>(d1>d)</span><br><span class=\"line\"><span class=\"built_in\">print</span>(<span class=\"string\">"d1/d = "</span>, d1/d)</span><br></pre></td></tr></table></figure>\n<p>为了比较两种解法求出的<span class=\"math inline\">\\(d\\)</span>的不同,在末尾还加上了4行打印和断言语句。代码的执行结果是</p>\n<figure class=\"highlight python\"><table><tr><td class=\"code\"><pre><span class=\"line\">>>></span><br><span class=\"line\">=============== RESTART: /Users/zixi/Downloads/Sum-O-Primes.py ==============</span><br><span class=\"line\"><span class=\"string\">b'picoCTF{pl33z_n0_g1v3_c0ngru3nc3_0f_5qu4r35_92fe3557}'</span></span><br><span class=\"line\">d = <span class=\"number\">1590433953643304448870807755026766943237397482033766155980367645454600169745357277163199312196609495875891431590581528929277583062406061101224041553945564552302546648687338536694903918084325519368961617691238793972703013656395301935576994660878296156727353260699130612675943209520489312860964899655070852366584778594425834982623831654304915478835573020874834723387183369976749895237126850604587166433366381884290402338703266523462767765540527102747754912478720160791675179128443712374832507705614160658601242723842366612805686436771142338154848447759947887908800687914418476358484536216953925324788380823429735298973</span></span><br><span class=\"line\">d1 = <span class=\"number\">11901952834426939436403812982514571575614906347331071933175950931208083895179963694981295931167346168378938101218143770786299673201984563299831132533757316974157649670783507276616478666261648674806749337918514985951832847720617452268824430679672778783943236259522437088812130196067329355430038927225825521934485847159262037514154059696664148362902872186817856316128403800463106817000251243818717005827615275821709043532925457271839955998044684537152992871171338447136672661193487297988293156428071068861346467230927990425182893890027896377626007826573834588309038513191969376781172191621785853174152547091371818954913</span></span><br><span class=\"line\">d1/d = <span class=\"number\">7.483462489694971</span></span><br></pre></td></tr></table></figure>\n<p>如上所示,此解法亦成功夺旗。新解法算出的<span class=\"math inline\">\\(d\\)</span>值(d1)是常规解法的7倍多。</p>\n<p>本文全部的代码点击这里下载:<a href=\"Sum-O-Primes.py.gz\">Sum-O-Primes.py.gz</a></p>\n","categories":["技术小札"],"tags":["密码学","Python编程","CTF"]},{"title":"嵌入式Linux系统中uClibc标准库使用经验笔记","url":"/2022/12/23/uClibc-tips/","content":"<p><a href=\"https://www.uclibc.org\">uClibc</a>是一个面向嵌入式Linux系统的小巧而精致的C标准库,在低配置的嵌入式系统及物联网设备的研发中应用广泛。这里分享最近的一些使用经验,为需要解决相似问题或需求的工程师提供便利。<span id=\"more\"></span></p>\n<div class=\"note success no-icon\"><p><strong>To me programming is more than an important practical art. It is also a gigantic undertaking in the foundations of knowledge.</strong><br> <strong>— <em>Grace Hopper</em>(格蕾丝·赫柏,美国计算机科学家、海军准将,创造了现代第一个编译器A-0系统,以及第一个高级商用计算机程序语言“COBOL”,被誉为“COBOL之母”)</strong></p>\n</div>\n<h3 id=\"uclibc简介\">uClibc简介</h3>\n<p>uClibc(有时写成μClibc)是一个小型的C标准库,旨在为使用基于Linux内核的操作系统的嵌入式系统和移动设备提供支持。uClibc原先是为了支持不需要内存管理单元的Linux版本μClinux的开发的,因此特别适合于微控制器系统。其名称中“uC”就是微控制器英文microcontroller的缩写,u是代表微小(\"micro\")的希腊字母μ的近似拉丁字母排版。</p>\n<p>uClibc是在GNU Lesser GPL下授权的自由和开源软件,其库函数封装了Linux内核的系统调用。它可以在标准或无MMU的Linux系统上运行,支持i386、x86-64、ARM、MIPS,和PowerPC等众多处理器。uClibc的开发始于1999年,大部分是从零开始编写的,但也吸收了glibc和其他项目的代码。uClibc的比glibc小得多,glibc的目标是在广泛的硬件和内核平台上完全支持所有相关的C标准,而uClibc则侧重于嵌入式Linux系统。它还允许开发者根据内存空间设计要求来开启或禁用一些功能。</p>\n<p>下面的记录显示在两个相似的嵌入式系统中C标准库文件的列表。第一个使用glibc-2.23版,第二个集成uClibc-0.9.33.2版。总计glibc库文件为2MB多,而uClibc库文件加起来不到1MB。可见使用uClibc确实节省了不少存储空间。</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">STM1:/<span class=\"comment\"># find . -name "*lib*2.23*" | xargs ls -alh</span></span><br><span class=\"line\">-rwxr-xr-x 1 root root 9.6K Jan 1 1970 ./lib/libanl-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 1.1M Jan 1 1970 ./lib/libc-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 177.5K Jan 1 1970 ./lib/libcidn-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 29.5K Jan 1 1970 ./lib/libcrypt-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 9.5K Jan 1 1970 ./lib/libdl-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 429.4K Jan 1 1970 ./lib/libm-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 65.8K Jan 1 1970 ./lib/libnsl-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 17.5K Jan 1 1970 ./lib/libnss_dns-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 33.6K Jan 1 1970 ./lib/libnss_files-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 90.5K Jan 1 1970 ./lib/libpthread-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 65.7K Jan 1 1970 ./lib/libresolv-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 25.9K Jan 1 1970 ./lib/librt-2.23.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 9.5K Jan 1 1970 ./lib/libutil-2.23.so</span><br><span class=\"line\"></span><br><span class=\"line\">STM2:/<span class=\"comment\"># find . -name "*lib*0.9.33*" | xargs ls -alh</span></span><br><span class=\"line\">-rwxr-xr-x 1 root root 28.0K Jan 1 1970 ./lib/ld-uClibc-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 36.1K Jan 1 1970 ./lib/libcrypt-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 16.2K Jan 1 1970 ./lib/libdl-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 72.1K Jan 1 1970 ./lib/libm-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 116.4K Jan 1 1970 ./lib/libpthread-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 16.2K Jan 1 1970 ./lib/librt-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 28.3K Jan 1 1970 ./lib/libthread_db-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 621.4K Jan 1 1970 ./lib/libuClibc-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 8.1K Jan 1 1970 ./lib/libubacktrace-0.9.33.2.so</span><br><span class=\"line\">-rwxr-xr-x 1 root root 4.1K Jan 1 1970 ./lib/libutil-0.9.33.2.so</span><br></pre></td></tr></table></figure>\n<h3 id=\"ipv6和接口api\">IPv6和接口API</h3>\n<p>随着IPv6的使用量稳步增长,为嵌入式系统添加IPv6协议栈支持已经成为必需。在一个为使用uClibc的设备加入IPv4/IPv6双栈功能的软件项目中,发现出现应用程序链接错误——<code>undefined reference to getifaddrs</code>。<code>getifaddrs()</code>是一个非常有用的函数,我们可以调用它得到系统所有的网络接口的地址信息。查询Linux编程手册:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\">SYNOPSIS</span><br><span class=\"line\"> <span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><sys/types.h></span></span></span><br><span class=\"line\"> <span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><ifaddrs.h></span></span></span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"function\"><span class=\"keyword\">int</span> <span class=\"title\">getifaddrs</span><span class=\"params\">(struct ifaddrs **ifap)</span></span>;</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> \t </span><br><span class=\"line\"> <span class=\"function\">DESCRIPTION</span></span><br><span class=\"line\"><span class=\"function\"> The <span class=\"title\">getifaddrs</span><span class=\"params\">()</span> function creates a linked <span class=\"built_in\">list</span> of structures</span></span><br><span class=\"line\"><span class=\"function\"> describing the network interfaces of the local system, <span class=\"keyword\">and</span> stores</span></span><br><span class=\"line\"><span class=\"function\"> the address of the first item of the <span class=\"built_in\">list</span> in *ifap.</span></span><br><span class=\"line\"><span class=\"function\"> ...</span></span><br><span class=\"line\"><span class=\"function\"> </span></span><br><span class=\"line\"><span class=\"function\"> VERSIONS</span></span><br><span class=\"line\"><span class=\"function\"> The <span class=\"title\">getifaddrs</span><span class=\"params\">()</span> function first appeared in glibc 2.3, but before</span></span><br><span class=\"line\"><span class=\"function\"> glibc 2.3.3, the implementation supported only IPv4 addresses</span>;</span><br><span class=\"line\"> IPv6 support was added in glibc <span class=\"number\">2.3</span><span class=\"number\">.3</span>. Support of address</span><br><span class=\"line\"> families other than IPv4 is available only on kernels that</span><br><span class=\"line\"> support netlink.</span><br><span class=\"line\"> ...</span><br></pre></td></tr></table></figure>\n<p>上面的最后一句话很关键:<strong>只有支持netlink的内核才能支持IPv4以外的地址系列</strong>。此系统运行的Linux内核版本是3.x, 是支持netlink的。那么,会不会是uClibc对netlink的支持出现问题导致了<code>getifaddrs()</code>函数没有被编译呢?</p>\n<p>带着这一疑问,搜索uClibc的源码目录,找到实现<code>getifaddrs()</code>函数的C文件:</p>\n<figure class=\"highlight c\"><figcaption><span>libc/inet/ifaddrs.c</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\">...</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">if</span> __ASSUME_NETLINK_SUPPORT</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifdef</span> __UCLIBC_SUPPORT_AI_ADDRCONFIG__</span></span><br><span class=\"line\"><span class=\"comment\">/* struct to hold the data for one ifaddrs entry, so we can allocate</span></span><br><span class=\"line\"><span class=\"comment\"> everything at once. */</span></span><br><span class=\"line\"><span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">ifaddrs_storage</span></span></span><br><span class=\"line\"><span class=\"class\">{</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">ifaddrs</span> <span class=\"title\">ifa</span>;</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">union</span></span></span><br><span class=\"line\"><span class=\"class\"> {</span></span><br><span class=\"line\"> <span class=\"comment\">/* Save space for the biggest of the four used sockaddr types and</span></span><br><span class=\"line\"><span class=\"comment\"> avoid a lot of casts. */</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr</span> <span class=\"title\">sa</span>;</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr_ll</span> <span class=\"title\">sl</span>;</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr_in</span> <span class=\"title\">s4</span>;</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifdef</span> __UCLIBC_HAS_IPV6__</span></span><br><span class=\"line\"> <span class=\"class\"><span class=\"keyword\">struct</span> <span class=\"title\">sockaddr_in6</span> <span class=\"title\">s6</span>;</span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\"> } addr, netmask, broadaddr;</span><br><span class=\"line\"> <span class=\"keyword\">char</span> name[IF_NAMESIZE + <span class=\"number\">1</span>];</span><br><span class=\"line\">};</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span> <span class=\"comment\">/* __UCLIBC_SUPPORT_AI_ADDRCONFIG__ */</span></span></span><br><span class=\"line\">...</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifdef</span> __UCLIBC_SUPPORT_AI_ADDRCONFIG__</span></span><br><span class=\"line\">...</span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">int</span></span></span><br><span class=\"line\"><span class=\"function\"><span class=\"title\">getifaddrs</span> <span class=\"params\">(struct ifaddrs **ifap)</span></span></span><br><span class=\"line\"><span class=\"function\">...</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span> <span class=\"comment\">/* __UCLIBC_SUPPORT_AI_ADDRCONFIG__ */</span></span></span></span><br><span class=\"line\"><span class=\"function\">...</span></span><br><span class=\"line\"><span class=\"function\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span> <span class=\"comment\">/* __ASSUME_NETLINK_SUPPORT */</span></span></span></span><br></pre></td></tr></table></figure>\n<p>果不其然!整个函数的实现和相关数据结构<code>ifaddrs_storage</code>的定义,都被置于三个嵌套的宏条件编译下:</p>\n<ol type=\"1\">\n<li>__ASSUME_NETLINK_SUPPORT</li>\n<li>__UCLIBC_SUPPORT_AI_ADDRCONFIG__</li>\n<li>__UCLIBC_HAS_IPV6__</li>\n</ol>\n<p>所以,只要将它们对应的配置行打开就应该能解决问题。如下改动uClibc的配置文件后,重建uClibc的动态链接库,再make应用程序就成功了:</p>\n<figure class=\"highlight diff\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">--- a/toolchain/uClibc/config-0.9.33.2/common</span></span><br><span class=\"line\"><span class=\"comment\">+++ b/toolchain/uClibc/config-0.9.33.2/common</span></span><br><span class=\"line\"><span class=\"meta\">@@ -147,7 +147,8 @@</span> UCLIBC_HAS_RPC=y</span><br><span class=\"line\"> UCLIBC_HAS_FULL_RPC=y</span><br><span class=\"line\"><span class=\"deletion\">-# UCLIBC_HAS_IPV6 is not set</span></span><br><span class=\"line\"><span class=\"addition\">+UCLIBC_HAS_IPV6=y</span></span><br><span class=\"line\"><span class=\"deletion\">-# UCLIBC_USE_NETLINK is not set</span></span><br><span class=\"line\"><span class=\"addition\">+UCLIBC_USE_NETLINK=y</span></span><br><span class=\"line\"><span class=\"addition\">+UCLIBC_SUPPORT_AI_ADDRCONFIG=y</span></span><br><span class=\"line\"> UCLIBC_HAS_BSD_RES_CLOSE=y</span><br></pre></td></tr></table></figure>\n<h3 id=\"sha-2散列函数\">SHA-2散列函数</h3>\n<p>嵌入式系统常常需要为系统管理员提供远程SSH登录服务,这就必须创建系统用户及其密码。Linux将用户名和密码的散列值(hash)保存在 /etc/shadow 文件中。散列值的存储格式遵循称为模块化加密格式(Modular Crypt Format,简写为MCF)的事实标准,其格式如下:</p>\n<figure class=\"highlight bash\"><table><tr><td class=\"code\"><pre><span class=\"line\">$<id>[$<param>=<value>(,<param>=<value>)*][$<salt>[$<<span class=\"built_in\">hash</span>>]]</span><br></pre></td></tr></table></figure>\n<p>这里</p>\n<ul>\n<li>id: 表示散列算法的标识符(例如 1 代表MD5,5 代表SHA-256,6 代表SHA-512)</li>\n<li>param=value:散列复杂度参数(如轮数/迭代次数)及其数值</li>\n<li>salt: radix-64(字符集[+/a-zA-Z0-9])编码的盐</li>\n<li>hash: 密码和盐的散列值的radix-64编码结果</li>\n</ul>\n<p>随着计算机算力跟随摩尔定律迅速增强,以前常用的基于 MD5 的散列方案因为太容易受到攻击已被淘汰。现在新设计的系统都换到 SHA-512 散列方案,对应于/etc/shadow 文件中所见的<code>$6$</code>。</p>\n<p>用户密码散列值的生成和验证都可用名为<code>crypt</code>的POSIX C 库函数实现。此函数的定义如下:</p>\n<figure class=\"highlight c\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"function\"><span class=\"keyword\">char</span> *<span class=\"title\">crypt</span><span class=\"params\">(<span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *key, <span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *salt)</span></span></span><br></pre></td></tr></table></figure>\n<p>输入参数<code>key</code>指向存有用户密码的字符串,<code>salt</code>指向格式为<code>$<id>$<salt></code>的字符串,标明所要使用的散列算法和盐。大部分的Linux发行版都使用glibc库提供的<code>crypt</code>函数实现。下图概括了Glibc为<code>crypt</code>所增加的功能:</p>\n<p><img src=\"crypt-glibc-features.png\" style=\"width:60.0%;height:60.0%\" /></p>\n<p>在集成uClibc的嵌入式Linux系统中,uClibc提供对<code>crypt</code>函数的支持。但是在测试中发现,它居然对<code>$6$<salt></code>的输入返回空指针!这是怎么回事?</p>\n<p>谜底就在uClibc的<code>crypt</code>函数实现中,找到相应的C源码:</p>\n<figure class=\"highlight c\"><figcaption><span>libcrypt/crypt.c</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><unistd.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\"><crypt.h></span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">include</span> <span class=\"meta-string\">"libcrypt.h"</span></span></span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"function\"><span class=\"keyword\">char</span> *<span class=\"title\">crypt</span><span class=\"params\">(<span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *key, <span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *salt)</span></span></span><br><span class=\"line\"><span class=\"function\"></span>{</span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">unsigned</span> <span class=\"keyword\">char</span> *ukey = (<span class=\"keyword\">const</span> <span class=\"keyword\">unsigned</span> <span class=\"keyword\">char</span> *)key;</span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">unsigned</span> <span class=\"keyword\">char</span> *usalt = (<span class=\"keyword\">const</span> <span class=\"keyword\">unsigned</span> <span class=\"keyword\">char</span> *)salt;</span><br><span class=\"line\"></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (salt[<span class=\"number\">0</span>] == <span class=\"string\">'$'</span>) {</span><br><span class=\"line\"> <span class=\"keyword\">if</span> (salt[<span class=\"number\">1</span>] && salt[<span class=\"number\">2</span>] == <span class=\"string\">'$'</span>) { <span class=\"comment\">/* no blowfish '2X' here ATM */</span></span><br><span class=\"line\"> <span class=\"keyword\">if</span> (*++salt == <span class=\"string\">'1'</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> __md5_crypt(ukey, usalt);</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifdef</span> __UCLIBC_HAS_SHA256_CRYPT_IMPL__</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (*salt == <span class=\"string\">'5'</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> __sha256_crypt(ukey, usalt);</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">ifdef</span> __UCLIBC_HAS_SHA512_CRYPT_IMPL__</span></span><br><span class=\"line\"> <span class=\"keyword\">else</span> <span class=\"keyword\">if</span> (*salt == <span class=\"string\">'6'</span>)</span><br><span class=\"line\"> <span class=\"keyword\">return</span> __sha512_crypt(ukey, usalt);</span><br><span class=\"line\"><span class=\"meta\">#<span class=\"meta-keyword\">endif</span></span></span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"comment\">/* __set_errno(EINVAL);*/</span> <span class=\"comment\">/* ENOSYS might be misleading */</span></span><br><span class=\"line\"> <span class=\"keyword\">return</span> <span class=\"literal\">NULL</span>;</span><br><span class=\"line\"> }</span><br><span class=\"line\"> <span class=\"keyword\">return</span> __des_crypt(ukey, usalt);</span><br><span class=\"line\">}</span><br></pre></td></tr></table></figure>\n<p>啊哈!原来它默认只做 MD5 散列,SHA-256 和 SHA-512 的代码需要各自的条件编译宏定义。这好办,编辑uClibc的配置文件将后面两者都打开就可以了。</p>\n<figure class=\"highlight diff\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">--- a/toolchain/uClibc/config-0.9.33.2/common</span></span><br><span class=\"line\"><span class=\"comment\">+++ b/toolchain/uClibc/config-0.9.33.2/common</span></span><br><span class=\"line\"><span class=\"meta\">@@ -151,8 +151,8 @@</span> UCLIBC_HAS_REGEX_OLD=y</span><br><span class=\"line\"> UCLIBC_HAS_RESOLVER_SUPPORT=y</span><br><span class=\"line\"><span class=\"deletion\">-# UCLIBC_HAS_SHA256_CRYPT_IMPL is not set</span></span><br><span class=\"line\"><span class=\"deletion\">-# UCLIBC_HAS_SHA512_CRYPT_IMPL is not set</span></span><br><span class=\"line\"><span class=\"addition\">+UCLIBC_HAS_SHA256_CRYPT_IMPL=y</span></span><br><span class=\"line\"><span class=\"addition\">+UCLIBC_HAS_SHA512_CRYPT_IMPL=y</span></span><br><span class=\"line\"> UCLIBC_HAS_SHADOW=y</span><br></pre></td></tr></table></figure>\n<p>最后去看看uClibc自带的测试 SHA-512 散列算法的程序。它清楚地列出了测试代码所定义的数据结构,其包括盐、输入密码和期待的输出,以及多个测试矢量:</p>\n<figure class=\"highlight c\"><figcaption><span>test/crypt/sha512c-test.c</span></figcaption><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"keyword\">static</span> <span class=\"keyword\">const</span> <span class=\"class\"><span class=\"keyword\">struct</span></span></span><br><span class=\"line\"><span class=\"class\">{</span></span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *salt;</span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *input;</span><br><span class=\"line\"> <span class=\"keyword\">const</span> <span class=\"keyword\">char</span> *expected;</span><br><span class=\"line\">} tests[] =</span><br><span class=\"line\">{</span><br><span class=\"line\"> { <span class=\"string\">"$6$saltstring"</span>, <span class=\"string\">"Hello world!"</span>,</span><br><span class=\"line\"> <span class=\"string\">"$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJu"</span></span><br><span class=\"line\"> <span class=\"string\">"esI68u4OTLiBFdcbYEdFCoEOfaS35inz1"</span> },</span><br><span class=\"line\"> { <span class=\"string\">"$6$rounds=10000$saltstringsaltstring"</span>, <span class=\"string\">"Hello world!"</span>,</span><br><span class=\"line\"> <span class=\"string\">"$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sb"</span></span><br><span class=\"line\"> <span class=\"string\">"HbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v."</span> },</span><br><span class=\"line\"> ...</span><br><span class=\"line\"> { <span class=\"string\">"$6$rounds=10$roundstoolow"</span>, <span class=\"string\">"the minimum number is still observed"</span>,</span><br><span class=\"line\"> <span class=\"string\">"$6$rounds=1000$roundstoolow$kUMsbe306n21p9R.FRkW3IGn.S9NPN0x50YhH1x"</span></span><br><span class=\"line\"> <span class=\"string\">"hLsPuWGsUSklZt58jaTfF4ZEQpyUNGc0dqbpBYYBaHHrsX."</span> },</span><br><span class=\"line\">};</span><br></pre></td></tr></table></figure>\n<p>可以看到,最后一个测试用例输入定义轮次为10(<code>$6$rounds=10$roundstoolow</code>),而输出结果显示的轮次为1000(<code>rounds=1000</code>)。这印证了uClibc的<code>crypt</code>函数实现与Glibc增加的功能相匹配 —— 为了保障安全性,如果输入指定的轮次太小,<code>crypt</code>将自动设定为最小轮次1000。</p>\n<h3 id=\"dns安全补丁\">DNS安全补丁</h3>\n<p>2022年5月初,一家专注于为工业和关键基础设施环境提供安全解决方案的公司,<a href=\"https://www.nozominetworks.com/blog/nozomi-networks-discovers-unpatched-dns-bug-in-popular-c-standard-library-putting-iot-at-risk/\">Nozomi Networks</a>,发布了他们发现的uClibc的一个新的安全漏洞 <a href=\"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-30295\">CVE-2022-30295</a>。此漏洞存在所有版本的uClibc及其分支<a href=\"https://www.uclibc-ng.org\">uClibc-ng</a>(1.0.41版之前)的域名系统(DNS)实现中。由于该实现在进行DNS请求时使用了可预测的事务ID,因此存在DNS缓存被攻击者毒化的风险。</p>\n<p>具体来说,应用程序常常调用<code>gethostbyname</code>库函数以解析某个主机名对用的网络地址。uClibc/uClibc-ng 内部实现了一个<code>__dns_lookup</code>函数提供实际的DNS域名请求及应答处理过程。以uClibc 最后的0.9.33.2版为例,下面的截图显示了<code>__dns_lookup</code>函数中问题代码:</p>\n<p><img src=\"CVE-2022-30295.png\" style=\"width:65.0%;height:65.0%\" /></p>\n<p>参考第1308行,在第一次DNS请求时,<code>local_id</code>变量被初始化为上一次DNS请求的事务ID值(保存在静态变量<code>last_id</code>中)。第1319行是实际的漏洞核心,它只是简单地将<code>local_id</code>旧值递增1来更新。这个新值又如第1322行所示被存回<code>last_id</code>变量中。最后,在第1334行,<code>local_id</code>的值被复制到结构变量<code>h</code>中,它代表了DNS请求头的实际内容。这段代码在所有可获得的uClibc和1.0.41版之前的uClibc-ng中都差不多。</p>\n<p>这种实现使得DNS请求中的事务ID变成可预测的,因为攻击者只要探察到正在的事务ID,就能估计出下一个请求中的事务ID数值。利用这个漏洞,攻击者只要制作一个包含正确源端口的DNS应答,以及在与DNS服务器返回的合法应答的竞争中获胜,就能扰乱/毒化主机的DNS缓存,使主机系统中应用程序的网络数据被导向攻击者设定的陷阱站点。</p>\n<p>在此安全漏洞公布之后,uClibc-ng的维护者反应很快。他们于2022年5月中旬就提交了<a href=\"https://cgit.uclibc-ng.org/cgi/cgit/uclibc-ng.git/commit/?id=f73fcb3d067e22817189077c9b7bd2417c930d34\">修复补丁</a>,并在当月底发布了包括此修改的1.0.41版。对于uClibc,由于此C标准库从2012年起就停止发布新版本,目前处于无人维护状态,所以需要系统研发工程师自己手工修补。下面的uClibc补丁可供参考:</p>\n<figure class=\"highlight diff\"><table><tr><td class=\"code\"><pre><span class=\"line\"><span class=\"comment\">diff --git a/libc/inet/resolv.c b/libc/inet/resolv.c</span></span><br><span class=\"line\"><span class=\"comment\">index 31e63810b..c2a8e2be4 100644</span></span><br><span class=\"line\"><span class=\"comment\">--- a/libc/inet/resolv.c</span></span><br><span class=\"line\"><span class=\"comment\">+++ b/libc/inet/resolv.c</span></span><br><span class=\"line\"><span class=\"meta\">@@ -315,6 +315,7 @@</span> Domain name in a message can be represented as either:</span><br><span class=\"line\"> #include <sys/utsname.h></span><br><span class=\"line\"> #include <sys/un.h></span><br><span class=\"line\"> #include <sys/stat.h></span><br><span class=\"line\"><span class=\"addition\">+#include <fcntl.h></span></span><br><span class=\"line\"> #include <sys/param.h></span><br><span class=\"line\"> #include <bits/uClibc_mutex.h></span><br><span class=\"line\"> #include "internal/parse_config.h"</span><br><span class=\"line\"><span class=\"meta\">@@ -1212,6 +1213,20 @@</span> static int __decode_answer(const unsigned char *message, /* packet */</span><br><span class=\"line\"> return i + RRFIXEDSZ + a->rdlength;</span><br><span class=\"line\"> }</span><br><span class=\"line\"></span><br><span class=\"line\"><span class=\"addition\">+uint16_t dnsrand_next(int urand_fd, int def_value) {</span></span><br><span class=\"line\"><span class=\"addition\">+ if (urand_fd == -1) return def_value;</span></span><br><span class=\"line\"><span class=\"addition\">+ uint16_t val;</span></span><br><span class=\"line\"><span class=\"addition\">+ if(read(urand_fd, &val, 2) != 2) return def_value;</span></span><br><span class=\"line\"><span class=\"addition\">+ return val;</span></span><br><span class=\"line\"><span class=\"addition\">+}</span></span><br><span class=\"line\"><span class=\"addition\">+</span></span><br><span class=\"line\"><span class=\"addition\">+int dnsrand_setup(int *urand_fd, int def_value) {</span></span><br><span class=\"line\"><span class=\"addition\">+ if (*urand_fd > 0) return dnsrand_next(*urand_fd, def_value);</span></span><br><span class=\"line\"><span class=\"addition\">+ *urand_fd = open("/dev/urandom", O_RDONLY);</span></span><br><span class=\"line\"><span class=\"addition\">+ if (*urand_fd == -1) return def_value;</span></span><br><span class=\"line\"><span class=\"addition\">+ return dnsrand_next(*urand_fd, def_value);</span></span><br><span class=\"line\"><span class=\"addition\">+}</span></span><br><span class=\"line\"><span class=\"addition\">+</span></span><br><span class=\"line\"> /* On entry:</span><br><span class=\"line\"> * a.buf(len) = auxiliary buffer for IP addresses after first one</span><br><span class=\"line\"> * a.add_count = how many additional addresses are there already</span><br><span class=\"line\"><span class=\"meta\">@@ -1237,6 +1252,7 @@</span> int __dns_lookup(const char *name,</span><br><span class=\"line\"> /* Protected by __resolv_lock: */</span><br><span class=\"line\"> static int last_ns_num = 0;</span><br><span class=\"line\"> static uint16_t last_id = 1;</span><br><span class=\"line\"><span class=\"addition\">+ static int urand_fd = -1;</span></span><br><span class=\"line\"></span><br><span class=\"line\"> int i, j, fd, rc;</span><br><span class=\"line\"> int packet_len;</span><br><span class=\"line\"><span class=\"meta\">@@ -1305,7 +1321,7 @@</span> int __dns_lookup(const char *name,</span><br><span class=\"line\"> }</span><br><span class=\"line\"> /* first time? pick starting server etc */</span><br><span class=\"line\"> if (local_ns_num < 0) {</span><br><span class=\"line\"><span class=\"deletion\">- local_id = last_id;</span></span><br><span class=\"line\"><span class=\"addition\">+ local_id = dnsrand_setup(&urand_fd, last_id);</span></span><br><span class=\"line\"> /*TODO: implement /etc/resolv.conf's "options rotate"</span><br><span class=\"line\"> (a.k.a. RES_ROTATE bit in _res.options)</span><br><span class=\"line\"> local_ns_num = 0;</span><br><span class=\"line\"><span class=\"meta\">@@ -1316,8 +1332,9 @@</span> int __dns_lookup(const char *name,</span><br><span class=\"line\"> retries_left--;</span><br><span class=\"line\"> if (local_ns_num >= __nameservers)</span><br><span class=\"line\"> local_ns_num = 0;</span><br><span class=\"line\"><span class=\"deletion\">- local_id++;</span></span><br><span class=\"line\"><span class=\"addition\">+ local_id = dnsrand_next(urand_fd, local_id++);</span></span><br><span class=\"line\"> local_id &= 0xffff;</span><br><span class=\"line\"><span class=\"addition\">+ DPRINTF("local_id:0x%hx\\n", local_id);</span></span><br><span class=\"line\"> /* write new values back while still under lock */</span><br><span class=\"line\"> last_id = local_id;</span><br><span class=\"line\"> last_ns_num = local_ns_num;</span><br></pre></td></tr></table></figure>\n<p>此uClibc补丁是uClibc-ng官方修复补丁的简化版,其核心是从系统的<code>/dev/urandom</code>文件读取双字节的随机数,然后用它设置原来的<code>local_id</code>,即DNS请求的事务ID。<code>/dev/urandom</code>是Linux系统的一个特殊的设备文件,用作非阻塞随机数发生器,它会重复使用熵池中的数据以产生伪随机数据。</p>\n<p>注意在上面的补丁中,<code>dnsrand_setup</code>函数一定要先检查<code>urand_fd</code>是否为正,只有不成立时才去打开<code>/dev/urandom</code>。否则,每一次应用程序做DNS查询时都会重新打开此文件,系统将很快达到允许使用的文件描述符数目的上限,系统将因为无法再打开任何文件而崩溃。</p>\n<p>最后,给出一个使用uClibc的嵌入式系统在加入DNS安全补丁前后的比较。如下是两个嗅探器截获的DNS数据包,第一个无补丁的系统,DNS请求的事务ID按顺序递增,这是明显的安全漏洞;第二个是加补丁后,每一个DNS请求的事务ID都是随机值,漏洞已补上。</p>\n<p><img src=\"uClibc-DNS-cve.png\" style=\"width:75.0%;height:75.0%\" /> <img src=\"uClibc-DNS-fix.png\" style=\"width:75.0%;height:75.0%\" /></p>\n","categories":["技术小札"],"tags":["密码学","TCP/IP","C/C++编程","系统编程","网络通信"]}]