作者: Shencha Fan, Jackson Sippe, Sakamoto San, Jade Sheffey, David Fifield, Amir Houmansadr, Elson Wedwards, Eric Wustrow
Network and Distributed System Security (NDSS) Symposium 2025
English version: Wallbleed: A Memory Disclosure Vulnerability in the Great Firewall of China
首次发布日期: 2025年2月25日,星期二
最后修改日期: 2026年7月21日,星期二
我们介绍了一个名为Wallbleed(墙出血)的缓冲区过读(buffer over-read)漏洞,该漏洞存在于中国防火长城(GFW)的DNS注入子系统中。Wallbleed导致某些全国范围的审查中间设备在审查特制的DNS查询时泄露多达125字节的自身内存。这一漏洞为我们提供了一个难得的机会,以深入了解防火长城最著名的网络攻击手段之一——DNS注入——的内部架构,以及审查者的操作行为。
为了理解Wallbleed的成因和影响,我们从2021年10月起进行了长达两年多的长期、全网范围的测量。我们(1)逆向工程了DNS注入器的解析逻辑,(2)评估了哪些信息被泄露以及中国境内外的互联网用户受到何种影响,并且(3)持续监测审查者的修补行为。我们识别出可能属于审查系统内部的流量,分析了审查系统的内存管理和负载均衡机制,并观察到某个注入节点的进程级变化。为辅助分析,我们采用了一个新的旁路信道来区分注入器的多个进程。我们的监测显示,审查者在2023年11月对Wallbleed实施了一次不正确的修补,并在2024年3月将其彻底修补。
Wallbleed例证了审查中间设备对互联网用户造成的危害甚至超出了其对言论自由的明显侵犯。当实现不当时,它还会对互联网用户的隐私和机密性构成严重威胁。
中国的国家互联网审查系统,通称为防火长城(GFW),由许多部分和子系统组成,每个部分都采用不同的技术来控制对在线信息的访问。其中一个突出的组成部分是DNS注入子系统,它针对被审查域名的DNS查询伪造DNS响应。直到2024年3月,某些DNS注入设备一直存在一个解析漏洞,该漏洞在特定条件下会使这些设备在其发送的伪造DNS响应中包含多达125字节的自身内存。我们将这一漏洞称为Wallbleed,以此致敬Heartbleed [1]、Ticketbleed [2]和Cloudbleed [3, 4]等类似的缓冲区过读漏洞。
在本研究中,1我们分析了Wallbleed的成因和影响。我们的研究证实Wallbleed至少存在了两年。(关于类似漏洞的报告早在2010年就已出现[5], [6])。我们在2021年10月至2024年4月期间进行了持续测量。该漏洞在2023年11月得到部分修补,但DNS注入器仍然容易受到某些特制查询的攻击,直到2024年3月才最终得到彻底修补。
Wallbleed为我们提供了前所未有的视角来审视防火长城(GFW),既包括其内部架构,也包括审查者的操作行为。虽然先前的研究已经探讨了中国封锁了哪些域名和资源,但人们对防火长城网络中间设备的内部运作机制仍知之甚少[7, 8, 9]。通过分析Wallbleed泄露的数据,我们得以辨识防火长城的底层架构,并逆向工程了导致Wallbleed的解析漏洞,用C语言实现了一个行为完全一致的版本。在研究过程中,我们发现了防火长城DNS注入的一些此前未知的特征,例如每个注入进程都独立地按固定顺序循环遍历一个伪造IP地址列表,这构成了一个能够区分注入器节点中多个进程的旁路信道。最后,我们进行了长期、全网范围的测量来监测审查者的修补活动,从而利用这个难得的机会,深入了解防火长城如何维护其审查基础设施。
我们检查了Wallbleed泄露的内存内容,发现了明显的网络协议头、载荷、x86_64栈帧和可执行代码(尽管我们提供了证据表明这并非防火长城本身的代码)。我们发送了带有可识别字节模式标记的流量,让其穿过防火长城,并在某些情况下于后续的Wallbleed响应中重新找回了这些标记,这表明该漏洞至少泄露了部分被防火长城看到的流量。在Wallbleed泄露的内存中,我们看到了明文网络流量和协议的样本,包括IP、TCP、UDP和HTTP,且并非所有这些流量都与DNS审查有关。我们还进行了全IPv4范围的扫描,以估计中国境内外可能有多少地址的流量曾被存在Wallbleed漏洞的中间设备处理。甚至一些源和目的地都在中国境外的流量也可能受到影响,因为这些流量会经由中国的网络边界进行路由。
进行此类研究伴随着重大的伦理考量。我们在第IX节中对此进行了深入讨论,其中包括是否应当披露某个系统中的漏洞——而该系统本身就被许多人视为一种危害来源[10, 11]。注入伪造的DNS响应是防火长城每天实施的众多持续性攻击之一。这些攻击的意图和效果众所周知:限制人们获取信息。Wallbleed就是一个例子,说明审查设备对安全和隐私构成的风险甚至超出了其对言论自由的明显侵犯。尽管这一特定漏洞最终得到修复,但此类设备的存在仍然是一种隐患。
GFW的DNS注入子系统在中国网络边界部署了一批中间设备,这些设备监控针对被封锁域名的DNS查询。当它们检测到这样的查询时,会注入一个DNS响应返回给客户端,伪造源地址,使其看似来自预期的解析器。注入的响应是对查询的错误回答,包含一个错误且无用的IP地址。当客户端随后尝试连接时,它不会连接到预期的目标,而是会遇到错误。这些注入中间设备是“旁路(on-path)”设备,而非“直连(in-path)”设备:它们不会阻止查询到达合法的解析器,也不会阻止真实的响应到达客户端。注入器的伪造响应之所以会“胜出”,是因为它率先到达——它被注入在网络路径中比真正的解析器更靠近客户端的位置[12]。在正常情况下,每个查询仅预期接收一个DNS响应,因此客户端会接受最先收到的响应。
DNS注入子系统是双向的: 它对离开中国或进入中国的查询都做出响应。 这一特性为分析该系统提供了便利: 相比于在中国境内获取和维护一个网络观察点, 从外部向中国发送数据包要容易得多。 通过向中国境内的非活动IP地址发送DNS查询, 我们可以确保收到的任何响应都是中间设备注入器发出的, 而非终端主机的真实回复。
正如GFW由不同的组件组成,DNS注入也是由至少三种不同类型的DNS注入器完成的。关于这一主题的基础研究包括2009年gfwrev的工作[13],2014年Anonymous的工作[7],2020年Anonymous等人的“三重审查(Triplet censors)”[8],以及2021年Hoang等人的“防火长城有多强大?(How great is the Great Firewall?)”[9]。不同类型的注入器在其封锁列表、IP和DNS层的网络指纹以及解析逻辑的特性上均有所不同。Wallbleed漏洞仅存在于其中一种注入器中,即Anonymous等人称为“3号注入器”("Injector 3")的那种[8 §4.1]。
DNS注入长期以来一直是GFW的主要技术之一。但仅仅绕过它是不够的,因为还有其他系统在运作。即使客户端能够通过某种方式获得正确的DNS响应,其通信仍可能被另一个子系统干扰,例如IP地址过滤[14 §4.1],TLS SNI过滤[14, 15, 16]或TLS ESNI过滤[14, 17]。在本文中,我们只关注DNS注入,并且只关注这一种DNS注入器。
中国政府并不是唯一使用DNS注入进行审查的国家。例如,请参阅Master和Garman在2023年综述中的表1“DNS篡改(DNS Tampering)”一栏[18],以及Nourin等人对土库曼斯坦境内针对DNS及其他协议的双向注入的研究[19]。
由于Wallbleed漏洞源于底层的解析错误,了解DNS消息在网络传输中和内存中的表示方式将非常重要。DNS消息的格式在RFC 1035中规定[20]。查询和响应具有相同的基本格式:一个12字节的头部,后跟四个可变长度的部分:问题(question)、答案(answer)、授权(authority)和附加(additional)。我们将只关注问题(question)和答案(answer)部分。问题(question)部分包含被查询的DNS名称(或在响应中,响应所针对的名称)。答案(answer)部分仅在响应中存在,包含查询请求的信息(通常是一个IP地址),以一种称为资源记录(resource record)的数据结构表示。
图1是一个注入的DNS响应示例。它展示了理解本研究中出现的DNS消息所需的一切。我们将始终一致地使用这些字段名称和背景颜色。
| 1234 ID |
|
| 8180 flags | |
| 0001 QDCOUNT | |
| 0001 ANCOUNT | |
| 0000 NSCOUNT | |
| 0000 ARCOUNT | |
| 03 r s f 03 o r g 00 QNAME |
|
| 0001 QTYPE (A, IPv4 address) | |
| 0001 QCLASS (IN) | |
| c00c NAME (pointer to QNAME) |
|
| 0001 TYPE (A, IPv4 address) | |
| 0001 CLASS (IN) | |
| 000000ec TTL (236 seconds) | |
| 0004 RDLENGTH | |
| 1f0d 5b 21 RDATA (31.13.91.33) |
图 1:一个注入的DNS响应的结构。
该消息是一个DNS响应(而不是查询),这可以通过标志(flags)字段的最高有效位被置位来识别。它有一个问题(question)和一个答案(answer);授权(authority)和附加(additional)部分是空的。问题(question)部分中的QNAME是DNS客户端请求解析的名称,rsf.org。答案(answer)部分由GFW注入器构建。它声称客户端的QNAME解析为一个不正确的IPv4地址(注入器可能使用的数百个地址之一)。
最重要的是理解DNS名称的编码。名称在DNS协议中无处不在:每个问题(question)部分(QNAME字段)中都有一个,且每个资源记录(NAME字段)中至少有一个。名称是标签的序列。标签则又是一个字节序列,并以一个表示其长度的字节作为前缀。名称在一个空标签处结束,即仅由长度前缀00构成的标签。名称example.com 有三个标签,分别为7、3和0字节。其编码长度为13字节:07 e x a m p l e 03 c o m00。
名称长度前缀编码有一个例外。如果长度前缀的两个最高有效位被置位,那么该字节的其余6位和下一个字节的8位将构成一个14位的压缩指针,指示名称中剩余的标签从消息中给定的字节偏移处开始。消息压缩很有用,因为DNS消息中经常多次包含相同的名称,或包含几个具有共同后缀的名称。压缩指针模式 c00c是一个应当能一眼认出的模式。它指向字节偏移12,即问题(question)部分中QNAME字段的偏移量。易受Wallbleed攻击的注入器不会将QNAME复制到答案(answer)部分,而是用一个 c00c压缩指针作为答案(answer)部分的开头。(使用压缩指针并不是GFW独有的;合法的解析器也会使用它们。在GFW中存在的各种DNS注入器中,有些使用 c00c,有些则复制一份QNAME[8 §4.1]。)
DNS名称的格式带有长度前缀和指针间接寻址,容易在解析器中引发内存安全错误。在处理标签长度前缀时,必须检查标签的结尾是否落在消息的边界之内。缺乏这样的检查正是Wallbleed溢出漏洞的根本原因。
这是一个格式良好的查询,其QNAME,rsf.org,在GFW的封锁列表上:
1234 0100 0001 0000 0000 0000 03 r s f 03 o r g 00 0001 0001
如果我们从中国境外的主机向中国境内的主机发送这些字节(以目的端口为53的UDP数据报的形式),就会收到一个注入的DNS响应。(实际上会收到多个响应,因为这个QNAME位于多种注入器的封锁列表上。)中国境内的任何目标IP地址都可以,即使是一个无响应的地址——查询只需在传输途中经过一个注入器中间设备即可。
一个注入的响应如下所示(这是图1的更紧凑形式):
1234 8180 0001 0001 0000 0000 03 r s f 03 o r g 00 0001 0001 c00c 0001 0001 000000ec 0004 1f 0d 5b 21
ID和问题(question)部分是从查询中复制的。标志位已按响应报文的要求进行设置。答案(answer)部分谎称名称rsf.org (由压缩指针 c00c表示)解析为IPv4地址31.13.91.33(1f0d 5b 21 )。如附录A中详细说明的,这个伪造地址是注入器可能使用的众多地址之一。如果我们再次发送该查询,很可能会得到另一个不同的地址。
现在看看如果我们人为地将org 标签的长度前缀从03(3)增加到20(32)会发生什么:
1234 0100 0001 0000 0000 0000 03 r s f 20 o r g 00 0001 0001
首先,现在我们只会得到一个注入的响应:这个格式错误的查询会被Wallbleed以外的注入器忽略。答案部分中的TTL和IP地址与之前不同,这在意料之中:它们通常在每个响应中都会变化。更重要的是,注入的响应在答案部分之前包含了29个额外的字节。这些字节来自处理该查询的注入设备的内存。在本例中,泄露的字节是一段UPnP HTTP头部的片段:
12348180 0001 0001 0000 0000 03 r s f 20 o r g 00 0001 0001 C u s t o m / 1 . 0 U P n P / 1 . 0 P r o c / V e r 0d c00c 0001 0001 000000820004 68 f4 2e a5
每当注入器对这样的查询作出响应时,它都会泄露出其内存中的一小段窗口,且每次泄露的内容都各不相同。
我们推测,注入器设备内部一定发生了类似如下的过程。在其网络分路器(tap)上观察到DNS查询后,注入器将数据包复制到内存中进行处理。它的目标是从查询中解析出QNAME,将其与封锁列表进行比对,并在需要时注入响应。在解析QNAME时,注入器首先看到3字节的标签rsf:到目前为止一切正常。但长度前缀20表明下一个标签长达32字节,这一长度跨越了org 标签和空标签、QTYPE和QCLASS字段,并越过了查询的末尾。由于未能执行边界检查,注入器便把内存中紧跟数据包之后的字节也当作查询的一部分——就好像QNAME是下面这38个字节 03 r s f 20 o r g 00 00 01 ⋅⋅⋅ P r o c / 。尽管名称末尾带有多余的字节,它仍然与封锁列表匹配,原因我们将在第III-A节中解释。注入器会把它所看到的整个QNAME复制到DNS响应中。 接下来的4字节(在此示例中, V e r 0d )被解释为查询的QTYPE和QCLASS,同样被复制到响应中。
为什么解析器在 / 字节处停止,而不是把它当作长度前缀去读取下一个标签?我们在附录B中给出了对解析算法的精确逆向工程描述,它回答了这个问题以及其他问题。就本例而言,这是因为QNAME解析器会在遇到第一个越过查询末尾的标签长度前缀后停止。
当注入器检查一个名称是否在其封锁列表中时,它并不使用该名称的线格式(wire format)表示。相反,它将QNAME展平为一个以00字节结尾的点分隔字符串。传递给封锁列表查找函数的正是这个字符串。支持这一论断的证据在于:当查询中的某个标签包含ASCII点字符 . 或空字节00时,封锁列表匹配器会分别将其解释为标签分隔符或名称终止符。例如,若名称example.com 在封锁列表中,以下任一QNAME 07 e x a m p l e 03 c o m 00 或 0f e x a m p l e . c o m 00 a b c 00 都会引发注入。尽管这两个名称在DNS层面上并不相同(第一个由三个标签组成,长度分别为7、3和0字节;第二个由两个标签组成,长度分别为15和0字节),但它们都被展平为相同的等效字符串“example.com”。
这解释了为什么前一个示例中被注入器理解的那个QNAME 03 r s f 20 o r g 00 00 01 ⋅⋅⋅ P r o c / 会匹配rsf.org的封锁列表规则。尽管第二个标签并不只是org,而是org再加上许多额外的字节,但这些额外字节中的第一个是00,当名称被展平为字符串时,它会终止该名称。这些额外的字节会包含在注入的DNS响应中,但并不影响封锁列表匹配。
这也解释了为什么我们修改的是org标签的长度前缀,而不是rsf标签。假如我们延长了rsf标签,那么03在 o r g 之前就会被解释为展平后名称字符串中的一个字面字符——而由于字符串“rsf\x03org”与封锁列表上的任何内容都不匹配,它便不会得到响应。相反,通过延长org标签,rsf和org仍然是彼此独立的标签,而末尾的空标签00则成为字符串终止符。改变第一个长度前缀也可行,但这样一来第二个长度前缀也必须改为一个点,以便在最终字符串中分隔这两个标签: 20 r s f . o r g 00。
封锁列表规则并不是字面名称,而是模式,类似于正则表达式[9 §4.1]。例如,单条规则可以封锁整个域及其所有子域。这些模式的构建并不统一(显示出人工编制、因而难免出错的迹象)。我们一直使用的rsf.org 模式是结尾锚定且标签锚定的:rsf.org 和x.rsf.org 与该模式匹配,但xrsf.org、rsf.org.x和rsf.orgx 不匹配。用正则表达式表示,它大致相当于(.*\.)*rsf\.org$。
相比之下,shadowvpn.com的模式是开始锚定而非标签锚定的:shadowvpn.com、shadowvpn.comx和shadowvpn.com.x与它匹配,但xshadowvpn.com和x.shadowvpn.com不匹配。它的正则表达式将是^shadowvpn\.com.*。
单个响应中可能泄露的最大字节数是125。这是因为注入响应中的问题(question)部分最大为131字节,而触发响应的查询中最短的问题(question)部分长度为6字节。响应中的问题(question)部分在开头包含查询中问题(question)部分的副本;其余部分是泄露的内存。为了最大化泄露的内存量,需最小化查询中问题(question)部分的大小(查询实际有多大),并最大化响应中问题(question)部分的大小(注入器认为查询有多大)。
最小化查询大小的第一步是省略QTYPE和QCLASS字段。当这些字段缺失时,注入器会从其自身内存中读取它们。QCLASS没有影响,而QTYPE仅控制注入器是生成类型A(IPv4)还是类型AAAA(IPv6)的响应。对于未知的QTYPE,注入器默认为类型A;只有当QTYPE是 001c时,它们才发送类型AAAA的响应。无论哪种情况,问题(question)部分的大小都是相同的。
最小化查询大小的另一部分是使用短的QNAME。为了找到触发注入响应的短DNS名称,我们枚举了所有形式为a.b、a.bc和ab.c的名称,其中a、b和c的范围包括字符‘a’–‘z’、‘0’–‘9’、‘-’和‘_’,并通过DNS查询将它们发送到中国。我们找到了八个有效的短名称:3.tt、4.tt、5.tt、6.tt、7.tt、8.tt、9.tt和x.co。 2 每个这样的名称都需要6字节来编码(例如,01 3 02t t00)。 3
在第III节的开头,我们通过将QNAME标签的长度前缀从3增加到32,导致注入器泄露了29个字节。直观地说,为了泄露更多字节,应该进一步增加标签的长度。这种直觉是正确的,但仅在某个程度上有效。图2展示了随着查询中标签长度的增加,响应中问题(question)部分的大小如何变化。(我们关注的注入器不会强制执行RFC 1035对标签的63字节长度限制[20 §4.1.4],而是天真地将每个字节值直接当作长度来解释。)两者一一对应地递增,直到响应中的问题(question)部分达到最大值131字节。超过该点后,问题(question)部分会略小于最大值,为130字节。
这种奇怪的行为是由于查询解析算法的混乱逻辑造成的(见附录B)。导致算法主循环终止的两个条件是:在处理标签内容时,QNAME的总长度超过127字节,以及解析器刚读取到的长度前缀已越过查询的末尾。当QNAME恰好为127字节(包括最后的标签长度前缀)时,就出现了131字节这一最佳点(sweet spot)。在这种情况下,第一个退出条件得以避免,使循环的下一次迭代在退出前得以多读取1个字节。QNAME的127个字节,加上缺失的QTYPE和QCLASS的4个字节,使得问题(question)部分的总长度为131个字节。
QNAME长度限制是这种类型注入器的一般特性,与Wallbleed解析漏洞无关。我们向中国发送了针对长度递增的名称的格式规范查询(a.google.sm、aa.google.sm、aaa.google.sm,...),所用的基础域名google.sm已知与封锁列表匹配。一旦最后一个标签末尾的 m 字节被挤出前127字节之外,注入器便停止响应。无论是类型A还是类型AAAA的查询,也无论QNAME中有多少个标签,该限制都是相同的。RFC 1035规定的最大名称长度为255字节[20 §2.3.4]。
虽然知道绝对限制及其原因令人满意,但在实践中,130字节和131字节之间几乎没有区别。在本文的许多实验中(一些是在我们理解解析算法的细微差别之前进行的),我们使用了ff的标签长度前缀,它每次查询所泄露的字节数比理论上的最大值少1字节。对于足够大的长度前缀,响应中130字节的问题(question)部分与klzgrad在2012年的发现一致[6]。
GFW在2023年9月至11月间尝试修补Wallbleed,为DNS消息解析算法增加了一些限制。我们在第VII节中记录了修补过程的演进。QTYPE和QCLASS字段不能再省略,且QCLASS必须取值 0001。此外,如果某个标签长度前缀溢出了查询末尾、但未达到127字节的QNAME长度阈值,该查询就会被忽略。像下面这样的查询不再能用于泄露DNS注入器的内存:
00000120 0001 0000 0000 0000 03 w w w 06 g o o g l e ff c o m 00
但第一个补丁忽略了解析循环中的一个退出条件。一个同时带有QTYPE和QCLASS、且最终标签长度前缀超过127字节阈值的查询,仍会使解析器误以为该查询比实际更大。稍加修改后的探测包格式依然可以引出内存内容:
00000120 0001 0000 0000 0000 03 w w w 06 g o o g l e 03 c o m ff0001 0001
我们将修补前和修补后的漏洞分别命名为Wallbleed v1和Wallbleed v2。本文描述的大多数实验都使用了Wallbleed v1探测包。修补之后,我们得以使用修改后的探测包恢复实验,直到Wallbleed v2于2024年3月最终被修补。对于Wallbleed v2,只有最大长度的溢出才可能实现:标签长度取ff有效,但取20则无效。我们发现,最短的那些域名(如3.tt)不再能作为触发器,因此在后续实验中改用了te.rs,即下一个最短的有效域名。
这里我们说明触发注入的条件的另外几个细节。请注意,防火长城(GFW)中还存在其他类型的DNS注入器[8, 9],它们各有自己的封锁列表和实现上的怪癖。
注入器默认返回类型A的响应。 DNS注入器仅响应QNAME与某个封锁列表匹配的查询。易受攻击的注入器对类型AAAA的查询注入类型AAAA的响应,对所有其他类型的查询注入类型A的响应。
注入器同时适用于IPv4和IPv6。 承载DNS查询的UDP数据报可以通过IPv4或IPv6发送;注入器对两者都响应,并根据查询相应地伪造IPv4或IPv6响应。(这里我们指的是查询所使用的IP版本,而不是查询的QTYPE。通过IPv4发送的查询可能请求IPv6地址,反之亦然。)2023年5月9日,我们发送了QNAME为 ff g o o g l e . s m 00 的Wallbleed探针,从旧金山(AS14061)到阿里云(北京,AS37963)的一个IPv6主机,以及中国的一个非DNS服务器2400:dd01:103a:4041::101。在这两种情况下,我们都收到了包含泄露内存的注入DNS响应。然而,我们无法在相反方向触发DNS注入,即从中国的VPS向美国的VPS或其他IPv6地址发送查询。这可能是因为注入器并未部署在从我们的中国VPS到所测试的国外目的地的路径上。
仅检查目标端口53。 2023年5月9日,我们从美国的VPS向中国的VPS发送了google.sm 的查询,并使UDP目标端口取遍0到65535之间的每个值。只有发送到端口53的查询才触发了注入。这一观察结果与Lowe等人在2007年[21 §6.4]和Anonymous等人在2020年[8 §2.1]的先前发现一致。
为了更好地理解漏洞泄露了哪些信息,我们进行了一项为期两年的纵向测量,从2021年11月21日到2023年11月29日持续收集数据。表I总结了本实验以及后续各节的实验。
表I:实验时间线和观察点。我们总共在腾讯云(TC,北京)(AS45090)使用了三个VPS,并在科罗拉多大学博尔德分校(Scan,CO)(AS104)和马萨诸塞大学阿默斯特分校(Long,MA)(AS1249)各使用了一台机器。
| 实验 |
时间跨度
|
持续时间
|
中国主机 | 美国主机 | 章节 | ||
| 特征化 | 2021年10月2日 – 2022年2月10日 | 4 | 月 | 1 (TC,北京) | 1 (Long, MA) | §III-B | |
| 重新特征化 | 2023年5月9日 – 9月10日 | & 2024年2月 | 5 | 月 | 1 (TC,北京) | 1 (Long, MA) | §III |
| 纵向 | 2021年11月21日 – 2024年4月16日 | 2 | 年 | 3 (TC,北京) | 1 (Long, MA) | §IV | |
| 观察我们自己的 | 2023年8月12日 – 9月8日 | & 2024年3月13日 | 4 | 周 | 1 (TC,北京) | 1 (Scan, CO) | §V |
| 互联网扫描 | 2023年6月25日 & 8月23日 | & 2024年3月6日 | 3 | 天 | - | 1 (Scan, CO) | §VI |
| 修补行为 | 2023年9月6日 – 11月7日 | & 2024年3月6日 – 4月16日 2024 | 3 | 月 | 2 (TC,北京) | 2 (Scan & Long) | §VII, §III-C |
基于第III-B节中的观察,我们设计了以下Wallbleed探测包来触发该漏洞:
00000120 0001 0000 0000 0000 01 3 ff t t
该探测包是对3.tt的查询,但在QNAME的终止空标签之前被截断(省略了QCLASS和QTYPE字段),并将tt标签的长度前缀从02增加到ff。(根据脚注3,对于这么短的QNAME,不需要最后的00标签。)正如第III-B节所述,这个探测包会导致124字节的内存泄露。
实验设置。我们从美国一所大学的一台主机向中国的一个IP地址发送Wallbleed探测包。中国的这个地址是我们在腾讯云(AS45090)中控制的一个VPS。我们让探测包的UDP源端口在1,000个端口号(10001到11000)的范围内变化,因为已有研究表明源端口号可能会影响DNS注入[22]。我们以每秒100个数据包(pps)的速率发送探测包,并在两年内收集了51亿个Wallbleed响应。
查询名称。我们最初使用的QNAME 3.tt显然已从注入器的封锁列表中移除,并于2023年8月7日(周一)11:04:01(中国标准时间,UTC+8)停止引发注入响应。我们将QNAME更改为4.tt,这是来自第III-B节的另一个短名称。
查看这些124字节内存泄露片段的样本,可以立即看出它们包含网络流量片段。这些片段至少部分来自经过注入设备的数据包:在第V节中,我们演示了如何恢复我们自己通过GFW发送的数据包负载。但其中的协议组合,与人们对进出中国的所有流量进行均匀采样时所预期的情况并不相同。
在对一部分响应样本进行初步的人工分析后,我们使用正则表达式来搜索常见或敏感的字符串。为降低分析到可识别个人身份信息的风险,我们的程序仅输出匹配的数量。如表II所示,我们发现了UPnP、SSDP、HTTP、SMTP、SSH和TLS的实例,以及诸如HTTP cookie和密码等潜在的敏感信息。
表II:对两年间观察到的51亿个Wallbleed响应进行正则表达式匹配得到的计数。
| 正则表达式 | 协议 |
计数
|
比例 | |
| ssdp:discover | SSDP | 184 | M | 3.61% |
| UPnP/IGD\xml | UPnP | 174 | M | 3.41% |
| (?s)[3-4]\xfftt.....-CONTROL | (§IV-B) | 121 | M | 2.37% |
| \x45\x00 | (§IV-A) | 2.8 | M | 0.05% |
| uuid:WAN | SSDP | 34 | M | 0.67% |
| Host:␣ | HTTP | 21 | M | 0.41% |
| (?i)Date:\s* … | (§IV-C) | 16 | M | 0.31% |
| \x7f\x00\x00 | (§IV-D) | 2.8 | M | 0.05% |
| Cookie:␣ | HTTP | 2.0 | M | 0.04% |
| RCPT␣TO | SMTP | 72.5 | k | 0.0014% |
| &key= | URL | 58.1 | k | 0.0011% |
| MAIL␣FROM | SMTP | 42.4 | k | 0.0008% |
| &password= | URL | 26.9 | k | 0.0005% |
值得注意的是,内存中包含了DNS以外的应用层协议。由于注入器仅在UDP端口53上响应DNS查询(第III-D节),我们本可能预期只会看到DNS、或仅有UDP端口53的流量;但实际上,我们看到了各种协议,包括那些通常运行在不同端口和传输协议之上的协议。其中相当大的一部分是UPnP(通用即插即用)和SSDP(简单服务发现协议)。UPnP使用HTTP——但UPnP的数量比其他形式的HTTP多出一个数量级以上。第III节中的示例响应就是这样一个UPnP实例。我们从1.66亿个UPnP片段中提取了HTTP Location首部:在每一个案例中,URL的主机部分都是RFC 1918[23]某个私有范围内的字面IP地址。私有地址与UPnP和SSDP相符,二者通常用于本地网络中的服务发现。尽管如此,仍难以解释为什么它们会在这个存在漏洞的注入器的内存中以如此高的比例出现。
除了应用层协议,还有网络层和传输层的首部与数据包。例如,其中有IPv4首部。为找到这些首部,我们首先查找通常位于IPv4首部开头的两字节模式4500 ,然后(将其后的字节解释为首部)筛选出IP校验和有效的项。表III显示了181,834个IPv4首部中protocol字段的分布。TCP、UDP和ICMP最为常见,此外还有由43种其他协议构成的长尾。
表III:IPv4首部中最常见的 protocol(协议)字段。仅显示计数超过10的条目。
| 编号 | 协议名称 | 计数 |
| 6 | TCP | 120,087 |
| 17 | UDP | 59,882 |
| 1 | ICMP | 1,735 |
| 50 | ESP | 38 |
| 0 | IPv6 Hop-by-Hop Option | 36 |
| 47 | GRE | 12 |
在7,743个案例中,IP首部之后跟着一个TCP首部,以及足以构成一个完整TCP段的数据,且其长度字段一致、IP与TCP校验和均有效。TCP首部含有标志位和端口号,据此我们可以启发式地推断出IP首部中的两个IP地址哪个是服务器、哪个是客户端。为避免分析可识别个人身份的信息,我们将IP地址匿名化为两个粗略的类别:私有(RFC 1918)和公共。随后我们统计了客户端/服务器与私有/公共的各项比例;结果如表IV所示。
由于DNS注入器监控的是公共互联网流量,我们原本预期从其内存中恢复的TCP段大多带有公共IP地址;然而,只有11%的TCP段是公共到公共的。它们大多实际上涉及一个私有客户端和一个公共服务器。由于私有IP地址不可在全球路由,人们可能会怀疑它们代表了GFW内部流量的一部分(这与上文关于UPnP的观察相符)。然而,内存泄露规模有限,这意味着我们只能统计较短的TCP段(最多125字节)。此外也有可能,我们所看到的TCP段是被封装在诸如GRE这样的更高层协议之中,而非直接路由经过中间设备。
在纵向实验开始时,Wallbleed响应中泄露数据的前4个字节与其余字节不同。它们通常看起来更随机,当某次泄露的其余部分由可读的ASCII构成时,这一点尤为明显。不相似的字节序列或许可归因于被部分覆盖的内存,但这里的情况有所不同:它们始终恰好是前4个字节,4而且不像其他字节那样包含网络协议的片段。我们逐渐把这些字节称为“摘要”字节,猜测它们代表查询包的某种哈希值,可能用于负载均衡。(这仅仅是一个猜测——我们尝试过,但未能找到能复现这些摘要字节的哈希算法。)摘要字节在2022年至2023年间分两个阶段从Wallbleed响应中消失。
摘要字节实际上并非随机,而是由DNS查询的内容(包括其UDP四元组)决定的。在2022年2月15日(当时所有Wallbleed响应都带有摘要字节),我们发送了一批Wallbleed探测包,它们的有效载荷以及源、目标IP地址和端口都完全相同。在由此产生的全部114,717次注入中,前4个字节都恰好是d8fd d0 41 。然而,在保持四元组不变的情况下改变有效载荷中的一个比特,就会使摘要字节随之改变。5这一点可与第V-A节中的注入器进程分配相比较,后者取决于四元组而与有效载荷无关。
我们通过检索一个特定字符串ACHE-CONTROL (HTTP Cache-Control头部的一部分)来测量摘要字节随时间的出现频率,该字符串经常出现在Wallbleed响应的开头(表II)。当摘要字节存在时,该字符串的前4个字节会被覆盖。图3展示了摘要字节如何在九个月内分两个阶段逐渐消失。在我们开始测量时,所有响应都带有摘要字节。第一个不带摘要字节的响应出现在2022年9月3日星期六01:31(中国标准时间,UTC+8)。此后,摘要字节是否出现取决于探测包的源端口;在任一给定时刻,大约一半的端口会引发摘要字节。哪些端口会产生摘要字节,其映射关系时有变化,但始终维持在50%的比例——我们怀疑这体现了负载均衡。在2023年6月8日星期四15:33(UTC+8)之后,摘要字节几乎完全消失。
图3:下图显示了带有摘要字节的Wallbleed响应所占的比例,该比例按一天内所有探测包源端口取平均。在2022年9月3日之前,所有响应都带有摘要字节;在2023年6月8日之后,则没有任何响应带有摘要字节;在两者之间的时段内,则一半有、一半没有。在过渡期内,某个给定源端口是否引发摘要字节,在较短时间跨度内保持一致。上图显示了按探测包源端口和日期统计的摘要响应比例,其数值始终接近0%或100%。
我们通过寻找自然出现的时间戳来估计字节通常会在内存中停留多久,即HTTP Date头。这些字符串的格式为Date: Wed, 21 Apr 2021 00:00:00 GMT,表示HTTP响应生成的时间。图4显示了包含完整Date头的1630万Wallbleed响应的年龄分布:即响应接收时间与其Date头中编码的时间戳之间的差值。大多数Date头都来自不久前:其中75%的年龄在0到5秒之间,7%则更为久远。约10%相对于捕获时刻名义上几乎正好处于未来8小时,这可能是由于服务器错误地将本地时间当作UTC上报所致。
在第V-A节中,我们进行了一项类似的 内存年龄实验,使用我们自己故意放置的时间戳。
图4:HTTP Date时间戳相对于捕获时间的累积分布。上图的刻度为秒;下图的刻度为小时。大多数时间戳的年龄小于5秒。时区错误使得部分时间戳看起来处于未来8小时。
泄露的内存偶尔包含看起来像x86_64指针的内容。这些是小端字节序的64位值,其最高有效的16位为零,并且位于常规地址范围内。在Linux上,典型的栈指针地址范围是0x00007f0000000000–0x00007fffffffffff,而代码和堆指针的地址范围是0x0000550000000000–0x000056ffffffffff。
在Linux上,典型的栈包含一个栈地址,后跟一个代码地址(分别对应于保存的帧指针和返回地址)。我们在泄露的有效载荷中寻找这些模式。我们找到了70,497个例子,并注意到几个常见的模式。我们基于每个有效载荷中存在的14个64位字创建了模式模板。例如,一个栈地址(在典型的Linux栈指针范围内的64位值)被替换为单个字符‘s’。以这种方式,代码指针(‘c’)和常见数字,包括零(‘0’)、−128(‘1’)、22(‘2’)和4(‘4’)被替换,剩余未标记的字被转换为‘_’。这产生了3,559个不同的模式,我们在图5中绘制了其中最常出现的模式。
两条红线标示了摘要字节转换的各个阶段,取自图3。第一条线在2023年9月3日,与栈帧模式转向最常出现的形态相吻合;第二条线在2023年6月8日,则没有显示出那么明显的模式变化。不过,我们无法从这些变化的模式中得出更具体的结论;它们可能纯属巧合。
我们看到的栈帧与启用了ASLR的Linux栈帧一致,这一点可从某个给定模式仅在一部分位上出现随机化看出:栈/代码指针中最低有效的12位保持一致,对应于4KB页面内一致的偏移。在一些栈帧中,我们还观察到疑似glibc栈金丝雀[24]的值:在一对栈/代码地址之前,出现一个最低有效8位被置为0的随机值。
我们还观察到x86_64指令序列,如函数序言。我们认为这些是GFW在网络上看到的代码,而非GFW自身的代码,理由有二。首先,在基于栈的内存泄露中,指令不太可能被泄露,因为Linux会在分配前将页面清零,并且不允许可写页面中包含可执行代码。其次,我们还在某大学网络分流点看到的流量中观察到x86_64代码,它们似乎是微软的代码更新,会发送(经过签名的)明文二进制文件[25]。
在第IV-A节中,我们看到有证据表明,Wallbleed泄露了至少一部分流经注入设备的网络流量,甚至包括非DNS流量。在此我们通过一个专门的实验证实了这一事实。我们将自己打了标记的流量跨境发送到中国,之后得以在Wallbleed响应中恢复出其中的一小部分。
被标记的流量只能在发送后几秒钟内恢复。恢复率很低,并且随一天中的时间而变化。注入设备在内部被划分为多个独立的进程;我们借助注入的伪造IP地址排序中一个此前未见记载的旁路信道揭示了这一点。每个进程都有自己的内存:只有当某个Wallbleed探针恰好被分配到同一进程时,才有可能恢复过去的流量。数据包到进程的分配是确定性的,并且(至少)取决于探针的源端口。通过IPv6发送的探针可能会恢复最初通过IPv4发送的流量,反之亦然。
我们为这个实验开发了一种新的探针。魔法序列探针是一个UDP数据包,发往53端口,其40字节的有效载荷是一个20字节序列的两个副本:
G F W B l e e d exp pkt rep timestamp
其中exp是实验ID,pkt是递增的数据包ID,rep对于探针中序列的第一个副本为0,第二个副本为1,timestamp是一个纪元时间戳。固定字符串“GFWBleed”和唯一ID使得在Wallbleed响应中很容易识别出魔法序列。时间戳让我们能够估计一个恢复出的魔法序列在内存中保存了多久。虽然魔法序列探针使用UDP和目标端口53,但它们在结构上并非DNS。
在发送魔法序列探针的同时,我们也发送了Wallbleed探针,如第IV节中所述,以恢复我们试图放入内存中的序列。我们从美国的一所大学向中国境内的一个目的地发送探针,时间从2023年8月12日到9月8日(表I)。该目的地主机与第IV节中使用的不同,以避免两个实验之间的潜在干扰。我们以平均每秒30个数据包的速率,从单个源端口10000发送魔法序列探针。我们以每秒100个数据包的速率,从199个源端口(20001至20199范围内)发送Wallbleed探针。事实证明,为魔法序列探针选用单个源端口这一决定意义重大,因为它有助于揭示离散的注入器进程的存在。我们共收集到3,521个包含魔法序列的Wallbleed响应。
恢复到的流量通常存在不到1秒。图6显示了魔法序列探针中编码的时间戳与它在Wallbleed响应中被恢复的时刻之间的差值。与第IV-C节中的HTTP Date时间戳一样,流量在注入器内存中存活的时间很短:99%恢复到的魔法序列存在时间不足1.5秒。−1秒到0秒之间均匀的斜率是timestamp一秒粒度所造成的假象。与HTTP Date实验不同,这里不存在时区混淆的可能性。
图6:魔法序列中存储的时间戳与我们在Wallbleed响应中恢复它时的时间差的累积分布。图中显示了2023年8月12日至9月8日期间收集的3,521个包含魔法序列的Wallbleed响应的分布。时间差的范围是−10.19秒到−0.23秒。
恢复到流量的可能性呈日周期变化。图7显示了28天内一天中每个小时收到的包含魔法序列的Wallbleed响应数量。虽然我们以恒定速率发送Wallbleed探针和魔法序列探针,但每小时恢复到的探针数量呈24小时周期变化,峰值在04:00到05:00之间,谷值在22:00到23:00之间(中国标准时间,UTC+8)。这与中国互联网流量的反向昼夜模式一致:注入器处理的流量越多,我们观察到自己数据包的可能性就越小。
图7:观察到魔法序列的可能性取决于一天中的时间。淡色背景点代表了从2023年8月14日开始的四周内每小时收到的包含魔法序列的Wallbleed响应数量。深色前景点是所有28天中相应小时的平均值。
数据包在内存中具有一致的对齐方式。当我们恢复一个魔法序列时,并不能得到完整的全部40字节。开头部分几乎总是被触发该响应的Wallbleed探针的字节覆盖。使用第IV节中的Wallbleed探针时,前18字节被覆盖,后22字节保持完整。注入设备很可能将数据包的第一个字节对齐到内存中一致的位置。其他观察也支持这一假设:在第IV-B节中,我们利用了常见的ACHE-CONTROL字符串的对齐来检测摘要字节是否存在。
只有一部分源端口曾经看到过魔法序列。我们从单个源端口(10000)发送魔法序列探针。尽管我们从199个不同的源端口(20001–20199)发送了Wallbleed探针,但只有64个源端口曾经恢复过魔法序列。(成功恢复的那些端口平均恢复了55个魔法序列。)进一步的调查使我们相信,每个DNS注入设备由多个独立的进程组成,每个进程都有自己的内存缓冲区,并且数据包会根据包括源端口在内的特征被确定性地分配到某个进程。(但这些特征不包括有效载荷,因为魔法序列探针的有效载荷是可变的。这与第IV-B节的摘要字节形成对比,后者确实依赖于有效载荷。)只有当某个Wallbleed探针被分配到与原始魔法序列探针相同的进程时,它才有机会恢复出该序列。(这可以解释图3中的水平带:在一段时间内,一半的进程使用摘要字节,另一半则没有。)在下一小节中,我们将以注入的DNS响应中伪造IP地址里一个此前未知的旁路信道为形式,展示更多支持多进程假设的证据。
先前的研究表明,GFW的DNS注入从一个固定的地址池中抽取伪造的响应IP地址,并且根据所查询的名称使用该池的不同子集[8 §3.2],[9 §5.3]。而在此之前一直未被注意到的是,这些地址池还是有序的和循环的。当以足够高的速率(每秒约100个查询或更多——远高于注入器的自然注入速率)进行探测时,若使用一致的查询名称以及源/目标IP地址和端口元组,注入的响应会以相同的顺序反复循环遍历这些IP地址(偶尔会出现注入器因响应其他用户的查询而留下的间隙)。通过重复探测,可以获得该序列的多个副本,弥合这些间隙,并恢复出给定查询名称对应的完整伪造IP地址有序列表。查询名称4.tt的592个IP地址的示例有序列表见附录A。
任选一个IP地址作为循环中的“第一个”,我们便可以构建一个从IP地址到其索引的反向映射。与Wallbleed泄露无关,每个DNS响应都会在注入时刻揭示注入器的内部索引变量。图8显示了在从199个源端口以高速率探测时,45秒区间内Wallbleed响应所包含IP地址的索引。我们看到的不是一个,而是三个大致线性的序列。它们是循环的:当一个到达顶部时,便会回绕到底部。相同的源端口始终映射到相同的序列。在我们看来,这像是注入设备内部多个进程之间基于哈希的负载均衡。该负载均衡分配的输入包括数据包的UDP四元组,但不包括其数据有效载荷(因为魔法序列探针的有效载荷是可变的)。在四元组其余部分保持不变的情况下,源端口会根据其被分配到的注入器进程落入少数几个等价类。这解释了为什么199个源端口中只有64个恢复了魔法序列:这些正是恰好被分配到与源端口10000的魔法序列探针相同进程的端口。
图8:2023年8月23日,在45秒内从199个不同的源端口进行高速率探测后收到的Wallbleed响应样本。每个响应中的IP地址已被反向映射到其在附录A的有序列表中的索引(从1到592)。这些索引不是随机的,而是形成了三个不同的循环序列——每个源端口始终映射到其中一个。每个序列代表DNS注入器中的一个进程,具有自己的地址列表迭代器和内存分配。199个源端口中只有64个映射到了正确的进程,从而得以恢复第V-A节的魔法序列探针。
Wallbleed提供了一种方法来判断IPv4和IPv6数据包是在相同还是不同的GFW节点上处理的。如果我们通过IPv4向GFW之外发送一个独特的有效载荷,并在基于IPv6的Wallbleed查询所泄露的内存中看到该有效载荷的部分内容,那么我们就知道存在同时在相同内存中处理IPv4和IPv6的节点。
我们从MaxMind的GeoLite2国家代码数据库[26](下载于2024年3月12日)中收集了一组地理定位到中国的IPv6前缀,并根据RouteViews BGP数据(下载于2024年3月13日)排除了未被路由的前缀。我们向每个IPv6前缀中的8个随机地址各发送了一个Wallbleed v2探针。如果其中至少6个以Wallbleed泄露作出响应,我们就保留该前缀。我们对这610个IPv6前缀进行抽样,得到133k个随机IPv6地址,它们很有可能会经过某个GFW节点。对于IPv4地址,我们从对一次全IPv4范围ZMap扫描(进行于2024年3月6日)作出响应的地址中,随机抽样了126k个IPv4地址。
对每个IPv4和IPv6地址,我们都发送了一个针(needle):一个UDP端口53数据包,其900字节的有效载荷由一个8字节字符串、2字节实验ID和4字节索引所构成的序列重复组成,其中索引用于标识我们把这根针发往了哪个IP地址。与此同时,我们以每秒50个数据包的速度向每个地址发送Wallbleed v2探针,并收集响应,以查看其中是否包含先前发送的针。我们在80分钟内将这一过程重复了五次。
共出现了70个实例:某个地址收到的Wallbleed泄露有效载荷中,包含一个最初发送给另一个地址的针。在这些实例中,12个是从IPv4针泄露到IPv4探测地址,47个是IPv6到IPv6,8个是IPv4到IPv6,3个是IPv6到IPv4。IPv4到IPv6以及IPv6到IPv4泄露的存在表明,存在Wallbleed漏洞的DNS注入器在同一内存空间中处理IPv4和IPv6流量。
易受Wallbleed影响的DNS注入器是中国防火长城(GFW)的一部分。这些注入器是否影响了中国的每一处,乃至中国境外的某些地方?有多少IP地址的流量可能经过了存在漏洞的注入器,从而有可能被泄露?为回答这些问题,我们从中国境外进行了覆盖整个IPv4地址空间的扫描。Wallbleed v1和v2都影响了中国各地的IP地址,这与在网络边界部署DNS注入的假设相符。在许多情况下,即使是从美国发往中国境外某地的探测包也会收到Wallbleed注入,这是因为相应的网络路径穿越了边界。
我们使用ZMap[27]从美国的一所大学扫描了公共IPv4地址空间。为了发现受Wallbleed v1影响的IP地址,我们将以下有效载荷发送到UDP端口53:
该有效载荷旨在诱使Wallbleed注入器产生溢出,且仅带来少量(14字节)溢出以确认该漏洞。如脚注3中所述,这个非常短的QNAME无需尾随的00即可生效。我们以250 Mbps的速率发送数据包,扫描耗时三个小时。
我们之所以选择名称4.tt ,是因为它不太可能出现在中国以外国家的DNS封锁列表中。迟至2020年11月,4.tt 仍是一个中文赌博网站。6 (赌博是防火长城封锁的主题之一[28 Art. 15], [9 §4.2]。)该名称已不再解析到IP地址,且至少自2023年7月起便是如此。在扫描中使用一个针对中国且已失效的名称,可以降低触发其他国家DNS注入器的可能性。
为了发现受Wallbleed v2影响的IP地址,我们将以下有效载荷发送到UDP端口53:
如第III-C节所述,te.rs 是Wallbleed v2的最短有效QNAME,其标签长度前缀必须超过解析器中的某个常量阈值。7
局限性。我们仅进行了三次扫描:其中针对Wallbleed v1的扫描在2023年6月25日和2023年8月23日进行,针对Wallbleed v2的扫描在2024年3月6日进行。我们从位于美国的一台主机进行扫描;其他地点通往中国的网络路径不同,可能会得出不同的结果。这项快照研究的结果反映了扫描时的路由模式,我们无法断言它们会随时间如何变化。类似的注入器中间设备——无论是否存在类似Wallbleed的漏洞——可能存在于其他国家,但我们的扫描无法发现它们,因为我们使用的是一个仅在中国被封锁的域名。
除非另有说明,本节的分析基于2023年8月23日的扫描。2023年6月25日针对Wallbleed v1的扫描和2024年3月6日针对Wallbleed v2的扫描,其结果在定性上是相似的。8 该扫描从245.4百万个不同的IP地址引发了248.3百万个响应。2.17百万个IP地址收到了不止一个响应,其中某一例甚至多达20,270个,这可能是路由环路造成的[29, 30]。
我们使用两步过滤来将Wallbleed注入与其他响应区分开。首先,我们筛选出答案部分包含已知为Wallbleed注入器所用的伪造IP地址的响应。具体而言,我们保留了以如下形式的资源记录结尾的响应c00c 0001 0001 TTL 0004 a b c d (类型A),或c00c 001c 0001 TTL 0010 a b c d e f g h (类型AAAA), 其中a.b.c.d 或a:b:c:d:e:f:g:h 是附录A中的IP地址之一。(类型A和类型AAAA响应都有可能出现,尽管探测包并未指定QTYPE。)接下来, 我们筛选出以如下字节模式开头的响应 00008180 0001 0001 0000 0000 01 4 10 t t ;也就是说,这是一个针对探测包QNAME和ID字段的响应,标志位等于 8180,这正是受影响注入器的特征。
过滤后,剩下244,911,941个响应(占所有响应的98.6%),它们来自242,442,549个不同的IP地址,是确定无疑的Wallbleed注入。表V显示了UDP有效载荷长度和DNS答案资源记录类型的分布。
表V:Wallbleed v1扫描中Wallbleed响应的UDP长度和DNS资源记录类型。
| UDP有效载荷长度(字节) | 响应数量 | 类型 |
| 52 | 244,881,083 | A |
| 64 | 30,837 | AAAA |
| 33 | 8 | A |
| 48 | 7 | A |
| 45, 46, 50, 51, 158 | 1 | A |
| 68 | 1 | AAAA |
在几乎所有情况下(99.99%),对我们探测包的响应都是一个52字节的A型(IPv4)响应。考虑到探测包中的标签长度前缀以及注入器答案部分的固定大小,52字节正是预期的长度。在少数情况下,响应是64字节的AAAA型(IPv6)响应。这一现象是有原因的:由于我们的探测包不含QTYPE字段,注入器便从内存中紧邻探测包之后的字节里获取QTYPE。注入器默认生成A型响应,但在特殊情况下,当对应QTYPE的字节值为 001c时,注入器则转而构造一个AAAA型响应。
我们使用IP地理定位和IP到ASN映射,来确定在水平扫描中收到Wallbleed响应的那些IP地址的位置(在过滤掉前一小节所述的非Wallbleed响应之后)。不出所料,据报告几乎所有IP地址都位于中国,且遍布该国的各个地理区域。少数作出响应的IP地址据报告位于中国境外(此处已与多个数据库交叉核对,以降低地理定位出错的可能性)。
我们在国家级的IP2Location LITE DB5数据库[31](2023年6月30日)和CAIDA ASN数据库[32](2023年7月18日)中查找了每一个受Wallbleed响应影响的IP地址。收到Wallbleed响应的2.42亿个IP地址映射到32个国家或地区,分属381个AS、共554个不同的ASN。表VI列出了按响应IP地址数量排名前十的AS,它们全部位于中国。
表VI:受Wallbleed影响IP地址数量最多的自治系统(AS)。根据某地理定位数据库,它们全部位于中国。当一个AS拥有多个ASN时,我们只列出受影响IP地址最多的那个ASN。
| 自治系统名称 | 自治系统编号 | # IP数量 |
| China Telecom | 4134, … | 104.2 M |
| China Unicom Backbone | 4837, … | 54.9 M |
| China Mobile | 9808, … | 23.9 M |
| China TieTong | 9394, … | 12.8 M |
| China Unicom | 4837, … | 12.7 M |
| Alibaba | 37963, … | 7.3 M |
| Tencent | 45090, … | 5.2 M |
| China Networks IX | 4847 | 3.7 M |
| CERNET | 4538 | 3.1 M |
| Oriental Cable Network | 9812 | 1.7 M |
为了获得更精细的粒度,我们抽取了10,000个在国家级地理定位中被定位于中国的IP地址,并在城市级和省级的IP2Location LITE DB5数据库[31](2023年8月24日)中对其进行查找。抽取的这些IP地址覆盖了中国全部22个省、5个自治区和4个直辖市。因此我们推测,易受Wallbleed影响的DNS注入器影响了整个国家,而不仅仅是某些地区。
表VII:从美国进行水平扫描时收到Wallbleed响应的中国境外网络。表中反映了两次扫描,一次在2023年6月25日,另一次在2023年8月23日。表格列出了受影响IP地址数量最多的十个自治系统(AS)。总体而言,6月的扫描中有来自37个国家的104个非中国AS,8月的扫描中有来自31个国家的99个AS。
| 自治系统名称 | 自治系统编号 | 国家代码 |
# 独立IP数量
|
|
| 六月 | 八月 | |||
| Dreamline | 9457 | KR | 1,534 | 1,086 |
| MASTER-7-AS | 26380 | AU | 315 | 489 |
| Anpple Tech | 133847 | MY | 243 | 257 |
| Chinanet Backbone | 4134 | HK | 235 | 248 |
| AZT | 53587 | US | 186 | 168 |
| Network Joint | 133762 | HK | 63 | 61 |
| HK Broadband | 9269 | HK | 50 | 85 |
| STACKS-INC-01 | 398704 | HK | 31 | 78 |
| Viettel Group | 7552 | VN | 31 | 30 |
| Aofei Data | 135391 | HK | 29 | 28 |
在国家级地理定位中,只有110,676个(0.05%)IP地址被映射到中国以外的国家。地址在中国境外受到影响并非没有可能,因为DNS注入已知会影响那些仅仅途经中国中转的网络路径[33]。但由于地理定位数据库可能并不准确[34 §6.2],我们又进行了额外的过滤,以剔除那些不太确定位于中国境外的地址:
该过滤设计得偏于保守,即它倾向于把IP地址判定为位于中国。经过过滤后,还剩下6,822个IP地址。表VII按AS对它们做了汇总,图9展示了它们的地理位置。
图9:从我们位于美国的主机进行扫描时,收到Wallbleed响应的中国境外IP地址的城市级地理定位。
尽管很可能仍残留少量错误的地理定位,但可以明确的是,中国境外的一些流量也可能已暴露于Wallbleed所代表的隐私风险之中。2010年,Sparks等人观察到有109个地区遭受DNS污染,主要原因是GFW在通往TLD服务器的中转路径上进行DNS注入[33 §4.4]。2021年,墨西哥的主机无法访问whatsapp.net ,因为GFW对途经中国、发往根DNS服务器的查询注入了伪造的响应[37, 38]。
我们预计GFW最终会修补Wallbleed漏洞。通过持续监控与全中国范围的扫描,我们记录了2023年9月/10月对Wallbleed v1、以及2024年3月对Wallbleed v2的修补过程。
实验设置。 为进行持续监控,我们从美国以100 pps的速率,向我们在中国控制的一个IP地址发送针对同一QNAME的Wallbleed探测包和普通DNS查询。我们对v1探测包采用4.tt ,对v2探测包采用te.rs 。普通DNS查询作为对照,用于区分漏洞已被修补与注入器离线或QNAME已从封锁列表中移除这几种情况。如果注入器停止响应Wallbleed探测包,却仍不间断地响应普通探测包,这就表明审查者能够以极短的停机时间对GFW进行热修补。反之,如果注入器在一段时间内对两者都停止响应,之后仅恢复对普通探测包的响应,那么我们就能测量出与修补相关的停机时间。我们使用一台位于UMass Amherst的机器,在2023年9月6日至11月7日期间对Wallbleed v1、在2024年3月6日至4月16日期间对Wallbleed v2进行了持续监控。
我们还对中国约一百万个地址的样本进行了扫描。这些扫描旨在检验修补究竟是在不同地区于不同时间发生,还是在全国范围内同时发生。我们从第VI节的IPv4扫描中发现的2.15亿个有响应的IPv4地址里,为每个/24子网选取一个代表,共得到1,130,343个IP地址。我们使用ZMap[27]每15分钟向其中每个IP地址发送一个Wallbleed探测包。我们从科罗拉多大学博尔德分校进行这些扫描,其中Wallbleed v1的扫描时间为2023年9月6日至11月7日,Wallbleed v2的扫描时间为2024年3月28日至4月16日。
实验结果。 图10显示了在ZMap扫描中响应Wallbleed v1探测包的/24子网数量,以及每小时的流失率:即在某一小时有响应、而在下一小时没有响应(或反之)的IP地址数量。我们按小时对有响应的IP地址进行聚合,以减少由丢包导致的假阴性。
图10:我们跟踪了响应Wallbleed v1探测包的IPv4 /24子网数量随时间的变化。我们在2023年9月6日至11月7日期间,每15分钟扫描1,130,343个IP地址(每个子网一个)。我们未能在2023年9月17日至10月4日期间收集到数据。Wallbleed v1的修补分为两个主要阶段:2023年9月6日至14日;以及2023年10月22日至11月1日。
在临近10月23日之前,响应率存在一些波动;从10月23日起,Wallbleed漏洞在约一周的时间内被逐步修补。从10月23日开始,我们观察到随着漏洞被逐步修补,响应率呈离散的阶梯式下降。最后三次下降分别发生在10月30日(星期一)、10月31日(星期二)和11月1日(星期三),且每天都在相同时段:10:00至12:00(中国标准时间,UTC+8)。在11月1日12:00之后,我们所扫描的任何IP地址都不再出现Wallbleed v1响应。我们检查了在10月30日那次下降中转为无响应的IP地址。这3.9万个地址中有86%属于某个不再响应的/20子网,这表明这些离散的台阶对应于大块IP地址成批同步变化,而非某种更为随机的、负载均衡式的更新方式。
Wallbleed v2在2024年3月28日之前已被完全修补。遗憾的是,我们只在四次横向扫描中捕获到修补过程的最后60分钟。与Wallbleed v1类似,Wallbleed v2也是以离散的台阶方式修补的。我们将Wallbleed v2最终修补的时间确定在2024年3月28日16:01:30至16:16:30之间(中国标准时间,UTC+8)(与v1处于一天中的不同时段)。
在最后一小时的捕获中,有42,084个IP地址触发了Wallbleed v2响应。有趣的是,其中33,779个(80.3%)地址属于AS4538(CERNET,中国教育和科研计算机网中心),其余则分布在由中国移动和中国各高校拥有的49个自治系统构成的长尾中。这一观察支持了如下假设:CERNET维护着国家GFW基础设施的一个子集。CERNET中的DNS注入器与GFW的其余部分同样存在Wallbleed v2漏洞,这表明二者受到统一管理并进行了协调一致的修补。同时,其独特的修补时间安排又表明它在运行与维护上具有一定程度的独立性。
防火长城的DNS注入是互联网审查中最古老、也是研究最多的形式之一。我们所知最早的文献记录来自2002年的两项独立研究,一项由Dong完成[39],另一项由Zittrain和Edelman完成[40],两者都发现所有注入响应都使用了同一个伪造的IP地址。2009年,gfwrev发现了中国境内两种具有不同指纹的DNS注入器,并在2002年所用的那个IP地址之外,又记录了另外七个响应IP地址[13]。2014年,Anonymous等人分析了注入响应中的IP ID和TTL模式,推断出存在367个独立的注入进程,每个进程以每秒0到60个伪造DNS响应的速率进行注入[7 §7]。到2016年,在用的伪造IP地址数量已增长到至少174个[41, 42]。Anonymous等人在2020年区分出了至少三种DNS注入器的指纹[8]。Hoang等人在2021年的大规模测量表明,跟踪GFW的DNS域名封锁列表的变化,有助于理解中国的审查趋势[9]。
与我们的工作最为相似、并且确实启发了我们的过往研究,是gfw-looking-glass.sh——这是gfwrev的klzgrad在2010年发布的一个单行shell脚本[5, 6]。据我们所知,这是GFW中第一个内存转储漏洞。当DNS查询的名称在一个2字节压缩指针的第一个字节之后被截断时,GFW的DNS解析器会把附近的内存当作名称的一部分,并将其随注入响应一并泄露回来。在我们发现Wallbleed之前,该漏洞已被修复。该脚本还顺带证明了,一个包含内嵌点字符的查询名称, 06 w u x . r u ,会被当作与被正确拆分为独立标签的名称相同来处理, 03 w u x 02 r u , 这表明GFW在当时同样是先将名称序列化为点分字符串,再与封锁列表进行匹配,而不是按结构化标签进行匹配。2014年,klzgrad发现GFW的DNS注入器已不再解析压缩指针,这为使用以非常规方式运用指针的查询来规避DNS注入创造了机会[43]。
Wallbleed由Sakamoto和Wedwards在2023年独立发现[44]。他们分析了泄露的数据,推断出GFW各进程的特征,并提出了利用该漏洞的几种攻击。除了证实他们的观察结果外,我们还通过自2021年10月以来长达两年多的纵向、全网范围的测量,进一步深化了对Wallbleed的研究。我们揭示了Wallbleed的根本原因,用C代码重建了其解析逻辑,利用一种新颖的旁路信道来识别易受攻击的注入器中的各个进程,检查了受影响的IP地址,并在2023年11月的第一次不完整的修补之后,发现了Wallbleed v2漏洞。
Wallbleed的命名沿袭了其他类似的内存泄露漏洞。Heartbleed是OpenSSL中的一个漏洞,它允许客户端一次泄露多达64 KB的TLS服务器内存[1]。Cloudbleed是2017年出现在Cloudflare内容分发网络边缘服务器所使用的HTML解析器中的一个漏洞[3, 4]。类似地,Ticketbleed记录了F5中间设备中的一个漏洞[2]。
本研究涉及三个主要的伦理考量。第一个是实验数据的处理,例如我们在纵向实验中历时两年收集的数据。如果正如我们所主张的那样,Wallbleed对那些流量流经存在漏洞的注入器的用户构成了隐私风险,那么对泄露数据的存储与分析就需要审慎和小心。第二个是,是否以及在何种情况下,可以利用一个自身可能被视为敌对网络攻击者的系统中的安全漏洞[10, 11]——在本例中即GFW。第三个是如何进行披露。
第V节的实验表明,Wallbleed泄露给第三方的数据中,至少一些源自流经防火墙的流量。这带来了隐私方面的担忧:网络流量可能包含用户名、密码或网页请求等敏感信息。我们已将研究计划提交给所在机构的机构审查委员会(IRB),该委员会认定本研究不涉及人类受试者而予以豁免。下面我们详述针对这些数据所做的考量与所采取的保护措施。
数据收集。在减少数据收集与开展有意义的分析之间,存在着不可避免的权衡。一旦理解了Wallbleed漏洞,泄露单个字节就足以确认其存在,但如此有限的测量无法让我们研究防火墙的架构,也无法了解互联网用户受到了怎样的影响。对内存中(而非已存储)数据进行即时分析虽能让我们报告部分结果,但我们将无法察觉、也无法分析那些未曾预料的变化,例如第IV-B节中“摘要”字节的逐渐消失。因此,我们采取的策略是保护已收集的数据,而不是人为地限制所收集的内容。最终,经过团队内部以及与审稿人的讨论,我们决定在本文发表时删除所收集的数据。
利用此类漏洞在伦理上颇为复杂。从道义论的角度来看[45 §4.1],安全研究人员可能会决定,在任何情况下都不去利用自己无法控制的系统中的漏洞,因为这样做可能带来难以预料的意外负面影响。反之,从结果论的角度来看[45 §4.1],就必须权衡研究的收益与其可能造成危害的风险。
在研究中,我们识别出潜在危害与负面影响的两大来源:(1)我们收集的数据可能包含敏感信息,存在泄露的可能;(2)我们发送的探测包可能导致GFW或其他中间设备、终端主机崩溃或发生故障。第一类风险来源已在第IX-A节中讨论。下面我们讨论如何管理第二类风险来源。
鉴于我们所利用的系统本身被许多人视为危害之源[10, 11],即使我们的实验对GFW造成了损害,其实质也是通过阻碍审查来减少对超过十亿人的危害。尤其是,GFW的任何崩溃都不太可能妨碍网络流量。以往研究表明,GFW的DNS注入器是旁路(on-path)设备[7, 8, 9, 12, 21, 39, 46, 47, 48];也就是说,它们通过获取流量的镜像副本来工作,本身并不是传输链路中的一环。最后,已有先前工作利用僵尸网络、中间设备等其他有害系统中的漏洞,以研究这些问题系统[29, 49, 50, 51, 52]。
为最大限度降低导致其他中间设备和终端主机崩溃的风险,在实验的前18个月里,我们谨慎地只向自己控制的主机发送流量。只有在观察到没有不利影响之后,我们才开始进行全网扫描。遵循互联网扫描的最佳实践[27],我们将发往每个不受我们控制的主机的流量限制为每15分钟仅一个UDP数据包。我们在扫描所用的源IP地址上托管了一个网页,展示项目说明并解释如何选择退出扫描。在研究过程中,我们收到并遵从了一次选择退出请求。
披露此类漏洞同样是复杂的。报告该漏洞,是否等于我们最终在“帮助”GFW?此外还需权衡立即披露与延迟披露:是现在就消除对用户的隐私风险,还是花时间更深入地了解这一审查系统,以期在未来避免更大的风险与危害?
我们决定采取协调披露的策略,但前提是先借助该漏洞带来的机会,尽可能多地了解DNS注入子系统。有两个因素促使我们最终决定披露。其一是对用户隐私的风险:一旦这个尚未修补的漏洞被公开,就可能被其他不顾用户安全的人所利用。其二是,Wallbleed漏洞并不会降低DNS审查系统的有效性;在Wallbleed被修复之后,注入器仍会一如既往地干扰连接,只是不会干扰得更多。
这种伦理权衡是针对具体情况而言的。在其他情形下,我们可能会做出不同的决定。假如GFW中存在某个实现错误,使其未能审查一部分连接,但除此之外并未增加对用户的风险,那么我们就没有义务去报告它。我们所忠于的并非抽象意义上的漏洞修复,而是用户的安全。我们坚持认为,对于像Wallbleed这样的漏洞,唯一正确的修复办法是将受影响的设备(即GFW注入器)从网络中移除:真正的“bug”在于它们的存在本身,而不在于它们无疑存在的那些具体实现错误。 2023年11月那个不完整的修补导致了Wallbleed v2变种,这进一步印证了这一点:只要注入器存在,它们就会对用户构成风险。
最终,还没等我们向CNCERT报告该问题,漏洞就已被修补,我们是否披露的决定也就变得无关紧要了。本文本身同样是我们披露策略的一部分:记录并公开这一漏洞,将促使人们更多地关注审查所带来的诸多危险。
我们的研究提供了一个独特的案例,展示了在保护用户数据与研究数据用于理解某个系统时的效用之间如何取得平衡。事后看来,我们固然能发现一些本可以少收集数据的地方(从而降低收集到个人信息的风险),但我们要指出,事先很难知道非结构化数据的最佳边界所在。例如,我们是通过研究大量完整载荷才了解到4字节的“摘要字节”特性的。事后看来,我们或许只需泄露4字节就能发现GFW的这一特性,因此收集超过4字节的数据似乎并无必要。但若事先并不了解这一特性的性质,就很难判断4字节是否足够。同样,在选择泄露多少字节时,我们面临一个艰难的权衡:要么冒着收集到个人数据的风险泄露更多字节(但有可能借此更多地了解GFW某个尚不为人知的特性),要么冒着了解更少的风险泄露更少字节(但可限制对敏感数据的潜在收集)。这类权衡在所有研究中都应审慎考量,我们希望通过记录我们的思考过程,激发研究界的进一步讨论与辩论。
IRB的决定。有人要求我们对机构审查委员会(IRB)将我们的工作认定为豁免的决定提出异议,因为审稿人认为我们的工作还存在该决定未能涵盖的其他伦理考量。我们同意我们的工作涉及复杂的伦理考量,但不同意这些考量明确落在IRB的职权范围之内,也不同意应当要求作者对自己不认同的IRB决定提出异议。
我们在提交给IRB的研究方案中做到了透明,包括说明所收集的数据可能包含第三方网络流量。以下是我们所提交方案的摘录:
“我们发现,在处理某些格式错误的DNS查询时,该子系统可能会在其注入的响应中夹带不相关的系统内存片段。简而言之,防火墙会‘泄露’小段内存,其中可能偶然包含经过GFW的他人网络流量。这一发现对网络安全和理解防火长城都具有重要意义。虽然被泄露内存的内容不可预测,但其中有可能包含个人身份信息,例如IP地址。因此,我们正在寻求IRB就如何推进给予指导,尤其是这项研究是否需要完整的IRB审查。”
我们认识到,IRB豁免并不等同于IRB对该工作做出了伦理判断,也不必然意味着IRB认为不存在潜在危害或无需伦理考量。相反,豁免只是意味着IRB认定它不落在“人类受试者研究”这一狭义定义之内[53 § 219.102]。为此,我们将所收集的数据视为敏感数据,并在发表前将其删除,以防被潜在滥用。尽管如此,我们认为让我们的社区了解IRB的局限性十分重要,并避免将其当作伦理决策的替代品。
在这项工作中,我们提出并研究了Wallbleed,它是中国防火长城(GFW)的DNS注入子系统中的一个缓冲区过读漏洞。我们进行了纵向和全网范围的测量,以了解Wallbleed的成因和影响。我们还揭示了GFW内部架构和运作的诸多细节,若没有Wallbleed,这些细节将无从知晓。Wallbleed表明,审查中间设备对互联网用户造成的危害甚至超出了审查本身直接(且蓄意设计)的危害:它还可能严重侵犯用户的隐私与保密性。
为了鼓励未来的研究并提倡透明性和可重复性,我们已公开提供代码、匿名化数据,以及关于我们工作及发表过程的更多背景信息。为了提升可访问性,我们同时提供了论文的英文和中文HTML版本。项目主页位于: https://gfw.report/publications/ndss25/en。
我们深深感谢几位希望保持匿名的同事,他们在整个项目中提供了宝贵的贡献和指导。我们还感谢来自gfwrev的klzgrad,感谢其在2010年富有启发性的开创性工作,以及在本次研究中多轮深思熟虑的评论。此外,我们感谢Alberto Dainotti、Ali Zohaib、Cecylia Bocovich、Diogo Barradas、J. Alex Halderman、Jakub Dalek、Jeffrey Knockel、Michael Carl Tschantz、Nadia Heninger、Philipp Winter、ppmaootc、Prateek Mittal、Xiao Qiang和Zakir Durumeric。我们也感谢匿名审稿人提供的有益评论和指导。
以下是受Wallbleed影响的DNS注入器在伪造对A类和AAAA类查询的响应包时分别使用的592个IPv4地址和30个IPv6地址的有序列表,其中查询名称为4.tt。其他注入器和其他查询名称对应的池可能有所不同[8 § 3.2] [9 § 5.2]。当一个注入器进程注入DNS响应包时,它从其有序列表中取出下一个IP地址,到达末尾后再循环回到开头。当以足够高的采样率(大约每秒100个数据包或更多)收集注入的响应包时,这一点便清晰可见。每一轮循环中"第一个"地址的选取是任意的。
我们将这些有序列表用于两个用途:在第VI-C节中,用于从其他响应中筛选出与Wallbleed相关的DNS响应;在第V-B节中,用于分离出每个注入器设备内的多个独立进程。机器可读的版本已包含在与本文一同发布的数据中。
IPv6池中的每个地址都包含模式‘face:b00c’,这是facebook.com真实IP地址的特征。GFW使用互联网公司知名地址的情况在过去已被注意到[8 §3.2],[9 §5.1]。
下面的 C 代码是我们对导致 Wallbleed 的错误 DNS 查询处理算法进行逆向工程的尝试。该代码在所有重要方面复现了受 Wallbleed 影响的 DNS 注入器的行为。如果PATCHED为false,则代码实现的是Wallbleed v1漏洞;如果为true,则实现的是部分修补的Wallbleed v2(参见第III-C节和第VII节)。
在观察到DNS查询并将其复制到内存后,注入设备解析出QNAME以决定查询是否需要被审查,如果需要则准备一个响应。该过程中存在多个漏洞,其中最严重的是未能对DNS名称标签长度进行边界检查,以确保其不超出消息本身的大小。
1// 检查msg是否为应被审查的名称的DNS查询。 2// 如果是,则将msg就地更改为响应并返回响应的长度。 3// 如果不是,则返回0。 4size_t response(unsigned char *msg, size_t msg_len) 5{ 6 if (msg_len < 12 || (msg[2] & 0x80) != 0)如果 msg 太短或不是查询,则不会响应。 7 return 0; 8 9 char qname[126];QNAME 的点分隔、空终止表示将存储在 qname 中。 10 size_t qname_i = 0; 11 // 循环解析QNAME。 12 size_t msg_i = 12;msg_ptr 旨在跟踪 msg_i,并在循环结束时指向 QNAME 之后的位置。 13 unsigned char *msg_ptr; 14 for (;;) { 15 size_t label_len = msg[msg_i++];Bug:没有检查 msg_i 是否在范围内。 16 msg_ptr = &msg[msg_i];将 msg_ptr 与 msg_i 同步。 17 18 if (label_len == 0)退出条件 1:空标签(label)。 19 break; 20 if (msg_i > msg_len)退出条件 2:刚刚解析出的标签长度前缀已超出消息末端的边界。 21 break; 22 23#if !PATCHED 24 if (qname_i + 1 > 124)退出条件 3:没有足够的空间来至少容纳标签的一个字节
(还需留出一个点和一个空终止符)。
向 qname 追加一个点,但第一个标签之前除外。 25 break; 26#else 27 msg_i += MIN(label_len, 124 - qname_i);尽可能多地获取标签内容,留出一个点和一个空终止符的空间。 28 29 if (qname_i + label_len > 124)退出条件 4:标签太长,无法放入 qname。
Bug:在这种情况下,msg_ptr ≠ msg + msg_i。 30 break; 31#endif 32 33 if (qname_i > 0)如果不是第一个标签,则向 qname 追加一个点。 34 qname[qname_i++] = ’.’; 35 size_t n = MIN(label_len, 125 - qname_i);复制标签中尽可能多的内容以放入 qname。
Bug:没有检查 msg_ptr + n 是否在范围内。 36 memcpy(qname + qname_i, msg_ptr, n); 37 qname_i += n; 38#if !PATCHED 39 msg_i += n; 40 if (n < label_len)退出条件 4:标签太长,无法放入 qname。
Bug:在这种情况下,qname_ptr ≠ msg_ptr。 41 break; 42#endif 43 } 44 qname[qname_i] = ’\0’;为点分隔的名称字符串添加空终止符。 45 46 if (!name_matches_blocklist(qname))提取出的 QNAME 字符串是否与封锁列表匹配?
如果不匹配,则不发送响应。 47 return 0; 48 49 // 读取 QTYPE。 50 uint16_t qtype = ntohs(*(uint16_t *) msg_ptr);msg_ptr 可能与此处的 msg_i 不一致。 51#if PATCHED 52 // 读取QCLASS,强制QCLASS == 1。 53 uint16_t qclass = ntohs(*(uint16_t *) (msg_ptr + 2)); 54 if (qclass != 1) 55 return 0; 56#endif 57 // 将查询更改为响应。 58 size_t resp_len = msg_i + 4;为查询的QTYPE和QCLASS添加4个字节。 59 if ((msg[2] & 0x01) == 0)如果查询中未设置RD标志,则在响应中设置AD。 60 *(uint16_t *) &msg[2] = htons(0x8400); 61 else如果查询中设置了RD,则在响应中设置RD和RA。 62 *(uint16_t *) &msg[2] = htons(0x8180); 63 *(uint16_t *) &msg[ 4] = htons(1);QDCOUNT = 1 64 *(uint16_t *) &msg[ 6] = htons(1);ANCOUNT = 1 65 *(uint16_t *) &msg[ 8] = htons(0);NSCOUNT = 0 66 *(uint16_t *) &msg[10] = htons(0);ARCOUNT = 0 67 // 根据 QTYPE 追加一个回答部分。 68 const unsigned char *rdata; 69 uint32_t rdlength; 70 if (qtype == 28) {AAAA类型的查询会得到一个AAAA类型的响应。 71 rdlength = 16; 72 rdata = next_aaaa_address();从循环池中获取下一个IPv6地址。 73 } else {所有其他QTYPE类型都会得到一个A类型的响应。 74 qtype = 1; 75 rdlength = 4; 76 rdata = next_a_address();从循环池中获取下一个IPv4地址。 77 } 78 uint32_t ttl = rand_in_range(64, 254);TTL在64到254之间(包括64和254)随机选择。 79 unsigned char rr[] = {构造一个资源记录。 80 0xc0, 0x0c, // NAME指向QNAME的压缩指针。 81 0, 0, // TYPE占位符 82 0x00, 0x01, // CLASS = IN 83 0, 0, 0, 0, // TTL占位符 84 0, 0, // RDLENGTH占位符 85 }; 86 *(uint16_t *) &rr[ 2] = htons(qtype);设置 QTYPE。 87 *(uint32_t *) &rr[ 6] = htonl(ttl);设置 TTL。 88 *(uint16_t *) &rr[10] = htons(rdlength);设置 RDLENGTH。 89 memcpy(msg + resp_len, rr, sizeof(rr));追加资源记录,直到 RDATA 之前。 90 resp_len += sizeof(rr); 91 memcpy(msg + resp_len, rdata, rdlength);追加 RDATA(伪造的 IP 地址)。 92 resp_len += rdlength; 93 94 return resp_len;这个查询会得到一个注入的响应。 95}
我们并不确切知道数据包的载荷在被观察到后是如何存入内存的。例如,它可能是通过软件复制,或者是网络接口的自动 DMA 传输。无论具体过程如何,DNS 注入器的一些细微行为特征,可以用数据包被复制到内存的方式来解释。这些特征包括:内存缓冲区的第 18 个字节始终为零 脚注 3;曾经有一段时间,泄露的内存中前 4 个字节与其他部分不同(“摘要”字节,第 IV-B 节);以及上述 response 函数中的 msg_len 限制来自 UDP 头部,而不是数据包实际可用的字节数(当这两个值不同时)。
1// 将观察到的UDP数据包载荷复制到内存中进行分析,并可能修改, 2// 如果它是针对封锁列表上某个名称的DNS查询,则注入一个响应。 3// hdr_len是UDP头部中代表载荷长度的值(不包括头本身的长度)。 4 5void udp_packet_callback(const void *data, size_t data_len, size_t hdr_len) 6{ 7 data_len = MIN(data_len, hdr_len);将数据包载荷修剪到头部中指定的大小。 8 void *work = allocate_memory(hdr_len + 28);为查询和响应记录分配内存。 9 memset(work, 0x00, 18);清除缓冲区开头的内容;请参阅脚注3。 10 memcpy(work, data, data_len);将数据包复制到工作内存中。复制使用
data_len,但解析使用 hdr_len。 11 12 if (USE_DIGEST)“Digest”字节(第 IV-B 节),如果存在,则
紧接在查询之后。我们使用的是固定字节模式。 13 memset(work + data_len, ’D’, 4); 14 size_t resp_len = response(work, hdr_len);这个数据包是需要响应的DNS查询吗? 15 if (resp_len > 0)如果是,则注入响应。我们省略了伪造源地址等细节。 16 inject(work, resp_len); 17 free_memory(work); 18}