Windows 7 TCP/IP劫持漏洞:利用IP_ID全局计数器的盲攻击

本文详细分析了Windows 7中TCP/IP劫持漏洞,利用IP_ID作为全局计数器的特性实现盲攻击,包括发现客户端端口、服务器SQN和客户端SQN的技术细节,并提供了PoC工具和实际测试结果。

Windows 7 TCP/IP劫持

盲TCP/IP劫持在Windows 7上仍然可行……且不仅限于此。尽管2020年1月14日是Windows 7的官方终止支持(EOL)日期,但它仍然是“多汁”的目标之一。根据各种数据,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劫持?

很可能大多数读者都知道这是什么。对于不知道的人,我鼓励您阅读互联网上许多关于此的优秀文章。

值得一提的是, probably the most famous blind TCP/IP hijacking attack was done by Kevin Mitnick against the computers of Tsutomu Shimomura at the San Diego Supercomputer Center on Christmas Day, 1994.

这是一种非常古老的技术,没人期望它在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:猜测的客户端端口发送一个设置了SYN标志的TCP包到服务器-IP:服务器端口(我们需要能够发送欺骗IP包以使此技术工作)。

当攻击者猜对了有效的客户端端口时,服务器向真实客户端(不是攻击者)回复ACK。如果端口不正确,服务器向真实客户端回复SYN+ACK。真实客户端没有启动新连接,因此它向服务器回复RST。

所以,我们要测试猜测的客户端端口是否正确,只需:

  • 发送PING到客户端,记录IP ID
  • 发送我们的欺骗SYN包
  • 重新发送PING到客户端,记录新的IP ID
  • 比较两个IP ID以确定猜测的端口是否正确。

找到服务器的SND.NEXT

这是 essential part,我能做的最好是再次引用phrack文章:

每当主机收到具有良好源/目标端口但 incorrect seq 和/或 ack 的TCP包时,它会发送回一个简单的ACK,其中包含正确的SEQ/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,那么 exactly one of them will be valid。例如,让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正确,至少 one of the two packet will be accepted。序列号空间比客户端端口空间大得多,但两个事实使此扫描更容易:首先,当客户端收到我们的包时,它立即回复。不像客户端端口扫描那样存在客户端和服务器之间的延迟问题。因此,两个IP ID探测之间的时间可以非常短,加速我们的扫描并大大减少客户端在我们探测之间具有IP流量并干扰我们检测的几率。其次,由于接收者的窗口,不需要测试所有可能的序列号。事实上,我们最多只需要做大约(2^32 / 客户端的RCV.WND)次猜测(此事实已在[6]中提及)。当然,我们不知道客户端的RCV.WND。我们可以大胆猜测RCV.WND=64K,执行扫描(尝试每个SEQ的64K倍数)。然后,如果我们没有找到任何东西,我们可以尝试所有SEQ,如seq = 32K + i64K for all i。然后,所有SEQ如seq=16k + i32k,依此类推……缩小窗口,同时避免重新测试已经尝试过的SEQ。在典型的“现代”连接上,此扫描通常需要不到15分钟与我们的工具。有了服务器的SND.NEXT known,和一种绕过我们对ACK的无知的方法,我们可以以“服务器 -> 客户端”的方式劫持连接。这不错,但不是非常有用,我们更希望能够从客户端发送数据到服务器,使客户端执行命令等……为了做到这一点,我们需要找到客户端的SND.NEXT。

这里是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。否则,它意味着……好吧,你猜对了, that (guessed_ACK – SND_NEXT) > 0。使用此知识,我们可以通过执行二进制搜索(稍微修改的,因为序列空间是循环的)在最多32次尝试中找到 exact 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包来执行此攻击。
  • 我们的攻击依赖于“扫描”和 constant “poking” of IP_ID:
    • 受害者和服务器之间的任何延迟都会影响此类逻辑。
    • 受害者的机器过载(繁重或慢速流量)显然会影响攻击。采取适当的受害者网络性能措施可能对于正确调整攻击是必要的。

概念验证

最初,我在2008年实现了lkm的攻击,并针对Windows XP进行了测试。当我在现代系统上运行编译的二进制文件时,一切工作正常。然而,当我获取原始源代码并想在现代Linux环境中重新编译它时,我的工具停止工作(!)。新二进制文件无法找到客户端端口 neither 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 
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计