SEEDLab-测信道攻击与Heartbleed漏洞
SEEDLab-测信道攻击与Heartbleed漏洞
1 侧信道攻击
本实验的第一个任务,是通过使用所提供的漏洞代码
sidechannel.c,设计一个攻击代码,利用侧信道攻击的方式获取密码,并通过shellcode
来获取 root shell。我们首先分析一下目标程序sidechannel.c。 
Step 1:目标程序分析
目标程序sidechannel.c 是一个简单的密码验证程序。程序运行时需要输入两个参数:argv[1] 为用户猜测的密码,argv[2] 为一段将被执行的代码。程序首先检查参数数量以及输入密码长度,如果不符合要求则直接退出。随后程序调用 setresuid(geteuid(), geteuid(), geteuid()),将进程的用户 ID 设置为程序的有效用户 ID。如果程序被设置为 SUID root,则程序运行时将具有 root 权限。 之后程序从文件 s.pass 中读取真实密码,并存入数组 pass 中。接着程序逐字符比较用户输入的密码:如果对应位置字符相同,则变量 correct 加一;如果不相同,则进入循环检查该字符是否出现在密码的其他位置,并统计到变量 misplaced 中。当 correct 等于 19 时,程序会将 argv[2] 强制转换为函数指针并执行。 该程序的关键漏洞在于密码比较逻辑存在明显的时间差。当字符匹配时,程序只进行一次比较操作;当字符不匹配时,程序会进入额外的循环进行多次比较。这使得程序在不同输入情况下执行时间不同,从而产生时间侧信道。攻击者可以通过反复运行程序并测量执行时间,判断某一位置的字符是否正确,从而逐字符恢复完整密码。此外,当 correct == 19 时程序会执行 argv[2] 指向的代码,这意味着攻击者可以将 shellcode 作为第二个参数传入,从而执行任意代码。
Step 2:攻击脚本的编写
为了利用该时间侧信道漏洞,可以编写一个自动化攻击脚本。脚本的基本思路是逐位猜测密码字符。首先定义可能的字符集合(如大小写字母和数字),然后针对每一个位置枚举所有可能字符,并将当前猜测的密码作为参数运行 sidechannel 程序。脚本使用高精度计时函数记录程序执行时间,并对每个候选字符重复执行多次,计算平均运行时间以减少系统噪声带来的影响。由于字符匹配时程序执行路径较短,因此执行时间通常更短。脚本通过比较不同字符对应的平均执行时间,选取时间最短的字符作为该位置的正确字符,并继续猜测下一位,直到恢复完整密码。

Step 3:攻击测试
运行攻击脚本后,程序会逐步输出恢复出的密码字符。经过多轮测试后,可以成功恢复 s.pass 文件中的完整密码。当得到正确密码后,将其与 shellcode 一起作为参数运行目标程序。由于程序在密码完全正确时会执行 argv[2] 指向的代码,shellcode 将被当作函数执行。若 shellcode 的功能为执行 /bin/sh,则可以成功启动一个 shell。由于程序具有 root 权限,最终可以获得 root shell,表明攻击成功。

2 Heartbleed漏洞
2.1 任务1:发起Heartbleed攻击
在本任务中,学生将对我们的社交网络网站发起 Heartbleed 攻击,并观察这种攻击可能造成什么样的损害。Heartbleed 攻击造成的实际损害取决于服务器内存中存储的信息类型。如果服务器上没有太多活动,你可能无法窃取到有用的数据。因此,我们需要以合法用户的身份与Web服务器进行一些交互。以管理员身份完成以下操作:
- 在浏览器中访问 https://www.heartbleedlabelgg.com
- 以网站管理员身份登录(用户名:admin;密码:seedelgg)
- 将 Boby 添加为好友(进入 More → Members,点击 Boby → Add Friend)
- 给 Boby 发送一条私信 当你以合法用户身份完成足够的交互之后,就可以发起攻击,看看能够从受害服务器中获取哪些信息。 从零开始编写Heartbleed攻击程序并不容易,因为它需要对Heartbeat协议的底层实现有深入了解。幸运的是,已经有人编写好了攻击代码。因此,我们将使用现有的代码来获得对 Heartbleed 攻击的第一手实践经验。我们使用的代码叫attack.py,最初由Jared Stafford编写。为了教学目的,我们对代码做了一些小的修改。 需要多运行几次攻击程序,才能获得有用的数据。可以从目标服务器获取以下信息:
- 用户名和密码
- 用户的活动记录(用户做过什么操作)
- 私信的具体内容
如图1所示,在/etc/hosts文件中添加域名解析记录,将目标域名www.heartbleedelgg.com定向到本地。

