Windows 7 TCP/IP劫持漏洞深度剖析
Windows 7 TCP/IP劫持
作者:pi3
盲TCP/IP劫持在Windows 7上依然可行……且不仅限于此。尽管2020年1月14日是Windows 7的官方终止支持(EOL)日期,但它仍然是“多汁”的攻击目标之一。根据各种数据,Windows 7在操作系统(OS)市场中占据约25%的份额,仍然是全球第二受欢迎的桌面操作系统。
历史背景
2012年加入微软担任安全软件工程师之前的几个月,我向微软提交了一份报告,其中包含一个影响所有Microsoft Windows版本(包括当时最新的Windows 7)的有趣漏洞。这是TCP/IP栈实现中的一个问题,允许攻击者执行盲TCP/IP劫持攻击。在与MSRC(微软安全响应中心)的讨论中,他们承认该漏洞存在,但对漏洞的影响表示怀疑,声称利用“非常困难且非常不可靠”。因此,他们不打算在当前操作系统中修复它,但会在即将发布的Windows 8中解决。
我不同意MSRC的评估。2008年,我开发了一个完全可用的PoC工具,可以自动查找执行盲TCP/IP劫持攻击所需的所有基本要素(客户端端口、SQN和ACK)。该工具利用的正是我报告的TCP/IP栈中的相同弱点。微软表示,如果我分享我的工具(我不愿意这样做),他们会重新考虑决定。但当时不会分配CVE,此问题预计在Windows 8中解决。
随后几个月,我作为正式员工(FTE)加入微软,并验证了此问题在Windows 8中已修复。多年来,我完全忘记了此事。然而,离开微软后,我在清理旧笔记本电脑时找到了我的旧工具。我复制了它,并决定在有时间时重新审视。我找到了一些时间,认为我的工具值得发布和详细描述。
什么是TCP/IP劫持?
大多数读者可能已经了解这一点。对于不了解的读者,我鼓励您阅读互联网上许多关于此的优秀文章。
值得一提的是,最著名的盲TCP/IP劫持攻击可能是Kevin Mitnick于1994年圣诞节对Tsutomu Shimomura在圣地亚哥超级计算机中心的计算机发起的攻击。
这是一种非常古老的技术,没人预料到它在2021年仍然存在……然而,今天仍然可以在不攻击负责生成初始TCP序列号(ISN)的PRNG的情况下执行TCP/IP会话劫持。
TCP/IP劫持的当前影响
(不)幸的是,它不再像过去那样具有灾难性。主要原因是大多数现代协议都实现了加密。当然,如果攻击者可以劫持任何已建立的TCP/IP会话,那是非常糟糕的。但是,如果上层协议正确实现了加密,攻击者在此方面的能力将受到限制,除非他们能够正确生成加密消息。
也就是说,我们仍然广泛部署未加密流量的协议,例如FTP、SMTP、HTTP、DNS、IMAP等。值得庆幸的是,像Telnet或Rlogin这样的协议(希望?)只能在博物馆中看到。
漏洞在哪里?
TL;DR:在Windows 7的TCP/IP栈实现中,IP_ID是一个全局计数器。
详情
我在2008年开发的工具实现了一种已知攻击,由“lkm”(作者真实昵称是“klm”,此处有拼写错误)在Phrack 64杂志中描述,可以在此处阅读:
这是一篇精彩的文章(研究),我鼓励大家仔细研究所有细节。
早在2007年(和2008年),这种攻击可以成功在许多现代操作系统(当时)上执行,包括Windows 2K/XP或FreeBSD 4。我在波兰的一个本地会议(SysDay 2009)上对此攻击进行了现场演示,针对Windows XP。
在深入如何执行所述攻击之前,有必要刷新TCP如何处理通信的更多细节。引用phrack论文:
连接中涉及的两个主机在连接建立时随机计算一个32位SEQ号。此初始SEQ号称为ISN。然后,每次主机发送带有N字节数据的包时,它将N添加到SEQ号。发送方在每个传出TCP包的SEQ字段中填入其当前SEQ。ACK字段填入来自另一主机的下一个预期SEQ号。每个主机将维护自己的下一个序列号(称为SND.NEXT),以及来自另一主机的下一个预期SEQ号(称为RCV.NEXT)。TCP通过定义“窗口”的概念实现流量控制机制。每个主机有一个TCP窗口大小(动态的,特定于每个TCP连接,并在TCP包中宣布),我们称之为RCV.WND。在任何给定时间,主机将接受序列号在RCV.NXT和(RCV.NXT+RCV.WND-1)之间的字节。此机制确保在任何时间,最多只有RCV.WND字节“在传输中”到主机。
简而言之,为了执行TCP/IP劫持攻击,我们必须知道:
- 客户端IP
- 服务器IP(通常已知)
- 客户端端口
- 服务器端口(通常已知)
- 客户端的序列号
- 服务器的序列号
但这与IP ID有什么关系?
1998年(!),Salvatore Sanfilippo(又名antirez)在Bugtraq邮件列表中发布了一种新的端口扫描技术描述,今天称为“空闲扫描”。原始帖子可以在这里找到:
https://seclists.org/bugtraq/1998/Dec/79
有关空闲扫描的更多信息,您可以在这里阅读:
https://nmap.org/book/idlescan.html
简而言之,如果IP_ID实现为全局计数器(例如在Windows 7中),它只是随每个发送的IP包递增。通过“探测”受害者的IP_ID,我们知道每次“探测”之间发送了多少包。这种“探测”可以通过向受害者发送任何导致回复攻击者的包来执行。“lkm”建议使用ICMP包,但可以是任何带有IP头的包:
[===================================================================]
attacker Host
--[PING]->
<-[PING REPLY, IP_ID=1000]--
... wait a little ...
--[PING]->
<-[PING REPLY, IP_ID=1010]--
<attacker> Uh oh, the Host sent 9 IP packets between my pings.
[===================================================================]
这本质上创建了一种“隐蔽通道”,远程攻击者可以利用它来“发现”执行TCP/IP劫持攻击所需的所有信息。如何?让我们引用原始phrack文章:
发现客户端端口
假设我们已经知道客户端/服务器IP和服务器端口,有一种众所周知的方法来测试给定端口是否是正确的客户端端口。为此,我们可以向server-IP:server-port发送一个设置了SYN标志的TCP包,来自client-IP:guessed-client-port(我们需要能够发送伪造的IP包才能使此技术工作)。
当攻击者猜中有效的客户端端口时,服务器向真实客户端(而非攻击者)回复ACK。如果端口不正确,服务器向真实客户端回复SYN+ACK。真实客户端没有启动新连接,因此它向服务器回复RST。
因此,我们要测试猜测的客户端端口是否正确,只需:
- 向客户端发送PING,记录IP ID
- 发送我们伪造的SYN包
- 重新向客户端发送PING,记录新的IP ID
- 比较两个IP ID以确定猜测的端口是否正确。
查找服务器的SND.NEXT
这是关键部分,我能做的最好是再次引用phrack文章:
每当主机收到具有正确源/目标端口但SEQ和/或ACK不正确的TCP包时,它会发送回一个带有正确SEQ/ACK号的简单ACK。在深入研究此事之前,让我们准确定义什么是正确的seq/ack组合,如RFC793 [2]所定义:正确的SEQ是介于接收包的主机的RCV.NEXT和(RCV.NEXT+RCV.WND-1)之间的SEQ。通常,RCV.WND是一个相当大的数字(至少几万字节)。正确的ACK是对应于接收ACK的主机已发送的序列号的ACK。也就是说,主机收到的包的ACK字段必须小于或等于主机自己的当前SND.SEQ,否则ACK无效(你不能确认从未发送过的数据!)。重要的是要注意序列号空间是“循环的”。例如,接收主机用于检查ACK有效性的条件不是简单的无符号比较“ACK <= 接收者的SND.NEXT”,而是有符号比较“(ACK - 接收者的SND.NEXT)<= 0”。现在,回到我们的原始问题:我们想猜测服务器的SND.NEXT。我们知道,如果我们向客户端发送错误的SEQ或ACK(来自服务器),客户端将发送回ACK,而如果我们猜对了,客户端将不发送任何内容。与客户端端口检测一样,这可以通过IP ID测试。如果我们查看ACK检查公式,我们注意到如果我们随机选择两个ACK值,称它们为ack1和ack2,使得|ack1-ack2| = 231,那么恰好其中一个将是有效的。例如,让ack1=0和ack2=231。如果真实ACK在1和231之间,则ack2将是可接受的ack。如果真实ACK是0,或在(232 – 1)和(2^31 + 1)之间,则ack1将是可接受的。考虑到这一点,我们可以更轻松地扫描序列号空间以找到服务器的SND.NEXT。每次猜测将涉及发送两个包,每个包的SEQ字段设置为猜测的服务器SND.NEXT。第一个包(resp. 第二个包)将其ACK字段设置为ack1(resp. ack2),因此我们确信如果猜测的SND.NEXT正确,至少两个包中的一个将被接受。序列号空间比客户端端口空间大得多,但两个事实使此扫描更容易:首先,当客户端收到我们的包时,它立即回复。不存在客户端和服务器之间的延迟问题,如客户端端口扫描中那样。因此,两个IP ID探测之间的时间可以非常短,加速我们的扫描并大大减少客户端在我们探测之间具有IP流量并干扰我们检测的几率。其次,由于接收者的窗口,不需要测试所有可能的序列号。事实上,我们最多只需要进行约(2^32 / 客户端的RCV.WND)次猜测(此事实已在[6]中提及)。当然,我们不知道客户端的RCV.WND。我们可以大胆猜测RCV.WND=64K,执行扫描(尝试每个64K的倍数的SEQ)。然后,如果我们没有找到任何东西,我们可以尝试所有SEQ,如seq = 32K + i64K(对于所有i)。然后,所有SEQ如seq=16k + i32k,依此类推……缩小窗口,同时避免重新测试已尝试的SEQ。在典型的“现代”连接上,此扫描通常需要不到15分钟与我们的工具。
Windows 7的细微差异
Windows 7有一个小而奇怪的差异。描述的方案完美适用于Windows XP,但我在Windows 7中遇到了不同的行为。使用两个边缘情况作为ACK值来满足ACK公式并没有真正改变任何东西,并且我通过始终使用一个边缘值作为ACK获得了完全相同的结果(仅在Windows 7中)。最初,我认为我的攻击实现不适用于Windows 7。然而,经过一些测试和调整,事实证明并非如此。我不确定为什么或我错过了什么,但最终,您可以发送更少的包(少一半)并加速整体攻击。
查找客户端的SND.NEXT
引用:
我们可以做什么来找到客户端的SND.NEXT?显然,我们不能使用与服务器SND.NEXT相同的方法,因为服务器的OS可能不易受此攻击,此外,服务器上的繁重网络流量将使IP ID分析不可行。然而,我们知道服务器的SND.NEXT。我们还知道客户端的SND.NEXT用于检查客户端传入包的ACK字段。因此,我们可以从服务器向客户端发送包,SEQ字段设置为服务器的SND.NEXT,选择一个ACK,并确定(再次使用IP ID)我们的ACK是否可接受。如果我们检测到我们的ACK可接受,那意味着(guessed_ACK – SND.NEXT)<= 0。否则,它意味着……好吧,你猜对了,(guessed_ACK – SND_NEXT)> 0。利用这些知识,我们可以通过二进制搜索(稍微修改的,因为序列空间是循环的)在最多32次尝试中找到确切的SND_NEXT。现在,最后我们拥有了所有需要的信息,我们可以从客户端或服务器执行会话劫持。
(不)幸的是,Windows 7在这里也不同。这与前一阶段处理ACK正确性的差异有关。无论guessed_ACK值如何((guessed_ACK - SND.NEXT)<= 0或(guessed_ACK - SND_NEXT)> 0),Windows 7都不会向服务器发送任何包。本质上,我们在这里是盲目的,无法进行同样非常有效的“二进制搜索”来找到正确的ACK。然而,我们并没有完全迷失。如果我们有正确的SQN,我们总是可以暴力破解ACK。我们仍然不需要验证每个可能的ACK值,我们仍然可以使用相同的TCP窗口大小技巧。然而,为了更有效且不错过正确的ACK括号,我选择使用窗口大小值为0x3FF。本质上,我们向服务器 flooding 伪造的包,其中包含我们的注入负载,具有正确的SQN和猜测的ACK。此操作大约需要5分钟并且是有效的。但是,如果由于任何原因我们的负载未注入,应选择较小的TCP窗口大小(例如0xFF)。
重要说明
- 这种类型的攻击不限于任何特定OS,而是利用通过将IP_ID实现为全局计数器生成的“隐蔽通道”。简而言之,任何易受“空闲扫描”影响的OS也易受老式盲TCP/IP劫持攻击的影响。
- 我们需要能够发送伪造的IP包来执行此攻击。
- 我们的攻击依赖于“扫描”和持续“探测”IP_ID:
- 受害者和服务器之间的任何延迟都会影响此类逻辑。
- 如果受害者的机器过载(流量繁重或缓慢),它显然会影响攻击。可能需要对受害者的网络性能采取适当措施以正确调整攻击。
概念验证
最初,我在2008年实现了lkm的攻击,并针对Windows XP进行了测试。当我在现代系统上运行编译的二进制文件时,一切正常。然而,当我获取原始源代码并想在现代Linux环境中重新编译时,我的工具停止了工作(!)。新二进制文件无法找到客户端端口和SQN。但是,旧二进制文件仍然完美工作。这对我来说是一个谜。strace工具的输出给了我一些线索:
旧二进制文件生成的包:
sendmsg(4, {msg_name={sa_family=AF_INET, sin_port=htons(21), sin_addr=inet_addr("192.168.1.169")}, msg_namelen=16, msg_iov=[{iov_base="E\0\0(\0\0\0\0@\6\0\0\300\250\1\356\300\250\1\251\277\314\0\25\0\0\0224\0\0VxP\2\26\320\353\234\0\0", iov_len=40}], msg_iovlen=1, msg_control=[{cmsg_len=24, cmsg_level=SOL_IP, cmsg_type=IP_PKTINFO, cmsg_data={ipi_ifindex=0, ipi_spec_dst=inet_addr("0.0.0.0"), ipi_addr=inet_addr("0.0.0.0")}}], msg_controllen=24, msg_flags=0}, 0) = 40
新二进制文件生成的包:
sendmsg(4, {msg_name={sa_family=AF_INET, sin_port=htons(21), sin_addr=inet_addr("192.168.1.169")}, msg_namelen=16, msg_iov=[{iov_base="E\0\0(\0\0\0\0@\6\0\0\300\250\1\356\300\250\1\251\277\314\0\25\0\0\0224\0\0VxP\2\26\320\2563\0\0", iov_len=40}], msg_iovlen=1, msg_control=[{cmsg_len=28, cmsg_level=SOL_IP, cmsg_type=IP_PKTINFO, cmsg_data={ipi_ifindex=0, ipi_spec_dst=inet_addr("0.0.0.0"), ipi_addr=inet_addr("0.0.0.0")}}], msg_controllen=32, msg_flags=0}, 0) = 40
cmsg_len和msg_controllen具有不同的值。但是,我没有修改源代码,这怎么可能?一些GCC/Glibc更改破坏了发送伪造包的功能。我在这里找到了答案:
https://sourceware.org/pipermail/libc-alpha/2016-May/071274.html
我需要重写欺骗功能以使其在现代Linux环境中再次功能正常。然而,为此我需要使用不同的API。我想知道有多少非攻击性工具被此更改破坏。
Windows 7测试
我针对完全更新的Windows 7测试了此工具。令人惊讶的是,重写PoC并不是最困难的任务……设置完全更新的Windows 7要麻烦得多。许多更新会破坏更新通道/服务(!)本身,您需要手动修复它。通常,这意味着手动下载特定的KB并在“安全模式”下安装它。然后它可以“解锁”更新服务,您可以继续您的工作。最终,我花了大约2-3天时间来获得完全更新的Windows 7,它看起来像这样:
- 192.168.1.132 – 攻击者的IP地址
- 192.168.1.238 – 受害者的Windows 7机器IP地址
- 192.168.1.169 – 在Linux上运行的FTP服务器。我测试了在git TOT内核(5.11+)下运行的ProFTPd和vsFTP服务器
此工具不对每个受害者进行适当的“调整”,这可以显著加速攻击。然而,在我的特定情况下,完整攻击(意味着查找客户端端口地址、查找服务器的SQN和查找客户端的SQN)花了大约45分钟。
我找到了攻击Windows XP(约2009年)的旧日志,整个攻击花了将近一小时:
pi3-darkstar z_new # time ./test -r 192.168.254.20 -s 192.168.254.46 -l 192.168.254.31 -p 21 -P 5357 -c 49450 -C “PWD”
…::: -=[ [d]evil_pi3 TCP/IP Blind Spoofer by Adam ‘pi3’ Zabrocki ]=- :::…
[+] Trying to find client port
[+] Found port => 49456!
[+] Veryfing… OK!
[+] Second level of verifcation
[+] Found port => 49456!
[+] Veryfing… OK!
[!!] Port is found (49456)! Let’s go further…
[+] Trying to find server’s window SQN
[+] Found server’s window SQN => 1874825280, with ACK => 758086748 with seq_offset => 65535
[+] Rechecking…
[+] Found server’s window SQN => 1874825280, with ACK => 758086748 with seq_offset => 65535
[!!] SQN => 1874825280, with seq_offset => 65535
[+] Trying to find server’s real SQN
[+] Found server’s real SQN => 1874825279 => seq_offset 32767
[+] Found server’s real SQN => 1874825277 => seq_offset 16383
[+] Found server’s real SQN => 1874825275 => seq_offset 8191
[+] Found server’s real SQN => 1874825273 => seq_offset 4095
[+] Found server’s real SQN => 1874823224 => seq_offset 2047
[+] Found server’s real SQN => 1874822199 => seq_offset 1023
[+] Found server’s real SQN => 1874821686 => seq_offset 511
[+] Found server’s real SQN => 1874821684 => seq_offset 255
[+] Found server’s real SQN => 1874821555 => seq_offset 127
[+] Found server’s real SQN => 1874821553 => seq_offset 63
[+] Found server’s real SQN => 1874821520 => seq_offset 31
[+] Found server’s real SQN => 1874821518 => seq_offset 15
[+] Found server’s real SQN => 1874821509 => seq_offset 7
[+] Found server’s real SQN => 1874821507 => seq_offset 3
[+] Found server’s real SQN => 1874821505 => seq_offset 1
[+] Found server’s real SQN => 1874821505 => seq_offset 1
[+] Rechecking…
[+] Found server’s real SQN => 1874821505 => seq_offset 1
[+] Found server’s real SQN => 1874821505 => seq_offset 1
[!!] Real server’s SQN => 1874821505
[+] Finish! check whether command was injected (should be :))
[!] Next SQN [1874822706]
real 56m38.321s
user 0m8.955s
sys 0m29.181s
pi3-darkstar z_new #
更多说明
- 有时您可以看到工具在尝试查找“服务器的真实SQN”时围绕相同值旋转。如果在括号中的数字旁边看到数字1,请终止攻击,复制计算的SQN(工具围绕其旋转的值)并将其粘贴为SQN起始参数(-M)。它应该修复该边缘情况。
- 有时您可能会遇到通过64KB窗口大小扫描可能“跳过”适当SQN括号的问题。您可能希望减小窗口大小。但是,如果工具完成当前窗口大小的完整SQN范围扫描且未找到正确值,它应自动更改窗口大小。然而,这需要时间。您可能希望以较小的窗口大小开始扫描(但这意味着更长的攻击)。
- 默认情况下,工具向受害者的机器发送ICMP消息以读取IP_ID。但是,我已实现功能,它可以从任何IP包读取该字段。它发送标准SYN包并等待回复以提取IP_ID。请向适当参数(-P)提供适当的TCP端口。
工具可以在这里找到:
http://site.pi3.com.pl/exp/devil_pi3.c
结束语
现代操作系统(如Windows 10)通常将IP_ID实现为“每会话”本地计数器。如果您监视特定会话中的IP_ID,您可以看到它只是随每个发送的包递增。但是,每个会话都有独立的IP_ID基础。
祝黑客愉快,
Adam
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
公众号二维码