描述
近日,研究人员发现了一个Grub2的漏洞,版本1.98(2009年发布)到2.02(2015年发布)均受影响。本地用户能够通过这个漏洞绕过任何形式的认证(明文密码或者哈希密码),使得攻击者进而可以获得电脑的控制权限。而大部分的linux系统都将Grub2作为开机引导程序,包括一些嵌入式系统。因此将有不计其数的设备受到此漏洞的威胁。
如下图所示,我们成功的在Debian 7.5 QEMU下利用这个漏洞获得了Grub rescue shell。
快速判断你的系统是否有该漏洞?
想要快速判断你的系统是否有这个漏洞,只需在grub出现输入用户名的界面时,连续按28次
Backspace(退格键),如果系统重启或者返回rescue shell ,那么你的grub就会受到该漏洞影响。
影响
成功利用此漏洞的攻击者可以得到Grub rescue shell,Grub rescue是一个权限非常高的shell,通过这个漏洞可以做到以下事情:
1.权限提升: 攻击者在没有有效的用户名密码的情况下即可获得grub控制台的所有权限。
2.信息泄露: 攻击者可以通过加载一个定制的内核和initramfs(比如从USB加载),从而获得一个更便利的环境,拷贝窃取整个硬盘的数据或者往系统里安装一个rootkit。
3.拒绝服务: 攻击者能够摧毁任何数据包括grub自身,即使硬盘是加密的数据也可以被覆盖,从而造成DOS(系统无法使用)。
细节
这个漏洞从1.98(2009)版本就存在grub的代码中了,b391bdb2f2c5ccf29da66cecdbfb7566656a704d是这个漏洞的提交编号,问题就存在于grub_password_get()函数中。
其中有两个函数都存在整数下溢问题grub_username_get()和grub_password_get()。它们分别存在于grub-core/normal/auth.c和lib/crypto.c文件中。这两个函数除了对printf()的调用在其他地方都是一样的,就像下面所示的grub_username_get()那样。本文中的POC是通过利用grub_username_get()中的漏洞获取Grub rescue shell的。
以下是grub_username_get()函数中的漏洞细节代码:
grub_username_get()函数代码
这个问题是由于自减变量cur_len没有做范围校验导致的。
利用(POC)
这段代码中,对cur_len变量的使用存在两个问题 off-by-two 和 Out of bounds overwrite (代码中后面两个注释)。前面那个错误注释点,在用于存储用户名的缓冲区中会有两个字节的超长,但是这里没有办法利用,被覆盖的内存是用于填充的。
后面那个错误注释点(// Out of bounds overwrite 这里)比较有意思,因为这句代码允许我们用0×00去覆盖用于存储用户名缓存的内存。这是因为grub_memset()函数会尝试将用户名缓冲区未使用的字节设置成0×00。为了达到这个目的,这段代码会计算出第一个未使用的字节地址和需要被填充为0×00的缓冲区的大小。这两个计算的结果会作为参数传递给grub_memset()函数:
grub_memset (buf + cur_len, 0, buf_size - cur_len);
举个例子,当在用户名中输入“root”时,cur_len的值为5,grub_memset()函数会将用户名缓冲区的第5到1024字节(用户名和密码的缓冲区长度为1024字节)清空(设置为0×00)。这样写代码其健壮性是很好的。例如,如果输入的用户名被存储在干净的1024-byte数组中,就可以直接将整个1024-byte内存与有效的用户名进行比较,而不是去比较两个字符串。这样能够防御一些short of side-channels攻击,比如timing attacks(比如第一次输入username为aaaaaaaa,然后接着又输入bbbb,这样编码就可以避免出现第二次结果为bbbb[0x00]aaa的情况)。
最简单快速的验证这个内存覆盖越界的方法就是不停的按backspace (退格键)让cur_len变量下溢,达到一个非常大的值,这个值马上会被用来计算待清空空间的起始地址。
memset destination address = buf + cur_len
通过这个点,由于用户名缓冲区地址的值超出了32位变量能够存储的范围,第二次缓冲区溢出被触发。因此,我们要通过精心构造第一次下溢与第二次缓冲区溢出来计算grub_memset()函数将要使用的目的地址:
cur_len--; // Integer Underflow grub_memset (buf + cur_len, 0, buf_size - cur_len); // Integer Overflow
下面的这个例子可以帮助你们去理解我们是如何利用这个漏洞的。假设用户名缓冲区的起始地址为0x7f674,然后攻击者按一次退格键(下溢值为0xFFFFFFFF),那么memset就是下面这样的:
grub_memset (0x7f673, 0, 1025);
第一个参数:(buf+cur_len) = (0x7f674+0xFFFFFFFF)=(0x7f674-1) = 0x7f673;第二个参数:用来覆盖内存的常量,这里是0;第三个参数是要覆盖的大小:(buf_size-cur_len)=(1024-(-1))=1025。结果就是,整个用户名的缓冲区空间(1024)外加前面的一个字节全都被0×00覆盖。
按下的退格键的次数,就是用户名缓冲区之前填充的0×00的数量。
现在,我们已经能够覆盖用户名缓冲区的任意数量的字节。接下来需要去找0×00覆盖到并且可以用来实现恶意代码执行的内存地址。 在栈帧中进行寻找,可以很快发现grub_memset()函数的返回地址能够被覆盖到。下面这张图可以清楚的展示出栈的内存布局:
Grub2: 重定向控制流
如图中所示,grub_memset()函数的返回地址与用户名缓冲区之间的距离为16字节。换句话说,如果我们按17次退格键,我们就能够覆盖到返回地址的最高字节。所以,函数返回地址0x07eb53e8会被替换掉,最终会跳转到0x00eb53e8。当grub_memset()执行结束时,控制流会重定向到0x00eb53e8,导致系统重启。同样的,按退格键18,19,20次,都会导致系统重启。
到这里,我们能够重定向控制流了。
我们跳过了对0x00eb53e8,0x000053e8、0x000000e8地址处的代码分析,因为跳到这几个地址中,只能导致系统重启,没有办法控制执行流。
虽然成功的构造攻击跳转到0×0看起来非常困难,但是我们下面将会展示我们最终是如何做到的。
跳转到0×0之后系统能否继续存活?
0×0地址是处理器的IVT(中断向量表)的入口。这里包含了大量的段偏移表的指针。
IVT中断最低地址处的代码
在启动的早期阶段,处理器和执行框架都还不具备所有的功能。下面是能否成功利用的一些关键因素,这在每个系统中可能有所不同:
1.处理器处于“保护模式”,Grub2在最开始的阶段会开启这个模式 2.未启用虚拟内存 3.没有内存保护,内存是可读/可写/可执行的,并且没有NX/DEP保护 4.处理器执行32位指令集,即使在64位架构下 5.处理器会自动处理动态自修改的代码:如果写入影响到一条预取指令,指令队列将会无效。 6.没用栈保护机制(SSP) 7.没有开启地址空间布局随机化(ASLR)
因此,跳转到0×0地址并不会造成系统自身崩溃,但是我们需要控制执行流让代码走到包含 Grub2 Rescue Shell功能的目标函数grub_rescue_run()中。
要跳转到0×0,需要控制哪些东西?
当用户按下[Enter]或者[Esc]时,grub_username_get()函数的主循环将会结束。此时,%ebx寄存器中会保存最后一个按键的值(Enter的ascii码为0xd,Esc的ascii码为0×8)。%esi寄存器会保存cur_len变量的值。
如上图所示,指令指针(EIP)指向0×0地址,%esi寄存器的值为-28(利用程序连按了28次退格键),然后按下[Enter](%ebx=0xd)。
IVT逆向
如果处理器的状态像前面所述的那样(会自动处理动态自修改的代码),IVT中的代码就有像memcpy()一样的功能,会从%esi寄存器指向的地址中拷贝代码到0×0(IVT自身)。因此,IVT是自修改的代码,并且我们能够选择我们想拷贝的代码区块。
下面的步骤展示了代码真正的执行顺序,此时%esi寄存器的值为-28(0xffffffe4):
在第三次循环中,往0×0007处插入了retw指令,此时%esp指向的地址为0xe00c(栈顶返回地址)。
因此,当retw指令执行后,执行流跳到0xe00c。这个地址属于grub_rescue_run()函数:
通过这步利用,GRUB2进到了grub rescue函数中,我们获得了一个高权限的shell。
幸运的是,内存虽然被轻微的修改,但是还是能够使用GRUB的所有功能。IVT的中断向量虽然被修改了,但由于处理器现在处于保护模式,IVT不会再被使用。
近一步深入
虽然我们进到了GRUB2 rescue函数中,但却并没有真正的通过认证。如果要进入“normal”模式(这个模式提供了grub菜单和完整的编辑功能),GRUB会要求你输入正确的用户名密码。我们可以直接输入GRUB2命令,甚至引入一个新的模块来添加一个新的GRUB功能,最终通过往系统里部署恶意软件启动完整的bash shell来获取一个更便利环境。要运行linux的bash,我们可以使用GRUB2的命令,比如linux, initrd或者insmod。
虽然使用GRUB2命令运行linux内核来部署恶意软件是完全可行的,但是我们发现了一个更简单的解决方案,往GRUB2的RAM中写入代码补丁来绕过认证,然后再回到“normal”模式。这个方法的思路是修改用户认证的校验条件,其相关代码在 grub-core/normal/auth.c 文件中的is_authenticated()函数中。
完成这个修改使用了GRUB2 rescue的命令write_word。这样,返回GRUB2的“normal”模式的所有条件都已经达成了。 句话说,我们不需要用户名密码就可以进入GRUB2的“编辑模式”了。
APT攻击如何使用这个0day?
物理接触是APT攻击的一个“高级”特性。APT攻击的一个主要目标就是窃取敏感信息。下面是一个非常简单的例子展示一个APT如何影响系统,持续性的获取用户数据的。以下是目标系统配置的概述:
1.BIOS/UEFI 使用密码进行了保护 2.GRUB2编辑模式使用了密码进行保护 3.扩展启动方式被禁用:CDROM,DVD,USB,Netboot,PXE… 4.用户数据被加密
正如前面提到的,我们的目标是窃取用户数据。由于数据被加密了,我们的策略是先感染系统然后等待用户解密数据(通过登录系统),然后我们直接获取明文信息。
准备环境部署恶意软件
通过我们刚刚对GRUB2漏洞利用的分析与展示,我们可以很容易的修改linux入口去加载一个linux内核来获取root权限的shell。这是一个很老但却仍然有效的欺骗方法,只需要添加init=/bin/bash到linux入口处,我们就能够获取root权限的linux shell,这个环境能够让我们更方便的部署恶意软件。
由于/bin/bash 是第一个启动的进程,syslog监控还没有运行,日志不会记录。因此,这一入侵将不会被常见的linux监控检测到。
部署恶意软件来获得持续性的控制
为了展示通过利用这个Grub2 0day漏洞能够做多少事情,我们开发了一个简单的POC。这个POC可以篡改火狐的链接库,能够创建一个新线程并启动一个反弹shell连接到控制服务器的53号端口上。当然这只是一个简单的例子,在实际场景中的恶意软件获取信息会隐秘很多。
将修改后的链接库上传到virustotal中检测,55款杀毒引擎没有一个能检测出这是一个恶意软件。火狐是一款web浏览器,向HTTP和DNS端口发送请求,所以,我们修改的链接库使用这些端口看起来并不是什么可疑行为。
为了感染系统我们将被修改的libplc4.so放到USB中然后替换掉源文件。我们必须将USB设备挂载到系统上并且赋予可写的权限,正如下图所示:
感染系统
当任意用户运行火狐时,一个反弹shell就会发起连接。此时,所有的用户数据都已经被解密了,这允许我们窃取用户的所有信息。下图展示了用户Bob(被攻击用户)正在使用火狐浏览网页,而Alice(攻击者)则完全获得了Bob的数据。
想要更持续性的控制系统,可以修改一个简单的内核放到未加密的/boot分区中,来提权部署一个更为持久的恶意软件,这样我们就可以为所欲为了。
修复方案
这个漏洞很容易修复,只要防止cur_len溢出就行。目前主流厂商都已经意识到了这个漏洞,因此我们也顺便写了个“紧急补丁”放到GRUB2的git中:
补丁地址: http://hmarco.org/bugs/patches/0001-Fix-CVE-2015-8370-Grub2-user-pass-vulnerability.patch
GRUB 2.02修复命令:
经过我们的深入分析,最终成功利用这个漏洞。 但正如文中所说,成功的利用需要很多条件:BIOS版本、GRUB版本、RAM容量、内存布局能否修改,且每个系统都需要深入的分析去构造特殊的利用。
最后提一下这里我们没有利用的地方,大家可以去发散一下:grub_memset()函数可以被滥用以便设置内存的0×00区块而不跳转到0×0,另外用户名和密码缓冲区也可以被用来存储payloads。
另外,更复杂的攻击方法(需要更大的下溢或者payload),可以被用在键盘仿真设备上,例如Teensy device。我们可以记录键盘按下的攻击序列,然后在目标系统上重现。
比较幸运的是,本文所展示的GRUB2漏洞利用方法不是通用的,但却仍有可能存在其他更多变种的利用在你的环境中生效。
* 参考来源: hmarco.org ,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)