写点什么

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

作者:qife122
  • 2025-09-02
    中国台湾
  • 本文字数:7486 字

    阅读完需:约 25 分钟

Windows 7 TCP/IP 劫持

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

历史背景

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


我不同意 MSRC 的评估。2008 年,我开发了一个完全可用的概念验证(PoC)工具,能自动发现执行盲 TCP/IP 劫持攻击所需的所有基本要素(客户端端口、序列号和确认号)。该工具利用的正是我报告的 TCP/IP 栈中的相同弱点。微软表示,如果我分享工具(我不愿意),他们会重新考虑决定。但当时不会分配 CVE,此问题预计在 Windows 8 中解决。


随后我作为全职员工加入微软,并验证了此问题在 Windows 8 中已修复。多年来,我完全忘记了此事。然而,离开微软后,我在清理旧笔记本电脑时发现了这个旧工具。我复制了它,并决定在有时间时重新审视。最终我认为这个工具值得发布和详细描述。

什么是 TCP/IP 劫持?

大多数读者可能已知晓此技术。对于不了解的读者,我鼓励阅读互联网上许多相关文章。最著名的盲 TCP/IP 劫持攻击可能是 1994 年圣诞节 Kevin Mitnick 对 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 号。发送方在每个传出 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 是一个相当大的数字(至少几十 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,执行扫描(尝试每个 64K 的倍数 SEQ)。然后,如果我们没找到任何东西,我们可以尝试所有 SEQ,如 seq = 32K + i64K 对于所有 i。然后,所有 SEQ 如 seq=16k + i32k,依此类推...缩小窗口,同时避免重新测试已尝试的 SEQ。在典型的“现代”连接上,此扫描通常用我们的工具在 15 分钟内完成。


有了服务器的 SND.NEXT 已知和绕过我们对 ACK 无知的方法,我们可以以“服务器 -> 客户端”的方式劫持连接。这不错,但不是非常有用,我们更希望能够从客户端向服务器发送数据,使客户端执行命令等...为此,我们需要找到客户端的 SND.NEXT。


这里是 Windows 7 的一个小而奇怪的差异。所述场景完美适用于 Windows XP,但我在 Windows 7 中遇到了不同的行为。有两个边缘情况作为 ACK 值来满足 ACK 公式并没有真正改变任何事情,并且我(仅在 Windows 7 中)通过始终使用一个边缘值作为 ACK 获得了完全相同的结果。最初,我认为我的攻击实现不针对 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 工具的输出给了我一些线索:


旧二进制生成的包:


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.321suser    0m8.955ssys     0m29.181spi3-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 智能小助手)公众号二维码


办公AI智能小助手


用户头像

qife122

关注

还未添加个人签名 2021-05-19 加入

还未添加个人简介

评论

发布
暂无评论
Windows 7 TCP/IP劫持漏洞深度剖析_漏洞利用_qife122_InfoQ写作社区