如图2和图3所示,使用seed系统中的浏览器访问目标域名,使用管理员身份进行登录。添加Boby为好友并且编辑一条短信发送给Boby。
如图4,现在使用attack.py进行攻击,多尝试几次后发现成功打印出了管理员的账号和密码。
如图5,再多尝试几次后成功获得了admin发送给Boby的私信主题及内容。
2.2 任务2:找到Heartbleed漏洞的成因
在本任务中,学生需要比较攻击代码发送的正常数据包(benign packet)和恶意数据包(malicious packet)的结果,从而找出 Heartbleed 漏洞的根本原因。
Heartbleed 攻击基于 Heartbeat 请求(Heartbeat request)。这种请求会向服务器发送一些数据,服务器随后会把这些数据复制到响应包中,因此这些数据会被原样返回。
在正常情况下,假设请求中包含 3 个字节的数据 “ABC”,那么 length 字段的值就是 3。服务器会把这段数据放到内存中,并从数据起始位置复制 3 个字节到响应数据包中。
在攻击场景中,请求中可能仍然只包含 3 个字节的数据,但 length 字段却被设置为 1003。当服务器构造响应数据包时,它会从数据的起始位置(即 “ABC”)开始复制 1003 个字节,而不是 3 个字节。这多出来的 1000 个字节显然不是来自请求数据包,而是来自服务器的私有内存,其中可能包含其他用户的信息、密钥、密码等敏感数据。
在本任务中,我们将通过修改请求中的 length 字段来进行实验。首先,我们根据 Figure 2 来理解 Heartbeat 响应数据包是如何构造的。当 Heartbeat 请求到达服务器时,服务器会解析数据包,获取 payload 和 payload length(如 Figure 2 中高亮部分)。这里,payload 是一个 3 字节字符串 “ABC”,而 payload length 的值为 3。服务器程序会直接相信请求数据包中的这个长度值。随后它构造响应数据包:指向存储字符串 “ABC” 的内存位置,并复制 payload length 字节的数据到响应包的 payload 中。这样,响应数据包中就包含 3 字节字符串 “ABC”。
我们可以像 Figure 3 所示那样发起 Heartbleed 攻击。我们保持相同的 payload(3 字节),但将 payload length 字段设置为 1003。服务器在构造响应数据包时仍然会盲目使用这个 payload length 值。这一次,服务器程序会指向字符串 “ABC”,并从内存中复制 1003 个字节到响应数据包的 payload 中。除了字符串 “ABC” 之外,额外复制的 1000 个字节会被包含在响应包中,这些数据可能是内存中的任何内容,例如用户活动记录、日志信息、密码等。
我们的攻击代码允许你尝试不同的 payload length 值。默认情况下,该值被设置为一个较大的数 0x4000。你可以使用命令选项 “-l”(字母 ell) 或 “–length” 来减小这个值,例如:
1 | |
你的任务是使用不同的 payload length 值运行攻击程序,并回答以下问题: 问题 2.1:随着 length 变量减小,你观察到了什么样的变化? 问题 2.2:随着 length 变量减小,会存在一个边界值(boundary value)。当输入的 length 等于或小于这个边界值时,Heartbeat 查询只会收到一个没有附带额外数据的响应包(也就是说,请求是正常的 benign 请求)。请找出这个边界长度值。你可能需要尝试多个不同的长度值,直到 Web 服务器返回不包含额外数据的响应。 为了帮助你判断,当返回的字节数小于期望长度时,程序会打印以下信息:
1 | |
对于问题2.1,随着 length
变量减小,可以发现返回包中泄露的内存数据越来越少。
对于问题2.2,如图6,经过多次尝试后,发现附带数据包的临界长度是0x16(22),大于该长度均会泄露内存中的内容。而这正好是attack.py攻击程序中构建的请求包中载荷的实际长度。
2.3 任务3:对策与漏洞修复
要修复 Heartbleed 漏洞,最好的方法是将 OpenSSL 库更新到最新版本。这可以使用以下命令实现。需要注意的是,一旦更新,就很难回到易受攻击的版本。因此,请确保在进行更新之前已完成之前的任务。你也可以在更新之前对虚拟机进行快照。
1 | |
问题3.1:在更新 OpenSSL 库后再次尝试攻击。请描述你的观察结果。
图11 更新过后的攻击结果 此时攻击失败,具体表现为: (1)服务器不会返回任何额外的内存数据,仅会正常回应合法的 Heartbeat 请求; (2)无法窃取到用户名、密码、私信内容等任何服务器内存中的敏感信息。
问题3.2:
此任务的目标是找出如何修复源代码中的 Heartbleed 漏洞。以下 C 风格的结构(与源代码不完全相同)是 Heartbeat 请求/响应包的格式。
数据包的第一个字段(1 字节)是类型信息,第二个字段(2 字节)是有效载荷长度,后面跟着实际的有效载荷和填充。有效载荷的大小应与有效载荷长度字段中的值相同,但在攻击场景中,有效载荷长度可以设置为不同的值。
请指出清单 1 中代码的问题,并提供解决该 bug 的方法(即,需要进行哪些修改来修复该 bug)。你不需要重新编译代码;只需在实验报告中描述如何解决该问题即可。此外,请对 Alice、Bob 和 Eva 关于 Heartbleed 漏洞根本原因的讨论进行评论:Alice 认为根本原因是在缓冲区复制过程中缺少边界检查;Bob 认为原因是缺少对用户输入的验证;Eva 认为只需删除数据包中的长度值就能解决所有问题。
payload_length与实际传输的 payload 字节数是否一致,直接信任了攻击者可控的payload_length值,导致内存越界读取,具体代码问题如下: (1)代码第 15 行n2s(p, payload)直接从请求包中读取攻击者设置的payload_length,未做任何校验; (2)代码第 32 行根据该未校验的长度分配响应缓冲区OPENSSL_malloc(1 + 2 + payload + padding); (3)代码第 40 行memcpy(bp, pl, payload)直接按照该未校验的长度,从请求包 payload 的内存地址开始复制字节——若声明的长度远大于实际 payload 的字节数,memcpy会越过 payload 的实际边界,读取服务器内存中后续的敏感数据。
解决方法:核心修复思路是在使用payload_length前,先校验其合法性,确保声明的长度不超过实际接收到的、可访问的 payload 字节数,具体修改步骤如下:
- 计算实际可用的 payload 字节数:服务器接收到的 Heartbeat 请求包中,p指针在读取payload_length后,剩余的可访问字节数为总请求包长度 - 已解析的字节数(类型 1 字节 + 长度 2 字节),记为actual_payload_len;
- 添加长度校验逻辑:若从请求包中读取的payload(声明长度)大于实际可用的actual_payload_len,或payload为负数/过大的非法值,直接拒绝该 Heartbeat 请求,释放内存并返回错误,不执行后续的缓冲区分配和memcpy操作;
- 仅在校验通过后,使用合法的payload值:若校验通过,再继续执行OPENSSL_malloc和memcpy,确保复制的字节数不超过实际 payload 的边界。
评价:Bob 的观点最贴合根本原因:payload_length是攻击者可控的用户输入,服务器未对该输入做任何合法性校验,直接将其作为内存操作的参数,这是安全设计的核心缺陷;Alice 的观点是直接技术成因:漏洞的直接表现是memcpy(bp, pl, payload)的无边界复制,若在复制前检查payload是否超过实际可用的内存边界,就能直接避免越界读取;Eva 的观点无合理性:违背了 Heartbeat 协议的设计逻辑,无法解决问题还会破坏正常功能。Heartbeat 协议的设计本身依赖payload_length字段—— 服务器需要通过该字段知道请求包中 payload 的实际长度,才能正确解析和回显数据。删除该字段后,协议本身无法正常工作,服务器将无法判断 payload 的结束位置,导致合法的 Heartbeat 请求也会失败。