Windows 7 TCP/IP劫持漏洞深度解析

本文详细分析了Windows 7系统中存在的TCP/IP劫持漏洞,探讨了利用IP_ID全局计数器的盲劫持攻击技术,包括攻击原理、实施步骤和实际影响,并提供了PoC工具和测试结果。

Windows 7 TCP/IP劫持

盲TCP/IP劫持在Windows 7上仍然可行……且不仅限于此。尽管2020年1月14日是Windows 7的官方终止支持(EOL)日期,但该版本Windows无疑是最“多汁”的目标之一。根据各种数据,Windows 7约占操作系统(OS)市场的25%份额,并且仍然是全球第二受欢迎的桌面操作系统。

一点历史

在2012年我加入微软担任安全软件工程师之前的几个月,我向他们发送了一份报告,其中包含所有版本Microsoft Windows(包括当时最新的Windows 7)中的一个有趣错误/漏洞。这是TCP/IP栈实现中的一个问题,允许攻击者执行盲TCP/IP劫持攻击。在与MSRC(Microsoft安全响应中心)的讨论中,他们承认该错误存在,但对问题的影响表示怀疑,声称利用“非常困难且非常不可靠”。因此,他们不打算在当前操作系统中解决它。然而,他们会在即将发布的操作系统(Windows 8)中修复它。

我不同意MSRC的评估。2008年,我开发了一个完全可用的PoC,可以自动找到所有必要的原语(客户端端口、SQN和ACK)来执行盲TCP/IP劫持攻击。该工具利用的正是我报告的TCP/IP栈中的相同弱点。尽管如此,微软通知我,如果我分享我的工具(我不想这样做),他们会重新考虑他们的决定。但目前,不会分配CVE,这个问题预计在Windows 8中解决。

在接下来的几个月里,我开始作为FTE(全职员工)为微软工作,并验证了这个问题在Windows 8中已修复。多年来,我完全忘记了它。然而,当我离开微软时,我在我的旧笔记本电脑上做了一些清理,并找到了我的旧工具。我从笔记本电脑上复制了它,并决定在有更多时间时重新审视它。我找到了一些时间,并认为我的工具值得发布和适当的描述。

什么是TCP/IP劫持?

很可能大多数读者都知道这是什么。对于那些不知道的人,我鼓励你阅读如今在互联网上可以找到的许多关于它的优秀文章。

值得一提的是,可能最著名的盲TCP/IP劫持攻击是Kevin Mitnick于1994年圣诞节对San Diego超级计算机中心的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号。发送方将他当前的SEQ放在每个传出TCP包的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头的包:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
[===================================================================]
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和服务器端口,有一个众所周知的方法来测试给定端口是否是正确的客户端端口。为了做到这一点,我们可以从客户端IP:猜测的客户端端口向服务器-IP:服务器端口发送一个设置了SYN标志的TCP包(我们需要能够发送欺骗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是一个相当大的数字(至少几十KB)。正确的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| = 2^31,那么恰好其中一个将是有效的。例如,让ack1=0和ack2=2^31。如果真实ACK在1和2^31之间,那么ack2将是一个可接受的ack。如果真实ACK是0,或在(2^32 – 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,执行扫描(尝试每个SEQ的64K倍数)。然后,如果我们没有找到任何东西,我们可以尝试所有SEQ,如seq = 32K + i64K 对于所有i。然后,所有SEQ如seq=16k + i32k,等等……缩小窗口,同时避免重新测试已经尝试过的SEQ。在典型的“现代”连接上,这种扫描通常用我们的工具花费不到15分钟。有了服务器的SND.NEXT已知,以及一种解决我们对ACK无知的方法,我们可以以“服务器 -> 客户端”的方式劫持连接。这不错,但不是非常有用,我们更希望能够从客户端向服务器发送数据,使客户端执行命令等……为了做到这一点,我们需要找到客户端的SND.NEXT。

这里是Windows 7的一个小的、奇怪的差异。描述的 scenario 完美适用于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。本质上,我们正在用包含我们注入负载的欺骗包淹没服务器,具有正确的SQN和猜测的ACK。这个操作大约需要5分钟并且是有效的。然而,如果由于任何原因我们的负载没有被注入,应该选择更小的TCP窗口大小(例如,0xFF)。

重要说明

这种类型的攻击不限于任何特定OS,而是利用通过将IP_ID实现为全局计数器生成的“隐蔽通道”。简而言之,任何容易受到“空闲扫描”攻击的OS也容易受到老派盲TCP/IP劫持攻击。

  • 我们需要能够发送欺骗IP包来执行这种攻击。
  • 我们的攻击依赖于“扫描”和不断“探测”IP_ID:
    • 受害者和服务器之间的任何延迟都会影响这种逻辑。
    • 如果受害者的机器过载(繁重或慢速流量),它显然会影响攻击。采取适当的受害者网络性能测量可能对于正确调整攻击是必要的。

概念验证

最初,我在2008年实现了lkm的攻击,并针对Windows XP进行了测试。当我在现代系统上运行编译的二进制文件时,一切工作正常。然而,当我获取原始源代码并想在现代Linux环境上重新编译它时,我的工具停止工作(!)。新二进制文件无法找到客户端端口或SQN。然而,旧二进制文件仍然完美工作。这对我来说是一个谜。strace工具的输出给了我一些线索:

旧二进制文件生成的包:

1
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

新二进制文件生成的包:

1
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)的旧日志,整个攻击花了将近一个小时:

1
pi3-darkstar z_new # time ./test -r 192.168.254.20 -s 192.168.254.46 -l 192.168.254.31 -p 21 -P 
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计